diff --git a/.dockerignore b/.dockerignore index c200f675..bdd388f3 100644 --- a/.dockerignore +++ b/.dockerignore @@ -1,40 +1,3 @@ -.DS_Store -/.git -/.github -/.vscode -.idea -/AdGuardHome -/AdGuardHome.exe -/AdGuardHome.yaml -/AdGuardHome.log -/data -/build -/dist -/client/node_modules -/.gitattributes -/.gitignore -/.goreleaser.yml -/changelog.config.js -/coverage.txt -/Dockerfile -/LICENSE.txt -/Makefile -/querylog.json -/querylog.json.1 -/*.md - -# Test output -dnsfilter/tests/top-1m.csv -dnsfilter/tests/dnsfilter.TestLotsOfRules*.pprof - -# Snapcraft build temporary files -*.snap -launchpad_credentials -snapcraft_login -snapcraft.yaml.bak - -# IntelliJ IDEA project files -*.iml - -# Packr -*-packr.go +# Ignore everything except for explicitly allowed stuff. +* +!dist/docker diff --git a/.githooks/pre-commit b/.githooks/pre-commit deleted file mode 100755 index 278d7d3e..00000000 --- a/.githooks/pre-commit +++ /dev/null @@ -1,13 +0,0 @@ -#!/bin/sh - -set -e -f -u - -if [ "$(git diff --cached --name-only -- '*.js')" ] -then - make js-lint js-test -fi - -if [ "$(git diff --cached --name-only -- '*.go' 'go.mod')" ] -then - make go-lint go-test -fi diff --git a/.github/ISSUE_TEMPLATE/Bug_report.md b/.github/ISSUE_TEMPLATE/Bug_report.md index 6e565fe3..9aaebca4 100644 --- a/.github/ISSUE_TEMPLATE/Bug_report.md +++ b/.github/ISSUE_TEMPLATE/Bug_report.md @@ -1,11 +1,9 @@ --- name: Bug report about: Create a bug report to help us improve AdGuard Home - --- - +Have a question or an idea? Please search it [on our forum](https://github.com/AdguardTeam/AdGuardHome/discussions) to make sure it was not yet asked. If you cannot find what you had in mind, please [submit it here](https://github.com/AdguardTeam/AdGuardHome/discussions/new). ### Prerequisites @@ -17,14 +15,18 @@ Please answer the following questions for yourself before submitting an issue. * ### Issue Details - + * **Version of AdGuard Home server:** - * + * +* **How did you install AdGuard Home:** + * * **How did you setup DNS configuration:** * * **If it's a router or IoT, please write device model:** * +* **CPU architecture:** + * * **Operating system and version:** * diff --git a/.github/ISSUE_TEMPLATE/Feature_request.md b/.github/ISSUE_TEMPLATE/Feature_request.md index 4937f5b6..094531b3 100644 --- a/.github/ISSUE_TEMPLATE/Feature_request.md +++ b/.github/ISSUE_TEMPLATE/Feature_request.md @@ -1,11 +1,9 @@ --- name: Feature request -about: Suggest an idea for AdGuard Home - +about: Suggest a feature request for AdGuard Home --- - +Have a question or an idea? Please search it [on our forum](https://github.com/AdguardTeam/AdGuardHome/discussions) to make sure it was not yet asked. If you cannot find what you had in mind, please [submit it here](https://github.com/AdguardTeam/AdGuardHome/discussions/new). ### Prerequisites diff --git a/.github/stale.yml b/.github/stale.yml index 90e24289..518db8b9 100644 --- a/.github/stale.yml +++ b/.github/stale.yml @@ -1,19 +1,22 @@ # Number of days of inactivity before an issue becomes stale. -'daysUntilStale': 60 +'daysUntilStale': 90 # Number of days of inactivity before a stale issue is closed. -'daysUntilClose': 7 +'daysUntilClose': 15 # Issues with these labels will never be considered stale. 'exemptLabels': - 'bug' - 'enhancement' - 'feature request' - 'localization' +- 'needs investigation' +- 'recurrent' +- 'research' # Label to use when marking an issue as stale. 'staleLabel': 'wontfix' # Comment to post when marking an issue as stale. Set to `false` to disable. 'markComment': > This issue has been automatically marked as stale because it has not had - recent activity. It will be closed if no further activity occurs. Thank you + recent activity. It will be closed if no further activity occurs. Thank you for your contributions. # Comment to post when closing a stale issue. Set to `false` to disable. 'closeComment': false diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index b2851e53..4df5b455 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -2,7 +2,7 @@ 'env': 'GO_VERSION': '1.14' - 'NODE_VERSION': '13' + 'NODE_VERSION': '14' 'on': 'push': @@ -39,7 +39,9 @@ 'with': 'node-version': '${{ env.NODE_VERSION }}' - 'name': 'Set up Go modules cache' - 'uses': 'actions/cache@v2' + # TODO(a.garipov): Update when they fix the macOS issue. The issue is + # most probably https://github.com/actions/cache/issues/527. + 'uses': 'actions/cache@v2.1.3' 'with': 'path': '~/go/pkg/mod' 'key': "${{ runner.os }}-go-${{ hashFiles('go.sum') }}" @@ -48,21 +50,23 @@ 'id': 'npm-cache' 'run': 'echo "::set-output name=dir::$(npm config get cache)"' - 'name': 'Set up npm cache' - 'uses': 'actions/cache@v2' + # TODO(a.garipov): Update when they fix the macOS issue. The issue is + # most probably https://github.com/actions/cache/issues/527. + 'uses': 'actions/cache@v2.1.3' 'with': 'path': '${{ steps.npm-cache.outputs.dir }}' 'key': "${{ runner.os }}-node-${{ hashFiles('client/package-lock.json') }}" 'restore-keys': '${{ runner.os }}-node-' - 'name': 'Run make ci' 'shell': 'bash' - 'run': 'make ci' + 'run': 'make VERBOSE=1 ci' - 'name': 'Upload coverage' 'uses': 'codecov/codecov-action@v1' 'if': "success() && matrix.os == 'ubuntu-latest'" 'with': 'token': '${{ secrets.CODECOV_TOKEN }}' 'file': './coverage.txt' - 'app': + 'build-release': 'runs-on': 'ubuntu-latest' 'needs': 'test' 'steps': @@ -79,7 +83,9 @@ 'with': 'node-version': '${{ env.NODE_VERSION }}' - 'name': 'Set up Go modules cache' - 'uses': 'actions/cache@v2' + # TODO(a.garipov): Update when they fix the macOS issue. The issue is + # most probably https://github.com/actions/cache/issues/527. + 'uses': 'actions/cache@v2.1.3' 'with': 'path': '~/go/pkg/mod' 'key': "${{ runner.os }}-go-${{ hashFiles('go.sum') }}" @@ -87,38 +93,26 @@ - 'name': 'Get npm cache directory' 'id': 'npm-cache' 'run': 'echo "::set-output name=dir::$(npm config get cache)"' - - 'name': 'Set up node_modules cache' - 'uses': 'actions/cache@v2' + - 'name': 'Set up npm cache' + # TODO(a.garipov): Update when they fix the macOS issue. The issue is + # most probably https://github.com/actions/cache/issues/527. + 'uses': 'actions/cache@v2.1.3' 'with': 'path': '${{ steps.npm-cache.outputs.dir }}' 'key': "${{ runner.os }}-node-${{ hashFiles('client/package-lock.json') }}" 'restore-keys': '${{ runner.os }}-node-' - 'name': 'Set up Snapcraft' 'run': 'sudo apt-get -yq --no-install-suggests --no-install-recommends install snapcraft' - - 'name': 'Set up GoReleaser' - 'run': 'curl -sfL https://install.goreleaser.com/github.com/goreleaser/goreleaser.sh | BINDIR="$(go env GOPATH)/bin" sh' - - 'name': 'Run snapshot build' - 'run': 'make release' - - 'docker': - 'runs-on': 'ubuntu-latest' - 'needs': 'test' - 'steps': - - 'name': 'Checkout' - 'uses': 'actions/checkout@v2' - 'with': - 'fetch-depth': 0 - 'name': 'Set up QEMU' 'uses': 'docker/setup-qemu-action@v1' - 'name': 'Set up Docker Buildx' 'uses': 'docker/setup-buildx-action@v1' - - 'name': 'Docker Buildx (build)' - 'run': 'make docker-multi-arch' + - 'name': 'Run snapshot build' + 'run': 'make SIGN=0 VERBOSE=1 build-release build-docker' 'notify': 'needs': - - 'app' - - 'docker' + - 'build-release' # Secrets are not passed to workflows that are triggered by a pull request # from a fork. # @@ -139,7 +133,7 @@ 'uses': '8398a7/action-slack@v3' 'with': 'status': '${{ env.WORKFLOW_CONCLUSION }}' - 'fields': 'repo, message, commit, author, job' + 'fields': 'repo, message, commit, author, workflow' 'env': 'GITHUB_TOKEN': '${{ secrets.GITHUB_TOKEN }}' 'SLACK_WEBHOOK_URL': '${{ secrets.SLACK_WEBHOOK_URL }}' diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index d003dee9..0ed7c02c 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -13,15 +13,15 @@ - 'uses': 'actions/checkout@v2' - 'name': 'run-lint' 'run': > - make go-install-tools go-lint + make go-deps go-tools go-lint 'eslint': 'runs-on': 'ubuntu-latest' 'steps': - 'uses': 'actions/checkout@v2' - 'name': 'Install modules' - 'run': 'npm --prefix client ci' + 'run': 'npm --prefix="./client" ci' - 'name': 'Run ESLint' - 'run': 'npm --prefix client run lint' + 'run': 'npm --prefix="./client" run lint' 'notify': 'needs': - 'go-lint' @@ -46,7 +46,7 @@ 'uses': '8398a7/action-slack@v3' 'with': 'status': '${{ env.WORKFLOW_CONCLUSION }}' - 'fields': 'repo, message, commit, author, job' + 'fields': 'repo, message, commit, author, workflow' 'env': 'GITHUB_TOKEN': '${{ secrets.GITHUB_TOKEN }}' 'SLACK_WEBHOOK_URL': '${{ secrets.SLACK_WEBHOOK_URL }}' diff --git a/.gitignore b/.gitignore index beba3ce1..6cbf3ecc 100644 --- a/.gitignore +++ b/.gitignore @@ -7,9 +7,11 @@ # Only build, run, and test outputs here. Sorted. *-packr.go *.db +*.log *.snap /bin/ /build/ +/build2/ /data/ /dist/ /dnsfilter/tests/dnsfilter.TestLotsOfRules*.pprof @@ -19,4 +21,5 @@ /snapcraft_login AdGuardHome* coverage.txt +leases.db node_modules/ diff --git a/.goreleaser.yml b/.goreleaser.yml deleted file mode 100644 index c533712b..00000000 --- a/.goreleaser.yml +++ /dev/null @@ -1,115 +0,0 @@ -'project_name': 'AdGuardHome' - -'env': -- 'GO111MODULE=on' -- 'GOPROXY=https://goproxy.io' - -'before': - 'hooks': - - 'go mod download' - - 'go generate ./...' - -'builds': -- 'main': './main.go' - 'ldflags': - - '-s -w -X main.version={{.Version}} -X main.channel={{.Env.CHANNEL}} -X main.goarm={{.Env.GOARM}}' - 'env': - - 'CGO_ENABLED=0' - 'goos': - - 'darwin' - - 'linux' - - 'freebsd' - - 'windows' - 'goarch': - - '386' - - 'amd64' - - 'arm' - - 'arm64' - - 'mips' - - 'mipsle' - - 'mips64' - - 'mips64le' - 'goarm': - - '5' - - '6' - - '7' - 'gomips': - - 'softfloat' - 'ignore': - - 'goos': 'freebsd' - 'goarch': 'mips' - - 'goos': 'freebsd' - 'goarch': 'mipsle' - -'archives': -- # Archive name template. - # Defaults: - # - if format is `tar.gz`, `tar.xz`, `gz` or `zip`: - # - `{{ .ProjectName }}_{{ .Version }}_{{ .Os }}_{{ .Arch }}{{ if .Arm }}v{{ .Arm }}{{ end }}{{ if .Mips }}_{{ .Mips }}{{ end }}` - # - if format is `binary`: - # - `{{ .Binary }}_{{ .Version }}_{{ .Os }}_{{ .Arch }}{{ if .Arm }}v{{ .Arm }}{{ end }}{{ if .Mips }}_{{ .Mips }}{{ end }}` - 'name_template': '{{ .ProjectName }}_{{ .Os }}_{{ .Arch }}{{ if .Arm }}v{{ .Arm }}{{ end }}{{ if .Mips }}_{{ .Mips }}{{ end }}' - 'wrap_in_directory': 'AdGuardHome' - 'format_overrides': - - 'goos': 'windows' - 'format': 'zip' - - 'goos': 'darwin' - 'format': 'zip' - 'files': - - 'LICENSE.txt' - - 'README.md' - -'snapcrafts': -- 'name': 'adguard-home' - 'base': 'core20' - 'name_template': '{{ .ProjectName }}_{{ .Arch }}{{ if .Arm }}v{{ .Arm }}{{ end }}' - 'summary': 'Network-wide ads & trackers blocking DNS server' - 'description': | - AdGuard Home is a network-wide software for blocking ads & tracking. After - you set it up, it'll cover ALL your home devices, and you don't need any - client-side software for that. - - It operates as a DNS server that re-routes tracking domains to a "black hole," - thus preventing your devices from connecting to those servers. It's based - on software we use for our public AdGuard DNS servers -- both share a lot - of common code. - 'grade': 'stable' - 'confinement': 'strict' - 'publish': false - 'license': 'GPL-3.0' - 'extra_files': - - 'source': 'scripts/snap/local/adguard-home-web.sh' - 'destination': 'adguard-home-web.sh' - 'mode': 0755 - - 'source': 'scripts/snap/gui/adguard-home-web.desktop' - 'destination': 'meta/gui/adguard-home-web.desktop' - 'mode': 0644 - - 'source': 'scripts/snap/gui/adguard-home-web.png' - 'destination': 'meta/gui/adguard-home-web.png' - 'mode': 0644 - 'apps': - 'adguard-home': - 'command': 'AdGuardHome -w $SNAP_DATA --no-check-update' - 'plugs': - # Add the "netrwork-bind" plug to bind to interfaces. - - 'network-bind' - # Add the "netrwork-observe" plug to be able to bind to ports below 1024 - # (cap_net_bind_service) and also to bind to a particular interface using - # SO_BINDTODEVICE (cap_net_raw). - - 'network-observe' - 'daemon': 'simple' - 'adguard-home-web': - 'command': 'adguard-home-web.sh' - 'plugs': - - 'desktop' - -'checksum': - 'name_template': 'checksums.txt' - -'snapshot': - # TODO(a.garipov): A temporary solution to trim the prerelease versions. - # A real solution would consist of making a better versioning scheme that also - # doesn't break SemVer or Snapcraft. - # - # See https://github.com/AdguardTeam/AdGuardHome/issues/2412. - 'name_template': '{{ slice .Tag 0 8 }}-SNAPSHOT-{{ .ShortCommit }}' diff --git a/AGHTechDoc.md b/AGHTechDoc.md index b91b8586..ed7f48ba 100644 --- a/AGHTechDoc.md +++ b/AGHTechDoc.md @@ -1846,7 +1846,7 @@ Response: } There are also deprecated properties `filter_id` and `rule` on the top level of -the response object. Their usaga should be replaced with +the response object. Their usage should be replaced with `rules[*].filter_list_id` and `rules[*].text` correspondingly. See the _OpenAPI_ documentation and the `./openapi/CHANGELOG.md` file. diff --git a/CHANGELOG.md b/CHANGELOG.md index e63c913c..cd5293c1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,11 +10,84 @@ and this project adheres to ## [Unreleased] + + +### Fixed + +- Incomplete OpenWRT detection ([#2757]). +- DHCP lease's `expired` field incorrect time format ([#2692]). +- Incomplete DNS upstreams validation ([#2674]). +- Wrong parsing of DHCP options of the `ip` type ([#2688]). + +[#2674]: https://github.com/AdguardTeam/AdGuardHome/issues/2674 +[#2688]: https://github.com/AdguardTeam/AdGuardHome/issues/2688 +[#2692]: https://github.com/AdguardTeam/AdGuardHome/issues/2692 +[#2757]: https://github.com/AdguardTeam/AdGuardHome/issues/2757 + +### Security + +- Session token doesn't contain user's information anymore ([#2470]). + +[#2470]: https://github.com/AdguardTeam/AdGuardHome/issues/2470 + + + +## [v0.105.1] - 2021-02-15 + +### Changed + +- Increased HTTP API timeouts ([#2671], [#2682]). +- "Permission denied" errors when checking if the machine has a static IP no + longer prevent the DHCP server from starting ([#2667]). +- The server name sent by clients of TLS APIs is not only checked when + `strict_sni_check` is enabled ([#2664]). +- HTTP API request body size limit for the `POST /control/access/set` and `POST + /control/filtering/set_rules` HTTP APIs is increased ([#2666], [#2675]). + +### Fixed + +- Error when enabling the DHCP server when AdGuard Home couldn't determine if + the machine has a static IP. +- Optical issue on custom rules ([#2641]). +- Occasional crashes during startup. +- The field `"range_start"` in the `GET /control/dhcp/status` HTTP API response + is now correctly named again ([#2678]). +- DHCPv6 server's `ra_slaac_only` and `ra_allow_slaac` settings aren't reset to + `false` on update any more ([#2653]). +- The `Vary` header is now added along with `Access-Control-Allow-Origin` to + prevent cache-related and other issues in browsers ([#2658]). +- The request body size limit is now set for HTTPS requests as well. +- Incorrect version tag in the Docker release ([#2663]). +- DNSCrypt queries weren't marked as such in logs ([#2662]). + +[#2641]: https://github.com/AdguardTeam/AdGuardHome/issues/2641 +[#2653]: https://github.com/AdguardTeam/AdGuardHome/issues/2653 +[#2658]: https://github.com/AdguardTeam/AdGuardHome/issues/2658 +[#2662]: https://github.com/AdguardTeam/AdGuardHome/issues/2662 +[#2663]: https://github.com/AdguardTeam/AdGuardHome/issues/2663 +[#2664]: https://github.com/AdguardTeam/AdGuardHome/issues/2664 +[#2666]: https://github.com/AdguardTeam/AdGuardHome/issues/2666 +[#2667]: https://github.com/AdguardTeam/AdGuardHome/issues/2667 +[#2671]: https://github.com/AdguardTeam/AdGuardHome/issues/2671 +[#2675]: https://github.com/AdguardTeam/AdGuardHome/issues/2675 +[#2678]: https://github.com/AdguardTeam/AdGuardHome/issues/2678 +[#2682]: https://github.com/AdguardTeam/AdGuardHome/issues/2682 + + + +## [v0.105.0] - 2021-02-10 + ### Added +- Added more services to the "Blocked services" list ([#2224], [#2401]). +- `ipset` subdomain matching, just like `dnsmasq` does ([#2179]). +- Client ID support for DNS-over-HTTPS, DNS-over-QUIC, and DNS-over-TLS + ([#1383]). - `$dnsrewrite` modifier for filters ([#2102]). - The host checking API and the query logs API can now return multiple matched rules ([#2102]). @@ -26,15 +99,13 @@ and this project adheres to - `$dnstype` modifier for filters ([#2337]). - HTTP API request body size limit ([#2305]). -[#1361]: https://github.com/AdguardTeam/AdGuardHome/issues/1361 -[#2102]: https://github.com/AdguardTeam/AdGuardHome/issues/2102 -[#2302]: https://github.com/AdguardTeam/AdGuardHome/issues/2302 -[#2304]: https://github.com/AdguardTeam/AdGuardHome/issues/2304 -[#2305]: https://github.com/AdguardTeam/AdGuardHome/issues/2305 -[#2337]: https://github.com/AdguardTeam/AdGuardHome/issues/2337 - ### Changed +- `Access-Control-Allow-Origin` is now only set to the same origin as the + domain, but with an HTTP scheme as opposed to `*` ([#2484]). +- `workDir` now supports symlinks. +- Stopped mounting together the directories `/opt/adguardhome/conf` and + `/opt/adguardhome/work` in our Docker images ([#2589]). - When `dns.bogus_nxdomain` option is used, the server will now transform responses if there is at least one bogus address instead of all of them ([#2394]). The new behavior is the same as in `dnsmasq`. @@ -44,20 +115,20 @@ and this project adheres to improve error response ([#2358]). - Improved HTTP requests handling and timeouts ([#2343]). - Our snap package now uses the `core20` image as its base ([#2306]). -- Various internal improvements ([#2267], [#2271], [#2297]). +- New build system and various internal improvements ([#2271], [#2276], [#2297], + [#2509], [#2552], [#2639], [#2646]). -[#2231]: https://github.com/AdguardTeam/AdGuardHome/issues/2231 -[#2267]: https://github.com/AdguardTeam/AdGuardHome/issues/2267 -[#2271]: https://github.com/AdguardTeam/AdGuardHome/issues/2271 -[#2297]: https://github.com/AdguardTeam/AdGuardHome/issues/2297 -[#2306]: https://github.com/AdguardTeam/AdGuardHome/issues/2306 -[#2343]: https://github.com/AdguardTeam/AdGuardHome/issues/2343 -[#2358]: https://github.com/AdguardTeam/AdGuardHome/issues/2358 -[#2391]: https://github.com/AdguardTeam/AdGuardHome/issues/2391 -[#2394]: https://github.com/AdguardTeam/AdGuardHome/issues/2394 +### Deprecated + +- Go 1.14 support. v0.106.0 will require at least Go 1.15 to build. +- The `darwin/386` port. It will be removed in v0.106.0. +- The `"rule"` and `"filter_id"` fields in `GET /filtering/check_host` and + `GET /querylog` responses. They will be removed in v0.106.0 ([#2102]). ### Fixed +- Autoupdate bug in the Darwin (macOS) version ([#2630]). +- Unnecessary conversions from `string` to `net.IP`, and vice versa ([#2508]). - Inability to set DNS cache TTL limits ([#2459]). - Possible freezes on slower machines ([#2225]). - A mitigation against records being shown in the wrong order on the query log @@ -66,16 +137,48 @@ and this project adheres to - Incorrect detection of the IPv6 address of an interface as well as another infinite loop in the `/dhcp/find_active_dhcp` HTTP API ([#2355]). -[#2225]: https://github.com/AdguardTeam/AdGuardHome/issues/2225 -[#2293]: https://github.com/AdguardTeam/AdGuardHome/issues/2293 -[#2345]: https://github.com/AdguardTeam/AdGuardHome/issues/2345 -[#2355]: https://github.com/AdguardTeam/AdGuardHome/issues/2355 -[#2459]: https://github.com/AdguardTeam/AdGuardHome/issues/2459 - ### Removed +- The undocumented ability to use hostnames as any of `bind_host` values in + configuration. Documentation requires them to be valid IP addresses, and now + the implementation makes sure that that is the case ([#2508]). +- `Dockerfile` ([#2276]). Replaced with the script + `scripts/make/build-docker.sh` which uses `scripts/make/Dockerfile`. - Support for pre-v0.99.3 format of query logs ([#2102]). +[#1361]: https://github.com/AdguardTeam/AdGuardHome/issues/1361 +[#1383]: https://github.com/AdguardTeam/AdGuardHome/issues/1383 +[#2102]: https://github.com/AdguardTeam/AdGuardHome/issues/2102 +[#2179]: https://github.com/AdguardTeam/AdGuardHome/issues/2179 +[#2224]: https://github.com/AdguardTeam/AdGuardHome/issues/2224 +[#2225]: https://github.com/AdguardTeam/AdGuardHome/issues/2225 +[#2231]: https://github.com/AdguardTeam/AdGuardHome/issues/2231 +[#2271]: https://github.com/AdguardTeam/AdGuardHome/issues/2271 +[#2276]: https://github.com/AdguardTeam/AdGuardHome/issues/2276 +[#2293]: https://github.com/AdguardTeam/AdGuardHome/issues/2293 +[#2297]: https://github.com/AdguardTeam/AdGuardHome/issues/2297 +[#2302]: https://github.com/AdguardTeam/AdGuardHome/issues/2302 +[#2304]: https://github.com/AdguardTeam/AdGuardHome/issues/2304 +[#2305]: https://github.com/AdguardTeam/AdGuardHome/issues/2305 +[#2306]: https://github.com/AdguardTeam/AdGuardHome/issues/2306 +[#2337]: https://github.com/AdguardTeam/AdGuardHome/issues/2337 +[#2343]: https://github.com/AdguardTeam/AdGuardHome/issues/2343 +[#2345]: https://github.com/AdguardTeam/AdGuardHome/issues/2345 +[#2355]: https://github.com/AdguardTeam/AdGuardHome/issues/2355 +[#2358]: https://github.com/AdguardTeam/AdGuardHome/issues/2358 +[#2391]: https://github.com/AdguardTeam/AdGuardHome/issues/2391 +[#2394]: https://github.com/AdguardTeam/AdGuardHome/issues/2394 +[#2401]: https://github.com/AdguardTeam/AdGuardHome/issues/2401 +[#2459]: https://github.com/AdguardTeam/AdGuardHome/issues/2459 +[#2484]: https://github.com/AdguardTeam/AdGuardHome/issues/2484 +[#2508]: https://github.com/AdguardTeam/AdGuardHome/issues/2508 +[#2509]: https://github.com/AdguardTeam/AdGuardHome/issues/2509 +[#2552]: https://github.com/AdguardTeam/AdGuardHome/issues/2552 +[#2589]: https://github.com/AdguardTeam/AdGuardHome/issues/2589 +[#2630]: https://github.com/AdguardTeam/AdGuardHome/issues/2630 +[#2639]: https://github.com/AdguardTeam/AdGuardHome/issues/2639 +[#2646]: https://github.com/AdguardTeam/AdGuardHome/issues/2646 + ## [v0.104.3] - 2020-11-19 ### Fixed @@ -101,7 +204,7 @@ and this project adheres to - Query logs from file not loading after the ones buffered in memory ([#2325]). - Unnecessary errors in query logs when switching between log files ([#2324]). -- `404 Not Found` errors on the DHCP settings page on *Windows*. The page now +- `404 Not Found` errors on the DHCP settings page on Windows. The page now correctly shows that DHCP is not currently available on that OS ([#2295]). - Infinite loop in `/dhcp/find_active_dhcp` ([#2301]). @@ -115,9 +218,12 @@ and this project adheres to -[Unreleased]: https://github.com/AdguardTeam/AdGuardHome/compare/v0.104.3...HEAD + +[Unreleased]: https://github.com/AdguardTeam/AdGuardHome/compare/v0.105.1...HEAD +[v0.105.1]: https://github.com/AdguardTeam/AdGuardHome/compare/v0.105.0...v0.105.1 +[v0.105.0]: https://github.com/AdguardTeam/AdGuardHome/compare/v0.104.3...v0.105.0 [v0.104.3]: https://github.com/AdguardTeam/AdGuardHome/compare/v0.104.2...v0.104.3 [v0.104.2]: https://github.com/AdguardTeam/AdGuardHome/compare/v0.104.1...v0.104.2 diff --git a/Dockerfile b/Dockerfile deleted file mode 100644 index b0c54832..00000000 --- a/Dockerfile +++ /dev/null @@ -1,77 +0,0 @@ -FROM --platform=${BUILDPLATFORM:-linux/amd64} tonistiigi/xx:golang AS xgo -FROM --platform=${BUILDPLATFORM:-linux/amd64} golang:1.14-alpine as builder - -ARG BUILD_DATE -ARG VCS_REF -ARG VERSION=dev -ARG CHANNEL=release - -ENV CGO_ENABLED 0 -ENV GO111MODULE on -ENV GOPROXY https://goproxy.io - -COPY --from=xgo / / -RUN go env - -RUN apk --update --no-cache add \ - build-base \ - gcc \ - git \ - npm \ - && rm -rf /tmp/* /var/cache/apk/* - -WORKDIR /app - -COPY . ./ - -# Prepare the client code -RUN npm --prefix client ci && npm --prefix client run build-prod - -# Download go dependencies -RUN go mod download -RUN go generate ./... - -# It's important to place TARGET* arguments here to avoid running npm and go mod download for every platform -ARG TARGETPLATFORM -ARG TARGETOS -ARG TARGETARCH -RUN go build -ldflags="-s -w -X main.version=${VERSION} -X main.channel=${CHANNEL} -X main.goarm=${GOARM}" - -FROM --platform=${TARGETPLATFORM:-linux/amd64} alpine:latest - -ARG BUILD_DATE -ARG VCS_REF -ARG VERSION -ARG CHANNEL - -LABEL maintainer="AdGuard Team " \ - org.opencontainers.image.created=$BUILD_DATE \ - org.opencontainers.image.url="https://adguard.com/adguard-home.html" \ - org.opencontainers.image.source="https://github.com/AdguardTeam/AdGuardHome" \ - org.opencontainers.image.version=$VERSION \ - org.opencontainers.image.revision=$VCS_REF \ - org.opencontainers.image.vendor="AdGuard" \ - org.opencontainers.image.title="AdGuard Home" \ - org.opencontainers.image.description="Network-wide ads & trackers blocking DNS server" \ - org.opencontainers.image.licenses="GPL-3.0" - -RUN apk --update --no-cache add \ - ca-certificates \ - libcap \ - libressl \ - && rm -rf /tmp/* /var/cache/apk/* - -COPY --from=builder --chown=nobody:nogroup /app/AdGuardHome /opt/adguardhome/AdGuardHome -COPY --from=builder --chown=nobody:nogroup /usr/local/go/lib/time/zoneinfo.zip /usr/local/go/lib/time/zoneinfo.zip - -RUN /opt/adguardhome/AdGuardHome --version \ - && mkdir -p /opt/adguardhome/conf /opt/adguardhome/work \ - && chown -R nobody: /opt/adguardhome \ - && setcap 'cap_net_bind_service=+eip' /opt/adguardhome/AdGuardHome - -EXPOSE 53/tcp 53/udp 67/udp 68/udp 80/tcp 443/tcp 853/tcp 3000/tcp -WORKDIR /opt/adguardhome/work -VOLUME ["/opt/adguardhome/conf", "/opt/adguardhome/work"] - -ENTRYPOINT ["/opt/adguardhome/AdGuardHome"] -CMD ["-h", "0.0.0.0", "-c", "/opt/adguardhome/conf/AdGuardHome.yaml", "-w", "/opt/adguardhome/work", "--no-check-update"] diff --git a/HACKING.md b/HACKING.md index a16d6496..f3c42eb8 100644 --- a/HACKING.md +++ b/HACKING.md @@ -1,17 +1,33 @@ - # *AdGuardHome* Developer Guidelines + # AdGuard Home Developer Guidelines -As of **December 2020**, this document is partially a work-in-progress, but +As of **February 2021**, this document is partially a work-in-progress, but should still be followed. Some of the rules aren't enforced as thoroughly or remain broken in old code, but this is still the place to find out about what we **want** our code to look like. The rules are mostly sorted in the alphabetical order. -## *Git* +## Contents + + * [Git](#git) + * [Go](#go) + * [Code And Naming](#code-and-naming) + * [Commenting](#commenting) + * [Formatting](#formatting) + * [Recommended Reading](#recommended-reading) + * [Markdown](#markdown) + * [Shell Scripting](#shell-scripting) + * [Text, Including Comments](#text-including-comments) + * [YAML](#yaml) + + + +## Git * Call your branches either `NNNN-fix-foo` (where `NNNN` is the ID of the - *GitHub* issue you worked on in this branch) or just `fix-foo` if there was - no *GitHub* issue. + GitHub issue you worked on in this branch) or just `fix-foo` if there was no + GitHub issue. * Follow the commit message header format: @@ -19,8 +35,9 @@ The rules are mostly sorted in the alphabetical order. pkg: fix the network error logging issue ``` - Where `pkg` is the package where most changes took place. If there are - several such packages, or the change is top-level only, write `all`. + Where `pkg` is the directory or Go package (without the `internal/` part) + where most changes took place. If there are several such packages, or the + change is top-level only, write `all`. * Keep your commit messages, including headers, to eighty (**80**) columns. @@ -30,14 +47,14 @@ The rules are mostly sorted in the alphabetical order. The only exceptions are direct mentions of identifiers from the source code and filenames like `HACKING.md`. -## *Go* +## Go > Not Golang, not GO, not GOLANG, not GoLang. It is Go in natural language, > golang for others. — [@rakyll](https://twitter.com/rakyll/status/1229850223184269312) - ### Code And Naming + ### Code And Naming * Avoid `goto`. @@ -45,6 +62,14 @@ The rules are mostly sorted in the alphabetical order. * Avoid `new`, especially with structs. + * Check against empty strings like this: + + ```go + if s == "" { + // … + } + ``` + * Constructors should validate their arguments and return meaningful errors. As a corollary, avoid lazy initialization. @@ -53,9 +78,12 @@ The rules are mostly sorted in the alphabetical order. * Don't use underscores in file and package names, unless they're build tags or for tests. This is to prevent accidental build errors with weird tags. - * Don't write code with more than four (**4**) levels of indentation. Just - like [Linus said], plus an additional level for an occasional error check or - struct initialization. + * Don't write non-test code with more than four (**4**) levels of indentation. + Just like [Linus said], plus an additional level for an occasional error + check or struct initialization. + + The exception proving the rule is the table-driven test code, where an + additional level of indentation is allowed. * Eschew external dependencies, including transitive, unless absolutely necessary. @@ -70,6 +98,14 @@ The rules are mostly sorted in the alphabetical order. func TestType_Method_suffix(t *testing.T) { /* … */ } ``` + * Name parameters in interface definitions: + + ```go + type Frobulator interface { + Frobulate(f Foo, b Bar) (r Result, err error) + } + ``` + * Name the deferred errors (e.g. when closing something) `cerr`. * No shadowing, since it can often lead to subtle bugs, especially with @@ -78,6 +114,9 @@ The rules are mostly sorted in the alphabetical order. * Prefer constants to variables where possible. Reduce global variables. Use [constant errors] instead of `errors.New`. + * Program code lines should not be longer than one hundred (**100**) columns. + For comments, see the text section below. + * Unused arguments in anonymous functions must be called `_`: ```go @@ -93,12 +132,9 @@ The rules are mostly sorted in the alphabetical order. * Write logs and error messages in lowercase only to make it easier to `grep` logs and error messages without using the `-i` flag. -[constant errors]: https://dave.cheney.net/2016/04/07/constant-errors -[Linus said]: https://www.kernel.org/doc/html/v4.17/process/coding-style.html#indentation + ### Commenting - ### Commenting - - * See also the *Text, Including Comments* section below. + * See also the “[Text, Including Comments]” section below. * Document everything, including unexported top-level identifiers, to build a habit of writing documentation. @@ -127,7 +163,7 @@ The rules are mostly sorted in the alphabetical order. } ``` - ### Formatting + ### Formatting * Add an empty line before `break`, `continue`, `fallthrough`, and `return`, unless it's the only statement in that block. @@ -149,7 +185,7 @@ The rules are mostly sorted in the alphabetical order. }} ``` - ### Recommended Reading + ### Recommended Reading * . @@ -157,13 +193,24 @@ The rules are mostly sorted in the alphabetical order. * -## *Markdown* +[constant errors]: https://dave.cheney.net/2016/04/07/constant-errors +[Linus said]: https://www.kernel.org/doc/html/v4.17/process/coding-style.html#indentation +[Text, Including Comments]: #text-including-comments - * **TODO(a.garipov):** Define our *Markdown* conventions. +## Markdown -## Shell Scripting + * **TODO(a.garipov):** Define more Markdown conventions. - * Avoid bashisms and GNUisms, prefer *POSIX* features only. + * Prefer triple-backtick preformatted code blocks to indented code blocks. + + * Use asterisks and not underscores for bold and italic. + + * Use either link references or link destinations only. Put all link + reference definitions at the end of the second-level block. + +## Shell Scripting + + * Avoid bashisms and GNUisms, prefer POSIX features only. * Prefer `'raw strings'` to `"double quoted strings"` whenever possible. @@ -172,10 +219,15 @@ The rules are mostly sorted in the alphabetical order. * Put utility flags in the ASCII order and **don't** group them together. For example, `ls -1 -A -q`. - * `snake_case`, not `camelCase`. + * `snake_case`, not `camelCase` for variables. `kebab-case` for filenames. + + * UPPERCASE names for external exported variables, lowercase for local, + unexported ones. * Use `set -e -f -u` and also `set -x` in verbose mode. + * Use `readonly` liberally. + * Use the `"$var"` form instead of the `$var` form, unless word splitting is required. @@ -197,7 +249,7 @@ The rules are mostly sorted in the alphabetical order. dir="${TOP_DIR}"/sub ``` -## Text, Including Comments +## Text, Including Comments * End sentences with appropriate punctuation. @@ -218,7 +270,7 @@ The rules are mostly sorted in the alphabetical order. * Use double spacing between sentences to make sentence borders more clear. - * Use the serial comma (a.k.a. *Oxford* comma) to improve comprehension, + * Use the serial comma (a.k.a. Oxford comma) to improve comprehension, decrease ambiguity, and use a common standard. * Write todos like this: @@ -233,16 +285,16 @@ The rules are mostly sorted in the alphabetical order. // TODO(usr1, usr2): Fix the frobulation issue. ``` -## *YAML* +## YAML * **TODO(a.garipov):** Define naming conventions for schema names in our - *OpenAPI* *YAML* file. And just generally OpenAPI conventions. + OpenAPI YAML file. And just generally OpenAPI conventions. - * **TODO(a.garipov):** Find a *YAML* formatter or write our own. + * **TODO(a.garipov):** Find a YAML formatter or write our own. - * All strings, including keys, must be quoted. Reason: the [*NO-rway Law*]. + * All strings, including keys, must be quoted. Reason: the “[NO-rway Law]”. - * Indent with two (**2**) spaces. *YAML* documents can get pretty + * Indent with two (**2**) spaces. YAML documents can get pretty deeply-nested. * No extra indentation in multiline arrays: @@ -256,8 +308,8 @@ The rules are mostly sorted in the alphabetical order. * Prefer single quotes for strings to prevent accidental escaping, unless escaping is required or there are single quotes inside the string (e.g. for - *GitHub Actions*). + GitHub Actions). * Use `>` for multiline strings, unless you need to keep the line breaks. -[*NO-rway Law*]: https://news.ycombinator.com/item?id=17359376 +[NO-rway Law]: https://news.ycombinator.com/item?id=17359376 diff --git a/Makefile b/Makefile index 2bdee355..b119de72 100644 --- a/Makefile +++ b/Makefile @@ -1,347 +1,103 @@ +# Keep the Makefile POSIX-compliant. We currently allow hyphens in +# target names, but that may change in the future. # -# Available targets +# See https://pubs.opengroup.org/onlinepubs/9699919799/utilities/make.html. +.POSIX: + +CHANNEL = development +CLIENT_BETA_DIR = client2 +CLIENT_DIR = client +COMMIT = $$(git rev-parse --short HEAD) +DIST_DIR = dist +GO = go +# TODO(a.garipov): Add more default proxies using pipes after update to +# Go 1.15. # -# * build -- builds AdGuardHome for the current platform -# * client -- builds client-side code of AdGuard Home -# * client-watch -- builds client-side code of AdGuard Home and watches for changes there -# * docker -- builds a docker image for the current platform -# * clean -- clean everything created by previous builds -# * lint -- run all linters -# * test -- run all unit-tests -# * dependencies -- installs dependencies (go and npm modules) -# * ci -- installs dependencies, runs linters and tests, intended to be used by CI/CD -# -# Building releases: -# -# * release -- builds AdGuard Home distros. CHANNEL must be specified (edge, release or beta). -# * release_and_sign -- builds AdGuard Home distros and signs the binary files. -# CHANNEL must be specified (edge, release or beta). -# * sign -- Repacks all release archive files and signs the binary files inside them. -# For signing to work, the public+private key pair for $(GPG_KEY) must be imported: -# gpg --import public.txt -# gpg --import private.txt -# GPG_KEY_PASSPHRASE must contain the GPG key passphrase -# * docker-multi-arch -- builds a multi-arch image. If you want it to be pushed to docker hub, -# you must specify: -# * DOCKER_IMAGE_NAME - adguard/adguard-home -# * DOCKER_OUTPUT - type=image,name=adguard/adguard-home,push=true +# GOPROXY = https://goproxy.io|https://goproxy.cn|direct +GOPROXY = https://goproxy.cn,https://goproxy.io,direct +GPG_KEY = devteam@adguard.com +GPG_KEY_PASSPHRASE = not-a-real-password +NPM = npm +NPM_FLAGS = --prefix $(CLIENT_DIR) +SIGN = 1 +VERBOSE = 0 +VERSION = v0.0.0 +YARN = yarn +YARN_FLAGS = --cwd $(CLIENT_BETA_DIR) -GO := go -GOPATH := $(shell $(GO) env GOPATH) -PWD := $(shell pwd) -TARGET=AdGuardHome -BASE_URL="https://static.adguard.com/adguardhome/$(CHANNEL)" -GPG_KEY := devteam@adguard.com -GPG_KEY_PASSPHRASE := -GPG_CMD := gpg --detach-sig --default-key $(GPG_KEY) --pinentry-mode loopback --passphrase $(GPG_KEY_PASSPHRASE) -VERBOSE := -v -REBUILD_CLIENT = 1 +ENV = env\ + COMMIT='$(COMMIT)'\ + CHANNEL='$(CHANNEL)'\ + GPG_KEY='$(GPG_KEY)'\ + GPG_KEY_PASSPHRASE='$(GPG_KEY_PASSPHRASE)'\ + DIST_DIR='$(DIST_DIR)'\ + GO='$(GO)'\ + GOPROXY='$(GOPROXY)'\ + PATH="$${PWD}/bin:$$($(GO) env GOPATH)/bin:$${PATH}"\ + SIGN='$(SIGN)'\ + VERBOSE='$(VERBOSE)'\ + VERSION='$(VERSION)'\ -# See release target -DIST_DIR=dist +# Keep the line above blank. -# Update channel. Can be release, beta or edge. Uses edge by default. -CHANNEL ?= edge +# Keep this target first, so that a naked make invocation triggers +# a full build. +build: deps quick-build -# Validate channel -ifneq ($(CHANNEL),release) -ifneq ($(CHANNEL),beta) -ifneq ($(CHANNEL),edge) -$(error CHANNEL value is not valid. Valid values are release,beta or edge) -endif -endif -endif +quick-build: js-build go-build -# Version history URL (see -VERSION_HISTORY_URL="https://github.com/AdguardTeam/AdGuardHome/releases" -ifeq ($(CHANNEL),edge) - VERSION_HISTORY_URL="https://github.com/AdguardTeam/AdGuardHome/commits/master" -endif - -# goreleaser command depends on the $CHANNEL -GORELEASER_COMMAND=goreleaser release --rm-dist --skip-publish --snapshot --parallelism 1 -ifneq ($(CHANNEL),edge) - # If this is not an "edge" build, use normal release command - GORELEASER_COMMAND=goreleaser release --rm-dist --skip-publish --parallelism 1 -endif - -# Version properties -COMMIT=$(shell git rev-parse --short HEAD) -# TODO(a.garipov): The cut call is a temporary solution to trim -# prerelease versions. See the comment in .goreleaser.yml. -TAG_NAME=$(shell git describe --abbrev=0 | cut -c 1-8) -RELEASE_VERSION=$(TAG_NAME) -SNAPSHOT_VERSION=$(RELEASE_VERSION)-SNAPSHOT-$(COMMIT) - -# Set proper version -VERSION= -ifeq ($(TAG_NAME),$(shell git describe --abbrev=4)) - ifeq ($(CHANNEL),edge) - VERSION=$(SNAPSHOT_VERSION) - else - VERSION=$(RELEASE_VERSION) - endif -else - VERSION=$(SNAPSHOT_VERSION) -endif - -# Docker target parameters -DOCKER_IMAGE_NAME ?= adguardhome-dev -DOCKER_IMAGE_FULL_NAME = $(DOCKER_IMAGE_NAME):$(VERSION) -DOCKER_PLATFORMS=linux/amd64,linux/arm/v6,linux/arm/v7,linux/arm64,linux/386,linux/ppc64le -DOCKER_OUTPUT ?= type=image,name=$(DOCKER_IMAGE_NAME),push=false -BUILD_DATE=$(shell date -u +'%Y-%m-%dT%H:%M:%SZ') - -# Docker tags (can be redefined) -DOCKER_TAGS ?= -ifndef DOCKER_TAGS - ifeq ($(CHANNEL),release) - DOCKER_TAGS := --tag $(DOCKER_IMAGE_NAME):latest - endif - ifeq ($(CHANNEL),beta) - DOCKER_TAGS := --tag $(DOCKER_IMAGE_NAME):beta - endif - ifeq ($(CHANNEL),edge) - # Don't set the version tag when pushing to "edge" - DOCKER_IMAGE_FULL_NAME := $(DOCKER_IMAGE_NAME):edge - # DOCKER_TAGS := --tag $(DOCKER_IMAGE_NAME):edge - endif -endif - -# Validate docker build arguments -ifndef DOCKER_IMAGE_NAME -$(error DOCKER_IMAGE_NAME value is not set) -endif - -# OS-specific flags -TEST_FLAGS := --race $(VERBOSE) -ifeq ($(OS),Windows_NT) - TEST_FLAGS := -endif - -.PHONY: all build client client-watch docker lint lint-js lint-go test dependencies clean release docker-multi-arch -all: build - -init: - git config core.hooksPath .githooks - -build: - test '$(REBUILD_CLIENT)' = '1' && $(MAKE) client_with_deps || exit 0 - $(GO) mod download - PATH=$(GOPATH)/bin:$(PATH) $(GO) generate ./... - CGO_ENABLED=0 $(GO) build -ldflags="-s -w -X main.version=$(VERSION) -X main.channel=$(CHANNEL) -X main.goarm=$(GOARM)" - PATH=$(GOPATH)/bin:$(PATH) packr clean - -client: - npm --prefix client run build-prod - -client_with_deps: - npm --prefix client ci - npm --prefix client run build-prod - -client-watch: - npm --prefix client run watch - -docker: - DOCKER_CLI_EXPERIMENTAL=enabled \ - docker buildx build \ - --build-arg VERSION=$(VERSION) \ - --build-arg CHANNEL=$(CHANNEL) \ - --build-arg VCS_REF=$(COMMIT) \ - --build-arg BUILD_DATE=$(BUILD_DATE) \ - $(DOCKER_TAGS) \ - --load \ - -t "$(DOCKER_IMAGE_NAME)" -f ./Dockerfile . - - @echo Now you can run the docker image: - @echo docker run --name "adguard-home" -p 53:53/tcp -p 53:53/udp -p 80:80/tcp -p 443:443/tcp -p 853:853/tcp -p 3000:3000/tcp $(DOCKER_IMAGE_NAME) +ci: deps test +deps: js-deps go-deps lint: js-lint go-lint - -js-lint: dependencies - npm --prefix client run lint - -go-install-tools: - env GO=$(GO) sh ./scripts/go-install-tools.sh - -go-lint: - env GO=$(GO) PATH="$$PWD/bin:$$PATH" sh ./scripts/go-lint.sh - test: js-test go-test -js-test: - npm run test --prefix client +# Here and below, keep $(SHELL) in quotes, because on Windows this will +# expand to something like "C:/Program Files/Git/usr/bin/sh.exe". +build-docker: ; $(ENV) "$(SHELL)" ./scripts/make/build-docker.sh -go-test: - $(GO) test $(TEST_FLAGS) --coverprofile coverage.txt ./... +build-release: deps js-build + $(ENV) "$(SHELL)" ./scripts/make/build-release.sh -ci: client_with_deps - $(GO) mod download - $(MAKE) test +clean: ; $(ENV) "$(SHELL)" ./scripts/make/clean.sh +init: ; git config core.hooksPath ./scripts/hooks +js-build: + $(NPM) $(NPM_FLAGS) run build-prod + $(YARN) $(YARN_FLAGS) build +js-deps: + $(NPM) $(NPM_FLAGS) ci + $(YARN) $(YARN_FLAGS) install + +# TODO(a.garipov): Remove the legacy client tasks support once the new +# client is done and the old one is removed. +js-lint: ; $(NPM) $(NPM_FLAGS) run lint +js-test: ; $(NPM) $(NPM_FLAGS) run test +js-beta-lint: ; $(YARN) $(YARN_FLAGS) lint +js-beta-test: ; # TODO(v.abdulmyanov): Add tests for the new client. + +go-build: ; $(ENV) "$(SHELL)" ./scripts/make/go-build.sh +go-deps: ; $(ENV) "$(SHELL)" ./scripts/make/go-deps.sh +go-lint: ; $(ENV) "$(SHELL)" ./scripts/make/go-lint.sh +go-test: ; $(ENV) "$(SHELL)" ./scripts/make/go-test.sh +go-tools: ; $(ENV) "$(SHELL)" ./scripts/make/go-tools.sh + +go-check: go-tools go-lint go-test + +openapi-lint: ; cd ./openapi/ && $(YARN) test +openapi-show: ; cd ./openapi/ && $(YARN) start + +# TODO(a.garipov): Remove the legacy targets once the build +# infrastructure stops using them. dependencies: - npm --prefix client ci - $(GO) mod download - -clean: - rm -f ./AdGuardHome ./AdGuardHome.exe ./coverage.txt - rm -f -r ./build/ ./client/node_modules/ ./data/ ./$(DIST_DIR)/ -# Set the GOPATH explicitly in case make clean is called from under sudo -# after a Docker build. - env PATH="$(GOPATH)/bin:$$PATH" packr clean - rm -f -r ./bin/ - + @ echo "use make deps instead" + @ $(MAKE) deps docker-multi-arch: - DOCKER_CLI_EXPERIMENTAL=enabled \ - docker buildx build \ - --platform $(DOCKER_PLATFORMS) \ - --build-arg VERSION=$(VERSION) \ - --build-arg CHANNEL=$(CHANNEL) \ - --build-arg VCS_REF=$(COMMIT) \ - --build-arg BUILD_DATE=$(BUILD_DATE) \ - $(DOCKER_TAGS) \ - --output "$(DOCKER_OUTPUT)" \ - -t "$(DOCKER_IMAGE_FULL_NAME)" -f ./Dockerfile . - - @echo If the image was pushed to the registry, you can now run it: - @echo docker run --name "adguard-home" -p 53:53/tcp -p 53:53/udp -p 80:80/tcp -p 443:443/tcp -p 853:853/tcp -p 3000:3000/tcp $(DOCKER_IMAGE_NAME) - -release: client_with_deps - $(GO) mod download - @echo Starting release build: version $(VERSION), channel $(CHANNEL) - CHANNEL=$(CHANNEL) $(GORELEASER_COMMAND) - $(call write_version_file,$(VERSION)) - PATH=$(GOPATH)/bin:$(PATH) packr clean - -release_and_sign: client_with_deps - $(MAKE) release - $(call repack_dist) - -sign: - $(call repack_dist) - -define write_version_file - $(eval version := $(1)) - - @echo Writing version file: $(version) - - # Variables for CI - rm -f $(DIST_DIR)/version.txt - echo "version=$(version)" > $(DIST_DIR)/version.txt - - # Prepare the version.json file - rm -f $(DIST_DIR)/version.json - echo "{" >> $(DIST_DIR)/version.json - echo " \"version\": \"$(version)\"," >> $(DIST_DIR)/version.json - echo " \"announcement\": \"AdGuard Home $(version) is now available!\"," >> $(DIST_DIR)/version.json - echo " \"announcement_url\": \"$(VERSION_HISTORY_URL)\"," >> $(DIST_DIR)/version.json - echo " \"selfupdate_min_version\": \"0.0\"," >> $(DIST_DIR)/version.json - - # Windows builds - echo " \"download_windows_amd64\": \"$(BASE_URL)/AdGuardHome_windows_amd64.zip\"," >> $(DIST_DIR)/version.json - echo " \"download_windows_386\": \"$(BASE_URL)/AdGuardHome_windows_386.zip\"," >> $(DIST_DIR)/version.json - - # MacOS builds - echo " \"download_darwin_amd64\": \"$(BASE_URL)/AdGuardHome_darwin_amd64.zip\"," >> $(DIST_DIR)/version.json - echo " \"download_darwin_386\": \"$(BASE_URL)/AdGuardHome_darwin_386.zip\"," >> $(DIST_DIR)/version.json - - # Linux - echo " \"download_linux_amd64\": \"$(BASE_URL)/AdGuardHome_linux_amd64.tar.gz\"," >> $(DIST_DIR)/version.json - echo " \"download_linux_386\": \"$(BASE_URL)/AdGuardHome_linux_386.tar.gz\"," >> $(DIST_DIR)/version.json - - # Linux, all kinds of ARM - echo " \"download_linux_arm\": \"$(BASE_URL)/AdGuardHome_linux_armv6.tar.gz\"," >> $(DIST_DIR)/version.json - echo " \"download_linux_armv5\": \"$(BASE_URL)/AdGuardHome_linux_armv5.tar.gz\"," >> $(DIST_DIR)/version.json - echo " \"download_linux_armv6\": \"$(BASE_URL)/AdGuardHome_linux_armv6.tar.gz\"," >> $(DIST_DIR)/version.json - echo " \"download_linux_armv7\": \"$(BASE_URL)/AdGuardHome_linux_armv7.tar.gz\"," >> $(DIST_DIR)/version.json - echo " \"download_linux_arm64\": \"$(BASE_URL)/AdGuardHome_linux_arm64.tar.gz\"," >> $(DIST_DIR)/version.json - - # Linux, MIPS - echo " \"download_linux_mips\": \"$(BASE_URL)/AdGuardHome_linux_mips_softfloat.tar.gz\"," >> $(DIST_DIR)/version.json - echo " \"download_linux_mipsle\": \"$(BASE_URL)/AdGuardHome_linux_mipsle_softfloat.tar.gz\"," >> $(DIST_DIR)/version.json - echo " \"download_linux_mips64\": \"$(BASE_URL)/AdGuardHome_linux_mips64_softfloat.tar.gz\"," >> $(DIST_DIR)/version.json - echo " \"download_linux_mips64le\": \"$(BASE_URL)/AdGuardHome_linux_mips64le_softfloat.tar.gz\"," >> $(DIST_DIR)/version.json - - # FreeBSD - echo " \"download_freebsd_386\": \"$(BASE_URL)/AdGuardHome_freebsd_386.tar.gz\"," >> $(DIST_DIR)/version.json - echo " \"download_freebsd_amd64\": \"$(BASE_URL)/AdGuardHome_freebsd_amd64.tar.gz\"," >> $(DIST_DIR)/version.json - - # FreeBSD, all kinds of ARM - echo " \"download_freebsd_arm\": \"$(BASE_URL)/AdGuardHome_freebsd_armv6.tar.gz\"," >> $(DIST_DIR)/version.json - echo " \"download_freebsd_armv5\": \"$(BASE_URL)/AdGuardHome_freebsd_armv5.tar.gz\"," >> $(DIST_DIR)/version.json - echo " \"download_freebsd_armv6\": \"$(BASE_URL)/AdGuardHome_freebsd_armv6.tar.gz\"," >> $(DIST_DIR)/version.json - echo " \"download_freebsd_armv7\": \"$(BASE_URL)/AdGuardHome_freebsd_armv7.tar.gz\"," >> $(DIST_DIR)/version.json - echo " \"download_freebsd_arm64\": \"$(BASE_URL)/AdGuardHome_freebsd_arm64.tar.gz\"" >> $(DIST_DIR)/version.json - - # Finish - echo "}" >> $(DIST_DIR)/version.json -endef - -define repack_dist - # Repack archive files - # A temporary solution for our auto-update code to be able to unpack these archive files - # The problem is that goreleaser doesn't add directory AdGuardHome/ to the archive file - # and we can't create it - rm -rf $(DIST_DIR)/AdGuardHome - - # Windows builds - $(call zip_repack_windows,AdGuardHome_windows_amd64.zip) - $(call zip_repack_windows,AdGuardHome_windows_386.zip) - - # MacOS builds - $(call zip_repack,AdGuardHome_darwin_amd64.zip) - $(call zip_repack,AdGuardHome_darwin_386.zip) - - # Linux - $(call tar_repack,AdGuardHome_linux_amd64.tar.gz) - $(call tar_repack,AdGuardHome_linux_386.tar.gz) - - # Linux, all kinds of ARM - $(call tar_repack,AdGuardHome_linux_armv5.tar.gz) - $(call tar_repack,AdGuardHome_linux_armv6.tar.gz) - $(call tar_repack,AdGuardHome_linux_armv7.tar.gz) - $(call tar_repack,AdGuardHome_linux_arm64.tar.gz) - - # Linux, MIPS - $(call tar_repack,AdGuardHome_linux_mips_softfloat.tar.gz) - $(call tar_repack,AdGuardHome_linux_mipsle_softfloat.tar.gz) - $(call tar_repack,AdGuardHome_linux_mips64_softfloat.tar.gz) - $(call tar_repack,AdGuardHome_linux_mips64le_softfloat.tar.gz) - - # FreeBSD - $(call tar_repack,AdGuardHome_freebsd_386.tar.gz) - $(call tar_repack,AdGuardHome_freebsd_amd64.tar.gz) - - # FreeBSD, all kinds of ARM - $(call tar_repack,AdGuardHome_freebsd_armv5.tar.gz) - $(call tar_repack,AdGuardHome_freebsd_armv6.tar.gz) - $(call tar_repack,AdGuardHome_freebsd_armv7.tar.gz) - $(call tar_repack,AdGuardHome_freebsd_arm64.tar.gz) -endef - -define zip_repack_windows - $(eval ARC := $(1)) - cd $(DIST_DIR) && \ - unzip $(ARC) && \ - $(GPG_CMD) AdGuardHome/AdGuardHome.exe && \ - zip -r $(ARC) AdGuardHome/ && \ - rm -rf AdGuardHome -endef - -define zip_repack - $(eval ARC := $(1)) - cd $(DIST_DIR) && \ - unzip $(ARC) && \ - $(GPG_CMD) AdGuardHome/AdGuardHome && \ - zip -r $(ARC) AdGuardHome/ && \ - rm -rf AdGuardHome -endef - -define tar_repack - $(eval ARC := $(1)) - cd $(DIST_DIR) && \ - tar xzf $(ARC) && \ - $(GPG_CMD) AdGuardHome/AdGuardHome && \ - tar czf $(ARC) AdGuardHome/ && \ - rm -rf AdGuardHome -endef + @ echo "use make build-docker instead" + @ $(MAKE) build-docker +go-install-tools: + @ echo "use make go-tools instead" + @ $(MAKE) go-tools +release: + @ echo "use make build-release instead" + @ $(MAKE) build-release diff --git a/README.md b/README.md index eeb8f10e..f9ee5dae 100644 --- a/README.md +++ b/README.md @@ -45,7 +45,7 @@ AdGuard Home is a network-wide software for blocking ads & tracking. After you set it up, it'll cover ALL your home devices, and you don't need any client-side software for that. -It operates as a DNS server that re-routes tracking domains to a "black hole," thus preventing your devices from connecting to those servers. It's based on software we use for our public [AdGuard DNS](https://adguard.com/en/adguard-dns/overview.html) servers -- both share a lot of common code. +It operates as a DNS server that re-routes tracking domains to a "black hole", thus preventing your devices from connecting to those servers. It's based on software we use for our public [AdGuard DNS](https://adguard.com/en/adguard-dns/overview.html) servers -- both share a lot of common code. * [Getting Started](#getting-started) * [Comparing AdGuard Home to other solutions](#comparison) @@ -58,7 +58,7 @@ It operates as a DNS server that re-routes tracking domains to a "black hole," t * [Reporting issues](#reporting-issues) * [Help with translations](#translate) * [Other](#help-other) -* [Projects that use AdGuardHome](#uses) +* [Projects that use AdGuard Home](#uses) * [Acknowledgments](#acknowledgments) * [Privacy](#privacy) @@ -87,12 +87,21 @@ If you're running **Linux**, there's a secure and easy way to install AdGuard Ho ### Guides -* [FAQ](https://github.com/AdguardTeam/AdGuardHome/wiki/FAQ) -* [Configuration](https://github.com/AdguardTeam/AdGuardHome/wiki/Configuration) -* [AdGuard Home as a DNS-over-HTTPS or DNS-over-TLS server](https://github.com/AdguardTeam/AdGuardHome/wiki/Encryption) -* [How to install and run AdGuard Home on Raspberry Pi](https://github.com/AdguardTeam/AdGuardHome/wiki/Raspberry-Pi) -* [How to install and run AdGuard Home on a Virtual Private Server](https://github.com/AdguardTeam/AdGuardHome/wiki/VPS) -* [How to write your own hosts blocklists properly](https://github.com/AdguardTeam/AdGuardHome/wiki/Hosts-Blocklists) +* [Getting Started](https://github.com/AdguardTeam/AdGuardHome/wiki/Getting-Started) + * [FAQ](https://github.com/AdguardTeam/AdGuardHome/wiki/FAQ) + * [How to Write Hosts Blocklists](https://github.com/AdguardTeam/AdGuardHome/wiki/Hosts-Blocklists) + * [Comparing AdGuard Home to Other Solutions](https://github.com/AdguardTeam/AdGuardHome/wiki/Comparison) +* Configuring AdGuard + * [Configuration](https://github.com/AdguardTeam/AdGuardHome/wiki/Configuration) + * [Configuring AdGuard Home Clients](https://github.com/AdguardTeam/AdGuardHome/wiki/Clients) + * [AdGuard Home as a DoH, DoT, or DoQ Server](https://github.com/AdguardTeam/AdGuardHome/wiki/Encryption) + * [AdGuard Home as a DNSCrypt Server](https://github.com/AdguardTeam/AdGuardHome/wiki/DNSCrypt) + * [AdGuard Home as a DHCP Server](https://github.com/AdguardTeam/AdGuardHome/wiki/DHCP) +* Installing AdGuard Home + * [Docker](https://github.com/AdguardTeam/AdGuardHome/wiki/Docker) + * [How to Install and Run AdGuard Home on a Raspberry Pi](https://github.com/AdguardTeam/AdGuardHome/wiki/Raspberry-Pi) + * [How to Install and Run AdGuard Home on a Virtual Private Server](https://github.com/AdguardTeam/AdGuardHome/wiki/VPS) +* [Verifying Releases](https://github.com/AdguardTeam/AdGuardHome/wiki/Verify-Releases) ### API @@ -123,20 +132,21 @@ AdGuard Home provides a lot of features out-of-the-box with no need to install a > Disclaimer: some of the listed features can be added to Pi-Hole by installing additional software or by manually using SSH terminal and reconfiguring one of the utilities Pi-Hole consists of. However, in our opinion, this cannot be legitimately counted as a Pi-Hole's feature. -| Feature | AdGuard Home | Pi-Hole | -|-------------------------------------------------------------------------|--------------|--------------------------------------------------------| -| Blocking ads and trackers | ✅ | ✅ | -| Customizing blocklists | ✅ | ✅ | -| Built-in DHCP server | ✅ | ✅ | -| HTTPS for the Admin interface | ✅ | Kind of, but you'll need to manually configure lighthttpd | -| Encrypted DNS upstream servers (DNS-over-HTTPS, DNS-over-TLS, DNSCrypt) | ✅ | ❌ (requires additional software) | -| Cross-platform | ✅ | ❌ (not natively, only via Docker) | -| Running as a DNS-over-HTTPS or DNS-over-TLS server | ✅ | ❌ (requires additional software) | -| Blocking phishing and malware domains | ✅ | ❌ (requires non-default blocklists) | -| Parental control (blocking adult domains) | ✅ | ❌ | -| Force Safe search on search engines | ✅ | ❌ | -| Per-client (device) configuration | ✅ | ✅ | -| Access settings (choose who can use AGH DNS) | ✅ | ❌ | +| Feature | AdGuard Home | Pi-Hole | +|-------------------------------------------------------------------------|-------------------|-----------------------------------------------------------| +| Blocking ads and trackers | ✅ | ✅ | +| Customizing blocklists | ✅ | ✅ | +| Built-in DHCP server | ✅ | ✅ | +| HTTPS for the Admin interface | ✅ | Kind of, but you'll need to manually configure lighthttpd | +| Encrypted DNS upstream servers (DNS-over-HTTPS, DNS-over-TLS, DNSCrypt) | ✅ | ❌ (requires additional software) | +| Cross-platform | ✅ | ❌ (not natively, only via Docker) | +| Running as a DNS-over-HTTPS or DNS-over-TLS server | ✅ | ❌ (requires additional software) | +| Blocking phishing and malware domains | ✅ | ❌ (requires non-default blocklists) | +| Parental control (blocking adult domains) | ✅ | ❌ | +| Force Safe search on search engines | ✅ | ❌ | +| Per-client (device) configuration | ✅ | ✅ | +| Access settings (choose who can use AGH DNS) | ✅ | ❌ | +| Running [without root privileges](https://github.com/AdguardTeam/AdGuardHome/wiki/Getting-Started#running-without-superuser) | ✅ | ❌ | ### How does AdGuard Home compare to traditional ad blockers @@ -145,7 +155,7 @@ It depends. "DNS sinkholing" is capable of blocking a big percentage of ads, but it lacks flexibility and power of traditional ad blockers. You can get a good impression about the difference between these methods by reading [this article](https://adguard.com/en/blog/adguard-vs-adaway-dns66/). It compares AdGuard for Android (a traditional ad blocker) to hosts-level ad blockers (which are almost identical to DNS-based blockers in their capabilities). -However, this level of protection is enough for some users. Additionally, using a DNS-based blocker can help to block ads, tracking and analytics requests on other types of devices, such as SmartTVs, smart speakers or other kinds of IoT devices (on which you can't install tradtional ad blockers). +However, this level of protection is enough for some users. Additionally, using a DNS-based blocker can help to block ads, tracking and analytics requests on other types of devices, such as SmartTVs, smart speakers or other kinds of IoT devices (on which you can't install traditional ad blockers). **Known limitations** @@ -169,7 +179,8 @@ You will need this to build AdGuard Home: * [go](https://golang.org/dl/) v1.14 or later. * [node.js](https://nodejs.org/en/download/) v10.16.2 or later. - * [npm](https://www.npmjs.com/) v6.14 or later. + * [npm](https://www.npmjs.com/) v6.14 or later (temporary requirement, TODO: remove when redesign is finished). + * [yarn](https://yarnpkg.com/) v1.22.5 or later. ### Building @@ -181,6 +192,12 @@ cd AdGuardHome make ``` +Please note, that the non-standard `-j` flag is currently not supported, so +building with `make -j 4` or setting your `MAKEFLAGS` to include, for example, +`-j 4` is likely to break the build. If you do have your `MAKEFLAGS` set to +that, and you don't want to change it, you can override it by running +`make -j 1`. + Check the [`Makefile`](https://github.com/AdguardTeam/AdGuardHome/blob/master/Makefile) to learn about other commands. **Building for a different platform.** You can build AdGuard for any OS/ARCH just like any other Go project. @@ -188,26 +205,28 @@ In order to do this, specify `GOOS` and `GOARCH` env variables before running ma For example: ``` -GOOS=linux GOARCH=arm64 make +env GOOS='linux' GOARCH='arm64' make +``` +Or: +``` +make GOOS='linux' GOARCH='arm64' ``` #### Preparing release You'll need this to prepare a release build: -* [goreleaser](https://goreleaser.com/) * [snapcraft](https://snapcraft.io/) Commands: -* `make release` - builds a snapshot build (CHANNEL=edge) -* `CHANNEL=beta make release` - builds beta version, tag is mandatory. -* `CHANNEL=release make release` - builds release version, tag is mandatory. +``` +make build-release CHANNEL='...' VERSION='...' +``` #### Docker image -* Run `make docker` to build the Docker image locally. -* Run `make docker-multi-arch` to build the multi-arch Docker image (the one that we publish to Docker Hub). +* Run `make build-docker` to build the Docker image locally (the one that we publish to DockerHub). Please note, that we're using [Docker Buildx](https://docs.docker.com/buildx/working-with-buildx/) to build our official image. @@ -290,17 +309,20 @@ Here is a link to AdGuard Home project: https://crowdin.com/project/adguard-appl Here's what you can also do to contribute: 1. [Look for issues](https://github.com/AdguardTeam/AdGuardHome/issues?q=is%3Aissue+is%3Aopen+label%3A%22help+wanted%22+) marked as "help wanted". -2. Actualize the list of *Blocked services*. It it can be found in [dnsfilter/blocked_services.go](https://github.com/AdguardTeam/AdGuardHome/blob/master/internal/dnsfilter/blocked_services.go). +2. Actualize the list of *Blocked services*. It it can be found in [dnsfilter/blocked.go](https://github.com/AdguardTeam/AdGuardHome/blob/master/internal/dnsfilter/blocked.go). 3. Actualize the list of known *trackers*. It it can be found in [client/src/helpers/trackers/adguard.json](https://github.com/AdguardTeam/AdGuardHome/blob/master/client/src/helpers/trackers/adguard.json). 4. Actualize the list of vetted *blocklists*. It it can be found in [client/src/helpers/filters/filters.json](https://github.com/AdguardTeam/AdGuardHome/blob/master/client/src/helpers/filters/filters.json). -## Projects that use AdGuardHome - -* Python library (https://github.com/frenck/python-adguardhome) -* Hass.io add-on (https://github.com/hassio-addons/addon-adguard-home) -* OpenWrt LUCI app (https://github.com/rufengsuixing/luci-app-adguardhome) +## Projects that use AdGuard Home +* [AdGuard Home Remote](https://apps.apple.com/app/apple-store/id1543143740) - iOS app by [Joost](https://rocketscience-it.nl/) +* [Python library](https://github.com/frenck/python-adguardhome) by [@frenck](https://github.com/frenck) +* [Home Assistant add-on](https://github.com/hassio-addons/addon-adguard-home) by [@frenck](https://github.com/frenck) +* [OpenWrt LUCI app](https://github.com/kongfl888/luci-app-adguardhome) by [@kongfl888](https://github.com/kongfl888) (originally by [@rufengsuixing](https://github.com/rufengsuixing)) +* [Prometheus exporter for AdGuard Home](https://github.com/ebrianne/adguard-exporter) by [@ebrianne](https://github.com/ebrianne) +* [AdGuard Home on GLInet routers](https://forum.gl-inet.com/t/adguardhome-on-gl-routers/10664) by [Gl-Inet](https://gl-inet.com/) +* [Cloudron app](https://git.cloudron.io/cloudron/adguard-home-app) by [@gramakri](https://github.com/gramakri) ## Acknowledgments @@ -321,7 +343,7 @@ This software wouldn't have been possible without: * And many more node.js packages. * [whotracks.me data](https://github.com/cliqz-oss/whotracks.me) -You might have seen that [CoreDNS](https://coredns.io) was mentioned here before — we've stopped using it in AdGuardHome. While we still use it on our servers for [AdGuard DNS](https://adguard.com/adguard-dns/overview.html) service, it seemed like an overkill for Home as it impeded with Home features that we plan to implement. +You might have seen that [CoreDNS](https://coredns.io) was mentioned here before — we've stopped using it in AdGuard Home. While we still use it on our servers for [AdGuard DNS](https://adguard.com/adguard-dns/overview.html) service, it seemed like an overkill for Home as it impeded with Home features that we plan to implement. For a full list of all node.js packages in use, please take a look at [client/package.json](https://github.com/AdguardTeam/AdGuardHome/blob/master/client/package.json) file. diff --git a/client/package-lock.json b/client/package-lock.json index f1172e00..56bf51c4 100644 --- a/client/package-lock.json +++ b/client/package-lock.json @@ -3066,12 +3066,6 @@ "pkg-up": "^2.0.0" } }, - "caniuse-lite": { - "version": "1.0.30001062", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001062.tgz", - "integrity": "sha512-ei9ZqeOnN7edDrb24QfJ0OZicpEbsWxv7WusOiQGz/f2SfvBgHHbOEwBJ8HKGVSyx8Z6ndPjxzR6m0NQq+0bfw==", - "dev": true - }, "postcss": { "version": "7.0.30", "resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.30.tgz", @@ -3928,9 +3922,9 @@ } }, "caniuse-lite": { - "version": "1.0.30001059", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001059.tgz", - "integrity": "sha512-oOrc+jPJWooKIA0IrNZ5sYlsXc7NP7KLhNWrSGEJhnfSzDvDJ0zd3i6HXsslExY9bbu+x0FQ5C61LcqmPt7bOQ==", + "version": "1.0.30001165", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001165.tgz", + "integrity": "sha512-8cEsSMwXfx7lWSUMA2s08z9dIgsnR5NAqjXP23stdsU3AUWkCr/rr4s4OFtHXn5XXr6+7kam3QFVoYyXNPdJPA==", "dev": true }, "capture-exit": { diff --git a/client/public/index.html b/client/public/index.html index e841f191..517048b9 100644 --- a/client/public/index.html +++ b/client/public/index.html @@ -2,7 +2,7 @@ - + diff --git a/client/public/install.html b/client/public/install.html index 0fe426a3..2b607f9b 100644 --- a/client/public/install.html +++ b/client/public/install.html @@ -2,7 +2,7 @@ - + diff --git a/client/public/login.html b/client/public/login.html index 43ff190b..4908ac63 100644 --- a/client/public/login.html +++ b/client/public/login.html @@ -2,7 +2,7 @@ - + diff --git a/client/src/__locales/be.json b/client/src/__locales/be.json index a1a340e5..b89af602 100644 --- a/client/src/__locales/be.json +++ b/client/src/__locales/be.json @@ -32,6 +32,7 @@ "form_error_ip_format": "Няслушны фармат IP-адраса", "form_error_mac_format": "Некарэктны фармат MAC", "form_error_client_id_format": "Няслушны фармат ID кліента", + "form_error_server_name": "Няслушнае імя сервера", "form_error_positive": "Павінна быць больш 0", "form_error_negative": "Павінна быць не менш 0", "range_end_error": "Павінен перавышаць пачатак дыяпазону", @@ -247,8 +248,16 @@ "custom_ip": "Свой IP", "blocking_ipv4": "Блакаванне IPv4", "blocking_ipv6": "Блакаванне IPv6", + "dnscrypt": "DNSCrypt", "dns_over_https": "DNS-over-HTTPS", "dns_over_tls": "DNS-over-TLS", + "dns_over_quic": "DNS-over-QUIC", + "client_id": "Ідэнтыфікатар кліента", + "client_id_placeholder": "Увядзіце ідэнтыфікатар кліента", + "client_id_desc": "Розныя кліенты могуць ідэнтыфікавацца па адмысловым ідэнтыфікатары кліента. Тут вы можаце даведацца больш пра ідэнтыфікацыю кліентаў.", + "download_mobileconfig_doh": "Спампаваць .mobileconfig для DNS-over-HTTPS", + "download_mobileconfig_dot": "Спампаваць .mobileconfig для DNS-over-TLS", + "download_mobileconfig": "Загрузіць файл канфігурацыі", "plain_dns": "Нешыфраваны DNS", "form_enter_rate_limit": "Увядзіце rate limit", "rate_limit": "Ограничение скорости", @@ -267,7 +276,7 @@ "source_label": "Крыніца", "found_in_known_domain_db": "Знойдзены ў базе вядомых даменаў.", "category_label": "Катэгорыя", - "rule_label": "Правіла", + "rule_label": "Правіла(ы)", "list_label": "Спіс", "unknown_filter": "Невядомы фільтр {{filterId}}", "known_tracker": "Вядомы трэкер", @@ -416,6 +425,7 @@ "setup_dns_privacy_1": "<0>DNS-over-TLS: Ужывайце радок <1>{{address}}.", "setup_dns_privacy_2": "<0>DNS-over-HTTPS: Ужывайце радок <1>{{address}}.", "setup_dns_privacy_3": "<0>Вось спіс ПА, якое вы можаце выкарыстоўваць.", + "setup_dns_privacy_4": "На прыладах з iOS 14 і macOS Big Sur вы можаце спампаваць адмысловы файл '.mobileconfig', які дадае DNS-over-HTTPS ці DNS-over-TLS серверы ў налады DNS.", "setup_dns_privacy_android_1": "Android 9 натыўна падтрымвае DNS-over-TLS. Для налады, перайдзіце ў Налады → Сеціва і Інтэрнэт → Дадаткова → Персанальны DNS сервер, і ўвядзіце туды ваша даменавае імя.", "setup_dns_privacy_android_2": "<0>AdGuard для Android падтрымвае <1>DNS-over-HTTPS і <1>DNS-over-TLS.", "setup_dns_privacy_android_3": "<0>Intra дадае падтрымка <1>DNS-over-HTTPS на Android.", @@ -427,6 +437,7 @@ "setup_dns_privacy_other_3": "<0>dnscrypt-proxy падтрымвае <1>DNS-over-HTTPS.", "setup_dns_privacy_other_4": "<0>Mozilla Firefox падтрымвае <1>DNS-over-HTTPS.", "setup_dns_privacy_other_5": "Вы можаце знайсці яшчэ варыянты <0>тут і <1>тут.", + "setup_dns_privacy_ioc_mac": "Канфігурацыя для iOS і macOS", "setup_dns_notice": "Каб выкарыстоўваць <1>DNS-over-HTTPS ці <1>DNS-over-TLS, вам патрэбна <0>наладзіць шыфраванне у наладах AdGuard Home.", "rewrite_added": "Правіла перанакіравання DNS для \"{{key}}\" паспяхова дададзена", "rewrite_deleted": "Правіла перанакіравання DNS для \"{{key}}\" паспяхова выдалена", @@ -526,7 +537,6 @@ "check_ip": "IP-адрасы: {{ip}}", "check_cname": "CNAME: {{cname}}", "check_reason": "Прычына: {{reason}}", - "check_rule": "Правіла: {{rule}}", "check_service": "Назва сэрвісу: {{service}}", "service_name": "Назва сэрвіса", "check_not_found": "Не знойдзена ў вашым спісе фільтраў", diff --git a/client/src/__locales/bg.json b/client/src/__locales/bg.json index 7cf95826..2ae2c568 100644 --- a/client/src/__locales/bg.json +++ b/client/src/__locales/bg.json @@ -144,7 +144,6 @@ "source_label": "Източник", "found_in_known_domain_db": "Намерен в списъците с домейни.", "category_label": "Категория", - "rule_label": "Правило", "unknown_filter": "Непознат филтър {{filterId}}", "install_welcome_title": "Добре дошли в AdGuard Home!", "install_welcome_desc": "AdGuard Home e мрежово решение за блокиране на реклами и тракери на DNS ниво. Създадено е за да ви даде пълен контрол над мрежата и всичките ви устройства, без да е необходимо допълнително инсталиране на друг софтуер.", @@ -202,7 +201,6 @@ "encryption_config_saved": "Конфигурацията е успешно записана", "encryption_server": "Име на сървъра", "encryption_server_enter": "Въведете име на домейна", - "encryption_server_desc": "За да използвате HTTPS, трябва името на сървъра да съвпада с това на SSL сертификата.", "encryption_redirect": "Автоматично пренасочване към HTTPS", "encryption_redirect_desc": "Служи за автоматично пренасочване от HTTP към HTTPS на страницата за Администрация в AdGuard Home.", "encryption_https": "HTTPS порт", diff --git a/client/src/__locales/cs.json b/client/src/__locales/cs.json index e813a3ea..84cf3e32 100644 --- a/client/src/__locales/cs.json +++ b/client/src/__locales/cs.json @@ -32,6 +32,7 @@ "form_error_ip_format": "Neplatný formát IP", "form_error_mac_format": "Neplatný formát MAC", "form_error_client_id_format": "Neplatný formát ID klienta", + "form_error_server_name": "Neplatný název serveru", "form_error_positive": "Musí být větší než 0", "form_error_negative": "Musí být rovno nebo větší než 0", "range_end_error": "Musí být větší než začátek rozsahu", @@ -247,10 +248,16 @@ "custom_ip": "Vlastní IP", "blocking_ipv4": "Blokování IPv4", "blocking_ipv6": "Blokování IPv6", + "dnscrypt": "DNSCrypt", "dns_over_https": "DNS přes HTTPS", "dns_over_tls": "DNS přes TLS", + "dns_over_quic": "DNS skrze QUIC", + "client_id": "ID klienta", + "client_id_placeholder": "Zadejte ID klienta", + "client_id_desc": "Různé klienty lze identifikovat pomocí speciálního ID klienta. Zde se můžete dozvědět více o tom, jak klienty identifikovat.", "download_mobileconfig_doh": "Stáhnout .mobileconfig pro DNS skrze HTTPS", "download_mobileconfig_dot": "Stáhnout .mobileconfig pro DNS skrze TLS", + "download_mobileconfig": "Stáhnout konfigurační soubor", "plain_dns": "Čisté DNS", "form_enter_rate_limit": "Zadejte rychlostní limit", "rate_limit": "Rychlostní limit", @@ -269,7 +276,7 @@ "source_label": "Zdroj", "found_in_known_domain_db": "Nalezeno v databázi známých domén", "category_label": "Kategorie", - "rule_label": "Pravidlo", + "rule_label": "Pravidla", "list_label": "Seznam", "unknown_filter": "Neznámý filtr {{filterId}}", "known_tracker": "Známý slídič", @@ -330,7 +337,7 @@ "encryption_config_saved": "Konfigurace šifrování byla uložena", "encryption_server": "Název serveru", "encryption_server_enter": "Zadejte název domény", - "encryption_server_desc": "Abyste mohli používat protokol HTTPS, musíte zadat název serveru, který odpovídá vašemu certifikátu SSL.", + "encryption_server_desc": "Abyste mohli používat HTTPS, musíte zadat název serveru, který odpovídá vašemu certifikátu SSL nebo zástupnému certifikátu. Pokud není pole nastaveno, bude přijímat připojení TLS pro libovolnou doménu.", "encryption_redirect": "Automaticky přesměrovat na HTTPS", "encryption_redirect_desc": "Pokud je zaškrtnuto, AdGuard Home vás automaticky přesměruje z adres HTTP na HTTPS.", "encryption_https": "HTTPS port", @@ -386,7 +393,7 @@ "client_edit": "Upravit klienta", "client_identifier": "Identifikátor", "ip_address": "IP adresa", - "client_identifier_desc": "Klienti můžou být identifikováni podle IP adresy, CIDR nebo MAC adresy. Upozorňujeme, že použití MAC jako identifikátoru je možné pouze v případě, že je AdGuard Home také <0>DHCP server", + "client_identifier_desc": "Klienti můžou být identifikováni podle IP adresy, CIDR, MAC adresy nebo speciálního ID klienta (může být použito pro DoT/DoH/DoQ). <0>Zde se můžete dozvědět více o tom, jak klienty identifikovat.", "form_enter_ip": "Zadejte IP", "form_enter_mac": "Zadejte MAC", "form_enter_id": "Zadejte identifikátor", @@ -430,6 +437,7 @@ "setup_dns_privacy_other_3": "<0>dnscrypt-proxy podporuje <1>DNS-přes-HTTPS.", "setup_dns_privacy_other_4": "<0>Mozilla Firefox podporuje <1>DNS-přes-HTTPS.", "setup_dns_privacy_other_5": "Další implementace naleznete <0>zde a <1>zde.", + "setup_dns_privacy_ioc_mac": "Konfigurace pro iOS a macOS", "setup_dns_notice": "Pro použití <1>DNS-přes-HTTPS nebo <1>DNS-přes-TLS potřebujete v nastaveních AdGuard Home <0>nakonfigurovat šifrování.", "rewrite_added": "Přesměrování DNS pro „{{key}}“ úspěšně přidáno", "rewrite_deleted": "Přesměrování DNS pro „{{key}}“ úspěšně smazáno", @@ -529,7 +537,6 @@ "check_ip": "IP adresy: {{ip}}", "check_cname": "CNAME: {{cname}}", "check_reason": "Důvod: {{reason}}", - "check_rule": "Pravidlo: {{rule}}", "check_service": "Název služby: {{service}}", "service_name": "Název služby", "check_not_found": "Nenalezeno ve Vašich seznamech filtrů", diff --git a/client/src/__locales/da.json b/client/src/__locales/da.json index 1e80a324..3ba7f9b4 100644 --- a/client/src/__locales/da.json +++ b/client/src/__locales/da.json @@ -32,6 +32,7 @@ "form_error_ip_format": "Ugyldigt IP-format", "form_error_mac_format": "Ugyldigt MAC-format", "form_error_client_id_format": "Ugyldigt klient-ID-format", + "form_error_server_name": "Ugyldigt servernavn", "form_error_positive": "Skal være større end 0", "form_error_negative": "Skal være lig med 0 eller større", "range_end_error": "Skal være større end starten af ​​intervallet", @@ -247,10 +248,16 @@ "custom_ip": "Tilpasset IP", "blocking_ipv4": "IPv4-blokering", "blocking_ipv6": "IPv6-blokering", + "dnscrypt": "DNSCrypt", "dns_over_https": "DNS-over-HTTPS", "dns_over_tls": "DNS-over-TLS", + "dns_over_quic": "DNS-over-Quic", + "client_id": "Klient-ID", + "client_id_placeholder": "Indtast klient-ID", + "client_id_desc": "Forskellige klienter kan identificeres ved hjælp af et specielt klient-ID. Her kan du lære mere om, hvordan du identificerer klienter.", "download_mobileconfig_doh": "Download .mobileconfig til DNS-over-HTTPS", "download_mobileconfig_dot": "Download .mobileconfig til DNS-over-TLS", + "download_mobileconfig": "Download konfigurationsfil", "plain_dns": "Almindelig DNS", "form_enter_rate_limit": "Indtast hyppighedsgrænse", "rate_limit": "Hyppighedsgrænse", @@ -269,7 +276,7 @@ "source_label": "Kilde", "found_in_known_domain_db": "Fundet i databasen med kendte domæner.", "category_label": "Kategori", - "rule_label": "Regel", + "rule_label": "Regel(regler)", "list_label": "Liste", "unknown_filter": "Ukendt filter {{filterId}}", "known_tracker": "Kendt tracker", @@ -330,7 +337,7 @@ "encryption_config_saved": "Krypteringskonfiguration gemt", "encryption_server": "Servernavn", "encryption_server_enter": "Indtast dit domænenavn", - "encryption_server_desc": "For at kunne bruge HTTPS skal du indtaste servernavnet, der matcher dit SSL-certifikat.", + "encryption_server_desc": "For at kunne bruge HTTPS skal du indtaste det servernavn, der matcher dit SSL-certifikat eller wildcard-certifikat. Hvis feltet ikke er indstillet, accepterer det TLS-forbindelser til ethvert domæne.", "encryption_redirect": "Omdiriger automatisk til HTTPS", "encryption_redirect_desc": "Hvis afkrydset, vil AdGuard Home automatisk omdirigere dig fra HTTP til HTTPS-adresser.", "encryption_https": "HTTPS-port", @@ -386,7 +393,7 @@ "client_edit": "Rediger Klient", "client_identifier": "Identifikator", "ip_address": "IP-adresse", - "client_identifier_desc": "Klienter kan identificeres ud fra IP-adressen, CIDR eller MAC-adressen. Bemærk, at det kun er muligt at bruge MAC som identifikator, hvis AdGuard Home også er en <0>DHCP-server", + "client_identifier_desc": "Klienter kan identificeres ud fra IP-adressen, CIDR eller MAC-adressen eller et specielt klient-ID (kan bruges til DoT/DoH/DoQ). <0>Her kan du lære mere om, hvordan du identificerer klienter.", "form_enter_ip": "Indtast IP", "form_enter_mac": "Indtast MAC", "form_enter_id": "Indtast identifikator", @@ -430,6 +437,7 @@ "setup_dns_privacy_other_3": "<0>dnscrypt-proxy understøtter <1>DNS-over-HTTPS.", "setup_dns_privacy_other_4": "<0>Mozilla Firefox understøtter <1>DNS-over-HTTPS.", "setup_dns_privacy_other_5": "Du kan finde flere implementeringer <0>her og <1>her.", + "setup_dns_privacy_ioc_mac": "iOS- og macOS-konfiguration", "setup_dns_notice": "For at kunne bruge <1>DNS-over-HTTPS eller <1>DNS-over-TLS, skal du <0>konfigurere Krypteringen i indstillingerne i AdGuard Home.", "rewrite_added": "DNS-omskrivning for \"{{key}}\" blev tilføjet", "rewrite_deleted": "DNS-omskrivning for \"{{key}}\" blev slettet", @@ -529,7 +537,6 @@ "check_ip": "IP-adresser: {{ip}}", "check_cname": "CNAME: {{cname}}", "check_reason": "Årsag: {{reason}}", - "check_rule": "Regel: {{rule}}", "check_service": "Servicenavn: {{service}}", "service_name": "Navn på tjeneste", "check_not_found": "Ikke fundet i dine filterlister", diff --git a/client/src/__locales/de.json b/client/src/__locales/de.json index 35a126b2..17d5832e 100644 --- a/client/src/__locales/de.json +++ b/client/src/__locales/de.json @@ -32,6 +32,7 @@ "form_error_ip_format": "Ungültiges IPv4-Format", "form_error_mac_format": "Ungültiges MAC-Format", "form_error_client_id_format": "Ungültiges Client-ID-Format", + "form_error_server_name": "Ungültiger Servername", "form_error_positive": "Muss größer als 0 sein.", "form_error_negative": "Muss gleich oder größer als 0 (Null) sein", "range_end_error": "Muss größer als der Bereichsbeginn sein", @@ -91,16 +92,16 @@ "disabled_protection": "Schutz deaktiviert", "refresh_statics": "Statistiken aktualisieren", "dns_query": "DNS-Anfragen", - "blocked_by": "<0>Blockiert durch die Filter", - "stats_malware_phishing": "Blockierte Malware/Phishing", - "stats_adult": "Blockierte Webseiten für Erwachsene", + "blocked_by": "<0>Durch Filter gesperrt", + "stats_malware_phishing": "Gesperrte Schädliche/Phishing-Webseiten", + "stats_adult": "Gesperrte jugendgefährdende Webseiten", "stats_query_domain": "Am häufigsten angefragte Domains", "for_last_24_hours": "für die letzten 24 Stunden", "for_last_days": "am letzten {{count}} Tag", "for_last_days_plural": "in den letzten {{count}} Tage", "no_domains_found": "Keine Domains gefunden", "requests_count": "Anzahl der Anfragen", - "top_blocked_domains": "Am häufigsten blockierte Domains", + "top_blocked_domains": "Am häufigsten gesperrte Domains", "top_clients": "Top Clients", "no_clients_found": "Keine Clients gefunden", "general_statistics": "Allgemeine Statistiken", @@ -109,7 +110,7 @@ "number_of_dns_query_24_hours": "Anzahl der in den letzten 24 Stunden durchgeführten DNS-Anfragen", "number_of_dns_query_blocked_24_hours": "Anzahl der durch Werbefilter und Host-Blocklisten geblockten DNS-Anfragen", "number_of_dns_query_blocked_24_hours_by_sec": "Anzahl der durch das AdGuard-Modul „Internetsicherheit” gesperrten DNS-Anfragen", - "number_of_dns_query_blocked_24_hours_adult": "Anzahl der blockierten Webseiten für Erwachsene", + "number_of_dns_query_blocked_24_hours_adult": "Anzahl der gesperrten Webseiten mit jugendgefährdenden Inhalten", "enforced_save_search": "SafeSearch erzwungen", "number_of_dns_query_to_safe_search": "Anzahl der DNS-Anfragen bei denen SafeSearch für Suchanfragen erzwungen wurde", "average_processing_time": "Durchschnittliche Bearbeitungsdauer", @@ -247,10 +248,16 @@ "custom_ip": "Benutzerdefinierte IP", "blocking_ipv4": "IPv4-Sperren", "blocking_ipv6": "IPv6-Sperren", + "dnscrypt": "DNSCrypt", "dns_over_https": "DNS-over-HTTPS (DNS-Abrage über HTTPS)", "dns_over_tls": "DNS-over-TLS (DNS-Abrage über TLS)", + "dns_over_quic": "DNS-over-QUIC", + "client_id": "Client-ID", + "client_id_placeholder": "Client-ID eingeben", + "client_id_desc": "Verschiedene Clients können durch eine spezielle Client-ID identifiziert werden. Hier können Sie mehr darüber erfahren, wie Sie Clients identifizieren können.", "download_mobileconfig_doh": ".mobileconfig für DNS-über-HTTPS herunterladen", "download_mobileconfig_dot": ".mobileconfig für DNS-über-TLS herunterladen", + "download_mobileconfig": "Konfigurationsdatei herunterladen", "plain_dns": "Einfaches DNS", "form_enter_rate_limit": "Begrenzungswert eingeben", "rate_limit": "Begrenzungswert", @@ -269,7 +276,7 @@ "source_label": "Quelle", "found_in_known_domain_db": "In der Datenbank der bekannten Domains gefunden.", "category_label": "Kategorie", - "rule_label": "Regel", + "rule_label": "Regel(n)", "list_label": "Liste", "unknown_filter": "Unbekannter Filter {{filterId}}", "known_tracker": "Bekannte Tracker", @@ -430,6 +437,7 @@ "setup_dns_privacy_other_3": "<0>dnscrypt-proxy unterstützt <1>DNS-over-HTTPS.", "setup_dns_privacy_other_4": "<0>Mozilla Firefox unterstützt <1>DNS-over-HTTPS.", "setup_dns_privacy_other_5": "Weitere Umsetzungen finden Sie <0>hier und <1>hier.", + "setup_dns_privacy_ioc_mac": "Konfiguration für iOS und macOS", "setup_dns_notice": "Um <1>DNS-over-HTTTPS oder <1>DNS-over-TLS verwenden zu können, müssen Sie in den AdGuard Home Einstellungen die <0>Verschlüsselung konfigurieren.", "rewrite_added": "DNS-Umschreibung für „{{key}}” erfolgreich hinzugefügt", "rewrite_deleted": "DNS-Umschreibung für „{{key}}” erfolgreich entfernt", @@ -498,7 +506,7 @@ "descr": "Beschreibung", "whois": "Whois", "filtering_rules_learn_more": "<0>Erfahren Sie mehr über die Erstellung eigener Hosts-Listen.", - "blocked_by_response": "Nach CNAME oder IP-Antwort blockiert", + "blocked_by_response": "Gesperrt nach Antwort von CNAME oder IP", "blocked_by_cname_or_ip": "Gesperrt durch CNAME oder IP", "try_again": "Erneut versuchen", "domain_desc": "Geben Sie den Domain-Namen oder den Platzhalter ein, der umgeschrieben werden soll.", @@ -529,7 +537,6 @@ "check_ip": "IP-Adressen: {{ip}}", "check_cname": "CNAME: {{cname}}", "check_reason": "Grund: {{reason}}", - "check_rule": "Regel: {{rule}}", "check_service": "Dienstname: {{service}}", "service_name": "Name des Dienstes", "check_not_found": "Nicht in Ihren Filterlisten enthalten", diff --git a/client/src/__locales/en.json b/client/src/__locales/en.json index 388691b0..3b0aa663 100644 --- a/client/src/__locales/en.json +++ b/client/src/__locales/en.json @@ -32,6 +32,7 @@ "form_error_ip_format": "Invalid IP format", "form_error_mac_format": "Invalid MAC format", "form_error_client_id_format": "Invalid client ID format", + "form_error_server_name": "Invalid server name", "form_error_positive": "Must be greater than 0", "form_error_negative": "Must be equal to 0 or greater", "range_end_error": "Must be greater than range start", @@ -247,11 +248,16 @@ "custom_ip": "Custom IP", "blocking_ipv4": "Blocking IPv4", "blocking_ipv6": "Blocking IPv6", + "dnscrypt": "DNSCrypt", "dns_over_https": "DNS-over-HTTPS", "dns_over_tls": "DNS-over-TLS", "dns_over_quic": "DNS-over-QUIC", + "client_id": "Client ID", + "client_id_placeholder": "Enter client ID", + "client_id_desc": "Different clients can be identified by a special client ID. Here you can learn more about how to identify clients.", "download_mobileconfig_doh": "Download .mobileconfig for DNS-over-HTTPS", "download_mobileconfig_dot": "Download .mobileconfig for DNS-over-TLS", + "download_mobileconfig": "Download configuration file", "plain_dns": "Plain DNS", "form_enter_rate_limit": "Enter rate limit", "rate_limit": "Rate limit", @@ -270,7 +276,7 @@ "source_label": "Source", "found_in_known_domain_db": "Found in the known domains database.", "category_label": "Category", - "rule_label": "Rule", + "rule_label": "Rule(s)", "list_label": "List", "unknown_filter": "Unknown filter {{filterId}}", "known_tracker": "Known tracker", @@ -331,7 +337,7 @@ "encryption_config_saved": "Encryption config saved", "encryption_server": "Server name", "encryption_server_enter": "Enter your domain name", - "encryption_server_desc": "In order to use HTTPS, you need to enter the server name that matches your SSL certificate.", + "encryption_server_desc": "In order to use HTTPS, you need to enter the server name that matches your SSL certificate or wildcard certificate. If the field is not set, it will accept TLS connections for any domain.", "encryption_redirect": "Redirect to HTTPS automatically", "encryption_redirect_desc": "If checked, AdGuard Home will automatically redirect you from HTTP to HTTPS addresses.", "encryption_https": "HTTPS port", @@ -387,7 +393,7 @@ "client_edit": "Edit Client", "client_identifier": "Identifier", "ip_address": "IP address", - "client_identifier_desc": "Clients can be identified by the IP address, CIDR, MAC address. Please note that using MAC as identifier is possible only if AdGuard Home is also a <0>DHCP server", + "client_identifier_desc": "Clients can be identified by the IP address, CIDR, MAC address or a special client ID (can be used for DoT/DoH/DoQ). <0>Here you can learn more about how to identify clients.", "form_enter_ip": "Enter IP", "form_enter_mac": "Enter MAC", "form_enter_id": "Enter identifier", @@ -431,6 +437,7 @@ "setup_dns_privacy_other_3": "<0>dnscrypt-proxy supports <1>DNS-over-HTTPS.", "setup_dns_privacy_other_4": "<0>Mozilla Firefox supports <1>DNS-over-HTTPS.", "setup_dns_privacy_other_5": "You will find more implementations <0>here and <1>here.", + "setup_dns_privacy_ioc_mac": "iOS and macOS configuration", "setup_dns_notice": "In order to use <1>DNS-over-HTTPS or <1>DNS-over-TLS, you need to <0>configure Encryption in AdGuard Home settings.", "rewrite_added": "DNS rewrite for \"{{key}}\" successfully added", "rewrite_deleted": "DNS rewrite for \"{{key}}\" successfully deleted", @@ -530,7 +537,6 @@ "check_ip": "IP addresses: {{ip}}", "check_cname": "CNAME: {{cname}}", "check_reason": "Reason: {{reason}}", - "check_rule": "Rule: {{rule}}", "check_service": "Service name: {{service}}", "service_name": "Service name", "check_not_found": "Not found in your filter lists", @@ -588,4 +594,4 @@ "adg_will_drop_dns_queries": "AdGuard Home will be dropping all DNS queries from this client.", "client_not_in_allowed_clients": "The client is not allowed because it is not in the \"Allowed clients\" list.", "experimental": "Experimental" -} +} \ No newline at end of file diff --git a/client/src/__locales/es.json b/client/src/__locales/es.json index 9035b6d0..cf0d7d02 100644 --- a/client/src/__locales/es.json +++ b/client/src/__locales/es.json @@ -32,6 +32,7 @@ "form_error_ip_format": "Formato IP no válido", "form_error_mac_format": "Formato MAC no válido", "form_error_client_id_format": "Formato de ID de cliente no válido", + "form_error_server_name": "Nombre de servidor no válido", "form_error_positive": "Debe ser mayor que 0", "form_error_negative": "Debe ser igual o mayor que 0", "range_end_error": "Debe ser mayor que el inicio de rango", @@ -50,7 +51,7 @@ "dhcp_table_expires": "Expira", "dhcp_warning": "Si de todos modos deseas habilitar el servidor DHCP, asegúrate de que no hay otro servidor DHCP activo en tu red. ¡De lo contrario, puedes dejar sin Internet a los dispositivos conectados!", "dhcp_error": "No pudimos determinar si hay otro servidor DHCP en la red.", - "dhcp_static_ip_error": "Para poder utilizar el servidor DHCP se debe establecer una dirección IP estática. No hemos podido determinar si esta interfaz de red está configurada utilizando una dirección IP estática. Por favor establezca una dirección IP estática manualmente.", + "dhcp_static_ip_error": "Para poder utilizar el servidor DHCP se debe establecer una dirección IP estática. No hemos podido determinar si esta interfaz de red está configurada utilizando una dirección IP estática. Por favor establece una dirección IP estática manualmente.", "dhcp_dynamic_ip_found": "Tu sistema utiliza la configuración de dirección IP dinámica para la interfaz <0>{{interfaceName}}. Para poder utilizar el servidor DHCP se debe establecer una dirección IP estática. Tu dirección IP actual es <0>{{ipAddress}}. Si presionas el botón Habilitar servidor DHCP, estableceremos automáticamente esta dirección IP como estática.", "dhcp_lease_added": "Asignación estática \"{{key}}\" añadido correctamente", "dhcp_lease_deleted": "Asignación estática \"{{key}}\" eliminado correctamente", @@ -247,10 +248,16 @@ "custom_ip": "IP personalizada", "blocking_ipv4": "Bloqueo de IPv4", "blocking_ipv6": "Bloqueo de IPv6", + "dnscrypt": "DNSCrypt", "dns_over_https": "DNS mediante HTTPS", "dns_over_tls": "DNS mediante TLS", + "dns_over_quic": "DNS mediante QUIC", + "client_id": "ID de cliente", + "client_id_placeholder": "Ingresa el ID del cliente", + "client_id_desc": "Diferentes clientes pueden ser identificados por un ID de cliente especial. Aquí puedes obtener más información sobre cómo identificar clientes.", "download_mobileconfig_doh": "Descargar .mobileconfig para DNS mediante HTTPS", "download_mobileconfig_dot": "Descargar .mobileconfig para DNS mediante TLS", + "download_mobileconfig": "Descargar archivo de configuración", "plain_dns": "DNS simple", "form_enter_rate_limit": "Ingresa el límite de cantidad", "rate_limit": "Límite de cantidad", @@ -294,7 +301,7 @@ "install_devices_title": "Configura tus dispositivos", "install_devices_desc": "Para comenzar a utilizar AdGuard Home, debes configurar tus dispositivos para usarlo.", "install_submit_title": "¡Felicitaciones!", - "install_submit_desc": "El proceso de configuración ha finalizado y está listo para comenzar a usar AdGuard Home.", + "install_submit_desc": "El proceso de configuración ha finalizado y estás listo para comenzar a usar AdGuard Home.", "install_devices_router": "Router", "install_devices_router_desc": "Esta configuración cubrirá automáticamente todos los dispositivos conectados a tu router doméstico y no necesitarás configurar cada uno de ellos manualmente.", "install_devices_address": "El servidor DNS de AdGuard Home está escuchando en las siguientes direcciones", @@ -330,7 +337,7 @@ "encryption_config_saved": "Configuración de cifrado guardado", "encryption_server": "Nombre del servidor", "encryption_server_enter": "Ingresa el nombre del dominio", - "encryption_server_desc": "Para utilizar HTTPS, debes ingresar el nombre del servidor que coincida con tu certificado SSL.", + "encryption_server_desc": "Para utilizar HTTPS, debes ingresar el nombre del servidor que coincida con tu certificado SSL o certificado comodín. Si el campo no está establecido, el servidor aceptará conexiones TLS para cualquier dominio.", "encryption_redirect": "Redireccionar a HTTPS automáticamente", "encryption_redirect_desc": "Si está marcado, AdGuard Home redireccionará automáticamente de HTTP a las direcciones HTTPS.", "encryption_https": "Puerto HTTPS", @@ -386,7 +393,7 @@ "client_edit": "Editar cliente", "client_identifier": "Identificador", "ip_address": "Dirección IP", - "client_identifier_desc": "Los clientes pueden ser identificados por la dirección IP, MAC y CIDR. Ten en cuenta que el uso de MAC como identificador solo es posible si AdGuard Home también es un <0>servidor DHCP", + "client_identifier_desc": "Los clientes pueden ser identificados por la dirección IP, MAC, CIDR o un ID de cliente especial (puede ser utilizado para DoT/DoH/DoQ). <0>Aquí puedes obtener más información sobre cómo identificar clientes.", "form_enter_ip": "Ingresa la IP", "form_enter_mac": "Ingresa la MAC", "form_enter_id": "Ingresa el identificador", @@ -430,6 +437,7 @@ "setup_dns_privacy_other_3": "<0>dnscrypt-proxy soporta <1>DNS mediante HTTPS.", "setup_dns_privacy_other_4": "<0>Mozilla Firefox soporta <1>DNS mediante HTTPS.", "setup_dns_privacy_other_5": "Encontrarás más implementaciones <0>aquí y <1>aquí.", + "setup_dns_privacy_ioc_mac": "Configuración de iOS y macOS", "setup_dns_notice": "Para utilizar <1>DNS mediante HTTPS o <1>DNS mediante TLS, debes <0>configurar el cifrado en la configuración de AdGuard Home.", "rewrite_added": "Reescritura DNS para \"{{key}}\" añadido correctamente", "rewrite_deleted": "Reescritura DNS para \"{{key}}\" eliminado correctamente", @@ -529,7 +537,6 @@ "check_ip": "Direcciones IP: {{ip}}", "check_cname": "CNAME: {{cname}}", "check_reason": "Razón: {{reason}}", - "check_rule": "Regla: {{rule}}", "check_service": "Nombre del servicio: {{service}}", "service_name": "Nombre del servicio", "check_not_found": "No se ha encontrado en tus listas de filtros", diff --git a/client/src/__locales/fa.json b/client/src/__locales/fa.json index 105ccddd..af9ab9c6 100644 --- a/client/src/__locales/fa.json +++ b/client/src/__locales/fa.json @@ -239,7 +239,6 @@ "source_label": "منبع", "found_in_known_domain_db": "در پایگاه داده دامنه های شناخته شده پیدا شد", "category_label": "دسته بندی", - "rule_label": "دستور", "list_label": "لیست", "unknown_filter": "فیلتر ناشناخته {{filterId}}", "known_tracker": "ردیاب های شناخته شده", @@ -300,7 +299,6 @@ "encryption_config_saved": "پیکربندی رمزگذاری ذخیره شد", "encryption_server": "نام سرور", "encryption_server_enter": "نام دامنه خود را وارد کنید", - "encryption_server_desc": "به منظور استفاده از HTTPS،شما باید نام سرور مطابق با گواهینامه اِس اِس اِل را وارد کنید.", "encryption_redirect": "تغییر مسیر خودکار به HTTPS", "encryption_redirect_desc": "اگر انتخاب شده باشد،AdGuard Home خودکار شما را از آدرس HTTP به HTTPS منتقل می کند", "encryption_https": "پورت HTTPS", @@ -354,7 +352,6 @@ "client_edit": "ویرایش کلاینت", "client_identifier": "احراز با", "ip_address": "آدرس آی پی", - "client_identifier_desc": "کلاینت میتواند با آدرس آی پی یا آدرس مَک احراز شود. لطفا توجه کنید،که استفاده از مَک بعنوان عامل احراز زمانی امکان دارد که AdGuard Home نیز <0>سرور DHCP باشد", "form_enter_ip": "آی پی را وارد کنید", "form_enter_mac": "مَک را وارد کنید", "form_enter_id": "خطای احرازکننده", @@ -487,7 +484,6 @@ "check_ip": "آدرس آی پی: {{ip}}", "check_cname": "CNAME: {{cname}}", "check_reason": "علت: {{reason}}", - "check_rule": "دستور: {{rule}}", "check_service": "نام سرویس: {{service}}", "check_not_found": "در لیست فیلترهای شما یافت نشد", "client_confirm_block": "آیا واقعا میخواهید کلاینت \"{{ip}}\" را مسدود کنید؟", diff --git a/client/src/__locales/fr.json b/client/src/__locales/fr.json index fa636342..064f4ccc 100644 --- a/client/src/__locales/fr.json +++ b/client/src/__locales/fr.json @@ -22,7 +22,7 @@ "dhcp_found": "Il y a plusieurs serveurs DHCP actifs sur le réseau. Ce n'est pas prudent d'activer le serveur DHCP intégré en ce moment.", "dhcp_leases": "Locations des serveurs DHCP", "dhcp_static_leases": "Baux statiques DHCP", - "dhcp_leases_not_found": "Aucune location des serveurs DHCP trouvée", + "dhcp_leases_not_found": "Aucun bail DHCP trouvé", "dhcp_config_saved": "La configuration du serveur DHCP est sauvegardée", "dhcp_ipv4_settings": "Paramètres IPv4 du DHCP", "dhcp_ipv6_settings": "Paramètres IPv6 du DHCP", @@ -32,6 +32,7 @@ "form_error_ip_format": "Format IPv4 invalide", "form_error_mac_format": "Format MAC invalide", "form_error_client_id_format": "Format d'ID client non valide", + "form_error_server_name": "Nom de serveur invalide", "form_error_positive": "Doit être supérieur à 0", "form_error_negative": "Doit être égal à 0 ou supérieur", "range_end_error": "Doit être supérieur au début de la gamme", @@ -92,7 +93,7 @@ "refresh_statics": "Renouveler les statistiques", "dns_query": "Requêtes DNS", "blocked_by": "<0>Bloqué par Filtres", - "stats_malware_phishing": "Tentative de malware/hammeçonnage bloquée", + "stats_malware_phishing": "Tentative de malware/hameçonnage bloquée", "stats_adult": "Sites à contenu adulte bloqués", "stats_query_domain": "Domaines les plus recherchés", "for_last_24_hours": "pendant les dernières 24 heures", @@ -178,13 +179,13 @@ "custom_filter_rules": "Règles de filtrage d'utilisateur", "custom_filter_rules_hint": "Saisissez la règle en une ligne. C'est possible d'utiliser les règles de blocage ou la syntaxe des fichiers hosts.", "examples_title": "Exemples", - "example_meaning_filter_block": "bloquer l'accés au domaine exemple.org et à tous ses sous-domaines", - "example_meaning_filter_whitelist": "débloquer l'accés au domaine exemple.org et à tous ses sous-domaines", + "example_meaning_filter_block": "bloque l’accès au domaine example.org et à tous ses sous-domaines", + "example_meaning_filter_whitelist": "débloque l’accès au domaine example.org et à tous ses sous-domaines", "example_meaning_host_block": "AdGuard Home va retourner l'adresse 127.0.0.1 au domaine example.org (mais pas aux sous-domaines).", "example_comment": "! Voici comment ajouter une déscription", "example_comment_meaning": "commentaire", "example_comment_hash": "# Et comme ça aussi on peut laisser des commentaires", - "example_regex_meaning": "bloquer l'accés aux domaines correspondants à l'expression régulière spécifiée", + "example_regex_meaning": "bloque l’accès aux domaines correspondants à l'expression régulière spécifiée", "example_upstream_regular": "DNS classique (au-dessus de UDP)", "example_upstream_dot": "<0>DNS-over-TLS chiffré", "example_upstream_doh": "<0>DNS-over-HTTPS chiffré", @@ -247,10 +248,16 @@ "custom_ip": "IP personnalisée", "blocking_ipv4": "Blocage IPv4", "blocking_ipv6": "Blocage IPv6", + "dnscrypt": "DNSCrypt", "dns_over_https": "DNS-over-HTTPS", "dns_over_tls": "DNS-over-TLS", + "dns_over_quic": "DNS-over-QUIC", + "client_id": "ID du client", + "client_id_placeholder": "Saisissez le ID du client", + "client_id_desc": "Les clients différents peuvent être identifiés par aide d'un ID client spécial. Vous trouverez plus d'information sur l'identification des clients ici .", "download_mobileconfig_doh": "Télécharger .mobileconfig pour DNS-sur-HTTPS", "download_mobileconfig_dot": "Télécharger .mobileconfig pour DNS-sur-TLS", + "download_mobileconfig": "Télécharger le fichier de configuration", "plain_dns": "DNS brut", "form_enter_rate_limit": "Entrez la limite de taux", "rate_limit": "Limite de taux", @@ -269,7 +276,7 @@ "source_label": "Source", "found_in_known_domain_db": "Trouvé dans la base de données des domaines connus", "category_label": "Catégorie", - "rule_label": "Règle", + "rule_label": "Règle(s)", "list_label": "Liste", "unknown_filter": "Filtre inconnu {{filterId}}", "known_tracker": "Pisteur connu", @@ -330,7 +337,7 @@ "encryption_config_saved": "Configuration de chiffrement enregistrée", "encryption_server": "Nom du serveur", "encryption_server_enter": "Entrez votre nom de domaine", - "encryption_server_desc": "Pour utiliser HTTPS, vous devez entrer le nom du serveur qui correspond à votre certificat SSL.", + "encryption_server_desc": "Pour utiliser HTTPS, vous devez saisir le nom du serveur qui correspond à votre certificat SSL ou wildcard. Si le champ n'est pas configuré, les connexions TLS pour tous les domaines seront acceptées.", "encryption_redirect": "Redirection automatiquement vers HTTPS", "encryption_redirect_desc": "Si coché, AdGuard Home vous redirigera automatiquement d'adresses HTTP vers HTTPS.", "encryption_https": "Port HTTPS", @@ -386,7 +393,7 @@ "client_edit": "Modifier le client", "client_identifier": "Identifiant", "ip_address": "Adresse IP", - "client_identifier_desc": "Les clients peuvent être identifiés par les adresses IP ou MAC. Veuillez noter que l'utilisation de l'adresse MAC comme identifiant est possible uniquement si AdGuard Home est aussi un <0>serveur DHCP", + "client_identifier_desc": "Les clients peuvent être identifiés par les adresses IP, CIDR, MAC ou un ID client spécial (qui peut être utilisé pour DoT/DoH/DoQ). Vous trouverez plus d'information sur l'identification des clients <0>ici .", "form_enter_ip": "Saisissez l'IP", "form_enter_mac": "Saisissez MAC", "form_enter_id": "Entrer identifiant", @@ -430,7 +437,8 @@ "setup_dns_privacy_other_3": "<0>dnscrypt-proxy supporte le <1>DNS-over-HTTPS.", "setup_dns_privacy_other_4": "<0>Mozilla Firefox supporte le <1>DNS-over-HTTPS.", "setup_dns_privacy_other_5": "Vous trouverez plus d'implémentations <0>ici et <1>ici.", - "setup_dns_notice": "Pour utiliser le <1>DNS-over-HTTPS ou le <1>DNS-over-TLS, vous devez <0>configurer le Cryptage dans les paramètres de AdGuard Home.", + "setup_dns_privacy_ioc_mac": "Configuration sur iOS et macOS", + "setup_dns_notice": "Pour utiliser le <1>DNS-over-HTTPS ou le <1>DNS-over-TLS, vous devez <0>configurer le Chiffrement dans les paramètres de AdGuard Home.", "rewrite_added": "Réécriture DNS pour \"{{key}}\" ajoutée", "rewrite_deleted": "Réécriture DNS pour \"{{key}}\" supprimée", "rewrite_add": "Ajouter une réécriture DNS", @@ -529,7 +537,6 @@ "check_ip": "Adresses IP : {{ip}}", "check_cname": "CNAME : {{cname}}", "check_reason": "Raison : {{reason}}", - "check_rule": "Règle : {{rule}}", "check_service": "Nom du service : {{service}}", "service_name": "Nom du service", "check_not_found": "Introuvable dans vos listes de filtres", diff --git a/client/src/__locales/hr.json b/client/src/__locales/hr.json index baab39ed..ab5567f0 100644 --- a/client/src/__locales/hr.json +++ b/client/src/__locales/hr.json @@ -32,6 +32,7 @@ "form_error_ip_format": "Nevažeći format IP adrese", "form_error_mac_format": "Nevažeći MAC format", "form_error_client_id_format": "Nevažeći format ID-a klijenta", + "form_error_server_name": "Nevažeće ime poslužitelja", "form_error_positive": "Mora biti veće od 0", "form_error_negative": "Mora biti jednako ili veće od 0", "range_end_error": "Mora biti veće od početne vrijednosti raspona", @@ -247,10 +248,16 @@ "custom_ip": "Prilagođen IP", "blocking_ipv4": "Blokiranje IPv4", "blocking_ipv6": "Blokiranje IPv6", + "dnscrypt": "DNSCrypt", "dns_over_https": "DNS-over-HTTPS", "dns_over_tls": "DNS-over-TLS", + "dns_over_quic": "DNS-over-Quic", + "client_id": "ID klijenta", + "client_id_placeholder": "Unesite ID klijenta", + "client_id_desc": "Razni klijenti mogu biti prepoznati po specijalnom identifikatoru. Ovdje možete saznati više kako možete identificirati klijente.", "download_mobileconfig_doh": "Preuzmi .mobileconfig za DNS-over-HTTPS", "download_mobileconfig_dot": "Preuzmi .mobileconfig za DNS-over-TLS", + "download_mobileconfig": "Preuzmite konfiguracijsku datoteku", "plain_dns": "Obični DNS", "form_enter_rate_limit": "Unesite ograničenje", "rate_limit": "Ograničenje", @@ -430,6 +437,7 @@ "setup_dns_privacy_other_3": "<0>dnscrypt-proxy podržava <1>DNS-over-HTTPS.", "setup_dns_privacy_other_4": "<0>Mozilla Firefox podržava <1>DNS-over-HTTPS.", "setup_dns_privacy_other_5": "Možete pronaći više implementacija <0>ovdje i <1>ovdje.", + "setup_dns_privacy_ioc_mac": "konfiguracija za iOS i macOS", "setup_dns_notice": "Da biste koristili <1>DNS-over-HTTPS ili <1>DNS-over-TLS, morate <0>postaviti šifriranje u AdGuard Home postavkama.", "rewrite_added": "DNS prijepis za \"{{key}}\" je uspješno dodan", "rewrite_deleted": "DNS prijepis za \"{{key}}\" je uspješno uklonjen", @@ -529,7 +537,6 @@ "check_ip": "IP adrese: {{ip}}", "check_cname": "CNAME: {{cname}}", "check_reason": "Razlog: {{reason}}", - "check_rule": "Pravilo: {{rule}}", "check_service": "Naziv usluge: {{service}}", "service_name": "Naziv usluge", "check_not_found": "Nije pronađeno na vašoj listi filtara", diff --git a/client/src/__locales/hu.json b/client/src/__locales/hu.json index 5899e30a..73b31907 100644 --- a/client/src/__locales/hu.json +++ b/client/src/__locales/hu.json @@ -88,7 +88,7 @@ "enable_protection": "Védelem engedélyezése", "enabled_protection": "Védelem engedélyezve", "disable_protection": "Védelem letiltása", - "disabled_protection": "Letiltott védelem", + "disabled_protection": "Védelem letiltva", "refresh_statics": "Statisztikák frissítése", "dns_query": "DNS lekérdezés", "blocked_by": "<0>Szűrők által blokkolt", @@ -269,7 +269,6 @@ "source_label": "Forrás", "found_in_known_domain_db": "Benne van az ismert domainek listájában.", "category_label": "Kategória", - "rule_label": "Szabály", "list_label": "Lista", "unknown_filter": "Ismeretlen szűrő: {{filterId}}", "known_tracker": "Ismert követő", @@ -330,7 +329,6 @@ "encryption_config_saved": "Titkosítási beállítások mentve", "encryption_server": "Szerver neve", "encryption_server_enter": "Adja meg az Ön domain címét", - "encryption_server_desc": "A HTTPS használatához be kell írnia egy, az SSL-tanúsítvánnyal megegyező kiszolgálónevet.", "encryption_redirect": "Automatikus átirányítás HTTPS kapcsolatra", "encryption_redirect_desc": "Ha be van jelölve, az AdGuard Home automatikusan átirányítja a HTTP kapcsolatokat a biztonságos HTTPS protokollra.", "encryption_https": "HTTPS port", @@ -386,7 +384,6 @@ "client_edit": "Kliens módosítása", "client_identifier": "Azonosító", "ip_address": "IP cím", - "client_identifier_desc": "A klienseket be lehet azonosítani IP-cím, CIDR, valamint MAC-cím alapján. Kérjük, vegye figyelembe, hogy a MAC-cím alapján történő azonosítás csak akkor működik, ha az AdGuard Home egyben <0>DHCP szerverként is funkcionál", "form_enter_ip": "IP-cím megadása", "form_enter_mac": "MAC-cím megadása", "form_enter_id": "Azonosító megadása", @@ -409,7 +406,7 @@ "access_disallowed_title": "Nem engedélyezett kliensek", "access_disallowed_desc": "A CIDR vagy IP címek listája. Ha konfigurálva van, az AdGuard Home eldobja a lekérdezéseket ezekről az IP-címekről.", "access_blocked_title": "Nem engedélyezett domainek", - "access_blocked_desc": "Ne keverje össze ezt a szűrőkkel. Az AdGuard Home az összes DNS kérést el fogja dobni, ami ezekkel a domainekkel kapcsolatos. Itt megadhatja a pontos domainneveket, a helyettesítő karaktereket és az urlfilter-szabályokat, pl. 'example.org', '*.example.org' or '||example.org^'.", + "access_blocked_desc": "Ne keverje össze ezt a szűrőkkel. Az AdGuard Home az összes DNS kérést el fogja dobni, ami ezekkel a domainekkel kapcsolatos. Itt megadhatja a pontos domainneveket, a helyettesítő karaktereket és az urlfilter-szabályokat, pl. 'example.org', '*.example.org' vagy '||example.org^'.", "access_settings_saved": "A hozzáférési beállítások sikeresen mentésre kerültek", "updates_checked": "A frissítések sikeresen ellenőrizve lettek", "updates_version_equal": "Az AdGuard Home naprakész", @@ -529,7 +526,6 @@ "check_ip": "IP-címek: {{ip}}", "check_cname": "CNAME: {{cname}}", "check_reason": "Indok: {{reason}}", - "check_rule": "Szabály: {{rule}}", "check_service": "Szolgáltatás neve: {{service}}", "service_name": "Szolgáltatás neve", "check_not_found": "Nem található az Ön szűrőlistái között", diff --git a/client/src/__locales/id.json b/client/src/__locales/id.json index 6d0a3d39..2bd91ea8 100644 --- a/client/src/__locales/id.json +++ b/client/src/__locales/id.json @@ -269,7 +269,6 @@ "source_label": "Sumber", "found_in_known_domain_db": "Ditemukan di database domain dikenal", "category_label": "Kategori", - "rule_label": "Aturan", "list_label": "Daftar", "unknown_filter": "Penyaringan {{filterId}} tidak dikenal", "known_tracker": "Pelacak yang dikenal", @@ -330,7 +329,6 @@ "encryption_config_saved": "Pengaturan enkripsi telah tersimpan", "encryption_server": "Nama server", "encryption_server_enter": "Masukkan nama domain anda", - "encryption_server_desc": "Untuk menggunakan HTTPS, Anda harus memasukkan nama server yang cocok dengan sertifikat SSL Anda.", "encryption_redirect": "Alihkan ke HTTPS secara otomatis", "encryption_redirect_desc": "Jika dicentang, AdGuard Home akan secara otomatis mengarahkan anda dari HTTP ke alamat HTTPS.", "encryption_https": "Port HTTPS", @@ -386,7 +384,6 @@ "client_edit": "Ubah Klien", "client_identifier": "Identifikasi", "ip_address": "Alamat IP", - "client_identifier_desc": "Klien dapat diidentifikasi dengan alamat IP atau alamat MAC. Harap dicatat bahwa menggunakan MAC sebagai pengidentifikasi hanya dimungkinkan jika AdGuard Home juga merupakan <0>server DHCP", "form_enter_ip": "Masukkan IP", "form_enter_mac": "Masukkan MAC", "form_enter_id": "Masukkan pengenal", @@ -529,7 +526,6 @@ "check_ip": "Alamat IP: {{ip}}", "check_cname": "CNAME: {{cname}}", "check_reason": "Alasan: {{reason}}", - "check_rule": "Aturan: {{rule}}", "check_service": "Nama layanan: {{service}}", "service_name": "Nama layanan", "check_not_found": "Tidak di temukan di daftar penyaringan anda", diff --git a/client/src/__locales/it.json b/client/src/__locales/it.json index 05827058..1f3a0207 100644 --- a/client/src/__locales/it.json +++ b/client/src/__locales/it.json @@ -32,6 +32,7 @@ "form_error_ip_format": "Formato IPv4 non valido", "form_error_mac_format": "Formato MAC non valido", "form_error_client_id_format": "Formato ID cliente non valido", + "form_error_server_name": "Nome server non valido", "form_error_positive": "Deve essere maggiore di 0", "form_error_negative": "Deve essere maggiore o uguale a 0 (zero)", "range_end_error": "Deve essere maggiore dell'intervallo di inizio", @@ -186,10 +187,10 @@ "example_comment_hash": "# Un altro commento", "example_regex_meaning": "blocca l'accesso ai domini che corrispondono alla specifica espressione regolare", "example_upstream_regular": "DNS regolari (via UDP)", - "example_upstream_dot": "<0>DNS_over_TLS crittografato", - "example_upstream_doh": "<0>DNS-over-HTTPS crittografato", - "example_upstream_doq": "<0>DNS_over_QUIC crittografato", - "example_upstream_sdns": "puoi utilizzare <0>DNS Stamps per <1>DNSCrypt oppure dei resolver con <2>DNS-over-HTTPS", + "example_upstream_dot": "<0>DNS su TLS crittografato", + "example_upstream_doh": "<0>DNS su HTTPS crittografato", + "example_upstream_doq": "<0>DNS su QUIC crittografato", + "example_upstream_sdns": "puoi utilizzare <0>DNS Stamps per <1>DNSCrypt oppure dei resolver con <2>DNS su HTTPS", "example_upstream_tcp": "DNS regolari (via TCP)", "all_lists_up_to_date_toast": "Tutte le liste sono aggiornate", "updated_upstream_dns_toast": "Server DNS upstream aggiornati", @@ -247,10 +248,16 @@ "custom_ip": "IP personalizzato", "blocking_ipv4": "Blocca IPv4", "blocking_ipv6": "Blocca IPv6", - "dns_over_https": "DNS-over-HTTPS", - "dns_over_tls": "DNS-over-TLS", - "download_mobileconfig_doh": "Scarica .mobileconfig per DNS-over-HTTPS", - "download_mobileconfig_dot": "Scarica .mobileconfig per DNS-over-TLS", + "dnscrypt": "DNSCrypt", + "dns_over_https": "DNS su HTTPS", + "dns_over_tls": "DNS su TLS", + "dns_over_quic": "DNS su Quic", + "client_id": "ID client", + "client_id_placeholder": "Inserisci ID client", + "client_id_desc": "Client differenti possono essere identificati da uno speciale ID. Qui potrai saperne di più sui metodi per identificarli.", + "download_mobileconfig_doh": "Scarica .mobileconfig per DNS su HTTPS", + "download_mobileconfig_dot": "Scarica .mobileconfig per DNS su TLS", + "download_mobileconfig": "Scarica file di configurazione", "plain_dns": "DNS semplice", "form_enter_rate_limit": "Imposta limite delle richieste", "rate_limit": "Limite delle richieste", @@ -269,7 +276,7 @@ "source_label": "Fonte", "found_in_known_domain_db": "Trovato nel database dei domini conosciuti.", "category_label": "Categoria", - "rule_label": "Regola", + "rule_label": "Regola(e)", "list_label": "Lista", "unknown_filter": "Filtro sconosciuto {{filterId}}", "known_tracker": "Tracker conosciuto", @@ -330,14 +337,14 @@ "encryption_config_saved": "Configurazione della crittografia salvata", "encryption_server": "Nome server", "encryption_server_enter": "Inserisci il tuo nome di dominio", - "encryption_server_desc": "Per utilizzare HTTPS, è necessario inserire il nome del server che corrisponde al certificato SSL.", + "encryption_server_desc": "Per utilizzare HTTPS, è necessario immettere il nome del server che corrisponde al certificato SSL o al certificato wildcard. Se il campo risulterà vuoto, accetterà connessioni TLS per qualsiasi dominio.", "encryption_redirect": "Reindirizza automaticamente a HTTPS", "encryption_redirect_desc": "Se selezionato, AdGuard Home ti reindirizzerà automaticamente da indirizzi HTTP a HTTPS.", "encryption_https": "Porta HTTPS", - "encryption_https_desc": "Se la porta HTTPS è configurata, l'interfaccia di amministrazione di AdGuard Home sarà accessibile tramite HTTPS e fornirà anche DNS-over-HTTPS nella posizione \"/ dns-query\".", - "encryption_dot": "DNS-su porta-TLS", - "encryption_dot_desc": "Se questa porta è configurata, AdGuard Home eseguirà un server DNS-over-TLS su questa porta.", - "encryption_doq": "DNS-su porta-QUIC", + "encryption_https_desc": "Se la porta HTTPS è configurata, l'interfaccia di amministrazione di AdGuard Home sarà accessibile tramite HTTPS e fornirà anche DNS su HTTPS nella posizione \"/ dns-query\".", + "encryption_dot": "DNS su porta TLS", + "encryption_dot_desc": "Se questa porta è configurata, AdGuard Home eseguirà un server DNS su TLS su questa porta.", + "encryption_doq": "DNS su porta QUIC", "encryption_doq_desc": "Se questa porta è configurata, AdGuard Home eseguirà un server DNS su porta QUIC. Questa opzione è sperimentale e potrebbe non risultare affidabile. Inoltre, al momento non sono molti i client a supportarla.", "encryption_certificates": "Certificati", "encryption_certificates_desc": "Per utilizzare la crittografia, è necessario fornire una catena di certificati SSL valida per il proprio dominio. Puoi ottenere un certificato gratuito su <0> {{link}} o puoi acquistarlo da una delle Autorità di certificazione attendibili.", @@ -346,8 +353,8 @@ "encryption_expire": "Scaduto", "encryption_key": "Chiave privata", "encryption_key_input": "Copia/Incolla qui la tua chiave privata codificata PEM per il tuo certificato.", - "encryption_enable": "Attiva crittografia (HTTPS, DNS-su-HTTPS e DNS-su-TLS)", - "encryption_enable_desc": "Se la crittografia è attiva, l'interfaccia di amministrazione di AdGuard Home funzionerà su HTTPS e il server DNS ascolterà le richieste su DNS-over-HTTPS e DNS-over-TLS.", + "encryption_enable": "Attiva crittografia (HTTPS, DNS su HTTPS e DNS su TLS)", + "encryption_enable_desc": "Se la crittografia è attiva, l'interfaccia di amministrazione di AdGuard Home funzionerà su HTTPS e il server DNS ascolterà le richieste su DNS su HTTPS e DNS su TLS.", "encryption_chain_valid": "La catena di certificati è valida", "encryption_chain_invalid": "La catena di certificati non è valida", "encryption_key_valid": "Questa è una chiave privata {{type}} valida", @@ -386,7 +393,7 @@ "client_edit": "Modifica Client", "client_identifier": "Identificatore", "ip_address": "Indirizzo IP", - "client_identifier_desc": "I client possono essere identificati dall indirizzo IP o dall' indirizzo MAC. Nota che l' utilizzo dell' indirizzo MAC come identificatore è consentito solo se AdGuard Home è anche il <0>server DHCP", + "client_identifier_desc": "I client possono essere identificati dall'indirizzo IP, CIDR, indirizzo MAC o un ID speciale (che può essere utilizzato per DoT/DoH/DoQ). <0>Qui potrai saperne di più sui metodi per identificarli.", "form_enter_ip": "Inserisci IP", "form_enter_mac": "Inserisci MAC", "form_enter_id": "Inserisci identificatore", @@ -415,22 +422,23 @@ "updates_version_equal": "AdGuard Home è aggiornato", "check_updates_now": "Controlla aggiornamenti adesso", "dns_privacy": "Privacy DNS", - "setup_dns_privacy_1": "<0>DNS-over-TLS: Utilizza la stringa <1>{{address}}.", - "setup_dns_privacy_2": "<0>DNS-over-HTTPS: Utilizza la stringa <1>{{address}}.", + "setup_dns_privacy_1": "<0>DNS su TLS: Utilizza la stringa <1>{{address}}.", + "setup_dns_privacy_2": "<0>DNS su HTTPS: Utilizza la stringa <1>{{address}}.", "setup_dns_privacy_3": "<0>Ecco un elenco di software che è possibile utilizzare.", - "setup_dns_privacy_4": "Si usa un dispositivo iOS 14 o macOS Big Sur puoi scaricare uno file speciale.mobileconfig' che aggiunge i server DNS-over-HTTPS or DNS-over-TLS alle configurazioni DNS.", - "setup_dns_privacy_android_1": "Android 9 supporta DNS-over-TLS in modo nativo. Per configurarlo, vai su Impostazioni → Rete e Internet → Avanzate → DNS privato e inserisci qui il tuo nome di dominio.", - "setup_dns_privacy_android_2": "<0>AdGuard per Android supporta <1>DNS-over-HTTPS e <1>DNS-over-TLS.", - "setup_dns_privacy_android_3": "<0>Intra aggiunge <1>DNS-over-HTTPS il supporto ad Android.", - "setup_dns_privacy_ios_1": "<0>DNSCloak supporta <1>DNS-over-HTTPS, ma per configurarlo per l'utilizzo del proprio server, è necessario generare un <2> DNS Stamp apposito.", - "setup_dns_privacy_ios_2": "<0>AdGuard per iOSsupporta l'impostazione <1>DNS-over-HTTPS e <1>DNS-over-TLS.", + "setup_dns_privacy_4": "Si usa un dispositivo iOS 14 o macOS Big Sur puoi scaricare uno file speciale.mobileconfig' che aggiunge i server DNS su HTTPS or DNS su TLS alle configurazioni DNS.", + "setup_dns_privacy_android_1": "Android 9 supporta DNS su TLS in modo nativo. Per configurarlo, vai su Impostazioni → Rete e Internet → Avanzate → DNS privato e inserisci qui il tuo nome di dominio.", + "setup_dns_privacy_android_2": "<0>AdGuard per Android supporta <1>DNS su HTTPS e <1>DNS su TLS.", + "setup_dns_privacy_android_3": "<0>Intra aggiunge <1>DNS su HTTPS il supporto ad Android.", + "setup_dns_privacy_ios_1": "<0>DNSCloak supporta <1>DNS su HTTPS, ma per configurarlo per l'utilizzo del proprio server, è necessario generare un <2> DNS Stamp apposito.", + "setup_dns_privacy_ios_2": "<0>AdGuard per iOSsupporta l'impostazione <1>DNS su HTTPS e <1>DNS su TLS.", "setup_dns_privacy_other_title": "Altre implementazion", "setup_dns_privacy_other_1": "AdGuard Home può essere un client DNS sicuro su qualsiasi piattaforma.", "setup_dns_privacy_other_2": "<0>dnsproxy supporta tutti i protocolli DNS sicuri noti.", - "setup_dns_privacy_other_3": "<0>dnscrypt-proxy supporta <1>DNS-over-HTTPS.", - "setup_dns_privacy_other_4": "<0>Mozilla Firefox supporta <1>DNS-over-HTTPS.", + "setup_dns_privacy_other_3": "<0>dnscrypt-proxy supporta <1>DNS su HTTPS.", + "setup_dns_privacy_other_4": "<0>Mozilla Firefox supporta <1>DNS su HTTPS.", "setup_dns_privacy_other_5": "Troverai più implementazioni <0>qui e <1>qui.", - "setup_dns_notice": "Per utilizzare <1>DNS-over-HTTPS o <1>DNS-over-TLS, è necessario <0>configurare la crittografia nelle impostazioni di AdGuard Home.", + "setup_dns_privacy_ioc_mac": "configurazione iOS e macOS", + "setup_dns_notice": "Per utilizzare <1>DNS su HTTPS o <1>DNS su TLS, è necessario <0>configurare la crittografia nelle impostazioni di AdGuard Home.", "rewrite_added": "Riscrittura DNS per \"{{key}}\" aggiunta correttamente", "rewrite_deleted": "La riscrittura DNS per \"{{key}}\" è stata eliminata correttamente", "rewrite_add": "Aggiungi la riscrittura DNS", @@ -529,7 +537,6 @@ "check_ip": "Indirizzi IP: {{ip}}", "check_cname": "CNAME: {{cname}}", "check_reason": "Motivo: {{reason}}", - "check_rule": "Regola: {{rule}}", "check_service": "Nome servizio: {{service}}", "service_name": "Nome servizio", "check_not_found": "Non trovato negli elenchi dei filtri", diff --git a/client/src/__locales/ja.json b/client/src/__locales/ja.json index 0d83676f..7eb45326 100644 --- a/client/src/__locales/ja.json +++ b/client/src/__locales/ja.json @@ -1,6 +1,7 @@ { "client_settings": "クライアント設定", "example_upstream_reserved": "<0>特定のドメインに対してDNSアップストリームを指定できます", + "example_upstream_comment": "コメントを指定できます", "upstream_parallel": "並列リクエストを使用する(すべてのアップストリームサーバーを同時に照会することで解決スピードが向上します)", "parallel_requests": "並列リクエスト", "load_balancing": "ロードバランシング", @@ -31,6 +32,7 @@ "form_error_ip_format": "IPv4フォーマットではありません", "form_error_mac_format": "MACフォーマットではありません", "form_error_client_id_format": "Client IDの形式が無効です", + "form_error_server_name": "サーバ名が無効です", "form_error_positive": "0より大きい必要があります", "form_error_negative": "0以上である必要があります", "range_end_error": "範囲開始よりも大きくなければなりません", @@ -246,8 +248,16 @@ "custom_ip": "カスタムIP", "blocking_ipv4": "ブロック中のIPv4", "blocking_ipv6": "ブロック中のIPv6", + "dnscrypt": "DNSCrypt", "dns_over_https": "DNS-over-HTTPS", "dns_over_tls": "DNS-over-TLS", + "dns_over_quic": "DNS-over-QUIC", + "client_id": "Client ID(クライアントID)", + "client_id_placeholder": "クライアントIDを入力してください", + "client_id_desc": "それぞれのクライアントは、特別なクライアントIDで識別できます。 ここでは、クライアントを特定する方法について詳しく知ることができます。", + "download_mobileconfig_doh": "DNS-over-HTTPS用の .mobileconfig をダウンロード", + "download_mobileconfig_dot": "DNS-over-TLS用の .mobileconfig をダウンロード", + "download_mobileconfig": "設定ファイルをダウンロードする", "plain_dns": "通常のDNS", "form_enter_rate_limit": "頻度制限を入力してください", "rate_limit": "頻度制限", @@ -327,7 +337,7 @@ "encryption_config_saved": "暗号化の設定を保存しました", "encryption_server": "サーバ名", "encryption_server_enter": "ドメイン名を入力してください", - "encryption_server_desc": "HTTPSを使用するには、SSL証明書と一致するサーバ名を入力する必要があります。", + "encryption_server_desc": "HTTPSを使用するには、SSL証明書またはワイルドカード証明書と一致するサーバー名を入力する必要があります。このフィールドが設定されていない場合は、任意のドメインのTLS接続を受け入れます。", "encryption_redirect": "HTTPSに自動的にリダイレクト", "encryption_redirect_desc": "チェックすると、AdGuard Homeは自動的にHTTPからHTTPSアドレスへリダイレクトします。", "encryption_https": "HTTPS ポート", @@ -383,7 +393,7 @@ "client_edit": "クライアントの編集", "client_identifier": "識別子", "ip_address": "IPアドレス", - "client_identifier_desc": "クライアントはIPアドレスまたはMACアドレスで識別できます。AdGuard Homeが<0>DHCPサーバでもある場合にのみ、識別子としてMACを使用することが可能であることにご注意ください。", + "client_identifier_desc": "クライアントは、IPアドレス、CIDR、MACアドレス、または特別なクライアントID(DoT/DoH/DoQで使用可能)によって識別することができます。<0>ここでは、クライアントの識別方法についてより詳しく説明しています。", "form_enter_ip": "IPアドレスを入力してください", "form_enter_mac": "MACアドレスを入力してください", "form_enter_id": "識別子を入力してください", @@ -414,6 +424,7 @@ "dns_privacy": "DNSプライバシー", "setup_dns_privacy_1": "<0>DNS-over-TLS: <1>{{address}}という文字列を使用してください。", "setup_dns_privacy_2": "<0>DNS-over-HTTPS: <1>{{address}}という文字列を使用してください。", + "setup_dns_privacy_3": "<0>使用できるソフトウェアのリストは次の通りです。", "setup_dns_privacy_4": "iOS 14 または macOS Big Sur デバイスにて、DNS-over-HTTPSまたはDNS-over-TLSサーバをDNS設定へ追加する特別な「.mobileconfig」ファイルをダウンロードできます。", "setup_dns_privacy_android_1": "Android 9はDNS-over-TLSをネイティブにサポートします。設定するには、設定 → ネットワークとインターネット → 詳細設定 → プライベートDNS へ遷移し、そこにドメイン名を入力してください。", "setup_dns_privacy_android_2": "<0>AdGuard for Androidは、<1>DNS-over-HTTPSと<1>DNS-over-TLSをサポートしています。", @@ -426,6 +437,7 @@ "setup_dns_privacy_other_3": "<0>dnscrypt-proxyは<1>DNS-over-HTTPSをサポートします。", "setup_dns_privacy_other_4": "<0>Mozilla Firefoxは<1>DNS-over-HTTPSをサポートしています。", "setup_dns_privacy_other_5": "もっと多くの実装を<0>ここや<1>ここで見つけられます。", + "setup_dns_privacy_ioc_mac": "iOS と macOS での設定", "setup_dns_notice": "<1>DNS-over-HTTPSまたは<1>DNS-over-TLSを使用するには、AdGuard Home 設定の<0>暗号化設定が必要です。", "rewrite_added": "\"{{key}}\" のためのDNS書き換え情報を追加完了しました", "rewrite_deleted": "\"{{key}}\" のためのDNS書き換え情報を削除完了しました", @@ -525,8 +537,8 @@ "check_ip": "IPアドレス: {{ip}}", "check_cname": "CNAME: {{cname}}", "check_reason": "理由: {{reason}}", - "check_rule": "ルール: {{rule}}", "check_service": "サービス名: {{service}}", + "service_name": "サービス名", "check_not_found": "フィルタ一覧には見つかりません", "client_confirm_block": "クライアント\"{{ip}}\"をブロックしてもよろしいですか?", "client_confirm_unblock": "クライアント\"{{ip}}\"のブロックを解除してもよろしいですか?", @@ -561,6 +573,11 @@ "cache_size_desc": "DNSキャッシュサイズ(バイト単位)", "cache_ttl_min_override": "最小TTLの上書き(秒単位)", "cache_ttl_max_override": "最大TTLの上書き(秒単位)", + "enter_cache_size": "キャッシュサイズ(バイト単位)を入力してください", + "enter_cache_ttl_min_override": "最小TTL(秒単位)を入力してください", + "enter_cache_ttl_max_override": "最大TTL(秒単位)を入力してください", + "cache_ttl_min_override_desc": "DNS応答をキャッシュするとき、上流サーバから受信した短いTTL(秒単位)を延長します", + "cache_ttl_max_override_desc": "DNSキャッシュ内のエントリの最大TTL(秒単位)を設定します", "ttl_cache_validation": "最小キャッシュTTL値は最大値以下にする必要があります", "filter_category_general": "一般", "filter_category_security": "セキュリティ", @@ -570,9 +587,11 @@ "filter_category_security_desc": "マルウェア、フィッシング、詐欺ドメインのブロック専用リストです。", "filter_category_regional_desc": "それぞれの地域の広告と追跡サーバをターゲットするリストです。", "filter_category_other_desc": "その他のブロックリストです。", + "setup_config_to_enable_dhcp_server": "DHCPサーバを有効にするには構成を設定してください", "original_response": "当初の応答", "click_to_view_queries": "クエリを表示するにはクリックしてください", "port_53_faq_link": "多くの場合、ポート53は \"DNSStubListener\" または \"systemd-resolved\" サービスによって利用されています。これを解決する方法については、<0>この手順をお読みください。", "adg_will_drop_dns_queries": "AdGuard Homeは、このクライアントからすべてのDNSクエリを落とします。", + "client_not_in_allowed_clients": "「許可されたクライアント」リストにないため、このクライアントは許可されていません。", "experimental": "実験用" } \ No newline at end of file diff --git a/client/src/__locales/ko.json b/client/src/__locales/ko.json index 16c37a6a..2f25cbc6 100644 --- a/client/src/__locales/ko.json +++ b/client/src/__locales/ko.json @@ -32,6 +32,7 @@ "form_error_ip_format": "잘못된 IP 형식", "form_error_mac_format": "잘못된 MAC 형식", "form_error_client_id_format": "잘못된 클라이언트 ID 형식", + "form_error_server_name": "유효하지 않은 서버 이름입니다", "form_error_positive": "0보다 커야 합니다", "form_error_negative": "반드시 0 이상이여야 합니다", "range_end_error": "입력 값은 범위의 시작 지점보다 큰 값 이여야 합니다.", @@ -247,10 +248,16 @@ "custom_ip": "사용자 지정 IP", "blocking_ipv4": "IPv4 차단", "blocking_ipv6": "IPv6 차단", + "dnscrypt": "DNSCrypt", "dns_over_https": "DNS-over-HTTPS", "dns_over_tls": "DNS-over-TLS", + "dns_over_quic": "DNS-over-QUIC", + "client_id": "클라이언트 ID", + "client_id_placeholder": "클라이언트 ID 입력", + "client_id_desc": "클라이언트는 특별한 클라이언트 ID를 기반으로 구분됩니다. 여기에서 클라이언트를 구분하는 방법을 자세히 알아보세요.", "download_mobileconfig_doh": "DNS-over-HTTPS용 .mobileconfig 다운로드", "download_mobileconfig_dot": "DNS-over-TLS용 .mobileconfig 다운로드", + "download_mobileconfig": "설정 파일 내려받기", "plain_dns": "평문 DNS", "form_enter_rate_limit": "한도 제한 입력하기", "rate_limit": "한도 제한", @@ -301,7 +308,7 @@ "install_devices_router_list_1": "라우터의 환경 설정을 여세요. 환경 설정은 다음의 주소(http://192.168.0.1/ 혹은 http://192.168.1.1/)를 통해 브라우저로 접근 가능합니다. 비밀번호를 입력해야할 수 있습니다. 비밀번호를 잊었다면 대개 라우터 기기에 있는 버튼을 눌러 비밀번호를 초기화할 수 있습니다. 어떤 라우터들은 당신의 컴퓨터/핸드폰에 설치할 수 있는 특정 어플리케이션을 필요로합니다.", "install_devices_router_list_2": "각각 1~3자리 숫자의 네 그룹으로 분할된 두 세트의 숫자를 허용하는 필드 옆에 있는 DNS 문자를 찾으세요.", "install_devices_router_list_3": "AdGuard Home 서버 주소를 입력하세요", - "install_devices_router_list_4": "일부 라우터는 DNS서버의 커스텀 설정이 불가합니다. 간혹 AdGuard Home을 DHCP서버로 이용하여 문제를 해결하는 경우가 있지만 문제가 지속될 경우 사용하시는 라우터 모델의 매뉴얼을 참고하시어 <0>DNS서버 커스텀 설정 방법을 직접 살펴보셔야 합니다.", + "install_devices_router_list_4": "일부 라우터 유형에서는 사용자 정의 DNS 서버를 설정할 수 없습니다. 이 경우에는 AdGuard Home을 <0>DHCP 서버로 설정할 수 있습니다. 그렇지 않으면 특정 라우터 모델에 맞게 DNS 서버를 설정하는 방법을 찾아야 합니다.", "install_devices_windows_list_1": "시작 메뉴 또는 윈도우 검색을 통해 제어판을 여세요", "install_devices_windows_list_2": "네트워크 및 인터넷 카테고리로 이동한 다음 네트워크 및 공유 센터로 이동하세요.", "install_devices_windows_list_3": "화면 왼쪽에서 어댑터 설정 변경을 찾아 클릭하세요.", @@ -370,7 +377,7 @@ "dns_status_error": "DNS 서버 상태를 가져오는 도중 오류가 발생했습니다", "down": "다운로드", "fix": "수정", - "dns_providers": "여기에 선택가능한 DNS 목록 이 있습니다.", + "dns_providers": "다음은 선택할 수 있는 <0>알려진 DNS 공급자 목록입니다.", "update_now": "지금 업데이트", "update_failed": "자동 업데이트 실패 되었습니다. 단계를 따라 수동으로 업데이트하세요", "processing_update": "잠시만 기다려주세요, AdGuard Home가 업데이트 중입니다.", @@ -386,7 +393,7 @@ "client_edit": "클라이언트 수정", "client_identifier": "식별자", "ip_address": "IP 주소", - "client_identifier_desc": "사용자는 IP 주소 또는 MAC 주소로 식별할 수 있지만 AdGuard Home이 <0>DHCP 서버인 경우에만 사용자는 MAC 주소로 식별할 수 있습니다.", + "client_identifier_desc": "클라이언트는 IP 주소, CIDR, MAC 주소 또는 특수 클라이언트 ID로 식별할 수 있습니다 (DoT/DoH/DoQ에 사용 가능). <0>여기에서 클라이언트를 식별하는 방법에 대한 자세한 내용은 확인하실 수 있습니다.", "form_enter_ip": "IP 입력", "form_enter_mac": "MAC 입력", "form_enter_id": "식별자 입력", @@ -430,6 +437,7 @@ "setup_dns_privacy_other_3": "<0>dnscrypt-proxy <1>DNS-over-HTTPS 지원합니다.", "setup_dns_privacy_other_4": "<0>Mozilla Firefox<1>DNS-over-HTTPS지원합니다.", "setup_dns_privacy_other_5": "<0>이곳이나 <1>이곳을 클릭하여 더 많은 구현에 대한 정보를 확인하세요.", + "setup_dns_privacy_ioc_mac": "iOS 및 macOS 설정", "setup_dns_notice": "<1>DNS-over-HTTPS 또는 <1>DNS-over-TLS를 사용하려면 AdGuard Home 설정에서 <0>암호화를 구성해야 합니다.", "rewrite_added": "\"{{key}}\"에 대한 DNS 수정 정보를 성공적으로 추가 됩니다.", "rewrite_deleted": "\"{{key}}\"에 대한 DNS 수정 정보를 성공적으로 삭제 됩니다.", @@ -529,7 +537,6 @@ "check_ip": "IP 주소: {{ip}}", "check_cname": "CNAME: {{cname}}", "check_reason": "이유: {{reason}}", - "check_rule": "규칙: {{rule}}", "check_service": "서비스 이름: {{service}}", "service_name": "서비스 이름", "check_not_found": "필터 목록에서 찾을 수 없음", diff --git a/client/src/__locales/nl.json b/client/src/__locales/nl.json index 224d1b3e..b903ced6 100644 --- a/client/src/__locales/nl.json +++ b/client/src/__locales/nl.json @@ -6,7 +6,7 @@ "parallel_requests": "Parallelle verzoeken", "load_balancing": "Volume balanceren", "load_balancing_desc": "Eén server per keer bevragen. AdGuard Home gebruikt hiervoor een gewogen willekeurig algoritme om de server te kiezen zodat de snelste server meer zal gebruikt worden.", - "bootstrap_dns": "Bootstrap DNS servers", + "bootstrap_dns": "Bootstrap DNS-servers", "bootstrap_dns_desc": "Bootstrap DNS-servers worden gebruikt om IP-adressen op te lossen van de DoH / DoT-resolvers die u opgeeft als upstreams.", "check_dhcp_servers": "Zoek achter DHCP servers", "save_config": "Configuratie opslaan", @@ -23,7 +23,7 @@ "dhcp_leases": "DHCP lease overzicht", "dhcp_static_leases": "DHCP statische lease", "dhcp_leases_not_found": "Geen DHCP lease gevonden", - "dhcp_config_saved": "DHCP server configuratie opgeslagen", + "dhcp_config_saved": "DHCP configuratie succesvol opgeslagen", "dhcp_ipv4_settings": "DHCP IPv4 instellingen", "dhcp_ipv6_settings": "DHCP IPv6 instellingen", "form_error_required": "Vereist veld", @@ -32,6 +32,7 @@ "form_error_ip_format": "Ongeldig IPv4 formaat", "form_error_mac_format": "Ongeldig MAC formaat.", "form_error_client_id_format": "Opmaak cliënt-ID is ongeldig", + "form_error_server_name": "Ongeldige servernaam", "form_error_positive": "Moet groter zijn dan 0", "form_error_negative": "Moet 0 of hoger dan 0 zijn", "range_end_error": "Moet groter zijn dan het startbereik", @@ -132,8 +133,8 @@ "custom_filtering_rules": "Aangepaste filter regels", "encryption_settings": "Encryptie Instellingen", "dhcp_settings": "DHCP Instellingen", - "upstream_dns": "Upstream DNS servers", - "upstream_dns_help": "Server adressen invoeren, een per regel. Meer weten over het configureren van upstream DNS servers.", + "upstream_dns": "Upstream DNS-servers", + "upstream_dns_help": "Server adressen invoeren, een per regel. Meer weten over het configureren van upstream DNS-servers.", "upstream_dns_configured_in_file": "Geconfigureerd in {{path}}", "test_upstream_btn": "Test upstream", "upstreams": "Upstreams", @@ -186,15 +187,15 @@ "example_comment_hash": "# Nog een opmerking", "example_regex_meaning": "blokkeer de toegang tot de domeinen die overeenkomen met de opgegeven reguliere expressie", "example_upstream_regular": "standaard DNS (over UDP)", - "example_upstream_dot": "versleutelde <0>DNS_over_TLS", - "example_upstream_doh": "versleutelde <0>DNS_over_HTTPS", + "example_upstream_dot": "versleutelde <0>DNS-via-TLS", + "example_upstream_doh": "versleutelde <0>DNS-via-HTTPS", "example_upstream_doq": "versleutelde <0>DNS-via-QUIC", - "example_upstream_sdns": "je kunt <0>DNS Stamps voor <1>DNSCrypt of <2>DNS-over-HTTPS resolvers", + "example_upstream_sdns": "je kunt <0>DNS Stamps voor <1>DNSCrypt of <2>DNS-via-HTTPS oplossingen gebruiken", "example_upstream_tcp": "standaard DNS (over TCP)", "all_lists_up_to_date_toast": "Alle lijsten zijn reeds up-to-date", "updated_upstream_dns_toast": "De upstream DNS-servers zijn bijgewerkt", "dns_test_ok_toast": "Opgegeven DNS-servers werken correct", - "dns_test_not_ok_toast": "Server \"{{key}}\": kon niet worden gebruikt, controleer of u het correct hebt geschreven", + "dns_test_not_ok_toast": "Server \"{{key}}\": kon niet worden gebruikt, controleer of je het correct hebt geschreven", "unblock": "Deblokkeren", "block": "Blokkeren", "disallow_this_client": "Toepassing/systeem niet toelaten", @@ -212,7 +213,7 @@ "empty_response_status": "Leeg", "show_all_filter_type": "Toon alles", "show_filtered_type": "Toon gefilterde", - "no_logs_found": "Geen log bestanden gevonden", + "no_logs_found": "Geen logboeken gevonden", "refresh_btn": "Verversen", "previous_btn": "Vorige", "next_btn": "Volgende", @@ -235,8 +236,8 @@ "query_log_strict_search": "Gebruik dubbele aanhalingstekens voor strikt zoeken", "query_log_retention_confirm": "Weet u zeker dat u de bewaartermijn van het query logboek wilt wijzigen? Als u de intervalwaarde verlaagt, gaan sommige gegevens verloren", "anonymize_client_ip": "Cliënt IP anonimiseren", - "anonymize_client_ip_desc": "Het volledige IP-adres van de cliënt niet opnemen in log- en statistiekbestanden", - "dns_config": "DNS server configuratie", + "anonymize_client_ip_desc": "Het volledige IP-adres van de cliënt niet opnemen in logboeken en statistiekbestanden", + "dns_config": "DNS-server configuratie", "dns_cache_config": "DNS cache configuratie", "dns_cache_config_desc": "Hier kan de DNS cache geconfigureerd worden", "blocking_mode": "Blocking modus", @@ -247,10 +248,16 @@ "custom_ip": "Aangepast IP", "blocking_ipv4": "Blokkeren IP4", "blocking_ipv6": "Blokkeren IP6", + "dnscrypt": "DNSCrypt", "dns_over_https": "DNS-via-HTTPS", "dns_over_tls": "DNS-via-TLS", + "dns_over_quic": "DNS-via-QUIC", + "client_id": "Apparaat-ID", + "client_id_placeholder": "Apparaat-ID invoeren", + "client_id_desc": "Verschillende apparaten kunnen worden geïdentificeerd door hun specifiek apparaat-ID. Hier vind je meer informatie over het identificeren van apparaten.", "download_mobileconfig_doh": ".mobileconfig voor DNS-via-HTTPS downloaden", "download_mobileconfig_dot": ".mobileconfig voor DNS-via-TLS downloaden", + "download_mobileconfig": "Configuratiebestand downloaden", "plain_dns": "Gewone DNS", "form_enter_rate_limit": "Voer ratio limiet in", "rate_limit": "Ratio limiet", @@ -269,19 +276,19 @@ "source_label": "Bron", "found_in_known_domain_db": "Gevonden in de bekende domeingegevensbank.", "category_label": "Categorie", - "rule_label": "Regel", + "rule_label": "Regel(s)", "list_label": "Lijst", "unknown_filter": "Onbekend filter {{filterId}}", "known_tracker": "Bekende volger", "install_welcome_title": "Welkom bij AdGuard Home!", - "install_welcome_desc": "AdGuard Home is een netwerk DNS server die advertenties en trackers blokkeert. Het doel is om jou controle te geven over je gehele netwerk en al je apparaten, en er hoeft geen client-side programma te worden gebruikt.", + "install_welcome_desc": "AdGuard Home is een netwerk DNS-server die advertenties en trackers blokkeert. Het doel is om jou controle te geven over je gehele netwerk en al je apparaten, en er hoeft geen client-side programma te worden gebruikt.", "install_settings_title": "Admin webinterface", "install_settings_listen": "Luister interface", "install_settings_port": "Poort", "install_settings_interface_link": "De webinterface van AdGuard Home admin is beschikbaar op de volgende adressen:", "form_error_port": "Voer geldige poortwaarde in", - "install_settings_dns": "DNS server", - "install_settings_dns_desc": "U moet uw apparaten of router configureren om de DNS-server te gebruiken op de volgende adressen:", + "install_settings_dns": "DNS-server", + "install_settings_dns_desc": "Je moet jouw apparaten of router configureren om de DNS-server te gebruiken op de volgende adressen:", "install_settings_all_interfaces": "Alle interfaces", "install_auth_title": "Authenticatie", "install_auth_desc": "Het wordt ten zeerste aanbevolen om wachtwoordverificatie te configureren voor de AdGuard Home admin webinterface. Zelfs als het alleen toegankelijk is in uw lokale netwerk, is het nog steeds belangrijk om het te beschermen tegen onbeperkte toegang.", @@ -301,26 +308,26 @@ "install_devices_router_list_1": "Open de instellingen pagina voor uw router. Meestal kunt u deze vanuit uw browser openen via een URL (zoals http://192.168.0.1/ of http://192.168.1.1/). Mogelijk wordt u gevraagd om het wachtwoord in te voeren. Als u het niet meer weet, kunt u het wachtwoord vaak opnieuw instellen door op een knop op de router zelf te drukken. Voor sommige routers is een specifieke toepassing vereist, die in dat geval al op uw computer / telefoon moet zijn geïnstalleerd.", "install_devices_router_list_2": "Zoek de DHCP/DNS-instellingen. Zoek naar de DNS-letters naast een veld dat twee of drie reeksen nummers toestaat, elk verdeeld in vier groepen van één tot drie cijfers.", "install_devices_router_list_3": "Voer je AdGuard Home server adressen daar in.", - "install_devices_router_list_4": "Je kan de DNS server niet aanpassen op sommige routers. In dat geval kan het een oplossing zijn om AdGuard Home te definiëren als een <0>DHCP server. Je kan ook in de handleiding van je router kijken hoe je een DNS server aanpast.", + "install_devices_router_list_4": "Je kan de DNS-server niet aanpassen op sommige routers. In dat geval kan het een oplossing zijn om AdGuard Home te definiëren als een <0>DHCP-server. Je kan ook in de handleiding van je router kijken hoe je een DNS-server aanpast.", "install_devices_windows_list_1": "Open het Configuratiescherm via het menu Start of Windows zoeken.", "install_devices_windows_list_2": "Ga naar de categorie Netwerk en Internet en vervolgens naar Netwerkcentrum.", "install_devices_windows_list_3": "Zoek aan de linkerkant van het scherm Adapter-instellingen wijzigen en klik erop.", "install_devices_windows_list_4": "Selecteer jouw actieve verbinding, klik er met de rechtermuisknop op en kies Eigenschappen.", "install_devices_windows_list_5": "Zoek Internet Protocol versie 4 (TCP / IP) in de lijst, selecteer het en klik vervolgens opnieuw op Eigenschappen.", - "install_devices_windows_list_6": "Kies Gebruik de volgende DNS-serveradressen en voer uw AdGuard Home-serveradressen in.", + "install_devices_windows_list_6": "Kies Gebruik de volgende DNS-server adressen en voer jouw AdGuard Home server adressen in.", "install_devices_macos_list_1": "Klik op het Apple-pictogram en ga naar Systeemvoorkeuren.", "install_devices_macos_list_2": "Klik op Netwerk.", "install_devices_macos_list_3": "Selecteer de eerste verbinding in jouw lijst en klik op Geavanceerd.", - "install_devices_macos_list_4": "Selecteer het tabblad DNS en voer uw AdGuard Home-serveradressen in.", + "install_devices_macos_list_4": "Selecteer het tabblad DNS en voer jouw AdGuard Home server adressen in.", "install_devices_android_list_1": "Tik op het startscherm van het Android-menu op Instellingen.", "install_devices_android_list_2": "Tik op wifi in het menu. Het scherm met alle beschikbare netwerken wordt getoond (het is niet mogelijk om een aangepaste DNS in te stellen voor een mobiele verbinding).", "install_devices_android_list_3": "Druk lang op het netwerk waarmee je bent verbonden en tik op Netwerk instellingen aanpassen.", "install_devices_android_list_4": "Op sommige apparaten moet u het vakje aanvinken voor Geavanceerd om verdere instellingen te bekijken. Om uw Android DNS-instellingen aan te passen, moet u de IP-instellingen wijzigen van DHCP in Statisch.", - "install_devices_android_list_5": "Wijzig de DNS 1-waarden en DNS 2-waarden in uw AdGuard Home-serveradressen.", + "install_devices_android_list_5": "Wijzig de DNS 1-waarden en DNS 2-waarden in jouw AdGuard Home server adressen.", "install_devices_ios_list_1": "Tik op het startscherm op Instellingen.", "install_devices_ios_list_2": "Kies Wi-Fi in het linkermenu (DNS kan niet worden geconfigureerd voor mobiele netwerken).", "install_devices_ios_list_3": "Tik op de naam van het momenteel actieve netwerk.", - "install_devices_ios_list_4": "Voer in het DNS-veld uw AdGuard Home-serveradressen in.", + "install_devices_ios_list_4": "Voer in het DNS veld jouw AdGuard Home server adressen in.", "get_started": "Beginnen", "next": "Volgende", "open_dashboard": "Open Dashboard", @@ -330,13 +337,13 @@ "encryption_config_saved": "Encryptie configuratie opgeslagen", "encryption_server": "Server naam", "encryption_server_enter": "Voer domein naam in", - "encryption_server_desc": "Om HTTPS te gebruiken, voer de naam in van de server overeenkomstig met het SSL certificaat.", + "encryption_server_desc": "Om HTTPS te gebruiken, moet je de servernaam invoeren die overeenkomt met je SSL-certificaat of jokerteken-certificaat. Als het veld niet is ingesteld, accepteert het TLS-verbindingen voor elk domein.", "encryption_redirect": "Herleid automatisch naar HTTPS", "encryption_redirect_desc": "Indien ingeschakeld, zal AdGuard Home je automatisch herleiden van HTTP naar HTTPS.", "encryption_https": "HTTPS poort", - "encryption_https_desc": "Als de HTTPS-poort is geconfigureerd, is de AdGuard Home beheerders interface toegankelijk via HTTPS en biedt deze ook DNS-over-HTTPS op de locatie '/ dns-query'.", - "encryption_dot": "DNS-over-TLS poort", - "encryption_dot_desc": "Indien deze poort is geconfigureerd, zal AdGuard Home gebruik maken van een DNS-over-TLS server via deze poort.", + "encryption_https_desc": "Als de HTTPS-poort is geconfigureerd, is de AdGuard Home beheerders interface toegankelijk via HTTPS en biedt deze ook DNS-via-HTTPS op de locatie '/ dns-query'.", + "encryption_dot": "DNS-via-TLS poort", + "encryption_dot_desc": "Indien deze poort is geconfigureerd, zal AdGuard Home gebruik maken van een DNS-via-TLS server via deze poort.", "encryption_doq": "DNS-via-QUIC poort", "encryption_doq_desc": "Als deze poort is geconfigureerd, zal AdGuard Home een DNS-via-QUIC server gebruiken via deze poort. Dit is experimenteel en kan onbetrouwbaar zijn. Er zijn overigens nog niet veel systemen die dit nu al ondersteunen.", "encryption_certificates": "Certificaten", @@ -346,8 +353,8 @@ "encryption_expire": "Verloopt", "encryption_key": "Prive sleutel", "encryption_key_input": "Kopieër en plak je PEM-gecodeerde prive sleutel voor je certificaat hier.", - "encryption_enable": "Activeer encryptie (HTTPS, DNS-over-HTTPS, en DNS-over-TLS)", - "encryption_enable_desc": "Als encryptie is geactiveerd, is de AdGuard Home beheerders interface toegankelijk via HTTPS en de DNS-server zal luisteren naar aanvragen via DNS-over-HTTPS en DNS-over-TLS.", + "encryption_enable": "Activeer encryptie (HTTPS, DNS-via-HTTPS, en DNS-via-TLS)", + "encryption_enable_desc": "Als encryptie is geactiveerd, is de AdGuard Home beheerders interface toegankelijk via HTTPS en de DNS-server zal luisteren naar aanvragen via DNS-via-HTTPS en DNS-via-TLS.", "encryption_chain_valid": "certificaatketen is geldig", "encryption_chain_invalid": "certificaatketen is ongeldig", "encryption_key_valid": "Dit is een geldig {{type}} privé sleutel", @@ -366,8 +373,8 @@ "update_announcement": "AdGuard Home{{version}} is nu beschikbaar! <0>klik hier voor meer info.", "setup_guide": "Installatie gids", "dns_addresses": "DNS adressen", - "dns_start": "DNS server aan het opstarten", - "dns_status_error": "Fout bij het oproepen van de DNS server status", + "dns_start": "DNS-server aan het opstarten", + "dns_status_error": "Fout bij het oproepen van de DNS-server status", "down": "Uitgeschakeld", "fix": "Los op", "dns_providers": "hier is een <0>lijst of gekende DNS providers waarvan je kan kiezen.", @@ -380,13 +387,13 @@ "settings_custom": "Aangepast", "table_client": "Gebruiker", "table_name": "Naam", - "save_btn": "Bewaar", + "save_btn": "Opslaan", "client_add": "Voeg gebruiker toe", "client_new": "Nieuwe gebruiker", "client_edit": "Wijzig gebruiker", "client_identifier": "Identificeer via", "ip_address": "IP adres", - "client_identifier_desc": "Gebruikers kunnen worden geïdentificeerd door het IP-adres, CIDR of MAC-adres. Hou er rekening mee dat het gebruik van MAC als ID alleen mogelijk is als AdGuard Home ook een <0>DHCP-server is", + "client_identifier_desc": "Apparaten kunnen worden geïdentificeerd door hun IP-adres, CIDR, MAC-adres of een speciaal apparaat-ID (kan gebruikt worden voor DoT/DoH/DoQ). <0>Hier kan je meer lezen over het identificeren van apparaten.", "form_enter_ip": "Vul IP in", "form_enter_mac": "Vul MAC in", "form_enter_id": "ID invoeren", @@ -403,34 +410,35 @@ "auto_clients_title": "Gebruikers (runtime)", "auto_clients_desc": "Data over gebruikers die AdGuard Home gebruiken, maar niet geconfigureerd zijn", "access_title": "Toegangs instellingen", - "access_desc": "Hier kan je toegangsregels voor de AdGuard Home DNS server instellen.", + "access_desc": "Hier kan je toegangsregels voor de AdGuard Home DNS-server instellen.", "access_allowed_title": "Toegestane gebruikers", "access_allowed_desc": "Een lijst van CIDR of IP adressen. Indien ingesteld, zal AdGuard Home alleen van deze IP adressen aanvragen accepteren.", "access_disallowed_title": "Verworpen gebruikers", "access_disallowed_desc": "Een lijst van CIDR of IP adressen. Indien ingesteld, zal AdGuard Home aanvragen van deze IP adressen verwerpen.", "access_blocked_title": "Niet toegelaten domeinen", "access_blocked_desc": "Verwar dit niet met filters. AdGuard Home zal deze DNS-zoekopdrachten niet uitvoeren die deze domeinen in de zoekopdracht bevatten. Hier kan je de domeinnamen, wildcards en url-filter-regels specifiëren, bijv. 'example.org', '*.example.org' or '||example.org^'.", - "access_settings_saved": "Toegangsinstellingen met succes opgeslagen", + "access_settings_saved": "Toegangsinstellingen succesvol opgeslagen", "updates_checked": "Met succes op updates gecontroleerd", "updates_version_equal": "AdGuard Home is up-to-date", "check_updates_now": "Controleer op updates", "dns_privacy": "DNS Privacy", - "setup_dns_privacy_1": "<0>DNS-over-TLS: Gebruik <1>{{address}} string.", - "setup_dns_privacy_2": "<0>DNS-over-HTTPS: Gebruik <1>{{address}} string.", + "setup_dns_privacy_1": "<0>DNS-via-TLS: Gebruik <1>{{address}} string.", + "setup_dns_privacy_2": "<0>DNS-via-HTTPS: Gebruik <1>{{address}} string.", "setup_dns_privacy_3": "<0>Hou er rekening mee dat het beveiligde DNS protocol alleen beschikbaar is voor Android 9. U moet dus extra software installeren voor andere besturingssystemen.<0>Hier is een lijst van te gebruiken software.", "setup_dns_privacy_4": "Op een iOS 14 of macOS Big Sur apparaat kan je een speciaal '.mobileconfig'-bestand downloaden dat DNS-via-HTTPS of DNS-via-TLS servers aan de DNS-instellingen toevoegt.", - "setup_dns_privacy_android_1": "Android 9 ondersteunt native DNS-over-TLS. Om het te configureren, ga naar Instellingen → Netwerk & internet → Geavanceerd → Privé DNS en voer daar uw domeinnaam in.", - "setup_dns_privacy_android_2": "<0>AdGuard voor Androidondersteunt<1>DNS-over-HTTPS en<1>DNS-over-TLS.", - "setup_dns_privacy_android_3": "<0> Intra voegt <1> DNS-over-HTTPS ondersteuning toe aan Android.", - "setup_dns_privacy_ios_1": "<0>DNSCloak ondersteunt <1> DNS-over-HTTPS , maar om het te configureren om op uw eigen server te gebruiken moet er een <2> DNS-stempel gegenereerd worden.", - "setup_dns_privacy_ios_2": "<0> AdGuard voor iOS ondersteunt de instellingen <1> DNS-over-HTTPS en <1> DNS-over-TLS .", + "setup_dns_privacy_android_1": "Android 9 ondersteunt native DNS-via-TLS. Om het te configureren, ga naar Instellingen → Netwerk & internet → Geavanceerd → Privé DNS en voer daar je domeinnaam in.", + "setup_dns_privacy_android_2": "<0>AdGuard voor Androidondersteunt<1>DNS-via-HTTPS en<1>DNS-via-TLS.", + "setup_dns_privacy_android_3": "<0> Intra voegt <1> DNS-via-HTTPS ondersteuning toe aan Android.", + "setup_dns_privacy_ios_1": "<0>DNSCloak ondersteunt <1> DNS-via-HTTPS , maar om het te configureren op jouw eigen server moet er een <2> DNS-stempel gegenereerd worden.", + "setup_dns_privacy_ios_2": "<0> AdGuard voor iOS ondersteunt de instellingen <1> DNS-via-HTTPS en <1> DNS-via-TLS .", "setup_dns_privacy_other_title": "Overig gebruik", "setup_dns_privacy_other_1": "AdGuard Home kan op elk platform een ​​veilige DNS-client zijn.", "setup_dns_privacy_other_2": "<0>dnsproxy ondersteunt alle bekende beveiligde DNS-protocollen.", - "setup_dns_privacy_other_3": "<0> dnscrypt-proxy ondersteunt <1> DNS-over-HTTPS .", - "setup_dns_privacy_other_4": "<0> Mozilla Firefox ondersteunt <1> DNS-over-HTTPS .", + "setup_dns_privacy_other_3": "<0>dnscrypt-proxy ondersteunt <1>DNS-via-HTTPS.", + "setup_dns_privacy_other_4": "<0>Mozilla Firefox ondersteunt <1>DNS-via-HTTPS.", "setup_dns_privacy_other_5": "U vindt meer implementaties <0> hier en <1> hier .", - "setup_dns_notice": "Om <1> DNS-over-HTTPS of <1> DNS-over-TLS te gebruiken, moet u <0> Codering configureren in de AdGuard Home-instellingen.", + "setup_dns_privacy_ioc_mac": "iOS en macOS configuratie", + "setup_dns_notice": "Om <1>DNS-via-HTTPS of <1>DNS-via-TLS te gebruiken, moet je <0>Versleuteling configureren in de AdGuard Home instellingen.", "rewrite_added": "DNS-herschrijving voor \"{{key}}\" met succes toegevoegd", "rewrite_deleted": "DNS-herschrijving voor \"{{key}}\" met succes verwijderd", "rewrite_add": "DNS-herschrijving toevoegen", @@ -511,8 +519,8 @@ "disable_ipv6": "Zet IPv6 uit", "disable_ipv6_desc": "Als deze functie is ingeschakeld, worden alle DNS-query's voor IPv6-adressen (type AAAA) verwijderd.", "fastest_addr": "Snelste IP adres", - "fastest_addr_desc": "Alle DNS servers bevragen en het snelste IP adres terugkoppelen. Dit zal de DNS verzoeken vertragen omdat we moeten wachten op de antwoorden van alles DNS servers, maar verbetert wel de connectiviteit.", - "autofix_warning_text": "Als je op \"Repareren\" klikt, configureert AdGuard Home uw systeem om de AdGuard Home DNS-server te gebruiken.", + "fastest_addr_desc": "Alle DNS-servers bevragen en het snelste IP adres terugkoppelen. Dit zal de DNS verzoeken vertragen omdat we moeten wachten op de antwoorden van alles DNS-servers, maar verbetert wel de connectiviteit.", + "autofix_warning_text": "Als je op \"Repareren\" klikt, configureert AdGuard Home jouw systeem om de AdGuard Home DNS-server te gebruiken.", "autofix_warning_list": "De volgende taken worden uitgevoerd: <0> Deactiveren van Systeem DNSStubListener <0> DNS-serveradres instellen op 127.0.0.1 <0> Symbolisch koppelingsdoel van /etc/resolv.conf vervangen door /run/systemd/resolve/resolv.conf <0> Stop DNSStubListener (herlaad systemd-resolved service) ", "autofix_warning_result": "Als gevolg hiervan worden alle DNS-verzoeken van je systeem standaard door AdGuard Home verwerkt.", "tags_title": "Labels", @@ -529,7 +537,6 @@ "check_ip": "IP-adressen: {{ip}}", "check_cname": "CNAME: {{cname}}", "check_reason": "Reden: {{reason}}", - "check_rule": "Regel: {{rule}}", "check_service": "Servicenaam: {{service}}", "service_name": "Naam service", "check_not_found": "Niet in je lijst met filters gevonden", diff --git a/client/src/__locales/no.json b/client/src/__locales/no.json index 6f38a145..dde12de0 100644 --- a/client/src/__locales/no.json +++ b/client/src/__locales/no.json @@ -32,6 +32,7 @@ "form_error_ip_format": "Ugyldig IPv4-format", "form_error_mac_format": "Ugyldig MAC-format", "form_error_client_id_format": "Ugyldig ID-klientformat", + "form_error_server_name": "Ugyldig tjenernavn", "form_error_positive": "Må være høyere enn 0", "form_error_negative": "Må være ≥0", "range_end_error": "Må være høyere enn rekkeviddens start", @@ -247,8 +248,11 @@ "custom_ip": "Tilpasset IP", "blocking_ipv4": "IPv4-blokkering", "blocking_ipv6": "IPv6-blokkering", + "dnscrypt": "DNSCrypt", "dns_over_https": "DNS-over-HTTPS", "dns_over_tls": "DNS-over-TLS", + "dns_over_quic": "DNS-over-QUIC", + "client_id": "Klient-ID", "download_mobileconfig_doh": "Last ned .mobileconfig for DNS-over-HTTPS", "download_mobileconfig_dot": "Last ned .mobileconfig for DNS-over-TLS", "plain_dns": "Ordinær DNS", @@ -259,6 +263,7 @@ "rate_limit_desc": "Antallet forespørsler per sekund som én enkelt klient har lov til å be om (0: ubegrenset)", "blocking_ipv4_desc": "IP-adressen som det skal svares med for blokkerte A-forespørsler", "blocking_ipv6_desc": "IP-adressen som det skal svares med for blokkerte AAAA-forespørsler", + "blocking_mode_default": "Standard: Svar med null-IP-adresse (0.0.0.0 for A; :: for AAAA) når den blokkeres av adblock-aktige oppføringer; svar med IP-adressen som er spesifisert i oppføringen når den blokkeres av /etc/hosts-typeoppføringer", "blocking_mode_refused": "REFUSED: Svar med REFUSED-koden", "blocking_mode_nxdomain": "NXDOMAIN: Svar med NXDOMAIN-koden", "blocking_mode_null_ip": "Null IP: Svar med en 0-IP-adresse (0.0.0.0 for A; :: for AAAA)", @@ -268,7 +273,6 @@ "source_label": "Kilde", "found_in_known_domain_db": "Funnet i databasen over kjente domener.", "category_label": "Kategori", - "rule_label": "Oppføring", "list_label": "Liste", "unknown_filter": "Ukjent filter {{filterId}}", "known_tracker": "Kjent sporer", @@ -329,7 +333,6 @@ "encryption_config_saved": "Krypteringsoppsettet ble lagret", "encryption_server": "Tjenerens navn", "encryption_server_enter": "Skriv inn domenenavnet ditt", - "encryption_server_desc": "For å kunne bruke HTTPS, må du skrive inn tjenernavnet som samsvarer med ditt SSL-sertifikat.", "encryption_redirect": "Automatisk omdiriger til HTTPS", "encryption_redirect_desc": "Dersom dette er valgt, vil AdGuard Home automatisk omdirigere deg fra HTTP til HTTPS-adresser.", "encryption_https": "HTTPS-port", @@ -385,7 +388,6 @@ "client_edit": "Rediger klienten", "client_identifier": "Identifikator", "ip_address": "IP-adresse", - "client_identifier_desc": "Klienter kan bli identifisert gjennom IP-adressen eller MAC-adressen. Vennligst bemerk at å bruke MAC som en identifikator, bare er mulig dersom AdGuard Home også er en <0>DHCP-tjener", "form_enter_ip": "Skriv inn IP", "form_enter_mac": "Skriv inn MAC", "form_enter_id": "Skriv inn identifikator", @@ -416,6 +418,7 @@ "dns_privacy": "DNS-privatliv", "setup_dns_privacy_1": "<0>DNS-over-TLS: Benytt <1>{{address}}-strengen.", "setup_dns_privacy_2": "<0>DNS-over-HTTPS: Benytt <1>{{address}}-strengen.", + "setup_dns_privacy_3": "<0>Her er en liste over programvarer du kan bruke.", "setup_dns_privacy_android_1": "Android 9 har innebygd støtte for DNS-over-TLS. For å sette det opp, gå til Innstillinger → Nettverk og internett → Avansert → Privat DNS, og skriv inn domenenavnet ditt der.", "setup_dns_privacy_android_2": "<0>AdGuard for Android støtter <1>DNS-over-HTTPS og <1>DNS-over-TLS.", "setup_dns_privacy_android_3": "<0>Intra legger til <1>DNS-over-HTTPS-støtte i Android.", @@ -526,7 +529,6 @@ "check_ip": "IP-adresser: {{ip}}", "check_cname": "CNAME: {{cname}}", "check_reason": "Årsak: {{reason}}", - "check_rule": "Oppføring: {{rule}}", "check_service": "Tjenestenavn: {{service}}", "service_name": "Tjenestenavn", "check_not_found": "Ikke funnet i filterlistene dine", diff --git a/client/src/__locales/pl.json b/client/src/__locales/pl.json index 56d59db0..e44ef6d4 100644 --- a/client/src/__locales/pl.json +++ b/client/src/__locales/pl.json @@ -32,6 +32,7 @@ "form_error_ip_format": "Nieprawidłowy format IP", "form_error_mac_format": "Nieprawidłowy format MAC", "form_error_client_id_format": "Nieprawidłowy format identyfikatora klienta", + "form_error_server_name": "Nieprawidłowa nazwa serwera", "form_error_positive": "Musi być większa niż 0", "form_error_negative": "Musi być równy 0 lub większy", "range_end_error": "Zakres musi być większy niż początkowy", @@ -247,10 +248,16 @@ "custom_ip": "Niestandardowy adres IP", "blocking_ipv4": "Blokowanie IPv4", "blocking_ipv6": "Blokowanie IPv6", + "dnscrypt": "DNSCrypt", "dns_over_https": "DNS-over-HTTPS", "dns_over_tls": "DNS-over-TLS", + "dns_over_quic": "DNS-over-QUIC", + "client_id": "ID klienta", + "client_id_placeholder": "Wpisz ID klienta", + "client_id_desc": "Różnych klientów można zidentyfikować za pomocą specjalnego ID klienta. Tutaj możesz dowiedzieć się więcej o tym, jak identyfikować klientów.", "download_mobileconfig_doh": "Pobierz plik .mobileconfig dla DNS-over-HTTPS", "download_mobileconfig_dot": "Pobierz plik .mobileconfig dla DNS-over-TLS", + "download_mobileconfig": "Pobierz plik konfiguracyjny", "plain_dns": "Zwykły DNS", "form_enter_rate_limit": "Wpisz limit ilościowy", "rate_limit": "Limit ilościowy", @@ -269,7 +276,7 @@ "source_label": "Źródło", "found_in_known_domain_db": "Znaleziono w bazie danych znanych domen.", "category_label": "Kategoria", - "rule_label": "Reguła", + "rule_label": "Reguła(y)", "list_label": "Lista", "unknown_filter": "Nieznany filtr {{filterId}}", "known_tracker": "Znany element śledzący", @@ -330,7 +337,7 @@ "encryption_config_saved": "Zapisano konfigurację szyfrowania", "encryption_server": "Nazwa serwera", "encryption_server_enter": "Wpisz swoją nazwę domeny", - "encryption_server_desc": "Aby korzystać z protokołu HTTPS, musisz wprowadzić nazwę serwera zgodną z certyfikatem SSL.", + "encryption_server_desc": "Aby korzystać z protokołu HTTPS, musisz wprowadzić nazwę serwera, która jest zgodna z certyfikatem SSL lub certyfikatem typu wildcard. Jeśli pole nie jest ustawione, będzie akceptować połączenia TLS dla dowolnej domeny.", "encryption_redirect": "Przekieruj automatycznie do HTTPS", "encryption_redirect_desc": "Jeśli zaznaczone, AdGuard Home automatycznie przekieruje Cię z adresów HTTP na HTTPS.", "encryption_https": "Port HTTPS", @@ -386,7 +393,7 @@ "client_edit": "Edytuj klienta", "client_identifier": "Identyfikator", "ip_address": "Adres IP", - "client_identifier_desc": "Klienci mogą być identyfikowani na podstawie adresu IP, CIDR, adresu MAC. Pamiętaj, że użycie MAC jako identyfikatora jest możliwe tylko wtedy, gdy AdGuard Home jest również <0>serwerem DHCP", + "client_identifier_desc": "Klientów można zidentyfikować po adresie IP, CIDR, adresie MAC lub specjalnym identyfikatorze klienta (może służyć do DoT/DoH/DoQ). <0>Tutaj możesz dowiedzieć się więcej o tym, jak identyfikować klientów.", "form_enter_ip": "Wpisz adres IP", "form_enter_mac": "Wpisz adres MAC", "form_enter_id": "Wpisz identyfikator", @@ -430,6 +437,7 @@ "setup_dns_privacy_other_3": "<0>dnscrypt-proxy obsługuje <1>DNS-over-HTTPS.", "setup_dns_privacy_other_4": "<0>Mozilla Firefox obsługuje <1>DNS-over-HTTPS.", "setup_dns_privacy_other_5": "Znajdziesz więcej implementacji <0>tutaj i <1>tutaj.", + "setup_dns_privacy_ioc_mac": "Konfiguracja iOS i macOS", "setup_dns_notice": "Aby skorzystać z <1>DNS-over-HTTPS lub <1>DNS-over-TLS, musisz w ustawieniach AdGuard Home <0>skonfigurować szyfrowanie.", "rewrite_added": "Pomyślnie dodano przepisanie DNS dla „{{key}}”", "rewrite_deleted": "Przepisanie DNS dla „{{key}}” zostało pomyślnie usunięte", @@ -529,7 +537,6 @@ "check_ip": "Adresy IP: {{ip}}", "check_cname": "CNAME: {{cname}}", "check_reason": "Powód: {{reason}}", - "check_rule": "Reguła: {{rule}}", "check_service": "Nazwa usługi: {{service}}", "service_name": "Nazwa usługi", "check_not_found": "Nie znaleziono na Twoich listach filtrów", diff --git a/client/src/__locales/pt-br.json b/client/src/__locales/pt-br.json index e6932800..844aa1b6 100644 --- a/client/src/__locales/pt-br.json +++ b/client/src/__locales/pt-br.json @@ -1,6 +1,6 @@ { "client_settings": "Configurações do cliente", - "example_upstream_reserved": "Você pode especificar o DNS upstream <0>para o domínio(s) especifico", + "example_upstream_reserved": "Você pode especificar o DNS primário <0>para o domínio(s) especifico", "example_upstream_comment": "Você pode especificar o comentário", "upstream_parallel": "Usar consultas paralelas para acelerar a resolução consultando simultaneamente todos os servidores upstream", "parallel_requests": "Solicitações paralelas", @@ -32,6 +32,7 @@ "form_error_ip_format": "Formato de endereço IPv inválido", "form_error_mac_format": "Formato do endereço MAC inválido", "form_error_client_id_format": "Formato do ID de cliente inválido", + "form_error_server_name": "Nome de servidor inválido", "form_error_positive": "Deve ser maior que 0", "form_error_negative": "Deve ser igual ou superior a 0", "range_end_error": "Deve ser maior que o início do intervalo", @@ -125,18 +126,18 @@ "no_servers_specified": "Nenhum servidor especificado", "general_settings": "Configurações gerais", "dns_settings": "Configurações de DNS", - "dns_blocklists": "Listas negra de DNS", - "dns_allowlists": "Listas branca de DNS", - "dns_blocklists_desc": "O AdGuard Home bloqueará domínios que correspondam às listas negras.", - "dns_allowlists_desc": "Os domínios das listas branca de DNS serão permitidos mesmo que estejam em qualquer uma das listas negra.", + "dns_blocklists": "Listas de bloqueio de DNS", + "dns_allowlists": "Listas de permissões de DNS", + "dns_blocklists_desc": "O AdGuard Home bloqueará domínios que correspondam às listas de bloqueio.", + "dns_allowlists_desc": "Os domínios das listas de permissões de DNS serão permitidos mesmo que estejam em qualquer uma das listas de bloqueio.", "custom_filtering_rules": "Regras de filtragem personalizadas", "encryption_settings": "Configurações de criptografia", "dhcp_settings": "Configurações de DHCP", - "upstream_dns": "Servidores DNS upstream", + "upstream_dns": "Servidores DNS primário", "upstream_dns_help": "Insira os endereços dos servidores, um por linha. Saber mais sobre a configuração de servidores DNS primários.", "upstream_dns_configured_in_file": "Configurado em {{path}}", - "test_upstream_btn": "Testar upstreams", - "upstreams": "Upstreams", + "test_upstream_btn": "Testar DNS primário", + "upstreams": "DNS primário", "apply_btn": "Aplicar", "disabled_filtering_toast": "Filtragem desativada", "enabled_filtering_toast": "Filtragem ativada", @@ -157,22 +158,22 @@ "delete_table_action": "Excluir", "elapsed": "Tempo decorrido", "filters_and_hosts_hint": "O AdGuard Home entende regras básicas de bloqueio de anúncios e a sintaxe de arquivos de hosts.", - "no_blocklist_added": "Nenhuma lista negra foi adicionada", - "no_whitelist_added": "Nenhuma lista branca foi adicionada", - "add_blocklist": "Adicionar lista negra", - "add_allowlist": "Adicionar lista branca", + "no_blocklist_added": "Nenhuma lista de bloqueio adicionada", + "no_whitelist_added": "Nenhuma lista de permissões foi adicionada", + "add_blocklist": "Adicionar lista de bloqueio", + "add_allowlist": "Adicionar lista de permissões", "cancel_btn": "Cancelar", "enter_name_hint": "Digite o nome", "enter_url_or_path_hint": "Digite a URL ou o local da lista", "check_updates_btn": "Verificar atualizações", - "new_blocklist": "Nova lista negra", - "new_allowlist": "Nova lista branca", - "edit_blocklist": "Editar lista negra", - "edit_allowlist": "Editar lista branca", - "choose_blocklist": "Escolher as listas negras", - "choose_allowlist": "Escolher as listas brancas", - "enter_valid_blocklist": "Digite uma URL válida para a lista negra.", - "enter_valid_allowlist": "Digite uma URL válida para a lista branca.", + "new_blocklist": "Nova lista de bloqueio", + "new_allowlist": "Nova lista de permissão", + "edit_blocklist": "Editar lista de bloqueio", + "edit_allowlist": "Editar lista de permissões", + "choose_blocklist": "Escolher as listasde bloqueio", + "choose_allowlist": "Escolher as listas de permissões", + "enter_valid_blocklist": "Digite um URL válido para a lista de bloqueio.", + "enter_valid_allowlist": "Digite uma URL válida para a lista de permissões.", "form_error_url_format": "Formato da URL inválida", "form_error_url_or_path_format": "URL ou local da lista inválida", "custom_filter_rules": "Regras de filtragem personalizadas", @@ -247,10 +248,16 @@ "custom_ip": "IP personalizado", "blocking_ipv4": "Bloqueando IPv4", "blocking_ipv6": "Bloqueando IPv6", + "dnscrypt": "DNSCrypt", "dns_over_https": "DNS-sobre-HTTPS", "dns_over_tls": "DNS-sobre-TLS", + "dns_over_quic": "DNS-sobre-QUIC", + "client_id": "ID do cliente", + "client_id_placeholder": "Digite o ID do cliente", + "client_id_desc": "Diferentes clientes podem ser identificados por um ID de cliente especial. Aqui você pode aprender mais sobre como identificar clientes.", "download_mobileconfig_doh": "BAixar .mobileconfig para DNS-sobre-HTTPS", "download_mobileconfig_dot": "BAixar .mobileconfig para DNS-sobre-TLS", + "download_mobileconfig": "Baixar arquivo de configuração", "plain_dns": "DNS simples", "form_enter_rate_limit": "Insira a taxa limite", "rate_limit": "Taxa limite", @@ -269,7 +276,7 @@ "source_label": "Fonte", "found_in_known_domain_db": "Encontrado no banco de dados de domínios conhecidos.", "category_label": "Categoria", - "rule_label": "Regra", + "rule_label": "Regra(s)", "list_label": "Lista", "unknown_filter": "Filtro desconhecido {{filterId}}", "known_tracker": "Rastreador conhecido", @@ -330,7 +337,7 @@ "encryption_config_saved": "Configuração de criptografia salva", "encryption_server": "Nome do servidor", "encryption_server_enter": "Digite seu nome de domínio", - "encryption_server_desc": "Para usar o protocolo HTTPS, você precisa digitar o nome do servidor que corresponde ao seu certificado SSL.", + "encryption_server_desc": "Para usar HTTPS, você precisa inserir o nome do servidor que corresponda ao seu certificado SSL ou certificado curinga. Se o campo não estiver definido, ele aceitará conexões TLS para qualquer domínio.", "encryption_redirect": "Redirecionar automaticamente para HTTPS", "encryption_redirect_desc": "Se marcado, o AdGuard Home irá redirecionar automaticamente os endereços HTTP para HTTPS.", "encryption_https": "Porta HTTPS", @@ -386,7 +393,7 @@ "client_edit": "Editar cliente", "client_identifier": "Identificador", "ip_address": "Endereço de IP", - "client_identifier_desc": "Clientes podem ser identificados pelo endereço de IP ou pelo endereço MAC. Observe que o uso do endereço MAC como identificador só é possível se o AdGuard Home também for um <0>servidor DHCP", + "client_identifier_desc": "Os clientes podem ser identificados pelo endereço IP, CIDR, Endereço MAC ou um ID de cliente especial (pode ser usado para DoT/DoH/DoQ). <0>Aqui você pode aprender mais sobre como identificar clientes.", "form_enter_ip": "Digite o endereço de IP", "form_enter_mac": "Digite o endereço MAC", "form_enter_id": "Inserir identificador", @@ -430,6 +437,7 @@ "setup_dns_privacy_other_3": "<0>dnscrypt-proxy suporta <1>DNS-sobre-HTTPS", "setup_dns_privacy_other_4": "<0>Mozilla Firefox suporta <1>DNS-sobre-HTTPS.", "setup_dns_privacy_other_5": "Você encontrará mais implementações <0>aqui e <1>aqui.", + "setup_dns_privacy_ioc_mac": "configuração para iOS e macOS", "setup_dns_notice": "Para usar o <1>DNS-sobre-HTTPS ou <1>DNS-sobre-TLS, você precisa <0>configurar a criptografia nas configurações do AdGuard Home.", "rewrite_added": "Reescrita de DNS para \"{{key}}\" adicionada com sucesso", "rewrite_deleted": "Reescrita de DNS para \"{{key}}\" excluída com sucesso", @@ -529,7 +537,6 @@ "check_ip": "Endereços de IP: {{ip}}", "check_cname": "CNAME: {{cname}}", "check_reason": "Motivo: {{reason}}", - "check_rule": "Regra: {{rule}}", "check_service": "Nome do serviço: {{service}}", "service_name": "Nome do serviço", "check_not_found": "Não encontrado em suas listas de filtros", @@ -560,7 +567,7 @@ "filtered": "Filtrado", "rewritten": "Reescrito", "safe_search": "Pesquisa segura", - "blocklist": "Lista negra", + "blocklist": "Lista de bloqueio", "milliseconds_abbreviation": "ms", "cache_size": "Tamanho do cache", "cache_size_desc": "Tamanho do cache do DNS (em bytes)", @@ -579,7 +586,7 @@ "filter_category_general_desc": "Listas que bloqueiam o rastreamento e a publicidade na maioria dos dispositivos", "filter_category_security_desc": "Listas especializadas em bloquear domínios de malware, phishing ou fraude", "filter_category_regional_desc": "Listas focadas em anúncios regionais e servidores de rastreamento", - "filter_category_other_desc": "Outras listas negras", + "filter_category_other_desc": "Outras listas de bloqueio", "setup_config_to_enable_dhcp_server": "Configure a configuração para habilitar o servidor DHCP", "original_response": "Resposta original", "click_to_view_queries": "Clique para ver as consultas", diff --git a/client/src/__locales/pt-pt.json b/client/src/__locales/pt-pt.json index 13151fb3..8e08db07 100644 --- a/client/src/__locales/pt-pt.json +++ b/client/src/__locales/pt-pt.json @@ -1,12 +1,12 @@ { "client_settings": "Definições do cliente", - "example_upstream_reserved": "pode especificar um DNS upstream <0>para domínio(s) especifico(s)", + "example_upstream_reserved": "Podes especificar um DNS primário <0>para domínio(s) especifico(s)", "example_upstream_comment": "Tu podes especificar o comentário", "upstream_parallel": "Usar consultas paralelas para acelerar a resolução consultando simultaneamente todos os servidores upstream", "parallel_requests": "Solicitações paralelas", "load_balancing": "Balanceamento de carga", "load_balancing_desc": "Consulta um servidor de cada vez. O AdGuard Home usará o algoritmo aleatório ponderado para escolher o servidor, para que o servidor mais rápido seja usado com mais frequência.", - "bootstrap_dns": "Servidores DNS de inicialização", + "bootstrap_dns": "Servidores DNS de arranque", "bootstrap_dns_desc": "Servidores DNS de inicialização são usados para resolver endereços IP dos resolvedores DoH/DoT que especifica como upstreams.", "check_dhcp_servers": "Verificar por servidores DHCP", "save_config": "Guardar definição", @@ -23,7 +23,7 @@ "dhcp_leases": "Concessões DHCP", "dhcp_static_leases": "Concessões de DHCP estático", "dhcp_leases_not_found": "Nenhuma concessão DHCP encontrada", - "dhcp_config_saved": "Configurações DHCP guardadas com sucesso", + "dhcp_config_saved": "Definições DHCP guardadas com sucesso", "dhcp_ipv4_settings": "Definições DHCP IPv4", "dhcp_ipv6_settings": "Definições DHCP IPv6", "form_error_required": "Campo obrigatório", @@ -32,6 +32,7 @@ "form_error_ip_format": "Formato de endereço IPv4 inválido", "form_error_mac_format": "Formato do endereço MAC inválido", "form_error_client_id_format": "Formato inválido", + "form_error_server_name": "Nome de servidor inválido", "form_error_positive": "Deve ser maior que 0", "form_error_negative": "Deve ser igual ou superior a 0", "range_end_error": "Deve ser maior que o início do intervalo", @@ -75,7 +76,7 @@ "query_log": "Registo de consultas", "compact": "Compacto", "nothing_found": "Nada encontrado", - "faq": "Perguntas frequentes", + "faq": "FAQ", "version": "Versão", "address": "Endereço", "protocol": "Protocolo", @@ -91,7 +92,7 @@ "disabled_protection": "Desactivar protecção", "refresh_statics": "Repor estatísticas", "dns_query": "Consultas de DNS", - "blocked_by": "<0>Bloqueado por Filtros", + "blocked_by": "<0>Bloqueado por filtros", "stats_malware_phishing": "Malware/phishing bloqueados", "stats_adult": "Sites adultos bloqueados", "stats_query_domain": "Principais domínios consultados", @@ -125,18 +126,18 @@ "no_servers_specified": "Nenhum servidor especificado", "general_settings": "Definições gerais", "dns_settings": "Definições de DNS", - "dns_blocklists": "Listas negra de DNS", - "dns_allowlists": "Listas branca de DNS", - "dns_blocklists_desc": "O AdGuard Home bloqueará domínios que correspondam às listas negras.", - "dns_allowlists_desc": "Os domínios das listas branca de DNS serão permitidos mesmo que estejam em qualquer uma das listas negra.", + "dns_blocklists": "Lista de bloqueio de DNS", + "dns_allowlists": "Listas de permissões de DNS", + "dns_blocklists_desc": "O AdGuard Home bloqueará domínios que correspondam às listas de bloqueio.", + "dns_allowlists_desc": "Os domínios das listas de permissões de DNS serão permitidos mesmo que estejam em qualquer uma das listas de bloqueio.", "custom_filtering_rules": "Regras de filtragem personalizadas", - "encryption_settings": "Configurações de criptografia", - "dhcp_settings": "Configurações de DHCP", - "upstream_dns": "Servidores DNS upstream", + "encryption_settings": "Definições de criptografia", + "dhcp_settings": "Definições de DHCP", + "upstream_dns": "Servidores DNS primário", "upstream_dns_help": "Insira os endereços dos servidores, um por linha. Saber mais sobre a definição de servidores DNS primários.", "upstream_dns_configured_in_file": "Configurado em {{path}}", - "test_upstream_btn": "Testar upstreams", - "upstreams": "Upstreams", + "test_upstream_btn": "Testar DNS primário", + "upstreams": "DNS primário", "apply_btn": "Aplicar", "disabled_filtering_toast": "Filtragem desactivada", "enabled_filtering_toast": "Filtragem activada", @@ -157,22 +158,22 @@ "delete_table_action": "Apagar", "elapsed": "Tempo decorrido", "filters_and_hosts_hint": "O AdGuard Home entende regras básicas de bloqueio de anúncios e a sintaxe de arquivos de hosts.", - "no_blocklist_added": "Nenhuma lista negra foi adicionada", - "no_whitelist_added": "Nenhuma lista branca foi adicionada", - "add_blocklist": "Adicionar lista negra", - "add_allowlist": "Adicionar lista branca", + "no_blocklist_added": "Nenhuma lista de bloqueio foi adicionada", + "no_whitelist_added": "Nenhuma lista de permissões foi adicionada", + "add_blocklist": "Adicionar lista de bloqueio", + "add_allowlist": "Adicionar lista de permissões", "cancel_btn": "Cancelar", "enter_name_hint": "Insira o nome", "enter_url_or_path_hint": "Digite a URL ou o local da lista", "check_updates_btn": "Verificar actualizações", - "new_blocklist": "Nova lista negra", - "new_allowlist": "Nova lista branca", - "edit_blocklist": "Editar lista negra", - "edit_allowlist": "Editar lista branca", - "choose_blocklist": "Escolher as listas negras", - "choose_allowlist": "Escolher as listas brancas", - "enter_valid_blocklist": "Digite uma URL válida para a lista negra.", - "enter_valid_allowlist": "Digite uma URL válida para a lista branca.", + "new_blocklist": "Nova lista de bloqueio", + "new_allowlist": "Nova lista de permissões", + "edit_blocklist": "Editar lista de bloqueio", + "edit_allowlist": "Editar lista de permissões", + "choose_blocklist": "Escolher as listas de bloqueio", + "choose_allowlist": "Escolher as listas de permissões", + "enter_valid_blocklist": "Digite uma URL válida para a lista de bloqueio.", + "enter_valid_allowlist": "Digite uma URL válida para a lista de permissões.", "form_error_url_format": "Formato da URL inválida", "form_error_url_or_path_format": "URL ou local da lista inválida", "custom_filter_rules": "Regras de filtragem personalizadas", @@ -185,7 +186,7 @@ "example_comment_meaning": "apenas um comentário", "example_comment_hash": "# Também um comentário", "example_regex_meaning": "bloquear o acesso aos domínios que correspondam à expressão regular especificada", - "example_upstream_regular": "dNS regular (através do UDP)", + "example_upstream_regular": "DNS regular (através do UDP)", "example_upstream_dot": "<0>DNS-sobre-TLS criptografado", "example_upstream_doh": "<0>DNS-sobre-HTTPS criptografado", "example_upstream_doq": "<0>DNS-sobre-QUIC criptografado", @@ -247,10 +248,16 @@ "custom_ip": "IP Personalizado", "blocking_ipv4": "A bloquear IPv4", "blocking_ipv6": "A bloquear IPv6", + "dnscrypt": "DNSCrypt", "dns_over_https": "DNS-sobre-HTTPS", "dns_over_tls": "DNS-sobre-TLS", + "dns_over_quic": "DNS-sobre-QUIC", + "client_id": "ID do cliente", + "client_id_placeholder": "Insira o ID do cliente", + "client_id_desc": "Diferentes clientes podem ser identificados por um ID de cliente especial. Aqui você pode aprender mais sobre como identificar clientes.", "download_mobileconfig_doh": "Transferir .mobileconfig para DNS-sobre-HTTPS", "download_mobileconfig_dot": "Transferir .mobileconfig para DNS-sobre-TLS", + "download_mobileconfig": "Transferir arquivo de configuração", "plain_dns": "DNS simples", "form_enter_rate_limit": "Insira o limite de taxa", "rate_limit": "Limite de taxa", @@ -269,7 +276,7 @@ "source_label": "Fonte", "found_in_known_domain_db": "Encontrado no banco de dados de domínios conhecido.", "category_label": "Categoria", - "rule_label": "Regra", + "rule_label": "Regra(s)", "list_label": "Lista", "unknown_filter": "Filtro desconhecido {{filterId}}", "known_tracker": "Rastreador conhecido", @@ -330,7 +337,7 @@ "encryption_config_saved": "Definição de criptografia guardada", "encryption_server": "Nome do servidor", "encryption_server_enter": "Insira o seu nome de domínio", - "encryption_server_desc": "Para usar o protocolo HTTPS, precisa de inserir o nome do servidor que corresponde ao seu certificado SSL.", + "encryption_server_desc": "Para usar HTTPS, você precisa inserir o nome do servidor que corresponda ao seu certificado SSL ou certificado curinga. Se o campo não estiver definido, ele aceitará conexões TLS para qualquer domínio.", "encryption_redirect": "Redireccionar automaticamente para HTTPS", "encryption_redirect_desc": "Se marcado, o AdGuard Home irá redireccionar automaticamente os endereços HTTP para HTTPS.", "encryption_https": "Porta HTTPS", @@ -338,7 +345,7 @@ "encryption_dot": "Porta DNS-sobre-TLS", "encryption_dot_desc": "Se essa porta estiver configurada, o AdGuard Home irá executar o servidor DNS-sobre- TSL nesta porta.", "encryption_doq": "Porta DNS-sobre-QUIC", - "encryption_doq_desc": "Se esta porta estiver configurada, o AdGuard Home executará um servidor DNS-sobre-QUIC nesta porta. É experimental e pode não ser confiável. Além disso, não há muitos clientes que ofereçam suporte no momento.", + "encryption_doq_desc": "Se esta porta estiver configurada, o AdGuard Home executará um servidor DNS-sobre-QUIC nesta porta. É experimental e pode não ser confiável. Além disso, não há demasiados clientes que ofereçam suporte no momento.", "encryption_certificates": "Certificados", "encryption_certificates_desc": "Para usar criptografia, precisa de fornecer uma cadeia de certificados SSL válida para o seu domínio. Pode obter um certificado gratuito em <0> {{link}} ou pode comprá-lo numa das autoridades de certificação confiáveis.", "encryption_certificates_input": "Copie/cole aqui o seu certificado codificado em PEM.", @@ -362,7 +369,7 @@ "form_error_port_unsafe": "Esta porta não é segura", "form_error_equal": "Não deve ser igual", "form_error_password": "As palavras-passe não coincidem", - "reset_settings": "Repor configurações", + "reset_settings": "Repor definições", "update_announcement": "AdGuard Home {{version}} está disponível!<0>Clique aqui para mais informações.", "setup_guide": "Guia de instalação", "dns_addresses": "Endereços DNS", @@ -386,14 +393,14 @@ "client_edit": "Editar cliente", "client_identifier": "Identificador", "ip_address": "Endereço de IP", - "client_identifier_desc": "Os clientes podem ser identificados pelo endereço de IP, CIDR, ou pelo endereço MAC. Observe que o uso do endereço MAC como identificador só é possível se o AdGuard Home também for um <0>servidor DHCP", + "client_identifier_desc": "Os clientes podem ser identificados pelo endereço IP, CIDR, Endereço MAC ou um ID de cliente especial (pode ser usado para DoT/DoH/DoQ). <0>Aqui você pode aprender mais sobre como identificar clientes.", "form_enter_ip": "Insira IP", "form_enter_mac": "Insira o endereço MAC", "form_enter_id": "Inserir identificador", "form_add_id": "Adicionar identificador", "form_client_name": "Insira o nome do cliente", "name": "Nome", - "client_global_settings": "Usar configurações globais", + "client_global_settings": "Usar definições globais", "client_deleted": "Cliente \"{{key}}\" excluído com sucesso", "client_added": "Cliente \"{{key}}\" adicionado com sucesso", "client_updated": "Cliente \"{{key}}\" actualizado com sucesso", @@ -402,7 +409,7 @@ "list_confirm_delete": "Você tem certeza de que deseja excluir essa lista?", "auto_clients_title": "Clientes (tempo de execução)", "auto_clients_desc": "Dados dos clientes que usam o AdGuard Home, que não são armazenados na definição", - "access_title": "Configurações de acesso", + "access_title": "Definições de acesso", "access_desc": "Aqui pode configurar as regras de acesso para o servidores de DNS do AdGuard Home.", "access_allowed_title": "Clientes permitidos", "access_allowed_desc": "Uma lista de endereços IP ou CIDR. Ao configurar, o AdGuard Home irá permitir solicitações apenas desses endereços de IP.", @@ -410,7 +417,7 @@ "access_disallowed_desc": "Uma lista de endereços IP ou CIDR. Ao configurar, o AdGuard Home irá descartar as solicitações desses endereços de IP.", "access_blocked_title": "Domínios bloqueados", "access_blocked_desc": "Não confunda isso com os filtros. O AdGuard Home irá descartar as consultas DNS com esses domínios.", - "access_settings_saved": "Configurações de acesso foram guardadas com sucesso", + "access_settings_saved": "Definições de acesso foram guardadas com sucesso", "updates_checked": "Actualizações verificadas com sucesso", "updates_version_equal": "O AdGuard Home está actualizado", "check_updates_now": "Verificar actualizações", @@ -430,6 +437,7 @@ "setup_dns_privacy_other_3": "<0>dnscrypt-proxy suporta <1>DNS-sobre-HTTPS.", "setup_dns_privacy_other_4": "<0>Mozilla Firefox suporta <1>DNS-sobre-HTTPS.", "setup_dns_privacy_other_5": "Encontrará mais implementações <0>aqui e <1>aqui.", + "setup_dns_privacy_ioc_mac": "configuração para iOS e macOS", "setup_dns_notice": "Para usar o <1>DNS-sobre-HTTPS ou <1>DNS-sobre-TLS, precisa de <0>configurar a criptografia nas configurações do AdGuard Home.", "rewrite_added": "Reescrita de DNS para \"{{key}}\" adicionada com sucesso", "rewrite_deleted": "Reescrita de DNS para \"{{key}}\" excluída com sucesso", @@ -487,7 +495,7 @@ "username_placeholder": "Insira o nome de utilizador", "password_label": "Palavra-passe", "password_placeholder": "Insira palavra-passe", - "sign_in": "Entrar", + "sign_in": "Iniciar sessão", "sign_out": "Sair", "forgot_password": "Não se lembra da palavra-passe?", "forgot_password_desc": "Siga <0>estes passos para criar uma nova palavra-passe para a sua conta de utilizador.", @@ -529,7 +537,6 @@ "check_ip": "Endereços de IP: {{ip}}", "check_cname": "CNAME: {{cname}}", "check_reason": "Motivo: {{reason}}", - "check_rule": "Regra: {{rule}}", "check_service": "Nome do serviço: {{service}}", "service_name": "Nome do serviço", "check_not_found": "Não encontrado nas tuas listas de filtros", @@ -551,7 +558,7 @@ "validated_with_dnssec": "Validado com DNSSEC", "all_queries": "Todas as consultas", "show_blocked_responses": "Bloqueado", - "show_whitelisted_responses": "Lista Branca", + "show_whitelisted_responses": "Na lista branca", "show_processed_responses": "Processado", "blocked_safebrowsing": "Bloqueado pela navegação segura", "blocked_adult_websites": "Sítios adultos bloqueados", @@ -560,7 +567,7 @@ "filtered": "Filtrado", "rewritten": "Reescrito", "safe_search": "Pesquisa segura", - "blocklist": "Lista negra", + "blocklist": "Lista de bloqueio", "milliseconds_abbreviation": "ms", "cache_size": "Tamanho do cache", "cache_size_desc": "Tamanho do cache do DNS (em bytes)", @@ -575,11 +582,11 @@ "filter_category_general": "Geral", "filter_category_security": "Segurança", "filter_category_regional": "Regional", - "filter_category_other": "Outro", + "filter_category_other": "Noutro", "filter_category_general_desc": "Listas que bloqueiam o monitorização e a publicidade na maioria dos dispositivos", "filter_category_security_desc": "Listas especializadas em bloquear domínios de malware, phishing ou fraude", "filter_category_regional_desc": "Listas focadas em anúncios regionais e servidores de monitorização", - "filter_category_other_desc": "Outras listas negras", + "filter_category_other_desc": "Outras listas de bloqueio", "setup_config_to_enable_dhcp_server": "Defina a definição para habilitar o servidor DHCP", "original_response": "Resposta original", "click_to_view_queries": "Clique para ver as consultas", diff --git a/client/src/__locales/ro.json b/client/src/__locales/ro.json index 5295b2dc..80929301 100644 --- a/client/src/__locales/ro.json +++ b/client/src/__locales/ro.json @@ -1,6 +1,6 @@ { "client_settings": "Setări client", - "example_upstream_reserved": "Puteți preciza un DNS upstream <0>de domeniu(ii) specific(e)", + "example_upstream_reserved": "Puteți preciza un DNS în amonte <0>de domeniu(ii) specific(e)", "example_upstream_comment": "Puteți specifica comentariul", "upstream_parallel": "Folosiți interogări paralele pentru rezolvări rapide interogând simultan toate serverele în amonte", "parallel_requests": "Solicitări paralele", @@ -32,6 +32,7 @@ "form_error_ip_format": "Format IP invalid", "form_error_mac_format": "Format MAC invalid", "form_error_client_id_format": "Format ID de client invalid", + "form_error_server_name": "Nume de server nevalid", "form_error_positive": "Trebuie să fie mai mare de 0", "form_error_negative": "Trebuie să fie egală cu 0 sau mai mare", "range_end_error": "Trebuie să fie mai mare decât începutul intervalului", @@ -107,7 +108,7 @@ "number_of_dns_query_days": "Un număr de interogări DNS procesate în ultima {{count}} zi", "number_of_dns_query_days_plural": "Un număr de interogări DNS procesate în ultimele {{count}} zile", "number_of_dns_query_24_hours": "Un număr de interogări DNS procesate în ultimele 24 de ore", - "number_of_dns_query_blocked_24_hours": "Un număr de solicitări DNS blocate de filtrele de blocare și listele de blocaj de hosts", + "number_of_dns_query_blocked_24_hours": "Un număr de solicitări DNS blocate de filtrele de blocare și lista de blocaje din hosts", "number_of_dns_query_blocked_24_hours_by_sec": "Un număr de solicitări DNS blocate de modulul de securitate de navigare AdGuard", "number_of_dns_query_blocked_24_hours_adult": "Un număr de site-uri web pentru adulți blocate", "enforced_save_search": "Căutare protejată întărită", @@ -117,7 +118,7 @@ "block_domain_use_filters_and_hosts": "Blocați domenii folosind filtre și fișiere hosts", "filters_block_toggle_hint": "Puteți configura regulile de blocare în setările Filtre.", "use_adguard_browsing_sec": "Utilizați serviciul Navigarea în Securitate AdGuard", - "use_adguard_browsing_sec_hint": "AdGuard Home va verifica dacă domeniul este în lista negră a serviciul web de securitate de navigare. Pentru acesta va utiliza un lookup API discret: un prefix scurt al numelui de domeniu SHA256 hash este trimis serverului.", + "use_adguard_browsing_sec_hint": "AdGuard Home va verifica dacă domeniul este în lista de blocări a serviciul web de securitate de navigare. Pentru acesta va utiliza un lookup API discret: un prefix scurt al numelui de domeniu SHA256 hash este trimis serverului.", "use_adguard_parental": "Utilizați Controlul Parental AdGuard", "use_adguard_parental_hint": "AdGuard Home va verifica pentru conținut adult pe domeniu. Utilizează același API discret ca cel utilizat de serviciul de securitate de navigare.", "enforce_safe_search": "Căutare protejată întărită", @@ -125,14 +126,14 @@ "no_servers_specified": "Nu sunt specificate servere", "general_settings": "Setări Generale", "dns_settings": "Setări DNS", - "dns_blocklists": "DNS liste blocări", - "dns_allowlists": "DNS liste autorizări", + "dns_blocklists": "Liste de blocări DNS", + "dns_allowlists": "Listă de DNS-uri autorizate", "dns_blocklists_desc": "AdGuard Home blochează domenii incluse în liste de blocări.", "dns_allowlists_desc": "Domeniile DNS autorizate vor fi permise, chiar dacă se află pe orice listă de blocări.", "custom_filtering_rules": "Reguli filtrare personale", "encryption_settings": "Setări de criptare", "dhcp_settings": "Setări DHCP", - "upstream_dns": "Servere upstream DNS", + "upstream_dns": "Servere DNS în amonte", "upstream_dns_help": "Introduceți adresele serverelor una pe linie. Aflați mai multe despre configurarea serverelor DNS în amonte.", "upstream_dns_configured_in_file": "Configurat în {{path}}", "test_upstream_btn": "Testați upstreams", @@ -170,7 +171,7 @@ "edit_blocklist": "Editare blocare", "edit_allowlist": "Editare autorizare", "choose_blocklist": "Alegeți liste de blocări", - "choose_allowlist": "Alegeți liste de permisiuni", + "choose_allowlist": "Alegeți liste de autorizări", "enter_valid_blocklist": "Introduceți un URL valid pentru blocare.", "enter_valid_allowlist": "Introduceți un URL valid pentru autorizare.", "form_error_url_format": "Format URL invalid", @@ -192,7 +193,7 @@ "example_upstream_sdns": "puteți utiliza <0>DNS Stamps pentru rezolvere <1>DNSCrypt sau <2>DNS-over-HTTPS", "example_upstream_tcp": "DNS clasic (over TCP)", "all_lists_up_to_date_toast": "Toate listele sunt deja la zi", - "updated_upstream_dns_toast": "Serverele DNS upstream aduse la zi", + "updated_upstream_dns_toast": "Serverele DNS în amonte aduse la zi", "dns_test_ok_toast": "Serverele DNS specificate funcționează corect", "dns_test_not_ok_toast": "Serverul \"{{key}}\": nu a putut fi utilizat, verificați dacă l-ați scris corect", "unblock": "Deblocați", @@ -212,7 +213,7 @@ "empty_response_status": "Gol", "show_all_filter_type": "Arată tot", "show_filtered_type": "Arată cele filtrate", - "no_logs_found": "Nici un jurnal găsit", + "no_logs_found": "Niciun jurnal găsit", "refresh_btn": "Actualizare", "previous_btn": "Anterior", "next_btn": "Următor", @@ -247,10 +248,16 @@ "custom_ip": "IP personalizat", "blocking_ipv4": "Blocarea IPv4", "blocking_ipv6": "Blocarea IPv6", + "dnscrypt": "DNSCrypt", "dns_over_https": "DNS-over-HTTPS", "dns_over_tls": "DNS-over-TLS", + "dns_over_quic": "DNS-over-QUIC", + "client_id": "ID Client", + "client_id_placeholder": "Introduceți ID client", + "client_id_desc": "Diferiți clienți pot fi identificați printr-un ID special al clientului. Aici puteți afla mai multe despre cum să identificați clienții.", "download_mobileconfig_doh": "Descărcați .mobileconfig pentru DNS-over-HTTPS", "download_mobileconfig_dot": "Descărcați .mobileconfig pentru DNS-over-TLS", + "download_mobileconfig": "Descărcați fișierul de configurare", "plain_dns": "DNS simplu", "form_enter_rate_limit": "Introduceți limita ratei", "rate_limit": "Limita ratei", @@ -269,7 +276,7 @@ "source_label": "Sursă", "found_in_known_domain_db": "Găsit în baza de date de domenii cunoscută.", "category_label": "Categorie", - "rule_label": "Regulă", + "rule_label": "Regulă(reguli)", "list_label": "Listă", "unknown_filter": "Filtru necunoscut {{filterId}}", "known_tracker": "Tracker cunoscut", @@ -330,7 +337,7 @@ "encryption_config_saved": "Configurația de criptare salvată", "encryption_server": "Nume de server", "encryption_server_enter": "Introduceți numele domeniului", - "encryption_server_desc": "Pentru a utiliza HTTPS, trebuie introdus numele serverului care corespunde certificatului SSL.", + "encryption_server_desc": "Pentru a utiliza HTTPS, trebuie să introduceți numele serverului care se potrivește cu certificatul SSL sau certificatul wildcard al dvs. În cazul în care câmpul nu este setat, va accepta conexiuni TLS pentru orice domeniu.", "encryption_redirect": "Redirecționați automat la HTTPS", "encryption_redirect_desc": "Dacă este bifat, AdGuard Home vă va redirecționa automat de la adrese HTTP la HTTPS.", "encryption_https": "Port HTTPS", @@ -386,7 +393,7 @@ "client_edit": "Editare client", "client_identifier": "Identificator", "ip_address": "Adresa IP", - "client_identifier_desc": "Clienții pot fi identificați prin adresa IP, CIDR, adresa MAC. Vă rugăm să rețineți că utilizarea MAC ca identificator este posibilă numai dacă AdGuard Home este și un <0>server DHCP", + "client_identifier_desc": "Clienții pot fi identificați prin adresa IP, CIDR, adresa MAC sau un ID special al clientului (poate fi folosit pentru DoT/DoH/DoQ). <0>Aici puteți afla mai multe despre cum să identificați clienții.", "form_enter_ip": "Introduceți IP", "form_enter_mac": "Introduceți MAC", "form_enter_id": "Introduceți identificator", @@ -430,6 +437,7 @@ "setup_dns_privacy_other_3": "<0>dnscrypt-proxy acceptă <1>DNS-over-HTTPS.", "setup_dns_privacy_other_4": "<0>Mozilla Firefox acceptă <1>DNS-over-HTTPS.", "setup_dns_privacy_other_5": "Veți găsi mai multe implementări <0>aici și <1>aici.", + "setup_dns_privacy_ioc_mac": "Configurarea iOS și macOS", "setup_dns_notice": "Pentru a utiliza <1>DNS-over-HTTPS sau <1>DNS-over-TLS, trebuie să <0>configurați Criptarea în setările AdGuard Home.", "rewrite_added": "Rescriere DNS pentru \"{{key}}\" adăugată cu succes", "rewrite_deleted": "Rescriere DNS pentru \"{{key}}\" ștearsă cu succes", @@ -529,7 +537,6 @@ "check_ip": "Adrese IP: {{ip}}", "check_cname": "CNAME: {{cname}}", "check_reason": "Cauza: {{reason}}", - "check_rule": "Regula: {{rule}}", "check_service": "Nume servici: {{service}}", "service_name": "Numele serviciului", "check_not_found": "Nu se găsește în listele de filtre", @@ -560,7 +567,7 @@ "filtered": "Filtrate", "rewritten": "Rescrise", "safe_search": "Căutare sigură", - "blocklist": "Lista neagră", + "blocklist": "Lista de blocări", "milliseconds_abbreviation": "ms", "cache_size": "Mărime cache", "cache_size_desc": "Mărime cache DNS (în octeți)", diff --git a/client/src/__locales/ru.json b/client/src/__locales/ru.json index c0fb3705..1b9f0a39 100644 --- a/client/src/__locales/ru.json +++ b/client/src/__locales/ru.json @@ -32,6 +32,7 @@ "form_error_ip_format": "Неверный формат IP-адреса", "form_error_mac_format": "Некорректный формат MAC", "form_error_client_id_format": "Неверный формат ID клиента", + "form_error_server_name": "Неверное имя сервера", "form_error_positive": "Должно быть больше 0", "form_error_negative": "Должно быть не меньше 0", "range_end_error": "Должно превышать начало диапазона", @@ -247,10 +248,16 @@ "custom_ip": "Свой IP", "blocking_ipv4": "Блокировка IPv4", "blocking_ipv6": "Блокировка IPv6", + "dnscrypt": "DNSCrypt", "dns_over_https": "DNS-over-HTTPS", "dns_over_tls": "DNS-over-TLS", + "dns_over_quic": "DNS-over-QUIC", + "client_id": "Идентификатор клиента", + "client_id_placeholder": "Введите идентификатор клиента", + "client_id_desc": "Различные клиенты могут идентифицироваться по специальному идентификатору клиента. Здесь вы можете узнать больше об идентификации клиентов.", "download_mobileconfig_doh": "Скачать .mobileconfig для DNS-over-HTTPS", "download_mobileconfig_dot": "Скачать .mobileconfig для DNS-over-TLS", + "download_mobileconfig": "Загрузить файл конфигурации", "plain_dns": "Нешифрованный DNS", "form_enter_rate_limit": "Введите rate limit", "rate_limit": "Rate limit", @@ -269,7 +276,7 @@ "source_label": "Источник", "found_in_known_domain_db": "Найден в базе известных доменов.", "category_label": "Категория", - "rule_label": "Правило", + "rule_label": "Правило(-а)", "list_label": "Список", "unknown_filter": "Неизвестный фильтр {{filterId}}", "known_tracker": "Известный трекер", @@ -330,7 +337,7 @@ "encryption_config_saved": "Настройки шифрования сохранены", "encryption_server": "Имя сервера", "encryption_server_enter": "Введите ваше доменное имя", - "encryption_server_desc": "Для использования HTTPS вам необходимо ввести имя сервера, которое подходит вашему SSL-сертификату.", + "encryption_server_desc": "Для использования HTTPS вам необходимо ввести имя сервера, которое подходит вашему SSL-сертификату или сертификату с поддержкой поддоменов. Если это поле не задано, сервер будет принимать TLS-соединения для любого домена.", "encryption_redirect": "Автоматически перенаправлять на HTTPS", "encryption_redirect_desc": "Если включено, AdGuard Home будет автоматически перенаправлять вас с HTTP на HTTPS адрес.", "encryption_https": "Порт HTTPS", @@ -386,7 +393,7 @@ "client_edit": "Редактировать клиента", "client_identifier": "Идентификатор", "ip_address": "IP-адрес", - "client_identifier_desc": "Клиенты могут быть идентифицированы по IP-адресу, CIDR или MAC-адресу. Обратите внимание, что использование MAC как идентификатора возможно, только если AdGuard Home также является и <0>DHCP-сервером", + "client_identifier_desc": "Клиенты могут быть идентифицированы по IP-адресу, CIDR или MAC-адресу или специальному ID (можно использовать для DoT/DoH/DoQ). <0>Здесь вы можете узнать больше об идентификации клиентов.", "form_enter_ip": "Введите IP", "form_enter_mac": "Введите MAC", "form_enter_id": "Введите идентификатор", @@ -430,6 +437,7 @@ "setup_dns_privacy_other_3": "<0>dnscrypt-proxy поддерживает <1>DNS-over-HTTPS.", "setup_dns_privacy_other_4": "<0>Mozilla Firefox поддерживает <1>DNS-over-HTTPS.", "setup_dns_privacy_other_5": "Вы можете найти еще варианты <0>тут и <1>тут.", + "setup_dns_privacy_ioc_mac": "Конфигурация для iOS и macOS", "setup_dns_notice": "Чтобы использовать <1>DNS-over-HTTPS или <1>DNS-over-TLS, вам нужно <0>настроить шифрование в настройках AdGuard Home.", "rewrite_added": "Правило перенаправления DNS для \"{{key}}\" успешно добавлено", "rewrite_deleted": "Правило перенаправления DNS для \"{{key}}\" успешно удалено", @@ -511,7 +519,7 @@ "disable_ipv6": "Отключить IPv6", "disable_ipv6_desc": "Если эта опция включена, все DNS-запросы адресов IPv6 (тип AAAA) будут игнорироваться.", "fastest_addr": "Самый быстрый IP-адрес", - "fastest_addr_desc": "Опросить все DNS-серверы и вернуть самый быстрый IP-адрес из полученных ответов", + "fastest_addr_desc": "Опросить все DNS-серверы и вернуть самый быстрый IP-адрес из полученных ответов. Это замедлит DNS-запросы, так как нужно будет дождаться ответов со всех DNS-серверов, но улучшит соединение.", "autofix_warning_text": "При нажатии \"Исправить\" AdGuard Home настроит вашу систему на использование DNS-сервера AdGuard Home.", "autofix_warning_list": "Будут выполняться следующие задачи: <0>Деактивировать системный DNSStubListener <0>Установить адрес сервера DNS на 127.0.0.1 <0>Создать символическую ссылку /etc/resolv.conf на /run/systemd/resolve/resolv.conf <0>Остановить DNSStubListener (перезагрузить системную службу).", "autofix_warning_result": "В результате все DNS-запросы от вашей системы будут по умолчанию обрабатываться AdGuard Home.\n", @@ -529,7 +537,6 @@ "check_ip": "IP-адреса: {{ip}}", "check_cname": "CNAME: {{cname}}", "check_reason": "Причина: {{reason}}", - "check_rule": "Правило: {{rule}}", "check_service": "Название сервиса: {{service}}", "service_name": "Имя сервиса", "check_not_found": "Не найдено в вашем списке фильтров", diff --git a/client/src/__locales/si-lk.json b/client/src/__locales/si-lk.json index a3c09d5f..42909355 100644 --- a/client/src/__locales/si-lk.json +++ b/client/src/__locales/si-lk.json @@ -1,5 +1,6 @@ { "client_settings": "අනුග්‍රාහක සැකසුම්", + "example_upstream_comment": "ඔබට අදහසක් සඳහන් කළ හැකිය", "parallel_requests": "සමාන්තර ඉල්ලීම්", "load_balancing": "ධාරිතාව තුලනය", "check_dhcp_servers": "ග.ධා.වි.කෙ. සේවාදායකයන් සඳහා පරීක්ෂා කරන්න", @@ -12,13 +13,13 @@ "dhcp_enable": "ග.ධා.වි.කෙ. සේවාදායකය සබල කරන්න", "dhcp_disable": "ග.ධා.වි.කෙ. සේවාදායකය අබල කරන්න", "dhcp_config_saved": "ග.ධා.වි.කෙ. වින්‍යාසය සාර්ථකව සුරකින ලදි", - "dhcp_ipv4_settings": "ග.ධා.වි.කෙ. IPv4 සැකසුම්", - "dhcp_ipv6_settings": "ග.ධා.වි.කෙ. IPv6 සැකසුම්", + "dhcp_ipv4_settings": "ග.ධා.වි.කෙ. අයිපීවී 4 සැකසුම්", + "dhcp_ipv6_settings": "ග.ධා.වි.කෙ. අයිපීවී 6 සැකසුම්", "form_error_required": "අවශ්‍ය ක්ෂේත්‍රයකි", "form_error_ip4_format": "වලංගු නොවන IPv4 ආකෘතියකි", "form_error_ip6_format": "වලංගු නොවන IPv6 ආකෘතියකි", "form_error_ip_format": "වලංගු නොවන අ.ජා. කෙ. (IP) ආකෘතියකි", - "form_error_mac_format": "වලංගු නොවන MAC ආකෘතියකි", + "form_error_mac_format": "වලංගු නොවන මා.ප්‍ර.පා. ආකෘතියකි", "form_error_client_id_format": "වලංගු නොවන අනුග්‍රාහක හැඳුනුම් ආකෘතියකි", "form_error_positive": "0 ට වඩා වැඩි විය යුතුය", "form_error_negative": "0 හෝ ඊට වැඩි විය යුතුය", @@ -71,7 +72,7 @@ "dns_query": "ව.නා.ප. (DNS) විමසුම්", "blocked_by": "<0>පෙරහන් මගින් අවහිර කරන ලද", "stats_malware_phishing": "අවහිර කළ ද්වේශාංග/තතුබෑම්", - "stats_adult": "අවහිර කළ වැඩිහිටි වෙබ් අඩවි", + "stats_adult": "අවහිර කළ වැඩිහිටි වියමන අඩවි", "stats_query_domain": "ජනප්‍රිය විමසන ලද වසම්", "for_last_24_hours": "පසුගිය පැය 24 සඳහා", "for_last_days": "පසුගිය දින {{count}} සඳහා", @@ -82,29 +83,29 @@ "top_clients": "ජනප්‍රිය අනුග්‍රාහකයන්", "general_statistics": "පොදු සංඛ්‍යාලේඛන", "number_of_dns_query_blocked_24_hours": "දැන්වීම් වාරණ පෙරහන් සහ ධාරක වාරණ ලැයිස්තු මගින් අවහිර කරන ලද ව.නා.ප. ඉල්ලීම් ගණන", - "number_of_dns_query_blocked_24_hours_by_sec": "AdGuard browsing security ඒකකය මගින් අවහිර කරන ලද ව.නා.ප. ඉල්ලීම් ගණන", - "number_of_dns_query_blocked_24_hours_adult": "අවහිර කළ වැඩිහිටි වෙබ් අඩවි ගණන", + "number_of_dns_query_blocked_24_hours_by_sec": "ඇඩ්ගාර්ඩ් පිරික්සුම් ආරක්ෂණ ඒකකය මගින් අවහිර කරන ලද ව.නා.ප. ඉල්ලීම් ගණන", + "number_of_dns_query_blocked_24_hours_adult": "අවහිර කළ වැඩිහිටි වියමන අඩවි ගණන", "enforced_save_search": "ආරක්ෂිත සෙවීම බලාත්මක කරන ලද", "number_of_dns_query_to_safe_search": "ආරක්ෂිත සෙවීම බලාත්මක කළ සෙවුම් යන්ත්‍ර සඳහා ව.නා.ප. ඉල්ලීම් ගණන", "average_processing_time": "සාමාන්‍ය සැකසුම් කාලය", "average_processing_time_hint": "ව.නා.ප. ඉල්ලීමක් සැකසීමේ සාමාන්‍ය කාලය මිලි තත්පර වලින්", "block_domain_use_filters_and_hosts": "පෙරහන් සහ ධාරක ගොනු භාවිතා කරමින් වසම් අවහිර කරන්න", "filters_block_toggle_hint": "ඔබට අවහිර කිරීමේ නීති පෙරහන් තුළ පිහිටුවිය හැකිය.", - "use_adguard_browsing_sec": "AdGuard browsing security වෙබ් සේවාව භාවිතා කරන්න", - "use_adguard_parental": "AdGuard parental control වෙබ් සේවාව භාවිතා කරන්න", - "use_adguard_parental_hint": "වසමේ වැඩිහිටියන්ට අදාල කරුණු අඩංගු දැයි ඇඩ්ගාර්ඩ් හෝම් විසින් පරීක්ෂා කරනු ඇත. එය browsing security වෙබ් සේවාව මෙන් රහස්‍යතා හිතකාමී යෙ.ක්‍ර. අ.මු. (API) භාවිතා කරයි.", + "use_adguard_browsing_sec": "ඇඩ්ගාර්ඩ් පිරික්සුම් ආරක්ෂණ වියමන සේවාව භාවිතා කරන්න", + "use_adguard_parental": "ඇඩ්ගාර්ඩ් දෙමාපිය පාලන වියමන සේවාව භාවිතා කරන්න", + "use_adguard_parental_hint": "වසමේ වැඩිහිටියන්ට අදාල කරුණු අඩංගු දැයි ඇඩ්ගාර්ඩ් හෝම් විසින් පරීක්ෂා කරනු ඇත. එය පිරික්සුම් ආරක්ෂණ වියමන සේවාව මෙන් රහස්‍යතා හිතකාමී යෙ.ක්‍ර. අ.මු. (API) භාවිතා කරයි.", "enforce_safe_search": "ආරක්ෂිත සෙවීම බලාත්මක කරන්න", "enforce_save_search_hint": "ඇඩ්ගාර්ඩ් හෝම් හට පහත සෙවුම් යන්ත්‍ර තුළ ආරක්ෂිත සෙවීම බලාත්මක කළ හැකිය: ගූගල්, යූටියුබ්, බින්ග්, ඩක්ඩක්ගෝ, යාන්ඩෙක්ස් සහ පික්සාබේ.", "no_servers_specified": "සේවාදායක කිසිවක් නිශ්චිතව දක්වා නැත", "general_settings": "පොදු සැකසුම්", - "dns_settings": "DNS සැකසුම්", - "dns_blocklists": "DNS අවහිර කිරී‌‌‌‌‌මේ ලැයිස්තු", - "dns_allowlists": "DNS අවසර දීමේ ලැයිස්තු", + "dns_settings": "ව.නා.ප. සැකසුම්", + "dns_blocklists": "ව.නා.ප. අවහිර කිරී‌‌‌‌‌මේ ලැයිස්තු", + "dns_allowlists": "ව.නා.ප. අවසර දීමේ ලැයිස්තු", "dns_blocklists_desc": "ඇඩ්ගාර්ඩ් හෝම් විසින් අවහිර කිරී‌‌‌‌‌මේ ලැයිස්තු වලට ගැලපෙන වසම් අවහිර කරනු ඇත.", "dns_allowlists_desc": "අවසර දීමේ ව.නා.ප. ලැයිස්තුවල වසම් කිසියම් අවහිර කිරී‌‌‌‌‌මේ ලැයිස්තුවක අඩංගු වුවද එය නොසලකා හැර අවසර දෙනු ලැබේ.", "custom_filtering_rules": "අභිරුචි පෙරීමේ නීති", "encryption_settings": "සංකේතාංකන සැකසුම්", - "dhcp_settings": "DHCP සැකසුම්", + "dhcp_settings": "ග.ධා.වි.කෙ. සැකසුම්", "upstream_dns": "Upstream ව.නා.ප. සේවාදායකයන්", "upstream_dns_help": "පේළියකට එක් සේවාදායක ලිපිනය බැගින් ඇතුළත් කරන්න. upstream ව.නා.ප. (DNS) \n සේවාදායකයන් වින්‍යාසගත කිරීම ගැන තව දැනගන්න.", "apply_btn": "යොදන්න", @@ -118,7 +119,7 @@ "enabled_save_search_toast": "ආරක්ෂිත සෙවීම සබල කර ඇත", "enabled_table_header": "සබල කර ඇත", "name_table_header": "නම", - "list_url_table_header": "URL ලැයිස්තුව", + "list_url_table_header": "ඒ.ස.නි.(URL) ලැයිස්තුව", "rules_count_table_header": "නීති ගණන", "last_time_updated_table_header": "අවසන් වරට යාවත්කාලීන කරන ලද", "actions_table_header": "ක්‍රියාමාර්ග", @@ -133,7 +134,7 @@ "add_allowlist": "අවසර දීමේ ලැයිස්තුවක් එකතු කරන්න", "cancel_btn": "අහෝසි කරන්න", "enter_name_hint": "නම ඇතුළත් කරන්න", - "enter_url_or_path_hint": "ලැයිස්තුවක URL හෝ ස්ථීර මාර්ගයක් ඇතුළත් කරන්න", + "enter_url_or_path_hint": "ලැයිස්තුවක ඒ.ස.නි.(URL) හෝ ස්ථීර මාර්ගයක් ඇතුළත් කරන්න", "check_updates_btn": "යාවත්කාල පරීක්ෂා කරන්න", "new_blocklist": "නව අවහිර කිරී‌‌‌‌‌මේ ලැයිස්තුව", "new_allowlist": "නව අවසර දීමේ ලැයිස්තුව", @@ -141,10 +142,10 @@ "edit_allowlist": "අවසර දීමේ ලැයිස්තුව සංස්කරණය කරන්න", "choose_blocklist": "අවහිර කීරීමේ ලැයිස්තුවක් තෝරන්න", "choose_allowlist": "අනවහිර කීරීමේ ලැයිස්තුවක් තෝරන්න", - "enter_valid_blocklist": "අවහිර කිරී‌‌‌‌‌මේ ලැයිස්තුවට වලංගු URL ලිපිනයක් ඇතුළත් කරන්න.", - "enter_valid_allowlist": "අවසර දීමේ ලැයිස්තුවට වලංගු URL ලිපිනයක් ඇතුළත් කරන්න.", - "form_error_url_format": "වලංගු නොවන URL ආකෘතියකි", - "form_error_url_or_path_format": "ලැයිස්තුවක වලංගු නොවන URL හෝ ස්ථීර මාර්ගයකි", + "enter_valid_blocklist": "අවහිර කිරී‌‌‌‌‌මේ ලැයිස්තුවට වලංගු ඒ.ස.නි.(URL) ලිපිනයක් ඇතුළත් කරන්න.", + "enter_valid_allowlist": "අවසර දීමේ ලැයිස්තුවට වලංගු ඒ.ස.නි.(URL) ලිපිනයක් ඇතුළත් කරන්න.", + "form_error_url_format": "වලංගු නොවන ඒ.ස.නි.(URL) ආකෘතියකි", + "form_error_url_or_path_format": "ලැයිස්තුවක වලංගු නොවන ඒ.ස.නි.(URL) හෝ ස්ථීර මාර්ගයකි", "custom_filter_rules": "අභිරුචි පෙරීමේ නීති", "custom_filter_rules_hint": "පේළියකට එක් නීතියක් බැගින් ඇතුළත් කරන්න. ඔබට දැන්වීම් අවහිර කිරීමේ නීති හෝ ධාරක ගොනු පද ගැලපුම් භාවිතා කළ හැකිය.", "examples_title": "උදාහරණ", @@ -155,11 +156,11 @@ "example_comment_meaning": "විස්තර කිරීමක්", "example_comment_hash": "# එසේම අදහස් දැක්වීමක්", "example_regex_meaning": "නිශ්චිතව දක්වා ඇති නිත්‍ය වාක්‍යවිධියට ගැලපෙන වසම් වෙත පිවිසීම අවහිර කරයි", - "example_upstream_regular": "සාමාන්‍ය DNS (UDP හරහා)", + "example_upstream_regular": "සාමාන්‍ය ව.නා.ප. (UDP හරහා)", "example_upstream_dot": "සංකේතාංකනය කළ <0>DNS-over-TLS", "example_upstream_doh": "සංකේතාංකනය කළ <0>DNS-over-HTTPS", "example_upstream_doq": "සංකේතාංකනය කළ <0>DNS-over-QUIC", - "example_upstream_tcp": "සාමාන්‍ය DNS (TCP හරහා)", + "example_upstream_tcp": "සාමාන්‍ය ව.නා.ප. (TCP/ස.පා.කෙ. හරහා) ", "all_lists_up_to_date_toast": "සියලුම ලැයිස්තු දැනටමත් යාවත්කාලීනයි", "dns_test_ok_toast": "සඳහන් කළ ව.නා.ප. සේවාදායකයන් නිවැරදිව ක්‍රියා කරයි", "dns_test_not_ok_toast": "සේවාදායක \"{{key}}\": භාවිතා කළ නොහැකි විය, කරුණාකර ඔබ එය නිවැරදිව ලියා ඇත්දැයි පරීක්ෂා කරන්න", @@ -201,14 +202,16 @@ "anonymize_client_ip": "අනුග්‍රාහකයෙහි අ.ජා. කෙ. (IP) නිර්නාමික කරන්න", "anonymize_client_ip_desc": "ලොග සහ සංඛ්‍යාලේඛන තුළ අනුග්‍රාහකයේ සම්පූර්ණ අ.ජා. කෙ. ලිපිනය සුරකීමෙන් වලකින්න", "dns_config": "ව.නා.ප. සේවාදායක වින්‍යාසය", + "dns_cache_config": "ව.නා.ප. නිහිත වින්‍යාසය", + "dns_cache_config_desc": "මෙහිදී ඔබට ව.නා.ප. නිහිතය වින්‍යාසගත කළ හැකිය", "blocking_mode": "අවහිර කරන ආකාරය", "default": "සුපුරුදු", "nxdomain": "නොපවතින වසම", "refused": "REFUSED", "null_ip": "අභිශූන්‍යය අ.ජා. කෙ.", "custom_ip": "අභිරුචි අ.ජා. කෙ.", - "dns_over_https": "DNS-over-HTTPS", - "dns_over_tls": "DNS-over-TLS", + "blocking_ipv4": "අයි.පී.වී.4 අවහිර කිරීම\n", + "blocking_ipv6": "අයි.පී.වී.6 අවහිර කිරීම", "form_enter_rate_limit": "අනුපාත සීමාව ඇතුළත් කරන්න", "rate_limit": "අනුපාත සීමාව", "edns_enable": "EDNS අනුග්‍රාහක අනුජාලය සබල කරන්න", @@ -219,26 +222,26 @@ "blocking_mode_nxdomain": "නොපවතින වසම (NXDOMAIN): NXDOMAIN කේතය සමඟ ප්‍රතිචාර දක්වයි", "blocking_mode_null_ip": "අභිශූන්‍යය අ.ජා. කෙ. : ශුන්‍ය අ.ජා. කෙ. ලිපිනය සමඟ ප්‍රතිචාර දක්වයි (A සඳහා 0.0.0.0; AAAA සඳහා ::)", "blocking_mode_custom_ip": "අභිරුචි අන්තර්ජාල කෙටුම්පත: අතින් සැකසූ අ.ජා. කෙ. ලිපිනයක් සමඟ ප්‍රතිචාර දක්වයි", - "upstream_dns_client_desc": "ඔබ මෙම ක්ෂේත්‍රය හිස්ව තබා ගන්නේ නම්, ඇඩ්ගාර්ඩ් හෝම් විසින් <0>DNS සැකසුම් හි වින්‍යාසගත කර ඇති සේවාදායකයන් භාවිතා කරනු ඇත.", + "upstream_dns_client_desc": "ඔබ මෙම ක්ෂේත්‍රය හිස්ව තබා ගන්නේ නම්, ඇඩ්ගාර්ඩ් හෝම් විසින් <0>ව.නා.ප. සැකසුම් හි වින්‍යාසගත කර ඇති සේවාදායකයන් භාවිතා කරනු ඇත.", "tracker_source": "ලුහුබැඳීම් මූලාශ්‍රය", "source_label": "මූලාශ්‍රය", "found_in_known_domain_db": "දැනුවත් වසම් දත්ත ගබඩාවේ හමු විය.", "category_label": "ප්‍රවර්ගය", - "rule_label": "නීතිය", "list_label": "ලැයිස්තුව", "unknown_filter": "{{filterId}} නොදන්නා පෙරහනකි", "known_tracker": "දැනුවත් ලුහුබැඳීමක්", "install_welcome_title": "ඇඩ්ගාර්ඩ් හෝම් වෙත සාදරයෙන් පිළිගනිමු!", "install_welcome_desc": "ඇඩ්ගාර්ඩ් හෝම් යනු ජාලය පුරා ඇති දැන්වීම් සහ ලුහුබැඳීම අවහිර කරන ව.නා.ප. සේවාදායකි. ඔබගේ මුළු ජාලය සහ සියලුම උපාංග පාලනය කිරීමට ඉඩ සලසා දීම එහි පරමාර්ථය යි, එයට අනුග්‍රාහක පාර්ශවීය වැඩසටහනක් භාවිතා කිරීම අවශ්‍ය නොවේ.", - "install_settings_title": "පරිපාලක වෙබ් අතුරු මුහුණත", + "install_settings_title": "පරිපාලක වියමන අතුරු මුහුණත", "install_settings_listen": "සවන් දෙන අතුරු මුහුණත", "install_settings_port": "කවුළුව", + "install_settings_interface_link": "ඔබගේ ඇඩ්ගාර්ඩ් හෝම් පරිපාලක වියමන අතුරු මුහුණත පහත ලිපිනයන්ගෙන් ප්‍රවේශ විය හැකිය:", "form_error_port": "වලංගු කවුළුවක අගයක් ඇතුළත් කරන්න", "install_settings_dns": "ව.නා.ප. සේවාදායකය", - "install_settings_dns_desc": "පහත ලිපිනයන්හි ව.නා.ප. සේවාදායකය භාවිතා කිරීම සඳහා ඔබගේ උපාංග හෝ රවුටරය වින්‍යාසගත කිරීමට අවශ්‍ය වනු ඇත:", + "install_settings_dns_desc": "පහත ලිපිනයන්හි ව.නා.ප. සේවාදායකය භාවිතා කිරීම සඳහා ඔබගේ උපාංග හෝ මාර්ගකාරකය වින්‍යාසගත කිරීමට අවශ්‍ය වනු ඇත:", "install_settings_all_interfaces": "සියලුම අතුරුමුහුණත්", "install_auth_title": "සත්‍යාපනය", - "install_auth_desc": "ඔබගේ ඇඩ්ගාර්ඩ් හෝම් පරිපාලක වෙබ් අතුරු මුහුණතට මුරපද සත්‍යාපනය වින්‍යාසගත කිරීම අතිශයින් නිර්දේශ කෙරේ. එය ඔබගේ ස්ථානීය ජාල‌යෙන් පමණක් ප්‍රවේශ විය හැකි වුවද, එය තව දුරටත් සීමා රහිත ප්‍රවේශයකින් ආරක්ෂා කර ගැනීම වැදගත් ය.", + "install_auth_desc": "ඔබගේ ඇඩ්ගාර්ඩ් හෝම් පරිපාලක වියමන අතුරු මුහුණතට මුරපද සත්‍යාපනය වින්‍යාසගත කිරීම අතිශයින් නිර්දේශ කෙරේ. එය ඔබගේ ස්ථානීය ජාල‌යෙන් පමණක් ප්‍රවේශ විය හැකි වුවද, එය තව දුරටත් සීමා රහිත ප්‍රවේශයකින් ආරක්ෂා කර ගැනීම වැදගත් ය.", "install_auth_username": "පරිශීලක නාමය", "install_auth_password": "මුරපදය", "install_auth_confirm": "මුරපදය තහවුරු කරන්න", @@ -251,10 +254,11 @@ "install_submit_desc": "පිහිටුවීමේ ක්‍රියා පටිපාටිය අවසන් වී ඇති අතර ඔබ ඇඩ්ගාර්ඩ් හෝම් භාවිතය ආරම්භ කිරීමට සූදානම්ය.", "install_devices_router": "මාර්ගකාරකය", "install_devices_router_desc": "මෙම පිහිටුම ඔබගේ නිවසේ මාර්ගකාරකයට සම්බන්ධ සියලුම උපාංග ස්වයංක්‍රීයව ආවරණය කරන අතර ඔබට ඒවා අතින් වින්‍යාසගත කිරීමට අවශ්‍ය නොවනු ඇත.", - "install_devices_router_list_1": "ඔබේ මාර්ගකාරකය සඳහා වූ මනාපයන් විවෘත කරන්න. සාමාන්‍යයෙන්, එය ඔබගේ බ්‍රව්සරයෙන් URL එකක් හරහා (http://192.168.0.1/ හෝ http://192.168.1.1/ වැනි) පිවිසිය හැකිය. මුරපදය ඇතුළත් කිරීමට ඔබෙන් ඉල්ලා සිටිය හැකිය. ඔබට එය මතක නැතිනම්, බොහෝ විට මාර්ගකාරකයේ බොත්තමක් එබීමෙන් මුරපදය නැවත සැකසිය හැක. සමහර මාර්ගකාරක සඳහා විශේෂිත යෙදුමක් අවශ්‍ය වන අතර, එය දැනටමත් ඔබේ පරිගණකයේ/දුරකථනයේ ස්ථාපනය කර තිබිය යුතුය.", + "install_devices_address": "ඇඩ්ගාර්ඩ් හෝම් ව.නා.ප. සේවාදායකය පහත ලිපිනයන්ට සවන් දෙමින් පවතී", + "install_devices_router_list_1": "ඔබේ මාර්ගකාරකය සඳහා වූ මනාපයන් විවෘත කරන්න. සාමාන්‍යයෙන්, එය ඔබගේ අතිරික්සුවෙන් ඒ.ස.නි.(URL) ක් හරහා (http://192.168.0.1/ හෝ http://192.168.1.1/ වැනි) පිවිසිය හැකිය. මුර පදය ඇතුළත් කිරීමට ඔබෙන් ඉල්ලා සිටිය හැකිය. ඔබට එය මතක නැතිනම්, බොහෝ විට මාර්ගකාරකයේ බොත්තමක් එබීමෙන් මුරපදය නැවත සැකසිය හැක. සමහර මාර්ගකාරක සඳහා විශේෂිත යෙදුමක් අවශ්‍ය වන අතර, එය දැනටමත් ඔබේ පරිගණකයේ/දුරකථනයේ ස්ථාපනය කර තිබිය යුතුය.", "install_devices_router_list_2": "ග.ධා.වි.කෙ. (DHCP)/ ව.නා.ප. (DNS) සැකසුම් සොයා ගන්න. ඉලක්කම් කට්ටල දෙකකට හෝ තුනකට ඉඩ දෙන ක්ෂේත්‍රයක් අසල ඇති ව.නා.ප. අක්ෂර සොයන්න, සෑම එකක්ම ඉලක්කම් එකේ සිට තුන දක්වා කාණ්ඩ හතරකට බෙදා ඇත.", "install_devices_router_list_3": "ඔබගේ ඇඩ්ගාර්ඩ් හෝම් සේවාදායක ලිපින එහි ඇතුළත් කරන්න.", - "install_devices_router_list_4": "ඔබට සමහර වර්ගයේ රවුටර වල අභිරුචි ව.නා.ප. (DNS) සේවාදායකයක් සැකසිය නොහැක. මෙම අවස්ථාවේදී ඇඩ්ගාර්ඩ් හෝම් <0>ග.ධා.වි.කෙ. සේවාදායකයක් ලෙස පිහිටුවන්නේ නම් එය උපකාර වනු ඇත. එසේ නොමැතිනම්, ඔබගේ විශේෂිත මාර්ගකාරක මාදිළිය සඳහා වූ ව.නා.ප. සේවාදායකයන් රිසිකරණය කරන්නේ කෙසේද යන්න පිළිබඳ අත්පොත සෙවිය යුතුය.", + "install_devices_router_list_4": "ඔබට සමහර වර්ගයේ මාර්ගකාරකය වල අභිරුචි ව.නා.ප. සේවාදායකයක් සැකසිය නොහැක. මෙම අවස්ථාවේදී ඇඩ්ගාර්ඩ් හෝම් <0>ග.ධා.වි.කෙ. සේවාදායකයක් ලෙස පිහිටුවන්නේ නම් එය උපකාර වනු ඇත. එසේ නොමැතිනම්, ඔබගේ විශේෂිත මාර්ගකාරක මාදිළිය සඳහා වූ ව.නා.ප. සේවාදායකයන් රිසිකරණය කරන්නේ කෙසේද යන්න පිළිබඳ අත්පොත සෙවිය යුතුය.", "install_devices_windows_list_1": "ආරම්භක මෙනුව හෝ වින්ඩෝස් සෙවුම හරහා පාලක පැනලය විවෘත කරන්න.", "install_devices_windows_list_2": "ජාල සහ අන්තර්ජාල ප්‍රවර්ගයට ගොස් පසුව ජාල සහ බෙදාගැනීමේ මධ්‍යස්ථානය වෙත යන්න.", "install_devices_windows_list_3": "උපයුක්තක‌‌‌යෙහි සැකසුම් වෙනස් කිරීම තිරයේ වම් පසින් සොයාගෙන එය මත ක්ලික් කරන්න.", @@ -279,7 +283,7 @@ "open_dashboard": "උපකරණ පුවරුව විවෘත කරන්න", "install_saved": "සාර්ථකව සුරකින ලදි", "encryption_title": "සංකේතාංකනය", - "encryption_desc": "ගුප්තකේතනය (HTTPS/TLS) සඳහා ව.නා.ප. සහ පරිපාලක වෙබ් අතුරු මුහුණත සහය දක්වයි", + "encryption_desc": "ගුප්තකේතනය (HTTPS/TLS) සඳහා ව.නා.ප. සහ පරිපාලක වියමන අතුරු මුහුණත සහය දක්වයි", "encryption_config_saved": "සංකේතාංකන වින්‍යාසය සුරකින ලදි", "encryption_server": "සේවාදායක‌‌‌‌යේ නම", "encryption_server_enter": "ඔබගේ වසම් නාමය ඇතුළත් කරන්න", @@ -314,7 +318,7 @@ "reset_settings": "සැකසුම් යළි පිහිටුවන්න", "update_announcement": "ඇඩ්ගාර්ඩ් හෝම් {{version}} දැන් ලබා ගත හැකිය! වැඩි විස්තර සඳහා <0>මෙහි ක්ලික් කරන්න.", "setup_guide": "පිහිටුවීමේ මාර්ගෝපදේශය", - "dns_addresses": "DNS ලිපින", + "dns_addresses": "ව.නා.ප. ලිපින", "dns_start": "ව.නා.ප. සේවාදායකය ආරම්භ වෙමින් පවතී", "dns_status_error": "ව.නා.ප. සේවාදායකයේ තත්වය පරීක්ෂා කිරීමේදී දෝෂයකි", "down": "පහත", @@ -335,9 +339,8 @@ "client_edit": "අනුග්‍රාහකය සංස්කරණය කරන්න", "client_identifier": "හඳුන්වනය", "ip_address": "අ.ජා. කෙ. (IP) ලිපිනය", - "client_identifier_desc": "අනුග්‍රාහකයන් අ.ජා. කෙ. (IP) ලිපිනයක් හෝ මා.ප්‍ර.පා. (MAC) ලිපිනයක් මගින් හඳුනාගත හැකිය. මා.ප්‍ර.පා. හඳුන්වනයක් ලෙස භාවිතා කළ හැක්කේ ඇඩ්ගාර්ඩ් හෝම් ද <0>DHCP සේවාදායකයක් නම් පමණක් බව කරුණාවෙන් සලකන්න. ", "form_enter_ip": "අ.ජා. කෙ. (IP) ඇතුළත් කරන්න", - "form_enter_mac": "MAC ඇතුළත් කරන්න", + "form_enter_mac": "මා.ප්‍ර.පා. (MAC) ඇතුළත් කරන්න", "form_enter_id": "හඳුන්වනය ඇතුළත් කරන්න", "form_add_id": "හඳුන්වනයක් එක් කරන්න", "form_client_name": "අනුග්‍රාහකයේ නම ඇතුළත් කරන්න", @@ -360,22 +363,20 @@ "updates_checked": "යාවත්කාලීන කිරීම් සාර්ථකව පරික්ෂා කර ඇත", "updates_version_equal": "ඇඩ්ගාර්ඩ් හෝම් යාවත්කාලීනයි", "check_updates_now": "යාවත්කාල කිරීම සඳහා දැන් පරීක්ෂා කරන්න", - "dns_privacy": "DNS රහස්‍යතා", - "setup_dns_privacy_1": "<0>DNS-over-TLS: <1>{{address}} තන්තුව භාවිතා කරයි.", - "setup_dns_privacy_2": "<0>DNS-over-HTTPS: <1>{{address}} තන්තුව භාවිතා කරයි.", - "setup_dns_privacy_android_2": "<1>DNS-over-HTTPS සහ <1>DNS-over-TLS සඳහා <0>AdGuard for Android සහය දක්වයි.", - "setup_dns_privacy_ios_2": "<1>DNS-over-HTTPS සහ <1>DNS-over-TLS පිහිටුවීම් සඳහා <0>AdGuard for iOS සහය දක්වයි.", - "setup_dns_privacy_other_2": "<0>dnsproxy දන්නා සියලුම ආරක්ෂිත DNS කෙටුම්පත් සඳහා සහය දක්වයි.", + "dns_privacy": "ව.නා.ප. රහස්‍යතා", + "setup_dns_privacy_3": "<0>මෙහි ඔබට භාවිතා කළ හැකි මෘදුකාංග ලැයිස්තුවක් ඇත.", + "setup_dns_privacy_other_2": "<0>ඩීඑන්එස්ප්‍රොක්සි දන්නා සියලුම ආරක්ෂිත ව.නා.ප. කෙටුම්පත් සඳහා සහය දක්වයි.", "setup_dns_privacy_other_3": "<1>DNS-over-HTTPS සඳහා <0>dnscrypt-පෙරකලාසිය සහය දක්වයි.", "setup_dns_privacy_other_4": "<1>DNS-over-HTTPS සඳහා <0>මොසිල්ලා ෆයර්ෆොක්ස් සහය දක්වයි.", "setup_dns_notice": "ඔබට <1>DNS-over-HTTPS හෝ <1>DNS-over-TLS භාවිතා කිරීම සඳහා ඇඩ්ගාර්ඩ් හෝම් සැකසුම් තුළ <0>සංකේතාංකනය වින්‍යාසගත කිරීමට අවශ්‍ය වේ.", - "rewrite_add": "DNS නැවත ලිවීමක් එකතු කරන්න", - "rewrite_confirm_delete": "\"{{key}}\" සඳහා DNS නැවත ලිවීම ඉවත් කිරීමට අවශ්‍ය බව ඔබට විශ්වාසද?", + "rewrite_added": "\"{{key}}\" සඳහා ව.නා.ප. නැවත ලිවීම සාර්ථකව එකතු කරන ලදි", + "rewrite_add": "ව.නා.ප. නැවත ලිවීමක් එකතු කරන්න", + "rewrite_not_found": "ව.නා.ප. නැවත ලිවීම් හමු නොවීය", + "rewrite_confirm_delete": "\"{{key}}\" සඳහා ව.නා.ප. නැවත ලිවීම ඉවත් කිරීමට අවශ්‍ය බව ඔබට විශ්වාසද?", "rewrite_desc": "විශේෂිත වසම් නාමයක් සඳහා අභිරුචි ව.නා.ප. ප්‍රතිචාර පහසුවෙන් වින්‍යාසගත කිරීමට ඉඩ දෙයි.", "rewrite_applied": "නැවත ලිවීමේ නීතිය යොදා ඇත", "rewrite_hosts_applied": "ධාරක ගොනු නීතිය මගින් නැවත ලියා ඇත", - "dns_rewrites": "DNS නැවත ලිවීම්", - "form_domain": "වසම ඇතුළත් කරන්න", + "dns_rewrites": "ව.නා.ප. නැවත ලිවීම්", "form_answer": "අ.ජා. කෙ. (IP) ලිපිනය ‌හෝ වසම ඇතුළත් කරන්න ", "form_error_domain_format": "වලංගු නොවන වසම් ආකෘතියකි", "form_error_answer_format": "වලංගු නොවන පිළිතුරු ආකෘතියකි", @@ -403,6 +404,7 @@ "domain": "වසම", "answer": "පිළිතුර", "filter_added_successfully": "පෙරහන සාර්ථකව එකතු කරන ලදි", + "filter_removed_successfully": "ලැයිස්තුව සාර්ථකව ඉවත් කරන ලදි", "filter_updated": "ලැයිස්තුව සාර්ථකව යාවත්කාලීන කර ඇත", "statistics_configuration": "සංඛ්‍යාලේඛන වින්‍යාසය", "statistics_retention": "සංඛ්‍යාලේඛන රඳවා තබා ගැනීම", @@ -431,20 +433,24 @@ "network": "ජාලය", "descr": "විස්තරය", "whois": "Whois", + "filtering_rules_learn_more": "ඔබගේ ම ධාරක ලැයිස්තු සෑදීම පිළිබඳව <0>තව දැනගන්න.", "blocked_by_response": "ප්‍රතිචාරය අන්. නාමයක් (CNAME) හෝ අ.ජා. කෙ. මගින් අවහිර කර ඇත", "blocked_by_cname_or_ip": "අන්. නාමයක් (CNAME) හෝ අ.ජා. කෙ. මගින් අවහිර කර ඇත", "try_again": "නැවත උත්සහා කරන්න", "example_rewrite_domain": "මෙම වසම් නාමය සඳහා පමණක් ප්‍රතිචාර නැවත ලියන්න.", "example_rewrite_wildcard": "<0>example.org සහ එහි සියලුම උප වසම් සඳහා ප්‍රතිචාර නැවත ලියයි.", + "rewrite_ip_address": "අ.ජා. කෙ. ලිපිනය: මෙම අ.ජා. කෙටුම්පත A හෝ AAAA ප්‍රතිචාරයකට භාවිතා කරන්න", "rewrite_domain_name": "වසම් නාමය: අන්. නා. (CNAME) වාර්තාවක් එක් කරන්න", "disable_ipv6": "IPv6 අබල කරන්න", "disable_ipv6_desc": "මෙම අංගය සක්‍රීය කර ඇත්නම්, IPv6 ලිපින සඳහා වන සියලුම ව.නා.ප. විමසුම් (AAAA වර්ගය) අතහැර දමනු ලැබේ.", "fastest_addr": "වේගවත්ම අන්තර්ජාල කෙටුම්පත් (IP) ලිපිනය", + "fastest_addr_desc": "සියලුම ව.නා.ප. සේවාදායකයන් හරහා විමසා සියලු ප්‍රතිචාර අතරින් වේගවත්ම අ.ජා. කෙ. ලිපිනය ලබා දෙයි. සියලුම ව.නා.ප. සේවාදායකයන්ගේ ප්‍රතිචාර සඳහා අප බලා සිටිය යුතු බැවින් මෙය ව.නා.ප. විමසුම් මන්දගාමී කරන නමුත් සමස්ත සම්බන්ධතාවය වැඩි දියුණු කරයි.", "autofix_warning_text": "ඔබ \"නිරාකරණය කරන්න\" බොත්තම එබුවහොත්, ඔබගේ පද්ධතිය ඇඩ්ගාර්ඩ් හෝම් ව.නා.ප. සේවාදායකය භාවිතා කිරීමට වින්‍යාසගත කරනු ඇත.", "tags_title": "හැඳුනුම් සංකේත", "tags_desc": "අනුග්‍රාහකයට අනුරූප වන හැඳුනුම් සංකේත ඔබට තෝරා ගත හැකිය. පෙරහන් නීති වලට හැඳුනුම් සංකේත ඇතුළත් කළ හැකි අතර ඒවා වඩාත් නිවැරදිව යෙදීමට ඔබට ඉඩ සලසයි. <0>වැඩිදුර ඉගෙන ගන්න", "form_select_tags": "අනුග්‍රාහක හැඳුනුම් සංකේත", "check_title": "පෙරීම පරීක්ෂා කරන්න", + "check_desc": "ධාරක නාමය පෙරහන් කර ඇත්දැයි පරීක්ෂා කරන්න", "check": "පරීක්ෂා කරන්න", "form_enter_host": "ධාරක නාමයක් ඇතුළත් කරන්න", "filtered_custom_rules": "අභිරුචි පෙරීමේ නීති මගින් පෙරහන් කරන ලදි", @@ -454,14 +460,14 @@ "check_ip": "අ.ජා. කෙ. (IP) ලිපින: {{ip}}", "check_cname": "අන්. නාමය (CNAME): {{cname}}", "check_reason": "හේතුව: {{reason}}", - "check_rule": "නීතිය: {{rule}}", "check_service": "සේවාවෙහි නම: {{service}}", + "service_name": "සේවාවේ නම", "check_not_found": "ඔබගේ පෙරහන් ලැයිස්තු තුළ සොයා ගත නොහැක", "client_confirm_block": "{{ip}} අනුග්‍රාහකය අවහිර කිරීමට අවශ්‍ය බව ඔබට විශ්වාසද?", "client_confirm_unblock": "{{ip}} අනුග්‍රාහකය අනවහිර කිරීමට අවශ්‍ය බව ඔබට විශ්වාසද?", "client_blocked": "අනුග්‍රාහකය \"{{ip}}\" සාර්ථකව අවහිර කරන ලදි", "client_unblocked": "අනුග්‍රාහකය \"{{ip}}\" සාර්ථකව අනවහිර කරන ලදි", - "static_ip": "ස්ථිතික අ.ජා. කෙ. (IP) ලිපිනය", + "static_ip": "ස්ථිතික අ.ජා. කෙ. ලිපිනය", "static_ip_desc": "ඇඩ්ගාර්ඩ් හෝම් යනු සේවාදායකයක් බැවින් එය නිසි ලෙස ක්‍රියා කිරීමට ස්ථිතික අන්තර්ජාල කෙටුම්පත් (IP) ලිපිනයක් අවශ්‍ය වේ. එසේ නොමැතිනම්, යම් අවස්ථාවක දී ඔබගේ මාර්ගකාරකය මෙම උපාංගයට වෙනත් අ.ජා. කෙ. ලිපිනයක් ලබා දිය හැකිය.", "set_static_ip": "ස්ථිතික අ.ජා. කෙ. (IP) ලිපිනයක් සකසන්න", "install_static_ok": "සුභ තොරතුරක්! ස්ථිතික අන්තර්ජාල කෙටුම්පත් (IP) ලිපිනය දැනටමත් වින්‍යාසගත කර ඇත", @@ -470,13 +476,11 @@ "confirm_static_ip": "ඇඩ්ගාර්ඩ් හෝම් ඔබේ ස්ථිතික අ.ජා. කෙ. (IP) ලිපිනය ලෙස {{ip}} වින්‍යාසගත කරනු ඇත. ඔබට ඉදිරියට යාමට අවශ්‍යද?", "list_updated": "{{count}} ලැයිස්තුව යාවත්කාලීන කරන ලදි", "list_updated_plural": "ලැයිස්තු {{count}} ක් යාවත්කාලීන කරන ලදි", - "dnssec_enable": "DNSSEC සබල කරන්න", - "validated_with_dnssec": "DNSSEC සමඟ තහවුරු කර ඇත", "all_queries": "සියලුම විමසුම්", "show_blocked_responses": "අවහිර කර ඇත", "show_whitelisted_responses": "සුදු ලැයිස්තුගත කර ඇත", "blocked_safebrowsing": "ආරක්ෂිත සෙවීම මගින් අවහිර කරන ලද", - "blocked_adult_websites": "අවහිර කළ වැඩිහිටි වෙබ් අඩවි", + "blocked_adult_websites": "අවහිර කළ වැඩිහිටි වියමන අඩවි", "blocked_threats": "අවහිර කළ තර්ජන", "allowed": "අවසර ලත්", "filtered": "පෙරහන් කරන ලද", @@ -484,8 +488,15 @@ "safe_search": "ආරක්ෂිත සෙවීම", "blocklist": "අවහිර කිරී‌‌‌‌‌මේ ලැයිස්තුව", "milliseconds_abbreviation": "මිලි තත්.", + "cache_size": "නිහිතයෙහි ප්‍රමාණය", + "cache_size_desc": "ව.නා.ප. නිහිතයෙහි ප්‍රමාණය (බයිට වලින්)", + "cache_ttl_min_override": "අවම පව. කා. අභිබවන්න", + "cache_ttl_max_override": "උපරිම පව. කා. අභිබවන්න", + "enter_cache_size": "ව.නා.ප. නිහිතයෙහි ප්‍රමාණය ඇතුළත් කරන්න (බයිට)", "enter_cache_ttl_min_override": "අවම පව. කා. (TTL) ඇතුළත් කරන්න", "enter_cache_ttl_max_override": "උපරිම පව. කා. (TTL) ඇතුළත් කරන්න", + "cache_ttl_max_override_desc": "ව.නා.ප. නිහිතයෙහි ඇති ඇතුළත් කිරීම් සඳහා ඉතා වැඩි පවත්නා කාලයක අගයක් (තත්පර) සකසන්න", + "ttl_cache_validation": "නිහිතයෙහි අවම පව. කා. (TTL) අගය උපරිම අගයට වඩා අඩු හෝ සමාන විය යුතුය", "filter_category_general": "පොදු", "filter_category_security": "ආරක්ෂණ", "filter_category_regional": "ප්‍රාදේශ්‍රීය", @@ -499,5 +510,6 @@ "click_to_view_queries": "විමසුම් බැලීමට ඔබන්න", "port_53_faq_link": "53 කවුළුව බොහෝ විට \"DNSStubListener\" හෝ \"systemd-resolved\" සේවාවන් භාවිතයට ගනු ලැබේ. කරුණාකර මෙය විසඳන්නේ කෙසේද යන්න පිළිබඳ <0>මෙම උපදෙස් කියවන්න.", "adg_will_drop_dns_queries": "ඇඩ්ගාර්ඩ් හෝම් විසින් මෙම අනුග්‍රාහකයේ සියලුම ව.නා.ප. විමසුම් අතහැර දමනු ඇත.", + "client_not_in_allowed_clients": "\"අවසර ලත් අනුග්‍රාහකයින්\" ලැයිස්තුවේ නොමැති නිසා අනුග්‍රාහකයට අවසර නැත.", "experimental": "පරීක්ෂණාත්මක" } \ No newline at end of file diff --git a/client/src/__locales/sk.json b/client/src/__locales/sk.json index 7c9fb056..7c0d4fa9 100644 --- a/client/src/__locales/sk.json +++ b/client/src/__locales/sk.json @@ -32,6 +32,7 @@ "form_error_ip_format": "Nesprávny formát IPv4", "form_error_mac_format": "Nesprávny MAC formát", "form_error_client_id_format": "Neplatný formát client ID", + "form_error_server_name": "Neplatné meno servera", "form_error_positive": "Musí byť väčšie ako 0", "form_error_negative": "Musí byť číslo 0 alebo viac", "range_end_error": "Musí byť väčšie ako začiatok rozsahu", @@ -50,7 +51,7 @@ "dhcp_table_expires": "Vyprší", "dhcp_warning": "Ak chcete server DHCP napriek tomu povoliť, uistite sa, že v sieti nie je žiadny iný aktívny DHCP server. V opačnom prípade sa môže prerušiť internet pre už pripojené zariadenia!", "dhcp_error": "Nebolo možné určiť, či je v sieti iný DHCP server.", - "dhcp_static_ip_error": "Aby bolo možné používať DHCP server, musí byť nastavená statická IP adresa. Nepodarilo sa určiť, či je toto sieťové rozhranie nakonfigurované pomocou statickej adresy IP. Nastavte statickú adresu IP manuálne.", + "dhcp_static_ip_error": "Aby bolo možné používať DHCP server, musí byť nastavená statická IP adresa. Nepodarilo sa určiť, či je toto sieťové rozhranie nakonfigurované pomocou statickej adresy IP. Nastavte statickú IP adresu manuálne.", "dhcp_dynamic_ip_found": "Váš systém používa dynamickú konfiguráciu IP adresy pre rozhranie <0{{interfaceName}}. Aby bolo možné používať DHCP server, musí byť nastavená statická IP adresa. Vaša aktuálna adresa IP je <0>{{ipAddress}}. Automaticky nastavíme túto IP adresu ako statickú, ak stlačíte tlačidlo Povoliť DHCP.", "dhcp_lease_added": "Statický \"{{key}}\" prenájmu bol úspešne pridaný", "dhcp_lease_deleted": "Statický \"{{key}}\" prenájmu bol úspešne vymazaný", @@ -247,10 +248,16 @@ "custom_ip": "Vlastná IP adresa", "blocking_ipv4": "Blokovanie IPv4", "blocking_ipv6": "Blokovanie IPv6", + "dnscrypt": "DNSCrypt", "dns_over_https": "DNS-over-HTTPS", "dns_over_tls": "DNS-over-TLS", + "dns_over_quic": "DNS-over-QUIC", + "client_id": "ID klienta", + "client_id_placeholder": "Zadať ID klienta", + "client_id_desc": "Rôznych klientov možno identifikovať podľa špeciálneho ID klienta. Tu sa dozviete viac o tom, ako identifikovať klientov.", "download_mobileconfig_doh": "Prevziať .mobileconfig pre DNS-over-HTTPS", "download_mobileconfig_dot": "Prevziať .mobileconfig pre DNS-over-TLS", + "download_mobileconfig": "Stiahnuť konfiguračný súbor", "plain_dns": "Obyčajné DNS", "form_enter_rate_limit": "Zadajte rýchlostný limit", "rate_limit": "Rýchlostný limit", @@ -269,7 +276,6 @@ "source_label": "Zdroj", "found_in_known_domain_db": "Nájdené v databáze známych domén.", "category_label": "Kategória", - "rule_label": "Pravidlo", "list_label": "Zoznam", "unknown_filter": "Neznámy filter {{filterId}}", "known_tracker": "Známy sledovač", @@ -330,7 +336,6 @@ "encryption_config_saved": "Konfigurácia šifrovania uložená", "encryption_server": "Meno servera", "encryption_server_enter": "Zadajte meno Vašej domény", - "encryption_server_desc": "Ak chcete používať HTTPS, musíte zadať meno servera, ktoré zodpovedá Vášmu SSL certifikátu.", "encryption_redirect": "Automaticky presmerovať na HTTPS", "encryption_redirect_desc": "Ak je táto možnosť začiarknutá, služba AdGuard Home Vás automaticky presmeruje z adresy HTTP na adresy HTTPS.", "encryption_https": "HTTPS port", @@ -386,7 +391,6 @@ "client_edit": "Upraviť klienta", "client_identifier": "Identifikátor", "ip_address": "IP adresa", - "client_identifier_desc": "Klienti môžu byť identifikovaní podľa IP adresy, CIDR alebo MAC adresy. Upozorňujeme, že používanie MAC ako identifikátora je možné len vtedy, ak je AdGuard Home tiež <0>DHCP server", "form_enter_ip": "Zadajte IP adresu", "form_enter_mac": "Zadajte MAC adresu", "form_enter_id": "Zadajte identifikátor", @@ -430,6 +434,7 @@ "setup_dns_privacy_other_3": "<0>dnscrypt-proxy podporuje <1>DNS-over-HTTPS.", "setup_dns_privacy_other_4": "<0>Mozilla Firefox podporuje <1>DNS-over-HTTPS.", "setup_dns_privacy_other_5": "Viac implementácií nájdete <0>tu a <1>tu.", + "setup_dns_privacy_ioc_mac": "Konfigurácia iOS a macOS", "setup_dns_notice": "Pre použitie <1>DNS-over-HTTPS alebo <1>DNS-over-TLS, potrebujete v nastaveniach AdGuard Home <0>nakonfigurovať šifrovanie.", "rewrite_added": "DNS prepísanie pre \"{{key}}\" bolo úspešne pridané", "rewrite_deleted": "DNS prepísanie pre \"{{key}}\" bolo úspešne vymazané", @@ -529,7 +534,6 @@ "check_ip": "IP adresy: {{ip}}", "check_cname": "CNAME: {{cname}}", "check_reason": "Dôvod: {{reason}}", - "check_rule": "Pravidlo: {{rule}}", "check_service": "Meno služby: {{service}}", "service_name": "Názov služby", "check_not_found": "Nenašlo sa vo Vašom zozname filtrov", diff --git a/client/src/__locales/sl.json b/client/src/__locales/sl.json index 2678693c..007283bb 100644 --- a/client/src/__locales/sl.json +++ b/client/src/__locales/sl.json @@ -32,6 +32,7 @@ "form_error_ip_format": "Neveljaven format IP", "form_error_mac_format": "Neveljaven MAC format", "form_error_client_id_format": "Neveljaven format ID odjemalca", + "form_error_server_name": "Neveljavno ime strežnika", "form_error_positive": "Mora biti večja od 0", "form_error_negative": "Mora biti enako ali več kot 0", "range_end_error": "Mora biti večji od začtka razpona", @@ -247,10 +248,16 @@ "custom_ip": "IP po meri", "blocking_ipv4": "Onemogočanje IPv4", "blocking_ipv6": "Onemogočanje IPv6", + "dnscrypt": "DNSCrypt", "dns_over_https": "DNS-prek-HTTPS", "dns_over_tls": "DNS-prek-TLS", + "dns_over_quic": "DNS-prek-QIUC", + "client_id": "ID odjemalca", + "client_id_placeholder": "Vnesite ID odjemalca", + "client_id_desc": "Različne odjemalce je mogoče prepoznati s posebnim ID-jem odjemalca. Tukaj lahko izveste več o prepoznavanju odjemalcev.", "download_mobileconfig_doh": "Prenos .mobileconfig za DNS-preko-HTTPS", "download_mobileconfig_dot": "Prenos .mobileconfig za DNS-preko-TLS", + "download_mobileconfig": "Prenesi nastavitveno datoteko", "plain_dns": "Navadni DNS", "form_enter_rate_limit": "Vnesite omejitev hitrosti", "rate_limit": "Omejitev hitrosti", @@ -269,7 +276,7 @@ "source_label": "Vir", "found_in_known_domain_db": "Najdeno v zbirki podatkov znanih domen.", "category_label": "Kategorija", - "rule_label": "Pravilo", + "rule_label": "Pravila", "list_label": "Seznam", "unknown_filter": "Neznan filter {{filterId}}", "known_tracker": "Znan sledilec", @@ -386,7 +393,7 @@ "client_edit": "Uredi odjemalca", "client_identifier": "Identifikator", "ip_address": "IP naslov", - "client_identifier_desc": "Odjemalce je mogoče identificirati po naslovu IP, CIDR, MAC naslovu. Upoštevajte, da je uporaba MAC kot identifikatorja mogoča le, če je AdGuard Home tudi <0>strežnik DHCP", + "client_identifier_desc": "Odjemalce je mogoče prepoznati po naslovu IP, CIDR, naslovu MAC ali posebnem ID-ju odjemalca (lahko se uporablja za DoT/DoH/DoQ). <0>Tukaj lahko izveste več o prepoznavanju odjemalcev.", "form_enter_ip": "Vnesite IP", "form_enter_mac": "Vnesite MAC", "form_enter_id": "Vnesi identifikatorja", @@ -430,6 +437,7 @@ "setup_dns_privacy_other_3": "<0>dnscrypt-proxy podpira <1>DNS-prek-HTTPS.", "setup_dns_privacy_other_4": "<0>Mozilla Firefox podpira <1>DNS-prek-HTTPS.", "setup_dns_privacy_other_5": "Našli boste več izvedb <0>tukaj in <1>tukaj.", + "setup_dns_privacy_ioc_mac": "Nastavitve iOS in macOS", "setup_dns_notice": "Za uporabo <1>DNS-prek-HTTPS ali <1>DNS-prek-TLS, morate <0>konfigurirati šifriranje v nastavitvah AdGuard Home.", "rewrite_added": "Uspešno je dodano DNS prepisovanje za \"{{key}}\"", "rewrite_deleted": "Uspešno je izbrisano DNS prepisovanje za \"{{key}}\"", @@ -529,7 +537,6 @@ "check_ip": "IP naslovi: {{ip}}", "check_cname": "CNAME: {{cname}}", "check_reason": "Razlog: {{reason}}", - "check_rule": "Pravilo: {{rule}}", "check_service": "Ime storitve: {{service}}", "service_name": "Ime storitve", "check_not_found": "Ni najdeno na vašem seznamu filtrov", diff --git a/client/src/__locales/sr-cs.json b/client/src/__locales/sr-cs.json index 060b2616..8f63a450 100644 --- a/client/src/__locales/sr-cs.json +++ b/client/src/__locales/sr-cs.json @@ -269,7 +269,6 @@ "source_label": "Izvor", "found_in_known_domain_db": "Pronađeno u poznatim bazama podataka domena.", "category_label": "Kategorija", - "rule_label": "Pravilo", "list_label": "Lista", "unknown_filter": "Nepoznat filter {{filterId}}", "known_tracker": "Poznato praćenje", @@ -330,7 +329,6 @@ "encryption_config_saved": "Konfiguracija šifrovanja je sačuvana", "encryption_server": "Ime servera", "encryption_server_enter": "Unesite vaše ime domena", - "encryption_server_desc": "Kako biste koristili HTTPS, potrebno je da unesete ime servera koje se podudara sa SSL sertifikatom.", "encryption_redirect": "Automatski preusmeri na HTTPS", "encryption_redirect_desc": "Ako je označeno, AdGuard Home će vas automatski preusmeravati sa HTTP na HTTPS adrese.", "encryption_https": "HTTPS port", @@ -386,7 +384,6 @@ "client_edit": "Izmeni klijent", "client_identifier": "Identifikator", "ip_address": "IP adresa", - "client_identifier_desc": "Klijenti mogu da budu prepoznati po IP adresi ili MAC adresi. Imajte na umu da je korišćenje MAC adrese kao identifikatora moguće samo ako je AdGuard Home takođe a <0>DHCP server", "form_enter_ip": "Unesite IP", "form_enter_mac": "Unesite MAC", "form_enter_id": "Unesite identifikator", @@ -529,7 +526,6 @@ "check_ip": "IP adrese: {{ip}}", "check_cname": "CNAME: {{cname}}", "check_reason": "Razlog: {{reason}}", - "check_rule": "Pravilo: {{rule}}", "check_service": "Ime usluge: {{service}}", "service_name": "Ime usluge", "check_not_found": "Nije pronađeno na vašoj listi filtera", diff --git a/client/src/__locales/sv.json b/client/src/__locales/sv.json index 69105d28..0fae1dda 100644 --- a/client/src/__locales/sv.json +++ b/client/src/__locales/sv.json @@ -177,7 +177,6 @@ "source_label": "Källa", "found_in_known_domain_db": "Hittad i domändatabas.", "category_label": "Kategori", - "rule_label": "Regel", "unknown_filter": "Okänt filter {{filterId}}", "install_welcome_title": "Välkommen till AdGuard Home!", "install_welcome_desc": "AdGuard Home är en DNS-server för nätverkstäckande annons- och spårningsblockering. Dess syfte är att de dig kontroll över hela nätverket och alla dina enheter, utan behov av att använda klientbaserade program.", @@ -235,7 +234,6 @@ "encryption_config_saved": "Krypteringsinställningar sparade", "encryption_server": "Servernamn", "encryption_server_enter": "Skriv in ditt domännamn", - "encryption_server_desc": "För att använda HTTPS behöver du skriva in servernamnet som stämmer överens med ditt SSL-certifikat.", "encryption_redirect": "Omdirigera till HTTPS automatiskt", "encryption_redirect_desc": "Om bockad kommer AdGuard Home automatiskt att omdirigera dig från HTTP till HTTPS-adresser.", "encryption_https": "HTTPS-port", @@ -287,7 +285,6 @@ "client_edit": "Redigera klient", "client_identifier": "Identifikator", "ip_address": "IP-adress", - "client_identifier_desc": "Klienter kan identifieras genom IP-adresser eller MAC-adresser. Notera att användning av MAC som identifierare bara är möjligt om AdGuard Home också är en <0>DHCP-server", "form_enter_ip": "Skriv in IP", "form_enter_mac": "Skriv in MAC", "form_client_name": "Skriv in klientnamn", diff --git a/client/src/__locales/th.json b/client/src/__locales/th.json index b5197b4c..aeb75325 100644 --- a/client/src/__locales/th.json +++ b/client/src/__locales/th.json @@ -200,7 +200,6 @@ "source_label": "ที่มา", "found_in_known_domain_db": "พบในฐานข้อมูลโดเมนที่รู้จัก", "category_label": "ประเภท", - "rule_label": "กฎ", "unknown_filter": "ตัวกรองที่ไม่รู้จัก {{filterId}}", "install_welcome_title": "ยินดีต้อนรับสู่ AdGuard Home", "install_welcome_desc": "AdGuard Home เป็นเซิร์ฟเวอร์ DNS ปิดกั้นโฆษณาและติดตามทั่วทั้งเครือข่าย วัตถุประสงค์คือเพื่อให้คุณควบคุมเครือข่ายทั้งหมดและอุปกรณ์ทั้งหมดของคุณและไม่จำเป็นต้องใช้โปรแกรมฝั่งไคลเอ็นต์", @@ -258,7 +257,6 @@ "encryption_config_saved": "บันทึกการตั้งค่าเข้ารหัสเรียบร้อยแล้ว", "encryption_server": "ชื่อเซิร์ฟเวอร์", "encryption_server_enter": "ป้อนชื่อโดเมน", - "encryption_server_desc": "ในการใช้ HTTPS คุณต้องป้อนชื่อเซิร์ฟเวอร์ที่ตรงกับใบรับรอง SSL ของคุณ", "encryption_redirect": "ไปเส้นทาง HTTPS อัตโนมัติ", "encryption_redirect_desc": "หากเลือกตัวเลือกนี้ AdGuard Home จะเปลี่ยนเส้นทางคุณจากที่อยู่ HTTP ไปยัง HTTPS โดยอัตโนมัติ", "encryption_https": "พอร์ท HTTPS", @@ -312,7 +310,6 @@ "client_edit": "แก้ไขเครื่องลูกข่าย", "client_identifier": "ตรวจสอบโดย", "ip_address": "IP addresses", - "client_identifier_desc": "ลูกค้าสามารถระบุได้โดยที่อยู่ IP, CIDR, ที่อยู่ MAC โปรดทราบว่าการใช้ MAC เป็นตัวระบุเป็นไปได้ก็ต่อเมื่อ AdGuard Home เป็น <0>เซิร์ฟเวอร์ DHCP ด้วย", "form_enter_ip": "กรอก IP", "form_enter_mac": "กรอก MAC", "form_enter_id": "ป้อนตัวระบุ", diff --git a/client/src/__locales/tr.json b/client/src/__locales/tr.json index e937207e..04b06729 100644 --- a/client/src/__locales/tr.json +++ b/client/src/__locales/tr.json @@ -2,18 +2,20 @@ "client_settings": "İstemci ayarları", "example_upstream_reserved": "<0>Belirli alan adları için DNS üst sunucusu tanımlayabilirsiniz.", "example_upstream_comment": "Bir yorum belirtebilirsiniz", - "upstream_parallel": "Tüm üst sunucuları eş zamanlı sorgulayarak çözümü hızlandırmak için paralel sorguları kullan", + "upstream_parallel": "Tüm üst sunucuları eş zamanlı sorgulayarak çözümü hızlandırmak için paralel istekleri kullan", "parallel_requests": "Paralel istekler", + "load_balancing": "Yük dengeleme", + "load_balancing_desc": "Her seferinde bir sunucuyu sorgulayın. AdGuard Home, sunucuyu seçmek için ağırlıklı rastgele algoritmayı kullanacak, böylece en hızlı sunucu daha sık kullanılacak.", "bootstrap_dns": "DNS Önyükleme sunucuları", "bootstrap_dns_desc": "DNS Önyükleme sunucuları, seçtiğiniz üst sunucuların DoH/DoT çözücülerine ait ip adreslerinin çözülmesi için kullanılır.", - "check_dhcp_servers": "DHCP sunucularını yokla", - "save_config": "Ayarları kaydet", + "check_dhcp_servers": "DHCP sunucularını denetle", + "save_config": "Yapılandırmayı kaydet", "enabled_dhcp": "DHCP sunucusu etkinleştirildi", "disabled_dhcp": "DHCP sunucusu devre dışı bırakıldı", "unavailable_dhcp": "DHCP kullanılamıyor", "unavailable_dhcp_desc": "AdGuard Home, işletim sisteminizde DHCP sunucusu çalıştıramıyor", "dhcp_title": "DHCP sunucusu (deneysel!)", - "dhcp_description": "Eğer router'ınız DHCP ayarlarını sunmuyorsa AdGuard'ın dahili DHCP sunucusunu kullanabilirsiniz.", + "dhcp_description": "Yönlendiriciniz DHCP ayarlarını sağlamıyorsa, AdGuard'ın kendi yerleşik DHCP sunucusunu kullanabilirsiniz.", "dhcp_enable": "DHCP sunucusunu etkinleştir", "dhcp_disable": "DHCP sunucusunu devre dışı bırak", "dhcp_not_found": "Yerleşik DHCP sunucusunu etkinleştirmek güvenlidir - Ağ üzerinde herhangi bir aktif DHCP sunucusu bulamadık. Ancak, otomatik testimiz şu anda %100 garanti vermediği için el ile tekrar kontrol etmenizi öneririz.", @@ -21,15 +23,16 @@ "dhcp_leases": "DHCP kiralamaları", "dhcp_static_leases": "Sabit DHCP kiralamaları", "dhcp_leases_not_found": "DHCP kiralaması bulunamadı", - "dhcp_config_saved": "DHCP sunucusu ayarı kaydedildi", + "dhcp_config_saved": "DHCP sunucusu yapılandırması kaydedildi", "dhcp_ipv4_settings": "DHCP IPv4 Ayarları", "dhcp_ipv6_settings": "DHCP IPv6 Ayarları", "form_error_required": "Gerekli alan", - "form_error_ip4_format": "Geçersiz IPv4 formatı", - "form_error_ip6_format": "Geçersiz IPv6 formatı", - "form_error_ip_format": "Geçersiz IPv4 formatı", + "form_error_ip4_format": "Geçersiz IPv4 biçimi", + "form_error_ip6_format": "Geçersiz IPv6 biçimi", + "form_error_ip_format": "Geçersiz IP biçimi", "form_error_mac_format": "Geçersiz MAC biçimi", - "form_error_client_id_format": "Geçersiz müşteri kimliği formatı", + "form_error_client_id_format": "Geçersiz istemci kimliği biçimi", + "form_error_server_name": "Geçersiz sunucu adı", "form_error_positive": "0'dan büyük olmalı", "form_error_negative": "0 veya daha büyük olmalıdır", "range_end_error": "Başlangıç aralığından daha büyük olmalı", @@ -43,10 +46,10 @@ "dhcp_interface_select": "DHCP arayüzünü seç", "dhcp_hardware_address": "Donanım adresi", "dhcp_ip_addresses": "IP adresleri", - "ip": "IP Adresi", - "dhcp_table_hostname": "Bilgisayar Adı", + "ip": "IP", + "dhcp_table_hostname": "Ana bilgisayar Adı", "dhcp_table_expires": "Geçerlilik Tarihi", - "dhcp_warning": "Dahili DHCP sunucusunu etkinleştirmek istiyorsanız başka aktif DHCP sunucusu olmadığından emin olun. Aksi takdirde cihazlar internete bağlanamayabilir.", + "dhcp_warning": "DHCP sunucusunu yine de etkinleştirmek istiyorsanız, ağınızda başka bir aktif DHCP sunucusu olmadığından emin olun. Aksi takdirde, bağlı cihazlar için interneti kırabilir!", "dhcp_error": "Ağda başka bir DHCP sunucusu olup olmadığını belirleyemedik.", "dhcp_static_ip_error": "DHCP sunucusunu kullanmak için statik bir IP adresi ayarlanmalıdır. Bu ağ arayüzünün statik IP adresi kullanılarak yapılandırılıp yapılandırılmadığını belirleyemedik. Lütfen statik bir IP adresini elle ayarlayın.", "dhcp_dynamic_ip_found": "Sisteminiz <0>{{interfaceName}} arayüzü için dinamik IP adresi yapılandırması kullanıyor. DHCP sunucusunu kullanmak için statik bir IP adresi ayarlanmalıdır. Geçerli IP adresiniz <0>{{ipAddress}}. DHCP'yi etkinleştir düğmesine basarsanız bu IP adresini statik IP adresiniz olarak ayarlayacağız.", @@ -59,8 +62,10 @@ "country": "Ülke", "city": "Şehir", "delete_confirm": "\"{{key}}\" silmek istediğinizden emin misiniz?", - "form_enter_hostname": "Cihaz ismi girin", + "form_enter_hostname": "Ana bilgisayar adı girin", "error_details": "Hata detayları", + "response_details": "Yanıt ayrıntıları", + "request_details": "İstek ayrıntıları", "client_details": "İstemci detayları", "details": "Detaylar", "back": "Geri", @@ -69,6 +74,7 @@ "filters": "Filtreler", "filter": "Filtre", "query_log": "Sorgu Günlüğü", + "compact": "Kompakt", "nothing_found": "Hiçbir şey bulunamadı", "faq": "SSS", "version": "Sürüm", @@ -76,7 +82,7 @@ "protocol": "Protokol", "on": "AÇIK", "off": "KAPALI", - "copyright": "Tüm hakları saklıdır", + "copyright": "Telif hakkı", "homepage": "Anasayfa", "report_an_issue": "Bir sorun bildir", "privacy_policy": "Gizlilik politikası", @@ -109,7 +115,7 @@ "number_of_dns_query_to_safe_search": "Güvenli Aramanın zorunlu kıldığı arama motorlarına gönderilen DNS isteklerinin sayısı", "average_processing_time": "Ortalama işlem süresi", "average_processing_time_hint": "Bir DNS isteğinin mili saniye cinsinden ortalama işlem süresi", - "block_domain_use_filters_and_hosts": "Filtreleri ve hosts listelerini kullanarak alan adlarını engelle", + "block_domain_use_filters_and_hosts": "Filtre ve ana bilgisayar listelerini kullanarak alan adlarını engelle", "filters_block_toggle_hint": "Filtreler sayfasından engelleme kurallarını ayarlayabilirsiniz.", "use_adguard_browsing_sec": "AdGuard gezinti koruması web hizmetini kullan", "use_adguard_browsing_sec_hint": "AdGuard Home, alan adının gezinti koruması web hizmetinde kara listede olup olmadığını kontrol edecek. Kontrol işlemi gizlilik dostu API kullanılarak yapılacak: yalnızca alan adının kısa bir ön eki SHA256 ile şifrelenip sunucuya gönderilecek.", @@ -121,9 +127,9 @@ "general_settings": "Genel ayarlar", "dns_settings": "DNS ayarları", "dns_blocklists": "DNS engelleme listeleri", - "dns_allowlists": "DNS izin listeleri", + "dns_allowlists": "DNS izin verilen listeleri", "dns_blocklists_desc": "AdGuard Home, engelleme listeleriyle eşleşen alanları engeller.", - "dns_allowlists_desc": "DNS izin listelerindeki alanlara, engelleme listelerinden birinde olsalar bile izin verilir.", + "dns_allowlists_desc": "DNS izin verilen listelerindeki alanlara, engelleme listelerinden birinde olsalar bile izin verilir.", "custom_filtering_rules": "Özel filtreleme kuralları", "encryption_settings": "Şifreleme ayarları", "dhcp_settings": "DHCP ayarları", @@ -131,7 +137,7 @@ "upstream_dns_help": "Her satıra bir sunucu adresi girin. Üst DNS sunucularını yapılandırma hakkında daha fazla bilgi edinin.", "upstream_dns_configured_in_file": "{{path}} içinde yapılandırıldı", "test_upstream_btn": "Üst sunucuyu test et", - "upstreams": "Upstreams", + "upstreams": "Üst kaynak", "apply_btn": "Uygula", "disabled_filtering_toast": "Filtreleme devre dışı", "enabled_filtering_toast": "Filtreleme çalışıyor", @@ -151,11 +157,11 @@ "edit_table_action": "Düzenle", "delete_table_action": "Sil", "elapsed": "Geçen zaman", - "filters_and_hosts_hint": "AdGuard Home temel reklam engelleme kurallarını ve hosts dosyalarının söz dizim kurallarını anlamaktadır.", - "no_blocklist_added": "Hiçbir engelleme listesi eklenmedi", + "filters_and_hosts_hint": "AdGuard Home temel reklam engelleme kuralları ve ana bilgisayar dosyalarının sözdizim kurallarını anlamaktadır.", + "no_blocklist_added": "Engelleme listesi eklenmedi", "no_whitelist_added": "İzin verilen listesi eklenmedi", "add_blocklist": "Engelleme listesi ekle", - "add_allowlist": "İzin listesi ekle", + "add_allowlist": "İzin verilen listesine ekle", "cancel_btn": "İptal", "enter_name_hint": "İsim girin", "enter_url_or_path_hint": "Bir URL ya da listenin tam yolunu girin", @@ -168,16 +174,16 @@ "choose_allowlist": "İzin verilen listelerini seç", "enter_valid_blocklist": "Engelleme listesine geçerli bir URL girin.", "enter_valid_allowlist": "İzin verilen listesine geçerli bir URL girin.", - "form_error_url_format": "Geçersiz url biçim", + "form_error_url_format": "Geçersiz URL biçimi", "form_error_url_or_path_format": "Geçersiz URL ya da listenin tam yolu", "custom_filter_rules": "Özel filtreleme kuralları", - "custom_filter_rules_hint": "Her satıra bir kural girin. Reklama engelleme kuralı veya hosts dosyası söz dizimi kullanabilirsiniz.", + "custom_filter_rules_hint": "Her satıra bir kural girin. Reklama engelleme kuralı veya ana bilgisayar dosyası sözdizimi kullanabilirsiniz.", "examples_title": "Örnekler", "example_meaning_filter_block": "example.org alan adına ve tüm alt alan adlarına olan erişimi engeller", "example_meaning_filter_whitelist": "example.org alan adına ve tüm alt alan adlarına olan erişim engelini kaldırır", "example_meaning_host_block": "AdGuard Home bu example.org adresi için 127.0.0.1 adresine yönlendirme yapacaktır (alt alan adları için geçerli değildir)", "example_comment": "! Buraya bir yorum ekledim", - "example_comment_meaning": "yorum eklemek", + "example_comment_meaning": "sadece bir yorum", "example_comment_hash": "# Bir yorum daha ekledim", "example_regex_meaning": "belirtilen düzenli ifadelerle eşleşen alan adlarına erişimi engelle", "example_upstream_regular": "normal DNS (UDP üzerinden)", @@ -202,6 +208,7 @@ "domain_or_client": "Alan adı veya istemci", "type_table_header": "Tür", "response_table_header": "Yanıt", + "response_code": "Yanıt kodu", "client_table_header": "İstemci", "empty_response_status": "Boş", "show_all_filter_type": "Tümünü göster", @@ -220,9 +227,10 @@ "query_log_filtered": "{{filter}} tarafından filtrelendi", "query_log_confirm_clear": "Tüm sorgu günlüğünü temizlemek istediğinizden emin misiniz?", "query_log_cleared": "Sorgu günlüğü başarıyla temizlendi", - "query_log_clear": "Sorgu kayıtlarını temizle", - "query_log_retention": "Sorgu kayıtlarının saklanması", - "query_log_enable": "Günlük kaydını etkinleştir", + "query_log_updated": "Sorgu günlüğü başarıyla güncellendi", + "query_log_clear": "Sorgu günlüklerini temizle", + "query_log_retention": "Sorgu günlüklerinin saklanması", + "query_log_enable": "Günlüğü etkinleştir", "query_log_configuration": "Günlük yapılandırması", "query_log_disabled": "Sorgu günlüğü devre dışı bırakıldı ve <0>ayarlarda yapılandırılabilir", "query_log_strict_search": "Katı arama için çift tırnak işareti kullanın", @@ -240,14 +248,22 @@ "custom_ip": "Özel IP", "blocking_ipv4": "IPv4 engelleme", "blocking_ipv6": "IPv6 engelleme", - "dns_over_https": "DNS üzerinden HTTPS", + "dnscrypt": "DNSCrypt", + "dns_over_https": "DNS-over-HTTPS", "dns_over_tls": "DNS-over-TLS", + "dns_over_quic": "DNS-over-QUIC", + "client_id": "İstemci Kimliği", + "client_id_placeholder": "İstemci Kimliği girin", + "client_id_desc": "Farklı istemciler, özel bir istemci kimliği ile tanımlanabilir. Burada istemcileri nasıl belirleyeceğiniz hakkında daha fazla bilgi edinebilirsiniz.", "download_mobileconfig_doh": "DNS-over-HTTPS için .mobileconfig dosyasını indir", "download_mobileconfig_dot": "DNS-over-TLS için .mobileconfig dosyasını indir", + "download_mobileconfig": "Yapılandırma dosyasını indir", + "plain_dns": "Sade DNS", "form_enter_rate_limit": "Sıklık limitini girin", "rate_limit": "Sıklık limiti", "edns_enable": "EDNS İstemci Alt Ağını Etkinleştir", "edns_cs_desc": "Etkinleştirilirse, AdGuard Home, istemcilerin alt ağlarını DNS sunucularına gönderir.", + "rate_limit_desc": "Tek bir istemcinin yapmasına izin verilen saniye başına istek sayısı (0'a ayarlamak sınırsız anlamına gelir)", "blocking_ipv4_desc": "Engellenen bir A isteği için geri döndürülecek IP adresi", "blocking_ipv6_desc": "Engellenen bir AAAA isteği için geri döndürülecek IP adresi", "blocking_mode_default": "Varsayılan: Reklam engelleme stili kuralı tarafından engellendiğinde sıfır IP adresiyle (A için 0.0.0.0; AAAA için) yanıt verin; /etc/hosts-style kuralı tarafından engellendiğinde, kuralda belirtilen IP adresiyle yanıt verin", @@ -268,17 +284,17 @@ "install_welcome_desc": "AdGuard Home, ağ genelinde reklam ve izleyicileri engelleyen bir DNS sunucusudur. Tüm ağınızı ve tüm cihazlarınızı kontrol etmenize yarayan bir araçtır, istemci tarafında bir program kullanmanıza gerek duymaz.", "install_settings_title": "Yönetici Web Arayüzü", "install_settings_listen": "Dinleme arayüzü", - "install_settings_port": "Port", + "install_settings_port": "Bağlantı noktası", "install_settings_interface_link": "AdGuard Home yönetici web arayüzü sayfanız şu adresten erişilebilir olacaktır:", - "form_error_port": "Geçerli bir port değeri girin", + "form_error_port": "Geçerli bir bağlantı noktası değeri girin", "install_settings_dns": "DNS sunucusu", "install_settings_dns_desc": "Cihazlarınızı veya yönlendiricinizi şu adresteki DNS sunucusunu kullanması için ayarlamanız gerekecek:", "install_settings_all_interfaces": "Tüm arayüzler", "install_auth_title": "Kimlik Doğrulama", "install_auth_desc": "AdAdGuard Home yönetici web arayüzüne erişim için kullanıcı adı ve şifresi oluşturmanız şiddetle tavsiye edilir. Sadece yerel ağınız erişilebilir olsa bile izinsiz giriş yapılmasını engellemek için şifrenizin olması önemlidir.", "install_auth_username": "Kullanıcı adı", - "install_auth_password": "Şifre", - "install_auth_confirm": "Şifreyi onayla", + "install_auth_password": "Parola", + "install_auth_confirm": "Parolayı onayla", "install_auth_username_enter": "Kullanıcı adı girin", "install_auth_password_enter": "Şifre girin", "install_step": "Adım", @@ -289,7 +305,7 @@ "install_devices_router": "Yönlendirici", "install_devices_router_desc": "Bu kurulum evdeki yönlendiricinize bağlı tüm cihazlarınızı otomatik olarak kapsar ve her birini elle ayarlamanız gerekmez.", "install_devices_address": "AdGuard Home DNS sunucusu şu adresi dinleyecektir", - "install_devices_router_list_1": "Yönlendiricinizin ayarlarına girin. Genelde internet tarayıcınızdan bir URL vasıtasıyla erişebilirsiniz (http://192.168.0.1/ veya http://192.168.1.1/ gibi). Sizden şifre girmenizi isteyebilir. Hatırlamıyorsanız yönlendiricinizin arkasındaki 'reset' tuşuna basılı tutup fabrika ayarlarına sıfırlayabilirsiniz. Bazı yönlendiriciler belirli uygulamalarla çalışır, bu durumda bilgisayarınıza/telefonunuza kurulması gerekir.", + "install_devices_router_list_1": "Yönlendiricinizin ayarlarına girin. Genelde internet tarayıcınızdan bir URL vasıtasıyla erişebilirsiniz (http://192.168.0.1/ veya http://192.168.1.1/ gibi). Sizden parola girmenizi isteyebilir. Hatırlamıyorsanız yönlendiricinizin arkasındaki 'reset' tuşuna basılı tutup fabrika ayarlarına sıfırlayabilirsiniz. Bazı yönlendiriciler belirli uygulamalarla çalışır, bu durumda bilgisayarınıza/telefonunuza kurulması gerekir.", "install_devices_router_list_2": "DHCP/DNS ayarlarını bulun. DNS satırlarını arayın, genelde iki veya üç tanedir, üç rakam girilebilen dört ayrı grup içeren satırdır.", "install_devices_router_list_3": "AdGuard Home sunucusunun adresini o kısma yazın.", "install_devices_router_list_4": "Bazı yönlendirici tiplerinde özel bir DNS sunucusu ayarlayamazsınız. Bu durumda AdGuard Home'u bir DHCP sunucu olarak ayarlamanız yardımcı olabilir. Aksi halde, yönlendirici modeliniz için <0>DNS sunucularını elle nasıl özelleştirebileceğinizi aramalısınız.", @@ -313,7 +329,7 @@ "install_devices_ios_list_3": "Bağlı olduğunuz ağın ismine dokunun.", "install_devices_ios_list_4": "DNS alanına AdGuard Home sunucunuzun adreslerini girin.", "get_started": "Başlarken", - "next": "İleri", + "next": "Sonraki", "open_dashboard": "Panoyu Aç", "install_saved": "Başarıyla kaydedildi", "encryption_title": "Şifreleme", @@ -321,7 +337,7 @@ "encryption_config_saved": "Şifreleme ayarı kaydedildi", "encryption_server": "Sunucu adı", "encryption_server_enter": "Alan adınızı girin", - "encryption_server_desc": "HTTPS kullanmak için SSL sertifikanızla eşleşen sunucu adını girmeniz gerekir", + "encryption_server_desc": "HTTPS kullanmak için, SSL sertifikanız veya joker karakter sertifikanızla eşleşen sunucu adını girmeniz gerekir. Alan ayarlanmazsa, herhangi bir alan adı için TKG bağlantılarını kabul eder.", "encryption_redirect": "Otomatik olarak HTTPS'e yönlendir", "encryption_redirect_desc": "Etkinleştirirseniz AdGuard Home sizi HTTP yerine HTTPS adreslerine yönlendirir.", "encryption_https": "HTTPS bağlantı noktası", @@ -332,11 +348,11 @@ "encryption_doq_desc": "Bu bağlantı noktası yapılandırılırsa, AdGuard Home bu bağlantı noktasında DNS-over-QUIC sunucusu çalıştıracaktır. Deneysel ve güvenilir olmayabilir. Ayrıca, şu anda bunu destekleyen çok fazla istemci yok.", "encryption_certificates": "Sertifikalar", "encryption_certificates_desc": "Şifrelemeyi kullanmak için alan adınız için geçerli bir SSL sertifika zinciri temin etmeniz gerekir. <0>{{link}} adresinden ücretsiz temin edebilirsiniz veya güvenilir Sertifika Otoritelerinden satın alabilirsiniz.", - "encryption_certificates_input": "PEM formatındaki sertifikalarınızı buraya yapıştırın.", + "encryption_certificates_input": "PEM biçimindeki sertifikalarınızı buraya yapıştırın.", "encryption_status": "Durum", "encryption_expire": "Bitiş tarihi", "encryption_key": "Özel anahtar", - "encryption_key_input": "Sertifikanızın PEM formatı özel anahtarını buraya yapıştırın.", + "encryption_key_input": "Sertifikanızın PEM biçimi özel anahtarını buraya yapıştırın.", "encryption_enable": "Şifrelemeyi etkinleştir (HTTPS, DNS-over-HTTPS ve DNS-over-TLS)", "encryption_enable_desc": "Şifrelemeyi etkinleştirirseniz, AdGuard Home yönetici arayüzü HTTPS üzerinden çalışır ve DNS sunucusu DNS-over-HTTPS ve DNS-over-TLS üzerinden gelen istekleri dinler.", "encryption_chain_valid": "Sertifika zinciri geçerli", @@ -345,12 +361,12 @@ "encryption_key_invalid": "Bu geçersiz bir {{type}} özel anahtar", "encryption_subject": "Konu", "encryption_issuer": "Sertifikayı veren", - "encryption_hostnames": "Ana bilgisayar isimleri", + "encryption_hostnames": "Ana bilgisayar adları", "encryption_reset": "Şifreleme ayarlarını sıfırlamak istediğinize emin misiniz?", "topline_expiring_certificate": "SSL sertifikanızın süresi dolmak üzere. <0>Şifreleme ayarlarını güncelleyin.", "topline_expired_certificate": "SSL sertifikanızın süresi dolmuş. <0>Şifreleme ayarlarını güncelleyin.", - "form_error_port_range": "80-65535 aralığında geçerli bir port değeri girin.", - "form_error_port_unsafe": "Bu güvenli olmayan bir port", + "form_error_port_range": "80-65535 aralığında geçerli bir bağlantı noktası değeri girin", + "form_error_port_unsafe": "Bu güvenli olmayan bir bağlantı noktası", "form_error_equal": "Aynı olmamalı", "form_error_password": "Şifreler uyuşmuyor", "reset_settings": "Ayarları sıfırla", @@ -377,12 +393,13 @@ "client_edit": "İstemciyi düzenle", "client_identifier": "Tanımlayıcı", "ip_address": "IP adresi", - "client_identifier_desc": "İstemciler IP adresleri veya MAC adresleri ile tanımlanabilir. Lütfen not edin, MAC adresi ile tanımlamayı kullanmak için AdGuard Home'un <0>DHCP Sunucusu olması gerekir.", + "client_identifier_desc": "İstemciler IP adresi, CIDR, MAC adresi veya özel bir istemci kimliği ile tanımlanabilir (DoT/DoH/DoQ için kullanılabilir). <0>Burada istemcileri nasıl belirleyeceğiniz hakkında daha fazla bilgi edinebilirsiniz.", "form_enter_ip": "IP Girin", "form_enter_mac": "MAC Girin", "form_enter_id": "Tanımlayıcı girin", "form_add_id": "Tanımlayıcı ekle", "form_client_name": "İstemci ismi girin", + "name": "İsim", "client_global_settings": "Genel ayarları kullan", "client_deleted": "\"{{key}}\" istemcisi başarıyla silindi", "client_added": "\"{{key}}\" istemcisi başarıyla eklendi", @@ -407,6 +424,8 @@ "dns_privacy": "DNS Gizliliği", "setup_dns_privacy_1": "<0>DNS-over-TLS: <1>{{address}} dizesini kullan.", "setup_dns_privacy_2": "<0>DNS-over-HTTPS: <1>{{address}} dizesini kullan.", + "setup_dns_privacy_3": "<0>İşte kullanabileceğiniz yazılımların bir listesi.", + "setup_dns_privacy_4": "Bir iOS 14 veya macOS Big Sur cihazında, DNS ayarlarına DNS-over-HTTPS veya DNS-over-TLS sunucuları ekleyen özel '.mobileconfig' dosyasını indirebilirsiniz.", "setup_dns_privacy_android_1": "Android 9 aslen DNS-over-TLS desteklemektedir. Yapılandırmak için, Ayarlar → Ağ ve internet → Gelişmiş → Özel DNS seçeneğine gidin ve alan adınızı buraya girin.", "setup_dns_privacy_android_2": "<0>AdGuard for Android, <1>DNS-over-HTTPS ve <1>DNS-over-TLS desteklemektedir.", "setup_dns_privacy_android_3": "<0>Intra Android'e <1>DNS-over-HTTPS desteğini ekler.", @@ -418,6 +437,7 @@ "setup_dns_privacy_other_3": "<0>dnscrypt-proxy, <1>DNS-over-HTTPS destekler.", "setup_dns_privacy_other_4": "<0>Mozilla Firefox, <1>DNS-over-HTTPS desteklemektedir.", "setup_dns_privacy_other_5": "<0>Burada ve <1>burada daha fazla uygulama bulacaksınız.", + "setup_dns_privacy_ioc_mac": "iOS ve macOS yapılandırması", "setup_dns_notice": "<1>DNS-over-HTTPS veya <1>DNS-over-TLS kullanmak için, AdGuard Home ayarlarında <0>Şifreleme yapılandırmasını yapmanız gerekir.", "rewrite_added": "\"{{key}}\" için DNS yeniden yazımı başarıyla eklendi", "rewrite_deleted": "\"{{key}}\" için DNS yeniden yazımı başarıyla silindi", @@ -426,12 +446,12 @@ "rewrite_confirm_delete": "\"{{key}}\" için DNS yeniden yazımını silmek istediğinize emin misiniz?", "rewrite_desc": "Belirli bir alan adı için kolayca özel DNS yanıtı yapılandırmanıza olanak tanır.", "rewrite_applied": "Uygulanan Yeniden Yazım kuralı", - "rewrite_hosts_applied": "Host dosyası kuralı tarafından yeniden yazıldı", + "rewrite_hosts_applied": "Ana bilgisayar dosyası kuralı tarafından yeniden yazıldı", "dns_rewrites": "DNS yeniden yazımları", "form_domain": "Alan adı girin", "form_answer": "IP adresini veya alan adı girin", - "form_error_domain_format": "Geçersiz alan adı formatı", - "form_error_answer_format": "Geçersiz cevap formatı", + "form_error_domain_format": "Geçersiz alan adı biçimi", + "form_error_answer_format": "Geçersiz cevap biçimi", "configure": "Yapılandır", "main_settings": "Ana ayarlar", "block_services": "Belirli hizmetleri engelle", @@ -485,43 +505,45 @@ "network": "Ağ", "descr": "Açıklama", "whois": "Whois", - "filtering_rules_learn_more": "Ana makinelere dair kendi kara listelerinizi oluşturmakla alakalı <0>daha fazla bilgi edinin.", + "filtering_rules_learn_more": "Kendi ana bilgisayar listelerinizi oluşturma hakkında <0>daha fazla bilgi edinin.", "blocked_by_response": "Cevap olarak CNAME veya IP tarafından engellendi", "blocked_by_cname_or_ip": "CNAME veya IP tarafından engellendi", "try_again": "Tekrar deneyin", "domain_desc": "Yeniden yazılmasını istediğiniz alan adını veya joker karakteri girin.", "example_rewrite_domain": "cevapları yalnızca bu alan adı için yeniden yaz.", "example_rewrite_wildcard": "tüm <0>example.org alt alanları için cevapları yeniden yaz.", + "rewrite_ip_address": "IP adresi: bu IP'yi A veya AAAA yanıtında kullanın", "rewrite_domain_name": "Alan adı: bir CNAME kaydı ekleyin", + "rewrite_A": "<0>A: özel değer, üst sunucudan gelen <0>A kayıtlarını tut", + "rewrite_AAAA": "<0>AAA: özel değer, üst sunucudan gelen <0>AAA kayıtlarını tut", "disable_ipv6": "IPv6'yı Devre Dışı Bırak", "disable_ipv6_desc": "Bu özelliği etkinleştirirseniz, IPv6 adresleri (AAAA tipi) için gönderilen tüm DNS istekleri cevapsız bırakılacaktır.", "fastest_addr": "En hızlı IP adresi", "fastest_addr_desc": "Tüm DNS sunucularını sorgulayın ve tüm yanıtlar arasından en hızlı IP adresini döndürün. Bu, tüm DNS sunucularından yanıt beklememiz gerektiğinden DNS sorgularını yavaşlatacak ancak genel bağlantıyı iyileştirecektir.", "autofix_warning_text": "\"Düzelt\" i tıklatırsanız, AdGuardHome sisteminizi AdGuardHome DNS sunucusunu kullanacak şekilde yapılandırır.", - "autofix_warning_list": "Bu görevleri gerçekleştirecektir: <0> sistemi DNSStubListener'ı devre dışı bırakma <0> DNS sunucu adresini 127.0.0.1 olarak ayarlayın <0> /etc/resolv.conf / / run / systemd sembolik bağlantı hedefini değiştirin /resolve/resolv.conf <0> durdur DNSStubListener (sistemde yeniden çözülmüş hizmeti yeniden yükle) ", - "autofix_warning_result": "Sonuç olarak, sisteminizden gelen tüm DNS istekleri varsayılan olarak AdGuardHome tarafından işlenir.", + "autofix_warning_list": "Bu görevleri gerçekleştirecek: <0>Sistem DNSStubListener'ı devre dışı bırakın <0>DNS sunucusu adresini 127.0.0.1 olarak ayarlayın <0>/etc/resolv.conf'un sembolik bağlantı hedefini /run/systemd/resolve/resolv.conf ile değiştirin<0> <0>DNSStubListener'ı durdurun (systemd çözümlenmiş hizmeti yeniden yükleyin)", + "autofix_warning_result": "Sonuç olarak, sisteminizden gelen tüm DNS istekleri varsayılan olarak AdGuard Home tarafından işlenecektir.", "tags_title": "Etiketler", - "tags_desc": "İstemciye karşılık gelen etiketleri seçebilirsiniz. Etiketler, filtreleme kurallarına dahil edilebilir ve bunları daha doğru bir şekilde uygulamanıza olanak tanır. <0> Daha fazla bilgi edinin ", - "form_select_tags": "Müşteri etiketlerini seçin", - "check_title": "Filtrelemeyi kontrol edin", + "tags_desc": "Müşteriye karşılık gelen etiketleri seçebilirsiniz. Etiketler filtreleme kurallarına dahil edilebilir ve bunları daha doğru uygulamanıza olanak tanır. <0>Daha fazla bilgi edinin", + "form_select_tags": "İstemci etiketlerini seçin", + "check_title": "Filtrelemeyi denetleyin", "check_desc": "Ana bilgisayar adının filtrelenip filtrelenmediğini kontrol edin", - "check": "Kontrol", - "form_enter_host": "Bir ana bilgisayar adı girin", + "check": "Denetle", + "form_enter_host": "Ana bilgisayar adı girin", "filtered_custom_rules": "Özel filtreleme kurallarına göre filtrelendi", "choose_from_list": "Listeden seç", "add_custom_list": "Özel bir liste ekle", - "host_whitelisted": "Ana makine beyaz listeye alındı", + "host_whitelisted": "Ana bilgisayar beyaz listeye eklendi", "check_ip": "IP adresleri: {{ip}}", "check_cname": "CNAME: {{cname}}", "check_reason": "Sebep: {{reason}}", - "check_rule": "Kural: {{rule}}", "check_service": "Hizmet adı: {{service}}", "service_name": "Servis adı", "check_not_found": "Filtre listelerinizde bulunamadı", "client_confirm_block": "\"{{ip}}\" istemcisini engellemek istediğinizden emin misiniz?", "client_confirm_unblock": "\"{{ip}}\" istemcisinin engellemesini kaldırmak istediğinizden emin misiniz?", "client_blocked": "\"{{ip}}\" istemcisi başarıyla engellendi", - "client_unblocked": "\"{{ip}}\" müşterisinin engellemesi başarıyla kaldırıldı", + "client_unblocked": "\"{{ip}}\" istemcinin engellemesi başarıyla kaldırıldı", "static_ip": "Statik IP adres", "static_ip_desc": "AdGuard Home bir sunucudur, bu nedenle düzgün çalışması için statik bir IP adresine ihtiyaç duyar. Aksi takdirde, bir noktada yönlendiriciniz bu cihaza farklı bir IP adresi atayabilir.", "set_static_ip": "Statik IP adresi ayarlama", @@ -533,6 +555,7 @@ "list_updated_plural": "{{count}} liste güncellendi", "dnssec_enable": "DNSSEC'i etkinleştir", "dnssec_enable_desc": "DNSSEC'i giden DNS sorguları için etkinleştir ve sonucu kontrol et (DNSSEC-etkin sorgulama gerekli)", + "validated_with_dnssec": "DNSSEC ile doğrulandı", "all_queries": "Tüm sorgular", "show_blocked_responses": "Engellendi", "show_whitelisted_responses": "Beyaz listeye eklendi", @@ -542,6 +565,7 @@ "blocked_threats": "Engellenen Tehditler", "allowed": "İzin verildi", "filtered": "Filtrelenmiş", + "rewritten": "Yeniden yazılan", "safe_search": "Güvenli arama", "blocklist": "Engelleme listesi", "milliseconds_abbreviation": "ms", @@ -561,10 +585,13 @@ "filter_category_other": "Diğer", "filter_category_general_desc": "Çoğu cihazda izlemeyi ve reklamları engelleyen listeler", "filter_category_security_desc": "Kötü amaçlı yazılım, kimlik avı veya dolandırıcılık alanlarını engelleme konusunda özelleştirilmiş listeler", + "filter_category_regional_desc": "Bölgesel reklamlara ve izleme sunucularına odaklanan listeler", "filter_category_other_desc": "Diğer engelleme listeleri", "setup_config_to_enable_dhcp_server": "DHCP sunucusunu etkinleştirmek için kurulum yapılandırması", + "original_response": "Esas yanıt", "click_to_view_queries": "Sorguları görmek için tıklayın", "port_53_faq_link": "Port 53 genellikle \"DNSStubListener\" veya \"sistemd-resolved\" hizmetler tarafından kullanılır. Lütfen problemin nasıl çözüleceğine ilişkin <0>bu talimatı okuyun.", "adg_will_drop_dns_queries": "AdGuard Home, bu istemciden gelen tüm DNS sorgularını iptal eder.", + "client_not_in_allowed_clients": "İstemciye \"İzin verilen istemciler\" listesinde olmadığı için izin verilmiyor.", "experimental": "Deneysel" } \ No newline at end of file diff --git a/client/src/__locales/vi.json b/client/src/__locales/vi.json index d593c812..368b0f9a 100644 --- a/client/src/__locales/vi.json +++ b/client/src/__locales/vi.json @@ -1,6 +1,7 @@ { "client_settings": "Cài đặt máy khách", "example_upstream_reserved": "bạn có thể chỉ định DNS ngược tuyến <0>cho một tên miền cụ thể(hoặc nhiều)", + "example_upstream_comment": "Bạn có thể thêm chú thích cụ thể", "upstream_parallel": "Sử dụng truy vấn song song để tăng tốc độ giải quyết bằng cách truy vấn đồng thời tất cả các máy chủ ngược tuyến", "parallel_requests": "Yêu cầu song song", "load_balancing": "Cân bằng tải", @@ -31,6 +32,7 @@ "form_error_ip_format": "Định dạng IPv4 không hợp lệ", "form_error_mac_format": "Định dạng MAC không hợp lệ", "form_error_client_id_format": "Định dạng client ID không hợp lệ", + "form_error_server_name": "Tên máy chủ không hợp lệ", "form_error_positive": "Phải lớn hơn 0", "form_error_negative": "Phải lớn hơn hoặc bằng 0", "range_end_error": "Phải lớn hơn khoảng bắt đầu", @@ -132,6 +134,8 @@ "encryption_settings": "Cài đặt mã hóa", "dhcp_settings": "Cài đặt DHCP", "upstream_dns": "Máy chủ DNS tìm kiếm", + "upstream_dns_help": "Nhập địa chỉ máy chủ một trên mỗi dòng. Tìm hiểu thêm về cách định cấu hình máy chủ DNS ngược dòng.", + "upstream_dns_configured_in_file": "Cấu hình tại {{path}}", "test_upstream_btn": "Kiểm tra", "upstreams": "Nguồn", "apply_btn": "Áp dụng", @@ -185,6 +189,7 @@ "example_upstream_regular": "DNS thông thường (dùng UDP)", "example_upstream_dot": "được mã hoá <0>DNS-over-TLS", "example_upstream_doh": "được mã hoá <0>DNS-over-HTTPS", + "example_upstream_doq": "được mã hoá <0>DNS-over-QUIC", "example_upstream_sdns": "bạn có thể sử dụng <0>DNS Stamps for <1>DNSCrypt hoặc <2>DNS-over-HTTPS ", "example_upstream_tcp": "DNS thông thường(dùng TCP)", "all_lists_up_to_date_toast": "Tất cả danh sách đã ở phiên bản mới nhất", @@ -193,6 +198,10 @@ "dns_test_not_ok_toast": "Máy chủ \"\"': không thể sử dụng, vui lòng kiểm tra lại", "unblock": "Bỏ chặn", "block": "Chặn", + "disallow_this_client": "Không cho phép client này", + "allow_this_client": "Cho phép ứng dụng khách này", + "block_for_this_client_only": "Chỉ chặn ứng dụng khách này", + "unblock_for_this_client_only": "Chỉ hủy chặn ứng dụng khách này", "time_table_header": "Thời gian", "date": "Ngày", "domain_name_table_header": "Tên miền", @@ -234,19 +243,31 @@ "blocking_mode": "Chế độ chặn", "default": "Mặc định", "nxdomain": "NXDOMAIN", + "refused": "REFUSED", "null_ip": "Địa chỉ IP rỗng", "custom_ip": "IP tuỳ chỉnh", "blocking_ipv4": "Chặn IPv4", "blocking_ipv6": "Chặn IPv6", + "dnscrypt": "DNSCrypt", "dns_over_https": "DNS-over-HTTPS", "dns_over_tls": "DNS-over-TLS", + "dns_over_quic": "DNS-over-QUIC", + "client_id": "ID khách hàng", + "client_id_placeholder": "Nhập ID khách hàng", + "client_id_desc": "Các khách hàng khác nhau có thể được xác định bằng một ID khách hàng đặc biệt. Tại đây bạn có thể tìm hiểu thêm về cách xác định khách hàng.", + "download_mobileconfig_doh": "Tải xuống .mobileconfig cho DNS-over-HTTPS", + "download_mobileconfig_dot": "Tải xuống .mobileconfig cho DNS-over-TLS", + "download_mobileconfig": "Tải xuống tệp cấu hình", "plain_dns": "DNS thuần", "form_enter_rate_limit": "Nhập giới hạn yêu cầu", "rate_limit": "Giới hạn yêu cầu", "edns_enable": "Bật mạng con EDNS Client", "edns_cs_desc": "Nếu được bật, AdGuard Home sẽ gửi các mạng con của khách hàng đến các máy chủ DNS.", + "rate_limit_desc": "Số lượng yêu cầu mỗi giây mà một khách hàng được phép thực hiện (0: không giới hạn)", "blocking_ipv4_desc": "Địa chỉ IP được trả lại cho một yêu cầu A bị chặn", "blocking_ipv6_desc": "Địa chỉ IP được trả lại cho một yêu cầu AAA bị chặn", + "blocking_mode_default": "Mặc định: Trả lời với NXDOMAIN khi bị chặn bởi quy tắc kiểu Adblock; phản hồi với địa chỉ IP được chỉ định trong quy tắc khi bị chặn bởi quy tắc / etc / hosts-style", + "blocking_mode_refused": "REFUSED: Trả lời bằng mã REFUSED", "blocking_mode_nxdomain": "NXDOMAIN: Phản hổi với mã NXDOMAIN", "blocking_mode_null_ip": "Null IP: Trả lời bằng không địa chỉ IP (0.0.0.0 cho A; :: cho AAAA)", "blocking_mode_custom_ip": "IP tùy chỉnh: Phản hồi với địa chỉ IP đã được tiết lập", @@ -255,7 +276,6 @@ "source_label": "Nguồn", "found_in_known_domain_db": "Tìm thấy trong cơ sở dữ liệu tên miền", "category_label": "Thể loại", - "rule_label": "Quy tắc", "list_label": "Danh sách", "unknown_filter": "Bộ lọc không rõ {{filterId}}", "known_tracker": "Theo dõi đã biết", @@ -316,13 +336,14 @@ "encryption_config_saved": "Đã lưu cấu hình mã hóa", "encryption_server": "Tên máy chủ", "encryption_server_enter": "Nhập tên miền của bạn", - "encryption_server_desc": "Để sử dụng HTTPS, bạn cần nhập tên máy chủ phù hợp với chứng chỉ SSL của bạn.", "encryption_redirect": "Tự động chuyển hướng đến HTTPS", "encryption_redirect_desc": "Nếu được chọn, AdGuard Home sẽ tự động chuyển hướng bạn từ địa chỉ HTTP sang địa chỉ HTTPS.", "encryption_https": "Cổng HTTPS", "encryption_https_desc": "Nếu cổng HTTPS được định cấu hình, giao diện quản trị viên AdGuard Home sẽ có thể truy cập thông qua HTTPS và nó cũng sẽ cung cấp DNS-over-HTTPS trên vị trí '/dns-query'.", "encryption_dot": "Cổng DNS-over-TLS", "encryption_dot_desc": "Nếu cổng này được định cấu hình, AdGuard Home sẽ chạy máy chủ DNS-over-TLS trên cổng này.", + "encryption_doq": "Cổng DNS-over-QUIC", + "encryption_doq_desc": "Nếu cổng này được định cấu hình, AdGuard Home sẽ chạy máy chủ DNS qua QUIC trên cổng này. Đó là thử nghiệm và có thể không đáng tin cậy. Ngoài ra, không có quá nhiều khách hàng hỗ trợ nó vào lúc này.", "encryption_certificates": "Chứng chỉ", "encryption_certificates_desc": "Để sử dụng mã hóa, bạn cần cung cấp chuỗi chứng chỉ SSL hợp lệ cho miền của mình. Bạn có thể nhận chứng chỉ miễn phí trên <0>{{link}} hoặc bạn có thể mua chứng chỉ từ một trong các Cơ Quan Chứng Nhận tin cậy.", "encryption_certificates_input": "Sao chép/dán chứng chỉ được mã hóa PEM của bạn tại đây.", @@ -370,7 +391,6 @@ "client_edit": "Chỉnh Sửa Máy Khách", "client_identifier": "Định danh", "ip_address": "Địa chỉ IP", - "client_identifier_desc": "Các máy khách có thể được xác định bằng địa chỉ IP hoặc địa chỉ MAC. Xin lưu ý rằng chỉ có thể sử dụng MAC làm định danh nếu AdGuard Home cũng là <0>máy chủ DHCP", "form_enter_ip": "Nhập IP", "form_enter_mac": "Nhập MAC", "form_enter_id": "Nhập định danh", @@ -401,6 +421,8 @@ "dns_privacy": "DNS Riêng Tư", "setup_dns_privacy_1": "<0>DNS-over-TLS: Sử dụng chuỗi <1>{{address}}.", "setup_dns_privacy_2": "<0>DNS-over-HTTPS: Sử dụng chuỗi <1>{{address}}.", + "setup_dns_privacy_3": "<0>Đây là danh sách phần mềm bạn có thể sử dụng.", + "setup_dns_privacy_4": "Trên thiết bị chạy iOS 14 hoặc macOS Big Sur bạn có thể tải tệp '.mobileconfig' đặc biệt có chứa máy chủ DNS-over-HTTPS hoặc DNS-over-TLS trong thiết lập DNS.", "setup_dns_privacy_android_1": "Android 9 hỗ trợ DNS trên TLS nguyên bản. Để định cấu hình, hãy đi tới Cài đặt → Mạng & internet → Nâng cao → DNS Riêng Tư và nhập tên miền của bạn vào đó.", "setup_dns_privacy_android_2": "<0>AdGuard for Android hỗ trợ <1>DNS-over-HTTPS và <1>DNS-over-TLS.", "setup_dns_privacy_android_3": "<0>Intra thêm <1>DNS-over-HTTPS hỗ trợ cho Android.", @@ -412,6 +434,7 @@ "setup_dns_privacy_other_3": "<0>dnscrypt-proxy hỗ trợ <1>DNS-over-HTTPS.", "setup_dns_privacy_other_4": "<0>Mozilla Firefox hỗ trợ <1>DNS-over-HTTPS.", "setup_dns_privacy_other_5": "Bạn sẽ tìm thấy nhiều triển khai hơn <0>tại đây và <1>tại đây.", + "setup_dns_privacy_ioc_mac": "Cấu hình iOS và macOS", "setup_dns_notice": "Để sử dụng <1>DNS-over-HTTPS hoặc <1>DNS-over-TLS, bạn cần <0>định cấu hình Mã hóa trong cài đặt AdGuard Home.", "rewrite_added": "DNS viết lại cho \"{{key}}\" đã thêm thành công", "rewrite_deleted": "DNS viết lại cho \"{{key}}\" đã xóa thành công", @@ -511,8 +534,8 @@ "check_ip": "Địa chỉ IP: {{ip}}", "check_cname": "CNAME: {{cname}}", "check_reason": "Lý do: {{reason}}", - "check_rule": "Quy tắc: {{rule}}", "check_service": "Tên dịch vụ: {{service}}", + "service_name": "Tên dịch vụ", "check_not_found": "Không tìm thấy trong danh sách bộ lọc của bạn", "client_confirm_block": "Bạn có muốn chặn người dùng {{ip}}?", "client_confirm_unblock": "Bạn có muốn bỏ chặn người dùng {{ip}}?", @@ -545,6 +568,14 @@ "milliseconds_abbreviation": "ms", "cache_size": "Kích thước cache", "cache_size_desc": "Kích thước cache DNS (bytes)", + "cache_ttl_min_override": "Ghi đè TTL tối thiểu", + "cache_ttl_max_override": "Ghi đè TTL tối đa", + "enter_cache_size": "Nhập kích thước bộ nhớ cache (byte)", + "enter_cache_ttl_min_override": "Nhập TTL tối thiểu (giây)", + "enter_cache_ttl_max_override": "Nhập TTL tối đa (giây)", + "cache_ttl_min_override_desc": "Mở rộng giá trị thời gian tồn tại ngắn (giây) nhận được từ máy chủ ngược dòng khi phản hồi DNS vào bộ nhớ đệm", + "cache_ttl_max_override_desc": "Đặt giá trị thời gian tồn tại tối đa (giây) cho các mục nhập trong bộ nhớ cache DNS", + "ttl_cache_validation": "Giá trị TTL trong bộ nhớ cache tối thiểu phải nhỏ hơn hoặc bằng giá trị lớn nhất", "filter_category_general": "Chung", "filter_category_security": "Bảo mật", "filter_category_regional": "Khu vực", @@ -556,5 +587,8 @@ "setup_config_to_enable_dhcp_server": "Thiết lập cấu hình để bật máy chủ DHCP", "original_response": "Phản hồi gốc", "click_to_view_queries": "Nhấp để xem truy xuất", - "port_53_faq_link": "Cổng 53 thường được sử dụng \"DNSStubListener\" hoặc \"systemd-resolved\". Vui lòng đọc <0>hướng dẫn để giải quyết vấn đề này." + "port_53_faq_link": "Cổng 53 thường được sử dụng \"DNSStubListener\" hoặc \"systemd-resolved\". Vui lòng đọc <0>hướng dẫn để giải quyết vấn đề này.", + "adg_will_drop_dns_queries": "AdGuard Home sẽ loại bỏ tất cả các truy vấn DNS từ ứng dụng khách này.", + "client_not_in_allowed_clients": "Ứng dụng khách không được phép vì nó không có trong danh sách \"Ứng dụng khách được phép\".", + "experimental": "Thử nghiệm" } \ No newline at end of file diff --git a/client/src/__locales/zh-cn.json b/client/src/__locales/zh-cn.json index 37095a3c..edf26ede 100644 --- a/client/src/__locales/zh-cn.json +++ b/client/src/__locales/zh-cn.json @@ -1,6 +1,6 @@ { "client_settings": "客户端设置", - "example_upstream_reserved": "您可以将上游DNS 服务器<0>指定为特定域名", + "example_upstream_reserved": "您可以<0>为特定域名指定上游 DNS 服务器", "example_upstream_comment": "您可以指定注解", "upstream_parallel": "通过同时查询所有上游服务器,使用并行请求以加速解析", "parallel_requests": "并行请求", @@ -32,6 +32,7 @@ "form_error_ip_format": "无效的 IPv4 格式", "form_error_mac_format": "无效的 MAC 格式", "form_error_client_id_format": "无效的客户端 ID 格式", + "form_error_server_name": "无效的服务器名", "form_error_positive": "必须大于 0", "form_error_negative": "必须大于等于 0", "range_end_error": "必须大于范围起始值", @@ -247,10 +248,16 @@ "custom_ip": "自定义 IP", "blocking_ipv4": "拦截 IPv4", "blocking_ipv6": "拦截 IPv6", + "dnscrypt": "DNSCrypt", "dns_over_https": "DNS-over-HTTPS", "dns_over_tls": "DNS-over-TLS", + "dns_over_quic": "DNS-over-QUIC", + "client_id": "客户端 ID", + "client_id_placeholder": "输入客户端 ID", + "client_id_desc": "可根据一个特殊的客户端 ID 识别不同客户端。在 这里你可以了解到更多关于如何识别客户端的信息。", "download_mobileconfig_doh": "下载适用于 DNS-over-HTTPS 的 .mobileconfig", "download_mobileconfig_dot": "下载适用于 DNS-over-TLS 的 .mobileconfig", + "download_mobileconfig": "下载配置文件", "plain_dns": "无加密DNS", "form_enter_rate_limit": "输入限制速率", "rate_limit": "速度限制", @@ -330,7 +337,7 @@ "encryption_config_saved": "加密配置已保存", "encryption_server": "服务器名称", "encryption_server_enter": "输入您的域名", - "encryption_server_desc": "若要使用 HTTPS ,您需要输入与 SSL 证书相匹配的服务器名称。", + "encryption_server_desc": "为了使用 HTTPS,请您输入与 SSL 证书或通配证书相匹配的服务器名称。如此字段未设置,服务器将要为所有域名接受 TLS 连接。", "encryption_redirect": "HTTPS 自动重定向", "encryption_redirect_desc": "如果勾选此选项,AdGuard Home 将自动将您从 HTTP 重定向到 HTTPS 地址。", "encryption_https": "HTTPS 端口", @@ -386,7 +393,7 @@ "client_edit": "编辑客户端", "client_identifier": "标识符", "ip_address": "IP 地址", - "client_identifier_desc": "客户端可通过 IP 地址或 MAC 地址识别。请注意,如 AdGuard Home 也是 <0>DHCP 服务器,则仅能将 MAC 用作标识符", + "client_identifier_desc": "客户端可通过 IP 、MAC 地址、CIDR 或特殊 ID(可用于 DoT/DoH/DoQ)被识别。<0>这里您可多了解如何识别客户端。", "form_enter_ip": "输入 IP", "form_enter_mac": "输入 MAC", "form_enter_id": "输入标识符", @@ -430,6 +437,7 @@ "setup_dns_privacy_other_3": "<0>dnscrypt-proxy 支持 <1>DNS-over-HTTPS。", "setup_dns_privacy_other_4": "<0>Mozilla Firefox 支持 <1>DNS-over-HTTPS。", "setup_dns_privacy_other_5": "您可以从 <0>这里 和 <1>这里 找到更多的实施方案。", + "setup_dns_privacy_ioc_mac": "iOS 和 macOS 配置", "setup_dns_notice": "为了使用 <1>DNS-over-HTTPS 或者 <1>DNS-over-TLS ,您需要在 AdGuard Home 设置中 <0>配置加密 。", "rewrite_added": "已成功添加 \"{{key}}\" 的 DNS 重写", "rewrite_deleted": "已成功删除 \"{{key}}\" 的 DNS 重写", @@ -529,7 +537,6 @@ "check_ip": "IP地址:{{ip}}", "check_cname": "CNAME: {{cname}}", "check_reason": "原因:{{reason}}", - "check_rule": "规则:{{rule}}", "check_service": "服务名称:{{service}}", "service_name": "服务名称", "check_not_found": "未在您的筛选列表中找到", diff --git a/client/src/__locales/zh-hk.json b/client/src/__locales/zh-hk.json index 4247df2a..2b52d4ed 100644 --- a/client/src/__locales/zh-hk.json +++ b/client/src/__locales/zh-hk.json @@ -2,12 +2,12 @@ "client_settings": "用戶端設定", "example_upstream_reserved": "您可以<0>指定網域使用特定 DNS 查詢", "example_upstream_comment": "您可以指定註解", - "upstream_parallel": "使用平行查詢,同時查詢所有上游伺服器來來加速解析結果", + "upstream_parallel": "使用平行查詢,同時查詢所有上游伺服器來加速解析結果", "parallel_requests": "平行處理", - "load_balancing": "負載平衝", + "load_balancing": "負載平衡", "load_balancing_desc": "一次只查詢一個伺服器。AdGuard Home 會使用加權隨機取樣來選擇使用的查詢結果,以確保速度最快的伺服器能被充分運用。", "bootstrap_dns": "引導(Boostrap) DNS 伺服器", - "bootstrap_dns_desc": "引導(Bootstrap)DNS 伺服器用來解析 DoH/DoT 的域名 IP。", + "bootstrap_dns_desc": "Bootstrap DNS 伺服器用於解析您所設定的上游 DoH/DoT 解析器的 IP 地址", "check_dhcp_servers": "檢查 DHCP 伺服器", "save_config": "儲存設定", "enabled_dhcp": "DHCP 伺服器已啟動", @@ -117,9 +117,9 @@ "block_domain_use_filters_and_hosts": "使用過濾器與 hosts 檔案阻擋網域查詢", "filters_block_toggle_hint": "您可在過濾器設定中設定封鎖規則。", "use_adguard_browsing_sec": "使用 AdGuard 瀏覽安全網路服務", - "use_adguard_browsing_sec_hint": "AdGuard Home 將檢查查詢網域是否在瀏覽安全服務黑名單內。它使用尊重個人隱私的 API 來進行檢查:會先透過 SHA256 將網域編碼後取簡短前置字串傳送到伺服器核對。", + "use_adguard_browsing_sec_hint": "AdGuard Home 將比對查詢網域是否在瀏覽安全服務黑名單內。AdGuard Home 選擇使用尊重個人隱私的 API 進行比對,先透過 SHA256 將網域編碼,取前置字串傳送到伺服器進行比對。", "use_adguard_parental": "使用 AdGuard 家長監護功能", - "use_adguard_parental_hint": "AdGuard Home 將檢查查詢網域是否含有成人內容。它使用與 AdGuard 瀏覽安全一樣的尊重個人隱私的 API 來進行檢查。", + "use_adguard_parental_hint": "AdGuard Home 將比對查詢網域是否含有成人內容。它使用與 AdGuard 瀏覽安全一樣的尊重個人隱私的 API 來進行檢查。", "enforce_safe_search": "強制使用安全搜尋", "enforce_save_search_hint": "AdGuard Home 可在下列搜尋引擎使用強制安全搜尋:Google、YouTube、Bing、DuckDuckGo 和 Yandex。", "no_servers_specified": "沒有指定的伺服器", @@ -133,8 +133,8 @@ "encryption_settings": "加密設定", "dhcp_settings": "DHCP 設定", "upstream_dns": "上游 DNS 伺服器", - "upstream_dns_help": "每行輸入一個伺服器位址。了解更多有關配置上遊 DNS 伺服器的內容", - "upstream_dns_configured_in_file": "被配置在 {{path}}", + "upstream_dns_help": "每行輸入一個伺服器位址。了解更多有關設定上游 DNS 伺服器的內容", + "upstream_dns_configured_in_file": "設定在 {{path}}", "test_upstream_btn": "測試上游 DNS", "upstreams": "上游", "apply_btn": "套用", @@ -188,19 +188,19 @@ "example_upstream_regular": "一般 DNS(透過 UDP)", "example_upstream_dot": "<0>DNS-over-TLS(流量加密)", "example_upstream_doh": "<0>DNS-over-HTTPS(流量加密)", - "example_upstream_doq": "加密的<0>DNS-over-QUIC", + "example_upstream_doq": "加密 <0>DNS-over-QUIC", "example_upstream_sdns": "您可以使透過 <0>DNS Stamps 來解析 <1>DNSCrypt 或 <2>DNS-over-HTTPS", "example_upstream_tcp": "一般 DNS(透過 TCP)", - "all_lists_up_to_date_toast": "所有清單已經是最新的", + "all_lists_up_to_date_toast": "所有清單已更新至最新", "updated_upstream_dns_toast": "已更新上游 DNS 伺服器", "dns_test_ok_toast": "設定中的 DNS 上游運作正常", - "dns_test_not_ok_toast": "設定中的 \"{{key}}\" DNS 出現錯誤,請檢察拼字", + "dns_test_not_ok_toast": "DNS 設定中的 \"{{key}}\" 出現錯誤,請確認是否正確輸入", "unblock": "解除封鎖", "block": "封鎖", "disallow_this_client": "不允許此用戶端", "allow_this_client": "允許此用戶端", - "block_for_this_client_only": "僅為此用戶端封鎖", - "unblock_for_this_client_only": "僅為此用戶端解除封鎖", + "block_for_this_client_only": "僅封鎖此用戶端", + "unblock_for_this_client_only": "僅解除封鎖此用戶端", "time_table_header": "時間", "date": "日期", "domain_name_table_header": "域名", @@ -254,7 +254,7 @@ "plain_dns": "一般未加密 DNS", "form_enter_rate_limit": "輸入速率限制", "rate_limit": "速率限制", - "edns_enable": "編輯 EDNS 用戶端子網路", + "edns_enable": "啟用 EDNS Client Subnet", "edns_cs_desc": "開啟後 AdGuard Home 將會傳送用戶端的子網路給 DNS 伺服器。", "rate_limit_desc": "限制單一裝置每秒發出的查詢次數(設定為 0 即表示無限制)", "blocking_ipv4_desc": "回覆指定 IPv4 位址給被封鎖的網域的 A 紀錄查詢", @@ -269,7 +269,6 @@ "source_label": "來源", "found_in_known_domain_db": "在已知網域資料庫中找到。", "category_label": "類別", - "rule_label": "規則", "list_label": "清單", "unknown_filter": "未知過濾器 {{filterId}}", "known_tracker": "已知追蹤器", @@ -330,15 +329,14 @@ "encryption_config_saved": "加密設定已儲存", "encryption_server": "伺服器名稱", "encryption_server_enter": "輸入您的網域名稱", - "encryption_server_desc": "要使用 HTTPS,您必須輸入與您 SSL 憑證相符的伺服器名稱。", - "encryption_redirect": "自重新導向到 HTTPS", + "encryption_redirect": "自動重新導向到 HTTPS", "encryption_redirect_desc": "如果啟用,AdGuard Home 將會自動導向 HTTP 到 HTTPS。", "encryption_https": "HTTPS 連接埠", "encryption_https_desc": "如果已設定 HTTPS,AdGuard Home 網頁管理介面將會使用 HTTPS 來存取,且「/dns-query」也提供 DNS-over-HTTPS 查詢。", "encryption_dot": "DNS-over-TLS 連接埠", "encryption_dot_desc": "如果已設定此連接埠,AdGuard Home 將啟動 DNS-over-TLS 伺服器來監聽請求。", - "encryption_doq": "DNS-over-QUIC端口", - "encryption_doq_desc": "如果此端口被配置了, AdGuard Home將會在此端口運行DNS-over-QUIC服務. 這目前還是實驗性的功能,可能不可靠. 另外,目前還沒有大量支持它的客戶端", + "encryption_doq": "DNS-over-QUIC 連接埠", + "encryption_doq_desc": "若設定此連接埠,AdGuard Home 將在此連接埠上運行 DNS-over-QUIC 服務。此功能還是實驗性功能,可能並不可靠。此外目前還沒有太多客戶端支援。", "encryption_certificates": "憑證", "encryption_certificates_desc": "要使用加密連線,必須擁有一個有效的 SSL 憑證對應您的網域。您可以從<0>{{link}}取得免費的 SSL 憑證或從受信任的 SSL 憑證簽發機構購買。", "encryption_certificates_input": "在這裡複製/貼上您的 PEM 憑證。", @@ -370,7 +368,7 @@ "dns_status_error": "檢查 DNS 伺服器狀態錯誤", "down": "離線", "fix": "修正", - "dns_providers": "下列是可以使用的<0>軟體清單。", + "dns_providers": "下列為常見的<0> DNS 伺服器。", "update_now": "立即更新", "update_failed": "自動更新發生錯誤。請嘗試依照以下步驟 來手動更新。", "processing_update": "請稍候,AdGuard Home 正在更新", @@ -386,14 +384,13 @@ "client_edit": "編輯用戶端", "client_identifier": "識別碼", "ip_address": "IP 位址", - "client_identifier_desc": "可通過 IP 地址、CIDR、MAC 地址來辨識使用者裝置。注意:必須使用 AdGuard Home 內建 <0>DHCP 伺服器 才能偵測 MAC 地址。", "form_enter_ip": "輸入 IP", "form_enter_mac": "輸入 MAC 地址", "form_enter_id": "輸入識別碼", "form_add_id": "新增識別碼", "form_client_name": "輸入用戶端名稱", "name": "名稱", - "client_global_settings": "Use global settings", + "client_global_settings": "使用全域設定", "client_deleted": "已刪除「{{key}}」", "client_added": "已新增「{{key}}」", "client_updated": "已更新「{{key}}」", @@ -418,13 +415,13 @@ "setup_dns_privacy_1": "<0>DNS-over-TLS:使用 <1>{{address}}。", "setup_dns_privacy_2": "<0>DNS-over-HTTPS:使用 <1>{{address}}。", "setup_dns_privacy_3": "<0>以下是您可以使用軟體的列表", - "setup_dns_privacy_4": "在 iOS 14 或 macOS Big Sur 裝置上,您可以下載特定的 '.mobileconfig' 檔案。此檔案將DNS-over-HTTPSDNS-over-TLS 伺服器添加至 DNS 設定。", - "setup_dns_privacy_android_1": "Android 9 原生支援 DNS-over-TLS。前網「設定」→「網路 & 網際網路」→「進階」→「私人 DNS」設定。", + "setup_dns_privacy_4": "在 iOS 14 或 macOS Big Sur 裝置上,您可以下載特定的 '.mobileconfig' 檔案。此檔案將DNS-over-HTTPSDNS-over-TLS 伺服器新增至 DNS 設定。", + "setup_dns_privacy_android_1": "Android 9 原生支援 DNS-over-TLS。前往「設定」→「網路 & 網際網路」→「進階」→「私人 DNS」設定。", "setup_dns_privacy_android_2": "<0>AdGuard for Android 支援 <1>DNS-over-HTTPS 與 <1>DNS-over-TLS。", "setup_dns_privacy_android_3": "<0>Intra 對 Android 新增支援 <1>DNS-over-HTTPS。", "setup_dns_privacy_ios_1": "<0>DNSCloak 支援 <1>DNS-over-HTTPS,若要使用您必須先產生 <2>DNS Stamp。", "setup_dns_privacy_ios_2": "<0>AdGuard for iOS 支援 <1>DNS-over-HTTPS 與 <1>DNS-over-TLS 設定。", - "setup_dns_privacy_other_title": "其他實作軟體", + "setup_dns_privacy_other_title": "其他實作方式", "setup_dns_privacy_other_1": "AdGuard Home 本身在任何平台都是安全的 DNS 用戶端。", "setup_dns_privacy_other_2": "<0>dnsproxy 支援所有加密 DNS 協定。", "setup_dns_privacy_other_3": "<0>dnscrypt-proxy 支援 <1>DNS-over-HTTPS。", @@ -440,7 +437,7 @@ "rewrite_applied": "已套用 DNS 覆寫規則", "rewrite_hosts_applied": "由「hosts 檔案」覆寫", "dns_rewrites": "DNS 覆寫", - "form_domain": "輸入網域名稱或使用萬用字元。", + "form_domain": "輸入網域名稱或使用 wildcard 字元。", "form_answer": "輸入 IP 或網域名稱", "form_error_domain_format": "網域格式無效", "form_error_answer_format": "回應格式無效", @@ -497,11 +494,11 @@ "network": "網路", "descr": "描述", "whois": "Whois", - "filtering_rules_learn_more": "<0>進一步了解關於創建自己的「hosts 檔案」", + "filtering_rules_learn_more": "<0>進一步了解如何創建自己的「hosts 檔案」", "blocked_by_response": "回應時被 CNAME 或 IP 封鎖", "blocked_by_cname_or_ip": "使用 CNAME 或 IP 封鎖", "try_again": "再試一次", - "domain_desc": "輸入您想要覆寫的網域或萬用字元。", + "domain_desc": "輸入您想要覆寫的網域或 wildcard 字元。", "example_rewrite_domain": "DNS 覆寫只套用在這個域名。", "example_rewrite_wildcard": "DNS 覆寫會套用在 <0>example.org 及所有子域名。", "rewrite_ip_address": "IP 位址:使用 A 或 AAAA 紀錄回應", @@ -509,27 +506,26 @@ "rewrite_A": "<0>A: 特殊值,將上游查詢結果覆寫 <0>A 紀錄", "rewrite_AAAA": "<0>AAAA: 特殊值,將上游查詢結果覆寫 <0>AAAA 紀錄", "disable_ipv6": "停用 IPv6", - "disable_ipv6_desc": "開啟此功能後所有,所有對於 IPv6 位址(AAAA)的查詢都會被捨棄。", + "disable_ipv6_desc": "開啟此功能後,將捨棄所有對 IPv6 位址(AAAA)的查詢。", "fastest_addr": "Fastest IP 位址", - "fastest_addr_desc": "從所有 DNS 伺服器查詢中回應最快的 IP 位址", + "fastest_addr_desc": "從所有 DNS 伺服器查詢中回應最快的 IP 位址。但這操作會等待所有 DNS 查詢結果後才能回應,導致速度有所降低,不過同時卻也改善了整體連線品質。", "autofix_warning_text": "如果您點擊「修復」,AdGuard Home 將更改您的系統 DNS 設定更改為 AdGuard Home DNS 伺服器", "autofix_warning_list": "它將執行這些任務:<0>停用系統 DNSStubListener <0>將 DNS 設定為 127.0.0.1 <0>更換軟連結將 /etc/resolv.conf 為 /run/systemd/resolve/resolv.conf <0>停止 DNSStubListener(重新載入 systemd-resolved)", "autofix_warning_result": "就結論來說 DNS 請求預設由本機的 AdGuard Home 處理。", "tags_title": "標籤", - "tags_desc": "您可以選擇與用戶端相對應的標籤。標籤可已包含在過濾規則內且使用上更精確。\n<0>進一步了解", + "tags_desc": "可在此指定用戶端的標籤。標籤可包含在過濾規則內,並且在指定上更為精確。\n<0>進一步了解", "form_select_tags": "選擇用戶端標籤", "check_title": "過濾檢查", "check_desc": "檢查網域是否被封鎖", "check": "檢查", "form_enter_host": "輸入網域", - "filtered_custom_rules": "被自訂過濾規則封鎖", + "filtered_custom_rules": "已套用自訂規則", "choose_from_list": "從清單中選取", "add_custom_list": "新增自訂清單", "host_whitelisted": "主機已列入白名單", "check_ip": "IP 位址:{{ip}}", "check_cname": "CNAME:{{cname}}", "check_reason": "原因:{{reason}}", - "check_rule": "規則:{{rule}}", "check_service": "服務名稱:{{service}}", "service_name": "服務名稱", "check_not_found": "未在您的過濾清單中找到", @@ -569,7 +565,7 @@ "enter_cache_size": "輸入快取大小(bytes)", "enter_cache_ttl_min_override": "輸入最小 TTL 值(秒)", "enter_cache_ttl_max_override": "輸入最大 TTL 值(秒)", - "cache_ttl_min_override_desc": "快取 DNS 回應時,延長從上遊伺服器收到的 TTL 值 (秒)", + "cache_ttl_min_override_desc": "快取 DNS 回應時,延長從上游伺服器收到的 TTL 值 (秒)", "cache_ttl_max_override_desc": "設定 DNS 快取條目的最大 TTL 值(秒)", "ttl_cache_validation": "最小快取 TTL 值必須小於或等於最大值", "filter_category_general": "一般", @@ -584,7 +580,7 @@ "original_response": "原始回應", "click_to_view_queries": "按一下以檢視查詢結果", "port_53_faq_link": "連接埠 53 經常被「DNSStubListener」或「systemd-resolved」服務佔用。請閱讀下列有關解決<0>這個問題的說明", - "adg_will_drop_dns_queries": "AdGuard Home 將要終止所有來自此用戶端的 DNS 查詢。", + "adg_will_drop_dns_queries": "AdGuard Home 將停止回應此用戶端的所有 DNS 查詢。", "client_not_in_allowed_clients": "此用戶端不被允許,它不在\"允許的用戶端\"列表中。", "experimental": "實驗性" } \ No newline at end of file diff --git a/client/src/__locales/zh-tw.json b/client/src/__locales/zh-tw.json index 8d0f198f..84df5c3e 100644 --- a/client/src/__locales/zh-tw.json +++ b/client/src/__locales/zh-tw.json @@ -1,6 +1,6 @@ { "client_settings": "用戶端設定", - "example_upstream_reserved": "您可明確指定<0>用於特定的網域之 DNS 上游", + "example_upstream_reserved": "您可<0>對於特定的網域明確指定 DNS 上游", "example_upstream_comment": "您可明確指定註解", "upstream_parallel": "透過同時地查詢所有上游的伺服器,使用並行的查詢以加速解析網域", "parallel_requests": "並行的請求", @@ -32,6 +32,7 @@ "form_error_ip_format": "無效的 IP 格式", "form_error_mac_format": "無效的媒體存取控制(MAC)格式", "form_error_client_id_format": "無效的用戶端 ID 格式", + "form_error_server_name": "無效的伺服器名稱", "form_error_positive": "必須大於 0", "form_error_negative": "必須等於或大於 0", "range_end_error": "必須大於起始範圍", @@ -74,7 +75,7 @@ "filter": "過濾器", "query_log": "查詢記錄", "compact": "精簡的", - "nothing_found": "無什麼被找到", + "nothing_found": "沒找到什麼", "faq": "常見問答集", "version": "版本", "address": "位址", @@ -247,10 +248,16 @@ "custom_ip": "自訂的 IP", "blocking_ipv4": "封鎖 IPv4", "blocking_ipv6": "封鎖 IPv6", + "dnscrypt": "DNSCrypt", "dns_over_https": "DNS-over-HTTPS", "dns_over_tls": "DNS-over-TLS", + "dns_over_quic": "DNS-over-QUIC", + "client_id": "用戶端 ID", + "client_id_placeholder": "輸入用戶端 ID", + "client_id_desc": "不同的用戶端可根據特殊的用戶端 ID 被識別。<0>於此,您可了解更多關於如何識別用戶端。", "download_mobileconfig_doh": "下載用於 DNS-over-HTTPS 的 .mobileconfig", "download_mobileconfig_dot": "下載用於 DNS-over-TLS 的 .mobileconfig", + "download_mobileconfig": "下載配置檔案", "plain_dns": "一般的 DNS", "form_enter_rate_limit": "輸入速率限制", "rate_limit": "速率限制", @@ -259,7 +266,7 @@ "rate_limit_desc": "單一的用戶端被允許傳送的每秒請求之數量(設定它為 0 表示無限制的)", "blocking_ipv4_desc": "要被返回給已封鎖的 A 請求之 IP 位址", "blocking_ipv6_desc": "要被返回給已封鎖的 AAAA 請求之 IP 位址", - "blocking_mode_default": "預設:當被廣告封鎖樣式的規則封鎖時,以零值 IP 位址(0.0.0.0 供 A;:: 供 AAAA)回覆;當被 /etc/hosts 樣式的規則封鎖時,以在該規則中之已明確指定的 IP 位址回覆", + "blocking_mode_default": "預設:當被 AdBlock 樣式的規則封鎖時,以零值 IP 位址(0.0.0.0 供 A;:: 供 AAAA)回覆;當被 /etc/hosts 樣式的規則封鎖時,以在該規則中之已明確指定的 IP 位址回覆", "blocking_mode_refused": "已拒絕(REFUSED):以 REFUSED 碼回覆", "blocking_mode_nxdomain": "不存在的網域(NXDOMAIN):以 NXDOMAIN 碼回覆", "blocking_mode_null_ip": "無效的 IP:以零值 IP 位址(0.0.0.0 供 A;:: 供 AAAA)回覆", @@ -284,7 +291,7 @@ "install_settings_dns_desc": "您將需要配置您的裝置或路由器以使用於下列的位址上之 DNS 伺服器:", "install_settings_all_interfaces": "所有的介面", "install_auth_title": "驗證", - "install_auth_desc": "配置屬於您的 AdGuard Home 管理員網路介面之密碼驗證是被非常建議的。即使它僅在您的區域網路中為可存取的,保護它免於不受限制的存取為仍然重要的。", + "install_auth_desc": "我們強烈建議您設定 AdGuard Home 管理員網路介面密碼。即使它僅能在您的區域網路中使用,保護它免於不受限制的存取仍然很重要。", "install_auth_username": "使用者名稱", "install_auth_password": "密碼", "install_auth_confirm": "確認密碼", @@ -330,7 +337,7 @@ "encryption_config_saved": "加密配置被儲存", "encryption_server": "伺服器名稱", "encryption_server_enter": "輸入您的域名", - "encryption_server_desc": "為了使用 HTTPS,您需要輸入與您的安全通訊端層(SSL)憑證相符的伺服器名稱。", + "encryption_server_desc": "為了使用 HTTPS,您需要輸入與您的安全通訊端層(SSL)憑證或萬用字元憑證相符的伺服器名稱。如果此欄位未被設定,它將接受向任何網域的傳輸層安全性協定(TLS)連線。", "encryption_redirect": "自動地重新導向到 HTTPS", "encryption_redirect_desc": "如果被勾選,AdGuard Home 將自動地重新導向您從 HTTP 到 HTTPS 位址。", "encryption_https": "HTTPS 連接埠", @@ -386,7 +393,7 @@ "client_edit": "編輯用戶端", "client_identifier": "識別碼", "ip_address": "IP 位址", - "client_identifier_desc": "用戶端可被 IP 位址、無類別網域間路由(CIDR)或媒體存取控制(MAC)位址識別。請注意,只要 AdGuard Home 也是<0>動態主機設定協定(DHCP)伺服器,使用 MAC 作為識別碼是可能的", + "client_identifier_desc": "用戶端可根據 IP 位址、無類別網域間路由(CIDR)、媒體存取控制(MAC)位址或特殊的用戶端 ID(可被用於 DoT/DoH/DoQ)被識別。<0>於此,您可了解更多關於如何識別用戶端。", "form_enter_ip": "輸入 IP", "form_enter_mac": "輸入媒體存取控制(MAC)", "form_enter_id": "輸入識別碼", @@ -430,6 +437,7 @@ "setup_dns_privacy_other_3": "<0>dnscrypt-proxy 支援 <1>DNS-over-HTTPS。", "setup_dns_privacy_other_4": "<0>Mozilla Firefox 支援 <1>DNS-over-HTTPS。", "setup_dns_privacy_other_5": "在<0>這裡和<1>這裡,您將發現更多的執行。", + "setup_dns_privacy_ioc_mac": "iOS 和 macOS 配置", "setup_dns_notice": "為了使用 <1>DNS-over-HTTPS 或 <1>DNS-over-TLS,您需要在 AdGuard Home 設定裡<0>配置加密。", "rewrite_added": "對於 \"{{key}}\" 之 DNS 改寫被成功地加入", "rewrite_deleted": "對於 \"{{key}}\" 之 DNS 改寫被成功地刪除", @@ -529,7 +537,6 @@ "check_ip": "IP 位址:{{ip}}", "check_cname": "正規名稱(CNAME):{{cname}}", "check_reason": "原因:{{reason}}", - "check_rule": "規則:{{rule}}", "check_service": "服務名稱:{{service}}", "service_name": "服務名稱", "check_not_found": "未在您的過濾器中被找到", @@ -585,6 +592,6 @@ "click_to_view_queries": "點擊以檢視查詢", "port_53_faq_link": "連接埠 53 常被 \"DNSStubListener\" 或 \"systemd-resolved\" 服務佔用。請閱讀有關如何解決這個的<0>用法說明。", "adg_will_drop_dns_queries": "AdGuard Home 將持續排除來自此用戶端之所有的 DNS 查詢。", - "client_not_in_allowed_clients": "因為該用戶端不在\"已允許的用戶端\"清單中,它未被允許。", + "client_not_in_allowed_clients": "該用戶端未被允許,因為它不在\"已允許的用戶端\"清單中。", "experimental": "實驗性的" } \ No newline at end of file diff --git a/client/src/actions/index.js b/client/src/actions/index.js index 04054091..58e2d018 100644 --- a/client/src/actions/index.js +++ b/client/src/actions/index.js @@ -287,7 +287,7 @@ export const getDnsStatus = () => async (dispatch) => { try { checkStatus(handleRequestSuccess, handleRequestError); } catch (error) { - handleRequestError(error); + handleRequestError(); } }; diff --git a/client/src/actions/queryLogs.js b/client/src/actions/queryLogs.js index 1c653fb3..076a9008 100644 --- a/client/src/actions/queryLogs.js +++ b/client/src/actions/queryLogs.js @@ -8,11 +8,11 @@ import { import { addErrorToast, addSuccessToast } from './toasts'; const enrichWithClientInfo = async (logs) => { - const clientsParams = getParamsForClientsSearch(logs, 'client'); + const clientsParams = getParamsForClientsSearch(logs, 'client', 'client_id'); if (Object.keys(clientsParams).length > 0) { const clients = await apiClient.findClients(clientsParams); - return addClientInfo(logs, clients, 'client'); + return addClientInfo(logs, clients, 'client_id', 'client'); } return logs; diff --git a/client/src/components/App/index.css b/client/src/components/App/index.css index 4f18ee18..990f13e4 100644 --- a/client/src/components/App/index.css +++ b/client/src/components/App/index.css @@ -1,10 +1,11 @@ :root { --yellow-pale: rgba(247, 181, 0, 0.1); - --green79: #67B279; + --green79: #67b279; --gray-a5: #a5a5a5; --gray-d8: #d8d8d8; - --gray-f3: #F3F3F3; + --gray-f3: #f3f3f3; --font-family-monospace: Monaco, Menlo, "Ubuntu Mono", Consolas, source-code-pro, monospace; + --font-size-disable-autozoom: 1rem; } body { @@ -13,6 +14,13 @@ body { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Helvetica Neue", Arial, sans-serif; } +/* Disable Auto Zoom in Input - Safari on iPhone https://stackoverflow.com/a/6394497 */ +@media screen and (max-width: 767px) { + input, select, textarea { + font-size: var(--font-size-disable-autozoom); + } +} + .status { margin-top: 30px; } @@ -71,3 +79,11 @@ body { .button-action--active { visibility: visible; } + +.ReactModal__Body--open { + overflow: hidden; +} + +a.btn-success.disabled { + color: #fff; +} diff --git a/client/src/components/Dashboard/Clients.js b/client/src/components/Dashboard/Clients.js index ccac7baf..46edc46f 100644 --- a/client/src/components/Dashboard/Clients.js +++ b/client/src/components/Dashboard/Clients.js @@ -9,7 +9,7 @@ import Card from '../ui/Card'; import Cell from '../ui/Cell'; import { getPercent, sortIp } from '../../helpers/helpers'; -import { BLOCK_ACTIONS, STATUS_COLORS } from '../../helpers/constants'; +import { BLOCK_ACTIONS, R_CLIENT_ID, STATUS_COLORS } from '../../helpers/constants'; import { toggleClientBlock } from '../../actions/access'; import { renderFormattedClientCell } from '../../helpers/renderFormattedClientCell'; import { getStats } from '../../actions/stats'; @@ -35,6 +35,10 @@ const CountCell = (row) => { }; const renderBlockingButton = (ip, disallowed, disallowed_rule) => { + if (R_CLIENT_ID.test(ip)) { + return null; + } + const dispatch = useDispatch(); const { t } = useTranslation(); const processingSet = useSelector((state) => state.access.processingSet); @@ -59,17 +63,19 @@ const renderBlockingButton = (ip, disallowed, disallowed_rule) => { const text = disallowed ? BLOCK_ACTIONS.UNBLOCK : BLOCK_ACTIONS.BLOCK; const isNotInAllowedList = disallowed && disallowed_rule === ''; - return
- -
; + > + {text} + + + ); }; const ClientCell = (row) => { @@ -90,13 +96,14 @@ const Clients = ({ const { t } = useTranslation(); const topClients = useSelector((state) => state.stats.topClients, shallowEqual); - return - + ({ @@ -107,7 +114,7 @@ const Clients = ({ }))} columns={[ { - Header: 'IP', + Header: client_table_header, accessor: 'ip', sortMethod: sortIp, Cell: ClientCell, @@ -134,8 +141,9 @@ const Clients = ({ return disallowed ? { className: 'logs__row--red' } : {}; }} - /> - ; + /> + + ); }; Clients.propTypes = { diff --git a/client/src/components/Dashboard/Dashboard.css b/client/src/components/Dashboard/Dashboard.css index c299464b..9e733044 100644 --- a/client/src/components/Dashboard/Dashboard.css +++ b/client/src/components/Dashboard/Dashboard.css @@ -34,7 +34,7 @@ align-items: center; } -.dashboard-title__button{ +.dashboard-title__button { margin: 0 0.5rem; } @@ -44,7 +44,7 @@ align-items: flex-start; } - .dashboard-title__button{ + .dashboard-title__button { margin: 0.5rem 0; display: block; } diff --git a/client/src/components/Dashboard/index.js b/client/src/components/Dashboard/index.js index 4de5b8dd..d7e8874e 100644 --- a/client/src/components/Dashboard/index.js +++ b/client/src/components/Dashboard/index.js @@ -44,6 +44,7 @@ const Dashboard = ({ const refreshButton = ; }; -const getTitle = (reason) => { +const getTitle = () => { const { t } = useTranslation(); const filters = useSelector((state) => state.filtering.filters, shallowEqual); const whitelistFilters = useSelector((state) => state.filtering.whitelistFilters, shallowEqual); - const filter_id = useSelector((state) => state.filtering.check.filter_id); - - const filterName = getFilterName( - filters, - whitelistFilters, - filter_id, - 'filtered_custom_rules', - (filter) => (filter?.name ? t('query_log_filtered', { filter: filter.name }) : ''), - ); + const rules = useSelector((state) => state.filtering.check.rules, shallowEqual); + const reason = useSelector((state) => state.filtering.check.reason); const getReasonFiltered = (reason) => { const filterKey = reason.replace(FILTERED, ''); return i18next.t('query_log_filtered', { filter: filterKey }); }; + const ruleAndFilterNames = getRulesToFilterList(rules, filters, whitelistFilters); + const REASON_TO_TITLE_MAP = { [FILTERED_STATUS.NOT_FILTERED_NOT_FOUND]: t('check_not_found'), [FILTERED_STATUS.REWRITE]: t('rewrite_applied'), [FILTERED_STATUS.REWRITE_HOSTS]: t('rewrite_hosts_applied'), - [FILTERED_STATUS.FILTERED_BLACK_LIST]: filterName, - [FILTERED_STATUS.NOT_FILTERED_WHITE_LIST]: filterName, + [FILTERED_STATUS.FILTERED_BLACK_LIST]: ruleAndFilterNames, + [FILTERED_STATUS.NOT_FILTERED_WHITE_LIST]: ruleAndFilterNames, [FILTERED_STATUS.FILTERED_SAFE_SEARCH]: getReasonFiltered(reason), [FILTERED_STATUS.FILTERED_SAFE_BROWSING]: getReasonFiltered(reason), [FILTERED_STATUS.FILTERED_PARENTAL]: getReasonFiltered(reason), @@ -78,7 +73,11 @@ const getTitle = (reason) => { return <>
{t('check_reason', { reason })}
-
{filterName}
+
+ {t('rule_label')}: +   + {ruleAndFilterNames} +
; }; @@ -86,14 +85,13 @@ const Info = () => { const { hostname, reason, - rule, service_name, cname, ip_addrs, } = useSelector((state) => state.filtering.check, shallowEqual); const { t } = useTranslation(); - const title = getTitle(reason); + const title = getTitle(); const className = classNames('card mb-0 p-3', { 'logs__row--red': checkFiltered(reason), @@ -112,7 +110,6 @@ const Info = () => {
{title}
{!onlyFiltered && <> - {rule &&
{t('check_rule', { rule })}
} {service_name &&
{t('check_service', { service: service_name })}
} {cname &&
{t('check_cname', { cname })}
} {ip_addrs &&
{t('check_ip', { ip: ip_addrs.join(', ') })}
} diff --git a/client/src/components/Header/index.js b/client/src/components/Header/index.js index 071f950f..554ec603 100644 --- a/client/src/components/Header/index.js +++ b/client/src/components/Header/index.js @@ -46,7 +46,7 @@ const Header = () => {
- + AdGuard Home logo {!processing && isCoreRunning && autoClient.name === client); const source = autoClient?.source; const whoisAvailable = whois_info && Object.keys(whois_info).length > 0; + const clientName = name || client_id; + const clientInfo = { ...info, name: clientName }; const id = nanoid(); const data = { address: client, - name, + name: clientName, country: whois_info?.country, city: whois_info?.city, network: whois_info?.orgname, @@ -99,13 +102,20 @@ const ClientCell = ({ if (options.length === 0) { return null; } - return <>{options.map(({ name, onClick, disabled }) => )}; + return ( + <> + {options.map(({ name, onClick, disabled }) => ( + + ))} + + ); }; const content = getOptions(BUTTON_OPTIONS); @@ -125,45 +135,70 @@ const ClientCell = ({ 'button-action__container--detailed': isDetailed, }); - return
- - {content && } -
; + > + {t(buttonType)} + + {content && ( + + )} +
+ ); }; - return
- -
-
- {renderFormattedClientCell(client, info, isDetailed, true)} + return ( +
+ +
+
+ {renderFormattedClientCell(client, clientInfo, isDetailed, true)} +
+ {isDetailed && clientName && !whoisAvailable && ( +
+ {clientName} +
+ )}
- {isDetailed && name && !whoisAvailable - &&
{name}
} + {renderBlockingButton(isFiltered, domain)}
- {renderBlockingButton(isFiltered, domain)} -
; + ); }; ClientCell.propTypes = { client: propTypes.string.isRequired, + client_id: propTypes.string, domain: propTypes.string.isRequired, info: propTypes.oneOfType([ propTypes.string, diff --git a/client/src/components/Logs/Cells/IconTooltip.css b/client/src/components/Logs/Cells/IconTooltip.css index 966f036f..811c7623 100644 --- a/client/src/components/Logs/Cells/IconTooltip.css +++ b/client/src/components/Logs/Cells/IconTooltip.css @@ -35,7 +35,7 @@ } .grid--title { - font-weight: bold; + font-weight: 600; } .grid--title:not(:first-child) { @@ -65,12 +65,12 @@ } .grid .key-colon, .grid .title--border { - font-weight: bold; + font-weight: 600; } } .grid .key-colon:nth-child(odd)::after { - content: ':'; + content: ":"; } .grid__one-row { @@ -95,7 +95,7 @@ } .title--border:before { - content: ''; + content: ""; position: absolute; left: 0; border-top: 0.5px solid var(--gray-d8) !important; diff --git a/client/src/components/Logs/Cells/ResponseCell.js b/client/src/components/Logs/Cells/ResponseCell.js index 816f35a3..3a3aeb60 100644 --- a/client/src/components/Logs/Cells/ResponseCell.js +++ b/client/src/components/Logs/Cells/ResponseCell.js @@ -4,8 +4,9 @@ import classNames from 'classnames'; import React from 'react'; import propTypes from 'prop-types'; import { + getRulesToFilterList, formatElapsedMs, - getFilterName, + getFilterNames, getServiceName, } from '../../../helpers/helpers'; import { FILTERED_STATUS, FILTERED_STATUS_TO_META_MAP } from '../../../helpers/constants'; @@ -18,8 +19,7 @@ const ResponseCell = ({ response, status, upstream, - rule, - filterId, + rules, service_name, }) => { const { t } = useTranslation(); @@ -36,7 +36,6 @@ const ResponseCell = ({ const statusLabel = t(isBlockedByResponse ? 'blocked_by_cname_or_ip' : FILTERED_STATUS_TO_META_MAP[reason]?.LABEL || reason); const boldStatusLabel = {statusLabel}; - const filter = getFilterName(filters, whitelistFilters, filterId); const renderResponses = (responseArr) => { if (!responseArr || responseArr.length === 0) { @@ -57,13 +56,17 @@ const ResponseCell = ({ install_settings_dns: upstream, elapsed: formattedElapsedMs, response_code: status, - ...(service_name ? { service_name: getServiceName(service_name) } : { filter }), - rule_label: rule, + ...(service_name + && { service_name: getServiceName(service_name) } + ), + ...(rules.length > 0 + && { rule_label: getRulesToFilterList(rules, filters, whitelistFilters) } + ), response_table_header: renderResponses(response), original_response: renderResponses(originalResponse), }; - const content = rule + const content = rules.length > 0 ? Object.entries(COMMON_CONTENT) : Object.entries({ ...COMMON_CONTENT, @@ -78,7 +81,8 @@ const ResponseCell = ({ } return getServiceName(service_name); case FILTERED_STATUS.FILTERED_BLACK_LIST: - return filter; + case FILTERED_STATUS.NOT_FILTERED_WHITE_LIST: + return getFilterNames(rules, filters, whitelistFilters).join(', '); default: return formattedElapsedMs; } @@ -113,8 +117,10 @@ ResponseCell.propTypes = { response: propTypes.array.isRequired, status: propTypes.string.isRequired, upstream: propTypes.string.isRequired, - rule: propTypes.string, - filterId: propTypes.number, + rules: propTypes.arrayOf(propTypes.shape({ + text: propTypes.string.isRequired, + filter_list_id: propTypes.number.isRequired, + })), service_name: propTypes.string, }; diff --git a/client/src/components/Logs/Cells/index.js b/client/src/components/Logs/Cells/index.js index 2e2635d9..5ec64f1f 100644 --- a/client/src/components/Logs/Cells/index.js +++ b/client/src/components/Logs/Cells/index.js @@ -6,11 +6,11 @@ import propTypes from 'prop-types'; import { captitalizeWords, checkFiltered, + getRulesToFilterList, formatDateTime, formatElapsedMs, formatTime, getBlockingClientName, - getFilterName, getServiceName, processContent, } from '../../../helpers/helpers'; @@ -70,8 +70,8 @@ const Row = memo(({ upstream, type, client_proto, - filterId, - rule, + client_id, + rules, originalResponse, status, service_name, @@ -107,8 +107,6 @@ const Row = memo(({ const sourceData = getSourceData(tracker); - const filter = getFilterName(filters, whitelistFilters, filterId); - const { confirmMessage, buttonKey: blockingClientKey, @@ -172,13 +170,14 @@ const Row = memo(({ response_details: 'title', install_settings_dns: upstream, elapsed: formattedElapsedMs, - filter: rule ? filter : null, - rule_label: rule, + ...(rules.length > 0 + && { rule_label: getRulesToFilterList(rules, filters, whitelistFilters) } + ), response_table_header: response?.join('\n'), response_code: status, client_details: 'title', ip_address: client, - name: info?.name, + name: info?.name || client_id, country, city, network, @@ -235,8 +234,11 @@ Row.propTypes = { upstream: propTypes.string.isRequired, type: propTypes.string.isRequired, client_proto: propTypes.string.isRequired, - filterId: propTypes.number, - rule: propTypes.string, + client_id: propTypes.string, + rules: propTypes.arrayOf(propTypes.shape({ + text: propTypes.string.isRequired, + filter_list_id: propTypes.number.isRequired, + })), originalResponse: propTypes.array, status: propTypes.string.isRequired, service_name: propTypes.string, diff --git a/client/src/components/Logs/Logs.css b/client/src/components/Logs/Logs.css index c4f70827..4a642cb5 100644 --- a/client/src/components/Logs/Logs.css +++ b/client/src/components/Logs/Logs.css @@ -9,21 +9,18 @@ --size-response: 150; --size-client: 123; --gray-216: rgba(216, 216, 216, 0.23); - --gray-4d: #4D4D4D; - --gray-f3: #F3F3F3; + --gray-4d: #4d4d4d; + --gray-f3: #f3f3f3; --gray-8: #888; --gray-3: #333; - --danger: #DF3812; + --danger: #df3812; --white80: rgba(255, 255, 255, 0.8); - - --btn-block: #C23814; - --btn-block-disabled: #E3B3A6; - --btn-block-active: #A62200; - + --btn-block: #c23814; + --btn-block-disabled: #e3b3a6; + --btn-block-active: #a62200; --btn-unblock: #888888; - --btn-unblock-disabled: #D8D8D8; - --btn-unblock-active: #4D4D4D; - + --btn-unblock-disabled: #d8d8d8; + --btn-unblock-active: #4d4d4d; --option-border-radius: 4px; } @@ -40,7 +37,7 @@ } .logs__text--bold { - font-weight: bold; + font-weight: 600; } .logs__time { @@ -87,7 +84,7 @@ } .custom-select__arrow--left { - background: var(--white) url('../ui/svg/chevron-down.svg') no-repeat; + background: var(--white) url("../ui/svg/chevron-down.svg") no-repeat; background-position: 5px 9px; background-size: 22px; } @@ -167,12 +164,13 @@ } .logs__refresh { - --size: 2.5rem; position: relative; top: 3px; display: inline-flex; align-items: center; justify-content: center; + + --size: 2.5rem; width: var(--size); height: var(--size); padding: 0; @@ -360,7 +358,7 @@ color: var(--gray-4d); background-color: var(--white80); pointer-events: none; - font-weight: bold; + font-weight: 600; text-align: center; padding-top: 21rem; display: block; @@ -431,3 +429,13 @@ margin-right: 1px; opacity: 0.5; } + +.filteringRules__rule { + margin-bottom: 0; +} + +.filteringRules__filter { + font-style: italic; + font-weight: normal; + margin-bottom: 1rem; +} diff --git a/client/src/components/Settings/Clients/Form.js b/client/src/components/Settings/Clients/Form.js index 20f1f828..631272d8 100644 --- a/client/src/components/Settings/Clients/Form.js +++ b/client/src/components/Settings/Clients/Form.js @@ -282,7 +282,7 @@ let Form = (props) => {
+ link , ]} diff --git a/client/src/components/Settings/Encryption/CertificateStatus.js b/client/src/components/Settings/Encryption/CertificateStatus.js index a93389f5..911d1d36 100644 --- a/client/src/components/Settings/Encryption/CertificateStatus.js +++ b/client/src/components/Settings/Encryption/CertificateStatus.js @@ -50,7 +50,7 @@ const CertificateStatus = ({ {dnsNames && (
  • encryption_hostnames:  - {dnsNames} + {dnsNames.join(', ')}
  • )} @@ -65,7 +65,7 @@ CertificateStatus.propTypes = { subject: PropTypes.string, issuer: PropTypes.string, notAfter: PropTypes.string, - dnsNames: PropTypes.string, + dnsNames: PropTypes.arrayOf(PropTypes.string), }; export default withTranslation()(CertificateStatus); diff --git a/client/src/components/Settings/Encryption/Form.js b/client/src/components/Settings/Encryption/Form.js index 1619ea3e..58cd181e 100644 --- a/client/src/components/Settings/Encryption/Form.js +++ b/client/src/components/Settings/Encryption/Form.js @@ -12,7 +12,7 @@ import { toNumber, } from '../../../helpers/form'; import { - validateIsSafePort, validatePort, validatePortQuic, validatePortTLS, + validateServerName, validateIsSafePort, validatePort, validatePortQuic, validatePortTLS, } from '../../../helpers/validators'; import i18n from '../../../i18n'; import KeyStatus from './KeyStatus'; @@ -127,6 +127,7 @@ let Form = (props) => { placeholder={t('encryption_server_enter')} onChange={handleChange} disabled={!isEnabled} + validate={validateServerName} />
    encryption_server_desc @@ -413,7 +414,7 @@ Form.propTypes = { valid_key: PropTypes.bool, valid_cert: PropTypes.bool, valid_pair: PropTypes.bool, - dns_names: PropTypes.string, + dns_names: PropTypes.arrayOf(PropTypes.string), key_type: PropTypes.string, issuer: PropTypes.string, subject: PropTypes.string, diff --git a/client/src/components/ui/Guide.js b/client/src/components/ui/Guide/Guide.js similarity index 71% rename from client/src/components/ui/Guide.js rename to client/src/components/ui/Guide/Guide.js index a06af83b..cf1af0b0 100644 --- a/client/src/components/ui/Guide.js +++ b/client/src/components/ui/Guide/Guide.js @@ -3,27 +3,12 @@ import PropTypes from 'prop-types'; import { Trans, useTranslation } from 'react-i18next'; import i18next from 'i18next'; import { useSelector } from 'react-redux'; -import Tabs from './Tabs'; -import Icons from './Icons'; -import { getPathWithQueryString } from '../../helpers/helpers'; -const MOBILE_CONFIG_LINKS = { - DOT: '/apple/dot.mobileconfig', - DOH: '/apple/doh.mobileconfig', -}; -const renderMobileconfigInfo = ({ label, components, server_name }) =>
  • - {label} - -
  • ; +import { MOBILE_CONFIG_LINKS } from '../../../helpers/constants'; + +import Tabs from '../Tabs'; +import Icons from '../Icons'; +import MobileConfigForm from './MobileConfigForm'; const renderLi = ({ label, components }) =>
  • { @@ -41,49 +26,8 @@ const renderLi = ({ label, components }) =>
  • ; -const getDnsPrivacyList = (server_name) => { - const iosList = [ - { - label: 'setup_dns_privacy_ios_2', - components: [ - { - key: 0, - href: 'https://adguard.com/adguard-ios/overview.html', - }, - text, - ], - }, - { - label: 'setup_dns_privacy_ios_1', - components: [ - { - key: 0, - href: 'https://itunes.apple.com/app/id1452162351', - }, - text, - { - key: 2, - href: 'https://dnscrypt.info/stamps', - }, - - ], - }]; - /* Insert second element if can generate .mobileconfig links */ - if (server_name) { - iosList.splice(1, 0, { - label: 'setup_dns_privacy_4', - components: { - highlight: , - }, - renderComponent: ({ label, components }) => renderMobileconfigInfo({ - label, - components, - server_name, - }), - }); - } - - return [{ +const getDnsPrivacyList = () => [ + { title: 'Android', list: [ { @@ -113,7 +57,32 @@ const getDnsPrivacyList = (server_name) => { }, { title: 'iOS', - list: iosList, + list: [ + { + label: 'setup_dns_privacy_ios_2', + components: [ + { + key: 0, + href: 'https://adguard.com/adguard-ios/overview.html', + }, + text, + ], + }, + { + label: 'setup_dns_privacy_ios_1', + components: [ + { + key: 0, + href: 'https://itunes.apple.com/app/id1452162351', + }, + text, + { + key: 2, + href: 'https://dnscrypt.info/stamps', + }, + ], + }, + ], }, { title: 'setup_dns_privacy_other_title', @@ -166,20 +135,20 @@ const getDnsPrivacyList = (server_name) => { }, ], }, - ]; -}; +]; -const renderDnsPrivacyList = ({ title, list }) =>
    - {title} -
      {list.map( - ({ - label, - components, - renderComponent = renderLi, - }) => renderComponent({ label, components }), - )} -
    -
    ; +const renderDnsPrivacyList = ({ title, list }) => ( +
    + + {title} + +
      + {list.map(({ label, components, renderComponent = renderLi }) => ( + renderComponent({ label, components }) + ))} +
    +
    +); const getTabs = ({ tlsAddress, @@ -267,8 +236,8 @@ const getTabs = ({
    )} - {showDnsPrivacyNotice - ?
    + {showDnsPrivacyNotice ? ( +
    - : <> + ) : ( + <>
    text

    ]}> setup_dns_privacy_3
    - {getDnsPrivacyList(server_name).map(renderDnsPrivacyList)} - } + {getDnsPrivacyList().map(renderDnsPrivacyList)} +
    + + + setup_dns_privacy_ioc_mac + + +
    +
    + }}> + setup_dns_privacy_4 + +
    + + + )}
    ; }, }, }); -const renderContent = ({ title, list, getTitle }) =>
    -
    {i18next.t(title)}
    -
    - {getTitle?.()} - {list - &&
      {list.map((item) =>
    1. - {item} -
    2. )} -
    } +const renderContent = ({ title, list, getTitle }) => ( +
    +
    + {i18next.t(title)} +
    +
    + {getTitle?.()} + {list && ( +
      + {list.map((item) => ( +
    1. + {item} +
    2. + ))} +
    + )} +
    -
    ; +); const Guide = ({ dnsAddresses }) => { const { t } = useTranslation(); - const server_name = useSelector((state) => state.encryption.server_name); + const server_name = useSelector((state) => state.encryption?.server_name); const tlsAddress = dnsAddresses?.filter((item) => item.includes('tls://')) ?? ''; const httpsAddress = dnsAddresses?.filter((item) => item.includes('https://')) ?? ''; const showDnsPrivacyNotice = httpsAddress.length < 1 && tlsAddress.length < 1; @@ -332,9 +330,14 @@ const Guide = ({ dnsAddresses }) => { return (
    + + {activeTab} + - {activeTab}
    ); }; @@ -364,6 +367,4 @@ renderLi.propTypes = { components: PropTypes.string, }; -renderMobileconfigInfo.propTypes = renderLi.propTypes; - export default Guide; diff --git a/client/src/components/ui/Guide/MobileConfigForm.js b/client/src/components/ui/Guide/MobileConfigForm.js new file mode 100644 index 00000000..e1726d99 --- /dev/null +++ b/client/src/components/ui/Guide/MobileConfigForm.js @@ -0,0 +1,131 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { Trans } from 'react-i18next'; +import { useSelector } from 'react-redux'; +import { Field, reduxForm } from 'redux-form'; +import i18next from 'i18next'; +import cn from 'classnames'; + +import { getPathWithQueryString } from '../../../helpers/helpers'; +import { FORM_NAME, MOBILE_CONFIG_LINKS } from '../../../helpers/constants'; +import { + renderInputField, + renderSelectField, +} from '../../../helpers/form'; +import { + validateClientId, + validateServerName, +} from '../../../helpers/validators'; + +const getDownloadLink = (host, clientId, protocol, invalid) => { + if (!host || invalid) { + return ( + + ); + } + + const linkParams = { host }; + + if (clientId) { + linkParams.client_id = clientId; + } + + return ( + + download_mobileconfig + + ); +}; + +const MobileConfigForm = ({ invalid }) => { + const formValues = useSelector((state) => state.form[FORM_NAME.MOBILE_CONFIG]?.values); + + if (!formValues) { + return null; + } + + const { host, clientId, protocol } = formValues; + + const githubLink = ( + + text + + ); + + return ( +
    e.preventDefault()}> +
    +
    + + +
    +
    + +
    + + client_id_desc + +
    + +
    +
    + + + + + +
    +
    + + {getDownloadLink(host, clientId, protocol, invalid)} +
    + ); +}; + +MobileConfigForm.propTypes = { + invalid: PropTypes.bool.isRequired, +}; + +export default reduxForm({ form: FORM_NAME.MOBILE_CONFIG })(MobileConfigForm); diff --git a/client/src/components/ui/Guide/index.js b/client/src/components/ui/Guide/index.js new file mode 100644 index 00000000..ee660aeb --- /dev/null +++ b/client/src/components/ui/Guide/index.js @@ -0,0 +1 @@ +export { default } from './Guide'; diff --git a/client/src/components/ui/Icons.css b/client/src/components/ui/Icons.css index 24d71538..73f4c864 100644 --- a/client/src/components/ui/Icons.css +++ b/client/src/components/ui/Icons.css @@ -6,18 +6,21 @@ .icon--24 { --size: 1.5rem; + width: var(--size); height: var(--size); } .icon--20 { --size: 1.25rem; + width: var(--size); height: var(--size); } .icon--18 { --size: 1.125rem; + width: var(--size); height: var(--size); } diff --git a/client/src/components/ui/Icons.js b/client/src/components/ui/Icons.js index ec5bf8a3..e2186529 100644 Binary files a/client/src/components/ui/Icons.js and b/client/src/components/ui/Icons.js differ diff --git a/client/src/components/ui/texareaCommentsHighlight.css b/client/src/components/ui/texareaCommentsHighlight.css index 8551966d..19c84fac 100644 --- a/client/src/components/ui/texareaCommentsHighlight.css +++ b/client/src/components/ui/texareaCommentsHighlight.css @@ -15,7 +15,7 @@ white-space: pre-wrap; line-height: 1.5rem; word-wrap: break-word; - font-size: 0.9375rem; + font-size: var(--font-size-disable-autozoom); margin: 0; } diff --git a/client/src/helpers/constants.js b/client/src/helpers/constants.js index af10524f..0c9919ac 100644 --- a/client/src/helpers/constants.js +++ b/client/src/helpers/constants.js @@ -13,6 +13,8 @@ export const R_MAC = /^((([a-fA-F0-9][a-fA-F0-9]+[-]){5}|([a-fA-F0-9][a-fA-F0-9] export const R_CIDR_IPV6 = /^s*((([0-9A-Fa-f]{1,4}:){7}([0-9A-Fa-f]{1,4}|:))|(([0-9A-Fa-f]{1,4}:){6}(:[0-9A-Fa-f]{1,4}|((25[0-5]|2[0-4]d|1dd|[1-9]?d)(.(25[0-5]|2[0-4]d|1dd|[1-9]?d)){3})|:))|(([0-9A-Fa-f]{1,4}:){5}(((:[0-9A-Fa-f]{1,4}){1,2})|:((25[0-5]|2[0-4]d|1dd|[1-9]?d)(.(25[0-5]|2[0-4]d|1dd|[1-9]?d)){3})|:))|(([0-9A-Fa-f]{1,4}:){4}(((:[0-9A-Fa-f]{1,4}){1,3})|((:[0-9A-Fa-f]{1,4})?:((25[0-5]|2[0-4]d|1dd|[1-9]?d)(.(25[0-5]|2[0-4]d|1dd|[1-9]?d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){3}(((:[0-9A-Fa-f]{1,4}){1,4})|((:[0-9A-Fa-f]{1,4}){0,2}:((25[0-5]|2[0-4]d|1dd|[1-9]?d)(.(25[0-5]|2[0-4]d|1dd|[1-9]?d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){2}(((:[0-9A-Fa-f]{1,4}){1,5})|((:[0-9A-Fa-f]{1,4}){0,3}:((25[0-5]|2[0-4]d|1dd|[1-9]?d)(.(25[0-5]|2[0-4]d|1dd|[1-9]?d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){1}(((:[0-9A-Fa-f]{1,4}){1,6})|((:[0-9A-Fa-f]{1,4}){0,4}:((25[0-5]|2[0-4]d|1dd|[1-9]?d)(.(25[0-5]|2[0-4]d|1dd|[1-9]?d)){3}))|:))|(:(((:[0-9A-Fa-f]{1,4}){1,7})|((:[0-9A-Fa-f]{1,4}){0,5}:((25[0-5]|2[0-4]d|1dd|[1-9]?d)(.(25[0-5]|2[0-4]d|1dd|[1-9]?d)){3}))|:)))(%.+)?s*(\/(12[0-8]|1[0-1][0-9]|[1-9][0-9]|[0-9]))$/; +export const R_DOMAIN = /^([a-zA-Z0-9][a-zA-Z0-9-_]*\.)*[a-zA-Z0-9]*[a-zA-Z0-9-_]*[[a-zA-Z0-9]+$/; + export const R_PATH_LAST_PART = /\/[^/]*$/; // eslint-disable-next-line no-control-regex @@ -21,6 +23,8 @@ export const R_UNIX_ABSOLUTE_PATH = /^(\/[^/\x00]+)+$/; // eslint-disable-next-line no-control-regex export const R_WIN_ABSOLUTE_PATH = /^([a-zA-Z]:)?(\\|\/)(?:[^\\/:*?"<>|\x00]+\\)*[^\\/:*?"<>|\x00]*$/; +export const R_CLIENT_ID = /^[a-z0-9-]{1,64}$/; + export const HTML_PAGES = { INSTALL: '/install.html', LOGIN: '/login.html', @@ -196,92 +200,144 @@ export const FILTERS_URLS = { export const SERVICES = [ { - id: 'facebook', - name: 'Facebook', - }, - { - id: 'whatsapp', - name: 'WhatsApp', - }, - { - id: 'instagram', - name: 'Instagram', - }, - { - id: 'twitter', - name: 'Twitter', - }, - { - id: 'youtube', - name: 'YouTube', - }, - { - id: 'netflix', - name: 'Netflix', - }, - { - id: 'snapchat', - name: 'Snapchat', - }, - { - id: 'twitch', - name: 'Twitch', - }, - { - id: 'discord', - name: 'Discord', - }, - { - id: 'skype', - name: 'Skype', + id: '9gag', + name: '9Gag', }, { id: 'amazon', name: 'Amazon', }, - { - id: 'ebay', - name: 'eBay', - }, - { - id: 'origin', - name: 'Origin', - }, { id: 'cloudflare', - name: 'Cloudflare', + name: 'CloudFlare', }, { - id: 'steam', - name: 'Steam', + id: 'dailymotion', + name: 'Dailymotion', + }, + { + id: 'discord', + name: 'Discord', + }, + { + id: 'disneyplus', + name: 'Disney+', + }, + { + id: 'ebay', + name: 'EBay', }, { id: 'epic_games', name: 'Epic Games', }, + { + id: 'facebook', + name: 'Facebook', + }, + { + id: 'hulu', + name: 'Hulu', + }, + { + id: 'imgur', + name: 'Imgur', + }, + { + id: 'instagram', + name: 'Instagram', + }, + { + id: 'mail_ru', + name: 'Mail.ru', + }, + { + id: 'netflix', + name: 'Netflix', + }, + { + id: 'ok', + name: 'OK.ru', + }, + { + id: 'origin', + name: 'Origin', + }, + { + id: 'pinterest', + name: 'Pinterest', + }, + { + id: 'qq', + name: 'QQ', + }, { id: 'reddit', name: 'Reddit', }, { - id: 'ok', - name: 'OK', + id: 'skype', + name: 'Skype', }, { - id: 'vk', - name: 'VK', + id: 'snapchat', + name: 'Snapchat', }, { - id: 'mail_ru', - name: 'mail.ru', + id: 'spotify', + name: 'Spotify', + }, + { + id: 'steam', + name: 'Steam', + }, + { + id: 'telegram', + name: 'Telegram', }, { id: 'tiktok', name: 'TikTok', }, { - id: 'qq', - name: 'QQ', + id: 'tinder', + name: 'Tinder', + }, + { + id: 'twitch', + name: 'Twitch', + }, + { + id: 'twitter', + name: 'Twitter', + }, + { + id: 'viber', + name: 'Viber', + }, + { + id: 'vimeo', + name: 'Vimeo', + }, + { + id: 'vk', + name: 'VK.com', + }, + { + id: 'wechat', + name: 'WeChat', + }, + { + id: 'weibo', + name: 'Weibo', + }, + { + id: 'whatsapp', + name: 'WhatsApp', + }, + { + id: 'youtube', + name: 'YouTube', }, ]; @@ -339,6 +395,7 @@ export const FILTERED_STATUS = { FILTERED_BLOCKED_SERVICE: 'FilteredBlockedService', REWRITE: 'Rewrite', REWRITE_HOSTS: 'RewriteEtcHosts', + REWRITE_RULE: 'RewriteRule', FILTERED_SAFE_SEARCH: 'FilteredSafeSearch', FILTERED_SAFE_BROWSING: 'FilteredSafeBrowsing', FILTERED_PARENTAL: 'FilteredParental', @@ -430,6 +487,10 @@ export const FILTERED_STATUS_TO_META_MAP = { LABEL: RESPONSE_FILTER.REWRITTEN.LABEL, COLOR: QUERY_STATUS_COLORS.BLUE, }, + [FILTERED_STATUS.REWRITE_RULE]: { + LABEL: RESPONSE_FILTER.REWRITTEN.LABEL, + COLOR: QUERY_STATUS_COLORS.BLUE, + }, [FILTERED_STATUS.FILTERED_SAFE_BROWSING]: { LABEL: RESPONSE_FILTER.BLOCKED_THREATS.LABEL, COLOR: QUERY_STATUS_COLORS.YELLOW, @@ -473,6 +534,7 @@ export const BLOCK_ACTIONS = { }; export const SCHEME_TO_PROTOCOL_MAP = { + dnscrypt: 'dnscrypt', doh: 'dns_over_https', dot: 'dns_over_tls', doq: 'dns_over_quic', @@ -509,6 +571,7 @@ export const FORM_NAME = { INSTALL: 'install', LOGIN: 'login', CACHE: 'cache', + MOBILE_CONFIG: 'mobileConfig', ...DHCP_FORM_NAMES, }; @@ -569,6 +632,7 @@ export const TOAST_TIMEOUTS = { export const ADDRESS_TYPES = { IP: 'IP', CIDR: 'CIDR', + CLIENT_ID: 'CLIENT_ID', UNKNOWN: 'UNKNOWN', }; @@ -580,3 +644,8 @@ export const CACHE_CONFIG_FIELDS = { export const isFirefox = navigator.userAgent.indexOf('Firefox') !== -1; export const COMMENT_LINE_DEFAULT_TOKEN = '#'; + +export const MOBILE_CONFIG_LINKS = { + DOT: '/apple/dot.mobileconfig', + DOH: '/apple/doh.mobileconfig', +}; diff --git a/client/src/helpers/filters/filters.json b/client/src/helpers/filters/filters.json index 93f73854..6cd61084 100644 --- a/client/src/helpers/filters/filters.json +++ b/client/src/helpers/filters/filters.json @@ -54,11 +54,11 @@ "homepage": "https://github.com/Perflyst/PiHoleBlocklist", "source": "https://raw.githubusercontent.com/Perflyst/PiHoleBlocklist/master/SmartTV-AGH.txt" }, - "malwaredomainlist-com-hosts-list": { - "name": "MalwareDomainList.com Hosts List", - "categoryId": "security", - "homepage": "https://www.malwaredomainlist.com/", - "source": "https://www.malwaredomainlist.com/hostslist/hosts.txt" + "windows-spy-blocker" : { + "name": "WindowsSpyBlocker - Hosts spy rules", + "categoryId": "general", + "homepage": "https://github.com/crazy-max/WindowsSpyBlocker", + "source": "https://raw.githubusercontent.com/crazy-max/WindowsSpyBlocker/master/data/hosts/spy.txt" }, "spam404": { "name": "Spam404", @@ -76,7 +76,7 @@ "name": "The Big List of Hacked Malware Web Sites", "categoryId": "security", "homepage": "https://github.com/mitchellkrogza/The-Big-List-of-Hacked-Malware-Web-Sites", - "source": "https://raw.githubusercontent.com/mitchellkrogza/The-Big-List-of-Hacked-Malware-Web-Sites/master/hacked-domains.list" + "source": "https://raw.githubusercontent.com/mitchellkrogza/The-Big-List-of-Hacked-Malware-Web-Sites/master/hosts" }, "scam-blocklist-by-durable-napkin": { "name": "Scam Blocklist by DurableNapkin", @@ -84,6 +84,12 @@ "homepage": "https://github.com/durablenapkin/scamblocklist", "source": "https://raw.githubusercontent.com/durablenapkin/scamblocklist/master/adguard.txt" }, + "urlhaus-filter-online": { + "name": "Online Malicious URL Blocklist", + "categoryId": "security", + "homepage": "https://gitlab.com/curben/urlhaus-filter", + "source": "https://curben.gitlab.io/malware-filter/urlhaus-filter-agh-online.txt" + }, "NOR-dandelion-sprouts-nordiske-filtre": { "name": "NOR: Dandelion Sprouts nordiske filtre", "categoryId": "regional", @@ -126,12 +132,6 @@ "homepage": "https://filtri-dns.ga/", "source": "https://filtri-dns.ga/filtri.txt" }, - "JPN-280blocker": { - "name": "JPN: 280blocker adblock domain lists", - "categoryId": "regional", - "homepage": "https://280blocker.net/", - "source": "https://280blocker.net/files/280blocker_domain.txt" - }, "IRN-unwanted-iranian-domains": { "name": "IRN: Unwanted Iranian domains", "categoryId": "regional", @@ -150,7 +150,13 @@ "homepage": "https://anti-ad.net/", "source": "https://anti-ad.net/easylist.txt" }, - "BarbBlock": { + "IDN-abpindo": { + "name": "IDN: ABPindo", + "categoryId": "regional", + "homepage": "https://github.com/ABPindo/indonesianadblockrules/", + "source": "https://raw.githubusercontent.com/ABPindo/indonesianadblockrules/master/subscriptions/abpindo.txt" + }, + "barb-block": { "name": "BarbBlock", "categoryId": "other", "homepage": "https://github.com/paulgb/BarbBlock/", diff --git a/client/src/helpers/helpers.js b/client/src/helpers/helpers.js index 0cefcfdf..f8f276c8 100644 --- a/client/src/helpers/helpers.js +++ b/client/src/helpers/helpers.js @@ -4,9 +4,9 @@ import dateFormat from 'date-fns/format'; import round from 'lodash/round'; import axios from 'axios'; import i18n from 'i18next'; -import uniqBy from 'lodash/uniqBy'; import ipaddr from 'ipaddr.js'; import queryString from 'query-string'; +import React from 'react'; import { getTrackerData } from './trackers/trackers'; import { @@ -21,6 +21,7 @@ import { DHCP_VALUES_PLACEHOLDERS, FILTERED, FILTERED_STATUS, + R_CLIENT_ID, SERVICES_ID_NAME_MAP, STANDARD_DNS_PORT, STANDARD_HTTPS_PORT, @@ -61,6 +62,7 @@ export const normalizeLogs = (logs) => logs.map((log) => { answer_dnssec, client, client_proto, + client_id, elapsedMs, question, reason, @@ -68,6 +70,7 @@ export const normalizeLogs = (logs) => logs.map((log) => { time, filterId, rule, + rules, service_name, original_answer, upstream, @@ -80,6 +83,15 @@ export const normalizeLogs = (logs) => logs.map((log) => { return `${type}: ${value} (ttl=${ttl})`; }) : []); + let newRules = rules; + /* TODO 'filterId' and 'rule' are deprecated, will be removed in 0.106 */ + if (rule !== undefined && filterId !== undefined && rules !== undefined && rules.length === 0) { + newRules = { + filter_list_id: filterId, + text: rule, + }; + } + return { time, domain, @@ -88,8 +100,11 @@ export const normalizeLogs = (logs) => logs.map((log) => { reason, client, client_proto, + client_id, + /* TODO 'filterId' and 'rule' are deprecated, will be removed in 0.106 */ filterId, rule, + rules: newRules, status, service_name, originalAnswer: original_answer, @@ -113,12 +128,21 @@ export const normalizeTopStats = (stats) => ( })) ); -export const addClientInfo = (data, clients, param) => data.map((row) => { - const clientIp = row[param]; - const info = clients.find((item) => item[clientIp]) || ''; +export const addClientInfo = (data, clients, ...params) => data.map((row) => { + let info = ''; + params.find((param) => { + const id = row[param]; + if (id) { + const client = clients.find((item) => item[id]) || ''; + info = client?.[id] ?? ''; + } + + return info; + }); + return { ...row, - info: info?.[clientIp] ?? '', + info, }; }); @@ -190,7 +214,12 @@ export const getIpList = (interfaces) => Object.values(interfaces) .reduce((acc, curr) => acc.concat(curr.ip_addresses), []) .sort(); -export const getDnsAddress = (ip, port = '') => { +/** + * @param {string} ip + * @param {number} [port] + * @returns {string} + */ +export const getDnsAddress = (ip, port = 0) => { const isStandardDnsPort = port === STANDARD_DNS_PORT; let address = ip; @@ -205,7 +234,12 @@ export const getDnsAddress = (ip, port = '') => { return address; }; -export const getWebAddress = (ip, port = '') => { +/** + * @param {string} ip + * @param {number} [port] + * @returns {string} + */ +export const getWebAddress = (ip, port = 0) => { const isStandardWebPort = port === STANDARD_WEB_PORT; let address = `http://${ip}`; @@ -391,14 +425,21 @@ export const getPathWithQueryString = (path, params) => { return `${path}?${searchParams.toString()}`; }; -export const getParamsForClientsSearch = (data, param) => { - const uniqueClients = uniqBy(data, param); - return uniqueClients - .reduce((acc, item, idx) => { - const key = `ip${idx}`; - acc[key] = item[param]; - return acc; - }, {}); +export const getParamsForClientsSearch = (data, param, additionalParam) => { + const clients = new Set(); + data.forEach((e) => { + clients.add(e[param]); + if (e[additionalParam]) { + clients.add(e[additionalParam]); + } + }); + const params = {}; + const ids = Array.from(clients.values()); + ids.forEach((id, i) => { + params[`ip${i}`] = id; + }); + + return params; }; /** @@ -511,7 +552,7 @@ export const isIpInCidr = (ip, cidr) => { /** * * @param ipOrCidr - * @returns {'IP' | 'CIDR' | 'UNKNOWN'} + * @returns {'IP' | 'CIDR' | 'CLIENT_ID' | 'UNKNOWN'} * */ export const findAddressType = (address) => { @@ -524,6 +565,9 @@ export const findAddressType = (address) => { if (cidrMaybe && ipaddr.parseCIDR(address)) { return ADDRESS_TYPES.CIDR; } + if (R_CLIENT_ID.test(address)) { + return ADDRESS_TYPES.CLIENT_ID; + } return ADDRESS_TYPES.UNKNOWN; } catch (e) { @@ -544,20 +588,31 @@ export const separateIpsAndCidrs = (ids) => ids.reduce((acc, curr) => { if (addressType === ADDRESS_TYPES.CIDR) { acc.cidrs.push(curr); } + if (addressType === ADDRESS_TYPES.CLIENT_ID) { + acc.clientIds.push(curr); + } return acc; -}, { ips: [], cidrs: [] }); +}, { ips: [], cidrs: [], clientIds: [] }); export const countClientsStatistics = (ids, autoClients) => { - const { ips, cidrs } = separateIpsAndCidrs(ids); + const { ips, cidrs, clientIds } = separateIpsAndCidrs(ids); const ipsCount = ips.reduce((acc, curr) => { const count = autoClients[curr] || 0; return acc + count; }, 0); + const clientIdsCount = clientIds.reduce((acc, curr) => { + const count = autoClients[curr] || 0; + return acc + count; + }, 0); + const cidrsCount = Object.entries(autoClients) .reduce((acc, curr) => { const [id, count] = curr; + if (!ipaddr.isValid(id)) { + return false; + } if (cidrs.some((cidr) => isIpInCidr(id, cidr))) { // eslint-disable-next-line no-param-reassign acc += count; @@ -565,7 +620,7 @@ export const countClientsStatistics = (ids, autoClients) => { return acc; }, 0); - return ipsCount + cidrsCount; + return ipsCount + cidrsCount + clientIdsCount; }; /** @@ -716,6 +771,75 @@ export const getFilterName = ( return resolveFilterName(filter); }; +/** + * @param {array} rules + * @param {array} filters + * @param {array} whitelistFilters + * @returns {string[]} + */ +export const getFilterNames = (rules, filters, whitelistFilters) => rules.map( + ({ filter_list_id }) => getFilterName(filters, whitelistFilters, filter_list_id), +); + +/** + * @param {array} rules + * @returns {string[]} + */ +export const getRuleNames = (rules) => rules.map(({ text }) => text); + +/** + * @param {array} rules + * @param {array} filters + * @param {array} whitelistFilters + * @returns {object} + */ +export const getFilterNameToRulesMap = (rules, filters, whitelistFilters) => rules.reduce( + (acc, { text, filter_list_id }) => { + const filterName = getFilterName(filters, whitelistFilters, filter_list_id); + + acc[filterName] = (acc[filterName] || []).concat(text); + return acc; + }, {}, +); + +/** + * @param {array} rules + * @param {array} filters + * @param {array} whitelistFilters + * @param {object} classes + * @returns {JSXElement} + */ +export const getRulesToFilterList = (rules, filters, whitelistFilters, classes = { + list: 'filteringRules', + rule: 'filteringRules__rule font-monospace', + filter: 'filteringRules__filter', +}) => { + const filterNameToRulesMap = getFilterNameToRulesMap(rules, filters, whitelistFilters); + + return
    + {Object.entries(filterNameToRulesMap).reduce( + (acc, [filterName, rulesArr]) => acc + .concat(rulesArr.map((rule, i) =>
    {rule}
    )) + .concat(
    {filterName}
    ), + [], + )} +
    ; +}; + +/** +* @param {array} rules +* @param {array} filters +* @param {array} whitelistFilters +* @returns {string} +*/ +export const getRulesAndFilterNames = (rules, filters, whitelistFilters) => { + const filterNameToRulesMap = getFilterNameToRulesMap(rules, filters, whitelistFilters); + + return Object.entries(filterNameToRulesMap).map( + ([filterName, filterRules]) => filterRules.concat(filterName).join('\n'), + ).join('\n\n'); +}; + /** * @param ip {string} * @param gateway_ip {string} diff --git a/client/src/helpers/renderFormattedClientCell.js b/client/src/helpers/renderFormattedClientCell.js index d677c4ca..f7e59a84 100644 --- a/client/src/helpers/renderFormattedClientCell.js +++ b/client/src/helpers/renderFormattedClientCell.js @@ -31,7 +31,7 @@ const getFormattedWhois = (whois) => { * @param {object} info.whois_info * @param {boolean} [isDetailed] * @param {boolean} [isLogs] - * @returns {JSX.Element} + * @returns {JSXElement} */ export const renderFormattedClientCell = (value, info, isDetailed = false, isLogs = false) => { let whoisContainer = null; diff --git a/client/src/helpers/validators.js b/client/src/helpers/validators.js index f2fee026..c26bbb8e 100644 --- a/client/src/helpers/validators.js +++ b/client/src/helpers/validators.js @@ -9,6 +9,8 @@ import { R_URL_REQUIRES_PROTOCOL, STANDARD_WEB_PORT, UNSAFE_PORTS, + R_CLIENT_ID, + R_DOMAIN, } from './constants'; import { getLastIpv4Octet, isValidAbsolutePath } from './form'; @@ -16,7 +18,7 @@ import { getLastIpv4Octet, isValidAbsolutePath } from './form'; // https://redux-form.com/8.3.0/examples/fieldlevelvalidation/ // If the value is valid, the validation function should return undefined. /** - * @param value {string} + * @param value {string|number} * @returns {undefined|string} */ export const validateRequiredValue = (value) => { @@ -71,12 +73,28 @@ export const validateClientId = (value) => { || R_MAC.test(formattedValue) || R_CIDR.test(formattedValue) || R_CIDR_IPV6.test(formattedValue) + || R_CLIENT_ID.test(formattedValue) )) { return 'form_error_client_id_format'; } return undefined; }; +/** + * @param value {string} + * @returns {undefined|string} + */ +export const validateServerName = (value) => { + if (!value) { + return undefined; + } + const formattedValue = value ? value.trim() : value; + if (formattedValue && !R_DOMAIN.test(formattedValue)) { + return 'form_error_server_name'; + } + return undefined; +}; + /** * @param value {string} * @returns {undefined|string} diff --git a/client/src/install/Setup/AddressList.js b/client/src/install/Setup/AddressList.js index 15cf7113..ed58127c 100644 --- a/client/src/install/Setup/AddressList.js +++ b/client/src/install/Setup/AddressList.js @@ -41,16 +41,13 @@ const AddressList = ({ AddressList.propTypes = { interfaces: PropTypes.object.isRequired, address: PropTypes.string.isRequired, - port: PropTypes.oneOfType([ - PropTypes.string, - PropTypes.number, - ]), + port: PropTypes.number.isRequired, isDns: PropTypes.bool, }; renderItem.propTypes = { ip: PropTypes.string.isRequired, - port: PropTypes.string.isRequired, + port: PropTypes.number.isRequired, isDns: PropTypes.bool.isRequired, }; diff --git a/client/src/install/Setup/Setup.css b/client/src/install/Setup/Setup.css index 9a404222..b08f4cb8 100644 --- a/client/src/install/Setup/Setup.css +++ b/client/src/install/Setup/Setup.css @@ -1,3 +1,10 @@ +/* Disable Auto Zoom in Input - Safari on iPhone https://stackoverflow.com/a/6394497 */ +@media screen and (max-width: 767px) { + input, select, textarea { + font-size: 1rem; + } +} + .setup { min-height: calc(100vh - 71px); line-height: 1.48; diff --git a/client/src/login/Login/Login.css b/client/src/login/Login/Login.css index c38f27bf..0d3dbfc1 100644 --- a/client/src/login/Login/Login.css +++ b/client/src/login/Login/Login.css @@ -1,3 +1,10 @@ +/* Disable Auto Zoom in Input - Safari on iPhone https://stackoverflow.com/a/6394497 */ +@media screen and (max-width: 767px) { + input, select, textarea { + font-size: 1rem; + } +} + .login { display: flex; flex-direction: column; diff --git a/client/src/reducers/access.js b/client/src/reducers/access.js index e90bb314..69bf580d 100644 --- a/client/src/reducers/access.js +++ b/client/src/reducers/access.js @@ -24,13 +24,7 @@ const access = handleActions( [actions.setAccessListRequest]: (state) => ({ ...state, processingSet: true }), [actions.setAccessListFailure]: (state) => ({ ...state, processingSet: false }), - [actions.setAccessListSuccess]: (state) => { - const newState = { - ...state, - processingSet: false, - }; - return newState; - }, + [actions.setAccessListSuccess]: (state) => ({ ...state, processingSet: false }), [actions.toggleClientBlockRequest]: (state) => ({ ...state, processingSet: true }), [actions.toggleClientBlockFailure]: (state) => ({ ...state, processingSet: false }), diff --git a/client2/.eslintignore b/client2/.eslintignore new file mode 100644 index 00000000..d30c2950 --- /dev/null +++ b/client2/.eslintignore @@ -0,0 +1,6 @@ +scripts +node_modules +postcss.config.js +src/lib/entities +src/lib/apis +openApi \ No newline at end of file diff --git a/client2/.eslintrc b/client2/.eslintrc new file mode 100644 index 00000000..86f6ae47 --- /dev/null +++ b/client2/.eslintrc @@ -0,0 +1,5 @@ +{ + "extends": [ + "./scripts/lint/dev.js" + ] +} \ No newline at end of file diff --git a/client2/declaration.d.ts b/client2/declaration.d.ts new file mode 100644 index 00000000..87c53150 --- /dev/null +++ b/client2/declaration.d.ts @@ -0,0 +1,18 @@ +declare module '*.pcss' { + const content: {[className: string]: string}; + export default content; +} +declare module '*.css' { + const content: {[className: string]: string}; + export default content; +} +declare module '*.png' +declare module '*.jpg' +declare let AUTH_TOKEN: string; +declare let MAIN_TOKEN: string | undefined; +declare let NO_CAPTCHA: boolean | undefined; +declare module 'dygraphs'; +declare module '@novnc/novnc/core/rfb'; +// cp - CloudPayments script +declare let cp: any; +declare const DEV: any; diff --git a/client2/package.json b/client2/package.json new file mode 100644 index 00000000..71f4b70c --- /dev/null +++ b/client2/package.json @@ -0,0 +1,89 @@ +{ + "author": "Performix", + "private": true, + "name": "adguard-home", + "version": "0.1.0", + "scripts": { + "build": "rm -rf ../build2 && yarn install && webpack --config ./scripts/webpack/webpack.config.prod.js", + "start": "webpack serve --config ./scripts/webpack/webpack.config.dev.js", + "generate": "rm -rf ./src/lib/entities ./src/lib/apis && ts-node --compiler-options '{ \"module\": \"CommonJS\" }' ./scripts/generator/index.ts", + "translations:check": "ts-node --compiler-options '{ \"module\": \"CommonJS\" }' ./scripts/plugins/checkTranslations.ts", + "lint": "eslint -c ./scripts/lint/prod.js --ext .tsx --ext .ts ./", + "go:build": "cd .. && make REBUILD_CLIENT=0 build", + "go:run": "sudo ../AdguardHome" + }, + "license": "ISC", + "dependencies": { + "@adguard/translate": "^0.2.0", + "@ant-design/icons": "^4.4.0", + "@sentry/react": "^5.27.0", + "antd": "^4.7.2", + "classnames": "^2.2.6", + "dayjs": "^1.9.3", + "formik": "^2.2.0", + "mobx": "^6.0.1", + "mobx-react-lite": "^3.0.1", + "qs": "^6.9.4", + "react": "^17.0.0", + "react-dom": "^17.0.0", + "react-router-dom": "^5.2.0", + "recharts": "^2.0.3" + }, + "devDependencies": { + "@types/classnames": "^2.2.10", + "@types/qs": "^6.9.5", + "@types/react": "^16.9.53", + "@types/react-dom": "^16.9.8", + "@types/react-redux": "^7.1.9", + "@types/react-router-dom": "^5.1.6", + "@typescript-eslint/eslint-plugin": "^4.5.0", + "@typescript-eslint/parser": "^4.5.0", + "antd-dayjs-webpack-plugin": "^1.0.1", + "autoprefixer": "^10.0.1", + "connect-history-api-fallback": "^1.6.0", + "copy-webpack-plugin": "^6.2.1", + "css-loader": "^5.0.0", + "eslint": "^7.11.0", + "eslint-config-airbnb-base": "^14.2.0", + "eslint-config-airbnb-typescript": "^12.0.0", + "eslint-import-resolver-typescript": "^2.3.0", + "eslint-loader": "^4.0.2", + "eslint-plugin-import": "^2.22.1", + "eslint-plugin-react": "^7.21.5", + "eslint-plugin-react-hooks": "^4.2.0", + "file-loader": "^6.1.1", + "html-webpack-plugin": "^4.5.0", + "http-proxy-middleware": "^1.0.6", + "less": "^3.12.2", + "less-loader": "^5.0.0", + "mini-css-extract-plugin": "^1.1.1", + "optimize-css-assets-webpack-plugin": "^5.0.4", + "postcss": "^8.1.2", + "postcss-calc": "^7.0.5", + "postcss-css-variables": "^0.17.0", + "postcss-custom-media": "^7.0.8", + "postcss-import": "^13.0.0", + "postcss-inline-svg": "^4.1.0", + "postcss-loader": "^4.0.4", + "postcss-mixins": "^7.0.1", + "postcss-modules": "^3.2.2", + "postcss-nested": "^5.0.1", + "postcss-preset-env": "^6.7.0", + "postcss-reporter": "^7.0.1", + "postcss-variables": "^1.1.1", + "style-loader": "^2.0.0", + "stylelint": "^13.7.2", + "stylelint-webpack-plugin": "^2.1.1", + "terser-webpack-plugin": "^5.0.0", + "ts-loader": "^8.0.6", + "ts-morph": "^8.1.2", + "ts-node": "^9.0.0", + "typescript": "^4.0.3", + "url-loader": "^4.1.1", + "webpack": "^5.10.0", + "webpack-cli": "^4.2.0", + "webpack-dev-server": "^3.11.0", + "webpack-merge": "^5.2.0", + "yaml": "^1.10.0" + } +} diff --git a/client2/postcss.config.js b/client2/postcss.config.js new file mode 100644 index 00000000..ee95a234 --- /dev/null +++ b/client2/postcss.config.js @@ -0,0 +1,17 @@ +module.exports = { + plugins: [ + ['postcss-import', {}], + ['postcss-nested', {}], + ['postcss-custom-media', {}], + ['postcss-variables', {}], + ['postcss-calc', {}], + ['postcss-mixins', {}], + ['postcss-preset-env', { stage: 3, features: { 'nesting-rules': true } }], + ['postcss-reporter', { clearMessages: true }], + ['postcss-inline-svg', { + paths: ['frontend/icons', 'vendor/adguard/utils-bundle/src/Resources/frontend/icons'], + svgo: { plugins: [{ cleanupAttrs: true }] } + }], + ['autoprefixer'], + ] +}; diff --git a/client2/public/assets/apple-touch-icon-180x180.png b/client2/public/assets/apple-touch-icon-180x180.png new file mode 100644 index 00000000..ebc0be50 Binary files /dev/null and b/client2/public/assets/apple-touch-icon-180x180.png differ diff --git a/client2/public/assets/favicon.png b/client2/public/assets/favicon.png new file mode 100644 index 00000000..992631c1 Binary files /dev/null and b/client2/public/assets/favicon.png differ diff --git a/client2/public/assets/safari-pinned-tab.svg b/client2/public/assets/safari-pinned-tab.svg new file mode 100644 index 00000000..132b35d0 --- /dev/null +++ b/client2/public/assets/safari-pinned-tab.svg @@ -0,0 +1,8 @@ + + + + + + diff --git a/client2/public/index.html b/client2/public/index.html new file mode 100644 index 00000000..2e2d1e33 --- /dev/null +++ b/client2/public/index.html @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + AdGuard Home + + + +
    + + diff --git a/client2/public/install.html b/client2/public/install.html new file mode 100644 index 00000000..e0b70d14 --- /dev/null +++ b/client2/public/install.html @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + Setup AdGuard Home + + + +
    + + diff --git a/client2/public/login.html b/client2/public/login.html new file mode 100644 index 00000000..38145e15 --- /dev/null +++ b/client2/public/login.html @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + Login + + + +
    + + diff --git a/client2/scripts/consts.ts b/client2/scripts/consts.ts new file mode 100644 index 00000000..bbec9361 --- /dev/null +++ b/client2/scripts/consts.ts @@ -0,0 +1,12 @@ +export const OPEN_API_PATH = '../openapi/openapi.yaml'; +export const ENT_DIR = './src/lib/entities'; +export const API_DIR = './src/lib/apis'; +export const LOCALE_FOLDER_PATH = './src/lib/intl/__locales'; +export const TRANSLATOR_CLASS_NAME = 'Translator'; +export const USE_INTL_NAME = 'useIntl'; + +export const trimQuotes = (str: string) => { + return str.replace(/\'|\"/g, ''); +}; + +export const GENERATOR_ENTITY_ALLIAS = 'Entities/'; \ No newline at end of file diff --git a/client2/scripts/generator/index.ts b/client2/scripts/generator/index.ts new file mode 100644 index 00000000..19ac2256 --- /dev/null +++ b/client2/scripts/generator/index.ts @@ -0,0 +1,18 @@ +import * as fs from 'fs'; +import * as YAML from 'yaml'; +import { OPEN_API_PATH } from '../consts'; + +import EntitiesGenerator from './src/generateEntities'; +import ApisGenerator from './src/generateApis'; + + +const generateApi = (openApi: Record) => { + const ent = new EntitiesGenerator(openApi); + ent.save(); + + const api = new ApisGenerator(openApi); + api.save(); +} + +const openApiFile = fs.readFileSync(OPEN_API_PATH, 'utf8'); +generateApi(YAML.parse(openApiFile)); diff --git a/client2/scripts/generator/src/generateApis.ts b/client2/scripts/generator/src/generateApis.ts new file mode 100644 index 00000000..4201da2b --- /dev/null +++ b/client2/scripts/generator/src/generateApis.ts @@ -0,0 +1,317 @@ +/* eslint-disable no-template-curly-in-string */ +/* eslint-disable @typescript-eslint/no-unused-expressions */ +import * as fs from 'fs'; +import * as path from 'path'; +import { stringify } from 'qs'; +// eslint-disable-next-line import/no-extraneous-dependencies +import * as morph from 'ts-morph'; + +import { + API_DIR as API_DIR_CONST, + GENERATOR_ENTITY_ALLIAS, +} from '../../consts'; +import { toCamel, capitalize, schemaParamParser } from './utils'; + + +const API_DIR = path.resolve(API_DIR_CONST); +if (!fs.existsSync(API_DIR)) { + fs.mkdirSync(API_DIR); +} + +const { Project, QuoteKind } = morph; + + +class ApiGenerator { + project = new Project({ + tsConfigFilePath: './tsconfig.json', + addFilesFromTsConfig: false, + manipulationSettings: { + quoteKind: QuoteKind.Single, + usePrefixAndSuffixTextForRename: false, + useTrailingCommas: true, + }, + }); + + openapi: Record; + + serverUrl: string; + + paths: any; + + /* interface Controllers { + [controller: string]: { + [operationId: string]: { parameters - from opneApi, responses - from opneApi, method } + } + } */ + controllers: Record = {}; + + apis: morph.SourceFile[] = []; + + constructor(openapi: Record) { + this.openapi = openapi; + this.paths = openapi.paths; + this.serverUrl = openapi.servers[0].url; + + Object.keys(this.paths).forEach((pathKey) => { + Object.keys(this.paths[pathKey]).forEach((method) => { + const { + tags, operationId, parameters, responses, requestBody, security, + } = this.paths[pathKey][method]; + const controller = toCamel((tags ? tags[0] : pathKey.split('/')[1]).replace('-controller', '')); + + if (this.controllers[controller]) { + this.controllers[controller][operationId] = { + parameters, + responses, + method, + requestBody, + security, + pathKey: pathKey.replace(/{/g, '${'), + }; + } else { + this.controllers[controller] = { [operationId]: { + parameters, + responses, + method, + requestBody, + security, + pathKey: pathKey.replace(/{/g, '${'), + } }; + } + }); + }); + + this.generateApiFiles(); + } + + generateApiFiles = () => { + Object.keys(this.controllers).forEach(this.generateApiFile); + }; + + generateApiFile = (cName: string) => { + const apiFile = this.project.createSourceFile(`${API_DIR}/${cName}.ts`); + apiFile.addStatements([ + '// This file was autogenerated. Please do not change.', + '// All changes will be overwrited on commit.', + '', + ]); + + // const schemaProperties = schemas[schemaName].properties; + const importEntities: any[] = []; + + // add api class to file + const apiClass = apiFile.addClass({ + name: `${capitalize(cName)}Api`, + isDefaultExport: true, + }); + + // get operations of controller + const controllerOperations = this.controllers[cName]; + const operationList = Object.keys(controllerOperations).sort(); + // for each operation add fetcher + operationList.forEach((operation) => { + const { + requestBody, responses, parameters, method, pathKey, security, + } = controllerOperations[operation]; + + const queryParams: any[] = []; // { name, type } + const bodyParam: any[] = []; // { name, type } + + let hasResponseBodyType: /* boolean | ReturnType */ false | [string, boolean, boolean, boolean, boolean] = false; + let contentType = ''; + if (parameters) { + parameters.forEach((p: any) => { + const [ + pType, isArray, isClass, isImport, + ] = schemaParamParser(p.schema, this.openapi); + + if (isImport) { + importEntities.push({ type: pType, isClass }); + } + if (p.in === 'query') { + queryParams.push({ + name: p.name, type: `${pType}${isArray ? '[]' : ''}`, hasQuestionToken: !p.required }); + } + }); + } + if (queryParams.length > 0) { + const imp = apiFile.getImportDeclaration((i) => { + return i.getModuleSpecifierValue() === 'qs'; + }); if (!imp) { + apiFile.addImportDeclaration({ + moduleSpecifier: 'qs', + defaultImport: 'qs', + }); + } + } + if (requestBody) { + let content = requestBody.content; + const { $ref }: { $ref: string } = requestBody; + + if (!content && $ref) { + const name = $ref.split('/').pop() as string; + content = this.openapi.components.requestBodies[name].content; + } + + [contentType] = Object.keys(content); + const data = content[contentType]; + + const [ + pType, isArray, isClass, isImport, + ] = schemaParamParser(data.schema, this.openapi); + + if (isImport) { + importEntities.push({ type: pType, isClass }); + bodyParam.push({ name: pType.toLowerCase(), type: `${isClass ? 'I' : ''}${pType}${isArray ? '[]' : ''}`, isClass, pType }); + } else { + bodyParam.push({ name: 'data', type: `${pType}${isArray ? '[]' : ''}` }); + + } + } + if (responses['200']) { + const { content, headers } = responses['200']; + if (content && (content['*/*'] || content['application/json'])) { + const { schema, examples } = content['*/*'] || content['application/json']; + + if (!schema) { + process.exit(0); + } + + const propType = schemaParamParser(schema, this.openapi); + const [pType, , isClass, isImport] = propType; + + if (isImport) { + importEntities.push({ type: pType, isClass }); + } + hasResponseBodyType = propType; + } + } + let returnType = ''; + if (hasResponseBodyType) { + const [pType, isArray, isClass] = hasResponseBodyType as any; + let data = `Promise<${isClass ? 'I' : ''}${pType}${isArray ? '[]' : ''}`; + returnType = data; + } else { + returnType = 'Promise b.isClass); + if (shouldValidate.length > 0) { + returnType += ' | string[]'; + } + // append Error to default type return; + returnType += ' | Error>'; + + const fetcher = apiClass.addMethod({ + isAsync: true, + isStatic: true, + name: operation, + returnType, + }); + const params = [...queryParams, ...bodyParam].sort((a, b) => (Number(!!a.hasQuestionToken) - Number(!!b.hasQuestionToken))); + fetcher.addParameters(params); + + fetcher.setBodyText((w) => { + // Add data to URLSearchParams + if (contentType === 'text/plain') { + bodyParam.forEach((b) => { + w.writeLine(`const params = String(${b.name});`); + }); + } else { + if (shouldValidate.length > 0) { + w.writeLine(`const haveError: string[] = [];`); + shouldValidate.forEach((b) => { + w.writeLine(`const ${b.name}Valid = new ${b.pType}(${b.name});`); + w.writeLine(`haveError.push(...${b.name}Valid.validate());`); + }); + w.writeLine(`if (haveError.length > 0) {`); + w.writeLine(` return Promise.resolve(haveError);`) + w.writeLine(`}`); + } + } + // Switch return of fetch in case on queryParams + if (queryParams.length > 0) { + w.writeLine('const queryParams = {'); + queryParams.forEach((q) => { + w.writeLine(` ${q.name}: ${q.name},`); + }); + w.writeLine('}'); + w.writeLine(`return await fetch(\`${this.serverUrl}${pathKey}?\${qs.stringify(queryParams, { arrayFormat: 'comma' })}\`, {`); + } else { + w.writeLine(`return await fetch(\`${this.serverUrl}${pathKey}\`, {`); + } + // Add method + w.writeLine(` method: '${method.toUpperCase()}',`); + + // add Fetch options + if (contentType && contentType !== 'multipart/form-data') { + w.writeLine(' headers: {'); + w.writeLine(` 'Content-Type': '${contentType}',`); + w.writeLine(' },'); + } + if (contentType) { + switch (contentType) { + case 'text/plain': + w.writeLine(' body: params,'); + break; + default: + w.writeLine(` body: JSON.stringify(${bodyParam.map((b) => b.isClass ? `${b.name}Valid.serialize()` : b.name).join(', ')}),`); + break; + } + } + + // Handle response + if (hasResponseBodyType) { + w.writeLine('}).then(async (res) => {'); + w.writeLine(' if (res.status === 200) {'); + w.writeLine(' return res.json();'); + } else { + w.writeLine('}).then(async (res) => {'); + w.writeLine(' if (res.status === 200) {'); + w.writeLine(' return res.status;'); + } + + // Handle Error + w.writeLine(' } else {'); + w.writeLine(' return new Error(String(res.status));'); + w.writeLine(' }'); + w.writeLine('})'); + }); + }); + + const imports: any[] = []; + const types: string[] = []; + importEntities.forEach((i) => { + const { type } = i; + if (!types.includes(type)) { + imports.push(i); + types.push(type); + } + }); + imports.sort((a,b) => a.type > b.type ? 1 : -1).forEach((ie) => { + const { type: pType, isClass } = ie; + if (isClass) { + apiFile.addImportDeclaration({ + moduleSpecifier: `${GENERATOR_ENTITY_ALLIAS}${pType}`, + defaultImport: pType, + namedImports: [`I${pType}`], + }); + } else { + apiFile.addImportDeclaration({ + moduleSpecifier: `${GENERATOR_ENTITY_ALLIAS}${pType}`, + namedImports: [pType], + }); + } + }); + + this.apis.push(apiFile); + }; + + save = () => { + this.apis.forEach(async (e) => { + await e.saveSync(); + }); + }; +} + + +export default ApiGenerator; diff --git a/client2/scripts/generator/src/generateEntities.ts b/client2/scripts/generator/src/generateEntities.ts new file mode 100644 index 00000000..7ace9fa1 --- /dev/null +++ b/client2/scripts/generator/src/generateEntities.ts @@ -0,0 +1,603 @@ +import * as fs from 'fs'; +import * as path from 'path'; +// eslint-disable-next-line import/no-extraneous-dependencies +import * as morph from 'ts-morph'; + +import { ENT_DIR } from '../../consts'; +import { TYPES, toCamel, schemaParamParser, uncapitalize } from './utils'; + +const { Project, QuoteKind } = morph; + + +const EntDir = path.resolve(ENT_DIR); +if (!fs.existsSync(EntDir)) { + fs.mkdirSync(EntDir); +} + +class EntitiesGenerator { + project = new Project({ + tsConfigFilePath: './tsconfig.json', + addFilesFromTsConfig: false, + manipulationSettings: { + quoteKind: QuoteKind.Single, + usePrefixAndSuffixTextForRename: false, + useTrailingCommas: true, + }, + }); + + openapi: Record; + + schemas: Record; + + schemaNames: string[]; + + entities: morph.SourceFile[] = []; + + constructor(openapi: Record) { + this.openapi = openapi; + this.schemas = openapi.components.schemas; + this.schemaNames = Object.keys(this.schemas); + this.generateEntities(); + } + + generateEntities = () => { + this.schemaNames.forEach(this.generateEntity); + }; + + generateEntity = (sName: string) => { + const { properties, type, oneOf } = this.schemas[sName]; + const notAClass = !properties && TYPES[type as keyof typeof TYPES]; + + if (oneOf) { + this.generateOneOf(sName); + return; + } + + if (notAClass) { + this.generateEnum(sName); + } else { + this.generateClass(sName); + } + }; + + generateEnum = (sName: string) => { + const entityFile = this.project.createSourceFile(`${EntDir}/${sName}.ts`); + entityFile.addStatements([ + '// This file was autogenerated. Please do not change.', + '// All changes will be overwrited on commit.', + '', + ]); + + const { enum: enumMembers } = this.schemas[sName]; + entityFile.addEnum({ + name: sName, + members: enumMembers.map((e: string) => ({ name: e.toUpperCase(), value: e })), + isExported: true, + }); + + this.entities.push(entityFile); + }; + + generateOneOf = (sName: string) => { + const entityFile = this.project.createSourceFile(`${EntDir}/${sName}.ts`); + entityFile.addStatements([ + '// This file was autogenerated. Please do not change.', + '// All changes will be overwrited on commit.', + '', + ]); + const importEntities: { type: string, isClass: boolean }[] = []; + const entities = this.schemas[sName].oneOf.map((elem: any) => { + const [ + pType, isArray, isClass, isImport, + ] = schemaParamParser(elem, this.openapi); + importEntities.push({ type: pType, isClass }); + return { type: pType, isArray }; + }); + entityFile.addTypeAlias({ + name: sName, + isExported: true, + type: entities.map((e: any) => e.isArray ? `I${e.type}[]` : `I${e.type}`).join(' | '), + }) + + // add import + importEntities.sort((a, b) => a.type > b.type ? 1 : -1).forEach((ie) => { + const { type: pType, isClass } = ie; + if (isClass) { + entityFile.addImportDeclaration({ + moduleSpecifier: `./${pType}`, + namedImports: [`I${pType}`], + }); + } else { + entityFile.addImportDeclaration({ + moduleSpecifier: `./${pType}`, + namedImports: [pType], + }); + } + }); + this.entities.push(entityFile); + } + + generateClass = (sName: string) => { + const entityFile = this.project.createSourceFile(`${EntDir}/${sName}.ts`); + entityFile.addStatements([ + '// This file was autogenerated. Please do not change.', + '// All changes will be overwrited on commit.', + '', + ]); + + + const { properties: sProps, required, $ref, additionalProperties } = this.schemas[sName]; + if ($ref) { + const temp = $ref.split('/'); + const importSchemaName = `${temp[temp.length - 1]}`; + entityFile.addImportDeclaration({ + defaultImport: importSchemaName, + moduleSpecifier: `./${importSchemaName}`, + namedImports: [`I${importSchemaName}`], + }); + + entityFile.addTypeAlias({ + name: `I${sName}`, + type: `I${importSchemaName}`, + isExported: true, + }) + + entityFile.addStatements(`export default ${importSchemaName};`); + this.entities.push(entityFile); + return; + } + + const importEntities: { type: string, isClass: boolean }[] = []; + const entityInterface = entityFile.addInterface({ + name: `I${sName}`, + isExported: true, + }); + + const sortedSProps = Object.keys(sProps || {}).sort(); + const additionalPropsOnly = additionalProperties && sortedSProps.length === 0; + + // add server response interface to entityFile + sortedSProps.forEach((sPropName) => { + const [ + pType, isArray, isClass, isImport, isAdditional + ] = schemaParamParser(sProps[sPropName], this.openapi); + + if (isImport) { + importEntities.push({ type: pType, isClass }); + } + const propertyType = isAdditional + ? `{ [key: string]: ${isClass ? 'I' : ''}${pType}${isArray ? '[]' : ''} }` + : `${isClass ? 'I' : ''}${pType}${isArray ? '[]' : ''}`; + entityInterface.addProperty({ + name: sPropName, + type: propertyType, + hasQuestionToken: !( + (required && required.includes(sPropName)) || sProps[sPropName].required + ), + }); + }); + if (additionalProperties) { + const [ + pType, isArray, isClass, isImport, isAdditional + ] = schemaParamParser(additionalProperties, this.openapi); + + if (isImport) { + importEntities.push({ type: pType, isClass }); + } + const type = isAdditional + ? `{ [key: string]: ${isClass ? 'I' : ''}${pType}${isArray ? '[]' : ''} }` + : `${isClass ? 'I' : ''}${pType}${isArray ? '[]' : ''}`; + entityInterface.addIndexSignature({ + keyName: 'key', + keyType: 'string', + returnType: additionalPropsOnly ? type : `${type} | undefined`, + }); + } + + // add import + const imports: { type: string, isClass: boolean }[] = []; + const types: string[] = []; + importEntities.forEach((i) => { + const { type } = i; + if (!types.includes(type)) { + imports.push(i); + types.push(type); + } + }); + imports.sort((a, b) => a.type > b.type ? 1 : -1).forEach((ie) => { + const { type: pType, isClass } = ie; + if (isClass) { + entityFile.addImportDeclaration({ + defaultImport: pType, + moduleSpecifier: `./${pType}`, + namedImports: [`I${pType}`], + }); + } else { + entityFile.addImportDeclaration({ + moduleSpecifier: `./${pType}`, + namedImports: [pType], + }); + } + }); + + const entityClass = entityFile.addClass({ + name: sName, + isDefaultExport: true, + }); + + // addProperties to class; + sortedSProps.forEach((sPropName) => { + const [pType, isArray, isClass, isImport, isAdditional] = schemaParamParser(sProps[sPropName], this.openapi); + + const isRequred = (required && required.includes(sPropName)) + || sProps[sPropName].required; + + const propertyType = isAdditional + ? `{ [key: string]: ${pType}${isArray ? '[]' : ''}${isRequred ? '' : ' | undefined'} }` + : `${pType}${isArray ? '[]' : ''}${isRequred ? '' : ' | undefined'}`; + + entityClass.addProperty({ + name: `_${sPropName}`, + isReadonly: true, + type: propertyType, + }); + const getter = entityClass.addGetAccessor({ + name: toCamel(sPropName), + returnType: propertyType, + statements: [`return this._${sPropName};`], + }); + const { description, example, minItems, maxItems, maxLength, minLength, maximum, minimum } = sProps[sPropName]; + if (description || example) { + getter.addJsDoc(`${example ? `Description: ${description}` : ''}${example ? `\nExample: ${example}` : ''}`); + } + if (minItems) { + entityClass.addGetAccessor({ + isStatic: true, + name: `${toCamel(sPropName)}MinItems`, + statements: [`return ${minItems};`], + }); + } + if (maxItems) { + entityClass.addGetAccessor({ + isStatic: true, + name: `${toCamel(sPropName)}MaxItems`, + statements: [`return ${maxItems};`], + }); + } + if (typeof minLength === 'number') { + entityClass.addGetAccessor({ + isStatic: true, + name: `${toCamel(sPropName)}MinLength`, + statements: [`return ${minLength};`], + }); + } + if (maxLength) { + entityClass.addGetAccessor({ + isStatic: true, + name: `${toCamel(sPropName)}MaxLength`, + statements: [`return ${maxLength};`], + }); + } + if (typeof minimum === 'number') { + entityClass.addGetAccessor({ + isStatic: true, + name: `${toCamel(sPropName)}MinValue`, + statements: [`return ${minimum};`], + }); + } + if (maximum) { + entityClass.addGetAccessor({ + isStatic: true, + name: `${toCamel(sPropName)}MaxValue`, + statements: [`return ${maximum};`], + }); + } + + if (!(isArray && isClass) && !isClass) { + const isEnum = !isClass && isImport; + const isRequired = (required && required.includes(sPropName)) || sProps[sPropName].required; + const { maxLength, minLength, maximum, minimum } = sProps[sPropName]; + const haveValidationFields = maxLength || typeof minLength === 'number' || maximum || typeof minimum === 'number'; + if (isRequired || haveValidationFields) { + const prop = toCamel(sPropName); + const validateField = entityClass.addMethod({ + isStatic: true, + name: `${prop}Validate`, + returnType: `boolean`, + parameters: [{ + name: prop, + type: `${pType}${isArray ? '[]' : ''}${isRequred ? '' : ' | undefined'}`, + }], + }) + + validateField.setBodyText((w) => { + w.write('return '); + const nonRequiredCall = isRequired ? prop : `!${prop} ? true : ${prop}`; + if (pType === 'string') { + if (isArray) { + w.write(`${nonRequiredCall}.reduce((result, p) => result && (typeof p === 'string' && !!p.trim()), true)`); + } else { + if (typeof minLength === 'number' && maxLength) { + w.write(`(${nonRequiredCall}.length >${minLength > 0 ? '=' : ''} ${minLength}) && (${nonRequiredCall}.length <= ${maxLength})`); + } + if (typeof minLength !== 'number' || !maxLength) { + w.write(`${isRequired ? `typeof ${prop} === 'string'` : `!${prop} ? true : typeof ${prop} === 'string'`} && !!${nonRequiredCall}.trim()`); + } + } + } else if (pType === 'number') { + if (isArray) { + w.write(`${nonRequiredCall}.reduce((result, p) => result && typeof p === 'number', true)`); + } else { + if (typeof minimum === 'number' && maximum) { + w.write(`${isRequired ? `${prop} >= ${minimum} && ${prop} <= ${maximum}` : `!${prop} ? true : ((${prop} >= ${minimum}) && (${prop} <= ${maximum}))`}`); + } + if (typeof minimum !== 'number' || !maximum) { + w.write(`${isRequired ? `typeof ${prop} === 'number'` : `!${prop} ? true : typeof ${prop} === 'number'`}`); + } + } + } else if (pType === 'boolean') { + w.write(`${isRequired ? `typeof ${prop} === 'boolean'` : `!${prop} ? true : typeof ${prop} === 'boolean'`}`); + } else if (isEnum) { + if (isArray){ + w.write(`${nonRequiredCall}.reduce((result, p) => result && Object.keys(${pType}).includes(${prop}), true)`); + } else { + w.write(`${isRequired ? `Object.keys(${pType}).includes(${prop})` : `!${prop} ? true : typeof ${prop} === 'boolean'`}`); + } + } + + w.write(';'); + }); + } + } + }); + if (additionalProperties) { + const [ + pType, isArray, isClass, isImport, isAdditional + ] = schemaParamParser(additionalProperties, this.openapi); + const type = `Record`; + + entityClass.addProperty({ + name: additionalPropsOnly ? 'data' : `${uncapitalize(pType)}Data`, + isReadonly: true, + type: type, + }); + } + // add constructor; + const ctor = entityClass.addConstructor({ + parameters: [{ + name: 'props', + type: `I${sName}`, + }], + }); + ctor.setBodyText((w) => { + if (additionalProperties) { + const [ + pType, isArray, isClass, isImport, isAdditional + ] = schemaParamParser(additionalProperties, this.openapi); + w.writeLine(`this.${additionalPropsOnly ? 'data' : `${uncapitalize(pType)}Data`} = Object.entries(props).reduce>((prev, [key, value]) => {`); + if (isClass) { + w.writeLine(` prev[key] = new ${pType}(value!);`); + } else { + w.writeLine(' prev[key] = value!;') + } + w.writeLine(' return prev;'); + w.writeLine('}, {})'); + return; + } + sortedSProps.forEach((sPropName) => { + const [ + pType, isArray, isClass, , isAdditional + ] = schemaParamParser(sProps[sPropName], this.openapi); + const req = (required && required.includes(sPropName)) + || sProps[sPropName].required; + if (!req) { + if ((pType === 'boolean' || pType === 'number' || pType ==='string') && !isClass && !isArray) { + w.writeLine(`if (typeof props.${sPropName} === '${pType}') {`); + } else { + w.writeLine(`if (props.${sPropName}) {`); + } + } + if (isAdditional) { + if (isArray && isClass) { + w.writeLine(`${!req ? ' ' : ''}this._${sPropName} = props.${sPropName}.map((p) => Object.keys(p).reduce((prev, key) => { + return { ...prev, [key]: new ${pType}(p[key])}; + },{}))`); + } else if (isClass) { + w.writeLine(`${!req ? ' ' : ''}this._${sPropName} = Object.keys(props.${sPropName}).reduce((prev, key) => { + return { ...prev, [key]: new ${pType}(props.${sPropName}[key])}; + },{})`); + } else { + if (pType === 'string' && !isArray) { + w.writeLine(`${!req ? ' ' : ''}this._${sPropName} = Object.keys(props.${sPropName}).reduce((prev, key) => { + return { ...prev, [key]: props.${sPropName}[key].trim()}; + },{})`); + } else { + w.writeLine(`${!req ? ' ' : ''}this._${sPropName} = Object.keys(props.${sPropName}).reduce((prev, key) => { + return { ...prev, [key]: props.${sPropName}[key]}; + },{})`); + } + } + } else { + if (isArray && isClass) { + w.writeLine(`${!req ? ' ' : ''}this._${sPropName} = props.${sPropName}.map((p) => new ${pType}(p));`); + } else if (isClass) { + w.writeLine(`${!req ? ' ' : ''}this._${sPropName} = new ${pType}(props.${sPropName});`); + } else { + if (pType === 'string' && !isArray) { + w.writeLine(`${!req ? ' ' : ''}this._${sPropName} = props.${sPropName}.trim();`); + } else { + w.writeLine(`${!req ? ' ' : ''}this._${sPropName} = props.${sPropName};`); + } + } + } + if (!req) { + w.writeLine('}'); + } + }); + + }); + + // add serialize method; + const serialize = entityClass.addMethod({ + isStatic: false, + name: 'serialize', + returnType: `I${sName}`, + }); + serialize.setBodyText((w) => { + if (additionalProperties) { + const [ + pType, isArray, isClass, isImport, isAdditional + ] = schemaParamParser(additionalProperties, this.openapi); + w.writeLine(`return Object.entries(this.${additionalPropsOnly ? 'data' : `${uncapitalize(pType)}Data`}).reduce>((prev, [key, value]) => {`); + if (isClass) { + w.writeLine(` prev[key] = value.serialize();`); + } else { + w.writeLine(' prev[key] = value;') + } + w.writeLine(' return prev;'); + w.writeLine('}, {})'); + return; + } + w.writeLine(`const data: I${sName} = {`); + const unReqFields: string[] = []; + sortedSProps.forEach((sPropName) => { + const req = (required && required.includes(sPropName)) + || sProps[sPropName].required; + const [, isArray, isClass, , isAdditional] = schemaParamParser(sProps[sPropName], this.openapi); + if (!req) { + unReqFields.push(sPropName); + return; + } + if (isAdditional) { + if (isArray && isClass) { + w.writeLine(` ${sPropName}: this._${sPropName}.map((p) => Object.keys(p).reduce((prev, key) => ({ ...prev, [key]: p[key].serialize() }))),`); + } else if (isClass) { + w.writeLine(` ${sPropName}: Object.keys(this._${sPropName}).reduce>((prev, key) => ({ ...prev, [key]: this._${sPropName}[key].serialize() }), {}),`); + } else { + w.writeLine(` ${sPropName}: Object.keys(this._${sPropName}).reduce((prev, key) => ({ ...prev, [key]: this._${sPropName}[key] })),`); + } + } else { + if (isArray && isClass) { + w.writeLine(` ${sPropName}: this._${sPropName}.map((p) => p.serialize()),`); + } else if (isClass) { + w.writeLine(` ${sPropName}: this._${sPropName}.serialize(),`); + } else { + w.writeLine(` ${sPropName}: this._${sPropName},`); + } + } + + }); + w.writeLine('};'); + unReqFields.forEach((sPropName) => { + const [, isArray, isClass, , isAdditional] = schemaParamParser(sProps[sPropName], this.openapi); + w.writeLine(`if (typeof this._${sPropName} !== 'undefined') {`); + if (isAdditional) { + if (isArray && isClass) { + w.writeLine(` data.${sPropName} = this._${sPropName}.map((p) => Object.keys(p).reduce((prev, key) => ({ ...prev, [key]: p[key].serialize() }), {}));`); + } else if (isClass) { + w.writeLine(` data.${sPropName} = Object.keys(this._${sPropName}).reduce((prev, key) => ({ ...prev, [key]: this._${sPropName}[key].serialize() }), {});`); + } else { + w.writeLine(` data.${sPropName} = Object.keys(this._${sPropName}).reduce((prev, key) => ({ ...prev, [key]: this._${sPropName}[key] }), {});`); + } + } else { + if (isArray && isClass) { + w.writeLine(` data.${sPropName} = this._${sPropName}.map((p) => p.serialize());`); + } else if (isClass) { + w.writeLine(` data.${sPropName} = this._${sPropName}.serialize();`); + } else { + w.writeLine(` data.${sPropName} = this._${sPropName};`); + + } + } + + w.writeLine(`}`); + }); + w.writeLine('return data;'); + }); + + // add validate method + const validate = entityClass.addMethod({ + isStatic: false, + name: 'validate', + returnType: `string[]`, + }) + validate.setBodyText((w) => { + if (additionalPropsOnly) { + w.writeLine('return []') + return; + } + w.writeLine('const validate = {'); + Object.keys(sProps || {}).forEach((sPropName) => { + const [pType, isArray, isClass, , isAdditional] = schemaParamParser(sProps[sPropName], this.openapi); + + const { maxLength, minLength, maximum, minimum } = sProps[sPropName]; + + const isRequired = (required && required.includes(sPropName)) || sProps[sPropName].required; + const nonRequiredCall = isRequired ? `this._${sPropName}` : `!this._${sPropName} ? true : this._${sPropName}`; + + if (isArray && isClass) { + w.writeLine(` ${sPropName}: ${nonRequiredCall}.reduce((result, p) => result && p.validate().length === 0, true),`); + } else if (isClass && !isAdditional) { + w.writeLine(` ${sPropName}: ${nonRequiredCall}.validate().length === 0,`); + } else { + if (pType === 'string') { + if (isArray) { + w.writeLine(` ${sPropName}: ${nonRequiredCall}.reduce((result, p) => result && typeof p === 'string', true),`); + } else { + if (typeof minLength === 'number' && maxLength) { + w.writeLine(` ${sPropName}: (${nonRequiredCall}.length >${minLength > 0 ? '=' : ''} ${minLength}) && (${nonRequiredCall}.length <= ${maxLength}),`); + } + if (typeof minLength !== 'number' || !maxLength) { + w.writeLine(` ${sPropName}: ${isRequired ? `typeof this._${sPropName} === 'string'` : `!this._${sPropName} ? true : typeof this._${sPropName} === 'string'`} && !this._${sPropName} ? true : this._${sPropName},`); + } + } + } else if (pType === 'number') { + if (isArray) { + w.writeLine(` ${sPropName}: ${nonRequiredCall}.reduce((result, p) => result && typeof p === 'number', true),`); + } else { + if (typeof minimum === 'number' && maximum) { + w.writeLine(` ${sPropName}: ${isRequired ? `this._${sPropName} >= ${minimum} && this._${sPropName} <= ${maximum}` : `!this._${sPropName} ? true : ((this._${sPropName} >= ${minimum}) && (this._${sPropName} <= ${maximum}))`},`); + } + if (typeof minimum !== 'number' || !maximum) { + w.writeLine(` ${sPropName}: ${isRequired ? `typeof this._${sPropName} === 'number'` : `!this._${sPropName} ? true : typeof this._${sPropName} === 'number'`},`); + } + } + } else if (pType === 'boolean') { + w.writeLine(` ${sPropName}: ${isRequired ? `typeof this._${sPropName} === 'boolean'` : `!this._${sPropName} ? true : typeof this._${sPropName} === 'boolean'`},`); + } + } + }); + w.writeLine('};'); + w.writeLine('const isError: string[] = [];') + w.writeLine('Object.keys(validate).forEach((key) => {'); + w.writeLine(' if (!(validate as any)[key]) {'); + w.writeLine(' isError.push(key);'); + w.writeLine(' }'); + w.writeLine('});'); + w.writeLine('return isError;'); + + }); + + // add update method; + const update = entityClass.addMethod({ + isStatic: false, + name: 'update', + returnType: `${sName}`, + }); + update.addParameter({ + name: 'props', + type: additionalPropsOnly ? `I${sName}` : `Partial`, + }); + update.setBodyText((w) => { w.writeLine(`return new ${sName}({ ...this.serialize(), ...props });`); }); + + this.entities.push(entityFile); + }; + + save = () => { + this.entities.forEach(async (e) => { + await e.saveSync(); + }); + }; +} + +export default EntitiesGenerator; diff --git a/client2/scripts/generator/src/utils.ts b/client2/scripts/generator/src/utils.ts new file mode 100644 index 00000000..5183b6c5 --- /dev/null +++ b/client2/scripts/generator/src/utils.ts @@ -0,0 +1,83 @@ +const toCamel = (s: string) => { + return s.replace(/([-_][a-z])/ig, ($1) => { + return $1.toUpperCase() + .replace('-', '') + .replace('_', ''); + }); +}; +const capitalize = (s: string) => { + return s[0].toUpperCase() + s.slice(1); +}; +const uncapitalize = (s: string) => { + return s[0].toLowerCase() + s.slice(1); +}; +const TYPES = { + integer: 'number', + float: 'number', + number: 'number', + string: 'string', + boolean: 'boolean', +}; + +/** + * @param schemaProp: valueof shema.properties[key] + * @param openApi: openapi object + * @returns [propType - basicType or import one, isArray, isClass, isImport] + */ +const schemaParamParser = (schemaProp: any, openApi: any): [string, boolean, boolean, boolean, boolean] => { + let type = ''; + let isImport = false; + let isClass = false; + let isArray = false; + let isAdditional = false; + + if (schemaProp.$ref || schemaProp.additionalProperties?.$ref) { + const temp = (schemaProp.$ref || schemaProp.additionalProperties?.$ref).split('/'); + + if (schemaProp.additionalProperties) { + isAdditional = true; + } + + type = `${temp[temp.length - 1]}`; + + const cl = openApi ? openApi.components.schemas[type] : {}; + + if (cl.$ref) { + const link = schemaParamParser(cl, openApi); + link.shift(); + return [type, ...link] as any; + } + + if (cl.type === 'string' && cl.enum) { + isImport = true; + } + + if (cl.type === 'object' && !cl.oneOf) { + isClass = true; + isImport = true; + } else if (cl.type === 'array') { + const temp: any = schemaParamParser(cl.items, openApi); + type = `${temp[0]}`; + isArray = true; + isClass = isClass || temp[2]; + isImport = isImport || temp[3]; + } + } else if (schemaProp.type === 'array') { + const temp: any = schemaParamParser(schemaProp.items, openApi); + type = `${temp[0]}`; + isArray = true; + isClass = isClass || temp[2]; + isImport = isImport || temp[3]; + } else { + type = (TYPES as Record)[schemaProp.type]; + } + if (!type) { + // TODO: Fix bug with Error fields. + type = 'any'; + // throw new Error('Failed to find entity type'); + } + + return [type, isArray, isClass, isImport, isAdditional]; +}; + +export { TYPES, toCamel, capitalize, uncapitalize, schemaParamParser }; diff --git a/client2/scripts/helpers/checkTranslations.ts b/client2/scripts/helpers/checkTranslations.ts new file mode 100644 index 00000000..6634f65b --- /dev/null +++ b/client2/scripts/helpers/checkTranslations.ts @@ -0,0 +1,226 @@ +import * as fs from 'fs'; +import { + Project, + VariableStatement, + SyntaxKind, + Node, + Statement, + ts, + Identifier, + SourceFile, +} from 'ts-morph'; +import { + LOCALE_FOLDER_PATH, + TRANSLATOR_CLASS_NAME, + USE_INTL_NAME, + trimQuotes, +} from '../consts'; +import { checkForms, AvailableLocales } from '../../src/localization/Translator'; + +const project = new Project({ + tsConfigFilePath: './tsconfig.json', +}); + +let lang = 'ru'; +let option = ''; + +if (process.argv.length > 2) { + lang = process.argv[2]; + option = process.argv[3]; +} + +const usedTranslations: string[] = []; +const usedPluralTranslations: string[] = []; + +const problemFiles: string[] = []; +const sourceFiles = project.getSourceFiles(); +const sourceFilesWithIntl = sourceFiles.filter((sf) => { + return !!sf.getImportDeclarations().find((id) => { + return !!id.getNamedImports().find((ni) => ni.getName() === USE_INTL_NAME) + }) +}); +const getFileUsedIntl = (statements: Statement[]) => { + statements.forEach((s) => { + if (s instanceof VariableStatement) { + s.forEachDescendant((node) => { + let intVariableDeclaration: Identifier = null; + switch (node.getKind()) { + case SyntaxKind.VariableDeclaration: + if (node.getSymbol()) { + const name = node.getSymbol().getName(); + const callExp = node.getChildren().find((n) => n.getKind() === SyntaxKind.CallExpression); + if (callExp) { + const callExpIden = callExp.getChildren().find(n => n.getKind() === SyntaxKind.Identifier); + if (callExpIden && callExpIden.getSymbol().getName() === USE_INTL_NAME) { + intVariableDeclaration = node as Identifier; + } + } + } + break; + default: + break; + } + if (intVariableDeclaration) { + intVariableDeclaration.findReferencesAsNodes().forEach((fr) => { + if (fr instanceof Node) { + const parent = fr.getParentIfKind(SyntaxKind.PropertyAccessExpression); + if (parent && (parent.getName() === 'getMessage' || parent.getName() === 'getPlural')) { + const syntaxList = parent.getNextSiblings().find((n) => n.getKind() === SyntaxKind.SyntaxList); + if (syntaxList) { + const id = syntaxList.getChildren()[0]; + if (id && id.getKind() !== SyntaxKind.StringLiteral) { + problemFiles.push(fr.getSourceFile().getFilePath()); + } + if (id) { + usedTranslations.push(trimQuotes(id.getText())); + if (parent.getName() === 'getPlural') { + usedPluralTranslations.push(trimQuotes(id.getText())); + } + } + } + } + } + }) + } + }); + } + }) +} + +const getFileUsedTranslations = (file: SourceFile) => { + const namedImport = file.getImportDeclarations().find((id) => !!id.getNamedImports().find((ni) => ni.getName() === TRANSLATOR_CLASS_NAME)); + if (namedImport) { + const identifier = namedImport.getImportClause().getNamedImports().find((iden) => iden.getName() === TRANSLATOR_CLASS_NAME); + const translateReferences = identifier.getNodeProperty('name').findReferencesAsNodes(); + if (translateReferences.length > 0) { + translateReferences.forEach((identifierNode) => { + if (identifierNode.getParentIfKind(SyntaxKind.TypeReference)) { + const translatorVariable = identifierNode.getParent().getPreviousSibling().getPreviousSiblingIfKind(SyntaxKind.Identifier); + if (translatorVariable) { + translatorVariable.findReferencesAsNodes().forEach((node) => { + const parent = node.getParentIfKind(SyntaxKind.PropertyAccessExpression); + if (parent && (parent.getName() === 'getMessage' || parent.getName() === 'getPlural')) { + + const syntaxList = parent.getNextSiblings().find((n) => n.getKind() === SyntaxKind.SyntaxList); + if (syntaxList) { + const id = syntaxList.getChildren()[0]; + if (id && id.getKind() !== SyntaxKind.StringLiteral) { + problemFiles.push(parent.getSourceFile().getFilePath()); + } + if (id) { + usedTranslations.push(trimQuotes(id.getText())); + if (parent.getName() === 'getPlural') { + usedPluralTranslations.push(trimQuotes(id.getText())); + } + } + } + } + }) + } + } + }) + } + + } +} +sourceFilesWithIntl.forEach((file) => { + getFileUsedIntl(file.getStatements()); +}) + +const sourceFilesWithTranslator = project.getSourceFiles().filter((sf) => { + return !!sf.getImportDeclarations().find((id) => { + return !!id.getNamedImports().find((ni) => ni.getName() === TRANSLATOR_CLASS_NAME) + }) +}); +sourceFilesWithTranslator.forEach((file) => { + getFileUsedTranslations(file); +}) +const filteredUsedTranslations = Array.from(new Set(usedTranslations)); +const filteredUsedPluralTranslations = Array.from(new Set(usedPluralTranslations)); + +if (problemFiles.length) { + console.warn(`\n============== Files where translation id provided not as string ==============\n`); + console.log(problemFiles.join('\n')); + process.exit(255); +} + +const allFiles = fs.readdirSync(LOCALE_FOLDER_PATH); +// Use ru or needed language +const translationFile = allFiles.find((file) => file.includes(`${lang}.json`)); + +if (!translationFile) { + console.error('File not found'); + process.exit(255); +} + +const translationsObject = JSON.parse(fs.readFileSync(`./src/lib/intl/__locales/${translationFile}`, { flag: 'r+' }) as unknown as string); +const translations = { + locale: translationFile, + messages: Object.keys(translationsObject), +}; + +const someMessagesNotFound: string[] = []; +const notUsed: string[] = []; +const notFound: string[] = []; +const checkLocaleMessages = (locale: string, messages: string[]) => { + filteredUsedTranslations.forEach(f => { + if (!messages.includes(f)) { + notFound.push(f); + } + }); + messages.forEach(t => { + if (!filteredUsedTranslations.includes(t)) { + notUsed.push(t); + } + }); + if (notFound.length > 0) { + someMessagesNotFound.push(locale); + } +} + +const render = (data: string[], title: string) => { + console.log(`============ ${title} ============`); + console.table(data); + console.log(`============ ${title} ============`); +} + +checkLocaleMessages(translations.locale, translations.messages); + +const checkPluralForm = () => { + const pluralFormWrong: string[] = []; + filteredUsedPluralTranslations.forEach((id) => { + const message = translationsObject[id]; + if (!checkForms(message, lang as AvailableLocales, id)) { + pluralFormWrong.push(id) + } + }); + return pluralFormWrong; +} + +const plural = checkPluralForm(); +if (!option && (someMessagesNotFound.length || plural.length > 0 )) { + someMessagesNotFound.forEach(locale => console.error(`\nSome translatins for ${locale} was not found!\n`)); + plural.forEach(id => console.error(`\nTranslation with id: "${id}" - have wrong number of plural forms!\n`)); + process.exit(255); +} +if (option) { + switch (option) { + case '--show-missing': { + render(notFound, 'NotFound') + break; + } + case '--show-unused': { + render(notUsed, 'notUsed') + break; + } + case '--check-plurals': { + render(plural, 'Wrong Plural Form') + } + default: { + if (someMessagesNotFound.length) { + someMessagesNotFound.forEach(locale => console.error(`\nSome translatins for ${locale} was not found!\n\n`)); + process.exit(255); + } + } + } +} diff --git a/client2/scripts/lint/common.js b/client2/scripts/lint/common.js new file mode 100644 index 00000000..759fa44f --- /dev/null +++ b/client2/scripts/lint/common.js @@ -0,0 +1,79 @@ +module.exports = { + parser: '@typescript-eslint/parser', + parserOptions: { + project: './tsconfig.json', + ecmaFeatures: { + jsx: true + }, + extraFileExtensions: ['mjs', 'tsx', 'ts'], + ecmaVersion: 2020, + sourceType: 'module' + }, + plugins: ['react', '@typescript-eslint', 'import'], + env: { + browser: true, + commonjs: true, + es6: true, + es2020: true, + jest: true, + }, + settings: { + react: { + pragma: 'React', + version: 'detect', + }, + 'import/resolver': { + typescript: { + alwaysTryTypes: true + } + }, + 'import/parsers': { + '@typescript-eslint/parser': ['.ts', '.tsx'], + }, + }, + rules: { + '@typescript-eslint/explicit-module-boundary-types': 0, + '@typescript-eslint/explicit-function-return-type': [0, { allowExpressions: true }], + '@typescript-eslint/indent': ['error', 4], + '@typescript-eslint/interface-name-prefix': [0, { prefixWithI: 'never' }], + '@typescript-eslint/no-explicit-any': [0], + '@typescript-eslint/naming-convention': [2, { + selector: 'enum', format: ['UPPER_CASE', 'PascalCase'], + }], + '@typescript-eslint/no-non-null-assertion': 0, + 'arrow-body-style': 'off', + 'consistent-return': 0, + curly: [2, 'all'], + 'default-case': 0, + 'import/no-cycle': 0, + 'import/prefer-default-export': 'off', + 'import/no-named-as-default': 0, + indent: [0, 4], + 'no-alert': 2, + 'no-console': 2, + 'no-debugger': 2, + 'no-underscore-dangle': 'off', + 'no-useless-escape': 'off', + 'object-curly-newline': 'off', + 'react-hooks/exhaustive-deps': 0, + 'react/display-name': 0, + 'react/jsx-indent-props': ['error', 4], + 'react/jsx-indent': ['error', 4], + 'react/jsx-one-expression-per-line': 'off', + 'react/jsx-props-no-spreading': 0, + 'react/prop-types': 'off', + 'react/state-in-constructor': 'off', + }, + extends: [ + 'airbnb-base', + 'airbnb-typescript/base', + 'airbnb/hooks', + 'plugin:react/recommended', + 'plugin:@typescript-eslint/eslint-recommended', + 'plugin:@typescript-eslint/recommended', + 'plugin:import/errors', + 'plugin:import/warnings', + 'plugin:import/typescript', + ], + globals: {}, +}; diff --git a/client2/scripts/lint/dev.js b/client2/scripts/lint/dev.js new file mode 100644 index 00000000..33784298 --- /dev/null +++ b/client2/scripts/lint/dev.js @@ -0,0 +1,10 @@ +module.exports = { + rules: { + 'no-alert': 0, + 'no-debugger': 0, + 'no-console': 0, + }, + extends: [ + './common', + ], +}; diff --git a/client2/scripts/lint/prod.js b/client2/scripts/lint/prod.js new file mode 100644 index 00000000..f31bae1f --- /dev/null +++ b/client2/scripts/lint/prod.js @@ -0,0 +1,5 @@ +module.exports = { + extends: [ + './common.js', + ], +}; diff --git a/client2/scripts/webpack/helpers.js b/client2/scripts/webpack/helpers.js new file mode 100644 index 00000000..2a72e0b6 --- /dev/null +++ b/client2/scripts/webpack/helpers.js @@ -0,0 +1,40 @@ +const yaml = require('yaml'); +const fs = require('fs'); + +const ZERO_HOST = '0.0.0.0'; +const LOCALHOST = '127.0.0.1'; +const DEFAULT_PORT = 80; + +const importConfig = () => { + try { + const doc = yaml.parse(fs.readFileSync('../AdguardHome.yaml', 'utf8')); + const { bind_host, bind_port } = doc; + return { + bind_host, + bind_port, + }; + } catch (e) { + return { + bind_host: ZERO_HOST, + bind_port: DEFAULT_PORT, + }; + } +}; + +const getDevServerConfig = () => { + const { bind_host: host, bind_port: port } = importConfig(); + const { DEV_SERVER_PORT } = process.env; + + const devServerHost = host === ZERO_HOST ? LOCALHOST : host; + const devServerPort = 3000 || port + 8000; + + return { + host: devServerHost, + port: devServerPort + }; +}; + +module.exports = { + importConfig, + getDevServerConfig +}; diff --git a/client2/scripts/webpack/webpack.config.base.js b/client2/scripts/webpack/webpack.config.base.js new file mode 100644 index 00000000..babb4f54 --- /dev/null +++ b/client2/scripts/webpack/webpack.config.base.js @@ -0,0 +1,74 @@ +const path = require('path'); +const AntdDayjsWebpackPlugin = require('antd-dayjs-webpack-plugin'); +const HtmlWebpackPlugin = require('html-webpack-plugin'); +const tsconfig = require('../../tsconfig.json'); + +const RESOURCES_PATH = path.resolve(__dirname, '../../'); +const HTML_PATH = path.resolve(RESOURCES_PATH, 'public/index.html'); +const HTML_INSTALL_PATH = path.resolve(RESOURCES_PATH, 'public/install.html'); + +module.exports = { + entry: { + install: './src/Install.tsx', + main: './src/App.tsx' + }, + resolve: { + extensions: ['.tsx', '.ts', '.js', '.pcss'], + alias: Object.keys(tsconfig.compilerOptions.paths).reduce((aliases, key) => { + // Reduce to load aliases from ./tsconfig.json in appropriate for webpack form + const paths = tsconfig.compilerOptions.paths[key].map(p => p.replace('/*', '')); + aliases[key.replace('/*', '')] = path.resolve( + __dirname, + '../../', + tsconfig.compilerOptions.baseUrl, + ...paths, + ); + return aliases; + }, {}), + }, + module: { + rules: [ + { + test: /\.tsx?$/, + use: 'ts-loader', + exclude: /node_modules/, + }, + { + test: /\.(woff|woff2)$/, + use: [{ + loader: 'file-loader', + options:{ + outputPath:'./', + } + }], + }, + { + test:/\.(png|jpe?g|gif)$/, + exclude: /(node_modules)/, + use:[{ + loader:'file-loader', + options:{ + outputPath:'./images', + } + }] + } + ], + }, + + plugins: [ + // new AntdDayjsWebpackPlugin() + new HtmlWebpackPlugin({ + inject: true, + cache: false, + chunks: ['main'], + template: HTML_PATH, + }), + new HtmlWebpackPlugin({ + inject: true, + cache: false, + chunks: ['install'], + filename: 'install.html', + template: HTML_INSTALL_PATH, + }), + ], +}; diff --git a/client2/scripts/webpack/webpack.config.dev.js b/client2/scripts/webpack/webpack.config.dev.js new file mode 100644 index 00000000..2d5c1c8a --- /dev/null +++ b/client2/scripts/webpack/webpack.config.dev.js @@ -0,0 +1,114 @@ +const history = require('connect-history-api-fallback'); +const { merge } = require('webpack-merge'); +const path = require('path'); +const proxy = require('http-proxy-middleware'); +const Webpack = require('webpack'); + +const { getDevServerConfig } = require('./helpers'); +const baseConfig = require('./webpack.config.base'); + +const devHost = process.env.DEV_HOST +const target = getDevServerConfig(); + +const options = { + target: devHost || `http://${target.host}:${target.port}`, // target host + changeOrigin: true, // needed for virtual hosted sites +}; +const apiProxy = proxy.createProxyMiddleware(options); + +module.exports = merge(baseConfig, { + mode: 'development', + output: { + path: path.resolve(__dirname, '../../build2'), + filename: '[name].bundle.js', + }, + optimization: { + noEmitOnErrors: true, + }, + devServer: { + port: 4000, + historyApiFallback: true, + before: (app) => { + app.use('/control', apiProxy); + app.use(history({ + rewrites: [ + { + from: /\.(png|jpe?g|gif)$/, + to: (context) => { + const name = context.parsedUrl.pathname.split('/'); + return `/images/${name[name.length - 1]}` + } + }, { + from: /\.(woff|woff2)$/, + to: (context) => { + const name = context.parsedUrl.pathname.split('/'); + return `/${name[name.length - 1]}` + } + }, { + from: /\.(js|css)$/, + to: (context) => { + const name = context.parsedUrl.pathname.split('/'); + return `/${name[name.length - 1]}` + } + } + ], + })); + } + }, + devtool: 'eval-source-map', + module: { + rules: [ + { + enforce: 'pre', + test: /\.tsx?$/, + exclude: /node_modules/, + loader: 'eslint-loader', + options: { + configFile: path.resolve(__dirname, '../lint/dev.js'), + } + }, + { + test: (resource) => { + return ( + resource.indexOf('.pcss')+1 + || resource.indexOf('.css')+1 + || resource.indexOf('.less')+1 + ) && !(resource.indexOf('.module.')+1); + }, + use: ['style-loader', 'css-loader', 'postcss-loader', { + loader: 'less-loader', + options: { + javascriptEnabled: true, + }, + }], + }, + { + test: /\.module\.p?css$/, + use: [ + 'style-loader', + { + loader: 'css-loader', + options: { + modules: true, + sourceMap: true, + importLoaders: 1, + modules: { + localIdentName: "[name]__[local]___[hash:base64:5]", + } + }, + }, + 'postcss-loader', + ], + exclude: /node_modules/, + }, + ] + }, + plugins: [ + new Webpack.DefinePlugin({ + DEV: true, + 'process.env.DEV_SERVER_PORT': JSON.stringify(3000), + }), + new Webpack.HotModuleReplacementPlugin(), + new Webpack.ProgressPlugin(), + ], +}); diff --git a/client2/scripts/webpack/webpack.config.prod.js b/client2/scripts/webpack/webpack.config.prod.js new file mode 100644 index 00000000..c0bddae4 --- /dev/null +++ b/client2/scripts/webpack/webpack.config.prod.js @@ -0,0 +1,85 @@ +const path = require('path'); +const { merge } = require('webpack-merge'); +const baseConfig = require('./webpack.config.base'); +const MiniCssExtractPlugin = require("mini-css-extract-plugin"); +const OptimizeCSSAssetsPlugin = require('optimize-css-assets-webpack-plugin'); +const TerserJSPlugin = require('terser-webpack-plugin'); +const Webpack = require('webpack'); +const CopyPlugin = require('copy-webpack-plugin'); + +module.exports = merge(baseConfig, { + mode: 'production', + devtool: 'source-map', + output: { + path: path.resolve(__dirname, '../../../build2/static'), + filename: '[name].bundle.[hash:5].js', + publicPath: '/' + }, + optimization: { + minimizer: [new TerserJSPlugin({terserOptions: { + output: { + comments: false, + }, + }, + extractComments: false, + }), new OptimizeCSSAssetsPlugin({})], + splitChunks: { + cacheGroups: { + styles: { + name: 'styles', + test: /\.css$/, + chunks: 'all', + enforce: true, + }, + }, + }, + }, + module: { + rules: [ + { + test: (resource) => { + return ( + resource.indexOf('.pcss')+1 + || resource.indexOf('.css')+1 + || resource.indexOf('.less')+1 + ) && !(resource.indexOf('.module.')+1); + }, + use: [{ + loader: MiniCssExtractPlugin.loader, + }, 'css-loader', 'postcss-loader', { + loader: 'less-loader', + options: { + javascriptEnabled: true, + }, + }], + exclude: /node_modules/, + }, + { + test: /\.module\.p?css$/, + use: [ + { + loader: MiniCssExtractPlugin.loader, + }, + { + loader: 'css-loader', + options: { + modules: true, + sourceMap: true, + importLoaders: 1, + }, + }, + 'postcss-loader', + ], + exclude: /node_modules/, + } + ] + }, + plugins: [ + new Webpack.DefinePlugin({ + DEV: false, + }), + new MiniCssExtractPlugin({ + filename: '[name].[hash:5].css', + }), + ] +}); diff --git a/client2/src/App.tsx b/client2/src/App.tsx new file mode 100644 index 00000000..f01e3253 --- /dev/null +++ b/client2/src/App.tsx @@ -0,0 +1,18 @@ +import './main.pcss'; +import './lib/ant/ant.less'; +import React from 'react'; +import ReactDOM from 'react-dom'; +import Store, { storeValue } from 'Store'; +import './lib/ant'; + +import App from './components/App'; + +const Container = () => { + return ( + + + + ); +}; + +ReactDOM.render(, document.getElementById('app')); diff --git a/client2/src/Install.tsx b/client2/src/Install.tsx new file mode 100644 index 00000000..ccc61315 --- /dev/null +++ b/client2/src/Install.tsx @@ -0,0 +1,18 @@ +import './main.pcss'; +import './lib/ant/ant.less'; +import React from 'react'; +import ReactDOM from 'react-dom'; +import Store, { storeValue } from 'Store/installStore'; +import './lib/ant'; + +import Install from './components/Install'; + +const Container = () => { + return ( + + + + ); +}; + +ReactDOM.render(, document.getElementById('app')); diff --git a/client2/src/assets/img/install.png b/client2/src/assets/img/install.png new file mode 100644 index 00000000..68dbd780 Binary files /dev/null and b/client2/src/assets/img/install.png differ diff --git a/client2/src/components/App/App.tsx b/client2/src/components/App/App.tsx new file mode 100644 index 00000000..cddb50c6 --- /dev/null +++ b/client2/src/components/App/App.tsx @@ -0,0 +1,20 @@ +import React, { FC } from 'react'; +import { BrowserRouter } from 'react-router-dom'; + +import Icons from 'Common/ui/Icons'; +import Routes from './Routes'; + +import { ErrorBoundary } from './Errors'; + +const App: FC = () => { + return ( + + + + + + + ); +}; + +export default App; diff --git a/client2/src/components/App/Dashboard/Dashboard.tsx b/client2/src/components/App/Dashboard/Dashboard.tsx new file mode 100644 index 00000000..02846ca1 --- /dev/null +++ b/client2/src/components/App/Dashboard/Dashboard.tsx @@ -0,0 +1,136 @@ +import React, { FC, useContext } from 'react'; +import { Row, Col } from 'antd'; +import { observer } from 'mobx-react-lite'; + +import Store from 'Store'; +import { InnerLayout } from 'Common/ui/layouts'; +import theme from 'Lib/theme'; +import { BlockCard, TopDomains, BlockedQueries, TopClients, ServerStatistics } from './components'; + +const Dashboard:FC = observer(() => { + const store = useContext(Store); + const { + dashboard: { stats, filteringConfig }, + system: { status }, + ui: { intl }, + } = store; + + if (!stats || !filteringConfig) { + return null; + } + + const { + numBlockedFiltering, + numReplacedParental, + numReplacedSafebrowsing, + replacedParental, + replacedSafebrowsing, + avgProcessingTime, + blockedFiltering, + + topBlockedDomains, + topQueriedDomains, + dnsQueries, + numDnsQueries, + + } = stats; + + const { filters } = filteringConfig!; + const allFilters = filters?.length; + const allRules = filters?.reduce((prev, e) => prev + (e.rulesCount || 0), 0); + const enabled = filters?.filter((e) => e.enabled).length; + + return ( + +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + {/* TODO: fix chart */} + + + + + +
    +
    + ); +}); + +export default Dashboard; diff --git a/client2/src/components/App/Dashboard/components/BlockCard/BlockCard.module.pcss b/client2/src/components/App/Dashboard/components/BlockCard/BlockCard.module.pcss new file mode 100644 index 00000000..62eb6b4f --- /dev/null +++ b/client2/src/components/App/Dashboard/components/BlockCard/BlockCard.module.pcss @@ -0,0 +1,20 @@ +.container { + display: flex; + flex-flow: column; + padding: 24px; + background-color: var(--white); +} + +.title { + font-size: 14px; + line-height: 22px; + margin-bottom: 4px; + color: var(--gray700); +} + +.overal { + font-size: 30px; + line-height: 38px; + margin-bottom: 18px; + color: var(--gray900); +} \ No newline at end of file diff --git a/client2/src/components/App/Dashboard/components/BlockCard/BlockCard.tsx b/client2/src/components/App/Dashboard/components/BlockCard/BlockCard.tsx new file mode 100644 index 00000000..b962db01 --- /dev/null +++ b/client2/src/components/App/Dashboard/components/BlockCard/BlockCard.tsx @@ -0,0 +1,35 @@ +import React, { FC } from 'react'; +import { AreaChart, Area, ResponsiveContainer } from 'recharts'; + +import s from './BlockCard.module.pcss'; + +interface BlockCardProps { + overal: number | string; + data?: number[]; + text?: string; + color?: string; + title: string; +} + +const BlockCard: FC = ({ overal, data, color, title, text }) => { + return ( +
    +
    {title}
    +
    {overal}
    + {data && ( + + ({ name: 'data', value: n }))}> + + + + )} + {text && ( +
    + {text} +
    + )} +
    + ); +}; + +export default BlockCard; diff --git a/client2/src/components/App/Dashboard/components/BlockCard/index.ts b/client2/src/components/App/Dashboard/components/BlockCard/index.ts new file mode 100644 index 00000000..085552ae --- /dev/null +++ b/client2/src/components/App/Dashboard/components/BlockCard/index.ts @@ -0,0 +1 @@ +export { default as BlockCard } from './BlockCard'; diff --git a/client2/src/components/App/Dashboard/components/BlockedQueries/BlockedQueries.module.pcss b/client2/src/components/App/Dashboard/components/BlockedQueries/BlockedQueries.module.pcss new file mode 100644 index 00000000..7520f474 --- /dev/null +++ b/client2/src/components/App/Dashboard/components/BlockedQueries/BlockedQueries.module.pcss @@ -0,0 +1,16 @@ +.container { + display: flex; + flex-flow: column; + padding: 24px; + background-color: var(--white); +} + +.title { + font-size: 14px; + line-height: 22px; + margin-bottom: 4px; + color: var(--gray700); +} +.pie { + padding: 34px 0px; +} \ No newline at end of file diff --git a/client2/src/components/App/Dashboard/components/BlockedQueries/BlockedQueries.tsx b/client2/src/components/App/Dashboard/components/BlockedQueries/BlockedQueries.tsx new file mode 100644 index 00000000..7b8eec9d --- /dev/null +++ b/client2/src/components/App/Dashboard/components/BlockedQueries/BlockedQueries.tsx @@ -0,0 +1,76 @@ +import theme from 'Lib/theme'; +import React, { FC, useContext, useState } from 'react'; +import { PieChart, Pie, ResponsiveContainer, Sector, Cell } from 'recharts'; + +import Store from 'Store'; + +import s from './BlockedQueries.module.pcss'; + +interface BlockCardProps { + ads: number; + trackers: number; + other: number; +} + +const renderActiveShape = (props: any): any => { + const { + cx, cy, innerRadius, outerRadius, startAngle, endAngle, + fill, payload, percent, + } = props; + return ( + + {payload.name} + {Math.round(percent * 100)}% + + + ); +}; + +const BlockedQueries: FC = ({ ads, trackers, other }) => { + const store = useContext(Store); + const [activeIndex, setActiveIndex] = useState(0); + const { ui: { intl } } = store; + const data = [ + { name: intl.getMessage('other'), value: other, color: theme.chartColors.gray700 }, + { name: intl.getMessage('ads'), value: ads, color: theme.chartColors.red }, + { name: intl.getMessage('trackers'), value: trackers, color: theme.chartColors.orange }, + ]; + const onChart: any = (_: any, index: number) => { + setActiveIndex(index); + }; + return ( +
    +
    {intl.getMessage('dashboard_blocked_queries')}
    +
    + + + + {data.map((entry, index) => ( + + ))} + + + +
    +
    + ); +}; + +export default BlockedQueries; diff --git a/client2/src/components/App/Dashboard/components/BlockedQueries/index.ts b/client2/src/components/App/Dashboard/components/BlockedQueries/index.ts new file mode 100644 index 00000000..019ab9c9 --- /dev/null +++ b/client2/src/components/App/Dashboard/components/BlockedQueries/index.ts @@ -0,0 +1 @@ +export { default as BlockedQueries } from './BlockedQueries'; diff --git a/client2/src/components/App/Dashboard/components/ServerStatistics/ServerStatistics.module.pcss b/client2/src/components/App/Dashboard/components/ServerStatistics/ServerStatistics.module.pcss new file mode 100644 index 00000000..cc8003da --- /dev/null +++ b/client2/src/components/App/Dashboard/components/ServerStatistics/ServerStatistics.module.pcss @@ -0,0 +1,46 @@ +.container { + display: flex; + flex-flow: column; + background-color: var(--white); + margin-top: 24px; +} + +.title { + padding: 24px; + font-size: 16px; + font-weight: 500; + line-height: 22px; + border-bottom: 1px solid var(--gray300); + color: var(--gray900); +} + +.card { + padding: 24px; + height: 100%; +} + +.cardBorder { + border-right: 1px solid var(--gray300); + + &:last-of-type { + border-right: 0; + } +} + +.cardTitle { + font-weight: 500; + margin-bottom: 12px; +} + +.cardDesc { + color: var(--gray700); +} + +.cardValue { + color: var(--gray900); + font-size: 30px; +} + +.chart { + margin-top: 24px; +} \ No newline at end of file diff --git a/client2/src/components/App/Dashboard/components/ServerStatistics/ServerStatistics.tsx b/client2/src/components/App/Dashboard/components/ServerStatistics/ServerStatistics.tsx new file mode 100644 index 00000000..1ec98476 --- /dev/null +++ b/client2/src/components/App/Dashboard/components/ServerStatistics/ServerStatistics.tsx @@ -0,0 +1,89 @@ +import React, { FC, useContext } from 'react'; +import { Row, Col } from 'antd'; +import { AreaChart, Area, ResponsiveContainer } from 'recharts'; + +import Store from 'Store'; +import theme from 'Lib/theme'; + +import s from './ServerStatistics.module.pcss'; + +const ServerStatistics: FC = () => { + const store = useContext(Store); + const { ui: { intl } } = store; + + const data = [0, 10, 2, 14, 12, 24, 5, 8, 10, 0, 3, 5, 7, 8, 3]; + return ( +
    +
    {intl.getMessage('dashboard_server_statistics')}
    + + +
    +
    + Average server load +
    +
    +
    + Processes: 213 +
    +
    + Cores: 2 +
    +
    + + ({ name: 'data', value: n }))}> + + + +
    + + +
    +
    + Memory usage +
    +
    + 236 Mb +
    + + ({ name: 'data', value: n }))}> + + + +
    + + +
    +
    + DNS cashe size +
    +
    + 2 363 records +
    +
    +
    + 32 Mb +
    +
    +
    + + +
    +
    + Upstream servers data +
    +
    +
    + Processes: 213 +
    +
    + Cores: 2 +
    +
    +
    + +
    +
    + ); +}; + +export default ServerStatistics; diff --git a/client2/src/components/App/Dashboard/components/ServerStatistics/index.ts b/client2/src/components/App/Dashboard/components/ServerStatistics/index.ts new file mode 100644 index 00000000..98228599 --- /dev/null +++ b/client2/src/components/App/Dashboard/components/ServerStatistics/index.ts @@ -0,0 +1 @@ +export { default as ServerStatistics } from './ServerStatistics'; diff --git a/client2/src/components/App/Dashboard/components/TopClients/TopClients.module.pcss b/client2/src/components/App/Dashboard/components/TopClients/TopClients.module.pcss new file mode 100644 index 00000000..d4b4b01a --- /dev/null +++ b/client2/src/components/App/Dashboard/components/TopClients/TopClients.module.pcss @@ -0,0 +1,43 @@ +.container { + display: flex; + flex-flow: column; + background-color: var(--white); +} + +.title { + font-size: 16px; + line-height: 22px; + margin-bottom: 4px; + padding: 24px; + color: var(--gray900); +} + +.table { + position: relative; +} + +.tableTitle { + color: var(--gray700); + background-color: #fafafa; + padding: 24px; + position: sticky; + top: 0; +} + +.tableGrid { + display: grid; + grid-template-columns: 4fr 1fr 1fr 1.5fr 1fr .5fr; + padding: 16px 24px; + border-bottom: 1px solid var(--gray300); + + &:last-of-type { + border-bottom: 0; + } + + > div { + align-self: center; + } +} +.ids { + color: var(--gray700) +} \ No newline at end of file diff --git a/client2/src/components/App/Dashboard/components/TopClients/TopClients.tsx b/client2/src/components/App/Dashboard/components/TopClients/TopClients.tsx new file mode 100644 index 00000000..9e743964 --- /dev/null +++ b/client2/src/components/App/Dashboard/components/TopClients/TopClients.tsx @@ -0,0 +1,71 @@ +import React, { FC, useContext } from 'react'; +import { Button } from 'antd'; +import cn from 'classnames'; +import { observer } from 'mobx-react-lite'; + +import Store from 'Store'; + +import s from './TopClients.module.pcss'; + +const TopClients: FC = observer(() => { + const store = useContext(Store); + const { ui: { intl }, dashboard } = store; + const { clientsInfo, stats } = dashboard; + const topClients = new Map(); + stats?.topClients?.forEach((client) => { + const [id, requests] = Object.entries(client.numberData); + topClients.set(id, requests); + }); + const clients = Array.from(clientsInfo.entries()); + + return ( +
    +
    {intl.getMessage('Top Clients')}
    +
    +
    +
    {intl.getMessage('client_table_header')}
    +
    {intl.getMessage('requests')}
    +
    {intl.getMessage('show_blocked_responses')}
    +
    %
    +
    +
    +
    + {clients.map(([id, c]) => { + const request = topClients.get(id); + return ( +
    +
    + {c.name} +
    + {c.ids?.map((cid) => ( +
    {cid}
    + ))} +
    +
    +
    + {request} +
    +
    + API + {/* TODO: api */} +
    +
    + API / {request} +
    +
    + +
    +
    + ... +
    +
    + ); + })} +
    +
    + ); +}); + +export default TopClients; diff --git a/client2/src/components/App/Dashboard/components/TopClients/index.ts b/client2/src/components/App/Dashboard/components/TopClients/index.ts new file mode 100644 index 00000000..b495d493 --- /dev/null +++ b/client2/src/components/App/Dashboard/components/TopClients/index.ts @@ -0,0 +1 @@ +export { default as TopClients } from './TopClients'; diff --git a/client2/src/components/App/Dashboard/components/TopDomains/TopDomains.module.pcss b/client2/src/components/App/Dashboard/components/TopDomains/TopDomains.module.pcss new file mode 100644 index 00000000..b61ab72a --- /dev/null +++ b/client2/src/components/App/Dashboard/components/TopDomains/TopDomains.module.pcss @@ -0,0 +1,62 @@ +.container { + display: flex; + flex-flow: column; + background-color: var(--white); +} + +.title { + padding: 24px; + font-size: 16px; + font-weight: 500; + line-height: 22px; + border-bottom: 1px solid var(--gray300); + margin-bottom: 16px; + color: var(--gray900); +} + +.content { + padding: 24px; + +} + +.overal { + font-size: 24px; + line-height: 32px; + margin-bottom: 24px; + color: var(--gray900); +} + +.table { + position: relative; + overflow-y: auto; + max-height: 280px; + width: 100%; +} + +.tableHeader { + /* TODO: color */ + position: sticky; + top: 0; + width: inherit; + background-color: #fafafa; + font-weight: 500; + z-index: 10; +} + +.tableRow { + display: grid; + grid-template-columns: 3fr 1fr 1.5fr; + grid-column-gap: 10px; + padding: 8px 16px; + border-bottom: 1px solid var(--gray300); +} + +.domain { + text-overflow: ellipsis; + overflow: hidden; + white-space: nowrap; +} + +.progress { + display: flex; +} \ No newline at end of file diff --git a/client2/src/components/App/Dashboard/components/TopDomains/TopDomains.tsx b/client2/src/components/App/Dashboard/components/TopDomains/TopDomains.tsx new file mode 100644 index 00000000..35fe686f --- /dev/null +++ b/client2/src/components/App/Dashboard/components/TopDomains/TopDomains.tsx @@ -0,0 +1,73 @@ +import React, { FC, useContext } from 'react'; +import { Progress } from 'antd'; +import cn from 'classnames'; +import { AreaChart, Area, ResponsiveContainer } from 'recharts'; + +import TopArrayEntry from 'Entities/TopArrayEntry'; +import theme from 'Lib/theme'; +import Store from 'Store'; + +import s from './TopDomains.module.pcss'; + +interface TopDomainsProps { + title: string; + overal: number; + chartData: number[]; + tableData: TopArrayEntry[]; + color: string; + useValueColor?: boolean; +} + +const TopDomains: FC = ( + { title, overal, chartData, tableData, color, useValueColor }, +) => { + const store = useContext(Store); + const { ui: { intl } } = store; + const data = tableData.map((e) => { + const [domain, value] = Object.entries(e.numberData)[0]; + return { domain, value }; + }); + + return ( +
    +
    {title}
    +
    +
    + {overal.toLocaleString('en')} + + ({ name: 'data', value: n }))}> + + + +
    +
    +
    +
    + {intl.getMessage('domain')} +
    +
    + {intl.getMessage('all_queries')} +
    +
    + % +
    +
    + {data.map(({ domain, value }) => ( +
    +
    {domain}
    +
    {value}
    + +
    + ))} +
    +
    +
    + ); +}; + +export default TopDomains; diff --git a/client2/src/components/App/Dashboard/components/TopDomains/index.ts b/client2/src/components/App/Dashboard/components/TopDomains/index.ts new file mode 100644 index 00000000..79fcc1b8 --- /dev/null +++ b/client2/src/components/App/Dashboard/components/TopDomains/index.ts @@ -0,0 +1 @@ +export { default as TopDomains } from './TopDomains'; diff --git a/client2/src/components/App/Dashboard/components/index.ts b/client2/src/components/App/Dashboard/components/index.ts new file mode 100644 index 00000000..9fc2523c --- /dev/null +++ b/client2/src/components/App/Dashboard/components/index.ts @@ -0,0 +1,5 @@ +export { BlockCard } from './BlockCard'; +export { TopClients } from './TopClients'; +export { TopDomains } from './TopDomains'; +export { BlockedQueries } from './BlockedQueries'; +export { ServerStatistics } from './ServerStatistics'; diff --git a/client2/src/components/App/Dashboard/index.ts b/client2/src/components/App/Dashboard/index.ts new file mode 100644 index 00000000..449ae567 --- /dev/null +++ b/client2/src/components/App/Dashboard/index.ts @@ -0,0 +1 @@ +export { default } from './Dashboard'; diff --git a/client2/src/components/App/Errors/ErrorBoundary.tsx b/client2/src/components/App/Errors/ErrorBoundary.tsx new file mode 100644 index 00000000..42204783 --- /dev/null +++ b/client2/src/components/App/Errors/ErrorBoundary.tsx @@ -0,0 +1,31 @@ +import React, { Component, ReactNode } from 'react'; +import cn from 'classnames'; + +import s from './Errors.module.pcss'; + +export default class ErrorBoundary extends Component { + state = { + isError: false, + }; + + static getDerivedStateFromError(): { isError: boolean } { + return { isError: true }; + } + + render(): ReactNode { + const { isError } = this.state; + const { children } = this.props; + + if (isError) { + return ( +
    +
    + Something went wrong +
    +
    + ); + } + + return children; + } +} diff --git a/client2/src/components/App/Errors/Errors.module.pcss b/client2/src/components/App/Errors/Errors.module.pcss new file mode 100644 index 00000000..92e6f908 --- /dev/null +++ b/client2/src/components/App/Errors/Errors.module.pcss @@ -0,0 +1,79 @@ +.content { + display: flex; + align-items: center; + justify-content: center; + flex-direction: column; + max-width: 455px; + min-height: calc(100vh - var(--header-height) - 64px); + margin: 0 auto; + text-align: center; + + &_boundary { + min-height: 100vh; + } +} + +.title { + margin-bottom: 8px; + font-size: 18px; + font-weight: 500; + + @media (--s-viewport) { + margin-bottom: 20px; + font-size: 24px; + } +} + +.code { + position: relative; + margin-bottom: 32px; + font-size: 120px; + font-weight: 700; + line-height: 108px; + color: var(--morning); + user-select: none; + + @media (--s-viewport) { + margin-bottom: 54px; + font-size: 180px; + line-height: 162px; + } +} + +.warning { + width: 160px; + height: 173px; + + @media (--s-viewport) { + width: 243px; + height: 262px; + } + + &_code { + position: absolute; + top: -20px; + left: 50%; + transform: translateX(-50%); + + @media (--s-viewport) { + top: -34px; + } + } +} + +.error { + margin-bottom: 10px; + cursor: pointer; +} + +.desc { + margin-bottom: 8px; + max-width: 384px; + font-size: 13px; + color: var(--gray); + + @media (--s-viewport) { + margin-bottom: 20px; + font-size: 14px; + } +} diff --git a/client2/src/components/App/Errors/index.ts b/client2/src/components/App/Errors/index.ts new file mode 100644 index 00000000..e5d6dda2 --- /dev/null +++ b/client2/src/components/App/Errors/index.ts @@ -0,0 +1 @@ +export { default as ErrorBoundary } from './ErrorBoundary'; diff --git a/client2/src/components/App/Header/Header.module.pcss b/client2/src/components/App/Header/Header.module.pcss new file mode 100644 index 00000000..78f6f067 --- /dev/null +++ b/client2/src/components/App/Header/Header.module.pcss @@ -0,0 +1,81 @@ +.header { + position: relative; + z-index: 1; + color: var(--gray900); + background-color: var(--white); + box-shadow: 0 1px 4px 0 rgba(0, 21, 41, 0.12); +} + +.top, +.bottom { + padding: 12px 16px; + + @media (--l-viewport) { + padding: 12px 32px; + } +} + +.top { + background-color: var(--black); + + @media (--l-viewport) { + display: none; + } +} + +.bottom { + display: flex; + flex-direction: column; + + @media (--l-viewport) { + align-items: center; + flex-direction: row; + height: var(--header-height); + } +} + +.icon { + margin-right: 10px; +} + +.status { + display: flex; + align-items: center; + margin-bottom: 12px; + + @media (--l-viewport) { + margin: 0 16px 0 0; + } +} + +.action { + min-width: 80px; + margin-right: auto; +} + +.languages, +.user { + display: none; + + @media (--l-viewport) { + display: flex; + align-items: center; + } +} + +.user { + margin-right: 32px; +} + +.menu { + color: var(--white); + background-color: transparent; + border: 0; + + &:hover, + &:focus, + &:active { + color: var(--gray400); + background-color: transparent; + } +} diff --git a/client2/src/components/App/Header/Header.tsx b/client2/src/components/App/Header/Header.tsx new file mode 100644 index 00000000..d5da7456 --- /dev/null +++ b/client2/src/components/App/Header/Header.tsx @@ -0,0 +1,60 @@ +import React, { FC, useContext } from 'react'; +import { Button } from 'antd'; +import { MenuOutlined } from '@ant-design/icons'; +import { observer } from 'mobx-react-lite'; + +import { Icon, LangSelect } from 'Common/ui'; +import Store from 'Store'; + +import s from './Header.module.pcss'; + +const Header: FC = observer(() => { + const store = useContext(Store); + const { ui: { intl }, system, ui } = store; + const { status, profile } = system; + + const updateServerStatus = () => { + system.switchServerStatus(!status?.protectionEnabled); + }; + + return ( +
    +
    +
    +
    +
    + + {status?.protectionEnabled + ? intl.getMessage('header_adguard_status_enabled') + : intl.getMessage('header_adguard_status_disabled')} +
    + + {profile?.name && ( +
    + + {profile?.name} +
    + )} +
    + +
    +
    +
    + ); +}); + +export default Header; diff --git a/client2/src/components/App/Header/index.ts b/client2/src/components/App/Header/index.ts new file mode 100644 index 00000000..579f1ac2 --- /dev/null +++ b/client2/src/components/App/Header/index.ts @@ -0,0 +1 @@ +export { default } from './Header'; diff --git a/client2/src/components/App/Login/ForgotPassword.tsx b/client2/src/components/App/Login/ForgotPassword.tsx new file mode 100644 index 00000000..f19b8a60 --- /dev/null +++ b/client2/src/components/App/Login/ForgotPassword.tsx @@ -0,0 +1,65 @@ +import React, { FC, useContext } from 'react'; +import { Button } from 'antd'; +import cn from 'classnames'; + +import { CommonLayout } from 'Common/ui/layouts'; +import { code } from 'Common/formating'; +import { Link } from 'Common/ui'; +import Store from 'Store'; +import theme from 'Lib/theme'; + +import s from './Login.module.pcss'; +import { RoutePath } from '../Routes/Paths'; + +const ForgotPassword: FC = () => { + const store = useContext(Store); + const { ui: { intl } } = store; + + return ( + +
    +
    + {intl.getMessage('login_password_title')} +
    + +

    + {intl.getMessage('login_password_hash')} +

    + +
    +
    + {intl.getMessage('login_password_step_1')} +
    +
    + {intl.getMessage('login_password_step_2', { code })} +
    +
    + {intl.getMessage('login_password_step_3', { code })} +
    +
    + {intl.getMessage('login_password_step_4')} +
    +
    + {intl.getMessage('login_password_step_5')} +
    +
    + +

    + {intl.getMessage('login_password_result')} +

    + + + + +
    +
    + ); +}; + +export default ForgotPassword; diff --git a/client2/src/components/App/Login/Login.module.pcss b/client2/src/components/App/Login/Login.module.pcss new file mode 100644 index 00000000..ae887cd9 --- /dev/null +++ b/client2/src/components/App/Login/Login.module.pcss @@ -0,0 +1,34 @@ +.title { + margin-bottom: 12px; + font-size: 28px; + text-align: center; + + &_form { + margin-bottom: 32px; + } +} + +.link { + display: inline-block; + vertical-align: middle; + margin-top: 32px; + font-size: 16px; + text-align: center; +} + +.paragraph { + font-size: 16px; + margin: 0 0 14px; +} + +.list { + margin-bottom: 16px; + padding-left: 20px; + font-size: 16px; +} + +.step { + margin-bottom: 5px; + display: list-item; + list-style: decimal; +} diff --git a/client2/src/components/App/Login/Login.tsx b/client2/src/components/App/Login/Login.tsx new file mode 100644 index 00000000..844e03f9 --- /dev/null +++ b/client2/src/components/App/Login/Login.tsx @@ -0,0 +1,102 @@ +import React, { FC, useContext } from 'react'; +import { Button } from 'antd'; +import { observer } from 'mobx-react-lite'; +import { Formik, FormikHelpers } from 'formik'; +import cn from 'classnames'; + +import { Input } from 'Common/controls'; +import { CommonLayout } from 'Common/ui/layouts'; +import { Link } from 'Common/ui'; +import { RoutePath } from 'Components/App/Routes/Paths'; +import Store from 'Store'; +import theme from 'Lib/theme'; + +import s from './Login.module.pcss'; + +type FormValues = { + name: string; + password: string; +}; + +const Login: FC = observer(() => { + const store = useContext(Store); + const { ui: { intl }, login } = store; + + const onSubmit = async (values: FormValues, { setSubmitting }: FormikHelpers) => { + const { name, password } = values; + + const error = await login.login({ + name, + password, + }); + + if (error) { + setSubmitting(false); + } + }; + + const initialValues: FormValues = { + name: '', + password: '', + }; + + return ( + +
    +
    + {intl.getMessage('login')} +
    + + + {({ + values, + handleSubmit, + setFieldValue, + isSubmitting, + }) => ( +
    + setFieldValue('name', v)} + autoFocus + /> + setFieldValue('password', v)} + /> + +
    + )} +
    + +
    + + {intl.getMessage('login_password_link')} + +
    +
    +
    + ); +}); + +export default Login; diff --git a/client2/src/components/App/Login/index.ts b/client2/src/components/App/Login/index.ts new file mode 100644 index 00000000..3da6e50e --- /dev/null +++ b/client2/src/components/App/Login/index.ts @@ -0,0 +1,2 @@ +export { default as Login } from './Login'; +export { default as ForgotPassword } from './ForgotPassword'; diff --git a/client2/src/components/App/Routes/Paths.ts b/client2/src/components/App/Routes/Paths.ts new file mode 100644 index 00000000..f722a790 --- /dev/null +++ b/client2/src/components/App/Routes/Paths.ts @@ -0,0 +1,63 @@ +import qs from 'qs'; +import { Locale } from 'Localization'; + +const BasicPath = '/'; +const pathBuilder = (path: string) => (`${BasicPath}${path}`); + +export enum RoutePath { + Dashboard = 'Dashboard', + FiltersBlocklist = 'FiltersBlocklist', + FiltersAllowlist = 'FiltersAllowlist', + FiltersRewrites = 'FiltersRewrites', + FiltersServices = 'FiltersServices', + FiltersCustom = 'FiltersCustom', + QueryLog = 'QueryLog', + SetupGuide = 'SetupGuide', + SettingsGeneral = 'SettingsGeneral', + SettingsDns = 'SettingsDns', + SettingsEncryption = 'SettingsEncryption', + SettingsClients = 'SettingsClients', + SettingsDhcp = 'SettingsDhcp', + Login = 'Login', + ForgotPassword = 'ForgotPassword', +} + +export const Paths: Record = { + Dashboard: pathBuilder('dashboard'), + FiltersBlocklist: pathBuilder('filters/blocklists'), + FiltersAllowlist: pathBuilder('filters/allowlists'), + FiltersRewrites: pathBuilder('filters/rewrites'), + FiltersServices: pathBuilder('filters/services'), + FiltersCustom: pathBuilder('filters/custom'), + QueryLog: pathBuilder('logs'), + SetupGuide: pathBuilder('guide'), + SettingsGeneral: pathBuilder('settings/general'), + SettingsDns: pathBuilder('settings/dns'), + SettingsEncryption: pathBuilder('settings/encryption'), + SettingsClients: pathBuilder('settings/clients'), + SettingsDhcp: pathBuilder('settings/dhcp'), + Login: pathBuilder(''), + ForgotPassword: pathBuilder('forgot_password'), +}; + +export enum LinkParamsKeys {} +export enum QueryParams {} +export type LinkParams = Partial>; + +export const linkPathBuilder = ( + route: RoutePath, + params?: LinkParams, + lang?: Locale, + query?: Partial>, +) => { + let path = Paths[route]; // .replace(BasicPath, `/${lang}`); + if (params) { + Object.keys(params).forEach((key: unknown) => { + path = path.replace(`:${key}`, String(params[key as LinkParamsKeys])); + }); + } + if (query) { + path += `?${qs.stringify(query)}`; + } + return path; +}; diff --git a/client2/src/components/App/Routes/Routes.module.pcss b/client2/src/components/App/Routes/Routes.module.pcss new file mode 100644 index 00000000..e1561e76 --- /dev/null +++ b/client2/src/components/App/Routes/Routes.module.pcss @@ -0,0 +1,3 @@ +.app { + min-height: 100vh; +} \ No newline at end of file diff --git a/client2/src/components/App/Routes/Routes.tsx b/client2/src/components/App/Routes/Routes.tsx new file mode 100644 index 00000000..ad6df6cd --- /dev/null +++ b/client2/src/components/App/Routes/Routes.tsx @@ -0,0 +1,76 @@ +import React, { FC, useContext } from 'react'; +import { Layout } from 'antd'; +import { Switch, Route, Redirect } from 'react-router-dom'; +import { observer } from 'mobx-react-lite'; + +import Store from 'Store'; +import { Paths } from './Paths'; + +import Dashboard from '../Dashboard'; +import { Login, ForgotPassword } from '../Login'; +import Sidebar from '../Sidebar'; +import Header from '../Header'; +import SetupGuide from '../SetupGuide'; +import { GeneralSettings } from '../Settings'; + +import s from './Routes.module.pcss'; + +const { Content } = Layout; + +const AuthRoutes: FC = React.memo(() => { + return ( + + + + + ); +}); + +const AppRoutes: FC = observer(() => { + return ( + + + +
    + + + + + + + + + + + ); +}); + +const Routes: FC = observer(() => { + const store = useContext(Store); + const { login: { loggedIn } } = store; + if (loggedIn) { + return ; + } + return ; +}); + +export default Routes; diff --git a/client2/src/components/App/Routes/index.ts b/client2/src/components/App/Routes/index.ts new file mode 100644 index 00000000..edc668a3 --- /dev/null +++ b/client2/src/components/App/Routes/index.ts @@ -0,0 +1 @@ +export { default } from './Routes'; diff --git a/client2/src/components/App/Settings/GeneralSettings/GeneralSettings.tsx b/client2/src/components/App/Settings/GeneralSettings/GeneralSettings.tsx new file mode 100644 index 00000000..00a273bb --- /dev/null +++ b/client2/src/components/App/Settings/GeneralSettings/GeneralSettings.tsx @@ -0,0 +1,52 @@ +import React, { FC, useContext, useEffect } from 'react'; +import { Tabs, Grid } from 'antd'; +import { observer } from 'mobx-react-lite'; + +import { InnerLayout } from 'Common/ui'; +import Store from 'Store'; + +import { General, QueryLog, Statistics, TAB_KEY } from './components'; + +const { useBreakpoint } = Grid; +const { TabPane } = Tabs; + +const GeneralSettings: FC = observer(() => { + const store = useContext(Store); + const { ui: { intl }, generalSettings } = store; + const { inited } = generalSettings; + const screens = useBreakpoint(); + + useEffect(() => { + if (!inited) { + generalSettings.init(); + } + }, [inited]); + + if (!inited) { + return null; + } + + const tabsPosition = screens.lg ? 'left' : 'top'; + + return ( + + + + + + + + + + + + + + ); +}); + +export default GeneralSettings; diff --git a/client2/src/components/App/Settings/GeneralSettings/components/Common.module.pcss b/client2/src/components/App/Settings/GeneralSettings/components/Common.module.pcss new file mode 100644 index 00000000..e1570e85 --- /dev/null +++ b/client2/src/components/App/Settings/GeneralSettings/components/Common.module.pcss @@ -0,0 +1,45 @@ +.title { + font-size: 20px; + font-weight: 500; + color: var(--gray900); + margin-bottom: 48px; + display: flex; + justify-content: space-between; +} + +.radio { + display: block; + height: 30px; + line-height: 30px; + + &:first-of-type { + margin-top: -12px; + } +} +.save { + display: block; + margin-top: 24px; +} + +.item { + display: flex; + justify-content: space-between; + margin-bottom: 24px; +} + +.nameTitle { + color: var(--black); +} +.nameDesc { + color: var(--gray700); + margin-right: 40px; + + @media (--m-viewport) { + margin-right: 200px; + } +} +.select { + margin-bottom: 24px; + margin-top: -12px; + width: 200px; +} \ No newline at end of file diff --git a/client2/src/components/App/Settings/GeneralSettings/components/General.tsx b/client2/src/components/App/Settings/GeneralSettings/components/General.tsx new file mode 100644 index 00000000..72847496 --- /dev/null +++ b/client2/src/components/App/Settings/GeneralSettings/components/General.tsx @@ -0,0 +1,169 @@ +import React, { FC, useContext } from 'react'; +import { Button, Switch, Select } from 'antd'; +import { Formik, FormikHelpers } from 'formik'; +import { observer } from 'mobx-react-lite'; + +import { Link } from 'Common/ui'; +import Store from 'Store'; +import { RoutePath } from 'Paths'; + +import { s } from '.'; + +const { Option } = Select; + +const General: FC = observer(() => { + const store = useContext(Store); + const { ui: { intl }, generalSettings } = store; + const { + safebrowsing, + filteringConfig, + parental, + safesearch, + } = generalSettings; + + const initialValues = { + ...filteringConfig!.serialize(), + safebrowsing, + parental, + safesearch, + }; + + type InitialValues = typeof initialValues; + + const onSubmit = async (values: InitialValues, helpers: FormikHelpers) => { + // await generalSettings.updateQueryLogConfig(values); + if (initialValues.parental !== values.parental) { + generalSettings[values.parental ? 'parentalEnable' : 'parentalDisable'](); + } + if (initialValues.safesearch !== values.safesearch) { + generalSettings[values.safesearch ? 'safebrowsingEnable' : 'safebrowsingDisable'](); + } + if (initialValues.safebrowsing !== values.safebrowsing) { + generalSettings[values.safebrowsing ? 'safebrowsingEnable' : 'safebrowsingDisable'](); + } + if (initialValues.enabled !== values.enabled + || initialValues.interval !== values.interval) { + generalSettings.updateFilteringConfig({ + interval: values.interval, + enabled: values.enabled, + }); + } + helpers.setSubmitting(false); + }; + + const filtersLink = (e: string) => { + // TODO: fix link + return {e}; + }; + + return ( + <> +
    + {intl.getMessage('filter_category_general')} +
    + + {({ + handleSubmit, + values, + setFieldValue, + isSubmitting, + dirty, + }) => ( +
    +
    +
    +
    + {intl.getMessage('block_domain_use_filters_and_hosts')} +
    +
    + {intl.getMessage('filters_block_toggle_hint', { a: filtersLink })} +
    +
    + setFieldValue('enabled', e)}/> +
    +
    +
    +
    + {intl.getMessage('filters_interval')} +
    +
    +
    + +
    +
    +
    + {intl.getMessage('use_adguard_browsing_sec')} +
    +
    + {intl.getMessage('use_adguard_browsing_sec_hint')} +
    +
    + setFieldValue('safebrowsing', e)}/> +
    +
    +
    +
    + {intl.getMessage('use_adguard_parental')} +
    +
    + {intl.getMessage('use_adguard_parental_hint')} +
    +
    + setFieldValue('parental', e)}/> +
    +
    +
    +
    + {intl.getMessage('enforce_safe_search')} +
    +
    + {intl.getMessage('enforce_save_search_hint')} +
    +
    + setFieldValue('safesearch', e)}/> +
    + {dirty && ( + + )} +
    + )} +
    + + ); +}); + +export default General; diff --git a/client2/src/components/App/Settings/GeneralSettings/components/QueryLog.tsx b/client2/src/components/App/Settings/GeneralSettings/components/QueryLog.tsx new file mode 100644 index 00000000..c13bf599 --- /dev/null +++ b/client2/src/components/App/Settings/GeneralSettings/components/QueryLog.tsx @@ -0,0 +1,124 @@ +import React, { FC, useContext, useState } from 'react'; +import { Radio, Button, Switch } from 'antd'; +import { Formik, FormikHelpers } from 'formik'; +import { observer } from 'mobx-react-lite'; + +import { notifySuccess, ConfirmModalLayout } from 'Common/ui'; +import { IQueryLogConfig } from 'Entities/QueryLogConfig'; +import Store from 'Store'; + +import { s } from '.'; + +const { Group } = Radio; + +const QueryLog: FC = observer(() => { + const store = useContext(Store); + const [showConfirm, setShowConfirm] = useState(false); + const { ui: { intl }, generalSettings } = store; + const { + queryLogConfig, + } = generalSettings; + + const onSubmit = async (values: IQueryLogConfig, helpers: FormikHelpers) => { + await generalSettings.updateQueryLogConfig(values); + helpers.setSubmitting(false); + }; + + const onReset = async () => { + const result = await generalSettings.querylogClear(); + if (result) { + notifySuccess(intl.getMessage('query_log_cleared')); + } + }; + + return ( + <> +
    + {intl.getMessage('query_log_configuration')} + +
    + setShowConfirm(false)} + title={intl.getMessage('query_log_clear')} + buttonText={intl.getMessage('query_log_clear')} + > + {intl.getMessage('query_log_confirm_clear')} + + + {({ + handleSubmit, + values, + setFieldValue, + isSubmitting, + dirty, + }) => ( +
    +
    +
    +
    + {intl.getMessage('query_log_enable')} +
    +
    + setFieldValue('enabled', e)}/> +
    +
    +
    +
    + {intl.getMessage('anonymize_client_ip')} +
    +
    + {intl.getMessage('anonymize_client_ip_desc')} +
    +
    + setFieldValue('anonymize_client_ip', e)}/> +
    +
    +
    +
    + {intl.getMessage('query_log_retention')} +
    +
    + {intl.getMessage('query_log_retention_confirm')} +
    +
    +
    + setFieldValue('interval', e.target.value)}> + + {intl.getMessage('interval_24_hour')} + + + {intl.getPlural('interval_days', 7, { count: 7 })} + + + {intl.getPlural('interval_days', 30, { count: 30 })} + + + {intl.getPlural('interval_days', 90, { count: 90 })} + + + {dirty && ( + + )} +
    + )} +
    + + ); +}); + +export default QueryLog; diff --git a/client2/src/components/App/Settings/GeneralSettings/components/Statistics.tsx b/client2/src/components/App/Settings/GeneralSettings/components/Statistics.tsx new file mode 100644 index 00000000..7b6e2112 --- /dev/null +++ b/client2/src/components/App/Settings/GeneralSettings/components/Statistics.tsx @@ -0,0 +1,105 @@ +import React, { FC, useContext, useState } from 'react'; +import { Radio, Button } from 'antd'; +import { Formik, FormikHelpers } from 'formik'; +import { observer } from 'mobx-react-lite'; + +import { notifySuccess, ConfirmModalLayout } from 'Common/ui'; +import { IStatsConfig } from 'Entities/StatsConfig'; +import Store from 'Store'; + +import { s } from '.'; + +const { Group } = Radio; + +const Statistics: FC = observer(() => { + const store = useContext(Store); + const [showConfirm, setShowConfirm] = useState(false); + const { ui: { intl }, generalSettings } = store; + const { + statsConfig, + } = generalSettings; + + const onSubmit = async (values: IStatsConfig, helpers: FormikHelpers) => { + await generalSettings.updateStatsConfig(values); + helpers.setSubmitting(false); + }; + + const onReset = async () => { + const result = await generalSettings.statsReset(); + if (result) { + notifySuccess(intl.getMessage('stats_reset')); + } + }; + + return ( + <> +
    + {intl.getMessage('statistics_configuration')} + +
    + setShowConfirm(false)} + title={intl.getMessage('statistics_clear')} + buttonText={intl.getMessage('statistics_clear')} + > + {intl.getMessage('statistics_clear_confirm')} + + + {({ + handleSubmit, + values, + setFieldValue, + isSubmitting, + dirty, + }) => ( +
    +
    +
    +
    + {intl.getMessage('statistics_retention')} +
    +
    + {intl.getMessage('statistics_retention_desc')} +
    +
    +
    + setFieldValue('interval', e.target.value)}> + + {intl.getMessage('interval_24_hour')} + + + {intl.getPlural('interval_days', 7, { count: 7 })} + + + {intl.getPlural('interval_days', 30, { count: 30 })} + + + {intl.getPlural('interval_days', 90, { count: 90 })} + + + {dirty && ( + + )} +
    + )} +
    + + ); +}); + +export default Statistics; diff --git a/client2/src/components/App/Settings/GeneralSettings/components/index.ts b/client2/src/components/App/Settings/GeneralSettings/components/index.ts new file mode 100644 index 00000000..872b4789 --- /dev/null +++ b/client2/src/components/App/Settings/GeneralSettings/components/index.ts @@ -0,0 +1,9 @@ +export { default as General } from './General'; +export { default as QueryLog } from './QueryLog'; +export { default as Statistics } from './Statistics'; +export enum TAB_KEY { + GENERAL = 'GENERAL', + QUERY_LOG = 'QUERY_LOG', + STATISTICS = 'STATISTICS', +} +export { default as s } from './Common.module.pcss'; diff --git a/client2/src/components/App/Settings/GeneralSettings/index.ts b/client2/src/components/App/Settings/GeneralSettings/index.ts new file mode 100644 index 00000000..103dbe79 --- /dev/null +++ b/client2/src/components/App/Settings/GeneralSettings/index.ts @@ -0,0 +1 @@ +export { default as GeneralSettings } from './GeneralSettings'; diff --git a/client2/src/components/App/Settings/index.ts b/client2/src/components/App/Settings/index.ts new file mode 100644 index 00000000..dfb1ece8 --- /dev/null +++ b/client2/src/components/App/Settings/index.ts @@ -0,0 +1 @@ +export { GeneralSettings } from './GeneralSettings'; diff --git a/client2/src/components/App/SetupGuide/SetupGuide.module.pcss b/client2/src/components/App/SetupGuide/SetupGuide.module.pcss new file mode 100644 index 00000000..c4ac60c5 --- /dev/null +++ b/client2/src/components/App/SetupGuide/SetupGuide.module.pcss @@ -0,0 +1,31 @@ +.title { + margin-bottom: 16px; + font-size: 20px; + font-weight: 500; + + @media (--m-viewport) { + margin-bottom: 24px; + } +} + +.text { + margin-bottom: 32px; + font-size: 14px; + color: var(--gray900); + + p { + margin: 0 0 5px; + } +} + +.addresses { + margin-top: 16px; +} + +.address { + font-family: var(--font-family-monospace); + font-size: 16px; + font-weight: 600; + word-break: break-all; + color: var(--green400); +} diff --git a/client2/src/components/App/SetupGuide/SetupGuide.tsx b/client2/src/components/App/SetupGuide/SetupGuide.tsx new file mode 100644 index 00000000..f4c5a760 --- /dev/null +++ b/client2/src/components/App/SetupGuide/SetupGuide.tsx @@ -0,0 +1,92 @@ +import React, { FC, useContext } from 'react'; +import { Tabs, Grid } from 'antd'; + +import { InnerLayout } from 'Common/ui'; +import { externalLink, p } from 'Common/formating'; +import { DHCP_LINK } from 'Consts/common'; +import Store from 'Store'; + +import s from './SetupGuide.module.pcss'; + +const { useBreakpoint } = Grid; +const { TabPane } = Tabs; + +const SetupGuide: FC = () => { + const store = useContext(Store); + const { ui: { intl }, system } = store; + const screens = useBreakpoint(); + const tabsPosition = screens.lg ? 'left' : 'top'; + + const { status } = system; + + const tabs = [ + { + key: intl.getMessage('router'), + text: intl.getMessage('install_configure_router', { p }), + }, + { + key: 'Windows', + text: intl.getMessage('install_configure_windows', { p }), + }, + { + key: 'macOS', + text: intl.getMessage('install_configure_macos', { p }), + }, + { + key: 'Linux', + text: intl.getMessage('install_configure_router', { p }), + }, + { + key: 'Android', + text: intl.getMessage('install_configure_android', { p }), + }, + { + key: 'iOS', + text: intl.getMessage('install_configure_ios', { p }), + }, + ]; + + const addresses = ( + <> +
    + {intl.getMessage('install_configure_adresses')} + {status?.dnsAddresses && ( +
    + {status.dnsAddresses.map((address) => ( +
    + {address} +
    + ))} +
    + )} +
    +
    + {intl.getMessage('install_configure_dhcp', { dhcp: externalLink(DHCP_LINK) })} +
    + + ); + + return ( + + + {tabs.map((tab) => ( + +
    + {intl.getMessage('install_configure_how_to_title', { value: tab.key })} +
    +
    + {tab.text} +
    + {addresses} +
    + ))} +
    +
    + ); +}; + +export default SetupGuide; diff --git a/client2/src/components/App/SetupGuide/index.tsx b/client2/src/components/App/SetupGuide/index.tsx new file mode 100644 index 00000000..97a2bb20 --- /dev/null +++ b/client2/src/components/App/SetupGuide/index.tsx @@ -0,0 +1 @@ +export { default } from './SetupGuide'; diff --git a/client2/src/components/App/Sidebar/Sidebar.module.pcss b/client2/src/components/App/Sidebar/Sidebar.module.pcss new file mode 100644 index 00000000..6942d66a --- /dev/null +++ b/client2/src/components/App/Sidebar/Sidebar.module.pcss @@ -0,0 +1,23 @@ +.logo { + width: 118px; + height: 31px; + margin: 20px; +} + +.icon { + width: 16px; + height: 16px; + margin-right: 10px; +} + +.menu { + display: flex; + flex-direction: column; + min-height: calc(100% - 71px); +} + +.logout { + @media (--m-viewport) { + margin-top: auto!important; + } +} diff --git a/client2/src/components/App/Sidebar/Sidebar.tsx b/client2/src/components/App/Sidebar/Sidebar.tsx new file mode 100644 index 00000000..f7485bc7 --- /dev/null +++ b/client2/src/components/App/Sidebar/Sidebar.tsx @@ -0,0 +1,116 @@ +import React, { FC, useContext } from 'react'; +import { Layout, Menu, Grid } from 'antd'; +import { observer } from 'mobx-react-lite'; +import { PieChartOutlined, FormOutlined, TableOutlined, ProfileOutlined, SettingOutlined } from '@ant-design/icons'; + +import Store from 'Store'; +import { Link, Icon, Mask } from 'Common/ui'; +import { RoutePath, linkPathBuilder } from 'Components/App/Routes/Paths'; + +import s from './Sidebar.module.pcss'; + +const { Sider } = Layout; +const { Item: MenuItem, SubMenu } = Menu; +const { useBreakpoint } = Grid; + +const Sidebar: FC = observer(() => { + const store = useContext(Store); + const screens = useBreakpoint(); + const { ui: { intl, sidebarOpen, toggleSidebar } } = store; + + if (!Object.keys(screens).length) { + return null; + } + + const handleSidebar = () => { + if (!screens.xl) { + toggleSidebar(); + } + }; + + return ( + <> + + + + + + + {intl.getMessage('dashboard')} + + + + + + {intl.getMessage('filters')} + + + + + + {intl.getMessage('query_log')} + + + + + + {intl.getMessage('setup_guide')} + + + } + title={intl.getMessage('settings')} + > + + + {intl.getMessage('general_settings')} + + + + + {intl.getMessage('dns_settings')} + + + + + {intl.getMessage('encryption_settings')} + + + + + {intl.getMessage('client_settings')} + + + + + {intl.getMessage('dhcp_settings')} + + + + + + + {intl.getMessage('sign_out')} + + + + + + + ); +}); + +export default Sidebar; diff --git a/client2/src/components/App/Sidebar/index.ts b/client2/src/components/App/Sidebar/index.ts new file mode 100644 index 00000000..e842a859 --- /dev/null +++ b/client2/src/components/App/Sidebar/index.ts @@ -0,0 +1 @@ +export { default } from './Sidebar'; diff --git a/client2/src/components/App/index.ts b/client2/src/components/App/index.ts new file mode 100644 index 00000000..9122fa1a --- /dev/null +++ b/client2/src/components/App/index.ts @@ -0,0 +1 @@ +export { default } from './App'; diff --git a/client2/src/components/Install/Install.tsx b/client2/src/components/Install/Install.tsx new file mode 100644 index 00000000..3e7d797e --- /dev/null +++ b/client2/src/components/Install/Install.tsx @@ -0,0 +1,122 @@ +import React, { FC } from 'react'; +import { Layout } from 'antd'; +import { Formik, FormikHelpers } from 'formik'; +import { observer } from 'mobx-react-lite'; +import cn from 'classnames'; + +import { IInitialConfigurationBeta } from 'Entities/InitialConfigurationBeta'; +import Icons from 'Common/ui/Icons'; +import { + DEFAULT_DNS_ADDRESS, + DEFAULT_DNS_PORT, + DEFAULT_IP_ADDRESS, + DEFAULT_IP_PORT, +} from 'Consts/install'; +import { notifyError } from 'Common/ui'; +import InstallStore from 'Store/stores/Install'; +import theme from 'Lib/theme'; + +import AdminInterface from './components/AdminInterface'; +import Auth from './components/Auth'; +import DnsServer from './components/DnsServer'; +import Stepper from './components/Stepper'; +import Welcome from './components/Welcome'; +import ConfigureDevices from './components/ConfigureDevices'; + +const { Content } = Layout; + +export type FormValues = IInitialConfigurationBeta & { step: number }; + +const InstallForm: FC = observer(() => { + const initialValues: FormValues = { + step: 0, + web: { + ip: [DEFAULT_IP_ADDRESS], + port: DEFAULT_IP_PORT, + }, + dns: { + ip: [DEFAULT_DNS_ADDRESS], + port: DEFAULT_DNS_PORT, + }, + password: '', + username: '', + }; + + const onNext = async (values: FormValues, { setFieldValue }: FormikHelpers) => { + const currentStep = values.step; + const checker = (condition: boolean, message: string) => { + if (condition) { + setFieldValue('step', currentStep + 1); + } else { + notifyError(message); + } + }; + switch (currentStep) { + case 1: { + // web + const check = await InstallStore.checkConfig(values); + checker(check?.web?.status === '', check?.web?.status || ''); + break; + } + case 3: { + // dns + const check = await InstallStore.checkConfig(values); + checker(check?.dns?.status === '', check?.dns?.status || ''); + break; + } + case 4: { + // configure + const config = await InstallStore.configure(values); + if (config) { + const { web } = values; + window.location.href = `http://${web.ip[0]}:${web.port}`; + } + break; + } + default: + setFieldValue('step', currentStep + 1); + break; + } + }; + + return ( + + {({ values, handleSubmit, setFieldValue }) => ( +
    + + {values.step === 0 && ( + setFieldValue('step', 1)}/> + )} + {values.step === 1 && ( + + )} + {values.step === 2 && ( + + )} + {values.step === 3 && ( + + )} + {values.step === 4 && ( + + )} + + )} +
    + ); +}); + +const Install: FC = () => { + return ( + + + + + + + ); +}; + +export default Install; diff --git a/client2/src/components/Install/components/AdminInterface/AdminInterface.tsx b/client2/src/components/Install/components/AdminInterface/AdminInterface.tsx new file mode 100644 index 00000000..1a4b3757 --- /dev/null +++ b/client2/src/components/Install/components/AdminInterface/AdminInterface.tsx @@ -0,0 +1,142 @@ +import React, { FC, useContext } from 'react'; +import cn from 'classnames'; +import { observer } from 'mobx-react-lite'; +import { FormikHelpers } from 'formik'; + +import { Input, Radio, Switch } from 'Common/controls'; +import { DEFAULT_IP_ADDRESS } from 'Consts/install'; +import { chechNetworkType, NETWORK_TYPE } from 'Helpers/installHelpers'; +import theme from 'Lib/theme'; +import Store from 'Store/installStore'; + +import { FormValues } from '../../Install'; +import StepButtons from '../StepButtons'; + +enum NETWORK_OPTIONS { + ALL = 'all', + CUSTOM = 'custom', +} + +interface AdminInterfaceProps { + values: FormValues; + setFieldValue: FormikHelpers['setFieldValue']; +} + +const AdminInterface: FC = observer(({ + values, + setFieldValue, +}) => { + const { ui: { intl }, install: { addresses } } = useContext(Store); + const { web: { ip } } = values; + const radioValue = ip.length === 1 && ip[0] === DEFAULT_IP_ADDRESS + ? NETWORK_OPTIONS.ALL : NETWORK_OPTIONS.CUSTOM; + + const onSelectRadio = (v: string | number) => { + const value = v === NETWORK_OPTIONS.ALL + ? [DEFAULT_IP_ADDRESS] : []; + setFieldValue('web.ip', value); + }; + + const getManualBlock = () => ( +
    + {addresses?.interfaces.map((a) => { + let name = ''; + const type = chechNetworkType(a.name); + switch (type) { + case NETWORK_TYPE.ETHERNET: + name = `${intl.getMessage('ethernet')} (${a.name}) `; + break; + case NETWORK_TYPE.LOCAL: + name = `${intl.getMessage('localhost')} (${a.name}) `; + break; + default: + name = a.name || ''; + break; + } + return ( +
    +
    + {name} +
    + {a.ipAddresses?.map((addrIp) => ( +
    +
    + http://{addrIp} +
    + { + const temp = new Set(ip); + if (temp.has(addrIp)) { + temp.delete(addrIp); + } else { + temp.add(addrIp); + } + setFieldValue('web.ip', Array.from(temp.values())); + }}/> +
    + ))} +
    + ); + })} +
    + ); + + return ( + <> +
    + {intl.getMessage('install_admin_interface_title')} +
    +
    + {intl.getMessage('install_admin_interface_title_decs')} +
    +
    + {intl.getMessage('install_admin_interface_where_interface')} +
    +
    + {intl.getMessage('install_admin_interface_where_interface_desc')} +
    + + { radioValue !== NETWORK_OPTIONS.ALL && getManualBlock()} +
    + {intl.getMessage('install_admin_interface_port')} +
    +
    + {intl.getMessage('install_admin_interface_port_desc')} +
    + { + const port = v === '' ? '' : parseInt(v, 10); + setFieldValue('web.port', port); + }} + /> + + + ); +}); + +export default AdminInterface; diff --git a/client2/src/components/Install/components/AdminInterface/index.ts b/client2/src/components/Install/components/AdminInterface/index.ts new file mode 100644 index 00000000..e0f7b851 --- /dev/null +++ b/client2/src/components/Install/components/AdminInterface/index.ts @@ -0,0 +1 @@ +export { default } from './AdminInterface'; diff --git a/client2/src/components/Install/components/Auth/Auth.tsx b/client2/src/components/Install/components/Auth/Auth.tsx new file mode 100644 index 00000000..fe346328 --- /dev/null +++ b/client2/src/components/Install/components/Auth/Auth.tsx @@ -0,0 +1,55 @@ +import React, { FC, useContext } from 'react'; +import cn from 'classnames'; +import { observer } from 'mobx-react-lite'; +import { FormikHelpers } from 'formik'; + +import { Input } from 'Common/controls'; +import theme from 'Lib/theme'; +import Store from 'Store/installStore'; + +import StepButtons from '../StepButtons'; +import { FormValues } from '../../Install'; + +interface AuthProps { + values: FormValues; + setFieldValue: FormikHelpers['setFieldValue']; +} + +const Auth: FC = observer(({ + values, + setFieldValue, +}) => { + const { ui: { intl } } = useContext(Store); + + return ( + <> +
    + {intl.getMessage('install_auth_title')} +
    +
    + {intl.getMessage('install_auth_description')} +
    + setFieldValue('username', v)} + /> + setFieldValue('password', v)} + /> + + + ); +}); + +export default Auth; diff --git a/client2/src/components/Install/components/Auth/index.ts b/client2/src/components/Install/components/Auth/index.ts new file mode 100644 index 00000000..b1dea29b --- /dev/null +++ b/client2/src/components/Install/components/Auth/index.ts @@ -0,0 +1 @@ +export { default } from './Auth'; diff --git a/client2/src/components/Install/components/ConfigureDevices/ConfigureDevices.tsx b/client2/src/components/Install/components/ConfigureDevices/ConfigureDevices.tsx new file mode 100644 index 00000000..fc305adf --- /dev/null +++ b/client2/src/components/Install/components/ConfigureDevices/ConfigureDevices.tsx @@ -0,0 +1,142 @@ +import React, { FC, useContext } from 'react'; +import { Tabs, Grid } from 'antd'; +import cn from 'classnames'; +import { FormikHelpers } from 'formik'; + +import { DHCP_LINK } from 'Consts/common'; +import { danger, externalLink, p } from 'Common/formating'; +import { DEFAULT_DNS_PORT, DEFAULT_IP_ADDRESS, DEFAULT_IP_PORT } from 'Consts/install'; +import Store from 'Store/installStore'; +import theme from 'Lib/theme'; + +import { FormValues } from '../../Install'; +import StepButtons from '../StepButtons'; + +const { useBreakpoint } = Grid; +const { TabPane } = Tabs; + +interface ConfigureDevicesProps { + values: FormValues; + setFieldValue: FormikHelpers['setFieldValue']; +} + +const ConfigureDevices: FC = ({ + values, setFieldValue, +}) => { + const { ui: { intl }, install: { addresses } } = useContext(Store); + const screens = useBreakpoint(); + const tabsPosition = screens.md ? 'left' : 'top'; + + const allIps = addresses?.interfaces.reduce((all, data) => { + const { ipAddresses } = data; + if (ipAddresses) { + all.push(...ipAddresses); + } + return all; + }, [] as string[]); + + const { web: { ip: webIp }, dns: { ip: dnsIp } } = values; + const selectedWebIps = webIp.length === 1 && webIp[0] === DEFAULT_IP_ADDRESS + ? allIps : webIp; + const selectedDnsIps = dnsIp.length === 1 && dnsIp[0] === DEFAULT_IP_ADDRESS + ? allIps : dnsIp; + + return ( + <> +
    + {intl.getMessage('install_configure_title')} +
    +
    + {intl.getMessage('install_configure_danger_notice', { danger })} +
    + + + +
    + {intl.getMessage('install_configure_how_to_title', { value: intl.getMessage('router') })} +
    +
    + {intl.getMessage('install_configure_router', { p })} +
    +
    + +
    + {intl.getMessage('install_configure_how_to_title', { value: 'Windows' })} +
    +
    + {intl.getMessage('install_configure_windows', { p })} +
    +
    + +
    + {intl.getMessage('install_configure_how_to_title', { value: 'macOS' })} +
    +
    + {intl.getMessage('install_configure_macos', { p })} +
    +
    + +
    + {intl.getMessage('install_configure_how_to_title', { value: 'Linux' })} +
    +
    + {/* TODO: add linux setup */} + {intl.getMessage('install_configure_router', { p })} +
    +
    + +
    + {intl.getMessage('install_configure_how_to_title', { value: 'Android' })} +
    +
    + {intl.getMessage('install_configure_android', { p })} +
    +
    + +
    + {intl.getMessage('install_configure_how_to_title', { value: 'iOS' })} +
    +
    + {intl.getMessage('install_configure_ios', { p })} +
    +
    +
    + +
    + {intl.getMessage('install_configure_adresses')} +
    +
    +
    + {intl.getMessage('install_admin_interface_title')} +
    +
    + {selectedWebIps?.map((ip) => ( +
    + {ip}{values.web.port !== DEFAULT_IP_PORT && `:${values.web.port}`} +
    + ))} +
    +
    + {intl.getMessage('install_dns_server_title')} +
    +
    + {selectedDnsIps?.map((ip) => ( +
    + {ip}{values.dns.port !== DEFAULT_DNS_PORT && `:${values.dns.port}`} +
    + ))} +
    +
    +
    + {intl.getMessage('install_configure_dhcp', { dhcp: externalLink(DHCP_LINK) })} +
    + + + ); +}; + +export default ConfigureDevices; diff --git a/client2/src/components/Install/components/ConfigureDevices/index.ts b/client2/src/components/Install/components/ConfigureDevices/index.ts new file mode 100644 index 00000000..928cdfa0 --- /dev/null +++ b/client2/src/components/Install/components/ConfigureDevices/index.ts @@ -0,0 +1 @@ +export { default } from './ConfigureDevices'; diff --git a/client2/src/components/Install/components/DnsServer/DnsServer.tsx b/client2/src/components/Install/components/DnsServer/DnsServer.tsx new file mode 100644 index 00000000..60db1260 --- /dev/null +++ b/client2/src/components/Install/components/DnsServer/DnsServer.tsx @@ -0,0 +1,142 @@ +import React, { FC, useContext } from 'react'; +import cn from 'classnames'; +import { observer } from 'mobx-react-lite'; +import { FormikHelpers } from 'formik'; + +import { Input, Radio, Switch } from 'Common/controls'; +import { DEFAULT_IP_ADDRESS } from 'Consts/install'; +import { chechNetworkType, NETWORK_TYPE } from 'Helpers/installHelpers'; +import theme from 'Lib/theme'; +import Store from 'Store/installStore'; + +import { FormValues } from '../../Install'; +import StepButtons from '../StepButtons'; + +enum NETWORK_OPTIONS { + ALL = 'all', + CUSTOM = 'custom', +} + +interface DnsServerProps { + values: FormValues; + setFieldValue: FormikHelpers['setFieldValue']; +} + +const DnsServer: FC = observer(({ + values, + setFieldValue, +}) => { + const { ui: { intl }, install: { addresses } } = useContext(Store); + const { dns: { ip } } = values; + const radioValue = ip.length === 1 && ip[0] === DEFAULT_IP_ADDRESS + ? NETWORK_OPTIONS.ALL : NETWORK_OPTIONS.CUSTOM; + + const onSelectRadio = (v: string | number) => { + const value = v === NETWORK_OPTIONS.ALL + ? [DEFAULT_IP_ADDRESS] : []; + setFieldValue('dns.ip', value); + }; + + const getManualBlock = () => ( +
    + {addresses?.interfaces.map((a) => { + let name = ''; + const type = chechNetworkType(a.name); + switch (type) { + case NETWORK_TYPE.ETHERNET: + name = `${intl.getMessage('ethernet')} (${a.name}) `; + break; + case NETWORK_TYPE.LOCAL: + name = `${intl.getMessage('localhost')} (${a.name}) `; + break; + default: + name = a.name || ''; + break; + } + return ( +
    +
    + {name} +
    + {a.ipAddresses?.map((addrIp) => ( +
    +
    + {addrIp} +
    + { + const temp = new Set(ip); + if (temp.has(addrIp)) { + temp.delete(addrIp); + } else { + temp.add(addrIp); + } + setFieldValue('dns.ip', Array.from(temp.values())); + }}/> +
    + ))} +
    + ); + })} +
    + ); + + return ( +
    +
    + {intl.getMessage('install_dns_server_title')} +
    +
    + {intl.getMessage('install_dns_server_desc')} +
    +
    + {intl.getMessage('install_dns_server_network_interfaces')} +
    +
    + {intl.getMessage('install_dns_server_network_interfaces_desc')} +
    + + { radioValue !== NETWORK_OPTIONS.ALL && getManualBlock()} +
    + {intl.getMessage('install_dns_server_port')} +
    +
    + {intl.getMessage('install_dns_server_port_desc')} +
    + { + const port = v === '' ? '' : parseInt(v, 10); + setFieldValue('dns.port', port); + }} + /> + +
    + ); +}); + +export default DnsServer; diff --git a/client2/src/components/Install/components/DnsServer/index.ts b/client2/src/components/Install/components/DnsServer/index.ts new file mode 100644 index 00000000..95d67c54 --- /dev/null +++ b/client2/src/components/Install/components/DnsServer/index.ts @@ -0,0 +1 @@ +export { default } from './DnsServer'; diff --git a/client2/src/components/Install/components/StepButtons/StepButtons.tsx b/client2/src/components/Install/components/StepButtons/StepButtons.tsx new file mode 100644 index 00000000..d5875eac --- /dev/null +++ b/client2/src/components/Install/components/StepButtons/StepButtons.tsx @@ -0,0 +1,44 @@ +import React, { FC, useContext } from 'react'; +import { Button } from 'antd'; +import { observer } from 'mobx-react-lite'; +import { FormikHelpers } from 'formik'; + +import Store from 'Store/installStore'; +import theme from 'Lib/theme'; + +import { FormValues } from '../../Install'; + +interface StepButtonsProps { + setFieldValue: FormikHelpers['setFieldValue']; + currentStep: number; + values: FormValues; +} + +const StepButtons: FC = observer(({ + setFieldValue, + currentStep, +}) => { + const { ui: { intl } } = useContext(Store); + return ( +
    + + +
    + ); +}); + +export default StepButtons; diff --git a/client2/src/components/Install/components/StepButtons/index.ts b/client2/src/components/Install/components/StepButtons/index.ts new file mode 100644 index 00000000..a4875a80 --- /dev/null +++ b/client2/src/components/Install/components/StepButtons/index.ts @@ -0,0 +1 @@ +export { default } from './StepButtons'; diff --git a/client2/src/components/Install/components/Stepper/Stepper.module.pcss b/client2/src/components/Install/components/Stepper/Stepper.module.pcss new file mode 100644 index 00000000..3f6f3a0d --- /dev/null +++ b/client2/src/components/Install/components/Stepper/Stepper.module.pcss @@ -0,0 +1,66 @@ +.stepper { + position: relative; + display: flex; + align-items: center; + justify-content: space-between; + height: 16px; + margin-bottom: 32px; + + @media (--m-viewport) { + margin-bottom: 48px; + } +} + +.wrap { + flex: 1; + position: relative; + display: inline-flex; + align-items: center; + justify-content: flex-end; + height: 16px; + + &:before { + content: ""; + position: absolute; + left: 0; + bottom: 7px; + width: 100%; + height: 1px; + background-color: var(--gray400); + } + + &:first-child { + flex: 0; + + &:before { + display: none; + } + } + + &.current .circle { + transform: scale(2); + background-color: var(--green400); + border-color: var(--green400); + } + + &.active .circle { + background-color: var(--green400); + border-color: var(--green400); + } + + &.current:before, + &.active:before { + background-color: var(--green400); + } +} + +.circle { + position: relative; + z-index: 1; + width: 8px; + height: 8px; + background-color: var(--white); + border-radius: 50%; + border: 1px solid var(--gray400); + transition: var(--transition) transform, var(--transition) background, var(--transition) border; +} diff --git a/client2/src/components/Install/components/Stepper/Stepper.tsx b/client2/src/components/Install/components/Stepper/Stepper.tsx new file mode 100644 index 00000000..29950ca8 --- /dev/null +++ b/client2/src/components/Install/components/Stepper/Stepper.tsx @@ -0,0 +1,35 @@ +import React, { FC } from 'react'; +import cn from 'classnames'; + +import s from './Stepper.module.pcss'; + +interface StepProps { + active: boolean; + current: boolean; +} + +const Step: FC = ({ active, current }) => { + return ( +
    +
    +
    + ); +}; + +interface StepperProps { + currentStep: number; +} + +const Stepper: FC = ({ currentStep }) => { + return ( +
    + = 0} /> + = 1} /> + = 2} /> + = 3} /> + = 4} /> +
    + ); +}; + +export default Stepper; diff --git a/client2/src/components/Install/components/Stepper/index.ts b/client2/src/components/Install/components/Stepper/index.ts new file mode 100644 index 00000000..2fb2a1bd --- /dev/null +++ b/client2/src/components/Install/components/Stepper/index.ts @@ -0,0 +1 @@ +export { default } from './Stepper'; diff --git a/client2/src/components/Install/components/Welcome/Welcome.tsx b/client2/src/components/Install/components/Welcome/Welcome.tsx new file mode 100644 index 00000000..baa81184 --- /dev/null +++ b/client2/src/components/Install/components/Welcome/Welcome.tsx @@ -0,0 +1,38 @@ +import React, { FC, useContext } from 'react'; +import { Button } from 'antd'; +import { observer } from 'mobx-react-lite'; + +import Store from 'Store/installStore'; +import Icon from 'Common/ui/Icon'; +import theme from 'Lib/theme'; + +interface WelcomeProps { + onNext: () => void; +} + +const Welcome: FC = observer(({ onNext }) => { + const { ui: { intl } } = useContext(Store); + return ( + <> + +
    + {intl.getMessage('install_wellcome_title')} +
    +
    + {intl.getMessage('install_wellcome_desc')} +
    +
    + +
    + + ); +}); + +export default Welcome; diff --git a/client2/src/components/Install/components/Welcome/index.ts b/client2/src/components/Install/components/Welcome/index.ts new file mode 100644 index 00000000..7cc4be19 --- /dev/null +++ b/client2/src/components/Install/components/Welcome/index.ts @@ -0,0 +1 @@ +export { default } from './Welcome'; diff --git a/client2/src/components/Install/index.ts b/client2/src/components/Install/index.ts new file mode 100644 index 00000000..6808555d --- /dev/null +++ b/client2/src/components/Install/index.ts @@ -0,0 +1 @@ +export { default } from './Install'; diff --git a/client2/src/components/common/controls/Button/Button.tsx b/client2/src/components/common/controls/Button/Button.tsx new file mode 100644 index 00000000..667b9abd --- /dev/null +++ b/client2/src/components/common/controls/Button/Button.tsx @@ -0,0 +1,67 @@ +import React, { FC, FocusEvent } from 'react'; +import { Button as ButtonControl } from 'antd'; +import cn from 'classnames'; + +type ButtonSize = 'small' | 'medium' | 'big'; +type ButtonType = 'primary' | 'icon' | 'link' | 'outlined' | 'border' | 'ghost' | 'input' | 'edit'; +type ButtonHTMLType = 'submit' | 'button' | 'reset'; +type ButtonShape = 'circle' | 'round'; + +export interface ButtonProps { + className?: string; + danger?: boolean; + dataAttrs?: { + [key: string]: string; + }; + disabled?: boolean; + htmlType?: ButtonHTMLType; + // icon?: IconType | 'dots_loader'; + iconClassName?: string; + id?: string; + inGroup?: boolean; + onClick?: React.MouseEventHandler; + onBlur?: (e: FocusEvent) => void; + shape?: ButtonShape; + size?: ButtonSize; + type: ButtonType; + block?: boolean; +} + +const Button: FC = ({ + children, + className, + danger, + dataAttrs, + disabled, + htmlType, + // icon, + id, + onClick, + onBlur, + shape, +}) => { + const buttonClass = cn( + className, + ); + + return ( + + // : )} + id={id} + onClick={onClick} + onBlur={onBlur} + shape={shape} + > + {children} + + ); +}; + +export default Button; diff --git a/client2/src/components/common/controls/Button/index.ts b/client2/src/components/common/controls/Button/index.ts new file mode 100644 index 00000000..efe8c800 --- /dev/null +++ b/client2/src/components/common/controls/Button/index.ts @@ -0,0 +1 @@ +export { default } from './Button'; diff --git a/client2/src/components/common/controls/Input/Input.tsx b/client2/src/components/common/controls/Input/Input.tsx new file mode 100644 index 00000000..2b7503bb --- /dev/null +++ b/client2/src/components/common/controls/Input/Input.tsx @@ -0,0 +1,146 @@ +import React, { FC, FocusEvent, KeyboardEvent, ClipboardEvent, ChangeEvent, useState } from 'react'; +import { Input as InputControl } from 'antd'; +import { InputProps as InputControlProps } from 'antd/lib/input'; +import cn from 'classnames'; + +import { Icon } from 'Common/ui'; +import theme from 'Lib/theme'; + +interface AdminInterfaceProps { + autoComplete?: InputControlProps['autoComplete']; + autoFocus?: InputControlProps['autoFocus']; + className?: string; + description?: string; + disabled?: boolean; + error?: boolean; + id?: string; + inputMode?: InputControlProps['inputMode']; + label?: string; + wrapperClassName?: string; + name: string; + onBlur?: (e: FocusEvent) => void; + onChange?: (data: string, e?: ChangeEvent) => void; + onFocus?: (e: FocusEvent) => void; + onKeyDown?: (e: KeyboardEvent) => void; + onPaste?: (e: ClipboardEvent) => void; + pattern?: InputControlProps['pattern']; + placeholder: string; + prefix?: InputControlProps['prefix']; + size?: InputControlProps['size']; + suffix?: InputControlProps['suffix']; + type: InputControlProps['type']; + value: string | number; +} + +const InputComponent: FC = ({ + autoComplete, + autoFocus, + className, + description, + disabled, + error, + id, + inputMode, + label, + wrapperClassName, + name, + onBlur, + onChange, + onFocus, + onKeyDown, + onPaste, + pattern, + placeholder, + prefix, + size = 'middle', + suffix, + type, + value, +}) => { + const [inputType, setInputType] = useState(type); + + const inputClass = cn( + 'input', + { input_big: size === 'large' }, + { input_medium: size === 'middle' }, + { input_small: size === 'small' }, + className, + ); + + const handleBlur = (e: FocusEvent) => { + if (onBlur) { + onBlur(e); + } + }; + + const showPassword = () => { + if (inputType === 'password') { + setInputType('text'); + } else { + setInputType('password'); + } + }; + + const showPasswordIcon = () => { + const icon = inputType === 'password' ? 'visibility_disable' : 'visibility_enable'; + return ( + + ); + }; + + const validSuffix = ( + <> + {!!suffix && suffix} + {(type === 'password') && showPasswordIcon()} + + ); + + let descriptionView = null; + if (description) { + descriptionView = ( +
    + {description} +
    + ); + } + + return ( + + ); +}; + +export default InputComponent; diff --git a/client2/src/components/common/controls/Input/index.ts b/client2/src/components/common/controls/Input/index.ts new file mode 100644 index 00000000..b4d38647 --- /dev/null +++ b/client2/src/components/common/controls/Input/index.ts @@ -0,0 +1 @@ +export { default as Input } from './Input'; diff --git a/client2/src/components/common/controls/Radio/Radio.module.pcss b/client2/src/components/common/controls/Radio/Radio.module.pcss new file mode 100644 index 00000000..ebbce092 --- /dev/null +++ b/client2/src/components/common/controls/Radio/Radio.module.pcss @@ -0,0 +1,20 @@ +.group { + width: 100%; +} + +.radio { + display: flex; + align-items: center; + margin-bottom: 16px; + padding-bottom: 16px; + width: 100%; + border-bottom: 1px solid var(--gray300); + + &:last-child { + border-bottom: 0; + } +} + +.desc { + color: var(--gray400); +} diff --git a/client2/src/components/common/controls/Radio/Radio.tsx b/client2/src/components/common/controls/Radio/Radio.tsx new file mode 100644 index 00000000..5b816858 --- /dev/null +++ b/client2/src/components/common/controls/Radio/Radio.tsx @@ -0,0 +1,55 @@ +import React, { FC } from 'react'; +import { Radio } from 'antd'; +import { observer } from 'mobx-react-lite'; + +import s from './Radio.module.pcss'; + +const { Group } = Radio; + +interface RadioProps { + options: { + label: string; + desc?: string; + value: string | number; + }[]; + onSelect: (value: string | number) => void; + value: string | number; +} + +const RadioComponent: FC = observer(({ + options, onSelect, value, +}) => { + if (options.length === 0) { + return null; + } + + return ( + { + onSelect(e.target.value); + }} + className={s.group} + > + {options.map((o) => ( + +
    + {o.label} +
    + {o.desc && ( +
    + {o.desc} +
    + )} +
    + ))} +
    + + ); +}); + +export default RadioComponent; diff --git a/client2/src/components/common/controls/Radio/index.ts b/client2/src/components/common/controls/Radio/index.ts new file mode 100644 index 00000000..61c95671 --- /dev/null +++ b/client2/src/components/common/controls/Radio/index.ts @@ -0,0 +1 @@ +export { default } from './Radio'; diff --git a/client2/src/components/common/controls/Switch/Switch.tsx b/client2/src/components/common/controls/Switch/Switch.tsx new file mode 100644 index 00000000..bb762eec --- /dev/null +++ b/client2/src/components/common/controls/Switch/Switch.tsx @@ -0,0 +1,3 @@ +import { Switch as SwitchE } from 'antd'; + +export default SwitchE; diff --git a/client2/src/components/common/controls/Switch/index.ts b/client2/src/components/common/controls/Switch/index.ts new file mode 100644 index 00000000..4f1eea55 --- /dev/null +++ b/client2/src/components/common/controls/Switch/index.ts @@ -0,0 +1 @@ +export { default as Switch } from './Switch'; diff --git a/client2/src/components/common/controls/index.ts b/client2/src/components/common/controls/index.ts new file mode 100644 index 00000000..3746e6fe --- /dev/null +++ b/client2/src/components/common/controls/index.ts @@ -0,0 +1,4 @@ +export { default as Radio } from './Radio'; +export { Input } from './Input'; +export { Switch } from './Switch'; +export { default as Button } from './Button'; diff --git a/client2/src/components/common/formating/code.tsx b/client2/src/components/common/formating/code.tsx new file mode 100644 index 00000000..60de648c --- /dev/null +++ b/client2/src/components/common/formating/code.tsx @@ -0,0 +1,12 @@ +import React from 'react'; +import theme from 'Lib/theme'; + +const code = (e: string) => { + return ( + + {e} + + ); +}; + +export default code; diff --git a/client2/src/components/common/formating/danger.tsx b/client2/src/components/common/formating/danger.tsx new file mode 100644 index 00000000..a04cce3c --- /dev/null +++ b/client2/src/components/common/formating/danger.tsx @@ -0,0 +1,12 @@ +import React from 'react'; +import theme from 'Lib/theme'; + +const danger = (e: string) => { + return ( + + {e} + + ); +}; + +export default danger; diff --git a/client2/src/components/common/formating/externalLink.tsx b/client2/src/components/common/formating/externalLink.tsx new file mode 100644 index 00000000..14e0cad5 --- /dev/null +++ b/client2/src/components/common/formating/externalLink.tsx @@ -0,0 +1,13 @@ +import React from 'react'; +import theme from 'Lib/theme'; + +export const externalLink = (link: string) => (e: string) => ( + + {e} + +); diff --git a/client2/src/components/common/formating/index.ts b/client2/src/components/common/formating/index.ts new file mode 100644 index 00000000..7b1322b1 --- /dev/null +++ b/client2/src/components/common/formating/index.ts @@ -0,0 +1,4 @@ +export { default as danger } from './danger'; +export { default as p } from './p'; +export { default as code } from './code'; +export { externalLink } from './externalLink'; diff --git a/client2/src/components/common/formating/p.tsx b/client2/src/components/common/formating/p.tsx new file mode 100644 index 00000000..68b39915 --- /dev/null +++ b/client2/src/components/common/formating/p.tsx @@ -0,0 +1,11 @@ +import React from 'react'; + +const danger = (e: string) => { + return ( +

    + {e} +

    + ); +}; + +export default danger; diff --git a/client2/src/components/common/index.ts b/client2/src/components/common/index.ts new file mode 100644 index 00000000..e69de29b diff --git a/client2/src/components/common/ui/Icon/Icon.module.pcss b/client2/src/components/common/ui/Icon/Icon.module.pcss new file mode 100644 index 00000000..a935fe21 --- /dev/null +++ b/client2/src/components/common/ui/Icon/Icon.module.pcss @@ -0,0 +1,7 @@ +.icon { + display: inline-block; + vertical-align: middle; + width: 24px; + height: 24px; + flex-shrink: 0; +} diff --git a/client2/src/components/common/ui/Icon/Icon.tsx b/client2/src/components/common/ui/Icon/Icon.tsx new file mode 100644 index 00000000..261852b3 --- /dev/null +++ b/client2/src/components/common/ui/Icon/Icon.tsx @@ -0,0 +1,25 @@ +import React, { FC } from 'react'; +import cn from 'classnames'; +import { IconType } from 'Common/ui/Icons'; + +import s from './Icon.module.pcss'; + +interface IconProps { + icon: IconType; + color?: string; + className?: string; + onClick?: () => void; +} + +const Icon: FC = ({ icon, color, className, onClick }) => { + const iconClass = cn(s.icon, color, className); + + return ( + + + + ); +}; + +export default Icon; +export { IconType } from 'Common/ui/Icons'; diff --git a/client2/src/components/common/ui/Icon/index.ts b/client2/src/components/common/ui/Icon/index.ts new file mode 100644 index 00000000..c1c8457b --- /dev/null +++ b/client2/src/components/common/ui/Icon/index.ts @@ -0,0 +1 @@ +export { default, IconType } from './Icon'; diff --git a/client2/src/components/common/ui/Icons/Icon.pcss b/client2/src/components/common/ui/Icons/Icon.pcss new file mode 100644 index 00000000..a77a74a7 --- /dev/null +++ b/client2/src/components/common/ui/Icons/Icon.pcss @@ -0,0 +1,3 @@ +.icons { + display: none; +} diff --git a/client2/src/components/common/ui/Icons/index.tsx b/client2/src/components/common/ui/Icons/index.tsx new file mode 100644 index 00000000..dcf0e338 --- /dev/null +++ b/client2/src/components/common/ui/Icons/index.tsx @@ -0,0 +1,84 @@ +import React, { FC } from 'react'; +import './Icon.pcss'; + +export type IconType = + 'logo' | + 'visibility_disable' | + 'visibility_enable' | + 'logo_shield' | + 'logo_light' | + 'sign_out' | + 'user' | + 'language' | + 'close_big'; + +const Icons: FC = () => ( + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +); + +export default Icons; diff --git a/client2/src/components/common/ui/LangSelect/LangSelect.module.pcss b/client2/src/components/common/ui/LangSelect/LangSelect.module.pcss new file mode 100644 index 00000000..1342ea34 --- /dev/null +++ b/client2/src/components/common/ui/LangSelect/LangSelect.module.pcss @@ -0,0 +1,10 @@ +.wrap { + display: inline-flex; + align-items: center; +} + +.icon { + font-size: 22px; + margin-right: 10px; + color: var(--gray700); +} diff --git a/client2/src/components/common/ui/LangSelect/LangSelect.tsx b/client2/src/components/common/ui/LangSelect/LangSelect.tsx new file mode 100644 index 00000000..24bc15b2 --- /dev/null +++ b/client2/src/components/common/ui/LangSelect/LangSelect.tsx @@ -0,0 +1,23 @@ +import React, { FC, useContext } from 'react'; + +import { Icon } from 'Common/ui'; +import Store from 'Store'; +import { LANGUAGES } from 'Localization'; + +import s from './LangSelect.module.pcss'; + +const LangSelector: FC = () => { + const store = useContext(Store); + const { ui: { currentLang } } = store; + + const lang = LANGUAGES.find((e) => e.code === currentLang)!; + + return ( +
    + + {lang.name} +
    + ); +}; + +export default LangSelector; diff --git a/client2/src/components/common/ui/LangSelect/index.tsx b/client2/src/components/common/ui/LangSelect/index.tsx new file mode 100644 index 00000000..25f35f87 --- /dev/null +++ b/client2/src/components/common/ui/LangSelect/index.tsx @@ -0,0 +1 @@ +export { default } from './LangSelect'; diff --git a/client2/src/components/common/ui/Link.tsx b/client2/src/components/common/ui/Link.tsx new file mode 100644 index 00000000..5abbc0a1 --- /dev/null +++ b/client2/src/components/common/ui/Link.tsx @@ -0,0 +1,63 @@ +import React, { FC, MouseEvent } from 'react'; +import { Link as L, LinkProps as LProps } from 'react-router-dom'; +import cn from 'classnames'; + +import { linkPathBuilder, RoutePath, LinkParams, LinkParamsKeys } from 'Paths'; + +interface LinkProps { + to: RoutePath; + props?: LinkParams; + className?: string; + type?: LProps['type']; + stop?: boolean; + disabled?: boolean; + onClick?: () => void; + id?: string; +} + +const Link: FC = ({ + to, children, className, props, type, stop, disabled, onClick, id, +}) => { + if (props) { + Object.keys(props).forEach((key: unknown) => { + if (!props[key as LinkParamsKeys]) { + throw new Error(`Got wrong ${key} propKey: ${props[key as LinkParamsKeys]} in Link`); + } + }); + } + + const handleClick = (e: MouseEvent) => { + if (stop) { + e.stopPropagation(); + } + if (onClick) { + onClick(); + } + }; + + if (disabled) { + return ( +
    + {children} +
    + ); + } + + return ( + + {children} + + ); +}; + +export default Link; diff --git a/client2/src/components/common/ui/Mask/Mask.module.pcss b/client2/src/components/common/ui/Mask/Mask.module.pcss new file mode 100644 index 00000000..7c7e1930 --- /dev/null +++ b/client2/src/components/common/ui/Mask/Mask.module.pcss @@ -0,0 +1,26 @@ +.mask { + position: fixed; + top: 0; + right: 0; + bottom: 0; + left: 0; + z-index: 1040; + height: 100%; + background-color: rgba(0, 0, 0, 0.45); + opacity: 0; + visibility: hidden; + transition: opacity var(--transition); + cursor: pointer; + + &_visible { + opacity: 1; + visibility: visible; + } + + @media (--l-viewport) { + &_visible { + opacity: 0; + visibility: hidden; + } + } +} diff --git a/client2/src/components/common/ui/Mask/Mask.tsx b/client2/src/components/common/ui/Mask/Mask.tsx new file mode 100644 index 00000000..162b2349 --- /dev/null +++ b/client2/src/components/common/ui/Mask/Mask.tsx @@ -0,0 +1,23 @@ +import React, { FC } from 'react'; +import cn from 'classnames'; + +import s from './Mask.module.pcss'; + +interface MaskProps { + open: boolean; + handle: () => void; +} + +const Mask: FC = ({ open, handle }) => { + return ( +
    + ); +}; + +export default Mask; diff --git a/client2/src/components/common/ui/Mask/index.ts b/client2/src/components/common/ui/Mask/index.ts new file mode 100644 index 00000000..7933fa49 --- /dev/null +++ b/client2/src/components/common/ui/Mask/index.ts @@ -0,0 +1 @@ +export { default } from './Mask'; diff --git a/client2/src/components/common/ui/Notifications/index.ts b/client2/src/components/common/ui/Notifications/index.ts new file mode 100644 index 00000000..69c284eb --- /dev/null +++ b/client2/src/components/common/ui/Notifications/index.ts @@ -0,0 +1 @@ +export { notifyError, notifySuccess } from './notifications'; diff --git a/client2/src/components/common/ui/Notifications/notifications.tsx b/client2/src/components/common/ui/Notifications/notifications.tsx new file mode 100644 index 00000000..6a5da09b --- /dev/null +++ b/client2/src/components/common/ui/Notifications/notifications.tsx @@ -0,0 +1,42 @@ +import React from 'react'; +import { notification } from 'antd'; + +import { DEFAULT_NOTIFICATION_DURATION } from 'Consts/common'; + +export const notifySuccess = (title: string, code?: string) => { + notification.success({ + message: ( +
    + {title} +
    + ), + placement: 'bottomRight', + duration: DEFAULT_NOTIFICATION_DURATION, + className: 'notification', + }); +}; + +export const notifyError = ( + title: string, + options?: { + btn?: React.ReactNode; + duration?: number; + onClose?: () => void; + }, +) => { + const { btn, duration, onClose } = options || {}; + notification.error({ + onClose, + message: ( +
    + {title} +
    + ), + placement: 'bottomRight', + duration: typeof duration === 'number' ? duration : DEFAULT_NOTIFICATION_DURATION, + className: 'notification', + btn, + }); +}; diff --git a/client2/src/components/common/ui/index.ts b/client2/src/components/common/ui/index.ts new file mode 100644 index 00000000..c505ed48 --- /dev/null +++ b/client2/src/components/common/ui/index.ts @@ -0,0 +1,6 @@ +export { default as Icon } from './Icon'; +export { notifyError, notifySuccess } from './Notifications'; +export { default as Link } from './Link'; +export { default as LangSelect } from './LangSelect'; +export { default as Mask } from './Mask'; +export { CommonLayout, InnerLayout, CommonModalLayout, ConfirmModalLayout } from './layouts'; diff --git a/client2/src/components/common/ui/layouts/CommonLayout.tsx b/client2/src/components/common/ui/layouts/CommonLayout.tsx new file mode 100644 index 00000000..e0c73cb3 --- /dev/null +++ b/client2/src/components/common/ui/layouts/CommonLayout.tsx @@ -0,0 +1,16 @@ +import { Layout } from 'antd'; +import React, { FC } from 'react'; + +interface CommonLayoutProps { + className?: string; +} + +const CommonLayout: FC = ({ children, className }) => { + return ( + + {children} + + ); +}; + +export default CommonLayout; diff --git a/client2/src/components/common/ui/layouts/CommonModalLayout.tsx b/client2/src/components/common/ui/layouts/CommonModalLayout.tsx new file mode 100644 index 00000000..8e62049e --- /dev/null +++ b/client2/src/components/common/ui/layouts/CommonModalLayout.tsx @@ -0,0 +1,87 @@ +import React, { FC, useContext, useEffect } from 'react'; +import { Modal, Button } from 'antd'; +import cn from 'classnames'; + +import { Icon } from 'Common/ui'; +import Store from 'Store'; + +interface CommonModalLayoutProps { + visible: boolean; + title: string; + buttonText?: string; + className?: string; + width?: number; + onClose: () => void; + onSubmit?: () => void; + noFooter?: boolean; + disabled?: boolean; + centered?: boolean; +} + +const CommonModalLayout: FC = ({ + visible, + children, + title, + buttonText, + className, + width, + onClose, + onSubmit, + noFooter, + disabled, + centered, +}) => { + const store = useContext(Store); + const { ui: { intl } } = store; + + useEffect(() => { + const onEnter = (e: KeyboardEvent) => { + if (e.key === 'Enter' && onSubmit) { + onSubmit(); + } + }; + if (onSubmit) { + window.addEventListener('keyup', onEnter); + } + return () => { + window.removeEventListener('keyup', onEnter); + }; + }, [onSubmit]); + const footer = noFooter ? null : [ + , + , + ]; + + return ( + } + width={width || 480} + centered={centered} + > + {children} + + ); +}; + +export default CommonModalLayout; diff --git a/client2/src/components/common/ui/layouts/ConfirmModalLayout.tsx b/client2/src/components/common/ui/layouts/ConfirmModalLayout.tsx new file mode 100644 index 00000000..503b858c --- /dev/null +++ b/client2/src/components/common/ui/layouts/ConfirmModalLayout.tsx @@ -0,0 +1,34 @@ +import React, { FC } from 'react'; + +import CommonModalLayout from './CommonModalLayout'; + +interface DeleteModalLayoutProps { + visible: boolean; + title: string; + buttonText: string; + onClose: () => void; + onConfirm?: () => void; +} + +const DeleteModalLayout: FC = ({ + visible, + children, + title, + buttonText, + onClose, + onConfirm, +}) => { + return ( + + {children} + + ); +}; + +export default DeleteModalLayout; diff --git a/client2/src/components/common/ui/layouts/InnerLayout.tsx b/client2/src/components/common/ui/layouts/InnerLayout.tsx new file mode 100644 index 00000000..4ecab818 --- /dev/null +++ b/client2/src/components/common/ui/layouts/InnerLayout.tsx @@ -0,0 +1,41 @@ +import { Layout } from 'antd'; +import React, { FC } from 'react'; +import cn from 'classnames'; + +import theme from 'Lib/theme'; + +interface InnerLayoutProps { + title: string; + className?: string; + containerClassName?: string; +} + +const InnerLayout: FC = ({ + children, title, className, containerClassName, +}) => { + return ( + +
    +
    +
    + {title} +
    +
    + {children} +
    +
    + ); +}; + +export default InnerLayout; diff --git a/client2/src/components/common/ui/layouts/index.ts b/client2/src/components/common/ui/layouts/index.ts new file mode 100644 index 00000000..3751f879 --- /dev/null +++ b/client2/src/components/common/ui/layouts/index.ts @@ -0,0 +1,4 @@ +export { default as CommonLayout } from './CommonLayout'; +export { default as InnerLayout } from './InnerLayout'; +export { default as ConfirmModalLayout } from './ConfirmModalLayout'; +export { default as CommonModalLayout } from './CommonModalLayout'; diff --git a/client2/src/lib/ant/Modal.pcss b/client2/src/lib/ant/Modal.pcss new file mode 100644 index 00000000..c0c98671 --- /dev/null +++ b/client2/src/lib/ant/Modal.pcss @@ -0,0 +1,47 @@ +.modal { + & .ant-modal-close-x { + display: flex; + align-items: center; + justify-content: center; + width: 24px; + height: 24px; + color: var(--black); + border-radius: 2px; + background-color: var(--white); + transition: background-color 0.3s; + + &:hover, + &:focus { + background-color: var(--cloud); + } + + &:active { + background-color: var(--borders-white); + } + + & svg { + width: 20px; + height: 20px; + } + + @media (--s-viewport) { + width: 40px; + height: 40px; + + & svg { + width: 24px; + height: 24px; + } + } + } + + & .ant-modal-close { + top: 11px; + right: 8px; + + @media (--s-viewport) { + top: 15px; + right: 15px; + } + } +} diff --git a/client2/src/lib/ant/Radio.pcss b/client2/src/lib/ant/Radio.pcss new file mode 100644 index 00000000..b3937d8d --- /dev/null +++ b/client2/src/lib/ant/Radio.pcss @@ -0,0 +1,15 @@ +.ant-radio { + margin-right: 18px; +} + +.ant-radio-inner { + width: 20px; + height: 20px; + background-color: transparent; + border-color: var(--gray400); + + &::after { + width: 12px; + height: 12px; + } +} \ No newline at end of file diff --git a/client2/src/lib/ant/Sidebar.pcss b/client2/src/lib/ant/Sidebar.pcss new file mode 100644 index 00000000..a0d55dfe --- /dev/null +++ b/client2/src/lib/ant/Sidebar.pcss @@ -0,0 +1,26 @@ +.sidebar { + position: fixed; + top: 0; + height: 100vh; + font-weight: 500; + overflow: auto; + z-index: 1041; + scrollbar-width: none; + + &::-webkit-scrollbar { + display: none; + } + + @media (--l-viewport) { + position: sticky; + z-index: 1040; + } + + & .ant-menu-item-group { + @media (--m-viewport) { + &:last-child { + margin-top: auto; + } + } + } +} \ No newline at end of file diff --git a/client2/src/lib/ant/Tabs.pcss b/client2/src/lib/ant/Tabs.pcss new file mode 100644 index 00000000..0b110210 --- /dev/null +++ b/client2/src/lib/ant/Tabs.pcss @@ -0,0 +1,45 @@ +.tabs { + border-radius: 2px; + background-color: var(--white); + + & .ant-tabs-tab { + padding: 10px 16px; + margin-right: 10px; + color: var(--gray900); + transition: color var(--transition), background var(--transition); + + &.ant-tabs-tab-active { + background-color: #E6F4EA; + } + } + + &.ant-tabs-left > .ant-tabs-nav .ant-tabs-tab { + @media (--l-viewport) { + min-width: 230px; + margin-bottom: 7px; + padding: 10px 24px; + } + } + + &.ant-tabs-left > .ant-tabs-content-holder > .ant-tabs-content > .ant-tabs-tabpane { + @media (--l-viewport) { + padding: 24px 40px; + } + } + + & .ant-tabs-nav { + margin-bottom: 0; + } + + & .ant-tabs-tabpane { + padding: 24px 16px; + } + + & .ant-tabs-nav-list { + padding: 0 16px; + + @media (--l-viewport) { + padding: 24px 0; + } + } +} diff --git a/client2/src/lib/ant/ant-overrides.less b/client2/src/lib/ant/ant-overrides.less new file mode 100644 index 00000000..1de4b6e5 --- /dev/null +++ b/client2/src/lib/ant/ant-overrides.less @@ -0,0 +1,12 @@ +@primary-color: #67b279; +@success-color: #4d995f; +@text-color: #000; +@link-hover-color: #4d995f; +@link-active-color: #4d995f; +@text-selection-bg: #e7efff; +@layout-body-background: #f3f3f3; +@layout-header-background: #131313; +@menu-dark-submenu-bg: #131313; + +@font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Helvetica Neue", Arial, sans-serif; +@font-size-base: 14px; diff --git a/client2/src/lib/ant/ant.less b/client2/src/lib/ant/ant.less new file mode 100644 index 00000000..a4e7d2ec --- /dev/null +++ b/client2/src/lib/ant/ant.less @@ -0,0 +1,6 @@ +@import '~antd/dist/antd.less'; +@import './ant-overrides.less'; + +::selection { + color: #000; +} diff --git a/client2/src/lib/ant/index.ts b/client2/src/lib/ant/index.ts new file mode 100644 index 00000000..9fd16b8d --- /dev/null +++ b/client2/src/lib/ant/index.ts @@ -0,0 +1,7 @@ +import './Radio.pcss'; +import './Sidebar.pcss'; +import './Tabs.pcss'; +import './Modal.pcss'; + +const insertStyles = true; +export default insertStyles; diff --git a/client2/src/lib/apis/blockedServices.ts b/client2/src/lib/apis/blockedServices.ts new file mode 100644 index 00000000..381a236d --- /dev/null +++ b/client2/src/lib/apis/blockedServices.ts @@ -0,0 +1,31 @@ +// This file was autogenerated. Please do not change. +// All changes will be overwrited on commit. +export default class BlockedServicesApi { + static async blockedServicesList(): Promise { + return await fetch(`/control/blocked_services/list`, { + method: 'GET', + }).then(async (res) => { + if (res.status === 200) { + return res.json(); + } else { + return new Error(String(res.status)); + } + }) + } + + static async blockedServicesSet(data: string[]): Promise { + return await fetch(`/control/blocked_services/set`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify(data), + }).then(async (res) => { + if (res.status === 200) { + return res.status; + } else { + return new Error(String(res.status)); + } + }) + } +} diff --git a/client2/src/lib/apis/clients.ts b/client2/src/lib/apis/clients.ts new file mode 100644 index 00000000..187bcc9c --- /dev/null +++ b/client2/src/lib/apis/clients.ts @@ -0,0 +1,139 @@ +import qs from 'qs'; +import AccessListResponse, { IAccessListResponse } from 'Entities/AccessListResponse'; +import AccessSetRequest, { IAccessSetRequest } from 'Entities/AccessSetRequest'; +import Client, { IClient } from 'Entities/Client'; +import ClientDelete, { IClientDelete } from 'Entities/ClientDelete'; +import ClientUpdate, { IClientUpdate } from 'Entities/ClientUpdate'; +import Clients, { IClients } from 'Entities/Clients'; +import ClientsFindEntry, { IClientsFindEntry } from 'Entities/ClientsFindEntry'; + +// This file was autogenerated. Please do not change. +// All changes will be overwrited on commit. +export default class ClientsApi { + static async accessList(): Promise { + return await fetch(`/control/access/list`, { + method: 'GET', + }).then(async (res) => { + if (res.status === 200) { + return res.json(); + } else { + return new Error(String(res.status)); + } + }) + } + + static async accessSet(accesssetrequest: IAccessSetRequest): Promise { + const haveError: string[] = []; + const accesssetrequestValid = new AccessSetRequest(accesssetrequest); + haveError.push(...accesssetrequestValid.validate()); + if (haveError.length > 0) { + return Promise.resolve(haveError); + } + return await fetch(`/control/access/set`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify(accesssetrequestValid.serialize()), + }).then(async (res) => { + if (res.status === 200) { + return res.status; + } else { + return new Error(String(res.status)); + } + }) + } + + static async clientsAdd(client: IClient): Promise { + const haveError: string[] = []; + const clientValid = new Client(client); + haveError.push(...clientValid.validate()); + if (haveError.length > 0) { + return Promise.resolve(haveError); + } + return await fetch(`/control/clients/add`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify(clientValid.serialize()), + }).then(async (res) => { + if (res.status === 200) { + return res.status; + } else { + return new Error(String(res.status)); + } + }) + } + + static async clientsDelete(clientdelete: IClientDelete): Promise { + const haveError: string[] = []; + const clientdeleteValid = new ClientDelete(clientdelete); + haveError.push(...clientdeleteValid.validate()); + if (haveError.length > 0) { + return Promise.resolve(haveError); + } + return await fetch(`/control/clients/delete`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify(clientdeleteValid.serialize()), + }).then(async (res) => { + if (res.status === 200) { + return res.status; + } else { + return new Error(String(res.status)); + } + }) + } + + static async clientsFind(ip0?: string): Promise { + const queryParams = { + ip0: ip0, + } + return await fetch(`/control/clients/find?${qs.stringify(queryParams, { arrayFormat: 'comma' })}`, { + method: 'GET', + }).then(async (res) => { + if (res.status === 200) { + return res.json(); + } else { + return new Error(String(res.status)); + } + }) + } + + static async clientsStatus(): Promise { + return await fetch(`/control/clients`, { + method: 'GET', + }).then(async (res) => { + if (res.status === 200) { + return res.json(); + } else { + return new Error(String(res.status)); + } + }) + } + + static async clientsUpdate(clientupdate: IClientUpdate): Promise { + const haveError: string[] = []; + const clientupdateValid = new ClientUpdate(clientupdate); + haveError.push(...clientupdateValid.validate()); + if (haveError.length > 0) { + return Promise.resolve(haveError); + } + return await fetch(`/control/clients/update`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify(clientupdateValid.serialize()), + }).then(async (res) => { + if (res.status === 200) { + return res.status; + } else { + return new Error(String(res.status)); + } + }) + } +} diff --git a/client2/src/lib/apis/dhcp.ts b/client2/src/lib/apis/dhcp.ts new file mode 100644 index 00000000..2d2df29f --- /dev/null +++ b/client2/src/lib/apis/dhcp.ts @@ -0,0 +1,123 @@ +import DhcpConfig, { IDhcpConfig } from 'Entities/DhcpConfig'; +import DhcpSearchResult, { IDhcpSearchResult } from 'Entities/DhcpSearchResult'; +import DhcpStaticLease, { IDhcpStaticLease } from 'Entities/DhcpStaticLease'; +import DhcpStatus, { IDhcpStatus } from 'Entities/DhcpStatus'; +import NetInterfaces, { INetInterfaces } from 'Entities/NetInterfaces'; + +// This file was autogenerated. Please do not change. +// All changes will be overwrited on commit. +export default class DhcpApi { + static async checkActiveDhcp(): Promise { + return await fetch(`/control/dhcp/find_active_dhcp`, { + method: 'POST', + }).then(async (res) => { + if (res.status === 200) { + return res.json(); + } else { + return new Error(String(res.status)); + } + }) + } + + static async dhcpAddStaticLease(dhcpstaticlease: IDhcpStaticLease): Promise { + const haveError: string[] = []; + const dhcpstaticleaseValid = new DhcpStaticLease(dhcpstaticlease); + haveError.push(...dhcpstaticleaseValid.validate()); + if (haveError.length > 0) { + return Promise.resolve(haveError); + } + return await fetch(`/control/dhcp/add_static_lease`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify(dhcpstaticleaseValid.serialize()), + }).then(async (res) => { + if (res.status === 200) { + return res.status; + } else { + return new Error(String(res.status)); + } + }) + } + + static async dhcpInterfaces(): Promise { + return await fetch(`/control/dhcp/interfaces`, { + method: 'GET', + }).then(async (res) => { + if (res.status === 200) { + return res.json(); + } else { + return new Error(String(res.status)); + } + }) + } + + static async dhcpRemoveStaticLease(dhcpstaticlease: IDhcpStaticLease): Promise { + const haveError: string[] = []; + const dhcpstaticleaseValid = new DhcpStaticLease(dhcpstaticlease); + haveError.push(...dhcpstaticleaseValid.validate()); + if (haveError.length > 0) { + return Promise.resolve(haveError); + } + return await fetch(`/control/dhcp/remove_static_lease`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify(dhcpstaticleaseValid.serialize()), + }).then(async (res) => { + if (res.status === 200) { + return res.status; + } else { + return new Error(String(res.status)); + } + }) + } + + static async dhcpReset(): Promise { + return await fetch(`/control/dhcp/reset`, { + method: 'POST', + }).then(async (res) => { + if (res.status === 200) { + return res.status; + } else { + return new Error(String(res.status)); + } + }) + } + + static async dhcpSetConfig(dhcpconfig: IDhcpConfig): Promise { + const haveError: string[] = []; + const dhcpconfigValid = new DhcpConfig(dhcpconfig); + haveError.push(...dhcpconfigValid.validate()); + if (haveError.length > 0) { + return Promise.resolve(haveError); + } + return await fetch(`/control/dhcp/set_config`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify(dhcpconfigValid.serialize()), + }).then(async (res) => { + if (res.status === 200) { + return res.status; + } else { + return new Error(String(res.status)); + } + }) + } + + static async dhcpStatus(): Promise { + return await fetch(`/control/dhcp/status`, { + method: 'GET', + }).then(async (res) => { + if (res.status === 200) { + return res.json(); + } else { + return new Error(String(res.status)); + } + }) + } +} diff --git a/client2/src/lib/apis/filtering.ts b/client2/src/lib/apis/filtering.ts new file mode 100644 index 00000000..cdaf1e91 --- /dev/null +++ b/client2/src/lib/apis/filtering.ts @@ -0,0 +1,167 @@ +import qs from 'qs'; +import AddUrlRequest, { IAddUrlRequest } from 'Entities/AddUrlRequest'; +import FilterCheckHostResponse, { IFilterCheckHostResponse } from 'Entities/FilterCheckHostResponse'; +import FilterConfig, { IFilterConfig } from 'Entities/FilterConfig'; +import FilterRefreshRequest, { IFilterRefreshRequest } from 'Entities/FilterRefreshRequest'; +import FilterRefreshResponse, { IFilterRefreshResponse } from 'Entities/FilterRefreshResponse'; +import FilterSetUrl, { IFilterSetUrl } from 'Entities/FilterSetUrl'; +import FilterStatus, { IFilterStatus } from 'Entities/FilterStatus'; +import RemoveUrlRequest, { IRemoveUrlRequest } from 'Entities/RemoveUrlRequest'; + +// This file was autogenerated. Please do not change. +// All changes will be overwrited on commit. +export default class FilteringApi { + static async filteringAddURL(addurlrequest: IAddUrlRequest): Promise { + const haveError: string[] = []; + const addurlrequestValid = new AddUrlRequest(addurlrequest); + haveError.push(...addurlrequestValid.validate()); + if (haveError.length > 0) { + return Promise.resolve(haveError); + } + return await fetch(`/control/filtering/add_url`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify(addurlrequestValid.serialize()), + }).then(async (res) => { + if (res.status === 200) { + return res.status; + } else { + return new Error(String(res.status)); + } + }) + } + + static async filteringCheckHost(name?: string): Promise { + const queryParams = { + name: name, + } + return await fetch(`/control/filtering/check_host?${qs.stringify(queryParams, { arrayFormat: 'comma' })}`, { + method: 'GET', + }).then(async (res) => { + if (res.status === 200) { + return res.json(); + } else { + return new Error(String(res.status)); + } + }) + } + + static async filteringConfig(filterconfig: IFilterConfig): Promise { + const haveError: string[] = []; + const filterconfigValid = new FilterConfig(filterconfig); + haveError.push(...filterconfigValid.validate()); + if (haveError.length > 0) { + return Promise.resolve(haveError); + } + return await fetch(`/control/filtering/config`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify(filterconfigValid.serialize()), + }).then(async (res) => { + if (res.status === 200) { + return res.status; + } else { + return new Error(String(res.status)); + } + }) + } + + static async filteringRefresh(filterrefreshrequest: IFilterRefreshRequest): Promise { + const haveError: string[] = []; + const filterrefreshrequestValid = new FilterRefreshRequest(filterrefreshrequest); + haveError.push(...filterrefreshrequestValid.validate()); + if (haveError.length > 0) { + return Promise.resolve(haveError); + } + return await fetch(`/control/filtering/refresh`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify(filterrefreshrequestValid.serialize()), + }).then(async (res) => { + if (res.status === 200) { + return res.json(); + } else { + return new Error(String(res.status)); + } + }) + } + + static async filteringRemoveURL(removeurlrequest: IRemoveUrlRequest): Promise { + const haveError: string[] = []; + const removeurlrequestValid = new RemoveUrlRequest(removeurlrequest); + haveError.push(...removeurlrequestValid.validate()); + if (haveError.length > 0) { + return Promise.resolve(haveError); + } + return await fetch(`/control/filtering/remove_url`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify(removeurlrequestValid.serialize()), + }).then(async (res) => { + if (res.status === 200) { + return res.status; + } else { + return new Error(String(res.status)); + } + }) + } + + static async filteringSetRules(data: string): Promise { + const params = String(data); + return await fetch(`/control/filtering/set_rules`, { + method: 'POST', + headers: { + 'Content-Type': 'text/plain', + }, + body: params, + }).then(async (res) => { + if (res.status === 200) { + return res.status; + } else { + return new Error(String(res.status)); + } + }) + } + + static async filteringSetURL(filterseturl: IFilterSetUrl): Promise { + const haveError: string[] = []; + const filterseturlValid = new FilterSetUrl(filterseturl); + haveError.push(...filterseturlValid.validate()); + if (haveError.length > 0) { + return Promise.resolve(haveError); + } + return await fetch(`/control/filtering/set_url`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify(filterseturlValid.serialize()), + }).then(async (res) => { + if (res.status === 200) { + return res.status; + } else { + return new Error(String(res.status)); + } + }) + } + + static async filteringStatus(): Promise { + return await fetch(`/control/filtering/status`, { + method: 'GET', + }).then(async (res) => { + if (res.status === 200) { + return res.json(); + } else { + return new Error(String(res.status)); + } + }) + } +} diff --git a/client2/src/lib/apis/global.ts b/client2/src/lib/apis/global.ts new file mode 100644 index 00000000..d66e7139 --- /dev/null +++ b/client2/src/lib/apis/global.ts @@ -0,0 +1,160 @@ +import DNSConfig, { IDNSConfig } from 'Entities/DNSConfig'; +import GetVersionRequest, { IGetVersionRequest } from 'Entities/GetVersionRequest'; +import Login, { ILogin } from 'Entities/Login'; +import ProfileInfo, { IProfileInfo } from 'Entities/ProfileInfo'; +import ServerStatus, { IServerStatus } from 'Entities/ServerStatus'; +import UpstreamsConfig, { IUpstreamsConfig } from 'Entities/UpstreamsConfig'; +import UpstreamsConfigResponse, { IUpstreamsConfigResponse } from 'Entities/UpstreamsConfigResponse'; +import VersionInfo, { IVersionInfo } from 'Entities/VersionInfo'; + +// This file was autogenerated. Please do not change. +// All changes will be overwrited on commit. +export default class GlobalApi { + static async beginUpdate(): Promise { + return await fetch(`/control/update`, { + method: 'POST', + }).then(async (res) => { + if (res.status === 200) { + return res.status; + } else { + return new Error(String(res.status)); + } + }) + } + + static async dnsConfig(dnsconfig: IDNSConfig): Promise { + const haveError: string[] = []; + const dnsconfigValid = new DNSConfig(dnsconfig); + haveError.push(...dnsconfigValid.validate()); + if (haveError.length > 0) { + return Promise.resolve(haveError); + } + return await fetch(`/control/dns_config`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify(dnsconfigValid.serialize()), + }).then(async (res) => { + if (res.status === 200) { + return res.status; + } else { + return new Error(String(res.status)); + } + }) + } + + static async dnsInfo(): Promise { + return await fetch(`/control/dns_info`, { + method: 'GET', + }).then(async (res) => { + if (res.status === 200) { + return res.json(); + } else { + return new Error(String(res.status)); + } + }) + } + + static async getProfile(): Promise { + return await fetch(`/control/profile`, { + method: 'GET', + }).then(async (res) => { + if (res.status === 200) { + return res.json(); + } else { + return new Error(String(res.status)); + } + }) + } + + static async getVersionJson(getversionrequest: IGetVersionRequest): Promise { + const haveError: string[] = []; + const getversionrequestValid = new GetVersionRequest(getversionrequest); + haveError.push(...getversionrequestValid.validate()); + if (haveError.length > 0) { + return Promise.resolve(haveError); + } + return await fetch(`/control/version.json`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify(getversionrequestValid.serialize()), + }).then(async (res) => { + if (res.status === 200) { + return res.json(); + } else { + return new Error(String(res.status)); + } + }) + } + + static async login(login: ILogin): Promise { + const haveError: string[] = []; + const loginValid = new Login(login); + haveError.push(...loginValid.validate()); + if (haveError.length > 0) { + return Promise.resolve(haveError); + } + return await fetch(`/control/login`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify(loginValid.serialize()), + }).then(async (res) => { + if (res.status === 200) { + return res.status; + } else { + return new Error(String(res.status)); + } + }) + } + + static async logout(): Promise { + return await fetch(`/control/logout`, { + method: 'GET', + }).then(async (res) => { + if (res.status === 200) { + return res.status; + } else { + return new Error(String(res.status)); + } + }) + } + + static async status(): Promise { + return await fetch(`/control/status`, { + method: 'GET', + }).then(async (res) => { + if (res.status === 200) { + return res.json(); + } else { + return new Error(String(res.status)); + } + }) + } + + static async testUpstreamDNS(upstreamsconfig: IUpstreamsConfig): Promise { + const haveError: string[] = []; + const upstreamsconfigValid = new UpstreamsConfig(upstreamsconfig); + haveError.push(...upstreamsconfigValid.validate()); + if (haveError.length > 0) { + return Promise.resolve(haveError); + } + return await fetch(`/control/test_upstream_dns`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify(upstreamsconfigValid.serialize()), + }).then(async (res) => { + if (res.status === 200) { + return res.json(); + } else { + return new Error(String(res.status)); + } + }) + } +} diff --git a/client2/src/lib/apis/i18n.ts b/client2/src/lib/apis/i18n.ts new file mode 100644 index 00000000..045ea104 --- /dev/null +++ b/client2/src/lib/apis/i18n.ts @@ -0,0 +1,32 @@ +// This file was autogenerated. Please do not change. +// All changes will be overwrited on commit. +export default class I18nApi { + static async changeLanguage(data: string): Promise { + const params = String(data); + return await fetch(`/control/i18n/change_language`, { + method: 'POST', + headers: { + 'Content-Type': 'text/plain', + }, + body: params, + }).then(async (res) => { + if (res.status === 200) { + return res.status; + } else { + return new Error(String(res.status)); + } + }) + } + + static async currentLanguage(): Promise { + return await fetch(`/control/i18n/current_language`, { + method: 'GET', + }).then(async (res) => { + if (res.status === 200) { + return res.status; + } else { + return new Error(String(res.status)); + } + }) + } +} diff --git a/client2/src/lib/apis/install.ts b/client2/src/lib/apis/install.ts new file mode 100644 index 00000000..6de0d230 --- /dev/null +++ b/client2/src/lib/apis/install.ts @@ -0,0 +1,123 @@ +import AddressesInfo, { IAddressesInfo } from 'Entities/AddressesInfo'; +import AddressesInfoBeta, { IAddressesInfoBeta } from 'Entities/AddressesInfoBeta'; +import CheckConfigRequest, { ICheckConfigRequest } from 'Entities/CheckConfigRequest'; +import CheckConfigRequestBeta, { ICheckConfigRequestBeta } from 'Entities/CheckConfigRequestBeta'; +import CheckConfigResponse, { ICheckConfigResponse } from 'Entities/CheckConfigResponse'; +import InitialConfiguration, { IInitialConfiguration } from 'Entities/InitialConfiguration'; +import InitialConfigurationBeta, { IInitialConfigurationBeta } from 'Entities/InitialConfigurationBeta'; + +// This file was autogenerated. Please do not change. +// All changes will be overwrited on commit. +export default class InstallApi { + static async installCheckConfig(checkconfigrequest: ICheckConfigRequest): Promise { + const haveError: string[] = []; + const checkconfigrequestValid = new CheckConfigRequest(checkconfigrequest); + haveError.push(...checkconfigrequestValid.validate()); + if (haveError.length > 0) { + return Promise.resolve(haveError); + } + return await fetch(`/control/install/check_config`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify(checkconfigrequestValid.serialize()), + }).then(async (res) => { + if (res.status === 200) { + return res.json(); + } else { + return new Error(String(res.status)); + } + }) + } + + static async installCheckConfigBeta(checkconfigrequestbeta: ICheckConfigRequestBeta): Promise { + const haveError: string[] = []; + const checkconfigrequestbetaValid = new CheckConfigRequestBeta(checkconfigrequestbeta); + haveError.push(...checkconfigrequestbetaValid.validate()); + if (haveError.length > 0) { + return Promise.resolve(haveError); + } + return await fetch(`/control/install/check_config_beta`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify(checkconfigrequestbetaValid.serialize()), + }).then(async (res) => { + if (res.status === 200) { + return res.json(); + } else { + return new Error(String(res.status)); + } + }) + } + + static async installConfigure(initialconfiguration: IInitialConfiguration): Promise { + const haveError: string[] = []; + const initialconfigurationValid = new InitialConfiguration(initialconfiguration); + haveError.push(...initialconfigurationValid.validate()); + if (haveError.length > 0) { + return Promise.resolve(haveError); + } + return await fetch(`/control/install/configure`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify(initialconfigurationValid.serialize()), + }).then(async (res) => { + if (res.status === 200) { + return res.status; + } else { + return new Error(String(res.status)); + } + }) + } + + static async installConfigureBeta(initialconfigurationbeta: IInitialConfigurationBeta): Promise { + const haveError: string[] = []; + const initialconfigurationbetaValid = new InitialConfigurationBeta(initialconfigurationbeta); + haveError.push(...initialconfigurationbetaValid.validate()); + if (haveError.length > 0) { + return Promise.resolve(haveError); + } + return await fetch(`/control/install/configure_beta`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify(initialconfigurationbetaValid.serialize()), + }).then(async (res) => { + if (res.status === 200) { + return res.status; + } else { + return new Error(String(res.status)); + } + }) + } + + static async installGetAddresses(): Promise { + return await fetch(`/control/install/get_addresses`, { + method: 'GET', + }).then(async (res) => { + if (res.status === 200) { + return res.json(); + } else { + return new Error(String(res.status)); + } + }) + } + + static async installGetAddressesBeta(): Promise { + return await fetch(`/control/install/get_addresses_beta`, { + method: 'GET', + }).then(async (res) => { + if (res.status === 200) { + return res.json(); + } else { + return new Error(String(res.status)); + } + }) + } +} diff --git a/client2/src/lib/apis/log.ts b/client2/src/lib/apis/log.ts new file mode 100644 index 00000000..f0aa37fb --- /dev/null +++ b/client2/src/lib/apis/log.ts @@ -0,0 +1,72 @@ +import qs from 'qs'; +import QueryLog, { IQueryLog } from 'Entities/QueryLog'; +import QueryLogConfig, { IQueryLogConfig } from 'Entities/QueryLogConfig'; + +// This file was autogenerated. Please do not change. +// All changes will be overwrited on commit. +export default class LogApi { + static async queryLog(older_than?: string, offset?: number, limit?: number, search?: string, response_status?: string): Promise { + const queryParams = { + older_than: older_than, + offset: offset, + limit: limit, + search: search, + response_status: response_status, + } + return await fetch(`/control/querylog?${qs.stringify(queryParams, { arrayFormat: 'comma' })}`, { + method: 'GET', + }).then(async (res) => { + if (res.status === 200) { + return res.json(); + } else { + return new Error(String(res.status)); + } + }) + } + + static async queryLogConfig(querylogconfig: IQueryLogConfig): Promise { + const haveError: string[] = []; + const querylogconfigValid = new QueryLogConfig(querylogconfig); + haveError.push(...querylogconfigValid.validate()); + if (haveError.length > 0) { + return Promise.resolve(haveError); + } + return await fetch(`/control/querylog_config`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify(querylogconfigValid.serialize()), + }).then(async (res) => { + if (res.status === 200) { + return res.status; + } else { + return new Error(String(res.status)); + } + }) + } + + static async queryLogInfo(): Promise { + return await fetch(`/control/querylog_info`, { + method: 'GET', + }).then(async (res) => { + if (res.status === 200) { + return res.json(); + } else { + return new Error(String(res.status)); + } + }) + } + + static async querylogClear(): Promise { + return await fetch(`/control/querylog_clear`, { + method: 'POST', + }).then(async (res) => { + if (res.status === 200) { + return res.status; + } else { + return new Error(String(res.status)); + } + }) + } +} diff --git a/client2/src/lib/apis/mobileconfig.ts b/client2/src/lib/apis/mobileconfig.ts new file mode 100644 index 00000000..5abeffa0 --- /dev/null +++ b/client2/src/lib/apis/mobileconfig.ts @@ -0,0 +1,37 @@ +import qs from 'qs'; + +// This file was autogenerated. Please do not change. +// All changes will be overwrited on commit. +export default class MobileconfigApi { + static async mobileConfigDoH(host?: string, client_id?: string): Promise { + const queryParams = { + host: host, + client_id: client_id, + } + return await fetch(`/control/apple/doh.mobileconfig?${qs.stringify(queryParams, { arrayFormat: 'comma' })}`, { + method: 'GET', + }).then(async (res) => { + if (res.status === 200) { + return res.status; + } else { + return new Error(String(res.status)); + } + }) + } + + static async mobileConfigDoT(host?: string, client_id?: string): Promise { + const queryParams = { + host: host, + client_id: client_id, + } + return await fetch(`/control/apple/dot.mobileconfig?${qs.stringify(queryParams, { arrayFormat: 'comma' })}`, { + method: 'GET', + }).then(async (res) => { + if (res.status === 200) { + return res.status; + } else { + return new Error(String(res.status)); + } + }) + } +} diff --git a/client2/src/lib/apis/parental.ts b/client2/src/lib/apis/parental.ts new file mode 100644 index 00000000..61f693f5 --- /dev/null +++ b/client2/src/lib/apis/parental.ts @@ -0,0 +1,44 @@ +// This file was autogenerated. Please do not change. +// All changes will be overwrited on commit. +export default class ParentalApi { + static async parentalDisable(): Promise { + return await fetch(`/control/parental/disable`, { + method: 'POST', + }).then(async (res) => { + if (res.status === 200) { + return res.status; + } else { + return new Error(String(res.status)); + } + }) + } + + static async parentalEnable(data: string): Promise { + const params = String(data); + return await fetch(`/control/parental/enable`, { + method: 'POST', + headers: { + 'Content-Type': 'text/plain', + }, + body: params, + }).then(async (res) => { + if (res.status === 200) { + return res.status; + } else { + return new Error(String(res.status)); + } + }) + } + + static async parentalStatus(): Promise { + return await fetch(`/control/parental/status`, { + method: 'GET', + }).then(async (res) => { + if (res.status === 200) { + return res.json(); + } else { + return new Error(String(res.status)); + } + }) + } +} diff --git a/client2/src/lib/apis/rewrite.ts b/client2/src/lib/apis/rewrite.ts new file mode 100644 index 00000000..eded5c97 --- /dev/null +++ b/client2/src/lib/apis/rewrite.ts @@ -0,0 +1,61 @@ +import RewriteEntry, { IRewriteEntry } from 'Entities/RewriteEntry'; + +// This file was autogenerated. Please do not change. +// All changes will be overwrited on commit. +export default class RewriteApi { + static async rewriteAdd(rewriteentry: IRewriteEntry): Promise { + const haveError: string[] = []; + const rewriteentryValid = new RewriteEntry(rewriteentry); + haveError.push(...rewriteentryValid.validate()); + if (haveError.length > 0) { + return Promise.resolve(haveError); + } + return await fetch(`/control/rewrite/add`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify(rewriteentryValid.serialize()), + }).then(async (res) => { + if (res.status === 200) { + return res.status; + } else { + return new Error(String(res.status)); + } + }) + } + + static async rewriteDelete(rewriteentry: IRewriteEntry): Promise { + const haveError: string[] = []; + const rewriteentryValid = new RewriteEntry(rewriteentry); + haveError.push(...rewriteentryValid.validate()); + if (haveError.length > 0) { + return Promise.resolve(haveError); + } + return await fetch(`/control/rewrite/delete`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify(rewriteentryValid.serialize()), + }).then(async (res) => { + if (res.status === 200) { + return res.status; + } else { + return new Error(String(res.status)); + } + }) + } + + static async rewriteList(): Promise { + return await fetch(`/control/rewrite/list`, { + method: 'GET', + }).then(async (res) => { + if (res.status === 200) { + return res.json(); + } else { + return new Error(String(res.status)); + } + }) + } +} diff --git a/client2/src/lib/apis/safebrowsing.ts b/client2/src/lib/apis/safebrowsing.ts new file mode 100644 index 00000000..9d82a5a1 --- /dev/null +++ b/client2/src/lib/apis/safebrowsing.ts @@ -0,0 +1,39 @@ +// This file was autogenerated. Please do not change. +// All changes will be overwrited on commit. +export default class SafebrowsingApi { + static async safebrowsingDisable(): Promise { + return await fetch(`/control/safebrowsing/disable`, { + method: 'POST', + }).then(async (res) => { + if (res.status === 200) { + return res.status; + } else { + return new Error(String(res.status)); + } + }) + } + + static async safebrowsingEnable(): Promise { + return await fetch(`/control/safebrowsing/enable`, { + method: 'POST', + }).then(async (res) => { + if (res.status === 200) { + return res.status; + } else { + return new Error(String(res.status)); + } + }) + } + + static async safebrowsingStatus(): Promise { + return await fetch(`/control/safebrowsing/status`, { + method: 'GET', + }).then(async (res) => { + if (res.status === 200) { + return res.json(); + } else { + return new Error(String(res.status)); + } + }) + } +} diff --git a/client2/src/lib/apis/safesearch.ts b/client2/src/lib/apis/safesearch.ts new file mode 100644 index 00000000..e36e79f9 --- /dev/null +++ b/client2/src/lib/apis/safesearch.ts @@ -0,0 +1,39 @@ +// This file was autogenerated. Please do not change. +// All changes will be overwrited on commit. +export default class SafesearchApi { + static async safesearchDisable(): Promise { + return await fetch(`/control/safesearch/disable`, { + method: 'POST', + }).then(async (res) => { + if (res.status === 200) { + return res.status; + } else { + return new Error(String(res.status)); + } + }) + } + + static async safesearchEnable(): Promise { + return await fetch(`/control/safesearch/enable`, { + method: 'POST', + }).then(async (res) => { + if (res.status === 200) { + return res.status; + } else { + return new Error(String(res.status)); + } + }) + } + + static async safesearchStatus(): Promise { + return await fetch(`/control/safesearch/status`, { + method: 'GET', + }).then(async (res) => { + if (res.status === 200) { + return res.json(); + } else { + return new Error(String(res.status)); + } + }) + } +} diff --git a/client2/src/lib/apis/stats.ts b/client2/src/lib/apis/stats.ts new file mode 100644 index 00000000..630f17c0 --- /dev/null +++ b/client2/src/lib/apis/stats.ts @@ -0,0 +1,64 @@ +import Stats, { IStats } from 'Entities/Stats'; +import StatsConfig, { IStatsConfig } from 'Entities/StatsConfig'; + +// This file was autogenerated. Please do not change. +// All changes will be overwrited on commit. +export default class StatsApi { + static async stats(): Promise { + return await fetch(`/control/stats`, { + method: 'GET', + }).then(async (res) => { + if (res.status === 200) { + return res.json(); + } else { + return new Error(String(res.status)); + } + }) + } + + static async statsConfig(statsconfig: IStatsConfig): Promise { + const haveError: string[] = []; + const statsconfigValid = new StatsConfig(statsconfig); + haveError.push(...statsconfigValid.validate()); + if (haveError.length > 0) { + return Promise.resolve(haveError); + } + return await fetch(`/control/stats_config`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify(statsconfigValid.serialize()), + }).then(async (res) => { + if (res.status === 200) { + return res.status; + } else { + return new Error(String(res.status)); + } + }) + } + + static async statsInfo(): Promise { + return await fetch(`/control/stats_info`, { + method: 'GET', + }).then(async (res) => { + if (res.status === 200) { + return res.json(); + } else { + return new Error(String(res.status)); + } + }) + } + + static async statsReset(): Promise { + return await fetch(`/control/stats_reset`, { + method: 'POST', + }).then(async (res) => { + if (res.status === 200) { + return res.status; + } else { + return new Error(String(res.status)); + } + }) + } +} diff --git a/client2/src/lib/apis/tls.ts b/client2/src/lib/apis/tls.ts new file mode 100644 index 00000000..c9f064d7 --- /dev/null +++ b/client2/src/lib/apis/tls.ts @@ -0,0 +1,61 @@ +import TlsConfig, { ITlsConfig } from 'Entities/TlsConfig'; + +// This file was autogenerated. Please do not change. +// All changes will be overwrited on commit. +export default class TlsApi { + static async tlsConfigure(tlsconfig: ITlsConfig): Promise { + const haveError: string[] = []; + const tlsconfigValid = new TlsConfig(tlsconfig); + haveError.push(...tlsconfigValid.validate()); + if (haveError.length > 0) { + return Promise.resolve(haveError); + } + return await fetch(`/control/tls/configure`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify(tlsconfigValid.serialize()), + }).then(async (res) => { + if (res.status === 200) { + return res.json(); + } else { + return new Error(String(res.status)); + } + }) + } + + static async tlsStatus(): Promise { + return await fetch(`/control/tls/status`, { + method: 'GET', + }).then(async (res) => { + if (res.status === 200) { + return res.json(); + } else { + return new Error(String(res.status)); + } + }) + } + + static async tlsValidate(tlsconfig: ITlsConfig): Promise { + const haveError: string[] = []; + const tlsconfigValid = new TlsConfig(tlsconfig); + haveError.push(...tlsconfigValid.validate()); + if (haveError.length > 0) { + return Promise.resolve(haveError); + } + return await fetch(`/control/tls/validate`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify(tlsconfigValid.serialize()), + }).then(async (res) => { + if (res.status === 200) { + return res.json(); + } else { + return new Error(String(res.status)); + } + }) + } +} diff --git a/client2/src/lib/consts/common.ts b/client2/src/lib/consts/common.ts new file mode 100644 index 00000000..668536b0 --- /dev/null +++ b/client2/src/lib/consts/common.ts @@ -0,0 +1,3 @@ +export const DEFAULT_NOTIFICATION_DURATION = 5; + +export const DHCP_LINK = 'https://github.com/AdguardTeam/AdGuardHome/wiki/DHCP'; diff --git a/client2/src/lib/consts/forms.ts b/client2/src/lib/consts/forms.ts new file mode 100644 index 00000000..85b4220d --- /dev/null +++ b/client2/src/lib/consts/forms.ts @@ -0,0 +1 @@ +export const EMPTY_FIELD_ERROR = 'empty_field'; diff --git a/client2/src/lib/consts/install.ts b/client2/src/lib/consts/install.ts new file mode 100644 index 00000000..fcc3904f --- /dev/null +++ b/client2/src/lib/consts/install.ts @@ -0,0 +1,7 @@ +export const DEFAULT_IP_ADDRESS = '0.0.0.0'; + +export const DEFAULT_IP_PORT = 80; + +export const DEFAULT_DNS_ADDRESS = '0.0.0.0'; + +export const DEFAULT_DNS_PORT = 53; diff --git a/client2/src/lib/entities/AccessList.ts b/client2/src/lib/entities/AccessList.ts new file mode 100644 index 00000000..7d127465 --- /dev/null +++ b/client2/src/lib/entities/AccessList.ts @@ -0,0 +1,76 @@ +// This file was autogenerated. Please do not change. +// All changes will be overwrited on commit. +export interface IAccessList { + allowed_clients?: string[]; + blocked_hosts?: string[]; + disallowed_clients?: string[]; +} + +export default class AccessList { + readonly _allowed_clients: string[] | undefined; + + /** */ + get allowedClients(): string[] | undefined { + return this._allowed_clients; + } + + readonly _blocked_hosts: string[] | undefined; + + /** */ + get blockedHosts(): string[] | undefined { + return this._blocked_hosts; + } + + readonly _disallowed_clients: string[] | undefined; + + /** */ + get disallowedClients(): string[] | undefined { + return this._disallowed_clients; + } + + constructor(props: IAccessList) { + if (props.allowed_clients) { + this._allowed_clients = props.allowed_clients; + } + if (props.blocked_hosts) { + this._blocked_hosts = props.blocked_hosts; + } + if (props.disallowed_clients) { + this._disallowed_clients = props.disallowed_clients; + } + } + + serialize(): IAccessList { + const data: IAccessList = { + }; + if (typeof this._allowed_clients !== 'undefined') { + data.allowed_clients = this._allowed_clients; + } + if (typeof this._blocked_hosts !== 'undefined') { + data.blocked_hosts = this._blocked_hosts; + } + if (typeof this._disallowed_clients !== 'undefined') { + data.disallowed_clients = this._disallowed_clients; + } + return data; + } + + validate(): string[] { + const validate = { + allowed_clients: !this._allowed_clients ? true : this._allowed_clients.reduce((result, p) => result && typeof p === 'string', true), + disallowed_clients: !this._disallowed_clients ? true : this._disallowed_clients.reduce((result, p) => result && typeof p === 'string', true), + blocked_hosts: !this._blocked_hosts ? true : this._blocked_hosts.reduce((result, p) => result && typeof p === 'string', true), + }; + const isError: string[] = []; + Object.keys(validate).forEach((key) => { + if (!(validate as any)[key]) { + isError.push(key); + } + }); + return isError; + } + + update(props: Partial): AccessList { + return new AccessList({ ...this.serialize(), ...props }); + } +} diff --git a/client2/src/lib/entities/AccessListResponse.ts b/client2/src/lib/entities/AccessListResponse.ts new file mode 100644 index 00000000..c4443f0a --- /dev/null +++ b/client2/src/lib/entities/AccessListResponse.ts @@ -0,0 +1,6 @@ +import AccessList, { IAccessList } from './AccessList'; + +// This file was autogenerated. Please do not change. +// All changes will be overwrited on commit. +export type IAccessListResponse = IAccessList; +export default AccessList; diff --git a/client2/src/lib/entities/AccessSetRequest.ts b/client2/src/lib/entities/AccessSetRequest.ts new file mode 100644 index 00000000..45ebbab1 --- /dev/null +++ b/client2/src/lib/entities/AccessSetRequest.ts @@ -0,0 +1,6 @@ +import AccessList, { IAccessList } from './AccessList'; + +// This file was autogenerated. Please do not change. +// All changes will be overwrited on commit. +export type IAccessSetRequest = IAccessList; +export default AccessList; diff --git a/client2/src/lib/entities/AddUrlRequest.ts b/client2/src/lib/entities/AddUrlRequest.ts new file mode 100644 index 00000000..076dd4d2 --- /dev/null +++ b/client2/src/lib/entities/AddUrlRequest.ts @@ -0,0 +1,78 @@ +// This file was autogenerated. Please do not change. +// All changes will be overwrited on commit. +export interface IAddUrlRequest { + name?: string; + url?: string; + whitelist?: boolean; +} + +export default class AddUrlRequest { + readonly _name: string | undefined; + + get name(): string | undefined { + return this._name; + } + + readonly _url: string | undefined; + + /** + * Description: URL or an absolute path to the file containing filtering rules. + * + * Example: https://filters.adtidy.org/windows/filters/15.txt + */ + get url(): string | undefined { + return this._url; + } + + readonly _whitelist: boolean | undefined; + + get whitelist(): boolean | undefined { + return this._whitelist; + } + + constructor(props: IAddUrlRequest) { + if (typeof props.name === 'string') { + this._name = props.name.trim(); + } + if (typeof props.url === 'string') { + this._url = props.url.trim(); + } + if (typeof props.whitelist === 'boolean') { + this._whitelist = props.whitelist; + } + } + + serialize(): IAddUrlRequest { + const data: IAddUrlRequest = { + }; + if (typeof this._name !== 'undefined') { + data.name = this._name; + } + if (typeof this._url !== 'undefined') { + data.url = this._url; + } + if (typeof this._whitelist !== 'undefined') { + data.whitelist = this._whitelist; + } + return data; + } + + validate(): string[] { + const validate = { + name: !this._name ? true : typeof this._name === 'string' && !this._name ? true : this._name, + url: !this._url ? true : typeof this._url === 'string' && !this._url ? true : this._url, + whitelist: !this._whitelist ? true : typeof this._whitelist === 'boolean', + }; + const isError: string[] = []; + Object.keys(validate).forEach((key) => { + if (!(validate as any)[key]) { + isError.push(key); + } + }); + return isError; + } + + update(props: Partial): AddUrlRequest { + return new AddUrlRequest({ ...this.serialize(), ...props }); + } +} diff --git a/client2/src/lib/entities/AddressInfo.ts b/client2/src/lib/entities/AddressInfo.ts new file mode 100644 index 00000000..1afdecb4 --- /dev/null +++ b/client2/src/lib/entities/AddressInfo.ts @@ -0,0 +1,67 @@ +// This file was autogenerated. Please do not change. +// All changes will be overwrited on commit. +export interface IAddressInfo { + ip: string; + port: number; +} + +export default class AddressInfo { + readonly _ip: string; + + /** + * Description: undefined + * Example: 127.0.0.1 + */ + get ip(): string { + return this._ip; + } + + static ipValidate(ip: string): boolean { + return typeof ip === 'string' && !!ip.trim(); + } + + readonly _port: number; + + /** + * Description: undefined + * Example: 53 + */ + get port(): number { + return this._port; + } + + static portValidate(port: number): boolean { + return typeof port === 'number'; + } + + constructor(props: IAddressInfo) { + this._ip = props.ip.trim(); + this._port = props.port; + } + + serialize(): IAddressInfo { + const data: IAddressInfo = { + ip: this._ip, + port: this._port, + }; + return data; + } + + validate(): string[] { + const validate = { + ip: typeof this._ip === 'string' && !this._ip ? true : this._ip, + port: typeof this._port === 'number', + }; + const isError: string[] = []; + Object.keys(validate).forEach((key) => { + if (!(validate as any)[key]) { + isError.push(key); + } + }); + return isError; + } + + update(props: Partial): AddressInfo { + return new AddressInfo({ ...this.serialize(), ...props }); + } +} diff --git a/client2/src/lib/entities/AddressInfoBeta.ts b/client2/src/lib/entities/AddressInfoBeta.ts new file mode 100644 index 00000000..8b5073a3 --- /dev/null +++ b/client2/src/lib/entities/AddressInfoBeta.ts @@ -0,0 +1,71 @@ +// This file was autogenerated. Please do not change. +// All changes will be overwrited on commit. +export interface IAddressInfoBeta { + ip: string[]; + port: number; +} + +export default class AddressInfoBeta { + readonly _ip: string[]; + + /** + * Description: undefined + * Example: 127.0.0.1 + */ + get ip(): string[] { + return this._ip; + } + + static get ipMinItems() { + return 1; + } + + static ipValidate(ip: string[]): boolean { + return ip.reduce((result, p) => result && (typeof p === 'string' && !!p.trim()), true); + } + + readonly _port: number; + + /** + * Description: undefined + * Example: 53 + */ + get port(): number { + return this._port; + } + + static portValidate(port: number): boolean { + return typeof port === 'number'; + } + + constructor(props: IAddressInfoBeta) { + this._ip = props.ip; + this._port = props.port; + } + + serialize(): IAddressInfoBeta { + const data: IAddressInfoBeta = { + ip: this._ip, + port: this._port, + }; + return data; + } + + validate(): string[] { + const validate = { + ip: this._ip.reduce((result, p) => result && typeof p === 'string', true), + port: typeof this._port === 'number', + }; + const isError: string[] = []; + Object.keys(validate).forEach((key) => { + if (!(validate as any)[key]) { + isError.push(key); + } + }); + return isError; + } + + update(props: Partial): AddressInfoBeta { + return new AddressInfoBeta({ ...this.serialize(), ...props }); + } +} diff --git a/client2/src/lib/entities/AddressesInfo.ts b/client2/src/lib/entities/AddressesInfo.ts new file mode 100644 index 00000000..af864b17 --- /dev/null +++ b/client2/src/lib/entities/AddressesInfo.ts @@ -0,0 +1,79 @@ +import NetInterfaces, { INetInterfaces } from './NetInterfaces'; + +// This file was autogenerated. Please do not change. +// All changes will be overwrited on commit. +export interface IAddressesInfo { + dns_port: number; + interfaces: INetInterfaces; + web_port: number; +} + +export default class AddressesInfo { + readonly _dns_port: number; + + /** + * Description: undefined + * Example: 53 + */ + get dnsPort(): number { + return this._dns_port; + } + + static dnsPortValidate(dnsPort: number): boolean { + return typeof dnsPort === 'number'; + } + + readonly _interfaces: NetInterfaces; + + get interfaces(): NetInterfaces { + return this._interfaces; + } + + readonly _web_port: number; + + /** + * Description: undefined + * Example: 80 + */ + get webPort(): number { + return this._web_port; + } + + static webPortValidate(webPort: number): boolean { + return typeof webPort === 'number'; + } + + constructor(props: IAddressesInfo) { + this._dns_port = props.dns_port; + this._interfaces = new NetInterfaces(props.interfaces); + this._web_port = props.web_port; + } + + serialize(): IAddressesInfo { + const data: IAddressesInfo = { + dns_port: this._dns_port, + interfaces: this._interfaces.serialize(), + web_port: this._web_port, + }; + return data; + } + + validate(): string[] { + const validate = { + dns_port: typeof this._dns_port === 'number', + web_port: typeof this._web_port === 'number', + interfaces: this._interfaces.validate().length === 0, + }; + const isError: string[] = []; + Object.keys(validate).forEach((key) => { + if (!(validate as any)[key]) { + isError.push(key); + } + }); + return isError; + } + + update(props: Partial): AddressesInfo { + return new AddressesInfo({ ...this.serialize(), ...props }); + } +} diff --git a/client2/src/lib/entities/AddressesInfoBeta.ts b/client2/src/lib/entities/AddressesInfoBeta.ts new file mode 100644 index 00000000..603adafe --- /dev/null +++ b/client2/src/lib/entities/AddressesInfoBeta.ts @@ -0,0 +1,80 @@ +import NetInterface, { INetInterface } from './NetInterface'; + +// This file was autogenerated. Please do not change. +// All changes will be overwrited on commit. +export interface IAddressesInfoBeta { + dns_port: number; + interfaces: INetInterface[]; + web_port: number; +} + +export default class AddressesInfoBeta { + readonly _dns_port: number; + + /** + * Description: undefined + * Example: 53 + */ + get dnsPort(): number { + return this._dns_port; + } + + static dnsPortValidate(dnsPort: number): boolean { + return typeof dnsPort === 'number'; + } + + readonly _interfaces: NetInterface[]; + + /** */ + get interfaces(): NetInterface[] { + return this._interfaces; + } + + readonly _web_port: number; + + /** + * Description: undefined + * Example: 80 + */ + get webPort(): number { + return this._web_port; + } + + static webPortValidate(webPort: number): boolean { + return typeof webPort === 'number'; + } + + constructor(props: IAddressesInfoBeta) { + this._dns_port = props.dns_port; + this._interfaces = props.interfaces.map((p) => new NetInterface(p)); + this._web_port = props.web_port; + } + + serialize(): IAddressesInfoBeta { + const data: IAddressesInfoBeta = { + dns_port: this._dns_port, + interfaces: this._interfaces.map((p) => p.serialize()), + web_port: this._web_port, + }; + return data; + } + + validate(): string[] { + const validate = { + dns_port: typeof this._dns_port === 'number', + web_port: typeof this._web_port === 'number', + interfaces: this._interfaces.reduce((result, p) => result && p.validate().length === 0, true), + }; + const isError: string[] = []; + Object.keys(validate).forEach((key) => { + if (!(validate as any)[key]) { + isError.push(key); + } + }); + return isError; + } + + update(props: Partial): AddressesInfoBeta { + return new AddressesInfoBeta({ ...this.serialize(), ...props }); + } +} diff --git a/client2/src/lib/entities/BlockedServicesArray.ts b/client2/src/lib/entities/BlockedServicesArray.ts new file mode 100644 index 00000000..6c175abe --- /dev/null +++ b/client2/src/lib/entities/BlockedServicesArray.ts @@ -0,0 +1,31 @@ +// This file was autogenerated. Please do not change. +// All changes will be overwrited on commit. +export interface IBlockedServicesArray { +} + +export default class BlockedServicesArray { + constructor(props: IBlockedServicesArray) { + } + + serialize(): IBlockedServicesArray { + const data: IBlockedServicesArray = { + }; + return data; + } + + validate(): string[] { + const validate = { + }; + const isError: string[] = []; + Object.keys(validate).forEach((key) => { + if (!(validate as any)[key]) { + isError.push(key); + } + }); + return isError; + } + + update(props: Partial): BlockedServicesArray { + return new BlockedServicesArray({ ...this.serialize(), ...props }); + } +} diff --git a/client2/src/lib/entities/CheckConfigRequest.ts b/client2/src/lib/entities/CheckConfigRequest.ts new file mode 100644 index 00000000..dba0f741 --- /dev/null +++ b/client2/src/lib/entities/CheckConfigRequest.ts @@ -0,0 +1,75 @@ +import CheckConfigRequestInfo, { ICheckConfigRequestInfo } from './CheckConfigRequestInfo'; + +// This file was autogenerated. Please do not change. +// All changes will be overwrited on commit. +export interface ICheckConfigRequest { + dns?: ICheckConfigRequestInfo; + set_static_ip?: boolean; + web?: ICheckConfigRequestInfo; +} + +export default class CheckConfigRequest { + readonly _dns: CheckConfigRequestInfo | undefined; + + get dns(): CheckConfigRequestInfo | undefined { + return this._dns; + } + + readonly _set_static_ip: boolean | undefined; + + get setStaticIp(): boolean | undefined { + return this._set_static_ip; + } + + readonly _web: CheckConfigRequestInfo | undefined; + + get web(): CheckConfigRequestInfo | undefined { + return this._web; + } + + constructor(props: ICheckConfigRequest) { + if (props.dns) { + this._dns = new CheckConfigRequestInfo(props.dns); + } + if (typeof props.set_static_ip === 'boolean') { + this._set_static_ip = props.set_static_ip; + } + if (props.web) { + this._web = new CheckConfigRequestInfo(props.web); + } + } + + serialize(): ICheckConfigRequest { + const data: ICheckConfigRequest = { + }; + if (typeof this._dns !== 'undefined') { + data.dns = this._dns.serialize(); + } + if (typeof this._set_static_ip !== 'undefined') { + data.set_static_ip = this._set_static_ip; + } + if (typeof this._web !== 'undefined') { + data.web = this._web.serialize(); + } + return data; + } + + validate(): string[] { + const validate = { + dns: !this._dns ? true : this._dns.validate().length === 0, + web: !this._web ? true : this._web.validate().length === 0, + set_static_ip: !this._set_static_ip ? true : typeof this._set_static_ip === 'boolean', + }; + const isError: string[] = []; + Object.keys(validate).forEach((key) => { + if (!(validate as any)[key]) { + isError.push(key); + } + }); + return isError; + } + + update(props: Partial): CheckConfigRequest { + return new CheckConfigRequest({ ...this.serialize(), ...props }); + } +} diff --git a/client2/src/lib/entities/CheckConfigRequestBeta.ts b/client2/src/lib/entities/CheckConfigRequestBeta.ts new file mode 100644 index 00000000..b34c855f --- /dev/null +++ b/client2/src/lib/entities/CheckConfigRequestBeta.ts @@ -0,0 +1,75 @@ +import CheckConfigRequestInfoBeta, { ICheckConfigRequestInfoBeta } from './CheckConfigRequestInfoBeta'; + +// This file was autogenerated. Please do not change. +// All changes will be overwrited on commit. +export interface ICheckConfigRequestBeta { + dns?: ICheckConfigRequestInfoBeta; + set_static_ip?: boolean; + web?: ICheckConfigRequestInfoBeta; +} + +export default class CheckConfigRequestBeta { + readonly _dns: CheckConfigRequestInfoBeta | undefined; + + get dns(): CheckConfigRequestInfoBeta | undefined { + return this._dns; + } + + readonly _set_static_ip: boolean | undefined; + + get setStaticIp(): boolean | undefined { + return this._set_static_ip; + } + + readonly _web: CheckConfigRequestInfoBeta | undefined; + + get web(): CheckConfigRequestInfoBeta | undefined { + return this._web; + } + + constructor(props: ICheckConfigRequestBeta) { + if (props.dns) { + this._dns = new CheckConfigRequestInfoBeta(props.dns); + } + if (typeof props.set_static_ip === 'boolean') { + this._set_static_ip = props.set_static_ip; + } + if (props.web) { + this._web = new CheckConfigRequestInfoBeta(props.web); + } + } + + serialize(): ICheckConfigRequestBeta { + const data: ICheckConfigRequestBeta = { + }; + if (typeof this._dns !== 'undefined') { + data.dns = this._dns.serialize(); + } + if (typeof this._set_static_ip !== 'undefined') { + data.set_static_ip = this._set_static_ip; + } + if (typeof this._web !== 'undefined') { + data.web = this._web.serialize(); + } + return data; + } + + validate(): string[] { + const validate = { + dns: !this._dns ? true : this._dns.validate().length === 0, + web: !this._web ? true : this._web.validate().length === 0, + set_static_ip: !this._set_static_ip ? true : typeof this._set_static_ip === 'boolean', + }; + const isError: string[] = []; + Object.keys(validate).forEach((key) => { + if (!(validate as any)[key]) { + isError.push(key); + } + }); + return isError; + } + + update(props: Partial): CheckConfigRequestBeta { + return new CheckConfigRequestBeta({ ...this.serialize(), ...props }); + } +} diff --git a/client2/src/lib/entities/CheckConfigRequestInfo.ts b/client2/src/lib/entities/CheckConfigRequestInfo.ts new file mode 100644 index 00000000..c8da0cf3 --- /dev/null +++ b/client2/src/lib/entities/CheckConfigRequestInfo.ts @@ -0,0 +1,81 @@ +// This file was autogenerated. Please do not change. +// All changes will be overwrited on commit. +export interface ICheckConfigRequestInfo { + autofix?: boolean; + ip?: string; + port?: number; +} + +export default class CheckConfigRequestInfo { + readonly _autofix: boolean | undefined; + + get autofix(): boolean | undefined { + return this._autofix; + } + + readonly _ip: string | undefined; + + /** + * Description: undefined + * Example: 127.0.0.1 + */ + get ip(): string | undefined { + return this._ip; + } + + readonly _port: number | undefined; + + /** + * Description: undefined + * Example: 53 + */ + get port(): number | undefined { + return this._port; + } + + constructor(props: ICheckConfigRequestInfo) { + if (typeof props.autofix === 'boolean') { + this._autofix = props.autofix; + } + if (typeof props.ip === 'string') { + this._ip = props.ip.trim(); + } + if (typeof props.port === 'number') { + this._port = props.port; + } + } + + serialize(): ICheckConfigRequestInfo { + const data: ICheckConfigRequestInfo = { + }; + if (typeof this._autofix !== 'undefined') { + data.autofix = this._autofix; + } + if (typeof this._ip !== 'undefined') { + data.ip = this._ip; + } + if (typeof this._port !== 'undefined') { + data.port = this._port; + } + return data; + } + + validate(): string[] { + const validate = { + ip: !this._ip ? true : typeof this._ip === 'string' && !this._ip ? true : this._ip, + port: !this._port ? true : typeof this._port === 'number', + autofix: !this._autofix ? true : typeof this._autofix === 'boolean', + }; + const isError: string[] = []; + Object.keys(validate).forEach((key) => { + if (!(validate as any)[key]) { + isError.push(key); + } + }); + return isError; + } + + update(props: Partial): CheckConfigRequestInfo { + return new CheckConfigRequestInfo({ ...this.serialize(), ...props }); + } +} diff --git a/client2/src/lib/entities/CheckConfigRequestInfoBeta.ts b/client2/src/lib/entities/CheckConfigRequestInfoBeta.ts new file mode 100644 index 00000000..d086e771 --- /dev/null +++ b/client2/src/lib/entities/CheckConfigRequestInfoBeta.ts @@ -0,0 +1,85 @@ +// This file was autogenerated. Please do not change. +// All changes will be overwrited on commit. +export interface ICheckConfigRequestInfoBeta { + autofix?: boolean; + ip?: string[]; + port?: number; +} + +export default class CheckConfigRequestInfoBeta { + readonly _autofix: boolean | undefined; + + get autofix(): boolean | undefined { + return this._autofix; + } + + readonly _ip: string[] | undefined; + + /** + * Description: undefined + * Example: 127.0.0.1 + */ + get ip(): string[] | undefined { + return this._ip; + } + + static get ipMinItems() { + return 1; + } + + readonly _port: number | undefined; + + /** + * Description: undefined + * Example: 53 + */ + get port(): number | undefined { + return this._port; + } + + constructor(props: ICheckConfigRequestInfoBeta) { + if (typeof props.autofix === 'boolean') { + this._autofix = props.autofix; + } + if (props.ip) { + this._ip = props.ip; + } + if (typeof props.port === 'number') { + this._port = props.port; + } + } + + serialize(): ICheckConfigRequestInfoBeta { + const data: ICheckConfigRequestInfoBeta = { + }; + if (typeof this._autofix !== 'undefined') { + data.autofix = this._autofix; + } + if (typeof this._ip !== 'undefined') { + data.ip = this._ip; + } + if (typeof this._port !== 'undefined') { + data.port = this._port; + } + return data; + } + + validate(): string[] { + const validate = { + ip: !this._ip ? true : this._ip.reduce((result, p) => result && typeof p === 'string', true), + port: !this._port ? true : typeof this._port === 'number', + autofix: !this._autofix ? true : typeof this._autofix === 'boolean', + }; + const isError: string[] = []; + Object.keys(validate).forEach((key) => { + if (!(validate as any)[key]) { + isError.push(key); + } + }); + return isError; + } + + update(props: Partial): CheckConfigRequestInfoBeta { + return new CheckConfigRequestInfoBeta({ ...this.serialize(), ...props }); + } +} diff --git a/client2/src/lib/entities/CheckConfigResponse.ts b/client2/src/lib/entities/CheckConfigResponse.ts new file mode 100644 index 00000000..74d45519 --- /dev/null +++ b/client2/src/lib/entities/CheckConfigResponse.ts @@ -0,0 +1,64 @@ +import CheckConfigResponseInfo, { ICheckConfigResponseInfo } from './CheckConfigResponseInfo'; +import CheckConfigStaticIpInfo, { ICheckConfigStaticIpInfo } from './CheckConfigStaticIpInfo'; + +// This file was autogenerated. Please do not change. +// All changes will be overwrited on commit. +export interface ICheckConfigResponse { + dns: ICheckConfigResponseInfo; + static_ip: ICheckConfigStaticIpInfo; + web: ICheckConfigResponseInfo; +} + +export default class CheckConfigResponse { + readonly _dns: CheckConfigResponseInfo; + + get dns(): CheckConfigResponseInfo { + return this._dns; + } + + readonly _static_ip: CheckConfigStaticIpInfo; + + get staticIp(): CheckConfigStaticIpInfo { + return this._static_ip; + } + + readonly _web: CheckConfigResponseInfo; + + get web(): CheckConfigResponseInfo { + return this._web; + } + + constructor(props: ICheckConfigResponse) { + this._dns = new CheckConfigResponseInfo(props.dns); + this._static_ip = new CheckConfigStaticIpInfo(props.static_ip); + this._web = new CheckConfigResponseInfo(props.web); + } + + serialize(): ICheckConfigResponse { + const data: ICheckConfigResponse = { + dns: this._dns.serialize(), + static_ip: this._static_ip.serialize(), + web: this._web.serialize(), + }; + return data; + } + + validate(): string[] { + const validate = { + dns: this._dns.validate().length === 0, + web: this._web.validate().length === 0, + static_ip: this._static_ip.validate().length === 0, + }; + const isError: string[] = []; + Object.keys(validate).forEach((key) => { + if (!(validate as any)[key]) { + isError.push(key); + } + }); + return isError; + } + + update(props: Partial): CheckConfigResponse { + return new CheckConfigResponse({ ...this.serialize(), ...props }); + } +} diff --git a/client2/src/lib/entities/CheckConfigResponseInfo.ts b/client2/src/lib/entities/CheckConfigResponseInfo.ts new file mode 100644 index 00000000..09d9d2a9 --- /dev/null +++ b/client2/src/lib/entities/CheckConfigResponseInfo.ts @@ -0,0 +1,59 @@ +// This file was autogenerated. Please do not change. +// All changes will be overwrited on commit. +export interface ICheckConfigResponseInfo { + can_autofix: boolean; + status: string; +} + +export default class CheckConfigResponseInfo { + readonly _can_autofix: boolean; + + get canAutofix(): boolean { + return this._can_autofix; + } + + static canAutofixValidate(canAutofix: boolean): boolean { + return typeof canAutofix === 'boolean'; + } + + readonly _status: string; + + get status(): string { + return this._status; + } + + static statusValidate(status: string): boolean { + return typeof status === 'string' && !!status.trim(); + } + + constructor(props: ICheckConfigResponseInfo) { + this._can_autofix = props.can_autofix; + this._status = props.status.trim(); + } + + serialize(): ICheckConfigResponseInfo { + const data: ICheckConfigResponseInfo = { + can_autofix: this._can_autofix, + status: this._status, + }; + return data; + } + + validate(): string[] { + const validate = { + status: typeof this._status === 'string' && !this._status ? true : this._status, + can_autofix: typeof this._can_autofix === 'boolean', + }; + const isError: string[] = []; + Object.keys(validate).forEach((key) => { + if (!(validate as any)[key]) { + isError.push(key); + } + }); + return isError; + } + + update(props: Partial): CheckConfigResponseInfo { + return new CheckConfigResponseInfo({ ...this.serialize(), ...props }); + } +} diff --git a/client2/src/lib/entities/CheckConfigStaticIpInfo.ts b/client2/src/lib/entities/CheckConfigStaticIpInfo.ts new file mode 100644 index 00000000..b7c7a34c --- /dev/null +++ b/client2/src/lib/entities/CheckConfigStaticIpInfo.ts @@ -0,0 +1,79 @@ +import { CheckConfigStaticIpInfoStatic } from './CheckConfigStaticIpInfoStatic'; + +// This file was autogenerated. Please do not change. +// All changes will be overwrited on commit. +export interface ICheckConfigStaticIpInfo { + error?: string; + ip?: string; + static?: CheckConfigStaticIpInfoStatic; +} + +export default class CheckConfigStaticIpInfo { + readonly _error: string | undefined; + + /** */ + get error(): string | undefined { + return this._error; + } + + readonly _ip: string | undefined; + + /** + * Description: Current dynamic IP address. Set if static=no + * Example: 192.168.1.1 + */ + get ip(): string | undefined { + return this._ip; + } + + readonly _static: CheckConfigStaticIpInfoStatic | undefined; + + get static(): CheckConfigStaticIpInfoStatic | undefined { + return this._static; + } + + constructor(props: ICheckConfigStaticIpInfo) { + if (typeof props.error === 'string') { + this._error = props.error.trim(); + } + if (typeof props.ip === 'string') { + this._ip = props.ip.trim(); + } + if (props.static) { + this._static = props.static; + } + } + + serialize(): ICheckConfigStaticIpInfo { + const data: ICheckConfigStaticIpInfo = { + }; + if (typeof this._error !== 'undefined') { + data.error = this._error; + } + if (typeof this._ip !== 'undefined') { + data.ip = this._ip; + } + if (typeof this._static !== 'undefined') { + data.static = this._static; + } + return data; + } + + validate(): string[] { + const validate = { + ip: !this._ip ? true : typeof this._ip === 'string' && !this._ip ? true : this._ip, + error: !this._error ? true : typeof this._error === 'string' && !this._error ? true : this._error, + }; + const isError: string[] = []; + Object.keys(validate).forEach((key) => { + if (!(validate as any)[key]) { + isError.push(key); + } + }); + return isError; + } + + update(props: Partial): CheckConfigStaticIpInfo { + return new CheckConfigStaticIpInfo({ ...this.serialize(), ...props }); + } +} diff --git a/client2/src/lib/entities/CheckConfigStaticIpInfoStatic.ts b/client2/src/lib/entities/CheckConfigStaticIpInfoStatic.ts new file mode 100644 index 00000000..9609e1ec --- /dev/null +++ b/client2/src/lib/entities/CheckConfigStaticIpInfoStatic.ts @@ -0,0 +1,7 @@ +// This file was autogenerated. Please do not change. +// All changes will be overwrited on commit. +export enum CheckConfigStaticIpInfoStatic { + YES = 'yes', + NO = 'no', + ERROR = 'error' +} diff --git a/client2/src/lib/entities/Client.ts b/client2/src/lib/entities/Client.ts new file mode 100644 index 00000000..c86a9d8f --- /dev/null +++ b/client2/src/lib/entities/Client.ts @@ -0,0 +1,176 @@ +// This file was autogenerated. Please do not change. +// All changes will be overwrited on commit. +export interface IClient { + blocked_services?: string[]; + filtering_enabled?: boolean; + ids?: string[]; + name?: string; + parental_enabled?: boolean; + safebrowsing_enabled?: boolean; + safesearch_enabled?: boolean; + upstreams?: string[]; + use_global_blocked_services?: boolean; + use_global_settings?: boolean; +} + +export default class Client { + readonly _blocked_services: string[] | undefined; + + get blockedServices(): string[] | undefined { + return this._blocked_services; + } + + readonly _filtering_enabled: boolean | undefined; + + get filteringEnabled(): boolean | undefined { + return this._filtering_enabled; + } + + readonly _ids: string[] | undefined; + + /** */ + get ids(): string[] | undefined { + return this._ids; + } + + readonly _name: string | undefined; + + /** + * Description: Name + * Example: localhost + */ + get name(): string | undefined { + return this._name; + } + + readonly _parental_enabled: boolean | undefined; + + get parentalEnabled(): boolean | undefined { + return this._parental_enabled; + } + + readonly _safebrowsing_enabled: boolean | undefined; + + get safebrowsingEnabled(): boolean | undefined { + return this._safebrowsing_enabled; + } + + readonly _safesearch_enabled: boolean | undefined; + + get safesearchEnabled(): boolean | undefined { + return this._safesearch_enabled; + } + + readonly _upstreams: string[] | undefined; + + get upstreams(): string[] | undefined { + return this._upstreams; + } + + readonly _use_global_blocked_services: boolean | undefined; + + get useGlobalBlockedServices(): boolean | undefined { + return this._use_global_blocked_services; + } + + readonly _use_global_settings: boolean | undefined; + + get useGlobalSettings(): boolean | undefined { + return this._use_global_settings; + } + + constructor(props: IClient) { + if (props.blocked_services) { + this._blocked_services = props.blocked_services; + } + if (typeof props.filtering_enabled === 'boolean') { + this._filtering_enabled = props.filtering_enabled; + } + if (props.ids) { + this._ids = props.ids; + } + if (typeof props.name === 'string') { + this._name = props.name.trim(); + } + if (typeof props.parental_enabled === 'boolean') { + this._parental_enabled = props.parental_enabled; + } + if (typeof props.safebrowsing_enabled === 'boolean') { + this._safebrowsing_enabled = props.safebrowsing_enabled; + } + if (typeof props.safesearch_enabled === 'boolean') { + this._safesearch_enabled = props.safesearch_enabled; + } + if (props.upstreams) { + this._upstreams = props.upstreams; + } + if (typeof props.use_global_blocked_services === 'boolean') { + this._use_global_blocked_services = props.use_global_blocked_services; + } + if (typeof props.use_global_settings === 'boolean') { + this._use_global_settings = props.use_global_settings; + } + } + + serialize(): IClient { + const data: IClient = { + }; + if (typeof this._blocked_services !== 'undefined') { + data.blocked_services = this._blocked_services; + } + if (typeof this._filtering_enabled !== 'undefined') { + data.filtering_enabled = this._filtering_enabled; + } + if (typeof this._ids !== 'undefined') { + data.ids = this._ids; + } + if (typeof this._name !== 'undefined') { + data.name = this._name; + } + if (typeof this._parental_enabled !== 'undefined') { + data.parental_enabled = this._parental_enabled; + } + if (typeof this._safebrowsing_enabled !== 'undefined') { + data.safebrowsing_enabled = this._safebrowsing_enabled; + } + if (typeof this._safesearch_enabled !== 'undefined') { + data.safesearch_enabled = this._safesearch_enabled; + } + if (typeof this._upstreams !== 'undefined') { + data.upstreams = this._upstreams; + } + if (typeof this._use_global_blocked_services !== 'undefined') { + data.use_global_blocked_services = this._use_global_blocked_services; + } + if (typeof this._use_global_settings !== 'undefined') { + data.use_global_settings = this._use_global_settings; + } + return data; + } + + validate(): string[] { + const validate = { + name: !this._name ? true : typeof this._name === 'string' && !this._name ? true : this._name, + ids: !this._ids ? true : this._ids.reduce((result, p) => result && typeof p === 'string', true), + use_global_settings: !this._use_global_settings ? true : typeof this._use_global_settings === 'boolean', + filtering_enabled: !this._filtering_enabled ? true : typeof this._filtering_enabled === 'boolean', + parental_enabled: !this._parental_enabled ? true : typeof this._parental_enabled === 'boolean', + safebrowsing_enabled: !this._safebrowsing_enabled ? true : typeof this._safebrowsing_enabled === 'boolean', + safesearch_enabled: !this._safesearch_enabled ? true : typeof this._safesearch_enabled === 'boolean', + use_global_blocked_services: !this._use_global_blocked_services ? true : typeof this._use_global_blocked_services === 'boolean', + blocked_services: !this._blocked_services ? true : this._blocked_services.reduce((result, p) => result && typeof p === 'string', true), + upstreams: !this._upstreams ? true : this._upstreams.reduce((result, p) => result && typeof p === 'string', true), + }; + const isError: string[] = []; + Object.keys(validate).forEach((key) => { + if (!(validate as any)[key]) { + isError.push(key); + } + }); + return isError; + } + + update(props: Partial): Client { + return new Client({ ...this.serialize(), ...props }); + } +} diff --git a/client2/src/lib/entities/ClientAuto.ts b/client2/src/lib/entities/ClientAuto.ts new file mode 100644 index 00000000..2696a4c2 --- /dev/null +++ b/client2/src/lib/entities/ClientAuto.ts @@ -0,0 +1,85 @@ +// This file was autogenerated. Please do not change. +// All changes will be overwrited on commit. +export interface IClientAuto { + ip?: string; + name?: string; + source?: string; +} + +export default class ClientAuto { + readonly _ip: string | undefined; + + /** + * Description: IP address + * Example: 127.0.0.1 + */ + get ip(): string | undefined { + return this._ip; + } + + readonly _name: string | undefined; + + /** + * Description: Name + * Example: localhost + */ + get name(): string | undefined { + return this._name; + } + + readonly _source: string | undefined; + + /** + * Description: The source of this information + * Example: etc/hosts + */ + get source(): string | undefined { + return this._source; + } + + constructor(props: IClientAuto) { + if (typeof props.ip === 'string') { + this._ip = props.ip.trim(); + } + if (typeof props.name === 'string') { + this._name = props.name.trim(); + } + if (typeof props.source === 'string') { + this._source = props.source.trim(); + } + } + + serialize(): IClientAuto { + const data: IClientAuto = { + }; + if (typeof this._ip !== 'undefined') { + data.ip = this._ip; + } + if (typeof this._name !== 'undefined') { + data.name = this._name; + } + if (typeof this._source !== 'undefined') { + data.source = this._source; + } + return data; + } + + validate(): string[] { + const validate = { + ip: !this._ip ? true : typeof this._ip === 'string' && !this._ip ? true : this._ip, + name: !this._name ? true : typeof this._name === 'string' && !this._name ? true : this._name, + source: !this._source ? true : typeof this._source === 'string' && !this._source ? true : this._source, + }; + const isError: string[] = []; + Object.keys(validate).forEach((key) => { + if (!(validate as any)[key]) { + isError.push(key); + } + }); + return isError; + } + + update(props: Partial): ClientAuto { + return new ClientAuto({ ...this.serialize(), ...props }); + } +} diff --git a/client2/src/lib/entities/ClientDelete.ts b/client2/src/lib/entities/ClientDelete.ts new file mode 100644 index 00000000..6a009c03 --- /dev/null +++ b/client2/src/lib/entities/ClientDelete.ts @@ -0,0 +1,45 @@ +// This file was autogenerated. Please do not change. +// All changes will be overwrited on commit. +export interface IClientDelete { + name?: string; +} + +export default class ClientDelete { + readonly _name: string | undefined; + + get name(): string | undefined { + return this._name; + } + + constructor(props: IClientDelete) { + if (typeof props.name === 'string') { + this._name = props.name.trim(); + } + } + + serialize(): IClientDelete { + const data: IClientDelete = { + }; + if (typeof this._name !== 'undefined') { + data.name = this._name; + } + return data; + } + + validate(): string[] { + const validate = { + name: !this._name ? true : typeof this._name === 'string' && !this._name ? true : this._name, + }; + const isError: string[] = []; + Object.keys(validate).forEach((key) => { + if (!(validate as any)[key]) { + isError.push(key); + } + }); + return isError; + } + + update(props: Partial): ClientDelete { + return new ClientDelete({ ...this.serialize(), ...props }); + } +} diff --git a/client2/src/lib/entities/ClientFindSubEntry.ts b/client2/src/lib/entities/ClientFindSubEntry.ts new file mode 100644 index 00000000..8d067549 --- /dev/null +++ b/client2/src/lib/entities/ClientFindSubEntry.ts @@ -0,0 +1,222 @@ +import WhoisInfo, { IWhoisInfo } from './WhoisInfo'; + +// This file was autogenerated. Please do not change. +// All changes will be overwrited on commit. +export interface IClientFindSubEntry { + blocked_services?: string[]; + disallowed?: boolean; + disallowed_rule?: string; + filtering_enabled?: boolean; + ids?: string[]; + name?: string; + parental_enabled?: boolean; + safebrowsing_enabled?: boolean; + safesearch_enabled?: boolean; + upstreams?: string[]; + use_global_blocked_services?: boolean; + use_global_settings?: boolean; + whois_info?: IWhoisInfo; +} + +export default class ClientFindSubEntry { + readonly _blocked_services: string[] | undefined; + + get blockedServices(): string[] | undefined { + return this._blocked_services; + } + + readonly _disallowed: boolean | undefined; + + /** */ + get disallowed(): boolean | undefined { + return this._disallowed; + } + + readonly _disallowed_rule: string | undefined; + + /** */ + get disallowedRule(): string | undefined { + return this._disallowed_rule; + } + + readonly _filtering_enabled: boolean | undefined; + + get filteringEnabled(): boolean | undefined { + return this._filtering_enabled; + } + + readonly _ids: string[] | undefined; + + /** */ + get ids(): string[] | undefined { + return this._ids; + } + + readonly _name: string | undefined; + + /** + * Description: Name + * Example: localhost + */ + get name(): string | undefined { + return this._name; + } + + readonly _parental_enabled: boolean | undefined; + + get parentalEnabled(): boolean | undefined { + return this._parental_enabled; + } + + readonly _safebrowsing_enabled: boolean | undefined; + + get safebrowsingEnabled(): boolean | undefined { + return this._safebrowsing_enabled; + } + + readonly _safesearch_enabled: boolean | undefined; + + get safesearchEnabled(): boolean | undefined { + return this._safesearch_enabled; + } + + readonly _upstreams: string[] | undefined; + + get upstreams(): string[] | undefined { + return this._upstreams; + } + + readonly _use_global_blocked_services: boolean | undefined; + + get useGlobalBlockedServices(): boolean | undefined { + return this._use_global_blocked_services; + } + + readonly _use_global_settings: boolean | undefined; + + get useGlobalSettings(): boolean | undefined { + return this._use_global_settings; + } + + readonly _whois_info: WhoisInfo | undefined; + + get whoisInfo(): WhoisInfo | undefined { + return this._whois_info; + } + + constructor(props: IClientFindSubEntry) { + if (props.blocked_services) { + this._blocked_services = props.blocked_services; + } + if (typeof props.disallowed === 'boolean') { + this._disallowed = props.disallowed; + } + if (typeof props.disallowed_rule === 'string') { + this._disallowed_rule = props.disallowed_rule.trim(); + } + if (typeof props.filtering_enabled === 'boolean') { + this._filtering_enabled = props.filtering_enabled; + } + if (props.ids) { + this._ids = props.ids; + } + if (typeof props.name === 'string') { + this._name = props.name.trim(); + } + if (typeof props.parental_enabled === 'boolean') { + this._parental_enabled = props.parental_enabled; + } + if (typeof props.safebrowsing_enabled === 'boolean') { + this._safebrowsing_enabled = props.safebrowsing_enabled; + } + if (typeof props.safesearch_enabled === 'boolean') { + this._safesearch_enabled = props.safesearch_enabled; + } + if (props.upstreams) { + this._upstreams = props.upstreams; + } + if (typeof props.use_global_blocked_services === 'boolean') { + this._use_global_blocked_services = props.use_global_blocked_services; + } + if (typeof props.use_global_settings === 'boolean') { + this._use_global_settings = props.use_global_settings; + } + if (props.whois_info) { + this._whois_info = new WhoisInfo(props.whois_info); + } + } + + serialize(): IClientFindSubEntry { + const data: IClientFindSubEntry = { + }; + if (typeof this._blocked_services !== 'undefined') { + data.blocked_services = this._blocked_services; + } + if (typeof this._disallowed !== 'undefined') { + data.disallowed = this._disallowed; + } + if (typeof this._disallowed_rule !== 'undefined') { + data.disallowed_rule = this._disallowed_rule; + } + if (typeof this._filtering_enabled !== 'undefined') { + data.filtering_enabled = this._filtering_enabled; + } + if (typeof this._ids !== 'undefined') { + data.ids = this._ids; + } + if (typeof this._name !== 'undefined') { + data.name = this._name; + } + if (typeof this._parental_enabled !== 'undefined') { + data.parental_enabled = this._parental_enabled; + } + if (typeof this._safebrowsing_enabled !== 'undefined') { + data.safebrowsing_enabled = this._safebrowsing_enabled; + } + if (typeof this._safesearch_enabled !== 'undefined') { + data.safesearch_enabled = this._safesearch_enabled; + } + if (typeof this._upstreams !== 'undefined') { + data.upstreams = this._upstreams; + } + if (typeof this._use_global_blocked_services !== 'undefined') { + data.use_global_blocked_services = this._use_global_blocked_services; + } + if (typeof this._use_global_settings !== 'undefined') { + data.use_global_settings = this._use_global_settings; + } + if (typeof this._whois_info !== 'undefined') { + data.whois_info = this._whois_info.serialize(); + } + return data; + } + + validate(): string[] { + const validate = { + name: !this._name ? true : typeof this._name === 'string' && !this._name ? true : this._name, + ids: !this._ids ? true : this._ids.reduce((result, p) => result && typeof p === 'string', true), + use_global_settings: !this._use_global_settings ? true : typeof this._use_global_settings === 'boolean', + filtering_enabled: !this._filtering_enabled ? true : typeof this._filtering_enabled === 'boolean', + parental_enabled: !this._parental_enabled ? true : typeof this._parental_enabled === 'boolean', + safebrowsing_enabled: !this._safebrowsing_enabled ? true : typeof this._safebrowsing_enabled === 'boolean', + safesearch_enabled: !this._safesearch_enabled ? true : typeof this._safesearch_enabled === 'boolean', + use_global_blocked_services: !this._use_global_blocked_services ? true : typeof this._use_global_blocked_services === 'boolean', + blocked_services: !this._blocked_services ? true : this._blocked_services.reduce((result, p) => result && typeof p === 'string', true), + upstreams: !this._upstreams ? true : this._upstreams.reduce((result, p) => result && typeof p === 'string', true), + whois_info: !this._whois_info ? true : this._whois_info.validate().length === 0, + disallowed: !this._disallowed ? true : typeof this._disallowed === 'boolean', + disallowed_rule: !this._disallowed_rule ? true : typeof this._disallowed_rule === 'string' && !this._disallowed_rule ? true : this._disallowed_rule, + }; + const isError: string[] = []; + Object.keys(validate).forEach((key) => { + if (!(validate as any)[key]) { + isError.push(key); + } + }); + return isError; + } + + update(props: Partial): ClientFindSubEntry { + return new ClientFindSubEntry({ ...this.serialize(), ...props }); + } +} diff --git a/client2/src/lib/entities/ClientUpdate.ts b/client2/src/lib/entities/ClientUpdate.ts new file mode 100644 index 00000000..2a817b3a --- /dev/null +++ b/client2/src/lib/entities/ClientUpdate.ts @@ -0,0 +1,61 @@ +import Client, { IClient } from './Client'; + +// This file was autogenerated. Please do not change. +// All changes will be overwrited on commit. +export interface IClientUpdate { + data?: IClient; + name?: string; +} + +export default class ClientUpdate { + readonly _data: Client | undefined; + + get data(): Client | undefined { + return this._data; + } + + readonly _name: string | undefined; + + get name(): string | undefined { + return this._name; + } + + constructor(props: IClientUpdate) { + if (props.data) { + this._data = new Client(props.data); + } + if (typeof props.name === 'string') { + this._name = props.name.trim(); + } + } + + serialize(): IClientUpdate { + const data: IClientUpdate = { + }; + if (typeof this._data !== 'undefined') { + data.data = this._data.serialize(); + } + if (typeof this._name !== 'undefined') { + data.name = this._name; + } + return data; + } + + validate(): string[] { + const validate = { + name: !this._name ? true : typeof this._name === 'string' && !this._name ? true : this._name, + data: !this._data ? true : this._data.validate().length === 0, + }; + const isError: string[] = []; + Object.keys(validate).forEach((key) => { + if (!(validate as any)[key]) { + isError.push(key); + } + }); + return isError; + } + + update(props: Partial): ClientUpdate { + return new ClientUpdate({ ...this.serialize(), ...props }); + } +} diff --git a/client2/src/lib/entities/Clients.ts b/client2/src/lib/entities/Clients.ts new file mode 100644 index 00000000..8ba5a8e8 --- /dev/null +++ b/client2/src/lib/entities/Clients.ts @@ -0,0 +1,62 @@ +import Client, { IClient } from './Client'; +import ClientAuto, { IClientAuto } from './ClientAuto'; + +// This file was autogenerated. Please do not change. +// All changes will be overwrited on commit. +export interface IClients { + auto_clients?: IClientAuto[]; + clients?: IClient[]; +} + +export default class Clients { + readonly _auto_clients: ClientAuto[] | undefined; + + get autoClients(): ClientAuto[] | undefined { + return this._auto_clients; + } + + readonly _clients: Client[] | undefined; + + get clients(): Client[] | undefined { + return this._clients; + } + + constructor(props: IClients) { + if (props.auto_clients) { + this._auto_clients = props.auto_clients.map((p) => new ClientAuto(p)); + } + if (props.clients) { + this._clients = props.clients.map((p) => new Client(p)); + } + } + + serialize(): IClients { + const data: IClients = { + }; + if (typeof this._auto_clients !== 'undefined') { + data.auto_clients = this._auto_clients.map((p) => p.serialize()); + } + if (typeof this._clients !== 'undefined') { + data.clients = this._clients.map((p) => p.serialize()); + } + return data; + } + + validate(): string[] { + const validate = { + clients: !this._clients ? true : this._clients.reduce((result, p) => result && p.validate().length === 0, true), + auto_clients: !this._auto_clients ? true : this._auto_clients.reduce((result, p) => result && p.validate().length === 0, true), + }; + const isError: string[] = []; + Object.keys(validate).forEach((key) => { + if (!(validate as any)[key]) { + isError.push(key); + } + }); + return isError; + } + + update(props: Partial): Clients { + return new Clients({ ...this.serialize(), ...props }); + } +} diff --git a/client2/src/lib/entities/ClientsArray.ts b/client2/src/lib/entities/ClientsArray.ts new file mode 100644 index 00000000..774e162b --- /dev/null +++ b/client2/src/lib/entities/ClientsArray.ts @@ -0,0 +1,31 @@ +// This file was autogenerated. Please do not change. +// All changes will be overwrited on commit. +export interface IClientsArray { +} + +export default class ClientsArray { + constructor(props: IClientsArray) { + } + + serialize(): IClientsArray { + const data: IClientsArray = { + }; + return data; + } + + validate(): string[] { + const validate = { + }; + const isError: string[] = []; + Object.keys(validate).forEach((key) => { + if (!(validate as any)[key]) { + isError.push(key); + } + }); + return isError; + } + + update(props: Partial): ClientsArray { + return new ClientsArray({ ...this.serialize(), ...props }); + } +} diff --git a/client2/src/lib/entities/ClientsAutoArray.ts b/client2/src/lib/entities/ClientsAutoArray.ts new file mode 100644 index 00000000..855c9e8a --- /dev/null +++ b/client2/src/lib/entities/ClientsAutoArray.ts @@ -0,0 +1,31 @@ +// This file was autogenerated. Please do not change. +// All changes will be overwrited on commit. +export interface IClientsAutoArray { +} + +export default class ClientsAutoArray { + constructor(props: IClientsAutoArray) { + } + + serialize(): IClientsAutoArray { + const data: IClientsAutoArray = { + }; + return data; + } + + validate(): string[] { + const validate = { + }; + const isError: string[] = []; + Object.keys(validate).forEach((key) => { + if (!(validate as any)[key]) { + isError.push(key); + } + }); + return isError; + } + + update(props: Partial): ClientsAutoArray { + return new ClientsAutoArray({ ...this.serialize(), ...props }); + } +} diff --git a/client2/src/lib/entities/ClientsFindEntry.ts b/client2/src/lib/entities/ClientsFindEntry.ts new file mode 100644 index 00000000..8b220a78 --- /dev/null +++ b/client2/src/lib/entities/ClientsFindEntry.ts @@ -0,0 +1,33 @@ +import ClientFindSubEntry, { IClientFindSubEntry } from './ClientFindSubEntry'; + +// This file was autogenerated. Please do not change. +// All changes will be overwrited on commit. +export interface IClientsFindEntry { + [key: string]: IClientFindSubEntry; +} + +export default class ClientsFindEntry { + readonly data: Record; + + constructor(props: IClientsFindEntry) { + this.data = Object.entries(props).reduce>((prev, [key, value]) => { + prev[key] = new ClientFindSubEntry(value!); + return prev; + }, {}) + } + + serialize(): IClientsFindEntry { + return Object.entries(this.data).reduce>((prev, [key, value]) => { + prev[key] = value.serialize(); + return prev; + }, {}) + } + + validate(): string[] { + return [] + } + + update(props: IClientsFindEntry): ClientsFindEntry { + return new ClientsFindEntry({ ...this.serialize(), ...props }); + } +} diff --git a/client2/src/lib/entities/ClientsFindResponse.ts b/client2/src/lib/entities/ClientsFindResponse.ts new file mode 100644 index 00000000..63685be6 --- /dev/null +++ b/client2/src/lib/entities/ClientsFindResponse.ts @@ -0,0 +1,31 @@ +// This file was autogenerated. Please do not change. +// All changes will be overwrited on commit. +export interface IClientsFindResponse { +} + +export default class ClientsFindResponse { + constructor(props: IClientsFindResponse) { + } + + serialize(): IClientsFindResponse { + const data: IClientsFindResponse = { + }; + return data; + } + + validate(): string[] { + const validate = { + }; + const isError: string[] = []; + Object.keys(validate).forEach((key) => { + if (!(validate as any)[key]) { + isError.push(key); + } + }); + return isError; + } + + update(props: Partial): ClientsFindResponse { + return new ClientsFindResponse({ ...this.serialize(), ...props }); + } +} diff --git a/client2/src/lib/entities/DNSConfig.ts b/client2/src/lib/entities/DNSConfig.ts new file mode 100644 index 00000000..d56883e5 --- /dev/null +++ b/client2/src/lib/entities/DNSConfig.ts @@ -0,0 +1,250 @@ +// This file was autogenerated. Please do not change. +// All changes will be overwrited on commit. +export interface IDNSConfig { + blocking_ipv4?: string; + blocking_ipv6?: string; + blocking_mode?: string; + bootstrap_dns?: string[]; + cache_size?: number; + cache_ttl_max?: number; + cache_ttl_min?: number; + dhcp_available?: boolean; + dnssec_enabled?: boolean; + edns_cs_enabled?: boolean; + protection_enabled?: boolean; + ratelimit?: number; + upstream_dns?: string[]; + upstream_dns_file?: string; + upstream_mode?: any; +} + +export default class DNSConfig { + readonly _blocking_ipv4: string | undefined; + + get blockingIpv4(): string | undefined { + return this._blocking_ipv4; + } + + readonly _blocking_ipv6: string | undefined; + + get blockingIpv6(): string | undefined { + return this._blocking_ipv6; + } + + readonly _blocking_mode: string | undefined; + + get blockingMode(): string | undefined { + return this._blocking_mode; + } + + readonly _bootstrap_dns: string[] | undefined; + + /** + * Description: Bootstrap servers, port is optional after colon. Empty value will reset it to default values. + * + * Example: 8.8.8.8:53,1.1.1.1:53 + */ + get bootstrapDns(): string[] | undefined { + return this._bootstrap_dns; + } + + readonly _cache_size: number | undefined; + + get cacheSize(): number | undefined { + return this._cache_size; + } + + readonly _cache_ttl_max: number | undefined; + + get cacheTtlMax(): number | undefined { + return this._cache_ttl_max; + } + + readonly _cache_ttl_min: number | undefined; + + get cacheTtlMin(): number | undefined { + return this._cache_ttl_min; + } + + readonly _dhcp_available: boolean | undefined; + + get dhcpAvailable(): boolean | undefined { + return this._dhcp_available; + } + + readonly _dnssec_enabled: boolean | undefined; + + get dnssecEnabled(): boolean | undefined { + return this._dnssec_enabled; + } + + readonly _edns_cs_enabled: boolean | undefined; + + get ednsCsEnabled(): boolean | undefined { + return this._edns_cs_enabled; + } + + readonly _protection_enabled: boolean | undefined; + + get protectionEnabled(): boolean | undefined { + return this._protection_enabled; + } + + readonly _ratelimit: number | undefined; + + get ratelimit(): number | undefined { + return this._ratelimit; + } + + readonly _upstream_dns: string[] | undefined; + + /** + * Description: Upstream servers, port is optional after colon. Empty value will reset it to default values. + * + * Example: tls://1.1.1.1,tls://1.0.0.1 + */ + get upstreamDns(): string[] | undefined { + return this._upstream_dns; + } + + readonly _upstream_dns_file: string | undefined; + + get upstreamDnsFile(): string | undefined { + return this._upstream_dns_file; + } + + readonly _upstream_mode: any | undefined; + + get upstreamMode(): any | undefined { + return this._upstream_mode; + } + + constructor(props: IDNSConfig) { + if (typeof props.blocking_ipv4 === 'string') { + this._blocking_ipv4 = props.blocking_ipv4.trim(); + } + if (typeof props.blocking_ipv6 === 'string') { + this._blocking_ipv6 = props.blocking_ipv6.trim(); + } + if (typeof props.blocking_mode === 'string') { + this._blocking_mode = props.blocking_mode.trim(); + } + if (props.bootstrap_dns) { + this._bootstrap_dns = props.bootstrap_dns; + } + if (typeof props.cache_size === 'number') { + this._cache_size = props.cache_size; + } + if (typeof props.cache_ttl_max === 'number') { + this._cache_ttl_max = props.cache_ttl_max; + } + if (typeof props.cache_ttl_min === 'number') { + this._cache_ttl_min = props.cache_ttl_min; + } + if (typeof props.dhcp_available === 'boolean') { + this._dhcp_available = props.dhcp_available; + } + if (typeof props.dnssec_enabled === 'boolean') { + this._dnssec_enabled = props.dnssec_enabled; + } + if (typeof props.edns_cs_enabled === 'boolean') { + this._edns_cs_enabled = props.edns_cs_enabled; + } + if (typeof props.protection_enabled === 'boolean') { + this._protection_enabled = props.protection_enabled; + } + if (typeof props.ratelimit === 'number') { + this._ratelimit = props.ratelimit; + } + if (props.upstream_dns) { + this._upstream_dns = props.upstream_dns; + } + if (typeof props.upstream_dns_file === 'string') { + this._upstream_dns_file = props.upstream_dns_file.trim(); + } + if (props.upstream_mode) { + this._upstream_mode = props.upstream_mode; + } + } + + serialize(): IDNSConfig { + const data: IDNSConfig = { + }; + if (typeof this._blocking_ipv4 !== 'undefined') { + data.blocking_ipv4 = this._blocking_ipv4; + } + if (typeof this._blocking_ipv6 !== 'undefined') { + data.blocking_ipv6 = this._blocking_ipv6; + } + if (typeof this._blocking_mode !== 'undefined') { + data.blocking_mode = this._blocking_mode; + } + if (typeof this._bootstrap_dns !== 'undefined') { + data.bootstrap_dns = this._bootstrap_dns; + } + if (typeof this._cache_size !== 'undefined') { + data.cache_size = this._cache_size; + } + if (typeof this._cache_ttl_max !== 'undefined') { + data.cache_ttl_max = this._cache_ttl_max; + } + if (typeof this._cache_ttl_min !== 'undefined') { + data.cache_ttl_min = this._cache_ttl_min; + } + if (typeof this._dhcp_available !== 'undefined') { + data.dhcp_available = this._dhcp_available; + } + if (typeof this._dnssec_enabled !== 'undefined') { + data.dnssec_enabled = this._dnssec_enabled; + } + if (typeof this._edns_cs_enabled !== 'undefined') { + data.edns_cs_enabled = this._edns_cs_enabled; + } + if (typeof this._protection_enabled !== 'undefined') { + data.protection_enabled = this._protection_enabled; + } + if (typeof this._ratelimit !== 'undefined') { + data.ratelimit = this._ratelimit; + } + if (typeof this._upstream_dns !== 'undefined') { + data.upstream_dns = this._upstream_dns; + } + if (typeof this._upstream_dns_file !== 'undefined') { + data.upstream_dns_file = this._upstream_dns_file; + } + if (typeof this._upstream_mode !== 'undefined') { + data.upstream_mode = this._upstream_mode; + } + return data; + } + + validate(): string[] { + const validate = { + bootstrap_dns: !this._bootstrap_dns ? true : this._bootstrap_dns.reduce((result, p) => result && typeof p === 'string', true), + upstream_dns: !this._upstream_dns ? true : this._upstream_dns.reduce((result, p) => result && typeof p === 'string', true), + upstream_dns_file: !this._upstream_dns_file ? true : typeof this._upstream_dns_file === 'string' && !this._upstream_dns_file ? true : this._upstream_dns_file, + protection_enabled: !this._protection_enabled ? true : typeof this._protection_enabled === 'boolean', + dhcp_available: !this._dhcp_available ? true : typeof this._dhcp_available === 'boolean', + ratelimit: !this._ratelimit ? true : typeof this._ratelimit === 'number', + blocking_mode: !this._blocking_mode ? true : typeof this._blocking_mode === 'string' && !this._blocking_mode ? true : this._blocking_mode, + blocking_ipv4: !this._blocking_ipv4 ? true : typeof this._blocking_ipv4 === 'string' && !this._blocking_ipv4 ? true : this._blocking_ipv4, + blocking_ipv6: !this._blocking_ipv6 ? true : typeof this._blocking_ipv6 === 'string' && !this._blocking_ipv6 ? true : this._blocking_ipv6, + edns_cs_enabled: !this._edns_cs_enabled ? true : typeof this._edns_cs_enabled === 'boolean', + dnssec_enabled: !this._dnssec_enabled ? true : typeof this._dnssec_enabled === 'boolean', + cache_size: !this._cache_size ? true : typeof this._cache_size === 'number', + cache_ttl_min: !this._cache_ttl_min ? true : typeof this._cache_ttl_min === 'number', + cache_ttl_max: !this._cache_ttl_max ? true : typeof this._cache_ttl_max === 'number', + }; + const isError: string[] = []; + Object.keys(validate).forEach((key) => { + if (!(validate as any)[key]) { + isError.push(key); + } + }); + return isError; + } + + update(props: Partial): DNSConfig { + return new DNSConfig({ ...this.serialize(), ...props }); + } +} diff --git a/client2/src/lib/entities/DhcpConfig.ts b/client2/src/lib/entities/DhcpConfig.ts new file mode 100644 index 00000000..61874da0 --- /dev/null +++ b/client2/src/lib/entities/DhcpConfig.ts @@ -0,0 +1,90 @@ +import DhcpConfigV4, { IDhcpConfigV4 } from './DhcpConfigV4'; +import DhcpConfigV6, { IDhcpConfigV6 } from './DhcpConfigV6'; + +// This file was autogenerated. Please do not change. +// All changes will be overwrited on commit. +export interface IDhcpConfig { + enabled?: boolean; + interface_name?: string; + v4?: IDhcpConfigV4; + v6?: IDhcpConfigV6; +} + +export default class DhcpConfig { + readonly _enabled: boolean | undefined; + + get enabled(): boolean | undefined { + return this._enabled; + } + + readonly _interface_name: string | undefined; + + get interfaceName(): string | undefined { + return this._interface_name; + } + + readonly _v4: DhcpConfigV4 | undefined; + + get v4(): DhcpConfigV4 | undefined { + return this._v4; + } + + readonly _v6: DhcpConfigV6 | undefined; + + get v6(): DhcpConfigV6 | undefined { + return this._v6; + } + + constructor(props: IDhcpConfig) { + if (typeof props.enabled === 'boolean') { + this._enabled = props.enabled; + } + if (typeof props.interface_name === 'string') { + this._interface_name = props.interface_name.trim(); + } + if (props.v4) { + this._v4 = new DhcpConfigV4(props.v4); + } + if (props.v6) { + this._v6 = new DhcpConfigV6(props.v6); + } + } + + serialize(): IDhcpConfig { + const data: IDhcpConfig = { + }; + if (typeof this._enabled !== 'undefined') { + data.enabled = this._enabled; + } + if (typeof this._interface_name !== 'undefined') { + data.interface_name = this._interface_name; + } + if (typeof this._v4 !== 'undefined') { + data.v4 = this._v4.serialize(); + } + if (typeof this._v6 !== 'undefined') { + data.v6 = this._v6.serialize(); + } + return data; + } + + validate(): string[] { + const validate = { + enabled: !this._enabled ? true : typeof this._enabled === 'boolean', + interface_name: !this._interface_name ? true : typeof this._interface_name === 'string' && !this._interface_name ? true : this._interface_name, + v4: !this._v4 ? true : this._v4.validate().length === 0, + v6: !this._v6 ? true : this._v6.validate().length === 0, + }; + const isError: string[] = []; + Object.keys(validate).forEach((key) => { + if (!(validate as any)[key]) { + isError.push(key); + } + }); + return isError; + } + + update(props: Partial): DhcpConfig { + return new DhcpConfig({ ...this.serialize(), ...props }); + } +} diff --git a/client2/src/lib/entities/DhcpConfigV4.ts b/client2/src/lib/entities/DhcpConfigV4.ts new file mode 100644 index 00000000..ae4ebdc2 --- /dev/null +++ b/client2/src/lib/entities/DhcpConfigV4.ts @@ -0,0 +1,117 @@ +// This file was autogenerated. Please do not change. +// All changes will be overwrited on commit. +export interface IDhcpConfigV4 { + gateway_ip?: string; + lease_duration?: number; + range_end?: string; + range_start?: string; + subnet_mask?: string; +} + +export default class DhcpConfigV4 { + readonly _gateway_ip: string | undefined; + + /** + * Description: undefined + * Example: 192.168.1.1 + */ + get gatewayIp(): string | undefined { + return this._gateway_ip; + } + + readonly _lease_duration: number | undefined; + + get leaseDuration(): number | undefined { + return this._lease_duration; + } + + readonly _range_end: string | undefined; + + /** + * Description: undefined + * Example: 192.168.10.50 + */ + get rangeEnd(): string | undefined { + return this._range_end; + } + + readonly _range_start: string | undefined; + + /** + * Description: undefined + * Example: 192.168.1.2 + */ + get rangeStart(): string | undefined { + return this._range_start; + } + + readonly _subnet_mask: string | undefined; + + /** + * Description: undefined + * Example: 255.255.255.0 + */ + get subnetMask(): string | undefined { + return this._subnet_mask; + } + + constructor(props: IDhcpConfigV4) { + if (typeof props.gateway_ip === 'string') { + this._gateway_ip = props.gateway_ip.trim(); + } + if (typeof props.lease_duration === 'number') { + this._lease_duration = props.lease_duration; + } + if (typeof props.range_end === 'string') { + this._range_end = props.range_end.trim(); + } + if (typeof props.range_start === 'string') { + this._range_start = props.range_start.trim(); + } + if (typeof props.subnet_mask === 'string') { + this._subnet_mask = props.subnet_mask.trim(); + } + } + + serialize(): IDhcpConfigV4 { + const data: IDhcpConfigV4 = { + }; + if (typeof this._gateway_ip !== 'undefined') { + data.gateway_ip = this._gateway_ip; + } + if (typeof this._lease_duration !== 'undefined') { + data.lease_duration = this._lease_duration; + } + if (typeof this._range_end !== 'undefined') { + data.range_end = this._range_end; + } + if (typeof this._range_start !== 'undefined') { + data.range_start = this._range_start; + } + if (typeof this._subnet_mask !== 'undefined') { + data.subnet_mask = this._subnet_mask; + } + return data; + } + + validate(): string[] { + const validate = { + gateway_ip: !this._gateway_ip ? true : typeof this._gateway_ip === 'string' && !this._gateway_ip ? true : this._gateway_ip, + subnet_mask: !this._subnet_mask ? true : typeof this._subnet_mask === 'string' && !this._subnet_mask ? true : this._subnet_mask, + range_start: !this._range_start ? true : typeof this._range_start === 'string' && !this._range_start ? true : this._range_start, + range_end: !this._range_end ? true : typeof this._range_end === 'string' && !this._range_end ? true : this._range_end, + lease_duration: !this._lease_duration ? true : typeof this._lease_duration === 'number', + }; + const isError: string[] = []; + Object.keys(validate).forEach((key) => { + if (!(validate as any)[key]) { + isError.push(key); + } + }); + return isError; + } + + update(props: Partial): DhcpConfigV4 { + return new DhcpConfigV4({ ...this.serialize(), ...props }); + } +} diff --git a/client2/src/lib/entities/DhcpConfigV6.ts b/client2/src/lib/entities/DhcpConfigV6.ts new file mode 100644 index 00000000..cf7d6763 --- /dev/null +++ b/client2/src/lib/entities/DhcpConfigV6.ts @@ -0,0 +1,59 @@ +// This file was autogenerated. Please do not change. +// All changes will be overwrited on commit. +export interface IDhcpConfigV6 { + lease_duration?: number; + range_start?: string; +} + +export default class DhcpConfigV6 { + readonly _lease_duration: number | undefined; + + get leaseDuration(): number | undefined { + return this._lease_duration; + } + + readonly _range_start: string | undefined; + + get rangeStart(): string | undefined { + return this._range_start; + } + + constructor(props: IDhcpConfigV6) { + if (typeof props.lease_duration === 'number') { + this._lease_duration = props.lease_duration; + } + if (typeof props.range_start === 'string') { + this._range_start = props.range_start.trim(); + } + } + + serialize(): IDhcpConfigV6 { + const data: IDhcpConfigV6 = { + }; + if (typeof this._lease_duration !== 'undefined') { + data.lease_duration = this._lease_duration; + } + if (typeof this._range_start !== 'undefined') { + data.range_start = this._range_start; + } + return data; + } + + validate(): string[] { + const validate = { + range_start: !this._range_start ? true : typeof this._range_start === 'string' && !this._range_start ? true : this._range_start, + lease_duration: !this._lease_duration ? true : typeof this._lease_duration === 'number', + }; + const isError: string[] = []; + Object.keys(validate).forEach((key) => { + if (!(validate as any)[key]) { + isError.push(key); + } + }); + return isError; + } + + update(props: Partial): DhcpConfigV6 { + return new DhcpConfigV6({ ...this.serialize(), ...props }); + } +} diff --git a/client2/src/lib/entities/DhcpLease.ts b/client2/src/lib/entities/DhcpLease.ts new file mode 100644 index 00000000..b285496b --- /dev/null +++ b/client2/src/lib/entities/DhcpLease.ts @@ -0,0 +1,103 @@ +// This file was autogenerated. Please do not change. +// All changes will be overwrited on commit. +export interface IDhcpLease { + expires: string; + hostname: string; + ip: string; + mac: string; +} + +export default class DhcpLease { + readonly _expires: string; + + /** + * Description: undefined + * Example: 2017-07-21T17:32:28Z + */ + get expires(): string { + return this._expires; + } + + static expiresValidate(expires: string): boolean { + return typeof expires === 'string' && !!expires.trim(); + } + + readonly _hostname: string; + + /** + * Description: undefined + * Example: dell + */ + get hostname(): string { + return this._hostname; + } + + static hostnameValidate(hostname: string): boolean { + return typeof hostname === 'string' && !!hostname.trim(); + } + + readonly _ip: string; + + /** + * Description: undefined + * Example: 192.168.1.22 + */ + get ip(): string { + return this._ip; + } + + static ipValidate(ip: string): boolean { + return typeof ip === 'string' && !!ip.trim(); + } + + readonly _mac: string; + + /** + * Description: undefined + * Example: 00:11:09:b3:b3:b8 + */ + get mac(): string { + return this._mac; + } + + static macValidate(mac: string): boolean { + return typeof mac === 'string' && !!mac.trim(); + } + + constructor(props: IDhcpLease) { + this._expires = props.expires.trim(); + this._hostname = props.hostname.trim(); + this._ip = props.ip.trim(); + this._mac = props.mac.trim(); + } + + serialize(): IDhcpLease { + const data: IDhcpLease = { + expires: this._expires, + hostname: this._hostname, + ip: this._ip, + mac: this._mac, + }; + return data; + } + + validate(): string[] { + const validate = { + mac: typeof this._mac === 'string' && !this._mac ? true : this._mac, + ip: typeof this._ip === 'string' && !this._ip ? true : this._ip, + hostname: typeof this._hostname === 'string' && !this._hostname ? true : this._hostname, + expires: typeof this._expires === 'string' && !this._expires ? true : this._expires, + }; + const isError: string[] = []; + Object.keys(validate).forEach((key) => { + if (!(validate as any)[key]) { + isError.push(key); + } + }); + return isError; + } + + update(props: Partial): DhcpLease { + return new DhcpLease({ ...this.serialize(), ...props }); + } +} diff --git a/client2/src/lib/entities/DhcpSearchResult.ts b/client2/src/lib/entities/DhcpSearchResult.ts new file mode 100644 index 00000000..c168a244 --- /dev/null +++ b/client2/src/lib/entities/DhcpSearchResult.ts @@ -0,0 +1,62 @@ +import DhcpSearchV4, { IDhcpSearchV4 } from './DhcpSearchV4'; +import DhcpSearchV6, { IDhcpSearchV6 } from './DhcpSearchV6'; + +// This file was autogenerated. Please do not change. +// All changes will be overwrited on commit. +export interface IDhcpSearchResult { + v4?: IDhcpSearchV4; + v6?: IDhcpSearchV6; +} + +export default class DhcpSearchResult { + readonly _v4: DhcpSearchV4 | undefined; + + get v4(): DhcpSearchV4 | undefined { + return this._v4; + } + + readonly _v6: DhcpSearchV6 | undefined; + + get v6(): DhcpSearchV6 | undefined { + return this._v6; + } + + constructor(props: IDhcpSearchResult) { + if (props.v4) { + this._v4 = new DhcpSearchV4(props.v4); + } + if (props.v6) { + this._v6 = new DhcpSearchV6(props.v6); + } + } + + serialize(): IDhcpSearchResult { + const data: IDhcpSearchResult = { + }; + if (typeof this._v4 !== 'undefined') { + data.v4 = this._v4.serialize(); + } + if (typeof this._v6 !== 'undefined') { + data.v6 = this._v6.serialize(); + } + return data; + } + + validate(): string[] { + const validate = { + v4: !this._v4 ? true : this._v4.validate().length === 0, + v6: !this._v6 ? true : this._v6.validate().length === 0, + }; + const isError: string[] = []; + Object.keys(validate).forEach((key) => { + if (!(validate as any)[key]) { + isError.push(key); + } + }); + return isError; + } + + update(props: Partial): DhcpSearchResult { + return new DhcpSearchResult({ ...this.serialize(), ...props }); + } +} diff --git a/client2/src/lib/entities/DhcpSearchResultOtherServer.ts b/client2/src/lib/entities/DhcpSearchResultOtherServer.ts new file mode 100644 index 00000000..be50bbdd --- /dev/null +++ b/client2/src/lib/entities/DhcpSearchResultOtherServer.ts @@ -0,0 +1,65 @@ +// This file was autogenerated. Please do not change. +// All changes will be overwrited on commit. +export interface IDhcpSearchResultOtherServer { + error?: string; + found?: string; +} + +export default class DhcpSearchResultOtherServer { + readonly _error: string | undefined; + + /** */ + get error(): string | undefined { + return this._error; + } + + readonly _found: string | undefined; + + /** + * Description: The result of searching the other DHCP server. + * + * Example: no + */ + get found(): string | undefined { + return this._found; + } + + constructor(props: IDhcpSearchResultOtherServer) { + if (typeof props.error === 'string') { + this._error = props.error.trim(); + } + if (typeof props.found === 'string') { + this._found = props.found.trim(); + } + } + + serialize(): IDhcpSearchResultOtherServer { + const data: IDhcpSearchResultOtherServer = { + }; + if (typeof this._error !== 'undefined') { + data.error = this._error; + } + if (typeof this._found !== 'undefined') { + data.found = this._found; + } + return data; + } + + validate(): string[] { + const validate = { + found: !this._found ? true : typeof this._found === 'string' && !this._found ? true : this._found, + error: !this._error ? true : typeof this._error === 'string' && !this._error ? true : this._error, + }; + const isError: string[] = []; + Object.keys(validate).forEach((key) => { + if (!(validate as any)[key]) { + isError.push(key); + } + }); + return isError; + } + + update(props: Partial): DhcpSearchResultOtherServer { + return new DhcpSearchResultOtherServer({ ...this.serialize(), ...props }); + } +} diff --git a/client2/src/lib/entities/DhcpSearchResultStaticIP.ts b/client2/src/lib/entities/DhcpSearchResultStaticIP.ts new file mode 100644 index 00000000..f3fc2155 --- /dev/null +++ b/client2/src/lib/entities/DhcpSearchResultStaticIP.ts @@ -0,0 +1,65 @@ +// This file was autogenerated. Please do not change. +// All changes will be overwrited on commit. +export interface IDhcpSearchResultStaticIP { + ip?: string; + static?: string; +} + +export default class DhcpSearchResultStaticIP { + readonly _ip: string | undefined; + + /** */ + get ip(): string | undefined { + return this._ip; + } + + readonly _static: string | undefined; + + /** + * Description: The result of determining static IP address. + * + * Example: yes + */ + get static(): string | undefined { + return this._static; + } + + constructor(props: IDhcpSearchResultStaticIP) { + if (typeof props.ip === 'string') { + this._ip = props.ip.trim(); + } + if (typeof props.static === 'string') { + this._static = props.static.trim(); + } + } + + serialize(): IDhcpSearchResultStaticIP { + const data: IDhcpSearchResultStaticIP = { + }; + if (typeof this._ip !== 'undefined') { + data.ip = this._ip; + } + if (typeof this._static !== 'undefined') { + data.static = this._static; + } + return data; + } + + validate(): string[] { + const validate = { + static: !this._static ? true : typeof this._static === 'string' && !this._static ? true : this._static, + ip: !this._ip ? true : typeof this._ip === 'string' && !this._ip ? true : this._ip, + }; + const isError: string[] = []; + Object.keys(validate).forEach((key) => { + if (!(validate as any)[key]) { + isError.push(key); + } + }); + return isError; + } + + update(props: Partial): DhcpSearchResultStaticIP { + return new DhcpSearchResultStaticIP({ ...this.serialize(), ...props }); + } +} diff --git a/client2/src/lib/entities/DhcpSearchV4.ts b/client2/src/lib/entities/DhcpSearchV4.ts new file mode 100644 index 00000000..01af4419 --- /dev/null +++ b/client2/src/lib/entities/DhcpSearchV4.ts @@ -0,0 +1,62 @@ +import DhcpSearchResultOtherServer, { IDhcpSearchResultOtherServer } from './DhcpSearchResultOtherServer'; +import DhcpSearchResultStaticIP, { IDhcpSearchResultStaticIP } from './DhcpSearchResultStaticIP'; + +// This file was autogenerated. Please do not change. +// All changes will be overwrited on commit. +export interface IDhcpSearchV4 { + other_server?: IDhcpSearchResultOtherServer; + static_ip?: IDhcpSearchResultStaticIP; +} + +export default class DhcpSearchV4 { + readonly _other_server: DhcpSearchResultOtherServer | undefined; + + get otherServer(): DhcpSearchResultOtherServer | undefined { + return this._other_server; + } + + readonly _static_ip: DhcpSearchResultStaticIP | undefined; + + get staticIp(): DhcpSearchResultStaticIP | undefined { + return this._static_ip; + } + + constructor(props: IDhcpSearchV4) { + if (props.other_server) { + this._other_server = new DhcpSearchResultOtherServer(props.other_server); + } + if (props.static_ip) { + this._static_ip = new DhcpSearchResultStaticIP(props.static_ip); + } + } + + serialize(): IDhcpSearchV4 { + const data: IDhcpSearchV4 = { + }; + if (typeof this._other_server !== 'undefined') { + data.other_server = this._other_server.serialize(); + } + if (typeof this._static_ip !== 'undefined') { + data.static_ip = this._static_ip.serialize(); + } + return data; + } + + validate(): string[] { + const validate = { + other_server: !this._other_server ? true : this._other_server.validate().length === 0, + static_ip: !this._static_ip ? true : this._static_ip.validate().length === 0, + }; + const isError: string[] = []; + Object.keys(validate).forEach((key) => { + if (!(validate as any)[key]) { + isError.push(key); + } + }); + return isError; + } + + update(props: Partial): DhcpSearchV4 { + return new DhcpSearchV4({ ...this.serialize(), ...props }); + } +} diff --git a/client2/src/lib/entities/DhcpSearchV6.ts b/client2/src/lib/entities/DhcpSearchV6.ts new file mode 100644 index 00000000..e02134a5 --- /dev/null +++ b/client2/src/lib/entities/DhcpSearchV6.ts @@ -0,0 +1,47 @@ +import DhcpSearchResultOtherServer, { IDhcpSearchResultOtherServer } from './DhcpSearchResultOtherServer'; + +// This file was autogenerated. Please do not change. +// All changes will be overwrited on commit. +export interface IDhcpSearchV6 { + other_server?: IDhcpSearchResultOtherServer; +} + +export default class DhcpSearchV6 { + readonly _other_server: DhcpSearchResultOtherServer | undefined; + + get otherServer(): DhcpSearchResultOtherServer | undefined { + return this._other_server; + } + + constructor(props: IDhcpSearchV6) { + if (props.other_server) { + this._other_server = new DhcpSearchResultOtherServer(props.other_server); + } + } + + serialize(): IDhcpSearchV6 { + const data: IDhcpSearchV6 = { + }; + if (typeof this._other_server !== 'undefined') { + data.other_server = this._other_server.serialize(); + } + return data; + } + + validate(): string[] { + const validate = { + other_server: !this._other_server ? true : this._other_server.validate().length === 0, + }; + const isError: string[] = []; + Object.keys(validate).forEach((key) => { + if (!(validate as any)[key]) { + isError.push(key); + } + }); + return isError; + } + + update(props: Partial): DhcpSearchV6 { + return new DhcpSearchV6({ ...this.serialize(), ...props }); + } +} diff --git a/client2/src/lib/entities/DhcpStaticLease.ts b/client2/src/lib/entities/DhcpStaticLease.ts new file mode 100644 index 00000000..a365f3b6 --- /dev/null +++ b/client2/src/lib/entities/DhcpStaticLease.ts @@ -0,0 +1,85 @@ +// This file was autogenerated. Please do not change. +// All changes will be overwrited on commit. +export interface IDhcpStaticLease { + hostname: string; + ip: string; + mac: string; +} + +export default class DhcpStaticLease { + readonly _hostname: string; + + /** + * Description: undefined + * Example: dell + */ + get hostname(): string { + return this._hostname; + } + + static hostnameValidate(hostname: string): boolean { + return typeof hostname === 'string' && !!hostname.trim(); + } + + readonly _ip: string; + + /** + * Description: undefined + * Example: 192.168.1.22 + */ + get ip(): string { + return this._ip; + } + + static ipValidate(ip: string): boolean { + return typeof ip === 'string' && !!ip.trim(); + } + + readonly _mac: string; + + /** + * Description: undefined + * Example: 00:11:09:b3:b3:b8 + */ + get mac(): string { + return this._mac; + } + + static macValidate(mac: string): boolean { + return typeof mac === 'string' && !!mac.trim(); + } + + constructor(props: IDhcpStaticLease) { + this._hostname = props.hostname.trim(); + this._ip = props.ip.trim(); + this._mac = props.mac.trim(); + } + + serialize(): IDhcpStaticLease { + const data: IDhcpStaticLease = { + hostname: this._hostname, + ip: this._ip, + mac: this._mac, + }; + return data; + } + + validate(): string[] { + const validate = { + mac: typeof this._mac === 'string' && !this._mac ? true : this._mac, + ip: typeof this._ip === 'string' && !this._ip ? true : this._ip, + hostname: typeof this._hostname === 'string' && !this._hostname ? true : this._hostname, + }; + const isError: string[] = []; + Object.keys(validate).forEach((key) => { + if (!(validate as any)[key]) { + isError.push(key); + } + }); + return isError; + } + + update(props: Partial): DhcpStaticLease { + return new DhcpStaticLease({ ...this.serialize(), ...props }); + } +} diff --git a/client2/src/lib/entities/DhcpStatus.ts b/client2/src/lib/entities/DhcpStatus.ts new file mode 100644 index 00000000..604daf95 --- /dev/null +++ b/client2/src/lib/entities/DhcpStatus.ts @@ -0,0 +1,116 @@ +import DhcpConfigV4, { IDhcpConfigV4 } from './DhcpConfigV4'; +import DhcpConfigV6, { IDhcpConfigV6 } from './DhcpConfigV6'; +import DhcpLease, { IDhcpLease } from './DhcpLease'; +import DhcpStaticLease, { IDhcpStaticLease } from './DhcpStaticLease'; + +// This file was autogenerated. Please do not change. +// All changes will be overwrited on commit. +export interface IDhcpStatus { + enabled?: boolean; + interface_name?: string; + leases: IDhcpLease[]; + static_leases?: IDhcpStaticLease[]; + v4?: IDhcpConfigV4; + v6?: IDhcpConfigV6; +} + +export default class DhcpStatus { + readonly _enabled: boolean | undefined; + + get enabled(): boolean | undefined { + return this._enabled; + } + + readonly _interface_name: string | undefined; + + get interfaceName(): string | undefined { + return this._interface_name; + } + + readonly _leases: DhcpLease[]; + + get leases(): DhcpLease[] { + return this._leases; + } + + readonly _static_leases: DhcpStaticLease[] | undefined; + + get staticLeases(): DhcpStaticLease[] | undefined { + return this._static_leases; + } + + readonly _v4: DhcpConfigV4 | undefined; + + get v4(): DhcpConfigV4 | undefined { + return this._v4; + } + + readonly _v6: DhcpConfigV6 | undefined; + + get v6(): DhcpConfigV6 | undefined { + return this._v6; + } + + constructor(props: IDhcpStatus) { + if (typeof props.enabled === 'boolean') { + this._enabled = props.enabled; + } + if (typeof props.interface_name === 'string') { + this._interface_name = props.interface_name.trim(); + } + this._leases = props.leases.map((p) => new DhcpLease(p)); + if (props.static_leases) { + this._static_leases = props.static_leases.map((p) => new DhcpStaticLease(p)); + } + if (props.v4) { + this._v4 = new DhcpConfigV4(props.v4); + } + if (props.v6) { + this._v6 = new DhcpConfigV6(props.v6); + } + } + + serialize(): IDhcpStatus { + const data: IDhcpStatus = { + leases: this._leases.map((p) => p.serialize()), + }; + if (typeof this._enabled !== 'undefined') { + data.enabled = this._enabled; + } + if (typeof this._interface_name !== 'undefined') { + data.interface_name = this._interface_name; + } + if (typeof this._static_leases !== 'undefined') { + data.static_leases = this._static_leases.map((p) => p.serialize()); + } + if (typeof this._v4 !== 'undefined') { + data.v4 = this._v4.serialize(); + } + if (typeof this._v6 !== 'undefined') { + data.v6 = this._v6.serialize(); + } + return data; + } + + validate(): string[] { + const validate = { + enabled: !this._enabled ? true : typeof this._enabled === 'boolean', + interface_name: !this._interface_name ? true : typeof this._interface_name === 'string' && !this._interface_name ? true : this._interface_name, + v4: !this._v4 ? true : this._v4.validate().length === 0, + v6: !this._v6 ? true : this._v6.validate().length === 0, + leases: this._leases.reduce((result, p) => result && p.validate().length === 0, true), + static_leases: !this._static_leases ? true : this._static_leases.reduce((result, p) => result && p.validate().length === 0, true), + }; + const isError: string[] = []; + Object.keys(validate).forEach((key) => { + if (!(validate as any)[key]) { + isError.push(key); + } + }); + return isError; + } + + update(props: Partial): DhcpStatus { + return new DhcpStatus({ ...this.serialize(), ...props }); + } +} diff --git a/client2/src/lib/entities/DnsAnswer.ts b/client2/src/lib/entities/DnsAnswer.ts new file mode 100644 index 00000000..d3351be3 --- /dev/null +++ b/client2/src/lib/entities/DnsAnswer.ts @@ -0,0 +1,85 @@ +// This file was autogenerated. Please do not change. +// All changes will be overwrited on commit. +export interface IDnsAnswer { + ttl?: number; + type?: string; + value?: string; +} + +export default class DnsAnswer { + readonly _ttl: number | undefined; + + /** + * Description: undefined + * Example: 55 + */ + get ttl(): number | undefined { + return this._ttl; + } + + readonly _type: string | undefined; + + /** + * Description: undefined + * Example: A + */ + get type(): string | undefined { + return this._type; + } + + readonly _value: string | undefined; + + /** + * Description: undefined + * Example: 217.69.139.201 + */ + get value(): string | undefined { + return this._value; + } + + constructor(props: IDnsAnswer) { + if (typeof props.ttl === 'number') { + this._ttl = props.ttl; + } + if (typeof props.type === 'string') { + this._type = props.type.trim(); + } + if (typeof props.value === 'string') { + this._value = props.value.trim(); + } + } + + serialize(): IDnsAnswer { + const data: IDnsAnswer = { + }; + if (typeof this._ttl !== 'undefined') { + data.ttl = this._ttl; + } + if (typeof this._type !== 'undefined') { + data.type = this._type; + } + if (typeof this._value !== 'undefined') { + data.value = this._value; + } + return data; + } + + validate(): string[] { + const validate = { + ttl: !this._ttl ? true : typeof this._ttl === 'number', + type: !this._type ? true : typeof this._type === 'string' && !this._type ? true : this._type, + value: !this._value ? true : typeof this._value === 'string' && !this._value ? true : this._value, + }; + const isError: string[] = []; + Object.keys(validate).forEach((key) => { + if (!(validate as any)[key]) { + isError.push(key); + } + }); + return isError; + } + + update(props: Partial): DnsAnswer { + return new DnsAnswer({ ...this.serialize(), ...props }); + } +} diff --git a/client2/src/lib/entities/DnsQuestion.ts b/client2/src/lib/entities/DnsQuestion.ts new file mode 100644 index 00000000..42849b17 --- /dev/null +++ b/client2/src/lib/entities/DnsQuestion.ts @@ -0,0 +1,85 @@ +// This file was autogenerated. Please do not change. +// All changes will be overwrited on commit. +export interface IDnsQuestion { + class?: string; + host?: string; + type?: string; +} + +export default class DnsQuestion { + readonly _class: string | undefined; + + /** + * Description: undefined + * Example: IN + */ + get class(): string | undefined { + return this._class; + } + + readonly _host: string | undefined; + + /** + * Description: undefined + * Example: example.org + */ + get host(): string | undefined { + return this._host; + } + + readonly _type: string | undefined; + + /** + * Description: undefined + * Example: A + */ + get type(): string | undefined { + return this._type; + } + + constructor(props: IDnsQuestion) { + if (typeof props.class === 'string') { + this._class = props.class.trim(); + } + if (typeof props.host === 'string') { + this._host = props.host.trim(); + } + if (typeof props.type === 'string') { + this._type = props.type.trim(); + } + } + + serialize(): IDnsQuestion { + const data: IDnsQuestion = { + }; + if (typeof this._class !== 'undefined') { + data.class = this._class; + } + if (typeof this._host !== 'undefined') { + data.host = this._host; + } + if (typeof this._type !== 'undefined') { + data.type = this._type; + } + return data; + } + + validate(): string[] { + const validate = { + class: !this._class ? true : typeof this._class === 'string' && !this._class ? true : this._class, + host: !this._host ? true : typeof this._host === 'string' && !this._host ? true : this._host, + type: !this._type ? true : typeof this._type === 'string' && !this._type ? true : this._type, + }; + const isError: string[] = []; + Object.keys(validate).forEach((key) => { + if (!(validate as any)[key]) { + isError.push(key); + } + }); + return isError; + } + + update(props: Partial): DnsQuestion { + return new DnsQuestion({ ...this.serialize(), ...props }); + } +} diff --git a/client2/src/lib/entities/Error.ts b/client2/src/lib/entities/Error.ts new file mode 100644 index 00000000..79d16b5c --- /dev/null +++ b/client2/src/lib/entities/Error.ts @@ -0,0 +1,46 @@ +// This file was autogenerated. Please do not change. +// All changes will be overwrited on commit. +export interface IError { + message?: string; +} + +export default class Error { + readonly _message: string | undefined; + + /** */ + get message(): string | undefined { + return this._message; + } + + constructor(props: IError) { + if (typeof props.message === 'string') { + this._message = props.message.trim(); + } + } + + serialize(): IError { + const data: IError = { + }; + if (typeof this._message !== 'undefined') { + data.message = this._message; + } + return data; + } + + validate(): string[] { + const validate = { + message: !this._message ? true : typeof this._message === 'string' && !this._message ? true : this._message, + }; + const isError: string[] = []; + Object.keys(validate).forEach((key) => { + if (!(validate as any)[key]) { + isError.push(key); + } + }); + return isError; + } + + update(props: Partial): Error { + return new Error({ ...this.serialize(), ...props }); + } +} diff --git a/client2/src/lib/entities/Filter.ts b/client2/src/lib/entities/Filter.ts new file mode 100644 index 00000000..c7f3ff87 --- /dev/null +++ b/client2/src/lib/entities/Filter.ts @@ -0,0 +1,136 @@ +// This file was autogenerated. Please do not change. +// All changes will be overwrited on commit. +export interface IFilter { + enabled: boolean; + id: number; + last_updated: string; + name: string; + rules_count: number; + url: string; +} + +export default class Filter { + readonly _enabled: boolean; + + get enabled(): boolean { + return this._enabled; + } + + static enabledValidate(enabled: boolean): boolean { + return typeof enabled === 'boolean'; + } + + readonly _id: number; + + /** + * Description: undefined + * Example: 1234 + */ + get id(): number { + return this._id; + } + + static idValidate(id: number): boolean { + return typeof id === 'number'; + } + + readonly _last_updated: string; + + /** + * Description: undefined + * Example: 2018-10-30T12:18:57+03:00 + */ + get lastUpdated(): string { + return this._last_updated; + } + + static lastUpdatedValidate(lastUpdated: string): boolean { + return typeof lastUpdated === 'string' && !!lastUpdated.trim(); + } + + readonly _name: string; + + /** + * Description: undefined + * Example: AdGuard Simplified Domain Names filter + */ + get name(): string { + return this._name; + } + + static nameValidate(name: string): boolean { + return typeof name === 'string' && !!name.trim(); + } + + readonly _rules_count: number; + + /** + * Description: undefined + * Example: 5912 + */ + get rulesCount(): number { + return this._rules_count; + } + + static rulesCountValidate(rulesCount: number): boolean { + return typeof rulesCount === 'number'; + } + + readonly _url: string; + + /** + * Description: undefined + * Example: https://adguardteam.github.io/AdGuardSDNSFilter/Filters/filter.txt + * + */ + get url(): string { + return this._url; + } + + static urlValidate(url: string): boolean { + return typeof url === 'string' && !!url.trim(); + } + + constructor(props: IFilter) { + this._enabled = props.enabled; + this._id = props.id; + this._last_updated = props.last_updated.trim(); + this._name = props.name.trim(); + this._rules_count = props.rules_count; + this._url = props.url.trim(); + } + + serialize(): IFilter { + const data: IFilter = { + enabled: this._enabled, + id: this._id, + last_updated: this._last_updated, + name: this._name, + rules_count: this._rules_count, + url: this._url, + }; + return data; + } + + validate(): string[] { + const validate = { + enabled: typeof this._enabled === 'boolean', + id: typeof this._id === 'number', + last_updated: typeof this._last_updated === 'string' && !this._last_updated ? true : this._last_updated, + name: typeof this._name === 'string' && !this._name ? true : this._name, + rules_count: typeof this._rules_count === 'number', + url: typeof this._url === 'string' && !this._url ? true : this._url, + }; + const isError: string[] = []; + Object.keys(validate).forEach((key) => { + if (!(validate as any)[key]) { + isError.push(key); + } + }); + return isError; + } + + update(props: Partial): Filter { + return new Filter({ ...this.serialize(), ...props }); + } +} diff --git a/client2/src/lib/entities/FilterCheckHostResponse.ts b/client2/src/lib/entities/FilterCheckHostResponse.ts new file mode 100644 index 00000000..2f0daa9a --- /dev/null +++ b/client2/src/lib/entities/FilterCheckHostResponse.ts @@ -0,0 +1,143 @@ +import ResultRule, { IResultRule } from './ResultRule'; + +// This file was autogenerated. Please do not change. +// All changes will be overwrited on commit. +export interface IFilterCheckHostResponse { + cname?: string; + filter_id?: number; + ip_addrs?: string[]; + reason?: string; + rule?: string; + rules?: IResultRule[]; + service_name?: string; +} + +export default class FilterCheckHostResponse { + readonly _cname: string | undefined; + + /** */ + get cname(): string | undefined { + return this._cname; + } + + readonly _filter_id: number | undefined; + + /** */ + get filterId(): number | undefined { + return this._filter_id; + } + + readonly _ip_addrs: string[] | undefined; + + /** */ + get ipAddrs(): string[] | undefined { + return this._ip_addrs; + } + + readonly _reason: string | undefined; + + /** */ + get reason(): string | undefined { + return this._reason; + } + + readonly _rule: string | undefined; + + /** + * Description: Filtering rule applied to the request (if any). + * Deprecated: use `rules[*].text` instead. + * + * Example: ||example.org^ + */ + get rule(): string | undefined { + return this._rule; + } + + readonly _rules: ResultRule[] | undefined; + + /** */ + get rules(): ResultRule[] | undefined { + return this._rules; + } + + readonly _service_name: string | undefined; + + /** */ + get serviceName(): string | undefined { + return this._service_name; + } + + constructor(props: IFilterCheckHostResponse) { + if (typeof props.cname === 'string') { + this._cname = props.cname.trim(); + } + if (typeof props.filter_id === 'number') { + this._filter_id = props.filter_id; + } + if (props.ip_addrs) { + this._ip_addrs = props.ip_addrs; + } + if (typeof props.reason === 'string') { + this._reason = props.reason.trim(); + } + if (typeof props.rule === 'string') { + this._rule = props.rule.trim(); + } + if (props.rules) { + this._rules = props.rules.map((p) => new ResultRule(p)); + } + if (typeof props.service_name === 'string') { + this._service_name = props.service_name.trim(); + } + } + + serialize(): IFilterCheckHostResponse { + const data: IFilterCheckHostResponse = { + }; + if (typeof this._cname !== 'undefined') { + data.cname = this._cname; + } + if (typeof this._filter_id !== 'undefined') { + data.filter_id = this._filter_id; + } + if (typeof this._ip_addrs !== 'undefined') { + data.ip_addrs = this._ip_addrs; + } + if (typeof this._reason !== 'undefined') { + data.reason = this._reason; + } + if (typeof this._rule !== 'undefined') { + data.rule = this._rule; + } + if (typeof this._rules !== 'undefined') { + data.rules = this._rules.map((p) => p.serialize()); + } + if (typeof this._service_name !== 'undefined') { + data.service_name = this._service_name; + } + return data; + } + + validate(): string[] { + const validate = { + reason: !this._reason ? true : typeof this._reason === 'string' && !this._reason ? true : this._reason, + filter_id: !this._filter_id ? true : typeof this._filter_id === 'number', + rule: !this._rule ? true : typeof this._rule === 'string' && !this._rule ? true : this._rule, + rules: !this._rules ? true : this._rules.reduce((result, p) => result && p.validate().length === 0, true), + service_name: !this._service_name ? true : typeof this._service_name === 'string' && !this._service_name ? true : this._service_name, + cname: !this._cname ? true : typeof this._cname === 'string' && !this._cname ? true : this._cname, + ip_addrs: !this._ip_addrs ? true : this._ip_addrs.reduce((result, p) => result && typeof p === 'string', true), + }; + const isError: string[] = []; + Object.keys(validate).forEach((key) => { + if (!(validate as any)[key]) { + isError.push(key); + } + }); + return isError; + } + + update(props: Partial): FilterCheckHostResponse { + return new FilterCheckHostResponse({ ...this.serialize(), ...props }); + } +} diff --git a/client2/src/lib/entities/FilterConfig.ts b/client2/src/lib/entities/FilterConfig.ts new file mode 100644 index 00000000..b1691de1 --- /dev/null +++ b/client2/src/lib/entities/FilterConfig.ts @@ -0,0 +1,59 @@ +// This file was autogenerated. Please do not change. +// All changes will be overwrited on commit. +export interface IFilterConfig { + enabled?: boolean; + interval?: number; +} + +export default class FilterConfig { + readonly _enabled: boolean | undefined; + + get enabled(): boolean | undefined { + return this._enabled; + } + + readonly _interval: number | undefined; + + get interval(): number | undefined { + return this._interval; + } + + constructor(props: IFilterConfig) { + if (typeof props.enabled === 'boolean') { + this._enabled = props.enabled; + } + if (typeof props.interval === 'number') { + this._interval = props.interval; + } + } + + serialize(): IFilterConfig { + const data: IFilterConfig = { + }; + if (typeof this._enabled !== 'undefined') { + data.enabled = this._enabled; + } + if (typeof this._interval !== 'undefined') { + data.interval = this._interval; + } + return data; + } + + validate(): string[] { + const validate = { + enabled: !this._enabled ? true : typeof this._enabled === 'boolean', + interval: !this._interval ? true : typeof this._interval === 'number', + }; + const isError: string[] = []; + Object.keys(validate).forEach((key) => { + if (!(validate as any)[key]) { + isError.push(key); + } + }); + return isError; + } + + update(props: Partial): FilterConfig { + return new FilterConfig({ ...this.serialize(), ...props }); + } +} diff --git a/client2/src/lib/entities/FilterRefreshRequest.ts b/client2/src/lib/entities/FilterRefreshRequest.ts new file mode 100644 index 00000000..3b274ddb --- /dev/null +++ b/client2/src/lib/entities/FilterRefreshRequest.ts @@ -0,0 +1,45 @@ +// This file was autogenerated. Please do not change. +// All changes will be overwrited on commit. +export interface IFilterRefreshRequest { + whitelist?: boolean; +} + +export default class FilterRefreshRequest { + readonly _whitelist: boolean | undefined; + + get whitelist(): boolean | undefined { + return this._whitelist; + } + + constructor(props: IFilterRefreshRequest) { + if (typeof props.whitelist === 'boolean') { + this._whitelist = props.whitelist; + } + } + + serialize(): IFilterRefreshRequest { + const data: IFilterRefreshRequest = { + }; + if (typeof this._whitelist !== 'undefined') { + data.whitelist = this._whitelist; + } + return data; + } + + validate(): string[] { + const validate = { + whitelist: !this._whitelist ? true : typeof this._whitelist === 'boolean', + }; + const isError: string[] = []; + Object.keys(validate).forEach((key) => { + if (!(validate as any)[key]) { + isError.push(key); + } + }); + return isError; + } + + update(props: Partial): FilterRefreshRequest { + return new FilterRefreshRequest({ ...this.serialize(), ...props }); + } +} diff --git a/client2/src/lib/entities/FilterRefreshResponse.ts b/client2/src/lib/entities/FilterRefreshResponse.ts new file mode 100644 index 00000000..96d45b09 --- /dev/null +++ b/client2/src/lib/entities/FilterRefreshResponse.ts @@ -0,0 +1,45 @@ +// This file was autogenerated. Please do not change. +// All changes will be overwrited on commit. +export interface IFilterRefreshResponse { + updated?: number; +} + +export default class FilterRefreshResponse { + readonly _updated: number | undefined; + + get updated(): number | undefined { + return this._updated; + } + + constructor(props: IFilterRefreshResponse) { + if (typeof props.updated === 'number') { + this._updated = props.updated; + } + } + + serialize(): IFilterRefreshResponse { + const data: IFilterRefreshResponse = { + }; + if (typeof this._updated !== 'undefined') { + data.updated = this._updated; + } + return data; + } + + validate(): string[] { + const validate = { + updated: !this._updated ? true : typeof this._updated === 'number', + }; + const isError: string[] = []; + Object.keys(validate).forEach((key) => { + if (!(validate as any)[key]) { + isError.push(key); + } + }); + return isError; + } + + update(props: Partial): FilterRefreshResponse { + return new FilterRefreshResponse({ ...this.serialize(), ...props }); + } +} diff --git a/client2/src/lib/entities/FilterSetUrl.ts b/client2/src/lib/entities/FilterSetUrl.ts new file mode 100644 index 00000000..92eccfc7 --- /dev/null +++ b/client2/src/lib/entities/FilterSetUrl.ts @@ -0,0 +1,72 @@ +// This file was autogenerated. Please do not change. +// All changes will be overwrited on commit. +export interface IFilterSetUrl { + data?: any; + url?: string; + whitelist?: boolean; +} + +export default class FilterSetUrl { + readonly _data: any | undefined; + + get data(): any | undefined { + return this._data; + } + + readonly _url: string | undefined; + + get url(): string | undefined { + return this._url; + } + + readonly _whitelist: boolean | undefined; + + get whitelist(): boolean | undefined { + return this._whitelist; + } + + constructor(props: IFilterSetUrl) { + if (props.data) { + this._data = props.data; + } + if (typeof props.url === 'string') { + this._url = props.url.trim(); + } + if (typeof props.whitelist === 'boolean') { + this._whitelist = props.whitelist; + } + } + + serialize(): IFilterSetUrl { + const data: IFilterSetUrl = { + }; + if (typeof this._data !== 'undefined') { + data.data = this._data; + } + if (typeof this._url !== 'undefined') { + data.url = this._url; + } + if (typeof this._whitelist !== 'undefined') { + data.whitelist = this._whitelist; + } + return data; + } + + validate(): string[] { + const validate = { + url: !this._url ? true : typeof this._url === 'string' && !this._url ? true : this._url, + whitelist: !this._whitelist ? true : typeof this._whitelist === 'boolean', + }; + const isError: string[] = []; + Object.keys(validate).forEach((key) => { + if (!(validate as any)[key]) { + isError.push(key); + } + }); + return isError; + } + + update(props: Partial): FilterSetUrl { + return new FilterSetUrl({ ...this.serialize(), ...props }); + } +} diff --git a/client2/src/lib/entities/FilterStatus.ts b/client2/src/lib/entities/FilterStatus.ts new file mode 100644 index 00000000..be63d512 --- /dev/null +++ b/client2/src/lib/entities/FilterStatus.ts @@ -0,0 +1,89 @@ +import Filter, { IFilter } from './Filter'; + +// This file was autogenerated. Please do not change. +// All changes will be overwrited on commit. +export interface IFilterStatus { + enabled?: boolean; + filters?: IFilter[]; + interval?: number; + user_rules?: string[]; +} + +export default class FilterStatus { + readonly _enabled: boolean | undefined; + + get enabled(): boolean | undefined { + return this._enabled; + } + + readonly _filters: Filter[] | undefined; + + get filters(): Filter[] | undefined { + return this._filters; + } + + readonly _interval: number | undefined; + + get interval(): number | undefined { + return this._interval; + } + + readonly _user_rules: string[] | undefined; + + get userRules(): string[] | undefined { + return this._user_rules; + } + + constructor(props: IFilterStatus) { + if (typeof props.enabled === 'boolean') { + this._enabled = props.enabled; + } + if (props.filters) { + this._filters = props.filters.map((p) => new Filter(p)); + } + if (typeof props.interval === 'number') { + this._interval = props.interval; + } + if (props.user_rules) { + this._user_rules = props.user_rules; + } + } + + serialize(): IFilterStatus { + const data: IFilterStatus = { + }; + if (typeof this._enabled !== 'undefined') { + data.enabled = this._enabled; + } + if (typeof this._filters !== 'undefined') { + data.filters = this._filters.map((p) => p.serialize()); + } + if (typeof this._interval !== 'undefined') { + data.interval = this._interval; + } + if (typeof this._user_rules !== 'undefined') { + data.user_rules = this._user_rules; + } + return data; + } + + validate(): string[] { + const validate = { + enabled: !this._enabled ? true : typeof this._enabled === 'boolean', + interval: !this._interval ? true : typeof this._interval === 'number', + filters: !this._filters ? true : this._filters.reduce((result, p) => result && p.validate().length === 0, true), + user_rules: !this._user_rules ? true : this._user_rules.reduce((result, p) => result && typeof p === 'string', true), + }; + const isError: string[] = []; + Object.keys(validate).forEach((key) => { + if (!(validate as any)[key]) { + isError.push(key); + } + }); + return isError; + } + + update(props: Partial): FilterStatus { + return new FilterStatus({ ...this.serialize(), ...props }); + } +} diff --git a/client2/src/lib/entities/GetVersionRequest.ts b/client2/src/lib/entities/GetVersionRequest.ts new file mode 100644 index 00000000..8677bb11 --- /dev/null +++ b/client2/src/lib/entities/GetVersionRequest.ts @@ -0,0 +1,46 @@ +// This file was autogenerated. Please do not change. +// All changes will be overwrited on commit. +export interface IGetVersionRequest { + recheck_now?: boolean; +} + +export default class GetVersionRequest { + readonly _recheck_now: boolean | undefined; + + /** */ + get recheckNow(): boolean | undefined { + return this._recheck_now; + } + + constructor(props: IGetVersionRequest) { + if (typeof props.recheck_now === 'boolean') { + this._recheck_now = props.recheck_now; + } + } + + serialize(): IGetVersionRequest { + const data: IGetVersionRequest = { + }; + if (typeof this._recheck_now !== 'undefined') { + data.recheck_now = this._recheck_now; + } + return data; + } + + validate(): string[] { + const validate = { + recheck_now: !this._recheck_now ? true : typeof this._recheck_now === 'boolean', + }; + const isError: string[] = []; + Object.keys(validate).forEach((key) => { + if (!(validate as any)[key]) { + isError.push(key); + } + }); + return isError; + } + + update(props: Partial): GetVersionRequest { + return new GetVersionRequest({ ...this.serialize(), ...props }); + } +} diff --git a/client2/src/lib/entities/InitialConfiguration.ts b/client2/src/lib/entities/InitialConfiguration.ts new file mode 100644 index 00000000..1bb32c3c --- /dev/null +++ b/client2/src/lib/entities/InitialConfiguration.ts @@ -0,0 +1,89 @@ +import AddressInfo, { IAddressInfo } from './AddressInfo'; + +// This file was autogenerated. Please do not change. +// All changes will be overwrited on commit. +export interface IInitialConfiguration { + dns: IAddressInfo; + password: string; + username: string; + web: IAddressInfo; +} + +export default class InitialConfiguration { + readonly _dns: AddressInfo; + + get dns(): AddressInfo { + return this._dns; + } + + readonly _password: string; + + /** + * Description: Basic auth password + * Example: password + */ + get password(): string { + return this._password; + } + + static passwordValidate(password: string): boolean { + return typeof password === 'string' && !!password.trim(); + } + + readonly _username: string; + + /** + * Description: Basic auth username + * Example: admin + */ + get username(): string { + return this._username; + } + + static usernameValidate(username: string): boolean { + return typeof username === 'string' && !!username.trim(); + } + + readonly _web: AddressInfo; + + get web(): AddressInfo { + return this._web; + } + + constructor(props: IInitialConfiguration) { + this._dns = new AddressInfo(props.dns); + this._password = props.password.trim(); + this._username = props.username.trim(); + this._web = new AddressInfo(props.web); + } + + serialize(): IInitialConfiguration { + const data: IInitialConfiguration = { + dns: this._dns.serialize(), + password: this._password, + username: this._username, + web: this._web.serialize(), + }; + return data; + } + + validate(): string[] { + const validate = { + dns: this._dns.validate().length === 0, + web: this._web.validate().length === 0, + username: typeof this._username === 'string' && !this._username ? true : this._username, + password: typeof this._password === 'string' && !this._password ? true : this._password, + }; + const isError: string[] = []; + Object.keys(validate).forEach((key) => { + if (!(validate as any)[key]) { + isError.push(key); + } + }); + return isError; + } + + update(props: Partial): InitialConfiguration { + return new InitialConfiguration({ ...this.serialize(), ...props }); + } +} diff --git a/client2/src/lib/entities/InitialConfigurationBeta.ts b/client2/src/lib/entities/InitialConfigurationBeta.ts new file mode 100644 index 00000000..bbf09595 --- /dev/null +++ b/client2/src/lib/entities/InitialConfigurationBeta.ts @@ -0,0 +1,89 @@ +import AddressInfoBeta, { IAddressInfoBeta } from './AddressInfoBeta'; + +// This file was autogenerated. Please do not change. +// All changes will be overwrited on commit. +export interface IInitialConfigurationBeta { + dns: IAddressInfoBeta; + password: string; + username: string; + web: IAddressInfoBeta; +} + +export default class InitialConfigurationBeta { + readonly _dns: AddressInfoBeta; + + get dns(): AddressInfoBeta { + return this._dns; + } + + readonly _password: string; + + /** + * Description: Basic auth password + * Example: password + */ + get password(): string { + return this._password; + } + + static passwordValidate(password: string): boolean { + return typeof password === 'string' && !!password.trim(); + } + + readonly _username: string; + + /** + * Description: Basic auth username + * Example: admin + */ + get username(): string { + return this._username; + } + + static usernameValidate(username: string): boolean { + return typeof username === 'string' && !!username.trim(); + } + + readonly _web: AddressInfoBeta; + + get web(): AddressInfoBeta { + return this._web; + } + + constructor(props: IInitialConfigurationBeta) { + this._dns = new AddressInfoBeta(props.dns); + this._password = props.password.trim(); + this._username = props.username.trim(); + this._web = new AddressInfoBeta(props.web); + } + + serialize(): IInitialConfigurationBeta { + const data: IInitialConfigurationBeta = { + dns: this._dns.serialize(), + password: this._password, + username: this._username, + web: this._web.serialize(), + }; + return data; + } + + validate(): string[] { + const validate = { + dns: this._dns.validate().length === 0, + web: this._web.validate().length === 0, + username: typeof this._username === 'string' && !this._username ? true : this._username, + password: typeof this._password === 'string' && !this._password ? true : this._password, + }; + const isError: string[] = []; + Object.keys(validate).forEach((key) => { + if (!(validate as any)[key]) { + isError.push(key); + } + }); + return isError; + } + + update(props: Partial): InitialConfigurationBeta { + return new InitialConfigurationBeta({ ...this.serialize(), ...props }); + } +} diff --git a/client2/src/lib/entities/Login.ts b/client2/src/lib/entities/Login.ts new file mode 100644 index 00000000..808c6f6e --- /dev/null +++ b/client2/src/lib/entities/Login.ts @@ -0,0 +1,61 @@ +// This file was autogenerated. Please do not change. +// All changes will be overwrited on commit. +export interface ILogin { + name?: string; + password?: string; +} + +export default class Login { + readonly _name: string | undefined; + + /** */ + get name(): string | undefined { + return this._name; + } + + readonly _password: string | undefined; + + /** */ + get password(): string | undefined { + return this._password; + } + + constructor(props: ILogin) { + if (typeof props.name === 'string') { + this._name = props.name.trim(); + } + if (typeof props.password === 'string') { + this._password = props.password.trim(); + } + } + + serialize(): ILogin { + const data: ILogin = { + }; + if (typeof this._name !== 'undefined') { + data.name = this._name; + } + if (typeof this._password !== 'undefined') { + data.password = this._password; + } + return data; + } + + validate(): string[] { + const validate = { + name: !this._name ? true : typeof this._name === 'string' && !this._name ? true : this._name, + password: !this._password ? true : typeof this._password === 'string' && !this._password ? true : this._password, + }; + const isError: string[] = []; + Object.keys(validate).forEach((key) => { + if (!(validate as any)[key]) { + isError.push(key); + } + }); + return isError; + } + + update(props: Partial): Login { + return new Login({ ...this.serialize(), ...props }); + } +} diff --git a/client2/src/lib/entities/NetInterface.ts b/client2/src/lib/entities/NetInterface.ts new file mode 100644 index 00000000..c11d7a2c --- /dev/null +++ b/client2/src/lib/entities/NetInterface.ts @@ -0,0 +1,114 @@ +// This file was autogenerated. Please do not change. +// All changes will be overwrited on commit. +export interface INetInterface { + flags: string; + hardware_address: string; + ip_addresses?: string[]; + mtu: number; + name: string; +} + +export default class NetInterface { + readonly _flags: string; + + /** + * Description: Flags could be any combination of the following values, divided by the "|" character: "up", "broadcast", "loopback", "pointtopoint" and "multicast". + * + * Example: up|broadcast|multicast + */ + get flags(): string { + return this._flags; + } + + static flagsValidate(flags: string): boolean { + return typeof flags === 'string' && !!flags.trim(); + } + + readonly _hardware_address: string; + + /** + * Description: undefined + * Example: 52:54:00:11:09:ba + */ + get hardwareAddress(): string { + return this._hardware_address; + } + + static hardwareAddressValidate(hardwareAddress: string): boolean { + return typeof hardwareAddress === 'string' && !!hardwareAddress.trim(); + } + + readonly _ip_addresses: string[] | undefined; + + get ipAddresses(): string[] | undefined { + return this._ip_addresses; + } + + readonly _mtu: number; + + get mtu(): number { + return this._mtu; + } + + static mtuValidate(mtu: number): boolean { + return typeof mtu === 'number'; + } + + readonly _name: string; + + /** + * Description: undefined + * Example: eth0 + */ + get name(): string { + return this._name; + } + + static nameValidate(name: string): boolean { + return typeof name === 'string' && !!name.trim(); + } + + constructor(props: INetInterface) { + this._flags = props.flags.trim(); + this._hardware_address = props.hardware_address.trim(); + if (props.ip_addresses) { + this._ip_addresses = props.ip_addresses; + } + this._mtu = props.mtu; + this._name = props.name.trim(); + } + + serialize(): INetInterface { + const data: INetInterface = { + flags: this._flags, + hardware_address: this._hardware_address, + mtu: this._mtu, + name: this._name, + }; + if (typeof this._ip_addresses !== 'undefined') { + data.ip_addresses = this._ip_addresses; + } + return data; + } + + validate(): string[] { + const validate = { + flags: typeof this._flags === 'string' && !this._flags ? true : this._flags, + hardware_address: typeof this._hardware_address === 'string' && !this._hardware_address ? true : this._hardware_address, + name: typeof this._name === 'string' && !this._name ? true : this._name, + ip_addresses: !this._ip_addresses ? true : this._ip_addresses.reduce((result, p) => result && typeof p === 'string', true), + mtu: typeof this._mtu === 'number', + }; + const isError: string[] = []; + Object.keys(validate).forEach((key) => { + if (!(validate as any)[key]) { + isError.push(key); + } + }); + return isError; + } + + update(props: Partial): NetInterface { + return new NetInterface({ ...this.serialize(), ...props }); + } +} diff --git a/client2/src/lib/entities/NetInterfaces.ts b/client2/src/lib/entities/NetInterfaces.ts new file mode 100644 index 00000000..29a33304 --- /dev/null +++ b/client2/src/lib/entities/NetInterfaces.ts @@ -0,0 +1,33 @@ +import NetInterface, { INetInterface } from './NetInterface'; + +// This file was autogenerated. Please do not change. +// All changes will be overwrited on commit. +export interface INetInterfaces { + [key: string]: INetInterface; +} + +export default class NetInterfaces { + readonly data: Record; + + constructor(props: INetInterfaces) { + this.data = Object.entries(props).reduce>((prev, [key, value]) => { + prev[key] = new NetInterface(value!); + return prev; + }, {}) + } + + serialize(): INetInterfaces { + return Object.entries(this.data).reduce>((prev, [key, value]) => { + prev[key] = value.serialize(); + return prev; + }, {}) + } + + validate(): string[] { + return [] + } + + update(props: INetInterfaces): NetInterfaces { + return new NetInterfaces({ ...this.serialize(), ...props }); + } +} diff --git a/client2/src/lib/entities/ProfileInfo.ts b/client2/src/lib/entities/ProfileInfo.ts new file mode 100644 index 00000000..0a8f099c --- /dev/null +++ b/client2/src/lib/entities/ProfileInfo.ts @@ -0,0 +1,45 @@ +// This file was autogenerated. Please do not change. +// All changes will be overwrited on commit. +export interface IProfileInfo { + name?: string; +} + +export default class ProfileInfo { + readonly _name: string | undefined; + + get name(): string | undefined { + return this._name; + } + + constructor(props: IProfileInfo) { + if (typeof props.name === 'string') { + this._name = props.name.trim(); + } + } + + serialize(): IProfileInfo { + const data: IProfileInfo = { + }; + if (typeof this._name !== 'undefined') { + data.name = this._name; + } + return data; + } + + validate(): string[] { + const validate = { + name: !this._name ? true : typeof this._name === 'string' && !this._name ? true : this._name, + }; + const isError: string[] = []; + Object.keys(validate).forEach((key) => { + if (!(validate as any)[key]) { + isError.push(key); + } + }); + return isError; + } + + update(props: Partial): ProfileInfo { + return new ProfileInfo({ ...this.serialize(), ...props }); + } +} diff --git a/client2/src/lib/entities/QueryLog.ts b/client2/src/lib/entities/QueryLog.ts new file mode 100644 index 00000000..da2b7761 --- /dev/null +++ b/client2/src/lib/entities/QueryLog.ts @@ -0,0 +1,65 @@ +import QueryLogItem, { IQueryLogItem } from './QueryLogItem'; + +// This file was autogenerated. Please do not change. +// All changes will be overwrited on commit. +export interface IQueryLog { + data?: IQueryLogItem[]; + oldest?: string; +} + +export default class QueryLog { + readonly _data: QueryLogItem[] | undefined; + + get data(): QueryLogItem[] | undefined { + return this._data; + } + + readonly _oldest: string | undefined; + + /** + * Description: undefined + * Example: 2018-11-26T00:02:41+03:00 + */ + get oldest(): string | undefined { + return this._oldest; + } + + constructor(props: IQueryLog) { + if (props.data) { + this._data = props.data.map((p) => new QueryLogItem(p)); + } + if (typeof props.oldest === 'string') { + this._oldest = props.oldest.trim(); + } + } + + serialize(): IQueryLog { + const data: IQueryLog = { + }; + if (typeof this._data !== 'undefined') { + data.data = this._data.map((p) => p.serialize()); + } + if (typeof this._oldest !== 'undefined') { + data.oldest = this._oldest; + } + return data; + } + + validate(): string[] { + const validate = { + oldest: !this._oldest ? true : typeof this._oldest === 'string' && !this._oldest ? true : this._oldest, + data: !this._data ? true : this._data.reduce((result, p) => result && p.validate().length === 0, true), + }; + const isError: string[] = []; + Object.keys(validate).forEach((key) => { + if (!(validate as any)[key]) { + isError.push(key); + } + }); + return isError; + } + + update(props: Partial): QueryLog { + return new QueryLog({ ...this.serialize(), ...props }); + } +} diff --git a/client2/src/lib/entities/QueryLogConfig.ts b/client2/src/lib/entities/QueryLogConfig.ts new file mode 100644 index 00000000..e767d72c --- /dev/null +++ b/client2/src/lib/entities/QueryLogConfig.ts @@ -0,0 +1,76 @@ +// This file was autogenerated. Please do not change. +// All changes will be overwrited on commit. +export interface IQueryLogConfig { + anonymize_client_ip?: boolean; + enabled?: boolean; + interval?: number; +} + +export default class QueryLogConfig { + readonly _anonymize_client_ip: boolean | undefined; + + /** */ + get anonymizeClientIp(): boolean | undefined { + return this._anonymize_client_ip; + } + + readonly _enabled: boolean | undefined; + + /** */ + get enabled(): boolean | undefined { + return this._enabled; + } + + readonly _interval: number | undefined; + + /** */ + get interval(): number | undefined { + return this._interval; + } + + constructor(props: IQueryLogConfig) { + if (typeof props.anonymize_client_ip === 'boolean') { + this._anonymize_client_ip = props.anonymize_client_ip; + } + if (typeof props.enabled === 'boolean') { + this._enabled = props.enabled; + } + if (typeof props.interval === 'number') { + this._interval = props.interval; + } + } + + serialize(): IQueryLogConfig { + const data: IQueryLogConfig = { + }; + if (typeof this._anonymize_client_ip !== 'undefined') { + data.anonymize_client_ip = this._anonymize_client_ip; + } + if (typeof this._enabled !== 'undefined') { + data.enabled = this._enabled; + } + if (typeof this._interval !== 'undefined') { + data.interval = this._interval; + } + return data; + } + + validate(): string[] { + const validate = { + enabled: !this._enabled ? true : typeof this._enabled === 'boolean', + interval: !this._interval ? true : typeof this._interval === 'number', + anonymize_client_ip: !this._anonymize_client_ip ? true : typeof this._anonymize_client_ip === 'boolean', + }; + const isError: string[] = []; + Object.keys(validate).forEach((key) => { + if (!(validate as any)[key]) { + isError.push(key); + } + }); + return isError; + } + + update(props: Partial): QueryLogConfig { + return new QueryLogConfig({ ...this.serialize(), ...props }); + } +} diff --git a/client2/src/lib/entities/QueryLogItem.ts b/client2/src/lib/entities/QueryLogItem.ts new file mode 100644 index 00000000..6dffead3 --- /dev/null +++ b/client2/src/lib/entities/QueryLogItem.ts @@ -0,0 +1,297 @@ +import DnsAnswer, { IDnsAnswer } from './DnsAnswer'; +import DnsQuestion, { IDnsQuestion } from './DnsQuestion'; +import ResultRule, { IResultRule } from './ResultRule'; + +// This file was autogenerated. Please do not change. +// All changes will be overwrited on commit. +export interface IQueryLogItem { + answer?: IDnsAnswer[]; + answer_dnssec?: boolean; + client?: string; + client_id?: string; + client_proto?: any; + elapsedMs?: string; + filterId?: number; + original_answer?: IDnsAnswer[]; + question?: IDnsQuestion; + reason?: string; + rule?: string; + rules?: IResultRule[]; + service_name?: string; + status?: string; + time?: string; + upstream?: string; +} + +export default class QueryLogItem { + readonly _answer: DnsAnswer[] | undefined; + + get answer(): DnsAnswer[] | undefined { + return this._answer; + } + + readonly _answer_dnssec: boolean | undefined; + + get answerDnssec(): boolean | undefined { + return this._answer_dnssec; + } + + readonly _client: string | undefined; + + /** + * Description: The client's IP address. + * + * Example: 192.168.0.1 + */ + get client(): string | undefined { + return this._client; + } + + readonly _client_id: string | undefined; + + /** + * Description: The client ID, if provided in DOH, DOQ, or DOT. + * + * Example: cli123 + */ + get clientId(): string | undefined { + return this._client_id; + } + + readonly _client_proto: any | undefined; + + get clientProto(): any | undefined { + return this._client_proto; + } + + readonly _elapsedMs: string | undefined; + + /** + * Description: undefined + * Example: 54.023928 + */ + get elapsedMs(): string | undefined { + return this._elapsedMs; + } + + readonly _filterId: number | undefined; + + /** + * Description: In case if there's a rule applied to this DNS request, this is ID of the filter list that the rule belongs to. + * Deprecated: use `rules[*].filter_list_id` instead. + * + * Example: 123123 + */ + get filterId(): number | undefined { + return this._filterId; + } + + readonly _original_answer: DnsAnswer[] | undefined; + + /** */ + get originalAnswer(): DnsAnswer[] | undefined { + return this._original_answer; + } + + readonly _question: DnsQuestion | undefined; + + get question(): DnsQuestion | undefined { + return this._question; + } + + readonly _reason: string | undefined; + + /** */ + get reason(): string | undefined { + return this._reason; + } + + readonly _rule: string | undefined; + + /** + * Description: Filtering rule applied to the request (if any). + * Deprecated: use `rules[*].text` instead. + * + * Example: ||example.org^ + */ + get rule(): string | undefined { + return this._rule; + } + + readonly _rules: ResultRule[] | undefined; + + /** */ + get rules(): ResultRule[] | undefined { + return this._rules; + } + + readonly _service_name: string | undefined; + + /** */ + get serviceName(): string | undefined { + return this._service_name; + } + + readonly _status: string | undefined; + + /** + * Description: DNS response status + * Example: NOERROR + */ + get status(): string | undefined { + return this._status; + } + + readonly _time: string | undefined; + + /** + * Description: DNS request processing start time + * Example: 2018-11-26T00:02:41+03:00 + */ + get time(): string | undefined { + return this._time; + } + + readonly _upstream: string | undefined; + + /** */ + get upstream(): string | undefined { + return this._upstream; + } + + constructor(props: IQueryLogItem) { + if (props.answer) { + this._answer = props.answer.map((p) => new DnsAnswer(p)); + } + if (typeof props.answer_dnssec === 'boolean') { + this._answer_dnssec = props.answer_dnssec; + } + if (typeof props.client === 'string') { + this._client = props.client.trim(); + } + if (typeof props.client_id === 'string') { + this._client_id = props.client_id.trim(); + } + if (props.client_proto) { + this._client_proto = props.client_proto; + } + if (typeof props.elapsedMs === 'string') { + this._elapsedMs = props.elapsedMs.trim(); + } + if (typeof props.filterId === 'number') { + this._filterId = props.filterId; + } + if (props.original_answer) { + this._original_answer = props.original_answer.map((p) => new DnsAnswer(p)); + } + if (props.question) { + this._question = new DnsQuestion(props.question); + } + if (typeof props.reason === 'string') { + this._reason = props.reason.trim(); + } + if (typeof props.rule === 'string') { + this._rule = props.rule.trim(); + } + if (props.rules) { + this._rules = props.rules.map((p) => new ResultRule(p)); + } + if (typeof props.service_name === 'string') { + this._service_name = props.service_name.trim(); + } + if (typeof props.status === 'string') { + this._status = props.status.trim(); + } + if (typeof props.time === 'string') { + this._time = props.time.trim(); + } + if (typeof props.upstream === 'string') { + this._upstream = props.upstream.trim(); + } + } + + serialize(): IQueryLogItem { + const data: IQueryLogItem = { + }; + if (typeof this._answer !== 'undefined') { + data.answer = this._answer.map((p) => p.serialize()); + } + if (typeof this._answer_dnssec !== 'undefined') { + data.answer_dnssec = this._answer_dnssec; + } + if (typeof this._client !== 'undefined') { + data.client = this._client; + } + if (typeof this._client_id !== 'undefined') { + data.client_id = this._client_id; + } + if (typeof this._client_proto !== 'undefined') { + data.client_proto = this._client_proto; + } + if (typeof this._elapsedMs !== 'undefined') { + data.elapsedMs = this._elapsedMs; + } + if (typeof this._filterId !== 'undefined') { + data.filterId = this._filterId; + } + if (typeof this._original_answer !== 'undefined') { + data.original_answer = this._original_answer.map((p) => p.serialize()); + } + if (typeof this._question !== 'undefined') { + data.question = this._question.serialize(); + } + if (typeof this._reason !== 'undefined') { + data.reason = this._reason; + } + if (typeof this._rule !== 'undefined') { + data.rule = this._rule; + } + if (typeof this._rules !== 'undefined') { + data.rules = this._rules.map((p) => p.serialize()); + } + if (typeof this._service_name !== 'undefined') { + data.service_name = this._service_name; + } + if (typeof this._status !== 'undefined') { + data.status = this._status; + } + if (typeof this._time !== 'undefined') { + data.time = this._time; + } + if (typeof this._upstream !== 'undefined') { + data.upstream = this._upstream; + } + return data; + } + + validate(): string[] { + const validate = { + answer: !this._answer ? true : this._answer.reduce((result, p) => result && p.validate().length === 0, true), + original_answer: !this._original_answer ? true : this._original_answer.reduce((result, p) => result && p.validate().length === 0, true), + upstream: !this._upstream ? true : typeof this._upstream === 'string' && !this._upstream ? true : this._upstream, + answer_dnssec: !this._answer_dnssec ? true : typeof this._answer_dnssec === 'boolean', + client: !this._client ? true : typeof this._client === 'string' && !this._client ? true : this._client, + client_id: !this._client_id ? true : typeof this._client_id === 'string' && !this._client_id ? true : this._client_id, + elapsedMs: !this._elapsedMs ? true : typeof this._elapsedMs === 'string' && !this._elapsedMs ? true : this._elapsedMs, + question: !this._question ? true : this._question.validate().length === 0, + filterId: !this._filterId ? true : typeof this._filterId === 'number', + rule: !this._rule ? true : typeof this._rule === 'string' && !this._rule ? true : this._rule, + rules: !this._rules ? true : this._rules.reduce((result, p) => result && p.validate().length === 0, true), + reason: !this._reason ? true : typeof this._reason === 'string' && !this._reason ? true : this._reason, + service_name: !this._service_name ? true : typeof this._service_name === 'string' && !this._service_name ? true : this._service_name, + status: !this._status ? true : typeof this._status === 'string' && !this._status ? true : this._status, + time: !this._time ? true : typeof this._time === 'string' && !this._time ? true : this._time, + }; + const isError: string[] = []; + Object.keys(validate).forEach((key) => { + if (!(validate as any)[key]) { + isError.push(key); + } + }); + return isError; + } + + update(props: Partial): QueryLogItem { + return new QueryLogItem({ ...this.serialize(), ...props }); + } +} diff --git a/client2/src/lib/entities/RemoveUrlRequest.ts b/client2/src/lib/entities/RemoveUrlRequest.ts new file mode 100644 index 00000000..b45571a0 --- /dev/null +++ b/client2/src/lib/entities/RemoveUrlRequest.ts @@ -0,0 +1,49 @@ +// This file was autogenerated. Please do not change. +// All changes will be overwrited on commit. +export interface IRemoveUrlRequest { + url?: string; +} + +export default class RemoveUrlRequest { + readonly _url: string | undefined; + + /** + * Description: Previously added URL containing filtering rules + * Example: https://filters.adtidy.org/windows/filters/15.txt + */ + get url(): string | undefined { + return this._url; + } + + constructor(props: IRemoveUrlRequest) { + if (typeof props.url === 'string') { + this._url = props.url.trim(); + } + } + + serialize(): IRemoveUrlRequest { + const data: IRemoveUrlRequest = { + }; + if (typeof this._url !== 'undefined') { + data.url = this._url; + } + return data; + } + + validate(): string[] { + const validate = { + url: !this._url ? true : typeof this._url === 'string' && !this._url ? true : this._url, + }; + const isError: string[] = []; + Object.keys(validate).forEach((key) => { + if (!(validate as any)[key]) { + isError.push(key); + } + }); + return isError; + } + + update(props: Partial): RemoveUrlRequest { + return new RemoveUrlRequest({ ...this.serialize(), ...props }); + } +} diff --git a/client2/src/lib/entities/ResultRule.ts b/client2/src/lib/entities/ResultRule.ts new file mode 100644 index 00000000..d0f03c98 --- /dev/null +++ b/client2/src/lib/entities/ResultRule.ts @@ -0,0 +1,69 @@ +// This file was autogenerated. Please do not change. +// All changes will be overwrited on commit. +export interface IResultRule { + filter_list_id?: number; + text?: string; +} + +export default class ResultRule { + readonly _filter_list_id: number | undefined; + + /** + * Description: In case if there's a rule applied to this DNS request, this is ID of the filter list that the rule belongs to. + * + * Example: 123123 + */ + get filterListId(): number | undefined { + return this._filter_list_id; + } + + readonly _text: string | undefined; + + /** + * Description: The text of the filtering rule applied to the request (if any). + * + * Example: ||example.org^ + */ + get text(): string | undefined { + return this._text; + } + + constructor(props: IResultRule) { + if (typeof props.filter_list_id === 'number') { + this._filter_list_id = props.filter_list_id; + } + if (typeof props.text === 'string') { + this._text = props.text.trim(); + } + } + + serialize(): IResultRule { + const data: IResultRule = { + }; + if (typeof this._filter_list_id !== 'undefined') { + data.filter_list_id = this._filter_list_id; + } + if (typeof this._text !== 'undefined') { + data.text = this._text; + } + return data; + } + + validate(): string[] { + const validate = { + filter_list_id: !this._filter_list_id ? true : typeof this._filter_list_id === 'number', + text: !this._text ? true : typeof this._text === 'string' && !this._text ? true : this._text, + }; + const isError: string[] = []; + Object.keys(validate).forEach((key) => { + if (!(validate as any)[key]) { + isError.push(key); + } + }); + return isError; + } + + update(props: Partial): ResultRule { + return new ResultRule({ ...this.serialize(), ...props }); + } +} diff --git a/client2/src/lib/entities/RewriteEntry.ts b/client2/src/lib/entities/RewriteEntry.ts new file mode 100644 index 00000000..31612b57 --- /dev/null +++ b/client2/src/lib/entities/RewriteEntry.ts @@ -0,0 +1,67 @@ +// This file was autogenerated. Please do not change. +// All changes will be overwrited on commit. +export interface IRewriteEntry { + answer?: string; + domain?: string; +} + +export default class RewriteEntry { + readonly _answer: string | undefined; + + /** + * Description: value of A, AAAA or CNAME DNS record + * Example: 127.0.0.1 + */ + get answer(): string | undefined { + return this._answer; + } + + readonly _domain: string | undefined; + + /** + * Description: Domain name + * Example: example.org + */ + get domain(): string | undefined { + return this._domain; + } + + constructor(props: IRewriteEntry) { + if (typeof props.answer === 'string') { + this._answer = props.answer.trim(); + } + if (typeof props.domain === 'string') { + this._domain = props.domain.trim(); + } + } + + serialize(): IRewriteEntry { + const data: IRewriteEntry = { + }; + if (typeof this._answer !== 'undefined') { + data.answer = this._answer; + } + if (typeof this._domain !== 'undefined') { + data.domain = this._domain; + } + return data; + } + + validate(): string[] { + const validate = { + domain: !this._domain ? true : typeof this._domain === 'string' && !this._domain ? true : this._domain, + answer: !this._answer ? true : typeof this._answer === 'string' && !this._answer ? true : this._answer, + }; + const isError: string[] = []; + Object.keys(validate).forEach((key) => { + if (!(validate as any)[key]) { + isError.push(key); + } + }); + return isError; + } + + update(props: Partial): RewriteEntry { + return new RewriteEntry({ ...this.serialize(), ...props }); + } +} diff --git a/client2/src/lib/entities/RewriteList.ts b/client2/src/lib/entities/RewriteList.ts new file mode 100644 index 00000000..a92ff328 --- /dev/null +++ b/client2/src/lib/entities/RewriteList.ts @@ -0,0 +1,31 @@ +// This file was autogenerated. Please do not change. +// All changes will be overwrited on commit. +export interface IRewriteList { +} + +export default class RewriteList { + constructor(props: IRewriteList) { + } + + serialize(): IRewriteList { + const data: IRewriteList = { + }; + return data; + } + + validate(): string[] { + const validate = { + }; + const isError: string[] = []; + Object.keys(validate).forEach((key) => { + if (!(validate as any)[key]) { + isError.push(key); + } + }); + return isError; + } + + update(props: Partial): RewriteList { + return new RewriteList({ ...this.serialize(), ...props }); + } +} diff --git a/client2/src/lib/entities/ServerStatus.ts b/client2/src/lib/entities/ServerStatus.ts new file mode 100644 index 00000000..d70f0adf --- /dev/null +++ b/client2/src/lib/entities/ServerStatus.ts @@ -0,0 +1,179 @@ +// This file was autogenerated. Please do not change. +// All changes will be overwrited on commit. +export interface IServerStatus { + dhcp_available?: boolean; + dns_addresses: string[]; + dns_port: number; + http_port: number; + language: string; + protection_enabled: boolean; + running: boolean; + version: string; +} + +export default class ServerStatus { + readonly _dhcp_available: boolean | undefined; + + get dhcpAvailable(): boolean | undefined { + return this._dhcp_available; + } + + readonly _dns_addresses: string[]; + + /** + * Description: undefined + * Example: 127.0.0.1 + */ + get dnsAddresses(): string[] { + return this._dns_addresses; + } + + static dnsAddressesValidate(dnsAddresses: string[]): boolean { + return dnsAddresses.reduce((result, p) => result && (typeof p === 'string' && !!p.trim()), true); + } + + readonly _dns_port: number; + + /** + * Description: undefined + * Example: 53 + */ + get dnsPort(): number { + return this._dns_port; + } + + static get dnsPortMinValue() { + return 1; + } + + static get dnsPortMaxValue() { + return 65535; + } + + static dnsPortValidate(dnsPort: number): boolean { + return dnsPort >= 1 && dnsPort <= 65535; + } + + readonly _http_port: number; + + /** + * Description: undefined + * Example: 80 + */ + get httpPort(): number { + return this._http_port; + } + + static get httpPortMinValue() { + return 1; + } + + static get httpPortMaxValue() { + return 65535; + } + + static httpPortValidate(httpPort: number): boolean { + return httpPort >= 1 && httpPort <= 65535; + } + + readonly _language: string; + + /** + * Description: undefined + * Example: en + */ + get language(): string { + return this._language; + } + + static languageValidate(language: string): boolean { + return typeof language === 'string' && !!language.trim(); + } + + readonly _protection_enabled: boolean; + + get protectionEnabled(): boolean { + return this._protection_enabled; + } + + static protectionEnabledValidate(protectionEnabled: boolean): boolean { + return typeof protectionEnabled === 'boolean'; + } + + readonly _running: boolean; + + get running(): boolean { + return this._running; + } + + static runningValidate(running: boolean): boolean { + return typeof running === 'boolean'; + } + + readonly _version: string; + + /** + * Description: undefined + * Example: 0.1 + */ + get version(): string { + return this._version; + } + + static versionValidate(version: string): boolean { + return typeof version === 'string' && !!version.trim(); + } + + constructor(props: IServerStatus) { + if (typeof props.dhcp_available === 'boolean') { + this._dhcp_available = props.dhcp_available; + } + this._dns_addresses = props.dns_addresses; + this._dns_port = props.dns_port; + this._http_port = props.http_port; + this._language = props.language.trim(); + this._protection_enabled = props.protection_enabled; + this._running = props.running; + this._version = props.version.trim(); + } + + serialize(): IServerStatus { + const data: IServerStatus = { + dns_addresses: this._dns_addresses, + dns_port: this._dns_port, + http_port: this._http_port, + language: this._language, + protection_enabled: this._protection_enabled, + running: this._running, + version: this._version, + }; + if (typeof this._dhcp_available !== 'undefined') { + data.dhcp_available = this._dhcp_available; + } + return data; + } + + validate(): string[] { + const validate = { + dns_addresses: this._dns_addresses.reduce((result, p) => result && typeof p === 'string', true), + dns_port: this._dns_port >= 1 && this._dns_port <= 65535, + http_port: this._http_port >= 1 && this._http_port <= 65535, + protection_enabled: typeof this._protection_enabled === 'boolean', + dhcp_available: !this._dhcp_available ? true : typeof this._dhcp_available === 'boolean', + running: typeof this._running === 'boolean', + version: typeof this._version === 'string' && !this._version ? true : this._version, + language: typeof this._language === 'string' && !this._language ? true : this._language, + }; + const isError: string[] = []; + Object.keys(validate).forEach((key) => { + if (!(validate as any)[key]) { + isError.push(key); + } + }); + return isError; + } + + update(props: Partial): ServerStatus { + return new ServerStatus({ ...this.serialize(), ...props }); + } +} diff --git a/client2/src/lib/entities/Stats.ts b/client2/src/lib/entities/Stats.ts new file mode 100644 index 00000000..bae0e1b5 --- /dev/null +++ b/client2/src/lib/entities/Stats.ts @@ -0,0 +1,257 @@ +import TopArrayEntry, { ITopArrayEntry } from './TopArrayEntry'; + +// This file was autogenerated. Please do not change. +// All changes will be overwrited on commit. +export interface IStats { + avg_processing_time?: number; + blocked_filtering?: number[]; + dns_queries?: number[]; + num_blocked_filtering?: number; + num_dns_queries?: number; + num_replaced_parental?: number; + num_replaced_safebrowsing?: number; + num_replaced_safesearch?: number; + replaced_parental?: number[]; + replaced_safebrowsing?: number[]; + time_units?: string; + top_blocked_domains?: ITopArrayEntry[]; + top_clients?: ITopArrayEntry[]; + top_queried_domains?: ITopArrayEntry[]; +} + +export default class Stats { + readonly _avg_processing_time: number | undefined; + + /** + * Description: Average time in milliseconds on processing a DNS + * Example: 0.34 + */ + get avgProcessingTime(): number | undefined { + return this._avg_processing_time; + } + + readonly _blocked_filtering: number[] | undefined; + + get blockedFiltering(): number[] | undefined { + return this._blocked_filtering; + } + + readonly _dns_queries: number[] | undefined; + + get dnsQueries(): number[] | undefined { + return this._dns_queries; + } + + readonly _num_blocked_filtering: number | undefined; + + /** + * Description: Number of requests blocked by filtering rules + * Example: 50 + */ + get numBlockedFiltering(): number | undefined { + return this._num_blocked_filtering; + } + + readonly _num_dns_queries: number | undefined; + + /** + * Description: Total number of DNS queries + * Example: 123 + */ + get numDnsQueries(): number | undefined { + return this._num_dns_queries; + } + + readonly _num_replaced_parental: number | undefined; + + /** + * Description: Number of blocked adult websites + * Example: 15 + */ + get numReplacedParental(): number | undefined { + return this._num_replaced_parental; + } + + readonly _num_replaced_safebrowsing: number | undefined; + + /** + * Description: Number of requests blocked by safebrowsing module + * Example: 5 + */ + get numReplacedSafebrowsing(): number | undefined { + return this._num_replaced_safebrowsing; + } + + readonly _num_replaced_safesearch: number | undefined; + + /** + * Description: Number of requests blocked by safesearch module + * Example: 5 + */ + get numReplacedSafesearch(): number | undefined { + return this._num_replaced_safesearch; + } + + readonly _replaced_parental: number[] | undefined; + + get replacedParental(): number[] | undefined { + return this._replaced_parental; + } + + readonly _replaced_safebrowsing: number[] | undefined; + + get replacedSafebrowsing(): number[] | undefined { + return this._replaced_safebrowsing; + } + + readonly _time_units: string | undefined; + + /** + * Description: Time units + * Example: hours + */ + get timeUnits(): string | undefined { + return this._time_units; + } + + readonly _top_blocked_domains: TopArrayEntry[] | undefined; + + get topBlockedDomains(): TopArrayEntry[] | undefined { + return this._top_blocked_domains; + } + + readonly _top_clients: TopArrayEntry[] | undefined; + + get topClients(): TopArrayEntry[] | undefined { + return this._top_clients; + } + + readonly _top_queried_domains: TopArrayEntry[] | undefined; + + get topQueriedDomains(): TopArrayEntry[] | undefined { + return this._top_queried_domains; + } + + constructor(props: IStats) { + if (typeof props.avg_processing_time === 'number') { + this._avg_processing_time = props.avg_processing_time; + } + if (props.blocked_filtering) { + this._blocked_filtering = props.blocked_filtering; + } + if (props.dns_queries) { + this._dns_queries = props.dns_queries; + } + if (typeof props.num_blocked_filtering === 'number') { + this._num_blocked_filtering = props.num_blocked_filtering; + } + if (typeof props.num_dns_queries === 'number') { + this._num_dns_queries = props.num_dns_queries; + } + if (typeof props.num_replaced_parental === 'number') { + this._num_replaced_parental = props.num_replaced_parental; + } + if (typeof props.num_replaced_safebrowsing === 'number') { + this._num_replaced_safebrowsing = props.num_replaced_safebrowsing; + } + if (typeof props.num_replaced_safesearch === 'number') { + this._num_replaced_safesearch = props.num_replaced_safesearch; + } + if (props.replaced_parental) { + this._replaced_parental = props.replaced_parental; + } + if (props.replaced_safebrowsing) { + this._replaced_safebrowsing = props.replaced_safebrowsing; + } + if (typeof props.time_units === 'string') { + this._time_units = props.time_units.trim(); + } + if (props.top_blocked_domains) { + this._top_blocked_domains = props.top_blocked_domains.map((p) => new TopArrayEntry(p)); + } + if (props.top_clients) { + this._top_clients = props.top_clients.map((p) => new TopArrayEntry(p)); + } + if (props.top_queried_domains) { + this._top_queried_domains = props.top_queried_domains.map((p) => new TopArrayEntry(p)); + } + } + + serialize(): IStats { + const data: IStats = { + }; + if (typeof this._avg_processing_time !== 'undefined') { + data.avg_processing_time = this._avg_processing_time; + } + if (typeof this._blocked_filtering !== 'undefined') { + data.blocked_filtering = this._blocked_filtering; + } + if (typeof this._dns_queries !== 'undefined') { + data.dns_queries = this._dns_queries; + } + if (typeof this._num_blocked_filtering !== 'undefined') { + data.num_blocked_filtering = this._num_blocked_filtering; + } + if (typeof this._num_dns_queries !== 'undefined') { + data.num_dns_queries = this._num_dns_queries; + } + if (typeof this._num_replaced_parental !== 'undefined') { + data.num_replaced_parental = this._num_replaced_parental; + } + if (typeof this._num_replaced_safebrowsing !== 'undefined') { + data.num_replaced_safebrowsing = this._num_replaced_safebrowsing; + } + if (typeof this._num_replaced_safesearch !== 'undefined') { + data.num_replaced_safesearch = this._num_replaced_safesearch; + } + if (typeof this._replaced_parental !== 'undefined') { + data.replaced_parental = this._replaced_parental; + } + if (typeof this._replaced_safebrowsing !== 'undefined') { + data.replaced_safebrowsing = this._replaced_safebrowsing; + } + if (typeof this._time_units !== 'undefined') { + data.time_units = this._time_units; + } + if (typeof this._top_blocked_domains !== 'undefined') { + data.top_blocked_domains = this._top_blocked_domains.map((p) => p.serialize()); + } + if (typeof this._top_clients !== 'undefined') { + data.top_clients = this._top_clients.map((p) => p.serialize()); + } + if (typeof this._top_queried_domains !== 'undefined') { + data.top_queried_domains = this._top_queried_domains.map((p) => p.serialize()); + } + return data; + } + + validate(): string[] { + const validate = { + time_units: !this._time_units ? true : typeof this._time_units === 'string' && !this._time_units ? true : this._time_units, + num_dns_queries: !this._num_dns_queries ? true : typeof this._num_dns_queries === 'number', + num_blocked_filtering: !this._num_blocked_filtering ? true : typeof this._num_blocked_filtering === 'number', + num_replaced_safebrowsing: !this._num_replaced_safebrowsing ? true : typeof this._num_replaced_safebrowsing === 'number', + num_replaced_safesearch: !this._num_replaced_safesearch ? true : typeof this._num_replaced_safesearch === 'number', + num_replaced_parental: !this._num_replaced_parental ? true : typeof this._num_replaced_parental === 'number', + avg_processing_time: !this._avg_processing_time ? true : typeof this._avg_processing_time === 'number', + top_queried_domains: !this._top_queried_domains ? true : this._top_queried_domains.reduce((result, p) => result && p.validate().length === 0, true), + top_clients: !this._top_clients ? true : this._top_clients.reduce((result, p) => result && p.validate().length === 0, true), + top_blocked_domains: !this._top_blocked_domains ? true : this._top_blocked_domains.reduce((result, p) => result && p.validate().length === 0, true), + dns_queries: !this._dns_queries ? true : this._dns_queries.reduce((result, p) => result && typeof p === 'number', true), + blocked_filtering: !this._blocked_filtering ? true : this._blocked_filtering.reduce((result, p) => result && typeof p === 'number', true), + replaced_safebrowsing: !this._replaced_safebrowsing ? true : this._replaced_safebrowsing.reduce((result, p) => result && typeof p === 'number', true), + replaced_parental: !this._replaced_parental ? true : this._replaced_parental.reduce((result, p) => result && typeof p === 'number', true), + }; + const isError: string[] = []; + Object.keys(validate).forEach((key) => { + if (!(validate as any)[key]) { + isError.push(key); + } + }); + return isError; + } + + update(props: Partial): Stats { + return new Stats({ ...this.serialize(), ...props }); + } +} diff --git a/client2/src/lib/entities/StatsConfig.ts b/client2/src/lib/entities/StatsConfig.ts new file mode 100644 index 00000000..516105ea --- /dev/null +++ b/client2/src/lib/entities/StatsConfig.ts @@ -0,0 +1,46 @@ +// This file was autogenerated. Please do not change. +// All changes will be overwrited on commit. +export interface IStatsConfig { + interval?: number; +} + +export default class StatsConfig { + readonly _interval: number | undefined; + + /** */ + get interval(): number | undefined { + return this._interval; + } + + constructor(props: IStatsConfig) { + if (typeof props.interval === 'number') { + this._interval = props.interval; + } + } + + serialize(): IStatsConfig { + const data: IStatsConfig = { + }; + if (typeof this._interval !== 'undefined') { + data.interval = this._interval; + } + return data; + } + + validate(): string[] { + const validate = { + interval: !this._interval ? true : typeof this._interval === 'number', + }; + const isError: string[] = []; + Object.keys(validate).forEach((key) => { + if (!(validate as any)[key]) { + isError.push(key); + } + }); + return isError; + } + + update(props: Partial): StatsConfig { + return new StatsConfig({ ...this.serialize(), ...props }); + } +} diff --git a/client2/src/lib/entities/TlsConfig.ts b/client2/src/lib/entities/TlsConfig.ts new file mode 100644 index 00000000..be3ecad1 --- /dev/null +++ b/client2/src/lib/entities/TlsConfig.ts @@ -0,0 +1,404 @@ +// This file was autogenerated. Please do not change. +// All changes will be overwrited on commit. +export interface ITlsConfig { + certificate_chain?: string; + certificate_path?: string; + dns_names?: string[]; + enabled?: boolean; + force_https?: boolean; + issuer?: string; + key_type?: string; + not_after?: string; + not_before?: string; + port_dns_over_quic?: number; + port_dns_over_tls?: number; + port_https?: number; + private_key?: string; + private_key_path?: string; + server_name?: string; + subject?: string; + valid_cert?: boolean; + valid_chain?: boolean; + valid_key?: boolean; + valid_pair?: boolean; + warning_validation?: string; +} + +export default class TlsConfig { + readonly _certificate_chain: string | undefined; + + /** */ + get certificateChain(): string | undefined { + return this._certificate_chain; + } + + readonly _certificate_path: string | undefined; + + /** */ + get certificatePath(): string | undefined { + return this._certificate_path; + } + + readonly _dns_names: string[] | undefined; + + /** + * Description: The value of SubjectAltNames field of the first certificate in the chain. + * + * Example: *.example.org + */ + get dnsNames(): string[] | undefined { + return this._dns_names; + } + + readonly _enabled: boolean | undefined; + + /** + * Description: enabled is the encryption (DOT/DOH/HTTPS) status + * Example: true + */ + get enabled(): boolean | undefined { + return this._enabled; + } + + readonly _force_https: boolean | undefined; + + /** + * Description: if true, forces HTTP->HTTPS redirect + * Example: true + */ + get forceHttps(): boolean | undefined { + return this._force_https; + } + + readonly _issuer: string | undefined; + + /** + * Description: The issuer of the first certificate in the chain. + * Example: CN=Let's Encrypt Authority X3,O=Let's Encrypt,C=US + */ + get issuer(): string | undefined { + return this._issuer; + } + + readonly _key_type: string | undefined; + + /** + * Description: Key type. + * Example: RSA + */ + get keyType(): string | undefined { + return this._key_type; + } + + readonly _not_after: string | undefined; + + /** + * Description: The NotAfter field of the first certificate in the chain. + * + * Example: 2019-05-01T10:47:32Z + */ + get notAfter(): string | undefined { + return this._not_after; + } + + readonly _not_before: string | undefined; + + /** + * Description: The NotBefore field of the first certificate in the chain. + * + * Example: 2019-01-31T10:47:32Z + */ + get notBefore(): string | undefined { + return this._not_before; + } + + readonly _port_dns_over_quic: number | undefined; + + /** + * Description: DNS-over-QUIC port. If 0, DOQ will be disabled. + * Example: 784 + */ + get portDnsOverQuic(): number | undefined { + return this._port_dns_over_quic; + } + + readonly _port_dns_over_tls: number | undefined; + + /** + * Description: DNS-over-TLS port. If 0, DOT will be disabled. + * Example: 853 + */ + get portDnsOverTls(): number | undefined { + return this._port_dns_over_tls; + } + + readonly _port_https: number | undefined; + + /** + * Description: HTTPS port. If 0, HTTPS will be disabled. + * Example: 443 + */ + get portHttps(): number | undefined { + return this._port_https; + } + + readonly _private_key: string | undefined; + + /** */ + get privateKey(): string | undefined { + return this._private_key; + } + + readonly _private_key_path: string | undefined; + + /** */ + get privateKeyPath(): string | undefined { + return this._private_key_path; + } + + readonly _server_name: string | undefined; + + /** + * Description: server_name is the hostname of your HTTPS/TLS server + * Example: example.org + */ + get serverName(): string | undefined { + return this._server_name; + } + + readonly _subject: string | undefined; + + /** + * Description: The subject of the first certificate in the chain. + * Example: CN=example.org + */ + get subject(): string | undefined { + return this._subject; + } + + readonly _valid_cert: boolean | undefined; + + /** + * Description: Set to true if the specified certificates chain is a valid chain of X509 certificates. + * + * Example: true + */ + get validCert(): boolean | undefined { + return this._valid_cert; + } + + readonly _valid_chain: boolean | undefined; + + /** + * Description: Set to true if the specified certificates chain is verified and issued by a known CA. + * + * Example: true + */ + get validChain(): boolean | undefined { + return this._valid_chain; + } + + readonly _valid_key: boolean | undefined; + + /** + * Description: Set to true if the key is a valid private key. + * Example: true + */ + get validKey(): boolean | undefined { + return this._valid_key; + } + + readonly _valid_pair: boolean | undefined; + + /** + * Description: Set to true if both certificate and private key are correct. + * + * Example: true + */ + get validPair(): boolean | undefined { + return this._valid_pair; + } + + readonly _warning_validation: string | undefined; + + /** + * Description: A validation warning message with the issue description. + * + * Example: You have specified an empty certificate + */ + get warningValidation(): string | undefined { + return this._warning_validation; + } + + constructor(props: ITlsConfig) { + if (typeof props.certificate_chain === 'string') { + this._certificate_chain = props.certificate_chain.trim(); + } + if (typeof props.certificate_path === 'string') { + this._certificate_path = props.certificate_path.trim(); + } + if (props.dns_names) { + this._dns_names = props.dns_names; + } + if (typeof props.enabled === 'boolean') { + this._enabled = props.enabled; + } + if (typeof props.force_https === 'boolean') { + this._force_https = props.force_https; + } + if (typeof props.issuer === 'string') { + this._issuer = props.issuer.trim(); + } + if (typeof props.key_type === 'string') { + this._key_type = props.key_type.trim(); + } + if (typeof props.not_after === 'string') { + this._not_after = props.not_after.trim(); + } + if (typeof props.not_before === 'string') { + this._not_before = props.not_before.trim(); + } + if (typeof props.port_dns_over_quic === 'number') { + this._port_dns_over_quic = props.port_dns_over_quic; + } + if (typeof props.port_dns_over_tls === 'number') { + this._port_dns_over_tls = props.port_dns_over_tls; + } + if (typeof props.port_https === 'number') { + this._port_https = props.port_https; + } + if (typeof props.private_key === 'string') { + this._private_key = props.private_key.trim(); + } + if (typeof props.private_key_path === 'string') { + this._private_key_path = props.private_key_path.trim(); + } + if (typeof props.server_name === 'string') { + this._server_name = props.server_name.trim(); + } + if (typeof props.subject === 'string') { + this._subject = props.subject.trim(); + } + if (typeof props.valid_cert === 'boolean') { + this._valid_cert = props.valid_cert; + } + if (typeof props.valid_chain === 'boolean') { + this._valid_chain = props.valid_chain; + } + if (typeof props.valid_key === 'boolean') { + this._valid_key = props.valid_key; + } + if (typeof props.valid_pair === 'boolean') { + this._valid_pair = props.valid_pair; + } + if (typeof props.warning_validation === 'string') { + this._warning_validation = props.warning_validation.trim(); + } + } + + serialize(): ITlsConfig { + const data: ITlsConfig = { + }; + if (typeof this._certificate_chain !== 'undefined') { + data.certificate_chain = this._certificate_chain; + } + if (typeof this._certificate_path !== 'undefined') { + data.certificate_path = this._certificate_path; + } + if (typeof this._dns_names !== 'undefined') { + data.dns_names = this._dns_names; + } + if (typeof this._enabled !== 'undefined') { + data.enabled = this._enabled; + } + if (typeof this._force_https !== 'undefined') { + data.force_https = this._force_https; + } + if (typeof this._issuer !== 'undefined') { + data.issuer = this._issuer; + } + if (typeof this._key_type !== 'undefined') { + data.key_type = this._key_type; + } + if (typeof this._not_after !== 'undefined') { + data.not_after = this._not_after; + } + if (typeof this._not_before !== 'undefined') { + data.not_before = this._not_before; + } + if (typeof this._port_dns_over_quic !== 'undefined') { + data.port_dns_over_quic = this._port_dns_over_quic; + } + if (typeof this._port_dns_over_tls !== 'undefined') { + data.port_dns_over_tls = this._port_dns_over_tls; + } + if (typeof this._port_https !== 'undefined') { + data.port_https = this._port_https; + } + if (typeof this._private_key !== 'undefined') { + data.private_key = this._private_key; + } + if (typeof this._private_key_path !== 'undefined') { + data.private_key_path = this._private_key_path; + } + if (typeof this._server_name !== 'undefined') { + data.server_name = this._server_name; + } + if (typeof this._subject !== 'undefined') { + data.subject = this._subject; + } + if (typeof this._valid_cert !== 'undefined') { + data.valid_cert = this._valid_cert; + } + if (typeof this._valid_chain !== 'undefined') { + data.valid_chain = this._valid_chain; + } + if (typeof this._valid_key !== 'undefined') { + data.valid_key = this._valid_key; + } + if (typeof this._valid_pair !== 'undefined') { + data.valid_pair = this._valid_pair; + } + if (typeof this._warning_validation !== 'undefined') { + data.warning_validation = this._warning_validation; + } + return data; + } + + validate(): string[] { + const validate = { + enabled: !this._enabled ? true : typeof this._enabled === 'boolean', + server_name: !this._server_name ? true : typeof this._server_name === 'string' && !this._server_name ? true : this._server_name, + force_https: !this._force_https ? true : typeof this._force_https === 'boolean', + port_https: !this._port_https ? true : typeof this._port_https === 'number', + port_dns_over_tls: !this._port_dns_over_tls ? true : typeof this._port_dns_over_tls === 'number', + port_dns_over_quic: !this._port_dns_over_quic ? true : typeof this._port_dns_over_quic === 'number', + certificate_chain: !this._certificate_chain ? true : typeof this._certificate_chain === 'string' && !this._certificate_chain ? true : this._certificate_chain, + private_key: !this._private_key ? true : typeof this._private_key === 'string' && !this._private_key ? true : this._private_key, + certificate_path: !this._certificate_path ? true : typeof this._certificate_path === 'string' && !this._certificate_path ? true : this._certificate_path, + private_key_path: !this._private_key_path ? true : typeof this._private_key_path === 'string' && !this._private_key_path ? true : this._private_key_path, + valid_cert: !this._valid_cert ? true : typeof this._valid_cert === 'boolean', + valid_chain: !this._valid_chain ? true : typeof this._valid_chain === 'boolean', + subject: !this._subject ? true : typeof this._subject === 'string' && !this._subject ? true : this._subject, + issuer: !this._issuer ? true : typeof this._issuer === 'string' && !this._issuer ? true : this._issuer, + not_before: !this._not_before ? true : typeof this._not_before === 'string' && !this._not_before ? true : this._not_before, + not_after: !this._not_after ? true : typeof this._not_after === 'string' && !this._not_after ? true : this._not_after, + dns_names: !this._dns_names ? true : this._dns_names.reduce((result, p) => result && typeof p === 'string', true), + valid_key: !this._valid_key ? true : typeof this._valid_key === 'boolean', + key_type: !this._key_type ? true : typeof this._key_type === 'string' && !this._key_type ? true : this._key_type, + warning_validation: !this._warning_validation ? true : typeof this._warning_validation === 'string' && !this._warning_validation ? true : this._warning_validation, + valid_pair: !this._valid_pair ? true : typeof this._valid_pair === 'boolean', + }; + const isError: string[] = []; + Object.keys(validate).forEach((key) => { + if (!(validate as any)[key]) { + isError.push(key); + } + }); + return isError; + } + + update(props: Partial): TlsConfig { + return new TlsConfig({ ...this.serialize(), ...props }); + } +} diff --git a/client2/src/lib/entities/TopArrayEntry.ts b/client2/src/lib/entities/TopArrayEntry.ts new file mode 100644 index 00000000..2720683a --- /dev/null +++ b/client2/src/lib/entities/TopArrayEntry.ts @@ -0,0 +1,47 @@ +// This file was autogenerated. Please do not change. +// All changes will be overwrited on commit. +export interface ITopArrayEntry { + domain_or_ip?: number; + [key: string]: number | undefined; +} + +export default class TopArrayEntry { + readonly _domain_or_ip: number | undefined; + + get domainOrIp(): number | undefined { + return this._domain_or_ip; + } + + readonly numberData: Record; + + constructor(props: ITopArrayEntry) { + this.numberData = Object.entries(props).reduce>((prev, [key, value]) => { + prev[key] = value!; + return prev; + }, {}) + } + + serialize(): ITopArrayEntry { + return Object.entries(this.numberData).reduce>((prev, [key, value]) => { + prev[key] = value; + return prev; + }, {}) + } + + validate(): string[] { + const validate = { + domain_or_ip: !this._domain_or_ip ? true : typeof this._domain_or_ip === 'number', + }; + const isError: string[] = []; + Object.keys(validate).forEach((key) => { + if (!(validate as any)[key]) { + isError.push(key); + } + }); + return isError; + } + + update(props: Partial): TopArrayEntry { + return new TopArrayEntry({ ...this.serialize(), ...props }); + } +} diff --git a/client2/src/lib/entities/UpstreamsConfig.ts b/client2/src/lib/entities/UpstreamsConfig.ts new file mode 100644 index 00000000..5b9c0a6b --- /dev/null +++ b/client2/src/lib/entities/UpstreamsConfig.ts @@ -0,0 +1,69 @@ +// This file was autogenerated. Please do not change. +// All changes will be overwrited on commit. +export interface IUpstreamsConfig { + bootstrap_dns: string[]; + upstream_dns: string[]; +} + +export default class UpstreamsConfig { + readonly _bootstrap_dns: string[]; + + /** + * Description: Bootstrap servers, port is optional after colon. Empty value will reset it to default values. + * + * Example: 8.8.8.8:53,1.1.1.1:53 + */ + get bootstrapDns(): string[] { + return this._bootstrap_dns; + } + + static bootstrapDnsValidate(bootstrapDns: string[]): boolean { + return bootstrapDns.reduce((result, p) => result && (typeof p === 'string' && !!p.trim()), true); + } + + readonly _upstream_dns: string[]; + + /** + * Description: Upstream servers, port is optional after colon. Empty value will reset it to default values. + * + * Example: tls://1.1.1.1,tls://1.0.0.1 + */ + get upstreamDns(): string[] { + return this._upstream_dns; + } + + static upstreamDnsValidate(upstreamDns: string[]): boolean { + return upstreamDns.reduce((result, p) => result && (typeof p === 'string' && !!p.trim()), true); + } + + constructor(props: IUpstreamsConfig) { + this._bootstrap_dns = props.bootstrap_dns; + this._upstream_dns = props.upstream_dns; + } + + serialize(): IUpstreamsConfig { + const data: IUpstreamsConfig = { + bootstrap_dns: this._bootstrap_dns, + upstream_dns: this._upstream_dns, + }; + return data; + } + + validate(): string[] { + const validate = { + bootstrap_dns: this._bootstrap_dns.reduce((result, p) => result && typeof p === 'string', true), + upstream_dns: this._upstream_dns.reduce((result, p) => result && typeof p === 'string', true), + }; + const isError: string[] = []; + Object.keys(validate).forEach((key) => { + if (!(validate as any)[key]) { + isError.push(key); + } + }); + return isError; + } + + update(props: Partial): UpstreamsConfig { + return new UpstreamsConfig({ ...this.serialize(), ...props }); + } +} diff --git a/client2/src/lib/entities/UpstreamsConfigResponse.ts b/client2/src/lib/entities/UpstreamsConfigResponse.ts new file mode 100644 index 00000000..5d827a7e --- /dev/null +++ b/client2/src/lib/entities/UpstreamsConfigResponse.ts @@ -0,0 +1,31 @@ +// This file was autogenerated. Please do not change. +// All changes will be overwrited on commit. +export interface IUpstreamsConfigResponse { + [key: string]: string; +} + +export default class UpstreamsConfigResponse { + readonly data: Record; + + constructor(props: IUpstreamsConfigResponse) { + this.data = Object.entries(props).reduce>((prev, [key, value]) => { + prev[key] = value!; + return prev; + }, {}) + } + + serialize(): IUpstreamsConfigResponse { + return Object.entries(this.data).reduce>((prev, [key, value]) => { + prev[key] = value; + return prev; + }, {}) + } + + validate(): string[] { + return [] + } + + update(props: IUpstreamsConfigResponse): UpstreamsConfigResponse { + return new UpstreamsConfigResponse({ ...this.serialize(), ...props }); + } +} diff --git a/client2/src/lib/entities/VersionInfo.ts b/client2/src/lib/entities/VersionInfo.ts new file mode 100644 index 00000000..59d4b937 --- /dev/null +++ b/client2/src/lib/entities/VersionInfo.ts @@ -0,0 +1,115 @@ +// This file was autogenerated. Please do not change. +// All changes will be overwrited on commit. +export interface IVersionInfo { + announcement?: string; + announcement_url?: string; + can_autoupdate?: boolean; + disabled: boolean; + new_version?: string; +} + +export default class VersionInfo { + readonly _announcement: string | undefined; + + /** + * Description: undefined + * Example: AdGuard Home v0.9 is now available! + */ + get announcement(): string | undefined { + return this._announcement; + } + + readonly _announcement_url: string | undefined; + + /** + * Description: undefined + * Example: https://github.com/AdguardTeam/AdGuardHome/releases/tag/v0.9 + * + */ + get announcementUrl(): string | undefined { + return this._announcement_url; + } + + readonly _can_autoupdate: boolean | undefined; + + get canAutoupdate(): boolean | undefined { + return this._can_autoupdate; + } + + readonly _disabled: boolean; + + /** */ + get disabled(): boolean { + return this._disabled; + } + + static disabledValidate(disabled: boolean): boolean { + return typeof disabled === 'boolean'; + } + + readonly _new_version: string | undefined; + + /** + * Description: undefined + * Example: v0.9 + */ + get newVersion(): string | undefined { + return this._new_version; + } + + constructor(props: IVersionInfo) { + if (typeof props.announcement === 'string') { + this._announcement = props.announcement.trim(); + } + if (typeof props.announcement_url === 'string') { + this._announcement_url = props.announcement_url.trim(); + } + if (typeof props.can_autoupdate === 'boolean') { + this._can_autoupdate = props.can_autoupdate; + } + this._disabled = props.disabled; + if (typeof props.new_version === 'string') { + this._new_version = props.new_version.trim(); + } + } + + serialize(): IVersionInfo { + const data: IVersionInfo = { + disabled: this._disabled, + }; + if (typeof this._announcement !== 'undefined') { + data.announcement = this._announcement; + } + if (typeof this._announcement_url !== 'undefined') { + data.announcement_url = this._announcement_url; + } + if (typeof this._can_autoupdate !== 'undefined') { + data.can_autoupdate = this._can_autoupdate; + } + if (typeof this._new_version !== 'undefined') { + data.new_version = this._new_version; + } + return data; + } + + validate(): string[] { + const validate = { + disabled: typeof this._disabled === 'boolean', + new_version: !this._new_version ? true : typeof this._new_version === 'string' && !this._new_version ? true : this._new_version, + announcement: !this._announcement ? true : typeof this._announcement === 'string' && !this._announcement ? true : this._announcement, + announcement_url: !this._announcement_url ? true : typeof this._announcement_url === 'string' && !this._announcement_url ? true : this._announcement_url, + can_autoupdate: !this._can_autoupdate ? true : typeof this._can_autoupdate === 'boolean', + }; + const isError: string[] = []; + Object.keys(validate).forEach((key) => { + if (!(validate as any)[key]) { + isError.push(key); + } + }); + return isError; + } + + update(props: Partial): VersionInfo { + return new VersionInfo({ ...this.serialize(), ...props }); + } +} diff --git a/client2/src/lib/entities/WhoisInfo.ts b/client2/src/lib/entities/WhoisInfo.ts new file mode 100644 index 00000000..aa5ee628 --- /dev/null +++ b/client2/src/lib/entities/WhoisInfo.ts @@ -0,0 +1,31 @@ +// This file was autogenerated. Please do not change. +// All changes will be overwrited on commit. +export interface IWhoisInfo { + [key: string]: string; +} + +export default class WhoisInfo { + readonly data: Record; + + constructor(props: IWhoisInfo) { + this.data = Object.entries(props).reduce>((prev, [key, value]) => { + prev[key] = value!; + return prev; + }, {}) + } + + serialize(): IWhoisInfo { + return Object.entries(this.data).reduce>((prev, [key, value]) => { + prev[key] = value; + return prev; + }, {}) + } + + validate(): string[] { + return [] + } + + update(props: IWhoisInfo): WhoisInfo { + return new WhoisInfo({ ...this.serialize(), ...props }); + } +} diff --git a/client2/src/lib/helpers/apiErrors.ts b/client2/src/lib/helpers/apiErrors.ts new file mode 100644 index 00000000..c1916e1f --- /dev/null +++ b/client2/src/lib/helpers/apiErrors.ts @@ -0,0 +1,14 @@ +interface ErrorCheck { + error?: Error; + result?: T; +} + +export function errorChecker(response: Error | any): ErrorCheck { + if (typeof response !== 'object') { + return { result: response }; + } + if (response instanceof Error) { + return { error: response }; + } + return { result: response }; +} diff --git a/client2/src/lib/helpers/installHelpers.ts b/client2/src/lib/helpers/installHelpers.ts new file mode 100644 index 00000000..a4258402 --- /dev/null +++ b/client2/src/lib/helpers/installHelpers.ts @@ -0,0 +1,17 @@ +export enum NETWORK_TYPE { + LOCAL = 'LOCAL', + ETHERNET = 'ETHERNET', + OTHER = 'OTHER', +} + +export const chechNetworkType = (network: string | undefined) => { + if (!network) { + return NETWORK_TYPE.OTHER; + } + if (network.includes('en')) { + return NETWORK_TYPE.ETHERNET; + } + if (network.includes('lo')) { + return NETWORK_TYPE.LOCAL; + } +}; diff --git a/client2/src/lib/theme/Content.module.pcss b/client2/src/lib/theme/Content.module.pcss new file mode 100644 index 00000000..8f2246df --- /dev/null +++ b/client2/src/lib/theme/Content.module.pcss @@ -0,0 +1,51 @@ +.content { + min-height: 100vh; + + &_auth { + @media (--m-viewport) { + background-color: var(--gray100); + background-image: url('../../assets/img/install.png'); + background-position: center 20px; + background-repeat: no-repeat; + background-size: 100%; + } + } + + &_inner { + min-height: calc(100vh - var(--header-height)); + } +} + +.container { + width: 100%; + margin: 0 auto; + padding: 16px; + + @media (--l-viewport) { + padding: 24px; + } + + &_auth { + max-width: 432px; + padding: 24px 16px 40px; + + @media (--m-viewport) { + padding: 40px 16px; + } + } +} + +.header { + margin-bottom: 16px; + padding: 0 16px; + + @media (--m-viewport) { + margin-bottom: 24px; + padding: 0 24px; + } +} + +.title { + font-size: 16px; + font-weight: 600; +} diff --git a/client2/src/lib/theme/Form.module.pcss b/client2/src/lib/theme/Form.module.pcss new file mode 100644 index 00000000..f4988df6 --- /dev/null +++ b/client2/src/lib/theme/Form.module.pcss @@ -0,0 +1,78 @@ +.group { + display: block; + margin-bottom: 24px; + + &_last, + &:last-child { + margin-bottom: 0; + } +} + +.label { + margin-bottom: 4px; + font-size: 14px; + color: var(--gray700); +} + +.reveal { + color: var(--black); + transition: color var(--transition); + cursor: pointer; + + &:hover, + &:focus { + color: var(--gray); + } +} + +.reveal + .suffix { + margin-left: 16px; +} + +.addon { + display: flex; + height: 48px; + padding: 14px 3px 14px 14px; + font-size: 14px; + font-weight: 500; + line-height: 1.4; + cursor: pointer; + overflow: hidden; + + @media (--m-viewport) { + padding-right: 6px; + font-size: 16px; + line-height: 1.3; + } +} + +.addonCountry { + min-width: 28px; + margin-right: 3px; + + @media (--m-viewport) { + margin-right: 12px; + } +} + +.addonIcon { + position: relative; + top: -2px; + margin-right: 9px; + color: var(--concrete); +} + +.addonCode { + position: relative; + padding-left: 12px; + + &::before { + content: ""; + position: absolute; + top: -14px; + left: 0; + width: 1px; + height: 48px; + background-color: var(--borders-white); + } +} diff --git a/client2/src/lib/theme/Install.module.pcss b/client2/src/lib/theme/Install.module.pcss new file mode 100644 index 00000000..39b5d682 --- /dev/null +++ b/client2/src/lib/theme/Install.module.pcss @@ -0,0 +1,130 @@ +.title, +.subtitle, +.text { + color: var(--gray900); +} + +.title { + margin-bottom: 20px; + font-size: 28px; + line-height: 1.1; +} + +.subtitle { + margin-bottom: 12px; + font-size: 20px; + line-height: 1.4; +} + +.text { + font-size: 16px; + line-height: 1.5; + + &_block { + margin-bottom: 35px; + } + + &_base { + margin-bottom: 12px; + } +} + +.danger { + text-transform: capitalize; + color: var(--red400); + font-weight: bold; +} + +.actions { + display: flex; + align-items: center; + justify-content: space-between; + flex-direction: column-reverse; + margin-top: 48px; + + @media (--m-viewport) { + flex-direction: row; + } +} + +.button { + width: 100%; + min-width: 188px; + + &:last-child { + margin-bottom: 16px; + } + + &:only-child { + margin: 0; + } + + @media (--m-viewport) { + width: auto; + + &:first-child { + margin-right: 12px; + } + + &:last-child { + margin-bottom: 0; + margin-left: 12px; + } + + &:only-child { + margin: 0; + } + } +} + +.logo { + width: 130px; + height: 40px; + margin-bottom: 35px; + + @media (--m-viewport) { + width: 185px; + height: 57px; + } +} + +.ip { + font-family: var(--font-family-monospace); + font-size: 16px; + font-weight: 600; + word-break: break-all; + color: var(--green400); +} + +.options { + margin-bottom: 48px; +} + +.name { + padding-bottom: 5px; + border-bottom: 1px solid var(--gray300); + margin-bottom: 16px; + margin-top: 20px; +} + +.option { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 8px; +} + +.address { + margin-right: 16px; + word-break: break-all; + color: var(--gray400); +} + +.tabs { + width: 100%; + + @media (--m-viewport) { + width: 505px; + margin-left: -131px; + } +} diff --git a/client2/src/lib/theme/Link.module.pcss b/client2/src/lib/theme/Link.module.pcss new file mode 100644 index 00000000..b0751d90 --- /dev/null +++ b/client2/src/lib/theme/Link.module.pcss @@ -0,0 +1,27 @@ +.link { + color: var(--green400); + text-decoration: underline; + + &:hover, + &:focus { + color: var(--green700); + text-decoration: none; + } + + &:active { + color: var(--green400); + } + + &.gray { + color: var(--gray900); + + &:hover, + &:focus { + color: var(--gray700); + } + + &:active { + color: var(--gray700); + } + } +} diff --git a/client2/src/lib/theme/Text.module.pcss b/client2/src/lib/theme/Text.module.pcss new file mode 100644 index 00000000..55a5f4e3 --- /dev/null +++ b/client2/src/lib/theme/Text.module.pcss @@ -0,0 +1,41 @@ +.f14 { + font-size: 14px; +} + +.f16 { + font-size: 16px; +} + +.f20 { + font-size: 20px; +} + +.bold { + font-weight: 700; +} + +.medium { + font-weight: 600; +} + +.regular { + font-weight: 400; +} + +.code { + padding: 3px 5px; + font-size: 14px; + font-family: var(--font-family-monospace); + background-color: var(--gray300); + border-radius: 2px; +} + +.danger { + text-transform: uppercase; + color: var(--red400); + font-weight: bold; +} + +.center { + text-align: center; +} diff --git a/client2/src/lib/theme/colors.ts b/client2/src/lib/theme/colors.ts new file mode 100644 index 00000000..09b157b8 --- /dev/null +++ b/client2/src/lib/theme/colors.ts @@ -0,0 +1,8 @@ +export const colors = { + red: '#c23814', + orange: '#eb9300', + purple: '#b267a0', + green: '#67b279', + gray300: '#d8d8d8', + gray700: '#888888', +}; diff --git a/client2/src/lib/theme/index.ts b/client2/src/lib/theme/index.ts new file mode 100644 index 00000000..c215d1a0 --- /dev/null +++ b/client2/src/lib/theme/index.ts @@ -0,0 +1,18 @@ +import form from './Form.module.pcss'; +import text from './Text.module.pcss'; +import install from './Install.module.pcss'; +import link from './Link.module.pcss'; +import content from './Content.module.pcss'; + +import { colors } from './colors'; + +const theme = { + form, + chartColors: colors, + text, + install, + link, + content, +}; + +export default theme; diff --git a/client2/src/localization/index.ts b/client2/src/localization/index.ts new file mode 100644 index 00000000..a5a02eae --- /dev/null +++ b/client2/src/localization/index.ts @@ -0,0 +1,9 @@ +import { Locale, DatePickerLocale, messages, DEFAULT_LOCALE, LANGUAGES } from './locales'; + +export { Locale, DatePickerLocale, messages, DEFAULT_LOCALE, LANGUAGES }; +export const i18n = (lang: Locale) => ({ + getMessage: (key: string) => messages[lang][key], + getUILanguage: () => lang, + getBaseMessage: (key: string) => messages[DEFAULT_LOCALE][key] || key, + getBaseUILanguage: () => DEFAULT_LOCALE, +}); diff --git a/client2/src/localization/locales/en.json b/client2/src/localization/locales/en.json new file mode 100644 index 00000000..a0094f6a --- /dev/null +++ b/client2/src/localization/locales/en.json @@ -0,0 +1,137 @@ +{ + "back": "Back", + "ethernet": "Ethernet", + "localhost": "localhost", + "login": "Login", + "password": "Password", + "next": "Next", + "port": "Port", + "router": "Router", + "username": "Username", + "sign_in": "Sign in", + "sign_out": "Sign out", + "dashboard": "Dashboard", + "setup_guide": "Setup guide", + "query_log": "Query Log", + "filters": "Filters", + "settings": "Settings", + "general_settings": "General settings", + "dns_settings": "DNS settings", + "encryption_settings": "Encryption settings", + "client_settings": "Client settings", + "dhcp_settings": "DHCP settings", + "disable": "Disable", + "disabled": "Disabled", + "enable": "Enable", + "clear": "Clear", + "cancel": "Cancel", + + "login_password_title": "Reset Password", + "login_password_link": "Forgot password?", + "login_password_hash": "AdGuard Home stores passwords as a BCrypt-encoded hash. Here's what you need to do to change the password:", + "login_password_step_1": "Stop AdGuard Home", + "login_password_step_2": "Edit AdGuardHome.yaml", + "login_password_step_3": "Find password field there", + "login_password_step_4": "Replace it with the new value. You can use .htpasswd password generator tool or any online BCrypt generator tool (there are many available online).", + "login_password_step_5": "Start AdGuard Home", + "login_password_result": "Now you'll be able to log in to web interface using your new password.", + + "install_admin_interface_port_desc": "Now it is working at 3000 port, just in case, but we recomended to use 80 port. Using this ports allow to access to Web interface like to common site", + "install_admin_interface_port": "Which port will be used", + "install_admin_interface_title_decs": "Admin web interface is used to control AdGuard Home. You can open it in your browser and it does not require using a client-side program", + "install_admin_interface_title": "Admin interface settings", + "install_admin_interface_where_interface_desc": "Set what kind of networks will be able to access to Admin interface. For example: if you choose a local interface only, then Admin inteface will be accessed by this local device only", + "install_admin_interface_where_interface": "Where can I open Admin interface", + + "install_all_networks_description": "All available web interfaces", + "install_all_networks": "All networks", + "install_choose_networks_desc": "For advanced users", + "install_choose_networks": "Choose manually", + + "install_wellcome_button": "Let's go", + "install_wellcome_desc": "You have installed AdGuard Home on your device. It is a network-wide ad-and-tracker blocking DNS server with Admin Web interface. Let’s set some settings to correct DNS working", + "install_wellcome_title": "Welcome to AdGuard Home", + + "install_auth_title": "Login and password", + "install_auth_description": "Set login and password for accessing to Web interface", + + "install_dns_server_title": "DNS server settings", + "install_dns_server_desc": "AdGuard DNS server works like common DNS server but also blocks ads and tracking domains", + "install_dns_server_network_interfaces": "Network interfaces", + "install_dns_server_network_interfaces_desc": "You should set for what kind of networks will be use AdGuard Home DNS server. Most often you need to have available all interfaces", + "install_dns_server_port": "Which port will be used", + "install_dns_server_port_desc": "You have to use port 53 for correct internet working. Change this value only if you have reason", + "install_dns_server_non_static_ip": "How to use non-static IP adresses?", + + "install_configure_title": "Configure your devices", + "install_configure_danger_notice": "IMPORTANT! To start using AdGuard Home, you need to configure your devices manually", + "install_configure_how_to_title": "How to configure %value%", + "install_configure_router": "

    This setup will automatically cover all the devices connected to your home router and you will not need to configure each of them manually.

    Open the preferences for your router. Usually, you can access it from your browser via a URL (like http://192.168.0.1/ or http://192.168.1.1/). You may be asked to enter the password. If you don't remember it, you can often reset the password by pressing a button on the router itself. Some routers require a specific application, which in that case should be already installed on your computer/phone.

    Find the DHCP/DNS settings. Look for the DNS letters next to a field which allows two or three sets of numbers, each broken into four groups of one to three digits.

    Enter your AdGuard Home server addresses there.

    ", + "install_configure_windows": "

    Open Control Panel through Start menu or Windows search.

    Go to Network and Internet category and then to Network and Sharing Center.

    On the left side of the screen find Change adapter settings and click on it.

    Select your active connection, right-click on it and choose Properties.

    Find Internet Protocol Version 4 (TCP/IP) in the list, select it and then click on Properties again.

    Choose Use the following DNS server addresses and enter your AdGuard Home server addresses.

    ", + "install_configure_macos": "

    Click on Apple icon and go to System Preferences.

    Click on Network.

    Select the first connection in your list and click Advanced.

    Select the DNS tab and enter your AdGuard Home server addresses.

    ", + "install_configure_android": "

    From the Android Menu home screen, tap Settings.

    Tap Wi-Fi on the menu. The screen listing all of the available networks will be shown (it is impossible to set custom DNS for mobile connection).

    Long press the network you're connected to, and tap Modify Network.

    On some devices, you may need to check the box for Advanced to see further settings. To adjust your Android DNS settings, you will need to switch the IP settings from DHCP to Static.

    Change set DNS 1 and DNS 2 values to your AdGuard Home server addresses.

    ", + "install_configure_ios": "

    From the home screen, tap Settings.

    Choose Wi-Fi in the left menu (it is impossible to configure DNS for mobile networks).

    Tap on the name of the currently active network.

    In the DNS field enter your AdGuard Home server addresses.

    ", + "install_configure_adresses": "AdGuard Home addresses:", + "install_configure_dhcp": "You can't set a custom DNS server on some types of routers. In this case it may help if you set up AdGuard Home as a DHCP server. Otherwise, you should search for the manual on how to customize DNS servers for your particular router model.", + + "header_adguard_status_enabled": "AdGuard Home is enabled", + "header_adguard_status_disabled": "AdGuard Home is disabled", + "header_server_uptime": "Server uptime is %value%", + + "top_clients": "Top clients", + "client_table_header": "Client", + "requests": "Requests", + "show_blocked_responses": "Blocked", + + "filter_category_general": "General", + "query_log_configuration": "Logs configuration", + "statistics_configuration": "Statistics configuration", + "statistics_clear": " Clear statistics", + "interval_24_hour": "24 hours", + "interval_days": "| %count% day | %count% days", + "interval_hours": "| %count% hour | %count% hours", + "save_btn": "Save", + "stats_reset": "Statistics reseted succesfully", + "statistics_retention": "Statistics retention", + "statistics_retention_desc": "If you decrease the interval value, some data will be lost", + "query_log_enable": "Enable log", + "query_log_clear": "Clear query logs", + "query_log_retention": "Query logs retention", + "query_log_cleared": "The query log has been successfully cleared", + "anonymize_client_ip": "Anonymize client IP", + "anonymize_client_ip_desc": "Don't save the full IP address of the client in logs and statistics", + "query_log_retention_confirm": "If you decrease the interval value, some data will be lost", + "query_log_confirm_clear": "Are you sure you want to clear the entire query log?", + "query_log_reset": "Query log cleared succesfully", + "statistics_clear_confirm": "Are you sure you want to clear statistics?", + + "stats_query_domain": "Top queried domains", + "top_blocked_domains": "Top blocked domains", + "domain": "Domain", + "all_queries": "All queries", + + "block_domain_use_filters_and_hosts": "Block domains using filters and hosts files", + "filters_interval": "Filters update interval", + "filters_block_toggle_hint": "You can setup blocking rules in the Filters settings.", + "use_adguard_browsing_sec": "Use AdGuard browsing security web service", + "use_adguard_browsing_sec_hint": "AdGuard Home will check if domain is blacklisted by the browsing security web service. It will use privacy-friendly lookup API to perform the check: only a short prefix of the domain name SHA256 hash is sent to the server.", + "use_adguard_parental": "Use AdGuard parental control web service", + "use_adguard_parental_hint": "AdGuard Home will check if domain contains adult materials. It uses the same privacy-friendly API as the browsing security web service.", + "enforce_safe_search": "Enforce safe search", + "enforce_save_search_hint": "AdGuard Home can enforce safe search in the following search engines: Google, Youtube, Bing, DuckDuckGo, Yandex, Pixabay.", + + "dashboard_blocked_ads": "Blocked Ads", + "dashboard_blocked_trackers": "Blocked trackers", + "dashboard_filter_rules": "Count of filter rules", + "dashboard_blocked_queries": "Blocked queries", + "dashboard_filter_rules_count": "%enabled% of %all% filters", + "dashboard_server_statistics": "Internal server statistic", + "other" : "Other", + "ads" : "Ads", + "trackers" : "Trackers", + "stats_adult": "Blocked adult websites", + "stats_malware_phishing": "Blocked malware/phishing", + "average_processing_time": "Average processing time", + "milliseconds_abbreviation": "ms" + +} diff --git a/client2/src/localization/locales/index.ts b/client2/src/localization/locales/index.ts new file mode 100644 index 00000000..98266112 --- /dev/null +++ b/client2/src/localization/locales/index.ts @@ -0,0 +1,36 @@ +import 'dayjs/locale/ru'; + +import { PickerLocale } from 'antd/es/date-picker/generatePicker'; +import ruPicker from 'antd/es/date-picker/locale/ru_RU'; +import enPicker from 'antd/es/date-picker/locale/en_GB'; + +import ruLang from './ru.json'; +import enLang from './en.json'; + +export enum Locale { + en = 'en', + ru = 'ru', +} +export const DatePickerLocale: Record = { + [Locale.ru]: ruPicker, + [Locale.en]: enPicker, +}; + +export const messages: Record> = { + [Locale.ru]: ruLang, + [Locale.en]: enLang, +}; + +// TODO get languages and default locale from .twosky file +export const DEFAULT_LOCALE = Locale.en; + +export const LANGUAGES: { code: Locale; name: string }[] = [ + { + code: Locale.en, + name: 'English', + }, + { + code: Locale.ru, + name: 'Русский', + }, +]; diff --git a/client2/src/localization/locales/ru.json b/client2/src/localization/locales/ru.json new file mode 100644 index 00000000..af53fc02 --- /dev/null +++ b/client2/src/localization/locales/ru.json @@ -0,0 +1,4 @@ +{ + "install_wellcome_title": "Добро пожаловать в AdGuard Home", + "install_wellcome_desc": "Русский текст" +} \ No newline at end of file diff --git a/client2/src/main.pcss b/client2/src/main.pcss new file mode 100644 index 00000000..9d7573ba --- /dev/null +++ b/client2/src/main.pcss @@ -0,0 +1,31 @@ +:root { + --white: #ffffff; + --gray100: #f3f3f3; + --gray300: #d8d8d8; + --gray400: #a4a4a4; + --gray700: #888888; + --gray900: #4d4d4d; + --black: #131313; + --green400: #67b279; + --green700: #4d995f; + --red400: #c23814; + --text-selection: #e7efff; + --header-height: 48px; + --transition: 0.3s cubic-bezier(0.645, 0.045, 0.355, 1); + --font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Helvetica Neue", Arial, sans-serif; + --font-family-monospace: Monaco, Menlo, "Ubuntu Mono", Consolas, source-code-pro, monospace; +} + +body { + font-size: 16px; + color: var(--gray900); +} + +::selection { + background: var(--text-selection); + color: var(--black); +} + +@custom-media --m-viewport (min-width: 768px); +@custom-media --l-viewport (min-width: 992px); +@custom-media --xl-viewport (min-width: 1200px); diff --git a/client2/src/store/index.ts b/client2/src/store/index.ts new file mode 100644 index 00000000..a1c7b93b --- /dev/null +++ b/client2/src/store/index.ts @@ -0,0 +1 @@ +export { default, Store, storeValue } from './store'; diff --git a/client2/src/store/installStore.ts b/client2/src/store/installStore.ts new file mode 100644 index 00000000..dfe7bfd7 --- /dev/null +++ b/client2/src/store/installStore.ts @@ -0,0 +1,19 @@ +import { createContext } from 'react'; +import Install from './stores/Install'; +import UI from './stores/ui'; + +export class Store { + ui: UI; + + install: Install; + + constructor() { + this.ui = new UI(this); + this.install = new Install(this); + } +} + +export const storeValue = new Store(); + +const StoreContext = createContext(storeValue); +export default StoreContext; diff --git a/client2/src/store/store.ts b/client2/src/store/store.ts new file mode 100644 index 00000000..4debd161 --- /dev/null +++ b/client2/src/store/store.ts @@ -0,0 +1,36 @@ +import { createContext } from 'react'; +import UI from './stores/ui'; +import Login from './stores/Login'; +import Dashboard from './stores/Dasnboard'; +import System from './stores/System'; +import GeneralSettings from './stores/GeneralSettings'; + +export class Store { + ui: UI; + + login: Login; + + dashboard: Dashboard; + + system: System; + + generalSettings: GeneralSettings; + + constructor() { + this.ui = new UI(this); + this.login = new Login(this); + this.dashboard = new Dashboard(this); + this.system = new System(this); + this.generalSettings = new GeneralSettings(this); + } + + init() { + this.dashboard.init(); + this.system.init(); + } +} + +export const storeValue = new Store(); + +const StoreContext = createContext(storeValue); +export default StoreContext; diff --git a/client2/src/store/stores/Dasnboard.ts b/client2/src/store/stores/Dasnboard.ts new file mode 100644 index 00000000..c4aa3062 --- /dev/null +++ b/client2/src/store/stores/Dasnboard.ts @@ -0,0 +1,120 @@ +import { flow, makeAutoObservable, observable } from 'mobx'; + +import clientsApi from 'Apis/clients'; +import statsApi from 'Apis/stats'; +import filteringApi from 'Apis/filtering'; +import tlsApi from 'Apis/tls'; + +import { errorChecker } from 'Helpers/apiErrors'; +import { Store } from 'Store'; +import Stats, { IStats } from 'Entities/Stats'; +import StatsConfig, { IStatsConfig } from 'Entities/StatsConfig'; +import TlsConfig, { ITlsConfig } from 'Entities/TlsConfig'; +import { IClientsFindEntry } from 'Entities/ClientsFindEntry'; +import ClientFindSubEntry from 'Entities/ClientFindSubEntry'; +import FilterStatus, { IFilterStatus } from 'Entities/FilterStatus'; + +import { IStore } from './utils'; + +export default class Dashboard implements IStore { + rootStore: Store; + + inited = false; + + stats: Stats | undefined; + + statsConfig: StatsConfig | undefined; + + clientsInfo: Map; + + tlsConfig: TlsConfig | undefined; + + filteringConfig: FilterStatus | undefined; + + constructor(rootStore: Store) { + this.rootStore = rootStore; + makeAutoObservable(this, { + rootStore: false, + inited: observable, + init: flow, + + getStatsConfig: flow, + getTlsConfig: flow, + getClient: flow, + filteringStatus: flow, + + stats: observable.ref, + statsConfig: observable.ref, + clientsInfo: observable.ref, + tlsConfig: observable.ref, + filteringConfig: observable.ref, + }); + this.clientsInfo = new Map(); + if (this.rootStore.login.loggedIn) { + this.init(); + } + } + + * init() { + yield this.getStatsConfig(); + yield this.getTlsConfig(); + yield this.getStats(); + yield this.filteringStatus(); + this.inited = true; + } + + * getStats() { + const response = yield statsApi.stats(); + const { result } = errorChecker(response); + if (result) { + this.stats = new Stats(result); + if (this.stats.topClients) { + // TODO: fix bycicle + const topClients = this.stats.topClients.map((e) => { + return Object.keys(e.numberData)[0]; + }); + let firstClient = topClients.shift(); + firstClient += '&'; + const topClientsReq = firstClient + topClients.map((ip, index) => `ip${index + 1}=${ip}`).join('&'); + yield this.getClient(topClientsReq); + } + } + } + + * getClient(ip: string) { + // if & is encoding set in clientsFind qs options - encode: false + const response = yield clientsApi.clientsFind(ip); + const { result } = errorChecker(response); + if (result) { + this.clientsInfo = new Map(); + result.forEach((client) => { + const [clientIp, data] = Object.entries(client)[0]; + this.clientsInfo.set(clientIp, new ClientFindSubEntry(data)); + }); + } + } + + * getStatsConfig() { + const response = yield statsApi.statsInfo(); + const { result } = errorChecker(response); + if (result) { + this.statsConfig = new StatsConfig(result); + } + } + + * getTlsConfig() { + const response = yield tlsApi.tlsStatus(); + const { result } = errorChecker(response); + if (result) { + this.tlsConfig = new TlsConfig(result); + } + } + + * filteringStatus() { + const response = yield filteringApi.filteringStatus(); + const { result } = errorChecker(response); + if (result) { + this.filteringConfig = new FilterStatus(result); + } + } +} diff --git a/client2/src/store/stores/GeneralSettings.ts b/client2/src/store/stores/GeneralSettings.ts new file mode 100644 index 00000000..6b5e8287 --- /dev/null +++ b/client2/src/store/stores/GeneralSettings.ts @@ -0,0 +1,218 @@ +import { flow, makeAutoObservable, observable } from 'mobx'; +import { Store } from 'Store'; + +import statsApi from 'Apis/stats'; +import queryApi from 'Apis/log'; +import safeBrowsingApi from 'Apis/safebrowsing'; +import filteringApi from 'Apis/filtering'; +import parentalApi from 'Apis/parental'; +import safesearchApi from 'Apis/safesearch'; + +import StatsConfig, { IStatsConfig } from 'Entities/StatsConfig'; +import QueryLogConfig, { IQueryLogConfig } from 'Entities/QueryLogConfig'; +import FilterConfig, { IFilterConfig } from 'Entities/FilterConfig'; +import FilterStatus, { IFilterStatus } from 'Entities/FilterStatus'; + +import { errorChecker } from 'Helpers/apiErrors'; + +import { IStore } from './utils'; + +export default class SomeStore implements IStore { + rootStore: Store; + + inited = false; + + statsConfig: StatsConfig | undefined; + + queryLogConfig: QueryLogConfig | undefined; + + safebrowsing: boolean | undefined; + + filteringConfig: FilterConfig | undefined; + + parental: boolean | undefined; + + safesearch: boolean | undefined; + + constructor(rootStore: Store) { + this.rootStore = rootStore; + makeAutoObservable(this, { + rootStore: false, + inited: observable, + init: flow, + + statsConfig: observable.ref, + queryLogConfig: observable.ref, + safebrowsing: observable, + filteringConfig: observable.ref, + parental: observable, + safesearch: observable, + + updateStatsConfig: flow, + statsInfo: flow, + statsReset: flow, + updateQueryLogConfig: flow, + queryLogInfo: flow, + querylogClear: flow, + safebrowsingDisable: flow, + safebrowsingEnable: flow, + safebrowsingStatus: flow, + updateFilteringConfig: flow, + filteringStatus: flow, + parentalDisable: flow, + parentalEnable: flow, + parentalStatus: flow, + safesearchDisable: flow, + safesearchEnable: flow, + safesearchStatus: flow, + }); + } + + * init() { + yield this.statsInfo(); + yield this.queryLogInfo(); + yield this.safebrowsingStatus(); + yield this.filteringStatus(); + yield this.parentalStatus(); + yield this.safesearchStatus(); + this.inited = yield true; + } + + * updateStatsConfig(statsconfig: IStatsConfig) { + const response = yield statsApi.statsConfig(statsconfig); + const { result } = errorChecker(response); + if (result) { + yield this.statsInfo(); + } + } + + * statsInfo() { + const response = yield statsApi.statsInfo(); + const { result } = errorChecker(response); + if (result) { + this.statsConfig = new StatsConfig(result); + } + } + + * statsReset() { + const response = yield statsApi.statsReset(); + const { result } = errorChecker(response); + if (result) { + yield this.statsInfo(); + return true; + } + } + + * updateQueryLogConfig(querylogconfig: IQueryLogConfig) { + const response = yield queryApi.queryLogConfig(querylogconfig); + const { result } = errorChecker(response); + if (result) { + yield this.queryLogInfo(); + } + } + + * queryLogInfo() { + const response = yield queryApi.queryLogInfo(); + const { result } = errorChecker(response); + if (result) { + this.queryLogConfig = new QueryLogConfig(result); + } + } + + * querylogClear() { + const response = yield queryApi.querylogClear(); + const { result } = errorChecker(response); + if (result) { + yield this.queryLogInfo(); + } + } + + * safebrowsingDisable() { + const response = yield safeBrowsingApi.safebrowsingDisable(); + const { result } = errorChecker(response); + if (result) { + this.safebrowsing = false; + } + } + + * safebrowsingEnable() { + const response = yield safeBrowsingApi.safebrowsingEnable(); + const { result } = errorChecker(response); + if (result) { + this.safebrowsing = true; + } + } + + * safebrowsingStatus() { + const response = yield safeBrowsingApi.safebrowsingStatus(); + const { result } = errorChecker(response); + if (result) { + this.safebrowsing = result.enabled; + } + } + + * updateFilteringConfig(filterconfig: IFilterConfig) { + const response = yield filteringApi.filteringConfig(filterconfig); + const { result } = errorChecker(response); + if (result) { + yield this.filteringStatus(); + } + } + + * filteringStatus() { + const response = yield filteringApi.filteringStatus(); + const { result } = errorChecker(response); + if (result) { + this.filteringConfig = new FilterStatus(result); + } + } + + * parentalDisable() { + const response = yield parentalApi.parentalDisable(); + const { result } = errorChecker(response); + if (result) { + this.parental = false; + } + } + + * parentalEnable() { + // TODO: remove magic; + const response = yield parentalApi.parentalEnable('sensitivity=TEEN'); + const { result } = errorChecker(response); + if (result) { + this.parental = true; + } + } + + * parentalStatus() { + const response = yield parentalApi.parentalStatus(); + const { result } = errorChecker(response); + if (result) { + this.parental = result.enabled; + } + } + + * safesearchDisable() { + const response = yield safesearchApi.safesearchDisable(); + const { result } = errorChecker(response); + if (result) { + this.safesearch = false; + } + } + + * safesearchEnable() { + const response = yield safesearchApi.safesearchEnable(); + const { result } = errorChecker(response); + if (result) { + this.safesearch = true; + } + } + + * safesearchStatus() { + const response = yield safesearchApi.safesearchStatus(); + const { result } = errorChecker(response); + if (result) { + this.safesearch = result.enabled; + } + } +} diff --git a/client2/src/store/stores/Install.ts b/client2/src/store/stores/Install.ts new file mode 100644 index 00000000..92b2ff06 --- /dev/null +++ b/client2/src/store/stores/Install.ts @@ -0,0 +1,50 @@ +import InstallApi from 'Apis/install'; +import AddressesInfoBeta, { IAddressesInfoBeta } from 'Entities/AddressesInfoBeta'; +import { ICheckConfigRequestBeta } from 'Entities/CheckConfigRequestBeta'; +import CheckConfigResponse, { ICheckConfigResponse } from 'Entities/CheckConfigResponse'; +import { IInitialConfigurationBeta } from 'Entities/InitialConfigurationBeta'; +import { errorChecker } from 'Helpers/apiErrors'; +import { flow, makeAutoObservable } from 'mobx'; + +import { Store } from 'Store/installStore'; + +export default class Install { + rootStore: Store; + + addresses: AddressesInfoBeta | null; + + constructor(rootStore: Store) { + this.rootStore = rootStore; + this.addresses = null; + + makeAutoObservable(this, { + rootStore: false, + getAddresses: flow, + }); + this.getAddresses(); + } + + * getAddresses() { + const response = yield InstallApi.installGetAddressesBeta(); + const { result } = errorChecker(response); + if (result) { + this.addresses = new AddressesInfoBeta(result); + } + } + + static async checkConfig(config: ICheckConfigRequestBeta) { + const response = await InstallApi.installCheckConfigBeta(config); + const { result } = errorChecker(response); + if (result) { + return new CheckConfigResponse(result); + } + } + + static async configure(config: IInitialConfigurationBeta) { + const response = await InstallApi.installConfigureBeta(config); + const { result } = errorChecker(response); + if (result) { + return true; + } + } +} diff --git a/client2/src/store/stores/Login.ts b/client2/src/store/stores/Login.ts new file mode 100644 index 00000000..b1534731 --- /dev/null +++ b/client2/src/store/stores/Login.ts @@ -0,0 +1,45 @@ +import { flow, makeAutoObservable, observable } from 'mobx'; +import globalApi from 'Apis/global'; + +import { Store } from 'Store'; +import { errorChecker } from 'Helpers/apiErrors'; +import ProfileInfo, { IProfileInfo } from 'Entities/ProfileInfo'; +import { ILogin } from 'Entities/Login'; + +export default class Login { + rootStore: Store; + + loggedIn = false; + + constructor(rootStore: Store) { + this.rootStore = rootStore; + makeAutoObservable(this, { + loggedIn: observable, + rootStore: false, + checkLoggedIn: flow, + login: flow, + }); + this.checkLoggedIn(); + } + + * checkLoggedIn() { + const response = yield globalApi.getProfile(); + const { result } = errorChecker(response); + if (result) { + this.loggedIn = true; + this.rootStore.system.setProfile(new ProfileInfo(result)); + this.rootStore.init(); + } + // TODO: make smth with result, to not duplicate the request; + } + + * login(login: ILogin) { + const response = yield globalApi.login(login); + const { result, error } = errorChecker(response); + if (result === 200) { + this.loggedIn = true; + return; + } + return error; + } +} diff --git a/client2/src/store/stores/System.ts b/client2/src/store/stores/System.ts new file mode 100644 index 00000000..41652998 --- /dev/null +++ b/client2/src/store/stores/System.ts @@ -0,0 +1,75 @@ +import { flow, makeAutoObservable, observable, action } from 'mobx'; +import globalApi from 'Apis/global'; + +import { Store } from 'Store'; +import { errorChecker } from 'Helpers/apiErrors'; +import ProfileInfo, { IProfileInfo } from 'Entities/ProfileInfo'; +import ServerStatus, { IServerStatus } from 'Entities/ServerStatus'; + +import { IStore } from './utils'; + +export default class System implements IStore { + rootStore: Store; + + inited = false; + + status: ServerStatus | undefined; + + profile: ProfileInfo | undefined; + + constructor(rootStore: Store) { + this.rootStore = rootStore; + makeAutoObservable(this, { + rootStore: false, + inited: observable, + getServerStatus: flow, + init: flow, + setProfile: action, + switchServerStatus: flow, + getProfile: flow, + status: observable, + profile: observable, + }); + if (this.rootStore.login.loggedIn) { + this.init(); + } + } + + * init() { + yield this.getServerStatus(); + if (!this.profile) { + yield this.getProfile(); + } + this.inited = true; + } + + setProfile(profile: ProfileInfo) { + this.profile = profile; + } + + * getProfile() { + const response = yield globalApi.getProfile(); + const { result } = errorChecker(response); + if (result) { + this.profile = new ProfileInfo(result); + } + } + + * getServerStatus() { + const response = yield globalApi.status(); + const { result } = errorChecker(response); + if (result) { + this.status = new ServerStatus(result); + } + } + + * switchServerStatus(enable: boolean) { + const response = yield globalApi.dnsConfig({ + protection_enabled: enable, + }); + const { result } = errorChecker(response); + if (result) { + yield this.getServerStatus(); + } + } +} diff --git a/client2/src/store/stores/ui.ts b/client2/src/store/stores/ui.ts new file mode 100644 index 00000000..c0d0377e --- /dev/null +++ b/client2/src/store/stores/ui.ts @@ -0,0 +1,36 @@ +import React from 'react'; +import { makeAutoObservable, observable, action } from 'mobx'; +import { translate } from '@adguard/translate'; + +import { Locale, DEFAULT_LOCALE, i18n } from 'Localization'; +import { Store } from 'Store'; +import { Store as InstallStore } from 'Store/installStore'; + +export default class UI { + rootStore: Store | InstallStore; + + currentLang = DEFAULT_LOCALE; + + intl = translate.createReactTranslator(i18n(this.currentLang), React); + + sidebarOpen = false; + + constructor(rootStore: Store | InstallStore) { + this.rootStore = rootStore; + makeAutoObservable(this, { + intl: observable.struct, + rootStore: false, + sidebarOpen: observable, + toggleSidebar: action, + }); + } + + updateLang = (lang: Locale) => { + this.currentLang = lang; + this.intl = translate.createReactTranslator(i18n(this.currentLang), React); + }; + + toggleSidebar = () => { + this.sidebarOpen = !this.sidebarOpen; + }; +} diff --git a/client2/src/store/stores/utils.ts b/client2/src/store/stores/utils.ts new file mode 100644 index 00000000..df679d58 --- /dev/null +++ b/client2/src/store/stores/utils.ts @@ -0,0 +1,38 @@ +import { Store } from 'Store'; + +export interface IStore { + rootStore: Store; + init: () => void; + inited: boolean; +} +/* +Each store should implement IStore to work properly if user not loggged in +and after log in like: + +import { flow, makeAutoObservable, observable } from 'mobx'; +import { Store } from 'Store'; +import { IStore } from './utils'; + +export default class SomeStore implements IStore { + rootStore: Store; + + inited = false; + + constructor(rootStore: Store) { + this.rootStore = rootStore; + makeAutoObservable(this, { + rootStore: false, + inited: observable, + init: flow, + }); + if (this.rootStore.login.loggedIn) { + this.init(); + } + } + + * init() { + this.inited = true; + } +} + +*/ diff --git a/client2/tsconfig.json b/client2/tsconfig.json new file mode 100644 index 00000000..fa3398ad --- /dev/null +++ b/client2/tsconfig.json @@ -0,0 +1,40 @@ +{ + "compilerOptions": { + "allowSyntheticDefaultImports": true, + "alwaysStrict": true, + "target": "es6", + "module": "ESNext", + "moduleResolution": "node", + "noResolve": false, + "noImplicitAny": true, + "strict": true, + "removeComments": true, + "sourceMap": true, + "jsx": "react", + "resolveJsonModule": true, + "baseUrl": "src", + "paths": { + "Apis/*": ["lib/apis/*"], + "Common": ["components/common/index"], + "Common/*": ["components/common/*"], + "Components/*": ["components/*"], + "Consts/*": ["lib/consts/*"], + "Entities/*": ["lib/entities/*"], + "Hooks": ["lib/hooks"], + "Helpers/*": ["lib/helpers/*"], + "Lib/*": ["lib/*"], + "Localization": ["localization/index"], + "Paths": ["components/App/Routes/Paths"], + "Store": ["store/store"], + "Store/*": ["store/*"] + }, + "types": [ + "react", + "react-dom", + "node" + ], + "useDefineForClassFields": true + }, + "include": ["src/**/*", "./declaration.d.ts"], + "exclude": ["./node_modules/**/*", "./scripts/generator/**/*"] +} diff --git a/client2/yarn.lock b/client2/yarn.lock new file mode 100644 index 00000000..d171bb54 --- /dev/null +++ b/client2/yarn.lock @@ -0,0 +1,8554 @@ +# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. +# yarn lockfile v1 + + +"@adguard/translate@^0.2.0": + version "0.2.0" + resolved "https://registry.yarnpkg.com/@adguard/translate/-/translate-0.2.0.tgz#6b74b037167ec0cae32e6b7423cb35eb1b08a1b3" + integrity sha512-zvpaEKMABcCCuEr7WpGRGgfdzp8L0OMuM0KUI7uwWapX+/i75ifffbWnKxi/LUdZDJu8kJPnmLN/DD5hngZuQQ== + +"@ant-design/colors@^5.0.0": + version "5.0.1" + resolved "https://registry.yarnpkg.com/@ant-design/colors/-/colors-5.0.1.tgz#09670f2f44a7473d7bc01be901c48ec10f12c7a4" + integrity sha512-x1TUaRILaqy3zgFNo+kIqOa3eTYPt81H1/3E4dCjDP4Qvk/xaPEizLDFdRUcIx0cWwyu2LklwfyLHWpbYK8v6A== + dependencies: + "@ctrl/tinycolor" "^3.3.1" + +"@ant-design/icons-svg@^4.0.0": + version "4.1.0" + resolved "https://registry.yarnpkg.com/@ant-design/icons-svg/-/icons-svg-4.1.0.tgz#480b025f4b20ef7fe8f47d4a4846e4fee84ea06c" + integrity sha512-Fi03PfuUqRs76aI3UWYpP864lkrfPo0hluwGqh7NJdLhvH4iRDc3jbJqZIvRDLHKbXrvAfPPV3+zjUccfFvWOQ== + +"@ant-design/icons@^4.4.0": + version "4.4.0" + resolved "https://registry.yarnpkg.com/@ant-design/icons/-/icons-4.4.0.tgz#d4e4ba5910454e1d3f67a802d2aad9ee75a51dea" + integrity sha512-+X44IouK56JbP3r7zM+Zoykv5wQlXBlxY0NTaFXGpiyYSS/Bh6HIo9aTF62QkSuDTqA3UpeNVTRFioKKRmkWDQ== + dependencies: + "@ant-design/colors" "^5.0.0" + "@ant-design/icons-svg" "^4.0.0" + "@babel/runtime" "^7.11.2" + classnames "^2.2.6" + insert-css "^2.0.0" + rc-util "^5.0.1" + +"@ant-design/react-slick@~0.28.1": + version "0.28.1" + resolved "https://registry.yarnpkg.com/@ant-design/react-slick/-/react-slick-0.28.1.tgz#2e0720838cb57ab8818384dcc96b2a8c61fcd01e" + integrity sha512-Uk+GNexHOmiK3BMk/xvliNsNt+LYnN49u5o4lqeuMKXJlNqE9kGpEF03KpxDqu/zybO0/0yAJALha8oPtR5iHA== + dependencies: + "@babel/runtime" "^7.10.4" + classnames "^2.2.5" + json2mq "^0.2.0" + lodash "^4.17.15" + resize-observer-polyfill "^1.5.0" + +"@babel/code-frame@^7.0.0", "@babel/code-frame@^7.10.4", "@babel/code-frame@^7.12.11": + version "7.12.11" + resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.12.11.tgz#f4ad435aa263db935b8f10f2c552d23fb716a63f" + integrity sha512-Zt1yodBx1UcyiePMSkWnU4hPqhwq7hGi2nFL1LeA3EUl+q2LQx16MISgJ0+z7dnmgvP9QtIleuETGOiOH1RcIw== + dependencies: + "@babel/highlight" "^7.10.4" + +"@babel/core@>=7.9.0": + version "7.12.10" + resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.12.10.tgz#b79a2e1b9f70ed3d84bbfb6d8c4ef825f606bccd" + integrity sha512-eTAlQKq65zHfkHZV0sIVODCPGVgoo1HdBlbSLi9CqOzuZanMv2ihzY+4paiKr1mH+XmYESMAmJ/dpZ68eN6d8w== + dependencies: + "@babel/code-frame" "^7.10.4" + "@babel/generator" "^7.12.10" + "@babel/helper-module-transforms" "^7.12.1" + "@babel/helpers" "^7.12.5" + "@babel/parser" "^7.12.10" + "@babel/template" "^7.12.7" + "@babel/traverse" "^7.12.10" + "@babel/types" "^7.12.10" + convert-source-map "^1.7.0" + debug "^4.1.0" + gensync "^1.0.0-beta.1" + json5 "^2.1.2" + lodash "^4.17.19" + semver "^5.4.1" + source-map "^0.5.0" + +"@babel/generator@^7.12.10", "@babel/generator@^7.12.11": + version "7.12.11" + resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.12.11.tgz#98a7df7b8c358c9a37ab07a24056853016aba3af" + integrity sha512-Ggg6WPOJtSi8yYQvLVjG8F/TlpWDlKx0OpS4Kt+xMQPs5OaGYWy+v1A+1TvxI6sAMGZpKWWoAQ1DaeQbImlItA== + dependencies: + "@babel/types" "^7.12.11" + jsesc "^2.5.1" + source-map "^0.5.0" + +"@babel/helper-function-name@^7.12.11": + version "7.12.11" + resolved "https://registry.yarnpkg.com/@babel/helper-function-name/-/helper-function-name-7.12.11.tgz#1fd7738aee5dcf53c3ecff24f1da9c511ec47b42" + integrity sha512-AtQKjtYNolKNi6nNNVLQ27CP6D9oFR6bq/HPYSizlzbp7uC1M59XJe8L+0uXjbIaZaUJF99ruHqVGiKXU/7ybA== + dependencies: + "@babel/helper-get-function-arity" "^7.12.10" + "@babel/template" "^7.12.7" + "@babel/types" "^7.12.11" + +"@babel/helper-get-function-arity@^7.12.10": + version "7.12.10" + resolved "https://registry.yarnpkg.com/@babel/helper-get-function-arity/-/helper-get-function-arity-7.12.10.tgz#b158817a3165b5faa2047825dfa61970ddcc16cf" + integrity sha512-mm0n5BPjR06wh9mPQaDdXWDoll/j5UpCAPl1x8fS71GHm7HA6Ua2V4ylG1Ju8lvcTOietbPNNPaSilKj+pj+Ag== + dependencies: + "@babel/types" "^7.12.10" + +"@babel/helper-member-expression-to-functions@^7.12.7": + version "7.12.7" + resolved "https://registry.yarnpkg.com/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.12.7.tgz#aa77bd0396ec8114e5e30787efa78599d874a855" + integrity sha512-DCsuPyeWxeHgh1Dus7APn7iza42i/qXqiFPWyBDdOFtvS581JQePsc1F/nD+fHrcswhLlRc2UpYS1NwERxZhHw== + dependencies: + "@babel/types" "^7.12.7" + +"@babel/helper-module-imports@^7.12.1": + version "7.12.5" + resolved "https://registry.yarnpkg.com/@babel/helper-module-imports/-/helper-module-imports-7.12.5.tgz#1bfc0229f794988f76ed0a4d4e90860850b54dfb" + integrity sha512-SR713Ogqg6++uexFRORf/+nPXMmWIn80TALu0uaFb+iQIUoR7bOC7zBWyzBs5b3tBBJXuyD0cRu1F15GyzjOWA== + dependencies: + "@babel/types" "^7.12.5" + +"@babel/helper-module-transforms@^7.12.1": + version "7.12.1" + resolved "https://registry.yarnpkg.com/@babel/helper-module-transforms/-/helper-module-transforms-7.12.1.tgz#7954fec71f5b32c48e4b303b437c34453fd7247c" + integrity sha512-QQzehgFAZ2bbISiCpmVGfiGux8YVFXQ0abBic2Envhej22DVXV9nCFaS5hIQbkyo1AdGb+gNME2TSh3hYJVV/w== + dependencies: + "@babel/helper-module-imports" "^7.12.1" + "@babel/helper-replace-supers" "^7.12.1" + "@babel/helper-simple-access" "^7.12.1" + "@babel/helper-split-export-declaration" "^7.11.0" + "@babel/helper-validator-identifier" "^7.10.4" + "@babel/template" "^7.10.4" + "@babel/traverse" "^7.12.1" + "@babel/types" "^7.12.1" + lodash "^4.17.19" + +"@babel/helper-optimise-call-expression@^7.12.10": + version "7.12.10" + resolved "https://registry.yarnpkg.com/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.12.10.tgz#94ca4e306ee11a7dd6e9f42823e2ac6b49881e2d" + integrity sha512-4tpbU0SrSTjjt65UMWSrUOPZTsgvPgGG4S8QSTNHacKzpS51IVWGDj0yCwyeZND/i+LSN2g/O63jEXEWm49sYQ== + dependencies: + "@babel/types" "^7.12.10" + +"@babel/helper-replace-supers@^7.12.1": + version "7.12.11" + resolved "https://registry.yarnpkg.com/@babel/helper-replace-supers/-/helper-replace-supers-7.12.11.tgz#ea511658fc66c7908f923106dd88e08d1997d60d" + integrity sha512-q+w1cqmhL7R0FNzth/PLLp2N+scXEK/L2AHbXUyydxp828F4FEa5WcVoqui9vFRiHDQErj9Zof8azP32uGVTRA== + dependencies: + "@babel/helper-member-expression-to-functions" "^7.12.7" + "@babel/helper-optimise-call-expression" "^7.12.10" + "@babel/traverse" "^7.12.10" + "@babel/types" "^7.12.11" + +"@babel/helper-simple-access@^7.12.1": + version "7.12.1" + resolved "https://registry.yarnpkg.com/@babel/helper-simple-access/-/helper-simple-access-7.12.1.tgz#32427e5aa61547d38eb1e6eaf5fd1426fdad9136" + integrity sha512-OxBp7pMrjVewSSC8fXDFrHrBcJATOOFssZwv16F3/6Xtc138GHybBfPbm9kfiqQHKhYQrlamWILwlDCeyMFEaA== + dependencies: + "@babel/types" "^7.12.1" + +"@babel/helper-split-export-declaration@^7.11.0", "@babel/helper-split-export-declaration@^7.12.11": + version "7.12.11" + resolved "https://registry.yarnpkg.com/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.12.11.tgz#1b4cc424458643c47d37022223da33d76ea4603a" + integrity sha512-LsIVN8j48gHgwzfocYUSkO/hjYAOJqlpJEc7tGXcIm4cubjVUf8LGW6eWRyxEu7gA25q02p0rQUWoCI33HNS5g== + dependencies: + "@babel/types" "^7.12.11" + +"@babel/helper-validator-identifier@^7.10.4", "@babel/helper-validator-identifier@^7.12.11": + version "7.12.11" + resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.12.11.tgz#c9a1f021917dcb5ccf0d4e453e399022981fc9ed" + integrity sha512-np/lG3uARFybkoHokJUmf1QfEvRVCPbmQeUQpKow5cQ3xWrV9i3rUHodKDJPQfTVX61qKi+UdYk8kik84n7XOw== + +"@babel/helpers@^7.12.5": + version "7.12.5" + resolved "https://registry.yarnpkg.com/@babel/helpers/-/helpers-7.12.5.tgz#1a1ba4a768d9b58310eda516c449913fe647116e" + integrity sha512-lgKGMQlKqA8meJqKsW6rUnc4MdUk35Ln0ATDqdM1a/UpARODdI4j5Y5lVfUScnSNkJcdCRAaWkspykNoFg9sJA== + dependencies: + "@babel/template" "^7.10.4" + "@babel/traverse" "^7.12.5" + "@babel/types" "^7.12.5" + +"@babel/highlight@^7.10.4": + version "7.10.4" + resolved "https://registry.yarnpkg.com/@babel/highlight/-/highlight-7.10.4.tgz#7d1bdfd65753538fabe6c38596cdb76d9ac60143" + integrity sha512-i6rgnR/YgPEQzZZnbTHHuZdlE8qyoBNalD6F+q4vAFlcMEcqmkoG+mPqJYJCo63qPf74+Y1UZsl3l6f7/RIkmA== + dependencies: + "@babel/helper-validator-identifier" "^7.10.4" + chalk "^2.0.0" + js-tokens "^4.0.0" + +"@babel/parser@^7.12.10", "@babel/parser@^7.12.11", "@babel/parser@^7.12.7": + version "7.12.11" + resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.12.11.tgz#9ce3595bcd74bc5c466905e86c535b8b25011e79" + integrity sha512-N3UxG+uuF4CMYoNj8AhnbAcJF0PiuJ9KHuy1lQmkYsxTer/MAH9UBNHsBoAX/4s6NvlDD047No8mYVGGzLL4hg== + +"@babel/runtime@^7.1.2", "@babel/runtime@^7.10.1", "@babel/runtime@^7.10.2", "@babel/runtime@^7.10.4", "@babel/runtime@^7.11.1", "@babel/runtime@^7.11.2", "@babel/runtime@^7.12.1", "@babel/runtime@^7.12.5", "@babel/runtime@^7.8.4": + version "7.12.5" + resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.12.5.tgz#410e7e487441e1b360c29be715d870d9b985882e" + integrity sha512-plcc+hbExy3McchJCEQG3knOsuh3HH+Prx1P6cLIkET/0dLuQDEnrT+s27Axgc9bqfsmNUNHfscgMUdBpC9xfg== + dependencies: + regenerator-runtime "^0.13.4" + +"@babel/template@^7.10.4", "@babel/template@^7.12.7": + version "7.12.7" + resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.12.7.tgz#c817233696018e39fbb6c491d2fb684e05ed43bc" + integrity sha512-GkDzmHS6GV7ZeXfJZ0tLRBhZcMcY0/Lnb+eEbXDBfCAcZCjrZKe6p3J4we/D24O9Y8enxWAg1cWwof59yLh2ow== + dependencies: + "@babel/code-frame" "^7.10.4" + "@babel/parser" "^7.12.7" + "@babel/types" "^7.12.7" + +"@babel/traverse@^7.12.1", "@babel/traverse@^7.12.10", "@babel/traverse@^7.12.5": + version "7.12.12" + resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.12.12.tgz#d0cd87892704edd8da002d674bc811ce64743376" + integrity sha512-s88i0X0lPy45RrLM8b9mz8RPH5FqO9G9p7ti59cToE44xFm1Q+Pjh5Gq4SXBbtb88X7Uy7pexeqRIQDDMNkL0w== + dependencies: + "@babel/code-frame" "^7.12.11" + "@babel/generator" "^7.12.11" + "@babel/helper-function-name" "^7.12.11" + "@babel/helper-split-export-declaration" "^7.12.11" + "@babel/parser" "^7.12.11" + "@babel/types" "^7.12.12" + debug "^4.1.0" + globals "^11.1.0" + lodash "^4.17.19" + +"@babel/types@^7.12.1", "@babel/types@^7.12.10", "@babel/types@^7.12.11", "@babel/types@^7.12.12", "@babel/types@^7.12.5", "@babel/types@^7.12.7": + version "7.12.12" + resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.12.12.tgz#4608a6ec313abbd87afa55004d373ad04a96c299" + integrity sha512-lnIX7piTxOH22xE7fDXDbSHg9MM1/6ORnafpJmov5rs0kX5g4BZxeXNJLXsMRiO0U5Rb8/FvMS6xlTnTHvxonQ== + dependencies: + "@babel/helper-validator-identifier" "^7.12.11" + lodash "^4.17.19" + to-fast-properties "^2.0.0" + +"@csstools/convert-colors@^1.4.0": + version "1.4.0" + resolved "https://registry.yarnpkg.com/@csstools/convert-colors/-/convert-colors-1.4.0.tgz#ad495dc41b12e75d588c6db8b9834f08fa131eb7" + integrity sha512-5a6wqoJV/xEdbRNKVo6I4hO3VjyDq//8q2f9I6PBAvMesJHFauXDorcNCsr9RzvsZnaWi5NYCcfyqP1QeFHFbw== + +"@ctrl/tinycolor@^3.3.1": + version "3.3.3" + resolved "https://registry.yarnpkg.com/@ctrl/tinycolor/-/tinycolor-3.3.3.tgz#980487763bc7c9238d6d88d1ac0dee2d4df3df68" + integrity sha512-v75yutF4BDMv9weDQVM+K5XEfjiODhugSV729pnoxtBDO61ij2CsDnQa4N4E9xGaH3/FX5ASZjnajljT2F71tA== + +"@discoveryjs/json-ext@^0.5.0": + version "0.5.2" + resolved "https://registry.yarnpkg.com/@discoveryjs/json-ext/-/json-ext-0.5.2.tgz#8f03a22a04de437254e8ce8cc84ba39689288752" + integrity sha512-HyYEUDeIj5rRQU2Hk5HTB2uHsbRQpF70nvMhVzi+VJR0X+xNEhjPui4/kBf3VeH/wqD28PT4sVOm8qqLjBrSZg== + +"@dsherret/to-absolute-glob@^2.0.2": + version "2.0.2" + resolved "https://registry.yarnpkg.com/@dsherret/to-absolute-glob/-/to-absolute-glob-2.0.2.tgz#1f6475dc8bd974cea07a2daf3864b317b1dd332c" + integrity sha1-H2R13IvZdM6gei2vOGSzF7HdMyw= + dependencies: + is-absolute "^1.0.0" + is-negated-glob "^1.0.0" + +"@eslint/eslintrc@^0.3.0": + version "0.3.0" + resolved "https://registry.yarnpkg.com/@eslint/eslintrc/-/eslintrc-0.3.0.tgz#d736d6963d7003b6514e6324bec9c602ac340318" + integrity sha512-1JTKgrOKAHVivSvOYw+sJOunkBjUOvjqWk1DPja7ZFhIS2mX/4EgTT8M7eTK9jrKhL/FvXXEbQwIs3pg1xp3dg== + dependencies: + ajv "^6.12.4" + debug "^4.1.1" + espree "^7.3.0" + globals "^12.1.0" + ignore "^4.0.6" + import-fresh "^3.2.1" + js-yaml "^3.13.1" + lodash "^4.17.20" + minimatch "^3.0.4" + strip-json-comments "^3.1.1" + +"@nodelib/fs.scandir@2.1.4": + version "2.1.4" + resolved "https://registry.yarnpkg.com/@nodelib/fs.scandir/-/fs.scandir-2.1.4.tgz#d4b3549a5db5de2683e0c1071ab4f140904bbf69" + integrity sha512-33g3pMJk3bg5nXbL/+CY6I2eJDzZAni49PfJnL5fghPTggPvBd/pFNSgJsdAgWptuFu7qq/ERvOYFlhvsLTCKA== + dependencies: + "@nodelib/fs.stat" "2.0.4" + run-parallel "^1.1.9" + +"@nodelib/fs.stat@2.0.4", "@nodelib/fs.stat@^2.0.2": + version "2.0.4" + resolved "https://registry.yarnpkg.com/@nodelib/fs.stat/-/fs.stat-2.0.4.tgz#a3f2dd61bab43b8db8fa108a121cfffe4c676655" + integrity sha512-IYlHJA0clt2+Vg7bccq+TzRdJvv19c2INqBSsoOLp1je7xjtr7J26+WXR72MCdvU9q1qTzIWDfhMf+DRvQJK4Q== + +"@nodelib/fs.walk@^1.2.3": + version "1.2.6" + resolved "https://registry.yarnpkg.com/@nodelib/fs.walk/-/fs.walk-1.2.6.tgz#cce9396b30aa5afe9e3756608f5831adcb53d063" + integrity sha512-8Broas6vTtW4GIXTAHDoE32hnN2M5ykgCpWGbuXHQ15vEMqr23pB76e/GZcYsZCHALv50ktd24qhEyKr6wBtow== + dependencies: + "@nodelib/fs.scandir" "2.1.4" + fastq "^1.6.0" + +"@npmcli/move-file@^1.0.1": + version "1.1.0" + resolved "https://registry.yarnpkg.com/@npmcli/move-file/-/move-file-1.1.0.tgz#4ef8a53d727b9e43facf35404caf55ebf92cfec8" + integrity sha512-Iv2iq0JuyYjKeFkSR4LPaCdDZwlGK9X2cP/01nJcp3yMJ1FjNd9vpiEYvLUgzBxKPg2SFmaOhizoQsPc0LWeOQ== + dependencies: + mkdirp "^1.0.4" + rimraf "^2.7.1" + +"@sentry/browser@5.30.0": + version "5.30.0" + resolved "https://registry.yarnpkg.com/@sentry/browser/-/browser-5.30.0.tgz#c28f49d551db3172080caef9f18791a7fd39e3b3" + integrity sha512-rOb58ZNVJWh1VuMuBG1mL9r54nZqKeaIlwSlvzJfc89vyfd7n6tQ1UXMN383QBz/MS5H5z44Hy5eE+7pCrYAfw== + dependencies: + "@sentry/core" "5.30.0" + "@sentry/types" "5.30.0" + "@sentry/utils" "5.30.0" + tslib "^1.9.3" + +"@sentry/core@5.30.0": + version "5.30.0" + resolved "https://registry.yarnpkg.com/@sentry/core/-/core-5.30.0.tgz#6b203664f69e75106ee8b5a2fe1d717379b331f3" + integrity sha512-TmfrII8w1PQZSZgPpUESqjB+jC6MvZJZdLtE/0hZ+SrnKhW3x5WlYLvTXZpcWePYBku7rl2wn1RZu6uT0qCTeg== + dependencies: + "@sentry/hub" "5.30.0" + "@sentry/minimal" "5.30.0" + "@sentry/types" "5.30.0" + "@sentry/utils" "5.30.0" + tslib "^1.9.3" + +"@sentry/hub@5.30.0": + version "5.30.0" + resolved "https://registry.yarnpkg.com/@sentry/hub/-/hub-5.30.0.tgz#2453be9b9cb903404366e198bd30c7ca74cdc100" + integrity sha512-2tYrGnzb1gKz2EkMDQcfLrDTvmGcQPuWxLnJKXJvYTQDGLlEvi2tWz1VIHjunmOvJrB5aIQLhm+dcMRwFZDCqQ== + dependencies: + "@sentry/types" "5.30.0" + "@sentry/utils" "5.30.0" + tslib "^1.9.3" + +"@sentry/minimal@5.30.0": + version "5.30.0" + resolved "https://registry.yarnpkg.com/@sentry/minimal/-/minimal-5.30.0.tgz#ce3d3a6a273428e0084adcb800bc12e72d34637b" + integrity sha512-BwWb/owZKtkDX+Sc4zCSTNcvZUq7YcH3uAVlmh/gtR9rmUvbzAA3ewLuB3myi4wWRAMEtny6+J/FN/x+2wn9Xw== + dependencies: + "@sentry/hub" "5.30.0" + "@sentry/types" "5.30.0" + tslib "^1.9.3" + +"@sentry/react@^5.27.0": + version "5.30.0" + resolved "https://registry.yarnpkg.com/@sentry/react/-/react-5.30.0.tgz#320e05f766b6a26faefa8d76d1101fd50c69f541" + integrity sha512-dvn4mqCgbeEuUXEGp5P9PaW5j4GWTFUSdx/yG8f9IxNZv5zM+7otjog9ukrubFZvlxVxD/PrIxK0MhadfFY/Dw== + dependencies: + "@sentry/browser" "5.30.0" + "@sentry/minimal" "5.30.0" + "@sentry/types" "5.30.0" + "@sentry/utils" "5.30.0" + hoist-non-react-statics "^3.3.2" + tslib "^1.9.3" + +"@sentry/types@5.30.0": + version "5.30.0" + resolved "https://registry.yarnpkg.com/@sentry/types/-/types-5.30.0.tgz#19709bbe12a1a0115bc790b8942917da5636f402" + integrity sha512-R8xOqlSTZ+htqrfteCWU5Nk0CDN5ApUTvrlvBuiH1DyP6czDZ4ktbZB0hAgBlVcK0U+qpD3ag3Tqqpa5Q67rPw== + +"@sentry/utils@5.30.0": + version "5.30.0" + resolved "https://registry.yarnpkg.com/@sentry/utils/-/utils-5.30.0.tgz#9a5bd7ccff85ccfe7856d493bffa64cabc41e980" + integrity sha512-zaYmoH0NWWtvnJjC9/CBseXMtKHm/tm40sz3YfJRxeQjyzRqNQPgivpd9R/oDJCYj999mzdW382p/qi2ypjLww== + dependencies: + "@sentry/types" "5.30.0" + tslib "^1.9.3" + +"@stylelint/postcss-css-in-js@^0.37.2": + version "0.37.2" + resolved "https://registry.yarnpkg.com/@stylelint/postcss-css-in-js/-/postcss-css-in-js-0.37.2.tgz#7e5a84ad181f4234a2480803422a47b8749af3d2" + integrity sha512-nEhsFoJurt8oUmieT8qy4nk81WRHmJynmVwn/Vts08PL9fhgIsMhk1GId5yAN643OzqEEb5S/6At2TZW7pqPDA== + dependencies: + "@babel/core" ">=7.9.0" + +"@stylelint/postcss-markdown@^0.36.2": + version "0.36.2" + resolved "https://registry.yarnpkg.com/@stylelint/postcss-markdown/-/postcss-markdown-0.36.2.tgz#0a540c4692f8dcdfc13c8e352c17e7bfee2bb391" + integrity sha512-2kGbqUVJUGE8dM+bMzXG/PYUWKkjLIkRLWNh39OaADkiabDRdw8ATFCgbMz5xdIcvwspPAluSL7uY+ZiTWdWmQ== + dependencies: + remark "^13.0.0" + unist-util-find-all-after "^3.0.2" + +"@ts-morph/common@~0.6.0": + version "0.6.0" + resolved "https://registry.yarnpkg.com/@ts-morph/common/-/common-0.6.0.tgz#cbd4ee57c5ef971511b9c5778e0bb8eb27de4783" + integrity sha512-pI35nZz5bs3tL3btSVX2cWkAE8rc80F+Fn4TwSC6bQvn7fgn9IyLXVcAfpG6X6NBY5wN9TkSWXn/QYUkBvR/Fw== + dependencies: + "@dsherret/to-absolute-glob" "^2.0.2" + fast-glob "^3.2.4" + fs-extra "^9.0.1" + is-negated-glob "^1.0.0" + multimatch "^4.0.0" + typescript "~4.0.2" + +"@types/anymatch@*": + version "1.3.1" + resolved "https://registry.yarnpkg.com/@types/anymatch/-/anymatch-1.3.1.tgz#336badc1beecb9dacc38bea2cf32adf627a8421a" + integrity sha512-/+CRPXpBDpo2RK9C68N3b2cOvO0Cf5B9aPijHsoDQTHivnGSObdOF2BRQOYjojWTDy6nQvMjmqRXIxH55VjxxA== + +"@types/classnames@^2.2.10": + version "2.2.11" + resolved "https://registry.yarnpkg.com/@types/classnames/-/classnames-2.2.11.tgz#2521cc86f69d15c5b90664e4829d84566052c1cf" + integrity sha512-2koNhpWm3DgWRp5tpkiJ8JGc1xTn2q0l+jUNUE7oMKXUf5NpI9AIdC4kbjGNFBdHtcxBD18LAksoudAVhFKCjw== + +"@types/eslint-scope@^3.7.0": + version "3.7.0" + resolved "https://registry.yarnpkg.com/@types/eslint-scope/-/eslint-scope-3.7.0.tgz#4792816e31119ebd506902a482caec4951fabd86" + integrity sha512-O/ql2+rrCUe2W2rs7wMR+GqPRcgB6UiqN5RhrR5xruFlY7l9YLMn0ZkDzjoHLeiFkR8MCQZVudUuuvQ2BLC9Qw== + dependencies: + "@types/eslint" "*" + "@types/estree" "*" + +"@types/eslint@*": + version "7.2.6" + resolved "https://registry.yarnpkg.com/@types/eslint/-/eslint-7.2.6.tgz#5e9aff555a975596c03a98b59ecd103decc70c3c" + integrity sha512-I+1sYH+NPQ3/tVqCeUSBwTE/0heyvtXqpIopUUArlBm0Kpocb8FbMa3AZ/ASKIFpN3rnEx932TTXDbt9OXsNDw== + dependencies: + "@types/estree" "*" + "@types/json-schema" "*" + +"@types/estree@*", "@types/estree@^0.0.46": + version "0.0.46" + resolved "https://registry.yarnpkg.com/@types/estree/-/estree-0.0.46.tgz#0fb6bfbbeabd7a30880504993369c4bf1deab1fe" + integrity sha512-laIjwTQaD+5DukBZaygQ79K1Z0jb1bPEMRrkXSLjtCcZm+abyp5YbrqpSLzD42FwWW6gK/aS4NYpJ804nG2brg== + +"@types/glob@^7.1.1": + version "7.1.3" + resolved "https://registry.yarnpkg.com/@types/glob/-/glob-7.1.3.tgz#e6ba80f36b7daad2c685acd9266382e68985c183" + integrity sha512-SEYeGAIQIQX8NN6LDKprLjbrd5dARM5EXsd8GI/A5l0apYI1fGMWgPHSe4ZKL4eozlAyI+doUE9XbYS4xCkQ1w== + dependencies: + "@types/minimatch" "*" + "@types/node" "*" + +"@types/history@*": + version "4.7.8" + resolved "https://registry.yarnpkg.com/@types/history/-/history-4.7.8.tgz#49348387983075705fe8f4e02fb67f7daaec4934" + integrity sha512-S78QIYirQcUoo6UJZx9CSP0O2ix9IaeAXwQi26Rhr/+mg7qqPy8TzaxHSUut7eGjL8WmLccT7/MXf304WjqHcA== + +"@types/hoist-non-react-statics@^3.3.0": + version "3.3.1" + resolved "https://registry.yarnpkg.com/@types/hoist-non-react-statics/-/hoist-non-react-statics-3.3.1.tgz#1124aafe5118cb591977aeb1ceaaed1070eb039f" + integrity sha512-iMIqiko6ooLrTh1joXodJK5X9xeEALT1kM5G3ZLhD3hszxBdIEd5C75U834D9mLcINgD4OyZf5uQXjkuYydWvA== + dependencies: + "@types/react" "*" + hoist-non-react-statics "^3.3.0" + +"@types/html-minifier-terser@^5.0.0": + version "5.1.1" + resolved "https://registry.yarnpkg.com/@types/html-minifier-terser/-/html-minifier-terser-5.1.1.tgz#3c9ee980f1a10d6021ae6632ca3e79ca2ec4fb50" + integrity sha512-giAlZwstKbmvMk1OO7WXSj4OZ0keXAcl2TQq4LWHiiPH2ByaH7WeUzng+Qej8UPxxv+8lRTuouo0iaNDBuzIBA== + +"@types/http-proxy@^1.17.4": + version "1.17.5" + resolved "https://registry.yarnpkg.com/@types/http-proxy/-/http-proxy-1.17.5.tgz#c203c5e6e9dc6820d27a40eb1e511c70a220423d" + integrity sha512-GNkDE7bTv6Sf8JbV2GksknKOsk7OznNYHSdrtvPJXO0qJ9odZig6IZKUi5RFGi6d1bf6dgIAe4uXi3DBc7069Q== + dependencies: + "@types/node" "*" + +"@types/json-schema@*", "@types/json-schema@^7.0.3", "@types/json-schema@^7.0.5", "@types/json-schema@^7.0.6": + version "7.0.7" + resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.7.tgz#98a993516c859eb0d5c4c8f098317a9ea68db9ad" + integrity sha512-cxWFQVseBm6O9Gbw1IWb8r6OS4OhSt3hPZLkFApLjM8TEXROBuQGLAH2i2gZpcXdLBIrpXuTDhH7Vbm1iXmNGA== + +"@types/json5@^0.0.29": + version "0.0.29" + resolved "https://registry.yarnpkg.com/@types/json5/-/json5-0.0.29.tgz#ee28707ae94e11d2b827bcbe5270bcea7f3e71ee" + integrity sha1-7ihweulOEdK4J7y+UnC86n8+ce4= + +"@types/mdast@^3.0.0": + version "3.0.3" + resolved "https://registry.yarnpkg.com/@types/mdast/-/mdast-3.0.3.tgz#2d7d671b1cd1ea3deb306ea75036c2a0407d2deb" + integrity sha512-SXPBMnFVQg1s00dlMCc/jCdvPqdE4mXaMMCeRlxLDmTAEoegHT53xKtkDnzDTOcmMHUfcjyf36/YYZ6SxRdnsw== + dependencies: + "@types/unist" "*" + +"@types/minimatch@*", "@types/minimatch@^3.0.3": + version "3.0.3" + resolved "https://registry.yarnpkg.com/@types/minimatch/-/minimatch-3.0.3.tgz#3dca0e3f33b200fc7d1139c0cd96c1268cadfd9d" + integrity sha512-tHq6qdbT9U1IRSGf14CL0pUlULksvY9OZ+5eEgl1N7t+OA3tGvNpxJCzuKQlsNgCVwbAs670L1vcVQi8j9HjnA== + +"@types/minimist@^1.2.0": + version "1.2.1" + resolved "https://registry.yarnpkg.com/@types/minimist/-/minimist-1.2.1.tgz#283f669ff76d7b8260df8ab7a4262cc83d988256" + integrity sha512-fZQQafSREFyuZcdWFAExYjBiCL7AUCdgsk80iO0q4yihYYdcIiH28CcuPTGFgLOCC8RlW49GSQxdHwZP+I7CNg== + +"@types/node@*": + version "14.14.22" + resolved "https://registry.yarnpkg.com/@types/node/-/node-14.14.22.tgz#0d29f382472c4ccf3bd96ff0ce47daf5b7b84b18" + integrity sha512-g+f/qj/cNcqKkc3tFqlXOYjrmZA+jNBiDzbP3kH+B+otKFqAdPgVTGP1IeKRdMml/aE69as5S4FqtxAbl+LaMw== + +"@types/normalize-package-data@^2.4.0": + version "2.4.0" + resolved "https://registry.yarnpkg.com/@types/normalize-package-data/-/normalize-package-data-2.4.0.tgz#e486d0d97396d79beedd0a6e33f4534ff6b4973e" + integrity sha512-f5j5b/Gf71L+dbqxIpQ4Z2WlmI/mPJ0fOkGGmFgtb6sAu97EPczzbS3/tJKxmcYDj55OX6ssqwDAWOHIYDRDGA== + +"@types/parse-json@^4.0.0": + version "4.0.0" + resolved "https://registry.yarnpkg.com/@types/parse-json/-/parse-json-4.0.0.tgz#2f8bb441434d163b35fb8ffdccd7138927ffb8c0" + integrity sha512-//oorEZjL6sbPcKUaCdIGlIUeH26mgzimjBB77G6XRgnDl/L5wOnpyBGRe/Mmf5CVW3PwEBE1NjiMZ/ssFh4wA== + +"@types/prop-types@*": + version "15.7.3" + resolved "https://registry.yarnpkg.com/@types/prop-types/-/prop-types-15.7.3.tgz#2ab0d5da2e5815f94b0b9d4b95d1e5f243ab2ca7" + integrity sha512-KfRL3PuHmqQLOG+2tGpRO26Ctg+Cq1E01D2DMriKEATHgWLfeNDmq9e29Q9WIky0dQ3NPkd1mzYH8Lm936Z9qw== + +"@types/q@^1.5.1": + version "1.5.4" + resolved "https://registry.yarnpkg.com/@types/q/-/q-1.5.4.tgz#15925414e0ad2cd765bfef58842f7e26a7accb24" + integrity sha512-1HcDas8SEj4z1Wc696tH56G8OlRaH/sqZOynNNB+HF0WOeXPaxTtbYzJY2oEfiUxjSKjhCKr+MvR7dCHcEelug== + +"@types/qs@^6.9.5": + version "6.9.5" + resolved "https://registry.yarnpkg.com/@types/qs/-/qs-6.9.5.tgz#434711bdd49eb5ee69d90c1d67c354a9a8ecb18b" + integrity sha512-/JHkVHtx/REVG0VVToGRGH2+23hsYLHdyG+GrvoUGlGAd0ErauXDyvHtRI/7H7mzLm+tBCKA7pfcpkQ1lf58iQ== + +"@types/react-dom@^16.9.8": + version "16.9.10" + resolved "https://registry.yarnpkg.com/@types/react-dom/-/react-dom-16.9.10.tgz#4485b0bec3d41f856181b717f45fd7831101156f" + integrity sha512-ItatOrnXDMAYpv6G8UCk2VhbYVTjZT9aorLtA/OzDN9XJ2GKcfam68jutoAcILdRjsRUO8qb7AmyObF77Q8QFw== + dependencies: + "@types/react" "^16" + +"@types/react-redux@^7.1.9": + version "7.1.16" + resolved "https://registry.yarnpkg.com/@types/react-redux/-/react-redux-7.1.16.tgz#0fbd04c2500c12105494c83d4a3e45c084e3cb21" + integrity sha512-f/FKzIrZwZk7YEO9E1yoxIuDNRiDducxkFlkw/GNMGEnK9n4K8wJzlJBghpSuOVDgEUHoDkDF7Gi9lHNQR4siw== + dependencies: + "@types/hoist-non-react-statics" "^3.3.0" + "@types/react" "*" + hoist-non-react-statics "^3.3.0" + redux "^4.0.0" + +"@types/react-router-dom@^5.1.6": + version "5.1.7" + resolved "https://registry.yarnpkg.com/@types/react-router-dom/-/react-router-dom-5.1.7.tgz#a126d9ea76079ffbbdb0d9225073eb5797ab7271" + integrity sha512-D5mHD6TbdV/DNHYsnwBTv+y73ei+mMjrkGrla86HthE4/PVvL1J94Bu3qABU+COXzpL23T1EZapVVpwHuBXiUg== + dependencies: + "@types/history" "*" + "@types/react" "*" + "@types/react-router" "*" + +"@types/react-router@*": + version "5.1.11" + resolved "https://registry.yarnpkg.com/@types/react-router/-/react-router-5.1.11.tgz#b01ce4cb21bf7d6b32edc862fc1e2c0088044b5b" + integrity sha512-ofHbZMlp0Y2baOHgsWBQ4K3AttxY61bDMkwTiBOkPg7U6C/3UwwB5WaIx28JmSVi/eX3uFEMRo61BV22fDQIvg== + dependencies: + "@types/history" "*" + "@types/react" "*" + +"@types/react@*": + version "17.0.0" + resolved "https://registry.yarnpkg.com/@types/react/-/react-17.0.0.tgz#5af3eb7fad2807092f0046a1302b7823e27919b8" + integrity sha512-aj/L7RIMsRlWML3YB6KZiXB3fV2t41+5RBGYF8z+tAKU43Px8C3cYUZsDvf1/+Bm4FK21QWBrDutu8ZJ/70qOw== + dependencies: + "@types/prop-types" "*" + csstype "^3.0.2" + +"@types/react@^16", "@types/react@^16.9.53": + version "16.14.2" + resolved "https://registry.yarnpkg.com/@types/react/-/react-16.14.2.tgz#85dcc0947d0645349923c04ccef6018a1ab7538c" + integrity sha512-BzzcAlyDxXl2nANlabtT4thtvbbnhee8hMmH/CcJrISDBVcJS1iOsP1f0OAgSdGE0MsY9tqcrb9YoZcOFv9dbQ== + dependencies: + "@types/prop-types" "*" + csstype "^3.0.2" + +"@types/source-list-map@*": + version "0.1.2" + resolved "https://registry.yarnpkg.com/@types/source-list-map/-/source-list-map-0.1.2.tgz#0078836063ffaf17412349bba364087e0ac02ec9" + integrity sha512-K5K+yml8LTo9bWJI/rECfIPrGgxdpeNbj+d53lwN4QjW1MCwlkhUms+gtdzigTeUyBr09+u8BwOIY3MXvHdcsA== + +"@types/tapable@*", "@types/tapable@^1.0.5": + version "1.0.6" + resolved "https://registry.yarnpkg.com/@types/tapable/-/tapable-1.0.6.tgz#a9ca4b70a18b270ccb2bc0aaafefd1d486b7ea74" + integrity sha512-W+bw9ds02rAQaMvaLYxAbJ6cvguW/iJXNT6lTssS1ps6QdrMKttqEAMEG/b5CR8TZl3/L7/lH0ZV5nNR1LXikA== + +"@types/uglify-js@*": + version "3.11.1" + resolved "https://registry.yarnpkg.com/@types/uglify-js/-/uglify-js-3.11.1.tgz#97ff30e61a0aa6876c270b5f538737e2d6ab8ceb" + integrity sha512-7npvPKV+jINLu1SpSYVWG8KvyJBhBa8tmzMMdDoVc2pWUYHN8KIXlPJhjJ4LT97c4dXJA2SHL/q6ADbDriZN+Q== + dependencies: + source-map "^0.6.1" + +"@types/unist@*", "@types/unist@^2.0.0", "@types/unist@^2.0.2": + version "2.0.3" + resolved "https://registry.yarnpkg.com/@types/unist/-/unist-2.0.3.tgz#9c088679876f374eb5983f150d4787aa6fb32d7e" + integrity sha512-FvUupuM3rlRsRtCN+fDudtmytGO6iHJuuRKS1Ss0pG5z8oX0diNEw94UEL7hgDbpN94rgaK5R7sWm6RrSkZuAQ== + +"@types/webpack-sources@*": + version "2.1.0" + resolved "https://registry.yarnpkg.com/@types/webpack-sources/-/webpack-sources-2.1.0.tgz#8882b0bd62d1e0ce62f183d0d01b72e6e82e8c10" + integrity sha512-LXn/oYIpBeucgP1EIJbKQ2/4ZmpvRl+dlrFdX7+94SKRUV3Evy3FsfMZY318vGhkWUS5MPhtOM3w1/hCOAOXcg== + dependencies: + "@types/node" "*" + "@types/source-list-map" "*" + source-map "^0.7.3" + +"@types/webpack@^4.41.8": + version "4.41.26" + resolved "https://registry.yarnpkg.com/@types/webpack/-/webpack-4.41.26.tgz#27a30d7d531e16489f9c7607c747be6bc1a459ef" + integrity sha512-7ZyTfxjCRwexh+EJFwRUM+CDB2XvgHl4vfuqf1ZKrgGvcS5BrNvPQqJh3tsZ0P6h6Aa1qClVHaJZszLPzpqHeA== + dependencies: + "@types/anymatch" "*" + "@types/node" "*" + "@types/tapable" "*" + "@types/uglify-js" "*" + "@types/webpack-sources" "*" + source-map "^0.6.0" + +"@typescript-eslint/eslint-plugin@^4.5.0": + version "4.14.1" + resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-4.14.1.tgz#22dd301ce228aaab3416b14ead10b1db3e7d3180" + integrity sha512-5JriGbYhtqMS1kRcZTQxndz1lKMwwEXKbwZbkUZNnp6MJX0+OVXnG0kOlBZP4LUAxEyzu3cs+EXd/97MJXsGfw== + dependencies: + "@typescript-eslint/experimental-utils" "4.14.1" + "@typescript-eslint/scope-manager" "4.14.1" + debug "^4.1.1" + functional-red-black-tree "^1.0.1" + lodash "^4.17.15" + regexpp "^3.0.0" + semver "^7.3.2" + tsutils "^3.17.1" + +"@typescript-eslint/experimental-utils@4.14.1": + version "4.14.1" + resolved "https://registry.yarnpkg.com/@typescript-eslint/experimental-utils/-/experimental-utils-4.14.1.tgz#a5c945cb24dabb96747180e1cfc8487f8066f471" + integrity sha512-2CuHWOJwvpw0LofbyG5gvYjEyoJeSvVH2PnfUQSn0KQr4v8Dql2pr43ohmx4fdPQ/eVoTSFjTi/bsGEXl/zUUQ== + dependencies: + "@types/json-schema" "^7.0.3" + "@typescript-eslint/scope-manager" "4.14.1" + "@typescript-eslint/types" "4.14.1" + "@typescript-eslint/typescript-estree" "4.14.1" + eslint-scope "^5.0.0" + eslint-utils "^2.0.0" + +"@typescript-eslint/parser@4.4.1": + version "4.4.1" + resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-4.4.1.tgz#25fde9c080611f303f2f33cedb145d2c59915b80" + integrity sha512-S0fuX5lDku28Au9REYUsV+hdJpW/rNW0gWlc4SXzF/kdrRaAVX9YCxKpziH7djeWT/HFAjLZcnY7NJD8xTeUEg== + dependencies: + "@typescript-eslint/scope-manager" "4.4.1" + "@typescript-eslint/types" "4.4.1" + "@typescript-eslint/typescript-estree" "4.4.1" + debug "^4.1.1" + +"@typescript-eslint/parser@^4.5.0": + version "4.14.1" + resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-4.14.1.tgz#3bd6c24710cd557d8446625284bcc9c6d52817c6" + integrity sha512-mL3+gU18g9JPsHZuKMZ8Z0Ss9YP1S5xYZ7n68Z98GnPq02pYNQuRXL85b9GYhl6jpdvUc45Km7hAl71vybjUmw== + dependencies: + "@typescript-eslint/scope-manager" "4.14.1" + "@typescript-eslint/types" "4.14.1" + "@typescript-eslint/typescript-estree" "4.14.1" + debug "^4.1.1" + +"@typescript-eslint/scope-manager@4.14.1": + version "4.14.1" + resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-4.14.1.tgz#8444534254c6f370e9aa974f035ced7fe713ce02" + integrity sha512-F4bjJcSqXqHnC9JGUlnqSa3fC2YH5zTtmACS1Hk+WX/nFB0guuynVK5ev35D4XZbdKjulXBAQMyRr216kmxghw== + dependencies: + "@typescript-eslint/types" "4.14.1" + "@typescript-eslint/visitor-keys" "4.14.1" + +"@typescript-eslint/scope-manager@4.4.1": + version "4.4.1" + resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-4.4.1.tgz#d19447e60db2ce9c425898d62fa03b2cce8ea3f9" + integrity sha512-2oD/ZqD4Gj41UdFeWZxegH3cVEEH/Z6Bhr/XvwTtGv66737XkR4C9IqEkebCuqArqBJQSj4AgNHHiN1okzD/wQ== + dependencies: + "@typescript-eslint/types" "4.4.1" + "@typescript-eslint/visitor-keys" "4.4.1" + +"@typescript-eslint/types@4.14.1": + version "4.14.1" + resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-4.14.1.tgz#b3d2eb91dafd0fd8b3fce7c61512ac66bd0364aa" + integrity sha512-SkhzHdI/AllAgQSxXM89XwS1Tkic7csPdndUuTKabEwRcEfR8uQ/iPA3Dgio1rqsV3jtqZhY0QQni8rLswJM2w== + +"@typescript-eslint/types@4.4.1": + version "4.4.1" + resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-4.4.1.tgz#c507b35cf523bc7ba00aae5f75ee9b810cdabbc1" + integrity sha512-KNDfH2bCyax5db+KKIZT4rfA8rEk5N0EJ8P0T5AJjo5xrV26UAzaiqoJCxeaibqc0c/IvZxp7v2g3difn2Pn3w== + +"@typescript-eslint/typescript-estree@4.14.1": + version "4.14.1" + resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-4.14.1.tgz#20d3b8c8e3cdc8f764bdd5e5b0606dd83da6075b" + integrity sha512-M8+7MbzKC1PvJIA8kR2sSBnex8bsR5auatLCnVlNTJczmJgqRn8M+sAlQfkEq7M4IY3WmaNJ+LJjPVRrREVSHQ== + dependencies: + "@typescript-eslint/types" "4.14.1" + "@typescript-eslint/visitor-keys" "4.14.1" + debug "^4.1.1" + globby "^11.0.1" + is-glob "^4.0.1" + lodash "^4.17.15" + semver "^7.3.2" + tsutils "^3.17.1" + +"@typescript-eslint/typescript-estree@4.4.1": + version "4.4.1" + resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-4.4.1.tgz#598f6de488106c2587d47ca2462c60f6e2797cb8" + integrity sha512-wP/V7ScKzgSdtcY1a0pZYBoCxrCstLrgRQ2O9MmCUZDtmgxCO/TCqOTGRVwpP4/2hVfqMz/Vw1ZYrG8cVxvN3g== + dependencies: + "@typescript-eslint/types" "4.4.1" + "@typescript-eslint/visitor-keys" "4.4.1" + debug "^4.1.1" + globby "^11.0.1" + is-glob "^4.0.1" + lodash "^4.17.15" + semver "^7.3.2" + tsutils "^3.17.1" + +"@typescript-eslint/visitor-keys@4.14.1": + version "4.14.1" + resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-4.14.1.tgz#e93c2ff27f47ee477a929b970ca89d60a117da91" + integrity sha512-TAblbDXOI7bd0C/9PE1G+AFo7R5uc+ty1ArDoxmrC1ah61Hn6shURKy7gLdRb1qKJmjHkqu5Oq+e4Kt0jwf1IA== + dependencies: + "@typescript-eslint/types" "4.14.1" + eslint-visitor-keys "^2.0.0" + +"@typescript-eslint/visitor-keys@4.4.1": + version "4.4.1" + resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-4.4.1.tgz#1769dc7a9e2d7d2cfd3318b77ed8249187aed5c3" + integrity sha512-H2JMWhLaJNeaylSnMSQFEhT/S/FsJbebQALmoJxMPMxLtlVAMy2uJP/Z543n9IizhjRayLSqoInehCeNW9rWcw== + dependencies: + "@typescript-eslint/types" "4.4.1" + eslint-visitor-keys "^2.0.0" + +"@webassemblyjs/ast@1.11.0": + version "1.11.0" + resolved "https://registry.yarnpkg.com/@webassemblyjs/ast/-/ast-1.11.0.tgz#a5aa679efdc9e51707a4207139da57920555961f" + integrity sha512-kX2W49LWsbthrmIRMbQZuQDhGtjyqXfEmmHyEi4XWnSZtPmxY0+3anPIzsnRb45VH/J55zlOfWvZuY47aJZTJg== + dependencies: + "@webassemblyjs/helper-numbers" "1.11.0" + "@webassemblyjs/helper-wasm-bytecode" "1.11.0" + +"@webassemblyjs/floating-point-hex-parser@1.11.0": + version "1.11.0" + resolved "https://registry.yarnpkg.com/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.11.0.tgz#34d62052f453cd43101d72eab4966a022587947c" + integrity sha512-Q/aVYs/VnPDVYvsCBL/gSgwmfjeCb4LW8+TMrO3cSzJImgv8lxxEPM2JA5jMrivE7LSz3V+PFqtMbls3m1exDA== + +"@webassemblyjs/helper-api-error@1.11.0": + version "1.11.0" + resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-api-error/-/helper-api-error-1.11.0.tgz#aaea8fb3b923f4aaa9b512ff541b013ffb68d2d4" + integrity sha512-baT/va95eXiXb2QflSx95QGT5ClzWpGaa8L7JnJbgzoYeaA27FCvuBXU758l+KXWRndEmUXjP0Q5fibhavIn8w== + +"@webassemblyjs/helper-buffer@1.11.0": + version "1.11.0" + resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-buffer/-/helper-buffer-1.11.0.tgz#d026c25d175e388a7dbda9694e91e743cbe9b642" + integrity sha512-u9HPBEl4DS+vA8qLQdEQ6N/eJQ7gT7aNvMIo8AAWvAl/xMrcOSiI2M0MAnMCy3jIFke7bEee/JwdX1nUpCtdyA== + +"@webassemblyjs/helper-numbers@1.11.0": + version "1.11.0" + resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-numbers/-/helper-numbers-1.11.0.tgz#7ab04172d54e312cc6ea4286d7d9fa27c88cd4f9" + integrity sha512-DhRQKelIj01s5IgdsOJMKLppI+4zpmcMQ3XboFPLwCpSNH6Hqo1ritgHgD0nqHeSYqofA6aBN/NmXuGjM1jEfQ== + dependencies: + "@webassemblyjs/floating-point-hex-parser" "1.11.0" + "@webassemblyjs/helper-api-error" "1.11.0" + "@xtuc/long" "4.2.2" + +"@webassemblyjs/helper-wasm-bytecode@1.11.0": + version "1.11.0" + resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.11.0.tgz#85fdcda4129902fe86f81abf7e7236953ec5a4e1" + integrity sha512-MbmhvxXExm542tWREgSFnOVo07fDpsBJg3sIl6fSp9xuu75eGz5lz31q7wTLffwL3Za7XNRCMZy210+tnsUSEA== + +"@webassemblyjs/helper-wasm-section@1.11.0": + version "1.11.0" + resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.11.0.tgz#9ce2cc89300262509c801b4af113d1ca25c1a75b" + integrity sha512-3Eb88hcbfY/FCukrg6i3EH8H2UsD7x8Vy47iVJrP967A9JGqgBVL9aH71SETPx1JrGsOUVLo0c7vMCN22ytJew== + dependencies: + "@webassemblyjs/ast" "1.11.0" + "@webassemblyjs/helper-buffer" "1.11.0" + "@webassemblyjs/helper-wasm-bytecode" "1.11.0" + "@webassemblyjs/wasm-gen" "1.11.0" + +"@webassemblyjs/ieee754@1.11.0": + version "1.11.0" + resolved "https://registry.yarnpkg.com/@webassemblyjs/ieee754/-/ieee754-1.11.0.tgz#46975d583f9828f5d094ac210e219441c4e6f5cf" + integrity sha512-KXzOqpcYQwAfeQ6WbF6HXo+0udBNmw0iXDmEK5sFlmQdmND+tr773Ti8/5T/M6Tl/413ArSJErATd8In3B+WBA== + dependencies: + "@xtuc/ieee754" "^1.2.0" + +"@webassemblyjs/leb128@1.11.0": + version "1.11.0" + resolved "https://registry.yarnpkg.com/@webassemblyjs/leb128/-/leb128-1.11.0.tgz#f7353de1df38aa201cba9fb88b43f41f75ff403b" + integrity sha512-aqbsHa1mSQAbeeNcl38un6qVY++hh8OpCOzxhixSYgbRfNWcxJNJQwe2rezK9XEcssJbbWIkblaJRwGMS9zp+g== + dependencies: + "@xtuc/long" "4.2.2" + +"@webassemblyjs/utf8@1.11.0": + version "1.11.0" + resolved "https://registry.yarnpkg.com/@webassemblyjs/utf8/-/utf8-1.11.0.tgz#86e48f959cf49e0e5091f069a709b862f5a2cadf" + integrity sha512-A/lclGxH6SpSLSyFowMzO/+aDEPU4hvEiooCMXQPcQFPPJaYcPQNKGOCLUySJsYJ4trbpr+Fs08n4jelkVTGVw== + +"@webassemblyjs/wasm-edit@1.11.0": + version "1.11.0" + resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-edit/-/wasm-edit-1.11.0.tgz#ee4a5c9f677046a210542ae63897094c2027cb78" + integrity sha512-JHQ0damXy0G6J9ucyKVXO2j08JVJ2ntkdJlq1UTiUrIgfGMmA7Ik5VdC/L8hBK46kVJgujkBIoMtT8yVr+yVOQ== + dependencies: + "@webassemblyjs/ast" "1.11.0" + "@webassemblyjs/helper-buffer" "1.11.0" + "@webassemblyjs/helper-wasm-bytecode" "1.11.0" + "@webassemblyjs/helper-wasm-section" "1.11.0" + "@webassemblyjs/wasm-gen" "1.11.0" + "@webassemblyjs/wasm-opt" "1.11.0" + "@webassemblyjs/wasm-parser" "1.11.0" + "@webassemblyjs/wast-printer" "1.11.0" + +"@webassemblyjs/wasm-gen@1.11.0": + version "1.11.0" + resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-gen/-/wasm-gen-1.11.0.tgz#3cdb35e70082d42a35166988dda64f24ceb97abe" + integrity sha512-BEUv1aj0WptCZ9kIS30th5ILASUnAPEvE3tVMTrItnZRT9tXCLW2LEXT8ezLw59rqPP9klh9LPmpU+WmRQmCPQ== + dependencies: + "@webassemblyjs/ast" "1.11.0" + "@webassemblyjs/helper-wasm-bytecode" "1.11.0" + "@webassemblyjs/ieee754" "1.11.0" + "@webassemblyjs/leb128" "1.11.0" + "@webassemblyjs/utf8" "1.11.0" + +"@webassemblyjs/wasm-opt@1.11.0": + version "1.11.0" + resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-opt/-/wasm-opt-1.11.0.tgz#1638ae188137f4bb031f568a413cd24d32f92978" + integrity sha512-tHUSP5F4ywyh3hZ0+fDQuWxKx3mJiPeFufg+9gwTpYp324mPCQgnuVKwzLTZVqj0duRDovnPaZqDwoyhIO8kYg== + dependencies: + "@webassemblyjs/ast" "1.11.0" + "@webassemblyjs/helper-buffer" "1.11.0" + "@webassemblyjs/wasm-gen" "1.11.0" + "@webassemblyjs/wasm-parser" "1.11.0" + +"@webassemblyjs/wasm-parser@1.11.0": + version "1.11.0" + resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-parser/-/wasm-parser-1.11.0.tgz#3e680b8830d5b13d1ec86cc42f38f3d4a7700754" + integrity sha512-6L285Sgu9gphrcpDXINvm0M9BskznnzJTE7gYkjDbxET28shDqp27wpruyx3C2S/dvEwiigBwLA1cz7lNUi0kw== + dependencies: + "@webassemblyjs/ast" "1.11.0" + "@webassemblyjs/helper-api-error" "1.11.0" + "@webassemblyjs/helper-wasm-bytecode" "1.11.0" + "@webassemblyjs/ieee754" "1.11.0" + "@webassemblyjs/leb128" "1.11.0" + "@webassemblyjs/utf8" "1.11.0" + +"@webassemblyjs/wast-printer@1.11.0": + version "1.11.0" + resolved "https://registry.yarnpkg.com/@webassemblyjs/wast-printer/-/wast-printer-1.11.0.tgz#680d1f6a5365d6d401974a8e949e05474e1fab7e" + integrity sha512-Fg5OX46pRdTgB7rKIUojkh9vXaVN6sGYCnEiJN1GYkb0RPwShZXp6KTDqmoMdQPKhcroOXh3fEzmkWmCYaKYhQ== + dependencies: + "@webassemblyjs/ast" "1.11.0" + "@xtuc/long" "4.2.2" + +"@webpack-cli/configtest@^1.0.0": + version "1.0.0" + resolved "https://registry.yarnpkg.com/@webpack-cli/configtest/-/configtest-1.0.0.tgz#2aff5f1ebc6f793c13ba9b2a701d180eab17f5ee" + integrity sha512-Un0SdBoN1h4ACnIO7EiCjWuyhNI0Jl96JC+63q6xi4HDUYRZn8Auluea9D+v9NWKc5J4sICVEltdBaVjLX39xw== + +"@webpack-cli/info@^1.2.1": + version "1.2.1" + resolved "https://registry.yarnpkg.com/@webpack-cli/info/-/info-1.2.1.tgz#af98311f983d0b9fce7284cfcf1acaf1e9f4879c" + integrity sha512-fLnDML5HZ5AEKzHul8xLAksoKN2cibu6MgonkUj8R9V7bbeVRkd1XbGEGWrAUNYHbX1jcqCsDEpBviE5StPMzQ== + dependencies: + envinfo "^7.7.3" + +"@webpack-cli/serve@^1.2.2": + version "1.2.2" + resolved "https://registry.yarnpkg.com/@webpack-cli/serve/-/serve-1.2.2.tgz#1f8eee44f96524756268f5e3f43e9d943f864d41" + integrity sha512-03GkWxcgFfm8+WIwcsqJb9agrSDNDDoxaNnexPnCCexP5SCE4IgFd9lNpSy+K2nFqVMpgTFw6SwbmVAVTndVew== + +"@xtuc/ieee754@^1.2.0": + version "1.2.0" + resolved "https://registry.yarnpkg.com/@xtuc/ieee754/-/ieee754-1.2.0.tgz#eef014a3145ae477a1cbc00cd1e552336dceb790" + integrity sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA== + +"@xtuc/long@4.2.2": + version "4.2.2" + resolved "https://registry.yarnpkg.com/@xtuc/long/-/long-4.2.2.tgz#d291c6a4e97989b5c61d9acf396ae4fe133a718d" + integrity sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ== + +accepts@~1.3.4, accepts@~1.3.5, accepts@~1.3.7: + version "1.3.7" + resolved "https://registry.yarnpkg.com/accepts/-/accepts-1.3.7.tgz#531bc726517a3b2b41f850021c6cc15eaab507cd" + integrity sha512-Il80Qs2WjYlJIBNzNkK6KYqlVMTbZLXgHx2oT0pU/fjRHyEp+PEfEPY0R3WCwAGVOtauxh1hOxNgIf5bv7dQpA== + dependencies: + mime-types "~2.1.24" + negotiator "0.6.2" + +acorn-jsx@^5.3.1: + version "5.3.1" + resolved "https://registry.yarnpkg.com/acorn-jsx/-/acorn-jsx-5.3.1.tgz#fc8661e11b7ac1539c47dbfea2e72b3af34d267b" + integrity sha512-K0Ptm/47OKfQRpNQ2J/oIN/3QYiK6FwW+eJbILhsdxh2WTLdl+30o8aGdTbm5JbffpFFAg/g+zi1E+jvJha5ng== + +acorn@^7.4.0: + version "7.4.1" + resolved "https://registry.yarnpkg.com/acorn/-/acorn-7.4.1.tgz#feaed255973d2e77555b83dbc08851a6c63520fa" + integrity sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A== + +acorn@^8.0.4: + version "8.0.5" + resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.0.5.tgz#a3bfb872a74a6a7f661bc81b9849d9cac12601b7" + integrity sha512-v+DieK/HJkJOpFBETDJioequtc3PfxsWMaxIdIwujtF7FEV/MAyDQLlm6/zPvr7Mix07mLh6ccVwIsloceodlg== + +aggregate-error@^3.0.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/aggregate-error/-/aggregate-error-3.1.0.tgz#92670ff50f5359bdb7a3e0d40d0ec30c5737687a" + integrity sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA== + dependencies: + clean-stack "^2.0.0" + indent-string "^4.0.0" + +ajv-errors@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/ajv-errors/-/ajv-errors-1.0.1.tgz#f35986aceb91afadec4102fbd85014950cefa64d" + integrity sha512-DCRfO/4nQ+89p/RK43i8Ezd41EqdGIU4ld7nGF8OQ14oc/we5rEntLCUa7+jrn3nn83BosfwZA0wb4pon2o8iQ== + +ajv-keywords@^3.1.0, ajv-keywords@^3.5.2: + version "3.5.2" + resolved "https://registry.yarnpkg.com/ajv-keywords/-/ajv-keywords-3.5.2.tgz#31f29da5ab6e00d1c2d329acf7b5929614d5014d" + integrity sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ== + +ajv@^6.1.0, ajv@^6.10.0, ajv@^6.12.4, ajv@^6.12.5: + version "6.12.6" + resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.12.6.tgz#baf5a62e802b07d977034586f8c3baf5adf26df4" + integrity sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g== + dependencies: + fast-deep-equal "^3.1.1" + fast-json-stable-stringify "^2.0.0" + json-schema-traverse "^0.4.1" + uri-js "^4.2.2" + +ajv@^7.0.2: + version "7.0.3" + resolved "https://registry.yarnpkg.com/ajv/-/ajv-7.0.3.tgz#13ae747eff125cafb230ac504b2406cf371eece2" + integrity sha512-R50QRlXSxqXcQP5SvKUrw8VZeypvo12i2IX0EeR5PiZ7bEKeHWgzgo264LDadUsCU42lTJVhFikTqJwNeH34gQ== + dependencies: + fast-deep-equal "^3.1.1" + json-schema-traverse "^1.0.0" + require-from-string "^2.0.2" + uri-js "^4.2.2" + +alphanum-sort@^1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/alphanum-sort/-/alphanum-sort-1.0.2.tgz#97a1119649b211ad33691d9f9f486a8ec9fbe0a3" + integrity sha1-l6ERlkmyEa0zaR2fn0hqjsn74KM= + +ansi-colors@^3.0.0: + version "3.2.4" + resolved "https://registry.yarnpkg.com/ansi-colors/-/ansi-colors-3.2.4.tgz#e3a3da4bfbae6c86a9c285625de124a234026fbf" + integrity sha512-hHUXGagefjN2iRrID63xckIvotOXOojhQKWIPUZ4mNUZ9nLZW+7FMNoE1lOkEhNWYsx/7ysGIuJYCiMAA9FnrA== + +ansi-colors@^4.1.1: + version "4.1.1" + resolved "https://registry.yarnpkg.com/ansi-colors/-/ansi-colors-4.1.1.tgz#cbb9ae256bf750af1eab344f229aa27fe94ba348" + integrity sha512-JoX0apGbHaUJBNl6yF+p6JAFYZ666/hhCGKN5t9QFjbJQKUU/g8MNbFDbvfrgKXvI1QpZplPOnwIo99lX/AAmA== + +ansi-html@0.0.7: + version "0.0.7" + resolved "https://registry.yarnpkg.com/ansi-html/-/ansi-html-0.0.7.tgz#813584021962a9e9e6fd039f940d12f56ca7859e" + integrity sha1-gTWEAhliqenm/QOflA0S9WynhZ4= + +ansi-regex@^2.0.0: + version "2.1.1" + resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-2.1.1.tgz#c3b33ab5ee360d86e0e628f0468ae7ef27d654df" + integrity sha1-w7M6te42DYbg5ijwRorn7yfWVN8= + +ansi-regex@^4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-4.1.0.tgz#8b9f8f08cf1acb843756a839ca8c7e3168c51997" + integrity sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg== + +ansi-regex@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-5.0.0.tgz#388539f55179bf39339c81af30a654d69f87cb75" + integrity sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg== + +ansi-styles@^3.2.0, ansi-styles@^3.2.1: + version "3.2.1" + resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-3.2.1.tgz#41fbb20243e50b12be0f04b8dedbf07520ce841d" + integrity sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA== + dependencies: + color-convert "^1.9.0" + +ansi-styles@^4.0.0, ansi-styles@^4.1.0: + version "4.3.0" + resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-4.3.0.tgz#edd803628ae71c04c85ae7a0906edad34b648937" + integrity sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg== + dependencies: + color-convert "^2.0.1" + +antd-dayjs-webpack-plugin@^1.0.1: + version "1.0.6" + resolved "https://registry.yarnpkg.com/antd-dayjs-webpack-plugin/-/antd-dayjs-webpack-plugin-1.0.6.tgz#7d98bcb51422248b8cd4a32e352a0425a3bffa3a" + integrity sha512-UlK3BfA0iE2c5+Zz/Bd2iPAkT6cICtrKG4/swSik5MZweBHtgmu1aUQCHvICdiv39EAShdZy/edfP6mlkS/xXg== + +antd@^4.7.2: + version "4.11.2" + resolved "https://registry.yarnpkg.com/antd/-/antd-4.11.2.tgz#28c20409e2d186d8915cdc6eacf1cfb2b82d9b5b" + integrity sha512-cdjPRlmamETae6c2uvQHRXDN5/T7I/zPiByaeolbq/FRG14JYv9hyUaydXI7n4s6rynPQ2Q6bFdCQ+/r9xZYbA== + dependencies: + "@ant-design/colors" "^5.0.0" + "@ant-design/icons" "^4.4.0" + "@ant-design/react-slick" "~0.28.1" + "@babel/runtime" "^7.11.2" + array-tree-filter "^2.1.0" + classnames "^2.2.6" + copy-to-clipboard "^3.2.0" + lodash "^4.17.20" + moment "^2.25.3" + rc-cascader "~1.4.0" + rc-checkbox "~2.3.0" + rc-collapse "~3.1.0" + rc-dialog "~8.5.1" + rc-drawer "~4.2.0" + rc-dropdown "~3.2.0" + rc-field-form "~1.18.0" + rc-image "~5.1.1" + rc-input-number "~6.1.0" + rc-mentions "~1.5.0" + rc-menu "~8.10.0" + rc-motion "^2.4.0" + rc-notification "~4.5.2" + rc-pagination "~3.1.2" + rc-picker "~2.5.1" + rc-progress "~3.1.0" + rc-rate "~2.9.0" + rc-resize-observer "^1.0.0" + rc-select "~12.1.0" + rc-slider "~9.7.1" + rc-steps "~4.1.0" + rc-switch "~3.2.0" + rc-table "~7.12.0" + rc-tabs "~11.7.0" + rc-textarea "~0.3.0" + rc-tooltip "~5.0.0" + rc-tree "~4.1.0" + rc-tree-select "~4.3.0" + rc-trigger "^5.2.1" + rc-upload "~3.3.4" + rc-util "^5.7.0" + scroll-into-view-if-needed "^2.2.25" + warning "^4.0.3" + +anymatch@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/anymatch/-/anymatch-2.0.0.tgz#bcb24b4f37934d9aa7ac17b4adaf89e7c76ef2eb" + integrity sha512-5teOsQWABXHHBFP9y3skS5P3d/WfWXpv3FUpy+LorMrNYaT9pI4oLMQX7jzQ2KklNpGpWHzdCXTDT2Y3XGlZBw== + dependencies: + micromatch "^3.1.4" + normalize-path "^2.1.1" + +arg@^4.1.0: + version "4.1.3" + resolved "https://registry.yarnpkg.com/arg/-/arg-4.1.3.tgz#269fc7ad5b8e42cb63c896d5666017261c144089" + integrity sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA== + +argparse@^1.0.7: + version "1.0.10" + resolved "https://registry.yarnpkg.com/argparse/-/argparse-1.0.10.tgz#bcd6791ea5ae09725e17e5ad988134cd40b3d911" + integrity sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg== + dependencies: + sprintf-js "~1.0.2" + +arr-diff@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/arr-diff/-/arr-diff-4.0.0.tgz#d6461074febfec71e7e15235761a329a5dc7c520" + integrity sha1-1kYQdP6/7HHn4VI1dhoyml3HxSA= + +arr-flatten@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/arr-flatten/-/arr-flatten-1.1.0.tgz#36048bbff4e7b47e136644316c99669ea5ae91f1" + integrity sha512-L3hKV5R/p5o81R7O02IGnwpDmkp6E982XhtbuwSe3O4qOtMMMtodicASA1Cny2U+aCXcNpml+m4dPsvsJ3jatg== + +arr-union@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/arr-union/-/arr-union-3.1.0.tgz#e39b09aea9def866a8f206e288af63919bae39c4" + integrity sha1-45sJrqne+Gao8gbiiK9jkZuuOcQ= + +array-differ@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/array-differ/-/array-differ-3.0.0.tgz#3cbb3d0f316810eafcc47624734237d6aee4ae6b" + integrity sha512-THtfYS6KtME/yIAhKjZ2ul7XI96lQGHRputJQHO80LAWQnuGP4iCIN8vdMRboGbIEYBwU33q8Tch1os2+X0kMg== + +array-flatten@1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/array-flatten/-/array-flatten-1.1.1.tgz#9a5f699051b1e7073328f2a008968b64ea2955d2" + integrity sha1-ml9pkFGx5wczKPKgCJaLZOopVdI= + +array-flatten@^2.1.0: + version "2.1.2" + resolved "https://registry.yarnpkg.com/array-flatten/-/array-flatten-2.1.2.tgz#24ef80a28c1a893617e2149b0c6d0d788293b099" + integrity sha512-hNfzcOV8W4NdualtqBFPyVO+54DSJuZGY9qT4pRroB6S9e3iiido2ISIC5h9R2sPJ8H3FHCIiEnsv1lPXO3KtQ== + +array-includes@^3.1.1, array-includes@^3.1.2: + version "3.1.2" + resolved "https://registry.yarnpkg.com/array-includes/-/array-includes-3.1.2.tgz#a8db03e0b88c8c6aeddc49cb132f9bcab4ebf9c8" + integrity sha512-w2GspexNQpx+PutG3QpT437/BenZBj0M/MZGn5mzv/MofYqo0xmRHzn4lFsoDlWJ+THYsGJmFlW68WlDFx7VRw== + dependencies: + call-bind "^1.0.0" + define-properties "^1.1.3" + es-abstract "^1.18.0-next.1" + get-intrinsic "^1.0.1" + is-string "^1.0.5" + +array-tree-filter@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/array-tree-filter/-/array-tree-filter-2.1.0.tgz#873ac00fec83749f255ac8dd083814b4f6329190" + integrity sha512-4ROwICNlNw/Hqa9v+rk5h22KjmzB1JGTMVKP2AKJBOCgb0yL0ASf0+YvCcLNNwquOHNX48jkeZIJ3a+oOQqKcw== + +array-union@^1.0.1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/array-union/-/array-union-1.0.2.tgz#9a34410e4f4e3da23dea375be5be70f24778ec39" + integrity sha1-mjRBDk9OPaI96jdb5b5w8kd47Dk= + dependencies: + array-uniq "^1.0.1" + +array-union@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/array-union/-/array-union-2.1.0.tgz#b798420adbeb1de828d84acd8a2e23d3efe85e8d" + integrity sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw== + +array-uniq@^1.0.1: + version "1.0.3" + resolved "https://registry.yarnpkg.com/array-uniq/-/array-uniq-1.0.3.tgz#af6ac877a25cc7f74e058894753858dfdb24fdb6" + integrity sha1-r2rId6Jcx/dOBYiUdThY39sk/bY= + +array-unique@^0.3.2: + version "0.3.2" + resolved "https://registry.yarnpkg.com/array-unique/-/array-unique-0.3.2.tgz#a894b75d4bc4f6cd679ef3244a9fd8f46ae2d428" + integrity sha1-qJS3XUvE9s1nnvMkSp/Y9Gri1Cg= + +array.prototype.flat@^1.2.3: + version "1.2.4" + resolved "https://registry.yarnpkg.com/array.prototype.flat/-/array.prototype.flat-1.2.4.tgz#6ef638b43312bd401b4c6199fdec7e2dc9e9a123" + integrity sha512-4470Xi3GAPAjZqFcljX2xzckv1qeKPizoNkiS0+O4IoPR2ZNpcjE0pkhdihlDouK+x6QOast26B4Q/O9DJnwSg== + dependencies: + call-bind "^1.0.0" + define-properties "^1.1.3" + es-abstract "^1.18.0-next.1" + +array.prototype.flatmap@^1.2.3: + version "1.2.4" + resolved "https://registry.yarnpkg.com/array.prototype.flatmap/-/array.prototype.flatmap-1.2.4.tgz#94cfd47cc1556ec0747d97f7c7738c58122004c9" + integrity sha512-r9Z0zYoxqHz60vvQbWEdXIEtCwHF0yxaWfno9qzXeNHvfyl3BZqygmGzb84dsubyaXLH4husF+NFgMSdpZhk2Q== + dependencies: + call-bind "^1.0.0" + define-properties "^1.1.3" + es-abstract "^1.18.0-next.1" + function-bind "^1.1.1" + +arrify@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/arrify/-/arrify-1.0.1.tgz#898508da2226f380df904728456849c1501a4b0d" + integrity sha1-iYUI2iIm84DfkEcoRWhJwVAaSw0= + +arrify@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/arrify/-/arrify-2.0.1.tgz#c9655e9331e0abcd588d2a7cad7e9956f66701fa" + integrity sha512-3duEwti880xqi4eAMN8AyR4a0ByT90zoYdLlevfrvU43vb0YZwZVfxOgxWrLXXXpyugL0hNZc9G6BiB5B3nUug== + +assign-symbols@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/assign-symbols/-/assign-symbols-1.0.0.tgz#59667f41fadd4f20ccbc2bb96b8d4f7f78ec0367" + integrity sha1-WWZ/QfrdTyDMvCu5a41Pf3jsA2c= + +astral-regex@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/astral-regex/-/astral-regex-2.0.0.tgz#483143c567aeed4785759c0865786dc77d7d2e31" + integrity sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ== + +async-each@^1.0.1: + version "1.0.3" + resolved "https://registry.yarnpkg.com/async-each/-/async-each-1.0.3.tgz#b727dbf87d7651602f06f4d4ac387f47d91b0cbf" + integrity sha512-z/WhQ5FPySLdvREByI2vZiTWwCnF0moMJ1hK9YQwDTHKh6I7/uSckMetoRGb5UBZPC1z0jlw+n/XCgjeH7y1AQ== + +async-limiter@~1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/async-limiter/-/async-limiter-1.0.1.tgz#dd379e94f0db8310b08291f9d64c3209766617fd" + integrity sha512-csOlWGAcRFJaI6m+F2WKdnMKr4HhdhFVBk0H/QbJFMCr+uO2kwohwXQPxw/9OCxp05r5ghVBFSyioixx3gfkNQ== + +async-validator@^3.0.3: + version "3.5.1" + resolved "https://registry.yarnpkg.com/async-validator/-/async-validator-3.5.1.tgz#cd62b9688b2465f48420e27adb47760ab1b5559f" + integrity sha512-DDmKA7sdSAJtTVeNZHrnr2yojfFaoeW8MfQN8CeuXg8DDQHTqKk9Fdv38dSvnesHoO8MUwMI2HphOeSyIF+wmQ== + +async@^2.6.2: + version "2.6.3" + resolved "https://registry.yarnpkg.com/async/-/async-2.6.3.tgz#d72625e2344a3656e3a3ad4fa749fa83299d82ff" + integrity sha512-zflvls11DCy+dQWzTW2dzuilv8Z5X/pjfmZOWba6TNIVDm+2UDaJmXSOXlasHKfNBs8oo3M0aT50fDEWfKZjXg== + dependencies: + lodash "^4.17.14" + +at-least-node@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/at-least-node/-/at-least-node-1.0.0.tgz#602cd4b46e844ad4effc92a8011a3c46e0238dc2" + integrity sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg== + +atob@^2.1.2: + version "2.1.2" + resolved "https://registry.yarnpkg.com/atob/-/atob-2.1.2.tgz#6d9517eb9e030d2436666651e86bd9f6f13533c9" + integrity sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg== + +autoprefixer@^10.0.1: + version "10.2.3" + resolved "https://registry.yarnpkg.com/autoprefixer/-/autoprefixer-10.2.3.tgz#2834b55b75cfc10fa80c66000a66dc94b7136804" + integrity sha512-vlz+iv+EnLkVaTgX8wApfYzmK3LUfK8Z9XAnmflzxMy/+oFuNK8fVGQV79SOpBv4jxk2YQJimw4hXIKZ29570A== + dependencies: + browserslist "^4.16.1" + caniuse-lite "^1.0.30001178" + colorette "^1.2.1" + fraction.js "^4.0.13" + normalize-range "^0.1.2" + postcss-value-parser "^4.1.0" + +autoprefixer@^9.6.1, autoprefixer@^9.8.6: + version "9.8.6" + resolved "https://registry.yarnpkg.com/autoprefixer/-/autoprefixer-9.8.6.tgz#3b73594ca1bf9266320c5acf1588d74dea74210f" + integrity sha512-XrvP4VVHdRBCdX1S3WXVD8+RyG9qeb1D5Sn1DeLiG2xfSpzellk5k54xbUERJ3M5DggQxes39UGOTP8CFrEGbg== + dependencies: + browserslist "^4.12.0" + caniuse-lite "^1.0.30001109" + colorette "^1.2.1" + normalize-range "^0.1.2" + num2fraction "^1.2.2" + postcss "^7.0.32" + postcss-value-parser "^4.1.0" + +bail@^1.0.0: + version "1.0.5" + resolved "https://registry.yarnpkg.com/bail/-/bail-1.0.5.tgz#b6fa133404a392cbc1f8c4bf63f5953351e7a776" + integrity sha512-xFbRxM1tahm08yHBP16MMjVUAvDaBMD38zsM9EMAUN61omwLmKlOpB/Zku5QkjZ8TZ4vn53pj+t518cH0S03RQ== + +balanced-match@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.0.tgz#89b4d199ab2bee49de164ea02b89ce462d71b767" + integrity sha1-ibTRmasr7kneFk6gK4nORi1xt2c= + +base@^0.11.1: + version "0.11.2" + resolved "https://registry.yarnpkg.com/base/-/base-0.11.2.tgz#7bde5ced145b6d551a90db87f83c558b4eb48a8f" + integrity sha512-5T6P4xPgpp0YDFvSWwEZ4NoE3aM4QBQXDzmVbraCkFj8zHM+mba8SyqB5DbZWyR7mYHo6Y7BdQo3MoA4m0TeQg== + dependencies: + cache-base "^1.0.1" + class-utils "^0.3.5" + component-emitter "^1.2.1" + define-property "^1.0.0" + isobject "^3.0.1" + mixin-deep "^1.2.0" + pascalcase "^0.1.1" + +batch@0.6.1: + version "0.6.1" + resolved "https://registry.yarnpkg.com/batch/-/batch-0.6.1.tgz#dc34314f4e679318093fc760272525f94bf25c16" + integrity sha1-3DQxT05nkxgJP8dgJyUl+UvyXBY= + +big.js@^5.2.2: + version "5.2.2" + resolved "https://registry.yarnpkg.com/big.js/-/big.js-5.2.2.tgz#65f0af382f578bcdc742bd9c281e9cb2d7768328" + integrity sha512-vyL2OymJxmarO8gxMr0mhChsO9QGwhynfuu4+MHTAW6czfq9humCB7rKpUjDd9YUiDPU4mzpyupFSvOClAwbmQ== + +binary-extensions@^1.0.0: + version "1.13.1" + resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-1.13.1.tgz#598afe54755b2868a5330d2aff9d4ebb53209b65" + integrity sha512-Un7MIEDdUC5gNpcGDV97op1Ywk748MpHcFTHoYs6qnj1Z3j7I53VG3nwZhKzoBZmbdRNnb6WRdFlwl7tSDuZGw== + +bindings@^1.5.0: + version "1.5.0" + resolved "https://registry.yarnpkg.com/bindings/-/bindings-1.5.0.tgz#10353c9e945334bc0511a6d90b38fbc7c9c504df" + integrity sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ== + dependencies: + file-uri-to-path "1.0.0" + +body-parser@1.19.0: + version "1.19.0" + resolved "https://registry.yarnpkg.com/body-parser/-/body-parser-1.19.0.tgz#96b2709e57c9c4e09a6fd66a8fd979844f69f08a" + integrity sha512-dhEPs72UPbDnAQJ9ZKMNTP6ptJaionhP5cBb541nXPlW60Jepo9RV/a4fX4XWW9CuFNK22krhrj1+rgzifNCsw== + dependencies: + bytes "3.1.0" + content-type "~1.0.4" + debug "2.6.9" + depd "~1.1.2" + http-errors "1.7.2" + iconv-lite "0.4.24" + on-finished "~2.3.0" + qs "6.7.0" + raw-body "2.4.0" + type-is "~1.6.17" + +bonjour@^3.5.0: + version "3.5.0" + resolved "https://registry.yarnpkg.com/bonjour/-/bonjour-3.5.0.tgz#8e890a183d8ee9a2393b3844c691a42bcf7bc9f5" + integrity sha1-jokKGD2O6aI5OzhExpGkK897yfU= + dependencies: + array-flatten "^2.1.0" + deep-equal "^1.0.1" + dns-equal "^1.0.0" + dns-txt "^2.0.2" + multicast-dns "^6.0.1" + multicast-dns-service-types "^1.1.0" + +boolbase@^1.0.0, boolbase@~1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/boolbase/-/boolbase-1.0.0.tgz#68dff5fbe60c51eb37725ea9e3ed310dcc1e776e" + integrity sha1-aN/1++YMUes3cl6p4+0xDcwed24= + +brace-expansion@^1.1.7: + version "1.1.11" + resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.11.tgz#3c7fcbf529d87226f3d2f52b966ff5271eb441dd" + integrity sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA== + dependencies: + balanced-match "^1.0.0" + concat-map "0.0.1" + +braces@^2.3.1, braces@^2.3.2: + version "2.3.2" + resolved "https://registry.yarnpkg.com/braces/-/braces-2.3.2.tgz#5979fd3f14cd531565e5fa2df1abfff1dfaee729" + integrity sha512-aNdbnj9P8PjdXU4ybaWLK2IF3jc/EoDYbC7AazW6to3TRsfXxscC9UXOB5iDiEQrkyIbWp2SLQda4+QAa7nc3w== + dependencies: + arr-flatten "^1.1.0" + array-unique "^0.3.2" + extend-shallow "^2.0.1" + fill-range "^4.0.0" + isobject "^3.0.1" + repeat-element "^1.1.2" + snapdragon "^0.8.1" + snapdragon-node "^2.0.1" + split-string "^3.0.2" + to-regex "^3.0.1" + +braces@^3.0.1: + version "3.0.2" + resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.2.tgz#3454e1a462ee8d599e236df336cd9ea4f8afe107" + integrity sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A== + dependencies: + fill-range "^7.0.1" + +browserslist@^4.0.0, browserslist@^4.12.0, browserslist@^4.14.5, browserslist@^4.16.1, browserslist@^4.6.4: + version "4.16.1" + resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.16.1.tgz#bf757a2da376b3447b800a16f0f1c96358138766" + integrity sha512-UXhDrwqsNcpTYJBTZsbGATDxZbiVDsx6UjpmRUmtnP10pr8wAYr5LgFoEFw9ixriQH2mv/NX2SfGzE/o8GndLA== + dependencies: + caniuse-lite "^1.0.30001173" + colorette "^1.2.1" + electron-to-chromium "^1.3.634" + escalade "^3.1.1" + node-releases "^1.1.69" + +buffer-from@^1.0.0: + version "1.1.1" + resolved "https://registry.yarnpkg.com/buffer-from/-/buffer-from-1.1.1.tgz#32713bc028f75c02fdb710d7c7bcec1f2c6070ef" + integrity sha512-MQcXEUbCKtEo7bhqEs6560Hyd4XaovZlO/k9V3hjVUF/zwW7KBVdSK4gIt/bzwS9MbR5qob+F5jusZsb0YQK2A== + +buffer-indexof@^1.0.0: + version "1.1.1" + resolved "https://registry.yarnpkg.com/buffer-indexof/-/buffer-indexof-1.1.1.tgz#52fabcc6a606d1a00302802648ef68f639da268c" + integrity sha512-4/rOEg86jivtPTeOUUT61jJO1Ya1TrR/OkqCSZDyq84WJh3LuuiphBYJN+fm5xufIk4XAFcEwte/8WzC8If/1g== + +bytes@3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/bytes/-/bytes-3.0.0.tgz#d32815404d689699f85a4ea4fa8755dd13a96048" + integrity sha1-0ygVQE1olpn4Wk6k+odV3ROpYEg= + +bytes@3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/bytes/-/bytes-3.1.0.tgz#f6cf7933a360e0588fa9fde85651cdc7f805d1f6" + integrity sha512-zauLjrfCG+xvoyaqLoV8bLVXXNGC4JqlxFCutSDWA6fJrTo2ZuvLYTqZ7aHBLZSMOopbzwv8f+wZcVzfVTI2Dg== + +cacache@^15.0.5: + version "15.0.5" + resolved "https://registry.yarnpkg.com/cacache/-/cacache-15.0.5.tgz#69162833da29170d6732334643c60e005f5f17d0" + integrity sha512-lloiL22n7sOjEEXdL8NAjTgv9a1u43xICE9/203qonkZUCj5X1UEWIdf2/Y0d6QcCtMzbKQyhrcDbdvlZTs/+A== + dependencies: + "@npmcli/move-file" "^1.0.1" + chownr "^2.0.0" + fs-minipass "^2.0.0" + glob "^7.1.4" + infer-owner "^1.0.4" + lru-cache "^6.0.0" + minipass "^3.1.1" + minipass-collect "^1.0.2" + minipass-flush "^1.0.5" + minipass-pipeline "^1.2.2" + mkdirp "^1.0.3" + p-map "^4.0.0" + promise-inflight "^1.0.1" + rimraf "^3.0.2" + ssri "^8.0.0" + tar "^6.0.2" + unique-filename "^1.1.1" + +cache-base@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/cache-base/-/cache-base-1.0.1.tgz#0a7f46416831c8b662ee36fe4e7c59d76f666ab2" + integrity sha512-AKcdTnFSWATd5/GCPRxr2ChwIJ85CeyrEyjRHlKxQ56d4XJMGym0uAiKn0xbLOGOl3+yRpOTi484dVCEc5AUzQ== + dependencies: + collection-visit "^1.0.0" + component-emitter "^1.2.1" + get-value "^2.0.6" + has-value "^1.0.0" + isobject "^3.0.1" + set-value "^2.0.0" + to-object-path "^0.3.0" + union-value "^1.0.0" + unset-value "^1.0.0" + +call-bind@^1.0.0, call-bind@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/call-bind/-/call-bind-1.0.2.tgz#b1d4e89e688119c3c9a903ad30abb2f6a919be3c" + integrity sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA== + dependencies: + function-bind "^1.1.1" + get-intrinsic "^1.0.2" + +caller-callsite@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/caller-callsite/-/caller-callsite-2.0.0.tgz#847e0fce0a223750a9a027c54b33731ad3154134" + integrity sha1-hH4PzgoiN1CpoCfFSzNzGtMVQTQ= + dependencies: + callsites "^2.0.0" + +caller-path@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/caller-path/-/caller-path-2.0.0.tgz#468f83044e369ab2010fac5f06ceee15bb2cb1f4" + integrity sha1-Ro+DBE42mrIBD6xfBs7uFbsssfQ= + dependencies: + caller-callsite "^2.0.0" + +callsites@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/callsites/-/callsites-2.0.0.tgz#06eb84f00eea413da86affefacbffb36093b3c50" + integrity sha1-BuuE8A7qQT2oav/vrL/7Ngk7PFA= + +callsites@^3.0.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/callsites/-/callsites-3.1.0.tgz#b3630abd8943432f54b3f0519238e33cd7df2f73" + integrity sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ== + +camel-case@^4.1.1: + version "4.1.2" + resolved "https://registry.yarnpkg.com/camel-case/-/camel-case-4.1.2.tgz#9728072a954f805228225a6deea6b38461e1bd5a" + integrity sha512-gxGWBrTT1JuMx6R+o5PTXMmUnhnVzLQ9SNutD4YqKtI6ap897t3tKECYla6gCWEkplXnlNybEkZg9GEGxKFCgw== + dependencies: + pascal-case "^3.1.2" + tslib "^2.0.3" + +camelcase-css@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/camelcase-css/-/camelcase-css-2.0.1.tgz#ee978f6947914cc30c6b44741b6ed1df7f043fd5" + integrity sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA== + +camelcase-keys@^6.2.2: + version "6.2.2" + resolved "https://registry.yarnpkg.com/camelcase-keys/-/camelcase-keys-6.2.2.tgz#5e755d6ba51aa223ec7d3d52f25778210f9dc3c0" + integrity sha512-YrwaA0vEKazPBkn0ipTiMpSajYDSe+KjQfrjhcBMxJt/znbvlHd8Pw/Vamaz5EB4Wfhs3SUR3Z9mwRu/P3s3Yg== + dependencies: + camelcase "^5.3.1" + map-obj "^4.0.0" + quick-lru "^4.0.1" + +camelcase@^5.0.0, camelcase@^5.3.1: + version "5.3.1" + resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-5.3.1.tgz#e3c9b31569e106811df242f715725a1f4c494320" + integrity sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg== + +camelcase@^6.2.0: + version "6.2.0" + resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-6.2.0.tgz#924af881c9d525ac9d87f40d964e5cea982a1809" + integrity sha512-c7wVvbw3f37nuobQNtgsgG9POC9qMbNuMQmTCqZv23b6MIz0fcYpBiOlv9gEN/hdLdnZTDQhg6e9Dq5M1vKvfg== + +caniuse-api@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/caniuse-api/-/caniuse-api-3.0.0.tgz#5e4d90e2274961d46291997df599e3ed008ee4c0" + integrity sha512-bsTwuIg/BZZK/vreVTYYbSWoe2F+71P7K5QGEX+pT250DZbfU1MQ5prOKpPR+LL6uWKK3KMwMCAS74QB3Um1uw== + dependencies: + browserslist "^4.0.0" + caniuse-lite "^1.0.0" + lodash.memoize "^4.1.2" + lodash.uniq "^4.5.0" + +caniuse-lite@^1.0.0, caniuse-lite@^1.0.30000981, caniuse-lite@^1.0.30001109, caniuse-lite@^1.0.30001173, caniuse-lite@^1.0.30001178: + version "1.0.30001180" + resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001180.tgz#67abcd6d1edf48fa5e7d1e84091d1d65ab76e33b" + integrity sha512-n8JVqXuZMVSPKiPiypjFtDTXc4jWIdjxull0f92WLo7e1MSi3uJ3NvveakSh/aCl1QKFAvIz3vIj0v+0K+FrXw== + +chalk@^2.0.0, chalk@^2.4.1, chalk@^2.4.2: + version "2.4.2" + resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.4.2.tgz#cd42541677a54333cf541a49108c1432b44c9424" + integrity sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ== + dependencies: + ansi-styles "^3.2.1" + escape-string-regexp "^1.0.5" + supports-color "^5.3.0" + +chalk@^4.0.0, chalk@^4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/chalk/-/chalk-4.1.0.tgz#4e14870a618d9e2edd97dd8345fd9d9dc315646a" + integrity sha512-qwx12AxXe2Q5xQ43Ac//I6v5aXTipYrSESdOgzrN+9XjgEpyjpKuvSGaN4qE93f7TQTlerQQ8S+EQ0EyDoVL1A== + dependencies: + ansi-styles "^4.1.0" + supports-color "^7.1.0" + +character-entities-legacy@^1.0.0: + version "1.1.4" + resolved "https://registry.yarnpkg.com/character-entities-legacy/-/character-entities-legacy-1.1.4.tgz#94bc1845dce70a5bb9d2ecc748725661293d8fc1" + integrity sha512-3Xnr+7ZFS1uxeiUDvV02wQ+QDbc55o97tIV5zHScSPJpcLm/r0DFPcoY3tYRp+VZukxuMeKgXYmsXQHO05zQeA== + +character-entities@^1.0.0: + version "1.2.4" + resolved "https://registry.yarnpkg.com/character-entities/-/character-entities-1.2.4.tgz#e12c3939b7eaf4e5b15e7ad4c5e28e1d48c5b16b" + integrity sha512-iBMyeEHxfVnIakwOuDXpVkc54HijNgCyQB2w0VfGQThle6NXn50zU6V/u+LDhxHcDUPojn6Kpga3PTAD8W1bQw== + +character-reference-invalid@^1.0.0: + version "1.1.4" + resolved "https://registry.yarnpkg.com/character-reference-invalid/-/character-reference-invalid-1.1.4.tgz#083329cda0eae272ab3dbbf37e9a382c13af1560" + integrity sha512-mKKUkUbhPpQlCOfIuZkvSEgktjPFIsZKRRbC6KWVEMvlzblj3i3asQv5ODsrwt0N3pHAEvjP8KTQPHkp0+6jOg== + +chokidar@^2.1.8: + version "2.1.8" + resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-2.1.8.tgz#804b3a7b6a99358c3c5c61e71d8728f041cff917" + integrity sha512-ZmZUazfOzf0Nve7duiCKD23PFSCs4JPoYyccjUFF3aQkQadqBhfzhjkwBH2mNOG9cTBwhamM37EIsIkZw3nRgg== + dependencies: + anymatch "^2.0.0" + async-each "^1.0.1" + braces "^2.3.2" + glob-parent "^3.1.0" + inherits "^2.0.3" + is-binary-path "^1.0.0" + is-glob "^4.0.0" + normalize-path "^3.0.0" + path-is-absolute "^1.0.0" + readdirp "^2.2.1" + upath "^1.1.1" + optionalDependencies: + fsevents "^1.2.7" + +chownr@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/chownr/-/chownr-2.0.0.tgz#15bfbe53d2eab4cf70f18a8cd68ebe5b3cb1dece" + integrity sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ== + +chrome-trace-event@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/chrome-trace-event/-/chrome-trace-event-1.0.2.tgz#234090ee97c7d4ad1a2c4beae27505deffc608a4" + integrity sha512-9e/zx1jw7B4CO+c/RXoCsfg/x1AfUBioy4owYH0bJprEYAx5hRFLRhWBqHAG57D0ZM4H7vxbP7bPe0VwhQRYDQ== + dependencies: + tslib "^1.9.0" + +class-utils@^0.3.5: + version "0.3.6" + resolved "https://registry.yarnpkg.com/class-utils/-/class-utils-0.3.6.tgz#f93369ae8b9a7ce02fd41faad0ca83033190c463" + integrity sha512-qOhPa/Fj7s6TY8H8esGu5QNpMMQxz79h+urzrNYN6mn+9BnxlDGf5QZ+XeCDsxSjPqsSR56XOZOJmpeurnLMeg== + dependencies: + arr-union "^3.1.0" + define-property "^0.2.5" + isobject "^3.0.0" + static-extend "^0.1.1" + +classnames@2.x, classnames@^2.2.1, classnames@^2.2.3, classnames@^2.2.5, classnames@^2.2.6: + version "2.2.6" + resolved "https://registry.yarnpkg.com/classnames/-/classnames-2.2.6.tgz#43935bffdd291f326dad0a205309b38d00f650ce" + integrity sha512-JR/iSQOSt+LQIWwrwEzJ9uk0xfN3mTVYMwt1Ir5mUcSN6pU+V4zQFFaJsclJbPuAUQH+yfWef6tm7l1quW3C8Q== + +clean-css@^4.2.3: + version "4.2.3" + resolved "https://registry.yarnpkg.com/clean-css/-/clean-css-4.2.3.tgz#507b5de7d97b48ee53d84adb0160ff6216380f78" + integrity sha512-VcMWDN54ZN/DS+g58HYL5/n4Zrqe8vHJpGA8KdgUXFU4fuP/aHNw8eld9SyEIyabIMJX/0RaY/fplOo5hYLSFA== + dependencies: + source-map "~0.6.0" + +clean-stack@^2.0.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/clean-stack/-/clean-stack-2.2.0.tgz#ee8472dbb129e727b31e8a10a427dee9dfe4008b" + integrity sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A== + +cliui@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/cliui/-/cliui-5.0.0.tgz#deefcfdb2e800784aa34f46fa08e06851c7bbbc5" + integrity sha512-PYeGSEmmHM6zvoef2w8TPzlrnNpXIjTipYK780YswmIP9vjxmd6Y2a3CB2Ks6/AU8NHjZugXvo8w3oWM2qnwXA== + dependencies: + string-width "^3.1.0" + strip-ansi "^5.2.0" + wrap-ansi "^5.1.0" + +clone-deep@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/clone-deep/-/clone-deep-4.0.1.tgz#c19fd9bdbbf85942b4fd979c84dcf7d5f07c2387" + integrity sha512-neHB9xuzh/wk0dIHweyAXv2aPGZIVk3pLMe+/RNzINf17fe0OG96QroktYAUm7SM1PBnzTabaLboqqxDyMU+SQ== + dependencies: + is-plain-object "^2.0.4" + kind-of "^6.0.2" + shallow-clone "^3.0.0" + +clone-regexp@^2.1.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/clone-regexp/-/clone-regexp-2.2.0.tgz#7d65e00885cd8796405c35a737e7a86b7429e36f" + integrity sha512-beMpP7BOtTipFuW8hrJvREQ2DrRu3BE7by0ZpibtfBA+qfHYvMGTc2Yb1JMYPKg/JUw0CHYvpg796aNTSW9z7Q== + dependencies: + is-regexp "^2.0.0" + +clone@^2.1.1: + version "2.1.2" + resolved "https://registry.yarnpkg.com/clone/-/clone-2.1.2.tgz#1b7f4b9f591f1e8f83670401600345a02887435f" + integrity sha1-G39Ln1kfHo+DZwQBYANFoCiHQ18= + +coa@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/coa/-/coa-2.0.2.tgz#43f6c21151b4ef2bf57187db0d73de229e3e7ec3" + integrity sha512-q5/jG+YQnSy4nRTV4F7lPepBJZ8qBNJJDBuJdoejDyLXgmL7IEo+Le2JDZudFTFt7mrCqIRaSjws4ygRCTCAXA== + dependencies: + "@types/q" "^1.5.1" + chalk "^2.4.1" + q "^1.1.2" + +code-block-writer@^10.1.0: + version "10.1.1" + resolved "https://registry.yarnpkg.com/code-block-writer/-/code-block-writer-10.1.1.tgz#ad5684ed4bfb2b0783c8b131281ae84ee640a42f" + integrity sha512-67ueh2IRGst/51p0n6FvPrnRjAGHY5F8xdjkgrYE7DDzpJe6qA07RYQ9VcoUeo5ATOjSOiWpSL3SWBRRbempMw== + +collection-visit@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/collection-visit/-/collection-visit-1.0.0.tgz#4bc0373c164bc3291b4d368c829cf1a80a59dca0" + integrity sha1-S8A3PBZLwykbTTaMgpzxqApZ3KA= + dependencies: + map-visit "^1.0.0" + object-visit "^1.0.0" + +color-convert@^1.9.0, color-convert@^1.9.1: + version "1.9.3" + resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-1.9.3.tgz#bb71850690e1f136567de629d2d5471deda4c1e8" + integrity sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg== + dependencies: + color-name "1.1.3" + +color-convert@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-2.0.1.tgz#72d3a68d598c9bdb3af2ad1e84f21d896abd4de3" + integrity sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ== + dependencies: + color-name "~1.1.4" + +color-name@1.1.3: + version "1.1.3" + resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.3.tgz#a7d0558bd89c42f795dd42328f740831ca53bc25" + integrity sha1-p9BVi9icQveV3UIyj3QIMcpTvCU= + +color-name@^1.0.0, color-name@~1.1.4: + version "1.1.4" + resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.4.tgz#c2a09a87acbde69543de6f63fa3995c826c536a2" + integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA== + +color-string@^1.5.4: + version "1.5.4" + resolved "https://registry.yarnpkg.com/color-string/-/color-string-1.5.4.tgz#dd51cd25cfee953d138fe4002372cc3d0e504cb6" + integrity sha512-57yF5yt8Xa3czSEW1jfQDE79Idk0+AkN/4KWad6tbdxUmAs3MvjxlWSWD4deYytcRfoZ9nhKyFl1kj5tBvidbw== + dependencies: + color-name "^1.0.0" + simple-swizzle "^0.2.2" + +color@^3.0.0: + version "3.1.3" + resolved "https://registry.yarnpkg.com/color/-/color-3.1.3.tgz#ca67fb4e7b97d611dcde39eceed422067d91596e" + integrity sha512-xgXAcTHa2HeFCGLE9Xs/R82hujGtu9Jd9x4NW3T34+OMs7VoPsjwzRczKHvTAHeJwWFwX5j15+MgAppE8ztObQ== + dependencies: + color-convert "^1.9.1" + color-string "^1.5.4" + +colorette@^1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/colorette/-/colorette-1.2.1.tgz#4d0b921325c14faf92633086a536db6e89564b1b" + integrity sha512-puCDz0CzydiSYOrnXpz/PKd69zRrribezjtE9yd4zvytoRc8+RY/KJPvtPFKZS3E3wP6neGyMe0vOTlHO5L3Pw== + +commander@^2.20.0: + version "2.20.3" + resolved "https://registry.yarnpkg.com/commander/-/commander-2.20.3.tgz#fd485e84c03eb4881c20722ba48035e8531aeb33" + integrity sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ== + +commander@^4.1.1: + version "4.1.1" + resolved "https://registry.yarnpkg.com/commander/-/commander-4.1.1.tgz#9fd602bd936294e9e9ef46a3f4d6964044b18068" + integrity sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA== + +commander@^6.2.0: + version "6.2.1" + resolved "https://registry.yarnpkg.com/commander/-/commander-6.2.1.tgz#0792eb682dfbc325999bb2b84fddddba110ac73c" + integrity sha512-U7VdrJFnJgo4xjrHpTzu0yrHPGImdsmD95ZlgYSEajAn2JKzDhDTPG9kBTefmObL2w/ngeZnilk+OV9CG3d7UA== + +commondir@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/commondir/-/commondir-1.0.1.tgz#ddd800da0c66127393cca5950ea968a3aaf1253b" + integrity sha1-3dgA2gxmEnOTzKWVDqloo6rxJTs= + +component-emitter@^1.2.1: + version "1.3.0" + resolved "https://registry.yarnpkg.com/component-emitter/-/component-emitter-1.3.0.tgz#16e4070fba8ae29b679f2215853ee181ab2eabc0" + integrity sha512-Rd3se6QB+sO1TwqZjscQrurpEPIfO0/yYnSin6Q/rD3mOutHvUrCAhJub3r90uNb+SESBuE0QYoB90YdfatsRg== + +compressible@~2.0.16: + version "2.0.18" + resolved "https://registry.yarnpkg.com/compressible/-/compressible-2.0.18.tgz#af53cca6b070d4c3c0750fbd77286a6d7cc46fba" + integrity sha512-AF3r7P5dWxL8MxyITRMlORQNaOA2IkAFaTr4k7BUumjPtRpGDTZpl0Pb1XCO6JeDCBdp126Cgs9sMxqSjgYyRg== + dependencies: + mime-db ">= 1.43.0 < 2" + +compression@^1.7.4: + version "1.7.4" + resolved "https://registry.yarnpkg.com/compression/-/compression-1.7.4.tgz#95523eff170ca57c29a0ca41e6fe131f41e5bb8f" + integrity sha512-jaSIDzP9pZVS4ZfQ+TzvtiWhdpFhE2RDHz8QJkpX9SIpLq88VueF5jJw6t+6CUQcAoA6t+x89MLrWAqpfDE8iQ== + dependencies: + accepts "~1.3.5" + bytes "3.0.0" + compressible "~2.0.16" + debug "2.6.9" + on-headers "~1.0.2" + safe-buffer "5.1.2" + vary "~1.1.2" + +compute-scroll-into-view@^1.0.16: + version "1.0.16" + resolved "https://registry.yarnpkg.com/compute-scroll-into-view/-/compute-scroll-into-view-1.0.16.tgz#5b7bf4f7127ea2c19b750353d7ce6776a90ee088" + integrity sha512-a85LHKY81oQnikatZYA90pufpZ6sQx++BoCxOEMsjpZx+ZnaKGQnCyCehTRr/1p9GBIAHTjcU9k71kSYWloLiQ== + +concat-map@0.0.1: + version "0.0.1" + resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b" + integrity sha1-2Klr13/Wjfd5OnMDajug1UBdR3s= + +confusing-browser-globals@^1.0.10, confusing-browser-globals@^1.0.9: + version "1.0.10" + resolved "https://registry.yarnpkg.com/confusing-browser-globals/-/confusing-browser-globals-1.0.10.tgz#30d1e7f3d1b882b25ec4933d1d1adac353d20a59" + integrity sha512-gNld/3lySHwuhaVluJUKLePYirM3QNCKzVxqAdhJII9/WXKVX5PURzMVJspS1jTslSqjeuG4KMVTSouit5YPHA== + +connect-history-api-fallback@^1.6.0: + version "1.6.0" + resolved "https://registry.yarnpkg.com/connect-history-api-fallback/-/connect-history-api-fallback-1.6.0.tgz#8b32089359308d111115d81cad3fceab888f97bc" + integrity sha512-e54B99q/OUoH64zYYRf3HBP5z24G38h5D3qXu23JGRoigpX5Ss4r9ZnDk3g0Z8uQC2x2lPaJ+UlWBc1ZWBWdLg== + +contains-path@^0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/contains-path/-/contains-path-0.1.0.tgz#fe8cf184ff6670b6baef01a9d4861a5cbec4120a" + integrity sha1-/ozxhP9mcLa67wGp1IYaXL7EEgo= + +content-disposition@0.5.3: + version "0.5.3" + resolved "https://registry.yarnpkg.com/content-disposition/-/content-disposition-0.5.3.tgz#e130caf7e7279087c5616c2007d0485698984fbd" + integrity sha512-ExO0774ikEObIAEV9kDo50o+79VCUdEB6n6lzKgGwupcVeRlhrj3qGAfwq8G6uBJjkqLrhT0qEYFcWng8z1z0g== + dependencies: + safe-buffer "5.1.2" + +content-type@~1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/content-type/-/content-type-1.0.4.tgz#e138cc75e040c727b1966fe5e5f8c9aee256fe3b" + integrity sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA== + +convert-source-map@^1.7.0: + version "1.7.0" + resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-1.7.0.tgz#17a2cb882d7f77d3490585e2ce6c524424a3a442" + integrity sha512-4FJkXzKXEDB1snCFZlLP4gpC3JILicCpGbzG9f9G7tGqGCzETQ2hWPrcinA9oU4wtf2biUaEH5065UnMeR33oA== + dependencies: + safe-buffer "~5.1.1" + +cookie-signature@1.0.6: + version "1.0.6" + resolved "https://registry.yarnpkg.com/cookie-signature/-/cookie-signature-1.0.6.tgz#e303a882b342cc3ee8ca513a79999734dab3ae2c" + integrity sha1-4wOogrNCzD7oylE6eZmXNNqzriw= + +cookie@0.4.0: + version "0.4.0" + resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.4.0.tgz#beb437e7022b3b6d49019d088665303ebe9c14ba" + integrity sha512-+Hp8fLp57wnUSt0tY0tHEXh4voZRDnoIrZPqlo3DPiI4y9lwg/jqx+1Om94/W6ZaPDOUbnjOt/99w66zk+l1Xg== + +copy-anything@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/copy-anything/-/copy-anything-2.0.1.tgz#2afbce6da684bdfcbec93752fa762819cb480d9a" + integrity sha512-lA57e7viQHOdPQcrytv5jFeudZZOXuyk47lZym279FiDQ8jeZomXiGuVf6ffMKkJ+3TIai3J1J3yi6M+/4U35g== + dependencies: + is-what "^3.7.1" + +copy-descriptor@^0.1.0: + version "0.1.1" + resolved "https://registry.yarnpkg.com/copy-descriptor/-/copy-descriptor-0.1.1.tgz#676f6eb3c39997c2ee1ac3a924fd6124748f578d" + integrity sha1-Z29us8OZl8LuGsOpJP1hJHSPV40= + +copy-to-clipboard@^3.2.0: + version "3.3.1" + resolved "https://registry.yarnpkg.com/copy-to-clipboard/-/copy-to-clipboard-3.3.1.tgz#115aa1a9998ffab6196f93076ad6da3b913662ae" + integrity sha512-i13qo6kIHTTpCm8/Wup+0b1mVWETvu2kIMzKoK8FpkLkFxlt0znUAHcMzox+T8sPlqtZXq3CulEjQHsYiGFJUw== + dependencies: + toggle-selection "^1.0.6" + +copy-webpack-plugin@^6.2.1: + version "6.4.1" + resolved "https://registry.yarnpkg.com/copy-webpack-plugin/-/copy-webpack-plugin-6.4.1.tgz#138cd9b436dbca0a6d071720d5414848992ec47e" + integrity sha512-MXyPCjdPVx5iiWyl40Va3JGh27bKzOTNY3NjUTrosD2q7dR/cLD0013uqJ3BpFbUjyONINjb6qI7nDIJujrMbA== + dependencies: + cacache "^15.0.5" + fast-glob "^3.2.4" + find-cache-dir "^3.3.1" + glob-parent "^5.1.1" + globby "^11.0.1" + loader-utils "^2.0.0" + normalize-path "^3.0.0" + p-limit "^3.0.2" + schema-utils "^3.0.0" + serialize-javascript "^5.0.1" + webpack-sources "^1.4.3" + +core-util-is@~1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.2.tgz#b5fd54220aa2bc5ab57aab7140c940754503c1a7" + integrity sha1-tf1UIgqivFq1eqtxQMlAdUUDwac= + +cosmiconfig@^5.0.0: + version "5.2.1" + resolved "https://registry.yarnpkg.com/cosmiconfig/-/cosmiconfig-5.2.1.tgz#040f726809c591e77a17c0a3626ca45b4f168b1a" + integrity sha512-H65gsXo1SKjf8zmrJ67eJk8aIRKV5ff2D4uKZIBZShbhGSpEmsQOPW/SKMKYhSTrqR7ufy6RP69rPogdaPh/kA== + dependencies: + import-fresh "^2.0.0" + is-directory "^0.3.1" + js-yaml "^3.13.1" + parse-json "^4.0.0" + +cosmiconfig@^7.0.0: + version "7.0.0" + resolved "https://registry.yarnpkg.com/cosmiconfig/-/cosmiconfig-7.0.0.tgz#ef9b44d773959cae63ddecd122de23853b60f8d3" + integrity sha512-pondGvTuVYDk++upghXJabWzL6Kxu6f26ljFw64Swq9v6sQPUL3EUlVDV56diOjpCayKihL6hVe8exIACU4XcA== + dependencies: + "@types/parse-json" "^4.0.0" + import-fresh "^3.2.1" + parse-json "^5.0.0" + path-type "^4.0.0" + yaml "^1.10.0" + +create-require@^1.1.0: + version "1.1.1" + resolved "https://registry.yarnpkg.com/create-require/-/create-require-1.1.1.tgz#c1d7e8f1e5f6cfc9ff65f9cd352d37348756c333" + integrity sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ== + +cross-spawn@^6.0.0: + version "6.0.5" + resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-6.0.5.tgz#4a5ec7c64dfae22c3a14124dbacdee846d80cbc4" + integrity sha512-eTVLrBSt7fjbDygz805pMnstIs2VTBNkRm0qxZd+M7A5XDdxVRWO5MxGBXZhjY4cqLYLdtrGqRf8mBPmzwSpWQ== + dependencies: + nice-try "^1.0.4" + path-key "^2.0.1" + semver "^5.5.0" + shebang-command "^1.2.0" + which "^1.2.9" + +cross-spawn@^7.0.2, cross-spawn@^7.0.3: + version "7.0.3" + resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.3.tgz#f73a85b9d5d41d045551c177e2882d4ac85728a6" + integrity sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w== + dependencies: + path-key "^3.1.0" + shebang-command "^2.0.0" + which "^2.0.1" + +css-blank-pseudo@^0.1.4: + version "0.1.4" + resolved "https://registry.yarnpkg.com/css-blank-pseudo/-/css-blank-pseudo-0.1.4.tgz#dfdefd3254bf8a82027993674ccf35483bfcb3c5" + integrity sha512-LHz35Hr83dnFeipc7oqFDmsjHdljj3TQtxGGiNWSOsTLIAubSm4TEz8qCaKFpk7idaQ1GfWscF4E6mgpBysA1w== + dependencies: + postcss "^7.0.5" + +css-color-names@0.0.4, css-color-names@^0.0.4: + version "0.0.4" + resolved "https://registry.yarnpkg.com/css-color-names/-/css-color-names-0.0.4.tgz#808adc2e79cf84738069b646cb20ec27beb629e0" + integrity sha1-gIrcLnnPhHOAabZGyyDsJ762KeA= + +css-declaration-sorter@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/css-declaration-sorter/-/css-declaration-sorter-4.0.1.tgz#c198940f63a76d7e36c1e71018b001721054cb22" + integrity sha512-BcxQSKTSEEQUftYpBVnsH4SF05NTuBokb19/sBt6asXGKZ/6VP7PLG1CBCkFDYOnhXhPh0jMhO6xZ71oYHXHBA== + dependencies: + postcss "^7.0.1" + timsort "^0.3.0" + +css-has-pseudo@^0.10.0: + version "0.10.0" + resolved "https://registry.yarnpkg.com/css-has-pseudo/-/css-has-pseudo-0.10.0.tgz#3c642ab34ca242c59c41a125df9105841f6966ee" + integrity sha512-Z8hnfsZu4o/kt+AuFzeGpLVhFOGO9mluyHBaA2bA8aCGTwah5sT3WV/fTHH8UNZUytOIImuGPrl/prlb4oX4qQ== + dependencies: + postcss "^7.0.6" + postcss-selector-parser "^5.0.0-rc.4" + +css-loader@^5.0.0: + version "5.0.1" + resolved "https://registry.yarnpkg.com/css-loader/-/css-loader-5.0.1.tgz#9e4de0d6636a6266a585bd0900b422c85539d25f" + integrity sha512-cXc2ti9V234cq7rJzFKhirb2L2iPy8ZjALeVJAozXYz9te3r4eqLSixNAbMDJSgJEQywqXzs8gonxaboeKqwiw== + dependencies: + camelcase "^6.2.0" + cssesc "^3.0.0" + icss-utils "^5.0.0" + loader-utils "^2.0.0" + postcss "^8.1.4" + postcss-modules-extract-imports "^3.0.0" + postcss-modules-local-by-default "^4.0.0" + postcss-modules-scope "^3.0.0" + postcss-modules-values "^4.0.0" + postcss-value-parser "^4.1.0" + schema-utils "^3.0.0" + semver "^7.3.2" + +css-prefers-color-scheme@^3.1.1: + version "3.1.1" + resolved "https://registry.yarnpkg.com/css-prefers-color-scheme/-/css-prefers-color-scheme-3.1.1.tgz#6f830a2714199d4f0d0d0bb8a27916ed65cff1f4" + integrity sha512-MTu6+tMs9S3EUqzmqLXEcgNRbNkkD/TGFvowpeoWJn5Vfq7FMgsmRQs9X5NXAURiOBmOxm/lLjsDNXDE6k9bhg== + dependencies: + postcss "^7.0.5" + +css-select-base-adapter@^0.1.1: + version "0.1.1" + resolved "https://registry.yarnpkg.com/css-select-base-adapter/-/css-select-base-adapter-0.1.1.tgz#3b2ff4972cc362ab88561507a95408a1432135d7" + integrity sha512-jQVeeRG70QI08vSTwf1jHxp74JoZsr2XSgETae8/xC8ovSnL2WF87GTLO86Sbwdt2lK4Umg4HnnwMO4YF3Ce7w== + +css-select@^2.0.0, css-select@^2.0.2: + version "2.1.0" + resolved "https://registry.yarnpkg.com/css-select/-/css-select-2.1.0.tgz#6a34653356635934a81baca68d0255432105dbef" + integrity sha512-Dqk7LQKpwLoH3VovzZnkzegqNSuAziQyNZUcrdDM401iY+R5NkGBXGmtO05/yaXQziALuPogeG0b7UAgjnTJTQ== + dependencies: + boolbase "^1.0.0" + css-what "^3.2.1" + domutils "^1.7.0" + nth-check "^1.0.2" + +css-tree@1.0.0-alpha.37: + version "1.0.0-alpha.37" + resolved "https://registry.yarnpkg.com/css-tree/-/css-tree-1.0.0-alpha.37.tgz#98bebd62c4c1d9f960ec340cf9f7522e30709a22" + integrity sha512-DMxWJg0rnz7UgxKT0Q1HU/L9BeJI0M6ksor0OgqOnF+aRCDWg/N2641HmVyU9KVIu0OVVWOb2IpC9A+BJRnejg== + dependencies: + mdn-data "2.0.4" + source-map "^0.6.1" + +css-tree@^1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/css-tree/-/css-tree-1.1.2.tgz#9ae393b5dafd7dae8a622475caec78d3d8fbd7b5" + integrity sha512-wCoWush5Aeo48GLhfHPbmvZs59Z+M7k5+B1xDnXbdWNcEF423DoFdqSWE0PM5aNk5nI5cp1q7ms36zGApY/sKQ== + dependencies: + mdn-data "2.0.14" + source-map "^0.6.1" + +css-unit-converter@^1.1.1: + version "1.1.2" + resolved "https://registry.yarnpkg.com/css-unit-converter/-/css-unit-converter-1.1.2.tgz#4c77f5a1954e6dbff60695ecb214e3270436ab21" + integrity sha512-IiJwMC8rdZE0+xiEZHeru6YoONC4rfPMqGm2W85jMIbkFvv5nFTwJVFHam2eFrN6txmoUYFAFXiv8ICVeTO0MA== + +css-what@^3.2.1: + version "3.4.2" + resolved "https://registry.yarnpkg.com/css-what/-/css-what-3.4.2.tgz#ea7026fcb01777edbde52124e21f327e7ae950e4" + integrity sha512-ACUm3L0/jiZTqfzRM3Hi9Q8eZqd6IK37mMWPLz9PJxkLWllYeRf+EHUSHYEtFop2Eqytaq1FizFVh7XfBnXCDQ== + +cssdb@^4.4.0: + version "4.4.0" + resolved "https://registry.yarnpkg.com/cssdb/-/cssdb-4.4.0.tgz#3bf2f2a68c10f5c6a08abd92378331ee803cddb0" + integrity sha512-LsTAR1JPEM9TpGhl/0p3nQecC2LJ0kD8X5YARu1hk/9I1gril5vDtMZyNxcEpxxDj34YNck/ucjuoUd66K03oQ== + +cssesc@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/cssesc/-/cssesc-2.0.0.tgz#3b13bd1bb1cb36e1bcb5a4dcd27f54c5dcb35703" + integrity sha512-MsCAG1z9lPdoO/IUMLSBWBSVxVtJ1395VGIQ+Fc2gNdkQ1hNDnQdw3YhA71WJCBW1vdwA0cAnk/DnW6bqoEUYg== + +cssesc@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/cssesc/-/cssesc-3.0.0.tgz#37741919903b868565e1c09ea747445cd18983ee" + integrity sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg== + +cssnano-preset-default@^4.0.7: + version "4.0.7" + resolved "https://registry.yarnpkg.com/cssnano-preset-default/-/cssnano-preset-default-4.0.7.tgz#51ec662ccfca0f88b396dcd9679cdb931be17f76" + integrity sha512-x0YHHx2h6p0fCl1zY9L9roD7rnlltugGu7zXSKQx6k2rYw0Hi3IqxcoAGF7u9Q5w1nt7vK0ulxV8Lo+EvllGsA== + dependencies: + css-declaration-sorter "^4.0.1" + cssnano-util-raw-cache "^4.0.1" + postcss "^7.0.0" + postcss-calc "^7.0.1" + postcss-colormin "^4.0.3" + postcss-convert-values "^4.0.1" + postcss-discard-comments "^4.0.2" + postcss-discard-duplicates "^4.0.2" + postcss-discard-empty "^4.0.1" + postcss-discard-overridden "^4.0.1" + postcss-merge-longhand "^4.0.11" + postcss-merge-rules "^4.0.3" + postcss-minify-font-values "^4.0.2" + postcss-minify-gradients "^4.0.2" + postcss-minify-params "^4.0.2" + postcss-minify-selectors "^4.0.2" + postcss-normalize-charset "^4.0.1" + postcss-normalize-display-values "^4.0.2" + postcss-normalize-positions "^4.0.2" + postcss-normalize-repeat-style "^4.0.2" + postcss-normalize-string "^4.0.2" + postcss-normalize-timing-functions "^4.0.2" + postcss-normalize-unicode "^4.0.1" + postcss-normalize-url "^4.0.1" + postcss-normalize-whitespace "^4.0.2" + postcss-ordered-values "^4.1.2" + postcss-reduce-initial "^4.0.3" + postcss-reduce-transforms "^4.0.2" + postcss-svgo "^4.0.2" + postcss-unique-selectors "^4.0.1" + +cssnano-util-get-arguments@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/cssnano-util-get-arguments/-/cssnano-util-get-arguments-4.0.0.tgz#ed3a08299f21d75741b20f3b81f194ed49cc150f" + integrity sha1-7ToIKZ8h11dBsg87gfGU7UnMFQ8= + +cssnano-util-get-match@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/cssnano-util-get-match/-/cssnano-util-get-match-4.0.0.tgz#c0e4ca07f5386bb17ec5e52250b4f5961365156d" + integrity sha1-wOTKB/U4a7F+xeUiULT1lhNlFW0= + +cssnano-util-raw-cache@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/cssnano-util-raw-cache/-/cssnano-util-raw-cache-4.0.1.tgz#b26d5fd5f72a11dfe7a7846fb4c67260f96bf282" + integrity sha512-qLuYtWK2b2Dy55I8ZX3ky1Z16WYsx544Q0UWViebptpwn/xDBmog2TLg4f+DBMg1rJ6JDWtn96WHbOKDWt1WQA== + dependencies: + postcss "^7.0.0" + +cssnano-util-same-parent@^4.0.0: + version "4.0.1" + resolved "https://registry.yarnpkg.com/cssnano-util-same-parent/-/cssnano-util-same-parent-4.0.1.tgz#574082fb2859d2db433855835d9a8456ea18bbf3" + integrity sha512-WcKx5OY+KoSIAxBW6UBBRay1U6vkYheCdjyVNDm85zt5K9mHoGOfsOsqIszfAqrQQFIIKgjh2+FDgIj/zsl21Q== + +cssnano@^4.1.10: + version "4.1.10" + resolved "https://registry.yarnpkg.com/cssnano/-/cssnano-4.1.10.tgz#0ac41f0b13d13d465487e111b778d42da631b8b2" + integrity sha512-5wny+F6H4/8RgNlaqab4ktc3e0/blKutmq8yNlBFXA//nSFFAqAngjNVRzUvCgYROULmZZUoosL/KSoZo5aUaQ== + dependencies: + cosmiconfig "^5.0.0" + cssnano-preset-default "^4.0.7" + is-resolvable "^1.0.0" + postcss "^7.0.0" + +csso@^4.0.2: + version "4.2.0" + resolved "https://registry.yarnpkg.com/csso/-/csso-4.2.0.tgz#ea3a561346e8dc9f546d6febedd50187cf389529" + integrity sha512-wvlcdIbf6pwKEk7vHj8/Bkc0B4ylXZruLvOgs9doS5eOsOpuodOV2zJChSpkp+pRpYQLQMeF04nr3Z68Sta9jA== + dependencies: + css-tree "^1.1.2" + +csstype@^3.0.2: + version "3.0.6" + resolved "https://registry.yarnpkg.com/csstype/-/csstype-3.0.6.tgz#865d0b5833d7d8d40f4e5b8a6d76aea3de4725ef" + integrity sha512-+ZAmfyWMT7TiIlzdqJgjMb7S4f1beorDbWbsocyK4RaiqA5RTX3K14bnBWmmA9QEM0gRdsjyyrEmcyga8Zsxmw== + +d3-array@^2.3.0: + version "2.11.0" + resolved "https://registry.yarnpkg.com/d3-array/-/d3-array-2.11.0.tgz#5ed6a2869bc7d471aec8df9ff6ed9fef798facc4" + integrity sha512-26clcwmHQEdsLv34oNKq5Ia9tQ26Y/4HqS3dQzF42QBUqymZJ+9PORcN1G52bt37NsL2ABoX4lvyYZc+A9Y0zw== + dependencies: + internmap "^1.0.0" + +"d3-color@1 - 2": + version "2.0.0" + resolved "https://registry.yarnpkg.com/d3-color/-/d3-color-2.0.0.tgz#8d625cab42ed9b8f601a1760a389f7ea9189d62e" + integrity sha512-SPXi0TSKPD4g9tw0NMZFnR95XVgUZiBH+uUTqQuDu1OsE2zomHU7ho0FISciaPvosimixwHFl3WHLGabv6dDgQ== + +"d3-format@1 - 2": + version "2.0.0" + resolved "https://registry.yarnpkg.com/d3-format/-/d3-format-2.0.0.tgz#a10bcc0f986c372b729ba447382413aabf5b0767" + integrity sha512-Ab3S6XuE/Q+flY96HXT0jOXcM4EAClYFnRGY5zsjRGNy6qCYrQsMffs7cV5Q9xejb35zxW5hf/guKw34kvIKsA== + +"d3-interpolate@1.2.0 - 2", d3-interpolate@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/d3-interpolate/-/d3-interpolate-2.0.1.tgz#98be499cfb8a3b94d4ff616900501a64abc91163" + integrity sha512-c5UhwwTs/yybcmTpAVqwSFl6vrQ8JZJoT5F7xNFK9pymv5C0Ymcc9/LIJHtYIggg/yS9YHw8i8O8tgb9pupjeQ== + dependencies: + d3-color "1 - 2" + +"d3-path@1 - 2": + version "2.0.0" + resolved "https://registry.yarnpkg.com/d3-path/-/d3-path-2.0.0.tgz#55d86ac131a0548adae241eebfb56b4582dd09d8" + integrity sha512-ZwZQxKhBnv9yHaiWd6ZU4x5BtCQ7pXszEV9CU6kRgwIQVQGLMv1oiL4M+MK/n79sYzsj+gcgpPQSctJUsLN7fA== + +d3-scale@^3.2.3: + version "3.2.3" + resolved "https://registry.yarnpkg.com/d3-scale/-/d3-scale-3.2.3.tgz#be380f57f1f61d4ff2e6cbb65a40593a51649cfd" + integrity sha512-8E37oWEmEzj57bHcnjPVOBS3n4jqakOeuv1EDdQSiSrYnMCBdMd3nc4HtKk7uia8DUHcY/CGuJ42xxgtEYrX0g== + dependencies: + d3-array "^2.3.0" + d3-format "1 - 2" + d3-interpolate "1.2.0 - 2" + d3-time "1 - 2" + d3-time-format "2 - 3" + +d3-shape@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/d3-shape/-/d3-shape-2.0.0.tgz#2331b62fa784a2a1daac47a7233cfd69301381fd" + integrity sha512-djpGlA779ua+rImicYyyjnOjeubyhql1Jyn1HK0bTyawuH76UQRWXd+pftr67H6Fa8hSwetkgb/0id3agKWykw== + dependencies: + d3-path "1 - 2" + +"d3-time-format@2 - 3": + version "3.0.0" + resolved "https://registry.yarnpkg.com/d3-time-format/-/d3-time-format-3.0.0.tgz#df8056c83659e01f20ac5da5fdeae7c08d5f1bb6" + integrity sha512-UXJh6EKsHBTjopVqZBhFysQcoXSv/5yLONZvkQ5Kk3qbwiUYkdX17Xa1PT6U1ZWXGGfB1ey5L8dKMlFq2DO0Ag== + dependencies: + d3-time "1 - 2" + +"d3-time@1 - 2": + version "2.0.0" + resolved "https://registry.yarnpkg.com/d3-time/-/d3-time-2.0.0.tgz#ad7c127d17c67bd57a4c61f3eaecb81108b1e0ab" + integrity sha512-2mvhstTFcMvwStWd9Tj3e6CEqtOivtD8AUiHT8ido/xmzrI9ijrUUihZ6nHuf/vsScRBonagOdj0Vv+SEL5G3Q== + +date-fns@^2.15.0: + version "2.16.1" + resolved "https://registry.yarnpkg.com/date-fns/-/date-fns-2.16.1.tgz#05775792c3f3331da812af253e1a935851d3834b" + integrity sha512-sAJVKx/FqrLYHAQeN7VpJrPhagZc9R4ImZIWYRFZaaohR3KzmuK88touwsSwSVT8Qcbd4zoDsnGfX4GFB4imyQ== + +dayjs@^1.8.30, dayjs@^1.9.3: + version "1.10.4" + resolved "https://registry.yarnpkg.com/dayjs/-/dayjs-1.10.4.tgz#8e544a9b8683f61783f570980a8a80eaf54ab1e2" + integrity sha512-RI/Hh4kqRc1UKLOAf/T5zdMMX5DQIlDxwUe3wSyMMnEbGunnpENCdbUgM+dW7kXidZqCttBrmw7BhN4TMddkCw== + +debug@2.6.9, debug@^2.2.0, debug@^2.3.3, debug@^2.6.9: + version "2.6.9" + resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f" + integrity sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA== + dependencies: + ms "2.0.0" + +debug@^3.1.1, debug@^3.2.6: + version "3.2.7" + resolved "https://registry.yarnpkg.com/debug/-/debug-3.2.7.tgz#72580b7e9145fb39b6676f9c5e5fb100b934179a" + integrity sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ== + dependencies: + ms "^2.1.1" + +debug@^4.0.0, debug@^4.0.1, debug@^4.1.0, debug@^4.1.1, debug@^4.3.1: + version "4.3.1" + resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.1.tgz#f0d229c505e0c6d8c49ac553d1b13dc183f6b2ee" + integrity sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ== + dependencies: + ms "2.1.2" + +decamelize-keys@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/decamelize-keys/-/decamelize-keys-1.1.0.tgz#d171a87933252807eb3cb61dc1c1445d078df2d9" + integrity sha1-0XGoeTMlKAfrPLYdwcFEXQeN8tk= + dependencies: + decamelize "^1.1.0" + map-obj "^1.0.0" + +decamelize@^1.1.0, decamelize@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/decamelize/-/decamelize-1.2.0.tgz#f6534d15148269b20352e7bee26f501f9a191290" + integrity sha1-9lNNFRSCabIDUue+4m9QH5oZEpA= + +decimal.js-light@^2.4.1: + version "2.5.1" + resolved "https://registry.yarnpkg.com/decimal.js-light/-/decimal.js-light-2.5.1.tgz#134fd32508f19e208f4fb2f8dac0d2626a867934" + integrity sha512-qIMFpTMZmny+MMIitAB6D7iVPEorVw6YQRWkvarTkT4tBeSLLiHzcwj6q0MmYSFCiVpiqPJTJEYIrpcPzVEIvg== + +decode-uri-component@^0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/decode-uri-component/-/decode-uri-component-0.2.0.tgz#eb3913333458775cb84cd1a1fae062106bb87545" + integrity sha1-6zkTMzRYd1y4TNGh+uBiEGu4dUU= + +deep-equal@^1.0.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/deep-equal/-/deep-equal-1.1.1.tgz#b5c98c942ceffaf7cb051e24e1434a25a2e6076a" + integrity sha512-yd9c5AdiqVcR+JjcwUQb9DkhJc8ngNr0MahEBGvDiJw8puWab2yZlh+nkasOnZP+EGTAP6rRp2JzJhJZzvNF8g== + dependencies: + is-arguments "^1.0.4" + is-date-object "^1.0.1" + is-regex "^1.0.4" + object-is "^1.0.1" + object-keys "^1.1.1" + regexp.prototype.flags "^1.2.0" + +deep-is@^0.1.3: + version "0.1.3" + resolved "https://registry.yarnpkg.com/deep-is/-/deep-is-0.1.3.tgz#b369d6fb5dbc13eecf524f91b070feedc357cf34" + integrity sha1-s2nW+128E+7PUk+RsHD+7cNXzzQ= + +deepmerge@^2.1.1: + version "2.2.1" + resolved "https://registry.yarnpkg.com/deepmerge/-/deepmerge-2.2.1.tgz#5d3ff22a01c00f645405a2fbc17d0778a1801170" + integrity sha512-R9hc1Xa/NOBi9WRVUWg19rl1UB7Tt4kuPd+thNJgFZoxXsTz7ncaPaeIm+40oSGuP33DfMb4sZt1QIGiJzC4EA== + +default-gateway@^4.2.0: + version "4.2.0" + resolved "https://registry.yarnpkg.com/default-gateway/-/default-gateway-4.2.0.tgz#167104c7500c2115f6dd69b0a536bb8ed720552b" + integrity sha512-h6sMrVB1VMWVrW13mSc6ia/DwYYw5MN6+exNu1OaJeFac5aSAvwM7lZ0NVfTABuSkQelr4h5oebg3KB1XPdjgA== + dependencies: + execa "^1.0.0" + ip-regex "^2.1.0" + +define-properties@^1.1.2, define-properties@^1.1.3: + version "1.1.3" + resolved "https://registry.yarnpkg.com/define-properties/-/define-properties-1.1.3.tgz#cf88da6cbee26fe6db7094f61d870cbd84cee9f1" + integrity sha512-3MqfYKj2lLzdMSf8ZIZE/V+Zuy+BgD6f164e8K2w7dgnpKArBDerGYpM46IYYcjnkdPNMjPk9A6VFB8+3SKlXQ== + dependencies: + object-keys "^1.0.12" + +define-property@^0.2.5: + version "0.2.5" + resolved "https://registry.yarnpkg.com/define-property/-/define-property-0.2.5.tgz#c35b1ef918ec3c990f9a5bc57be04aacec5c8116" + integrity sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY= + dependencies: + is-descriptor "^0.1.0" + +define-property@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/define-property/-/define-property-1.0.0.tgz#769ebaaf3f4a63aad3af9e8d304c9bbe79bfb0e6" + integrity sha1-dp66rz9KY6rTr56NMEybvnm/sOY= + dependencies: + is-descriptor "^1.0.0" + +define-property@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/define-property/-/define-property-2.0.2.tgz#d459689e8d654ba77e02a817f8710d702cb16e9d" + integrity sha512-jwK2UV4cnPpbcG7+VRARKTZPUWowwXA8bzH5NP6ud0oeAxyYPuGZUAC7hMugpCdz4BeSZl2Dl9k66CHJ/46ZYQ== + dependencies: + is-descriptor "^1.0.2" + isobject "^3.0.1" + +del@^4.1.1: + version "4.1.1" + resolved "https://registry.yarnpkg.com/del/-/del-4.1.1.tgz#9e8f117222ea44a31ff3a156c049b99052a9f0b4" + integrity sha512-QwGuEUouP2kVwQenAsOof5Fv8K9t3D8Ca8NxcXKrIpEHjTXK5J2nXLdP+ALI1cgv8wj7KuwBhTwBkOZSJKM5XQ== + dependencies: + "@types/glob" "^7.1.1" + globby "^6.1.0" + is-path-cwd "^2.0.0" + is-path-in-cwd "^2.0.0" + p-map "^2.0.0" + pify "^4.0.1" + rimraf "^2.6.3" + +depd@~1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/depd/-/depd-1.1.2.tgz#9bcd52e14c097763e749b274c4346ed2e560b5a9" + integrity sha1-m81S4UwJd2PnSbJ0xDRu0uVgtak= + +destroy@~1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/destroy/-/destroy-1.0.4.tgz#978857442c44749e4206613e37946205826abd80" + integrity sha1-l4hXRCxEdJ5CBmE+N5RiBYJqvYA= + +detect-node@^2.0.4: + version "2.0.4" + resolved "https://registry.yarnpkg.com/detect-node/-/detect-node-2.0.4.tgz#014ee8f8f669c5c58023da64b8179c083a28c46c" + integrity sha512-ZIzRpLJrOj7jjP2miAtgqIfmzbxa4ZOr5jJc601zklsfEx9oTzmmj2nVpIPRpNlRTIh8lc1kyViIY7BWSGNmKw== + +diff@^4.0.1: + version "4.0.2" + resolved "https://registry.yarnpkg.com/diff/-/diff-4.0.2.tgz#60f3aecb89d5fae520c11aa19efc2bb982aade7d" + integrity sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A== + +dir-glob@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/dir-glob/-/dir-glob-3.0.1.tgz#56dbf73d992a4a93ba1584f4534063fd2e41717f" + integrity sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA== + dependencies: + path-type "^4.0.0" + +dns-equal@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/dns-equal/-/dns-equal-1.0.0.tgz#b39e7f1da6eb0a75ba9c17324b34753c47e0654d" + integrity sha1-s55/HabrCnW6nBcySzR1PEfgZU0= + +dns-packet@^1.3.1: + version "1.3.1" + resolved "https://registry.yarnpkg.com/dns-packet/-/dns-packet-1.3.1.tgz#12aa426981075be500b910eedcd0b47dd7deda5a" + integrity sha512-0UxfQkMhYAUaZI+xrNZOz/as5KgDU0M/fQ9b6SpkyLbk3GEswDi6PADJVaYJradtRVsRIlF1zLyOodbcTCDzUg== + dependencies: + ip "^1.1.0" + safe-buffer "^5.0.1" + +dns-txt@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/dns-txt/-/dns-txt-2.0.2.tgz#b91d806f5d27188e4ab3e7d107d881a1cc4642b6" + integrity sha1-uR2Ab10nGI5Ks+fRB9iBocxGQrY= + dependencies: + buffer-indexof "^1.0.0" + +doctrine@1.5.0: + version "1.5.0" + resolved "https://registry.yarnpkg.com/doctrine/-/doctrine-1.5.0.tgz#379dce730f6166f76cefa4e6707a159b02c5a6fa" + integrity sha1-N53Ocw9hZvds76TmcHoVmwLFpvo= + dependencies: + esutils "^2.0.2" + isarray "^1.0.0" + +doctrine@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/doctrine/-/doctrine-2.1.0.tgz#5cd01fc101621b42c4cd7f5d1a66243716d3f39d" + integrity sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw== + dependencies: + esutils "^2.0.2" + +doctrine@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/doctrine/-/doctrine-3.0.0.tgz#addebead72a6574db783639dc87a121773973961" + integrity sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w== + dependencies: + esutils "^2.0.2" + +dom-align@^1.7.0: + version "1.12.0" + resolved "https://registry.yarnpkg.com/dom-align/-/dom-align-1.12.0.tgz#56fb7156df0b91099830364d2d48f88963f5a29c" + integrity sha512-YkoezQuhp3SLFGdOlr5xkqZ640iXrnHAwVYcDg8ZKRUtO7mSzSC2BA5V0VuyAwPSJA4CLIc6EDDJh4bEsD2+zA== + +dom-converter@^0.2: + version "0.2.0" + resolved "https://registry.yarnpkg.com/dom-converter/-/dom-converter-0.2.0.tgz#6721a9daee2e293682955b6afe416771627bb768" + integrity sha512-gd3ypIPfOMr9h5jIKq8E3sHOTCjeirnl0WK5ZdS1AW0Odt0b1PaWaHdJ4Qk4klv+YB9aJBS7mESXjFoDQPu6DA== + dependencies: + utila "~0.4" + +dom-helpers@^3.4.0: + version "3.4.0" + resolved "https://registry.yarnpkg.com/dom-helpers/-/dom-helpers-3.4.0.tgz#e9b369700f959f62ecde5a6babde4bccd9169af8" + integrity sha512-LnuPJ+dwqKDIyotW1VzmOZ5TONUN7CwkCR5hrgawTUbkBGYdeoNLZo6nNfGkCrjtE1nXXaj7iMMpDa8/d9WoIA== + dependencies: + "@babel/runtime" "^7.1.2" + +dom-serializer@0: + version "0.2.2" + resolved "https://registry.yarnpkg.com/dom-serializer/-/dom-serializer-0.2.2.tgz#1afb81f533717175d478655debc5e332d9f9bb51" + integrity sha512-2/xPb3ORsQ42nHYiSunXkDjPLBaEj/xTwUO4B7XCZQTRk7EBtTOPaygh10YAAh2OI1Qrp6NWfpAhzswj0ydt9g== + dependencies: + domelementtype "^2.0.1" + entities "^2.0.0" + +dom-serializer@^0.1.1: + version "0.1.1" + resolved "https://registry.yarnpkg.com/dom-serializer/-/dom-serializer-0.1.1.tgz#1ec4059e284babed36eec2941d4a970a189ce7c0" + integrity sha512-l0IU0pPzLWSHBcieZbpOKgkIn3ts3vAh7ZuFyXNwJxJXk/c4Gwj9xaTJwIDVQCXawWD0qb3IzMGH5rglQaO0XA== + dependencies: + domelementtype "^1.3.0" + entities "^1.1.1" + +domelementtype@1, domelementtype@^1.3.0, domelementtype@^1.3.1: + version "1.3.1" + resolved "https://registry.yarnpkg.com/domelementtype/-/domelementtype-1.3.1.tgz#d048c44b37b0d10a7f2a3d5fee3f4333d790481f" + integrity sha512-BSKB+TSpMpFI/HOxCNr1O8aMOTZ8hT3pM3GQ0w/mWRmkhEDSFJkkyzz4XQsBV44BChwGkrDfMyjVD0eA2aFV3w== + +domelementtype@^2.0.1: + version "2.1.0" + resolved "https://registry.yarnpkg.com/domelementtype/-/domelementtype-2.1.0.tgz#a851c080a6d1c3d94344aed151d99f669edf585e" + integrity sha512-LsTgx/L5VpD+Q8lmsXSHW2WpA+eBlZ9HPf3erD1IoPF00/3JKHZ3BknUVA2QGDNu69ZNmyFmCWBSO45XjYKC5w== + +domhandler@^2.3.0: + version "2.4.2" + resolved "https://registry.yarnpkg.com/domhandler/-/domhandler-2.4.2.tgz#8805097e933d65e85546f726d60f5eb88b44f803" + integrity sha512-JiK04h0Ht5u/80fdLMCEmV4zkNh2BcoMFBmZ/91WtYZ8qVXSKjiw7fXMgFPnHcSZgOo3XdinHvmnDUeMf5R4wA== + dependencies: + domelementtype "1" + +domutils@^1.5.1, domutils@^1.7.0: + version "1.7.0" + resolved "https://registry.yarnpkg.com/domutils/-/domutils-1.7.0.tgz#56ea341e834e06e6748af7a1cb25da67ea9f8c2a" + integrity sha512-Lgd2XcJ/NjEw+7tFvfKxOzCYKZsdct5lczQ2ZaQY8Djz7pfAD3Gbp8ySJWtreII/vDlMVmxwa6pHmdxIYgttDg== + dependencies: + dom-serializer "0" + domelementtype "1" + +dot-case@^3.0.4: + version "3.0.4" + resolved "https://registry.yarnpkg.com/dot-case/-/dot-case-3.0.4.tgz#9b2b670d00a431667a8a75ba29cd1b98809ce751" + integrity sha512-Kv5nKlh6yRrdrGvxeJ2e5y2eRUpkUosIW4A2AS38zwSz27zu7ufDwQPi5Jhs3XAlGNetl3bmnGhQsMtkKJnj3w== + dependencies: + no-case "^3.0.4" + tslib "^2.0.3" + +dot-prop@^5.2.0: + version "5.3.0" + resolved "https://registry.yarnpkg.com/dot-prop/-/dot-prop-5.3.0.tgz#90ccce708cd9cd82cc4dc8c3ddd9abdd55b20e88" + integrity sha512-QM8q3zDe58hqUqjraQOmzZ1LIH9SWQJTlEKCH4kJ2oQvLZk7RbQXvtDM2XEq3fwkV9CCvvH4LA0AV+ogFsBM2Q== + dependencies: + is-obj "^2.0.0" + +ee-first@1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/ee-first/-/ee-first-1.1.1.tgz#590c61156b0ae2f4f0255732a158b266bc56b21d" + integrity sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0= + +electron-to-chromium@^1.3.634: + version "1.3.645" + resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.3.645.tgz#c0b269ae2ecece5aedc02dd4586397d8096affb1" + integrity sha512-T7mYop3aDpRHIQaUYcmzmh6j9MAe560n6ukqjJMbVC6bVTau7dSpvB18bcsBPPtOSe10cKxhJFtlbEzLa0LL1g== + +emoji-regex@^7.0.1: + version "7.0.3" + resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-7.0.3.tgz#933a04052860c85e83c122479c4748a8e4c72156" + integrity sha512-CwBLREIQ7LvYFB0WyRvwhq5N5qPhc6PMjD6bYggFlI5YyDgl+0vxq5VHbMOFqLg7hfWzmu8T5Z1QofhmTIhItA== + +emoji-regex@^8.0.0: + version "8.0.0" + resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-8.0.0.tgz#e818fd69ce5ccfcb404594f842963bf53164cc37" + integrity sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A== + +emojis-list@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/emojis-list/-/emojis-list-3.0.0.tgz#5570662046ad29e2e916e71aae260abdff4f6a78" + integrity sha512-/kyM18EfinwXZbno9FyUGeFh87KC8HRQBQGildHZbEuRyWFOmv1U10o9BBp8XVZDVNNuQKyIGIu5ZYAAXJ0V2Q== + +encodeurl@~1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/encodeurl/-/encodeurl-1.0.2.tgz#ad3ff4c86ec2d029322f5a02c3a9a606c95b3f59" + integrity sha1-rT/0yG7C0CkyL1oCw6mmBslbP1k= + +end-of-stream@^1.1.0: + version "1.4.4" + resolved "https://registry.yarnpkg.com/end-of-stream/-/end-of-stream-1.4.4.tgz#5ae64a5f45057baf3626ec14da0ca5e4b2431eb0" + integrity sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q== + dependencies: + once "^1.4.0" + +enhanced-resolve@^4.0.0: + version "4.5.0" + resolved "https://registry.yarnpkg.com/enhanced-resolve/-/enhanced-resolve-4.5.0.tgz#2f3cfd84dbe3b487f18f2db2ef1e064a571ca5ec" + integrity sha512-Nv9m36S/vxpsI+Hc4/ZGRs0n9mXqSWGGq49zxb/cJfPAQMbUtttJAlNPS4AQzaBdw/pKskw5bMbekT/Y7W/Wlg== + dependencies: + graceful-fs "^4.1.2" + memory-fs "^0.5.0" + tapable "^1.0.0" + +enhanced-resolve@^5.7.0: + version "5.7.0" + resolved "https://registry.yarnpkg.com/enhanced-resolve/-/enhanced-resolve-5.7.0.tgz#525c5d856680fbd5052de453ac83e32049958b5c" + integrity sha512-6njwt/NsZFUKhM6j9U8hzVyD4E4r0x7NQzhTCbcWOJ0IQjNSAoalWmb0AE51Wn+fwan5qVESWi7t2ToBxs9vrw== + dependencies: + graceful-fs "^4.2.4" + tapable "^2.2.0" + +enquirer@^2.3.5, enquirer@^2.3.6: + version "2.3.6" + resolved "https://registry.yarnpkg.com/enquirer/-/enquirer-2.3.6.tgz#2a7fe5dd634a1e4125a975ec994ff5456dc3734d" + integrity sha512-yjNnPr315/FjS4zIsUxYguYUPP2e1NK4d7E7ZOLiyYCcbFBiTMyID+2wvm2w6+pZ/odMA7cRkjhsPbltwBOrLg== + dependencies: + ansi-colors "^4.1.1" + +entities@^1.1.1: + version "1.1.2" + resolved "https://registry.yarnpkg.com/entities/-/entities-1.1.2.tgz#bdfa735299664dfafd34529ed4f8522a275fea56" + integrity sha512-f2LZMYl1Fzu7YSBKg+RoROelpOaNrcGmE9AZubeDfrCEia483oW4MI4VyFd5VNHIgQ/7qm1I0wUHK1eJnn2y2w== + +entities@^2.0.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/entities/-/entities-2.2.0.tgz#098dc90ebb83d8dffa089d55256b351d34c4da55" + integrity sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A== + +envinfo@^7.7.3: + version "7.7.3" + resolved "https://registry.yarnpkg.com/envinfo/-/envinfo-7.7.3.tgz#4b2d8622e3e7366afb8091b23ed95569ea0208cc" + integrity sha512-46+j5QxbPWza0PB1i15nZx0xQ4I/EfQxg9J8Had3b408SV63nEtor2e+oiY63amTo9KTuh2a3XLObNwduxYwwA== + +errno@^0.1.1, errno@^0.1.3: + version "0.1.8" + resolved "https://registry.yarnpkg.com/errno/-/errno-0.1.8.tgz#8bb3e9c7d463be4976ff888f76b4809ebc2e811f" + integrity sha512-dJ6oBr5SQ1VSd9qkk7ByRgb/1SH4JZjCHSW/mr63/QcXO9zLVxvJ6Oy13nio03rxpSnVDDjFor75SjVeZWPW/A== + dependencies: + prr "~1.0.1" + +error-ex@^1.2.0, error-ex@^1.3.1: + version "1.3.2" + resolved "https://registry.yarnpkg.com/error-ex/-/error-ex-1.3.2.tgz#b4ac40648107fdcdcfae242f428bea8a14d4f1bf" + integrity sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g== + dependencies: + is-arrayish "^0.2.1" + +es-abstract@^1.17.2: + version "1.17.7" + resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.17.7.tgz#a4de61b2f66989fc7421676c1cb9787573ace54c" + integrity sha512-VBl/gnfcJ7OercKA9MVaegWsBHFjV492syMudcnQZvt/Dw8ezpcOHYZXa/J96O8vx+g4x65YKhxOwDUh63aS5g== + dependencies: + es-to-primitive "^1.2.1" + function-bind "^1.1.1" + has "^1.0.3" + has-symbols "^1.0.1" + is-callable "^1.2.2" + is-regex "^1.1.1" + object-inspect "^1.8.0" + object-keys "^1.1.1" + object.assign "^4.1.1" + string.prototype.trimend "^1.0.1" + string.prototype.trimstart "^1.0.1" + +es-abstract@^1.18.0-next.1: + version "1.18.0-next.2" + resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.18.0-next.2.tgz#088101a55f0541f595e7e057199e27ddc8f3a5c2" + integrity sha512-Ih4ZMFHEtZupnUh6497zEL4y2+w8+1ljnCyaTa+adcoafI1GOvMwFlDjBLfWR7y9VLfrjRJe9ocuHY1PSR9jjw== + dependencies: + call-bind "^1.0.2" + es-to-primitive "^1.2.1" + function-bind "^1.1.1" + get-intrinsic "^1.0.2" + has "^1.0.3" + has-symbols "^1.0.1" + is-callable "^1.2.2" + is-negative-zero "^2.0.1" + is-regex "^1.1.1" + object-inspect "^1.9.0" + object-keys "^1.1.1" + object.assign "^4.1.2" + string.prototype.trimend "^1.0.3" + string.prototype.trimstart "^1.0.3" + +es-module-lexer@^0.3.26: + version "0.3.26" + resolved "https://registry.yarnpkg.com/es-module-lexer/-/es-module-lexer-0.3.26.tgz#7b507044e97d5b03b01d4392c74ffeb9c177a83b" + integrity sha512-Va0Q/xqtrss45hWzP8CZJwzGSZJjDM5/MJRE3IXXnUCcVLElR9BRaE9F62BopysASyc4nM3uwhSW7FFB9nlWAA== + +es-to-primitive@^1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/es-to-primitive/-/es-to-primitive-1.2.1.tgz#e55cd4c9cdc188bcefb03b366c736323fc5c898a" + integrity sha512-QCOllgZJtaUo9miYBcLChTUaHNjJF3PYs1VidD7AwiEj1kYxKeQTctLAezAOH5ZKRH0g2IgPn6KwB4IT8iRpvA== + dependencies: + is-callable "^1.1.4" + is-date-object "^1.0.1" + is-symbol "^1.0.2" + +escalade@^3.1.1: + version "3.1.1" + resolved "https://registry.yarnpkg.com/escalade/-/escalade-3.1.1.tgz#d8cfdc7000965c5a0174b4a82eaa5c0552742e40" + integrity sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw== + +escape-html@~1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/escape-html/-/escape-html-1.0.3.tgz#0258eae4d3d0c0974de1c169188ef0051d1d1988" + integrity sha1-Aljq5NPQwJdN4cFpGI7wBR0dGYg= + +escape-string-regexp@^1.0.3, escape-string-regexp@^1.0.5: + version "1.0.5" + resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz#1b61c0562190a8dff6ae3bb2cf0200ca130b86d4" + integrity sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ= + +eslint-config-airbnb-base@14.2.0: + version "14.2.0" + resolved "https://registry.yarnpkg.com/eslint-config-airbnb-base/-/eslint-config-airbnb-base-14.2.0.tgz#fe89c24b3f9dc8008c9c0d0d88c28f95ed65e9c4" + integrity sha512-Snswd5oC6nJaevs3nZoLSTvGJBvzTfnBqOIArkf3cbyTyq9UD79wOk8s+RiL6bhca0p/eRO6veczhf6A/7Jy8Q== + dependencies: + confusing-browser-globals "^1.0.9" + object.assign "^4.1.0" + object.entries "^1.1.2" + +eslint-config-airbnb-base@^14.2.0: + version "14.2.1" + resolved "https://registry.yarnpkg.com/eslint-config-airbnb-base/-/eslint-config-airbnb-base-14.2.1.tgz#8a2eb38455dc5a312550193b319cdaeef042cd1e" + integrity sha512-GOrQyDtVEc1Xy20U7vsB2yAoB4nBlfH5HZJeatRXHleO+OS5Ot+MWij4Dpltw4/DyIkqUfqz1epfhVR5XWWQPA== + dependencies: + confusing-browser-globals "^1.0.10" + object.assign "^4.1.2" + object.entries "^1.1.2" + +eslint-config-airbnb-typescript@^12.0.0: + version "12.0.0" + resolved "https://registry.yarnpkg.com/eslint-config-airbnb-typescript/-/eslint-config-airbnb-typescript-12.0.0.tgz#4bb6b4b72b1cfc45ef1fa0607735679ceb9a3814" + integrity sha512-TUCVru1Z09eKnVAX5i3XoNzjcCOU3nDQz2/jQGkg1jVYm+25fKClveziSl16celfCq+npU0MBPW/ZnXdGFZ9lw== + dependencies: + "@typescript-eslint/parser" "4.4.1" + eslint-config-airbnb "18.2.0" + eslint-config-airbnb-base "14.2.0" + +eslint-config-airbnb@18.2.0: + version "18.2.0" + resolved "https://registry.yarnpkg.com/eslint-config-airbnb/-/eslint-config-airbnb-18.2.0.tgz#8a82168713effce8fc08e10896a63f1235499dcd" + integrity sha512-Fz4JIUKkrhO0du2cg5opdyPKQXOI2MvF8KUvN2710nJMT6jaRUpRE2swrJftAjVGL7T1otLM5ieo5RqS1v9Udg== + dependencies: + eslint-config-airbnb-base "^14.2.0" + object.assign "^4.1.0" + object.entries "^1.1.2" + +eslint-import-resolver-node@^0.3.4: + version "0.3.4" + resolved "https://registry.yarnpkg.com/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.4.tgz#85ffa81942c25012d8231096ddf679c03042c717" + integrity sha512-ogtf+5AB/O+nM6DIeBUNr2fuT7ot9Qg/1harBfBtaP13ekEWFQEEMP94BCB7zaNW3gyY+8SHYF00rnqYwXKWOA== + dependencies: + debug "^2.6.9" + resolve "^1.13.1" + +eslint-import-resolver-typescript@^2.3.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/eslint-import-resolver-typescript/-/eslint-import-resolver-typescript-2.3.0.tgz#0870988098bc6c6419c87705e6b42bee89425445" + integrity sha512-MHSXvmj5e0SGOOBhBbt7C+fWj1bJbtSYFAD85Xeg8nvUtuooTod2HQb8bfhE9f5QyyNxEfgzqOYFCvmdDIcCuw== + dependencies: + debug "^4.1.1" + glob "^7.1.6" + is-glob "^4.0.1" + resolve "^1.17.0" + tsconfig-paths "^3.9.0" + +eslint-loader@^4.0.2: + version "4.0.2" + resolved "https://registry.yarnpkg.com/eslint-loader/-/eslint-loader-4.0.2.tgz#386a1e21bcb613b3cf2d252a3b708023ccfb41ec" + integrity sha512-EDpXor6lsjtTzZpLUn7KmXs02+nIjGcgees9BYjNkWra3jVq5vVa8IoCKgzT2M7dNNeoMBtaSG83Bd40N3poLw== + dependencies: + find-cache-dir "^3.3.1" + fs-extra "^8.1.0" + loader-utils "^2.0.0" + object-hash "^2.0.3" + schema-utils "^2.6.5" + +eslint-module-utils@^2.6.0: + version "2.6.0" + resolved "https://registry.yarnpkg.com/eslint-module-utils/-/eslint-module-utils-2.6.0.tgz#579ebd094f56af7797d19c9866c9c9486629bfa6" + integrity sha512-6j9xxegbqe8/kZY8cYpcp0xhbK0EgJlg3g9mib3/miLaExuuwc3n5UEfSnU6hWMbT0FAYVvDbL9RrRgpUeQIvA== + dependencies: + debug "^2.6.9" + pkg-dir "^2.0.0" + +eslint-plugin-import@^2.22.1: + version "2.22.1" + resolved "https://registry.yarnpkg.com/eslint-plugin-import/-/eslint-plugin-import-2.22.1.tgz#0896c7e6a0cf44109a2d97b95903c2bb689d7702" + integrity sha512-8K7JjINHOpH64ozkAhpT3sd+FswIZTfMZTjdx052pnWrgRCVfp8op9tbjpAk3DdUeI/Ba4C8OjdC0r90erHEOw== + dependencies: + array-includes "^3.1.1" + array.prototype.flat "^1.2.3" + contains-path "^0.1.0" + debug "^2.6.9" + doctrine "1.5.0" + eslint-import-resolver-node "^0.3.4" + eslint-module-utils "^2.6.0" + has "^1.0.3" + minimatch "^3.0.4" + object.values "^1.1.1" + read-pkg-up "^2.0.0" + resolve "^1.17.0" + tsconfig-paths "^3.9.0" + +eslint-plugin-react-hooks@^4.2.0: + version "4.2.0" + resolved "https://registry.yarnpkg.com/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-4.2.0.tgz#8c229c268d468956334c943bb45fc860280f5556" + integrity sha512-623WEiZJqxR7VdxFCKLI6d6LLpwJkGPYKODnkH3D7WpOG5KM8yWueBd8TLsNAetEJNF5iJmolaAKO3F8yzyVBQ== + +eslint-plugin-react@^7.21.5: + version "7.22.0" + resolved "https://registry.yarnpkg.com/eslint-plugin-react/-/eslint-plugin-react-7.22.0.tgz#3d1c542d1d3169c45421c1215d9470e341707269" + integrity sha512-p30tuX3VS+NWv9nQot9xIGAHBXR0+xJVaZriEsHoJrASGCJZDJ8JLNM0YqKqI0AKm6Uxaa1VUHoNEibxRCMQHA== + dependencies: + array-includes "^3.1.1" + array.prototype.flatmap "^1.2.3" + doctrine "^2.1.0" + has "^1.0.3" + jsx-ast-utils "^2.4.1 || ^3.0.0" + object.entries "^1.1.2" + object.fromentries "^2.0.2" + object.values "^1.1.1" + prop-types "^15.7.2" + resolve "^1.18.1" + string.prototype.matchall "^4.0.2" + +eslint-scope@^5.0.0, eslint-scope@^5.1.1: + version "5.1.1" + resolved "https://registry.yarnpkg.com/eslint-scope/-/eslint-scope-5.1.1.tgz#e786e59a66cb92b3f6c1fb0d508aab174848f48c" + integrity sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw== + dependencies: + esrecurse "^4.3.0" + estraverse "^4.1.1" + +eslint-utils@^2.0.0, eslint-utils@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/eslint-utils/-/eslint-utils-2.1.0.tgz#d2de5e03424e707dc10c74068ddedae708741b27" + integrity sha512-w94dQYoauyvlDc43XnGB8lU3Zt713vNChgt4EWwhXAP2XkBvndfxF0AgIqKOOasjPIPzj9JqgwkwbCYD0/V3Zg== + dependencies: + eslint-visitor-keys "^1.1.0" + +eslint-visitor-keys@^1.1.0, eslint-visitor-keys@^1.3.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-1.3.0.tgz#30ebd1ef7c2fdff01c3a4f151044af25fab0523e" + integrity sha512-6J72N8UNa462wa/KFODt/PJ3IU60SDpC3QXC1Hjc1BXXpfL2C9R5+AU7jhe0F6GREqVMh4Juu+NY7xn+6dipUQ== + +eslint-visitor-keys@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-2.0.0.tgz#21fdc8fbcd9c795cc0321f0563702095751511a8" + integrity sha512-QudtT6av5WXels9WjIM7qz1XD1cWGvX4gGXvp/zBn9nXG02D0utdU3Em2m/QjTnrsk6bBjmCygl3rmj118msQQ== + +eslint@^7.11.0: + version "7.18.0" + resolved "https://registry.yarnpkg.com/eslint/-/eslint-7.18.0.tgz#7fdcd2f3715a41fe6295a16234bd69aed2c75e67" + integrity sha512-fbgTiE8BfUJZuBeq2Yi7J3RB3WGUQ9PNuNbmgi6jt9Iv8qrkxfy19Ds3OpL1Pm7zg3BtTVhvcUZbIRQ0wmSjAQ== + dependencies: + "@babel/code-frame" "^7.0.0" + "@eslint/eslintrc" "^0.3.0" + ajv "^6.10.0" + chalk "^4.0.0" + cross-spawn "^7.0.2" + debug "^4.0.1" + doctrine "^3.0.0" + enquirer "^2.3.5" + eslint-scope "^5.1.1" + eslint-utils "^2.1.0" + eslint-visitor-keys "^2.0.0" + espree "^7.3.1" + esquery "^1.2.0" + esutils "^2.0.2" + file-entry-cache "^6.0.0" + functional-red-black-tree "^1.0.1" + glob-parent "^5.0.0" + globals "^12.1.0" + ignore "^4.0.6" + import-fresh "^3.0.0" + imurmurhash "^0.1.4" + is-glob "^4.0.0" + js-yaml "^3.13.1" + json-stable-stringify-without-jsonify "^1.0.1" + levn "^0.4.1" + lodash "^4.17.20" + minimatch "^3.0.4" + natural-compare "^1.4.0" + optionator "^0.9.1" + progress "^2.0.0" + regexpp "^3.1.0" + semver "^7.2.1" + strip-ansi "^6.0.0" + strip-json-comments "^3.1.0" + table "^6.0.4" + text-table "^0.2.0" + v8-compile-cache "^2.0.3" + +espree@^7.3.0, espree@^7.3.1: + version "7.3.1" + resolved "https://registry.yarnpkg.com/espree/-/espree-7.3.1.tgz#f2df330b752c6f55019f8bd89b7660039c1bbbb6" + integrity sha512-v3JCNCE64umkFpmkFGqzVKsOT0tN1Zr+ueqLZfpV1Ob8e+CEgPWa+OxCoGH3tnhimMKIaBm4m/vaRpJ/krRz2g== + dependencies: + acorn "^7.4.0" + acorn-jsx "^5.3.1" + eslint-visitor-keys "^1.3.0" + +esprima@^4.0.0: + version "4.0.1" + resolved "https://registry.yarnpkg.com/esprima/-/esprima-4.0.1.tgz#13b04cdb3e6c5d19df91ab6987a8695619b0aa71" + integrity sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A== + +esquery@^1.2.0: + version "1.3.1" + resolved "https://registry.yarnpkg.com/esquery/-/esquery-1.3.1.tgz#b78b5828aa8e214e29fb74c4d5b752e1c033da57" + integrity sha512-olpvt9QG0vniUBZspVRN6lwB7hOZoTRtT+jzR+tS4ffYx2mzbw+z0XCOk44aaLYKApNX5nMm+E+P6o25ip/DHQ== + dependencies: + estraverse "^5.1.0" + +esrecurse@^4.3.0: + version "4.3.0" + resolved "https://registry.yarnpkg.com/esrecurse/-/esrecurse-4.3.0.tgz#7ad7964d679abb28bee72cec63758b1c5d2c9921" + integrity sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag== + dependencies: + estraverse "^5.2.0" + +estraverse@^4.1.1: + version "4.3.0" + resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-4.3.0.tgz#398ad3f3c5a24948be7725e83d11a7de28cdbd1d" + integrity sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw== + +estraverse@^5.1.0, estraverse@^5.2.0: + version "5.2.0" + resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-5.2.0.tgz#307df42547e6cc7324d3cf03c155d5cdb8c53880" + integrity sha512-BxbNGGNm0RyRYvUdHpIwv9IWzeM9XClbOxwoATuFdOE7ZE6wHL+HQ5T8hoPM+zHvmKzzsEqhgy0GrQ5X13afiQ== + +esutils@^2.0.2: + version "2.0.3" + resolved "https://registry.yarnpkg.com/esutils/-/esutils-2.0.3.tgz#74d2eb4de0b8da1293711910d50775b9b710ef64" + integrity sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g== + +etag@~1.8.1: + version "1.8.1" + resolved "https://registry.yarnpkg.com/etag/-/etag-1.8.1.tgz#41ae2eeb65efa62268aebfea83ac7d79299b0887" + integrity sha1-Qa4u62XvpiJorr/qg6x9eSmbCIc= + +eventemitter3@^4.0.0, eventemitter3@^4.0.1: + version "4.0.7" + resolved "https://registry.yarnpkg.com/eventemitter3/-/eventemitter3-4.0.7.tgz#2de9b68f6528d5644ef5c59526a1b4a07306169f" + integrity sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw== + +events@^3.2.0: + version "3.2.0" + resolved "https://registry.yarnpkg.com/events/-/events-3.2.0.tgz#93b87c18f8efcd4202a461aec4dfc0556b639379" + integrity sha512-/46HWwbfCX2xTawVfkKLGxMifJYQBWMwY1mjywRtb4c9x8l5NP3KoJtnIOiL1hfdRkIuYhETxQlo62IF8tcnlg== + +eventsource@^1.0.7: + version "1.0.7" + resolved "https://registry.yarnpkg.com/eventsource/-/eventsource-1.0.7.tgz#8fbc72c93fcd34088090bc0a4e64f4b5cee6d8d0" + integrity sha512-4Ln17+vVT0k8aWq+t/bF5arcS3EpT9gYtW66EPacdj/mAFevznsnyoHLPy2BA8gbIQeIHoPsvwmfBftfcG//BQ== + dependencies: + original "^1.0.0" + +execa@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/execa/-/execa-1.0.0.tgz#c6236a5bb4df6d6f15e88e7f017798216749ddd8" + integrity sha512-adbxcyWV46qiHyvSp50TKt05tB4tK3HcmF7/nxfAdhnox83seTDbwnaqKO4sXRy7roHAIFqJP/Rw/AuEbX61LA== + dependencies: + cross-spawn "^6.0.0" + get-stream "^4.0.0" + is-stream "^1.1.0" + npm-run-path "^2.0.0" + p-finally "^1.0.0" + signal-exit "^3.0.0" + strip-eof "^1.0.0" + +execa@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/execa/-/execa-5.0.0.tgz#4029b0007998a841fbd1032e5f4de86a3c1e3376" + integrity sha512-ov6w/2LCiuyO4RLYGdpFGjkcs0wMTgGE8PrkTHikeUy5iJekXyPIKUjifk5CsE0pt7sMCrMZ3YNqoCj6idQOnQ== + dependencies: + cross-spawn "^7.0.3" + get-stream "^6.0.0" + human-signals "^2.1.0" + is-stream "^2.0.0" + merge-stream "^2.0.0" + npm-run-path "^4.0.1" + onetime "^5.1.2" + signal-exit "^3.0.3" + strip-final-newline "^2.0.0" + +execall@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/execall/-/execall-2.0.0.tgz#16a06b5fe5099df7d00be5d9c06eecded1663b45" + integrity sha512-0FU2hZ5Hh6iQnarpRtQurM/aAvp3RIbfvgLHrcqJYzhXyV2KFruhuChf9NC6waAhiUR7FFtlugkI4p7f2Fqlow== + dependencies: + clone-regexp "^2.1.0" + +expand-brackets@^2.1.4: + version "2.1.4" + resolved "https://registry.yarnpkg.com/expand-brackets/-/expand-brackets-2.1.4.tgz#b77735e315ce30f6b6eff0f83b04151a22449622" + integrity sha1-t3c14xXOMPa27/D4OwQVGiJEliI= + dependencies: + debug "^2.3.3" + define-property "^0.2.5" + extend-shallow "^2.0.1" + posix-character-classes "^0.1.0" + regex-not "^1.0.0" + snapdragon "^0.8.1" + to-regex "^3.0.1" + +express@^4.17.1: + version "4.17.1" + resolved "https://registry.yarnpkg.com/express/-/express-4.17.1.tgz#4491fc38605cf51f8629d39c2b5d026f98a4c134" + integrity sha512-mHJ9O79RqluphRrcw2X/GTh3k9tVv8YcoyY4Kkh4WDMUYKRZUq0h1o0w2rrrxBqM7VoeUVqgb27xlEMXTnYt4g== + dependencies: + accepts "~1.3.7" + array-flatten "1.1.1" + body-parser "1.19.0" + content-disposition "0.5.3" + content-type "~1.0.4" + cookie "0.4.0" + cookie-signature "1.0.6" + debug "2.6.9" + depd "~1.1.2" + encodeurl "~1.0.2" + escape-html "~1.0.3" + etag "~1.8.1" + finalhandler "~1.1.2" + fresh "0.5.2" + merge-descriptors "1.0.1" + methods "~1.1.2" + on-finished "~2.3.0" + parseurl "~1.3.3" + path-to-regexp "0.1.7" + proxy-addr "~2.0.5" + qs "6.7.0" + range-parser "~1.2.1" + safe-buffer "5.1.2" + send "0.17.1" + serve-static "1.14.1" + setprototypeof "1.1.1" + statuses "~1.5.0" + type-is "~1.6.18" + utils-merge "1.0.1" + vary "~1.1.2" + +extend-shallow@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/extend-shallow/-/extend-shallow-2.0.1.tgz#51af7d614ad9a9f610ea1bafbb989d6b1c56890f" + integrity sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8= + dependencies: + is-extendable "^0.1.0" + +extend-shallow@^3.0.0, extend-shallow@^3.0.2: + version "3.0.2" + resolved "https://registry.yarnpkg.com/extend-shallow/-/extend-shallow-3.0.2.tgz#26a71aaf073b39fb2127172746131c2704028db8" + integrity sha1-Jqcarwc7OfshJxcnRhMcJwQCjbg= + dependencies: + assign-symbols "^1.0.0" + is-extendable "^1.0.1" + +extend@^3.0.0, extend@^3.0.1: + version "3.0.2" + resolved "https://registry.yarnpkg.com/extend/-/extend-3.0.2.tgz#f8b1136b4071fbd8eb140aff858b1019ec2915fa" + integrity sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g== + +extglob@^2.0.4: + version "2.0.4" + resolved "https://registry.yarnpkg.com/extglob/-/extglob-2.0.4.tgz#ad00fe4dc612a9232e8718711dc5cb5ab0285543" + integrity sha512-Nmb6QXkELsuBr24CJSkilo6UHHgbekK5UiZgfE6UHD3Eb27YC6oD+bhcT+tJ6cl8dmsgdQxnWlcry8ksBIBLpw== + dependencies: + array-unique "^0.3.2" + define-property "^1.0.0" + expand-brackets "^2.1.4" + extend-shallow "^2.0.1" + fragment-cache "^0.2.1" + regex-not "^1.0.0" + snapdragon "^0.8.1" + to-regex "^3.0.1" + +fast-deep-equal@^3.1.1: + version "3.1.3" + resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz#3a7d56b559d6cbc3eb512325244e619a65c6c525" + integrity sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q== + +fast-glob@^3.1.1, fast-glob@^3.2.4, fast-glob@^3.2.5: + version "3.2.5" + resolved "https://registry.yarnpkg.com/fast-glob/-/fast-glob-3.2.5.tgz#7939af2a656de79a4f1901903ee8adcaa7cb9661" + integrity sha512-2DtFcgT68wiTTiwZ2hNdJfcHNke9XOfnwmBRWXhmeKM8rF0TGwmC/Qto3S7RoZKp5cilZbxzO5iTNTQsJ+EeDg== + dependencies: + "@nodelib/fs.stat" "^2.0.2" + "@nodelib/fs.walk" "^1.2.3" + glob-parent "^5.1.0" + merge2 "^1.3.0" + micromatch "^4.0.2" + picomatch "^2.2.1" + +fast-json-stable-stringify@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz#874bf69c6f404c2b5d99c481341399fd55892633" + integrity sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw== + +fast-levenshtein@^2.0.6: + version "2.0.6" + resolved "https://registry.yarnpkg.com/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz#3d8a5c66883a16a30ca8643e851f19baa7797917" + integrity sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc= + +fastest-levenshtein@^1.0.12: + version "1.0.12" + resolved "https://registry.yarnpkg.com/fastest-levenshtein/-/fastest-levenshtein-1.0.12.tgz#9990f7d3a88cc5a9ffd1f1745745251700d497e2" + integrity sha512-On2N+BpYJ15xIC974QNVuYGMOlEVt4s0EOI3wwMqOmK1fdDY+FN/zltPV8vosq4ad4c/gJ1KHScUn/6AWIgiow== + +fastq@^1.6.0: + version "1.10.0" + resolved "https://registry.yarnpkg.com/fastq/-/fastq-1.10.0.tgz#74dbefccade964932cdf500473ef302719c652bb" + integrity sha512-NL2Qc5L3iQEsyYzweq7qfgy5OtXCmGzGvhElGEd/SoFWEMOEczNh5s5ocaF01HDetxz+p8ecjNPA6cZxxIHmzA== + dependencies: + reusify "^1.0.4" + +faye-websocket@^0.11.3: + version "0.11.3" + resolved "https://registry.yarnpkg.com/faye-websocket/-/faye-websocket-0.11.3.tgz#5c0e9a8968e8912c286639fde977a8b209f2508e" + integrity sha512-D2y4bovYpzziGgbHYtGCMjlJM36vAl/y+xUyn1C+FVx8szd1E+86KwVw6XvYSzOP8iMpm1X0I4xJD+QtUb36OA== + dependencies: + websocket-driver ">=0.5.1" + +file-entry-cache@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/file-entry-cache/-/file-entry-cache-6.0.0.tgz#7921a89c391c6d93efec2169ac6bf300c527ea0a" + integrity sha512-fqoO76jZ3ZnYrXLDRxBR1YvOvc0k844kcOg40bgsPrE25LAb/PDqTY+ho64Xh2c8ZXgIKldchCFHczG2UVRcWA== + dependencies: + flat-cache "^3.0.4" + +file-loader@^6.1.1: + version "6.2.0" + resolved "https://registry.yarnpkg.com/file-loader/-/file-loader-6.2.0.tgz#baef7cf8e1840df325e4390b4484879480eebe4d" + integrity sha512-qo3glqyTa61Ytg4u73GultjHGjdRyig3tG6lPtyX/jOEJvHif9uB0/OCI2Kif6ctF3caQTW2G5gym21oAsI4pw== + dependencies: + loader-utils "^2.0.0" + schema-utils "^3.0.0" + +file-uri-to-path@1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz#553a7b8446ff6f684359c445f1e37a05dacc33dd" + integrity sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw== + +fill-range@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-4.0.0.tgz#d544811d428f98eb06a63dc402d2403c328c38f7" + integrity sha1-1USBHUKPmOsGpj3EAtJAPDKMOPc= + dependencies: + extend-shallow "^2.0.1" + is-number "^3.0.0" + repeat-string "^1.6.1" + to-regex-range "^2.1.0" + +fill-range@^7.0.1: + version "7.0.1" + resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-7.0.1.tgz#1919a6a7c75fe38b2c7c77e5198535da9acdda40" + integrity sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ== + dependencies: + to-regex-range "^5.0.1" + +finalhandler@~1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/finalhandler/-/finalhandler-1.1.2.tgz#b7e7d000ffd11938d0fdb053506f6ebabe9f587d" + integrity sha512-aAWcW57uxVNrQZqFXjITpW3sIUQmHGG3qSb9mUah9MgMC4NeWhNOlNjXEYq3HjRAvL6arUviZGGJsBg6z0zsWA== + dependencies: + debug "2.6.9" + encodeurl "~1.0.2" + escape-html "~1.0.3" + on-finished "~2.3.0" + parseurl "~1.3.3" + statuses "~1.5.0" + unpipe "~1.0.0" + +find-cache-dir@^3.3.1: + version "3.3.1" + resolved "https://registry.yarnpkg.com/find-cache-dir/-/find-cache-dir-3.3.1.tgz#89b33fad4a4670daa94f855f7fbe31d6d84fe880" + integrity sha512-t2GDMt3oGC/v+BMwzmllWDuJF/xcDtE5j/fCGbqDD7OLuJkj0cfh1YSA5VKPvwMeLFLNDBkwOKZ2X85jGLVftQ== + dependencies: + commondir "^1.0.1" + make-dir "^3.0.2" + pkg-dir "^4.1.0" + +find-up@^2.0.0, find-up@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/find-up/-/find-up-2.1.0.tgz#45d1b7e506c717ddd482775a2b77920a3c0c57a7" + integrity sha1-RdG35QbHF93UgndaK3eSCjwMV6c= + dependencies: + locate-path "^2.0.0" + +find-up@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/find-up/-/find-up-3.0.0.tgz#49169f1d7993430646da61ecc5ae355c21c97b73" + integrity sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg== + dependencies: + locate-path "^3.0.0" + +find-up@^4.0.0, find-up@^4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/find-up/-/find-up-4.1.0.tgz#97afe7d6cdc0bc5928584b7c8d7b16e8a9aa5d19" + integrity sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw== + dependencies: + locate-path "^5.0.0" + path-exists "^4.0.0" + +find-up@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/find-up/-/find-up-5.0.0.tgz#4c92819ecb7083561e4f4a240a86be5198f536fc" + integrity sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng== + dependencies: + locate-path "^6.0.0" + path-exists "^4.0.0" + +flat-cache@^3.0.4: + version "3.0.4" + resolved "https://registry.yarnpkg.com/flat-cache/-/flat-cache-3.0.4.tgz#61b0338302b2fe9f957dcc32fc2a87f1c3048b11" + integrity sha512-dm9s5Pw7Jc0GvMYbshN6zchCA9RgQlzzEZX3vylR9IqFfS8XciblUXOKfW6SiuJ0e13eDYZoZV5wdrev7P3Nwg== + dependencies: + flatted "^3.1.0" + rimraf "^3.0.2" + +flatted@^3.1.0: + version "3.1.1" + resolved "https://registry.yarnpkg.com/flatted/-/flatted-3.1.1.tgz#c4b489e80096d9df1dfc97c79871aea7c617c469" + integrity sha512-zAoAQiudy+r5SvnSw3KJy5os/oRJYHzrzja/tBDqrZtNhUw8bt6y8OBzMWcjWr+8liV8Eb6yOhw8WZ7VFZ5ZzA== + +flatten@^1.0.2: + version "1.0.3" + resolved "https://registry.yarnpkg.com/flatten/-/flatten-1.0.3.tgz#c1283ac9f27b368abc1e36d1ff7b04501a30356b" + integrity sha512-dVsPA/UwQ8+2uoFe5GHtiBMu48dWLTdsuEd7CKGlZlD78r1TTWBvDuFaFGKCo/ZfEr95Uk56vZoX86OsHkUeIg== + +follow-redirects@^1.0.0: + version "1.13.2" + resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.13.2.tgz#dd73c8effc12728ba5cf4259d760ea5fb83e3147" + integrity sha512-6mPTgLxYm3r6Bkkg0vNM0HTjfGrOEtsfbhagQvbxDEsEkpNhw582upBaoRZylzen6krEmxXJgt9Ju6HiI4O7BA== + +for-in@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/for-in/-/for-in-1.0.2.tgz#81068d295a8142ec0ac726c6e2200c30fb6d5e80" + integrity sha1-gQaNKVqBQuwKxybG4iAMMPttXoA= + +formik@^2.2.0: + version "2.2.6" + resolved "https://registry.yarnpkg.com/formik/-/formik-2.2.6.tgz#378a4bafe4b95caf6acf6db01f81f3fe5147559d" + integrity sha512-Kxk2zQRafy56zhLmrzcbryUpMBvT0tal5IvcifK5+4YNGelKsnrODFJ0sZQRMQboblWNym4lAW3bt+tf2vApSA== + dependencies: + deepmerge "^2.1.1" + hoist-non-react-statics "^3.3.0" + lodash "^4.17.14" + lodash-es "^4.17.14" + react-fast-compare "^2.0.1" + tiny-warning "^1.0.2" + tslib "^1.10.0" + +forwarded@~0.1.2: + version "0.1.2" + resolved "https://registry.yarnpkg.com/forwarded/-/forwarded-0.1.2.tgz#98c23dab1175657b8c0573e8ceccd91b0ff18c84" + integrity sha1-mMI9qxF1ZXuMBXPozszZGw/xjIQ= + +fraction.js@^4.0.13: + version "4.0.13" + resolved "https://registry.yarnpkg.com/fraction.js/-/fraction.js-4.0.13.tgz#3c1c315fa16b35c85fffa95725a36fa729c69dfe" + integrity sha512-E1fz2Xs9ltlUp+qbiyx9wmt2n9dRzPsS11Jtdb8D2o+cC7wr9xkkKsVKJuBX0ST+LVS+LhLO+SbLJNtfWcJvXA== + +fragment-cache@^0.2.1: + version "0.2.1" + resolved "https://registry.yarnpkg.com/fragment-cache/-/fragment-cache-0.2.1.tgz#4290fad27f13e89be7f33799c6bc5a0abfff0d19" + integrity sha1-QpD60n8T6Jvn8zeZxrxaCr//DRk= + dependencies: + map-cache "^0.2.2" + +fresh@0.5.2: + version "0.5.2" + resolved "https://registry.yarnpkg.com/fresh/-/fresh-0.5.2.tgz#3d8cadd90d976569fa835ab1f8e4b23a105605a7" + integrity sha1-PYyt2Q2XZWn6g1qx+OSyOhBWBac= + +fs-extra@^8.1.0: + version "8.1.0" + resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-8.1.0.tgz#49d43c45a88cd9677668cb7be1b46efdb8d2e1c0" + integrity sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g== + dependencies: + graceful-fs "^4.2.0" + jsonfile "^4.0.0" + universalify "^0.1.0" + +fs-extra@^9.0.1: + version "9.1.0" + resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-9.1.0.tgz#5954460c764a8da2094ba3554bf839e6b9a7c86d" + integrity sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ== + dependencies: + at-least-node "^1.0.0" + graceful-fs "^4.2.0" + jsonfile "^6.0.1" + universalify "^2.0.0" + +fs-minipass@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/fs-minipass/-/fs-minipass-2.1.0.tgz#7f5036fdbf12c63c169190cbe4199c852271f9fb" + integrity sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg== + dependencies: + minipass "^3.0.0" + +fs.realpath@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f" + integrity sha1-FQStJSMVjKpA20onh8sBQRmU6k8= + +fsevents@^1.2.7: + version "1.2.13" + resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-1.2.13.tgz#f325cb0455592428bcf11b383370ef70e3bfcc38" + integrity sha512-oWb1Z6mkHIskLzEJ/XWX0srkpkTQ7vaopMQkyaEIoq0fmtFVxOthb8cCxeT+p3ynTdkk/RZwbgG4brR5BeWECw== + dependencies: + bindings "^1.5.0" + nan "^2.12.1" + +function-bind@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.1.tgz#a56899d3ea3c9bab874bb9773b7c5ede92f4895d" + integrity sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A== + +functional-red-black-tree@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/functional-red-black-tree/-/functional-red-black-tree-1.0.1.tgz#1b0ab3bd553b2a0d6399d29c0e3ea0b252078327" + integrity sha1-GwqzvVU7Kg1jmdKcDj6gslIHgyc= + +generic-names@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/generic-names/-/generic-names-2.0.1.tgz#f8a378ead2ccaa7a34f0317b05554832ae41b872" + integrity sha512-kPCHWa1m9wGG/OwQpeweTwM/PYiQLrUIxXbt/P4Nic3LbGjCP0YwrALHW1uNLKZ0LIMg+RF+XRlj2ekT9ZlZAQ== + dependencies: + loader-utils "^1.1.0" + +gensync@^1.0.0-beta.1: + version "1.0.0-beta.2" + resolved "https://registry.yarnpkg.com/gensync/-/gensync-1.0.0-beta.2.tgz#32a6ee76c3d7f52d46b2b1ae5d93fea8580a25e0" + integrity sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg== + +get-caller-file@^2.0.1: + version "2.0.5" + resolved "https://registry.yarnpkg.com/get-caller-file/-/get-caller-file-2.0.5.tgz#4f94412a82db32f36e3b0b9741f8a97feb031f7e" + integrity sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg== + +get-intrinsic@^1.0.1, get-intrinsic@^1.0.2, get-intrinsic@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/get-intrinsic/-/get-intrinsic-1.1.0.tgz#892e62931e6938c8a23ea5aaebcfb67bd97da97e" + integrity sha512-M11rgtQp5GZMZzDL7jLTNxbDfurpzuau5uqRWDPvlHjfvg3TdScAZo96GLvhMjImrmR8uAt0FS2RLoMrfWGKlg== + dependencies: + function-bind "^1.1.1" + has "^1.0.3" + has-symbols "^1.0.1" + +get-stdin@^8.0.0: + version "8.0.0" + resolved "https://registry.yarnpkg.com/get-stdin/-/get-stdin-8.0.0.tgz#cbad6a73feb75f6eeb22ba9e01f89aa28aa97a53" + integrity sha512-sY22aA6xchAzprjyqmSEQv4UbAAzRN0L2dQB0NlN5acTTK9Don6nhoc3eAbUnpZiCANAMfd/+40kVdKfFygohg== + +get-stream@^4.0.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-4.1.0.tgz#c1b255575f3dc21d59bfc79cd3d2b46b1c3a54b5" + integrity sha512-GMat4EJ5161kIy2HevLlr4luNjBgvmj413KaQA7jt4V8B4RDsfpHk7WQ9GVqfYyyx8OS/L66Kox+rJRNklLK7w== + dependencies: + pump "^3.0.0" + +get-stream@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-6.0.0.tgz#3e0012cb6827319da2706e601a1583e8629a6718" + integrity sha512-A1B3Bh1UmL0bidM/YX2NsCOTnGJePL9rO/M+Mw3m9f2gUpfokS0hi5Eah0WSUEWZdZhIZtMjkIYS7mDfOqNHbg== + +get-value@^2.0.3, get-value@^2.0.6: + version "2.0.6" + resolved "https://registry.yarnpkg.com/get-value/-/get-value-2.0.6.tgz#dc15ca1c672387ca76bd37ac0a395ba2042a2c28" + integrity sha1-3BXKHGcjh8p2vTesCjlbogQqLCg= + +glob-parent@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-3.1.0.tgz#9e6af6299d8d3bd2bd40430832bd113df906c5ae" + integrity sha1-nmr2KZ2NO9K9QEMIMr0RPfkGxa4= + dependencies: + is-glob "^3.1.0" + path-dirname "^1.0.0" + +glob-parent@^5.0.0, glob-parent@^5.1.0, glob-parent@^5.1.1: + version "5.1.1" + resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-5.1.1.tgz#b6c1ef417c4e5663ea498f1c45afac6916bbc229" + integrity sha512-FnI+VGOpnlGHWZxthPGR+QhR78fuiK0sNLkHQv+bL9fQi57lNNdquIbna/WrfROrolq8GK5Ek6BiMwqL/voRYQ== + dependencies: + is-glob "^4.0.1" + +glob-to-regexp@^0.4.1: + version "0.4.1" + resolved "https://registry.yarnpkg.com/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz#c75297087c851b9a578bd217dd59a92f59fe546e" + integrity sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw== + +glob@^7.0.3, glob@^7.1.3, glob@^7.1.4, glob@^7.1.6: + version "7.1.6" + resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.6.tgz#141f33b81a7c2492e125594307480c46679278a6" + integrity sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA== + dependencies: + fs.realpath "^1.0.0" + inflight "^1.0.4" + inherits "2" + minimatch "^3.0.4" + once "^1.3.0" + path-is-absolute "^1.0.0" + +global-modules@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/global-modules/-/global-modules-2.0.0.tgz#997605ad2345f27f51539bea26574421215c7780" + integrity sha512-NGbfmJBp9x8IxyJSd1P+otYK8vonoJactOogrVfFRIAEY1ukil8RSKDz2Yo7wh1oihl51l/r6W4epkeKJHqL8A== + dependencies: + global-prefix "^3.0.0" + +global-prefix@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/global-prefix/-/global-prefix-3.0.0.tgz#fc85f73064df69f50421f47f883fe5b913ba9b97" + integrity sha512-awConJSVCHVGND6x3tmMaKcQvwXLhjdkmomy2W+Goaui8YPgYgXJZewhg3fWC+DlfqqQuWg8AwqjGTD2nAPVWg== + dependencies: + ini "^1.3.5" + kind-of "^6.0.2" + which "^1.3.1" + +globals@^11.1.0: + version "11.12.0" + resolved "https://registry.yarnpkg.com/globals/-/globals-11.12.0.tgz#ab8795338868a0babd8525758018c2a7eb95c42e" + integrity sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA== + +globals@^12.1.0: + version "12.4.0" + resolved "https://registry.yarnpkg.com/globals/-/globals-12.4.0.tgz#a18813576a41b00a24a97e7f815918c2e19925f8" + integrity sha512-BWICuzzDvDoH54NHKCseDanAhE3CeDorgDL5MT6LMXXj2WCnd9UC2szdk4AWLfjdgNBCXLUanXYcpBBKOSWGwg== + dependencies: + type-fest "^0.8.1" + +globby@^11.0.1, globby@^11.0.2: + version "11.0.2" + resolved "https://registry.yarnpkg.com/globby/-/globby-11.0.2.tgz#1af538b766a3b540ebfb58a32b2e2d5897321d83" + integrity sha512-2ZThXDvvV8fYFRVIxnrMQBipZQDr7MxKAmQK1vujaj9/7eF0efG7BPUKJ7jP7G5SLF37xKDXvO4S/KKLj/Z0og== + dependencies: + array-union "^2.1.0" + dir-glob "^3.0.1" + fast-glob "^3.1.1" + ignore "^5.1.4" + merge2 "^1.3.0" + slash "^3.0.0" + +globby@^6.1.0: + version "6.1.0" + resolved "https://registry.yarnpkg.com/globby/-/globby-6.1.0.tgz#f5a6d70e8395e21c858fb0489d64df02424d506c" + integrity sha1-9abXDoOV4hyFj7BInWTfAkJNUGw= + dependencies: + array-union "^1.0.1" + glob "^7.0.3" + object-assign "^4.0.1" + pify "^2.0.0" + pinkie-promise "^2.0.0" + +globjoin@^0.1.4: + version "0.1.4" + resolved "https://registry.yarnpkg.com/globjoin/-/globjoin-0.1.4.tgz#2f4494ac8919e3767c5cbb691e9f463324285d43" + integrity sha1-L0SUrIkZ43Z8XLtpHp9GMyQoXUM= + +gonzales-pe@^4.3.0: + version "4.3.0" + resolved "https://registry.yarnpkg.com/gonzales-pe/-/gonzales-pe-4.3.0.tgz#fe9dec5f3c557eead09ff868c65826be54d067b3" + integrity sha512-otgSPpUmdWJ43VXyiNgEYE4luzHCL2pz4wQ0OnDluC6Eg4Ko3Vexy/SrSynglw/eR+OhkzmqFCZa/OFa/RgAOQ== + dependencies: + minimist "^1.2.5" + +graceful-fs@^4.1.11, graceful-fs@^4.1.2, graceful-fs@^4.1.6, graceful-fs@^4.2.0, graceful-fs@^4.2.4: + version "4.2.4" + resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.4.tgz#2256bde14d3632958c465ebc96dc467ca07a29fb" + integrity sha512-WjKPNJF79dtJAVniUlGGWHYGz2jWxT6VhN/4m1NdkbZ2nOsEF+cI1Edgql5zCRhs/VsQYRvrXctxktVXZUkixw== + +handle-thing@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/handle-thing/-/handle-thing-2.0.1.tgz#857f79ce359580c340d43081cc648970d0bb234e" + integrity sha512-9Qn4yBxelxoh2Ow62nP+Ka/kMnOXRi8BXnRaUwezLNhqelnN49xKz4F/dPP8OYLxLxq6JDtZb2i9XznUQbNPTg== + +hard-rejection@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/hard-rejection/-/hard-rejection-2.1.0.tgz#1c6eda5c1685c63942766d79bb40ae773cecd883" + integrity sha512-VIZB+ibDhx7ObhAe7OVtoEbuP4h/MuOTHJ+J8h/eBXotJYl0fBgR72xDFCKgIh22OJZIOVNxBMWuhAr10r8HdA== + +has-flag@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-3.0.0.tgz#b5d454dc2199ae225699f3467e5a07f3b955bafd" + integrity sha1-tdRU3CGZriJWmfNGfloH87lVuv0= + +has-flag@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-4.0.0.tgz#944771fd9c81c81265c4d6941860da06bb59479b" + integrity sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ== + +has-symbols@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/has-symbols/-/has-symbols-1.0.1.tgz#9f5214758a44196c406d9bd76cebf81ec2dd31e8" + integrity sha512-PLcsoqu++dmEIZB+6totNFKq/7Do+Z0u4oT0zKOJNl3lYK6vGwwu2hjHs+68OEZbTjiUE9bgOABXbP/GvrS0Kg== + +has-value@^0.3.1: + version "0.3.1" + resolved "https://registry.yarnpkg.com/has-value/-/has-value-0.3.1.tgz#7b1f58bada62ca827ec0a2078025654845995e1f" + integrity sha1-ex9YutpiyoJ+wKIHgCVlSEWZXh8= + dependencies: + get-value "^2.0.3" + has-values "^0.1.4" + isobject "^2.0.0" + +has-value@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/has-value/-/has-value-1.0.0.tgz#18b281da585b1c5c51def24c930ed29a0be6b177" + integrity sha1-GLKB2lhbHFxR3vJMkw7SmgvmsXc= + dependencies: + get-value "^2.0.6" + has-values "^1.0.0" + isobject "^3.0.0" + +has-values@^0.1.4: + version "0.1.4" + resolved "https://registry.yarnpkg.com/has-values/-/has-values-0.1.4.tgz#6d61de95d91dfca9b9a02089ad384bff8f62b771" + integrity sha1-bWHeldkd/Km5oCCJrThL/49it3E= + +has-values@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/has-values/-/has-values-1.0.0.tgz#95b0b63fec2146619a6fe57fe75628d5a39efe4f" + integrity sha1-lbC2P+whRmGab+V/51Yo1aOe/k8= + dependencies: + is-number "^3.0.0" + kind-of "^4.0.0" + +has@^1.0.0, has@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/has/-/has-1.0.3.tgz#722d7cbfc1f6aa8241f16dd814e011e1f41e8796" + integrity sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw== + dependencies: + function-bind "^1.1.1" + +he@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/he/-/he-1.2.0.tgz#84ae65fa7eafb165fddb61566ae14baf05664f0f" + integrity sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw== + +hex-color-regex@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/hex-color-regex/-/hex-color-regex-1.1.0.tgz#4c06fccb4602fe2602b3c93df82d7e7dbf1a8a8e" + integrity sha512-l9sfDFsuqtOqKDsQdqrMRk0U85RZc0RtOR9yPI7mRVOa4FsR/BVnZ0shmQRM96Ji99kYZP/7hn1cedc1+ApsTQ== + +history@^4.9.0: + version "4.10.1" + resolved "https://registry.yarnpkg.com/history/-/history-4.10.1.tgz#33371a65e3a83b267434e2b3f3b1b4c58aad4cf3" + integrity sha512-36nwAD620w12kuzPAsyINPWJqlNbij+hpK1k9XRloDtym8mxzGYl2c17LnV6IAGB2Dmg4tEa7G7DlawS0+qjew== + dependencies: + "@babel/runtime" "^7.1.2" + loose-envify "^1.2.0" + resolve-pathname "^3.0.0" + tiny-invariant "^1.0.2" + tiny-warning "^1.0.0" + value-equal "^1.0.1" + +hoist-non-react-statics@^3.1.0, hoist-non-react-statics@^3.3.0, hoist-non-react-statics@^3.3.2: + version "3.3.2" + resolved "https://registry.yarnpkg.com/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz#ece0acaf71d62c2969c2ec59feff42a4b1a85b45" + integrity sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw== + dependencies: + react-is "^16.7.0" + +hosted-git-info@^2.1.4: + version "2.8.8" + resolved "https://registry.yarnpkg.com/hosted-git-info/-/hosted-git-info-2.8.8.tgz#7539bd4bc1e0e0a895815a2e0262420b12858488" + integrity sha512-f/wzC2QaWBs7t9IYqB4T3sR1xviIViXJRJTWBlx2Gf3g0Xi5vI7Yy4koXQ1c9OYDGHN9sBy1DQ2AB8fqZBWhUg== + +hosted-git-info@^3.0.6: + version "3.0.7" + resolved "https://registry.yarnpkg.com/hosted-git-info/-/hosted-git-info-3.0.7.tgz#a30727385ea85acfcee94e0aad9e368c792e036c" + integrity sha512-fWqc0IcuXs+BmE9orLDyVykAG9GJtGLGuZAAqgcckPgv5xad4AcXGIv8galtQvlwutxSlaMcdw7BUtq2EIvqCQ== + dependencies: + lru-cache "^6.0.0" + +hpack.js@^2.1.6: + version "2.1.6" + resolved "https://registry.yarnpkg.com/hpack.js/-/hpack.js-2.1.6.tgz#87774c0949e513f42e84575b3c45681fade2a0b2" + integrity sha1-h3dMCUnlE/QuhFdbPEVoH63ioLI= + dependencies: + inherits "^2.0.1" + obuf "^1.0.0" + readable-stream "^2.0.1" + wbuf "^1.1.0" + +hsl-regex@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/hsl-regex/-/hsl-regex-1.0.0.tgz#d49330c789ed819e276a4c0d272dffa30b18fe6e" + integrity sha1-1JMwx4ntgZ4nakwNJy3/owsY/m4= + +hsla-regex@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/hsla-regex/-/hsla-regex-1.0.0.tgz#c1ce7a3168c8c6614033a4b5f7877f3b225f9c38" + integrity sha1-wc56MWjIxmFAM6S194d/OyJfnDg= + +html-comment-regex@^1.1.0: + version "1.1.2" + resolved "https://registry.yarnpkg.com/html-comment-regex/-/html-comment-regex-1.1.2.tgz#97d4688aeb5c81886a364faa0cad1dda14d433a7" + integrity sha512-P+M65QY2JQ5Y0G9KKdlDpo0zK+/OHptU5AaBwUfAIDJZk1MYf32Frm84EcOytfJE0t5JvkAnKlmjsXDnWzCJmQ== + +html-entities@^1.3.1: + version "1.4.0" + resolved "https://registry.yarnpkg.com/html-entities/-/html-entities-1.4.0.tgz#cfbd1b01d2afaf9adca1b10ae7dffab98c71d2dc" + integrity sha512-8nxjcBcd8wovbeKx7h3wTji4e6+rhaVuPNpMqwWgnHh+N9ToqsCs6XztWRBPQ+UtzsoMAdKZtUENoVzU/EMtZA== + +html-minifier-terser@^5.0.1: + version "5.1.1" + resolved "https://registry.yarnpkg.com/html-minifier-terser/-/html-minifier-terser-5.1.1.tgz#922e96f1f3bb60832c2634b79884096389b1f054" + integrity sha512-ZPr5MNObqnV/T9akshPKbVgyOqLmy+Bxo7juKCfTfnjNniTAMdy4hz21YQqoofMBJD2kdREaqPPdThoR78Tgxg== + dependencies: + camel-case "^4.1.1" + clean-css "^4.2.3" + commander "^4.1.1" + he "^1.2.0" + param-case "^3.0.3" + relateurl "^0.2.7" + terser "^4.6.3" + +html-tags@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/html-tags/-/html-tags-3.1.0.tgz#7b5e6f7e665e9fb41f30007ed9e0d41e97fb2140" + integrity sha512-1qYz89hW3lFDEazhjW0yVAV87lw8lVkrJocr72XmBkMKsoSVJCQx3W8BXsC7hO2qAt8BoVjYjtAcZ9perqGnNg== + +html-webpack-plugin@^4.5.0: + version "4.5.1" + resolved "https://registry.yarnpkg.com/html-webpack-plugin/-/html-webpack-plugin-4.5.1.tgz#40aaf1b5cb78f2f23a83333999625c20929cda65" + integrity sha512-yzK7RQZwv9xB+pcdHNTjcqbaaDZ+5L0zJHXfi89iWIZmb/FtzxhLk0635rmJihcQbs3ZUF27Xp4oWGx6EK56zg== + dependencies: + "@types/html-minifier-terser" "^5.0.0" + "@types/tapable" "^1.0.5" + "@types/webpack" "^4.41.8" + html-minifier-terser "^5.0.1" + loader-utils "^1.2.3" + lodash "^4.17.20" + pretty-error "^2.1.1" + tapable "^1.1.3" + util.promisify "1.0.0" + +htmlparser2@^3.10.0, htmlparser2@^3.10.1: + version "3.10.1" + resolved "https://registry.yarnpkg.com/htmlparser2/-/htmlparser2-3.10.1.tgz#bd679dc3f59897b6a34bb10749c855bb53a9392f" + integrity sha512-IgieNijUMbkDovyoKObU1DUhm1iwNYE/fuifEoEHfd1oZKZDaONBSkal7Y01shxsM49R4XaMdGez3WnF9UfiCQ== + dependencies: + domelementtype "^1.3.1" + domhandler "^2.3.0" + domutils "^1.5.1" + entities "^1.1.1" + inherits "^2.0.1" + readable-stream "^3.1.1" + +http-deceiver@^1.2.7: + version "1.2.7" + resolved "https://registry.yarnpkg.com/http-deceiver/-/http-deceiver-1.2.7.tgz#fa7168944ab9a519d337cb0bec7284dc3e723d87" + integrity sha1-+nFolEq5pRnTN8sL7HKE3D5yPYc= + +http-errors@1.7.2: + version "1.7.2" + resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-1.7.2.tgz#4f5029cf13239f31036e5b2e55292bcfbcc85c8f" + integrity sha512-uUQBt3H/cSIVfch6i1EuPNy/YsRSOUBXTVfZ+yR7Zjez3qjBz6i9+i4zjNaoqcoFVI4lQJ5plg63TvGfRSDCRg== + dependencies: + depd "~1.1.2" + inherits "2.0.3" + setprototypeof "1.1.1" + statuses ">= 1.5.0 < 2" + toidentifier "1.0.0" + +http-errors@~1.6.2: + version "1.6.3" + resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-1.6.3.tgz#8b55680bb4be283a0b5bf4ea2e38580be1d9320d" + integrity sha1-i1VoC7S+KDoLW/TqLjhYC+HZMg0= + dependencies: + depd "~1.1.2" + inherits "2.0.3" + setprototypeof "1.1.0" + statuses ">= 1.4.0 < 2" + +http-errors@~1.7.2: + version "1.7.3" + resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-1.7.3.tgz#6c619e4f9c60308c38519498c14fbb10aacebb06" + integrity sha512-ZTTX0MWrsQ2ZAhA1cejAwDLycFsd7I7nVtnkT3Ol0aqodaKW+0CTZDQ1uBv5whptCnc8e8HeRRJxRs0kmm/Qfw== + dependencies: + depd "~1.1.2" + inherits "2.0.4" + setprototypeof "1.1.1" + statuses ">= 1.5.0 < 2" + toidentifier "1.0.0" + +http-parser-js@>=0.5.1: + version "0.5.3" + resolved "https://registry.yarnpkg.com/http-parser-js/-/http-parser-js-0.5.3.tgz#01d2709c79d41698bb01d4decc5e9da4e4a033d9" + integrity sha512-t7hjvef/5HEK7RWTdUzVUhl8zkEu+LlaE0IYzdMuvbSDipxBRpOn4Uhw8ZyECEa808iVT8XCjzo6xmYt4CiLZg== + +http-proxy-middleware@0.19.1: + version "0.19.1" + resolved "https://registry.yarnpkg.com/http-proxy-middleware/-/http-proxy-middleware-0.19.1.tgz#183c7dc4aa1479150306498c210cdaf96080a43a" + integrity sha512-yHYTgWMQO8VvwNS22eLLloAkvungsKdKTLO8AJlftYIKNfJr3GK3zK0ZCfzDDGUBttdGc8xFy1mCitvNKQtC3Q== + dependencies: + http-proxy "^1.17.0" + is-glob "^4.0.0" + lodash "^4.17.11" + micromatch "^3.1.10" + +http-proxy-middleware@^1.0.6: + version "1.0.6" + resolved "https://registry.yarnpkg.com/http-proxy-middleware/-/http-proxy-middleware-1.0.6.tgz#0618557722f450375d3796d701a8ac5407b3b94e" + integrity sha512-NyL6ZB6cVni7pl+/IT2W0ni5ME00xR0sN27AQZZrpKn1b+qRh+mLbBxIq9Cq1oGfmTc7BUq4HB77mxwCaxAYNg== + dependencies: + "@types/http-proxy" "^1.17.4" + http-proxy "^1.18.1" + is-glob "^4.0.1" + lodash "^4.17.20" + micromatch "^4.0.2" + +http-proxy@^1.17.0, http-proxy@^1.18.1: + version "1.18.1" + resolved "https://registry.yarnpkg.com/http-proxy/-/http-proxy-1.18.1.tgz#401541f0534884bbf95260334e72f88ee3976549" + integrity sha512-7mz/721AbnJwIVbnaSv1Cz3Am0ZLT/UBwkC92VlxhXv/k/BBQfM2fXElQNC27BVGr0uwUpplYPQM9LnaBMR5NQ== + dependencies: + eventemitter3 "^4.0.0" + follow-redirects "^1.0.0" + requires-port "^1.0.0" + +human-signals@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/human-signals/-/human-signals-2.1.0.tgz#dc91fcba42e4d06e4abaed33b3e7a3c02f514ea0" + integrity sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw== + +iconv-lite@0.4.24: + version "0.4.24" + resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.24.tgz#2022b4b25fbddc21d2f524974a474aafe733908b" + integrity sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA== + dependencies: + safer-buffer ">= 2.1.2 < 3" + +icss-replace-symbols@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/icss-replace-symbols/-/icss-replace-symbols-1.1.0.tgz#06ea6f83679a7749e386cfe1fe812ae5db223ded" + integrity sha1-Bupvg2ead0njhs/h/oEq5dsiPe0= + +icss-utils@^4.0.0, icss-utils@^4.1.1: + version "4.1.1" + resolved "https://registry.yarnpkg.com/icss-utils/-/icss-utils-4.1.1.tgz#21170b53789ee27447c2f47dd683081403f9a467" + integrity sha512-4aFq7wvWyMHKgxsH8QQtGpvbASCf+eM3wPRLI6R+MgAnTCZ6STYsRvttLvRWK0Nfif5piF394St3HeJDaljGPA== + dependencies: + postcss "^7.0.14" + +icss-utils@^5.0.0: + version "5.1.0" + resolved "https://registry.yarnpkg.com/icss-utils/-/icss-utils-5.1.0.tgz#c6be6858abd013d768e98366ae47e25d5887b1ae" + integrity sha512-soFhflCVWLfRNOPU3iv5Z9VUdT44xFRbzjLsEzSr5AQmgqPMTHdU3PMT1Cf1ssx8fLNJDA1juftYl+PUcv3MqA== + +ignore@^4.0.6: + version "4.0.6" + resolved "https://registry.yarnpkg.com/ignore/-/ignore-4.0.6.tgz#750e3db5862087b4737ebac8207ffd1ef27b25fc" + integrity sha512-cyFDKrqc/YdcWFniJhzI42+AzS+gNwmUzOSFcRCQYwySuBBBy/KjuxWLZ/FHEH6Moq1NizMOBWyTcv8O4OZIMg== + +ignore@^5.1.4, ignore@^5.1.8: + version "5.1.8" + resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.1.8.tgz#f150a8b50a34289b33e22f5889abd4d8016f0e57" + integrity sha512-BMpfD7PpiETpBl/A6S498BaIJ6Y/ABT93ETbby2fP00v4EbvPBXWEoaR1UBPKs3iR53pJY7EtZk5KACI57i1Uw== + +image-size@~0.5.0: + version "0.5.5" + resolved "https://registry.yarnpkg.com/image-size/-/image-size-0.5.5.tgz#09dfd4ab9d20e29eb1c3e80b8990378df9e3cb9c" + integrity sha1-Cd/Uq50g4p6xw+gLiZA3jfnjy5w= + +import-fresh@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/import-fresh/-/import-fresh-2.0.0.tgz#d81355c15612d386c61f9ddd3922d4304822a546" + integrity sha1-2BNVwVYS04bGH53dOSLUMEgipUY= + dependencies: + caller-path "^2.0.0" + resolve-from "^3.0.0" + +import-fresh@^3.0.0, import-fresh@^3.2.1: + version "3.3.0" + resolved "https://registry.yarnpkg.com/import-fresh/-/import-fresh-3.3.0.tgz#37162c25fcb9ebaa2e6e53d5b4d88ce17d9e0c2b" + integrity sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw== + dependencies: + parent-module "^1.0.0" + resolve-from "^4.0.0" + +import-lazy@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/import-lazy/-/import-lazy-4.0.0.tgz#e8eb627483a0a43da3c03f3e35548be5cb0cc153" + integrity sha512-rKtvo6a868b5Hu3heneU+L4yEQ4jYKLtjpnPeUdK7h0yzXGmyBTypknlkCvHFBqfX9YlorEiMM6Dnq/5atfHkw== + +import-local@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/import-local/-/import-local-2.0.0.tgz#55070be38a5993cf18ef6db7e961f5bee5c5a09d" + integrity sha512-b6s04m3O+s3CGSbqDIyP4R6aAwAeYlVq9+WUWep6iHa8ETRf9yei1U48C5MmfJmV9AiLYYBKPMq/W+/WRpQmCQ== + dependencies: + pkg-dir "^3.0.0" + resolve-cwd "^2.0.0" + +import-local@^3.0.2: + version "3.0.2" + resolved "https://registry.yarnpkg.com/import-local/-/import-local-3.0.2.tgz#a8cfd0431d1de4a2199703d003e3e62364fa6db6" + integrity sha512-vjL3+w0oulAVZ0hBHnxa/Nm5TAurf9YLQJDhqRZyqb+VKGOB6LU8t9H1Nr5CIo16vh9XfJTOoHwU0B71S557gA== + dependencies: + pkg-dir "^4.2.0" + resolve-cwd "^3.0.0" + +imurmurhash@^0.1.4: + version "0.1.4" + resolved "https://registry.yarnpkg.com/imurmurhash/-/imurmurhash-0.1.4.tgz#9218b9b2b928a238b13dc4fb6b6d576f231453ea" + integrity sha1-khi5srkoojixPcT7a21XbyMUU+o= + +indent-string@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/indent-string/-/indent-string-4.0.0.tgz#624f8f4497d619b2d9768531d58f4122854d7251" + integrity sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg== + +indexes-of@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/indexes-of/-/indexes-of-1.0.1.tgz#f30f716c8e2bd346c7b67d3df3915566a7c05607" + integrity sha1-8w9xbI4r00bHtn0985FVZqfAVgc= + +infer-owner@^1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/infer-owner/-/infer-owner-1.0.4.tgz#c4cefcaa8e51051c2a40ba2ce8a3d27295af9467" + integrity sha512-IClj+Xz94+d7irH5qRyfJonOdfTzuDaifE6ZPWfx0N0+/ATZCbuTPq2prFl526urkQd90WyUKIh1DfBQ2hMz9A== + +inflight@^1.0.4: + version "1.0.6" + resolved "https://registry.yarnpkg.com/inflight/-/inflight-1.0.6.tgz#49bd6331d7d02d0c09bc910a1075ba8165b56df9" + integrity sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk= + dependencies: + once "^1.3.0" + wrappy "1" + +inherits@2, inherits@2.0.4, inherits@^2.0.1, inherits@^2.0.3, inherits@^2.0.4, inherits@~2.0.3: + version "2.0.4" + resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c" + integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== + +inherits@2.0.3: + version "2.0.3" + resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.3.tgz#633c2c83e3da42a502f52466022480f4208261de" + integrity sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4= + +ini@^1.3.5: + version "1.3.8" + resolved "https://registry.yarnpkg.com/ini/-/ini-1.3.8.tgz#a29da425b48806f34767a4efce397269af28432c" + integrity sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew== + +insert-css@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/insert-css/-/insert-css-2.0.0.tgz#eb5d1097b7542f4c79ea3060d3aee07d053880f4" + integrity sha1-610Ql7dUL0x56jBg067gfQU4gPQ= + +internal-ip@^4.3.0: + version "4.3.0" + resolved "https://registry.yarnpkg.com/internal-ip/-/internal-ip-4.3.0.tgz#845452baad9d2ca3b69c635a137acb9a0dad0907" + integrity sha512-S1zBo1D6zcsyuC6PMmY5+55YMILQ9av8lotMx447Bq6SAgo/sDK6y6uUKmuYhW7eacnIhFfsPmCNYdDzsnnDCg== + dependencies: + default-gateway "^4.2.0" + ipaddr.js "^1.9.0" + +internal-slot@^1.0.2: + version "1.0.3" + resolved "https://registry.yarnpkg.com/internal-slot/-/internal-slot-1.0.3.tgz#7347e307deeea2faac2ac6205d4bc7d34967f59c" + integrity sha512-O0DB1JC/sPyZl7cIo78n5dR7eUSwwpYPiXRhTzNxZVAMUuB8vlnRFyLxdrVToks6XPLVnFfbzaVd5WLjhgg+vA== + dependencies: + get-intrinsic "^1.1.0" + has "^1.0.3" + side-channel "^1.0.4" + +internmap@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/internmap/-/internmap-1.0.0.tgz#3c6bf0944b0eae457698000412108752bbfddb56" + integrity sha512-SdoDWwNOTE2n4JWUsLn4KXZGuZPjPF9yyOGc8bnfWnBQh7BD/l80rzSznKc/r4Y0aQ7z3RTk9X+tV4tHBpu+dA== + +interpret@^2.2.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/interpret/-/interpret-2.2.0.tgz#1a78a0b5965c40a5416d007ad6f50ad27c417df9" + integrity sha512-Ju0Bz/cEia55xDwUWEa8+olFpCiQoypjnQySseKtmjNrnps3P+xfpUmGr90T7yjlVJmOtybRvPXhKMbHr+fWnw== + +ip-regex@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/ip-regex/-/ip-regex-2.1.0.tgz#fa78bf5d2e6913c911ce9f819ee5146bb6d844e9" + integrity sha1-+ni/XS5pE8kRzp+BnuUUa7bYROk= + +ip@^1.1.0, ip@^1.1.5: + version "1.1.5" + resolved "https://registry.yarnpkg.com/ip/-/ip-1.1.5.tgz#bdded70114290828c0a039e72ef25f5aaec4354a" + integrity sha1-vd7XARQpCCjAoDnnLvJfWq7ENUo= + +ipaddr.js@1.9.1, ipaddr.js@^1.9.0: + version "1.9.1" + resolved "https://registry.yarnpkg.com/ipaddr.js/-/ipaddr.js-1.9.1.tgz#bff38543eeb8984825079ff3a2a8e6cbd46781b3" + integrity sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g== + +is-absolute-url@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/is-absolute-url/-/is-absolute-url-2.1.0.tgz#50530dfb84fcc9aa7dbe7852e83a37b93b9f2aa6" + integrity sha1-UFMN+4T8yap9vnhS6Do3uTufKqY= + +is-absolute-url@^3.0.3: + version "3.0.3" + resolved "https://registry.yarnpkg.com/is-absolute-url/-/is-absolute-url-3.0.3.tgz#96c6a22b6a23929b11ea0afb1836c36ad4a5d698" + integrity sha512-opmNIX7uFnS96NtPmhWQgQx6/NYFgsUXYMllcfzwWKUMwfo8kku1TvE6hkNcH+Q1ts5cMVrsY7j0bxXQDciu9Q== + +is-absolute@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/is-absolute/-/is-absolute-1.0.0.tgz#395e1ae84b11f26ad1795e73c17378e48a301576" + integrity sha512-dOWoqflvcydARa360Gvv18DZ/gRuHKi2NU/wU5X1ZFzdYfH29nkiNZsF3mp4OJ3H4yo9Mx8A/uAGNzpzPN3yBA== + dependencies: + is-relative "^1.0.0" + is-windows "^1.0.1" + +is-accessor-descriptor@^0.1.6: + version "0.1.6" + resolved "https://registry.yarnpkg.com/is-accessor-descriptor/-/is-accessor-descriptor-0.1.6.tgz#a9e12cb3ae8d876727eeef3843f8a0897b5c98d6" + integrity sha1-qeEss66Nh2cn7u84Q/igiXtcmNY= + dependencies: + kind-of "^3.0.2" + +is-accessor-descriptor@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz#169c2f6d3df1f992618072365c9b0ea1f6878656" + integrity sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ== + dependencies: + kind-of "^6.0.0" + +is-alphabetical@^1.0.0: + version "1.0.4" + resolved "https://registry.yarnpkg.com/is-alphabetical/-/is-alphabetical-1.0.4.tgz#9e7d6b94916be22153745d184c298cbf986a686d" + integrity sha512-DwzsA04LQ10FHTZuL0/grVDk4rFoVH1pjAToYwBrHSxcrBIGQuXrQMtD5U1b0U2XVgKZCTLLP8u2Qxqhy3l2Vg== + +is-alphanumerical@^1.0.0: + version "1.0.4" + resolved "https://registry.yarnpkg.com/is-alphanumerical/-/is-alphanumerical-1.0.4.tgz#7eb9a2431f855f6b1ef1a78e326df515696c4dbf" + integrity sha512-UzoZUr+XfVz3t3v4KyGEniVL9BDRoQtY7tOyrRybkVNjDFWyo1yhXNGrrBTQxp3ib9BLAWs7k2YKBQsFRkZG9A== + dependencies: + is-alphabetical "^1.0.0" + is-decimal "^1.0.0" + +is-arguments@^1.0.4: + version "1.1.0" + resolved "https://registry.yarnpkg.com/is-arguments/-/is-arguments-1.1.0.tgz#62353031dfbee07ceb34656a6bde59efecae8dd9" + integrity sha512-1Ij4lOMPl/xB5kBDn7I+b2ttPMKa8szhEIrXDuXQD/oe3HJLTLhqhgGspwgyGd6MOywBUqVvYicF72lkgDnIHg== + dependencies: + call-bind "^1.0.0" + +is-arrayish@^0.2.1: + version "0.2.1" + resolved "https://registry.yarnpkg.com/is-arrayish/-/is-arrayish-0.2.1.tgz#77c99840527aa8ecb1a8ba697b80645a7a926a9d" + integrity sha1-d8mYQFJ6qOyxqLppe4BkWnqSap0= + +is-arrayish@^0.3.1: + version "0.3.2" + resolved "https://registry.yarnpkg.com/is-arrayish/-/is-arrayish-0.3.2.tgz#4574a2ae56f7ab206896fb431eaeed066fdf8f03" + integrity sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ== + +is-binary-path@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/is-binary-path/-/is-binary-path-1.0.1.tgz#75f16642b480f187a711c814161fd3a4a7655898" + integrity sha1-dfFmQrSA8YenEcgUFh/TpKdlWJg= + dependencies: + binary-extensions "^1.0.0" + +is-buffer@^1.1.5: + version "1.1.6" + resolved "https://registry.yarnpkg.com/is-buffer/-/is-buffer-1.1.6.tgz#efaa2ea9daa0d7ab2ea13a97b2b8ad51fefbe8be" + integrity sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w== + +is-buffer@^2.0.0: + version "2.0.5" + resolved "https://registry.yarnpkg.com/is-buffer/-/is-buffer-2.0.5.tgz#ebc252e400d22ff8d77fa09888821a24a658c191" + integrity sha512-i2R6zNFDwgEHJyQUtJEk0XFi1i0dPFn/oqjK3/vPCcDeJvW5NQ83V8QbicfF1SupOaB0h8ntgBC2YiE7dfyctQ== + +is-callable@^1.1.4, is-callable@^1.2.2: + version "1.2.2" + resolved "https://registry.yarnpkg.com/is-callable/-/is-callable-1.2.2.tgz#c7c6715cd22d4ddb48d3e19970223aceabb080d9" + integrity sha512-dnMqspv5nU3LoewK2N/y7KLtxtakvTuaCsU9FU50/QDmdbHNy/4/JuRtMHqRU22o3q+W89YQndQEeCVwK+3qrA== + +is-color-stop@^1.0.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/is-color-stop/-/is-color-stop-1.1.0.tgz#cfff471aee4dd5c9e158598fbe12967b5cdad345" + integrity sha1-z/9HGu5N1cnhWFmPvhKWe1za00U= + dependencies: + css-color-names "^0.0.4" + hex-color-regex "^1.1.0" + hsl-regex "^1.0.0" + hsla-regex "^1.0.0" + rgb-regex "^1.0.1" + rgba-regex "^1.0.0" + +is-core-module@^2.1.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/is-core-module/-/is-core-module-2.2.0.tgz#97037ef3d52224d85163f5597b2b63d9afed981a" + integrity sha512-XRAfAdyyY5F5cOXn7hYQDqh2Xmii+DEfIcQGxK/uNwMHhIkPWO0g8msXcbzLe+MpGoR951MlqM/2iIlU4vKDdQ== + dependencies: + has "^1.0.3" + +is-data-descriptor@^0.1.4: + version "0.1.4" + resolved "https://registry.yarnpkg.com/is-data-descriptor/-/is-data-descriptor-0.1.4.tgz#0b5ee648388e2c860282e793f1856fec3f301b56" + integrity sha1-C17mSDiOLIYCgueT8YVv7D8wG1Y= + dependencies: + kind-of "^3.0.2" + +is-data-descriptor@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz#d84876321d0e7add03990406abbbbd36ba9268c7" + integrity sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ== + dependencies: + kind-of "^6.0.0" + +is-date-object@^1.0.1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/is-date-object/-/is-date-object-1.0.2.tgz#bda736f2cd8fd06d32844e7743bfa7494c3bfd7e" + integrity sha512-USlDT524woQ08aoZFzh3/Z6ch9Y/EWXEHQ/AaRN0SkKq4t2Jw2R2339tSXmwuVoY7LLlBCbOIlx2myP/L5zk0g== + +is-decimal@^1.0.0: + version "1.0.4" + resolved "https://registry.yarnpkg.com/is-decimal/-/is-decimal-1.0.4.tgz#65a3a5958a1c5b63a706e1b333d7cd9f630d3fa5" + integrity sha512-RGdriMmQQvZ2aqaQq3awNA6dCGtKpiDFcOzrTWrDAT2MiWrKQVPmxLGHl7Y2nNu6led0kEyoX0enY0qXYsv9zw== + +is-descriptor@^0.1.0: + version "0.1.6" + resolved "https://registry.yarnpkg.com/is-descriptor/-/is-descriptor-0.1.6.tgz#366d8240dde487ca51823b1ab9f07a10a78251ca" + integrity sha512-avDYr0SB3DwO9zsMov0gKCESFYqCnE4hq/4z3TdUlukEy5t9C0YRq7HLrsN52NAcqXKaepeCD0n+B0arnVG3Hg== + dependencies: + is-accessor-descriptor "^0.1.6" + is-data-descriptor "^0.1.4" + kind-of "^5.0.0" + +is-descriptor@^1.0.0, is-descriptor@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/is-descriptor/-/is-descriptor-1.0.2.tgz#3b159746a66604b04f8c81524ba365c5f14d86ec" + integrity sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg== + dependencies: + is-accessor-descriptor "^1.0.0" + is-data-descriptor "^1.0.0" + kind-of "^6.0.2" + +is-directory@^0.3.1: + version "0.3.1" + resolved "https://registry.yarnpkg.com/is-directory/-/is-directory-0.3.1.tgz#61339b6f2475fc772fd9c9d83f5c8575dc154ae1" + integrity sha1-YTObbyR1/Hcv2cnYP1yFddwVSuE= + +is-extendable@^0.1.0, is-extendable@^0.1.1: + version "0.1.1" + resolved "https://registry.yarnpkg.com/is-extendable/-/is-extendable-0.1.1.tgz#62b110e289a471418e3ec36a617d472e301dfc89" + integrity sha1-YrEQ4omkcUGOPsNqYX1HLjAd/Ik= + +is-extendable@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/is-extendable/-/is-extendable-1.0.1.tgz#a7470f9e426733d81bd81e1155264e3a3507cab4" + integrity sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA== + dependencies: + is-plain-object "^2.0.4" + +is-extglob@^2.1.0, is-extglob@^2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/is-extglob/-/is-extglob-2.1.1.tgz#a88c02535791f02ed37c76a1b9ea9773c833f8c2" + integrity sha1-qIwCU1eR8C7TfHahueqXc8gz+MI= + +is-fullwidth-code-point@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz#a3b30a5c4f199183167aaab93beefae3ddfb654f" + integrity sha1-o7MKXE8ZkYMWeqq5O+764937ZU8= + +is-fullwidth-code-point@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz#f116f8064fe90b3f7844a38997c0b75051269f1d" + integrity sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg== + +is-glob@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-3.1.0.tgz#7ba5ae24217804ac70707b96922567486cc3e84a" + integrity sha1-e6WuJCF4BKxwcHuWkiVnSGzD6Eo= + dependencies: + is-extglob "^2.1.0" + +is-glob@^4.0.0, is-glob@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-4.0.1.tgz#7567dbe9f2f5e2467bc77ab83c4a29482407a5dc" + integrity sha512-5G0tKtBTFImOqDnLB2hG6Bp2qcKEFduo4tZu9MT/H6NQv/ghhy30o55ufafxJ/LdH79LLs2Kfrn85TLKyA7BUg== + dependencies: + is-extglob "^2.1.1" + +is-hexadecimal@^1.0.0: + version "1.0.4" + resolved "https://registry.yarnpkg.com/is-hexadecimal/-/is-hexadecimal-1.0.4.tgz#cc35c97588da4bd49a8eedd6bc4082d44dcb23a7" + integrity sha512-gyPJuv83bHMpocVYoqof5VDiZveEoGoFL8m3BXNb2VW8Xs+rz9kqO8LOQ5DH6EsuvilT1ApazU0pyl+ytbPtlw== + +is-negated-glob@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/is-negated-glob/-/is-negated-glob-1.0.0.tgz#6910bca5da8c95e784b5751b976cf5a10fee36d2" + integrity sha1-aRC8pdqMleeEtXUbl2z1oQ/uNtI= + +is-negative-zero@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/is-negative-zero/-/is-negative-zero-2.0.1.tgz#3de746c18dda2319241a53675908d8f766f11c24" + integrity sha512-2z6JzQvZRa9A2Y7xC6dQQm4FSTSTNWjKIYYTt4246eMTJmIo0Q+ZyOsU66X8lxK1AbB92dFeglPLrhwpeRKO6w== + +is-number@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/is-number/-/is-number-3.0.0.tgz#24fd6201a4782cf50561c810276afc7d12d71195" + integrity sha1-JP1iAaR4LPUFYcgQJ2r8fRLXEZU= + dependencies: + kind-of "^3.0.2" + +is-number@^7.0.0: + version "7.0.0" + resolved "https://registry.yarnpkg.com/is-number/-/is-number-7.0.0.tgz#7535345b896734d5f80c4d06c50955527a14f12b" + integrity sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng== + +is-obj@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/is-obj/-/is-obj-2.0.0.tgz#473fb05d973705e3fd9620545018ca8e22ef4982" + integrity sha512-drqDG3cbczxxEJRoOXcOjtdp1J/lyp1mNn0xaznRs8+muBhgQcrnbspox5X5fOw0HnMnbfDzvnEMEtqDEJEo8w== + +is-path-cwd@^2.0.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/is-path-cwd/-/is-path-cwd-2.2.0.tgz#67d43b82664a7b5191fd9119127eb300048a9fdb" + integrity sha512-w942bTcih8fdJPJmQHFzkS76NEP8Kzzvmw92cXsazb8intwLqPibPPdXf4ANdKV3rYMuuQYGIWtvz9JilB3NFQ== + +is-path-in-cwd@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/is-path-in-cwd/-/is-path-in-cwd-2.1.0.tgz#bfe2dca26c69f397265a4009963602935a053acb" + integrity sha512-rNocXHgipO+rvnP6dk3zI20RpOtrAM/kzbB258Uw5BWr3TpXi861yzjo16Dn4hUox07iw5AyeMLHWsujkjzvRQ== + dependencies: + is-path-inside "^2.1.0" + +is-path-inside@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/is-path-inside/-/is-path-inside-2.1.0.tgz#7c9810587d659a40d27bcdb4d5616eab059494b2" + integrity sha512-wiyhTzfDWsvwAW53OBWF5zuvaOGlZ6PwYxAbPVDhpm+gM09xKQGjBq/8uYN12aDvMxnAnq3dxTyoSoRNmg5YFg== + dependencies: + path-is-inside "^1.0.2" + +is-plain-obj@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/is-plain-obj/-/is-plain-obj-1.1.0.tgz#71a50c8429dfca773c92a390a4a03b39fcd51d3e" + integrity sha1-caUMhCnfync8kqOQpKA7OfzVHT4= + +is-plain-obj@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/is-plain-obj/-/is-plain-obj-2.1.0.tgz#45e42e37fccf1f40da8e5f76ee21515840c09287" + integrity sha512-YWnfyRwxL/+SsrWYfOpUtz5b3YD+nyfkHvjbcanzk8zgyO4ASD67uVMRt8k5bM4lLMDnXfriRhOpemw+NfT1eA== + +is-plain-object@^2.0.3, is-plain-object@^2.0.4: + version "2.0.4" + resolved "https://registry.yarnpkg.com/is-plain-object/-/is-plain-object-2.0.4.tgz#2c163b3fafb1b606d9d17928f05c2a1c38e07677" + integrity sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og== + dependencies: + isobject "^3.0.1" + +is-regex@^1.0.4, is-regex@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/is-regex/-/is-regex-1.1.1.tgz#c6f98aacc546f6cec5468a07b7b153ab564a57b9" + integrity sha512-1+QkEcxiLlB7VEyFtyBg94e08OAsvq7FUBgApTq/w2ymCLyKJgDPsybBENVtA7XCQEgEXxKPonG+mvYRxh/LIg== + dependencies: + has-symbols "^1.0.1" + +is-regexp@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/is-regexp/-/is-regexp-2.1.0.tgz#cd734a56864e23b956bf4e7c66c396a4c0b22c2d" + integrity sha512-OZ4IlER3zmRIoB9AqNhEggVxqIH4ofDns5nRrPS6yQxXE1TPCUpFznBfRQmQa8uC+pXqjMnukiJBxCisIxiLGA== + +is-relative@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/is-relative/-/is-relative-1.0.0.tgz#a1bb6935ce8c5dba1e8b9754b9b2dcc020e2260d" + integrity sha512-Kw/ReK0iqwKeu0MITLFuj0jbPAmEiOsIwyIXvvbfa6QfmN9pkD1M+8pdk7Rl/dTKbH34/XBFMbgD4iMJhLQbGA== + dependencies: + is-unc-path "^1.0.0" + +is-resolvable@^1.0.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/is-resolvable/-/is-resolvable-1.1.0.tgz#fb18f87ce1feb925169c9a407c19318a3206ed88" + integrity sha512-qgDYXFSR5WvEfuS5dMj6oTMEbrrSaM0CrFk2Yiq/gXnBvD9pMa2jGXxyhGLfvhZpuMZe18CJpFxAt3CRs42NMg== + +is-stream@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/is-stream/-/is-stream-1.1.0.tgz#12d4a3dd4e68e0b79ceb8dbc84173ae80d91ca44" + integrity sha1-EtSj3U5o4Lec6428hBc66A2RykQ= + +is-stream@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/is-stream/-/is-stream-2.0.0.tgz#bde9c32680d6fae04129d6ac9d921ce7815f78e3" + integrity sha512-XCoy+WlUr7d1+Z8GgSuXmpuUFC9fOhRXglJMx+dwLKTkL44Cjd4W1Z5P+BQZpr+cR93aGP4S/s7Ftw6Nd/kiEw== + +is-string@^1.0.5: + version "1.0.5" + resolved "https://registry.yarnpkg.com/is-string/-/is-string-1.0.5.tgz#40493ed198ef3ff477b8c7f92f644ec82a5cd3a6" + integrity sha512-buY6VNRjhQMiF1qWDouloZlQbRhDPCebwxSjxMjxgemYT46YMd2NR0/H+fBhEfWX4A/w9TBJ+ol+okqJKFE6vQ== + +is-svg@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/is-svg/-/is-svg-3.0.0.tgz#9321dbd29c212e5ca99c4fa9794c714bcafa2f75" + integrity sha512-gi4iHK53LR2ujhLVVj+37Ykh9GLqYHX6JOVXbLAucaG/Cqw9xwdFOjDM2qeifLs1sF1npXXFvDu0r5HNgCMrzQ== + dependencies: + html-comment-regex "^1.1.0" + +is-symbol@^1.0.2: + version "1.0.3" + resolved "https://registry.yarnpkg.com/is-symbol/-/is-symbol-1.0.3.tgz#38e1014b9e6329be0de9d24a414fd7441ec61937" + integrity sha512-OwijhaRSgqvhm/0ZdAcXNZt9lYdKFpcRDT5ULUuYXPoT794UNOdU+gpT6Rzo7b4V2HUl/op6GqY894AZwv9faQ== + dependencies: + has-symbols "^1.0.1" + +is-typedarray@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/is-typedarray/-/is-typedarray-1.0.0.tgz#e479c80858df0c1b11ddda6940f96011fcda4a9a" + integrity sha1-5HnICFjfDBsR3dppQPlgEfzaSpo= + +is-unc-path@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/is-unc-path/-/is-unc-path-1.0.0.tgz#d731e8898ed090a12c352ad2eaed5095ad322c9d" + integrity sha512-mrGpVd0fs7WWLfVsStvgF6iEJnbjDFZh9/emhRDcGWTduTfNHd9CHeUwH3gYIjdbwo4On6hunkztwOaAw0yllQ== + dependencies: + unc-path-regex "^0.1.2" + +is-what@^3.7.1: + version "3.12.0" + resolved "https://registry.yarnpkg.com/is-what/-/is-what-3.12.0.tgz#f4405ce4bd6dd420d3ced51a026fb90e03705e55" + integrity sha512-2ilQz5/f/o9V7WRWJQmpFYNmQFZ9iM+OXRonZKcYgTkCzjb949Vi4h282PD1UfmgHk666rcWonbRJ++KI41VGw== + +is-windows@^1.0.1, is-windows@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/is-windows/-/is-windows-1.0.2.tgz#d1850eb9791ecd18e6182ce12a30f396634bb19d" + integrity sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA== + +is-wsl@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/is-wsl/-/is-wsl-1.1.0.tgz#1f16e4aa22b04d1336b66188a66af3c600c3a66d" + integrity sha1-HxbkqiKwTRM2tmGIpmrzxgDDpm0= + +isarray@0.0.1: + version "0.0.1" + resolved "https://registry.yarnpkg.com/isarray/-/isarray-0.0.1.tgz#8a18acfca9a8f4177e09abfc6038939b05d1eedf" + integrity sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8= + +isarray@1.0.0, isarray@^1.0.0, isarray@~1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/isarray/-/isarray-1.0.0.tgz#bb935d48582cba168c06834957a54a3e07124f11" + integrity sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE= + +isexe@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10" + integrity sha1-6PvzdNxVb/iUehDcsFctYz8s+hA= + +isobject@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/isobject/-/isobject-2.1.0.tgz#f065561096a3f1da2ef46272f815c840d87e0c89" + integrity sha1-8GVWEJaj8dou9GJy+BXIQNh+DIk= + dependencies: + isarray "1.0.0" + +isobject@^3.0.0, isobject@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/isobject/-/isobject-3.0.1.tgz#4e431e92b11a9731636aa1f9c8d1ccbcfdab78df" + integrity sha1-TkMekrEalzFjaqH5yNHMvP2reN8= + +jest-worker@^26.6.2: + version "26.6.2" + resolved "https://registry.yarnpkg.com/jest-worker/-/jest-worker-26.6.2.tgz#7f72cbc4d643c365e27b9fd775f9d0eaa9c7a8ed" + integrity sha512-KWYVV1c4i+jbMpaBC+U++4Va0cp8OisU185o73T1vo99hqi7w8tSJfUXYswwqqrjzwxa6KpRK54WhPvwf5w6PQ== + dependencies: + "@types/node" "*" + merge-stream "^2.0.0" + supports-color "^7.0.0" + +"js-tokens@^3.0.0 || ^4.0.0", js-tokens@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499" + integrity sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ== + +js-yaml@^3.13.1: + version "3.14.1" + resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-3.14.1.tgz#dae812fdb3825fa306609a8717383c50c36a0537" + integrity sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g== + dependencies: + argparse "^1.0.7" + esprima "^4.0.0" + +jsesc@^2.5.1: + version "2.5.2" + resolved "https://registry.yarnpkg.com/jsesc/-/jsesc-2.5.2.tgz#80564d2e483dacf6e8ef209650a67df3f0c283a4" + integrity sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA== + +json-parse-better-errors@^1.0.1, json-parse-better-errors@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/json-parse-better-errors/-/json-parse-better-errors-1.0.2.tgz#bb867cfb3450e69107c131d1c514bab3dc8bcaa9" + integrity sha512-mrqyZKfX5EhL7hvqcV6WG1yYjnjeuYDzDhhcAAUrq8Po85NBQBJP+ZDUT75qZQ98IkUoBqdkExkukOU7Ts2wrw== + +json-parse-even-better-errors@^2.3.0: + version "2.3.1" + resolved "https://registry.yarnpkg.com/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz#7c47805a94319928e05777405dc12e1f7a4ee02d" + integrity sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w== + +json-schema-traverse@^0.4.1: + version "0.4.1" + resolved "https://registry.yarnpkg.com/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz#69f6a87d9513ab8bb8fe63bdb0979c448e684660" + integrity sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg== + +json-schema-traverse@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz#ae7bcb3656ab77a73ba5c49bf654f38e6b6860e2" + integrity sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug== + +json-stable-stringify-without-jsonify@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz#9db7b59496ad3f3cfef30a75142d2d930ad72651" + integrity sha1-nbe1lJatPzz+8wp1FC0tkwrXJlE= + +json2mq@^0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/json2mq/-/json2mq-0.2.0.tgz#b637bd3ba9eabe122c83e9720483aeb10d2c904a" + integrity sha1-tje9O6nqvhIsg+lyBIOusQ0skEo= + dependencies: + string-convert "^0.2.0" + +json3@^3.3.3: + version "3.3.3" + resolved "https://registry.yarnpkg.com/json3/-/json3-3.3.3.tgz#7fc10e375fc5ae42c4705a5cc0aa6f62be305b81" + integrity sha512-c7/8mbUsKigAbLkD5B010BK4D9LZm7A1pNItkEwiUZRpIN66exu/e7YQWysGun+TRKaJp8MhemM+VkfWv42aCA== + +json5@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/json5/-/json5-1.0.1.tgz#779fb0018604fa854eacbf6252180d83543e3dbe" + integrity sha512-aKS4WQjPenRxiQsC93MNfjx+nbF4PAdYzmd/1JIj8HYzqfbu86beTuNgXDzPknWk0n0uARlyewZo4s++ES36Ow== + dependencies: + minimist "^1.2.0" + +json5@^2.1.2: + version "2.1.3" + resolved "https://registry.yarnpkg.com/json5/-/json5-2.1.3.tgz#c9b0f7fa9233bfe5807fe66fcf3a5617ed597d43" + integrity sha512-KXPvOm8K9IJKFM0bmdn8QXh7udDh1g/giieX0NLCaMnb4hEiVFqnop2ImTXCc5e0/oHz3LTqmHGtExn5hfMkOA== + dependencies: + minimist "^1.2.5" + +jsonfile@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/jsonfile/-/jsonfile-4.0.0.tgz#8771aae0799b64076b76640fca058f9c10e33ecb" + integrity sha1-h3Gq4HmbZAdrdmQPygWPnBDjPss= + optionalDependencies: + graceful-fs "^4.1.6" + +jsonfile@^6.0.1: + version "6.1.0" + resolved "https://registry.yarnpkg.com/jsonfile/-/jsonfile-6.1.0.tgz#bc55b2634793c679ec6403094eb13698a6ec0aae" + integrity sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ== + dependencies: + universalify "^2.0.0" + optionalDependencies: + graceful-fs "^4.1.6" + +"jsx-ast-utils@^2.4.1 || ^3.0.0": + version "3.2.0" + resolved "https://registry.yarnpkg.com/jsx-ast-utils/-/jsx-ast-utils-3.2.0.tgz#41108d2cec408c3453c1bbe8a4aae9e1e2bd8f82" + integrity sha512-EIsmt3O3ljsU6sot/J4E1zDRxfBNrhjyf/OKjlydwgEimQuznlM4Wv7U+ueONJMyEn1WRE0K8dhi3dVAXYT24Q== + dependencies: + array-includes "^3.1.2" + object.assign "^4.1.2" + +killable@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/killable/-/killable-1.0.1.tgz#4c8ce441187a061c7474fb87ca08e2a638194892" + integrity sha512-LzqtLKlUwirEUyl/nicirVmNiPvYs7l5n8wOPP7fyJVpUPkvCnW/vuiXGpylGUlnPDnB7311rARzAt3Mhswpjg== + +kind-of@^3.0.2, kind-of@^3.0.3, kind-of@^3.2.0: + version "3.2.2" + resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-3.2.2.tgz#31ea21a734bab9bbb0f32466d893aea51e4a3c64" + integrity sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ= + dependencies: + is-buffer "^1.1.5" + +kind-of@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-4.0.0.tgz#20813df3d712928b207378691a45066fae72dd57" + integrity sha1-IIE989cSkosgc3hpGkUGb65y3Vc= + dependencies: + is-buffer "^1.1.5" + +kind-of@^5.0.0: + version "5.1.0" + resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-5.1.0.tgz#729c91e2d857b7a419a1f9aa65685c4c33f5845d" + integrity sha512-NGEErnH6F2vUuXDh+OlbcKW7/wOcfdRHaZ7VWtqCztfHri/++YKmP51OdWeGPuqCOba6kk2OTe5d02VmTB80Pw== + +kind-of@^6.0.0, kind-of@^6.0.2, kind-of@^6.0.3: + version "6.0.3" + resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-6.0.3.tgz#07c05034a6c349fa06e24fa35aa76db4580ce4dd" + integrity sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw== + +klona@^2.0.4: + version "2.0.4" + resolved "https://registry.yarnpkg.com/klona/-/klona-2.0.4.tgz#7bb1e3affb0cb8624547ef7e8f6708ea2e39dfc0" + integrity sha512-ZRbnvdg/NxqzC7L9Uyqzf4psi1OM4Cuc+sJAkQPjO6XkQIJTNbfK2Rsmbw8fx1p2mkZdp2FZYo2+LwXYY/uwIA== + +known-css-properties@^0.20.0: + version "0.20.0" + resolved "https://registry.yarnpkg.com/known-css-properties/-/known-css-properties-0.20.0.tgz#0570831661b47dd835293218381166090ff60e96" + integrity sha512-URvsjaA9ypfreqJ2/ylDr5MUERhJZ+DhguoWRr2xgS5C7aGCalXo+ewL+GixgKBfhT2vuL02nbIgNGqVWgTOYw== + +last-call-webpack-plugin@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/last-call-webpack-plugin/-/last-call-webpack-plugin-3.0.0.tgz#9742df0e10e3cf46e5c0381c2de90d3a7a2d7555" + integrity sha512-7KI2l2GIZa9p2spzPIVZBYyNKkN+e/SQPpnjlTiPhdbDW3F86tdKKELxKpzJ5sgU19wQWsACULZmpTPYHeWO5w== + dependencies: + lodash "^4.17.5" + webpack-sources "^1.1.0" + +less-loader@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/less-loader/-/less-loader-5.0.0.tgz#498dde3a6c6c4f887458ee9ed3f086a12ad1b466" + integrity sha512-bquCU89mO/yWLaUq0Clk7qCsKhsF/TZpJUzETRvJa9KSVEL9SO3ovCvdEHISBhrC81OwC8QSVX7E0bzElZj9cg== + dependencies: + clone "^2.1.1" + loader-utils "^1.1.0" + pify "^4.0.1" + +less@^3.12.2: + version "3.13.1" + resolved "https://registry.yarnpkg.com/less/-/less-3.13.1.tgz#0ebc91d2a0e9c0c6735b83d496b0ab0583077909" + integrity sha512-SwA1aQXGUvp+P5XdZslUOhhLnClSLIjWvJhmd+Vgib5BFIr9lMNlQwmwUNOjXThF/A0x+MCYYPeWEfeWiLRnTw== + dependencies: + copy-anything "^2.0.1" + tslib "^1.10.0" + optionalDependencies: + errno "^0.1.1" + graceful-fs "^4.1.2" + image-size "~0.5.0" + make-dir "^2.1.0" + mime "^1.4.1" + native-request "^1.0.5" + source-map "~0.6.0" + +levn@^0.4.1: + version "0.4.1" + resolved "https://registry.yarnpkg.com/levn/-/levn-0.4.1.tgz#ae4562c007473b932a6200d403268dd2fffc6ade" + integrity sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ== + dependencies: + prelude-ls "^1.2.1" + type-check "~0.4.0" + +lines-and-columns@^1.1.6: + version "1.1.6" + resolved "https://registry.yarnpkg.com/lines-and-columns/-/lines-and-columns-1.1.6.tgz#1c00c743b433cd0a4e80758f7b64a57440d9ff00" + integrity sha1-HADHQ7QzzQpOgHWPe2SldEDZ/wA= + +load-json-file@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/load-json-file/-/load-json-file-2.0.0.tgz#7947e42149af80d696cbf797bcaabcfe1fe29ca8" + integrity sha1-eUfkIUmvgNaWy/eXvKq8/h/inKg= + dependencies: + graceful-fs "^4.1.2" + parse-json "^2.2.0" + pify "^2.0.0" + strip-bom "^3.0.0" + +loader-runner@^4.2.0: + version "4.2.0" + resolved "https://registry.yarnpkg.com/loader-runner/-/loader-runner-4.2.0.tgz#d7022380d66d14c5fb1d496b89864ebcfd478384" + integrity sha512-92+huvxMvYlMzMt0iIOukcwYBFpkYJdpl2xsZ7LrlayO7E8SOv+JJUEK17B/dJIHAOLMfh2dZZ/Y18WgmGtYNw== + +loader-utils@^1.1.0, loader-utils@^1.2.3: + version "1.4.0" + resolved "https://registry.yarnpkg.com/loader-utils/-/loader-utils-1.4.0.tgz#c579b5e34cb34b1a74edc6c1fb36bfa371d5a613" + integrity sha512-qH0WSMBtn/oHuwjy/NucEgbx5dbxxnxup9s4PVXJUDHZBQY+s0NWA9rJf53RBnQZxfch7euUui7hpoAPvALZdA== + dependencies: + big.js "^5.2.2" + emojis-list "^3.0.0" + json5 "^1.0.1" + +loader-utils@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/loader-utils/-/loader-utils-2.0.0.tgz#e4cace5b816d425a166b5f097e10cd12b36064b0" + integrity sha512-rP4F0h2RaWSvPEkD7BLDFQnvSf+nK+wr3ESUjNTyAGobqrijmW92zc+SO6d4p4B1wh7+B/Jg1mkQe5NYUEHtHQ== + dependencies: + big.js "^5.2.2" + emojis-list "^3.0.0" + json5 "^2.1.2" + +locate-path@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-2.0.0.tgz#2b568b265eec944c6d9c0de9c3dbbbca0354cd8e" + integrity sha1-K1aLJl7slExtnA3pw9u7ygNUzY4= + dependencies: + p-locate "^2.0.0" + path-exists "^3.0.0" + +locate-path@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-3.0.0.tgz#dbec3b3ab759758071b58fe59fc41871af21400e" + integrity sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A== + dependencies: + p-locate "^3.0.0" + path-exists "^3.0.0" + +locate-path@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-5.0.0.tgz#1afba396afd676a6d42504d0a67a3a7eb9f62aa0" + integrity sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g== + dependencies: + p-locate "^4.1.0" + +locate-path@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-6.0.0.tgz#55321eb309febbc59c4801d931a72452a681d286" + integrity sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw== + dependencies: + p-locate "^5.0.0" + +lodash-es@^4.17.14: + version "4.17.20" + resolved "https://registry.yarnpkg.com/lodash-es/-/lodash-es-4.17.20.tgz#29f6332eefc60e849f869c264bc71126ad61e8f7" + integrity sha512-JD1COMZsq8maT6mnuz1UMV0jvYD0E0aUsSOdrr1/nAG3dhqQXwRRgeW0cSqH1U43INKcqxaiVIQNOUDld7gRDA== + +lodash._reinterpolate@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/lodash._reinterpolate/-/lodash._reinterpolate-3.0.0.tgz#0ccf2d89166af03b3663c796538b75ac6e114d9d" + integrity sha1-DM8tiRZq8Ds2Y8eWU4t1rG4RTZ0= + +lodash.camelcase@^4.3.0: + version "4.3.0" + resolved "https://registry.yarnpkg.com/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz#b28aa6288a2b9fc651035c7711f65ab6190331a6" + integrity sha1-soqmKIorn8ZRA1x3EfZathkDMaY= + +lodash.difference@^4.5.0: + version "4.5.0" + resolved "https://registry.yarnpkg.com/lodash.difference/-/lodash.difference-4.5.0.tgz#9ccb4e505d486b91651345772885a2df27fd017c" + integrity sha1-nMtOUF1Ia5FlE0V3KIWi3yf9AXw= + +lodash.forown@^4.4.0: + version "4.4.0" + resolved "https://registry.yarnpkg.com/lodash.forown/-/lodash.forown-4.4.0.tgz#85115cf04f73ef966eced52511d3893cc46683af" + integrity sha1-hRFc8E9z75ZuztUlEdOJPMRmg68= + +lodash.get@^4.4.2: + version "4.4.2" + resolved "https://registry.yarnpkg.com/lodash.get/-/lodash.get-4.4.2.tgz#2d177f652fa31e939b4438d5341499dfa3825e99" + integrity sha1-LRd/ZS+jHpObRDjVNBSZ36OCXpk= + +lodash.groupby@^4.6.0: + version "4.6.0" + resolved "https://registry.yarnpkg.com/lodash.groupby/-/lodash.groupby-4.6.0.tgz#0b08a1dcf68397c397855c3239783832df7403d1" + integrity sha1-Cwih3PaDl8OXhVwyOXg4Mt90A9E= + +lodash.memoize@^4.1.2: + version "4.1.2" + resolved "https://registry.yarnpkg.com/lodash.memoize/-/lodash.memoize-4.1.2.tgz#bcc6c49a42a2840ed997f323eada5ecd182e0bfe" + integrity sha1-vMbEmkKihA7Zl/Mj6tpezRguC/4= + +lodash.sortby@^4.7.0: + version "4.7.0" + resolved "https://registry.yarnpkg.com/lodash.sortby/-/lodash.sortby-4.7.0.tgz#edd14c824e2cc9c1e0b0a1b42bb5210516a42438" + integrity sha1-7dFMgk4sycHgsKG0K7UhBRakJDg= + +lodash.template@^4.5.0: + version "4.5.0" + resolved "https://registry.yarnpkg.com/lodash.template/-/lodash.template-4.5.0.tgz#f976195cf3f347d0d5f52483569fe8031ccce8ab" + integrity sha512-84vYFxIkmidUiFxidA/KjjH9pAycqW+h980j7Fuz5qxRtO9pgB7MDFTdys1N7A5mcucRiDyEq4fusljItR1T/A== + dependencies: + lodash._reinterpolate "^3.0.0" + lodash.templatesettings "^4.0.0" + +lodash.templatesettings@^4.0.0: + version "4.2.0" + resolved "https://registry.yarnpkg.com/lodash.templatesettings/-/lodash.templatesettings-4.2.0.tgz#e481310f049d3cf6d47e912ad09313b154f0fb33" + integrity sha512-stgLz+i3Aa9mZgnjr/O+v9ruKZsPsndy7qPZOchbqk2cnTU1ZaldKK+v7m54WoKIyxiuMZTKT2H81F8BeAc3ZQ== + dependencies: + lodash._reinterpolate "^3.0.0" + +lodash.uniq@^4.5.0: + version "4.5.0" + resolved "https://registry.yarnpkg.com/lodash.uniq/-/lodash.uniq-4.5.0.tgz#d0225373aeb652adc1bc82e4945339a842754773" + integrity sha1-0CJTc662Uq3BvILklFM5qEJ1R3M= + +lodash@^4.17.11, lodash@^4.17.14, lodash@^4.17.15, lodash@^4.17.19, lodash@^4.17.20, lodash@^4.17.5, lodash@~4.17.4: + version "4.17.20" + resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.20.tgz#b44a9b6297bcb698f1c51a3545a2b3b368d59c52" + integrity sha512-PlhdFcillOINfeV7Ni6oF1TAEayyZBoZ8bcshTHqOYJYlrqzRK5hagpagky5o4HfCzzd1TRkXPMFq6cKk9rGmA== + +log-symbols@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/log-symbols/-/log-symbols-4.0.0.tgz#69b3cc46d20f448eccdb75ea1fa733d9e821c920" + integrity sha512-FN8JBzLx6CzeMrB0tg6pqlGU1wCrXW+ZXGH481kfsBqer0hToTIiHdjH4Mq8xJUbvATujKCvaREGWpGUionraA== + dependencies: + chalk "^4.0.0" + +loglevel@^1.6.8: + version "1.7.1" + resolved "https://registry.yarnpkg.com/loglevel/-/loglevel-1.7.1.tgz#005fde2f5e6e47068f935ff28573e125ef72f197" + integrity sha512-Hesni4s5UkWkwCGJMQGAh71PaLUmKFM60dHvq0zi/vDhhrzuk+4GgNbTXJ12YYQJn6ZKBDNIjYcuQGKudvqrIw== + +longest-streak@^2.0.0: + version "2.0.4" + resolved "https://registry.yarnpkg.com/longest-streak/-/longest-streak-2.0.4.tgz#b8599957da5b5dab64dee3fe316fa774597d90e4" + integrity sha512-vM6rUVCVUJJt33bnmHiZEvr7wPT78ztX7rojL+LW51bHtLh6HTjx84LA5W4+oa6aKEJA7jJu5LR6vQRBpA5DVg== + +loose-envify@^1.0.0, loose-envify@^1.1.0, loose-envify@^1.2.0, loose-envify@^1.3.1, loose-envify@^1.4.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/loose-envify/-/loose-envify-1.4.0.tgz#71ee51fa7be4caec1a63839f7e682d8132d30caf" + integrity sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q== + dependencies: + js-tokens "^3.0.0 || ^4.0.0" + +lower-case@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/lower-case/-/lower-case-2.0.2.tgz#6fa237c63dbdc4a82ca0fd882e4722dc5e634e28" + integrity sha512-7fm3l3NAF9WfN6W3JOmf5drwpVqX78JtoGJ3A6W0a6ZnldM41w2fV5D490psKFTpMds8TJse/eHLFFsNHHjHgg== + dependencies: + tslib "^2.0.3" + +lru-cache@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-6.0.0.tgz#6d6fe6570ebd96aaf90fcad1dafa3b2566db3a94" + integrity sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA== + dependencies: + yallist "^4.0.0" + +make-dir@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/make-dir/-/make-dir-2.1.0.tgz#5f0310e18b8be898cc07009295a30ae41e91e6f5" + integrity sha512-LS9X+dc8KLxXCb8dni79fLIIUA5VyZoyjSMCwTluaXA0o27cCK0bhXkpgw+sTXVpPy/lSO57ilRixqk0vDmtRA== + dependencies: + pify "^4.0.1" + semver "^5.6.0" + +make-dir@^3.0.2: + version "3.1.0" + resolved "https://registry.yarnpkg.com/make-dir/-/make-dir-3.1.0.tgz#415e967046b3a7f1d185277d84aa58203726a13f" + integrity sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw== + dependencies: + semver "^6.0.0" + +make-error@^1.1.1: + version "1.3.6" + resolved "https://registry.yarnpkg.com/make-error/-/make-error-1.3.6.tgz#2eb2e37ea9b67c4891f684a1394799af484cf7a2" + integrity sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw== + +map-cache@^0.2.2: + version "0.2.2" + resolved "https://registry.yarnpkg.com/map-cache/-/map-cache-0.2.2.tgz#c32abd0bd6525d9b051645bb4f26ac5dc98a0dbf" + integrity sha1-wyq9C9ZSXZsFFkW7TyasXcmKDb8= + +map-obj@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/map-obj/-/map-obj-1.0.1.tgz#d933ceb9205d82bdcf4886f6742bdc2b4dea146d" + integrity sha1-2TPOuSBdgr3PSIb2dCvcK03qFG0= + +map-obj@^4.0.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/map-obj/-/map-obj-4.1.0.tgz#b91221b542734b9f14256c0132c897c5d7256fd5" + integrity sha512-glc9y00wgtwcDmp7GaE/0b0OnxpNJsVf3ael/An6Fe2Q51LLwN1er6sdomLRzz5h0+yMpiYLhWYF5R7HeqVd4g== + +map-visit@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/map-visit/-/map-visit-1.0.0.tgz#ecdca8f13144e660f1b5bd41f12f3479d98dfb8f" + integrity sha1-7Nyo8TFE5mDxtb1B8S80edmN+48= + dependencies: + object-visit "^1.0.0" + +mathml-tag-names@^2.1.3: + version "2.1.3" + resolved "https://registry.yarnpkg.com/mathml-tag-names/-/mathml-tag-names-2.1.3.tgz#4ddadd67308e780cf16a47685878ee27b736a0a3" + integrity sha512-APMBEanjybaPzUrfqU0IMU5I0AswKMH7k8OTLs0vvV4KZpExkTkY87nR/zpbuTPj+gARop7aGUbl11pnDfW6xg== + +mdast-util-from-markdown@^0.8.0: + version "0.8.4" + resolved "https://registry.yarnpkg.com/mdast-util-from-markdown/-/mdast-util-from-markdown-0.8.4.tgz#2882100c1b9fc967d3f83806802f303666682d32" + integrity sha512-jj891B5pV2r63n2kBTFh8cRI2uR9LQHsXG1zSDqfhXkIlDzrTcIlbB5+5aaYEkl8vOPIOPLf8VT7Ere1wWTMdw== + dependencies: + "@types/mdast" "^3.0.0" + mdast-util-to-string "^2.0.0" + micromark "~2.11.0" + parse-entities "^2.0.0" + unist-util-stringify-position "^2.0.0" + +mdast-util-to-markdown@^0.6.0: + version "0.6.2" + resolved "https://registry.yarnpkg.com/mdast-util-to-markdown/-/mdast-util-to-markdown-0.6.2.tgz#8fe6f42a2683c43c5609dfb40407c095409c85b4" + integrity sha512-iRczns6WMvu0hUw02LXsPDJshBIwtUPbvHBWo19IQeU0YqmzlA8Pd30U8V7uiI0VPkxzS7A/NXBXH6u+HS87Zg== + dependencies: + "@types/unist" "^2.0.0" + longest-streak "^2.0.0" + mdast-util-to-string "^2.0.0" + parse-entities "^2.0.0" + repeat-string "^1.0.0" + zwitch "^1.0.0" + +mdast-util-to-string@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/mdast-util-to-string/-/mdast-util-to-string-2.0.0.tgz#b8cfe6a713e1091cb5b728fc48885a4767f8b97b" + integrity sha512-AW4DRS3QbBayY/jJmD8437V1Gombjf8RSOUCMFBuo5iHi58AGEgVCKQ+ezHkZZDpAQS75hcBMpLqjpJTjtUL7w== + +mdn-data@2.0.14: + version "2.0.14" + resolved "https://registry.yarnpkg.com/mdn-data/-/mdn-data-2.0.14.tgz#7113fc4281917d63ce29b43446f701e68c25ba50" + integrity sha512-dn6wd0uw5GsdswPFfsgMp5NSB0/aDe6fK94YJV/AJDYXL6HVLWBsxeq7js7Ad+mU2K9LAlwpk6kN2D5mwCPVow== + +mdn-data@2.0.4: + version "2.0.4" + resolved "https://registry.yarnpkg.com/mdn-data/-/mdn-data-2.0.4.tgz#699b3c38ac6f1d728091a64650b65d388502fd5b" + integrity sha512-iV3XNKw06j5Q7mi6h+9vbx23Tv7JkjEVgKHW4pimwyDGWm0OIQntJJ+u1C6mg6mK1EaTv42XQ7w76yuzH7M2cA== + +media-typer@0.3.0: + version "0.3.0" + resolved "https://registry.yarnpkg.com/media-typer/-/media-typer-0.3.0.tgz#8710d7af0aa626f8fffa1ce00168545263255748" + integrity sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g= + +memory-fs@^0.4.1: + version "0.4.1" + resolved "https://registry.yarnpkg.com/memory-fs/-/memory-fs-0.4.1.tgz#3a9a20b8462523e447cfbc7e8bb80ed667bfc552" + integrity sha1-OpoguEYlI+RHz7x+i7gO1me/xVI= + dependencies: + errno "^0.1.3" + readable-stream "^2.0.1" + +memory-fs@^0.5.0: + version "0.5.0" + resolved "https://registry.yarnpkg.com/memory-fs/-/memory-fs-0.5.0.tgz#324c01288b88652966d161db77838720845a8e3c" + integrity sha512-jA0rdU5KoQMC0e6ppoNRtpp6vjFq6+NY7r8hywnC7V+1Xj/MtHwGIbB1QaK/dunyjWteJzmkpd7ooeWg10T7GA== + dependencies: + errno "^0.1.3" + readable-stream "^2.0.1" + +meow@^9.0.0: + version "9.0.0" + resolved "https://registry.yarnpkg.com/meow/-/meow-9.0.0.tgz#cd9510bc5cac9dee7d03c73ee1f9ad959f4ea364" + integrity sha512-+obSblOQmRhcyBt62furQqRAQpNyWXo8BuQ5bN7dG8wmwQ+vwHKp/rCFD4CrTP8CsDQD1sjoZ94K417XEUk8IQ== + dependencies: + "@types/minimist" "^1.2.0" + camelcase-keys "^6.2.2" + decamelize "^1.2.0" + decamelize-keys "^1.1.0" + hard-rejection "^2.1.0" + minimist-options "4.1.0" + normalize-package-data "^3.0.0" + read-pkg-up "^7.0.1" + redent "^3.0.0" + trim-newlines "^3.0.0" + type-fest "^0.18.0" + yargs-parser "^20.2.3" + +merge-descriptors@1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/merge-descriptors/-/merge-descriptors-1.0.1.tgz#b00aaa556dd8b44568150ec9d1b953f3f90cbb61" + integrity sha1-sAqqVW3YtEVoFQ7J0blT8/kMu2E= + +merge-stream@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/merge-stream/-/merge-stream-2.0.0.tgz#52823629a14dd00c9770fb6ad47dc6310f2c1f60" + integrity sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w== + +merge2@^1.3.0: + version "1.4.1" + resolved "https://registry.yarnpkg.com/merge2/-/merge2-1.4.1.tgz#4368892f885e907455a6fd7dc55c0c9d404990ae" + integrity sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg== + +methods@~1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/methods/-/methods-1.1.2.tgz#5529a4d67654134edcc5266656835b0f851afcee" + integrity sha1-VSmk1nZUE07cxSZmVoNbD4Ua/O4= + +micromark@~2.11.0: + version "2.11.2" + resolved "https://registry.yarnpkg.com/micromark/-/micromark-2.11.2.tgz#e8b6a05f54697d2d3d27fc89600c6bc40dd05f35" + integrity sha512-IXuP76p2uj8uMg4FQc1cRE7lPCLsfAXuEfdjtdO55VRiFO1asrCSQ5g43NmPqFtRwzEnEhafRVzn2jg0UiKArQ== + dependencies: + debug "^4.0.0" + parse-entities "^2.0.0" + +micromatch@^3.1.10, micromatch@^3.1.4: + version "3.1.10" + resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-3.1.10.tgz#70859bc95c9840952f359a068a3fc49f9ecfac23" + integrity sha512-MWikgl9n9M3w+bpsY3He8L+w9eF9338xRl8IAO5viDizwSzziFEyUzo2xrrloB64ADbTf8uA8vRqqttDTOmccg== + dependencies: + arr-diff "^4.0.0" + array-unique "^0.3.2" + braces "^2.3.1" + define-property "^2.0.2" + extend-shallow "^3.0.2" + extglob "^2.0.4" + fragment-cache "^0.2.1" + kind-of "^6.0.2" + nanomatch "^1.2.9" + object.pick "^1.3.0" + regex-not "^1.0.0" + snapdragon "^0.8.1" + to-regex "^3.0.2" + +micromatch@^4.0.0, micromatch@^4.0.2: + version "4.0.2" + resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-4.0.2.tgz#4fcb0999bf9fbc2fcbdd212f6d629b9a56c39259" + integrity sha512-y7FpHSbMUMoyPbYUSzO6PaZ6FyRnQOpHuKwbo1G+Knck95XVU4QAiKdGEnj5wwoS7PlOgthX/09u5iFJ+aYf5Q== + dependencies: + braces "^3.0.1" + picomatch "^2.0.5" + +mime-db@1.45.0, "mime-db@>= 1.43.0 < 2": + version "1.45.0" + resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.45.0.tgz#cceeda21ccd7c3a745eba2decd55d4b73e7879ea" + integrity sha512-CkqLUxUk15hofLoLyljJSrukZi8mAtgd+yE5uO4tqRZsdsAJKv0O+rFMhVDRJgozy+yG6md5KwuXhD4ocIoP+w== + +mime-types@^2.1.27, mime-types@~2.1.17, mime-types@~2.1.24: + version "2.1.28" + resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.28.tgz#1160c4757eab2c5363888e005273ecf79d2a0ecd" + integrity sha512-0TO2yJ5YHYr7M2zzT7gDU1tbwHxEUWBCLt0lscSNpcdAfFyJOVEpRYNS7EXVcTLNj/25QO8gulHC5JtTzSE2UQ== + dependencies: + mime-db "1.45.0" + +mime@1.6.0, mime@^1.4.1: + version "1.6.0" + resolved "https://registry.yarnpkg.com/mime/-/mime-1.6.0.tgz#32cd9e5c64553bd58d19a568af452acff04981b1" + integrity sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg== + +mime@^2.4.4: + version "2.5.0" + resolved "https://registry.yarnpkg.com/mime/-/mime-2.5.0.tgz#2b4af934401779806ee98026bb42e8c1ae1876b1" + integrity sha512-ft3WayFSFUVBuJj7BMLKAQcSlItKtfjsKDDsii3rqFDAZ7t11zRe8ASw/GlmivGwVUYtwkQrxiGGpL6gFvB0ag== + +mimic-fn@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/mimic-fn/-/mimic-fn-2.1.0.tgz#7ed2c2ccccaf84d3ffcb7a69b57711fc2083401b" + integrity sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg== + +min-indent@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/min-indent/-/min-indent-1.0.1.tgz#a63f681673b30571fbe8bc25686ae746eefa9869" + integrity sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg== + +mini-create-react-context@^0.4.0: + version "0.4.1" + resolved "https://registry.yarnpkg.com/mini-create-react-context/-/mini-create-react-context-0.4.1.tgz#072171561bfdc922da08a60c2197a497cc2d1d5e" + integrity sha512-YWCYEmd5CQeHGSAKrYvXgmzzkrvssZcuuQDDeqkT+PziKGMgE+0MCCtcKbROzocGBG1meBLl2FotlRwf4gAzbQ== + dependencies: + "@babel/runtime" "^7.12.1" + tiny-warning "^1.0.3" + +mini-css-extract-plugin@^1.1.1: + version "1.3.4" + resolved "https://registry.yarnpkg.com/mini-css-extract-plugin/-/mini-css-extract-plugin-1.3.4.tgz#706e69632cdcdb8b15bf8e638442a0dba304a9c8" + integrity sha512-dNjqyeogUd8ucUgw5sxm1ahvSfSUgef7smbmATRSbDm4EmNx5kQA6VdUEhEeCKSjX6CTYjb5vxgMUvRjqP3uHg== + dependencies: + loader-utils "^2.0.0" + schema-utils "^3.0.0" + webpack-sources "^1.1.0" + +mini-store@^3.0.1: + version "3.0.6" + resolved "https://registry.yarnpkg.com/mini-store/-/mini-store-3.0.6.tgz#44b86be5b2877271224ce0689b3a35a2dffb1ca9" + integrity sha512-YzffKHbYsMQGUWQRKdsearR79QsMzzJcDDmZKlJBqt5JNkqpyJHYlK6gP61O36X+sLf76sO9G6mhKBe83gIZIQ== + dependencies: + hoist-non-react-statics "^3.3.2" + shallowequal "^1.0.2" + +minimalistic-assert@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz#2e194de044626d4a10e7f7fbc00ce73e83e4d5c7" + integrity sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A== + +minimatch@^3.0.4: + version "3.0.4" + resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.0.4.tgz#5166e286457f03306064be5497e8dbb0c3d32083" + integrity sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA== + dependencies: + brace-expansion "^1.1.7" + +minimist-options@4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/minimist-options/-/minimist-options-4.1.0.tgz#c0655713c53a8a2ebd77ffa247d342c40f010619" + integrity sha512-Q4r8ghd80yhO/0j1O3B2BjweX3fiHg9cdOwjJd2J76Q135c+NDxGCqdYKQ1SKBuFfgWbAUzBfvYjPUEeNgqN1A== + dependencies: + arrify "^1.0.1" + is-plain-obj "^1.1.0" + kind-of "^6.0.3" + +minimist@^1.2.0, minimist@^1.2.5: + version "1.2.5" + resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.5.tgz#67d66014b66a6a8aaa0c083c5fd58df4e4e97602" + integrity sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw== + +minipass-collect@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/minipass-collect/-/minipass-collect-1.0.2.tgz#22b813bf745dc6edba2576b940022ad6edc8c617" + integrity sha512-6T6lH0H8OG9kITm/Jm6tdooIbogG9e0tLgpY6mphXSm/A9u8Nq1ryBG+Qspiub9LjWlBPsPS3tWQ/Botq4FdxA== + dependencies: + minipass "^3.0.0" + +minipass-flush@^1.0.5: + version "1.0.5" + resolved "https://registry.yarnpkg.com/minipass-flush/-/minipass-flush-1.0.5.tgz#82e7135d7e89a50ffe64610a787953c4c4cbb373" + integrity sha512-JmQSYYpPUqX5Jyn1mXaRwOda1uQ8HP5KAT/oDSLCzt1BYRhQU0/hDtsB1ufZfEEzMZ9aAVmsBw8+FWsIXlClWw== + dependencies: + minipass "^3.0.0" + +minipass-pipeline@^1.2.2: + version "1.2.4" + resolved "https://registry.yarnpkg.com/minipass-pipeline/-/minipass-pipeline-1.2.4.tgz#68472f79711c084657c067c5c6ad93cddea8214c" + integrity sha512-xuIq7cIOt09RPRJ19gdi4b+RiNvDFYe5JH+ggNvBqGqpQXcru3PcRmOZuHBKWK1Txf9+cQ+HMVN4d6z46LZP7A== + dependencies: + minipass "^3.0.0" + +minipass@^3.0.0, minipass@^3.1.1: + version "3.1.3" + resolved "https://registry.yarnpkg.com/minipass/-/minipass-3.1.3.tgz#7d42ff1f39635482e15f9cdb53184deebd5815fd" + integrity sha512-Mgd2GdMVzY+x3IJ+oHnVM+KG3lA5c8tnabyJKmHSaG2kAGpudxuOf8ToDkhumF7UzME7DecbQE9uOZhNm7PuJg== + dependencies: + yallist "^4.0.0" + +minizlib@^2.1.1: + version "2.1.2" + resolved "https://registry.yarnpkg.com/minizlib/-/minizlib-2.1.2.tgz#e90d3466ba209b932451508a11ce3d3632145931" + integrity sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg== + dependencies: + minipass "^3.0.0" + yallist "^4.0.0" + +mixin-deep@^1.2.0: + version "1.3.2" + resolved "https://registry.yarnpkg.com/mixin-deep/-/mixin-deep-1.3.2.tgz#1120b43dc359a785dce65b55b82e257ccf479566" + integrity sha512-WRoDn//mXBiJ1H40rqa3vH0toePwSsGb45iInWlTySa+Uu4k3tYUSxa2v1KqAiLtvlrSzaExqS1gtk96A9zvEA== + dependencies: + for-in "^1.0.2" + is-extendable "^1.0.1" + +mkdirp@^0.5.1, mkdirp@^0.5.5, mkdirp@~0.5.1: + version "0.5.5" + resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.5.tgz#d91cefd62d1436ca0f41620e251288d420099def" + integrity sha512-NKmAlESf6jMGym1++R0Ra7wvhV+wFW63FaSOFPwRahvea0gMUcGUhVeAg/0BC0wiv9ih5NYPB1Wn1UEI1/L+xQ== + dependencies: + minimist "^1.2.5" + +mkdirp@^1.0.3, mkdirp@^1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-1.0.4.tgz#3eb5ed62622756d79a5f0e2a221dfebad75c2f7e" + integrity sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw== + +mobx-react-lite@^3.0.1: + version "3.1.7" + resolved "https://registry.yarnpkg.com/mobx-react-lite/-/mobx-react-lite-3.1.7.tgz#8896e9ec4f3d2117ddc0a5bfc60802c39a5258f4" + integrity sha512-G6kgur98RrEpoi563ERzxlSwn606xY6Ay4BOtfYXl/QaHZYXbhbCe3YJGROPU2ao4f2bfhnt8iZW3YnBW2BqXQ== + +mobx@^6.0.1: + version "6.0.5" + resolved "https://registry.yarnpkg.com/mobx/-/mobx-6.0.5.tgz#617e716b7aa81d5b700598af3bb1643ff2144410" + integrity sha512-Q3/GiSj/QyazDn1n44PjdiMlokCE6gVs85BnFR0xCJmkF2d34k/ZnWAEG7wGbnJYEi+0f5CdvzDquBiKeo56bA== + +moment@^2.24.0, moment@^2.25.3: + version "2.29.1" + resolved "https://registry.yarnpkg.com/moment/-/moment-2.29.1.tgz#b2be769fa31940be9eeea6469c075e35006fa3d3" + integrity sha512-kHmoybcPV8Sqy59DwNDY3Jefr64lK/by/da0ViFcuA4DH0vQg5Q6Ze5VimxkfQNSC+Mls/Kx53s7TjP1RhFEDQ== + +ms@2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8" + integrity sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g= + +ms@2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.1.tgz#30a5864eb3ebb0a66f2ebe6d727af06a09d86e0a" + integrity sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg== + +ms@2.1.2: + version "2.1.2" + resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009" + integrity sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w== + +ms@^2.1.1: + version "2.1.3" + resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.3.tgz#574c8138ce1d2b5861f0b44579dbadd60c6615b2" + integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA== + +multicast-dns-service-types@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/multicast-dns-service-types/-/multicast-dns-service-types-1.1.0.tgz#899f11d9686e5e05cb91b35d5f0e63b773cfc901" + integrity sha1-iZ8R2WhuXgXLkbNdXw5jt3PPyQE= + +multicast-dns@^6.0.1: + version "6.2.3" + resolved "https://registry.yarnpkg.com/multicast-dns/-/multicast-dns-6.2.3.tgz#a0ec7bd9055c4282f790c3c82f4e28db3b31b229" + integrity sha512-ji6J5enbMyGRHIAkAOu3WdV8nggqviKCEKtXcOqfphZZtQrmHKycfynJ2V7eVPUA4NhJ6V7Wf4TmGbTwKE9B6g== + dependencies: + dns-packet "^1.3.1" + thunky "^1.0.2" + +multimatch@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/multimatch/-/multimatch-4.0.0.tgz#8c3c0f6e3e8449ada0af3dd29efb491a375191b3" + integrity sha512-lDmx79y1z6i7RNx0ZGCPq1bzJ6ZoDDKbvh7jxr9SJcWLkShMzXrHbYVpTdnhNM5MXpDUxCQ4DgqVttVXlBgiBQ== + dependencies: + "@types/minimatch" "^3.0.3" + array-differ "^3.0.0" + array-union "^2.1.0" + arrify "^2.0.1" + minimatch "^3.0.4" + +nan@^2.12.1: + version "2.14.2" + resolved "https://registry.yarnpkg.com/nan/-/nan-2.14.2.tgz#f5376400695168f4cc694ac9393d0c9585eeea19" + integrity sha512-M2ufzIiINKCuDfBSAUr1vWQ+vuVcA9kqx8JJUsbQi6yf1uGRyb7HfpdfUr5qLXf3B/t8dPvcjhKMmlfnP47EzQ== + +nanoid@^3.1.20: + version "3.1.20" + resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.1.20.tgz#badc263c6b1dcf14b71efaa85f6ab4c1d6cfc788" + integrity sha512-a1cQNyczgKbLX9jwbS/+d7W8fX/RfgYR7lVWwWOGIPNgK2m0MWvrGF6/m4kk6U3QcFMnZf3RIhL0v2Jgh/0Uxw== + +nanomatch@^1.2.9: + version "1.2.13" + resolved "https://registry.yarnpkg.com/nanomatch/-/nanomatch-1.2.13.tgz#b87a8aa4fc0de8fe6be88895b38983ff265bd119" + integrity sha512-fpoe2T0RbHwBTBUOftAfBPaDEi06ufaUai0mE6Yn1kacc3SnTErfb/h+X94VXzI64rKFHYImXSvdwGGCmwOqCA== + dependencies: + arr-diff "^4.0.0" + array-unique "^0.3.2" + define-property "^2.0.2" + extend-shallow "^3.0.2" + fragment-cache "^0.2.1" + is-windows "^1.0.2" + kind-of "^6.0.2" + object.pick "^1.3.0" + regex-not "^1.0.0" + snapdragon "^0.8.1" + to-regex "^3.0.1" + +native-request@^1.0.5: + version "1.0.8" + resolved "https://registry.yarnpkg.com/native-request/-/native-request-1.0.8.tgz#8f66bf606e0f7ea27c0e5995eb2f5d03e33ae6fb" + integrity sha512-vU2JojJVelUGp6jRcLwToPoWGxSx23z/0iX+I77J3Ht17rf2INGjrhOoQnjVo60nQd8wVsgzKkPfRXBiVdD2ag== + +natural-compare@^1.4.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/natural-compare/-/natural-compare-1.4.0.tgz#4abebfeed7541f2c27acfb29bdbbd15c8d5ba4f7" + integrity sha1-Sr6/7tdUHywnrPspvbvRXI1bpPc= + +negotiator@0.6.2: + version "0.6.2" + resolved "https://registry.yarnpkg.com/negotiator/-/negotiator-0.6.2.tgz#feacf7ccf525a77ae9634436a64883ffeca346fb" + integrity sha512-hZXc7K2e+PgeI1eDBe/10Ard4ekbfrrqG8Ep+8Jmf4JID2bNg7NvCPOZN+kfF574pFQI7mum2AUqDidoKqcTOw== + +neo-async@^2.6.2: + version "2.6.2" + resolved "https://registry.yarnpkg.com/neo-async/-/neo-async-2.6.2.tgz#b4aafb93e3aeb2d8174ca53cf163ab7d7308305f" + integrity sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw== + +nice-try@^1.0.4: + version "1.0.5" + resolved "https://registry.yarnpkg.com/nice-try/-/nice-try-1.0.5.tgz#a3378a7696ce7d223e88fc9b764bd7ef1089e366" + integrity sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ== + +no-case@^3.0.4: + version "3.0.4" + resolved "https://registry.yarnpkg.com/no-case/-/no-case-3.0.4.tgz#d361fd5c9800f558551a8369fc0dcd4662b6124d" + integrity sha512-fgAN3jGAh+RoxUGZHTSOLJIqUc2wmoBwGR4tbpNAKmmovFoWq0OdRkb0VkldReO2a2iBT/OEulG9XSUc10r3zg== + dependencies: + lower-case "^2.0.2" + tslib "^2.0.3" + +node-forge@^0.10.0: + version "0.10.0" + resolved "https://registry.yarnpkg.com/node-forge/-/node-forge-0.10.0.tgz#32dea2afb3e9926f02ee5ce8794902691a676bf3" + integrity sha512-PPmu8eEeG9saEUvI97fm4OYxXVB6bFvyNTyiUOBichBpFG8A1Ljw3bY62+5oOjDEMHRnd0Y7HQ+x7uzxOzC6JA== + +node-releases@^1.1.69: + version "1.1.70" + resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-1.1.70.tgz#66e0ed0273aa65666d7fe78febe7634875426a08" + integrity sha512-Slf2s69+2/uAD79pVVQo8uSiC34+g8GWY8UH2Qtqv34ZfhYrxpYpfzs9Js9d6O0mbDmALuxaTlplnBTnSELcrw== + +normalize-package-data@^2.3.2, normalize-package-data@^2.5.0: + version "2.5.0" + resolved "https://registry.yarnpkg.com/normalize-package-data/-/normalize-package-data-2.5.0.tgz#e66db1838b200c1dfc233225d12cb36520e234a8" + integrity sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA== + dependencies: + hosted-git-info "^2.1.4" + resolve "^1.10.0" + semver "2 || 3 || 4 || 5" + validate-npm-package-license "^3.0.1" + +normalize-package-data@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/normalize-package-data/-/normalize-package-data-3.0.0.tgz#1f8a7c423b3d2e85eb36985eaf81de381d01301a" + integrity sha512-6lUjEI0d3v6kFrtgA/lOx4zHCWULXsFNIjHolnZCKCTLA6m/G625cdn3O7eNmT0iD3jfo6HZ9cdImGZwf21prw== + dependencies: + hosted-git-info "^3.0.6" + resolve "^1.17.0" + semver "^7.3.2" + validate-npm-package-license "^3.0.1" + +normalize-path@^2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/normalize-path/-/normalize-path-2.1.1.tgz#1ab28b556e198363a8c1a6f7e6fa20137fe6aed9" + integrity sha1-GrKLVW4Zg2Oowab35vogE3/mrtk= + dependencies: + remove-trailing-separator "^1.0.1" + +normalize-path@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/normalize-path/-/normalize-path-3.0.0.tgz#0dcd69ff23a1c9b11fd0978316644a0388216a65" + integrity sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA== + +normalize-range@^0.1.2: + version "0.1.2" + resolved "https://registry.yarnpkg.com/normalize-range/-/normalize-range-0.1.2.tgz#2d10c06bdfd312ea9777695a4d28439456b75942" + integrity sha1-LRDAa9/TEuqXd2laTShDlFa3WUI= + +normalize-selector@^0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/normalize-selector/-/normalize-selector-0.2.0.tgz#d0b145eb691189c63a78d201dc4fdb1293ef0c03" + integrity sha1-0LFF62kRicY6eNIB3E/bEpPvDAM= + +normalize-url@^3.0.0: + version "3.3.0" + resolved "https://registry.yarnpkg.com/normalize-url/-/normalize-url-3.3.0.tgz#b2e1c4dc4f7c6d57743df733a4f5978d18650559" + integrity sha512-U+JJi7duF1o+u2pynbp2zXDW2/PADgC30f0GsHZtRh+HOcXHnw137TrNlyxxRvWW5fjKd3bcLHPxofWuCjaeZg== + +npm-run-path@^2.0.0: + version "2.0.2" + resolved "https://registry.yarnpkg.com/npm-run-path/-/npm-run-path-2.0.2.tgz#35a9232dfa35d7067b4cb2ddf2357b1871536c5f" + integrity sha1-NakjLfo11wZ7TLLd8jV7GHFTbF8= + dependencies: + path-key "^2.0.0" + +npm-run-path@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/npm-run-path/-/npm-run-path-4.0.1.tgz#b7ecd1e5ed53da8e37a55e1c2269e0b97ed748ea" + integrity sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw== + dependencies: + path-key "^3.0.0" + +nth-check@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/nth-check/-/nth-check-1.0.2.tgz#b2bd295c37e3dd58a3bf0700376663ba4d9cf05c" + integrity sha512-WeBOdju8SnzPN5vTUJYxYUxLeXpCaVP5i5e0LF8fg7WORF2Wd7wFX/pk0tYZk7s8T+J7VLy0Da6J1+wCT0AtHg== + dependencies: + boolbase "~1.0.0" + +num2fraction@^1.2.2: + version "1.2.2" + resolved "https://registry.yarnpkg.com/num2fraction/-/num2fraction-1.2.2.tgz#6f682b6a027a4e9ddfa4564cd2589d1d4e669ede" + integrity sha1-b2gragJ6Tp3fpFZM0lidHU5mnt4= + +object-assign@^4.0.1, object-assign@^4.1.1: + version "4.1.1" + resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863" + integrity sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM= + +object-copy@^0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/object-copy/-/object-copy-0.1.0.tgz#7e7d858b781bd7c991a41ba975ed3812754e998c" + integrity sha1-fn2Fi3gb18mRpBupde04EnVOmYw= + dependencies: + copy-descriptor "^0.1.0" + define-property "^0.2.5" + kind-of "^3.0.3" + +object-hash@^2.0.3: + version "2.1.1" + resolved "https://registry.yarnpkg.com/object-hash/-/object-hash-2.1.1.tgz#9447d0279b4fcf80cff3259bf66a1dc73afabe09" + integrity sha512-VOJmgmS+7wvXf8CjbQmimtCnEx3IAoLxI3fp2fbWehxrWBcAQFbk+vcwb6vzR0VZv/eNCJ/27j151ZTwqW/JeQ== + +object-inspect@^1.8.0, object-inspect@^1.9.0: + version "1.9.0" + resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.9.0.tgz#c90521d74e1127b67266ded3394ad6116986533a" + integrity sha512-i3Bp9iTqwhaLZBxGkRfo5ZbE07BQRT7MGu8+nNgwW9ItGp1TzCTw2DLEoWwjClxBjOFI/hWljTAmYGCEwmtnOw== + +object-is@^1.0.1: + version "1.1.4" + resolved "https://registry.yarnpkg.com/object-is/-/object-is-1.1.4.tgz#63d6c83c00a43f4cbc9434eb9757c8a5b8565068" + integrity sha512-1ZvAZ4wlF7IyPVOcE1Omikt7UpaFlOQq0HlSti+ZvDH3UiD2brwGMwDbyV43jao2bKJ+4+WdPJHSd7kgzKYVqg== + dependencies: + call-bind "^1.0.0" + define-properties "^1.1.3" + +object-keys@^1.0.12, object-keys@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/object-keys/-/object-keys-1.1.1.tgz#1c47f272df277f3b1daf061677d9c82e2322c60e" + integrity sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA== + +object-visit@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/object-visit/-/object-visit-1.0.1.tgz#f79c4493af0c5377b59fe39d395e41042dd045bb" + integrity sha1-95xEk68MU3e1n+OdOV5BBC3QRbs= + dependencies: + isobject "^3.0.0" + +object.assign@^4.1.0, object.assign@^4.1.1, object.assign@^4.1.2: + version "4.1.2" + resolved "https://registry.yarnpkg.com/object.assign/-/object.assign-4.1.2.tgz#0ed54a342eceb37b38ff76eb831a0e788cb63940" + integrity sha512-ixT2L5THXsApyiUPYKmW+2EHpXXe5Ii3M+f4e+aJFAHao5amFRW6J0OO6c/LU8Be47utCx2GL89hxGB6XSmKuQ== + dependencies: + call-bind "^1.0.0" + define-properties "^1.1.3" + has-symbols "^1.0.1" + object-keys "^1.1.1" + +object.entries@^1.1.2: + version "1.1.3" + resolved "https://registry.yarnpkg.com/object.entries/-/object.entries-1.1.3.tgz#c601c7f168b62374541a07ddbd3e2d5e4f7711a6" + integrity sha512-ym7h7OZebNS96hn5IJeyUmaWhaSM4SVtAPPfNLQEI2MYWCO2egsITb9nab2+i/Pwibx+R0mtn+ltKJXRSeTMGg== + dependencies: + call-bind "^1.0.0" + define-properties "^1.1.3" + es-abstract "^1.18.0-next.1" + has "^1.0.3" + +object.fromentries@^2.0.2: + version "2.0.3" + resolved "https://registry.yarnpkg.com/object.fromentries/-/object.fromentries-2.0.3.tgz#13cefcffa702dc67750314a3305e8cb3fad1d072" + integrity sha512-IDUSMXs6LOSJBWE++L0lzIbSqHl9KDCfff2x/JSEIDtEUavUnyMYC2ZGay/04Zq4UT8lvd4xNhU4/YHKibAOlw== + dependencies: + call-bind "^1.0.0" + define-properties "^1.1.3" + es-abstract "^1.18.0-next.1" + has "^1.0.3" + +object.getownpropertydescriptors@^2.0.3, object.getownpropertydescriptors@^2.1.0: + version "2.1.1" + resolved "https://registry.yarnpkg.com/object.getownpropertydescriptors/-/object.getownpropertydescriptors-2.1.1.tgz#0dfda8d108074d9c563e80490c883b6661091544" + integrity sha512-6DtXgZ/lIZ9hqx4GtZETobXLR/ZLaa0aqV0kzbn80Rf8Z2e/XFnhA0I7p07N2wH8bBBltr2xQPi6sbKWAY2Eng== + dependencies: + call-bind "^1.0.0" + define-properties "^1.1.3" + es-abstract "^1.18.0-next.1" + +object.pick@^1.3.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/object.pick/-/object.pick-1.3.0.tgz#87a10ac4c1694bd2e1cbf53591a66141fb5dd747" + integrity sha1-h6EKxMFpS9Lhy/U1kaZhQftd10c= + dependencies: + isobject "^3.0.1" + +object.values@^1.1.0, object.values@^1.1.1: + version "1.1.2" + resolved "https://registry.yarnpkg.com/object.values/-/object.values-1.1.2.tgz#7a2015e06fcb0f546bd652486ce8583a4731c731" + integrity sha512-MYC0jvJopr8EK6dPBiO8Nb9mvjdypOachO5REGk6MXzujbBrAisKo3HmdEI6kZDL6fC31Mwee/5YbtMebixeag== + dependencies: + call-bind "^1.0.0" + define-properties "^1.1.3" + es-abstract "^1.18.0-next.1" + has "^1.0.3" + +obuf@^1.0.0, obuf@^1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/obuf/-/obuf-1.1.2.tgz#09bea3343d41859ebd446292d11c9d4db619084e" + integrity sha512-PX1wu0AmAdPqOL1mWhqmlOd8kOIZQwGZw6rh7uby9fTc5lhaOWFLX3I6R1hrF9k3zUY40e6igsLGkDXK92LJNg== + +on-finished@~2.3.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/on-finished/-/on-finished-2.3.0.tgz#20f1336481b083cd75337992a16971aa2d906947" + integrity sha1-IPEzZIGwg811M3mSoWlxqi2QaUc= + dependencies: + ee-first "1.1.1" + +on-headers@~1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/on-headers/-/on-headers-1.0.2.tgz#772b0ae6aaa525c399e489adfad90c403eb3c28f" + integrity sha512-pZAE+FJLoyITytdqK0U5s+FIpjN0JP3OzFi/u8Rx+EV5/W+JTWGXG8xFzevE7AjBfDqHv/8vL8qQsIhHnqRkrA== + +once@^1.3.0, once@^1.3.1, once@^1.4.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1" + integrity sha1-WDsap3WWHUsROsF9nFC6753Xa9E= + dependencies: + wrappy "1" + +onetime@^5.1.2: + version "5.1.2" + resolved "https://registry.yarnpkg.com/onetime/-/onetime-5.1.2.tgz#d0e96ebb56b07476df1dd9c4806e5237985ca45e" + integrity sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg== + dependencies: + mimic-fn "^2.1.0" + +opn@^5.5.0: + version "5.5.0" + resolved "https://registry.yarnpkg.com/opn/-/opn-5.5.0.tgz#fc7164fab56d235904c51c3b27da6758ca3b9bfc" + integrity sha512-PqHpggC9bLV0VeWcdKhkpxY+3JTzetLSqTCWL/z/tFIbI6G8JCjondXklT1JinczLz2Xib62sSp0T/gKT4KksA== + dependencies: + is-wsl "^1.1.0" + +optimize-css-assets-webpack-plugin@^5.0.4: + version "5.0.4" + resolved "https://registry.yarnpkg.com/optimize-css-assets-webpack-plugin/-/optimize-css-assets-webpack-plugin-5.0.4.tgz#85883c6528aaa02e30bbad9908c92926bb52dc90" + integrity sha512-wqd6FdI2a5/FdoiCNNkEvLeA//lHHfG24Ln2Xm2qqdIk4aOlsR18jwpyOihqQ8849W3qu2DX8fOYxpvTMj+93A== + dependencies: + cssnano "^4.1.10" + last-call-webpack-plugin "^3.0.0" + +optionator@^0.9.1: + version "0.9.1" + resolved "https://registry.yarnpkg.com/optionator/-/optionator-0.9.1.tgz#4f236a6373dae0566a6d43e1326674f50c291499" + integrity sha512-74RlY5FCnhq4jRxVUPKDaRwrVNXMqsGsiW6AJw4XK8hmtm10wC0ypZBLw5IIp85NZMr91+qd1RvvENwg7jjRFw== + dependencies: + deep-is "^0.1.3" + fast-levenshtein "^2.0.6" + levn "^0.4.1" + prelude-ls "^1.2.1" + type-check "^0.4.0" + word-wrap "^1.2.3" + +original@^1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/original/-/original-1.0.2.tgz#e442a61cffe1c5fd20a65f3261c26663b303f25f" + integrity sha512-hyBVl6iqqUOJ8FqRe+l/gS8H+kKYjrEndd5Pm1MfBtsEKA038HkkdbAl/72EAXGyonD/PFsvmVG+EvcIpliMBg== + dependencies: + url-parse "^1.4.3" + +p-finally@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/p-finally/-/p-finally-1.0.0.tgz#3fbcfb15b899a44123b34b6dcc18b724336a2cae" + integrity sha1-P7z7FbiZpEEjs0ttzBi3JDNqLK4= + +p-limit@^1.1.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-1.3.0.tgz#b86bd5f0c25690911c7590fcbfc2010d54b3ccb8" + integrity sha512-vvcXsLAJ9Dr5rQOPk7toZQZJApBl2K4J6dANSsEuh6QI41JYcsS/qhTGa9ErIUUgK3WNQoJYvylxvjqmiqEA9Q== + dependencies: + p-try "^1.0.0" + +p-limit@^2.0.0, p-limit@^2.2.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-2.3.0.tgz#3dd33c647a214fdfffd835933eb086da0dc21db1" + integrity sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w== + dependencies: + p-try "^2.0.0" + +p-limit@^3.0.2, p-limit@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-3.1.0.tgz#e1daccbe78d0d1388ca18c64fea38e3e57e3706b" + integrity sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ== + dependencies: + yocto-queue "^0.1.0" + +p-locate@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/p-locate/-/p-locate-2.0.0.tgz#20a0103b222a70c8fd39cc2e580680f3dde5ec43" + integrity sha1-IKAQOyIqcMj9OcwuWAaA893l7EM= + dependencies: + p-limit "^1.1.0" + +p-locate@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/p-locate/-/p-locate-3.0.0.tgz#322d69a05c0264b25997d9f40cd8a891ab0064a4" + integrity sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ== + dependencies: + p-limit "^2.0.0" + +p-locate@^4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/p-locate/-/p-locate-4.1.0.tgz#a3428bb7088b3a60292f66919278b7c297ad4f07" + integrity sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A== + dependencies: + p-limit "^2.2.0" + +p-locate@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/p-locate/-/p-locate-5.0.0.tgz#83c8315c6785005e3bd021839411c9e110e6d834" + integrity sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw== + dependencies: + p-limit "^3.0.2" + +p-map@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/p-map/-/p-map-2.1.0.tgz#310928feef9c9ecc65b68b17693018a665cea175" + integrity sha512-y3b8Kpd8OAN444hxfBbFfj1FY/RjtTd8tzYwhUqNYXx0fXx2iX4maP4Qr6qhIKbQXI02wTLAda4fYUbDagTUFw== + +p-map@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/p-map/-/p-map-4.0.0.tgz#bb2f95a5eda2ec168ec9274e06a747c3e2904d2b" + integrity sha512-/bjOqmgETBYB5BoEeGVea8dmvHb2m9GLy1E9W43yeyfP6QQCZGFNa+XRceJEuDB6zqr+gKpIAmlLebMpykw/MQ== + dependencies: + aggregate-error "^3.0.0" + +p-retry@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/p-retry/-/p-retry-3.0.1.tgz#316b4c8893e2c8dc1cfa891f406c4b422bebf328" + integrity sha512-XE6G4+YTTkT2a0UWb2kjZe8xNwf8bIbnqpc/IS/idOBVhyves0mK5OJgeocjx7q5pvX/6m23xuzVPYT1uGM73w== + dependencies: + retry "^0.12.0" + +p-try@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/p-try/-/p-try-1.0.0.tgz#cbc79cdbaf8fd4228e13f621f2b1a237c1b207b3" + integrity sha1-y8ec26+P1CKOE/Yh8rGiN8GyB7M= + +p-try@^2.0.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/p-try/-/p-try-2.2.0.tgz#cb2868540e313d61de58fafbe35ce9004d5540e6" + integrity sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ== + +param-case@^3.0.3: + version "3.0.4" + resolved "https://registry.yarnpkg.com/param-case/-/param-case-3.0.4.tgz#7d17fe4aa12bde34d4a77d91acfb6219caad01c5" + integrity sha512-RXlj7zCYokReqWpOPH9oYivUzLYZ5vAPIfEmCTNViosC78F8F0H9y7T7gG2M39ymgutxF5gcFEsyZQSph9Bp3A== + dependencies: + dot-case "^3.0.4" + tslib "^2.0.3" + +parent-module@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/parent-module/-/parent-module-1.0.1.tgz#691d2709e78c79fae3a156622452d00762caaaa2" + integrity sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g== + dependencies: + callsites "^3.0.0" + +parse-entities@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/parse-entities/-/parse-entities-2.0.0.tgz#53c6eb5b9314a1f4ec99fa0fdf7ce01ecda0cbe8" + integrity sha512-kkywGpCcRYhqQIchaWqZ875wzpS/bMKhz5HnN3p7wveJTkTtyAB/AlnS0f8DFSqYW1T82t6yEAkEcB+A1I3MbQ== + dependencies: + character-entities "^1.0.0" + character-entities-legacy "^1.0.0" + character-reference-invalid "^1.0.0" + is-alphanumerical "^1.0.0" + is-decimal "^1.0.0" + is-hexadecimal "^1.0.0" + +parse-json@^2.2.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/parse-json/-/parse-json-2.2.0.tgz#f480f40434ef80741f8469099f8dea18f55a4dc9" + integrity sha1-9ID0BDTvgHQfhGkJn43qGPVaTck= + dependencies: + error-ex "^1.2.0" + +parse-json@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/parse-json/-/parse-json-4.0.0.tgz#be35f5425be1f7f6c747184f98a788cb99477ee0" + integrity sha1-vjX1Qlvh9/bHRxhPmKeIy5lHfuA= + dependencies: + error-ex "^1.3.1" + json-parse-better-errors "^1.0.1" + +parse-json@^5.0.0: + version "5.2.0" + resolved "https://registry.yarnpkg.com/parse-json/-/parse-json-5.2.0.tgz#c76fc66dee54231c962b22bcc8a72cf2f99753cd" + integrity sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg== + dependencies: + "@babel/code-frame" "^7.0.0" + error-ex "^1.3.1" + json-parse-even-better-errors "^2.3.0" + lines-and-columns "^1.1.6" + +parseurl@~1.3.2, parseurl@~1.3.3: + version "1.3.3" + resolved "https://registry.yarnpkg.com/parseurl/-/parseurl-1.3.3.tgz#9da19e7bee8d12dff0513ed5b76957793bc2e8d4" + integrity sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ== + +pascal-case@^3.1.2: + version "3.1.2" + resolved "https://registry.yarnpkg.com/pascal-case/-/pascal-case-3.1.2.tgz#b48e0ef2b98e205e7c1dae747d0b1508237660eb" + integrity sha512-uWlGT3YSnK9x3BQJaOdcZwrnV6hPpd8jFH1/ucpiLRPh/2zCVJKS19E4GvYHvaCcACn3foXZ0cLB9Wrx1KGe5g== + dependencies: + no-case "^3.0.4" + tslib "^2.0.3" + +pascalcase@^0.1.1: + version "0.1.1" + resolved "https://registry.yarnpkg.com/pascalcase/-/pascalcase-0.1.1.tgz#b363e55e8006ca6fe21784d2db22bd15d7917f14" + integrity sha1-s2PlXoAGym/iF4TS2yK9FdeRfxQ= + +path-dirname@^1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/path-dirname/-/path-dirname-1.0.2.tgz#cc33d24d525e099a5388c0336c6e32b9160609e0" + integrity sha1-zDPSTVJeCZpTiMAzbG4yuRYGCeA= + +path-exists@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-3.0.0.tgz#ce0ebeaa5f78cb18925ea7d810d7b59b010fd515" + integrity sha1-zg6+ql94yxiSXqfYENe1mwEP1RU= + +path-exists@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-4.0.0.tgz#513bdbe2d3b95d7762e8c1137efa195c6c61b5b3" + integrity sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w== + +path-is-absolute@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f" + integrity sha1-F0uSaHNVNP+8es5r9TpanhtcX18= + +path-is-inside@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/path-is-inside/-/path-is-inside-1.0.2.tgz#365417dede44430d1c11af61027facf074bdfc53" + integrity sha1-NlQX3t5EQw0cEa9hAn+s8HS9/FM= + +path-key@^2.0.0, path-key@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/path-key/-/path-key-2.0.1.tgz#411cadb574c5a140d3a4b1910d40d80cc9f40b40" + integrity sha1-QRyttXTFoUDTpLGRDUDYDMn0C0A= + +path-key@^3.0.0, path-key@^3.1.0: + version "3.1.1" + resolved "https://registry.yarnpkg.com/path-key/-/path-key-3.1.1.tgz#581f6ade658cbba65a0d3380de7753295054f375" + integrity sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q== + +path-parse@^1.0.6: + version "1.0.6" + resolved "https://registry.yarnpkg.com/path-parse/-/path-parse-1.0.6.tgz#d62dbb5679405d72c4737ec58600e9ddcf06d24c" + integrity sha512-GSmOT2EbHrINBf9SR7CDELwlJ8AENk3Qn7OikK4nFYAu3Ote2+JYNVvkpAEQm3/TLNEJFD/xZJjzyxg3KBWOzw== + +path-to-regexp@0.1.7: + version "0.1.7" + resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-0.1.7.tgz#df604178005f522f15eb4490e7247a1bfaa67f8c" + integrity sha1-32BBeABfUi8V60SQ5yR6G/qmf4w= + +path-to-regexp@^1.7.0: + version "1.8.0" + resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-1.8.0.tgz#887b3ba9d84393e87a0a0b9f4cb756198b53548a" + integrity sha512-n43JRhlUKUAlibEJhPeir1ncUID16QnEjNpwzNdO3Lm4ywrBpBZ5oLD0I6br9evr1Y9JTqwRtAh7JLoOzAQdVA== + dependencies: + isarray "0.0.1" + +path-type@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/path-type/-/path-type-2.0.0.tgz#f012ccb8415b7096fc2daa1054c3d72389594c73" + integrity sha1-8BLMuEFbcJb8LaoQVMPXI4lZTHM= + dependencies: + pify "^2.0.0" + +path-type@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/path-type/-/path-type-4.0.0.tgz#84ed01c0a7ba380afe09d90a8c180dcd9d03043b" + integrity sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw== + +performance-now@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/performance-now/-/performance-now-2.1.0.tgz#6309f4e0e5fa913ec1c69307ae364b4b377c9e7b" + integrity sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns= + +picomatch@^2.0.5, picomatch@^2.2.1: + version "2.2.2" + resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.2.2.tgz#21f333e9b6b8eaff02468f5146ea406d345f4dad" + integrity sha512-q0M/9eZHzmr0AulXyPwNfZjtwZ/RBZlbN3K3CErVrk50T2ASYI7Bye0EvekFY3IP1Nt2DHu0re+V2ZHIpMkuWg== + +pify@^2.0.0, pify@^2.3.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/pify/-/pify-2.3.0.tgz#ed141a6ac043a849ea588498e7dca8b15330e90c" + integrity sha1-7RQaasBDqEnqWISY59yosVMw6Qw= + +pify@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/pify/-/pify-4.0.1.tgz#4b2cd25c50d598735c50292224fd8c6df41e3231" + integrity sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g== + +pinkie-promise@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/pinkie-promise/-/pinkie-promise-2.0.1.tgz#2135d6dfa7a358c069ac9b178776288228450ffa" + integrity sha1-ITXW36ejWMBprJsXh3YogihFD/o= + dependencies: + pinkie "^2.0.0" + +pinkie@^2.0.0: + version "2.0.4" + resolved "https://registry.yarnpkg.com/pinkie/-/pinkie-2.0.4.tgz#72556b80cfa0d48a974e80e77248e80ed4f7f870" + integrity sha1-clVrgM+g1IqXToDnckjoDtT3+HA= + +pkg-dir@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/pkg-dir/-/pkg-dir-2.0.0.tgz#f6d5d1109e19d63edf428e0bd57e12777615334b" + integrity sha1-9tXREJ4Z1j7fQo4L1X4Sd3YVM0s= + dependencies: + find-up "^2.1.0" + +pkg-dir@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/pkg-dir/-/pkg-dir-3.0.0.tgz#2749020f239ed990881b1f71210d51eb6523bea3" + integrity sha512-/E57AYkoeQ25qkxMj5PBOVgF8Kiu/h7cYS30Z5+R7WaiCCBfLq58ZI/dSeaEKb9WVJV5n/03QwrN3IeWIFllvw== + dependencies: + find-up "^3.0.0" + +pkg-dir@^4.1.0, pkg-dir@^4.2.0: + version "4.2.0" + resolved "https://registry.yarnpkg.com/pkg-dir/-/pkg-dir-4.2.0.tgz#f099133df7ede422e81d1d8448270eeb3e4261f3" + integrity sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ== + dependencies: + find-up "^4.0.0" + +pkg-dir@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/pkg-dir/-/pkg-dir-5.0.0.tgz#a02d6aebe6ba133a928f74aec20bafdfe6b8e760" + integrity sha512-NPE8TDbzl/3YQYY7CSS228s3g2ollTFnc+Qi3tqmqJp9Vg2ovUpixcJEo2HJScN2Ez+kEaal6y70c0ehqJBJeA== + dependencies: + find-up "^5.0.0" + +portfinder@^1.0.26: + version "1.0.28" + resolved "https://registry.yarnpkg.com/portfinder/-/portfinder-1.0.28.tgz#67c4622852bd5374dd1dd900f779f53462fac778" + integrity sha512-Se+2isanIcEqf2XMHjyUKskczxbPH7dQnlMjXX6+dybayyHvAf/TCgyMRlzf/B6QDhAEFOGes0pzRo3by4AbMA== + dependencies: + async "^2.6.2" + debug "^3.1.1" + mkdirp "^0.5.5" + +posix-character-classes@^0.1.0: + version "0.1.1" + resolved "https://registry.yarnpkg.com/posix-character-classes/-/posix-character-classes-0.1.1.tgz#01eac0fe3b5af71a2a6c02feabb8c1fef7e00eab" + integrity sha1-AerA/jta9xoqbAL+q7jB/vfgDqs= + +postcss-attribute-case-insensitive@^4.0.1: + version "4.0.2" + resolved "https://registry.yarnpkg.com/postcss-attribute-case-insensitive/-/postcss-attribute-case-insensitive-4.0.2.tgz#d93e46b504589e94ac7277b0463226c68041a880" + integrity sha512-clkFxk/9pcdb4Vkn0hAHq3YnxBQ2p0CGD1dy24jN+reBck+EWxMbxSUqN4Yj7t0w8csl87K6p0gxBe1utkJsYA== + dependencies: + postcss "^7.0.2" + postcss-selector-parser "^6.0.2" + +postcss-calc@^7.0.1, postcss-calc@^7.0.5: + version "7.0.5" + resolved "https://registry.yarnpkg.com/postcss-calc/-/postcss-calc-7.0.5.tgz#f8a6e99f12e619c2ebc23cf6c486fdc15860933e" + integrity sha512-1tKHutbGtLtEZF6PT4JSihCHfIVldU72mZ8SdZHIYriIZ9fh9k9aWSppaT8rHsyI3dX+KSR+W+Ix9BMY3AODrg== + dependencies: + postcss "^7.0.27" + postcss-selector-parser "^6.0.2" + postcss-value-parser "^4.0.2" + +postcss-color-functional-notation@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/postcss-color-functional-notation/-/postcss-color-functional-notation-2.0.1.tgz#5efd37a88fbabeb00a2966d1e53d98ced93f74e0" + integrity sha512-ZBARCypjEDofW4P6IdPVTLhDNXPRn8T2s1zHbZidW6rPaaZvcnCS2soYFIQJrMZSxiePJ2XIYTlcb2ztr/eT2g== + dependencies: + postcss "^7.0.2" + postcss-values-parser "^2.0.0" + +postcss-color-gray@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/postcss-color-gray/-/postcss-color-gray-5.0.0.tgz#532a31eb909f8da898ceffe296fdc1f864be8547" + integrity sha512-q6BuRnAGKM/ZRpfDascZlIZPjvwsRye7UDNalqVz3s7GDxMtqPY6+Q871liNxsonUw8oC61OG+PSaysYpl1bnw== + dependencies: + "@csstools/convert-colors" "^1.4.0" + postcss "^7.0.5" + postcss-values-parser "^2.0.0" + +postcss-color-hex-alpha@^5.0.3: + version "5.0.3" + resolved "https://registry.yarnpkg.com/postcss-color-hex-alpha/-/postcss-color-hex-alpha-5.0.3.tgz#a8d9ca4c39d497c9661e374b9c51899ef0f87388" + integrity sha512-PF4GDel8q3kkreVXKLAGNpHKilXsZ6xuu+mOQMHWHLPNyjiUBOr75sp5ZKJfmv1MCus5/DWUGcK9hm6qHEnXYw== + dependencies: + postcss "^7.0.14" + postcss-values-parser "^2.0.1" + +postcss-color-mod-function@^3.0.3: + version "3.0.3" + resolved "https://registry.yarnpkg.com/postcss-color-mod-function/-/postcss-color-mod-function-3.0.3.tgz#816ba145ac11cc3cb6baa905a75a49f903e4d31d" + integrity sha512-YP4VG+xufxaVtzV6ZmhEtc+/aTXH3d0JLpnYfxqTvwZPbJhWqp8bSY3nfNzNRFLgB4XSaBA82OE4VjOOKpCdVQ== + dependencies: + "@csstools/convert-colors" "^1.4.0" + postcss "^7.0.2" + postcss-values-parser "^2.0.0" + +postcss-color-rebeccapurple@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/postcss-color-rebeccapurple/-/postcss-color-rebeccapurple-4.0.1.tgz#c7a89be872bb74e45b1e3022bfe5748823e6de77" + integrity sha512-aAe3OhkS6qJXBbqzvZth2Au4V3KieR5sRQ4ptb2b2O8wgvB3SJBsdG+jsn2BZbbwekDG8nTfcCNKcSfe/lEy8g== + dependencies: + postcss "^7.0.2" + postcss-values-parser "^2.0.0" + +postcss-colormin@^4.0.3: + version "4.0.3" + resolved "https://registry.yarnpkg.com/postcss-colormin/-/postcss-colormin-4.0.3.tgz#ae060bce93ed794ac71264f08132d550956bd381" + integrity sha512-WyQFAdDZpExQh32j0U0feWisZ0dmOtPl44qYmJKkq9xFWY3p+4qnRzCHeNrkeRhwPHz9bQ3mo0/yVkaply0MNw== + dependencies: + browserslist "^4.0.0" + color "^3.0.0" + has "^1.0.0" + postcss "^7.0.0" + postcss-value-parser "^3.0.0" + +postcss-convert-values@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/postcss-convert-values/-/postcss-convert-values-4.0.1.tgz#ca3813ed4da0f812f9d43703584e449ebe189a7f" + integrity sha512-Kisdo1y77KUC0Jmn0OXU/COOJbzM8cImvw1ZFsBgBgMgb1iL23Zs/LXRe3r+EZqM3vGYKdQ2YJVQ5VkJI+zEJQ== + dependencies: + postcss "^7.0.0" + postcss-value-parser "^3.0.0" + +postcss-css-variables@^0.17.0: + version "0.17.0" + resolved "https://registry.yarnpkg.com/postcss-css-variables/-/postcss-css-variables-0.17.0.tgz#56cba1d9f0360609136cfbfda8bbd2c1ed2e4082" + integrity sha512-/ZpFnJgksNOrQA72b3DKhExYh+0e2P5nEc3aPZ62G7JLmdDjWRFv3k/q4LxV7uzXFnmvkhXRbdVIiH5tKgfFNA== + dependencies: + balanced-match "^1.0.0" + escape-string-regexp "^1.0.3" + extend "^3.0.1" + postcss "^6.0.8" + +postcss-custom-media@^7.0.8: + version "7.0.8" + resolved "https://registry.yarnpkg.com/postcss-custom-media/-/postcss-custom-media-7.0.8.tgz#fffd13ffeffad73621be5f387076a28b00294e0c" + integrity sha512-c9s5iX0Ge15o00HKbuRuTqNndsJUbaXdiNsksnVH8H4gdc+zbLzr/UasOwNG6CTDpLFekVY4672eWdiiWu2GUg== + dependencies: + postcss "^7.0.14" + +postcss-custom-properties@^8.0.11: + version "8.0.11" + resolved "https://registry.yarnpkg.com/postcss-custom-properties/-/postcss-custom-properties-8.0.11.tgz#2d61772d6e92f22f5e0d52602df8fae46fa30d97" + integrity sha512-nm+o0eLdYqdnJ5abAJeXp4CEU1c1k+eB2yMCvhgzsds/e0umabFrN6HoTy/8Q4K5ilxERdl/JD1LO5ANoYBeMA== + dependencies: + postcss "^7.0.17" + postcss-values-parser "^2.0.1" + +postcss-custom-selectors@^5.1.2: + version "5.1.2" + resolved "https://registry.yarnpkg.com/postcss-custom-selectors/-/postcss-custom-selectors-5.1.2.tgz#64858c6eb2ecff2fb41d0b28c9dd7b3db4de7fba" + integrity sha512-DSGDhqinCqXqlS4R7KGxL1OSycd1lydugJ1ky4iRXPHdBRiozyMHrdu0H3o7qNOCiZwySZTUI5MV0T8QhCLu+w== + dependencies: + postcss "^7.0.2" + postcss-selector-parser "^5.0.0-rc.3" + +postcss-dir-pseudo-class@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/postcss-dir-pseudo-class/-/postcss-dir-pseudo-class-5.0.0.tgz#6e3a4177d0edb3abcc85fdb6fbb1c26dabaeaba2" + integrity sha512-3pm4oq8HYWMZePJY+5ANriPs3P07q+LW6FAdTlkFH2XqDdP4HeeJYMOzn0HYLhRSjBO3fhiqSwwU9xEULSrPgw== + dependencies: + postcss "^7.0.2" + postcss-selector-parser "^5.0.0-rc.3" + +postcss-discard-comments@^4.0.2: + version "4.0.2" + resolved "https://registry.yarnpkg.com/postcss-discard-comments/-/postcss-discard-comments-4.0.2.tgz#1fbabd2c246bff6aaad7997b2b0918f4d7af4033" + integrity sha512-RJutN259iuRf3IW7GZyLM5Sw4GLTOH8FmsXBnv8Ab/Tc2k4SR4qbV4DNbyyY4+Sjo362SyDmW2DQ7lBSChrpkg== + dependencies: + postcss "^7.0.0" + +postcss-discard-duplicates@^4.0.2: + version "4.0.2" + resolved "https://registry.yarnpkg.com/postcss-discard-duplicates/-/postcss-discard-duplicates-4.0.2.tgz#3fe133cd3c82282e550fc9b239176a9207b784eb" + integrity sha512-ZNQfR1gPNAiXZhgENFfEglF93pciw0WxMkJeVmw8eF+JZBbMD7jp6C67GqJAXVZP2BWbOztKfbsdmMp/k8c6oQ== + dependencies: + postcss "^7.0.0" + +postcss-discard-empty@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/postcss-discard-empty/-/postcss-discard-empty-4.0.1.tgz#c8c951e9f73ed9428019458444a02ad90bb9f765" + integrity sha512-B9miTzbznhDjTfjvipfHoqbWKwd0Mj+/fL5s1QOz06wufguil+Xheo4XpOnc4NqKYBCNqqEzgPv2aPBIJLox0w== + dependencies: + postcss "^7.0.0" + +postcss-discard-overridden@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/postcss-discard-overridden/-/postcss-discard-overridden-4.0.1.tgz#652aef8a96726f029f5e3e00146ee7a4e755ff57" + integrity sha512-IYY2bEDD7g1XM1IDEsUT4//iEYCxAmP5oDSFMVU/JVvT7gh+l4fmjciLqGgwjdWpQIdb0Che2VX00QObS5+cTg== + dependencies: + postcss "^7.0.0" + +postcss-double-position-gradients@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/postcss-double-position-gradients/-/postcss-double-position-gradients-1.0.0.tgz#fc927d52fddc896cb3a2812ebc5df147e110522e" + integrity sha512-G+nV8EnQq25fOI8CH/B6krEohGWnF5+3A6H/+JEpOncu5dCnkS1QQ6+ct3Jkaepw1NGVqqOZH6lqrm244mCftA== + dependencies: + postcss "^7.0.5" + postcss-values-parser "^2.0.0" + +postcss-env-function@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/postcss-env-function/-/postcss-env-function-2.0.2.tgz#0f3e3d3c57f094a92c2baf4b6241f0b0da5365d7" + integrity sha512-rwac4BuZlITeUbiBq60h/xbLzXY43qOsIErngWa4l7Mt+RaSkT7QBjXVGTcBHupykkblHMDrBFh30zchYPaOUw== + dependencies: + postcss "^7.0.2" + postcss-values-parser "^2.0.0" + +postcss-focus-visible@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/postcss-focus-visible/-/postcss-focus-visible-4.0.0.tgz#477d107113ade6024b14128317ade2bd1e17046e" + integrity sha512-Z5CkWBw0+idJHSV6+Bgf2peDOFf/x4o+vX/pwcNYrWpXFrSfTkQ3JQ1ojrq9yS+upnAlNRHeg8uEwFTgorjI8g== + dependencies: + postcss "^7.0.2" + +postcss-focus-within@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/postcss-focus-within/-/postcss-focus-within-3.0.0.tgz#763b8788596cee9b874c999201cdde80659ef680" + integrity sha512-W0APui8jQeBKbCGZudW37EeMCjDeVxKgiYfIIEo8Bdh5SpB9sxds/Iq8SEuzS0Q4YFOlG7EPFulbbxujpkrV2w== + dependencies: + postcss "^7.0.2" + +postcss-font-variant@^4.0.0: + version "4.0.1" + resolved "https://registry.yarnpkg.com/postcss-font-variant/-/postcss-font-variant-4.0.1.tgz#42d4c0ab30894f60f98b17561eb5c0321f502641" + integrity sha512-I3ADQSTNtLTTd8uxZhtSOrTCQ9G4qUVKPjHiDk0bV75QSxXjVWiJVJ2VLdspGUi9fbW9BcjKJoRvxAH1pckqmA== + dependencies: + postcss "^7.0.2" + +postcss-gap-properties@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/postcss-gap-properties/-/postcss-gap-properties-2.0.0.tgz#431c192ab3ed96a3c3d09f2ff615960f902c1715" + integrity sha512-QZSqDaMgXCHuHTEzMsS2KfVDOq7ZFiknSpkrPJY6jmxbugUPTuSzs/vuE5I3zv0WAS+3vhrlqhijiprnuQfzmg== + dependencies: + postcss "^7.0.2" + +postcss-html@^0.36.0: + version "0.36.0" + resolved "https://registry.yarnpkg.com/postcss-html/-/postcss-html-0.36.0.tgz#b40913f94eaacc2453fd30a1327ad6ee1f88b204" + integrity sha512-HeiOxGcuwID0AFsNAL0ox3mW6MHH5cstWN1Z3Y+n6H+g12ih7LHdYxWwEA/QmrebctLjo79xz9ouK3MroHwOJw== + dependencies: + htmlparser2 "^3.10.0" + +postcss-image-set-function@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/postcss-image-set-function/-/postcss-image-set-function-3.0.1.tgz#28920a2f29945bed4c3198d7df6496d410d3f288" + integrity sha512-oPTcFFip5LZy8Y/whto91L9xdRHCWEMs3e1MdJxhgt4jy2WYXfhkng59fH5qLXSCPN8k4n94p1Czrfe5IOkKUw== + dependencies: + postcss "^7.0.2" + postcss-values-parser "^2.0.0" + +postcss-import@^13.0.0: + version "13.0.0" + resolved "https://registry.yarnpkg.com/postcss-import/-/postcss-import-13.0.0.tgz#d6960cd9e3de5464743b04dd8cd9d870662f8b8c" + integrity sha512-LPUbm3ytpYopwQQjqgUH4S3EM/Gb9QsaSPP/5vnoi+oKVy3/mIk2sc0Paqw7RL57GpScm9MdIMUypw2znWiBpg== + dependencies: + postcss-value-parser "^4.0.0" + read-cache "^1.0.0" + resolve "^1.1.7" + +postcss-initial@^3.0.0: + version "3.0.2" + resolved "https://registry.yarnpkg.com/postcss-initial/-/postcss-initial-3.0.2.tgz#f018563694b3c16ae8eaabe3c585ac6319637b2d" + integrity sha512-ugA2wKonC0xeNHgirR4D3VWHs2JcU08WAi1KFLVcnb7IN89phID6Qtg2RIctWbnvp1TM2BOmDtX8GGLCKdR8YA== + dependencies: + lodash.template "^4.5.0" + postcss "^7.0.2" + +postcss-inline-svg@^4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/postcss-inline-svg/-/postcss-inline-svg-4.1.0.tgz#54e9199632242cc181af5b55f0f0691ad1020f95" + integrity sha512-0pYBJyoQ9/sJViYRc1cNOOTM7DYh0/rmASB0TBeRmWkG8YFK2tmgdkfjHkbRma1iFtBFKFHZFsHwRTDZTMKzSQ== + dependencies: + css-select "^2.0.2" + dom-serializer "^0.1.1" + htmlparser2 "^3.10.1" + postcss "^7.0.17" + postcss-value-parser "^4.0.0" + +postcss-js@^3.0.3: + version "3.0.3" + resolved "https://registry.yarnpkg.com/postcss-js/-/postcss-js-3.0.3.tgz#2f0bd370a2e8599d45439f6970403b5873abda33" + integrity sha512-gWnoWQXKFw65Hk/mi2+WTQTHdPD5UJdDXZmX073EY/B3BWnYjO4F4t0VneTCnCGQ5E5GsCdMkzPaTXwl3r5dJw== + dependencies: + camelcase-css "^2.0.1" + postcss "^8.1.6" + +postcss-lab-function@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/postcss-lab-function/-/postcss-lab-function-2.0.1.tgz#bb51a6856cd12289ab4ae20db1e3821ef13d7d2e" + integrity sha512-whLy1IeZKY+3fYdqQFuDBf8Auw+qFuVnChWjmxm/UhHWqNHZx+B99EwxTvGYmUBqe3Fjxs4L1BoZTJmPu6usVg== + dependencies: + "@csstools/convert-colors" "^1.4.0" + postcss "^7.0.2" + postcss-values-parser "^2.0.0" + +postcss-less@^3.1.4: + version "3.1.4" + resolved "https://registry.yarnpkg.com/postcss-less/-/postcss-less-3.1.4.tgz#369f58642b5928ef898ffbc1a6e93c958304c5ad" + integrity sha512-7TvleQWNM2QLcHqvudt3VYjULVB49uiW6XzEUFmvwHzvsOEF5MwBrIXZDJQvJNFGjJQTzSzZnDoCJ8h/ljyGXA== + dependencies: + postcss "^7.0.14" + +postcss-loader@^4.0.4: + version "4.2.0" + resolved "https://registry.yarnpkg.com/postcss-loader/-/postcss-loader-4.2.0.tgz#f6993ea3e0f46600fb3ee49bbd010448123a7db4" + integrity sha512-mqgScxHqbiz1yxbnNcPdKYo/6aVt+XExURmEbQlviFVWogDbM4AJ0A/B+ZBpYsJrTRxKw7HyRazg9x0Q9SWwLA== + dependencies: + cosmiconfig "^7.0.0" + klona "^2.0.4" + loader-utils "^2.0.0" + schema-utils "^3.0.0" + semver "^7.3.4" + +postcss-logical@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/postcss-logical/-/postcss-logical-3.0.0.tgz#2495d0f8b82e9f262725f75f9401b34e7b45d5b5" + integrity sha512-1SUKdJc2vuMOmeItqGuNaC+N8MzBWFWEkAnRnLpFYj1tGGa7NqyVBujfRtgNa2gXR+6RkGUiB2O5Vmh7E2RmiA== + dependencies: + postcss "^7.0.2" + +postcss-media-minmax@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/postcss-media-minmax/-/postcss-media-minmax-4.0.0.tgz#b75bb6cbc217c8ac49433e12f22048814a4f5ed5" + integrity sha512-fo9moya6qyxsjbFAYl97qKO9gyre3qvbMnkOZeZwlsW6XYFsvs2DMGDlchVLfAd8LHPZDxivu/+qW2SMQeTHBw== + dependencies: + postcss "^7.0.2" + +postcss-media-query-parser@^0.2.3: + version "0.2.3" + resolved "https://registry.yarnpkg.com/postcss-media-query-parser/-/postcss-media-query-parser-0.2.3.tgz#27b39c6f4d94f81b1a73b8f76351c609e5cef244" + integrity sha1-J7Ocb02U+Bsac7j3Y1HGCeXO8kQ= + +postcss-merge-longhand@^4.0.11: + version "4.0.11" + resolved "https://registry.yarnpkg.com/postcss-merge-longhand/-/postcss-merge-longhand-4.0.11.tgz#62f49a13e4a0ee04e7b98f42bb16062ca2549e24" + integrity sha512-alx/zmoeXvJjp7L4mxEMjh8lxVlDFX1gqWHzaaQewwMZiVhLo42TEClKaeHbRf6J7j82ZOdTJ808RtN0ZOZwvw== + dependencies: + css-color-names "0.0.4" + postcss "^7.0.0" + postcss-value-parser "^3.0.0" + stylehacks "^4.0.0" + +postcss-merge-rules@^4.0.3: + version "4.0.3" + resolved "https://registry.yarnpkg.com/postcss-merge-rules/-/postcss-merge-rules-4.0.3.tgz#362bea4ff5a1f98e4075a713c6cb25aefef9a650" + integrity sha512-U7e3r1SbvYzO0Jr3UT/zKBVgYYyhAz0aitvGIYOYK5CPmkNih+WDSsS5tvPrJ8YMQYlEMvsZIiqmn7HdFUaeEQ== + dependencies: + browserslist "^4.0.0" + caniuse-api "^3.0.0" + cssnano-util-same-parent "^4.0.0" + postcss "^7.0.0" + postcss-selector-parser "^3.0.0" + vendors "^1.0.0" + +postcss-minify-font-values@^4.0.2: + version "4.0.2" + resolved "https://registry.yarnpkg.com/postcss-minify-font-values/-/postcss-minify-font-values-4.0.2.tgz#cd4c344cce474343fac5d82206ab2cbcb8afd5a6" + integrity sha512-j85oO6OnRU9zPf04+PZv1LYIYOprWm6IA6zkXkrJXyRveDEuQggG6tvoy8ir8ZwjLxLuGfNkCZEQG7zan+Hbtg== + dependencies: + postcss "^7.0.0" + postcss-value-parser "^3.0.0" + +postcss-minify-gradients@^4.0.2: + version "4.0.2" + resolved "https://registry.yarnpkg.com/postcss-minify-gradients/-/postcss-minify-gradients-4.0.2.tgz#93b29c2ff5099c535eecda56c4aa6e665a663471" + integrity sha512-qKPfwlONdcf/AndP1U8SJ/uzIJtowHlMaSioKzebAXSG4iJthlWC9iSWznQcX4f66gIWX44RSA841HTHj3wK+Q== + dependencies: + cssnano-util-get-arguments "^4.0.0" + is-color-stop "^1.0.0" + postcss "^7.0.0" + postcss-value-parser "^3.0.0" + +postcss-minify-params@^4.0.2: + version "4.0.2" + resolved "https://registry.yarnpkg.com/postcss-minify-params/-/postcss-minify-params-4.0.2.tgz#6b9cef030c11e35261f95f618c90036d680db874" + integrity sha512-G7eWyzEx0xL4/wiBBJxJOz48zAKV2WG3iZOqVhPet/9geefm/Px5uo1fzlHu+DOjT+m0Mmiz3jkQzVHe6wxAWg== + dependencies: + alphanum-sort "^1.0.0" + browserslist "^4.0.0" + cssnano-util-get-arguments "^4.0.0" + postcss "^7.0.0" + postcss-value-parser "^3.0.0" + uniqs "^2.0.0" + +postcss-minify-selectors@^4.0.2: + version "4.0.2" + resolved "https://registry.yarnpkg.com/postcss-minify-selectors/-/postcss-minify-selectors-4.0.2.tgz#e2e5eb40bfee500d0cd9243500f5f8ea4262fbd8" + integrity sha512-D5S1iViljXBj9kflQo4YutWnJmwm8VvIsU1GeXJGiG9j8CIg9zs4voPMdQDUmIxetUOh60VilsNzCiAFTOqu3g== + dependencies: + alphanum-sort "^1.0.0" + has "^1.0.0" + postcss "^7.0.0" + postcss-selector-parser "^3.0.0" + +postcss-mixins@^7.0.1: + version "7.0.2" + resolved "https://registry.yarnpkg.com/postcss-mixins/-/postcss-mixins-7.0.2.tgz#9c5b9db70aa74095402062b3cf38431259b58971" + integrity sha512-er22AZ/2C1P+jZOL4ZExVEz0XuPWJWWy7SySbb2HWFmAgXG8F4qaOmMaRTdLfSK1AbgWxRT+KQ8GQ2i5kOn1aw== + dependencies: + globby "^11.0.1" + postcss-js "^3.0.3" + postcss-simple-vars "^6.0.1" + sugarss "^3.0.3" + +postcss-modules-extract-imports@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/postcss-modules-extract-imports/-/postcss-modules-extract-imports-2.0.0.tgz#818719a1ae1da325f9832446b01136eeb493cd7e" + integrity sha512-LaYLDNS4SG8Q5WAWqIJgdHPJrDDr/Lv775rMBFUbgjTz6j34lUznACHcdRWroPvXANP2Vj7yNK57vp9eFqzLWQ== + dependencies: + postcss "^7.0.5" + +postcss-modules-extract-imports@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/postcss-modules-extract-imports/-/postcss-modules-extract-imports-3.0.0.tgz#cda1f047c0ae80c97dbe28c3e76a43b88025741d" + integrity sha512-bdHleFnP3kZ4NYDhuGlVK+CMrQ/pqUm8bx/oGL93K6gVwiclvX5x0n76fYMKuIGKzlABOy13zsvqjb0f92TEXw== + +postcss-modules-local-by-default@^3.0.2: + version "3.0.3" + resolved "https://registry.yarnpkg.com/postcss-modules-local-by-default/-/postcss-modules-local-by-default-3.0.3.tgz#bb14e0cc78279d504dbdcbfd7e0ca28993ffbbb0" + integrity sha512-e3xDq+LotiGesympRlKNgaJ0PCzoUIdpH0dj47iWAui/kyTgh3CiAr1qP54uodmJhl6p9rN6BoNcdEDVJx9RDw== + dependencies: + icss-utils "^4.1.1" + postcss "^7.0.32" + postcss-selector-parser "^6.0.2" + postcss-value-parser "^4.1.0" + +postcss-modules-local-by-default@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/postcss-modules-local-by-default/-/postcss-modules-local-by-default-4.0.0.tgz#ebbb54fae1598eecfdf691a02b3ff3b390a5a51c" + integrity sha512-sT7ihtmGSF9yhm6ggikHdV0hlziDTX7oFoXtuVWeDd3hHObNkcHRo9V3yg7vCAY7cONyxJC/XXCmmiHHcvX7bQ== + dependencies: + icss-utils "^5.0.0" + postcss-selector-parser "^6.0.2" + postcss-value-parser "^4.1.0" + +postcss-modules-scope@^2.2.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/postcss-modules-scope/-/postcss-modules-scope-2.2.0.tgz#385cae013cc7743f5a7d7602d1073a89eaae62ee" + integrity sha512-YyEgsTMRpNd+HmyC7H/mh3y+MeFWevy7V1evVhJWewmMbjDHIbZbOXICC2y+m1xI1UVfIT1HMW/O04Hxyu9oXQ== + dependencies: + postcss "^7.0.6" + postcss-selector-parser "^6.0.0" + +postcss-modules-scope@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/postcss-modules-scope/-/postcss-modules-scope-3.0.0.tgz#9ef3151456d3bbfa120ca44898dfca6f2fa01f06" + integrity sha512-hncihwFA2yPath8oZ15PZqvWGkWf+XUfQgUGamS4LqoP1anQLOsOJw0vr7J7IwLpoY9fatA2qiGUGmuZL0Iqlg== + dependencies: + postcss-selector-parser "^6.0.4" + +postcss-modules-values@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/postcss-modules-values/-/postcss-modules-values-3.0.0.tgz#5b5000d6ebae29b4255301b4a3a54574423e7f10" + integrity sha512-1//E5jCBrZ9DmRX+zCtmQtRSV6PV42Ix7Bzj9GbwJceduuf7IqP8MgeTXuRDHOWj2m0VzZD5+roFWDuU8RQjcg== + dependencies: + icss-utils "^4.0.0" + postcss "^7.0.6" + +postcss-modules-values@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/postcss-modules-values/-/postcss-modules-values-4.0.0.tgz#d7c5e7e68c3bb3c9b27cbf48ca0bb3ffb4602c9c" + integrity sha512-RDxHkAiEGI78gS2ofyvCsu7iycRv7oqw5xMWn9iMoR0N/7mf9D50ecQqUo5BZ9Zh2vH4bCUR/ktCqbB9m8vJjQ== + dependencies: + icss-utils "^5.0.0" + +postcss-modules@^3.2.2: + version "3.2.2" + resolved "https://registry.yarnpkg.com/postcss-modules/-/postcss-modules-3.2.2.tgz#ee390de0f9f18e761e1778dfb9be26685c02c51f" + integrity sha512-JQ8IAqHELxC0N6tyCg2UF40pACY5oiL6UpiqqcIFRWqgDYO8B0jnxzoQ0EOpPrWXvcpu6BSbQU/3vSiq7w8Nhw== + dependencies: + generic-names "^2.0.1" + icss-replace-symbols "^1.1.0" + lodash.camelcase "^4.3.0" + postcss "^7.0.32" + postcss-modules-extract-imports "^2.0.0" + postcss-modules-local-by-default "^3.0.2" + postcss-modules-scope "^2.2.0" + postcss-modules-values "^3.0.0" + string-hash "^1.1.1" + +postcss-nested@^5.0.1: + version "5.0.3" + resolved "https://registry.yarnpkg.com/postcss-nested/-/postcss-nested-5.0.3.tgz#2f46d77a06fc98d9c22344fd097396f5431386db" + integrity sha512-R2LHPw+u5hFfDgJG748KpGbJyTv7Yr33/2tIMWxquYuHTd9EXu27PYnKi7BxMXLtzKC0a0WVsqHtd7qIluQu/g== + dependencies: + postcss-selector-parser "^6.0.4" + +postcss-nesting@^7.0.0: + version "7.0.1" + resolved "https://registry.yarnpkg.com/postcss-nesting/-/postcss-nesting-7.0.1.tgz#b50ad7b7f0173e5b5e3880c3501344703e04c052" + integrity sha512-FrorPb0H3nuVq0Sff7W2rnc3SmIcruVC6YwpcS+k687VxyxO33iE1amna7wHuRVzM8vfiYofXSBHNAZ3QhLvYg== + dependencies: + postcss "^7.0.2" + +postcss-normalize-charset@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/postcss-normalize-charset/-/postcss-normalize-charset-4.0.1.tgz#8b35add3aee83a136b0471e0d59be58a50285dd4" + integrity sha512-gMXCrrlWh6G27U0hF3vNvR3w8I1s2wOBILvA87iNXaPvSNo5uZAMYsZG7XjCUf1eVxuPfyL4TJ7++SGZLc9A3g== + dependencies: + postcss "^7.0.0" + +postcss-normalize-display-values@^4.0.2: + version "4.0.2" + resolved "https://registry.yarnpkg.com/postcss-normalize-display-values/-/postcss-normalize-display-values-4.0.2.tgz#0dbe04a4ce9063d4667ed2be476bb830c825935a" + integrity sha512-3F2jcsaMW7+VtRMAqf/3m4cPFhPD3EFRgNs18u+k3lTJJlVe7d0YPO+bnwqo2xg8YiRpDXJI2u8A0wqJxMsQuQ== + dependencies: + cssnano-util-get-match "^4.0.0" + postcss "^7.0.0" + postcss-value-parser "^3.0.0" + +postcss-normalize-positions@^4.0.2: + version "4.0.2" + resolved "https://registry.yarnpkg.com/postcss-normalize-positions/-/postcss-normalize-positions-4.0.2.tgz#05f757f84f260437378368a91f8932d4b102917f" + integrity sha512-Dlf3/9AxpxE+NF1fJxYDeggi5WwV35MXGFnnoccP/9qDtFrTArZ0D0R+iKcg5WsUd8nUYMIl8yXDCtcrT8JrdA== + dependencies: + cssnano-util-get-arguments "^4.0.0" + has "^1.0.0" + postcss "^7.0.0" + postcss-value-parser "^3.0.0" + +postcss-normalize-repeat-style@^4.0.2: + version "4.0.2" + resolved "https://registry.yarnpkg.com/postcss-normalize-repeat-style/-/postcss-normalize-repeat-style-4.0.2.tgz#c4ebbc289f3991a028d44751cbdd11918b17910c" + integrity sha512-qvigdYYMpSuoFs3Is/f5nHdRLJN/ITA7huIoCyqqENJe9PvPmLhNLMu7QTjPdtnVf6OcYYO5SHonx4+fbJE1+Q== + dependencies: + cssnano-util-get-arguments "^4.0.0" + cssnano-util-get-match "^4.0.0" + postcss "^7.0.0" + postcss-value-parser "^3.0.0" + +postcss-normalize-string@^4.0.2: + version "4.0.2" + resolved "https://registry.yarnpkg.com/postcss-normalize-string/-/postcss-normalize-string-4.0.2.tgz#cd44c40ab07a0c7a36dc5e99aace1eca4ec2690c" + integrity sha512-RrERod97Dnwqq49WNz8qo66ps0swYZDSb6rM57kN2J+aoyEAJfZ6bMx0sx/F9TIEX0xthPGCmeyiam/jXif0eA== + dependencies: + has "^1.0.0" + postcss "^7.0.0" + postcss-value-parser "^3.0.0" + +postcss-normalize-timing-functions@^4.0.2: + version "4.0.2" + resolved "https://registry.yarnpkg.com/postcss-normalize-timing-functions/-/postcss-normalize-timing-functions-4.0.2.tgz#8e009ca2a3949cdaf8ad23e6b6ab99cb5e7d28d9" + integrity sha512-acwJY95edP762e++00Ehq9L4sZCEcOPyaHwoaFOhIwWCDfik6YvqsYNxckee65JHLKzuNSSmAdxwD2Cud1Z54A== + dependencies: + cssnano-util-get-match "^4.0.0" + postcss "^7.0.0" + postcss-value-parser "^3.0.0" + +postcss-normalize-unicode@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/postcss-normalize-unicode/-/postcss-normalize-unicode-4.0.1.tgz#841bd48fdcf3019ad4baa7493a3d363b52ae1cfb" + integrity sha512-od18Uq2wCYn+vZ/qCOeutvHjB5jm57ToxRaMeNuf0nWVHaP9Hua56QyMF6fs/4FSUnVIw0CBPsU0K4LnBPwYwg== + dependencies: + browserslist "^4.0.0" + postcss "^7.0.0" + postcss-value-parser "^3.0.0" + +postcss-normalize-url@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/postcss-normalize-url/-/postcss-normalize-url-4.0.1.tgz#10e437f86bc7c7e58f7b9652ed878daaa95faae1" + integrity sha512-p5oVaF4+IHwu7VpMan/SSpmpYxcJMtkGppYf0VbdH5B6hN8YNmVyJLuY9FmLQTzY3fag5ESUUHDqM+heid0UVA== + dependencies: + is-absolute-url "^2.0.0" + normalize-url "^3.0.0" + postcss "^7.0.0" + postcss-value-parser "^3.0.0" + +postcss-normalize-whitespace@^4.0.2: + version "4.0.2" + resolved "https://registry.yarnpkg.com/postcss-normalize-whitespace/-/postcss-normalize-whitespace-4.0.2.tgz#bf1d4070fe4fcea87d1348e825d8cc0c5faa7d82" + integrity sha512-tO8QIgrsI3p95r8fyqKV+ufKlSHh9hMJqACqbv2XknufqEDhDvbguXGBBqxw9nsQoXWf0qOqppziKJKHMD4GtA== + dependencies: + postcss "^7.0.0" + postcss-value-parser "^3.0.0" + +postcss-ordered-values@^4.1.2: + version "4.1.2" + resolved "https://registry.yarnpkg.com/postcss-ordered-values/-/postcss-ordered-values-4.1.2.tgz#0cf75c820ec7d5c4d280189559e0b571ebac0eee" + integrity sha512-2fCObh5UanxvSxeXrtLtlwVThBvHn6MQcu4ksNT2tsaV2Fg76R2CV98W7wNSlX+5/pFwEyaDwKLLoEV7uRybAw== + dependencies: + cssnano-util-get-arguments "^4.0.0" + postcss "^7.0.0" + postcss-value-parser "^3.0.0" + +postcss-overflow-shorthand@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/postcss-overflow-shorthand/-/postcss-overflow-shorthand-2.0.0.tgz#31ecf350e9c6f6ddc250a78f0c3e111f32dd4c30" + integrity sha512-aK0fHc9CBNx8jbzMYhshZcEv8LtYnBIRYQD5i7w/K/wS9c2+0NSR6B3OVMu5y0hBHYLcMGjfU+dmWYNKH0I85g== + dependencies: + postcss "^7.0.2" + +postcss-page-break@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/postcss-page-break/-/postcss-page-break-2.0.0.tgz#add52d0e0a528cabe6afee8b46e2abb277df46bf" + integrity sha512-tkpTSrLpfLfD9HvgOlJuigLuk39wVTbbd8RKcy8/ugV2bNBUW3xU+AIqyxhDrQr1VUj1RmyJrBn1YWrqUm9zAQ== + dependencies: + postcss "^7.0.2" + +postcss-place@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/postcss-place/-/postcss-place-4.0.1.tgz#e9f39d33d2dc584e46ee1db45adb77ca9d1dcc62" + integrity sha512-Zb6byCSLkgRKLODj/5mQugyuj9bvAAw9LqJJjgwz5cYryGeXfFZfSXoP1UfveccFmeq0b/2xxwcTEVScnqGxBg== + dependencies: + postcss "^7.0.2" + postcss-values-parser "^2.0.0" + +postcss-preset-env@^6.7.0: + version "6.7.0" + resolved "https://registry.yarnpkg.com/postcss-preset-env/-/postcss-preset-env-6.7.0.tgz#c34ddacf8f902383b35ad1e030f178f4cdf118a5" + integrity sha512-eU4/K5xzSFwUFJ8hTdTQzo2RBLbDVt83QZrAvI07TULOkmyQlnYlpwep+2yIK+K+0KlZO4BvFcleOCCcUtwchg== + dependencies: + autoprefixer "^9.6.1" + browserslist "^4.6.4" + caniuse-lite "^1.0.30000981" + css-blank-pseudo "^0.1.4" + css-has-pseudo "^0.10.0" + css-prefers-color-scheme "^3.1.1" + cssdb "^4.4.0" + postcss "^7.0.17" + postcss-attribute-case-insensitive "^4.0.1" + postcss-color-functional-notation "^2.0.1" + postcss-color-gray "^5.0.0" + postcss-color-hex-alpha "^5.0.3" + postcss-color-mod-function "^3.0.3" + postcss-color-rebeccapurple "^4.0.1" + postcss-custom-media "^7.0.8" + postcss-custom-properties "^8.0.11" + postcss-custom-selectors "^5.1.2" + postcss-dir-pseudo-class "^5.0.0" + postcss-double-position-gradients "^1.0.0" + postcss-env-function "^2.0.2" + postcss-focus-visible "^4.0.0" + postcss-focus-within "^3.0.0" + postcss-font-variant "^4.0.0" + postcss-gap-properties "^2.0.0" + postcss-image-set-function "^3.0.1" + postcss-initial "^3.0.0" + postcss-lab-function "^2.0.1" + postcss-logical "^3.0.0" + postcss-media-minmax "^4.0.0" + postcss-nesting "^7.0.0" + postcss-overflow-shorthand "^2.0.0" + postcss-page-break "^2.0.0" + postcss-place "^4.0.1" + postcss-pseudo-class-any-link "^6.0.0" + postcss-replace-overflow-wrap "^3.0.0" + postcss-selector-matches "^4.0.0" + postcss-selector-not "^4.0.0" + +postcss-pseudo-class-any-link@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/postcss-pseudo-class-any-link/-/postcss-pseudo-class-any-link-6.0.0.tgz#2ed3eed393b3702879dec4a87032b210daeb04d1" + integrity sha512-lgXW9sYJdLqtmw23otOzrtbDXofUdfYzNm4PIpNE322/swES3VU9XlXHeJS46zT2onFO7V1QFdD4Q9LiZj8mew== + dependencies: + postcss "^7.0.2" + postcss-selector-parser "^5.0.0-rc.3" + +postcss-reduce-initial@^4.0.3: + version "4.0.3" + resolved "https://registry.yarnpkg.com/postcss-reduce-initial/-/postcss-reduce-initial-4.0.3.tgz#7fd42ebea5e9c814609639e2c2e84ae270ba48df" + integrity sha512-gKWmR5aUulSjbzOfD9AlJiHCGH6AEVLaM0AV+aSioxUDd16qXP1PCh8d1/BGVvpdWn8k/HiK7n6TjeoXN1F7DA== + dependencies: + browserslist "^4.0.0" + caniuse-api "^3.0.0" + has "^1.0.0" + postcss "^7.0.0" + +postcss-reduce-transforms@^4.0.2: + version "4.0.2" + resolved "https://registry.yarnpkg.com/postcss-reduce-transforms/-/postcss-reduce-transforms-4.0.2.tgz#17efa405eacc6e07be3414a5ca2d1074681d4e29" + integrity sha512-EEVig1Q2QJ4ELpJXMZR8Vt5DQx8/mo+dGWSR7vWXqcob2gQLyQGsionYcGKATXvQzMPn6DSN1vTN7yFximdIAg== + dependencies: + cssnano-util-get-match "^4.0.0" + has "^1.0.0" + postcss "^7.0.0" + postcss-value-parser "^3.0.0" + +postcss-replace-overflow-wrap@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/postcss-replace-overflow-wrap/-/postcss-replace-overflow-wrap-3.0.0.tgz#61b360ffdaedca84c7c918d2b0f0d0ea559ab01c" + integrity sha512-2T5hcEHArDT6X9+9dVSPQdo7QHzG4XKclFT8rU5TzJPDN7RIRTbO9c4drUISOVemLj03aezStHCR2AIcr8XLpw== + dependencies: + postcss "^7.0.2" + +postcss-reporter@^7.0.1: + version "7.0.2" + resolved "https://registry.yarnpkg.com/postcss-reporter/-/postcss-reporter-7.0.2.tgz#03e9e7381c1afe40646f9c22e7aeeb860e051065" + integrity sha512-JyQ96NTQQsso42y6L1H1RqHfWH1C3Jr0pt91mVv5IdYddZAE9DUZxuferNgk6q0o6vBVOrfVJb10X1FgDzjmDw== + dependencies: + colorette "^1.2.1" + lodash.difference "^4.5.0" + lodash.forown "^4.4.0" + lodash.get "^4.4.2" + lodash.groupby "^4.6.0" + lodash.sortby "^4.7.0" + +postcss-resolve-nested-selector@^0.1.1: + version "0.1.1" + resolved "https://registry.yarnpkg.com/postcss-resolve-nested-selector/-/postcss-resolve-nested-selector-0.1.1.tgz#29ccbc7c37dedfac304e9fff0bf1596b3f6a0e4e" + integrity sha1-Kcy8fDfe36wwTp//C/FZaz9qDk4= + +postcss-safe-parser@^4.0.2: + version "4.0.2" + resolved "https://registry.yarnpkg.com/postcss-safe-parser/-/postcss-safe-parser-4.0.2.tgz#a6d4e48f0f37d9f7c11b2a581bf00f8ba4870b96" + integrity sha512-Uw6ekxSWNLCPesSv/cmqf2bY/77z11O7jZGPax3ycZMFU/oi2DMH9i89AdHc1tRwFg/arFoEwX0IS3LCUxJh1g== + dependencies: + postcss "^7.0.26" + +postcss-sass@^0.4.4: + version "0.4.4" + resolved "https://registry.yarnpkg.com/postcss-sass/-/postcss-sass-0.4.4.tgz#91f0f3447b45ce373227a98b61f8d8f0785285a3" + integrity sha512-BYxnVYx4mQooOhr+zer0qWbSPYnarAy8ZT7hAQtbxtgVf8gy+LSLT/hHGe35h14/pZDTw1DsxdbrwxBN++H+fg== + dependencies: + gonzales-pe "^4.3.0" + postcss "^7.0.21" + +postcss-scss@^2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/postcss-scss/-/postcss-scss-2.1.1.tgz#ec3a75fa29a55e016b90bf3269026c53c1d2b383" + integrity sha512-jQmGnj0hSGLd9RscFw9LyuSVAa5Bl1/KBPqG1NQw9w8ND55nY4ZEsdlVuYJvLPpV+y0nwTV5v/4rHPzZRihQbA== + dependencies: + postcss "^7.0.6" + +postcss-selector-matches@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/postcss-selector-matches/-/postcss-selector-matches-4.0.0.tgz#71c8248f917ba2cc93037c9637ee09c64436fcff" + integrity sha512-LgsHwQR/EsRYSqlwdGzeaPKVT0Ml7LAT6E75T8W8xLJY62CE4S/l03BWIt3jT8Taq22kXP08s2SfTSzaraoPww== + dependencies: + balanced-match "^1.0.0" + postcss "^7.0.2" + +postcss-selector-not@^4.0.0: + version "4.0.1" + resolved "https://registry.yarnpkg.com/postcss-selector-not/-/postcss-selector-not-4.0.1.tgz#263016eef1cf219e0ade9a913780fc1f48204cbf" + integrity sha512-YolvBgInEK5/79C+bdFMyzqTg6pkYqDbzZIST/PDMqa/o3qtXenD05apBG2jLgT0/BQ77d4U2UK12jWpilqMAQ== + dependencies: + balanced-match "^1.0.0" + postcss "^7.0.2" + +postcss-selector-parser@^3.0.0: + version "3.1.2" + resolved "https://registry.yarnpkg.com/postcss-selector-parser/-/postcss-selector-parser-3.1.2.tgz#b310f5c4c0fdaf76f94902bbaa30db6aa84f5270" + integrity sha512-h7fJ/5uWuRVyOtkO45pnt1Ih40CEleeyCHzipqAZO2e5H20g25Y48uYnFUiShvY4rZWNJ/Bib/KVPmanaCtOhA== + dependencies: + dot-prop "^5.2.0" + indexes-of "^1.0.1" + uniq "^1.0.1" + +postcss-selector-parser@^5.0.0-rc.3, postcss-selector-parser@^5.0.0-rc.4: + version "5.0.0" + resolved "https://registry.yarnpkg.com/postcss-selector-parser/-/postcss-selector-parser-5.0.0.tgz#249044356697b33b64f1a8f7c80922dddee7195c" + integrity sha512-w+zLE5Jhg6Liz8+rQOWEAwtwkyqpfnmsinXjXg6cY7YIONZZtgvE0v2O0uhQBs0peNomOJwWRKt6JBfTdTd3OQ== + dependencies: + cssesc "^2.0.0" + indexes-of "^1.0.1" + uniq "^1.0.1" + +postcss-selector-parser@^6.0.0, postcss-selector-parser@^6.0.2, postcss-selector-parser@^6.0.4: + version "6.0.4" + resolved "https://registry.yarnpkg.com/postcss-selector-parser/-/postcss-selector-parser-6.0.4.tgz#56075a1380a04604c38b063ea7767a129af5c2b3" + integrity sha512-gjMeXBempyInaBqpp8gODmwZ52WaYsVOsfr4L4lDQ7n3ncD6mEyySiDtgzCT+NYC0mmeOLvtsF8iaEf0YT6dBw== + dependencies: + cssesc "^3.0.0" + indexes-of "^1.0.1" + uniq "^1.0.1" + util-deprecate "^1.0.2" + +postcss-simple-vars@^6.0.1: + version "6.0.2" + resolved "https://registry.yarnpkg.com/postcss-simple-vars/-/postcss-simple-vars-6.0.2.tgz#cc0cc7745fb53c83245df99aaf4452475b454eda" + integrity sha512-tABK0OVtCa88TgJjgosa/1aLgiF4hTJBJxUjVE5idTGA0597OVdzWZvbySAF+HKo/sZidxSSpnWhUycDrxO8LA== + +postcss-svgo@^4.0.2: + version "4.0.2" + resolved "https://registry.yarnpkg.com/postcss-svgo/-/postcss-svgo-4.0.2.tgz#17b997bc711b333bab143aaed3b8d3d6e3d38258" + integrity sha512-C6wyjo3VwFm0QgBy+Fu7gCYOkCmgmClghO+pjcxvrcBKtiKt0uCF+hvbMO1fyv5BMImRK90SMb+dwUnfbGd+jw== + dependencies: + is-svg "^3.0.0" + postcss "^7.0.0" + postcss-value-parser "^3.0.0" + svgo "^1.0.0" + +postcss-syntax@^0.36.2: + version "0.36.2" + resolved "https://registry.yarnpkg.com/postcss-syntax/-/postcss-syntax-0.36.2.tgz#f08578c7d95834574e5593a82dfbfa8afae3b51c" + integrity sha512-nBRg/i7E3SOHWxF3PpF5WnJM/jQ1YpY9000OaVXlAQj6Zp/kIqJxEDWIZ67tAd7NLuk7zqN4yqe9nc0oNAOs1w== + +postcss-unique-selectors@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/postcss-unique-selectors/-/postcss-unique-selectors-4.0.1.tgz#9446911f3289bfd64c6d680f073c03b1f9ee4bac" + integrity sha512-+JanVaryLo9QwZjKrmJgkI4Fn8SBgRO6WXQBJi7KiAVPlmxikB5Jzc4EvXMT2H0/m0RjrVVm9rGNhZddm/8Spg== + dependencies: + alphanum-sort "^1.0.0" + postcss "^7.0.0" + uniqs "^2.0.0" + +postcss-value-parser@^3.0.0, postcss-value-parser@^3.3.0: + version "3.3.1" + resolved "https://registry.yarnpkg.com/postcss-value-parser/-/postcss-value-parser-3.3.1.tgz#9ff822547e2893213cf1c30efa51ac5fd1ba8281" + integrity sha512-pISE66AbVkp4fDQ7VHBwRNXzAAKJjw4Vw7nWI/+Q3vuly7SNfgYXvm6i5IgFylHGK5sP/xHAbB7N49OS4gWNyQ== + +postcss-value-parser@^4.0.0, postcss-value-parser@^4.0.2, postcss-value-parser@^4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/postcss-value-parser/-/postcss-value-parser-4.1.0.tgz#443f6a20ced6481a2bda4fa8532a6e55d789a2cb" + integrity sha512-97DXOFbQJhk71ne5/Mt6cOu6yxsSfM0QGQyl0L25Gca4yGWEGJaig7l7gbCX623VqTBNGLRLaVUCnNkcedlRSQ== + +postcss-values-parser@^2.0.0, postcss-values-parser@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/postcss-values-parser/-/postcss-values-parser-2.0.1.tgz#da8b472d901da1e205b47bdc98637b9e9e550e5f" + integrity sha512-2tLuBsA6P4rYTNKCXYG/71C7j1pU6pK503suYOmn4xYrQIzW+opD+7FAFNuGSdZC/3Qfy334QbeMu7MEb8gOxg== + dependencies: + flatten "^1.0.2" + indexes-of "^1.0.1" + uniq "^1.0.1" + +postcss-variables@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/postcss-variables/-/postcss-variables-1.1.1.tgz#f86856acd0583b0c493767f22f7258509d37f9e0" + integrity sha512-a9b2ZXoy60xl28m+jedXYvbXLdYSLPXOqvgkLUHhOUbhIRlVoSHRGhGtpMLkcgVc05lu3JUBEypLVcTYNcltMw== + dependencies: + postcss "^6.0.9" + +postcss@^6.0.8, postcss@^6.0.9: + version "6.0.23" + resolved "https://registry.yarnpkg.com/postcss/-/postcss-6.0.23.tgz#61c82cc328ac60e677645f979054eb98bc0e3324" + integrity sha512-soOk1h6J3VMTZtVeVpv15/Hpdl2cBLX3CAw4TAbkpTJiNPk9YP/zWcD1ND+xEtvyuuvKzbxliTOIyvkSeSJ6ag== + dependencies: + chalk "^2.4.1" + source-map "^0.6.1" + supports-color "^5.4.0" + +postcss@^7.0.0, postcss@^7.0.1, postcss@^7.0.14, postcss@^7.0.17, postcss@^7.0.2, postcss@^7.0.21, postcss@^7.0.26, postcss@^7.0.27, postcss@^7.0.32, postcss@^7.0.35, postcss@^7.0.5, postcss@^7.0.6: + version "7.0.35" + resolved "https://registry.yarnpkg.com/postcss/-/postcss-7.0.35.tgz#d2be00b998f7f211d8a276974079f2e92b970e24" + integrity sha512-3QT8bBJeX/S5zKTTjTCIjRF3If4avAT6kqxcASlTWEtAFCb9NH0OUxNDfgZSWdP5fJnBYCMEWkIFfWeugjzYMg== + dependencies: + chalk "^2.4.2" + source-map "^0.6.1" + supports-color "^6.1.0" + +postcss@^8.1.2, postcss@^8.1.4, postcss@^8.1.6: + version "8.2.4" + resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.2.4.tgz#20a98a39cf303d15129c2865a9ec37eda0031d04" + integrity sha512-kRFftRoExRVXZlwUuay9iC824qmXPcQQVzAjbCCgjpXnkdMCJYBu2gTwAaFBzv8ewND6O8xFb3aELmEkh9zTzg== + dependencies: + colorette "^1.2.1" + nanoid "^3.1.20" + source-map "^0.6.1" + +prelude-ls@^1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.2.1.tgz#debc6489d7a6e6b0e7611888cec880337d316396" + integrity sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g== + +pretty-error@^2.1.1: + version "2.1.2" + resolved "https://registry.yarnpkg.com/pretty-error/-/pretty-error-2.1.2.tgz#be89f82d81b1c86ec8fdfbc385045882727f93b6" + integrity sha512-EY5oDzmsX5wvuynAByrmY0P0hcp+QpnAKbJng2A2MPjVKXCxrDSUkzghVJ4ZGPIv+JC4gX8fPUWscC0RtjsWGw== + dependencies: + lodash "^4.17.20" + renderkid "^2.0.4" + +process-nextick-args@~2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/process-nextick-args/-/process-nextick-args-2.0.1.tgz#7820d9b16120cc55ca9ae7792680ae7dba6d7fe2" + integrity sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag== + +progress@^2.0.0: + version "2.0.3" + resolved "https://registry.yarnpkg.com/progress/-/progress-2.0.3.tgz#7e8cf8d8f5b8f239c1bc68beb4eb78567d572ef8" + integrity sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA== + +promise-inflight@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/promise-inflight/-/promise-inflight-1.0.1.tgz#98472870bf228132fcbdd868129bad12c3c029e3" + integrity sha1-mEcocL8igTL8vdhoEputEsPAKeM= + +prop-types@^15.6.0, prop-types@^15.6.2, prop-types@^15.7.2: + version "15.7.2" + resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.7.2.tgz#52c41e75b8c87e72b9d9360e0206b99dcbffa6c5" + integrity sha512-8QQikdH7//R2vurIJSutZ1smHYTcLpRWEOlHnzcWHmBYrOGUysKwSsrC89BCiFj3CbrfJ/nXFdJepOVrY1GCHQ== + dependencies: + loose-envify "^1.4.0" + object-assign "^4.1.1" + react-is "^16.8.1" + +proxy-addr@~2.0.5: + version "2.0.6" + resolved "https://registry.yarnpkg.com/proxy-addr/-/proxy-addr-2.0.6.tgz#fdc2336505447d3f2f2c638ed272caf614bbb2bf" + integrity sha512-dh/frvCBVmSsDYzw6n926jv974gddhkFPfiN8hPOi30Wax25QZyZEGveluCgliBnqmuM+UJmBErbAUFIoDbjOw== + dependencies: + forwarded "~0.1.2" + ipaddr.js "1.9.1" + +prr@~1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/prr/-/prr-1.0.1.tgz#d3fc114ba06995a45ec6893f484ceb1d78f5f476" + integrity sha1-0/wRS6BplaRexok/SEzrHXj19HY= + +pump@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/pump/-/pump-3.0.0.tgz#b4a2116815bde2f4e1ea602354e8c75565107a64" + integrity sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww== + dependencies: + end-of-stream "^1.1.0" + once "^1.3.1" + +punycode@1.3.2: + version "1.3.2" + resolved "https://registry.yarnpkg.com/punycode/-/punycode-1.3.2.tgz#9653a036fb7c1ee42342f2325cceefea3926c48d" + integrity sha1-llOgNvt8HuQjQvIyXM7v6jkmxI0= + +punycode@^2.1.0: + version "2.1.1" + resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.1.1.tgz#b58b010ac40c22c5657616c8d2c2c02c7bf479ec" + integrity sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A== + +q@^1.1.2: + version "1.5.1" + resolved "https://registry.yarnpkg.com/q/-/q-1.5.1.tgz#7e32f75b41381291d04611f1bf14109ac00651d7" + integrity sha1-fjL3W0E4EpHQRhHxvxQQmsAGUdc= + +qs@6.7.0: + version "6.7.0" + resolved "https://registry.yarnpkg.com/qs/-/qs-6.7.0.tgz#41dc1a015e3d581f1621776be31afb2876a9b1bc" + integrity sha512-VCdBRNFTX1fyE7Nb6FYoURo/SPe62QCaAyzJvUjwRaIsc+NePBEniHlvxFmmX56+HZphIGtV0XeCirBtpDrTyQ== + +qs@^6.9.4: + version "6.9.6" + resolved "https://registry.yarnpkg.com/qs/-/qs-6.9.6.tgz#26ed3c8243a431b2924aca84cc90471f35d5a0ee" + integrity sha512-TIRk4aqYLNoJUbd+g2lEdz5kLWIuTMRagAXxl78Q0RiVjAOugHmeKNGdd3cwo/ktpf9aL9epCfFqWDEKysUlLQ== + +querystring@0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/querystring/-/querystring-0.2.0.tgz#b209849203bb25df820da756e747005878521620" + integrity sha1-sgmEkgO7Jd+CDadW50cAWHhSFiA= + +querystringify@^2.1.1: + version "2.2.0" + resolved "https://registry.yarnpkg.com/querystringify/-/querystringify-2.2.0.tgz#3345941b4153cb9d082d8eee4cda2016a9aef7f6" + integrity sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ== + +quick-lru@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/quick-lru/-/quick-lru-4.0.1.tgz#5b8878f113a58217848c6482026c73e1ba57727f" + integrity sha512-ARhCpm70fzdcvNQfPoy49IaanKkTlRWF2JMzqhcJbhSFRZv7nPTvZJdcY7301IPmvW+/p0RgIWnQDLJxifsQ7g== + +raf-schd@^4.0.2: + version "4.0.2" + resolved "https://registry.yarnpkg.com/raf-schd/-/raf-schd-4.0.2.tgz#bd44c708188f2e84c810bf55fcea9231bcaed8a0" + integrity sha512-VhlMZmGy6A6hrkJWHLNTGl5gtgMUm+xfGza6wbwnE914yeQ5Ybm18vgM734RZhMgfw4tacUrWseGZlpUrrakEQ== + +raf@^3.4.0: + version "3.4.1" + resolved "https://registry.yarnpkg.com/raf/-/raf-3.4.1.tgz#0742e99a4a6552f445d73e3ee0328af0ff1ede39" + integrity sha512-Sq4CW4QhwOHE8ucn6J34MqtZCeWFP2aQSmrlroYgqAV1PjStIhJXxYuTgUIfkEk7zTLjmIjLmU5q+fbD1NnOJA== + dependencies: + performance-now "^2.1.0" + +randombytes@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/randombytes/-/randombytes-2.1.0.tgz#df6f84372f0270dc65cdf6291349ab7a473d4f2a" + integrity sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ== + dependencies: + safe-buffer "^5.1.0" + +range-parser@^1.2.1, range-parser@~1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/range-parser/-/range-parser-1.2.1.tgz#3cf37023d199e1c24d1a55b84800c2f3e6468031" + integrity sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg== + +raw-body@2.4.0: + version "2.4.0" + resolved "https://registry.yarnpkg.com/raw-body/-/raw-body-2.4.0.tgz#a1ce6fb9c9bc356ca52e89256ab59059e13d0332" + integrity sha512-4Oz8DUIwdvoa5qMJelxipzi/iJIi40O5cGV1wNYp5hvZP8ZN0T+jiNkL0QepXs+EsQ9XJ8ipEDoiH70ySUJP3Q== + dependencies: + bytes "3.1.0" + http-errors "1.7.2" + iconv-lite "0.4.24" + unpipe "1.0.0" + +rc-align@^4.0.0: + version "4.0.9" + resolved "https://registry.yarnpkg.com/rc-align/-/rc-align-4.0.9.tgz#46d8801c4a139ff6a65ad1674e8efceac98f85f2" + integrity sha512-myAM2R4qoB6LqBul0leaqY8gFaiECDJ3MtQDmzDo9xM9NRT/04TvWOYd2YHU9zvGzqk9QXF6S9/MifzSKDZeMw== + dependencies: + "@babel/runtime" "^7.10.1" + classnames "2.x" + dom-align "^1.7.0" + rc-util "^5.3.0" + resize-observer-polyfill "^1.5.1" + +rc-cascader@~1.4.0: + version "1.4.2" + resolved "https://registry.yarnpkg.com/rc-cascader/-/rc-cascader-1.4.2.tgz#caa81098e3ef4d5f823f9156f6d8d6dbd6321afa" + integrity sha512-JVuLGrSi+3G8DZyPvlKlGVWJjhoi9NTz6REHIgRspa5WnznRkKGm2ejb0jJtz0m2IL8Q9BG4ZA2sXuqAu71ltQ== + dependencies: + "@babel/runtime" "^7.12.5" + array-tree-filter "^2.1.0" + rc-trigger "^5.0.4" + rc-util "^5.0.1" + warning "^4.0.1" + +rc-checkbox@~2.3.0: + version "2.3.2" + resolved "https://registry.yarnpkg.com/rc-checkbox/-/rc-checkbox-2.3.2.tgz#f91b3678c7edb2baa8121c9483c664fa6f0aefc1" + integrity sha512-afVi1FYiGv1U0JlpNH/UaEXdh6WUJjcWokj/nUN2TgG80bfG+MDdbfHKlLcNNba94mbjy2/SXJ1HDgrOkXGAjg== + dependencies: + "@babel/runtime" "^7.10.1" + classnames "^2.2.1" + +rc-collapse@~3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/rc-collapse/-/rc-collapse-3.1.0.tgz#4ce5e612568c5fbeaf368cc39214471c1461a1a1" + integrity sha512-EwpNPJcLe7b+5JfyaxM9ZNnkCgqArt3QQO0Cr5p5plwz/C9h8liAmjYY5I4+hl9lAjBqb7ZwLu94+z+rt5g1WQ== + dependencies: + "@babel/runtime" "^7.10.1" + classnames "2.x" + rc-motion "^2.3.4" + rc-util "^5.2.1" + shallowequal "^1.1.0" + +rc-dialog@~8.5.0, rc-dialog@~8.5.1: + version "8.5.1" + resolved "https://registry.yarnpkg.com/rc-dialog/-/rc-dialog-8.5.1.tgz#df316dd93e1685d7df1f5e4164ee35cba4a9af88" + integrity sha512-EcLgHHjF3Jp4C+TFceO2j7gIrpx0YIhY6ronki5QJDL/z+qWYozY5RNh4rnv4a6R21SPVhV+SK+gMMlMHZ/YRQ== + dependencies: + "@babel/runtime" "^7.10.1" + classnames "^2.2.6" + rc-motion "^2.3.0" + rc-util "^5.6.1" + +rc-drawer@~4.2.0: + version "4.2.2" + resolved "https://registry.yarnpkg.com/rc-drawer/-/rc-drawer-4.2.2.tgz#5fd8b18ce20575ff22b36e0c5ddbe363c13db555" + integrity sha512-zw48FATkAmJrEnfeRWiMqvKAzqGzUDLN1UXlluB7q7GgbR6mJFvc+QsmNrgxsFuMz86Lh9mKSIi7rXlPINmuzw== + dependencies: + "@babel/runtime" "^7.10.1" + classnames "^2.2.6" + rc-util "^5.7.0" + +rc-dropdown@^3.1.3, rc-dropdown@~3.2.0: + version "3.2.0" + resolved "https://registry.yarnpkg.com/rc-dropdown/-/rc-dropdown-3.2.0.tgz#da6c2ada403842baee3a9e909a0b1a91ba3e1090" + integrity sha512-j1HSw+/QqlhxyTEF6BArVZnTmezw2LnSmRk6I9W7BCqNCKaRwleRmMMs1PHbuaG8dKHVqP6e21RQ7vPBLVnnNw== + dependencies: + "@babel/runtime" "^7.10.1" + classnames "^2.2.6" + rc-trigger "^5.0.4" + +rc-field-form@~1.18.0: + version "1.18.1" + resolved "https://registry.yarnpkg.com/rc-field-form/-/rc-field-form-1.18.1.tgz#41027816c80d1acf6f51db085d34c2c35213a701" + integrity sha512-/YRnelnHLxygl/ROGhFqfCT+uAZ5xLvu3qjtlETOneb7fXKk7tqp+RGfYqZ4uNViXlsfxox3qqMMTVet6wYfEA== + dependencies: + "@babel/runtime" "^7.8.4" + async-validator "^3.0.3" + rc-util "^5.0.0" + +rc-image@~5.1.1: + version "5.1.4" + resolved "https://registry.yarnpkg.com/rc-image/-/rc-image-5.1.4.tgz#e251101159db80c3e33276aaa4669a991c7486c0" + integrity sha512-hilxwwEAYJXocY6i+lEdEibvHVOLgN43EQFfjKFiiruRNiUQzGWcdCseyaeTZgInTDrf+QWZP6MujlZjtEbpkA== + dependencies: + "@babel/runtime" "^7.11.2" + classnames "^2.2.6" + rc-dialog "~8.5.0" + rc-util "^5.0.6" + +rc-input-number@~6.1.0: + version "6.1.3" + resolved "https://registry.yarnpkg.com/rc-input-number/-/rc-input-number-6.1.3.tgz#d558be65793429807cc2cdc360af407599d80283" + integrity sha512-qCLWK9NuuKGTsPXjRU/XvSOX7EKdnHlOpg59nPjYSDdH/czsAHZyYq50O6b6RF2TMPOjVpmsZQoMjNJYcnn6JA== + dependencies: + "@babel/runtime" "^7.10.1" + classnames "^2.2.5" + rc-util "^5.0.1" + +rc-mentions@~1.5.0: + version "1.5.3" + resolved "https://registry.yarnpkg.com/rc-mentions/-/rc-mentions-1.5.3.tgz#b92bebadf8ad9fb3586ba1af922d63b49d991c67" + integrity sha512-NG/KB8YiKBCJPHHvr/QapAb4f9YzLJn7kDHtmI1K6t7ZMM5YgrjIxNNhoRKKP9zJvb9PdPts69Hbg4ZMvLVIFQ== + dependencies: + "@babel/runtime" "^7.10.1" + classnames "^2.2.6" + rc-menu "^8.0.1" + rc-textarea "^0.3.0" + rc-trigger "^5.0.4" + rc-util "^5.0.1" + +rc-menu@^8.0.1, rc-menu@^8.6.1, rc-menu@~8.10.0: + version "8.10.5" + resolved "https://registry.yarnpkg.com/rc-menu/-/rc-menu-8.10.5.tgz#44b7381c650cc76020dfd65753b535f415012179" + integrity sha512-8Ets93wQFy9IysmgRUm1VGdrEz6XfZTM0jQOqOPLYNXah5HgAmCh4xT0UOygfHB3IWiQeqDgr2uPB4uVhwI2+Q== + dependencies: + "@babel/runtime" "^7.10.1" + classnames "2.x" + mini-store "^3.0.1" + rc-motion "^2.0.1" + rc-trigger "^5.1.2" + rc-util "^5.7.0" + resize-observer-polyfill "^1.5.0" + shallowequal "^1.1.0" + +rc-motion@^2.0.0, rc-motion@^2.0.1, rc-motion@^2.2.0, rc-motion@^2.3.0, rc-motion@^2.3.4, rc-motion@^2.4.0: + version "2.4.1" + resolved "https://registry.yarnpkg.com/rc-motion/-/rc-motion-2.4.1.tgz#323f47c8635e6b2bc0cba2dfad25fc415b58e1dc" + integrity sha512-TWLvymfMu8SngPx5MDH8dQ0D2RYbluNTfam4hY/dNNx9RQ3WtGuZ/GXHi2ymLMzH+UNd6EEFYkOuR5JTTtm8Xg== + dependencies: + "@babel/runtime" "^7.11.1" + classnames "^2.2.1" + rc-util "^5.2.1" + +rc-notification@~4.5.2: + version "4.5.4" + resolved "https://registry.yarnpkg.com/rc-notification/-/rc-notification-4.5.4.tgz#1292e163003db4b9162c856a4630e5d0f1359356" + integrity sha512-VsN0ouF4uglE5g3C9oDsXLNYX0Sz++ZNUFYCswkxhpImYJ9u6nJOpyA71uOYDVCu6bAF54Y5Hi/b+EcnMzkepg== + dependencies: + "@babel/runtime" "^7.10.1" + classnames "2.x" + rc-motion "^2.2.0" + rc-util "^5.0.1" + +rc-overflow@^1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/rc-overflow/-/rc-overflow-1.0.2.tgz#f56bcd920029979989f576d55084b81f9632c19c" + integrity sha512-GXj4DAyNxm4f57LvXLwhJaZoJHzSge2l2lQq64MZP7NJAfLpQqOLD+v9JMV9ONTvDPZe8kdzR+UMmkAn7qlzFA== + dependencies: + "@babel/runtime" "^7.11.1" + classnames "^2.2.1" + rc-resize-observer "^1.0.0" + rc-util "^5.5.1" + +rc-pagination@~3.1.2: + version "3.1.3" + resolved "https://registry.yarnpkg.com/rc-pagination/-/rc-pagination-3.1.3.tgz#afd779839fefab2cb14248d5e7b74027960bb48b" + integrity sha512-Z7CdC4xGkedfAwcUHPtfqNhYwVyDgkmhkvfsmoByCOwAd89p42t5O5T3ORar1wRmVWf3jxk/Bf4k0atenNvlFA== + dependencies: + "@babel/runtime" "^7.10.1" + classnames "^2.2.1" + +rc-picker@~2.5.1: + version "2.5.2" + resolved "https://registry.yarnpkg.com/rc-picker/-/rc-picker-2.5.2.tgz#36d91b8cdddbf8b2474af29c2853b77502a7fb01" + integrity sha512-rQLgvjyFrxjiWlR+Q7CyXdTOP/gHbiXlBca7irOtuEb6HMRLdm+/OfIB7xaaPHgdkv1ZOsxCk8zCEX6j0qf24g== + dependencies: + "@babel/runtime" "^7.10.1" + classnames "^2.2.1" + date-fns "^2.15.0" + dayjs "^1.8.30" + moment "^2.24.0" + rc-trigger "^5.0.4" + rc-util "^5.4.0" + shallowequal "^1.1.0" + +rc-progress@~3.1.0: + version "3.1.3" + resolved "https://registry.yarnpkg.com/rc-progress/-/rc-progress-3.1.3.tgz#d77d8fd26d9d948d72c2a28b64b71a6e86df2426" + integrity sha512-Jl4fzbBExHYMoC6HBPzel0a9VmhcSXx24LVt/mdhDM90MuzoMCJjXZAlhA0V0CJi+SKjMhfBoIQ6Lla1nD4QNw== + dependencies: + "@babel/runtime" "^7.10.1" + classnames "^2.2.6" + +rc-rate@~2.9.0: + version "2.9.1" + resolved "https://registry.yarnpkg.com/rc-rate/-/rc-rate-2.9.1.tgz#e43cb95c4eb90a2c1e0b16ec6614d8c43530a731" + integrity sha512-MmIU7FT8W4LYRRHJD1sgG366qKtSaKb67D0/vVvJYR0lrCuRrCiVQ5qhfT5ghVO4wuVIORGpZs7ZKaYu+KMUzA== + dependencies: + "@babel/runtime" "^7.10.1" + classnames "^2.2.5" + rc-util "^5.0.1" + +rc-resize-observer@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/rc-resize-observer/-/rc-resize-observer-1.0.0.tgz#97fb89856f62fec32ab6e40933935cf58e2e102d" + integrity sha512-RgKGukg1mlzyGdvzF7o/LGFC8AeoMH9aGzXTUdp6m+OApvmRdUuOscq/Y2O45cJA+rXt1ApWlpFoOIioXL3AGg== + dependencies: + "@babel/runtime" "^7.10.1" + classnames "^2.2.1" + rc-util "^5.0.0" + resize-observer-polyfill "^1.5.1" + +rc-select@^12.0.0, rc-select@~12.1.0: + version "12.1.2" + resolved "https://registry.yarnpkg.com/rc-select/-/rc-select-12.1.2.tgz#7f5f06838b8a8c86516acb66ead26adbc55fbc16" + integrity sha512-WEcqj4ljz5kgp/yPN4RDQEZRvjGkwdk1PugpFrtd6tY+YqwKZs7vSZt6xphVIvWlmtwmZMe7e9G1U8XykUN0+g== + dependencies: + "@babel/runtime" "^7.10.1" + classnames "2.x" + rc-motion "^2.0.1" + rc-overflow "^1.0.0" + rc-trigger "^5.0.4" + rc-util "^5.0.1" + rc-virtual-list "^3.2.0" + +rc-slider@~9.7.1: + version "9.7.1" + resolved "https://registry.yarnpkg.com/rc-slider/-/rc-slider-9.7.1.tgz#63535177a74a3ee44f090909e8c6f98426eb9dba" + integrity sha512-r9r0dpFA3PEvxBhIfVi1lVzxuSogWxeY+tGvi2AqMM1rPgaOXQ7WbtT+9kVFkJ9K8TntA/vYPgiCCKfN29KTkw== + dependencies: + "@babel/runtime" "^7.10.1" + classnames "^2.2.5" + rc-tooltip "^5.0.1" + rc-util "^5.0.0" + shallowequal "^1.1.0" + +rc-steps@~4.1.0: + version "4.1.3" + resolved "https://registry.yarnpkg.com/rc-steps/-/rc-steps-4.1.3.tgz#208580e22db619e3830ddb7fa41bc886c65d9803" + integrity sha512-GXrMfWQOhN3sVze3JnzNboHpQdNHcdFubOETUHyDpa/U3HEKBZC3xJ8XK4paBgF4OJ3bdUVLC+uBPc6dCxvDYA== + dependencies: + "@babel/runtime" "^7.10.2" + classnames "^2.2.3" + rc-util "^5.0.1" + +rc-switch@~3.2.0: + version "3.2.2" + resolved "https://registry.yarnpkg.com/rc-switch/-/rc-switch-3.2.2.tgz#d001f77f12664d52595b4f6fb425dd9e66fba8e8" + integrity sha512-+gUJClsZZzvAHGy1vZfnwySxj+MjLlGRyXKXScrtCTcmiYNPzxDFOxdQ/3pK1Kt/0POvwJ/6ALOR8gwdXGhs+A== + dependencies: + "@babel/runtime" "^7.10.1" + classnames "^2.2.1" + rc-util "^5.0.1" + +rc-table@~7.12.0: + version "7.12.3" + resolved "https://registry.yarnpkg.com/rc-table/-/rc-table-7.12.3.tgz#c86e93b40dc3e2b8aae9e54ad7aa5a64ff7d261e" + integrity sha512-R87lx28os4Ftm/9x+MamwzLw+Dtp9yxihceAgLA/9JS9QP/sqej1qlO+3T/Uw4paZqCg9SpW0XzLI1MandviWA== + dependencies: + "@babel/runtime" "^7.10.1" + classnames "^2.2.5" + rc-resize-observer "^1.0.0" + rc-util "^5.4.0" + shallowequal "^1.1.0" + +rc-tabs@~11.7.0: + version "11.7.3" + resolved "https://registry.yarnpkg.com/rc-tabs/-/rc-tabs-11.7.3.tgz#32a30e59c6992d60fb58115ba0bf2652b337ed43" + integrity sha512-5nd2NVss9TprPRV9r8N05SjQyAE7zDrLejxFLcbJ+BdLxSwnGnk3ws/Iq0smqKZUnPQC0XEvnpF3+zlllUUT2w== + dependencies: + "@babel/runtime" "^7.11.2" + classnames "2.x" + rc-dropdown "^3.1.3" + rc-menu "^8.6.1" + rc-resize-observer "^1.0.0" + rc-util "^5.5.0" + +rc-textarea@^0.3.0, rc-textarea@~0.3.0: + version "0.3.4" + resolved "https://registry.yarnpkg.com/rc-textarea/-/rc-textarea-0.3.4.tgz#1408a64c87b5e76db5c847699ef9ab5ee97dd6f9" + integrity sha512-ILUYx831ZukQPv3m7R4RGRtVVWmL1LV4ME03L22mvT56US0DGCJJaRTHs4vmpcSjFHItph5OTmhodY4BOwy81A== + dependencies: + "@babel/runtime" "^7.10.1" + classnames "^2.2.1" + rc-resize-observer "^1.0.0" + rc-util "^5.7.0" + +rc-tooltip@^5.0.1, rc-tooltip@~5.0.0: + version "5.0.2" + resolved "https://registry.yarnpkg.com/rc-tooltip/-/rc-tooltip-5.0.2.tgz#e48258fc9931bd9281102b2d9eacc5b986cf3258" + integrity sha512-A4FejSG56PzYtSNUU4H1pVzfhtkV/+qMT2clK0CsSj+9mbc4USEtpWeX6A/jjVL+goBOMKj8qlH7BCZmZWh/Nw== + dependencies: + "@babel/runtime" "^7.11.2" + rc-trigger "^5.0.0" + +rc-tree-select@~4.3.0: + version "4.3.0" + resolved "https://registry.yarnpkg.com/rc-tree-select/-/rc-tree-select-4.3.0.tgz#714a4fe658aa73f2a7b0aa4bd6e43be63194a6ce" + integrity sha512-EEXB9dKBsJNJuKIU5NERZsaJ71GDGIj5uWLl7A4XiYr2jXM4JICfScvvp3O5jHMDfhqmgpqNc0z90mHkgh3hKg== + dependencies: + "@babel/runtime" "^7.10.1" + classnames "2.x" + rc-select "^12.0.0" + rc-tree "^4.0.0" + rc-util "^5.0.5" + +rc-tree@^4.0.0, rc-tree@~4.1.0: + version "4.1.1" + resolved "https://registry.yarnpkg.com/rc-tree/-/rc-tree-4.1.1.tgz#d40f418b31b75e61886e3969481df1444232c98b" + integrity sha512-ufq7CkWfvTQa+xMPzEWYfOjTfsEALlPr0/IyujEG4+4d8NdaR3e+0dc8LkkVWoe1VCcXV2FQqAsgr2z/ThFUrQ== + dependencies: + "@babel/runtime" "^7.10.1" + classnames "2.x" + rc-motion "^2.0.1" + rc-util "^5.0.0" + rc-virtual-list "^3.0.1" + +rc-trigger@^5.0.0, rc-trigger@^5.0.4, rc-trigger@^5.1.2, rc-trigger@^5.2.1: + version "5.2.1" + resolved "https://registry.yarnpkg.com/rc-trigger/-/rc-trigger-5.2.1.tgz#54686220b884ed1e0750c4f2411fbb34d4928c99" + integrity sha512-XZilSlSDnb0L/R3Ff2xo9C0Fho2aBDoAn8u3coM60XdLqTCo24nsOh1bfAMm0uIB1qVjh5eqeyFqnBPmXi8pJg== + dependencies: + "@babel/runtime" "^7.11.2" + classnames "^2.2.6" + rc-align "^4.0.0" + rc-motion "^2.0.0" + rc-util "^5.5.0" + +rc-upload@~3.3.4: + version "3.3.4" + resolved "https://registry.yarnpkg.com/rc-upload/-/rc-upload-3.3.4.tgz#b0668d18661595c69c0621cec220fd116cc79952" + integrity sha512-v2sirR4JL31UTHD/f0LGUdd+tpFaOVUTPeIEjAXRP9kRN8TFhqOgcXl5ixtyqj90FmtRUmKmafCv0EmhBQUHqQ== + dependencies: + "@babel/runtime" "^7.10.1" + classnames "^2.2.5" + rc-util "^5.2.0" + +rc-util@^5.0.0, rc-util@^5.0.1, rc-util@^5.0.5, rc-util@^5.0.6, rc-util@^5.0.7, rc-util@^5.2.0, rc-util@^5.2.1, rc-util@^5.3.0, rc-util@^5.4.0, rc-util@^5.5.0, rc-util@^5.5.1, rc-util@^5.6.1, rc-util@^5.7.0: + version "5.7.0" + resolved "https://registry.yarnpkg.com/rc-util/-/rc-util-5.7.0.tgz#776b14cf5bbfc24f419fd40c42ffadddda0718fc" + integrity sha512-0hh5XkJ+vBDeMJsHElqT1ijMx+gC3gpClwQ10h/5hccrrgrMx8VUem183KLlH1YrWCfMMPmDXWWNnwsn+p6URw== + dependencies: + "@babel/runtime" "^7.12.5" + react-is "^16.12.0" + shallowequal "^1.1.0" + +rc-virtual-list@^3.0.1, rc-virtual-list@^3.2.0: + version "3.2.6" + resolved "https://registry.yarnpkg.com/rc-virtual-list/-/rc-virtual-list-3.2.6.tgz#2c92a40f4425e19881b38134d6bd286a11137d2d" + integrity sha512-8FiQLDzm3c/tMX0d62SQtKDhLH7zFlSI6pWBAPt+TUntEqd3Lz9zFAmpvTu8gkvUom/HCsDSZs4wfV4wDPWC0Q== + dependencies: + classnames "^2.2.6" + rc-resize-observer "^1.0.0" + rc-util "^5.0.7" + +react-dom@^17.0.0: + version "17.0.1" + resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-17.0.1.tgz#1de2560474ec9f0e334285662ede52dbc5426fc6" + integrity sha512-6eV150oJZ9U2t9svnsspTMrWNyHc6chX0KzDeAOXftRa8bNeOKTTfCJ7KorIwenkHd2xqVTBTCZd79yk/lx/Ug== + dependencies: + loose-envify "^1.1.0" + object-assign "^4.1.1" + scheduler "^0.20.1" + +react-fast-compare@^2.0.1: + version "2.0.4" + resolved "https://registry.yarnpkg.com/react-fast-compare/-/react-fast-compare-2.0.4.tgz#e84b4d455b0fec113e0402c329352715196f81f9" + integrity sha512-suNP+J1VU1MWFKcyt7RtjiSWUjvidmQSlqu+eHslq+342xCbGTYmC0mEhPCOHxlW0CywylOC1u2DFAT+bv4dBw== + +react-is@^16.12.0, react-is@^16.6.0, react-is@^16.7.0, react-is@^16.8.1: + version "16.13.1" + resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4" + integrity sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ== + +react-lifecycles-compat@^3.0.4: + version "3.0.4" + resolved "https://registry.yarnpkg.com/react-lifecycles-compat/-/react-lifecycles-compat-3.0.4.tgz#4f1a273afdfc8f3488a8c516bfda78f872352362" + integrity sha512-fBASbA6LnOU9dOU2eW7aQ8xmYBSXUIWr+UmF9b1efZBazGNO+rcXT/icdKnYm2pTwcRylVUYwW7H1PHfLekVzA== + +react-resize-detector@^5.2.0: + version "5.2.0" + resolved "https://registry.yarnpkg.com/react-resize-detector/-/react-resize-detector-5.2.0.tgz#992083834432308c551a8251a2c52306d9d16718" + integrity sha512-PQAc03J2eyhvaiWgEdQ8+bKbbyGJzLEr70KuivBd1IEmP/iewNakLUMkxm6MWnDqsRPty85pioyg8MvGb0qC8A== + dependencies: + lodash "^4.17.20" + prop-types "^15.7.2" + raf-schd "^4.0.2" + resize-observer-polyfill "^1.5.1" + +react-router-dom@^5.2.0: + version "5.2.0" + resolved "https://registry.yarnpkg.com/react-router-dom/-/react-router-dom-5.2.0.tgz#9e65a4d0c45e13289e66c7b17c7e175d0ea15662" + integrity sha512-gxAmfylo2QUjcwxI63RhQ5G85Qqt4voZpUXSEqCwykV0baaOTQDR1f0PmY8AELqIyVc0NEZUj0Gov5lNGcXgsA== + dependencies: + "@babel/runtime" "^7.1.2" + history "^4.9.0" + loose-envify "^1.3.1" + prop-types "^15.6.2" + react-router "5.2.0" + tiny-invariant "^1.0.2" + tiny-warning "^1.0.0" + +react-router@5.2.0: + version "5.2.0" + resolved "https://registry.yarnpkg.com/react-router/-/react-router-5.2.0.tgz#424e75641ca8747fbf76e5ecca69781aa37ea293" + integrity sha512-smz1DUuFHRKdcJC0jobGo8cVbhO3x50tCL4icacOlcwDOEQPq4TMqwx3sY1TP+DvtTgz4nm3thuo7A+BK2U0Dw== + dependencies: + "@babel/runtime" "^7.1.2" + history "^4.9.0" + hoist-non-react-statics "^3.1.0" + loose-envify "^1.3.1" + mini-create-react-context "^0.4.0" + path-to-regexp "^1.7.0" + prop-types "^15.6.2" + react-is "^16.6.0" + tiny-invariant "^1.0.2" + tiny-warning "^1.0.0" + +react-smooth@^1.0.6: + version "1.0.6" + resolved "https://registry.yarnpkg.com/react-smooth/-/react-smooth-1.0.6.tgz#18b964f123f7bca099e078324338cd8739346d0a" + integrity sha512-B2vL4trGpNSMSOzFiAul9kFAsxTukL9Wyy9EXtkQy3GJr6sZqW9e1nShdVOJ3hRYamPZ94O17r3Q0bjSw3UYtg== + dependencies: + lodash "~4.17.4" + prop-types "^15.6.0" + raf "^3.4.0" + react-transition-group "^2.5.0" + +react-transition-group@^2.5.0: + version "2.9.0" + resolved "https://registry.yarnpkg.com/react-transition-group/-/react-transition-group-2.9.0.tgz#df9cdb025796211151a436c69a8f3b97b5b07c8d" + integrity sha512-+HzNTCHpeQyl4MJ/bdE0u6XRMe9+XG/+aL4mCxVN4DnPBQ0/5bfHWPDuOZUzYdMj94daZaZdCCc1Dzt9R/xSSg== + dependencies: + dom-helpers "^3.4.0" + loose-envify "^1.4.0" + prop-types "^15.6.2" + react-lifecycles-compat "^3.0.4" + +react@^17.0.0: + version "17.0.1" + resolved "https://registry.yarnpkg.com/react/-/react-17.0.1.tgz#6e0600416bd57574e3f86d92edba3d9008726127" + integrity sha512-lG9c9UuMHdcAexXtigOZLX8exLWkW0Ku29qPRU8uhF2R9BN96dLCt0psvzPLlHc5OWkgymP3qwTRgbnw5BKx3w== + dependencies: + loose-envify "^1.1.0" + object-assign "^4.1.1" + +read-cache@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/read-cache/-/read-cache-1.0.0.tgz#e664ef31161166c9751cdbe8dbcf86b5fb58f774" + integrity sha1-5mTvMRYRZsl1HNvo28+GtftY93Q= + dependencies: + pify "^2.3.0" + +read-pkg-up@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/read-pkg-up/-/read-pkg-up-2.0.0.tgz#6b72a8048984e0c41e79510fd5e9fa99b3b549be" + integrity sha1-a3KoBImE4MQeeVEP1en6mbO1Sb4= + dependencies: + find-up "^2.0.0" + read-pkg "^2.0.0" + +read-pkg-up@^7.0.1: + version "7.0.1" + resolved "https://registry.yarnpkg.com/read-pkg-up/-/read-pkg-up-7.0.1.tgz#f3a6135758459733ae2b95638056e1854e7ef507" + integrity sha512-zK0TB7Xd6JpCLmlLmufqykGE+/TlOePD6qKClNW7hHDKFh/J7/7gCWGR7joEQEW1bKq3a3yUZSObOoWLFQ4ohg== + dependencies: + find-up "^4.1.0" + read-pkg "^5.2.0" + type-fest "^0.8.1" + +read-pkg@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/read-pkg/-/read-pkg-2.0.0.tgz#8ef1c0623c6a6db0dc6713c4bfac46332b2368f8" + integrity sha1-jvHAYjxqbbDcZxPEv6xGMysjaPg= + dependencies: + load-json-file "^2.0.0" + normalize-package-data "^2.3.2" + path-type "^2.0.0" + +read-pkg@^5.2.0: + version "5.2.0" + resolved "https://registry.yarnpkg.com/read-pkg/-/read-pkg-5.2.0.tgz#7bf295438ca5a33e56cd30e053b34ee7250c93cc" + integrity sha512-Ug69mNOpfvKDAc2Q8DRpMjjzdtrnv9HcSMX+4VsZxD1aZ6ZzrIE7rlzXBtWTyhULSMKg076AW6WR5iZpD0JiOg== + dependencies: + "@types/normalize-package-data" "^2.4.0" + normalize-package-data "^2.5.0" + parse-json "^5.0.0" + type-fest "^0.6.0" + +readable-stream@^2.0.1, readable-stream@^2.0.2: + version "2.3.7" + resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.7.tgz#1eca1cf711aef814c04f62252a36a62f6cb23b57" + integrity sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw== + dependencies: + core-util-is "~1.0.0" + inherits "~2.0.3" + isarray "~1.0.0" + process-nextick-args "~2.0.0" + safe-buffer "~5.1.1" + string_decoder "~1.1.1" + util-deprecate "~1.0.1" + +readable-stream@^3.0.6, readable-stream@^3.1.1: + version "3.6.0" + resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-3.6.0.tgz#337bbda3adc0706bd3e024426a286d4b4b2c9198" + integrity sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA== + dependencies: + inherits "^2.0.3" + string_decoder "^1.1.1" + util-deprecate "^1.0.1" + +readdirp@^2.2.1: + version "2.2.1" + resolved "https://registry.yarnpkg.com/readdirp/-/readdirp-2.2.1.tgz#0e87622a3325aa33e892285caf8b4e846529a525" + integrity sha512-1JU/8q+VgFZyxwrJ+SVIOsh+KywWGpds3NTqikiKpDMZWScmAYyKIgqkO+ARvNWJfXeXR1zxz7aHF4u4CyH6vQ== + dependencies: + graceful-fs "^4.1.11" + micromatch "^3.1.10" + readable-stream "^2.0.2" + +recharts-scale@^0.4.2: + version "0.4.3" + resolved "https://registry.yarnpkg.com/recharts-scale/-/recharts-scale-0.4.3.tgz#040b4f638ed687a530357292ecac880578384b59" + integrity sha512-t8p5sccG9Blm7c1JQK/ak9O8o95WGhNXD7TXg/BW5bYbVlr6eCeRBNpgyigD4p6pSSMehC5nSvBUPj6F68rbFA== + dependencies: + decimal.js-light "^2.4.1" + +recharts@^2.0.3: + version "2.0.4" + resolved "https://registry.yarnpkg.com/recharts/-/recharts-2.0.4.tgz#0ffaa9437762ed0bf021968af42154777f97c94c" + integrity sha512-XwFRhyOW6APMKvrCqN8e1IPHAQi7lmrOqp48LKi40NFI8WjFWHgTaTfwBBMsGCnTJSezJEEd/41L3bo/tfevkw== + dependencies: + classnames "^2.2.5" + d3-interpolate "^2.0.1" + d3-scale "^3.2.3" + d3-shape "^2.0.0" + eventemitter3 "^4.0.1" + lodash "^4.17.19" + react-resize-detector "^5.2.0" + react-smooth "^1.0.6" + recharts-scale "^0.4.2" + reduce-css-calc "^2.1.7" + +rechoir@^0.7.0: + version "0.7.0" + resolved "https://registry.yarnpkg.com/rechoir/-/rechoir-0.7.0.tgz#32650fd52c21ab252aa5d65b19310441c7e03aca" + integrity sha512-ADsDEH2bvbjltXEP+hTIAmeFekTFK0V2BTxMkok6qILyAJEXV0AFfoWcAq4yfll5VdIMd/RVXq0lR+wQi5ZU3Q== + dependencies: + resolve "^1.9.0" + +redent@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/redent/-/redent-3.0.0.tgz#e557b7998316bb53c9f1f56fa626352c6963059f" + integrity sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg== + dependencies: + indent-string "^4.0.0" + strip-indent "^3.0.0" + +reduce-css-calc@^2.1.7: + version "2.1.8" + resolved "https://registry.yarnpkg.com/reduce-css-calc/-/reduce-css-calc-2.1.8.tgz#7ef8761a28d614980dc0c982f772c93f7a99de03" + integrity sha512-8liAVezDmUcH+tdzoEGrhfbGcP7nOV4NkGE3a74+qqvE7nt9i4sKLGBuZNOnpI4WiGksiNPklZxva80061QiPg== + dependencies: + css-unit-converter "^1.1.1" + postcss-value-parser "^3.3.0" + +redux@^4.0.0: + version "4.0.5" + resolved "https://registry.yarnpkg.com/redux/-/redux-4.0.5.tgz#4db5de5816e17891de8a80c424232d06f051d93f" + integrity sha512-VSz1uMAH24DM6MF72vcojpYPtrTUu3ByVWfPL1nPfVRb5mZVTve5GnNCUV53QM/BZ66xfWrm0CTWoM+Xlz8V1w== + dependencies: + loose-envify "^1.4.0" + symbol-observable "^1.2.0" + +regenerator-runtime@^0.13.4: + version "0.13.7" + resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.13.7.tgz#cac2dacc8a1ea675feaabaeb8ae833898ae46f55" + integrity sha512-a54FxoJDIr27pgf7IgeQGxmqUNYrcV338lf/6gH456HZ/PhX+5BcwHXG9ajESmwe6WRO0tAzRUrRmNONWgkrew== + +regex-not@^1.0.0, regex-not@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/regex-not/-/regex-not-1.0.2.tgz#1f4ece27e00b0b65e0247a6810e6a85d83a5752c" + integrity sha512-J6SDjUgDxQj5NusnOtdFxDwN/+HWykR8GELwctJ7mdqhcyy1xEc4SRFHUXvxTp661YaVKAjfRLZ9cCqS6tn32A== + dependencies: + extend-shallow "^3.0.2" + safe-regex "^1.1.0" + +regexp.prototype.flags@^1.2.0, regexp.prototype.flags@^1.3.0: + version "1.3.1" + resolved "https://registry.yarnpkg.com/regexp.prototype.flags/-/regexp.prototype.flags-1.3.1.tgz#7ef352ae8d159e758c0eadca6f8fcb4eef07be26" + integrity sha512-JiBdRBq91WlY7uRJ0ds7R+dU02i6LKi8r3BuQhNXn+kmeLN+EfHhfjqMRis1zJxnlu88hq/4dx0P2OP3APRTOA== + dependencies: + call-bind "^1.0.2" + define-properties "^1.1.3" + +regexpp@^3.0.0, regexpp@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/regexpp/-/regexpp-3.1.0.tgz#206d0ad0a5648cffbdb8ae46438f3dc51c9f78e2" + integrity sha512-ZOIzd8yVsQQA7j8GCSlPGXwg5PfmA1mrq0JP4nGhh54LaKN3xdai/vHUDu74pKwV8OxseMS65u2NImosQcSD0Q== + +relateurl@^0.2.7: + version "0.2.7" + resolved "https://registry.yarnpkg.com/relateurl/-/relateurl-0.2.7.tgz#54dbf377e51440aca90a4cd274600d3ff2d888a9" + integrity sha1-VNvzd+UUQKypCkzSdGANP/LYiKk= + +remark-parse@^9.0.0: + version "9.0.0" + resolved "https://registry.yarnpkg.com/remark-parse/-/remark-parse-9.0.0.tgz#4d20a299665880e4f4af5d90b7c7b8a935853640" + integrity sha512-geKatMwSzEXKHuzBNU1z676sGcDcFoChMK38TgdHJNAYfFtsfHDQG7MoJAjs6sgYMqyLduCYWDIWZIxiPeafEw== + dependencies: + mdast-util-from-markdown "^0.8.0" + +remark-stringify@^9.0.0: + version "9.0.1" + resolved "https://registry.yarnpkg.com/remark-stringify/-/remark-stringify-9.0.1.tgz#576d06e910548b0a7191a71f27b33f1218862894" + integrity sha512-mWmNg3ZtESvZS8fv5PTvaPckdL4iNlCHTt8/e/8oN08nArHRHjNZMKzA/YW3+p7/lYqIw4nx1XsjCBo/AxNChg== + dependencies: + mdast-util-to-markdown "^0.6.0" + +remark@^13.0.0: + version "13.0.0" + resolved "https://registry.yarnpkg.com/remark/-/remark-13.0.0.tgz#d15d9bf71a402f40287ebe36067b66d54868e425" + integrity sha512-HDz1+IKGtOyWN+QgBiAT0kn+2s6ovOxHyPAFGKVE81VSzJ+mq7RwHFledEvB5F1p4iJvOah/LOKdFuzvRnNLCA== + dependencies: + remark-parse "^9.0.0" + remark-stringify "^9.0.0" + unified "^9.1.0" + +remove-trailing-separator@^1.0.1: + version "1.1.0" + resolved "https://registry.yarnpkg.com/remove-trailing-separator/-/remove-trailing-separator-1.1.0.tgz#c24bce2a283adad5bc3f58e0d48249b92379d8ef" + integrity sha1-wkvOKig62tW8P1jg1IJJuSN52O8= + +renderkid@^2.0.4: + version "2.0.5" + resolved "https://registry.yarnpkg.com/renderkid/-/renderkid-2.0.5.tgz#483b1ac59c6601ab30a7a596a5965cabccfdd0a5" + integrity sha512-ccqoLg+HLOHq1vdfYNm4TBeaCDIi1FLt3wGojTDSvdewUv65oTmI3cnT2E4hRjl1gzKZIPK+KZrXzlUYKnR+vQ== + dependencies: + css-select "^2.0.2" + dom-converter "^0.2" + htmlparser2 "^3.10.1" + lodash "^4.17.20" + strip-ansi "^3.0.0" + +repeat-element@^1.1.2: + version "1.1.3" + resolved "https://registry.yarnpkg.com/repeat-element/-/repeat-element-1.1.3.tgz#782e0d825c0c5a3bb39731f84efee6b742e6b1ce" + integrity sha512-ahGq0ZnV5m5XtZLMb+vP76kcAM5nkLqk0lpqAuojSKGgQtn4eRi4ZZGm2olo2zKFH+sMsWaqOCW1dqAnOru72g== + +repeat-string@^1.0.0, repeat-string@^1.6.1: + version "1.6.1" + resolved "https://registry.yarnpkg.com/repeat-string/-/repeat-string-1.6.1.tgz#8dcae470e1c88abc2d600fff4a776286da75e637" + integrity sha1-jcrkcOHIirwtYA//Sndihtp15jc= + +require-directory@^2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/require-directory/-/require-directory-2.1.1.tgz#8c64ad5fd30dab1c976e2344ffe7f792a6a6df42" + integrity sha1-jGStX9MNqxyXbiNE/+f3kqam30I= + +require-from-string@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/require-from-string/-/require-from-string-2.0.2.tgz#89a7fdd938261267318eafe14f9c32e598c36909" + integrity sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw== + +require-main-filename@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/require-main-filename/-/require-main-filename-2.0.0.tgz#d0b329ecc7cc0f61649f62215be69af54aa8989b" + integrity sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg== + +requires-port@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/requires-port/-/requires-port-1.0.0.tgz#925d2601d39ac485e091cf0da5c6e694dc3dcaff" + integrity sha1-kl0mAdOaxIXgkc8NpcbmlNw9yv8= + +resize-observer-polyfill@^1.5.0, resize-observer-polyfill@^1.5.1: + version "1.5.1" + resolved "https://registry.yarnpkg.com/resize-observer-polyfill/-/resize-observer-polyfill-1.5.1.tgz#0e9020dd3d21024458d4ebd27e23e40269810464" + integrity sha512-LwZrotdHOo12nQuZlHEmtuXdqGoOD0OhaxopaNFxWzInpEgaLWoVuAMbTzixuosCx2nEG58ngzW3vxdWoxIgdg== + +resolve-cwd@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/resolve-cwd/-/resolve-cwd-2.0.0.tgz#00a9f7387556e27038eae232caa372a6a59b665a" + integrity sha1-AKn3OHVW4nA46uIyyqNypqWbZlo= + dependencies: + resolve-from "^3.0.0" + +resolve-cwd@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/resolve-cwd/-/resolve-cwd-3.0.0.tgz#0f0075f1bb2544766cf73ba6a6e2adfebcb13f2d" + integrity sha512-OrZaX2Mb+rJCpH/6CpSqt9xFVpN++x01XnN2ie9g6P5/3xelLAkXWVADpdz1IHD/KFfEXyE6V0U01OQ3UO2rEg== + dependencies: + resolve-from "^5.0.0" + +resolve-from@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-3.0.0.tgz#b22c7af7d9d6881bc8b6e653335eebcb0a188748" + integrity sha1-six699nWiBvItuZTM17rywoYh0g= + +resolve-from@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-4.0.0.tgz#4abcd852ad32dd7baabfe9b40e00a36db5f392e6" + integrity sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g== + +resolve-from@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-5.0.0.tgz#c35225843df8f776df21c57557bc087e9dfdfc69" + integrity sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw== + +resolve-pathname@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/resolve-pathname/-/resolve-pathname-3.0.0.tgz#99d02224d3cf263689becbb393bc560313025dcd" + integrity sha512-C7rARubxI8bXFNB/hqcp/4iUeIXJhJZvFPFPiSPRnhU5UPxzMFIl+2E6yY6c4k9giDJAhtV+enfA+G89N6Csng== + +resolve-url@^0.2.1: + version "0.2.1" + resolved "https://registry.yarnpkg.com/resolve-url/-/resolve-url-0.2.1.tgz#2c637fe77c893afd2a663fe21aa9080068e2052a" + integrity sha1-LGN/53yJOv0qZj/iGqkIAGjiBSo= + +resolve@^1.1.7, resolve@^1.10.0, resolve@^1.13.1, resolve@^1.17.0, resolve@^1.18.1, resolve@^1.9.0: + version "1.19.0" + resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.19.0.tgz#1af5bf630409734a067cae29318aac7fa29a267c" + integrity sha512-rArEXAgsBG4UgRGcynxWIWKFvh/XZCcS8UJdHhwy91zwAvCZIbcs+vAbflgBnNjYMs/i/i+/Ux6IZhML1yPvxg== + dependencies: + is-core-module "^2.1.0" + path-parse "^1.0.6" + +ret@~0.1.10: + version "0.1.15" + resolved "https://registry.yarnpkg.com/ret/-/ret-0.1.15.tgz#b8a4825d5bdb1fc3f6f53c2bc33f81388681c7bc" + integrity sha512-TTlYpa+OL+vMMNG24xSlQGEJ3B/RzEfUlLct7b5G/ytav+wPrplCpVMFuwzXbkecJrb6IYo1iFb0S9v37754mg== + +retry@^0.12.0: + version "0.12.0" + resolved "https://registry.yarnpkg.com/retry/-/retry-0.12.0.tgz#1b42a6266a21f07421d1b0b54b7dc167b01c013b" + integrity sha1-G0KmJmoh8HQh0bC1S33BZ7AcATs= + +reusify@^1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/reusify/-/reusify-1.0.4.tgz#90da382b1e126efc02146e90845a88db12925d76" + integrity sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw== + +rgb-regex@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/rgb-regex/-/rgb-regex-1.0.1.tgz#c0e0d6882df0e23be254a475e8edd41915feaeb1" + integrity sha1-wODWiC3w4jviVKR16O3UGRX+rrE= + +rgba-regex@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/rgba-regex/-/rgba-regex-1.0.0.tgz#43374e2e2ca0968b0ef1523460b7d730ff22eeb3" + integrity sha1-QzdOLiyglosO8VI0YLfXMP8i7rM= + +rimraf@^2.6.3, rimraf@^2.7.1: + version "2.7.1" + resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.7.1.tgz#35797f13a7fdadc566142c29d4f07ccad483e3ec" + integrity sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w== + dependencies: + glob "^7.1.3" + +rimraf@^3.0.2: + version "3.0.2" + resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-3.0.2.tgz#f1a5402ba6220ad52cc1282bac1ae3aa49fd061a" + integrity sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA== + dependencies: + glob "^7.1.3" + +run-parallel@^1.1.9: + version "1.1.10" + resolved "https://registry.yarnpkg.com/run-parallel/-/run-parallel-1.1.10.tgz#60a51b2ae836636c81377df16cb107351bcd13ef" + integrity sha512-zb/1OuZ6flOlH6tQyMPUrE3x3Ulxjlo9WIVXR4yVYi4H9UXQaeIsPbLn2R3O3vQCnDKkAl2qHiuocKKX4Tz/Sw== + +safe-buffer@5.1.2, safe-buffer@~5.1.0, safe-buffer@~5.1.1: + version "5.1.2" + resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d" + integrity sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g== + +safe-buffer@>=5.1.0, safe-buffer@^5.0.1, safe-buffer@^5.1.0, safe-buffer@~5.2.0: + version "5.2.1" + resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6" + integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ== + +safe-regex@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/safe-regex/-/safe-regex-1.1.0.tgz#40a3669f3b077d1e943d44629e157dd48023bf2e" + integrity sha1-QKNmnzsHfR6UPURinhV91IAjvy4= + dependencies: + ret "~0.1.10" + +"safer-buffer@>= 2.1.2 < 3": + version "2.1.2" + resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a" + integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg== + +sax@~1.2.4: + version "1.2.4" + resolved "https://registry.yarnpkg.com/sax/-/sax-1.2.4.tgz#2816234e2378bddc4e5354fab5caa895df7100d9" + integrity sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw== + +scheduler@^0.20.1: + version "0.20.1" + resolved "https://registry.yarnpkg.com/scheduler/-/scheduler-0.20.1.tgz#da0b907e24026b01181ecbc75efdc7f27b5a000c" + integrity sha512-LKTe+2xNJBNxu/QhHvDR14wUXHRQbVY5ZOYpOGWRzhydZUqrLb2JBvLPY7cAqFmqrWuDED0Mjk7013SZiOz6Bw== + dependencies: + loose-envify "^1.1.0" + object-assign "^4.1.1" + +schema-utils@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/schema-utils/-/schema-utils-1.0.0.tgz#0b79a93204d7b600d4b2850d1f66c2a34951c770" + integrity sha512-i27Mic4KovM/lnGsy8whRCHhc7VicJajAjTrYg11K9zfZXnYIt4k5F+kZkwjnrhKzLic/HLU4j11mjsz2G/75g== + dependencies: + ajv "^6.1.0" + ajv-errors "^1.0.0" + ajv-keywords "^3.1.0" + +schema-utils@^2.6.5: + version "2.7.1" + resolved "https://registry.yarnpkg.com/schema-utils/-/schema-utils-2.7.1.tgz#1ca4f32d1b24c590c203b8e7a50bf0ea4cd394d7" + integrity sha512-SHiNtMOUGWBQJwzISiVYKu82GiV4QYGePp3odlY1tuKO7gPtphAT5R/py0fA6xtbgLL/RvtJZnU9b8s0F1q0Xg== + dependencies: + "@types/json-schema" "^7.0.5" + ajv "^6.12.4" + ajv-keywords "^3.5.2" + +schema-utils@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/schema-utils/-/schema-utils-3.0.0.tgz#67502f6aa2b66a2d4032b4279a2944978a0913ef" + integrity sha512-6D82/xSzO094ajanoOSbe4YvXWMfn2A//8Y1+MUqFAJul5Bs+yn36xbK9OtNDcRVSBJ9jjeoXftM6CfztsjOAA== + dependencies: + "@types/json-schema" "^7.0.6" + ajv "^6.12.5" + ajv-keywords "^3.5.2" + +scroll-into-view-if-needed@^2.2.25: + version "2.2.26" + resolved "https://registry.yarnpkg.com/scroll-into-view-if-needed/-/scroll-into-view-if-needed-2.2.26.tgz#e4917da0c820135ff65ad6f7e4b7d7af568c4f13" + integrity sha512-SQ6AOKfABaSchokAmmaxVnL9IArxEnLEX9j4wAZw+x4iUTb40q7irtHG3z4GtAWz5veVZcCnubXDBRyLVQaohw== + dependencies: + compute-scroll-into-view "^1.0.16" + +select-hose@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/select-hose/-/select-hose-2.0.0.tgz#625d8658f865af43ec962bfc376a37359a4994ca" + integrity sha1-Yl2GWPhlr0Psliv8N2o3NZpJlMo= + +selfsigned@^1.10.8: + version "1.10.8" + resolved "https://registry.yarnpkg.com/selfsigned/-/selfsigned-1.10.8.tgz#0d17208b7d12c33f8eac85c41835f27fc3d81a30" + integrity sha512-2P4PtieJeEwVgTU9QEcwIRDQ/mXJLX8/+I3ur+Pg16nS8oNbrGxEso9NyYWy8NAmXiNl4dlAp5MwoNeCWzON4w== + dependencies: + node-forge "^0.10.0" + +"semver@2 || 3 || 4 || 5", semver@^5.4.1, semver@^5.5.0, semver@^5.6.0: + version "5.7.1" + resolved "https://registry.yarnpkg.com/semver/-/semver-5.7.1.tgz#a954f931aeba508d307bbf069eff0c01c96116f7" + integrity sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ== + +semver@^6.0.0, semver@^6.3.0: + version "6.3.0" + resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.0.tgz#ee0a64c8af5e8ceea67687b133761e1becbd1d3d" + integrity sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw== + +semver@^7.2.1, semver@^7.3.2, semver@^7.3.4: + version "7.3.4" + resolved "https://registry.yarnpkg.com/semver/-/semver-7.3.4.tgz#27aaa7d2e4ca76452f98d3add093a72c943edc97" + integrity sha512-tCfb2WLjqFAtXn4KEdxIhalnRtoKFN7nAwj0B3ZXCbQloV2tq5eDbcTmT68JJD3nRJq24/XgxtQKFIpQdtvmVw== + dependencies: + lru-cache "^6.0.0" + +send@0.17.1: + version "0.17.1" + resolved "https://registry.yarnpkg.com/send/-/send-0.17.1.tgz#c1d8b059f7900f7466dd4938bdc44e11ddb376c8" + integrity sha512-BsVKsiGcQMFwT8UxypobUKyv7irCNRHk1T0G680vk88yf6LBByGcZJOTJCrTP2xVN6yI+XjPJcNuE3V4fT9sAg== + dependencies: + debug "2.6.9" + depd "~1.1.2" + destroy "~1.0.4" + encodeurl "~1.0.2" + escape-html "~1.0.3" + etag "~1.8.1" + fresh "0.5.2" + http-errors "~1.7.2" + mime "1.6.0" + ms "2.1.1" + on-finished "~2.3.0" + range-parser "~1.2.1" + statuses "~1.5.0" + +serialize-javascript@^5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/serialize-javascript/-/serialize-javascript-5.0.1.tgz#7886ec848049a462467a97d3d918ebb2aaf934f4" + integrity sha512-SaaNal9imEO737H2c05Og0/8LUXG7EnsZyMa8MzkmuHoELfT6txuj0cMqRj6zfPKnmQ1yasR4PCJc8x+M4JSPA== + dependencies: + randombytes "^2.1.0" + +serve-index@^1.9.1: + version "1.9.1" + resolved "https://registry.yarnpkg.com/serve-index/-/serve-index-1.9.1.tgz#d3768d69b1e7d82e5ce050fff5b453bea12a9239" + integrity sha1-03aNabHn2C5c4FD/9bRTvqEqkjk= + dependencies: + accepts "~1.3.4" + batch "0.6.1" + debug "2.6.9" + escape-html "~1.0.3" + http-errors "~1.6.2" + mime-types "~2.1.17" + parseurl "~1.3.2" + +serve-static@1.14.1: + version "1.14.1" + resolved "https://registry.yarnpkg.com/serve-static/-/serve-static-1.14.1.tgz#666e636dc4f010f7ef29970a88a674320898b2f9" + integrity sha512-JMrvUwE54emCYWlTI+hGrGv5I8dEwmco/00EvkzIIsR7MqrHonbD9pO2MOfFnpFntl7ecpZs+3mW+XbQZu9QCg== + dependencies: + encodeurl "~1.0.2" + escape-html "~1.0.3" + parseurl "~1.3.3" + send "0.17.1" + +set-blocking@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/set-blocking/-/set-blocking-2.0.0.tgz#045f9782d011ae9a6803ddd382b24392b3d890f7" + integrity sha1-BF+XgtARrppoA93TgrJDkrPYkPc= + +set-value@^2.0.0, set-value@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/set-value/-/set-value-2.0.1.tgz#a18d40530e6f07de4228c7defe4227af8cad005b" + integrity sha512-JxHc1weCN68wRY0fhCoXpyK55m/XPHafOmK4UWD7m2CI14GMcFypt4w/0+NV5f/ZMby2F6S2wwA7fgynh9gWSw== + dependencies: + extend-shallow "^2.0.1" + is-extendable "^0.1.1" + is-plain-object "^2.0.3" + split-string "^3.0.1" + +setprototypeof@1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.1.0.tgz#d0bd85536887b6fe7c0d818cb962d9d91c54e656" + integrity sha512-BvE/TwpZX4FXExxOxZyRGQQv651MSwmWKZGqvmPcRIjDqWub67kTKuIMx43cZZrS/cBBzwBcNDWoFxt2XEFIpQ== + +setprototypeof@1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.1.1.tgz#7e95acb24aa92f5885e0abef5ba131330d4ae683" + integrity sha512-JvdAWfbXeIGaZ9cILp38HntZSFSo3mWg6xGcJJsd+d4aRMOqauag1C63dJfDw7OaMYwEbHMOxEZ1lqVRYP2OAw== + +shallow-clone@^3.0.0: + version "3.0.1" + resolved "https://registry.yarnpkg.com/shallow-clone/-/shallow-clone-3.0.1.tgz#8f2981ad92531f55035b01fb230769a40e02efa3" + integrity sha512-/6KqX+GVUdqPuPPd2LxDDxzX6CAbjJehAAOKlNpqqUpAqPM6HeL8f+o3a+JsyGjn2lv0WY8UsTgUJjU9Ok55NA== + dependencies: + kind-of "^6.0.2" + +shallowequal@^1.0.2, shallowequal@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/shallowequal/-/shallowequal-1.1.0.tgz#188d521de95b9087404fd4dcb68b13df0ae4e7f8" + integrity sha512-y0m1JoUZSlPAjXVtPPW70aZWfIL/dSP7AFkRnniLCrK/8MDKog3TySTBmckD+RObVxH0v4Tox67+F14PdED2oQ== + +shebang-command@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/shebang-command/-/shebang-command-1.2.0.tgz#44aac65b695b03398968c39f363fee5deafdf1ea" + integrity sha1-RKrGW2lbAzmJaMOfNj/uXer98eo= + dependencies: + shebang-regex "^1.0.0" + +shebang-command@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/shebang-command/-/shebang-command-2.0.0.tgz#ccd0af4f8835fbdc265b82461aaf0c36663f34ea" + integrity sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA== + dependencies: + shebang-regex "^3.0.0" + +shebang-regex@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/shebang-regex/-/shebang-regex-1.0.0.tgz#da42f49740c0b42db2ca9728571cb190c98efea3" + integrity sha1-2kL0l0DAtC2yypcoVxyxkMmO/qM= + +shebang-regex@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/shebang-regex/-/shebang-regex-3.0.0.tgz#ae16f1644d873ecad843b0307b143362d4c42172" + integrity sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A== + +side-channel@^1.0.3, side-channel@^1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/side-channel/-/side-channel-1.0.4.tgz#efce5c8fdc104ee751b25c58d4290011fa5ea2cf" + integrity sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw== + dependencies: + call-bind "^1.0.0" + get-intrinsic "^1.0.2" + object-inspect "^1.9.0" + +signal-exit@^3.0.0, signal-exit@^3.0.2, signal-exit@^3.0.3: + version "3.0.3" + resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.3.tgz#a1410c2edd8f077b08b4e253c8eacfcaf057461c" + integrity sha512-VUJ49FC8U1OxwZLxIbTTrDvLnf/6TDgxZcK8wxR8zs13xpx7xbG60ndBlhNrFi2EMuFRoeDoJO7wthSLq42EjA== + +simple-swizzle@^0.2.2: + version "0.2.2" + resolved "https://registry.yarnpkg.com/simple-swizzle/-/simple-swizzle-0.2.2.tgz#a4da6b635ffcccca33f70d17cb92592de95e557a" + integrity sha1-pNprY1/8zMoz9w0Xy5JZLeleVXo= + dependencies: + is-arrayish "^0.3.1" + +slash@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/slash/-/slash-3.0.0.tgz#6539be870c165adbd5240220dbe361f1bc4d4634" + integrity sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q== + +slice-ansi@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/slice-ansi/-/slice-ansi-4.0.0.tgz#500e8dd0fd55b05815086255b3195adf2a45fe6b" + integrity sha512-qMCMfhY040cVHT43K9BFygqYbUPFZKHOg7K73mtTWJRb8pyP3fzf4Ixd5SzdEJQ6MRUg/WBnOLxghZtKKurENQ== + dependencies: + ansi-styles "^4.0.0" + astral-regex "^2.0.0" + is-fullwidth-code-point "^3.0.0" + +snapdragon-node@^2.0.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/snapdragon-node/-/snapdragon-node-2.1.1.tgz#6c175f86ff14bdb0724563e8f3c1b021a286853b" + integrity sha512-O27l4xaMYt/RSQ5TR3vpWCAB5Kb/czIcqUFOM/C4fYcLnbZUc1PkjTAMjof2pBWaSTwOUd6qUHcFGVGj7aIwnw== + dependencies: + define-property "^1.0.0" + isobject "^3.0.0" + snapdragon-util "^3.0.1" + +snapdragon-util@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/snapdragon-util/-/snapdragon-util-3.0.1.tgz#f956479486f2acd79700693f6f7b805e45ab56e2" + integrity sha512-mbKkMdQKsjX4BAL4bRYTj21edOf8cN7XHdYUJEe+Zn99hVEYcMvKPct1IqNe7+AZPirn8BCDOQBHQZknqmKlZQ== + dependencies: + kind-of "^3.2.0" + +snapdragon@^0.8.1: + version "0.8.2" + resolved "https://registry.yarnpkg.com/snapdragon/-/snapdragon-0.8.2.tgz#64922e7c565b0e14204ba1aa7d6964278d25182d" + integrity sha512-FtyOnWN/wCHTVXOMwvSv26d+ko5vWlIDD6zoUJ7LW8vh+ZBC8QdljveRP+crNrtBwioEUWy/4dMtbBjA4ioNlg== + dependencies: + base "^0.11.1" + debug "^2.2.0" + define-property "^0.2.5" + extend-shallow "^2.0.1" + map-cache "^0.2.2" + source-map "^0.5.6" + source-map-resolve "^0.5.0" + use "^3.1.0" + +sockjs-client@^1.5.0: + version "1.5.0" + resolved "https://registry.yarnpkg.com/sockjs-client/-/sockjs-client-1.5.0.tgz#2f8ff5d4b659e0d092f7aba0b7c386bd2aa20add" + integrity sha512-8Dt3BDi4FYNrCFGTL/HtwVzkARrENdwOUf1ZoW/9p3M8lZdFT35jVdrHza+qgxuG9H3/shR4cuX/X9umUrjP8Q== + dependencies: + debug "^3.2.6" + eventsource "^1.0.7" + faye-websocket "^0.11.3" + inherits "^2.0.4" + json3 "^3.3.3" + url-parse "^1.4.7" + +sockjs@^0.3.21: + version "0.3.21" + resolved "https://registry.yarnpkg.com/sockjs/-/sockjs-0.3.21.tgz#b34ffb98e796930b60a0cfa11904d6a339a7d417" + integrity sha512-DhbPFGpxjc6Z3I+uX07Id5ZO2XwYsWOrYjaSeieES78cq+JaJvVe5q/m1uvjIQhXinhIeCFRH6JgXe+mvVMyXw== + dependencies: + faye-websocket "^0.11.3" + uuid "^3.4.0" + websocket-driver "^0.7.4" + +source-list-map@^2.0.0, source-list-map@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/source-list-map/-/source-list-map-2.0.1.tgz#3993bd873bfc48479cca9ea3a547835c7c154b34" + integrity sha512-qnQ7gVMxGNxsiL4lEuJwe/To8UnK7fAnmbGEEH8RpLouuKbeEm0lhbQVFIrNSuB+G7tVrAlVsZgETT5nljf+Iw== + +source-map-resolve@^0.5.0: + version "0.5.3" + resolved "https://registry.yarnpkg.com/source-map-resolve/-/source-map-resolve-0.5.3.tgz#190866bece7553e1f8f267a2ee82c606b5509a1a" + integrity sha512-Htz+RnsXWk5+P2slx5Jh3Q66vhQj1Cllm0zvnaY98+NFx+Dv2CF/f5O/t8x+KaNdrdIAsruNzoh/KpialbqAnw== + dependencies: + atob "^2.1.2" + decode-uri-component "^0.2.0" + resolve-url "^0.2.1" + source-map-url "^0.4.0" + urix "^0.1.0" + +source-map-support@^0.5.17, source-map-support@~0.5.12, source-map-support@~0.5.19: + version "0.5.19" + resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.5.19.tgz#a98b62f86dcaf4f67399648c085291ab9e8fed61" + integrity sha512-Wonm7zOCIJzBGQdB+thsPar0kYuCIzYvxZwlBa87yi/Mdjv7Tip2cyVbLj5o0cFPN4EVkuTwb3GDDyUx2DGnGw== + dependencies: + buffer-from "^1.0.0" + source-map "^0.6.0" + +source-map-url@^0.4.0: + version "0.4.0" + resolved "https://registry.yarnpkg.com/source-map-url/-/source-map-url-0.4.0.tgz#3e935d7ddd73631b97659956d55128e87b5084a3" + integrity sha1-PpNdfd1zYxuXZZlW1VEo6HtQhKM= + +source-map@^0.5.0, source-map@^0.5.6: + version "0.5.7" + resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.5.7.tgz#8a039d2d1021d22d1ea14c80d8ea468ba2ef3fcc" + integrity sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w= + +source-map@^0.6.0, source-map@^0.6.1, source-map@~0.6.0, source-map@~0.6.1: + version "0.6.1" + resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.6.1.tgz#74722af32e9614e9c287a8d0bbde48b5e2f1a263" + integrity sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g== + +source-map@^0.7.3, source-map@~0.7.2: + version "0.7.3" + resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.7.3.tgz#5302f8169031735226544092e64981f751750383" + integrity sha512-CkCj6giN3S+n9qrYiBTX5gystlENnRW5jZeNLHpe6aue+SrHcG5VYwujhW9s4dY31mEGsxBDrHR6oI69fTXsaQ== + +spdx-correct@^3.0.0: + version "3.1.1" + resolved "https://registry.yarnpkg.com/spdx-correct/-/spdx-correct-3.1.1.tgz#dece81ac9c1e6713e5f7d1b6f17d468fa53d89a9" + integrity sha512-cOYcUWwhCuHCXi49RhFRCyJEK3iPj1Ziz9DpViV3tbZOwXD49QzIN3MpOLJNxh2qwq2lJJZaKMVw9qNi4jTC0w== + dependencies: + spdx-expression-parse "^3.0.0" + spdx-license-ids "^3.0.0" + +spdx-exceptions@^2.1.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/spdx-exceptions/-/spdx-exceptions-2.3.0.tgz#3f28ce1a77a00372683eade4a433183527a2163d" + integrity sha512-/tTrYOC7PPI1nUAgx34hUpqXuyJG+DTHJTnIULG4rDygi4xu/tfgmq1e1cIRwRzwZgo4NLySi+ricLkZkw4i5A== + +spdx-expression-parse@^3.0.0: + version "3.0.1" + resolved "https://registry.yarnpkg.com/spdx-expression-parse/-/spdx-expression-parse-3.0.1.tgz#cf70f50482eefdc98e3ce0a6833e4a53ceeba679" + integrity sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q== + dependencies: + spdx-exceptions "^2.1.0" + spdx-license-ids "^3.0.0" + +spdx-license-ids@^3.0.0: + version "3.0.7" + resolved "https://registry.yarnpkg.com/spdx-license-ids/-/spdx-license-ids-3.0.7.tgz#e9c18a410e5ed7e12442a549fbd8afa767038d65" + integrity sha512-U+MTEOO0AiDzxwFvoa4JVnMV6mZlJKk2sBLt90s7G0Gd0Mlknc7kxEn3nuDPNZRta7O2uy8oLcZLVT+4sqNZHQ== + +spdy-transport@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/spdy-transport/-/spdy-transport-3.0.0.tgz#00d4863a6400ad75df93361a1608605e5dcdcf31" + integrity sha512-hsLVFE5SjA6TCisWeJXFKniGGOpBgMLmerfO2aCyCU5s7nJ/rpAepqmFifv/GCbSbueEeAJJnmSQ2rKC/g8Fcw== + dependencies: + debug "^4.1.0" + detect-node "^2.0.4" + hpack.js "^2.1.6" + obuf "^1.1.2" + readable-stream "^3.0.6" + wbuf "^1.7.3" + +spdy@^4.0.2: + version "4.0.2" + resolved "https://registry.yarnpkg.com/spdy/-/spdy-4.0.2.tgz#b74f466203a3eda452c02492b91fb9e84a27677b" + integrity sha512-r46gZQZQV+Kl9oItvl1JZZqJKGr+oEkB08A6BzkiR7593/7IbtuncXHd2YoYeTsG4157ZssMu9KYvUHLcjcDoA== + dependencies: + debug "^4.1.0" + handle-thing "^2.0.0" + http-deceiver "^1.2.7" + select-hose "^2.0.0" + spdy-transport "^3.0.0" + +specificity@^0.4.1: + version "0.4.1" + resolved "https://registry.yarnpkg.com/specificity/-/specificity-0.4.1.tgz#aab5e645012db08ba182e151165738d00887b019" + integrity sha512-1klA3Gi5PD1Wv9Q0wUoOQN1IWAuPu0D1U03ThXTr0cJ20+/iq2tHSDnK7Kk/0LXJ1ztUB2/1Os0wKmfyNgUQfg== + +split-string@^3.0.1, split-string@^3.0.2: + version "3.1.0" + resolved "https://registry.yarnpkg.com/split-string/-/split-string-3.1.0.tgz#7cb09dda3a86585705c64b39a6466038682e8fe2" + integrity sha512-NzNVhJDYpwceVVii8/Hu6DKfD2G+NrQHlS/V/qgv763EYudVwEcMQNxd2lh+0VrUByXN/oJkl5grOhYWvQUYiw== + dependencies: + extend-shallow "^3.0.0" + +sprintf-js@~1.0.2: + version "1.0.3" + resolved "https://registry.yarnpkg.com/sprintf-js/-/sprintf-js-1.0.3.tgz#04e6926f662895354f3dd015203633b857297e2c" + integrity sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw= + +ssri@^8.0.0: + version "8.0.0" + resolved "https://registry.yarnpkg.com/ssri/-/ssri-8.0.0.tgz#79ca74e21f8ceaeddfcb4b90143c458b8d988808" + integrity sha512-aq/pz989nxVYwn16Tsbj1TqFpD5LLrQxHf5zaHuieFV+R0Bbr4y8qUsOA45hXT/N4/9UNXTarBjnjVmjSOVaAA== + dependencies: + minipass "^3.1.1" + +stable@^0.1.8: + version "0.1.8" + resolved "https://registry.yarnpkg.com/stable/-/stable-0.1.8.tgz#836eb3c8382fe2936feaf544631017ce7d47a3cf" + integrity sha512-ji9qxRnOVfcuLDySj9qzhGSEFVobyt1kIOSkj1qZzYLzq7Tos/oUUWvotUPQLlrsidqsK6tBH89Bc9kL5zHA6w== + +static-extend@^0.1.1: + version "0.1.2" + resolved "https://registry.yarnpkg.com/static-extend/-/static-extend-0.1.2.tgz#60809c39cbff55337226fd5e0b520f341f1fb5c6" + integrity sha1-YICcOcv/VTNyJv1eC1IPNB8ftcY= + dependencies: + define-property "^0.2.5" + object-copy "^0.1.0" + +"statuses@>= 1.4.0 < 2", "statuses@>= 1.5.0 < 2", statuses@~1.5.0: + version "1.5.0" + resolved "https://registry.yarnpkg.com/statuses/-/statuses-1.5.0.tgz#161c7dac177659fd9811f43771fa99381478628c" + integrity sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow= + +string-convert@^0.2.0: + version "0.2.1" + resolved "https://registry.yarnpkg.com/string-convert/-/string-convert-0.2.1.tgz#6982cc3049fbb4cd85f8b24568b9d9bf39eeff97" + integrity sha1-aYLMMEn7tM2F+LJFaLnZvznu/5c= + +string-hash@^1.1.1: + version "1.1.3" + resolved "https://registry.yarnpkg.com/string-hash/-/string-hash-1.1.3.tgz#e8aafc0ac1855b4666929ed7dd1275df5d6c811b" + integrity sha1-6Kr8CsGFW0Zmkp7X3RJ1311sgRs= + +string-width@^3.0.0, string-width@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/string-width/-/string-width-3.1.0.tgz#22767be21b62af1081574306f69ac51b62203961" + integrity sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w== + dependencies: + emoji-regex "^7.0.1" + is-fullwidth-code-point "^2.0.0" + strip-ansi "^5.1.0" + +string-width@^4.2.0: + version "4.2.0" + resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.0.tgz#952182c46cc7b2c313d1596e623992bd163b72b5" + integrity sha512-zUz5JD+tgqtuDjMhwIg5uFVV3dtqZ9yQJlZVfq4I01/K5Paj5UHj7VyrQOJvzawSVlKpObApbfD0Ed6yJc+1eg== + dependencies: + emoji-regex "^8.0.0" + is-fullwidth-code-point "^3.0.0" + strip-ansi "^6.0.0" + +string.prototype.matchall@^4.0.2: + version "4.0.3" + resolved "https://registry.yarnpkg.com/string.prototype.matchall/-/string.prototype.matchall-4.0.3.tgz#24243399bc31b0a49d19e2b74171a15653ec996a" + integrity sha512-OBxYDA2ifZQ2e13cP82dWFMaCV9CGF8GzmN4fljBVw5O5wep0lu4gacm1OL6MjROoUnB8VbkWRThqkV2YFLNxw== + dependencies: + call-bind "^1.0.0" + define-properties "^1.1.3" + es-abstract "^1.18.0-next.1" + has-symbols "^1.0.1" + internal-slot "^1.0.2" + regexp.prototype.flags "^1.3.0" + side-channel "^1.0.3" + +string.prototype.trimend@^1.0.1, string.prototype.trimend@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/string.prototype.trimend/-/string.prototype.trimend-1.0.3.tgz#a22bd53cca5c7cf44d7c9d5c732118873d6cd18b" + integrity sha512-ayH0pB+uf0U28CtjlLvL7NaohvR1amUvVZk+y3DYb0Ey2PUV5zPkkKy9+U1ndVEIXO8hNg18eIv9Jntbii+dKw== + dependencies: + call-bind "^1.0.0" + define-properties "^1.1.3" + +string.prototype.trimstart@^1.0.1, string.prototype.trimstart@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/string.prototype.trimstart/-/string.prototype.trimstart-1.0.3.tgz#9b4cb590e123bb36564401d59824298de50fd5aa" + integrity sha512-oBIBUy5lea5tt0ovtOFiEQaBkoBBkyJhZXzJYrSmDo5IUUqbOPvVezuRs/agBIdZ2p2Eo1FD6bD9USyBLfl3xg== + dependencies: + call-bind "^1.0.0" + define-properties "^1.1.3" + +string_decoder@^1.1.1: + version "1.3.0" + resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.3.0.tgz#42f114594a46cf1a8e30b0a84f56c78c3edac21e" + integrity sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA== + dependencies: + safe-buffer "~5.2.0" + +string_decoder@~1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.1.1.tgz#9cf1611ba62685d7030ae9e4ba34149c3af03fc8" + integrity sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg== + dependencies: + safe-buffer "~5.1.0" + +strip-ansi@^3.0.0, strip-ansi@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-3.0.1.tgz#6a385fb8853d952d5ff05d0e8aaf94278dc63dcf" + integrity sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8= + dependencies: + ansi-regex "^2.0.0" + +strip-ansi@^5.0.0, strip-ansi@^5.1.0, strip-ansi@^5.2.0: + version "5.2.0" + resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-5.2.0.tgz#8c9a536feb6afc962bdfa5b104a5091c1ad9c0ae" + integrity sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA== + dependencies: + ansi-regex "^4.1.0" + +strip-ansi@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.0.tgz#0b1571dd7669ccd4f3e06e14ef1eed26225ae532" + integrity sha512-AuvKTrTfQNYNIctbR1K/YGTR1756GycPsg7b9bdV9Duqur4gv6aKqHXah67Z8ImS7WEz5QVcOtlfW2rZEugt6w== + dependencies: + ansi-regex "^5.0.0" + +strip-bom@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/strip-bom/-/strip-bom-3.0.0.tgz#2334c18e9c759f7bdd56fdef7e9ae3d588e68ed3" + integrity sha1-IzTBjpx1n3vdVv3vfprj1YjmjtM= + +strip-eof@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/strip-eof/-/strip-eof-1.0.0.tgz#bb43ff5598a6eb05d89b59fcd129c983313606bf" + integrity sha1-u0P/VZim6wXYm1n80SnJgzE2Br8= + +strip-final-newline@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/strip-final-newline/-/strip-final-newline-2.0.0.tgz#89b852fb2fcbe936f6f4b3187afb0a12c1ab58ad" + integrity sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA== + +strip-indent@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/strip-indent/-/strip-indent-3.0.0.tgz#c32e1cee940b6b3432c771bc2c54bcce73cd3001" + integrity sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ== + dependencies: + min-indent "^1.0.0" + +strip-json-comments@^3.1.0, strip-json-comments@^3.1.1: + version "3.1.1" + resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-3.1.1.tgz#31f1281b3832630434831c310c01cccda8cbe006" + integrity sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig== + +style-loader@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/style-loader/-/style-loader-2.0.0.tgz#9669602fd4690740eaaec137799a03addbbc393c" + integrity sha512-Z0gYUJmzZ6ZdRUqpg1r8GsaFKypE+3xAzuFeMuoHgjc9KZv3wMyCRjQIWEbhoFSq7+7yoHXySDJyyWQaPajeiQ== + dependencies: + loader-utils "^2.0.0" + schema-utils "^3.0.0" + +style-search@^0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/style-search/-/style-search-0.1.0.tgz#7958c793e47e32e07d2b5cafe5c0bf8e12e77902" + integrity sha1-eVjHk+R+MuB9K1yv5cC/jhLneQI= + +stylehacks@^4.0.0: + version "4.0.3" + resolved "https://registry.yarnpkg.com/stylehacks/-/stylehacks-4.0.3.tgz#6718fcaf4d1e07d8a1318690881e8d96726a71d5" + integrity sha512-7GlLk9JwlElY4Y6a/rmbH2MhVlTyVmiJd1PfTCqFaIBEGMYNsrO/v3SeGTdhBThLg4Z+NbOk/qFMwCa+J+3p/g== + dependencies: + browserslist "^4.0.0" + postcss "^7.0.0" + postcss-selector-parser "^3.0.0" + +stylelint-webpack-plugin@^2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/stylelint-webpack-plugin/-/stylelint-webpack-plugin-2.1.1.tgz#1c8ae72a4e5818f7e5925e3fff10502ad34a52a5" + integrity sha512-WHdaWCp4NANcTcltuRjZCjM7jVhdaSg7ag/sQLE22Bf84g5nQC4nBBK8FBdHAssJsho0fDRiwyrzGsIPO+b94A== + dependencies: + arrify "^2.0.1" + micromatch "^4.0.2" + schema-utils "^3.0.0" + +stylelint@^13.7.2: + version "13.9.0" + resolved "https://registry.yarnpkg.com/stylelint/-/stylelint-13.9.0.tgz#93921ee6e11d4556b9f31131f485dc813b68e32a" + integrity sha512-VVWH2oixOAxpWL1vH+V42ReCzBjW2AeqskSAbi8+3OjV1Xg3VZkmTcAqBZfRRvJeF4BvYuDLXebW3tIHxgZDEg== + dependencies: + "@stylelint/postcss-css-in-js" "^0.37.2" + "@stylelint/postcss-markdown" "^0.36.2" + autoprefixer "^9.8.6" + balanced-match "^1.0.0" + chalk "^4.1.0" + cosmiconfig "^7.0.0" + debug "^4.3.1" + execall "^2.0.0" + fast-glob "^3.2.5" + fastest-levenshtein "^1.0.12" + file-entry-cache "^6.0.0" + get-stdin "^8.0.0" + global-modules "^2.0.0" + globby "^11.0.2" + globjoin "^0.1.4" + html-tags "^3.1.0" + ignore "^5.1.8" + import-lazy "^4.0.0" + imurmurhash "^0.1.4" + known-css-properties "^0.20.0" + lodash "^4.17.20" + log-symbols "^4.0.0" + mathml-tag-names "^2.1.3" + meow "^9.0.0" + micromatch "^4.0.2" + normalize-selector "^0.2.0" + postcss "^7.0.35" + postcss-html "^0.36.0" + postcss-less "^3.1.4" + postcss-media-query-parser "^0.2.3" + postcss-resolve-nested-selector "^0.1.1" + postcss-safe-parser "^4.0.2" + postcss-sass "^0.4.4" + postcss-scss "^2.1.1" + postcss-selector-parser "^6.0.4" + postcss-syntax "^0.36.2" + postcss-value-parser "^4.1.0" + resolve-from "^5.0.0" + slash "^3.0.0" + specificity "^0.4.1" + string-width "^4.2.0" + strip-ansi "^6.0.0" + style-search "^0.1.0" + sugarss "^2.0.0" + svg-tags "^1.0.0" + table "^6.0.7" + v8-compile-cache "^2.2.0" + write-file-atomic "^3.0.3" + +sugarss@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/sugarss/-/sugarss-2.0.0.tgz#ddd76e0124b297d40bf3cca31c8b22ecb43bc61d" + integrity sha512-WfxjozUk0UVA4jm+U1d736AUpzSrNsQcIbyOkoE364GrtWmIrFdk5lksEupgWMD4VaT/0kVx1dobpiDumSgmJQ== + dependencies: + postcss "^7.0.2" + +sugarss@^3.0.3: + version "3.0.3" + resolved "https://registry.yarnpkg.com/sugarss/-/sugarss-3.0.3.tgz#bb2489961b98fbd15e4e35d6b9f4f2ee5547a6cb" + integrity sha512-uxa2bbuc+w7ov7DyYIhF6bM0qZF3UkFT5/nE8AJgboiVnKsBDbwxs++dehEIe1JNhpMaGJc37wGQ2QrrWey2Sg== + dependencies: + postcss "^8.1.6" + +supports-color@^5.3.0, supports-color@^5.4.0: + version "5.5.0" + resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-5.5.0.tgz#e2e69a44ac8772f78a1ec0b35b689df6530efc8f" + integrity sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow== + dependencies: + has-flag "^3.0.0" + +supports-color@^6.1.0: + version "6.1.0" + resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-6.1.0.tgz#0764abc69c63d5ac842dd4867e8d025e880df8f3" + integrity sha512-qe1jfm1Mg7Nq/NSh6XE24gPXROEVsWHxC1LIx//XNlD9iw7YZQGjZNjYN7xGaEG6iKdA8EtNFW6R0gjnVXp+wQ== + dependencies: + has-flag "^3.0.0" + +supports-color@^7.0.0, supports-color@^7.1.0: + version "7.2.0" + resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-7.2.0.tgz#1b7dcdcb32b8138801b3e478ba6a51caa89648da" + integrity sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw== + dependencies: + has-flag "^4.0.0" + +svg-tags@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/svg-tags/-/svg-tags-1.0.0.tgz#58f71cee3bd519b59d4b2a843b6c7de64ac04764" + integrity sha1-WPcc7jvVGbWdSyqEO2x95krAR2Q= + +svgo@^1.0.0: + version "1.3.2" + resolved "https://registry.yarnpkg.com/svgo/-/svgo-1.3.2.tgz#b6dc511c063346c9e415b81e43401145b96d4167" + integrity sha512-yhy/sQYxR5BkC98CY7o31VGsg014AKLEPxdfhora76l36hD9Rdy5NZA/Ocn6yayNPgSamYdtX2rFJdcv07AYVw== + dependencies: + chalk "^2.4.1" + coa "^2.0.2" + css-select "^2.0.0" + css-select-base-adapter "^0.1.1" + css-tree "1.0.0-alpha.37" + csso "^4.0.2" + js-yaml "^3.13.1" + mkdirp "~0.5.1" + object.values "^1.1.0" + sax "~1.2.4" + stable "^0.1.8" + unquote "~1.1.1" + util.promisify "~1.0.0" + +symbol-observable@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/symbol-observable/-/symbol-observable-1.2.0.tgz#c22688aed4eab3cdc2dfeacbb561660560a00804" + integrity sha512-e900nM8RRtGhlV36KGEU9k65K3mPb1WV70OdjfxlG2EAuM1noi/E/BaW/uMhL7bPEssK8QV57vN3esixjUvcXQ== + +table@^6.0.4, table@^6.0.7: + version "6.0.7" + resolved "https://registry.yarnpkg.com/table/-/table-6.0.7.tgz#e45897ffbcc1bcf9e8a87bf420f2c9e5a7a52a34" + integrity sha512-rxZevLGTUzWna/qBLObOe16kB2RTnnbhciwgPbMMlazz1yZGVEgnZK762xyVdVznhqxrfCeBMmMkgOOaPwjH7g== + dependencies: + ajv "^7.0.2" + lodash "^4.17.20" + slice-ansi "^4.0.0" + string-width "^4.2.0" + +tapable@^1.0.0, tapable@^1.1.3: + version "1.1.3" + resolved "https://registry.yarnpkg.com/tapable/-/tapable-1.1.3.tgz#a1fccc06b58db61fd7a45da2da44f5f3a3e67ba2" + integrity sha512-4WK/bYZmj8xLr+HUCODHGF1ZFzsYffasLUgEiMBY4fgtltdO6B4WJtlSbPaDTLpYTcGVwM2qLnFTICEcNxs3kA== + +tapable@^2.1.1, tapable@^2.2.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/tapable/-/tapable-2.2.0.tgz#5c373d281d9c672848213d0e037d1c4165ab426b" + integrity sha512-FBk4IesMV1rBxX2tfiK8RAmogtWn53puLOQlvO8XuwlgxcYbP4mVPS9Ph4aeamSyyVjOl24aYWAuc8U5kCVwMw== + +tar@^6.0.2: + version "6.1.0" + resolved "https://registry.yarnpkg.com/tar/-/tar-6.1.0.tgz#d1724e9bcc04b977b18d5c573b333a2207229a83" + integrity sha512-DUCttfhsnLCjwoDoFcI+B2iJgYa93vBnDUATYEeRx6sntCTdN01VnqsIuTlALXla/LWooNg0yEGeB+Y8WdFxGA== + dependencies: + chownr "^2.0.0" + fs-minipass "^2.0.0" + minipass "^3.0.0" + minizlib "^2.1.1" + mkdirp "^1.0.3" + yallist "^4.0.0" + +terser-webpack-plugin@^5.0.0, terser-webpack-plugin@^5.1.1: + version "5.1.1" + resolved "https://registry.yarnpkg.com/terser-webpack-plugin/-/terser-webpack-plugin-5.1.1.tgz#7effadee06f7ecfa093dbbd3e9ab23f5f3ed8673" + integrity sha512-5XNNXZiR8YO6X6KhSGXfY0QrGrCRlSwAEjIIrlRQR4W8nP69TaJUlh3bkuac6zzgspiGPfKEHcY295MMVExl5Q== + dependencies: + jest-worker "^26.6.2" + p-limit "^3.1.0" + schema-utils "^3.0.0" + serialize-javascript "^5.0.1" + source-map "^0.6.1" + terser "^5.5.1" + +terser@^4.6.3: + version "4.8.0" + resolved "https://registry.yarnpkg.com/terser/-/terser-4.8.0.tgz#63056343d7c70bb29f3af665865a46fe03a0df17" + integrity sha512-EAPipTNeWsb/3wLPeup1tVPaXfIaU68xMnVdPafIL1TV05OhASArYyIfFvnvJCNrR2NIOvDVNNTFRa+Re2MWyw== + dependencies: + commander "^2.20.0" + source-map "~0.6.1" + source-map-support "~0.5.12" + +terser@^5.5.1: + version "5.5.1" + resolved "https://registry.yarnpkg.com/terser/-/terser-5.5.1.tgz#540caa25139d6f496fdea056e414284886fb2289" + integrity sha512-6VGWZNVP2KTUcltUQJ25TtNjx/XgdDsBDKGt8nN0MpydU36LmbPPcMBd2kmtZNNGVVDLg44k7GKeHHj+4zPIBQ== + dependencies: + commander "^2.20.0" + source-map "~0.7.2" + source-map-support "~0.5.19" + +text-table@^0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/text-table/-/text-table-0.2.0.tgz#7f5ee823ae805207c00af2df4a84ec3fcfa570b4" + integrity sha1-f17oI66AUgfACvLfSoTsP8+lcLQ= + +thunky@^1.0.2: + version "1.1.0" + resolved "https://registry.yarnpkg.com/thunky/-/thunky-1.1.0.tgz#5abaf714a9405db0504732bbccd2cedd9ef9537d" + integrity sha512-eHY7nBftgThBqOyHGVN+l8gF0BucP09fMo0oO/Lb0w1OF80dJv+lDVpXG60WMQvkcxAkNybKsrEIE3ZtKGmPrA== + +timsort@^0.3.0: + version "0.3.0" + resolved "https://registry.yarnpkg.com/timsort/-/timsort-0.3.0.tgz#405411a8e7e6339fe64db9a234de11dc31e02bd4" + integrity sha1-QFQRqOfmM5/mTbmiNN4R3DHgK9Q= + +tiny-invariant@^1.0.2: + version "1.1.0" + resolved "https://registry.yarnpkg.com/tiny-invariant/-/tiny-invariant-1.1.0.tgz#634c5f8efdc27714b7f386c35e6760991d230875" + integrity sha512-ytxQvrb1cPc9WBEI/HSeYYoGD0kWnGEOR8RY6KomWLBVhqz0RgTwVO9dLrGz7dC+nN9llyI7OKAgRq8Vq4ZBSw== + +tiny-warning@^1.0.0, tiny-warning@^1.0.2, tiny-warning@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/tiny-warning/-/tiny-warning-1.0.3.tgz#94a30db453df4c643d0fd566060d60a875d84754" + integrity sha512-lBN9zLN/oAf68o3zNXYrdCt1kP8WsiGW8Oo2ka41b2IM5JL/S1CTyX1rW0mb/zSuJun0ZUrDxx4sqvYS2FWzPA== + +to-fast-properties@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/to-fast-properties/-/to-fast-properties-2.0.0.tgz#dc5e698cbd079265bc73e0377681a4e4e83f616e" + integrity sha1-3F5pjL0HkmW8c+A3doGk5Og/YW4= + +to-object-path@^0.3.0: + version "0.3.0" + resolved "https://registry.yarnpkg.com/to-object-path/-/to-object-path-0.3.0.tgz#297588b7b0e7e0ac08e04e672f85c1f4999e17af" + integrity sha1-KXWIt7Dn4KwI4E5nL4XB9JmeF68= + dependencies: + kind-of "^3.0.2" + +to-regex-range@^2.1.0: + version "2.1.1" + resolved "https://registry.yarnpkg.com/to-regex-range/-/to-regex-range-2.1.1.tgz#7c80c17b9dfebe599e27367e0d4dd5590141db38" + integrity sha1-fIDBe53+vlmeJzZ+DU3VWQFB2zg= + dependencies: + is-number "^3.0.0" + repeat-string "^1.6.1" + +to-regex-range@^5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/to-regex-range/-/to-regex-range-5.0.1.tgz#1648c44aae7c8d988a326018ed72f5b4dd0392e4" + integrity sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ== + dependencies: + is-number "^7.0.0" + +to-regex@^3.0.1, to-regex@^3.0.2: + version "3.0.2" + resolved "https://registry.yarnpkg.com/to-regex/-/to-regex-3.0.2.tgz#13cfdd9b336552f30b51f33a8ae1b42a7a7599ce" + integrity sha512-FWtleNAtZ/Ki2qtqej2CXTOayOH9bHDQF+Q48VpWyDXjbYxA4Yz8iDB31zXOBUlOHHKidDbqGVrTUvQMPmBGBw== + dependencies: + define-property "^2.0.2" + extend-shallow "^3.0.2" + regex-not "^1.0.2" + safe-regex "^1.1.0" + +toggle-selection@^1.0.6: + version "1.0.6" + resolved "https://registry.yarnpkg.com/toggle-selection/-/toggle-selection-1.0.6.tgz#6e45b1263f2017fa0acc7d89d78b15b8bf77da32" + integrity sha1-bkWxJj8gF/oKzH2J14sVuL932jI= + +toidentifier@1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/toidentifier/-/toidentifier-1.0.0.tgz#7e1be3470f1e77948bc43d94a3c8f4d7752ba553" + integrity sha512-yaOH/Pk/VEhBWWTlhI+qXxDFXlejDGcQipMlyxda9nthulaxLZUNcUqFxokp0vcYnvteJln5FNQDRrxj3YcbVw== + +trim-newlines@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/trim-newlines/-/trim-newlines-3.0.0.tgz#79726304a6a898aa8373427298d54c2ee8b1cb30" + integrity sha512-C4+gOpvmxaSMKuEf9Qc134F1ZuOHVXKRbtEflf4NTtuuJDEIJ9p5PXsalL8SkeRw+qit1Mo+yuvMPAKwWg/1hA== + +trough@^1.0.0: + version "1.0.5" + resolved "https://registry.yarnpkg.com/trough/-/trough-1.0.5.tgz#b8b639cefad7d0bb2abd37d433ff8293efa5f406" + integrity sha512-rvuRbTarPXmMb79SmzEp8aqXNKcK+y0XaB298IXueQ8I2PsrATcPBCSPyK/dDNa2iWOhKlfNnOjdAOTBU/nkFA== + +ts-loader@^8.0.6: + version "8.0.14" + resolved "https://registry.yarnpkg.com/ts-loader/-/ts-loader-8.0.14.tgz#e46ac1f8dcb88808d0b1335d2eae65b74bd78fe8" + integrity sha512-Jt/hHlUnApOZjnSjTmZ+AbD5BGlQFx3f1D0nYuNKwz0JJnuDGHJas6az+FlWKwwRTu+26GXpv249A8UAnYUpqA== + dependencies: + chalk "^4.1.0" + enhanced-resolve "^4.0.0" + loader-utils "^2.0.0" + micromatch "^4.0.0" + semver "^7.3.4" + +ts-morph@^8.1.2: + version "8.2.0" + resolved "https://registry.yarnpkg.com/ts-morph/-/ts-morph-8.2.0.tgz#41d83cd501cbd897eb029ac489d6d5b927555c57" + integrity sha512-NHHWu+7I2/AOZiTni5w3f+xCfIxrkzPCcQbTGa81Yk3pr23a4h9xLLEE6tIGuYIubWjkjr9QVC3ITqgmA5touQ== + dependencies: + "@dsherret/to-absolute-glob" "^2.0.2" + "@ts-morph/common" "~0.6.0" + code-block-writer "^10.1.0" + +ts-node@^9.0.0: + version "9.1.1" + resolved "https://registry.yarnpkg.com/ts-node/-/ts-node-9.1.1.tgz#51a9a450a3e959401bda5f004a72d54b936d376d" + integrity sha512-hPlt7ZACERQGf03M253ytLY3dHbGNGrAq9qIHWUY9XHYl1z7wYngSr3OQ5xmui8o2AaxsONxIzjafLUiWBo1Fg== + dependencies: + arg "^4.1.0" + create-require "^1.1.0" + diff "^4.0.1" + make-error "^1.1.1" + source-map-support "^0.5.17" + yn "3.1.1" + +tsconfig-paths@^3.9.0: + version "3.9.0" + resolved "https://registry.yarnpkg.com/tsconfig-paths/-/tsconfig-paths-3.9.0.tgz#098547a6c4448807e8fcb8eae081064ee9a3c90b" + integrity sha512-dRcuzokWhajtZWkQsDVKbWyY+jgcLC5sqJhg2PSgf4ZkH2aHPvaOY8YWGhmjb68b5qqTfasSsDO9k7RUiEmZAw== + dependencies: + "@types/json5" "^0.0.29" + json5 "^1.0.1" + minimist "^1.2.0" + strip-bom "^3.0.0" + +tslib@^1.10.0, tslib@^1.8.1, tslib@^1.9.0, tslib@^1.9.3: + version "1.14.1" + resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.14.1.tgz#cf2d38bdc34a134bcaf1091c41f6619e2f672d00" + integrity sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg== + +tslib@^2.0.3: + version "2.1.0" + resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.1.0.tgz#da60860f1c2ecaa5703ab7d39bc05b6bf988b97a" + integrity sha512-hcVC3wYEziELGGmEEXue7D75zbwIIVUMWAVbHItGPx0ziyXxrOMQx4rQEVEV45Ut/1IotuEvwqPopzIOkDMf0A== + +tsutils@^3.17.1: + version "3.20.0" + resolved "https://registry.yarnpkg.com/tsutils/-/tsutils-3.20.0.tgz#ea03ea45462e146b53d70ce0893de453ff24f698" + integrity sha512-RYbuQuvkhuqVeXweWT3tJLKOEJ/UUw9GjNEZGWdrLLlM+611o1gwLHBpxoFJKKl25fLprp2eVthtKs5JOrNeXg== + dependencies: + tslib "^1.8.1" + +type-check@^0.4.0, type-check@~0.4.0: + version "0.4.0" + resolved "https://registry.yarnpkg.com/type-check/-/type-check-0.4.0.tgz#07b8203bfa7056c0657050e3ccd2c37730bab8f1" + integrity sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew== + dependencies: + prelude-ls "^1.2.1" + +type-fest@^0.18.0: + version "0.18.1" + resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.18.1.tgz#db4bc151a4a2cf4eebf9add5db75508db6cc841f" + integrity sha512-OIAYXk8+ISY+qTOwkHtKqzAuxchoMiD9Udx+FSGQDuiRR+PJKJHc2NJAXlbhkGwTt/4/nKZxELY1w3ReWOL8mw== + +type-fest@^0.6.0: + version "0.6.0" + resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.6.0.tgz#8d2a2370d3df886eb5c90ada1c5bf6188acf838b" + integrity sha512-q+MB8nYR1KDLrgr4G5yemftpMC7/QLqVndBmEEdqzmNj5dcFOO4Oo8qlwZE3ULT3+Zim1F8Kq4cBnikNhlCMlg== + +type-fest@^0.8.1: + version "0.8.1" + resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.8.1.tgz#09e249ebde851d3b1e48d27c105444667f17b83d" + integrity sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA== + +type-is@~1.6.17, type-is@~1.6.18: + version "1.6.18" + resolved "https://registry.yarnpkg.com/type-is/-/type-is-1.6.18.tgz#4e552cd05df09467dcbc4ef739de89f2cf37c131" + integrity sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g== + dependencies: + media-typer "0.3.0" + mime-types "~2.1.24" + +typedarray-to-buffer@^3.1.5: + version "3.1.5" + resolved "https://registry.yarnpkg.com/typedarray-to-buffer/-/typedarray-to-buffer-3.1.5.tgz#a97ee7a9ff42691b9f783ff1bc5112fe3fca9080" + integrity sha512-zdu8XMNEDepKKR+XYOXAVPtWui0ly0NtohUscw+UmaHiAWT8hrV1rr//H6V+0DvJ3OQ19S979M0laLfX8rm82Q== + dependencies: + is-typedarray "^1.0.0" + +typescript@^4.0.3: + version "4.1.3" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.1.3.tgz#519d582bd94cba0cf8934c7d8e8467e473f53bb7" + integrity sha512-B3ZIOf1IKeH2ixgHhj6la6xdwR9QrLC5d1VKeCSY4tvkqhF2eqd9O7txNlS0PO3GrBAFIdr3L1ndNwteUbZLYg== + +typescript@~4.0.2: + version "4.0.5" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.0.5.tgz#ae9dddfd1069f1cb5beb3ef3b2170dd7c1332389" + integrity sha512-ywmr/VrTVCmNTJ6iV2LwIrfG1P+lv6luD8sUJs+2eI9NLGigaN+nUQc13iHqisq7bra9lnmUSYqbJvegraBOPQ== + +unc-path-regex@^0.1.2: + version "0.1.2" + resolved "https://registry.yarnpkg.com/unc-path-regex/-/unc-path-regex-0.1.2.tgz#e73dd3d7b0d7c5ed86fbac6b0ae7d8c6a69d50fa" + integrity sha1-5z3T17DXxe2G+6xrCufYxqadUPo= + +unified@^9.1.0: + version "9.2.0" + resolved "https://registry.yarnpkg.com/unified/-/unified-9.2.0.tgz#67a62c627c40589edebbf60f53edfd4d822027f8" + integrity sha512-vx2Z0vY+a3YoTj8+pttM3tiJHCwY5UFbYdiWrwBEbHmK8pvsPj2rtAX2BFfgXen8T39CJWblWRDT4L5WGXtDdg== + dependencies: + bail "^1.0.0" + extend "^3.0.0" + is-buffer "^2.0.0" + is-plain-obj "^2.0.0" + trough "^1.0.0" + vfile "^4.0.0" + +union-value@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/union-value/-/union-value-1.0.1.tgz#0b6fe7b835aecda61c6ea4d4f02c14221e109847" + integrity sha512-tJfXmxMeWYnczCVs7XAEvIV7ieppALdyepWMkHkwciRpZraG/xwT+s2JN8+pr1+8jCRf80FFzvr+MpQeeoF4Xg== + dependencies: + arr-union "^3.1.0" + get-value "^2.0.6" + is-extendable "^0.1.1" + set-value "^2.0.1" + +uniq@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/uniq/-/uniq-1.0.1.tgz#b31c5ae8254844a3a8281541ce2b04b865a734ff" + integrity sha1-sxxa6CVIRKOoKBVBzisEuGWnNP8= + +uniqs@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/uniqs/-/uniqs-2.0.0.tgz#ffede4b36b25290696e6e165d4a59edb998e6b02" + integrity sha1-/+3ks2slKQaW5uFl1KWe25mOawI= + +unique-filename@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/unique-filename/-/unique-filename-1.1.1.tgz#1d69769369ada0583103a1e6ae87681b56573230" + integrity sha512-Vmp0jIp2ln35UTXuryvjzkjGdRyf9b2lTXuSYUiPmzRcl3FDtYqAwOnTJkAngD9SWhnoJzDbTKwaOrZ+STtxNQ== + dependencies: + unique-slug "^2.0.0" + +unique-slug@^2.0.0: + version "2.0.2" + resolved "https://registry.yarnpkg.com/unique-slug/-/unique-slug-2.0.2.tgz#baabce91083fc64e945b0f3ad613e264f7cd4e6c" + integrity sha512-zoWr9ObaxALD3DOPfjPSqxt4fnZiWblxHIgeWqW8x7UqDzEtHEQLzji2cuJYQFCU6KmoJikOYAZlrTHHebjx2w== + dependencies: + imurmurhash "^0.1.4" + +unist-util-find-all-after@^3.0.2: + version "3.0.2" + resolved "https://registry.yarnpkg.com/unist-util-find-all-after/-/unist-util-find-all-after-3.0.2.tgz#fdfecd14c5b7aea5e9ef38d5e0d5f774eeb561f6" + integrity sha512-xaTC/AGZ0rIM2gM28YVRAFPIZpzbpDtU3dRmp7EXlNVA8ziQc4hY3H7BHXM1J49nEmiqc3svnqMReW+PGqbZKQ== + dependencies: + unist-util-is "^4.0.0" + +unist-util-is@^4.0.0: + version "4.0.4" + resolved "https://registry.yarnpkg.com/unist-util-is/-/unist-util-is-4.0.4.tgz#3e9e8de6af2eb0039a59f50c9b3e99698a924f50" + integrity sha512-3dF39j/u423v4BBQrk1AQ2Ve1FxY5W3JKwXxVFzBODQ6WEvccguhgp802qQLKSnxPODE6WuRZtV+ohlUg4meBA== + +unist-util-stringify-position@^2.0.0: + version "2.0.3" + resolved "https://registry.yarnpkg.com/unist-util-stringify-position/-/unist-util-stringify-position-2.0.3.tgz#cce3bfa1cdf85ba7375d1d5b17bdc4cada9bd9da" + integrity sha512-3faScn5I+hy9VleOq/qNbAd6pAx7iH5jYBMS9I1HgQVijz/4mv5Bvw5iw1sC/90CODiKo81G/ps8AJrISn687g== + dependencies: + "@types/unist" "^2.0.2" + +universalify@^0.1.0: + version "0.1.2" + resolved "https://registry.yarnpkg.com/universalify/-/universalify-0.1.2.tgz#b646f69be3942dabcecc9d6639c80dc105efaa66" + integrity sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg== + +universalify@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/universalify/-/universalify-2.0.0.tgz#75a4984efedc4b08975c5aeb73f530d02df25717" + integrity sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ== + +unpipe@1.0.0, unpipe@~1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/unpipe/-/unpipe-1.0.0.tgz#b2bf4ee8514aae6165b4817829d21b2ef49904ec" + integrity sha1-sr9O6FFKrmFltIF4KdIbLvSZBOw= + +unquote@~1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/unquote/-/unquote-1.1.1.tgz#8fded7324ec6e88a0ff8b905e7c098cdc086d544" + integrity sha1-j97XMk7G6IoP+LkF58CYzcCG1UQ= + +unset-value@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/unset-value/-/unset-value-1.0.0.tgz#8376873f7d2335179ffb1e6fc3a8ed0dfc8ab559" + integrity sha1-g3aHP30jNRef+x5vw6jtDfyKtVk= + dependencies: + has-value "^0.3.1" + isobject "^3.0.0" + +upath@^1.1.1: + version "1.2.0" + resolved "https://registry.yarnpkg.com/upath/-/upath-1.2.0.tgz#8f66dbcd55a883acdae4408af8b035a5044c1894" + integrity sha512-aZwGpamFO61g3OlfT7OQCHqhGnW43ieH9WZeP7QxN/G/jS4jfqUkZxoryvJgVPEcrl5NL/ggHsSmLMHuH64Lhg== + +uri-js@^4.2.2: + version "4.4.1" + resolved "https://registry.yarnpkg.com/uri-js/-/uri-js-4.4.1.tgz#9b1a52595225859e55f669d928f88c6c57f2a77e" + integrity sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg== + dependencies: + punycode "^2.1.0" + +urix@^0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/urix/-/urix-0.1.0.tgz#da937f7a62e21fec1fd18d49b35c2935067a6c72" + integrity sha1-2pN/emLiH+wf0Y1Js1wpNQZ6bHI= + +url-loader@^4.1.1: + version "4.1.1" + resolved "https://registry.yarnpkg.com/url-loader/-/url-loader-4.1.1.tgz#28505e905cae158cf07c92ca622d7f237e70a4e2" + integrity sha512-3BTV812+AVHHOJQO8O5MkWgZ5aosP7GnROJwvzLS9hWDj00lZ6Z0wNak423Lp9PBZN05N+Jk/N5Si8jRAlGyWA== + dependencies: + loader-utils "^2.0.0" + mime-types "^2.1.27" + schema-utils "^3.0.0" + +url-parse@^1.4.3, url-parse@^1.4.7: + version "1.4.7" + resolved "https://registry.yarnpkg.com/url-parse/-/url-parse-1.4.7.tgz#a8a83535e8c00a316e403a5db4ac1b9b853ae278" + integrity sha512-d3uaVyzDB9tQoSXFvuSUNFibTd9zxd2bkVrDRvF5TmvWWQwqE4lgYJ5m+x1DbecWkw+LK4RNl2CU1hHuOKPVlg== + dependencies: + querystringify "^2.1.1" + requires-port "^1.0.0" + +url@^0.11.0: + version "0.11.0" + resolved "https://registry.yarnpkg.com/url/-/url-0.11.0.tgz#3838e97cfc60521eb73c525a8e55bfdd9e2e28f1" + integrity sha1-ODjpfPxgUh63PFJajlW/3Z4uKPE= + dependencies: + punycode "1.3.2" + querystring "0.2.0" + +use@^3.1.0: + version "3.1.1" + resolved "https://registry.yarnpkg.com/use/-/use-3.1.1.tgz#d50c8cac79a19fbc20f2911f56eb973f4e10070f" + integrity sha512-cwESVXlO3url9YWlFW/TA9cshCEhtu7IKJ/p5soJ/gGpj7vbvFrAY/eIioQ6Dw23KjZhYgiIo8HOs1nQ2vr/oQ== + +util-deprecate@^1.0.1, util-deprecate@^1.0.2, util-deprecate@~1.0.1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf" + integrity sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8= + +util.promisify@1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/util.promisify/-/util.promisify-1.0.0.tgz#440f7165a459c9a16dc145eb8e72f35687097030" + integrity sha512-i+6qA2MPhvoKLuxnJNpXAGhg7HphQOSUq2LKMZD0m15EiskXUkMvKdF4Uui0WYeCUGea+o2cw/ZuwehtfsrNkA== + dependencies: + define-properties "^1.1.2" + object.getownpropertydescriptors "^2.0.3" + +util.promisify@~1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/util.promisify/-/util.promisify-1.0.1.tgz#6baf7774b80eeb0f7520d8b81d07982a59abbaee" + integrity sha512-g9JpC/3He3bm38zsLupWryXHoEcS22YHthuPQSJdMy6KNrzIRzWqcsHzD/WUnqe45whVou4VIsPew37DoXWNrA== + dependencies: + define-properties "^1.1.3" + es-abstract "^1.17.2" + has-symbols "^1.0.1" + object.getownpropertydescriptors "^2.1.0" + +utila@~0.4: + version "0.4.0" + resolved "https://registry.yarnpkg.com/utila/-/utila-0.4.0.tgz#8a16a05d445657a3aea5eecc5b12a4fa5379772c" + integrity sha1-ihagXURWV6Oupe7MWxKk+lN5dyw= + +utils-merge@1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/utils-merge/-/utils-merge-1.0.1.tgz#9f95710f50a267947b2ccc124741c1028427e713" + integrity sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM= + +uuid@^3.3.2, uuid@^3.4.0: + version "3.4.0" + resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.4.0.tgz#b23e4358afa8a202fe7a100af1f5f883f02007ee" + integrity sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A== + +v8-compile-cache@^2.0.3, v8-compile-cache@^2.2.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/v8-compile-cache/-/v8-compile-cache-2.2.0.tgz#9471efa3ef9128d2f7c6a7ca39c4dd6b5055b132" + integrity sha512-gTpR5XQNKFwOd4clxfnhaqvfqMpqEwr4tOtCyz4MtYZX2JYhfr1JvBFKdS+7K/9rfpZR3VLX+YWBbKoxCgS43Q== + +validate-npm-package-license@^3.0.1: + version "3.0.4" + resolved "https://registry.yarnpkg.com/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz#fc91f6b9c7ba15c857f4cb2c5defeec39d4f410a" + integrity sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew== + dependencies: + spdx-correct "^3.0.0" + spdx-expression-parse "^3.0.0" + +value-equal@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/value-equal/-/value-equal-1.0.1.tgz#1e0b794c734c5c0cade179c437d356d931a34d6c" + integrity sha512-NOJ6JZCAWr0zlxZt+xqCHNTEKOsrks2HQd4MqhP1qy4z1SkbEP467eNx6TgDKXMvUOb+OENfJCZwM+16n7fRfw== + +vary@~1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/vary/-/vary-1.1.2.tgz#2299f02c6ded30d4a5961b0b9f74524a18f634fc" + integrity sha1-IpnwLG3tMNSllhsLn3RSShj2NPw= + +vendors@^1.0.0: + version "1.0.4" + resolved "https://registry.yarnpkg.com/vendors/-/vendors-1.0.4.tgz#e2b800a53e7a29b93506c3cf41100d16c4c4ad8e" + integrity sha512-/juG65kTL4Cy2su4P8HjtkTxk6VmJDiOPBufWniqQ6wknac6jNiXS9vU+hO3wgusiyqWlzTbVHi0dyJqRONg3w== + +vfile-message@^2.0.0: + version "2.0.4" + resolved "https://registry.yarnpkg.com/vfile-message/-/vfile-message-2.0.4.tgz#5b43b88171d409eae58477d13f23dd41d52c371a" + integrity sha512-DjssxRGkMvifUOJre00juHoP9DPWuzjxKuMDrhNbk2TdaYYBNMStsNhEOt3idrtI12VQYM/1+iM0KOzXi4pxwQ== + dependencies: + "@types/unist" "^2.0.0" + unist-util-stringify-position "^2.0.0" + +vfile@^4.0.0: + version "4.2.1" + resolved "https://registry.yarnpkg.com/vfile/-/vfile-4.2.1.tgz#03f1dce28fc625c625bc6514350fbdb00fa9e624" + integrity sha512-O6AE4OskCG5S1emQ/4gl8zK586RqA3srz3nfK/Viy0UPToBc5Trp9BVFb1u0CjsKrAWwnpr4ifM/KBXPWwJbCA== + dependencies: + "@types/unist" "^2.0.0" + is-buffer "^2.0.0" + unist-util-stringify-position "^2.0.0" + vfile-message "^2.0.0" + +warning@^4.0.1, warning@^4.0.3: + version "4.0.3" + resolved "https://registry.yarnpkg.com/warning/-/warning-4.0.3.tgz#16e9e077eb8a86d6af7d64aa1e05fd85b4678ca3" + integrity sha512-rpJyN222KWIvHJ/F53XSZv0Zl/accqHR8et1kpaMTD/fLCRxtV8iX8czMzY7sVZupTI3zcUTg8eycS2kNF9l6w== + dependencies: + loose-envify "^1.0.0" + +watchpack@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/watchpack/-/watchpack-2.1.0.tgz#e63194736bf3aa22026f7b191cd57907b0f9f696" + integrity sha512-UjgD1mqjkG99+3lgG36at4wPnUXNvis2v1utwTgQ43C22c4LD71LsYMExdWXh4HZ+RmW+B0t1Vrg2GpXAkTOQw== + dependencies: + glob-to-regexp "^0.4.1" + graceful-fs "^4.1.2" + +wbuf@^1.1.0, wbuf@^1.7.3: + version "1.7.3" + resolved "https://registry.yarnpkg.com/wbuf/-/wbuf-1.7.3.tgz#c1d8d149316d3ea852848895cb6a0bfe887b87df" + integrity sha512-O84QOnr0icsbFGLS0O3bI5FswxzRr8/gHwWkDlQFskhSPryQXvrTMxjxGP4+iWYoauLoBvfDpkrOauZ+0iZpDA== + dependencies: + minimalistic-assert "^1.0.0" + +webpack-cli@^4.2.0: + version "4.4.0" + resolved "https://registry.yarnpkg.com/webpack-cli/-/webpack-cli-4.4.0.tgz#38c7fa01ea31510f5c490245dd1bb28018792f1b" + integrity sha512-/Qh07CXfXEkMu5S8wEpjuaw2Zj/CC0hf/qbTDp6N8N7JjdGuaOjZ7kttz+zhuJO/J5m7alQEhNk9lsc4rC6xgQ== + dependencies: + "@discoveryjs/json-ext" "^0.5.0" + "@webpack-cli/configtest" "^1.0.0" + "@webpack-cli/info" "^1.2.1" + "@webpack-cli/serve" "^1.2.2" + colorette "^1.2.1" + commander "^6.2.0" + enquirer "^2.3.6" + execa "^5.0.0" + fastest-levenshtein "^1.0.12" + import-local "^3.0.2" + interpret "^2.2.0" + rechoir "^0.7.0" + v8-compile-cache "^2.2.0" + webpack-merge "^5.7.3" + +webpack-dev-middleware@^3.7.2: + version "3.7.3" + resolved "https://registry.yarnpkg.com/webpack-dev-middleware/-/webpack-dev-middleware-3.7.3.tgz#0639372b143262e2b84ab95d3b91a7597061c2c5" + integrity sha512-djelc/zGiz9nZj/U7PTBi2ViorGJXEWo/3ltkPbDyxCXhhEXkW0ce99falaok4TPj+AsxLiXJR0EBOb0zh9fKQ== + dependencies: + memory-fs "^0.4.1" + mime "^2.4.4" + mkdirp "^0.5.1" + range-parser "^1.2.1" + webpack-log "^2.0.0" + +webpack-dev-server@^3.11.0: + version "3.11.2" + resolved "https://registry.yarnpkg.com/webpack-dev-server/-/webpack-dev-server-3.11.2.tgz#695ebced76a4929f0d5de7fd73fafe185fe33708" + integrity sha512-A80BkuHRQfCiNtGBS1EMf2ChTUs0x+B3wGDFmOeT4rmJOHhHTCH2naNxIHhmkr0/UillP4U3yeIyv1pNp+QDLQ== + dependencies: + ansi-html "0.0.7" + bonjour "^3.5.0" + chokidar "^2.1.8" + compression "^1.7.4" + connect-history-api-fallback "^1.6.0" + debug "^4.1.1" + del "^4.1.1" + express "^4.17.1" + html-entities "^1.3.1" + http-proxy-middleware "0.19.1" + import-local "^2.0.0" + internal-ip "^4.3.0" + ip "^1.1.5" + is-absolute-url "^3.0.3" + killable "^1.0.1" + loglevel "^1.6.8" + opn "^5.5.0" + p-retry "^3.0.1" + portfinder "^1.0.26" + schema-utils "^1.0.0" + selfsigned "^1.10.8" + semver "^6.3.0" + serve-index "^1.9.1" + sockjs "^0.3.21" + sockjs-client "^1.5.0" + spdy "^4.0.2" + strip-ansi "^3.0.1" + supports-color "^6.1.0" + url "^0.11.0" + webpack-dev-middleware "^3.7.2" + webpack-log "^2.0.0" + ws "^6.2.1" + yargs "^13.3.2" + +webpack-log@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/webpack-log/-/webpack-log-2.0.0.tgz#5b7928e0637593f119d32f6227c1e0ac31e1b47f" + integrity sha512-cX8G2vR/85UYG59FgkoMamwHUIkSSlV3bBMRsbxVXVUk2j6NleCKjQ/WE9eYg9WY4w25O9w8wKP4rzNZFmUcUg== + dependencies: + ansi-colors "^3.0.0" + uuid "^3.3.2" + +webpack-merge@^5.2.0, webpack-merge@^5.7.3: + version "5.7.3" + resolved "https://registry.yarnpkg.com/webpack-merge/-/webpack-merge-5.7.3.tgz#2a0754e1877a25a8bbab3d2475ca70a052708213" + integrity sha512-6/JUQv0ELQ1igjGDzHkXbVDRxkfA57Zw7PfiupdLFJYrgFqY5ZP8xxbpp2lU3EPwYx89ht5Z/aDkD40hFCm5AA== + dependencies: + clone-deep "^4.0.1" + wildcard "^2.0.0" + +webpack-sources@^1.1.0, webpack-sources@^1.4.3: + version "1.4.3" + resolved "https://registry.yarnpkg.com/webpack-sources/-/webpack-sources-1.4.3.tgz#eedd8ec0b928fbf1cbfe994e22d2d890f330a933" + integrity sha512-lgTS3Xhv1lCOKo7SA5TjKXMjpSM4sBjNV5+q2bqesbSPs5FjGmU6jjtBSkX9b4qW87vDIsCIlUPOEhbZrMdjeQ== + dependencies: + source-list-map "^2.0.0" + source-map "~0.6.1" + +webpack-sources@^2.1.1: + version "2.2.0" + resolved "https://registry.yarnpkg.com/webpack-sources/-/webpack-sources-2.2.0.tgz#058926f39e3d443193b6c31547229806ffd02bac" + integrity sha512-bQsA24JLwcnWGArOKUxYKhX3Mz/nK1Xf6hxullKERyktjNMC4x8koOeaDNTA2fEJ09BdWLbM/iTW0ithREUP0w== + dependencies: + source-list-map "^2.0.1" + source-map "^0.6.1" + +webpack@^5.10.0: + version "5.18.0" + resolved "https://registry.yarnpkg.com/webpack/-/webpack-5.18.0.tgz#bbcf13094aa0da0534d513f27d7ee72d74e499c6" + integrity sha512-RmiP/iy6ROvVe/S+u0TrvL/oOmvP+2+Bs8MWjvBwwY/j82Q51XJyDJ75m0QAGntL1Wx6B//Xc0+4VPP/hlNHmw== + dependencies: + "@types/eslint-scope" "^3.7.0" + "@types/estree" "^0.0.46" + "@webassemblyjs/ast" "1.11.0" + "@webassemblyjs/wasm-edit" "1.11.0" + "@webassemblyjs/wasm-parser" "1.11.0" + acorn "^8.0.4" + browserslist "^4.14.5" + chrome-trace-event "^1.0.2" + enhanced-resolve "^5.7.0" + es-module-lexer "^0.3.26" + eslint-scope "^5.1.1" + events "^3.2.0" + glob-to-regexp "^0.4.1" + graceful-fs "^4.2.4" + json-parse-better-errors "^1.0.2" + loader-runner "^4.2.0" + mime-types "^2.1.27" + neo-async "^2.6.2" + pkg-dir "^5.0.0" + schema-utils "^3.0.0" + tapable "^2.1.1" + terser-webpack-plugin "^5.1.1" + watchpack "^2.0.0" + webpack-sources "^2.1.1" + +websocket-driver@>=0.5.1, websocket-driver@^0.7.4: + version "0.7.4" + resolved "https://registry.yarnpkg.com/websocket-driver/-/websocket-driver-0.7.4.tgz#89ad5295bbf64b480abcba31e4953aca706f5760" + integrity sha512-b17KeDIQVjvb0ssuSDF2cYXSg2iztliJ4B9WdsuB6J952qCPKmnVq4DyW5motImXHDC1cBT/1UezrJVsKw5zjg== + dependencies: + http-parser-js ">=0.5.1" + safe-buffer ">=5.1.0" + websocket-extensions ">=0.1.1" + +websocket-extensions@>=0.1.1: + version "0.1.4" + resolved "https://registry.yarnpkg.com/websocket-extensions/-/websocket-extensions-0.1.4.tgz#7f8473bc839dfd87608adb95d7eb075211578a42" + integrity sha512-OqedPIGOfsDlo31UNwYbCFMSaO9m9G/0faIHj5/dZFDMFqPTcx6UwqyOy3COEaEOg/9VsGIpdqn62W5KhoKSpg== + +which-module@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/which-module/-/which-module-2.0.0.tgz#d9ef07dce77b9902b8a3a8fa4b31c3e3f7e6e87a" + integrity sha1-2e8H3Od7mQK4o6j6SzHD4/fm6Ho= + +which@^1.2.9, which@^1.3.1: + version "1.3.1" + resolved "https://registry.yarnpkg.com/which/-/which-1.3.1.tgz#a45043d54f5805316da8d62f9f50918d3da70b0a" + integrity sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ== + dependencies: + isexe "^2.0.0" + +which@^2.0.1: + version "2.0.2" + resolved "https://registry.yarnpkg.com/which/-/which-2.0.2.tgz#7c6a8dd0a636a0327e10b59c9286eee93f3f51b1" + integrity sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA== + dependencies: + isexe "^2.0.0" + +wildcard@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/wildcard/-/wildcard-2.0.0.tgz#a77d20e5200c6faaac979e4b3aadc7b3dd7f8fec" + integrity sha512-JcKqAHLPxcdb9KM49dufGXn2x3ssnfjbcaQdLlfZsL9rH9wgDQjUtDxbo8NE0F6SFvydeu1VhZe7hZuHsB2/pw== + +word-wrap@^1.2.3: + version "1.2.3" + resolved "https://registry.yarnpkg.com/word-wrap/-/word-wrap-1.2.3.tgz#610636f6b1f703891bd34771ccb17fb93b47079c" + integrity sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ== + +wrap-ansi@^5.1.0: + version "5.1.0" + resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-5.1.0.tgz#1fd1f67235d5b6d0fee781056001bfb694c03b09" + integrity sha512-QC1/iN/2/RPVJ5jYK8BGttj5z83LmSKmvbvrXPNCLZSEb32KKVDJDl/MOt2N01qU2H/FkzEa9PKto1BqDjtd7Q== + dependencies: + ansi-styles "^3.2.0" + string-width "^3.0.0" + strip-ansi "^5.0.0" + +wrappy@1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f" + integrity sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8= + +write-file-atomic@^3.0.3: + version "3.0.3" + resolved "https://registry.yarnpkg.com/write-file-atomic/-/write-file-atomic-3.0.3.tgz#56bd5c5a5c70481cd19c571bd39ab965a5de56e8" + integrity sha512-AvHcyZ5JnSfq3ioSyjrBkH9yW4m7Ayk8/9My/DD9onKeu/94fwrMocemO2QAJFAlnnDN+ZDS+ZjAR5ua1/PV/Q== + dependencies: + imurmurhash "^0.1.4" + is-typedarray "^1.0.0" + signal-exit "^3.0.2" + typedarray-to-buffer "^3.1.5" + +ws@^6.2.1: + version "6.2.1" + resolved "https://registry.yarnpkg.com/ws/-/ws-6.2.1.tgz#442fdf0a47ed64f59b6a5d8ff130f4748ed524fb" + integrity sha512-GIyAXC2cB7LjvpgMt9EKS2ldqr0MTrORaleiOno6TweZ6r3TKtoFQWay/2PceJ3RuBasOHzXNn5Lrw1X0bEjqA== + dependencies: + async-limiter "~1.0.0" + +y18n@^4.0.0: + version "4.0.1" + resolved "https://registry.yarnpkg.com/y18n/-/y18n-4.0.1.tgz#8db2b83c31c5d75099bb890b23f3094891e247d4" + integrity sha512-wNcy4NvjMYL8gogWWYAO7ZFWFfHcbdbE57tZO8e4cbpj8tfUcwrwqSl3ad8HxpYWCdXcJUCeKKZS62Av1affwQ== + +yallist@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/yallist/-/yallist-4.0.0.tgz#9bb92790d9c0effec63be73519e11a35019a3a72" + integrity sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A== + +yaml@^1.10.0: + version "1.10.0" + resolved "https://registry.yarnpkg.com/yaml/-/yaml-1.10.0.tgz#3b593add944876077d4d683fee01081bd9fff31e" + integrity sha512-yr2icI4glYaNG+KWONODapy2/jDdMSDnrONSjblABjD9B4Z5LgiircSt8m8sRZFNi08kG9Sm0uSHtEmP3zaEGg== + +yargs-parser@^13.1.2: + version "13.1.2" + resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-13.1.2.tgz#130f09702ebaeef2650d54ce6e3e5706f7a4fb38" + integrity sha512-3lbsNRf/j+A4QuSZfDRA7HRSfWrzO0YjqTJd5kjAq37Zep1CEgaYmrH9Q3GwPiB9cHyd1Y1UwggGhJGoxipbzg== + dependencies: + camelcase "^5.0.0" + decamelize "^1.2.0" + +yargs-parser@^20.2.3: + version "20.2.4" + resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-20.2.4.tgz#b42890f14566796f85ae8e3a25290d205f154a54" + integrity sha512-WOkpgNhPTlE73h4VFAFsOnomJVaovO8VqLDzy5saChRBFQFBoMYirowyW+Q9HB4HFF4Z7VZTiG3iSzJJA29yRA== + +yargs@^13.3.2: + version "13.3.2" + resolved "https://registry.yarnpkg.com/yargs/-/yargs-13.3.2.tgz#ad7ffefec1aa59565ac915f82dccb38a9c31a2dd" + integrity sha512-AX3Zw5iPruN5ie6xGRIDgqkT+ZhnRlZMLMHAs8tg7nRruy2Nb+i5o9bwghAogtM08q1dpr2LVoS8KSTMYpWXUw== + dependencies: + cliui "^5.0.0" + find-up "^3.0.0" + get-caller-file "^2.0.1" + require-directory "^2.1.1" + require-main-filename "^2.0.0" + set-blocking "^2.0.0" + string-width "^3.0.0" + which-module "^2.0.0" + y18n "^4.0.0" + yargs-parser "^13.1.2" + +yn@3.1.1: + version "3.1.1" + resolved "https://registry.yarnpkg.com/yn/-/yn-3.1.1.tgz#1e87401a09d767c1d5eab26a6e4c185182d2eb50" + integrity sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q== + +yocto-queue@^0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/yocto-queue/-/yocto-queue-0.1.0.tgz#0294eb3dee05028d31ee1a5fa2c556a6aaf10a1b" + integrity sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q== + +zwitch@^1.0.0: + version "1.0.5" + resolved "https://registry.yarnpkg.com/zwitch/-/zwitch-1.0.5.tgz#d11d7381ffed16b742f6af7b3f223d5cd9fe9920" + integrity sha512-V50KMwwzqJV0NpZIZFwfOD5/lyny3WlSzRiXgA0G7VUnRlqttta1L6UQIHzd6EuBY/cHGfwTIck7w1yH6Q5zUw== diff --git a/go.mod b/go.mod index 10866d88..b8d18f43 100644 --- a/go.mod +++ b/go.mod @@ -3,11 +3,12 @@ module github.com/AdguardTeam/AdGuardHome go 1.14 require ( - github.com/AdguardTeam/dnsproxy v0.33.7 + github.com/AdguardTeam/dnsproxy v0.33.9 github.com/AdguardTeam/golibs v0.4.4 - github.com/AdguardTeam/urlfilter v0.14.0 + github.com/AdguardTeam/urlfilter v0.14.3 github.com/NYTimes/gziphandler v1.1.1 github.com/ameshkov/dnscrypt/v2 v2.0.1 + github.com/digineo/go-ipset/v2 v2.2.1 github.com/fsnotify/fsnotify v1.4.9 github.com/go-ping/ping v0.0.0-20201115131931-3300c582a663 github.com/gobuffalo/envy v1.9.0 // indirect @@ -17,7 +18,9 @@ require ( github.com/insomniacslk/dhcp v0.0.0-20201112113307-4de412bc85d8 github.com/kardianos/service v1.2.0 github.com/karrick/godirwalk v1.16.1 // indirect + github.com/lucas-clemente/quic-go v0.19.3 github.com/mdlayher/ethernet v0.0.0-20190606142754-0394541c37b7 + github.com/mdlayher/netlink v1.1.2-0.20201013204415-ded538f7f4be github.com/mdlayher/raw v0.0.0-20191009151244-50f2db8cc065 github.com/miekg/dns v1.1.35 github.com/rogpeppe/go-internal v1.6.2 // indirect @@ -25,6 +28,7 @@ require ( github.com/sirupsen/logrus v1.7.0 // indirect github.com/spf13/cobra v1.1.1 // indirect github.com/stretchr/testify v1.6.1 + github.com/ti-mo/netfilter v0.4.0 github.com/u-root/u-root v7.0.0+incompatible go.etcd.io/bbolt v1.3.5 golang.org/x/crypto v0.0.0-20201217014255-9d1352758620 diff --git a/go.sum b/go.sum index 764a2fa0..4afc5ca7 100644 --- a/go.sum +++ b/go.sum @@ -18,16 +18,16 @@ dmitri.shuralyov.com/html/belt v0.0.0-20180602232347-f7d459c86be0/go.mod h1:JLBr dmitri.shuralyov.com/service/change v0.0.0-20181023043359-a85b471d5412/go.mod h1:a1inKt/atXimZ4Mv927x+r7UpyzRUf4emIoiiSC2TN4= dmitri.shuralyov.com/state v0.0.0-20180228185332-28bcc343414c/go.mod h1:0PRwlb0D6DFvNNtx+9ybjezNCa8XF0xaYcETyp6rHWU= git.apache.org/thrift.git v0.0.0-20180902110319-2566ecd5d999/go.mod h1:fPE2ZNJGynbRyZ4dJvy6G277gSllfV2HJqblrnkyeyg= -github.com/AdguardTeam/dnsproxy v0.33.7 h1:DXsLTJoBSUejB2ZqVHyMG0/kXD8PzuVPbLCsGKBdaDc= -github.com/AdguardTeam/dnsproxy v0.33.7/go.mod h1:dkI9VWh43XlOzF2XogDm1EmoVl7PANOR4isQV6X9LZs= +github.com/AdguardTeam/dnsproxy v0.33.9 h1:HUwywkhUV/M73E7qWcBAF+SdsNq742s82Lvox4pr/tM= +github.com/AdguardTeam/dnsproxy v0.33.9/go.mod h1:dkI9VWh43XlOzF2XogDm1EmoVl7PANOR4isQV6X9LZs= github.com/AdguardTeam/golibs v0.4.0/go.mod h1:skKsDKIBB7kkFflLJBpfGX+G8QFTx0WKUzB6TIgtUj4= github.com/AdguardTeam/golibs v0.4.2 h1:7M28oTZFoFwNmp8eGPb3ImmYbxGaJLyQXeIFVHjME0o= github.com/AdguardTeam/golibs v0.4.2/go.mod h1:skKsDKIBB7kkFflLJBpfGX+G8QFTx0WKUzB6TIgtUj4= github.com/AdguardTeam/golibs v0.4.4 h1:cM9UySQiYFW79zo5XRwnaIWVzfW4eNXmZktMrWbthpw= github.com/AdguardTeam/golibs v0.4.4/go.mod h1:skKsDKIBB7kkFflLJBpfGX+G8QFTx0WKUzB6TIgtUj4= github.com/AdguardTeam/gomitmproxy v0.2.0/go.mod h1:Qdv0Mktnzer5zpdpi5rAwixNJzW2FN91LjKJCkVbYGU= -github.com/AdguardTeam/urlfilter v0.14.0 h1:+aAhOvZDVGzl5gTERB4pOJCL1zxMyw7vLecJJ6TQTCw= -github.com/AdguardTeam/urlfilter v0.14.0/go.mod h1:klx4JbOfc4EaNb5lWLqOwfg+pVcyRukmoJRvO55lL5U= +github.com/AdguardTeam/urlfilter v0.14.3 h1:MBaLEXS0LRQNbHtLkDCYhHINDPtkevPrYWGiOUuLJU4= +github.com/AdguardTeam/urlfilter v0.14.3/go.mod h1:klx4JbOfc4EaNb5lWLqOwfg+pVcyRukmoJRvO55lL5U= github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= @@ -83,6 +83,8 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no= +github.com/digineo/go-ipset/v2 v2.2.1 h1:k6skY+0fMqeUjjeWO/m5OuWPSZUAn7AucHMnQ1MX77g= +github.com/digineo/go-ipset/v2 v2.2.1/go.mod h1:wBsNzJlZlABHUITkesrggFnZQtgW5wkqw1uo8Qxe0VU= github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= @@ -223,6 +225,7 @@ github.com/joomcode/errorx v1.0.3/go.mod h1:eQzdtdlNyN7etw6YCS4W4+lu442waxZYw5yv github.com/jsimonetti/rtnetlink v0.0.0-20190606172950-9527aa82566a/go.mod h1:Oz+70psSo5OFh8DBl0Zv2ACw7Esh6pPUphlvZG9x7uw= github.com/jsimonetti/rtnetlink v0.0.0-20200117123717-f846d4f6c1f4/go.mod h1:WGuG/smIU4J/54PblvSbh+xvCZmpJnFgr3ds6Z55XMQ= github.com/jsimonetti/rtnetlink v0.0.0-20201009170750-9c6f07d100c1/go.mod h1:hqoO/u39cqLeBLebZ8fWdE96O7FxrAsRYhnVOdgHxok= +github.com/jsimonetti/rtnetlink v0.0.0-20201110080708-d2c240429e6c h1:7cpGGTQO6+OuYQWkueqeXuErSjs1NZtpALpv1x7Mq4g= github.com/jsimonetti/rtnetlink v0.0.0-20201110080708-d2c240429e6c/go.mod h1:huN4d1phzjhlOsNIjFsw2SVRbwIHj3fJDMEU2SDPTmg= github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= @@ -269,10 +272,14 @@ github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNx github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= github.com/mdlayher/ethernet v0.0.0-20190606142754-0394541c37b7 h1:lez6TS6aAau+8wXUP3G9I3TGlmPFEq2CTxBaRqY6AGE= github.com/mdlayher/ethernet v0.0.0-20190606142754-0394541c37b7/go.mod h1:U6ZQobyTjI/tJyq2HG+i/dfSoFUt8/aZCM+GKtmFk/Y= +github.com/mdlayher/netlink v0.0.0-20190313131330-258ea9dff42c/go.mod h1:eQB3mZE4aiYnlUsyGGCOpPETfdQq4Jhsgf1fk3cwQaA= github.com/mdlayher/netlink v0.0.0-20190409211403-11939a169225/go.mod h1:eQB3mZE4aiYnlUsyGGCOpPETfdQq4Jhsgf1fk3cwQaA= github.com/mdlayher/netlink v1.0.0/go.mod h1:KxeJAFOFLG6AjpyDkQ/iIhxygIUKD+vcwqcnu43w/+M= github.com/mdlayher/netlink v1.1.0/go.mod h1:H4WCitaheIsdF9yOYu8CFmCgQthAPIWZmcKp9uZHgmY= +github.com/mdlayher/netlink v1.1.1 h1:VqG+Voq9V4uZ+04vjIrcSCWDpf91B1xxbP4QBUmUJE8= github.com/mdlayher/netlink v1.1.1/go.mod h1:WTYpFb/WTvlRJAyKhZL5/uy69TDDpHHu2VZmb2XgV7o= +github.com/mdlayher/netlink v1.1.2-0.20201013204415-ded538f7f4be h1:7JeFwhE5SIdgKRd0qnqjOYJxY8AML8x/j+/qvFZ8R+c= +github.com/mdlayher/netlink v1.1.2-0.20201013204415-ded538f7f4be/go.mod h1:WTYpFb/WTvlRJAyKhZL5/uy69TDDpHHu2VZmb2XgV7o= github.com/mdlayher/raw v0.0.0-20190606142536-fef19f00fc18/go.mod h1:7EpbotpCmVZcu+KCX4g9WaRNuu11uyhiW7+Le1dKawg= github.com/mdlayher/raw v0.0.0-20191009151244-50f2db8cc065 h1:aFkJ6lx4FPip+S+Uw4aTegFMct9shDvP+79PsSxpm3w= github.com/mdlayher/raw v0.0.0-20191009151244-50f2db8cc065/go.mod h1:7EpbotpCmVZcu+KCX4g9WaRNuu11uyhiW7+Le1dKawg= @@ -411,6 +418,9 @@ github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw= github.com/tarm/serial v0.0.0-20180830185346-98f6abe2eb07/go.mod h1:kDXzergiv9cbyO7IOYJZWg1U88JhDg3PB6klq9Hg2pA= +github.com/ti-mo/netfilter v0.2.0/go.mod h1:8GbBGsY/8fxtyIdfwy29JiluNcPK4K7wIT+x42ipqUU= +github.com/ti-mo/netfilter v0.4.0 h1:rTN1nBYULDmMfDeBHZpKuNKX/bWEXQUhe02a/10orzg= +github.com/ti-mo/netfilter v0.4.0/go.mod h1:V54q75mUx8CNA2JnFl+wv9iZ5+JP9nCcRlaFS5OZSRM= github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= github.com/u-root/u-root v7.0.0+incompatible h1:u+KSS04pSxJGI5E7WE4Bs9+Zd75QjFv+REkjy/aoAc8= github.com/u-root/u-root v7.0.0+incompatible/go.mod h1:RYkpo8pTHrNjW08opNd/U6p/RJE7K0D8fXO0d47+3YY= @@ -506,6 +516,7 @@ golang.org/x/net v0.0.0-20200707034311-ab3426394381 h1:VXak5I6aEWmAXeQjA+QSZzlgN golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= golang.org/x/net v0.0.0-20200904194848-62affa334b73/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= golang.org/x/net v0.0.0-20201010224723-4f7140c49acb/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20201016165138-7b1cca2348c0/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20201110031124-69a78807bb2b h1:uwuIcX0g4Yl1NC5XAz37xsr2lTtcqevgzYNVt49waME= golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20201209123823-ac852fbbde11 h1:lwlPPsmjDKK0J6eG6xDWd5XPehI0R024zxjDnw3esPA= @@ -540,6 +551,7 @@ golang.org/x/sys v0.0.0-20181205085412-a5c9d58dba9a/go.mod h1:STP8DvDyc/dI5b8T5h golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190316082340-a2f829d7f35f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190322080309-f49334f85ddc/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190411185658-b44545bcd369/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190418153312-f0ce4c0180be/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -567,6 +579,7 @@ golang.org/x/sys v0.0.0-20200519105757-fe76b779f299/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201009025420-dfb3f7c4e634/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201015000850-e3ed0017c211/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201017003518-b09fb700fbb7/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201101102859-da207088b7d1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201112073958-5cba982894dd h1:5CtCZbICpIOFdgO940moixOPjc0178IU44m4EjOO5IY= golang.org/x/sys v0.0.0-20201112073958-5cba982894dd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= diff --git a/internal/agherr/agherr_test.go b/internal/agherr/agherr_test.go index 123c45ef..3ac5aeab 100644 --- a/internal/agherr/agherr_test.go +++ b/internal/agherr/agherr_test.go @@ -5,14 +5,9 @@ import ( "fmt" "testing" - "github.com/AdguardTeam/AdGuardHome/internal/testutil" "github.com/stretchr/testify/assert" ) -func TestMain(m *testing.M) { - testutil.DiscardLogOutput(m) -} - func TestError_Error(t *testing.T) { testCases := []struct { name string diff --git a/internal/aghio/limitedreadcloser_test.go b/internal/aghio/limitedreadcloser_test.go index 1f10e32b..9cccda17 100644 --- a/internal/aghio/limitedreadcloser_test.go +++ b/internal/aghio/limitedreadcloser_test.go @@ -8,6 +8,7 @@ import ( "testing" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) func TestLimitReadCloser(t *testing.T) { @@ -78,11 +79,11 @@ func TestLimitedReadCloser_Read(t *testing.T) { buf := make([]byte, tc.limit+1) lreader, err := LimitReadCloser(readCloser, tc.limit) - assert.Nil(t, err) + require.Nil(t, err) n, err := lreader.Read(buf) - assert.Equal(t, n, tc.want) - assert.Equal(t, tc.err, err) + require.Equal(t, tc.err, err) + assert.Equal(t, tc.want, n) }) } } diff --git a/internal/testutil/testutil.go b/internal/aghtest/aghtest.go similarity index 93% rename from internal/testutil/testutil.go rename to internal/aghtest/aghtest.go index 69187969..4c453055 100644 --- a/internal/testutil/testutil.go +++ b/internal/aghtest/aghtest.go @@ -1,5 +1,5 @@ -// Package testutil contains utilities for testing. -package testutil +// Package aghtest contains utilities for testing. +package aghtest import ( "io" diff --git a/internal/aghtest/os.go b/internal/aghtest/os.go new file mode 100644 index 00000000..a9885c03 --- /dev/null +++ b/internal/aghtest/os.go @@ -0,0 +1,45 @@ +package aghtest + +import ( + "io/ioutil" + "os" + "runtime" + "testing" + "time" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +// PrepareTestDir returns the full path to temporary created directory and +// registers the appropriate cleanup for *t. +func PrepareTestDir(t *testing.T) (dir string) { + t.Helper() + + wd, err := os.Getwd() + require.Nil(t, err) + + dir, err = ioutil.TempDir(wd, "agh-test") + require.Nil(t, err) + require.NotEmpty(t, dir) + + t.Cleanup(func() { + // TODO(e.burkov): Replace with t.TempDir methods after updating + // go version to 1.15. + start := time.Now() + for { + err := os.RemoveAll(dir) + if err == nil { + break + } + + if runtime.GOOS != "windows" || time.Since(start) >= 500*time.Millisecond { + break + } + time.Sleep(5 * time.Millisecond) + } + assert.Nil(t, err) + }) + + return dir +} diff --git a/internal/aghtest/resolver.go b/internal/aghtest/resolver.go new file mode 100644 index 00000000..75fb6ce0 --- /dev/null +++ b/internal/aghtest/resolver.go @@ -0,0 +1,63 @@ +package aghtest + +import ( + "context" + "crypto/sha256" + "net" + "sync" +) + +// TestResolver is a Resolver for tests. +type TestResolver struct { + counter int + counterLock sync.Mutex +} + +// HostToIPs generates IPv4 and IPv6 from host. +// +// TODO(e.burkov): Replace with LookupIP after upgrading go to v1.15. +func (r *TestResolver) HostToIPs(host string) (ipv4, ipv6 net.IP) { + hash := sha256.Sum256([]byte(host)) + + return net.IP(hash[:4]), net.IP(hash[4:20]) +} + +// LookupIPAddr implements Resolver interface for *testResolver. It returns the +// slice of net.IPAddr with IPv4 and IPv6 instances. +func (r *TestResolver) LookupIPAddr(_ context.Context, host string) (ips []net.IPAddr, err error) { + ipv4, ipv6 := r.HostToIPs(host) + addrs := []net.IPAddr{{ + IP: ipv4, + }, { + IP: ipv6, + }} + + r.counterLock.Lock() + defer r.counterLock.Unlock() + r.counter++ + + return addrs, nil +} + +// LookupHost implements Resolver interface for *testResolver. It returns the +// slice of IPv4 and IPv6 instances converted to strings. +func (r *TestResolver) LookupHost(host string) (addrs []string, err error) { + ipv4, ipv6 := r.HostToIPs(host) + + r.counterLock.Lock() + defer r.counterLock.Unlock() + r.counter++ + + return []string{ + ipv4.String(), + ipv6.String(), + }, nil +} + +// Counter returns the number of requests handled. +func (r *TestResolver) Counter() int { + r.counterLock.Lock() + defer r.counterLock.Unlock() + + return r.counter +} diff --git a/internal/aghtest/upstream.go b/internal/aghtest/upstream.go new file mode 100644 index 00000000..78622771 --- /dev/null +++ b/internal/aghtest/upstream.go @@ -0,0 +1,175 @@ +package aghtest + +import ( + "crypto/sha256" + "encoding/hex" + "fmt" + "net" + "strings" + "sync" + + "github.com/AdguardTeam/AdGuardHome/internal/agherr" + "github.com/miekg/dns" +) + +// TestUpstream is a mock of real upstream. +type TestUpstream struct { + // Addr is the address for Address method. + Addr string + // CName is a map of hostname to canonical name. + CName map[string]string + // IPv4 is a map of hostname to IPv4. + IPv4 map[string][]net.IP + // IPv6 is a map of hostname to IPv6. + IPv6 map[string][]net.IP + // Reverse is a map of address to domain name. + Reverse map[string][]string +} + +// Exchange implements upstream.Upstream interface for *TestUpstream. +func (u *TestUpstream) Exchange(m *dns.Msg) (resp *dns.Msg, err error) { + resp = &dns.Msg{} + resp.SetReply(m) + + if len(m.Question) == 0 { + return nil, fmt.Errorf("question should not be empty") + } + name := m.Question[0].Name + + if cname, ok := u.CName[name]; ok { + resp.Answer = append(resp.Answer, &dns.CNAME{ + Hdr: dns.RR_Header{ + Name: name, + Rrtype: dns.TypeCNAME, + }, + Target: cname, + }) + } + + var hasRec bool + var rrType uint16 + var ips []net.IP + switch m.Question[0].Qtype { + case dns.TypeA: + rrType = dns.TypeA + if ipv4addr, ok := u.IPv4[name]; ok { + hasRec = true + ips = ipv4addr + } + case dns.TypeAAAA: + rrType = dns.TypeAAAA + if ipv6addr, ok := u.IPv6[name]; ok { + hasRec = true + ips = ipv6addr + } + case dns.TypePTR: + names, ok := u.Reverse[name] + if !ok { + break + } + + for _, n := range names { + resp.Answer = append(resp.Answer, &dns.PTR{ + Hdr: dns.RR_Header{ + Name: name, + Rrtype: rrType, + }, + Ptr: n, + }) + } + } + + for _, ip := range ips { + resp.Answer = append(resp.Answer, &dns.A{ + Hdr: dns.RR_Header{ + Name: name, + Rrtype: rrType, + }, + A: ip, + }) + } + + if len(resp.Answer) == 0 { + if hasRec { + // Set no error RCode if there are some records for + // given Qname but we didn't apply them. + resp.SetRcode(m, dns.RcodeSuccess) + + return resp, nil + } + // Set NXDomain RCode otherwise. + resp.SetRcode(m, dns.RcodeNameError) + } + + return resp, nil +} + +// Address implements upstream.Upstream interface for *TestUpstream. +func (u *TestUpstream) Address() string { + return u.Addr +} + +// TestBlockUpstream implements upstream.Upstream interface for replacing real +// upstream in tests. +type TestBlockUpstream struct { + Hostname string + Block bool + requestsCount int + lock sync.RWMutex +} + +// Exchange returns a message unique for TestBlockUpstream's Hostname-Block +// pair. +func (u *TestBlockUpstream) Exchange(r *dns.Msg) (*dns.Msg, error) { + u.lock.Lock() + defer u.lock.Unlock() + u.requestsCount++ + + hash := sha256.Sum256([]byte(u.Hostname)) + hashToReturn := hex.EncodeToString(hash[:]) + if !u.Block { + hashToReturn = hex.EncodeToString(hash[:])[:2] + strings.Repeat("ab", 28) + } + + m := &dns.Msg{} + m.Answer = []dns.RR{ + &dns.TXT{ + Hdr: dns.RR_Header{ + Name: r.Question[0].Name, + }, + Txt: []string{ + hashToReturn, + }, + }, + } + + return m, nil +} + +// Address always returns an empty string. +func (u *TestBlockUpstream) Address() string { + return "" +} + +// RequestsCount returns the number of handled requests. It's safe for +// concurrent use. +func (u *TestBlockUpstream) RequestsCount() int { + u.lock.Lock() + defer u.lock.Unlock() + + return u.requestsCount +} + +// TestErrUpstream implements upstream.Upstream interface for replacing real +// upstream in tests. +type TestErrUpstream struct{} + +// Exchange always returns nil Msg and non-nil error. +func (u *TestErrUpstream) Exchange(*dns.Msg) (*dns.Msg, error) { + return nil, agherr.Error("bad") +} + +// Address always returns an empty string. +func (u *TestErrUpstream) Address() string { + return "" +} diff --git a/internal/dhcpd/dhcpd.go b/internal/dhcpd/dhcpd.go index ecb7d48d..7a48d239 100644 --- a/internal/dhcpd/dhcpd.go +++ b/internal/dhcpd/dhcpd.go @@ -3,6 +3,8 @@ package dhcpd import ( "encoding/hex" + "encoding/json" + "fmt" "net" "net/http" "path/filepath" @@ -17,7 +19,12 @@ import ( const ( defaultDiscoverTime = time.Second * 3 - leaseExpireStatic = 1 + // leaseExpireStatic is used to define the Expiry field for static + // leases. + // + // TODO(e.burkov): Remove it when static leases determining mechanism + // will be improved. + leaseExpireStatic = 1 ) var webHandlersRegistered = false @@ -33,6 +40,51 @@ type Lease struct { Expiry time.Time `json:"expires"` } +// MarshalJSON implements the json.Marshaler interface for *Lease. +func (l *Lease) MarshalJSON() ([]byte, error) { + var expiryStr string + if expiry := l.Expiry; expiry.Unix() != leaseExpireStatic { + // The front-end is waiting for RFC 3999 format of the time + // value. It also shouldn't got an Expiry field for static + // leases. + // + // See https://github.com/AdguardTeam/AdGuardHome/issues/2692. + expiryStr = expiry.Format(time.RFC3339) + } + + type lease Lease + return json.Marshal(&struct { + HWAddr string `json:"mac"` + Expiry string `json:"expires,omitempty"` + *lease + }{ + HWAddr: l.HWAddr.String(), + Expiry: expiryStr, + lease: (*lease)(l), + }) +} + +// UnmarshalJSON implements the json.Unmarshaler interface for *Lease. +func (l *Lease) UnmarshalJSON(data []byte) (err error) { + type lease Lease + aux := struct { + HWAddr string `json:"mac"` + *lease + }{ + lease: (*lease)(l), + } + if err = json.Unmarshal(data, &aux); err != nil { + return err + } + + l.HWAddr, err = net.ParseMAC(aux.HWAddr) + if err != nil { + return fmt.Errorf("couldn't parse MAC address: %w", err) + } + + return nil +} + // ServerConfig - DHCP server configuration // field ordering is important -- yaml fields will mirror ordering from here type ServerConfig struct { @@ -82,14 +134,14 @@ type ServerInterface interface { } // Create - create object -func Create(config ServerConfig) *Server { +func Create(conf ServerConfig) *Server { s := &Server{} - s.conf.Enabled = config.Enabled - s.conf.InterfaceName = config.InterfaceName - s.conf.HTTPRegister = config.HTTPRegister - s.conf.ConfigModified = config.ConfigModified - s.conf.DBFilePath = filepath.Join(config.WorkDir, dbFilename) + s.conf.Enabled = conf.Enabled + s.conf.InterfaceName = conf.InterfaceName + s.conf.HTTPRegister = conf.HTTPRegister + s.conf.ConfigModified = conf.ConfigModified + s.conf.DBFilePath = filepath.Join(conf.WorkDir, dbFilename) if !webHandlersRegistered && s.conf.HTTPRegister != nil { if runtime.GOOS == "windows" { @@ -110,7 +162,7 @@ func Create(config ServerConfig) *Server { } var err4, err6 error - v4conf := config.Conf4 + v4conf := conf.Conf4 v4conf.Enabled = s.conf.Enabled if len(v4conf.RangeStart) == 0 { v4conf.Enabled = false @@ -119,7 +171,7 @@ func Create(config ServerConfig) *Server { v4conf.notify = s.onNotify s.srv4, err4 = v4Create(v4conf) - v6conf := config.Conf6 + v6conf := conf.Conf6 v6conf.Enabled = s.conf.Enabled if len(v6conf.RangeStart) == 0 { v6conf.Enabled = false @@ -137,6 +189,9 @@ func Create(config ServerConfig) *Server { return nil } + s.conf.Conf4 = conf.Conf4 + s.conf.Conf6 = conf.Conf6 + if s.conf.Enabled && !v4conf.Enabled && !v6conf.Enabled { log.Error("Can't enable DHCP server because neither DHCPv4 nor DHCPv6 servers are configured") return nil @@ -210,14 +265,10 @@ const ( LeasesAll = LeasesDynamic | LeasesStatic ) -// Leases returns the list of current DHCP leases (thread-safe) -func (s *Server) Leases(flags int) []Lease { - result := s.srv4.GetLeases(flags) - - v6leases := s.srv6.GetLeases(flags) - result = append(result, v6leases...) - - return result +// Leases returns the list of active IPv4 and IPv6 DHCP leases. It's safe for +// concurrent use. +func (s *Server) Leases(flags int) (leases []Lease) { + return append(s.srv4.GetLeases(flags), s.srv6.GetLeases(flags)...) } // FindMACbyIP - find a MAC address by IP address in the currently active DHCP leases @@ -255,17 +306,22 @@ func parseOptionString(s string) (uint8, []byte) { if err != nil { return 0, nil } - case "ip": ip := net.ParseIP(sval) if ip == nil { return 0, nil } - val = ip - if ip.To4() != nil { - val = ip.To4() - } + // Most DHCP options require IPv4, so do not put the 16-byte + // version if we can. Otherwise, the clients will receive weird + // data that looks like four IPv4 addresses. + // + // See https://github.com/AdguardTeam/AdGuardHome/issues/2688. + if ip4 := ip.To4(); ip4 != nil { + val = ip4 + } else { + val = ip + } default: return 0, nil } diff --git a/internal/dhcpd/dhcpd_test.go b/internal/dhcpd/dhcpd_test.go index 58b88bda..cca733d9 100644 --- a/internal/dhcpd/dhcpd_test.go +++ b/internal/dhcpd/dhcpd_test.go @@ -3,128 +3,188 @@ package dhcpd import ( - "bytes" "net" "os" "testing" "time" - "github.com/AdguardTeam/AdGuardHome/internal/testutil" + "github.com/AdguardTeam/AdGuardHome/internal/aghtest" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) func TestMain(m *testing.M) { - testutil.DiscardLogOutput(m) + aghtest.DiscardLogOutput(m) } func testNotify(flags uint32) { } -// Leases database store/load +// Leases database store/load. func TestDB(t *testing.T) { var err error - s := Server{} - s.conf.DBFilePath = dbFilename - - conf := V4ServerConf{ - Enabled: true, - RangeStart: "192.168.10.100", - RangeEnd: "192.168.10.200", - GatewayIP: "192.168.10.1", - SubnetMask: "255.255.255.0", - notify: testNotify, + s := Server{ + conf: ServerConfig{ + DBFilePath: dbFilename, + }, } - s.srv4, err = v4Create(conf) - assert.True(t, err == nil) + + s.srv4, err = v4Create(V4ServerConf{ + Enabled: true, + RangeStart: net.IP{192, 168, 10, 100}, + RangeEnd: net.IP{192, 168, 10, 200}, + GatewayIP: net.IP{192, 168, 10, 1}, + SubnetMask: net.IP{255, 255, 255, 0}, + notify: testNotify, + }) + require.Nil(t, err) s.srv6, err = v6Create(V6ServerConf{}) - assert.True(t, err == nil) + require.Nil(t, err) - l := Lease{} - l.IP = net.ParseIP("192.168.10.100").To4() - l.HWAddr, _ = net.ParseMAC("aa:aa:aa:aa:aa:aa") - exp1 := time.Now().Add(time.Hour) - l.Expiry = exp1 - s.srv4.(*v4Server).addLease(&l) + leases := []Lease{{ + IP: net.IP{192, 168, 10, 100}, + HWAddr: net.HardwareAddr{0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA}, + Expiry: time.Now().Add(time.Hour), + }, { + IP: net.IP{192, 168, 10, 101}, + HWAddr: net.HardwareAddr{0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xBB}, + }} - l2 := Lease{} - l2.IP = net.ParseIP("192.168.10.101").To4() - l2.HWAddr, _ = net.ParseMAC("aa:aa:aa:aa:aa:bb") - s.srv4.AddStaticLease(l2) + srv4, ok := s.srv4.(*v4Server) + require.True(t, ok) + + srv4.addLease(&leases[0]) + require.Nil(t, s.srv4.AddStaticLease(leases[1])) - _ = os.Remove("leases.db") s.dbStore() + t.Cleanup(func() { + assert.Nil(t, os.Remove(dbFilename)) + }) s.srv4.ResetLeases(nil) - s.dbLoad() ll := s.srv4.GetLeases(LeasesAll) + require.Len(t, ll, len(leases)) - assert.Equal(t, "aa:aa:aa:aa:aa:bb", ll[0].HWAddr.String()) - assert.Equal(t, "192.168.10.101", ll[0].IP.String()) - assert.Equal(t, int64(leaseExpireStatic), ll[0].Expiry.Unix()) + assert.Equal(t, leases[1].HWAddr, ll[0].HWAddr) + assert.Equal(t, leases[1].IP, ll[0].IP) + assert.EqualValues(t, leaseExpireStatic, ll[0].Expiry.Unix()) - assert.Equal(t, "aa:aa:aa:aa:aa:aa", ll[1].HWAddr.String()) - assert.Equal(t, "192.168.10.100", ll[1].IP.String()) - assert.Equal(t, exp1.Unix(), ll[1].Expiry.Unix()) - - _ = os.Remove("leases.db") + assert.Equal(t, leases[0].HWAddr, ll[1].HWAddr) + assert.Equal(t, leases[0].IP, ll[1].IP) + assert.Equal(t, leases[0].Expiry.Unix(), ll[1].Expiry.Unix()) } func TestIsValidSubnetMask(t *testing.T) { - assert.True(t, isValidSubnetMask([]byte{255, 255, 255, 0})) - assert.True(t, isValidSubnetMask([]byte{255, 255, 254, 0})) - assert.True(t, isValidSubnetMask([]byte{255, 255, 252, 0})) - assert.True(t, !isValidSubnetMask([]byte{255, 255, 253, 0})) - assert.True(t, !isValidSubnetMask([]byte{255, 255, 255, 1})) + testCases := []struct { + mask net.IP + want bool + }{{ + mask: net.IP{255, 255, 255, 0}, + want: true, + }, { + mask: net.IP{255, 255, 254, 0}, + want: true, + }, { + mask: net.IP{255, 255, 252, 0}, + want: true, + }, { + mask: net.IP{255, 255, 253, 0}, + }, { + mask: net.IP{255, 255, 255, 1}, + }} + + for _, tc := range testCases { + t.Run(tc.mask.String(), func(t *testing.T) { + assert.Equal(t, tc.want, isValidSubnetMask(tc.mask)) + }) + } } func TestNormalizeLeases(t *testing.T) { - dynLeases := []*Lease{} - staticLeases := []*Lease{} + dynLeases := []*Lease{{ + HWAddr: net.HardwareAddr{1, 2, 3, 4}, + }, { + HWAddr: net.HardwareAddr{1, 2, 3, 5}, + }} - lease := &Lease{} - lease.HWAddr = []byte{1, 2, 3, 4} - dynLeases = append(dynLeases, lease) - lease = new(Lease) - lease.HWAddr = []byte{1, 2, 3, 5} - dynLeases = append(dynLeases, lease) - - lease = new(Lease) - lease.HWAddr = []byte{1, 2, 3, 4} - lease.IP = []byte{0, 2, 3, 4} - staticLeases = append(staticLeases, lease) - lease = new(Lease) - lease.HWAddr = []byte{2, 2, 3, 4} - staticLeases = append(staticLeases, lease) + staticLeases := []*Lease{{ + HWAddr: net.HardwareAddr{1, 2, 3, 4}, + IP: net.IP{0, 2, 3, 4}, + }, { + HWAddr: net.HardwareAddr{2, 2, 3, 4}, + }} leases := normalizeLeases(staticLeases, dynLeases) + require.Len(t, leases, 3) - assert.True(t, len(leases) == 3) - assert.True(t, bytes.Equal(leases[0].HWAddr, []byte{1, 2, 3, 4})) - assert.True(t, bytes.Equal(leases[0].IP, []byte{0, 2, 3, 4})) - assert.True(t, bytes.Equal(leases[1].HWAddr, []byte{2, 2, 3, 4})) - assert.True(t, bytes.Equal(leases[2].HWAddr, []byte{1, 2, 3, 5})) + assert.Equal(t, leases[0].HWAddr, dynLeases[0].HWAddr) + assert.Equal(t, leases[0].IP, staticLeases[0].IP) + assert.Equal(t, leases[1].HWAddr, staticLeases[1].HWAddr) + assert.Equal(t, leases[2].HWAddr, dynLeases[1].HWAddr) } func TestOptions(t *testing.T) { - code, val := parseOptionString(" 12 hex abcdef ") - assert.Equal(t, uint8(12), code) - assert.True(t, bytes.Equal([]byte{0xab, 0xcd, 0xef}, val)) + testCases := []struct { + name string + optStr string + wantVal []byte + wantCode uint8 + }{{ + name: "success_hex", + optStr: "12 hex abcdef", + wantVal: []byte{0xab, 0xcd, 0xef}, + wantCode: 12, + }, { + name: "bad_hex", + optStr: "12 hex abcdefx", + wantVal: nil, + wantCode: 0, + }, { + name: "success_ip", + optStr: "123 ip 1.2.3.4", + wantVal: net.IP{1, 2, 3, 4}, + wantCode: 123, + }, { + name: "success_ipv6", + optStr: "123 ip ::1234", + wantVal: net.IP{ + 0, 0, 0, 0, + 0, 0, 0, 0, + 0, 0, 0, 0, + 0, 0, 0x12, 0x34, + }, + wantCode: 123, + }, { + name: "bad_code", + optStr: "256 ip 1.1.1.1", + wantVal: nil, + wantCode: 0, + }, { + name: "negative_code", + optStr: "-1 ip 1.1.1.1", + wantVal: nil, + wantCode: 0, + }, { + name: "bad_ip", + optStr: "12 ip 1.1.1.1x", + wantVal: nil, + wantCode: 0, + }, { + name: "bad_mode", + wantVal: nil, + optStr: "12 x 1.1.1.1", + wantCode: 0, + }} - code, _ = parseOptionString(" 12 hex abcdef1 ") - assert.Equal(t, uint8(0), code) - - code, val = parseOptionString("123 ip 1.2.3.4") - assert.Equal(t, uint8(123), code) - assert.Equal(t, "1.2.3.4", net.IP(string(val)).String()) - - code, _ = parseOptionString("256 ip 1.1.1.1") - assert.Equal(t, uint8(0), code) - code, _ = parseOptionString("-1 ip 1.1.1.1") - assert.Equal(t, uint8(0), code) - code, _ = parseOptionString("12 ip 1.1.1.1x") - assert.Equal(t, uint8(0), code) - code, _ = parseOptionString("12 x 1.1.1.1") - assert.Equal(t, uint8(0), code) + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + code, val := parseOptionString(tc.optStr) + require.Equal(t, tc.wantCode, code) + if tc.wantVal != nil { + assert.Equal(t, tc.wantVal, val) + } + }) + } } diff --git a/internal/dhcpd/helpers.go b/internal/dhcpd/helpers.go index aafda988..28856b5f 100644 --- a/internal/dhcpd/helpers.go +++ b/internal/dhcpd/helpers.go @@ -14,15 +14,17 @@ func isTimeout(err error) bool { return operr.Timeout() } -func parseIPv4(text string) (net.IP, error) { - result := net.ParseIP(text) - if result == nil { - return nil, fmt.Errorf("%s is not an IP address", text) +func tryTo4(ip net.IP) (ip4 net.IP, err error) { + if ip == nil { + return nil, fmt.Errorf("%v is not an IP address", ip) } - if result.To4() == nil { - return nil, fmt.Errorf("%s is not an IPv4 address", text) + + ip4 = ip.To4() + if ip4 == nil { + return nil, fmt.Errorf("%v is not an IPv4 address", ip) } - return result.To4(), nil + + return ip4, nil } // Return TRUE if subnet mask is correct (e.g. 255.255.255.0) diff --git a/internal/dhcpd/dhcphttp.go b/internal/dhcpd/http.go similarity index 55% rename from internal/dhcpd/dhcphttp.go rename to internal/dhcpd/http.go index 06a4cc79..2dab1132 100644 --- a/internal/dhcpd/dhcphttp.go +++ b/internal/dhcpd/http.go @@ -2,17 +2,16 @@ package dhcpd import ( "encoding/json" + "errors" "fmt" "io/ioutil" "net" "net/http" "os" "strings" - "time" "github.com/AdguardTeam/AdGuardHome/internal/sysutil" "github.com/AdguardTeam/AdGuardHome/internal/util" - "github.com/AdguardTeam/golibs/jsonutil" "github.com/AdguardTeam/golibs/log" ) @@ -22,44 +21,19 @@ func httpError(r *http.Request, w http.ResponseWriter, code int, format string, http.Error(w, text, code) } -// []Lease -> JSON -func convertLeases(inputLeases []Lease, includeExpires bool) []map[string]string { - leases := []map[string]string{} - for _, l := range inputLeases { - lease := map[string]string{ - "mac": l.HWAddr.String(), - "ip": l.IP.String(), - "hostname": l.Hostname, - } - - if includeExpires { - lease["expires"] = l.Expiry.Format(time.RFC3339) - } - - leases = append(leases, lease) - } - return leases -} - type v4ServerConfJSON struct { - GatewayIP string `json:"gateway_ip"` - SubnetMask string `json:"subnet_mask"` - RangeStart string `json:"range_start"` - RangeEnd string `json:"range_end"` + GatewayIP net.IP `json:"gateway_ip"` + SubnetMask net.IP `json:"subnet_mask"` + RangeStart net.IP `json:"range_start"` + RangeEnd net.IP `json:"range_end"` LeaseDuration uint32 `json:"lease_duration"` } -func v4ServerConfToJSON(c V4ServerConf) v4ServerConfJSON { - return v4ServerConfJSON{ - GatewayIP: c.GatewayIP, - SubnetMask: c.SubnetMask, - RangeStart: c.RangeStart, - RangeEnd: c.RangeEnd, - LeaseDuration: c.LeaseDuration, +func v4JSONToServerConf(j *v4ServerConfJSON) V4ServerConf { + if j == nil { + return V4ServerConf{} } -} -func v4JSONToServerConf(j v4ServerConfJSON) V4ServerConf { return V4ServerConf{ GatewayIP: j.GatewayIP, SubnetMask: j.SubnetMask, @@ -70,43 +44,45 @@ func v4JSONToServerConf(j v4ServerConfJSON) V4ServerConf { } type v6ServerConfJSON struct { - RangeStart string `json:"range_start"` + RangeStart net.IP `json:"range_start"` LeaseDuration uint32 `json:"lease_duration"` } -func v6ServerConfToJSON(c V6ServerConf) v6ServerConfJSON { - return v6ServerConfJSON{ - RangeStart: c.RangeStart, - LeaseDuration: c.LeaseDuration, +func v6JSONToServerConf(j *v6ServerConfJSON) V6ServerConf { + if j == nil { + return V6ServerConf{} } -} -func v6JSONToServerConf(j v6ServerConfJSON) V6ServerConf { return V6ServerConf{ RangeStart: j.RangeStart, LeaseDuration: j.LeaseDuration, } } +// dhcpStatusResponse is the response for /control/dhcp/status endpoint. +type dhcpStatusResponse struct { + Enabled bool `json:"enabled"` + IfaceName string `json:"interface_name"` + V4 V4ServerConf `json:"v4"` + V6 V6ServerConf `json:"v6"` + Leases []Lease `json:"leases"` + StaticLeases []Lease `json:"static_leases"` +} + func (s *Server) handleDHCPStatus(w http.ResponseWriter, r *http.Request) { - leases := convertLeases(s.Leases(LeasesDynamic), true) - staticLeases := convertLeases(s.Leases(LeasesStatic), false) - - v4conf := V4ServerConf{} - s.srv4.WriteDiskConfig4(&v4conf) - - v6conf := V6ServerConf{} - s.srv6.WriteDiskConfig6(&v6conf) - - status := map[string]interface{}{ - "enabled": s.conf.Enabled, - "interface_name": s.conf.InterfaceName, - "v4": v4ServerConfToJSON(v4conf), - "v6": v6ServerConfToJSON(v6conf), - "leases": leases, - "static_leases": staticLeases, + status := &dhcpStatusResponse{ + Enabled: s.conf.Enabled, + IfaceName: s.conf.InterfaceName, + V4: V4ServerConf{}, + V6: V6ServerConf{}, } + s.srv4.WriteDiskConfig4(&status.V4) + s.srv6.WriteDiskConfig6(&status.V6) + + status.Leases = s.Leases(LeasesDynamic) + status.StaticLeases = s.Leases(LeasesStatic) + w.Header().Set("Content-Type", "application/json") err := json.NewEncoder(w).Encode(status) if err != nil { @@ -115,27 +91,72 @@ func (s *Server) handleDHCPStatus(w http.ResponseWriter, r *http.Request) { } } -type staticLeaseJSON struct { - HWAddr string `json:"mac"` - IP string `json:"ip"` - Hostname string `json:"hostname"` +func (s *Server) enableDHCP(ifaceName string) (code int, err error) { + var hasStaticIP bool + hasStaticIP, err = sysutil.IfaceHasStaticIP(ifaceName) + if err != nil { + if errors.Is(err, os.ErrPermission) { + // ErrPermission may happen here on Linux systems where + // AdGuard Home is installed using Snap. That doesn't + // necessarily mean that the machine doesn't have + // a static IP, so we can assume that it has and go on. + // If the machine doesn't, we'll get an error later. + // + // See https://github.com/AdguardTeam/AdGuardHome/issues/2667. + // + // TODO(a.garipov): I was thinking about moving this + // into IfaceHasStaticIP, but then we wouldn't be able + // to log it. Think about it more. + log.Info("error while checking static ip: %s; "+ + "assuming machine has static ip and going on", err) + hasStaticIP = true + } else if errors.Is(err, sysutil.ErrNoStaticIPInfo) { + // Couldn't obtain a definitive answer. Assume static + // IP an go on. + log.Info("can't check for static ip; " + + "assuming machine has static ip and going on") + hasStaticIP = true + } else { + err = fmt.Errorf("checking static ip: %w", err) + + return http.StatusInternalServerError, err + } + } + + if !hasStaticIP { + err = sysutil.IfaceSetStaticIP(ifaceName) + if err != nil { + err = fmt.Errorf("setting static ip: %w", err) + + return http.StatusInternalServerError, err + } + } + + err = s.Start() + if err != nil { + return http.StatusBadRequest, fmt.Errorf("starting dhcp server: %w", err) + } + + return 0, nil } type dhcpServerConfigJSON struct { - Enabled bool `json:"enabled"` - InterfaceName string `json:"interface_name"` - V4 v4ServerConfJSON `json:"v4"` - V6 v6ServerConfJSON `json:"v6"` + V4 *v4ServerConfJSON `json:"v4"` + V6 *v6ServerConfJSON `json:"v6"` + InterfaceName string `json:"interface_name"` + Enabled nullBool `json:"enabled"` } func (s *Server) handleDHCPSetConfig(w http.ResponseWriter, r *http.Request) { - newconfig := dhcpServerConfigJSON{} - newconfig.Enabled = s.conf.Enabled - newconfig.InterfaceName = s.conf.InterfaceName + conf := dhcpServerConfigJSON{} + conf.Enabled = boolToNullBool(s.conf.Enabled) + conf.InterfaceName = s.conf.InterfaceName - js, err := jsonutil.DecodeObject(&newconfig, r.Body) + err := json.NewDecoder(r.Body).Decode(&conf) if err != nil { - httpError(r, w, http.StatusBadRequest, "Failed to parse new DHCP config json: %s", err) + httpError(r, w, http.StatusBadRequest, + "failed to parse new dhcp config json: %s", err) + return } @@ -144,80 +165,91 @@ func (s *Server) handleDHCPSetConfig(w http.ResponseWriter, r *http.Request) { v4Enabled := false v6Enabled := false - if js.Exists("v4") { - v4conf := v4JSONToServerConf(newconfig.V4) - v4conf.Enabled = newconfig.Enabled - if len(v4conf.RangeStart) == 0 { - v4conf.Enabled = false + if conf.V4 != nil { + v4Conf := v4JSONToServerConf(conf.V4) + v4Conf.Enabled = conf.Enabled == nbTrue + if len(v4Conf.RangeStart) == 0 { + v4Conf.Enabled = false } - v4Enabled = v4conf.Enabled - v4conf.InterfaceName = newconfig.InterfaceName + + v4Enabled = v4Conf.Enabled + v4Conf.InterfaceName = conf.InterfaceName c4 := V4ServerConf{} s.srv4.WriteDiskConfig4(&c4) - v4conf.notify = c4.notify - v4conf.ICMPTimeout = c4.ICMPTimeout + v4Conf.notify = c4.notify + v4Conf.ICMPTimeout = c4.ICMPTimeout - s4, err = v4Create(v4conf) + s4, err = v4Create(v4Conf) if err != nil { - httpError(r, w, http.StatusBadRequest, "Invalid DHCPv4 configuration: %s", err) + httpError(r, w, http.StatusBadRequest, + "invalid dhcpv4 configuration: %s", err) + return } } - if js.Exists("v6") { - v6conf := v6JSONToServerConf(newconfig.V6) - v6conf.Enabled = newconfig.Enabled - if len(v6conf.RangeStart) == 0 { - v6conf.Enabled = false + if conf.V6 != nil { + v6Conf := v6JSONToServerConf(conf.V6) + v6Conf.Enabled = conf.Enabled == nbTrue + if len(v6Conf.RangeStart) == 0 { + v6Conf.Enabled = false } - v6Enabled = v6conf.Enabled - v6conf.InterfaceName = newconfig.InterfaceName - v6conf.notify = s.onNotify - s6, err = v6Create(v6conf) + + // Don't overwrite the RA/SLAAC settings from the config file. + // + // TODO(a.garipov): Perhaps include them into the request to + // allow changing them from the HTTP API? + v6Conf.RASLAACOnly = s.conf.Conf6.RASLAACOnly + v6Conf.RAAllowSLAAC = s.conf.Conf6.RAAllowSLAAC + + v6Enabled = v6Conf.Enabled + v6Conf.InterfaceName = conf.InterfaceName + v6Conf.notify = s.onNotify + + s6, err = v6Create(v6Conf) if err != nil { - httpError(r, w, http.StatusBadRequest, "Invalid DHCPv6 configuration: %s", err) + httpError(r, w, http.StatusBadRequest, + "invalid dhcpv6 configuration: %s", err) + return } } - if newconfig.Enabled && !v4Enabled && !v6Enabled { - httpError(r, w, http.StatusBadRequest, "DHCPv4 or DHCPv6 configuration must be complete") + if conf.Enabled == nbTrue && !v4Enabled && !v6Enabled { + httpError(r, w, http.StatusBadRequest, + "dhcpv4 or dhcpv6 configuration must be complete") + return } s.Stop() - if js.Exists("enabled") { - s.conf.Enabled = newconfig.Enabled + if conf.Enabled != nbNull { + s.conf.Enabled = conf.Enabled == nbTrue } - if js.Exists("interface_name") { - s.conf.InterfaceName = newconfig.InterfaceName + if conf.InterfaceName != "" { + s.conf.InterfaceName = conf.InterfaceName } if s4 != nil { s.srv4 = s4 } + if s6 != nil { s.srv6 = s6 } + s.conf.ConfigModified() s.dbLoad() if s.conf.Enabled { - staticIP, err := sysutil.IfaceHasStaticIP(newconfig.InterfaceName) - if !staticIP && err == nil { - err = sysutil.IfaceSetStaticIP(newconfig.InterfaceName) - if err != nil { - httpError(r, w, http.StatusInternalServerError, "Failed to configure static IP: %s", err) - return - } - } - - err = s.Start() + var code int + code, err = s.enableDHCP(conf.InterfaceName) if err != nil { - httpError(r, w, http.StatusBadRequest, "Failed to start DHCP server: %s", err) + httpError(r, w, code, "enabling dhcp: %s", err) + return } } @@ -225,15 +257,15 @@ func (s *Server) handleDHCPSetConfig(w http.ResponseWriter, r *http.Request) { type netInterfaceJSON struct { Name string `json:"name"` - GatewayIP string `json:"gateway_ip"` + GatewayIP net.IP `json:"gateway_ip"` HardwareAddr string `json:"hardware_address"` - Addrs4 []string `json:"ipv4_addresses"` - Addrs6 []string `json:"ipv6_addresses"` + Addrs4 []net.IP `json:"ipv4_addresses"` + Addrs6 []net.IP `json:"ipv6_addresses"` Flags string `json:"flags"` } func (s *Server) handleDHCPInterfaces(w http.ResponseWriter, r *http.Request) { - response := map[string]interface{}{} + response := map[string]netInterfaceJSON{} ifaces, err := util.GetValidNetInterfaces() if err != nil { @@ -277,9 +309,9 @@ func (s *Server) handleDHCPInterfaces(w http.ResponseWriter, r *http.Request) { continue } if ipnet.IP.To4() != nil { - jsonIface.Addrs4 = append(jsonIface.Addrs4, ipnet.IP.String()) + jsonIface.Addrs4 = append(jsonIface.Addrs4, ipnet.IP) } else { - jsonIface.Addrs6 = append(jsonIface.Addrs6, ipnet.IP.String()) + jsonIface.Addrs6 = append(jsonIface.Addrs6, ipnet.IP) } } if len(jsonIface.Addrs4)+len(jsonIface.Addrs6) != 0 { @@ -295,6 +327,40 @@ func (s *Server) handleDHCPInterfaces(w http.ResponseWriter, r *http.Request) { } } +// dhcpSearchOtherResult contains information about other DHCP server for +// specific network interface. +type dhcpSearchOtherResult struct { + Found string `json:"found,omitempty"` + Error string `json:"error,omitempty"` +} + +// dhcpStaticIPStatus contains information about static IP address for DHCP +// server. +type dhcpStaticIPStatus struct { + Static string `json:"static"` + IP string `json:"ip,omitempty"` + Error string `json:"error,omitempty"` +} + +// dhcpSearchV4Result contains information about DHCPv4 server for specific +// network interface. +type dhcpSearchV4Result struct { + OtherServer dhcpSearchOtherResult `json:"other_server"` + StaticIP dhcpStaticIPStatus `json:"static_ip"` +} + +// dhcpSearchV6Result contains information about DHCPv6 server for specific +// network interface. +type dhcpSearchV6Result struct { + OtherServer dhcpSearchOtherResult `json:"other_server"` +} + +// dhcpSearchResult is a response for /control/dhcp/find_active_dhcp endpoint. +type dhcpSearchResult struct { + V4 dhcpSearchV4Result `json:"v4"` + V6 dhcpSearchV6Result `json:"v6"` +} + // Perform the following tasks: // . Search for another DHCP server running // . Check if a static IP is configured for the network interface @@ -317,50 +383,42 @@ func (s *Server) handleDHCPFindActiveServer(w http.ResponseWriter, r *http.Reque return } + result := dhcpSearchResult{ + V4: dhcpSearchV4Result{ + OtherServer: dhcpSearchOtherResult{}, + StaticIP: dhcpStaticIPStatus{}, + }, + V6: dhcpSearchV6Result{ + OtherServer: dhcpSearchOtherResult{}, + }, + } + found4, err4 := CheckIfOtherDHCPServersPresentV4(interfaceName) - staticIP := map[string]interface{}{} isStaticIP, err := sysutil.IfaceHasStaticIP(interfaceName) - staticIPStatus := "yes" if err != nil { - staticIPStatus = "error" - staticIP["error"] = err.Error() + result.V4.StaticIP.Static = "error" + result.V4.StaticIP.Error = err.Error() } else if !isStaticIP { - staticIPStatus = "no" - staticIP["ip"] = util.GetSubnet(interfaceName) + result.V4.StaticIP.Static = "no" + result.V4.StaticIP.IP = util.GetSubnet(interfaceName).String() } - staticIP["static"] = staticIPStatus - v4 := map[string]interface{}{} - othSrv := map[string]interface{}{} - foundVal := "no" if found4 { - foundVal = "yes" + result.V4.OtherServer.Found = "yes" } else if err4 != nil { - foundVal = "error" - othSrv["error"] = err4.Error() + result.V4.OtherServer.Found = "error" + result.V4.OtherServer.Error = err4.Error() } - othSrv["found"] = foundVal - v4["other_server"] = othSrv - v4["static_ip"] = staticIP found6, err6 := CheckIfOtherDHCPServersPresentV6(interfaceName) - v6 := map[string]interface{}{} - othSrv = map[string]interface{}{} - foundVal = "no" if found6 { - foundVal = "yes" + result.V6.OtherServer.Found = "yes" } else if err6 != nil { - foundVal = "error" - othSrv["error"] = err6.Error() + result.V6.OtherServer.Found = "error" + result.V6.OtherServer.Error = err6.Error() } - othSrv["found"] = foundVal - v6["other_server"] = othSrv - - result := map[string]interface{}{} - result["v4"] = v4 - result["v6"] = v6 w.Header().Set("Content-Type", "application/json") err = json.NewEncoder(w).Encode(result) @@ -371,103 +429,75 @@ func (s *Server) handleDHCPFindActiveServer(w http.ResponseWriter, r *http.Reque } func (s *Server) handleDHCPAddStaticLease(w http.ResponseWriter, r *http.Request) { - lj := staticLeaseJSON{} + lj := Lease{} err := json.NewDecoder(r.Body).Decode(&lj) if err != nil { httpError(r, w, http.StatusBadRequest, "json.Decode: %s", err) + return } - ip := net.ParseIP(lj.IP) - if ip != nil && ip.To4() == nil { - mac, err := net.ParseMAC(lj.HWAddr) - if err != nil { - httpError(r, w, http.StatusBadRequest, "invalid MAC") - return - } + if lj.IP == nil { + httpError(r, w, http.StatusBadRequest, "invalid IP") - lease := Lease{ - IP: ip, - HWAddr: mac, - } + return + } - err = s.srv6.AddStaticLease(lease) + ip4 := lj.IP.To4() + + if ip4 == nil { + lj.IP = lj.IP.To16() + + err = s.srv6.AddStaticLease(lj) if err != nil { httpError(r, w, http.StatusBadRequest, "%s", err) - return } + return } - ip, _ = parseIPv4(lj.IP) - if ip == nil { - httpError(r, w, http.StatusBadRequest, "invalid IP") - return - } - - mac, err := net.ParseMAC(lj.HWAddr) - if err != nil { - httpError(r, w, http.StatusBadRequest, "invalid MAC") - return - } - - lease := Lease{ - IP: ip, - HWAddr: mac, - Hostname: lj.Hostname, - } - err = s.srv4.AddStaticLease(lease) + lj.IP = ip4 + err = s.srv4.AddStaticLease(lj) if err != nil { httpError(r, w, http.StatusBadRequest, "%s", err) + return } } func (s *Server) handleDHCPRemoveStaticLease(w http.ResponseWriter, r *http.Request) { - lj := staticLeaseJSON{} + lj := Lease{} err := json.NewDecoder(r.Body).Decode(&lj) if err != nil { httpError(r, w, http.StatusBadRequest, "json.Decode: %s", err) + return } - ip := net.ParseIP(lj.IP) - if ip != nil && ip.To4() == nil { - mac, err := net.ParseMAC(lj.HWAddr) - if err != nil { - httpError(r, w, http.StatusBadRequest, "invalid MAC") - return - } + if lj.IP == nil { + httpError(r, w, http.StatusBadRequest, "invalid IP") - lease := Lease{ - IP: ip, - HWAddr: mac, - } + return + } - err = s.srv6.RemoveStaticLease(lease) + ip4 := lj.IP.To4() + + if ip4 == nil { + lj.IP = lj.IP.To16() + + err = s.srv6.RemoveStaticLease(lj) if err != nil { httpError(r, w, http.StatusBadRequest, "%s", err) - return } + return } - ip, _ = parseIPv4(lj.IP) - if ip == nil { - httpError(r, w, http.StatusBadRequest, "invalid IP") - return - } - - mac, _ := net.ParseMAC(lj.HWAddr) - - lease := Lease{ - IP: ip, - HWAddr: mac, - Hostname: lj.Hostname, - } - err = s.srv4.RemoveStaticLease(lease) + lj.IP = ip4 + err = s.srv4.RemoveStaticLease(lj) if err != nil { httpError(r, w, http.StatusBadRequest, "%s", err) + return } } diff --git a/internal/dhcpd/dhcphttp_test.go b/internal/dhcpd/http_test.go similarity index 87% rename from internal/dhcpd/dhcphttp_test.go rename to internal/dhcpd/http_test.go index 47b926dc..36a89a6e 100644 --- a/internal/dhcpd/dhcphttp_test.go +++ b/internal/dhcpd/http_test.go @@ -6,6 +6,7 @@ import ( "testing" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) func TestServer_notImplemented(t *testing.T) { @@ -14,7 +15,7 @@ func TestServer_notImplemented(t *testing.T) { w := httptest.NewRecorder() r, err := http.NewRequest(http.MethodGet, "/unsupported", nil) - assert.Nil(t, err) + require.Nil(t, err) h(w, r) assert.Equal(t, http.StatusNotImplemented, w.Code) diff --git a/internal/dhcpd/nclient4/client_test.go b/internal/dhcpd/nclient4/client_test.go index 99f99640..9ad376fe 100644 --- a/internal/dhcpd/nclient4/client_test.go +++ b/internal/dhcpd/nclient4/client_test.go @@ -17,14 +17,14 @@ import ( "testing" "time" - "github.com/AdguardTeam/AdGuardHome/internal/testutil" + "github.com/AdguardTeam/AdGuardHome/internal/aghtest" "github.com/hugelgupf/socketpair" "github.com/insomniacslk/dhcp/dhcpv4" "github.com/insomniacslk/dhcp/dhcpv4/server4" ) func TestMain(m *testing.M) { - testutil.DiscardLogOutput(m) + aghtest.DiscardLogOutput(m) } type handler struct { diff --git a/internal/dhcpd/nullbool.go b/internal/dhcpd/nullbool.go new file mode 100644 index 00000000..b07f6768 --- /dev/null +++ b/internal/dhcpd/nullbool.go @@ -0,0 +1,58 @@ +package dhcpd + +import ( + "bytes" + "fmt" +) + +// nullBool is a nullable boolean. Use these in JSON requests and responses +// instead of pointers to bool. +// +// TODO(a.garipov): Inspect uses of *bool, move this type into some new package +// if we need it somewhere else. +type nullBool uint8 + +// nullBool values +const ( + nbNull nullBool = iota + nbTrue + nbFalse +) + +// String implements the fmt.Stringer interface for nullBool. +func (nb nullBool) String() (s string) { + switch nb { + case nbNull: + return "null" + case nbTrue: + return "true" + case nbFalse: + return "false" + } + + return fmt.Sprintf("!invalid nullBool %d", uint8(nb)) +} + +// boolToNullBool converts a bool into a nullBool. +func boolToNullBool(cond bool) (nb nullBool) { + if cond { + return nbTrue + } + + return nbFalse +} + +// UnmarshalJSON implements the json.Unmarshaler interface for *nullBool. +func (nb *nullBool) UnmarshalJSON(b []byte) (err error) { + if len(b) == 0 || bytes.Equal(b, []byte("null")) { + *nb = nbNull + } else if bytes.Equal(b, []byte("true")) { + *nb = nbTrue + } else if bytes.Equal(b, []byte("false")) { + *nb = nbFalse + } else { + return fmt.Errorf("invalid nullBool value %q", b) + } + + return nil +} diff --git a/internal/dhcpd/nullbool_test.go b/internal/dhcpd/nullbool_test.go new file mode 100644 index 00000000..2570dd44 --- /dev/null +++ b/internal/dhcpd/nullbool_test.go @@ -0,0 +1,69 @@ +package dhcpd + +import ( + "encoding/json" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestNullBool_UnmarshalText(t *testing.T) { + testCases := []struct { + name string + data []byte + wantErrMsg string + want nullBool + }{{ + name: "empty", + data: []byte{}, + wantErrMsg: "", + want: nbNull, + }, { + name: "null", + data: []byte("null"), + wantErrMsg: "", + want: nbNull, + }, { + name: "true", + data: []byte("true"), + wantErrMsg: "", + want: nbTrue, + }, { + name: "false", + data: []byte("false"), + wantErrMsg: "", + want: nbFalse, + }, { + name: "invalid", + data: []byte("flase"), + wantErrMsg: `invalid nullBool value "flase"`, + want: nbNull, + }} + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + var got nullBool + err := got.UnmarshalJSON(tc.data) + if tc.wantErrMsg == "" { + assert.Nil(t, err) + } else { + require.NotNil(t, err) + assert.Equal(t, tc.wantErrMsg, err.Error()) + } + + assert.Equal(t, tc.want, got) + }) + } + + t.Run("json", func(t *testing.T) { + want := nbTrue + var got struct { + A nullBool + } + + err := json.Unmarshal([]byte(`{"A":true}`), &got) + require.Nil(t, err) + assert.Equal(t, want, got.A) + }) +} diff --git a/internal/dhcpd/routeradv.go b/internal/dhcpd/routeradv.go index f1d63c7d..59aad9d1 100644 --- a/internal/dhcpd/routeradv.go +++ b/internal/dhcpd/routeradv.go @@ -13,8 +13,8 @@ import ( ) type raCtx struct { - raAllowSlaac bool // send RA packets without MO flags - raSlaacOnly bool // send RA packets with MO flags + raAllowSLAAC bool // send RA packets without MO flags + raSLAACOnly bool // send RA packets with MO flags ipAddr net.IP // source IP address (link-local-unicast) dnsIPAddr net.IP // IP address for DNS Server option prefixIPAddr net.IP // IP address for Prefix option @@ -159,7 +159,7 @@ func createICMPv6RAPacket(params icmpv6RA) []byte { func (ra *raCtx) Init() error { ra.stop.Store(0) ra.conn = nil - if !(ra.raAllowSlaac || ra.raSlaacOnly) { + if !(ra.raAllowSLAAC || ra.raSLAACOnly) { return nil } @@ -167,8 +167,8 @@ func (ra *raCtx) Init() error { ra.ipAddr, ra.dnsIPAddr) params := icmpv6RA{ - managedAddressConfiguration: !ra.raSlaacOnly, - otherConfiguration: !ra.raSlaacOnly, + managedAddressConfiguration: !ra.raSLAACOnly, + otherConfiguration: !ra.raSLAACOnly, mtu: uint32(ra.iface.MTU), prefixLen: 64, recursiveDNSServer: ra.dnsIPAddr, diff --git a/internal/dhcpd/routeradv_test.go b/internal/dhcpd/routeradv_test.go index 95f3d4fa..4a0f4c5b 100644 --- a/internal/dhcpd/routeradv_test.go +++ b/internal/dhcpd/routeradv_test.go @@ -1,7 +1,6 @@ package dhcpd import ( - "bytes" "net" "testing" @@ -9,7 +8,7 @@ import ( ) func TestRA(t *testing.T) { - ra := icmpv6RA{ + data := createICMPv6RAPacket(icmpv6RA{ managedAddressConfiguration: false, otherConfiguration: true, mtu: 1500, @@ -17,8 +16,7 @@ func TestRA(t *testing.T) { prefixLen: 64, recursiveDNSServer: net.ParseIP("fe80::800:27ff:fe00:0"), sourceLinkLayerAddress: []byte{0x0a, 0x00, 0x27, 0x00, 0x00, 0x00}, - } - data := createICMPv6RAPacket(ra) + }) dataCorrect := []byte{ 0x86, 0x00, 0x00, 0x00, 0x40, 0x40, 0x07, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0x04, 0x40, 0xc0, 0x00, 0x00, 0x0e, 0x10, 0x00, 0x00, 0x0e, 0x10, 0x00, 0x00, 0x00, 0x00, @@ -27,5 +25,5 @@ func TestRA(t *testing.T) { 0x19, 0x03, 0x00, 0x00, 0x00, 0x00, 0x0e, 0x10, 0xfe, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x08, 0x00, 0x27, 0xff, 0xfe, 0x00, 0x00, 0x00, } - assert.True(t, bytes.Equal(data, dataCorrect)) + assert.Equal(t, dataCorrect, data) } diff --git a/internal/dhcpd/server.go b/internal/dhcpd/server.go index 240715ca..2fac533e 100644 --- a/internal/dhcpd/server.go +++ b/internal/dhcpd/server.go @@ -33,22 +33,22 @@ type DHCPServer interface { // V4ServerConf - server configuration type V4ServerConf struct { - Enabled bool `yaml:"-"` - InterfaceName string `yaml:"-"` + Enabled bool `yaml:"-" json:"-"` + InterfaceName string `yaml:"-" json:"-"` - GatewayIP string `yaml:"gateway_ip"` - SubnetMask string `yaml:"subnet_mask"` + GatewayIP net.IP `yaml:"gateway_ip" json:"gateway_ip"` + SubnetMask net.IP `yaml:"subnet_mask" json:"subnet_mask"` // The first & the last IP address for dynamic leases // Bytes [0..2] of the last allowed IP address must match the first IP - RangeStart string `yaml:"range_start"` - RangeEnd string `yaml:"range_end"` + RangeStart net.IP `yaml:"range_start" json:"range_start"` + RangeEnd net.IP `yaml:"range_end" json:"range_end"` - LeaseDuration uint32 `yaml:"lease_duration"` // in seconds + LeaseDuration uint32 `yaml:"lease_duration" json:"lease_duration"` // in seconds // IP conflict detector: time (ms) to wait for ICMP reply // 0: disable - ICMPTimeout uint32 `yaml:"icmp_timeout_msec"` + ICMPTimeout uint32 `yaml:"icmp_timeout_msec" json:"-"` // Custom Options. // @@ -58,7 +58,7 @@ type V4ServerConf struct { // // Option with IP data (only 1 IP is supported): // DEC_CODE ip IP_ADDR - Options []string `yaml:"options"` + Options []string `yaml:"options" json:"-"` ipStart net.IP // starting IP address for dynamic leases ipEnd net.IP // ending IP address for dynamic leases @@ -74,17 +74,17 @@ type V4ServerConf struct { // V6ServerConf - server configuration type V6ServerConf struct { - Enabled bool `yaml:"-"` - InterfaceName string `yaml:"-"` + Enabled bool `yaml:"-" json:"-"` + InterfaceName string `yaml:"-" json:"-"` // The first IP address for dynamic leases // The last allowed IP address ends with 0xff byte - RangeStart string `yaml:"range_start"` + RangeStart net.IP `yaml:"range_start" json:"range_start"` - LeaseDuration uint32 `yaml:"lease_duration"` // in seconds + LeaseDuration uint32 `yaml:"lease_duration" json:"lease_duration"` // in seconds - RaSlaacOnly bool `yaml:"ra_slaac_only"` // send ICMPv6.RA packets without MO flags - RaAllowSlaac bool `yaml:"ra_allow_slaac"` // send ICMPv6.RA packets with MO flags + RASLAACOnly bool `yaml:"ra_slaac_only" json:"-"` // send ICMPv6.RA packets without MO flags + RAAllowSLAAC bool `yaml:"ra_allow_slaac" json:"-"` // send ICMPv6.RA packets with MO flags ipStart net.IP // starting IP address for dynamic leases leaseTime time.Duration // the time during which a dynamic lease is considered valid diff --git a/internal/dhcpd/v4.go b/internal/dhcpd/v4.go index 81ba3a1d..7d24699e 100644 --- a/internal/dhcpd/v4.go +++ b/internal/dhcpd/v4.go @@ -23,7 +23,8 @@ type v4Server struct { srv *server4.Server leasesLock sync.Mutex leases []*Lease - ipAddrs [256]byte + // TODO(e.burkov): This field type should be a normal bitmap. + ipAddrs [256]byte conf V4ServerConf } @@ -77,7 +78,10 @@ func (s *v4Server) blacklisted(l *Lease) bool { // GetLeases returns the list of current DHCP leases (thread-safe) func (s *v4Server) GetLeases(flags int) []Lease { - var result []Lease + // The function shouldn't return nil value because zero-length slice + // behaves differently in cases like marshalling. Our front-end also + // requires non-nil value in the response. + result := []Lease{} now := time.Now().Unix() s.leasesLock.Lock() @@ -589,7 +593,7 @@ func (s *v4Server) Start() error { s.conf.dnsIPAddrs = dnsIPAddrs laddr := &net.UDPAddr{ - IP: net.ParseIP("0.0.0.0"), + IP: net.IP{0, 0, 0, 0}, Port: dhcpv4.ServerPort, } s.srv, err = server4.NewServer(iface.Name, laddr, s.packetHandler, server4.WithDebugLogger()) @@ -632,19 +636,18 @@ func v4Create(conf V4ServerConf) (DHCPServer, error) { } var err error - s.conf.routerIP, err = parseIPv4(s.conf.GatewayIP) + s.conf.routerIP, err = tryTo4(s.conf.GatewayIP) if err != nil { return s, fmt.Errorf("dhcpv4: %w", err) } - subnet, err := parseIPv4(s.conf.SubnetMask) - if err != nil || !isValidSubnetMask(subnet) { - return s, fmt.Errorf("dhcpv4: invalid subnet mask: %s", s.conf.SubnetMask) + if s.conf.SubnetMask == nil { + return s, fmt.Errorf("dhcpv4: invalid subnet mask: %v", s.conf.SubnetMask) } s.conf.subnetMask = make([]byte, 4) - copy(s.conf.subnetMask, subnet) + copy(s.conf.subnetMask, s.conf.SubnetMask.To4()) - s.conf.ipStart, err = parseIPv4(conf.RangeStart) + s.conf.ipStart, err = tryTo4(conf.RangeStart) if s.conf.ipStart == nil { return s, fmt.Errorf("dhcpv4: %w", err) } @@ -652,7 +655,7 @@ func v4Create(conf V4ServerConf) (DHCPServer, error) { return s, fmt.Errorf("dhcpv4: invalid range start IP") } - s.conf.ipEnd, err = parseIPv4(conf.RangeEnd) + s.conf.ipEnd, err = tryTo4(conf.RangeEnd) if s.conf.ipEnd == nil { return s, fmt.Errorf("dhcpv4: %w", err) } diff --git a/internal/dhcpd/v46_test.go b/internal/dhcpd/v46_test.go index 6007205d..6495eeee 100644 --- a/internal/dhcpd/v46_test.go +++ b/internal/dhcpd/v46_test.go @@ -7,6 +7,7 @@ import ( "github.com/AdguardTeam/AdGuardHome/internal/agherr" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) type fakeIface struct { @@ -79,8 +80,8 @@ func TestIfaceIPAddrs(t *testing.T) { for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { got, gotErr := ifaceIPAddrs(tc.iface, tc.ipv) + require.True(t, errors.Is(gotErr, tc.wantErr)) assert.Equal(t, tc.want, got) - assert.True(t, errors.Is(gotErr, tc.wantErr)) }) } } @@ -140,12 +141,8 @@ func TestIfaceDNSIPAddrs(t *testing.T) { want: nil, wantErr: errTest, }, { - name: "ipv4_wait", - iface: &waitingFakeIface{ - addrs: []net.Addr{addr4}, - err: nil, - n: 1, - }, + name: "ipv4_wait", + iface: &waitingFakeIface{addrs: []net.Addr{addr4}, err: nil, n: 1}, ipv: ipVersion4, want: []net.IP{ip4, ip4}, wantErr: nil, @@ -168,12 +165,8 @@ func TestIfaceDNSIPAddrs(t *testing.T) { want: nil, wantErr: errTest, }, { - name: "ipv6_wait", - iface: &waitingFakeIface{ - addrs: []net.Addr{addr6}, - err: nil, - n: 1, - }, + name: "ipv6_wait", + iface: &waitingFakeIface{addrs: []net.Addr{addr6}, err: nil, n: 1}, ipv: ipVersion6, want: []net.IP{ip6, ip6}, wantErr: nil, @@ -182,8 +175,8 @@ func TestIfaceDNSIPAddrs(t *testing.T) { for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { got, gotErr := ifaceDNSIPAddrs(tc.iface, tc.ipv, 2, 0) + require.True(t, errors.Is(gotErr, tc.wantErr)) assert.Equal(t, tc.want, got) - assert.True(t, errors.Is(gotErr, tc.wantErr)) }) } } diff --git a/internal/dhcpd/v4_test.go b/internal/dhcpd/v4_test.go index fe3ac2dd..d204a200 100644 --- a/internal/dhcpd/v4_test.go +++ b/internal/dhcpd/v4_test.go @@ -8,231 +8,283 @@ import ( "github.com/insomniacslk/dhcp/dhcpv4" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) func notify4(flags uint32) { } -func TestV4StaticLeaseAddRemove(t *testing.T) { - conf := V4ServerConf{ +func TestV4_AddRemove_static(t *testing.T) { + s, err := v4Create(V4ServerConf{ Enabled: true, - RangeStart: "192.168.10.100", - RangeEnd: "192.168.10.200", - GatewayIP: "192.168.10.1", - SubnetMask: "255.255.255.0", + RangeStart: net.IP{192, 168, 10, 100}, + RangeEnd: net.IP{192, 168, 10, 200}, + GatewayIP: net.IP{192, 168, 10, 1}, + SubnetMask: net.IP{255, 255, 255, 0}, notify: notify4, - } - s, err := v4Create(conf) - assert.True(t, err == nil) + }) + require.Nil(t, err) ls := s.GetLeases(LeasesStatic) - assert.Equal(t, 0, len(ls)) + assert.Empty(t, ls) - // add static lease - l := Lease{} - l.IP = net.ParseIP("192.168.10.150").To4() - l.HWAddr, _ = net.ParseMAC("aa:aa:aa:aa:aa:aa") - assert.True(t, s.AddStaticLease(l) == nil) + // Add static lease. + l := Lease{ + IP: net.IP{192, 168, 10, 150}, + HWAddr: net.HardwareAddr{0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA}, + } + require.Nil(t, s.AddStaticLease(l)) + assert.NotNil(t, s.AddStaticLease(l)) - // try to add the same static lease - fail - assert.True(t, s.AddStaticLease(l) != nil) - - // check ls = s.GetLeases(LeasesStatic) - assert.Equal(t, 1, len(ls)) - assert.Equal(t, "192.168.10.150", ls[0].IP.String()) - assert.Equal(t, "aa:aa:aa:aa:aa:aa", ls[0].HWAddr.String()) - assert.True(t, ls[0].Expiry.Unix() == leaseExpireStatic) + require.Len(t, ls, 1) + assert.True(t, l.IP.Equal(ls[0].IP)) + assert.Equal(t, l.HWAddr, ls[0].HWAddr) + assert.EqualValues(t, leaseExpireStatic, ls[0].Expiry.Unix()) - // try to remove static lease - fail - l.IP = net.ParseIP("192.168.10.110").To4() - l.HWAddr, _ = net.ParseMAC("aa:aa:aa:aa:aa:aa") - assert.True(t, s.RemoveStaticLease(l) != nil) + // Try to remove static lease. + assert.NotNil(t, s.RemoveStaticLease(Lease{ + IP: net.IP{192, 168, 10, 110}, + HWAddr: net.HardwareAddr{0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA}, + })) - // remove static lease - l.IP = net.ParseIP("192.168.10.150").To4() - l.HWAddr, _ = net.ParseMAC("aa:aa:aa:aa:aa:aa") - assert.True(t, s.RemoveStaticLease(l) == nil) - - // check + // Remove static lease. + require.Nil(t, s.RemoveStaticLease(l)) ls = s.GetLeases(LeasesStatic) - assert.Equal(t, 0, len(ls)) + assert.Empty(t, ls) } -func TestV4StaticLeaseAddReplaceDynamic(t *testing.T) { - conf := V4ServerConf{ +func TestV4_AddReplace(t *testing.T) { + sIface, err := v4Create(V4ServerConf{ Enabled: true, - RangeStart: "192.168.10.100", - RangeEnd: "192.168.10.200", - GatewayIP: "192.168.10.1", - SubnetMask: "255.255.255.0", + RangeStart: net.IP{192, 168, 10, 100}, + RangeEnd: net.IP{192, 168, 10, 200}, + GatewayIP: net.IP{192, 168, 10, 1}, + SubnetMask: net.IP{255, 255, 255, 0}, notify: notify4, - } - sIface, err := v4Create(conf) - s := sIface.(*v4Server) - assert.True(t, err == nil) + }) + require.Nil(t, err) - // add dynamic lease - ld := Lease{} - ld.IP = net.ParseIP("192.168.10.150").To4() - ld.HWAddr, _ = net.ParseMAC("11:aa:aa:aa:aa:aa") - s.addLease(&ld) + s, ok := sIface.(*v4Server) + require.True(t, ok) - // add dynamic lease - { - ld := Lease{} - ld.IP = net.ParseIP("192.168.10.151").To4() - ld.HWAddr, _ = net.ParseMAC("22:aa:aa:aa:aa:aa") - s.addLease(&ld) + dynLeases := []Lease{{ + IP: net.IP{192, 168, 10, 150}, + HWAddr: net.HardwareAddr{0x11, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA}, + }, { + IP: net.IP{192, 168, 10, 151}, + HWAddr: net.HardwareAddr{0x22, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA}, + }} + + for i := range dynLeases { + s.addLease(&dynLeases[i]) } - // add static lease with the same IP - l := Lease{} - l.IP = net.ParseIP("192.168.10.150").To4() - l.HWAddr, _ = net.ParseMAC("33:aa:aa:aa:aa:aa") - assert.True(t, s.AddStaticLease(l) == nil) + stLeases := []Lease{{ + IP: net.IP{192, 168, 10, 150}, + HWAddr: net.HardwareAddr{0x33, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA}, + }, { + IP: net.IP{192, 168, 10, 152}, + HWAddr: net.HardwareAddr{0x22, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA}, + }} - // add static lease with the same MAC - l = Lease{} - l.IP = net.ParseIP("192.168.10.152").To4() - l.HWAddr, _ = net.ParseMAC("22:aa:aa:aa:aa:aa") - assert.True(t, s.AddStaticLease(l) == nil) + for _, l := range stLeases { + require.Nil(t, s.AddStaticLease(l)) + } - // check ls := s.GetLeases(LeasesStatic) - assert.Equal(t, 2, len(ls)) + require.Len(t, ls, 2) - assert.Equal(t, "192.168.10.150", ls[0].IP.String()) - assert.Equal(t, "33:aa:aa:aa:aa:aa", ls[0].HWAddr.String()) - assert.True(t, ls[0].Expiry.Unix() == leaseExpireStatic) - - assert.Equal(t, "192.168.10.152", ls[1].IP.String()) - assert.Equal(t, "22:aa:aa:aa:aa:aa", ls[1].HWAddr.String()) - assert.True(t, ls[1].Expiry.Unix() == leaseExpireStatic) + for i, l := range ls { + assert.True(t, stLeases[i].IP.Equal(l.IP)) + assert.Equal(t, stLeases[i].HWAddr, l.HWAddr) + assert.EqualValues(t, leaseExpireStatic, l.Expiry.Unix()) + } } -func TestV4StaticLeaseGet(t *testing.T) { - conf := V4ServerConf{ +func TestV4StaticLease_Get(t *testing.T) { + var err error + sIface, err := v4Create(V4ServerConf{ Enabled: true, - RangeStart: "192.168.10.100", - RangeEnd: "192.168.10.200", - GatewayIP: "192.168.10.1", - SubnetMask: "255.255.255.0", + RangeStart: net.IP{192, 168, 10, 100}, + RangeEnd: net.IP{192, 168, 10, 200}, + GatewayIP: net.IP{192, 168, 10, 1}, + SubnetMask: net.IP{255, 255, 255, 0}, notify: notify4, + }) + require.Nil(t, err) + + s, ok := sIface.(*v4Server) + require.True(t, ok) + s.conf.dnsIPAddrs = []net.IP{{192, 168, 10, 1}} + + l := Lease{ + IP: net.IP{192, 168, 10, 150}, + HWAddr: net.HardwareAddr{0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA}, } - sIface, err := v4Create(conf) - s := sIface.(*v4Server) - assert.True(t, err == nil) - s.conf.dnsIPAddrs = []net.IP{net.ParseIP("192.168.10.1").To4()} + require.Nil(t, s.AddStaticLease(l)) - l := Lease{} - l.IP = net.ParseIP("192.168.10.150").To4() - l.HWAddr, _ = net.ParseMAC("aa:aa:aa:aa:aa:aa") - assert.True(t, s.AddStaticLease(l) == nil) + var req, resp *dhcpv4.DHCPv4 + mac := net.HardwareAddr{0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA} - // "Discover" - mac, _ := net.ParseMAC("aa:aa:aa:aa:aa:aa") - req, _ := dhcpv4.NewDiscovery(mac) - resp, _ := dhcpv4.NewReplyFromRequest(req) - assert.Equal(t, 1, s.process(req, resp)) + t.Run("discover", func(t *testing.T) { + var err error - // check "Offer" - assert.Equal(t, dhcpv4.MessageTypeOffer, resp.MessageType()) - assert.Equal(t, "aa:aa:aa:aa:aa:aa", resp.ClientHWAddr.String()) - assert.Equal(t, "192.168.10.150", resp.YourIPAddr.String()) - assert.Equal(t, "192.168.10.1", resp.Router()[0].String()) - assert.Equal(t, "192.168.10.1", resp.ServerIdentifier().String()) - assert.Equal(t, "255.255.255.0", net.IP(resp.SubnetMask()).String()) - assert.Equal(t, s.conf.leaseTime.Seconds(), resp.IPAddressLeaseTime(-1).Seconds()) + req, err = dhcpv4.NewDiscovery(mac) + require.Nil(t, err) - // "Request" - req, _ = dhcpv4.NewRequestFromOffer(resp) - resp, _ = dhcpv4.NewReplyFromRequest(req) - assert.Equal(t, 1, s.process(req, resp)) + resp, err = dhcpv4.NewReplyFromRequest(req) + require.Nil(t, err) + assert.Equal(t, 1, s.process(req, resp)) + }) + require.Nil(t, err) - // check "Ack" - assert.Equal(t, dhcpv4.MessageTypeAck, resp.MessageType()) - assert.Equal(t, "aa:aa:aa:aa:aa:aa", resp.ClientHWAddr.String()) - assert.Equal(t, "192.168.10.150", resp.YourIPAddr.String()) - assert.Equal(t, "192.168.10.1", resp.Router()[0].String()) - assert.Equal(t, "192.168.10.1", resp.ServerIdentifier().String()) - assert.Equal(t, "255.255.255.0", net.IP(resp.SubnetMask()).String()) - assert.Equal(t, s.conf.leaseTime.Seconds(), resp.IPAddressLeaseTime(-1).Seconds()) + t.Run("offer", func(t *testing.T) { + assert.Equal(t, dhcpv4.MessageTypeOffer, resp.MessageType()) + assert.Equal(t, mac, resp.ClientHWAddr) + assert.True(t, l.IP.Equal(resp.YourIPAddr)) + assert.True(t, s.conf.GatewayIP.Equal(resp.Router()[0])) + assert.True(t, s.conf.GatewayIP.Equal(resp.ServerIdentifier())) + assert.Equal(t, s.conf.subnetMask, resp.SubnetMask()) + assert.Equal(t, s.conf.leaseTime.Seconds(), resp.IPAddressLeaseTime(-1).Seconds()) + }) + + t.Run("request", func(t *testing.T) { + req, err = dhcpv4.NewRequestFromOffer(resp) + require.Nil(t, err) + + resp, err = dhcpv4.NewReplyFromRequest(req) + require.Nil(t, err) + assert.Equal(t, 1, s.process(req, resp)) + }) + require.Nil(t, err) + + t.Run("ack", func(t *testing.T) { + assert.Equal(t, dhcpv4.MessageTypeAck, resp.MessageType()) + assert.Equal(t, mac, resp.ClientHWAddr) + assert.True(t, l.IP.Equal(resp.YourIPAddr)) + assert.True(t, s.conf.GatewayIP.Equal(resp.Router()[0])) + assert.True(t, s.conf.GatewayIP.Equal(resp.ServerIdentifier())) + assert.Equal(t, s.conf.subnetMask, resp.SubnetMask()) + assert.Equal(t, s.conf.leaseTime.Seconds(), resp.IPAddressLeaseTime(-1).Seconds()) + }) dnsAddrs := resp.DNS() - assert.Equal(t, 1, len(dnsAddrs)) - assert.Equal(t, "192.168.10.1", dnsAddrs[0].String()) + require.Len(t, dnsAddrs, 1) + assert.True(t, s.conf.GatewayIP.Equal(dnsAddrs[0])) - // check lease - ls := s.GetLeases(LeasesStatic) - assert.Equal(t, 1, len(ls)) - assert.Equal(t, "192.168.10.150", ls[0].IP.String()) - assert.Equal(t, "aa:aa:aa:aa:aa:aa", ls[0].HWAddr.String()) + t.Run("check_lease", func(t *testing.T) { + ls := s.GetLeases(LeasesStatic) + require.Len(t, ls, 1) + assert.True(t, l.IP.Equal(ls[0].IP)) + assert.Equal(t, mac, ls[0].HWAddr) + }) } -func TestV4DynamicLeaseGet(t *testing.T) { - conf := V4ServerConf{ +func TestV4DynamicLease_Get(t *testing.T) { + var err error + sIface, err := v4Create(V4ServerConf{ Enabled: true, - RangeStart: "192.168.10.100", - RangeEnd: "192.168.10.200", - GatewayIP: "192.168.10.1", - SubnetMask: "255.255.255.0", + RangeStart: net.IP{192, 168, 10, 100}, + RangeEnd: net.IP{192, 168, 10, 200}, + GatewayIP: net.IP{192, 168, 10, 1}, + SubnetMask: net.IP{255, 255, 255, 0}, notify: notify4, Options: []string{ "81 hex 303132", "82 ip 1.2.3.4", }, - } - sIface, err := v4Create(conf) - s := sIface.(*v4Server) - assert.True(t, err == nil) - s.conf.dnsIPAddrs = []net.IP{net.ParseIP("192.168.10.1").To4()} + }) + require.Nil(t, err) - // "Discover" - mac, _ := net.ParseMAC("aa:aa:aa:aa:aa:aa") - req, _ := dhcpv4.NewDiscovery(mac) - resp, _ := dhcpv4.NewReplyFromRequest(req) - assert.Equal(t, 1, s.process(req, resp)) + s, ok := sIface.(*v4Server) + require.True(t, ok) + s.conf.dnsIPAddrs = []net.IP{{192, 168, 10, 1}} - // check "Offer" - assert.Equal(t, dhcpv4.MessageTypeOffer, resp.MessageType()) - assert.Equal(t, "aa:aa:aa:aa:aa:aa", resp.ClientHWAddr.String()) - assert.Equal(t, "192.168.10.100", resp.YourIPAddr.String()) - assert.Equal(t, "192.168.10.1", resp.Router()[0].String()) - assert.Equal(t, "192.168.10.1", resp.ServerIdentifier().String()) - assert.Equal(t, "255.255.255.0", net.IP(resp.SubnetMask()).String()) - assert.Equal(t, s.conf.leaseTime.Seconds(), resp.IPAddressLeaseTime(-1).Seconds()) - assert.Equal(t, []byte("012"), resp.Options[uint8(dhcpv4.OptionFQDN)]) - assert.Equal(t, "1.2.3.4", net.IP(resp.Options[uint8(dhcpv4.OptionRelayAgentInformation)]).String()) + var req, resp *dhcpv4.DHCPv4 + mac := net.HardwareAddr{0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA} - // "Request" - req, _ = dhcpv4.NewRequestFromOffer(resp) - resp, _ = dhcpv4.NewReplyFromRequest(req) - assert.Equal(t, 1, s.process(req, resp)) + t.Run("discover", func(t *testing.T) { + req, err = dhcpv4.NewDiscovery(mac) + require.Nil(t, err) - // check "Ack" - assert.Equal(t, dhcpv4.MessageTypeAck, resp.MessageType()) - assert.Equal(t, "aa:aa:aa:aa:aa:aa", resp.ClientHWAddr.String()) - assert.Equal(t, "192.168.10.100", resp.YourIPAddr.String()) - assert.Equal(t, "192.168.10.1", resp.Router()[0].String()) - assert.Equal(t, "192.168.10.1", resp.ServerIdentifier().String()) - assert.Equal(t, "255.255.255.0", net.IP(resp.SubnetMask()).String()) - assert.Equal(t, s.conf.leaseTime.Seconds(), resp.IPAddressLeaseTime(-1).Seconds()) + resp, err = dhcpv4.NewReplyFromRequest(req) + require.Nil(t, err) + assert.Equal(t, 1, s.process(req, resp)) + }) + require.Nil(t, err) + + t.Run("offer", func(t *testing.T) { + assert.Equal(t, dhcpv4.MessageTypeOffer, resp.MessageType()) + assert.Equal(t, mac, resp.ClientHWAddr) + assert.True(t, s.conf.RangeStart.Equal(resp.YourIPAddr)) + assert.True(t, s.conf.GatewayIP.Equal(resp.Router()[0])) + assert.True(t, s.conf.GatewayIP.Equal(resp.ServerIdentifier())) + assert.Equal(t, s.conf.subnetMask, resp.SubnetMask()) + assert.Equal(t, s.conf.leaseTime.Seconds(), resp.IPAddressLeaseTime(-1).Seconds()) + assert.Equal(t, []byte("012"), resp.Options[uint8(dhcpv4.OptionFQDN)]) + assert.True(t, net.IP{1, 2, 3, 4}.Equal(net.IP(resp.Options[uint8(dhcpv4.OptionRelayAgentInformation)]))) + }) + + t.Run("request", func(t *testing.T) { + var err error + + req, err = dhcpv4.NewRequestFromOffer(resp) + require.Nil(t, err) + + resp, err = dhcpv4.NewReplyFromRequest(req) + require.Nil(t, err) + assert.Equal(t, 1, s.process(req, resp)) + }) + require.Nil(t, err) + + t.Run("ack", func(t *testing.T) { + assert.Equal(t, dhcpv4.MessageTypeAck, resp.MessageType()) + assert.Equal(t, mac, resp.ClientHWAddr) + assert.True(t, s.conf.RangeStart.Equal(resp.YourIPAddr)) + assert.True(t, s.conf.GatewayIP.Equal(resp.Router()[0])) + assert.True(t, s.conf.GatewayIP.Equal(resp.ServerIdentifier())) + assert.Equal(t, s.conf.subnetMask, resp.SubnetMask()) + assert.Equal(t, s.conf.leaseTime.Seconds(), resp.IPAddressLeaseTime(-1).Seconds()) + }) dnsAddrs := resp.DNS() - assert.Equal(t, 1, len(dnsAddrs)) - assert.Equal(t, "192.168.10.1", dnsAddrs[0].String()) + require.Len(t, dnsAddrs, 1) + assert.True(t, net.IP{192, 168, 10, 1}.Equal(dnsAddrs[0])) // check lease - ls := s.GetLeases(LeasesDynamic) - assert.Equal(t, 1, len(ls)) - assert.Equal(t, "192.168.10.100", ls[0].IP.String()) - assert.Equal(t, "aa:aa:aa:aa:aa:aa", ls[0].HWAddr.String()) - - start := net.ParseIP("192.168.10.100").To4() - stop := net.ParseIP("192.168.10.200").To4() - assert.True(t, !ip4InRange(start, stop, net.ParseIP("192.168.10.99").To4())) - assert.True(t, !ip4InRange(start, stop, net.ParseIP("192.168.11.100").To4())) - assert.True(t, !ip4InRange(start, stop, net.ParseIP("192.168.11.201").To4())) - assert.True(t, ip4InRange(start, stop, net.ParseIP("192.168.10.100").To4())) + t.Run("check_lease", func(t *testing.T) { + ls := s.GetLeases(LeasesDynamic) + assert.Len(t, ls, 1) + assert.True(t, net.IP{192, 168, 10, 100}.Equal(ls[0].IP)) + assert.Equal(t, mac, ls[0].HWAddr) + }) +} + +func TestIP4InRange(t *testing.T) { + start := net.IP{192, 168, 10, 100} + stop := net.IP{192, 168, 10, 200} + + testCases := []struct { + ip net.IP + want bool + }{{ + ip: net.IP{192, 168, 10, 99}, + want: false, + }, { + ip: net.IP{192, 168, 11, 100}, + want: false, + }, { + ip: net.IP{192, 168, 11, 201}, + want: false, + }, { + ip: start, + want: true, + }} + + for _, tc := range testCases { + t.Run(tc.ip.String(), func(t *testing.T) { + assert.Equal(t, tc.want, ip4InRange(start, stop, tc.ip)) + }) + } } diff --git a/internal/dhcpd/v6.go b/internal/dhcpd/v6.go index 2dd41b5c..0b43d6d1 100644 --- a/internal/dhcpd/v6.go +++ b/internal/dhcpd/v6.go @@ -42,7 +42,6 @@ func (s *v6Server) WriteDiskConfig6(c *V6ServerConf) { } // Return TRUE if IP address is within range [start..0xff] -// nolint(staticcheck) func ip6InRange(start, ip net.IP) bool { if len(start) != 16 { return false @@ -72,7 +71,10 @@ func (s *v6Server) ResetLeases(ll []*Lease) { // GetLeases - get current leases func (s *v6Server) GetLeases(flags int) []Lease { - var result []Lease + // The function shouldn't return nil value because zero-length slice + // behaves differently in cases like marshalling. Our front-end also + // requires non-nil value in the response. + result := []Lease{} s.leasesLock.Lock() for _, lease := range s.leases { if lease.Expiry.Unix() == leaseExpireStatic { @@ -550,8 +552,8 @@ func (s *v6Server) initRA(iface *net.Interface) error { } } - s.ra.raAllowSlaac = s.conf.RaAllowSlaac - s.ra.raSlaacOnly = s.conf.RaSlaacOnly + s.ra.raAllowSLAAC = s.conf.RAAllowSLAAC + s.ra.raSLAACOnly = s.conf.RASLAACOnly s.ra.dnsIPAddr = s.ra.ipAddr s.ra.prefixIPAddr = s.conf.ipStart s.ra.ifaceName = s.conf.InterfaceName @@ -592,7 +594,7 @@ func (s *v6Server) Start() error { } // don't initialize DHCPv6 server if we must force the clients to use SLAAC - if s.conf.RaSlaacOnly { + if s.conf.RASLAACOnly { log.Debug("DHCPv6: not starting DHCPv6 server due to ra_slaac_only=true") return nil } @@ -657,7 +659,7 @@ func v6Create(conf V6ServerConf) (DHCPServer, error) { return s, nil } - s.conf.ipStart = net.ParseIP(conf.RangeStart) + s.conf.ipStart = conf.RangeStart if s.conf.ipStart == nil || s.conf.ipStart.To16() == nil { return s, fmt.Errorf("dhcpv6: invalid range-start IP: %s", conf.RangeStart) } diff --git a/internal/dhcpd/v6_test.go b/internal/dhcpd/v6_test.go index 7d7dd678..3eb06a89 100644 --- a/internal/dhcpd/v6_test.go +++ b/internal/dhcpd/v6_test.go @@ -9,217 +9,283 @@ import ( "github.com/insomniacslk/dhcp/dhcpv6" "github.com/insomniacslk/dhcp/iana" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) func notify6(flags uint32) { } -func TestV6StaticLeaseAddRemove(t *testing.T) { - conf := V6ServerConf{ +func TestV6_AddRemove_static(t *testing.T) { + s, err := v6Create(V6ServerConf{ Enabled: true, - RangeStart: "2001::1", + RangeStart: net.ParseIP("2001::1"), notify: notify6, + }) + require.Nil(t, err) + + require.Empty(t, s.GetLeases(LeasesStatic)) + + // Add static lease. + l := Lease{ + IP: net.ParseIP("2001::1"), + HWAddr: net.HardwareAddr{0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA}, } - s, err := v6Create(conf) - assert.True(t, err == nil) + require.Nil(t, s.AddStaticLease(l)) + + // Try to add the same static lease. + require.NotNil(t, s.AddStaticLease(l)) ls := s.GetLeases(LeasesStatic) - assert.Equal(t, 0, len(ls)) + require.Len(t, ls, 1) + assert.Equal(t, l.IP, ls[0].IP) + assert.Equal(t, l.HWAddr, ls[0].HWAddr) + assert.EqualValues(t, leaseExpireStatic, ls[0].Expiry.Unix()) - // add static lease - l := Lease{} - l.IP = net.ParseIP("2001::1") - l.HWAddr, _ = net.ParseMAC("aa:aa:aa:aa:aa:aa") - assert.True(t, s.AddStaticLease(l) == nil) + // Try to remove non-existent static lease. + require.NotNil(t, s.RemoveStaticLease(Lease{ + IP: net.ParseIP("2001::2"), + HWAddr: l.HWAddr, + })) - // try to add static lease - fail - assert.True(t, s.AddStaticLease(l) != nil) + // Remove static lease. + require.Nil(t, s.RemoveStaticLease(l)) - // check - ls = s.GetLeases(LeasesStatic) - assert.Equal(t, 1, len(ls)) - assert.Equal(t, "2001::1", ls[0].IP.String()) - assert.Equal(t, "aa:aa:aa:aa:aa:aa", ls[0].HWAddr.String()) - assert.True(t, ls[0].Expiry.Unix() == leaseExpireStatic) - - // try to remove static lease - fail - l.IP = net.ParseIP("2001::2") - l.HWAddr, _ = net.ParseMAC("aa:aa:aa:aa:aa:aa") - assert.True(t, s.RemoveStaticLease(l) != nil) - - // remove static lease - l.IP = net.ParseIP("2001::1") - l.HWAddr, _ = net.ParseMAC("aa:aa:aa:aa:aa:aa") - assert.True(t, s.RemoveStaticLease(l) == nil) - - // check - ls = s.GetLeases(LeasesStatic) - assert.Equal(t, 0, len(ls)) + assert.Empty(t, s.GetLeases(LeasesStatic)) } -func TestV6StaticLeaseAddReplaceDynamic(t *testing.T) { - conf := V6ServerConf{ +func TestV6_AddReplace(t *testing.T) { + sIface, err := v6Create(V6ServerConf{ Enabled: true, - RangeStart: "2001::1", + RangeStart: net.ParseIP("2001::1"), notify: notify6, - } - sIface, err := v6Create(conf) - s := sIface.(*v6Server) - assert.True(t, err == nil) + }) + require.Nil(t, err) + s, ok := sIface.(*v6Server) + require.True(t, ok) - // add dynamic lease - ld := Lease{} - ld.IP = net.ParseIP("2001::1") - ld.HWAddr, _ = net.ParseMAC("11:aa:aa:aa:aa:aa") - s.addLease(&ld) + // Add dynamic leases. + dynLeases := []*Lease{{ + IP: net.ParseIP("2001::1"), + HWAddr: net.HardwareAddr{0x11, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA}, + }, { + IP: net.ParseIP("2001::2"), + HWAddr: net.HardwareAddr{0x22, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA}, + }} - // add dynamic lease - { - ld := Lease{} - ld.IP = net.ParseIP("2001::2") - ld.HWAddr, _ = net.ParseMAC("22:aa:aa:aa:aa:aa") - s.addLease(&ld) + for _, l := range dynLeases { + s.addLease(l) } - // add static lease with the same IP - l := Lease{} - l.IP = net.ParseIP("2001::1") - l.HWAddr, _ = net.ParseMAC("33:aa:aa:aa:aa:aa") - assert.True(t, s.AddStaticLease(l) == nil) + stLeases := []Lease{{ + IP: net.ParseIP("2001::1"), + HWAddr: net.HardwareAddr{0x33, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA}, + }, { + IP: net.ParseIP("2001::3"), + HWAddr: net.HardwareAddr{0x22, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA}, + }} - // add static lease with the same MAC - l = Lease{} - l.IP = net.ParseIP("2001::3") - l.HWAddr, _ = net.ParseMAC("22:aa:aa:aa:aa:aa") - assert.True(t, s.AddStaticLease(l) == nil) + for _, l := range stLeases { + require.Nil(t, s.AddStaticLease(l)) + } - // check ls := s.GetLeases(LeasesStatic) - assert.Equal(t, 2, len(ls)) + require.Len(t, ls, 2) - assert.Equal(t, "2001::1", ls[0].IP.String()) - assert.Equal(t, "33:aa:aa:aa:aa:aa", ls[0].HWAddr.String()) - assert.True(t, ls[0].Expiry.Unix() == leaseExpireStatic) - - assert.Equal(t, "2001::3", ls[1].IP.String()) - assert.Equal(t, "22:aa:aa:aa:aa:aa", ls[1].HWAddr.String()) - assert.True(t, ls[1].Expiry.Unix() == leaseExpireStatic) + for i, l := range ls { + assert.True(t, stLeases[i].IP.Equal(l.IP)) + assert.Equal(t, stLeases[i].HWAddr, l.HWAddr) + assert.EqualValues(t, leaseExpireStatic, l.Expiry.Unix()) + } } func TestV6GetLease(t *testing.T) { - conf := V6ServerConf{ + var err error + sIface, err := v6Create(V6ServerConf{ Enabled: true, - RangeStart: "2001::1", + RangeStart: net.ParseIP("2001::1"), notify: notify6, - } - sIface, err := v6Create(conf) - s := sIface.(*v6Server) - assert.True(t, err == nil) - s.conf.dnsIPAddrs = []net.IP{net.ParseIP("2000::1")} + }) + require.Nil(t, err) + s, ok := sIface.(*v6Server) + require.True(t, ok) + + dnsAddr := net.ParseIP("2000::1") + s.conf.dnsIPAddrs = []net.IP{dnsAddr} s.sid = dhcpv6.Duid{ - Type: dhcpv6.DUID_LLT, - HwType: iana.HWTypeEthernet, + Type: dhcpv6.DUID_LLT, + HwType: iana.HWTypeEthernet, + LinkLayerAddr: net.HardwareAddr{0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA}, } - s.sid.LinkLayerAddr, _ = net.ParseMAC("aa:aa:aa:aa:aa:aa") - l := Lease{} - l.IP = net.ParseIP("2001::1") - l.HWAddr, _ = net.ParseMAC("aa:aa:aa:aa:aa:aa") - assert.True(t, s.AddStaticLease(l) == nil) + l := Lease{ + IP: net.ParseIP("2001::1"), + HWAddr: net.HardwareAddr{0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA}, + } + require.Nil(t, s.AddStaticLease(l)) - // "Solicit" - mac, _ := net.ParseMAC("aa:aa:aa:aa:aa:aa") - req, _ := dhcpv6.NewSolicit(mac) - msg, _ := req.GetInnerMessage() - resp, _ := dhcpv6.NewAdvertiseFromSolicit(msg) - assert.True(t, s.process(msg, req, resp)) + var req, resp, msg *dhcpv6.Message + mac := net.HardwareAddr{0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA} + t.Run("solicit", func(t *testing.T) { + req, err = dhcpv6.NewSolicit(mac) + require.Nil(t, err) + + msg, err = req.GetInnerMessage() + require.Nil(t, err) + + resp, err = dhcpv6.NewAdvertiseFromSolicit(msg) + require.Nil(t, err) + + assert.True(t, s.process(msg, req, resp)) + }) + require.Nil(t, err) resp.AddOption(dhcpv6.OptServerID(s.sid)) - // check "Advertise" - assert.Equal(t, dhcpv6.MessageTypeAdvertise, resp.Type()) - oia := resp.Options.OneIANA() - oiaAddr := oia.Options.OneAddress() - assert.Equal(t, "2001::1", oiaAddr.IPv6Addr.String()) - assert.Equal(t, s.conf.leaseTime.Seconds(), oiaAddr.ValidLifetime.Seconds()) + var oia *dhcpv6.OptIANA + var oiaAddr *dhcpv6.OptIAAddress + t.Run("advertise", func(t *testing.T) { + require.Equal(t, dhcpv6.MessageTypeAdvertise, resp.Type()) + oia = resp.Options.OneIANA() + oiaAddr = oia.Options.OneAddress() - // "Request" - req, _ = dhcpv6.NewRequestFromAdvertise(resp) - msg, _ = req.GetInnerMessage() - resp, _ = dhcpv6.NewReplyFromMessage(msg) - assert.True(t, s.process(msg, req, resp)) + assert.Equal(t, l.IP, oiaAddr.IPv6Addr) + assert.Equal(t, s.conf.leaseTime.Seconds(), oiaAddr.ValidLifetime.Seconds()) + }) - // check "Reply" - assert.Equal(t, dhcpv6.MessageTypeReply, resp.Type()) - oia = resp.Options.OneIANA() - oiaAddr = oia.Options.OneAddress() - assert.Equal(t, "2001::1", oiaAddr.IPv6Addr.String()) - assert.Equal(t, s.conf.leaseTime.Seconds(), oiaAddr.ValidLifetime.Seconds()) + t.Run("request", func(t *testing.T) { + req, err = dhcpv6.NewRequestFromAdvertise(resp) + require.Nil(t, err) + + msg, err = req.GetInnerMessage() + require.Nil(t, err) + + resp, err = dhcpv6.NewReplyFromMessage(msg) + require.Nil(t, err) + + assert.True(t, s.process(msg, req, resp)) + }) + require.Nil(t, err) + + t.Run("reply", func(t *testing.T) { + require.Equal(t, dhcpv6.MessageTypeReply, resp.Type()) + oia = resp.Options.OneIANA() + oiaAddr = oia.Options.OneAddress() + + assert.Equal(t, l.IP, oiaAddr.IPv6Addr) + assert.Equal(t, s.conf.leaseTime.Seconds(), oiaAddr.ValidLifetime.Seconds()) + }) dnsAddrs := resp.Options.DNS() - assert.Equal(t, 1, len(dnsAddrs)) - assert.Equal(t, "2000::1", dnsAddrs[0].String()) + require.Len(t, dnsAddrs, 1) + assert.Equal(t, dnsAddr, dnsAddrs[0]) - // check lease - ls := s.GetLeases(LeasesStatic) - assert.Equal(t, 1, len(ls)) - assert.Equal(t, "2001::1", ls[0].IP.String()) - assert.Equal(t, "aa:aa:aa:aa:aa:aa", ls[0].HWAddr.String()) + t.Run("lease", func(t *testing.T) { + ls := s.GetLeases(LeasesStatic) + require.Len(t, ls, 1) + assert.Equal(t, l.IP, ls[0].IP) + assert.Equal(t, l.HWAddr, ls[0].HWAddr) + }) } func TestV6GetDynamicLease(t *testing.T) { - conf := V6ServerConf{ + sIface, err := v6Create(V6ServerConf{ Enabled: true, - RangeStart: "2001::2", + RangeStart: net.ParseIP("2001::2"), notify: notify6, - } - sIface, err := v6Create(conf) - s := sIface.(*v6Server) - assert.True(t, err == nil) - s.conf.dnsIPAddrs = []net.IP{net.ParseIP("2000::1")} - s.sid = dhcpv6.Duid{ - Type: dhcpv6.DUID_LLT, - HwType: iana.HWTypeEthernet, - } - s.sid.LinkLayerAddr, _ = net.ParseMAC("aa:aa:aa:aa:aa:aa") + }) + require.Nil(t, err) + s, ok := sIface.(*v6Server) + require.True(t, ok) - // "Solicit" - mac, _ := net.ParseMAC("aa:aa:aa:aa:aa:aa") - req, _ := dhcpv6.NewSolicit(mac) - msg, _ := req.GetInnerMessage() - resp, _ := dhcpv6.NewAdvertiseFromSolicit(msg) - assert.True(t, s.process(msg, req, resp)) + dnsAddr := net.ParseIP("2000::1") + s.conf.dnsIPAddrs = []net.IP{dnsAddr} + s.sid = dhcpv6.Duid{ + Type: dhcpv6.DUID_LLT, + HwType: iana.HWTypeEthernet, + LinkLayerAddr: net.HardwareAddr{0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA}, + } + + var req, resp, msg *dhcpv6.Message + mac := net.HardwareAddr{0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA} + t.Run("solicit", func(t *testing.T) { + req, err = dhcpv6.NewSolicit(mac) + require.Nil(t, err) + + msg, err = req.GetInnerMessage() + require.Nil(t, err) + + resp, err = dhcpv6.NewAdvertiseFromSolicit(msg) + require.Nil(t, err) + + assert.True(t, s.process(msg, req, resp)) + }) + require.Nil(t, err) resp.AddOption(dhcpv6.OptServerID(s.sid)) - // check "Advertise" - assert.Equal(t, dhcpv6.MessageTypeAdvertise, resp.Type()) - oia := resp.Options.OneIANA() - oiaAddr := oia.Options.OneAddress() - assert.Equal(t, "2001::2", oiaAddr.IPv6Addr.String()) + var oia *dhcpv6.OptIANA + var oiaAddr *dhcpv6.OptIAAddress + t.Run("advertise", func(t *testing.T) { + require.Equal(t, dhcpv6.MessageTypeAdvertise, resp.Type()) + oia = resp.Options.OneIANA() + oiaAddr = oia.Options.OneAddress() + assert.Equal(t, "2001::2", oiaAddr.IPv6Addr.String()) + }) - // "Request" - req, _ = dhcpv6.NewRequestFromAdvertise(resp) - msg, _ = req.GetInnerMessage() - resp, _ = dhcpv6.NewReplyFromMessage(msg) - assert.True(t, s.process(msg, req, resp)) + t.Run("request", func(t *testing.T) { + req, err = dhcpv6.NewRequestFromAdvertise(resp) + require.Nil(t, err) - // check "Reply" - assert.Equal(t, dhcpv6.MessageTypeReply, resp.Type()) - oia = resp.Options.OneIANA() - oiaAddr = oia.Options.OneAddress() - assert.Equal(t, "2001::2", oiaAddr.IPv6Addr.String()) + msg, err = req.GetInnerMessage() + require.Nil(t, err) + + resp, err = dhcpv6.NewReplyFromMessage(msg) + require.Nil(t, err) + + assert.True(t, s.process(msg, req, resp)) + }) + require.Nil(t, err) + + t.Run("reply", func(t *testing.T) { + require.Equal(t, dhcpv6.MessageTypeReply, resp.Type()) + oia = resp.Options.OneIANA() + oiaAddr = oia.Options.OneAddress() + assert.Equal(t, "2001::2", oiaAddr.IPv6Addr.String()) + }) dnsAddrs := resp.Options.DNS() - assert.Equal(t, 1, len(dnsAddrs)) - assert.Equal(t, "2000::1", dnsAddrs[0].String()) + require.Len(t, dnsAddrs, 1) + assert.Equal(t, dnsAddr, dnsAddrs[0]) - // check lease - ls := s.GetLeases(LeasesDynamic) - assert.Equal(t, 1, len(ls)) - assert.Equal(t, "2001::2", ls[0].IP.String()) - assert.Equal(t, "aa:aa:aa:aa:aa:aa", ls[0].HWAddr.String()) - - assert.True(t, !ip6InRange(net.ParseIP("2001::2"), net.ParseIP("2001::1"))) - assert.True(t, !ip6InRange(net.ParseIP("2001::2"), net.ParseIP("2002::2"))) - assert.True(t, ip6InRange(net.ParseIP("2001::2"), net.ParseIP("2001::2"))) - assert.True(t, ip6InRange(net.ParseIP("2001::2"), net.ParseIP("2001::3"))) + t.Run("lease", func(t *testing.T) { + ls := s.GetLeases(LeasesDynamic) + require.Len(t, ls, 1) + assert.Equal(t, "2001::2", ls[0].IP.String()) + assert.Equal(t, mac, ls[0].HWAddr) + }) +} + +func TestIP6InRange(t *testing.T) { + start := net.ParseIP("2001::2") + + testCases := []struct { + ip net.IP + want bool + }{{ + ip: net.ParseIP("2001::1"), + want: false, + }, { + ip: net.ParseIP("2002::2"), + want: false, + }, { + ip: start, + want: true, + }, { + ip: net.ParseIP("2001::3"), + want: true, + }} + + for _, tc := range testCases { + t.Run(tc.ip.String(), func(t *testing.T) { + assert.Equal(t, tc.want, ip6InRange(start, tc.ip)) + }) + } } diff --git a/internal/dnsfilter/blocked.go b/internal/dnsfilter/blocked.go index 48b02932..0905933b 100644 --- a/internal/dnsfilter/blocked.go +++ b/internal/dnsfilter/blocked.go @@ -161,7 +161,73 @@ var serviceRulesArray = []svc{ "||douyin.com^", "||tiktokv.com^", }}, - {"qq", []string{"||qq.com^", "||qqzaixian.com^"}}, + {"vimeo", []string{ + "||vimeo.com^", + "||vimeocdn.com^", + "*vod-adaptive.akamaized.net^", + }}, + {"pinterest", []string{ + "||pinterest.*^", + "||pinimg.com^", + }}, + {"imgur", []string{ + "||imgur.com^", + }}, + {"dailymotion", []string{ + "||dailymotion.com^", + "||dm-event.net^", + "||dmcdn.net^", + }}, + {"qq", []string{ + // block qq.com and subdomains excluding WeChat domains + "^(?!weixin|wx)([^.]+\\.)?qq\\.com$", + "||qqzaixian.com^", + }}, + {"wechat", []string{ + "||wechat.com^", + "||weixin.qq.com^", + "||wx.qq.com^", + }}, + {"viber", []string{ + "||viber.com^", + }}, + {"weibo", []string{ + "||weibo.com^", + }}, + {"9gag", []string{ + "||9cache.com^", + "||gag.com^", + }}, + {"telegram", []string{ + "||t.me^", + "||telegram.me^", + "||telegram.org^", + }}, + {"disneyplus", []string{ + "||disney-plus.net^", + "||disneyplus.com^", + }}, + {"hulu", []string{ + "||hulu.com^", + }}, + {"spotify", []string{ + "/_spotify-connect._tcp.local/", + "||spotify.com^", + "||scdn.co^", + "||spotify.com.edgesuite.net^", + "||spotify.map.fastly.net^", + "||spotify.map.fastlylb.net^", + "||spotifycdn.net^", + "||audio-ak-spotify-com.akamaized.net^", + "||audio4-ak-spotify-com.akamaized.net^", + "||heads-ak-spotify-com.akamaized.net^", + "||heads4-ak-spotify-com.akamaized.net^", + }}, + {"tinder", []string{ + "||gotinder.com^", + "||tinder.com^", + "||tindersparks.com^", + }}, } // convert array to map @@ -242,6 +308,6 @@ func (d *DNSFilter) handleBlockedServicesSet(w http.ResponseWriter, r *http.Requ // registerBlockedServicesHandlers - register HTTP handlers func (d *DNSFilter) registerBlockedServicesHandlers() { - d.Config.HTTPRegister("GET", "/control/blocked_services/list", d.handleBlockedServicesList) - d.Config.HTTPRegister("POST", "/control/blocked_services/set", d.handleBlockedServicesSet) + d.Config.HTTPRegister(http.MethodGet, "/control/blocked_services/list", d.handleBlockedServicesList) + d.Config.HTTPRegister(http.MethodPost, "/control/blocked_services/set", d.handleBlockedServicesSet) } diff --git a/internal/dnsfilter/blocked_test.go b/internal/dnsfilter/blocked_test.go new file mode 100644 index 00000000..5227da6b --- /dev/null +++ b/internal/dnsfilter/blocked_test.go @@ -0,0 +1,37 @@ +// +build ignore + +package dnsfilter + +import ( + "fmt" + "sort" + "testing" +) + +// This is a simple tool that takes a list of services and prints them to the output. +// It is supposed to be used to update: +// client/src/helpers/constants.js +// client/src/components/ui/Icons.js +// +// Usage: +// 1. go run ./internal/dnsfilter/blocked_test.go +// 2. Use the output to replace `SERVICES` array in "client/src/helpers/constants.js". +// 3. You'll need to enter services names manually. +// 4. Don't forget to add missing icons to "client/src/components/ui/Icons.js". +// +// TODO(ameshkov): Rework generator: have a JSON file with all the metadata we need +// then use this JSON file to generate JS and Go code +func TestGenServicesArray(t *testing.T) { + services := make([]svc, len(serviceRulesArray)) + copy(services, serviceRulesArray) + + sort.Slice(services, func(i, j int) bool { + return services[i].name < services[j].name + }) + + fmt.Println("export const SERVICES = [") + for _, s := range services { + fmt.Printf(" {\n id: '%s',\n name: '%s',\n },\n", s.name, s.name) + } + fmt.Println("];") +} diff --git a/internal/dnsfilter/dnsfilter.go b/internal/dnsfilter/dnsfilter.go index 4a1b255e..34ab408d 100644 --- a/internal/dnsfilter/dnsfilter.go +++ b/internal/dnsfilter/dnsfilter.go @@ -2,6 +2,7 @@ package dnsfilter import ( + "context" "fmt" "io/ioutil" "net" @@ -36,12 +37,18 @@ type RequestFilteringSettings struct { ParentalEnabled bool ClientName string - ClientIP string + ClientIP net.IP ClientTags []string ServicesRules []ServiceEntry } +// Resolver is the interface for net.Resolver to simplify testing. +type Resolver interface { + // TODO(e.burkov): Replace with LookupIP after upgrading go to v1.15. + LookupIPAddr(ctx context.Context, host string) (ips []net.IPAddr, err error) +} + // Config allows you to configure DNS filtering with New() or just change variables directly. type Config struct { ParentalEnabled bool `yaml:"parental_enabled"` @@ -68,6 +75,9 @@ type Config struct { // Register an HTTP handler HTTPRegister func(string, string, func(http.ResponseWriter, *http.Request)) `yaml:"-"` + + // CustomResolver is the resolver used by DNSFilter. + CustomResolver Resolver } // LookupStats store stats collected during safebrowsing or parental checks @@ -110,6 +120,11 @@ type DNSFilter struct { // Channel for passing data to filters-initializer goroutine filtersInitializerChan chan filtersInitializerParams filtersInitializerLock sync.Mutex + + // resolver only looks up the IP address of the host while safe search. + // + // TODO(e.burkov): Use upstream that configured in dnsforward instead. + resolver Resolver } // Filter represents a filter list @@ -148,17 +163,21 @@ const ( // FilteredBlockedService - the host is blocked by "blocked services" settings FilteredBlockedService - // ReasonRewrite is returned when there was a rewrite by - // a legacy DNS Rewrite rule. - ReasonRewrite + // Rewritten is returned when there was a rewrite by a legacy DNS + // rewrite rule. + Rewritten - // RewriteAutoHosts is returned when there was a rewrite by - // autohosts rules (/etc/hosts and so on). - RewriteAutoHosts + // RewrittenAutoHosts is returned when there was a rewrite by autohosts + // rules (/etc/hosts and so on). + RewrittenAutoHosts - // DNSRewriteRule is returned when a $dnsrewrite filter rule was - // applied. - DNSRewriteRule + // RewrittenRule is returned when a $dnsrewrite filter rule was applied. + // + // TODO(a.garipov): Remove Rewritten and RewrittenAutoHosts by merging + // their functionality into RewrittenRule. + // + // See https://github.com/AdguardTeam/AdGuardHome/issues/2499. + RewrittenRule ) // TODO(a.garipov): Resync with actual code names or replace completely @@ -175,11 +194,9 @@ var reasonNames = []string{ FilteredSafeSearch: "FilteredSafeSearch", FilteredBlockedService: "FilteredBlockedService", - ReasonRewrite: "Rewrite", - - RewriteAutoHosts: "RewriteEtcHosts", - - DNSRewriteRule: "DNSRewriteRule", + Rewritten: "Rewrite", + RewrittenAutoHosts: "RewriteEtcHosts", + RewrittenRule: "RewriteRule", } func (r Reason) String() string { @@ -331,15 +348,15 @@ type Result struct { Rules []*ResultRule `json:",omitempty"` // ReverseHosts is the reverse lookup rewrite result. It is - // empty unless Reason is set to RewriteAutoHosts. + // empty unless Reason is set to RewrittenAutoHosts. ReverseHosts []string `json:",omitempty"` // IPList is the lookup rewrite result. It is empty unless - // Reason is set to RewriteAutoHosts or ReasonRewrite. + // Reason is set to RewrittenAutoHosts or Rewritten. IPList []net.IP `json:",omitempty"` // CanonName is the CNAME value from the lookup rewrite result. - // It is empty unless Reason is set to ReasonRewrite. + // It is empty unless Reason is set to Rewritten or RewrittenRule. CanonName string `json:",omitempty"` // ServiceName is the name of the blocked service. It is empty @@ -379,7 +396,7 @@ func (d *DNSFilter) CheckHost(host string, qtype uint16, setts *RequestFiltering // first - check rewrites, they have the highest priority result = d.processRewrites(host, qtype) - if result.Reason == ReasonRewrite { + if result.Reason == Rewritten { return result, nil } @@ -453,7 +470,7 @@ func (d *DNSFilter) CheckHost(host string, qtype uint16, setts *RequestFiltering func (d *DNSFilter) checkAutoHosts(host string, qtype uint16, result *Result) (matched bool) { ips := d.Config.AutoHosts.Process(host, qtype) if ips != nil { - result.Reason = RewriteAutoHosts + result.Reason = RewrittenAutoHosts result.IPList = ips return true @@ -461,7 +478,7 @@ func (d *DNSFilter) checkAutoHosts(host string, qtype uint16, result *Result) (m revHosts := d.Config.AutoHosts.ProcessReverse(host, qtype) if len(revHosts) != 0 { - result.Reason = RewriteAutoHosts + result.Reason = RewrittenAutoHosts // TODO(a.garipov): Optimize this with a buffer. result.ReverseHosts = make([]string, len(revHosts)) @@ -488,7 +505,7 @@ func (d *DNSFilter) processRewrites(host string, qtype uint16) (res Result) { rr := findRewrites(d.Rewrites, host) if len(rr) != 0 { - res.Reason = ReasonRewrite + res.Reason = Rewritten } cnames := map[string]bool{} @@ -674,9 +691,10 @@ func (d *DNSFilter) matchHost(host string, qtype uint16, setts RequestFilteringS ureq := urlfilter.DNSRequest{ Hostname: host, SortedClientTags: setts.ClientTags, - ClientIP: setts.ClientIP, - ClientName: setts.ClientName, - DNSType: qtype, + // TODO(e.burkov): Wait for urlfilter update to pass net.IP. + ClientIP: setts.ClientIP.String(), + ClientName: setts.ClientName, + DNSType: qtype, } if d.filteringEngineAllow != nil { @@ -696,7 +714,7 @@ func (d *DNSFilter) matchHost(host string, qtype uint16, setts RequestFilteringS // awkward. if dnsr := dnsres.DNSRewrites(); len(dnsr) > 0 { res = d.processDNSRewrites(dnsr) - if res.Reason == DNSRewriteRule && res.CanonName == host { + if res.Reason == RewrittenRule && res.CanonName == host { // A rewrite of a host to itself. Go on and // try matching other things. } else { @@ -781,6 +799,7 @@ func InitModule() { // New creates properly initialized DNS Filter that is ready to be used. func New(c *Config, blockFilters []Filter) *DNSFilter { + var resolver Resolver = net.DefaultResolver if c != nil { cacheConf := cache.Config{ EnableLRU: true, @@ -800,9 +819,15 @@ func New(c *Config, blockFilters []Filter) *DNSFilter { cacheConf.MaxSize = c.ParentalCacheSize gctx.parentalCache = cache.New(cacheConf) } + + if c.CustomResolver != nil { + resolver = c.CustomResolver + } } - d := new(DNSFilter) + d := &DNSFilter{ + resolver: resolver, + } err := d.initSecurityServices() if err != nil { diff --git a/internal/dnsfilter/dnsfilter_test.go b/internal/dnsfilter/dnsfilter_test.go index 2bae12de..25985dae 100644 --- a/internal/dnsfilter/dnsfilter_test.go +++ b/internal/dnsfilter/dnsfilter_test.go @@ -2,12 +2,13 @@ package dnsfilter import ( "bytes" + "context" "fmt" "net" - "strings" "testing" - "github.com/AdguardTeam/AdGuardHome/internal/testutil" + "github.com/AdguardTeam/AdGuardHome/internal/aghtest" + "github.com/AdguardTeam/golibs/cache" "github.com/AdguardTeam/golibs/log" "github.com/AdguardTeam/urlfilter/rules" "github.com/miekg/dns" @@ -15,34 +16,29 @@ import ( ) func TestMain(m *testing.M) { - testutil.DiscardLogOutput(m) + aghtest.DiscardLogOutput(m) } var setts RequestFilteringSettings -// HELPERS -// SAFE BROWSING -// SAFE SEARCH -// PARENTAL -// FILTERING -// BENCHMARKS - -// HELPERS +// Helpers. func purgeCaches() { - if gctx.safebrowsingCache != nil { - gctx.safebrowsingCache.Clear() - } - if gctx.parentalCache != nil { - gctx.parentalCache.Clear() - } - if gctx.safeSearchCache != nil { - gctx.safeSearchCache.Clear() + for _, c := range []cache.Cache{ + gctx.safebrowsingCache, + gctx.parentalCache, + gctx.safeSearchCache, + } { + if c != nil { + c.Clear() + } } } -func NewForTest(c *Config, filters []Filter) *DNSFilter { - setts = RequestFilteringSettings{} +func newForTest(c *Config, filters []Filter) *DNSFilter { + setts = RequestFilteringSettings{ + FilteringEnabled: true, + } setts.FilteringEnabled = true if c != nil { c.SafeBrowsingCacheSize = 10000 @@ -60,48 +56,31 @@ func NewForTest(c *Config, filters []Filter) *DNSFilter { func (d *DNSFilter) checkMatch(t *testing.T, hostname string) { t.Helper() + res, err := d.CheckHost(hostname, dns.TypeA, &setts) - if err != nil { - t.Errorf("Error while matching host %s: %s", hostname, err) - } - if !res.IsFiltered { - t.Errorf("Expected hostname %s to match", hostname) - } + assert.Nilf(t, err, "Error while matching host %s: %s", hostname, err) + assert.Truef(t, res.IsFiltered, "Expected hostname %s to match", hostname) } func (d *DNSFilter) checkMatchIP(t *testing.T, hostname, ip string, qtype uint16) { t.Helper() res, err := d.CheckHost(hostname, qtype, &setts) - if err != nil { - t.Errorf("Error while matching host %s: %s", hostname, err) - } - - if !res.IsFiltered { - t.Errorf("Expected hostname %s to match", hostname) - } - - if len(res.Rules) == 0 { - t.Errorf("Expected result to have rules") - - return - } - - r := res.Rules[0] - if r.IP == nil || r.IP.String() != ip { - t.Errorf("Expected ip %s to match, actual: %v", ip, r.IP) + assert.Nilf(t, err, "Error while matching host %s: %s", hostname, err) + assert.Truef(t, res.IsFiltered, "Expected hostname %s to match", hostname) + if assert.NotEmpty(t, res.Rules, "Expected result to have rules") { + r := res.Rules[0] + assert.NotNilf(t, r.IP, "Expected ip %s to match, actual: %v", ip, r.IP) + assert.Equalf(t, ip, r.IP.String(), "Expected ip %s to match, actual: %v", ip, r.IP) } } func (d *DNSFilter) checkMatchEmpty(t *testing.T, hostname string) { t.Helper() + res, err := d.CheckHost(hostname, dns.TypeA, &setts) - if err != nil { - t.Errorf("Error while matching host %s: %s", hostname, err) - } - if res.IsFiltered { - t.Errorf("Expected hostname %s to not match", hostname) - } + assert.Nilf(t, err, "Error while matching host %s: %s", hostname, err) + assert.Falsef(t, res.IsFiltered, "Expected hostname %s to not match", hostname) } func TestEtcHostsMatching(t *testing.T) { @@ -118,90 +97,99 @@ func TestEtcHostsMatching(t *testing.T) { filters := []Filter{{ ID: 0, Data: []byte(text), }} - d := NewForTest(nil, filters) - defer d.Close() + d := newForTest(nil, filters) + t.Cleanup(d.Close) d.checkMatchIP(t, "google.com", addr, dns.TypeA) d.checkMatchIP(t, "www.google.com", addr, dns.TypeA) d.checkMatchEmpty(t, "subdomain.google.com") d.checkMatchEmpty(t, "example.org") - // IPv4 + // IPv4 match. d.checkMatchIP(t, "block.com", "0.0.0.0", dns.TypeA) - // ...but empty IPv6 + // Empty IPv6. res, err := d.CheckHost("block.com", dns.TypeAAAA, &setts) assert.Nil(t, err) assert.True(t, res.IsFiltered) if assert.Len(t, res.Rules, 1) { assert.Equal(t, "0.0.0.0 block.com", res.Rules[0].Text) - assert.Len(t, res.Rules[0].IP, 0) + assert.Empty(t, res.Rules[0].IP) } - // IPv6 + // IPv6 match. d.checkMatchIP(t, "ipv6.com", addr6, dns.TypeAAAA) - // ...but empty IPv4 + // Empty IPv4. res, err = d.CheckHost("ipv6.com", dns.TypeA, &setts) assert.Nil(t, err) assert.True(t, res.IsFiltered) if assert.Len(t, res.Rules, 1) { assert.Equal(t, "::1 ipv6.com", res.Rules[0].Text) - assert.Len(t, res.Rules[0].IP, 0) + assert.Empty(t, res.Rules[0].IP) } - // 2 IPv4 (return only the first one) + // Two IPv4, the first one returned. res, err = d.CheckHost("host2", dns.TypeA, &setts) assert.Nil(t, err) assert.True(t, res.IsFiltered) if assert.Len(t, res.Rules, 1) { - loopback4 := net.IP{0, 0, 0, 1} - assert.Equal(t, res.Rules[0].IP, loopback4) + assert.Equal(t, res.Rules[0].IP, net.IP{0, 0, 0, 1}) } - // ...and 1 IPv6 address + // One IPv6 address. res, err = d.CheckHost("host2", dns.TypeAAAA, &setts) assert.Nil(t, err) assert.True(t, res.IsFiltered) if assert.Len(t, res.Rules, 1) { - loopback6 := net.IP{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1} - assert.Equal(t, res.Rules[0].IP, loopback6) + assert.Equal(t, res.Rules[0].IP, net.IPv6loopback) } } -// SAFE BROWSING +// Safe Browsing. func TestSafeBrowsing(t *testing.T) { logOutput := &bytes.Buffer{} - testutil.ReplaceLogWriter(t, logOutput) - testutil.ReplaceLogLevel(t, log.DEBUG) + aghtest.ReplaceLogWriter(t, logOutput) + aghtest.ReplaceLogLevel(t, log.DEBUG) - d := NewForTest(&Config{SafeBrowsingEnabled: true}, nil) - defer d.Close() - d.checkMatch(t, "wmconvirus.narod.ru") + d := newForTest(&Config{SafeBrowsingEnabled: true}, nil) + t.Cleanup(d.Close) + matching := "wmconvirus.narod.ru" + d.SetSafeBrowsingUpstream(&aghtest.TestBlockUpstream{ + Hostname: matching, + Block: true, + }) + d.checkMatch(t, matching) - assert.True(t, strings.Contains(logOutput.String(), "SafeBrowsing lookup for wmconvirus.narod.ru")) + assert.Contains(t, logOutput.String(), "SafeBrowsing lookup for "+matching) - d.checkMatch(t, "test.wmconvirus.narod.ru") + d.checkMatch(t, "test."+matching) d.checkMatchEmpty(t, "yandex.ru") d.checkMatchEmpty(t, "pornhub.com") - // test cached result + // Cached result. d.safeBrowsingServer = "127.0.0.1" - d.checkMatch(t, "wmconvirus.narod.ru") + d.checkMatch(t, matching) d.checkMatchEmpty(t, "pornhub.com") d.safeBrowsingServer = defaultSafebrowsingServer } func TestParallelSB(t *testing.T) { - d := NewForTest(&Config{SafeBrowsingEnabled: true}, nil) - defer d.Close() + d := newForTest(&Config{SafeBrowsingEnabled: true}, nil) + t.Cleanup(d.Close) + matching := "wmconvirus.narod.ru" + d.SetSafeBrowsingUpstream(&aghtest.TestBlockUpstream{ + Hostname: matching, + Block: true, + }) + t.Run("group", func(t *testing.T) { for i := 0; i < 100; i++ { t.Run(fmt.Sprintf("aaa%d", i), func(t *testing.T) { t.Parallel() - d.checkMatch(t, "wmconvirus.narod.ru") - d.checkMatch(t, "test.wmconvirus.narod.ru") + d.checkMatch(t, matching) + d.checkMatch(t, "test."+matching) d.checkMatchEmpty(t, "yandex.ru") d.checkMatchEmpty(t, "pornhub.com") }) @@ -209,115 +197,124 @@ func TestParallelSB(t *testing.T) { }) } -// SAFE SEARCH +// Safe Search. func TestSafeSearch(t *testing.T) { - d := NewForTest(&Config{SafeSearchEnabled: true}, nil) - defer d.Close() + d := newForTest(&Config{SafeSearchEnabled: true}, nil) + t.Cleanup(d.Close) val, ok := d.SafeSearchDomain("www.google.com") - if !ok { - t.Errorf("Expected safesearch to find result for www.google.com") - } - if val != "forcesafesearch.google.com" { - t.Errorf("Expected safesearch for google.com to be forcesafesearch.google.com") - } + assert.True(t, ok, "Expected safesearch to find result for www.google.com") + assert.Equal(t, "forcesafesearch.google.com", val, "Expected safesearch for google.com to be forcesafesearch.google.com") } func TestCheckHostSafeSearchYandex(t *testing.T) { - d := NewForTest(&Config{SafeSearchEnabled: true}, nil) - defer d.Close() + d := newForTest(&Config{SafeSearchEnabled: true}, nil) + t.Cleanup(d.Close) - // Slice of yandex domains - yandex := []string{"yAndeX.ru", "YANdex.COM", "yandex.ua", "yandex.by", "yandex.kz", "www.yandex.com"} - - // Check host for each domain - for _, host := range yandex { + // Check host for each domain. + for _, host := range []string{ + "yAndeX.ru", + "YANdex.COM", + "yandex.ua", + "yandex.by", + "yandex.kz", + "www.yandex.com", + } { res, err := d.CheckHost(host, dns.TypeA, &setts) assert.Nil(t, err) assert.True(t, res.IsFiltered) if assert.Len(t, res.Rules, 1) { - assert.Equal(t, res.Rules[0].IP.String(), "213.180.193.56") + assert.Equal(t, res.Rules[0].IP, net.IPv4(213, 180, 193, 56)) } } } func TestCheckHostSafeSearchGoogle(t *testing.T) { - d := NewForTest(&Config{SafeSearchEnabled: true}, nil) - defer d.Close() + d := newForTest(&Config{ + SafeSearchEnabled: true, + CustomResolver: &aghtest.TestResolver{}, + }, nil) + t.Cleanup(d.Close) - // Slice of google domains - googleDomains := []string{"www.google.com", "www.google.im", "www.google.co.in", "www.google.iq", "www.google.is", "www.google.it", "www.google.je"} - - // Check host for each domain - for _, host := range googleDomains { - res, err := d.CheckHost(host, dns.TypeA, &setts) - assert.Nil(t, err) - assert.True(t, res.IsFiltered) - if assert.Len(t, res.Rules, 1) { - assert.NotEqual(t, res.Rules[0].IP.String(), "0.0.0.0") - } + // Check host for each domain. + for _, host := range []string{ + "www.google.com", + "www.google.im", + "www.google.co.in", + "www.google.iq", + "www.google.is", + "www.google.it", + "www.google.je", + } { + t.Run(host, func(t *testing.T) { + res, err := d.CheckHost(host, dns.TypeA, &setts) + assert.Nil(t, err) + assert.True(t, res.IsFiltered) + assert.Len(t, res.Rules, 1) + }) } } func TestSafeSearchCacheYandex(t *testing.T) { - d := NewForTest(nil, nil) - defer d.Close() + d := newForTest(nil, nil) + t.Cleanup(d.Close) domain := "yandex.ru" // Check host with disabled safesearch. res, err := d.CheckHost(domain, dns.TypeA, &setts) assert.Nil(t, err) assert.False(t, res.IsFiltered) - assert.Len(t, res.Rules, 0) + assert.Empty(t, res.Rules) - d = NewForTest(&Config{SafeSearchEnabled: true}, nil) - defer d.Close() + d = newForTest(&Config{SafeSearchEnabled: true}, nil) + t.Cleanup(d.Close) res, err = d.CheckHost(domain, dns.TypeA, &setts) - if err != nil { - t.Fatalf("CheckHost for safesearh domain %s failed cause %s", domain, err) - } + assert.Nilf(t, err, "CheckHost for safesearh domain %s failed cause %s", domain, err) - // For yandex we already know valid ip. + // For yandex we already know valid IP. if assert.Len(t, res.Rules, 1) { - assert.Equal(t, res.Rules[0].IP.String(), "213.180.193.56") + assert.Equal(t, res.Rules[0].IP, net.IPv4(213, 180, 193, 56)) } // Check cache. cachedValue, isFound := getCachedResult(gctx.safeSearchCache, domain) assert.True(t, isFound) if assert.Len(t, cachedValue.Rules, 1) { - assert.Equal(t, cachedValue.Rules[0].IP.String(), "213.180.193.56") + assert.Equal(t, cachedValue.Rules[0].IP, net.IPv4(213, 180, 193, 56)) } } func TestSafeSearchCacheGoogle(t *testing.T) { - d := NewForTest(nil, nil) - defer d.Close() + resolver := &aghtest.TestResolver{} + d := newForTest(&Config{ + CustomResolver: resolver, + }, nil) + t.Cleanup(d.Close) + domain := "www.google.ru" res, err := d.CheckHost(domain, dns.TypeA, &setts) assert.Nil(t, err) assert.False(t, res.IsFiltered) - assert.Len(t, res.Rules, 0) + assert.Empty(t, res.Rules) - d = NewForTest(&Config{SafeSearchEnabled: true}, nil) - defer d.Close() + d = newForTest(&Config{SafeSearchEnabled: true}, nil) + t.Cleanup(d.Close) + d.resolver = resolver - // Let's lookup for safesearch domain + // Lookup for safesearch domain. safeDomain, ok := d.SafeSearchDomain(domain) - if !ok { - t.Fatalf("Failed to get safesearch domain for %s", domain) - } + assert.Truef(t, ok, "Failed to get safesearch domain for %s", domain) - ips, err := net.LookupIP(safeDomain) + ipAddrs, err := resolver.LookupIPAddr(context.Background(), safeDomain) if err != nil { t.Fatalf("Failed to lookup for %s", safeDomain) } - ip := ips[0] - for _, i := range ips { - if i.To4() != nil { - ip = i + ip := ipAddrs[0].IP + for _, ipAddr := range ipAddrs { + if ipAddr.IP.To4() != nil { + ip = ipAddr.IP break } } @@ -336,114 +333,324 @@ func TestSafeSearchCacheGoogle(t *testing.T) { } } -// PARENTAL +// Parental. func TestParentalControl(t *testing.T) { logOutput := &bytes.Buffer{} - testutil.ReplaceLogWriter(t, logOutput) - testutil.ReplaceLogLevel(t, log.DEBUG) + aghtest.ReplaceLogWriter(t, logOutput) + aghtest.ReplaceLogLevel(t, log.DEBUG) - d := NewForTest(&Config{ParentalEnabled: true}, nil) - defer d.Close() - d.checkMatch(t, "pornhub.com") - assert.True(t, strings.Contains(logOutput.String(), "Parental lookup for pornhub.com")) - d.checkMatch(t, "www.pornhub.com") + d := newForTest(&Config{ParentalEnabled: true}, nil) + t.Cleanup(d.Close) + matching := "pornhub.com" + d.SetParentalUpstream(&aghtest.TestBlockUpstream{ + Hostname: matching, + Block: true, + }) + + d.checkMatch(t, matching) + assert.Contains(t, logOutput.String(), "Parental lookup for "+matching) + d.checkMatch(t, "www."+matching) d.checkMatchEmpty(t, "www.yandex.ru") d.checkMatchEmpty(t, "yandex.ru") d.checkMatchEmpty(t, "api.jquery.com") // test cached result d.parentalServer = "127.0.0.1" - d.checkMatch(t, "pornhub.com") + d.checkMatch(t, matching) d.checkMatchEmpty(t, "yandex.ru") d.parentalServer = defaultParentalServer } -// FILTERING - -const nl = "\n" - -const ( - blockingRules = `||example.org^` + nl - allowlistRules = `||example.org^` + nl + `@@||test.example.org` + nl - importantRules = `@@||example.org^` + nl + `||test.example.org^$important` + nl - regexRules = `/example\.org/` + nl + `@@||test.example.org^` + nl - maskRules = `test*.example.org^` + nl + `exam*.com` + nl - dnstypeRules = `||example.org^$dnstype=AAAA` + nl + `@@||test.example.org^` + nl -) - -var tests = []struct { - testname string - rules string - hostname string - isFiltered bool - reason Reason - dnsType uint16 -}{ - {"sanity", "||doubleclick.net^", "www.doubleclick.net", true, FilteredBlockList, dns.TypeA}, - {"sanity", "||doubleclick.net^", "nodoubleclick.net", false, NotFilteredNotFound, dns.TypeA}, - {"sanity", "||doubleclick.net^", "doubleclick.net.ru", false, NotFilteredNotFound, dns.TypeA}, - {"sanity", "||doubleclick.net^", "wmconvirus.narod.ru", false, NotFilteredNotFound, dns.TypeA}, - - {"blocking", blockingRules, "example.org", true, FilteredBlockList, dns.TypeA}, - {"blocking", blockingRules, "test.example.org", true, FilteredBlockList, dns.TypeA}, - {"blocking", blockingRules, "test.test.example.org", true, FilteredBlockList, dns.TypeA}, - {"blocking", blockingRules, "testexample.org", false, NotFilteredNotFound, dns.TypeA}, - {"blocking", blockingRules, "onemoreexample.org", false, NotFilteredNotFound, dns.TypeA}, - - {"allowlist", allowlistRules, "example.org", true, FilteredBlockList, dns.TypeA}, - {"allowlist", allowlistRules, "test.example.org", false, NotFilteredAllowList, dns.TypeA}, - {"allowlist", allowlistRules, "test.test.example.org", false, NotFilteredAllowList, dns.TypeA}, - {"allowlist", allowlistRules, "testexample.org", false, NotFilteredNotFound, dns.TypeA}, - {"allowlist", allowlistRules, "onemoreexample.org", false, NotFilteredNotFound, dns.TypeA}, - - {"important", importantRules, "example.org", false, NotFilteredAllowList, dns.TypeA}, - {"important", importantRules, "test.example.org", true, FilteredBlockList, dns.TypeA}, - {"important", importantRules, "test.test.example.org", true, FilteredBlockList, dns.TypeA}, - {"important", importantRules, "testexample.org", false, NotFilteredNotFound, dns.TypeA}, - {"important", importantRules, "onemoreexample.org", false, NotFilteredNotFound, dns.TypeA}, - - {"regex", regexRules, "example.org", true, FilteredBlockList, dns.TypeA}, - {"regex", regexRules, "test.example.org", false, NotFilteredAllowList, dns.TypeA}, - {"regex", regexRules, "test.test.example.org", false, NotFilteredAllowList, dns.TypeA}, - {"regex", regexRules, "testexample.org", true, FilteredBlockList, dns.TypeA}, - {"regex", regexRules, "onemoreexample.org", true, FilteredBlockList, dns.TypeA}, - - {"mask", maskRules, "test.example.org", true, FilteredBlockList, dns.TypeA}, - {"mask", maskRules, "test2.example.org", true, FilteredBlockList, dns.TypeA}, - {"mask", maskRules, "example.com", true, FilteredBlockList, dns.TypeA}, - {"mask", maskRules, "exampleeee.com", true, FilteredBlockList, dns.TypeA}, - {"mask", maskRules, "onemoreexamsite.com", true, FilteredBlockList, dns.TypeA}, - {"mask", maskRules, "example.org", false, NotFilteredNotFound, dns.TypeA}, - {"mask", maskRules, "testexample.org", false, NotFilteredNotFound, dns.TypeA}, - {"mask", maskRules, "example.co.uk", false, NotFilteredNotFound, dns.TypeA}, - - {"dnstype", dnstypeRules, "onemoreexample.org", false, NotFilteredNotFound, dns.TypeA}, - {"dnstype", dnstypeRules, "example.org", false, NotFilteredNotFound, dns.TypeA}, - {"dnstype", dnstypeRules, "example.org", true, FilteredBlockList, dns.TypeAAAA}, - {"dnstype", dnstypeRules, "test.example.org", false, NotFilteredAllowList, dns.TypeA}, - {"dnstype", dnstypeRules, "test.example.org", false, NotFilteredAllowList, dns.TypeAAAA}, -} +// Filtering. func TestMatching(t *testing.T) { - for _, test := range tests { - t.Run(fmt.Sprintf("%s-%s", test.testname, test.hostname), func(t *testing.T) { - filters := []Filter{{ - ID: 0, Data: []byte(test.rules), - }} - d := NewForTest(nil, filters) - defer d.Close() + const nl = "\n" + const ( + blockingRules = `||example.org^` + nl + allowlistRules = `||example.org^` + nl + `@@||test.example.org` + nl + importantRules = `@@||example.org^` + nl + `||test.example.org^$important` + nl + regexRules = `/example\.org/` + nl + `@@||test.example.org^` + nl + maskRules = `test*.example.org^` + nl + `exam*.com` + nl + dnstypeRules = `||example.org^$dnstype=AAAA` + nl + `@@||test.example.org^` + nl + ) + testCases := []struct { + name string + rules string + host string + wantIsFiltered bool + wantReason Reason + wantDNSType uint16 + }{{ + name: "sanity", + rules: "||doubleclick.net^", + host: "www.doubleclick.net", + wantIsFiltered: true, + wantReason: FilteredBlockList, + wantDNSType: dns.TypeA, + }, { + name: "sanity", + rules: "||doubleclick.net^", + host: "nodoubleclick.net", + wantIsFiltered: false, + wantReason: NotFilteredNotFound, + wantDNSType: dns.TypeA, + }, { + name: "sanity", + rules: "||doubleclick.net^", + host: "doubleclick.net.ru", + wantIsFiltered: false, + wantReason: NotFilteredNotFound, + wantDNSType: dns.TypeA, + }, { + name: "sanity", + rules: "||doubleclick.net^", + host: "wmconvirus.narod.ru", + wantIsFiltered: false, + wantReason: NotFilteredNotFound, + wantDNSType: dns.TypeA, + }, { + name: "blocking", + rules: blockingRules, + host: "example.org", + wantIsFiltered: true, + wantReason: FilteredBlockList, + wantDNSType: dns.TypeA, + }, { + name: "blocking", + rules: blockingRules, + host: "test.example.org", + wantIsFiltered: true, + wantReason: FilteredBlockList, + wantDNSType: dns.TypeA, + }, { + name: "blocking", + rules: blockingRules, + host: "test.test.example.org", + wantIsFiltered: true, + wantReason: FilteredBlockList, + wantDNSType: dns.TypeA, + }, { + name: "blocking", + rules: blockingRules, + host: "testexample.org", + wantIsFiltered: false, + wantReason: NotFilteredNotFound, + wantDNSType: dns.TypeA, + }, { + name: "blocking", + rules: blockingRules, + host: "onemoreexample.org", + wantIsFiltered: false, + wantReason: NotFilteredNotFound, + wantDNSType: dns.TypeA, + }, { + name: "allowlist", + rules: allowlistRules, + host: "example.org", + wantIsFiltered: true, + wantReason: FilteredBlockList, + wantDNSType: dns.TypeA, + }, { + name: "allowlist", + rules: allowlistRules, + host: "test.example.org", + wantIsFiltered: false, + wantReason: NotFilteredAllowList, + wantDNSType: dns.TypeA, + }, { + name: "allowlist", + rules: allowlistRules, + host: "test.test.example.org", + wantIsFiltered: false, + wantReason: NotFilteredAllowList, + wantDNSType: dns.TypeA, + }, { + name: "allowlist", + rules: allowlistRules, + host: "testexample.org", + wantIsFiltered: false, + wantReason: NotFilteredNotFound, + wantDNSType: dns.TypeA, + }, { + name: "allowlist", + rules: allowlistRules, + host: "onemoreexample.org", + wantIsFiltered: false, + wantReason: NotFilteredNotFound, + wantDNSType: dns.TypeA, + }, { + name: "important", + rules: importantRules, + host: "example.org", + wantIsFiltered: false, + wantReason: NotFilteredAllowList, + wantDNSType: dns.TypeA, + }, { + name: "important", + rules: importantRules, + host: "test.example.org", + wantIsFiltered: true, + wantReason: FilteredBlockList, + wantDNSType: dns.TypeA, + }, { + name: "important", + rules: importantRules, + host: "test.test.example.org", + wantIsFiltered: true, + wantReason: FilteredBlockList, + wantDNSType: dns.TypeA, + }, { + name: "important", + rules: importantRules, + host: "testexample.org", + wantIsFiltered: false, + wantReason: NotFilteredNotFound, + wantDNSType: dns.TypeA, + }, { + name: "important", + rules: importantRules, + host: "onemoreexample.org", + wantIsFiltered: false, + wantReason: NotFilteredNotFound, + wantDNSType: dns.TypeA, + }, { + name: "regex", + rules: regexRules, + host: "example.org", + wantIsFiltered: true, + wantReason: FilteredBlockList, + wantDNSType: dns.TypeA, + }, { + name: "regex", + rules: regexRules, + host: "test.example.org", + wantIsFiltered: false, + wantReason: NotFilteredAllowList, + wantDNSType: dns.TypeA, + }, { + name: "regex", + rules: regexRules, + host: "test.test.example.org", + wantIsFiltered: false, + wantReason: NotFilteredAllowList, + wantDNSType: dns.TypeA, + }, { + name: "regex", + rules: regexRules, + host: "testexample.org", + wantIsFiltered: true, + wantReason: FilteredBlockList, + wantDNSType: dns.TypeA, + }, { + name: "regex", + rules: regexRules, + host: "onemoreexample.org", + wantIsFiltered: true, + wantReason: FilteredBlockList, + wantDNSType: dns.TypeA, + }, { + name: "mask", + rules: maskRules, + host: "test.example.org", + wantIsFiltered: true, + wantReason: FilteredBlockList, + wantDNSType: dns.TypeA, + }, { + name: "mask", + rules: maskRules, + host: "test2.example.org", + wantIsFiltered: true, + wantReason: FilteredBlockList, + wantDNSType: dns.TypeA, + }, { + name: "mask", + rules: maskRules, + host: "example.com", + wantIsFiltered: true, + wantReason: FilteredBlockList, + wantDNSType: dns.TypeA, + }, { + name: "mask", + rules: maskRules, + host: "exampleeee.com", + wantIsFiltered: true, + wantReason: FilteredBlockList, + wantDNSType: dns.TypeA, + }, { + name: "mask", + rules: maskRules, + host: "onemoreexamsite.com", + wantIsFiltered: true, + wantReason: FilteredBlockList, + wantDNSType: dns.TypeA, + }, { + name: "mask", + rules: maskRules, + host: "example.org", + wantIsFiltered: false, + wantReason: NotFilteredNotFound, + wantDNSType: dns.TypeA, + }, { + name: "mask", + rules: maskRules, + host: "testexample.org", + wantIsFiltered: false, + wantReason: NotFilteredNotFound, + wantDNSType: dns.TypeA, + }, { + name: "mask", + rules: maskRules, + host: "example.co.uk", + wantIsFiltered: false, + wantReason: NotFilteredNotFound, + wantDNSType: dns.TypeA, + }, { + name: "dnstype", + rules: dnstypeRules, + host: "onemoreexample.org", + wantIsFiltered: false, + wantReason: NotFilteredNotFound, + wantDNSType: dns.TypeA, + }, { + name: "dnstype", + rules: dnstypeRules, + host: "example.org", + wantIsFiltered: false, + wantReason: NotFilteredNotFound, + wantDNSType: dns.TypeA, + }, { + name: "dnstype", + rules: dnstypeRules, + host: "example.org", + wantIsFiltered: true, + wantReason: FilteredBlockList, + wantDNSType: dns.TypeAAAA, + }, { + name: "dnstype", + rules: dnstypeRules, + host: "test.example.org", + wantIsFiltered: false, + wantReason: NotFilteredAllowList, + wantDNSType: dns.TypeA, + }, { + name: "dnstype", + rules: dnstypeRules, + host: "test.example.org", + wantIsFiltered: false, + wantReason: NotFilteredAllowList, + wantDNSType: dns.TypeAAAA, + }} + for _, tc := range testCases { + t.Run(fmt.Sprintf("%s-%s", tc.name, tc.host), func(t *testing.T) { + filters := []Filter{{ID: 0, Data: []byte(tc.rules)}} + d := newForTest(nil, filters) + t.Cleanup(d.Close) - res, err := d.CheckHost(test.hostname, test.dnsType, &setts) - if err != nil { - t.Errorf("Error while matching host %s: %s", test.hostname, err) - } - if res.IsFiltered != test.isFiltered { - t.Errorf("Hostname %s has wrong result (%v must be %v)", test.hostname, res.IsFiltered, test.isFiltered) - } - if res.Reason != test.reason { - t.Errorf("Hostname %s has wrong reason (%v must be %v)", test.hostname, res.Reason.String(), test.reason.String()) - } + res, err := d.CheckHost(tc.host, tc.wantDNSType, &setts) + assert.Nilf(t, err, "Error while matching host %s: %s", tc.host, err) + assert.Equalf(t, tc.wantIsFiltered, res.IsFiltered, "Hostname %s has wrong result (%v must be %v)", tc.host, res.IsFiltered, tc.wantIsFiltered) + assert.Equalf(t, tc.wantReason, res.Reason, "Hostname %s has wrong reason (%v must be %v)", tc.host, res.Reason, tc.wantReason) }) } } @@ -462,28 +669,33 @@ func TestWhitelist(t *testing.T) { whiteFilters := []Filter{{ ID: 0, Data: []byte(whiteRules), }} - d := NewForTest(nil, filters) - d.SetFilters(filters, whiteFilters, false) - defer d.Close() + d := newForTest(nil, filters) - // matched by white filter + err := d.SetFilters(filters, whiteFilters, false) + assert.Nil(t, err) + + t.Cleanup(d.Close) + + // Matched by white filter. res, err := d.CheckHost("host1", dns.TypeA, &setts) - assert.True(t, err == nil) - assert.True(t, !res.IsFiltered && res.Reason == NotFilteredAllowList) + assert.Nil(t, err) + assert.False(t, res.IsFiltered) + assert.Equal(t, res.Reason, NotFilteredAllowList) if assert.Len(t, res.Rules, 1) { - assert.True(t, res.Rules[0].Text == "||host1^") + assert.Equal(t, "||host1^", res.Rules[0].Text) } - // not matched by white filter, but matched by block filter + // Not matched by white filter, but matched by block filter. res, err = d.CheckHost("host2", dns.TypeA, &setts) - assert.True(t, err == nil) - assert.True(t, res.IsFiltered && res.Reason == FilteredBlockList) + assert.Nil(t, err) + assert.True(t, res.IsFiltered) + assert.Equal(t, res.Reason, FilteredBlockList) if assert.Len(t, res.Rules, 1) { - assert.True(t, res.Rules[0].Text == "||host2^") + assert.Equal(t, "||host2^", res.Rules[0].Text) } } -// CLIENT SETTINGS +// Client Settings. func applyClientSettings(setts *RequestFilteringSettings) { setts.FilteringEnabled = false @@ -497,126 +709,131 @@ func applyClientSettings(setts *RequestFilteringSettings) { setts.ServicesRules = append(setts.ServicesRules, s) } -// Check behaviour without any per-client settings, -// then apply per-client settings and check behaviour once again func TestClientSettings(t *testing.T) { - var r Result - filters := []Filter{{ - ID: 0, Data: []byte("||example.org^\n"), + d := newForTest( + &Config{ + ParentalEnabled: true, + SafeBrowsingEnabled: false, + }, + []Filter{{ + ID: 0, Data: []byte("||example.org^\n"), + }}, + ) + t.Cleanup(d.Close) + d.SetParentalUpstream(&aghtest.TestBlockUpstream{ + Hostname: "pornhub.com", + Block: true, + }) + d.SetSafeBrowsingUpstream(&aghtest.TestBlockUpstream{ + Hostname: "wmconvirus.narod.ru", + Block: true, + }) + + type testCase struct { + name string + host string + before bool + wantReason Reason + } + testCases := []testCase{{ + name: "filters", + host: "example.org", + before: true, + wantReason: FilteredBlockList, + }, { + name: "parental", + host: "pornhub.com", + before: true, + wantReason: FilteredParental, + }, { + name: "safebrowsing", + host: "wmconvirus.narod.ru", + before: false, + wantReason: FilteredSafeBrowsing, + }, { + name: "additional_rules", + host: "facebook.com", + before: false, + wantReason: FilteredBlockedService, }} - d := NewForTest(&Config{ParentalEnabled: true, SafeBrowsingEnabled: false}, filters) - defer d.Close() - // no client settings: - - // blocked by filters - r, _ = d.CheckHost("example.org", dns.TypeA, &setts) - if !r.IsFiltered || r.Reason != FilteredBlockList { - t.Fatalf("CheckHost FilteredBlockList") + makeTester := func(tc testCase, before bool) func(t *testing.T) { + return func(t *testing.T) { + r, _ := d.CheckHost(tc.host, dns.TypeA, &setts) + if before { + assert.True(t, r.IsFiltered) + assert.Equal(t, tc.wantReason, r.Reason) + } else { + assert.False(t, r.IsFiltered) + } + } } - // blocked by parental - r, _ = d.CheckHost("pornhub.com", dns.TypeA, &setts) - if !r.IsFiltered || r.Reason != FilteredParental { - t.Fatalf("CheckHost FilteredParental") + // Check behaviour without any per-client settings, then apply per-client + // settings and check behaviour once again. + for _, tc := range testCases { + t.Run(tc.name, makeTester(tc, tc.before)) } - // safesearch is disabled - r, _ = d.CheckHost("wmconvirus.narod.ru", dns.TypeA, &setts) - if r.IsFiltered { - t.Fatalf("CheckHost safesearch") - } - - // not blocked - r, _ = d.CheckHost("facebook.com", dns.TypeA, &setts) - assert.True(t, !r.IsFiltered) - - // override client settings: applyClientSettings(&setts) - // override filtering settings - r, _ = d.CheckHost("example.org", dns.TypeA, &setts) - if r.IsFiltered { - t.Fatalf("CheckHost") + for _, tc := range testCases { + t.Run(tc.name, makeTester(tc, !tc.before)) } - - // override parental settings (force disable parental) - r, _ = d.CheckHost("pornhub.com", dns.TypeA, &setts) - if r.IsFiltered { - t.Fatalf("CheckHost") - } - - // override safesearch settings (force enable safesearch) - r, _ = d.CheckHost("wmconvirus.narod.ru", dns.TypeA, &setts) - if !r.IsFiltered || r.Reason != FilteredSafeBrowsing { - t.Fatalf("CheckHost FilteredSafeBrowsing") - } - - // blocked by additional rules - r, _ = d.CheckHost("facebook.com", dns.TypeA, &setts) - assert.True(t, r.IsFiltered && r.Reason == FilteredBlockedService) } -// BENCHMARKS +// Benchmarks. func BenchmarkSafeBrowsing(b *testing.B) { - d := NewForTest(&Config{SafeBrowsingEnabled: true}, nil) - defer d.Close() + d := newForTest(&Config{SafeBrowsingEnabled: true}, nil) + b.Cleanup(d.Close) + blocked := "wmconvirus.narod.ru" + d.SetSafeBrowsingUpstream(&aghtest.TestBlockUpstream{ + Hostname: blocked, + Block: true, + }) for n := 0; n < b.N; n++ { - hostname := "wmconvirus.narod.ru" - res, err := d.CheckHost(hostname, dns.TypeA, &setts) - if err != nil { - b.Errorf("Error while matching host %s: %s", hostname, err) - } - if !res.IsFiltered { - b.Errorf("Expected hostname %s to match", hostname) - } + res, err := d.CheckHost(blocked, dns.TypeA, &setts) + assert.Nilf(b, err, "Error while matching host %s: %s", blocked, err) + assert.True(b, res.IsFiltered, "Expected hostname %s to match", blocked) } } func BenchmarkSafeBrowsingParallel(b *testing.B) { - d := NewForTest(&Config{SafeBrowsingEnabled: true}, nil) - defer d.Close() + d := newForTest(&Config{SafeBrowsingEnabled: true}, nil) + b.Cleanup(d.Close) + blocked := "wmconvirus.narod.ru" + d.SetSafeBrowsingUpstream(&aghtest.TestBlockUpstream{ + Hostname: blocked, + Block: true, + }) b.RunParallel(func(pb *testing.PB) { for pb.Next() { - hostname := "wmconvirus.narod.ru" - res, err := d.CheckHost(hostname, dns.TypeA, &setts) - if err != nil { - b.Errorf("Error while matching host %s: %s", hostname, err) - } - if !res.IsFiltered { - b.Errorf("Expected hostname %s to match", hostname) - } + res, err := d.CheckHost(blocked, dns.TypeA, &setts) + assert.Nilf(b, err, "Error while matching host %s: %s", blocked, err) + assert.True(b, res.IsFiltered, "Expected hostname %s to match", blocked) } }) } func BenchmarkSafeSearch(b *testing.B) { - d := NewForTest(&Config{SafeSearchEnabled: true}, nil) - defer d.Close() + d := newForTest(&Config{SafeSearchEnabled: true}, nil) + b.Cleanup(d.Close) for n := 0; n < b.N; n++ { val, ok := d.SafeSearchDomain("www.google.com") - if !ok { - b.Errorf("Expected safesearch to find result for www.google.com") - } - if val != "forcesafesearch.google.com" { - b.Errorf("Expected safesearch for google.com to be forcesafesearch.google.com") - } + assert.True(b, ok, "Expected safesearch to find result for www.google.com") + assert.Equal(b, "forcesafesearch.google.com", val, "Expected safesearch for google.com to be forcesafesearch.google.com") } } func BenchmarkSafeSearchParallel(b *testing.B) { - d := NewForTest(&Config{SafeSearchEnabled: true}, nil) - defer d.Close() + d := newForTest(&Config{SafeSearchEnabled: true}, nil) + b.Cleanup(d.Close) b.RunParallel(func(pb *testing.PB) { for pb.Next() { val, ok := d.SafeSearchDomain("www.google.com") - if !ok { - b.Errorf("Expected safesearch to find result for www.google.com") - } - if val != "forcesafesearch.google.com" { - b.Errorf("Expected safesearch for google.com to be forcesafesearch.google.com") - } + assert.True(b, ok, "Expected safesearch to find result for www.google.com") + assert.Equal(b, "forcesafesearch.google.com", val, "Expected safesearch for google.com to be forcesafesearch.google.com") } }) } diff --git a/internal/dnsfilter/dnsrewrite.go b/internal/dnsfilter/dnsrewrite.go index 1239fbad..757f742c 100644 --- a/internal/dnsfilter/dnsrewrite.go +++ b/internal/dnsfilter/dnsrewrite.go @@ -7,8 +7,8 @@ import ( // DNSRewriteResult is the result of application of $dnsrewrite rules. type DNSRewriteResult struct { - RCode rules.RCode `json:",omitempty"` Response DNSRewriteResultResponse `json:",omitempty"` + RCode rules.RCode `json:",omitempty"` } // DNSRewriteResultResponse is the collection of DNS response records @@ -33,13 +33,13 @@ func (d *DNSFilter) processDNSRewrites(dnsr []*rules.NetworkRule) (res Result) { if dr.NewCNAME != "" { // NewCNAME rules have a higher priority than // the other rules. - rules := []*ResultRule{{ + rules = []*ResultRule{{ FilterListID: int64(nr.GetFilterListID()), Text: nr.RuleText, }} return Result{ - Reason: DNSRewriteRule, + Reason: RewrittenRule, Rules: rules, CanonName: dr.NewCNAME, } @@ -56,7 +56,7 @@ func (d *DNSFilter) processDNSRewrites(dnsr []*rules.NetworkRule) (res Result) { default: // RcodeRefused and other such codes have higher // priority. Return immediately. - rules := []*ResultRule{{ + rules = []*ResultRule{{ FilterListID: int64(nr.GetFilterListID()), Text: nr.RuleText, }} @@ -65,7 +65,7 @@ func (d *DNSFilter) processDNSRewrites(dnsr []*rules.NetworkRule) (res Result) { } return Result{ - Reason: DNSRewriteRule, + Reason: RewrittenRule, Rules: rules, DNSRewriteResult: dnsrr, } @@ -73,7 +73,7 @@ func (d *DNSFilter) processDNSRewrites(dnsr []*rules.NetworkRule) (res Result) { } return Result{ - Reason: DNSRewriteRule, + Reason: RewrittenRule, Rules: rules, DNSRewriteResult: dnsrr, } diff --git a/internal/dnsfilter/dnsrewrite_test.go b/internal/dnsfilter/dnsrewrite_test.go index 4918ccc0..c915d920 100644 --- a/internal/dnsfilter/dnsrewrite_test.go +++ b/internal/dnsfilter/dnsrewrite_test.go @@ -11,40 +11,41 @@ import ( func TestDNSFilter_CheckHostRules_dnsrewrite(t *testing.T) { const text = ` -|cname^$dnsrewrite=new_cname +|cname^$dnsrewrite=new-cname -|a_record^$dnsrewrite=127.0.0.1 +|a-record^$dnsrewrite=127.0.0.1 -|aaaa_record^$dnsrewrite=::1 +|aaaa-record^$dnsrewrite=::1 -|txt_record^$dnsrewrite=NOERROR;TXT;hello_world +|txt-record^$dnsrewrite=NOERROR;TXT;hello-world |refused^$dnsrewrite=REFUSED -|a_records^$dnsrewrite=127.0.0.1 -|a_records^$dnsrewrite=127.0.0.2 +|a-records^$dnsrewrite=127.0.0.1 +|a-records^$dnsrewrite=127.0.0.2 -|aaaa_records^$dnsrewrite=::1 -|aaaa_records^$dnsrewrite=::2 +|aaaa-records^$dnsrewrite=::1 +|aaaa-records^$dnsrewrite=::2 -|disable_one^$dnsrewrite=127.0.0.1 -|disable_one^$dnsrewrite=127.0.0.2 -@@||disable_one^$dnsrewrite=127.0.0.1 +|disable-one^$dnsrewrite=127.0.0.1 +|disable-one^$dnsrewrite=127.0.0.2 +@@||disable-one^$dnsrewrite=127.0.0.1 -|disable_cname^$dnsrewrite=127.0.0.1 -|disable_cname^$dnsrewrite=new_cname -@@||disable_cname^$dnsrewrite=new_cname +|disable-cname^$dnsrewrite=127.0.0.1 +|disable-cname^$dnsrewrite=new-cname +@@||disable-cname^$dnsrewrite=new-cname -|disable_cname_many^$dnsrewrite=127.0.0.1 -|disable_cname_many^$dnsrewrite=new_cname_1 -|disable_cname_many^$dnsrewrite=new_cname_2 -@@||disable_cname_many^$dnsrewrite=new_cname_1 +|disable-cname-many^$dnsrewrite=127.0.0.1 +|disable-cname-many^$dnsrewrite=new-cname-1 +|disable-cname-many^$dnsrewrite=new-cname-2 +@@||disable-cname-many^$dnsrewrite=new-cname-1 -|disable_all^$dnsrewrite=127.0.0.1 -|disable_all^$dnsrewrite=127.0.0.2 -@@||disable_all^$dnsrewrite +|disable-all^$dnsrewrite=127.0.0.1 +|disable-all^$dnsrewrite=127.0.0.2 +@@||disable-all^$dnsrewrite ` - f := NewForTest(nil, []Filter{{ID: 0, Data: []byte(text)}}) + + f := newForTest(nil, []Filter{{ID: 0, Data: []byte(text)}}) setts := &RequestFilteringSettings{ FilteringEnabled: true, } @@ -60,10 +61,10 @@ func TestDNSFilter_CheckHostRules_dnsrewrite(t *testing.T) { res, err := f.CheckHostRules(host, dtyp, setts) assert.Nil(t, err) - assert.Equal(t, "new_cname", res.CanonName) + assert.Equal(t, "new-cname", res.CanonName) }) - t.Run("a_record", func(t *testing.T) { + t.Run("a-record", func(t *testing.T) { dtyp := dns.TypeA host := path.Base(t.Name()) @@ -78,7 +79,7 @@ func TestDNSFilter_CheckHostRules_dnsrewrite(t *testing.T) { } }) - t.Run("aaaa_record", func(t *testing.T) { + t.Run("aaaa-record", func(t *testing.T) { dtyp := dns.TypeAAAA host := path.Base(t.Name()) @@ -93,7 +94,7 @@ func TestDNSFilter_CheckHostRules_dnsrewrite(t *testing.T) { } }) - t.Run("txt_record", func(t *testing.T) { + t.Run("txt-record", func(t *testing.T) { dtyp := dns.TypeTXT host := path.Base(t.Name()) res, err := f.CheckHostRules(host, dtyp, setts) @@ -102,7 +103,7 @@ func TestDNSFilter_CheckHostRules_dnsrewrite(t *testing.T) { if dnsrr := res.DNSRewriteResult; assert.NotNil(t, dnsrr) { assert.Equal(t, dns.RcodeSuccess, dnsrr.RCode) if strVals := dnsrr.Response[dtyp]; assert.Len(t, strVals, 1) { - assert.Equal(t, "hello_world", strVals[0]) + assert.Equal(t, "hello-world", strVals[0]) } } }) @@ -117,7 +118,7 @@ func TestDNSFilter_CheckHostRules_dnsrewrite(t *testing.T) { } }) - t.Run("a_records", func(t *testing.T) { + t.Run("a-records", func(t *testing.T) { dtyp := dns.TypeA host := path.Base(t.Name()) @@ -133,7 +134,7 @@ func TestDNSFilter_CheckHostRules_dnsrewrite(t *testing.T) { } }) - t.Run("aaaa_records", func(t *testing.T) { + t.Run("aaaa-records", func(t *testing.T) { dtyp := dns.TypeAAAA host := path.Base(t.Name()) @@ -149,7 +150,7 @@ func TestDNSFilter_CheckHostRules_dnsrewrite(t *testing.T) { } }) - t.Run("disable_one", func(t *testing.T) { + t.Run("disable-one", func(t *testing.T) { dtyp := dns.TypeA host := path.Base(t.Name()) @@ -164,13 +165,13 @@ func TestDNSFilter_CheckHostRules_dnsrewrite(t *testing.T) { } }) - t.Run("disable_cname", func(t *testing.T) { + t.Run("disable-cname", func(t *testing.T) { dtyp := dns.TypeA host := path.Base(t.Name()) res, err := f.CheckHostRules(host, dtyp, setts) assert.Nil(t, err) - assert.Equal(t, "", res.CanonName) + assert.Empty(t, res.CanonName) if dnsrr := res.DNSRewriteResult; assert.NotNil(t, dnsrr) { assert.Equal(t, dns.RcodeSuccess, dnsrr.RCode) @@ -180,23 +181,23 @@ func TestDNSFilter_CheckHostRules_dnsrewrite(t *testing.T) { } }) - t.Run("disable_cname_many", func(t *testing.T) { + t.Run("disable-cname-many", func(t *testing.T) { dtyp := dns.TypeA host := path.Base(t.Name()) res, err := f.CheckHostRules(host, dtyp, setts) assert.Nil(t, err) - assert.Equal(t, "new_cname_2", res.CanonName) + assert.Equal(t, "new-cname-2", res.CanonName) assert.Nil(t, res.DNSRewriteResult) }) - t.Run("disable_all", func(t *testing.T) { + t.Run("disable-all", func(t *testing.T) { dtyp := dns.TypeA host := path.Base(t.Name()) res, err := f.CheckHostRules(host, dtyp, setts) assert.Nil(t, err) - assert.Equal(t, "", res.CanonName) - assert.Len(t, res.Rules, 0) + assert.Empty(t, res.CanonName) + assert.Empty(t, res.Rules) }) } diff --git a/internal/dnsfilter/rewrites.go b/internal/dnsfilter/rewrites.go index 8db1fd0b..146be60a 100644 --- a/internal/dnsfilter/rewrites.go +++ b/internal/dnsfilter/rewrites.go @@ -219,7 +219,7 @@ func (d *DNSFilter) handleRewriteDelete(w http.ResponseWriter, r *http.Request) } func (d *DNSFilter) registerRewritesHandlers() { - d.Config.HTTPRegister("GET", "/control/rewrite/list", d.handleRewriteList) - d.Config.HTTPRegister("POST", "/control/rewrite/add", d.handleRewriteAdd) - d.Config.HTTPRegister("POST", "/control/rewrite/delete", d.handleRewriteDelete) + d.Config.HTTPRegister(http.MethodGet, "/control/rewrite/list", d.handleRewriteList) + d.Config.HTTPRegister(http.MethodPost, "/control/rewrite/add", d.handleRewriteAdd) + d.Config.HTTPRegister(http.MethodPost, "/control/rewrite/delete", d.handleRewriteDelete) } diff --git a/internal/dnsfilter/rewrites_test.go b/internal/dnsfilter/rewrites_test.go index 4304b6de..a56d2a48 100644 --- a/internal/dnsfilter/rewrites_test.go +++ b/internal/dnsfilter/rewrites_test.go @@ -9,7 +9,8 @@ import ( ) func TestRewrites(t *testing.T) { - d := DNSFilter{} + d := newForTest(nil, nil) + t.Cleanup(d.Close) // CNAME, A, AAAA d.Rewrites = []RewriteEntry{ {"somecname", "somehost.com", 0, nil}, @@ -25,16 +26,16 @@ func TestRewrites(t *testing.T) { assert.Equal(t, NotFilteredNotFound, r.Reason) r = d.processRewrites("www.host.com", dns.TypeA) - assert.Equal(t, ReasonRewrite, r.Reason) + assert.Equal(t, Rewritten, r.Reason) assert.Equal(t, "host.com", r.CanonName) - assert.Equal(t, 2, len(r.IPList)) - assert.True(t, r.IPList[0].Equal(net.ParseIP("1.2.3.4"))) - assert.True(t, r.IPList[1].Equal(net.ParseIP("1.2.3.5"))) + assert.Len(t, r.IPList, 2) + assert.True(t, r.IPList[0].Equal(net.IP{1, 2, 3, 4})) + assert.True(t, r.IPList[1].Equal(net.IP{1, 2, 3, 5})) r = d.processRewrites("www.host.com", dns.TypeAAAA) - assert.Equal(t, ReasonRewrite, r.Reason) + assert.Equal(t, Rewritten, r.Reason) assert.Equal(t, "host.com", r.CanonName) - assert.Equal(t, 1, len(r.IPList)) + assert.Len(t, r.IPList, 1) assert.True(t, r.IPList[0].Equal(net.ParseIP("1:2:3::4"))) // wildcard @@ -44,12 +45,12 @@ func TestRewrites(t *testing.T) { } d.prepareRewrites() r = d.processRewrites("host.com", dns.TypeA) - assert.Equal(t, ReasonRewrite, r.Reason) - assert.True(t, r.IPList[0].Equal(net.ParseIP("1.2.3.4"))) + assert.Equal(t, Rewritten, r.Reason) + assert.True(t, r.IPList[0].Equal(net.IP{1, 2, 3, 4})) r = d.processRewrites("www.host.com", dns.TypeA) - assert.Equal(t, ReasonRewrite, r.Reason) - assert.True(t, r.IPList[0].Equal(net.ParseIP("1.2.3.5"))) + assert.Equal(t, Rewritten, r.Reason) + assert.True(t, r.IPList[0].Equal(net.IP{1, 2, 3, 5})) r = d.processRewrites("www.host2.com", dns.TypeA) assert.Equal(t, NotFilteredNotFound, r.Reason) @@ -61,9 +62,9 @@ func TestRewrites(t *testing.T) { } d.prepareRewrites() r = d.processRewrites("a.host.com", dns.TypeA) - assert.Equal(t, ReasonRewrite, r.Reason) - assert.True(t, len(r.IPList) == 1) - assert.True(t, r.IPList[0].Equal(net.ParseIP("1.2.3.4"))) + assert.Equal(t, Rewritten, r.Reason) + assert.Len(t, r.IPList, 1) + assert.True(t, r.IPList[0].Equal(net.IP{1, 2, 3, 4})) // wildcard + CNAME d.Rewrites = []RewriteEntry{ @@ -72,9 +73,9 @@ func TestRewrites(t *testing.T) { } d.prepareRewrites() r = d.processRewrites("www.host.com", dns.TypeA) - assert.Equal(t, ReasonRewrite, r.Reason) + assert.Equal(t, Rewritten, r.Reason) assert.Equal(t, "host.com", r.CanonName) - assert.True(t, r.IPList[0].Equal(net.ParseIP("1.2.3.4"))) + assert.True(t, r.IPList[0].Equal(net.IP{1, 2, 3, 4})) // 2 CNAMEs d.Rewrites = []RewriteEntry{ @@ -84,10 +85,10 @@ func TestRewrites(t *testing.T) { } d.prepareRewrites() r = d.processRewrites("b.host.com", dns.TypeA) - assert.Equal(t, ReasonRewrite, r.Reason) + assert.Equal(t, Rewritten, r.Reason) assert.Equal(t, "host.com", r.CanonName) - assert.True(t, len(r.IPList) == 1) - assert.True(t, r.IPList[0].Equal(net.ParseIP("1.2.3.4"))) + assert.Len(t, r.IPList, 1) + assert.True(t, r.IPList[0].Equal(net.IP{1, 2, 3, 4})) // 2 CNAMEs + wildcard d.Rewrites = []RewriteEntry{ @@ -97,14 +98,15 @@ func TestRewrites(t *testing.T) { } d.prepareRewrites() r = d.processRewrites("b.host.com", dns.TypeA) - assert.Equal(t, ReasonRewrite, r.Reason) + assert.Equal(t, Rewritten, r.Reason) assert.Equal(t, "x.somehost.com", r.CanonName) - assert.True(t, len(r.IPList) == 1) - assert.True(t, r.IPList[0].Equal(net.ParseIP("1.2.3.4"))) + assert.Len(t, r.IPList, 1) + assert.True(t, r.IPList[0].Equal(net.IP{1, 2, 3, 4})) } func TestRewritesLevels(t *testing.T) { - d := DNSFilter{} + d := newForTest(nil, nil) + t.Cleanup(d.Close) // exact host, wildcard L2, wildcard L3 d.Rewrites = []RewriteEntry{ {"host.com", "1.1.1.1", 0, nil}, @@ -115,25 +117,26 @@ func TestRewritesLevels(t *testing.T) { // match exact r := d.processRewrites("host.com", dns.TypeA) - assert.Equal(t, ReasonRewrite, r.Reason) - assert.Equal(t, 1, len(r.IPList)) - assert.Equal(t, "1.1.1.1", r.IPList[0].String()) + assert.Equal(t, Rewritten, r.Reason) + assert.Len(t, r.IPList, 1) + assert.True(t, net.IP{1, 1, 1, 1}.Equal(r.IPList[0])) // match L2 r = d.processRewrites("sub.host.com", dns.TypeA) - assert.Equal(t, ReasonRewrite, r.Reason) - assert.Equal(t, 1, len(r.IPList)) - assert.Equal(t, "2.2.2.2", r.IPList[0].String()) + assert.Equal(t, Rewritten, r.Reason) + assert.Len(t, r.IPList, 1) + assert.True(t, net.IP{2, 2, 2, 2}.Equal(r.IPList[0])) // match L3 r = d.processRewrites("my.sub.host.com", dns.TypeA) - assert.Equal(t, ReasonRewrite, r.Reason) - assert.Equal(t, 1, len(r.IPList)) - assert.Equal(t, "3.3.3.3", r.IPList[0].String()) + assert.Equal(t, Rewritten, r.Reason) + assert.Len(t, r.IPList, 1) + assert.True(t, net.IP{3, 3, 3, 3}.Equal(r.IPList[0])) } func TestRewritesExceptionCNAME(t *testing.T) { - d := DNSFilter{} + d := newForTest(nil, nil) + t.Cleanup(d.Close) // wildcard; exception for a sub-domain d.Rewrites = []RewriteEntry{ {"*.host.com", "2.2.2.2", 0, nil}, @@ -143,9 +146,9 @@ func TestRewritesExceptionCNAME(t *testing.T) { // match sub-domain r := d.processRewrites("my.host.com", dns.TypeA) - assert.Equal(t, ReasonRewrite, r.Reason) - assert.Equal(t, 1, len(r.IPList)) - assert.Equal(t, "2.2.2.2", r.IPList[0].String()) + assert.Equal(t, Rewritten, r.Reason) + assert.Len(t, r.IPList, 1) + assert.True(t, net.IP{2, 2, 2, 2}.Equal(r.IPList[0])) // match sub-domain, but handle exception r = d.processRewrites("sub.host.com", dns.TypeA) @@ -153,7 +156,8 @@ func TestRewritesExceptionCNAME(t *testing.T) { } func TestRewritesExceptionWC(t *testing.T) { - d := DNSFilter{} + d := newForTest(nil, nil) + t.Cleanup(d.Close) // wildcard; exception for a sub-wildcard d.Rewrites = []RewriteEntry{ {"*.host.com", "2.2.2.2", 0, nil}, @@ -163,9 +167,9 @@ func TestRewritesExceptionWC(t *testing.T) { // match sub-domain r := d.processRewrites("my.host.com", dns.TypeA) - assert.Equal(t, ReasonRewrite, r.Reason) - assert.Equal(t, 1, len(r.IPList)) - assert.Equal(t, "2.2.2.2", r.IPList[0].String()) + assert.Equal(t, Rewritten, r.Reason) + assert.Len(t, r.IPList, 1) + assert.True(t, net.IP{2, 2, 2, 2}.Equal(r.IPList[0])) // match sub-domain, but handle exception r = d.processRewrites("my.sub.host.com", dns.TypeA) @@ -173,7 +177,8 @@ func TestRewritesExceptionWC(t *testing.T) { } func TestRewritesExceptionIP(t *testing.T) { - d := DNSFilter{} + d := newForTest(nil, nil) + t.Cleanup(d.Close) // exception for AAAA record d.Rewrites = []RewriteEntry{ {"host.com", "1.2.3.4", 0, nil}, @@ -186,9 +191,9 @@ func TestRewritesExceptionIP(t *testing.T) { // match domain r := d.processRewrites("host.com", dns.TypeA) - assert.Equal(t, ReasonRewrite, r.Reason) - assert.Equal(t, 1, len(r.IPList)) - assert.Equal(t, "1.2.3.4", r.IPList[0].String()) + assert.Equal(t, Rewritten, r.Reason) + assert.Len(t, r.IPList, 1) + assert.True(t, net.IP{1, 2, 3, 4}.Equal(r.IPList[0])) // match exception r = d.processRewrites("host.com", dns.TypeAAAA) @@ -200,8 +205,8 @@ func TestRewritesExceptionIP(t *testing.T) { // match domain r = d.processRewrites("host2.com", dns.TypeAAAA) - assert.Equal(t, ReasonRewrite, r.Reason) - assert.Equal(t, 1, len(r.IPList)) + assert.Equal(t, Rewritten, r.Reason) + assert.Len(t, r.IPList, 1) assert.Equal(t, "::1", r.IPList[0].String()) // match exception @@ -210,6 +215,6 @@ func TestRewritesExceptionIP(t *testing.T) { // match domain r = d.processRewrites("host3.com", dns.TypeAAAA) - assert.Equal(t, ReasonRewrite, r.Reason) - assert.Equal(t, 0, len(r.IPList)) + assert.Equal(t, Rewritten, r.Reason) + assert.Empty(t, r.IPList) } diff --git a/internal/dnsfilter/safebrowsing.go b/internal/dnsfilter/safebrowsing.go index f5aaca9f..9142c7c4 100644 --- a/internal/dnsfilter/safebrowsing.go +++ b/internal/dnsfilter/safebrowsing.go @@ -30,6 +30,20 @@ const ( pcTXTSuffix = `pc.dns.adguard.com.` ) +// SetParentalUpstream sets the parental upstream for *DNSFilter. +// +// TODO(e.burkov): Remove this in v1 API to forbid the direct access. +func (d *DNSFilter) SetParentalUpstream(u upstream.Upstream) { + d.parentalUpstream = u +} + +// SetSafeBrowsingUpstream sets the safe browsing upstream for *DNSFilter. +// +// TODO(e.burkov): Remove this in v1 API to forbid the direct access. +func (d *DNSFilter) SetSafeBrowsingUpstream(u upstream.Upstream) { + d.safeBrowsingUpstream = u +} + func (d *DNSFilter) initSecurityServices() error { var err error d.safeBrowsingServer = defaultSafebrowsingServer @@ -37,22 +51,24 @@ func (d *DNSFilter) initSecurityServices() error { opts := upstream.Options{ Timeout: dnsTimeout, ServerIPAddrs: []net.IP{ - net.ParseIP("94.140.14.15"), - net.ParseIP("94.140.15.16"), + {94, 140, 14, 15}, + {94, 140, 15, 16}, net.ParseIP("2a10:50c0::bad1:ff"), net.ParseIP("2a10:50c0::bad2:ff"), }, } - d.parentalUpstream, err = upstream.AddressToUpstream(d.parentalServer, opts) + parUps, err := upstream.AddressToUpstream(d.parentalServer, opts) if err != nil { - return err + return fmt.Errorf("converting parental server: %w", err) } + d.SetParentalUpstream(parUps) - d.safeBrowsingUpstream, err = upstream.AddressToUpstream(d.safeBrowsingServer, opts) + sbUps, err := upstream.AddressToUpstream(d.safeBrowsingServer, opts) if err != nil { - return err + return fmt.Errorf("converting safe browsing server: %w", err) } + d.SetSafeBrowsingUpstream(sbUps) return nil } @@ -200,7 +216,6 @@ func (c *sbCtx) processTXT(resp *dns.Msg) (bool, [][]byte) { log.Debug("%s: received hashes for %s: %v", c.svc, c.host, txt.Txt) for _, t := range txt.Txt { - if len(t) != 32*2 { continue } @@ -228,7 +243,7 @@ func (c *sbCtx) processTXT(resp *dns.Msg) (bool, [][]byte) { func (c *sbCtx) storeCache(hashes [][]byte) { sort.Slice(hashes, func(a, b int) bool { - return bytes.Compare(hashes[a], hashes[b]) < 0 + return bytes.Compare(hashes[a], hashes[b]) == -1 }) var curData []byte @@ -346,16 +361,12 @@ func (d *DNSFilter) handleSafeBrowsingDisable(w http.ResponseWriter, r *http.Req } func (d *DNSFilter) handleSafeBrowsingStatus(w http.ResponseWriter, r *http.Request) { - data := map[string]interface{}{ - "enabled": d.Config.SafeBrowsingEnabled, - } - jsonVal, err := json.Marshal(data) - if err != nil { - httpError(r, w, http.StatusInternalServerError, "Unable to marshal status json: %s", err) - } - w.Header().Set("Content-Type", "application/json") - _, err = w.Write(jsonVal) + err := json.NewEncoder(w).Encode(&struct { + Enabled bool `json:"enabled"` + }{ + Enabled: d.Config.SafeBrowsingEnabled, + }) if err != nil { httpError(r, w, http.StatusInternalServerError, "Unable to write response json: %s", err) return @@ -373,17 +384,12 @@ func (d *DNSFilter) handleParentalDisable(w http.ResponseWriter, r *http.Request } func (d *DNSFilter) handleParentalStatus(w http.ResponseWriter, r *http.Request) { - data := map[string]interface{}{ - "enabled": d.Config.ParentalEnabled, - } - jsonVal, err := json.Marshal(data) - if err != nil { - httpError(r, w, http.StatusInternalServerError, "Unable to marshal status json: %s", err) - return - } - w.Header().Set("Content-Type", "application/json") - _, err = w.Write(jsonVal) + err := json.NewEncoder(w).Encode(&struct { + Enabled bool `json:"enabled"` + }{ + Enabled: d.Config.ParentalEnabled, + }) if err != nil { httpError(r, w, http.StatusInternalServerError, "Unable to write response json: %s", err) return @@ -391,15 +397,15 @@ func (d *DNSFilter) handleParentalStatus(w http.ResponseWriter, r *http.Request) } func (d *DNSFilter) registerSecurityHandlers() { - d.Config.HTTPRegister("POST", "/control/safebrowsing/enable", d.handleSafeBrowsingEnable) - d.Config.HTTPRegister("POST", "/control/safebrowsing/disable", d.handleSafeBrowsingDisable) - d.Config.HTTPRegister("GET", "/control/safebrowsing/status", d.handleSafeBrowsingStatus) + d.Config.HTTPRegister(http.MethodPost, "/control/safebrowsing/enable", d.handleSafeBrowsingEnable) + d.Config.HTTPRegister(http.MethodPost, "/control/safebrowsing/disable", d.handleSafeBrowsingDisable) + d.Config.HTTPRegister(http.MethodGet, "/control/safebrowsing/status", d.handleSafeBrowsingStatus) - d.Config.HTTPRegister("POST", "/control/parental/enable", d.handleParentalEnable) - d.Config.HTTPRegister("POST", "/control/parental/disable", d.handleParentalDisable) - d.Config.HTTPRegister("GET", "/control/parental/status", d.handleParentalStatus) + d.Config.HTTPRegister(http.MethodPost, "/control/parental/enable", d.handleParentalEnable) + d.Config.HTTPRegister(http.MethodPost, "/control/parental/disable", d.handleParentalDisable) + d.Config.HTTPRegister(http.MethodGet, "/control/parental/status", d.handleParentalStatus) - d.Config.HTTPRegister("POST", "/control/safesearch/enable", d.handleSafeSearchEnable) - d.Config.HTTPRegister("POST", "/control/safesearch/disable", d.handleSafeSearchDisable) - d.Config.HTTPRegister("GET", "/control/safesearch/status", d.handleSafeSearchStatus) + d.Config.HTTPRegister(http.MethodPost, "/control/safesearch/enable", d.handleSafeSearchEnable) + d.Config.HTTPRegister(http.MethodPost, "/control/safesearch/disable", d.handleSafeSearchDisable) + d.Config.HTTPRegister(http.MethodGet, "/control/safesearch/status", d.handleSafeSearchStatus) } diff --git a/internal/dnsfilter/safebrowsing_test.go b/internal/dnsfilter/safebrowsing_test.go index 71e59446..f94a3c99 100644 --- a/internal/dnsfilter/safebrowsing_test.go +++ b/internal/dnsfilter/safebrowsing_test.go @@ -5,16 +5,15 @@ import ( "strings" "testing" - "github.com/AdguardTeam/AdGuardHome/internal/agherr" + "github.com/AdguardTeam/AdGuardHome/internal/aghtest" "github.com/AdguardTeam/golibs/cache" - "github.com/miekg/dns" "github.com/stretchr/testify/assert" ) func TestSafeBrowsingHash(t *testing.T) { // test hostnameToHashes() hashes := hostnameToHashes("1.2.3.sub.host.com") - assert.Equal(t, 3, len(hashes)) + assert.Len(t, hashes, 3) _, ok := hashes[sha256.Sum256([]byte("3.sub.host.com"))] assert.True(t, ok) _, ok = hashes[sha256.Sum256([]byte("sub.host.com"))] @@ -31,9 +30,9 @@ func TestSafeBrowsingHash(t *testing.T) { q := c.getQuestion() - assert.True(t, strings.Contains(q, "7a1b.")) - assert.True(t, strings.Contains(q, "af5a.")) - assert.True(t, strings.Contains(q, "eb11.")) + assert.Contains(t, q, "7a1b.") + assert.Contains(t, q, "af5a.") + assert.Contains(t, q, "eb11.") assert.True(t, strings.HasSuffix(q, "sb.dns.adguard.com.")) } @@ -81,7 +80,7 @@ func TestSafeBrowsingCache(t *testing.T) { c.hashToHost[hash] = "sub.host.com" hash = sha256.Sum256([]byte("nonexisting.com")) c.hashToHost[hash] = "nonexisting.com" - assert.Equal(t, 0, c.getCached()) + assert.Empty(t, c.getCached()) hash = sha256.Sum256([]byte("sub.host.com")) _, ok := c.hashToHost[hash] @@ -103,30 +102,17 @@ func TestSafeBrowsingCache(t *testing.T) { c.hashToHost[hash] = "sub.host.com" c.cache.Set(hash[0:2], make([]byte, 32)) - assert.Equal(t, 0, c.getCached()) -} - -// testErrUpstream implements upstream.Upstream interface for replacing real -// upstream in tests. -type testErrUpstream struct{} - -// Exchange always returns nil Msg and non-nil error. -func (teu *testErrUpstream) Exchange(*dns.Msg) (*dns.Msg, error) { - return nil, agherr.Error("bad") -} - -func (teu *testErrUpstream) Address() string { - return "" + assert.Empty(t, c.getCached()) } func TestSBPC_checkErrorUpstream(t *testing.T) { - d := NewForTest(&Config{SafeBrowsingEnabled: true}, nil) - defer d.Close() + d := newForTest(&Config{SafeBrowsingEnabled: true}, nil) + t.Cleanup(d.Close) - ups := &testErrUpstream{} + ups := &aghtest.TestErrUpstream{} - d.safeBrowsingUpstream = ups - d.parentalUpstream = ups + d.SetSafeBrowsingUpstream(ups) + d.SetParentalUpstream(ups) _, err := d.checkSafeBrowsing("smthng.com") assert.NotNil(t, err) @@ -134,3 +120,87 @@ func TestSBPC_checkErrorUpstream(t *testing.T) { _, err = d.checkParental("smthng.com") assert.NotNil(t, err) } + +func TestSBPC(t *testing.T) { + d := newForTest(&Config{SafeBrowsingEnabled: true}, nil) + t.Cleanup(d.Close) + + const hostname = "example.org" + + testCases := []struct { + name string + block bool + testFunc func(string) (Result, error) + testCache cache.Cache + }{{ + name: "sb_no_block", + block: false, + testFunc: d.checkSafeBrowsing, + testCache: gctx.safebrowsingCache, + }, { + name: "sb_block", + block: true, + testFunc: d.checkSafeBrowsing, + testCache: gctx.safebrowsingCache, + }, { + name: "pc_no_block", + block: false, + testFunc: d.checkParental, + testCache: gctx.parentalCache, + }, { + name: "pc_block", + block: true, + testFunc: d.checkParental, + testCache: gctx.parentalCache, + }} + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + // Prepare the upstream. + ups := &aghtest.TestBlockUpstream{ + Hostname: hostname, + Block: tc.block, + } + d.SetSafeBrowsingUpstream(ups) + d.SetParentalUpstream(ups) + + // Firstly, check the request blocking. + hits := 0 + res, err := tc.testFunc(hostname) + assert.Nil(t, err) + if tc.block { + assert.True(t, res.IsFiltered) + assert.Len(t, res.Rules, 1) + hits++ + } else { + assert.False(t, res.IsFiltered) + } + + // Check the cache state, check the response is now cached. + assert.Equal(t, 1, tc.testCache.Stats().Count) + assert.Equal(t, hits, tc.testCache.Stats().Hit) + + // There was one request to an upstream. + assert.Equal(t, 1, ups.RequestsCount()) + + // Now make the same request to check the cache was used. + res, err = tc.testFunc(hostname) + assert.Nil(t, err) + if tc.block { + assert.True(t, res.IsFiltered) + assert.Len(t, res.Rules, 1) + } else { + assert.False(t, res.IsFiltered) + } + + // Check the cache state, it should've been used. + assert.Equal(t, 1, tc.testCache.Stats().Count) + assert.Equal(t, hits+1, tc.testCache.Stats().Hit) + + // Check that there were no additional requests. + assert.Equal(t, 1, ups.RequestsCount()) + + purgeCaches() + }) + } +} diff --git a/internal/dnsfilter/safesearch.go b/internal/dnsfilter/safesearch.go index 4aefa5e1..bc72cc9e 100644 --- a/internal/dnsfilter/safesearch.go +++ b/internal/dnsfilter/safesearch.go @@ -2,6 +2,7 @@ package dnsfilter import ( "bytes" + "context" "encoding/binary" "encoding/gob" "encoding/json" @@ -101,15 +102,14 @@ func (d *DNSFilter) checkSafeSearch(host string) (Result, error) { return res, nil } - // TODO this address should be resolved with upstream that was configured in dnsforward - ips, err := net.LookupIP(safeHost) + ipAddrs, err := d.resolver.LookupIPAddr(context.Background(), safeHost) if err != nil { log.Tracef("SafeSearchDomain for %s was found but failed to lookup for %s cause %s", host, safeHost, err) return Result{}, err } - for _, ip := range ips { - if ipv4 := ip.To4(); ipv4 != nil { + for _, ipAddr := range ipAddrs { + if ipv4 := ipAddr.IP.To4(); ipv4 != nil { res.Rules[0].IP = ipv4 l := d.setCacheResult(gctx.safeSearchCache, host, res) @@ -133,17 +133,12 @@ func (d *DNSFilter) handleSafeSearchDisable(w http.ResponseWriter, r *http.Reque } func (d *DNSFilter) handleSafeSearchStatus(w http.ResponseWriter, r *http.Request) { - data := map[string]interface{}{ - "enabled": d.Config.SafeSearchEnabled, - } - jsonVal, err := json.Marshal(data) - if err != nil { - httpError(r, w, http.StatusInternalServerError, "Unable to marshal status json: %s", err) - return - } - w.Header().Set("Content-Type", "application/json") - _, err = w.Write(jsonVal) + err := json.NewEncoder(w).Encode(&struct { + Enabled bool `json:"enabled"` + }{ + Enabled: d.Config.SafeSearchEnabled, + }) if err != nil { httpError(r, w, http.StatusInternalServerError, "Unable to write response json: %s", err) return diff --git a/internal/dnsforward/access.go b/internal/dnsforward/access.go index 5038a89a..8afae955 100644 --- a/internal/dnsforward/access.go +++ b/internal/dnsforward/access.go @@ -83,20 +83,21 @@ func processIPCIDRArray(dst *map[string]bool, dstIPNet *[]net.IPNet, src []strin // Returns the item from the "disallowedClients" list that lead to blocking IP. // If it returns TRUE and an empty string, it means that the "allowedClients" is not empty, // but the ip does not belong to it. -func (a *accessCtx) IsBlockedIP(ip string) (bool, string) { +func (a *accessCtx) IsBlockedIP(ip net.IP) (bool, string) { + ipStr := ip.String() + a.lock.Lock() defer a.lock.Unlock() if len(a.allowedClients) != 0 || len(a.allowedClientsIPNet) != 0 { - _, ok := a.allowedClients[ip] + _, ok := a.allowedClients[ipStr] if ok { return false, "" } if len(a.allowedClientsIPNet) != 0 { - ipAddr := net.ParseIP(ip) for _, ipnet := range a.allowedClientsIPNet { - if ipnet.Contains(ipAddr) { + if ipnet.Contains(ip) { return false, "" } } @@ -105,15 +106,14 @@ func (a *accessCtx) IsBlockedIP(ip string) (bool, string) { return true, "" } - _, ok := a.disallowedClients[ip] + _, ok := a.disallowedClients[ipStr] if ok { - return true, ip + return true, ipStr } if len(a.disallowedClientsIPNet) != 0 { - ipAddr := net.ParseIP(ip) for _, ipnet := range a.disallowedClientsIPNet { - if ipnet.Contains(ipAddr) { + if ipnet.Contains(ip) { return true, ipnet.String() } } diff --git a/internal/dnsforward/access_test.go b/internal/dnsforward/access_test.go index 250b4931..af13b02e 100644 --- a/internal/dnsforward/access_test.go +++ b/internal/dnsforward/access_test.go @@ -1,6 +1,7 @@ package dnsforward import ( + "net" "testing" "github.com/stretchr/testify/assert" @@ -8,44 +9,44 @@ import ( func TestIsBlockedIPAllowed(t *testing.T) { a := &accessCtx{} - assert.True(t, a.Init([]string{"1.1.1.1", "2.2.0.0/16"}, nil, nil) == nil) + assert.Nil(t, a.Init([]string{"1.1.1.1", "2.2.0.0/16"}, nil, nil)) - disallowed, disallowedRule := a.IsBlockedIP("1.1.1.1") + disallowed, disallowedRule := a.IsBlockedIP(net.IPv4(1, 1, 1, 1)) assert.False(t, disallowed) - assert.Equal(t, "", disallowedRule) + assert.Empty(t, disallowedRule) - disallowed, disallowedRule = a.IsBlockedIP("1.1.1.2") + disallowed, disallowedRule = a.IsBlockedIP(net.IPv4(1, 1, 1, 2)) assert.True(t, disallowed) - assert.Equal(t, "", disallowedRule) + assert.Empty(t, disallowedRule) - disallowed, disallowedRule = a.IsBlockedIP("2.2.1.1") + disallowed, disallowedRule = a.IsBlockedIP(net.IPv4(2, 2, 1, 1)) assert.False(t, disallowed) - assert.Equal(t, "", disallowedRule) + assert.Empty(t, disallowedRule) - disallowed, disallowedRule = a.IsBlockedIP("2.3.1.1") + disallowed, disallowedRule = a.IsBlockedIP(net.IPv4(2, 3, 1, 1)) assert.True(t, disallowed) - assert.Equal(t, "", disallowedRule) + assert.Empty(t, disallowedRule) } func TestIsBlockedIPDisallowed(t *testing.T) { a := &accessCtx{} - assert.True(t, a.Init(nil, []string{"1.1.1.1", "2.2.0.0/16"}, nil) == nil) + assert.Nil(t, a.Init(nil, []string{"1.1.1.1", "2.2.0.0/16"}, nil)) - disallowed, disallowedRule := a.IsBlockedIP("1.1.1.1") + disallowed, disallowedRule := a.IsBlockedIP(net.IPv4(1, 1, 1, 1)) assert.True(t, disallowed) assert.Equal(t, "1.1.1.1", disallowedRule) - disallowed, disallowedRule = a.IsBlockedIP("1.1.1.2") + disallowed, disallowedRule = a.IsBlockedIP(net.IPv4(1, 1, 1, 2)) assert.False(t, disallowed) - assert.Equal(t, "", disallowedRule) + assert.Empty(t, disallowedRule) - disallowed, disallowedRule = a.IsBlockedIP("2.2.1.1") + disallowed, disallowedRule = a.IsBlockedIP(net.IPv4(2, 2, 1, 1)) assert.True(t, disallowed) assert.Equal(t, "2.2.0.0/16", disallowedRule) - disallowed, disallowedRule = a.IsBlockedIP("2.3.1.1") + disallowed, disallowedRule = a.IsBlockedIP(net.IPv4(2, 3, 1, 1)) assert.False(t, disallowed) - assert.Equal(t, "", disallowedRule) + assert.Empty(t, disallowedRule) } func TestIsBlockedIPBlockedDomain(t *testing.T) { @@ -60,13 +61,13 @@ func TestIsBlockedIPBlockedDomain(t *testing.T) { // match by "host2.com" assert.True(t, a.IsBlockedDomain("host1")) assert.True(t, a.IsBlockedDomain("host2")) - assert.True(t, !a.IsBlockedDomain("host3")) + assert.False(t, a.IsBlockedDomain("host3")) // match by wildcard "*.host.com" - assert.True(t, !a.IsBlockedDomain("host.com")) + assert.False(t, a.IsBlockedDomain("host.com")) assert.True(t, a.IsBlockedDomain("asdf.host.com")) assert.True(t, a.IsBlockedDomain("qwer.asdf.host.com")) - assert.True(t, !a.IsBlockedDomain("asdf.zhost.com")) + assert.False(t, a.IsBlockedDomain("asdf.zhost.com")) // match by wildcard "||host3.com^" assert.True(t, a.IsBlockedDomain("host3.com")) diff --git a/internal/dnsforward/clientid.go b/internal/dnsforward/clientid.go new file mode 100644 index 00000000..c497c7b7 --- /dev/null +++ b/internal/dnsforward/clientid.go @@ -0,0 +1,165 @@ +package dnsforward + +import ( + "crypto/tls" + "fmt" + "path" + "strings" + + "github.com/AdguardTeam/dnsproxy/proxy" + "github.com/lucas-clemente/quic-go" +) + +const maxDomainPartLen = 64 + +// ValidateClientID returns an error if clientID is not a valid client ID. +func ValidateClientID(clientID string) (err error) { + if len(clientID) > maxDomainPartLen { + return fmt.Errorf("client id %q is too long, max: %d", clientID, maxDomainPartLen) + } + + for i, r := range clientID { + if (r >= 'a' && r <= 'z') || (r >= '0' && r <= '9') || r == '-' { + continue + } + + return fmt.Errorf("invalid char %q at index %d in client id %q", r, i, clientID) + } + + return nil +} + +// clientIDFromClientServerName extracts and validates a client ID. hostSrvName +// is the server name of the host. cliSrvName is the server name as sent by the +// client. When strict is true, and client and host server name don't match, +// clientIDFromClientServerName will return an error. +func clientIDFromClientServerName(hostSrvName, cliSrvName string, strict bool) (clientID string, err error) { + if hostSrvName == cliSrvName { + return "", nil + } + + if !strings.HasSuffix(cliSrvName, hostSrvName) { + if !strict { + return "", nil + } + + return "", fmt.Errorf("client server name %q doesn't match host server name %q", cliSrvName, hostSrvName) + } + + clientID = cliSrvName[:len(cliSrvName)-len(hostSrvName)-1] + err = ValidateClientID(clientID) + if err != nil { + return "", fmt.Errorf("invalid client id: %w", err) + } + + return clientID, nil +} + +// processClientIDHTTPS extracts the client's ID from the path of the +// client's DNS-over-HTTPS request. +func processClientIDHTTPS(ctx *dnsContext) (rc resultCode) { + pctx := ctx.proxyCtx + r := pctx.HTTPRequest + if r == nil { + ctx.err = fmt.Errorf("proxy ctx http request of proto %s is nil", pctx.Proto) + + return resultCodeError + } + + origPath := r.URL.Path + parts := strings.Split(path.Clean(origPath), "/") + if parts[0] == "" { + parts = parts[1:] + } + + if len(parts) == 0 || parts[0] != "dns-query" { + ctx.err = fmt.Errorf("client id check: invalid path %q", origPath) + + return resultCodeError + } + + clientID := "" + switch len(parts) { + case 1: + // Just /dns-query, no client ID. + return resultCodeSuccess + case 2: + clientID = parts[1] + default: + ctx.err = fmt.Errorf("client id check: invalid path %q: extra parts", origPath) + + return resultCodeError + } + + err := ValidateClientID(clientID) + if err != nil { + ctx.err = fmt.Errorf("client id check: invalid client id: %w", err) + + return resultCodeError + } + + ctx.clientID = clientID + + return resultCodeSuccess +} + +// tlsConn is a narrow interface for *tls.Conn to simplify testing. +type tlsConn interface { + ConnectionState() (cs tls.ConnectionState) +} + +// quicSession is a narrow interface for quic.Session to simplify testing. +type quicSession interface { + ConnectionState() (cs quic.ConnectionState) +} + +// processClientID extracts the client's ID from the server name of the client's +// DOT or DOQ request or the path of the client's DOH. +func processClientID(dctx *dnsContext) (rc resultCode) { + pctx := dctx.proxyCtx + proto := pctx.Proto + if proto == proxy.ProtoHTTPS { + return processClientIDHTTPS(dctx) + } else if proto != proxy.ProtoTLS && proto != proxy.ProtoQUIC { + return resultCodeSuccess + } + + srvConf := dctx.srv.conf + hostSrvName := srvConf.TLSConfig.ServerName + if hostSrvName == "" { + return resultCodeSuccess + } + + cliSrvName := "" + if proto == proxy.ProtoTLS { + conn := pctx.Conn + tc, ok := conn.(tlsConn) + if !ok { + dctx.err = fmt.Errorf("proxy ctx conn of proto %s is %T, want *tls.Conn", proto, conn) + + return resultCodeError + } + + cliSrvName = tc.ConnectionState().ServerName + } else if proto == proxy.ProtoQUIC { + qs, ok := pctx.QUICSession.(quicSession) + if !ok { + dctx.err = fmt.Errorf("proxy ctx quic session of proto %s is %T, want quic.Session", proto, pctx.QUICSession) + + return resultCodeError + } + + cliSrvName = qs.ConnectionState().ServerName + } + + clientID, err := clientIDFromClientServerName(hostSrvName, cliSrvName, srvConf.StrictSNICheck) + if err != nil { + dctx.err = fmt.Errorf("client id check: %w", err) + + return resultCodeError + } + + dctx.clientID = clientID + + return resultCodeSuccess +} diff --git a/internal/dnsforward/clientid_test.go b/internal/dnsforward/clientid_test.go new file mode 100644 index 00000000..503203f9 --- /dev/null +++ b/internal/dnsforward/clientid_test.go @@ -0,0 +1,273 @@ +package dnsforward + +import ( + "crypto/tls" + "net" + "net/http" + "net/url" + "testing" + + "github.com/AdguardTeam/dnsproxy/proxy" + "github.com/lucas-clemente/quic-go" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +// testTLSConn is a tlsConn for tests. +type testTLSConn struct { + // Conn is embedded here simply to make testTLSConn a net.Conn without + // acctually implementing all methods. + net.Conn + + serverName string +} + +// ConnectionState implements the tlsConn interface for testTLSConn. +func (c testTLSConn) ConnectionState() (cs tls.ConnectionState) { + cs.ServerName = c.serverName + + return cs +} + +// testQUICSession is a quicSession for tests. +type testQUICSession struct { + // Session is embedded here simply to make testQUICSession + // a quic.Session without acctually implementing all methods. + quic.Session + + serverName string +} + +// ConnectionState implements the quicSession interface for testQUICSession. +func (c testQUICSession) ConnectionState() (cs quic.ConnectionState) { + cs.ServerName = c.serverName + + return cs +} + +func TestProcessClientID(t *testing.T) { + testCases := []struct { + name string + proto string + hostSrvName string + cliSrvName string + wantClientID string + wantErrMsg string + wantRes resultCode + strictSNI bool + }{{ + name: "udp", + proto: proxy.ProtoUDP, + hostSrvName: "", + cliSrvName: "", + wantClientID: "", + wantErrMsg: "", + wantRes: resultCodeSuccess, + strictSNI: false, + }, { + name: "tls_no_client_id", + proto: proxy.ProtoTLS, + hostSrvName: "example.com", + cliSrvName: "example.com", + wantClientID: "", + wantErrMsg: "", + wantRes: resultCodeSuccess, + strictSNI: true, + }, { + name: "tls_no_client_server_name", + proto: proxy.ProtoTLS, + hostSrvName: "example.com", + cliSrvName: "", + wantClientID: "", + wantErrMsg: `client id check: client server name "" ` + + `doesn't match host server name "example.com"`, + wantRes: resultCodeError, + strictSNI: true, + }, { + name: "tls_no_client_server_name_no_strict", + proto: proxy.ProtoTLS, + hostSrvName: "example.com", + cliSrvName: "", + wantClientID: "", + wantErrMsg: "", + wantRes: resultCodeSuccess, + strictSNI: false, + }, { + name: "tls_client_id", + proto: proxy.ProtoTLS, + hostSrvName: "example.com", + cliSrvName: "cli.example.com", + wantClientID: "cli", + wantErrMsg: "", + wantRes: resultCodeSuccess, + strictSNI: true, + }, { + name: "tls_client_id_hostname_error", + proto: proxy.ProtoTLS, + hostSrvName: "example.com", + cliSrvName: "cli.example.net", + wantClientID: "", + wantErrMsg: `client id check: client server name "cli.example.net" ` + + `doesn't match host server name "example.com"`, + wantRes: resultCodeError, + strictSNI: true, + }, { + name: "tls_invalid_client_id", + proto: proxy.ProtoTLS, + hostSrvName: "example.com", + cliSrvName: "!!!.example.com", + wantClientID: "", + wantErrMsg: `client id check: invalid client id: invalid char '!' ` + + `at index 0 in client id "!!!"`, + wantRes: resultCodeError, + strictSNI: true, + }, { + name: "tls_client_id_too_long", + proto: proxy.ProtoTLS, + hostSrvName: "example.com", + cliSrvName: `abcdefghijklmnopqrstuvwxyz0123456789abcdefghijklmno` + + `pqrstuvwxyz0123456789.example.com`, + wantClientID: "", + wantErrMsg: `client id check: invalid client id: client id "abcdefghijklmno` + + `pqrstuvwxyz0123456789abcdefghijklmnopqrstuvwxyz0123456789" ` + + `is too long, max: 64`, + wantRes: resultCodeError, + strictSNI: true, + }, { + name: "quic_client_id", + proto: proxy.ProtoQUIC, + hostSrvName: "example.com", + cliSrvName: "cli.example.com", + wantClientID: "cli", + wantErrMsg: "", + wantRes: resultCodeSuccess, + strictSNI: true, + }} + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + tlsConf := TLSConfig{ + ServerName: tc.hostSrvName, + StrictSNICheck: tc.strictSNI, + } + srv := &Server{ + conf: ServerConfig{TLSConfig: tlsConf}, + } + + var conn net.Conn + if tc.proto == proxy.ProtoTLS { + conn = testTLSConn{ + serverName: tc.cliSrvName, + } + } + + var qs quic.Session + if tc.proto == proxy.ProtoQUIC { + qs = testQUICSession{ + serverName: tc.cliSrvName, + } + } + + dctx := &dnsContext{ + srv: srv, + proxyCtx: &proxy.DNSContext{ + Proto: tc.proto, + Conn: conn, + QUICSession: qs, + }, + } + + res := processClientID(dctx) + assert.Equal(t, tc.wantRes, res) + assert.Equal(t, tc.wantClientID, dctx.clientID) + + if tc.wantErrMsg == "" { + assert.Nil(t, dctx.err) + } else { + require.NotNil(t, dctx.err) + assert.Equal(t, tc.wantErrMsg, dctx.err.Error()) + } + }) + } +} + +func TestProcessClientID_https(t *testing.T) { + testCases := []struct { + name string + path string + wantClientID string + wantErrMsg string + wantRes resultCode + }{{ + name: "no_client_id", + path: "/dns-query", + wantClientID: "", + wantErrMsg: "", + wantRes: resultCodeSuccess, + }, { + name: "no_client_id_slash", + path: "/dns-query/", + wantClientID: "", + wantErrMsg: "", + wantRes: resultCodeSuccess, + }, { + name: "client_id", + path: "/dns-query/cli", + wantClientID: "cli", + wantErrMsg: "", + wantRes: resultCodeSuccess, + }, { + name: "client_id_slash", + path: "/dns-query/cli/", + wantClientID: "cli", + wantErrMsg: "", + wantRes: resultCodeSuccess, + }, { + name: "bad_url", + path: "/foo", + wantClientID: "", + wantErrMsg: `client id check: invalid path "/foo"`, + wantRes: resultCodeError, + }, { + name: "extra", + path: "/dns-query/cli/foo", + wantClientID: "", + wantErrMsg: `client id check: invalid path "/dns-query/cli/foo": extra parts`, + wantRes: resultCodeError, + }, { + name: "invalid_client_id", + path: "/dns-query/!!!", + wantClientID: "", + wantErrMsg: `client id check: invalid client id: invalid char '!'` + + ` at index 0 in client id "!!!"`, + wantRes: resultCodeError, + }} + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + r := &http.Request{ + URL: &url.URL{ + Path: tc.path, + }, + } + + dctx := &dnsContext{ + proxyCtx: &proxy.DNSContext{ + Proto: proxy.ProtoHTTPS, + HTTPRequest: r, + }, + } + + res := processClientID(dctx) + assert.Equal(t, tc.wantRes, res) + assert.Equal(t, tc.wantClientID, dctx.clientID) + + if tc.wantErrMsg == "" { + assert.Nil(t, dctx.err) + } else { + require.NotNil(t, dctx.err) + assert.Equal(t, tc.wantErrMsg, dctx.err.Error()) + } + }) + } +} diff --git a/internal/dnsforward/config.go b/internal/dnsforward/config.go index 881174d1..81864899 100644 --- a/internal/dnsforward/config.go +++ b/internal/dnsforward/config.go @@ -24,22 +24,22 @@ type FilteringConfig struct { // Callbacks for other modules // -- - // Filtering callback function - FilterHandler func(clientAddr string, settings *dnsfilter.RequestFilteringSettings) `yaml:"-"` + // FilterHandler is an optional additional filtering callback. + FilterHandler func(clientAddr net.IP, clientID string, settings *dnsfilter.RequestFilteringSettings) `yaml:"-"` // GetCustomUpstreamByClient - a callback function that returns upstreams configuration // based on the client IP address. Returns nil if there are no custom upstreams for the client + // + // TODO(e.burkov): Replace argument type with net.IP. GetCustomUpstreamByClient func(clientAddr string) *proxy.UpstreamConfig `yaml:"-"` // Protection configuration // -- - ProtectionEnabled bool `yaml:"protection_enabled"` // whether or not use any of dnsfilter features - BlockingMode string `yaml:"blocking_mode"` // mode how to answer filtered requests - BlockingIPv4 string `yaml:"blocking_ipv4"` // IP address to be returned for a blocked A request - BlockingIPv6 string `yaml:"blocking_ipv6"` // IP address to be returned for a blocked AAAA request - BlockingIPAddrv4 net.IP `yaml:"-"` - BlockingIPAddrv6 net.IP `yaml:"-"` + ProtectionEnabled bool `yaml:"protection_enabled"` // whether or not use any of dnsfilter features + BlockingMode string `yaml:"blocking_mode"` // mode how to answer filtered requests + BlockingIPv4 net.IP `yaml:"blocking_ipv4"` // IP address to be returned for a blocked A request + BlockingIPv6 net.IP `yaml:"blocking_ipv6"` // IP address to be returned for a blocked AAAA request BlockedResponseTTL uint32 `yaml:"blocked_response_ttl"` // if 0, then default is used (3600) // IP (or domain name) which is used to respond to DNS requests blocked by parental control or safe-browsing @@ -110,6 +110,10 @@ type TLSConfig struct { CertificateChainData []byte `yaml:"-" json:"-"` PrivateKeyData []byte `yaml:"-" json:"-"` + // ServerName is the hostname of the server. Currently, it is only + // being used for client ID checking. + ServerName string `yaml:"-" json:"-"` + cert tls.Certificate // DNS names from certificate (SAN) or CN value from Subject dnsNames []string @@ -278,7 +282,7 @@ func (s *Server) prepareUpstreamSettings() error { } if len(upstreamConfig.Upstreams) == 0 { - log.Info("Warning: no default upstream servers specified, using %v", defaultDNS) + log.Info("warning: no default upstream servers specified, using %v", defaultDNS) uc, err := proxy.ParseUpstreamsConfig(defaultDNS, s.conf.BootstrapDNS, DefaultTimeout) if err != nil { return fmt.Errorf("dns: failed to parse default upstreams: %v", err) @@ -292,12 +296,13 @@ func (s *Server) prepareUpstreamSettings() error { // prepareIntlProxy - initializes DNS proxy that we use for internal DNS queries func (s *Server) prepareIntlProxy() { - intlProxyConfig := proxy.Config{ - CacheEnabled: true, - CacheSizeBytes: 4096, - UpstreamConfig: s.conf.UpstreamConfig, + s.internalProxy = &proxy.Proxy{ + Config: proxy.Config{ + CacheEnabled: true, + CacheSizeBytes: 4096, + UpstreamConfig: s.conf.UpstreamConfig, + }, } - s.internalProxy = &proxy.Proxy{Config: intlProxyConfig} } // prepareTLS - prepares TLS configuration for the DNS proxy diff --git a/internal/dnsforward/dns.go b/internal/dnsforward/dns.go index d7208d1c..acc6aa86 100644 --- a/internal/dnsforward/dns.go +++ b/internal/dnsforward/dns.go @@ -15,36 +15,69 @@ import ( // To transfer information between modules type dnsContext struct { - srv *Server - proxyCtx *proxy.DNSContext - setts *dnsfilter.RequestFilteringSettings // filtering settings for this client - startTime time.Time - result *dnsfilter.Result - origResp *dns.Msg // response received from upstream servers. Set when response is modified by filtering - origQuestion dns.Question // question received from client. Set when Rewrites are used. - err error // error returned from the module - protectionEnabled bool // filtering is enabled, dnsfilter object is ready - responseFromUpstream bool // response is received from upstream servers - origReqDNSSEC bool // DNSSEC flag in the original request from user + srv *Server + proxyCtx *proxy.DNSContext + // setts are the filtering settings for the client. + setts *dnsfilter.RequestFilteringSettings + startTime time.Time + result *dnsfilter.Result + // origResp is the response received from upstream. It is set when the + // response is modified by filters. + origResp *dns.Msg + // err is the error returned from a processing function. + err error + // clientID is the clientID from DOH, DOQ, or DOT, if provided. + clientID string + // origQuestion is the question received from the client. It is set + // when the request is modified by rewrites. + origQuestion dns.Question + // protectionEnabled shows if the filtering is enabled, and if the + // server's DNS filter is ready. + protectionEnabled bool + // responseFromUpstream shows if the response is received from the + // upstream servers. + responseFromUpstream bool + // origReqDNSSEC shows if the DNSSEC flag in the original request from + // the client is set. + origReqDNSSEC bool } +// resultCode is the result of a request processing function. +type resultCode int + const ( - resultDone = iota // module has completed its job, continue - resultFinish // module has completed its job, exit normally - resultError // an error occurred, exit with an error + // resultCodeSuccess is returned when a handler performed successfully, + // and the next handler must be called. + resultCodeSuccess resultCode = iota + // resultCodeFinish is returned when a handler performed successfully, + // and the processing of the request must be stopped. + resultCodeFinish + // resultCodeError is returned when a handler failed, and the processing + // of the request must be stopped. + resultCodeError ) // handleDNSRequest filters the incoming DNS requests and writes them to the query log func (s *Server) handleDNSRequest(_ *proxy.Proxy, d *proxy.DNSContext) error { - ctx := &dnsContext{srv: s, proxyCtx: d} - ctx.result = &dnsfilter.Result{} - ctx.startTime = time.Now() + ctx := &dnsContext{ + srv: s, + proxyCtx: d, + result: &dnsfilter.Result{}, + startTime: time.Now(), + } - type modProcessFunc func(ctx *dnsContext) int + type modProcessFunc func(ctx *dnsContext) (rc resultCode) + + // Since (*dnsforward.Server).handleDNSRequest(...) is used as + // proxy.(Config).RequestHandler, there is no need for additional index + // out of range checking in any of the following functions, because the + // (*proxy.Proxy).handleDNSRequest method performs it before calling the + // appropriate handler. mods := []modProcessFunc{ processInitial, processInternalHosts, processInternalIPAddrs, + processClientID, processFilteringBeforeRequest, processUpstream, processDNSSECAfterResponse, @@ -55,13 +88,13 @@ func (s *Server) handleDNSRequest(_ *proxy.Proxy, d *proxy.DNSContext) error { for _, process := range mods { r := process(ctx) switch r { - case resultDone: + case resultCodeSuccess: // continue: call the next filter - case resultFinish: + case resultCodeFinish: return nil - case resultError: + case resultCodeError: return ctx.err } } @@ -73,12 +106,12 @@ func (s *Server) handleDNSRequest(_ *proxy.Proxy, d *proxy.DNSContext) error { } // Perform initial checks; process WHOIS & rDNS -func processInitial(ctx *dnsContext) int { +func processInitial(ctx *dnsContext) (rc resultCode) { s := ctx.srv d := ctx.proxyCtx if s.conf.AAAADisabled && d.Req.Question[0].Qtype == dns.TypeAAAA { _ = proxy.CheckDisabledAAAARequest(d, true) - return resultFinish + return resultCodeFinish } if s.conf.OnDNSRequest != nil { @@ -90,10 +123,10 @@ func processInitial(ctx *dnsContext) int { if (d.Req.Question[0].Qtype == dns.TypeA || d.Req.Question[0].Qtype == dns.TypeAAAA) && d.Req.Question[0].Name == "use-application-dns.net." { d.Res = s.genNXDomain(d.Req) - return resultFinish + return resultCodeFinish } - return resultDone + return resultCodeSuccess } // Return TRUE if host names doesn't contain disallowed characters @@ -151,32 +184,32 @@ func (s *Server) onDHCPLeaseChanged(flags int) { } // Respond to A requests if the target host name is associated with a lease from our DHCP server -func processInternalHosts(ctx *dnsContext) int { +func processInternalHosts(ctx *dnsContext) (rc resultCode) { s := ctx.srv req := ctx.proxyCtx.Req if !(req.Question[0].Qtype == dns.TypeA || req.Question[0].Qtype == dns.TypeAAAA) { - return resultDone + return resultCodeSuccess } host := req.Question[0].Name host = strings.ToLower(host) if !strings.HasSuffix(host, ".lan.") { - return resultDone + return resultCodeSuccess } host = strings.TrimSuffix(host, ".lan.") s.tableHostToIPLock.Lock() if s.tableHostToIP == nil { s.tableHostToIPLock.Unlock() - return resultDone + return resultCodeSuccess } ip, ok := s.tableHostToIP[host] s.tableHostToIPLock.Unlock() if !ok { - return resultDone + return resultCodeSuccess } - log.Debug("DNS: internal record: %s -> %s", req.Question[0].Name, ip.String()) + log.Debug("DNS: internal record: %s -> %s", req.Question[0].Name, ip) resp := s.makeResponse(req) @@ -194,15 +227,15 @@ func processInternalHosts(ctx *dnsContext) int { } ctx.proxyCtx.Res = resp - return resultDone + return resultCodeSuccess } // Respond to PTR requests if the target IP address is leased by our DHCP server -func processInternalIPAddrs(ctx *dnsContext) int { +func processInternalIPAddrs(ctx *dnsContext) (rc resultCode) { s := ctx.srv req := ctx.proxyCtx.Req if req.Question[0].Qtype != dns.TypePTR { - return resultDone + return resultCodeSuccess } arpa := req.Question[0].Name @@ -210,18 +243,18 @@ func processInternalIPAddrs(ctx *dnsContext) int { arpa = strings.ToLower(arpa) ip := util.DNSUnreverseAddr(arpa) if ip == nil { - return resultDone + return resultCodeSuccess } s.tablePTRLock.Lock() if s.tablePTR == nil { s.tablePTRLock.Unlock() - return resultDone + return resultCodeSuccess } host, ok := s.tablePTR[ip.String()] s.tablePTRLock.Unlock() if !ok { - return resultDone + return resultCodeSuccess } log.Debug("DNS: reverse-lookup: %s -> %s", arpa, host) @@ -237,16 +270,16 @@ func processInternalIPAddrs(ctx *dnsContext) int { ptr.Ptr = host + "." resp.Answer = append(resp.Answer, ptr) ctx.proxyCtx.Res = resp - return resultDone + return resultCodeSuccess } // Apply filtering logic -func processFilteringBeforeRequest(ctx *dnsContext) int { +func processFilteringBeforeRequest(ctx *dnsContext) (rc resultCode) { s := ctx.srv d := ctx.proxyCtx if d.Res != nil { - return resultDone // response is already set - nothing to do + return resultCodeSuccess // response is already set - nothing to do } s.RLock() @@ -260,28 +293,28 @@ func processFilteringBeforeRequest(ctx *dnsContext) int { var err error ctx.protectionEnabled = s.conf.ProtectionEnabled && s.dnsFilter != nil if ctx.protectionEnabled { - ctx.setts = s.getClientRequestFilteringSettings(d) + ctx.setts = s.getClientRequestFilteringSettings(ctx) ctx.result, err = s.filterDNSRequest(ctx) } s.RUnlock() if err != nil { ctx.err = err - return resultError + return resultCodeError } - return resultDone + return resultCodeSuccess } -// Pass request to upstream servers; process the response -func processUpstream(ctx *dnsContext) int { +// processUpstream passes request to upstream servers and handles the response. +func processUpstream(ctx *dnsContext) (rc resultCode) { s := ctx.srv d := ctx.proxyCtx if d.Res != nil { - return resultDone // response is already set - nothing to do + return resultCodeSuccess // response is already set - nothing to do } if d.Addr != nil && s.conf.GetCustomUpstreamByClient != nil { - clientIP := ipFromAddr(d.Addr) + clientIP := IPStringFromAddr(d.Addr) upstreamsConf := s.conf.GetCustomUpstreamByClient(clientIP) if upstreamsConf != nil { log.Debug("Using custom upstreams for %s", clientIP) @@ -305,26 +338,26 @@ func processUpstream(ctx *dnsContext) int { err := s.dnsProxy.Resolve(d) if err != nil { ctx.err = err - return resultError + return resultCodeError } ctx.responseFromUpstream = true - return resultDone + return resultCodeSuccess } // Process DNSSEC after response from upstream server -func processDNSSECAfterResponse(ctx *dnsContext) int { +func processDNSSECAfterResponse(ctx *dnsContext) (rc resultCode) { d := ctx.proxyCtx if !ctx.responseFromUpstream || // don't process response if it's not from upstream servers !ctx.srv.conf.EnableDNSSEC { - return resultDone + return resultCodeSuccess } if !ctx.origReqDNSSEC { optResp := d.Res.IsEdns0() if optResp != nil && !optResp.Do() { - return resultDone + return resultCodeSuccess } // Remove RRSIG records from response @@ -355,19 +388,19 @@ func processDNSSECAfterResponse(ctx *dnsContext) int { d.Res.Ns = answers } - return resultDone + return resultCodeSuccess } // Apply filtering logic after we have received response from upstream servers -func processFilteringAfterResponse(ctx *dnsContext) int { +func processFilteringAfterResponse(ctx *dnsContext) (rc resultCode) { s := ctx.srv d := ctx.proxyCtx res := ctx.result var err error switch res.Reason { - case dnsfilter.ReasonRewrite, - dnsfilter.DNSRewriteRule: + case dnsfilter.Rewritten, + dnsfilter.RewrittenRule: if len(ctx.origQuestion.Name) == 0 { // origQuestion is set in case we get only CNAME without IP from rewrites table @@ -379,7 +412,7 @@ func processFilteringAfterResponse(ctx *dnsContext) int { if len(d.Res.Answer) != 0 { answer := []dns.RR{} - answer = append(answer, s.genCNAMEAnswer(d.Req, res.CanonName)) + answer = append(answer, s.genAnswerCNAME(d.Req, res.CanonName)) answer = append(answer, d.Res.Answer...) d.Res.Answer = answer } @@ -396,7 +429,7 @@ func processFilteringAfterResponse(ctx *dnsContext) int { ctx.result, err = s.filterDNSResponse(ctx) if err != nil { ctx.err = err - return resultError + return resultCodeError } if ctx.result != nil { ctx.origResp = origResp2 // matched by response @@ -405,5 +438,5 @@ func processFilteringAfterResponse(ctx *dnsContext) int { } } - return resultDone + return resultCodeSuccess } diff --git a/internal/dnsforward/dnsforward.go b/internal/dnsforward/dnsforward.go index f1b7e7d2..68a0b6ae 100644 --- a/internal/dnsforward/dnsforward.go +++ b/internal/dnsforward/dnsforward.go @@ -2,9 +2,11 @@ package dnsforward import ( + "errors" "fmt" "net" "net/http" + "os" "runtime" "sync" "time" @@ -83,10 +85,11 @@ type DNSCreateParams struct { // NewServer creates a new instance of the dnsforward.Server // Note: this function must be called only once func NewServer(p DNSCreateParams) *Server { - s := &Server{} - s.dnsFilter = p.DNSFilter - s.stats = p.Stats - s.queryLog = p.QueryLog + s := &Server{ + dnsFilter: p.DNSFilter, + stats: p.Stats, + queryLog: p.QueryLog, + } if p.DHCPServer != nil { s.dhcpServer = p.DHCPServer @@ -101,6 +104,16 @@ func NewServer(p DNSCreateParams) *Server { return s } +// NewCustomServer creates a new instance of *Server with custom internal proxy. +func NewCustomServer(internalProxy *proxy.Proxy) *Server { + s := &Server{} + if internalProxy != nil { + s.internalProxy = internalProxy + } + + return s +} + // Close - close object func (s *Server) Close() { s.Lock() @@ -108,6 +121,12 @@ func (s *Server) Close() { s.stats = nil s.queryLog = nil s.dnsProxy = nil + + err := s.ipset.Close() + if err != nil { + log.Error("closing ipset: %s", err) + } + s.Unlock() } @@ -155,15 +174,15 @@ func (s *Server) Exchange(req *dns.Msg) (*dns.Msg, error) { return ctx.Res, nil } -// Start starts the DNS server +// Start starts the DNS server. func (s *Server) Start() error { s.Lock() defer s.Unlock() - return s.startInternal() + return s.startLocked() } -// startInternal starts without locking -func (s *Server) startInternal() error { +// startLocked starts the DNS server without locking. For internal use only. +func (s *Server) startLocked() error { err := s.dnsProxy.Start() if err == nil { s.isRunning = true @@ -178,9 +197,7 @@ func (s *Server) Prepare(config *ServerConfig) error { if config != nil { s.conf = *config if s.conf.BlockingMode == "custom_ip" { - s.conf.BlockingIPAddrv4 = net.ParseIP(s.conf.BlockingIPv4) - s.conf.BlockingIPAddrv6 = net.ParseIP(s.conf.BlockingIPv6) - if s.conf.BlockingIPAddrv4 == nil || s.conf.BlockingIPAddrv6 == nil { + if s.conf.BlockingIPv4 == nil || s.conf.BlockingIPv6 == nil { return fmt.Errorf("dns: invalid custom blocking IP address specified") } } @@ -192,11 +209,27 @@ func (s *Server) Prepare(config *ServerConfig) error { // Initialize IPSET configuration // -- - s.ipset.init(s.conf.IPSETList) + err := s.ipset.init(s.conf.IPSETList) + if err != nil { + if !errors.Is(err, os.ErrInvalid) && !errors.Is(err, os.ErrPermission) { + return fmt.Errorf("cannot initialize ipset: %w", err) + } + + // ipset cannot currently be initialized if the server was + // installed from Snap or when the user or the binary doesn't + // have the required permissions, or when the kernel doesn't + // support netfilter. + // + // Log and go on. + // + // TODO(a.garipov): The Snap problem can probably be solved if + // we add the netlink-connector interface plug. + log.Error("cannot initialize ipset: %s", err) + } // Prepare DNS servers settings // -- - err := s.prepareUpstreamSettings() + err = s.prepareUpstreamSettings() if err != nil { return err } @@ -234,15 +267,15 @@ func (s *Server) Prepare(config *ServerConfig) error { return nil } -// Stop stops the DNS server +// Stop stops the DNS server. func (s *Server) Stop() error { s.Lock() defer s.Unlock() - return s.stopInternal() + return s.stopLocked() } -// stopInternal stops without locking -func (s *Server) stopInternal() error { +// stopLocked stops the DNS server without locking. For internal use only. +func (s *Server) stopLocked() error { if s.dnsProxy != nil { err := s.dnsProxy.Stop() if err != nil { @@ -267,7 +300,7 @@ func (s *Server) Reconfigure(config *ServerConfig) error { defer s.Unlock() log.Print("Start reconfiguring the server") - err := s.stopInternal() + err := s.stopLocked() if err != nil { return fmt.Errorf("could not reconfigure the server: %w", err) } @@ -281,7 +314,7 @@ func (s *Server) Reconfigure(config *ServerConfig) error { return fmt.Errorf("could not reconfigure the server: %w", err) } - err = s.startInternal() + err = s.startLocked() if err != nil { return fmt.Errorf("could not reconfigure the server: %w", err) } @@ -300,6 +333,6 @@ func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) { } // IsBlockedIP - return TRUE if this client should be blocked -func (s *Server) IsBlockedIP(ip string) (bool, string) { +func (s *Server) IsBlockedIP(ip net.IP) (bool, string) { return s.access.IsBlockedIP(ip) } diff --git a/internal/dnsforward/dnsforward_test.go b/internal/dnsforward/dnsforward_test.go index fdee8648..93b6bcb7 100644 --- a/internal/dnsforward/dnsforward_test.go +++ b/internal/dnsforward/dnsforward_test.go @@ -18,7 +18,7 @@ import ( "testing" "time" - "github.com/AdguardTeam/AdGuardHome/internal/testutil" + "github.com/AdguardTeam/AdGuardHome/internal/aghtest" "github.com/AdguardTeam/AdGuardHome/internal/util" "github.com/AdguardTeam/AdGuardHome/internal/dhcpd" @@ -30,7 +30,7 @@ import ( ) func TestMain(m *testing.M) { - testutil.DiscardLogOutput(m) + aghtest.DiscardLogOutput(m) } const ( @@ -38,82 +38,113 @@ const ( testMessagesCount = 10 ) -func TestServer(t *testing.T) { - s := createTestServer(t) +func startDeferStop(t *testing.T, s *Server) { + t.Helper() + err := s.Start() - if err != nil { - t.Fatalf("Failed to start server: %s", err) - } + assert.Nilf(t, err, "failed to start server: %s", err) - // message over UDP - req := createGoogleATestMessage() - addr := s.dnsProxy.Addr(proxy.ProtoUDP) - client := dns.Client{Net: "udp"} - reply, _, err := client.Exchange(req, addr.String()) - if err != nil { - t.Fatalf("Couldn't talk to server %s: %s", addr, err) - } - assertGoogleAResponse(t, reply) + t.Cleanup(func() { + err := s.Stop() + assert.Nilf(t, err, "dns server failed to stop: %s", err) + }) +} - // message over TCP - req = createGoogleATestMessage() - addr = s.dnsProxy.Addr("tcp") - client = dns.Client{Net: "tcp"} - reply, _, err = client.Exchange(req, addr.String()) - if err != nil { - t.Fatalf("Couldn't talk to server %s: %s", addr, err) +func TestServer(t *testing.T) { + s := createTestServer(t, &dnsfilter.Config{}, ServerConfig{ + UDPListenAddr: &net.UDPAddr{}, + TCPListenAddr: &net.TCPAddr{}, + }) + s.conf.UpstreamConfig.Upstreams = []upstream.Upstream{ + &aghtest.TestUpstream{ + IPv4: map[string][]net.IP{ + "google-public-dns-a.google.com.": {{8, 8, 8, 8}}, + }, + }, } - assertGoogleAResponse(t, reply) + startDeferStop(t, s) - err = s.Stop() - if err != nil { - t.Fatalf("DNS server failed to stop: %s", err) + testCases := []struct { + name string + proto string + }{{ + name: "message_over_udp", + proto: proxy.ProtoUDP, + }, { + name: "message_over_tcp", + proto: proxy.ProtoTCP, + }} + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + addr := s.dnsProxy.Addr(tc.proto) + client := dns.Client{Net: tc.proto} + + reply, _, err := client.Exchange(createGoogleATestMessage(), addr.String()) + assert.Nilf(t, err, "сouldn't talk to server %s: %s", addr, err) + + assertGoogleAResponse(t, reply) + }) } } func TestServerWithProtectionDisabled(t *testing.T) { - s := createTestServer(t) - s.conf.ProtectionEnabled = false - err := s.Start() - if err != nil { - t.Fatalf("Failed to start server: %s", err) + s := createTestServer(t, &dnsfilter.Config{}, ServerConfig{ + UDPListenAddr: &net.UDPAddr{}, + TCPListenAddr: &net.TCPAddr{}, + }) + s.conf.UpstreamConfig.Upstreams = []upstream.Upstream{ + &aghtest.TestUpstream{ + IPv4: map[string][]net.IP{ + "google-public-dns-a.google.com.": {{8, 8, 8, 8}}, + }, + }, } + startDeferStop(t, s) - // message over UDP + // Message over UDP. req := createGoogleATestMessage() addr := s.dnsProxy.Addr(proxy.ProtoUDP) - client := dns.Client{Net: "udp"} + client := dns.Client{Net: proxy.ProtoUDP} reply, _, err := client.Exchange(req, addr.String()) - if err != nil { - t.Fatalf("Couldn't talk to server %s: %s", addr, err) - } + assert.Nilf(t, err, "сouldn't talk to server %s: %s", addr, err) assertGoogleAResponse(t, reply) - - err = s.Stop() - if err != nil { - t.Fatalf("DNS server failed to stop: %s", err) - } } -func TestDotServer(t *testing.T) { - // Prepare the proxy server - _, certPem, keyPem := createServerTLSConfig(t) - s := createTestServer(t) +func createTestTLS(t *testing.T, tlsConf TLSConfig) (s *Server, certPem []byte) { + t.Helper() - s.conf.TLSConfig = TLSConfig{ - TLSListenAddr: &net.TCPAddr{Port: 0}, - CertificateChainData: certPem, - PrivateKeyData: keyPem, + var keyPem []byte + _, certPem, keyPem = createServerTLSConfig(t) + + s = createTestServer(t, &dnsfilter.Config{}, ServerConfig{ + UDPListenAddr: &net.UDPAddr{}, + TCPListenAddr: &net.TCPAddr{}, + }) + + tlsConf.CertificateChainData, tlsConf.PrivateKeyData = certPem, keyPem + s.conf.TLSConfig = tlsConf + + err := s.Prepare(nil) + assert.Nilf(t, err, "failed to prepare server: %s", err) + + return s, certPem +} + +func TestDoTServer(t *testing.T) { + s, certPem := createTestTLS(t, TLSConfig{ + TLSListenAddr: &net.TCPAddr{}, + }) + s.conf.UpstreamConfig.Upstreams = []upstream.Upstream{ + &aghtest.TestUpstream{ + IPv4: map[string][]net.IP{ + "google-public-dns-a.google.com.": {{8, 8, 8, 8}}, + }, + }, } + startDeferStop(t, s) - _ = s.Prepare(nil) - // Starting the server - err := s.Start() - if err != nil { - t.Fatalf("Failed to start server: %s", err) - } - - // Add our self-signed generated config to roots + // Add our self-signed generated config to roots. roots := x509.NewCertPool() roots.AppendCertsFromPEM(certPem) tlsConfig := &tls.Config{ @@ -122,277 +153,222 @@ func TestDotServer(t *testing.T) { MinVersion: tls.VersionTLS12, } - // Create a DNS-over-TLS client connection + // Create a DNS-over-TLS client connection. addr := s.dnsProxy.Addr(proxy.ProtoTLS) conn, err := dns.DialWithTLS("tcp-tls", addr.String(), tlsConfig) - if err != nil { - t.Fatalf("cannot connect to the proxy: %s", err) - } + assert.Nilf(t, err, "cannot connect to the proxy: %s", err) sendTestMessages(t, conn) - - // Stop the proxy - err = s.Stop() - if err != nil { - t.Fatalf("DNS server failed to stop: %s", err) - } } -func TestDoqServer(t *testing.T) { - // Prepare the proxy server - _, certPem, keyPem := createServerTLSConfig(t) - s := createTestServer(t) - - s.conf.TLSConfig = TLSConfig{ - QUICListenAddr: &net.UDPAddr{Port: 0}, - CertificateChainData: certPem, - PrivateKeyData: keyPem, +func TestDoQServer(t *testing.T) { + s, _ := createTestTLS(t, TLSConfig{ + QUICListenAddr: &net.UDPAddr{}, + }) + s.conf.UpstreamConfig.Upstreams = []upstream.Upstream{ + &aghtest.TestUpstream{ + IPv4: map[string][]net.IP{ + "google-public-dns-a.google.com.": {{8, 8, 8, 8}}, + }, + }, } + startDeferStop(t, s) - _ = s.Prepare(nil) - // Starting the server - err := s.Start() - assert.Nil(t, err) - - // Create a DNS-over-QUIC upstream + // Create a DNS-over-QUIC upstream. addr := s.dnsProxy.Addr(proxy.ProtoQUIC) opts := upstream.Options{InsecureSkipVerify: true} - u, err := upstream.AddressToUpstream(fmt.Sprintf("quic://%s", addr), opts) + u, err := upstream.AddressToUpstream(fmt.Sprintf("%s://%s", proxy.ProtoQUIC, addr), opts) assert.Nil(t, err) - // Send the test message + // Send the test message. req := createGoogleATestMessage() res, err := u.Exchange(req) assert.Nil(t, err) - assertGoogleAResponse(t, res) - // Stop the proxy - err = s.Stop() - if err != nil { - t.Fatalf("DNS server failed to stop: %s", err) - } + assertGoogleAResponse(t, res) } func TestServerRace(t *testing.T) { - s := createTestServer(t) - err := s.Start() - if err != nil { - t.Fatalf("Failed to start server: %s", err) - } + t.Skip("TODO(e.burkov): inspect the golibs/cache package for locks") - // message over UDP - addr := s.dnsProxy.Addr(proxy.ProtoUDP) - conn, err := dns.Dial("udp", addr.String()) - if err != nil { - t.Fatalf("cannot connect to the proxy: %s", err) + filterConf := &dnsfilter.Config{ + SafeBrowsingEnabled: true, + SafeBrowsingCacheSize: 1000, + SafeSearchEnabled: true, + SafeSearchCacheSize: 1000, + ParentalCacheSize: 1000, + CacheTime: 30, } + forwardConf := ServerConfig{ + UDPListenAddr: &net.UDPAddr{}, + TCPListenAddr: &net.TCPAddr{}, + FilteringConfig: FilteringConfig{ + ProtectionEnabled: true, + UpstreamDNS: []string{"8.8.8.8:53", "8.8.4.4:53"}, + }, + ConfigModified: func() {}, + } + s := createTestServer(t, filterConf, forwardConf) + s.conf.UpstreamConfig.Upstreams = []upstream.Upstream{ + &aghtest.TestUpstream{ + IPv4: map[string][]net.IP{ + "google-public-dns-a.google.com.": {{8, 8, 8, 8}}, + }, + }, + } + startDeferStop(t, s) + + // Message over UDP. + addr := s.dnsProxy.Addr(proxy.ProtoUDP) + conn, err := dns.Dial(proxy.ProtoUDP, addr.String()) + assert.Nilf(t, err, "cannot connect to the proxy: %s", err) sendTestMessagesAsync(t, conn) - - // Stop the proxy - err = s.Stop() - if err != nil { - t.Fatalf("DNS server failed to stop: %s", err) - } } func TestSafeSearch(t *testing.T) { - s := createTestServer(t) - err := s.Start() - if err != nil { - t.Fatalf("Failed to start server: %s", err) + resolver := &aghtest.TestResolver{} + filterConf := &dnsfilter.Config{ + SafeSearchEnabled: true, + SafeSearchCacheSize: 1000, + CacheTime: 30, + CustomResolver: resolver, } - - // Test safe search for yandex. We already know safe search ip - addr := s.dnsProxy.Addr(proxy.ProtoUDP) - client := dns.Client{Net: "udp"} - yandexDomains := []string{"yandex.com.", "yandex.by.", "yandex.kz.", "yandex.ru.", "yandex.com."} - for _, host := range yandexDomains { - exchangeAndAssertResponse(t, &client, addr, host, "213.180.193.56") + forwardConf := ServerConfig{ + UDPListenAddr: &net.UDPAddr{}, + TCPListenAddr: &net.TCPAddr{}, + FilteringConfig: FilteringConfig{ + ProtectionEnabled: true, + }, } + s := createTestServer(t, filterConf, forwardConf) + startDeferStop(t, s) - // Let's lookup for google safesearch ip - ips, err := net.LookupIP("forcesafesearch.google.com") - if err != nil { - t.Fatalf("Failed to lookup for forcesafesearch.google.com: %s", err) - } + addr := s.dnsProxy.Addr(proxy.ProtoUDP).String() + client := dns.Client{Net: proxy.ProtoUDP} - ip := ips[0] - for _, i := range ips { - if i.To4() != nil { - ip = i - break - } - } + yandexIP := net.IP{213, 180, 193, 56} + googleIP, _ := resolver.HostToIPs("forcesafesearch.google.com") - // Test safe search for google. - googleDomains := []string{"www.google.com.", "www.google.com.af.", "www.google.be.", "www.google.by."} - for _, host := range googleDomains { - exchangeAndAssertResponse(t, &client, addr, host, ip.String()) - } + testCases := []struct { + host string + want net.IP + }{{ + host: "yandex.com.", + want: yandexIP, + }, { + host: "yandex.by.", + want: yandexIP, + }, { + host: "yandex.kz.", + want: yandexIP, + }, { + host: "yandex.ru.", + want: yandexIP, + }, { + host: "www.google.com.", + want: googleIP, + }, { + host: "www.google.com.af.", + want: googleIP, + }, { + host: "www.google.be.", + want: googleIP, + }, { + host: "www.google.by.", + want: googleIP, + }} - err = s.Stop() - if err != nil { - t.Fatalf("Can not stopd server cause: %s", err) + for _, tc := range testCases { + t.Run(tc.host, func(t *testing.T) { + req := createTestMessage(tc.host) + reply, _, err := client.Exchange(req, addr) + assert.Nilf(t, err, "couldn't talk to server %s: %s", addr, err) + assertResponse(t, reply, tc.want) + }) } } func TestInvalidRequest(t *testing.T) { - s := createTestServer(t) - err := s.Start() - if err != nil { - t.Fatalf("Failed to start server: %s", err) + s := createTestServer(t, &dnsfilter.Config{}, ServerConfig{ + UDPListenAddr: &net.UDPAddr{}, + TCPListenAddr: &net.TCPAddr{}, + }) + startDeferStop(t, s) + + addr := s.dnsProxy.Addr(proxy.ProtoUDP).String() + req := dns.Msg{ + MsgHdr: dns.MsgHdr{ + Id: dns.Id(), + RecursionDesired: true, + }, } - // server is running, send a message - addr := s.dnsProxy.Addr(proxy.ProtoUDP) - req := dns.Msg{} - req.Id = dns.Id() - req.RecursionDesired = true + // Send a DNS request without question. + _, _, err := (&dns.Client{ + Net: proxy.ProtoUDP, + Timeout: 500 * time.Millisecond, + }).Exchange(&req, addr) - // send a DNS request without question - client := dns.Client{Net: "udp", Timeout: 500 * time.Millisecond} - _, _, err = client.Exchange(&req, addr.String()) - if err != nil { - t.Fatalf("got a response to an invalid query") - } - - err = s.Stop() - if err != nil { - t.Fatalf("DNS server failed to stop: %s", err) - } + assert.Nil(t, err, "got a response to an invalid query") } func TestBlockedRequest(t *testing.T) { - s := createTestServer(t) - err := s.Start() - if err != nil { - t.Fatalf("Failed to start server: %s", err) + forwardConf := ServerConfig{ + UDPListenAddr: &net.UDPAddr{}, + TCPListenAddr: &net.TCPAddr{}, + FilteringConfig: FilteringConfig{ + ProtectionEnabled: true, + }, } + s := createTestServer(t, &dnsfilter.Config{}, forwardConf) + startDeferStop(t, s) + addr := s.dnsProxy.Addr(proxy.ProtoUDP) - // - // Default blocking - NULL IP - // - req := dns.Msg{} - req.Id = dns.Id() - req.RecursionDesired = true - req.Question = []dns.Question{ - {Name: "nxdomain.example.org.", Qtype: dns.TypeA, Qclass: dns.ClassINET}, - } + // Default blocking. + req := createTestMessage("nxdomain.example.org.") - reply, err := dns.Exchange(&req, addr.String()) - if err != nil { - t.Fatalf("Couldn't talk to server %s: %s", addr, err) - } + reply, err := dns.Exchange(req, addr.String()) + assert.Nilf(t, err, "couldn't talk to server %s: %s", addr, err) assert.Equal(t, dns.RcodeSuccess, reply.Rcode) - assert.True(t, reply.Answer[0].(*dns.A).A.Equal(net.ParseIP("0.0.0.0"))) - - err = s.Stop() - if err != nil { - t.Fatalf("DNS server failed to stop: %s", err) - } + assert.True(t, reply.Answer[0].(*dns.A).A.IsUnspecified()) } func TestServerCustomClientUpstream(t *testing.T) { - s := createTestServer(t) - s.conf.GetCustomUpstreamByClient = func(_ string) *proxy.UpstreamConfig { - uc := &proxy.UpstreamConfig{} - u := &testUpstream{} - u.ipv4 = map[string][]net.IP{} - u.ipv4["host."] = []net.IP{net.ParseIP("192.168.0.1")} - uc.Upstreams = append(uc.Upstreams, u) - return uc + forwardConf := ServerConfig{ + UDPListenAddr: &net.UDPAddr{}, + TCPListenAddr: &net.TCPAddr{}, + FilteringConfig: FilteringConfig{ + ProtectionEnabled: true, + }, } - - assert.Nil(t, s.Start()) + s := createTestServer(t, &dnsfilter.Config{}, forwardConf) + s.conf.GetCustomUpstreamByClient = func(_ string) *proxy.UpstreamConfig { + return &proxy.UpstreamConfig{ + Upstreams: []upstream.Upstream{ + &aghtest.TestUpstream{ + IPv4: map[string][]net.IP{ + "host.": {{192, 168, 0, 1}}, + }, + }, + }, + } + } + startDeferStop(t, s) addr := s.dnsProxy.Addr(proxy.ProtoUDP) - // Send test request - req := dns.Msg{} - req.Id = dns.Id() - req.RecursionDesired = true - req.Question = []dns.Question{ - {Name: "host.", Qtype: dns.TypeA, Qclass: dns.ClassINET}, - } + // Send test request. + req := createTestMessage("host.") - reply, err := dns.Exchange(&req, addr.String()) + reply, err := dns.Exchange(req, addr.String()) assert.Nil(t, err) assert.Equal(t, dns.RcodeSuccess, reply.Rcode) - assert.NotNil(t, reply.Answer) - assert.Equal(t, "192.168.0.1", reply.Answer[0].(*dns.A).A.String()) - assert.Nil(t, s.Stop()) -} + assert.NotEmpty(t, reply.Answer) -// testUpstream is a mock of real upstream. -// specify fields with necessary values to simulate real upstream behaviour -type testUpstream struct { - cn map[string]string // Map of [name]canonical_name - ipv4 map[string][]net.IP // Map of [name]IPv4 - ipv6 map[string][]net.IP // Map of [name]IPv6 -} - -func (u *testUpstream) Exchange(m *dns.Msg) (*dns.Msg, error) { - resp := dns.Msg{} - resp.SetReply(m) - hasARecord := false - hasAAAARecord := false - - reqType := m.Question[0].Qtype - name := m.Question[0].Name - - // Let's check if we have any CNAME for given name - if cname, ok := u.cn[name]; ok { - cn := dns.CNAME{} - cn.Hdr.Name = name - cn.Hdr.Rrtype = dns.TypeCNAME - cn.Target = cname - resp.Answer = append(resp.Answer, &cn) - } - - // Let's check if we can add some A records to the answer - if ipv4addr, ok := u.ipv4[name]; ok && reqType == dns.TypeA { - hasARecord = true - for _, ipv4 := range ipv4addr { - respA := dns.A{} - respA.Hdr.Rrtype = dns.TypeA - respA.Hdr.Name = name - respA.A = ipv4 - resp.Answer = append(resp.Answer, &respA) - } - } - - // Let's check if we can add some AAAA records to the answer - if u.ipv6 != nil { - if ipv6addr, ok := u.ipv6[name]; ok && reqType == dns.TypeAAAA { - hasAAAARecord = true - for _, ipv6 := range ipv6addr { - respAAAA := dns.A{} - respAAAA.Hdr.Rrtype = dns.TypeAAAA - respAAAA.Hdr.Name = name - respAAAA.A = ipv6 - resp.Answer = append(resp.Answer, &respAAAA) - } - } - } - - if len(resp.Answer) == 0 { - if hasARecord || hasAAAARecord { - // Set No Error RCode if there are some records for given Qname but we didn't apply them - resp.SetRcode(m, dns.RcodeSuccess) - } else { - // Set NXDomain RCode otherwise - resp.SetRcode(m, dns.RcodeNameError) - } - } - - return &resp, nil -} - -func (u *testUpstream) Address() string { - return "test" + assert.Equal(t, net.IP{192, 168, 0, 1}, reply.Answer[0].(*dns.A).A) } func (s *Server) startWithUpstream(u upstream.Upstream) error { @@ -402,34 +378,43 @@ func (s *Server) startWithUpstream(u upstream.Upstream) error { if err != nil { return err } + s.dnsProxy.UpstreamConfig = &proxy.UpstreamConfig{ Upstreams: []upstream.Upstream{u}, } + return s.dnsProxy.Start() } -// testCNAMEs is a simple map of names and CNAMEs necessary for the testUpstream work +// testCNAMEs is a map of names and CNAMEs necessary for the TestUpstream work. var testCNAMEs = map[string]string{ "badhost.": "null.example.org.", "whitelist.example.org.": "null.example.org.", } -// testIPv4 is a simple map of names and IPv4s necessary for the testUpstream work +// testIPv4 is a map of names and IPv4s necessary for the TestUpstream work. var testIPv4 = map[string][]net.IP{ "null.example.org.": {{1, 2, 3, 4}}, "example.org.": {{127, 0, 0, 255}}, } func TestBlockCNAMEProtectionEnabled(t *testing.T) { - s := createTestServer(t) - testUpstm := &testUpstream{testCNAMEs, testIPv4, nil} + s := createTestServer(t, &dnsfilter.Config{}, ServerConfig{ + UDPListenAddr: &net.UDPAddr{}, + TCPListenAddr: &net.TCPAddr{}, + }) + testUpstm := &aghtest.TestUpstream{ + CName: testCNAMEs, + IPv4: testIPv4, + IPv6: nil, + } s.conf.ProtectionEnabled = false err := s.startWithUpstream(testUpstm) - assert.True(t, err == nil) + assert.Nil(t, err) addr := s.dnsProxy.Addr(proxy.ProtoUDP) - // 'badhost' has a canonical name 'null.example.org' which is blocked by filters: - // but protection is disabled - response is NOT blocked + // 'badhost' has a canonical name 'null.example.org' which is blocked by + // filters: but protection is disabled so response is _not_ blocked. req := createTestMessage("badhost.") reply, err := dns.Exchange(req, addr.String()) assert.Nil(t, err) @@ -437,308 +422,318 @@ func TestBlockCNAMEProtectionEnabled(t *testing.T) { } func TestBlockCNAME(t *testing.T) { - s := createTestServer(t) - testUpstm := &testUpstream{testCNAMEs, testIPv4, nil} - err := s.startWithUpstream(testUpstm) - assert.True(t, err == nil) - addr := s.dnsProxy.Addr(proxy.ProtoUDP) + forwardConf := ServerConfig{ + UDPListenAddr: &net.UDPAddr{}, + TCPListenAddr: &net.TCPAddr{}, + FilteringConfig: FilteringConfig{ + ProtectionEnabled: true, + }, + } + s := createTestServer(t, &dnsfilter.Config{}, forwardConf) + s.conf.UpstreamConfig.Upstreams = []upstream.Upstream{ + &aghtest.TestUpstream{ + CName: testCNAMEs, + IPv4: testIPv4, + }, + } + startDeferStop(t, s) - // 'badhost' has a canonical name 'null.example.org' which is blocked by filters: - // response is blocked - req := createTestMessage("badhost.") - reply, err := dns.Exchange(req, addr.String()) - assert.Nil(t, err, nil) - assert.Equal(t, dns.RcodeSuccess, reply.Rcode) - assert.True(t, reply.Answer[0].(*dns.A).A.Equal(net.ParseIP("0.0.0.0"))) + addr := s.dnsProxy.Addr(proxy.ProtoUDP).String() - // 'whitelist.example.org' has a canonical name 'null.example.org' which is blocked by filters - // but 'whitelist.example.org' is in a whitelist: - // response isn't blocked - req = createTestMessage("whitelist.example.org.") - reply, err = dns.Exchange(req, addr.String()) - assert.Nil(t, err) - assert.Equal(t, dns.RcodeSuccess, reply.Rcode) + testCases := []struct { + host string + want bool + }{{ + host: "badhost.", + // 'badhost' has a canonical name 'null.example.org' which is + // blocked by filters: response is blocked. + want: true, + }, { + host: "whitelist.example.org.", + // 'whitelist.example.org' has a canonical name + // 'null.example.org' which is blocked by filters + // but 'whitelist.example.org' is in a whitelist: + // response isn't blocked. + want: false, + }, { + host: "example.org.", + // 'example.org' has a canonical name 'cname1' with IP + // 127.0.0.255 which is blocked by filters: response is blocked. + want: true, + }} - // 'example.org' has a canonical name 'cname1' with IP 127.0.0.255 which is blocked by filters: - // response is blocked - req = createTestMessage("example.org.") - reply, err = dns.Exchange(req, addr.String()) - assert.Nil(t, err) - assert.Equal(t, dns.RcodeSuccess, reply.Rcode) - assert.True(t, reply.Answer[0].(*dns.A).A.Equal(net.ParseIP("0.0.0.0"))) - - _ = s.Stop() + for _, tc := range testCases { + t.Run("block_cname_"+tc.host, func(t *testing.T) { + req := createTestMessage(tc.host) + reply, err := dns.Exchange(req, addr) + assert.Nil(t, err) + assert.Equal(t, dns.RcodeSuccess, reply.Rcode) + if tc.want { + assert.True(t, reply.Answer[0].(*dns.A).A.IsUnspecified()) + } + }) + } } func TestClientRulesForCNAMEMatching(t *testing.T) { - s := createTestServer(t) - testUpstm := &testUpstream{testCNAMEs, testIPv4, nil} - s.conf.FilterHandler = func(_ string, settings *dnsfilter.RequestFilteringSettings) { - settings.FilteringEnabled = false + forwardConf := ServerConfig{ + UDPListenAddr: &net.UDPAddr{}, + TCPListenAddr: &net.TCPAddr{}, + FilteringConfig: FilteringConfig{ + ProtectionEnabled: true, + FilterHandler: func(_ net.IP, _ string, settings *dnsfilter.RequestFilteringSettings) { + settings.FilteringEnabled = false + }, + }, } - err := s.startWithUpstream(testUpstm) - assert.Nil(t, err) + s := createTestServer(t, &dnsfilter.Config{}, forwardConf) + s.conf.UpstreamConfig.Upstreams = []upstream.Upstream{ + &aghtest.TestUpstream{ + CName: testCNAMEs, + IPv4: testIPv4, + }, + } + startDeferStop(t, s) + addr := s.dnsProxy.Addr(proxy.ProtoUDP) - // 'badhost' has a canonical name 'null.example.org' which is blocked by filters: - // response is blocked - req := dns.Msg{} - req.Id = dns.Id() - req.Question = []dns.Question{ - {Name: "badhost.", Qtype: dns.TypeA, Qclass: dns.ClassINET}, + // 'badhost' has a canonical name 'null.example.org' which is blocked by + // filters: response is blocked. + req := dns.Msg{ + MsgHdr: dns.MsgHdr{ + Id: dns.Id(), + }, + Question: []dns.Question{{ + Name: "badhost.", + Qtype: dns.TypeA, + Qclass: dns.ClassINET, + }}, } - // However, in our case it should not be blocked - // as filtering is disabled on the client level + + // However, in our case it should not be blocked as filtering is + // disabled on the client level. reply, err := dns.Exchange(&req, addr.String()) assert.Nil(t, err) assert.Equal(t, dns.RcodeSuccess, reply.Rcode) } func TestNullBlockedRequest(t *testing.T) { - s := createTestServer(t) - s.conf.FilteringConfig.BlockingMode = "null_ip" - err := s.Start() - if err != nil { - t.Fatalf("Failed to start server: %s", err) + forwardConf := ServerConfig{ + UDPListenAddr: &net.UDPAddr{}, + TCPListenAddr: &net.TCPAddr{}, + FilteringConfig: FilteringConfig{ + ProtectionEnabled: true, + BlockingMode: "null_ip", + }, } + s := createTestServer(t, &dnsfilter.Config{}, forwardConf) + startDeferStop(t, s) addr := s.dnsProxy.Addr(proxy.ProtoUDP) - // - // Null filter blocking - // - req := dns.Msg{} - req.Id = dns.Id() - req.RecursionDesired = true - req.Question = []dns.Question{ - {Name: "null.example.org.", Qtype: dns.TypeA, Qclass: dns.ClassINET}, + // Nil filter blocking. + req := dns.Msg{ + MsgHdr: dns.MsgHdr{ + Id: dns.Id(), + RecursionDesired: true, + }, + Question: []dns.Question{{ + Name: "null.example.org.", + Qtype: dns.TypeA, + Qclass: dns.ClassINET, + }}, } reply, err := dns.Exchange(&req, addr.String()) - if err != nil { - t.Fatalf("Couldn't talk to server %s: %s", addr, err) - } - if len(reply.Answer) != 1 { - t.Fatalf("DNS server %s returned reply with wrong number of answers - %d", addr, len(reply.Answer)) - } - if a, ok := reply.Answer[0].(*dns.A); ok { - if !net.IPv4zero.Equal(a.A) { - t.Fatalf("DNS server %s returned wrong answer instead of 0.0.0.0: %v", addr, a.A) - } - } else { - t.Fatalf("DNS server %s returned wrong answer type instead of A: %v", addr, reply.Answer[0]) - } - - err = s.Stop() - if err != nil { - t.Fatalf("DNS server failed to stop: %s", err) - } + assert.Nilf(t, err, "couldn't talk to server %s: %s", addr, err) + assert.Lenf(t, reply.Answer, 1, "dns server %s returned reply with wrong number of answers - %d", addr, len(reply.Answer)) + a, ok := reply.Answer[0].(*dns.A) + assert.Truef(t, ok, "dns server %s returned wrong answer type instead of A: %v", addr, reply.Answer[0]) + assert.Truef(t, a.A.IsUnspecified(), "dns server %s returned wrong answer instead of 0.0.0.0: %v", addr, a.A) } func TestBlockedCustomIP(t *testing.T) { rules := "||nxdomain.example.org^\n||null.example.org^\n127.0.0.1 host.example.org\n@@||whitelist.example.org^\n||127.0.0.255\n" filters := []dnsfilter.Filter{{ - ID: 0, Data: []byte(rules), + ID: 0, + Data: []byte(rules), }} - c := dnsfilter.Config{} - f := dnsfilter.New(&c, filters) - s := NewServer(DNSCreateParams{DNSFilter: f}) - conf := ServerConfig{} - conf.UDPListenAddr = &net.UDPAddr{Port: 0} - conf.TCPListenAddr = &net.TCPAddr{Port: 0} - conf.ProtectionEnabled = true - conf.BlockingMode = "custom_ip" - conf.BlockingIPv4 = "bad IP" - conf.UpstreamDNS = []string{"8.8.8.8:53", "8.8.4.4:53"} - err := s.Prepare(&conf) - assert.True(t, err != nil) // invalid BlockingIPv4 + s := NewServer(DNSCreateParams{ + DNSFilter: dnsfilter.New(&dnsfilter.Config{}, filters), + }) + conf := ServerConfig{ + UDPListenAddr: &net.UDPAddr{}, + TCPListenAddr: &net.TCPAddr{}, + FilteringConfig: FilteringConfig{ + ProtectionEnabled: true, + BlockingMode: "custom_ip", + BlockingIPv4: nil, + UpstreamDNS: []string{"8.8.8.8:53", "8.8.4.4:53"}, + }, + } + // Invalid BlockingIPv4. + assert.NotNil(t, s.Prepare(&conf)) - conf.BlockingIPv4 = "0.0.0.1" - conf.BlockingIPv6 = "::1" - err = s.Prepare(&conf) - assert.Nil(t, err) - err = s.Start() - assert.Nil(t, err) + conf.BlockingIPv4 = net.IP{0, 0, 0, 1} + conf.BlockingIPv6 = net.ParseIP("::1") + assert.Nil(t, s.Prepare(&conf)) + + startDeferStop(t, s) addr := s.dnsProxy.Addr(proxy.ProtoUDP) req := createTestMessageWithType("null.example.org.", dns.TypeA) reply, err := dns.Exchange(req, addr.String()) assert.Nil(t, err) - assert.Equal(t, 1, len(reply.Answer)) + assert.Len(t, reply.Answer, 1) a, ok := reply.Answer[0].(*dns.A) assert.True(t, ok) - assert.Equal(t, "0.0.0.1", a.A.String()) + assert.True(t, net.IP{0, 0, 0, 1}.Equal(a.A)) req = createTestMessageWithType("null.example.org.", dns.TypeAAAA) reply, err = dns.Exchange(req, addr.String()) assert.Nil(t, err) - assert.Equal(t, 1, len(reply.Answer)) + assert.Len(t, reply.Answer, 1) a6, ok := reply.Answer[0].(*dns.AAAA) assert.True(t, ok) assert.Equal(t, "::1", a6.AAAA.String()) - - err = s.Stop() - if err != nil { - t.Fatalf("DNS server failed to stop: %s", err) - } } func TestBlockedByHosts(t *testing.T) { - s := createTestServer(t) - err := s.Start() - if err != nil { - t.Fatalf("Failed to start server: %s", err) + forwardConf := ServerConfig{ + UDPListenAddr: &net.UDPAddr{}, + TCPListenAddr: &net.TCPAddr{}, + FilteringConfig: FilteringConfig{ + ProtectionEnabled: true, + }, } + s := createTestServer(t, &dnsfilter.Config{}, forwardConf) + startDeferStop(t, s) addr := s.dnsProxy.Addr(proxy.ProtoUDP) - // - // Hosts blocking - // - req := dns.Msg{} - req.Id = dns.Id() - req.RecursionDesired = true - req.Question = []dns.Question{ - {Name: "host.example.org.", Qtype: dns.TypeA, Qclass: dns.ClassINET}, - } + // Hosts blocking. + req := createTestMessage("host.example.org.") - reply, err := dns.Exchange(&req, addr.String()) - if err != nil { - t.Fatalf("Couldn't talk to server %s: %s", addr, err) - } - if len(reply.Answer) != 1 { - t.Fatalf("DNS server %s returned reply with wrong number of answers - %d", addr, len(reply.Answer)) - } - if a, ok := reply.Answer[0].(*dns.A); ok { - if !net.IPv4(127, 0, 0, 1).Equal(a.A) { - t.Fatalf("DNS server %s returned wrong answer instead of 8.8.8.8: %v", addr, a.A) - } - } else { - t.Fatalf("DNS server %s returned wrong answer type instead of A: %v", addr, reply.Answer[0]) - } + reply, err := dns.Exchange(req, addr.String()) + assert.Nilf(t, err, "couldn't talk to server %s: %s", addr, err) + assert.Lenf(t, reply.Answer, 1, "dns server %s returned reply with wrong number of answers - %d", addr, len(reply.Answer)) - err = s.Stop() - if err != nil { - t.Fatalf("DNS server failed to stop: %s", err) - } + a, ok := reply.Answer[0].(*dns.A) + assert.Truef(t, ok, "dns server %s returned wrong answer type instead of A: %v", addr, reply.Answer[0]) + assert.Equalf(t, net.IP{127, 0, 0, 1}, a.A, "dns server %s returned wrong answer instead of 8.8.8.8: %v", addr, a.A) } func TestBlockedBySafeBrowsing(t *testing.T) { - s := createTestServer(t) - err := s.Start() - if err != nil { - t.Fatalf("Failed to start server: %s", err) + const hostname = "wmconvirus.narod.ru" + + sbUps := &aghtest.TestBlockUpstream{ + Hostname: hostname, + Block: true, } + ans, _ := (&aghtest.TestResolver{}).HostToIPs(hostname) + + filterConf := &dnsfilter.Config{ + SafeBrowsingEnabled: true, + } + forwardConf := ServerConfig{ + UDPListenAddr: &net.UDPAddr{}, + TCPListenAddr: &net.TCPAddr{}, + FilteringConfig: FilteringConfig{ + SafeBrowsingBlockHost: ans.String(), + ProtectionEnabled: true, + }, + } + s := createTestServer(t, filterConf, forwardConf) + s.dnsFilter.SetSafeBrowsingUpstream(sbUps) + startDeferStop(t, s) addr := s.dnsProxy.Addr(proxy.ProtoUDP) - // - // Safebrowsing blocking - // - req := dns.Msg{} - req.Id = dns.Id() - req.RecursionDesired = true - req.Question = []dns.Question{ - {Name: "wmconvirus.narod.ru.", Qtype: dns.TypeA, Qclass: dns.ClassINET}, - } - reply, err := dns.Exchange(&req, addr.String()) - if err != nil { - t.Fatalf("Couldn't talk to server %s: %s", addr, err) - } - if len(reply.Answer) != 1 { - t.Fatalf("DNS server %s returned reply with wrong number of answers - %d", addr, len(reply.Answer)) - } - if a, ok := reply.Answer[0].(*dns.A); ok { - addrs, lookupErr := net.LookupHost(safeBrowsingBlockHost) - if lookupErr != nil { - t.Fatalf("cannot resolve %s due to %s", safeBrowsingBlockHost, lookupErr) - } + // SafeBrowsing blocking. + req := createTestMessage(hostname + ".") - found := false - for _, blockAddr := range addrs { - if blockAddr == a.A.String() { - found = true - } - } + reply, err := dns.Exchange(req, addr.String()) + assert.Nilf(t, err, "couldn't talk to server %s: %s", addr, err) + assert.Lenf(t, reply.Answer, 1, "dns server %s returned reply with wrong number of answers - %d", addr, len(reply.Answer)) - if !found { - t.Fatalf("DNS server %s returned wrong answer: %v", addr, a.A) - } - } else { - t.Fatalf("DNS server %s returned wrong answer type instead of A: %v", addr, reply.Answer[0]) - } - - err = s.Stop() - if err != nil { - t.Fatalf("DNS server failed to stop: %s", err) + a, ok := reply.Answer[0].(*dns.A) + if assert.Truef(t, ok, "dns server %s returned wrong answer type instead of A: %v", addr, reply.Answer[0]) { + assert.Equal(t, ans, a.A, "dns server %s returned wrong answer: %v", addr, a.A) } } func TestRewrite(t *testing.T) { - c := dnsfilter.Config{} - c.Rewrites = []dnsfilter.RewriteEntry{ - { + c := &dnsfilter.Config{ + Rewrites: []dnsfilter.RewriteEntry{{ Domain: "test.com", Answer: "1.2.3.4", Type: dns.TypeA, - }, - { + }, { Domain: "alias.test.com", Answer: "test.com", Type: dns.TypeCNAME, - }, - { + }, { Domain: "my.alias.example.org", Answer: "example.org", Type: dns.TypeCNAME, + }}, + } + f := dnsfilter.New(c, nil) + + s := NewServer(DNSCreateParams{DNSFilter: f}) + err := s.Prepare(&ServerConfig{ + UDPListenAddr: &net.UDPAddr{}, + TCPListenAddr: &net.TCPAddr{}, + FilteringConfig: FilteringConfig{ + ProtectionEnabled: true, + UpstreamDNS: []string{"8.8.8.8:53"}, + }, + }) + s.conf.UpstreamConfig.Upstreams = []upstream.Upstream{ + &aghtest.TestUpstream{ + CName: map[string]string{ + "example.org": "somename", + }, + IPv4: map[string][]net.IP{ + "example.org.": {{4, 3, 2, 1}}, + }, }, } - - f := dnsfilter.New(&c, nil) - s := NewServer(DNSCreateParams{DNSFilter: f}) - conf := ServerConfig{} - conf.UDPListenAddr = &net.UDPAddr{Port: 0} - conf.TCPListenAddr = &net.TCPAddr{Port: 0} - conf.ProtectionEnabled = true - conf.UpstreamDNS = []string{"8.8.8.8:53"} - - err := s.Prepare(&conf) - assert.Nil(t, err) - err = s.Start() assert.Nil(t, err) + startDeferStop(t, s) + addr := s.dnsProxy.Addr(proxy.ProtoUDP) req := createTestMessageWithType("test.com.", dns.TypeA) reply, err := dns.Exchange(req, addr.String()) assert.Nil(t, err) - assert.Equal(t, 1, len(reply.Answer)) + assert.Len(t, reply.Answer, 1) a, ok := reply.Answer[0].(*dns.A) assert.True(t, ok) - assert.Equal(t, "1.2.3.4", a.A.String()) + assert.True(t, net.IP{1, 2, 3, 4}.Equal(a.A)) req = createTestMessageWithType("test.com.", dns.TypeAAAA) reply, err = dns.Exchange(req, addr.String()) assert.Nil(t, err) - assert.Equal(t, 0, len(reply.Answer)) + assert.Empty(t, reply.Answer) req = createTestMessageWithType("alias.test.com.", dns.TypeA) reply, err = dns.Exchange(req, addr.String()) assert.Nil(t, err) - assert.Equal(t, 2, len(reply.Answer)) + assert.Len(t, reply.Answer, 2) assert.Equal(t, "test.com.", reply.Answer[0].(*dns.CNAME).Target) - assert.Equal(t, "1.2.3.4", reply.Answer[1].(*dns.A).A.String()) + assert.True(t, net.IP{1, 2, 3, 4}.Equal(reply.Answer[1].(*dns.A).A)) req = createTestMessageWithType("my.alias.example.org.", dns.TypeA) reply, err = dns.Exchange(req, addr.String()) assert.Nil(t, err) - assert.Equal(t, "my.alias.example.org.", reply.Question[0].Name) // the original question is restored - assert.Equal(t, 2, len(reply.Answer)) + // The original question is restored. + assert.Equal(t, "my.alias.example.org.", reply.Question[0].Name) + assert.Len(t, reply.Answer, 2) assert.Equal(t, "example.org.", reply.Answer[0].(*dns.CNAME).Target) assert.Equal(t, dns.TypeA, reply.Answer[1].Header().Rrtype) - - _ = s.Stop() } -func createTestServer(t *testing.T) *Server { +func createTestServer(t *testing.T, filterConf *dnsfilter.Config, forwardConf ServerConfig) *Server { rules := `||nxdomain.example.org ||null.example.org^ 127.0.0.1 host.example.org @@ -747,39 +742,25 @@ func createTestServer(t *testing.T) *Server { filters := []dnsfilter.Filter{{ ID: 0, Data: []byte(rules), }} - c := dnsfilter.Config{} - c.SafeBrowsingEnabled = true - c.SafeBrowsingCacheSize = 1000 - c.SafeSearchEnabled = true - c.SafeSearchCacheSize = 1000 - c.ParentalCacheSize = 1000 - c.CacheTime = 30 - f := dnsfilter.New(&c, filters) + f := dnsfilter.New(filterConf, filters) s := NewServer(DNSCreateParams{DNSFilter: f}) - s.conf.UDPListenAddr = &net.UDPAddr{Port: 0} - s.conf.TCPListenAddr = &net.TCPAddr{Port: 0} - s.conf.UpstreamDNS = []string{"8.8.8.8:53", "8.8.4.4:53"} - s.conf.FilteringConfig.ProtectionEnabled = true - s.conf.ConfigModified = func() {} + s.conf = forwardConf + assert.Nil(t, s.Prepare(nil)) - err := s.Prepare(nil) - assert.True(t, err == nil) return s } func createServerTLSConfig(t *testing.T) (*tls.Config, []byte, []byte) { + t.Helper() + privateKey, err := rsa.GenerateKey(rand.Reader, 2048) - if err != nil { - t.Fatalf("cannot generate RSA key: %s", err) - } + assert.Nilf(t, err, "cannot generate RSA key: %s", err) serialNumberLimit := new(big.Int).Lsh(big.NewInt(1), 128) serialNumber, err := rand.Int(rand.Reader, serialNumberLimit) - if err != nil { - t.Fatalf("failed to generate serial number: %s", err) - } + assert.Nilf(t, err, "failed to generate serial number: %s", err) notBefore := time.Now() notAfter := notBefore.Add(5 * 365 * time.Hour * 24) @@ -800,77 +781,58 @@ func createServerTLSConfig(t *testing.T) (*tls.Config, []byte, []byte) { template.DNSNames = append(template.DNSNames, tlsServerName) derBytes, err := x509.CreateCertificate(rand.Reader, &template, &template, publicKey(privateKey), privateKey) - if err != nil { - t.Fatalf("failed to create certificate: %s", err) - } + assert.Nilf(t, err, "failed to create certificate: %s", err) certPem := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: derBytes}) keyPem := pem.EncodeToMemory(&pem.Block{Type: "RSA PRIVATE KEY", Bytes: x509.MarshalPKCS1PrivateKey(privateKey)}) cert, err := tls.X509KeyPair(certPem, keyPem) - if err != nil { - t.Fatalf("failed to create certificate: %s", err) - } + assert.Nilf(t, err, "failed to create certificate: %s", err) - return &tls.Config{Certificates: []tls.Certificate{cert}, ServerName: tlsServerName, MinVersion: tls.VersionTLS12}, certPem, keyPem + return &tls.Config{ + Certificates: []tls.Certificate{cert}, + ServerName: tlsServerName, + MinVersion: tls.VersionTLS12, + }, certPem, keyPem } -func sendTestMessageAsync(t *testing.T, conn *dns.Conn, g *sync.WaitGroup) { - defer func() { - g.Done() - }() - - req := createGoogleATestMessage() - err := conn.WriteMsg(req) - if err != nil { - panic(fmt.Sprintf("cannot write message: %s", err)) - } - - res, err := conn.ReadMsg() - if err != nil { - panic(fmt.Sprintf("cannot read response to message: %s", err)) - } - assertGoogleAResponse(t, res) -} - -// sendTestMessagesAsync sends messages in parallel -// so that we could find race issues +// sendTestMessagesAsync sends messages in parallel to check for race issues. +//lint:ignore U1000 it's called from the function which is skipped for now. func sendTestMessagesAsync(t *testing.T, conn *dns.Conn) { - g := &sync.WaitGroup{} - g.Add(testMessagesCount) + wg := &sync.WaitGroup{} for i := 0; i < testMessagesCount; i++ { - go sendTestMessageAsync(t, conn, g) + msg := createGoogleATestMessage() + wg.Add(1) + + go func() { + defer wg.Done() + + err := conn.WriteMsg(msg) + assert.Nilf(t, err, "cannot write message: %s", err) + + res, err := conn.ReadMsg() + assert.Nilf(t, err, "cannot read response to message: %s", err) + + assertGoogleAResponse(t, res) + }() } - g.Wait() + wg.Wait() } func sendTestMessages(t *testing.T, conn *dns.Conn) { - for i := 0; i < 10; i++ { - req := createGoogleATestMessage() - err := conn.WriteMsg(req) - if err != nil { - t.Fatalf("cannot write message #%d: %s", i, err) - } - - res, err := conn.ReadMsg() - if err != nil { - t.Fatalf("cannot read response to message #%d: %s", i, err) - } - assertGoogleAResponse(t, res) - } -} - -func exchangeAndAssertResponse(t *testing.T, client *dns.Client, addr net.Addr, host, ip string) { t.Helper() - req := createTestMessage(host) - reply, _, err := client.Exchange(req, addr.String()) - if err != nil { - t.Fatalf("Couldn't talk to server %s: %s", addr, err) + for i := 0; i < testMessagesCount; i++ { + req := createGoogleATestMessage() + err := conn.WriteMsg(req) + assert.Nilf(t, err, "cannot write message #%d: %s", i, err) + + res, err := conn.ReadMsg() + assert.Nilf(t, err, "cannot read response to message #%d: %s", i, err) + assertGoogleAResponse(t, res) } - assertResponse(t, reply, ip) } func createGoogleATestMessage() *dns.Msg { @@ -878,41 +840,40 @@ func createGoogleATestMessage() *dns.Msg { } func createTestMessage(host string) *dns.Msg { - req := dns.Msg{} - req.Id = dns.Id() - req.RecursionDesired = true - req.Question = []dns.Question{ - {Name: host, Qtype: dns.TypeA, Qclass: dns.ClassINET}, + return &dns.Msg{ + MsgHdr: dns.MsgHdr{ + Id: dns.Id(), + RecursionDesired: true, + }, + Question: []dns.Question{{ + Name: host, + Qtype: dns.TypeA, + Qclass: dns.ClassINET, + }}, } - return &req } func createTestMessageWithType(host string, qtype uint16) *dns.Msg { - req := dns.Msg{} - req.Id = dns.Id() - req.RecursionDesired = true - req.Question = []dns.Question{ - {Name: host, Qtype: qtype, Qclass: dns.ClassINET}, - } - return &req + req := createTestMessage(host) + req.Question[0].Qtype = qtype + + return req } func assertGoogleAResponse(t *testing.T, reply *dns.Msg) { - assertResponse(t, reply, "8.8.8.8") + assertResponse(t, reply, net.IP{8, 8, 8, 8}) } -func assertResponse(t *testing.T, reply *dns.Msg, ip string) { +func assertResponse(t *testing.T, reply *dns.Msg, ip net.IP) { t.Helper() - if len(reply.Answer) != 1 { - t.Fatalf("DNS server returned reply with wrong number of answers - %d", len(reply.Answer)) + if !assert.Lenf(t, reply.Answer, 1, "dns server returned reply with wrong number of answers - %d", len(reply.Answer)) { + return } - if a, ok := reply.Answer[0].(*dns.A); ok { - if !net.ParseIP(ip).Equal(a.A) { - t.Fatalf("DNS server returned wrong answer instead of %s: %v", ip, a.A) - } - } else { - t.Fatalf("DNS server returned wrong answer type instead of A: %v", reply.Answer[0]) + + a, ok := reply.Answer[0].(*dns.A) + if assert.Truef(t, ok, "dns server returned wrong answer type instead of A: %v", reply.Answer[0]) { + assert.Truef(t, a.A.Equal(ip), "dns server returned wrong answer instead of %s: %s", ip, a.A) } } @@ -920,76 +881,109 @@ func publicKey(priv interface{}) interface{} { switch k := priv.(type) { case *rsa.PrivateKey: return &k.PublicKey + case *ecdsa.PrivateKey: return &k.PublicKey + default: return nil } } func TestValidateUpstream(t *testing.T) { - invalidUpstreams := []string{ - "1.2.3.4.5", - "123.3.7m", - "htttps://google.com/dns-query", - "[/host.com]tls://dns.adguard.com", - "[host.ru]#", - } + testCases := []struct { + name string + upstream string + valid bool + wantDef bool + }{{ + name: "invalid", + upstream: "1.2.3.4.5", + valid: false, + }, { + name: "invalid", + upstream: "123.3.7m", + valid: false, + }, { + name: "invalid", + upstream: "htttps://google.com/dns-query", + valid: false, + }, { + name: "invalid", + upstream: "[/host.com]tls://dns.adguard.com", + valid: false, + }, { + name: "invalid", + upstream: "[host.ru]#", + valid: false, + }, { + name: "valid_default", + upstream: "1.1.1.1", + valid: true, + wantDef: true, + }, { + name: "valid_default", + upstream: "tls://1.1.1.1", + valid: true, + wantDef: true, + }, { + name: "valid_default", + upstream: "https://dns.adguard.com/dns-query", + valid: true, + wantDef: true, + }, { + name: "valid_default", + upstream: "sdns://AQMAAAAAAAAAFDE3Ni4xMDMuMTMwLjEzMDo1NDQzINErR_JS3PLCu_iZEIbq95zkSV2LFsigxDIuUso_OQhzIjIuZG5zY3J5cHQuZGVmYXVsdC5uczEuYWRndWFyZC5jb20", + valid: true, + wantDef: true, + }, { + name: "valid", + upstream: "[/host.com/]1.1.1.1", + valid: true, + wantDef: false, + }, { + name: "valid", + upstream: "[//]tls://1.1.1.1", + valid: true, + wantDef: false, + }, { + name: "valid", + upstream: "[/www.host.com/]#", + valid: true, + wantDef: false, + }, { + name: "valid", + upstream: "[/host.com/google.com/]8.8.8.8", + valid: true, + wantDef: false, + }, { + name: "valid", + upstream: "[/host/]sdns://AQMAAAAAAAAAFDE3Ni4xMDMuMTMwLjEzMDo1NDQzINErR_JS3PLCu_iZEIbq95zkSV2LFsigxDIuUso_OQhzIjIuZG5zY3J5cHQuZGVmYXVsdC5uczEuYWRndWFyZC5jb20", + valid: true, + wantDef: false, + }} - validDefaultUpstreams := []string{ - "1.1.1.1", - "tls://1.1.1.1", - "https://dns.adguard.com/dns-query", - "sdns://AQMAAAAAAAAAFDE3Ni4xMDMuMTMwLjEzMDo1NDQzINErR_JS3PLCu_iZEIbq95zkSV2LFsigxDIuUso_OQhzIjIuZG5zY3J5cHQuZGVmYXVsdC5uczEuYWRndWFyZC5jb20", - } - - validUpstreams := []string{ - "[/host.com/]1.1.1.1", - "[//]tls://1.1.1.1", - "[/www.host.com/]#", - "[/host.com/google.com/]8.8.8.8", - "[/host/]sdns://AQMAAAAAAAAAFDE3Ni4xMDMuMTMwLjEzMDo1NDQzINErR_JS3PLCu_iZEIbq95zkSV2LFsigxDIuUso_OQhzIjIuZG5zY3J5cHQuZGVmYXVsdC5uczEuYWRndWFyZC5jb20", - } - for _, u := range invalidUpstreams { - _, err := validateUpstream(u) - if err == nil { - t.Fatalf("upstream %s is invalid but it pass through validation", u) - } - } - - for _, u := range validDefaultUpstreams { - defaultUpstream, err := validateUpstream(u) - if err != nil { - t.Fatalf("upstream %s is valid but it doen't pass through validation cause: %s", u, err) - } - if !defaultUpstream { - t.Fatalf("upstream %s is default one!", u) - } - } - - for _, u := range validUpstreams { - defaultUpstream, err := validateUpstream(u) - if err != nil { - t.Fatalf("upstream %s is valid but it doen't pass through validation cause: %s", u, err) - } - if defaultUpstream { - t.Fatalf("upstream %s is default one!", u) - } + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + defaultUpstream, err := validateUpstream(tc.upstream) + assert.Equal(t, tc.valid, err == nil) + if err == nil { + assert.Equal(t, tc.wantDef, defaultUpstream) + } + }) } } func TestValidateUpstreamsSet(t *testing.T) { - // Empty upstreams array + // Empty upstreams array. var upstreamsSet []string - err := ValidateUpstreams(upstreamsSet) - assert.Nil(t, err, "empty upstreams array should be valid") + assert.Nil(t, ValidateUpstreams(upstreamsSet), "empty upstreams array should be valid") - // Comment in upstreams array + // Comment in upstreams array. upstreamsSet = []string{"# comment"} - err = ValidateUpstreams(upstreamsSet) - assert.Nil(t, err, "comments should not be validated") + assert.Nil(t, ValidateUpstreams(upstreamsSet), "comments should not be validated") - // Set of valid upstreams. There is no default upstream specified + // Set of valid upstreams. There is no default upstream specified. upstreamsSet = []string{ "[/host.com/]1.1.1.1", "[//]tls://1.1.1.1", @@ -997,52 +991,81 @@ func TestValidateUpstreamsSet(t *testing.T) { "[/host.com/google.com/]8.8.8.8", "[/host/]sdns://AQMAAAAAAAAAFDE3Ni4xMDMuMTMwLjEzMDo1NDQzINErR_JS3PLCu_iZEIbq95zkSV2LFsigxDIuUso_OQhzIjIuZG5zY3J5cHQuZGVmYXVsdC5uczEuYWRndWFyZC5jb20", } - err = ValidateUpstreams(upstreamsSet) - assert.NotNil(t, err, "there is no default upstream") + assert.NotNil(t, ValidateUpstreams(upstreamsSet), "there is no default upstream") - // Let's add default upstream + // Let's add default upstream. upstreamsSet = append(upstreamsSet, "8.8.8.8") - err = ValidateUpstreams(upstreamsSet) + err := ValidateUpstreams(upstreamsSet) assert.Nilf(t, err, "upstreams set is valid, but doesn't pass through validation cause: %s", err) - // Let's add invalid upstream + // Let's add invalid upstream. upstreamsSet = append(upstreamsSet, "dhcp://fake.dns") - err = ValidateUpstreams(upstreamsSet) - assert.NotNil(t, err, "there is an invalid upstream in set, but it pass through validation") + assert.NotNil(t, ValidateUpstreams(upstreamsSet), "there is an invalid upstream in set, but it pass through validation") } -func TestIpFromAddr(t *testing.T) { - addr := net.UDPAddr{} - addr.IP = net.ParseIP("1:2:3::4") - addr.Port = 12345 - addr.Zone = "eth0" - a := ipFromAddr(&addr) - assert.True(t, a == "1:2:3::4") - - a = ipFromAddr(nil) - assert.True(t, a == "") +func TestIPStringFromAddr(t *testing.T) { + addr := net.UDPAddr{ + IP: net.ParseIP("1:2:3::4"), + Port: 12345, + Zone: "eth0", + } + assert.Equal(t, IPStringFromAddr(&addr), addr.IP.String()) + assert.Empty(t, IPStringFromAddr(nil)) } func TestMatchDNSName(t *testing.T) { dnsNames := []string{"host1", "*.host2", "1.2.3.4"} sort.Strings(dnsNames) - assert.True(t, matchDNSName(dnsNames, "host1")) - assert.True(t, matchDNSName(dnsNames, "a.host2")) - assert.True(t, matchDNSName(dnsNames, "b.a.host2")) - assert.True(t, matchDNSName(dnsNames, "1.2.3.4")) - assert.True(t, !matchDNSName(dnsNames, "host2")) - assert.True(t, !matchDNSName(dnsNames, "")) - assert.True(t, !matchDNSName(dnsNames, "*.host2")) + + testCases := []struct { + name string + dnsName string + want bool + }{{ + name: "match", + dnsName: "host1", + want: true, + }, { + name: "match", + dnsName: "a.host2", + want: true, + }, { + name: "match", + dnsName: "b.a.host2", + want: true, + }, { + name: "match", + dnsName: "1.2.3.4", + want: true, + }, { + name: "mismatch", + dnsName: "host2", + want: false, + }, { + name: "mismatch", + dnsName: "", + want: false, + }, { + name: "mismatch", + dnsName: "*.host2", + want: false, + }} + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + assert.Equal(t, tc.want, matchDNSName(dnsNames, tc.dnsName)) + }) + } } -type testDHCP struct { -} +type testDHCP struct{} func (d *testDHCP) Leases(flags int) []dhcpd.Lease { - l := dhcpd.Lease{} - l.IP = net.ParseIP("127.0.0.1").To4() - l.HWAddr, _ = net.ParseMAC("aa:aa:aa:aa:aa:aa") - l.Hostname = "localhost" + l := dhcpd.Lease{ + IP: net.IP{127, 0, 0, 1}, + HWAddr: net.HardwareAddr{0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA}, + Hostname: "localhost", + } + return []dhcpd.Lease{l} } func (d *testDHCP) SetOnLeaseChanged(onLeaseChanged dhcpd.OnLeaseChangedT) {} @@ -1050,28 +1073,34 @@ func (d *testDHCP) SetOnLeaseChanged(onLeaseChanged dhcpd.OnLeaseChangedT) {} func TestPTRResponseFromDHCPLeases(t *testing.T) { dhcp := &testDHCP{} - c := dnsfilter.Config{} - f := dnsfilter.New(&c, nil) - s := NewServer(DNSCreateParams{DNSFilter: f, DHCPServer: dhcp}) - s.conf.UDPListenAddr = &net.UDPAddr{Port: 0} - s.conf.TCPListenAddr = &net.TCPAddr{Port: 0} + s := NewServer(DNSCreateParams{ + DNSFilter: dnsfilter.New(&dnsfilter.Config{}, nil), + DHCPServer: dhcp, + }) + + s.conf.UDPListenAddr = &net.UDPAddr{} + s.conf.TCPListenAddr = &net.TCPAddr{} s.conf.UpstreamDNS = []string{"127.0.0.1:53"} s.conf.FilteringConfig.ProtectionEnabled = true err := s.Prepare(nil) - assert.True(t, err == nil) - assert.Nil(t, s.Start()) + assert.Nil(t, err) + assert.Nil(t, s.Start()) addr := s.dnsProxy.Addr(proxy.ProtoUDP) - req := createTestMessage("1.0.0.127.in-addr.arpa.") - req.Question[0].Qtype = dns.TypePTR + + req := createTestMessageWithType("1.0.0.127.in-addr.arpa.", dns.TypePTR) resp, err := dns.Exchange(req, addr.String()) + assert.Nil(t, err) - assert.Equal(t, 1, len(resp.Answer)) + assert.Len(t, resp.Answer, 1) assert.Equal(t, dns.TypePTR, resp.Answer[0].Header().Rrtype) assert.Equal(t, "1.0.0.127.in-addr.arpa.", resp.Answer[0].Header().Name) - ptr := resp.Answer[0].(*dns.PTR) - assert.Equal(t, "localhost.", ptr.Ptr) + + ptr, ok := resp.Answer[0].(*dns.PTR) + if assert.True(t, ok) { + assert.Equal(t, "localhost.", ptr.Ptr) + } s.Close() } @@ -1081,39 +1110,44 @@ func TestPTRResponseFromHosts(t *testing.T) { AutoHosts: &util.AutoHosts{}, } - // Prepare test hosts file - hf, _ := ioutil.TempFile("", "") - defer func() { _ = os.Remove(hf.Name()) }() - defer hf.Close() + // Prepare test hosts file. + hf, err := ioutil.TempFile("", "") + if assert.Nil(t, err) { + t.Cleanup(func() { + assert.Nil(t, hf.Close()) + assert.Nil(t, os.Remove(hf.Name())) + }) + } _, _ = hf.WriteString(" 127.0.0.1 host # comment \n") _, _ = hf.WriteString(" ::1 localhost#comment \n") - // Init auto hosts + // Init auto hosts. c.AutoHosts.Init(hf.Name()) - defer c.AutoHosts.Close() + t.Cleanup(c.AutoHosts.Close) - f := dnsfilter.New(&c, nil) - s := NewServer(DNSCreateParams{DNSFilter: f}) - s.conf.UDPListenAddr = &net.UDPAddr{Port: 0} - s.conf.TCPListenAddr = &net.TCPAddr{Port: 0} + s := NewServer(DNSCreateParams{DNSFilter: dnsfilter.New(&c, nil)}) + s.conf.UDPListenAddr = &net.UDPAddr{} + s.conf.TCPListenAddr = &net.TCPAddr{} s.conf.UpstreamDNS = []string{"127.0.0.1:53"} s.conf.FilteringConfig.ProtectionEnabled = true - err := s.Prepare(nil) - assert.True(t, err == nil) + assert.Nil(t, s.Prepare(nil)) + assert.Nil(t, s.Start()) addr := s.dnsProxy.Addr(proxy.ProtoUDP) - req := createTestMessage("1.0.0.127.in-addr.arpa.") - req.Question[0].Qtype = dns.TypePTR + req := createTestMessageWithType("1.0.0.127.in-addr.arpa.", dns.TypePTR) resp, err := dns.Exchange(req, addr.String()) assert.Nil(t, err) - assert.Equal(t, 1, len(resp.Answer)) + assert.Len(t, resp.Answer, 1) assert.Equal(t, dns.TypePTR, resp.Answer[0].Header().Rrtype) assert.Equal(t, "1.0.0.127.in-addr.arpa.", resp.Answer[0].Header().Name) - ptr := resp.Answer[0].(*dns.PTR) - assert.Equal(t, "host.", ptr.Ptr) + + ptr, ok := resp.Answer[0].(*dns.PTR) + if assert.True(t, ok) { + assert.Equal(t, "host.", ptr.Ptr) + } s.Close() } diff --git a/internal/dnsforward/dnsrewrite.go b/internal/dnsforward/dnsrewrite.go index 01895323..3741e850 100644 --- a/internal/dnsforward/dnsrewrite.go +++ b/internal/dnsforward/dnsrewrite.go @@ -13,27 +13,55 @@ import ( ) // filterDNSRewriteResponse handles a single DNS rewrite response entry. -// It returns the constructed answer resource record. +// It returns the properly constructed answer resource record. func (s *Server) filterDNSRewriteResponse(req *dns.Msg, rr rules.RRType, v rules.RRValue) (ans dns.RR, err error) { + // TODO(a.garipov): As more types are added, we will probably want to + // use a handler-oriented approach here. So, think of a way to decouple + // the answer generation logic from the Server. + switch rr { case dns.TypeA, dns.TypeAAAA: ip, ok := v.(net.IP) if !ok { - return nil, fmt.Errorf("value has type %T, not net.IP", v) + return nil, fmt.Errorf("value for rr type %d has type %T, not net.IP", rr, v) } if rr == dns.TypeA { - return s.genAAnswer(req, ip.To4()), nil + return s.genAnswerA(req, ip.To4()), nil } - return s.genAAAAAnswer(req, ip), nil - case dns.TypeTXT: + return s.genAnswerAAAA(req, ip), nil + case dns.TypePTR, + dns.TypeTXT: str, ok := v.(string) if !ok { - return nil, fmt.Errorf("value has type %T, not string", v) + return nil, fmt.Errorf("value for rr type %d has type %T, not string", rr, v) } - return s.genTXTAnswer(req, []string{str}), nil + if rr == dns.TypeTXT { + return s.genAnswerTXT(req, []string{str}), nil + } + + return s.genAnswerPTR(req, str), nil + case dns.TypeMX: + mx, ok := v.(*rules.DNSMX) + if !ok { + return nil, fmt.Errorf("value for rr type %d has type %T, not *rules.DNSMX", rr, v) + } + + return s.genAnswerMX(req, mx), nil + case dns.TypeHTTPS, + dns.TypeSVCB: + svcb, ok := v.(*rules.DNSSVCB) + if !ok { + return nil, fmt.Errorf("value for rr type %d has type %T, not *rules.DNSSVCB", rr, v) + } + + if rr == dns.TypeHTTPS { + return s.genAnswerHTTPS(req, svcb), nil + } + + return s.genAnswerSVCB(req, svcb), nil default: log.Debug("don't know how to handle dns rr type %d, skipping", rr) diff --git a/internal/dnsforward/dnsrewrite_test.go b/internal/dnsforward/dnsrewrite_test.go new file mode 100644 index 00000000..4029038a --- /dev/null +++ b/internal/dnsforward/dnsrewrite_test.go @@ -0,0 +1,178 @@ +package dnsforward + +import ( + "net" + "testing" + + "github.com/AdguardTeam/AdGuardHome/internal/dnsfilter" + "github.com/AdguardTeam/dnsproxy/proxy" + "github.com/AdguardTeam/urlfilter/rules" + "github.com/miekg/dns" + "github.com/stretchr/testify/assert" +) + +func TestServer_FilterDNSRewrite(t *testing.T) { + // Helper data. + ip4 := net.IP{127, 0, 0, 1} + ip6 := net.IP{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1} + mx := &rules.DNSMX{ + Exchange: "mail.example.com", + Preference: 32, + } + svcb := &rules.DNSSVCB{ + Params: map[string]string{"alpn": "h3"}, + Target: "example.com", + Priority: 32, + } + const domain = "example.com" + + // Helper functions and entities. + srv := &Server{} + makeQ := func(qtype rules.RRType) (req *dns.Msg) { + return &dns.Msg{ + Question: []dns.Question{{ + Qtype: qtype, + }}, + } + } + makeRes := func(rcode rules.RCode, rr rules.RRType, v rules.RRValue) (res dnsfilter.Result) { + resp := dnsfilter.DNSRewriteResultResponse{ + rr: []rules.RRValue{v}, + } + return dnsfilter.Result{ + DNSRewriteResult: &dnsfilter.DNSRewriteResult{ + RCode: rcode, + Response: resp, + }, + } + } + + // Tests. + t.Run("nxdomain", func(t *testing.T) { + req := makeQ(dns.TypeA) + res := makeRes(dns.RcodeNameError, 0, nil) + d := &proxy.DNSContext{} + + err := srv.filterDNSRewrite(req, res, d) + assert.Nil(t, err) + assert.Equal(t, dns.RcodeNameError, d.Res.Rcode) + }) + + t.Run("noerror_empty", func(t *testing.T) { + req := makeQ(dns.TypeA) + res := makeRes(dns.RcodeSuccess, 0, nil) + d := &proxy.DNSContext{} + + err := srv.filterDNSRewrite(req, res, d) + assert.Nil(t, err) + assert.Equal(t, dns.RcodeSuccess, d.Res.Rcode) + assert.Empty(t, d.Res.Answer) + }) + + t.Run("noerror_a", func(t *testing.T) { + req := makeQ(dns.TypeA) + res := makeRes(dns.RcodeSuccess, dns.TypeA, ip4) + d := &proxy.DNSContext{} + + err := srv.filterDNSRewrite(req, res, d) + assert.Nil(t, err) + assert.Equal(t, dns.RcodeSuccess, d.Res.Rcode) + if assert.Len(t, d.Res.Answer, 1) { + assert.Equal(t, ip4, d.Res.Answer[0].(*dns.A).A) + } + }) + + t.Run("noerror_aaaa", func(t *testing.T) { + req := makeQ(dns.TypeAAAA) + res := makeRes(dns.RcodeSuccess, dns.TypeAAAA, ip6) + d := &proxy.DNSContext{} + + err := srv.filterDNSRewrite(req, res, d) + assert.Nil(t, err) + assert.Equal(t, dns.RcodeSuccess, d.Res.Rcode) + if assert.Len(t, d.Res.Answer, 1) { + assert.Equal(t, ip6, d.Res.Answer[0].(*dns.AAAA).AAAA) + } + }) + + t.Run("noerror_ptr", func(t *testing.T) { + req := makeQ(dns.TypePTR) + res := makeRes(dns.RcodeSuccess, dns.TypePTR, domain) + d := &proxy.DNSContext{} + + err := srv.filterDNSRewrite(req, res, d) + assert.Nil(t, err) + assert.Equal(t, dns.RcodeSuccess, d.Res.Rcode) + if assert.Len(t, d.Res.Answer, 1) { + assert.Equal(t, domain, d.Res.Answer[0].(*dns.PTR).Ptr) + } + }) + + t.Run("noerror_txt", func(t *testing.T) { + req := makeQ(dns.TypeTXT) + res := makeRes(dns.RcodeSuccess, dns.TypeTXT, domain) + d := &proxy.DNSContext{} + + err := srv.filterDNSRewrite(req, res, d) + assert.Nil(t, err) + assert.Equal(t, dns.RcodeSuccess, d.Res.Rcode) + if assert.Len(t, d.Res.Answer, 1) { + assert.Equal(t, []string{domain}, d.Res.Answer[0].(*dns.TXT).Txt) + } + }) + + t.Run("noerror_mx", func(t *testing.T) { + req := makeQ(dns.TypeMX) + res := makeRes(dns.RcodeSuccess, dns.TypeMX, mx) + d := &proxy.DNSContext{} + + err := srv.filterDNSRewrite(req, res, d) + assert.Nil(t, err) + assert.Equal(t, dns.RcodeSuccess, d.Res.Rcode) + if assert.Len(t, d.Res.Answer, 1) { + ans, ok := d.Res.Answer[0].(*dns.MX) + if assert.True(t, ok) { + assert.Equal(t, mx.Exchange, ans.Mx) + assert.Equal(t, mx.Preference, ans.Preference) + } + } + }) + + t.Run("noerror_svcb", func(t *testing.T) { + req := makeQ(dns.TypeSVCB) + res := makeRes(dns.RcodeSuccess, dns.TypeSVCB, svcb) + d := &proxy.DNSContext{} + + err := srv.filterDNSRewrite(req, res, d) + assert.Nil(t, err) + assert.Equal(t, dns.RcodeSuccess, d.Res.Rcode) + if assert.Len(t, d.Res.Answer, 1) { + ans, ok := d.Res.Answer[0].(*dns.SVCB) + if assert.True(t, ok) { + assert.Equal(t, dns.SVCB_ALPN, ans.Value[0].Key()) + assert.Equal(t, svcb.Params["alpn"], ans.Value[0].String()) + assert.Equal(t, svcb.Target, ans.Target) + assert.Equal(t, svcb.Priority, ans.Priority) + } + } + }) + + t.Run("noerror_https", func(t *testing.T) { + req := makeQ(dns.TypeHTTPS) + res := makeRes(dns.RcodeSuccess, dns.TypeHTTPS, svcb) + d := &proxy.DNSContext{} + + err := srv.filterDNSRewrite(req, res, d) + assert.Nil(t, err) + assert.Equal(t, dns.RcodeSuccess, d.Res.Rcode) + if assert.Len(t, d.Res.Answer, 1) { + ans, ok := d.Res.Answer[0].(*dns.HTTPS) + if assert.True(t, ok) { + assert.Equal(t, dns.SVCB_ALPN, ans.Value[0].Key()) + assert.Equal(t, svcb.Params["alpn"], ans.Value[0].String()) + assert.Equal(t, svcb.Target, ans.Target) + assert.Equal(t, svcb.Priority, ans.Priority) + } + } + }) +} diff --git a/internal/dnsforward/filter.go b/internal/dnsforward/filter.go index 5cd0090a..f3d9f758 100644 --- a/internal/dnsforward/filter.go +++ b/internal/dnsforward/filter.go @@ -12,7 +12,7 @@ import ( ) func (s *Server) beforeRequestHandler(_ *proxy.Proxy, d *proxy.DNSContext) (bool, error) { - ip := ipFromAddr(d.Addr) + ip := IPFromAddr(d.Addr) disallowed, _ := s.access.IsBlockedIP(ip) if disallowed { log.Tracef("Client IP %s is blocked by settings", ip) @@ -30,15 +30,15 @@ func (s *Server) beforeRequestHandler(_ *proxy.Proxy, d *proxy.DNSContext) (bool return true, nil } -// getClientRequestFilteringSettings lookups client filtering settings -// using the client's IP address from the DNSContext -func (s *Server) getClientRequestFilteringSettings(d *proxy.DNSContext) *dnsfilter.RequestFilteringSettings { +// getClientRequestFilteringSettings looks up client filtering settings using +// the client's IP address and ID, if any, from ctx. +func (s *Server) getClientRequestFilteringSettings(ctx *dnsContext) *dnsfilter.RequestFilteringSettings { setts := s.dnsFilter.GetConfig() setts.FilteringEnabled = true if s.conf.FilterHandler != nil { - clientAddr := ipFromAddr(d.Addr) - s.conf.FilterHandler(clientAddr, &setts) + s.conf.FilterHandler(IPFromAddr(ctx.proxyCtx.Addr), ctx.clientID, &setts) } + return &setts } @@ -55,7 +55,7 @@ func (s *Server) filterDNSRequest(ctx *dnsContext) (*dnsfilter.Result, error) { } else if res.IsFiltered { log.Tracef("Host %s is filtered, reason - %q, matched rule: %q", host, res.Reason, res.Rules[0].Text) d.Res = s.genDNSFilterMessage(d, &res) - } else if res.Reason.In(dnsfilter.ReasonRewrite, dnsfilter.DNSRewriteRule) && + } else if res.Reason.In(dnsfilter.Rewritten, dnsfilter.RewrittenRule) && res.CanonName != "" && len(res.IPList) == 0 { // Resolve the new canonical name, not the original host @@ -63,7 +63,7 @@ func (s *Server) filterDNSRequest(ctx *dnsContext) (*dnsfilter.Result, error) { // processFilteringAfterResponse. ctx.origQuestion = d.Req.Question[0] d.Req.Question[0].Name = dns.Fqdn(res.CanonName) - } else if res.Reason == dnsfilter.RewriteAutoHosts && len(res.ReverseHosts) != 0 { + } else if res.Reason == dnsfilter.RewrittenAutoHosts && len(res.ReverseHosts) != 0 { resp := s.makeResponse(req) for _, h := range res.ReverseHosts { hdr := dns.RR_Header{ @@ -82,29 +82,29 @@ func (s *Server) filterDNSRequest(ctx *dnsContext) (*dnsfilter.Result, error) { } d.Res = resp - } else if res.Reason == dnsfilter.ReasonRewrite || res.Reason == dnsfilter.RewriteAutoHosts { + } else if res.Reason == dnsfilter.Rewritten || res.Reason == dnsfilter.RewrittenAutoHosts { resp := s.makeResponse(req) name := host if len(res.CanonName) != 0 { - resp.Answer = append(resp.Answer, s.genCNAMEAnswer(req, res.CanonName)) + resp.Answer = append(resp.Answer, s.genAnswerCNAME(req, res.CanonName)) name = res.CanonName } for _, ip := range res.IPList { if req.Question[0].Qtype == dns.TypeA { - a := s.genAAnswer(req, ip.To4()) + a := s.genAnswerA(req, ip.To4()) a.Hdr.Name = dns.Fqdn(name) resp.Answer = append(resp.Answer, a) } else if req.Question[0].Qtype == dns.TypeAAAA { - a := s.genAAAAAnswer(req, ip) + a := s.genAnswerAAAA(req, ip) a.Hdr.Name = dns.Fqdn(name) resp.Answer = append(resp.Answer, a) } } d.Res = resp - } else if res.Reason == dnsfilter.DNSRewriteRule { + } else if res.Reason == dnsfilter.RewrittenRule { err = s.filterDNSRewrite(req, res, d) if err != nil { return nil, err diff --git a/internal/dnsforward/http.go b/internal/dnsforward/http.go index e24ba89e..527d3b38 100644 --- a/internal/dnsforward/http.go +++ b/internal/dnsforward/http.go @@ -8,6 +8,7 @@ import ( "strconv" "strings" + "github.com/AdguardTeam/dnsproxy/proxy" "github.com/AdguardTeam/dnsproxy/upstream" "github.com/AdguardTeam/golibs/log" "github.com/AdguardTeam/golibs/utils" @@ -28,8 +29,8 @@ type dnsConfig struct { ProtectionEnabled *bool `json:"protection_enabled"` RateLimit *uint32 `json:"ratelimit"` BlockingMode *string `json:"blocking_mode"` - BlockingIPv4 *string `json:"blocking_ipv4"` - BlockingIPv6 *string `json:"blocking_ipv6"` + BlockingIPv4 net.IP `json:"blocking_ipv4"` + BlockingIPv6 net.IP `json:"blocking_ipv6"` EDNSCSEnabled *bool `json:"edns_cs_enabled"` DNSSECEnabled *bool `json:"dnssec_enabled"` DisableIPv6 *bool `json:"disable_ipv6"` @@ -68,8 +69,8 @@ func (s *Server) getDNSConfig() dnsConfig { Bootstraps: &bootstraps, ProtectionEnabled: &protectionEnabled, BlockingMode: &blockingMode, - BlockingIPv4: &BlockingIPv4, - BlockingIPv6: &BlockingIPv6, + BlockingIPv4: BlockingIPv4, + BlockingIPv6: BlockingIPv6, RateLimit: &Ratelimit, EDNSCSEnabled: &EnableEDNSClientSubnet, DNSSECEnabled: &EnableDNSSEC, @@ -100,17 +101,11 @@ func (req *dnsConfig) checkBlockingMode() bool { bm := *req.BlockingMode if bm == "custom_ip" { - if req.BlockingIPv4 == nil || req.BlockingIPv6 == nil { + if req.BlockingIPv4.To4() == nil { return false } - ip4 := net.ParseIP(*req.BlockingIPv4) - if ip4 == nil || ip4.To4() == nil { - return false - } - - ip6 := net.ParseIP(*req.BlockingIPv6) - return ip6 != nil + return req.BlockingIPv6 != nil } for _, valid := range []string{ @@ -247,10 +242,8 @@ func (s *Server) setConfig(dc dnsConfig) (restart bool) { if dc.BlockingMode != nil { s.conf.BlockingMode = *dc.BlockingMode if *dc.BlockingMode == "custom_ip" { - s.conf.BlockingIPv4 = *dc.BlockingIPv4 - s.conf.BlockingIPAddrv4 = net.ParseIP(*dc.BlockingIPv4) - s.conf.BlockingIPv6 = *dc.BlockingIPv6 - s.conf.BlockingIPAddrv6 = net.ParseIP(*dc.BlockingIPv6) + s.conf.BlockingIPv4 = dc.BlockingIPv4.To4() + s.conf.BlockingIPv6 = dc.BlockingIPv6.To16() } } @@ -322,6 +315,11 @@ func ValidateUpstreams(upstreams []string) error { return nil } + _, err := proxy.ParseUpstreamsConfig(upstreams, []string{}, DefaultTimeout) + if err != nil { + return err + } + var defaultUpstreamFound bool for _, u := range upstreams { d, err := validateUpstream(u) @@ -530,12 +528,20 @@ func (s *Server) handleDOH(w http.ResponseWriter, r *http.Request) { } func (s *Server) registerHandlers() { - s.conf.HTTPRegister("GET", "/control/dns_info", s.handleGetConfig) - s.conf.HTTPRegister("POST", "/control/dns_config", s.handleSetConfig) - s.conf.HTTPRegister("POST", "/control/test_upstream_dns", s.handleTestUpstreamDNS) + s.conf.HTTPRegister(http.MethodGet, "/control/dns_info", s.handleGetConfig) + s.conf.HTTPRegister(http.MethodPost, "/control/dns_config", s.handleSetConfig) + s.conf.HTTPRegister(http.MethodPost, "/control/test_upstream_dns", s.handleTestUpstreamDNS) - s.conf.HTTPRegister("GET", "/control/access/list", s.handleAccessList) - s.conf.HTTPRegister("POST", "/control/access/set", s.handleAccessSet) + s.conf.HTTPRegister(http.MethodGet, "/control/access/list", s.handleAccessList) + s.conf.HTTPRegister(http.MethodPost, "/control/access/set", s.handleAccessSet) + // Register both versions, with and without the trailing slash, to + // prevent a 301 Moved Permanently redirect when clients request the + // path without the trailing slash. Those redirects break some clients. + // + // See go doc net/http.ServeMux. + // + // See also https://github.com/AdguardTeam/AdGuardHome/issues/2628. s.conf.HTTPRegister("", "/dns-query", s.handleDOH) + s.conf.HTTPRegister("", "/dns-query/", s.handleDOH) } diff --git a/internal/dnsforward/http_test.go b/internal/dnsforward/http_test.go index c8e2f9c5..f1ba7031 100644 --- a/internal/dnsforward/http_test.go +++ b/internal/dnsforward/http_test.go @@ -2,16 +2,35 @@ package dnsforward import ( "io/ioutil" + "net" "net/http" "net/http/httptest" "strings" "testing" + "github.com/AdguardTeam/AdGuardHome/internal/dnsfilter" "github.com/stretchr/testify/assert" ) func TestDNSForwardHTTTP_handleGetConfig(t *testing.T) { - s := createTestServer(t) + filterConf := &dnsfilter.Config{ + SafeBrowsingEnabled: true, + SafeBrowsingCacheSize: 1000, + SafeSearchEnabled: true, + SafeSearchCacheSize: 1000, + ParentalCacheSize: 1000, + CacheTime: 30, + } + forwardConf := ServerConfig{ + UDPListenAddr: &net.UDPAddr{}, + TCPListenAddr: &net.TCPAddr{}, + FilteringConfig: FilteringConfig{ + ProtectionEnabled: true, + UpstreamDNS: []string{"8.8.8.8:53", "8.8.4.4:53"}, + }, + ConfigModified: func() {}, + } + s := createTestServer(t, filterConf, forwardConf) err := s.Start() assert.Nil(t, err) defer assert.Nil(t, s.Stop()) @@ -35,6 +54,7 @@ func TestDNSForwardHTTTP_handleGetConfig(t *testing.T) { conf: func() ServerConfig { conf := defaultConf conf.FastestAddr = true + return conf }, want: "{\"upstream_dns\":[\"8.8.8.8:53\",\"8.8.4.4:53\"],\"upstream_dns_file\":\"\",\"bootstrap_dns\":[\"9.9.9.10\",\"149.112.112.10\",\"2620:fe::10\",\"2620:fe::fe:10\"],\"protection_enabled\":true,\"ratelimit\":0,\"blocking_mode\":\"\",\"blocking_ipv4\":\"\",\"blocking_ipv6\":\"\",\"edns_cs_enabled\":false,\"dnssec_enabled\":false,\"disable_ipv6\":false,\"upstream_mode\":\"fastest_addr\",\"cache_size\":0,\"cache_ttl_min\":0,\"cache_ttl_max\":0}\n", @@ -43,6 +63,7 @@ func TestDNSForwardHTTTP_handleGetConfig(t *testing.T) { conf: func() ServerConfig { conf := defaultConf conf.AllServers = true + return conf }, want: "{\"upstream_dns\":[\"8.8.8.8:53\",\"8.8.4.4:53\"],\"upstream_dns_file\":\"\",\"bootstrap_dns\":[\"9.9.9.10\",\"149.112.112.10\",\"2620:fe::10\",\"2620:fe::fe:10\"],\"protection_enabled\":true,\"ratelimit\":0,\"blocking_mode\":\"\",\"blocking_ipv4\":\"\",\"blocking_ipv6\":\"\",\"edns_cs_enabled\":false,\"dnssec_enabled\":false,\"disable_ipv6\":false,\"upstream_mode\":\"parallel\",\"cache_size\":0,\"cache_ttl_min\":0,\"cache_ttl_max\":0}\n", @@ -61,7 +82,24 @@ func TestDNSForwardHTTTP_handleGetConfig(t *testing.T) { } func TestDNSForwardHTTTP_handleSetConfig(t *testing.T) { - s := createTestServer(t) + filterConf := &dnsfilter.Config{ + SafeBrowsingEnabled: true, + SafeBrowsingCacheSize: 1000, + SafeSearchEnabled: true, + SafeSearchCacheSize: 1000, + ParentalCacheSize: 1000, + CacheTime: 30, + } + forwardConf := ServerConfig{ + UDPListenAddr: &net.UDPAddr{}, + TCPListenAddr: &net.TCPAddr{}, + FilteringConfig: FilteringConfig{ + ProtectionEnabled: true, + UpstreamDNS: []string{"8.8.8.8:53", "8.8.4.4:53"}, + }, + ConfigModified: func() {}, + } + s := createTestServer(t, filterConf, forwardConf) defaultConf := s.conf diff --git a/internal/dnsforward/ipset.go b/internal/dnsforward/ipset.go deleted file mode 100644 index 74a8ecd8..00000000 --- a/internal/dnsforward/ipset.go +++ /dev/null @@ -1,142 +0,0 @@ -package dnsforward - -import ( - "net" - "strings" - "sync" - - "github.com/AdguardTeam/AdGuardHome/internal/util" - "github.com/AdguardTeam/golibs/log" - "github.com/miekg/dns" -) - -type ipsetCtx struct { - ipsetList map[string][]string // domain -> []ipset_name - ipsetCache map[[4]byte]bool // cache for IP[] to prevent duplicate calls to ipset program - ipsetMutex *sync.Mutex - ipset6Cache map[[16]byte]bool // cache for IP[] to prevent duplicate calls to ipset program - ipset6Mutex *sync.Mutex -} - -// Convert configuration settings to an internal map -// DOMAIN[,DOMAIN].../IPSET1_NAME[,IPSET2_NAME]... -func (c *ipsetCtx) init(ipsetConfig []string) { - c.ipsetList = make(map[string][]string) - c.ipsetCache = make(map[[4]byte]bool) - c.ipsetMutex = &sync.Mutex{} - c.ipset6Cache = make(map[[16]byte]bool) - c.ipset6Mutex = &sync.Mutex{} - - for _, it := range ipsetConfig { - it = strings.TrimSpace(it) - hostsAndNames := strings.Split(it, "/") - if len(hostsAndNames) != 2 { - log.Debug("IPSET: invalid value %q", it) - continue - } - - ipsetNames := strings.Split(hostsAndNames[1], ",") - if len(ipsetNames) == 0 { - log.Debug("IPSET: invalid value %q", it) - continue - } - bad := false - for i := range ipsetNames { - ipsetNames[i] = strings.TrimSpace(ipsetNames[i]) - if len(ipsetNames[i]) == 0 { - bad = true - break - } - } - if bad { - log.Debug("IPSET: invalid value %q", it) - continue - } - - hosts := strings.Split(hostsAndNames[0], ",") - for _, host := range hosts { - host = strings.TrimSpace(host) - host = strings.ToLower(host) - if len(host) == 0 { - log.Debug("IPSET: invalid value %q", it) - continue - } - c.ipsetList[host] = ipsetNames - } - } - log.Debug("IPSET: added %d hosts", len(c.ipsetList)) -} - -func (c *ipsetCtx) getIP(rr dns.RR) net.IP { - switch a := rr.(type) { - case *dns.A: - var ip4 [4]byte - copy(ip4[:], a.A.To4()) - c.ipsetMutex.Lock() - defer c.ipsetMutex.Unlock() - _, found := c.ipsetCache[ip4] - if found { - return nil // this IP was added before - } - c.ipsetCache[ip4] = false - return a.A - - case *dns.AAAA: - var ip6 [16]byte - copy(ip6[:], a.AAAA) - c.ipset6Mutex.Lock() - defer c.ipset6Mutex.Unlock() - _, found := c.ipset6Cache[ip6] - if found { - return nil // this IP was added before - } - c.ipset6Cache[ip6] = false - return a.AAAA - - default: - return nil - } -} - -// Add IP addresses of the specified in configuration domain names to an ipset list -func (c *ipsetCtx) process(ctx *dnsContext) int { - req := ctx.proxyCtx.Req - if !(req.Question[0].Qtype == dns.TypeA || - req.Question[0].Qtype == dns.TypeAAAA) || - !ctx.responseFromUpstream { - return resultDone - } - - host := req.Question[0].Name - host = strings.TrimSuffix(host, ".") - host = strings.ToLower(host) - ipsetNames, found := c.ipsetList[host] - if !found { - return resultDone - } - - log.Debug("IPSET: found ipsets %v for host %s", ipsetNames, host) - - for _, it := range ctx.proxyCtx.Res.Answer { - ip := c.getIP(it) - if ip == nil { - continue - } - - ipStr := ip.String() - for _, name := range ipsetNames { - code, out, err := util.RunCommand("ipset", "add", name, ipStr) - if err != nil { - log.Info("IPSET: %s(%s) -> %s: %s", host, ipStr, name, err) - continue - } - if code != 0 { - log.Info("IPSET: ipset add: code:%d output:%q", code, out) - continue - } - log.Debug("IPSET: added %s(%s) -> %s", host, ipStr, name) - } - } - - return resultDone -} diff --git a/internal/dnsforward/ipset_linux.go b/internal/dnsforward/ipset_linux.go new file mode 100644 index 00000000..f74f5503 --- /dev/null +++ b/internal/dnsforward/ipset_linux.go @@ -0,0 +1,400 @@ +// +build linux + +package dnsforward + +import ( + "fmt" + "net" + "strings" + "sync" + + "github.com/AdguardTeam/AdGuardHome/internal/agherr" + "github.com/AdguardTeam/golibs/log" + "github.com/digineo/go-ipset/v2" + "github.com/mdlayher/netlink" + "github.com/miekg/dns" + "github.com/ti-mo/netfilter" +) + +// TODO(a.garipov): Cover with unit tests as well as document how to test it +// manually. The original PR by @dsheets on Github contained an integration +// test, but unfortunately I didn't have the time to properly refactor it and +// check it in. +// +// See https://github.com/AdguardTeam/AdGuardHome/issues/2611. + +// ipsetProps contains one Linux Netfilter ipset properties. +type ipsetProps struct { + name string + family netfilter.ProtoFamily +} + +// ipsetCtx is the Linux Netfilter ipset context. +type ipsetCtx struct { + // mu protects all properties below. + mu *sync.Mutex + + nameToIpset map[string]ipsetProps + domainToIpsets map[string][]ipsetProps + + // TODO(a.garipov): Currently, the ipset list is static, and we don't + // read the IPs already in sets, so we can assume that all incoming IPs + // are either added to all corresponding ipsets or not. When that stops + // being the case, for example if we add dynamic reconfiguration of + // ipsets, this map will need to become a per-ipset-name one. + addedIPs map[[16]byte]struct{} + + ipv4Conn *ipset.Conn + ipv6Conn *ipset.Conn +} + +// dialNetfilter establishes connections to Linux's netfilter module. +func (c *ipsetCtx) dialNetfilter(config *netlink.Config) (err error) { + // The kernel API does not actually require two sockets but package + // github.com/digineo/go-ipset does. + // + // TODO(a.garipov): Perhaps we can ditch package ipset altogether and + // just use packages netfilter and netlink. + c.ipv4Conn, err = ipset.Dial(netfilter.ProtoIPv4, config) + if err != nil { + return fmt.Errorf("dialing v4: %w", err) + } + + c.ipv6Conn, err = ipset.Dial(netfilter.ProtoIPv6, config) + if err != nil { + return fmt.Errorf("dialing v6: %w", err) + } + + return nil +} + +// ipsetProps returns the properties of an ipset with the given name. +func (c *ipsetCtx) ipsetProps(name string) (set ipsetProps, err error) { + // The family doesn't seem to matter when we use a header query, so + // query only the IPv4 one. + // + // TODO(a.garipov): Find out if this is a bug or a feature. + res, err := c.ipv4Conn.Header(name) + if err != nil { + return set, err + } + + if res == nil || res.Family == nil { + return set, agherr.Error("empty response or no family data") + } + + family := netfilter.ProtoFamily(res.Family.Value) + if family != netfilter.ProtoIPv4 && family != netfilter.ProtoIPv6 { + return set, fmt.Errorf("unexpected ipset family %s", family) + } + + return ipsetProps{ + name: name, + family: family, + }, nil +} + +// ipsets returns currently known ipsets. +func (c *ipsetCtx) ipsets(names []string) (sets []ipsetProps, err error) { + for _, name := range names { + set, ok := c.nameToIpset[name] + if ok { + sets = append(sets, set) + + continue + } + + var err error + set, err = c.ipsetProps(name) + if err != nil { + return nil, fmt.Errorf("querying ipset %q: %w", name, err) + } + + c.nameToIpset[name] = set + sets = append(sets, set) + } + + return sets, nil +} + +// parseIpsetConfig parses one ipset configuration string. +func parseIpsetConfig(cfgStr string) (hosts, ipsetNames []string, err error) { + cfgStr = strings.TrimSpace(cfgStr) + hostsAndNames := strings.Split(cfgStr, "/") + if len(hostsAndNames) != 2 { + return nil, nil, fmt.Errorf("invalid value %q: expected one slash", cfgStr) + } + + hosts = strings.Split(hostsAndNames[0], ",") + ipsetNames = strings.Split(hostsAndNames[1], ",") + + if len(ipsetNames) == 0 { + log.Info("ipset: resolutions for %q will not be stored", hosts) + + return nil, nil, nil + } + + for i := range ipsetNames { + ipsetNames[i] = strings.TrimSpace(ipsetNames[i]) + if len(ipsetNames[i]) == 0 { + return nil, nil, fmt.Errorf("invalid value %q: empty ipset name", cfgStr) + } + } + + for i := range hosts { + hosts[i] = strings.TrimSpace(hosts[i]) + hosts[i] = strings.ToLower(hosts[i]) + if len(hosts[i]) == 0 { + log.Info("ipset: root catchall in %q", ipsetNames) + } + } + + return hosts, ipsetNames, nil +} + +// init initializes the ipset context. It is not safe for concurrent use. +// +// TODO(a.garipov): Rewrite into a simple constructor? +func (c *ipsetCtx) init(ipsetConfig []string) (err error) { + c.mu = &sync.Mutex{} + c.nameToIpset = make(map[string]ipsetProps) + c.domainToIpsets = make(map[string][]ipsetProps) + c.addedIPs = make(map[[16]byte]struct{}) + + err = c.dialNetfilter(&netlink.Config{}) + if err != nil { + return fmt.Errorf("ipset: dialing netfilter: %w", err) + } + + for i, cfgStr := range ipsetConfig { + var hosts, ipsetNames []string + hosts, ipsetNames, err = parseIpsetConfig(cfgStr) + if err != nil { + return fmt.Errorf("ipset: config line at index %d: %w", i, err) + } + + var ipsets []ipsetProps + ipsets, err = c.ipsets(ipsetNames) + if err != nil { + return fmt.Errorf("ipset: getting ipsets config line at index %d: %w", i, err) + } + + for _, host := range hosts { + c.domainToIpsets[host] = append(c.domainToIpsets[host], ipsets...) + } + } + + log.Debug("ipset: added %d domains for %d ipsets", len(c.domainToIpsets), len(c.nameToIpset)) + + return nil +} + +// Close closes the Linux Netfilter connections. +func (c *ipsetCtx) Close() (err error) { + var errors []error + if c.ipv4Conn != nil { + err = c.ipv4Conn.Close() + if err != nil { + errors = append(errors, err) + } + } + + if c.ipv6Conn != nil { + err = c.ipv6Conn.Close() + if err != nil { + errors = append(errors, err) + } + } + + if len(errors) != 0 { + return agherr.Many("closing ipsets", errors...) + } + + return nil +} + +// ipFromRR returns an IP address from a DNS resource record. +func ipFromRR(rr dns.RR) (ip net.IP) { + switch a := rr.(type) { + case *dns.A: + return a.A + case *dns.AAAA: + return a.AAAA + default: + return nil + } +} + +// lookupHost find the ipsets for the host, taking subdomain wildcards into +// account. +func (c *ipsetCtx) lookupHost(host string) (sets []ipsetProps) { + // Search for matching ipset hosts starting with most specific + // subdomain. We could use a trie here but the simple, inefficient + // solution isn't that expensive. ~75 % for 10 subdomains vs 0, but + // still sub-microsecond on a Core i7. + // + // TODO(a.garipov): Re-add benchmarks from the original PR. + for i := 0; i != -1; i++ { + host = host[i:] + sets = c.domainToIpsets[host] + if sets != nil { + return sets + } + + i = strings.Index(host, ".") + if i == -1 { + break + } + } + + // Check the root catch-all one. + return c.domainToIpsets[""] +} + +// addIPs adds the IP addresses for the host to the ipset. set must be same +// family as set's family. +func (c *ipsetCtx) addIPs(host string, set ipsetProps, ips []net.IP) (err error) { + if len(ips) == 0 { + return + } + + entries := make([]*ipset.Entry, 0, len(ips)) + for _, ip := range ips { + entries = append(entries, ipset.NewEntry(ipset.EntryIP(ip))) + } + + var conn *ipset.Conn + switch set.family { + case netfilter.ProtoIPv4: + conn = c.ipv4Conn + case netfilter.ProtoIPv6: + conn = c.ipv6Conn + default: + return fmt.Errorf("unexpected family %s for ipset %q", set.family, set.name) + } + + err = conn.Add(set.name, entries...) + if err != nil { + return fmt.Errorf("adding %q%s to ipset %q: %w", host, ips, set.name, err) + } + + log.Debug("ipset: added %s%s to ipset %s", host, ips, set.name) + + return nil +} + +// skipIpsetProcessing returns true when the ipset processing can be skipped for +// this request. +func (c *ipsetCtx) skipIpsetProcessing(ctx *dnsContext) (ok bool) { + if len(c.domainToIpsets) == 0 || ctx == nil || !ctx.responseFromUpstream { + return true + } + + req := ctx.proxyCtx.Req + if req == nil || len(req.Question) == 0 { + return true + } + + qt := req.Question[0].Qtype + return qt != dns.TypeA && qt != dns.TypeAAAA && qt != dns.TypeANY +} + +// process adds the resolved IP addresses to the domain's ipsets, if any. +func (c *ipsetCtx) process(ctx *dnsContext) (rc resultCode) { + var err error + + if c == nil { + return resultCodeSuccess + } + + log.Debug("ipset: starting processing") + + c.mu.Lock() + defer c.mu.Unlock() + + if c.skipIpsetProcessing(ctx) { + log.Debug("ipset: skipped processing for request") + + return resultCodeSuccess + } + + req := ctx.proxyCtx.Req + host := req.Question[0].Name + host = strings.TrimSuffix(host, ".") + host = strings.ToLower(host) + sets := c.lookupHost(host) + if len(sets) == 0 { + log.Debug("ipset: no ipsets for host %s", host) + + return resultCodeSuccess + } + + log.Debug("ipset: found ipsets %+v for host %s", sets, host) + + if ctx.proxyCtx.Res == nil { + return resultCodeSuccess + } + + ans := ctx.proxyCtx.Res.Answer + l := len(ans) + v4s := make([]net.IP, 0, l) + v6s := make([]net.IP, 0, l) + for _, rr := range ans { + ip := ipFromRR(rr) + if ip == nil { + continue + } + + var iparr [16]byte + copy(iparr[:], ip.To16()) + if _, added := c.addedIPs[iparr]; added { + continue + } + + if ip.To4() == nil { + v6s = append(v6s, ip) + + continue + } + + v4s = append(v4s, ip) + } + +setLoop: + for _, set := range sets { + switch set.family { + case netfilter.ProtoIPv4: + err = c.addIPs(host, set, v4s) + if err != nil { + break setLoop + } + case netfilter.ProtoIPv6: + err = c.addIPs(host, set, v6s) + if err != nil { + break setLoop + } + default: + err = fmt.Errorf("unexpected family %s for ipset %q", set.family, set.name) + break setLoop + } + } + if err != nil { + log.Error("ipset: adding host ips: %s", err) + } else { + log.Debug("ipset: processed %d new ips", len(v4s)+len(v6s)) + } + + for _, ip := range v4s { + var iparr [16]byte + copy(iparr[:], ip.To16()) + c.addedIPs[iparr] = struct{}{} + } + + for _, ip := range v6s { + var iparr [16]byte + copy(iparr[:], ip.To16()) + c.addedIPs[iparr] = struct{}{} + } + + return resultCodeSuccess +} diff --git a/internal/dnsforward/ipset_others.go b/internal/dnsforward/ipset_others.go new file mode 100644 index 00000000..1b1c2e1b --- /dev/null +++ b/internal/dnsforward/ipset_others.go @@ -0,0 +1,26 @@ +// +build !linux + +package dnsforward + +import ( + "github.com/AdguardTeam/golibs/log" +) + +type ipsetCtx struct{} + +// init initializes the ipset context. +func (c *ipsetCtx) init(ipsetConfig []string) (err error) { + if len(ipsetConfig) != 0 { + log.Info("ipset: only available on linux") + } + + return nil +} + +// process adds the resolved IP addresses to the domain's ipsets, if any. +func (c *ipsetCtx) process(_ *dnsContext) (rc resultCode) { + return resultCodeSuccess +} + +// Close closes the Linux Netfilter connections. +func (c *ipsetCtx) Close() (_ error) { return nil } diff --git a/internal/dnsforward/ipset_test.go b/internal/dnsforward/ipset_test.go deleted file mode 100644 index 41be83d2..00000000 --- a/internal/dnsforward/ipset_test.go +++ /dev/null @@ -1,41 +0,0 @@ -package dnsforward - -import ( - "testing" - - "github.com/AdguardTeam/dnsproxy/proxy" - "github.com/miekg/dns" - "github.com/stretchr/testify/assert" -) - -func TestIPSET(t *testing.T) { - s := Server{} - s.conf.IPSETList = append(s.conf.IPSETList, "HOST.com/name") - s.conf.IPSETList = append(s.conf.IPSETList, "host2.com,host3.com/name23") - s.conf.IPSETList = append(s.conf.IPSETList, "host4.com/name4,name41") - c := ipsetCtx{} - c.init(s.conf.IPSETList) - - assert.Equal(t, "name", c.ipsetList["host.com"][0]) - assert.Equal(t, "name23", c.ipsetList["host2.com"][0]) - assert.Equal(t, "name23", c.ipsetList["host3.com"][0]) - assert.Equal(t, "name4", c.ipsetList["host4.com"][0]) - assert.Equal(t, "name41", c.ipsetList["host4.com"][1]) - - _, ok := c.ipsetList["host0.com"] - assert.False(t, ok) - - ctx := &dnsContext{ - srv: &s, - } - ctx.proxyCtx = &proxy.DNSContext{} - ctx.proxyCtx.Req = &dns.Msg{ - Question: []dns.Question{ - { - Name: "host.com.", - Qtype: dns.TypeA, - }, - }, - } - assert.Equal(t, resultDone, c.process(ctx)) -} diff --git a/internal/dnsforward/msg.go b/internal/dnsforward/msg.go index f8200056..ba95bbce 100644 --- a/internal/dnsforward/msg.go +++ b/internal/dnsforward/msg.go @@ -7,10 +7,12 @@ import ( "github.com/AdguardTeam/AdGuardHome/internal/dnsfilter" "github.com/AdguardTeam/dnsproxy/proxy" "github.com/AdguardTeam/golibs/log" + "github.com/AdguardTeam/urlfilter/rules" "github.com/miekg/dns" ) -// Create a DNS response by DNS request and set necessary flags +// makeResponse creates a DNS response by req and sets necessary flags. It also +// guarantees that req.Question will be not empty. func (s *Server) makeResponse(req *dns.Msg) (resp *dns.Msg) { resp = &dns.Msg{ MsgHdr: dns.MsgHdr{ @@ -58,9 +60,9 @@ func (s *Server) genDNSFilterMessage(d *proxy.DNSContext, result *dnsfilter.Resu switch m.Question[0].Qtype { case dns.TypeA: - return s.genARecord(m, s.conf.BlockingIPAddrv4) + return s.genARecord(m, s.conf.BlockingIPv4) case dns.TypeAAAA: - return s.genAAAARecord(m, s.conf.BlockingIPAddrv6) + return s.genAAAARecord(m, s.conf.BlockingIPv6) } } else if s.conf.BlockingMode == "nxdomain" { // means that we should return NXDOMAIN for any blocked request @@ -92,48 +94,64 @@ func (s *Server) genServerFailure(request *dns.Msg) *dns.Msg { func (s *Server) genARecord(request *dns.Msg, ip net.IP) *dns.Msg { resp := s.makeResponse(request) - resp.Answer = append(resp.Answer, s.genAAnswer(request, ip)) + resp.Answer = append(resp.Answer, s.genAnswerA(request, ip)) return resp } func (s *Server) genAAAARecord(request *dns.Msg, ip net.IP) *dns.Msg { resp := s.makeResponse(request) - resp.Answer = append(resp.Answer, s.genAAAAAnswer(request, ip)) + resp.Answer = append(resp.Answer, s.genAnswerAAAA(request, ip)) return resp } -func (s *Server) genAAnswer(req *dns.Msg, ip net.IP) *dns.A { - answer := new(dns.A) - answer.Hdr = dns.RR_Header{ +func (s *Server) hdr(req *dns.Msg, rrType rules.RRType) (h dns.RR_Header) { + return dns.RR_Header{ Name: req.Question[0].Name, - Rrtype: dns.TypeA, + Rrtype: rrType, Ttl: s.conf.BlockedResponseTTL, Class: dns.ClassINET, } - answer.A = ip - return answer } -func (s *Server) genAAAAAnswer(req *dns.Msg, ip net.IP) *dns.AAAA { - answer := new(dns.AAAA) - answer.Hdr = dns.RR_Header{ - Name: req.Question[0].Name, - Rrtype: dns.TypeAAAA, - Ttl: s.conf.BlockedResponseTTL, - Class: dns.ClassINET, +func (s *Server) genAnswerA(req *dns.Msg, ip net.IP) (ans *dns.A) { + return &dns.A{ + Hdr: s.hdr(req, dns.TypeA), + A: ip, } - answer.AAAA = ip - return answer } -func (s *Server) genTXTAnswer(req *dns.Msg, strs []string) (answer *dns.TXT) { +func (s *Server) genAnswerAAAA(req *dns.Msg, ip net.IP) (ans *dns.AAAA) { + return &dns.AAAA{ + Hdr: s.hdr(req, dns.TypeAAAA), + AAAA: ip, + } +} + +func (s *Server) genAnswerCNAME(req *dns.Msg, cname string) (ans *dns.CNAME) { + return &dns.CNAME{ + Hdr: s.hdr(req, dns.TypeCNAME), + Target: dns.Fqdn(cname), + } +} + +func (s *Server) genAnswerMX(req *dns.Msg, mx *rules.DNSMX) (ans *dns.MX) { + return &dns.MX{ + Hdr: s.hdr(req, dns.TypePTR), + Preference: mx.Preference, + Mx: mx.Exchange, + } +} + +func (s *Server) genAnswerPTR(req *dns.Msg, ptr string) (ans *dns.PTR) { + return &dns.PTR{ + Hdr: s.hdr(req, dns.TypePTR), + Ptr: ptr, + } +} + +func (s *Server) genAnswerTXT(req *dns.Msg, strs []string) (ans *dns.TXT) { return &dns.TXT{ - Hdr: dns.RR_Header{ - Name: req.Question[0].Name, - Rrtype: dns.TypeTXT, - Ttl: s.conf.BlockedResponseTTL, - Class: dns.ClassINET, - }, + Hdr: s.hdr(req, dns.TypeTXT), Txt: strs, } } @@ -198,19 +216,6 @@ func (s *Server) genBlockedHost(request *dns.Msg, newAddr string, d *proxy.DNSCo return resp } -// Make a CNAME response -func (s *Server) genCNAMEAnswer(req *dns.Msg, cname string) *dns.CNAME { - answer := new(dns.CNAME) - answer.Hdr = dns.RR_Header{ - Name: req.Question[0].Name, - Rrtype: dns.TypeCNAME, - Ttl: s.conf.BlockedResponseTTL, - Class: dns.ClassINET, - } - answer.Target = dns.Fqdn(cname) - return answer -} - // Create REFUSED DNS response func (s *Server) makeResponseREFUSED(request *dns.Msg) *dns.Msg { resp := dns.Msg{} diff --git a/internal/dnsforward/stats.go b/internal/dnsforward/stats.go index c447be05..29e6bb86 100644 --- a/internal/dnsforward/stats.go +++ b/internal/dnsforward/stats.go @@ -1,7 +1,6 @@ package dnsforward import ( - "net" "strings" "time" @@ -13,13 +12,13 @@ import ( ) // Write Stats data and logs -func processQueryLogsAndStats(ctx *dnsContext) int { +func processQueryLogsAndStats(ctx *dnsContext) (rc resultCode) { elapsed := time.Since(ctx.startTime) s := ctx.srv - d := ctx.proxyCtx + pctx := ctx.proxyCtx shouldLog := true - msg := d.Req + msg := pctx.Req // don't log ANY request if refuseAny is enabled if len(msg.Question) >= 1 && msg.Question[0].Qtype == dns.TypeANY && s.conf.RefuseAny { @@ -32,65 +31,67 @@ func processQueryLogsAndStats(ctx *dnsContext) int { if shouldLog && s.queryLog != nil { p := querylog.AddParams{ Question: msg, - Answer: d.Res, + Answer: pctx.Res, OrigAnswer: ctx.origResp, Result: ctx.result, Elapsed: elapsed, - ClientIP: getIP(d.Addr), + ClientIP: IPFromAddr(pctx.Addr), + ClientID: ctx.clientID, } - switch d.Proto { + switch pctx.Proto { case proxy.ProtoHTTPS: p.ClientProto = querylog.ClientProtoDOH case proxy.ProtoQUIC: p.ClientProto = querylog.ClientProtoDOQ case proxy.ProtoTLS: p.ClientProto = querylog.ClientProtoDOT + case proxy.ProtoDNSCrypt: + p.ClientProto = querylog.ClientProtoDNSCrypt default: - // Consider this a plain DNS-over-UDP or DNS-over-TCL + // Consider this a plain DNS-over-UDP or DNS-over-TCP // request. } - if d.Upstream != nil { - p.Upstream = d.Upstream.Address() + if pctx.Upstream != nil { + p.Upstream = pctx.Upstream.Address() } + s.queryLog.Add(p) } - s.updateStats(d, elapsed, *ctx.result) + s.updateStats(ctx, elapsed, *ctx.result) s.RUnlock() - return resultDone + return resultCodeSuccess } -func (s *Server) updateStats(d *proxy.DNSContext, elapsed time.Duration, res dnsfilter.Result) { +func (s *Server) updateStats(ctx *dnsContext, elapsed time.Duration, res dnsfilter.Result) { if s.stats == nil { return } + pctx := ctx.proxyCtx e := stats.Entry{} - e.Domain = strings.ToLower(d.Req.Question[0].Name) + e.Domain = strings.ToLower(pctx.Req.Question[0].Name) e.Domain = e.Domain[:len(e.Domain)-1] // remove last "." - switch addr := d.Addr.(type) { - case *net.UDPAddr: - e.Client = addr.IP - case *net.TCPAddr: - e.Client = addr.IP + + if clientID := ctx.clientID; clientID != "" { + e.Client = clientID + } else if ip := IPFromAddr(pctx.Addr); ip != nil { + e.Client = ip.String() } + e.Time = uint32(elapsed / 1000) e.Result = stats.RNotFiltered switch res.Reason { - case dnsfilter.FilteredSafeBrowsing: e.Result = stats.RSafeBrowsing - case dnsfilter.FilteredParental: e.Result = stats.RParental - case dnsfilter.FilteredSafeSearch: e.Result = stats.RSafeSearch - case dnsfilter.FilteredBlockList: fallthrough case dnsfilter.FilteredInvalid: diff --git a/internal/dnsforward/stats_test.go b/internal/dnsforward/stats_test.go new file mode 100644 index 00000000..3b5981bb --- /dev/null +++ b/internal/dnsforward/stats_test.go @@ -0,0 +1,198 @@ +package dnsforward + +import ( + "net" + "testing" + "time" + + "github.com/AdguardTeam/AdGuardHome/internal/dnsfilter" + "github.com/AdguardTeam/AdGuardHome/internal/querylog" + "github.com/AdguardTeam/AdGuardHome/internal/stats" + "github.com/AdguardTeam/dnsproxy/proxy" + "github.com/AdguardTeam/dnsproxy/upstream" + "github.com/miekg/dns" + "github.com/stretchr/testify/assert" +) + +// testQueryLog is a simple querylog.QueryLog implementation for tests. +type testQueryLog struct { + // QueryLog is embedded here simply to make testQueryLog + // a querylog.QueryLog without acctually implementing all methods. + querylog.QueryLog + + lastParams querylog.AddParams +} + +// Add implements the querylog.QueryLog interface for *testQueryLog. +func (l *testQueryLog) Add(p querylog.AddParams) { + l.lastParams = p +} + +// testStats is a simple stats.Stats implementation for tests. +type testStats struct { + // Stats is embedded here simply to make testStats a stats.Stats without + // acctually implementing all methods. + stats.Stats + + lastEntry stats.Entry +} + +// Update implements the stats.Stats interface for *testStats. +func (l *testStats) Update(e stats.Entry) { + l.lastEntry = e +} + +func TestProcessQueryLogsAndStats(t *testing.T) { + testCases := []struct { + name string + proto string + addr net.Addr + clientID string + wantLogProto querylog.ClientProto + wantStatClient string + wantCode resultCode + reason dnsfilter.Reason + wantStatResult stats.Result + }{{ + name: "success_udp", + proto: proxy.ProtoUDP, + addr: &net.UDPAddr{IP: net.IP{1, 2, 3, 4}, Port: 1234}, + clientID: "", + wantLogProto: "", + wantStatClient: "1.2.3.4", + wantCode: resultCodeSuccess, + reason: dnsfilter.NotFilteredNotFound, + wantStatResult: stats.RNotFiltered, + }, { + name: "success_tls_client_id", + proto: proxy.ProtoTLS, + addr: &net.TCPAddr{IP: net.IP{1, 2, 3, 4}, Port: 1234}, + clientID: "cli42", + wantLogProto: querylog.ClientProtoDOT, + wantStatClient: "cli42", + wantCode: resultCodeSuccess, + reason: dnsfilter.NotFilteredNotFound, + wantStatResult: stats.RNotFiltered, + }, { + name: "success_tls", + proto: proxy.ProtoTLS, + addr: &net.TCPAddr{IP: net.IP{1, 2, 3, 4}, Port: 1234}, + clientID: "", + wantLogProto: querylog.ClientProtoDOT, + wantStatClient: "1.2.3.4", + wantCode: resultCodeSuccess, + reason: dnsfilter.NotFilteredNotFound, + wantStatResult: stats.RNotFiltered, + }, { + name: "success_quic", + proto: proxy.ProtoQUIC, + addr: &net.UDPAddr{IP: net.IP{1, 2, 3, 4}, Port: 1234}, + clientID: "", + wantLogProto: querylog.ClientProtoDOQ, + wantStatClient: "1.2.3.4", + wantCode: resultCodeSuccess, + reason: dnsfilter.NotFilteredNotFound, + wantStatResult: stats.RNotFiltered, + }, { + name: "success_https", + proto: proxy.ProtoHTTPS, + addr: &net.TCPAddr{IP: net.IP{1, 2, 3, 4}, Port: 1234}, + clientID: "", + wantLogProto: querylog.ClientProtoDOH, + wantStatClient: "1.2.3.4", + wantCode: resultCodeSuccess, + reason: dnsfilter.NotFilteredNotFound, + wantStatResult: stats.RNotFiltered, + }, { + name: "success_dnscrypt", + proto: proxy.ProtoDNSCrypt, + addr: &net.TCPAddr{IP: net.IP{1, 2, 3, 4}, Port: 1234}, + clientID: "", + wantLogProto: querylog.ClientProtoDNSCrypt, + wantStatClient: "1.2.3.4", + wantCode: resultCodeSuccess, + reason: dnsfilter.NotFilteredNotFound, + wantStatResult: stats.RNotFiltered, + }, { + name: "success_udp_filtered", + proto: proxy.ProtoUDP, + addr: &net.UDPAddr{IP: net.IP{1, 2, 3, 4}, Port: 1234}, + clientID: "", + wantLogProto: "", + wantStatClient: "1.2.3.4", + wantCode: resultCodeSuccess, + reason: dnsfilter.FilteredBlockList, + wantStatResult: stats.RFiltered, + }, { + name: "success_udp_sb", + proto: proxy.ProtoUDP, + addr: &net.UDPAddr{IP: net.IP{1, 2, 3, 4}, Port: 1234}, + clientID: "", + wantLogProto: "", + wantStatClient: "1.2.3.4", + wantCode: resultCodeSuccess, + reason: dnsfilter.FilteredSafeBrowsing, + wantStatResult: stats.RSafeBrowsing, + }, { + name: "success_udp_ss", + proto: proxy.ProtoUDP, + addr: &net.UDPAddr{IP: net.IP{1, 2, 3, 4}, Port: 1234}, + clientID: "", + wantLogProto: "", + wantStatClient: "1.2.3.4", + wantCode: resultCodeSuccess, + reason: dnsfilter.FilteredSafeSearch, + wantStatResult: stats.RSafeSearch, + }, { + name: "success_udp_pc", + proto: proxy.ProtoUDP, + addr: &net.UDPAddr{IP: net.IP{1, 2, 3, 4}, Port: 1234}, + clientID: "", + wantLogProto: "", + wantStatClient: "1.2.3.4", + wantCode: resultCodeSuccess, + reason: dnsfilter.FilteredParental, + wantStatResult: stats.RParental, + }} + + ups, err := upstream.AddressToUpstream("1.1.1.1", upstream.Options{}) + assert.Nil(t, err) + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + req := &dns.Msg{ + Question: []dns.Question{{ + Name: "example.com.", + }}, + } + pctx := &proxy.DNSContext{ + Proto: tc.proto, + Req: req, + Res: &dns.Msg{}, + Addr: tc.addr, + Upstream: ups, + } + + ql := &testQueryLog{} + st := &testStats{} + dctx := &dnsContext{ + srv: &Server{ + queryLog: ql, + stats: st, + }, + proxyCtx: pctx, + startTime: time.Now(), + result: &dnsfilter.Result{ + Reason: tc.reason, + }, + clientID: tc.clientID, + } + + code := processQueryLogsAndStats(dctx) + assert.Equal(t, tc.wantCode, code) + assert.Equal(t, tc.wantLogProto, ql.lastParams.ClientProto) + assert.Equal(t, tc.wantStatClient, st.lastEntry.Client) + assert.Equal(t, tc.wantStatResult, st.lastEntry.Result) + }) + } +} diff --git a/internal/dnsforward/svcbmsg.go b/internal/dnsforward/svcbmsg.go new file mode 100644 index 00000000..2a8c27b4 --- /dev/null +++ b/internal/dnsforward/svcbmsg.go @@ -0,0 +1,168 @@ +package dnsforward + +import ( + "encoding/base64" + "net" + "strconv" + + "github.com/AdguardTeam/golibs/log" + "github.com/AdguardTeam/urlfilter/rules" + "github.com/miekg/dns" +) + +// genAnswerHTTPS returns a properly initialized HTTPS resource record. +// +// See the comment on genAnswerSVCB for a list of current restrictions on +// parameter values. +func (s *Server) genAnswerHTTPS(req *dns.Msg, svcb *rules.DNSSVCB) (ans *dns.HTTPS) { + ans = &dns.HTTPS{ + SVCB: *s.genAnswerSVCB(req, svcb), + } + + ans.Hdr.Rrtype = dns.TypeHTTPS + + return ans +} + +// strToSVCBKey is the string-to-svcb-key mapping. +// +// See https://github.com/miekg/dns/blob/23c4faca9d32b0abbb6e179aa1aadc45ac53a916/svcb.go#L27. +// +// TODO(a.garipov): Propose exporting this API or something similar in the +// github.com/miekg/dns module. +var strToSVCBKey = map[string]dns.SVCBKey{ + "alpn": dns.SVCB_ALPN, + "echconfig": dns.SVCB_ECHCONFIG, + "ipv4hint": dns.SVCB_IPV4HINT, + "ipv6hint": dns.SVCB_IPV6HINT, + "mandatory": dns.SVCB_MANDATORY, + "no-default-alpn": dns.SVCB_NO_DEFAULT_ALPN, + "port": dns.SVCB_PORT, +} + +// svcbKeyHandler is a handler for one SVCB parameter key. +type svcbKeyHandler func(valStr string) (val dns.SVCBKeyValue) + +// svcbKeyHandlers are the supported SVCB parameters handlers. +var svcbKeyHandlers = map[string]svcbKeyHandler{ + "alpn": func(valStr string) (val dns.SVCBKeyValue) { + return &dns.SVCBAlpn{ + Alpn: []string{valStr}, + } + }, + + "echconfig": func(valStr string) (val dns.SVCBKeyValue) { + ech, err := base64.StdEncoding.DecodeString(valStr) + if err != nil { + log.Debug("can't parse svcb/https echconfig: %s; ignoring", err) + + return nil + } + + return &dns.SVCBECHConfig{ + ECH: ech, + } + }, + + "ipv4hint": func(valStr string) (val dns.SVCBKeyValue) { + ip := net.ParseIP(valStr) + if ip4 := ip.To4(); ip == nil || ip4 == nil { + log.Debug("can't parse svcb/https ipv4 hint %q; ignoring", valStr) + + return nil + } + + return &dns.SVCBIPv4Hint{ + Hint: []net.IP{ip}, + } + }, + + "ipv6hint": func(valStr string) (val dns.SVCBKeyValue) { + ip := net.ParseIP(valStr) + if ip == nil { + log.Debug("can't parse svcb/https ipv6 hint %q; ignoring", valStr) + + return nil + } + + return &dns.SVCBIPv6Hint{ + Hint: []net.IP{ip}, + } + }, + + "mandatory": func(valStr string) (val dns.SVCBKeyValue) { + code, ok := strToSVCBKey[valStr] + if !ok { + log.Debug("unknown svcb/https mandatory key %q, ignoring", valStr) + + return nil + } + + return &dns.SVCBMandatory{ + Code: []dns.SVCBKey{code}, + } + }, + + "no-default-alpn": func(_ string) (val dns.SVCBKeyValue) { + return &dns.SVCBNoDefaultAlpn{} + }, + + "port": func(valStr string) (val dns.SVCBKeyValue) { + port64, err := strconv.ParseUint(valStr, 10, 16) + if err != nil { + log.Debug("can't parse svcb/https port: %s; ignoring", err) + + return nil + } + + return &dns.SVCBPort{ + Port: uint16(port64), + } + }, +} + +// genAnswerSVCB returns a properly initialized SVCB resource record. +// +// Currently, there are several restrictions on how the parameters are parsed. +// Firstly, the parsing of non-contiguous values isn't supported. Secondly, the +// parsing of value-lists is not supported either. +// +// ipv4hint=127.0.0.1 // Supported. +// ipv4hint="127.0.0.1" // Unsupported. +// ipv4hint=127.0.0.1,127.0.0.2 // Unsupported. +// ipv4hint="127.0.0.1,127.0.0.2" // Unsupported. +// +// TODO(a.garipov): Support all of these. +func (s *Server) genAnswerSVCB(req *dns.Msg, svcb *rules.DNSSVCB) (ans *dns.SVCB) { + ans = &dns.SVCB{ + Hdr: s.hdr(req, dns.TypeSVCB), + Priority: svcb.Priority, + Target: svcb.Target, + } + if len(svcb.Params) == 0 { + return ans + } + + values := make([]dns.SVCBKeyValue, 0, len(svcb.Params)) + for k, valStr := range svcb.Params { + handler, ok := svcbKeyHandlers[k] + if !ok { + log.Debug("unknown svcb/https key %q, ignoring", k) + + continue + } + + val := handler(valStr) + if val == nil { + continue + } + + values = append(values, val) + } + + if len(values) > 0 { + ans.Value = values + } + + return ans +} diff --git a/internal/dnsforward/svcbmsg_test.go b/internal/dnsforward/svcbmsg_test.go new file mode 100644 index 00000000..392e92ac --- /dev/null +++ b/internal/dnsforward/svcbmsg_test.go @@ -0,0 +1,154 @@ +package dnsforward + +import ( + "net" + "testing" + + "github.com/AdguardTeam/urlfilter/rules" + "github.com/miekg/dns" + "github.com/stretchr/testify/assert" +) + +func TestGenAnswerHTTPS_andSVCB(t *testing.T) { + // Preconditions. + + s := &Server{ + conf: ServerConfig{ + FilteringConfig: FilteringConfig{ + BlockedResponseTTL: 3600, + }, + }, + } + + req := &dns.Msg{ + Question: []dns.Question{{ + Name: "abcd", + }}, + } + + // Constants and helper values. + + const host = "example.com" + const prio = 32 + + ip4 := net.IPv4(127, 0, 0, 1) + ip6 := net.IP{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1} + + // Helper functions. + + dnssvcb := func(key, value string) (svcb *rules.DNSSVCB) { + svcb = &rules.DNSSVCB{ + Target: host, + Priority: prio, + } + + if key == "" { + return svcb + } + + svcb.Params = map[string]string{ + key: value, + } + + return svcb + } + + wantsvcb := func(kv dns.SVCBKeyValue) (want *dns.SVCB) { + want = &dns.SVCB{ + Hdr: s.hdr(req, dns.TypeSVCB), + Priority: prio, + Target: host, + } + + if kv == nil { + return want + } + + want.Value = []dns.SVCBKeyValue{kv} + + return want + } + + // Tests. + + testCases := []struct { + svcb *rules.DNSSVCB + want *dns.SVCB + name string + }{{ + svcb: dnssvcb("", ""), + want: wantsvcb(nil), + name: "no_params", + }, { + svcb: dnssvcb("foo", "bar"), + want: wantsvcb(nil), + name: "invalid", + }, { + svcb: dnssvcb("alpn", "h3"), + want: wantsvcb(&dns.SVCBAlpn{Alpn: []string{"h3"}}), + name: "alpn", + }, { + svcb: dnssvcb("echconfig", "AAAA"), + want: wantsvcb(&dns.SVCBECHConfig{ECH: []byte{0, 0, 0}}), + name: "echconfig", + }, { + svcb: dnssvcb("echconfig", "%BAD%"), + want: wantsvcb(nil), + name: "echconfig_invalid", + }, { + svcb: dnssvcb("ipv4hint", "127.0.0.1"), + want: wantsvcb(&dns.SVCBIPv4Hint{Hint: []net.IP{ip4}}), + name: "ipv4hint", + }, { + svcb: dnssvcb("ipv4hint", "127.0.01"), + want: wantsvcb(nil), + name: "ipv4hint_invalid", + }, { + svcb: dnssvcb("ipv6hint", "::1"), + want: wantsvcb(&dns.SVCBIPv6Hint{Hint: []net.IP{ip6}}), + name: "ipv6hint", + }, { + svcb: dnssvcb("ipv6hint", ":::1"), + want: wantsvcb(nil), + name: "ipv6hint_invalid", + }, { + svcb: dnssvcb("mandatory", "alpn"), + want: wantsvcb(&dns.SVCBMandatory{Code: []dns.SVCBKey{dns.SVCB_ALPN}}), + name: "mandatory", + }, { + svcb: dnssvcb("mandatory", "alpnn"), + want: wantsvcb(nil), + name: "mandatory_invalid", + }, { + svcb: dnssvcb("no-default-alpn", ""), + want: wantsvcb(&dns.SVCBNoDefaultAlpn{}), + name: "no-default-alpn", + }, { + svcb: dnssvcb("port", "8080"), + want: wantsvcb(&dns.SVCBPort{Port: 8080}), + name: "port", + }, { + svcb: dnssvcb("port", "1005008080"), + want: wantsvcb(nil), + name: "port", + }} + + for _, tc := range testCases { + t.Run("https", func(t *testing.T) { + t.Run(tc.name, func(t *testing.T) { + want := &dns.HTTPS{SVCB: *tc.want} + want.Hdr.Rrtype = dns.TypeHTTPS + + got := s.genAnswerHTTPS(req, tc.svcb) + assert.Equal(t, want, got) + }) + }) + + t.Run("svcb", func(t *testing.T) { + t.Run(tc.name, func(t *testing.T) { + got := s.genAnswerSVCB(req, tc.svcb) + assert.Equal(t, tc.want, got) + }) + }) + } +} diff --git a/internal/dnsforward/util.go b/internal/dnsforward/util.go index da87f810..4b57768b 100644 --- a/internal/dnsforward/util.go +++ b/internal/dnsforward/util.go @@ -8,38 +8,8 @@ import ( "github.com/AdguardTeam/golibs/utils" ) -// GetIPString is a helper function that extracts IP address from net.Addr -func GetIPString(addr net.Addr) string { - switch addr := addr.(type) { - case *net.UDPAddr: - return addr.IP.String() - case *net.TCPAddr: - return addr.IP.String() - } - return "" -} - -func stringArrayDup(a []string) []string { - a2 := make([]string, len(a)) - copy(a2, a) - return a2 -} - -// Get IP address from net.Addr object -// Note: we can't use net.SplitHostPort(a.String()) because of IPv6 zone: -// https://github.com/AdguardTeam/AdGuardHome/internal/issues/1261 -func ipFromAddr(a net.Addr) string { - switch addr := a.(type) { - case *net.UDPAddr: - return addr.IP.String() - case *net.TCPAddr: - return addr.IP.String() - } - return "" -} - -// Get IP address from net.Addr -func getIP(addr net.Addr) net.IP { +// IPFromAddr gets IP address from addr. +func IPFromAddr(addr net.Addr) (ip net.IP) { switch addr := addr.(type) { case *net.UDPAddr: return addr.IP @@ -49,6 +19,23 @@ func getIP(addr net.Addr) net.IP { return nil } +// IPStringFromAddr extracts IP address from net.Addr. +// Note: we can't use net.SplitHostPort(a.String()) because of IPv6 zone: +// https://github.com/AdguardTeam/AdGuardHome/internal/issues/1261 +func IPStringFromAddr(addr net.Addr) (ipStr string) { + if ip := IPFromAddr(addr); ip != nil { + return ip.String() + } + + return "" +} + +func stringArrayDup(a []string) []string { + a2 := make([]string, len(a)) + copy(a2, a) + return a2 +} + // Find value in a sorted array func findSorted(ar []string, val string) int { i := sort.SearchStrings(ar, val) diff --git a/internal/home/auth.go b/internal/home/auth.go index 00407fa0..76e7e7b6 100644 --- a/internal/home/auth.go +++ b/internal/home/auth.go @@ -2,13 +2,10 @@ package home import ( "crypto/rand" - "crypto/sha256" "encoding/binary" "encoding/hex" "encoding/json" "fmt" - "math" - "math/big" "net/http" "strings" "sync" @@ -20,8 +17,12 @@ import ( ) const ( - cookieTTL = 365 * 24 // in hours + // cookieTTL is given in hours. + cookieTTL = 365 * 24 sessionCookieName = "agh_session" + + // sessionTokenSize is the length of session token in bytes. + sessionTokenSize = 16 ) type session struct { @@ -59,10 +60,10 @@ func (s *session) deserialize(data []byte) bool { // Auth - global object type Auth struct { db *bbolt.DB - sessions map[string]*session // session name -> session data - lock sync.Mutex + sessions map[string]*session users []User - sessionTTL uint32 // in seconds + lock sync.Mutex + sessionTTL uint32 } // User object @@ -223,24 +224,35 @@ func (a *Auth) removeSession(sess []byte) { log.Debug("Auth: removed session from DB") } -// CheckSession - check if session is valid -// Return 0 if OK; -1 if session doesn't exist; 1 if session has expired -func (a *Auth) CheckSession(sess string) int { +// checkSessionResult is the result of checking a session. +type checkSessionResult int + +// checkSessionResult constants. +const ( + checkSessionOK checkSessionResult = 0 + checkSessionNotFound checkSessionResult = -1 + checkSessionExpired checkSessionResult = 1 +) + +// checkSession checks if the session is valid. +func (a *Auth) checkSession(sess string) (res checkSessionResult) { now := uint32(time.Now().UTC().Unix()) update := false a.lock.Lock() + defer a.lock.Unlock() + s, ok := a.sessions[sess] if !ok { - a.lock.Unlock() - return -1 + return checkSessionNotFound } + if s.expire <= now { delete(a.sessions, sess) key, _ := hex.DecodeString(sess) a.removeSession(key) - a.lock.Unlock() - return 1 + + return checkSessionExpired } newExpire := now + a.sessionTTL @@ -250,8 +262,6 @@ func (a *Auth) CheckSession(sess string) int { s.expire = newExpire } - a.lock.Unlock() - if update { key, _ := hex.DecodeString(sess) if a.storeSession(key, s) { @@ -259,7 +269,7 @@ func (a *Auth) CheckSession(sess string) int { } } - return 0 + return checkSessionOK } // RemoveSession - remove session @@ -276,16 +286,29 @@ type loginJSON struct { Password string `json:"password"` } -func getSession(u *User) ([]byte, error) { - maxSalt := big.NewInt(math.MaxUint32) - salt, err := rand.Int(rand.Reader, maxSalt) +// newSessionToken returns cryptographically secure randomly generated slice of +// bytes of sessionTokenSize length. +// +// TODO(e.burkov): Think about using byte array instead of byte slice. +func newSessionToken() (data []byte, err error) { + randData := make([]byte, sessionTokenSize) + + _, err = rand.Read(randData) if err != nil { return nil, err } - d := []byte(fmt.Sprintf("%s%s%s", salt, u.Name, u.PasswordHash)) - hash := sha256.Sum256(d) - return hash[:], nil + return randData, nil +} + +// cookieTimeFormat is the format to be used in (time.Time).Format for cookie's +// expiry field. +const cookieTimeFormat = "Mon, 02 Jan 2006 15:04:05 GMT" + +// cookieExpiryFormat returns the formatted exp to be used in cookie string. +// It's quite simple for now, but probably will be expanded in the future. +func cookieExpiryFormat(exp time.Time) (formatted string) { + return exp.Format(cookieTimeFormat) } func (a *Auth) httpCookie(req loginJSON) (string, error) { @@ -294,24 +317,23 @@ func (a *Auth) httpCookie(req loginJSON) (string, error) { return "", nil } - sess, err := getSession(&u) + sess, err := newSessionToken() if err != nil { return "", err } now := time.Now().UTC() - expire := now.Add(cookieTTL * time.Hour) - expstr := expire.Format(time.RFC1123) - expstr = expstr[:len(expstr)-len("UTC")] // "UTC" -> "GMT" - expstr += "GMT" - s := session{} - s.userName = u.Name - s.expire = uint32(now.Unix()) + a.sessionTTL - a.addSession(sess, &s) + a.addSession(sess, &session{ + userName: u.Name, + expire: uint32(now.Unix()) + a.sessionTTL, + }) - return fmt.Sprintf("%s=%s; Path=/; HttpOnly; Expires=%s", - sessionCookieName, hex.EncodeToString(sess), expstr), nil + return fmt.Sprintf( + "%s=%s; Path=/; HttpOnly; Expires=%s", + sessionCookieName, hex.EncodeToString(sess), + cookieExpiryFormat(now.Add(cookieTTL*time.Hour)), + ), nil } func handleLogin(w http.ResponseWriter, r *http.Request) { @@ -360,8 +382,8 @@ func handleLogout(w http.ResponseWriter, r *http.Request) { // RegisterAuthHandlers - register handlers func RegisterAuthHandlers() { - Context.mux.Handle("/control/login", postInstallHandler(ensureHandler("POST", handleLogin))) - httpRegister("GET", "/control/logout", handleLogout) + Context.mux.Handle("/control/login", postInstallHandler(ensureHandler(http.MethodPost, handleLogin))) + httpRegister(http.MethodGet, "/control/logout", handleLogout) } func parseCookie(cookie string) string { @@ -392,8 +414,8 @@ func optionalAuthThird(w http.ResponseWriter, r *http.Request) (authFirst bool) ok = true } else if err == nil { - r := Context.auth.CheckSession(cookie.Value) - if r == 0 { + r := Context.auth.checkSession(cookie.Value) + if r == checkSessionOK { ok = true } else if r < 0 { log.Debug("Auth: invalid cookie value: %s", cookie) @@ -434,12 +456,13 @@ func optionalAuth(handler func(http.ResponseWriter, *http.Request)) func(http.Re authRequired := Context.auth != nil && Context.auth.AuthRequired() cookie, err := r.Cookie(sessionCookieName) if authRequired && err == nil { - r := Context.auth.CheckSession(cookie.Value) - if r == 0 { + r := Context.auth.checkSession(cookie.Value) + if r == checkSessionOK { w.Header().Set("Location", "/") w.WriteHeader(http.StatusFound) + return - } else if r < 0 { + } else if r == checkSessionNotFound { log.Debug("Auth: invalid cookie value: %s", cookie) } } @@ -503,32 +526,34 @@ func (a *Auth) UserFind(login, password string) User { return User{} } -// GetCurrentUser - get the current user -func (a *Auth) GetCurrentUser(r *http.Request) User { +// getCurrentUser returns the current user. It returns an empty User if the +// user is not found. +func (a *Auth) getCurrentUser(r *http.Request) User { cookie, err := r.Cookie(sessionCookieName) if err != nil { - // there's no Cookie, check Basic authentication + // There's no Cookie, check Basic authentication. user, pass, ok := r.BasicAuth() if ok { - u := Context.auth.UserFind(user, pass) - return u + return Context.auth.UserFind(user, pass) } + return User{} } a.lock.Lock() + defer a.lock.Unlock() + s, ok := a.sessions[cookie.Value] if !ok { - a.lock.Unlock() return User{} } + for _, u := range a.users { if u.Name == s.userName { - a.lock.Unlock() return u } } - a.lock.Unlock() + return User{} } diff --git a/internal/home/auth_test.go b/internal/home/auth_test.go index 25db2dd6..7dbbf3c6 100644 --- a/internal/home/auth_test.go +++ b/internal/home/auth_test.go @@ -1,6 +1,8 @@ package home import ( + "bytes" + "crypto/rand" "encoding/hex" "net/http" "net/url" @@ -9,12 +11,13 @@ import ( "testing" "time" - "github.com/AdguardTeam/AdGuardHome/internal/testutil" + "github.com/AdguardTeam/AdGuardHome/internal/aghtest" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) func TestMain(m *testing.M) { - testutil.DiscardLogOutput(m) + aghtest.DiscardLogOutput(m) } func prepareTestDir() string { @@ -24,24 +27,44 @@ func prepareTestDir() string { return dir } +func TestNewSessionToken(t *testing.T) { + // Successful case. + token, err := newSessionToken() + require.Nil(t, err) + assert.Len(t, token, sessionTokenSize) + + // Break the rand.Reader. + prevReader := rand.Reader + t.Cleanup(func() { + rand.Reader = prevReader + }) + rand.Reader = &bytes.Buffer{} + + // Unsuccessful case. + token, err = newSessionToken() + require.NotNil(t, err) + assert.Empty(t, token) +} + func TestAuth(t *testing.T) { dir := prepareTestDir() - defer func() { _ = os.RemoveAll(dir) }() + t.Cleanup(func() { _ = os.RemoveAll(dir) }) fn := filepath.Join(dir, "sessions.db") - users := []User{ - {Name: "name", PasswordHash: "$2y$05$..vyzAECIhJPfaQiOK17IukcQnqEgKJHy0iETyYqxn3YXJl8yZuo2"}, - } + users := []User{{ + Name: "name", + PasswordHash: "$2y$05$..vyzAECIhJPfaQiOK17IukcQnqEgKJHy0iETyYqxn3YXJl8yZuo2", + }} a := InitAuth(fn, nil, 60) s := session{} user := User{Name: "name"} a.UserAdd(&user, "password") - assert.True(t, a.CheckSession("notfound") == -1) + assert.Equal(t, checkSessionNotFound, a.checkSession("notfound")) a.RemoveSession("notfound") - sess, err := getSession(&users[0]) + sess, err := newSessionToken() assert.Nil(t, err) sessStr := hex.EncodeToString(sess) @@ -49,13 +72,13 @@ func TestAuth(t *testing.T) { // check expiration s.expire = uint32(now) a.addSession(sess, &s) - assert.True(t, a.CheckSession(sessStr) == 1) + assert.Equal(t, checkSessionExpired, a.checkSession(sessStr)) // add session with TTL = 2 sec s = session{} s.expire = uint32(time.Now().UTC().Unix() + 2) a.addSession(sess, &s) - assert.True(t, a.CheckSession(sessStr) == 0) + assert.Equal(t, checkSessionOK, a.checkSession(sessStr)) a.Close() @@ -63,23 +86,22 @@ func TestAuth(t *testing.T) { a = InitAuth(fn, users, 60) // the session is still alive - assert.True(t, a.CheckSession(sessStr) == 0) - // reset our expiration time because CheckSession() has just updated it + assert.Equal(t, checkSessionOK, a.checkSession(sessStr)) + // reset our expiration time because checkSession() has just updated it s.expire = uint32(time.Now().UTC().Unix() + 2) a.storeSession(sess, &s) a.Close() u := a.UserFind("name", "password") - assert.True(t, len(u.Name) != 0) + assert.NotEmpty(t, u.Name) time.Sleep(3 * time.Second) // load and remove expired sessions a = InitAuth(fn, users, 60) - assert.True(t, a.CheckSession(sessStr) == -1) + assert.Equal(t, checkSessionNotFound, a.checkSession(sessStr)) a.Close() - os.Remove(fn) } // implements http.ResponseWriter @@ -111,7 +133,7 @@ func TestAuthHTTP(t *testing.T) { Context.auth = InitAuth(fn, users, 60) handlerCalled := false - handler := func(w http.ResponseWriter, r *http.Request) { + handler := func(_ http.ResponseWriter, _ *http.Request) { handlerCalled = true } handler2 := optionalAuth(handler) @@ -119,15 +141,15 @@ func TestAuthHTTP(t *testing.T) { w.hdr = make(http.Header) r := http.Request{} r.Header = make(http.Header) - r.Method = "GET" + r.Method = http.MethodGet // get / - we're redirected to login page r.URL = &url.URL{Path: "/"} handlerCalled = false handler2(&w, &r) - assert.True(t, w.statusCode == http.StatusFound) - assert.True(t, w.hdr.Get("Location") != "") - assert.True(t, !handlerCalled) + assert.Equal(t, http.StatusFound, w.statusCode) + assert.NotEmpty(t, w.hdr.Get("Location")) + assert.False(t, handlerCalled) // go to login page loginURL := w.hdr.Get("Location") @@ -139,7 +161,7 @@ func TestAuthHTTP(t *testing.T) { // perform login cookie, err := Context.auth.httpCookie(loginJSON{Name: "name", Password: "password"}) assert.Nil(t, err) - assert.True(t, cookie != "") + assert.NotEmpty(t, cookie) // get / handler2 = optionalAuth(handler) @@ -168,8 +190,8 @@ func TestAuthHTTP(t *testing.T) { r.URL = &url.URL{Path: loginURL} handlerCalled = false handler2(&w, &r) - assert.True(t, w.hdr.Get("Location") != "") - assert.True(t, !handlerCalled) + assert.NotEmpty(t, w.hdr.Get("Location")) + assert.False(t, handlerCalled) r.Header.Del("Cookie") // get login page with an invalid cookie diff --git a/internal/home/authglinet_test.go b/internal/home/authglinet_test.go index df5e3342..70bb6636 100644 --- a/internal/home/authglinet_test.go +++ b/internal/home/authglinet_test.go @@ -36,7 +36,7 @@ func TestAuthGL(t *testing.T) { binary.BigEndian.PutUint32(data, tval) } assert.Nil(t, ioutil.WriteFile(glFilePrefix+"test", data, 0o644)) - r, _ := http.NewRequest("GET", "http://localhost/", nil) + r, _ := http.NewRequest(http.MethodGet, "http://localhost/", nil) r.AddCookie(&http.Cookie{Name: glCookieName, Value: "test"}) assert.True(t, glProcessCookie(r)) GLMode = false diff --git a/internal/home/clients.go b/internal/home/clients.go index 3c6bfa48..c3eb366f 100644 --- a/internal/home/clients.go +++ b/internal/home/clients.go @@ -11,23 +11,21 @@ import ( "sync" "time" - "github.com/AdguardTeam/dnsproxy/proxy" - + "github.com/AdguardTeam/AdGuardHome/internal/agherr" "github.com/AdguardTeam/AdGuardHome/internal/dhcpd" "github.com/AdguardTeam/AdGuardHome/internal/dnsfilter" "github.com/AdguardTeam/AdGuardHome/internal/dnsforward" "github.com/AdguardTeam/AdGuardHome/internal/util" + "github.com/AdguardTeam/dnsproxy/proxy" "github.com/AdguardTeam/golibs/log" "github.com/AdguardTeam/golibs/utils" ) -const ( - clientsUpdatePeriod = 10 * time.Minute -) +const clientsUpdatePeriod = 10 * time.Minute var webHandlersRegistered = false -// Client information +// Client contains information about persistent clients. type Client struct { IDs []string Tags []string @@ -52,14 +50,13 @@ type Client struct { type clientSource uint -// Client sources +// Client sources. The order determines the priority. const ( - // Priority: etc/hosts > DHCP > ARP > rDNS > WHOIS - ClientSourceWHOIS clientSource = iota // from WHOIS - ClientSourceRDNS // from rDNS - ClientSourceDHCP // from DHCP - ClientSourceARP // from 'arp -a' - ClientSourceHostsFile // from /etc/hosts + ClientSourceWHOIS clientSource = iota + ClientSourceRDNS + ClientSourceDHCP + ClientSourceARP + ClientSourceHostsFile ) // ClientHost information @@ -70,8 +67,10 @@ type ClientHost struct { } type clientsContainer struct { + // TODO(a.garipov): Perhaps use a number of separate indices for + // different types (string, net.IP, and so on). list map[string]*Client // name -> client - idIndex map[string]*Client // IP -> client + idIndex map[string]*Client // ID -> client ipHost map[string]*ClientHost // IP -> Hostname lock sync.Mutex @@ -156,7 +155,7 @@ func (clients *clientsContainer) tagKnown(tag string) bool { func (clients *clientsContainer) addFromConfig(objects []clientObject) { for _, cy := range objects { - cli := Client{ + cli := &Client{ Name: cy.Name, IDs: cy.IDs, UseOwnSettings: !cy.UseGlobalSettings, @@ -172,7 +171,7 @@ func (clients *clientsContainer) addFromConfig(objects []clientObject) { for _, s := range cy.BlockedServices { if !dnsfilter.BlockedSvcKnown(s) { - log.Debug("Clients: skipping unknown blocked-service %q", s) + log.Debug("clients: skipping unknown blocked-service %q", s) continue } cli.BlockedServices = append(cli.BlockedServices, s) @@ -180,7 +179,7 @@ func (clients *clientsContainer) addFromConfig(objects []clientObject) { for _, t := range cy.Tags { if !clients.tagKnown(t) { - log.Debug("Clients: skipping unknown tag %q", t) + log.Debug("clients: skipping unknown tag %q", t) continue } cli.Tags = append(cli.Tags, t) @@ -208,10 +207,10 @@ func (clients *clientsContainer) WriteDiskConfig(objects *[]clientObject) { UseGlobalBlockedServices: !cli.UseOwnBlockedServices, } - cy.Tags = stringArrayDup(cli.Tags) - cy.IDs = stringArrayDup(cli.IDs) - cy.BlockedServices = stringArrayDup(cli.BlockedServices) - cy.Upstreams = stringArrayDup(cli.Upstreams) + cy.Tags = copyStrings(cli.Tags) + cy.IDs = copyStrings(cli.IDs) + cy.BlockedServices = copyStrings(cli.BlockedServices) + cy.Upstreams = copyStrings(cli.Upstreams) *objects = append(*objects, cy) } @@ -238,45 +237,44 @@ func (clients *clientsContainer) onHostsChanged() { clients.addFromHostsFile() } -// Exists checks if client with this IP already exists -func (clients *clientsContainer) Exists(ip string, source clientSource) bool { +// Exists checks if client with this ID already exists. +func (clients *clientsContainer) Exists(id string, source clientSource) (ok bool) { clients.lock.Lock() defer clients.lock.Unlock() - _, ok := clients.findByIP(ip) + _, ok = clients.findLocked(id) if ok { return true } - ch, ok := clients.ipHost[ip] + var ch *ClientHost + ch, ok = clients.ipHost[id] if !ok { return false } - if source > ch.Source { - return false // we're going to overwrite this client's info with a stronger source - } - return true + + // Return false if the new source has higher priority. + return source <= ch.Source } -func stringArrayDup(a []string) []string { - a2 := make([]string, len(a)) - copy(a2, a) - return a2 +func copyStrings(a []string) (b []string) { + return append(b, a...) } -// Find searches for a client by IP -func (clients *clientsContainer) Find(ip string) (Client, bool) { +// Find searches for a client by its ID. +func (clients *clientsContainer) Find(id string) (c *Client, ok bool) { clients.lock.Lock() defer clients.lock.Unlock() - c, ok := clients.findByIP(ip) + c, ok = clients.findLocked(id) if !ok { - return Client{}, false + return nil, false } - c.IDs = stringArrayDup(c.IDs) - c.Tags = stringArrayDup(c.Tags) - c.BlockedServices = stringArrayDup(c.BlockedServices) - c.Upstreams = stringArrayDup(c.Upstreams) + + c.IDs = copyStrings(c.IDs) + c.Tags = copyStrings(c.Tags) + c.BlockedServices = copyStrings(c.BlockedServices) + c.Upstreams = copyStrings(c.Upstreams) return c, true } @@ -287,7 +285,7 @@ func (clients *clientsContainer) FindUpstreams(ip string) *proxy.UpstreamConfig clients.lock.Lock() defer clients.lock.Unlock() - c, ok := clients.findByIP(ip) + c, ok := clients.findLocked(ip) if !ok { return nil } @@ -306,16 +304,16 @@ func (clients *clientsContainer) FindUpstreams(ip string) *proxy.UpstreamConfig return c.upstreamConfig } -// Find searches for a client by IP (and does not lock anything) -func (clients *clientsContainer) findByIP(ip string) (Client, bool) { - ipAddr := net.ParseIP(ip) - if ipAddr == nil { - return Client{}, false +// findLocked searches for a client by its ID. For internal use only. +func (clients *clientsContainer) findLocked(id string) (c *Client, ok bool) { + c, ok = clients.idIndex[id] + if ok { + return c, true } - c, ok := clients.idIndex[ip] - if ok { - return *c, true + ip := net.ParseIP(id) + if ip == nil { + return nil, false } for _, c = range clients.list { @@ -324,32 +322,36 @@ func (clients *clientsContainer) findByIP(ip string) (Client, bool) { if err != nil { continue } - if ipnet.Contains(ipAddr) { - return *c, true + + if ipnet.Contains(ip) { + return c, true } } } if clients.dhcpServer == nil { - return Client{}, false + return nil, false } - macFound := clients.dhcpServer.FindMACbyIP(ipAddr) + + macFound := clients.dhcpServer.FindMACbyIP(ip) if macFound == nil { - return Client{}, false + return nil, false } + for _, c = range clients.list { for _, id := range c.IDs { hwAddr, err := net.ParseMAC(id) if err != nil { continue } + if bytes.Equal(hwAddr, macFound) { - return *c, true + return c, true } } } - return Client{}, false + return nil, false } // FindAutoClient - search for an auto-client by IP @@ -369,44 +371,47 @@ func (clients *clientsContainer) FindAutoClient(ip string) (ClientHost, bool) { return ClientHost{}, false } -// Check if Client object's fields are correct -func (clients *clientsContainer) check(c *Client) error { - if len(c.Name) == 0 { - return fmt.Errorf("invalid Name") - } - - if len(c.IDs) == 0 { - return fmt.Errorf("id required") +// check validates the client. +func (clients *clientsContainer) check(c *Client) (err error) { + switch { + case c == nil: + return agherr.Error("client is nil") + case c.Name == "": + return agherr.Error("invalid name") + case len(c.IDs) == 0: + return agherr.Error("id required") + default: + // Go on. } for i, id := range c.IDs { - ip := net.ParseIP(id) - if ip != nil { - c.IDs[i] = ip.String() // normalize IP address - continue + // Normalize structured data. + var ip net.IP + var ipnet *net.IPNet + var mac net.HardwareAddr + if ip = net.ParseIP(id); ip != nil { + c.IDs[i] = ip.String() + } else if ip, ipnet, err = net.ParseCIDR(id); err == nil { + ipnet.IP = ip + c.IDs[i] = ipnet.String() + } else if mac, err = net.ParseMAC(id); err == nil { + c.IDs[i] = mac.String() + } else if err = dnsforward.ValidateClientID(id); err == nil { + c.IDs[i] = id + } else { + return fmt.Errorf("invalid client id at index %d: %q", i, id) } - - _, _, err := net.ParseCIDR(id) - if err == nil { - continue - } - - _, err = net.ParseMAC(id) - if err == nil { - continue - } - - return fmt.Errorf("invalid ID: %s", id) } for _, t := range c.Tags { if !clients.tagKnown(t) { - return fmt.Errorf("invalid tag: %s", t) + return fmt.Errorf("invalid tag: %q", t) } } + sort.Strings(c.Tags) - err := dnsforward.ValidateUpstreams(c.Upstreams) + err = dnsforward.ValidateUpstreams(c.Upstreams) if err != nil { return fmt.Errorf("invalid upstream servers: %w", err) } @@ -414,49 +419,52 @@ func (clients *clientsContainer) check(c *Client) error { return nil } -// Add a new client object -// Return true: success; false: client exists. -func (clients *clientsContainer) Add(c Client) (bool, error) { - e := clients.check(&c) - if e != nil { - return false, e +// Add adds a new client object. ok is false if such client already exists or +// if an error occurred. +func (clients *clientsContainer) Add(c *Client) (ok bool, err error) { + err = clients.check(c) + if err != nil { + return false, err } clients.lock.Lock() defer clients.lock.Unlock() // check Name index - _, ok := clients.list[c.Name] + _, ok = clients.list[c.Name] if ok { return false, nil } // check ID index for _, id := range c.IDs { - c2, ok := clients.idIndex[id] + var c2 *Client + c2, ok = clients.idIndex[id] if ok { - return false, fmt.Errorf("another client uses the same ID (%s): %s", id, c2.Name) + return false, fmt.Errorf("another client uses the same ID (%q): %q", id, c2.Name) } } // update Name index - clients.list[c.Name] = &c + clients.list[c.Name] = c // update ID index for _, id := range c.IDs { - clients.idIndex[id] = &c + clients.idIndex[id] = c } - log.Debug("Clients: added %q: ID:%v [%d]", c.Name, c.IDs, len(clients.list)) + log.Debug("clients: added %q: ID:%q [%d]", c.Name, c.IDs, len(clients.list)) + return true, nil } -// Del removes a client -func (clients *clientsContainer) Del(name string) bool { +// Del removes a client. ok is false if there is no such client. +func (clients *clientsContainer) Del(name string) (ok bool) { clients.lock.Lock() defer clients.lock.Unlock() - c, ok := clients.list[name] + var c *Client + c, ok = clients.list[name] if !ok { return false } @@ -468,25 +476,28 @@ func (clients *clientsContainer) Del(name string) bool { for _, id := range c.IDs { delete(clients.idIndex, id) } + return true } -// Return TRUE if arrays are equal -func arraysEqual(a, b []string) bool { +// equalStringSlices returns true if the slices are equal. +func equalStringSlices(a, b []string) (ok bool) { if len(a) != len(b) { return false } - for i := 0; i != len(a); i++ { + + for i := range a { if a[i] != b[i] { return false } } + return true } -// Update a client -func (clients *clientsContainer) Update(name string, c Client) error { - err := clients.check(&c) +// Update updates a client by its name. +func (clients *clientsContainer) Update(name string, c *Client) (err error) { + err = clients.check(c) if err != nil { return err } @@ -494,65 +505,69 @@ func (clients *clientsContainer) Update(name string, c Client) error { clients.lock.Lock() defer clients.lock.Unlock() - old, ok := clients.list[name] + prev, ok := clients.list[name] if !ok { - return fmt.Errorf("client not found") + return agherr.Error("client not found") } // check Name index - if old.Name != c.Name { + if prev.Name != c.Name { _, ok = clients.list[c.Name] if ok { - return fmt.Errorf("client already exists") + return agherr.Error("client already exists") } } // check IP index - if !arraysEqual(old.IDs, c.IDs) { + if !equalStringSlices(prev.IDs, c.IDs) { for _, id := range c.IDs { c2, ok := clients.idIndex[id] - if ok && c2 != old { - return fmt.Errorf("another client uses the same ID (%s): %s", id, c2.Name) + if ok && c2 != prev { + return fmt.Errorf("another client uses the same ID (%q): %q", id, c2.Name) } } // update ID index - for _, id := range old.IDs { + for _, id := range prev.IDs { delete(clients.idIndex, id) } for _, id := range c.IDs { - clients.idIndex[id] = old + clients.idIndex[id] = prev } } // update Name index - if old.Name != c.Name { - delete(clients.list, old.Name) - clients.list[c.Name] = old + if prev.Name != c.Name { + delete(clients.list, prev.Name) + clients.list[c.Name] = prev } // update upstreams cache c.upstreamConfig = nil - *old = c + *prev = *c + return nil } -// SetWhoisInfo - associate WHOIS information with a client +// SetWhoisInfo sets the WHOIS information for a client. +// +// TODO(a.garipov): Perhaps replace [][]string with map[string]string. func (clients *clientsContainer) SetWhoisInfo(ip string, info [][]string) { clients.lock.Lock() defer clients.lock.Unlock() - _, ok := clients.findByIP(ip) + _, ok := clients.findLocked(ip) if ok { - log.Debug("Clients: client for %s is already created, ignore WHOIS info", ip) + log.Debug("clients: client for %s is already created, ignore whois info", ip) return } ch, ok := clients.ipHost[ip] if ok { ch.WhoisInfo = info - log.Debug("Clients: set WHOIS info for auto-client %s: %v", ch.Host, ch.WhoisInfo) + log.Debug("clients: set whois info for auto-client %s: %q", ch.Host, info) + return } @@ -562,31 +577,33 @@ func (clients *clientsContainer) SetWhoisInfo(ip string, info [][]string) { } ch.WhoisInfo = info clients.ipHost[ip] = ch - log.Debug("Clients: set WHOIS info for auto-client with IP %s: %v", ip, ch.WhoisInfo) + log.Debug("clients: set whois info for auto-client with IP %s: %q", ip, info) } -// AddHost adds new IP -> Host pair -// Use priority of the source (etc/hosts > ARP > rDNS) -// so we overwrite existing entries with an equal or higher priority -func (clients *clientsContainer) AddHost(ip, host string, source clientSource) (bool, error) { +// AddHost adds a new IP-hostname pairing. The priorities of the sources is +// taken into account. ok is true if the pairing was added. +func (clients *clientsContainer) AddHost(ip, host string, src clientSource) (ok bool, err error) { clients.lock.Lock() - b := clients.addHost(ip, host, source) + ok = clients.addHostLocked(ip, host, src) clients.lock.Unlock() - return b, nil + + return ok, nil } -func (clients *clientsContainer) addHost(ip, host string, source clientSource) (addedNew bool) { - ch, ok := clients.ipHost[ip] +// addHostLocked adds a new IP-hostname pairing. For internal use only. +func (clients *clientsContainer) addHostLocked(ip, host string, src clientSource) (ok bool) { + var ch *ClientHost + ch, ok = clients.ipHost[ip] if ok { - if ch.Source > source { + if ch.Source > src { return false } - ch.Source = source + ch.Source = src } else { ch = &ClientHost{ Host: host, - Source: source, + Source: src, } clients.ipHost[ip] = ch @@ -597,11 +614,11 @@ func (clients *clientsContainer) addHost(ip, host string, source clientSource) ( return true } -// Remove all entries that match the specified source -func (clients *clientsContainer) rmHosts(source clientSource) { +// rmHostsBySrc removes all entries that match the specified source. +func (clients *clientsContainer) rmHostsBySrc(src clientSource) { n := 0 for k, v := range clients.ipHost { - if v.Source == source { + if v.Source == src { delete(clients.ipHost, k) n++ } @@ -610,19 +627,20 @@ func (clients *clientsContainer) rmHosts(source clientSource) { log.Debug("clients: removed %d client aliases", n) } -// addFromHostsFile fills the clients hosts list from the system's hosts files. +// addFromHostsFile fills the client-hostname pairing index from the system's +// hosts files. func (clients *clientsContainer) addFromHostsFile() { hosts := clients.autoHosts.List() clients.lock.Lock() defer clients.lock.Unlock() - clients.rmHosts(ClientSourceHostsFile) + clients.rmHostsBySrc(ClientSourceHostsFile) n := 0 for ip, names := range hosts { for _, name := range names { - ok := clients.addHost(ip, name, ClientSourceHostsFile) + ok := clients.addHostLocked(ip, name, ClientSourceHostsFile) if ok { n++ } @@ -632,31 +650,31 @@ func (clients *clientsContainer) addFromHostsFile() { log.Debug("Clients: added %d client aliases from system hosts-file", n) } -// Add IP -> Host pairs from the system's `arp -a` command output -// The command's output is: -// HOST (IP) at MAC on IFACE +// addFromSystemARP adds the IP-hostname pairings from the output of the arp -a +// command. func (clients *clientsContainer) addFromSystemARP() { if runtime.GOOS == "windows" { return } cmd := exec.Command("arp", "-a") - log.Tracef("executing %s %v", cmd.Path, cmd.Args) + log.Tracef("executing %q %q", cmd.Path, cmd.Args) data, err := cmd.Output() if err != nil || cmd.ProcessState.ExitCode() != 0 { - log.Debug("command %s has failed: %v code:%d", + log.Debug("command %q has failed: %q code:%d", cmd.Path, err, cmd.ProcessState.ExitCode()) return } clients.lock.Lock() defer clients.lock.Unlock() - clients.rmHosts(ClientSourceARP) + + clients.rmHostsBySrc(ClientSourceARP) n := 0 + // TODO(a.garipov): Rewrite to use bufio.Scanner. lines := strings.Split(string(data), "\n") for _, ln := range lines { - open := strings.Index(ln, " (") close := strings.Index(ln, ") ") if open == -1 || close == -1 || open >= close { @@ -669,16 +687,17 @@ func (clients *clientsContainer) addFromSystemARP() { continue } - ok := clients.addHost(ip, host, ClientSourceARP) + ok := clients.addHostLocked(ip, host, ClientSourceARP) if ok { n++ } } - log.Debug("Clients: added %d client aliases from 'arp -a' command output", n) + log.Debug("clients: added %d client aliases from 'arp -a' command output", n) } -// Add clients from DHCP that have non-empty Hostname property +// addFromDHCP adds the clients that have a non-empty hostname from the DHCP +// server. func (clients *clientsContainer) addFromDHCP() { if clients.dhcpServer == nil { return @@ -687,18 +706,20 @@ func (clients *clientsContainer) addFromDHCP() { clients.lock.Lock() defer clients.lock.Unlock() - clients.rmHosts(ClientSourceDHCP) + clients.rmHostsBySrc(ClientSourceDHCP) leases := clients.dhcpServer.Leases(dhcpd.LeasesAll) n := 0 for _, l := range leases { - if len(l.Hostname) == 0 { + if l.Hostname == "" { continue } - ok := clients.addHost(l.IP.String(), l.Hostname, ClientSourceDHCP) + + ok := clients.addHostLocked(l.IP.String(), l.Hostname, ClientSourceDHCP) if ok { n++ } } - log.Debug("Clients: added %d client aliases from DHCP", n) + + log.Debug("clients: added %d client aliases from dhcp", n) } diff --git a/internal/home/clients_test.go b/internal/home/clients_test.go index 9268c08f..a098bf4c 100644 --- a/internal/home/clients_test.go +++ b/internal/home/clients_test.go @@ -18,32 +18,35 @@ func TestClients(t *testing.T) { clients.Init(nil, nil, nil) t.Run("add_success", func(t *testing.T) { - c := Client{ + c := &Client{ IDs: []string{"1.1.1.1", "1:2:3::4", "aa:aa:aa:aa:aa:aa"}, Name: "client1", } - b, err := clients.Add(c) - assert.True(t, b) + ok, err := clients.Add(c) + assert.True(t, ok) assert.Nil(t, err) - c = Client{ + c = &Client{ IDs: []string{"2.2.2.2"}, Name: "client2", } - b, err = clients.Add(c) - assert.True(t, b) + ok, err = clients.Add(c) + assert.True(t, ok) assert.Nil(t, err) - c, b = clients.Find("1.1.1.1") - assert.True(t, b && c.Name == "client1") + c, ok = clients.Find("1.1.1.1") + assert.True(t, ok) + assert.Equal(t, "client1", c.Name) - c, b = clients.Find("1:2:3::4") - assert.True(t, b && c.Name == "client1") + c, ok = clients.Find("1:2:3::4") + assert.True(t, ok) + assert.Equal(t, "client1", c.Name) - c, b = clients.Find("2.2.2.2") - assert.True(t, b && c.Name == "client2") + c, ok = clients.Find("2.2.2.2") + assert.True(t, ok) + assert.Equal(t, "client2", c.Name) assert.True(t, !clients.Exists("1.2.3.4", ClientSourceHostsFile)) assert.True(t, clients.Exists("1.1.1.1", ClientSourceHostsFile)) @@ -51,29 +54,29 @@ func TestClients(t *testing.T) { }) t.Run("add_fail_name", func(t *testing.T) { - c := Client{ + c := &Client{ IDs: []string{"1.2.3.5"}, Name: "client1", } - b, err := clients.Add(c) - assert.False(t, b) + ok, err := clients.Add(c) + assert.False(t, ok) assert.Nil(t, err) }) t.Run("add_fail_ip", func(t *testing.T) { - c := Client{ + c := &Client{ IDs: []string{"2.2.2.2"}, Name: "client3", } - b, err := clients.Add(c) - assert.False(t, b) + ok, err := clients.Add(c) + assert.False(t, ok) assert.NotNil(t, err) }) t.Run("update_fail_name", func(t *testing.T) { - c := Client{ + c := &Client{ IDs: []string{"1.2.3.0"}, Name: "client3", } @@ -81,7 +84,7 @@ func TestClients(t *testing.T) { err := clients.Update("client3", c) assert.NotNil(t, err) - c = Client{ + c = &Client{ IDs: []string{"1.2.3.0"}, Name: "client2", } @@ -91,7 +94,7 @@ func TestClients(t *testing.T) { }) t.Run("update_fail_ip", func(t *testing.T) { - c := Client{ + c := &Client{ IDs: []string{"2.2.2.2"}, Name: "client1", } @@ -101,7 +104,7 @@ func TestClients(t *testing.T) { }) t.Run("update_success", func(t *testing.T) { - c := Client{ + c := &Client{ IDs: []string{"1.1.1.2"}, Name: "client1", } @@ -112,7 +115,7 @@ func TestClients(t *testing.T) { assert.True(t, !clients.Exists("1.1.1.1", ClientSourceHostsFile)) assert.True(t, clients.Exists("1.1.1.2", ClientSourceHostsFile)) - c = Client{ + c = &Client{ IDs: []string{"1.1.1.2"}, Name: "client1-renamed", UseOwnSettings: true, @@ -121,50 +124,52 @@ func TestClients(t *testing.T) { err = clients.Update("client1", c) assert.Nil(t, err) - c, b := clients.Find("1.1.1.2") - assert.True(t, b) - assert.True(t, c.Name == "client1-renamed") - assert.True(t, c.IDs[0] == "1.1.1.2") + c, ok := clients.Find("1.1.1.2") + assert.True(t, ok) + assert.Equal(t, "client1-renamed", c.Name) assert.True(t, c.UseOwnSettings) assert.Nil(t, clients.list["client1"]) + if assert.Len(t, c.IDs, 1) { + assert.Equal(t, "1.1.1.2", c.IDs[0]) + } }) t.Run("del_success", func(t *testing.T) { - b := clients.Del("client1-renamed") - assert.True(t, b) + ok := clients.Del("client1-renamed") + assert.True(t, ok) assert.False(t, clients.Exists("1.1.1.2", ClientSourceHostsFile)) }) t.Run("del_fail", func(t *testing.T) { - b := clients.Del("client3") - assert.False(t, b) + ok := clients.Del("client3") + assert.False(t, ok) }) t.Run("addhost_success", func(t *testing.T) { - b, err := clients.AddHost("1.1.1.1", "host", ClientSourceARP) - assert.True(t, b) + ok, err := clients.AddHost("1.1.1.1", "host", ClientSourceARP) + assert.True(t, ok) assert.Nil(t, err) - b, err = clients.AddHost("1.1.1.1", "host2", ClientSourceARP) - assert.True(t, b) + ok, err = clients.AddHost("1.1.1.1", "host2", ClientSourceARP) + assert.True(t, ok) assert.Nil(t, err) - b, err = clients.AddHost("1.1.1.1", "host3", ClientSourceHostsFile) - assert.True(t, b) + ok, err = clients.AddHost("1.1.1.1", "host3", ClientSourceHostsFile) + assert.True(t, ok) assert.Nil(t, err) assert.True(t, clients.Exists("1.1.1.1", ClientSourceHostsFile)) }) t.Run("addhost_fail", func(t *testing.T) { - b, err := clients.AddHost("1.1.1.1", "host1", ClientSourceRDNS) - assert.False(t, b) + ok, err := clients.AddHost("1.1.1.1", "host1", ClientSourceRDNS) + assert.False(t, ok) assert.Nil(t, err) }) } func TestClientsWhois(t *testing.T) { - var c Client + var c *Client clients := clientsContainer{} clients.testing = true clients.Init(nil, nil, nil) @@ -172,26 +177,36 @@ func TestClientsWhois(t *testing.T) { whois := [][]string{{"orgname", "orgname-val"}, {"country", "country-val"}} // set whois info on new client clients.SetWhoisInfo("1.1.1.255", whois) - assert.True(t, clients.ipHost["1.1.1.255"].WhoisInfo[0][1] == "orgname-val") + if assert.NotNil(t, clients.ipHost["1.1.1.255"]) { + h := clients.ipHost["1.1.1.255"] + if assert.Len(t, h.WhoisInfo, 2) && assert.Len(t, h.WhoisInfo[0], 2) { + assert.Equal(t, "orgname-val", h.WhoisInfo[0][1]) + } + } // set whois info on existing auto-client _, _ = clients.AddHost("1.1.1.1", "host", ClientSourceRDNS) clients.SetWhoisInfo("1.1.1.1", whois) - assert.True(t, clients.ipHost["1.1.1.1"].WhoisInfo[0][1] == "orgname-val") + if assert.NotNil(t, clients.ipHost["1.1.1.1"]) { + h := clients.ipHost["1.1.1.1"] + if assert.Len(t, h.WhoisInfo, 2) && assert.Len(t, h.WhoisInfo[0], 2) { + assert.Equal(t, "orgname-val", h.WhoisInfo[0][1]) + } + } // Check that we cannot set whois info on a manually-added client - c = Client{ + c = &Client{ IDs: []string{"1.1.1.2"}, Name: "client1", } _, _ = clients.Add(c) clients.SetWhoisInfo("1.1.1.2", whois) - assert.True(t, clients.ipHost["1.1.1.2"] == nil) + assert.Nil(t, clients.ipHost["1.1.1.2"]) _ = clients.Del("client1") } func TestClientsAddExisting(t *testing.T) { - var c Client + var c *Client clients := clientsContainer{} clients.testing = true clients.Init(nil, nil, nil) @@ -201,7 +216,7 @@ func TestClientsAddExisting(t *testing.T) { testIP := "1.2.3.4" // add a client - c = Client{ + c = &Client{ IDs: []string{"1.1.1.1", "1:2:3::4", "aa:aa:aa:aa:aa:aa", "2.2.2.0/24"}, Name: "client1", } @@ -230,7 +245,7 @@ func TestClientsAddExisting(t *testing.T) { assert.Nil(t, err) // add a new client with the same IP as for a client with MAC - c = Client{ + c = &Client{ IDs: []string{testIP}, Name: "client2", } @@ -239,7 +254,7 @@ func TestClientsAddExisting(t *testing.T) { assert.Nil(t, err) // add a new client with the IP from the client1's IP range - c = Client{ + c = &Client{ IDs: []string{"2.2.2.2"}, Name: "client3", } @@ -255,7 +270,7 @@ func TestClientsCustomUpstream(t *testing.T) { clients.Init(nil, nil, nil) // add client with upstreams - client := Client{ + c := &Client{ IDs: []string{"1.1.1.1", "1:2:3::4", "aa:aa:aa:aa:aa:aa"}, Name: "client1", Upstreams: []string{ @@ -263,7 +278,7 @@ func TestClientsCustomUpstream(t *testing.T) { "[/example.org/]8.8.8.8", }, } - ok, err := clients.Add(client) + ok, err := clients.Add(c) assert.Nil(t, err) assert.True(t, ok) diff --git a/internal/home/clientshttp.go b/internal/home/clientshttp.go index d8cc3ee3..23930411 100644 --- a/internal/home/clientshttp.go +++ b/internal/home/clientshttp.go @@ -3,6 +3,7 @@ package home import ( "encoding/json" "fmt" + "net" "net/http" ) @@ -21,7 +22,7 @@ type clientJSON struct { Upstreams []string `json:"upstreams"` - WhoisInfo map[string]interface{} `json:"whois_info"` + WhoisInfo map[string]string `json:"whois_info"` // Disallowed - if true -- client's IP is not disallowed // Otherwise, it is blocked. @@ -38,7 +39,7 @@ type clientHostJSON struct { Name string `json:"name"` Source string `json:"source"` - WhoisInfo map[string]interface{} `json:"whois_info"` + WhoisInfo map[string]string `json:"whois_info"` } type clientListJSON struct { @@ -74,7 +75,7 @@ func (clients *clientsContainer) handleGetClients(w http.ResponseWriter, _ *http cj.Source = "WHOIS" } - cj.WhoisInfo = make(map[string]interface{}) + cj.WhoisInfo = map[string]string{} for _, wi := range ch.WhoisInfo { cj.WhoisInfo[wi[0]] = wi[1] } @@ -139,7 +140,7 @@ func clientHostToJSON(ip string, ch ClientHost) clientJSON { IDs: []string{ip}, } - cj.WhoisInfo = make(map[string]interface{}) + cj.WhoisInfo = map[string]string{} for _, wi := range ch.WhoisInfo { cj.WhoisInfo[wi[0]] = wi[1] } @@ -157,7 +158,7 @@ func (clients *clientsContainer) handleAddClient(w http.ResponseWriter, r *http. } c := jsonToClient(cj) - ok, err := clients.Add(*c) + ok, err := clients.Add(c) if err != nil { httpError(w, http.StatusBadRequest, "%s", err) return @@ -215,7 +216,7 @@ func (clients *clientsContainer) handleUpdateClient(w http.ResponseWriter, r *ht } c := jsonToClient(dj.Data) - err = clients.Update(dj.Name, *c) + err = clients.Update(dj.Name, c) if err != nil { httpError(w, http.StatusBadRequest, "%s", err) return @@ -227,51 +228,78 @@ func (clients *clientsContainer) handleUpdateClient(w http.ResponseWriter, r *ht // Get the list of clients by IP address list func (clients *clientsContainer) handleFindClient(w http.ResponseWriter, r *http.Request) { q := r.URL.Query() - data := []map[string]interface{}{} - for i := 0; ; i++ { - ip := q.Get(fmt.Sprintf("ip%d", i)) - if len(ip) == 0 { + data := []map[string]clientJSON{} + for i := 0; i < len(q); i++ { + idStr := q.Get(fmt.Sprintf("ip%d", i)) + if idStr == "" { break } - el := map[string]interface{}{} - c, ok := clients.Find(ip) + + ip := net.ParseIP(idStr) + c, ok := clients.Find(idStr) + var cj clientJSON if !ok { - ch, ok := clients.FindAutoClient(ip) - if !ok { - continue // a client with this IP isn't found + var found bool + cj, found = clients.findTemporary(ip, idStr) + if !found { + continue } - cj := clientHostToJSON(ip, ch) - - cj.Disallowed, cj.DisallowedRule = clients.dnsServer.IsBlockedIP(ip) - el[ip] = cj } else { - cj := clientToJSON(&c) - + cj = clientToJSON(c) cj.Disallowed, cj.DisallowedRule = clients.dnsServer.IsBlockedIP(ip) - el[ip] = cj } - data = append(data, el) - } - - js, err := json.Marshal(data) - if err != nil { - httpError(w, http.StatusInternalServerError, "json.Marshal: %s", err) - return + data = append(data, map[string]clientJSON{ + idStr: cj, + }) } w.Header().Set("Content-Type", "application/json") - _, err = w.Write(js) + err := json.NewEncoder(w).Encode(data) if err != nil { httpError(w, http.StatusInternalServerError, "Couldn't write response: %s", err) } } +// findTemporary looks up the IP in temporary storages, like autohosts or +// blocklists. +func (clients *clientsContainer) findTemporary(ip net.IP, idStr string) (cj clientJSON, found bool) { + if ip == nil { + return cj, false + } + + ch, ok := clients.FindAutoClient(idStr) + if !ok { + // It is still possible that the IP used to be in the runtime + // clients list, but then the server was reloaded. So, check + // the DNS server's blocked IP list. + // + // See https://github.com/AdguardTeam/AdGuardHome/issues/2428. + disallowed, rule := clients.dnsServer.IsBlockedIP(ip) + if rule == "" { + return clientJSON{}, false + } + + cj = clientJSON{ + IDs: []string{idStr}, + Disallowed: disallowed, + DisallowedRule: rule, + } + + return cj, true + } + + cj = clientHostToJSON(idStr, ch) + cj.Disallowed, cj.DisallowedRule = clients.dnsServer.IsBlockedIP(ip) + + return cj, true +} + // RegisterClientsHandlers registers HTTP handlers func (clients *clientsContainer) registerWebHandlers() { - httpRegister("GET", "/control/clients", clients.handleGetClients) - httpRegister("POST", "/control/clients/add", clients.handleAddClient) - httpRegister("POST", "/control/clients/delete", clients.handleDelClient) - httpRegister("POST", "/control/clients/update", clients.handleUpdateClient) - httpRegister("GET", "/control/clients/find", clients.handleFindClient) + httpRegister(http.MethodGet, "/control/clients", clients.handleGetClients) + httpRegister(http.MethodPost, "/control/clients/add", clients.handleAddClient) + httpRegister(http.MethodPost, "/control/clients/delete", clients.handleDelClient) + httpRegister(http.MethodPost, "/control/clients/update", clients.handleUpdateClient) + httpRegister(http.MethodGet, "/control/clients/find", clients.handleFindClient) } diff --git a/internal/home/config.go b/internal/home/config.go index ed81c56e..65a9401c 100644 --- a/internal/home/config.go +++ b/internal/home/config.go @@ -1,7 +1,9 @@ package home import ( + "errors" "io/ioutil" + "net" "os" "path/filepath" "sync" @@ -11,6 +13,7 @@ import ( "github.com/AdguardTeam/AdGuardHome/internal/dnsforward" "github.com/AdguardTeam/AdGuardHome/internal/querylog" "github.com/AdguardTeam/AdGuardHome/internal/stats" + "github.com/AdguardTeam/AdGuardHome/internal/version" "github.com/AdguardTeam/golibs/file" "github.com/AdguardTeam/golibs/log" yaml "gopkg.in/yaml.v2" @@ -39,13 +42,14 @@ type configuration struct { // It's reset after config is parsed fileData []byte - BindHost string `yaml:"bind_host"` // BindHost is the IP address of the HTTP server to bind to - BindPort int `yaml:"bind_port"` // BindPort is the port the HTTP server - Users []User `yaml:"users"` // Users that can access HTTP server - ProxyURL string `yaml:"http_proxy"` // Proxy address for our HTTP client - Language string `yaml:"language"` // two-letter ISO 639-1 language code - RlimitNoFile uint `yaml:"rlimit_nofile"` // Maximum number of opened fd's per process (0: default) - DebugPProf bool `yaml:"debug_pprof"` // Enable pprof HTTP server on port 6060 + BindHost net.IP `yaml:"bind_host"` // BindHost is the IP address of the HTTP server to bind to + BindPort int `yaml:"bind_port"` // BindPort is the port the HTTP server + BetaBindPort int `yaml:"beta_bind_port"` // BetaBindPort is the port for new client + Users []User `yaml:"users"` // Users that can access HTTP server + ProxyURL string `yaml:"http_proxy"` // Proxy address for our HTTP client + Language string `yaml:"language"` // two-letter ISO 639-1 language code + RlimitNoFile uint `yaml:"rlimit_nofile"` // Maximum number of opened fd's per process (0: default) + DebugPProf bool `yaml:"debug_pprof"` // Enable pprof HTTP server on port 6060 // TTL for a web session (in hours) // An active session is automatically refreshed once a day. @@ -72,7 +76,7 @@ type configuration struct { // field ordering is important -- yaml fields will mirror ordering from here type dnsConfig struct { - BindHost string `yaml:"bind_host"` + BindHost net.IP `yaml:"bind_host"` Port int `yaml:"port"` // time interval for statistics (in days) @@ -117,10 +121,11 @@ type tlsConfigSettings struct { // initialize to default values, will be changed later when reading config or parsing command line var config = configuration{ - BindPort: 3000, - BindHost: "0.0.0.0", + BindPort: 3000, + BetaBindPort: 0, + BindHost: net.IP{0, 0, 0, 0}, DNS: dnsConfig{ - BindHost: "0.0.0.0", + BindHost: net.IP{0, 0, 0, 0}, Port: 53, StatsInterval: 1, FilteringConfig: dnsforward.FilteringConfig{ @@ -174,13 +179,17 @@ func initConfig() { config.DHCP.Conf4.LeaseDuration = 86400 config.DHCP.Conf4.ICMPTimeout = 1000 config.DHCP.Conf6.LeaseDuration = 86400 + + if ch := version.Channel(); ch == version.ChannelEdge || ch == version.ChannelDevelopment { + config.BetaBindPort = 3001 + } } // getConfigFilename returns path to the current config file func (c *configuration) getConfigFilename() string { configFile, err := filepath.EvalSymlinks(Context.configFilename) if err != nil { - if !os.IsNotExist(err) { + if !errors.Is(err, os.ErrNotExist) { log.Error("unexpected error while config file path evaluation: %s", err) } configFile = Context.configFilename diff --git a/internal/home/control.go b/internal/home/control.go index 3443515a..19876b12 100644 --- a/internal/home/control.go +++ b/internal/home/control.go @@ -2,6 +2,7 @@ package home import ( "encoding/json" + "errors" "fmt" "net" "net/http" @@ -11,6 +12,7 @@ import ( "strings" "github.com/AdguardTeam/AdGuardHome/internal/dnsforward" + "github.com/AdguardTeam/AdGuardHome/internal/version" "github.com/AdguardTeam/golibs/log" "github.com/NYTimes/gziphandler" ) @@ -35,48 +37,52 @@ func httpError(w http.ResponseWriter, code int, format string, args ...interface // --------------- // dns run control // --------------- -func addDNSAddress(dnsAddresses *[]string, addr string) { +func addDNSAddress(dnsAddresses *[]string, addr net.IP) { + hostport := addr.String() if config.DNS.Port != 53 { - addr = fmt.Sprintf("%s:%d", addr, config.DNS.Port) + hostport = net.JoinHostPort(hostport, strconv.Itoa(config.DNS.Port)) } - *dnsAddresses = append(*dnsAddresses, addr) + *dnsAddresses = append(*dnsAddresses, hostport) } -func handleStatus(w http.ResponseWriter, r *http.Request) { - c := dnsforward.FilteringConfig{} +// statusResponse is a response for /control/status endpoint. +type statusResponse struct { + DNSAddrs []string `json:"dns_addresses"` + DNSPort int `json:"dns_port"` + HTTPPort int `json:"http_port"` + IsProtectionEnabled bool `json:"protection_enabled"` + // TODO(e.burkov): Inspect if front-end doesn't requires this field as + // openapi.yaml declares. + IsDHCPAvailable bool `json:"dhcp_available"` + IsRunning bool `json:"running"` + Version string `json:"version"` + Language string `json:"language"` +} + +func handleStatus(w http.ResponseWriter, _ *http.Request) { + resp := statusResponse{ + DNSAddrs: getDNSAddresses(), + DNSPort: config.DNS.Port, + HTTPPort: config.BindPort, + IsRunning: isRunning(), + Version: version.Version(), + Language: config.Language, + } + + var c *dnsforward.FilteringConfig if Context.dnsServer != nil { - Context.dnsServer.WriteDiskConfig(&c) + c = &dnsforward.FilteringConfig{} + Context.dnsServer.WriteDiskConfig(c) + resp.IsProtectionEnabled = c.ProtectionEnabled } - data := map[string]interface{}{ - "dns_addresses": getDNSAddresses(), - "http_port": config.BindPort, - "dns_port": config.DNS.Port, - "running": isRunning(), - "version": versionString, - "language": config.Language, - - "protection_enabled": c.ProtectionEnabled, + // IsDHCPAvailable field is now false by default for Windows. + if runtime.GOOS != "windows" { + resp.IsDHCPAvailable = Context.dhcpServer != nil } - if runtime.GOOS == "windows" { - // Set the DHCP to false explicitly, because Context.dhcpServer - // is probably not nil, despite the fact that there is no - // support for DHCP on Windows in AdGuardHome. - // - // See also the TODO in dhcpd.Create. - data["dhcp_available"] = false - } else { - data["dhcp_available"] = (Context.dhcpServer != nil) - } - - jsonVal, err := json.Marshal(data) - if err != nil { - httpError(w, http.StatusInternalServerError, "Unable to marshal status json: %s", err) - return - } w.Header().Set("Content-Type", "application/json") - _, err = w.Write(jsonVal) + err := json.NewEncoder(w).Encode(resp) if err != nil { httpError(w, http.StatusInternalServerError, "Unable to write response json: %s", err) return @@ -89,7 +95,7 @@ type profileJSON struct { func handleGetProfile(w http.ResponseWriter, r *http.Request) { pj := profileJSON{} - u := Context.auth.GetCurrentUser(r) + u := Context.auth.getCurrentUser(r) pj.Name = u.Name data, err := json.Marshal(pj) @@ -118,7 +124,7 @@ func registerControlHandlers() { } func httpRegister(method, url string, handler func(http.ResponseWriter, *http.Request)) { - if len(method) == 0 { + if method == "" { // "/dns-query" handler doesn't need auth, gzip and isn't restricted by 1 HTTP method Context.mux.HandleFunc(url, postInstall(handler)) return @@ -139,7 +145,7 @@ func ensure(method string, handler func(http.ResponseWriter, *http.Request)) fun return } - if method == "POST" || method == "PUT" || method == "DELETE" { + if method == http.MethodPost || method == http.MethodPut || method == http.MethodDelete { Context.controlLock.Lock() defer Context.controlLock.Unlock() } @@ -149,11 +155,11 @@ func ensure(method string, handler func(http.ResponseWriter, *http.Request)) fun } func ensurePOST(handler func(http.ResponseWriter, *http.Request)) func(http.ResponseWriter, *http.Request) { - return ensure("POST", handler) + return ensure(http.MethodPost, handler) } func ensureGET(handler func(http.ResponseWriter, *http.Request)) func(http.ResponseWriter, *http.Request) { - return ensure("GET", handler) + return ensure(http.MethodGet, handler) } // Bridge between http.Handler object and Go function @@ -197,37 +203,84 @@ func preInstallHandler(handler http.Handler) http.Handler { return &preInstallHandlerStruct{handler} } -// postInstall lets the handler run only if firstRun is false, and redirects to /install.html otherwise -// it also enforces HTTPS if it is enabled and configured +const defaultHTTPSPort = 443 + +// handleHTTPSRedirect redirects the request to HTTPS, if needed. If ok is +// true, the middleware must continue handling the request. +func handleHTTPSRedirect(w http.ResponseWriter, r *http.Request) (ok bool) { + web := Context.web + if web.httpsServer.server == nil { + return true + } + + host, _, err := net.SplitHostPort(r.Host) + if err != nil { + // Check for the missing port error. If it is that error, just + // use the host as is. + // + // See the source code for net.SplitHostPort. + const missingPort = "missing port in address" + + addrErr := &net.AddrError{} + if !errors.As(err, &addrErr) || addrErr.Err != missingPort { + httpError(w, http.StatusBadRequest, "bad host: %s", err) + + return false + } + + host = r.Host + } + + if r.TLS == nil && web.forceHTTPS { + hostPort := host + if port := web.conf.PortHTTPS; port != defaultHTTPSPort { + portStr := strconv.Itoa(port) + hostPort = net.JoinHostPort(host, portStr) + } + + httpsURL := &url.URL{ + Scheme: "https", + Host: hostPort, + Path: r.URL.Path, + RawQuery: r.URL.RawQuery, + } + http.Redirect(w, r, httpsURL.String(), http.StatusTemporaryRedirect) + + return false + } + + // Allow the frontend from the HTTP origin to send requests to the HTTPS + // server. This can happen when the user has just set up HTTPS with + // redirects. Prevent cache-related errors by setting the Vary header. + // + // See https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Allow-Origin. + originURL := &url.URL{ + Scheme: "http", + Host: r.Host, + } + w.Header().Set("Access-Control-Allow-Origin", originURL.String()) + w.Header().Set("Vary", "Origin") + + return true +} + +// postInstall lets the handler to run only if firstRun is false. Otherwise, it +// redirects to /install.html. It also enforces HTTPS if it is enabled and +// configured and sets appropriate access control headers. func postInstall(handler func(http.ResponseWriter, *http.Request)) func(http.ResponseWriter, *http.Request) { return func(w http.ResponseWriter, r *http.Request) { - if Context.firstRun && - !strings.HasPrefix(r.URL.Path, "/install.") && - !strings.HasPrefix(r.URL.Path, "/assets/") { + path := r.URL.Path + if Context.firstRun && !strings.HasPrefix(path, "/install.") && + !strings.HasPrefix(path, "/assets/") { http.Redirect(w, r, "/install.html", http.StatusFound) + return } - // enforce https? - if r.TLS == nil && Context.web.forceHTTPS && Context.web.httpsServer.server != nil { - // yes, and we want host from host:port - host, _, err := net.SplitHostPort(r.Host) - if err != nil { - // no port in host - host = r.Host - } - // construct new URL to redirect to - newURL := url.URL{ - Scheme: "https", - Host: net.JoinHostPort(host, strconv.Itoa(Context.web.portHTTPS)), - Path: r.URL.Path, - RawQuery: r.URL.RawQuery, - } - http.Redirect(w, r, newURL.String(), http.StatusTemporaryRedirect) + if !handleHTTPSRedirect(w, r) { return } - w.Header().Set("Access-Control-Allow-Origin", "*") handler(w, r) } } diff --git a/internal/home/control_test.go b/internal/home/control_test.go index b047b65a..5b08c2bd 100644 --- a/internal/home/control_test.go +++ b/internal/home/control_test.go @@ -68,8 +68,8 @@ kXS9jgARhhiWXJrk data.KeyType == "RSA" && data.Subject == "CN=AdGuard Home,O=AdGuard Ltd" && data.Issuer == "CN=AdGuard Home,O=AdGuard Ltd" && - data.NotBefore == notBefore && - data.NotAfter == notAfter && + data.NotBefore.Equal(notBefore) && + data.NotAfter.Equal(notAfter) && // data.DNSNames[0] == && data.ValidPair) { t.Fatalf("valid cert & priv key: validateCertificates(): %v", data) diff --git a/internal/home/controlfiltering.go b/internal/home/controlfiltering.go index 1d0172e8..b64423d1 100644 --- a/internal/home/controlfiltering.go +++ b/internal/home/controlfiltering.go @@ -369,7 +369,7 @@ type checkHostResp struct { // for FilteredBlockedService: SvcName string `json:"service_name"` - // for ReasonRewrite: + // for Rewrite: CanonName string `json:"cname"` // CNAME value IPList []net.IP `json:"ip_addrs"` // list of IP addresses } @@ -417,14 +417,14 @@ func (f *Filtering) handleCheckHost(w http.ResponseWriter, r *http.Request) { // RegisterFilteringHandlers - register handlers func (f *Filtering) RegisterFilteringHandlers() { - httpRegister("GET", "/control/filtering/status", f.handleFilteringStatus) - httpRegister("POST", "/control/filtering/config", f.handleFilteringConfig) - httpRegister("POST", "/control/filtering/add_url", f.handleFilteringAddURL) - httpRegister("POST", "/control/filtering/remove_url", f.handleFilteringRemoveURL) - httpRegister("POST", "/control/filtering/set_url", f.handleFilteringSetURL) - httpRegister("POST", "/control/filtering/refresh", f.handleFilteringRefresh) - httpRegister("POST", "/control/filtering/set_rules", f.handleFilteringSetRules) - httpRegister("GET", "/control/filtering/check_host", f.handleCheckHost) + httpRegister(http.MethodGet, "/control/filtering/status", f.handleFilteringStatus) + httpRegister(http.MethodPost, "/control/filtering/config", f.handleFilteringConfig) + httpRegister(http.MethodPost, "/control/filtering/add_url", f.handleFilteringAddURL) + httpRegister(http.MethodPost, "/control/filtering/remove_url", f.handleFilteringRemoveURL) + httpRegister(http.MethodPost, "/control/filtering/set_url", f.handleFilteringSetURL) + httpRegister(http.MethodPost, "/control/filtering/refresh", f.handleFilteringRefresh) + httpRegister(http.MethodPost, "/control/filtering/set_rules", f.handleFilteringSetRules) + httpRegister(http.MethodGet, "/control/filtering/check_host", f.handleCheckHost) } func checkFiltersUpdateIntervalHours(i uint32) bool { diff --git a/internal/home/controlinstall.go b/internal/home/controlinstall.go index ed3cdd38..0ab24f60 100644 --- a/internal/home/controlinstall.go +++ b/internal/home/controlinstall.go @@ -12,6 +12,8 @@ import ( "path/filepath" "runtime" "strconv" + "strings" + "time" "github.com/AdguardTeam/AdGuardHome/internal/util" @@ -20,23 +22,16 @@ import ( "github.com/AdguardTeam/golibs/log" ) -type firstRunData struct { - WebPort int `json:"web_port"` - DNSPort int `json:"dns_port"` - Interfaces map[string]interface{} `json:"interfaces"` +// getAddrsResponse is the response for /install/get_addresses endpoint. +type getAddrsResponse struct { + WebPort int `json:"web_port"` + DNSPort int `json:"dns_port"` + Interfaces map[string]*util.NetInterface `json:"interfaces"` } -type netInterfaceJSON struct { - Name string `json:"name"` - MTU int `json:"mtu"` - HardwareAddr string `json:"hardware_address"` - Addresses []string `json:"ip_addresses"` - Flags string `json:"flags"` -} - -// Get initial installation settings +// handleInstallGetAddresses is the handler for /install/get_addresses endpoint. func (web *Web) handleInstallGetAddresses(w http.ResponseWriter, r *http.Request) { - data := firstRunData{} + data := getAddrsResponse{} data.WebPort = 80 data.DNSPort = 53 @@ -46,16 +41,9 @@ func (web *Web) handleInstallGetAddresses(w http.ResponseWriter, r *http.Request return } - data.Interfaces = make(map[string]interface{}) + data.Interfaces = make(map[string]*util.NetInterface) for _, iface := range ifaces { - ifaceJSON := netInterfaceJSON{ - Name: iface.Name, - MTU: iface.MTU, - HardwareAddr: iface.HardwareAddr, - Addresses: iface.Addresses, - Flags: iface.Flags, - } - data.Interfaces[iface.Name] = ifaceJSON + data.Interfaces[iface.Name] = iface } w.Header().Set("Content-Type", "application/json") @@ -68,7 +56,7 @@ func (web *Web) handleInstallGetAddresses(w http.ResponseWriter, r *http.Request type checkConfigReqEnt struct { Port int `json:"port"` - IP string `json:"ip"` + IP net.IP `json:"ip"` Autofix bool `json:"autofix"` } @@ -105,10 +93,10 @@ func (web *Web) handleInstallCheckConfig(w http.ResponseWriter, r *http.Request) return } - if reqData.Web.Port != 0 && reqData.Web.Port != config.BindPort { + if reqData.Web.Port != 0 && reqData.Web.Port != config.BindPort && reqData.Web.Port != config.BetaBindPort { err = util.CheckPortAvailable(reqData.Web.IP, reqData.Web.Port) if err != nil { - respData.Web.Status = fmt.Sprintf("%v", err) + respData.Web.Status = err.Error() } } @@ -136,8 +124,8 @@ func (web *Web) handleInstallCheckConfig(w http.ResponseWriter, r *http.Request) } if err != nil { - respData.DNS.Status = fmt.Sprintf("%v", err) - } else if reqData.DNS.IP != "0.0.0.0" { + respData.DNS.Status = err.Error() + } else if !reqData.DNS.IP.IsUnspecified() { respData.StaticIP = handleStaticIP(reqData.DNS.IP, reqData.SetStaticIP) } } @@ -153,7 +141,7 @@ func (web *Web) handleInstallCheckConfig(w http.ResponseWriter, r *http.Request) // handleStaticIP - handles static IP request // It either checks if we have a static IP // Or if set=true, it tries to set it -func handleStaticIP(ip string, set bool) staticIPJSON { +func handleStaticIP(ip net.IP, set bool) staticIPJSON { resp := staticIPJSON{} interfaceName := util.GetInterfaceByIP(ip) @@ -185,7 +173,7 @@ func handleStaticIP(ip string, set bool) staticIPJSON { if isStaticIP { resp.Static = "yes" } - resp.IP = util.GetSubnet(interfaceName) + resp.IP = util.GetSubnet(interfaceName).String() } return resp } @@ -261,7 +249,7 @@ func disableDNSStubListener() error { } type applyConfigReqEnt struct { - IP string `json:"ip"` + IP net.IP `json:"ip"` Port int `json:"port"` } @@ -276,10 +264,14 @@ type applyConfigReq struct { func copyInstallSettings(dst, src *configuration) { dst.BindHost = src.BindHost dst.BindPort = src.BindPort + dst.BetaBindPort = src.BetaBindPort dst.DNS.BindHost = src.DNS.BindHost dst.DNS.Port = src.DNS.Port } +// shutdownTimeout is the timeout for shutting HTTP server down operation. +const shutdownTimeout = 5 * time.Second + // Apply new configuration, start DNS server, restart Web server func (web *Web) handleInstallConfigure(w http.ResponseWriter, r *http.Request) { newSettings := applyConfigReq{} @@ -295,7 +287,7 @@ func (web *Web) handleInstallConfigure(w http.ResponseWriter, r *http.Request) { } restartHTTP := true - if config.BindHost == newSettings.Web.IP && config.BindPort == newSettings.Web.Port { + if config.BindHost.Equal(newSettings.Web.IP) && config.BindPort == newSettings.Web.Port { // no need to rebind restartHTTP = false } @@ -305,9 +297,10 @@ func (web *Web) handleInstallConfigure(w http.ResponseWriter, r *http.Request) { err = util.CheckPortAvailable(newSettings.Web.IP, newSettings.Web.Port) if err != nil { httpError(w, http.StatusBadRequest, "Impossible to listen on IP:port %s due to %s", - net.JoinHostPort(newSettings.Web.IP, strconv.Itoa(newSettings.Web.Port)), err) + net.JoinHostPort(newSettings.Web.IP.String(), strconv.Itoa(newSettings.Web.Port)), err) return } + } err = util.CheckPacketPortAvailable(newSettings.DNS.IP, newSettings.DNS.Port) @@ -331,6 +324,10 @@ func (web *Web) handleInstallConfigure(w http.ResponseWriter, r *http.Request) { config.DNS.BindHost = newSettings.DNS.IP config.DNS.Port = newSettings.DNS.Port + // TODO(e.burkov): StartMods() should be put in a separate goroutine at + // the moment we'll allow setting up TLS in the initial configuration or + // the configuration itself will use HTTPS protocol, because the + // underlying functions potentially restart the HTTPS server. err = StartMods() if err != nil { Context.firstRun = true @@ -362,12 +359,23 @@ func (web *Web) handleInstallConfigure(w http.ResponseWriter, r *http.Request) { f.Flush() } - // this needs to be done in a goroutine because Shutdown() is a blocking call, and it will block - // until all requests are finished, and _we_ are inside a request right now, so it will block indefinitely + // The Shutdown() method of (*http.Server) needs to be called in a + // separate goroutine, because it waits until all requests are handled + // and will be blocked by it's own caller. if restartHTTP { - go func() { - _ = Context.web.httpServer.Shutdown(context.TODO()) - }() + ctx, cancel := context.WithTimeout(context.Background(), shutdownTimeout) + + shut := func(srv *http.Server) { + defer cancel() + err := srv.Shutdown(ctx) + if err != nil { + log.Debug("error while shutting down HTTP server: %s", err) + } + } + go shut(web.httpServer) + if web.httpServerBeta != nil { + go shut(web.httpServerBeta) + } } } @@ -376,3 +384,186 @@ func (web *Web) registerInstallHandlers() { Context.mux.HandleFunc("/control/install/check_config", preInstall(ensurePOST(web.handleInstallCheckConfig))) Context.mux.HandleFunc("/control/install/configure", preInstall(ensurePOST(web.handleInstallConfigure))) } + +// checkConfigReqEntBeta is a struct representing new client's config check +// request entry. It supports multiple IP values unlike the checkConfigReqEnt. +// +// TODO(e.burkov): This should removed with the API v1 when the appropriate +// functionality will appear in default checkConfigReqEnt. +type checkConfigReqEntBeta struct { + Port int `json:"port"` + IP []net.IP `json:"ip"` + Autofix bool `json:"autofix"` +} + +// checkConfigReqBeta is a struct representing new client's config check request +// body. It uses checkConfigReqEntBeta instead of checkConfigReqEnt. +// +// TODO(e.burkov): This should removed with the API v1 when the appropriate +// functionality will appear in default checkConfigReq. +type checkConfigReqBeta struct { + Web checkConfigReqEntBeta `json:"web"` + DNS checkConfigReqEntBeta `json:"dns"` + SetStaticIP bool `json:"set_static_ip"` +} + +// handleInstallCheckConfigBeta is a substitution of /install/check_config +// handler for new client. +// +// TODO(e.burkov): This should removed with the API v1 when the appropriate +// functionality will appear in default handleInstallCheckConfig. +func (web *Web) handleInstallCheckConfigBeta(w http.ResponseWriter, r *http.Request) { + reqData := checkConfigReqBeta{} + err := json.NewDecoder(r.Body).Decode(&reqData) + if err != nil { + httpError(w, http.StatusBadRequest, "Failed to parse 'check_config' JSON data: %s", err) + return + } + + if len(reqData.DNS.IP) == 0 || len(reqData.Web.IP) == 0 { + httpError(w, http.StatusBadRequest, http.StatusText(http.StatusBadRequest)) + return + } + + nonBetaReqData := checkConfigReq{ + Web: checkConfigReqEnt{ + Port: reqData.Web.Port, + IP: reqData.Web.IP[0], + Autofix: reqData.Web.Autofix, + }, + DNS: checkConfigReqEnt{ + Port: reqData.DNS.Port, + IP: reqData.DNS.IP[0], + Autofix: reqData.DNS.Autofix, + }, + SetStaticIP: reqData.SetStaticIP, + } + + nonBetaReqBody := &strings.Builder{} + + err = json.NewEncoder(nonBetaReqBody).Encode(nonBetaReqData) + if err != nil { + httpError(w, http.StatusBadRequest, "Failed to encode 'check_config' JSON data: %s", err) + return + } + body := nonBetaReqBody.String() + r.Body = ioutil.NopCloser(strings.NewReader(body)) + r.ContentLength = int64(len(body)) + + web.handleInstallCheckConfig(w, r) +} + +// applyConfigReqEntBeta is a struct representing new client's config setting +// request entry. It supports multiple IP values unlike the applyConfigReqEnt. +// +// TODO(e.burkov): This should removed with the API v1 when the appropriate +// functionality will appear in default applyConfigReqEnt. +type applyConfigReqEntBeta struct { + IP []net.IP `json:"ip"` + Port int `json:"port"` +} + +// applyConfigReqBeta is a struct representing new client's config setting +// request body. It uses applyConfigReqEntBeta instead of applyConfigReqEnt. +// +// TODO(e.burkov): This should removed with the API v1 when the appropriate +// functionality will appear in default applyConfigReq. +type applyConfigReqBeta struct { + Web applyConfigReqEntBeta `json:"web"` + DNS applyConfigReqEntBeta `json:"dns"` + Username string `json:"username"` + Password string `json:"password"` +} + +// handleInstallConfigureBeta is a substitution of /install/configure handler +// for new client. +// +// TODO(e.burkov): This should removed with the API v1 when the appropriate +// functionality will appear in default handleInstallConfigure. +func (web *Web) handleInstallConfigureBeta(w http.ResponseWriter, r *http.Request) { + reqData := applyConfigReqBeta{} + err := json.NewDecoder(r.Body).Decode(&reqData) + if err != nil { + httpError(w, http.StatusBadRequest, "Failed to parse 'check_config' JSON data: %s", err) + return + } + + if len(reqData.DNS.IP) == 0 || len(reqData.Web.IP) == 0 { + httpError(w, http.StatusBadRequest, http.StatusText(http.StatusBadRequest)) + return + } + + nonBetaReqData := applyConfigReq{ + Web: applyConfigReqEnt{ + IP: reqData.Web.IP[0], + Port: reqData.Web.Port, + }, + DNS: applyConfigReqEnt{ + IP: reqData.DNS.IP[0], + Port: reqData.DNS.Port, + }, + Username: reqData.Username, + Password: reqData.Password, + } + + nonBetaReqBody := &strings.Builder{} + + err = json.NewEncoder(nonBetaReqBody).Encode(nonBetaReqData) + if err != nil { + httpError(w, http.StatusBadRequest, "Failed to encode 'check_config' JSON data: %s", err) + return + } + body := nonBetaReqBody.String() + r.Body = ioutil.NopCloser(strings.NewReader(body)) + r.ContentLength = int64(len(body)) + + web.handleInstallConfigure(w, r) +} + +// getAddrsResponseBeta is a struct representing new client's getting addresses +// request body. It uses array of structs instead of map. +// +// TODO(e.burkov): This should removed with the API v1 when the appropriate +// functionality will appear in default firstRunData. +type getAddrsResponseBeta struct { + WebPort int `json:"web_port"` + DNSPort int `json:"dns_port"` + Interfaces []*util.NetInterface `json:"interfaces"` +} + +// handleInstallConfigureBeta is a substitution of /install/get_addresses +// handler for new client. +// +// TODO(e.burkov): This should removed with the API v1 when the appropriate +// functionality will appear in default handleInstallGetAddresses. +func (web *Web) handleInstallGetAddressesBeta(w http.ResponseWriter, r *http.Request) { + data := getAddrsResponseBeta{} + data.WebPort = 80 + data.DNSPort = 53 + + ifaces, err := util.GetValidNetInterfacesForWeb() + if err != nil { + httpError(w, http.StatusInternalServerError, "Couldn't get interfaces: %s", err) + return + } + + data.Interfaces = ifaces + + w.Header().Set("Content-Type", "application/json") + err = json.NewEncoder(w).Encode(data) + if err != nil { + httpError(w, http.StatusInternalServerError, "Unable to marshal default addresses to json: %s", err) + return + } +} + +// registerBetaInstallHandlers registers the install handlers for new client +// with the structures it supports. +// +// TODO(e.burkov): This should removed with the API v1 when the appropriate +// functionality will appear in default handlers. +func (web *Web) registerBetaInstallHandlers() { + Context.mux.HandleFunc("/control/install/get_addresses_beta", preInstall(ensureGET(web.handleInstallGetAddressesBeta))) + Context.mux.HandleFunc("/control/install/check_config_beta", preInstall(ensurePOST(web.handleInstallCheckConfigBeta))) + Context.mux.HandleFunc("/control/install/configure_beta", preInstall(ensurePOST(web.handleInstallConfigureBeta))) +} diff --git a/internal/home/controlupdate.go b/internal/home/controlupdate.go index dcf428b9..8b3f1bf5 100644 --- a/internal/home/controlupdate.go +++ b/internal/home/controlupdate.go @@ -1,76 +1,104 @@ package home import ( + "context" "encoding/json" + "errors" "net/http" "os" "os/exec" "path/filepath" "runtime" - "strings" "syscall" + "time" "github.com/AdguardTeam/AdGuardHome/internal/sysutil" - "github.com/AdguardTeam/AdGuardHome/internal/update" + "github.com/AdguardTeam/AdGuardHome/internal/updater" "github.com/AdguardTeam/golibs/log" ) -type getVersionJSONRequest struct { - RecheckNow bool `json:"recheck_now"` +// temporaryError is the interface for temporary errors from the Go standard +// library. +type temporaryError interface { + error + Temporary() (ok bool) } // Get the latest available version from the Internet func handleGetVersionJSON(w http.ResponseWriter, r *http.Request) { + resp := &versionResponse{} if Context.disableUpdate { - resp := make(map[string]interface{}) - resp["disabled"] = true - d, _ := json.Marshal(resp) - _, _ = w.Write(d) + // w.Header().Set("Content-Type", "application/json") + resp.Disabled = true + _ = json.NewEncoder(w).Encode(resp) + // TODO(e.burkov): Add error handling and deal with headers. return } - req := getVersionJSONRequest{} + req := &struct { + Recheck bool `json:"recheck_now"` + }{} + var err error if r.ContentLength != 0 { - err = json.NewDecoder(r.Body).Decode(&req) + err = json.NewDecoder(r.Body).Decode(req) if err != nil { httpError(w, http.StatusBadRequest, "JSON parse: %s", err) return } } - var info update.VersionInfo for i := 0; i != 3; i++ { - Context.controlLock.Lock() - info, err = Context.updater.GetVersionResponse(req.RecheckNow) - Context.controlLock.Unlock() - if err != nil && strings.HasSuffix(err.Error(), "i/o timeout") { - // This case may happen while we're restarting DNS server - // https://github.com/AdguardTeam/AdGuardHome/internal/issues/934 - continue + func() { + Context.controlLock.Lock() + defer Context.controlLock.Unlock() + + resp.VersionInfo, err = Context.updater.VersionInfo(req.Recheck) + }() + + if err != nil { + var terr temporaryError + if errors.As(err, &terr) && terr.Temporary() { + // Temporary network error. This case may happen while + // we're restarting our DNS server. Log and sleep for + // some time. + // + // See https://github.com/AdguardTeam/AdGuardHome/issues/934. + d := time.Duration(i) * time.Second + log.Info("temp net error: %q; sleeping for %s and retrying", err, d) + time.Sleep(d) + + continue + } } + break } if err != nil { - httpError(w, http.StatusBadGateway, "Couldn't get version check json from %s: %T %s\n", versionCheckURL, err, err) + vcu := Context.updater.VersionCheckURL() + // TODO(a.garipov): Figure out the purpose of %T verb. + httpError(w, http.StatusBadGateway, "Couldn't get version check json from %s: %T %s\n", vcu, err, err) + return } + resp.confirmAutoUpdate() + w.Header().Set("Content-Type", "application/json") - _, err = w.Write(getVersionResp(info)) + err = json.NewEncoder(w).Encode(resp) if err != nil { httpError(w, http.StatusInternalServerError, "Couldn't write body: %s", err) } } -// Perform an update procedure to the latest available version +// handleUpdate performs an update to the latest available version procedure. func handleUpdate(w http.ResponseWriter, _ *http.Request) { - if len(Context.updater.NewVersion) == 0 { + if Context.updater.NewVersion() == "" { httpError(w, http.StatusBadRequest, "/update request isn't allowed now") return } - err := Context.updater.DoUpdate() + err := Context.updater.Update() if err != nil { httpError(w, http.StatusInternalServerError, "%s", err) return @@ -81,24 +109,33 @@ func handleUpdate(w http.ResponseWriter, _ *http.Request) { f.Flush() } - go finishUpdate() + // The background context is used because the underlying functions wrap + // it with timeout and shut down the server, which handles current + // request. It also should be done in a separate goroutine due to the + // same reason. + go func() { + finishUpdate(context.Background()) + }() } -// Convert version.json data to our JSON response -func getVersionResp(info update.VersionInfo) []byte { - ret := make(map[string]interface{}) - ret["can_autoupdate"] = false - ret["new_version"] = info.NewVersion - ret["announcement"] = info.Announcement - ret["announcement_url"] = info.AnnouncementURL +// versionResponse is the response for /control/version.json endpoint. +type versionResponse struct { + Disabled bool `json:"disabled"` + updater.VersionInfo +} - if info.CanAutoUpdate { +// confirmAutoUpdate checks the real possibility of auto update. +func (vr *versionResponse) confirmAutoUpdate() { + if vr.CanAutoUpdate != nil && *vr.CanAutoUpdate { canUpdate := true - tlsConf := tlsConfigSettings{} - Context.tls.WriteDiskConfig(&tlsConf) + var tlsConf *tlsConfigSettings + if runtime.GOOS != "windows" { + tlsConf = &tlsConfigSettings{} + Context.tls.WriteDiskConfig(tlsConf) + } - if runtime.GOOS != "windows" && + if tlsConf != nil && ((tlsConf.Enabled && (tlsConf.PortHTTPS < 1024 || tlsConf.PortDNSOverTLS < 1024 || tlsConf.PortDNSOverQUIC < 1024)) || @@ -106,17 +143,14 @@ func getVersionResp(info update.VersionInfo) []byte { config.DNS.Port < 1024) { canUpdate, _ = sysutil.CanBindPrivilegedPorts() } - ret["can_autoupdate"] = canUpdate + vr.CanAutoUpdate = &canUpdate } - - d, _ := json.Marshal(ret) - return d } -// Complete an update procedure -func finishUpdate() { +// finishUpdate completes an update procedure. +func finishUpdate(ctx context.Context) { log.Info("Stopping all tasks") - cleanup() + cleanup(ctx) cleanupAlways() exeName := "AdGuardHome" diff --git a/internal/home/controlupdate_test.go b/internal/home/controlupdate_test.go deleted file mode 100644 index 45112f50..00000000 --- a/internal/home/controlupdate_test.go +++ /dev/null @@ -1,102 +0,0 @@ -// +build ignore - -package home - -import ( - "os" - "testing" - - "github.com/stretchr/testify/assert" -) - -func TestDoUpdate(t *testing.T) { - config.DNS.Port = 0 - Context.workDir = "..." // set absolute path - newver := "v0.96" - - data := `{ - "version": "v0.96", - "announcement": "AdGuard Home v0.96 is now available!", - "announcement_url": "", - "download_windows_amd64": "https://static.adguard.com/adguardhome/beta/AdGuardHome_windows_amd64.zip", - "download_windows_386": "https://static.adguard.com/adguardhome/beta/AdGuardHome_windows_386.zip", - "download_darwin_amd64": "https://static.adguard.com/adguardhome/beta/AdGuardHome_darwin_amd64.zip", - "download_darwin_386": "https://static.adguard.com/adguardhome/beta/AdGuardHome_darwin_386.zip", - "download_linux_amd64": "https://static.adguard.com/adguardhome/beta/AdGuardHome_linux_amd64.tar.gz", - "download_linux_386": "https://static.adguard.com/adguardhome/beta/AdGuardHome_linux_386.tar.gz", - "download_linux_arm": "https://static.adguard.com/adguardhome/beta/AdGuardHome_linux_armv6.tar.gz", - "download_linux_armv5": "https://static.adguard.com/adguardhome/beta/AdGuardHome_linux_armv5.tar.gz", - "download_linux_armv6": "https://static.adguard.com/adguardhome/beta/AdGuardHome_linux_armv6.tar.gz", - "download_linux_armv7": "https://static.adguard.com/adguardhome/beta/AdGuardHome_linux_armv7.tar.gz", - "download_linux_arm64": "https://static.adguard.com/adguardhome/beta/AdGuardHome_linux_arm64.tar.gz", - "download_linux_mips": "https://static.adguard.com/adguardhome/beta/AdGuardHome_linux_mips_softfloat.tar.gz", - "download_linux_mipsle": "https://static.adguard.com/adguardhome/beta/AdGuardHome_linux_mipsle_softfloat.tar.gz", - "download_linux_mips64": "https://static.adguard.com/adguardhome/beta/AdGuardHome_linux_mips64_softfloat.tar.gz", - "download_linux_mips64le": "https://static.adguard.com/adguardhome/beta/AdGuardHome_linux_mips64le_softfloat.tar.gz", - "download_freebsd_386": "https://static.adguard.com/adguardhome/beta/AdGuardHome_freebsd_386.tar.gz", - "download_freebsd_amd64": "https://static.adguard.com/adguardhome/beta/AdGuardHome_freebsd_amd64.tar.gz", - "download_freebsd_arm": "https://static.adguard.com/adguardhome/beta/AdGuardHome_freebsd_armv6.tar.gz", - "download_freebsd_armv5": "https://static.adguard.com/adguardhome/beta/AdGuardHome_freebsd_armv5.tar.gz", - "download_freebsd_armv6": "https://static.adguard.com/adguardhome/beta/AdGuardHome_freebsd_armv6.tar.gz", - "download_freebsd_armv7": "https://static.adguard.com/adguardhome/beta/AdGuardHome_freebsd_armv7.tar.gz", - "download_freebsd_arm64": "https://static.adguard.com/adguardhome/beta/AdGuardHome_freebsd_arm64.tar.gz" - }` - uu, err := getUpdateInfo([]byte(data)) - if err != nil { - t.Fatalf("getUpdateInfo: %s", err) - } - - u := updateInfo{ - pkgURL: "https://static.adguard.com/adguardhome/beta/AdGuardHome_linux_armv6.tar.gz", - pkgName: Context.workDir + "/agh-update-" + newver + "/AdGuardHome_linux_amd64.tar.gz", - newVer: newver, - updateDir: Context.workDir + "/agh-update-" + newver, - backupDir: Context.workDir + "/agh-backup", - configName: Context.workDir + "/AdGuardHome.yaml", - updateConfigName: Context.workDir + "/agh-update-" + newver + "/AdGuardHome/internal/AdGuardHome.yaml", - curBinName: Context.workDir + "/AdGuardHome", - bkpBinName: Context.workDir + "/agh-backup/AdGuardHome", - newBinName: Context.workDir + "/agh-update-" + newver + "/AdGuardHome/internal/AdGuardHome", - } - - assert.Equal(t, uu.pkgURL, u.pkgURL) - assert.Equal(t, uu.pkgName, u.pkgName) - assert.Equal(t, uu.newVer, u.newVer) - assert.Equal(t, uu.updateDir, u.updateDir) - assert.Equal(t, uu.backupDir, u.backupDir) - assert.Equal(t, uu.configName, u.configName) - assert.Equal(t, uu.updateConfigName, u.updateConfigName) - assert.Equal(t, uu.curBinName, u.curBinName) - assert.Equal(t, uu.bkpBinName, u.bkpBinName) - assert.Equal(t, uu.newBinName, u.newBinName) - - e := doUpdate(&u) - if e != nil { - t.Fatalf("FAILED: %s", e) - } - os.RemoveAll(u.backupDir) -} - -func TestTargzFileUnpack(t *testing.T) { - fn := "../dist/AdGuardHome_linux_amd64.tar.gz" - outdir := "../test-unpack" - defer os.RemoveAll(outdir) - _ = os.Mkdir(outdir, 0o755) - files, e := targzFileUnpack(fn, outdir) - if e != nil { - t.Fatalf("FAILED: %s", e) - } - t.Logf("%v", files) -} - -func TestZipFileUnpack(t *testing.T) { - fn := "../dist/AdGuardHome_windows_amd64.zip" - outdir := "../test-unpack" - _ = os.Mkdir(outdir, 0o755) - files, e := zipFileUnpack(fn, outdir) - if e != nil { - t.Fatalf("FAILED: %s", e) - } - t.Logf("%v", files) - os.RemoveAll(outdir) -} diff --git a/internal/home/dns.go b/internal/home/dns.go index 1090d9be..3640fd03 100644 --- a/internal/home/dns.go +++ b/internal/home/dns.go @@ -3,8 +3,10 @@ package home import ( "fmt" "net" + "net/url" "os" "path/filepath" + "strconv" "github.com/AdguardTeam/AdGuardHome/internal/agherr" "github.com/AdguardTeam/AdGuardHome/internal/dnsfilter" @@ -55,10 +57,10 @@ func initDNSServer() error { filterConf := config.DNS.DnsfilterConf bindhost := config.DNS.BindHost - if config.DNS.BindHost == "0.0.0.0" { - bindhost = "127.0.0.1" + if config.DNS.BindHost.IsUnspecified() { + bindhost = net.IPv4(127, 0, 0, 1) } - filterConf.ResolverAddress = fmt.Sprintf("%s:%d", bindhost, config.DNS.Port) + filterConf.ResolverAddress = net.JoinHostPort(bindhost.String(), strconv.Itoa(config.DNS.Port)) filterConf.AutoHosts = &Context.autoHosts filterConf.ConfigModified = onConfigModified filterConf.HTTPRegister = httpRegister @@ -98,26 +100,24 @@ func isRunning() bool { } func onDNSRequest(d *proxy.DNSContext) { - ip := dnsforward.GetIPString(d.Addr) - if ip == "" { + ip := dnsforward.IPFromAddr(d.Addr) + if ip == nil { // This would be quite weird if we get here return } - ipAddr := net.ParseIP(ip) - if !ipAddr.IsLoopback() { + if !ip.IsLoopback() { Context.rdns.Begin(ip) } - if !Context.ipDetector.detectSpecialNetwork(ipAddr) { + if !Context.ipDetector.detectSpecialNetwork(ip) { Context.whois.Begin(ip) } } func generateServerConfig() (newconfig dnsforward.ServerConfig, err error) { - bindHost := net.ParseIP(config.DNS.BindHost) newconfig = dnsforward.ServerConfig{ - UDPListenAddr: &net.UDPAddr{IP: bindHost, Port: config.DNS.Port}, - TCPListenAddr: &net.TCPAddr{IP: bindHost, Port: config.DNS.Port}, + UDPListenAddr: &net.UDPAddr{IP: config.DNS.BindHost, Port: config.DNS.Port}, + TCPListenAddr: &net.TCPAddr{IP: config.DNS.BindHost, Port: config.DNS.Port}, FilteringConfig: config.DNS.FilteringConfig, ConfigModified: onConfigModified, HTTPRegister: httpRegister, @@ -128,23 +128,24 @@ func generateServerConfig() (newconfig dnsforward.ServerConfig, err error) { Context.tls.WriteDiskConfig(&tlsConf) if tlsConf.Enabled { newconfig.TLSConfig = tlsConf.TLSConfig + newconfig.TLSConfig.ServerName = tlsConf.ServerName if tlsConf.PortDNSOverTLS != 0 { newconfig.TLSListenAddr = &net.TCPAddr{ - IP: bindHost, + IP: config.DNS.BindHost, Port: tlsConf.PortDNSOverTLS, } } if tlsConf.PortDNSOverQUIC != 0 { newconfig.QUICListenAddr = &net.UDPAddr{ - IP: bindHost, + IP: config.DNS.BindHost, Port: int(tlsConf.PortDNSOverQUIC), } } if tlsConf.PortDNSCrypt != 0 { - newconfig.DNSCryptConfig, err = newDNSCrypt(bindHost, tlsConf) + newconfig.DNSCryptConfig, err = newDNSCrypt(config.DNS.BindHost, tlsConf) if err != nil { // Don't wrap the error, because it's already // wrapped by newDNSCrypt. @@ -209,43 +210,49 @@ type dnsEncryption struct { quic string } -func getDNSEncryption() dnsEncryption { - dnsEncryption := dnsEncryption{} - +func getDNSEncryption() (de dnsEncryption) { tlsConf := tlsConfigSettings{} Context.tls.WriteDiskConfig(&tlsConf) if tlsConf.Enabled && len(tlsConf.ServerName) != 0 { - + hostname := tlsConf.ServerName if tlsConf.PortHTTPS != 0 { - addr := tlsConf.ServerName + addr := hostname if tlsConf.PortHTTPS != 443 { - addr = fmt.Sprintf("%s:%d", addr, tlsConf.PortHTTPS) + addr = net.JoinHostPort(addr, strconv.Itoa(tlsConf.PortHTTPS)) } - addr = fmt.Sprintf("https://%s/dns-query", addr) - dnsEncryption.https = addr + + de.https = (&url.URL{ + Scheme: "https", + Host: addr, + Path: "/dns-query", + }).String() } if tlsConf.PortDNSOverTLS != 0 { - addr := fmt.Sprintf("tls://%s:%d", tlsConf.ServerName, tlsConf.PortDNSOverTLS) - dnsEncryption.tls = addr + de.tls = (&url.URL{ + Scheme: "tls", + Host: net.JoinHostPort(hostname, strconv.Itoa(tlsConf.PortDNSOverTLS)), + }).String() } if tlsConf.PortDNSOverQUIC != 0 { - addr := fmt.Sprintf("quic://%s:%d", tlsConf.ServerName, tlsConf.PortDNSOverQUIC) - dnsEncryption.quic = addr + de.quic = (&url.URL{ + Scheme: "quic", + Host: net.JoinHostPort(hostname, strconv.Itoa(int(tlsConf.PortDNSOverQUIC))), + }).String() } } - return dnsEncryption + return de } // Get the list of DNS addresses the server is listening on func getDNSAddresses() []string { dnsAddresses := []string{} - if config.DNS.BindHost == "0.0.0.0" { + if config.DNS.BindHost.IsUnspecified() { ifaces, e := util.GetValidNetInterfacesForWeb() if e != nil { log.Error("Couldn't get network interfaces: %v", e) @@ -275,21 +282,26 @@ func getDNSAddresses() []string { return dnsAddresses } -// If a client has his own settings, apply them -func applyAdditionalFiltering(clientAddr string, setts *dnsfilter.RequestFilteringSettings) { +// applyAdditionalFiltering adds additional client information and settings if +// the client has them. +func applyAdditionalFiltering(clientAddr net.IP, clientID string, setts *dnsfilter.RequestFilteringSettings) { Context.dnsFilter.ApplyBlockedServices(setts, nil, true) - if len(clientAddr) == 0 { + if clientAddr == nil { return } + setts.ClientIP = clientAddr - c, ok := Context.clients.Find(clientAddr) + c, ok := Context.clients.Find(clientID) if !ok { - return + c, ok = Context.clients.Find(clientAddr.String()) + if !ok { + return + } } - log.Debug("Using settings for client %s with IP %s", c.Name, clientAddr) + log.Debug("using settings for client %s with ip %s and id %q", c.Name, clientAddr, clientID) if c.UseOwnBlockedServices { Context.dnsFilter.ApplyBlockedServices(setts, c.BlockedServices, false) @@ -328,13 +340,11 @@ func startDNSServer() error { Context.queryLog.Start() const topClientsNumber = 100 // the number of clients to get - topClients := Context.stats.GetTopClientsIP(topClientsNumber) - for _, ip := range topClients { - ipAddr := net.ParseIP(ip) - if !ipAddr.IsLoopback() { + for _, ip := range Context.stats.GetTopClientsIP(topClientsNumber) { + if !ip.IsLoopback() { Context.rdns.Begin(ip) } - if !Context.ipDetector.detectSpecialNetwork(ipAddr) { + if !Context.ipDetector.detectSpecialNetwork(ip) { Context.whois.Begin(ip) } } diff --git a/internal/home/filter_test.go b/internal/home/filter_test.go index 2bc23be1..a5b6d20b 100644 --- a/internal/home/filter_test.go +++ b/internal/home/filter_test.go @@ -50,16 +50,17 @@ func TestFilters(t *testing.T) { // download ok, err := Context.filters.update(&f) - assert.Equal(t, nil, err) + assert.Nil(t, err) assert.True(t, ok) assert.Equal(t, 3, f.RulesCount) // refresh ok, err = Context.filters.update(&f) - assert.True(t, !ok && err == nil) + assert.False(t, ok) + assert.Nil(t, err) err = Context.filters.load(&f) - assert.True(t, err == nil) + assert.Nil(t, err) f.unload() _ = os.Remove(f.Path()) diff --git a/internal/home/home.go b/internal/home/home.go index 58c99ff6..bfc17750 100644 --- a/internal/home/home.go +++ b/internal/home/home.go @@ -5,6 +5,7 @@ import ( "context" "crypto/tls" "crypto/x509" + "errors" "fmt" "io/ioutil" "net" @@ -27,8 +28,9 @@ import ( "github.com/AdguardTeam/AdGuardHome/internal/querylog" "github.com/AdguardTeam/AdGuardHome/internal/stats" "github.com/AdguardTeam/AdGuardHome/internal/sysutil" - "github.com/AdguardTeam/AdGuardHome/internal/update" + "github.com/AdguardTeam/AdGuardHome/internal/updater" "github.com/AdguardTeam/AdGuardHome/internal/util" + "github.com/AdguardTeam/AdGuardHome/internal/version" "github.com/AdguardTeam/golibs/log" "gopkg.in/natefinch/lumberjack.v2" ) @@ -38,15 +40,6 @@ const ( configSyslog = "syslog" ) -// Update-related variables -var ( - versionString = "dev" - updateChannel = "none" - versionCheckURL = "" - ARMVersion = "" - MIPSVersion = "" -) - // Global context type homeContext struct { // Modules @@ -65,7 +58,7 @@ type homeContext struct { web *Web // Web (HTTP, HTTPS) module tls *TLSMod // TLS module autoHosts util.AutoHosts // IP-hostname pairs taken from system configuration (e.g. /etc/hosts) files - updater *update.Updater + updater *updater.Updater ipDetector *ipDetector @@ -99,14 +92,7 @@ func (c *homeContext) getDataDir() string { var Context homeContext // Main is the entry point -func Main(version, channel, armVer, mipsVer string) { - // Init update-related global variables - versionString = version - updateChannel = channel - ARMVersion = armVer - MIPSVersion = mipsVer - versionCheckURL = "https://static.adguard.com/adguardhome/" + updateChannel + "/version.json" - +func Main() { // config can be specified, which reads options from there, but other command line flags have to override config values // therefore, we must do it manually instead of using a lib args := loadOptions() @@ -123,7 +109,7 @@ func Main(version, channel, armVer, mipsVer string) { Context.tls.Reload() default: - cleanup() + cleanup(context.Background()) cleanupAlways() os.Exit(0) } @@ -139,23 +125,10 @@ func Main(version, channel, armVer, mipsVer string) { run(args) } -// version - returns the current version string -func version() string { - // TODO(a.garipov): I'm pretty sure we can extract some of this stuff - // from the build info. - msg := "AdGuard Home, version %s, channel %s, arch %s %s" - if ARMVersion != "" { - msg = msg + " v" + ARMVersion - } else if MIPSVersion != "" { - msg = msg + " " + MIPSVersion - } - - return fmt.Sprintf(msg, versionString, updateChannel, runtime.GOOS, runtime.GOARCH) -} - func setupContext(args options) { Context.runningAsService = args.runningAsService - Context.disableUpdate = args.disableUpdate + Context.disableUpdate = args.disableUpdate || + version.Channel() == version.ChannelDevelopment Context.firstRun = detectFirstRun() if Context.firstRun { @@ -214,15 +187,16 @@ func setupConfig(args options) { Context.autoHosts.Init("") - Context.updater = update.NewUpdater(update.Config{ - Client: Context.client, - WorkDir: Context.workDir, - VersionURL: versionCheckURL, - VersionString: versionString, - OS: runtime.GOOS, - Arch: runtime.GOARCH, - ARMVersion: ARMVersion, - ConfigName: config.getConfigFilename(), + Context.updater = updater.NewUpdater(&updater.Config{ + Client: Context.client, + Version: version.Version(), + Channel: version.Channel(), + GOARCH: runtime.GOARCH, + GOOS: runtime.GOOS, + GOARM: version.GOARM(), + GOMIPS: version.GOMIPS(), + WorkDir: Context.workDir, + ConfName: config.getConfigFilename(), }) Context.clients.Init(config.Clients, Context.dhcpServer, &Context.autoHosts) @@ -234,7 +208,7 @@ func setupConfig(args options) { } // override bind host/port from the console - if args.bindHost != "" { + if args.bindHost != nil { config.BindHost = args.bindHost } if args.bindPort != 0 { @@ -260,7 +234,7 @@ func run(args options) { memoryUsage(args) // print the first message after logger is configured - log.Println(version()) + log.Println(version.Full()) log.Debug("Current working directory is %s", Context.workDir) if args.runningAsService { log.Info("AdGuard Home is running as a service") @@ -316,19 +290,25 @@ func run(args options) { } webConf := webConfig{ - firstRun: Context.firstRun, - BindHost: config.BindHost, - BindPort: config.BindPort, + firstRun: Context.firstRun, + BindHost: config.BindHost, + BindPort: config.BindPort, + BetaBindPort: config.BetaBindPort, - ReadTimeout: ReadTimeout, - ReadHeaderTimeout: ReadHeaderTimeout, - WriteTimeout: WriteTimeout, + ReadTimeout: readTimeout, + ReadHeaderTimeout: readHdrTimeout, + WriteTimeout: writeTimeout, } Context.web = CreateWeb(&webConf) if Context.web == nil { log.Fatalf("Can't initialize Web module") } + Context.ipDetector, err = newIPDetector() + if err != nil { + log.Fatal(err) + } + if !Context.firstRun { err := initDNSServer() if err != nil { @@ -340,6 +320,7 @@ func run(args options) { go func() { err := startDNSServer() if err != nil { + closeDNSServer() log.Fatal(err) } }() @@ -349,18 +330,13 @@ func run(args options) { } } - Context.ipDetector, err = newIPDetector() - if err != nil { - log.Fatal(err) - } - Context.web.Start() // wait indefinitely for other go-routines to complete their job select {} } -// StartMods - initialize and start DNS after installation +// StartMods initializes and starts the DNS server after installation. func StartMods() error { err := initDNSServer() if err != nil { @@ -460,6 +436,10 @@ func initWorkingDir(args options) { } else { Context.workDir = filepath.Dir(execPath) } + + if workDir, err := filepath.EvalSymlinks(Context.workDir); err == nil { + Context.workDir = workDir + } } // configureLogger configures logger level and output @@ -527,11 +507,12 @@ func configureLogger(args options) { } } -func cleanup() { +// cleanup stops and resets all the modules. +func cleanup(ctx context.Context) { log.Info("Stopping AdGuard Home") if Context.web != nil { - Context.web.Close() + Context.web.Close(ctx) Context.web = nil } if Context.auth != nil { @@ -592,8 +573,6 @@ func loadOptions() options { // prints IP addresses which user can use to open the admin interface // proto is either "http" or "https" func printHTTPAddresses(proto string) { - var address string - tlsConf := tlsConfigSettings{} if Context.tls != nil { Context.tls.WriteDiskConfig(&tlsConf) @@ -604,31 +583,41 @@ func printHTTPAddresses(proto string) { port = strconv.Itoa(tlsConf.PortHTTPS) } + var hostStr string if proto == "https" && tlsConf.ServerName != "" { if tlsConf.PortHTTPS == 443 { log.Printf("Go to https://%s", tlsConf.ServerName) } else { log.Printf("Go to https://%s:%s", tlsConf.ServerName, port) } - } else if config.BindHost == "0.0.0.0" { + } else if config.BindHost.IsUnspecified() { log.Println("AdGuard Home is available on the following addresses:") ifaces, err := util.GetValidNetInterfacesForWeb() if err != nil { // That's weird, but we'll ignore it - address = net.JoinHostPort(config.BindHost, port) - log.Printf("Go to %s://%s", proto, address) + hostStr = config.BindHost.String() + log.Printf("Go to %s://%s", proto, net.JoinHostPort(hostStr, port)) + if config.BetaBindPort != 0 { + log.Printf("Go to %s://%s (BETA)", proto, net.JoinHostPort(hostStr, strconv.Itoa(config.BetaBindPort))) + } return } for _, iface := range ifaces { for _, addr := range iface.Addresses { - address = net.JoinHostPort(addr, strconv.Itoa(config.BindPort)) - log.Printf("Go to %s://%s", proto, address) + hostStr = addr.String() + log.Printf("Go to %s://%s", proto, net.JoinHostPort(hostStr, strconv.Itoa(config.BindPort))) + if config.BetaBindPort != 0 { + log.Printf("Go to %s://%s (BETA)", proto, net.JoinHostPort(hostStr, strconv.Itoa(config.BetaBindPort))) + } } } } else { - address = net.JoinHostPort(config.BindHost, port) - log.Printf("Go to %s://%s", proto, address) + hostStr = config.BindHost.String() + log.Printf("Go to %s://%s", proto, net.JoinHostPort(hostStr, port)) + if config.BetaBindPort != 0 { + log.Printf("Go to %s://%s (BETA)", proto, net.JoinHostPort(hostStr, strconv.Itoa(config.BetaBindPort))) + } } } @@ -641,7 +630,7 @@ func detectFirstRun() bool { configfile = filepath.Join(Context.workDir, Context.configFilename) } _, err := os.Stat(configfile) - return os.IsNotExist(err) + return errors.Is(err, os.ErrNotExist) } // Connect to a remote server resolving hostname using our own DNS server @@ -685,10 +674,11 @@ func customDialContext(ctx context.Context, network, addr string) (net.Conn, err return nil, agherr.Many(fmt.Sprintf("couldn't dial to %s", addr), dialErrs...) } -func getHTTPProxy(req *http.Request) (*url.URL, error) { - if len(config.ProxyURL) == 0 { +func getHTTPProxy(_ *http.Request) (*url.URL, error) { + if config.ProxyURL == "" { return nil, nil } + return url.Parse(config.ProxyURL) } diff --git a/internal/home/home_test.go b/internal/home/home_test.go index 1b16e357..02613b7c 100644 --- a/internal/home/home_test.go +++ b/internal/home/home_test.go @@ -1,6 +1,6 @@ // +build !race -// TODO(e.burkov): remove this weird buildtag. +// TODO(e.burkov): Remove this weird buildtag. package home @@ -119,7 +119,7 @@ func TestHome(t *testing.T) { fn := filepath.Join(dir, "AdGuardHome.yaml") // Prepare the test config - assert.True(t, ioutil.WriteFile(fn, []byte(yamlConf), 0o644) == nil) + assert.Nil(t, ioutil.WriteFile(fn, []byte(yamlConf), 0o644)) fn, _ = filepath.Abs(fn) config = configuration{} // the global variable is dirty because of the previous tests run @@ -133,16 +133,16 @@ func TestHome(t *testing.T) { h := http.Client{} for i := 0; i != 50; i++ { resp, err = h.Get("http://127.0.0.1:3000/") - if err == nil && resp.StatusCode != 404 { + if err == nil && resp.StatusCode != http.StatusNotFound { break } time.Sleep(100 * time.Millisecond) } - assert.Truef(t, err == nil, "%s", err) + assert.Nilf(t, err, "%s", err) assert.Equal(t, http.StatusOK, resp.StatusCode) resp, err = h.Get("http://127.0.0.1:3000/control/status") - assert.Truef(t, err == nil, "%s", err) + assert.Nilf(t, err, "%s", err) assert.Equal(t, http.StatusOK, resp.StatusCode) // test DNS over UDP @@ -159,16 +159,16 @@ func TestHome(t *testing.T) { req.RecursionDesired = true req.Question = []dns.Question{{Name: "static.adguard.com.", Qtype: dns.TypeA, Qclass: dns.ClassINET}} buf, err := req.Pack() - assert.True(t, err == nil, "%s", err) + assert.Nil(t, err) requestURL := "http://127.0.0.1:3000/dns-query?dns=" + base64.RawURLEncoding.EncodeToString(buf) resp, err = http.DefaultClient.Get(requestURL) - assert.True(t, err == nil, "%s", err) + assert.Nil(t, err) body, err := ioutil.ReadAll(resp.Body) - assert.True(t, err == nil, "%s", err) - assert.True(t, resp.StatusCode == http.StatusOK) + assert.Nil(t, err) + assert.Equal(t, http.StatusOK, resp.StatusCode) response := dns.Msg{} err = response.Unpack(body) - assert.True(t, err == nil, "%s", err) + assert.Nil(t, err) addrs = nil proxyutil.AppendIPAddrs(&addrs, response.Answer) haveIP = len(addrs) != 0 @@ -186,6 +186,6 @@ func TestHome(t *testing.T) { time.Sleep(1 * time.Second) } - cleanup() + cleanup(context.Background()) cleanupAlways() } diff --git a/internal/home/ipdetector_test.go b/internal/home/ipdetector_test.go index ee20612f..6609ba08 100644 --- a/internal/home/ipdetector_test.go +++ b/internal/home/ipdetector_test.go @@ -5,16 +5,15 @@ import ( "testing" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) func TestIPDetector_detectSpecialNetwork(t *testing.T) { var ipd *ipDetector + var err error - t.Run("newIPDetector", func(t *testing.T) { - var err error - ipd, err = newIPDetector() - assert.Nil(t, err) - }) + ipd, err = newIPDetector() + require.Nil(t, err) testCases := []struct { name string diff --git a/internal/home/middlewares.go b/internal/home/middlewares.go index a5758985..de38c3fa 100644 --- a/internal/home/middlewares.go +++ b/internal/home/middlewares.go @@ -22,15 +22,43 @@ func withMiddlewares(h http.Handler, middlewares ...middleware) (wrapped http.Ha return wrapped } -// RequestBodySizeLimit is maximum request body length in bytes. -const RequestBodySizeLimit = 64 * 1024 +// defaultReqBodySzLim is the default maximum request body size. +const defaultReqBodySzLim = 64 * 1024 + +// largerReqBodySzLim is the maximum request body size for APIs expecting larger +// requests. +const largerReqBodySzLim = 4 * 1024 * 1024 + +// expectsLargerRequests shows if this request should use a larger body size +// limit. 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. +// +// See https://github.com/AdguardTeam/AdGuardHome/issues/2666 and +// https://github.com/AdguardTeam/AdGuardHome/issues/2675. +func expectsLargerRequests(r *http.Request) (ok bool) { + m := r.Method + if m != http.MethodPost { + return false + } + + p := r.URL.Path + return p == "/control/access/set" || + p == "/control/filtering/set_rules" +} // limitRequestBody wraps underlying handler h, making it's request's body Read // method limited. func limitRequestBody(h http.Handler) (limited http.Handler) { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { var err error - r.Body, err = aghio.LimitReadCloser(r.Body, RequestBodySizeLimit) + + var szLim int64 = defaultReqBodySzLim + if expectsLargerRequests(r) { + szLim = largerReqBodySzLim + } + + r.Body, err = aghio.LimitReadCloser(r.Body, szLim) if err != nil { log.Error("limitRequestBody: %s", err) @@ -40,3 +68,18 @@ func limitRequestBody(h http.Handler) (limited http.Handler) { h.ServeHTTP(w, r) }) } + +// wrapIndexBeta returns handler that deals with new client. +func (web *Web) wrapIndexBeta(http.Handler) (wrapped http.Handler) { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + h, pattern := Context.mux.Handler(r) + switch pattern { + case "/": + web.handlerBeta.ServeHTTP(w, r) + case "/install.html": + web.installerBeta.ServeHTTP(w, r) + default: + h.ServeHTTP(w, r) + } + }) +} diff --git a/internal/home/middlewares_test.go b/internal/home/middlewares_test.go index 4d6a33d0..8397302b 100644 --- a/internal/home/middlewares_test.go +++ b/internal/home/middlewares_test.go @@ -9,11 +9,12 @@ import ( "github.com/AdguardTeam/AdGuardHome/internal/aghio" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) func TestLimitRequestBody(t *testing.T) { errReqLimitReached := &aghio.LimitReachedError{ - Limit: RequestBodySizeLimit, + Limit: defaultReqBodySzLim, } testCases := []struct { @@ -28,8 +29,8 @@ func TestLimitRequestBody(t *testing.T) { wantErr: nil, }, { name: "so_big", - body: string(make([]byte, RequestBodySizeLimit+1)), - want: make([]byte, RequestBodySizeLimit), + body: string(make([]byte, defaultReqBodySzLim+1)), + want: make([]byte, defaultReqBodySzLim), wantErr: errReqLimitReached, }, { name: "empty", @@ -42,7 +43,10 @@ func TestLimitRequestBody(t *testing.T) { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { var b []byte b, *err = ioutil.ReadAll(r.Body) - w.Write(b) + _, werr := w.Write(b) + if werr != nil { + panic(werr) + } }) } @@ -57,8 +61,8 @@ func TestLimitRequestBody(t *testing.T) { lim.ServeHTTP(res, req) + require.Equal(t, tc.wantErr, err) assert.Equal(t, tc.want, res.Body.Bytes()) - assert.Equal(t, tc.wantErr, err) }) } } diff --git a/internal/home/mobileconfig.go b/internal/home/mobileconfig.go index 2f06329e..3953e2e6 100644 --- a/internal/home/mobileconfig.go +++ b/internal/home/mobileconfig.go @@ -4,7 +4,10 @@ import ( "encoding/json" "fmt" "net/http" + "net/url" + "path" + "github.com/AdguardTeam/AdGuardHome/internal/dnsforward" "github.com/AdguardTeam/golibs/log" uuid "github.com/satori/go.uuid" "howett.net/plist" @@ -14,6 +17,7 @@ type dnsSettings struct { DNSProtocol string ServerURL string `plist:",omitempty"` ServerName string `plist:",omitempty"` + clientID string } type payloadContent struct { @@ -23,19 +27,19 @@ type payloadContent struct { PayloadIdentifier string PayloadType string PayloadUUID string - PayloadVersion int DNSSettings dnsSettings + PayloadVersion int } type mobileConfig struct { - PayloadContent []payloadContent PayloadDescription string PayloadDisplayName string PayloadIdentifier string - PayloadRemovalDisallowed bool PayloadType string PayloadUUID string + PayloadContent []payloadContent PayloadVersion int + PayloadRemovalDisallowed bool } func genUUIDv4() string { @@ -48,22 +52,35 @@ const ( ) func getMobileConfig(d dnsSettings) ([]byte, error) { - var name string + var dspName string switch d.DNSProtocol { case dnsProtoHTTPS: - name = fmt.Sprintf("%s DoH", d.ServerName) - d.ServerURL = fmt.Sprintf("https://%s/dns-query", d.ServerName) + dspName = fmt.Sprintf("%s DoH", d.ServerName) + + u := &url.URL{ + Scheme: "https", + Host: d.ServerName, + Path: "/dns-query", + } + if d.clientID != "" { + u.Path = path.Join(u.Path, d.clientID) + } + + d.ServerURL = u.String() case dnsProtoTLS: - name = fmt.Sprintf("%s DoT", d.ServerName) + dspName = fmt.Sprintf("%s DoT", d.ServerName) + if d.clientID != "" { + d.ServerName = d.clientID + "." + d.ServerName + } default: return nil, fmt.Errorf("bad dns protocol %q", d.DNSProtocol) } data := mobileConfig{ PayloadContent: []payloadContent{{ - Name: name, + Name: dspName, PayloadDescription: "Configures device to use AdGuard Home", - PayloadDisplayName: name, + PayloadDisplayName: dspName, PayloadIdentifier: fmt.Sprintf("com.apple.dnsSettings.managed.%s", genUUIDv4()), PayloadType: "com.apple.dnsSettings.managed", PayloadUUID: genUUIDv4(), @@ -71,7 +88,7 @@ func getMobileConfig(d dnsSettings) ([]byte, error) { DNSSettings: d, }}, PayloadDescription: "Adds AdGuard Home to Big Sur and iOS 14 or newer systems", - PayloadDisplayName: name, + PayloadDisplayName: dspName, PayloadIdentifier: genUUIDv4(), PayloadRemovalDisallowed: false, PayloadType: "Configuration", @@ -83,7 +100,10 @@ func getMobileConfig(d dnsSettings) ([]byte, error) { } func handleMobileConfig(w http.ResponseWriter, r *http.Request, dnsp string) { - host := r.URL.Query().Get("host") + var err error + + q := r.URL.Query() + host := q.Get("host") if host == "" { host = Context.tls.conf.ServerName } @@ -92,7 +112,7 @@ func handleMobileConfig(w http.ResponseWriter, r *http.Request, dnsp string) { w.WriteHeader(http.StatusInternalServerError) const msg = "no host in query parameters and no server_name" - err := json.NewEncoder(w).Encode(&jsonError{ + err = json.NewEncoder(w).Encode(&jsonError{ Message: msg, }) if err != nil { @@ -102,9 +122,25 @@ func handleMobileConfig(w http.ResponseWriter, r *http.Request, dnsp string) { return } + clientID := q.Get("client_id") + err = dnsforward.ValidateClientID(clientID) + if err != nil { + w.WriteHeader(http.StatusBadRequest) + + err = json.NewEncoder(w).Encode(&jsonError{ + Message: err.Error(), + }) + if err != nil { + log.Debug("writing 400 json response: %s", err) + } + + return + } + d := dnsSettings{ DNSProtocol: dnsp, ServerName: host, + clientID: clientID, } mobileconfig, err := getMobileConfig(d) @@ -115,6 +151,7 @@ func handleMobileConfig(w http.ResponseWriter, r *http.Request, dnsp string) { } w.Header().Set("Content-Type", "application/xml") + _, _ = w.Write(mobileconfig) } diff --git a/internal/home/mobileconfig_test.go b/internal/home/mobileconfig_test.go index f5bf3f2b..9dcafc97 100644 --- a/internal/home/mobileconfig_test.go +++ b/internal/home/mobileconfig_test.go @@ -23,7 +23,7 @@ func TestHandleMobileConfigDOH(t *testing.T) { _, err = plist.Unmarshal(w.Body.Bytes(), &mc) assert.Nil(t, err) - if assert.Equal(t, 1, len(mc.PayloadContent)) { + if assert.Len(t, mc.PayloadContent, 1) { assert.Equal(t, "example.org DoH", mc.PayloadContent[0].Name) assert.Equal(t, "example.org DoH", mc.PayloadContent[0].PayloadDisplayName) assert.Equal(t, "example.org", mc.PayloadContent[0].DNSSettings.ServerName) @@ -51,7 +51,7 @@ func TestHandleMobileConfigDOH(t *testing.T) { _, err = plist.Unmarshal(w.Body.Bytes(), &mc) assert.Nil(t, err) - if assert.Equal(t, 1, len(mc.PayloadContent)) { + if assert.Len(t, mc.PayloadContent, 1) { assert.Equal(t, "example.org DoH", mc.PayloadContent[0].Name) assert.Equal(t, "example.org DoH", mc.PayloadContent[0].PayloadDisplayName) assert.Equal(t, "example.org", mc.PayloadContent[0].DNSSettings.ServerName) @@ -73,6 +73,27 @@ func TestHandleMobileConfigDOH(t *testing.T) { handleMobileConfigDOH(w, r) assert.Equal(t, http.StatusInternalServerError, w.Code) }) + + t.Run("client_id", func(t *testing.T) { + r, err := http.NewRequest(http.MethodGet, "https://example.com:12345/apple/doh.mobileconfig?host=example.org&client_id=cli42", nil) + assert.Nil(t, err) + + w := httptest.NewRecorder() + + handleMobileConfigDOH(w, r) + assert.Equal(t, http.StatusOK, w.Code) + + var mc mobileConfig + _, err = plist.Unmarshal(w.Body.Bytes(), &mc) + assert.Nil(t, err) + + if assert.Len(t, mc.PayloadContent, 1) { + assert.Equal(t, "example.org DoH", mc.PayloadContent[0].Name) + assert.Equal(t, "example.org DoH", mc.PayloadContent[0].PayloadDisplayName) + assert.Equal(t, "example.org", mc.PayloadContent[0].DNSSettings.ServerName) + assert.Equal(t, "https://example.org/dns-query/cli42", mc.PayloadContent[0].DNSSettings.ServerURL) + } + }) } func TestHandleMobileConfigDOT(t *testing.T) { @@ -89,7 +110,7 @@ func TestHandleMobileConfigDOT(t *testing.T) { _, err = plist.Unmarshal(w.Body.Bytes(), &mc) assert.Nil(t, err) - if assert.Equal(t, 1, len(mc.PayloadContent)) { + if assert.Len(t, mc.PayloadContent, 1) { assert.Equal(t, "example.org DoT", mc.PayloadContent[0].Name) assert.Equal(t, "example.org DoT", mc.PayloadContent[0].PayloadDisplayName) assert.Equal(t, "example.org", mc.PayloadContent[0].DNSSettings.ServerName) @@ -116,7 +137,7 @@ func TestHandleMobileConfigDOT(t *testing.T) { _, err = plist.Unmarshal(w.Body.Bytes(), &mc) assert.Nil(t, err) - if assert.Equal(t, 1, len(mc.PayloadContent)) { + if assert.Len(t, mc.PayloadContent, 1) { assert.Equal(t, "example.org DoT", mc.PayloadContent[0].Name) assert.Equal(t, "example.org DoT", mc.PayloadContent[0].PayloadDisplayName) assert.Equal(t, "example.org", mc.PayloadContent[0].DNSSettings.ServerName) @@ -137,4 +158,24 @@ func TestHandleMobileConfigDOT(t *testing.T) { handleMobileConfigDOT(w, r) assert.Equal(t, http.StatusInternalServerError, w.Code) }) + + t.Run("client_id", func(t *testing.T) { + r, err := http.NewRequest(http.MethodGet, "https://example.com:12345/apple/dot.mobileconfig?host=example.org&client_id=cli42", nil) + assert.Nil(t, err) + + w := httptest.NewRecorder() + + handleMobileConfigDOT(w, r) + assert.Equal(t, http.StatusOK, w.Code) + + var mc mobileConfig + _, err = plist.Unmarshal(w.Body.Bytes(), &mc) + assert.Nil(t, err) + + if assert.Len(t, mc.PayloadContent, 1) { + assert.Equal(t, "example.org DoT", mc.PayloadContent[0].Name) + assert.Equal(t, "example.org DoT", mc.PayloadContent[0].PayloadDisplayName) + assert.Equal(t, "cli42.example.org", mc.PayloadContent[0].DNSSettings.ServerName) + } + }) } diff --git a/internal/home/options.go b/internal/home/options.go index 514ed3a1..897cbd08 100644 --- a/internal/home/options.go +++ b/internal/home/options.go @@ -2,8 +2,11 @@ package home import ( "fmt" + "net" "os" "strconv" + + "github.com/AdguardTeam/AdGuardHome/internal/version" ) // options passed from command-line arguments @@ -11,7 +14,7 @@ type options struct { verbose bool // is verbose logging enabled configFilename string // path to the config file workDir string // path to the working directory where we will store the filters data and the querylog - bindHost string // host address to bind HTTP server on + bindHost net.IP // host address to bind HTTP server on bindPort int // port to serve HTTP pages on logFile string // Path to the log file. If empty, write to stdout. If "syslog", writes to syslog pidFile string // File name to save PID to @@ -52,10 +55,19 @@ type arg struct { // against its zero value and return nil if the parameter value is // zero otherwise they return a string slice of the parameter +func ipSliceOrNil(ip net.IP) []string { + if ip == nil { + return nil + } + + return []string{ip.String()} +} + func stringSliceOrNil(s string) []string { if s == "" { return nil } + return []string{s} } @@ -63,6 +75,7 @@ func intSliceOrNil(i int) []string { if i == 0 { return nil } + return []string{strconv.Itoa(i)} } @@ -70,6 +83,7 @@ func boolSliceOrNil(b bool) []string { if b { return []string{} } + return nil } @@ -94,8 +108,8 @@ var workDirArg = arg{ var hostArg = arg{ "Host address to bind HTTP server on", "host", "h", - func(o options, v string) (options, error) { o.bindHost = v; return o, nil }, nil, nil, - func(o options) []string { return stringSliceOrNil(o.bindHost) }, + func(o options, v string) (options, error) { o.bindHost = net.ParseIP(v); return o, nil }, nil, nil, + func(o options) []string { return ipSliceOrNil(o.bindHost) }, } var portArg = arg{ @@ -180,7 +194,7 @@ var versionArg = arg{ "Show the version and exit", "version", "", nil, nil, func(o options, exec string) (effect, error) { - return func() error { fmt.Println(version()); os.Exit(0); return nil }, nil + return func() error { fmt.Println(version.Full()); os.Exit(0); return nil }, nil }, func(o options) []string { return nil }, } diff --git a/internal/home/options_test.go b/internal/home/options_test.go index afaa873f..f24dc816 100644 --- a/internal/home/options_test.go +++ b/internal/home/options_test.go @@ -2,6 +2,7 @@ package home import ( "fmt" + "net" "testing" ) @@ -65,14 +66,14 @@ func TestParseWorkDir(t *testing.T) { } func TestParseBindHost(t *testing.T) { - if testParseOk(t).bindHost != "" { + if testParseOk(t).bindHost != nil { t.Fatal("empty is no host") } - if testParseOk(t, "-h", "addr").bindHost != "addr" { + if !testParseOk(t, "-h", "1.2.3.4").bindHost.Equal(net.IP{1, 2, 3, 4}) { t.Fatal("-h is host") } testParseParamMissing(t, "-h") - if testParseOk(t, "--host", "addr").bindHost != "addr" { + if !testParseOk(t, "--host", "1.2.3.4").bindHost.Equal(net.IP{1, 2, 3, 4}) { t.Fatal("--host is host") } testParseParamMissing(t, "--host") @@ -204,7 +205,7 @@ func TestSerializeWorkDir(t *testing.T) { } func TestSerializeBindHost(t *testing.T) { - testSerialize(t, options{bindHost: "addr"}, "-h", "addr") + testSerialize(t, options{bindHost: net.IP{1, 2, 3, 4}}, "-h", "1.2.3.4") } func TestSerializeBindPort(t *testing.T) { diff --git a/internal/home/rdns.go b/internal/home/rdns.go index 05df66ef..c21a6f6e 100644 --- a/internal/home/rdns.go +++ b/internal/home/rdns.go @@ -2,6 +2,7 @@ package home import ( "encoding/binary" + "net" "strings" "time" @@ -15,7 +16,7 @@ import ( type RDNS struct { dnsServer *dnsforward.Server clients *clientsContainer - ipChannel chan string // pass data from DNS request handling thread to rDNS thread + ipChannel chan net.IP // pass data from DNS request handling thread to rDNS thread // Contains IP addresses of clients to be resolved by rDNS // If IP address is resolved, it stays here while it's inside Clients. @@ -26,24 +27,24 @@ type RDNS struct { // InitRDNS - create module context func InitRDNS(dnsServer *dnsforward.Server, clients *clientsContainer) *RDNS { - r := RDNS{} - r.dnsServer = dnsServer - r.clients = clients + r := &RDNS{ + dnsServer: dnsServer, + clients: clients, + ipAddrs: cache.New(cache.Config{ + EnableLRU: true, + MaxCount: 10000, + }), + ipChannel: make(chan net.IP, 256), + } - cconf := cache.Config{} - cconf.EnableLRU = true - cconf.MaxCount = 10000 - r.ipAddrs = cache.New(cconf) - - r.ipChannel = make(chan string, 256) go r.workerLoop() - return &r + return r } // Begin - add IP address to rDNS queue -func (r *RDNS) Begin(ip string) { +func (r *RDNS) Begin(ip net.IP) { now := uint64(time.Now().Unix()) - expire := r.ipAddrs.Get([]byte(ip)) + expire := r.ipAddrs.Get(ip) if len(expire) != 0 { exp := binary.BigEndian.Uint64(expire) if exp > now { @@ -54,9 +55,10 @@ func (r *RDNS) Begin(ip string) { expire = make([]byte, 8) const ttl = 1 * 60 * 60 binary.BigEndian.PutUint64(expire, now+ttl) - _ = r.ipAddrs.Set([]byte(ip), expire) + _ = r.ipAddrs.Set(ip, expire) - if r.clients.Exists(ip, ClientSourceRDNS) { + id := ip.String() + if r.clients.Exists(id, ClientSourceRDNS) { return } @@ -70,26 +72,26 @@ func (r *RDNS) Begin(ip string) { } // Use rDNS to get hostname by IP address -func (r *RDNS) resolve(ip string) string { +func (r *RDNS) resolve(ip net.IP) string { log.Tracef("Resolving host for %s", ip) - req := dns.Msg{} - req.Id = dns.Id() - req.RecursionDesired = true - req.Question = []dns.Question{ - { - Qtype: dns.TypePTR, - Qclass: dns.ClassINET, - }, - } - var err error - req.Question[0].Name, err = dns.ReverseAddr(ip) + name, err := dns.ReverseAddr(ip.String()) if err != nil { log.Debug("Error while calling dns.ReverseAddr(%s): %s", ip, err) return "" } - resp, err := r.dnsServer.Exchange(&req) + resp, err := r.dnsServer.Exchange(&dns.Msg{ + MsgHdr: dns.MsgHdr{ + Id: dns.Id(), + RecursionDesired: true, + }, + Question: []dns.Question{{ + Name: name, + Qtype: dns.TypePTR, + Qclass: dns.ClassINET, + }}, + }) if err != nil { log.Debug("Error while making an rDNS lookup for %s: %s", ip, err) return "" @@ -123,6 +125,6 @@ func (r *RDNS) workerLoop() { continue } - _, _ = r.clients.AddHost(ip, host, ClientSourceRDNS) + _, _ = r.clients.AddHost(ip.String(), host, ClientSourceRDNS) } } diff --git a/internal/home/rdns_test.go b/internal/home/rdns_test.go index 399886c4..b17efdd8 100644 --- a/internal/home/rdns_test.go +++ b/internal/home/rdns_test.go @@ -1,21 +1,32 @@ package home import ( + "net" "testing" + "github.com/AdguardTeam/AdGuardHome/internal/aghtest" "github.com/AdguardTeam/AdGuardHome/internal/dnsforward" + "github.com/AdguardTeam/dnsproxy/proxy" + "github.com/AdguardTeam/dnsproxy/upstream" "github.com/stretchr/testify/assert" ) func TestResolveRDNS(t *testing.T) { - dns := &dnsforward.Server{} - conf := &dnsforward.ServerConfig{} - conf.UpstreamDNS = []string{"8.8.8.8"} - err := dns.Prepare(conf) - assert.True(t, err == nil, "%s", err) + ups := &aghtest.TestUpstream{ + Reverse: map[string][]string{ + "1.1.1.1.in-addr.arpa.": {"one.one.one.one"}, + }, + } + dns := dnsforward.NewCustomServer(&proxy.Proxy{ + Config: proxy.Config{ + UpstreamConfig: &proxy.UpstreamConfig{ + Upstreams: []upstream.Upstream{ups}, + }, + }, + }) clients := &clientsContainer{} rdns := InitRDNS(dns, clients) - r := rdns.resolve("1.1.1.1") - assert.True(t, r == "one.one.one.one", "%s", r) + r := rdns.resolve(net.IP{1, 1, 1, 1}) + assert.Equal(t, "one.one.one.one", r, r) } diff --git a/internal/home/service.go b/internal/home/service.go index aa243634..3d817446 100644 --- a/internal/home/service.go +++ b/internal/home/service.go @@ -15,6 +15,9 @@ import ( "github.com/kardianos/service" ) +// TODO(a.garipov): Move shell templates into actual files. Either during the +// v0.106.0 cycle using packr or during the following cycle using go:embed. + const ( launchdStdoutPath = "/var/log/AdGuardHome.stdout.log" launchdStderrPath = "/var/log/AdGuardHome.stderr.log" @@ -504,6 +507,10 @@ status() { } ` +// TODO(a.garipov): Don't use .WorkingDirectory here. There are currently no +// guarantees that it will actually be the required directory. +// +// See https://github.com/AdguardTeam/AdGuardHome/issues/2614. const freeBSDScript = `#!/bin/sh # PROVIDE: {{.Name}} # REQUIRE: networking @@ -514,6 +521,6 @@ name="{{.Name}}" {{.Name}}_user="root" pidfile="/var/run/${name}.pid" command="/usr/sbin/daemon" -command_args="-P ${pidfile} -r -f {{.WorkingDirectory}}/{{.Name}}" +command_args="-P ${pidfile} -f -r {{.WorkingDirectory}}/{{.Name}}" run_rc_command "$1" ` diff --git a/internal/home/tls.go b/internal/home/tls.go index de6dcd18..d97207ee 100644 --- a/internal/home/tls.go +++ b/internal/home/tls.go @@ -1,6 +1,7 @@ package home import ( + "context" "crypto" "crypto/ecdsa" "crypto/rsa" @@ -92,7 +93,7 @@ func (t *TLSMod) setCertFileTime() { t.certLastMod = fi.ModTime().UTC() } -// Start - start the module +// Start updates the configuration of TLSMod and starts it. func (t *TLSMod) Start() { if !tlsWebHandlersRegistered { tlsWebHandlersRegistered = true @@ -102,10 +103,14 @@ func (t *TLSMod) Start() { t.confLock.Lock() tlsConf := t.conf t.confLock.Unlock() - Context.web.TLSConfigChanged(tlsConf) + + // The background context is used because the TLSConfigChanged wraps + // context with timeout on its own and shuts down the server, which + // handles current request. + Context.web.TLSConfigChanged(context.Background(), tlsConf) } -// Reload - reload certificate file +// Reload updates the configuration of TLSMod and restarts it. func (t *TLSMod) Reload() { t.confLock.Lock() tlsConf := t.conf @@ -139,7 +144,10 @@ func (t *TLSMod) Reload() { t.confLock.Lock() tlsConf = t.conf t.confLock.Unlock() - Context.web.TLSConfigChanged(tlsConf) + // The background context is used because the TLSConfigChanged wraps + // context with timeout on its own and shuts down the server, which + // handles current request. + Context.web.TLSConfigChanged(context.Background(), tlsConf) } // Set certificate and private key data @@ -296,11 +304,13 @@ func (t *TLSMod) handleTLSConfigure(w http.ResponseWriter, r *http.Request) { f.Flush() } - // this needs to be done in a goroutine because Shutdown() is a blocking call, and it will block - // until all requests are finished, and _we_ are inside a request right now, so it will block indefinitely + // The background context is used because the TLSConfigChanged wraps + // context with timeout on its own and shuts down the server, which + // handles current request. It is also should be done in a separate + // goroutine due to the same reason. if restartHTTPS { go func() { - Context.web.TLSConfigChanged(data) + Context.web.TLSConfigChanged(context.Background(), data) }() } } @@ -534,7 +544,7 @@ func marshalTLS(w http.ResponseWriter, data tlsConfig) { // registerWebHandlers registers HTTP handlers for TLS configuration func (t *TLSMod) registerWebHandlers() { - httpRegister("GET", "/control/tls/status", t.handleTLSStatus) - httpRegister("POST", "/control/tls/configure", t.handleTLSConfigure) - httpRegister("POST", "/control/tls/validate", t.handleTLSValidate) + httpRegister(http.MethodGet, "/control/tls/status", t.handleTLSStatus) + httpRegister(http.MethodPost, "/control/tls/configure", t.handleTLSConfigure) + httpRegister(http.MethodPost, "/control/tls/validate", t.handleTLSValidate) } diff --git a/internal/home/upgrade_test.go b/internal/home/upgrade_test.go index f884de04..4210305a 100644 --- a/internal/home/upgrade_test.go +++ b/internal/home/upgrade_test.go @@ -3,183 +3,108 @@ package home import ( "fmt" "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) -func TestUpgrade1to2(t *testing.T) { - // let's create test config for 1 schema version - diskConfig := createTestDiskConfig(1) +// any is a convenient alias for interface{}. +type any = interface{} - // update config - err := upgradeSchema1to2(&diskConfig) - if err != nil { - t.Fatalf("Can't upgrade schema version from 1 to 2") - } +// object is a convenient alias for map[string]interface{}. +type object = map[string]any - // ensure that schema version was bumped - compareSchemaVersion(t, diskConfig["schema_version"], 2) +func TestUpgradeSchema1to2(t *testing.T) { + diskConf := testDiskConf(1) - // old coredns entry should be removed - _, ok := diskConfig["coredns"] - if ok { - t.Fatalf("Core DNS config was not removed after upgrade schema version from 1 to 2") - } + err := upgradeSchema1to2(&diskConf) + require.Nil(t, err) - // pull out new dns config - dnsMap, ok := diskConfig["dns"] - if !ok { - t.Fatalf("No DNS config after upgrade schema version from 1 to 2") - } + require.Equal(t, diskConf["schema_version"], 2) - // cast dns configurations to maps and compare them - oldDNSConfig := castInterfaceToMap(t, createTestDNSConfig(1)) - newDNSConfig := castInterfaceToMap(t, dnsMap) - compareConfigs(t, &oldDNSConfig, &newDNSConfig) + _, ok := diskConf["coredns"] + require.False(t, ok) + + dnsMap, ok := diskConf["dns"] + require.True(t, ok) + + oldDNSConf := convertToObject(t, testDNSConf(1)) + newDNSConf := convertToObject(t, dnsMap) + assert.Equal(t, oldDNSConf, newDNSConf) - // exclude dns config and schema version from disk config comparison oldExcludedEntries := []string{"coredns", "schema_version"} newExcludedEntries := []string{"dns", "schema_version"} - oldDiskConfig := createTestDiskConfig(1) - compareConfigsWithoutEntries(t, &oldDiskConfig, &diskConfig, oldExcludedEntries, newExcludedEntries) + oldDiskConf := testDiskConf(1) + assertEqualExcept(t, oldDiskConf, diskConf, oldExcludedEntries, newExcludedEntries) } -func TestUpgrade2to3(t *testing.T) { - // let's create test config - diskConfig := createTestDiskConfig(2) +func TestUpgradeSchema2to3(t *testing.T) { + diskConf := testDiskConf(2) - // upgrade schema from 2 to 3 - err := upgradeSchema2to3(&diskConfig) - if err != nil { - t.Fatalf("Can't update schema version from 2 to 3: %s", err) - } + err := upgradeSchema2to3(&diskConf) + require.Nil(t, err) - // check new schema version - compareSchemaVersion(t, diskConfig["schema_version"], 3) + require.Equal(t, diskConf["schema_version"], 3) - // pull out new dns configuration - dnsMap, ok := diskConfig["dns"] - if !ok { - t.Fatalf("No dns config in new configuration") - } + dnsMap, ok := diskConf["dns"] + require.True(t, ok) - // cast dns configuration to map - newDNSConfig := castInterfaceToMap(t, dnsMap) - - // check if bootstrap DNS becomes an array - bootstrapDNS := newDNSConfig["bootstrap_dns"] + newDNSConf := convertToObject(t, dnsMap) + bootstrapDNS := newDNSConf["bootstrap_dns"] switch v := bootstrapDNS.(type) { case []string: - if len(v) != 1 { - t.Fatalf("Wrong count of bootsrap DNS servers: %d", len(v)) - } - - if v[0] != "8.8.8.8:53" { - t.Fatalf("Bootsrap DNS server is not 8.8.8.8:53 : %s", v[0]) - } + require.Len(t, v, 1) + require.Equal(t, "8.8.8.8:53", v[0]) default: - t.Fatalf("Wrong type for bootsrap DNS: %T", v) + t.Fatalf("wrong type for bootsrap dns: %T", v) } - // exclude bootstrap DNS from DNS configs comparison excludedEntries := []string{"bootstrap_dns"} - oldDNSConfig := castInterfaceToMap(t, createTestDNSConfig(2)) - compareConfigsWithoutEntries(t, &oldDNSConfig, &newDNSConfig, excludedEntries, excludedEntries) + oldDNSConf := convertToObject(t, testDNSConf(2)) + assertEqualExcept(t, oldDNSConf, newDNSConf, excludedEntries, excludedEntries) - // excluded dns config and schema version from disk config comparison excludedEntries = []string{"dns", "schema_version"} - oldDiskConfig := createTestDiskConfig(2) - compareConfigsWithoutEntries(t, &oldDiskConfig, &diskConfig, excludedEntries, excludedEntries) + oldDiskConf := testDiskConf(2) + assertEqualExcept(t, oldDiskConf, diskConf, excludedEntries, excludedEntries) } -func castInterfaceToMap(t *testing.T, oldConfig interface{}) (newConfig map[string]interface{}) { - newConfig = make(map[string]interface{}) - switch v := oldConfig.(type) { - case map[interface{}]interface{}: +func convertToObject(t *testing.T, oldConf any) (newConf object) { + t.Helper() + + switch v := oldConf.(type) { + case map[any]any: + newConf = make(object, len(v)) for key, value := range v { - newConfig[fmt.Sprint(key)] = value + newConf[fmt.Sprint(key)] = value } - case map[string]interface{}: + case object: + newConf = make(object, len(v)) for key, value := range v { - newConfig[key] = value + newConf[key] = value } default: - t.Fatalf("DNS configuration is not a map") + t.Fatalf("dns configuration is not a map, got %T", oldConf) } - return + + return newConf } -// compareConfigsWithoutEntry removes entries from configs and returns result of compareConfigs -func compareConfigsWithoutEntries(t *testing.T, oldConfig, newConfig *map[string]interface{}, oldKey, newKey []string) { - for _, k := range oldKey { - delete(*oldConfig, k) +// assertEqualExcept removes entries from configs and compares them. +func assertEqualExcept(t *testing.T, oldConf, newConf object, oldKeys, newKeys []string) { + t.Helper() + + for _, k := range oldKeys { + delete(oldConf, k) } - for _, k := range newKey { - delete(*newConfig, k) + for _, k := range newKeys { + delete(newConf, k) } - compareConfigs(t, oldConfig, newConfig) + + assert.Equal(t, oldConf, newConf) } -// compares configs before and after schema upgrade -func compareConfigs(t *testing.T, oldConfig, newConfig *map[string]interface{}) { - if len(*oldConfig) != len(*newConfig) { - t.Fatalf("wrong config entries count! Before upgrade: %d; After upgrade: %d", len(*oldConfig), len(*oldConfig)) - } - - // Check old and new entries - for k, v := range *newConfig { - switch value := v.(type) { - case string: - if value != (*oldConfig)[k] { - t.Fatalf("wrong value for string %s. Before update: %s; After update: %s", k, (*oldConfig)[k], value) - } - case int: - if value != (*oldConfig)[k] { - t.Fatalf("wrong value for int %s. Before update: %d; After update: %d", k, (*oldConfig)[k], value) - } - case []string: - for i, line := range value { - if len((*oldConfig)[k].([]string)) != len(value) { - t.Fatalf("wrong array length for %s. Before update: %d; After update: %d", k, len((*oldConfig)[k].([]string)), len(value)) - } - if (*oldConfig)[k].([]string)[i] != line { - t.Fatalf("wrong data for string array %s. Before update: %s; After update: %s", k, (*oldConfig)[k].([]string)[i], line) - } - } - case bool: - if v != (*oldConfig)[k].(bool) { - t.Fatalf("wrong boolean value for %s", k) - } - case []filter: - if len((*oldConfig)[k].([]filter)) != len(value) { - t.Fatalf("wrong filters count. Before update: %d; After update: %d", len((*oldConfig)[k].([]filter)), len(value)) - } - for i, newFilter := range value { - oldFilter := (*oldConfig)[k].([]filter)[i] - if oldFilter.Enabled != newFilter.Enabled || oldFilter.Name != newFilter.Name || oldFilter.RulesCount != newFilter.RulesCount { - t.Fatalf("old filter %s not equals new filter %s", oldFilter.Name, newFilter.Name) - } - } - default: - t.Fatalf("uknown data type for %s: %T", k, value) - } - } -} - -// compareSchemaVersion check if newSchemaVersion equals schemaVersion -func compareSchemaVersion(t *testing.T, newSchemaVersion interface{}, schemaVersion int) { - switch v := newSchemaVersion.(type) { - case int: - if v != schemaVersion { - t.Fatalf("Wrong schema version in new config file") - } - default: - t.Fatalf("Schema version is not an integer after update") - } -} - -func createTestDiskConfig(schemaVersion int) (diskConfig map[string]interface{}) { - diskConfig = make(map[string]interface{}) - diskConfig["language"] = "en" - diskConfig["filters"] = []filter{ +func testDiskConf(schemaVersion int) (diskConf object) { + filters := []filter{ { URL: "https://filters.adtidy.org/android/filters/111_optimized.txt", Name: "Latvian filter", @@ -191,40 +116,51 @@ func createTestDiskConfig(schemaVersion int) (diskConfig map[string]interface{}) RulesCount: 200, }, } - diskConfig["user_rules"] = []string{} - diskConfig["schema_version"] = schemaVersion - diskConfig["bind_host"] = "0.0.0.0" - diskConfig["bind_port"] = 80 - diskConfig["auth_name"] = "name" - diskConfig["auth_pass"] = "pass" - dnsConfig := createTestDNSConfig(schemaVersion) - if schemaVersion > 1 { - diskConfig["dns"] = dnsConfig - } else { - diskConfig["coredns"] = dnsConfig + diskConf = object{ + "language": "en", + "filters": filters, + "user_rules": []string{}, + "schema_version": schemaVersion, + "bind_host": "0.0.0.0", + "bind_port": 80, + "auth_name": "name", + "auth_pass": "pass", } - return diskConfig + + dnsConf := testDNSConf(schemaVersion) + if schemaVersion > 1 { + diskConf["dns"] = dnsConf + } else { + diskConf["coredns"] = dnsConf + } + + return diskConf } -func createTestDNSConfig(schemaVersion int) map[interface{}]interface{} { - dnsConfig := make(map[interface{}]interface{}) - dnsConfig["port"] = 53 - dnsConfig["blocked_response_ttl"] = 10 - dnsConfig["querylog_enabled"] = true - dnsConfig["ratelimit"] = 20 - dnsConfig["bootstrap_dns"] = "8.8.8.8:53" - if schemaVersion > 2 { - dnsConfig["bootstrap_dns"] = []string{"8.8.8.8:53"} +// testDNSConf creates a DNS config for test the way gopkg.in/yaml.v2 would +// unmarshal it. In YAML, keys aren't guaranteed to always only be strings. +func testDNSConf(schemaVersion int) (dnsConf map[any]any) { + dnsConf = map[any]any{ + "port": 53, + "blocked_response_ttl": 10, + "querylog_enabled": true, + "ratelimit": 20, + "bootstrap_dns": "8.8.8.8:53", + "parental_sensitivity": 13, + "ratelimit_whitelist": []string{}, + "upstream_dns": []string{"tls://1.1.1.1", "tls://1.0.0.1", "8.8.8.8"}, + "filtering_enabled": true, + "refuse_any": true, + "parental_enabled": true, + "bind_host": "0.0.0.0", + "protection_enabled": true, + "safesearch_enabled": true, + "safebrowsing_enabled": true, } - dnsConfig["parental_sensitivity"] = 13 - dnsConfig["ratelimit_whitelist"] = []string{} - dnsConfig["upstream_dns"] = []string{"tls://1.1.1.1", "tls://1.0.0.1", "8.8.8.8"} - dnsConfig["filtering_enabled"] = true - dnsConfig["refuse_any"] = true - dnsConfig["parental_enabled"] = true - dnsConfig["bind_host"] = "0.0.0.0" - dnsConfig["protection_enabled"] = true - dnsConfig["safesearch_enabled"] = true - dnsConfig["safebrowsing_enabled"] = true - return dnsConfig + + if schemaVersion > 2 { + dnsConf["bootstrap_dns"] = []string{"8.8.8.8:53"} + } + + return dnsConf } diff --git a/internal/home/web.go b/internal/home/web.go index 97f06979..e016e341 100644 --- a/internal/home/web.go +++ b/internal/home/web.go @@ -16,24 +16,22 @@ import ( ) const ( - // ReadTimeout is the maximum duration for reading the entire request, + // readTimeout is the maximum duration for reading the entire request, // including the body. - ReadTimeout = 10 * time.Second - - // ReadHeaderTimeout is the amount of time allowed to read request - // headers. - ReadHeaderTimeout = 10 * time.Second - - // WriteTimeout is the maximum duration before timing out writes of the + 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 = 10 * time.Second + writeTimeout = 60 * time.Second ) type webConfig struct { - firstRun bool - BindHost string - BindPort int - PortHTTPS int + firstRun bool + BindHost net.IP + BindPort int + BetaBindPort int + PortHTTPS int // ReadTimeout is an option to pass to http.Server for setting an // appropriate field. @@ -62,9 +60,16 @@ type HTTPSServer struct { type Web struct { conf *webConfig forceHTTPS bool - portHTTPS int httpServer *http.Server // HTTP module httpsServer HTTPSServer // HTTPS module + + // handlerBeta is the handler for new client. + handlerBeta http.Handler + // installerBeta is the pre-install handler for new client. + installerBeta http.Handler + + // httpServerBeta is a server for new client. + httpServerBeta *http.Server } // CreateWeb - create module @@ -76,15 +81,20 @@ func CreateWeb(conf *webConfig) *Web { // Initialize and run the admin Web interface box := packr.NewBox("../../build/static") + boxBeta := packr.NewBox("../../build2/static") // if not configured, redirect / to /install.html, otherwise redirect /install.html to / - Context.mux.Handle("/", postInstallHandler(optionalAuthHandler(gziphandler.GzipHandler(http.FileServer(box))))) + Context.mux.Handle("/", withMiddlewares(http.FileServer(box), gziphandler.GzipHandler, optionalAuthHandler, postInstallHandler)) + w.handlerBeta = withMiddlewares(http.FileServer(boxBeta), gziphandler.GzipHandler, optionalAuthHandler, postInstallHandler) // add handlers for /install paths, we only need them when we're not configured yet if conf.firstRun { log.Info("This is the first launch of AdGuard Home, redirecting everything to /install.html ") Context.mux.Handle("/install.html", preInstallHandler(http.FileServer(box))) + w.installerBeta = preInstallHandler(http.FileServer(boxBeta)) w.registerInstallHandlers() + // This must be removed in API v1. + w.registerBetaInstallHandlers() } else { registerControlHandlers() } @@ -109,12 +119,12 @@ func WebCheckPortAvailable(port int) bool { return true } -// TLSConfigChanged - called when TLS configuration has changed -func (web *Web) TLSConfigChanged(tlsConf tlsConfigSettings) { +// TLSConfigChanged updates the TLS configuration and restarts the HTTPS server +// if necessary. +func (web *Web) TLSConfigChanged(ctx context.Context, tlsConf tlsConfigSettings) { log.Debug("Web: applying new TLS configuration") web.conf.PortHTTPS = tlsConf.PortHTTPS web.forceHTTPS = (tlsConf.ForceHTTPS && tlsConf.Enabled && tlsConf.PortHTTPS != 0) - web.portHTTPS = tlsConf.PortHTTPS enabled := tlsConf.Enabled && tlsConf.PortHTTPS != 0 && @@ -131,7 +141,12 @@ func (web *Web) TLSConfigChanged(tlsConf tlsConfigSettings) { web.httpsServer.cond.L.Lock() if web.httpsServer.server != nil { - _ = web.httpsServer.server.Shutdown(context.TODO()) + ctx, cancel := context.WithTimeout(ctx, shutdownTimeout) + err = web.httpsServer.server.Shutdown(ctx) + cancel() + if err != nil { + log.Debug("error while shutting down HTTP server: %s", err) + } } web.httpsServer.enabled = enabled web.httpsServer.cert = cert @@ -147,19 +162,40 @@ func (web *Web) Start() { // this loop is used as an ability to change listening host and/or port for !web.httpsServer.shutdown { printHTTPAddresses("http") + errs := make(chan error, 2) + hostStr := web.conf.BindHost.String() // we need to have new instance, because after Shutdown() the Server is not usable - address := net.JoinHostPort(web.conf.BindHost, strconv.Itoa(web.conf.BindPort)) web.httpServer = &http.Server{ ErrorLog: log.StdLog("web: http", log.DEBUG), - Addr: address, + Addr: net.JoinHostPort(hostStr, strconv.Itoa(web.conf.BindPort)), Handler: withMiddlewares(Context.mux, limitRequestBody), ReadTimeout: web.conf.ReadTimeout, ReadHeaderTimeout: web.conf.ReadHeaderTimeout, WriteTimeout: web.conf.WriteTimeout, } + go func() { + errs <- web.httpServer.ListenAndServe() + }() - err := web.httpServer.ListenAndServe() + if web.conf.BetaBindPort != 0 { + web.httpServerBeta = &http.Server{ + ErrorLog: log.StdLog("web: http", log.DEBUG), + Addr: net.JoinHostPort(hostStr, strconv.Itoa(web.conf.BetaBindPort)), + Handler: withMiddlewares(Context.mux, limitRequestBody, web.wrapIndexBeta), + ReadTimeout: web.conf.ReadTimeout, + ReadHeaderTimeout: web.conf.ReadHeaderTimeout, + WriteTimeout: web.conf.WriteTimeout, + } + go func() { + betaErr := web.httpServerBeta.ListenAndServe() + if betaErr != nil { + log.Error("starting beta http server: %s", betaErr) + } + }() + } + + err := <-errs if err != http.ErrServerClosed { cleanupAlways() log.Fatal(err) @@ -168,19 +204,28 @@ func (web *Web) Start() { } } -// Close - stop HTTP server, possibly waiting for all active connections to be closed -func (web *Web) Close() { +// Close gracefully shuts down the HTTP servers. +func (web *Web) Close(ctx context.Context) { log.Info("Stopping HTTP server...") web.httpsServer.cond.L.Lock() web.httpsServer.shutdown = true web.httpsServer.cond.L.Unlock() - if web.httpsServer.server != nil { - _ = web.httpsServer.server.Shutdown(context.TODO()) - } - if web.httpServer != nil { - _ = web.httpServer.Shutdown(context.TODO()) + + shut := func(srv *http.Server) { + if srv == nil { + return + } + ctx, cancel := context.WithTimeout(ctx, shutdownTimeout) + defer cancel() + if err := srv.Shutdown(ctx); err != nil { + log.Debug("error while shutting down HTTP server: %s", err) + } } + shut(web.httpsServer.server) + shut(web.httpServer) + shut(web.httpServerBeta) + log.Info("Stopped HTTP server") } @@ -204,7 +249,7 @@ func (web *Web) tlsServerLoop() { web.httpsServer.cond.L.Unlock() // prepare HTTPS server - address := net.JoinHostPort(web.conf.BindHost, strconv.Itoa(web.conf.PortHTTPS)) + address := net.JoinHostPort(web.conf.BindHost.String(), strconv.Itoa(web.conf.PortHTTPS)) web.httpsServer.server = &http.Server{ ErrorLog: log.StdLog("web: https", log.DEBUG), Addr: address, @@ -214,7 +259,7 @@ func (web *Web) tlsServerLoop() { RootCAs: Context.tlsRoots, CipherSuites: Context.tlsCiphers, }, - Handler: Context.mux, + Handler: withMiddlewares(Context.mux, limitRequestBody), ReadTimeout: web.conf.ReadTimeout, ReadHeaderTimeout: web.conf.ReadHeaderTimeout, WriteTimeout: web.conf.WriteTimeout, diff --git a/internal/home/whois.go b/internal/home/whois.go index 4884d776..20f035e2 100644 --- a/internal/home/whois.go +++ b/internal/home/whois.go @@ -11,7 +11,6 @@ import ( "github.com/AdguardTeam/AdGuardHome/internal/aghio" "github.com/AdguardTeam/AdGuardHome/internal/util" - "github.com/AdguardTeam/golibs/cache" "github.com/AdguardTeam/golibs/log" ) @@ -25,29 +24,32 @@ const ( // Whois - module context type Whois struct { - clients *clientsContainer - ipChan chan string - timeoutMsec uint + clients *clientsContainer + ipChan chan net.IP // 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 } -// Create module context +// initWhois creates the Whois module context. func initWhois(clients *clientsContainer) *Whois { - w := Whois{} - w.timeoutMsec = 5000 - w.clients = clients + w := Whois{ + timeoutMsec: 5000, + clients: clients, + ipAddrs: cache.New(cache.Config{ + EnableLRU: true, + MaxCount: 10000, + }), + ipChan: make(chan net.IP, 255), + } - cconf := cache.Config{} - cconf.EnableLRU = true - cconf.MaxCount = 10000 - w.ipAddrs = cache.New(cconf) - - w.ipChan = make(chan string, 255) go w.workerLoop() + return &w } @@ -81,23 +83,16 @@ func whoisParse(data string) map[string]string { switch k { case "org-name": m["orgname"] = trimValue(v) - case "orgname": - fallthrough - case "city": - fallthrough - case "country": + case "city", "country", "orgname": m[k] = trimValue(v) - case "descr": if len(descr) == 0 { descr = v } case "netname": netname = v - case "whois": // "whois: whois.arin.net" m["whois"] = v - case "referralserver": // "ReferralServer: whois://whois.ripe.net" if strings.HasPrefix(v, "whois://") { m["whois"] = v[len("whois://"):] @@ -105,12 +100,16 @@ func whoisParse(data string) map[string]string { } } - // descr or netname -> orgname _, ok := m["orgname"] - if !ok && len(descr) != 0 { - m["orgname"] = trimValue(descr) - } else if !ok && len(netname) != 0 { - m["orgname"] = trimValue(netname) + if !ok { + // Set orgname from either descr or netname for the frontent. + // + // TODO(a.garipov): Perhaps don't do that in the V1 HTTP API? + if descr != "" { + m["orgname"] = trimValue(descr) + } else if netname != "" { + m["orgname"] = trimValue(netname) + } } return m @@ -120,12 +119,12 @@ func whoisParse(data string) map[string]string { const MaxConnReadSize = 64 * 1024 // Send request to a server and receive the response -func (w *Whois) query(target, serverAddr string) (string, error) { +func (w *Whois) query(ctx context.Context, target, serverAddr string) (string, error) { addr, _, _ := net.SplitHostPort(serverAddr) if addr == "whois.arin.net" { target = "n + " + target } - conn, err := customDialContext(context.TODO(), "tcp", serverAddr) + conn, err := customDialContext(ctx, "tcp", serverAddr) if err != nil { return "", err } @@ -153,11 +152,11 @@ func (w *Whois) query(target, serverAddr string) (string, error) { } // Query WHOIS servers (handle redirects) -func (w *Whois) queryAll(target string) (string, error) { +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(target, server) + resp, err := w.query(ctx, target, server) if err != nil { return "", err } @@ -183,9 +182,9 @@ func (w *Whois) queryAll(target string) (string, error) { } // Request WHOIS information -func (w *Whois) process(ip string) [][]string { +func (w *Whois) process(ctx context.Context, ip net.IP) [][]string { data := [][]string{} - resp, err := w.queryAll(ip) + resp, err := w.queryAll(ctx, ip.String()) if err != nil { log.Debug("Whois: error: %s IP:%s", err, ip) return data @@ -209,7 +208,7 @@ func (w *Whois) process(ip string) [][]string { } // Begin - begin requesting WHOIS info -func (w *Whois) Begin(ip string) { +func (w *Whois) Begin(ip net.IP) { now := uint64(time.Now().Unix()) expire := w.ipAddrs.Get([]byte(ip)) if len(expire) != 0 { @@ -232,16 +231,18 @@ func (w *Whois) Begin(ip string) { } } -// Get IP address from channel; get WHOIS info; associate info with a client +// 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 := <-w.ipChan - info := w.process(ip) + info := w.process(context.Background(), ip) if len(info) == 0 { continue } - w.clients.SetWhoisInfo(ip, info) + id := ip.String() + w.clients.SetWhoisInfo(id, info) } } diff --git a/internal/home/whois_test.go b/internal/home/whois_test.go index f8109e92..ed72740d 100644 --- a/internal/home/whois_test.go +++ b/internal/home/whois_test.go @@ -1,6 +1,7 @@ package home import ( + "context" "testing" "github.com/AdguardTeam/AdGuardHome/internal/dnsforward" @@ -12,14 +13,19 @@ func prepareTestDNSServer() error { Context.dnsServer = dnsforward.NewServer(dnsforward.DNSCreateParams{}) conf := &dnsforward.ServerConfig{} conf.UpstreamDNS = []string{"8.8.8.8"} + return Context.dnsServer.Prepare(conf) } +// TODO(e.burkov): It's kind of complicated to get rid of network access in this +// test. The thing is that *Whois creates new *net.Dialer each time it requests +// the server, so it becomes hard to simulate handling of request from test even +// with substituted upstream. However, it must be done. func TestWhois(t *testing.T) { assert.Nil(t, prepareTestDNSServer()) w := Whois{timeoutMsec: 5000} - resp, err := w.queryAll("8.8.8.8") + resp, err := w.queryAll(context.Background(), "8.8.8.8") assert.Nil(t, err) m := whoisParse(resp) assert.Equal(t, "Google LLC", m["orgname"]) diff --git a/internal/querylog/decode.go b/internal/querylog/decode.go index ed721489..3e9a5f33 100644 --- a/internal/querylog/decode.go +++ b/internal/querylog/decode.go @@ -17,14 +17,26 @@ import ( type logEntryHandler (func(t json.Token, ent *logEntry) error) var logEntryHandlers = map[string]logEntryHandler{ + "CID": func(t json.Token, ent *logEntry) error { + v, ok := t.(string) + if !ok { + return nil + } + + ent.ClientID = v + + return nil + }, "IP": func(t json.Token, ent *logEntry) error { v, ok := t.(string) if !ok { return nil } - if len(ent.IP) == 0 { - ent.IP = v + + if ent.IP == nil { + ent.IP = net.ParseIP(v) } + return nil }, "T": func(t json.Token, ent *logEntry) error { diff --git a/internal/querylog/decode_test.go b/internal/querylog/decode_test.go index ffcf94dc..fe64a624 100644 --- a/internal/querylog/decode_test.go +++ b/internal/querylog/decode_test.go @@ -8,8 +8,8 @@ import ( "testing" "time" + "github.com/AdguardTeam/AdGuardHome/internal/aghtest" "github.com/AdguardTeam/AdGuardHome/internal/dnsfilter" - "github.com/AdguardTeam/AdGuardHome/internal/testutil" "github.com/AdguardTeam/golibs/log" "github.com/AdguardTeam/urlfilter/rules" "github.com/miekg/dns" @@ -19,12 +19,13 @@ import ( func TestDecodeLogEntry(t *testing.T) { logOutput := &bytes.Buffer{} - testutil.ReplaceLogWriter(t, logOutput) - testutil.ReplaceLogLevel(t, log.DEBUG) + aghtest.ReplaceLogWriter(t, logOutput) + aghtest.ReplaceLogLevel(t, log.DEBUG) t.Run("success", func(t *testing.T) { const ansStr = `Qz+BgAABAAEAAAAAAmFuBnlhbmRleAJydQAAAQABwAwAAQABAAAACgAEAAAAAA==` const data = `{"IP":"127.0.0.1",` + + `"CID":"cli42",` + `"T":"2020-11-25T18:55:56.519796+03:00",` + `"QH":"an.yandex.ru",` + `"QT":"A",` + @@ -47,11 +48,12 @@ func TestDecodeLogEntry(t *testing.T) { assert.Nil(t, err) want := &logEntry{ - IP: "127.0.0.1", + IP: net.IPv4(127, 0, 0, 1), Time: time.Date(2020, 11, 25, 15, 55, 56, 519796000, time.UTC), QHost: "an.yandex.ru", QType: "A", QClass: "IN", + ClientID: "cli42", ClientProto: "", Answer: ans, Result: dnsfilter.Result{ @@ -84,7 +86,7 @@ func TestDecodeLogEntry(t *testing.T) { decodeLogEntry(got, data) s := logOutput.String() - assert.Equal(t, "", s) + assert.Empty(t, s) // Correct for time zones. got.Time = got.Time.UTC() @@ -172,7 +174,7 @@ func TestDecodeLogEntry(t *testing.T) { s := logOutput.String() if tc.want == "" { - assert.Equal(t, "", s) + assert.Empty(t, s) } else { assert.True(t, strings.HasSuffix(s, tc.want), "got %q", s) diff --git a/internal/querylog/http.go b/internal/querylog/http.go index 9bc63b7e..8948f9f6 100644 --- a/internal/querylog/http.go +++ b/internal/querylog/http.go @@ -22,10 +22,10 @@ type qlogConfig struct { // Register web handlers func (l *queryLog) initWeb() { - l.conf.HTTPRegister("GET", "/control/querylog", l.handleQueryLog) - l.conf.HTTPRegister("GET", "/control/querylog_info", l.handleQueryLogInfo) - l.conf.HTTPRegister("POST", "/control/querylog_clear", l.handleQueryLogClear) - l.conf.HTTPRegister("POST", "/control/querylog_config", l.handleQueryLogConfig) + l.conf.HTTPRegister(http.MethodGet, "/control/querylog", l.handleQueryLog) + l.conf.HTTPRegister(http.MethodGet, "/control/querylog_info", l.handleQueryLogInfo) + l.conf.HTTPRegister(http.MethodPost, "/control/querylog_clear", l.handleQueryLogClear) + l.conf.HTTPRegister(http.MethodPost, "/control/querylog_config", l.handleQueryLogConfig) } func httpError(r *http.Request, w http.ResponseWriter, code int, format string, args ...interface{}) { diff --git a/internal/querylog/json.go b/internal/querylog/json.go index 3beeb0f1..306a6bcb 100644 --- a/internal/querylog/json.go +++ b/internal/querylog/json.go @@ -4,6 +4,7 @@ import ( "fmt" "net" "strconv" + "strings" "time" "github.com/AdguardTeam/AdGuardHome/internal/dnsfilter" @@ -14,22 +15,19 @@ import ( // TODO(a.garipov): Use a proper structured approach here. // Get Client IP address -func (l *queryLog) getClientIP(clientIP string) string { - if l.conf.AnonymizeClientIP { - ip := net.ParseIP(clientIP) - if ip != nil { - ip4 := ip.To4() - const AnonymizeClientIP4Mask = 16 - const AnonymizeClientIP6Mask = 112 - if ip4 != nil { - clientIP = ip4.Mask(net.CIDRMask(AnonymizeClientIP4Mask, 32)).String() - } else { - clientIP = ip.Mask(net.CIDRMask(AnonymizeClientIP6Mask, 128)).String() - } +func (l *queryLog) getClientIP(ip net.IP) (clientIP net.IP) { + if l.conf.AnonymizeClientIP && ip != nil { + const AnonymizeClientIPv4Mask = 16 + const AnonymizeClientIPv6Mask = 112 + + if ip.To4() != nil { + return ip.Mask(net.CIDRMask(AnonymizeClientIPv4Mask, 32)) } + + return ip.Mask(net.CIDRMask(AnonymizeClientIPv6Mask, 128)) } - return clientIP + return ip } // jobject is a JSON object alias. @@ -82,6 +80,10 @@ func (l *queryLog) logEntryToJSONEntry(entry *logEntry) (jsonEntry jobject) { }, } + if entry.ClientID != "" { + jsonEntry["client_id"] = entry.ClientID + } + if msg != nil { jsonEntry["status"] = dns.RcodeToString[msg.Rcode] @@ -138,48 +140,60 @@ func resultRulesToJSONRules(rules []*dnsfilter.ResultRule) (jsonRules []jobject) return jsonRules } -func answerToMap(a *dns.Msg) (answers []jobject) { +type dnsAnswer struct { + Type string `json:"type"` + Value string `json:"value"` + TTL uint32 `json:"ttl"` +} + +func answerToMap(a *dns.Msg) (answers []*dnsAnswer) { if a == nil || len(a.Answer) == 0 { return nil } - answers = []jobject{} + answers = make([]*dnsAnswer, 0, len(a.Answer)) for _, k := range a.Answer { header := k.Header() - answer := jobject{ - "type": dns.TypeToString[header.Rrtype], - "ttl": header.Ttl, + answer := &dnsAnswer{ + Type: dns.TypeToString[header.Rrtype], + TTL: header.Ttl, } - // try most common record types + + // Some special treatment for some well-known types. + // + // TODO(a.garipov): Consider just calling String() for everyone + // instead. switch v := k.(type) { + case nil: + // Probably unlikely, but go on. case *dns.A: - answer["value"] = v.A.String() + answer.Value = v.A.String() case *dns.AAAA: - answer["value"] = v.AAAA.String() + answer.Value = v.AAAA.String() case *dns.MX: - answer["value"] = fmt.Sprintf("%v %v", v.Preference, v.Mx) + answer.Value = fmt.Sprintf("%v %v", v.Preference, v.Mx) case *dns.CNAME: - answer["value"] = v.Target + answer.Value = v.Target case *dns.NS: - answer["value"] = v.Ns + answer.Value = v.Ns case *dns.SPF: - answer["value"] = v.Txt + answer.Value = strings.Join(v.Txt, "\n") case *dns.TXT: - answer["value"] = v.Txt + answer.Value = strings.Join(v.Txt, "\n") case *dns.PTR: - answer["value"] = v.Ptr + answer.Value = v.Ptr case *dns.SOA: - answer["value"] = fmt.Sprintf("%v %v %v %v %v %v %v", v.Ns, v.Mbox, v.Serial, v.Refresh, v.Retry, v.Expire, v.Minttl) + answer.Value = fmt.Sprintf("%v %v %v %v %v %v %v", v.Ns, v.Mbox, v.Serial, v.Refresh, v.Retry, v.Expire, v.Minttl) case *dns.CAA: - answer["value"] = fmt.Sprintf("%v %v \"%v\"", v.Flag, v.Tag, v.Value) + answer.Value = fmt.Sprintf("%v %v \"%v\"", v.Flag, v.Tag, v.Value) case *dns.HINFO: - answer["value"] = fmt.Sprintf("\"%v\" \"%v\"", v.Cpu, v.Os) + answer.Value = fmt.Sprintf("\"%v\" \"%v\"", v.Cpu, v.Os) case *dns.RRSIG: - answer["value"] = fmt.Sprintf("%v %v %v %v %v %v %v %v %v", dns.TypeToString[v.TypeCovered], v.Algorithm, v.Labels, v.OrigTtl, v.Expiration, v.Inception, v.KeyTag, v.SignerName, v.Signature) + answer.Value = fmt.Sprintf("%v %v %v %v %v %v %v %v %v", dns.TypeToString[v.TypeCovered], v.Algorithm, v.Labels, v.OrigTtl, v.Expiration, v.Inception, v.KeyTag, v.SignerName, v.Signature) default: - // type unknown, marshall it as-is - answer["value"] = v + answer.Value = v.String() } + answers = append(answers, answer) } diff --git a/internal/querylog/qlog.go b/internal/querylog/qlog.go index 97343006..4726f075 100644 --- a/internal/querylog/qlog.go +++ b/internal/querylog/qlog.go @@ -2,7 +2,9 @@ package querylog import ( + "errors" "fmt" + "net" "os" "path/filepath" "strings" @@ -36,10 +38,11 @@ type ClientProto string // Client protocol names. const ( - ClientProtoDOH ClientProto = "doh" - ClientProtoDOQ ClientProto = "doq" - ClientProtoDOT ClientProto = "dot" - ClientProtoPlain ClientProto = "" + ClientProtoDOH ClientProto = "doh" + ClientProtoDOQ ClientProto = "doq" + ClientProtoDOT ClientProto = "dot" + ClientProtoDNSCrypt ClientProto = "dnscrypt" + ClientProtoPlain ClientProto = "" ) // NewClientProto validates that the client protocol name is valid and returns @@ -50,6 +53,7 @@ func NewClientProto(s string) (cp ClientProto, err error) { ClientProtoDOH, ClientProtoDOQ, ClientProtoDOT, + ClientProtoDNSCrypt, ClientProtoPlain: return cp, nil @@ -60,13 +64,14 @@ func NewClientProto(s string) (cp ClientProto, err error) { // logEntry - represents a single log entry type logEntry struct { - IP string `json:"IP"` // Client IP + IP net.IP `json:"IP"` // Client IP Time time.Time `json:"T"` QHost string `json:"QH"` QType string `json:"QT"` QClass string `json:"QC"` + ClientID string `json:"CID,omitempty"` ClientProto ClientProto `json:"CP"` Answer []byte `json:",omitempty"` // sometimes empty answers happen like binerdunt.top or rev2.globalrootservers.net @@ -118,14 +123,15 @@ func (l *queryLog) clear() { l.flushPending = false l.bufferLock.Unlock() - err := os.Remove(l.logFile + ".1") - if err != nil && !os.IsNotExist(err) { - log.Error("file remove: %s: %s", l.logFile+".1", err) + oldLogFile := l.logFile + ".1" + err := os.Remove(oldLogFile) + if err != nil && !errors.Is(err, os.ErrNotExist) { + log.Error("removing old log file %q: %s", oldLogFile, err) } err = os.Remove(l.logFile) - if err != nil && !os.IsNotExist(err) { - log.Error("file remove: %s: %s", l.logFile, err) + if err != nil && !errors.Is(err, os.ErrNotExist) { + log.Error("removing log file %q: %s", l.logFile, err) } log.Debug("Query log: cleared") @@ -147,12 +153,13 @@ func (l *queryLog) Add(params AddParams) { now := time.Now() entry := logEntry{ - IP: l.getClientIP(params.ClientIP.String()), + IP: l.getClientIP(params.ClientIP), Time: now, Result: *params.Result, Elapsed: params.Elapsed, Upstream: params.Upstream, + ClientID: params.ClientID, ClientProto: params.ClientProto, } q := params.Question.Question[0] diff --git a/internal/querylog/qlog_test.go b/internal/querylog/qlog_test.go index fd37a1de..c8b34eb5 100644 --- a/internal/querylog/qlog_test.go +++ b/internal/querylog/qlog_test.go @@ -1,242 +1,276 @@ package querylog import ( + "fmt" "math/rand" "net" - "os" "sort" "testing" "time" "github.com/AdguardTeam/dnsproxy/proxyutil" + "github.com/AdguardTeam/AdGuardHome/internal/aghtest" "github.com/AdguardTeam/AdGuardHome/internal/dnsfilter" - "github.com/AdguardTeam/AdGuardHome/internal/testutil" "github.com/miekg/dns" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) func TestMain(m *testing.M) { - testutil.DiscardLogOutput(m) + aghtest.DiscardLogOutput(m) } -func prepareTestDir() string { - const dir = "./agh-test" - _ = os.RemoveAll(dir) - _ = os.MkdirAll(dir, 0o755) - return dir -} - -// Check adding and loading (with filtering) entries from disk and memory +// TestQueryLog tests adding and loading (with filtering) entries from disk and +// memory. func TestQueryLog(t *testing.T) { - conf := Config{ + l := newQueryLog(Config{ Enabled: true, FileEnabled: true, Interval: 1, MemSize: 100, + BaseDir: aghtest.PrepareTestDir(t), + }) + + // Add disk entries. + addEntry(l, "example.org", net.IPv4(1, 1, 1, 1), net.IPv4(2, 2, 2, 1)) + // Write to disk (first file). + require.Nil(t, l.flushLogBuffer(true)) + // Start writing to the second file. + require.Nil(t, l.rotate()) + // Add disk entries. + addEntry(l, "example.org", net.IPv4(1, 1, 1, 2), net.IPv4(2, 2, 2, 2)) + // Write to disk. + require.Nil(t, l.flushLogBuffer(true)) + // 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)) + + type tcAssertion struct { + num int + host string + answer, client net.IP } - conf.BaseDir = prepareTestDir() - defer func() { _ = os.RemoveAll(conf.BaseDir) }() - l := newQueryLog(conf) - // add disk entries - addEntry(l, "example.org", "1.1.1.1", "2.2.2.1") - // write to disk (first file) - _ = l.flushLogBuffer(true) - // start writing to the second file - _ = l.rotate() - // add disk entries - addEntry(l, "example.org", "1.1.1.2", "2.2.2.2") - // write to disk - _ = l.flushLogBuffer(true) - // add memory entries - addEntry(l, "test.example.org", "1.1.1.3", "2.2.2.3") - addEntry(l, "example.com", "1.1.1.4", "2.2.2.4") + testCases := []struct { + name string + sCr []searchCriteria + want []tcAssertion + }{{ + name: "all", + sCr: []searchCriteria{}, + 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)}, + }, + }, { + name: "by_domain_strict", + sCr: []searchCriteria{{ + criteriaType: ctDomainOrClient, + strict: true, + value: "TEST.example.org", + }}, + want: []tcAssertion{{ + num: 0, host: "test.example.org", answer: net.IPv4(1, 1, 1, 3), client: net.IPv4(2, 2, 2, 3), + }}, + }, { + name: "by_domain_non-strict", + sCr: []searchCriteria{{ + criteriaType: ctDomainOrClient, + strict: false, + value: "example.ORG", + }}, + want: []tcAssertion{ + {num: 0, host: "test.example.org", answer: net.IPv4(1, 1, 1, 3), client: net.IPv4(2, 2, 2, 3)}, + {num: 1, host: "example.org", answer: net.IPv4(1, 1, 1, 2), client: net.IPv4(2, 2, 2, 2)}, + {num: 2, host: "example.org", answer: net.IPv4(1, 1, 1, 1), client: net.IPv4(2, 2, 2, 1)}, + }, + }, { + name: "by_client_ip_strict", + sCr: []searchCriteria{{ + criteriaType: ctDomainOrClient, + strict: true, + value: "2.2.2.2", + }}, + want: []tcAssertion{{ + num: 0, host: "example.org", answer: net.IPv4(1, 1, 1, 2), client: net.IPv4(2, 2, 2, 2), + }}, + }, { + name: "by_client_ip_non-strict", + sCr: []searchCriteria{{ + criteriaType: ctDomainOrClient, + strict: false, + 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)}, + }, + }} - // get all entries - params := newSearchParams() - entries, _ := l.search(params) - assert.Equal(t, 4, len(entries)) - assertLogEntry(t, entries[0], "example.com", "1.1.1.4", "2.2.2.4") - assertLogEntry(t, entries[1], "test.example.org", "1.1.1.3", "2.2.2.3") - assertLogEntry(t, entries[2], "example.org", "1.1.1.2", "2.2.2.2") - assertLogEntry(t, entries[3], "example.org", "1.1.1.1", "2.2.2.1") + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + params := newSearchParams() + params.searchCriteria = tc.sCr - // search by domain (strict) - params = newSearchParams() - params.searchCriteria = append(params.searchCriteria, searchCriteria{ - criteriaType: ctDomainOrClient, - strict: true, - value: "TEST.example.org", - }) - entries, _ = l.search(params) - assert.Equal(t, 1, len(entries)) - assertLogEntry(t, entries[0], "test.example.org", "1.1.1.3", "2.2.2.3") - - // search by domain (not strict) - params = newSearchParams() - params.searchCriteria = append(params.searchCriteria, searchCriteria{ - criteriaType: ctDomainOrClient, - strict: false, - value: "example.ORG", - }) - entries, _ = l.search(params) - assert.Equal(t, 3, len(entries)) - assertLogEntry(t, entries[0], "test.example.org", "1.1.1.3", "2.2.2.3") - assertLogEntry(t, entries[1], "example.org", "1.1.1.2", "2.2.2.2") - assertLogEntry(t, entries[2], "example.org", "1.1.1.1", "2.2.2.1") - - // search by client IP (strict) - params = newSearchParams() - params.searchCriteria = append(params.searchCriteria, searchCriteria{ - criteriaType: ctDomainOrClient, - strict: true, - value: "2.2.2.2", - }) - entries, _ = l.search(params) - assert.Equal(t, 1, len(entries)) - assertLogEntry(t, entries[0], "example.org", "1.1.1.2", "2.2.2.2") - - // search by client IP (part of) - params = newSearchParams() - params.searchCriteria = append(params.searchCriteria, searchCriteria{ - criteriaType: ctDomainOrClient, - strict: false, - value: "2.2.2", - }) - entries, _ = l.search(params) - assert.Equal(t, 4, len(entries)) - assertLogEntry(t, entries[0], "example.com", "1.1.1.4", "2.2.2.4") - assertLogEntry(t, entries[1], "test.example.org", "1.1.1.3", "2.2.2.3") - assertLogEntry(t, entries[2], "example.org", "1.1.1.2", "2.2.2.2") - assertLogEntry(t, entries[3], "example.org", "1.1.1.1", "2.2.2.1") + entries, _ := l.search(params) + require.Len(t, entries, len(tc.want)) + for _, want := range tc.want { + assertLogEntry(t, entries[want.num], want.host, want.answer, want.client) + } + }) + } } func TestQueryLogOffsetLimit(t *testing.T) { - conf := Config{ + l := newQueryLog(Config{ Enabled: true, Interval: 1, MemSize: 100, - } - conf.BaseDir = prepareTestDir() - defer func() { _ = os.RemoveAll(conf.BaseDir) }() - l := newQueryLog(conf) + BaseDir: aghtest.PrepareTestDir(t), + }) - // add 10 entries to the log - for i := 0; i < 10; i++ { - addEntry(l, "second.example.org", "1.1.1.1", "2.2.2.1") + const ( + entNum = 10 + 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 disk (first file) - _ = l.flushLogBuffer(true) - // add 10 more entries to the log (memory) - for i := 0; i < 10; i++ { - addEntry(l, "first.example.org", "1.1.1.1", "2.2.2.1") + // Write them to the first file. + require.Nil(t, l.flushLogBuffer(true)) + // 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)) } - // First page params := newSearchParams() - params.offset = 0 - params.limit = 10 - entries, _ := l.search(params) - assert.Equal(t, 10, len(entries)) - assert.Equal(t, entries[0].QHost, "first.example.org") - assert.Equal(t, entries[9].QHost, "first.example.org") - // Second page - params.offset = 10 - params.limit = 10 - entries, _ = l.search(params) - assert.Equal(t, 10, len(entries)) - assert.Equal(t, entries[0].QHost, "second.example.org") - assert.Equal(t, entries[9].QHost, "second.example.org") + testCases := []struct { + name string + offset int + limit int + wantLen int + want string + }{{ + name: "page_1", + offset: 0, + limit: 10, + wantLen: 10, + want: firstPageDomain, + }, { + name: "page_2", + offset: 10, + limit: 10, + wantLen: 10, + want: secondPageDomain, + }, { + name: "page_2.5", + offset: 15, + limit: 10, + wantLen: 5, + want: secondPageDomain, + }, { + name: "page_3", + offset: 20, + limit: 10, + wantLen: 0, + }} - // Second and a half page - params.offset = 15 - params.limit = 10 - entries, _ = l.search(params) - assert.Equal(t, 5, len(entries)) - assert.Equal(t, entries[0].QHost, "second.example.org") - assert.Equal(t, entries[4].QHost, "second.example.org") + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + params.offset = tc.offset + params.limit = tc.limit + entries, _ := l.search(params) - // Third page - params.offset = 20 - params.limit = 10 - entries, _ = l.search(params) - assert.Equal(t, 0, len(entries)) + require.Len(t, entries, tc.wantLen) + + if tc.wantLen > 0 { + assert.Equal(t, entries[0].QHost, tc.want) + assert.Equal(t, entries[tc.wantLen-1].QHost, tc.want) + } + }) + } } func TestQueryLogMaxFileScanEntries(t *testing.T) { - conf := Config{ + l := newQueryLog(Config{ Enabled: true, FileEnabled: true, Interval: 1, MemSize: 100, - } - conf.BaseDir = prepareTestDir() - defer func() { _ = os.RemoveAll(conf.BaseDir) }() - l := newQueryLog(conf) + BaseDir: aghtest.PrepareTestDir(t), + }) - // add 10 entries to the log - for i := 0; i < 10; i++ { - addEntry(l, "example.org", "1.1.1.1", "2.2.2.1") + const entNum = 10 + // Add entries to the log. + for i := 0; i < entNum; i++ { + addEntry(l, "example.org", net.IPv4(1, 1, 1, 1), net.IPv4(2, 2, 2, 1)) } - // write them to disk (first file) - _ = l.flushLogBuffer(true) + // Write them to disk. + require.Nil(t, l.flushLogBuffer(true)) params := newSearchParams() - params.maxFileScanEntries = 5 // do not scan more than 5 records - entries, _ := l.search(params) - assert.Equal(t, 5, len(entries)) - params.maxFileScanEntries = 0 // disable the limit - entries, _ = l.search(params) - assert.Equal(t, 10, len(entries)) + for _, maxFileScanEntries := range []int{5, 0} { + t.Run(fmt.Sprintf("limit_%d", maxFileScanEntries), func(t *testing.T) { + params.maxFileScanEntries = maxFileScanEntries + entries, _ := l.search(params) + assert.Len(t, entries, entNum-maxFileScanEntries) + }) + } } func TestQueryLogFileDisabled(t *testing.T) { - conf := Config{ + l := newQueryLog(Config{ Enabled: true, FileEnabled: false, Interval: 1, MemSize: 2, - } - conf.BaseDir = prepareTestDir() - defer func() { _ = os.RemoveAll(conf.BaseDir) }() - l := newQueryLog(conf) + BaseDir: aghtest.PrepareTestDir(t), + }) - addEntry(l, "example1.org", "1.1.1.1", "2.2.2.1") - addEntry(l, "example2.org", "1.1.1.1", "2.2.2.1") - addEntry(l, "example3.org", "1.1.1.1", "2.2.2.1") - // the oldest entry is now removed from mem buffer + addEntry(l, "example1.org", net.IPv4(1, 1, 1, 1), net.IPv4(2, 2, 2, 1)) + addEntry(l, "example2.org", net.IPv4(1, 1, 1, 1), net.IPv4(2, 2, 2, 1)) + // The oldest entry is going to be removed from memory buffer. + addEntry(l, "example3.org", net.IPv4(1, 1, 1, 1), net.IPv4(2, 2, 2, 1)) params := newSearchParams() ll, _ := l.search(params) - assert.Equal(t, 2, len(ll)) + require.Len(t, ll, 2) assert.Equal(t, "example3.org", ll[0].QHost) assert.Equal(t, "example2.org", ll[1].QHost) } -func addEntry(l *queryLog, host, answerStr, client string) { - q := dns.Msg{} - q.Question = append(q.Question, dns.Question{ - Name: host + ".", - Qtype: dns.TypeA, - Qclass: dns.ClassINET, - }) - - a := dns.Msg{} - a.Question = append(a.Question, q.Question[0]) - answer := new(dns.A) - answer.Hdr = dns.RR_Header{ - Name: q.Question[0].Name, - Rrtype: dns.TypeA, - Class: dns.ClassINET, +func addEntry(l *queryLog, host string, answerStr, client net.IP) { + q := dns.Msg{ + Question: []dns.Question{{ + Name: host + ".", + Qtype: dns.TypeA, + Qclass: dns.ClassINET, + }}, + } + + a := dns.Msg{ + Question: q.Question, + Answer: []dns.RR{&dns.A{ + Hdr: dns.RR_Header{ + Name: q.Question[0].Name, + Rrtype: dns.TypeA, + Class: dns.ClassINET, + }, + A: answerStr, + }}, } - answer.A = net.ParseIP(answerStr) - a.Answer = append(a.Answer, answer) res := dnsfilter.Result{ IsFiltered: true, - Reason: dnsfilter.ReasonRewrite, + Reason: dnsfilter.Rewritten, ServiceName: "SomeService", Rules: []*dnsfilter.ResultRule{{ FilterListID: 1, @@ -248,25 +282,28 @@ func addEntry(l *queryLog, host, answerStr, client string) { Answer: &a, OrigAnswer: &a, Result: &res, - ClientIP: net.ParseIP(client), + ClientIP: client, Upstream: "upstream", } l.Add(params) } -func assertLogEntry(t *testing.T, entry *logEntry, host, answer, client string) bool { +func assertLogEntry(t *testing.T, entry *logEntry, host string, answer, client net.IP) { + t.Helper() + + require.NotNil(t, entry) + assert.Equal(t, host, entry.QHost) assert.Equal(t, client, entry.IP) assert.Equal(t, "A", entry.QType) assert.Equal(t, "IN", entry.QClass) - msg := new(dns.Msg) - assert.Nil(t, msg.Unpack(entry.Answer)) - assert.Equal(t, 1, len(msg.Answer)) - ip := proxyutil.GetIPFromDNSRecord(msg.Answer[0]) - assert.NotNil(t, ip) - assert.Equal(t, answer, ip.String()) - return true + msg := &dns.Msg{} + require.Nil(t, msg.Unpack(entry.Answer)) + require.Len(t, msg.Answer, 1) + + ip := proxyutil.GetIPFromDNSRecord(msg.Answer[0]).To16() + assert.Equal(t, answer, ip) } func testEntries() (entries []*logEntry) { @@ -332,8 +369,8 @@ func TestLogEntriesByTime_sort(t *testing.T) { entries := testEntries() sort.Sort(logEntriesByTimeDesc(entries)) - for i := 1; i < len(entries); i++ { - assert.False(t, entries[i].Time.After(entries[i-1].Time), - "%s %s", entries[i].Time, entries[i-1].Time) + for i := range entries[1:] { + assert.False(t, entries[i+1].Time.After(entries[i].Time), + "%s %s", entries[i+1].Time, entries[i].Time) } } diff --git a/internal/querylog/qlogfile.go b/internal/querylog/qlogfile.go index 69a42ed2..3aa56f6f 100644 --- a/internal/querylog/qlogfile.go +++ b/internal/querylog/qlogfile.go @@ -251,7 +251,7 @@ func (q *QLogFile) readNextLine(position int64) (string, int64, error) { // the goal is to read a chunk of file that includes the line with the specified position. func (q *QLogFile) initBuffer(position int64) error { q.bufferStart = int64(0) - if (position - bufferSize) > 0 { + if position > bufferSize { q.bufferStart = position - bufferSize } @@ -264,12 +264,10 @@ func (q *QLogFile) initBuffer(position int64) error { if q.buffer == nil { q.buffer = make([]byte, bufferSize) } - q.bufferLen, err = q.file.Read(q.buffer) - if err != nil { - return err - } - return nil + q.bufferLen, err = q.file.Read(q.buffer) + + return err } // readProbeLine reads a line that includes the specified position @@ -280,7 +278,7 @@ func (q *QLogFile) readProbeLine(position int64) (string, int64, int64, error) { // In order to do this, we'll define the boundaries seekPosition := int64(0) relativePos := position // position relative to the buffer we're going to read - if (position - maxEntrySize) > 0 { + if position > maxEntrySize { seekPosition = position - maxEntrySize relativePos = maxEntrySize } diff --git a/internal/querylog/qlogfile_test.go b/internal/querylog/qlogfile_test.go index 950eaaf3..b74111fc 100644 --- a/internal/querylog/qlogfile_test.go +++ b/internal/querylog/qlogfile_test.go @@ -2,347 +2,347 @@ package querylog import ( "encoding/binary" + "errors" + "fmt" "io" "io/ioutil" "math" "net" - "os" "strings" "testing" "time" + "github.com/AdguardTeam/AdGuardHome/internal/aghtest" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) -func TestQLogFileEmpty(t *testing.T) { - testDir := prepareTestDir() - defer func() { _ = os.RemoveAll(testDir) }() - testFile := prepareTestFile(testDir, 0) +// prepareTestFiles prepares several test query log files, each with the +// specified lines count. +func prepareTestFiles(t *testing.T, filesNum, linesNum int) []string { + t.Helper() - // create the new QLogFile instance - q, err := NewQLogFile(testFile) - assert.Nil(t, err) - assert.NotNil(t, q) - defer q.Close() - - // seek to the start - pos, err := q.SeekStart() - assert.Nil(t, err) - assert.Equal(t, int64(0), pos) - - // try reading anyway - line, err := q.ReadNext() - assert.Equal(t, io.EOF, err) - assert.Equal(t, "", line) -} - -func TestQLogFileLarge(t *testing.T) { - // should be large enough - count := 50000 - - testDir := prepareTestDir() - defer func() { _ = os.RemoveAll(testDir) }() - testFile := prepareTestFile(testDir, count) - - // create the new QLogFile instance - q, err := NewQLogFile(testFile) - assert.Nil(t, err) - assert.NotNil(t, q) - defer q.Close() - - // seek to the start - pos, err := q.SeekStart() - assert.Nil(t, err) - assert.NotEqual(t, int64(0), pos) - - read := 0 - var line string - for err == nil { - line, err = q.ReadNext() - if err == nil { - assert.True(t, len(line) > 0) - read++ - } + if filesNum == 0 { + return []string{} } - assert.Equal(t, count, read) - assert.Equal(t, io.EOF, err) -} - -func TestQLogFileSeekLargeFile(t *testing.T) { - // more or less big file - count := 10000 - - testDir := prepareTestDir() - defer func() { _ = os.RemoveAll(testDir) }() - testFile := prepareTestFile(testDir, count) - - // create the new QLogFile instance - q, err := NewQLogFile(testFile) - assert.Nil(t, err) - assert.NotNil(t, q) - defer q.Close() - - // CASE 1: NOT TOO OLD LINE - testSeekLineQLogFile(t, q, 300) - - // CASE 2: OLD LINE - testSeekLineQLogFile(t, q, count-300) - - // CASE 3: FIRST LINE - testSeekLineQLogFile(t, q, 0) - - // CASE 4: LAST LINE - testSeekLineQLogFile(t, q, count) - - // CASE 5: Seek non-existent (too low) - _, _, err = q.SeekTS(123) - assert.NotNil(t, err) - - // CASE 6: Seek non-existent (too high) - ts, _ := time.Parse(time.RFC3339, "2100-01-02T15:04:05Z07:00") - _, _, err = q.SeekTS(ts.UnixNano()) - assert.NotNil(t, err) - - // CASE 7: "Almost" found - line, err := getQLogFileLine(q, count/2) - assert.Nil(t, err) - // ALMOST the record we need - timestamp := readQLogTimestamp(line) - 1 - assert.NotEqual(t, uint64(0), timestamp) - _, depth, err := q.SeekTS(timestamp) - assert.NotNil(t, err) - assert.True(t, depth <= int(math.Log2(float64(count))+3)) -} - -func TestQLogFileSeekSmallFile(t *testing.T) { - // more or less big file - count := 10 - - testDir := prepareTestDir() - defer func() { _ = os.RemoveAll(testDir) }() - testFile := prepareTestFile(testDir, count) - - // create the new QLogFile instance - q, err := NewQLogFile(testFile) - assert.Nil(t, err) - assert.NotNil(t, q) - defer q.Close() - - // CASE 1: NOT TOO OLD LINE - testSeekLineQLogFile(t, q, 2) - - // CASE 2: OLD LINE - testSeekLineQLogFile(t, q, count-2) - - // CASE 3: FIRST LINE - testSeekLineQLogFile(t, q, 0) - - // CASE 4: LAST LINE - testSeekLineQLogFile(t, q, count) - - // CASE 5: Seek non-existent (too low) - _, _, err = q.SeekTS(123) - assert.NotNil(t, err) - - // CASE 6: Seek non-existent (too high) - ts, _ := time.Parse(time.RFC3339, "2100-01-02T15:04:05Z07:00") - _, _, err = q.SeekTS(ts.UnixNano()) - assert.NotNil(t, err) - - // CASE 7: "Almost" found - line, err := getQLogFileLine(q, count/2) - assert.Nil(t, err) - // ALMOST the record we need - timestamp := readQLogTimestamp(line) - 1 - assert.NotEqual(t, uint64(0), timestamp) - _, depth, err := q.SeekTS(timestamp) - assert.NotNil(t, err) - assert.True(t, depth <= int(math.Log2(float64(count))+3)) -} - -func testSeekLineQLogFile(t *testing.T, q *QLogFile, lineNumber int) { - line, err := getQLogFileLine(q, lineNumber) - assert.Nil(t, err) - ts := readQLogTimestamp(line) - assert.NotEqual(t, uint64(0), ts) - - // try seeking to that line now - pos, _, err := q.SeekTS(ts) - assert.Nil(t, err) - assert.NotEqual(t, int64(0), pos) - - testLine, err := q.ReadNext() - assert.Nil(t, err) - assert.Equal(t, line, testLine) -} - -func getQLogFileLine(q *QLogFile, lineNumber int) (string, error) { - _, err := q.SeekStart() - if err != nil { - return "", err - } - - for i := 1; i < lineNumber; i++ { - _, err := q.ReadNext() - if err != nil { - return "", err - } - } - return q.ReadNext() -} - -// Check adding and loading (with filtering) entries from disk and memory -func TestQLogFile(t *testing.T) { - testDir := prepareTestDir() - defer func() { _ = os.RemoveAll(testDir) }() - testFile := prepareTestFile(testDir, 2) - - // create the new QLogFile instance - q, err := NewQLogFile(testFile) - assert.Nil(t, err) - assert.NotNil(t, q) - defer q.Close() - - // seek to the start - pos, err := q.SeekStart() - assert.Nil(t, err) - assert.True(t, pos > 0) - - // read first line - line, err := q.ReadNext() - assert.Nil(t, err) - assert.True(t, strings.Contains(line, "0.0.0.2"), line) - assert.True(t, strings.HasPrefix(line, "{"), line) - assert.True(t, strings.HasSuffix(line, "}"), line) - - // read second line - line, err = q.ReadNext() - assert.Nil(t, err) - assert.Equal(t, int64(0), q.position) - assert.True(t, strings.Contains(line, "0.0.0.1"), line) - assert.True(t, strings.HasPrefix(line, "{"), line) - assert.True(t, strings.HasSuffix(line, "}"), line) - - // try reading again (there's nothing to read anymore) - line, err = q.ReadNext() - assert.Equal(t, io.EOF, err) - assert.Equal(t, "", line) -} - -// prepareTestFile - prepares a test query log file with the specified number of lines -func prepareTestFile(dir string, linesCount int) string { - return prepareTestFiles(dir, 1, linesCount)[0] -} - -// prepareTestFiles - prepares several test query log files -// each of them -- with the specified linesCount -func prepareTestFiles(dir string, filesCount, linesCount int) []string { - format := `{"IP":"${IP}","T":"${TIMESTAMP}","QH":"example.org","QT":"A","QC":"IN","Answer":"AAAAAAABAAEAAAAAB2V4YW1wbGUDb3JnAAABAAEHZXhhbXBsZQNvcmcAAAEAAQAAAAAABAECAwQ=","Result":{},"Elapsed":0,"Upstream":"upstream"}` + const strV = "\"%s\"" + const nl = "\n" + const format = `{"IP":` + strV + `,"T":` + strV + `,` + + `"QH":"example.org","QT":"A","QC":"IN",` + + `"Answer":"AAAAAAABAAEAAAAAB2V4YW1wbGUDb3JnAAABAAEHZXhhbXBsZQNvcmcAAAEAAQAAAAAABAECAwQ=",` + + `"Result":{},"Elapsed":0,"Upstream":"upstream"}` + nl lineTime, _ := time.Parse(time.RFC3339Nano, "2020-02-18T22:36:35.920973+03:00") lineIP := uint32(0) - files := make([]string, filesCount) - for j := 0; j < filesCount; j++ { - f, _ := ioutil.TempFile(dir, "*.txt") - files[filesCount-j-1] = f.Name() + dir := aghtest.PrepareTestDir(t) - for i := 0; i < linesCount; i++ { + files := make([]string, filesNum) + for j := range files { + f, err := ioutil.TempFile(dir, "*.txt") + require.Nil(t, err) + files[filesNum-j-1] = f.Name() + + for i := 0; i < linesNum; i++ { lineIP++ lineTime = lineTime.Add(time.Second) ip := make(net.IP, 4) binary.BigEndian.PutUint32(ip, lineIP) - line := format - line = strings.ReplaceAll(line, "${IP}", ip.String()) - line = strings.ReplaceAll(line, "${TIMESTAMP}", lineTime.Format(time.RFC3339Nano)) + line := fmt.Sprintf(format, ip, lineTime.Format(time.RFC3339Nano)) - _, _ = f.WriteString(line) - _, _ = f.WriteString("\n") + _, err = f.WriteString(line) + require.Nil(t, err) } } return files } -func TestQLogSeek(t *testing.T) { - testDir := prepareTestDir() - defer func() { _ = os.RemoveAll(testDir) }() +// prepareTestFile prepares a test query log file with the specified number of +// lines. +func prepareTestFile(t *testing.T, linesCount int) string { + t.Helper() - d := `{"T":"2020-08-31T18:44:23.911246629+03:00","QH":"wfqvjymurpwegyv","QT":"A","QC":"IN","CP":"","Answer":"","Result":{},"Elapsed":66286385,"Upstream":"tls://dns-unfiltered.adguard.com:853"} -{"T":"2020-08-31T18:44:25.376690873+03:00"} -{"T":"2020-08-31T18:44:25.382540454+03:00"}` - f, _ := ioutil.TempFile(testDir, "*.txt") - _, _ = f.WriteString(d) - defer f.Close() - - q, err := NewQLogFile(f.Name()) - assert.Nil(t, err) - defer q.Close() - - target, _ := time.Parse(time.RFC3339, "2020-08-31T18:44:25.376690873+03:00") - - _, depth, err := q.SeekTS(target.UnixNano()) - assert.Nil(t, err) - assert.Equal(t, 1, depth) + return prepareTestFiles(t, 1, linesCount)[0] } -func TestQLogSeek_ErrTSTooLate(t *testing.T) { - testDir := prepareTestDir() +// newTestQLogFile creates new *QLogFile for tests and registers the required +// cleanup functions. +func newTestQLogFile(t *testing.T, linesNum int) (file *QLogFile) { + t.Helper() + + testFile := prepareTestFile(t, linesNum) + + // Create the new QLogFile instance. + file, err := NewQLogFile(testFile) + require.Nil(t, err) + assert.NotNil(t, file) t.Cleanup(func() { - _ = os.RemoveAll(testDir) + assert.Nil(t, file.Close()) }) - d := `{"T":"2020-08-31T18:44:23.911246629+03:00","QH":"wfqvjymurpwegyv","QT":"A","QC":"IN","CP":"","Answer":"","Result":{},"Elapsed":66286385,"Upstream":"tls://dns-unfiltered.adguard.com:853"} -{"T":"2020-08-31T18:44:25.376690873+03:00"} -{"T":"2020-08-31T18:44:25.382540454+03:00"} -` - f, err := ioutil.TempFile(testDir, "*.txt") - assert.Nil(t, err) - defer f.Close() - - _, err = f.WriteString(d) - assert.Nil(t, err) - - q, err := NewQLogFile(f.Name()) - assert.Nil(t, err) - defer q.Close() - - target, err := time.Parse(time.RFC3339, "2020-08-31T18:44:25.382540454+03:00") - assert.Nil(t, err) - - _, depth, err := q.SeekTS(target.UnixNano() + int64(time.Second)) - assert.Equal(t, ErrTSTooLate, err) - assert.Equal(t, 2, depth) + return file } -func TestQLogSeek_ErrTSTooEarly(t *testing.T) { - testDir := prepareTestDir() +func TestQLogFile_ReadNext(t *testing.T) { + testCases := []struct { + name string + linesNum int + }{{ + name: "empty", + linesNum: 0, + }, { + name: "large", + linesNum: 50000, + }} + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + q := newTestQLogFile(t, tc.linesNum) + + // Calculate the expected position. + fileInfo, err := q.file.Stat() + require.Nil(t, err) + var expPos int64 + if expPos = fileInfo.Size(); expPos > 0 { + expPos-- + } + + // Seek to the start. + pos, err := q.SeekStart() + require.Nil(t, err) + require.EqualValues(t, expPos, pos) + + var read int + var line string + for err == nil { + line, err = q.ReadNext() + if err == nil { + assert.NotEmpty(t, line) + read++ + } + } + + require.Equal(t, io.EOF, err) + assert.Equal(t, tc.linesNum, read) + }) + } +} + +func TestQLogFile_SeekTS_good(t *testing.T) { + linesCases := []struct { + name string + num int + }{{ + name: "large", + num: 10000, + }, { + name: "small", + num: 10, + }} + + for _, l := range linesCases { + testCases := []struct { + name string + linesNum int + line int + }{{ + name: "not_too_old", + line: 2, + }, { + name: "old", + line: l.num - 2, + }, { + name: "first", + line: 0, + }, { + name: "last", + line: l.num, + }} + + q := newTestQLogFile(t, l.num) + + for _, tc := range testCases { + t.Run(l.name+"_"+tc.name, func(t *testing.T) { + line, err := getQLogFileLine(q, tc.line) + require.Nil(t, err) + ts := readQLogTimestamp(line) + assert.NotEqualValues(t, 0, ts) + + // Try seeking to that line now. + pos, _, err := q.SeekTS(ts) + require.Nil(t, err) + assert.NotEqualValues(t, 0, pos) + + testLine, err := q.ReadNext() + require.Nil(t, err) + assert.Equal(t, line, testLine) + }) + } + } +} + +func TestQLogFile_SeekTS_bad(t *testing.T) { + linesCases := []struct { + name string + num int + }{{ + name: "large", + num: 10000, + }, { + name: "small", + num: 10, + }} + + for _, l := range linesCases { + testCases := []struct { + name string + ts int64 + leq bool + }{{ + name: "non-existent_long_ago", + }, { + name: "non-existent_far_ahead", + }, { + name: "almost", + leq: true, + }} + + q := newTestQLogFile(t, l.num) + testCases[0].ts = 123 + + lateTS, _ := time.Parse(time.RFC3339, "2100-01-02T15:04:05Z07:00") + testCases[1].ts = lateTS.UnixNano() + + line, err := getQLogFileLine(q, l.num/2) + require.Nil(t, err) + testCases[2].ts = readQLogTimestamp(line) - 1 + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + assert.NotEqualValues(t, 0, tc.ts) + + _, depth, err := q.SeekTS(tc.ts) + assert.NotEmpty(t, l.num) + require.NotNil(t, err) + if tc.leq { + assert.LessOrEqual(t, depth, int(math.Log2(float64(l.num))+3)) + } + }) + } + } +} + +func getQLogFileLine(q *QLogFile, lineNumber int) (line string, err error) { + if _, err = q.SeekStart(); err != nil { + return line, err + } + + for i := 1; i < lineNumber; i++ { + if _, err = q.ReadNext(); err != nil { + return line, err + } + } + + return q.ReadNext() +} + +// Check adding and loading (with filtering) entries from disk and memory. +func TestQLogFile(t *testing.T) { + // Create the new QLogFile instance. + q := newTestQLogFile(t, 2) + + // Seek to the start. + pos, err := q.SeekStart() + require.Nil(t, err) + assert.Greater(t, pos, int64(0)) + + // Read first line. + line, err := q.ReadNext() + require.Nil(t, err) + assert.Contains(t, line, "0.0.0.2") + assert.True(t, strings.HasPrefix(line, "{"), line) + assert.True(t, strings.HasSuffix(line, "}"), line) + + // Read second line. + line, err = q.ReadNext() + require.Nil(t, err) + assert.EqualValues(t, 0, q.position) + assert.Contains(t, line, "0.0.0.1") + assert.True(t, strings.HasPrefix(line, "{"), line) + assert.True(t, strings.HasSuffix(line, "}"), line) + + // Try reading again (there's nothing to read anymore). + line, err = q.ReadNext() + require.Equal(t, io.EOF, err) + assert.Empty(t, line) +} + +func NewTestQLogFileData(t *testing.T, data string) (file *QLogFile) { + f, err := ioutil.TempFile(aghtest.PrepareTestDir(t), "*.txt") + require.Nil(t, err) t.Cleanup(func() { - _ = os.RemoveAll(testDir) + assert.Nil(t, f.Close()) }) - d := `{"T":"2020-08-31T18:44:23.911246629+03:00","QH":"wfqvjymurpwegyv","QT":"A","QC":"IN","CP":"","Answer":"","Result":{},"Elapsed":66286385,"Upstream":"tls://dns-unfiltered.adguard.com:853"} -{"T":"2020-08-31T18:44:25.376690873+03:00"} -{"T":"2020-08-31T18:44:25.382540454+03:00"} -` - f, err := ioutil.TempFile(testDir, "*.txt") - assert.Nil(t, err) - defer f.Close() + _, err = f.WriteString(data) + require.Nil(t, err) - _, err = f.WriteString(d) - assert.Nil(t, err) + file, err = NewQLogFile(f.Name()) + require.Nil(t, err) + t.Cleanup(func() { + assert.Nil(t, file.Close()) + }) - q, err := NewQLogFile(f.Name()) - assert.Nil(t, err) - defer q.Close() - - target, err := time.Parse(time.RFC3339, "2020-08-31T18:44:23.911246629+03:00") - assert.Nil(t, err) - - _, depth, err := q.SeekTS(target.UnixNano() - int64(time.Second)) - assert.Equal(t, ErrTSTooEarly, err) - assert.Equal(t, 1, depth) + return file +} + +func TestQLog_Seek(t *testing.T) { + const nl = "\n" + const strV = "%s" + const recs = `{"T":"` + strV + `","QH":"wfqvjymurpwegyv","QT":"A","QC":"IN","CP":"","Answer":"","Result":{},"Elapsed":66286385,"Upstream":"tls://dns-unfiltered.adguard.com:853"}` + nl + + `{"T":"` + strV + `"}` + nl + + `{"T":"` + strV + `"}` + nl + timestamp, _ := time.Parse(time.RFC3339Nano, "2020-08-31T18:44:25.376690873+03:00") + + testCases := []struct { + name string + delta int + wantErr error + wantDepth int + }{{ + name: "ok", + delta: 0, + wantErr: nil, + wantDepth: 2, + }, { + name: "too_late", + delta: 2, + wantErr: ErrTSTooLate, + wantDepth: 2, + }, { + name: "too_early", + delta: -2, + wantErr: ErrTSTooEarly, + wantDepth: 1, + }} + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + data := fmt.Sprintf(recs, + timestamp.Add(-time.Second).Format(time.RFC3339Nano), + timestamp.Format(time.RFC3339Nano), + timestamp.Add(time.Second).Format(time.RFC3339Nano), + ) + + q := NewTestQLogFileData(t, data) + + _, depth, err := q.SeekTS(timestamp.Add(time.Second * time.Duration(tc.delta)).UnixNano()) + require.Truef(t, errors.Is(err, tc.wantErr), "%v", err) + assert.Equal(t, tc.wantDepth, depth) + }) + } } diff --git a/internal/querylog/qlogreader_test.go b/internal/querylog/qlogreader_test.go index d9dfb3ea..07fd6fd3 100644 --- a/internal/querylog/qlogreader_test.go +++ b/internal/querylog/qlogreader_test.go @@ -3,110 +3,77 @@ package querylog import ( "errors" "io" - "os" "testing" "time" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) -func TestQLogReaderEmpty(t *testing.T) { - r, err := NewQLogReader([]string{}) - assert.Nil(t, err) - assert.NotNil(t, r) - defer r.Close() +// newTestQLogReader creates new *QLogReader for tests and registers the +// required cleanup functions. +func newTestQLogReader(t *testing.T, filesNum, linesNum int) (reader *QLogReader) { + t.Helper() - // seek to the start - err = r.SeekStart() - assert.Nil(t, err) + testFiles := prepareTestFiles(t, filesNum, linesNum) - line, err := r.ReadNext() - assert.Equal(t, "", line) - assert.Equal(t, io.EOF, err) + // Create the new QLogReader instance. + reader, err := NewQLogReader(testFiles) + require.Nil(t, err) + assert.NotNil(t, reader) + t.Cleanup(func() { + assert.Nil(t, reader.Close()) + }) + + return reader } -func TestQLogReaderOneFile(t *testing.T) { - // let's do one small file - count := 10 - filesCount := 1 +func TestQLogReader(t *testing.T) { + testCases := []struct { + name string + filesNum int + linesNum int + }{{ + name: "empty", + filesNum: 0, + linesNum: 0, + }, { + name: "one_file", + filesNum: 1, + linesNum: 10, + }, { + name: "multiple_files", + filesNum: 5, + linesNum: 10000, + }} - testDir := prepareTestDir() - defer func() { _ = os.RemoveAll(testDir) }() - testFiles := prepareTestFiles(testDir, filesCount, count) + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + r := newTestQLogReader(t, tc.filesNum, tc.linesNum) - r, err := NewQLogReader(testFiles) - assert.Nil(t, err) - assert.NotNil(t, r) - defer r.Close() + // Seek to the start. + err := r.SeekStart() + require.Nil(t, err) - // seek to the start - err = r.SeekStart() - assert.Nil(t, err) + // Read everything. + var read int + var line string + for err == nil { + line, err = r.ReadNext() + if err == nil { + assert.NotEmpty(t, line) + read++ + } + } - // read everything - read := 0 - var line string - for err == nil { - line, err = r.ReadNext() - if err == nil { - assert.True(t, len(line) > 0) - read++ - } + require.Equal(t, io.EOF, err) + assert.Equal(t, tc.filesNum*tc.linesNum, read) + }) } - - assert.Equal(t, count*filesCount, read) - assert.Equal(t, io.EOF, err) -} - -func TestQLogReaderMultipleFiles(t *testing.T) { - // should be large enough - count := 10000 - filesCount := 5 - - testDir := prepareTestDir() - defer func() { _ = os.RemoveAll(testDir) }() - testFiles := prepareTestFiles(testDir, filesCount, count) - - r, err := NewQLogReader(testFiles) - assert.Nil(t, err) - assert.NotNil(t, r) - defer r.Close() - - // seek to the start - err = r.SeekStart() - assert.Nil(t, err) - - // read everything - read := 0 - var line string - for err == nil { - line, err = r.ReadNext() - if err == nil { - assert.True(t, len(line) > 0) - read++ - } - } - - assert.Equal(t, count*filesCount, read) - assert.Equal(t, io.EOF, err) } func TestQLogReader_Seek(t *testing.T) { - count := 10000 - filesCount := 2 - - testDir := prepareTestDir() - t.Cleanup(func() { - _ = os.RemoveAll(testDir) - }) - testFiles := prepareTestFiles(testDir, filesCount, count) - - r, err := NewQLogReader(testFiles) - assert.Nil(t, err) - assert.NotNil(t, r) - t.Cleanup(func() { - _ = r.Close() - }) + r := newTestQLogReader(t, 2, 10000) testCases := []struct { name string @@ -114,7 +81,7 @@ func TestQLogReader_Seek(t *testing.T) { want error }{{ name: "not_too_old", - time: "2020-02-19T04:04:56.920973+03:00", + time: "2020-02-18T22:39:35.920973+03:00", want: nil, }, { name: "old", @@ -122,7 +89,7 @@ func TestQLogReader_Seek(t *testing.T) { want: nil, }, { name: "first", - time: "2020-02-19T04:09:55.920973+03:00", + time: "2020-02-18T22:36:36.920973+03:00", want: nil, }, { name: "last", @@ -147,28 +114,20 @@ func TestQLogReader_Seek(t *testing.T) { timestamp, err := time.Parse(time.RFC3339Nano, tc.time) assert.Nil(t, err) + if tc.name == "first" { + assert.True(t, true) + } + err = r.SeekTS(timestamp.UnixNano()) - assert.True(t, errors.Is(err, tc.want), err) + assert.True(t, errors.Is(err, tc.want)) }) } } func TestQLogReader_ReadNext(t *testing.T) { - count := 10 - filesCount := 1 - - testDir := prepareTestDir() - t.Cleanup(func() { - _ = os.RemoveAll(testDir) - }) - testFiles := prepareTestFiles(testDir, filesCount, count) - - r, err := NewQLogReader(testFiles) - assert.Nil(t, err) - assert.NotNil(t, r) - t.Cleanup(func() { - _ = r.Close() - }) + const linesNum = 10 + const filesNum = 1 + r := newTestQLogReader(t, filesNum, linesNum) testCases := []struct { name string @@ -180,7 +139,7 @@ func TestQLogReader_ReadNext(t *testing.T) { want: nil, }, { name: "too_big", - start: count + 1, + start: linesNum + 1, want: io.EOF, }} @@ -199,70 +158,3 @@ func TestQLogReader_ReadNext(t *testing.T) { }) } } - -// TODO(e.burkov): Remove the tests below. Make tests above more compelling. -func TestQLogReaderSeek(t *testing.T) { - // more or less big file - count := 10000 - filesCount := 2 - - testDir := prepareTestDir() - defer func() { _ = os.RemoveAll(testDir) }() - testFiles := prepareTestFiles(testDir, filesCount, count) - - r, err := NewQLogReader(testFiles) - assert.Nil(t, err) - assert.NotNil(t, r) - defer r.Close() - - // CASE 1: NOT TOO OLD LINE - testSeekLineQLogReader(t, r, 300) - - // CASE 2: OLD LINE - testSeekLineQLogReader(t, r, count-300) - - // CASE 3: FIRST LINE - testSeekLineQLogReader(t, r, 0) - - // CASE 4: LAST LINE - testSeekLineQLogReader(t, r, count) - - // CASE 5: Seek non-existent (too low) - err = r.SeekTS(123) - assert.NotNil(t, err) - - // CASE 6: Seek non-existent (too high) - ts, _ := time.Parse(time.RFC3339, "2100-01-02T15:04:05Z07:00") - err = r.SeekTS(ts.UnixNano()) - assert.NotNil(t, err) -} - -func testSeekLineQLogReader(t *testing.T, r *QLogReader, lineNumber int) { - line, err := getQLogReaderLine(r, lineNumber) - assert.Nil(t, err) - ts := readQLogTimestamp(line) - assert.NotEqual(t, uint64(0), ts) - - // try seeking to that line now - err = r.SeekTS(ts) - assert.Nil(t, err) - - testLine, err := r.ReadNext() - assert.Nil(t, err) - assert.Equal(t, line, testLine) -} - -func getQLogReaderLine(r *QLogReader, lineNumber int) (string, error) { - err := r.SeekStart() - if err != nil { - return "", err - } - - for i := 1; i < lineNumber; i++ { - _, err := r.ReadNext() - if err != nil { - return "", err - } - } - return r.ReadNext() -} diff --git a/internal/querylog/querylog.go b/internal/querylog/querylog.go index 6a6e0a6c..98b8959d 100644 --- a/internal/querylog/querylog.go +++ b/internal/querylog/querylog.go @@ -46,6 +46,7 @@ type AddParams struct { OrigAnswer *dns.Msg // The response from an upstream server (optional) Result *dnsfilter.Result // Filtering result (optional) Elapsed time.Duration // Time spent for processing the request + ClientID string ClientIP net.IP Upstream string // Upstream server URL ClientProto ClientProto diff --git a/internal/querylog/querylogfile.go b/internal/querylog/querylogfile.go index c6d48235..a0fd165b 100644 --- a/internal/querylog/querylogfile.go +++ b/internal/querylog/querylogfile.go @@ -3,6 +3,7 @@ package querylog import ( "bytes" "encoding/json" + "errors" "os" "time" @@ -87,18 +88,19 @@ func (l *queryLog) rotate() error { from := l.logFile to := l.logFile + ".1" - if _, err := os.Stat(from); os.IsNotExist(err) { - // do nothing, file doesn't exist - return nil - } - err := os.Rename(from, to) if err != nil { + if errors.Is(err, os.ErrNotExist) { + return nil + } + log.Error("querylog: failed to rename file: %s", err) + return err } log.Debug("querylog: renamed %s -> %s", from, to) + return nil } diff --git a/internal/querylog/searchcriteria.go b/internal/querylog/searchcriteria.go index b98e0838..68672eaf 100644 --- a/internal/querylog/searchcriteria.go +++ b/internal/querylog/searchcriteria.go @@ -9,8 +9,13 @@ import ( type criteriaType int const ( - ctDomainOrClient criteriaType = iota // domain name or client IP address - ctFilteringStatus // filtering status + // ctDomainOrClient is for searching by the domain name, the client's IP + // address, or the clinet's ID. + ctDomainOrClient criteriaType = iota + // ctFilteringStatus is for searching by the filtering status. + // + // See (*searchCriteria).ctFilteringStatusCase for details. + ctFilteringStatus ) const ( @@ -38,9 +43,9 @@ var filteringStatusValues = []string{ // searchCriteria - every search request may contain a list of different search criteria // we use each of them to match the query type searchCriteria struct { + value string // search criteria value criteriaType criteriaType // type of the criteria strict bool // should we strictly match (equality) or not (indexOf) - value string // search criteria value } // quickMatch - quickly checks if the log entry matches this search criteria @@ -51,7 +56,8 @@ func (c *searchCriteria) quickMatch(line string) bool { switch c.criteriaType { case ctDomainOrClient: return c.quickMatchJSONValue(line, "QH") || - c.quickMatchJSONValue(line, "IP") + c.quickMatchJSONValue(line, "IP") || + c.quickMatchJSONValue(line, "CID") default: return true } @@ -89,21 +95,26 @@ func (c *searchCriteria) match(entry *logEntry) bool { } func (c *searchCriteria) ctDomainOrClientCase(entry *logEntry) bool { + clientID := strings.ToLower(entry.ClientID) qhost := strings.ToLower(entry.QHost) searchVal := strings.ToLower(c.value) - if c.strict && qhost == searchVal { - return true - } - if !c.strict && strings.Contains(qhost, searchVal) { + if c.strict && (qhost == searchVal || clientID == searchVal) { return true } - if c.strict && entry.IP == c.value { + if !c.strict && (strings.Contains(qhost, searchVal) || strings.Contains(clientID, searchVal)) { return true } - if !c.strict && strings.Contains(entry.IP, c.value) { + + ipStr := entry.IP.String() + if c.strict && ipStr == c.value { return true } + + if !c.strict && strings.Contains(ipStr, c.value) { + return true + } + return false } @@ -116,8 +127,9 @@ func (c *searchCriteria) ctFilteringStatusCase(res dnsfilter.Result) bool { return res.IsFiltered || res.Reason.In( dnsfilter.NotFilteredAllowList, - dnsfilter.ReasonRewrite, - dnsfilter.RewriteAutoHosts, + dnsfilter.Rewritten, + dnsfilter.RewrittenAutoHosts, + dnsfilter.RewrittenRule, ) case filteringStatusBlocked: @@ -137,7 +149,11 @@ func (c *searchCriteria) ctFilteringStatusCase(res dnsfilter.Result) bool { return res.Reason == dnsfilter.NotFilteredAllowList case filteringStatusRewritten: - return res.Reason.In(dnsfilter.ReasonRewrite, dnsfilter.RewriteAutoHosts) + return res.Reason.In( + dnsfilter.Rewritten, + dnsfilter.RewrittenAutoHosts, + dnsfilter.RewrittenRule, + ) case filteringStatusSafeSearch: return res.IsFiltered && res.Reason == dnsfilter.FilteredSafeSearch diff --git a/internal/stats/http.go b/internal/stats/http.go index 794bedf1..1580174a 100644 --- a/internal/stats/http.go +++ b/internal/stats/http.go @@ -19,26 +19,43 @@ func httpError(r *http.Request, w http.ResponseWriter, code int, format string, http.Error(w, text, code) } -// Return data +// statsResponse is a response for getting statistics. +type statsResponse struct { + TimeUnits string `json:"time_units"` + + NumDNSQueries uint64 `json:"num_dns_queries"` + NumBlockedFiltering uint64 `json:"num_blocked_filtering"` + NumReplacedSafebrowsing uint64 `json:"num_replaced_safebrowsing"` + NumReplacedSafesearch uint64 `json:"num_replaced_safesearch"` + NumReplacedParental uint64 `json:"num_replaced_parental"` + + AvgProcessingTime float64 `json:"avg_processing_time"` + + TopQueried []map[string]uint64 `json:"top_queried_domains"` + TopClients []map[string]uint64 `json:"top_clients"` + TopBlocked []map[string]uint64 `json:"top_blocked_domains"` + + DNSQueries []uint64 `json:"dns_queries"` + + BlockedFiltering []uint64 `json:"blocked_filtering"` + ReplacedSafebrowsing []uint64 `json:"replaced_safebrowsing"` + ReplacedParental []uint64 `json:"replaced_parental"` +} + +// handleStats is a handler for getting statistics. func (s *statsCtx) handleStats(w http.ResponseWriter, r *http.Request) { start := time.Now() - d := s.getData() + response, ok := s.getData() log.Debug("Stats: prepared data in %v", time.Since(start)) - if d == nil { + if !ok { httpError(r, w, http.StatusInternalServerError, "Couldn't get statistics data") - return - } - data, err := json.Marshal(d) - if err != nil { - httpError(r, w, http.StatusInternalServerError, "json encode: %s", err) return } w.Header().Set("Content-Type", "application/json") - - _, err = w.Write(data) + err := json.NewEncoder(w).Encode(response) if err != nil { httpError(r, w, http.StatusInternalServerError, "json encode: %s", err) @@ -96,8 +113,8 @@ func (s *statsCtx) initWeb() { return } - s.conf.HTTPRegister("GET", "/control/stats", s.handleStats) - s.conf.HTTPRegister("POST", "/control/stats_reset", s.handleStatsReset) - s.conf.HTTPRegister("POST", "/control/stats_config", s.handleStatsConfig) - s.conf.HTTPRegister("GET", "/control/stats_info", s.handleStatsInfo) + s.conf.HTTPRegister(http.MethodGet, "/control/stats", s.handleStats) + s.conf.HTTPRegister(http.MethodPost, "/control/stats_reset", s.handleStatsReset) + s.conf.HTTPRegister(http.MethodPost, "/control/stats_config", s.handleStatsConfig) + s.conf.HTTPRegister(http.MethodGet, "/control/stats_info", s.handleStatsInfo) } diff --git a/internal/stats/stats.go b/internal/stats/stats.go index 5cd5910d..7ed1d320 100644 --- a/internal/stats/stats.go +++ b/internal/stats/stats.go @@ -48,7 +48,7 @@ type Stats interface { Update(e Entry) // Get IP addresses of the clients with the most number of requests - GetTopClientsIP(limit uint) []string + GetTopClientsIP(limit uint) []net.IP // WriteDiskConfig - write configuration WriteDiskConfig(dc *DiskConfig) @@ -76,10 +76,14 @@ const ( rLast ) -// Entry - data to add +// Entry is a statistics data entry. type Entry struct { + // Clients is the client's primary ID. + // + // TODO(a.garipov): Make this a {net.IP, string} enum? + Client string + Domain string - Client net.IP Result Result Time uint32 // processing time (msec) } diff --git a/internal/stats/stats_test.go b/internal/stats/stats_test.go index 3a4bed66..e58f198c 100644 --- a/internal/stats/stats_test.go +++ b/internal/stats/stats_test.go @@ -7,12 +7,13 @@ import ( "sync/atomic" "testing" - "github.com/AdguardTeam/AdGuardHome/internal/testutil" + "github.com/AdguardTeam/AdGuardHome/internal/aghtest" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) func TestMain(m *testing.M) { - testutil.DiscardLogOutput(m) + aghtest.DiscardLogOutput(m) } func UIntArrayEquals(a, b []uint64) bool { @@ -34,124 +35,126 @@ func TestStats(t *testing.T) { Filename: "./stats.db", LimitDays: 1, } - s, _ := createObject(conf) - e := Entry{} + s, err := createObject(conf) + require.Nil(t, err) + t.Cleanup(func() { + s.clear() + s.Close() + assert.Nil(t, os.Remove(conf.Filename)) + }) - e.Domain = "domain" - e.Client = net.ParseIP("127.0.0.1") - e.Result = RFiltered - e.Time = 123456 - s.Update(e) + s.Update(Entry{ + Domain: "domain", + Client: "127.0.0.1", + Result: RFiltered, + Time: 123456, + }) + s.Update(Entry{ + Domain: "domain", + Client: "127.0.0.1", + Result: RNotFiltered, + Time: 123456, + }) - e.Domain = "domain" - e.Client = net.ParseIP("127.0.0.1") - e.Result = RNotFiltered - e.Time = 123456 - s.Update(e) + d, ok := s.getData() + require.True(t, ok) - d := s.getData() a := []uint64{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2} - assert.True(t, UIntArrayEquals(d["dns_queries"].([]uint64), a)) + assert.True(t, UIntArrayEquals(d.DNSQueries, a)) a = []uint64{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1} - assert.True(t, UIntArrayEquals(d["blocked_filtering"].([]uint64), a)) + assert.True(t, UIntArrayEquals(d.BlockedFiltering, a)) a = []uint64{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0} - assert.True(t, UIntArrayEquals(d["replaced_safebrowsing"].([]uint64), a)) + assert.True(t, UIntArrayEquals(d.ReplacedSafebrowsing, a)) a = []uint64{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0} - assert.True(t, UIntArrayEquals(d["replaced_parental"].([]uint64), a)) + assert.True(t, UIntArrayEquals(d.ReplacedParental, a)) - m := d["top_queried_domains"].([]map[string]uint64) - assert.True(t, m[0]["domain"] == 1) + m := d.TopQueried + require.NotEmpty(t, m) + assert.EqualValues(t, 1, m[0]["domain"]) - m = d["top_blocked_domains"].([]map[string]uint64) - assert.True(t, m[0]["domain"] == 1) + m = d.TopBlocked + require.NotEmpty(t, m) + assert.EqualValues(t, 1, m[0]["domain"]) - m = d["top_clients"].([]map[string]uint64) - assert.True(t, m[0]["127.0.0.1"] == 2) + m = d.TopClients + require.NotEmpty(t, m) + assert.EqualValues(t, 2, m[0]["127.0.0.1"]) - assert.True(t, d["num_dns_queries"].(uint64) == 2) - assert.True(t, d["num_blocked_filtering"].(uint64) == 1) - assert.True(t, d["num_replaced_safebrowsing"].(uint64) == 0) - assert.True(t, d["num_replaced_safesearch"].(uint64) == 0) - assert.True(t, d["num_replaced_parental"].(uint64) == 0) - assert.True(t, d["avg_processing_time"].(float64) == 0.123456) + assert.EqualValues(t, 2, d.NumDNSQueries) + assert.EqualValues(t, 1, d.NumBlockedFiltering) + assert.EqualValues(t, 0, d.NumReplacedSafebrowsing) + assert.EqualValues(t, 0, d.NumReplacedSafesearch) + assert.EqualValues(t, 0, d.NumReplacedParental) + assert.EqualValues(t, 0.123456, d.AvgProcessingTime) topClients := s.GetTopClientsIP(2) - assert.True(t, topClients[0] == "127.0.0.1") - - s.clear() - s.Close() - os.Remove(conf.Filename) + require.NotEmpty(t, topClients) + assert.True(t, net.IP{127, 0, 0, 1}.Equal(topClients[0])) } func TestLargeNumbers(t *testing.T) { - var hour int32 = 1 + var hour int32 = 0 newID := func() uint32 { - // use "atomic" to make Go race detector happy + // Use "atomic" to make go race detector happy. return uint32(atomic.LoadInt32(&hour)) } - // log.SetLevel(log.DEBUG) conf := Config{ Filename: "./stats.db", LimitDays: 1, UnitID: newID, } - os.Remove(conf.Filename) - s, _ := createObject(conf) - e := Entry{} + s, err := createObject(conf) + require.Nil(t, err) + t.Cleanup(func() { + s.Close() + assert.Nil(t, os.Remove(conf.Filename)) + }) - n := 1000 // number of distinct clients and domains every hour - for h := 0; h != 12; h++ { - if h != 0 { - atomic.AddInt32(&hour, 1) - } - for i := 0; i != n; i++ { - e.Domain = fmt.Sprintf("domain%d", i) - e.Client = net.ParseIP("127.0.0.1") - e.Client[2] = byte((i & 0xff00) >> 8) - e.Client[3] = byte(i & 0xff) - e.Result = RNotFiltered - e.Time = 123456 - s.Update(e) + // Number of distinct clients and domains every hour. + const n = 1000 + + for h := 0; h < 12; h++ { + atomic.AddInt32(&hour, 1) + for i := 0; i < n; i++ { + s.Update(Entry{ + Domain: fmt.Sprintf("domain%d", i), + Client: net.IP{ + 127, + 0, + byte((i & 0xff00) >> 8), + byte(i & 0xff), + }.String(), + Result: RNotFiltered, + Time: 123456, + }) } } - d := s.getData() - assert.True(t, d["num_dns_queries"].(uint64) == uint64(int(hour)*n)) - - s.Close() - os.Remove(conf.Filename) + d, ok := s.getData() + require.True(t, ok) + assert.EqualValues(t, hour*n, d.NumDNSQueries) } -// this code is a chunk copied from getData() that generates aggregate data per day -func aggregateDataPerDay(firstID uint32) int { - firstDayID := (firstID + 24 - 1) / 24 * 24 // align_ceil(24) - a := []uint64{} - var sum uint64 - id := firstDayID - nextDayID := firstDayID + 24 - for i := firstDayID - firstID; int(i) != 720; i++ { - sum++ - if id == nextDayID { - a = append(a, sum) - sum = 0 - nextDayID += 24 +func TestStatsCollector(t *testing.T) { + ng := func(_ *unitDB) uint64 { + return 0 + } + units := make([]*unitDB, 720) + + t.Run("hours", func(t *testing.T) { + statsData := statsCollector(units, 0, Hours, ng) + assert.Len(t, statsData, 720) + }) + + t.Run("days", func(t *testing.T) { + for i := 0; i != 25; i++ { + statsData := statsCollector(units, uint32(i), Days, ng) + require.Lenf(t, statsData, 30, "i=%d", i) } - id++ - } - if id <= nextDayID { - a = append(a, sum) - } - return len(a) -} - -func TestAggregateDataPerTimeUnit(t *testing.T) { - for i := 0; i != 25; i++ { - alen := aggregateDataPerDay(uint32(i)) - assert.True(t, alen == 30, "i=%d", i) - } + }) } diff --git a/internal/stats/unit.go b/internal/stats/unit.go index a8bd224c..96775c72 100644 --- a/internal/stats/unit.go +++ b/internal/stats/unit.go @@ -4,6 +4,7 @@ import ( "bytes" "encoding/binary" "encoding/gob" + "errors" "fmt" "net" "os" @@ -11,10 +12,14 @@ import ( "sync" "time" + "github.com/AdguardTeam/AdGuardHome/internal/agherr" "github.com/AdguardTeam/golibs/log" bolt "go.etcd.io/bbolt" ) +// TODO(a.garipov): Rewrite all of this. Add proper error handling and +// inspection. Improve logging. Decrease complexity. + const ( maxDomains = 100 // max number of top domains to store in file or return via Get() maxClients = 100 // max number of top clients to store in file or return via Get() @@ -61,11 +66,12 @@ type unitDB struct { TimeAvg uint32 // usec } -func createObject(conf Config) (*statsCtx, error) { - s := statsCtx{} +func createObject(conf Config) (s *statsCtx, err error) { + s = &statsCtx{} if !checkInterval(conf.LimitDays) { conf.LimitDays = 1 } + s.conf = &Config{} *s.conf = conf s.conf.limit = conf.LimitDays * 24 @@ -84,27 +90,43 @@ func createObject(conf Config) (*statsCtx, error) { log.Tracef("Deleting old units...") firstID := id - s.conf.limit - 1 unitDel := 0 - forEachBkt := func(name []byte, b *bolt.Bucket) error { - id := uint32(btoi(name)) - if id < firstID { - err := tx.DeleteBucket(name) - if err != nil { - log.Debug("tx.DeleteBucket: %s", err) + + // TODO(a.garipov): See if this is actually necessary. Looks + // like a rather bizarre solution. + errStop := agherr.Error("stop iteration") + forEachBkt := func(name []byte, _ *bolt.Bucket) (cberr error) { + nameID := uint32(btoi(name)) + if nameID < firstID { + cberr = tx.DeleteBucket(name) + if cberr != nil { + log.Debug("stats: tx.DeleteBucket: %s", cberr) + + return nil } - log.Debug("Stats: deleted unit %d", id) + + log.Debug("stats: deleted unit %d", nameID) unitDel++ + return nil } - return fmt.Errorf("") + + return errStop + } + + err = tx.ForEach(forEachBkt) + if err != nil && !errors.Is(err, errStop) { + log.Debug("stats: deleting units: %s", err) } - _ = tx.ForEach(forEachBkt) udb = s.loadUnitFromDB(tx, id) if unitDel != 0 { s.commitTxn(tx) } else { - _ = tx.Rollback() + err = tx.Rollback() + if err != nil { + log.Debug("rolling back: %s", err) + } } } @@ -115,8 +137,9 @@ func createObject(conf Config) (*statsCtx, error) { } s.unit = &u - log.Debug("Stats: initialized") - return &s, nil + log.Debug("stats: initialized") + + return s, nil } func (s *statsCtx) Start() { @@ -133,7 +156,7 @@ func (s *statsCtx) dbOpen() bool { log.Tracef("db.Open...") s.db, err = bolt.Open(s.conf.Filename, 0o644, nil) if err != nil { - log.Error("Stats: open DB: %s: %s", s.conf.Filename, err) + log.Error("stats: open DB: %s: %s", s.conf.Filename, err) if err.Error() == "invalid argument" { log.Error("AdGuard Home cannot be initialized due to an incompatible file system.\nPlease read the explanation here: https://github.com/AdguardTeam/AdGuardHome/internal/wiki/Getting-Started#limitations") } @@ -223,6 +246,7 @@ func (s *statsCtx) periodicFlush() { s.unitLock.Lock() ptr := s.unit s.unitLock.Unlock() + if ptr == nil { break } @@ -230,6 +254,7 @@ func (s *statsCtx) periodicFlush() { id := s.conf.UnitID() if ptr.id == id { time.Sleep(time.Second) + continue } @@ -243,6 +268,7 @@ func (s *statsCtx) periodicFlush() { if tx == nil { continue } + ok1 := s.flushUnitToDB(tx, u.id, udb) ok2 := s.deleteUnit(tx, id-s.conf.limit) if ok1 || ok2 { @@ -251,6 +277,7 @@ func (s *statsCtx) periodicFlush() { _ = tx.Rollback() } } + log.Tracef("periodicFlush() exited") } @@ -258,14 +285,17 @@ func (s *statsCtx) periodicFlush() { func (s *statsCtx) deleteUnit(tx *bolt.Tx, id uint32) bool { err := tx.DeleteBucket(unitName(id)) if err != nil { - log.Tracef("bolt DeleteBucket: %s", err) + log.Tracef("stats: bolt DeleteBucket: %s", err) + return false } - log.Debug("Stats: deleted unit %d", id) + + log.Debug("stats: deleted unit %d", id) + return true } -func convertMapToArray(m map[string]uint64, max int) []countPair { +func convertMapToSlice(m map[string]uint64, max int) []countPair { a := []countPair{} for k, v := range m { pair := countPair{} @@ -283,7 +313,7 @@ func convertMapToArray(m map[string]uint64, max int) []countPair { return a[:max] } -func convertArrayToMap(a []countPair) map[string]uint64 { +func convertSliceToMap(a []countPair) map[string]uint64 { m := map[string]uint64{} for _, it := range a { m[it.Name] = it.Count @@ -301,9 +331,9 @@ func serialize(u *unit) *unitDB { udb.TimeAvg = uint32(u.timeSum / u.nTotal) } - udb.Domains = convertMapToArray(u.domains, maxDomains) - udb.BlockedDomains = convertMapToArray(u.blockedDomains, maxDomains) - udb.Clients = convertMapToArray(u.clients, maxClients) + udb.Domains = convertMapToSlice(u.domains, maxDomains) + udb.BlockedDomains = convertMapToSlice(u.blockedDomains, maxDomains) + udb.Clients = convertMapToSlice(u.clients, maxClients) return &udb } @@ -319,9 +349,9 @@ func deserialize(u *unit, udb *unitDB) { u.nResult[i] = udb.NResult[i] } - u.domains = convertArrayToMap(udb.Domains) - u.blockedDomains = convertArrayToMap(udb.BlockedDomains) - u.clients = convertArrayToMap(udb.Clients) + u.domains = convertSliceToMap(udb.Domains) + u.blockedDomains = convertSliceToMap(udb.BlockedDomains) + u.clients = convertSliceToMap(udb.Clients) u.timeSum = uint64(udb.TimeAvg) * u.nTotal } @@ -372,7 +402,7 @@ func (s *statsCtx) loadUnitFromDB(tx *bolt.Tx, id uint32) *unitDB { return &udb } -func convertTopArray(a []countPair) []map[string]uint64 { +func convertTopSlice(a []countPair) []map[string]uint64 { m := []map[string]uint64{} for _, it := range a { ent := map[string]uint64{} @@ -386,7 +416,7 @@ func (s *statsCtx) setLimit(limitDays int) { conf := *s.conf conf.limit = uint32(limitDays) * 24 s.conf = &conf - log.Debug("Stats: set limit: %d", limitDays) + log.Debug("stats: set limit: %d", limitDays) } func (s *statsCtx) WriteDiskConfig(dc *DiskConfig) { @@ -411,7 +441,7 @@ func (s *statsCtx) Close() { log.Tracef("db.Close") } - log.Debug("Stats: closed") + log.Debug("stats: closed") } // Reset counters and clear database @@ -439,38 +469,42 @@ func (s *statsCtx) clear() { _ = s.dbOpen() - log.Debug("Stats: cleared") + log.Debug("stats: cleared") } // Get Client IP address -func (s *statsCtx) getClientIP(clientIP string) string { - if s.conf.AnonymizeClientIP { - ip := net.ParseIP(clientIP) - if ip != nil { - ip4 := ip.To4() - const AnonymizeClientIP4Mask = 16 - const AnonymizeClientIP6Mask = 112 - if ip4 != nil { - clientIP = ip4.Mask(net.CIDRMask(AnonymizeClientIP4Mask, 32)).String() - } else { - clientIP = ip.Mask(net.CIDRMask(AnonymizeClientIP6Mask, 128)).String() - } +func (s *statsCtx) getClientIP(ip net.IP) (clientIP net.IP) { + if s.conf.AnonymizeClientIP && ip != nil { + const AnonymizeClientIP4Mask = 16 + const AnonymizeClientIP6Mask = 112 + + if ip.To4() != nil { + return ip.Mask(net.CIDRMask(AnonymizeClientIP4Mask, 32)) } + + return ip.Mask(net.CIDRMask(AnonymizeClientIP6Mask, 128)) } - return clientIP + return ip } func (s *statsCtx) Update(e Entry) { if e.Result == 0 || e.Result >= rLast || - len(e.Domain) == 0 || - !(len(e.Client) == 4 || len(e.Client) == 16) { + e.Domain == "" || + e.Client == "" { return } - client := s.getClientIP(e.Client.String()) + + clientID := e.Client + if ip := net.ParseIP(clientID); ip != nil { + ip = s.getClientIP(ip) + clientID = ip.String() + } s.unitLock.Lock() + defer s.unitLock.Unlock() + u := s.unit u.nResult[e.Result]++ @@ -481,10 +515,9 @@ func (s *statsCtx) Update(e Entry) { u.blockedDomains[e.Domain]++ } - u.clients[client]++ + u.clients[clientID]++ u.timeSum += uint64(e.Time) u.nTotal++ - s.unitLock.Unlock() } func (s *statsCtx) loadUnits(limit uint32) ([]*unitDB, uint32) { @@ -521,6 +554,57 @@ func (s *statsCtx) loadUnits(limit uint32) ([]*unitDB, uint32) { return units, firstID } +// numsGetter is a signature for statsCollector argument. +type numsGetter func(u *unitDB) (num uint64) + +// statsCollector collects statisctics for the given *unitDB slice by specified +// timeUnit using ng to retrieve data. +func statsCollector(units []*unitDB, firstID uint32, timeUnit TimeUnit, ng numsGetter) (nums []uint64) { + if timeUnit == Hours { + for _, u := range units { + nums = append(nums, ng(u)) + } + } else { + // Per time unit counters: 720 hours may span 31 days, so we + // skip data for the first day in this case. + // align_ceil(24) + firstDayID := (firstID + 24 - 1) / 24 * 24 + + var sum uint64 + id := firstDayID + nextDayID := firstDayID + 24 + for i := int(firstDayID - firstID); i != len(units); i++ { + sum += ng(units[i]) + if id == nextDayID { + nums = append(nums, sum) + sum = 0 + nextDayID += 24 + } + id++ + } + if id <= nextDayID { + nums = append(nums, sum) + } + } + return nums +} + +// pairsGetter is a signature for topsCollector argument. +type pairsGetter func(u *unitDB) (pairs []countPair) + +// topsCollector collects statistics about highest values fro the given *unitDB +// slice using pg to retrieve data. +func topsCollector(units []*unitDB, max int, pg pairsGetter) []map[string]uint64 { + m := map[string]uint64{} + for _, u := range units { + for _, it := range pg(u) { + m[it.Name] += it.Count + } + } + a2 := convertMapToSlice(m, max) + return convertTopSlice(a2) +} + /* Algorithm: . Prepare array of N units, where N is the value of "limit" configuration setting . Load data for the most recent units from file @@ -548,10 +632,9 @@ func (s *statsCtx) loadUnits(limit uint32) ([]*unitDB, uint32) { * parental-blocked These values are just the sum of data for all units. */ -func (s *statsCtx) getData() map[string]interface{} { +func (s *statsCtx) getData() (statsResponse, bool) { limit := s.conf.limit - d := map[string]interface{}{} timeUnit := Hours if limit/24 > 7 { timeUnit = Days @@ -559,72 +642,28 @@ func (s *statsCtx) getData() map[string]interface{} { units, firstID := s.loadUnits(limit) if units == nil { - return nil + return statsResponse{}, false } - // per time unit counters: - // 720 hours may span 31 days, so we skip data for the first day in this case - firstDayID := (firstID + 24 - 1) / 24 * 24 // align_ceil(24) - - statsCollector := func(numsGetter func(u *unitDB) (num uint64)) (nums []uint64) { - if timeUnit == Hours { - for _, u := range units { - nums = append(nums, numsGetter(u)) - } - } else { - var sum uint64 - id := firstDayID - nextDayID := firstDayID + 24 - for i := int(firstDayID - firstID); i != len(units); i++ { - sum += numsGetter(units[i]) - if id == nextDayID { - nums = append(nums, sum) - sum = 0 - nextDayID += 24 - } - id++ - } - if id <= nextDayID { - nums = append(nums, sum) - } - } - return nums - } - - topsCollector := func(max int, pairsGetter func(u *unitDB) (pairs []countPair)) []map[string]uint64 { - m := map[string]uint64{} - for _, u := range units { - for _, it := range pairsGetter(u) { - m[it.Name] += it.Count - } - } - a2 := convertMapToArray(m, max) - return convertTopArray(a2) - } - - dnsQueries := statsCollector(func(u *unitDB) (num uint64) { return u.NTotal }) + dnsQueries := statsCollector(units, firstID, timeUnit, func(u *unitDB) (num uint64) { return u.NTotal }) if timeUnit != Hours && len(dnsQueries) != int(limit/24) { log.Fatalf("len(dnsQueries) != limit: %d %d", len(dnsQueries), limit) } - statsData := map[string]interface{}{ - "dns_queries": dnsQueries, - "blocked_filtering": statsCollector(func(u *unitDB) (num uint64) { return u.NResult[RFiltered] }), - "replaced_safebrowsing": statsCollector(func(u *unitDB) (num uint64) { return u.NResult[RSafeBrowsing] }), - "replaced_parental": statsCollector(func(u *unitDB) (num uint64) { return u.NResult[RParental] }), - "top_queried_domains": topsCollector(maxDomains, func(u *unitDB) (pairs []countPair) { return u.Domains }), - "top_blocked_domains": topsCollector(maxDomains, func(u *unitDB) (pairs []countPair) { return u.BlockedDomains }), - "top_clients": topsCollector(maxClients, func(u *unitDB) (pairs []countPair) { return u.Clients }), + data := statsResponse{ + DNSQueries: dnsQueries, + BlockedFiltering: statsCollector(units, firstID, timeUnit, func(u *unitDB) (num uint64) { return u.NResult[RFiltered] }), + ReplacedSafebrowsing: statsCollector(units, firstID, timeUnit, func(u *unitDB) (num uint64) { return u.NResult[RSafeBrowsing] }), + ReplacedParental: statsCollector(units, firstID, timeUnit, func(u *unitDB) (num uint64) { return u.NResult[RParental] }), + TopQueried: topsCollector(units, maxDomains, func(u *unitDB) (pairs []countPair) { return u.Domains }), + TopBlocked: topsCollector(units, maxDomains, func(u *unitDB) (pairs []countPair) { return u.BlockedDomains }), + TopClients: topsCollector(units, maxClients, func(u *unitDB) (pairs []countPair) { return u.Clients }), } - for dataKey, dataValue := range statsData { - d[dataKey] = dataValue + // Total counters: + sum := unitDB{ + NResult: make([]uint64, rLast), } - - // total counters: - - sum := unitDB{} - sum.NResult = make([]uint64, rLast) timeN := 0 for _, u := range units { sum.NTotal += u.NTotal @@ -638,27 +677,25 @@ func (s *statsCtx) getData() map[string]interface{} { sum.NResult[RParental] += u.NResult[RParental] } - d["num_dns_queries"] = sum.NTotal - d["num_blocked_filtering"] = sum.NResult[RFiltered] - d["num_replaced_safebrowsing"] = sum.NResult[RSafeBrowsing] - d["num_replaced_safesearch"] = sum.NResult[RSafeSearch] - d["num_replaced_parental"] = sum.NResult[RParental] + data.NumDNSQueries = sum.NTotal + data.NumBlockedFiltering = sum.NResult[RFiltered] + data.NumReplacedSafebrowsing = sum.NResult[RSafeBrowsing] + data.NumReplacedSafesearch = sum.NResult[RSafeSearch] + data.NumReplacedParental = sum.NResult[RParental] - avgTime := float64(0) if timeN != 0 { - avgTime = float64(sum.TimeAvg/uint32(timeN)) / 1000000 + data.AvgProcessingTime = float64(sum.TimeAvg/uint32(timeN)) / 1000000 } - d["avg_processing_time"] = avgTime - d["time_units"] = "hours" + data.TimeUnits = "hours" if timeUnit == Days { - d["time_units"] = "days" + data.TimeUnits = "days" } - return d + return data, true } -func (s *statsCtx) GetTopClientsIP(maxCount uint) []string { +func (s *statsCtx) GetTopClientsIP(maxCount uint) []net.IP { units, _ := s.loadUnits(s.conf.limit) if units == nil { return nil @@ -671,10 +708,10 @@ func (s *statsCtx) GetTopClientsIP(maxCount uint) []string { m[it.Name] += it.Count } } - a := convertMapToArray(m, int(maxCount)) - d := []string{} + a := convertMapToSlice(m, int(maxCount)) + d := []net.IP{} for _, it := range a { - d = append(d, it.Name) + d = append(d, net.ParseIP(it.Name)) } return d } diff --git a/internal/sysutil/net.go b/internal/sysutil/net.go index 557dd8d7..9ba41704 100644 --- a/internal/sysutil/net.go +++ b/internal/sysutil/net.go @@ -5,10 +5,17 @@ import ( "os/exec" "strings" + "github.com/AdguardTeam/AdGuardHome/internal/agherr" "github.com/AdguardTeam/golibs/log" ) +// ErrNoStaticIPInfo is returned by IfaceHasStaticIP when no information about +// the IP being static is available. +const ErrNoStaticIPInfo agherr.Error = "no information about static ip" + // IfaceHasStaticIP checks if interface is configured to have static IP address. +// If it can't give a definitive answer, it returns false and an error for which +// errors.Is(err, ErrNoStaticIPInfo) is true. func IfaceHasStaticIP(ifaceName string) (has bool, err error) { return ifaceHasStaticIP(ifaceName) } @@ -19,12 +26,12 @@ func IfaceSetStaticIP(ifaceName string) (err error) { } // GatewayIP returns IP address of interface's gateway. -func GatewayIP(ifaceName string) string { +func GatewayIP(ifaceName string) net.IP { cmd := exec.Command("ip", "route", "show", "dev", ifaceName) log.Tracef("executing %s %v", cmd.Path, cmd.Args) d, err := cmd.Output() if err != nil || cmd.ProcessState.ExitCode() != 0 { - return "" + return nil } fields := strings.Fields(string(d)) @@ -32,13 +39,8 @@ func GatewayIP(ifaceName string) string { // "default" at first field and default gateway IP address at third // field. if len(fields) < 3 || fields[0] != "default" { - return "" + return nil } - ip := net.ParseIP(fields[2]) - if ip == nil { - return "" - } - - return fields[2] + return net.ParseIP(fields[2]) } diff --git a/internal/sysutil/net_linux.go b/internal/sysutil/net_linux.go index 5206f9fd..1964f9dd 100644 --- a/internal/sysutil/net_linux.go +++ b/internal/sysutil/net_linux.go @@ -21,7 +21,11 @@ import ( const maxConfigFileSize = 1024 * 1024 func ifaceHasStaticIP(ifaceName string) (has bool, err error) { - var f *os.File + // TODO(a.garipov): Currently, this function returns the first + // definitive result. So if /etc/dhcpcd.conf has a static IP while + // /etc/network/interfaces doesn't, it will return true. Perhaps this + // is not the most desirable behavior. + for _, check := range []struct { checker func(io.Reader, string) (bool, error) filePath string @@ -32,28 +36,37 @@ func ifaceHasStaticIP(ifaceName string) (has bool, err error) { checker: ifacesStaticConfig, filePath: "/etc/network/interfaces", }} { + var f *os.File f, err = os.Open(check.filePath) - if errors.Is(err, os.ErrNotExist) { - continue - } if err != nil { + // ErrNotExist can happen here if there is no such file. + // This is normal, as not every system uses those files. + if errors.Is(err, os.ErrNotExist) { + err = nil + + continue + } + return false, err } defer f.Close() - fileReadCloser, err := aghio.LimitReadCloser(f, maxConfigFileSize) + var fileReadCloser io.ReadCloser + fileReadCloser, err = aghio.LimitReadCloser(f, maxConfigFileSize) if err != nil { return false, err } defer fileReadCloser.Close() has, err = check.checker(fileReadCloser, ifaceName) - if has || err != nil { - break + if err != nil { + return false, err } + + return has, nil } - return has, err + return false, ErrNoStaticIPInfo } // dhcpcdStaticConfig checks if interface is configured by /etc/dhcpcd.conf to @@ -119,17 +132,13 @@ func ifacesStaticConfig(r io.Reader, ifaceName string) (has bool, err error) { } func ifaceSetStaticIP(ifaceName string) (err error) { - ip := util.GetSubnet(ifaceName) - if len(ip) == 0 { + ipNet := util.GetSubnet(ifaceName) + if ipNet.IP == nil { return errors.New("can't get IP address") } - ip4, _, err := net.ParseCIDR(ip) - if err != nil { - return err - } gatewayIP := GatewayIP(ifaceName) - add := updateStaticIPdhcpcdConf(ifaceName, ip, gatewayIP, ip4.String()) + add := updateStaticIPdhcpcdConf(ifaceName, ipNet.String(), gatewayIP, ipNet.IP) body, err := ioutil.ReadFile("/etc/dhcpcd.conf") if err != nil { @@ -147,14 +156,14 @@ func ifaceSetStaticIP(ifaceName string) (err error) { // updateStaticIPdhcpcdConf sets static IP address for the interface by writing // into dhcpd.conf. -func updateStaticIPdhcpcdConf(ifaceName, ip, gatewayIP, dnsIP string) string { +func updateStaticIPdhcpcdConf(ifaceName, ip string, gatewayIP, dnsIP net.IP) string { var body []byte add := fmt.Sprintf("\ninterface %s\nstatic ip_address=%s\n", ifaceName, ip) body = append(body, []byte(add)...) - if len(gatewayIP) != 0 { + if gatewayIP != nil { add = fmt.Sprintf("static routers=%s\n", gatewayIP) body = append(body, []byte(add)...) diff --git a/internal/sysutil/net_linux_test.go b/internal/sysutil/net_linux_test.go index 8cadbbb7..3fbfc547 100644 --- a/internal/sysutil/net_linux_test.go +++ b/internal/sysutil/net_linux_test.go @@ -4,9 +4,11 @@ package sysutil import ( "bytes" + "net" "testing" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) const nl = "\n" @@ -47,7 +49,7 @@ func TestDHCPCDStaticConfig(t *testing.T) { t.Run(tc.name, func(t *testing.T) { r := bytes.NewReader(tc.data) has, err := dhcpcdStaticConfig(r, "wlan0") - assert.Nil(t, err) + require.Nil(t, err) assert.Equal(t, tc.want, has) }) } @@ -84,26 +86,36 @@ func TestIfacesStaticConfig(t *testing.T) { t.Run(tc.name, func(t *testing.T) { r := bytes.NewReader(tc.data) has, err := ifacesStaticConfig(r, "enp0s3") - assert.Nil(t, err) + require.Nil(t, err) assert.Equal(t, tc.want, has) }) } } func TestSetStaticIPdhcpcdConf(t *testing.T) { - dhcpcdConf := nl + `interface wlan0` + nl + - `static ip_address=192.168.0.2/24` + nl + - `static routers=192.168.0.1` + nl + - `static domain_name_servers=192.168.0.2` + nl + nl + testCases := []struct { + name string + dhcpcdConf string + routers net.IP + }{{ + name: "with_gateway", + dhcpcdConf: nl + `interface wlan0` + nl + + `static ip_address=192.168.0.2/24` + nl + + `static routers=192.168.0.1` + nl + + `static domain_name_servers=192.168.0.2` + nl + nl, + routers: net.IP{192, 168, 0, 1}, + }, { + name: "without_gateway", + dhcpcdConf: nl + `interface wlan0` + nl + + `static ip_address=192.168.0.2/24` + nl + + `static domain_name_servers=192.168.0.2` + nl + nl, + routers: nil, + }} - s := updateStaticIPdhcpcdConf("wlan0", "192.168.0.2/24", "192.168.0.1", "192.168.0.2") - assert.Equal(t, dhcpcdConf, s) - - // without gateway - dhcpcdConf = nl + `interface wlan0` + nl + - `static ip_address=192.168.0.2/24` + nl + - `static domain_name_servers=192.168.0.2` + nl + nl - - s = updateStaticIPdhcpcdConf("wlan0", "192.168.0.2/24", "", "192.168.0.2") - assert.Equal(t, dhcpcdConf, s) + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + s := updateStaticIPdhcpcdConf("wlan0", "192.168.0.2/24", tc.routers, net.IP{192, 168, 0, 2}) + assert.Equal(t, tc.dhcpcdConf, s) + }) + } } diff --git a/internal/sysutil/syslog_others.go b/internal/sysutil/syslog_others.go index 0e0e1c3f..91fe27cf 100644 --- a/internal/sysutil/syslog_others.go +++ b/internal/sysutil/syslog_others.go @@ -1,4 +1,4 @@ -// +build !windows,!nacl,!plan9 +// +build !windows,!plan9 package sysutil diff --git a/internal/sysutil/syslog_windows.go b/internal/sysutil/syslog_windows.go index 2160ea43..1d5cd03f 100644 --- a/internal/sysutil/syslog_windows.go +++ b/internal/sysutil/syslog_windows.go @@ -1,4 +1,4 @@ -// +build windows nacl plan9 +// +build windows plan9 package sysutil diff --git a/internal/sysutil/sysutil_test.go b/internal/sysutil/sysutil_test.go index 0cddbf42..8ca0ff66 100644 --- a/internal/sysutil/sysutil_test.go +++ b/internal/sysutil/sysutil_test.go @@ -3,9 +3,9 @@ package sysutil import ( "testing" - "github.com/AdguardTeam/AdGuardHome/internal/testutil" + "github.com/AdguardTeam/AdGuardHome/internal/aghtest" ) func TestMain(m *testing.M) { - testutil.DiscardLogOutput(m) + aghtest.DiscardLogOutput(m) } diff --git a/internal/update/check.go b/internal/update/check.go deleted file mode 100644 index e83ab5c2..00000000 --- a/internal/update/check.go +++ /dev/null @@ -1,114 +0,0 @@ -package update - -import ( - "encoding/json" - "fmt" - "io/ioutil" - "strings" - "time" - - "github.com/AdguardTeam/AdGuardHome/internal/aghio" -) - -const versionCheckPeriod = 8 * 60 * 60 - -// VersionInfo - VersionInfo -type VersionInfo struct { - NewVersion string // New version string - Announcement string // Announcement text - AnnouncementURL string // Announcement URL - SelfUpdateMinVersion string // Min version starting with which we can auto-update - CanAutoUpdate bool // If true - we can auto-update -} - -// MaxResponseSize is responses on server's requests maximum length in bytes. -const MaxResponseSize = 64 * 1024 - -// GetVersionResponse - downloads version.json (if needed) and deserializes it -func (u *Updater) GetVersionResponse(forceRecheck bool) (VersionInfo, error) { - if !forceRecheck && - u.versionCheckLastTime.Unix()+versionCheckPeriod > time.Now().Unix() { - return u.parseVersionResponse(u.versionJSON) - } - - resp, err := u.Client.Get(u.VersionURL) - if err != nil { - return VersionInfo{}, fmt.Errorf("updater: HTTP GET %s: %w", u.VersionURL, err) - } - defer resp.Body.Close() - - resp.Body, err = aghio.LimitReadCloser(resp.Body, MaxResponseSize) - if err != nil { - return VersionInfo{}, fmt.Errorf("updater: LimitReadCloser: %w", err) - } - defer resp.Body.Close() - - // This use of ReadAll is safe, because we just limited the appropriate - // ReadCloser. - body, err := ioutil.ReadAll(resp.Body) - if err != nil { - return VersionInfo{}, fmt.Errorf("updater: HTTP GET %s: %w", u.VersionURL, err) - } - - u.versionJSON = body - u.versionCheckLastTime = time.Now() - - return u.parseVersionResponse(body) -} - -func (u *Updater) parseVersionResponse(data []byte) (VersionInfo, error) { - info := VersionInfo{} - versionJSON := make(map[string]interface{}) - err := json.Unmarshal(data, &versionJSON) - if err != nil { - return info, fmt.Errorf("version.json: %w", err) - } - - var ok1, ok2, ok3, ok4 bool - info.NewVersion, ok1 = versionJSON["version"].(string) - info.Announcement, ok2 = versionJSON["announcement"].(string) - info.AnnouncementURL, ok3 = versionJSON["announcement_url"].(string) - info.SelfUpdateMinVersion, ok4 = versionJSON["selfupdate_min_version"].(string) - if !ok1 || !ok2 || !ok3 || !ok4 { - return info, fmt.Errorf("version.json: invalid data") - } - - packageURL, ok := u.getDownloadURL(versionJSON) - - if ok && - info.NewVersion != u.VersionString && - strings.TrimPrefix(u.VersionString, "v") >= strings.TrimPrefix(info.SelfUpdateMinVersion, "v") { - info.CanAutoUpdate = true - } - - u.NewVersion = info.NewVersion - u.PackageURL = packageURL - - return info, nil -} - -// Get download URL for the current GOOS/GOARCH/ARMVersion -func (u *Updater) getDownloadURL(json map[string]interface{}) (string, bool) { - var key string - - if u.Arch == "arm" && u.ARMVersion != "" { - // the key is: - // download_linux_armv5 for ARMv5 - // download_linux_armv6 for ARMv6 - // download_linux_armv7 for ARMv7 - key = fmt.Sprintf("download_%s_%sv%s", u.OS, u.Arch, u.ARMVersion) - } - - val, ok := json[key] - if !ok { - // the key is download_linux_arm or download_linux_arm64 for regular ARM versions - key = fmt.Sprintf("download_%s_%s", u.OS, u.Arch) - val, ok = json[key] - } - - if !ok { - return "", false - } - - return val.(string), true -} diff --git a/internal/update/update_test.go b/internal/update/update_test.go deleted file mode 100644 index ceca2b9d..00000000 --- a/internal/update/update_test.go +++ /dev/null @@ -1,215 +0,0 @@ -package update - -import ( - "fmt" - "io/ioutil" - "net" - "net/http" - "os" - "testing" - - "github.com/AdguardTeam/AdGuardHome/internal/testutil" - "github.com/stretchr/testify/assert" -) - -func TestMain(m *testing.M) { - testutil.DiscardLogOutput(m) -} - -func startHTTPServer(data string) (net.Listener, uint16) { - mux := http.NewServeMux() - mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { - _, _ = w.Write([]byte(data)) - }) - - listener, err := net.Listen("tcp", ":0") - if err != nil { - panic(err) - } - - go func() { _ = http.Serve(listener, mux) }() - return listener, uint16(listener.Addr().(*net.TCPAddr).Port) -} - -func TestUpdateGetVersion(t *testing.T) { - const jsonData = `{ - "version": "v0.103.0-beta2", - "announcement": "AdGuard Home v0.103.0-beta2 is now available!", - "announcement_url": "https://github.com/AdguardTeam/AdGuardHome/internal/releases", - "selfupdate_min_version": "v0.0", - "download_windows_amd64": "https://static.adguard.com/adguardhome/beta/AdGuardHome_windows_amd64.zip", - "download_windows_386": "https://static.adguard.com/adguardhome/beta/AdGuardHome_windows_386.zip", - "download_darwin_amd64": "https://static.adguard.com/adguardhome/beta/AdGuardHome_darwin_amd64.zip", - "download_darwin_386": "https://static.adguard.com/adguardhome/beta/AdGuardHome_darwin_386.zip", - "download_linux_amd64": "https://static.adguard.com/adguardhome/beta/AdGuardHome_linux_amd64.tar.gz", - "download_linux_386": "https://static.adguard.com/adguardhome/beta/AdGuardHome_linux_386.tar.gz", - "download_linux_arm": "https://static.adguard.com/adguardhome/beta/AdGuardHome_linux_armv6.tar.gz", - "download_linux_armv5": "https://static.adguard.com/adguardhome/beta/AdGuardHome_linux_armv5.tar.gz", - "download_linux_armv6": "https://static.adguard.com/adguardhome/beta/AdGuardHome_linux_armv6.tar.gz", - "download_linux_armv7": "https://static.adguard.com/adguardhome/beta/AdGuardHome_linux_armv7.tar.gz", - "download_linux_arm64": "https://static.adguard.com/adguardhome/beta/AdGuardHome_linux_arm64.tar.gz", - "download_linux_mips": "https://static.adguard.com/adguardhome/beta/AdGuardHome_linux_mips_softfloat.tar.gz", - "download_linux_mipsle": "https://static.adguard.com/adguardhome/beta/AdGuardHome_linux_mipsle_softfloat.tar.gz", - "download_linux_mips64": "https://static.adguard.com/adguardhome/beta/AdGuardHome_linux_mips64_softfloat.tar.gz", - "download_linux_mips64le": "https://static.adguard.com/adguardhome/beta/AdGuardHome_linux_mips64le_softfloat.tar.gz", - "download_freebsd_386": "https://static.adguard.com/adguardhome/beta/AdGuardHome_freebsd_386.tar.gz", - "download_freebsd_amd64": "https://static.adguard.com/adguardhome/beta/AdGuardHome_freebsd_amd64.tar.gz", - "download_freebsd_arm": "https://static.adguard.com/adguardhome/beta/AdGuardHome_freebsd_armv6.tar.gz", - "download_freebsd_armv5": "https://static.adguard.com/adguardhome/beta/AdGuardHome_freebsd_armv5.tar.gz", - "download_freebsd_armv6": "https://static.adguard.com/adguardhome/beta/AdGuardHome_freebsd_armv6.tar.gz", - "download_freebsd_armv7": "https://static.adguard.com/adguardhome/beta/AdGuardHome_freebsd_armv7.tar.gz", - "download_freebsd_arm64": "https://static.adguard.com/adguardhome/beta/AdGuardHome_freebsd_arm64.tar.gz" -}` - - l, lport := startHTTPServer(jsonData) - defer func() { _ = l.Close() }() - - u := NewUpdater(Config{ - Client: &http.Client{}, - VersionURL: fmt.Sprintf("http://127.0.0.1:%d/", lport), - OS: "linux", - Arch: "arm", - VersionString: "v0.103.0-beta1", - }) - - info, err := u.GetVersionResponse(false) - assert.Nil(t, err) - assert.Equal(t, "v0.103.0-beta2", info.NewVersion) - assert.Equal(t, "AdGuard Home v0.103.0-beta2 is now available!", info.Announcement) - assert.Equal(t, "https://github.com/AdguardTeam/AdGuardHome/internal/releases", info.AnnouncementURL) - assert.Equal(t, "v0.0", info.SelfUpdateMinVersion) - assert.True(t, info.CanAutoUpdate) - - _ = l.Close() - - // check cached - _, err = u.GetVersionResponse(false) - assert.Nil(t, err) -} - -func TestUpdate(t *testing.T) { - _ = os.Mkdir("aghtest", 0o755) - defer func() { - _ = os.RemoveAll("aghtest") - }() - - // create "current" files - assert.Nil(t, ioutil.WriteFile("aghtest/AdGuardHome", []byte("AdGuardHome"), 0o755)) - assert.Nil(t, ioutil.WriteFile("aghtest/README.md", []byte("README.md"), 0o644)) - assert.Nil(t, ioutil.WriteFile("aghtest/LICENSE.txt", []byte("LICENSE.txt"), 0o644)) - assert.Nil(t, ioutil.WriteFile("aghtest/AdGuardHome.yaml", []byte("AdGuardHome.yaml"), 0o644)) - - // start server for returning package file - pkgData, err := ioutil.ReadFile("test/AdGuardHome.tar.gz") - assert.Nil(t, err) - l, lport := startHTTPServer(string(pkgData)) - defer func() { _ = l.Close() }() - - u := NewUpdater(Config{ - Client: &http.Client{}, - PackageURL: fmt.Sprintf("http://127.0.0.1:%d/AdGuardHome.tar.gz", lport), - VersionString: "v0.103.0", - NewVersion: "v0.103.1", - ConfigName: "aghtest/AdGuardHome.yaml", - WorkDir: "aghtest", - }) - - assert.Nil(t, u.prepare()) - u.currentExeName = "aghtest/AdGuardHome" - assert.Nil(t, u.downloadPackageFile(u.PackageURL, u.packageName)) - assert.Nil(t, u.unpack()) - // assert.Nil(t, u.check()) - assert.Nil(t, u.backup()) - assert.Nil(t, u.replace()) - u.clean() - - // check backup files - d, err := ioutil.ReadFile("aghtest/agh-backup/AdGuardHome.yaml") - assert.Nil(t, err) - assert.Equal(t, "AdGuardHome.yaml", string(d)) - - d, err = ioutil.ReadFile("aghtest/agh-backup/AdGuardHome") - assert.Nil(t, err) - assert.Equal(t, "AdGuardHome", string(d)) - - // check updated files - d, err = ioutil.ReadFile("aghtest/AdGuardHome") - assert.Nil(t, err) - assert.Equal(t, "1", string(d)) - - d, err = ioutil.ReadFile("aghtest/README.md") - assert.Nil(t, err) - assert.Equal(t, "2", string(d)) - - d, err = ioutil.ReadFile("aghtest/LICENSE.txt") - assert.Nil(t, err) - assert.Equal(t, "3", string(d)) - - d, err = ioutil.ReadFile("aghtest/AdGuardHome.yaml") - assert.Nil(t, err) - assert.Equal(t, "AdGuardHome.yaml", string(d)) -} - -func TestUpdateWindows(t *testing.T) { - _ = os.Mkdir("aghtest", 0o755) - defer func() { - _ = os.RemoveAll("aghtest") - }() - - // create "current" files - assert.Nil(t, ioutil.WriteFile("aghtest/AdGuardHome.exe", []byte("AdGuardHome.exe"), 0o755)) - assert.Nil(t, ioutil.WriteFile("aghtest/README.md", []byte("README.md"), 0o644)) - assert.Nil(t, ioutil.WriteFile("aghtest/LICENSE.txt", []byte("LICENSE.txt"), 0o644)) - assert.Nil(t, ioutil.WriteFile("aghtest/AdGuardHome.yaml", []byte("AdGuardHome.yaml"), 0o644)) - - // start server for returning package file - pkgData, err := ioutil.ReadFile("test/AdGuardHome.zip") - assert.Nil(t, err) - l, lport := startHTTPServer(string(pkgData)) - defer func() { _ = l.Close() }() - - u := NewUpdater(Config{ - WorkDir: "aghtest", - Client: &http.Client{}, - PackageURL: fmt.Sprintf("http://127.0.0.1:%d/AdGuardHome.zip", lport), - OS: "windows", - VersionString: "v0.103.0", - NewVersion: "v0.103.1", - ConfigName: "aghtest/AdGuardHome.yaml", - }) - - assert.Nil(t, u.prepare()) - u.currentExeName = "aghtest/AdGuardHome.exe" - assert.Nil(t, u.downloadPackageFile(u.PackageURL, u.packageName)) - assert.Nil(t, u.unpack()) - // assert.Nil(t, u.check()) - assert.Nil(t, u.backup()) - assert.Nil(t, u.replace()) - u.clean() - - // check backup files - d, err := ioutil.ReadFile("aghtest/agh-backup/AdGuardHome.yaml") - assert.Nil(t, err) - assert.Equal(t, "AdGuardHome.yaml", string(d)) - - d, err = ioutil.ReadFile("aghtest/agh-backup/AdGuardHome.exe") - assert.Nil(t, err) - assert.Equal(t, "AdGuardHome.exe", string(d)) - - // check updated files - d, err = ioutil.ReadFile("aghtest/AdGuardHome.exe") - assert.Nil(t, err) - assert.Equal(t, "1", string(d)) - - d, err = ioutil.ReadFile("aghtest/README.md") - assert.Nil(t, err) - assert.Equal(t, "2", string(d)) - - d, err = ioutil.ReadFile("aghtest/LICENSE.txt") - assert.Nil(t, err) - assert.Equal(t, "3", string(d)) - - d, err = ioutil.ReadFile("aghtest/AdGuardHome.yaml") - assert.Nil(t, err) - assert.Equal(t, "AdGuardHome.yaml", string(d)) -} diff --git a/internal/updater/check.go b/internal/updater/check.go new file mode 100644 index 00000000..a23cfc05 --- /dev/null +++ b/internal/updater/check.go @@ -0,0 +1,127 @@ +package updater + +import ( + "encoding/json" + "fmt" + "io/ioutil" + "strings" + "time" + + "github.com/AdguardTeam/AdGuardHome/internal/aghio" +) + +// TODO(a.garipov): Make configurable. +const versionCheckPeriod = 8 * time.Hour + +// VersionInfo contains information about a new version. +type VersionInfo struct { + NewVersion string `json:"new_version,omitempty"` + Announcement string `json:"announcement,omitempty"` + AnnouncementURL string `json:"announcement_url,omitempty"` + SelfUpdateMinVersion string `json:"-"` + CanAutoUpdate *bool `json:"can_autoupdate,omitempty"` +} + +// MaxResponseSize is responses on server's requests maximum length in bytes. +const MaxResponseSize = 64 * 1024 + +// VersionInfo downloads the latest version information. If forceRecheck is +// false and there are cached results, those results are returned. +func (u *Updater) VersionInfo(forceRecheck bool) (VersionInfo, error) { + u.mu.Lock() + defer u.mu.Unlock() + + now := time.Now() + recheckTime := u.prevCheckTime.Add(versionCheckPeriod) + if !forceRecheck && now.Before(recheckTime) { + return u.prevCheckResult, u.prevCheckError + } + + vcu := u.versionCheckURL + resp, err := u.client.Get(vcu) + if err != nil { + return VersionInfo{}, fmt.Errorf("updater: HTTP GET %s: %w", vcu, err) + } + defer resp.Body.Close() + + resp.Body, err = aghio.LimitReadCloser(resp.Body, MaxResponseSize) + if err != nil { + return VersionInfo{}, fmt.Errorf("updater: LimitReadCloser: %w", err) + } + defer resp.Body.Close() + + // This use of ReadAll is safe, because we just limited the appropriate + // ReadCloser. + body, err := ioutil.ReadAll(resp.Body) + if err != nil { + return VersionInfo{}, fmt.Errorf("updater: HTTP GET %s: %w", vcu, err) + } + + u.prevCheckTime = time.Now() + u.prevCheckResult, u.prevCheckError = u.parseVersionResponse(body) + + return u.prevCheckResult, u.prevCheckError +} + +func (u *Updater) parseVersionResponse(data []byte) (VersionInfo, error) { + var canAutoUpdate bool + info := VersionInfo{ + CanAutoUpdate: &canAutoUpdate, + } + versionJSON := map[string]string{ + "version": "", + "announcement": "", + "announcement_url": "", + "selfupdate_min_version": "", + } + err := json.Unmarshal(data, &versionJSON) + if err != nil { + return info, fmt.Errorf("version.json: %w", err) + } + + for _, v := range versionJSON { + if v == "" { + return info, fmt.Errorf("version.json: invalid data") + } + } + + info.NewVersion = versionJSON["version"] + info.Announcement = versionJSON["announcement"] + info.AnnouncementURL = versionJSON["announcement_url"] + info.SelfUpdateMinVersion = versionJSON["selfupdate_min_version"] + + packageURL, ok := u.downloadURL(versionJSON) + if ok && + info.NewVersion != u.version && + strings.TrimPrefix(u.version, "v") >= strings.TrimPrefix(info.SelfUpdateMinVersion, "v") { + canAutoUpdate = true + } + + u.newVersion = info.NewVersion + u.packageURL = packageURL + + return info, nil +} + +// downloadURL returns the download URL for current build. +func (u *Updater) downloadURL(json map[string]string) (string, bool) { + var key string + + if u.goarch == "arm" && u.goarm != "" { + key = fmt.Sprintf("download_%s_%sv%s", u.goos, u.goarch, u.goarm) + } else if u.goarch == "mips" && u.gomips != "" { + key = fmt.Sprintf("download_%s_%s_%s", u.goos, u.goarch, u.gomips) + } + + val, ok := json[key] + if !ok { + key = fmt.Sprintf("download_%s_%s", u.goos, u.goarch) + val, ok = json[key] + } + + if !ok { + return "", false + } + + return val, true +} diff --git a/internal/update/test/AdGuardHome.tar.gz b/internal/updater/testdata/AdGuardHome.tar.gz similarity index 100% rename from internal/update/test/AdGuardHome.tar.gz rename to internal/updater/testdata/AdGuardHome.tar.gz diff --git a/internal/update/test/AdGuardHome.zip b/internal/updater/testdata/AdGuardHome.zip similarity index 100% rename from internal/update/test/AdGuardHome.zip rename to internal/updater/testdata/AdGuardHome.zip diff --git a/internal/update/updater.go b/internal/updater/updater.go similarity index 67% rename from internal/update/updater.go rename to internal/updater/updater.go index b6901889..73a2f7cb 100644 --- a/internal/update/updater.go +++ b/internal/updater/updater.go @@ -1,5 +1,5 @@ -// Package update provides an updater for AdGuardHome. -package update +// Package updater provides an updater for AdGuardHome. +package updater import ( "archive/tar" @@ -9,62 +9,106 @@ import ( "io" "io/ioutil" "net/http" + "net/url" "os" "os/exec" + "path" "path/filepath" "strings" + "sync" "time" "github.com/AdguardTeam/AdGuardHome/internal/aghio" "github.com/AdguardTeam/AdGuardHome/internal/util" + "github.com/AdguardTeam/AdGuardHome/internal/version" "github.com/AdguardTeam/golibs/log" ) -// Updater - Updater +// Updater is the AdGuard Home updater. type Updater struct { - Config // Updater configuration + client *http.Client + version string + channel string + goarch string + goos string + goarm string + gomips string + + workDir string + confName string + versionCheckURL string + + // mu protects all fields below. + mu *sync.RWMutex + + // TODO(a.garipov): See if all of these fields actually have to be in + // this struct. currentExeName string // current binary executable - updateDir string // "work_dir/agh-update-v0.103.0" - packageName string // "work_dir/agh-update-v0.103.0/pkg_name.tar.gz" - backupDir string // "work_dir/agh-backup" - backupExeName string // "work_dir/agh-backup/AdGuardHome[.exe]" - updateExeName string // "work_dir/agh-update-v0.103.0/AdGuardHome[.exe]" + updateDir string // "workDir/agh-update-v0.103.0" + packageName string // "workDir/agh-update-v0.103.0/pkg_name.tar.gz" + backupDir string // "workDir/agh-backup" + backupExeName string // "workDir/agh-backup/AdGuardHome[.exe]" + updateExeName string // "workDir/agh-update-v0.103.0/AdGuardHome[.exe]" unpackedFiles []string - // cached version.json to avoid hammering github.io for each page reload - versionJSON []byte - versionCheckLastTime time.Time + newVersion string + packageURL string + + // Cached fields to prevent too many API requests. + prevCheckError error + prevCheckTime time.Time + prevCheckResult VersionInfo } -// Config - updater config +// Config is the AdGuard Home updater configuration. type Config struct { Client *http.Client - VersionURL string // version.json URL - VersionString string - OS string // GOOS - Arch string // GOARCH - ARMVersion string // ARM version, e.g. "6" - NewVersion string // VersionInfo.NewVersion - PackageURL string // VersionInfo.PackageURL - ConfigName string // current config file ".../AdGuardHome.yaml" - WorkDir string // updater work dir (where backup/upd dirs will be created) + Version string + Channel string + GOARCH string + GOOS string + GOARM string + GOMIPS string + + // ConfName is the name of the current configuration file. Typically, + // "AdGuardHome.yaml". + ConfName string + // WorkDir is the working directory that is used for temporary files. + WorkDir string } -// NewUpdater - creates a new instance of the Updater -func NewUpdater(cfg Config) *Updater { +// NewUpdater creates a new Updater. +func NewUpdater(conf *Config) *Updater { + u := &url.URL{ + Scheme: "https", + Host: "static.adguard.com", + Path: path.Join("adguardhome", conf.Channel, "version.json"), + } return &Updater{ - Config: cfg, + client: conf.Client, + + version: conf.Version, + channel: conf.Channel, + goarch: conf.GOARCH, + goos: conf.GOOS, + goarm: conf.GOARM, + gomips: conf.GOMIPS, + + confName: conf.ConfName, + workDir: conf.WorkDir, + versionCheckURL: u.String(), + + mu: &sync.RWMutex{}, } } -// DoUpdate - conducts the auto-update -// 1. Downloads the update file -// 2. Unpacks it and checks the contents -// 3. Backups the current version and configuration -// 4. Replaces the old files -func (u *Updater) DoUpdate() error { +// Update performs the auto-update. +func (u *Updater) Update() error { + u.mu.Lock() + defer u.mu.Unlock() + err := u.prepare() if err != nil { return err @@ -72,7 +116,7 @@ func (u *Updater) DoUpdate() error { defer u.clean() - err = u.downloadPackageFile(u.PackageURL, u.packageName) + err = u.downloadPackageFile(u.packageURL, u.packageName) if err != nil { return err } @@ -84,7 +128,6 @@ func (u *Updater) DoUpdate() error { err = u.check() if err != nil { - u.clean() return err } @@ -101,40 +144,57 @@ func (u *Updater) DoUpdate() error { return nil } -func (u *Updater) prepare() error { - u.updateDir = filepath.Join(u.WorkDir, fmt.Sprintf("agh-update-%s", u.NewVersion)) +// NewVersion returns the available new version. +func (u *Updater) NewVersion() (nv string) { + u.mu.RLock() + defer u.mu.RUnlock() - _, pkgNameOnly := filepath.Split(u.PackageURL) - if len(pkgNameOnly) == 0 { + return u.newVersion +} + +// VersionCheckURL returns the version check URL. +func (u *Updater) VersionCheckURL() (vcu string) { + u.mu.RLock() + defer u.mu.RUnlock() + + return u.versionCheckURL +} + +func (u *Updater) prepare() error { + u.updateDir = filepath.Join(u.workDir, fmt.Sprintf("agh-update-%s", u.newVersion)) + + _, pkgNameOnly := filepath.Split(u.packageURL) + if pkgNameOnly == "" { return fmt.Errorf("invalid PackageURL") } + u.packageName = filepath.Join(u.updateDir, pkgNameOnly) - u.backupDir = filepath.Join(u.WorkDir, "agh-backup") + u.backupDir = filepath.Join(u.workDir, "agh-backup") exeName := "AdGuardHome" - if u.OS == "windows" { + if u.goos == "windows" { exeName = "AdGuardHome.exe" } u.backupExeName = filepath.Join(u.backupDir, exeName) u.updateExeName = filepath.Join(u.updateDir, exeName) - log.Info("Updating from %s to %s. URL:%s", - u.VersionString, u.NewVersion, u.PackageURL) + log.Info("Updating from %s to %s. URL:%s", version.Version(), u.newVersion, u.packageURL) - // If the binary file isn't found in working directory, we won't be able to auto-update - // Getting the full path to the current binary file on UNIX and checking write permissions - // is more difficult. - u.currentExeName = filepath.Join(u.WorkDir, exeName) + // If the binary file isn't found in working directory, we won't be able + // to auto-update. Getting the full path to the current binary file on + // Unix and checking write permissions is more difficult. + u.currentExeName = filepath.Join(u.workDir, exeName) if !util.FileExists(u.currentExeName) { return fmt.Errorf("executable file %s doesn't exist", u.currentExeName) } + return nil } func (u *Updater) unpack() error { var err error - _, pkgNameOnly := filepath.Split(u.PackageURL) + _, pkgNameOnly := filepath.Split(u.packageURL) log.Debug("updater: unpacking the package") if strings.HasSuffix(pkgNameOnly, ".zip") { @@ -158,7 +218,7 @@ func (u *Updater) unpack() error { func (u *Updater) check() error { log.Debug("updater: checking configuration") - err := copyFile(u.ConfigName, filepath.Join(u.updateDir, "AdGuardHome.yaml")) + err := copyFile(u.confName, filepath.Join(u.updateDir, "AdGuardHome.yaml")) if err != nil { return fmt.Errorf("copyFile() failed: %w", err) } @@ -173,27 +233,25 @@ func (u *Updater) check() error { func (u *Updater) backup() error { log.Debug("updater: backing up the current configuration") _ = os.Mkdir(u.backupDir, 0o755) - err := copyFile(u.ConfigName, filepath.Join(u.backupDir, "AdGuardHome.yaml")) + err := copyFile(u.confName, filepath.Join(u.backupDir, "AdGuardHome.yaml")) if err != nil { return fmt.Errorf("copyFile() failed: %w", err) } - // workdir/README.md -> backup/README.md - err = copySupportingFiles(u.unpackedFiles, u.WorkDir, u.backupDir) + wd := u.workDir + err = copySupportingFiles(u.unpackedFiles, wd, u.backupDir) if err != nil { return fmt.Errorf("copySupportingFiles(%s, %s) failed: %s", - u.WorkDir, u.backupDir, err) + wd, u.backupDir, err) } return nil } func (u *Updater) replace() error { - // update/README.md -> workdir/README.md - err := copySupportingFiles(u.unpackedFiles, u.updateDir, u.WorkDir) + err := copySupportingFiles(u.unpackedFiles, u.updateDir, u.workDir) if err != nil { - return fmt.Errorf("copySupportingFiles(%s, %s) failed: %s", - u.updateDir, u.WorkDir, err) + return fmt.Errorf("copySupportingFiles(%s, %s) failed: %s", u.updateDir, u.workDir, err) } log.Debug("updater: renaming: %s -> %s", u.currentExeName, u.backupExeName) @@ -202,7 +260,7 @@ func (u *Updater) replace() error { return err } - if u.OS == "windows" { + if u.goos == "windows" { // rename fails with "File in use" error err = copyFile(u.updateExeName, u.currentExeName) } else { @@ -211,7 +269,9 @@ func (u *Updater) replace() error { if err != nil { return err } + log.Debug("updater: renamed: %s -> %s", u.updateExeName, u.currentExeName) + return nil } @@ -226,7 +286,7 @@ const MaxPackageFileSize = 32 * 1024 * 1024 // Download package file and save it to disk func (u *Updater) downloadPackageFile(url, filename string) error { - resp, err := u.Client.Get(url) + resp, err := u.client.Get(url) if err != nil { return fmt.Errorf("http request failed: %w", err) } @@ -288,13 +348,23 @@ func tarGzFileUnpack(tarfile, outdir string) ([]string, error) { } _, inputNameOnly := filepath.Split(header.Name) - if len(inputNameOnly) == 0 { + if inputNameOnly == "" { continue } outputName := filepath.Join(outdir, inputNameOnly) if header.Typeflag == tar.TypeDir { + if inputNameOnly == "AdGuardHome" { + // Top-level AdGuardHome/. Skip it. + // + // TODO(a.garipov): This whole package needs to + // be rewritten and covered in more integration + // tests. It has weird assumptions and file + // mode issues. + continue + } + err = os.Mkdir(outputName, os.FileMode(header.Mode&0o777)) if err != nil && !os.IsExist(err) { err2 = fmt.Errorf("os.Mkdir(%s): %w", outputName, err) @@ -355,13 +425,21 @@ func zipFileUnpack(zipfile, outdir string) ([]string, error) { fi := zf.FileInfo() inputNameOnly := fi.Name() - if len(inputNameOnly) == 0 { + if inputNameOnly == "" { continue } outputName := filepath.Join(outdir, inputNameOnly) if fi.IsDir() { + if inputNameOnly == "AdGuardHome" { + // Top-level AdGuardHome/. Skip it. + // + // TODO(a.garipov): See the similar todo in + // tarGzFileUnpack. + continue + } + err = os.Mkdir(outputName, fi.Mode()) if err != nil && !os.IsExist(err) { err2 = fmt.Errorf("os.Mkdir(): %w", err) diff --git a/internal/updater/updater_test.go b/internal/updater/updater_test.go new file mode 100644 index 00000000..197e2142 --- /dev/null +++ b/internal/updater/updater_test.go @@ -0,0 +1,323 @@ +package updater + +import ( + "io/ioutil" + "net" + "net/http" + "net/url" + "os" + "path" + "path/filepath" + "strconv" + "testing" + + "github.com/AdguardTeam/AdGuardHome/internal/aghtest" + "github.com/AdguardTeam/AdGuardHome/internal/version" + "github.com/stretchr/testify/assert" +) + +// TODO(a.garipov): Rewrite these tests. + +func TestMain(m *testing.M) { + aghtest.DiscardLogOutput(m) +} + +func startHTTPServer(data string) (l net.Listener, portStr string) { + mux := http.NewServeMux() + mux.HandleFunc("/", func(w http.ResponseWriter, _ *http.Request) { + _, _ = w.Write([]byte(data)) + }) + + listener, err := net.Listen("tcp", ":0") + if err != nil { + panic(err) + } + + go func() { _ = http.Serve(listener, mux) }() + return listener, strconv.FormatUint(uint64(listener.Addr().(*net.TCPAddr).Port), 10) +} + +func TestUpdateGetVersion(t *testing.T) { + const jsonData = `{ + "version": "v0.103.0-beta.2", + "announcement": "AdGuard Home v0.103.0-beta.2 is now available!", + "announcement_url": "https://github.com/AdguardTeam/AdGuardHome/internal/releases", + "selfupdate_min_version": "v0.0", + "download_windows_amd64": "https://static.adguard.com/adguardhome/beta/AdGuardHome_windows_amd64.zip", + "download_windows_386": "https://static.adguard.com/adguardhome/beta/AdGuardHome_windows_386.zip", + "download_darwin_amd64": "https://static.adguard.com/adguardhome/beta/AdGuardHome_darwin_amd64.zip", + "download_darwin_386": "https://static.adguard.com/adguardhome/beta/AdGuardHome_darwin_386.zip", + "download_linux_amd64": "https://static.adguard.com/adguardhome/beta/AdGuardHome_linux_amd64.tar.gz", + "download_linux_386": "https://static.adguard.com/adguardhome/beta/AdGuardHome_linux_386.tar.gz", + "download_linux_arm": "https://static.adguard.com/adguardhome/beta/AdGuardHome_linux_armv6.tar.gz", + "download_linux_armv5": "https://static.adguard.com/adguardhome/beta/AdGuardHome_linux_armv5.tar.gz", + "download_linux_armv6": "https://static.adguard.com/adguardhome/beta/AdGuardHome_linux_armv6.tar.gz", + "download_linux_armv7": "https://static.adguard.com/adguardhome/beta/AdGuardHome_linux_armv7.tar.gz", + "download_linux_arm64": "https://static.adguard.com/adguardhome/beta/AdGuardHome_linux_arm64.tar.gz", + "download_linux_mips": "https://static.adguard.com/adguardhome/beta/AdGuardHome_linux_mips_softfloat.tar.gz", + "download_linux_mipsle": "https://static.adguard.com/adguardhome/beta/AdGuardHome_linux_mipsle_softfloat.tar.gz", + "download_linux_mips64": "https://static.adguard.com/adguardhome/beta/AdGuardHome_linux_mips64_softfloat.tar.gz", + "download_linux_mips64le": "https://static.adguard.com/adguardhome/beta/AdGuardHome_linux_mips64le_softfloat.tar.gz", + "download_freebsd_386": "https://static.adguard.com/adguardhome/beta/AdGuardHome_freebsd_386.tar.gz", + "download_freebsd_amd64": "https://static.adguard.com/adguardhome/beta/AdGuardHome_freebsd_amd64.tar.gz", + "download_freebsd_arm": "https://static.adguard.com/adguardhome/beta/AdGuardHome_freebsd_armv6.tar.gz", + "download_freebsd_armv5": "https://static.adguard.com/adguardhome/beta/AdGuardHome_freebsd_armv5.tar.gz", + "download_freebsd_armv6": "https://static.adguard.com/adguardhome/beta/AdGuardHome_freebsd_armv6.tar.gz", + "download_freebsd_armv7": "https://static.adguard.com/adguardhome/beta/AdGuardHome_freebsd_armv7.tar.gz", + "download_freebsd_arm64": "https://static.adguard.com/adguardhome/beta/AdGuardHome_freebsd_arm64.tar.gz" +}` + + l, lport := startHTTPServer(jsonData) + t.Cleanup(func() { assert.Nil(t, l.Close()) }) + + u := NewUpdater(&Config{ + Client: &http.Client{}, + Version: "v0.103.0-beta.1", + Channel: version.ChannelBeta, + GOARCH: "arm", + GOOS: "linux", + }) + + fakeURL := &url.URL{ + Scheme: "http", + Host: net.JoinHostPort("127.0.0.1", lport), + Path: path.Join("adguardhome", version.ChannelBeta, "version.json"), + } + u.versionCheckURL = fakeURL.String() + + info, err := u.VersionInfo(false) + assert.Nil(t, err) + assert.Equal(t, "v0.103.0-beta.2", info.NewVersion) + assert.Equal(t, "AdGuard Home v0.103.0-beta.2 is now available!", info.Announcement) + assert.Equal(t, "https://github.com/AdguardTeam/AdGuardHome/internal/releases", info.AnnouncementURL) + assert.Equal(t, "v0.0", info.SelfUpdateMinVersion) + if assert.NotNil(t, info.CanAutoUpdate) { + assert.True(t, *info.CanAutoUpdate) + } + + // check cached + _, err = u.VersionInfo(false) + assert.Nil(t, err) +} + +func TestUpdate(t *testing.T) { + // TODO(a.garipov): Uncomment and remove the code below in Go 1.15. + // + // wd := t.TempDir() + wd, err := ioutil.TempDir("", "aghtest") + assert.Nil(t, err) + t.Cleanup(func() { assert.Nil(t, os.RemoveAll(wd)) }) + + assert.Nil(t, ioutil.WriteFile(filepath.Join(wd, "AdGuardHome"), []byte("AdGuardHome"), 0o755)) + assert.Nil(t, ioutil.WriteFile(filepath.Join(wd, "README.md"), []byte("README.md"), 0o644)) + assert.Nil(t, ioutil.WriteFile(filepath.Join(wd, "LICENSE.txt"), []byte("LICENSE.txt"), 0o644)) + assert.Nil(t, ioutil.WriteFile(filepath.Join(wd, "AdGuardHome.yaml"), []byte("AdGuardHome.yaml"), 0o644)) + + // start server for returning package file + pkgData, err := ioutil.ReadFile("testdata/AdGuardHome.tar.gz") + assert.Nil(t, err) + l, lport := startHTTPServer(string(pkgData)) + t.Cleanup(func() { assert.Nil(t, l.Close()) }) + + u := NewUpdater(&Config{ + Client: &http.Client{}, + Version: "v0.103.0", + }) + + fakeURL := &url.URL{ + Scheme: "http", + Host: net.JoinHostPort("127.0.0.1", lport), + Path: "AdGuardHome.tar.gz", + } + + u.workDir = wd + u.confName = filepath.Join(u.workDir, "AdGuardHome.yaml") + u.newVersion = "v0.103.1" + u.packageURL = fakeURL.String() + + assert.Nil(t, u.prepare()) + u.currentExeName = filepath.Join(wd, "AdGuardHome") + assert.Nil(t, u.downloadPackageFile(u.packageURL, u.packageName)) + assert.Nil(t, u.unpack()) + // assert.Nil(t, u.check()) + assert.Nil(t, u.backup()) + assert.Nil(t, u.replace()) + u.clean() + + // check backup files + d, err := ioutil.ReadFile(filepath.Join(wd, "agh-backup", "AdGuardHome.yaml")) + assert.Nil(t, err) + assert.Equal(t, "AdGuardHome.yaml", string(d)) + + d, err = ioutil.ReadFile(filepath.Join(wd, "agh-backup", "AdGuardHome")) + assert.Nil(t, err) + assert.Equal(t, "AdGuardHome", string(d)) + + // check updated files + d, err = ioutil.ReadFile(filepath.Join(wd, "AdGuardHome")) + assert.Nil(t, err) + assert.Equal(t, "1", string(d)) + + d, err = ioutil.ReadFile(filepath.Join(wd, "README.md")) + assert.Nil(t, err) + assert.Equal(t, "2", string(d)) + + d, err = ioutil.ReadFile(filepath.Join(wd, "LICENSE.txt")) + assert.Nil(t, err) + assert.Equal(t, "3", string(d)) + + d, err = ioutil.ReadFile(filepath.Join(wd, "AdGuardHome.yaml")) + assert.Nil(t, err) + assert.Equal(t, "AdGuardHome.yaml", string(d)) +} + +func TestUpdateWindows(t *testing.T) { + // TODO(a.garipov): Uncomment and remove the code below in Go 1.15. + // + // wd := t.TempDir() + wd, err := ioutil.TempDir("", "aghtest") + assert.Nil(t, err) + t.Cleanup(func() { assert.Nil(t, os.RemoveAll(wd)) }) + + assert.Nil(t, ioutil.WriteFile(filepath.Join(wd, "AdGuardHome.exe"), []byte("AdGuardHome.exe"), 0o755)) + assert.Nil(t, ioutil.WriteFile(filepath.Join(wd, "README.md"), []byte("README.md"), 0o644)) + assert.Nil(t, ioutil.WriteFile(filepath.Join(wd, "LICENSE.txt"), []byte("LICENSE.txt"), 0o644)) + assert.Nil(t, ioutil.WriteFile(filepath.Join(wd, "AdGuardHome.yaml"), []byte("AdGuardHome.yaml"), 0o644)) + + // start server for returning package file + pkgData, err := ioutil.ReadFile("testdata/AdGuardHome.zip") + assert.Nil(t, err) + + l, lport := startHTTPServer(string(pkgData)) + t.Cleanup(func() { assert.Nil(t, l.Close()) }) + + u := NewUpdater(&Config{ + Client: &http.Client{}, + GOOS: "windows", + Version: "v0.103.0", + }) + + fakeURL := &url.URL{ + Scheme: "http", + Host: net.JoinHostPort("127.0.0.1", lport), + Path: "AdGuardHome.zip", + } + + u.workDir = wd + u.confName = filepath.Join(u.workDir, "AdGuardHome.yaml") + u.newVersion = "v0.103.1" + u.packageURL = fakeURL.String() + + assert.Nil(t, u.prepare()) + u.currentExeName = filepath.Join(wd, "AdGuardHome.exe") + assert.Nil(t, u.downloadPackageFile(u.packageURL, u.packageName)) + assert.Nil(t, u.unpack()) + // assert.Nil(t, u.check()) + assert.Nil(t, u.backup()) + assert.Nil(t, u.replace()) + u.clean() + + // check backup files + d, err := ioutil.ReadFile(filepath.Join(wd, "agh-backup", "AdGuardHome.yaml")) + assert.Nil(t, err) + assert.Equal(t, "AdGuardHome.yaml", string(d)) + + d, err = ioutil.ReadFile(filepath.Join(wd, "agh-backup", "AdGuardHome.exe")) + assert.Nil(t, err) + assert.Equal(t, "AdGuardHome.exe", string(d)) + + // check updated files + d, err = ioutil.ReadFile(filepath.Join(wd, "AdGuardHome.exe")) + assert.Nil(t, err) + assert.Equal(t, "1", string(d)) + + d, err = ioutil.ReadFile(filepath.Join(wd, "README.md")) + assert.Nil(t, err) + assert.Equal(t, "2", string(d)) + + d, err = ioutil.ReadFile(filepath.Join(wd, "LICENSE.txt")) + assert.Nil(t, err) + assert.Equal(t, "3", string(d)) + + d, err = ioutil.ReadFile(filepath.Join(wd, "AdGuardHome.yaml")) + assert.Nil(t, err) + assert.Equal(t, "AdGuardHome.yaml", string(d)) +} + +func TestUpdater_VersionInto_ARM(t *testing.T) { + const jsonData = `{ + "version": "v0.103.0-beta.2", + "announcement": "AdGuard Home v0.103.0-beta.2 is now available!", + "announcement_url": "https://github.com/AdguardTeam/AdGuardHome/internal/releases", + "selfupdate_min_version": "v0.0", + "download_linux_armv7": "https://static.adguard.com/adguardhome/beta/AdGuardHome_linux_armv7.tar.gz" +}` + + l, lport := startHTTPServer(jsonData) + t.Cleanup(func() { assert.Nil(t, l.Close()) }) + + u := NewUpdater(&Config{ + Client: &http.Client{}, + Version: "v0.103.0-beta.1", + Channel: version.ChannelBeta, + GOARCH: "arm", + GOOS: "linux", + GOARM: "7", + }) + + fakeURL := &url.URL{ + Scheme: "http", + Host: net.JoinHostPort("127.0.0.1", lport), + Path: path.Join("adguardhome", version.ChannelBeta, "version.json"), + } + u.versionCheckURL = fakeURL.String() + + info, err := u.VersionInfo(false) + assert.Nil(t, err) + assert.Equal(t, "v0.103.0-beta.2", info.NewVersion) + assert.Equal(t, "AdGuard Home v0.103.0-beta.2 is now available!", info.Announcement) + assert.Equal(t, "https://github.com/AdguardTeam/AdGuardHome/internal/releases", info.AnnouncementURL) + assert.Equal(t, "v0.0", info.SelfUpdateMinVersion) + if assert.NotNil(t, info.CanAutoUpdate) { + assert.True(t, *info.CanAutoUpdate) + } +} + +func TestUpdater_VersionInto_MIPS(t *testing.T) { + const jsonData = `{ + "version": "v0.103.0-beta.2", + "announcement": "AdGuard Home v0.103.0-beta.2 is now available!", + "announcement_url": "https://github.com/AdguardTeam/AdGuardHome/internal/releases", + "selfupdate_min_version": "v0.0", + "download_linux_mips_softfloat": "https://static.adguard.com/adguardhome/beta/AdGuardHome_linux_mips_softfloat.tar.gz" +}` + + l, lport := startHTTPServer(jsonData) + t.Cleanup(func() { assert.Nil(t, l.Close()) }) + + u := NewUpdater(&Config{ + Client: &http.Client{}, + Version: "v0.103.0-beta.1", + Channel: version.ChannelBeta, + GOARCH: "mips", + GOOS: "linux", + GOMIPS: "softfloat", + }) + + fakeURL := &url.URL{ + Scheme: "http", + Host: net.JoinHostPort("127.0.0.1", lport), + Path: path.Join("adguardhome", version.ChannelBeta, "version.json"), + } + u.versionCheckURL = fakeURL.String() + + info, err := u.VersionInfo(false) + assert.Nil(t, err) + assert.Equal(t, "v0.103.0-beta.2", info.NewVersion) + assert.Equal(t, "AdGuard Home v0.103.0-beta.2 is now available!", info.Announcement) + assert.Equal(t, "https://github.com/AdguardTeam/AdGuardHome/internal/releases", info.AnnouncementURL) + assert.Equal(t, "v0.0", info.SelfUpdateMinVersion) + if assert.NotNil(t, info.CanAutoUpdate) { + assert.True(t, *info.CanAutoUpdate) + } +} diff --git a/internal/util/autohosts.go b/internal/util/autohosts.go index a9b4f752..ad7825d5 100644 --- a/internal/util/autohosts.go +++ b/internal/util/autohosts.go @@ -29,10 +29,12 @@ type AutoHosts struct { // TODO(a.garipov): Make better use of newtypes. Perhaps a custom map. tableReverse map[string][]string - hostsFn string // path to the main hosts-file - hostsDirs []string // paths to OS-specific directories with hosts-files - watcher *fsnotify.Watcher // file and directory watcher object - updateChan chan bool // signal for 'updateLoop' goroutine + hostsFn string // path to the main hosts-file + hostsDirs []string // paths to OS-specific directories with hosts-files + watcher *fsnotify.Watcher // file and directory watcher object + + // onlyWritesChan used to contain only writing events from watcher. + onlyWritesChan chan fsnotify.Event onChanged onChangedT // notification to other modules } @@ -54,7 +56,7 @@ func (a *AutoHosts) notify() { // hostsFn: Override default name for the hosts-file (optional) func (a *AutoHosts) Init(hostsFn string) { a.table = make(map[string][]net.IP) - a.updateChan = make(chan bool, 2) + a.onlyWritesChan = make(chan fsnotify.Event, 2) a.hostsFn = "/etc/hosts" if runtime.GOOS == "windows" { @@ -82,8 +84,7 @@ func (a *AutoHosts) Init(hostsFn string) { func (a *AutoHosts) Start() { log.Debug("Start AutoHosts module") - go a.updateLoop() - a.updateChan <- true + a.updateHosts() if a.watcher != nil { go a.watcherLoop() @@ -104,11 +105,10 @@ func (a *AutoHosts) Start() { // Close - close module func (a *AutoHosts) Close() { - a.updateChan <- false - close(a.updateChan) if a.watcher != nil { _ = a.watcher.Close() } + close(a.onlyWritesChan) } // Process returns the list of IP addresses for the hostname or nil if nothing @@ -273,20 +273,32 @@ func (a *AutoHosts) load(table map[string][]net.IP, tableRev map[string][]string } } +// onlyWrites is a filter for (*fsnotify.Watcher).Events. +func (a *AutoHosts) onlyWrites() { + for event := range a.watcher.Events { + if event.Op&fsnotify.Write == fsnotify.Write { + a.onlyWritesChan <- event + } + } +} + // Receive notifications from fsnotify package func (a *AutoHosts) watcherLoop() { + go a.onlyWrites() for { select { - case event, ok := <-a.watcher.Events: + case event, ok := <-a.onlyWritesChan: if !ok { return } + // Assume that we sometimes have the same event occurred + // several times. repeat := true for repeat { select { - case <-a.watcher.Events: - // Skip this duplicating event + case _, ok = <-a.onlyWritesChan: + repeat = ok default: repeat = false } @@ -294,12 +306,7 @@ func (a *AutoHosts) watcherLoop() { if event.Op&fsnotify.Write == fsnotify.Write { log.Debug("AutoHosts: modified: %s", event.Name) - select { - case a.updateChan <- true: - // sent a signal to 'updateLoop' goroutine - default: - // queue is full - } + a.updateHosts() } case err, ok := <-a.watcher.Errors: @@ -311,18 +318,6 @@ func (a *AutoHosts) watcherLoop() { } } -// updateLoop reads static hosts from system files. -func (a *AutoHosts) updateLoop() { - for ok := range a.updateChan { - if !ok { - log.Debug("Finished AutoHosts update loop") - return - } - - a.updateHosts() - } -} - // updateHosts - loads system hosts func (a *AutoHosts) updateHosts() { table := make(map[string][]net.IP) diff --git a/internal/util/autohosts_test.go b/internal/util/autohosts_test.go index 04911142..82e16da9 100644 --- a/internal/util/autohosts_test.go +++ b/internal/util/autohosts_test.go @@ -8,117 +8,165 @@ import ( "testing" "time" - "github.com/AdguardTeam/AdGuardHome/internal/testutil" + "github.com/AdguardTeam/AdGuardHome/internal/aghtest" "github.com/miekg/dns" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) func TestMain(m *testing.M) { - testutil.DiscardLogOutput(m) + aghtest.DiscardLogOutput(m) } -func prepareTestDir() string { - const dir = "./agh-test" - _ = os.RemoveAll(dir) - _ = os.MkdirAll(dir, 0o755) - return dir +func prepareTestFile(t *testing.T) (f *os.File) { + t.Helper() + + dir := aghtest.PrepareTestDir(t) + + f, err := ioutil.TempFile(dir, "") + require.Nil(t, err) + require.NotNil(t, f) + t.Cleanup(func() { + assert.Nil(t, f.Close()) + }) + + return f +} + +func assertWriting(t *testing.T, f *os.File, strs ...string) { + t.Helper() + + for _, str := range strs { + n, err := f.WriteString(str) + require.Nil(t, err) + assert.Equal(t, n, len(str)) + } } func TestAutoHostsResolution(t *testing.T) { - ah := AutoHosts{} + ah := &AutoHosts{} - dir := prepareTestDir() - defer func() { _ = os.RemoveAll(dir) }() - - f, _ := ioutil.TempFile(dir, "") - defer func() { _ = os.Remove(f.Name()) }() - defer f.Close() - - _, _ = f.WriteString(" 127.0.0.1 host localhost # comment \n") - _, _ = f.WriteString(" ::1 localhost#comment \n") + f := prepareTestFile(t) + assertWriting(t, f, + " 127.0.0.1 host localhost # comment \n", + " ::1 localhost#comment \n", + ) ah.Init(f.Name()) - // Existing host - ips := ah.Process("localhost", dns.TypeA) - assert.NotNil(t, ips) - assert.Equal(t, 1, len(ips)) - assert.Equal(t, net.ParseIP("127.0.0.1"), ips[0]) + t.Run("existing_host", func(t *testing.T) { + ips := ah.Process("localhost", dns.TypeA) + require.Len(t, ips, 1) + assert.Equal(t, net.IPv4(127, 0, 0, 1), ips[0]) + }) - // Unknown host - ips = ah.Process("newhost", dns.TypeA) - assert.Nil(t, ips) + t.Run("unknown_host", func(t *testing.T) { + ips := ah.Process("newhost", dns.TypeA) + assert.Nil(t, ips) - // Unknown host (comment) - ips = ah.Process("comment", dns.TypeA) - assert.Nil(t, ips) + // Comment. + ips = ah.Process("comment", dns.TypeA) + assert.Nil(t, ips) + }) - // Test hosts file - table := ah.List() - names, ok := table["127.0.0.1"] - assert.True(t, ok) - assert.Equal(t, []string{"host", "localhost"}, names) + t.Run("hosts_file", func(t *testing.T) { + names, ok := ah.List()["127.0.0.1"] + require.True(t, ok) + assert.Equal(t, []string{"host", "localhost"}, names) + }) - // Test PTR - a, _ := dns.ReverseAddr("127.0.0.1") - a = strings.TrimSuffix(a, ".") - hosts := ah.ProcessReverse(a, dns.TypePTR) - if assert.Len(t, hosts, 2) { - assert.Equal(t, hosts[0], "host") - } + t.Run("ptr", func(t *testing.T) { + testCases := []struct { + wantIP string + wantLen int + wantHost string + }{ + {wantIP: "127.0.0.1", wantLen: 2, wantHost: "host"}, + {wantIP: "::1", wantLen: 1, wantHost: "localhost"}, + } - a, _ = dns.ReverseAddr("::1") - a = strings.TrimSuffix(a, ".") - hosts = ah.ProcessReverse(a, dns.TypePTR) - if assert.Len(t, hosts, 1) { - assert.Equal(t, hosts[0], "localhost") - } + for _, tc := range testCases { + a, err := dns.ReverseAddr(tc.wantIP) + require.Nil(t, err) + + a = strings.TrimSuffix(a, ".") + hosts := ah.ProcessReverse(a, dns.TypePTR) + require.Len(t, hosts, tc.wantLen) + assert.Equal(t, tc.wantHost, hosts[0]) + } + }) } func TestAutoHostsFSNotify(t *testing.T) { - ah := AutoHosts{} + ah := &AutoHosts{} - dir := prepareTestDir() - defer func() { _ = os.RemoveAll(dir) }() + f := prepareTestFile(t) - f, _ := ioutil.TempFile(dir, "") - defer func() { _ = os.Remove(f.Name()) }() - defer f.Close() - - // Init - _, _ = f.WriteString(" 127.0.0.1 host localhost \n") + assertWriting(t, f, " 127.0.0.1 host localhost \n") ah.Init(f.Name()) - // Unknown host - ips := ah.Process("newhost", dns.TypeA) - assert.Nil(t, ips) + t.Run("unknown_host", func(t *testing.T) { + ips := ah.Process("newhost", dns.TypeA) + assert.Nil(t, ips) + }) - // Stat monitoring for changes + // Start monitoring for changes. ah.Start() - defer ah.Close() + t.Cleanup(ah.Close) - // Update file - _, _ = f.WriteString("127.0.0.2 newhost\n") - _ = f.Sync() + assertWriting(t, f, "127.0.0.2 newhost\n") + require.Nil(t, f.Sync()) - // wait until fsnotify has triggerred and processed the file-modification event + // Wait until fsnotify has triggerred and processed the + // file-modification event. time.Sleep(50 * time.Millisecond) - // Check if we are notified about changes - ips = ah.Process("newhost", dns.TypeA) - assert.NotNil(t, ips) - assert.Equal(t, 1, len(ips)) - assert.Equal(t, "127.0.0.2", ips[0].String()) + t.Run("notified", func(t *testing.T) { + ips := ah.Process("newhost", dns.TypeA) + assert.NotNil(t, ips) + require.Len(t, ips, 1) + assert.True(t, net.IP{127, 0, 0, 2}.Equal(ips[0])) + }) } -func TestIP(t *testing.T) { - assert.Equal(t, "127.0.0.1", DNSUnreverseAddr("1.0.0.127.in-addr.arpa").String()) - assert.Equal(t, "::abcd:1234", DNSUnreverseAddr("4.3.2.1.d.c.b.a.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.ip6.arpa").String()) - assert.Equal(t, "::abcd:1234", DNSUnreverseAddr("4.3.2.1.d.c.B.A.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.ip6.arpa").String()) +func TestDNSReverseAddr(t *testing.T) { + testCases := []struct { + name string + have string + want net.IP + }{{ + name: "good_ipv4", + have: "1.0.0.127.in-addr.arpa", + want: net.IP{127, 0, 0, 1}, + }, { + name: "good_ipv6", + have: "4.3.2.1.d.c.b.a.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.ip6.arpa", + want: net.ParseIP("::abcd:1234"), + }, { + name: "good_ipv6_case", + have: "4.3.2.1.d.c.B.A.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.ip6.arpa", + want: net.ParseIP("::abcd:1234"), + }, { + name: "bad_ipv4_dot", + have: "1.0.0.127.in-addr.arpa.", + }, { + name: "wrong_ipv4", + have: ".0.0.127.in-addr.arpa", + }, { + name: "wrong_ipv6", + have: ".3.2.1.d.c.b.a.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.ip6.arpa", + }, { + name: "bad_ipv6_dot", + have: "4.3.2.1.d.c.b.a.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0..ip6.arpa", + }, { + name: "bad_ipv6_space", + have: "4.3.2.1.d.c.b. .0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.ip6.arpa", + }} - assert.Nil(t, DNSUnreverseAddr("1.0.0.127.in-addr.arpa.")) - assert.Nil(t, DNSUnreverseAddr(".0.0.127.in-addr.arpa")) - assert.Nil(t, DNSUnreverseAddr(".3.2.1.d.c.b.a.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.ip6.arpa")) - assert.Nil(t, DNSUnreverseAddr("4.3.2.1.d.c.b.a.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0..ip6.arpa")) - assert.Nil(t, DNSUnreverseAddr("4.3.2.1.d.c.b. .0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.ip6.arpa")) + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + ip := DNSUnreverseAddr(tc.have) + assert.True(t, tc.want.Equal(ip)) + }) + } } diff --git a/internal/util/helpers.go b/internal/util/helpers.go index 2770fa44..1b759f45 100644 --- a/internal/util/helpers.go +++ b/internal/util/helpers.go @@ -5,10 +5,12 @@ package util import ( + "bytes" "fmt" "io/ioutil" "os" "os/exec" + "path/filepath" "runtime" "strings" ) @@ -26,7 +28,7 @@ func ContainsString(strs []string, str string) bool { // FileExists returns true if file exists. func FileExists(fn string) bool { _, err := os.Stat(fn) - return err == nil + return err == nil || !os.IsNotExist(err) } // RunCommand runs shell command. @@ -64,16 +66,43 @@ func SplitNext(str *string, splitBy byte) string { return strings.TrimSpace(s) } -// IsOpenWRT checks if OS is OpenWRT. +// IsOpenWRT returns true if host OS is OpenWRT. func IsOpenWRT() bool { if runtime.GOOS != "linux" { return false } - body, err := ioutil.ReadFile("/etc/os-release") + const etcDir = "/etc" + + // TODO(e.burkov): Take care of dealing with fs package after updating + // Go version to 1.16. + fileInfos, err := ioutil.ReadDir(etcDir) if err != nil { return false } - return strings.Contains(string(body), "OpenWrt") + // fNameSubstr is a part of a name of the desired file. + const fNameSubstr = "release" + osNameData := []byte("OpenWrt") + + for _, fileInfo := range fileInfos { + if fileInfo.IsDir() { + continue + } + + if !strings.Contains(fileInfo.Name(), fNameSubstr) { + continue + } + + body, err := ioutil.ReadFile(filepath.Join(etcDir, fileInfo.Name())) + if err != nil { + continue + } + + if bytes.Contains(body, osNameData) { + return true + } + } + + return false } diff --git a/internal/util/helpers_test.go b/internal/util/helpers_test.go index d5e90637..a09d97e6 100644 --- a/internal/util/helpers_test.go +++ b/internal/util/helpers_test.go @@ -4,11 +4,14 @@ import ( "testing" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) func TestSplitNext(t *testing.T) { s := " a,b , c " - assert.True(t, SplitNext(&s, ',') == "a") - assert.True(t, SplitNext(&s, ',') == "b") - assert.True(t, SplitNext(&s, ',') == "c" && len(s) == 0) + + assert.Equal(t, "a", SplitNext(&s, ',')) + assert.Equal(t, "b", SplitNext(&s, ',')) + assert.Equal(t, "c", SplitNext(&s, ',')) + require.Empty(t, s) } diff --git a/internal/util/network.go b/internal/util/network.go index 1731ed08..52507617 100644 --- a/internal/util/network.go +++ b/internal/util/network.go @@ -1,6 +1,7 @@ package util import ( + "encoding/json" "errors" "fmt" "net" @@ -13,14 +14,30 @@ import ( "github.com/AdguardTeam/golibs/log" ) -// NetInterface represents a list of network interfaces +// NetInterface represents an entry of network interfaces map. type NetInterface struct { - Name string // Network interface name - MTU int // MTU - HardwareAddr string // Hardware address - Addresses []string // Array with the network interface addresses - Subnets []string // Array with CIDR addresses of this network interface - Flags string // Network interface flags (up, broadcast, etc) + MTU int `json:"mtu"` + Name string `json:"name"` + HardwareAddr net.HardwareAddr `json:"hardware_address"` + Flags net.Flags `json:"flags"` + // Array with the network interface addresses. + Addresses []net.IP `json:"ip_addresses,omitempty"` + // Array with IP networks for this network interface. + Subnets []*net.IPNet `json:"-"` +} + +// MarshalJSON implements the json.Marshaler interface for *NetInterface. +func (iface *NetInterface) MarshalJSON() ([]byte, error) { + type netInterface NetInterface + return json.Marshal(&struct { + HardwareAddr string `json:"hardware_address"` + Flags string `json:"flags"` + *netInterface + }{ + HardwareAddr: iface.HardwareAddr.String(), + Flags: iface.Flags.String(), + netInterface: (*netInterface)(iface), + }) } // GetValidNetInterfaces returns interfaces that are eligible for DNS and/or DHCP @@ -40,7 +57,7 @@ func GetValidNetInterfaces() ([]net.Interface, error) { // GetValidNetInterfacesForWeb returns interfaces that are eligible for DNS and WEB only // we do not return link-local addresses here -func GetValidNetInterfacesForWeb() ([]NetInterface, error) { +func GetValidNetInterfacesForWeb() ([]*NetInterface, error) { ifaces, err := GetValidNetInterfaces() if err != nil { return nil, fmt.Errorf("couldn't get interfaces: %w", err) @@ -49,7 +66,7 @@ func GetValidNetInterfacesForWeb() ([]NetInterface, error) { return nil, errors.New("couldn't find any legible interface") } - var netInterfaces []NetInterface + var netInterfaces []*NetInterface for _, iface := range ifaces { addrs, err := iface.Addrs() @@ -57,32 +74,29 @@ func GetValidNetInterfacesForWeb() ([]NetInterface, error) { return nil, fmt.Errorf("failed to get addresses for interface %s: %w", iface.Name, err) } - netIface := NetInterface{ - Name: iface.Name, + netIface := &NetInterface{ MTU: iface.MTU, - HardwareAddr: iface.HardwareAddr.String(), + Name: iface.Name, + HardwareAddr: iface.HardwareAddr, + Flags: iface.Flags, } - if iface.Flags != 0 { - netIface.Flags = iface.Flags.String() - } - - // Collect network interface addresses + // Collect network interface addresses. for _, addr := range addrs { ipNet, ok := addr.(*net.IPNet) if !ok { - // not an IPNet, should not happen + // Should be net.IPNet, this is weird. return nil, fmt.Errorf("got iface.Addrs() element %s that is not net.IPNet, it is %T", addr, addr) } - // ignore link-local + // Ignore link-local. if ipNet.IP.IsLinkLocalUnicast() { continue } - netIface.Addresses = append(netIface.Addresses, ipNet.IP.String()) - netIface.Subnets = append(netIface.Subnets, ipNet.String()) + netIface.Addresses = append(netIface.Addresses, ipNet.IP) + netIface.Subnets = append(netIface.Subnets, ipNet) } - // Discard interfaces with no addresses + // Discard interfaces with no addresses. if len(netIface.Addresses) != 0 { netInterfaces = append(netInterfaces, netIface) } @@ -91,8 +105,8 @@ func GetValidNetInterfacesForWeb() ([]NetInterface, error) { return netInterfaces, nil } -// GetInterfaceByIP - Get interface name by its IP address. -func GetInterfaceByIP(ip string) string { +// GetInterfaceByIP returns the name of interface containing provided ip. +func GetInterfaceByIP(ip net.IP) string { ifaces, err := GetValidNetInterfacesForWeb() if err != nil { return "" @@ -100,7 +114,7 @@ func GetInterfaceByIP(ip string) string { for _, iface := range ifaces { for _, addr := range iface.Addresses { - if ip == addr { + if ip.Equal(addr) { return iface.Name } } @@ -109,13 +123,13 @@ func GetInterfaceByIP(ip string) string { return "" } -// GetSubnet - Get IP address with netmask for the specified interface -// Returns an empty string if it fails to find it -func GetSubnet(ifaceName string) string { +// GetSubnet returns pointer to net.IPNet for the specified interface or nil if +// the search fails. +func GetSubnet(ifaceName string) *net.IPNet { netIfaces, err := GetValidNetInterfacesForWeb() if err != nil { log.Error("Could not get network interfaces info: %v", err) - return "" + return nil } for _, netIface := range netIfaces { @@ -124,12 +138,12 @@ func GetSubnet(ifaceName string) string { } } - return "" + return nil } // CheckPortAvailable - check if TCP port is available -func CheckPortAvailable(host string, port int) error { - ln, err := net.Listen("tcp", net.JoinHostPort(host, strconv.Itoa(port))) +func CheckPortAvailable(host net.IP, port int) error { + ln, err := net.Listen("tcp", net.JoinHostPort(host.String(), strconv.Itoa(port))) if err != nil { return err } @@ -142,8 +156,8 @@ func CheckPortAvailable(host string, port int) error { } // CheckPacketPortAvailable - check if UDP port is available -func CheckPacketPortAvailable(host string, port int) error { - ln, err := net.ListenPacket("udp", net.JoinHostPort(host, strconv.Itoa(port))) +func CheckPacketPortAvailable(host net.IP, port int) error { + ln, err := net.ListenPacket("udp", net.JoinHostPort(host.String(), strconv.Itoa(port))) if err != nil { return err } diff --git a/internal/util/network_test.go b/internal/util/network_test.go index 9b2a9554..bcc1da4c 100644 --- a/internal/util/network_test.go +++ b/internal/util/network_test.go @@ -2,22 +2,15 @@ package util import ( "testing" + + "github.com/stretchr/testify/require" ) func TestGetValidNetInterfacesForWeb(t *testing.T) { ifaces, err := GetValidNetInterfacesForWeb() - if err != nil { - t.Fatalf("Cannot get net interfaces: %s", err) - } - if len(ifaces) == 0 { - t.Fatalf("No net interfaces found") - } - + require.Nilf(t, err, "Cannot get net interfaces: %s", err) + require.NotEmpty(t, ifaces, "No net interfaces found") for _, iface := range ifaces { - if len(iface.Addresses) == 0 { - t.Fatalf("No addresses found for %s", iface.Name) - } - - t.Logf("%v", iface) + require.NotEmptyf(t, iface.Addresses, "No addresses found for %s", iface.Name) } } diff --git a/internal/version/version.go b/internal/version/version.go new file mode 100644 index 00000000..df79a9c0 --- /dev/null +++ b/internal/version/version.go @@ -0,0 +1,61 @@ +// Package version contains AdGuard Home version information. +package version + +import ( + "fmt" + "runtime" +) + +// These are set by the linker. Unfortunately we cannot set constants during +// linking, and Go doesn't have a concept of immutable variables, so to be +// thorough we have to only export them through getters. +// +// TODO(a.garipov): Find out if we can get GOARM and GOMIPS values the same way +// we can GOARCH and GOOS. +var ( + channel string = ChannelDevelopment + goarm string + gomips string + version string +) + +// Channel constants. +const ( + ChannelDevelopment = "development" + ChannelEdge = "edge" + ChannelBeta = "beta" + ChannelRelease = "release" +) + +// Channel returns the current AdGuard Home release channel. +func Channel() (v string) { + return channel +} + +// Full returns the full current version of AdGuard Home. +func Full() (v string) { + msg := "AdGuard Home, version %s, channel %s, arch %s %s" + if goarm != "" { + msg = msg + " v" + goarm + } else if gomips != "" { + msg = msg + " " + gomips + } + + return fmt.Sprintf(msg, version, channel, runtime.GOOS, runtime.GOARCH) +} + +// GOARM returns the GOARM value used to build the current AdGuard Home release. +func GOARM() (v string) { + return goarm +} + +// GOMIPS returns the GOMIPS value used to build the current AdGuard Home +// release. +func GOMIPS() (v string) { + return gomips +} + +// Version returns the AdGuard Home build version. +func Version() (v string) { + return version +} diff --git a/main.go b/main.go index ecbb3d3b..cf69ed04 100644 --- a/main.go +++ b/main.go @@ -1,4 +1,3 @@ -//go:generate go install -v github.com/gobuffalo/packr/packr //go:generate packr clean //go:generate packr -z package main @@ -7,20 +6,6 @@ import ( "github.com/AdguardTeam/AdGuardHome/internal/home" ) -// version is the release version. It is set by the linker. -var version = "undefined" - -// channel is the release channel. It is set by the linker. -var channel = "release" - -// goarm is the GOARM value. It is set by the linker. -var goarm = "" - -// gomips is the GOMIPS value. It is set by the linker. -// -// TODO(a.garipov): Implement. -var gomips = "" - func main() { - home.Main(version, channel, goarm, gomips) + home.Main() } diff --git a/openapi/CHANGELOG.md b/openapi/CHANGELOG.md index 076d9896..7cbc626b 100644 --- a/openapi/CHANGELOG.md +++ b/openapi/CHANGELOG.md @@ -4,9 +4,20 @@ ## v0.105: API changes +### New `"client_id"` field in `GET /querylog` response + +* The new field `"client_id"` of `QueryLogItem` objects is the ID sent by the + client for encrypted requests, if there was any. See the + "[Identifying clients]" section of our wiki. + +### New `"dnscrypt"` `"client_proto"` value in `GET /querylog` response + +* The field `"client_proto"` can now have the value `"dnscrypt"` when the + request was sent over a DNSCrypt connection. + ### New `"reason"` in `GET /filtering/check_host` and `GET /querylog` -* The new `DNSRewriteRule` reason is added to `GET /filtering/check_host` and +* The new `RewriteRule` reason is added to `GET /filtering/check_host` and `GET /querylog`. * Also, the reason which was incorrectly documented as `"ReasonRewrite"` is now @@ -62,6 +73,10 @@ The old fields will be removed in v0.106.0. +As well as other documentation fixes. + +[Identifying clients]: https://github.com/AdguardTeam/AdGuardHome/wiki/Clients#idclient + ## v0.103: API changes ### API: replace settings in GET /control/dns_info & POST /control/dns_config diff --git a/openapi/openapi.yaml b/openapi/openapi.yaml index 61db836e..5892d704 100644 --- a/openapi/openapi.yaml +++ b/openapi/openapi.yaml @@ -103,6 +103,8 @@ server works, any other text means an error. 'content': 'application/json': + 'schema': + '$ref': '#/components/schemas/UpstreamsConfigResponse' 'examples': 'response': 'value': @@ -348,7 +350,26 @@ 'application/json': 'schema': '$ref': '#/components/schemas/DhcpStatus' - '501': + '500': + 'content': + 'application/json': + 'schema': + '$ref': '#/components/schemas/Error' + 'description': 'Not implemented (for example, on Windows).' + '/dhcp/interfaces': + 'get': + 'tags': + - 'dhcp' + 'operationId': 'dhcpInterfaces' + 'summary': 'Gets the available interfaces' + 'responses': + '200': + 'description': 'OK.' + 'content': + 'application/json': + 'schema': + '$ref': '#/components/schemas/NetInterfaces' + '500': 'content': 'application/json': 'schema': @@ -604,6 +625,11 @@ 'description': 'OK.' 'content': 'application/json': + 'schema': + 'type': 'object' + 'properties': + 'enabled': + 'type': 'boolean' 'examples': 'response': 'value': @@ -655,6 +681,13 @@ 'description': 'OK.' 'content': 'application/json': + 'schema': + 'type': 'object' + 'properties': + 'enable': + 'type': 'boolean' + 'sensitivity': + 'type': 'integer' 'examples': 'response': 'value': @@ -689,6 +722,11 @@ 'description': 'OK.' 'content': 'application/json': + 'schema': + 'type': 'object' + 'properties': + 'enabled': + 'type': 'boolean' 'examples': 'response': 'value': @@ -756,11 +794,17 @@ 'tags': - 'clients' 'operationId': 'clientsFind' - 'summary': 'Get information about selected clients by their IP address' + 'summary': > + Get information about clients by their IP addresses or client IDs. 'parameters': - 'name': 'ip0' 'in': 'query' - 'description': 'Filter by IP address' + 'description': > + Filter by IP address or client IDs. Parameters with names `ip1`, + `ip2`, and so on are also accepted and interpreted as "ip0 OR ip1 OR + ip2". + + TODO(a.garipov): Replace with a better query API. 'schema': 'type': 'string' 'responses': @@ -770,6 +814,39 @@ 'application/json': 'schema': '$ref': '#/components/schemas/ClientsFindResponse' + '/access/list': + 'get': + 'operationId': 'accessList' + 'responses': + '200': + 'description': 'OK.' + 'content': + 'application/json': + 'schema': + '$ref': '#/components/schemas/AccessListResponse' + 'summary': 'List (dis)allowed clients, blocked hosts, etc.' + 'tags': + - 'clients' + '/access/set': + 'post': + 'operationId': 'accessSet' + 'requestBody': + 'content': + 'application/json': + 'schema': + '$ref': '#/components/schemas/AccessSetRequest' + 'required': true + 'responses': + '200': + 'description': 'OK.' + '400': + 'description': > + Failed to parse JSON or cannot save the list. + '500': + 'description': 'Internal error.' + 'summary': 'Set (dis)allowed clients, blocked hosts, etc.' + 'tags': + - 'clients' '/blocked_services/list': 'get': 'tags': @@ -867,6 +944,20 @@ 'examples': 'response': 'value': 'en' + '/install/get_addresses_beta': + 'get': + 'tags': + - 'install' + 'operationId': 'installGetAddressesBeta' + 'summary': > + 'UNSTABLE!: Gets the network interfaces information.' + 'responses': + '200': + 'description': 'OK.' + 'content': + 'application/json': + 'schema': + '$ref': '#/components/schemas/AddressesInfoBeta' '/install/get_addresses': 'get': 'tags': @@ -880,6 +971,30 @@ 'application/json': 'schema': '$ref': '#/components/schemas/AddressesInfo' + '/install/check_config_beta': + 'post': + 'tags': + - 'install' + 'operationId': 'installCheckConfigBeta' + 'summary': > + 'UNSTABLE!: Checks configuration' + 'requestBody': + 'content': + 'application/json': + 'schema': + '$ref': '#/components/schemas/CheckConfigRequestBeta' + 'description': 'Configuration to be checked' + 'required': true + 'responses': + '200': + 'description': 'OK.' + 'content': + 'application/json': + 'schema': + '$ref': '#/components/schemas/CheckConfigResponse' + '400': + 'description': > + Failed to parse JSON or cannot listen on the specified address. '/install/check_config': 'post': 'tags': @@ -903,6 +1018,29 @@ '400': 'description': > Failed to parse JSON or cannot listen on the specified address. + '/install/configure_beta': + 'post': + 'tags': + - 'install' + 'operationId': 'installConfigureBeta' + 'summary': > + 'UNSTABLE!: Applies the initial configuration.' + 'requestBody': + 'content': + 'application/json': + 'schema': + '$ref': '#/components/schemas/InitialConfigurationBeta' + 'description': 'Initial configuration JSON' + 'required': true + 'responses': + '200': + 'description': 'OK.' + '400': + 'description': > + Failed to parse initial configuration or cannot listen to the + specified addresses. + '500': + 'description': 'Cannot start the DNS server' '/install/configure': 'post': 'tags': @@ -977,6 +1115,13 @@ 'name': 'host' 'schema': 'type': 'string' + - 'description': > + Client ID. + 'example': 'client-1' + 'in': 'query' + 'name': 'client_id' + 'schema': + 'type': 'string' 'responses': '200': 'description': 'DNS over HTTPS plist file.' @@ -1004,6 +1149,13 @@ 'name': 'host' 'schema': 'type': 'string' + - 'description': > + Client ID. + 'example': 'client-1' + 'in': 'query' + 'name': 'client_id' + 'schema': + 'type': 'string' 'responses': '200': 'description': 'DNS over TLS plist file' @@ -1044,31 +1196,35 @@ 'type': 'object' 'description': 'AdGuard Home server status and configuration' 'required': - - 'dns_address' + - 'dns_addresses' - 'dns_port' + - 'http_port' - 'protection_enabled' - - 'querylog_enabled' - 'running' - - 'bootstrap_dns' - - 'upstream_dns' - 'version' - 'language' 'properties': - 'dns_address': - 'type': 'string' - 'example': '127.0.0.1' + 'dns_addresses': + 'example': ['127.0.0.1'] + 'items': + 'type': 'string' + 'type': 'array' 'dns_port': 'type': 'integer' - 'format': 'int32' + 'format': 'uint16' 'example': 53 'minimum': 1 'maximum': 65535 + 'http_port': + 'type': 'integer' + 'format': 'uint16' + 'example': 80 + 'minimum': 1 + 'maximum': 65535 'protection_enabled': 'type': 'boolean' 'dhcp_available': 'type': 'boolean' - 'querylog_enabled': - 'type': 'boolean' 'running': 'type': 'boolean' 'version': @@ -1123,6 +1279,8 @@ 'type': 'string' 'edns_cs_enabled': 'type': 'boolean' + 'disable_ipv6': + 'type': 'boolean' 'dnssec_enabled': 'type': 'boolean' 'cache_size': @@ -1163,32 +1321,39 @@ 'example': - 'tls://1.1.1.1' - 'tls://1.0.0.1' + 'UpstreamsConfigResponse': + 'type': 'object' + 'description': 'Upstreams configuration response' + 'additionalProperties': + 'type': 'string' 'Filter': 'type': 'object' 'description': 'Filter subscription info' 'required': - 'enabled' - 'id' - - 'lastUpdated' + - 'last_updated' - 'name' - - 'rulesCount' + - 'rules_count' - 'url' 'properties': 'enabled': 'type': 'boolean' 'id': - 'type': 'integer' 'example': 1234 - 'lastUpdated': - 'type': 'string' - 'format': 'date-time' - 'example': '2018-10-30T12:18:57+03:00' - 'name': - 'type': 'string' - 'example': 'AdGuard Simplified Domain Names filter' - 'rulesCount': + 'format': 'int64' 'type': 'integer' + 'last_updated': + 'example': '2018-10-30T12:18:57+03:00' + 'format': 'date-time' + 'type': 'string' + 'name': + 'example': 'AdGuard Simplified Domain Names filter' + 'type': 'string' + 'rules_count': 'example': 5912 + 'format': 'uint32' + 'type': 'integer' 'url': 'type': 'string' 'example': > @@ -1259,7 +1424,7 @@ - 'FilteredBlockedService' - 'Rewrite' - 'RewriteEtcHosts' - - 'DNSRewriteRule' + - 'RewriteRule' 'filter_id': 'deprecated': true 'description': > @@ -1311,7 +1476,13 @@ 'type': 'object' 'description': > Information about the latest available version of AdGuard Home. + 'required': + - 'disabled' 'properties': + 'disabled': + 'type': 'boolean' + 'description': > + If true then other fields doesn't appear. 'new_version': 'type': 'string' 'example': 'v0.9' @@ -1330,7 +1501,10 @@ 'properties': 'time_units': 'type': 'string' - 'description': 'Time units (hours | days)' + 'enum': + - 'hours' + - 'days' + 'description': 'Time units' 'example': 'hours' 'num_dns_queries': 'type': 'integer' @@ -1392,6 +1566,8 @@ 'properties': 'domain_or_ip': 'type': 'integer' + 'additionalProperties': + 'type': 'integer' 'StatsConfig': 'type': 'object' 'description': 'Statistics configuration' @@ -1496,6 +1672,12 @@ 'type': 'array' 'items': '$ref': '#/components/schemas/DhcpStaticLease' + 'NetInterfaces': + 'type': 'object' + 'description': > + Network interfaces dictionary, keys are interface names. + 'additionalProperties': + '$ref': '#/components/schemas/NetInterface' 'DhcpSearchResult': 'type': 'object' @@ -1526,7 +1708,12 @@ 'properties': 'found': 'type': 'string' - 'description': 'yes|no|error' + 'enum': + - 'yes' + - 'no' + - 'error' + 'description': > + The result of searching the other DHCP server. 'example': 'no' 'error': 'type': 'string' @@ -1538,7 +1725,12 @@ 'properties': 'static': 'type': 'string' - 'description': 'yes|no|error' + 'enum': + - 'yes' + - 'no' + - 'error' + 'description': > + The result of determining static IP address. 'example': 'yes' 'ip': 'type': 'string' @@ -1550,8 +1742,9 @@ 'description': 'DNS answer section' 'properties': 'ttl': - 'type': 'integer' 'example': 55 + 'format': 'uint32' + 'type': 'integer' 'type': 'type': 'string' 'example': 'A' @@ -1613,13 +1806,21 @@ 'answer_dnssec': 'type': 'boolean' 'client': - 'type': 'string' + 'description': > + The client's IP address. 'example': '192.168.0.1' + 'type': 'string' + 'client_id': + 'description': > + The client ID, if provided in DOH, DOQ, or DOT. + 'example': 'cli123' + 'type': 'string' 'client_proto': 'enum': - 'dot' - 'doh' - 'doq' + - 'dnscrypt' - '' 'elapsedMs': 'type': 'string' @@ -1663,7 +1864,7 @@ - 'FilteredBlockedService' - 'Rewrite' - 'RewriteEtcHosts' - - 'DNSRewriteRule' + - 'RewriteRule' 'service_name': 'type': 'string' 'description': 'Set if reason=FilteredBlockedService' @@ -1733,17 +1934,17 @@ 'description': 'if true, forces HTTP->HTTPS redirect' 'port_https': 'type': 'integer' - 'format': 'int32' + 'format': 'uint16' 'example': 443 'description': 'HTTPS port. If 0, HTTPS will be disabled.' 'port_dns_over_tls': 'type': 'integer' - 'format': 'int32' + 'format': 'uint16' 'example': 853 'description': 'DNS-over-TLS port. If 0, DOT will be disabled.' 'port_dns_over_quic': 'type': 'integer' - 'format': 'int32' + 'format': 'uint16' 'example': 784 'description': 'DNS-over-QUIC port. If 0, DOQ will be disabled.' 'certificate_chain': @@ -1821,9 +2022,18 @@ 'NetInterface': 'type': 'object' 'description': 'Network interface info' + 'required': + - 'flags' + - 'hardware_address' + - 'name' + - 'mtu' 'properties': 'flags': 'type': 'string' + 'description': > + Flags could be any combination of the following values, divided by + the "|" character: "up", "broadcast", "loopback", "pointtopoint" and + "multicast". 'example': 'up|broadcast|multicast' 'hardware_address': 'type': 'string' @@ -1831,44 +2041,83 @@ 'name': 'type': 'string' 'example': 'eth0' - 'ipv4_addresses': + 'ip_addresses': 'type': 'array' 'items': 'type': 'string' - 'ipv6_addresses': + 'mtu': + 'type': 'integer' + 'AddressInfoBeta': + 'type': 'object' + 'description': 'Port information' + 'required': + - 'ip' + - 'port' + 'properties': + 'ip': 'type': 'array' 'items': 'type': 'string' - 'gateway_ip': - 'type': 'string' + 'minItems': 1 + 'example': + - '127.0.0.1' + 'port': + 'type': 'integer' + 'format': 'uint16' + 'example': 53 'AddressInfo': 'type': 'object' 'description': 'Port information' + 'required': + - 'ip' + - 'port' 'properties': 'ip': 'type': 'string' 'example': '127.0.0.1' 'port': 'type': 'integer' - 'format': 'int32' + 'format': 'uint16' 'example': 53 'AddressesInfo': 'type': 'object' 'description': 'AdGuard Home addresses configuration' + 'required': + - 'dns_port' + - 'web_port' + - 'interfaces' 'properties': 'dns_port': 'type': 'integer' - 'format': 'int32' + 'format': 'uint16' 'example': 53 'web_port': 'type': 'integer' - 'format': 'int32' + 'format': 'uint16' 'example': 80 'interfaces': - 'type': 'object' + '$ref': '#/components/schemas/NetInterfaces' + 'AddressesInfoBeta': + 'type': 'object' + 'description': 'AdGuard Home addresses configuration' + 'required': + - 'dns_port' + - 'web_port' + - 'interfaces' + 'properties': + 'dns_port': + 'type': 'integer' + 'format': 'uint16' + 'example': 53 + 'web_port': + 'type': 'integer' + 'format': 'uint16' + 'example': 80 + 'interfaces': + 'type': 'array' 'description': > Network interfaces dictionary, keys are interface names. - 'additionalProperties': + 'items': '$ref': '#/components/schemas/NetInterface' 'ProfileInfo': 'type': 'object' @@ -1878,7 +2127,7 @@ 'type': 'string' 'Client': 'type': 'object' - 'description': 'Client information' + 'description': 'Client information.' 'properties': 'name': 'type': 'string' @@ -1886,7 +2135,7 @@ 'example': 'localhost' 'ids': 'type': 'array' - 'description': 'IP, CIDR or MAC address' + 'description': 'IP, CIDR, MAC, or client ID.' 'items': 'type': 'string' 'use_global_settings': @@ -1941,18 +2190,68 @@ 'type': 'string' 'ClientsFindResponse': 'type': 'array' - 'description': 'Response to clients find operation' + 'description': 'Client search results.' 'items': '$ref': '#/components/schemas/ClientsFindEntry' + 'example': + - 'cli42': + 'name': 'Client 42' + 'ids': ['cli42'] + 'use_global_settings': true + 'filtering_enabled': true + 'parental_enabled': true + 'safebrowsing_enabled': true + 'safesearch_enabled': true + 'use_global_blocked_services': true + 'blocked_services': null + 'upstreams': null + 'whois_info': null + 'disallowed': false + 'disallowed_rule': '' + - '1.2.3.4': + 'name': 'Client 1-2-3-4' + 'ids': ['1.2.3.4'] + 'use_global_settings': true + 'filtering_enabled': true + 'parental_enabled': true + 'safebrowsing_enabled': true + 'safesearch_enabled': true + 'use_global_blocked_services': true + 'blocked_services': null + 'upstreams': null + 'whois_info': null + 'disallowed': false + 'disallowed_rule': '' + 'AccessListResponse': + '$ref': '#/components/schemas/AccessList' + 'AccessSetRequest': + '$ref': '#/components/schemas/AccessList' + 'AccessList': + 'description': 'Client and host access list' + 'properties': + 'allowed_clients': + 'description': 'Allowlist of clients.' + 'items': + 'type': 'string' + 'type': 'array' + 'disallowed_clients': + 'description': 'Blocklist of clients.' + 'items': + 'type': 'string' + 'type': 'array' + 'blocked_hosts': + 'description': 'Blocklist of hosts.' + 'items': + 'type': 'string' + 'type': 'array' + 'type': 'object' 'ClientsFindEntry': 'type': 'object' - 'properties': - '1.2.3.4': - 'items': - '$ref': '#/components/schemas/ClientFindSubEntry' - + 'additionalProperties': + '$ref': '#/components/schemas/ClientFindSubEntry' 'ClientFindSubEntry': 'type': 'object' + 'description': 'Client information.' 'properties': 'name': 'type': 'string' @@ -1960,7 +2259,7 @@ 'example': 'localhost' 'ids': 'type': 'array' - 'description': 'IP, CIDR or MAC address' + 'description': 'IP, CIDR, MAC, or client ID.' 'items': 'type': 'string' 'use_global_settings': @@ -1984,9 +2283,7 @@ 'items': 'type': 'string' 'whois_info': - 'type': 'array' - 'items': - '$ref': '#/components/schemas/WhoisInfo' + '$ref': '#/components/schemas/WhoisInfo' 'disallowed': 'type': 'boolean' 'description': > @@ -2001,9 +2298,8 @@ 'WhoisInfo': 'type': 'object' - 'properties': - 'key': - 'type': 'string' + 'additionalProperties': + 'type': 'string' 'Clients': 'type': 'object' @@ -2043,6 +2339,17 @@ 'type': 'array' 'items': 'type': 'string' + 'CheckConfigRequestBeta': + 'type': 'object' + 'description': 'Configuration to be checked' + 'properties': + 'dns': + '$ref': '#/components/schemas/CheckConfigRequestInfoBeta' + 'web': + '$ref': '#/components/schemas/CheckConfigRequestInfoBeta' + 'set_static_ip': + 'type': 'boolean' + 'example': false 'CheckConfigRequest': 'type': 'object' 'description': 'Configuration to be checked' @@ -2054,6 +2361,23 @@ 'set_static_ip': 'type': 'boolean' 'example': false + 'CheckConfigRequestInfoBeta': + 'type': 'object' + 'properties': + 'ip': + 'type': 'array' + 'items': + 'type': 'string' + 'minItems': 1 + 'example': + - '127.0.0.1' + 'port': + 'type': 'integer' + 'format': 'uint16' + 'example': 53 + 'autofix': + 'type': 'boolean' + 'example': false 'CheckConfigRequestInfo': 'type': 'object' 'properties': @@ -2062,13 +2386,17 @@ 'example': '127.0.0.1' 'port': 'type': 'integer' - 'format': 'int32' + 'format': 'uint16' 'example': 53 'autofix': 'type': 'boolean' 'example': false 'CheckConfigResponse': 'type': 'object' + 'required': + - 'dns' + - 'web' + - 'static_ip' 'properties': 'dns': '$ref': '#/components/schemas/CheckConfigResponseInfo' @@ -2078,32 +2406,69 @@ '$ref': '#/components/schemas/CheckConfigStaticIpInfo' 'CheckConfigResponseInfo': 'type': 'object' + 'required': + - 'status' + - 'can_autofix' 'properties': 'status': 'type': 'string' - 'example': '' + 'default': '' 'can_autofix': 'type': 'boolean' 'example': false + 'CheckConfigStaticIpInfoStatic': + 'type': 'string' + 'example': 'no' + 'enum': + - 'yes' + - 'no' + - 'error' + 'description': 'Can be: yes, no, error' 'CheckConfigStaticIpInfo': 'type': 'object' 'properties': 'static': - 'type': 'string' - 'example': 'no' - 'description': 'Can be: yes, no, error' + '$ref': '#/components/schemas/CheckConfigStaticIpInfoStatic' 'ip': 'type': 'string' + 'default': '' 'example': '192.168.1.1' 'description': 'Current dynamic IP address. Set if static=no' 'error': 'type': 'string' - 'example': '' + 'default': '' 'description': 'Error text. Set if static=error' + 'InitialConfigurationBeta': + 'type': 'object' + 'description': > + AdGuard Home initial configuration for the first-install wizard. + 'required': + - 'dns' + - 'web' + - 'username' + - 'password' + 'properties': + 'dns': + '$ref': '#/components/schemas/AddressInfoBeta' + 'web': + '$ref': '#/components/schemas/AddressInfoBeta' + 'username': + 'type': 'string' + 'description': 'Basic auth username' + 'example': 'admin' + 'password': + 'type': 'string' + 'description': 'Basic auth password' + 'example': 'password' 'InitialConfiguration': 'type': 'object' 'description': > AdGuard Home initial configuration for the first-install wizard. + 'required': + - 'dns' + - 'web' + - 'username' + - 'password' 'properties': 'dns': '$ref': '#/components/schemas/AddressInfo' @@ -2121,7 +2486,7 @@ 'type': 'object' 'description': 'Login request data' 'properties': - 'username': + 'name': 'type': 'string' 'description': 'User name' 'password': diff --git a/scripts/README.md b/scripts/README.md new file mode 100644 index 00000000..020a8444 --- /dev/null +++ b/scripts/README.md @@ -0,0 +1,161 @@ + # AdGuard Home Scripts + +## `hooks/`: Git Hooks + + ### Usage + +Run `make init` from the project root. + +## `querylog/`: Query Log Helpers + + ### Usage + + * `npm install`: install dependencies. Run this first. + * `npm run anonymize `: read the query log from the `` + and write anonymized version to ``. + +## `make/`: Makefile Scripts + +The release channels are: `development` (the default), `edge`, `beta`, and +`release`. If verbosity levels aren't documented here, there are only two: `0`, +don't print anything, and `1`, be verbose. + + ### `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 + +Required environment: + * `CHANNEL`: release channel, see above. + * `GPG_KEY` and `GPG_KEY_PASSPHRASE`: data for `gpg`. Only required if `SIGN` + is `1`. + +Optional environment: + * `DIST_DIR`: the directory to build a release into. The default value is + `dist`. + * `GO`: set an alternarive 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 + it has the default `Makefile` value of `v0.0.0`. + + ### `clean.sh`: Cleanup + +Optional environment: + * `GO`: set an alternarive name for the Go compiler. + +Required environment: + * `DIST_DIR`: the directory where a release has previously been built. + + ### `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 alternarive name for the Go compiler. + * `OUT`: output binary name. + * `PARALLELISM`: set the maximum number of concurrently run build commands + (that is, compiler, linker, etc.). + * `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. + +Required environment: + * `CHANNEL`: release channel, see above. + * `VERSION`: release version. + + ### `go-deps.sh`: Install Backend Dependencies + +Optional environment: + * `GO`: set an alternarive 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 + +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 alternarive 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 + +Optional environment: + * `GO`: set an alternarive 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 + +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 alternarive name for the Go compiler. + + ### `version.sh`: Print The Current Version + +Required environment: + * `CHANNEL`: release channel, see above. + +## `snap/`: Snap GUI Files + +App icons (see https://github.com/AdguardTeam/AdGuardHome/pull/1836), Snap +manifest file templates, and helper scripts. + +## `translations/`: Twosky Integration Script + + ### Usage + + * `npm install`: install dependencies. Run this first. + * `npm run locales:download`: download and save all translations. + * `npm run locales:upload`: upload the base `en` locale. + * `npm run locales:summary`: show the current locales summary. + * `npm run locales:unused`: show the list of unused strings. + +After the download you'll find the output locales in the `client/src/__locales/` +directory. + +## `whotracksme/`: Whotracks.me Database Converter + +A simple script that converts the Ghostery/Cliqz trackers database to a json format. + + ### Usage + +```sh +yarn install +node index.js +``` + +You'll find the output in the `whotracksmedb.json` file. Then, move it to +`client/src/helpers/trackers`. diff --git a/scripts/go-install-tools.sh b/scripts/go-install-tools.sh deleted file mode 100644 index e75a54e4..00000000 --- a/scripts/go-install-tools.sh +++ /dev/null @@ -1,21 +0,0 @@ -#!/bin/sh - -test "$VERBOSE" = '1' && set -x -set -e -f -u - -# TODO(a.garipov): Add goconst? - -env GOBIN="${PWD}/bin" "$GO" install --modfile=./internal/tools/go.mod\ - github.com/fzipp/gocyclo/cmd/gocyclo\ - github.com/golangci/misspell/cmd/misspell\ - github.com/gordonklaus/ineffassign\ - github.com/kisielk/errcheck\ - github.com/kyoh86/looppointer/cmd/looppointer\ - github.com/securego/gosec/v2/cmd/gosec\ - golang.org/x/lint/golint\ - golang.org/x/tools/go/analysis/passes/nilness/cmd/nilness\ - golang.org/x/tools/go/analysis/passes/shadow/cmd/shadow\ - honnef.co/go/tools/cmd/staticcheck\ - mvdan.cc/gofumpt\ - mvdan.cc/unparam\ - ; diff --git a/scripts/hooks/pre-commit b/scripts/hooks/pre-commit new file mode 100755 index 00000000..859f536f --- /dev/null +++ b/scripts/hooks/pre-commit @@ -0,0 +1,23 @@ +#!/bin/sh + +set -e -f -u + +if [ "$(git diff --cached --name-only -- 'client/*.js')" ] +then + make js-lint js-test +fi + +if [ "$(git diff --cached --name-only -- 'client2/*.js' 'client2/*.ts' 'client2/*.tsx')" ] +then + make js-beta-lint js-beta-test +fi + +if [ "$(git diff --cached --name-only -- '*.go' 'go.mod')" ] +then + make go-lint go-test +fi + +if [ "$(git diff --cached --name-only -- './openapi/openapi.yaml')" ] +then + make openapi-lint +fi diff --git a/scripts/install.sh b/scripts/install.sh index 6f1fa85f..7a410cee 100644 --- a/scripts/install.sh +++ b/scripts/install.sh @@ -1,6 +1,6 @@ #!/bin/sh -# AdGuardHome installation script +# AdGuard Home Installation Script # # 1. Download the package # 2. Unpack it @@ -97,7 +97,7 @@ detect_cpu() CPU=armv7 ;; - aarch64) + aarch64 | arm64) CPU=arm64 ;; @@ -184,12 +184,25 @@ main() { OS=$(detect_os) || error_exit "Cannot detect your OS" CPU=$(detect_cpu) || error_exit "Cannot detect your CPU" + + # TODO: Remove when Mac M1 native support is added + if [ "${OS}" = "darwin" ] && [ "${CPU}" = "arm64" ]; then + CPU="amd64" + log_info "Use ${CPU} build on Mac M1 until the native ARM support is added" + fi + PKG_EXT=$(package_extension) PKG_NAME=AdGuardHome_${OS}_${CPU}.${PKG_EXT} SCRIPT_URL="https://raw.githubusercontent.com/AdguardTeam/AdGuardHome/master/scripts/install.sh" URL="https://static.adguard.com/adguardhome/${CHANNEL}/${PKG_NAME}" - OUT_DIR=/opt + OUT_DIR="/opt" + if [ "${OS}" = "darwin" ]; then + # It may be important to install AdGuard Home to /Applications on MacOS + # Otherwise, it may not grant enough privileges to it + OUT_DIR="/Applications" + fi + AGH_DIR="${OUT_DIR}/AdGuardHome" # Root check @@ -215,10 +228,16 @@ main() { download "${URL}" "${PKG_NAME}" || error_exit "Cannot download the package" - unpack "${PKG_NAME}" "${OUT_DIR}" "${PKG_EXT}" || error_exit "Cannot unpack the package" + if [ "${OS}" = "darwin" ]; then + # TODO: remove this after v0.106.0 release + mkdir "${AGH_DIR}" + unpack "${PKG_NAME}" "${AGH_DIR}" "${PKG_EXT}" || error_exit "Cannot unpack the package" + else + unpack "${PKG_NAME}" "${OUT_DIR}" "${PKG_EXT}" || error_exit "Cannot unpack the package" + fi - # Install AdGuard Home service and run it - ${AGH_DIR}/AdGuardHome -s install || error_exit "Cannot install AdGuardHome as a service" + # Install AdGuard Home service and run it. + ( cd "${AGH_DIR}" && ./AdGuardHome -s install || error_exit "Cannot install AdGuardHome as a service" ) rm "${PKG_NAME}" @@ -227,4 +246,4 @@ main() { log_info " sudo ${AGH_DIR}/AdGuardHome -s start|stop|restart|status|install|uninstall" } -main "$@" \ No newline at end of file +main "$@" diff --git a/scripts/make/Dockerfile b/scripts/make/Dockerfile new file mode 100644 index 00000000..ce10b621 --- /dev/null +++ b/scripts/make/Dockerfile @@ -0,0 +1,57 @@ +# A docker file for scripts/make/build-docker.sh. + +FROM alpine:3.12 + +ARG BUILD_DATE +ARG VERSION +ARG VCS_REF +LABEL maintainer="AdGuard Team " \ + org.opencontainers.image.created=$BUILD_DATE \ + org.opencontainers.image.url="https://adguard.com/adguard-home.html" \ + org.opencontainers.image.source="https://github.com/AdguardTeam/AdGuardHome" \ + org.opencontainers.image.version=$VERSION \ + org.opencontainers.image.revision=$VCS_REF \ + org.opencontainers.image.vendor="AdGuard" \ + org.opencontainers.image.title="AdGuard Home" \ + org.opencontainers.image.description="Network-wide ads & trackers blocking DNS server" \ + org.opencontainers.image.licenses="GPL-3.0" + +# Update certificates. +RUN apk --no-cache --update add ca-certificates libcap && \ + rm -rf /var/cache/apk/* && \ + mkdir -p /opt/adguardhome/conf /opt/adguardhome/work && \ + chown -R nobody: /opt/adguardhome + +ARG DIST_DIR +ARG TARGETARCH +ARG TARGETOS +ARG TARGETVARIANT + +COPY --chown=nobody:nogroup\ + ./${DIST_DIR}/docker/AdGuardHome_${TARGETOS}_${TARGETARCH}_${TARGETVARIANT}\ + /opt/adguardhome/AdGuardHome + +RUN setcap 'cap_net_bind_service=+eip' /opt/adguardhome/AdGuardHome + +# 53 : DNS +# 67, 68 : DHCP +# 80 : HTTP +# 443 : HTTPS, DNS-over-HTTPS, DNSCrypt +# 784 : DNS-over-QUIC +# 853 : DNS-over-TLS +# 3000 : HTTP alt +# 3001 : HTTP beta +# 5443 : DNSCrypt alt +EXPOSE 53/tcp 53/udp 67/udp 68/udp 80/tcp 443/tcp 443/udp 784/udp\ + 853/tcp 3000/tcp 3001/tcp 5443/tcp 5443/udp + +WORKDIR /opt/adguardhome/work + +ENTRYPOINT ["/opt/adguardhome/AdGuardHome"] + +CMD [ \ + "--no-check-update", \ + "-c", "/opt/adguardhome/conf/AdGuardHome.yaml", \ + "-h", "0.0.0.0", \ + "-w", "/opt/adguardhome/work" \ +] diff --git a/scripts/make/build-docker.sh b/scripts/make/build-docker.sh new file mode 100644 index 00000000..92adb4fb --- /dev/null +++ b/scripts/make/build-docker.sh @@ -0,0 +1,109 @@ +#!/bin/sh + +verbose="${VERBOSE:-0}" + +if [ "$verbose" -gt '0' ] +then + set -x + debug_flags='-D' +else + set +x + debug_flags='' +fi + +set -e -f -u + +# Require these to be set. The channel value is validated later. +readonly channel="$CHANNEL" +readonly commit="$COMMIT" +readonly dist_dir="$DIST_DIR" + +if [ "${VERSION:-}" = 'v0.0.0' -o "${VERSION:-}" = '' ] +then + readonly version="$(sh ./scripts/make/version.sh)" +else + readonly version="$VERSION" +fi + +echo $version + +# Allow users to use sudo. +readonly sudo_cmd="${SUDO:-}" + +readonly docker_platforms="\ +linux/386,\ +linux/amd64,\ +linux/arm/v6,\ +linux/arm/v7,\ +linux/arm64,\ +linux/ppc64le" + +readonly build_date="$(date -u +'%Y-%m-%dT%H:%M:%SZ')" + +# Set DOCKER_IMAGE_NAME to 'adguard/adguard-home' if you want (and are +# allowed) to push to DockerHub. +readonly docker_image_name="${DOCKER_IMAGE_NAME:-adguardhome-dev}" + +# Set DOCKER_OUTPUT to 'type=image,name=adguard/adguard-home,push=true' +# if you want (and are allowed) to push to DockerHub. +readonly docker_output="${DOCKER_OUTPUT:-type=image,name=${docker_image_name},push=false}" + +case "$channel" +in +('release') + readonly docker_image_full_name="${docker_image_name}:${version}" + readonly docker_tags="--tag ${docker_image_name}:latest" + ;; +('beta') + readonly docker_image_full_name="${docker_image_name}:${version}" + readonly docker_tags="--tag ${docker_image_name}:beta" + ;; +('edge') + # Don't set the version tag when pushing to the edge channel. + readonly docker_image_full_name="${docker_image_name}:edge" + readonly docker_tags='' + ;; +('development') + readonly docker_image_full_name="${docker_image_name}" + readonly docker_tags='' + ;; +(*) + echo "invalid channel '$channel', supported values are\ + 'development', 'edge', 'beta', and 'release'" 1>&2 + exit 1 + ;; +esac + +# Copy the binaries into a new directory under new names, so that it's +# eaiser to COPY them later. DO NOT remove the trailing underscores. +# See scripts/make/Dockerfile. +readonly dist_docker="${dist_dir}/docker" +mkdir -p "$dist_docker" +cp "${dist_dir}/AdGuardHome_linux_386/AdGuardHome/AdGuardHome"\ + "${dist_docker}/AdGuardHome_linux_386_" +cp "${dist_dir}/AdGuardHome_linux_amd64/AdGuardHome/AdGuardHome"\ + "${dist_docker}/AdGuardHome_linux_amd64_" +cp "${dist_dir}/AdGuardHome_linux_arm64/AdGuardHome/AdGuardHome"\ + "${dist_docker}/AdGuardHome_linux_arm64_" +cp "${dist_dir}/AdGuardHome_linux_arm_6/AdGuardHome/AdGuardHome"\ + "${dist_docker}/AdGuardHome_linux_arm_v6" +cp "${dist_dir}/AdGuardHome_linux_arm_7/AdGuardHome/AdGuardHome"\ + "${dist_docker}/AdGuardHome_linux_arm_v7" +cp "${dist_dir}/AdGuardHome_linux_ppc64le/AdGuardHome/AdGuardHome"\ + "${dist_docker}/AdGuardHome_linux_ppc64le_" + +# 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\ + buildx build\ + --build-arg BUILD_DATE="$build_date"\ + --build-arg DIST_DIR="$dist_dir"\ + --build-arg VCS_REF="$commit"\ + --build-arg VERSION="$version"\ + --output "$docker_output"\ + --platform "$docker_platforms"\ + $docker_tags\ + -t "$docker_image_full_name"\ + -f ./scripts/make/Dockerfile\ + . diff --git a/scripts/make/build-release.sh b/scripts/make/build-release.sh new file mode 100644 index 00000000..2fc3944f --- /dev/null +++ b/scripts/make/build-release.sh @@ -0,0 +1,369 @@ +#!/bin/sh + +# AdGuard Home Release Script +# +# The commentary in this file is written with the assumption that the +# reader only has superficial knowledge of the POSIX shell language and +# alike. Experienced readers may find it overly verbose. + +# The default verbosity level is 0. Show every command that is run if +# the caller requested verbosity level greater than 0. Show the +# environment if the callre requested verbosity level greater than 1. +# Otherwise, print nothing. +# +# The level of verbosity for the build script is the same minus one +# level. See below in build(). +readonly verbose="${VERBOSE:-0}" +if [ "$verbose" -gt '1' ] +then + env + set -x +elif [ "$verbose" -gt '0' ] +then + set -x +fi + +# By default, sign the packages, but allow users to skip that step. +readonly sign="${SIGN:-1}" + +# Exit the script if a pipeline fails (-e), prevent accidental filename +# expansion (-f), and consider undefined variables as errors (-u). +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>&2 + fi +} + +log 'starting to build AdGuard Home release' + +# Require the channel to be set. Additional validation is performed +# later by go-build.sh. +readonly channel="$CHANNEL" + +# Check VERSION against the default value from the Makefile. If it is +# that, use the version calculation script. +if [ "${VERSION:-}" = 'v0.0.0' -o "${VERSION:-}" = '' ] +then + readonly version="$(sh ./scripts/make/version.sh)" +else + readonly version="$VERSION" +fi + +log "channel '$channel'" +log "version '$version'" + +# Require the gpg key and passphrase to be set if the signing is +# required. +if [ "$sign" = '1' ] +then + readonly gpg_key_passphrase="$GPG_KEY_PASSPHRASE" + readonly gpg_key="$GPG_KEY" +fi + +# The default distribution files directory is dist. +readonly dist="${DIST_DIR:-dist}" + +# Give users the ability to override the go command from environment. +# For example, to build two releases with two different Go versions and +# test the difference. +readonly go="${GO:-go}" + +log "checking tools" + +# Make sure we fail gracefully if one of the tools we need is missing. +for tool in gpg gzip sed sha256sum snapcraft tar zip +do + which "$tool" >/dev/null ||\ + { log "pieces don't fit, '$tool' not found"; exit 1; } +done + +# Data section. Arrange data into space-separated tables for read -r to +# read. Use 0 for missing values. + +readonly arms='5 +6 +7' + +readonly mipses='softfloat' + +# TODO(a.garipov): Remove armv6, because it was always overwritten by +# armv7. Rename armv7 to armhf. Rename the 386 snap to i386. + +# os arch arm mips snap +readonly platforms="\ +darwin 386 0 0 0 +darwin amd64 0 0 0 +freebsd 386 0 0 0 +freebsd amd64 0 0 0 +freebsd arm 5 0 0 +freebsd arm 6 0 0 +freebsd arm 7 0 0 +freebsd arm64 0 0 0 +linux 386 0 0 386 +linux amd64 0 0 amd64 +linux arm 5 0 0 +linux arm 6 0 armv6 +linux arm 7 0 armv7 +linux arm64 0 0 arm64 +linux mips 0 softfloat 0 +linux mips64 0 softfloat 0 +linux mips64le 0 softfloat 0 +linux mipsle 0 softfloat 0 +linux ppc64le 0 0 0 +windows 386 0 0 0 +windows amd64 0 0 0" + +# Function build builds the release for one platform. It builds +# a binary, an archive and, if needed, a snap package. +build() { + # Get the arguments. Here and below, use the "build_" prefix + # for all variables local to function build. + build_dir="${dist}/${1}/AdGuardHome"\ + build_ar="$2"\ + build_os="$3"\ + build_arch="$4"\ + build_arm="$5"\ + build_mips="$6"\ + build_snap="$7"\ + ; + + # Use the ".exe" filename extension if we build a Windows + # release. + if [ "$build_os" = 'windows' ] + then + build_output="./${build_dir}/AdGuardHome.exe" + else + build_output="./${build_dir}/AdGuardHome" + fi + + mkdir -p "./${build_dir}" + + # Build the binary. + # + # Set GOARM and GOMIPS to an empty string if $build_arm and + # $build_mips are zero by removing the zero as if it's a prefix. + # + # Don't use quotes with $build_par because we want an empty + # space if parallelism wasn't set. + env\ + GOARCH="$build_arch"\ + GOARM="${build_arm#0}"\ + GOMIPS="${build_mips#0}"\ + GOOS="$os"\ + VERBOSE="$(( verbose - 1 ))"\ + VERSION="$version"\ + OUT="$build_output"\ + sh ./scripts/make/go-build.sh\ + ; + + log "$build_output" + + if [ "$sign" = '1' ] + then + gpg\ + --default-key "$gpg_key"\ + --detach-sig\ + --passphrase "$gpg_key_passphrase"\ + --pinentry-mode loopback\ + -q\ + "$build_output"\ + ; + fi + + # Prepare the build directory for archiving. + cp ./CHANGELOG.md ./LICENSE.txt ./README.md "$build_dir" + + # Make archives. Windows and macOS prefer ZIP archives; the + # rest, gzipped tarballs. + case "$build_os" + in + ('darwin'|'windows') + build_archive="${PWD}/${dist}/${build_ar}.zip" + ( cd "${dist}/${1}" && zip -9 -q -r "$build_archive" "./AdGuardHome" ) + ;; + (*) + build_archive="./${dist}/${build_ar}.tar.gz" + tar -C "./${dist}/${1}" -c -f - "./AdGuardHome"\ + | gzip -9 - >"$build_archive" + ;; + esac + + log "$build_archive" + + if [ "$build_snap" = '0' ] + then + return + fi + + # Prepare 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/" + + # TODO(a.garipov): Remove this crutch later. + case "$build_snap" + in + ('386') + build_snap_arch="i386" + ;; + ('armv6'|'armv7') + build_snap_arch="armhf" + ;; + (*) + build_snap_arch="$build_snap" + ;; + esac + + # Create a snap.yaml file, setting the values. + sed -e 's/%VERSION%/'"$version"'/'\ + -e 's/%ARCH%/'"$build_snap_arch"'/'\ + ./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" != '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 +do + case "$arch" + in + (arm) + dir="AdGuardHome_${os}_${arch}_${arm}" + ar="AdGuardHome_${os}_${arch}v${arm}" + ;; + (mips*) + dir="AdGuardHome_${os}_${arch}_${mips}" + ar="$dir" + ;; + (*) + dir="AdGuardHome_${os}_${arch}" + ar="$dir" + ;; + esac + + build "$dir" "$ar" "$os" "$arch" "$arm" "$mips" "$snap" +done + +log "calculating checksums" + +# Calculate the checksums of the files in a subshell with file expansion +# enabled (+f) so that we don't need to use find or basename. +( + set +f + + cd "./${dist}" + + # Don't use quotes to get word splitting. + sha256sum $(ls -1 -A -q *.tar.gz *.zip) > ./checksums.txt +) + +log "writing versions" + +echo "version=$version" > "./${dist}/version.txt" + +# Create the verison.json file. +# +# TODO(a.garipov): Perhaps rewrite this as a go run program. Dealing +# with structured documents is really not a Shell's job. + +readonly version_download_url="https://static.adguard.com/adguardhome/${channel}" +readonly version_json="./${dist}/version.json" + +# Point users to the master branch if the channel is edge. +if [ "$channel" = 'edge' ] +then + readonly version_history_url='https://github.com/AdguardTeam/AdGuardHome/commits/master' +else + readonly version_history_url='https://github.com/AdguardTeam/AdGuardHome/releases' +fi + +rm -f "$version_json" +echo "{ + \"version\": \"${version}\", + \"announcement\": \"AdGuard Home ${version} is now available!\", + \"announcement_url\": \"${version_history_url}\", + \"selfupdate_min_version\": \"0.0\", +" >> "$version_json" + +# Add the old object keys for compatibility with pre-v0.105.0 MIPS that +# did not mention the softfloat variant. +# +# TODO(a.garipov): Remove this around the time we hit v0.107.0. +echo " + \"download_linux_mips\": \"${version_download_url}/AdGuardHome_linux_mips_softfloat.tar.gz\", + \"download_linux_mipsle\": \"${version_download_url}/AdGuardHome_linux_mipsle_softfloat.tar.gz\", + \"download_linux_mips64\": \"${version_download_url}/AdGuardHome_linux_mips64_softfloat.tar.gz\", + \"download_linux_mips64le\": \"${version_download_url}/AdGuardHome_linux_mips64le_softfloat.tar.gz\", +" >> "$version_json" + +( + # Use +f here so that ls works and we don't need to use find. + set +f + + readonly ar_files="$(ls -1 -A -q "./${dist}/"*.tar.gz "./${dist}/"*.zip)" + readonly ar_files_len="$(echo "$ar_files" | wc -l)" + + i='1' + # Don't use quotes to get word splitting. + for f in $ar_files + do + platform="$f" + + # Remove the prefix. + platform="${platform#./${dist}/AdGuardHome_}" + + # Remove the filename extensions. + platform="${platform%.zip}" + platform="${platform%.tar.gz}" + + # Use the filename's base path. + filename="${f#./${dist}/}" + + if [ "$i" = "$ar_files_len" ] + then + echo " \"download_${platform}\": \"${version_download_url}/${filename}\"" >> "$version_json" + else + echo " \"download_${platform}\": \"${version_download_url}/${filename}\"," >> "$version_json" + fi + + i="$(( i + 1 ))" + done +) + +echo '}' >> "$version_json" + +log "finished" diff --git a/scripts/make/clean.sh b/scripts/make/clean.sh new file mode 100644 index 00000000..a366e79c --- /dev/null +++ b/scripts/make/clean.sh @@ -0,0 +1,33 @@ +#!/bin/sh + +verbose="${VERBOSE:-0}" + +if [ "$verbose" -gt '0' ] +then + set -x +fi + +set -e -f -u + +dist_dir="$DIST_DIR" +go="${GO:-go}" + +# Set the GOPATH explicitly in case make clean is called from under sudo +# after a Docker build. +env PATH="$("$go" env GOPATH)/bin":"$PATH" packr clean + +rm -f\ + ./AdGuardHome\ + ./AdGuardHome.exe\ + ./coverage.txt\ + ; + +rm -f -r\ + ./bin/\ + ./build/\ + ./build2/\ + ./client/node_modules/\ + ./client2/node_modules/\ + ./data/\ + "./${dist_dir}/"\ + ; diff --git a/scripts/make/go-build.sh b/scripts/make/go-build.sh new file mode 100644 index 00000000..f8520f21 --- /dev/null +++ b/scripts/make/go-build.sh @@ -0,0 +1,99 @@ +#!/bin/sh + +# AdGuard Home Build Script +# +# The commentary in this file is written with the assumption that the +# reader only has superficial knowledge of the POSIX shell language and +# alike. Experienced readers may find it overly verbose. + +# The default verbosity level is 0. Show every command that is run and +# every package that is processed if the caller requested verbosity +# level greater than 0. Also show subcommands if the requested +# verbosity level is greater than 1. Otherwise, do nothing. +verbose="${VERBOSE:-0}" +if [ "$verbose" -gt '1' ] +then + env + set -x + readonly v_flags='-v' + readonly x_flags='-x' +elif [ "$verbose" -gt '0' ] +then + set -x + readonly v_flags='-v' + readonly x_flags='' +else + set +x + readonly v_flags='' + readonly x_flags='' +fi + +# Exit the script if a pipeline fails (-e), prevent accidental filename +# expansion (-f), and consider undefined variables as errors (-u). +set -e -f -u + +# Allow users to set the Go version. +go="${GO:-go}" + +# Require the channel to be set and validate the value. +channel="$CHANNEL" +case "$channel" +in +('development'|'edge'|'beta'|'release') + # All is well, go on. + ;; +(*) + echo "invalid channel '$channel', supported values are\ + 'development', 'edge', 'beta', and 'release'" 1>&2 + exit 1 + ;; +esac + +# Require the version to be set. +# +# TODO(a.garipov): Additional validation? +version="$VERSION" + +# Set the linker flags accordingly: set the release channel and the +# current version as well as goarm and gomips variable values, if the +# variables are set and are not empty. +readonly version_pkg='github.com/AdguardTeam/AdGuardHome/internal/version' +ldflags="-s -w -X ${version_pkg}.version=${version}" +ldflags="${ldflags} -X ${version_pkg}.channel=${channel}" +if [ "${GOARM:-}" != '' ] +then + ldflags="${ldflags} -X ${version_pkg}.goarm=${GOARM}" +elif [ "${GOMIPS:-}" != '' ] +then + ldflags="${ldflags} -X ${version_pkg}.gomips=${GOMIPS}" +fi + +# Allow users to limit the build's parallelism. +readonly parallelism="${PARALLELISM:-}" +if [ "$parallelism" != '' ] +then + readonly par_flags="-p ${parallelism}" +else + readonly par_flags='' +fi + +# Allow users to specify a different output name. +readonly out="${OUT:-}" +if [ "$out" != '' ] +then + readonly out_flags="-o ${out}" +else + readonly out_flags='' +fi + +# Don't use cgo. Use modules. +export CGO_ENABLED='0' GO111MODULE='on' + +readonly build_flags="${BUILD_FLAGS:-$out_flags $par_flags\ + $v_flags $x_flags}" + +# Don't use quotes with flag variables to get word splitting. +"$go" generate $v_flags $x_flags ./... + +# Don't use quotes with flag variables to get word splitting. +"$go" build --ldflags "$ldflags" $build_flags diff --git a/scripts/make/go-deps.sh b/scripts/make/go-deps.sh new file mode 100644 index 00000000..f3c8a77b --- /dev/null +++ b/scripts/make/go-deps.sh @@ -0,0 +1,39 @@ +#!/bin/sh + +verbose="${VERBOSE:-0}" + +if [ "$verbose" -gt '1' ] +then + env + set -x + readonly v_flags='-v' + readonly x_flags='-x' +elif [ "$verbose" -gt '0' ] +then + set -x + readonly v_flags='-v' + readonly x_flags='' +else + set +x + readonly v_flags='' + readonly x_flags='' +fi + +set -e -f -u + +go="${GO:-go}" + +# Don't use quotes with flag variables because we want an empty space if +# those aren't set. +"$go" mod download $x_flags + +# Reset GOARCH and GOOS to make sure we install the tools for the native +# architecture even when we're cross-compiling the main binary, and also +# to prevent the "cannot install cross-compiled binaries when GOBIN is +# set" error. +env\ + GOARCH=""\ + GOOS=""\ + GOBIN="${PWD}/bin"\ + "$go" install $v_flags $x_flags\ + github.com/gobuffalo/packr/packr diff --git a/scripts/go-lint.sh b/scripts/make/go-lint.sh similarity index 66% rename from scripts/go-lint.sh rename to scripts/make/go-lint.sh index ca30e1e7..30ed6141 100644 --- a/scripts/go-lint.sh +++ b/scripts/make/go-lint.sh @@ -1,23 +1,37 @@ #!/bin/sh +verbose="${VERBOSE:-0}" + # Verbosity levels: # 0 = Don't print anything except for errors. # 1 = Print commands, but not nested commands. # 2 = Print everything. -test "${VERBOSE:=0}" -gt '0' && set -x +if [ "$verbose" -gt '0' ] +then + set -x +fi -# Set $EXITONERROR to zero to see all errors. -test "${EXITONERROR:=1}" = '0' && set +e || set -e +# Set $EXIT_ON_ERROR to zero to see all errors. +if [ "${EXIT_ON_ERROR:-1}" = '0' ] +then + set +e +else + set -e +fi # We don't need glob expansions and we want to see errors about unset # variables. set -f -u + + +# Deferred Helpers + not_found_msg=' looks like a binary not found error. make sure you have installed the linter binaries using: - $ make go-install-tools + $ make go-tools ' not_found() { @@ -33,6 +47,10 @@ not_found() { } trap not_found EXIT + + +# Simple Analyzers + # blocklist_imports is a simple check against unwanted packages. # Currently it only looks for package log which is replaced by our own # package github.com/AdguardTeam/golibs/log. @@ -40,6 +58,12 @@ blocklist_imports() { git grep -F -e '"log"' -- '*.go' || exit 0; } +# method_const is a simple check against the usage of some raw strings +# and numbers where one should use named constants. +method_const() { + git grep -F -e '"GET"' -e '"POST"' -- '*.go' || exit 0; +} + # underscores is a simple check against Go filenames with underscores. underscores() { git ls-files '*_*.go' | { grep -F -e '_darwin.go' \ @@ -48,16 +72,30 @@ underscores() { -v || exit 0; } } + + +# Helpers + # exit_on_output exits with a nonzero exit code if there is anything in # the command's combined output. -exit_on_output() { - test "$VERBOSE" -lt '2' && set +x +exit_on_output() ( + set +e + + if [ "$VERBOSE" -lt '2' ] + then + set +x + fi cmd="$1" shift - exitcode='0' output="$("$cmd" "$@" 2>&1)" + exitcode="$?" + if [ "$exitcode" != '0' ] + then + echo "'$cmd' failed with code $exitcode" + fi + if [ "$output" != '' ] then if [ "$*" != '' ] @@ -69,16 +107,23 @@ exit_on_output() { echo "$output" - exitcode='1' + if [ "$exitcode" = '0' ] + then + exitcode='1' + fi fi - test "$VERBOSE" -gt '0' && set -x - return "$exitcode" -} +) + + + +# Checks exit_on_output blocklist_imports +exit_on_output method_const + exit_on_output underscores exit_on_output gofumpt --extra -l -s . @@ -87,7 +132,7 @@ golint --set_exit_status ./... "$GO" vet ./... -gocyclo --over 20 . +gocyclo --over 19 . gosec --quiet . @@ -95,7 +140,9 @@ ineffassign . unparam ./... -git ls-files -- '*.go' '*.md' '*.yaml' '*.yml' | xargs misspell --error +git ls-files -- '*.go' '*.md' '*.mod' '*.sh' '*.yaml' '*.yml'\ + 'Makefile'\ + | xargs misspell --error looppointer ./... @@ -105,11 +152,13 @@ nilness ./... # shadow --strict ./... # TODO(a.garipov): Enable errcheck fully after handling all errors, -# including the deferred ones, properly. Also, perhaps, enable --blank. +# including the deferred and generated ones, properly. Also, perhaps, +# enable --blank. +# # errcheck ./... exit_on_output sh -c ' - errcheck --asserts ./... |\ - { grep -e "defer" -e "_test\.go:" -v || exit 0; } + errcheck --asserts --ignoregenerated ./... |\ + { grep -e "defer" -v || exit 0; } ' staticcheck ./... diff --git a/scripts/make/go-test.sh b/scripts/make/go-test.sh new file mode 100644 index 00000000..0654157e --- /dev/null +++ b/scripts/make/go-test.sh @@ -0,0 +1,43 @@ +#!/bin/sh + +verbose="${VERBOSE:-0}" + +# Verbosity levels: +# 0 = Don't print anything except for errors. +# 1 = Print commands, but not nested commands. +# 2 = Print everything. +if [ "$verbose" -gt '1' ] +then + set -x + v_flags='-v' + x_flags='-x' +elif [ "$verbose" -gt '0' ] +then + set -x + v_flags='-v' + x_flags='' +else + set +x + v_flags='' + x_flags='' +fi + +set -e -f -u + +race="${RACE:-1}" +if [ "$race" = '0' ] +then + race_flags='' +else + race_flags='--race' +fi + +readonly go="${GO:-go}" +readonly timeout_flags="${TIMEOUT_FLAGS:---timeout 30s}" +readonly cover_flags='--coverprofile ./coverage.txt' +readonly count_flags='--count 1' + +# Don't use quotes with flag variables because we want an empty space if +# those aren't set. +"$go" test $count_flags $cover_flags $race_flags $timeout_flags\ + $x_flags $v_flags ./... diff --git a/scripts/make/go-tools.sh b/scripts/make/go-tools.sh new file mode 100644 index 00000000..32e30c08 --- /dev/null +++ b/scripts/make/go-tools.sh @@ -0,0 +1,49 @@ +#!/bin/sh + +verbose="${VERBOSE:-0}" + +if [ "$verbose" -gt '1' ] +then + set -x + readonly v_flags='-v' + readonly x_flags='-x' +elif [ "$verbose" -gt '0' ] +then + set -x + readonly v_flags='-v' + readonly x_flags='' +else + set +x + readonly v_flags='' + readonly x_flags='' +fi + +set -e -f -u + +go="${GO:-go}" + +# TODO(a.garipov): Add goconst? + +# Reset GOARCH and GOOS to make sure we install the tools for the native +# architecture even when we're cross-compiling the main binary, and also +# to prevent the "cannot install cross-compiled binaries when GOBIN is +# set" error. +env\ + GOARCH=""\ + GOOS=""\ + GOBIN="${PWD}/bin"\ + "$go" install --modfile=./internal/tools/go.mod\ + $v_flags $x_flags\ + github.com/fzipp/gocyclo/cmd/gocyclo\ + github.com/golangci/misspell/cmd/misspell\ + github.com/gordonklaus/ineffassign\ + github.com/kisielk/errcheck\ + github.com/kyoh86/looppointer/cmd/looppointer\ + github.com/securego/gosec/v2/cmd/gosec\ + golang.org/x/lint/golint\ + golang.org/x/tools/go/analysis/passes/nilness/cmd/nilness\ + golang.org/x/tools/go/analysis/passes/shadow/cmd/shadow\ + honnef.co/go/tools/cmd/staticcheck\ + mvdan.cc/gofumpt\ + mvdan.cc/unparam\ + ; diff --git a/scripts/make/version.sh b/scripts/make/version.sh new file mode 100644 index 00000000..08f9b482 --- /dev/null +++ b/scripts/make/version.sh @@ -0,0 +1,57 @@ +#!/bin/sh + +readonly verbose="${VERBOSE:-0}" +if [ "$verbose" -gt '1' ] +then + set -x +fi + +set -e -f -u + +readonly awk_program='/^v[0-9]+\.[0-9]+\.[0-9]+.*$/ { + if (!$4) { + # The last tag is a full release version, so bump the + # minor one to get the next one. + $2++; + } + + print($1 "." $2 "." $3); + + next; +} + +{ + printf("invalid version: \"%s\"\n", $0); + + exit 1; +}' + +readonly last_tag="$(git describe --abbrev=0)" +readonly current_desc="$(git describe)" + +readonly channel="$CHANNEL" +case "$channel" +in +('development') + echo 'v0.0.0' + ;; +('edge') + next=$(echo $last_tag | awk -F '[.+-]' "$awk_program") + echo "${next}-SNAPSHOT-$(git rev-parse --short HEAD)" + ;; +('beta'|'release') + if [ "$current_desc" != "$last_tag" ] + then + echo 'need a tag' 1>&2 + + exit 1 + fi + + echo "$last_tag" + ;; +(*) + echo "invalid channel '$channel', supported values are\ + 'development', 'edge', 'beta', and 'release'" 1>&2 + exit 1 + ;; +esac diff --git a/scripts/querylog/README.md b/scripts/querylog/README.md deleted file mode 100644 index 219d9581..00000000 --- a/scripts/querylog/README.md +++ /dev/null @@ -1,10 +0,0 @@ -# Helper tools to work with the Query log - -### Usage - -- `npm install` - Install the dependencies -- `npm run anonymize ` - Reads querylog from the `` and writes anonymized version to `` - -### Examples - -- `npm run anonymize test/querylog.json test/anonquerylog.json` - anonymizes the `test/querylog.json`. \ No newline at end of file diff --git a/scripts/snap/README.md b/scripts/snap/README.md deleted file mode 100644 index 6997dfee..00000000 --- a/scripts/snap/README.md +++ /dev/null @@ -1,6 +0,0 @@ -# Snap GUI - -These files are added to the AdGuard Home snap in order to add an app icon: -https://github.com/AdguardTeam/AdGuardHome/pull/1836 - -See .goreleaser.yml: snapcrafts.extra_files \ No newline at end of file diff --git a/scripts/snap/snap.tmpl.yaml b/scripts/snap/snap.tmpl.yaml new file mode 100644 index 00000000..1dff31f6 --- /dev/null +++ b/scripts/snap/snap.tmpl.yaml @@ -0,0 +1,37 @@ +# The %VARIABLES% are be replaced by actual values by the build script. + +'name': 'adguard-home' +'base': 'core20' +'version': '%VERSION%' +'summary': Network-wide ads & trackers blocking DNS server +'description': | + AdGuard Home is a network-wide software for blocking ads & tracking. After + you set it up, it'll cover ALL your home devices, and you don't need any + client-side software for that. + + It operates as a DNS server that re-routes tracking domains to a "black hole," + thus preventing your devices from connecting to those servers. It's based + on software we use for our public AdGuard DNS servers -- both share a lot + of common code. +'grade': 'stable' +'confinement': 'strict' + +'architectures': +- '%ARCH%' + +'apps': + 'adguard-home': + 'command': 'AdGuardHome --no-check-update -w $SNAP_DATA' + 'plugs': + # Add the "netrwork-bind" plug to bind to interfaces. + - 'network-bind' + # Add the "netrwork-observe" plug to be able to bind to ports below 1024 + # (cap_net_bind_service) and also to bind to a particular interface using + # SO_BINDTODEVICE (cap_net_raw). + - 'network-observe' + 'daemon': 'simple' + 'restart-condition': 'always' + 'adguard-home-web': + 'command': 'adguard-home-web.sh' + 'plugs': + - 'desktop' diff --git a/scripts/translations/README.md b/scripts/translations/README.md deleted file mode 100644 index 3a5e336c..00000000 --- a/scripts/translations/README.md +++ /dev/null @@ -1,11 +0,0 @@ -## Twosky integration script - -### Usage - -- `npm install` Install the dependencies in the local node_modules folder -- `npm run locales:download` - Download and save all translations -- `npm run locales:upload` - Upload base `en` locale -- `npm run locales:summary` - Shows the current locales summary -- `npm run locales:unused` - Shows a list of unused strings - -After download you'll find the output locales in the `client/src/__locales/` folder. diff --git a/scripts/translations/download.js b/scripts/translations/download.js index ef176c8d..d3db8e12 100644 --- a/scripts/translations/download.js +++ b/scripts/translations/download.js @@ -80,10 +80,18 @@ const request = (url, locale) => ( return `${locale} - Not OK`; })); +/** + * Sleep. + * @param {number} ms + */ +const sleep = (ms) => new Promise((resolve) => { + setTimeout(resolve, ms); +}); + /** * Download locales */ -const download = () => { +const download = async () => { const locales = LOCALES_LIST; if (!TWOSKY_URI) { @@ -91,10 +99,16 @@ const download = () => { return; } - const requests = locales.map((locale) => { + const requests = []; + for (let i = 0; i < locales.length; i++) { + const locale = locales[i]; const url = getRequestUrl(locale, TWOSKY_URI, TWOSKY_PROJECT_ID); - return request(url, locale); - }); + requests.push(request(url, locale)); + + // Don't request the Crowdin API too aggressively to prevent spurious + // 400 errors. + await sleep(200); + } Promise .all(requests) diff --git a/scripts/translations/package-lock.json b/scripts/translations/package-lock.json index 5361d859..42f5d23a 100644 --- a/scripts/translations/package-lock.json +++ b/scripts/translations/package-lock.json @@ -1,8 +1,466 @@ { "name": "translations", - "version": "0.1.0", - "lockfileVersion": 1, + "version": "0.2.0", + "lockfileVersion": 2, "requires": true, + "packages": { + "": { + "version": "0.2.0", + "dependencies": { + "request": "^2.88.0", + "request-promise": "^4.2.2" + } + }, + "node_modules/ajv": { + "version": "6.5.5", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.5.5.tgz", + "integrity": "sha512-7q7gtRQDJSyuEHjuVgHoUa2VuemFiCMrfQc9Tc08XTAc4Zj/5U1buQJ0HU6i7fKjXU09SVgSmxa4sLvuvS8Iyg==", + "dependencies": { + "fast-deep-equal": "^2.0.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + } + }, + "node_modules/asn1": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.4.tgz", + "integrity": "sha512-jxwzQpLQjSmWXgwaCZE9Nz+glAG01yF1QnWgbhGwHI5A6FRIEY6IVqtHhIepHqI7/kyEyQEagBC5mBEFlIYvdg==", + "dependencies": { + "safer-buffer": "~2.1.0" + } + }, + "node_modules/assert-plus": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", + "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=", + "engines": { + "node": ">=0.8" + } + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k=" + }, + "node_modules/aws-sign2": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.7.0.tgz", + "integrity": "sha1-tG6JCTSpWR8tL2+G1+ap8bP+dqg=", + "engines": { + "node": "*" + } + }, + "node_modules/aws4": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.8.0.tgz", + "integrity": "sha512-ReZxvNHIOv88FlT7rxcXIIC0fPt4KZqZbOlivyWtXLt8ESx84zd3kMC6iK5jVeS2qt+g7ftS7ye4fi06X5rtRQ==" + }, + "node_modules/bcrypt-pbkdf": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz", + "integrity": "sha1-pDAdOJtqQ/m2f/PKEaP2Y342Dp4=", + "dependencies": { + "tweetnacl": "^0.14.3" + } + }, + "node_modules/bluebird": { + "version": "3.5.3", + "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.5.3.tgz", + "integrity": "sha512-/qKPUQlaW1OyR51WeCPBvRnAlnZFUJkCSG5HzGnuIqhgyJtF+T94lFnn33eiazjRm2LAHVy2guNnaq48X9SJuw==" + }, + "node_modules/caseless": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz", + "integrity": "sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw=" + }, + "node_modules/combined-stream": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.7.tgz", + "integrity": "sha512-brWl9y6vOB1xYPZcpZde3N9zDByXTosAeMDo4p1wzo6UMOX4vumB+TP1RZ76sfE6Md68Q0NJSrE/gbezd4Ul+w==", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/core-util-is": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", + "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=" + }, + "node_modules/dashdash": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz", + "integrity": "sha1-hTz6D3y+L+1d4gMmuN1YEDX24vA=", + "dependencies": { + "assert-plus": "^1.0.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk=", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/ecc-jsbn": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz", + "integrity": "sha1-OoOpBOVDUyh4dMVkt1SThoSamMk=", + "dependencies": { + "jsbn": "~0.1.0", + "safer-buffer": "^2.1.0" + } + }, + "node_modules/extend": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==" + }, + "node_modules/extsprintf": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.3.0.tgz", + "integrity": "sha1-lpGEQOMEGnpBT4xS48V06zw+HgU=", + "engines": [ + "node >=0.6.0" + ] + }, + "node_modules/fast-deep-equal": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-2.0.1.tgz", + "integrity": "sha1-ewUhjd+WZ79/Nwv3/bLLFf3Qqkk=" + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.0.0.tgz", + "integrity": "sha1-1RQsDK7msRifh9OnYREGT4bIu/I=" + }, + "node_modules/forever-agent": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz", + "integrity": "sha1-+8cfDEGt6zf5bFd60e1C2P2sypE=", + "engines": { + "node": "*" + } + }, + "node_modules/form-data": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.3.3.tgz", + "integrity": "sha512-1lLKB2Mu3aGP1Q/2eCOx0fNbRMe7XdwktwOruhfqqd0rIJWwN4Dh+E3hrPSlDCXnSR7UtZ1N38rVXm+6+MEhJQ==", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.6", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 0.12" + } + }, + "node_modules/getpass": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/getpass/-/getpass-0.1.7.tgz", + "integrity": "sha1-Xv+OPmhNVprkyysSgmBOi6YhSfo=", + "dependencies": { + "assert-plus": "^1.0.0" + } + }, + "node_modules/har-schema": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/har-schema/-/har-schema-2.0.0.tgz", + "integrity": "sha1-qUwiJOvKwEeCoNkDVSHyRzW37JI=", + "engines": { + "node": ">=4" + } + }, + "node_modules/har-validator": { + "version": "5.1.3", + "resolved": "https://registry.npmjs.org/har-validator/-/har-validator-5.1.3.tgz", + "integrity": "sha512-sNvOCzEQNr/qrvJgc3UG/kD4QtlHycrzwS+6mfTrrSq97BvaYcPZZI1ZSqGSPR73Cxn4LKTD4PttRwfU7jWq5g==", + "dependencies": { + "ajv": "^6.5.5", + "har-schema": "^2.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/http-signature": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.2.0.tgz", + "integrity": "sha1-muzZJRFHcvPZW2WmCruPfBj7rOE=", + "dependencies": { + "assert-plus": "^1.0.0", + "jsprim": "^1.2.2", + "sshpk": "^1.7.0" + }, + "engines": { + "node": ">=0.8", + "npm": ">=1.3.7" + } + }, + "node_modules/is-typedarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", + "integrity": "sha1-5HnICFjfDBsR3dppQPlgEfzaSpo=" + }, + "node_modules/isstream": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz", + "integrity": "sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo=" + }, + "node_modules/jsbn": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz", + "integrity": "sha1-peZUwuWi3rXyAdls77yoDA7y9RM=" + }, + "node_modules/json-schema": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.2.3.tgz", + "integrity": "sha1-tIDIkuWaLwWVTOcnvT8qTogvnhM=" + }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==" + }, + "node_modules/json-stringify-safe": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", + "integrity": "sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus=" + }, + "node_modules/jsprim": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.4.1.tgz", + "integrity": "sha1-MT5mvB5cwG5Di8G3SZwuXFastqI=", + "engines": [ + "node >=0.6.0" + ], + "dependencies": { + "assert-plus": "1.0.0", + "extsprintf": "1.3.0", + "json-schema": "0.2.3", + "verror": "1.10.0" + } + }, + "node_modules/lodash": { + "version": "4.17.20", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.20.tgz", + "integrity": "sha512-PlhdFcillOINfeV7Ni6oF1TAEayyZBoZ8bcshTHqOYJYlrqzRK5hagpagky5o4HfCzzd1TRkXPMFq6cKk9rGmA==" + }, + "node_modules/mime-db": { + "version": "1.37.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.37.0.tgz", + "integrity": "sha512-R3C4db6bgQhlIhPU48fUtdVmKnflq+hRdad7IyKhtFj06VPNVdk2RhiYL3UjQIlso8L+YxAtFkobT0VK+S/ybg==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.21", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.21.tgz", + "integrity": "sha512-3iL6DbwpyLzjR3xHSFNFeb9Nz/M8WDkX33t1GFQnFOllWk8pOrh/LSrB5OXlnlW5P9LH73X6loW/eogc+F5lJg==", + "dependencies": { + "mime-db": "~1.37.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/oauth-sign": { + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.9.0.tgz", + "integrity": "sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ==", + "engines": { + "node": "*" + } + }, + "node_modules/performance-now": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz", + "integrity": "sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns=" + }, + "node_modules/psl": { + "version": "1.1.29", + "resolved": "https://registry.npmjs.org/psl/-/psl-1.1.29.tgz", + "integrity": "sha512-AeUmQ0oLN02flVHXWh9sSJF7mcdFq0ppid/JkErufc3hGIV/AMa8Fo9VgDo/cT2jFdOWoFvHp90qqBH54W+gjQ==" + }, + "node_modules/punycode": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz", + "integrity": "sha1-wNWmOycYgArY4esPpSachN1BhF4=" + }, + "node_modules/qs": { + "version": "6.5.2", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.2.tgz", + "integrity": "sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA==", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/request": { + "version": "2.88.0", + "resolved": "https://registry.npmjs.org/request/-/request-2.88.0.tgz", + "integrity": "sha512-NAqBSrijGLZdM0WZNsInLJpkJokL72XYjUpnB0iwsRgxh7dB6COrHnTBNwN0E+lHDAJzu7kLAkDeY08z2/A0hg==", + "dependencies": { + "aws-sign2": "~0.7.0", + "aws4": "^1.8.0", + "caseless": "~0.12.0", + "combined-stream": "~1.0.6", + "extend": "~3.0.2", + "forever-agent": "~0.6.1", + "form-data": "~2.3.2", + "har-validator": "~5.1.0", + "http-signature": "~1.2.0", + "is-typedarray": "~1.0.0", + "isstream": "~0.1.2", + "json-stringify-safe": "~5.0.1", + "mime-types": "~2.1.19", + "oauth-sign": "~0.9.0", + "performance-now": "^2.1.0", + "qs": "~6.5.2", + "safe-buffer": "^5.1.2", + "tough-cookie": "~2.4.3", + "tunnel-agent": "^0.6.0", + "uuid": "^3.3.2" + }, + "engines": { + "node": ">= 4" + } + }, + "node_modules/request-promise": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/request-promise/-/request-promise-4.2.2.tgz", + "integrity": "sha1-0epG1lSm7k+O5qT+oQGMIpEZBLQ=", + "dependencies": { + "bluebird": "^3.5.0", + "request-promise-core": "1.1.1", + "stealthy-require": "^1.1.0", + "tough-cookie": ">=2.3.3" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/request-promise-core": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/request-promise-core/-/request-promise-core-1.1.1.tgz", + "integrity": "sha1-Pu4AssWqgyOc+wTFcA2jb4HNCLY=", + "dependencies": { + "lodash": "^4.13.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" + }, + "node_modules/sshpk": { + "version": "1.15.2", + "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.15.2.tgz", + "integrity": "sha512-Ra/OXQtuh0/enyl4ETZAfTaeksa6BXks5ZcjpSUNrjBr0DvrJKX+1fsKDPpT9TBXgHAFsa4510aNVgI8g/+SzA==", + "dependencies": { + "asn1": "~0.2.3", + "assert-plus": "^1.0.0", + "bcrypt-pbkdf": "^1.0.0", + "dashdash": "^1.12.0", + "ecc-jsbn": "~0.1.1", + "getpass": "^0.1.1", + "jsbn": "~0.1.0", + "safer-buffer": "^2.0.2", + "tweetnacl": "~0.14.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/stealthy-require": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/stealthy-require/-/stealthy-require-1.1.1.tgz", + "integrity": "sha1-NbCYdbT/SfJqd35QmzCQoyJr8ks=", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/tough-cookie": { + "version": "2.4.3", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.4.3.tgz", + "integrity": "sha512-Q5srk/4vDM54WJsJio3XNn6K2sCG+CQ8G5Wz6bZhRZoAe/+TxjWB/GlFAnYEbkYVlON9FMk/fE3h2RLpPXo4lQ==", + "dependencies": { + "psl": "^1.1.24", + "punycode": "^1.4.1" + }, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/tunnel-agent": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", + "integrity": "sha1-J6XeoGs2sEoKmWZ3SykIaPD8QP0=", + "dependencies": { + "safe-buffer": "^5.0.1" + }, + "engines": { + "node": "*" + } + }, + "node_modules/tweetnacl": { + "version": "0.14.5", + "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz", + "integrity": "sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q=" + }, + "node_modules/uri-js": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.2.2.tgz", + "integrity": "sha512-KY9Frmirql91X2Qgjry0Wd4Y+YTdrdZheS8TFwvkbLWf/G5KNJDCh6pKL5OZctEW4+0Baa5idK2ZQuELRwPznQ==", + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/uri-js/node_modules/punycode": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz", + "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==", + "engines": { + "node": ">=6" + } + }, + "node_modules/uuid": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.3.2.tgz", + "integrity": "sha512-yXJmeNaw3DnnKAOKJE51sL/ZaYfWJRl1pK9dr19YFCu0ObS231AB1/LbqTKRAQ5kw8A90rA6fr4riOUpTZvQZA==", + "bin": { + "uuid": "bin/uuid" + } + }, + "node_modules/verror": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/verror/-/verror-1.10.0.tgz", + "integrity": "sha1-OhBcoXBTr1XW4nDB+CiGguGNpAA=", + "engines": [ + "node >=0.6.0" + ], + "dependencies": { + "assert-plus": "^1.0.0", + "core-util-is": "1.0.2", + "extsprintf": "^1.2.0" + } + } + }, "dependencies": { "ajv": { "version": "6.5.5", @@ -205,9 +663,9 @@ } }, "lodash": { - "version": "4.17.15", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.15.tgz", - "integrity": "sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A==" + "version": "4.17.20", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.20.tgz", + "integrity": "sha512-PlhdFcillOINfeV7Ni6oF1TAEayyZBoZ8bcshTHqOYJYlrqzRK5hagpagky5o4HfCzzd1TRkXPMFq6cKk9rGmA==" }, "mime-db": { "version": "1.37.0", diff --git a/scripts/whotracksme/README.md b/scripts/whotracksme/README.md deleted file mode 100644 index cd5618ad..00000000 --- a/scripts/whotracksme/README.md +++ /dev/null @@ -1,13 +0,0 @@ -## Whotracks.me database converter - -A simple script that converts the Ghostery/Cliqz trackers database to a json format. - -### Usage - -``` -yarn install -node index.js -``` - -You'll find the output in the `whotracksmedb.json` file. -Move it to `client/src/helpers/trackers`. \ No newline at end of file diff --git a/staticcheck.conf b/staticcheck.conf index 43639bf6..4dd93176 100644 --- a/staticcheck.conf +++ b/staticcheck.conf @@ -7,8 +7,13 @@ initialisms = [ , "DOQ" , "DOT" , "EDNS" +, "MX" +, "PTR" , "QUIC" +, "RA" , "SDNS" +, "SLAAC" +, "SVCB" ] dot_import_whitelist = [] http_status_code_whitelist = []