Compare commits
62 Commits
v0.103.0
...
1228-proxy
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f85de51452 | ||
|
|
c3123473cf | ||
|
|
8d0c8ad438 | ||
|
|
1e2e965ea7 | ||
|
|
0b539ced92 | ||
|
|
9e09dffbc3 | ||
|
|
473d881871 | ||
|
|
a1ca7862f8 | ||
|
|
8ea1e64c7b | ||
|
|
2f8e34e73b | ||
|
|
705a9d909d | ||
|
|
97df19898f | ||
|
|
de92c85256 | ||
|
|
d3f5b40700 | ||
|
|
20a0ba5f60 | ||
|
|
9b9902f004 | ||
|
|
020a30fb6d | ||
|
|
39f2d5c4ae | ||
|
|
6ce3c52456 | ||
|
|
5188da60cf | ||
|
|
e57cbc36d9 | ||
|
|
57e43a66c3 | ||
|
|
4f3d503916 | ||
|
|
6bb6c700d6 | ||
|
|
ed76a3cb8b | ||
|
|
335d62b08e | ||
|
|
99625da1e4 | ||
|
|
9fecab8675 | ||
|
|
b9aa969a56 | ||
|
|
ce21514246 | ||
|
|
3ff0c964dc | ||
|
|
552280e9a3 | ||
|
|
d154456ae5 | ||
|
|
3cecd6f090 | ||
|
|
dc1fc82b9e | ||
|
|
a033b68bfd | ||
|
|
4ca4fb8a11 | ||
|
|
8d10a269ed | ||
|
|
a536357427 | ||
|
|
cecf848364 | ||
|
|
ddb9a2e872 | ||
|
|
cfdfd250a0 | ||
|
|
cb01d05ef4 | ||
|
|
c7e7b76cec | ||
|
|
f5128d27f1 | ||
|
|
005f8fb279 | ||
|
|
244fe093cd | ||
|
|
ff9d1c234c | ||
|
|
1c9d3acaa8 | ||
|
|
0dab36a108 | ||
|
|
611ed94884 | ||
|
|
22935c5fed | ||
|
|
4a8dcbeeed | ||
|
|
1ab650bb86 | ||
|
|
4743743b1f | ||
|
|
dd2c9d96e7 | ||
|
|
946bda37a3 | ||
|
|
ad4e85d8f5 | ||
|
|
4b9ab97271 | ||
|
|
d2bf1e176e | ||
|
|
ffeb88ac0c | ||
|
|
c71b8d3ad2 |
2
.github/workflows/lint.yml
vendored
@@ -44,4 +44,4 @@ jobs:
|
||||
fields: repo,message,commit,author
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }}
|
||||
SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }}
|
||||
|
||||
157
AGHTechDoc.md
@@ -22,7 +22,8 @@ Contents:
|
||||
* Update client
|
||||
* Delete client
|
||||
* API: Find clients by IP
|
||||
* Enable DHCP server
|
||||
* DHCP server
|
||||
* DHCP server in DNS
|
||||
* "Show DHCP status" command
|
||||
* "Check DHCP" command
|
||||
* "Enable DHCP" command
|
||||
@@ -51,19 +52,26 @@ Contents:
|
||||
* API: Get query log
|
||||
* API: Set querylog parameters
|
||||
* API: Get querylog parameters
|
||||
* Filtering
|
||||
* DNS Filtering
|
||||
* Filters update mechanism
|
||||
* API: Get filtering parameters
|
||||
* API: Set filtering parameters
|
||||
* API: Refresh filters
|
||||
* API: Add Filter
|
||||
* API: Set URL parameters
|
||||
* API: Delete URL
|
||||
* API: Set Filter parameters
|
||||
* API: Delete Filter
|
||||
* API: Domain Check
|
||||
* HTTP Proxy
|
||||
* API: Get Proxy settings
|
||||
* API: Set Proxy settings
|
||||
* API: Get Proxy filtering parameters
|
||||
* API: Add Proxy Filter
|
||||
* API: Delete Proxy Filter
|
||||
* Log-in page
|
||||
* API: Log in
|
||||
* API: Log out
|
||||
* API: Get current user info
|
||||
* Safe services
|
||||
|
||||
|
||||
## Relations between subsystems
|
||||
@@ -374,9 +382,9 @@ Error response:
|
||||
UI shows error message "Auto-update has failed"
|
||||
|
||||
|
||||
## Enable DHCP server
|
||||
## DHCP server
|
||||
|
||||
Algorithm:
|
||||
Enable DHCP server algorithm:
|
||||
|
||||
* UI shows DHCP configuration screen with "Enabled DHCP" button disabled, and "Check DHCP" button enabled
|
||||
* User clicks on "Check DHCP"; UI sends request to server
|
||||
@@ -388,6 +396,21 @@ Algorithm:
|
||||
* UI shows the status
|
||||
|
||||
|
||||
### DHCP server in DNS
|
||||
|
||||
DHCP leases are used in several ways by DNS module.
|
||||
|
||||
* For "A" DNS reqeust we reply with an IP address leased by our DHCP server.
|
||||
|
||||
< A bills-notebook.lan.
|
||||
> A bills-notebook.lan. = 192.168.1.100
|
||||
|
||||
* For "PTR" DNS request we reply with a hostname from an active DHCP lease.
|
||||
|
||||
< PTR 100.1.168.192.in-addr.arpa.
|
||||
> PTR 100.1.168.192.in-addr.arpa. = bills-notebook.
|
||||
|
||||
|
||||
### "Show DHCP status" command
|
||||
|
||||
Request:
|
||||
@@ -1460,7 +1483,7 @@ Response:
|
||||
}
|
||||
|
||||
|
||||
## Filtering
|
||||
## DNS Filtering
|
||||
|
||||

|
||||
|
||||
@@ -1531,7 +1554,19 @@ Response:
|
||||
}
|
||||
...
|
||||
],
|
||||
"user_rules":["...", ...]
|
||||
"user_rules":["...", ...],
|
||||
|
||||
"proxy_filtering_enabled": true | false
|
||||
"proxy_filters":[
|
||||
{
|
||||
"enabled":true,
|
||||
"url":"https://...",
|
||||
"name":"...",
|
||||
"rules_count":1234,
|
||||
"last_updated":"2019-09-04T18:29:30+00:00",
|
||||
}
|
||||
...
|
||||
],
|
||||
}
|
||||
|
||||
For both arrays `filters` and `whitelist_filters` there are unique values: id, url.
|
||||
@@ -1546,6 +1581,7 @@ Request:
|
||||
|
||||
{
|
||||
"enabled": true | false
|
||||
"proxy_filtering_enabled": true | false
|
||||
"interval": 0 | 1 | 12 | 1*24 || 3*24 || 7*24
|
||||
}
|
||||
|
||||
@@ -1561,7 +1597,7 @@ Request:
|
||||
POST /control/filtering/refresh
|
||||
|
||||
{
|
||||
"whitelist": true
|
||||
"type": blocklist | whitelist | proxylist
|
||||
}
|
||||
|
||||
Response:
|
||||
@@ -1582,7 +1618,7 @@ Request:
|
||||
{
|
||||
"name": "..."
|
||||
"url": "..." // URL or an absolute file path
|
||||
"whitelist": true
|
||||
"type": blocklist | whitelist | proxylist
|
||||
}
|
||||
|
||||
Response:
|
||||
@@ -1590,7 +1626,7 @@ Response:
|
||||
200 OK
|
||||
|
||||
|
||||
### API: Set URL parameters
|
||||
### API: Set Filter parameters
|
||||
|
||||
Request:
|
||||
|
||||
@@ -1598,11 +1634,11 @@ Request:
|
||||
|
||||
{
|
||||
"url": "..."
|
||||
"whitelist": true
|
||||
"type": blocklist | whitelist | proxylist
|
||||
"data": {
|
||||
"name": "..."
|
||||
"url": "..."
|
||||
"enabled": true | false
|
||||
"enabled": true
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1611,7 +1647,7 @@ Response:
|
||||
200 OK
|
||||
|
||||
|
||||
### API: Delete URL
|
||||
### API: Delete Filter
|
||||
|
||||
Request:
|
||||
|
||||
@@ -1619,7 +1655,7 @@ Request:
|
||||
|
||||
{
|
||||
"url": "..."
|
||||
"whitelist": true
|
||||
"type": blocklist | whitelist | proxylist
|
||||
}
|
||||
|
||||
Response:
|
||||
@@ -1651,6 +1687,60 @@ Response:
|
||||
}
|
||||
|
||||
|
||||
## HTTP Proxy
|
||||
|
||||
Browser <-(HTTP)-> AGH Proxy <-(HTTP)-> Internet Server
|
||||
|
||||
HTTPS MITM:
|
||||
|
||||
. Browser --(CONNECT...)-> AGH Proxy --(handshake)-> Internet Server
|
||||
. Browser <-(handshake,cert/AGH)-- AGH Proxy <-(cert/issuer)-- Internet Server
|
||||
. Browser <-(TLS/session2)-> AGH Proxy <-(TLS/session1)-> Internet Server
|
||||
|
||||
|
||||
### API: Get Proxy settings
|
||||
|
||||
Request:
|
||||
|
||||
GET /control/proxy_info
|
||||
|
||||
Response:
|
||||
|
||||
200 OK
|
||||
|
||||
{
|
||||
"enabled": true|false,
|
||||
"listen_address": "ip",
|
||||
"listen_port": 12345,
|
||||
|
||||
"auth_username": "",
|
||||
"auth_password": ""
|
||||
}
|
||||
|
||||
|
||||
### API: Set Proxy settings
|
||||
|
||||
Request:
|
||||
|
||||
POST /control/proxy_config
|
||||
|
||||
{
|
||||
"enabled": true|false,
|
||||
"listen_address": "ip",
|
||||
"listen_port": 12345,
|
||||
|
||||
"auth_username": "",
|
||||
"auth_password": "",
|
||||
|
||||
"cert_data":"...", // user-specified certificate. "": generate new
|
||||
"pkey_data":"...",
|
||||
}
|
||||
|
||||
Response:
|
||||
|
||||
200 OK
|
||||
|
||||
|
||||
## Log-in page
|
||||
|
||||
After user completes the steps of installation wizard, he must log in into dashboard using his name and password. After user successfully logs in, he gets the Cookie which allows the server to authenticate him next time without password. After the Cookie is expired, user needs to perform log-in operation again.
|
||||
@@ -1747,3 +1837,40 @@ Response:
|
||||
}
|
||||
|
||||
If no client is configured then authentication is disabled and server sends an empty response.
|
||||
|
||||
|
||||
### Safe services
|
||||
|
||||
Check if host name is blocked by SB/PC service:
|
||||
|
||||
* For each host name component, search for the result in cache by the first 2 bytes of SHA-256 hashes of host name components (max. is 4, i.e. sub2.sub1.host.com), excluding TLD:
|
||||
|
||||
hashes[] = cache_search(sha256(host.com)[0..1])
|
||||
...
|
||||
|
||||
If hash prefix is found, search for a full hash sum in the cached data.
|
||||
If found, the host is blocked.
|
||||
If not found, the host is not blocked - don't request data for this prefix from the Family server again.
|
||||
If hash prefix is not found, request data for this prefix from the Family server.
|
||||
|
||||
* Prepare query string which is generated from the first 2 bytes (converted to a 4-character string) of SHA-256 hashes of host name components (max. is 4, i.e. sub2.sub1.host.com), excluding TLD:
|
||||
|
||||
qs = ... + string(sha256(sub.host.com)[0..1]) + "." + string(sha256(host.com)[0..1]) + ".sb.dns.adguard.com."
|
||||
|
||||
For PC `.pc.dns.adguard.com` suffix is used.
|
||||
|
||||
* Send TXT query to Family server, receive response which contains the array of complete hash sums of the blocked hosts
|
||||
|
||||
* Check if one of received hash sums (`hashes[]`) matches hash sums for our host name
|
||||
|
||||
hashes[0] <> sha256(host.com)
|
||||
hashes[0] <> sha256(sub.host.com)
|
||||
hashes[1] <> sha256(host.com)
|
||||
hashes[1] <> sha256(sub.host.com)
|
||||
...
|
||||
|
||||
* Store all received hash sums in cache:
|
||||
|
||||
sha256(host.com)[0..1] -> hashes[0],hashes[1],...
|
||||
sha256(sub.host.com)[0..1] -> hashes[2],...
|
||||
...
|
||||
|
||||
111
Makefile
@@ -14,6 +14,13 @@
|
||||
# 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
|
||||
@@ -23,6 +30,9 @@ 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)
|
||||
|
||||
# See release target
|
||||
DIST_DIR=dist
|
||||
@@ -39,6 +49,12 @@ endif
|
||||
endif
|
||||
endif
|
||||
|
||||
# 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
|
||||
ifneq ($(CHANNEL),edge)
|
||||
@@ -98,7 +114,8 @@ all: build
|
||||
init:
|
||||
git config core.hooksPath .githooks
|
||||
|
||||
build: dependencies client
|
||||
build: client_with_deps
|
||||
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
|
||||
@@ -106,6 +123,10 @@ build: dependencies client
|
||||
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
|
||||
|
||||
@@ -134,10 +155,14 @@ lint-go:
|
||||
golangci-lint run
|
||||
|
||||
test:
|
||||
@echo Running unit-tests
|
||||
@echo Running JS unit-tests
|
||||
npm run test --prefix client
|
||||
@echo Running Go unit-tests
|
||||
go test -race -v -bench=. -coverprofile=coverage.txt -covermode=atomic ./...
|
||||
|
||||
ci: dependencies client test
|
||||
ci: client_with_deps
|
||||
go mod download
|
||||
$(MAKE) test
|
||||
|
||||
dependencies:
|
||||
npm --prefix client ci
|
||||
@@ -174,13 +199,20 @@ docker-multi-arch:
|
||||
@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: dependencies client
|
||||
release: client_with_deps
|
||||
go mod download
|
||||
@echo Starting release build: version $(VERSION), channel $(CHANNEL)
|
||||
CHANNEL=$(CHANNEL) $(GORELEASER_COMMAND)
|
||||
$(call repack_dist)
|
||||
$(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))
|
||||
|
||||
@@ -195,7 +227,7 @@ define write_version_file
|
||||
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\": \"https://github.com/AdguardTeam/AdGuardHome/releases\"," >> $(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
|
||||
@@ -245,29 +277,64 @@ define repack_dist
|
||||
# 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
|
||||
cd $(DIST_DIR) && tar xzf AdGuardHome_linux_amd64.tar.gz && tar czf AdGuardHome_linux_amd64.tar.gz AdGuardHome/ && rm -rf AdGuardHome
|
||||
cd $(DIST_DIR) && tar xzf AdGuardHome_linux_386.tar.gz && tar czf AdGuardHome_linux_386.tar.gz AdGuardHome/ && rm -rf AdGuardHome
|
||||
$(call tar_repack,AdGuardHome_linux_amd64.tar.gz)
|
||||
$(call tar_repack,AdGuardHome_linux_386.tar.gz)
|
||||
|
||||
# Linux, all kinds of ARM
|
||||
cd $(DIST_DIR) && tar xzf AdGuardHome_linux_armv5.tar.gz && tar czf AdGuardHome_linux_armv5.tar.gz AdGuardHome/ && rm -rf AdGuardHome
|
||||
cd $(DIST_DIR) && tar xzf AdGuardHome_linux_armv6.tar.gz && tar czf AdGuardHome_linux_armv6.tar.gz AdGuardHome/ && rm -rf AdGuardHome
|
||||
cd $(DIST_DIR) && tar xzf AdGuardHome_linux_armv7.tar.gz && tar czf AdGuardHome_linux_armv7.tar.gz AdGuardHome/ && rm -rf AdGuardHome
|
||||
cd $(DIST_DIR) && tar xzf AdGuardHome_linux_arm64.tar.gz && tar czf AdGuardHome_linux_arm64.tar.gz AdGuardHome/ && rm -rf AdGuardHome
|
||||
$(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
|
||||
cd $(DIST_DIR) && tar xzf AdGuardHome_linux_mips_softfloat.tar.gz && tar czf AdGuardHome_linux_mips_softfloat.tar.gz AdGuardHome/ && rm -rf AdGuardHome
|
||||
cd $(DIST_DIR) && tar xzf AdGuardHome_linux_mipsle_softfloat.tar.gz && tar czf AdGuardHome_linux_mipsle_softfloat.tar.gz AdGuardHome/ && rm -rf AdGuardHome
|
||||
cd $(DIST_DIR) && tar xzf AdGuardHome_linux_mips64_softfloat.tar.gz && tar czf AdGuardHome_linux_mips64_softfloat.tar.gz AdGuardHome/ && rm -rf AdGuardHome
|
||||
cd $(DIST_DIR) && tar xzf AdGuardHome_linux_mips64le_softfloat.tar.gz && tar czf AdGuardHome_linux_mips64le_softfloat.tar.gz AdGuardHome/ && rm -rf AdGuardHome
|
||||
$(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
|
||||
cd $(DIST_DIR) && tar xzf AdGuardHome_freebsd_386.tar.gz && tar czf AdGuardHome_freebsd_386.tar.gz AdGuardHome/ && rm -rf AdGuardHome
|
||||
cd $(DIST_DIR) && tar xzf AdGuardHome_freebsd_amd64.tar.gz && tar czf AdGuardHome_freebsd_amd64.tar.gz AdGuardHome/ && rm -rf AdGuardHome
|
||||
$(call tar_repack,AdGuardHome_freebsd_386.tar.gz)
|
||||
$(call tar_repack,AdGuardHome_freebsd_amd64.tar.gz)
|
||||
|
||||
# FreeBSD, all kinds of ARM
|
||||
cd $(DIST_DIR) && tar xzf AdGuardHome_freebsd_armv5.tar.gz && tar czf AdGuardHome_freebsd_armv5.tar.gz AdGuardHome/ && rm -rf AdGuardHome
|
||||
cd $(DIST_DIR) && tar xzf AdGuardHome_freebsd_armv6.tar.gz && tar czf AdGuardHome_freebsd_armv6.tar.gz AdGuardHome/ && rm -rf AdGuardHome
|
||||
cd $(DIST_DIR) && tar xzf AdGuardHome_freebsd_armv7.tar.gz && tar czf AdGuardHome_freebsd_armv7.tar.gz AdGuardHome/ && rm -rf AdGuardHome
|
||||
cd $(DIST_DIR) && tar xzf AdGuardHome_freebsd_arm64.tar.gz && tar czf AdGuardHome_freebsd_arm64.tar.gz AdGuardHome/ && rm -rf AdGuardHome
|
||||
$(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
|
||||
|
||||
@@ -155,7 +155,10 @@ Run `make init` to prepare the development environment.
|
||||
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 or later.
|
||||
* [node.js](https://nodejs.org/en/download/) v10.16.2 or later.
|
||||
* [npm](https://www.npmjs.com/) v6.14 or later.
|
||||
|
||||
Optionally, for Go devs:
|
||||
* [golangci-lint](https://github.com/golangci/golangci-lint)
|
||||
|
||||
### Building
|
||||
@@ -214,6 +217,8 @@ You may need to prepare before using these builds:
|
||||
|
||||
You are welcome to fork this repository, make your changes and submit a pull request — https://github.com/AdguardTeam/AdGuardHome/pulls
|
||||
|
||||
Please note, that we don't expect people to contribute to both UI and golang parts of the program simultaneously. Ideally, the golang part is implemented first, i.e. configuration, API, and the functionality itself. The UI part can be implemented later in a different pull request by a different person.
|
||||
|
||||
<a id="test-unstable-versions"></a>
|
||||
### Test unstable versions
|
||||
|
||||
@@ -230,7 +235,7 @@ There are three options how you can install an unstable version:
|
||||
|
||||
There are three options how you can install an unstable version.
|
||||
|
||||
1. You can either install a beta version of AdGuard Home which we update periodically.
|
||||
1. You can either install a beta or edge version of AdGuard Home which we update periodically. If you're already using stable version of AdGuard Home, just replace the executable with a new one.
|
||||
2. You can use the Docker image from the `edge` tag, which is synced with the repo master branch.
|
||||
3. You can install AdGuard Home from `beta` or `edge` channels on the Snap Store.
|
||||
|
||||
|
||||
1
client/babel.config.js
vendored
@@ -11,6 +11,7 @@ module.exports = (api) => {
|
||||
'@babel/plugin-proposal-object-rest-spread',
|
||||
'@babel/plugin-proposal-nullish-coalescing-operator',
|
||||
'@babel/plugin-proposal-optional-chaining',
|
||||
'react-hot-loader/babel',
|
||||
],
|
||||
};
|
||||
};
|
||||
|
||||
11
client/constants.js
vendored
Normal file
@@ -0,0 +1,11 @@
|
||||
const BUILD_ENVS = {
|
||||
dev: 'development',
|
||||
prod: 'production',
|
||||
};
|
||||
|
||||
const BASE_URL = '/control';
|
||||
|
||||
module.exports = {
|
||||
BUILD_ENVS,
|
||||
BASE_URL,
|
||||
};
|
||||
146
client/package-lock.json
generated
vendored
@@ -1356,6 +1356,17 @@
|
||||
"resolved": "https://registry.npmjs.org/@emotion/weak-memoize/-/weak-memoize-0.2.5.tgz",
|
||||
"integrity": "sha512-6U71C2Wp7r5XtFtQzYrW5iKFT67OixrSxjI4MptCHzdSVlgabczzqLe0ZSgnub/5Kp4hSbpDB1tMytZY9pwxxA=="
|
||||
},
|
||||
"@hot-loader/react-dom": {
|
||||
"version": "16.13.0",
|
||||
"resolved": "https://registry.npmjs.org/@hot-loader/react-dom/-/react-dom-16.13.0.tgz",
|
||||
"integrity": "sha512-lJZrmkucz2MrQJTQtJobx5MICXcfQvKihszqv655p557HPi0hMOWxrNpiHv3DWD8ugNWjtWcVWqRnFvwsHq1mQ==",
|
||||
"requires": {
|
||||
"loose-envify": "^1.1.0",
|
||||
"object-assign": "^4.1.1",
|
||||
"prop-types": "^15.6.2",
|
||||
"scheduler": "^0.19.0"
|
||||
}
|
||||
},
|
||||
"@istanbuljs/load-nyc-config": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz",
|
||||
@@ -2257,6 +2268,12 @@
|
||||
"@types/istanbul-lib-report": "*"
|
||||
}
|
||||
},
|
||||
"@types/json-schema": {
|
||||
"version": "7.0.5",
|
||||
"resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.5.tgz",
|
||||
"integrity": "sha512-7+2BITlgjgDhH0vvwZU/HZJVyk+2XUlvxXe8dFMedNX/aMkaOq++rMAFXc0tM7ij15QaWlbdQASBR9dihi+bDQ==",
|
||||
"dev": true
|
||||
},
|
||||
"@types/minimatch": {
|
||||
"version": "3.0.3",
|
||||
"resolved": "https://registry.npmjs.org/@types/minimatch/-/minimatch-3.0.3.tgz",
|
||||
@@ -2728,7 +2745,6 @@
|
||||
"version": "1.0.10",
|
||||
"resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz",
|
||||
"integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"sprintf-js": "~1.0.2"
|
||||
}
|
||||
@@ -5108,6 +5124,12 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"dom-walk": {
|
||||
"version": "0.1.2",
|
||||
"resolved": "https://registry.npmjs.org/dom-walk/-/dom-walk-0.1.2.tgz",
|
||||
"integrity": "sha512-6QvTW9mrGeIegrFXdtQi9pk7O/nSK6lSdXW2eqUspN5LWD7UTji2Fqw5V2YLjBpHEoU9Xl/eUWNpDeZvoyOv2w==",
|
||||
"dev": true
|
||||
},
|
||||
"domain-browser": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/domain-browser/-/domain-browser-1.2.0.tgz",
|
||||
@@ -5201,9 +5223,9 @@
|
||||
"dev": true
|
||||
},
|
||||
"elliptic": {
|
||||
"version": "6.5.2",
|
||||
"resolved": "https://registry.npmjs.org/elliptic/-/elliptic-6.5.2.tgz",
|
||||
"integrity": "sha512-f4x70okzZbIQl/NSRLkI/+tteV/9WqL98zx+SQ69KbXxmVrmjwsNUPn/gYJJ0sHvEak24cZgHIPegRePAtA/xw==",
|
||||
"version": "6.5.3",
|
||||
"resolved": "https://registry.npmjs.org/elliptic/-/elliptic-6.5.3.tgz",
|
||||
"integrity": "sha512-IMqzv5wNQf+E6aHeIqATs0tOLeOTwj1QKbRcS3jBbYkl5oLAserA8yJTT7/VyHUYG91PRmPyeQDObKLPpeS4dw==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"bn.js": "^4.4.0",
|
||||
@@ -5216,9 +5238,9 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"bn.js": {
|
||||
"version": "4.11.8",
|
||||
"resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.11.8.tgz",
|
||||
"integrity": "sha512-ItfYfPLkWHUjckQCk8xC+LwxgK8NYcXywGigJgSwOP8Y2iyWT4f2vsZnoOXTTbo+o5yXmIUJ4gn5538SO5S3gA==",
|
||||
"version": "4.11.9",
|
||||
"resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.11.9.tgz",
|
||||
"integrity": "sha512-E6QoYqCKZfgatHTdHzs1RRKP7ip4vvm+EyRUeE2RF0NblwVvb0p6jSVeNTOFxPn26QXN2o6SMfNxKp6kU8zQaw==",
|
||||
"dev": true
|
||||
}
|
||||
}
|
||||
@@ -5887,8 +5909,7 @@
|
||||
"esprima": {
|
||||
"version": "4.0.1",
|
||||
"resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz",
|
||||
"integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==",
|
||||
"dev": true
|
||||
"integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A=="
|
||||
},
|
||||
"esquery": {
|
||||
"version": "1.3.1",
|
||||
@@ -6844,6 +6865,16 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"global": {
|
||||
"version": "4.4.0",
|
||||
"resolved": "https://registry.npmjs.org/global/-/global-4.4.0.tgz",
|
||||
"integrity": "sha512-wv/LAoHdRE3BeTGz53FAamhGlPLhlssK45usmGFThIi4XqnBmjKQ16u+RNbP7WvigRZDxUsM0J3gcQ5yicaL0w==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"min-document": "^2.19.0",
|
||||
"process": "^0.11.10"
|
||||
}
|
||||
},
|
||||
"global-modules": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/global-modules/-/global-modules-2.0.0.tgz",
|
||||
@@ -7357,18 +7388,6 @@
|
||||
"requires-port": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"http-proxy-middleware": {
|
||||
"version": "0.19.1",
|
||||
"resolved": "https://registry.npmjs.org/http-proxy-middleware/-/http-proxy-middleware-0.19.1.tgz",
|
||||
"integrity": "sha512-yHYTgWMQO8VvwNS22eLLloAkvungsKdKTLO8AJlftYIKNfJr3GK3zK0ZCfzDDGUBttdGc8xFy1mCitvNKQtC3Q==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"http-proxy": "^1.17.0",
|
||||
"is-glob": "^4.0.0",
|
||||
"lodash": "^4.17.11",
|
||||
"micromatch": "^3.1.10"
|
||||
}
|
||||
},
|
||||
"http-signature": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.2.0.tgz",
|
||||
@@ -9898,10 +9917,9 @@
|
||||
"integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="
|
||||
},
|
||||
"js-yaml": {
|
||||
"version": "3.13.1",
|
||||
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.13.1.tgz",
|
||||
"integrity": "sha512-YfbcO7jXDdyj0DGxYVSlSeQNHbD7XPWvrVWeVUujrQEoZzWJIRrCPoyk6kL6IAjAG2IolMK4T0hNUe0HOUs5Jw==",
|
||||
"dev": true,
|
||||
"version": "3.14.0",
|
||||
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.0.tgz",
|
||||
"integrity": "sha512-/4IbIeHcD9VMHFqDR/gQ7EdZdLimOvW2DdcxFjdyyZ9NsbS+ccrXqVWDtab/lRl5AlUqmpBx8EhPaWR+OtY17A==",
|
||||
"requires": {
|
||||
"argparse": "^1.0.7",
|
||||
"esprima": "^4.0.0"
|
||||
@@ -10168,9 +10186,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.19",
|
||||
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.19.tgz",
|
||||
"integrity": "sha512-JNvd8XER9GQX0v2qJgsaN/mzFCNA5BRe/j8JN9d+tWyGLSodKQHKFicdwNYzWwI3wjRnaKPsGj1XkBjx/F96DQ=="
|
||||
},
|
||||
"lodash-es": {
|
||||
"version": "4.17.15",
|
||||
@@ -10628,6 +10646,15 @@
|
||||
"integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==",
|
||||
"dev": true
|
||||
},
|
||||
"min-document": {
|
||||
"version": "2.19.0",
|
||||
"resolved": "https://registry.npmjs.org/min-document/-/min-document-2.19.0.tgz",
|
||||
"integrity": "sha1-e9KC4/WELtKVu3SM3Z8f+iyCRoU=",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"dom-walk": "^0.1.0"
|
||||
}
|
||||
},
|
||||
"min-indent": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/min-indent/-/min-indent-1.0.0.tgz",
|
||||
@@ -12287,6 +12314,39 @@
|
||||
"scheduler": "^0.19.1"
|
||||
}
|
||||
},
|
||||
"react-hot-loader": {
|
||||
"version": "4.12.21",
|
||||
"resolved": "https://registry.npmjs.org/react-hot-loader/-/react-hot-loader-4.12.21.tgz",
|
||||
"integrity": "sha512-Ynxa6ROfWUeKWsTHxsrL2KMzujxJVPjs385lmB2t5cHUxdoRPGind9F00tOkdc1l5WBleOF4XEAMILY1KPIIDA==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"fast-levenshtein": "^2.0.6",
|
||||
"global": "^4.3.0",
|
||||
"hoist-non-react-statics": "^3.3.0",
|
||||
"loader-utils": "^1.1.0",
|
||||
"prop-types": "^15.6.1",
|
||||
"react-lifecycles-compat": "^3.0.4",
|
||||
"shallowequal": "^1.1.0",
|
||||
"source-map": "^0.7.3"
|
||||
},
|
||||
"dependencies": {
|
||||
"hoist-non-react-statics": {
|
||||
"version": "3.3.2",
|
||||
"resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz",
|
||||
"integrity": "sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"react-is": "^16.7.0"
|
||||
}
|
||||
},
|
||||
"source-map": {
|
||||
"version": "0.7.3",
|
||||
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.3.tgz",
|
||||
"integrity": "sha512-CkCj6giN3S+n9qrYiBTX5gystlENnRW5jZeNLHpe6aue+SrHcG5VYwujhW9s4dY31mEGsxBDrHR6oI69fTXsaQ==",
|
||||
"dev": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"react-i18next": {
|
||||
"version": "11.4.0",
|
||||
"resolved": "https://registry.npmjs.org/react-i18next/-/react-i18next-11.4.0.tgz",
|
||||
@@ -13328,6 +13388,12 @@
|
||||
"safe-buffer": "^5.0.1"
|
||||
}
|
||||
},
|
||||
"shallowequal": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/shallowequal/-/shallowequal-1.1.0.tgz",
|
||||
"integrity": "sha512-y0m1JoUZSlPAjXVtPPW70aZWfIL/dSP7AFkRnniLCrK/8MDKog3TySTBmckD+RObVxH0v4Tox67+F14PdED2oQ==",
|
||||
"dev": true
|
||||
},
|
||||
"shebang-command": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
|
||||
@@ -13752,8 +13818,7 @@
|
||||
"sprintf-js": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz",
|
||||
"integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=",
|
||||
"dev": true
|
||||
"integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw="
|
||||
},
|
||||
"sshpk": {
|
||||
"version": "1.16.1",
|
||||
@@ -14080,12 +14145,13 @@
|
||||
}
|
||||
},
|
||||
"schema-utils": {
|
||||
"version": "2.6.6",
|
||||
"resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-2.6.6.tgz",
|
||||
"integrity": "sha512-wHutF/WPSbIi9x6ctjGGk2Hvl0VOz5l3EKEuKbjPlB30mKZUzb9A5k9yEXRX3pwyqVLPvpfZZEllaFq/M718hA==",
|
||||
"version": "2.7.0",
|
||||
"resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-2.7.0.tgz",
|
||||
"integrity": "sha512-0ilKFI6QQF5nxDZLFn2dMjvc4hjg/Wkg7rHd3jK6/A4a1Hl9VFdQWvgB1UMGoU94pad1P/8N7fMcEnLnSiju8A==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"ajv": "^6.12.0",
|
||||
"@types/json-schema": "^7.0.4",
|
||||
"ajv": "^6.12.2",
|
||||
"ajv-keywords": "^3.4.1"
|
||||
}
|
||||
}
|
||||
@@ -15931,6 +15997,18 @@
|
||||
"ms": "^2.1.1"
|
||||
}
|
||||
},
|
||||
"http-proxy-middleware": {
|
||||
"version": "0.19.1",
|
||||
"resolved": "https://registry.npmjs.org/http-proxy-middleware/-/http-proxy-middleware-0.19.1.tgz",
|
||||
"integrity": "sha512-yHYTgWMQO8VvwNS22eLLloAkvungsKdKTLO8AJlftYIKNfJr3GK3zK0ZCfzDDGUBttdGc8xFy1mCitvNKQtC3Q==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"http-proxy": "^1.17.0",
|
||||
"is-glob": "^4.0.0",
|
||||
"lodash": "^4.17.11",
|
||||
"micromatch": "^3.1.10"
|
||||
}
|
||||
},
|
||||
"ms": {
|
||||
"version": "2.1.2",
|
||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
|
||||
|
||||
8
client/package.json
vendored
@@ -4,14 +4,16 @@
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"build-dev": "cross-env BUILD_ENV=dev webpack --config webpack.dev.js",
|
||||
"watch": "cross-env BUILD_ENV=dev webpack --config webpack.dev.js --watch",
|
||||
"build-prod": "cross-env BUILD_ENV=prod webpack --config webpack.prod.js",
|
||||
"watch": "cross-env BUILD_ENV=dev webpack --config webpack.dev.js --watch",
|
||||
"watch:hot": "cross-env BUILD_ENV=dev webpack-dev-server --config webpack.dev.js",
|
||||
"lint": "eslint src",
|
||||
"lint:fix": "eslint src --fix",
|
||||
"test": "jest",
|
||||
"test:watch": "jest --watch"
|
||||
},
|
||||
"dependencies": {
|
||||
"@hot-loader/react-dom": "^16.13.0",
|
||||
"@nivo/line": "^0.49.1",
|
||||
"axios": "^0.19.2",
|
||||
"classnames": "^2.2.6",
|
||||
@@ -19,7 +21,8 @@
|
||||
"i18next": "^19.4.4",
|
||||
"i18next-browser-languagedetector": "^4.2.0",
|
||||
"ipaddr.js": "^1.9.1",
|
||||
"lodash": "^4.17.15",
|
||||
"js-yaml": "^3.14.0",
|
||||
"lodash": "^4.17.19",
|
||||
"nanoid": "^3.1.9",
|
||||
"prop-types": "^15.7.2",
|
||||
"query-string": "^6.13.1",
|
||||
@@ -73,6 +76,7 @@
|
||||
"path": "^0.12.7",
|
||||
"postcss-flexbugs-fixes": "4.2.1",
|
||||
"postcss-loader": "^3.0.0",
|
||||
"react-hot-loader": "^4.12.21",
|
||||
"style-loader": "^1.2.1",
|
||||
"stylelint": "^13.5.0",
|
||||
"stylelint-webpack-plugin": "2.0.0",
|
||||
|
||||
@@ -84,7 +84,7 @@
|
||||
"disable_protection": "Onemogući zaštitu",
|
||||
"disabled_protection": "Onemogućena zaštita",
|
||||
"refresh_statics": "Osvježi statistiku",
|
||||
"dns_query": "DNS Upiti",
|
||||
"dns_query": "DNS upiti",
|
||||
"blocked_by": "<0>Blokirano filtrima</0>",
|
||||
"stats_malware_phishing": "Blokiran zločudni program/krađe identiteta",
|
||||
"stats_adult": "Blokirane web stranice za odrasle",
|
||||
@@ -111,7 +111,7 @@
|
||||
"block_domain_use_filters_and_hosts": "Blokiraj domene koristeći filtre ili hosts datoteke",
|
||||
"filters_block_toggle_hint": "Pravila blokiranja možete postaviti u postavkama <a href='#filters'>filtara</a>.",
|
||||
"use_adguard_browsing_sec": "Koristi AdGuard uslugu zaštite pregledavanja",
|
||||
"use_adguard_browsing_sec_hint": "AdGuard Home će provjeriti nalazi li se domena na popisu neželjenih domena od usluge zaštite pregledavanja. Za provjeru će se koristiti API za provjeru koji poštuje vašu privatnost. Samo mali dio SHA256 hash-a odnaziva domene se šalje poslužitelju.",
|
||||
"use_adguard_browsing_sec_hint": "AdGuard Home će provjeriti nalazi li se domena na popisu neželjenih domena od usluge zaštite pregledavanja. Za provjeru će se koristiti API za provjeru koji poštuje vašu privatnost. Samo mali dio SHA256 hash-a od naziva domene se šalje poslužitelju.",
|
||||
"use_adguard_parental": "Koristi web uslugu AdGuard roditeljske zaštite",
|
||||
"use_adguard_parental_hint": "AdGuard Home provjeriti će sadrži li domena sadržaj za odrasle. Koristi isti API za zaštitu privatnosti kao i naša usluga zaštite pregledavanja.",
|
||||
"enforce_safe_search": "Omogući sigurno pretraživanje",
|
||||
|
||||
@@ -3,6 +3,8 @@
|
||||
"example_upstream_reserved": "Anda dapat menetapkan DNS upstream <0>untuk domain spesifik</0>",
|
||||
"upstream_parallel": "Gunakan kueri paralel untuk mempercepat resoluasi dengan menanyakan semua server upstream secara bersamaan",
|
||||
"parallel_requests": "Permintaan paralel",
|
||||
"load_balancing": "Penyeimbang beban",
|
||||
"load_balancing_desc": "Permintaan satu server pada satu waktu. AdGuard Home akan menggunakan algoritma acak tertimbang untuk memilih server sehingga server tercepat akan lebih sering digunakan.",
|
||||
"bootstrap_dns": "Server DNS bootstrap",
|
||||
"bootstrap_dns_desc": "Server Bootstrap DNS dapat digunakan untuk meresolve alamat IP pada DoH/DoT resolvers yang Anda tentukan sebagai upstreams.",
|
||||
"check_dhcp_servers": "Cek untuk server DHCP",
|
||||
@@ -51,9 +53,11 @@
|
||||
"dhcp_add_static_lease": "Tambah static lease",
|
||||
"dhcp_reset": "Apakah anda yakin ingin mengatur ulang konfigurasi DHCP anda?",
|
||||
"country": "Negara",
|
||||
"city": "Kota",
|
||||
"delete_confirm": "Apakah anda yakin ingin menghapus \"{{key}}\"?",
|
||||
"form_enter_hostname": "Masukkan hostname",
|
||||
"error_details": "Detail kesalahan",
|
||||
"response_details": "Detail respon",
|
||||
"request_details": "Detai permintaan",
|
||||
"client_details": "Detail klien",
|
||||
"details": "Detail",
|
||||
@@ -63,6 +67,8 @@
|
||||
"filters": "Penyaring",
|
||||
"filter": "Filter",
|
||||
"query_log": "Catatan Kueri",
|
||||
"compact": "Rapat",
|
||||
"nothing_found": "Tidak ditemukan",
|
||||
"faq": "Tanya Jawab",
|
||||
"version": "versi",
|
||||
"address": "Alamat",
|
||||
@@ -114,12 +120,16 @@
|
||||
"general_settings": "Pengaturan umum",
|
||||
"dns_settings": "Pengaturan DNS",
|
||||
"dns_blocklists": "Daftar blokir DNS",
|
||||
"dns_allowlists": "Daftar putih DNS",
|
||||
"dns_blocklists_desc": "AdGuard Home akan memblokir domain yang cocok dengan daftar hitam.",
|
||||
"dns_allowlists_desc": "Domain dari daftar putih DNS akan diizinkan bahkan jika mereka ada juga di daftar hitam.",
|
||||
"custom_filtering_rules": "Aturan penyaringan khusus",
|
||||
"encryption_settings": "Pengaturan enkripsi",
|
||||
"dhcp_settings": "Pengaturan DHCP",
|
||||
"upstream_dns": "Server DNS hulu",
|
||||
"upstream_dns_hint": "Jika Anda mengosongkan kolom ini, AdGuard Home akan menggunakan <a href='https://1.1.1.1/' target='_blank'>Cloudflare DNS</a> sebagai hulu. Gunakan tls:// untuk server DNS over TLS.",
|
||||
"test_upstream_btn": "Uji hulu",
|
||||
"upstreams": "Upstream",
|
||||
"apply_btn": "Terapkan",
|
||||
"disabled_filtering_toast": "Penyaringan nonaktif",
|
||||
"enabled_filtering_toast": "Penyaringan aktif",
|
||||
@@ -131,6 +141,7 @@
|
||||
"enabled_save_search_toast": "Pencarian aman diaktifkan",
|
||||
"enabled_table_header": "Diaktifkan",
|
||||
"name_table_header": "Nama",
|
||||
"list_url_table_header": "Daftar URL",
|
||||
"rules_count_table_header": "Jumlah Aturan",
|
||||
"last_time_updated_table_header": "Terakhir diperbaharui",
|
||||
"actions_table_header": "Aksi",
|
||||
@@ -139,10 +150,23 @@
|
||||
"delete_table_action": "Hapus",
|
||||
"elapsed": "Berlalu",
|
||||
"filters_and_hosts_hint": "AdGuard Home memahami aturan dasar adblock dan sintak file hosts.",
|
||||
"no_blocklist_added": "Tiada daftar hitam ditambahkan",
|
||||
"no_whitelist_added": "Tiada daftar putih ditambahkan",
|
||||
"add_blocklist": "Tambah daftar hitam",
|
||||
"add_allowlist": "Tambah daftar putih",
|
||||
"cancel_btn": "Batal",
|
||||
"enter_name_hint": "Masukkan nama",
|
||||
"enter_url_or_path_hint": "Masukan sebuah URL atau jalur absolut dari daftar",
|
||||
"check_updates_btn": "Cek pembaruan",
|
||||
"new_blocklist": "Daftar hitam baru",
|
||||
"new_allowlist": "Daftar putih baru",
|
||||
"edit_blocklist": "Edit daftar hitam",
|
||||
"edit_allowlist": "Edit daftar putih",
|
||||
"choose_blocklist": "Pilih daftar hitam",
|
||||
"choose_allowlist": "Pilih daftar putih",
|
||||
"enter_valid_blocklist": "Masukkan valid URL ke daftar hitam.",
|
||||
"enter_valid_allowlist": "Masukkan valid URL ke daftar putih.",
|
||||
"form_error_url_format": "Format URL tidak valid",
|
||||
"form_error_url_or_path_format": "URL atau jalur absolut dari daftar tidak valid",
|
||||
"custom_filter_rules": "Aturan penyaringan khusus",
|
||||
"custom_filter_rules_hint": "Masukkan satu aturan dalam sebuah baris. Anda dapat menggunakan baik aturan adblock maupun sintaks file hosts.",
|
||||
@@ -159,6 +183,7 @@
|
||||
"example_upstream_doh": "terenkripsi <0>DNS-over-HTTPS</0>",
|
||||
"example_upstream_sdns": "anda bisa menggunakan <0>Stempel DNS</0> untuk <1>DNSCrypt</1> atau pengarah <2>DNS-over-HTTPS</2>",
|
||||
"example_upstream_tcp": "DNS reguler (melalui TCP)",
|
||||
"all_lists_up_to_date_toast": "Semua daftar sudah diperbarui",
|
||||
"updated_upstream_dns_toast": "Server DNS hulu terbarui",
|
||||
"dns_test_ok_toast": "Server DNS yang ditentukan bekerja dengan benar",
|
||||
"dns_test_not_ok_toast": "Server \"{{key}}\": tidak dapat digunakan, mohon cek bahwa Anda telah menulisnya dengan benar",
|
||||
@@ -170,6 +195,7 @@
|
||||
"domain_or_client": "Domain atau klien",
|
||||
"type_table_header": "Tipe",
|
||||
"response_table_header": "Respon",
|
||||
"response_code": "Kode respon",
|
||||
"client_table_header": "Klien",
|
||||
"empty_response_status": "Kosong",
|
||||
"show_all_filter_type": "Tampilkan semua",
|
||||
@@ -188,6 +214,7 @@
|
||||
"query_log_filtered": "Difilter oleh {{filter}}",
|
||||
"query_log_confirm_clear": "Apakah Anda yakin ingin menghapus seluruh kueri log?",
|
||||
"query_log_cleared": "Kueri log telah berhasil dihapus",
|
||||
"query_log_updated": "Log permintaan telah berhasil diperbarui",
|
||||
"query_log_clear": "Hapus kueri log",
|
||||
"query_log_retention": "Retensi kueri log",
|
||||
"query_log_enable": "Aktifkan log",
|
||||
@@ -195,19 +222,39 @@
|
||||
"query_log_disabled": "Kueri log dinonaktifkan dan dapat dikonfigurasi di <0>pengaturan</0>",
|
||||
"query_log_strict_search": "Gunakan tanda kutip ganda untuk pencarian ketat",
|
||||
"query_log_retention_confirm": "Apakah Anda yakin ingin mengubah retensi kueri log? Jika Anda menurunkan nilai interval, beberapa data akan hilang",
|
||||
"anonymize_client_ip": "Anonim IP klien",
|
||||
"anonymize_client_ip_desc": "Jangan simpan alamat lengkap IP klien dalam log dan statistik",
|
||||
"dns_config": "Konfigurasi server DNS",
|
||||
"dns_cache_config": "Konfigurasi cache DNS",
|
||||
"dns_cache_config_desc": "Disini Anda bisa mengonfigurasi cache DNS",
|
||||
"blocking_mode": "Mode blokir",
|
||||
"default": "Standar",
|
||||
"nxdomain": "NXDOMAIN",
|
||||
"null_ip": "Null IP",
|
||||
"custom_ip": "Custom IP",
|
||||
"blocking_ipv4": "Blokiran IPv4",
|
||||
"blocking_ipv6": "Blokiran IPv6",
|
||||
"dns_over_https": "DNS-over-HTTPS",
|
||||
"dns_over_tls": "DNS-over-TLS",
|
||||
"plain_dns": "Plain DNS",
|
||||
"form_enter_rate_limit": "Masukkan batas nilai",
|
||||
"rate_limit": "Batas nilai",
|
||||
"edns_enable": "Aktifkan EDNS Klien Subnet",
|
||||
"edns_cs_desc": "Apabila dinyalakan, AdGuard Home akan mengirim subnet klien ke server-server DNS.",
|
||||
"rate_limit_desc": "Jumlah permintaan per detik yang diperbolehkan untuk satu klien (0: tidak terbatas)",
|
||||
"blocking_ipv4_desc": "Alamat IP akan dikembalikan untuk permintaan A yang diblokir",
|
||||
"blocking_ipv6_desc": "Alamat IP akan dipulihkan untuk permintaan AAAA yang diblokir",
|
||||
"blocking_mode_default": "Standar: Respon pakai NXDOMAIN saat diblokir oleh aturan gaya Adblock; membalas dengan alamat IP yang ditentukan dalam aturan ketika diblokir oleh /et /aturan hosts-style",
|
||||
"blocking_mode_nxdomain": "NXDOMAIN: Respon pakai kode NXDOMAIN",
|
||||
"blocking_mode_null_ip": "Null IP: Respon pakai alamat IP kosong (0.0.0.0 untuk A; :: untuk AAAA)",
|
||||
"blocking_mode_custom_ip": "IP kustom: respon dengan alamat IP yang diset secara manual",
|
||||
"upstream_dns_client_desc": "Jika Anda biarkan bidang ini kosong, AdGuard Home akan memakai server yang dikonfigurasi di<0>Pengaturan DNS</0>.",
|
||||
"tracker_source": "Sumber pelacak",
|
||||
"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",
|
||||
"install_welcome_title": "Selamat datang di AdGuard Home!",
|
||||
@@ -324,6 +371,8 @@
|
||||
"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</0>",
|
||||
"form_enter_ip": "Masukkan IP",
|
||||
"form_enter_mac": "Masukkan MAC",
|
||||
"form_enter_id": "Masukkan pengenal",
|
||||
"form_add_id": "Tambah pengenal",
|
||||
"form_client_name": "Masukkan nama klien",
|
||||
"name": "Nama",
|
||||
"client_global_settings": "Gunakan pengaturan global",
|
||||
@@ -332,6 +381,7 @@
|
||||
"client_updated": "Klien \"{{key}}\" berhasil diperbarui",
|
||||
"clients_not_found": "Tidak ada klien ditemukan",
|
||||
"client_confirm_delete": "Apakah anda yakin ingin menghapus klien \"{{key}}\"?",
|
||||
"list_confirm_delete": "Anda yakin ingin menghapus daftar ini?",
|
||||
"auto_clients_title": "Klien (waktu berjalan)",
|
||||
"auto_clients_desc": "Data pada klien yang menggunakan AdGuard Home, tetapi tidak disimpan dalam konfigurasi",
|
||||
"access_title": "Pengaturan akses",
|
||||
@@ -399,6 +449,8 @@
|
||||
"domain": "Domain",
|
||||
"answer": "Jawab",
|
||||
"filter_added_successfully": "Filter telah berhasil ditambahkan",
|
||||
"filter_removed_successfully": "Daftar ini telah sukses dihapus",
|
||||
"filter_updated": "Daftar telah sukses diperbarui",
|
||||
"statistics_configuration": "Konfigurasi statistik",
|
||||
"statistics_retention": "Statistik disimpan",
|
||||
"statistics_retention_desc": "Jika Anda menurunkan nilai interval, beberapa data akan hilang",
|
||||
@@ -428,13 +480,33 @@
|
||||
"whois": "Whois",
|
||||
"filtering_rules_learn_more": "<0>Pelajari lebih lanjut</0> tentang membuat daftar hitam host Anda sendiri.",
|
||||
"blocked_by_response": "Diblokir oleh CNAME atau IP sebagai respon",
|
||||
"blocked_by_cname_or_ip": "Diblokir oleh CNAME atau IP",
|
||||
"try_again": "Coba lagi",
|
||||
"domain_desc": "Masukkan nama domain atau wildcard yang ingin Anda tulis ulang.",
|
||||
"example_rewrite_domain": "tulis ulang respon hanya untuk domain ini saja.",
|
||||
"example_rewrite_wildcard": "tulis ulang respon untuk semua subdomain <0>contoh.org</0>.",
|
||||
"rewrite_ip_address": "Alamat IP: pakai IP ini dalam respons A atau AAAA",
|
||||
"rewrite_domain_name": "Nama domain: tambah ke rekaman CNAME",
|
||||
"rewrite_A": "<0>A</0>: nilai khusus, biarkan <0>A</0> merekam dari upstream",
|
||||
"rewrite_AAAA": "<0>AAAA</0>: nilai khusus, biarkan <0>AAAA</0> merekam dari upstream",
|
||||
"disable_ipv6": "Matikan IPv6",
|
||||
"disable_ipv6_desc": "Apabila fitur ini dinyalakan, semua permintaan DNS untuk alamat-alamat IPv6 (tipe AAAA) akan diputus.",
|
||||
"fastest_addr": "Alamat IP tercepat",
|
||||
"fastest_addr_desc": "Permintaan semua server DNS dan kembalinya alamat IP tercepat di antara semua respons",
|
||||
"autofix_warning_text": "Apabila anda menekan \"Perbaiki\", AdGuardHome akan mengatur sistem anda untuk menggunakan server DNS AdGuardHome.",
|
||||
"autofix_warning_list": "Ini akan melakukan tugas berikut: <0>Nonaktifkan sistem DNSStubListener</0> <0> Atur alamat server DNS ke 127.0.0.1</0> <0>Ganti target tautan simbolis /etc/resolv.conf pakai /run/systemd/resolve/resolv.conf</0> <0>Hentikan DNSStubListener (muat ulang layanan sistemd-resolve service)</0>",
|
||||
"autofix_warning_result": "Hasilnya, semua permintaan DNS dari sistem anda akan diproses oleh AdGuardHome secara standar.",
|
||||
"tags_title": "Tag",
|
||||
"tags_desc": "Anda dapat memilih tag sesuai dengan klien. Tag dapat dimasukkan dalam aturan pemfilteran dan memungkinkan Anda untuk menerapkannya lebih akurat. <0>Pelajari lebih</0>",
|
||||
"form_select_tags": "Pilih tag klien",
|
||||
"check_title": "Periksa penyaringan",
|
||||
"check_desc": "Periksa apakah nama host telah tersaring",
|
||||
"check": "Periksa",
|
||||
"form_enter_host": "Masukkan nama host",
|
||||
"filtered_custom_rules": "Tersaring oleh aturan penyaring Buatan",
|
||||
"choose_from_list": "Pilih dari daftar",
|
||||
"add_custom_list": "Tambah daftar buatan",
|
||||
"host_whitelisted": "Host didaftar putihkan",
|
||||
"check_ip": "Alamat IP: {{ip}}",
|
||||
"check_cname": "CNAME: {{cname}}",
|
||||
"check_reason": "Alasan: {{reason}}",
|
||||
@@ -446,11 +518,50 @@
|
||||
"client_blocked": "Klien \"{{ip}}\" sukses di blokir",
|
||||
"client_unblocked": "Klien \"{{ip}}\" sukses di unblock",
|
||||
"static_ip": "Alamat IP statis",
|
||||
"static_ip_desc": "AdGuard Home adalah server jadi perlu alamat IP statis agar berfungsi dengan benar. Jika tidak, pada titik tertentu, router Anda dapat menetapkan alamat IP yang berbeda untuk perangkat ini.",
|
||||
"set_static_ip": "Atur alamat IP statik",
|
||||
"install_static_ok": "Kabar baik! Alamat IP statis sudah dikonfigurasi",
|
||||
"install_static_error": "AdGuard Home tidak dapat mengonfigurasinya secara otomatis untuk antarmuka jaringan ini. Silakan cari instruksi tentang cara melakukan ini secara manual.",
|
||||
"install_static_configure": "Kami mendeteksi alamat IP dinamis digunakan - <0>{{ip}}</0>. Anda ingin menggunakannya sebagai alamat statis Anda?",
|
||||
"confirm_static_ip": "AdGuard Home akan mengonfigurasi {{ip}} menjadi alamat IP statis Anda. Anda ingin melanjutkan?",
|
||||
"list_updated": "{{count}} daftar terbarui",
|
||||
"list_updated_plural": "{{count}} daftar terbarui",
|
||||
"dnssec_enable": "Aktifkan DNSSEC",
|
||||
"dnssec_enable_desc": "Atur bendera DNSSEC di permintaan keluar DNS dan periksa hasilnya (resolver berkemampuan DNSSEC diperlukan)",
|
||||
"validated_with_dnssec": "Tervalidasi dengan DNSSEC",
|
||||
"all_queries": "Semua permintaan",
|
||||
"show_blocked_responses": "Diblokir",
|
||||
"show_whitelisted_responses": "Dalam Daftar Putih",
|
||||
"show_processed_responses": "Terproses",
|
||||
"blocked_safebrowsing": "Terblokir oleh Safebrowsing",
|
||||
"blocked_adult_websites": "Situs Dewasa Terblokir",
|
||||
"blocked_threats": "Blokir Ancaman",
|
||||
"allowed": "Dibolehkan",
|
||||
"filtered": "Tersaring",
|
||||
"rewritten": "Tulis ulang",
|
||||
"safe_search": "Pencarian aman",
|
||||
"blocklist": "Daftar blokir",
|
||||
"milliseconds_abbreviation": "ms"
|
||||
"milliseconds_abbreviation": "ms",
|
||||
"cache_size": "Ukuran cache",
|
||||
"cache_size_desc": "Ukuran DNS cache (dalam bit)",
|
||||
"cache_ttl_min_override": "Tumpuk TTL minimum",
|
||||
"cache_ttl_max_override": "Tumpuk TTL maksimum",
|
||||
"enter_cache_size": "Masukkan ukuran cache",
|
||||
"enter_cache_ttl_min_override": "Masukkan TTL minimum",
|
||||
"enter_cache_ttl_max_override": "Masukkan TTL maksimum",
|
||||
"cache_ttl_min_override_desc": "Ganti nilai TTL (minimum) yang diterima dari server upstream. Nilai ini tidak boleh lebih dari 3600 (1 jam)",
|
||||
"cache_ttl_max_override_desc": "Ganti nilai TTL (maksimum) yang diterima dari server upstream",
|
||||
"min_exceeds_max_value": "Nilai minimum melebihi nilai maksimum",
|
||||
"value_not_larger_than": "Nilai tidak bisa lebih dari {{maximum}}",
|
||||
"filter_category_general": "Umum",
|
||||
"filter_category_security": "Keamanan",
|
||||
"filter_category_regional": "Wilayah",
|
||||
"filter_category_other": "Lainnya",
|
||||
"filter_category_general_desc": "Daftar yang memblokir pelacakan dan iklan di sebagian besar perangkat",
|
||||
"filter_category_security_desc": "Daftar yang khusus pada pemblokiran malware, phishing, atau domain penipuan",
|
||||
"filter_category_regional_desc": "Daftar yang berfokus pada iklan regional dan server pelacakan",
|
||||
"filter_category_other_desc": "Daftar hitam lain",
|
||||
"original_response": "Respon asli",
|
||||
"click_to_view_queries": "Klik untuk lihat permintaan",
|
||||
"port_53_faq_link": "Port 53 sering ditempati oleh layanan \"DNSStubListener\" atau \"systemd-resolved\". Silakan baca <0>instruksi ini</0> tentang cara menyelesaikan ini."
|
||||
}
|
||||
@@ -562,5 +562,6 @@
|
||||
"filter_category_regional_desc": "それぞれの地域の広告と追跡サーバをターゲットするリストです。",
|
||||
"filter_category_other_desc": "その他のブロックリストです。",
|
||||
"original_response": "当初の応答",
|
||||
"click_to_view_queries": "クエリを表示するにはクリックしてください"
|
||||
"click_to_view_queries": "クエリを表示するにはクリックしてください",
|
||||
"port_53_faq_link": "多くの場合、ポート53は \"DNSStubListener\" または \"systemd-resolved\" サービスによって利用されています。これを解決する方法については、<0>この手順</0>をお読みください。"
|
||||
}
|
||||
@@ -108,7 +108,7 @@
|
||||
"number_of_dns_query_to_safe_search": "Liczba żądań DNS do wyszukiwarek, dla których zastosowano wymuszenie bezpiecznego wyszukiwania",
|
||||
"average_processing_time": "Średni czas przetwarzania",
|
||||
"average_processing_time_hint": "Średni czas przetwarzania żądania DNS liczony w milisekundach",
|
||||
"block_domain_use_filters_and_hosts": "Blokuj domeny za pomocą filtrów i plików host",
|
||||
"block_domain_use_filters_and_hosts": "Zablokuj domeny za pomocą filtrów i plików host",
|
||||
"filters_block_toggle_hint": "Możesz skonfigurować reguły blokowania w ustawieniach <a href='#filters'>Filtry</a> ",
|
||||
"use_adguard_browsing_sec": "Użyj usługi sieciowej Bezpieczne Przeglądanie AdGuard",
|
||||
"use_adguard_browsing_sec_hint": "AdGuard Home sprawdzi, czy domena jest na czarnej liście przez serwis internetowy Bezpieczne Przeglądanie. Będzie korzystać z interfejsu API przyjaznego dla prywatności w celu przeprowadzenia kontroli: na serwer wysyłany jest tylko krótki prefiks nazwy domeny SHA256.",
|
||||
@@ -177,7 +177,7 @@
|
||||
"example_comment": "! Tutaj jest komentarz",
|
||||
"example_comment_meaning": "komentarz",
|
||||
"example_comment_hash": "# Również komentarz",
|
||||
"example_regex_meaning": "blokuj dostęp do domen pasujących do określonego wyrażenia regularnego",
|
||||
"example_regex_meaning": "zablokuj dostęp do domen pasujących do określonego wyrażenia regularnego",
|
||||
"example_upstream_regular": "normalny DNS (przez UDP)",
|
||||
"example_upstream_dot": "zaszyfrowany <0>DNS-over-TLS</0>",
|
||||
"example_upstream_doh": "zaszyfrowany <0>DNS-over-HTTPS</0>",
|
||||
@@ -427,7 +427,7 @@
|
||||
"form_error_answer_format": "Nieprawidłowy format odpowiedzi",
|
||||
"configure": "Skonfiguruj",
|
||||
"main_settings": "Ustawienia główne",
|
||||
"block_services": "Blokuj określone usługi",
|
||||
"block_services": "Zablokuj określone usługi",
|
||||
"blocked_services": "Zablokowane usługi",
|
||||
"blocked_services_desc": "Pozwala szybko zablokować popularne witryny i usługi.",
|
||||
"blocked_services_saved": "Zablokowane usługi zostały pomyślnie zapisane",
|
||||
@@ -444,7 +444,7 @@
|
||||
"stats_params": "Konfiguracja statystyk",
|
||||
"config_successfully_saved": "Konfiguracja została pomyślnie zapisana",
|
||||
"interval_24_hour": "24 godziny",
|
||||
"interval_days": "{{count}} dzień",
|
||||
"interval_days": "{{count}} dni",
|
||||
"interval_days_plural": "{{count}} dni",
|
||||
"domain": "Domena",
|
||||
"answer": "Odpowiedź",
|
||||
|
||||
@@ -530,7 +530,7 @@
|
||||
"dnssec_enable_desc": "Установите флаг DNSSEC в исходящих DNS-запросах и проверьте результат (требуется резолвер с поддержкой DNSSEC)",
|
||||
"validated_with_dnssec": "Подтверждено с помощью DNSSEC",
|
||||
"all_queries": "Все запросы",
|
||||
"show_blocked_responses": "Blocked",
|
||||
"show_blocked_responses": "Заблокировано",
|
||||
"show_whitelisted_responses": "В белом списке",
|
||||
"show_processed_responses": "Обработан",
|
||||
"blocked_safebrowsing": "Заблокировано согласно базе данных Safebrowsing",
|
||||
|
||||
@@ -562,5 +562,6 @@
|
||||
"filter_category_regional_desc": "Zoznamy zamerané na regionálne reklamy a sledovacie servery",
|
||||
"filter_category_other_desc": "Iné blokovacie zoznamy",
|
||||
"original_response": "Pôvodná odozva",
|
||||
"click_to_view_queries": "Kliknite pre zobrazenie dopytov"
|
||||
"click_to_view_queries": "Kliknite pre zobrazenie dopytov",
|
||||
"port_53_faq_link": "Port 53 je často obsadený službami \"DNSStubListener\" alebo \"systemd-resolved\". Prečítajte si <0>tento návod</0> o tom, ako to vyriešiť."
|
||||
}
|
||||
@@ -562,5 +562,6 @@
|
||||
"filter_category_regional_desc": "Lista koja se usredsređuje na regionalne reklame i servere praćenja",
|
||||
"filter_category_other_desc": "Ostale liste blokiranja",
|
||||
"original_response": "Izvorni odgovor",
|
||||
"click_to_view_queries": "Kliknite da pogledate zahteve"
|
||||
"click_to_view_queries": "Kliknite da pogledate zahteve",
|
||||
"port_53_faq_link": "Port 53 je najčešće zauzet od \"DNSStubListener\" ili \"systemd-resolved\" usluga. Pročitajte <0>ovo uputstvo</0> kako da to rešite."
|
||||
}
|
||||
@@ -239,7 +239,7 @@
|
||||
"rule_label": "Kural",
|
||||
"list_label": "Liste",
|
||||
"unknown_filter": "Bilinmeyen filtre {{filterId}}",
|
||||
"install_welcome_title": "AdGuard Home'a hoşgeldiniz!",
|
||||
"install_welcome_title": "AdGuard Home'a hoş geldiniz!",
|
||||
"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ü",
|
||||
|
||||
@@ -544,8 +544,8 @@
|
||||
"milliseconds_abbreviation": "ms",
|
||||
"cache_size": "快取大小",
|
||||
"cache_size_desc": "DNS 快取大小(以位元組)",
|
||||
"cache_ttl_min_override": "覆寫最小的存活時間(TTL)",
|
||||
"cache_ttl_max_override": "覆寫最大的存活時間(TTL)",
|
||||
"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)",
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { getIpMatchListStatus } from '../helpers/helpers';
|
||||
import { getIpMatchListStatus, sortIp } from '../helpers/helpers';
|
||||
import { IP_MATCH_LIST_STATUS } from '../helpers/constants';
|
||||
|
||||
describe('getIpMatchListStatus', () => {
|
||||
@@ -129,3 +129,356 @@ describe('getIpMatchListStatus', () => {
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('sortIp', () => {
|
||||
describe('ipv4', () => {
|
||||
test('one octet differ', () => {
|
||||
const arr = [
|
||||
'127.0.2.0',
|
||||
'127.0.3.0',
|
||||
'127.0.1.0',
|
||||
];
|
||||
const sortedArr = [
|
||||
'127.0.1.0',
|
||||
'127.0.2.0',
|
||||
'127.0.3.0',
|
||||
];
|
||||
expect(arr.sort(sortIp)).toStrictEqual(sortedArr);
|
||||
});
|
||||
test('few octets differ', () => {
|
||||
const arr = [
|
||||
'192.168.11.10',
|
||||
'192.168.10.0',
|
||||
'192.168.11.11',
|
||||
'192.168.10.10',
|
||||
'192.168.1.10',
|
||||
'192.168.0.1',
|
||||
'192.168.1.0',
|
||||
'192.168.1.1',
|
||||
'192.168.11.0',
|
||||
'192.168.0.10',
|
||||
'192.168.10.11',
|
||||
'192.168.0.11',
|
||||
'192.168.1.11',
|
||||
'192.168.0.0',
|
||||
'192.168.10.1',
|
||||
'192.168.11.1',
|
||||
];
|
||||
const sortedArr = [
|
||||
'192.168.0.0',
|
||||
'192.168.0.1',
|
||||
'192.168.0.10',
|
||||
'192.168.0.11',
|
||||
'192.168.1.0',
|
||||
'192.168.1.1',
|
||||
'192.168.1.10',
|
||||
'192.168.1.11',
|
||||
'192.168.10.0',
|
||||
'192.168.10.1',
|
||||
'192.168.10.10',
|
||||
'192.168.10.11',
|
||||
'192.168.11.0',
|
||||
'192.168.11.1',
|
||||
'192.168.11.10',
|
||||
'192.168.11.11',
|
||||
];
|
||||
expect(arr.sort(sortIp)).toStrictEqual(sortedArr);
|
||||
|
||||
// Example from issue https://github.com/AdguardTeam/AdGuardHome/issues/1778#issuecomment-640937599
|
||||
const arr2 = [
|
||||
'192.168.2.11',
|
||||
'192.168.3.1',
|
||||
'192.168.2.100',
|
||||
'192.168.2.2',
|
||||
'192.168.2.1',
|
||||
'192.168.2.10',
|
||||
'192.168.2.99',
|
||||
'192.168.2.200',
|
||||
'192.168.2.199',
|
||||
];
|
||||
const sortedArr2 = [
|
||||
'192.168.2.1',
|
||||
'192.168.2.2',
|
||||
'192.168.2.10',
|
||||
'192.168.2.11',
|
||||
'192.168.2.99',
|
||||
'192.168.2.100',
|
||||
'192.168.2.199',
|
||||
'192.168.2.200',
|
||||
'192.168.3.1',
|
||||
];
|
||||
expect(arr2.sort(sortIp)).toStrictEqual(sortedArr2);
|
||||
});
|
||||
});
|
||||
describe('ipv6', () => {
|
||||
test('only long form', () => {
|
||||
const arr = [
|
||||
'2001:db8:11a3:9d7:0:0:0:2',
|
||||
'2001:db8:11a3:9d7:0:0:0:3',
|
||||
'2001:db8:11a3:9d7:0:0:0:1',
|
||||
];
|
||||
const sortedArr = [
|
||||
'2001:db8:11a3:9d7:0:0:0:1',
|
||||
'2001:db8:11a3:9d7:0:0:0:2',
|
||||
'2001:db8:11a3:9d7:0:0:0:3',
|
||||
];
|
||||
expect(arr.sort(sortIp)).toStrictEqual(sortedArr);
|
||||
});
|
||||
test('only short form', () => {
|
||||
const arr = [
|
||||
'2001:db8::',
|
||||
'2001:db7::',
|
||||
'2001:db9::',
|
||||
];
|
||||
const sortedArr = [
|
||||
'2001:db7::',
|
||||
'2001:db8::',
|
||||
'2001:db9::',
|
||||
];
|
||||
expect(arr.sort(sortIp)).toStrictEqual(sortedArr);
|
||||
});
|
||||
test('long and short forms', () => {
|
||||
const arr = [
|
||||
'2001:db8::',
|
||||
'2001:db7:11a3:9d7:0:0:0:2',
|
||||
'2001:db6:11a3:9d7:0:0:0:1',
|
||||
'2001:db6::',
|
||||
'2001:db7:11a3:9d7:0:0:0:1',
|
||||
'2001:db7::',
|
||||
];
|
||||
const sortedArr = [
|
||||
'2001:db6::',
|
||||
'2001:db6:11a3:9d7:0:0:0:1',
|
||||
'2001:db7::',
|
||||
'2001:db7:11a3:9d7:0:0:0:1',
|
||||
'2001:db7:11a3:9d7:0:0:0:2',
|
||||
'2001:db8::',
|
||||
];
|
||||
expect(arr.sort(sortIp)).toStrictEqual(sortedArr);
|
||||
});
|
||||
});
|
||||
describe('ipv4 and ipv6', () => {
|
||||
test('ipv6 long form', () => {
|
||||
const arr = [
|
||||
'127.0.0.3',
|
||||
'2001:db8:11a3:9d7:0:0:0:1',
|
||||
'2001:db8:11a3:9d7:0:0:0:3',
|
||||
'127.0.0.1',
|
||||
'2001:db8:11a3:9d7:0:0:0:2',
|
||||
'127.0.0.2',
|
||||
];
|
||||
const sortedArr = [
|
||||
'127.0.0.1',
|
||||
'127.0.0.2',
|
||||
'127.0.0.3',
|
||||
'2001:db8:11a3:9d7:0:0:0:1',
|
||||
'2001:db8:11a3:9d7:0:0:0:2',
|
||||
'2001:db8:11a3:9d7:0:0:0:3',
|
||||
];
|
||||
expect(arr.sort(sortIp)).toStrictEqual(sortedArr);
|
||||
});
|
||||
test('ipv6 short form', () => {
|
||||
const arr = [
|
||||
'2001:db8:11a3:9d7::1',
|
||||
'127.0.0.3',
|
||||
'2001:db8:11a3:9d7::3',
|
||||
'127.0.0.1',
|
||||
'2001:db8:11a3:9d7::2',
|
||||
'127.0.0.2',
|
||||
];
|
||||
const sortedArr = [
|
||||
'127.0.0.1',
|
||||
'127.0.0.2',
|
||||
'127.0.0.3',
|
||||
'2001:db8:11a3:9d7::1',
|
||||
'2001:db8:11a3:9d7::2',
|
||||
'2001:db8:11a3:9d7::3',
|
||||
];
|
||||
expect(arr.sort(sortIp)).toStrictEqual(sortedArr);
|
||||
});
|
||||
test('ipv6 long and short forms', () => {
|
||||
const arr = [
|
||||
'2001:db8:11a3:9d7::1',
|
||||
'127.0.0.3',
|
||||
'2001:db8:11a3:9d7:0:0:0:2',
|
||||
'127.0.0.1',
|
||||
'2001:db8:11a3:9d7::3',
|
||||
'127.0.0.2',
|
||||
];
|
||||
const sortedArr = [
|
||||
'127.0.0.1',
|
||||
'127.0.0.2',
|
||||
'127.0.0.3',
|
||||
'2001:db8:11a3:9d7::1',
|
||||
'2001:db8:11a3:9d7:0:0:0:2',
|
||||
'2001:db8:11a3:9d7::3',
|
||||
];
|
||||
expect(arr.sort(sortIp)).toStrictEqual(sortedArr);
|
||||
});
|
||||
test('always put ipv4 before ipv6', () => {
|
||||
const arr = [
|
||||
'::1',
|
||||
'0.0.0.2',
|
||||
'127.0.0.1',
|
||||
'::2',
|
||||
'2001:db8:11a3:9d7:0:0:0:2',
|
||||
'0.0.0.1',
|
||||
'2001:db8:11a3:9d7::1',
|
||||
];
|
||||
const sortedArr = [
|
||||
'0.0.0.1',
|
||||
'0.0.0.2',
|
||||
'127.0.0.1',
|
||||
'::1',
|
||||
'::2',
|
||||
'2001:db8:11a3:9d7::1',
|
||||
'2001:db8:11a3:9d7:0:0:0:2',
|
||||
];
|
||||
expect(arr.sort(sortIp)).toStrictEqual(sortedArr);
|
||||
});
|
||||
});
|
||||
describe('cidr', () => {
|
||||
test('only ipv4 cidr', () => {
|
||||
const arr = [
|
||||
'192.168.0.1/9',
|
||||
'192.168.0.1/7',
|
||||
'192.168.0.1/8',
|
||||
];
|
||||
const sortedArr = [
|
||||
'192.168.0.1/7',
|
||||
'192.168.0.1/8',
|
||||
'192.168.0.1/9',
|
||||
];
|
||||
expect(arr.sort(sortIp)).toStrictEqual(sortedArr);
|
||||
});
|
||||
test('ipv4 and cidr ipv4', () => {
|
||||
const arr = [
|
||||
'192.168.0.1/9',
|
||||
'192.168.0.1',
|
||||
'192.168.0.1/32',
|
||||
'192.168.0.1/7',
|
||||
'192.168.0.1/8',
|
||||
];
|
||||
const sortedArr = [
|
||||
'192.168.0.1/7',
|
||||
'192.168.0.1/8',
|
||||
'192.168.0.1/9',
|
||||
'192.168.0.1/32',
|
||||
'192.168.0.1',
|
||||
];
|
||||
expect(arr.sort(sortIp)).toStrictEqual(sortedArr);
|
||||
});
|
||||
test('only ipv6 cidr', () => {
|
||||
const arr = [
|
||||
'2001:db8:11a3:9d7::1/32',
|
||||
'2001:db8:11a3:9d7::1/64',
|
||||
'2001:db8:11a3:9d7::1/128',
|
||||
'2001:db8:11a3:9d7::1/24',
|
||||
];
|
||||
const sortedArr = [
|
||||
'2001:db8:11a3:9d7::1/24',
|
||||
'2001:db8:11a3:9d7::1/32',
|
||||
'2001:db8:11a3:9d7::1/64',
|
||||
'2001:db8:11a3:9d7::1/128',
|
||||
];
|
||||
expect(arr.sort(sortIp)).toStrictEqual(sortedArr);
|
||||
});
|
||||
test('ipv6 and cidr ipv6', () => {
|
||||
const arr = [
|
||||
'2001:db8:11a3:9d7::1/32',
|
||||
'2001:db8:11a3:9d7::1',
|
||||
'2001:db8:11a3:9d7::1/64',
|
||||
'2001:db8:11a3:9d7::1/128',
|
||||
'2001:db8:11a3:9d7::1/24',
|
||||
];
|
||||
const sortedArr = [
|
||||
'2001:db8:11a3:9d7::1/24',
|
||||
'2001:db8:11a3:9d7::1/32',
|
||||
'2001:db8:11a3:9d7::1/64',
|
||||
'2001:db8:11a3:9d7::1/128',
|
||||
'2001:db8:11a3:9d7::1',
|
||||
];
|
||||
expect(arr.sort(sortIp)).toStrictEqual(sortedArr);
|
||||
});
|
||||
});
|
||||
describe('invalid input', () => {
|
||||
const originalError = console.error;
|
||||
|
||||
beforeEach(() => {
|
||||
console.error = jest.fn();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
expect(console.error).toHaveBeenCalled();
|
||||
console.error = originalError;
|
||||
});
|
||||
|
||||
test('invalid strings', () => {
|
||||
const arr = ['invalid ip', 'invalid cidr'];
|
||||
expect(arr.sort(sortIp)).toStrictEqual(arr);
|
||||
});
|
||||
test('invalid ip', () => {
|
||||
const arr = ['127.0.0.2.', '.127.0.0.1.', '.2001:db8:11a3:9d7:0:0:0:0'];
|
||||
expect(arr.sort(sortIp)).toStrictEqual(arr);
|
||||
});
|
||||
test('invalid cidr', () => {
|
||||
const arr = ['127.0.0.2/33', '2001:db8:11a3:9d7:0:0:0:0/129'];
|
||||
expect(arr.sort(sortIp)).toStrictEqual(arr);
|
||||
});
|
||||
test('valid and invalid ip', () => {
|
||||
const arr = ['127.0.0.4.', '127.0.0.1', '.127.0.0.3', '127.0.0.2'];
|
||||
expect(arr.sort(sortIp)).toStrictEqual(arr);
|
||||
});
|
||||
});
|
||||
describe('mixed', () => {
|
||||
test('ipv4, ipv6 in short and long forms and cidr', () => {
|
||||
const arr = [
|
||||
'2001:db8:11a3:9d7:0:0:0:1/32',
|
||||
'192.168.1.2',
|
||||
'127.0.0.2',
|
||||
'2001:db8:11a3:9d7::1/128',
|
||||
'2001:db8:11a3:9d7:0:0:0:1',
|
||||
'127.0.0.1/12',
|
||||
'192.168.1.1',
|
||||
'2001:db8::/32',
|
||||
'2001:db8:11a3:9d7::1/24',
|
||||
'192.168.1.2/12',
|
||||
'2001:db7::/32',
|
||||
'127.0.0.1',
|
||||
'2001:db8:11a3:9d7:0:0:0:2',
|
||||
'192.168.1.1/24',
|
||||
'2001:db7::/64',
|
||||
'2001:db7::',
|
||||
'2001:db8::',
|
||||
'2001:db8:11a3:9d7:0:0:0:1/128',
|
||||
'192.168.1.1/12',
|
||||
'127.0.0.1/32',
|
||||
'::1',
|
||||
];
|
||||
const sortedArr = [
|
||||
'127.0.0.1/12',
|
||||
'127.0.0.1/32',
|
||||
'127.0.0.1',
|
||||
'127.0.0.2',
|
||||
'192.168.1.1/12',
|
||||
'192.168.1.1/24',
|
||||
'192.168.1.1',
|
||||
'192.168.1.2/12',
|
||||
'192.168.1.2',
|
||||
'::1',
|
||||
'2001:db7::/32',
|
||||
'2001:db7::/64',
|
||||
'2001:db7::',
|
||||
'2001:db8::/32',
|
||||
'2001:db8::',
|
||||
'2001:db8:11a3:9d7::1/24',
|
||||
'2001:db8:11a3:9d7:0:0:0:1/32',
|
||||
'2001:db8:11a3:9d7::1/128',
|
||||
'2001:db8:11a3:9d7:0:0:0:1/128',
|
||||
'2001:db8:11a3:9d7:0:0:0:1',
|
||||
'2001:db8:11a3:9d7:0:0:0:2',
|
||||
];
|
||||
expect(arr.sort(sortIp)).toStrictEqual(sortedArr);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -148,7 +148,7 @@ const checkStatus = async (handleRequestSuccess, handleRequestError, attempts =
|
||||
const rmTimeout = (t) => t && clearTimeout(t);
|
||||
|
||||
try {
|
||||
const response = await axios.get('control/status');
|
||||
const response = await axios.get(`${apiClient.baseUrl}/status`);
|
||||
rmTimeout(timeout);
|
||||
if (response?.status === 200) {
|
||||
handleRequestSuccess(response);
|
||||
|
||||
@@ -2,9 +2,10 @@ import axios from 'axios';
|
||||
|
||||
import { getPathWithQueryString } from '../helpers/helpers';
|
||||
import { R_PATH_LAST_PART } from '../helpers/constants';
|
||||
import { BASE_URL } from '../../constants';
|
||||
|
||||
class Api {
|
||||
baseUrl = 'control';
|
||||
baseUrl = BASE_URL;
|
||||
|
||||
async makeRequest(path, method = 'POST', config) {
|
||||
try {
|
||||
@@ -26,18 +27,30 @@ class Api {
|
||||
|
||||
throw new Error(`${errorPath} | ${error.response.data} | ${error.response.status}`);
|
||||
}
|
||||
throw new Error(`${errorPath} | ${error.message ? error.message : error}`);
|
||||
throw new Error(`${errorPath} | ${error.message || error}`);
|
||||
}
|
||||
}
|
||||
|
||||
// Global methods
|
||||
GLOBAL_STATUS = { path: 'status', method: 'GET' };
|
||||
GLOBAL_STATUS = {
|
||||
path: 'status',
|
||||
method: 'GET',
|
||||
};
|
||||
|
||||
GLOBAL_TEST_UPSTREAM_DNS = { path: 'test_upstream_dns', method: 'POST' };
|
||||
GLOBAL_TEST_UPSTREAM_DNS = {
|
||||
path: 'test_upstream_dns',
|
||||
method: 'POST',
|
||||
};
|
||||
|
||||
GLOBAL_VERSION = { path: 'version.json', method: 'POST' };
|
||||
GLOBAL_VERSION = {
|
||||
path: 'version.json',
|
||||
method: 'POST',
|
||||
};
|
||||
|
||||
GLOBAL_UPDATE = { path: 'update', method: 'POST' };
|
||||
GLOBAL_UPDATE = {
|
||||
path: 'update',
|
||||
method: 'POST',
|
||||
};
|
||||
|
||||
getGlobalStatus() {
|
||||
const { path, method } = this.GLOBAL_STATUS;
|
||||
@@ -68,21 +81,45 @@ class Api {
|
||||
}
|
||||
|
||||
// Filtering
|
||||
FILTERING_STATUS = { path: 'filtering/status', method: 'GET' };
|
||||
FILTERING_STATUS = {
|
||||
path: 'filtering/status',
|
||||
method: 'GET',
|
||||
};
|
||||
|
||||
FILTERING_ADD_FILTER = { path: 'filtering/add_url', method: 'POST' };
|
||||
FILTERING_ADD_FILTER = {
|
||||
path: 'filtering/add_url',
|
||||
method: 'POST',
|
||||
};
|
||||
|
||||
FILTERING_REMOVE_FILTER = { path: 'filtering/remove_url', method: 'POST' };
|
||||
FILTERING_REMOVE_FILTER = {
|
||||
path: 'filtering/remove_url',
|
||||
method: 'POST',
|
||||
};
|
||||
|
||||
FILTERING_SET_RULES = { path: 'filtering/set_rules', method: 'POST' };
|
||||
FILTERING_SET_RULES = {
|
||||
path: 'filtering/set_rules',
|
||||
method: 'POST',
|
||||
};
|
||||
|
||||
FILTERING_REFRESH = { path: 'filtering/refresh', method: 'POST' };
|
||||
FILTERING_REFRESH = {
|
||||
path: 'filtering/refresh',
|
||||
method: 'POST',
|
||||
};
|
||||
|
||||
FILTERING_SET_URL = { path: 'filtering/set_url', method: 'POST' };
|
||||
FILTERING_SET_URL = {
|
||||
path: 'filtering/set_url',
|
||||
method: 'POST',
|
||||
};
|
||||
|
||||
FILTERING_CONFIG = { path: 'filtering/config', method: 'POST' };
|
||||
FILTERING_CONFIG = {
|
||||
path: 'filtering/config',
|
||||
method: 'POST',
|
||||
};
|
||||
|
||||
FILTERING_CHECK_HOST = { path: 'filtering/check_host', method: 'GET' };
|
||||
FILTERING_CHECK_HOST = {
|
||||
path: 'filtering/check_host',
|
||||
method: 'GET',
|
||||
};
|
||||
|
||||
getFilteringStatus() {
|
||||
const { path, method } = this.FILTERING_STATUS;
|
||||
@@ -153,11 +190,20 @@ class Api {
|
||||
}
|
||||
|
||||
// Parental
|
||||
PARENTAL_STATUS = { path: 'parental/status', method: 'GET' };
|
||||
PARENTAL_STATUS = {
|
||||
path: 'parental/status',
|
||||
method: 'GET',
|
||||
};
|
||||
|
||||
PARENTAL_ENABLE = { path: 'parental/enable', method: 'POST' };
|
||||
PARENTAL_ENABLE = {
|
||||
path: 'parental/enable',
|
||||
method: 'POST',
|
||||
};
|
||||
|
||||
PARENTAL_DISABLE = { path: 'parental/disable', method: 'POST' };
|
||||
PARENTAL_DISABLE = {
|
||||
path: 'parental/disable',
|
||||
method: 'POST',
|
||||
};
|
||||
|
||||
getParentalStatus() {
|
||||
const { path, method } = this.PARENTAL_STATUS;
|
||||
@@ -180,11 +226,20 @@ class Api {
|
||||
}
|
||||
|
||||
// Safebrowsing
|
||||
SAFEBROWSING_STATUS = { path: 'safebrowsing/status', method: 'GET' };
|
||||
SAFEBROWSING_STATUS = {
|
||||
path: 'safebrowsing/status',
|
||||
method: 'GET',
|
||||
};
|
||||
|
||||
SAFEBROWSING_ENABLE = { path: 'safebrowsing/enable', method: 'POST' };
|
||||
SAFEBROWSING_ENABLE = {
|
||||
path: 'safebrowsing/enable',
|
||||
method: 'POST',
|
||||
};
|
||||
|
||||
SAFEBROWSING_DISABLE = { path: 'safebrowsing/disable', method: 'POST' };
|
||||
SAFEBROWSING_DISABLE = {
|
||||
path: 'safebrowsing/disable',
|
||||
method: 'POST',
|
||||
};
|
||||
|
||||
getSafebrowsingStatus() {
|
||||
const { path, method } = this.SAFEBROWSING_STATUS;
|
||||
@@ -202,11 +257,20 @@ class Api {
|
||||
}
|
||||
|
||||
// Safesearch
|
||||
SAFESEARCH_STATUS = { path: 'safesearch/status', method: 'GET' };
|
||||
SAFESEARCH_STATUS = {
|
||||
path: 'safesearch/status',
|
||||
method: 'GET',
|
||||
};
|
||||
|
||||
SAFESEARCH_ENABLE = { path: 'safesearch/enable', method: 'POST' };
|
||||
SAFESEARCH_ENABLE = {
|
||||
path: 'safesearch/enable',
|
||||
method: 'POST',
|
||||
};
|
||||
|
||||
SAFESEARCH_DISABLE = { path: 'safesearch/disable', method: 'POST' };
|
||||
SAFESEARCH_DISABLE = {
|
||||
path: 'safesearch/disable',
|
||||
method: 'POST',
|
||||
};
|
||||
|
||||
getSafesearchStatus() {
|
||||
const { path, method } = this.SAFESEARCH_STATUS;
|
||||
@@ -224,9 +288,15 @@ class Api {
|
||||
}
|
||||
|
||||
// Language
|
||||
CURRENT_LANGUAGE = { path: 'i18n/current_language', method: 'GET' };
|
||||
CURRENT_LANGUAGE = {
|
||||
path: 'i18n/current_language',
|
||||
method: 'GET',
|
||||
};
|
||||
|
||||
CHANGE_LANGUAGE = { path: 'i18n/change_language', method: 'POST' };
|
||||
CHANGE_LANGUAGE = {
|
||||
path: 'i18n/change_language',
|
||||
method: 'POST',
|
||||
};
|
||||
|
||||
getCurrentLanguage() {
|
||||
const { path, method } = this.CURRENT_LANGUAGE;
|
||||
@@ -243,19 +313,40 @@ class Api {
|
||||
}
|
||||
|
||||
// DHCP
|
||||
DHCP_STATUS = { path: 'dhcp/status', method: 'GET' };
|
||||
DHCP_STATUS = {
|
||||
path: 'dhcp/status',
|
||||
method: 'GET',
|
||||
};
|
||||
|
||||
DHCP_SET_CONFIG = { path: 'dhcp/set_config', method: 'POST' };
|
||||
DHCP_SET_CONFIG = {
|
||||
path: 'dhcp/set_config',
|
||||
method: 'POST',
|
||||
};
|
||||
|
||||
DHCP_FIND_ACTIVE = { path: 'dhcp/find_active_dhcp', method: 'POST' };
|
||||
DHCP_FIND_ACTIVE = {
|
||||
path: 'dhcp/find_active_dhcp',
|
||||
method: 'POST',
|
||||
};
|
||||
|
||||
DHCP_INTERFACES = { path: 'dhcp/interfaces', method: 'GET' };
|
||||
DHCP_INTERFACES = {
|
||||
path: 'dhcp/interfaces',
|
||||
method: 'GET',
|
||||
};
|
||||
|
||||
DHCP_ADD_STATIC_LEASE = { path: 'dhcp/add_static_lease', method: 'POST' };
|
||||
DHCP_ADD_STATIC_LEASE = {
|
||||
path: 'dhcp/add_static_lease',
|
||||
method: 'POST',
|
||||
};
|
||||
|
||||
DHCP_REMOVE_STATIC_LEASE = { path: 'dhcp/remove_static_lease', method: 'POST' };
|
||||
DHCP_REMOVE_STATIC_LEASE = {
|
||||
path: 'dhcp/remove_static_lease',
|
||||
method: 'POST',
|
||||
};
|
||||
|
||||
DHCP_RESET = { path: 'dhcp/reset', method: 'POST' };
|
||||
DHCP_RESET = {
|
||||
path: 'dhcp/reset',
|
||||
method: 'POST',
|
||||
};
|
||||
|
||||
getDhcpStatus() {
|
||||
const { path, method } = this.DHCP_STATUS;
|
||||
@@ -309,11 +400,20 @@ class Api {
|
||||
}
|
||||
|
||||
// Installation
|
||||
INSTALL_GET_ADDRESSES = { path: 'install/get_addresses', method: 'GET' };
|
||||
INSTALL_GET_ADDRESSES = {
|
||||
path: 'install/get_addresses',
|
||||
method: 'GET',
|
||||
};
|
||||
|
||||
INSTALL_CONFIGURE = { path: 'install/configure', method: 'POST' };
|
||||
INSTALL_CONFIGURE = {
|
||||
path: 'install/configure',
|
||||
method: 'POST',
|
||||
};
|
||||
|
||||
INSTALL_CHECK_CONFIG = { path: 'install/check_config', method: 'POST' };
|
||||
INSTALL_CHECK_CONFIG = {
|
||||
path: 'install/check_config',
|
||||
method: 'POST',
|
||||
};
|
||||
|
||||
getDefaultAddresses() {
|
||||
const { path, method } = this.INSTALL_GET_ADDRESSES;
|
||||
@@ -339,11 +439,20 @@ class Api {
|
||||
}
|
||||
|
||||
// DNS-over-HTTPS and DNS-over-TLS
|
||||
TLS_STATUS = { path: 'tls/status', method: 'GET' };
|
||||
TLS_STATUS = {
|
||||
path: 'tls/status',
|
||||
method: 'GET',
|
||||
};
|
||||
|
||||
TLS_CONFIG = { path: 'tls/configure', method: 'POST' };
|
||||
TLS_CONFIG = {
|
||||
path: 'tls/configure',
|
||||
method: 'POST',
|
||||
};
|
||||
|
||||
TLS_VALIDATE = { path: 'tls/validate', method: 'POST' };
|
||||
TLS_VALIDATE = {
|
||||
path: 'tls/validate',
|
||||
method: 'POST',
|
||||
};
|
||||
|
||||
getTlsStatus() {
|
||||
const { path, method } = this.TLS_STATUS;
|
||||
@@ -369,15 +478,30 @@ class Api {
|
||||
}
|
||||
|
||||
// Per-client settings
|
||||
GET_CLIENTS = { path: 'clients', method: 'GET' };
|
||||
GET_CLIENTS = {
|
||||
path: 'clients',
|
||||
method: 'GET',
|
||||
};
|
||||
|
||||
FIND_CLIENTS = { path: 'clients/find', method: 'GET' };
|
||||
FIND_CLIENTS = {
|
||||
path: 'clients/find',
|
||||
method: 'GET',
|
||||
};
|
||||
|
||||
ADD_CLIENT = { path: 'clients/add', method: 'POST' };
|
||||
ADD_CLIENT = {
|
||||
path: 'clients/add',
|
||||
method: 'POST',
|
||||
};
|
||||
|
||||
DELETE_CLIENT = { path: 'clients/delete', method: 'POST' };
|
||||
DELETE_CLIENT = {
|
||||
path: 'clients/delete',
|
||||
method: 'POST',
|
||||
};
|
||||
|
||||
UPDATE_CLIENT = { path: 'clients/update', method: 'POST' };
|
||||
UPDATE_CLIENT = {
|
||||
path: 'clients/update',
|
||||
method: 'POST',
|
||||
};
|
||||
|
||||
getClients() {
|
||||
const { path, method } = this.GET_CLIENTS;
|
||||
@@ -418,9 +542,15 @@ class Api {
|
||||
}
|
||||
|
||||
// DNS access settings
|
||||
ACCESS_LIST = { path: 'access/list', method: 'GET' };
|
||||
ACCESS_LIST = {
|
||||
path: 'access/list',
|
||||
method: 'GET',
|
||||
};
|
||||
|
||||
ACCESS_SET = { path: 'access/set', method: 'POST' };
|
||||
ACCESS_SET = {
|
||||
path: 'access/set',
|
||||
method: 'POST',
|
||||
};
|
||||
|
||||
getAccessList() {
|
||||
const { path, method } = this.ACCESS_LIST;
|
||||
@@ -437,11 +567,20 @@ class Api {
|
||||
}
|
||||
|
||||
// DNS rewrites
|
||||
REWRITES_LIST = { path: 'rewrite/list', method: 'GET' };
|
||||
REWRITES_LIST = {
|
||||
path: 'rewrite/list',
|
||||
method: 'GET',
|
||||
};
|
||||
|
||||
REWRITE_ADD = { path: 'rewrite/add', method: 'POST' };
|
||||
REWRITE_ADD = {
|
||||
path: 'rewrite/add',
|
||||
method: 'POST',
|
||||
};
|
||||
|
||||
REWRITE_DELETE = { path: 'rewrite/delete', method: 'POST' };
|
||||
REWRITE_DELETE = {
|
||||
path: 'rewrite/delete',
|
||||
method: 'POST',
|
||||
};
|
||||
|
||||
getRewritesList() {
|
||||
const { path, method } = this.REWRITES_LIST;
|
||||
@@ -467,9 +606,15 @@ class Api {
|
||||
}
|
||||
|
||||
// Blocked services
|
||||
BLOCKED_SERVICES_LIST = { path: 'blocked_services/list', method: 'GET' };
|
||||
BLOCKED_SERVICES_LIST = {
|
||||
path: 'blocked_services/list',
|
||||
method: 'GET',
|
||||
};
|
||||
|
||||
BLOCKED_SERVICES_SET = { path: 'blocked_services/set', method: 'POST' };
|
||||
BLOCKED_SERVICES_SET = {
|
||||
path: 'blocked_services/set',
|
||||
method: 'POST',
|
||||
};
|
||||
|
||||
getBlockedServices() {
|
||||
const { path, method } = this.BLOCKED_SERVICES_LIST;
|
||||
@@ -486,13 +631,25 @@ class Api {
|
||||
}
|
||||
|
||||
// Settings for statistics
|
||||
GET_STATS = { path: 'stats', method: 'GET' };
|
||||
GET_STATS = {
|
||||
path: 'stats',
|
||||
method: 'GET',
|
||||
};
|
||||
|
||||
STATS_INFO = { path: 'stats_info', method: 'GET' };
|
||||
STATS_INFO = {
|
||||
path: 'stats_info',
|
||||
method: 'GET',
|
||||
};
|
||||
|
||||
STATS_CONFIG = { path: 'stats_config', method: 'POST' };
|
||||
STATS_CONFIG = {
|
||||
path: 'stats_config',
|
||||
method: 'POST',
|
||||
};
|
||||
|
||||
STATS_RESET = { path: 'stats_reset', method: 'POST' };
|
||||
STATS_RESET = {
|
||||
path: 'stats_reset',
|
||||
method: 'POST',
|
||||
};
|
||||
|
||||
getStats() {
|
||||
const { path, method } = this.GET_STATS;
|
||||
@@ -519,13 +676,25 @@ class Api {
|
||||
}
|
||||
|
||||
// Query log
|
||||
GET_QUERY_LOG = { path: 'querylog', method: 'GET' };
|
||||
GET_QUERY_LOG = {
|
||||
path: 'querylog',
|
||||
method: 'GET',
|
||||
};
|
||||
|
||||
QUERY_LOG_CONFIG = { path: 'querylog_config', method: 'POST' };
|
||||
QUERY_LOG_CONFIG = {
|
||||
path: 'querylog_config',
|
||||
method: 'POST',
|
||||
};
|
||||
|
||||
QUERY_LOG_INFO = { path: 'querylog_info', method: 'GET' };
|
||||
QUERY_LOG_INFO = {
|
||||
path: 'querylog_info',
|
||||
method: 'GET',
|
||||
};
|
||||
|
||||
QUERY_LOG_CLEAR = { path: 'querylog_clear', method: 'POST' };
|
||||
QUERY_LOG_CLEAR = {
|
||||
path: 'querylog_clear',
|
||||
method: 'POST',
|
||||
};
|
||||
|
||||
getQueryLog(params) {
|
||||
const { path, method } = this.GET_QUERY_LOG;
|
||||
@@ -553,7 +722,10 @@ class Api {
|
||||
}
|
||||
|
||||
// Login
|
||||
LOGIN = { path: 'login', method: 'POST' };
|
||||
LOGIN = {
|
||||
path: 'login',
|
||||
method: 'POST',
|
||||
};
|
||||
|
||||
login(data) {
|
||||
const { path, method } = this.LOGIN;
|
||||
@@ -565,7 +737,10 @@ class Api {
|
||||
}
|
||||
|
||||
// Profile
|
||||
GET_PROFILE = { path: 'profile', method: 'GET' };
|
||||
GET_PROFILE = {
|
||||
path: 'profile',
|
||||
method: 'GET',
|
||||
};
|
||||
|
||||
getProfile() {
|
||||
const { path, method } = this.GET_PROFILE;
|
||||
@@ -573,9 +748,15 @@ class Api {
|
||||
}
|
||||
|
||||
// DNS config
|
||||
GET_DNS_CONFIG = { path: 'dns_info', method: 'GET' };
|
||||
GET_DNS_CONFIG = {
|
||||
path: 'dns_info',
|
||||
method: 'GET',
|
||||
};
|
||||
|
||||
SET_DNS_CONFIG = { path: 'dns_config', method: 'POST' };
|
||||
SET_DNS_CONFIG = {
|
||||
path: 'dns_config',
|
||||
method: 'POST',
|
||||
};
|
||||
|
||||
getDnsConfig() {
|
||||
const { path, method } = this.GET_DNS_CONFIG;
|
||||
|
||||
@@ -28,10 +28,6 @@ body {
|
||||
}
|
||||
|
||||
@media screen and (max-width: 992px) {
|
||||
.container {
|
||||
padding: 0 !important;
|
||||
}
|
||||
|
||||
.container--wrap {
|
||||
min-height: calc(100vh);
|
||||
}
|
||||
|
||||
@@ -1,30 +1,16 @@
|
||||
import React, { Component, Fragment } from 'react';
|
||||
import React, { useEffect } from 'react';
|
||||
import { HashRouter, Route } from 'react-router-dom';
|
||||
import PropTypes from 'prop-types';
|
||||
import { withTranslation } from 'react-i18next';
|
||||
import LoadingBar from 'react-redux-loading-bar';
|
||||
import { hot } from 'react-hot-loader/root';
|
||||
|
||||
import 'react-table/react-table.css';
|
||||
import '../ui/Tabler.css';
|
||||
import '../ui/ReactTable.css';
|
||||
import './index.css';
|
||||
|
||||
import Header from '../../containers/Header';
|
||||
import Dashboard from '../../containers/Dashboard';
|
||||
import Settings from '../../containers/Settings';
|
||||
import { shallowEqual, useDispatch, useSelector } from 'react-redux';
|
||||
|
||||
import CustomRules from '../../containers/CustomRules';
|
||||
import DnsBlocklist from '../../containers/DnsBlocklist';
|
||||
import DnsAllowlist from '../../containers/DnsAllowlist';
|
||||
import DnsRewrites from '../../containers/DnsRewrites';
|
||||
|
||||
import Dns from '../../containers/Dns';
|
||||
import Encryption from '../../containers/Encryption';
|
||||
import Dhcp from '../../containers/Dhcp';
|
||||
import Clients from '../../containers/Clients';
|
||||
|
||||
import Logs from '../../containers/Logs';
|
||||
import SetupGuide from '../../containers/SetupGuide';
|
||||
import propTypes from 'prop-types';
|
||||
import Toasts from '../Toasts';
|
||||
import Footer from '../ui/Footer';
|
||||
import Status from '../ui/Status';
|
||||
@@ -35,31 +21,107 @@ import Icons from '../ui/Icons';
|
||||
import i18n from '../../i18n';
|
||||
import Loading from '../ui/Loading';
|
||||
import { FILTERS_URLS, MENU_URLS, SETTINGS_URLS } from '../../helpers/constants';
|
||||
import Services from '../Filters/Services';
|
||||
import { getLogsUrlParams, setHtmlLangAttr } from '../../helpers/helpers';
|
||||
import Header from '../Header';
|
||||
import { changeLanguage, getDnsStatus } from '../../actions';
|
||||
|
||||
class App extends Component {
|
||||
componentDidMount() {
|
||||
this.props.getDnsStatus();
|
||||
}
|
||||
import Dashboard from '../../containers/Dashboard';
|
||||
import Logs from '../../containers/Logs';
|
||||
import SetupGuide from '../../containers/SetupGuide';
|
||||
import Settings from '../../containers/Settings';
|
||||
import Dns from '../../containers/Dns';
|
||||
import Encryption from '../../containers/Encryption';
|
||||
import Dhcp from '../../containers/Dhcp';
|
||||
import Clients from '../../containers/Clients';
|
||||
import DnsBlocklist from '../../containers/DnsBlocklist';
|
||||
import DnsAllowlist from '../../containers/DnsAllowlist';
|
||||
import DnsRewrites from '../../containers/DnsRewrites';
|
||||
import CustomRules from '../../containers/CustomRules';
|
||||
import Services from '../Filters/Services';
|
||||
|
||||
componentDidUpdate(prevProps) {
|
||||
if (this.props.dashboard.language !== prevProps.dashboard.language) {
|
||||
this.setLanguage();
|
||||
}
|
||||
}
|
||||
const ROUTES = [
|
||||
{
|
||||
path: MENU_URLS.root,
|
||||
component: Dashboard,
|
||||
exact: true,
|
||||
},
|
||||
{
|
||||
path: [`${MENU_URLS.logs}${getLogsUrlParams(':search?', ':response_status?')}`, MENU_URLS.logs],
|
||||
component: Logs,
|
||||
},
|
||||
{
|
||||
path: MENU_URLS.guide,
|
||||
component: SetupGuide,
|
||||
},
|
||||
{
|
||||
path: SETTINGS_URLS.settings,
|
||||
component: Settings,
|
||||
},
|
||||
{
|
||||
path: SETTINGS_URLS.dns,
|
||||
component: Dns,
|
||||
},
|
||||
{
|
||||
path: SETTINGS_URLS.encryption,
|
||||
component: Encryption,
|
||||
},
|
||||
{
|
||||
path: SETTINGS_URLS.dhcp,
|
||||
component: Dhcp,
|
||||
},
|
||||
{
|
||||
path: SETTINGS_URLS.clients,
|
||||
component: Clients,
|
||||
},
|
||||
{
|
||||
path: FILTERS_URLS.dns_blocklists,
|
||||
component: DnsBlocklist,
|
||||
},
|
||||
{
|
||||
path: FILTERS_URLS.dns_allowlists,
|
||||
component: DnsAllowlist,
|
||||
},
|
||||
{
|
||||
path: FILTERS_URLS.dns_rewrites,
|
||||
component: DnsRewrites,
|
||||
},
|
||||
{
|
||||
path: FILTERS_URLS.custom_rules,
|
||||
component: CustomRules,
|
||||
},
|
||||
{
|
||||
path: FILTERS_URLS.blocked_services,
|
||||
component: Services,
|
||||
},
|
||||
];
|
||||
|
||||
reloadPage = () => {
|
||||
window.location.reload();
|
||||
};
|
||||
const renderRoute = ({ path, component, exact }, idx) => <Route
|
||||
key={idx}
|
||||
exact={exact}
|
||||
path={path}
|
||||
component={component}
|
||||
/>;
|
||||
|
||||
handleUpdate = () => {
|
||||
this.props.getUpdate();
|
||||
};
|
||||
const App = () => {
|
||||
const dispatch = useDispatch();
|
||||
const {
|
||||
language,
|
||||
isCoreRunning,
|
||||
isUpdateAvailable,
|
||||
processing,
|
||||
} = useSelector((state) => state.dashboard, shallowEqual);
|
||||
|
||||
setLanguage = () => {
|
||||
const { processing, language } = this.props.dashboard;
|
||||
const { processing: processingEncryption } = useSelector((
|
||||
state,
|
||||
) => state.encryption, shallowEqual);
|
||||
|
||||
const updateAvailable = isCoreRunning && isUpdateAvailable;
|
||||
|
||||
useEffect(() => {
|
||||
dispatch(getDnsStatus());
|
||||
}, []);
|
||||
|
||||
const setLanguage = () => {
|
||||
if (!processing) {
|
||||
if (language) {
|
||||
i18n.changeLanguage(language);
|
||||
@@ -68,93 +130,52 @@ class App extends Component {
|
||||
}
|
||||
|
||||
i18n.on('languageChanged', (lang) => {
|
||||
this.props.changeLanguage(lang);
|
||||
dispatch(changeLanguage(lang));
|
||||
});
|
||||
};
|
||||
|
||||
render() {
|
||||
const { dashboard, encryption, getVersion } = this.props;
|
||||
const updateAvailable = dashboard.isCoreRunning && dashboard.isUpdateAvailable;
|
||||
useEffect(() => {
|
||||
setLanguage();
|
||||
}, [language]);
|
||||
|
||||
return (
|
||||
<HashRouter hashType="noslash">
|
||||
<Fragment>
|
||||
{updateAvailable && (
|
||||
<Fragment>
|
||||
<UpdateTopline
|
||||
url={dashboard.announcementUrl}
|
||||
version={dashboard.newVersion}
|
||||
canAutoUpdate={dashboard.canAutoUpdate}
|
||||
getUpdate={this.handleUpdate}
|
||||
processingUpdate={dashboard.processingUpdate}
|
||||
/>
|
||||
<UpdateOverlay processingUpdate={dashboard.processingUpdate} />
|
||||
</Fragment>
|
||||
)}
|
||||
{!encryption.processing && (
|
||||
<EncryptionTopline notAfter={encryption.not_after} />
|
||||
)}
|
||||
<LoadingBar className="loading-bar" updateTime={1000} />
|
||||
<Route component={Header} />
|
||||
<div className="container container--wrap pb-5">
|
||||
{dashboard.processing && <Loading />}
|
||||
{!dashboard.isCoreRunning && (
|
||||
<div className="row row-cards">
|
||||
<div className="col-lg-12">
|
||||
<Status reloadPage={this.reloadPage}
|
||||
message="dns_start"
|
||||
/>
|
||||
<Loading />
|
||||
</div>
|
||||
const reloadPage = () => {
|
||||
window.location.reload();
|
||||
};
|
||||
|
||||
return (
|
||||
<HashRouter hashType="noslash">
|
||||
<>
|
||||
{updateAvailable && <>
|
||||
<UpdateTopline />
|
||||
<UpdateOverlay />
|
||||
</>}
|
||||
{!processingEncryption && <EncryptionTopline />}
|
||||
<LoadingBar className="loading-bar" updateTime={1000} />
|
||||
<Header />
|
||||
<div className="container container--wrap pb-5">
|
||||
{processing && <Loading />}
|
||||
{!isCoreRunning && (
|
||||
<div className="row row-cards">
|
||||
<div className="col-lg-12">
|
||||
<Status reloadPage={reloadPage} message="dns_start" />
|
||||
<Loading />
|
||||
</div>
|
||||
)}
|
||||
{!dashboard.processing && dashboard.isCoreRunning && (
|
||||
<>
|
||||
<Route path={MENU_URLS.root} exact component={Dashboard} />
|
||||
<Route
|
||||
path={[`${MENU_URLS.logs}${getLogsUrlParams(':search?', ':response_status?')}`, MENU_URLS.logs]}
|
||||
component={Logs} />
|
||||
<Route path={MENU_URLS.guide} component={SetupGuide} />
|
||||
<Route path={SETTINGS_URLS.settings} component={Settings} />
|
||||
<Route path={SETTINGS_URLS.dns} component={Dns} />
|
||||
<Route path={SETTINGS_URLS.encryption} component={Encryption} />
|
||||
<Route path={SETTINGS_URLS.dhcp} component={Dhcp} />
|
||||
<Route path={SETTINGS_URLS.clients} component={Clients} />
|
||||
<Route path={FILTERS_URLS.dns_blocklists}
|
||||
component={DnsBlocklist} />
|
||||
<Route path={FILTERS_URLS.dns_allowlists}
|
||||
component={DnsAllowlist} />
|
||||
<Route path={FILTERS_URLS.dns_rewrites} component={DnsRewrites} />
|
||||
<Route path={FILTERS_URLS.custom_rules} component={CustomRules} />
|
||||
<Route path={FILTERS_URLS.blocked_services} component={Services} />
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
<Footer
|
||||
dnsVersion={dashboard.dnsVersion}
|
||||
dnsPort={dashboard.dnsPort}
|
||||
processingVersion={dashboard.processingVersion}
|
||||
getVersion={getVersion}
|
||||
checkUpdateFlag={dashboard.checkUpdateFlag}
|
||||
/>
|
||||
<Toasts />
|
||||
<Icons />
|
||||
</Fragment>
|
||||
</HashRouter>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
App.propTypes = {
|
||||
getDnsStatus: PropTypes.func,
|
||||
getUpdate: PropTypes.func,
|
||||
enableDns: PropTypes.func,
|
||||
dashboard: PropTypes.object,
|
||||
isCoreRunning: PropTypes.bool,
|
||||
error: PropTypes.string,
|
||||
changeLanguage: PropTypes.func,
|
||||
encryption: PropTypes.object,
|
||||
getVersion: PropTypes.func,
|
||||
</div>
|
||||
)}
|
||||
{!processing && isCoreRunning && ROUTES.map(renderRoute)}
|
||||
</div>
|
||||
<Footer />
|
||||
<Toasts />
|
||||
<Icons />
|
||||
</>
|
||||
</HashRouter>
|
||||
);
|
||||
};
|
||||
|
||||
export default withTranslation()(App);
|
||||
renderRoute.propTypes = {
|
||||
path: propTypes.oneOfType([propTypes.string, propTypes.arrayOf(propTypes.string)]).isRequired,
|
||||
component: propTypes.element.isRequired,
|
||||
exact: propTypes.bool,
|
||||
};
|
||||
|
||||
export default hot(App);
|
||||
|
||||
@@ -6,7 +6,7 @@ import { Trans, withTranslation } from 'react-i18next';
|
||||
import Card from '../ui/Card';
|
||||
import Cell from '../ui/Cell';
|
||||
|
||||
import { getPercent, getIpMatchListStatus } from '../../helpers/helpers';
|
||||
import { getPercent, getIpMatchListStatus, sortIp } from '../../helpers/helpers';
|
||||
import { IP_MATCH_LIST_STATUS, STATUS_COLORS } from '../../helpers/constants';
|
||||
import { formatClientCell } from '../../helpers/formatClientCell';
|
||||
|
||||
@@ -99,7 +99,7 @@ const Clients = ({
|
||||
{
|
||||
Header: 'IP',
|
||||
accessor: 'ip',
|
||||
sortMethod: (a, b) => parseInt(a.replace(/\./g, ''), 10) - parseInt(b.replace(/\./g, ''), 10),
|
||||
sortMethod: sortIp,
|
||||
Cell: clientCell(t, toggleClientStatus, processingAccessSet, disallowedClients),
|
||||
},
|
||||
{
|
||||
|
||||
@@ -21,7 +21,7 @@ const Row = ({
|
||||
<Trans components={translationComponents}>{label}</Trans>
|
||||
<Tooltip content={tooltipTitle} placement="top"
|
||||
className="tooltip-container tooltip-custom--narrow text-center">
|
||||
<svg className="icons icon--20 icon--lightgray ml-1">
|
||||
<svg className="icons icon--20 icon--lightgray ml-2">
|
||||
<use xlinkHref="#question" />
|
||||
</svg>
|
||||
</Tooltip>
|
||||
|
||||
@@ -62,23 +62,13 @@ class Table extends Component {
|
||||
showPagination
|
||||
defaultPageSize={10}
|
||||
minRows={5}
|
||||
previousText={
|
||||
<svg className="icons icon--24 icon--gray">
|
||||
<use xlinkHref="#arrow-left" />
|
||||
</svg>}
|
||||
nextText={
|
||||
<svg className="icons icon--24 icon--gray">
|
||||
<use xlinkHref="#arrow-right" />
|
||||
</svg>}
|
||||
loadingText={t('loading_table_status')}
|
||||
pageText=''
|
||||
ofText=''
|
||||
ofText="/"
|
||||
previousText={t('previous_btn')}
|
||||
nextText={t('next_btn')}
|
||||
pageText={t('page_table_footer_text')}
|
||||
rowsText={t('rows_table_footer_text')}
|
||||
loadingText={t('loading_table_status')}
|
||||
noDataText={t('rewrite_not_found')}
|
||||
showPageSizeOptions={false}
|
||||
showPageJump={false}
|
||||
renderTotalPagesCount={() => false}
|
||||
getPaginationProps={() => ({ className: 'custom-pagination' })}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -59,6 +59,4 @@ const Services = () => {
|
||||
);
|
||||
};
|
||||
|
||||
Services.propTypes = {};
|
||||
|
||||
export default Services;
|
||||
|
||||
@@ -128,24 +128,15 @@ class Table extends Component {
|
||||
columns={this.columns}
|
||||
showPagination
|
||||
defaultPageSize={10}
|
||||
showPageSizeOptions={false}
|
||||
showPageJump={false}
|
||||
renderTotalPagesCount={() => false}
|
||||
loading={loading}
|
||||
minRows={6}
|
||||
pageText=''
|
||||
ofText=''
|
||||
ofText="/"
|
||||
previousText={t('previous_btn')}
|
||||
nextText={t('next_btn')}
|
||||
pageText={t('page_table_footer_text')}
|
||||
rowsText={t('rows_table_footer_text')}
|
||||
loadingText={t('loading_table_status')}
|
||||
noDataText={whitelist ? t('no_whitelist_added') : t('no_blocklist_added')}
|
||||
getPaginationProps={() => ({ className: 'custom-pagination' })}
|
||||
previousText={
|
||||
<svg className="icons icon--24 icon--gray w-100 h-100">
|
||||
<use xlinkHref="#arrow-left" />
|
||||
</svg>}
|
||||
nextText={
|
||||
<svg className="icons icon--24 icon--gray w-100 h-100">
|
||||
<use xlinkHref="#arrow-right" />
|
||||
</svg>}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -90,9 +90,8 @@ class Menu extends Component {
|
||||
};
|
||||
|
||||
getActiveClassForDropdown = (URLS) => {
|
||||
const { pathname } = this.props.location;
|
||||
const isActivePage = Object.values(URLS)
|
||||
.some((item) => item === pathname);
|
||||
.some((item) => item === this.props.pathname);
|
||||
|
||||
return isActivePage ? 'active' : '';
|
||||
};
|
||||
@@ -180,9 +179,9 @@ class Menu extends Component {
|
||||
}
|
||||
|
||||
Menu.propTypes = {
|
||||
isMenuOpen: PropTypes.bool,
|
||||
closeMenu: PropTypes.func,
|
||||
location: PropTypes.object,
|
||||
isMenuOpen: PropTypes.bool.isRequired,
|
||||
closeMenu: PropTypes.func.isRequired,
|
||||
pathname: PropTypes.string.isRequired,
|
||||
t: PropTypes.func,
|
||||
};
|
||||
|
||||
|
||||
@@ -1,83 +1,77 @@
|
||||
import React, { Component } from 'react';
|
||||
import { Link } from 'react-router-dom';
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { useState } from 'react';
|
||||
import { Link, useLocation } from 'react-router-dom';
|
||||
import { shallowEqual, useSelector } from 'react-redux';
|
||||
import { Trans } from 'react-i18next';
|
||||
import classnames from 'classnames';
|
||||
import { Trans, withTranslation } from 'react-i18next';
|
||||
|
||||
import Menu from './Menu';
|
||||
import logo from '../ui/svg/logo.svg';
|
||||
import './Header.css';
|
||||
|
||||
class Header extends Component {
|
||||
state = {
|
||||
isMenuOpen: false,
|
||||
const Header = () => {
|
||||
const [isMenuOpen, setIsMenuOpen] = useState(false);
|
||||
|
||||
const {
|
||||
protectionEnabled,
|
||||
processing,
|
||||
isCoreRunning,
|
||||
processingProfile,
|
||||
name,
|
||||
} = useSelector((state) => state.dashboard, shallowEqual);
|
||||
|
||||
const { pathname } = useLocation();
|
||||
|
||||
const toggleMenuOpen = () => {
|
||||
setIsMenuOpen((isMenuOpen) => !isMenuOpen);
|
||||
};
|
||||
|
||||
toggleMenuOpen = () => {
|
||||
this.setState((prevState) => ({ isMenuOpen: !prevState.isMenuOpen }));
|
||||
const closeMenu = () => {
|
||||
setIsMenuOpen(false);
|
||||
};
|
||||
|
||||
closeMenu = () => {
|
||||
this.setState({ isMenuOpen: false });
|
||||
};
|
||||
const badgeClass = classnames('badge dns-status', {
|
||||
'badge-success': protectionEnabled,
|
||||
'badge-danger': !protectionEnabled,
|
||||
});
|
||||
|
||||
render() {
|
||||
const { dashboard, location } = this.props;
|
||||
const { isMenuOpen } = this.state;
|
||||
const badgeClass = classnames({
|
||||
'badge dns-status': true,
|
||||
'badge-success': dashboard.protectionEnabled,
|
||||
'badge-danger': !dashboard.protectionEnabled,
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="header">
|
||||
<div className="header__container">
|
||||
<div className="header__row">
|
||||
<div
|
||||
className="header-toggler d-lg-none ml-lg-0 collapsed"
|
||||
onClick={this.toggleMenuOpen}
|
||||
>
|
||||
<span className="header-toggler-icon" />
|
||||
</div>
|
||||
<div className="header__column">
|
||||
<div className="d-flex align-items-center">
|
||||
<Link to="/" className="nav-link pl-0 pr-1">
|
||||
<img src={logo} alt="" className="header-brand-img" />
|
||||
</Link>
|
||||
{!dashboard.processing && dashboard.isCoreRunning && (
|
||||
<span className={badgeClass}>
|
||||
<Trans>{dashboard.protectionEnabled ? 'on' : 'off'}</Trans>
|
||||
return (
|
||||
<div className="header">
|
||||
<div className="header__container">
|
||||
<div className="header__row">
|
||||
<div
|
||||
className="header-toggler d-lg-none ml-lg-0 collapsed"
|
||||
onClick={toggleMenuOpen}
|
||||
>
|
||||
<span className="header-toggler-icon" />
|
||||
</div>
|
||||
<div className="header__column">
|
||||
<div className="d-flex align-items-center">
|
||||
<Link to="/" className="nav-link pl-0 pr-1">
|
||||
<img src={logo} alt="" className="header-brand-img" />
|
||||
</Link>
|
||||
{!processing && isCoreRunning && (
|
||||
<span className={badgeClass}>
|
||||
<Trans>{protectionEnabled ? 'on' : 'off'}</Trans>
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<Menu
|
||||
location={location}
|
||||
isMenuOpen={isMenuOpen}
|
||||
closeMenu={this.closeMenu}
|
||||
/>
|
||||
<div className="header__column">
|
||||
<div className="header__right">
|
||||
{!dashboard.processingProfile && dashboard.name
|
||||
&& <a href="control/logout" className="btn btn-sm btn-outline-secondary">
|
||||
<Trans>sign_out</Trans>
|
||||
</a>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
<Menu
|
||||
pathname={pathname}
|
||||
isMenuOpen={isMenuOpen}
|
||||
closeMenu={closeMenu}
|
||||
/>
|
||||
<div className="header__column">
|
||||
<div className="header__right">
|
||||
{!processingProfile && name
|
||||
&& <a href="control/logout" className="btn btn-sm btn-outline-secondary">
|
||||
<Trans>sign_out</Trans>
|
||||
</a>}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Header.propTypes = {
|
||||
dashboard: PropTypes.object.isRequired,
|
||||
location: PropTypes.object.isRequired,
|
||||
getVersion: PropTypes.func.isRequired,
|
||||
t: PropTypes.func.isRequired,
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default withTranslation()(Header);
|
||||
export default Header;
|
||||
|
||||
@@ -16,6 +16,7 @@ const getClientCell = ({
|
||||
|
||||
const autoClient = autoClients.find((autoClient) => autoClient.name === client);
|
||||
const source = autoClient?.source;
|
||||
const whoisAvailable = whois_info && Object.keys(whois_info).length > 0;
|
||||
|
||||
const id = nanoid();
|
||||
|
||||
@@ -33,7 +34,7 @@ const getClientCell = ({
|
||||
const isFiltered = checkFiltered(reason);
|
||||
|
||||
const nameClass = classNames('w-90 o-hidden d-flex flex-column', {
|
||||
'mt-2': isDetailed && !name && !whois_info,
|
||||
'mt-2': isDetailed && !name && !whoisAvailable,
|
||||
'white-space--nowrap': isDetailed,
|
||||
});
|
||||
|
||||
@@ -78,12 +79,19 @@ const getClientCell = ({
|
||||
content: processedData,
|
||||
placement: 'bottom',
|
||||
})}
|
||||
<div
|
||||
className={nameClass}>
|
||||
<div data-tip={true} data-for={id}>{formatClientCell(row, isDetailed)}</div>
|
||||
{isDetailed && name
|
||||
&& !whois_info && <div className="detailed-info d-none d-sm-block logs__text"
|
||||
title={name}>{name}</div>}
|
||||
<div className={nameClass}>
|
||||
<div data-tip={true} data-for={id}>
|
||||
{formatClientCell(row, isDetailed)}
|
||||
</div>
|
||||
|
||||
{isDetailed && name && !whoisAvailable && (
|
||||
<div
|
||||
className="detailed-info d-none d-sm-block logs__text"
|
||||
title={name}
|
||||
>
|
||||
{name}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{renderBlockingButton(isFiltered, domain)}
|
||||
</div>
|
||||
|
||||
@@ -38,59 +38,20 @@ const getResponseCell = (row, filtering, t, isDetailed, getFilterName) => {
|
||||
})}</div>;
|
||||
};
|
||||
|
||||
const FILTERED_STATUS_TO_FIELDS_MAP = {
|
||||
[FILTERED_STATUS.NOT_FILTERED_NOT_FOUND]: {
|
||||
encryption_status: boldStatusLabel,
|
||||
install_settings_dns: upstream,
|
||||
elapsed: formattedElapsedMs,
|
||||
response_code: status,
|
||||
response_table_header: renderResponses(response),
|
||||
},
|
||||
[FILTERED_STATUS.FILTERED_BLOCKED_SERVICE]: {
|
||||
encryption_status: boldStatusLabel,
|
||||
install_settings_dns: upstream,
|
||||
elapsed: formattedElapsedMs,
|
||||
filter,
|
||||
rule_label: rule,
|
||||
response_code: status,
|
||||
original_response: renderResponses(originalResponse),
|
||||
},
|
||||
[FILTERED_STATUS.NOT_FILTERED_WHITE_LIST]: {
|
||||
encryption_status: boldStatusLabel,
|
||||
install_settings_dns: upstream,
|
||||
elapsed: formattedElapsedMs,
|
||||
filter,
|
||||
rule_label: rule,
|
||||
response_code: status,
|
||||
},
|
||||
[FILTERED_STATUS.NOT_FILTERED_WHITE_LIST]: {
|
||||
encryption_status: boldStatusLabel,
|
||||
filter,
|
||||
rule_label: rule,
|
||||
response_code: status,
|
||||
},
|
||||
[FILTERED_STATUS.FILTERED_SAFE_SEARCH]: {
|
||||
encryption_status: boldStatusLabel,
|
||||
install_settings_dns: upstream,
|
||||
elapsed: formattedElapsedMs,
|
||||
response_code: status,
|
||||
response_table_header: renderResponses(response),
|
||||
},
|
||||
[FILTERED_STATUS.FILTERED_BLACK_LIST]: {
|
||||
encryption_status: boldStatusLabel,
|
||||
filter,
|
||||
rule_label: rule,
|
||||
install_settings_dns: upstream,
|
||||
elapsed: formattedElapsedMs,
|
||||
response_code: status,
|
||||
original_response: renderResponses(originalResponse),
|
||||
},
|
||||
const COMMON_CONTENT = {
|
||||
encryption_status: boldStatusLabel,
|
||||
install_settings_dns: upstream,
|
||||
elapsed: formattedElapsedMs,
|
||||
response_code: status,
|
||||
filter,
|
||||
rule_label: rule,
|
||||
response_table_header: renderResponses(response),
|
||||
original_response: renderResponses(originalResponse),
|
||||
};
|
||||
|
||||
const content = FILTERED_STATUS_TO_FIELDS_MAP[reason]
|
||||
? Object.entries(FILTERED_STATUS_TO_FIELDS_MAP[reason])
|
||||
: Object.entries(FILTERED_STATUS_TO_FIELDS_MAP.NotFilteredNotFound);
|
||||
|
||||
const content = rule
|
||||
? Object.entries(COMMON_CONTENT)
|
||||
: Object.entries({ ...COMMON_CONTENT, filter: '' });
|
||||
const detailedInfo = isBlocked ? filter : formattedElapsedMs;
|
||||
|
||||
return (
|
||||
|
||||
@@ -143,10 +143,11 @@ const Form = (props) => {
|
||||
const normalizeOnBlur = (data) => data.trim();
|
||||
|
||||
return (
|
||||
<form className="d-flex flex-wrap form-control--container"
|
||||
onSubmit={(e) => {
|
||||
e.preventDefault();
|
||||
}}
|
||||
<form
|
||||
className="d-flex flex-wrap form-control--container"
|
||||
onSubmit={(e) => {
|
||||
e.preventDefault();
|
||||
}}
|
||||
>
|
||||
<div className="field__search">
|
||||
<Field
|
||||
@@ -166,13 +167,21 @@ const Form = (props) => {
|
||||
<Field
|
||||
name={FORM_NAMES.response_status}
|
||||
component="select"
|
||||
className={classNames('form-control custom-select custom-select--logs custom-select__arrow--left ml-small form-control--transparent', responseStatusClass)}
|
||||
className={classNames('form-control custom-select custom-select--logs custom-select__arrow--left form-control--transparent', responseStatusClass)}
|
||||
>
|
||||
{Object.values(RESPONSE_FILTER)
|
||||
.map(({
|
||||
query, label, disabled,
|
||||
}) => <option key={label} value={query}
|
||||
disabled={disabled}>{t(label)}</option>)}
|
||||
}) => (
|
||||
<option
|
||||
key={label}
|
||||
value={query}
|
||||
disabled={disabled}
|
||||
>
|
||||
{t(label)}
|
||||
</option>
|
||||
))
|
||||
}
|
||||
</Field>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
@@ -9,7 +9,7 @@ const Filters = ({ filter, refreshLogs, setIsLoading }) => (
|
||||
<Trans>query_log</Trans>
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-icon--green ml-3 bg-transparent"
|
||||
className="btn btn-icon--green logs__refresh"
|
||||
onClick={refreshLogs}
|
||||
>
|
||||
<svg className="icons icon--24">
|
||||
|
||||
@@ -58,8 +58,7 @@
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.logs__text--wrap,
|
||||
.logs__text--whois {
|
||||
.logs__text--wrap {
|
||||
line-height: 1.4;
|
||||
white-space: normal;
|
||||
}
|
||||
@@ -71,6 +70,7 @@
|
||||
|
||||
.logs__text--whois {
|
||||
line-height: 1.2;
|
||||
color: #9aa0ac;
|
||||
}
|
||||
|
||||
.logs__row .tooltip-custom {
|
||||
@@ -362,7 +362,7 @@
|
||||
display: flex !important;
|
||||
}
|
||||
|
||||
.-pageInfo {
|
||||
.logs__table .-pageInfo {
|
||||
--side-size: 2rem;
|
||||
font-variant-numeric: tabular-nums !important;
|
||||
background-color: transparent !important;
|
||||
@@ -376,18 +376,18 @@
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.pagination-bottom {
|
||||
.logs__table .pagination-bottom {
|
||||
justify-content: center !important;
|
||||
display: flex !important;
|
||||
}
|
||||
|
||||
.-center:before {
|
||||
.logs__table .-center:before {
|
||||
content: '...';
|
||||
transform: translateY(-0.25rem);
|
||||
margin: auto;
|
||||
}
|
||||
|
||||
.-center:after {
|
||||
.logs__table .-center:after {
|
||||
content: '...';
|
||||
transform: translateY(-0.25rem);
|
||||
margin: auto;
|
||||
@@ -437,12 +437,13 @@
|
||||
}
|
||||
|
||||
.custom-select__arrow--left {
|
||||
background: #fff url('./chevron-down.svg') no-repeat left 0.2rem center;
|
||||
background-size: 1.5rem;
|
||||
background: var(--white) url('../ui/svg/chevron-down.svg') no-repeat;
|
||||
background-position: 5px 9px;
|
||||
background-size: 22px;
|
||||
}
|
||||
|
||||
.custom-select--logs {
|
||||
padding: 0.5rem 0.75rem 0.5rem 1.75rem !important;
|
||||
padding: 0.5rem 0.75rem 0.5rem 2rem !important;
|
||||
}
|
||||
|
||||
.bg--danger {
|
||||
@@ -511,6 +512,8 @@
|
||||
|
||||
.field__select {
|
||||
margin-top: 1.5rem;
|
||||
padding-left: 24px;
|
||||
padding-right: 24px;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -536,3 +539,16 @@
|
||||
.loading__text {
|
||||
transform: translateY(3rem);
|
||||
}
|
||||
|
||||
.logs__refresh {
|
||||
position: relative;
|
||||
top: 3px;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
padding: 0;
|
||||
margin-left: 15px;
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
@@ -355,7 +355,7 @@ const Table = (props) => {
|
||||
response_details: 'title',
|
||||
install_settings_dns: upstream,
|
||||
elapsed: formattedElapsedMs,
|
||||
filter: isBlocked ? filter : null,
|
||||
filter: rule ? filter : null,
|
||||
rule_label: rule,
|
||||
response_table_header: response?.join('\n'),
|
||||
response_code: status,
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="#9aa0ac" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-chevron-down"><polyline points="6 9 12 15 18 9"></polyline></svg>
|
||||
|
Before Width: | Height: | Size: 264 B |
@@ -10,7 +10,7 @@ import {
|
||||
BLOCK_ACTIONS,
|
||||
TABLE_DEFAULT_PAGE_SIZE,
|
||||
TABLE_FIRST_PAGE,
|
||||
smallScreenSize,
|
||||
SMALL_SCREEN_SIZE,
|
||||
} from '../../helpers/constants';
|
||||
import Loading from '../ui/Loading';
|
||||
import Filters from './Filters';
|
||||
@@ -76,7 +76,7 @@ const Logs = (props) => {
|
||||
const search = filter?.search || search_url_param;
|
||||
const response_status = filter?.response_status || response_status_url_param;
|
||||
|
||||
const [isSmallScreen, setIsSmallScreen] = useState(window.innerWidth < smallScreenSize);
|
||||
const [isSmallScreen, setIsSmallScreen] = useState(window.innerWidth < SMALL_SCREEN_SIZE);
|
||||
const [detailedDataCurrent, setDetailedDataCurrent] = useState({});
|
||||
const [buttonType, setButtonType] = useState(BLOCK_ACTIONS.BLOCK);
|
||||
const [isModalOpened, setModalOpened] = useState(false);
|
||||
@@ -114,7 +114,7 @@ const Logs = (props) => {
|
||||
},
|
||||
} = props;
|
||||
|
||||
const mediaQuery = window.matchMedia(`(max-width: ${smallScreenSize}px)`);
|
||||
const mediaQuery = window.matchMedia(`(max-width: ${SMALL_SCREEN_SIZE}px)`);
|
||||
const mediaQueryHandler = (e) => {
|
||||
setIsSmallScreen(e.matches);
|
||||
if (e.matches) {
|
||||
|
||||
@@ -8,6 +8,7 @@ import CellWrap from '../../ui/CellWrap';
|
||||
|
||||
import whoisCell from './whoisCell';
|
||||
import LogsSearchLink from '../../ui/LogsSearchLink';
|
||||
import { sortIp } from '../../../helpers/helpers';
|
||||
|
||||
const COLUMN_MIN_WIDTH = 200;
|
||||
|
||||
@@ -18,6 +19,7 @@ class AutoClients extends Component {
|
||||
accessor: 'ip',
|
||||
minWidth: COLUMN_MIN_WIDTH,
|
||||
Cell: CellWrap,
|
||||
sortMethod: sortIp,
|
||||
},
|
||||
{
|
||||
Header: this.props.t('table_name'),
|
||||
@@ -85,23 +87,13 @@ class AutoClients extends Component {
|
||||
showPagination
|
||||
defaultPageSize={10}
|
||||
minRows={5}
|
||||
showPageSizeOptions={false}
|
||||
showPageJump={false}
|
||||
renderTotalPagesCount={() => false}
|
||||
previousText={
|
||||
<svg className="icons icon--24 icon--gray w-100 h-100">
|
||||
<use xlinkHref="#arrow-left" />
|
||||
</svg>}
|
||||
nextText={
|
||||
<svg className="icons icon--24 icon--gray w-100 h-100">
|
||||
<use xlinkHref="#arrow-right" />
|
||||
</svg>}
|
||||
loadingText={t('loading_table_status')}
|
||||
pageText=''
|
||||
ofText=''
|
||||
ofText="/"
|
||||
previousText={t('previous_btn')}
|
||||
nextText={t('next_btn')}
|
||||
pageText={t('page_table_footer_text')}
|
||||
rowsText={t('rows_table_footer_text')}
|
||||
loadingText={t('loading_table_status')}
|
||||
noDataText={t('clients_not_found')}
|
||||
getPaginationProps={() => ({ className: 'custom-pagination' })}
|
||||
/>
|
||||
</Card>
|
||||
);
|
||||
|
||||
@@ -297,23 +297,13 @@ class ClientsTable extends Component {
|
||||
showPagination
|
||||
defaultPageSize={10}
|
||||
minRows={5}
|
||||
showPageSizeOptions={false}
|
||||
showPageJump={false}
|
||||
renderTotalPagesCount={() => false}
|
||||
previousText={
|
||||
<svg className="icons icon--24 icon--gray w-100 h-100">
|
||||
<use xlinkHref="#arrow-left" />
|
||||
</svg>}
|
||||
nextText={
|
||||
<svg className="icons icon--24 icon--gray w-100 h-100">
|
||||
<use xlinkHref="#arrow-right" />
|
||||
</svg>}
|
||||
loadingText={t('loading_table_status')}
|
||||
pageText=''
|
||||
ofText=''
|
||||
ofText="/"
|
||||
previousText={t('previous_btn')}
|
||||
nextText={t('next_btn')}
|
||||
pageText={t('page_table_footer_text')}
|
||||
rowsText={t('rows_table_footer_text')}
|
||||
loadingText={t('loading_table_status')}
|
||||
noDataText={t('clients_not_found')}
|
||||
getPaginationProps={() => ({ className: 'custom-pagination' })}
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
|
||||
@@ -3,6 +3,7 @@ import PropTypes from 'prop-types';
|
||||
import ReactTable from 'react-table';
|
||||
import { Trans, withTranslation } from 'react-i18next';
|
||||
import { LEASES_TABLE_DEFAULT_PAGE_SIZE } from '../../../helpers/constants';
|
||||
import { sortIp } from '../../../helpers/helpers';
|
||||
|
||||
class Leases extends Component {
|
||||
cellWrap = ({ value }) => (
|
||||
@@ -27,6 +28,7 @@ class Leases extends Component {
|
||||
Header: 'IP',
|
||||
accessor: 'ip',
|
||||
Cell: this.cellWrap,
|
||||
sortMethod: sortIp,
|
||||
}, {
|
||||
Header: <Trans>dhcp_table_hostname</Trans>,
|
||||
accessor: 'hostname',
|
||||
|
||||
@@ -3,7 +3,7 @@ import PropTypes from 'prop-types';
|
||||
import ReactTable from 'react-table';
|
||||
import { Trans, withTranslation } from 'react-i18next';
|
||||
import { LEASES_TABLE_DEFAULT_PAGE_SIZE } from '../../../../helpers/constants';
|
||||
|
||||
import { sortIp } from '../../../../helpers/helpers';
|
||||
import Modal from './Modal';
|
||||
|
||||
class StaticLeases extends Component {
|
||||
@@ -49,6 +49,7 @@ class StaticLeases extends Component {
|
||||
{
|
||||
Header: 'IP',
|
||||
accessor: 'ip',
|
||||
sortMethod: sortIp,
|
||||
Cell: this.cellWrap,
|
||||
},
|
||||
{
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { Component, Fragment } from 'react';
|
||||
import React, { Component } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import classnames from 'classnames';
|
||||
import { Trans, withTranslation } from 'react-i18next';
|
||||
@@ -34,13 +34,14 @@ class Dhcp extends Component {
|
||||
} = this.props.dhcp;
|
||||
const otherDhcpFound = check?.otherServer
|
||||
&& check.otherServer.found === DHCP_STATUS_RESPONSE.YES;
|
||||
const filledConfig = Object.keys(config).every((key) => {
|
||||
if (key === 'enabled' || key === 'icmp_timeout_msec') {
|
||||
return true;
|
||||
}
|
||||
const filledConfig = Object.keys(config)
|
||||
.every((key) => {
|
||||
if (key === 'enabled' || key === 'icmp_timeout_msec') {
|
||||
return true;
|
||||
}
|
||||
|
||||
return config[key];
|
||||
});
|
||||
return config[key];
|
||||
});
|
||||
|
||||
if (config.enabled) {
|
||||
return (
|
||||
@@ -114,40 +115,35 @@ class Dhcp extends Component {
|
||||
|
||||
getStaticIpWarning = (t, check, interfaceName) => {
|
||||
if (check.staticIP.static === DHCP_STATUS_RESPONSE.ERROR) {
|
||||
return (
|
||||
<Fragment>
|
||||
<div className="text-danger mb-2">
|
||||
<Trans>dhcp_static_ip_error</Trans>
|
||||
<div className="mt-2 mb-2">
|
||||
<Accordion label={t('error_details')}>
|
||||
<span>{check.staticIP.error}</span>
|
||||
</Accordion>
|
||||
</div>
|
||||
return <>
|
||||
<div className="text-danger mb-2">
|
||||
<Trans>dhcp_static_ip_error</Trans>
|
||||
<div className="mt-2 mb-2">
|
||||
<Accordion label={t('error_details')}>
|
||||
<span>{check.staticIP.error}</span>
|
||||
</Accordion>
|
||||
</div>
|
||||
<hr className="mt-4 mb-4" />
|
||||
</Fragment>
|
||||
);
|
||||
} if (
|
||||
check.staticIP.static === DHCP_STATUS_RESPONSE.NO
|
||||
</div>
|
||||
<hr className="mt-4 mb-4" />
|
||||
</>;
|
||||
}
|
||||
if (check.staticIP.static === DHCP_STATUS_RESPONSE.NO
|
||||
&& check.staticIP.ip
|
||||
&& interfaceName
|
||||
) {
|
||||
return (
|
||||
<Fragment>
|
||||
<div className="text-secondary mb-2">
|
||||
<Trans
|
||||
components={[<strong key="0">example</strong>]}
|
||||
values={{
|
||||
interfaceName,
|
||||
ipAddress: check.staticIP.ip,
|
||||
}}
|
||||
>
|
||||
dhcp_dynamic_ip_found
|
||||
</Trans>
|
||||
</div>
|
||||
<hr className="mt-4 mb-4" />
|
||||
</Fragment>
|
||||
);
|
||||
&& interfaceName) {
|
||||
return <>
|
||||
<div className="text-secondary mb-2">
|
||||
<Trans
|
||||
components={[<strong key="0">example</strong>]}
|
||||
values={{
|
||||
interfaceName,
|
||||
ipAddress: check.staticIP.ip,
|
||||
}}
|
||||
>
|
||||
dhcp_dynamic_ip_found
|
||||
</Trans>
|
||||
</div>
|
||||
<hr className="mt-4 mb-4" />
|
||||
</>;
|
||||
}
|
||||
|
||||
return '';
|
||||
@@ -163,104 +159,101 @@ class Dhcp extends Component {
|
||||
removeStaticLease,
|
||||
toggleLeaseModal,
|
||||
} = this.props;
|
||||
|
||||
const statusButtonClass = classnames({
|
||||
'btn btn-primary btn-standard': true,
|
||||
'btn btn-primary btn-standard btn-loading': dhcp.processingStatus,
|
||||
});
|
||||
const { enabled, interface_name, ...values } = dhcp.config;
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
<PageTitle title={t('dhcp_settings')} />
|
||||
{(dhcp.processing || dhcp.processingInterfaces) && <Loading />}
|
||||
{!dhcp.processing && !dhcp.processingInterfaces && (
|
||||
<Fragment>
|
||||
<Card
|
||||
title={t('dhcp_title')}
|
||||
subtitle={t('dhcp_description')}
|
||||
bodyType="card-body box-body--settings"
|
||||
>
|
||||
<div className="dhcp">
|
||||
<Fragment>
|
||||
<Form
|
||||
onSubmit={this.handleFormSubmit}
|
||||
initialValues={{
|
||||
interface_name,
|
||||
...values,
|
||||
}}
|
||||
interfaces={dhcp.interfaces}
|
||||
processingConfig={dhcp.processingConfig}
|
||||
processingInterfaces={dhcp.processingInterfaces}
|
||||
enabled={enabled}
|
||||
resetDhcp={resetDhcp}
|
||||
/>
|
||||
<hr />
|
||||
<div className="card-actions mb-3">
|
||||
{this.getToggleDhcpButton()}
|
||||
<button
|
||||
type="button"
|
||||
className={statusButtonClass}
|
||||
onClick={() => findActiveDhcp(interface_name)}
|
||||
disabled={
|
||||
enabled || !interface_name || dhcp.processingConfig
|
||||
}
|
||||
>
|
||||
<Trans>check_dhcp_servers</Trans>
|
||||
</button>
|
||||
</div>
|
||||
{!enabled && dhcp.check && (
|
||||
<Fragment>
|
||||
{this.getStaticIpWarning(t, dhcp.check, interface_name)}
|
||||
{this.getActiveDhcpMessage(t, dhcp.check)}
|
||||
{this.getDhcpWarning(dhcp.check)}
|
||||
</Fragment>
|
||||
)}
|
||||
</Fragment>
|
||||
return <>
|
||||
<PageTitle title={t('dhcp_settings')} />
|
||||
{(dhcp.processing || dhcp.processingInterfaces) && <Loading />}
|
||||
{!dhcp.processing && !dhcp.processingInterfaces && <>
|
||||
<Card
|
||||
title={t('dhcp_title')}
|
||||
subtitle={t('dhcp_description')}
|
||||
bodyType="card-body box-body--settings"
|
||||
>
|
||||
<div className="dhcp">
|
||||
<>
|
||||
<Form
|
||||
onSubmit={this.handleFormSubmit}
|
||||
initialValues={{
|
||||
interface_name,
|
||||
...values,
|
||||
}}
|
||||
interfaces={dhcp.interfaces}
|
||||
processingConfig={dhcp.processingConfig}
|
||||
processingInterfaces={dhcp.processingInterfaces}
|
||||
enabled={enabled}
|
||||
resetDhcp={resetDhcp}
|
||||
/>
|
||||
<hr />
|
||||
<div className="card-actions mb-3">
|
||||
{this.getToggleDhcpButton()}
|
||||
<button
|
||||
type="button"
|
||||
className={statusButtonClass}
|
||||
onClick={() => findActiveDhcp(interface_name)}
|
||||
disabled={
|
||||
enabled || !interface_name || dhcp.processingConfig
|
||||
}
|
||||
>
|
||||
<Trans>check_dhcp_servers</Trans>
|
||||
</button>
|
||||
</div>
|
||||
</Card>
|
||||
{dhcp.config.enabled && (
|
||||
<Card
|
||||
title={t('dhcp_leases')}
|
||||
bodyType="card-body box-body--settings"
|
||||
>
|
||||
<div className="row">
|
||||
<div className="col">
|
||||
<Leases leases={dhcp.leases} />
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
)}
|
||||
<Card
|
||||
title={t('dhcp_static_leases')}
|
||||
bodyType="card-body box-body--settings"
|
||||
>
|
||||
<div className="row">
|
||||
<div className="col-12">
|
||||
<StaticLeases
|
||||
staticLeases={dhcp.staticLeases}
|
||||
isModalOpen={dhcp.isModalOpen}
|
||||
addStaticLease={addStaticLease}
|
||||
removeStaticLease={removeStaticLease}
|
||||
toggleLeaseModal={toggleLeaseModal}
|
||||
processingAdding={dhcp.processingAdding}
|
||||
processingDeleting={dhcp.processingDeleting}
|
||||
/>
|
||||
</div>
|
||||
<div className="col-12">
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-success btn-standard mt-3"
|
||||
onClick={() => toggleLeaseModal()}
|
||||
>
|
||||
<Trans>dhcp_add_static_lease</Trans>
|
||||
</button>
|
||||
</div>
|
||||
{!enabled && dhcp.check && (
|
||||
<>
|
||||
{this.getStaticIpWarning(t, dhcp.check, interface_name)}
|
||||
{this.getActiveDhcpMessage(t, dhcp.check)}
|
||||
{this.getDhcpWarning(dhcp.check)}
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
</div>
|
||||
</Card>
|
||||
{dhcp.config.enabled && (
|
||||
<Card
|
||||
title={t('dhcp_leases')}
|
||||
bodyType="card-body box-body--settings"
|
||||
>
|
||||
<div className="row">
|
||||
<div className="col">
|
||||
<Leases leases={dhcp.leases} />
|
||||
</div>
|
||||
</Card>
|
||||
</Fragment>
|
||||
</div>
|
||||
</Card>
|
||||
)}
|
||||
</Fragment>
|
||||
);
|
||||
<Card
|
||||
title={t('dhcp_static_leases')}
|
||||
bodyType="card-body box-body--settings"
|
||||
>
|
||||
<div className="row">
|
||||
<div className="col-12">
|
||||
<StaticLeases
|
||||
staticLeases={dhcp.staticLeases}
|
||||
isModalOpen={dhcp.isModalOpen}
|
||||
addStaticLease={addStaticLease}
|
||||
removeStaticLease={removeStaticLease}
|
||||
toggleLeaseModal={toggleLeaseModal}
|
||||
processingAdding={dhcp.processingAdding}
|
||||
processingDeleting={dhcp.processingDeleting}
|
||||
/>
|
||||
</div>
|
||||
<div className="col-12">
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-success btn-standard mt-3"
|
||||
onClick={() => toggleLeaseModal()}
|
||||
>
|
||||
<Trans>dhcp_add_static_lease</Trans>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
</>}
|
||||
</>;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,40 +1,36 @@
|
||||
import React, { Component } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { withTranslation } from 'react-i18next';
|
||||
|
||||
import React from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { shallowEqual, useDispatch, useSelector } from 'react-redux';
|
||||
import Form from './Form';
|
||||
import Card from '../../../ui/Card';
|
||||
import { setAccessList } from '../../../../actions/access';
|
||||
|
||||
class Access extends Component {
|
||||
handleFormSubmit = (values) => {
|
||||
this.props.setAccessList(values);
|
||||
const Access = () => {
|
||||
const { t } = useTranslation();
|
||||
const dispatch = useDispatch();
|
||||
const {
|
||||
processing,
|
||||
processingSet,
|
||||
...values
|
||||
} = useSelector((state) => state.access, shallowEqual);
|
||||
|
||||
const handleFormSubmit = (values) => {
|
||||
dispatch(setAccessList(values));
|
||||
};
|
||||
|
||||
render() {
|
||||
const { t, access } = this.props;
|
||||
|
||||
const { processing, processingSet, ...values } = access;
|
||||
|
||||
return (
|
||||
<Card
|
||||
title={t('access_title')}
|
||||
subtitle={t('access_desc')}
|
||||
bodyType="card-body box-body--settings"
|
||||
>
|
||||
<Form
|
||||
initialValues={values}
|
||||
onSubmit={this.handleFormSubmit}
|
||||
processingSet={processingSet}
|
||||
/>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Access.propTypes = {
|
||||
access: PropTypes.object.isRequired,
|
||||
setAccessList: PropTypes.func.isRequired,
|
||||
t: PropTypes.func.isRequired,
|
||||
return (
|
||||
<Card
|
||||
title={t('access_title')}
|
||||
subtitle={t('access_desc')}
|
||||
bodyType="card-body box-body--settings"
|
||||
>
|
||||
<Form
|
||||
initialValues={values}
|
||||
onSubmit={handleFormSubmit}
|
||||
processingSet={processingSet}
|
||||
/>
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
|
||||
export default withTranslation()(Access);
|
||||
export default Access;
|
||||
|
||||
@@ -53,7 +53,7 @@ const Form = ({
|
||||
{INPUTS_FIELDS.map(({
|
||||
name, title, description, placeholder, validate, max,
|
||||
}) => <div className="col-12" key={name}>
|
||||
<div className="col-7 p-0">
|
||||
<div className="col-12 col-md-7 p-0">
|
||||
<div className="form__group form__group--settings">
|
||||
<label htmlFor={name}
|
||||
className="form__label form__label--with-desc">{t(title)}</label>
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
import React, { Fragment } from 'react';
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { connect } from 'react-redux';
|
||||
import { Field, reduxForm, formValueSelector } from 'redux-form';
|
||||
import { Trans, withTranslation } from 'react-i18next';
|
||||
import flow from 'lodash/flow';
|
||||
import { shallowEqual, useSelector } from 'react-redux';
|
||||
import { Field, reduxForm } from 'redux-form';
|
||||
import { Trans, useTranslation } from 'react-i18next';
|
||||
import {
|
||||
renderInputField,
|
||||
renderRadioField,
|
||||
@@ -18,32 +17,36 @@ import {
|
||||
} from '../../../../helpers/validators';
|
||||
import { BLOCKING_MODES, FORM_NAME } from '../../../../helpers/constants';
|
||||
|
||||
const checkboxes = [{
|
||||
name: 'edns_cs_enabled',
|
||||
placeholder: 'edns_enable',
|
||||
subtitle: 'edns_cs_desc',
|
||||
},
|
||||
{
|
||||
name: 'dnssec_enabled',
|
||||
placeholder: 'dnssec_enable',
|
||||
subtitle: 'dnssec_enable_desc',
|
||||
},
|
||||
{
|
||||
name: 'disable_ipv6',
|
||||
placeholder: 'disable_ipv6',
|
||||
subtitle: 'disable_ipv6_desc',
|
||||
}];
|
||||
const checkboxes = [
|
||||
{
|
||||
name: 'edns_cs_enabled',
|
||||
placeholder: 'edns_enable',
|
||||
subtitle: 'edns_cs_desc',
|
||||
},
|
||||
{
|
||||
name: 'dnssec_enabled',
|
||||
placeholder: 'dnssec_enable',
|
||||
subtitle: 'dnssec_enable_desc',
|
||||
},
|
||||
{
|
||||
name: 'disable_ipv6',
|
||||
placeholder: 'disable_ipv6',
|
||||
subtitle: 'disable_ipv6_desc',
|
||||
},
|
||||
];
|
||||
|
||||
const customIps = [{
|
||||
description: 'blocking_ipv4_desc',
|
||||
name: 'blocking_ipv4',
|
||||
validateIp: validateIpv4,
|
||||
},
|
||||
{
|
||||
description: 'blocking_ipv6_desc',
|
||||
name: 'blocking_ipv6',
|
||||
validateIp: validateIpv6,
|
||||
}];
|
||||
const customIps = [
|
||||
{
|
||||
description: 'blocking_ipv4_desc',
|
||||
name: 'blocking_ipv4',
|
||||
validateIp: validateIpv4,
|
||||
},
|
||||
{
|
||||
description: 'blocking_ipv6_desc',
|
||||
name: 'blocking_ipv6',
|
||||
validateIp: validateIpv6,
|
||||
},
|
||||
];
|
||||
|
||||
const getFields = (processing, t) => Object.values(BLOCKING_MODES)
|
||||
.map((mode) => (
|
||||
@@ -58,114 +61,107 @@ const getFields = (processing, t) => Object.values(BLOCKING_MODES)
|
||||
/>
|
||||
));
|
||||
|
||||
let Form = ({
|
||||
handleSubmit, submitting, invalid, processing, blockingMode, t,
|
||||
}) => <form onSubmit={handleSubmit}>
|
||||
<div className="row">
|
||||
<div className="col-12 col-sm-6">
|
||||
<div className="form__group form__group--settings">
|
||||
<label htmlFor="ratelimit"
|
||||
className="form__label form__label--with-desc">
|
||||
<Trans>rate_limit</Trans>
|
||||
</label>
|
||||
<div className="form__desc form__desc--top">
|
||||
<Trans>rate_limit_desc</Trans>
|
||||
</div>
|
||||
<Field
|
||||
name="ratelimit"
|
||||
type="number"
|
||||
component={renderInputField}
|
||||
className="form-control"
|
||||
placeholder={t('form_enter_rate_limit')}
|
||||
normalize={toNumber}
|
||||
validate={[validateRequiredValue, validateBiggerOrEqualZeroValue]}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{checkboxes.map(({ name, placeholder, subtitle }) => <div className="col-12" key={name}>
|
||||
<div className="form__group form__group--settings">
|
||||
<Field
|
||||
name={name}
|
||||
type="checkbox"
|
||||
component={renderSelectField}
|
||||
placeholder={t(placeholder)}
|
||||
disabled={processing}
|
||||
subtitle={t(subtitle)}
|
||||
/>
|
||||
</div>
|
||||
</div>)}
|
||||
<div className="col-12">
|
||||
<div className="form__group form__group--settings mb-4">
|
||||
<label className="form__label form__label--with-desc">
|
||||
<Trans>blocking_mode</Trans>
|
||||
</label>
|
||||
<div className="form__desc form__desc--top">
|
||||
{Object.values(BLOCKING_MODES)
|
||||
.map((mode) => (
|
||||
<li key={mode}>
|
||||
<Trans>{`blocking_mode_${mode}`}</Trans>
|
||||
</li>
|
||||
))}
|
||||
</div>
|
||||
<div className="custom-controls-stacked">
|
||||
{getFields(processing, t)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{blockingMode === BLOCKING_MODES.custom_ip && (
|
||||
<Fragment>
|
||||
{customIps.map(({
|
||||
description,
|
||||
name,
|
||||
validateIp,
|
||||
}) => <div className="col-12 col-sm-6" key={name}>
|
||||
<div className="form__group form__group--settings">
|
||||
<label className="form__label form__label--with-desc"
|
||||
htmlFor={name}><Trans>{name}</Trans>
|
||||
</label>
|
||||
<div className="form__desc form__desc--top">
|
||||
<Trans>{description}</Trans>
|
||||
</div>
|
||||
<Field
|
||||
name={name}
|
||||
component={renderInputField}
|
||||
className="form-control"
|
||||
placeholder={t('form_enter_ip')}
|
||||
validate={[validateIp, validateRequiredValue]}
|
||||
/>
|
||||
const Form = ({
|
||||
handleSubmit, submitting, invalid, processing,
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
const {
|
||||
blocking_mode,
|
||||
} = useSelector((state) => state.form[FORM_NAME.BLOCKING_MODE].values ?? {}, shallowEqual);
|
||||
|
||||
return <form onSubmit={handleSubmit}>
|
||||
<div className="row">
|
||||
<div className="col-12 col-sm-6">
|
||||
<div className="form__group form__group--settings">
|
||||
<label htmlFor="ratelimit"
|
||||
className="form__label form__label--with-desc">
|
||||
<Trans>rate_limit</Trans>
|
||||
</label>
|
||||
<div className="form__desc form__desc--top">
|
||||
<Trans>rate_limit_desc</Trans>
|
||||
</div>
|
||||
</div>)}
|
||||
</Fragment>
|
||||
)}
|
||||
</div>
|
||||
<button
|
||||
type="submit"
|
||||
className="btn btn-success btn-standard btn-large"
|
||||
disabled={submitting || invalid || processing}
|
||||
>
|
||||
<Trans>save_btn</Trans>
|
||||
</button>
|
||||
</form>;
|
||||
<Field
|
||||
name="ratelimit"
|
||||
type="number"
|
||||
component={renderInputField}
|
||||
className="form-control"
|
||||
placeholder={t('form_enter_rate_limit')}
|
||||
normalize={toNumber}
|
||||
validate={[validateRequiredValue, validateBiggerOrEqualZeroValue]}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{checkboxes.map(({ name, placeholder, subtitle }) => <div className="col-12" key={name}>
|
||||
<div className="form__group form__group--settings">
|
||||
<Field
|
||||
name={name}
|
||||
type="checkbox"
|
||||
component={renderSelectField}
|
||||
placeholder={t(placeholder)}
|
||||
disabled={processing}
|
||||
subtitle={t(subtitle)}
|
||||
/>
|
||||
</div>
|
||||
</div>)}
|
||||
<div className="col-12">
|
||||
<div className="form__group form__group--settings mb-4">
|
||||
<label className="form__label form__label--with-desc">
|
||||
<Trans>blocking_mode</Trans>
|
||||
</label>
|
||||
<div className="form__desc form__desc--top">
|
||||
{Object.values(BLOCKING_MODES)
|
||||
.map((mode) => (
|
||||
<li key={mode}>
|
||||
<Trans>{`blocking_mode_${mode}`}</Trans>
|
||||
</li>
|
||||
))}
|
||||
</div>
|
||||
<div className="custom-controls-stacked">
|
||||
{getFields(processing, t)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{blocking_mode === BLOCKING_MODES.custom_ip && (
|
||||
<>
|
||||
{customIps.map(({
|
||||
description,
|
||||
name,
|
||||
validateIp,
|
||||
}) => <div className="col-12 col-sm-6" key={name}>
|
||||
<div className="form__group form__group--settings">
|
||||
<label className="form__label form__label--with-desc"
|
||||
htmlFor={name}><Trans>{name}</Trans>
|
||||
</label>
|
||||
<div className="form__desc form__desc--top">
|
||||
<Trans>{description}</Trans>
|
||||
</div>
|
||||
<Field
|
||||
name={name}
|
||||
component={renderInputField}
|
||||
className="form-control"
|
||||
placeholder={t('form_enter_ip')}
|
||||
validate={[validateIp, validateRequiredValue]}
|
||||
/>
|
||||
</div>
|
||||
</div>)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
<button
|
||||
type="submit"
|
||||
className="btn btn-success btn-standard btn-large"
|
||||
disabled={submitting || invalid || processing}
|
||||
>
|
||||
<Trans>save_btn</Trans>
|
||||
</button>
|
||||
</form>;
|
||||
};
|
||||
|
||||
Form.propTypes = {
|
||||
blockingMode: PropTypes.string.isRequired,
|
||||
handleSubmit: PropTypes.func.isRequired,
|
||||
submitting: PropTypes.bool.isRequired,
|
||||
invalid: PropTypes.bool.isRequired,
|
||||
processing: PropTypes.bool.isRequired,
|
||||
t: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
const selector = formValueSelector(FORM_NAME.BLOCKING_MODE);
|
||||
|
||||
Form = connect((state) => {
|
||||
const blockingMode = selector(state, 'blocking_mode');
|
||||
return {
|
||||
blockingMode,
|
||||
};
|
||||
})(Form);
|
||||
|
||||
export default flow([
|
||||
withTranslation(),
|
||||
reduxForm({ form: FORM_NAME.BLOCKING_MODE }),
|
||||
])(Form);
|
||||
export default reduxForm({ form: FORM_NAME.BLOCKING_MODE })(Form);
|
||||
|
||||
@@ -1,15 +1,13 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { withTranslation } from 'react-i18next';
|
||||
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { shallowEqual, useDispatch, useSelector } from 'react-redux';
|
||||
import Card from '../../../ui/Card';
|
||||
import Form from './Form';
|
||||
import { setDnsConfig } from '../../../../actions/dnsConfig';
|
||||
|
||||
const Config = ({ t, dnsConfig, setDnsConfig }) => {
|
||||
const handleFormSubmit = (values) => {
|
||||
setDnsConfig(values);
|
||||
};
|
||||
|
||||
const Config = () => {
|
||||
const { t } = useTranslation();
|
||||
const dispatch = useDispatch();
|
||||
const {
|
||||
blocking_mode,
|
||||
ratelimit,
|
||||
@@ -19,7 +17,11 @@ const Config = ({ t, dnsConfig, setDnsConfig }) => {
|
||||
dnssec_enabled,
|
||||
disable_ipv6,
|
||||
processingSetConfig,
|
||||
} = dnsConfig;
|
||||
} = useSelector((state) => state.dnsConfig, shallowEqual);
|
||||
|
||||
const handleFormSubmit = (values) => {
|
||||
dispatch(setDnsConfig(values));
|
||||
};
|
||||
|
||||
return (
|
||||
<Card
|
||||
@@ -46,10 +48,4 @@ const Config = ({ t, dnsConfig, setDnsConfig }) => {
|
||||
);
|
||||
};
|
||||
|
||||
Config.propTypes = {
|
||||
dnsConfig: PropTypes.object.isRequired,
|
||||
setDnsConfig: PropTypes.func.isRequired,
|
||||
t: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
export default withTranslation()(Config);
|
||||
export default Config;
|
||||
|
||||
@@ -52,10 +52,11 @@ const Form = ({
|
||||
submitting, invalid, processingSetConfig, processingTestUpstream, handleSubmit,
|
||||
}) => {
|
||||
const dispatch = useDispatch();
|
||||
const [t] = useTranslation();
|
||||
const { t } = useTranslation();
|
||||
const upstream_dns = useSelector((store) => store.form[FORM_NAME.UPSTREAM].values.upstream_dns);
|
||||
const bootstrap_dns = useSelector((store) => store.form[FORM_NAME.UPSTREAM]
|
||||
.values.bootstrap_dns);
|
||||
const bootstrap_dns = useSelector(
|
||||
(store) => store.form[FORM_NAME.UPSTREAM].values.bootstrap_dns,
|
||||
);
|
||||
|
||||
const handleUpstreamTest = () => dispatch(testUpstream({
|
||||
upstream_dns,
|
||||
|
||||
@@ -1,56 +1,46 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import { shallowEqual, useDispatch, useSelector } from 'react-redux';
|
||||
import Form from './Form';
|
||||
import Card from '../../../ui/Card';
|
||||
import { setDnsConfig } from '../../../../actions/dnsConfig';
|
||||
|
||||
const Upstream = (props) => {
|
||||
const [t] = useTranslation();
|
||||
const Upstream = () => {
|
||||
const { t } = useTranslation();
|
||||
const dispatch = useDispatch();
|
||||
const {
|
||||
upstream_dns,
|
||||
bootstrap_dns,
|
||||
upstream_mode,
|
||||
processingSetConfig,
|
||||
} = useSelector((state) => state.dnsConfig, shallowEqual);
|
||||
|
||||
const { processingTestUpstream } = useSelector((state) => state.settings, shallowEqual);
|
||||
|
||||
const handleSubmit = (values) => {
|
||||
dispatch(setDnsConfig(values));
|
||||
};
|
||||
|
||||
const {
|
||||
processingTestUpstream,
|
||||
dnsConfig: {
|
||||
upstream_dns,
|
||||
bootstrap_dns,
|
||||
processingSetConfig,
|
||||
upstream_mode,
|
||||
},
|
||||
} = props;
|
||||
|
||||
return (
|
||||
<Card
|
||||
title={t('upstream_dns')}
|
||||
subtitle={t('upstream_dns_hint')}
|
||||
bodyType="card-body box-body--settings"
|
||||
>
|
||||
<div className="row">
|
||||
<div className="col">
|
||||
<Form
|
||||
initialValues={{
|
||||
upstream_dns,
|
||||
bootstrap_dns,
|
||||
upstream_mode,
|
||||
}}
|
||||
onSubmit={handleSubmit}
|
||||
processingTestUpstream={processingTestUpstream}
|
||||
processingSetConfig={processingSetConfig}
|
||||
/>
|
||||
</div>
|
||||
return <Card
|
||||
title={t('upstream_dns')}
|
||||
subtitle={t('upstream_dns_hint')}
|
||||
bodyType="card-body box-body--settings"
|
||||
>
|
||||
<div className="row">
|
||||
<div className="col">
|
||||
<Form
|
||||
initialValues={{
|
||||
upstream_dns,
|
||||
bootstrap_dns,
|
||||
upstream_mode,
|
||||
}}
|
||||
onSubmit={handleSubmit}
|
||||
processingTestUpstream={processingTestUpstream}
|
||||
processingSetConfig={processingSetConfig}
|
||||
/>
|
||||
</div>
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
|
||||
Upstream.propTypes = {
|
||||
processingTestUpstream: PropTypes.bool.isRequired,
|
||||
dnsConfig: PropTypes.object.isRequired,
|
||||
</div>
|
||||
</Card>;
|
||||
};
|
||||
|
||||
export default Upstream;
|
||||
|
||||
@@ -1,67 +1,40 @@
|
||||
import React, { useEffect } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import Upstream from './Upstream';
|
||||
import Access from './Access';
|
||||
import Config from './Config';
|
||||
import PageTitle from '../../ui/PageTitle';
|
||||
import Loading from '../../ui/Loading';
|
||||
import CacheConfig from './Cache';
|
||||
import { getDnsConfig } from '../../../actions/dnsConfig';
|
||||
import { getAccessList } from '../../../actions/access';
|
||||
|
||||
const Dns = (props) => {
|
||||
const Dns = () => {
|
||||
const { t } = useTranslation();
|
||||
const dispatch = useDispatch();
|
||||
const processing = useSelector((state) => state.access.processing);
|
||||
const processingGetConfig = useSelector((state) => state.dnsConfig.processingGetConfig);
|
||||
|
||||
const isDataLoading = processing || processingGetConfig;
|
||||
|
||||
useEffect(() => {
|
||||
props.getAccessList();
|
||||
props.getDnsConfig();
|
||||
dispatch(getAccessList());
|
||||
dispatch(getDnsConfig());
|
||||
}, []);
|
||||
|
||||
const {
|
||||
settings,
|
||||
access,
|
||||
setAccessList,
|
||||
dnsConfig,
|
||||
setDnsConfig,
|
||||
} = props;
|
||||
|
||||
const isDataLoading = access.processing || dnsConfig.processingGetConfig;
|
||||
|
||||
return (
|
||||
<>
|
||||
<PageTitle title={t('dns_settings')} />
|
||||
{isDataLoading
|
||||
? <Loading />
|
||||
: <>
|
||||
<Upstream
|
||||
processingTestUpstream={settings.processingTestUpstream}
|
||||
dnsConfig={dnsConfig}
|
||||
/>
|
||||
<Config
|
||||
dnsConfig={dnsConfig}
|
||||
setDnsConfig={setDnsConfig}
|
||||
/>
|
||||
<CacheConfig
|
||||
dnsConfig={dnsConfig}
|
||||
setDnsConfig={setDnsConfig}
|
||||
/>
|
||||
<Access
|
||||
access={access}
|
||||
setAccessList={setAccessList}
|
||||
/>
|
||||
</>}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
Dns.propTypes = {
|
||||
settings: PropTypes.object.isRequired,
|
||||
getAccessList: PropTypes.func.isRequired,
|
||||
setAccessList: PropTypes.func.isRequired,
|
||||
access: PropTypes.object.isRequired,
|
||||
dnsConfig: PropTypes.object.isRequired,
|
||||
setDnsConfig: PropTypes.func.isRequired,
|
||||
getDnsConfig: PropTypes.func.isRequired,
|
||||
return <>
|
||||
<PageTitle title={t('dns_settings')} />
|
||||
{isDataLoading
|
||||
? <Loading />
|
||||
: <>
|
||||
<Upstream />
|
||||
<Config />
|
||||
<CacheConfig />
|
||||
<Access />
|
||||
</>}
|
||||
</>;
|
||||
};
|
||||
|
||||
export default Dns;
|
||||
|
||||
@@ -10,53 +10,50 @@ import Checkbox from '../ui/Checkbox';
|
||||
import Loading from '../ui/Loading';
|
||||
import PageTitle from '../ui/PageTitle';
|
||||
import Card from '../ui/Card';
|
||||
|
||||
import { getObjectKeysSorted } from '../../helpers/helpers';
|
||||
import './Settings.css';
|
||||
|
||||
class Settings extends Component {
|
||||
settings = {
|
||||
safebrowsing: {
|
||||
enabled: false,
|
||||
title: 'use_adguard_browsing_sec',
|
||||
subtitle: 'use_adguard_browsing_sec_hint',
|
||||
},
|
||||
parental: {
|
||||
enabled: false,
|
||||
title: 'use_adguard_parental',
|
||||
subtitle: 'use_adguard_parental_hint',
|
||||
},
|
||||
safesearch: {
|
||||
enabled: false,
|
||||
title: 'enforce_safe_search',
|
||||
subtitle: 'enforce_save_search_hint',
|
||||
},
|
||||
};
|
||||
const ORDER_KEY = 'order';
|
||||
|
||||
const SETTINGS = {
|
||||
safebrowsing: {
|
||||
enabled: false,
|
||||
title: 'use_adguard_browsing_sec',
|
||||
subtitle: 'use_adguard_browsing_sec_hint',
|
||||
[ORDER_KEY]: 0,
|
||||
},
|
||||
parental: {
|
||||
enabled: false,
|
||||
title: 'use_adguard_parental',
|
||||
subtitle: 'use_adguard_parental_hint',
|
||||
[ORDER_KEY]: 1,
|
||||
},
|
||||
safesearch: {
|
||||
enabled: false,
|
||||
title: 'enforce_safe_search',
|
||||
subtitle: 'enforce_save_search_hint',
|
||||
[ORDER_KEY]: 2,
|
||||
},
|
||||
};
|
||||
|
||||
class Settings extends Component {
|
||||
componentDidMount() {
|
||||
this.props.initSettings(this.settings);
|
||||
this.props.initSettings(SETTINGS);
|
||||
this.props.getStatsConfig();
|
||||
this.props.getLogsConfig();
|
||||
this.props.getFilteringStatus();
|
||||
}
|
||||
|
||||
renderSettings = (settings) => {
|
||||
const settingsKeys = Object.keys(settings);
|
||||
|
||||
if (settingsKeys.length > 0) {
|
||||
return settingsKeys.map((key) => {
|
||||
const setting = settings[key];
|
||||
const { enabled } = setting;
|
||||
return (
|
||||
<Checkbox
|
||||
{...settings[key]}
|
||||
key={key}
|
||||
handleChange={() => this.props.toggleSetting(key, enabled)}
|
||||
/>
|
||||
);
|
||||
});
|
||||
}
|
||||
return '';
|
||||
};
|
||||
renderSettings = (settings) => getObjectKeysSorted(settings, ORDER_KEY)
|
||||
.map((key) => {
|
||||
const setting = settings[key];
|
||||
const { enabled } = setting;
|
||||
return <Checkbox
|
||||
{...setting}
|
||||
key={key}
|
||||
handleChange={() => this.props.toggleSetting(key, enabled)}
|
||||
/>;
|
||||
});
|
||||
|
||||
render() {
|
||||
const {
|
||||
|
||||
@@ -13,3 +13,17 @@
|
||||
margin-bottom: 20px;
|
||||
font-size: 15px;
|
||||
}
|
||||
|
||||
.guide__address {
|
||||
display: block;
|
||||
margin-bottom: 7px;
|
||||
font-size: 13px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
@media screen and (min-width: 768px) {
|
||||
.guide__address {
|
||||
display: list-item;
|
||||
font-size: 15px;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,8 +24,8 @@ const SetupGuide = ({
|
||||
<div className="mt-1">
|
||||
<Trans>install_devices_address</Trans>:
|
||||
</div>
|
||||
<div className="mt-2 font-weight-bold">
|
||||
{dnsAddresses.map((ip) => <li key={ip}>{ip}</li>)}
|
||||
<div className="mt-3">
|
||||
{dnsAddresses.map((ip) => <li key={ip} className="guide__address">{ip}</li>)}
|
||||
</div>
|
||||
</div>
|
||||
<Guide dnsAddresses={dnsAddresses} />
|
||||
|
||||
@@ -1,19 +1,20 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { Trans, withTranslation } from 'react-i18next';
|
||||
import { Trans } from 'react-i18next';
|
||||
import isAfter from 'date-fns/is_after';
|
||||
import addDays from 'date-fns/add_days';
|
||||
|
||||
import { useSelector } from 'react-redux';
|
||||
import Topline from './Topline';
|
||||
import { EMPTY_DATE } from '../../helpers/constants';
|
||||
|
||||
const EncryptionTopline = (props) => {
|
||||
if (props.notAfter === EMPTY_DATE) {
|
||||
return false;
|
||||
const EncryptionTopline = () => {
|
||||
const not_after = useSelector((state) => state.encryption.not_after);
|
||||
|
||||
if (not_after === EMPTY_DATE) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const isAboutExpire = isAfter(addDays(Date.now(), 30), props.notAfter);
|
||||
const isExpired = isAfter(Date.now(), props.notAfter);
|
||||
const isAboutExpire = isAfter(addDays(Date.now(), 30), not_after);
|
||||
const isExpired = isAfter(Date.now(), not_after);
|
||||
|
||||
if (isExpired) {
|
||||
return (
|
||||
@@ -23,7 +24,9 @@ const EncryptionTopline = (props) => {
|
||||
</Trans>
|
||||
</Topline>
|
||||
);
|
||||
} if (isAboutExpire) {
|
||||
}
|
||||
|
||||
if (isAboutExpire) {
|
||||
return (
|
||||
<Topline type="warning">
|
||||
<Trans components={[<a href="#encryption" key="0">link</a>]}>
|
||||
@@ -36,8 +39,4 @@ const EncryptionTopline = (props) => {
|
||||
return false;
|
||||
};
|
||||
|
||||
EncryptionTopline.propTypes = {
|
||||
notAfter: PropTypes.string.isRequired,
|
||||
};
|
||||
|
||||
export default withTranslation()(EncryptionTopline);
|
||||
export default EncryptionTopline;
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import classNames from 'classnames';
|
||||
|
||||
@@ -28,7 +27,7 @@ const linksData = [
|
||||
},
|
||||
];
|
||||
|
||||
const Footer = (props) => {
|
||||
const Footer = () => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const getYear = () => {
|
||||
@@ -59,11 +58,6 @@ const Footer = (props) => {
|
||||
{t(name)}
|
||||
</a>);
|
||||
|
||||
|
||||
const {
|
||||
dnsVersion, processingVersion, getVersion, checkUpdateFlag,
|
||||
} = props;
|
||||
|
||||
return (
|
||||
<>
|
||||
<footer className="footer">
|
||||
@@ -94,12 +88,7 @@ const Footer = (props) => {
|
||||
<div className="footer__row">
|
||||
{renderCopyright()}
|
||||
<div className="footer__column footer__column--language">
|
||||
<Version
|
||||
dnsVersion={dnsVersion}
|
||||
processingVersion={processingVersion}
|
||||
getVersion={getVersion}
|
||||
checkUpdateFlag={checkUpdateFlag}
|
||||
/>
|
||||
<Version />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -108,11 +97,4 @@ const Footer = (props) => {
|
||||
);
|
||||
};
|
||||
|
||||
Footer.propTypes = {
|
||||
dnsVersion: PropTypes.string,
|
||||
processingVersion: PropTypes.bool,
|
||||
getVersion: PropTypes.func,
|
||||
checkUpdateFlag: PropTypes.bool,
|
||||
};
|
||||
|
||||
export default Footer;
|
||||
|
||||
@@ -1,9 +1,14 @@
|
||||
import React from 'react';
|
||||
|
||||
import PropTypes from 'prop-types';
|
||||
import classNames from 'classnames';
|
||||
import './Loading.css';
|
||||
|
||||
const Loading = () => (
|
||||
<div className="loading" />
|
||||
const Loading = ({ className }) => (
|
||||
<div className={classNames('loading', className)} />
|
||||
);
|
||||
|
||||
Loading.propTypes = {
|
||||
className: PropTypes.string,
|
||||
};
|
||||
|
||||
export default Loading;
|
||||
|
||||
@@ -6,18 +6,24 @@
|
||||
.page-header--logs {
|
||||
flex-direction: row;
|
||||
align-items: flex-end;
|
||||
margin: 2rem 0 3rem;
|
||||
margin: 2rem 0 2.8rem;
|
||||
}
|
||||
|
||||
.page-header--logs .page-title {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
@media (max-width: 991px) {
|
||||
.page-header--logs {
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
margin-bottom: 1.5rem;
|
||||
margin: 1.1rem 0;
|
||||
}
|
||||
|
||||
.page-header--logs .page-title {
|
||||
padding-bottom: 2.5rem;;
|
||||
margin-bottom: 1.1rem;
|
||||
font-size: 1.8rem;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -2,8 +2,16 @@
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 15px;
|
||||
padding: 15px 0;
|
||||
padding: 10px 0;
|
||||
border-bottom: 1px solid #e8e8e8;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
@media screen and (min-width: 768px) {
|
||||
.tabs__controls {
|
||||
padding: 15px 0;
|
||||
overflow: initial;
|
||||
}
|
||||
}
|
||||
|
||||
.tabs__controls--form {
|
||||
@@ -26,11 +34,18 @@
|
||||
align-items: center;
|
||||
min-width: 70px;
|
||||
font-size: 13px;
|
||||
white-space: nowrap;
|
||||
color: #555555;
|
||||
cursor: pointer;
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
@media screen and (min-width: 768px) {
|
||||
.tab__control {
|
||||
white-space: normal;
|
||||
}
|
||||
}
|
||||
|
||||
.tab__control:hover,
|
||||
.tab__control:focus {
|
||||
opacity: 1;
|
||||
|
||||
@@ -1,5 +1,11 @@
|
||||
.tooltip-container {
|
||||
border: 0;
|
||||
padding: 0.7rem;
|
||||
box-shadow: 1px 1px 6px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
.tooltip-custom--narrow {
|
||||
max-width: 13.75rem;
|
||||
max-width: 14rem;
|
||||
}
|
||||
|
||||
.tooltip-custom--wide {
|
||||
|
||||
@@ -2,7 +2,12 @@ import React from 'react';
|
||||
import TooltipTrigger from 'react-popper-tooltip';
|
||||
import propTypes from 'prop-types';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { HIDE_TOOLTIP_DELAY } from '../../helpers/constants';
|
||||
|
||||
import {
|
||||
HIDE_TOOLTIP_DELAY,
|
||||
MEDIUM_SCREEN_SIZE,
|
||||
SHOW_TOOLTIP_DELAY,
|
||||
} from '../../helpers/constants';
|
||||
import 'react-popper-tooltip/dist/styles.css';
|
||||
import './Tooltip.css';
|
||||
|
||||
@@ -13,30 +18,51 @@ const Tooltip = ({
|
||||
className = 'tooltip-container',
|
||||
placement = 'bottom',
|
||||
trigger = 'hover',
|
||||
delayShow = SHOW_TOOLTIP_DELAY,
|
||||
delayHide = HIDE_TOOLTIP_DELAY,
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
const touchEventsAvailable = 'ontouchstart' in window;
|
||||
|
||||
return <TooltipTrigger
|
||||
placement={placement}
|
||||
trigger={trigger}
|
||||
delayHide={delayHide}
|
||||
tooltip={({
|
||||
tooltipRef,
|
||||
getTooltipProps,
|
||||
}) => <div {...getTooltipProps({
|
||||
ref: tooltipRef,
|
||||
className,
|
||||
})}>
|
||||
{typeof content === 'string' ? t(content) : content}
|
||||
</div>
|
||||
}>{({ getTriggerProps, triggerRef }) => <span
|
||||
{...getTriggerProps({
|
||||
ref: triggerRef,
|
||||
className: triggerClass,
|
||||
})}
|
||||
>{children}</span>}
|
||||
</TooltipTrigger>;
|
||||
let triggerValue = trigger;
|
||||
let delayHideValue = delayHide;
|
||||
let delayShowValue = delayShow;
|
||||
|
||||
if (window.matchMedia(`(max-width: ${MEDIUM_SCREEN_SIZE}px)`).matches || touchEventsAvailable) {
|
||||
triggerValue = 'click';
|
||||
delayHideValue = 0;
|
||||
delayShowValue = 0;
|
||||
}
|
||||
|
||||
return (
|
||||
<TooltipTrigger
|
||||
placement={placement}
|
||||
trigger={triggerValue}
|
||||
delayHide={delayHideValue}
|
||||
delayShow={delayShowValue}
|
||||
tooltip={({ tooltipRef, getTooltipProps }) => (
|
||||
<div
|
||||
{...getTooltipProps({
|
||||
ref: tooltipRef,
|
||||
className,
|
||||
})}
|
||||
>
|
||||
{typeof content === 'string' ? t(content) : content}
|
||||
</div>
|
||||
)}
|
||||
>
|
||||
{({ getTriggerProps, triggerRef }) => (
|
||||
<span
|
||||
{...getTriggerProps({
|
||||
ref: triggerRef,
|
||||
className: triggerClass,
|
||||
})}
|
||||
>
|
||||
{children}
|
||||
</span>
|
||||
)}
|
||||
</TooltipTrigger>
|
||||
);
|
||||
};
|
||||
|
||||
Tooltip.propTypes = {
|
||||
@@ -51,6 +77,7 @@ Tooltip.propTypes = {
|
||||
placement: propTypes.string,
|
||||
trigger: propTypes.string,
|
||||
delayHide: propTypes.string,
|
||||
delayShow: propTypes.string,
|
||||
className: propTypes.string,
|
||||
triggerClass: propTypes.string,
|
||||
};
|
||||
|
||||
@@ -1,14 +1,13 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { Trans, withTranslation } from 'react-i18next';
|
||||
import { Trans } from 'react-i18next';
|
||||
import classnames from 'classnames';
|
||||
|
||||
import { useSelector } from 'react-redux';
|
||||
import './Overlay.css';
|
||||
|
||||
const UpdateOverlay = (props) => {
|
||||
const overlayClass = classnames({
|
||||
overlay: true,
|
||||
'overlay--visible': props.processingUpdate,
|
||||
const UpdateOverlay = () => {
|
||||
const processingUpdate = useSelector((state) => state.dashboard.processingUpdate);
|
||||
const overlayClass = classnames('overlay', {
|
||||
'overlay--visible': processingUpdate,
|
||||
});
|
||||
|
||||
return (
|
||||
@@ -19,8 +18,4 @@ const UpdateOverlay = (props) => {
|
||||
);
|
||||
};
|
||||
|
||||
UpdateOverlay.propTypes = {
|
||||
processingUpdate: PropTypes.bool,
|
||||
};
|
||||
|
||||
export default withTranslation()(UpdateOverlay);
|
||||
export default UpdateOverlay;
|
||||
|
||||
@@ -1,42 +1,46 @@
|
||||
import React, { Fragment } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { Trans, withTranslation } from 'react-i18next';
|
||||
|
||||
import React from 'react';
|
||||
import { Trans } from 'react-i18next';
|
||||
import { shallowEqual, useDispatch, useSelector } from 'react-redux';
|
||||
import Topline from './Topline';
|
||||
import { getUpdate } from '../../actions';
|
||||
|
||||
const UpdateTopline = (props) => (
|
||||
<Topline type="info">
|
||||
<Fragment>
|
||||
const UpdateTopline = () => {
|
||||
const {
|
||||
announcementUrl,
|
||||
newVersion,
|
||||
canAutoUpdate,
|
||||
processingUpdate,
|
||||
} = useSelector((state) => state.dashboard, shallowEqual);
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const handleUpdate = () => {
|
||||
dispatch(getUpdate());
|
||||
};
|
||||
|
||||
return <Topline type="info">
|
||||
<>
|
||||
<Trans
|
||||
values={{ version: props.version }}
|
||||
values={{ version: newVersion }}
|
||||
components={[
|
||||
<a href={props.url} target="_blank" rel="noopener noreferrer" key="0">
|
||||
<a href={announcementUrl} target="_blank" rel="noopener noreferrer" key="0">
|
||||
Click here
|
||||
</a>,
|
||||
]}
|
||||
>
|
||||
update_announcement
|
||||
</Trans>
|
||||
{props.canAutoUpdate
|
||||
&& <button
|
||||
type="button"
|
||||
className="btn btn-sm btn-primary ml-3"
|
||||
onClick={props.getUpdate}
|
||||
disabled={props.processingUpdate}
|
||||
>
|
||||
<Trans>update_now</Trans>
|
||||
</button>
|
||||
{canAutoUpdate
|
||||
&& <button
|
||||
type="button"
|
||||
className="btn btn-sm btn-primary ml-3"
|
||||
onClick={handleUpdate}
|
||||
disabled={processingUpdate}
|
||||
>
|
||||
<Trans>update_now</Trans>
|
||||
</button>
|
||||
}
|
||||
</Fragment>
|
||||
</Topline>
|
||||
);
|
||||
|
||||
UpdateTopline.propTypes = {
|
||||
version: PropTypes.string,
|
||||
url: PropTypes.string.isRequired,
|
||||
canAutoUpdate: PropTypes.bool,
|
||||
getUpdate: PropTypes.func,
|
||||
processingUpdate: PropTypes.bool,
|
||||
</>
|
||||
</Topline>;
|
||||
};
|
||||
|
||||
export default withTranslation()(UpdateTopline);
|
||||
export default UpdateTopline;
|
||||
|
||||
@@ -1,13 +1,21 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { Trans, withTranslation } from 'react-i18next';
|
||||
|
||||
import { Trans, useTranslation } from 'react-i18next';
|
||||
import { shallowEqual, useDispatch, useSelector } from 'react-redux';
|
||||
import { getVersion } from '../../actions';
|
||||
import './Version.css';
|
||||
|
||||
const Version = (props) => {
|
||||
const Version = () => {
|
||||
const dispatch = useDispatch();
|
||||
const { t } = useTranslation();
|
||||
const {
|
||||
dnsVersion, processingVersion, t, checkUpdateFlag,
|
||||
} = props;
|
||||
dnsVersion,
|
||||
processingVersion,
|
||||
checkUpdateFlag,
|
||||
} = useSelector((state) => state?.dashboard ?? {}, shallowEqual);
|
||||
|
||||
const onClick = () => {
|
||||
dispatch(getVersion(true));
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="version">
|
||||
@@ -20,7 +28,7 @@ const Version = (props) => {
|
||||
{checkUpdateFlag && <button
|
||||
type="button"
|
||||
className="btn btn-icon btn-icon-sm btn-outline-primary btn-sm ml-2"
|
||||
onClick={() => props.getVersion(true)}
|
||||
onClick={onClick}
|
||||
disabled={processingVersion}
|
||||
title={t('check_updates_now')}
|
||||
>
|
||||
@@ -33,12 +41,4 @@ const Version = (props) => {
|
||||
);
|
||||
};
|
||||
|
||||
Version.propTypes = {
|
||||
dnsVersion: PropTypes.string,
|
||||
getVersion: PropTypes.func,
|
||||
processingVersion: PropTypes.bool,
|
||||
checkUpdateFlag: PropTypes.bool,
|
||||
t: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
export default withTranslation()(Version);
|
||||
export default Version;
|
||||
|
||||
@@ -1 +1,4 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="#9aa0ac" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-chevron-down"><polyline points="6 9 12 15 18 9"></polyline></svg>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="#9aa0ac"
|
||||
stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-chevron-down">
|
||||
<polyline points="6 9 12 15 18 9"></polyline>
|
||||
</svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 264 B After Width: | Height: | Size: 276 B |
@@ -1 +1,5 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="#9aa0ac" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-globe"><circle cx="12" cy="12" r="10"/><path d="M2 12h20M12 2a15.3 15.3 0 0 1 4 10 15.3 15.3 0 0 1-4 10 15.3 15.3 0 0 1-4-10 15.3 15.3 0 0 1 4-10z"/></svg>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="#9aa0ac"
|
||||
stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-globe">
|
||||
<circle cx="12" cy="12" r="10"/>
|
||||
<path d="M2 12h20M12 2a15.3 15.3 0 0 1 4 10 15.3 15.3 0 0 1-4 10 15.3 15.3 0 0 1-4-10 15.3 15.3 0 0 1 4-10z"/>
|
||||
</svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 354 B After Width: | Height: | Size: 371 B |
@@ -1 +1,6 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="#9aa0ac" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-help-circle"><circle cx="12" cy="12" r="10"></circle><path d="M9.09 9a3 3 0 0 1 5.83 1c0 2-3 3-3 3"></path><line x1="12" y1="17" x2="12" y2="17"></line></svg>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="#9aa0ac"
|
||||
stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-help-circle">
|
||||
<circle cx="12" cy="12" r="10"></circle>
|
||||
<path d="M9.09 9a3 3 0 0 1 5.83 1c0 2-3 3-3 3"></path>
|
||||
<line x1="12" y1="17" x2="12" y2="17"></line>
|
||||
</svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 357 B After Width: | Height: | Size: 379 B |
@@ -1 +1,8 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="164" height="41" viewBox="0 0 164 41"><g fill-rule="evenodd"><path d="M129.984 22l-1.162-2.945h-5.792L121.931 22H118l6.277-15h3.509L134 22h-4.016zm-4.016-10.996l-1.902 5.149h3.762l-1.86-5.149zM117 16.1c0 .88-.153 1.682-.46 2.404a5.223 5.223 0 0 1-1.318 1.857c-.57.516-1.26.918-2.066 1.207-.807.289-1.703.433-2.688.433-1 0-1.9-.144-2.699-.433-.8-.29-1.477-.691-2.034-1.207a5.232 5.232 0 0 1-1.285-1.857c-.3-.722-.45-1.524-.45-2.404V7h3.64v8.81c0 .4.054.777.161 1.135.108.358.272.677.493.96.221.281.514.505.878.67.364.165.803.248 1.317.248.514 0 .953-.083 1.317-.248.365-.165.66-.389.89-.67.228-.283.392-.602.492-.96.1-.358.15-.736.15-1.135V7H117v9.099zm-16 4.673c-.733.362-1.59.658-2.57.886-.98.228-2.047.342-3.203.342-1.199 0-2.302-.181-3.31-.544-1.008-.362-1.875-.872-2.601-1.53a6.977 6.977 0 0 1-1.703-2.366c-.409-.92-.613-1.943-.613-3.07 0-1.141.208-2.175.624-3.1a6.903 6.903 0 0 1 1.723-2.367 7.71 7.71 0 0 1 2.58-1.5C92.914 7.174 93.98 7 95.121 7c1.184 0 2.284.171 3.299.513 1.015.343 1.84.802 2.474 1.38l-2.284 2.476c-.352-.39-.817-.708-1.395-.956-.579-.249-1.234-.373-1.967-.373-.635 0-1.22.111-1.756.332a4.23 4.23 0 0 0-1.395.927 4.178 4.178 0 0 0-.92 1.41 4.734 4.734 0 0 0-.328 1.78c0 .659.099 1.263.296 1.813.197.55.49 1.024.878 1.42.387.395.867.704 1.438.926.57.221 1.223.332 1.956.332.423 0 .825-.03 1.205-.09.381-.061.733-.158 1.058-.293V16h-2.855v-2.779H101v7.55zm63-6.314c0 1.313-.244 2.447-.73 3.4a6.855 6.855 0 0 1-1.928 2.352 8.035 8.035 0 0 1-2.7 1.356 10.94 10.94 0 0 1-3.05.434H150V7h5.422c1.06 0 2.104.124 3.135.37a7.866 7.866 0 0 1 2.753 1.23c.805.572 1.454 1.338 1.949 2.298.494.96.741 2.147.741 3.56zm-3.77 0c0-.848-.138-1.55-.413-2.108a3.549 3.549 0 0 0-1.101-1.335 4.405 4.405 0 0 0-1.568-.71 7.7 7.7 0 0 0-1.81-.212h-1.8v8.771h1.715c.65 0 1.274-.074 1.874-.222a4.43 4.43 0 0 0 1.589-.731 3.62 3.62 0 0 0 1.1-1.356c.276-.565.414-1.264.414-2.097zm-75.23 0c0 1.313-.244 2.447-.73 3.4a6.855 6.855 0 0 1-1.928 2.352 8.035 8.035 0 0 1-2.7 1.356 10.94 10.94 0 0 1-3.05.434H71V7h5.422c1.06 0 2.104.124 3.135.37A7.866 7.866 0 0 1 82.31 8.6c.805.572 1.454 1.338 1.949 2.298.494.96.741 2.147.741 3.56zm-3.77 0c0-.848-.138-1.55-.413-2.108a3.549 3.549 0 0 0-1.101-1.335 4.405 4.405 0 0 0-1.568-.71 7.7 7.7 0 0 0-1.81-.212h-1.8v8.771h1.715c.65 0 1.274-.074 1.874-.222a4.43 4.43 0 0 0 1.589-.731 3.62 3.62 0 0 0 1.1-1.356c.276-.565.414-1.264.414-2.097zM65.984 22l-1.162-2.945H59.03L57.931 22H54l6.277-15h3.509L70 22h-4.016zm-4.016-10.996l-1.902 5.149h3.762l-1.86-5.149zM143.855 22l-3.171-5.953h-1.202V22H136V7h5.596c.705 0 1.392.074 2.062.222.67.149 1.271.4 1.803.753a3.9 3.9 0 0 1 1.275 1.398c.318.579.476 1.3.476 2.16 0 1.018-.269 1.872-.808 2.564-.539.693-1.285 1.187-2.238 1.484L148 22h-4.145zm-.145-10.403c0-.353-.073-.639-.218-.858a1.502 1.502 0 0 0-.56-.508 2.393 2.393 0 0 0-.766-.244 5.535 5.535 0 0 0-.819-.063h-1.886v3.495h1.679c.29 0 .587-.024.891-.074.304-.05.58-.137.83-.264.248-.128.452-.311.61-.551.16-.24.239-.551.239-.933zM55 37.851v-8.702h.951v3.866h4.866V29.15h.952v8.702h-.952v-3.916h-4.866v3.916H55zM68.068 38c-2.565 0-4.288-2.076-4.288-4.5 0-2.4 1.747-4.5 4.312-4.5 2.565 0 4.288 2.076 4.288 4.5 0 2.4-1.747 4.5-4.312 4.5zm.024-.907c1.927 0 3.3-1.592 3.3-3.593 0-1.977-1.397-3.593-3.324-3.593-1.927 0-3.3 1.592-3.3 3.593 0 1.977 1.397 3.593 3.324 3.593zm6.3.758v-8.702h.963l3.07 4.749 3.072-4.749h.964v8.702h-.952v-7.049l-3.071 4.662h-.048l-3.071-4.65v7.037h-.928zm10.453 0v-8.702h6.095v.895h-5.143v2.971h4.6v.895h-4.6v3.046H91v.895h-6.155z"/><path fill-rule="nonzero" d="M2.831 14.045c.775 4.287 2.266 8.333 4.685 12.143 2.958 4.659 7.21 8.797 12.984 12.319 5.774-3.522 10.026-7.66 12.984-12.319 2.42-3.81 3.91-7.856 4.685-12.143.489-2.706.644-4.844.672-8.003C33.368 3.522 26.636 2.14 20.5 2.14c-6.137 0-12.869 1.381-18.341 3.9.028 3.16.183 5.298.672 8.004zM20.5 0C26.908 0 34.637 1.47 41 4.706c0 6.988.087 24.398-20.5 36.294C-.088 29.104 0 11.694 0 4.706 6.363 1.47 14.092 0 20.5 0z"/><path d="M20.234 27L33 11.344c-.935-.682-1.756-.2-2.208.172l-.016.001-10.644 10.076-4.01-4.392c-1.913-2.011-4.514-.477-5.122-.072L20.234 27"/></g></svg>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="164" height="41" viewBox="0 0 164 41">
|
||||
<g fill-rule="evenodd">
|
||||
<path d="M129.984 22l-1.162-2.945h-5.792L121.931 22H118l6.277-15h3.509L134 22h-4.016zm-4.016-10.996l-1.902 5.149h3.762l-1.86-5.149zM117 16.1c0 .88-.153 1.682-.46 2.404a5.223 5.223 0 0 1-1.318 1.857c-.57.516-1.26.918-2.066 1.207-.807.289-1.703.433-2.688.433-1 0-1.9-.144-2.699-.433-.8-.29-1.477-.691-2.034-1.207a5.232 5.232 0 0 1-1.285-1.857c-.3-.722-.45-1.524-.45-2.404V7h3.64v8.81c0 .4.054.777.161 1.135.108.358.272.677.493.96.221.281.514.505.878.67.364.165.803.248 1.317.248.514 0 .953-.083 1.317-.248.365-.165.66-.389.89-.67.228-.283.392-.602.492-.96.1-.358.15-.736.15-1.135V7H117v9.099zm-16 4.673c-.733.362-1.59.658-2.57.886-.98.228-2.047.342-3.203.342-1.199 0-2.302-.181-3.31-.544-1.008-.362-1.875-.872-2.601-1.53a6.977 6.977 0 0 1-1.703-2.366c-.409-.92-.613-1.943-.613-3.07 0-1.141.208-2.175.624-3.1a6.903 6.903 0 0 1 1.723-2.367 7.71 7.71 0 0 1 2.58-1.5C92.914 7.174 93.98 7 95.121 7c1.184 0 2.284.171 3.299.513 1.015.343 1.84.802 2.474 1.38l-2.284 2.476c-.352-.39-.817-.708-1.395-.956-.579-.249-1.234-.373-1.967-.373-.635 0-1.22.111-1.756.332a4.23 4.23 0 0 0-1.395.927 4.178 4.178 0 0 0-.92 1.41 4.734 4.734 0 0 0-.328 1.78c0 .659.099 1.263.296 1.813.197.55.49 1.024.878 1.42.387.395.867.704 1.438.926.57.221 1.223.332 1.956.332.423 0 .825-.03 1.205-.09.381-.061.733-.158 1.058-.293V16h-2.855v-2.779H101v7.55zm63-6.314c0 1.313-.244 2.447-.73 3.4a6.855 6.855 0 0 1-1.928 2.352 8.035 8.035 0 0 1-2.7 1.356 10.94 10.94 0 0 1-3.05.434H150V7h5.422c1.06 0 2.104.124 3.135.37a7.866 7.866 0 0 1 2.753 1.23c.805.572 1.454 1.338 1.949 2.298.494.96.741 2.147.741 3.56zm-3.77 0c0-.848-.138-1.55-.413-2.108a3.549 3.549 0 0 0-1.101-1.335 4.405 4.405 0 0 0-1.568-.71 7.7 7.7 0 0 0-1.81-.212h-1.8v8.771h1.715c.65 0 1.274-.074 1.874-.222a4.43 4.43 0 0 0 1.589-.731 3.62 3.62 0 0 0 1.1-1.356c.276-.565.414-1.264.414-2.097zm-75.23 0c0 1.313-.244 2.447-.73 3.4a6.855 6.855 0 0 1-1.928 2.352 8.035 8.035 0 0 1-2.7 1.356 10.94 10.94 0 0 1-3.05.434H71V7h5.422c1.06 0 2.104.124 3.135.37A7.866 7.866 0 0 1 82.31 8.6c.805.572 1.454 1.338 1.949 2.298.494.96.741 2.147.741 3.56zm-3.77 0c0-.848-.138-1.55-.413-2.108a3.549 3.549 0 0 0-1.101-1.335 4.405 4.405 0 0 0-1.568-.71 7.7 7.7 0 0 0-1.81-.212h-1.8v8.771h1.715c.65 0 1.274-.074 1.874-.222a4.43 4.43 0 0 0 1.589-.731 3.62 3.62 0 0 0 1.1-1.356c.276-.565.414-1.264.414-2.097zM65.984 22l-1.162-2.945H59.03L57.931 22H54l6.277-15h3.509L70 22h-4.016zm-4.016-10.996l-1.902 5.149h3.762l-1.86-5.149zM143.855 22l-3.171-5.953h-1.202V22H136V7h5.596c.705 0 1.392.074 2.062.222.67.149 1.271.4 1.803.753a3.9 3.9 0 0 1 1.275 1.398c.318.579.476 1.3.476 2.16 0 1.018-.269 1.872-.808 2.564-.539.693-1.285 1.187-2.238 1.484L148 22h-4.145zm-.145-10.403c0-.353-.073-.639-.218-.858a1.502 1.502 0 0 0-.56-.508 2.393 2.393 0 0 0-.766-.244 5.535 5.535 0 0 0-.819-.063h-1.886v3.495h1.679c.29 0 .587-.024.891-.074.304-.05.58-.137.83-.264.248-.128.452-.311.61-.551.16-.24.239-.551.239-.933zM55 37.851v-8.702h.951v3.866h4.866V29.15h.952v8.702h-.952v-3.916h-4.866v3.916H55zM68.068 38c-2.565 0-4.288-2.076-4.288-4.5 0-2.4 1.747-4.5 4.312-4.5 2.565 0 4.288 2.076 4.288 4.5 0 2.4-1.747 4.5-4.312 4.5zm.024-.907c1.927 0 3.3-1.592 3.3-3.593 0-1.977-1.397-3.593-3.324-3.593-1.927 0-3.3 1.592-3.3 3.593 0 1.977 1.397 3.593 3.324 3.593zm6.3.758v-8.702h.963l3.07 4.749 3.072-4.749h.964v8.702h-.952v-7.049l-3.071 4.662h-.048l-3.071-4.65v7.037h-.928zm10.453 0v-8.702h6.095v.895h-5.143v2.971h4.6v.895h-4.6v3.046H91v.895h-6.155z"/>
|
||||
<path fill-rule="nonzero"
|
||||
d="M2.831 14.045c.775 4.287 2.266 8.333 4.685 12.143 2.958 4.659 7.21 8.797 12.984 12.319 5.774-3.522 10.026-7.66 12.984-12.319 2.42-3.81 3.91-7.856 4.685-12.143.489-2.706.644-4.844.672-8.003C33.368 3.522 26.636 2.14 20.5 2.14c-6.137 0-12.869 1.381-18.341 3.9.028 3.16.183 5.298.672 8.004zM20.5 0C26.908 0 34.637 1.47 41 4.706c0 6.988.087 24.398-20.5 36.294C-.088 29.104 0 11.694 0 4.706 6.363 1.47 14.092 0 20.5 0z"/>
|
||||
<path d="M20.234 27L33 11.344c-.935-.682-1.756-.2-2.208.172l-.016.001-10.644 10.076-4.01-4.392c-1.913-2.011-4.514-.477-5.122-.072L20.234 27"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 4.0 KiB After Width: | Height: | Size: 4.1 KiB |
@@ -1 +1,7 @@
|
||||
<svg stroke="#84868c" fill="none" height="24" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" viewBox="0 0 24 24" width="24" xmlns="http://www.w3.org/2000/svg"><path d="m3 6h2 16"/><path d="m19 6v14a2 2 0 0 1 -2 2h-10a2 2 0 0 1 -2-2v-14m3 0v-2a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"/><path d="m10 11v6"/><path d="m14 11v6"/></svg>
|
||||
<svg stroke="#84868c" fill="none" height="24" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||
viewBox="0 0 24 24" width="24" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="m3 6h2 16"/>
|
||||
<path d="m19 6v14a2 2 0 0 1 -2 2h-10a2 2 0 0 1 -2-2v-14m3 0v-2a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"/>
|
||||
<path d="m10 11v6"/>
|
||||
<path d="m14 11v6"/>
|
||||
</svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 340 B After Width: | Height: | Size: 367 B |
@@ -1 +1,5 @@
|
||||
<svg stroke="#84868c" fill="none" height="24" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" viewBox="0 0 24 24" width="24" xmlns="http://www.w3.org/2000/svg"><path d="m18 6-12 12"/><path d="m6 6 12 12"/></svg>
|
||||
<svg stroke="#84868c" fill="none" height="24" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||
viewBox="0 0 24 24" width="24" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="m18 6-12 12"/>
|
||||
<path d="m6 6 12 12"/>
|
||||
</svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 227 B After Width: | Height: | Size: 244 B |
@@ -1,14 +0,0 @@
|
||||
import { connect } from 'react-redux';
|
||||
import * as actionCreators from '../actions';
|
||||
import App from '../components/App';
|
||||
|
||||
const mapStateToProps = (state) => {
|
||||
const { dashboard, encryption } = state;
|
||||
const props = { dashboard, encryption };
|
||||
return props;
|
||||
};
|
||||
|
||||
export default connect(
|
||||
mapStateToProps,
|
||||
actionCreators,
|
||||
)(App);
|
||||
@@ -1,18 +0,0 @@
|
||||
import { connect } from 'react-redux';
|
||||
import { getVersion } from '../actions';
|
||||
import Header from '../components/Header';
|
||||
|
||||
const mapStateToProps = (state) => {
|
||||
const { dashboard } = state;
|
||||
const props = { dashboard };
|
||||
return props;
|
||||
};
|
||||
|
||||
const mapDispatchToProps = {
|
||||
getVersion,
|
||||
};
|
||||
|
||||
export default connect(
|
||||
mapStateToProps,
|
||||
mapDispatchToProps,
|
||||
)(Header);
|
||||
@@ -1,13 +1,23 @@
|
||||
export const R_URL_REQUIRES_PROTOCOL = /^https?:\/\/[^/\s]+(\/.*)?$/;
|
||||
export const R_HOST = /^(\*\.)?([\w-]+\.)+[\w-]+$/;
|
||||
|
||||
// matches hostname or *.wildcard
|
||||
export const R_HOST = /^(\*\.)?[\w.-]+$/;
|
||||
|
||||
export const R_IPV4 = /^(?:(?:^|\.)(?:2(?:5[0-5]|[0-4]\d)|1?\d?\d)){4}$/;
|
||||
|
||||
export const R_IPV6 = /^(([0-9a-fA-F]{1,4}:){7,7}[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,7}:|([0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,5}(:[0-9a-fA-F]{1,4}){1,2}|([0-9a-fA-F]{1,4}:){1,4}(:[0-9a-fA-F]{1,4}){1,3}|([0-9a-fA-F]{1,4}:){1,3}(:[0-9a-fA-F]{1,4}){1,4}|([0-9a-fA-F]{1,4}:){1,2}(:[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}:((:[0-9a-fA-F]{1,4}){1,6})|:((:[0-9a-fA-F]{1,4}){1,7}|:)|fe80:(:[0-9a-fA-F]{0,4}){0,4}%[0-9a-zA-Z]{1,}|::(ffff(:0{1,4}){0,1}:){0,1}((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])|([0-9a-fA-F]{1,4}:){1,4}:((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9]))$/;
|
||||
|
||||
export const R_CIDR = /^(([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\.){3}([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])(\/([0-9]|[1-2][0-9]|3[0-2]))$/;
|
||||
|
||||
export const R_MAC = /^((([a-fA-F0-9][a-fA-F0-9]+[-]){5}|([a-fA-F0-9][a-fA-F0-9]+[:]){5})([a-fA-F0-9][a-fA-F0-9])$)|(^([a-fA-F0-9][a-fA-F0-9][a-fA-F0-9][a-fA-F0-9]+[.]){2}([a-fA-F0-9][a-fA-F0-9][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_PATH_LAST_PART = /\/[^/]*$/;
|
||||
|
||||
// eslint-disable-next-line no-control-regex
|
||||
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]*$/;
|
||||
|
||||
@@ -62,6 +72,7 @@ export const CHECK_TIMEOUT = 1000;
|
||||
export const SUCCESS_TOAST_TIMEOUT = 5000;
|
||||
export const FAILURE_TOAST_TIMEOUT = 30000;
|
||||
export const HIDE_TOOLTIP_DELAY = 300;
|
||||
export const SHOW_TOOLTIP_DELAY = 200;
|
||||
export const MODAL_OPEN_TIMEOUT = 150;
|
||||
|
||||
export const UNSAFE_PORTS = [
|
||||
@@ -288,50 +299,6 @@ export const WHOIS_ICONS = {
|
||||
descr: '',
|
||||
};
|
||||
|
||||
export const DNS_RECORD_TYPES = [
|
||||
'A',
|
||||
'AAAA',
|
||||
'AFSDB',
|
||||
'APL',
|
||||
'CAA',
|
||||
'CDNSKEY',
|
||||
'CDS',
|
||||
'CERT',
|
||||
'CNAME',
|
||||
'CSYNC',
|
||||
'DHCID',
|
||||
'DLV',
|
||||
'DNAME',
|
||||
'DNSKEY',
|
||||
'DS',
|
||||
'HIP',
|
||||
'IPSECKEY',
|
||||
'KEY',
|
||||
'KX',
|
||||
'LOC',
|
||||
'MX',
|
||||
'NAPTR',
|
||||
'NS',
|
||||
'NSEC',
|
||||
'NSEC3',
|
||||
'NSEC3PARAM',
|
||||
'OPENPGPKEY',
|
||||
'PTR',
|
||||
'RRSIG',
|
||||
'RP',
|
||||
'SIG',
|
||||
'SMIMEA',
|
||||
'SOA',
|
||||
'SRV',
|
||||
'SSHFP',
|
||||
'TA',
|
||||
'TKEY',
|
||||
'TLSA',
|
||||
'TSIG',
|
||||
'TXT',
|
||||
'URI',
|
||||
];
|
||||
|
||||
export const DEFAULT_LOGS_FILTER = {
|
||||
search: '',
|
||||
response_status: '',
|
||||
@@ -339,7 +306,7 @@ export const DEFAULT_LOGS_FILTER = {
|
||||
|
||||
export const DEFAULT_LANGUAGE = 'en';
|
||||
|
||||
export const TABLE_DEFAULT_PAGE_SIZE = 50;
|
||||
export const TABLE_DEFAULT_PAGE_SIZE = 25;
|
||||
|
||||
export const TABLE_FIRST_PAGE = 0;
|
||||
|
||||
@@ -370,11 +337,6 @@ export const RESPONSE_FILTER = {
|
||||
query: 'processed',
|
||||
label: 'show_processed_responses',
|
||||
},
|
||||
SPACE: {
|
||||
query: 'all',
|
||||
label: '',
|
||||
disabled: true,
|
||||
},
|
||||
BLOCKED: {
|
||||
query: 'blocked',
|
||||
label: 'show_blocked_responses',
|
||||
@@ -401,10 +363,11 @@ export const RESPONSE_FILTER = {
|
||||
},
|
||||
};
|
||||
|
||||
export const RESPONSE_FILTER_QUERIES = Object.values(RESPONSE_FILTER).reduce((acc, { query }) => {
|
||||
acc[query] = query;
|
||||
return acc;
|
||||
}, {});
|
||||
export const RESPONSE_FILTER_QUERIES = Object.values(RESPONSE_FILTER)
|
||||
.reduce((acc, { query }) => {
|
||||
acc[query] = query;
|
||||
return acc;
|
||||
}, {});
|
||||
|
||||
export const FILTERED_STATUS_TO_META_MAP = {
|
||||
[FILTERED_STATUS.NOT_FILTERED_WHITE_LIST]: {
|
||||
@@ -516,6 +479,7 @@ export const FORM_NAME = {
|
||||
CACHE: 'cache',
|
||||
};
|
||||
|
||||
export const smallScreenSize = 767;
|
||||
export const SMALL_SCREEN_SIZE = 767;
|
||||
export const MEDIUM_SCREEN_SIZE = 1023;
|
||||
|
||||
export const SECONDS_IN_HOUR = 60 * 60;
|
||||
|
||||
@@ -75,7 +75,7 @@
|
||||
"the-big-list-of-hacked-malware-web-sites": {
|
||||
"name": "The Big List of Hacked Malware Web Sites",
|
||||
"categoryId": "security",
|
||||
"homepage": "https://github.com/hoshsadiq/adblock-nocoin-list/",
|
||||
"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"
|
||||
},
|
||||
"scam-blocklist-by-durable-napkin": {
|
||||
|
||||
@@ -31,10 +31,11 @@ export const formatClientCell = (row, isDetailed = false, isLogs = true) => {
|
||||
|
||||
if (info) {
|
||||
const { name, whois_info } = info;
|
||||
const whoisAvailable = whois_info && Object.keys(whois_info).length > 0;
|
||||
|
||||
if (name) {
|
||||
if (isLogs) {
|
||||
nameContainer = !whois_info && isDetailed
|
||||
nameContainer = !whoisAvailable && isDetailed
|
||||
? (
|
||||
<small title={value}>{value}</small>
|
||||
) : (
|
||||
@@ -54,7 +55,7 @@ export const formatClientCell = (row, isDetailed = false, isLogs = true) => {
|
||||
}
|
||||
}
|
||||
|
||||
if (whois_info && isDetailed) {
|
||||
if (whoisAvailable && isDetailed) {
|
||||
whoisContainer = (
|
||||
<div className="logs__text logs__text--wrap logs__text--whois">
|
||||
{getFormattedWhois(whois_info)}
|
||||
|
||||
@@ -5,7 +5,6 @@ import subHours from 'date-fns/sub_hours';
|
||||
import addHours from 'date-fns/add_hours';
|
||||
import addDays from 'date-fns/add_days';
|
||||
import subDays from 'date-fns/sub_days';
|
||||
import isSameDay from 'date-fns/is_same_day';
|
||||
import round from 'lodash/round';
|
||||
import axios from 'axios';
|
||||
import i18n from 'i18next';
|
||||
@@ -20,7 +19,6 @@ import {
|
||||
DEFAULT_LANGUAGE,
|
||||
DEFAULT_TIME_FORMAT,
|
||||
DETAILED_DATE_FORMAT_OPTIONS,
|
||||
DNS_RECORD_TYPES,
|
||||
FILTERED,
|
||||
FILTERED_STATUS,
|
||||
IP_MATCH_LIST_STATUS,
|
||||
@@ -31,6 +29,7 @@ import {
|
||||
|
||||
/**
|
||||
* @param time {string} The time to format
|
||||
* @param options {string}
|
||||
* @returns {string} Returns the time in the format HH:mm:ss
|
||||
*/
|
||||
export const formatTime = (time, options = DEFAULT_TIME_FORMAT) => {
|
||||
@@ -60,12 +59,6 @@ export const formatDetailedDateTime = (dateTime) => formatDateTime(
|
||||
dateTime, DETAILED_DATE_FORMAT_OPTIONS,
|
||||
);
|
||||
|
||||
/**
|
||||
* @param date {string}
|
||||
* @returns {boolean}
|
||||
*/
|
||||
export const isToday = (date) => isSameDay(new Date(date), new Date());
|
||||
|
||||
export const normalizeLogs = (logs) => logs.map((log) => {
|
||||
const {
|
||||
answer,
|
||||
@@ -351,39 +344,6 @@ export const normalizeTopClients = (topClients) => topClients.reduce(
|
||||
},
|
||||
);
|
||||
|
||||
export const getClientInfo = (clients, ip) => {
|
||||
const client = clients
|
||||
.find((item) => item.ip_addrs?.find((clientIp) => clientIp === ip));
|
||||
|
||||
if (!client) {
|
||||
return '';
|
||||
}
|
||||
|
||||
const { name, whois_info } = client;
|
||||
const whois = Object.keys(whois_info).length > 0 ? whois_info : '';
|
||||
|
||||
return {
|
||||
name,
|
||||
whois,
|
||||
};
|
||||
};
|
||||
|
||||
export const getAutoClientInfo = (clients, ip) => {
|
||||
const client = clients.find((item) => ip === item.ip);
|
||||
|
||||
if (!client) {
|
||||
return '';
|
||||
}
|
||||
|
||||
const { name, whois_info } = client;
|
||||
const whois = Object.keys(whois_info).length > 0 ? whois_info : '';
|
||||
|
||||
return {
|
||||
name,
|
||||
whois,
|
||||
};
|
||||
};
|
||||
|
||||
export const sortClients = (clients) => {
|
||||
const compare = (a, b) => {
|
||||
const nameA = a.name.toUpperCase();
|
||||
@@ -443,8 +403,6 @@ export const normalizeWhois = (whois) => {
|
||||
return whois;
|
||||
};
|
||||
|
||||
export const isValidQuestionType = (type) => type && DNS_RECORD_TYPES.includes(type.toUpperCase());
|
||||
|
||||
export const getPathWithQueryString = (path, params) => {
|
||||
const searchParams = new URLSearchParams(params);
|
||||
|
||||
@@ -542,10 +500,10 @@ export const getMap = (arr, key, value) => arr.reduce((acc, curr) => {
|
||||
|
||||
/**
|
||||
* @param parsedIp {object} ipaddr.js IPv4 or IPv6 object
|
||||
* @param cidr {array} ipaddr.js CIDR array
|
||||
* @param parsedCidr {array} ipaddr.js CIDR array
|
||||
* @returns {boolean}
|
||||
*/
|
||||
export const isIpMatchCidr = (parsedIp, parsedCidr) => {
|
||||
const isIpMatchCidr = (parsedIp, parsedCidr) => {
|
||||
try {
|
||||
const cidrIpVersion = parsedCidr[0].kind();
|
||||
const ipVersion = parsedIp.kind();
|
||||
@@ -556,6 +514,75 @@ export const isIpMatchCidr = (parsedIp, parsedCidr) => {
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* The purpose of this method is to quickly check
|
||||
* if this IP can possibly be in the specified CIDR range.
|
||||
*
|
||||
* @param ip {string}
|
||||
* @param listItem {string}
|
||||
* @returns {boolean}
|
||||
*/
|
||||
const isIpQuickMatchCIDR = (ip, listItem) => {
|
||||
const ipv6 = ip.indexOf(':') !== -1;
|
||||
const cidrIpv6 = listItem.indexOf(':') !== -1;
|
||||
if (ipv6 !== cidrIpv6) {
|
||||
// CIDR is for a different IP type
|
||||
return false;
|
||||
}
|
||||
|
||||
if (cidrIpv6) {
|
||||
// We don't do quick check for IPv6 addresses
|
||||
return true;
|
||||
}
|
||||
|
||||
const idx = listItem.indexOf('/');
|
||||
if (idx === -1) {
|
||||
// Not a CIDR, return false immediately
|
||||
return false;
|
||||
}
|
||||
|
||||
const cidrIp = listItem.substring(0, idx);
|
||||
const cidrRange = parseInt(listItem.substring(idx + 1), 10);
|
||||
if (Number.isNaN(cidrRange)) {
|
||||
// Not a valid CIDR
|
||||
return false;
|
||||
}
|
||||
|
||||
const parts = cidrIp.split('.');
|
||||
if (parts.length !== 4) {
|
||||
// Invalid IP, return immediately
|
||||
return false;
|
||||
}
|
||||
|
||||
// Now depending on the range we check if the IP can possibly be in that range
|
||||
if (cidrRange < 8) {
|
||||
// Use the slow approach
|
||||
return true;
|
||||
}
|
||||
|
||||
if (cidrRange < 16) {
|
||||
// Check the first part
|
||||
// Example: 0.0.0.0/8 matches 0.*.*.*
|
||||
return ip.indexOf(`${parts[0]}.`) === 0;
|
||||
}
|
||||
|
||||
if (cidrRange < 24) {
|
||||
// Check the first two parts
|
||||
// Example: 0.0.0.0/16 matches 0.0.*.*
|
||||
return ip.indexOf(`${parts[0]}.${parts[1]}.`) === 0;
|
||||
}
|
||||
|
||||
if (cidrRange <= 32) {
|
||||
// Check the first two parts
|
||||
// Example: 0.0.0.0/16 matches 0.0.*.*
|
||||
return ip.indexOf(`${parts[0]}.${parts[1]}.${parts[2]}.`) === 0;
|
||||
}
|
||||
|
||||
// range for IPv4 CIDR cannot be more than 32
|
||||
// no need to check further, this CIDR is invalid
|
||||
return false;
|
||||
};
|
||||
|
||||
/**
|
||||
* @param ip {string}
|
||||
* @param list {string}
|
||||
@@ -573,20 +600,29 @@ export const getIpMatchListStatus = (ip, list) => {
|
||||
for (let i = 0; i < listArr.length; i += 1) {
|
||||
const listItem = listArr[i];
|
||||
|
||||
const parsedIp = ipaddr.parse(ip);
|
||||
const isItemAnIp = ipaddr.isValid(listItem);
|
||||
const parsedItem = isItemAnIp ? ipaddr.parse(listItem) : ipaddr.parseCIDR(listItem);
|
||||
|
||||
if (isItemAnIp && parsedIp.toString() === parsedItem.toString()) {
|
||||
if (ip === listItem.trim()) {
|
||||
return IP_MATCH_LIST_STATUS.EXACT;
|
||||
}
|
||||
|
||||
if (!isItemAnIp && isIpMatchCidr(parsedIp, parsedItem)) {
|
||||
return IP_MATCH_LIST_STATUS.CIDR;
|
||||
// Using ipaddr.js is quite slow so we first do a quick check
|
||||
// to see if it's possible that this IP may be in the specified CIDR range
|
||||
if (isIpQuickMatchCIDR(ip, listItem)) {
|
||||
const parsedIp = ipaddr.parse(ip);
|
||||
const isItemAnIp = ipaddr.isValid(listItem);
|
||||
const parsedItem = isItemAnIp ? ipaddr.parse(listItem) : ipaddr.parseCIDR(listItem);
|
||||
|
||||
if (isItemAnIp && parsedIp.toString() === parsedItem.toString()) {
|
||||
return IP_MATCH_LIST_STATUS.EXACT;
|
||||
}
|
||||
|
||||
if (!isItemAnIp && isIpMatchCidr(parsedIp, parsedItem)) {
|
||||
return IP_MATCH_LIST_STATUS.CIDR;
|
||||
}
|
||||
}
|
||||
}
|
||||
return IP_MATCH_LIST_STATUS.NOT_FOUND;
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
return IP_MATCH_LIST_STATUS.NOT_FOUND;
|
||||
}
|
||||
};
|
||||
@@ -633,5 +669,80 @@ export const getLogsUrlParams = (search, response_status) => `?${queryString.str
|
||||
})}`;
|
||||
|
||||
export const processContent = (content) => (Array.isArray(content)
|
||||
? content.filter(([, value]) => value)
|
||||
.flat() : content);
|
||||
? content.filter(([, value]) => value).reduce((acc, val) => acc.concat(val), [])
|
||||
: content
|
||||
);
|
||||
|
||||
/**
|
||||
* @param object {object}
|
||||
* @param sortKey {string}
|
||||
* @returns {string[]}
|
||||
*/
|
||||
export const getObjectKeysSorted = (object, sortKey) => Object.entries(object)
|
||||
.sort(([, { [sortKey]: order1 }], [, { [sortKey]: order2 }]) => order1 - order2)
|
||||
.map(([key]) => key);
|
||||
|
||||
/**
|
||||
* @param ip
|
||||
* @returns {[IPv4|IPv6, 33|129]}
|
||||
*/
|
||||
const getParsedIpWithPrefixLength = (ip) => {
|
||||
const MAX_PREFIX_LENGTH_V4 = 32;
|
||||
const MAX_PREFIX_LENGTH_V6 = 128;
|
||||
|
||||
const parsedIp = ipaddr.parse(ip);
|
||||
const prefixLength = parsedIp.kind() === 'ipv4' ? MAX_PREFIX_LENGTH_V4 : MAX_PREFIX_LENGTH_V6;
|
||||
|
||||
// Increment prefix length to always put IP after CIDR, e.g. 127.0.0.1/32, 127.0.0.1
|
||||
return [parsedIp, prefixLength + 1];
|
||||
};
|
||||
|
||||
/**
|
||||
* Helper function for IP and CIDR comparison (supports both v4 and v6)
|
||||
* @param item - ip or cidr
|
||||
* @returns {number[]}
|
||||
*/
|
||||
const getAddressesComparisonBytes = (item) => {
|
||||
// Sort ipv4 before ipv6
|
||||
const IP_V4_COMPARISON_CODE = 0;
|
||||
const IP_V6_COMPARISON_CODE = 1;
|
||||
|
||||
const [parsedIp, cidr] = ipaddr.isValid(item)
|
||||
? getParsedIpWithPrefixLength(item)
|
||||
: ipaddr.parseCIDR(item);
|
||||
|
||||
const [normalizedBytes, ipVersionComparisonCode] = parsedIp.kind() === 'ipv4'
|
||||
? [parsedIp.toIPv4MappedAddress().parts, IP_V4_COMPARISON_CODE]
|
||||
: [parsedIp.parts, IP_V6_COMPARISON_CODE];
|
||||
|
||||
return [ipVersionComparisonCode, ...normalizedBytes, cidr];
|
||||
};
|
||||
|
||||
/**
|
||||
* Compare function for IP and CIDR sort in ascending order (supports both v4 and v6)
|
||||
* @param a
|
||||
* @param b
|
||||
* @returns {number} -1 | 0 | 1
|
||||
*/
|
||||
export const sortIp = (a, b) => {
|
||||
try {
|
||||
const comparisonBytesA = getAddressesComparisonBytes(a);
|
||||
const comparisonBytesB = getAddressesComparisonBytes(b);
|
||||
|
||||
for (let i = 0; i < comparisonBytesA.length; i += 1) {
|
||||
const byteA = comparisonBytesA[i];
|
||||
const byteB = comparisonBytesB[i];
|
||||
|
||||
if (byteA === byteB) {
|
||||
// eslint-disable-next-line no-continue
|
||||
continue;
|
||||
}
|
||||
return byteA > byteB ? 1 : -1;
|
||||
}
|
||||
|
||||
return 0;
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
return 0;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import { Provider } from 'react-redux';
|
||||
import './components/App/index.css';
|
||||
import App from './containers/App';
|
||||
import configureStore from './configureStore';
|
||||
import reducers from './reducers';
|
||||
import App from './components/App';
|
||||
import './components/App/index.css';
|
||||
import './i18n';
|
||||
|
||||
const store = configureStore(reducers, {}); // set initial state
|
||||
|
||||
ReactDOM.render(
|
||||
<Provider store={store}>
|
||||
<App />
|
||||
|
||||
@@ -42,7 +42,7 @@ const queryLogs = handleActions(
|
||||
const rowsStart = pageSize * page;
|
||||
const rowsEnd = rowsStart + pageSize;
|
||||
const logsSlice = logs.slice(rowsStart, rowsEnd);
|
||||
const isFiltered = Object.keys(filter).some((key) => filter[key]);
|
||||
const isFiltered = filter && Object.keys(filter).some((key) => filter[key]);
|
||||
|
||||
return {
|
||||
...state,
|
||||
|
||||
28
client/webpack.common.js
vendored
@@ -5,6 +5,7 @@ const flexBugsFixes = require('postcss-flexbugs-fixes');
|
||||
const { CleanWebpackPlugin } = require('clean-webpack-plugin');
|
||||
const CopyPlugin = require('copy-webpack-plugin');
|
||||
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
|
||||
const { BUILD_ENVS } = require('./constants');
|
||||
|
||||
const RESOURCES_PATH = path.resolve(__dirname);
|
||||
const ENTRY_REACT = path.resolve(RESOURCES_PATH, 'src/index.js');
|
||||
@@ -18,13 +19,10 @@ const ASSETS_PATH = path.resolve(RESOURCES_PATH, 'public/assets');
|
||||
const PUBLIC_PATH = path.resolve(__dirname, '../build/static');
|
||||
const PUBLIC_ASSETS_PATH = path.resolve(PUBLIC_PATH, 'assets');
|
||||
|
||||
const BUILD_ENVS = {
|
||||
dev: 'development',
|
||||
prod: 'production',
|
||||
};
|
||||
|
||||
const BUILD_ENV = BUILD_ENVS[process.env.BUILD_ENV];
|
||||
|
||||
const isDev = BUILD_ENV === BUILD_ENVS.dev;
|
||||
|
||||
const config = {
|
||||
mode: BUILD_ENV,
|
||||
target: 'web',
|
||||
@@ -36,22 +34,35 @@ const config = {
|
||||
},
|
||||
output: {
|
||||
path: PUBLIC_PATH,
|
||||
filename: '[name].[chunkhash].js',
|
||||
filename: '[name].[hash].js',
|
||||
},
|
||||
resolve: {
|
||||
modules: ['node_modules'],
|
||||
alias: {
|
||||
MainRoot: path.resolve(__dirname, '../'),
|
||||
ClientRoot: path.resolve(__dirname, './src'),
|
||||
// TODO: change to '@hot-loader/react-dom' when v16.13.1 is released
|
||||
// https://stackoverflow.com/a/62671689/12942752
|
||||
'react-dom': 'react-dom',
|
||||
},
|
||||
},
|
||||
module: {
|
||||
rules: [
|
||||
{
|
||||
test: /\.ya?ml$/,
|
||||
type: 'json',
|
||||
use: 'yaml-loader',
|
||||
},
|
||||
{
|
||||
test: /\.css$/i,
|
||||
use: [
|
||||
'style-loader',
|
||||
MiniCssExtractPlugin.loader,
|
||||
{
|
||||
loader: MiniCssExtractPlugin.loader,
|
||||
options: {
|
||||
hmr: isDev,
|
||||
},
|
||||
},
|
||||
{
|
||||
loader: 'css-loader',
|
||||
options: {
|
||||
@@ -122,7 +133,8 @@ const config = {
|
||||
template: HTML_LOGIN_PATH,
|
||||
}),
|
||||
new MiniCssExtractPlugin({
|
||||
filename: '[name].[contenthash].css',
|
||||
filename: isDev ? '[name].css' : '[name].[hash].css',
|
||||
chunkFilename: isDev ? '[id].css' : '[id].[hash].css',
|
||||
}),
|
||||
new CopyPlugin({
|
||||
patterns: [
|
||||
|
||||
46
client/webpack.dev.js
vendored
@@ -1,5 +1,50 @@
|
||||
const merge = require('webpack-merge');
|
||||
const yaml = require('js-yaml');
|
||||
const fs = require('fs');
|
||||
const common = require('./webpack.common.js');
|
||||
const { BASE_URL } = require('./constants');
|
||||
|
||||
const ZERO_HOST = '0.0.0.0';
|
||||
const LOCALHOST = '127.0.0.1';
|
||||
const DEFAULT_PORT = 80;
|
||||
|
||||
/**
|
||||
* Get document, or throw exception on error
|
||||
* @returns {{bind_host: string, bind_port: number}}
|
||||
*/
|
||||
const importConfig = () => {
|
||||
try {
|
||||
const doc = yaml.safeLoad(fs.readFileSync('../AdguardHome.yaml', 'utf8'));
|
||||
const { bind_host, bind_port } = doc;
|
||||
return {
|
||||
bind_host,
|
||||
bind_port,
|
||||
};
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
return {
|
||||
bind_host: ZERO_HOST,
|
||||
bind_port: DEFAULT_PORT,
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
const getDevServerConfig = (proxyUrl = BASE_URL) => {
|
||||
const { bind_host: host, bind_port: port } = importConfig();
|
||||
const { DEV_SERVER_PORT } = process.env;
|
||||
|
||||
const devServerHost = host === ZERO_HOST ? LOCALHOST : host;
|
||||
const devServerPort = DEV_SERVER_PORT || port + 8000;
|
||||
|
||||
return {
|
||||
hot: true,
|
||||
host: devServerHost,
|
||||
port: devServerPort,
|
||||
proxy: {
|
||||
[proxyUrl]: `http://${devServerHost}:${port}`,
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
module.exports = merge(common, {
|
||||
devtool: 'eval-source-map',
|
||||
@@ -16,4 +61,5 @@ module.exports = merge(common, {
|
||||
},
|
||||
],
|
||||
},
|
||||
devServer: process.env.WEBPACK_DEV_SERVER ? getDevServerConfig(BASE_URL) : undefined,
|
||||
});
|
||||
|
||||
19
client/webpack.prod.js
vendored
@@ -2,18 +2,19 @@ const StyleLintPlugin = require('stylelint-webpack-plugin');
|
||||
const merge = require('webpack-merge');
|
||||
const common = require('./webpack.common.js');
|
||||
|
||||
|
||||
module.exports = merge(common, {
|
||||
module: {
|
||||
rules: [{
|
||||
test: /\.js$/,
|
||||
exclude: /node_modules/,
|
||||
loader: 'eslint-loader',
|
||||
options: {
|
||||
failOnError: true,
|
||||
configFile: 'prod.eslintrc',
|
||||
rules: [
|
||||
{
|
||||
test: /\.js$/,
|
||||
exclude: /node_modules/,
|
||||
loader: 'eslint-loader',
|
||||
options: {
|
||||
failOnError: true,
|
||||
configFile: 'prod.eslintrc',
|
||||
},
|
||||
},
|
||||
}],
|
||||
],
|
||||
},
|
||||
plugins: [
|
||||
new StyleLintPlugin({
|
||||
|
||||
@@ -47,9 +47,9 @@ func NewForTest(c *Config, filters []Filter) *Dnsfilter {
|
||||
setts = RequestFilteringSettings{}
|
||||
setts.FilteringEnabled = true
|
||||
if c != nil {
|
||||
c.SafeBrowsingCacheSize = 1000
|
||||
c.SafeBrowsingCacheSize = 10000
|
||||
c.ParentalCacheSize = 10000
|
||||
c.SafeSearchCacheSize = 1000
|
||||
c.ParentalCacheSize = 1000
|
||||
c.CacheTime = 30
|
||||
setts.SafeSearchEnabled = c.SafeSearchEnabled
|
||||
setts.SafeBrowsingEnabled = c.SafeBrowsingEnabled
|
||||
@@ -146,12 +146,6 @@ func TestEtcHostsMatching(t *testing.T) {
|
||||
|
||||
// SAFE BROWSING
|
||||
|
||||
func TestSafeBrowsingHash(t *testing.T) {
|
||||
q, hashes := hostnameToHashParam("1.2.3.4.5.6")
|
||||
assert.Equal(t, "0132d0fa.b5413b4e.5fa067c1.e7f6c011.", q)
|
||||
assert.Equal(t, 4, len(hashes))
|
||||
}
|
||||
|
||||
func TestSafeBrowsing(t *testing.T) {
|
||||
d := NewForTest(&Config{SafeBrowsingEnabled: true}, nil)
|
||||
defer d.Close()
|
||||
|
||||
149
dnsfilter/safe_search.go
Normal file
@@ -0,0 +1,149 @@
|
||||
package dnsfilter
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/binary"
|
||||
"encoding/gob"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/AdguardTeam/golibs/cache"
|
||||
"github.com/AdguardTeam/golibs/log"
|
||||
)
|
||||
|
||||
/*
|
||||
expire byte[4]
|
||||
res Result
|
||||
*/
|
||||
func (d *Dnsfilter) setCacheResult(cache cache.Cache, host string, res Result) int {
|
||||
var buf bytes.Buffer
|
||||
|
||||
expire := uint(time.Now().Unix()) + d.Config.CacheTime*60
|
||||
var exp []byte
|
||||
exp = make([]byte, 4)
|
||||
binary.BigEndian.PutUint32(exp, uint32(expire))
|
||||
_, _ = buf.Write(exp)
|
||||
|
||||
enc := gob.NewEncoder(&buf)
|
||||
err := enc.Encode(res)
|
||||
if err != nil {
|
||||
log.Error("gob.Encode(): %s", err)
|
||||
return 0
|
||||
}
|
||||
val := buf.Bytes()
|
||||
_ = cache.Set([]byte(host), val)
|
||||
return len(val)
|
||||
}
|
||||
|
||||
func getCachedResult(cache cache.Cache, host string) (Result, bool) {
|
||||
data := cache.Get([]byte(host))
|
||||
if data == nil {
|
||||
return Result{}, false
|
||||
}
|
||||
|
||||
exp := int(binary.BigEndian.Uint32(data[:4]))
|
||||
if exp <= int(time.Now().Unix()) {
|
||||
cache.Del([]byte(host))
|
||||
return Result{}, false
|
||||
}
|
||||
|
||||
var buf bytes.Buffer
|
||||
buf.Write(data[4:])
|
||||
dec := gob.NewDecoder(&buf)
|
||||
r := Result{}
|
||||
err := dec.Decode(&r)
|
||||
if err != nil {
|
||||
log.Debug("gob.Decode(): %s", err)
|
||||
return Result{}, false
|
||||
}
|
||||
|
||||
return r, true
|
||||
}
|
||||
|
||||
// SafeSearchDomain returns replacement address for search engine
|
||||
func (d *Dnsfilter) SafeSearchDomain(host string) (string, bool) {
|
||||
val, ok := safeSearchDomains[host]
|
||||
return val, ok
|
||||
}
|
||||
|
||||
func (d *Dnsfilter) checkSafeSearch(host string) (Result, error) {
|
||||
if log.GetLevel() >= log.DEBUG {
|
||||
timer := log.StartTimer()
|
||||
defer timer.LogElapsed("SafeSearch: lookup for %s", host)
|
||||
}
|
||||
|
||||
// Check cache. Return cached result if it was found
|
||||
cachedValue, isFound := getCachedResult(gctx.safeSearchCache, host)
|
||||
if isFound {
|
||||
// atomic.AddUint64(&gctx.stats.Safesearch.CacheHits, 1)
|
||||
log.Tracef("SafeSearch: found in cache: %s", host)
|
||||
return cachedValue, nil
|
||||
}
|
||||
|
||||
safeHost, ok := d.SafeSearchDomain(host)
|
||||
if !ok {
|
||||
return Result{}, nil
|
||||
}
|
||||
|
||||
res := Result{IsFiltered: true, Reason: FilteredSafeSearch}
|
||||
if ip := net.ParseIP(safeHost); ip != nil {
|
||||
res.IP = ip
|
||||
valLen := d.setCacheResult(gctx.safeSearchCache, host, res)
|
||||
log.Debug("SafeSearch: stored in cache: %s (%d bytes)", host, valLen)
|
||||
return res, nil
|
||||
}
|
||||
|
||||
// TODO this address should be resolved with upstream that was configured in dnsforward
|
||||
addrs, err := net.LookupIP(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 _, i := range addrs {
|
||||
if ipv4 := i.To4(); ipv4 != nil {
|
||||
res.IP = ipv4
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if len(res.IP) == 0 {
|
||||
return Result{}, fmt.Errorf("no ipv4 addresses in safe search response for %s", safeHost)
|
||||
}
|
||||
|
||||
// Cache result
|
||||
valLen := d.setCacheResult(gctx.safeSearchCache, host, res)
|
||||
log.Debug("SafeSearch: stored in cache: %s (%d bytes)", host, valLen)
|
||||
return res, nil
|
||||
}
|
||||
|
||||
func (d *Dnsfilter) handleSafeSearchEnable(w http.ResponseWriter, r *http.Request) {
|
||||
d.Config.SafeSearchEnabled = true
|
||||
d.Config.ConfigModified()
|
||||
}
|
||||
|
||||
func (d *Dnsfilter) handleSafeSearchDisable(w http.ResponseWriter, r *http.Request) {
|
||||
d.Config.SafeSearchEnabled = false
|
||||
d.Config.ConfigModified()
|
||||
}
|
||||
|
||||
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)
|
||||
if err != nil {
|
||||
httpError(r, w, http.StatusInternalServerError, "Unable to write response json: %s", err)
|
||||
return
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
// Parental Control, Safe Browsing, Safe Search
|
||||
// Safe Browsing, Parental Control
|
||||
|
||||
package dnsfilter
|
||||
|
||||
@@ -6,12 +6,12 @@ import (
|
||||
"bytes"
|
||||
"crypto/sha256"
|
||||
"encoding/binary"
|
||||
"encoding/gob"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/http"
|
||||
"sort"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
@@ -22,9 +22,6 @@ import (
|
||||
"golang.org/x/net/publicsuffix"
|
||||
)
|
||||
|
||||
// Servers to use for resolution of SB/PC server name
|
||||
var bootstrapServers = []string{"176.103.130.130", "176.103.130.131"}
|
||||
|
||||
const dnsTimeout = 3 * time.Second
|
||||
const defaultSafebrowsingServer = "https://dns-family.adguard.com/dns-query"
|
||||
const defaultParentalServer = "https://dns-family.adguard.com/dns-query"
|
||||
@@ -35,7 +32,15 @@ func (d *Dnsfilter) initSecurityServices() error {
|
||||
var err error
|
||||
d.safeBrowsingServer = defaultSafebrowsingServer
|
||||
d.parentalServer = defaultParentalServer
|
||||
opts := upstream.Options{Timeout: dnsTimeout, Bootstrap: bootstrapServers}
|
||||
opts := upstream.Options{
|
||||
Timeout: dnsTimeout,
|
||||
ServerIPAddrs: []net.IP{
|
||||
net.ParseIP("176.103.130.132"),
|
||||
net.ParseIP("176.103.130.134"),
|
||||
net.ParseIP("2a00:5a60::bad1:ff"),
|
||||
net.ParseIP("2a00:5a60::bad2:ff"),
|
||||
},
|
||||
}
|
||||
|
||||
d.parentalUpstream, err = upstream.AddressToUpstream(d.parentalServer, opts)
|
||||
if err != nil {
|
||||
@@ -52,115 +57,65 @@ func (d *Dnsfilter) initSecurityServices() error {
|
||||
|
||||
/*
|
||||
expire byte[4]
|
||||
res Result
|
||||
hash byte[32]
|
||||
...
|
||||
*/
|
||||
func (d *Dnsfilter) setCacheResult(cache cache.Cache, host string, res Result) int {
|
||||
var buf bytes.Buffer
|
||||
|
||||
expire := uint(time.Now().Unix()) + d.Config.CacheTime*60
|
||||
var exp []byte
|
||||
exp = make([]byte, 4)
|
||||
binary.BigEndian.PutUint32(exp, uint32(expire))
|
||||
_, _ = buf.Write(exp)
|
||||
|
||||
enc := gob.NewEncoder(&buf)
|
||||
err := enc.Encode(res)
|
||||
if err != nil {
|
||||
log.Error("gob.Encode(): %s", err)
|
||||
return 0
|
||||
}
|
||||
val := buf.Bytes()
|
||||
_ = cache.Set([]byte(host), val)
|
||||
return len(val)
|
||||
func (c *sbCtx) setCache(prefix []byte, hashes []byte) {
|
||||
d := make([]byte, 4+len(hashes))
|
||||
expire := uint(time.Now().Unix()) + c.cacheTime*60
|
||||
binary.BigEndian.PutUint32(d[:4], uint32(expire))
|
||||
copy(d[4:], hashes)
|
||||
c.cache.Set(prefix, d)
|
||||
log.Debug("%s: stored in cache: %v", c.svc, prefix)
|
||||
}
|
||||
|
||||
func getCachedResult(cache cache.Cache, host string) (Result, bool) {
|
||||
data := cache.Get([]byte(host))
|
||||
if data == nil {
|
||||
return Result{}, false
|
||||
}
|
||||
|
||||
exp := int(binary.BigEndian.Uint32(data[:4]))
|
||||
if exp <= int(time.Now().Unix()) {
|
||||
cache.Del([]byte(host))
|
||||
return Result{}, false
|
||||
}
|
||||
|
||||
var buf bytes.Buffer
|
||||
buf.Write(data[4:])
|
||||
dec := gob.NewDecoder(&buf)
|
||||
r := Result{}
|
||||
err := dec.Decode(&r)
|
||||
if err != nil {
|
||||
log.Debug("gob.Decode(): %s", err)
|
||||
return Result{}, false
|
||||
}
|
||||
|
||||
return r, true
|
||||
}
|
||||
|
||||
// SafeSearchDomain returns replacement address for search engine
|
||||
func (d *Dnsfilter) SafeSearchDomain(host string) (string, bool) {
|
||||
val, ok := safeSearchDomains[host]
|
||||
return val, ok
|
||||
}
|
||||
|
||||
func (d *Dnsfilter) checkSafeSearch(host string) (Result, error) {
|
||||
if log.GetLevel() >= log.DEBUG {
|
||||
timer := log.StartTimer()
|
||||
defer timer.LogElapsed("SafeSearch: lookup for %s", host)
|
||||
}
|
||||
|
||||
// Check cache. Return cached result if it was found
|
||||
cachedValue, isFound := getCachedResult(gctx.safeSearchCache, host)
|
||||
if isFound {
|
||||
// atomic.AddUint64(&gctx.stats.Safesearch.CacheHits, 1)
|
||||
log.Tracef("SafeSearch: found in cache: %s", host)
|
||||
return cachedValue, nil
|
||||
}
|
||||
|
||||
safeHost, ok := d.SafeSearchDomain(host)
|
||||
if !ok {
|
||||
return Result{}, nil
|
||||
}
|
||||
|
||||
res := Result{IsFiltered: true, Reason: FilteredSafeSearch}
|
||||
if ip := net.ParseIP(safeHost); ip != nil {
|
||||
res.IP = ip
|
||||
valLen := d.setCacheResult(gctx.safeSearchCache, host, res)
|
||||
log.Debug("SafeSearch: stored in cache: %s (%d bytes)", host, valLen)
|
||||
return res, nil
|
||||
}
|
||||
|
||||
// TODO this address should be resolved with upstream that was configured in dnsforward
|
||||
addrs, err := net.LookupIP(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 _, i := range addrs {
|
||||
if ipv4 := i.To4(); ipv4 != nil {
|
||||
res.IP = ipv4
|
||||
break
|
||||
func (c *sbCtx) getCached() int {
|
||||
now := time.Now().Unix()
|
||||
hashesToRequest := map[[32]byte]string{}
|
||||
for k, v := range c.hashToHost {
|
||||
key := k[0:2]
|
||||
val := c.cache.Get(key)
|
||||
if val != nil {
|
||||
expire := binary.BigEndian.Uint32(val)
|
||||
if now >= int64(expire) {
|
||||
val = nil
|
||||
} else {
|
||||
for i := 4; i < len(val); i += 32 {
|
||||
hash := val[i : i+32]
|
||||
var hash32 [32]byte
|
||||
copy(hash32[:], hash[0:32])
|
||||
_, found := c.hashToHost[hash32]
|
||||
if found {
|
||||
log.Debug("%s: found in cache: %s: blocked by %v", c.svc, c.host, hash32)
|
||||
return 1
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if val == nil {
|
||||
hashesToRequest[k] = v
|
||||
}
|
||||
}
|
||||
|
||||
if len(res.IP) == 0 {
|
||||
return Result{}, fmt.Errorf("no ipv4 addresses in safe search response for %s", safeHost)
|
||||
if len(hashesToRequest) == 0 {
|
||||
log.Debug("%s: found in cache: %s: not blocked", c.svc, c.host)
|
||||
return -1
|
||||
}
|
||||
|
||||
// Cache result
|
||||
valLen := d.setCacheResult(gctx.safeSearchCache, host, res)
|
||||
log.Debug("SafeSearch: stored in cache: %s (%d bytes)", host, valLen)
|
||||
return res, nil
|
||||
c.hashToHost = hashesToRequest
|
||||
return 0
|
||||
}
|
||||
|
||||
// for each dot, hash it and add it to string
|
||||
// The maximum is 4 components: "a.b.c.d"
|
||||
func hostnameToHashParam(host string) (string, map[string]bool) {
|
||||
var hashparam bytes.Buffer
|
||||
hashes := map[string]bool{}
|
||||
type sbCtx struct {
|
||||
host string
|
||||
svc string
|
||||
hashToHost map[[32]byte]string
|
||||
cache cache.Cache
|
||||
cacheTime uint
|
||||
}
|
||||
|
||||
func hostnameToHashes(host string) map[[32]byte]string {
|
||||
hashes := map[[32]byte]string{}
|
||||
tld, icann := publicsuffix.PublicSuffix(host)
|
||||
if !icann {
|
||||
// private suffixes like cloudfront.net
|
||||
@@ -190,8 +145,7 @@ func hostnameToHashParam(host string) (string, map[string]bool) {
|
||||
}
|
||||
|
||||
sum := sha256.Sum256([]byte(curhost))
|
||||
hashes[hex.EncodeToString(sum[:])] = true
|
||||
hashparam.WriteString(fmt.Sprintf("%s.", hex.EncodeToString(sum[0:4])))
|
||||
hashes[sum] = curhost
|
||||
|
||||
pos := strings.IndexByte(curhost, byte('.'))
|
||||
if pos < 0 {
|
||||
@@ -199,26 +153,91 @@ func hostnameToHashParam(host string) (string, map[string]bool) {
|
||||
}
|
||||
curhost = curhost[pos+1:]
|
||||
}
|
||||
return hashparam.String(), hashes
|
||||
return hashes
|
||||
}
|
||||
|
||||
// convert hash array to string
|
||||
func (c *sbCtx) getQuestion() string {
|
||||
q := ""
|
||||
for hash := range c.hashToHost {
|
||||
q += fmt.Sprintf("%s.", hex.EncodeToString(hash[0:2]))
|
||||
}
|
||||
if c.svc == "SafeBrowsing" {
|
||||
q += sbTXTSuffix
|
||||
} else {
|
||||
q += pcTXTSuffix
|
||||
}
|
||||
return q
|
||||
}
|
||||
|
||||
// Find the target hash in TXT response
|
||||
func (d *Dnsfilter) processTXT(svc, host string, resp *dns.Msg, hashes map[string]bool) bool {
|
||||
func (c *sbCtx) processTXT(resp *dns.Msg) (bool, [][]byte) {
|
||||
matched := false
|
||||
hashes := [][]byte{}
|
||||
for _, a := range resp.Answer {
|
||||
txt, ok := a.(*dns.TXT)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
log.Tracef("%s: hashes for %s: %v", svc, host, txt.Txt)
|
||||
log.Debug("%s: received hashes for %s: %v", c.svc, c.host, txt.Txt)
|
||||
|
||||
for _, t := range txt.Txt {
|
||||
_, ok := hashes[t]
|
||||
if ok {
|
||||
log.Tracef("%s: matched %s by %s", svc, host, t)
|
||||
return true
|
||||
|
||||
if len(t) != 32*2 {
|
||||
continue
|
||||
}
|
||||
hash, err := hex.DecodeString(t)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
hashes = append(hashes, hash)
|
||||
|
||||
if !matched {
|
||||
var hash32 [32]byte
|
||||
copy(hash32[:], hash)
|
||||
hashHost, ok := c.hashToHost[hash32]
|
||||
if ok {
|
||||
log.Debug("%s: matched %s by %s/%s", c.svc, c.host, hashHost, t)
|
||||
matched = true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return false
|
||||
|
||||
return matched, hashes
|
||||
}
|
||||
|
||||
func (c *sbCtx) storeCache(hashes [][]byte) {
|
||||
sort.Slice(hashes, func(a, b int) bool {
|
||||
return bytes.Compare(hashes[a], hashes[b]) < 0
|
||||
})
|
||||
|
||||
var curData []byte
|
||||
var prevPrefix []byte
|
||||
for i, hash := range hashes {
|
||||
prefix := hash[0:2]
|
||||
if !bytes.Equal(prefix, prevPrefix) {
|
||||
if i != 0 {
|
||||
c.setCache(prevPrefix, curData)
|
||||
curData = nil
|
||||
}
|
||||
prevPrefix = hashes[i][0:2]
|
||||
}
|
||||
curData = append(curData, hash...)
|
||||
}
|
||||
|
||||
if len(prevPrefix) != 0 {
|
||||
c.setCache(prevPrefix, curData)
|
||||
}
|
||||
|
||||
for hash := range c.hashToHost {
|
||||
prefix := hash[0:2]
|
||||
val := c.cache.Get(prefix)
|
||||
if val == nil {
|
||||
c.setCache(prefix, nil)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Disabling "dupl": the algorithm of SB/PC is similar, but it uses different data
|
||||
@@ -229,18 +248,29 @@ func (d *Dnsfilter) checkSafeBrowsing(host string) (Result, error) {
|
||||
defer timer.LogElapsed("SafeBrowsing lookup for %s", host)
|
||||
}
|
||||
|
||||
// check cache
|
||||
cachedValue, isFound := getCachedResult(gctx.safebrowsingCache, host)
|
||||
if isFound {
|
||||
// atomic.AddUint64(&gctx.stats.Safebrowsing.CacheHits, 1)
|
||||
log.Tracef("SafeBrowsing: found in cache: %s", host)
|
||||
return cachedValue, nil
|
||||
result := Result{}
|
||||
hashes := hostnameToHashes(host)
|
||||
|
||||
c := &sbCtx{
|
||||
host: host,
|
||||
svc: "SafeBrowsing",
|
||||
hashToHost: hashes,
|
||||
cache: gctx.safebrowsingCache,
|
||||
cacheTime: d.Config.CacheTime,
|
||||
}
|
||||
|
||||
result := Result{}
|
||||
question, hashes := hostnameToHashParam(host)
|
||||
question = question + sbTXTSuffix
|
||||
// check cache
|
||||
match := c.getCached()
|
||||
if match < 0 {
|
||||
return result, nil
|
||||
} else if match > 0 {
|
||||
result.IsFiltered = true
|
||||
result.Reason = FilteredSafeBrowsing
|
||||
result.Rule = "adguard-malware-shavar"
|
||||
return result, nil
|
||||
}
|
||||
|
||||
question := c.getQuestion()
|
||||
log.Tracef("SafeBrowsing: checking %s: %s", host, question)
|
||||
|
||||
req := dns.Msg{}
|
||||
@@ -250,14 +280,14 @@ func (d *Dnsfilter) checkSafeBrowsing(host string) (Result, error) {
|
||||
return result, err
|
||||
}
|
||||
|
||||
if d.processTXT("SafeBrowsing", host, resp, hashes) {
|
||||
matched, receivedHashes := c.processTXT(resp)
|
||||
if matched {
|
||||
result.IsFiltered = true
|
||||
result.Reason = FilteredSafeBrowsing
|
||||
result.Rule = "adguard-malware-shavar"
|
||||
}
|
||||
c.storeCache(receivedHashes)
|
||||
|
||||
valLen := d.setCacheResult(gctx.safebrowsingCache, host, result)
|
||||
log.Debug("SafeBrowsing: stored in cache: %s (%d bytes)", host, valLen)
|
||||
return result, nil
|
||||
}
|
||||
|
||||
@@ -269,18 +299,29 @@ func (d *Dnsfilter) checkParental(host string) (Result, error) {
|
||||
defer timer.LogElapsed("Parental lookup for %s", host)
|
||||
}
|
||||
|
||||
// check cache
|
||||
cachedValue, isFound := getCachedResult(gctx.parentalCache, host)
|
||||
if isFound {
|
||||
// atomic.AddUint64(&gctx.stats.Parental.CacheHits, 1)
|
||||
log.Tracef("Parental: found in cache: %s", host)
|
||||
return cachedValue, nil
|
||||
result := Result{}
|
||||
hashes := hostnameToHashes(host)
|
||||
|
||||
c := &sbCtx{
|
||||
host: host,
|
||||
svc: "Parental",
|
||||
hashToHost: hashes,
|
||||
cache: gctx.parentalCache,
|
||||
cacheTime: d.Config.CacheTime,
|
||||
}
|
||||
|
||||
result := Result{}
|
||||
question, hashes := hostnameToHashParam(host)
|
||||
question = question + pcTXTSuffix
|
||||
// check cache
|
||||
match := c.getCached()
|
||||
if match < 0 {
|
||||
return result, nil
|
||||
} else if match > 0 {
|
||||
result.IsFiltered = true
|
||||
result.Reason = FilteredParental
|
||||
result.Rule = "parental CATEGORY_BLACKLISTED"
|
||||
return result, nil
|
||||
}
|
||||
|
||||
question := c.getQuestion()
|
||||
log.Tracef("Parental: checking %s: %s", host, question)
|
||||
|
||||
req := dns.Msg{}
|
||||
@@ -290,14 +331,14 @@ func (d *Dnsfilter) checkParental(host string) (Result, error) {
|
||||
return result, err
|
||||
}
|
||||
|
||||
if d.processTXT("Parental", host, resp, hashes) {
|
||||
matched, receivedHashes := c.processTXT(resp)
|
||||
if matched {
|
||||
result.IsFiltered = true
|
||||
result.Reason = FilteredParental
|
||||
result.Rule = "parental CATEGORY_BLACKLISTED"
|
||||
}
|
||||
c.storeCache(receivedHashes)
|
||||
|
||||
valLen := d.setCacheResult(gctx.parentalCache, host, result)
|
||||
log.Debug("Parental: stored in cache: %s (%d bytes)", host, valLen)
|
||||
return result, err
|
||||
}
|
||||
|
||||
@@ -362,34 +403,6 @@ func (d *Dnsfilter) handleParentalStatus(w http.ResponseWriter, r *http.Request)
|
||||
}
|
||||
}
|
||||
|
||||
func (d *Dnsfilter) handleSafeSearchEnable(w http.ResponseWriter, r *http.Request) {
|
||||
d.Config.SafeSearchEnabled = true
|
||||
d.Config.ConfigModified()
|
||||
}
|
||||
|
||||
func (d *Dnsfilter) handleSafeSearchDisable(w http.ResponseWriter, r *http.Request) {
|
||||
d.Config.SafeSearchEnabled = false
|
||||
d.Config.ConfigModified()
|
||||
}
|
||||
|
||||
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)
|
||||
if err != nil {
|
||||
httpError(r, w, http.StatusInternalServerError, "Unable to write response json: %s", err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func (d *Dnsfilter) registerSecurityHandlers() {
|
||||
d.Config.HTTPRegister("POST", "/control/safebrowsing/enable", d.handleSafeBrowsingEnable)
|
||||
d.Config.HTTPRegister("POST", "/control/safebrowsing/disable", d.handleSafeBrowsingDisable)
|
||||
91
dnsfilter/sb_pc_test.go
Normal file
@@ -0,0 +1,91 @@
|
||||
package dnsfilter
|
||||
|
||||
import (
|
||||
"crypto/sha256"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/AdguardTeam/golibs/cache"
|
||||
"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))
|
||||
_, ok := hashes[sha256.Sum256([]byte("3.sub.host.com"))]
|
||||
assert.True(t, ok)
|
||||
_, ok = hashes[sha256.Sum256([]byte("sub.host.com"))]
|
||||
assert.True(t, ok)
|
||||
_, ok = hashes[sha256.Sum256([]byte("host.com"))]
|
||||
assert.True(t, ok)
|
||||
_, ok = hashes[sha256.Sum256([]byte("com"))]
|
||||
assert.False(t, ok)
|
||||
|
||||
c := &sbCtx{
|
||||
svc: "SafeBrowsing",
|
||||
}
|
||||
|
||||
// test getQuestion()
|
||||
c.hashToHost = hashes
|
||||
q := c.getQuestion()
|
||||
assert.True(t, strings.Index(q, "7a1b.") >= 0)
|
||||
assert.True(t, strings.Index(q, "af5a.") >= 0)
|
||||
assert.True(t, strings.Index(q, "eb11.") >= 0)
|
||||
assert.True(t, strings.Index(q, "sb.dns.adguard.com.") > 0)
|
||||
}
|
||||
|
||||
func TestSafeBrowsingCache(t *testing.T) {
|
||||
c := &sbCtx{
|
||||
svc: "SafeBrowsing",
|
||||
cacheTime: 100,
|
||||
}
|
||||
conf := cache.Config{}
|
||||
c.cache = cache.New(conf)
|
||||
|
||||
// store in cache hashes for "3.sub.host.com" and "host.com"
|
||||
// and empty data for hash-prefix for "sub.host.com"
|
||||
hash := sha256.Sum256([]byte("sub.host.com"))
|
||||
c.hashToHost = make(map[[32]byte]string)
|
||||
c.hashToHost[hash] = "sub.host.com"
|
||||
var hashesArray [][]byte
|
||||
hash4 := sha256.Sum256([]byte("3.sub.host.com"))
|
||||
hashesArray = append(hashesArray, hash4[:])
|
||||
hash2 := sha256.Sum256([]byte("host.com"))
|
||||
hashesArray = append(hashesArray, hash2[:])
|
||||
c.storeCache(hashesArray)
|
||||
|
||||
// match "3.sub.host.com" or "host.com" from cache
|
||||
c.hashToHost = make(map[[32]byte]string)
|
||||
hash = sha256.Sum256([]byte("3.sub.host.com"))
|
||||
c.hashToHost[hash] = "3.sub.host.com"
|
||||
hash = sha256.Sum256([]byte("sub.host.com"))
|
||||
c.hashToHost[hash] = "sub.host.com"
|
||||
hash = sha256.Sum256([]byte("host.com"))
|
||||
c.hashToHost[hash] = "host.com"
|
||||
assert.Equal(t, 1, c.getCached())
|
||||
|
||||
// match "sub.host.com" from cache
|
||||
c.hashToHost = make(map[[32]byte]string)
|
||||
hash = sha256.Sum256([]byte("sub.host.com"))
|
||||
c.hashToHost[hash] = "sub.host.com"
|
||||
assert.Equal(t, -1, c.getCached())
|
||||
|
||||
// match "sub.host.com" from cache,
|
||||
// but another hash for "nonexisting.com" is not in cache
|
||||
// which means that we must get data from server for it
|
||||
c.hashToHost = make(map[[32]byte]string)
|
||||
hash = sha256.Sum256([]byte("sub.host.com"))
|
||||
c.hashToHost[hash] = "sub.host.com"
|
||||
hash = sha256.Sum256([]byte("nonexisting.com"))
|
||||
c.hashToHost[hash] = "nonexisting.com"
|
||||
assert.Equal(t, 0, c.getCached())
|
||||
|
||||
hash = sha256.Sum256([]byte("sub.host.com"))
|
||||
_, ok := c.hashToHost[hash]
|
||||
assert.False(t, ok)
|
||||
|
||||
hash = sha256.Sum256([]byte("nonexisting.com"))
|
||||
_, ok = c.hashToHost[hash]
|
||||
assert.True(t, ok)
|
||||
}
|
||||
@@ -81,6 +81,7 @@ type FilteringConfig struct {
|
||||
AAAADisabled bool `yaml:"aaaa_disabled"` // Respond with an empty answer to all AAAA requests
|
||||
EnableDNSSEC bool `yaml:"enable_dnssec"` // Set DNSSEC flag in outcoming DNS request
|
||||
EnableEDNSClientSubnet bool `yaml:"edns_client_subnet"` // Enable EDNS Client Subnet option
|
||||
MaxGoroutines uint32 `yaml:"max_goroutines"` // Max. number of parallel goroutines for processing incoming requests
|
||||
}
|
||||
|
||||
// TLSConfig is the TLS configuration for HTTPS, DNS-over-HTTPS, and DNS-over-TLS
|
||||
@@ -133,8 +134,8 @@ var defaultValues = ServerConfig{
|
||||
// createProxyConfig creates and validates configuration for the main proxy
|
||||
func (s *Server) createProxyConfig() (proxy.Config, error) {
|
||||
proxyConfig := proxy.Config{
|
||||
UDPListenAddr: s.conf.UDPListenAddr,
|
||||
TCPListenAddr: s.conf.TCPListenAddr,
|
||||
UDPListenAddr: []*net.UDPAddr{s.conf.UDPListenAddr},
|
||||
TCPListenAddr: []*net.TCPAddr{s.conf.TCPListenAddr},
|
||||
Ratelimit: int(s.conf.Ratelimit),
|
||||
RatelimitWhitelist: s.conf.RatelimitWhitelist,
|
||||
RefuseAny: s.conf.RefuseAny,
|
||||
@@ -144,6 +145,7 @@ func (s *Server) createProxyConfig() (proxy.Config, error) {
|
||||
BeforeRequestHandler: s.beforeRequestHandler,
|
||||
RequestHandler: s.handleDNSRequest,
|
||||
EnableEDNSClientSubnet: s.conf.EnableEDNSClientSubnet,
|
||||
MaxGoroutines: int(s.conf.MaxGoroutines),
|
||||
}
|
||||
|
||||
if s.conf.CacheSize != 0 {
|
||||
@@ -229,7 +231,7 @@ func (s *Server) prepareIntlProxy() {
|
||||
// prepareTLS - prepares TLS configuration for the DNS proxy
|
||||
func (s *Server) prepareTLS(proxyConfig *proxy.Config) error {
|
||||
if s.conf.TLSListenAddr != nil && len(s.conf.CertificateChainData) != 0 && len(s.conf.PrivateKeyData) != 0 {
|
||||
proxyConfig.TLSListenAddr = s.conf.TLSListenAddr
|
||||
proxyConfig.TLSListenAddr = []*net.TCPAddr{s.conf.TLSListenAddr}
|
||||
var err error
|
||||
s.conf.cert, err = tls.X509KeyPair(s.conf.CertificateChainData, s.conf.PrivateKeyData)
|
||||
if err != nil {
|
||||
|
||||
@@ -51,6 +51,9 @@ type Server struct {
|
||||
stats stats.Stats
|
||||
access *accessCtx
|
||||
|
||||
tableHostToIP map[string]net.IP // "hostname -> IP" table for internal addresses (DHCP)
|
||||
tableHostToIPLock sync.Mutex
|
||||
|
||||
tablePTR map[string]string // "IP -> hostname" table for reverse lookup
|
||||
tablePTRLock sync.Mutex
|
||||
|
||||
@@ -176,6 +179,9 @@ func (s *Server) Prepare(config *ServerConfig) error {
|
||||
return fmt.Errorf("DNS: invalid custom blocking IP address specified")
|
||||
}
|
||||
}
|
||||
if s.conf.MaxGoroutines == 0 {
|
||||
s.conf.MaxGoroutines = 50
|
||||
}
|
||||
}
|
||||
|
||||
// 2. Set default values in the case if nothing is configured
|
||||
|
||||
@@ -252,10 +252,6 @@ func TestBlockedRequest(t *testing.T) {
|
||||
|
||||
func TestServerCustomClientUpstream(t *testing.T) {
|
||||
s := createTestServer(t)
|
||||
err := s.Start()
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to start server: %s", err)
|
||||
}
|
||||
s.conf.GetCustomUpstreamByClient = func(clientAddr string) *proxy.UpstreamConfig {
|
||||
uc := &proxy.UpstreamConfig{}
|
||||
u := &testUpstream{}
|
||||
@@ -264,6 +260,9 @@ func TestServerCustomClientUpstream(t *testing.T) {
|
||||
uc.Upstreams = append(uc.Upstreams, u)
|
||||
return uc
|
||||
}
|
||||
|
||||
assert.Nil(t, s.Start())
|
||||
|
||||
addr := s.dnsProxy.Addr(proxy.ProtoUDP)
|
||||
|
||||
// Send test request
|
||||
@@ -670,6 +669,11 @@ func TestRewrite(t *testing.T) {
|
||||
assert.True(t, ok)
|
||||
assert.Equal(t, "1.2.3.4", a.A.String())
|
||||
|
||||
req = createTestMessageWithType("test.com.", dns.TypeAAAA)
|
||||
reply, err = dns.Exchange(req, addr.String())
|
||||
assert.Nil(t, err)
|
||||
assert.Equal(t, 0, len(reply.Answer))
|
||||
|
||||
req = createTestMessageWithType("alias.test.com.", dns.TypeA)
|
||||
reply, err = dns.Exchange(req, addr.String())
|
||||
assert.Nil(t, err)
|
||||
|
||||
@@ -54,8 +54,12 @@ func (s *Server) filterDNSRequest(ctx *dnsContext) (*dnsfilter.Result, error) {
|
||||
// log.Tracef("Host %s is filtered, reason - '%s', matched rule: '%s'", host, res.Reason, res.Rule)
|
||||
d.Res = s.genDNSFilterMessage(d, &res)
|
||||
|
||||
} else if (res.Reason == dnsfilter.ReasonRewrite || res.Reason == dnsfilter.RewriteEtcHosts) &&
|
||||
len(res.IPList) != 0 {
|
||||
} else if res.Reason == dnsfilter.ReasonRewrite && len(res.CanonName) != 0 && len(res.IPList) == 0 {
|
||||
ctx.origQuestion = d.Req.Question[0]
|
||||
// resolve canonical name, not the original host name
|
||||
d.Req.Question[0].Name = dns.Fqdn(res.CanonName)
|
||||
|
||||
} else if res.Reason == dnsfilter.ReasonRewrite || res.Reason == dnsfilter.RewriteEtcHosts {
|
||||
resp := s.makeResponse(req)
|
||||
|
||||
name := host
|
||||
@@ -78,11 +82,6 @@ func (s *Server) filterDNSRequest(ctx *dnsContext) (*dnsfilter.Result, error) {
|
||||
|
||||
d.Res = resp
|
||||
|
||||
} else if res.Reason == dnsfilter.ReasonRewrite && len(res.CanonName) != 0 {
|
||||
ctx.origQuestion = d.Req.Question[0]
|
||||
// resolve canonical name, not the original host name
|
||||
d.Req.Question[0].Name = dns.Fqdn(res.CanonName)
|
||||
|
||||
} else if res.Reason == dnsfilter.RewriteEtcHosts && len(res.ReverseHost) != 0 {
|
||||
|
||||
resp := s.makeResponse(req)
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package dnsforward
|
||||
|
||||
import (
|
||||
"net"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
@@ -42,6 +43,7 @@ func (s *Server) handleDNSRequest(_ *proxy.Proxy, d *proxy.DNSContext) error {
|
||||
type modProcessFunc func(ctx *dnsContext) int
|
||||
mods := []modProcessFunc{
|
||||
processInitial,
|
||||
processInternalHosts,
|
||||
processInternalIPAddrs,
|
||||
processFilteringBeforeRequest,
|
||||
processUpstream,
|
||||
@@ -92,6 +94,20 @@ func processInitial(ctx *dnsContext) int {
|
||||
return resultDone
|
||||
}
|
||||
|
||||
// Return TRUE if host names doesn't contain disallowed characters
|
||||
func isHostnameOK(hostname string) bool {
|
||||
for _, c := range hostname {
|
||||
if !((c >= 'a' && c <= 'z') ||
|
||||
(c >= 'A' && c <= 'Z') ||
|
||||
(c >= '0' && c <= '9') ||
|
||||
c == '.' || c == '-') {
|
||||
log.Debug("DNS: skipping invalid hostname %s from DHCP", hostname)
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func (s *Server) onDHCPLeaseChanged(flags int) {
|
||||
switch flags {
|
||||
case dhcpd.LeaseChangedAdded,
|
||||
@@ -102,20 +118,83 @@ func (s *Server) onDHCPLeaseChanged(flags int) {
|
||||
return
|
||||
}
|
||||
|
||||
hostToIP := make(map[string]net.IP)
|
||||
m := make(map[string]string)
|
||||
|
||||
ll := s.dhcpServer.Leases(dhcpd.LeasesAll)
|
||||
|
||||
for _, l := range ll {
|
||||
if len(l.Hostname) == 0 {
|
||||
if len(l.Hostname) == 0 || !isHostnameOK(l.Hostname) {
|
||||
continue
|
||||
}
|
||||
m[l.IP.String()] = l.Hostname
|
||||
|
||||
lowhost := strings.ToLower(l.Hostname)
|
||||
|
||||
m[l.IP.String()] = lowhost
|
||||
|
||||
ip := make(net.IP, 4)
|
||||
copy(ip, l.IP.To4())
|
||||
hostToIP[lowhost] = ip
|
||||
}
|
||||
log.Debug("DNS: added %d PTR entries from DHCP", len(m))
|
||||
|
||||
log.Debug("DNS: added %d A/PTR entries from DHCP", len(m))
|
||||
|
||||
s.tableHostToIPLock.Lock()
|
||||
s.tableHostToIP = hostToIP
|
||||
s.tableHostToIPLock.Unlock()
|
||||
|
||||
s.tablePTRLock.Lock()
|
||||
s.tablePTR = m
|
||||
s.tablePTRLock.Unlock()
|
||||
}
|
||||
|
||||
// Respond to A requests if the target host name is associated with a lease from our DHCP server
|
||||
func processInternalHosts(ctx *dnsContext) int {
|
||||
s := ctx.srv
|
||||
req := ctx.proxyCtx.Req
|
||||
if !(req.Question[0].Qtype == dns.TypeA || req.Question[0].Qtype == dns.TypeAAAA) {
|
||||
return resultDone
|
||||
}
|
||||
|
||||
host := req.Question[0].Name
|
||||
host = strings.ToLower(host)
|
||||
if !strings.HasSuffix(host, ".lan.") {
|
||||
return resultDone
|
||||
}
|
||||
host = strings.TrimSuffix(host, ".lan.")
|
||||
|
||||
s.tableHostToIPLock.Lock()
|
||||
if s.tableHostToIP == nil {
|
||||
s.tableHostToIPLock.Unlock()
|
||||
return resultDone
|
||||
}
|
||||
ip, ok := s.tableHostToIP[host]
|
||||
s.tableHostToIPLock.Unlock()
|
||||
if !ok {
|
||||
return resultDone
|
||||
}
|
||||
|
||||
log.Debug("DNS: internal record: %s -> %s", req.Question[0].Name, ip.String())
|
||||
|
||||
resp := s.makeResponse(req)
|
||||
|
||||
if req.Question[0].Qtype == dns.TypeA {
|
||||
a := &dns.A{}
|
||||
a.Hdr = dns.RR_Header{
|
||||
Name: req.Question[0].Name,
|
||||
Rrtype: dns.TypeA,
|
||||
Ttl: s.conf.BlockedResponseTTL,
|
||||
Class: dns.ClassINET,
|
||||
}
|
||||
a.A = make([]byte, 4)
|
||||
copy(a.A, ip)
|
||||
resp.Answer = append(resp.Answer, a)
|
||||
}
|
||||
|
||||
ctx.proxyCtx.Res = resp
|
||||
return resultDone
|
||||
}
|
||||
|
||||
// Respond to PTR requests if the target IP address is leased by our DHCP server
|
||||
func processInternalIPAddrs(ctx *dnsContext) int {
|
||||
s := ctx.srv
|
||||
|
||||
247
filters/filter_file.go
Normal file
@@ -0,0 +1,247 @@
|
||||
package filters
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/AdguardTeam/AdGuardHome/util"
|
||||
"github.com/AdguardTeam/golibs/log"
|
||||
)
|
||||
|
||||
// Allows printable UTF-8 text with CR, LF, TAB characters
|
||||
func isPrintableText(data []byte) bool {
|
||||
for _, c := range data {
|
||||
if (c >= ' ' && c != 0x7f) || c == '\n' || c == '\r' || c == '\t' {
|
||||
continue
|
||||
}
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// Download filter data
|
||||
// Return nil on success. Set f.Path to a file path, or "" if the file was not modified
|
||||
func (fs *filterStg) downloadFilter(f *Filter) error {
|
||||
log.Debug("Filters: Downloading filter from %s", f.URL)
|
||||
|
||||
// create temp file
|
||||
tmpFile, err := ioutil.TempFile(filepath.Join(fs.conf.FilterDir), "")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer func() {
|
||||
if tmpFile != nil {
|
||||
_ = tmpFile.Close()
|
||||
_ = os.Remove(tmpFile.Name())
|
||||
}
|
||||
}()
|
||||
|
||||
// create data reader object
|
||||
var reader io.Reader
|
||||
if filepath.IsAbs(f.URL) {
|
||||
f, err := os.Open(f.URL)
|
||||
if err != nil {
|
||||
return fmt.Errorf("open file: %s", err)
|
||||
}
|
||||
defer f.Close()
|
||||
reader = f
|
||||
} else {
|
||||
req, err := http.NewRequest("GET", f.URL, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if len(f.LastModified) != 0 {
|
||||
req.Header.Add("If-Modified-Since", f.LastModified)
|
||||
}
|
||||
|
||||
resp, err := fs.conf.HTTPClient.Do(req)
|
||||
if resp != nil && resp.Body != nil {
|
||||
defer resp.Body.Close()
|
||||
}
|
||||
if err != nil {
|
||||
f.networkError = true
|
||||
return err
|
||||
}
|
||||
|
||||
if resp.StatusCode == 304 { // "NOT_MODIFIED"
|
||||
log.Debug("Filters: filter %s isn't modified since %s",
|
||||
f.URL, f.LastModified)
|
||||
f.LastUpdated = time.Now()
|
||||
f.Path = ""
|
||||
return nil
|
||||
|
||||
} else if resp.StatusCode != 200 {
|
||||
err := fmt.Errorf("Filters: Couldn't download filter from %s: status code: %d",
|
||||
f.URL, resp.StatusCode)
|
||||
return err
|
||||
}
|
||||
|
||||
f.LastModified = resp.Header.Get("Last-Modified")
|
||||
|
||||
reader = resp.Body
|
||||
}
|
||||
|
||||
// parse and validate data, write to a file
|
||||
err = writeFile(f, reader, tmpFile)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Closing the file before renaming it is necessary on Windows
|
||||
_ = tmpFile.Close()
|
||||
fname := fs.filePath(*f)
|
||||
err = os.Rename(tmpFile.Name(), fname)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
tmpFile = nil // prevent from deleting this file in "defer" handler
|
||||
|
||||
log.Debug("Filters: saved filter %s at %s", f.URL, fname)
|
||||
f.Path = fname
|
||||
f.LastUpdated = time.Now()
|
||||
return nil
|
||||
}
|
||||
|
||||
func gatherUntil(dst []byte, dstLen int, src []byte, until int) int {
|
||||
num := util.MinInt(len(src), until-dstLen)
|
||||
return copy(dst[dstLen:], src[:num])
|
||||
}
|
||||
|
||||
func isHTML(buf []byte) bool {
|
||||
s := strings.ToLower(string(buf))
|
||||
return strings.Contains(s, "<html") ||
|
||||
strings.Contains(s, "<!doctype")
|
||||
}
|
||||
|
||||
// Read file data and count the number of rules
|
||||
func parseFilter(f *Filter, reader io.Reader) error {
|
||||
ruleCount := 0
|
||||
r := bufio.NewReader(reader)
|
||||
|
||||
log.Debug("Filters: parsing %s", f.URL)
|
||||
|
||||
var err error
|
||||
for err == nil {
|
||||
var line string
|
||||
line, err = r.ReadString('\n')
|
||||
if err != nil && err != io.EOF {
|
||||
return err
|
||||
}
|
||||
|
||||
line = strings.TrimSpace(line)
|
||||
|
||||
if len(line) == 0 ||
|
||||
line[0] == '#' ||
|
||||
line[0] == '!' {
|
||||
continue
|
||||
}
|
||||
|
||||
ruleCount++
|
||||
}
|
||||
|
||||
log.Debug("Filters: %s: %d rules", f.URL, ruleCount)
|
||||
|
||||
f.RuleCount = uint64(ruleCount)
|
||||
return nil
|
||||
}
|
||||
|
||||
// Read data, parse, write to a file
|
||||
func writeFile(f *Filter, reader io.Reader, outFile *os.File) error {
|
||||
ruleCount := 0
|
||||
buf := make([]byte, 64*1024)
|
||||
total := 0
|
||||
var chunk []byte
|
||||
|
||||
firstChunk := make([]byte, 4*1024)
|
||||
firstChunkLen := 0
|
||||
|
||||
for {
|
||||
n, err := reader.Read(buf)
|
||||
if err != nil && err != io.EOF {
|
||||
return err
|
||||
}
|
||||
total += n
|
||||
|
||||
if !isPrintableText(buf[:n]) {
|
||||
return fmt.Errorf("data contains non-printable characters")
|
||||
}
|
||||
|
||||
if firstChunk != nil {
|
||||
// gather full buffer firstChunk and perform its data tests
|
||||
firstChunkLen += gatherUntil(firstChunk, firstChunkLen, buf[:n], len(firstChunk))
|
||||
|
||||
if firstChunkLen == len(firstChunk) ||
|
||||
err == io.EOF {
|
||||
|
||||
if isHTML(firstChunk[:firstChunkLen]) {
|
||||
return fmt.Errorf("data is HTML, not plain text")
|
||||
}
|
||||
|
||||
firstChunk = nil
|
||||
}
|
||||
}
|
||||
|
||||
_, err2 := outFile.Write(buf[:n])
|
||||
if err2 != nil {
|
||||
return err2
|
||||
}
|
||||
|
||||
chunk = append(chunk, buf[:n]...)
|
||||
s := string(chunk)
|
||||
for len(s) != 0 {
|
||||
i, line := splitNext(&s, '\n')
|
||||
if i < 0 && err != io.EOF {
|
||||
// no more lines in the current chunk
|
||||
break
|
||||
}
|
||||
chunk = []byte(s)
|
||||
|
||||
if len(line) == 0 ||
|
||||
line[0] == '#' ||
|
||||
line[0] == '!' {
|
||||
continue
|
||||
}
|
||||
|
||||
ruleCount++
|
||||
}
|
||||
|
||||
if err == io.EOF {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
log.Debug("Filters: updated filter %s: %d bytes, %d rules",
|
||||
f.URL, total, ruleCount)
|
||||
|
||||
f.RuleCount = uint64(ruleCount)
|
||||
return nil
|
||||
}
|
||||
|
||||
// SplitNext - split string by a byte
|
||||
// Whitespace is trimmed
|
||||
// Return byte position and the first chunk
|
||||
func splitNext(data *string, by byte) (int, string) {
|
||||
s := *data
|
||||
i := strings.IndexByte(s, by)
|
||||
var chunk string
|
||||
if i < 0 {
|
||||
chunk = s
|
||||
s = ""
|
||||
|
||||
} else {
|
||||
chunk = s[:i]
|
||||
s = s[i+1:]
|
||||
}
|
||||
|
||||
*data = s
|
||||
chunk = strings.TrimSpace(chunk)
|
||||
return i, chunk
|
||||
}
|
||||
329
filters/filter_http.go
Normal file
@@ -0,0 +1,329 @@
|
||||
package filters
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/AdguardTeam/AdGuardHome/util"
|
||||
"github.com/AdguardTeam/golibs/jsonutil"
|
||||
"github.com/AdguardTeam/golibs/log"
|
||||
)
|
||||
|
||||
// Print to log and set HTTP error message
|
||||
func httpError(r *http.Request, w http.ResponseWriter, code int, format string, args ...interface{}) {
|
||||
text := fmt.Sprintf(format, args...)
|
||||
log.Info("Filters: %s %s: %s", r.Method, r.URL, text)
|
||||
http.Error(w, text, code)
|
||||
}
|
||||
|
||||
// IsValidURL - return TRUE if URL or file path is valid
|
||||
func IsValidURL(rawurl string) bool {
|
||||
if filepath.IsAbs(rawurl) {
|
||||
// this is a file path
|
||||
return util.FileExists(rawurl)
|
||||
}
|
||||
|
||||
url, err := url.ParseRequestURI(rawurl)
|
||||
if err != nil {
|
||||
return false //Couldn't even parse the rawurl
|
||||
}
|
||||
if len(url.Scheme) == 0 {
|
||||
return false //No Scheme found
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func (f *Filtering) getFilterModule(t string) Filters {
|
||||
switch t {
|
||||
case "blocklist":
|
||||
return f.dnsBlocklist
|
||||
|
||||
case "whitelist":
|
||||
return f.dnsAllowlist
|
||||
|
||||
case "proxylist":
|
||||
return f.Proxylist
|
||||
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func (f *Filtering) restartMods(t string) {
|
||||
fN := f.getFilterModule(t)
|
||||
fN.NotifyObserver(EventBeforeUpdate)
|
||||
fN.NotifyObserver(EventAfterUpdate)
|
||||
}
|
||||
|
||||
func (f *Filtering) handleFilterAdd(w http.ResponseWriter, r *http.Request) {
|
||||
type reqJSON struct {
|
||||
Name string `json:"name"`
|
||||
URL string `json:"url"`
|
||||
Type string `json:"type"`
|
||||
}
|
||||
req := reqJSON{}
|
||||
_, err := jsonutil.DecodeObject(&req, r.Body)
|
||||
if err != nil {
|
||||
httpError(r, w, http.StatusBadRequest, "json.Decode: %s", err)
|
||||
return
|
||||
}
|
||||
|
||||
filterN := f.getFilterModule(req.Type)
|
||||
if filterN == nil {
|
||||
httpError(r, w, http.StatusBadRequest, "invalid type: %s", req.Type)
|
||||
return
|
||||
}
|
||||
|
||||
filt := Filter{
|
||||
Enabled: true,
|
||||
Name: req.Name,
|
||||
URL: req.URL,
|
||||
}
|
||||
err = filterN.Add(filt)
|
||||
if err != nil {
|
||||
httpError(r, w, http.StatusBadRequest, "add filter: %s", err)
|
||||
return
|
||||
}
|
||||
|
||||
f.conf.ConfigModified()
|
||||
|
||||
f.restartMods(req.Type)
|
||||
}
|
||||
|
||||
func (f *Filtering) handleFilterRemove(w http.ResponseWriter, r *http.Request) {
|
||||
type reqJSON struct {
|
||||
URL string `json:"url"`
|
||||
Type string `json:"type"`
|
||||
}
|
||||
req := reqJSON{}
|
||||
_, err := jsonutil.DecodeObject(&req, r.Body)
|
||||
if err != nil {
|
||||
httpError(r, w, http.StatusBadRequest, "json.Decode: %s", err)
|
||||
return
|
||||
}
|
||||
|
||||
filterN := f.getFilterModule(req.Type)
|
||||
if filterN == nil {
|
||||
httpError(r, w, http.StatusBadRequest, "invalid type: %s", req.Type)
|
||||
return
|
||||
}
|
||||
|
||||
removed := filterN.Delete(req.URL)
|
||||
if removed == nil {
|
||||
httpError(r, w, http.StatusInternalServerError, "no filter with such URL")
|
||||
return
|
||||
}
|
||||
|
||||
f.conf.ConfigModified()
|
||||
|
||||
if removed.Enabled {
|
||||
f.restartMods(req.Type)
|
||||
}
|
||||
|
||||
err = os.Remove(removed.Path)
|
||||
if err != nil {
|
||||
log.Error("os.Remove: %s", err)
|
||||
}
|
||||
}
|
||||
|
||||
func (f *Filtering) handleFilterModify(w http.ResponseWriter, r *http.Request) {
|
||||
type propsJSON struct {
|
||||
Name string `json:"name"`
|
||||
URL string `json:"url"`
|
||||
Enabled bool `json:"enabled"`
|
||||
}
|
||||
type reqJSON struct {
|
||||
URL string `json:"url"`
|
||||
Type string `json:"type"`
|
||||
Data propsJSON `json:"data"`
|
||||
}
|
||||
req := reqJSON{}
|
||||
_, err := jsonutil.DecodeObject(&req, r.Body)
|
||||
if err != nil {
|
||||
httpError(r, w, http.StatusBadRequest, "json.Decode: %s", err)
|
||||
return
|
||||
}
|
||||
|
||||
filterN := f.getFilterModule(req.Type)
|
||||
if filterN == nil {
|
||||
httpError(r, w, http.StatusBadRequest, "invalid type: %s", req.Type)
|
||||
return
|
||||
}
|
||||
|
||||
st, _, err := filterN.Modify(req.URL, req.Data.Enabled, req.Data.Name, req.Data.URL)
|
||||
if err != nil {
|
||||
httpError(r, w, http.StatusBadRequest, "%s", err)
|
||||
return
|
||||
}
|
||||
|
||||
f.conf.ConfigModified()
|
||||
|
||||
if st == StatusChangedEnabled ||
|
||||
st == StatusChangedURL {
|
||||
|
||||
// TODO StatusChangedURL: delete old file
|
||||
|
||||
f.restartMods(req.Type)
|
||||
}
|
||||
}
|
||||
|
||||
func (f *Filtering) handleFilteringSetRules(w http.ResponseWriter, r *http.Request) {
|
||||
body, err := ioutil.ReadAll(r.Body)
|
||||
if err != nil {
|
||||
httpError(r, w, http.StatusBadRequest, "Failed to read request body: %s", err)
|
||||
return
|
||||
}
|
||||
|
||||
f.conf.UserRules = strings.Split(string(body), "\n")
|
||||
f.conf.ConfigModified()
|
||||
f.restartMods("blocklist")
|
||||
}
|
||||
|
||||
func (f *Filtering) handleFilteringRefresh(w http.ResponseWriter, r *http.Request) {
|
||||
type reqJSON struct {
|
||||
Type string `json:"type"`
|
||||
}
|
||||
req := reqJSON{}
|
||||
_, err := jsonutil.DecodeObject(&req, r.Body)
|
||||
if err != nil {
|
||||
httpError(r, w, http.StatusBadRequest, "json.Decode: %s", err)
|
||||
return
|
||||
}
|
||||
|
||||
filterN := f.getFilterModule(req.Type)
|
||||
if filterN == nil {
|
||||
httpError(r, w, http.StatusBadRequest, "invalid type: %s", req.Type)
|
||||
return
|
||||
}
|
||||
|
||||
filterN.Refresh(0)
|
||||
}
|
||||
|
||||
type filterJSON struct {
|
||||
ID int64 `json:"id"`
|
||||
Enabled bool `json:"enabled"`
|
||||
URL string `json:"url"`
|
||||
Name string `json:"name"`
|
||||
RulesCount uint32 `json:"rules_count"`
|
||||
LastUpdated string `json:"last_updated"`
|
||||
}
|
||||
|
||||
func filterToJSON(f Filter) filterJSON {
|
||||
fj := filterJSON{
|
||||
ID: int64(f.ID),
|
||||
Enabled: f.Enabled,
|
||||
URL: f.URL,
|
||||
Name: f.Name,
|
||||
RulesCount: uint32(f.RuleCount),
|
||||
}
|
||||
|
||||
if !f.LastUpdated.IsZero() {
|
||||
fj.LastUpdated = f.LastUpdated.Format(time.RFC3339)
|
||||
}
|
||||
|
||||
return fj
|
||||
}
|
||||
|
||||
// Get filtering configuration
|
||||
func (f *Filtering) handleFilteringStatus(w http.ResponseWriter, r *http.Request) {
|
||||
type respJSON struct {
|
||||
Enabled bool `json:"enabled"`
|
||||
Interval uint32 `json:"interval"` // in hours
|
||||
|
||||
Filters []filterJSON `json:"filters"`
|
||||
WhitelistFilters []filterJSON `json:"whitelist_filters"`
|
||||
UserRules []string `json:"user_rules"`
|
||||
|
||||
Proxylist []filterJSON `json:"proxy_filters"`
|
||||
}
|
||||
resp := respJSON{}
|
||||
|
||||
resp.Enabled = f.conf.Enabled
|
||||
resp.Interval = f.conf.UpdateIntervalHours
|
||||
resp.UserRules = f.conf.UserRules
|
||||
|
||||
f0 := f.dnsBlocklist.List(0)
|
||||
f1 := f.dnsAllowlist.List(0)
|
||||
f2 := f.Proxylist.List(0)
|
||||
|
||||
for _, filt := range f0 {
|
||||
fj := filterToJSON(filt)
|
||||
resp.Filters = append(resp.Filters, fj)
|
||||
}
|
||||
for _, filt := range f1 {
|
||||
fj := filterToJSON(filt)
|
||||
resp.WhitelistFilters = append(resp.WhitelistFilters, fj)
|
||||
}
|
||||
for _, filt := range f2 {
|
||||
fj := filterToJSON(filt)
|
||||
resp.Proxylist = append(resp.Proxylist, fj)
|
||||
}
|
||||
|
||||
jsonVal, err := json.Marshal(resp)
|
||||
if err != nil {
|
||||
httpError(r, w, http.StatusInternalServerError, "json encode: %s", err)
|
||||
return
|
||||
}
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
_, _ = w.Write(jsonVal)
|
||||
}
|
||||
|
||||
// Set filtering configuration
|
||||
func (f *Filtering) handleFilteringConfig(w http.ResponseWriter, r *http.Request) {
|
||||
type reqJSON struct {
|
||||
Enabled bool `json:"enabled"`
|
||||
Interval uint32 `json:"interval"`
|
||||
}
|
||||
req := reqJSON{}
|
||||
_, err := jsonutil.DecodeObject(&req, r.Body)
|
||||
if err != nil {
|
||||
httpError(r, w, http.StatusBadRequest, "json.Decode: %s", err)
|
||||
return
|
||||
}
|
||||
if !CheckFiltersUpdateIntervalHours(req.Interval) {
|
||||
httpError(r, w, http.StatusBadRequest, "Unsupported interval")
|
||||
return
|
||||
}
|
||||
|
||||
restart := false
|
||||
if f.conf.Enabled != req.Enabled {
|
||||
restart = true
|
||||
}
|
||||
f.conf.Enabled = req.Enabled
|
||||
f.conf.UpdateIntervalHours = req.Interval
|
||||
|
||||
c := Conf{}
|
||||
c.UpdateIntervalHours = req.Interval
|
||||
f.dnsBlocklist.SetConfig(c)
|
||||
f.dnsAllowlist.SetConfig(c)
|
||||
f.Proxylist.SetConfig(c)
|
||||
|
||||
f.conf.ConfigModified()
|
||||
|
||||
if restart {
|
||||
f.restartMods("blocklist")
|
||||
}
|
||||
}
|
||||
|
||||
// registerWebHandlers - register handlers
|
||||
func (f *Filtering) registerWebHandlers() {
|
||||
f.conf.HTTPRegister("GET", "/control/filtering/status", f.handleFilteringStatus)
|
||||
f.conf.HTTPRegister("POST", "/control/filtering/config", f.handleFilteringConfig)
|
||||
f.conf.HTTPRegister("POST", "/control/filtering/add_url", f.handleFilterAdd)
|
||||
f.conf.HTTPRegister("POST", "/control/filtering/remove_url", f.handleFilterRemove)
|
||||
f.conf.HTTPRegister("POST", "/control/filtering/set_url", f.handleFilterModify)
|
||||
f.conf.HTTPRegister("POST", "/control/filtering/refresh", f.handleFilteringRefresh)
|
||||
f.conf.HTTPRegister("POST", "/control/filtering/set_rules", f.handleFilteringSetRules)
|
||||
}
|
||||
|
||||
// CheckFiltersUpdateIntervalHours - verify update interval
|
||||
func CheckFiltersUpdateIntervalHours(i uint32) bool {
|
||||
return i == 0 || i == 1 || i == 12 || i == 1*24 || i == 3*24 || i == 7*24
|
||||
}
|
||||
118
filters/filter_module.go
Normal file
@@ -0,0 +1,118 @@
|
||||
package filters
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"path/filepath"
|
||||
)
|
||||
|
||||
// Filtering - module object
|
||||
type Filtering struct {
|
||||
dnsBlocklist Filters // DNS blocklist filters
|
||||
dnsAllowlist Filters // DNS allowlist filters
|
||||
Proxylist Filters // MITM Proxy filtering module
|
||||
|
||||
conf ModuleConf
|
||||
}
|
||||
|
||||
// ModuleConf - module config
|
||||
type ModuleConf struct {
|
||||
Enabled bool
|
||||
UpdateIntervalHours uint32 // 0: disabled
|
||||
HTTPClient *http.Client
|
||||
DataDir string
|
||||
DNSBlocklist []Filter
|
||||
DNSAllowlist []Filter
|
||||
Proxylist []Filter
|
||||
UserRules []string
|
||||
|
||||
// Called when the configuration is changed by HTTP request
|
||||
ConfigModified func()
|
||||
|
||||
// Register an HTTP handler
|
||||
HTTPRegister func(string, string, func(http.ResponseWriter, *http.Request))
|
||||
}
|
||||
|
||||
// NewModule - create module
|
||||
func NewModule(conf ModuleConf) *Filtering {
|
||||
f := Filtering{}
|
||||
f.conf = conf
|
||||
|
||||
fconf := Conf{}
|
||||
fconf.FilterDir = filepath.Join(conf.DataDir, "filters_dnsblock")
|
||||
fconf.List = conf.DNSBlocklist
|
||||
fconf.UpdateIntervalHours = conf.UpdateIntervalHours
|
||||
fconf.HTTPClient = conf.HTTPClient
|
||||
f.dnsBlocklist = New(fconf)
|
||||
|
||||
fconf = Conf{}
|
||||
fconf.FilterDir = filepath.Join(conf.DataDir, "filters_dnsallow")
|
||||
fconf.List = conf.DNSAllowlist
|
||||
fconf.UpdateIntervalHours = conf.UpdateIntervalHours
|
||||
fconf.HTTPClient = conf.HTTPClient
|
||||
f.dnsAllowlist = New(fconf)
|
||||
|
||||
fconf = Conf{}
|
||||
fconf.FilterDir = filepath.Join(conf.DataDir, "filters_mitmproxy")
|
||||
fconf.List = conf.Proxylist
|
||||
fconf.UpdateIntervalHours = conf.UpdateIntervalHours
|
||||
fconf.HTTPClient = conf.HTTPClient
|
||||
f.Proxylist = New(fconf)
|
||||
|
||||
return &f
|
||||
}
|
||||
|
||||
const (
|
||||
DNSBlocklist = iota
|
||||
DNSAllowlist
|
||||
Proxylist
|
||||
)
|
||||
|
||||
func stringArrayDup(a []string) []string {
|
||||
a2 := make([]string, len(a))
|
||||
copy(a2, a)
|
||||
return a2
|
||||
}
|
||||
|
||||
// WriteDiskConfig - write configuration data
|
||||
func (f *Filtering) WriteDiskConfig(mc *ModuleConf) {
|
||||
mc.Enabled = f.conf.Enabled
|
||||
mc.UpdateIntervalHours = f.conf.UpdateIntervalHours
|
||||
mc.UserRules = stringArrayDup(f.conf.UserRules)
|
||||
|
||||
c := Conf{}
|
||||
f.dnsBlocklist.WriteDiskConfig(&c)
|
||||
mc.DNSBlocklist = c.List
|
||||
|
||||
c = Conf{}
|
||||
f.dnsAllowlist.WriteDiskConfig(&c)
|
||||
mc.DNSAllowlist = c.List
|
||||
|
||||
c = Conf{}
|
||||
f.Proxylist.WriteDiskConfig(&c)
|
||||
mc.Proxylist = c.List
|
||||
}
|
||||
|
||||
// GetList - get specific filter list
|
||||
func (f *Filtering) GetList(t uint32) Filters {
|
||||
switch t {
|
||||
case DNSBlocklist:
|
||||
return f.dnsBlocklist
|
||||
case DNSAllowlist:
|
||||
return f.dnsAllowlist
|
||||
case Proxylist:
|
||||
return f.Proxylist
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Start - start module
|
||||
func (f *Filtering) Start() {
|
||||
f.dnsBlocklist.Start()
|
||||
f.dnsAllowlist.Start()
|
||||
f.Proxylist.Start()
|
||||
f.registerWebHandlers()
|
||||
}
|
||||
|
||||
// Close - close the module
|
||||
func (f *Filtering) Close() {
|
||||
}
|
||||
246
filters/filter_storage.go
Normal file
@@ -0,0 +1,246 @@
|
||||
package filters
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/AdguardTeam/golibs/log"
|
||||
"go.uber.org/atomic"
|
||||
)
|
||||
|
||||
// filter storage object
|
||||
type filterStg struct {
|
||||
updateTaskRunning bool
|
||||
updated []Filter // list of filters that were downloaded during update procedure
|
||||
updateChan chan bool // signal for the update goroutine
|
||||
|
||||
conf *Conf
|
||||
confLock sync.Mutex
|
||||
nextID atomic.Uint64 // next filter ID
|
||||
|
||||
observer EventHandler // user function that receives notifications
|
||||
}
|
||||
|
||||
// initialize the module
|
||||
func newFiltersObj(conf Conf) Filters {
|
||||
fs := filterStg{}
|
||||
fs.conf = &Conf{}
|
||||
*fs.conf = conf
|
||||
fs.nextID.Store(uint64(time.Now().Unix()))
|
||||
fs.updateChan = make(chan bool, 2)
|
||||
return &fs
|
||||
}
|
||||
|
||||
// Start - start module
|
||||
func (fs *filterStg) Start() {
|
||||
_ = os.MkdirAll(fs.conf.FilterDir, 0755)
|
||||
|
||||
// Load all enabled filters
|
||||
// On error, RuleCount is set to 0 - users won't try to use such filters
|
||||
// and in the future the update procedure will re-download the file
|
||||
for i := range fs.conf.List {
|
||||
f := &fs.conf.List[i]
|
||||
|
||||
fname := fs.filePath(*f)
|
||||
st, err := os.Stat(fname)
|
||||
if err != nil {
|
||||
log.Debug("Filters: os.Stat: %s %s", fname, err)
|
||||
continue
|
||||
}
|
||||
f.LastUpdated = st.ModTime()
|
||||
|
||||
if !f.Enabled {
|
||||
continue
|
||||
}
|
||||
|
||||
file, err := os.OpenFile(fname, os.O_RDONLY, 0)
|
||||
if err != nil {
|
||||
log.Error("Filters: os.OpenFile: %s %s", fname, err)
|
||||
continue
|
||||
}
|
||||
|
||||
_ = parseFilter(f, file)
|
||||
file.Close()
|
||||
|
||||
f.nextUpdate = f.LastUpdated.Add(time.Duration(fs.conf.UpdateIntervalHours) * time.Hour)
|
||||
}
|
||||
|
||||
if !fs.updateTaskRunning {
|
||||
fs.updateTaskRunning = true
|
||||
go fs.updateBySignal()
|
||||
go fs.updateByTimer()
|
||||
}
|
||||
}
|
||||
|
||||
// Close - close the module
|
||||
func (fs *filterStg) Close() {
|
||||
fs.updateChan <- false
|
||||
close(fs.updateChan)
|
||||
}
|
||||
|
||||
// Duplicate filter array
|
||||
func arrayFilterDup(f []Filter) []Filter {
|
||||
nf := make([]Filter, len(f))
|
||||
copy(nf, f)
|
||||
return nf
|
||||
}
|
||||
|
||||
// WriteDiskConfig - write configuration on disk
|
||||
func (fs *filterStg) WriteDiskConfig(c *Conf) {
|
||||
fs.confLock.Lock()
|
||||
*c = *fs.conf
|
||||
c.List = arrayFilterDup(fs.conf.List)
|
||||
fs.confLock.Unlock()
|
||||
}
|
||||
|
||||
// SetConfig - set new configuration settings
|
||||
func (fs *filterStg) SetConfig(c Conf) {
|
||||
fs.conf.UpdateIntervalHours = c.UpdateIntervalHours
|
||||
}
|
||||
|
||||
// SetObserver - set user handler for notifications
|
||||
func (fs *filterStg) SetObserver(handler EventHandler) {
|
||||
fs.observer = handler
|
||||
}
|
||||
|
||||
// NotifyObserver - notify users about the event
|
||||
func (fs *filterStg) NotifyObserver(flags uint) {
|
||||
if fs.observer == nil {
|
||||
return
|
||||
}
|
||||
fs.observer(flags)
|
||||
}
|
||||
|
||||
// List (thread safe)
|
||||
func (fs *filterStg) List(flags uint) []Filter {
|
||||
fs.confLock.Lock()
|
||||
list := make([]Filter, len(fs.conf.List))
|
||||
for i, f := range fs.conf.List {
|
||||
nf := f
|
||||
nf.Path = fs.filePath(f)
|
||||
list[i] = nf
|
||||
}
|
||||
fs.confLock.Unlock()
|
||||
return list
|
||||
}
|
||||
|
||||
// Add - add filter (thread safe)
|
||||
func (fs *filterStg) Add(nf Filter) error {
|
||||
fs.confLock.Lock()
|
||||
defer fs.confLock.Unlock()
|
||||
|
||||
for _, f := range fs.conf.List {
|
||||
if f.Name == nf.Name || f.URL == nf.URL {
|
||||
return fmt.Errorf("filter with this Name or URL already exists")
|
||||
}
|
||||
}
|
||||
|
||||
nf.ID = fs.nextFilterID()
|
||||
nf.Enabled = true
|
||||
err := fs.downloadFilter(&nf)
|
||||
if err != nil {
|
||||
log.Debug("%s", err)
|
||||
return err
|
||||
}
|
||||
fs.conf.List = append(fs.conf.List, nf)
|
||||
log.Debug("Filters: added filter %s", nf.URL)
|
||||
return nil
|
||||
}
|
||||
|
||||
// Delete - remove filter (thread safe)
|
||||
func (fs *filterStg) Delete(url string) *Filter {
|
||||
fs.confLock.Lock()
|
||||
defer fs.confLock.Unlock()
|
||||
|
||||
nf := []Filter{}
|
||||
var found *Filter
|
||||
for i := range fs.conf.List {
|
||||
f := &fs.conf.List[i]
|
||||
|
||||
if f.URL == url {
|
||||
found = f
|
||||
continue
|
||||
}
|
||||
nf = append(nf, *f)
|
||||
}
|
||||
if found == nil {
|
||||
return nil
|
||||
}
|
||||
fs.conf.List = nf
|
||||
log.Debug("Filters: removed filter %s", url)
|
||||
found.Path = fs.filePath(*found) // the caller will delete the file
|
||||
return found
|
||||
}
|
||||
|
||||
// Modify - set filter properties (thread safe)
|
||||
// Return Status* bitarray
|
||||
func (fs *filterStg) Modify(url string, enabled bool, name string, newURL string) (int, Filter, error) {
|
||||
fs.confLock.Lock()
|
||||
defer fs.confLock.Unlock()
|
||||
|
||||
st := 0
|
||||
|
||||
for i := range fs.conf.List {
|
||||
f := &fs.conf.List[i]
|
||||
if f.URL == url {
|
||||
|
||||
backup := *f
|
||||
f.Name = name
|
||||
|
||||
if f.Enabled != enabled {
|
||||
f.Enabled = enabled
|
||||
st |= StatusChangedEnabled
|
||||
}
|
||||
|
||||
if f.URL != newURL {
|
||||
f.URL = newURL
|
||||
st |= StatusChangedURL
|
||||
}
|
||||
|
||||
needDownload := false
|
||||
|
||||
if (st & StatusChangedURL) != 0 {
|
||||
f.ID = fs.nextFilterID()
|
||||
needDownload = true
|
||||
|
||||
} else if (st&StatusChangedEnabled) != 0 && enabled {
|
||||
fname := fs.filePath(*f)
|
||||
file, err := os.OpenFile(fname, os.O_RDONLY, 0)
|
||||
if err != nil {
|
||||
log.Debug("Filters: os.OpenFile: %s %s", fname, err)
|
||||
needDownload = true
|
||||
} else {
|
||||
_ = parseFilter(f, file)
|
||||
file.Close()
|
||||
}
|
||||
}
|
||||
|
||||
if needDownload {
|
||||
f.LastModified = ""
|
||||
f.RuleCount = 0
|
||||
err := fs.downloadFilter(f)
|
||||
if err != nil {
|
||||
*f = backup
|
||||
return 0, Filter{}, err
|
||||
}
|
||||
}
|
||||
|
||||
return st, backup, nil
|
||||
}
|
||||
}
|
||||
|
||||
return 0, Filter{}, fmt.Errorf("filter %s not found", url)
|
||||
}
|
||||
|
||||
// Get filter file name
|
||||
func (fs *filterStg) filePath(f Filter) string {
|
||||
return filepath.Join(fs.conf.FilterDir, fmt.Sprintf("%d.txt", f.ID))
|
||||
}
|
||||
|
||||
// Get next filter ID
|
||||
func (fs *filterStg) nextFilterID() uint64 {
|
||||
return fs.nextID.Inc()
|
||||
}
|
||||
154
filters/filter_test.go
Normal file
@@ -0,0 +1,154 @@
|
||||
package filters
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net"
|
||||
"net/http"
|
||||
"os"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"go.uber.org/atomic"
|
||||
)
|
||||
|
||||
func testStartFilterListener(counter *atomic.Uint32) net.Listener {
|
||||
mux := http.NewServeMux()
|
||||
|
||||
mux.HandleFunc("/filters/1.txt", func(w http.ResponseWriter, r *http.Request) {
|
||||
(*counter).Inc()
|
||||
content := `||example.org^$third-party
|
||||
# Inline comment example
|
||||
||example.com^$third-party
|
||||
0.0.0.0 example.com
|
||||
`
|
||||
_, _ = w.Write([]byte(content))
|
||||
})
|
||||
|
||||
mux.HandleFunc("/filters/2.txt", func(w http.ResponseWriter, r *http.Request) {
|
||||
(*counter).Inc()
|
||||
content := `||example.org^$third-party
|
||||
# Inline comment example
|
||||
||example.com^$third-party
|
||||
0.0.0.0 example.com
|
||||
1.1.1.1 example1.com
|
||||
`
|
||||
_, _ = w.Write([]byte(content))
|
||||
})
|
||||
|
||||
listener, err := net.Listen("tcp", ":0")
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
go func() {
|
||||
_ = http.Serve(listener, mux)
|
||||
}()
|
||||
return listener
|
||||
}
|
||||
|
||||
func prepareTestDir() string {
|
||||
const dir = "./agh-test"
|
||||
_ = os.RemoveAll(dir)
|
||||
_ = os.MkdirAll(dir, 0755)
|
||||
return dir
|
||||
}
|
||||
|
||||
var updateStatus atomic.Uint32
|
||||
|
||||
func onFiltersUpdate(flags uint) {
|
||||
switch flags {
|
||||
case EventBeforeUpdate:
|
||||
updateStatus.Store(updateStatus.Load() | 1)
|
||||
|
||||
case EventAfterUpdate:
|
||||
updateStatus.Store(updateStatus.Load() | 2)
|
||||
}
|
||||
}
|
||||
|
||||
func TestFilters(t *testing.T) {
|
||||
counter := atomic.Uint32{}
|
||||
lhttp := testStartFilterListener(&counter)
|
||||
defer func() { _ = lhttp.Close() }()
|
||||
|
||||
dir := prepareTestDir()
|
||||
defer func() { _ = os.RemoveAll(dir) }()
|
||||
|
||||
fconf := Conf{}
|
||||
fconf.UpdateIntervalHours = 1
|
||||
fconf.FilterDir = dir
|
||||
fconf.HTTPClient = &http.Client{
|
||||
Timeout: 5 * time.Second,
|
||||
}
|
||||
fs := New(fconf)
|
||||
fs.SetObserver(onFiltersUpdate)
|
||||
fs.Start()
|
||||
|
||||
port := lhttp.Addr().(*net.TCPAddr).Port
|
||||
URL := fmt.Sprintf("http://127.0.0.1:%d/filters/1.txt", port)
|
||||
|
||||
// add and download
|
||||
f := Filter{
|
||||
URL: URL,
|
||||
}
|
||||
err := fs.Add(f)
|
||||
assert.Equal(t, nil, err)
|
||||
|
||||
// check
|
||||
l := fs.List(0)
|
||||
assert.Equal(t, 1, len(l))
|
||||
assert.Equal(t, URL, l[0].URL)
|
||||
assert.True(t, l[0].Enabled)
|
||||
assert.Equal(t, uint64(3), l[0].RuleCount)
|
||||
assert.True(t, l[0].ID != 0)
|
||||
|
||||
// disable
|
||||
st, _, err := fs.Modify(f.URL, false, "name", f.URL)
|
||||
assert.Equal(t, StatusChangedEnabled, st)
|
||||
|
||||
// check: disabled
|
||||
l = fs.List(0)
|
||||
assert.Equal(t, 1, len(l))
|
||||
assert.True(t, !l[0].Enabled)
|
||||
|
||||
// modify URL
|
||||
newURL := fmt.Sprintf("http://127.0.0.1:%d/filters/2.txt", port)
|
||||
st, modified, err := fs.Modify(URL, false, "name", newURL)
|
||||
assert.Equal(t, StatusChangedURL, st)
|
||||
|
||||
_ = os.Remove(modified.Path)
|
||||
|
||||
// check: new ID, new URL
|
||||
l = fs.List(0)
|
||||
assert.Equal(t, 1, len(l))
|
||||
assert.Equal(t, newURL, l[0].URL)
|
||||
assert.Equal(t, uint64(4), l[0].RuleCount)
|
||||
assert.True(t, modified.ID != l[0].ID)
|
||||
|
||||
// enable
|
||||
st, _, err = fs.Modify(newURL, true, "name", newURL)
|
||||
assert.Equal(t, StatusChangedEnabled, st)
|
||||
|
||||
// update
|
||||
cnt := counter.Load()
|
||||
fs.Refresh(0)
|
||||
for i := 0; ; i++ {
|
||||
if i == 2 {
|
||||
assert.True(t, false)
|
||||
break
|
||||
}
|
||||
if cnt != counter.Load() {
|
||||
// filter was updated
|
||||
break
|
||||
}
|
||||
time.Sleep(time.Second)
|
||||
}
|
||||
assert.Equal(t, uint32(1|2), updateStatus.Load())
|
||||
|
||||
// delete
|
||||
removed := fs.Delete(newURL)
|
||||
assert.NotNil(t, removed)
|
||||
_ = os.Remove(removed.Path)
|
||||
|
||||
fs.Close()
|
||||
}
|
||||
176
filters/filter_update.go
Normal file
@@ -0,0 +1,176 @@
|
||||
package filters
|
||||
|
||||
import (
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"github.com/AdguardTeam/golibs/log"
|
||||
)
|
||||
|
||||
// Refresh - begin filters update procedure
|
||||
func (fs *filterStg) Refresh(flags uint) {
|
||||
fs.confLock.Lock()
|
||||
defer fs.confLock.Unlock()
|
||||
|
||||
for i := range fs.conf.List {
|
||||
f := &fs.conf.List[i]
|
||||
f.nextUpdate = time.Time{}
|
||||
}
|
||||
|
||||
fs.updateChan <- true
|
||||
}
|
||||
|
||||
// Start update procedure periodically
|
||||
func (fs *filterStg) updateByTimer() {
|
||||
const maxPeriod = 1 * 60 * 60
|
||||
period := 5 // use a dynamically increasing time interval, while network or DNS is down
|
||||
for {
|
||||
if fs.conf.UpdateIntervalHours == 0 {
|
||||
period = maxPeriod
|
||||
// update is disabled
|
||||
time.Sleep(time.Duration(period) * time.Second)
|
||||
continue
|
||||
}
|
||||
|
||||
fs.updateChan <- true
|
||||
|
||||
time.Sleep(time.Duration(period) * time.Second)
|
||||
period += period
|
||||
if period > maxPeriod {
|
||||
period = maxPeriod
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Begin update procedure by signal
|
||||
func (fs *filterStg) updateBySignal() {
|
||||
for {
|
||||
select {
|
||||
case ok := <-fs.updateChan:
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
fs.updateAll()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Update filters
|
||||
// Algorithm:
|
||||
// . Get next filter to update:
|
||||
// . Download data from Internet and store on disk (in a new file)
|
||||
// . Add new filter to the special list
|
||||
// . Repeat for next filter
|
||||
// (All filters are downloaded)
|
||||
// . Stop modules that use filters
|
||||
// . For each updated filter:
|
||||
// . Rename "new file name" -> "old file name"
|
||||
// . Update meta data
|
||||
// . Restart modules that use filters
|
||||
func (fs *filterStg) updateAll() {
|
||||
log.Debug("Filters: updating...")
|
||||
|
||||
for {
|
||||
var uf Filter
|
||||
fs.confLock.Lock()
|
||||
f := fs.getNextToUpdate()
|
||||
if f != nil {
|
||||
uf = *f
|
||||
}
|
||||
fs.confLock.Unlock()
|
||||
|
||||
if f == nil {
|
||||
fs.applyUpdate()
|
||||
return
|
||||
}
|
||||
|
||||
uf.ID = fs.nextFilterID()
|
||||
err := fs.downloadFilter(&uf)
|
||||
if err != nil {
|
||||
if uf.networkError {
|
||||
fs.confLock.Lock()
|
||||
f.nextUpdate = time.Now().Add(10 * time.Second)
|
||||
fs.confLock.Unlock()
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
// add new filter to the list
|
||||
fs.updated = append(fs.updated, uf)
|
||||
}
|
||||
}
|
||||
|
||||
// Get next filter to update
|
||||
func (fs *filterStg) getNextToUpdate() *Filter {
|
||||
now := time.Now()
|
||||
|
||||
for i := range fs.conf.List {
|
||||
f := &fs.conf.List[i]
|
||||
|
||||
if f.Enabled &&
|
||||
f.nextUpdate.Unix() <= now.Unix() {
|
||||
|
||||
f.nextUpdate = now.Add(time.Duration(fs.conf.UpdateIntervalHours) * time.Hour)
|
||||
return f
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Replace filter files
|
||||
func (fs *filterStg) applyUpdate() {
|
||||
if len(fs.updated) == 0 {
|
||||
log.Debug("Filters: no filters were updated")
|
||||
return
|
||||
}
|
||||
|
||||
fs.NotifyObserver(EventBeforeUpdate)
|
||||
|
||||
nUpdated := 0
|
||||
|
||||
fs.confLock.Lock()
|
||||
for _, uf := range fs.updated {
|
||||
found := false
|
||||
|
||||
for i := range fs.conf.List {
|
||||
f := &fs.conf.List[i]
|
||||
|
||||
if uf.URL == f.URL {
|
||||
found = true
|
||||
fpath := fs.filePath(*f)
|
||||
f.LastUpdated = uf.LastUpdated
|
||||
|
||||
if len(uf.Path) == 0 {
|
||||
// the data hasn't changed - just update file mod time
|
||||
err := os.Chtimes(fpath, f.LastUpdated, f.LastUpdated)
|
||||
if err != nil {
|
||||
log.Error("Filters: os.Chtimes: %s", err)
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
err := os.Rename(uf.Path, fpath)
|
||||
if err != nil {
|
||||
log.Error("Filters: os.Rename:%s", err)
|
||||
}
|
||||
|
||||
f.RuleCount = uf.RuleCount
|
||||
nUpdated++
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if !found {
|
||||
// the updated filter was downloaded,
|
||||
// but it's already removed from the main list
|
||||
_ = os.Remove(fs.filePath(uf))
|
||||
}
|
||||
}
|
||||
fs.confLock.Unlock()
|
||||
|
||||
log.Debug("Filters: %d filters were updated", nUpdated)
|
||||
|
||||
fs.updated = nil
|
||||
fs.NotifyObserver(EventAfterUpdate)
|
||||
}
|
||||
93
filters/filters.go
Normal file
@@ -0,0 +1,93 @@
|
||||
package filters
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Filters - main interface
|
||||
type Filters interface {
|
||||
// Start - start module
|
||||
Start()
|
||||
|
||||
// Close - close the module
|
||||
Close()
|
||||
|
||||
// WriteDiskConfig - write configuration on disk
|
||||
WriteDiskConfig(c *Conf)
|
||||
|
||||
// SetConfig - set new configuration settings
|
||||
// Currently only UpdateIntervalHours is supported
|
||||
SetConfig(c Conf)
|
||||
|
||||
// SetObserver - set user handler for notifications
|
||||
SetObserver(handler EventHandler)
|
||||
|
||||
// NotifyObserver - notify users about the event
|
||||
NotifyObserver(flags uint)
|
||||
|
||||
// List (thread safe)
|
||||
List(flags uint) []Filter
|
||||
|
||||
// Add - add filter (thread safe)
|
||||
Add(nf Filter) error
|
||||
|
||||
// Delete - remove filter (thread safe)
|
||||
Delete(url string) *Filter
|
||||
|
||||
// Modify - set filter properties (thread safe)
|
||||
// Return Status* bitarray, old filter properties and error
|
||||
Modify(url string, enabled bool, name string, newURL string) (int, Filter, error)
|
||||
|
||||
// Refresh - begin filters update procedure
|
||||
Refresh(flags uint)
|
||||
}
|
||||
|
||||
// Filter - filter object
|
||||
type Filter struct {
|
||||
ID uint64 `yaml:"id"`
|
||||
Enabled bool `yaml:"enabled"`
|
||||
Name string `yaml:"name"`
|
||||
URL string `yaml:"url"`
|
||||
LastModified string `yaml:"last_modified"` // value of Last-Modified HTTP header field
|
||||
|
||||
Path string `yaml:"-"`
|
||||
|
||||
// number of rules
|
||||
// 0 means the file isn't loaded - user shouldn't use this filter
|
||||
RuleCount uint64 `yaml:"-"`
|
||||
|
||||
LastUpdated time.Time `yaml:"-"` // time of the last update (= file modification time)
|
||||
nextUpdate time.Time // time of the next update
|
||||
networkError bool // network error during download
|
||||
}
|
||||
|
||||
const (
|
||||
// EventBeforeUpdate - this event is signalled before the update procedure renames/removes old filter files
|
||||
EventBeforeUpdate = iota
|
||||
// EventAfterUpdate - this event is signalled after the update procedure is finished
|
||||
EventAfterUpdate
|
||||
)
|
||||
|
||||
// EventHandler - event handler function
|
||||
type EventHandler func(flags uint)
|
||||
|
||||
const (
|
||||
// StatusChangedEnabled - changed 'Enabled'
|
||||
StatusChangedEnabled = 2
|
||||
// StatusChangedURL - changed 'URL'
|
||||
StatusChangedURL = 4
|
||||
)
|
||||
|
||||
// Conf - configuration
|
||||
type Conf struct {
|
||||
FilterDir string
|
||||
UpdateIntervalHours uint32 // 0: disabled
|
||||
HTTPClient *http.Client
|
||||
List []Filter
|
||||
}
|
||||
|
||||
// New - create object
|
||||
func New(conf Conf) Filters {
|
||||
return newFiltersObj(conf)
|
||||
}
|
||||
6
go.mod
@@ -3,9 +3,10 @@ module github.com/AdguardTeam/AdGuardHome
|
||||
go 1.14
|
||||
|
||||
require (
|
||||
github.com/AdguardTeam/dnsproxy v0.29.1
|
||||
github.com/AdguardTeam/dnsproxy v0.30.1
|
||||
github.com/AdguardTeam/golibs v0.4.2
|
||||
github.com/AdguardTeam/urlfilter v0.11.0
|
||||
github.com/AdguardTeam/gomitmproxy v0.2.0
|
||||
github.com/AdguardTeam/urlfilter v0.11.2
|
||||
github.com/NYTimes/gziphandler v1.1.1
|
||||
github.com/fsnotify/fsnotify v1.4.7
|
||||
github.com/gobuffalo/packr v1.30.1
|
||||
@@ -17,6 +18,7 @@ require (
|
||||
github.com/sparrc/go-ping v0.0.0-20190613174326-4e5b6552494c
|
||||
github.com/stretchr/testify v1.5.1
|
||||
go.etcd.io/bbolt v1.3.4
|
||||
go.uber.org/atomic v1.6.0
|
||||
golang.org/x/crypto v0.0.0-20200403201458-baeed622b8d8
|
||||
golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e
|
||||
golang.org/x/sys v0.0.0-20200331124033-c3d80250170d
|
||||
|
||||
16
go.sum
@@ -1,11 +1,12 @@
|
||||
github.com/AdguardTeam/dnsproxy v0.29.1 h1:Stc+JLh67C9K38vbrH2920+3FnbXKkFzYQqRiu5auUo=
|
||||
github.com/AdguardTeam/dnsproxy v0.29.1/go.mod h1:hOYFV9TW+pd5XKYz7KZf2FFD8SvSPqjyGTxUae86s58=
|
||||
github.com/AdguardTeam/dnsproxy v0.30.1 h1:SnsL5kM/eFTrtLLdww1EePOhVDZTWzMkse+5tadGhvc=
|
||||
github.com/AdguardTeam/dnsproxy v0.30.1/go.mod h1:hOYFV9TW+pd5XKYz7KZf2FFD8SvSPqjyGTxUae86s58=
|
||||
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/gomitmproxy v0.2.0 h1:rvCOf17pd1/CnMyMQW891zrEiIQBpQ8cIGjKN9pinUU=
|
||||
github.com/AdguardTeam/gomitmproxy v0.2.0/go.mod h1:Qdv0Mktnzer5zpdpi5rAwixNJzW2FN91LjKJCkVbYGU=
|
||||
github.com/AdguardTeam/urlfilter v0.11.0 h1:tgZss6uZs1UZAaxpovD/QuX+VVIQLDOlKc7rdF8dwNw=
|
||||
github.com/AdguardTeam/urlfilter v0.11.0/go.mod h1:aMuejlNxpWppOVjiEV87X6z0eMf7wsXHTAIWQuylfZY=
|
||||
github.com/AdguardTeam/urlfilter v0.11.2 h1:gCrWGh63Yqw3z4yi9pgikfsbshIEyvAu/KYV3MvTBlc=
|
||||
github.com/AdguardTeam/urlfilter v0.11.2/go.mod h1:aMuejlNxpWppOVjiEV87X6z0eMf7wsXHTAIWQuylfZY=
|
||||
github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ=
|
||||
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
|
||||
github.com/NYTimes/gziphandler v1.1.1 h1:ZUDjpQae29j0ryrS0u/B8HZfJBtBQHjqw2rQ2cqUQ3I=
|
||||
@@ -113,6 +114,8 @@ github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8/go.mod h1:VFNgLljT
|
||||
github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q=
|
||||
go.etcd.io/bbolt v1.3.4 h1:hi1bXHMVrlQh6WwxAy+qZCV/SYIlqo+Ushwdpa4tAKg=
|
||||
go.etcd.io/bbolt v1.3.4/go.mod h1:G5EMThwa9y8QZGBClrRx5EY+Yw9kAhnjy3bSjsnlVTQ=
|
||||
go.uber.org/atomic v1.6.0 h1:Ezj3JGmsOnG1MoRWQkPBsKLe9DwWD9QeXzTRzzldNVk=
|
||||
go.uber.org/atomic v1.6.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ=
|
||||
golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20190621222207-cc06ce4a13d4/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
@@ -120,6 +123,8 @@ golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8U
|
||||
golang.org/x/crypto v0.0.0-20200323165209-0ec3e9974c59/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.0.0-20200403201458-baeed622b8d8 h1:fpnn/HnJONpIu6hkXi1u/7rR0NzilgWr4T0JmWkEitk=
|
||||
golang.org/x/crypto v0.0.0-20200403201458-baeed622b8d8/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/lint v0.0.0-20190930215403-16217165b5de h1:5hukYrvBGR8/eNkX5mdUezrA6JiaEZDtJb9Ei+1LlBs=
|
||||
golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
|
||||
golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
|
||||
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
@@ -145,9 +150,12 @@ golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs=
|
||||
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
||||
golang.org/x/tools v0.0.0-20190624180213-70d37148ca0c/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
|
||||
golang.org/x/tools v0.0.0-20191029041327-9cc4af7d6b2c/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20191216052735-49a3e744a425 h1:VvQyQJN0tSuecqgcIxMWnnfG5kSmgy9KZR9sW3W5QeA=
|
||||
golang.org/x/tools v0.0.0-20191216052735-49a3e744a425/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
|
||||
@@ -9,6 +9,8 @@ import (
|
||||
"github.com/AdguardTeam/AdGuardHome/dhcpd"
|
||||
"github.com/AdguardTeam/AdGuardHome/dnsfilter"
|
||||
"github.com/AdguardTeam/AdGuardHome/dnsforward"
|
||||
"github.com/AdguardTeam/AdGuardHome/filters"
|
||||
"github.com/AdguardTeam/AdGuardHome/mitmproxy"
|
||||
"github.com/AdguardTeam/AdGuardHome/querylog"
|
||||
"github.com/AdguardTeam/AdGuardHome/stats"
|
||||
"github.com/AdguardTeam/golibs/file"
|
||||
@@ -17,8 +19,7 @@ import (
|
||||
)
|
||||
|
||||
const (
|
||||
dataDir = "data" // data storage
|
||||
filterDir = "filters" // cache location for downloaded filters, it's under DataDir
|
||||
dataDir = "data" // data storage
|
||||
)
|
||||
|
||||
// logSettings
|
||||
@@ -54,9 +55,13 @@ type configuration struct {
|
||||
DNS dnsConfig `yaml:"dns"`
|
||||
TLS tlsConfigSettings `yaml:"tls"`
|
||||
|
||||
Filters []filter `yaml:"filters"`
|
||||
WhitelistFilters []filter `yaml:"whitelist_filters"`
|
||||
UserRules []string `yaml:"user_rules"`
|
||||
MITM mitmproxy.Config `yaml:"mitmproxy"`
|
||||
|
||||
Filters []filters.Filter `yaml:"filters"`
|
||||
WhitelistFilters []filters.Filter `yaml:"whitelist_filters"`
|
||||
UserRules []string `yaml:"user_rules"`
|
||||
|
||||
ProxyFilters []filters.Filter `yaml:"proxy_filters"`
|
||||
|
||||
DHCP dhcpd.ServerConfig `yaml:"dhcp"`
|
||||
|
||||
@@ -155,7 +160,43 @@ func initConfig() {
|
||||
config.DNS.DnsfilterConf.SafeSearchCacheSize = 1 * 1024 * 1024
|
||||
config.DNS.DnsfilterConf.ParentalCacheSize = 1 * 1024 * 1024
|
||||
config.DNS.DnsfilterConf.CacheTime = 30
|
||||
config.Filters = defaultFilters()
|
||||
config.Filters = defaultDNSBlocklistFilters()
|
||||
|
||||
config.ProxyFilters = defaultContentFilters()
|
||||
}
|
||||
|
||||
func defaultDNSBlocklistFilters() []filters.Filter {
|
||||
return []filters.Filter{
|
||||
{
|
||||
ID: 1,
|
||||
Enabled: true,
|
||||
URL: "https://adguardteam.github.io/AdGuardSDNSFilter/Filters/filter.txt",
|
||||
Name: "AdGuard Simplified Domain Names filter",
|
||||
},
|
||||
{
|
||||
ID: 2,
|
||||
Enabled: false,
|
||||
URL: "https://adaway.org/hosts.txt",
|
||||
Name: "AdAway",
|
||||
},
|
||||
{
|
||||
ID: 3,
|
||||
Enabled: false,
|
||||
URL: "https://www.malwaredomainlist.com/hostslist/hosts.txt",
|
||||
Name: "MalwareDomainList.com Hosts List",
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func defaultContentFilters() []filters.Filter {
|
||||
return []filters.Filter{
|
||||
{
|
||||
ID: 1,
|
||||
Enabled: true,
|
||||
URL: "https://filters.adtidy.org/extension/chromium/filters/2.txt",
|
||||
Name: "AdGuard Base filter",
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// getConfigFilename returns path to the current config file
|
||||
@@ -203,7 +244,7 @@ func parseConfig() error {
|
||||
return err
|
||||
}
|
||||
|
||||
if !checkFiltersUpdateIntervalHours(config.DNS.FiltersUpdateIntervalHours) {
|
||||
if !filters.CheckFiltersUpdateIntervalHours(config.DNS.FiltersUpdateIntervalHours) {
|
||||
config.DNS.FiltersUpdateIntervalHours = 24
|
||||
}
|
||||
|
||||
@@ -263,6 +304,17 @@ func (c *configuration) write() error {
|
||||
config.DNS.DnsfilterConf = c
|
||||
}
|
||||
|
||||
if Context.filters != nil {
|
||||
fconf := filters.ModuleConf{}
|
||||
Context.filters.WriteDiskConfig(&fconf)
|
||||
config.DNS.FilteringEnabled = fconf.Enabled
|
||||
config.DNS.FiltersUpdateIntervalHours = fconf.UpdateIntervalHours
|
||||
config.Filters = fconf.DNSBlocklist
|
||||
config.WhitelistFilters = fconf.DNSAllowlist
|
||||
config.ProxyFilters = fconf.Proxylist
|
||||
config.UserRules = fconf.UserRules
|
||||
}
|
||||
|
||||
if Context.dnsServer != nil {
|
||||
c := dnsforward.FilteringConfig{}
|
||||
Context.dnsServer.WriteDiskConfig(&c)
|
||||
@@ -275,6 +327,12 @@ func (c *configuration) write() error {
|
||||
config.DHCP = c
|
||||
}
|
||||
|
||||
if Context.mitmProxy != nil {
|
||||
c := mitmproxy.Config{}
|
||||
Context.mitmProxy.WriteDiskConfig(&c)
|
||||
config.MITM = c
|
||||
}
|
||||
|
||||
configFile := config.getConfigFilename()
|
||||
log.Debug("Writing YAML file: %s", configFile)
|
||||
yamlText, err := yaml.Marshal(&config)
|
||||
|
||||
@@ -12,6 +12,7 @@ import (
|
||||
"github.com/AdguardTeam/AdGuardHome/dnsforward"
|
||||
"github.com/AdguardTeam/golibs/log"
|
||||
"github.com/NYTimes/gziphandler"
|
||||
"github.com/miekg/dns"
|
||||
)
|
||||
|
||||
// ----------------
|
||||
@@ -87,6 +88,48 @@ func handleGetProfile(w http.ResponseWriter, r *http.Request) {
|
||||
_, _ = w.Write(data)
|
||||
}
|
||||
|
||||
type checkHostResp struct {
|
||||
Reason string `json:"reason"`
|
||||
FilterID int64 `json:"filter_id"`
|
||||
Rule string `json:"rule"`
|
||||
|
||||
// for FilteredBlockedService:
|
||||
SvcName string `json:"service_name"`
|
||||
|
||||
// for ReasonRewrite:
|
||||
CanonName string `json:"cname"` // CNAME value
|
||||
IPList []net.IP `json:"ip_addrs"` // list of IP addresses
|
||||
}
|
||||
|
||||
func handleCheckHost(w http.ResponseWriter, r *http.Request) {
|
||||
q := r.URL.Query()
|
||||
host := q.Get("name")
|
||||
|
||||
setts := Context.dnsFilter.GetConfig()
|
||||
setts.FilteringEnabled = true
|
||||
Context.dnsFilter.ApplyBlockedServices(&setts, nil, true)
|
||||
result, err := Context.dnsFilter.CheckHost(host, dns.TypeA, &setts)
|
||||
if err != nil {
|
||||
httpError(w, http.StatusInternalServerError, "couldn't apply filtering: %s: %s", host, err)
|
||||
return
|
||||
}
|
||||
|
||||
resp := checkHostResp{}
|
||||
resp.Reason = result.Reason.String()
|
||||
resp.FilterID = result.FilterID
|
||||
resp.Rule = result.Rule
|
||||
resp.SvcName = result.ServiceName
|
||||
resp.CanonName = result.CanonName
|
||||
resp.IPList = result.IPList
|
||||
js, err := json.Marshal(resp)
|
||||
if err != nil {
|
||||
httpError(w, http.StatusInternalServerError, "json encode: %s", err)
|
||||
return
|
||||
}
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
_, _ = w.Write(js)
|
||||
}
|
||||
|
||||
// ------------------------
|
||||
// registration of handlers
|
||||
// ------------------------
|
||||
@@ -96,6 +139,7 @@ func registerControlHandlers() {
|
||||
httpRegister(http.MethodGet, "/control/i18n/current_language", handleI18nCurrentLanguage)
|
||||
http.HandleFunc("/control/version.json", postInstall(optionalAuth(handleGetVersionJSON)))
|
||||
httpRegister(http.MethodPost, "/control/update", handleUpdate)
|
||||
httpRegister("GET", "/control/filtering/check_host", handleCheckHost)
|
||||
|
||||
httpRegister("GET", "/control/profile", handleGetProfile)
|
||||
RegisterAuthHandlers()
|
||||
|
||||
@@ -1,391 +0,0 @@
|
||||
package home
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/AdguardTeam/AdGuardHome/util"
|
||||
"github.com/AdguardTeam/golibs/log"
|
||||
"github.com/miekg/dns"
|
||||
)
|
||||
|
||||
// IsValidURL - return TRUE if URL or file path is valid
|
||||
func IsValidURL(rawurl string) bool {
|
||||
if filepath.IsAbs(rawurl) {
|
||||
// this is a file path
|
||||
return util.FileExists(rawurl)
|
||||
}
|
||||
|
||||
url, err := url.ParseRequestURI(rawurl)
|
||||
if err != nil {
|
||||
return false //Couldn't even parse the rawurl
|
||||
}
|
||||
if len(url.Scheme) == 0 {
|
||||
return false //No Scheme found
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
type filterAddJSON struct {
|
||||
Name string `json:"name"`
|
||||
URL string `json:"url"`
|
||||
Whitelist bool `json:"whitelist"`
|
||||
}
|
||||
|
||||
func (f *Filtering) handleFilteringAddURL(w http.ResponseWriter, r *http.Request) {
|
||||
fj := filterAddJSON{}
|
||||
err := json.NewDecoder(r.Body).Decode(&fj)
|
||||
if err != nil {
|
||||
httpError(w, http.StatusBadRequest, "Failed to parse request body json: %s", err)
|
||||
return
|
||||
}
|
||||
|
||||
if !IsValidURL(fj.URL) {
|
||||
http.Error(w, "Invalid URL or file path", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
// Check for duplicates
|
||||
if filterExists(fj.URL) {
|
||||
httpError(w, http.StatusBadRequest, "Filter URL already added -- %s", fj.URL)
|
||||
return
|
||||
}
|
||||
|
||||
// Set necessary properties
|
||||
filt := filter{
|
||||
Enabled: true,
|
||||
URL: fj.URL,
|
||||
Name: fj.Name,
|
||||
white: fj.Whitelist,
|
||||
}
|
||||
filt.ID = assignUniqueFilterID()
|
||||
|
||||
// Download the filter contents
|
||||
ok, err := f.update(&filt)
|
||||
if err != nil {
|
||||
httpError(w, http.StatusBadRequest, "Couldn't fetch filter from url %s: %s", filt.URL, err)
|
||||
return
|
||||
}
|
||||
if !ok {
|
||||
httpError(w, http.StatusBadRequest, "Filter at the url %s is invalid (maybe it points to blank page?)", filt.URL)
|
||||
return
|
||||
}
|
||||
|
||||
// URL is deemed valid, append it to filters, update config, write new filter file and tell dns to reload it
|
||||
if !filterAdd(filt) {
|
||||
httpError(w, http.StatusBadRequest, "Filter URL already added -- %s", filt.URL)
|
||||
return
|
||||
}
|
||||
|
||||
onConfigModified()
|
||||
enableFilters(true)
|
||||
|
||||
_, err = fmt.Fprintf(w, "OK %d rules\n", filt.RulesCount)
|
||||
if err != nil {
|
||||
httpError(w, http.StatusInternalServerError, "Couldn't write body: %s", err)
|
||||
}
|
||||
}
|
||||
|
||||
func (f *Filtering) handleFilteringRemoveURL(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
type request struct {
|
||||
URL string `json:"url"`
|
||||
Whitelist bool `json:"whitelist"`
|
||||
}
|
||||
req := request{}
|
||||
err := json.NewDecoder(r.Body).Decode(&req)
|
||||
if err != nil {
|
||||
httpError(w, http.StatusBadRequest, "Failed to parse request body json: %s", err)
|
||||
return
|
||||
}
|
||||
|
||||
// go through each element and delete if url matches
|
||||
config.Lock()
|
||||
newFilters := []filter{}
|
||||
filters := &config.Filters
|
||||
if req.Whitelist {
|
||||
filters = &config.WhitelistFilters
|
||||
}
|
||||
for _, filter := range *filters {
|
||||
if filter.URL != req.URL {
|
||||
newFilters = append(newFilters, filter)
|
||||
} else {
|
||||
err := os.Rename(filter.Path(), filter.Path()+".old")
|
||||
if err != nil {
|
||||
log.Error("os.Rename: %s: %s", filter.Path(), err)
|
||||
}
|
||||
}
|
||||
}
|
||||
// Update the configuration after removing filter files
|
||||
*filters = newFilters
|
||||
config.Unlock()
|
||||
|
||||
onConfigModified()
|
||||
enableFilters(true)
|
||||
|
||||
// Note: the old files "filter.txt.old" aren't deleted - it's not really necessary,
|
||||
// but will require the additional code to run after enableFilters() is finished: i.e. complicated
|
||||
}
|
||||
|
||||
type filterURLJSON struct {
|
||||
Name string `json:"name"`
|
||||
URL string `json:"url"`
|
||||
Enabled bool `json:"enabled"`
|
||||
}
|
||||
|
||||
type filterURLReq struct {
|
||||
URL string `json:"url"`
|
||||
Whitelist bool `json:"whitelist"`
|
||||
Data filterURLJSON `json:"data"`
|
||||
}
|
||||
|
||||
func (f *Filtering) handleFilteringSetURL(w http.ResponseWriter, r *http.Request) {
|
||||
fj := filterURLReq{}
|
||||
err := json.NewDecoder(r.Body).Decode(&fj)
|
||||
if err != nil {
|
||||
httpError(w, http.StatusBadRequest, "json decode: %s", err)
|
||||
return
|
||||
}
|
||||
|
||||
if !IsValidURL(fj.URL) {
|
||||
http.Error(w, "invalid URL or file path", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
filt := filter{
|
||||
Enabled: fj.Data.Enabled,
|
||||
Name: fj.Data.Name,
|
||||
URL: fj.Data.URL,
|
||||
}
|
||||
status := f.filterSetProperties(fj.URL, filt, fj.Whitelist)
|
||||
if (status & statusFound) == 0 {
|
||||
http.Error(w, "URL doesn't exist", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
if (status & statusURLExists) != 0 {
|
||||
http.Error(w, "URL already exists", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
onConfigModified()
|
||||
restart := false
|
||||
if (status & statusEnabledChanged) != 0 {
|
||||
// we must add or remove filter rules
|
||||
restart = true
|
||||
}
|
||||
if (status&statusUpdateRequired) != 0 && fj.Data.Enabled {
|
||||
// download new filter and apply its rules
|
||||
flags := FilterRefreshBlocklists
|
||||
if fj.Whitelist {
|
||||
flags = FilterRefreshAllowlists
|
||||
}
|
||||
nUpdated, _ := f.refreshFilters(flags, true)
|
||||
// if at least 1 filter has been updated, refreshFilters() restarts the filtering automatically
|
||||
// if not - we restart the filtering ourselves
|
||||
restart = false
|
||||
if nUpdated == 0 {
|
||||
restart = true
|
||||
}
|
||||
}
|
||||
if restart {
|
||||
enableFilters(true)
|
||||
}
|
||||
}
|
||||
|
||||
func (f *Filtering) handleFilteringSetRules(w http.ResponseWriter, r *http.Request) {
|
||||
body, err := ioutil.ReadAll(r.Body)
|
||||
if err != nil {
|
||||
httpError(w, http.StatusBadRequest, "Failed to read request body: %s", err)
|
||||
return
|
||||
}
|
||||
|
||||
config.UserRules = strings.Split(string(body), "\n")
|
||||
onConfigModified()
|
||||
enableFilters(true)
|
||||
}
|
||||
|
||||
func (f *Filtering) handleFilteringRefresh(w http.ResponseWriter, r *http.Request) {
|
||||
type Req struct {
|
||||
White bool `json:"whitelist"`
|
||||
}
|
||||
type Resp struct {
|
||||
Updated int `json:"updated"`
|
||||
}
|
||||
resp := Resp{}
|
||||
var err error
|
||||
|
||||
req := Req{}
|
||||
err = json.NewDecoder(r.Body).Decode(&req)
|
||||
if err != nil {
|
||||
httpError(w, http.StatusBadRequest, "json decode: %s", err)
|
||||
return
|
||||
}
|
||||
|
||||
Context.controlLock.Unlock()
|
||||
flags := FilterRefreshBlocklists
|
||||
if req.White {
|
||||
flags = FilterRefreshAllowlists
|
||||
}
|
||||
resp.Updated, err = f.refreshFilters(flags|FilterRefreshForce, false)
|
||||
Context.controlLock.Lock()
|
||||
if err != nil {
|
||||
httpError(w, http.StatusInternalServerError, "%s", err)
|
||||
return
|
||||
}
|
||||
|
||||
js, err := json.Marshal(resp)
|
||||
if err != nil {
|
||||
httpError(w, http.StatusInternalServerError, "json encode: %s", err)
|
||||
return
|
||||
}
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
_, _ = w.Write(js)
|
||||
}
|
||||
|
||||
type filterJSON struct {
|
||||
ID int64 `json:"id"`
|
||||
Enabled bool `json:"enabled"`
|
||||
URL string `json:"url"`
|
||||
Name string `json:"name"`
|
||||
RulesCount uint32 `json:"rules_count"`
|
||||
LastUpdated string `json:"last_updated"`
|
||||
}
|
||||
|
||||
type filteringConfig struct {
|
||||
Enabled bool `json:"enabled"`
|
||||
Interval uint32 `json:"interval"` // in hours
|
||||
Filters []filterJSON `json:"filters"`
|
||||
WhitelistFilters []filterJSON `json:"whitelist_filters"`
|
||||
UserRules []string `json:"user_rules"`
|
||||
}
|
||||
|
||||
func filterToJSON(f filter) filterJSON {
|
||||
fj := filterJSON{
|
||||
ID: f.ID,
|
||||
Enabled: f.Enabled,
|
||||
URL: f.URL,
|
||||
Name: f.Name,
|
||||
RulesCount: uint32(f.RulesCount),
|
||||
}
|
||||
|
||||
if !f.LastUpdated.IsZero() {
|
||||
fj.LastUpdated = f.LastUpdated.Format(time.RFC3339)
|
||||
}
|
||||
|
||||
return fj
|
||||
}
|
||||
|
||||
// Get filtering configuration
|
||||
func (f *Filtering) handleFilteringStatus(w http.ResponseWriter, r *http.Request) {
|
||||
resp := filteringConfig{}
|
||||
config.RLock()
|
||||
resp.Enabled = config.DNS.FilteringEnabled
|
||||
resp.Interval = config.DNS.FiltersUpdateIntervalHours
|
||||
for _, f := range config.Filters {
|
||||
fj := filterToJSON(f)
|
||||
resp.Filters = append(resp.Filters, fj)
|
||||
}
|
||||
for _, f := range config.WhitelistFilters {
|
||||
fj := filterToJSON(f)
|
||||
resp.WhitelistFilters = append(resp.WhitelistFilters, fj)
|
||||
}
|
||||
resp.UserRules = config.UserRules
|
||||
config.RUnlock()
|
||||
|
||||
jsonVal, err := json.Marshal(resp)
|
||||
if err != nil {
|
||||
httpError(w, http.StatusInternalServerError, "json encode: %s", err)
|
||||
return
|
||||
}
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
_, err = w.Write(jsonVal)
|
||||
if err != nil {
|
||||
httpError(w, http.StatusInternalServerError, "http write: %s", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Set filtering configuration
|
||||
func (f *Filtering) handleFilteringConfig(w http.ResponseWriter, r *http.Request) {
|
||||
req := filteringConfig{}
|
||||
err := json.NewDecoder(r.Body).Decode(&req)
|
||||
if err != nil {
|
||||
httpError(w, http.StatusBadRequest, "json decode: %s", err)
|
||||
return
|
||||
}
|
||||
|
||||
if !checkFiltersUpdateIntervalHours(req.Interval) {
|
||||
httpError(w, http.StatusBadRequest, "Unsupported interval")
|
||||
return
|
||||
}
|
||||
|
||||
config.DNS.FilteringEnabled = req.Enabled
|
||||
config.DNS.FiltersUpdateIntervalHours = req.Interval
|
||||
onConfigModified()
|
||||
enableFilters(true)
|
||||
}
|
||||
|
||||
type checkHostResp struct {
|
||||
Reason string `json:"reason"`
|
||||
FilterID int64 `json:"filter_id"`
|
||||
Rule string `json:"rule"`
|
||||
|
||||
// for FilteredBlockedService:
|
||||
SvcName string `json:"service_name"`
|
||||
|
||||
// for ReasonRewrite:
|
||||
CanonName string `json:"cname"` // CNAME value
|
||||
IPList []net.IP `json:"ip_addrs"` // list of IP addresses
|
||||
}
|
||||
|
||||
func (f *Filtering) handleCheckHost(w http.ResponseWriter, r *http.Request) {
|
||||
q := r.URL.Query()
|
||||
host := q.Get("name")
|
||||
|
||||
setts := Context.dnsFilter.GetConfig()
|
||||
setts.FilteringEnabled = true
|
||||
Context.dnsFilter.ApplyBlockedServices(&setts, nil, true)
|
||||
result, err := Context.dnsFilter.CheckHost(host, dns.TypeA, &setts)
|
||||
if err != nil {
|
||||
httpError(w, http.StatusInternalServerError, "couldn't apply filtering: %s: %s", host, err)
|
||||
return
|
||||
}
|
||||
|
||||
resp := checkHostResp{}
|
||||
resp.Reason = result.Reason.String()
|
||||
resp.FilterID = result.FilterID
|
||||
resp.Rule = result.Rule
|
||||
resp.SvcName = result.ServiceName
|
||||
resp.CanonName = result.CanonName
|
||||
resp.IPList = result.IPList
|
||||
js, err := json.Marshal(resp)
|
||||
if err != nil {
|
||||
httpError(w, http.StatusInternalServerError, "json encode: %s", err)
|
||||
return
|
||||
}
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
_, _ = w.Write(js)
|
||||
}
|
||||
|
||||
// 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)
|
||||
}
|
||||
|
||||
func checkFiltersUpdateIntervalHours(i uint32) bool {
|
||||
return i == 0 || i == 1 || i == 12 || i == 1*24 || i == 3*24 || i == 7*24
|
||||
}
|
||||