Compare commits
406 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
89753c4efb | ||
|
|
8e57243275 | ||
|
|
e08c5efd99 | ||
|
|
c17c282901 | ||
|
|
8966383ca3 | ||
|
|
82da886df5 | ||
|
|
afe234759f | ||
|
|
d1f5f781c9 | ||
|
|
f95bea325b | ||
|
|
d8c97cbabe | ||
|
|
c995726f78 | ||
|
|
d2a0d03332 | ||
|
|
69cc597b87 | ||
|
|
15f8cfce64 | ||
|
|
939c902fb0 | ||
|
|
d9a65631b9 | ||
|
|
093bd164d6 | ||
|
|
c500345d16 | ||
|
|
a0482fc201 | ||
|
|
a6c9210461 | ||
|
|
4ae91f0c1b | ||
|
|
903c1da993 | ||
|
|
dcbf083d5b | ||
|
|
1fa250bb35 | ||
|
|
18f210eef5 | ||
|
|
f94c63ed5b | ||
|
|
e4998651fe | ||
|
|
668dcebf13 | ||
|
|
cdd2e8ecb4 | ||
|
|
63f20bc397 | ||
|
|
83544ab0f6 | ||
|
|
2139bb9c79 | ||
|
|
0530f5dff2 | ||
|
|
4e27ad0c8e | ||
|
|
166bc72ff3 | ||
|
|
25f20bd5a7 | ||
|
|
345e4dc89a | ||
|
|
1ae6af44d1 | ||
|
|
3779407291 | ||
|
|
ced5499083 | ||
|
|
5bf38041c5 | ||
|
|
25f469efd7 | ||
|
|
3d3e8e7dbc | ||
|
|
346fa6e921 | ||
|
|
54ee16634c | ||
|
|
3c427ba295 | ||
|
|
a6e4c48567 | ||
|
|
628323761a | ||
|
|
e1276d089b | ||
|
|
d47a23269d | ||
|
|
beab9a1be0 | ||
|
|
82bc5965f4 | ||
|
|
8d209773b3 | ||
|
|
67c8abcb8e | ||
|
|
bd39509458 | ||
|
|
fc7d93b920 | ||
|
|
4a357f1345 | ||
|
|
3693047270 | ||
|
|
92fbbc8cc5 | ||
|
|
914eb612cd | ||
|
|
cc40826299 | ||
|
|
2e879896ff | ||
|
|
451922b858 | ||
|
|
7f018234f6 | ||
|
|
efdd1c1ff2 | ||
|
|
9bc4bf66ed | ||
|
|
a6022fc198 | ||
|
|
d6f560ecaf | ||
|
|
839c2ebdd4 | ||
|
|
9cd7a37646 | ||
|
|
cd75c406c1 | ||
|
|
2449075bca | ||
|
|
4c9a84dda0 | ||
|
|
262e9acc03 | ||
|
|
484c0ceaff | ||
|
|
e399a5fe37 | ||
|
|
19e30dbccc | ||
|
|
49ff0d2b9a | ||
|
|
800002f83d | ||
|
|
73e20d1dd0 | ||
|
|
9bb788ecb5 | ||
|
|
f3fa497af3 | ||
|
|
54bdacdde2 | ||
|
|
0e065a2e61 | ||
|
|
591065aa3a | ||
|
|
760e3596b6 | ||
|
|
21b8b233f8 | ||
|
|
32d4e80c93 | ||
|
|
30f3eb446c | ||
|
|
f711d6558f | ||
|
|
abd1d306dc | ||
|
|
1e1ce606c5 | ||
|
|
abb51ddb8a | ||
|
|
2b2a797cf7 | ||
|
|
c39831abbc | ||
|
|
9173b0ee7a | ||
|
|
c427034e27 | ||
|
|
3cd3b93511 | ||
|
|
41c9a89516 | ||
|
|
9863c1f1ac | ||
|
|
79468ab1bc | ||
|
|
4b821f0bd7 | ||
|
|
54b0f073e8 | ||
|
|
90ed48e9fb | ||
|
|
7a68c3dfc6 | ||
|
|
234ab23557 | ||
|
|
234e29697f | ||
|
|
4590564fea | ||
|
|
bfb7a252ad | ||
|
|
1d12e35dac | ||
|
|
3854a7acf9 | ||
|
|
3be7366ae1 | ||
|
|
e1069f6bd1 | ||
|
|
f8ee8a7907 | ||
|
|
b6bc613c87 | ||
|
|
e466a09e20 | ||
|
|
98bf5322a3 | ||
|
|
b3ae247520 | ||
|
|
b3840b5790 | ||
|
|
0c4646201f | ||
|
|
66b83a5fb5 | ||
|
|
12706d4a97 | ||
|
|
50d2c0a8d3 | ||
|
|
4ad29ee65d | ||
|
|
b2998d77f0 | ||
|
|
a528ed9f94 | ||
|
|
a1bc008190 | ||
|
|
d3a6a86254 | ||
|
|
5437a9d3a6 | ||
|
|
bdfb141d36 | ||
|
|
550dc3b129 | ||
|
|
bacc465ebd | ||
|
|
e606d63525 | ||
|
|
dbde07eea2 | ||
|
|
fc2f01f933 | ||
|
|
a267dbf625 | ||
|
|
c46fcce87d | ||
|
|
af9c47c40a | ||
|
|
59323b2008 | ||
|
|
f0823f1195 | ||
|
|
dca9aebccb | ||
|
|
1ed9faa0c2 | ||
|
|
0d44e3ccdc | ||
|
|
cf9414c107 | ||
|
|
47ce0f3e98 | ||
|
|
2e1acc2bac | ||
|
|
96142b4164 | ||
|
|
1af11c4e45 | ||
|
|
3e2a3afc52 | ||
|
|
40d1b18b28 | ||
|
|
a126a3868c | ||
|
|
c2ba8de206 | ||
|
|
82269bcf33 | ||
|
|
aa691a068a | ||
|
|
bdaea88bf0 | ||
|
|
8c11449d23 | ||
|
|
0e04954673 | ||
|
|
1059669b57 | ||
|
|
c9736ec0fa | ||
|
|
dcbee729fb | ||
|
|
c35f260e53 | ||
|
|
2f61b42e90 | ||
|
|
e67695df8b | ||
|
|
e356540872 | ||
|
|
3d01c3512e | ||
|
|
49567219dc | ||
|
|
3f85625dc6 | ||
|
|
d0d98ba762 | ||
|
|
d2e5692694 | ||
|
|
9d030f38b7 | ||
|
|
5fb603f6c9 | ||
|
|
11e8853a34 | ||
|
|
880ad362a8 | ||
|
|
5192e95a0d | ||
|
|
ac6e0add31 | ||
|
|
7ff89baf45 | ||
|
|
bad88961e9 | ||
|
|
838406353b | ||
|
|
1cdbe3f879 | ||
|
|
47a9c6555e | ||
|
|
35368619b0 | ||
|
|
6ca881ee86 | ||
|
|
1233901822 | ||
|
|
bc11f872fa | ||
|
|
599426a1f9 | ||
|
|
fb2d90832c | ||
|
|
8d13770b0d | ||
|
|
751be05a31 | ||
|
|
33958c5a25 | ||
|
|
1e18235c1d | ||
|
|
4b8ee9ce83 | ||
|
|
5be66e7dc7 | ||
|
|
8cf898e8d9 | ||
|
|
4995c1a1a8 | ||
|
|
557c2268dc | ||
|
|
aa7b99d78c | ||
|
|
e7b6ab4750 | ||
|
|
383f1c2fb3 | ||
|
|
3a74dfdfa4 | ||
|
|
413228e6ec | ||
|
|
c3df81bb8d | ||
|
|
e689c7d940 | ||
|
|
5ae2a32d6e | ||
|
|
a5d1053520 | ||
|
|
e2295c1a11 | ||
|
|
d591ea6264 | ||
|
|
ee8759f063 | ||
|
|
151944bc27 | ||
|
|
a6172d1966 | ||
|
|
90bef94500 | ||
|
|
f5deff63ba | ||
|
|
903b20dcab | ||
|
|
945bd24f67 | ||
|
|
ae9964c445 | ||
|
|
3a5ecb9fc1 | ||
|
|
c499c435c3 | ||
|
|
2113bb5436 | ||
|
|
8503f76747 | ||
|
|
c2be5917ef | ||
|
|
a54984f688 | ||
|
|
5533b434da | ||
|
|
4984c55bce | ||
|
|
9b489c8ddb | ||
|
|
eb5f66ad9e | ||
|
|
75d74a017b | ||
|
|
93c451cb0c | ||
|
|
0545aeff3f | ||
|
|
814005021c | ||
|
|
ca794aed63 | ||
|
|
37f6d38c49 | ||
|
|
165722585f | ||
|
|
7dea729656 | ||
|
|
16b1a343a0 | ||
|
|
a15f21ca1c | ||
|
|
a15c59e24e | ||
|
|
5718f55b9a | ||
|
|
6de0871f2c | ||
|
|
6a90efe957 | ||
|
|
763dcc46e9 | ||
|
|
3109529dbb | ||
|
|
2c84cd6448 | ||
|
|
0440ef016a | ||
|
|
182fa37e5f | ||
|
|
ea1125f57d | ||
|
|
4ecb84f9ad | ||
|
|
a2434d4574 | ||
|
|
3b1faa1365 | ||
|
|
dc1042c3e9 | ||
|
|
a63fe958ae | ||
|
|
0ee112e8a0 | ||
|
|
656d092ad6 | ||
|
|
2244c21b76 | ||
|
|
2c33905a79 | ||
|
|
16fd1359cd | ||
|
|
3a7a80f15f | ||
|
|
5b9a5fff97 | ||
|
|
3f8450337f | ||
|
|
19e76b6938 | ||
|
|
856e26edcf | ||
|
|
51ec58b0ce | ||
|
|
c6eabb5b67 | ||
|
|
1cc1e3749d | ||
|
|
cb97a254a5 | ||
|
|
9e939e5754 | ||
|
|
b72d6f68e6 | ||
|
|
3aac7e7bc9 | ||
|
|
57ade2c3c3 | ||
|
|
7d7360c700 | ||
|
|
8c76e17b1b | ||
|
|
991574f236 | ||
|
|
d7596fe860 | ||
|
|
0c3c8dba9b | ||
|
|
04e9f74435 | ||
|
|
7b7f713880 | ||
|
|
e20bfe9d08 | ||
|
|
c40f7b4d5c | ||
|
|
d7039d9222 | ||
|
|
3282a45978 | ||
|
|
98994916b5 | ||
|
|
f1ae5d78d2 | ||
|
|
2c72035000 | ||
|
|
c7790a8d9f | ||
|
|
c9e10c9de7 | ||
|
|
de7b2d5e6b | ||
|
|
ff86d6b7dc | ||
|
|
3afd8fccc7 | ||
|
|
2cf22898dd | ||
|
|
381b96a4b1 | ||
|
|
a65a40c6be | ||
|
|
da62fac76e | ||
|
|
6a53dd0f00 | ||
|
|
09a39cce03 | ||
|
|
50b188a086 | ||
|
|
dd8396cec1 | ||
|
|
ea320f5ee3 | ||
|
|
afd1fe21f6 | ||
|
|
119d38fa8e | ||
|
|
620212ad37 | ||
|
|
bd0fa4cc4f | ||
|
|
b0549a8e5b | ||
|
|
92399b8ebf | ||
|
|
d8fbb2cd3b | ||
|
|
469b93eaa4 | ||
|
|
92b681cb41 | ||
|
|
1c1b952d48 | ||
|
|
c2a2b3ea6a | ||
|
|
f727f999f9 | ||
|
|
02b28f4511 | ||
|
|
43fcf4117d | ||
|
|
68422b8399 | ||
|
|
c3f6a96f2f | ||
|
|
2c2b951fd6 | ||
|
|
fba70b8b73 | ||
|
|
38cfe95280 | ||
|
|
a76fd7618a | ||
|
|
8d23e29190 | ||
|
|
a185161ad4 | ||
|
|
81c7dbbc16 | ||
|
|
e733c19504 | ||
|
|
0e173d2f70 | ||
|
|
0292d2b32b | ||
|
|
ba56d6c01d | ||
|
|
b8213bf88a | ||
|
|
4548eb8d11 | ||
|
|
a2f06aadc0 | ||
|
|
df12038f33 | ||
|
|
c2aa39efe5 | ||
|
|
5d046c5c16 | ||
|
|
ae50a2f827 | ||
|
|
dcbe3dd405 | ||
|
|
ded02d112c | ||
|
|
076c9de68e | ||
|
|
d237df6389 | ||
|
|
22a5abb7b8 | ||
|
|
828bb40084 | ||
|
|
548010e002 | ||
|
|
5c6aa910ef | ||
|
|
b9999f155e | ||
|
|
3b44efc8e3 | ||
|
|
9258fada47 | ||
|
|
6c70d8ca37 | ||
|
|
5554643cd0 | ||
|
|
7c71d4b445 | ||
|
|
3a92520764 | ||
|
|
aa2e5500e7 | ||
|
|
e2cf9ffd84 | ||
|
|
d8a3ee3676 | ||
|
|
46e447589c | ||
|
|
97161ab4f0 | ||
|
|
3901dda39c | ||
|
|
d49e3769a1 | ||
|
|
c1e16cc584 | ||
|
|
9c1dc6d373 | ||
|
|
fced9178b8 | ||
|
|
d34049b513 | ||
|
|
43dbef8935 | ||
|
|
f6d7d6a37a | ||
|
|
b54f9a7a36 | ||
|
|
9e6ed7f273 | ||
|
|
04faff3e2c | ||
|
|
5fdaf7cb66 | ||
|
|
76f98e2950 | ||
|
|
ba836220b8 | ||
|
|
3189341089 | ||
|
|
4ba8293c06 | ||
|
|
7094ed4f28 | ||
|
|
f623c3d909 | ||
|
|
8198b65f29 | ||
|
|
38b3fe6718 | ||
|
|
9682dc6bc1 | ||
|
|
659b530381 | ||
|
|
1b5748e328 | ||
|
|
ebf2380af4 | ||
|
|
6fc50cd743 | ||
|
|
3b9aaff861 | ||
|
|
c572c7b0e9 | ||
|
|
74275bebdc | ||
|
|
1f0fdef8d6 | ||
|
|
04562dece3 | ||
|
|
c7a5275d42 | ||
|
|
fe397943d6 | ||
|
|
876854d403 | ||
|
|
c143e3d57f | ||
|
|
1102963fa0 | ||
|
|
f2621c4a9a | ||
|
|
859f1590dd | ||
|
|
0ce40fd46e | ||
|
|
33fbccf0ba | ||
|
|
e122d9138b | ||
|
|
606bed9d20 | ||
|
|
3b11648e14 | ||
|
|
e62050fb7e | ||
|
|
fa8bc57082 | ||
|
|
0e99a65687 | ||
|
|
bed92f89f0 | ||
|
|
f12ef5d504 | ||
|
|
379e14c28b | ||
|
|
2ca1a0e586 | ||
|
|
30553c6a9a | ||
|
|
7bf513b638 | ||
|
|
c4fefa10b0 | ||
|
|
3af62e463a | ||
|
|
ad91ba8f43 | ||
|
|
f054dcede4 | ||
|
|
d53f9bafe9 | ||
|
|
0421e1f4f8 |
1
.gitattributes
vendored
Normal file
1
.gitattributes
vendored
Normal file
@@ -0,0 +1 @@
|
||||
client/* linguist-vendored
|
||||
17
.gitignore
vendored
17
.gitignore
vendored
@@ -1,7 +1,20 @@
|
||||
/AdguardDNS
|
||||
/AdguardDNS.yaml
|
||||
.DS_Store
|
||||
.vscode
|
||||
.idea
|
||||
debug
|
||||
/AdGuardHome
|
||||
/AdGuardHome.yaml
|
||||
/data/
|
||||
/build/
|
||||
/client/node_modules/
|
||||
/coredns
|
||||
/Corefile
|
||||
/dnsfilter.txt
|
||||
/querylog.json
|
||||
/querylog.json.1
|
||||
/scripts/translations/node_modules
|
||||
/scripts/translations/oneskyapp.json
|
||||
|
||||
# Test output
|
||||
dnsfilter/dnsfilter.TestLotsOfRules*.pprof
|
||||
tests/top-1m.csv
|
||||
|
||||
26
.travis.yml
Normal file
26
.travis.yml
Normal file
@@ -0,0 +1,26 @@
|
||||
language: go
|
||||
sudo: false
|
||||
go:
|
||||
- 1.11.x
|
||||
- 1.x
|
||||
|
||||
cache:
|
||||
directories:
|
||||
- $HOME/.cache/go-build
|
||||
- $HOME/gopath/pkg/mod
|
||||
- $HOME/Library/Caches/go-build
|
||||
|
||||
os:
|
||||
- linux
|
||||
- osx
|
||||
|
||||
install:
|
||||
- go get -v -d -t ./...
|
||||
- npm --prefix client install
|
||||
- go env
|
||||
|
||||
script:
|
||||
- (cd `go env GOPATH`/src/github.com/prometheus/client_golang && git checkout -q v0.8.0)
|
||||
- go test ./...
|
||||
- make
|
||||
|
||||
48
Dockerfile.arm
Normal file
48
Dockerfile.arm
Normal file
@@ -0,0 +1,48 @@
|
||||
FROM easypi/alpine-arm:latest
|
||||
LABEL maintainer="Erik Rogers <erik.rogers@live.com>"
|
||||
|
||||
# AdGuard version
|
||||
ARG ADGUARD_VERSION="0.91"
|
||||
ENV ADGUARD_VERSION $ADGUARD_VERSION
|
||||
|
||||
# AdGuard architecture and package info
|
||||
ARG ADGUARD_ARCH="linux_arm"
|
||||
ENV ADGUARD_ARCH ${ADGUARD_ARCH}
|
||||
ENV ADGUARD_PACKAGE "AdGuardHome_v${ADGUARD_VERSION}_${ADGUARD_ARCH}"
|
||||
|
||||
# AdGuard release info
|
||||
ARG ADGUARD_ARCHIVE="${ADGUARD_PACKAGE}.tar.gz"
|
||||
ENV ADGUARD_ARCHIVE ${ADGUARD_ARCHIVE}
|
||||
ARG ADGUARD_RELEASE="https://github.com/AdguardTeam/AdGuardHome/releases/download/v${ADGUARD_VERSION}/${ADGUARD_ARCHIVE}"
|
||||
ENV ADGUARD_RELEASE ${ADGUARD_RELEASE}
|
||||
|
||||
# AdGuard directory
|
||||
ARG ADGUARD_DIR="/data/adguard"
|
||||
ENV ADGUARD_DIR ${ADGUARD_DIR}
|
||||
|
||||
# Update CA certs and download AdGuard binaries
|
||||
RUN apk --no-cache --update add ca-certificates \
|
||||
&& cd /tmp \
|
||||
&& wget ${ADGUARD_RELEASE} \
|
||||
&& tar xvf ${ADGUARD_ARCHIVE} \
|
||||
&& mkdir -p "${ADGUARD_DIR}" \
|
||||
&& cp "AdGuardHome/AdGuardHome" "${ADGUARD_DIR}" \
|
||||
&& chmod +x "${ADGUARD_DIR}/AdGuardHome" \
|
||||
&& rm -rf "AdGuardHome" \
|
||||
&& rm ${ADGUARD_ARCHIVE}
|
||||
|
||||
# Expose DNS port 53
|
||||
EXPOSE 53
|
||||
|
||||
# Expose UI port 3000
|
||||
ARG ADGUARD_UI_HOST="0.0.0.0"
|
||||
ENV ADGUARD_UI_HOST ${ADGUARD_UI_HOST}
|
||||
ARG ADGUARD_UI_PORT="3000"
|
||||
ENV ADGUARD_UI_PORT ${ADGUARD_UI_PORT}
|
||||
|
||||
EXPOSE ${ADGUARD_UI_PORT}
|
||||
|
||||
# Run AdGuardHome
|
||||
WORKDIR ${ADGUARD_DIR}
|
||||
VOLUME ${ADGUARD_DIR}
|
||||
ENTRYPOINT ./AdGuardHome --host ${ADGUARD_UI_HOST} --port ${ADGUARD_UI_PORT}
|
||||
48
Dockerfile.linux
Normal file
48
Dockerfile.linux
Normal file
@@ -0,0 +1,48 @@
|
||||
FROM alpine:latest
|
||||
LABEL maintainer="Erik Rogers <erik.rogers@live.com>"
|
||||
|
||||
# AdGuard version
|
||||
ARG ADGUARD_VERSION="0.91"
|
||||
ENV ADGUARD_VERSION $ADGUARD_VERSION
|
||||
|
||||
# AdGuard architecture and package info
|
||||
ARG ADGUARD_ARCH="linux_386"
|
||||
ENV ADGUARD_ARCH ${ADGUARD_ARCH}
|
||||
ENV ADGUARD_PACKAGE "AdGuardHome_v${ADGUARD_VERSION}_${ADGUARD_ARCH}"
|
||||
|
||||
# AdGuard release info
|
||||
ARG ADGUARD_ARCHIVE="${ADGUARD_PACKAGE}.tar.gz"
|
||||
ENV ADGUARD_ARCHIVE ${ADGUARD_ARCHIVE}
|
||||
ARG ADGUARD_RELEASE="https://github.com/AdguardTeam/AdGuardHome/releases/download/v${ADGUARD_VERSION}/${ADGUARD_ARCHIVE}"
|
||||
ENV ADGUARD_RELEASE ${ADGUARD_RELEASE}
|
||||
|
||||
# AdGuard directory
|
||||
ARG ADGUARD_DIR="/data/adguard"
|
||||
ENV ADGUARD_DIR ${ADGUARD_DIR}
|
||||
|
||||
# Update CA certs and download AdGuard binaries
|
||||
RUN apk --no-cache --update add ca-certificates \
|
||||
&& cd /tmp \
|
||||
&& wget ${ADGUARD_RELEASE} \
|
||||
&& tar xvf ${ADGUARD_ARCHIVE} \
|
||||
&& mkdir -p "${ADGUARD_DIR}" \
|
||||
&& cp "AdGuardHome/AdGuardHome" "${ADGUARD_DIR}" \
|
||||
&& chmod +x "${ADGUARD_DIR}/AdGuardHome" \
|
||||
&& rm -rf "AdGuardHome" \
|
||||
&& rm ${ADGUARD_ARCHIVE}
|
||||
|
||||
# Expose DNS port 53
|
||||
EXPOSE 53
|
||||
|
||||
# Expose UI port 3000
|
||||
ARG ADGUARD_UI_HOST="0.0.0.0"
|
||||
ENV ADGUARD_UI_HOST ${ADGUARD_UI_HOST}
|
||||
ARG ADGUARD_UI_PORT="3000"
|
||||
ENV ADGUARD_UI_PORT ${ADGUARD_UI_PORT}
|
||||
|
||||
EXPOSE ${ADGUARD_UI_PORT}
|
||||
|
||||
# Run AdGuardHome
|
||||
WORKDIR ${ADGUARD_DIR}
|
||||
VOLUME ${ADGUARD_DIR}
|
||||
ENTRYPOINT ./AdGuardHome --host ${ADGUARD_UI_HOST} --port ${ADGUARD_UI_PORT}
|
||||
48
Dockerfile.linux64
Normal file
48
Dockerfile.linux64
Normal file
@@ -0,0 +1,48 @@
|
||||
FROM alpine:latest
|
||||
LABEL maintainer="Erik Rogers <erik.rogers@live.com>"
|
||||
|
||||
# AdGuard version
|
||||
ARG ADGUARD_VERSION="0.91"
|
||||
ENV ADGUARD_VERSION $ADGUARD_VERSION
|
||||
|
||||
# AdGuard architecture and package info
|
||||
ARG ADGUARD_ARCH="linux_amd64"
|
||||
ENV ADGUARD_ARCH ${ADGUARD_ARCH}
|
||||
ENV ADGUARD_PACKAGE "AdGuardHome_v${ADGUARD_VERSION}_${ADGUARD_ARCH}"
|
||||
|
||||
# AdGuard release info
|
||||
ARG ADGUARD_ARCHIVE="${ADGUARD_PACKAGE}.tar.gz"
|
||||
ENV ADGUARD_ARCHIVE ${ADGUARD_ARCHIVE}
|
||||
ARG ADGUARD_RELEASE="https://github.com/AdguardTeam/AdGuardHome/releases/download/v${ADGUARD_VERSION}/${ADGUARD_ARCHIVE}"
|
||||
ENV ADGUARD_RELEASE ${ADGUARD_RELEASE}
|
||||
|
||||
# AdGuard directory
|
||||
ARG ADGUARD_DIR="/data/adguard"
|
||||
ENV ADGUARD_DIR ${ADGUARD_DIR}
|
||||
|
||||
# Update CA certs and download AdGuard binaries
|
||||
RUN apk --no-cache --update add ca-certificates \
|
||||
&& cd /tmp \
|
||||
&& wget ${ADGUARD_RELEASE} \
|
||||
&& tar xvf ${ADGUARD_ARCHIVE} \
|
||||
&& mkdir -p "${ADGUARD_DIR}" \
|
||||
&& cp "AdGuardHome/AdGuardHome" "${ADGUARD_DIR}" \
|
||||
&& chmod +x "${ADGUARD_DIR}/AdGuardHome" \
|
||||
&& rm -rf "AdGuardHome" \
|
||||
&& rm ${ADGUARD_ARCHIVE}
|
||||
|
||||
# Expose DNS port 53
|
||||
EXPOSE 53
|
||||
|
||||
# Expose UI port 3000
|
||||
ARG ADGUARD_UI_HOST="0.0.0.0"
|
||||
ENV ADGUARD_UI_HOST ${ADGUARD_UI_HOST}
|
||||
ARG ADGUARD_UI_PORT="3000"
|
||||
ENV ADGUARD_UI_PORT ${ADGUARD_UI_PORT}
|
||||
|
||||
EXPOSE ${ADGUARD_UI_PORT}
|
||||
|
||||
# Run AdGuardHome
|
||||
WORKDIR ${ADGUARD_DIR}
|
||||
VOLUME ${ADGUARD_DIR}
|
||||
ENTRYPOINT ./AdGuardHome --host ${ADGUARD_UI_HOST} --port ${ADGUARD_UI_PORT}
|
||||
41
Makefile
41
Makefile
@@ -1,33 +1,32 @@
|
||||
GIT_VERSION := $(shell git describe --abbrev=4 --dirty --always --tags)
|
||||
GOPATH := $(shell go env GOPATH)
|
||||
NATIVE_GOOS = $(shell unset GOOS; go env GOOS)
|
||||
NATIVE_GOARCH = $(shell unset GOARCH; go env GOARCH)
|
||||
mkfile_path := $(abspath $(lastword $(MAKEFILE_LIST)))
|
||||
mkfile_dir := $(patsubst %/,%,$(dir $(mkfile_path)))
|
||||
STATIC := build/static/bundle.css build/static/bundle.js build/static/index.html
|
||||
GOPATH := $(shell go env GOPATH)
|
||||
JSFILES = $(shell find client -path client/node_modules -prune -o -type f -name '*.js')
|
||||
STATIC = build/static/index.html
|
||||
|
||||
TARGET=AdGuardHome
|
||||
|
||||
.PHONY: all build clean
|
||||
all: build
|
||||
|
||||
build: AdguardDNS coredns
|
||||
build: $(TARGET)
|
||||
|
||||
$(STATIC):
|
||||
yarn --cwd client install
|
||||
yarn --cwd client run build-prod
|
||||
client/node_modules: client/package.json client/package-lock.json
|
||||
npm --prefix client install
|
||||
touch client/node_modules
|
||||
|
||||
AdguardDNS: $(STATIC) *.go
|
||||
echo mkfile_dir = $(mkfile_dir)
|
||||
go get -v -d .
|
||||
GOOS=$(NATIVE_GOOS) GOARCH=$(NATIVE_GOARCH) go get -v github.com/gobuffalo/packr/...
|
||||
PATH=$(GOPATH)/bin:$(PATH) packr build -ldflags="-X main.VersionString=$(GIT_VERSION)" -o AdguardDNS
|
||||
$(STATIC): $(JSFILES) client/node_modules
|
||||
npm --prefix client run build-prod
|
||||
|
||||
coredns: coredns_plugin/*.go dnsfilter/*.go
|
||||
echo mkfile_dir = $(mkfile_dir)
|
||||
go get -v -d github.com/coredns/coredns
|
||||
cd $(GOPATH)/src/github.com/coredns/coredns && grep -q 'dnsfilter:' plugin.cfg || sed -E -i.bak $$'s|^log:log|log:log\\\ndnsfilter:github.com/AdguardTeam/AdguardDNS/coredns_plugin|g' plugin.cfg
|
||||
cd $(GOPATH)/src/github.com/coredns/coredns && GOOS=$(NATIVE_GOOS) GOARCH=$(NATIVE_GOARCH) go generate
|
||||
cd $(GOPATH)/src/github.com/coredns/coredns && go get -v -d .
|
||||
cd $(GOPATH)/src/github.com/coredns/coredns && go build -o $(mkfile_dir)/coredns
|
||||
$(TARGET): $(STATIC) *.go coredns_plugin/*.go dnsfilter/*.go
|
||||
GOPATH=$(GOPATH) GOOS=$(NATIVE_GOOS) GOARCH=$(NATIVE_GOARCH) GO111MODULE=off go get -v github.com/gobuffalo/packr/...
|
||||
GOPATH=$(GOPATH) PATH=$(GOPATH)/bin:$(PATH) packr build -ldflags="-X main.VersionString=$(GIT_VERSION)" -asmflags="-trimpath=$(PWD)" -gcflags="-trimpath=$(PWD)" -o $(TARGET)
|
||||
|
||||
clean:
|
||||
rm -vf coredns AdguardDNS
|
||||
$(MAKE) cleanfast
|
||||
rm -rf build
|
||||
rm -rf client/node_modules
|
||||
|
||||
cleanfast:
|
||||
rm -f $(TARGET)
|
||||
|
||||
198
README.md
198
README.md
@@ -1,66 +1,202 @@
|
||||
# Self-hosted AdGuard DNS
|
||||
|
||||
<p align="center">
|
||||
<img src="https://cdn.adguard.com/public/Adguard/Common/adguard_home.svg" width="300px" alt="AdGuard Home" />
|
||||
</p>
|
||||
<h3 align="center">Privacy protection center for you and your devices</h3>
|
||||
<p align="center">
|
||||
Free and open source, powerful network-wide ads & trackers blocking DNS server.
|
||||
</p>
|
||||
|
||||
AdGuard DNS is an ad-filtering DNS server with built-in phishing protection and optional family-friendly protection.
|
||||
<p align="center">
|
||||
<a href="https://adguard.com/">AdGuard.com</a> |
|
||||
<a href="https://github.com/AdguardTeam/AdGuardHome/wiki">Wiki</a> |
|
||||
<a href="https://reddit.com/r/Adguard">Reddit</a> |
|
||||
<a href="https://twitter.com/AdGuard">Twitter</a>
|
||||
<br /><br />
|
||||
<a href="https://travis-ci.org/AdguardTeam/AdGuardHome">
|
||||
<img src="https://travis-ci.org/AdguardTeam/AdGuardHome.svg" alt="Build status" />
|
||||
</a>
|
||||
<a href="https://github.com/AdguardTeam/AdGuardHome/releases">
|
||||
<img src="https://img.shields.io/github/release/AdguardTeam/AdGuardHome/all.svg" alt="Latest release" />
|
||||
</a>
|
||||
</p>
|
||||
|
||||
This repository describes how to set up and run your self-hosted instance of AdGuard DNS -- it comes with a web dashboard that can be accessed from browser to control the DNS server and change its settings, it also allows you to add your filters in both AdGuard and hosts format.
|
||||
<br />
|
||||
|
||||
If this seems too complicated, you can always use AdGuard DNS servers that provide same functionality — https://adguard.com/en/adguard-dns/overview.html
|
||||
<p align="center">
|
||||
<img src="https://cdn.adguard.com/public/Adguard/Common/adguard_home.gif" width="800" />
|
||||
</p>
|
||||
|
||||
<hr />
|
||||
|
||||
# AdGuard Home
|
||||
|
||||
AdGuard Home is a network-wide software for blocking ads & tracking. After you set it up, it'll cover ALL your home devices, and you don't need any client-side software for that.
|
||||
|
||||
## How does AdGuard Home work?
|
||||
|
||||
AdGuard Home operates as a DNS server that re-routes tracking domains to a "black hole," thus preventing your devices from connecting to those servers. It's based on software we use for our public [AdGuard DNS](https://adguard.com/en/adguard-dns/overview.html) servers -- both share a lot of common code.
|
||||
|
||||
## How is this different from public AdGuard DNS servers?
|
||||
|
||||
Running your own AdGuard Home server allows you to do much more than using a public DNS server.
|
||||
|
||||
* Choose what exactly will the server block or not block;
|
||||
* Monitor your network activity;
|
||||
* Add your own custom filtering rules;
|
||||
|
||||
In the future, AdGuard Home is supposed to become more than just a DNS server.
|
||||
|
||||
## Installation
|
||||
|
||||
Go to https://github.com/AdguardTeam/AdguardDNS/releases and download the binaries for your platform:
|
||||
|
||||
### Mac
|
||||
Download file `AdguardDNS_*_darwin_amd64.tar.gz`, then unpack it and follow [how to run](#How-to-run) instructions below.
|
||||
|
||||
### Linux
|
||||
Download file `AdguardDNS_*_linux_amd64.tar.gz`, then unpack it and follow [how to run](#How-to-run) instructions below.
|
||||
Download this file: [AdGuardHome_v0.91_MacOS.zip](https://github.com/AdguardTeam/AdGuardHome/releases/download/v0.91/AdGuardHome_v0.91_MacOS.zip), then unpack it and follow ["How to run"](#how-to-run) instructions below.
|
||||
|
||||
## How to build your own
|
||||
### Linux 64-bit Intel
|
||||
|
||||
Download this file: [AdGuardHome_v0.91_linux_amd64.tar.gz](https://github.com/AdguardTeam/AdGuardHome/releases/download/v0.91/AdGuardHome_v0.91_linux_amd64.tar.gz), then unpack it and follow ["How to run"](#how-to-run) instructions below.
|
||||
|
||||
### Linux 32-bit Intel
|
||||
|
||||
Download this file: [AdGuardHome_v0.91_linux_386.tar.gz](https://github.com/AdguardTeam/AdGuardHome/releases/download/v0.91/AdGuardHome_v0.91_linux_386.tar.gz), then unpack it and follow ["How to run"](#how-to-run) instructions below.
|
||||
|
||||
### Raspberry Pi (32-bit ARM)
|
||||
|
||||
Download this file: [AdGuardHome_v0.91_linux_arm.tar.gz](https://github.com/AdguardTeam/AdGuardHome/releases/download/v0.91/AdGuardHome_v0.91_linux_arm.tar.gz), then unpack it and follow ["How to run"](#how-to-run) instructions below.
|
||||
|
||||
## How to run
|
||||
|
||||
DNS works on port 53, which requires superuser privileges. Therefore, you need to run it with `sudo` in terminal:
|
||||
|
||||
```bash
|
||||
sudo ./AdGuardHome
|
||||
```
|
||||
|
||||
Now open the browser and navigate to http://localhost:3000/ to control your AdGuard Home service.
|
||||
|
||||
### Running without superuser
|
||||
|
||||
You can run AdGuard Home without superuser privileges, but you need to instruct it to use a different port rather than 53. You can do that by editing `AdGuardHome.yaml` and finding these two lines:
|
||||
|
||||
```yaml
|
||||
coredns:
|
||||
port: 53
|
||||
```
|
||||
|
||||
You can change port 53 to anything above 1024 to avoid requiring superuser privileges.
|
||||
|
||||
If the file does not exist, create it in the same folder, type these two lines down and save.
|
||||
|
||||
### Additional configuration
|
||||
|
||||
Upon the first execution, a file named `AdGuardHome.yaml` will be created, with default values written in it. You can modify the file while your AdGuard Home service is not running. Otherwise, any changes to the file will be lost because the running program will overwrite them.
|
||||
|
||||
Settings are stored in [YAML format](https://en.wikipedia.org/wiki/YAML), possible parameters that you can configure are listed below:
|
||||
|
||||
* `bind_host` — Web interface IP address to listen on
|
||||
* `bind_port` — Web interface IP port to listen on
|
||||
* `auth_name` — Web interface optional authorization username
|
||||
* `auth_pass` — Web interface optional authorization password
|
||||
* `coredns` — CoreDNS configuration section
|
||||
* `port` — DNS server port to listen on
|
||||
* `filtering_enabled` — Filtering of DNS requests based on filter lists
|
||||
* `safebrowsing_enabled` — Filtering of DNS requests based on safebrowsing
|
||||
* `safesearch_enabled` — Enforcing "Safe search" option for search engines, when possible
|
||||
* `parental_enabled` — Parental control-based DNS requests filtering
|
||||
* `parental_sensitivity` — Age group for parental control-based filtering, must be either 3, 10, 13 or 17
|
||||
* `querylog_enabled` — Query logging (also used to calculate top 50 clients, blocked domains and requested domains for statistic purposes)
|
||||
* `bootstrap_dns` — DNS server used for initial hostnames resolution in case if upstream is DoH or DoT with a hostname
|
||||
* `upstream_dns` — List of upstream DNS servers
|
||||
* `filters` — List of filters, each filter has the following values:
|
||||
* `ID` - filter ID (must be unique)
|
||||
* `url` — URL pointing to the filter contents (filtering rules)
|
||||
* `enabled` — Current filter's status (enabled/disabled)
|
||||
* `user_rules` — User-specified filtering rules
|
||||
|
||||
Removing an entry from settings file will reset it to the default value. Deleting the file will reset all settings to the default values.
|
||||
|
||||
## How to build from source
|
||||
|
||||
### Prerequisites
|
||||
|
||||
You will need:
|
||||
* [go](https://golang.org/dl/)
|
||||
* [node.js](https://nodejs.org/en/download/)
|
||||
* [yarn](https://yarnpkg.com/en/docs/install)
|
||||
|
||||
You can either install it from these websites or use [brew.sh](https://brew.sh/) if you're on Mac:
|
||||
* [go](https://golang.org/dl/) v1.11 or later.
|
||||
* [node.js](https://nodejs.org/en/download/)
|
||||
|
||||
You can either install it via the provided links or use [brew.sh](https://brew.sh/) if you're on Mac:
|
||||
|
||||
```bash
|
||||
brew install go node yarn
|
||||
brew install go node
|
||||
```
|
||||
|
||||
### Building
|
||||
|
||||
Open Terminal and execute these commands:
|
||||
|
||||
```bash
|
||||
git clone https://github.com/AdguardTeam/AdguardDNS
|
||||
cd AdguardDNS
|
||||
git clone https://github.com/AdguardTeam/AdGuardHome
|
||||
cd AdGuardHome
|
||||
make
|
||||
```
|
||||
|
||||
## How to run
|
||||
### How to update translations
|
||||
|
||||
DNS works on port 53, which requires superuser privileges. Therefore, you need to run it with sudo:
|
||||
```bash
|
||||
sudo ./AdguardDNS
|
||||
Before updating translations you need to install dependencies:
|
||||
```
|
||||
cd scripts/translations
|
||||
npm install
|
||||
```
|
||||
|
||||
Now open the browser and point it to http://localhost:3000/ to control AdGuard DNS server.
|
||||
Create file `oneskyapp.json` in `scripts/translations` folder.
|
||||
|
||||
## Running without superuser
|
||||
|
||||
You can run it without superuser privileges, but you need to instruct it to use other port rather than 53. You can do that by opening `AdguardDNS.yaml` and adding this line:
|
||||
```yaml
|
||||
coredns:
|
||||
port: 53535
|
||||
Example of `oneskyapp.json`
|
||||
```
|
||||
{
|
||||
"url": "https://platform.api.onesky.io/1/projects/",
|
||||
"projectId": <PROJECT ID>,
|
||||
"apiKey": <API KEY>,
|
||||
"secretKey": <SECRET KEY>
|
||||
}
|
||||
```
|
||||
|
||||
If the file does not exist, create it and put these two lines down.
|
||||
#### Upload translations
|
||||
```
|
||||
node upload.js
|
||||
```
|
||||
|
||||
#### Download translations
|
||||
```
|
||||
node download.js
|
||||
```
|
||||
|
||||
## Contributing
|
||||
|
||||
You are welcome to fork this repository, make your changes and submit a pull request — https://github.com/AdguardTeam/AdguardDNS/pulls
|
||||
You are welcome to fork this repository, make your changes and submit a pull request — https://github.com/AdguardTeam/AdGuardHome/pulls
|
||||
|
||||
If you want to help with AdGuard Home translations, please learn more about translating AdGuard products here: https://kb.adguard.com/en/general/adguard-translations
|
||||
|
||||
Here is a direct link to AdGuard Home project: http://translate.adguard.com/collaboration/project?id=153384
|
||||
|
||||
## Reporting issues
|
||||
|
||||
If you come across any problem, or have a suggestion, head to [this page](https://github.com/AdguardTeam/AdguardDNS/issues) and click on the New issue button.
|
||||
If you run into any problem or have a suggestion, head to [this page](https://github.com/AdguardTeam/AdGuardHome/issues) and click on the `New issue` button.
|
||||
|
||||
## Acknowledgments
|
||||
|
||||
This software wouldn't have been possible without:
|
||||
|
||||
* [Go](https://golang.org/dl/) and it's libraries:
|
||||
* [CoreDNS](https://coredns.io)
|
||||
* [packr](https://github.com/gobuffalo/packr)
|
||||
* [gcache](https://github.com/bluele/gcache)
|
||||
* [miekg's dns](https://github.com/miekg/dns)
|
||||
* [go-yaml](https://github.com/go-yaml/yaml)
|
||||
* [Node.js](https://nodejs.org/) and it's libraries:
|
||||
* [React.js](https://reactjs.org)
|
||||
* [Tabler](https://github.com/tabler/tabler)
|
||||
* And many more node.js packages.
|
||||
* [whotracks.me data](https://github.com/cliqz-oss/whotracks.me)
|
||||
|
||||
For a full list of all node.js packages in use, please take a look at [client/package.json](https://github.com/AdguardTeam/AdGuardHome/blob/master/client/package.json) file.
|
||||
|
||||
228
app.go
228
app.go
@@ -1,6 +1,7 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"fmt"
|
||||
"log"
|
||||
"net"
|
||||
@@ -10,20 +11,31 @@ import (
|
||||
"strconv"
|
||||
|
||||
"github.com/gobuffalo/packr"
|
||||
"golang.org/x/crypto/ssh/terminal"
|
||||
)
|
||||
|
||||
// VersionString will be set through ldflags, contains current version
|
||||
var VersionString = "undefined"
|
||||
|
||||
func main() {
|
||||
log.Printf("AdGuard DNS web interface backend, version %s\n", VersionString)
|
||||
log.Printf("AdGuard Home web interface backend, version %s\n", VersionString)
|
||||
box := packr.NewBox("build/static")
|
||||
{
|
||||
executable, err := os.Executable()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
config.ourBinaryDir = filepath.Dir(executable)
|
||||
|
||||
executableName := filepath.Base(executable)
|
||||
if executableName == "AdGuardHome" {
|
||||
// Binary build
|
||||
config.ourBinaryDir = filepath.Dir(executable)
|
||||
} else {
|
||||
// Most likely we're debugging -- using current working directory in this case
|
||||
workDir, _ := os.Getwd()
|
||||
config.ourBinaryDir = workDir
|
||||
}
|
||||
log.Printf("Current working directory is %s", config.ourBinaryDir)
|
||||
}
|
||||
|
||||
// config can be specified, which reads options from there, but other command line flags have to override config values
|
||||
@@ -96,11 +108,19 @@ func main() {
|
||||
if configFilename != nil {
|
||||
config.ourConfigFilename = *configFilename
|
||||
}
|
||||
// parse from config file
|
||||
err := parseConfig()
|
||||
|
||||
err := askUsernamePasswordIfPossible()
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
// parse from config file
|
||||
err = parseConfig()
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
// override bind host/port from the console
|
||||
if bindHost != nil {
|
||||
config.BindHost = *bindHost
|
||||
}
|
||||
@@ -109,20 +129,212 @@ func main() {
|
||||
}
|
||||
}
|
||||
|
||||
err := writeConfig()
|
||||
// Eat all args so that coredns can start happily
|
||||
if len(os.Args) > 1 {
|
||||
os.Args = os.Args[:1]
|
||||
}
|
||||
|
||||
// Do the upgrade if necessary
|
||||
err := upgradeConfig()
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
// Save the updated config
|
||||
err = writeConfig()
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
// Load filters from the disk
|
||||
for i := range config.Filters {
|
||||
filter := &config.Filters[i]
|
||||
err = filter.load()
|
||||
if err != nil {
|
||||
// This is okay for the first start, the filter will be loaded later
|
||||
log.Printf("Couldn't load filter %d contents due to %s", filter.ID, err)
|
||||
}
|
||||
}
|
||||
|
||||
address := net.JoinHostPort(config.BindHost, strconv.Itoa(config.BindPort))
|
||||
|
||||
runStatsCollectors()
|
||||
runFilterRefreshers()
|
||||
runFiltersUpdatesTimer()
|
||||
|
||||
http.Handle("/", http.FileServer(box))
|
||||
http.Handle("/", optionalAuthHandler(http.FileServer(box)))
|
||||
registerControlHandlers()
|
||||
|
||||
err = startDNSServer()
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
URL := fmt.Sprintf("http://%s", address)
|
||||
log.Println("Go to " + URL)
|
||||
log.Fatal(http.ListenAndServe(address, nil))
|
||||
}
|
||||
|
||||
func getInput() (string, error) {
|
||||
scanner := bufio.NewScanner(os.Stdin)
|
||||
scanner.Scan()
|
||||
text := scanner.Text()
|
||||
err := scanner.Err()
|
||||
return text, err
|
||||
}
|
||||
|
||||
func promptAndGet(prompt string) (string, error) {
|
||||
for {
|
||||
fmt.Printf(prompt)
|
||||
input, err := getInput()
|
||||
if err != nil {
|
||||
log.Printf("Failed to get input, aborting: %s", err)
|
||||
return "", err
|
||||
}
|
||||
if len(input) != 0 {
|
||||
return input, nil
|
||||
}
|
||||
// try again
|
||||
}
|
||||
}
|
||||
|
||||
func promptAndGetPassword(prompt string) (string, error) {
|
||||
for {
|
||||
fmt.Printf(prompt)
|
||||
password, err := terminal.ReadPassword(int(os.Stdin.Fd()))
|
||||
fmt.Printf("\n")
|
||||
if err != nil {
|
||||
log.Printf("Failed to get input, aborting: %s", err)
|
||||
return "", err
|
||||
}
|
||||
if len(password) != 0 {
|
||||
return string(password), nil
|
||||
}
|
||||
// try again
|
||||
}
|
||||
}
|
||||
|
||||
func askUsernamePasswordIfPossible() error {
|
||||
configfile := config.ourConfigFilename
|
||||
if !filepath.IsAbs(configfile) {
|
||||
configfile = filepath.Join(config.ourBinaryDir, config.ourConfigFilename)
|
||||
}
|
||||
_, err := os.Stat(configfile)
|
||||
if !os.IsNotExist(err) {
|
||||
// do nothing, file exists
|
||||
trace("File %s exists, won't ask for password", configfile)
|
||||
return nil
|
||||
}
|
||||
if !terminal.IsTerminal(int(os.Stdin.Fd())) {
|
||||
return nil // do nothing
|
||||
}
|
||||
if !terminal.IsTerminal(int(os.Stdout.Fd())) {
|
||||
return nil // do nothing
|
||||
}
|
||||
fmt.Printf("Would you like to set user/password for the web interface authentication (yes/no)?\n")
|
||||
yesno, err := promptAndGet("Please type 'yes' or 'no': ")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if yesno[0] != 'y' && yesno[0] != 'Y' {
|
||||
return nil
|
||||
}
|
||||
username, err := promptAndGet("Please enter the username: ")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
password, err := promptAndGetPassword("Please enter the password: ")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
password2, err := promptAndGetPassword("Please enter password again: ")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if password2 != password {
|
||||
fmt.Printf("Passwords do not match! Aborting\n")
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
config.AuthName = username
|
||||
config.AuthPass = password
|
||||
return nil
|
||||
}
|
||||
|
||||
// Performs necessary upgrade operations if needed
|
||||
func upgradeConfig() error {
|
||||
|
||||
if config.SchemaVersion == SchemaVersion {
|
||||
// No upgrade, do nothing
|
||||
return nil
|
||||
}
|
||||
|
||||
if config.SchemaVersion > SchemaVersion {
|
||||
// Unexpected -- the config file is newer than we expect
|
||||
return fmt.Errorf("configuration file is supposed to be used with a newer version of AdGuard Home, schema=%d", config.SchemaVersion)
|
||||
}
|
||||
|
||||
// Perform upgrade operations for each consecutive version upgrade
|
||||
for oldVersion, newVersion := config.SchemaVersion, config.SchemaVersion+1; newVersion <= SchemaVersion; {
|
||||
|
||||
err := upgradeConfigSchema(oldVersion, newVersion)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
// Increment old and new versions
|
||||
oldVersion++
|
||||
newVersion++
|
||||
}
|
||||
|
||||
// Save the current schema version
|
||||
config.SchemaVersion = SchemaVersion
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Upgrade from oldVersion to newVersion
|
||||
func upgradeConfigSchema(oldVersion int, newVersion int) error {
|
||||
|
||||
if oldVersion == 0 && newVersion == 1 {
|
||||
log.Printf("Updating schema from %d to %d", oldVersion, newVersion)
|
||||
|
||||
// The first schema upgrade:
|
||||
// Added "ID" field to "filter" -- we need to populate this field now
|
||||
// Added "config.ourDataDir" -- where we will now store filters contents
|
||||
for i := range config.Filters {
|
||||
|
||||
filter := &config.Filters[i] // otherwise we will be operating on a copy
|
||||
|
||||
// Set the filter ID
|
||||
log.Printf("Seting ID=%d for filter %s", NextFilterId, filter.URL)
|
||||
filter.ID = NextFilterId
|
||||
NextFilterId++
|
||||
|
||||
// Forcibly update the filter
|
||||
_, err := filter.update(true)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
// Saving it to the filters dir now
|
||||
err = filter.save()
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
// No more "dnsfilter.txt", filters are now loaded from config.ourDataDir/filters/
|
||||
dnsFilterPath := filepath.Join(config.ourBinaryDir, "dnsfilter.txt")
|
||||
_, err := os.Stat(dnsFilterPath)
|
||||
if !os.IsNotExist(err) {
|
||||
log.Printf("Deleting %s as we don't need it anymore", dnsFilterPath)
|
||||
err = os.Remove(dnsFilterPath)
|
||||
if err != nil {
|
||||
log.Printf("Cannot remove %s due to %s", dnsFilterPath, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
9
client/.eslintrc
vendored
9
client/.eslintrc
vendored
@@ -13,6 +13,13 @@
|
||||
"commonjs": true
|
||||
},
|
||||
|
||||
"settings": {
|
||||
"react": {
|
||||
"pragma": "React",
|
||||
"version": "16.4"
|
||||
}
|
||||
},
|
||||
|
||||
"rules": {
|
||||
"indent": ["error", 4, {
|
||||
"SwitchCase": 1,
|
||||
@@ -43,6 +50,6 @@
|
||||
}],
|
||||
"no-console": ["warn", { "allow": ["warn", "error"] }],
|
||||
"import/no-extraneous-dependencies": ["error", { "devDependencies": true }],
|
||||
"import/prefer-default-export": "off",
|
||||
"import/prefer-default-export": "off"
|
||||
}
|
||||
}
|
||||
|
||||
16687
client/package-lock.json
generated
vendored
Normal file
16687
client/package-lock.json
generated
vendored
Normal file
File diff suppressed because it is too large
Load Diff
16
client/package.json
vendored
16
client/package.json
vendored
@@ -3,31 +3,36 @@
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"build-dev": "NODE_ENV=development webpack --config webpack.dev.js",
|
||||
"watch": "NODE_ENV=development webpack --config webpack.dev.js --watch",
|
||||
"build-prod": "NODE_ENV=production webpack --config webpack.prod.js",
|
||||
"build-dev": "NODE_ENV=development ./node_modules/.bin/webpack --config webpack.dev.js",
|
||||
"watch": "NODE_ENV=development ./node_modules/.bin/webpack --config webpack.dev.js --watch",
|
||||
"build-prod": "NODE_ENV=production ./node_modules/.bin/webpack --config webpack.prod.js",
|
||||
"lint": "eslint frontend/"
|
||||
},
|
||||
"dependencies": {
|
||||
"@nivo/line": "^0.42.1",
|
||||
"@nivo/line": "^0.49.1",
|
||||
"axios": "^0.18.0",
|
||||
"classnames": "^2.2.6",
|
||||
"date-fns": "^1.29.0",
|
||||
"file-saver": "^1.3.8",
|
||||
"i18next": "^12.0.0",
|
||||
"i18next-browser-languagedetector": "^2.2.3",
|
||||
"lodash": "^4.17.10",
|
||||
"nanoid": "^1.2.3",
|
||||
"prop-types": "^15.6.1",
|
||||
"react": "^16.4.0",
|
||||
"react-click-outside": "^3.0.1",
|
||||
"react-dom": "^16.4.0",
|
||||
"react-i18next": "^8.2.0",
|
||||
"react-modal": "^3.4.5",
|
||||
"react-redux": "^5.0.7",
|
||||
"react-redux-loading-bar": "^4.0.7",
|
||||
"react-router-dom": "^4.2.2",
|
||||
"react-table": "^6.8.6",
|
||||
"react-transition-group": "^2.4.0",
|
||||
"redux": "^4.0.0",
|
||||
"redux-actions": "^2.4.0",
|
||||
"redux-thunk": "^2.3.0",
|
||||
"svg-url-loader": "^2.3.2",
|
||||
"tabler-react": "^1.10.0",
|
||||
"whatwg-fetch": "2.0.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
@@ -41,6 +46,7 @@
|
||||
"babel-preset-react": "^6.24.1",
|
||||
"babel-preset-stage-2": "^6.24.1",
|
||||
"babel-runtime": "6.26.0",
|
||||
"clean-webpack-plugin": "^0.1.19",
|
||||
"compression-webpack-plugin": "^1.1.11",
|
||||
"css-loader": "^0.28.11",
|
||||
"eslint": "^4.19.1",
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
|
||||
<meta name="theme-color" content="#000000">
|
||||
<link rel="shortcut icon" href="https://adguard.com/img/favicons/favicon.ico">
|
||||
<title>AdGuard DNS</title>
|
||||
<title>AdGuard Home</title>
|
||||
</head>
|
||||
<body>
|
||||
<noscript>
|
||||
|
||||
128
client/src/__locales/en.json
Normal file
128
client/src/__locales/en.json
Normal file
@@ -0,0 +1,128 @@
|
||||
{
|
||||
"back": "Back",
|
||||
"dashboard": "Dashboard",
|
||||
"settings": "Settings",
|
||||
"filters": "Filters",
|
||||
"query_log": "Query Log",
|
||||
"faq": "FAQ",
|
||||
"version": "version",
|
||||
"address": "address",
|
||||
"on": "ON",
|
||||
"off": "OFF",
|
||||
"copyright": "Copyright",
|
||||
"homepage": "Homepage",
|
||||
"report_an_issue": "Report an issue",
|
||||
"enable_protection": "Enable protection",
|
||||
"enabled_protection": "Enabled protection",
|
||||
"disable_protection": "Disable protection",
|
||||
"disabled_protection": "Disabled protection",
|
||||
"refresh_statics": "Refresh statistics",
|
||||
"dns_query": "DNS Queries",
|
||||
"blocked_by": "Blocked by",
|
||||
"stats_malware_phishing": "Blocked malware\/phishing",
|
||||
"stats_adult": "Blocked adult websites",
|
||||
"stats_query_domain": "Top queried domains",
|
||||
"for_last_24_hours": "for the last 24 hours",
|
||||
"no_domains_found": "No domains found",
|
||||
"requests_count": "Requests count",
|
||||
"top_blocked_domains": "Top blocked domains",
|
||||
"top_clients": "Top clients",
|
||||
"no_clients_found": "No clients found",
|
||||
"general_statistics": "General statistics",
|
||||
"number_of_dns_query_24_hours": "A number of DNS quieries processed for the last 24 hours",
|
||||
"number_of_dns_query_blocked_24_hours": "A number of DNS requests blocked by adblock filters and hosts blocklists",
|
||||
"number_of_dns_query_blocked_24_hours_by_sec": "A number of DNS requests blocked by the AdGuard browsing security module",
|
||||
"number_of_dns_query_blocked_24_hours_adult": "A number of adult websites blocked",
|
||||
"enforced_save_search": "Enforced safe search",
|
||||
"number_of_dns_query_to_safe_search": "A number of DNS requests to search engines for which Safe Search was enforced",
|
||||
"average_processing_time": "Average processing time",
|
||||
"average_processing_time_hint": "Average time in milliseconds on processing a DNS request",
|
||||
"block_domain_use_filters_and_hosts": "Block domains using filters and hosts files",
|
||||
"filters_block_toggle_hint": "You can setup blocking rules in the <a href='#filters'>Filters<\/a> settings.",
|
||||
"use_adguard_browsing_sec": "Use AdGuard browsing security web service",
|
||||
"use_adguard_browsing_sec_hint": "AdGuard Home will check if domain is blacklisted by the browsing security web service. It will use privacy-friendly lookup API to perform the check: only a short prefix of the domain name SHA256 hash is sent to the server.",
|
||||
"use_adguard_parental": "Use AdGuard parental control web service",
|
||||
"use_adguard_parental_hint": "AdGuard Home will check if domain contains adult materials. It uses the same privacy-friendly API as the browsing security web service.",
|
||||
"enforce_safe_search": "Enforce safe search",
|
||||
"enforce_save_search_hint": "AdGuard Home can enforce safe search in the following search engines: Google, Youtube, Bing, and Yandex.",
|
||||
"no_servers_specified": "No servers specified",
|
||||
"no_settings": "No settings",
|
||||
"general_settings": "General settings",
|
||||
"upstream_dns": "Upstream DNS servers",
|
||||
"upstream_dns_hint": "If you keep this field empty, AdGuard Home will use <a href='https:\/\/1.1.1.1\/' target='_blank'>Cloudflare DNS<\/a> as an upstream. Use tls:\/\/ prefix for DNS over TLS servers.",
|
||||
"test_upstream_btn": "Test upstreams",
|
||||
"apply_btn": "Apply",
|
||||
"disabled_filtering_toast": "Disabled filtering",
|
||||
"enabled_filtering_toast": "Enabled filtering",
|
||||
"disabled_safe_browsing_toast": "Disabled safebrowsing",
|
||||
"enabled_safe_browsing_toast": "Enabled safebrowsing",
|
||||
"disabled_parental_toast": "Disabled parental control",
|
||||
"enabled_parental_toast": "Enabled parental control",
|
||||
"disabled_safe_search_toast": "Disabled safe search",
|
||||
"enabled_save_search_toast": "Enabled safe search",
|
||||
"enabled_table_header": "Enabled",
|
||||
"name_table_header": "Name",
|
||||
"filter_url_table_header": "Filter URL",
|
||||
"rules_count_table_header": "Rules count",
|
||||
"last_time_updated_table_header": "Last time updated",
|
||||
"actions_table_header": "Actions",
|
||||
"delete_table_action": "Delete",
|
||||
"filters_and_hosts": "Filters and hosts blocklists",
|
||||
"filters_and_hosts_hint": "AdGuard Home understands basic adblock rules and hosts files syntax.",
|
||||
"no_filters_added": "No filters added",
|
||||
"add_filter_btn": "Add filter",
|
||||
"cancel_btn": "Cancel",
|
||||
"enter_name_hint": "Enter name",
|
||||
"enter_url_hint": "Enter URL",
|
||||
"check_updates_btn": "Check updates",
|
||||
"new_filter_btn": "New filter subscription",
|
||||
"enter_valid_filter_url": "Enter a valid URL to a filter subscription or a hosts file.",
|
||||
"custom_filter_rules": "Custom filtering rules",
|
||||
"custom_filter_rules_hint": "Enter one rule on a line. You can use either adblock rules or hosts files syntax.",
|
||||
"examples_title": "Examples",
|
||||
"example_meaning_filter_block": "block access to the example.org domain and all its subdomains",
|
||||
"example_meaning_filter_whitelist": "unblock access to the example.org domain and all its subdomains",
|
||||
"example_meaning_host_block": "AdGuard Home will now return 127.0.0.1 address for the example.org domain (but not its subdomains).",
|
||||
"example_comment": "! Here goes a comment",
|
||||
"example_comment_meaning": "just a comment",
|
||||
"example_comment_hash": "# Also a comment",
|
||||
"example_upstream_regular": "regular DNS (over UDP)",
|
||||
"example_upstream_dot": "encrypted <a href='https:\/\/en.wikipedia.org\/wiki\/DNS_over_TLS' target='_blank'>DNS-over-TLS<\/a>",
|
||||
"example_upstream_doh": "encrypted <a href='https:\/\/en.wikipedia.org\/wiki\/DNS_over_HTTPS' target='_blank'>DNS-over-HTTPS<\/a>",
|
||||
"example_upstream_tcp": "regular DNS (over TCP)",
|
||||
"all_filters_up_to_date_toast": "All filters are already up-to-date",
|
||||
"updated_upstream_dns_toast": "Updated the upstream DNS servers",
|
||||
"dns_test_ok_toast": "Specified DNS servers are working correctly",
|
||||
"dns_test_not_ok_toast": "Server \"{{key}}\": could not be used, please check that you've written it correctly",
|
||||
"unblock_btn": "Unblock",
|
||||
"block_btn": "Block",
|
||||
"time_table_header": "Time",
|
||||
"domain_name_table_header": "Domain name",
|
||||
"type_table_header": "Type",
|
||||
"response_table_header": "Response",
|
||||
"empty_response_status": "Empty",
|
||||
"show_all_filter_type": "Show all",
|
||||
"show_filtered_type": "Show filtered",
|
||||
"no_logs_found": "No logs found",
|
||||
"disabled_log_btn": "Disable log",
|
||||
"download_log_file_btn": "Download log file",
|
||||
"refresh_btn": "Refresh",
|
||||
"enabled_log_btn": "Enable log",
|
||||
"last_dns_queries": "Last 5000 DNS queries",
|
||||
"previous_btn": "Previous",
|
||||
"next_btn": "Next",
|
||||
"loading_table_status": "Loading...",
|
||||
"page_table_footer_text": "Page",
|
||||
"of_table_footer_text": "of",
|
||||
"rows_table_footer_text": "rows",
|
||||
"updated_custom_filtering_toast": "Updated the custom filtering rules",
|
||||
"rule_removed_from_custom_filtering_toast": "Rule removed from the custom filtering rules",
|
||||
"rule_added_to_custom_filtering_toast": "Rule added to the custom filtering rules",
|
||||
"query_log_disabled_toast": "Query log disabled",
|
||||
"query_log_enabled_toast": "Query log enabled",
|
||||
"source_label": "Source",
|
||||
"found_in_known_domain_db": "Found in the known domains database.",
|
||||
"category_label": "Category",
|
||||
"rule_label": "Rule",
|
||||
"filter_label": "Filter"
|
||||
}
|
||||
124
client/src/__locales/es.json
Normal file
124
client/src/__locales/es.json
Normal file
@@ -0,0 +1,124 @@
|
||||
{
|
||||
"back": "Atr\u00e1s",
|
||||
"dashboard": "Tablero de rendimiento",
|
||||
"settings": "Ajustes",
|
||||
"filters": "Filtros",
|
||||
"query_log": "Log de consulta",
|
||||
"faq": "FAQ",
|
||||
"version": "versi\u00f3n",
|
||||
"address": "direcci\u00f3n",
|
||||
"on": "Activado",
|
||||
"off": "Desactivado",
|
||||
"copyright": "Copyright",
|
||||
"homepage": "P\u00e1gina de inicio",
|
||||
"report_an_issue": "Reportar el error",
|
||||
"enable_protection": "Activar la protecci\u00f3n",
|
||||
"enabled_protection": "Protecci\u00f3n activada",
|
||||
"disable_protection": "Desactivar protecci\u00f3n",
|
||||
"disabled_protection": "Protecci\u00f3n desactivada",
|
||||
"refresh_statics": "Renovas estad\u00edsticas",
|
||||
"dns_query": "DNS Queries",
|
||||
"blocked_by": "Bloqueado por",
|
||||
"stats_malware_phishing": "Malware\/phishing bloqueado",
|
||||
"stats_adult": "Contenido adulto bloqueado",
|
||||
"stats_query_domain": "Dominios m\u00e1s solicitados",
|
||||
"for_last_24_hours": "en las \u00faltimas 24 horas",
|
||||
"no_domains_found": "No dominios encontrados",
|
||||
"requests_count": "N\u00famero de solicitudes",
|
||||
"top_blocked_domains": "Dominios m\u00e1s bloqueados",
|
||||
"top_clients": "Clientes m\u00e1s populares",
|
||||
"no_clients_found": "No hay clientes",
|
||||
"general_statistics": "Estad\u00edstica general",
|
||||
"number_of_dns_query_24_hours": "Una serie de consultas DNS procesadas durante las \u00faltimas 24 horas",
|
||||
"number_of_dns_query_blocked_24_hours": "El n\u00famero de solicitudes de DNS bloqueadas por los filtros y listas de block",
|
||||
"number_of_dns_query_blocked_24_hours_by_sec": "El n\u00famero de solicitudes de DNS bloqueadas por el m\u00f3dulo de navegaci\u00f3n segura de AdGuard",
|
||||
"number_of_dns_query_blocked_24_hours_adult": "El n\u00famero de sitios para adultos bloqueados",
|
||||
"enforced_save_search": "B\u00fasqueda segura forzada",
|
||||
"number_of_dns_query_to_safe_search": "Una serie de solicitudes de DNS a los motores de b\u00fasqueda para los que se aplic\u00f3 la B\u00fasqueda Segura",
|
||||
"average_processing_time": "Tiempo medio de tratamiento",
|
||||
"average_processing_time_hint": "Tiempo medio en milisegundos al procesar una solicitud DNS",
|
||||
"block_domain_use_filters_and_hosts": "Bloquear dominios usando filtros y archivos hosts",
|
||||
"filters_block_toggle_hint": "Puede configurar las reglas de bloqueo en los ajustes <a href='#filters'>Filtros<\/a>.",
|
||||
"use_adguard_browsing_sec": "Usar el servicio web de Seguridad de navegaci\u00f3n de AdGuard",
|
||||
"use_adguard_browsing_sec_hint": "AdGuard Home comprobar\u00e1 si el dominio est\u00e1 en la lista negra del servicio web de seguridad de navegaci\u00f3n. Utilizar\u00e1 la API de b\u00fasqueda de privacidad para realizar la comprobaci\u00f3n: s\u00f3lo se env\u00eda al servidor un prefijo corto del hash del nombre de dominio SHA256.",
|
||||
"use_adguard_parental": "Usar Control Parental de AdGuard ",
|
||||
"use_adguard_parental_hint": "AdGuard Home comprobar\u00e1 si el dominio contiene materiales para adultos. Utiliza la misma API de privacidad que el servicio web de seguridad de navegaci\u00f3n.",
|
||||
"enforce_safe_search": "Enforzar b\u00fasqueda segura",
|
||||
"enforce_save_search_hint": "AdGuard Home puede hacer cumplir la b\u00fasqueda segura en los siguientes motores de b\u00fasqueda: Google, Youtube, Bing y Yandex.",
|
||||
"no_servers_specified": "No hay servidores especificados",
|
||||
"no_settings": "No hay ajustes",
|
||||
"general_settings": "Ajustes generales",
|
||||
"upstream_dns": "Servidores DNS upstream",
|
||||
"upstream_dns_hint": "Si mantiene este campo vac\u00edo, AdGuard Home utilizar\u00e1 <a href='https:\/\/1.1.1.1\/' target='_blank'>Cloudflare DNS<\/a> como upstream. Utilice el prefijo tls:\/\/ para DNS sobre servidores TLS.",
|
||||
"test_upstream_btn": "Probar upstream",
|
||||
"apply_btn": "Aplicar",
|
||||
"disabled_filtering_toast": "Desactivar filtrado",
|
||||
"enabled_filtering_toast": "Filtrado activado",
|
||||
"disabled_safe_browsing_toast": "Navegaci\u00f3n segura desactivada",
|
||||
"enabled_safe_browsing_toast": "Navegaci\u00f3n segura activada",
|
||||
"disabled_parental_toast": "Control parental desactivado",
|
||||
"enabled_parental_toast": "Control parental activado",
|
||||
"disabled_safe_search_toast": "B\u00fasqueda segura desactivada",
|
||||
"enabled_save_search_toast": "B\u00fasqueda segura activada",
|
||||
"enabled_table_header": "Activado",
|
||||
"name_table_header": "Nombre",
|
||||
"filter_url_table_header": "Filtro URL",
|
||||
"rules_count_table_header": "N\u00famero de reglas",
|
||||
"last_time_updated_table_header": "\u00daltima actualizaci\u00f3n",
|
||||
"actions_table_header": "Acciones",
|
||||
"delete_table_action": "Eliminar",
|
||||
"filters_and_hosts": "Filtros y hosts blocklists",
|
||||
"filters_and_hosts_hint": "AdGuard Home entiende reglas b\u00e1sicas de bloqueo y la sintaxis de los archivos de hosts.",
|
||||
"no_filters_added": "No hay filtros agregados",
|
||||
"add_filter_btn": "Agregar filtro",
|
||||
"cancel_btn": "Cancelar",
|
||||
"enter_name_hint": "Ingresar nombre",
|
||||
"enter_url_hint": "Ingresar URL",
|
||||
"check_updates_btn": "Revisar si hay actualizaciones",
|
||||
"new_filter_btn": "Nueva suscripci\u00f3n al filtro",
|
||||
"enter_valid_filter_url": "Ingrese el URL v\u00e1lido para suscribirse o un archivo hosts.",
|
||||
"custom_filter_rules": "Personalizar reglas del filtrado",
|
||||
"custom_filter_rules_hint": "Introduzca una regla en una l\u00ednea. Puede utilizar reglas de bloqueo de anuncios o sintaxis de archivos de hosts.",
|
||||
"examples_title": "Ejemplos",
|
||||
"example_meaning_filter_block": "bloquear acceso para el dominio ejemplo.org\ny todos sus subdominios ",
|
||||
"example_meaning_filter_whitelist": "desbloquear el acceso para el dominio ejemplo.org y sus subdominios",
|
||||
"example_meaning_host_block": "AdGuard Home regresar\u00e1 la direcci\u00f3n 127.0.0.1 para el dominio ejemplo.org (pero no para sus subdominios).",
|
||||
"example_comment": "! Aqu\u00ed va el comentario",
|
||||
"example_comment_meaning": "solo un comentario",
|
||||
"example_comment_hash": "# Tambi\u00e9n un comentario",
|
||||
"all_filters_up_to_date_toast": "Todos los filtros son actualizados",
|
||||
"updated_upstream_dns_toast": "Servidores DNS upstream son actualizados",
|
||||
"dns_test_ok_toast": "Servidores DNS especificados funcionan correctamente",
|
||||
"dns_test_not_ok_toast": "Servidor \"{{key}}\": no puede ser usado, por favor, revise si lo ha escrito correctamente",
|
||||
"unblock_btn": "Desbloquear",
|
||||
"block_btn": "Bloquear",
|
||||
"time_table_header": "Tiempo",
|
||||
"domain_name_table_header": "Nombre de dominio",
|
||||
"type_table_header": "Tipo",
|
||||
"response_table_header": "Respuesta",
|
||||
"empty_response_status": "Vac\u00edo",
|
||||
"show_all_filter_type": "Mostrar todo",
|
||||
"show_filtered_type": "Mostrar filtrados",
|
||||
"no_logs_found": "No se han encontrado registros",
|
||||
"disabled_log_btn": "Desactivar registro",
|
||||
"download_log_file_btn": "Descargar el archivo de registro",
|
||||
"refresh_btn": "Renovar",
|
||||
"enabled_log_btn": "Activar registro",
|
||||
"last_dns_queries": "\u00daltimas 500 solicitudes de DNS",
|
||||
"previous_btn": "Anterior",
|
||||
"next_btn": "Siguiente",
|
||||
"loading_table_status": "Cargando...",
|
||||
"page_table_footer_text": "P\u00e1gina",
|
||||
"of_table_footer_text": "de",
|
||||
"rows_table_footer_text": "filas",
|
||||
"updated_custom_filtering_toast": "Actualizadas las reglas de filtrado personalizadas",
|
||||
"rule_removed_from_custom_filtering_toast": "Regla eliminada de las reglas de filtrado personalizadas",
|
||||
"rule_added_to_custom_filtering_toast": "Regla a\u00f1adida a las reglas de filtrado personalizadas",
|
||||
"query_log_disabled_toast": "Log de consulta desactivado",
|
||||
"query_log_enabled_toast": "Log de consulta activado",
|
||||
"source_label": "Fuente",
|
||||
"found_in_known_domain_db": "Encontrado en la base de datos de dominios conocidos.",
|
||||
"category_label": "Categor\u00eda",
|
||||
"rule_label": "Regla",
|
||||
"filter_label": "Filtro"
|
||||
}
|
||||
124
client/src/__locales/fr.json
Normal file
124
client/src/__locales/fr.json
Normal file
@@ -0,0 +1,124 @@
|
||||
{
|
||||
"back": "Retour",
|
||||
"dashboard": "Tableau de bord",
|
||||
"settings": "Param\u00e8tres",
|
||||
"filters": "Filtres",
|
||||
"query_log": "Journal des requ\u00eates\u001c",
|
||||
"faq": "FAQ",
|
||||
"version": "version",
|
||||
"address": "addresse",
|
||||
"on": "Activ\u00e9",
|
||||
"off": "\u00c9teint",
|
||||
"copyright": "Copyright",
|
||||
"homepage": "Page d'accueil",
|
||||
"report_an_issue": "Signaler un probl\u00e8me",
|
||||
"enable_protection": "Activer la protection",
|
||||
"enabled_protection": "Protection activ\u00e9e",
|
||||
"disable_protection": "D\u00e9sactiver la protection",
|
||||
"disabled_protection": "Protection d\u00e9sactiv\u00e9e",
|
||||
"refresh_statics": "Renouveler les statistiques",
|
||||
"dns_query": "Requ\u00eates\u001c DNS",
|
||||
"blocked_by": "Bloqu\u00e9 par",
|
||||
"stats_malware_phishing": "Tentative de malware\/hamme\u00e7onnage bloqu\u00e9e",
|
||||
"stats_adult": "Sites \u00e0 contenu adulte bloqu\u00e9s",
|
||||
"stats_query_domain": "Domaines les plus recherch\u00e9s",
|
||||
"for_last_24_hours": "pendant les derni\u00e8res 24 heures",
|
||||
"no_domains_found": "Pas de domaines trouv\u00e9s",
|
||||
"requests_count": "Nombre de requ\u00eates",
|
||||
"top_blocked_domains": "Les domaines les plus fr\u00e9quemment bloqu\u00e9s",
|
||||
"top_clients": "Meilleurs clients",
|
||||
"no_clients_found": "Pas de clients trouv\u00e9s",
|
||||
"general_statistics": "Statistiques g\u00e9n\u00e9rales",
|
||||
"number_of_dns_query_24_hours": "Un nombre de requ\u00eates DNS quieries trait\u00e9es pendant les 24 heures derni\u00e8res",
|
||||
"number_of_dns_query_blocked_24_hours": "Un nombre de requ\u00eates DNS bloqu\u00e9es par les filtres adblock et les listes de blocage des hosts",
|
||||
"number_of_dns_query_blocked_24_hours_by_sec": "Un nombre de requ\u00eates DNS bloqu\u00e9es par le module S\u00e9curit\u00e9 de navigation d'AdGuard",
|
||||
"number_of_dns_query_blocked_24_hours_adult": "Un nombre de sites \u00e0 contenu adulte bloqu\u00e9s",
|
||||
"enforced_save_search": "Recherche s\u00e9curis\u00e9e renforc\u00e9e",
|
||||
"number_of_dns_query_to_safe_search": "Un nombre de requ\u00eates DNS faites avec la Recherche securis\u00e9e",
|
||||
"average_processing_time": "Temps moyen de traitement",
|
||||
"average_processing_time_hint": "Temps moyen (en millisecondes) de traitement d'une requ\u00eate DNS",
|
||||
"block_domain_use_filters_and_hosts": "Bloquez les domaines \u00e0 l'aide des filtres et fichiers hosts",
|
||||
"filters_block_toggle_hint": "Vous pouvez configurer les r\u00e8gles de filtrage dans les param\u00e8tres des <a href='#filters'>Filtres<\/a>.",
|
||||
"use_adguard_browsing_sec": "Utilisez le service S\u00e9curit\u00e9 de navigation d'AdGuard",
|
||||
"use_adguard_browsing_sec_hint": "AdGuard Home va v\u00e9rifier si le domaine est dans la liste noire du service de s\u00e9curit\u00e9 de navigation. Pour cela il va utiliser un lookup API discret : le pr\u00e9fixe court du hash du nom de domaine SHA256 sera envoy\u00e9 au serveur.",
|
||||
"use_adguard_parental": "Utiliser le contr\u00f4le parental d'AdGuard",
|
||||
"use_adguard_parental_hint": "AdGuard Home va v\u00e9rifier s'il y a du contenu pour adultes sur le domaine. Ce sera fait par aide du m\u00eame API discret que celui utilis\u00e9 par le service de S\u00e9curit\u00e9 de navigation.",
|
||||
"enforce_safe_search": "Renforcer la recherche s\u00e9curis\u00e9e",
|
||||
"enforce_save_search_hint": "AdGuard Home peut renforcer la Recherche s\u00e9curis\u00e9e dans les moteurs de recherche suivants : Google, Youtube, Bing et Yandex.",
|
||||
"no_servers_specified": "Pas de serveurs sp\u00e9cifi\u00e9s",
|
||||
"no_settings": "Pas de param\u00e8tres",
|
||||
"general_settings": "Param\u00e8tres g\u00e9n\u00e9raux",
|
||||
"upstream_dns": "Serveurs DNS upstream",
|
||||
"upstream_dns_hint": "Si vous laisses ce champ vide, AdGuard Home va utiliser <a href='https:\/\/1.1.1.1\/' target='_blank'>Cloudflare DNS<\/a> somme upstream. Utilisez le pr\u00e9fixe tls:\/\/ pour DNS via les serveurs TLS .",
|
||||
"test_upstream_btn": "Tester les upstreams",
|
||||
"apply_btn": "Appliquer",
|
||||
"disabled_filtering_toast": "Filtrage d\u00e9sactiv\u00e9",
|
||||
"enabled_filtering_toast": "Filtrage activ\u00e9",
|
||||
"disabled_safe_browsing_toast": "Surfing s\u00e9curis\u00e9 d\u00e9sactiv\u00e9",
|
||||
"enabled_safe_browsing_toast": "Surfing s\u00e9curis\u00e9 activ\u00e9",
|
||||
"disabled_parental_toast": "Contr\u00f4le parental d\u00e9sactiv\u00e9",
|
||||
"enabled_parental_toast": "Contr\u00f4le parental activ\u00e9",
|
||||
"disabled_safe_search_toast": "Recherche s\u00e9curis\u00e9e d\u00e9sactiv\u00e9e",
|
||||
"enabled_save_search_toast": "Recherche s\u00e9curis\u00e9e activ\u00e9e",
|
||||
"enabled_table_header": "Activ\u00e9",
|
||||
"name_table_header": "Nom",
|
||||
"filter_url_table_header": "URL du filtre",
|
||||
"rules_count_table_header": "Nombre des r\u00e8gles",
|
||||
"last_time_updated_table_header": "Derni\u00e8re mise \u00e0 jour",
|
||||
"actions_table_header": "Actions",
|
||||
"delete_table_action": "Supprimer",
|
||||
"filters_and_hosts": "Listes de blocage des filtres et hosts",
|
||||
"filters_and_hosts_hint": "AdGuard Home comprend les r\u00e8gles basiques de blocage ainsi que la syntaxe des fichiers hosts.",
|
||||
"no_filters_added": "Aucun filtre ajout\u00e9",
|
||||
"add_filter_btn": "Ajouter filtre",
|
||||
"cancel_btn": "Annuler",
|
||||
"enter_name_hint": "Saisir nom",
|
||||
"enter_url_hint": "Saisir URL",
|
||||
"check_updates_btn": "V\u00e9rifier les mises \u00e0 jour",
|
||||
"new_filter_btn": "Abonnement \u00e0 un nouveau filtre",
|
||||
"enter_valid_filter_url": "Saisir un URL valide pour s'abonner au filtre ou \u00e0 un fichier host.",
|
||||
"custom_filter_rules": "R\u00e8gles de filtrage d'utilisateur",
|
||||
"custom_filter_rules_hint": "Saisissez la r\u00e8gle en une ligne. C'est possible d'utiliser les r\u00e8gles de blocage ou la syntaxe des fichiers hosts.",
|
||||
"examples_title": "Exemples",
|
||||
"example_meaning_filter_block": "bloquer l'acc\u00e9s au domaine exemple.org et \u00e0 tous ses sous-domaines",
|
||||
"example_meaning_filter_whitelist": "d\u00e9bloquer l'acc\u00e9s au domaine exemple.org et \u00e0 tous ses sous-domaines",
|
||||
"example_meaning_host_block": "AdGuard Home va retourner l'adresse 127.0.0.1 au domaine example.org (mais pas aux sous-domaines).",
|
||||
"example_comment": "! Voici comment ajouter une d\u00e9scription",
|
||||
"example_comment_meaning": "commentaire",
|
||||
"example_comment_hash": "# Et comme \u00e7a aussi on peut laisser des commentaires",
|
||||
"all_filters_up_to_date_toast": "Tous les filtres sont mis \u00e0 jour",
|
||||
"updated_upstream_dns_toast": "Les serveurs DNS upstream sont mis \u00e0 jour",
|
||||
"dns_test_ok_toast": "Les serveurs DNS sp\u00e9cifi\u00e9s fonctionnent de mani\u00e8re incorrecte",
|
||||
"dns_test_not_ok_toast": "Server \"{{key}}\": could not be used, please check that you've written it correctly",
|
||||
"unblock_btn": "D\u00e9bloquer",
|
||||
"block_btn": "Bloquer",
|
||||
"time_table_header": "Temps",
|
||||
"domain_name_table_header": "Nom de domaine",
|
||||
"type_table_header": "Type",
|
||||
"response_table_header": "R\u00e9ponse",
|
||||
"empty_response_status": "Vide",
|
||||
"show_all_filter_type": "Montrer tout",
|
||||
"show_filtered_type": "Montrer les sites filtr\u00e9s",
|
||||
"no_logs_found": "Aucun journal trouv\u00e9",
|
||||
"disabled_log_btn": "D\u00e9sactiver le journal",
|
||||
"download_log_file_btn": "T\u00e9l\u00e9charger le fichier de journal",
|
||||
"refresh_btn": "Actualiser",
|
||||
"enabled_log_btn": "Activer le journal",
|
||||
"last_dns_queries": "5000 derni\u00e8res requ\u00eates DNS",
|
||||
"previous_btn": "Pr\u00e9c\u00e9dent",
|
||||
"next_btn": "Suivant",
|
||||
"loading_table_status": "Chargement en cours ...",
|
||||
"page_table_footer_text": "Page",
|
||||
"of_table_footer_text": "de",
|
||||
"rows_table_footer_text": "lignes",
|
||||
"updated_custom_filtering_toast": "R\u00e8gles de filtrage d'utilisateur mises \u00e0 jour",
|
||||
"rule_removed_from_custom_filtering_toast": "R\u00e8gle retir\u00e9e des r\u00e8gles d'utilisateur",
|
||||
"rule_added_to_custom_filtering_toast": "R\u00e8gle ajout\u00e9e aux r\u00e8gles d'utilisateur",
|
||||
"query_log_disabled_toast": "Journal de requ\u00eates d\u00e9sactiv\u00e9",
|
||||
"query_log_enabled_toast": "Journal de requ\u00eates activ\u00e9",
|
||||
"source_label": "Source",
|
||||
"found_in_known_domain_db": "Trouv\u00e9 dans la base de donn\u00e9es des domaines connus",
|
||||
"category_label": "Cat\u00e9gorie",
|
||||
"rule_label": "R\u00e8gle",
|
||||
"filter_label": "Filtre"
|
||||
}
|
||||
124
client/src/__locales/ja.json
Normal file
124
client/src/__locales/ja.json
Normal file
@@ -0,0 +1,124 @@
|
||||
{
|
||||
"back": "\u623b\u308b",
|
||||
"dashboard": "\u30c0\u30c3\u30b7\u30e5\u30dc\u30fc\u30c9",
|
||||
"settings": "\u8a2d\u5b9a",
|
||||
"filters": "\u30d5\u30a3\u30eb\u30bf",
|
||||
"query_log": "\u30af\u30a8\u30ea\u30fb\u30ed\u30b0",
|
||||
"faq": "FAQ",
|
||||
"version": "\u30d0\u30fc\u30b8\u30e7\u30f3",
|
||||
"address": "\u30a2\u30c9\u30ec\u30b9",
|
||||
"on": "ON",
|
||||
"off": "OFF",
|
||||
"copyright": "\u8457\u4f5c\u6a29",
|
||||
"homepage": "\u30db\u30fc\u30e0\u30da\u30fc\u30b8",
|
||||
"report_an_issue": "\u554f\u984c\u3092\u5831\u544a\u3059\u308b",
|
||||
"enable_protection": "\u4fdd\u8b77\u3092\u6709\u52b9\u306b\u3059\u308b",
|
||||
"enabled_protection": "\u4fdd\u8b77\u6709\u52b9",
|
||||
"disable_protection": "\u4fdd\u8b77\u3092\u7121\u52b9\u306b\u3059\u308b",
|
||||
"disabled_protection": "\u4fdd\u8b77\u7121\u52b9",
|
||||
"refresh_statics": "\u7d71\u8a08\u30c7\u30fc\u30bf\u3092\u66f4\u65b0\u3059\u308b",
|
||||
"dns_query": "DNS\u30af\u30a8\u30ea",
|
||||
"blocked_by": "\u306b\u3088\u3063\u3066\u30d6\u30ed\u30c3\u30af\u6e08\u307f",
|
||||
"stats_malware_phishing": "\u30d6\u30ed\u30c3\u30af\u6e08\u307f\u30de\u30eb\u30a6\u30a7\u30a2\uff0f\u30d5\u30a3\u30c3\u30b7\u30f3\u30b0",
|
||||
"stats_adult": "\u30d6\u30ed\u30c3\u30af\u6e08\u307f\u30a2\u30c0\u30eb\u30c8\u30a6\u30a7\u30d6\u30b5\u30a4\u30c8",
|
||||
"stats_query_domain": "\u983b\u7e41\u306b\u554f\u3044\u5408\u308f\u305b\u3055\u308c\u308b\u30c9\u30e1\u30a4\u30f3",
|
||||
"for_last_24_hours": "\u904e\u53bb24\u6642\u9593\u5185",
|
||||
"no_domains_found": "\u30c9\u30e1\u30a4\u30f3\u898b\u3064\u304b\u308a\u307e\u305b\u3093\u3067\u3057\u305f",
|
||||
"requests_count": "\u30ea\u30af\u30a8\u30b9\u30c8\u6570",
|
||||
"top_blocked_domains": "\u6700\u3082\u30d6\u30ed\u30c3\u30af\u3055\u308c\u308b\u30c9\u30e1\u30a4\u30f3",
|
||||
"top_clients": "\u30c8\u30c3\u30d7\u30af\u30e9\u30a4\u30a2\u30f3\u30c8",
|
||||
"no_clients_found": "\u30af\u30e9\u30a4\u30a2\u30f3\u30c8\u898b\u3064\u304b\u308a\u307e\u305b\u3093\u3067\u3057\u305f",
|
||||
"general_statistics": "\u4e00\u822c\u7d71\u8a08",
|
||||
"number_of_dns_query_24_hours": "\u904e\u53bb24\u6642\u9593\u306b\u51e6\u7406\u3055\u308c\u305fDNS\u30af\u30a8\u30ea\u306e\u6570",
|
||||
"number_of_dns_query_blocked_24_hours": "\u5e83\u544a\u30d6\u30ed\u30c3\u30af\u30d5\u30a3\u30eb\u30bf\u3068\u30db\u30b9\u30c8\u30d6\u30ed\u30c3\u30af\u30ea\u30b9\u30c8\u306b\u3088\u3063\u3066\u30d6\u30ed\u30c3\u30af\u3055\u308c\u305fDNS\u30ea\u30af\u30a8\u30b9\u30c8\u306e\u6570",
|
||||
"number_of_dns_query_blocked_24_hours_by_sec": "AdGuard\u30d6\u30e9\u30a6\u30b8\u30f3\u30b0\u30bb\u30ad\u30e5\u30ea\u30c6\u30a3\u30e2\u30b8\u30e5\u30fc\u30eb\u306b\u3088\u3063\u3066\u30d6\u30ed\u30c3\u30af\u3055\u308c\u305fDNS\u30ea\u30af\u30a8\u30b9\u30c8\u306e\u6570",
|
||||
"number_of_dns_query_blocked_24_hours_adult": "\u30d6\u30ed\u30c3\u30af\u6e08\u307f\u6210\u4eba\u5411\u3051\u30a6\u30a7\u30d6\u30b5\u30a4\u30c8\u306e\u6570",
|
||||
"enforced_save_search": "\u30bb\u30fc\u30d5\u30b5\u30fc\u30c1\u9069\u7528\u6e08\u307f",
|
||||
"number_of_dns_query_to_safe_search": "\u30bb\u30fc\u30d5\u30b5\u30fc\u30c1\u304c\u9069\u7528\u3055\u308c\u305f\u691c\u7d22\u30a8\u30f3\u30b8\u30f3\u306b\u5bfe\u3059\u308bDNS\u30ea\u30af\u30a8\u30b9\u30c8\u306e\u6570",
|
||||
"average_processing_time": "\u5e73\u5747\u51e6\u7406\u6642\u9593",
|
||||
"average_processing_time_hint": "DNS\u30ea\u30af\u30a8\u30b9\u30c8\u306e\u51e6\u7406\u306b\u304b\u304b\u308b\u5e73\u5747\u6642\u9593\uff08\u30df\u30ea\u79d2\u5358\u4f4d\uff09",
|
||||
"block_domain_use_filters_and_hosts": "\u30d5\u30a3\u30eb\u30bf\u3068\u30db\u30b9\u30c8\u30d5\u30a1\u30a4\u30eb\u3092\u4f7f\u7528\u3057\u3066\u30c9\u30e1\u30a4\u30f3\u3092\u30d6\u30ed\u30c3\u30af\u3059\u308b",
|
||||
"filters_block_toggle_hint": "<a href='#filters'>\u30d5\u30a3\u30eb\u30bf<\/a>\u306e\u8a2d\u5b9a\u3067\u30d6\u30ed\u30c3\u30ad\u30f3\u30b0\u30eb\u30fc\u30eb\u3092\u8a2d\u5b9a\u3059\u308b\u3053\u3068\u304c\u3067\u304d\u307e\u3059\u3002",
|
||||
"use_adguard_browsing_sec": "AdGuard\u30d6\u30e9\u30a6\u30b8\u30f3\u30b0\u30bb\u30ad\u30e5\u30ea\u30c6\u30a3Web\u30b5\u30fc\u30d3\u30b9\u3092\u4f7f\u7528\u3059\u308b",
|
||||
"use_adguard_browsing_sec_hint": "AdGuard Home\u306f\u3001\u30d6\u30e9\u30a6\u30b8\u30f3\u30b0\u30bb\u30ad\u30e5\u30ea\u30c6\u30a3Web\u30b5\u30fc\u30d3\u30b9\u306b\u3088\u3063\u3066\u30c9\u30e1\u30a4\u30f3\u304c\u30d6\u30e9\u30c3\u30af\u30ea\u30b9\u30c8\u306b\u767b\u9332\u3055\u308c\u3066\u3044\u308b\u304b\u3069\u3046\u304b\u3092\u78ba\u8a8d\u3057\u307e\u3059\u3002 \u3053\u308c\u306f\u30d7\u30e9\u30a4\u30d0\u30b7\u30fc\u3092\u8003\u616e\u3057\u305fAPI\u3092\u4f7f\u7528\u3057\u3066\u30c1\u30a7\u30c3\u30af\u3092\u5b9f\u884c\u3057\u307e\u3059\u3002\u30c9\u30e1\u30a4\u30f3\u540dSHA256\u30cf\u30c3\u30b7\u30e5\u306e\u77ed\u3044\u30d7\u30ec\u30d5\u30a3\u30c3\u30af\u30b9\u306e\u307f\u304c\u30b5\u30fc\u30d0\u30fc\u306b\u9001\u4fe1\u3055\u308c\u307e\u3059\u3002",
|
||||
"use_adguard_parental": "AdGuard\u30da\u30a2\u30ec\u30f3\u30bf\u30eb\u30b3\u30f3\u30c8\u30ed\u30fc\u30ebWeb\u30b5\u30fc\u30d3\u30b9\u3092\u4f7f\u7528\u3059\u308b",
|
||||
"use_adguard_parental_hint": "AdGuard Home\u306f\u3001\u30c9\u30e1\u30a4\u30f3\u306b\u30a2\u30c0\u30eb\u30c8\u30b3\u30f3\u30c6\u30f3\u30c4\u304c\u542b\u307e\u308c\u3066\u3044\u308b\u304b\u3069\u3046\u304b\u3092\u78ba\u8a8d\u3057\u307e\u3059\u3002 \u30d6\u30e9\u30a6\u30b8\u30f3\u30b0\u30bb\u30ad\u30e5\u30ea\u30c6\u30a3Web\u30b5\u30fc\u30d3\u30b9\u3068\u540c\u3058\u30d7\u30e9\u30a4\u30d0\u30b7\u30fc\u306b\u512a\u3057\u3044API\u3092\u4f7f\u7528\u3057\u307e\u3059\u3002",
|
||||
"enforce_safe_search": "\u30bb\u30fc\u30d5\u30b5\u30fc\u30c1\u3092\u9069\u7528\u3059\u308b",
|
||||
"enforce_save_search_hint": "AdGuard Home\u306f\u3001Google\u3001Youtube\u3001Bing\u3001Yandex\u306e\u691c\u7d22\u30a8\u30f3\u30b8\u30f3\u3067\u30bb\u30fc\u30d5\u30b5\u30fc\u30c1\u3092\u9069\u7528\u3067\u304d\u307e\u3059\u3002",
|
||||
"no_servers_specified": "\u30b5\u30fc\u30d0\u30fc\u304c\u6307\u5b9a\u3055\u308c\u3066\u3044\u307e\u305b\u3093",
|
||||
"no_settings": "\u8a2d\u5b9a\u306a\u3057",
|
||||
"general_settings": "\u4e00\u822c\u8a2d\u5b9a",
|
||||
"upstream_dns": "\u30a2\u30c3\u30d7\u30b9\u30c8\u30ea\u30fc\u30e0DNS\u30b5\u30fc\u30d0\u30fc",
|
||||
"upstream_dns_hint": "\u3053\u306e\u30d5\u30a3\u30fc\u30eb\u30c9\u3092\u7a7a\u306e\u307e\u307e\u306b\u3059\u308b\u3068\u3001AdGuard Home\u306f<a href='https:\/\/1.1.1.1\/' target='_blank'>Cloudflare DNS<\/a>\u3092\u30a2\u30c3\u30d7\u30b9\u30c8\u30ea\u30fc\u30e0\u3068\u3057\u3066\u4f7f\u7528\u3057\u307e\u3059\u3002 TLS\u30b5\u30fc\u30d0\u30fc\u7d4c\u7531\u306eDNS\u306b\u306f\u3001\uff62tls:\/\/\u300d\u30d7\u30ec\u30d5\u30a3\u30c3\u30af\u30b9\u3092\u4f7f\u7528\u3057\u3066\u304f\u3060\u3055\u3044\u3002",
|
||||
"test_upstream_btn": "\u30a2\u30c3\u30d7\u30b9\u30c8\u30ea\u30fc\u30e0\u30b5\u30fc\u30d0\u30fc\u30c6\u30b9\u30c8",
|
||||
"apply_btn": "\u9069\u7528",
|
||||
"disabled_filtering_toast": "\u30d5\u30a3\u30eb\u30bf\u30ea\u30f3\u30b0\u7121\u52b9",
|
||||
"enabled_filtering_toast": "\u30d5\u30a3\u30eb\u30bf\u30ea\u30f3\u30b0\u6709\u52b9",
|
||||
"disabled_safe_browsing_toast": "\u30bb\u30fc\u30d5\u30d6\u30e9\u30a6\u30b8\u30f3\u30b0\u7121\u52b9",
|
||||
"enabled_safe_browsing_toast": "\u30bb\u30fc\u30d5\u30d6\u30e9\u30a6\u30b8\u30f3\u30b0\u6709\u52b9",
|
||||
"disabled_parental_toast": "\u30da\u30a2\u30ec\u30f3\u30bf\u30eb\u30b3\u30f3\u30c8\u30ed\u30fc\u30eb\u7121\u52b9",
|
||||
"enabled_parental_toast": "\u30da\u30a2\u30ec\u30f3\u30bf\u30eb\u30b3\u30f3\u30c8\u30ed\u30fc\u30eb\u6709\u52b9",
|
||||
"disabled_safe_search_toast": "\u30bb\u30fc\u30d5\u30b5\u30fc\u30c1\u7121\u52b9",
|
||||
"enabled_save_search_toast": "\u30bb\u30fc\u30d5\u30b5\u30fc\u30c1\u6709\u52b9",
|
||||
"enabled_table_header": "\u6709\u52b9",
|
||||
"name_table_header": "\u540d\u79f0",
|
||||
"filter_url_table_header": "\u30d5\u30a3\u30eb\u30bf\u306eURL",
|
||||
"rules_count_table_header": "\u30eb\u30fc\u30eb\u6570",
|
||||
"last_time_updated_table_header": "\u6700\u7d42\u66f4\u65b0\u65e5",
|
||||
"actions_table_header": "\u64cd\u4f5c",
|
||||
"delete_table_action": "\u524a\u9664",
|
||||
"filters_and_hosts": "\u30d5\u30a3\u30eb\u30bf\u3068\u30db\u30b9\u30c8\u30d6\u30ed\u30c3\u30af\u30ea\u30b9\u30c8",
|
||||
"filters_and_hosts_hint": "AdGuard Home\u306f\u3001\u57fa\u672c\u7684\u306a\u5e83\u544a\u30d6\u30ed\u30c3\u30af\u30eb\u30fc\u30eb\u3068\u30db\u30b9\u30c8\u30d5\u30a1\u30a4\u30eb\u306e\u69cb\u6587\u3092\u7406\u89e3\u3057\u3066\u3044\u307e\u3059\u3002",
|
||||
"no_filters_added": "\u30d5\u30a3\u30eb\u30bf\u306f\u8ffd\u52a0\u3055\u308c\u307e\u305b\u3093\u3067\u3057\u305f",
|
||||
"add_filter_btn": "\u30d5\u30a3\u30eb\u30bf\u3092\u8ffd\u52a0\u3059\u308b",
|
||||
"cancel_btn": "\u30ad\u30e3\u30f3\u30bb\u30eb",
|
||||
"enter_name_hint": "\u540d\u79f0\u3092\u5165\u529b",
|
||||
"enter_url_hint": "URL\u3092\u5165\u529b",
|
||||
"check_updates_btn": "\u30a2\u30c3\u30d7\u30c7\u30fc\u30c8\u3092\u78ba\u8a8d\u3059\u308b",
|
||||
"new_filter_btn": "\u65b0\u3057\u3044\u30d5\u30a3\u30eb\u30bf\u30fb\u30b5\u30d6\u30b9\u30af\u30ea\u30d7\u30b7\u30e7\u30f3",
|
||||
"enter_valid_filter_url": "\u30d5\u30a3\u30eb\u30bf\u30fb\u30b5\u30d6\u30b9\u30af\u30ea\u30d7\u30b7\u30e7\u30f3\u304a\u3088\u3073\u30db\u30b9\u30c8\u30d5\u30a1\u30a4\u30eb\u306e\u6709\u52b9\u306aURL\u3092\u5165\u529b\u3057\u3066\u304f\u3060\u3055\u3044\u3002",
|
||||
"custom_filter_rules": "\u30ab\u30b9\u30bf\u30e0\u30fb\u30d5\u30a3\u30eb\u30bf\u30ea\u30f3\u30b0\u30eb\u30fc\u30eb",
|
||||
"custom_filter_rules_hint": "1\u3064\u306e\u884c\u306b1\u3064\u306e\u30eb\u30fc\u30eb\u3092\u5165\u529b\u3057\u3066\u304f\u3060\u3055\u3044\u3002 \u5e83\u544a\u30d6\u30ed\u30c3\u30af\u30eb\u30fc\u30eb\u3084\u30db\u30b9\u30c8\u30d5\u30a1\u30a4\u30eb\u69cb\u6587\u3092\u4f7f\u7528\u3067\u304d\u307e\u3059\u3002",
|
||||
"examples_title": "\u4f8b",
|
||||
"example_meaning_filter_block": "example.org\u30c9\u30e1\u30a4\u30f3\u3068\u305d\u306e\u3059\u3079\u3066\u306e\u30b5\u30d6\u30c9\u30e1\u30a4\u30f3\u3078\u306e\u30a2\u30af\u30bb\u30b9\u3092\u30d6\u30ed\u30c3\u30af\u3059\u308b",
|
||||
"example_meaning_filter_whitelist": "example.org\u30c9\u30e1\u30a4\u30f3\u3068\u305d\u306e\u3059\u3079\u3066\u306e\u30b5\u30d6\u30c9\u30e1\u30a4\u30f3\u3078\u306e\u30a2\u30af\u30bb\u30b9\u306e\u30d6\u30ed\u30c3\u30af\u3092\u89e3\u9664\u3059\u308b",
|
||||
"example_meaning_host_block": "AdGuard Home\u306f\u3001example.org\u30c9\u30e1\u30a4\u30f3\uff08\u30b5\u30d6\u30c9\u30e1\u30a4\u30f3\u306f\u9664\u304f\uff09\u306b\u5bfe\u3057\u3066127.0.0.1\u306e\u30a2\u30c9\u30ec\u30b9\u3092\u8fd4\u3059\u3088\u3046\u306b\u306a\u308a\u307e\u3057\u305f\u3002",
|
||||
"example_comment": "! \u3053\u3053\u306b\u306f\u30b3\u30e1\u30f3\u30c8\u304c\u5165\u308a\u307e\u3059",
|
||||
"example_comment_meaning": "\u305f\u3060\u306e\u30b3\u30e1\u30f3\u30c8",
|
||||
"example_comment_hash": "# \u3053\u3053\u3082\u30b3\u30e1\u30f3\u30c8",
|
||||
"all_filters_up_to_date_toast": "\u30d5\u30a3\u30eb\u30bf\u306f\u65e2\u306b\u3059\u3079\u3066\u6700\u65b0\u3067\u3059",
|
||||
"updated_upstream_dns_toast": "\u30a2\u30c3\u30d7\u30b9\u30c8\u30ea\u30fc\u30e0DNS\u30b5\u30fc\u30d0\u30fc\u3092\u66f4\u65b0\u3057\u307e\u3057\u305f",
|
||||
"dns_test_ok_toast": "\u6307\u5b9a\u3055\u308c\u305fDNS\u30b5\u30fc\u30d0\u30fc\u306f\u6b63\u3057\u304f\u52d5\u4f5c\u3057\u3066\u3044\u307e\u3059",
|
||||
"dns_test_not_ok_toast": "\u30b5\u30fc\u30d0 \"{{key}}\": \u4f7f\u7528\u3067\u304d\u307e\u305b\u3093\u3067\u3057\u305f\u3002\u6b63\u3057\u304f\u5165\u529b\u3055\u308c\u3066\u3044\u308b\u304b\u3069\u3046\u304b\u3092\u78ba\u8a8d\u3057\u3066\u304f\u3060\u3055\u3044",
|
||||
"unblock_btn": "\u30d6\u30ed\u30c3\u30af\u89e3\u9664",
|
||||
"block_btn": "\u30d6\u30ed\u30c3\u30af",
|
||||
"time_table_header": "\u6642\u523b",
|
||||
"domain_name_table_header": "\u30c9\u30e1\u30a4\u30f3\u540d",
|
||||
"type_table_header": "\u7a2e\u985e",
|
||||
"response_table_header": "\u5fdc\u7b54",
|
||||
"empty_response_status": "\u7a7a",
|
||||
"show_all_filter_type": "\u5168\u3066\u8868\u793a",
|
||||
"show_filtered_type": "\u30d5\u30a3\u30eb\u30bf\u30ea\u30f3\u30b0\u6e08\u307f\u3092\u8868\u793a",
|
||||
"no_logs_found": "\u30ed\u30b0\u898b\u3064\u304b\u308a\u307e\u305b\u3093\u3067\u3057\u305f",
|
||||
"disabled_log_btn": "\u30ed\u30b0\u3092\u7121\u52b9\u306b\u3059\u308b",
|
||||
"download_log_file_btn": "\u30ed\u30b0\u30d5\u30a1\u30a4\u30eb\u3092\u30c0\u30a6\u30f3\u30ed\u30fc\u30c9",
|
||||
"refresh_btn": "\u66f4\u65b0",
|
||||
"enabled_log_btn": "\u30ed\u30b0\u3092\u6709\u52b9\u306b\u3059\u308b",
|
||||
"last_dns_queries": "\u6700\u7d42\uff15\uff10\uff10\uff10\u672c\u306eDNS\u30af\u30a8\u30ea",
|
||||
"previous_btn": "\u524d",
|
||||
"next_btn": "\u6b21\u3078",
|
||||
"loading_table_status": "\u8aad\u307f\u8fbc\u307f\u4e2d\u2026",
|
||||
"page_table_footer_text": "\u30da\u30fc\u30b8",
|
||||
"of_table_footer_text": "\uff0f",
|
||||
"rows_table_footer_text": "\u884c",
|
||||
"updated_custom_filtering_toast": "\u30ab\u30b9\u30bf\u30e0\u30fb\u30d5\u30a3\u30eb\u30bf\u30ea\u30f3\u30b0\u30eb\u30fc\u30eb\u3092\u66f4\u65b0\u3057\u307e\u3057\u305f",
|
||||
"rule_removed_from_custom_filtering_toast": "\u30ab\u30b9\u30bf\u30e0\u30fb\u30d5\u30a3\u30eb\u30bf\u30ea\u30f3\u30b0\u30eb\u30fc\u30eb\u304b\u3089\u30eb\u30fc\u30eb\u3092\u9664\u53bb\u3057\u307e\u3057\u305f",
|
||||
"rule_added_to_custom_filtering_toast": "\u30ab\u30b9\u30bf\u30e0\u30fb\u30d5\u30a3\u30eb\u30bf\u30ea\u30f3\u30b0\u30eb\u30fc\u30eb\u306b\u30eb\u30fc\u30eb\u3092\u8ffd\u52a0\u3057\u307e\u3057\u305f",
|
||||
"query_log_disabled_toast": "\u30af\u30a8\u30ea\u30fb\u30ed\u30b0\u7121\u52b9",
|
||||
"query_log_enabled_toast": "\u30af\u30a8\u30ea\u30fb\u30ed\u30b0\u6709\u52b9",
|
||||
"source_label": "\u30bd\u30fc\u30b9",
|
||||
"found_in_known_domain_db": "\u65e2\u77e5\u306e\u30c9\u30e1\u30a4\u30f3\u30c7\u30fc\u30bf\u30d9\u30fc\u30b9\u306b\u898b\u3064\u304b\u308a\u307e\u3057\u305f\u3002",
|
||||
"category_label": "\u30ab\u30c6\u30b4\u30ea",
|
||||
"rule_label": "\u30eb\u30fc\u30eb",
|
||||
"filter_label": "\u30d5\u30a3\u30eb\u30bf"
|
||||
}
|
||||
124
client/src/__locales/pt-br.json
Normal file
124
client/src/__locales/pt-br.json
Normal file
@@ -0,0 +1,124 @@
|
||||
{
|
||||
"back": "Voltar",
|
||||
"dashboard": "Painel",
|
||||
"settings": "Configura\u00e7\u00f5es",
|
||||
"filters": "Filtros",
|
||||
"query_log": "Registro de consultas",
|
||||
"faq": "FAQ",
|
||||
"version": "vers\u00e3o",
|
||||
"address": "endere\u00e7o",
|
||||
"on": "ON",
|
||||
"off": "OFF",
|
||||
"copyright": "Copyright",
|
||||
"homepage": "P\u00e1gina inicial",
|
||||
"report_an_issue": "Reportar um problema",
|
||||
"enable_protection": "Ativar prote\u00e7\u00e3o",
|
||||
"enabled_protection": "Prote\u00e7\u00e3o ativada",
|
||||
"disable_protection": "Desativar prote\u00e7\u00e3o",
|
||||
"disabled_protection": "Prote\u00e7\u00e3o desativada",
|
||||
"refresh_statics": "Atualizar estat\u00edsticas",
|
||||
"dns_query": "Consultas de DNS",
|
||||
"blocked_by": "Bloqueador por",
|
||||
"stats_malware_phishing": "Bloqueado malware\/phishing",
|
||||
"stats_adult": "Bloqueado sites adultos",
|
||||
"stats_query_domain": "Principais dom\u00ednios consultados",
|
||||
"for_last_24_hours": "nas \u00faltimas 24 horas",
|
||||
"no_domains_found": "Nenhum dom\u00ednio encontrado",
|
||||
"requests_count": "Contagem de solicita\u00e7\u00f5es",
|
||||
"top_blocked_domains": "Principais dom\u00ednios bloqueados",
|
||||
"top_clients": "Principais clientes",
|
||||
"no_clients_found": "Nenhuma cliente encontrado",
|
||||
"general_statistics": "Estat\u00edsticas gerais",
|
||||
"number_of_dns_query_24_hours": "O n\u00famero de consultas DNS processadas nas \u00faltimas 24 horas",
|
||||
"number_of_dns_query_blocked_24_hours": "V\u00e1rias solicita\u00e7\u00f5es DNS bloqueadas por filtros de bloqueio de an\u00fancios e listas de bloqueio de hosts",
|
||||
"number_of_dns_query_blocked_24_hours_by_sec": "V\u00e1rias solicita\u00e7\u00f5es de DNS bloqueadas pelo m\u00f3dulo de seguran\u00e7a da navega\u00e7\u00e3o do AdGuard",
|
||||
"number_of_dns_query_blocked_24_hours_adult": "V\u00e1rios sites adultos bloqueados",
|
||||
"enforced_save_search": "For\u00e7ar pesquisa segura",
|
||||
"number_of_dns_query_to_safe_search": "V\u00e1rias solicita\u00e7\u00f5es de DNS para motores de busca para os quais a pesquisa segura foi aplicada",
|
||||
"average_processing_time": "Tempo m\u00e9dio de processamento",
|
||||
"average_processing_time_hint": "Tempo m\u00e9dio em milissegundos no processamento de uma solicita\u00e7\u00e3o DNS",
|
||||
"block_domain_use_filters_and_hosts": "Bloquear dom\u00ednios usando arquivos de filtros e hosts",
|
||||
"filters_block_toggle_hint": "Voc\u00ea pode configurar as regras de bloqueio nas configura\u00e7\u00f5es de <a href='#filters'>Filtros<\/a>.",
|
||||
"use_adguard_browsing_sec": "Usar o servi\u00e7o de seguran\u00e7a da navega\u00e7\u00e3o do AdGuard",
|
||||
"use_adguard_browsing_sec_hint": "O AdGuard Home ir\u00e1 verificar se o dom\u00ednio est\u00e1 na lista negra do servi\u00e7o de seguran\u00e7a da navega\u00e7\u00e3o. Ele usar\u00e1 a API de pesquisa de privacidade para executar a verifica\u00e7\u00e3o: apenas um prefixo curto do hash do nome de dom\u00ednio SHA256 \u00e9 enviado para o servidor.",
|
||||
"use_adguard_parental": "Usar o servi\u00e7o de controle parental do AdGuard",
|
||||
"use_adguard_parental_hint": "O AdGuard Home ir\u00e1 verificar se o dom\u00ednio cont\u00e9m conte\u00fado adulto. Ele usa a mesma API amig\u00e1vel de privacidade que o servi\u00e7o de seguran\u00e7a da navega\u00e7\u00e3o.",
|
||||
"enforce_safe_search": "For\u00e7ar pesquisa segura",
|
||||
"enforce_save_search_hint": "O AdGuard Home pode for\u00e7ar a pesquisa segura nos seguintes motores de busca: Google, Youtube, Bing e Yandex.",
|
||||
"no_servers_specified": "Nenhum servidor especificado",
|
||||
"no_settings": "N\u00e3o configurado",
|
||||
"general_settings": "Configura\u00e7\u00f5es gerais",
|
||||
"upstream_dns": "Servidores DNS upstream",
|
||||
"upstream_dns_hint": "Se voc\u00ea deixar este campo vazio, o AdGuard Home ir\u00e1 usar o<a href='https:\/\/1.1.1.1\/' target='_blank'>DNS da Cloudflare<\/a> como upstream. Use o prefixo tls:\/\/ para servidores DNS com TLS.",
|
||||
"test_upstream_btn": "Testar upstreams",
|
||||
"apply_btn": "Aplicar",
|
||||
"disabled_filtering_toast": "Filtragem desativada",
|
||||
"enabled_filtering_toast": "Filtragem ativada",
|
||||
"disabled_safe_browsing_toast": "Navega\u00e7\u00e3o segura desativada",
|
||||
"enabled_safe_browsing_toast": "Navega\u00e7\u00e3o segura ativada",
|
||||
"disabled_parental_toast": "Controle parental desativado",
|
||||
"enabled_parental_toast": "Controle parental ativado",
|
||||
"disabled_safe_search_toast": "Pesquisa segura desativada",
|
||||
"enabled_save_search_toast": "Pesquisa segura ativada",
|
||||
"enabled_table_header": "Ativado",
|
||||
"name_table_header": "Nome",
|
||||
"filter_url_table_header": "URL do filtro",
|
||||
"rules_count_table_header": "Quantidade de regras",
|
||||
"last_time_updated_table_header": "\u00daltima atualiza\u00e7\u00e3o",
|
||||
"actions_table_header": "A\u00e7\u00f5es",
|
||||
"delete_table_action": "Excluir",
|
||||
"filters_and_hosts": "Filtros e listas de bloqueio de hosts",
|
||||
"filters_and_hosts_hint": "O AdGuard Home entende regras b\u00e1sicas de bloqueio de an\u00fancios e a sintaxe de arquivos de hosts.",
|
||||
"no_filters_added": "Nenhum filtro adicionado",
|
||||
"add_filter_btn": "Adicionar filtro",
|
||||
"cancel_btn": "Cancelar",
|
||||
"enter_name_hint": "Digite o nome",
|
||||
"enter_url_hint": "Digite a URL",
|
||||
"check_updates_btn": "Verificar atualiza\u00e7\u00f5es",
|
||||
"new_filter_btn": "Nova inscri\u00e7\u00e3o de filtro",
|
||||
"enter_valid_filter_url": "Digite a URL v\u00e1lida para efetuar a inscri\u00e7\u00e3o de filtro ou um arquivo de hosts.",
|
||||
"custom_filter_rules": "Regras de filtragem personalizadas",
|
||||
"custom_filter_rules_hint": "Digite uma regra por linha. Voc\u00ea pode usar regras de bloqueio de an\u00fancios ou a sintaxe de arquivos de hosts.",
|
||||
"examples_title": "Exemplos",
|
||||
"example_meaning_filter_block": "bloqueia o acesso ao dom\u00ednio exemplo.org e a todos os seus subdom\u00ednios",
|
||||
"example_meaning_filter_whitelist": "desbloqueia o acesso ao dom\u00ednio exemplo.org e a todos os seus subdom\u00ednios",
|
||||
"example_meaning_host_block": "O AdGuard Home ir\u00e1 retornar o endere\u00e7o 127.0.0.1 para o dom\u00ednio exemplo.org (exceto seus subdom\u00ednios).",
|
||||
"example_comment": "! Aqui vai um coment\u00e1rio",
|
||||
"example_comment_meaning": "apenas um coment\u00e1rio",
|
||||
"example_comment_hash": "# Tamb\u00e9m um coment\u00e1rio",
|
||||
"all_filters_up_to_date_toast": "Todos os filtros j\u00e1 est\u00e3o atualizados",
|
||||
"updated_upstream_dns_toast": "Atualizado os servidores DNS upstream",
|
||||
"dns_test_ok_toast": "Os servidores DNS especificados est\u00e3o funcionando corretamente",
|
||||
"dns_test_not_ok_toast": "O servidor \"{{key}}\": n\u00e3o p\u00f4de ser utilizado. Por favor, verifique se voc\u00ea escreveu corretamente",
|
||||
"unblock_btn": "Desbloquear",
|
||||
"block_btn": "Bloquear",
|
||||
"time_table_header": "Data",
|
||||
"domain_name_table_header": "Nome de dom\u00ednio",
|
||||
"type_table_header": "Tipo",
|
||||
"response_table_header": "Resposta",
|
||||
"empty_response_status": "Vazio",
|
||||
"show_all_filter_type": "Mostrar todos",
|
||||
"show_filtered_type": "Mostrar filtrados",
|
||||
"no_logs_found": "Nenhum registro encontrado",
|
||||
"disabled_log_btn": "Desativar registros",
|
||||
"download_log_file_btn": "Baixar arquivo de registros",
|
||||
"refresh_btn": "Atualizar",
|
||||
"enabled_log_btn": "Ativar registros",
|
||||
"last_dns_queries": "\u00daltimas 5000 consultas DNS",
|
||||
"previous_btn": "Anterior",
|
||||
"next_btn": "Pr\u00f3ximo",
|
||||
"loading_table_status": "Carregando",
|
||||
"page_table_footer_text": "P\u00e1gina",
|
||||
"of_table_footer_text": "de",
|
||||
"rows_table_footer_text": "linhas",
|
||||
"updated_custom_filtering_toast": "Regras de filtragem personalizadas atualizadas",
|
||||
"rule_removed_from_custom_filtering_toast": "Regra removida das regras de filtragem personalizadas",
|
||||
"rule_added_to_custom_filtering_toast": "Regra adicionada \u00e0s regras de filtragem personalizadas",
|
||||
"query_log_disabled_toast": "Registros de consultas desativado",
|
||||
"query_log_enabled_toast": "Registros de consultas ativado",
|
||||
"source_label": "Fonte",
|
||||
"found_in_known_domain_db": "Encontrado no banco de dados de dom\u00ednios conhecidos.",
|
||||
"category_label": "Categoria",
|
||||
"rule_label": "Regra",
|
||||
"filter_label": "Filtro"
|
||||
}
|
||||
128
client/src/__locales/ru.json
Normal file
128
client/src/__locales/ru.json
Normal file
@@ -0,0 +1,128 @@
|
||||
{
|
||||
"back": "\u041d\u0430\u0437\u0430\u0434",
|
||||
"dashboard": "\u041f\u0430\u043d\u0435\u043b\u044c \u0443\u043f\u0440\u0430\u0432\u043b\u0435\u043d\u0438\u044f",
|
||||
"settings": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438",
|
||||
"filters": "\u0424\u0438\u043b\u044c\u0442\u0440\u044b",
|
||||
"query_log": "\u0416\u0443\u0440\u043d\u0430\u043b",
|
||||
"faq": "FAQ",
|
||||
"version": "\u0432\u0435\u0440\u0441\u0438\u044f",
|
||||
"address": "\u0430\u0434\u0440\u0435\u0441",
|
||||
"on": "\u0412\u043a\u043b",
|
||||
"off": "\u0412\u044b\u043a\u043b",
|
||||
"copyright": "\u0412\u0441\u0435 \u043f\u0440\u0430\u0432\u0430 \u0437\u0430\u0449\u0438\u0449\u0435\u043d\u044b",
|
||||
"homepage": "\u0413\u043b\u0430\u0432\u043d\u0430\u044f",
|
||||
"report_an_issue": "\u0421\u043e\u043e\u0431\u0449\u0438\u0442\u044c \u043e \u043f\u0440\u043e\u0431\u043b\u0435\u043c\u0435",
|
||||
"enable_protection": "\u0412\u043a\u043b\u044e\u0447\u0438\u0442\u044c \u0437\u0430\u0449\u0438\u0442\u0443",
|
||||
"enabled_protection": "\u0417\u0430\u0449\u0438\u0442\u0430 \u0432\u043a\u043b.",
|
||||
"disable_protection": "\u041e\u0442\u043a\u043b\u044e\u0447\u0438\u0442\u044c \u0437\u0430\u0449\u0438\u0442\u0443",
|
||||
"disabled_protection": "\u0417\u0430\u0449\u0438\u0442\u0430 \u0432\u044b\u043a\u043b.",
|
||||
"refresh_statics": "\u041e\u0431\u043d\u043e\u0432\u0438\u0442\u044c \u0441\u0442\u0430\u0442\u0438\u0441\u0442\u0438\u043a\u0443",
|
||||
"dns_query": "DNS-\u0437\u0430\u043f\u0440\u043e\u0441\u044b",
|
||||
"blocked_by": "\u0417\u0430\u0431\u043b\u043e\u043a\u0438\u0440\u043e\u0432\u0430\u043d\u043e ",
|
||||
"stats_malware_phishing": "\u0417\u0430\u0431\u043b\u043e\u043a\u0438\u0440\u043e\u0432\u0430\u043d\u043d\u044b\u0435 \u0432\u0440\u0435\u0434\u043e\u043d\u043e\u0441\u043d\u044b\u0435 \u0438 \u0444\u0438\u0448\u0438\u043d\u0433\u043e\u0432\u044b\u0435 \u0441\u0430\u0439\u0442\u044b",
|
||||
"stats_adult": "\u0417\u0430\u0431\u043b\u043e\u043a\u0438\u0440\u043e\u0432\u0430\u043d\u043d\u044b\u0435 \"\u0432\u0437\u0440\u043e\u0441\u043b\u044b\u0435\" \u0441\u0430\u0439\u0442\u044b",
|
||||
"stats_query_domain": "\u0427\u0430\u0441\u0442\u043e \u0437\u0430\u043f\u0440\u0430\u0448\u0438\u0432\u0430\u0435\u043c\u044b\u0435 \u0434\u043e\u043c\u0435\u043d\u044b",
|
||||
"for_last_24_hours": "\u0437\u0430 24 \u0447\u0430\u0441\u0430",
|
||||
"no_domains_found": "\u0414\u043e\u043c\u0435\u043d\u044b \u043d\u0435 \u043d\u0430\u0439\u0434\u0435\u043d\u044b",
|
||||
"requests_count": "\u041a\u043e\u043b\u0438\u0447\u0435\u0441\u0442\u0432\u043e \u0437\u0430\u043f\u0440\u043e\u0441\u043e\u0432",
|
||||
"top_blocked_domains": "\u0427\u0430\u0441\u0442\u043e \u0431\u043b\u043e\u043a\u0438\u0440\u0443\u0435\u043c\u044b\u0435 \u0434\u043e\u043c\u0435\u043d\u044b",
|
||||
"top_clients": "\u0427\u0430\u0441\u0442\u044b\u0435 \u043a\u043b\u0438\u0435\u043d\u0442\u044b",
|
||||
"no_clients_found": "\u041a\u043b\u0438\u0435\u043d\u0442\u043e\u0432 \u043d\u0435 \u043d\u0430\u0439\u0434\u0435\u043d\u043e",
|
||||
"general_statistics": "\u041e\u0431\u0449\u0430\u044f \u0441\u0442\u0430\u0442\u0438\u0441\u0442\u0438\u043a\u0430",
|
||||
"number_of_dns_query_24_hours": "\u041a\u043e\u043b\u0438\u0447\u0435\u0441\u0442\u0432\u043e DNS-\u0437\u0430\u043f\u0440\u043e\u0441\u043e\u0432 \u0437\u0430 24 \u0447\u0430\u0441\u0430",
|
||||
"number_of_dns_query_blocked_24_hours": "\u041a\u043e\u043b\u0438\u0447\u0435\u0441\u0442\u0432\u043e DNS-\u0437\u0430\u043f\u0440\u043e\u0441\u043e\u0432, \u0437\u0430\u0431\u043b\u043e\u043a\u0438\u0440\u043e\u0432\u0430\u043d\u043d\u044b\u0445 \u0444\u0438\u043b\u044c\u0442\u0440\u0430\u043c\u0438 \u0438 \u0431\u043b\u043e\u043a-\u0441\u043f\u0438\u0441\u043a\u0430\u043c\u0438",
|
||||
"number_of_dns_query_blocked_24_hours_by_sec": "\u041a\u043e\u043b\u0438\u0447\u0435\u0441\u0442\u0432\u043e DNS-\u0437\u0430\u043f\u0440\u043e\u0441\u043e\u0432, \u0437\u0430\u0431\u043b\u043e\u043a\u0438\u0440\u043e\u0432\u0430\u043d\u043d\u044b\u0445 \u043c\u043e\u0434\u0443\u043b\u0435\u043c \u0410\u043d\u0442\u0438\u0444\u0438\u0448\u0438\u043d\u0433\u0430 AdGuard",
|
||||
"number_of_dns_query_blocked_24_hours_adult": "\u041a\u043e\u043b\u0438\u0447\u0435\u0441\u0442\u0432\u043e \u0437\u0430\u0431\u043b\u043e\u043a\u0438\u0440\u043e\u0432\u0430\u043d\u043d\u044b\u0445 \"\u0441\u0430\u0439\u0442\u043e\u0432 \u0434\u043b\u044f \u0432\u0437\u0440\u043e\u0441\u043b\u044b\u0445\"",
|
||||
"enforced_save_search": "\u041f\u0440\u0438\u043c\u0435\u043d\u0435\u043d \u0431\u0435\u0437\u043e\u043f\u0430\u0441\u043d\u044b\u0439 \u043f\u043e\u0438\u0441\u043a",
|
||||
"number_of_dns_query_to_safe_search": "\u041a\u043e\u043b\u0438\u0447\u0435\u0441\u0442\u0432\u043e \u0437\u0430\u043f\u0440\u043e\u0441\u043e\u0432 DNS \u0434\u043b\u044f \u043f\u043e\u0438\u0441\u043a\u043e\u0432\u044b\u0445 \u0441\u0438\u0441\u0442\u0435\u043c, \u0434\u043b\u044f \u043a\u043e\u0442\u043e\u0440\u044b\u0445 \u0431\u044b\u043b \u043f\u0440\u0438\u043c\u0435\u043d\u0435\u043d \u0411\u0435\u0437\u043e\u043f\u0430\u0441\u043d\u044b\u0439 \u043f\u043e\u0438\u0441\u043a",
|
||||
"average_processing_time": "\u0421\u0440\u0435\u0434\u043d\u0435\u0435 \u0432\u0440\u0435\u043c\u044f \u043e\u0431\u0440\u0430\u0431\u043e\u0442\u043a\u0438 \u0437\u0430\u043f\u0440\u043e\u0441\u0430",
|
||||
"average_processing_time_hint": "\u0421\u0440\u0435\u0434\u043d\u0435\u0435 \u0432\u0440\u0435\u043c\u044f \u0434\u043b\u044f \u043e\u0431\u0440\u0430\u0431\u043e\u0442\u043a\u0438 \u0437\u0430\u043f\u0440\u043e\u0441\u0430 DNS \u0432 \u043c\u0438\u043b\u043b\u0438\u0441\u0435\u043a\u0443\u043d\u0434\u0430\u0445",
|
||||
"block_domain_use_filters_and_hosts": "\u0411\u043b\u043e\u043a\u0438\u0440\u043e\u0432\u0430\u0442\u044c \u0434\u043e\u043c\u0435\u043d\u044b \u0441 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u043d\u0438\u0435\u043c \u0444\u0438\u043b\u044c\u0442\u0440\u043e\u0432 \u0438 \u0444\u0430\u0439\u043b\u043e\u0432 \u0445\u043e\u0441\u0442\u043e\u0432",
|
||||
"filters_block_toggle_hint": "\u0412\u044b \u043c\u043e\u0436\u0435\u0442\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u044c \u043f\u0440\u0430\u0432\u0438\u043b\u0430 \u0431\u043b\u043e\u043a\u0438\u0440\u043e\u0432\u043a\u0438 \u0432 <a href='#filters'> \"\u0424\u0438\u043b\u044c\u0442\u0440\u0430\u0445\"<\/a>.",
|
||||
"use_adguard_browsing_sec": "\u0418\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u044c \u0411\u0435\u0437\u043e\u043f\u0430\u0441\u043d\u0443\u044e \u043d\u0430\u0432\u0438\u0433\u0430\u0446\u0438\u044e AdGuard",
|
||||
"use_adguard_browsing_sec_hint": "AdGuard Home \u043f\u0440\u043e\u0432\u0435\u0440\u0438\u0442, \u0432\u043a\u043b\u044e\u0447\u0435\u043d \u043b\u0438 \u0434\u043e\u043c\u0435\u043d \u0432 \u0432\u0435\u0431-\u0441\u043b\u0443\u0436\u0431\u0443 \u0431\u0435\u0437\u043e\u043f\u0430\u0441\u043d\u043e\u0441\u0442\u0438 \u0431\u0440\u0430\u0443\u0437\u0435\u0440\u0430. \u041e\u043d \u0431\u0443\u0434\u0435\u0442 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u044c API, \u0447\u0442\u043e\u0431\u044b \u0432\u044b\u043f\u043e\u043b\u043d\u0438\u0442\u044c \u043f\u0440\u043e\u0432\u0435\u0440\u043a\u0443: \u043d\u0430 \u0441\u0435\u0440\u0432\u0435\u0440 \u043e\u0442\u043f\u0440\u0430\u0432\u043b\u044f\u0435\u0442\u0441\u044f \u0442\u043e\u043b\u044c\u043a\u043e \u043a\u043e\u0440\u043e\u0442\u043a\u0438\u0439 \u043f\u0440\u0435\u0444\u0438\u043a\u0441 \u0438\u043c\u0435\u043d\u0438 \u0434\u043e\u043c\u0435\u043d\u0430 SHA256.",
|
||||
"use_adguard_parental": "\u0418\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0439\u0442\u0435 \u043c\u043e\u0434\u0443\u043b\u044c \u0420\u043e\u0434\u0438\u0442\u0435\u043b\u044c\u0441\u043a\u043e\u0433\u043e \u043a\u043e\u043d\u0442\u0440\u043e\u043b\u044f AdGuard ",
|
||||
"use_adguard_parental_hint": "AdGuard Home \u043f\u0440\u043e\u0432\u0435\u0440\u0438\u0442, \u0441\u043e\u0434\u0435\u0440\u0436\u0438\u0442 \u043b\u0438 \u0434\u043e\u043c\u0435\u043d \u043c\u0430\u0442\u0435\u0440\u0438\u0430\u043b\u044b 18+. \u041e\u043d \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0435\u0442 \u0442\u043e\u0442 \u0436\u0435 API \u0434\u043b\u044f \u043e\u0431\u0435\u0441\u043f\u0435\u0447\u0435\u043d\u0438\u044f \u043a\u043e\u043d\u0444\u0438\u0434\u0435\u043d\u0446\u0438\u0430\u043b\u044c\u043d\u043e\u0441\u0442\u0438, \u0447\u0442\u043e \u0438 \u0432\u0435\u0431-\u0441\u043b\u0443\u0436\u0431\u0430 \u0431\u0435\u0437\u043e\u043f\u0430\u0441\u043d\u043e\u0441\u0442\u0438 \u0431\u0440\u0430\u0443\u0437\u0435\u0440\u0430.",
|
||||
"enforce_safe_search": "\u0423\u0441\u0438\u043b\u0438\u0442\u044c \u0431\u0435\u0437\u043e\u043f\u0430\u0441\u043d\u044b\u0439 \u043f\u043e\u0438\u0441\u043a",
|
||||
"enforce_save_search_hint": "AdGuard Home \u043c\u043e\u0436\u0435\u0442 \u043e\u0431\u0435\u0441\u043f\u0435\u0447\u0438\u0442\u044c \u0431\u0435\u0437\u043e\u043f\u0430\u0441\u043d\u044b\u0439 \u043f\u043e\u0438\u0441\u043a \u0432 \u0441\u043b\u0435\u0434\u0443\u044e\u0449\u0438\u0445 \u0441\u0438\u0441\u0442\u0435\u043c\u0430\u0445: Google, Youtube, Bing \u0438 Yandex.",
|
||||
"no_servers_specified": "\u041d\u0435\u0442 \u0443\u043a\u0430\u0437\u0430\u043d\u043d\u044b\u0445 \u0441\u0435\u0440\u0432\u0435\u0440\u043e\u0432",
|
||||
"no_settings": "\u041d\u0435\u0442 \u043d\u0430\u0441\u0442\u0440\u043e\u0435\u043a",
|
||||
"general_settings": "\u041e\u0441\u043d\u043e\u0432\u043d\u044b\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438",
|
||||
"upstream_dns": "Upstream DNS-\u0441\u0435\u0440\u0432\u0435\u0440\u044b",
|
||||
"upstream_dns_hint": "\u0415\u0441\u043b\u0438 \u0432\u044b \u043e\u0441\u0442\u0430\u0432\u0438\u0442\u0435 \u044d\u0442\u043e \u043f\u043e\u043b\u0435 \u043f\u0443\u0441\u0442\u044b\u043c, \u0442\u043e AdGuard Home \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0435\u0442 <a href='https:\/\/1.1.1.1\/' target='_blank'>Cloudflare DNS<\/a> \u0432 \u043a\u0430\u0447\u0435\u0441\u0442\u0432\u0435 upstream. \u0418\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0439\u0442\u0435 tls:\/\/ \u0434\u043b\u044f DNS \u0447\u0435\u0440\u0435\u0437 \u0441\u0435\u0440\u0432\u0435\u0440\u044b TLS.",
|
||||
"test_upstream_btn": "\u0422\u0435\u0441\u0442 upstream \u0441\u0435\u0440\u0432\u0435\u0440\u043e\u0432",
|
||||
"apply_btn": "\u041f\u0440\u0438\u043c\u0435\u043d\u0438\u0442\u044c",
|
||||
"disabled_filtering_toast": "\u0424\u0438\u043b\u044c\u0442\u0440\u0430\u0446\u0438\u044f \u0432\u044b\u043a\u043b.",
|
||||
"enabled_filtering_toast": "\u0424\u0438\u043b\u044c\u0442\u0440\u0430\u0446\u0438\u044f \u0432\u043a\u043b.",
|
||||
"disabled_safe_browsing_toast": "\u0411\u0435\u0437\u043e\u043f\u0430\u0441\u043d\u0430\u044f \u043d\u0430\u0432\u0438\u0433\u0430\u0446\u0438\u044f \u0432\u044b\u043a\u043b.",
|
||||
"enabled_safe_browsing_toast": "\u0411\u0435\u0437\u043e\u043f\u0430\u0441\u043d\u0430\u044f \u043d\u0430\u0432\u0438\u0433\u0430\u0446\u0438\u044f \u0432\u043a\u043b.",
|
||||
"disabled_parental_toast": "\u0420\u043e\u0434\u0438\u0442\u0435\u043b\u044c\u0441\u043a\u0438\u0439 \u043a\u043e\u043d\u0442\u0440\u043e\u043b\u044c \u0432\u044b\u043a\u043b.",
|
||||
"enabled_parental_toast": "\u0420\u043e\u0434\u0438\u0442\u0435\u043b\u044c\u0441\u043a\u0438\u0439 \u043a\u043e\u043d\u0442\u0440\u043e\u043b\u044c \u0432\u043a\u043b.",
|
||||
"disabled_safe_search_toast": "\u0411\u0435\u0437\u043e\u043f\u0430\u0441\u043d\u044b\u0439 \u043f\u043e\u0438\u0441\u043a \u0432\u044b\u043a\u043b.",
|
||||
"enabled_save_search_toast": "\u0411\u0435\u0437\u043e\u043f\u0430\u0441\u043d\u044b\u0439 \u043f\u043e\u0438\u0441\u043a \u0432\u043a\u043b.",
|
||||
"enabled_table_header": "\u0412\u043a\u043b.",
|
||||
"name_table_header": "\u0418\u043c\u044f",
|
||||
"filter_url_table_header": "URL \u0444\u0438\u043b\u044c\u0442\u0440\u0430",
|
||||
"rules_count_table_header": "\u041a\u043e\u043b\u0438\u0447\u0435\u0441\u0442\u0432\u043e \u043f\u0440\u0430\u0432\u0438\u043b:",
|
||||
"last_time_updated_table_header": "\u041f\u043e\u0441\u043b\u0435\u0434\u043d\u0435\u0435 \u043e\u0431\u043d\u043e\u0432\u043b\u0435\u043d\u0438\u0435",
|
||||
"actions_table_header": "\u0414\u0435\u0439\u0441\u0442\u0432\u0438\u044f",
|
||||
"delete_table_action": "\u0423\u0434\u0430\u043b\u0438\u0442\u044c",
|
||||
"filters_and_hosts": "\u0424\u0438\u043b\u044c\u0442\u0440\u044b \u0438 \u0447\u0435\u0440\u043d\u044b\u0435 \u0441\u043f\u0438\u0441\u043a\u0438 hosts",
|
||||
"filters_and_hosts_hint": "AdGuard Home \u0440\u0430\u0441\u043f\u043e\u0437\u043d\u0430\u0435\u0442 \u0431\u0430\u0437\u043e\u0432\u044b\u0435 \u043f\u0440\u0430\u0432\u0438\u043b\u0430 \u0431\u043b\u043e\u043a\u0438\u0440\u043e\u0432\u043a\u0438 \u0438 \u0441\u0438\u043d\u0442\u0430\u043a\u0441\u0438\u0441 \u0444\u0430\u0439\u043b\u043e\u0432 hosts.",
|
||||
"no_filters_added": "\u0424\u0438\u043b\u044c\u0442\u0440\u044b \u043d\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u044b",
|
||||
"add_filter_btn": "\u0414\u043e\u0431\u0430\u0432\u0438\u0442\u044c \u0444\u0438\u043b\u044c\u0442\u0440",
|
||||
"cancel_btn": "\u041e\u0442\u043c\u0435\u043d\u0430",
|
||||
"enter_name_hint": "\u0412\u0432\u0435\u0434\u0438\u0442\u0435 \u0438\u043c\u044f",
|
||||
"enter_url_hint": "\u0412\u0432\u0435\u0434\u0438\u0442\u0435 URL",
|
||||
"check_updates_btn": "\u041f\u0440\u043e\u0432\u0435\u0440\u0438\u0442\u044c \u043e\u0431\u043d\u043e\u0432\u043b\u0435\u043d\u0438\u044f",
|
||||
"new_filter_btn": "\u0414\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u0438\u0435 \u043d\u043e\u0432\u043e\u0433\u043e \u0444\u0438\u043b\u044c\u0442\u0440\u0430",
|
||||
"enter_valid_filter_url": "\u0412\u0432\u0435\u0434\u0438\u0442\u0435 \u0434\u0435\u0439\u0441\u0442\u0432\u0443\u044e\u0449\u0438\u0439 URL \u0434\u043b\u044f \u043f\u043e\u0434\u043f\u0438\u0441\u043a\u0438 \u043d\u0430 \u0444\u0438\u043b\u044c\u0442\u0440 \u0438\u043b\u0438 \u0444\u0430\u0439\u043b hosts.",
|
||||
"custom_filter_rules": "\u041F\u043E\u043B\u044C\u0437\u043E\u0432\u0430\u0442\u0435\u043B\u044C\u0441\u043A\u0438\u0439 \u0444\u0438\u043B\u044C\u0442\u0440",
|
||||
"custom_filter_rules_hint": "\u0412\u0432\u043e\u0434\u0438\u0442\u0435 \u043f\u043e \u043e\u0434\u043d\u043e\u043c\u0443 \u043f\u0440\u0430\u0432\u0438\u043b\u0443 \u043d\u0430 \u0441\u0442\u0440\u043e\u0447\u043a\u0443. \u0412\u044b \u043c\u043e\u0436\u0435\u0442\u0435 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u044c \u043f\u0440\u0430\u0432\u0438\u043b\u0430 \u0431\u043b\u043e\u043a\u0438\u0440\u043e\u0432\u043a\u0438 \u0438\u043b\u0438 \u0441\u0438\u043d\u0442\u0430\u043a\u0441\u0438\u0441 \u0444\u0430\u0439\u043b\u043e\u0432 hosts.",
|
||||
"examples_title": "\u041f\u0440\u0438\u043c\u0435\u0440\u044b",
|
||||
"example_meaning_filter_block": "\u0437\u0430\u0431\u043b\u043e\u043a\u0438\u0440\u043e\u0432\u0430\u0442\u044c \u0434\u043e\u0441\u0442\u0443\u043f \u043a \u0434\u043e\u043c\u0435\u043d\u0443 example.org \u0438 \u0432\u0441\u0435\u043c \u0435\u0433\u043e \u043f\u043e\u0434\u0434\u043e\u043c\u0435\u043d\u0430\u043c",
|
||||
"example_meaning_filter_whitelist": "\u0440\u0430\u0437\u0431\u043b\u043e\u043a\u0438\u0440\u043e\u0432\u0430\u0442\u044c \u0434\u043e\u0441\u0442\u0443\u043f \u043a \u0434\u043e\u043c\u0435\u043d\u0443 example.org \u0438 \u0432\u0441\u0435\u043c \u0435\u0433\u043e \u043f\u043e\u0434\u0434\u043e\u043c\u0435\u043d\u0430\u043c",
|
||||
"example_meaning_host_block": "\u0422\u0435\u043f\u0435\u0440\u044c AdGuard Home \u0432\u0435\u0440\u043d\u0435\u0442 127.0.0.1 \u0434\u043b\u044f \u0434\u043e\u043c\u0435\u043d\u0430 example.org (\u043d\u043e \u043d\u0435 \u0434\u043b\u044f \u0435\u0433\u043e \u043f\u043e\u0434\u0434\u043e\u043c\u0435\u043d\u043e\u0432).",
|
||||
"example_comment": "! \u0422\u0430\u043a \u043c\u043e\u0436\u043d\u043e \u0434\u043e\u0431\u0430\u0432\u043b\u044f\u0442\u044c \u043e\u043f\u0438\u0441\u0430\u043d\u0438\u0435",
|
||||
"example_comment_meaning": "\u043a\u043e\u043c\u043c\u0435\u043d\u0442\u0430\u0440\u0438\u0439",
|
||||
"example_comment_hash": "# \u0418 \u0432\u043e\u0442 \u0442\u0430\u043a \u0442\u043e\u0436\u0435",
|
||||
"example_upstream_regular": "\u043e\u0431\u044b\u0447\u043d\u044b\u0439 DNS (\u043f\u043e\u0432\u0435\u0440\u0445 UDP)",
|
||||
"example_upstream_dot": "\u0437\u0430\u0448\u0438\u0444\u0440\u043e\u0432\u0430\u043d\u043d\u044b\u0439 <a href='https:\/\/en.wikipedia.org\/wiki\/DNS_over_TLS' target='_blank'>DNS-\u043f\u043e\u0432\u0435\u0440\u0445-TLS<\/a>",
|
||||
"example_upstream_doh": "\u0437\u0430\u0448\u0438\u0444\u0440\u043e\u0432\u0430\u043d\u043d\u044b\u0439 <a href='https:\/\/en.wikipedia.org\/wiki\/DNS_over_HTTPS' target='_blank'>DNS-\u043f\u043e\u0432\u0435\u0440\u0445-HTTPS<\/a>",
|
||||
"example_upstream_tcp": "\u043e\u0431\u044b\u0447\u043d\u044b\u0439 DNS (\u043f\u043e\u0432\u0435\u0440\u0445 TCP)",
|
||||
"all_filters_up_to_date_toast": "\u0412\u0441\u0435 \u0444\u0438\u043b\u044c\u0442\u0440\u044b \u043e\u0431\u043d\u043e\u0432\u043b\u0435\u043d\u044b",
|
||||
"updated_upstream_dns_toast": "Upstream DNS-\u0441\u0435\u0440\u0432\u0435\u0440\u044b \u043e\u0431\u043d\u043e\u0432\u043b\u0435\u043d\u044b",
|
||||
"dns_test_ok_toast": "\u0423\u043a\u0430\u0437\u0430\u043d\u043d\u044b\u0435 \u0441\u0435\u0440\u0432\u0435\u0440\u044b DNS \u0440\u0430\u0431\u043e\u0442\u0430\u044e\u0442 \u043a\u043e\u0440\u0440\u0435\u043a\u0442\u043d\u043e",
|
||||
"dns_test_not_ok_toast": "\u0421\u0435\u0440\u0432\u0435\u0440 \"{{key}}\": \u043d\u0435\u0432\u043e\u0437\u043c\u043e\u0436\u043d\u043e \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u044c, \u043f\u0440\u043e\u0432\u0435\u0440\u044c\u0442\u0435 \u043f\u0440\u0430\u0432\u0438\u043b\u044c\u043d\u043e\u0441\u0442\u044c \u043d\u0430\u043f\u0438\u0441\u0430\u043d\u0438\u044f",
|
||||
"unblock_btn": "\u0420\u0430\u0437\u0431\u043b\u043e\u043a\u0438\u0440\u043e\u0432\u0430\u0442\u044c",
|
||||
"block_btn": "\u0417\u0430\u0431\u043b\u043e\u043a\u0438\u0440\u043e\u0432\u0430\u0442\u044c",
|
||||
"time_table_header": "\u0412\u0440\u0435\u043c\u044f",
|
||||
"domain_name_table_header": "\u0414\u043e\u043c\u0435\u043d",
|
||||
"type_table_header": "\u0422\u0438\u043f",
|
||||
"response_table_header": "\u041e\u0442\u0432\u0435\u0442",
|
||||
"empty_response_status": "\u041f\u0443\u0441\u0442\u043e",
|
||||
"show_all_filter_type": "\u041f\u043e\u043a\u0430\u0437\u0430\u0442\u044c \u0432\u0441\u0435",
|
||||
"show_filtered_type": "\u041f\u043e\u043a\u0430\u0437\u0430\u0442\u044c \u043e\u0442\u0444\u0438\u043b\u044c\u0442\u0440\u043e\u0432\u0430\u043d\u043d\u044b\u0435",
|
||||
"no_logs_found": "\u041b\u043e\u0433\u0438 \u043d\u0435 \u043d\u0430\u0439\u0434\u0435\u043d\u044b",
|
||||
"disabled_log_btn": "\u0416\u0443\u0440\u043d\u0430\u043b \u0444\u0438\u043b\u044c\u0442\u0440\u0430\u0446\u0438\u0438 \u0432\u044b\u043a\u043b.",
|
||||
"download_log_file_btn": "\u0417\u0430\u0433\u0440\u0443\u0437\u0438\u0442\u044c \u043e\u0442\u0447\u0451\u0442",
|
||||
"refresh_btn": "\u041e\u0431\u043d\u043e\u0432\u0438\u0442\u044c",
|
||||
"enabled_log_btn": "\u0416\u0443\u0440\u043d\u0430\u043b \u0444\u0438\u043b\u044c\u0442\u0440\u0430\u0446\u0438\u0438 \u0432\u043a\u043b.",
|
||||
"last_dns_queries": "\u041f\u043e\u0441\u043b\u0435\u0434\u043d\u0438\u0435 5000 DNS-\u0437\u0430\u043f\u0440\u043e\u0441\u043e\u0432",
|
||||
"previous_btn": "\u041d\u0430\u0437\u0430\u0434",
|
||||
"next_btn": "\u0412\u043f\u0435\u0440\u0451\u0434",
|
||||
"loading_table_status": "\u0417\u0430\u0433\u0440\u0443\u0437\u043a\u0430...",
|
||||
"page_table_footer_text": "\u0421\u0442\u0440\u0430\u043d\u0438\u0446\u0430",
|
||||
"of_table_footer_text": "\u0438\u0437",
|
||||
"rows_table_footer_text": "\u0441\u0442\u0440\u043e\u043a",
|
||||
"updated_custom_filtering_toast": "\u0412\u043d\u0435\u0441\u0435\u043d\u044b \u0438\u0437\u043c\u0435\u043d\u0435\u043d\u0438\u044f \u0432 \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044c\u0441\u043a\u0438\u0435 \u043f\u0440\u0430\u0432\u0438\u043b\u0430",
|
||||
"rule_removed_from_custom_filtering_toast": "\u041f\u0440\u0430\u0432\u0438\u043b\u043e \u0443\u0434\u0430\u043b\u0435\u043d\u043e \u0438\u0437 \u0430\u0432\u0442\u043e\u0440\u0441\u043a\u043e\u0433\u043e \u0441\u043f\u0438\u0441\u043a\u0430 \u043f\u0440\u0430\u0432\u0438\u043b \u0444\u0438\u043b\u044c\u0442\u0440\u0430\u0446\u0438\u0438",
|
||||
"rule_added_to_custom_filtering_toast": "\u041f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044c\u0441\u043a\u043e\u0435 \u043f\u0440\u0430\u0432\u0438\u043b\u043e \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u043e",
|
||||
"query_log_disabled_toast": "\u0416\u0443\u0440\u043d\u0430\u043b \u0437\u0430\u043f\u0440\u043e\u0441\u043e\u0432 \u0432\u044b\u043a\u043b.",
|
||||
"query_log_enabled_toast": "\u0416\u0443\u0440\u043d\u0430\u043b \u0437\u0430\u043f\u0440\u043e\u0441\u043e\u0432 \u0432\u043a\u043b.",
|
||||
"source_label": "\u0418\u0441\u0442\u043e\u0447\u043d\u0438\u043a",
|
||||
"found_in_known_domain_db": "\u041d\u0430\u0439\u0434\u0435\u043d \u0432 \u0431\u0430\u0437\u0435 \u0438\u0437\u0432\u0435\u0441\u0442\u043d\u044b\u0445 \u0434\u043e\u043c\u0435\u043d\u043e\u0432.",
|
||||
"category_label": "\u041a\u0430\u0442\u0435\u0433\u043e\u0440\u0438\u044f",
|
||||
"rule_label": "\u041f\u0440\u0430\u0432\u0438\u043b\u043e",
|
||||
"filter_label": "\u0424\u0438\u043b\u044c\u0442\u0440"
|
||||
}
|
||||
124
client/src/__locales/sv.json
Normal file
124
client/src/__locales/sv.json
Normal file
@@ -0,0 +1,124 @@
|
||||
{
|
||||
"back": "Tiilbaka",
|
||||
"dashboard": "Kontrollpanel",
|
||||
"settings": "Inst\u00e4llningar",
|
||||
"filters": "Filter",
|
||||
"query_log": "F\u00f6rfr\u00e5gningslogg",
|
||||
"faq": "FAQ",
|
||||
"version": "version",
|
||||
"address": "adress",
|
||||
"on": "P\u00c5",
|
||||
"off": "AV",
|
||||
"copyright": "Copyright",
|
||||
"homepage": "Hemsida",
|
||||
"report_an_issue": "Rapportera ett problem",
|
||||
"enable_protection": "Koppla p\u00e5 skydd",
|
||||
"enabled_protection": "Kopplade p\u00e5 skydd",
|
||||
"disable_protection": "Koppla bort skydd",
|
||||
"disabled_protection": "Kopplade bort skydd",
|
||||
"refresh_statics": "Uppdatera statistik",
|
||||
"dns_query": "DNS-f\u00f6rfr\u00e5gningar",
|
||||
"blocked_by": "Blockerat av",
|
||||
"stats_malware_phishing": "Blockerad skadekod\/phising",
|
||||
"stats_adult": "Blockerade vuxensajter",
|
||||
"stats_query_domain": "Mest efters\u00f6kta dom\u00e4ner",
|
||||
"for_last_24_hours": "under de senaste 24 timamrna",
|
||||
"no_domains_found": "Inga dom\u00e4ner hittade",
|
||||
"requests_count": "F\u00f6rfr\u00e5gningsantal",
|
||||
"top_blocked_domains": "Flest blockerade dom\u00e4ner",
|
||||
"top_clients": "Toppklienter",
|
||||
"no_clients_found": "Inga hitatde klienter",
|
||||
"general_statistics": "Allm\u00e4n statistik",
|
||||
"number_of_dns_query_24_hours": "Ett antal DNS-f\u00f6rfr\u00e5gningar utf\u00f6rdes under de senaste 244 timamrna",
|
||||
"number_of_dns_query_blocked_24_hours": "Ett antal DNS-f\u00f6rfr\u00e5gningar blockerades av annonsfilter och v\u00e4rdens bloceringsklistor",
|
||||
"number_of_dns_query_blocked_24_hours_by_sec": "Ett antal DNS-f\u00f6rfr\u00e5gningar blockerades av AdGuards modul f\u00f6r surfs\u00e4kerhet",
|
||||
"number_of_dns_query_blocked_24_hours_adult": "Ett anta vuxensajter blockerades",
|
||||
"enforced_save_search": "Aktivering av S\u00e4ker surf",
|
||||
"number_of_dns_query_to_safe_search": "Ett antal DNS-f\u00f6rfr\u00e5gningar genomf\u00f6rdes p\u00e5 s\u00f6kmotorer med S\u00e4ker surf aktiverat",
|
||||
"average_processing_time": "Genomsnittlig processtid",
|
||||
"average_processing_time_hint": "Genomsnittlig processtid i millisekunder f\u00f6r DNS-f\u00f6rfr\u00e5gning",
|
||||
"block_domain_use_filters_and_hosts": "Blockera dom\u00e4ner med filter- och v\u00e4rdfiler",
|
||||
"filters_block_toggle_hint": "Du kan st\u00e4lla in egna blockerings regler i <a href='#filters'>Filterinst\u00e4llningar<\/a>.",
|
||||
"use_adguard_browsing_sec": "Amv\u00e4nd AdGuards webbservice f\u00f6r surfs\u00e4kerhet",
|
||||
"use_adguard_browsing_sec_hint": "AdGuard Home kommer att kontrollera om en dom\u00e4n \u00e4r svartlistad i webbservicens surfs\u00e4kerhet. Med en integritetsv\u00e4nlig metod g\u00f6rs en API-lookup f\u00f6r att kontrollera : endast en kort prefix i dom\u00e4nnamnet SHA256 hash skickas till servern.",
|
||||
"use_adguard_parental": "Anv\u00e4nda AdGuards webbservice f\u00f6r f\u00e4r\u00e4ldrakontroll",
|
||||
"use_adguard_parental_hint": "AdGuard Home kommer att kontrollera dom\u00e4ner f\u00f6r inneh\u00e5ll av vuxenmaterial . Samma integritetsv\u00e4nliga metod f\u00f6r API-lookup som till\u00e4mpas i webbservicens surfs\u00e4kerhet anv\u00e4nds.",
|
||||
"enforce_safe_search": "Till\u00e4mpa S\u00e4ker surf",
|
||||
"enforce_save_search_hint": "AdGuard Home kan framtvinga s\u00e4ker surf i f\u00f6ljande s\u00f6kmoterer: Google, Youtube, Bing, och Yandex.",
|
||||
"no_servers_specified": "Inga servrar angivna",
|
||||
"no_settings": "Inga inst\u00e4llningar",
|
||||
"general_settings": "Allm\u00e4nna inst\u00e4llningar",
|
||||
"upstream_dns": "Upstream DNS-servrar",
|
||||
"upstream_dns_hint": "Om du l\u00e5ter f\u00e4ltet vara tomt kommer AdGuard Home att anv\u00e4nda <a href='https:\/\/1.1.1.1\/' target='_blank'>Cloudflare DNS<\/a> f\u00f6r upstream. Anv\u00e4nd tls:\/\/ prefix f\u00f6r DNS \u00f6ver TLS-servrar.",
|
||||
"test_upstream_btn": "Testa uppstr\u00f6mmar",
|
||||
"apply_btn": "Till\u00e4mpa",
|
||||
"disabled_filtering_toast": "Filtrering bortkopplad",
|
||||
"enabled_filtering_toast": "Filtrering inkopplad",
|
||||
"disabled_safe_browsing_toast": "S\u00e4ker surfning bortkopplat",
|
||||
"enabled_safe_browsing_toast": "S\u00e4ker surfning inkopplat",
|
||||
"disabled_parental_toast": "F\u00f6r\u00e4ldrakontroll bortkopplat",
|
||||
"enabled_parental_toast": "F\u00f6r\u00e4ldrakontroll inkopplat",
|
||||
"disabled_safe_search_toast": "S\u00e4ker webbs\u00f6kning bortkopplat",
|
||||
"enabled_save_search_toast": "S\u00e4ker webbs\u00f6kning inkopplat",
|
||||
"enabled_table_header": "Inkopplat",
|
||||
"name_table_header": "Namn",
|
||||
"filter_url_table_header": "Filtrerar URL",
|
||||
"rules_count_table_header": "Regelantal",
|
||||
"last_time_updated_table_header": "Uppdaterades senast",
|
||||
"actions_table_header": "\u00c5tg\u00e4rder",
|
||||
"delete_table_action": "Ta bort",
|
||||
"filters_and_hosts": "Filtrerings- och v\u00e4rdlistor f\u00f6r blockering",
|
||||
"filters_and_hosts_hint": "AdGuard till\u00e4mpar grundl\u00e4ggande annonsblockeringsregler och v\u00e4rdfiltersyntaxer",
|
||||
"no_filters_added": "Inga filter tillagda",
|
||||
"add_filter_btn": "L\u00e4gg till filter",
|
||||
"cancel_btn": "Avbryt",
|
||||
"enter_name_hint": "Skriv in namn",
|
||||
"enter_url_hint": "Skriv in URL",
|
||||
"check_updates_btn": "S\u00f6k efter uppdateringar",
|
||||
"new_filter_btn": "Nytt filterabonemang",
|
||||
"enter_valid_filter_url": "Skriv in en giltigt URL till ett filterabonnemang eller v\u00e4rdfil.",
|
||||
"custom_filter_rules": "Egna filterregler",
|
||||
"custom_filter_rules_hint": "Skriv en regel per rad. Du kan anv\u00e4nda antingen annonsblockeringsregler eller v\u00e4rdfilssyntax.",
|
||||
"examples_title": "Exempel",
|
||||
"example_meaning_filter_block": "blockera \u00e5tkomst till dom\u00e4n example.org domain och alla dess subdom\u00e4ner",
|
||||
"example_meaning_filter_whitelist": "avblockera \u00e5tkomst till dom\u00e4n example.org domain och alla dess subdom\u00e4ner",
|
||||
"example_meaning_host_block": "AdGuard Home kommer nu att returnera adress 127.0.0.1 f\u00f6r dom\u00e4nexemplet example.org (dock utan dess subdom\u00e4ner).",
|
||||
"example_comment": "! H\u00e4r kommer en kommentar",
|
||||
"example_comment_meaning": "Endast en kommentar",
|
||||
"example_comment_hash": "# Ocks\u00e5 en kommentar",
|
||||
"all_filters_up_to_date_toast": "Alla filter \u00e4r redan aktuella",
|
||||
"updated_upstream_dns_toast": "Uppdaterade uppstr\u00f6ms-dns-servrar",
|
||||
"dns_test_ok_toast": "Angivna DNS servrar fungerar korrekt",
|
||||
"dns_test_not_ok_toast": "Server \"{{key}}\": kunde inte anv\u00e4ndas. Var sn\u00e4ll och kolla att du skrivit in r\u00e4tt",
|
||||
"unblock_btn": "Avblockera",
|
||||
"block_btn": "Blockera",
|
||||
"time_table_header": "Tid",
|
||||
"domain_name_table_header": "Dom\u00e4nnamn",
|
||||
"type_table_header": "Typ",
|
||||
"response_table_header": "Svar",
|
||||
"empty_response_status": "Empty",
|
||||
"show_all_filter_type": "Visa alla",
|
||||
"show_filtered_type": "Visa filtrerade",
|
||||
"no_logs_found": "Inga logga funna",
|
||||
"disabled_log_btn": "Koppla bort logg",
|
||||
"download_log_file_btn": "Ladda ner loggfil",
|
||||
"refresh_btn": "L\u00e4s in igen",
|
||||
"enabled_log_btn": "Koppla in logg",
|
||||
"last_dns_queries": "De senaste 5000 DNS-anropen",
|
||||
"previous_btn": "F\u00f6reg\u00e5ende",
|
||||
"next_btn": "N\u00e4sta",
|
||||
"loading_table_status": "L\u00e4ser in...",
|
||||
"page_table_footer_text": "Sida",
|
||||
"of_table_footer_text": "av",
|
||||
"rows_table_footer_text": "rader",
|
||||
"updated_custom_filtering_toast": "Uppdaterade de egna filterreglerna",
|
||||
"rule_removed_from_custom_filtering_toast": "Regel borttagen fr\u00e5n de egna filterreglerna",
|
||||
"rule_added_to_custom_filtering_toast": "Regel tillagd till de egna filterreglerna",
|
||||
"query_log_disabled_toast": "F\u00f6rfr\u00e5gningsloggen bortkopplad",
|
||||
"query_log_enabled_toast": "F\u00f6rfr\u00e5gningsloggen inkopplad",
|
||||
"source_label": "K\u00e4lla",
|
||||
"found_in_known_domain_db": "Hittad i dom\u00e4ndatabas.",
|
||||
"category_label": "Kategori",
|
||||
"rule_label": "Regel",
|
||||
"filter_label": "Filter"
|
||||
}
|
||||
124
client/src/__locales/vi.json
Normal file
124
client/src/__locales/vi.json
Normal file
@@ -0,0 +1,124 @@
|
||||
{
|
||||
"back": "Quay l\u1ea1i",
|
||||
"dashboard": "T\u1ed5ng quan",
|
||||
"settings": "C\u00e0i \u0111\u1eb7t",
|
||||
"filters": "B\u1ed9 l\u1ecdc",
|
||||
"query_log": "L\u1ecbch s\u1eed truy v\u1ea5n",
|
||||
"faq": "H\u1ecfi \u0111\u00e1p",
|
||||
"version": "phi\u00ean b\u1ea3n",
|
||||
"address": "\u0111\u1ecba ch\u1ec9",
|
||||
"on": "\u0110ang b\u1eadt",
|
||||
"off": "\u0110ang t\u1eaft",
|
||||
"copyright": "B\u1ea3n quy\u1ec1n",
|
||||
"homepage": "Trang ch\u1ee7",
|
||||
"report_an_issue": "B\u00e1o l\u1ed7i",
|
||||
"enable_protection": "B\u1eadt b\u1ea3o v\u1ec7",
|
||||
"enabled_protection": "\u0110\u00e3 b\u1eadt b\u1ea3o v\u1ec7",
|
||||
"disable_protection": "T\u1eaft b\u1ea3o v\u1ec7",
|
||||
"disabled_protection": "\u0110\u00e3 t\u1eaft b\u1ea3o v\u1ec7",
|
||||
"refresh_statics": "L\u00e0m m\u1edbi th\u1ed1ng k\u00ea",
|
||||
"dns_query": "Truy v\u1ea5n DNS",
|
||||
"blocked_by": "Ch\u1eb7n b\u1edfi",
|
||||
"stats_malware_phishing": "M\u00e3 \u0111\u1ed9c\/l\u1eeba \u0111\u1ea3o \u0111\u00e3 ch\u1eb7n",
|
||||
"stats_adult": "Website ng\u01b0\u1eddi l\u1edbn \u0111\u00e3 ch\u1eb7n",
|
||||
"stats_query_domain": "T\u00ean mi\u1ec1n truy v\u1ea5n nhi\u1ec1u",
|
||||
"for_last_24_hours": "trong 24 gi\u1edd qua",
|
||||
"no_domains_found": "Kh\u00f4ng c\u00f3 t\u00ean mi\u1ec1n n\u00e0o",
|
||||
"requests_count": "S\u1ed1 l\u1ea7n y\u00eau c\u1ea7u",
|
||||
"top_blocked_domains": "T\u00ean mi\u1ec1n ch\u1eb7n nhi\u1ec1u",
|
||||
"top_clients": "Client d\u00f9ng nhi\u1ec1u",
|
||||
"no_clients_found": "Kh\u00f4ng c\u00f3 client n\u00e0o",
|
||||
"general_statistics": "Th\u1ed1ng k\u00ea chung",
|
||||
"number_of_dns_query_24_hours": "S\u1ed1 y\u00eau c\u1ea7u DNS \u0111\u00e3 x\u1eed l\u00fd trong 24 gi\u1edd qua",
|
||||
"number_of_dns_query_blocked_24_hours": "S\u1ed1 y\u00eau c\u1ea7u DNS b\u1ecb ch\u1eb7n b\u1edfi b\u1ed9 l\u1ecdc qu\u1ea3ng c\u00e1o v\u00e0 danh s\u00e1ch ch\u1eb7n host",
|
||||
"number_of_dns_query_blocked_24_hours_by_sec": "S\u1ed1 y\u00eau c\u1ea7u DNS b\u1ecb ch\u1eb7n b\u1edfi ch\u1ebf \u0111\u1ed9 b\u1ea3o v\u1ec7 duy\u1ec7t web AdGuard",
|
||||
"number_of_dns_query_blocked_24_hours_adult": "S\u1ed1 website ng\u01b0\u1eddi l\u1edbn \u0111\u00e3 ch\u1eb7n",
|
||||
"enforced_save_search": "T\u00ecm ki\u1ebfm an to\u00e0n",
|
||||
"number_of_dns_query_to_safe_search": "S\u1ed1 y\u00eau c\u1ea7u DNS t\u1edbi c\u00f4ng c\u1ee5 t\u00ecm ki\u1ebfm \u0111\u00e3 chuy\u1ec3n th\u00e0nh t\u00ecm ki\u1ebfm an to\u00e0n",
|
||||
"average_processing_time": "Th\u1eddi gian x\u1eed l\u00fd trung b\u00ecnh",
|
||||
"average_processing_time_hint": "Th\u1eddi gian trung b\u00ecnh cho m\u1ed9t y\u00eau c\u1ea7u DNS t\u00ednh b\u1eb1ng mili gi\u00e2y",
|
||||
"block_domain_use_filters_and_hosts": "Ch\u1eb7n t\u00ean mi\u1ec1n s\u1eed d\u1ee5ng c\u00e1c b\u1ed9 l\u1ecdc v\u00e0 file hosts",
|
||||
"filters_block_toggle_hint": "B\u1ea1n c\u00f3 th\u1ec3 thi\u1ebft l\u1eadp quy t\u1eafc ch\u1eb7n t\u1ea1i c\u00e0i \u0111\u1eb7t <a href='#filters'>B\u1ed9 l\u1ecdc<\/a>.",
|
||||
"use_adguard_browsing_sec": "S\u1eed d\u1ee5ng d\u1ecbch v\u1ee5 b\u1ea3o v\u1ec7 duy\u1ec7t web AdGuard",
|
||||
"use_adguard_browsing_sec_hint": "AdGuard Home s\u1ebd ki\u1ec3m tra t\u00ean mi\u1ec1n v\u1edbi d\u1ecbch v\u1ee5 b\u1ea3o v\u1ec7 duy\u1ec7t web. T\u00ednh n\u0103ng s\u1eed d\u1ee5ng m\u1ed9t API th\u00e2n thi\u1ec7n v\u1edbi quy\u1ec1n ri\u00eang t\u01b0: ch\u1ec9 m\u1ed9t ph\u1ea7n ng\u1eafn ti\u1ec1n t\u1ed1 m\u00e3 b\u0103m SHA256 \u0111\u01b0\u1ee3c g\u1eedi \u0111\u1ebfn m\u00e1y ch\u1ee7",
|
||||
"use_adguard_parental": "S\u1eed d\u1ee5ng d\u1ecbch v\u1ee5 qu\u1ea3n l\u00fd c\u1ee7a ph\u1ee5 huynh AdGuard",
|
||||
"use_adguard_parental_hint": "AdGuard Home s\u1ebd ki\u1ec3m tra n\u1ebfu t\u00ean mi\u1ec1n ch\u1ee9a t\u1eeb kho\u00e1 ng\u01b0\u1eddi l\u1edbn. T\u00ednh n\u0103ng s\u1eed d\u1ee5ng API th\u00e2n thi\u1ec7n v\u1edbi quy\u1ec1n ri\u00eang t\u01b0 t\u01b0\u01a1ng t\u1ef1 v\u1edbi d\u1ecbch v\u1ee5 b\u1ea3o v\u1ec7 duy\u1ec7t web",
|
||||
"enforce_safe_search": "B\u1eaft bu\u1ed9c t\u00ecm ki\u1ebfm an to\u00e0n",
|
||||
"enforce_save_search_hint": "AdGuard Home c\u00f3 th\u1ec3 b\u1eaft bu\u1ed9c t\u00ecm ki\u1ebfm an to\u00e0n v\u1edbi c\u00e1c d\u1ecbch v\u1ee5 t\u00ecm ki\u1ebfm: Google, Youtube, Bing, Yandex.",
|
||||
"no_servers_specified": "Kh\u00f4ng c\u00f3 m\u00e1y ch\u1ee7 n\u00e0o \u0111\u01b0\u1ee3c li\u1ec7t k\u00ea",
|
||||
"no_settings": "Kh\u00f4ng c\u00f3 c\u00e0i \u0111\u1eb7t n\u00e0o",
|
||||
"general_settings": "C\u00e0i \u0111\u1eb7t chung",
|
||||
"upstream_dns": "M\u00e1y ch\u1ee7 DNS t\u00ecm ki\u1ebfm",
|
||||
"upstream_dns_hint": "N\u1ebfu b\u1ea1n \u0111\u1ec3 tr\u1ed1ng m\u1ee5c n\u00e0y, AdGuard Home s\u1ebd s\u1eed d\u1ee5ng <a href='https:\/\/1.1.1.1\/' target='_blank'>Cloudflare DNS<\/a> \u0111\u1ec3 t\u00ecm ki\u1ebfm. S\u1eed d\u1ee5ng ti\u1ec1n t\u1ed1 tls:\/\/ cho c\u00e1c m\u00e1y ch\u1ee7 DNS d\u1ef1a tr\u00ean TLS.",
|
||||
"test_upstream_btn": "Ki\u1ec3m tra",
|
||||
"apply_btn": "\u00c1p d\u1ee5ng",
|
||||
"disabled_filtering_toast": "\u0110\u00e3 t\u1eaft ch\u1eb7n qu\u1ea3ng c\u00e1o",
|
||||
"enabled_filtering_toast": "\u0110\u00e3 b\u1eadt ch\u1eb7n qu\u1ea3ng c\u00e1o",
|
||||
"disabled_safe_browsing_toast": "\u0110\u00e3 t\u1eaft b\u1ea3o v\u1ec7 duy\u1ec7t web",
|
||||
"enabled_safe_browsing_toast": "\u0110\u00e3 b\u1eadt b\u1ea3o v\u1ec7 duy\u1ec7t web",
|
||||
"disabled_parental_toast": "\u0110\u00e3 t\u1eaft qu\u1ea3n l\u00fd c\u1ee7a ph\u1ee5 huynh",
|
||||
"enabled_parental_toast": "\u0110\u00e3 b\u1eadt qu\u1ea3n l\u00fd c\u1ee7a ph\u1ee5 huynh",
|
||||
"disabled_safe_search_toast": "\u0110\u00e3 t\u1eaft t\u00ecm ki\u1ebfm an to\u00e0n",
|
||||
"enabled_save_search_toast": "\u0110\u00e3 b\u1eadt t\u00ecm ki\u1ebfm an to\u00e0n",
|
||||
"enabled_table_header": "K\u00edch ho\u1ea1t",
|
||||
"name_table_header": "T\u00ean",
|
||||
"filter_url_table_header": "URL b\u1ed9 l\u1ecdc",
|
||||
"rules_count_table_header": "S\u1ed1 quy t\u1eafc",
|
||||
"last_time_updated_table_header": "C\u1eadp nh\u1eadt cu\u1ed1i",
|
||||
"actions_table_header": "Thao t\u00e1c",
|
||||
"delete_table_action": "Xo\u00e1",
|
||||
"filters_and_hosts": "Danh s\u00e1ch b\u1ed9 l\u1ecdc v\u00e0 hosts",
|
||||
"filters_and_hosts_hint": "AdGuard home hi\u1ec3u c\u00e1c quy t\u1eafc ch\u1eb7n qu\u1ea3ng c\u00e1o \u0111\u01a1n gi\u1ea3n v\u00e0 c\u00fa ph\u00e1p file hosts",
|
||||
"no_filters_added": "Kh\u00f4ng c\u00f3 b\u1ed9 l\u1ecdc n\u00e0o \u0111\u01b0\u1ee3c th\u00eam",
|
||||
"add_filter_btn": "Th\u00eam b\u1ed9 l\u1ecdc",
|
||||
"cancel_btn": "Hu\u1ef7",
|
||||
"enter_name_hint": "Nh\u1eadp t\u00ean",
|
||||
"enter_url_hint": "Nh\u1eadp URL",
|
||||
"check_updates_btn": "Ki\u1ec3m tra c\u1eadp nh\u1eadt",
|
||||
"new_filter_btn": "\u0110\u0103ng k\u00fd b\u1ed9 l\u1ecdc m\u1edbi",
|
||||
"enter_valid_filter_url": "Nh\u1eadp URL h\u1ee3p l\u1ec7 c\u1ee7a b\u1ed9 l\u1ecdc ho\u1eb7c file hosts",
|
||||
"custom_filter_rules": "Quy t\u1eafc l\u1ecdc tu\u1ef3 ch\u1ec9nh",
|
||||
"custom_filter_rules_hint": "Nh\u1eadp m\u1ed7i quy t\u1eafc 1 d\u00f2ng. C\u00f3 th\u1ec3 s\u1eed d\u1ee5ng quy t\u1eafc ch\u1eb7n qu\u1ea3ng c\u00e1o ho\u1eb7c c\u00fa ph\u00e1p file host",
|
||||
"examples_title": "V\u00ed d\u1ee5",
|
||||
"example_meaning_filter_block": "Ch\u1eb7n truy c\u1eadp t\u1edbi t\u00ean mi\u1ec1n example.org v\u00e0 t\u1ea5t c\u1ea3 t\u00ean mi\u1ec1n con",
|
||||
"example_meaning_filter_whitelist": "Kh\u00f4ng ch\u1eb7n truy c\u1eadp t\u1edbi t\u00ean mi\u1ec1n example.org v\u00e0 t\u1ea5t c\u1ea3 t\u00ean mi\u1ec1n con",
|
||||
"example_meaning_host_block": "AdGuard Home s\u1ebd ph\u1ea3n h\u1ed3i \u0111\u1ecba ch\u1ec9 IP 127.0.0.1 cho t\u00ean mi\u1ec1n example.org (kh\u00f4ng \u00e1p d\u1ee5ng t\u00ean mi\u1ec1n con)",
|
||||
"example_comment": "! \u0110\u00e2y l\u00e0 m\u1ed9t ch\u00fa th\u00edch",
|
||||
"example_comment_meaning": "Ch\u1ec9 l\u00e0 m\u1ed9t ch\u00fa th\u00edch",
|
||||
"example_comment_hash": "# C\u0169ng l\u00e0 m\u1ed9t ch\u00fa th\u00edch",
|
||||
"all_filters_up_to_date_toast": "T\u1ea5t c\u1ea3 b\u1ed9 l\u1ecdc \u0111\u00e3 \u0111\u01b0\u1ee3c c\u1eadp nh\u1eadt",
|
||||
"updated_upstream_dns_toast": "\u0110\u00e3 c\u1eadp nh\u1eadt m\u00e1y ch\u1ee7 DNS t\u00ecm ki\u1ebfm",
|
||||
"dns_test_ok_toast": "M\u00e1y ch\u1ee7 DNS c\u00f3 th\u1ec3 s\u1eed d\u1ee5ng",
|
||||
"dns_test_not_ok_toast": "M\u00e1y ch\u1ee7 '{{key}}': kh\u00f4ng th\u1ec3 s\u1eed d\u1ee5ng, vui l\u00f2ng ki\u1ec3m tra b\u1ea1n \u0111\u00e3 \u0111i\u1ec1n ch\u00ednh x\u00e1c",
|
||||
"unblock_btn": "B\u1ecf ch\u1eb7n",
|
||||
"block_btn": "Ch\u1eb7n",
|
||||
"time_table_header": "Th\u1eddi gian",
|
||||
"domain_name_table_header": "T\u00ean mi\u1ec1n",
|
||||
"type_table_header": "Lo\u1ea1i",
|
||||
"response_table_header": "Ph\u1ea3n h\u1ed3i",
|
||||
"empty_response_status": "R\u1ed7ng",
|
||||
"show_all_filter_type": "Hi\u1ec7n t\u1ea5t c\u1ea3",
|
||||
"show_filtered_type": "Ch\u1ec9 hi\u1ec7n \u0111\u00e3 ch\u1eb7n",
|
||||
"no_logs_found": "Kh\u00f4ng c\u00f3 l\u1ecbch s\u1eed truy v\u1ea5n",
|
||||
"disabled_log_btn": "T\u1eaft l\u1ecbch s\u1eed truy v\u1ea5n",
|
||||
"download_log_file_btn": "T\u1ea3i t\u1eadp tin l\u1ecbch s\u1eed truy v\u1ea5n",
|
||||
"refresh_btn": "L\u00e0m m\u1edbi",
|
||||
"enabled_log_btn": "B\u1eadt l\u1ecbch s\u1eed truy v\u1ea5n",
|
||||
"last_dns_queries": "5000 truy v\u1ea5n DNS g\u1ea7n nh\u1ea5t",
|
||||
"previous_btn": "Trang tr\u01b0\u1edbc",
|
||||
"next_btn": "Trang sau",
|
||||
"loading_table_status": "\u0110ang t\u1ea3i...",
|
||||
"page_table_footer_text": "Trang",
|
||||
"of_table_footer_text": "c\u1ee7a",
|
||||
"rows_table_footer_text": "h\u00e0ng",
|
||||
"updated_custom_filtering_toast": "\u0110\u00e3 c\u1eadp nh\u1eadt quy t\u1eafc l\u1ecdc tu\u1ef3 ch\u1ec9nh",
|
||||
"rule_removed_from_custom_filtering_toast": "Quy t\u1eafc \u0111\u00e3 \u0111\u01b0\u1ee3c xo\u00e1 kh\u1ecfi quy t\u1eafc l\u1ecdc tu\u1ef3 ch\u1ec9nh",
|
||||
"rule_added_to_custom_filtering_toast": "Quy t\u1eafc \u0111\u00e3 \u0111\u01b0\u1ee3c th\u00eam v\u00e0o quy t\u1eafc l\u1ecdc tu\u1ef3 ch\u1ec9nh",
|
||||
"query_log_disabled_toast": "\u0110\u00e3 t\u1eaft l\u1ecbch s\u1eed truy v\u1ea5n",
|
||||
"query_log_enabled_toast": "\u0110\u00e3 b\u1eadt l\u1ecbch s\u1eed truy v\u1ea5n",
|
||||
"source_label": "Ngu\u1ed3n",
|
||||
"found_in_known_domain_db": "T\u00ecm th\u1ea5y trong c\u01a1 s\u1edf d\u1eef li\u1ec7u t\u00ean mi\u1ec1n",
|
||||
"category_label": "Th\u1ec3 lo\u1ea1i",
|
||||
"rule_label": "Quy t\u1eafc",
|
||||
"filter_label": "B\u1ed9 l\u1ecdc"
|
||||
}
|
||||
@@ -1,50 +1,71 @@
|
||||
import { createAction } from 'redux-actions';
|
||||
import round from 'lodash/round';
|
||||
import { t } from 'i18next';
|
||||
import { showLoading, hideLoading } from 'react-redux-loading-bar';
|
||||
|
||||
import { normalizeHistory, normalizeFilteringStatus } from '../helpers/helpers';
|
||||
import { normalizeHistory, normalizeFilteringStatus, normalizeLogs } from '../helpers/helpers';
|
||||
import Api from '../api/Api';
|
||||
|
||||
const apiClient = new Api();
|
||||
|
||||
export const addErrorToast = createAction('ADD_ERROR_TOAST');
|
||||
export const addSuccessToast = createAction('ADD_SUCCESS_TOAST');
|
||||
export const removeToast = createAction('REMOVE_TOAST');
|
||||
|
||||
export const toggleSettingStatus = createAction('SETTING_STATUS_TOGGLE');
|
||||
export const showSettingsFailure = createAction('SETTINGS_FAILURE_SHOW');
|
||||
|
||||
export const toggleSetting = (settingKey, status) => async (dispatch) => {
|
||||
switch (settingKey) {
|
||||
case 'filtering':
|
||||
if (status) {
|
||||
await apiClient.disableFiltering();
|
||||
} else {
|
||||
await apiClient.enableFiltering();
|
||||
}
|
||||
dispatch(toggleSettingStatus({ settingKey }));
|
||||
break;
|
||||
case 'safebrowsing':
|
||||
if (status) {
|
||||
await apiClient.disableSafebrowsing();
|
||||
} else {
|
||||
await apiClient.enableSafebrowsing();
|
||||
}
|
||||
dispatch(toggleSettingStatus({ settingKey }));
|
||||
break;
|
||||
case 'parental':
|
||||
if (status) {
|
||||
await apiClient.disableParentalControl();
|
||||
} else {
|
||||
await apiClient.enableParentalControl();
|
||||
}
|
||||
dispatch(toggleSettingStatus({ settingKey }));
|
||||
break;
|
||||
case 'safesearch':
|
||||
if (status) {
|
||||
await apiClient.disableSafesearch();
|
||||
} else {
|
||||
await apiClient.enableSafesearch();
|
||||
}
|
||||
dispatch(toggleSettingStatus({ settingKey }));
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
let successMessage = '';
|
||||
try {
|
||||
// TODO move setting keys to constants
|
||||
switch (settingKey) {
|
||||
case 'filtering':
|
||||
if (status) {
|
||||
successMessage = 'disabled_filtering_toast';
|
||||
await apiClient.disableFiltering();
|
||||
} else {
|
||||
successMessage = 'enabled_filtering_toast';
|
||||
await apiClient.enableFiltering();
|
||||
}
|
||||
dispatch(toggleSettingStatus({ settingKey }));
|
||||
break;
|
||||
case 'safebrowsing':
|
||||
if (status) {
|
||||
successMessage = 'disabled_safe_browsing_toast';
|
||||
await apiClient.disableSafebrowsing();
|
||||
} else {
|
||||
successMessage = 'enabled_safe_browsing_toast';
|
||||
await apiClient.enableSafebrowsing();
|
||||
}
|
||||
dispatch(toggleSettingStatus({ settingKey }));
|
||||
break;
|
||||
case 'parental':
|
||||
if (status) {
|
||||
successMessage = 'disabled_parental_toast';
|
||||
await apiClient.disableParentalControl();
|
||||
} else {
|
||||
successMessage = 'enabled_parental_toast';
|
||||
await apiClient.enableParentalControl();
|
||||
}
|
||||
dispatch(toggleSettingStatus({ settingKey }));
|
||||
break;
|
||||
case 'safesearch':
|
||||
if (status) {
|
||||
successMessage = 'disabled_safe_search_toast';
|
||||
await apiClient.disableSafesearch();
|
||||
} else {
|
||||
successMessage = 'enabled_save_search_toast';
|
||||
await apiClient.enableSafesearch();
|
||||
}
|
||||
dispatch(toggleSettingStatus({ settingKey }));
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
dispatch(addSuccessToast(successMessage));
|
||||
} catch (error) {
|
||||
dispatch(addErrorToast({ error }));
|
||||
}
|
||||
};
|
||||
|
||||
@@ -73,11 +94,51 @@ export const initSettings = settingsList => async (dispatch) => {
|
||||
};
|
||||
dispatch(initSettingsSuccess({ settingsList: newSettingsList }));
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
dispatch(addErrorToast({ error }));
|
||||
dispatch(initSettingsFailure());
|
||||
}
|
||||
};
|
||||
|
||||
export const getFilteringRequest = createAction('GET_FILTERING_REQUEST');
|
||||
export const getFilteringFailure = createAction('GET_FILTERING_FAILURE');
|
||||
export const getFilteringSuccess = createAction('GET_FILTERING_SUCCESS');
|
||||
|
||||
export const getFiltering = () => async (dispatch) => {
|
||||
dispatch(getFilteringRequest());
|
||||
try {
|
||||
const filteringStatus = await apiClient.getFilteringStatus();
|
||||
dispatch(getFilteringSuccess(filteringStatus.enabled));
|
||||
} catch (error) {
|
||||
dispatch(addErrorToast({ error }));
|
||||
dispatch(getFilteringFailure());
|
||||
}
|
||||
};
|
||||
|
||||
export const toggleProtectionRequest = createAction('TOGGLE_PROTECTION_REQUEST');
|
||||
export const toggleProtectionFailure = createAction('TOGGLE_PROTECTION_FAILURE');
|
||||
export const toggleProtectionSuccess = createAction('TOGGLE_PROTECTION_SUCCESS');
|
||||
|
||||
export const toggleProtection = status => async (dispatch) => {
|
||||
dispatch(toggleProtectionRequest());
|
||||
let successMessage = '';
|
||||
|
||||
try {
|
||||
if (status) {
|
||||
successMessage = 'disabled_protection';
|
||||
await apiClient.disableGlobalProtection();
|
||||
} else {
|
||||
successMessage = 'enabled_protection';
|
||||
await apiClient.enableGlobalProtection();
|
||||
}
|
||||
|
||||
dispatch(addSuccessToast(successMessage));
|
||||
dispatch(toggleProtectionSuccess());
|
||||
} catch (error) {
|
||||
dispatch(addErrorToast({ error }));
|
||||
dispatch(toggleProtectionFailure());
|
||||
}
|
||||
};
|
||||
|
||||
export const dnsStatusRequest = createAction('DNS_STATUS_REQUEST');
|
||||
export const dnsStatusFailure = createAction('DNS_STATUS_FAILURE');
|
||||
export const dnsStatusSuccess = createAction('DNS_STATUS_SUCCESS');
|
||||
@@ -88,7 +149,7 @@ export const getDnsStatus = () => async (dispatch) => {
|
||||
const dnsStatus = await apiClient.getGlobalStatus();
|
||||
dispatch(dnsStatusSuccess(dnsStatus));
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
dispatch(addErrorToast({ error }));
|
||||
dispatch(initSettingsFailure());
|
||||
}
|
||||
};
|
||||
@@ -103,7 +164,7 @@ export const enableDns = () => async (dispatch) => {
|
||||
await apiClient.startGlobalFiltering();
|
||||
dispatch(enableDnsSuccess());
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
dispatch(addErrorToast({ error }));
|
||||
dispatch(enableDnsFailure());
|
||||
}
|
||||
};
|
||||
@@ -118,8 +179,8 @@ export const disableDns = () => async (dispatch) => {
|
||||
await apiClient.stopGlobalFiltering();
|
||||
dispatch(disableDnsSuccess());
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
dispatch(disableDnsFailure());
|
||||
dispatch(disableDnsFailure(error));
|
||||
dispatch(addErrorToast({ error }));
|
||||
}
|
||||
};
|
||||
|
||||
@@ -139,30 +200,45 @@ export const getStats = () => async (dispatch) => {
|
||||
|
||||
dispatch(getStatsSuccess(processedStats));
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
dispatch(addErrorToast({ error }));
|
||||
dispatch(getStatsFailure());
|
||||
}
|
||||
};
|
||||
|
||||
export const getVersionRequest = createAction('GET_VERSION_REQUEST');
|
||||
export const getVersionFailure = createAction('GET_VERSION_FAILURE');
|
||||
export const getVersionSuccess = createAction('GET_VERSION_SUCCESS');
|
||||
|
||||
export const getVersion = () => async (dispatch) => {
|
||||
dispatch(getVersionRequest());
|
||||
try {
|
||||
const newVersion = await apiClient.getGlobalVersion();
|
||||
dispatch(getVersionSuccess(newVersion));
|
||||
} catch (error) {
|
||||
dispatch(addErrorToast({ error }));
|
||||
dispatch(getVersionFailure());
|
||||
}
|
||||
};
|
||||
|
||||
export const getTopStatsRequest = createAction('GET_TOP_STATS_REQUEST');
|
||||
export const getTopStatsFailure = createAction('GET_TOP_STATS_FAILURE');
|
||||
export const getTopStatsSuccess = createAction('GET_TOP_STATS_SUCCESS');
|
||||
|
||||
export const getTopStats = () => async (dispatch, getState) => {
|
||||
dispatch(getTopStatsRequest());
|
||||
try {
|
||||
const timer = setInterval(async () => {
|
||||
const state = getState();
|
||||
const timer = setInterval(async () => {
|
||||
if (state.dashboard.isCoreRunning) {
|
||||
if (state.dashboard.isCoreRunning) {
|
||||
clearInterval(timer);
|
||||
try {
|
||||
const stats = await apiClient.getGlobalStatsTop();
|
||||
dispatch(getTopStatsSuccess(stats));
|
||||
clearInterval(timer);
|
||||
} catch (error) {
|
||||
dispatch(addErrorToast({ error }));
|
||||
dispatch(getTopStatsFailure(error));
|
||||
}
|
||||
}, 100);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
dispatch(getTopStatsFailure());
|
||||
}
|
||||
}
|
||||
}, 100);
|
||||
};
|
||||
|
||||
export const getLogsRequest = createAction('GET_LOGS_REQUEST');
|
||||
@@ -171,19 +247,19 @@ export const getLogsSuccess = createAction('GET_LOGS_SUCCESS');
|
||||
|
||||
export const getLogs = () => async (dispatch, getState) => {
|
||||
dispatch(getLogsRequest());
|
||||
try {
|
||||
const timer = setInterval(async () => {
|
||||
const state = getState();
|
||||
const timer = setInterval(async () => {
|
||||
if (state.dashboard.isCoreRunning) {
|
||||
const logs = await apiClient.getQueryLog();
|
||||
if (state.dashboard.isCoreRunning) {
|
||||
clearInterval(timer);
|
||||
try {
|
||||
const logs = normalizeLogs(await apiClient.getQueryLog());
|
||||
dispatch(getLogsSuccess(logs));
|
||||
clearInterval(timer);
|
||||
} catch (error) {
|
||||
dispatch(addErrorToast({ error }));
|
||||
dispatch(getLogsFailure(error));
|
||||
}
|
||||
}, 100);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
dispatch(getLogsFailure());
|
||||
}
|
||||
}
|
||||
}, 100);
|
||||
};
|
||||
|
||||
export const toggleLogStatusRequest = createAction('TOGGLE_LOGS_REQUEST');
|
||||
@@ -193,16 +269,20 @@ export const toggleLogStatusSuccess = createAction('TOGGLE_LOGS_SUCCESS');
|
||||
export const toggleLogStatus = queryLogEnabled => async (dispatch) => {
|
||||
dispatch(toggleLogStatusRequest());
|
||||
let toggleMethod;
|
||||
let successMessage;
|
||||
if (queryLogEnabled) {
|
||||
toggleMethod = apiClient.disableQueryLog.bind(apiClient);
|
||||
successMessage = 'query_log_disabled_toast';
|
||||
} else {
|
||||
toggleMethod = apiClient.enableQueryLog.bind(apiClient);
|
||||
successMessage = 'query_log_enabled_toast';
|
||||
}
|
||||
try {
|
||||
await toggleMethod();
|
||||
dispatch(addSuccessToast(successMessage));
|
||||
dispatch(toggleLogStatusSuccess());
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
dispatch(addErrorToast({ error }));
|
||||
dispatch(toggleLogStatusFailure());
|
||||
}
|
||||
};
|
||||
@@ -214,10 +294,14 @@ export const setRulesSuccess = createAction('SET_RULES_SUCCESS');
|
||||
export const setRules = rules => async (dispatch) => {
|
||||
dispatch(setRulesRequest());
|
||||
try {
|
||||
await apiClient.setRules(rules);
|
||||
const replacedLineEndings = rules
|
||||
.replace(/^\n/g, '')
|
||||
.replace(/\n\s*\n/g, '\n');
|
||||
await apiClient.setRules(replacedLineEndings);
|
||||
dispatch(addSuccessToast('updated_custom_filtering_toast'));
|
||||
dispatch(setRulesSuccess());
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
dispatch(addErrorToast({ error }));
|
||||
dispatch(setRulesFailure());
|
||||
}
|
||||
};
|
||||
@@ -232,7 +316,7 @@ export const getFilteringStatus = () => async (dispatch) => {
|
||||
const status = await apiClient.getFilteringStatus();
|
||||
dispatch(getFilteringStatusSuccess({ status: normalizeFilteringStatus(status) }));
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
dispatch(addErrorToast({ error }));
|
||||
dispatch(getFilteringStatusFailure());
|
||||
}
|
||||
};
|
||||
@@ -258,7 +342,7 @@ export const toggleFilterStatus = url => async (dispatch, getState) => {
|
||||
dispatch(toggleFilterSuccess(url));
|
||||
dispatch(getFilteringStatus());
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
dispatch(addErrorToast({ error }));
|
||||
dispatch(toggleFilterFailure());
|
||||
}
|
||||
};
|
||||
@@ -269,13 +353,27 @@ export const refreshFiltersSuccess = createAction('FILTERING_REFRESH_SUCCESS');
|
||||
|
||||
export const refreshFilters = () => async (dispatch) => {
|
||||
dispatch(refreshFiltersRequest);
|
||||
dispatch(showLoading());
|
||||
try {
|
||||
await apiClient.refreshFilters();
|
||||
const refreshText = await apiClient.refreshFilters();
|
||||
dispatch(refreshFiltersSuccess);
|
||||
|
||||
if (refreshText.includes('OK')) {
|
||||
if (refreshText.includes('OK 0')) {
|
||||
dispatch(addSuccessToast('all_filters_up_to_date_toast'));
|
||||
} else {
|
||||
dispatch(addSuccessToast(refreshText.replace(/OK /g, '')));
|
||||
}
|
||||
} else {
|
||||
dispatch(addErrorToast({ error: refreshText }));
|
||||
}
|
||||
|
||||
dispatch(getFilteringStatus());
|
||||
dispatch(hideLoading());
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
dispatch(addErrorToast({ error }));
|
||||
dispatch(refreshFiltersFailure());
|
||||
dispatch(hideLoading());
|
||||
}
|
||||
};
|
||||
|
||||
@@ -292,7 +390,7 @@ export const getStatsHistory = () => async (dispatch) => {
|
||||
const normalizedHistory = normalizeHistory(statsHistory);
|
||||
dispatch(getStatsHistorySuccess(normalizedHistory));
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
dispatch(addErrorToast({ error }));
|
||||
dispatch(getStatsHistoryFailure());
|
||||
}
|
||||
};
|
||||
@@ -301,14 +399,14 @@ export const addFilterRequest = createAction('ADD_FILTER_REQUEST');
|
||||
export const addFilterFailure = createAction('ADD_FILTER_FAILURE');
|
||||
export const addFilterSuccess = createAction('ADD_FILTER_SUCCESS');
|
||||
|
||||
export const addFilter = url => async (dispatch) => {
|
||||
export const addFilter = (url, name) => async (dispatch) => {
|
||||
dispatch(addFilterRequest());
|
||||
try {
|
||||
await apiClient.addFilter(url);
|
||||
await apiClient.addFilter(url, name);
|
||||
dispatch(addFilterSuccess(url));
|
||||
dispatch(getFilteringStatus());
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
dispatch(addErrorToast({ error }));
|
||||
dispatch(addFilterFailure());
|
||||
}
|
||||
};
|
||||
@@ -325,7 +423,7 @@ export const removeFilter = url => async (dispatch) => {
|
||||
dispatch(removeFilterSuccess(url));
|
||||
dispatch(getFilteringStatus());
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
dispatch(addErrorToast({ error }));
|
||||
dispatch(removeFilterFailure());
|
||||
}
|
||||
};
|
||||
@@ -344,7 +442,7 @@ export const downloadQueryLog = () => async (dispatch) => {
|
||||
data = await apiClient.downloadQueryLog();
|
||||
dispatch(downloadQueryLogSuccess());
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
dispatch(addErrorToast({ error }));
|
||||
dispatch(downloadQueryLogFailure());
|
||||
}
|
||||
return data;
|
||||
@@ -359,9 +457,68 @@ export const setUpstream = url => async (dispatch) => {
|
||||
dispatch(setUpstreamRequest());
|
||||
try {
|
||||
await apiClient.setUpstream(url);
|
||||
dispatch(addSuccessToast('updated_upstream_dns_toast'));
|
||||
dispatch(setUpstreamSuccess());
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
dispatch(addErrorToast({ error }));
|
||||
dispatch(setUpstreamFailure());
|
||||
}
|
||||
};
|
||||
|
||||
export const testUpstreamRequest = createAction('TEST_UPSTREAM_REQUEST');
|
||||
export const testUpstreamFailure = createAction('TEST_UPSTREAM_FAILURE');
|
||||
export const testUpstreamSuccess = createAction('TEST_UPSTREAM_SUCCESS');
|
||||
|
||||
export const testUpstream = servers => async (dispatch) => {
|
||||
dispatch(testUpstreamRequest());
|
||||
try {
|
||||
const upstreamResponse = await apiClient.testUpstream(servers);
|
||||
|
||||
const testMessages = Object.keys(upstreamResponse).map((key) => {
|
||||
const message = upstreamResponse[key];
|
||||
if (message !== 'OK') {
|
||||
dispatch(addErrorToast({ error: t('dns_test_not_ok_toast', { key }) }));
|
||||
}
|
||||
return message;
|
||||
});
|
||||
|
||||
if (testMessages.every(message => message === 'OK')) {
|
||||
dispatch(addSuccessToast('dns_test_ok_toast'));
|
||||
}
|
||||
|
||||
dispatch(testUpstreamSuccess());
|
||||
} catch (error) {
|
||||
dispatch(addErrorToast({ error }));
|
||||
dispatch(testUpstreamFailure());
|
||||
}
|
||||
};
|
||||
|
||||
export const changeLanguageRequest = createAction('CHANGE_LANGUAGE_REQUEST');
|
||||
export const changeLanguageFailure = createAction('CHANGE_LANGUAGE_FAILURE');
|
||||
export const changeLanguageSuccess = createAction('CHANGE_LANGUAGE_SUCCESS');
|
||||
|
||||
export const changeLanguage = lang => async (dispatch) => {
|
||||
dispatch(changeLanguageRequest());
|
||||
try {
|
||||
await apiClient.changeLanguage(lang);
|
||||
dispatch(changeLanguageSuccess());
|
||||
} catch (error) {
|
||||
dispatch(addErrorToast({ error }));
|
||||
dispatch(changeLanguageFailure());
|
||||
}
|
||||
};
|
||||
|
||||
export const getLanguageRequest = createAction('GET_LANGUAGE_REQUEST');
|
||||
export const getLanguageFailure = createAction('GET_LANGUAGE_FAILURE');
|
||||
export const getLanguageSuccess = createAction('GET_LANGUAGE_SUCCESS');
|
||||
|
||||
export const getLanguage = () => async (dispatch) => {
|
||||
dispatch(getLanguageRequest());
|
||||
try {
|
||||
const language = await apiClient.getCurrentLanguage();
|
||||
dispatch(getLanguageSuccess(language));
|
||||
} catch (error) {
|
||||
dispatch(addErrorToast({ error }));
|
||||
dispatch(getLanguageFailure());
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1,18 +1,22 @@
|
||||
import axios from 'axios';
|
||||
import startOfToday from 'date-fns/start_of_today';
|
||||
import endOfToday from 'date-fns/end_of_today';
|
||||
import subHours from 'date-fns/sub_hours';
|
||||
import dateFormat from 'date-fns/format';
|
||||
|
||||
export default class Api {
|
||||
baseUrl = 'control';
|
||||
|
||||
async makeRequest(path, method = 'POST', config) {
|
||||
const response = await axios({
|
||||
url: `${this.baseUrl}/${path}`,
|
||||
method,
|
||||
...config,
|
||||
});
|
||||
return response.data;
|
||||
try {
|
||||
const response = await axios({
|
||||
url: `${this.baseUrl}/${path}`,
|
||||
method,
|
||||
...config,
|
||||
});
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
throw new Error(`${this.baseUrl}/${path} | ${error.response.data} | ${error.response.status}`);
|
||||
}
|
||||
}
|
||||
|
||||
// Global methods
|
||||
@@ -27,6 +31,10 @@ export default class Api {
|
||||
GLOBAL_QUERY_LOG_ENABLE = { path: 'querylog_enable', method: 'POST' };
|
||||
GLOBAL_QUERY_LOG_DISABLE = { path: 'querylog_disable', method: 'POST' };
|
||||
GLOBAL_SET_UPSTREAM_DNS = { path: 'set_upstream_dns', method: 'POST' };
|
||||
GLOBAL_TEST_UPSTREAM_DNS = { path: 'test_upstream_dns', method: 'POST' };
|
||||
GLOBAL_VERSION = { path: 'version.json', method: 'GET' };
|
||||
GLOBAL_ENABLE_PROTECTION = { path: 'enable_protection', method: 'POST' };
|
||||
GLOBAL_DISABLE_PROTECTION = { path: 'disable_protection', method: 'POST' };
|
||||
|
||||
restartGlobalFiltering() {
|
||||
const { path, method } = this.GLOBAL_RESTART;
|
||||
@@ -51,13 +59,12 @@ export default class Api {
|
||||
getGlobalStatsHistory() {
|
||||
const { path, method } = this.GLOBAL_STATS_HISTORY;
|
||||
const format = 'YYYY-MM-DDTHH:mm:ssZ';
|
||||
const todayStart = dateFormat(startOfToday(), format);
|
||||
const todayEnd = dateFormat(endOfToday(), format);
|
||||
const dateNow = Date.now();
|
||||
|
||||
const config = {
|
||||
params: {
|
||||
start_time: todayStart,
|
||||
end_time: todayEnd,
|
||||
start_time: dateFormat(subHours(dateNow, 24), format),
|
||||
end_time: dateFormat(dateNow, format),
|
||||
time_unit: 'hours',
|
||||
},
|
||||
};
|
||||
@@ -104,6 +111,30 @@ export default class Api {
|
||||
return this.makeRequest(path, method, config);
|
||||
}
|
||||
|
||||
testUpstream(servers) {
|
||||
const { path, method } = this.GLOBAL_TEST_UPSTREAM_DNS;
|
||||
const config = {
|
||||
data: servers,
|
||||
header: { 'Content-Type': 'text/plain' },
|
||||
};
|
||||
return this.makeRequest(path, method, config);
|
||||
}
|
||||
|
||||
getGlobalVersion() {
|
||||
const { path, method } = this.GLOBAL_VERSION;
|
||||
return this.makeRequest(path, method);
|
||||
}
|
||||
|
||||
enableGlobalProtection() {
|
||||
const { path, method } = this.GLOBAL_ENABLE_PROTECTION;
|
||||
return this.makeRequest(path, method);
|
||||
}
|
||||
|
||||
disableGlobalProtection() {
|
||||
const { path, method } = this.GLOBAL_DISABLE_PROTECTION;
|
||||
return this.makeRequest(path, method);
|
||||
}
|
||||
|
||||
// Filtering
|
||||
FILTERING_STATUS = { path: 'filtering/status', method: 'GET' };
|
||||
FILTERING_ENABLE = { path: 'filtering/enable', method: 'POST' };
|
||||
@@ -136,13 +167,13 @@ export default class Api {
|
||||
return this.makeRequest(path, method);
|
||||
}
|
||||
|
||||
addFilter(url) {
|
||||
addFilter(url, name) {
|
||||
const { path, method } = this.FILTERING_ADD_FILTER;
|
||||
const parameter = 'url';
|
||||
const requestBody = `${parameter}=${url}`;
|
||||
const config = {
|
||||
data: requestBody,
|
||||
header: { 'Content-Type': 'text/plain' },
|
||||
data: {
|
||||
name,
|
||||
url,
|
||||
},
|
||||
};
|
||||
return this.makeRequest(path, method, config);
|
||||
}
|
||||
@@ -253,4 +284,22 @@ export default class Api {
|
||||
const { path, method } = this.SAFESEARCH_DISABLE;
|
||||
return this.makeRequest(path, method);
|
||||
}
|
||||
|
||||
// Language
|
||||
CURRENT_LANGUAGE = { path: 'i18n/current_language', method: 'GET' };
|
||||
CHANGE_LANGUAGE = { path: 'i18n/change_language', method: 'POST' };
|
||||
|
||||
getCurrentLanguage() {
|
||||
const { path, method } = this.CURRENT_LANGUAGE;
|
||||
return this.makeRequest(path, method);
|
||||
}
|
||||
|
||||
changeLanguage(lang) {
|
||||
const { path, method } = this.CHANGE_LANGUAGE;
|
||||
const parameters = {
|
||||
data: lang,
|
||||
headers: { 'Content-Type': 'text/plain' },
|
||||
};
|
||||
return this.makeRequest(path, method, parameters);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,3 +17,10 @@ body {
|
||||
min-height: calc(100vh - 117px);
|
||||
}
|
||||
}
|
||||
|
||||
.loading-bar {
|
||||
position: absolute;
|
||||
z-index: 103;
|
||||
height: 3px;
|
||||
background: linear-gradient(45deg, rgba(99, 125, 120, 1) 0%, rgba(88, 177, 101, 1) 100%);
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import React, { Component, Fragment } from 'react';
|
||||
import { HashRouter, Route } from 'react-router-dom';
|
||||
import PropTypes from 'prop-types';
|
||||
import LoadingBar from 'react-redux-loading-bar';
|
||||
|
||||
import 'react-table/react-table.css';
|
||||
import '../ui/Tabler.css';
|
||||
@@ -13,23 +14,58 @@ import Settings from '../../containers/Settings';
|
||||
import Filters from '../../containers/Filters';
|
||||
import Logs from '../../containers/Logs';
|
||||
import Footer from '../ui/Footer';
|
||||
|
||||
import Toasts from '../Toasts';
|
||||
import Status from '../ui/Status';
|
||||
import Update from '../ui/Update';
|
||||
import i18n from '../../i18n';
|
||||
|
||||
class App extends Component {
|
||||
componentDidMount() {
|
||||
this.props.getDnsStatus();
|
||||
this.props.getVersion();
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps) {
|
||||
if (this.props.dashboard.language !== prevProps.dashboard.language) {
|
||||
this.setLanguage();
|
||||
}
|
||||
}
|
||||
|
||||
handleStatusChange = () => {
|
||||
this.props.enableDns();
|
||||
};
|
||||
|
||||
setLanguage = () => {
|
||||
const { processing, language } = this.props.dashboard;
|
||||
|
||||
if (!processing) {
|
||||
if (language) {
|
||||
i18n.changeLanguage(language);
|
||||
}
|
||||
}
|
||||
|
||||
i18n.on('languageChanged', (lang) => {
|
||||
this.props.changeLanguage(lang);
|
||||
});
|
||||
}
|
||||
|
||||
render() {
|
||||
const { dashboard } = this.props;
|
||||
const updateAvailable =
|
||||
!dashboard.processingVersions &&
|
||||
dashboard.isCoreRunning &&
|
||||
dashboard.isUpdateAvailable;
|
||||
|
||||
return (
|
||||
<HashRouter hashType='noslash'>
|
||||
<Fragment>
|
||||
{updateAvailable &&
|
||||
<Update
|
||||
announcement={dashboard.announcement}
|
||||
announcementUrl={dashboard.announcementUrl}
|
||||
/>
|
||||
}
|
||||
<LoadingBar className="loading-bar" updateTime={1000} />
|
||||
<Route component={Header} />
|
||||
<div className="container container--wrap">
|
||||
{!dashboard.processing && !dashboard.isCoreRunning &&
|
||||
@@ -49,6 +85,7 @@ class App extends Component {
|
||||
}
|
||||
</div>
|
||||
<Footer />
|
||||
<Toasts />
|
||||
</Fragment>
|
||||
</HashRouter>
|
||||
);
|
||||
@@ -60,6 +97,9 @@ App.propTypes = {
|
||||
enableDns: PropTypes.func,
|
||||
dashboard: PropTypes.object,
|
||||
isCoreRunning: PropTypes.bool,
|
||||
error: PropTypes.string,
|
||||
getVersion: PropTypes.func,
|
||||
changeLanguage: PropTypes.func,
|
||||
};
|
||||
|
||||
export default App;
|
||||
|
||||
@@ -1,34 +1,79 @@
|
||||
import React from 'react';
|
||||
import React, { Component } from 'react';
|
||||
import ReactTable from 'react-table';
|
||||
import PropTypes from 'prop-types';
|
||||
import map from 'lodash/map';
|
||||
import { withNamespaces, Trans } from 'react-i18next';
|
||||
|
||||
import Card from '../ui/Card';
|
||||
import Cell from '../ui/Cell';
|
||||
import Popover from '../ui/Popover';
|
||||
|
||||
const Clients = props => (
|
||||
<Card title="Top blocked domains" subtitle="in the last 3 minutes" bodyType="card-table" refresh={props.refreshButton}>
|
||||
<ReactTable
|
||||
data={map(props.topBlockedDomains, (value, prop) => (
|
||||
{ ip: prop, domain: value }
|
||||
))}
|
||||
columns={[{
|
||||
Header: 'IP',
|
||||
accessor: 'ip',
|
||||
}, {
|
||||
Header: 'Domain name',
|
||||
accessor: 'domain',
|
||||
}]}
|
||||
showPagination={false}
|
||||
noDataText="No domains found"
|
||||
minRows={6}
|
||||
className="-striped -highlight card-table-overflow"
|
||||
/>
|
||||
</Card>
|
||||
);
|
||||
import { getTrackerData } from '../../helpers/trackers/trackers';
|
||||
import { getPercent } from '../../helpers/helpers';
|
||||
import { STATUS_COLORS } from '../../helpers/constants';
|
||||
|
||||
Clients.propTypes = {
|
||||
class BlockedDomains extends Component {
|
||||
columns = [{
|
||||
Header: 'IP',
|
||||
accessor: 'ip',
|
||||
Cell: (row) => {
|
||||
const { value } = row;
|
||||
const trackerData = getTrackerData(value);
|
||||
|
||||
return (
|
||||
<div className="logs__row">
|
||||
<div className="logs__text" title={value}>
|
||||
{value}
|
||||
</div>
|
||||
{trackerData && <Popover data={trackerData} />}
|
||||
</div>
|
||||
);
|
||||
},
|
||||
}, {
|
||||
Header: <Trans>requests_count</Trans>,
|
||||
accessor: 'domain',
|
||||
maxWidth: 190,
|
||||
Cell: ({ value }) => {
|
||||
const {
|
||||
blockedFiltering,
|
||||
replacedSafebrowsing,
|
||||
replacedParental,
|
||||
} = this.props;
|
||||
const blocked = blockedFiltering + replacedSafebrowsing + replacedParental;
|
||||
const percent = getPercent(blocked, value);
|
||||
|
||||
return (
|
||||
<Cell value={value} percent={percent} color={STATUS_COLORS.red} />
|
||||
);
|
||||
},
|
||||
}];
|
||||
|
||||
render() {
|
||||
const { t } = this.props;
|
||||
return (
|
||||
<Card title={ t('top_blocked_domains') } subtitle={ t('for_last_24_hours') } bodyType="card-table" refresh={this.props.refreshButton}>
|
||||
<ReactTable
|
||||
data={map(this.props.topBlockedDomains, (value, prop) => (
|
||||
{ ip: prop, domain: value }
|
||||
))}
|
||||
columns={this.columns}
|
||||
showPagination={false}
|
||||
noDataText={ t('no_domains_found') }
|
||||
minRows={6}
|
||||
className="-striped -highlight card-table-overflow stats__table"
|
||||
/>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
BlockedDomains.propTypes = {
|
||||
topBlockedDomains: PropTypes.object.isRequired,
|
||||
refreshButton: PropTypes.node,
|
||||
blockedFiltering: PropTypes.number.isRequired,
|
||||
replacedSafebrowsing: PropTypes.number.isRequired,
|
||||
replacedParental: PropTypes.number.isRequired,
|
||||
refreshButton: PropTypes.node.isRequired,
|
||||
t: PropTypes.func,
|
||||
};
|
||||
|
||||
export default Clients;
|
||||
export default withNamespaces()(BlockedDomains);
|
||||
|
||||
@@ -1,34 +1,67 @@
|
||||
import React from 'react';
|
||||
import React, { Component } from 'react';
|
||||
import ReactTable from 'react-table';
|
||||
import PropTypes from 'prop-types';
|
||||
import map from 'lodash/map';
|
||||
import { Trans, withNamespaces } from 'react-i18next';
|
||||
|
||||
import Card from '../ui/Card';
|
||||
import Cell from '../ui/Cell';
|
||||
|
||||
const Clients = props => (
|
||||
<Card title="Top clients" subtitle="in the last 3 minutes" bodyType="card-table" refresh={props.refreshButton}>
|
||||
<ReactTable
|
||||
data={map(props.topClients, (value, prop) => (
|
||||
{ ip: prop, count: value }
|
||||
))}
|
||||
columns={[{
|
||||
Header: 'IP',
|
||||
accessor: 'ip',
|
||||
}, {
|
||||
Header: 'Request count',
|
||||
accessor: 'count',
|
||||
}]}
|
||||
showPagination={false}
|
||||
noDataText="No clients found"
|
||||
minRows={6}
|
||||
className="-striped -highlight card-table-overflow"
|
||||
/>
|
||||
</Card>
|
||||
);
|
||||
import { getPercent } from '../../helpers/helpers';
|
||||
import { STATUS_COLORS } from '../../helpers/constants';
|
||||
|
||||
class Clients extends Component {
|
||||
getPercentColor = (percent) => {
|
||||
if (percent > 50) {
|
||||
return STATUS_COLORS.green;
|
||||
} else if (percent > 10) {
|
||||
return STATUS_COLORS.yellow;
|
||||
}
|
||||
return STATUS_COLORS.red;
|
||||
}
|
||||
|
||||
columns = [{
|
||||
Header: 'IP',
|
||||
accessor: 'ip',
|
||||
Cell: ({ value }) => (<div className="logs__row logs__row--overflow"><span className="logs__text" title={value}>{value}</span></div>),
|
||||
sortMethod: (a, b) => parseInt(a.replace(/\./g, ''), 10) - parseInt(b.replace(/\./g, ''), 10),
|
||||
}, {
|
||||
Header: <Trans>requests_count</Trans>,
|
||||
accessor: 'count',
|
||||
Cell: ({ value }) => {
|
||||
const percent = getPercent(this.props.dnsQueries, value);
|
||||
const percentColor = this.getPercentColor(percent);
|
||||
|
||||
return (
|
||||
<Cell value={value} percent={percent} color={percentColor} />
|
||||
);
|
||||
},
|
||||
}];
|
||||
|
||||
render() {
|
||||
const { t } = this.props;
|
||||
return (
|
||||
<Card title={ t('top_clients') } subtitle={ t('for_last_24_hours') } bodyType="card-table" refresh={this.props.refreshButton}>
|
||||
<ReactTable
|
||||
data={map(this.props.topClients, (value, prop) => (
|
||||
{ ip: prop, count: value }
|
||||
))}
|
||||
columns={this.columns}
|
||||
showPagination={false}
|
||||
noDataText={ t('no_clients_found') }
|
||||
minRows={6}
|
||||
className="-striped -highlight card-table-overflow"
|
||||
/>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Clients.propTypes = {
|
||||
topClients: PropTypes.object.isRequired,
|
||||
refreshButton: PropTypes.node,
|
||||
dnsQueries: PropTypes.number.isRequired,
|
||||
refreshButton: PropTypes.node.isRequired,
|
||||
t: PropTypes.func,
|
||||
};
|
||||
|
||||
export default Clients;
|
||||
export default withNamespaces()(Clients);
|
||||
|
||||
@@ -1,17 +1,20 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { Trans, withNamespaces } from 'react-i18next';
|
||||
|
||||
import Card from '../ui/Card';
|
||||
import Tooltip from '../ui/Tooltip';
|
||||
|
||||
const tooltipType = 'tooltip-custom--narrow';
|
||||
|
||||
const Counters = props => (
|
||||
<Card title="General counters" subtitle="in the last 3 minutes" bodyType="card-table" refresh={props.refreshButton}>
|
||||
<Card title={ props.t('general_statistics') } subtitle={ props.t('for_last_24_hours') } bodyType="card-table" refresh={props.refreshButton}>
|
||||
<table className="table card-table">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>
|
||||
DNS Queries
|
||||
<Tooltip text="A number of DNS quieries processed in the last 3 minutes" />
|
||||
<Trans>dns_query</Trans>
|
||||
<Tooltip text={ props.t('number_of_dns_query_24_hours') } type={tooltipType} />
|
||||
</td>
|
||||
<td className="text-right">
|
||||
<span className="text-muted">
|
||||
@@ -21,8 +24,8 @@ const Counters = props => (
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
Blocked by filters
|
||||
<Tooltip text="A number of DNS requests blocked by filters" />
|
||||
<Trans>blocked_by</Trans> <a href="#filters"><Trans>filters</Trans></a>
|
||||
<Tooltip text={ props.t('number_of_dns_query_blocked_24_hours') } type={tooltipType} />
|
||||
</td>
|
||||
<td className="text-right">
|
||||
<span className="text-muted">
|
||||
@@ -32,8 +35,8 @@ const Counters = props => (
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
Blocked malware/phishing
|
||||
<Tooltip text="A number of DNS requests blocked" />
|
||||
<Trans>stats_malware_phishing</Trans>
|
||||
<Tooltip text={ props.t('number_of_dns_query_blocked_24_hours_by_sec') } type={tooltipType} />
|
||||
</td>
|
||||
<td className="text-right">
|
||||
<span className="text-muted">
|
||||
@@ -43,8 +46,8 @@ const Counters = props => (
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
Blocked adult websites
|
||||
<Tooltip text="A number of adult websites blocked" />
|
||||
<Trans>stats_adult</Trans>
|
||||
<Tooltip text={ props.t('number_of_dns_query_blocked_24_hours_adult') } type={tooltipType} />
|
||||
</td>
|
||||
<td className="text-right">
|
||||
<span className="text-muted">
|
||||
@@ -54,8 +57,8 @@ const Counters = props => (
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
Enforced safe search
|
||||
<Tooltip text="A number of DNS requests to search engines for which Safe Search was enforced" />
|
||||
<Trans>enforced_save_search</Trans>
|
||||
<Tooltip text={ props.t('number_of_dns_query_to_safe_search') } type={tooltipType} />
|
||||
</td>
|
||||
<td className="text-right">
|
||||
<span className="text-muted">
|
||||
@@ -65,8 +68,8 @@ const Counters = props => (
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
Average processing time
|
||||
<Tooltip text="Average time in milliseconds on processing a DNS request" />
|
||||
<Trans>average_processing_time</Trans>
|
||||
<Tooltip text={ props.t('average_processing_time_hint') } type={tooltipType} />
|
||||
</td>
|
||||
<td className="text-right">
|
||||
<span className="text-muted">
|
||||
@@ -86,7 +89,8 @@ Counters.propTypes = {
|
||||
replacedParental: PropTypes.number.isRequired,
|
||||
replacedSafesearch: PropTypes.number.isRequired,
|
||||
avgProcessingTime: PropTypes.number.isRequired,
|
||||
refreshButton: PropTypes.node,
|
||||
refreshButton: PropTypes.node.isRequired,
|
||||
t: PropTypes.func,
|
||||
};
|
||||
|
||||
export default Counters;
|
||||
export default withNamespaces()(Counters);
|
||||
|
||||
22
client/src/components/Dashboard/Dashboard.css
Normal file
22
client/src/components/Dashboard/Dashboard.css
Normal file
@@ -0,0 +1,22 @@
|
||||
.stats__table .popover__body {
|
||||
left: 0;
|
||||
transform: none;
|
||||
}
|
||||
|
||||
.stats__table .popover__body:after {
|
||||
left: 13px;
|
||||
}
|
||||
|
||||
.stats__table .rt-tr-group:first-child .popover__body,
|
||||
.stats__table .rt-tr-group:nth-child(2) .popover__body {
|
||||
top: calc(100% + 5px);
|
||||
bottom: initial;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.stats__table .rt-tr-group:first-child .popover__body:after,
|
||||
.stats__table .rt-tr-group:nth-child(2) .popover__body:after {
|
||||
top: -11px;
|
||||
border-top: 6px solid transparent;
|
||||
border-bottom: 6px solid #585965;
|
||||
}
|
||||
@@ -1,34 +1,81 @@
|
||||
import React from 'react';
|
||||
import React, { Component } from 'react';
|
||||
import ReactTable from 'react-table';
|
||||
import PropTypes from 'prop-types';
|
||||
import map from 'lodash/map';
|
||||
import { withNamespaces, Trans } from 'react-i18next';
|
||||
|
||||
import Card from '../ui/Card';
|
||||
import Cell from '../ui/Cell';
|
||||
import Popover from '../ui/Popover';
|
||||
|
||||
const QueriedDomains = props => (
|
||||
<Card title="Top queried domains" subtitle="in the last 3 minutes" bodyType="card-table" refresh={props.refreshButton}>
|
||||
<ReactTable
|
||||
data={map(props.topQueriedDomains, (value, prop) => (
|
||||
{ ip: prop, count: value }
|
||||
))}
|
||||
columns={[{
|
||||
Header: 'IP',
|
||||
accessor: 'ip',
|
||||
}, {
|
||||
Header: 'Request count',
|
||||
accessor: 'count',
|
||||
}]}
|
||||
showPagination={false}
|
||||
noDataText="No domains found"
|
||||
minRows={6}
|
||||
className="-striped -highlight card-table-overflow"
|
||||
/>
|
||||
</Card>
|
||||
);
|
||||
import { getTrackerData } from '../../helpers/trackers/trackers';
|
||||
import { getPercent } from '../../helpers/helpers';
|
||||
import { STATUS_COLORS } from '../../helpers/constants';
|
||||
|
||||
class QueriedDomains extends Component {
|
||||
getPercentColor = (percent) => {
|
||||
if (percent > 10) {
|
||||
return STATUS_COLORS.red;
|
||||
} else if (percent > 5) {
|
||||
return STATUS_COLORS.yellow;
|
||||
}
|
||||
return STATUS_COLORS.green;
|
||||
}
|
||||
|
||||
columns = [{
|
||||
Header: 'IP',
|
||||
accessor: 'ip',
|
||||
Cell: (row) => {
|
||||
const { value } = row;
|
||||
const trackerData = getTrackerData(value);
|
||||
|
||||
return (
|
||||
<div className="logs__row">
|
||||
<div className="logs__text" title={value}>
|
||||
{value}
|
||||
</div>
|
||||
{trackerData && <Popover data={trackerData} />}
|
||||
</div>
|
||||
);
|
||||
},
|
||||
}, {
|
||||
Header: <Trans>requests_count</Trans>,
|
||||
accessor: 'count',
|
||||
maxWidth: 190,
|
||||
Cell: ({ value }) => {
|
||||
const percent = getPercent(this.props.dnsQueries, value);
|
||||
const percentColor = this.getPercentColor(percent);
|
||||
|
||||
return (
|
||||
<Cell value={value} percent={percent} color={percentColor} />
|
||||
);
|
||||
},
|
||||
}];
|
||||
|
||||
render() {
|
||||
const { t } = this.props;
|
||||
return (
|
||||
<Card title={ t('stats_query_domain') } subtitle={ t('for_last_24_hours') } bodyType="card-table" refresh={this.props.refreshButton}>
|
||||
<ReactTable
|
||||
data={map(this.props.topQueriedDomains, (value, prop) => (
|
||||
{ ip: prop, count: value }
|
||||
))}
|
||||
columns={this.columns}
|
||||
showPagination={false}
|
||||
noDataText={ t('no_domains_found') }
|
||||
minRows={6}
|
||||
className="-striped -highlight card-table-overflow stats__table"
|
||||
/>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
QueriedDomains.propTypes = {
|
||||
topQueriedDomains: PropTypes.object.isRequired,
|
||||
refreshButton: PropTypes.node,
|
||||
dnsQueries: PropTypes.number.isRequired,
|
||||
refreshButton: PropTypes.node.isRequired,
|
||||
t: PropTypes.func,
|
||||
};
|
||||
|
||||
export default QueriedDomains;
|
||||
export default withNamespaces()(QueriedDomains);
|
||||
|
||||
@@ -1,61 +1,110 @@
|
||||
import React from 'react';
|
||||
import { ResponsiveLine } from '@nivo/line';
|
||||
import React, { Component } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { Trans, withNamespaces } from 'react-i18next';
|
||||
|
||||
import Card from '../ui/Card';
|
||||
import Line from '../ui/Line';
|
||||
|
||||
const Statistics = props => (
|
||||
<Card title="Statistics" subtitle="Today" bodyType="card-graph" refresh={props.refreshButton}>
|
||||
{props.history ?
|
||||
<ResponsiveLine
|
||||
data={props.history}
|
||||
margin={{
|
||||
top: 50,
|
||||
right: 40,
|
||||
bottom: 80,
|
||||
left: 80,
|
||||
}}
|
||||
minY="auto"
|
||||
stacked={false}
|
||||
curve='monotoneX'
|
||||
axisBottom={{
|
||||
orient: 'bottom',
|
||||
tickSize: 5,
|
||||
tickPadding: 5,
|
||||
tickRotation: -45,
|
||||
legend: 'time',
|
||||
legendOffset: 50,
|
||||
legendPosition: 'center',
|
||||
}}
|
||||
axisLeft={{
|
||||
orient: 'left',
|
||||
tickSize: 5,
|
||||
tickPadding: 5,
|
||||
tickRotation: 0,
|
||||
legend: 'count',
|
||||
legendOffset: -40,
|
||||
legendPosition: 'center',
|
||||
}}
|
||||
enableArea={true}
|
||||
dotSize={10}
|
||||
dotColor="inherit:darker(0.3)"
|
||||
dotBorderWidth={2}
|
||||
dotBorderColor="#ffffff"
|
||||
dotLabel="y"
|
||||
dotLabelYOffset={-12}
|
||||
animate={true}
|
||||
motionStiffness={90}
|
||||
motionDamping={15}
|
||||
/>
|
||||
:
|
||||
<h2 className="text-muted">Empty data</h2>
|
||||
}
|
||||
</Card>
|
||||
);
|
||||
import { getPercent } from '../../helpers/helpers';
|
||||
import { STATUS_COLORS } from '../../helpers/constants';
|
||||
|
||||
class Statistics extends Component {
|
||||
render() {
|
||||
const {
|
||||
dnsQueries,
|
||||
blockedFiltering,
|
||||
replacedSafebrowsing,
|
||||
replacedParental,
|
||||
} = this.props;
|
||||
|
||||
const filteringData = [this.props.history[1]];
|
||||
const queriesData = [this.props.history[2]];
|
||||
const parentalData = [this.props.history[3]];
|
||||
const safebrowsingData = [this.props.history[4]];
|
||||
|
||||
return (
|
||||
<div className="row">
|
||||
<div className="col-sm-6 col-lg-3">
|
||||
<Card type="card--full" bodyType="card-wrap">
|
||||
<div className="card-body-stats">
|
||||
<div className="card-value card-value-stats text-blue">
|
||||
{dnsQueries}
|
||||
</div>
|
||||
<div className="card-title-stats">
|
||||
<Trans>dns_query</Trans>
|
||||
</div>
|
||||
</div>
|
||||
<div className="card-chart-bg">
|
||||
<Line data={queriesData} color={STATUS_COLORS.blue}/>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
<div className="col-sm-6 col-lg-3">
|
||||
<Card type="card--full" bodyType="card-wrap">
|
||||
<div className="card-body-stats">
|
||||
<div className="card-value card-value-stats text-red">
|
||||
{blockedFiltering}
|
||||
</div>
|
||||
<div className="card-value card-value-percent text-red">
|
||||
{getPercent(dnsQueries, blockedFiltering)}
|
||||
</div>
|
||||
<div className="card-title-stats">
|
||||
<Trans>blocked_by</Trans><a href="#filters"> <Trans>filters</Trans></a>
|
||||
</div>
|
||||
</div>
|
||||
<div className="card-chart-bg">
|
||||
<Line data={filteringData} color={STATUS_COLORS.red}/>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
<div className="col-sm-6 col-lg-3">
|
||||
<Card type="card--full" bodyType="card-wrap">
|
||||
<div className="card-body-stats">
|
||||
<div className="card-value card-value-stats text-green">
|
||||
{replacedSafebrowsing}
|
||||
</div>
|
||||
<div className="card-value card-value-percent text-green">
|
||||
{getPercent(dnsQueries, replacedSafebrowsing)}
|
||||
</div>
|
||||
<div className="card-title-stats">
|
||||
<Trans>stats_malware_phishing</Trans>
|
||||
</div>
|
||||
</div>
|
||||
<div className="card-chart-bg">
|
||||
<Line data={safebrowsingData} color={STATUS_COLORS.green}/>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
<div className="col-sm-6 col-lg-3">
|
||||
<Card type="card--full" bodyType="card-wrap">
|
||||
<div className="card-body-stats">
|
||||
<div className="card-value card-value-stats text-yellow">
|
||||
{replacedParental}
|
||||
</div>
|
||||
<div className="card-value card-value-percent text-yellow">
|
||||
{getPercent(dnsQueries, replacedParental)}
|
||||
</div>
|
||||
<div className="card-title-stats">
|
||||
<Trans>stats_adult</Trans>
|
||||
</div>
|
||||
</div>
|
||||
<div className="card-chart-bg">
|
||||
<Line data={parentalData} color={STATUS_COLORS.yellow}/>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Statistics.propTypes = {
|
||||
history: PropTypes.array.isRequired,
|
||||
refreshButton: PropTypes.node,
|
||||
dnsQueries: PropTypes.number.isRequired,
|
||||
blockedFiltering: PropTypes.number.isRequired,
|
||||
replacedSafebrowsing: PropTypes.number.isRequired,
|
||||
replacedParental: PropTypes.number.isRequired,
|
||||
refreshButton: PropTypes.node.isRequired,
|
||||
};
|
||||
|
||||
export default Statistics;
|
||||
export default withNamespaces()(Statistics);
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import React, { Component, Fragment } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import 'whatwg-fetch';
|
||||
import { Trans, withNamespaces } from 'react-i18next';
|
||||
|
||||
import Statistics from './Statistics';
|
||||
import Counters from './Counters';
|
||||
@@ -10,31 +11,47 @@ import BlockedDomains from './BlockedDomains';
|
||||
|
||||
import PageTitle from '../ui/PageTitle';
|
||||
import Loading from '../ui/Loading';
|
||||
import './Dashboard.css';
|
||||
|
||||
class Dashboard extends Component {
|
||||
componentDidMount() {
|
||||
this.getAllStats();
|
||||
}
|
||||
|
||||
getAllStats = () => {
|
||||
this.props.getStats();
|
||||
this.props.getStatsHistory();
|
||||
this.props.getTopStats();
|
||||
}
|
||||
|
||||
getToggleFilteringButton = () => {
|
||||
const { protectionEnabled } = this.props.dashboard;
|
||||
const buttonText = protectionEnabled ? 'disable_protection' : 'enable_protection';
|
||||
const buttonClass = protectionEnabled ? 'btn-gray' : 'btn-success';
|
||||
|
||||
return (
|
||||
<button type="button" className={`btn btn-sm mr-2 ${buttonClass}`} onClick={() => this.props.toggleProtection(protectionEnabled)}>
|
||||
<Trans>{buttonText}</Trans>
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
const { dashboard } = this.props;
|
||||
const { dashboard, t } = this.props;
|
||||
const dashboardProcessing =
|
||||
dashboard.processing ||
|
||||
dashboard.processingStats ||
|
||||
dashboard.processingStatsHistory ||
|
||||
dashboard.processingTopStats;
|
||||
|
||||
const disableButton = <button type="button" className="btn btn-outline-secondary btn-sm mr-2" onClick={() => this.props.disableDns()}>Disable DNS</button>;
|
||||
const refreshFullButton = <button type="button" className="btn btn-outline-primary btn-sm" onClick={() => this.props.getStats()}>Refresh statistics</button>;
|
||||
const refreshButton = <button type="button" className="btn btn-outline-primary btn-sm card-refresh" onClick={() => this.props.getStats()}></button>;
|
||||
const refreshFullButton = <button type="button" className="btn btn-outline-primary btn-sm" onClick={() => this.getAllStats()}><Trans>refresh_statics</Trans></button>;
|
||||
const refreshButton = <button type="button" className="btn btn-outline-primary btn-sm card-refresh" onClick={() => this.getAllStats()} />;
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
<PageTitle title="Dashboard">
|
||||
<PageTitle title={ t('dashboard') }>
|
||||
<div className="page-title__actions">
|
||||
{disableButton}
|
||||
{this.getToggleFilteringButton()}
|
||||
{refreshFullButton}
|
||||
</div>
|
||||
</PageTitle>
|
||||
@@ -46,6 +63,10 @@ class Dashboard extends Component {
|
||||
<Statistics
|
||||
history={dashboard.statsHistory}
|
||||
refreshButton={refreshButton}
|
||||
dnsQueries={dashboard.stats.dns_queries}
|
||||
blockedFiltering={dashboard.stats.blocked_filtering}
|
||||
replacedSafebrowsing={dashboard.stats.replaced_safebrowsing}
|
||||
replacedParental={dashboard.stats.replaced_parental}
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
@@ -66,12 +87,14 @@ class Dashboard extends Component {
|
||||
<Fragment>
|
||||
<div className="col-lg-6">
|
||||
<Clients
|
||||
dnsQueries={dashboard.stats.dns_queries}
|
||||
refreshButton={refreshButton}
|
||||
topClients={dashboard.topStats.top_clients}
|
||||
/>
|
||||
</div>
|
||||
<div className="col-lg-6">
|
||||
<QueriedDomains
|
||||
dnsQueries={dashboard.stats.dns_queries}
|
||||
refreshButton={refreshButton}
|
||||
topQueriedDomains={dashboard.topStats.top_queried_domains}
|
||||
/>
|
||||
@@ -80,6 +103,9 @@ class Dashboard extends Component {
|
||||
<BlockedDomains
|
||||
refreshButton={refreshButton}
|
||||
topBlockedDomains={dashboard.topStats.top_blocked_domains}
|
||||
blockedFiltering={dashboard.stats.blocked_filtering}
|
||||
replacedSafebrowsing={dashboard.stats.replaced_safebrowsing}
|
||||
replacedParental={dashboard.stats.replaced_parental}
|
||||
/>
|
||||
</div>
|
||||
</Fragment>
|
||||
@@ -95,9 +121,11 @@ Dashboard.propTypes = {
|
||||
getStats: PropTypes.func,
|
||||
getStatsHistory: PropTypes.func,
|
||||
getTopStats: PropTypes.func,
|
||||
disableDns: PropTypes.func,
|
||||
dashboard: PropTypes.object,
|
||||
isCoreRunning: PropTypes.bool,
|
||||
getFiltering: PropTypes.func,
|
||||
toggleProtection: PropTypes.func,
|
||||
t: PropTypes.func,
|
||||
};
|
||||
|
||||
export default Dashboard;
|
||||
export default withNamespaces()(Dashboard);
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import React, { Component } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { Trans, withNamespaces } from 'react-i18next';
|
||||
import Card from '../ui/Card';
|
||||
|
||||
export default class UserRules extends Component {
|
||||
class UserRules extends Component {
|
||||
handleChange = (e) => {
|
||||
const { value } = e.currentTarget;
|
||||
this.props.handleRulesChange(value);
|
||||
@@ -14,44 +15,42 @@ export default class UserRules extends Component {
|
||||
};
|
||||
|
||||
render() {
|
||||
const { t } = this.props;
|
||||
return (
|
||||
<Card
|
||||
title="Custom filtering rules"
|
||||
subtitle="Enter one rule on a line. You can use either adblock rules or hosts files syntax."
|
||||
title={ t('custom_filter_rules') }
|
||||
subtitle={ t('custom_filter_rules_hint') }
|
||||
>
|
||||
<form onSubmit={this.handleSubmit}>
|
||||
<textarea className="form-control" value={this.props.userRules} onChange={this.handleChange} />
|
||||
<textarea className="form-control form-control--textarea-large" value={this.props.userRules} onChange={this.handleChange} />
|
||||
<div className="card-actions">
|
||||
<button
|
||||
className="btn btn-success btn-standart"
|
||||
type="submit"
|
||||
onClick={this.handleSubmit}
|
||||
>
|
||||
Apply...
|
||||
<Trans>apply_btn</Trans>
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
<hr/>
|
||||
<div className="list leading-loose">
|
||||
Examples:
|
||||
<Trans>examples_title</Trans>:
|
||||
<ol className="leading-loose">
|
||||
<li>
|
||||
<code>||example.org^</code> - block access to the example.org domain
|
||||
and all its subdomains
|
||||
<code>||example.org^</code> - { t('example_meaning_filter_block') }
|
||||
</li>
|
||||
<li>
|
||||
<code> @@||example.org^</code> - unblock access to the example.org
|
||||
domain and all its subdomains
|
||||
<code> @@||example.org^</code> - { t('example_meaning_filter_whitelist') }
|
||||
</li>
|
||||
<li>
|
||||
<code>example.org 127.0.0.1</code> - AdGuard DNS will now return
|
||||
127.0.0.1 address for the example.org domain (but not its subdomains).
|
||||
<code>127.0.0.1 example.org</code> - { t('example_meaning_host_block') }
|
||||
</li>
|
||||
<li>
|
||||
<code>! Here goes a comment</code> - just a comment
|
||||
<code>{ t('example_comment') }</code> - { t('example_comment_meaning') }
|
||||
</li>
|
||||
<li>
|
||||
<code># Also a comment</code> - just a comment
|
||||
<code>{ t('example_comment_hash') }</code> - { t('example_comment_meaning') }
|
||||
</li>
|
||||
</ol>
|
||||
</div>
|
||||
@@ -64,4 +63,7 @@ UserRules.propTypes = {
|
||||
userRules: PropTypes.string,
|
||||
handleRulesChange: PropTypes.func,
|
||||
handleRulesSubmit: PropTypes.func,
|
||||
t: PropTypes.func,
|
||||
};
|
||||
|
||||
export default withNamespaces()(UserRules);
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import React, { Component } from 'react';
|
||||
import ReactTable from 'react-table';
|
||||
import PropTypes from 'prop-types';
|
||||
import { Trans, withNamespaces } from 'react-i18next';
|
||||
import Modal from '../ui/Modal';
|
||||
import PageTitle from '../ui/PageTitle';
|
||||
import Card from '../ui/Card';
|
||||
@@ -33,57 +34,61 @@ class Filters extends Component {
|
||||
};
|
||||
|
||||
columns = [{
|
||||
Header: 'Enabled',
|
||||
Header: this.props.t('enabled_table_header'),
|
||||
accessor: 'enabled',
|
||||
Cell: this.renderCheckbox,
|
||||
width: 90,
|
||||
className: 'text-center',
|
||||
}, {
|
||||
Header: 'Filter name',
|
||||
Header: this.props.t('name_table_header'),
|
||||
accessor: 'name',
|
||||
Cell: ({ value }) => (<div className="logs__row logs__row--overflow"><span className="logs__text" title={value}>{value}</span></div>),
|
||||
}, {
|
||||
Header: 'Host file URL',
|
||||
Header: this.props.t('filter_url_table_header'),
|
||||
accessor: 'url',
|
||||
Cell: ({ value }) => (<div className="logs__row logs__row--overflow"><a href={value} target='_blank' rel='noopener noreferrer' className="link logs__text">{value}</a></div>),
|
||||
}, {
|
||||
Header: 'Rules count',
|
||||
Header: this.props.t('rules_count_table_header'),
|
||||
accessor: 'rulesCount',
|
||||
className: 'text-center',
|
||||
Cell: props => props.value.toLocaleString(),
|
||||
}, {
|
||||
Header: 'Last time update',
|
||||
Header: this.props.t('last_time_updated_table_header'),
|
||||
accessor: 'lastUpdated',
|
||||
className: 'text-center',
|
||||
}, {
|
||||
Header: 'Actions',
|
||||
Header: this.props.t('actions_table_header'),
|
||||
accessor: 'url',
|
||||
Cell: ({ value }) => (<span className='remove-icon fe fe-trash-2' onClick={() => this.props.removeFilter(value)}/>),
|
||||
Cell: ({ value }) => (<span title={ this.props.t('delete_table_action') } className='remove-icon fe fe-trash-2' onClick={() => this.props.removeFilter(value)}/>),
|
||||
className: 'text-center',
|
||||
width: 75,
|
||||
width: 80,
|
||||
sortable: false,
|
||||
},
|
||||
];
|
||||
|
||||
render() {
|
||||
const { t } = this.props;
|
||||
const { filters, userRules } = this.props.filtering;
|
||||
return (
|
||||
<div>
|
||||
<PageTitle title="Filters" />
|
||||
<PageTitle title={ t('filters') } />
|
||||
<div className="content">
|
||||
<div className="row">
|
||||
<div className="col-md-12">
|
||||
<Card
|
||||
title="Blocking filters and hosts files"
|
||||
subtitle="AdGuard DNS understands basic adblock rules and hosts files syntax."
|
||||
title={ t('filters_and_hosts') }
|
||||
subtitle={ t('filters_and_hosts_hint') }
|
||||
>
|
||||
<ReactTable
|
||||
data={filters}
|
||||
columns={this.columns}
|
||||
showPagination={false}
|
||||
noDataText="No filters added"
|
||||
noDataText={ t('no_filters_added') }
|
||||
minRows={4} // TODO find out what to show if rules.length is 0
|
||||
/>
|
||||
<div className="card-actions">
|
||||
<button className="btn btn-success btn-standart mr-2" type="submit" onClick={this.props.toggleFilteringModal}>Add filter</button>
|
||||
<button className="btn btn-primary btn-standart" type="submit" onClick={this.props.refreshFilters}>Check updates</button>
|
||||
<button className="btn btn-success btn-standart mr-2" type="submit" onClick={this.props.toggleFilteringModal}><Trans>add_filter_btn</Trans></button>
|
||||
<button className="btn btn-primary btn-standart" type="submit" onClick={this.props.refreshFilters}><Trans>check_updates_btn</Trans></button>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
@@ -101,8 +106,8 @@ class Filters extends Component {
|
||||
toggleModal={this.props.toggleFilteringModal}
|
||||
addFilter={this.props.addFilter}
|
||||
isFilterAdded={this.props.filtering.isFilterAdded}
|
||||
title="New filter subscription"
|
||||
inputDescription="Enter valid URL or file path of the filter into field above. You will be subscribed to that filter."
|
||||
title={ t('new_filter_btn') }
|
||||
inputDescription={ t('enter_valid_filter_url') }
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
@@ -124,7 +129,8 @@ Filters.propTypes = {
|
||||
toggleFilteringModal: PropTypes.func.isRequired,
|
||||
handleRulesChange: PropTypes.func.isRequired,
|
||||
refreshFilters: PropTypes.func.isRequired,
|
||||
t: PropTypes.func,
|
||||
};
|
||||
|
||||
|
||||
export default Filters;
|
||||
export default withNamespaces()(Filters);
|
||||
|
||||
@@ -67,13 +67,17 @@
|
||||
}
|
||||
|
||||
.nav-version {
|
||||
padding: 16px 0;
|
||||
font-size: 0.85rem;
|
||||
padding: 7px 0;
|
||||
font-size: 0.80rem;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.nav-version__value {
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.header-brand-img {
|
||||
height: 26px;
|
||||
height: 32px;
|
||||
}
|
||||
|
||||
@media screen and (min-width: 992px) {
|
||||
@@ -84,6 +88,8 @@
|
||||
.nav-tabs .nav-link {
|
||||
width: auto;
|
||||
border-bottom: 1px solid transparent;
|
||||
font-size: 13px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.mobile-menu {
|
||||
@@ -103,7 +109,16 @@
|
||||
|
||||
.nav-version {
|
||||
padding: 0;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (min-width: 1280px) {
|
||||
.nav-tabs .nav-link {
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.nav-version {
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -3,6 +3,8 @@ import { NavLink } from 'react-router-dom';
|
||||
import PropTypes from 'prop-types';
|
||||
import enhanceWithClickOutside from 'react-click-outside';
|
||||
import classnames from 'classnames';
|
||||
import { Trans, withNamespaces } from 'react-i18next';
|
||||
import { REPOSITORY } from '../../helpers/constants';
|
||||
|
||||
class Menu extends Component {
|
||||
handleClickOutside = () => {
|
||||
@@ -15,44 +17,50 @@ class Menu extends Component {
|
||||
|
||||
render() {
|
||||
const menuClass = classnames({
|
||||
'col-lg mobile-menu': true,
|
||||
'col-lg-6 mobile-menu': true,
|
||||
'mobile-menu--active': this.props.isMenuOpen,
|
||||
});
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
<div className={menuClass}>
|
||||
<ul className="nav nav-tabs border-0 flex-column flex-lg-row">
|
||||
<ul className="nav nav-tabs border-0 flex-column flex-lg-row flex-nowrap">
|
||||
<li className="nav-item border-bottom d-lg-none" onClick={this.toggleMenu}>
|
||||
<div className="nav-link nav-link--back">
|
||||
<svg className="nav-icon" fill="none" height="24" strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" viewBox="0 0 24 24" width="24" xmlns="http://www.w3.org/2000/svg"><path d="m19 12h-14"/><path d="m12 19-7-7 7-7"/></svg>
|
||||
Back
|
||||
<Trans>back</Trans>
|
||||
</div>
|
||||
</li>
|
||||
<li className="nav-item">
|
||||
<NavLink to="/" exact={true} className="nav-link">
|
||||
<svg className="nav-icon" fill="none" height="24" stroke="#9aa0ac" strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" viewBox="0 0 24 24" width="24" xmlns="http://www.w3.org/2000/svg"><path d="m3 9 9-7 9 7v11a2 2 0 0 1 -2 2h-14a2 2 0 0 1 -2-2z"/><path d="m9 22v-10h6v10"/></svg>
|
||||
Dashboard
|
||||
<Trans>dashboard</Trans>
|
||||
</NavLink>
|
||||
</li>
|
||||
<li className="nav-item">
|
||||
<NavLink to="/settings" className="nav-link">
|
||||
<svg className="nav-icon" fill="none" height="24" strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" viewBox="0 0 24 24" width="24" xmlns="http://www.w3.org/2000/svg"><circle cx="12" cy="12" r="3"/><path d="m19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1 0 2.83 2 2 0 0 1 -2.83 0l-.06-.06a1.65 1.65 0 0 0 -1.82-.33 1.65 1.65 0 0 0 -1 1.51v.17a2 2 0 0 1 -2 2 2 2 0 0 1 -2-2v-.09a1.65 1.65 0 0 0 -1.08-1.51 1.65 1.65 0 0 0 -1.82.33l-.06.06a2 2 0 0 1 -2.83 0 2 2 0 0 1 0-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0 -1.51-1h-.17a2 2 0 0 1 -2-2 2 2 0 0 1 2-2h.09a1.65 1.65 0 0 0 1.51-1.08 1.65 1.65 0 0 0 -.33-1.82l-.06-.06a2 2 0 0 1 0-2.83 2 2 0 0 1 2.83 0l.06.06a1.65 1.65 0 0 0 1.82.33h.08a1.65 1.65 0 0 0 1-1.51v-.17a2 2 0 0 1 2-2 2 2 0 0 1 2 2v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 0 2 2 0 0 1 0 2.83l-.06.06a1.65 1.65 0 0 0 -.33 1.82v.08a1.65 1.65 0 0 0 1.51 1h.17a2 2 0 0 1 2 2 2 2 0 0 1 -2 2h-.09a1.65 1.65 0 0 0 -1.51 1z"/></svg>
|
||||
Settings
|
||||
<Trans>settings</Trans>
|
||||
</NavLink>
|
||||
</li>
|
||||
<li className="nav-item">
|
||||
<NavLink to="/filters" className="nav-link">
|
||||
<svg className="nav-icon" fill="none" height="24" strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" viewBox="0 0 24 24" width="24" xmlns="http://www.w3.org/2000/svg"><path d="m22 3h-20l8 9.46v6.54l4 2v-8.54z"/></svg>
|
||||
Filters
|
||||
<Trans>filters</Trans>
|
||||
</NavLink>
|
||||
</li>
|
||||
<li className="nav-item">
|
||||
<NavLink to="/logs" className="nav-link">
|
||||
<svg className="nav-icon" fill="none" height="24" strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" viewBox="0 0 24 24" width="24" xmlns="http://www.w3.org/2000/svg"><path d="m14 2h-8a2 2 0 0 0 -2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2v-12z"/><path d="m14 2v6h6"/><path d="m16 13h-8"/><path d="m16 17h-8"/><path d="m10 9h-1-1"/></svg>
|
||||
Query Log
|
||||
<Trans>query_log</Trans>
|
||||
</NavLink>
|
||||
</li>
|
||||
<li className="nav-item">
|
||||
<a href={`${REPOSITORY.URL}/wiki`} className="nav-link" target="_blank" rel="noopener noreferrer">
|
||||
<svg className="nav-icon" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="#66b574" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><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>
|
||||
<Trans>faq</Trans>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</Fragment>
|
||||
@@ -66,4 +74,4 @@ Menu.propTypes = {
|
||||
toggleMenuOpen: PropTypes.func,
|
||||
};
|
||||
|
||||
export default enhanceWithClickOutside(Menu);
|
||||
export default withNamespaces()(enhanceWithClickOutside(Menu));
|
||||
|
||||
@@ -1,11 +1,17 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { Trans, withNamespaces } from 'react-i18next';
|
||||
|
||||
export default function Version(props) {
|
||||
function Version(props) {
|
||||
const { dnsVersion, dnsAddress, dnsPort } = props;
|
||||
return (
|
||||
<div className="nav-version">
|
||||
v.{dnsVersion} / address: {dnsAddress}:{dnsPort}
|
||||
<div className="nav-version__text">
|
||||
<Trans>version</Trans>: <span className="nav-version__value">{dnsVersion}</span>
|
||||
</div>
|
||||
<div className="nav-version__text">
|
||||
<Trans>address</Trans>: <span className="nav-version__value">{dnsAddress}:{dnsPort}</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -15,3 +21,5 @@ Version.propTypes = {
|
||||
dnsAddress: PropTypes.string,
|
||||
dnsPort: PropTypes.number,
|
||||
};
|
||||
|
||||
export default withNamespaces()(Version);
|
||||
|
||||
@@ -2,6 +2,7 @@ import React, { Component } from 'react';
|
||||
import { Link } from 'react-router-dom';
|
||||
import PropTypes from 'prop-types';
|
||||
import classnames from 'classnames';
|
||||
import { Trans, withNamespaces } from 'react-i18next';
|
||||
|
||||
import Menu from './Menu';
|
||||
import Version from './Version';
|
||||
@@ -26,8 +27,8 @@ class Header extends Component {
|
||||
const { dashboard } = this.props;
|
||||
const badgeClass = classnames({
|
||||
'badge dns-status': true,
|
||||
'badge-success': dashboard.isCoreRunning,
|
||||
'badge-danger': !dashboard.isCoreRunning,
|
||||
'badge-success': dashboard.protectionEnabled,
|
||||
'badge-danger': !dashboard.protectionEnabled,
|
||||
});
|
||||
|
||||
return (
|
||||
@@ -42,9 +43,9 @@ class Header extends Component {
|
||||
<Link to="/" className="nav-link pl-0 pr-1">
|
||||
<img src={logo} alt="" className="header-brand-img" />
|
||||
</Link>
|
||||
{!dashboard.proccessing &&
|
||||
{!dashboard.proccessing && dashboard.isCoreRunning &&
|
||||
<span className={badgeClass}>
|
||||
{dashboard.isCoreRunning ? 'ON' : 'OFF'}
|
||||
<Trans>{dashboard.protectionEnabled ? 'on' : 'off'}</Trans>
|
||||
</span>
|
||||
}
|
||||
</div>
|
||||
@@ -72,4 +73,4 @@ Header.propTypes = {
|
||||
location: PropTypes.object,
|
||||
};
|
||||
|
||||
export default Header;
|
||||
export default withNamespaces()(Header);
|
||||
|
||||
@@ -1 +1 @@
|
||||
<svg viewBox="0 0 118 26" xmlns="http://www.w3.org/2000/svg"><path fill="#232323" d="M92.535 18.314l-.897-2.259h-4.47l-.849 2.259h-3.034L88.13 6.809h2.708l4.796 11.505h-3.1zm-3.1-8.434l-1.468 3.949h2.904L89.435 9.88zm-6.607 4.095c0 .693-.117 1.324-.35 1.893a4.115 4.115 0 0 1-1.004 1.463 4.63 4.63 0 0 1-1.574.95c-.614.228-1.297.341-2.047.341-.761 0-1.447-.113-2.056-.34a4.468 4.468 0 0 1-1.55-.951 4.126 4.126 0 0 1-.978-1.463 5.038 5.038 0 0 1-.343-1.893V6.809H75.7v6.939c0 .314.041.612.123.893.081.282.206.534.375.756.169.222.392.398.669.528s.612.195 1.003.195c.392 0 .726-.065 1.003-.195a1.83 1.83 0 0 0 .677-.528 2.1 2.1 0 0 0 .376-.756c.076-.281.114-.58.114-.893v-6.94h2.79v7.167zm-11.446 3.64a8.898 8.898 0 0 1-1.982.715 10.43 10.43 0 0 1-2.472.276c-.924 0-1.775-.146-2.553-.439a5.895 5.895 0 0 1-2.006-1.235 5.63 5.63 0 0 1-1.314-1.909c-.315-.742-.473-1.568-.473-2.478 0-.92.16-1.755.482-2.502a5.567 5.567 0 0 1 1.33-1.91 5.893 5.893 0 0 1 1.99-1.21 7.044 7.044 0 0 1 2.463-.423c.913 0 1.762.138 2.545.414.783.277 1.419.648 1.908 1.114l-1.762 1.998a3.05 3.05 0 0 0-1.076-.772c-.446-.2-.952-.3-1.517-.3-.49 0-.941.09-1.354.268a3.256 3.256 0 0 0-1.077.747 3.39 3.39 0 0 0-.71 1.138 3.977 3.977 0 0 0-.253 1.438c0 .53.077 1.018.229 1.463.152.444.378.826.677 1.145.299.32.669.569 1.11.748.44.178.943.268 1.508.268.326 0 .636-.025.93-.073.294-.05.566-.128.816-.236v-2.096h-2.203V11.52h4.764v6.094zm46.107-5.086c0 1.007-.188 1.877-.563 2.608a5.262 5.262 0 0 1-1.484 1.804 6.199 6.199 0 0 1-2.08 1.04 8.459 8.459 0 0 1-2.35.333h-4.306V6.809h4.176c.816 0 1.62.095 2.414.284.794.19 1.501.504 2.121.943.62.438 1.12 1.026 1.5 1.763.382.736.572 1.646.572 2.73zm-2.904 0c0-.65-.106-1.19-.318-1.617a2.724 2.724 0 0 0-.848-1.024 3.4 3.4 0 0 0-1.208-.544 5.955 5.955 0 0 0-1.394-.163h-1.387v6.728h1.321c.5 0 .982-.057 1.444-.17.462-.115.87-.301 1.224-.562a2.78 2.78 0 0 0 .848-1.04c.212-.433.318-.97.318-1.608zm-55.226 0c0 1.007-.188 1.877-.563 2.608a5.262 5.262 0 0 1-1.484 1.804 6.199 6.199 0 0 1-2.08 1.04 8.459 8.459 0 0 1-2.35.333h-4.306V6.809h4.176c.816 0 1.62.095 2.414.284.794.19 1.501.504 2.121.943.62.438 1.12 1.026 1.5 1.763.382.736.572 1.646.572 2.73zm-2.904 0c0-.65-.106-1.19-.318-1.617a2.724 2.724 0 0 0-.848-1.024 3.4 3.4 0 0 0-1.207-.544 5.955 5.955 0 0 0-1.395-.163H51.3v6.728h1.321c.5 0 .982-.057 1.444-.17.462-.115.87-.301 1.224-.562a2.78 2.78 0 0 0 .848-1.04c.212-.433.318-.97.318-1.608zm-11.86 5.785l-.897-2.259h-4.47l-.848 2.259h-3.034L40.19 6.809h2.708l4.796 11.505h-3.1zm-3.1-8.434l-1.467 3.949h2.903L41.496 9.88zm61.203 8.434l-2.496-4.566h-.946v4.566h-2.74V6.809h4.404c.555 0 1.096.057 1.623.17.528.114 1 .306 1.42.577.418.271.752.629 1.003 1.073.25.444.375.996.375 1.657 0 .78-.212 1.436-.636 1.966-.425.531-1.012.91-1.762 1.138l3.018 4.924h-3.263zm-.114-7.979c0-.27-.057-.49-.171-.658a1.172 1.172 0 0 0-.44-.39 1.919 1.919 0 0 0-.604-.187 4.469 4.469 0 0 0-.645-.049H99.24v2.681h1.321c.228 0 .462-.018.701-.056.24-.038.457-.106.653-.204.196-.097.356-.238.481-.422s.188-.422.188-.715z"/><path fill="#68bc71" d="M12.651 0C8.697 0 3.927.93 0 2.977c0 4.42-.054 15.433 12.651 22.958C25.357 18.41 25.303 7.397 25.303 2.977 21.376.93 16.606 0 12.651 0z"/><path fill="#67b279" d="M12.638 25.927C-.054 18.403 0 7.396 0 2.977 3.923.932 8.687.002 12.638 0v25.927z"/><path fill="#fff" d="M12.19 17.305l7.65-10.311c-.56-.45-1.052-.133-1.323.113h-.01l-6.379 6.636-2.403-2.892c-1.147-1.325-2.705-.314-3.07-.047l5.535 6.5"/></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: 3.4 KiB After Width: | Height: | Size: 4.0 KiB |
89
client/src/components/Logs/Logs.css
Normal file
89
client/src/components/Logs/Logs.css
Normal file
@@ -0,0 +1,89 @@
|
||||
.logs__row {
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
min-height: 26px;
|
||||
}
|
||||
|
||||
.logs__row--overflow {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.logs__row .list-unstyled {
|
||||
margin-bottom: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.logs__text,
|
||||
.logs__row .list-unstyled li {
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.logs__row .tooltip-custom {
|
||||
top: 0;
|
||||
margin-left: 0;
|
||||
margin-right: 5px;
|
||||
}
|
||||
|
||||
.logs__action {
|
||||
position: absolute;
|
||||
top: 10px;
|
||||
right: 15px;
|
||||
background-color: #fff;
|
||||
border-radius: 4px;
|
||||
transition: opacity 0.2s ease, visibility 0.2s ease;
|
||||
visibility: hidden;
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.logs__table .rt-td {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.logs__table .rt-tr:hover .logs__action {
|
||||
visibility: visible;
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.logs__table .rt-tr-group:first-child .tooltip-custom:before {
|
||||
top: calc(100% + 12px);
|
||||
bottom: initial;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.logs__table .rt-tr-group:first-child .tooltip-custom:after {
|
||||
top: initial;
|
||||
bottom: -4px;
|
||||
border-top: 6px solid transparent;
|
||||
border-bottom: 6px solid #585965;
|
||||
}
|
||||
|
||||
.logs__table .rt-tr-group:first-child .popover__body {
|
||||
top: calc(100% + 5px);
|
||||
bottom: initial;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.logs__table .rt-tr-group:first-child .popover__body:after {
|
||||
top: -11px;
|
||||
border-top: 6px solid transparent;
|
||||
border-bottom: 6px solid #585965;
|
||||
}
|
||||
|
||||
.logs__table .rt-thead.-filters input,
|
||||
.logs__table .rt-thead.-filters select {
|
||||
padding: 6px 7px;
|
||||
border-radius: 3px;
|
||||
font-size: 0.9375rem;
|
||||
line-height: 1.6;
|
||||
color: #495057;
|
||||
border: 1px solid rgba(0, 40, 100, 0.12);
|
||||
}
|
||||
|
||||
.logs__table .rt-thead.-filters input:focus,
|
||||
.logs__table .rt-thead.-filters select:focus {
|
||||
border-color: #1991eb;
|
||||
box-shadow: 0 0 0 2px rgba(70, 127, 207, 0.25);
|
||||
}
|
||||
@@ -1,21 +1,26 @@
|
||||
import React, { Component } from 'react';
|
||||
import React, { Component, Fragment } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import ReactTable from 'react-table';
|
||||
import { saveAs } from 'file-saver/FileSaver';
|
||||
import escapeRegExp from 'lodash/escapeRegExp';
|
||||
import endsWith from 'lodash/endsWith';
|
||||
import { Trans, withNamespaces } from 'react-i18next';
|
||||
|
||||
import { formatTime } from '../../helpers/helpers';
|
||||
import { getTrackerData } from '../../helpers/trackers/trackers';
|
||||
import PageTitle from '../ui/PageTitle';
|
||||
import Card from '../ui/Card';
|
||||
import Loading from '../ui/Loading';
|
||||
import { normalizeLogs } from '../../helpers/helpers';
|
||||
|
||||
import PopoverFiltered from '../ui/PopoverFilter';
|
||||
import Popover from '../ui/Popover';
|
||||
import './Logs.css';
|
||||
|
||||
const DOWNLOAD_LOG_FILENAME = 'dns-logs.txt';
|
||||
|
||||
class Logs extends Component {
|
||||
componentDidMount() {
|
||||
// get logs on initialization if queryLogIsEnabled
|
||||
if (this.props.dashboard.queryLogEnabled) {
|
||||
this.props.getLogs();
|
||||
}
|
||||
this.getLogs();
|
||||
this.props.getFilteringStatus();
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps) {
|
||||
@@ -25,46 +30,225 @@ class Logs extends Component {
|
||||
}
|
||||
}
|
||||
|
||||
getLogs = () => {
|
||||
// get logs on initialization if queryLogIsEnabled
|
||||
if (this.props.dashboard.queryLogEnabled) {
|
||||
this.props.getLogs();
|
||||
}
|
||||
}
|
||||
|
||||
renderTooltip(isFiltered, rule, filter) {
|
||||
if (rule) {
|
||||
return (isFiltered && <PopoverFiltered rule={rule} filter={filter}/>);
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
toggleBlocking = (type, domain) => {
|
||||
const { userRules } = this.props.filtering;
|
||||
const { t } = this.props;
|
||||
const lineEnding = !endsWith(userRules, '\n') ? '\n' : '';
|
||||
const baseRule = `||${domain}^$important`;
|
||||
const baseUnblocking = `@@${baseRule}`;
|
||||
const blockingRule = type === 'block' ? baseUnblocking : baseRule;
|
||||
const unblockingRule = type === 'block' ? baseRule : baseUnblocking;
|
||||
const preparedBlockingRule = new RegExp(`(^|\n)${escapeRegExp(blockingRule)}($|\n)`);
|
||||
const preparedUnblockingRule = new RegExp(`(^|\n)${escapeRegExp(unblockingRule)}($|\n)`);
|
||||
|
||||
if (userRules.match(preparedBlockingRule)) {
|
||||
this.props.setRules(userRules.replace(`${blockingRule}`, ''));
|
||||
this.props.addSuccessToast(`${t('rule_removed_from_custom_filtering_toast')}: ${blockingRule}`);
|
||||
} else if (!userRules.match(preparedUnblockingRule)) {
|
||||
this.props.setRules(`${userRules}${lineEnding}${unblockingRule}\n`);
|
||||
this.props.addSuccessToast(`${t('rule_added_to_custom_filtering_toast')}: ${unblockingRule}`);
|
||||
}
|
||||
|
||||
this.props.getFilteringStatus();
|
||||
}
|
||||
|
||||
renderBlockingButton(isFiltered, domain) {
|
||||
const buttonClass = isFiltered ? 'btn-outline-secondary' : 'btn-outline-danger';
|
||||
const buttonText = isFiltered ? 'unblock_btn' : 'block_btn';
|
||||
const buttonType = isFiltered ? 'unblock' : 'block';
|
||||
|
||||
return (
|
||||
<div className="logs__action">
|
||||
<button
|
||||
type="button"
|
||||
className={`btn btn-sm ${buttonClass}`}
|
||||
onClick={() => this.toggleBlocking(buttonType, domain)}
|
||||
>
|
||||
<Trans>{buttonText}</Trans>
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
renderLogs(logs) {
|
||||
const { t } = this.props;
|
||||
const columns = [{
|
||||
Header: 'Time',
|
||||
Header: t('time_table_header'),
|
||||
accessor: 'time',
|
||||
maxWidth: 150,
|
||||
maxWidth: 110,
|
||||
filterable: false,
|
||||
Cell: ({ value }) => (<div className="logs__row"><span className="logs__text" title={value}>{formatTime(value)}</span></div>),
|
||||
}, {
|
||||
Header: 'Domain name',
|
||||
Header: t('domain_name_table_header'),
|
||||
accessor: 'domain',
|
||||
Cell: (row) => {
|
||||
const response = row.value;
|
||||
const trackerData = getTrackerData(response);
|
||||
|
||||
return (
|
||||
<div className="logs__row" title={response}>
|
||||
<div className="logs__text">
|
||||
{response}
|
||||
</div>
|
||||
{trackerData && <Popover data={trackerData}/>}
|
||||
</div>
|
||||
);
|
||||
},
|
||||
}, {
|
||||
Header: 'Type',
|
||||
Header: t('type_table_header'),
|
||||
accessor: 'type',
|
||||
maxWidth: 100,
|
||||
maxWidth: 60,
|
||||
}, {
|
||||
Header: 'Response',
|
||||
Header: t('response_table_header'),
|
||||
accessor: 'response',
|
||||
Cell: (row) => {
|
||||
const responses = row.value;
|
||||
const { reason } = row.original;
|
||||
const isFiltered = row ? reason.indexOf('Filtered') === 0 : false;
|
||||
const parsedFilteredReason = reason.replace('Filtered', 'Filtered by ');
|
||||
const rule = row && row.original && row.original.rule;
|
||||
const { filterId } = row.original;
|
||||
const { filters } = this.props.filtering;
|
||||
let filterName = '';
|
||||
|
||||
if (reason === 'FilteredBlackList' || reason === 'NotFilteredWhiteList') {
|
||||
if (filterId === 0) {
|
||||
filterName = t('custom_filter_rules');
|
||||
} else {
|
||||
const filterItem = Object.keys(filters)
|
||||
.filter(key => filters[key].id === filterId);
|
||||
filterName = filters[filterItem].name;
|
||||
}
|
||||
}
|
||||
|
||||
if (isFiltered) {
|
||||
return (
|
||||
<div className="logs__row">
|
||||
<span className="logs__text" title={parsedFilteredReason}>
|
||||
{parsedFilteredReason}
|
||||
</span>
|
||||
{this.renderTooltip(isFiltered, rule, filterName)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (responses.length > 0) {
|
||||
const liNodes = responses.map((response, index) =>
|
||||
(<li key={index}>{response}</li>));
|
||||
return (<ul className="list-unstyled">{liNodes}</ul>);
|
||||
(<li key={index} title={response}>{response}</li>));
|
||||
const isRenderTooltip = reason === 'NotFilteredWhiteList';
|
||||
|
||||
return (
|
||||
<div className="logs__row">
|
||||
<ul className="list-unstyled">{liNodes}</ul>
|
||||
{this.renderTooltip(isRenderTooltip, rule, filterName)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
return 'Empty';
|
||||
return (
|
||||
<div className="logs__row">
|
||||
<span><Trans>empty_response_status</Trans></span>
|
||||
{this.renderTooltip(isFiltered, rule, filterName)}
|
||||
</div>
|
||||
);
|
||||
},
|
||||
}];
|
||||
filterMethod: (filter, row) => {
|
||||
if (filter.value === 'filtered') {
|
||||
// eslint-disable-next-line no-underscore-dangle
|
||||
return row._original.reason.indexOf('Filtered') === 0 || row._original.reason === 'NotFilteredWhiteList';
|
||||
}
|
||||
return true;
|
||||
},
|
||||
Filter: ({ filter, onChange }) =>
|
||||
<select
|
||||
onChange={event => onChange(event.target.value)}
|
||||
className="form-control"
|
||||
value={filter ? filter.value : 'all'}
|
||||
>
|
||||
<option value="all">{ t('show_all_filter_type') }</option>
|
||||
<option value="filtered">{ t('show_filtered_type') }</option>
|
||||
</select>,
|
||||
}, {
|
||||
Header: t('Client'),
|
||||
accessor: 'client',
|
||||
maxWidth: 250,
|
||||
Cell: (row) => {
|
||||
const { reason } = row.original;
|
||||
const isFiltered = row ? reason.indexOf('Filtered') === 0 : false;
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
<div className="logs__row">
|
||||
{row.value}
|
||||
</div>
|
||||
{this.renderBlockingButton(isFiltered, row.original.domain)}
|
||||
</Fragment>
|
||||
);
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
if (logs) {
|
||||
const normalizedLogs = normalizeLogs(logs);
|
||||
return (<ReactTable
|
||||
data={normalizedLogs}
|
||||
className='logs__table'
|
||||
filterable
|
||||
data={logs}
|
||||
columns={columns}
|
||||
showPagination={false}
|
||||
showPagination={true}
|
||||
defaultPageSize={50}
|
||||
minRows={7}
|
||||
noDataText="No logs found"
|
||||
// Text
|
||||
previousText={ t('previous_btn') }
|
||||
nextText={ t('next_btn') }
|
||||
loadingText={ t('loading_table_status') }
|
||||
pageText={ t('page_table_footer_text') }
|
||||
ofText={ t('of_table_footer_text') }
|
||||
rowsText={ t('rows_table_footer_text') }
|
||||
noDataText={ t('no_logs_found') }
|
||||
defaultFilterMethod={(filter, row) => {
|
||||
const id = filter.pivotId || filter.id;
|
||||
return row[id] !== undefined ?
|
||||
String(row[id]).indexOf(filter.value) !== -1 : true;
|
||||
}}
|
||||
defaultSorted={[
|
||||
{
|
||||
id: 'time',
|
||||
desc: true,
|
||||
},
|
||||
]}
|
||||
getTrProps={(_state, rowInfo) => {
|
||||
// highlight filtered requests
|
||||
if (!rowInfo) {
|
||||
return {};
|
||||
}
|
||||
|
||||
if (rowInfo.original.reason.indexOf('Filtered') === 0) {
|
||||
return {
|
||||
className: 'red',
|
||||
};
|
||||
} else if (rowInfo.original.reason === 'NotFilteredWhiteList') {
|
||||
return {
|
||||
className: 'green',
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
className: '',
|
||||
};
|
||||
}}
|
||||
/>);
|
||||
}
|
||||
return undefined;
|
||||
@@ -79,34 +263,53 @@ class Logs extends Component {
|
||||
};
|
||||
|
||||
renderButtons(queryLogEnabled) {
|
||||
return (<div className="card-actions-top">
|
||||
<button
|
||||
className="btn btn-success btn-standart mr-2"
|
||||
type="submit"
|
||||
onClick={() => this.props.toggleLogStatus(queryLogEnabled)}
|
||||
>{queryLogEnabled ? 'Disable log' : 'Enable log'}</button>
|
||||
{queryLogEnabled &&
|
||||
<button
|
||||
className="btn btn-primary btn-standart"
|
||||
type="submit"
|
||||
onClick={this.handleDownloadButton}
|
||||
>Download log file</button> }
|
||||
</div>);
|
||||
if (queryLogEnabled) {
|
||||
return (
|
||||
<Fragment>
|
||||
<button
|
||||
className="btn btn-gray btn-sm mr-2"
|
||||
type="submit"
|
||||
onClick={() => this.props.toggleLogStatus(queryLogEnabled)}
|
||||
><Trans>disabled_log_btn</Trans></button>
|
||||
<button
|
||||
className="btn btn-primary btn-sm mr-2"
|
||||
type="submit"
|
||||
onClick={this.handleDownloadButton}
|
||||
><Trans>download_log_file_btn</Trans></button>
|
||||
<button
|
||||
className="btn btn-outline-primary btn-sm"
|
||||
type="submit"
|
||||
onClick={this.getLogs}
|
||||
><Trans>refresh_btn</Trans></button>
|
||||
</Fragment>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<button
|
||||
className="btn btn-success btn-sm mr-2"
|
||||
type="submit"
|
||||
onClick={() => this.props.toggleLogStatus(queryLogEnabled)}
|
||||
><Trans>enabled_log_btn</Trans></button>
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
const { queryLogs, dashboard } = this.props;
|
||||
const { queryLogs, dashboard, t } = this.props;
|
||||
const { queryLogEnabled } = dashboard;
|
||||
return (
|
||||
<div>
|
||||
<PageTitle title="Query Log" subtitle="DNS queries log" />
|
||||
<Fragment>
|
||||
<PageTitle title={ t('query_log') } subtitle={ t('last_dns_queries') }>
|
||||
<div className="page-title__actions">
|
||||
{this.renderButtons(queryLogEnabled)}
|
||||
</div>
|
||||
</PageTitle>
|
||||
<Card>
|
||||
{this.renderButtons(queryLogEnabled)}
|
||||
{queryLogEnabled && queryLogs.processing && <Loading />}
|
||||
{queryLogEnabled && !queryLogs.processing &&
|
||||
{queryLogEnabled && queryLogs.getLogsProcessing && <Loading />}
|
||||
{queryLogEnabled && !queryLogs.getLogsProcessing &&
|
||||
this.renderLogs(queryLogs.logs)}
|
||||
</Card>
|
||||
</div>
|
||||
</Fragment>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -117,6 +320,12 @@ Logs.propTypes = {
|
||||
dashboard: PropTypes.object,
|
||||
toggleLogStatus: PropTypes.func,
|
||||
downloadQueryLog: PropTypes.func,
|
||||
getFilteringStatus: PropTypes.func,
|
||||
filtering: PropTypes.object,
|
||||
userRules: PropTypes.string,
|
||||
setRules: PropTypes.func,
|
||||
addSuccessToast: PropTypes.func,
|
||||
t: PropTypes.func,
|
||||
};
|
||||
|
||||
export default Logs;
|
||||
export default withNamespaces()(Logs);
|
||||
|
||||
@@ -10,3 +10,11 @@
|
||||
padding-left: 20px;
|
||||
padding-right: 20px;
|
||||
}
|
||||
|
||||
.form-control--textarea {
|
||||
min-height: 110px;
|
||||
}
|
||||
|
||||
.form-control--textarea-large {
|
||||
min-height: 240px;
|
||||
}
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
import React, { Component } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import classnames from 'classnames';
|
||||
import { Trans, withNamespaces } from 'react-i18next';
|
||||
import Card from '../ui/Card';
|
||||
|
||||
export default class Upstream extends Component {
|
||||
class Upstream extends Component {
|
||||
handleChange = (e) => {
|
||||
const { value } = e.currentTarget;
|
||||
this.props.handleUpstreamChange(value);
|
||||
@@ -13,31 +15,66 @@ export default class Upstream extends Component {
|
||||
this.props.handleUpstreamSubmit();
|
||||
};
|
||||
|
||||
handleTest = () => {
|
||||
this.props.handleUpstreamTest();
|
||||
}
|
||||
|
||||
render() {
|
||||
const testButtonClass = classnames({
|
||||
'btn btn-primary btn-standart mr-2': true,
|
||||
'btn btn-primary btn-standart mr-2 btn-loading': this.props.processingTestUpstream,
|
||||
});
|
||||
const { t } = this.props;
|
||||
|
||||
return (
|
||||
<Card
|
||||
title="Upstream DNS servers"
|
||||
subtitle="If you keep this field empty, AdGuard will use <a href='https://1.1.1.1/' target='_blank'>Cloudflare DNS</a> as an upstream."
|
||||
title={ t('upstream_dns') }
|
||||
subtitle={ t('upstream_dns_hint') }
|
||||
bodyType="card-body box-body--settings"
|
||||
>
|
||||
<div className="row">
|
||||
<div className="col">
|
||||
<form>
|
||||
<textarea
|
||||
className="form-control"
|
||||
value={this.props.upstream}
|
||||
className="form-control form-control--textarea"
|
||||
value={this.props.upstreamDns}
|
||||
onChange={this.handleChange}
|
||||
/>
|
||||
<div className="card-actions">
|
||||
<button
|
||||
className={testButtonClass}
|
||||
type="button"
|
||||
onClick={this.handleTest}
|
||||
>
|
||||
<Trans>test_upstream_btn</Trans>
|
||||
</button>
|
||||
<button
|
||||
className="btn btn-success btn-standart"
|
||||
type="submit"
|
||||
onClick={this.handleSubmit}
|
||||
>
|
||||
Apply
|
||||
<Trans>apply_btn</Trans>
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
<hr/>
|
||||
<div className="list leading-loose">
|
||||
<Trans>examples_title</Trans>:
|
||||
<ol className="leading-loose">
|
||||
<li>
|
||||
<code>1.1.1.1</code> - { t('example_upstream_regular') }
|
||||
</li>
|
||||
<li>
|
||||
<code>tls://1dot1dot1dot1.cloudflare-dns.com</code> - <span dangerouslySetInnerHTML={{ __html: t('example_upstream_dot') }} />
|
||||
</li>
|
||||
<li>
|
||||
<code>https://cloudflare-dns.com/dns-query</code> - <span dangerouslySetInnerHTML={{ __html: t('example_upstream_doh') }} />
|
||||
</li>
|
||||
<li>
|
||||
<code>tcp://1.1.1.1</code> - { t('example_upstream_tcp') }
|
||||
</li>
|
||||
</ol>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
@@ -46,7 +83,12 @@ export default class Upstream extends Component {
|
||||
}
|
||||
|
||||
Upstream.propTypes = {
|
||||
upstream: PropTypes.string,
|
||||
upstreamDns: PropTypes.string,
|
||||
processingTestUpstream: PropTypes.bool,
|
||||
handleUpstreamChange: PropTypes.func,
|
||||
handleUpstreamSubmit: PropTypes.func,
|
||||
handleUpstreamTest: PropTypes.func,
|
||||
t: PropTypes.func,
|
||||
};
|
||||
|
||||
export default withNamespaces()(Upstream);
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import React, { Component, Fragment } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { withNamespaces, Trans } from 'react-i18next';
|
||||
import Upstream from './Upstream';
|
||||
import Checkbox from '../ui/Checkbox';
|
||||
import Loading from '../ui/Loading';
|
||||
@@ -7,27 +8,27 @@ import PageTitle from '../ui/PageTitle';
|
||||
import Card from '../ui/Card';
|
||||
import './Settings.css';
|
||||
|
||||
export default class Settings extends Component {
|
||||
class Settings extends Component {
|
||||
settings = {
|
||||
filtering: {
|
||||
enabled: false,
|
||||
title: 'Block domains using filters and hosts files',
|
||||
subtitle: 'You can setup blocking rules in the <a href="#filters">Filters</a> settings.',
|
||||
title: 'block_domain_use_filters_and_hosts',
|
||||
subtitle: 'filters_block_toggle_hint',
|
||||
},
|
||||
safebrowsing: {
|
||||
enabled: false,
|
||||
title: 'Use AdGuard browsing security web service',
|
||||
subtitle: 'AdGuard DNS will check if domain is blacklisted by the browsing security web service (sb.adtidy.org). It will use privacy-safe lookup API to do the check.',
|
||||
title: 'use_adguard_browsing_sec',
|
||||
subtitle: 'use_adguard_browsing_sec_hint',
|
||||
},
|
||||
parental: {
|
||||
enabled: false,
|
||||
title: 'Use AdGuard parental control web service',
|
||||
subtitle: 'AdGuard DNS will check if domain contains adult materials. It uses the same privacy-friendly API as the browsing security web service.',
|
||||
title: 'use_adguard_parental',
|
||||
subtitle: 'use_adguard_parental_hint',
|
||||
},
|
||||
safesearch: {
|
||||
enabled: false,
|
||||
title: 'Enforce safe search',
|
||||
subtitle: 'AdGuard DNS can enforce safe search in the major search engines: Google, Bing, Yandex.',
|
||||
title: 'enforce_safe_search',
|
||||
subtitle: 'enforce_save_search_hint',
|
||||
},
|
||||
};
|
||||
|
||||
@@ -36,11 +37,19 @@ export default class Settings extends Component {
|
||||
}
|
||||
|
||||
handleUpstreamChange = (value) => {
|
||||
this.props.handleUpstreamChange({ upstream: value });
|
||||
this.props.handleUpstreamChange({ upstreamDns: value });
|
||||
};
|
||||
|
||||
handleUpstreamSubmit = () => {
|
||||
this.props.setUpstream(this.props.settings.upstream);
|
||||
this.props.setUpstream(this.props.dashboard.upstreamDns);
|
||||
};
|
||||
|
||||
handleUpstreamTest = () => {
|
||||
if (this.props.dashboard.upstreamDns.length > 0) {
|
||||
this.props.testUpstream(this.props.dashboard.upstreamDns);
|
||||
} else {
|
||||
this.props.addErrorToast({ error: this.props.t('no_servers_specified') });
|
||||
}
|
||||
};
|
||||
|
||||
renderSettings = (settings) => {
|
||||
@@ -56,29 +65,32 @@ export default class Settings extends Component {
|
||||
});
|
||||
}
|
||||
return (
|
||||
<div>No settings</div>
|
||||
<div><Trans>no_settings</Trans></div>
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
const { settings, upstream } = this.props;
|
||||
const { settings, t } = this.props;
|
||||
const { upstreamDns } = this.props.dashboard;
|
||||
return (
|
||||
<Fragment>
|
||||
<PageTitle title="Settings" />
|
||||
<PageTitle title={ t('settings') } />
|
||||
{settings.processing && <Loading />}
|
||||
{!settings.processing &&
|
||||
<div className="content">
|
||||
<div className="row">
|
||||
<div className="col-md-12">
|
||||
<Card title="General settings" bodyType="card-body box-body--settings">
|
||||
<Card title={ t('general_settings') } bodyType="card-body box-body--settings">
|
||||
<div className="form">
|
||||
{this.renderSettings(settings.settingsList)}
|
||||
</div>
|
||||
</Card>
|
||||
<Upstream
|
||||
upstream={upstream}
|
||||
upstreamDns={upstreamDns}
|
||||
processingTestUpstream={settings.processingTestUpstream}
|
||||
handleUpstreamChange={this.handleUpstreamChange}
|
||||
handleUpstreamSubmit={this.handleUpstreamSubmit}
|
||||
handleUpstreamTest={this.handleUpstreamTest}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@@ -97,4 +109,7 @@ Settings.propTypes = {
|
||||
handleUpstreamChange: PropTypes.func,
|
||||
setUpstream: PropTypes.func,
|
||||
upstream: PropTypes.string,
|
||||
t: PropTypes.func,
|
||||
};
|
||||
|
||||
export default withNamespaces()(Settings);
|
||||
|
||||
60
client/src/components/Toasts/Toast.css
Normal file
60
client/src/components/Toasts/Toast.css
Normal file
@@ -0,0 +1,60 @@
|
||||
.toasts {
|
||||
position: fixed;
|
||||
right: 24px;
|
||||
bottom: 24px;
|
||||
z-index: 103;
|
||||
width: 345px;
|
||||
}
|
||||
|
||||
.toast {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
margin-bottom: 12px;
|
||||
padding: 16px;
|
||||
font-weight: 600;
|
||||
color: #ffffff;
|
||||
border-radius: 4px;
|
||||
background-color: rgba(236, 53, 53, 0.75);
|
||||
}
|
||||
|
||||
.toast--success {
|
||||
background-color: rgba(90, 173, 99, 0.75);
|
||||
}
|
||||
|
||||
.toast:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.toast__content {
|
||||
flex: 1 1 auto;
|
||||
margin: 0 12px 0 0;
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.toast__dismiss {
|
||||
display: block;
|
||||
flex: 0 0 auto;
|
||||
padding: 0;
|
||||
background: transparent;
|
||||
border: 0;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.toast-enter {
|
||||
opacity: 0.01;
|
||||
}
|
||||
|
||||
.toast-enter-active {
|
||||
opacity: 1;
|
||||
transition: all 0.3s ease-out;
|
||||
}
|
||||
|
||||
.toast-exit {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.toast-exit-active {
|
||||
opacity: 0.01;
|
||||
transition: all 0.3s ease-out;
|
||||
}
|
||||
39
client/src/components/Toasts/Toast.js
Normal file
39
client/src/components/Toasts/Toast.js
Normal file
@@ -0,0 +1,39 @@
|
||||
import React, { Component } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { Trans, withNamespaces } from 'react-i18next';
|
||||
|
||||
class Toast extends Component {
|
||||
componentDidMount() {
|
||||
const timeout = this.props.type === 'error' ? 30000 : 5000;
|
||||
|
||||
setTimeout(() => {
|
||||
this.props.removeToast(this.props.id);
|
||||
}, timeout);
|
||||
}
|
||||
|
||||
shouldComponentUpdate() {
|
||||
return false;
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div className={`toast toast--${this.props.type}`}>
|
||||
<p className="toast__content">
|
||||
<Trans>{this.props.message}</Trans>
|
||||
</p>
|
||||
<button className="toast__dismiss" onClick={() => this.props.removeToast(this.props.id)}>
|
||||
<svg stroke="#fff" fill="none" width="20" height="20" strokeWidth="2" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path d="m18 6-12 12"/><path d="m6 6 12 12"/></svg>
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Toast.propTypes = {
|
||||
id: PropTypes.string.isRequired,
|
||||
message: PropTypes.string.isRequired,
|
||||
type: PropTypes.string.isRequired,
|
||||
removeToast: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
export default withNamespaces()(Toast);
|
||||
42
client/src/components/Toasts/index.js
Normal file
42
client/src/components/Toasts/index.js
Normal file
@@ -0,0 +1,42 @@
|
||||
import { connect } from 'react-redux';
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { CSSTransition, TransitionGroup } from 'react-transition-group';
|
||||
import * as actionCreators from '../../actions';
|
||||
import Toast from './Toast';
|
||||
|
||||
import './Toast.css';
|
||||
|
||||
const Toasts = props => (
|
||||
<TransitionGroup className="toasts">
|
||||
{props.toasts.notices && props.toasts.notices.map((toast) => {
|
||||
const { id } = toast;
|
||||
return (
|
||||
<CSSTransition
|
||||
key={id}
|
||||
timeout={500}
|
||||
classNames="toast"
|
||||
>
|
||||
<Toast removeToast={props.removeToast} {...toast} />
|
||||
</CSSTransition>
|
||||
);
|
||||
})}
|
||||
</TransitionGroup>
|
||||
);
|
||||
|
||||
Toasts.propTypes = {
|
||||
toasts: PropTypes.object,
|
||||
removeToast: PropTypes.func,
|
||||
};
|
||||
|
||||
const mapStateToProps = (state) => {
|
||||
const { toasts } = state;
|
||||
const props = { toasts };
|
||||
return props;
|
||||
};
|
||||
|
||||
export default connect(
|
||||
mapStateToProps,
|
||||
actionCreators,
|
||||
)(Toasts);
|
||||
|
||||
@@ -26,7 +26,6 @@
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 400px;
|
||||
}
|
||||
|
||||
.card-body--status {
|
||||
@@ -40,11 +39,61 @@
|
||||
background-size: 14px;
|
||||
background-position: center;
|
||||
background-repeat: no-repeat;
|
||||
background-image: url('data:image/svg+xml;base64,PHN2ZyBmaWxsPSJub25lIiBoZWlnaHQ9IjI0IiBzdHJva2U9IiM0NjdmY2YiIHN0cm9rZS1saW5lY2FwPSJyb3VuZCIgc3Ryb2tlLWxpbmVqb2luPSJyb3VuZCIgc3Ryb2tlLXdpZHRoPSIyIiB2aWV3Qm94PSIwIDAgMjQgMjQiIHdpZHRoPSIyNCIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj48cGF0aCBkPSJtMjMgNHY2aC02Ii8+PHBhdGggZD0ibTEgMjB2LTZoNiIvPjxwYXRoIGQ9Im0zLjUxIDlhOSA5IDAgMCAxIDE0Ljg1LTMuMzZsNC42NCA0LjM2bS0yMiA0IDQuNjQgNC4zNmE5IDkgMCAwIDAgMTQuODUtMy4zNiIvPjwvc3ZnPg==');
|
||||
background-image: url("data:image/svg+xml;base64,PHN2ZyBmaWxsPSJub25lIiBoZWlnaHQ9IjI0IiBzdHJva2U9IiM0NjdmY2YiIHN0cm9rZS1saW5lY2FwPSJyb3VuZCIgc3Ryb2tlLWxpbmVqb2luPSJyb3VuZCIgc3Ryb2tlLXdpZHRoPSIyIiB2aWV3Qm94PSIwIDAgMjQgMjQiIHdpZHRoPSIyNCIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj48cGF0aCBkPSJtMjMgNHY2aC02Ii8+PHBhdGggZD0ibTEgMjB2LTZoNiIvPjxwYXRoIGQ9Im0zLjUxIDlhOSA5IDAgMCAxIDE0Ljg1LTMuMzZsNC42NCA0LjM2bS0yMiA0IDQuNjQgNC4zNmE5IDkgMCAwIDAgMTQuODUtMy4zNiIvPjwvc3ZnPg==");
|
||||
}
|
||||
|
||||
.card-refresh:hover,
|
||||
.card-refresh:not(:disabled):not(.disabled):active,
|
||||
.card-refresh:focus:active {
|
||||
background-image: url('data:image/svg+xml;base64,PHN2ZyBmaWxsPSJub25lIiBoZWlnaHQ9IjI0IiBzdHJva2U9IiNmZmYiIHN0cm9rZS1saW5lY2FwPSJyb3VuZCIgc3Ryb2tlLWxpbmVqb2luPSJyb3VuZCIgc3Ryb2tlLXdpZHRoPSIyIiB2aWV3Qm94PSIwIDAgMjQgMjQiIHdpZHRoPSIyNCIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj48cGF0aCBkPSJtMjMgNHY2aC02Ii8+PHBhdGggZD0ibTEgMjB2LTZoNiIvPjxwYXRoIGQ9Im0zLjUxIDlhOSA5IDAgMCAxIDE0Ljg1LTMuMzZsNC42NCA0LjM2bS0yMiA0IDQuNjQgNC4zNmE5IDkgMCAwIDAgMTQuODUtMy4zNiIvPjwvc3ZnPg==');
|
||||
background-image: url("data:image/svg+xml;base64,PHN2ZyBmaWxsPSJub25lIiBoZWlnaHQ9IjI0IiBzdHJva2U9IiNmZmYiIHN0cm9rZS1saW5lY2FwPSJyb3VuZCIgc3Ryb2tlLWxpbmVqb2luPSJyb3VuZCIgc3Ryb2tlLXdpZHRoPSIyIiB2aWV3Qm94PSIwIDAgMjQgMjQiIHdpZHRoPSIyNCIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj48cGF0aCBkPSJtMjMgNHY2aC02Ii8+PHBhdGggZD0ibTEgMjB2LTZoNiIvPjxwYXRoIGQ9Im0zLjUxIDlhOSA5IDAgMCAxIDE0Ljg1LTMuMzZsNC42NCA0LjM2bS0yMiA0IDQuNjQgNC4zNmE5IDkgMCAwIDAgMTQuODUtMy4zNiIvPjwvc3ZnPg==");
|
||||
}
|
||||
|
||||
.card-title-stats {
|
||||
font-size: 13px;
|
||||
color: #9aa0ac;
|
||||
}
|
||||
|
||||
.card-body-stats {
|
||||
position: relative;
|
||||
flex: 1 1 auto;
|
||||
height: calc(100% - 3rem);
|
||||
margin: 0;
|
||||
padding: 1rem 1.5rem;
|
||||
}
|
||||
|
||||
.card-value-stats {
|
||||
display: block;
|
||||
font-size: 2.1rem;
|
||||
line-height: 2.7rem;
|
||||
height: 2.7rem;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.card-value-percent {
|
||||
position: absolute;
|
||||
top: 15px;
|
||||
right: 15px;
|
||||
font-size: 0.9rem;
|
||||
line-height: 1;
|
||||
height: auto;
|
||||
}
|
||||
|
||||
.card-value-percent:after {
|
||||
content: "%";
|
||||
}
|
||||
|
||||
.card--full {
|
||||
height: calc(100% - 1.5rem);
|
||||
}
|
||||
|
||||
.card-wrap {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
@media screen and (min-width: 1280px) {
|
||||
.card-title-stats {
|
||||
font-size: 14px;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,8 +4,8 @@ import PropTypes from 'prop-types';
|
||||
import './Card.css';
|
||||
|
||||
const Card = props => (
|
||||
<div className="card">
|
||||
{ props.title &&
|
||||
<div className={props.type ? `card ${props.type}` : 'card'}>
|
||||
{props.title &&
|
||||
<div className="card-header with-border">
|
||||
<div className="card-inner">
|
||||
<div className="card-title">
|
||||
@@ -33,6 +33,7 @@ Card.propTypes = {
|
||||
title: PropTypes.string,
|
||||
subtitle: PropTypes.string,
|
||||
bodyType: PropTypes.string,
|
||||
type: PropTypes.string,
|
||||
refresh: PropTypes.node,
|
||||
children: PropTypes.node.isRequired,
|
||||
};
|
||||
|
||||
30
client/src/components/ui/Cell.js
Normal file
30
client/src/components/ui/Cell.js
Normal file
@@ -0,0 +1,30 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
const Cell = props => (
|
||||
<div className="stats__row">
|
||||
<div className="stats__row-value mb-1">
|
||||
<strong>{props.value}</strong>
|
||||
<small className="ml-3 text-muted">
|
||||
{props.percent}%
|
||||
</small>
|
||||
</div>
|
||||
<div className="progress progress-xs">
|
||||
<div
|
||||
className="progress-bar"
|
||||
style={{
|
||||
width: `${props.percent}%`,
|
||||
backgroundColor: props.color,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
Cell.propTypes = {
|
||||
value: PropTypes.number.isRequired,
|
||||
percent: PropTypes.number.isRequired,
|
||||
color: PropTypes.string.isRequired,
|
||||
};
|
||||
|
||||
export default Cell;
|
||||
@@ -1,5 +1,6 @@
|
||||
import React, { Component } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { withNamespaces } from 'react-i18next';
|
||||
|
||||
import './Checkbox.css';
|
||||
|
||||
@@ -10,6 +11,7 @@ class Checkbox extends Component {
|
||||
subtitle,
|
||||
enabled,
|
||||
handleChange,
|
||||
t,
|
||||
} = this.props;
|
||||
return (
|
||||
<div className="form__group">
|
||||
@@ -18,8 +20,8 @@ class Checkbox extends Component {
|
||||
<input type="checkbox" className="checkbox__input" onChange={handleChange} checked={enabled}/>
|
||||
<span className="checkbox__label">
|
||||
<span className="checkbox__label-text">
|
||||
<span className="checkbox__label-title">{title}</span>
|
||||
<span className="checkbox__label-subtitle" dangerouslySetInnerHTML={{ __html: subtitle }}/>
|
||||
<span className="checkbox__label-title">{ t(title) }</span>
|
||||
<span className="checkbox__label-subtitle" dangerouslySetInnerHTML={{ __html: t(subtitle) }}/>
|
||||
</span>
|
||||
</span>
|
||||
</label>
|
||||
@@ -33,6 +35,7 @@ Checkbox.propTypes = {
|
||||
subtitle: PropTypes.string.isRequired,
|
||||
enabled: PropTypes.bool.isRequired,
|
||||
handleChange: PropTypes.func.isRequired,
|
||||
t: PropTypes.func,
|
||||
};
|
||||
|
||||
export default Checkbox;
|
||||
export default withNamespaces()(Checkbox);
|
||||
|
||||
45
client/src/components/ui/Footer.css
Normal file
45
client/src/components/ui/Footer.css
Normal file
@@ -0,0 +1,45 @@
|
||||
.footer__row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.footer__column {
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.footer__column--language {
|
||||
min-width: 220px;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.footer__link {
|
||||
display: inline-block;
|
||||
vertical-align: middle;
|
||||
margin-right: 15px;
|
||||
}
|
||||
|
||||
.footer__link--report {
|
||||
position: relative;
|
||||
top: 1px;
|
||||
margin-right: 0;
|
||||
}
|
||||
|
||||
@media screen and (min-width: 768px) {
|
||||
.footer__copyright {
|
||||
margin-right: 25px;
|
||||
}
|
||||
|
||||
.footer__row {
|
||||
flex-direction: row;
|
||||
}
|
||||
|
||||
.footer__column {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.footer__column--language {
|
||||
min-width: initial;
|
||||
margin-left: auto;
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,10 @@
|
||||
import React, { Component } from 'react';
|
||||
import { Trans, withNamespaces } from 'react-i18next';
|
||||
import { REPOSITORY, LANGUAGES } from '../../helpers/constants';
|
||||
import i18n from '../../i18n';
|
||||
|
||||
import './Footer.css';
|
||||
import './Select.css';
|
||||
|
||||
class Footer extends Component {
|
||||
getYear = () => {
|
||||
@@ -6,26 +12,35 @@ class Footer extends Component {
|
||||
return today.getFullYear();
|
||||
};
|
||||
|
||||
changeLanguage = (event) => {
|
||||
i18n.changeLanguage(event.target.value);
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<footer className="footer">
|
||||
<div className="container">
|
||||
<div className="row align-items-center flex-row-reverse">
|
||||
<div className="col-12 col-lg-auto ml-lg-auto">
|
||||
<ul className="list-inline list-inline-dots text-center mb-0">
|
||||
<li className="list-inline-item">
|
||||
<a href="https://adguard.com/welcome.html" target="_blank" rel="noopener noreferrer">Homepage</a>
|
||||
</li>
|
||||
<li className="list-inline-item">
|
||||
<a href="https://github.com/AdguardTeam/" target="_blank" rel="noopener noreferrer">Github</a>
|
||||
</li>
|
||||
<li className="list-inline-item">
|
||||
<a href="https://adguard.com/privacy.html" target="_blank" rel="noopener noreferrer">Privacy Policy</a>
|
||||
</li>
|
||||
</ul>
|
||||
<div className="footer__row">
|
||||
<div className="footer__column">
|
||||
<div className="footer__copyright">
|
||||
<Trans>copyright</Trans> © {this.getYear()} <a href="https://adguard.com/">AdGuard</a>
|
||||
</div>
|
||||
</div>
|
||||
<div className="col-12 col-lg-auto mt-3 mt-lg-0 text-center">
|
||||
© AdGuard {this.getYear()}
|
||||
<div className="footer__column">
|
||||
<a href={REPOSITORY.URL} className="footer__link" target="_blank" rel="noopener noreferrer">
|
||||
<Trans>homepage</Trans>
|
||||
</a>
|
||||
<a href={`${REPOSITORY.URL}/issues/new`} className="btn btn-outline-primary btn-sm footer__link footer__link--report" target="_blank" rel="noopener noreferrer">
|
||||
<Trans>report_an_issue</Trans>
|
||||
</a>
|
||||
</div>
|
||||
<div className="footer__column footer__column--language">
|
||||
<select className="form-control select select--language" value={i18n.language} onChange={this.changeLanguage}>
|
||||
{LANGUAGES.map(language =>
|
||||
<option key={language.key} value={language.key}>
|
||||
{language.name}
|
||||
</option>)}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -34,4 +49,4 @@ class Footer extends Component {
|
||||
}
|
||||
}
|
||||
|
||||
export default Footer;
|
||||
export default withNamespaces()(Footer);
|
||||
|
||||
9
client/src/components/ui/Line.css
Normal file
9
client/src/components/ui/Line.css
Normal file
@@ -0,0 +1,9 @@
|
||||
.line__tooltip {
|
||||
padding: 2px 10px 7px;
|
||||
line-height: 1.1;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.line__tooltip-text {
|
||||
font-size: 0.7rem;
|
||||
}
|
||||
64
client/src/components/ui/Line.js
Normal file
64
client/src/components/ui/Line.js
Normal file
@@ -0,0 +1,64 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { ResponsiveLine } from '@nivo/line';
|
||||
|
||||
import './Line.css';
|
||||
|
||||
const Line = props => (
|
||||
props.data &&
|
||||
<ResponsiveLine
|
||||
data={props.data}
|
||||
margin={{
|
||||
top: 15,
|
||||
right: 0,
|
||||
bottom: 1,
|
||||
left: 0,
|
||||
}}
|
||||
minY="auto"
|
||||
stacked={false}
|
||||
curve='linear'
|
||||
axisBottom={{
|
||||
tickSize: 0,
|
||||
tickPadding: 10,
|
||||
}}
|
||||
axisLeft={{
|
||||
tickSize: 0,
|
||||
tickPadding: 10,
|
||||
}}
|
||||
enableGridX={false}
|
||||
enableGridY={false}
|
||||
enableDots={false}
|
||||
enableArea={true}
|
||||
animate={false}
|
||||
colorBy={() => (props.color)}
|
||||
tooltip={slice => (
|
||||
<div>
|
||||
{slice.data.map(d => (
|
||||
<div key={d.serie.id} className="line__tooltip">
|
||||
<span className="line__tooltip-text">
|
||||
<strong>{d.data.y}</strong>
|
||||
<br/>
|
||||
<small>{d.data.x}</small>
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
theme={{
|
||||
tooltip: {
|
||||
container: {
|
||||
padding: '0',
|
||||
background: '#333',
|
||||
borderRadius: '4px',
|
||||
},
|
||||
},
|
||||
}}
|
||||
/>
|
||||
);
|
||||
|
||||
Line.propTypes = {
|
||||
data: PropTypes.array.isRequired,
|
||||
color: PropTypes.string.isRequired,
|
||||
};
|
||||
|
||||
export default Line;
|
||||
@@ -2,16 +2,20 @@ import React, { Component } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import ReactModal from 'react-modal';
|
||||
import classnames from 'classnames';
|
||||
import { Trans, withNamespaces } from 'react-i18next';
|
||||
import { R_URL_REQUIRES_PROTOCOL } from '../../helpers/constants';
|
||||
import './Modal.css';
|
||||
|
||||
ReactModal.setAppElement('#root');
|
||||
|
||||
export default class Modal extends Component {
|
||||
state = {
|
||||
url: '',
|
||||
isUrlValid: false,
|
||||
};
|
||||
const initialState = {
|
||||
url: '',
|
||||
name: '',
|
||||
isUrlValid: false,
|
||||
};
|
||||
|
||||
class Modal extends Component {
|
||||
state = initialState;
|
||||
|
||||
// eslint-disable-next-line
|
||||
isUrlValid = url => {
|
||||
@@ -27,33 +31,48 @@ export default class Modal extends Component {
|
||||
}
|
||||
};
|
||||
|
||||
handleNameChange = (e) => {
|
||||
const { value: name } = e.currentTarget;
|
||||
this.setState({ ...this.state, name });
|
||||
};
|
||||
|
||||
handleNext = () => {
|
||||
this.props.addFilter(this.state.url);
|
||||
this.props.addFilter(this.state.url, this.state.name);
|
||||
setTimeout(() => {
|
||||
if (this.props.isFilterAdded) {
|
||||
this.props.toggleModal();
|
||||
this.closeModal();
|
||||
}
|
||||
}, 2000);
|
||||
};
|
||||
|
||||
closeModal = () => {
|
||||
this.props.toggleModal();
|
||||
this.setState({ ...this.state, ...initialState });
|
||||
}
|
||||
|
||||
render() {
|
||||
const {
|
||||
isOpen,
|
||||
toggleModal,
|
||||
title,
|
||||
inputDescription,
|
||||
} = this.props;
|
||||
const { isUrlValid, url } = this.state;
|
||||
const inputClass = classnames({
|
||||
const { isUrlValid, url, name } = this.state;
|
||||
const inputUrlClass = classnames({
|
||||
'form-control mb-2': true,
|
||||
'is-invalid': url.length > 0 && !isUrlValid,
|
||||
'is-valid': url.length > 0 && isUrlValid,
|
||||
});
|
||||
const inputNameClass = classnames({
|
||||
'form-control mb-2': true,
|
||||
'is-valid': name.length > 0,
|
||||
});
|
||||
|
||||
const renderBody = () => {
|
||||
if (!this.props.isFilterAdded) {
|
||||
return (
|
||||
<React.Fragment>
|
||||
<input type="text" className={inputClass} placeholder="Enter URL or path" onChange={this.handleUrlChange}/>
|
||||
<input type="text" className={inputNameClass} placeholder={ this.props.t('enter_name_hint') } onChange={this.handleNameChange} />
|
||||
<input type="text" className={inputUrlClass} placeholder={ this.props.t('enter_url_hint') } onChange={this.handleUrlChange} />
|
||||
{inputDescription &&
|
||||
<div className="description">
|
||||
{inputDescription}
|
||||
@@ -63,26 +82,26 @@ export default class Modal extends Component {
|
||||
}
|
||||
return (
|
||||
<div className="description">
|
||||
Url added successfully
|
||||
<Trans>Url added successfully</Trans>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const isValidForSubmit = !(url.length > 0 && isUrlValid);
|
||||
const isValidForSubmit = !(url.length > 0 && isUrlValid && name.length > 0);
|
||||
|
||||
return (
|
||||
<ReactModal
|
||||
className="Modal__Bootstrap modal-dialog modal-dialog-centered"
|
||||
closeTimeoutMS={0}
|
||||
isOpen={ isOpen }
|
||||
onRequestClose={toggleModal}
|
||||
onRequestClose={this.closeModal}
|
||||
>
|
||||
<div className="modal-content">
|
||||
<div className="modal-header">
|
||||
<h4 className="modal-title">
|
||||
{title}
|
||||
</h4>
|
||||
<button type="button" className="close" onClick={toggleModal}>
|
||||
<button type="button" className="close" onClick={this.closeModal}>
|
||||
<span className="sr-only">Close</span>
|
||||
</button>
|
||||
</div>
|
||||
@@ -92,8 +111,8 @@ export default class Modal extends Component {
|
||||
{
|
||||
!this.props.isFilterAdded &&
|
||||
<div className="modal-footer">
|
||||
<button type="button" className="btn btn-secondary" onClick={toggleModal}>Cancel</button>
|
||||
<button type="button" className="btn btn-success" onClick={this.handleNext} disabled={isValidForSubmit}>Add filter</button>
|
||||
<button type="button" className="btn btn-secondary" onClick={this.closeModal}><Trans>cancel_btn</Trans></button>
|
||||
<button type="button" className="btn btn-success" onClick={this.handleNext} disabled={isValidForSubmit}><Trans>add_filter_btn</Trans></button>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
@@ -109,4 +128,7 @@ Modal.propTypes = {
|
||||
inputDescription: PropTypes.string,
|
||||
addFilter: PropTypes.func.isRequired,
|
||||
isFilterAdded: PropTypes.bool,
|
||||
t: PropTypes.func,
|
||||
};
|
||||
|
||||
export default withNamespaces()(Modal);
|
||||
|
||||
102
client/src/components/ui/Popover.css
Normal file
102
client/src/components/ui/Popover.css
Normal file
@@ -0,0 +1,102 @@
|
||||
.popover-wrap {
|
||||
position: relative;
|
||||
top: 1px;
|
||||
display: inline-block;
|
||||
vertical-align: middle;
|
||||
align-self: flex-start;
|
||||
}
|
||||
|
||||
.popover__trigger {
|
||||
position: relative;
|
||||
top: 3px;
|
||||
margin: 0 8px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.popover__trigger:after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
top: -6px;
|
||||
left: -3px;
|
||||
width: 26px;
|
||||
height: 24px;
|
||||
}
|
||||
|
||||
.popover__body {
|
||||
content: "";
|
||||
display: flex;
|
||||
position: absolute;
|
||||
bottom: calc(100% + 3px);
|
||||
left: 50%;
|
||||
min-width: 275px;
|
||||
padding: 10px 15px;
|
||||
font-size: 0.8rem;
|
||||
white-space: normal;
|
||||
color: #fff;
|
||||
background-color: #585965;
|
||||
border-radius: 3px;
|
||||
transition: opacity 0.2s ease-in-out, visibility 0.2s ease-in-out;
|
||||
transform: translateX(-50%);
|
||||
visibility: hidden;
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.popover__body--filter {
|
||||
min-width: 100%;
|
||||
}
|
||||
|
||||
.popover__body:after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
bottom: -5px;
|
||||
left: calc(50% - 6px);
|
||||
width: 0;
|
||||
height: 0;
|
||||
border-left: 6px solid transparent;
|
||||
border-right: 6px solid transparent;
|
||||
border-top: 6px solid #585965;
|
||||
}
|
||||
|
||||
.popover__trigger:hover + .popover__body,
|
||||
.popover__body:hover {
|
||||
visibility: visible;
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.popover__icon {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
stroke: #9aa0ac;
|
||||
}
|
||||
|
||||
.popover__icon--green {
|
||||
stroke: #66b574;
|
||||
}
|
||||
|
||||
.popover__list-title {
|
||||
margin-bottom: 3px;
|
||||
}
|
||||
|
||||
.popover__list-item {
|
||||
margin-bottom: 2px;
|
||||
}
|
||||
|
||||
.popover__list-item--nowrap {
|
||||
max-width: 300px;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.popover__list-item:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.popover__link {
|
||||
color: #66b586;
|
||||
}
|
||||
|
||||
.popover__link:hover,
|
||||
.popover__link:focus {
|
||||
color: #66b586;
|
||||
}
|
||||
56
client/src/components/ui/Popover.js
Normal file
56
client/src/components/ui/Popover.js
Normal file
@@ -0,0 +1,56 @@
|
||||
import React, { Component } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { Trans, withNamespaces } from 'react-i18next';
|
||||
import { getSourceData } from '../../helpers/trackers/trackers';
|
||||
import { captitalizeWords } from '../../helpers/helpers';
|
||||
|
||||
import './Popover.css';
|
||||
|
||||
class Popover extends Component {
|
||||
render() {
|
||||
const { data } = this.props;
|
||||
|
||||
const sourceData = getSourceData(data);
|
||||
|
||||
const source = (
|
||||
<div className="popover__list-item">
|
||||
<Trans>source_label</Trans>: <a className="popover__link" target="_blank" rel="noopener noreferrer" href={sourceData.url}><strong>{sourceData.name}</strong></a>
|
||||
</div>
|
||||
);
|
||||
|
||||
const tracker = (
|
||||
<div className="popover__list-item">
|
||||
<Trans>name_table_header</Trans>: <a className="popover__link" target="_blank" rel="noopener noreferrer" href={data.url}><strong>{data.name}</strong></a>
|
||||
</div>
|
||||
);
|
||||
|
||||
const categoryName = captitalizeWords(data.category);
|
||||
|
||||
return (
|
||||
<div className="popover-wrap">
|
||||
<div className="popover__trigger">
|
||||
<svg className="popover__icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"></path><circle cx="12" cy="12" r="3"></circle></svg>
|
||||
</div>
|
||||
<div className="popover__body">
|
||||
<div className="popover__list">
|
||||
<div className="popover__list-title">
|
||||
<Trans>found_in_known_domain_db</Trans>
|
||||
</div>
|
||||
{tracker}
|
||||
<div className="popover__list-item">
|
||||
<Trans>category_label</Trans>: <strong>
|
||||
<Trans>{categoryName}</Trans></strong>
|
||||
</div>
|
||||
{source}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Popover.propTypes = {
|
||||
data: PropTypes.object.isRequired,
|
||||
};
|
||||
|
||||
export default withNamespaces()(Popover);
|
||||
34
client/src/components/ui/PopoverFilter.js
Normal file
34
client/src/components/ui/PopoverFilter.js
Normal file
@@ -0,0 +1,34 @@
|
||||
import React, { Component } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { Trans, withNamespaces } from 'react-i18next';
|
||||
|
||||
import './Popover.css';
|
||||
|
||||
class PopoverFilter extends Component {
|
||||
render() {
|
||||
return (
|
||||
<div className="popover-wrap">
|
||||
<div className="popover__trigger popover__trigger--filter">
|
||||
<svg className="popover__icon popover__icon--green" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><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>
|
||||
</div>
|
||||
<div className="popover__body popover__body--filter">
|
||||
<div className="popover__list">
|
||||
<div className="popover__list-item popover__list-item--nowrap">
|
||||
<Trans>rule_label</Trans>: <strong>{this.props.rule}</strong>
|
||||
</div>
|
||||
{this.props.filter && <div className="popover__list-item popover__list-item--nowrap">
|
||||
<Trans>filter_label</Trans>: <strong>{this.props.filter}</strong>
|
||||
</div>}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
PopoverFilter.propTypes = {
|
||||
rule: PropTypes.string.isRequired,
|
||||
filter: PropTypes.string,
|
||||
};
|
||||
|
||||
export default withNamespaces()(PopoverFilter);
|
||||
@@ -1,4 +1,17 @@
|
||||
.ReactTable .rt-th,
|
||||
.ReactTable .rt-td {
|
||||
padding: 10px 15px;
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
.ReactTable .rt-tbody {
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
.rt-tr-group .red {
|
||||
background-color: #fff4f2;
|
||||
}
|
||||
|
||||
.rt-tr-group .green {
|
||||
background-color: #f1faf3;
|
||||
}
|
||||
|
||||
16
client/src/components/ui/Select.css
Normal file
16
client/src/components/ui/Select.css
Normal file
@@ -0,0 +1,16 @@
|
||||
.select.select--language {
|
||||
height: 45px;
|
||||
padding: 0 32px 2px 33px;
|
||||
outline: 0;
|
||||
border-color: rgba(0, 40, 100, 0.12);
|
||||
background-image: url("./svg/globe.svg"), url("./svg/chevron-down.svg");
|
||||
background-repeat: no-repeat, no-repeat;
|
||||
background-position: left 11px center, right 9px center;
|
||||
background-size: 14px, 17px 20px;
|
||||
appearance: none;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.select--language::-ms-expand {
|
||||
opacity: 0;
|
||||
}
|
||||
@@ -7,10 +7,10 @@ const Status = props => (
|
||||
<div className="status">
|
||||
<Card bodyType="card-body card-body--status">
|
||||
<div className="h4 font-weight-light mb-4">
|
||||
You are currently not using AdGuard DNS
|
||||
You are currently not using AdGuard Home
|
||||
</div>
|
||||
<button className="btn btn-success" onClick={props.handleStatusChange}>
|
||||
Enable AdGuard DNS
|
||||
Enable AdGuard Home
|
||||
</button>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
@@ -10373,6 +10373,8 @@ body.fixed-header .header {
|
||||
font-size: 0.875rem;
|
||||
padding: 1.25rem 0;
|
||||
color: #9aa0ac;
|
||||
position: relative;
|
||||
z-index: 102;
|
||||
}
|
||||
|
||||
.footer a:not(.btn) {
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
vertical-align: middle;
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
flex-shrink: 0;
|
||||
margin-left: 5px;
|
||||
background-image: url("./svg/help-circle.svg");
|
||||
background-size: 100%;
|
||||
@@ -15,15 +16,15 @@
|
||||
content: attr(data-tooltip);
|
||||
display: block;
|
||||
position: absolute;
|
||||
bottom: calc(100% + 12px);
|
||||
left: calc(50% - 103px);
|
||||
width: 206px;
|
||||
bottom: calc(100% + 10px);
|
||||
left: 50%;
|
||||
padding: 10px 15px;
|
||||
font-size: 0.85rem;
|
||||
text-align: center;
|
||||
color: #fff;
|
||||
background-color: #585965;
|
||||
border-radius: 3px;
|
||||
transform: translateX(-50%);
|
||||
visibility: hidden;
|
||||
opacity: 0;
|
||||
}
|
||||
@@ -31,7 +32,7 @@
|
||||
.tooltip-custom:after {
|
||||
content: "";
|
||||
position: relative;
|
||||
top: -9px;
|
||||
top: -7px;
|
||||
left: calc(50% - 6px);
|
||||
visibility: hidden;
|
||||
opacity: 0;
|
||||
@@ -47,3 +48,7 @@
|
||||
visibility: visible;
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.tooltip-custom--narrow:before {
|
||||
width: 220px;
|
||||
}
|
||||
|
||||
@@ -4,11 +4,12 @@ import PropTypes from 'prop-types';
|
||||
import './Tooltip.css';
|
||||
|
||||
const Tooltip = props => (
|
||||
<div data-tooltip={props.text} className="tooltip-custom"></div>
|
||||
<div data-tooltip={props.text} className={`tooltip-custom ${props.type || ''}`}></div>
|
||||
);
|
||||
|
||||
Tooltip.propTypes = {
|
||||
text: PropTypes.string.isRequired,
|
||||
type: PropTypes.string,
|
||||
};
|
||||
|
||||
export default Tooltip;
|
||||
|
||||
6
client/src/components/ui/Update.css
Normal file
6
client/src/components/ui/Update.css
Normal file
@@ -0,0 +1,6 @@
|
||||
.update {
|
||||
position: relative;
|
||||
z-index: 102;
|
||||
margin-bottom: 0;
|
||||
padding: 0.75rem 0;
|
||||
}
|
||||
19
client/src/components/ui/Update.js
Normal file
19
client/src/components/ui/Update.js
Normal file
@@ -0,0 +1,19 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import './Update.css';
|
||||
|
||||
const Update = props => (
|
||||
<div className="alert alert-info update">
|
||||
<div className="container">
|
||||
{props.announcement} <a href={props.announcementUrl} target="_blank" rel="noopener noreferrer">Click here</a> for more info.
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
Update.propTypes = {
|
||||
announcement: PropTypes.string.isRequired,
|
||||
announcementUrl: PropTypes.string.isRequired,
|
||||
};
|
||||
|
||||
export default Update;
|
||||
1
client/src/components/ui/svg/chevron-down.svg
Normal file
1
client/src/components/ui/svg/chevron-down.svg
Normal file
@@ -0,0 +1 @@
|
||||
<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>
|
||||
|
After Width: | Height: | Size: 264 B |
1
client/src/components/ui/svg/globe.svg
Normal file
1
client/src/components/ui/svg/globe.svg
Normal file
@@ -0,0 +1 @@
|
||||
<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>
|
||||
|
After Width: | Height: | Size: 354 B |
@@ -1,10 +1,10 @@
|
||||
import { connect } from 'react-redux';
|
||||
import { getLogs, toggleLogStatus, downloadQueryLog } from '../actions';
|
||||
import { getLogs, toggleLogStatus, downloadQueryLog, getFilteringStatus, setRules, addSuccessToast } from '../actions';
|
||||
import Logs from '../components/Logs';
|
||||
|
||||
const mapStateToProps = (state) => {
|
||||
const { queryLogs, dashboard } = state;
|
||||
const props = { queryLogs, dashboard };
|
||||
const { queryLogs, dashboard, filtering } = state;
|
||||
const props = { queryLogs, dashboard, filtering };
|
||||
return props;
|
||||
};
|
||||
|
||||
@@ -12,6 +12,9 @@ const mapDispatchToProps = {
|
||||
getLogs,
|
||||
toggleLogStatus,
|
||||
downloadQueryLog,
|
||||
getFilteringStatus,
|
||||
setRules,
|
||||
addSuccessToast,
|
||||
};
|
||||
|
||||
export default connect(
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import { connect } from 'react-redux';
|
||||
import { initSettings, toggleSetting, handleUpstreamChange, setUpstream } from '../actions';
|
||||
import { initSettings, toggleSetting, handleUpstreamChange, setUpstream, testUpstream, addErrorToast } from '../actions';
|
||||
import Settings from '../components/Settings';
|
||||
|
||||
const mapStateToProps = (state) => {
|
||||
const { settings } = state;
|
||||
const props = { settings };
|
||||
const { settings, dashboard } = state;
|
||||
const props = { settings, dashboard };
|
||||
return props;
|
||||
};
|
||||
|
||||
@@ -13,6 +13,8 @@ const mapDispatchToProps = {
|
||||
toggleSetting,
|
||||
handleUpstreamChange,
|
||||
setUpstream,
|
||||
testUpstream,
|
||||
addErrorToast,
|
||||
};
|
||||
|
||||
export default connect(
|
||||
|
||||
@@ -1 +1,57 @@
|
||||
export const R_URL_REQUIRES_PROTOCOL = /^https?:\/\/\w[\w_\-.]*\.[a-z]{2,8}[^\s]*$/;
|
||||
|
||||
export const STATS_NAMES = {
|
||||
avg_processing_time: 'average_processing_time',
|
||||
blocked_filtering: 'Blocked by filters',
|
||||
dns_queries: 'DNS queries',
|
||||
replaced_parental: 'stats_adult',
|
||||
replaced_safebrowsing: 'stats_malware_phishing',
|
||||
replaced_safesearch: 'enforced_save_search',
|
||||
};
|
||||
|
||||
export const STATUS_COLORS = {
|
||||
blue: '#467fcf',
|
||||
red: '#cd201f',
|
||||
green: '#5eba00',
|
||||
yellow: '#f1c40f',
|
||||
};
|
||||
|
||||
export const REPOSITORY = {
|
||||
URL: 'https://github.com/AdguardTeam/AdGuardHome',
|
||||
TRACKERS_DB: 'https://github.com/AdguardTeam/AdGuardHome/tree/master/client/src/helpers/trackers/adguard.json',
|
||||
};
|
||||
|
||||
export const LANGUAGES = [
|
||||
{
|
||||
key: 'en',
|
||||
name: 'English',
|
||||
},
|
||||
{
|
||||
key: 'es',
|
||||
name: 'Español',
|
||||
},
|
||||
{
|
||||
key: 'fr',
|
||||
name: 'Français',
|
||||
},
|
||||
{
|
||||
key: 'pt-br',
|
||||
name: 'Português (BR)',
|
||||
},
|
||||
{
|
||||
key: 'sv',
|
||||
name: 'Svenska',
|
||||
},
|
||||
{
|
||||
key: 'vi',
|
||||
name: 'Tiếng Việt',
|
||||
},
|
||||
{
|
||||
key: 'ru',
|
||||
name: 'Русский',
|
||||
},
|
||||
{
|
||||
key: 'ja',
|
||||
name: '日本語',
|
||||
},
|
||||
];
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
import dateParse from 'date-fns/parse';
|
||||
import dateFormat from 'date-fns/format';
|
||||
import startOfToday from 'date-fns/start_of_today';
|
||||
import subHours from 'date-fns/sub_hours';
|
||||
import addHours from 'date-fns/add_hours';
|
||||
import round from 'lodash/round';
|
||||
|
||||
const formatTime = (time) => {
|
||||
import { STATS_NAMES } from './constants';
|
||||
|
||||
export const formatTime = (time) => {
|
||||
const parsedTime = dateParse(time);
|
||||
return dateFormat(parsedTime, 'HH:mm:ss');
|
||||
};
|
||||
@@ -14,6 +16,10 @@ export const normalizeLogs = logs => logs.map((log) => {
|
||||
time,
|
||||
question,
|
||||
answer: response,
|
||||
reason,
|
||||
client,
|
||||
filterId,
|
||||
rule,
|
||||
} = log;
|
||||
const { host: domain, type } = question;
|
||||
const responsesArray = response ? response.map((response) => {
|
||||
@@ -21,20 +27,27 @@ export const normalizeLogs = logs => logs.map((log) => {
|
||||
return `${type}: ${value} (ttl=${ttl})`;
|
||||
}) : [];
|
||||
return {
|
||||
time: formatTime(time),
|
||||
time,
|
||||
domain,
|
||||
type,
|
||||
response: responsesArray,
|
||||
reason,
|
||||
client,
|
||||
filterId,
|
||||
rule,
|
||||
};
|
||||
});
|
||||
|
||||
export const normalizeHistory = history => Object.keys(history).map((key) => {
|
||||
const id = key.replace(/_/g, ' ').replace(/^\w/, c => c.toUpperCase());
|
||||
let id = STATS_NAMES[key];
|
||||
if (!id) {
|
||||
id = key.replace(/_/g, ' ').replace(/^\w/, c => c.toUpperCase());
|
||||
}
|
||||
|
||||
const today = startOfToday();
|
||||
const dayAgo = subHours(Date.now(), 24);
|
||||
|
||||
const data = history[key].map((item, index) => {
|
||||
const formatHour = dateFormat(addHours(today, index), 'HH:mm');
|
||||
const formatHour = dateFormat(addHours(dayAgo, index), 'ddd HH:00');
|
||||
const roundValue = round(item, 2);
|
||||
|
||||
return {
|
||||
@@ -53,13 +66,22 @@ export const normalizeFilteringStatus = (filteringStatus) => {
|
||||
const { enabled, filters, user_rules: userRules } = filteringStatus;
|
||||
const newFilters = filters ? filters.map((filter) => {
|
||||
const {
|
||||
url, enabled, last_updated: lastUpdated = Date.now(), name = 'Default name', rules_count: rulesCount = 0,
|
||||
id, url, enabled, lastUpdated: lastUpdated = Date.now(), name = 'Default name', rulesCount: rulesCount = 0,
|
||||
} = filter;
|
||||
|
||||
return {
|
||||
url, enabled, lastUpdated: formatTime(lastUpdated), name, rulesCount,
|
||||
id, url, enabled, lastUpdated: formatTime(lastUpdated), name, rulesCount,
|
||||
};
|
||||
}) : [];
|
||||
const newUserRules = Array.isArray(userRules) ? userRules.join('\n') : '';
|
||||
return { enabled, userRules: newUserRules, filters: newFilters };
|
||||
};
|
||||
|
||||
export const getPercent = (amount, number) => {
|
||||
if (amount > 0 && number > 0) {
|
||||
return round(100 / (amount / number), 2);
|
||||
}
|
||||
return 0;
|
||||
};
|
||||
|
||||
export const captitalizeWords = text => text.split(/[ -_]/g).map(str => str.charAt(0).toUpperCase() + str.substr(1)).join(' ');
|
||||
|
||||
83
client/src/helpers/trackers/adguard.json
Normal file
83
client/src/helpers/trackers/adguard.json
Normal file
@@ -0,0 +1,83 @@
|
||||
{
|
||||
"timeUpdated": "2018-10-14",
|
||||
"categories": {
|
||||
"0": "audio_video_player",
|
||||
"1": "comments",
|
||||
"2": "customer_interaction",
|
||||
"3": "pornvertising",
|
||||
"4": "advertising",
|
||||
"5": "essential",
|
||||
"6": "site_analytics",
|
||||
"7": "social_media",
|
||||
"8": "misc",
|
||||
"9": "cdn",
|
||||
"10": "hosting",
|
||||
"11": "unknown",
|
||||
"12": "extensions",
|
||||
"101": "mobile_analytics"
|
||||
},
|
||||
"trackers": {
|
||||
"facebook_audience": {
|
||||
"name": "Facebook Audience Network",
|
||||
"categoryId": 4,
|
||||
"url": "https://www.facebook.com/business/products/audience-network"
|
||||
},
|
||||
"crashlytics": {
|
||||
"name": "Crashlytics",
|
||||
"categoryId": 101,
|
||||
"url": "https://crashlytics.com/"
|
||||
},
|
||||
"flurry": {
|
||||
"name": "Flurry",
|
||||
"categoryId": 101,
|
||||
"url": "http://www.flurry.com/"
|
||||
},
|
||||
"hockeyapp": {
|
||||
"name": "HockeyApp",
|
||||
"categoryId": 101,
|
||||
"url": "https://hockeyapp.net/"
|
||||
},
|
||||
"firebase": {
|
||||
"name": "Firebase",
|
||||
"categoryId": 101,
|
||||
"url": "https://firebase.google.com/"
|
||||
},
|
||||
"appsflyer": {
|
||||
"name": "AppsFlyer",
|
||||
"categoryId": 101,
|
||||
"url": "https://www.appsflyer.com/"
|
||||
},
|
||||
"yandex_appmetrica": {
|
||||
"name": "Yandex AppMetrica",
|
||||
"categoryId": 101,
|
||||
"url": "https://appmetrica.yandex.com/"
|
||||
},
|
||||
"adjust": {
|
||||
"name": "Adjust",
|
||||
"categoryId": 101,
|
||||
"url": "https://www.adjust.com/"
|
||||
},
|
||||
"branch": {
|
||||
"name": "Branch.io",
|
||||
"categoryId": 101,
|
||||
"url": "https://branch.io/"
|
||||
},
|
||||
"adocean": {
|
||||
"name": "Gemius Adocean",
|
||||
"categoryId": 4,
|
||||
"url": "https://adocean-global.com/"
|
||||
}
|
||||
},
|
||||
"trackerDomains": {
|
||||
"graph.facebook.com": "facebook_audience",
|
||||
"crashlytics.com": "crashlytics",
|
||||
"flurry.com": "flurry",
|
||||
"hockeyapp.net": "hockeyapp",
|
||||
"app-measurement.com": "firebase",
|
||||
"appsflyer.com": "appsflyer",
|
||||
"appmetrica.yandex.com": "yandex_appmetrica",
|
||||
"adjust.com": "adjust",
|
||||
"mobileapptracking.com": "branch",
|
||||
"adocean.cz": "adocean"
|
||||
}
|
||||
}
|
||||
104
client/src/helpers/trackers/trackers.js
Normal file
104
client/src/helpers/trackers/trackers.js
Normal file
@@ -0,0 +1,104 @@
|
||||
import whotracksmeDb from './whotracksme.json';
|
||||
import adguardDb from './adguard.json';
|
||||
import { REPOSITORY } from '../constants';
|
||||
|
||||
/**
|
||||
@typedef TrackerData
|
||||
@type {object}
|
||||
@property {string} id - tracker ID.
|
||||
@property {string} name - tracker name.
|
||||
@property {string} url - tracker website url.
|
||||
@property {number} category - tracker category.
|
||||
@property {source} source - tracker data source.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Tracker data sources
|
||||
*/
|
||||
export const sources = {
|
||||
WHOTRACKSME: 1,
|
||||
ADGUARD: 2,
|
||||
};
|
||||
|
||||
/**
|
||||
* Gets tracker data in the specified database
|
||||
*
|
||||
* @param {String} domainName domain name to check
|
||||
* @param {*} trackersDb trackers database
|
||||
* @param {number} source source ID
|
||||
* @returns {TrackerData} tracker data or null if no matching tracker found
|
||||
*/
|
||||
const getTrackerDataFromDb = (domainName, trackersDb, source) => {
|
||||
if (!domainName) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const parts = domainName.split(/\./g).reverse();
|
||||
let hostToCheck = '';
|
||||
|
||||
// Check every subdomain
|
||||
for (let i = 0; i < parts.length; i += 1) {
|
||||
hostToCheck = parts[i] + (i > 0 ? '.' : '') + hostToCheck;
|
||||
const trackerId = trackersDb.trackerDomains[hostToCheck];
|
||||
|
||||
if (trackerId) {
|
||||
const trackerData = trackersDb.trackers[trackerId];
|
||||
const categoryName = trackersDb.categories[trackerData.categoryId];
|
||||
|
||||
return {
|
||||
id: trackerId,
|
||||
name: trackerData.name,
|
||||
url: trackerData.url,
|
||||
category: categoryName,
|
||||
source,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// No tracker found for the specified domain
|
||||
return null;
|
||||
};
|
||||
|
||||
/**
|
||||
* Gets the source metadata for the specified tracker
|
||||
* @param {TrackerData} trackerData tracker data
|
||||
* @returns {source} source metadata or null if no matching tracker found
|
||||
*/
|
||||
export const getSourceData = (trackerData) => {
|
||||
if (!trackerData || !trackerData.source) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (trackerData.source === sources.WHOTRACKSME) {
|
||||
return {
|
||||
name: 'Whotracks.me',
|
||||
url: `https://whotracks.me/trackers/${trackerData.id}.html`,
|
||||
};
|
||||
} else if (trackerData.source === sources.ADGUARD) {
|
||||
return {
|
||||
name: 'AdGuard',
|
||||
url: REPOSITORY.TRACKERS_DB,
|
||||
};
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
/**
|
||||
* Gets tracker data from the trackers database
|
||||
*
|
||||
* @param {String} domainName domain name to check
|
||||
* @returns {TrackerData} tracker data or null if no matching tracker found
|
||||
*/
|
||||
export const getTrackerData = (domainName) => {
|
||||
if (!domainName) {
|
||||
return null;
|
||||
}
|
||||
|
||||
let data = getTrackerDataFromDb(domainName, adguardDb, sources.ADGUARD);
|
||||
if (!data) {
|
||||
data = getTrackerDataFromDb(domainName, whotracksmeDb, sources.WHOTRACKSME);
|
||||
}
|
||||
|
||||
return data;
|
||||
};
|
||||
8628
client/src/helpers/trackers/whotracksme.json
Normal file
8628
client/src/helpers/trackers/whotracksme.json
Normal file
File diff suppressed because it is too large
Load Diff
63
client/src/helpers/versionCompare.js
Normal file
63
client/src/helpers/versionCompare.js
Normal file
@@ -0,0 +1,63 @@
|
||||
/*
|
||||
* Project: tiny-version-compare https://github.com/bfred-it/tiny-version-compare
|
||||
* License (MIT) https://github.com/bfred-it/tiny-version-compare/blob/master/LICENSE
|
||||
*/
|
||||
const split = v => String(v).replace(/^[vr]/, '') // Drop initial 'v' or 'r'
|
||||
.replace(/([a-z]+)/gi, '.$1.') // Sort each word separately
|
||||
.replace(/[-.]+/g, '.') // Consider dashes as separators (+ trim multiple separators)
|
||||
.split('.');
|
||||
|
||||
// Development versions are considered "negative",
|
||||
// but localeCompare doesn't handle negative numbers.
|
||||
// This offset is applied to reset the lowest development version to 0
|
||||
const offset = (part) => {
|
||||
// Not numeric, return as is
|
||||
if (Number.isNaN(part)) {
|
||||
return part;
|
||||
}
|
||||
return 5 + Number(part);
|
||||
};
|
||||
|
||||
const parsePart = (part) => {
|
||||
// Missing, consider it zero
|
||||
if (typeof part === 'undefined') {
|
||||
return 0;
|
||||
}
|
||||
// Sort development versions
|
||||
switch (part.toLowerCase()) {
|
||||
case 'dev':
|
||||
return -5;
|
||||
case 'alpha':
|
||||
return -4;
|
||||
case 'beta':
|
||||
return -3;
|
||||
case 'rc':
|
||||
return -2;
|
||||
case 'pre':
|
||||
return -1;
|
||||
default:
|
||||
}
|
||||
// Return as is, it’s either a plain number or text that will be sorted alphabetically
|
||||
return part;
|
||||
};
|
||||
|
||||
const versionCompare = (prev, next) => {
|
||||
const a = split(prev);
|
||||
const b = split(next);
|
||||
for (let i = 0; i < a.length || i < b.length; i += 1) {
|
||||
const ai = offset(parsePart(a[i]));
|
||||
const bi = offset(parsePart(b[i]));
|
||||
const sort = String(ai).localeCompare(bi, 'en', {
|
||||
numeric: true,
|
||||
});
|
||||
// Once the difference is found,
|
||||
// stop comparing the rest of the parts
|
||||
if (sort !== 0) {
|
||||
return sort;
|
||||
}
|
||||
}
|
||||
// No difference found
|
||||
return 0;
|
||||
};
|
||||
|
||||
export default versionCompare;
|
||||
59
client/src/i18n.js
Normal file
59
client/src/i18n.js
Normal file
@@ -0,0 +1,59 @@
|
||||
import i18n from 'i18next';
|
||||
import { reactI18nextModule } from 'react-i18next';
|
||||
import { initReactI18n } from 'react-i18next/hooks';
|
||||
import langDetect from 'i18next-browser-languagedetector';
|
||||
|
||||
import vi from './__locales/vi.json';
|
||||
import en from './__locales/en.json';
|
||||
import ru from './__locales/ru.json';
|
||||
import es from './__locales/es.json';
|
||||
import fr from './__locales/fr.json';
|
||||
import ja from './__locales/ja.json';
|
||||
import sv from './__locales/sv.json';
|
||||
import ptBR from './__locales/pt-br.json';
|
||||
|
||||
const resources = {
|
||||
en: {
|
||||
translation: en,
|
||||
},
|
||||
vi: {
|
||||
translation: vi,
|
||||
},
|
||||
ru: {
|
||||
translation: ru,
|
||||
},
|
||||
es: {
|
||||
translation: es,
|
||||
},
|
||||
fr: {
|
||||
translation: fr,
|
||||
},
|
||||
ja: {
|
||||
translation: ja,
|
||||
},
|
||||
sv: {
|
||||
translation: sv,
|
||||
},
|
||||
'pt-BR': {
|
||||
translation: ptBR,
|
||||
},
|
||||
};
|
||||
|
||||
i18n
|
||||
.use(langDetect)
|
||||
.use(initReactI18n)
|
||||
.use(reactI18nextModule) // passes i18n down to react-i18next
|
||||
.init({
|
||||
resources,
|
||||
fallbackLng: 'en',
|
||||
keySeparator: false, // we use content as keys
|
||||
nsSeparator: false, // Fix character in content
|
||||
interpolation: {
|
||||
escapeValue: false, // not needed for react!!
|
||||
},
|
||||
react: {
|
||||
wait: true,
|
||||
},
|
||||
});
|
||||
|
||||
export default i18n;
|
||||
@@ -5,6 +5,7 @@ import './components/App/index.css';
|
||||
import App from './containers/App';
|
||||
import configureStore from './configureStore';
|
||||
import reducers from './reducers';
|
||||
import './i18n';
|
||||
|
||||
const store = configureStore(reducers, {}); // set initial state
|
||||
ReactDOM.render(
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
import { combineReducers } from 'redux';
|
||||
import { handleActions } from 'redux-actions';
|
||||
import { loadingBarReducer } from 'react-redux-loading-bar';
|
||||
import nanoid from 'nanoid';
|
||||
import versionCompare from '../helpers/versionCompare';
|
||||
|
||||
import * as actions from '../actions';
|
||||
|
||||
@@ -24,14 +27,14 @@ const settings = handleActions({
|
||||
[actions.setUpstreamRequest]: state => ({ ...state, processingUpstream: true }),
|
||||
[actions.setUpstreamFailure]: state => ({ ...state, processingUpstream: false }),
|
||||
[actions.setUpstreamSuccess]: state => ({ ...state, processingUpstream: false }),
|
||||
[actions.handleUpstreamChange]: (state, { payload }) => {
|
||||
const { upstream } = payload;
|
||||
return { ...state, upstream };
|
||||
},
|
||||
|
||||
[actions.testUpstreamRequest]: state => ({ ...state, processingTestUpstream: true }),
|
||||
[actions.testUpstreamFailure]: state => ({ ...state, processingTestUpstream: false }),
|
||||
[actions.testUpstreamSuccess]: state => ({ ...state, processingTestUpstream: false }),
|
||||
}, {
|
||||
processing: true,
|
||||
processingUpstream: true,
|
||||
upstream: '',
|
||||
processingTestUpstream: false,
|
||||
processingSetUpstream: false,
|
||||
});
|
||||
|
||||
const dashboard = handleActions({
|
||||
@@ -44,6 +47,9 @@ const dashboard = handleActions({
|
||||
dns_port: dnsPort,
|
||||
dns_address: dnsAddress,
|
||||
querylog_enabled: queryLogEnabled,
|
||||
upstream_dns: upstreamDns,
|
||||
protection_enabled: protectionEnabled,
|
||||
language,
|
||||
} = payload;
|
||||
const newState = {
|
||||
...state,
|
||||
@@ -53,6 +59,9 @@ const dashboard = handleActions({
|
||||
dnsPort,
|
||||
dnsAddress,
|
||||
queryLogEnabled,
|
||||
upstreamDns: upstreamDns.join('\n'),
|
||||
protectionEnabled,
|
||||
language,
|
||||
};
|
||||
return newState;
|
||||
},
|
||||
@@ -98,12 +107,61 @@ const dashboard = handleActions({
|
||||
const { queryLogEnabled } = state;
|
||||
return ({ ...state, queryLogEnabled: !queryLogEnabled, logStatusProcessing: false });
|
||||
},
|
||||
|
||||
[actions.getVersionRequest]: state => ({ ...state, processingVersion: true }),
|
||||
[actions.getVersionFailure]: state => ({ ...state, processingVersion: false }),
|
||||
[actions.getVersionSuccess]: (state, { payload }) => {
|
||||
const currentVersion = state.dnsVersion === 'undefined' ? 0 : state.dnsVersion;
|
||||
|
||||
if (versionCompare(currentVersion, payload.version) === -1) {
|
||||
const {
|
||||
announcement,
|
||||
announcement_url: announcementUrl,
|
||||
} = payload;
|
||||
|
||||
const newState = {
|
||||
...state,
|
||||
announcement,
|
||||
announcementUrl,
|
||||
isUpdateAvailable: true,
|
||||
};
|
||||
return newState;
|
||||
}
|
||||
|
||||
return state;
|
||||
},
|
||||
|
||||
[actions.getFilteringRequest]: state => ({ ...state, processingFiltering: true }),
|
||||
[actions.getFilteringFailure]: state => ({ ...state, processingFiltering: false }),
|
||||
[actions.getFilteringSuccess]: (state, { payload }) => {
|
||||
const newState = { ...state, isFilteringEnabled: payload, processingFiltering: false };
|
||||
return newState;
|
||||
},
|
||||
|
||||
[actions.toggleProtectionSuccess]: (state) => {
|
||||
const newState = { ...state, protectionEnabled: !state.protectionEnabled };
|
||||
return newState;
|
||||
},
|
||||
|
||||
[actions.handleUpstreamChange]: (state, { payload }) => {
|
||||
const { upstreamDns } = payload;
|
||||
return { ...state, upstreamDns };
|
||||
},
|
||||
|
||||
[actions.getLanguageSuccess]: (state, { payload }) => {
|
||||
const newState = { ...state, language: payload };
|
||||
return newState;
|
||||
},
|
||||
}, {
|
||||
processing: true,
|
||||
isCoreRunning: false,
|
||||
processingTopStats: true,
|
||||
processingStats: true,
|
||||
logStatusProcessing: false,
|
||||
processingVersion: true,
|
||||
processingFiltering: true,
|
||||
upstreamDns: [],
|
||||
protectionEnabled: false,
|
||||
});
|
||||
|
||||
const queryLogs = handleActions({
|
||||
@@ -172,9 +230,39 @@ const filtering = handleActions({
|
||||
userRules: '',
|
||||
});
|
||||
|
||||
const toasts = handleActions({
|
||||
[actions.addErrorToast]: (state, { payload }) => {
|
||||
const errorToast = {
|
||||
id: nanoid(),
|
||||
message: payload.error.toString(),
|
||||
type: 'error',
|
||||
};
|
||||
|
||||
const newState = { ...state, notices: [...state.notices, errorToast] };
|
||||
return newState;
|
||||
},
|
||||
[actions.addSuccessToast]: (state, { payload }) => {
|
||||
const successToast = {
|
||||
id: nanoid(),
|
||||
message: payload,
|
||||
type: 'success',
|
||||
};
|
||||
|
||||
const newState = { ...state, notices: [...state.notices, successToast] };
|
||||
return newState;
|
||||
},
|
||||
[actions.removeToast]: (state, { payload }) => {
|
||||
const filtered = state.notices.filter(notice => notice.id !== payload);
|
||||
const newState = { ...state, notices: filtered };
|
||||
return newState;
|
||||
},
|
||||
}, { notices: [] });
|
||||
|
||||
export default combineReducers({
|
||||
settings,
|
||||
dashboard,
|
||||
queryLogs,
|
||||
filtering,
|
||||
toasts,
|
||||
loadingBar: loadingBarReducer,
|
||||
});
|
||||
|
||||
11
client/webpack.common.js
vendored
11
client/webpack.common.js
vendored
@@ -4,6 +4,7 @@ const HtmlWebpackPlugin = require('html-webpack-plugin');
|
||||
const ExtractTextPlugin = require('extract-text-webpack-plugin');
|
||||
const webpack = require('webpack');
|
||||
const flexBugsFixes = require('postcss-flexbugs-fixes');
|
||||
const CleanWebpackPlugin = require('clean-webpack-plugin');
|
||||
|
||||
const RESOURCES_PATH = path.resolve(__dirname);
|
||||
const ENTRY_REACT = path.resolve(RESOURCES_PATH, 'src/index.js');
|
||||
@@ -19,7 +20,7 @@ const config = {
|
||||
},
|
||||
output: {
|
||||
path: PUBLIC_PATH,
|
||||
filename: '[name].js',
|
||||
filename: '[name].[chunkhash].js',
|
||||
},
|
||||
resolve: {
|
||||
modules: ['node_modules'],
|
||||
@@ -92,12 +93,18 @@ const config = {
|
||||
new webpack.DefinePlugin({
|
||||
'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV),
|
||||
}),
|
||||
new CleanWebpackPlugin(['*.*'], {
|
||||
root: PUBLIC_PATH,
|
||||
verbose: false,
|
||||
dry: false,
|
||||
}),
|
||||
new HtmlWebpackPlugin({
|
||||
inject: true,
|
||||
cache: false,
|
||||
template: HTML_PATH,
|
||||
}),
|
||||
new ExtractTextPlugin({
|
||||
filename: '[name].css',
|
||||
filename: '[name].[contenthash].css',
|
||||
}),
|
||||
],
|
||||
};
|
||||
|
||||
9626
client/yarn.lock
vendored
9626
client/yarn.lock
vendored
File diff suppressed because it is too large
Load Diff
234
config.go
234
config.go
@@ -14,79 +14,141 @@ import (
|
||||
"gopkg.in/yaml.v2"
|
||||
)
|
||||
|
||||
// Current schema version. We compare it with the value from
|
||||
// the configuration file and perform necessary upgrade operations if needed
|
||||
const SchemaVersion = 1
|
||||
|
||||
// Directory where we'll store all downloaded filters contents
|
||||
const FiltersDir = "filters"
|
||||
|
||||
// User filter ID is always 0
|
||||
const UserFilterId = 0
|
||||
|
||||
// Just a counter that we use for incrementing the filter ID
|
||||
var NextFilterId = time.Now().Unix()
|
||||
|
||||
// configuration is loaded from YAML
|
||||
type configuration struct {
|
||||
// Config filename (can be overriden via the command line arguments)
|
||||
ourConfigFilename string
|
||||
ourBinaryDir string
|
||||
// Basically, this is our working directory
|
||||
ourBinaryDir string
|
||||
// Directory to store data (i.e. filters contents)
|
||||
ourDataDir string
|
||||
|
||||
BindHost string `yaml:"bind_host"`
|
||||
BindPort int `yaml:"bind_port"`
|
||||
CoreDNS coreDNSConfig `yaml:"coredns"`
|
||||
Filters []filter `yaml:"filters"`
|
||||
UserRules []string `yaml:"user_rules"`
|
||||
// Schema version of the config file. This value is used when performing the app updates.
|
||||
SchemaVersion int `yaml:"schema_version"`
|
||||
BindHost string `yaml:"bind_host"`
|
||||
BindPort int `yaml:"bind_port"`
|
||||
AuthName string `yaml:"auth_name"`
|
||||
AuthPass string `yaml:"auth_pass"`
|
||||
CoreDNS coreDNSConfig `yaml:"coredns"`
|
||||
Filters []filter `yaml:"filters"`
|
||||
UserRules []string `yaml:"user_rules"`
|
||||
Language string `yaml:"language"` // two-letter ISO 639-1 language code
|
||||
|
||||
sync.Mutex `yaml:"-"`
|
||||
sync.RWMutex `yaml:"-"`
|
||||
}
|
||||
|
||||
type coreDnsFilter struct {
|
||||
ID int64 `yaml:"-"`
|
||||
Path string `yaml:"-"`
|
||||
}
|
||||
|
||||
type coreDNSConfig struct {
|
||||
Port int `yaml:"port"`
|
||||
binaryFile string
|
||||
coreFile string
|
||||
FilterFile string `yaml:"-"`
|
||||
FilteringEnabled bool `yaml:"filtering_enabled"`
|
||||
SafeBrowsingEnabled bool `yaml:"safebrowsing_enabled"`
|
||||
SafeSearchEnabled bool `yaml:"safesearch_enabled"`
|
||||
ParentalEnabled bool `yaml:"parental_enabled"`
|
||||
ParentalSensitivity int `yaml:"parental_sensitivity"`
|
||||
QueryLogEnabled bool `yaml:"querylog_enabled"`
|
||||
Pprof string `yaml:"pprof"`
|
||||
UpstreamDNS []string `yaml:"upstream_dns"`
|
||||
Cache string `yaml:"cache"`
|
||||
Prometheus string `yaml:"prometheus"`
|
||||
Filters []coreDnsFilter `yaml:"-"`
|
||||
Port int `yaml:"port"`
|
||||
ProtectionEnabled bool `yaml:"protection_enabled"`
|
||||
FilteringEnabled bool `yaml:"filtering_enabled"`
|
||||
SafeBrowsingEnabled bool `yaml:"safebrowsing_enabled"`
|
||||
SafeSearchEnabled bool `yaml:"safesearch_enabled"`
|
||||
ParentalEnabled bool `yaml:"parental_enabled"`
|
||||
ParentalSensitivity int `yaml:"parental_sensitivity"`
|
||||
BlockedResponseTTL int `yaml:"blocked_response_ttl"`
|
||||
QueryLogEnabled bool `yaml:"querylog_enabled"`
|
||||
Ratelimit int `yaml:"ratelimit"`
|
||||
RefuseAny bool `yaml:"refuse_any"`
|
||||
Pprof string `yaml:"-"`
|
||||
Cache string `yaml:"-"`
|
||||
Prometheus string `yaml:"-"`
|
||||
BootstrapDNS string `yaml:"bootstrap_dns"`
|
||||
UpstreamDNS []string `yaml:"upstream_dns"`
|
||||
}
|
||||
|
||||
type filter struct {
|
||||
Enabled bool `json:"enabled"`
|
||||
ID int64 `json:"id" yaml:"id"` // auto-assigned when filter is added (see NextFilterId)
|
||||
URL string `json:"url"`
|
||||
RulesCount int `json:"rules_count" yaml:"-"`
|
||||
Name string `json:"name" yaml:"-"`
|
||||
Name string `json:"name" yaml:"name"`
|
||||
Enabled bool `json:"enabled"`
|
||||
RulesCount int `json:"rulesCount" yaml:"-"`
|
||||
contents []byte
|
||||
LastUpdated time.Time `json:"last_updated" yaml:"-"`
|
||||
LastUpdated time.Time `json:"lastUpdated" yaml:"last_updated"`
|
||||
}
|
||||
|
||||
var defaultDNS = []string{"1.1.1.1", "1.0.0.1"}
|
||||
var defaultDNS = []string{"tls://1.1.1.1", "tls://1.0.0.1"}
|
||||
|
||||
// initialize to default values, will be changed later when reading config or parsing command line
|
||||
var config = configuration{
|
||||
ourConfigFilename: "AdguardDNS.yaml",
|
||||
ourConfigFilename: "AdGuardHome.yaml",
|
||||
ourDataDir: "data",
|
||||
BindPort: 3000,
|
||||
BindHost: "127.0.0.1",
|
||||
CoreDNS: coreDNSConfig{
|
||||
Port: 53,
|
||||
binaryFile: "coredns", // only filename, no path
|
||||
coreFile: "Corefile", // only filename, no path
|
||||
FilterFile: "dnsfilter.txt", // only filename, no path
|
||||
binaryFile: "coredns", // only filename, no path
|
||||
coreFile: "Corefile", // only filename, no path
|
||||
ProtectionEnabled: true,
|
||||
FilteringEnabled: true,
|
||||
SafeBrowsingEnabled: true,
|
||||
SafeBrowsingEnabled: false,
|
||||
BlockedResponseTTL: 10, // in seconds
|
||||
QueryLogEnabled: true,
|
||||
Ratelimit: 20,
|
||||
RefuseAny: true,
|
||||
BootstrapDNS: "8.8.8.8:53",
|
||||
UpstreamDNS: defaultDNS,
|
||||
Cache: "cache",
|
||||
Prometheus: "prometheus :9153",
|
||||
},
|
||||
Filters: []filter{
|
||||
{Enabled: true, URL: "https://filters.adtidy.org/windows/filters/15.txt"},
|
||||
{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://hosts-file.net/ad_servers.txt", Name: "hpHosts - Ad and Tracking servers only"},
|
||||
{ID: 4, Enabled: false, URL: "http://www.malwaredomainlist.com/hostslist/hosts.txt", Name: "MalwareDomainList.com Hosts List"},
|
||||
},
|
||||
}
|
||||
|
||||
// Creates a helper object for working with the user rules
|
||||
func getUserFilter() filter {
|
||||
|
||||
// TODO: This should be calculated when UserRules are set
|
||||
var contents []byte
|
||||
for _, rule := range config.UserRules {
|
||||
contents = append(contents, []byte(rule)...)
|
||||
contents = append(contents, '\n')
|
||||
}
|
||||
|
||||
userFilter := filter{
|
||||
// User filter always has constant ID=0
|
||||
ID: UserFilterId,
|
||||
contents: contents,
|
||||
Enabled: true,
|
||||
}
|
||||
|
||||
return userFilter
|
||||
}
|
||||
|
||||
// Loads configuration from the YAML file
|
||||
func parseConfig() error {
|
||||
configfile := filepath.Join(config.ourBinaryDir, config.ourConfigFilename)
|
||||
log.Printf("Reading YAML file: %s", configfile)
|
||||
if _, err := os.Stat(configfile); os.IsNotExist(err) {
|
||||
configFile := filepath.Join(config.ourBinaryDir, config.ourConfigFilename)
|
||||
log.Printf("Reading YAML file: %s", configFile)
|
||||
if _, err := os.Stat(configFile); os.IsNotExist(err) {
|
||||
// do nothing, file doesn't exist
|
||||
log.Printf("YAML file doesn't exist, skipping: %s", configfile)
|
||||
log.Printf("YAML file doesn't exist, skipping: %s", configFile)
|
||||
return nil
|
||||
}
|
||||
yamlFile, err := ioutil.ReadFile(configfile)
|
||||
yamlFile, err := ioutil.ReadFile(configFile)
|
||||
if err != nil {
|
||||
log.Printf("Couldn't read config file: %s", err)
|
||||
return err
|
||||
@@ -97,22 +159,54 @@ func parseConfig() error {
|
||||
return err
|
||||
}
|
||||
|
||||
// Deduplicate filters
|
||||
{
|
||||
i := 0 // output index, used for deletion later
|
||||
urls := map[string]bool{}
|
||||
for _, filter := range config.Filters {
|
||||
if _, ok := urls[filter.URL]; !ok {
|
||||
// we didn't see it before, keep it
|
||||
urls[filter.URL] = true // remember the URL
|
||||
config.Filters[i] = filter
|
||||
i++
|
||||
}
|
||||
}
|
||||
// all entries we want to keep are at front, delete the rest
|
||||
config.Filters = config.Filters[:i]
|
||||
}
|
||||
|
||||
// Set the next filter ID to max(filter.ID) + 1
|
||||
for i := range config.Filters {
|
||||
if NextFilterId < config.Filters[i].ID {
|
||||
NextFilterId = config.Filters[i].ID + 1
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Saves configuration to the YAML file and also saves the user filter contents to a file
|
||||
func writeConfig() error {
|
||||
configfile := filepath.Join(config.ourBinaryDir, config.ourConfigFilename)
|
||||
log.Printf("Writing YAML file: %s", configfile)
|
||||
configFile := filepath.Join(config.ourBinaryDir, config.ourConfigFilename)
|
||||
log.Printf("Writing YAML file: %s", configFile)
|
||||
yamlText, err := yaml.Marshal(&config)
|
||||
if err != nil {
|
||||
log.Printf("Couldn't generate YAML file: %s", err)
|
||||
return err
|
||||
}
|
||||
err = ioutil.WriteFile(configfile, yamlText, 0644)
|
||||
err = writeFileSafe(configFile, yamlText)
|
||||
if err != nil {
|
||||
log.Printf("Couldn't write YAML config: %s", err)
|
||||
log.Printf("Couldn't save YAML config: %s", err)
|
||||
return err
|
||||
}
|
||||
|
||||
userFilter := getUserFilter()
|
||||
err = userFilter.save()
|
||||
if err != nil {
|
||||
log.Printf("Couldn't save the user filter: %s", err)
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -120,18 +214,19 @@ func writeConfig() error {
|
||||
// coredns config
|
||||
// --------------
|
||||
func writeCoreDNSConfig() error {
|
||||
corefile := filepath.Join(config.ourBinaryDir, config.CoreDNS.coreFile)
|
||||
log.Printf("Writing DNS config: %s", corefile)
|
||||
configtext, err := generateCoreDNSConfigText()
|
||||
coreFile := filepath.Join(config.ourBinaryDir, config.CoreDNS.coreFile)
|
||||
log.Printf("Writing DNS config: %s", coreFile)
|
||||
configText, err := generateCoreDNSConfigText()
|
||||
if err != nil {
|
||||
log.Printf("Couldn't generate DNS config: %s", err)
|
||||
return err
|
||||
}
|
||||
err = ioutil.WriteFile(corefile, []byte(configtext), 0644)
|
||||
err = writeFileSafe(coreFile, []byte(configText))
|
||||
if err != nil {
|
||||
log.Printf("Couldn't write DNS config: %s", err)
|
||||
log.Printf("Couldn't save DNS config: %s", err)
|
||||
return err
|
||||
}
|
||||
return err
|
||||
return nil
|
||||
}
|
||||
|
||||
func writeAllConfigs() error {
|
||||
@@ -148,15 +243,26 @@ func writeAllConfigs() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
const coreDNSConfigTemplate = `. {
|
||||
{{if .FilteringEnabled}}dnsfilter {{.FilterFile}} {
|
||||
const coreDNSConfigTemplate = `.:{{.Port}} {
|
||||
{{if .ProtectionEnabled}}dnsfilter {
|
||||
{{if .SafeBrowsingEnabled}}safebrowsing{{end}}
|
||||
{{if .ParentalEnabled}}parental {{.ParentalSensitivity}}{{end}}
|
||||
{{if .SafeSearchEnabled}}safesearch{{end}}
|
||||
{{if .QueryLogEnabled}}querylog{{end}}
|
||||
blocked_ttl {{.BlockedResponseTTL}}
|
||||
{{if .FilteringEnabled}}
|
||||
{{range .Filters}}
|
||||
filter {{.ID}} "{{.Path}}"
|
||||
{{end}}
|
||||
{{end}}
|
||||
}{{end}}
|
||||
{{.Pprof}}
|
||||
{{if .UpstreamDNS}}forward . {{range .UpstreamDNS}}{{.}} {{end}}{{end}}
|
||||
{{if .RefuseAny}}refuseany{{end}}
|
||||
{{if gt .Ratelimit 0}}ratelimit {{.Ratelimit}}{{end}}
|
||||
hosts {
|
||||
fallthrough
|
||||
}
|
||||
{{if .UpstreamDNS}}upstream {{range .UpstreamDNS}}{{.}} {{end}} { bootstrap {{.BootstrapDNS}} }{{end}}
|
||||
{{.Cache}}
|
||||
{{.Prometheus}}
|
||||
}
|
||||
@@ -164,7 +270,7 @@ const coreDNSConfigTemplate = `. {
|
||||
|
||||
var removeEmptyLines = regexp.MustCompile("([\t ]*\n)+")
|
||||
|
||||
// generate config text
|
||||
// generate CoreDNS config text
|
||||
func generateCoreDNSConfigText() (string, error) {
|
||||
t, err := template.New("config").Parse(coreDNSConfigTemplate)
|
||||
if err != nil {
|
||||
@@ -173,15 +279,37 @@ func generateCoreDNSConfigText() (string, error) {
|
||||
}
|
||||
|
||||
var configBytes bytes.Buffer
|
||||
temporaryConfig := config.CoreDNS
|
||||
|
||||
// fill the list of filters
|
||||
filters := make([]coreDnsFilter, 0)
|
||||
|
||||
// first of all, append the user filter
|
||||
userFilter := getUserFilter()
|
||||
|
||||
if len(userFilter.contents) > 0 {
|
||||
filters = append(filters, coreDnsFilter{ID: userFilter.ID, Path: userFilter.getFilterFilePath()})
|
||||
}
|
||||
|
||||
// then go through other filters
|
||||
for i := range config.Filters {
|
||||
filter := &config.Filters[i]
|
||||
|
||||
if filter.Enabled && len(filter.contents) > 0 {
|
||||
filters = append(filters, coreDnsFilter{ID: filter.ID, Path: filter.getFilterFilePath()})
|
||||
}
|
||||
}
|
||||
temporaryConfig.Filters = filters
|
||||
|
||||
// run the template
|
||||
err = t.Execute(&configBytes, config.CoreDNS)
|
||||
err = t.Execute(&configBytes, &temporaryConfig)
|
||||
if err != nil {
|
||||
log.Printf("Couldn't generate DNS config: %s", err)
|
||||
return "", err
|
||||
}
|
||||
configtext := configBytes.String()
|
||||
configText := configBytes.String()
|
||||
|
||||
// remove empty lines from generated config
|
||||
configtext = removeEmptyLines.ReplaceAllString(configtext, "\n")
|
||||
return configtext, nil
|
||||
configText = removeEmptyLines.ReplaceAllString(configText, "\n")
|
||||
return configText, nil
|
||||
}
|
||||
|
||||
1070
control.go
1070
control.go
File diff suppressed because it is too large
Load Diff
132
coredns.go
Normal file
132
coredns.go
Normal file
@@ -0,0 +1,132 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sync" // Include all plugins.
|
||||
|
||||
_ "github.com/AdguardTeam/AdGuardHome/coredns_plugin"
|
||||
_ "github.com/AdguardTeam/AdGuardHome/coredns_plugin/ratelimit"
|
||||
_ "github.com/AdguardTeam/AdGuardHome/coredns_plugin/refuseany"
|
||||
_ "github.com/AdguardTeam/AdGuardHome/upstream"
|
||||
"github.com/coredns/coredns/core/dnsserver"
|
||||
"github.com/coredns/coredns/coremain"
|
||||
_ "github.com/coredns/coredns/plugin/auto"
|
||||
_ "github.com/coredns/coredns/plugin/autopath"
|
||||
_ "github.com/coredns/coredns/plugin/bind"
|
||||
_ "github.com/coredns/coredns/plugin/cache"
|
||||
_ "github.com/coredns/coredns/plugin/chaos"
|
||||
_ "github.com/coredns/coredns/plugin/debug"
|
||||
_ "github.com/coredns/coredns/plugin/dnssec"
|
||||
_ "github.com/coredns/coredns/plugin/dnstap"
|
||||
_ "github.com/coredns/coredns/plugin/erratic"
|
||||
_ "github.com/coredns/coredns/plugin/errors"
|
||||
_ "github.com/coredns/coredns/plugin/file"
|
||||
_ "github.com/coredns/coredns/plugin/forward"
|
||||
_ "github.com/coredns/coredns/plugin/health"
|
||||
_ "github.com/coredns/coredns/plugin/hosts"
|
||||
_ "github.com/coredns/coredns/plugin/loadbalance"
|
||||
_ "github.com/coredns/coredns/plugin/log"
|
||||
_ "github.com/coredns/coredns/plugin/loop"
|
||||
_ "github.com/coredns/coredns/plugin/metadata"
|
||||
_ "github.com/coredns/coredns/plugin/metrics"
|
||||
_ "github.com/coredns/coredns/plugin/nsid"
|
||||
_ "github.com/coredns/coredns/plugin/pprof"
|
||||
_ "github.com/coredns/coredns/plugin/proxy"
|
||||
_ "github.com/coredns/coredns/plugin/reload"
|
||||
_ "github.com/coredns/coredns/plugin/rewrite"
|
||||
_ "github.com/coredns/coredns/plugin/root"
|
||||
_ "github.com/coredns/coredns/plugin/secondary"
|
||||
_ "github.com/coredns/coredns/plugin/template"
|
||||
_ "github.com/coredns/coredns/plugin/tls"
|
||||
_ "github.com/coredns/coredns/plugin/whoami"
|
||||
_ "github.com/mholt/caddy/onevent"
|
||||
)
|
||||
|
||||
// Directives are registered in the order they should be
|
||||
// executed.
|
||||
//
|
||||
// Ordering is VERY important. Every plugin will
|
||||
// feel the effects of all other plugin below
|
||||
// (after) them during a request, but they must not
|
||||
// care what plugin above them are doing.
|
||||
|
||||
var directives = []string{
|
||||
"metadata",
|
||||
"tls",
|
||||
"reload",
|
||||
"nsid",
|
||||
"root",
|
||||
"bind",
|
||||
"debug",
|
||||
"health",
|
||||
"pprof",
|
||||
"prometheus",
|
||||
"errors",
|
||||
"log",
|
||||
"refuseany",
|
||||
"ratelimit",
|
||||
"dnsfilter",
|
||||
"dnstap",
|
||||
"chaos",
|
||||
"loadbalance",
|
||||
"cache",
|
||||
"rewrite",
|
||||
"dnssec",
|
||||
"autopath",
|
||||
"template",
|
||||
"hosts",
|
||||
"file",
|
||||
"auto",
|
||||
"secondary",
|
||||
"loop",
|
||||
"forward",
|
||||
"proxy",
|
||||
"upstream",
|
||||
"erratic",
|
||||
"whoami",
|
||||
"on",
|
||||
}
|
||||
|
||||
func init() {
|
||||
dnsserver.Directives = directives
|
||||
}
|
||||
|
||||
var (
|
||||
isCoreDNSRunningLock sync.Mutex
|
||||
isCoreDNSRunning = false
|
||||
)
|
||||
|
||||
func isRunning() bool {
|
||||
isCoreDNSRunningLock.Lock()
|
||||
value := isCoreDNSRunning
|
||||
isCoreDNSRunningLock.Unlock()
|
||||
return value
|
||||
}
|
||||
|
||||
func startDNSServer() error {
|
||||
isCoreDNSRunningLock.Lock()
|
||||
if isCoreDNSRunning {
|
||||
isCoreDNSRunningLock.Unlock()
|
||||
return fmt.Errorf("Unable to start coreDNS: Already running")
|
||||
}
|
||||
isCoreDNSRunning = true
|
||||
isCoreDNSRunningLock.Unlock()
|
||||
|
||||
configpath := filepath.Join(config.ourBinaryDir, config.CoreDNS.coreFile)
|
||||
os.Args = os.Args[:1]
|
||||
os.Args = append(os.Args, "-conf")
|
||||
os.Args = append(os.Args, configpath)
|
||||
|
||||
err := writeCoreDNSConfig()
|
||||
if err != nil {
|
||||
errortext := fmt.Errorf("Unable to write coredns config: %s", err)
|
||||
log.Println(errortext)
|
||||
return errortext
|
||||
}
|
||||
|
||||
go coremain.Run()
|
||||
return nil
|
||||
}
|
||||
@@ -12,20 +12,16 @@ import (
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/AdguardTeam/AdGuardHome/dnsfilter"
|
||||
"github.com/coredns/coredns/core/dnsserver"
|
||||
"github.com/coredns/coredns/plugin"
|
||||
"github.com/coredns/coredns/plugin/metrics"
|
||||
"github.com/coredns/coredns/plugin/pkg/dnstest"
|
||||
"github.com/coredns/coredns/plugin/pkg/upstream"
|
||||
"github.com/coredns/coredns/request"
|
||||
|
||||
"github.com/mholt/caddy"
|
||||
|
||||
"github.com/miekg/dns"
|
||||
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
|
||||
"github.com/AdguardTeam/AdguardDNS/dnsfilter"
|
||||
"golang.org/x/net/context"
|
||||
)
|
||||
|
||||
@@ -45,73 +41,63 @@ func init() {
|
||||
})
|
||||
}
|
||||
|
||||
type Plugin struct {
|
||||
d *dnsfilter.Dnsfilter
|
||||
Next plugin.Handler
|
||||
upstream upstream.Upstream
|
||||
hosts map[string]net.IP
|
||||
type plugFilter struct {
|
||||
ID int64
|
||||
Path string
|
||||
}
|
||||
|
||||
type plugSettings struct {
|
||||
SafeBrowsingBlockHost string
|
||||
ParentalBlockHost string
|
||||
QueryLogEnabled bool
|
||||
BlockedTTL uint32 // in seconds, default 3600
|
||||
Filters []plugFilter
|
||||
}
|
||||
|
||||
var defaultPlugin = Plugin{
|
||||
type plug struct {
|
||||
d *dnsfilter.Dnsfilter
|
||||
Next plugin.Handler
|
||||
upstream upstream.Upstream
|
||||
settings plugSettings
|
||||
|
||||
sync.RWMutex
|
||||
}
|
||||
|
||||
var defaultPluginSettings = plugSettings{
|
||||
SafeBrowsingBlockHost: "safebrowsing.block.dns.adguard.com",
|
||||
ParentalBlockHost: "family.block.dns.adguard.com",
|
||||
BlockedTTL: 3600, // in seconds
|
||||
Filters: make([]plugFilter, 0),
|
||||
}
|
||||
|
||||
func newDnsCounter(name string, help string) prometheus.Counter {
|
||||
return prometheus.NewCounter(prometheus.CounterOpts{
|
||||
Namespace: plugin.Namespace,
|
||||
Subsystem: "dnsfilter",
|
||||
Name: name,
|
||||
Help: help,
|
||||
})
|
||||
}
|
||||
|
||||
var (
|
||||
requests = newDnsCounter("requests_total", "Count of requests seen by dnsfilter.")
|
||||
filtered = newDnsCounter("filtered_total", "Count of requests filtered by dnsfilter.")
|
||||
filteredLists = newDnsCounter("filtered_lists_total", "Count of requests filtered by dnsfilter using lists.")
|
||||
filteredSafebrowsing = newDnsCounter("filtered_safebrowsing_total", "Count of requests filtered by dnsfilter using safebrowsing.")
|
||||
filteredParental = newDnsCounter("filtered_parental_total", "Count of requests filtered by dnsfilter using parental.")
|
||||
filteredInvalid = newDnsCounter("filtered_invalid_total", "Count of requests filtered by dnsfilter because they were invalid.")
|
||||
whitelisted = newDnsCounter("whitelisted_total", "Count of requests not filtered by dnsfilter because they are whitelisted.")
|
||||
safesearch = newDnsCounter("safesearch_total", "Count of requests replaced by dnsfilter safesearch.")
|
||||
errorsTotal = newDnsCounter("errors_total", "Count of requests that dnsfilter couldn't process because of transitive errors.")
|
||||
)
|
||||
|
||||
//
|
||||
// coredns handling functions
|
||||
//
|
||||
func setupPlugin(c *caddy.Controller) (*Plugin, error) {
|
||||
func setupPlugin(c *caddy.Controller) (*plug, error) {
|
||||
// create new Plugin and copy default values
|
||||
var d = new(Plugin)
|
||||
*d = defaultPlugin
|
||||
d.d = dnsfilter.New()
|
||||
d.hosts = make(map[string]net.IP)
|
||||
p := &plug{
|
||||
settings: defaultPluginSettings,
|
||||
d: dnsfilter.New(),
|
||||
}
|
||||
|
||||
log.Println("Initializing the CoreDNS plugin")
|
||||
|
||||
var filterFileName string
|
||||
for c.Next() {
|
||||
args := c.RemainingArgs()
|
||||
if len(args) == 0 {
|
||||
// must have at least one argument
|
||||
return nil, c.ArgErr()
|
||||
}
|
||||
filterFileName = args[0]
|
||||
for c.NextBlock() {
|
||||
switch c.Val() {
|
||||
blockValue := c.Val()
|
||||
switch blockValue {
|
||||
case "safebrowsing":
|
||||
d.d.EnableSafeBrowsing()
|
||||
log.Println("Browsing security service is enabled")
|
||||
p.d.EnableSafeBrowsing()
|
||||
if c.NextArg() {
|
||||
if len(c.Val()) == 0 {
|
||||
return nil, c.ArgErr()
|
||||
}
|
||||
d.d.SetSafeBrowsingServer(c.Val())
|
||||
p.d.SetSafeBrowsingServer(c.Val())
|
||||
}
|
||||
case "safesearch":
|
||||
d.d.EnableSafeSearch()
|
||||
log.Println("Safe search is enabled")
|
||||
p.d.EnableSafeSearch()
|
||||
case "parental":
|
||||
if !c.NextArg() {
|
||||
return nil, c.ArgErr()
|
||||
@@ -120,7 +106,9 @@ func setupPlugin(c *caddy.Controller) (*Plugin, error) {
|
||||
if err != nil {
|
||||
return nil, c.ArgErr()
|
||||
}
|
||||
err = d.d.EnableParental(sensitivity)
|
||||
|
||||
log.Println("Parental control is enabled")
|
||||
err = p.d.EnableParental(sensitivity)
|
||||
if err != nil {
|
||||
return nil, c.ArgErr()
|
||||
}
|
||||
@@ -128,118 +116,165 @@ func setupPlugin(c *caddy.Controller) (*Plugin, error) {
|
||||
if len(c.Val()) == 0 {
|
||||
return nil, c.ArgErr()
|
||||
}
|
||||
d.ParentalBlockHost = c.Val()
|
||||
p.settings.ParentalBlockHost = c.Val()
|
||||
}
|
||||
case "blocked_ttl":
|
||||
if !c.NextArg() {
|
||||
return nil, c.ArgErr()
|
||||
}
|
||||
blockedTtl, err := strconv.ParseUint(c.Val(), 10, 32)
|
||||
if err != nil {
|
||||
return nil, c.ArgErr()
|
||||
}
|
||||
log.Printf("Blocked request TTL is %d", blockedTtl)
|
||||
p.settings.BlockedTTL = uint32(blockedTtl)
|
||||
case "querylog":
|
||||
d.QueryLogEnabled = true
|
||||
once.Do(func() {
|
||||
go startQueryLogServer() // TODO: how to handle errors?
|
||||
log.Println("Query log is enabled")
|
||||
p.settings.QueryLogEnabled = true
|
||||
case "filter":
|
||||
if !c.NextArg() {
|
||||
return nil, c.ArgErr()
|
||||
}
|
||||
|
||||
filterId, err := strconv.ParseInt(c.Val(), 10, 64)
|
||||
if err != nil {
|
||||
return nil, c.ArgErr()
|
||||
}
|
||||
if !c.NextArg() {
|
||||
return nil, c.ArgErr()
|
||||
}
|
||||
filterPath := c.Val()
|
||||
|
||||
// Initialize filter and add it to the list
|
||||
p.settings.Filters = append(p.settings.Filters, plugFilter{
|
||||
ID: filterId,
|
||||
Path: filterPath,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
file, err := os.Open(filterFileName)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer file.Close()
|
||||
for _, filter := range p.settings.Filters {
|
||||
log.Printf("Loading rules from %s", filter.Path)
|
||||
|
||||
scanner := bufio.NewScanner(file)
|
||||
for scanner.Scan() {
|
||||
text := scanner.Text()
|
||||
if d.parseEtcHosts(text) {
|
||||
continue
|
||||
}
|
||||
err = d.d.AddRule(text, 0)
|
||||
if err == dnsfilter.ErrInvalidSyntax {
|
||||
continue
|
||||
}
|
||||
file, err := os.Open(filter.Path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
//noinspection GoDeferInLoop
|
||||
defer file.Close()
|
||||
|
||||
count := 0
|
||||
scanner := bufio.NewScanner(file)
|
||||
for scanner.Scan() {
|
||||
text := scanner.Text()
|
||||
|
||||
err = p.d.AddRule(text, filter.ID)
|
||||
if err == dnsfilter.ErrAlreadyExists || err == dnsfilter.ErrInvalidSyntax {
|
||||
continue
|
||||
}
|
||||
if err != nil {
|
||||
log.Printf("Cannot add rule %s: %s", text, err)
|
||||
// Just ignore invalid rules
|
||||
continue
|
||||
}
|
||||
count++
|
||||
}
|
||||
log.Printf("Added %d rules from filter ID=%d", count, filter.ID)
|
||||
|
||||
if err = scanner.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
if err = scanner.Err(); err != nil {
|
||||
log.Printf("Loading stats from querylog")
|
||||
err := fillStatsFromQueryLog()
|
||||
if err != nil {
|
||||
log.Printf("Failed to load stats from querylog: %s", err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
d.upstream, err = upstream.New(nil)
|
||||
if p.settings.QueryLogEnabled {
|
||||
onceQueryLog.Do(func() {
|
||||
go periodicQueryLogRotate()
|
||||
go periodicHourlyTopRotate()
|
||||
go statsRotator()
|
||||
})
|
||||
}
|
||||
|
||||
onceHook.Do(func() {
|
||||
caddy.RegisterEventHook("dnsfilter-reload", hook)
|
||||
})
|
||||
|
||||
p.upstream, err = upstream.New(nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return d, nil
|
||||
return p, nil
|
||||
}
|
||||
|
||||
func setup(c *caddy.Controller) error {
|
||||
d, err := setupPlugin(c)
|
||||
p, err := setupPlugin(c)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
config := dnsserver.GetConfig(c)
|
||||
config.AddPlugin(func(next plugin.Handler) plugin.Handler {
|
||||
d.Next = next
|
||||
return d
|
||||
p.Next = next
|
||||
return p
|
||||
})
|
||||
|
||||
c.OnStartup(func() error {
|
||||
once.Do(func() {
|
||||
m := dnsserver.GetConfig(c).Handler("prometheus")
|
||||
if m == nil {
|
||||
return
|
||||
}
|
||||
if x, ok := m.(*metrics.Metrics); ok {
|
||||
x.MustRegister(requests)
|
||||
x.MustRegister(filtered)
|
||||
x.MustRegister(filteredLists)
|
||||
x.MustRegister(filteredSafebrowsing)
|
||||
x.MustRegister(filteredParental)
|
||||
x.MustRegister(whitelisted)
|
||||
x.MustRegister(safesearch)
|
||||
x.MustRegister(errorsTotal)
|
||||
x.MustRegister(d)
|
||||
}
|
||||
})
|
||||
m := dnsserver.GetConfig(c).Handler("prometheus")
|
||||
if m == nil {
|
||||
return nil
|
||||
}
|
||||
if x, ok := m.(*metrics.Metrics); ok {
|
||||
x.MustRegister(requests)
|
||||
x.MustRegister(filtered)
|
||||
x.MustRegister(filteredLists)
|
||||
x.MustRegister(filteredSafebrowsing)
|
||||
x.MustRegister(filteredParental)
|
||||
x.MustRegister(whitelisted)
|
||||
x.MustRegister(safesearch)
|
||||
x.MustRegister(errorsTotal)
|
||||
x.MustRegister(elapsedTime)
|
||||
x.MustRegister(p)
|
||||
}
|
||||
return nil
|
||||
})
|
||||
c.OnShutdown(d.OnShutdown)
|
||||
c.OnShutdown(p.onShutdown)
|
||||
c.OnFinalShutdown(p.onFinalShutdown)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *Plugin) parseEtcHosts(text string) bool {
|
||||
if pos := strings.IndexByte(text, '#'); pos != -1 {
|
||||
text = text[0:pos]
|
||||
}
|
||||
fields := strings.Fields(text)
|
||||
if len(fields) < 2 {
|
||||
return false
|
||||
}
|
||||
addr := net.ParseIP(fields[0])
|
||||
if addr == nil {
|
||||
return false
|
||||
}
|
||||
for _, host := range fields[1:] {
|
||||
if val, ok := d.hosts[host]; ok {
|
||||
log.Printf("warning: host %s already has value %s, will overwrite it with %s", host, val, addr)
|
||||
}
|
||||
d.hosts[host] = addr
|
||||
}
|
||||
return true
|
||||
func (p *plug) onShutdown() error {
|
||||
p.Lock()
|
||||
p.d.Destroy()
|
||||
p.d = nil
|
||||
p.Unlock()
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *Plugin) OnShutdown() error {
|
||||
d.d.Destroy()
|
||||
d.d = nil
|
||||
func (p *plug) onFinalShutdown() error {
|
||||
logBufferLock.Lock()
|
||||
err := flushToFile(logBuffer)
|
||||
if err != nil {
|
||||
log.Printf("failed to flush to file: %s", err)
|
||||
return err
|
||||
}
|
||||
logBufferLock.Unlock()
|
||||
return nil
|
||||
}
|
||||
|
||||
type statsFunc func(ch interface{}, name string, text string, value float64, valueType prometheus.ValueType)
|
||||
|
||||
//noinspection GoUnusedParameter
|
||||
func doDesc(ch interface{}, name string, text string, value float64, valueType prometheus.ValueType) {
|
||||
realch, ok := ch.(chan<- *prometheus.Desc)
|
||||
if ok == false {
|
||||
if !ok {
|
||||
log.Printf("Couldn't convert ch to chan<- *prometheus.Desc\n")
|
||||
return
|
||||
}
|
||||
@@ -248,7 +283,7 @@ func doDesc(ch interface{}, name string, text string, value float64, valueType p
|
||||
|
||||
func doMetric(ch interface{}, name string, text string, value float64, valueType prometheus.ValueType) {
|
||||
realch, ok := ch.(chan<- prometheus.Metric)
|
||||
if ok == false {
|
||||
if !ok {
|
||||
log.Printf("Couldn't convert ch to chan<- prometheus.Metric\n")
|
||||
return
|
||||
}
|
||||
@@ -260,34 +295,39 @@ func gen(ch interface{}, doFunc statsFunc, name string, text string, value float
|
||||
doFunc(ch, name, text, value, valueType)
|
||||
}
|
||||
|
||||
func (d *Plugin) doStats(ch interface{}, doFunc statsFunc) {
|
||||
stats := d.d.GetStats()
|
||||
gen(ch, doFunc, "coredns_dnsfilter_safebrowsing_requests", "Number of safebrowsing HTTP requests that were sent", float64(stats.Safebrowsing.Requests), prometheus.CounterValue)
|
||||
gen(ch, doFunc, "coredns_dnsfilter_safebrowsing_cachehits", "Number of safebrowsing lookups that didn't need HTTP requests", float64(stats.Safebrowsing.CacheHits), prometheus.CounterValue)
|
||||
gen(ch, doFunc, "coredns_dnsfilter_safebrowsing_pending", "Number of currently pending safebrowsing HTTP requests", float64(stats.Safebrowsing.Pending), prometheus.GaugeValue)
|
||||
gen(ch, doFunc, "coredns_dnsfilter_safebrowsing_pending_max", "Maximum number of pending safebrowsing HTTP requests", float64(stats.Safebrowsing.PendingMax), prometheus.GaugeValue)
|
||||
gen(ch, doFunc, "coredns_dnsfilter_parental_requests", "Number of parental HTTP requests that were sent", float64(stats.Parental.Requests), prometheus.CounterValue)
|
||||
gen(ch, doFunc, "coredns_dnsfilter_parental_cachehits", "Number of parental lookups that didn't need HTTP requests", float64(stats.Parental.CacheHits), prometheus.CounterValue)
|
||||
gen(ch, doFunc, "coredns_dnsfilter_parental_pending", "Number of currently pending parental HTTP requests", float64(stats.Parental.Pending), prometheus.GaugeValue)
|
||||
gen(ch, doFunc, "coredns_dnsfilter_parental_pending_max", "Maximum number of pending parental HTTP requests", float64(stats.Parental.PendingMax), prometheus.GaugeValue)
|
||||
func doStatsLookup(ch interface{}, doFunc statsFunc, name string, lookupstats *dnsfilter.LookupStats) {
|
||||
gen(ch, doFunc, fmt.Sprintf("coredns_dnsfilter_%s_requests", name), fmt.Sprintf("Number of %s HTTP requests that were sent", name), float64(lookupstats.Requests), prometheus.CounterValue)
|
||||
gen(ch, doFunc, fmt.Sprintf("coredns_dnsfilter_%s_cachehits", name), fmt.Sprintf("Number of %s lookups that didn't need HTTP requests", name), float64(lookupstats.CacheHits), prometheus.CounterValue)
|
||||
gen(ch, doFunc, fmt.Sprintf("coredns_dnsfilter_%s_pending", name), fmt.Sprintf("Number of currently pending %s HTTP requests", name), float64(lookupstats.Pending), prometheus.GaugeValue)
|
||||
gen(ch, doFunc, fmt.Sprintf("coredns_dnsfilter_%s_pending_max", name), fmt.Sprintf("Maximum number of pending %s HTTP requests", name), float64(lookupstats.PendingMax), prometheus.GaugeValue)
|
||||
}
|
||||
|
||||
func (d *Plugin) Describe(ch chan<- *prometheus.Desc) {
|
||||
d.doStats(ch, doDesc)
|
||||
func (p *plug) doStats(ch interface{}, doFunc statsFunc) {
|
||||
p.RLock()
|
||||
stats := p.d.GetStats()
|
||||
doStatsLookup(ch, doFunc, "safebrowsing", &stats.Safebrowsing)
|
||||
doStatsLookup(ch, doFunc, "parental", &stats.Parental)
|
||||
p.RUnlock()
|
||||
}
|
||||
|
||||
func (d *Plugin) Collect(ch chan<- prometheus.Metric) {
|
||||
d.doStats(ch, doMetric)
|
||||
// Describe is called by prometheus handler to know stat types
|
||||
func (p *plug) Describe(ch chan<- *prometheus.Desc) {
|
||||
p.doStats(ch, doDesc)
|
||||
}
|
||||
|
||||
func (d *Plugin) replaceHostWithValAndReply(ctx context.Context, w dns.ResponseWriter, r *dns.Msg, host string, val string, question dns.Question) (int, error) {
|
||||
// Collect is called by prometheus handler to collect stats
|
||||
func (p *plug) Collect(ch chan<- prometheus.Metric) {
|
||||
p.doStats(ch, doMetric)
|
||||
}
|
||||
|
||||
func (p *plug) replaceHostWithValAndReply(ctx context.Context, w dns.ResponseWriter, r *dns.Msg, host string, val string, question dns.Question) (int, error) {
|
||||
// check if it's a domain name or IP address
|
||||
addr := net.ParseIP(val)
|
||||
var records []dns.RR
|
||||
log.Println("Will give", val, "instead of", host)
|
||||
// log.Println("Will give", val, "instead of", host) // debug logging
|
||||
if addr != nil {
|
||||
// this is an IP address, return it
|
||||
result, err := dns.NewRR(host + " A " + val)
|
||||
result, err := dns.NewRR(fmt.Sprintf("%s %d A %s", host, p.settings.BlockedTTL, val))
|
||||
if err != nil {
|
||||
log.Printf("Got error %s\n", err)
|
||||
return dns.RcodeServerFailure, fmt.Errorf("plugin/dnsfilter: %s", err)
|
||||
@@ -299,7 +339,7 @@ func (d *Plugin) replaceHostWithValAndReply(ctx context.Context, w dns.ResponseW
|
||||
req.SetQuestion(dns.Fqdn(val), question.Qtype)
|
||||
req.RecursionDesired = true
|
||||
reqstate := request.Request{W: w, Req: req, Context: ctx}
|
||||
result, err := d.upstream.Lookup(reqstate, dns.Fqdn(val), reqstate.QType())
|
||||
result, err := p.upstream.Lookup(reqstate, dns.Fqdn(val), reqstate.QType())
|
||||
if err != nil {
|
||||
log.Printf("Got error %s\n", err)
|
||||
return dns.RcodeServerFailure, fmt.Errorf("plugin/dnsfilter: %s", err)
|
||||
@@ -327,9 +367,9 @@ func (d *Plugin) replaceHostWithValAndReply(ctx context.Context, w dns.ResponseW
|
||||
|
||||
// generate SOA record that makes DNS clients cache NXdomain results
|
||||
// the only value that is important is TTL in header, other values like refresh, retry, expire and minttl are irrelevant
|
||||
func genSOA(r *dns.Msg) []dns.RR {
|
||||
func (p *plug) genSOA(r *dns.Msg) []dns.RR {
|
||||
zone := r.Question[0].Name
|
||||
header := dns.RR_Header{Name: zone, Rrtype: dns.TypeSOA, Ttl: 3600, Class: dns.ClassINET}
|
||||
header := dns.RR_Header{Name: zone, Rrtype: dns.TypeSOA, Ttl: p.settings.BlockedTTL, Class: dns.ClassINET}
|
||||
|
||||
Mbox := "hostmaster."
|
||||
if zone[0] != '.' {
|
||||
@@ -337,20 +377,20 @@ func genSOA(r *dns.Msg) []dns.RR {
|
||||
}
|
||||
Ns := "fake-for-negative-caching.adguard.com."
|
||||
|
||||
soa := defaultSOA
|
||||
soa := *defaultSOA
|
||||
soa.Hdr = header
|
||||
soa.Mbox = Mbox
|
||||
soa.Ns = Ns
|
||||
soa.Serial = uint32(time.Now().Unix())
|
||||
return []dns.RR{soa}
|
||||
soa.Serial = 100500 // faster than uint32(time.Now().Unix())
|
||||
return []dns.RR{&soa}
|
||||
}
|
||||
|
||||
func writeNXdomain(ctx context.Context, w dns.ResponseWriter, r *dns.Msg) (int, error) {
|
||||
func (p *plug) writeNXdomain(ctx context.Context, w dns.ResponseWriter, r *dns.Msg) (int, error) {
|
||||
state := request.Request{W: w, Req: r, Context: ctx}
|
||||
m := new(dns.Msg)
|
||||
m.SetRcode(state.Req, dns.RcodeNameError)
|
||||
m.Authoritative, m.RecursionAvailable, m.Compress = true, true, true
|
||||
m.Ns = genSOA(r)
|
||||
m.Ns = p.genSOA(r)
|
||||
|
||||
state.SizeAndDo(m)
|
||||
err := state.W.WriteMsg(m)
|
||||
@@ -361,108 +401,116 @@ func writeNXdomain(ctx context.Context, w dns.ResponseWriter, r *dns.Msg) (int,
|
||||
return dns.RcodeNameError, nil
|
||||
}
|
||||
|
||||
func (d *Plugin) serveDNSInternal(ctx context.Context, w dns.ResponseWriter, r *dns.Msg) (int, error, dnsfilter.Result) {
|
||||
func (p *plug) serveDNSInternal(ctx context.Context, w dns.ResponseWriter, r *dns.Msg) (int, dnsfilter.Result, error) {
|
||||
if len(r.Question) != 1 {
|
||||
// google DNS, bind and others do the same
|
||||
return dns.RcodeFormatError, fmt.Errorf("Got DNS request with != 1 questions"), dnsfilter.Result{}
|
||||
return dns.RcodeFormatError, dnsfilter.Result{}, fmt.Errorf("got a DNS request with more than one Question")
|
||||
}
|
||||
for _, question := range r.Question {
|
||||
host := strings.ToLower(strings.TrimSuffix(question.Name, "."))
|
||||
// if input is empty host, filter it out right away
|
||||
if index := strings.IndexByte(host, byte('.')); index == -1 {
|
||||
rcode, err := writeNXdomain(ctx, w, r)
|
||||
if err != nil {
|
||||
return rcode, err, dnsfilter.Result{}
|
||||
}
|
||||
return rcode, err, dnsfilter.Result{Reason: dnsfilter.FilteredInvalid}
|
||||
}
|
||||
// is it a safesearch domain?
|
||||
if val, ok := d.d.SafeSearchDomain(host); ok {
|
||||
rcode, err := d.replaceHostWithValAndReply(ctx, w, r, host, val, question)
|
||||
p.RLock()
|
||||
if val, ok := p.d.SafeSearchDomain(host); ok {
|
||||
rcode, err := p.replaceHostWithValAndReply(ctx, w, r, host, val, question)
|
||||
if err != nil {
|
||||
return rcode, err, dnsfilter.Result{}
|
||||
p.RUnlock()
|
||||
return rcode, dnsfilter.Result{}, err
|
||||
}
|
||||
return rcode, err, dnsfilter.Result{Reason: dnsfilter.FilteredSafeSearch}
|
||||
}
|
||||
|
||||
// is it in hosts?
|
||||
if val, ok := d.hosts[host]; ok {
|
||||
// it is, if it's a loopback host, reply with NXDOMAIN
|
||||
if val.IsLoopback() {
|
||||
rcode, err := writeNXdomain(ctx, w, r)
|
||||
if err != nil {
|
||||
return rcode, err, dnsfilter.Result{}
|
||||
}
|
||||
return rcode, err, dnsfilter.Result{Reason: dnsfilter.FilteredInvalid}
|
||||
}
|
||||
// it's not a loopback host, replace it with value specified
|
||||
rcode, err := d.replaceHostWithValAndReply(ctx, w, r, host, val.String(), question)
|
||||
if err != nil {
|
||||
return rcode, err, dnsfilter.Result{}
|
||||
}
|
||||
return rcode, err, dnsfilter.Result{Reason: dnsfilter.FilteredSafeSearch}
|
||||
p.RUnlock()
|
||||
return rcode, dnsfilter.Result{Reason: dnsfilter.FilteredSafeSearch}, err
|
||||
}
|
||||
p.RUnlock()
|
||||
|
||||
// needs to be filtered instead
|
||||
result, err := d.d.CheckHost(host)
|
||||
p.RLock()
|
||||
result, err := p.d.CheckHost(host)
|
||||
if err != nil {
|
||||
log.Printf("plugin/dnsfilter: %s\n", err)
|
||||
return dns.RcodeServerFailure, fmt.Errorf("plugin/dnsfilter: %s", err), dnsfilter.Result{}
|
||||
p.RUnlock()
|
||||
return dns.RcodeServerFailure, dnsfilter.Result{}, fmt.Errorf("plugin/dnsfilter: %s", err)
|
||||
}
|
||||
p.RUnlock()
|
||||
|
||||
// safebrowsing
|
||||
if result.IsFiltered == true && result.Reason == dnsfilter.FilteredSafeBrowsing {
|
||||
// return cname safebrowsing.block.dns.adguard.com
|
||||
val := d.SafeBrowsingBlockHost
|
||||
rcode, err := d.replaceHostWithValAndReply(ctx, w, r, host, val, question)
|
||||
if err != nil {
|
||||
return rcode, err, dnsfilter.Result{}
|
||||
}
|
||||
return rcode, err, result
|
||||
}
|
||||
if result.IsFiltered {
|
||||
switch result.Reason {
|
||||
case dnsfilter.FilteredSafeBrowsing:
|
||||
// return cname safebrowsing.block.dns.adguard.com
|
||||
val := p.settings.SafeBrowsingBlockHost
|
||||
rcode, err := p.replaceHostWithValAndReply(ctx, w, r, host, val, question)
|
||||
if err != nil {
|
||||
return rcode, dnsfilter.Result{}, err
|
||||
}
|
||||
return rcode, result, err
|
||||
case dnsfilter.FilteredParental:
|
||||
// return cname family.block.dns.adguard.com
|
||||
val := p.settings.ParentalBlockHost
|
||||
rcode, err := p.replaceHostWithValAndReply(ctx, w, r, host, val, question)
|
||||
if err != nil {
|
||||
return rcode, dnsfilter.Result{}, err
|
||||
}
|
||||
return rcode, result, err
|
||||
case dnsfilter.FilteredBlackList:
|
||||
|
||||
// parental
|
||||
if result.IsFiltered == true && result.Reason == dnsfilter.FilteredParental {
|
||||
// return cname
|
||||
val := d.ParentalBlockHost
|
||||
rcode, err := d.replaceHostWithValAndReply(ctx, w, r, host, val, question)
|
||||
if err != nil {
|
||||
return rcode, err, dnsfilter.Result{}
|
||||
if result.Ip == nil {
|
||||
// return NXDomain
|
||||
rcode, err := p.writeNXdomain(ctx, w, r)
|
||||
if err != nil {
|
||||
return rcode, dnsfilter.Result{}, err
|
||||
}
|
||||
return rcode, result, err
|
||||
} else {
|
||||
// This is a hosts-syntax rule
|
||||
rcode, err := p.replaceHostWithValAndReply(ctx, w, r, host, result.Ip.String(), question)
|
||||
if err != nil {
|
||||
return rcode, dnsfilter.Result{}, err
|
||||
}
|
||||
return rcode, result, err
|
||||
}
|
||||
case dnsfilter.FilteredInvalid:
|
||||
// return NXdomain
|
||||
rcode, err := p.writeNXdomain(ctx, w, r)
|
||||
if err != nil {
|
||||
return rcode, dnsfilter.Result{}, err
|
||||
}
|
||||
return rcode, result, err
|
||||
default:
|
||||
log.Printf("SHOULD NOT HAPPEN -- got unknown reason for filtering host \"%s\": %v, %+v", host, result.Reason, result)
|
||||
}
|
||||
return rcode, err, result
|
||||
}
|
||||
|
||||
// blacklist
|
||||
if result.IsFiltered == true && result.Reason == dnsfilter.FilteredBlackList {
|
||||
rcode, err := writeNXdomain(ctx, w, r)
|
||||
if err != nil {
|
||||
return rcode, err, dnsfilter.Result{}
|
||||
} else {
|
||||
switch result.Reason {
|
||||
case dnsfilter.NotFilteredWhiteList:
|
||||
rcode, err := plugin.NextOrFailure(p.Name(), p.Next, ctx, w, r)
|
||||
return rcode, result, err
|
||||
case dnsfilter.NotFilteredNotFound:
|
||||
// do nothing, pass through to lower code
|
||||
default:
|
||||
log.Printf("SHOULD NOT HAPPEN -- got unknown reason for not filtering host \"%s\": %v, %+v", host, result.Reason, result)
|
||||
}
|
||||
return rcode, err, result
|
||||
}
|
||||
if result.IsFiltered == false && result.Reason == dnsfilter.NotFilteredWhiteList {
|
||||
rcode, err := plugin.NextOrFailure(d.Name(), d.Next, ctx, w, r)
|
||||
return rcode, err, result
|
||||
}
|
||||
}
|
||||
rcode, err := plugin.NextOrFailure(d.Name(), d.Next, ctx, w, r)
|
||||
return rcode, err, dnsfilter.Result{}
|
||||
rcode, err := plugin.NextOrFailure(p.Name(), p.Next, ctx, w, r)
|
||||
return rcode, dnsfilter.Result{}, err
|
||||
}
|
||||
|
||||
func (d *Plugin) ServeDNS(ctx context.Context, w dns.ResponseWriter, r *dns.Msg) (int, error) {
|
||||
// ServeDNS handles the DNS request and refuses if it's in filterlists
|
||||
func (p *plug) ServeDNS(ctx context.Context, w dns.ResponseWriter, r *dns.Msg) (int, error) {
|
||||
start := time.Now()
|
||||
requests.Inc()
|
||||
state := request.Request{W: w, Req: r}
|
||||
ip := state.IP()
|
||||
|
||||
// capture the written answer
|
||||
rrw := dnstest.NewRecorder(w)
|
||||
rcode, err, result := d.serveDNSInternal(ctx, rrw, r)
|
||||
rcode, result, err := p.serveDNSInternal(ctx, rrw, r)
|
||||
if rcode > 0 {
|
||||
// actually send the answer if we have one
|
||||
state := request.Request{W: w, Req: r}
|
||||
answer := new(dns.Msg)
|
||||
answer.SetRcode(r, rcode)
|
||||
state.SizeAndDo(answer)
|
||||
w.WriteMsg(answer)
|
||||
err = w.WriteMsg(answer)
|
||||
if err != nil {
|
||||
return dns.RcodeServerFailure, err
|
||||
}
|
||||
}
|
||||
|
||||
// increment counters
|
||||
@@ -496,12 +544,16 @@ func (d *Plugin) ServeDNS(ctx context.Context, w dns.ResponseWriter, r *dns.Msg)
|
||||
}
|
||||
|
||||
// log
|
||||
if d.QueryLogEnabled {
|
||||
logRequest(rrw.Msg, result, time.Since(start))
|
||||
elapsed := time.Since(start)
|
||||
elapsedTime.Observe(elapsed.Seconds())
|
||||
if p.settings.QueryLogEnabled {
|
||||
logRequest(r, rrw.Msg, result, time.Since(start), ip)
|
||||
}
|
||||
return rcode, err
|
||||
}
|
||||
|
||||
func (d *Plugin) Name() string { return "dnsfilter" }
|
||||
// Name returns name of the plugin as seen in Corefile and plugin.cfg
|
||||
func (p *plug) Name() string { return "dnsfilter" }
|
||||
|
||||
var once sync.Once
|
||||
var onceHook sync.Once
|
||||
var onceQueryLog sync.Once
|
||||
|
||||
@@ -20,10 +20,21 @@ func TestSetup(t *testing.T) {
|
||||
config string
|
||||
failing bool
|
||||
}{
|
||||
{`dnsfilter`, true},
|
||||
{`dnsfilter ../tests/dns.txt`, false},
|
||||
{`dnsfilter ../tests/dns.txt { safebrowsing }`, false},
|
||||
{`dnsfilter ../tests/dns.txt { parental }`, true},
|
||||
{`dnsfilter`, false},
|
||||
{`dnsfilter {
|
||||
filter 0 /dev/nonexistent/abcdef
|
||||
}`, true},
|
||||
{`dnsfilter {
|
||||
filter 0 ../tests/dns.txt
|
||||
}`, false},
|
||||
{`dnsfilter {
|
||||
safebrowsing
|
||||
filter 0 ../tests/dns.txt
|
||||
}`, false},
|
||||
{`dnsfilter {
|
||||
parental
|
||||
filter 0 ../tests/dns.txt
|
||||
}`, true},
|
||||
} {
|
||||
c := caddy.NewTestController("dns", testcase.config)
|
||||
err := setup(c)
|
||||
@@ -39,57 +50,23 @@ func TestSetup(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestEtcHostsParse(t *testing.T) {
|
||||
addr := "216.239.38.120"
|
||||
text := []byte(fmt.Sprintf(" %s google.com www.google.com # enforce google's safesearch ", addr))
|
||||
tmpfile, err := ioutil.TempFile("", "")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, err := tmpfile.Write(text); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := tmpfile.Close(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
defer os.Remove(tmpfile.Name())
|
||||
|
||||
c := caddy.NewTestController("dns", fmt.Sprintf("dnsfilter %s", tmpfile.Name()))
|
||||
p, err := setupPlugin(c)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if len(p.hosts) != 2 {
|
||||
t.Fatal("Expected p.hosts to have two keys")
|
||||
}
|
||||
|
||||
val, ok := p.hosts["google.com"]
|
||||
if !ok {
|
||||
t.Fatal("Expected google.com to be set in p.hosts")
|
||||
}
|
||||
if !val.Equal(net.ParseIP(addr)) {
|
||||
t.Fatalf("Expected google.com's value %s to match %s", val, addr)
|
||||
}
|
||||
}
|
||||
|
||||
func TestEtcHostsFilter(t *testing.T) {
|
||||
text := []byte("127.0.0.1 doubleclick.net\n" + "127.0.0.1 example.org example.net www.example.org www.example.net")
|
||||
tmpfile, err := ioutil.TempFile("", "")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, err := tmpfile.Write(text); err != nil {
|
||||
if _, err = tmpfile.Write(text); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := tmpfile.Close(); err != nil {
|
||||
if err = tmpfile.Close(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
defer os.Remove(tmpfile.Name())
|
||||
|
||||
c := caddy.NewTestController("dns", fmt.Sprintf("dnsfilter %s", tmpfile.Name()))
|
||||
configText := fmt.Sprintf("dnsfilter {\nfilter 0 %s\n}", tmpfile.Name())
|
||||
c := caddy.NewTestController("dns", configText)
|
||||
p, err := setupPlugin(c)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
@@ -126,11 +103,16 @@ func TestEtcHostsFilter(t *testing.T) {
|
||||
if rcode != rrw.Rcode {
|
||||
t.Fatalf("ServeDNS return value for host %s has rcode %d that does not match captured rcode %d", testcase.host, rcode, rrw.Rcode)
|
||||
}
|
||||
filtered := rcode == dns.RcodeNameError
|
||||
if testcase.filtered == true && testcase.filtered != filtered {
|
||||
A, ok := rrw.Msg.Answer[0].(*dns.A)
|
||||
if !ok {
|
||||
t.Fatalf("Host %s expected to have result A", testcase.host)
|
||||
}
|
||||
ip := net.IPv4(127, 0, 0, 1)
|
||||
filtered := ip.Equal(A.A)
|
||||
if testcase.filtered && testcase.filtered != filtered {
|
||||
t.Fatalf("Host %s expected to be filtered, instead it is not filtered", testcase.host)
|
||||
}
|
||||
if testcase.filtered == false && testcase.filtered != filtered {
|
||||
if !testcase.filtered && testcase.filtered != filtered {
|
||||
t.Fatalf("Host %s expected to be not filtered, instead it is filtered", testcase.host)
|
||||
}
|
||||
}
|
||||
|
||||
410
coredns_plugin/coredns_stats.go
Normal file
410
coredns_plugin/coredns_stats.go
Normal file
@@ -0,0 +1,410 @@
|
||||
package dnsfilter
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"log"
|
||||
"net/http"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/coredns/coredns/plugin"
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
)
|
||||
|
||||
var (
|
||||
requests = newDNSCounter("requests_total", "Count of requests seen by dnsfilter.")
|
||||
filtered = newDNSCounter("filtered_total", "Count of requests filtered by dnsfilter.")
|
||||
filteredLists = newDNSCounter("filtered_lists_total", "Count of requests filtered by dnsfilter using lists.")
|
||||
filteredSafebrowsing = newDNSCounter("filtered_safebrowsing_total", "Count of requests filtered by dnsfilter using safebrowsing.")
|
||||
filteredParental = newDNSCounter("filtered_parental_total", "Count of requests filtered by dnsfilter using parental.")
|
||||
filteredInvalid = newDNSCounter("filtered_invalid_total", "Count of requests filtered by dnsfilter because they were invalid.")
|
||||
whitelisted = newDNSCounter("whitelisted_total", "Count of requests not filtered by dnsfilter because they are whitelisted.")
|
||||
safesearch = newDNSCounter("safesearch_total", "Count of requests replaced by dnsfilter safesearch.")
|
||||
errorsTotal = newDNSCounter("errors_total", "Count of requests that dnsfilter couldn't process because of transitive errors.")
|
||||
elapsedTime = newDNSHistogram("request_duration", "Histogram of the time (in seconds) each request took.")
|
||||
)
|
||||
|
||||
// entries for single time period (for example all per-second entries)
|
||||
type statsEntries map[string][statsHistoryElements]float64
|
||||
|
||||
// how far back to keep the stats
|
||||
const statsHistoryElements = 60 + 1 // +1 for calculating delta
|
||||
|
||||
// each periodic stat is a map of arrays
|
||||
type periodicStats struct {
|
||||
Entries statsEntries
|
||||
period time.Duration // how long one entry lasts
|
||||
LastRotate time.Time // last time this data was rotated
|
||||
|
||||
sync.RWMutex
|
||||
}
|
||||
|
||||
type stats struct {
|
||||
PerSecond periodicStats
|
||||
PerMinute periodicStats
|
||||
PerHour periodicStats
|
||||
PerDay periodicStats
|
||||
}
|
||||
|
||||
// per-second/per-minute/per-hour/per-day stats
|
||||
var statistics stats
|
||||
|
||||
func initPeriodicStats(periodic *periodicStats, period time.Duration) {
|
||||
periodic.Entries = statsEntries{}
|
||||
periodic.LastRotate = time.Now()
|
||||
periodic.period = period
|
||||
}
|
||||
|
||||
func init() {
|
||||
purgeStats()
|
||||
}
|
||||
|
||||
func purgeStats() {
|
||||
initPeriodicStats(&statistics.PerSecond, time.Second)
|
||||
initPeriodicStats(&statistics.PerMinute, time.Minute)
|
||||
initPeriodicStats(&statistics.PerHour, time.Hour)
|
||||
initPeriodicStats(&statistics.PerDay, time.Hour*24)
|
||||
}
|
||||
|
||||
func (p *periodicStats) Inc(name string, when time.Time) {
|
||||
// calculate how many periods ago this happened
|
||||
elapsed := int64(time.Since(when) / p.period)
|
||||
// trace("%s: %v as %v -> [%v]", name, time.Since(when), p.period, elapsed)
|
||||
if elapsed >= statsHistoryElements {
|
||||
return // outside of our timeframe
|
||||
}
|
||||
p.Lock()
|
||||
currentValues := p.Entries[name]
|
||||
currentValues[elapsed]++
|
||||
p.Entries[name] = currentValues
|
||||
p.Unlock()
|
||||
}
|
||||
|
||||
func (p *periodicStats) Observe(name string, when time.Time, value float64) {
|
||||
// calculate how many periods ago this happened
|
||||
elapsed := int64(time.Since(when) / p.period)
|
||||
// trace("%s: %v as %v -> [%v]", name, time.Since(when), p.period, elapsed)
|
||||
if elapsed >= statsHistoryElements {
|
||||
return // outside of our timeframe
|
||||
}
|
||||
p.Lock()
|
||||
{
|
||||
countname := name + "_count"
|
||||
currentValues := p.Entries[countname]
|
||||
value := currentValues[elapsed]
|
||||
// trace("Will change p.Entries[%s][%d] from %v to %v", countname, elapsed, value, value+1)
|
||||
value += 1
|
||||
currentValues[elapsed] = value
|
||||
p.Entries[countname] = currentValues
|
||||
}
|
||||
{
|
||||
totalname := name + "_sum"
|
||||
currentValues := p.Entries[totalname]
|
||||
currentValues[elapsed] += value
|
||||
p.Entries[totalname] = currentValues
|
||||
}
|
||||
p.Unlock()
|
||||
}
|
||||
|
||||
func (p *periodicStats) statsRotate(now time.Time) {
|
||||
p.Lock()
|
||||
rotations := int64(now.Sub(p.LastRotate) / p.period)
|
||||
if rotations > statsHistoryElements {
|
||||
rotations = statsHistoryElements
|
||||
}
|
||||
// calculate how many times we should rotate
|
||||
for r := int64(0); r < rotations; r++ {
|
||||
for key, values := range p.Entries {
|
||||
newValues := [statsHistoryElements]float64{}
|
||||
for i := 1; i < len(values); i++ {
|
||||
newValues[i] = values[i-1]
|
||||
}
|
||||
p.Entries[key] = newValues
|
||||
}
|
||||
}
|
||||
if rotations > 0 {
|
||||
p.LastRotate = now
|
||||
}
|
||||
p.Unlock()
|
||||
}
|
||||
|
||||
func statsRotator() {
|
||||
for range time.Tick(time.Second) {
|
||||
now := time.Now()
|
||||
statistics.PerSecond.statsRotate(now)
|
||||
statistics.PerMinute.statsRotate(now)
|
||||
statistics.PerHour.statsRotate(now)
|
||||
statistics.PerDay.statsRotate(now)
|
||||
}
|
||||
}
|
||||
|
||||
// counter that wraps around prometheus Counter but also adds to periodic stats
|
||||
type counter struct {
|
||||
name string // used as key in periodic stats
|
||||
value int64
|
||||
prom prometheus.Counter
|
||||
}
|
||||
|
||||
func newDNSCounter(name string, help string) *counter {
|
||||
// trace("called")
|
||||
c := &counter{}
|
||||
c.prom = prometheus.NewCounter(prometheus.CounterOpts{
|
||||
Namespace: plugin.Namespace,
|
||||
Subsystem: "dnsfilter",
|
||||
Name: name,
|
||||
Help: help,
|
||||
})
|
||||
c.name = name
|
||||
|
||||
return c
|
||||
}
|
||||
|
||||
func (c *counter) IncWithTime(when time.Time) {
|
||||
statistics.PerSecond.Inc(c.name, when)
|
||||
statistics.PerMinute.Inc(c.name, when)
|
||||
statistics.PerHour.Inc(c.name, when)
|
||||
statistics.PerDay.Inc(c.name, when)
|
||||
c.value++
|
||||
c.prom.Inc()
|
||||
}
|
||||
|
||||
func (c *counter) Inc() {
|
||||
c.IncWithTime(time.Now())
|
||||
}
|
||||
|
||||
func (c *counter) Describe(ch chan<- *prometheus.Desc) {
|
||||
c.prom.Describe(ch)
|
||||
}
|
||||
|
||||
func (c *counter) Collect(ch chan<- prometheus.Metric) {
|
||||
c.prom.Collect(ch)
|
||||
}
|
||||
|
||||
type histogram struct {
|
||||
name string // used as key in periodic stats
|
||||
count int64
|
||||
total float64
|
||||
prom prometheus.Histogram
|
||||
}
|
||||
|
||||
func newDNSHistogram(name string, help string) *histogram {
|
||||
// trace("called")
|
||||
h := &histogram{}
|
||||
h.prom = prometheus.NewHistogram(prometheus.HistogramOpts{
|
||||
Namespace: plugin.Namespace,
|
||||
Subsystem: "dnsfilter",
|
||||
Name: name,
|
||||
Help: help,
|
||||
})
|
||||
h.name = name
|
||||
|
||||
return h
|
||||
}
|
||||
|
||||
func (h *histogram) ObserveWithTime(value float64, when time.Time) {
|
||||
statistics.PerSecond.Observe(h.name, when, value)
|
||||
statistics.PerMinute.Observe(h.name, when, value)
|
||||
statistics.PerHour.Observe(h.name, when, value)
|
||||
statistics.PerDay.Observe(h.name, when, value)
|
||||
h.count++
|
||||
h.total += value
|
||||
h.prom.Observe(value)
|
||||
}
|
||||
|
||||
func (h *histogram) Observe(value float64) {
|
||||
h.ObserveWithTime(value, time.Now())
|
||||
}
|
||||
|
||||
func (h *histogram) Describe(ch chan<- *prometheus.Desc) {
|
||||
h.prom.Describe(ch)
|
||||
}
|
||||
|
||||
func (h *histogram) Collect(ch chan<- prometheus.Metric) {
|
||||
h.prom.Collect(ch)
|
||||
}
|
||||
|
||||
// -----
|
||||
// stats
|
||||
// -----
|
||||
func HandleStats(w http.ResponseWriter, r *http.Request) {
|
||||
const numHours = 24
|
||||
histrical := generateMapFromStats(&statistics.PerHour, 0, numHours)
|
||||
// sum them up
|
||||
summed := map[string]interface{}{}
|
||||
for key, values := range histrical {
|
||||
summedValue := 0.0
|
||||
floats, ok := values.([]float64)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
for _, v := range floats {
|
||||
summedValue += v
|
||||
}
|
||||
summed[key] = summedValue
|
||||
}
|
||||
// don't forget to divide by number of elements in returned slice
|
||||
if val, ok := summed["avg_processing_time"]; ok {
|
||||
if flval, flok := val.(float64); flok {
|
||||
flval /= numHours
|
||||
summed["avg_processing_time"] = flval
|
||||
}
|
||||
}
|
||||
|
||||
summed["stats_period"] = "24 hours"
|
||||
|
||||
json, err := json.Marshal(summed)
|
||||
if err != nil {
|
||||
errortext := fmt.Sprintf("Unable to marshal status json: %s", err)
|
||||
log.Println(errortext)
|
||||
http.Error(w, errortext, 500)
|
||||
return
|
||||
}
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
_, err = w.Write(json)
|
||||
if err != nil {
|
||||
errortext := fmt.Sprintf("Unable to write response json: %s", err)
|
||||
log.Println(errortext)
|
||||
http.Error(w, errortext, 500)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func generateMapFromStats(stats *periodicStats, start int, end int) map[string]interface{} {
|
||||
// clamp
|
||||
start = clamp(start, 0, statsHistoryElements)
|
||||
end = clamp(end, 0, statsHistoryElements)
|
||||
|
||||
avgProcessingTime := make([]float64, 0)
|
||||
|
||||
count := getReversedSlice(stats.Entries[elapsedTime.name+"_count"], start, end)
|
||||
sum := getReversedSlice(stats.Entries[elapsedTime.name+"_sum"], start, end)
|
||||
for i := 0; i < len(count); i++ {
|
||||
var avg float64
|
||||
if count[i] != 0 {
|
||||
avg = sum[i] / count[i]
|
||||
avg *= 1000
|
||||
}
|
||||
avgProcessingTime = append(avgProcessingTime, avg)
|
||||
}
|
||||
|
||||
result := map[string]interface{}{
|
||||
"dns_queries": getReversedSlice(stats.Entries[requests.name], start, end),
|
||||
"blocked_filtering": getReversedSlice(stats.Entries[filtered.name], start, end),
|
||||
"replaced_safebrowsing": getReversedSlice(stats.Entries[filteredSafebrowsing.name], start, end),
|
||||
"replaced_safesearch": getReversedSlice(stats.Entries[safesearch.name], start, end),
|
||||
"replaced_parental": getReversedSlice(stats.Entries[filteredParental.name], start, end),
|
||||
"avg_processing_time": avgProcessingTime,
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func HandleStatsHistory(w http.ResponseWriter, r *http.Request) {
|
||||
// handle time unit and prepare our time window size
|
||||
now := time.Now()
|
||||
timeUnitString := r.URL.Query().Get("time_unit")
|
||||
var stats *periodicStats
|
||||
var timeUnit time.Duration
|
||||
switch timeUnitString {
|
||||
case "seconds":
|
||||
timeUnit = time.Second
|
||||
stats = &statistics.PerSecond
|
||||
case "minutes":
|
||||
timeUnit = time.Minute
|
||||
stats = &statistics.PerMinute
|
||||
case "hours":
|
||||
timeUnit = time.Hour
|
||||
stats = &statistics.PerHour
|
||||
case "days":
|
||||
timeUnit = time.Hour * 24
|
||||
stats = &statistics.PerDay
|
||||
default:
|
||||
http.Error(w, "Must specify valid time_unit parameter", 400)
|
||||
return
|
||||
}
|
||||
|
||||
// parse start and end time
|
||||
startTime, err := time.Parse(time.RFC3339, r.URL.Query().Get("start_time"))
|
||||
if err != nil {
|
||||
errortext := fmt.Sprintf("Must specify valid start_time parameter: %s", err)
|
||||
log.Println(errortext)
|
||||
http.Error(w, errortext, 400)
|
||||
return
|
||||
}
|
||||
endTime, err := time.Parse(time.RFC3339, r.URL.Query().Get("end_time"))
|
||||
if err != nil {
|
||||
errortext := fmt.Sprintf("Must specify valid end_time parameter: %s", err)
|
||||
log.Println(errortext)
|
||||
http.Error(w, errortext, 400)
|
||||
return
|
||||
}
|
||||
|
||||
// check if start and time times are within supported time range
|
||||
timeRange := timeUnit * statsHistoryElements
|
||||
if startTime.Add(timeRange).Before(now) {
|
||||
http.Error(w, "start_time parameter is outside of supported range", 501)
|
||||
return
|
||||
}
|
||||
if endTime.Add(timeRange).Before(now) {
|
||||
http.Error(w, "end_time parameter is outside of supported range", 501)
|
||||
return
|
||||
}
|
||||
|
||||
// calculate start and end of our array
|
||||
// basically it's how many hours/minutes/etc have passed since now
|
||||
start := int(now.Sub(endTime) / timeUnit)
|
||||
end := int(now.Sub(startTime) / timeUnit)
|
||||
|
||||
// swap them around if they're inverted
|
||||
if start > end {
|
||||
start, end = end, start
|
||||
}
|
||||
|
||||
data := generateMapFromStats(stats, start, end)
|
||||
json, err := json.Marshal(data)
|
||||
if err != nil {
|
||||
errortext := fmt.Sprintf("Unable to marshal status json: %s", err)
|
||||
log.Println(errortext)
|
||||
http.Error(w, errortext, 500)
|
||||
return
|
||||
}
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
_, err = w.Write(json)
|
||||
if err != nil {
|
||||
errortext := fmt.Sprintf("Unable to write response json: %s", err)
|
||||
log.Println(errortext)
|
||||
http.Error(w, errortext, 500)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func HandleStatsReset(w http.ResponseWriter, r *http.Request) {
|
||||
purgeStats()
|
||||
_, err := fmt.Fprintf(w, "OK\n")
|
||||
if err != nil {
|
||||
errortext := fmt.Sprintf("Couldn't write body: %s", err)
|
||||
log.Println(errortext)
|
||||
http.Error(w, errortext, http.StatusInternalServerError)
|
||||
}
|
||||
}
|
||||
|
||||
func clamp(value, low, high int) int {
|
||||
if value < low {
|
||||
return low
|
||||
}
|
||||
if value > high {
|
||||
return high
|
||||
}
|
||||
return value
|
||||
}
|
||||
|
||||
// --------------------------
|
||||
// helper functions for stats
|
||||
// --------------------------
|
||||
func getReversedSlice(input [statsHistoryElements]float64, start int, end int) []float64 {
|
||||
output := make([]float64, 0)
|
||||
for i := start; i <= end; i++ {
|
||||
output = append([]float64{input[i]}, output...)
|
||||
}
|
||||
return output
|
||||
}
|
||||
@@ -5,70 +5,166 @@ import (
|
||||
"fmt"
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
"path"
|
||||
"runtime"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/AdguardTeam/AdguardDNS/dnsfilter"
|
||||
"github.com/AdguardTeam/AdGuardHome/dnsfilter"
|
||||
"github.com/coredns/coredns/plugin/pkg/response"
|
||||
"github.com/miekg/dns"
|
||||
"github.com/zfjagann/golang-ring"
|
||||
)
|
||||
|
||||
var logBuffer = ring.Ring{}
|
||||
const (
|
||||
logBufferCap = 5000 // maximum capacity of logBuffer before it's flushed to disk
|
||||
queryLogTimeLimit = time.Hour * 24 // how far in the past we care about querylogs
|
||||
queryLogRotationPeriod = time.Hour * 24 // rotate the log every 24 hours
|
||||
queryLogFileName = "querylog.json" // .gz added during compression
|
||||
queryLogSize = 5000 // maximum API response for /querylog
|
||||
queryLogTopSize = 500 // Keep in memory only top N values
|
||||
)
|
||||
|
||||
var (
|
||||
logBufferLock sync.RWMutex
|
||||
logBuffer []*logEntry
|
||||
|
||||
queryLogCache []*logEntry
|
||||
queryLogLock sync.RWMutex
|
||||
)
|
||||
|
||||
type logEntry struct {
|
||||
R *dns.Msg
|
||||
Result dnsfilter.Result
|
||||
Time time.Time
|
||||
Elapsed time.Duration
|
||||
Question []byte
|
||||
Answer []byte `json:",omitempty"` // sometimes empty answers happen like binerdunt.top or rev2.globalrootservers.net
|
||||
Result dnsfilter.Result
|
||||
Time time.Time
|
||||
Elapsed time.Duration
|
||||
IP string
|
||||
}
|
||||
|
||||
func init() {
|
||||
logBuffer.SetCapacity(1000)
|
||||
}
|
||||
func logRequest(question *dns.Msg, answer *dns.Msg, result dnsfilter.Result, elapsed time.Duration, ip string) {
|
||||
var q []byte
|
||||
var a []byte
|
||||
var err error
|
||||
|
||||
func logRequest(r *dns.Msg, result dnsfilter.Result, elapsed time.Duration) {
|
||||
entry := logEntry{
|
||||
R: r,
|
||||
Result: result,
|
||||
Time: time.Now(),
|
||||
Elapsed: elapsed,
|
||||
if question != nil {
|
||||
q, err = question.Pack()
|
||||
if err != nil {
|
||||
log.Printf("failed to pack question for querylog: %s", err)
|
||||
return
|
||||
}
|
||||
}
|
||||
if answer != nil {
|
||||
a, err = answer.Pack()
|
||||
if err != nil {
|
||||
log.Printf("failed to pack answer for querylog: %s", err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
now := time.Now()
|
||||
entry := logEntry{
|
||||
Question: q,
|
||||
Answer: a,
|
||||
Result: result,
|
||||
Time: now,
|
||||
Elapsed: elapsed,
|
||||
IP: ip,
|
||||
}
|
||||
var flushBuffer []*logEntry
|
||||
|
||||
logBufferLock.Lock()
|
||||
logBuffer = append(logBuffer, &entry)
|
||||
if len(logBuffer) >= logBufferCap {
|
||||
flushBuffer = logBuffer
|
||||
logBuffer = nil
|
||||
}
|
||||
logBufferLock.Unlock()
|
||||
queryLogLock.Lock()
|
||||
queryLogCache = append(queryLogCache, &entry)
|
||||
if len(queryLogCache) > queryLogSize {
|
||||
toremove := len(queryLogCache) - queryLogSize
|
||||
queryLogCache = queryLogCache[toremove:]
|
||||
}
|
||||
queryLogLock.Unlock()
|
||||
|
||||
// add it to running top
|
||||
err = runningTop.addEntry(&entry, question, now)
|
||||
if err != nil {
|
||||
log.Printf("Failed to add entry to running top: %s", err)
|
||||
// don't do failure, just log
|
||||
}
|
||||
|
||||
// if buffer needs to be flushed to disk, do it now
|
||||
if len(flushBuffer) > 0 {
|
||||
// write to file
|
||||
// do it in separate goroutine -- we are stalling DNS response this whole time
|
||||
go flushToFile(flushBuffer)
|
||||
}
|
||||
logBuffer.Enqueue(entry)
|
||||
}
|
||||
|
||||
func handler(w http.ResponseWriter, r *http.Request) {
|
||||
values := logBuffer.Values()
|
||||
//noinspection GoUnusedParameter
|
||||
func HandleQueryLog(w http.ResponseWriter, r *http.Request) {
|
||||
queryLogLock.RLock()
|
||||
values := make([]*logEntry, len(queryLogCache))
|
||||
copy(values, queryLogCache)
|
||||
queryLogLock.RUnlock()
|
||||
|
||||
// reverse it so that newest is first
|
||||
for left, right := 0, len(values)-1; left < right; left, right = left+1, right-1 {
|
||||
values[left], values[right] = values[right], values[left]
|
||||
}
|
||||
|
||||
var data = []map[string]interface{}{}
|
||||
for _, value := range values {
|
||||
entry, ok := value.(logEntry)
|
||||
if !ok {
|
||||
continue
|
||||
for _, entry := range values {
|
||||
var q *dns.Msg
|
||||
var a *dns.Msg
|
||||
|
||||
if len(entry.Question) > 0 {
|
||||
q = new(dns.Msg)
|
||||
if err := q.Unpack(entry.Question); err != nil {
|
||||
// ignore, log and move on
|
||||
log.Printf("Failed to unpack dns message question: %s", err)
|
||||
q = nil
|
||||
}
|
||||
}
|
||||
if len(entry.Answer) > 0 {
|
||||
a = new(dns.Msg)
|
||||
if err := a.Unpack(entry.Answer); err != nil {
|
||||
// ignore, log and move on
|
||||
log.Printf("Failed to unpack dns message question: %s", err)
|
||||
a = nil
|
||||
}
|
||||
}
|
||||
|
||||
jsonentry := map[string]interface{}{
|
||||
jsonEntry := map[string]interface{}{
|
||||
"reason": entry.Result.Reason.String(),
|
||||
"elapsed_ms": strconv.FormatFloat(entry.Elapsed.Seconds()*1000, 'f', -1, 64),
|
||||
"elapsedMs": strconv.FormatFloat(entry.Elapsed.Seconds()*1000, 'f', -1, 64),
|
||||
"time": entry.Time.Format(time.RFC3339),
|
||||
"client": entry.IP,
|
||||
}
|
||||
question := map[string]interface{}{
|
||||
"host": strings.ToLower(strings.TrimSuffix(entry.R.Question[0].Name, ".")),
|
||||
"type": dns.Type(entry.R.Question[0].Qtype).String(),
|
||||
"class": dns.Class(entry.R.Question[0].Qclass).String(),
|
||||
if q != nil {
|
||||
jsonEntry["question"] = map[string]interface{}{
|
||||
"host": strings.ToLower(strings.TrimSuffix(q.Question[0].Name, ".")),
|
||||
"type": dns.Type(q.Question[0].Qtype).String(),
|
||||
"class": dns.Class(q.Question[0].Qclass).String(),
|
||||
}
|
||||
}
|
||||
jsonentry["question"] = question
|
||||
|
||||
status, _ := response.Typify(entry.R, time.Now().UTC())
|
||||
jsonentry["status"] = status.String()
|
||||
if a != nil {
|
||||
status, _ := response.Typify(a, time.Now().UTC())
|
||||
jsonEntry["status"] = status.String()
|
||||
}
|
||||
if len(entry.Result.Rule) > 0 {
|
||||
jsonentry["rule"] = entry.Result.Rule
|
||||
jsonEntry["rule"] = entry.Result.Rule
|
||||
jsonEntry["filterId"] = entry.Result.FilterID
|
||||
}
|
||||
|
||||
if len(entry.R.Answer) > 0 {
|
||||
if a != nil && len(a.Answer) > 0 {
|
||||
var answers = []map[string]interface{}{}
|
||||
for _, k := range entry.R.Answer {
|
||||
for _, k := range a.Answer {
|
||||
header := k.Header()
|
||||
answer := map[string]interface{}{
|
||||
"type": dns.TypeToString[header.Rrtype],
|
||||
@@ -106,41 +202,39 @@ func handler(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
answers = append(answers, answer)
|
||||
}
|
||||
jsonentry["answer"] = answers
|
||||
jsonEntry["answer"] = answers
|
||||
}
|
||||
|
||||
data = append(data, jsonentry)
|
||||
data = append(data, jsonEntry)
|
||||
}
|
||||
|
||||
json, err := json.Marshal(data)
|
||||
jsonVal, err := json.Marshal(data)
|
||||
if err != nil {
|
||||
errortext := fmt.Sprintf("Couldn't marshal data into json: %s", err)
|
||||
log.Println(errortext)
|
||||
http.Error(w, errortext, http.StatusInternalServerError)
|
||||
errorText := fmt.Sprintf("Couldn't marshal data into json: %s", err)
|
||||
log.Println(errorText)
|
||||
http.Error(w, errorText, http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
_, err = w.Write(json)
|
||||
_, err = w.Write(jsonVal)
|
||||
if err != nil {
|
||||
errortext := fmt.Sprintf("Unable to write response json: %s", err)
|
||||
log.Println(errortext)
|
||||
http.Error(w, errortext, 500)
|
||||
errorText := fmt.Sprintf("Unable to write response json: %s", err)
|
||||
log.Println(errorText)
|
||||
http.Error(w, errorText, http.StatusInternalServerError)
|
||||
}
|
||||
}
|
||||
|
||||
func startQueryLogServer() {
|
||||
listenAddr := "127.0.0.1:8618" // sha512sum of "querylog" then each byte summed
|
||||
|
||||
http.HandleFunc("/querylog", handler)
|
||||
if err := http.ListenAndServe(listenAddr, nil); err != nil {
|
||||
log.Fatalf("error in ListenAndServe: %s", err)
|
||||
}
|
||||
}
|
||||
|
||||
func trace(text string) {
|
||||
func trace(format string, args ...interface{}) {
|
||||
pc := make([]uintptr, 10) // at least 1 entry needed
|
||||
runtime.Callers(2, pc)
|
||||
f := runtime.FuncForPC(pc[0])
|
||||
log.Printf("%s(): %s\n", f.Name(), text)
|
||||
var buf strings.Builder
|
||||
buf.WriteString(fmt.Sprintf("%s(): ", path.Base(f.Name())))
|
||||
text := fmt.Sprintf(format, args...)
|
||||
buf.WriteString(text)
|
||||
if len(text) == 0 || text[len(text)-1] != '\n' {
|
||||
buf.WriteRune('\n')
|
||||
}
|
||||
fmt.Fprint(os.Stderr, buf.String())
|
||||
}
|
||||
|
||||
291
coredns_plugin/querylog_file.go
Normal file
291
coredns_plugin/querylog_file.go
Normal file
@@ -0,0 +1,291 @@
|
||||
package dnsfilter
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"compress/gzip"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/go-test/deep"
|
||||
)
|
||||
|
||||
var (
|
||||
fileWriteLock sync.Mutex
|
||||
)
|
||||
|
||||
const enableGzip = false
|
||||
|
||||
func flushToFile(buffer []*logEntry) error {
|
||||
if len(buffer) == 0 {
|
||||
return nil
|
||||
}
|
||||
start := time.Now()
|
||||
|
||||
var b bytes.Buffer
|
||||
e := json.NewEncoder(&b)
|
||||
for _, entry := range buffer {
|
||||
err := e.Encode(entry)
|
||||
if err != nil {
|
||||
log.Printf("Failed to marshal entry: %s", err)
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
elapsed := time.Since(start)
|
||||
log.Printf("%d elements serialized via json in %v: %d kB, %v/entry, %v/entry", len(buffer), elapsed, b.Len()/1024, float64(b.Len())/float64(len(buffer)), elapsed/time.Duration(len(buffer)))
|
||||
|
||||
err := checkBuffer(buffer, b)
|
||||
if err != nil {
|
||||
log.Printf("failed to check buffer: %s", err)
|
||||
return err
|
||||
}
|
||||
|
||||
var zb bytes.Buffer
|
||||
filename := queryLogFileName
|
||||
|
||||
// gzip enabled?
|
||||
if enableGzip {
|
||||
filename += ".gz"
|
||||
|
||||
zw := gzip.NewWriter(&zb)
|
||||
zw.Name = queryLogFileName
|
||||
zw.ModTime = time.Now()
|
||||
|
||||
_, err = zw.Write(b.Bytes())
|
||||
if err != nil {
|
||||
log.Printf("Couldn't compress to gzip: %s", err)
|
||||
zw.Close()
|
||||
return err
|
||||
}
|
||||
|
||||
if err = zw.Close(); err != nil {
|
||||
log.Printf("Couldn't close gzip writer: %s", err)
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
zb = b
|
||||
}
|
||||
|
||||
fileWriteLock.Lock()
|
||||
defer fileWriteLock.Unlock()
|
||||
f, err := os.OpenFile(filename, os.O_WRONLY|os.O_CREATE|os.O_APPEND, 0644)
|
||||
if err != nil {
|
||||
log.Printf("failed to create file \"%s\": %s", filename, err)
|
||||
return err
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
n, err := f.Write(zb.Bytes())
|
||||
if err != nil {
|
||||
log.Printf("Couldn't write to file: %s", err)
|
||||
return err
|
||||
}
|
||||
|
||||
log.Printf("ok \"%s\": %v bytes written", filename, n)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func checkBuffer(buffer []*logEntry, b bytes.Buffer) error {
|
||||
l := len(buffer)
|
||||
d := json.NewDecoder(&b)
|
||||
|
||||
i := 0
|
||||
for d.More() {
|
||||
entry := &logEntry{}
|
||||
err := d.Decode(entry)
|
||||
if err != nil {
|
||||
log.Printf("Failed to decode: %s", err)
|
||||
return err
|
||||
}
|
||||
if diff := deep.Equal(entry, buffer[i]); diff != nil {
|
||||
log.Printf("decoded buffer differs: %s", diff)
|
||||
return fmt.Errorf("decoded buffer differs: %s", diff)
|
||||
}
|
||||
i++
|
||||
}
|
||||
if i != l {
|
||||
err := fmt.Errorf("check fail: %d vs %d entries", l, i)
|
||||
log.Print(err)
|
||||
return err
|
||||
}
|
||||
log.Printf("check ok: %d entries", i)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func rotateQueryLog() error {
|
||||
from := queryLogFileName
|
||||
to := queryLogFileName + ".1"
|
||||
|
||||
if enableGzip {
|
||||
from = queryLogFileName + ".gz"
|
||||
to = queryLogFileName + ".gz.1"
|
||||
}
|
||||
|
||||
if _, err := os.Stat(from); os.IsNotExist(err) {
|
||||
// do nothing, file doesn't exist
|
||||
return nil
|
||||
}
|
||||
|
||||
err := os.Rename(from, to)
|
||||
if err != nil {
|
||||
log.Printf("Failed to rename querylog: %s", err)
|
||||
return err
|
||||
}
|
||||
|
||||
log.Printf("Rotated from %s to %s successfully", from, to)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func periodicQueryLogRotate() {
|
||||
for range time.Tick(queryLogRotationPeriod) {
|
||||
err := rotateQueryLog()
|
||||
if err != nil {
|
||||
log.Printf("Failed to rotate querylog: %s", err)
|
||||
// do nothing, continue rotating
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func genericLoader(onEntry func(entry *logEntry) error, needMore func() bool, timeWindow time.Duration) error {
|
||||
now := time.Now()
|
||||
// read from querylog files, try newest file first
|
||||
files := []string{}
|
||||
|
||||
if enableGzip {
|
||||
files = []string{
|
||||
queryLogFileName + ".gz",
|
||||
queryLogFileName + ".gz.1",
|
||||
}
|
||||
} else {
|
||||
files = []string{
|
||||
queryLogFileName,
|
||||
queryLogFileName + ".1",
|
||||
}
|
||||
}
|
||||
|
||||
// read from all files
|
||||
for _, file := range files {
|
||||
if !needMore() {
|
||||
break
|
||||
}
|
||||
if _, err := os.Stat(file); os.IsNotExist(err) {
|
||||
// do nothing, file doesn't exist
|
||||
continue
|
||||
}
|
||||
|
||||
f, err := os.Open(file)
|
||||
if err != nil {
|
||||
log.Printf("Failed to open file \"%s\": %s", file, err)
|
||||
// try next file
|
||||
continue
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
var d *json.Decoder
|
||||
|
||||
if enableGzip {
|
||||
trace("Creating gzip reader")
|
||||
zr, err := gzip.NewReader(f)
|
||||
if err != nil {
|
||||
log.Printf("Failed to create gzip reader: %s", err)
|
||||
continue
|
||||
}
|
||||
defer zr.Close()
|
||||
|
||||
trace("Creating json decoder")
|
||||
d = json.NewDecoder(zr)
|
||||
} else {
|
||||
d = json.NewDecoder(f)
|
||||
}
|
||||
|
||||
i := 0
|
||||
over := 0
|
||||
max := 10000 * time.Second
|
||||
var sum time.Duration
|
||||
// entries on file are in oldest->newest order
|
||||
// we want maxLen newest
|
||||
for d.More() {
|
||||
if !needMore() {
|
||||
break
|
||||
}
|
||||
var entry logEntry
|
||||
err := d.Decode(&entry)
|
||||
if err != nil {
|
||||
log.Printf("Failed to decode: %s", err)
|
||||
// next entry can be fine, try more
|
||||
continue
|
||||
}
|
||||
|
||||
if now.Sub(entry.Time) > timeWindow {
|
||||
// trace("skipping entry") // debug logging
|
||||
continue
|
||||
}
|
||||
|
||||
if entry.Elapsed > max {
|
||||
over++
|
||||
} else {
|
||||
sum += entry.Elapsed
|
||||
}
|
||||
|
||||
i++
|
||||
err = onEntry(&entry)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
elapsed := time.Since(now)
|
||||
var perunit time.Duration
|
||||
var avg time.Duration
|
||||
if i > 0 {
|
||||
perunit = elapsed / time.Duration(i)
|
||||
avg = sum / time.Duration(i)
|
||||
}
|
||||
log.Printf("file \"%s\": read %d entries in %v, %v/entry, %v over %v, %v avg", file, i, elapsed, perunit, over, max, avg)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func appendFromLogFile(values []*logEntry, maxLen int, timeWindow time.Duration) []*logEntry {
|
||||
a := []*logEntry{}
|
||||
|
||||
onEntry := func(entry *logEntry) error {
|
||||
a = append(a, entry)
|
||||
if len(a) > maxLen {
|
||||
toskip := len(a) - maxLen
|
||||
a = a[toskip:]
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
needMore := func() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
err := genericLoader(onEntry, needMore, timeWindow)
|
||||
if err != nil {
|
||||
log.Printf("Failed to load entries from querylog: %s", err)
|
||||
return values
|
||||
}
|
||||
|
||||
// now that we've read all eligible entries, reverse the slice to make it go from newest->oldest
|
||||
for left, right := 0, len(a)-1; left < right; left, right = left+1, right-1 {
|
||||
a[left], a[right] = a[right], a[left]
|
||||
}
|
||||
|
||||
// append it to values
|
||||
values = append(values, a...)
|
||||
|
||||
// then cut off of it is bigger than maxLen
|
||||
if len(values) > maxLen {
|
||||
values = values[:maxLen]
|
||||
}
|
||||
|
||||
return values
|
||||
}
|
||||
386
coredns_plugin/querylog_top.go
Normal file
386
coredns_plugin/querylog_top.go
Normal file
@@ -0,0 +1,386 @@
|
||||
package dnsfilter
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
"path"
|
||||
"runtime"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/AdguardTeam/AdGuardHome/dnsfilter"
|
||||
"github.com/bluele/gcache"
|
||||
"github.com/miekg/dns"
|
||||
)
|
||||
|
||||
type hourTop struct {
|
||||
domains gcache.Cache
|
||||
blocked gcache.Cache
|
||||
clients gcache.Cache
|
||||
|
||||
mutex sync.RWMutex
|
||||
}
|
||||
|
||||
func (top *hourTop) init() {
|
||||
top.domains = gcache.New(queryLogTopSize).LRU().Build()
|
||||
top.blocked = gcache.New(queryLogTopSize).LRU().Build()
|
||||
top.clients = gcache.New(queryLogTopSize).LRU().Build()
|
||||
}
|
||||
|
||||
type dayTop struct {
|
||||
hours []*hourTop
|
||||
hoursLock sync.RWMutex // writelock this lock ONLY WHEN rotating or intializing hours!
|
||||
|
||||
loaded bool
|
||||
loadedLock sync.Mutex
|
||||
}
|
||||
|
||||
var runningTop dayTop
|
||||
|
||||
func init() {
|
||||
runningTop.hoursWriteLock()
|
||||
for i := 0; i < 24; i++ {
|
||||
hour := hourTop{}
|
||||
hour.init()
|
||||
runningTop.hours = append(runningTop.hours, &hour)
|
||||
}
|
||||
runningTop.hoursWriteUnlock()
|
||||
}
|
||||
|
||||
func rotateHourlyTop() {
|
||||
log.Printf("Rotating hourly top")
|
||||
hour := &hourTop{}
|
||||
hour.init()
|
||||
runningTop.hoursWriteLock()
|
||||
runningTop.hours = append([]*hourTop{hour}, runningTop.hours...)
|
||||
runningTop.hours = runningTop.hours[:24]
|
||||
runningTop.hoursWriteUnlock()
|
||||
}
|
||||
|
||||
func periodicHourlyTopRotate() {
|
||||
t := time.Hour
|
||||
for range time.Tick(t) {
|
||||
rotateHourlyTop()
|
||||
}
|
||||
}
|
||||
|
||||
func (top *hourTop) incrementValue(key string, cache gcache.Cache) error {
|
||||
top.Lock()
|
||||
defer top.Unlock()
|
||||
ivalue, err := cache.Get(key)
|
||||
if err == gcache.KeyNotFoundError {
|
||||
// we just set it and we're done
|
||||
err = cache.Set(key, 1)
|
||||
if err != nil {
|
||||
log.Printf("Failed to set hourly top value: %s", err)
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
log.Printf("gcache encountered an error during get: %s", err)
|
||||
return err
|
||||
}
|
||||
|
||||
cachedValue, ok := ivalue.(int)
|
||||
if !ok {
|
||||
err = fmt.Errorf("SHOULD NOT HAPPEN: gcache has non-int as value: %v", ivalue)
|
||||
log.Println(err)
|
||||
return err
|
||||
}
|
||||
|
||||
err = cache.Set(key, cachedValue+1)
|
||||
if err != nil {
|
||||
log.Printf("Failed to set hourly top value: %s", err)
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (top *hourTop) incrementDomains(key string) error {
|
||||
return top.incrementValue(key, top.domains)
|
||||
}
|
||||
|
||||
func (top *hourTop) incrementBlocked(key string) error {
|
||||
return top.incrementValue(key, top.blocked)
|
||||
}
|
||||
|
||||
func (top *hourTop) incrementClients(key string) error {
|
||||
return top.incrementValue(key, top.clients)
|
||||
}
|
||||
|
||||
// if does not exist -- return 0
|
||||
func (top *hourTop) lockedGetValue(key string, cache gcache.Cache) (int, error) {
|
||||
ivalue, err := cache.Get(key)
|
||||
if err == gcache.KeyNotFoundError {
|
||||
return 0, nil
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
log.Printf("gcache encountered an error during get: %s", err)
|
||||
return 0, err
|
||||
}
|
||||
|
||||
value, ok := ivalue.(int)
|
||||
if !ok {
|
||||
err := fmt.Errorf("SHOULD NOT HAPPEN: gcache has non-int as value: %v", ivalue)
|
||||
log.Println(err)
|
||||
return 0, err
|
||||
}
|
||||
|
||||
return value, nil
|
||||
}
|
||||
|
||||
func (top *hourTop) lockedGetDomains(key string) (int, error) {
|
||||
return top.lockedGetValue(key, top.domains)
|
||||
}
|
||||
|
||||
func (top *hourTop) lockedGetBlocked(key string) (int, error) {
|
||||
return top.lockedGetValue(key, top.blocked)
|
||||
}
|
||||
|
||||
func (top *hourTop) lockedGetClients(key string) (int, error) {
|
||||
return top.lockedGetValue(key, top.clients)
|
||||
}
|
||||
|
||||
func (r *dayTop) addEntry(entry *logEntry, q *dns.Msg, now time.Time) error {
|
||||
// figure out which hour bucket it belongs to
|
||||
hour := int(now.Sub(entry.Time).Hours())
|
||||
if hour >= 24 {
|
||||
log.Printf("t %v is >24 hours ago, ignoring", entry.Time)
|
||||
return nil
|
||||
}
|
||||
|
||||
hostname := strings.ToLower(strings.TrimSuffix(q.Question[0].Name, "."))
|
||||
|
||||
// get value, if not set, crate one
|
||||
runningTop.hoursReadLock()
|
||||
defer runningTop.hoursReadUnlock()
|
||||
err := runningTop.hours[hour].incrementDomains(hostname)
|
||||
if err != nil {
|
||||
log.Printf("Failed to increment value: %s", err)
|
||||
return err
|
||||
}
|
||||
|
||||
if entry.Result.IsFiltered {
|
||||
err := runningTop.hours[hour].incrementBlocked(hostname)
|
||||
if err != nil {
|
||||
log.Printf("Failed to increment value: %s", err)
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if len(entry.IP) > 0 {
|
||||
err := runningTop.hours[hour].incrementClients(entry.IP)
|
||||
if err != nil {
|
||||
log.Printf("Failed to increment value: %s", err)
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func fillStatsFromQueryLog() error {
|
||||
now := time.Now()
|
||||
runningTop.loadedWriteLock()
|
||||
defer runningTop.loadedWriteUnlock()
|
||||
if runningTop.loaded {
|
||||
return nil
|
||||
}
|
||||
onEntry := func(entry *logEntry) error {
|
||||
if len(entry.Question) == 0 {
|
||||
log.Printf("entry question is absent, skipping")
|
||||
return nil
|
||||
}
|
||||
|
||||
if entry.Time.After(now) {
|
||||
log.Printf("t %v vs %v is in the future, ignoring", entry.Time, now)
|
||||
return nil
|
||||
}
|
||||
|
||||
q := new(dns.Msg)
|
||||
if err := q.Unpack(entry.Question); err != nil {
|
||||
log.Printf("failed to unpack dns message question: %s", err)
|
||||
return err
|
||||
}
|
||||
|
||||
if len(q.Question) != 1 {
|
||||
log.Printf("malformed dns message, has no questions, skipping")
|
||||
return nil
|
||||
}
|
||||
|
||||
err := runningTop.addEntry(entry, q, now)
|
||||
if err != nil {
|
||||
log.Printf("Failed to add entry to running top: %s", err)
|
||||
return err
|
||||
}
|
||||
|
||||
queryLogLock.Lock()
|
||||
queryLogCache = append(queryLogCache, entry)
|
||||
if len(queryLogCache) > queryLogSize {
|
||||
toremove := len(queryLogCache) - queryLogSize
|
||||
queryLogCache = queryLogCache[toremove:]
|
||||
}
|
||||
queryLogLock.Unlock()
|
||||
|
||||
requests.IncWithTime(entry.Time)
|
||||
if entry.Result.IsFiltered {
|
||||
filtered.IncWithTime(entry.Time)
|
||||
}
|
||||
switch entry.Result.Reason {
|
||||
case dnsfilter.NotFilteredWhiteList:
|
||||
whitelisted.IncWithTime(entry.Time)
|
||||
case dnsfilter.NotFilteredError:
|
||||
errorsTotal.IncWithTime(entry.Time)
|
||||
case dnsfilter.FilteredBlackList:
|
||||
filteredLists.IncWithTime(entry.Time)
|
||||
case dnsfilter.FilteredSafeBrowsing:
|
||||
filteredSafebrowsing.IncWithTime(entry.Time)
|
||||
case dnsfilter.FilteredParental:
|
||||
filteredParental.IncWithTime(entry.Time)
|
||||
case dnsfilter.FilteredInvalid:
|
||||
// do nothing
|
||||
case dnsfilter.FilteredSafeSearch:
|
||||
safesearch.IncWithTime(entry.Time)
|
||||
}
|
||||
elapsedTime.ObserveWithTime(entry.Elapsed.Seconds(), entry.Time)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
needMore := func() bool { return true }
|
||||
err := genericLoader(onEntry, needMore, queryLogTimeLimit)
|
||||
if err != nil {
|
||||
log.Printf("Failed to load entries from querylog: %s", err)
|
||||
return err
|
||||
}
|
||||
|
||||
runningTop.loaded = true
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func HandleStatsTop(w http.ResponseWriter, r *http.Request) {
|
||||
domains := map[string]int{}
|
||||
blocked := map[string]int{}
|
||||
clients := map[string]int{}
|
||||
|
||||
do := func(keys []interface{}, getter func(key string) (int, error), result map[string]int) {
|
||||
for _, ikey := range keys {
|
||||
key, ok := ikey.(string)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
value, err := getter(key)
|
||||
if err != nil {
|
||||
log.Printf("Failed to get top domains value for %v: %s", key, err)
|
||||
return
|
||||
}
|
||||
result[key] += value
|
||||
}
|
||||
}
|
||||
|
||||
runningTop.hoursReadLock()
|
||||
for hour := 0; hour < 24; hour++ {
|
||||
runningTop.hours[hour].RLock()
|
||||
do(runningTop.hours[hour].domains.Keys(), runningTop.hours[hour].lockedGetDomains, domains)
|
||||
do(runningTop.hours[hour].blocked.Keys(), runningTop.hours[hour].lockedGetBlocked, blocked)
|
||||
do(runningTop.hours[hour].clients.Keys(), runningTop.hours[hour].lockedGetClients, clients)
|
||||
runningTop.hours[hour].RUnlock()
|
||||
}
|
||||
runningTop.hoursReadUnlock()
|
||||
|
||||
// use manual json marshalling because we want maps to be sorted by value
|
||||
json := bytes.Buffer{}
|
||||
json.WriteString("{\n")
|
||||
|
||||
gen := func(json *bytes.Buffer, name string, top map[string]int, addComma bool) {
|
||||
json.WriteString(" ")
|
||||
json.WriteString(fmt.Sprintf("%q", name))
|
||||
json.WriteString(": {\n")
|
||||
sorted := sortByValue(top)
|
||||
// no more than 50 entries
|
||||
if len(sorted) > 50 {
|
||||
sorted = sorted[:50]
|
||||
}
|
||||
for i, key := range sorted {
|
||||
json.WriteString(" ")
|
||||
json.WriteString(fmt.Sprintf("%q", key))
|
||||
json.WriteString(": ")
|
||||
json.WriteString(strconv.Itoa(top[key]))
|
||||
if i+1 != len(sorted) {
|
||||
json.WriteByte(',')
|
||||
}
|
||||
json.WriteByte('\n')
|
||||
}
|
||||
json.WriteString(" }")
|
||||
if addComma {
|
||||
json.WriteByte(',')
|
||||
}
|
||||
json.WriteByte('\n')
|
||||
}
|
||||
gen(&json, "top_queried_domains", domains, true)
|
||||
gen(&json, "top_blocked_domains", blocked, true)
|
||||
gen(&json, "top_clients", clients, true)
|
||||
json.WriteString(" \"stats_period\": \"24 hours\"\n")
|
||||
json.WriteString("}\n")
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
_, err := w.Write(json.Bytes())
|
||||
if err != nil {
|
||||
errortext := fmt.Sprintf("Couldn't write body: %s", err)
|
||||
log.Println(errortext)
|
||||
http.Error(w, errortext, http.StatusInternalServerError)
|
||||
}
|
||||
}
|
||||
|
||||
// helper function for querylog API
|
||||
func sortByValue(m map[string]int) []string {
|
||||
type kv struct {
|
||||
k string
|
||||
v int
|
||||
}
|
||||
var ss []kv
|
||||
for k, v := range m {
|
||||
ss = append(ss, kv{k, v})
|
||||
}
|
||||
sort.Slice(ss, func(l, r int) bool {
|
||||
return ss[l].v > ss[r].v
|
||||
})
|
||||
|
||||
sorted := []string{}
|
||||
for _, v := range ss {
|
||||
sorted = append(sorted, v.k)
|
||||
}
|
||||
return sorted
|
||||
}
|
||||
|
||||
func (d *dayTop) hoursWriteLock() { tracelock(); d.hoursLock.Lock() }
|
||||
func (d *dayTop) hoursWriteUnlock() { tracelock(); d.hoursLock.Unlock() }
|
||||
func (d *dayTop) hoursReadLock() { tracelock(); d.hoursLock.RLock() }
|
||||
func (d *dayTop) hoursReadUnlock() { tracelock(); d.hoursLock.RUnlock() }
|
||||
func (d *dayTop) loadedWriteLock() { tracelock(); d.loadedLock.Lock() }
|
||||
func (d *dayTop) loadedWriteUnlock() { tracelock(); d.loadedLock.Unlock() }
|
||||
|
||||
func (h *hourTop) Lock() { tracelock(); h.mutex.Lock() }
|
||||
func (h *hourTop) RLock() { tracelock(); h.mutex.RLock() }
|
||||
func (h *hourTop) RUnlock() { tracelock(); h.mutex.RUnlock() }
|
||||
func (h *hourTop) Unlock() { tracelock(); h.mutex.Unlock() }
|
||||
|
||||
func tracelock() {
|
||||
if false { // not commented out to make code checked during compilation
|
||||
pc := make([]uintptr, 10) // at least 1 entry needed
|
||||
runtime.Callers(2, pc)
|
||||
f := path.Base(runtime.FuncForPC(pc[1]).Name())
|
||||
lockf := path.Base(runtime.FuncForPC(pc[0]).Name())
|
||||
fmt.Fprintf(os.Stderr, "%s(): %s\n", f, lockf)
|
||||
}
|
||||
}
|
||||
@@ -3,8 +3,8 @@ package ratelimit
|
||||
import (
|
||||
"errors"
|
||||
"log"
|
||||
"sort"
|
||||
"strconv"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
// ratelimiting and per-ip buckets
|
||||
@@ -15,6 +15,7 @@ import (
|
||||
"github.com/coredns/coredns/core/dnsserver"
|
||||
"github.com/coredns/coredns/plugin"
|
||||
"github.com/coredns/coredns/plugin/metrics"
|
||||
"github.com/coredns/coredns/plugin/pkg/dnstest"
|
||||
"github.com/coredns/coredns/request"
|
||||
"github.com/mholt/caddy"
|
||||
"github.com/miekg/dns"
|
||||
@@ -22,15 +23,15 @@ import (
|
||||
"golang.org/x/net/context"
|
||||
)
|
||||
|
||||
const defaultRatelimit = 100
|
||||
const defaultMaxRateLimitedIPs = 1024 * 1024
|
||||
const defaultRatelimit = 30
|
||||
const defaultResponseSize = 1000
|
||||
|
||||
var (
|
||||
tokenBuckets = cache.New(time.Hour, time.Hour)
|
||||
)
|
||||
|
||||
// main function
|
||||
func (p *Plugin) ServeDNS(ctx context.Context, w dns.ResponseWriter, r *dns.Msg) (int, error) {
|
||||
// ServeDNS handles the DNS request and refuses if it's an beyind specified ratelimit
|
||||
func (p *plug) ServeDNS(ctx context.Context, w dns.ResponseWriter, r *dns.Msg) (int, error) {
|
||||
state := request.Request{W: w, Req: r}
|
||||
ip := state.IP()
|
||||
allow, err := p.allowRequest(ip)
|
||||
@@ -41,10 +42,34 @@ func (p *Plugin) ServeDNS(ctx context.Context, w dns.ResponseWriter, r *dns.Msg)
|
||||
ratelimited.Inc()
|
||||
return 0, nil
|
||||
}
|
||||
return plugin.NextOrFailure(p.Name(), p.Next, ctx, w, r)
|
||||
|
||||
// Record response to get status code and size of the reply.
|
||||
rw := dnstest.NewRecorder(w)
|
||||
status, err := plugin.NextOrFailure(p.Name(), p.Next, ctx, rw, r)
|
||||
|
||||
size := rw.Len
|
||||
|
||||
if size > defaultResponseSize && state.Proto() == "udp" {
|
||||
// For large UDP responses we call allowRequest more times
|
||||
// The exact number of times depends on the response size
|
||||
for i := 0; i < size/defaultResponseSize; i++ {
|
||||
p.allowRequest(ip)
|
||||
}
|
||||
}
|
||||
|
||||
return status, err
|
||||
}
|
||||
|
||||
func (p *Plugin) allowRequest(ip string) (bool, error) {
|
||||
func (p *plug) allowRequest(ip string) (bool, error) {
|
||||
|
||||
if len(p.whitelist) > 0 {
|
||||
i := sort.SearchStrings(p.whitelist, ip)
|
||||
|
||||
if i < len(p.whitelist) && p.whitelist[i] == ip {
|
||||
return true, nil
|
||||
}
|
||||
}
|
||||
|
||||
if _, found := tokenBuckets.Get(ip); !found {
|
||||
tokenBuckets.Set(ip, rate.New(p.ratelimit, time.Second), time.Hour)
|
||||
}
|
||||
@@ -59,7 +84,7 @@ func (p *Plugin) allowRequest(ip string) (bool, error) {
|
||||
}
|
||||
|
||||
rl, ok := value.(*rate.RateLimiter)
|
||||
if ok == false {
|
||||
if !ok {
|
||||
text := "SHOULD NOT HAPPEN: non-bool entry found in safebrowsing lookup cache"
|
||||
log.Println(text)
|
||||
err := errors.New(text)
|
||||
@@ -80,51 +105,69 @@ func init() {
|
||||
})
|
||||
}
|
||||
|
||||
type Plugin struct {
|
||||
type plug struct {
|
||||
Next plugin.Handler
|
||||
|
||||
// configuration for creating above
|
||||
ratelimit int // in requests per second per IP
|
||||
ratelimit int // in requests per second per IP
|
||||
whitelist []string // a list of whitelisted IP addresses
|
||||
}
|
||||
|
||||
func setup(c *caddy.Controller) error {
|
||||
p := &Plugin{ratelimit: defaultRatelimit}
|
||||
config := dnsserver.GetConfig(c)
|
||||
func setupPlugin(c *caddy.Controller) (*plug, error) {
|
||||
|
||||
p := &plug{ratelimit: defaultRatelimit}
|
||||
|
||||
for c.Next() {
|
||||
args := c.RemainingArgs()
|
||||
if len(args) <= 0 {
|
||||
continue
|
||||
if len(args) > 0 {
|
||||
ratelimit, err := strconv.Atoi(args[0])
|
||||
if err != nil {
|
||||
return nil, c.ArgErr()
|
||||
}
|
||||
p.ratelimit = ratelimit
|
||||
}
|
||||
ratelimit, err := strconv.Atoi(args[0])
|
||||
if err != nil {
|
||||
return c.ArgErr()
|
||||
for c.NextBlock() {
|
||||
switch c.Val() {
|
||||
case "whitelist":
|
||||
p.whitelist = c.RemainingArgs()
|
||||
|
||||
if len(p.whitelist) > 0 {
|
||||
sort.Strings(p.whitelist)
|
||||
}
|
||||
}
|
||||
}
|
||||
p.ratelimit = ratelimit
|
||||
}
|
||||
|
||||
return p, nil
|
||||
}
|
||||
|
||||
func setup(c *caddy.Controller) error {
|
||||
p, err := setupPlugin(c)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
config := dnsserver.GetConfig(c)
|
||||
config.AddPlugin(func(next plugin.Handler) plugin.Handler {
|
||||
p.Next = next
|
||||
return p
|
||||
})
|
||||
|
||||
c.OnStartup(func() error {
|
||||
once.Do(func() {
|
||||
m := dnsserver.GetConfig(c).Handler("prometheus")
|
||||
if m == nil {
|
||||
return
|
||||
}
|
||||
if x, ok := m.(*metrics.Metrics); ok {
|
||||
x.MustRegister(ratelimited)
|
||||
}
|
||||
})
|
||||
m := dnsserver.GetConfig(c).Handler("prometheus")
|
||||
if m == nil {
|
||||
return nil
|
||||
}
|
||||
if x, ok := m.(*metrics.Metrics); ok {
|
||||
x.MustRegister(ratelimited)
|
||||
}
|
||||
return nil
|
||||
})
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func newDnsCounter(name string, help string) prometheus.Counter {
|
||||
func newDNSCounter(name string, help string) prometheus.Counter {
|
||||
return prometheus.NewCounter(prometheus.CounterOpts{
|
||||
Namespace: plugin.Namespace,
|
||||
Subsystem: "ratelimit",
|
||||
@@ -134,9 +177,8 @@ func newDnsCounter(name string, help string) prometheus.Counter {
|
||||
}
|
||||
|
||||
var (
|
||||
ratelimited = newDnsCounter("dropped_total", "Count of requests that have been dropped because of rate limit")
|
||||
ratelimited = newDNSCounter("dropped_total", "Count of requests that have been dropped because of rate limit")
|
||||
)
|
||||
|
||||
func (d *Plugin) Name() string { return "ratelimit" }
|
||||
|
||||
var once sync.Once
|
||||
// Name returns name of the plugin as seen in Corefile and plugin.cfg
|
||||
func (p *plug) Name() string { return "ratelimit" }
|
||||
|
||||
82
coredns_plugin/ratelimit/ratelimit_test.go
Normal file
82
coredns_plugin/ratelimit/ratelimit_test.go
Normal file
@@ -0,0 +1,82 @@
|
||||
package ratelimit
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/mholt/caddy"
|
||||
)
|
||||
|
||||
func TestSetup(t *testing.T) {
|
||||
for i, testcase := range []struct {
|
||||
config string
|
||||
failing bool
|
||||
}{
|
||||
{`ratelimit`, false},
|
||||
{`ratelimit 100`, false},
|
||||
{`ratelimit {
|
||||
whitelist 127.0.0.1
|
||||
}`, false},
|
||||
{`ratelimit 50 {
|
||||
whitelist 127.0.0.1 176.103.130.130
|
||||
}`, false},
|
||||
{`ratelimit test`, true},
|
||||
} {
|
||||
c := caddy.NewTestController("dns", testcase.config)
|
||||
err := setup(c)
|
||||
if err != nil {
|
||||
if !testcase.failing {
|
||||
t.Fatalf("Test #%d expected no errors, but got: %v", i, err)
|
||||
}
|
||||
continue
|
||||
}
|
||||
if testcase.failing {
|
||||
t.Fatalf("Test #%d expected to fail but it didn't", i)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestRatelimiting(t *testing.T) {
|
||||
|
||||
// rate limit is 1 per sec
|
||||
c := caddy.NewTestController("dns", `ratelimit 1`)
|
||||
p, err := setupPlugin(c)
|
||||
|
||||
if err != nil {
|
||||
t.Fatal("Failed to initialize the plugin")
|
||||
}
|
||||
|
||||
allowed, err := p.allowRequest("127.0.0.1")
|
||||
|
||||
if err != nil || !allowed {
|
||||
t.Fatal("First request must have been allowed")
|
||||
}
|
||||
|
||||
allowed, err = p.allowRequest("127.0.0.1")
|
||||
|
||||
if err != nil || allowed {
|
||||
t.Fatal("Second request must have been ratelimited")
|
||||
}
|
||||
}
|
||||
|
||||
func TestWhitelist(t *testing.T) {
|
||||
|
||||
// rate limit is 1 per sec
|
||||
c := caddy.NewTestController("dns", `ratelimit 1 { whitelist 127.0.0.2 127.0.0.1 127.0.0.125 }`)
|
||||
p, err := setupPlugin(c)
|
||||
|
||||
if err != nil {
|
||||
t.Fatal("Failed to initialize the plugin")
|
||||
}
|
||||
|
||||
allowed, err := p.allowRequest("127.0.0.1")
|
||||
|
||||
if err != nil || !allowed {
|
||||
t.Fatal("First request must have been allowed")
|
||||
}
|
||||
|
||||
allowed, err = p.allowRequest("127.0.0.1")
|
||||
|
||||
if err != nil || !allowed {
|
||||
t.Fatal("Second request must have been allowed due to whitelist")
|
||||
}
|
||||
}
|
||||
@@ -3,7 +3,6 @@ package refuseany
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"sync"
|
||||
|
||||
"github.com/coredns/coredns/core/dnsserver"
|
||||
"github.com/coredns/coredns/plugin"
|
||||
@@ -15,11 +14,12 @@ import (
|
||||
"golang.org/x/net/context"
|
||||
)
|
||||
|
||||
type Plugin struct {
|
||||
type plug struct {
|
||||
Next plugin.Handler
|
||||
}
|
||||
|
||||
func (p *Plugin) ServeDNS(ctx context.Context, w dns.ResponseWriter, r *dns.Msg) (int, error) {
|
||||
// ServeDNS handles the DNS request and refuses if it's an ANY request
|
||||
func (p *plug) ServeDNS(ctx context.Context, w dns.ResponseWriter, r *dns.Msg) (int, error) {
|
||||
if len(r.Question) != 1 {
|
||||
// google DNS, bind and others do the same
|
||||
return dns.RcodeFormatError, fmt.Errorf("Got DNS request with != 1 questions")
|
||||
@@ -27,8 +27,6 @@ func (p *Plugin) ServeDNS(ctx context.Context, w dns.ResponseWriter, r *dns.Msg)
|
||||
|
||||
q := r.Question[0]
|
||||
if q.Qtype == dns.TypeANY {
|
||||
log.Printf("Got request with type ANY, will respond with NOTIMP\n")
|
||||
|
||||
state := request.Request{W: w, Req: r, Context: ctx}
|
||||
rcode := dns.RcodeNotImplemented
|
||||
|
||||
@@ -41,9 +39,9 @@ func (p *Plugin) ServeDNS(ctx context.Context, w dns.ResponseWriter, r *dns.Msg)
|
||||
return dns.RcodeServerFailure, err
|
||||
}
|
||||
return rcode, nil
|
||||
} else {
|
||||
return plugin.NextOrFailure(p.Name(), p.Next, ctx, w, r)
|
||||
}
|
||||
|
||||
return plugin.NextOrFailure(p.Name(), p.Next, ctx, w, r)
|
||||
}
|
||||
|
||||
func init() {
|
||||
@@ -54,7 +52,7 @@ func init() {
|
||||
}
|
||||
|
||||
func setup(c *caddy.Controller) error {
|
||||
p := &Plugin{}
|
||||
p := &plug{}
|
||||
config := dnsserver.GetConfig(c)
|
||||
|
||||
config.AddPlugin(func(next plugin.Handler) plugin.Handler {
|
||||
@@ -63,22 +61,20 @@ func setup(c *caddy.Controller) error {
|
||||
})
|
||||
|
||||
c.OnStartup(func() error {
|
||||
once.Do(func() {
|
||||
m := dnsserver.GetConfig(c).Handler("prometheus")
|
||||
if m == nil {
|
||||
return
|
||||
}
|
||||
if x, ok := m.(*metrics.Metrics); ok {
|
||||
x.MustRegister(ratelimited)
|
||||
}
|
||||
})
|
||||
m := dnsserver.GetConfig(c).Handler("prometheus")
|
||||
if m == nil {
|
||||
return nil
|
||||
}
|
||||
if x, ok := m.(*metrics.Metrics); ok {
|
||||
x.MustRegister(ratelimited)
|
||||
}
|
||||
return nil
|
||||
})
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func newDnsCounter(name string, help string) prometheus.Counter {
|
||||
func newDNSCounter(name string, help string) prometheus.Counter {
|
||||
return prometheus.NewCounter(prometheus.CounterOpts{
|
||||
Namespace: plugin.Namespace,
|
||||
Subsystem: "refuseany",
|
||||
@@ -88,9 +84,8 @@ func newDnsCounter(name string, help string) prometheus.Counter {
|
||||
}
|
||||
|
||||
var (
|
||||
ratelimited = newDnsCounter("refusedany_total", "Count of ANY requests that have been dropped")
|
||||
ratelimited = newDNSCounter("refusedany_total", "Count of ANY requests that have been dropped")
|
||||
)
|
||||
|
||||
func (d *Plugin) Name() string { return "refuseany" }
|
||||
|
||||
var once sync.Once
|
||||
// Name returns name of the plugin as seen in Corefile and plugin.cfg
|
||||
func (p *plug) Name() string { return "refuseany" }
|
||||
|
||||
36
coredns_plugin/reload.go
Normal file
36
coredns_plugin/reload.go
Normal file
@@ -0,0 +1,36 @@
|
||||
package dnsfilter
|
||||
|
||||
import (
|
||||
"log"
|
||||
|
||||
"github.com/mholt/caddy"
|
||||
)
|
||||
|
||||
var Reload = make(chan bool)
|
||||
|
||||
func hook(event caddy.EventName, info interface{}) error {
|
||||
if event != caddy.InstanceStartupEvent {
|
||||
return nil
|
||||
}
|
||||
|
||||
// this should be an instance. ok to panic if not
|
||||
instance := info.(*caddy.Instance)
|
||||
|
||||
go func() {
|
||||
for range Reload {
|
||||
corefile, err := caddy.LoadCaddyfile(instance.Caddyfile().ServerType())
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
_, err = instance.Restart(corefile)
|
||||
if err != nil {
|
||||
log.Printf("Corefile changed but reload failed: %s", err)
|
||||
continue
|
||||
}
|
||||
// hook will be called again from new instance
|
||||
return
|
||||
}
|
||||
}()
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -1,9 +1,9 @@
|
||||
# AdGuard DNS Go library
|
||||
# AdGuard Home's DNS filtering go library
|
||||
|
||||
Example use:
|
||||
```bash
|
||||
[ -z "$GOPATH" ] && export GOPATH=$HOME/go
|
||||
go get -d github.com/AdguardTeam/AdguardDNS/dnsfilter
|
||||
go get -d github.com/AdguardTeam/AdGuardHome/dnsfilter
|
||||
```
|
||||
|
||||
Create file filter.go
|
||||
@@ -11,7 +11,7 @@ Create file filter.go
|
||||
package main
|
||||
|
||||
import (
|
||||
"github.com/AdguardTeam/AdguardDNS/dnsfilter"
|
||||
"github.com/AdguardTeam/AdGuardHome/dnsfilter"
|
||||
"log"
|
||||
)
|
||||
|
||||
@@ -48,7 +48,7 @@ You can also enable checking against AdGuard's SafeBrowsing:
|
||||
package main
|
||||
|
||||
import (
|
||||
"github.com/AdguardTeam/AdguardDNS/dnsfilter"
|
||||
"github.com/AdguardTeam/AdGuardHome/dnsfilter"
|
||||
"log"
|
||||
)
|
||||
|
||||
|
||||
@@ -9,6 +9,7 @@ import (
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"net"
|
||||
"net/http"
|
||||
"regexp"
|
||||
"strings"
|
||||
@@ -21,16 +22,23 @@ import (
|
||||
)
|
||||
|
||||
const defaultCacheSize = 64 * 1024 // in number of elements
|
||||
const defaultCacheTime time.Duration = 30 * time.Minute
|
||||
const defaultCacheTime = 30 * time.Minute
|
||||
|
||||
const defaultHTTPTimeout time.Duration = 5 * time.Minute
|
||||
const defaultHTTPTimeout = 5 * time.Minute
|
||||
const defaultHTTPMaxIdleConnections = 100
|
||||
|
||||
const defaultSafebrowsingServer = "sb.adtidy.org"
|
||||
const defaultSafebrowsingURL = "http://%s/safebrowsing-lookup-hash.html?prefixes=%s"
|
||||
const defaultParentalURL = "http://pctrl.adguard.com/check-parental-control-hash?prefixes=%s&sensitivity=%d"
|
||||
const defaultParentalServer = "pctrl.adguard.com"
|
||||
const defaultParentalURL = "http://%s/check-parental-control-hash?prefixes=%s&sensitivity=%d"
|
||||
|
||||
// ErrInvalidSyntax is returned by AddRule when the rule is invalid
|
||||
var ErrInvalidSyntax = errors.New("dnsfilter: invalid rule syntax")
|
||||
|
||||
// ErrInvalidSyntax is returned by AddRule when the rule was already added to the filter
|
||||
var ErrAlreadyExists = errors.New("dnsfilter: rule was already added")
|
||||
|
||||
// ErrInvalidParental is returned by EnableParental when sensitivity is not a valid value
|
||||
var ErrInvalidParental = errors.New("dnsfilter: invalid parental sensitivity, must be either 3, 10, 13 or 17")
|
||||
|
||||
const shortcutLength = 6 // used for rule search optimization, 6 hits the sweet spot
|
||||
@@ -38,29 +46,35 @@ const shortcutLength = 6 // used for rule search optimization, 6 hits the sweet
|
||||
const enableFastLookup = true // flag for debugging, must be true in production for faster performance
|
||||
const enableDelayedCompilation = true // flag for debugging, must be true in production for faster performance
|
||||
|
||||
type Config struct {
|
||||
type config struct {
|
||||
parentalServer string
|
||||
parentalSensitivity int // must be either 3, 10, 13 or 17
|
||||
parentalEnabled bool
|
||||
safeSearchEnabled bool
|
||||
safeBrowsingEnabled bool
|
||||
safeBrowsingServer string
|
||||
parentalEnabled bool
|
||||
parentalSensitivity int // must be either 3, 10, 13 or 17
|
||||
}
|
||||
|
||||
type Rule struct {
|
||||
type rule struct {
|
||||
text string // text without @@ decorators or $ options
|
||||
shortcut string // for speeding up lookup
|
||||
originalText string // original text for reporting back to applications
|
||||
ip net.IP // IP address (for the case when we're matching a hosts file)
|
||||
|
||||
// options
|
||||
options []string // optional options after $
|
||||
|
||||
// parsed options
|
||||
apps []string
|
||||
isWhitelist bool
|
||||
isImportant bool
|
||||
apps []string
|
||||
|
||||
// user-supplied data
|
||||
listID uint32
|
||||
listID int64
|
||||
|
||||
// suffix matching
|
||||
isSuffix bool
|
||||
suffix string
|
||||
|
||||
// compiled regexp
|
||||
compiled *regexp.Regexp
|
||||
@@ -68,6 +82,7 @@ type Rule struct {
|
||||
sync.RWMutex
|
||||
}
|
||||
|
||||
// LookupStats store stats collected during safebrowsing or parental checks
|
||||
type LookupStats struct {
|
||||
Requests uint64 // number of HTTP requests that were sent
|
||||
CacheHits uint64 // number of lookups that didn't need HTTP requests
|
||||
@@ -75,6 +90,7 @@ type LookupStats struct {
|
||||
PendingMax int64 // maximum number of pending HTTP requests
|
||||
}
|
||||
|
||||
// Stats store LookupStats for both safebrowsing and parental
|
||||
type Stats struct {
|
||||
Safebrowsing LookupStats
|
||||
Parental LookupStats
|
||||
@@ -82,7 +98,7 @@ type Stats struct {
|
||||
|
||||
// Dnsfilter holds added rules and performs hostname matches against the rules
|
||||
type Dnsfilter struct {
|
||||
storage map[string]*Rule // rule storage, not used for matching, needs to be key->value
|
||||
storage map[string]bool // rule storage, not used for matching, just for filtering out duplicates
|
||||
storageMutex sync.RWMutex
|
||||
|
||||
// rules are checked against these lists in the order defined here
|
||||
@@ -94,12 +110,12 @@ type Dnsfilter struct {
|
||||
client http.Client // handle for http client -- single instance as recommended by docs
|
||||
transport *http.Transport // handle for http transport used by http client
|
||||
|
||||
config Config
|
||||
config config
|
||||
}
|
||||
|
||||
//go:generate stringer -type=Reason
|
||||
|
||||
// filtered/notfiltered reason
|
||||
// Reason holds an enum detailing why it was filtered or not filtered
|
||||
type Reason int
|
||||
|
||||
const (
|
||||
@@ -119,27 +135,31 @@ const (
|
||||
// these variables need to survive coredns reload
|
||||
var (
|
||||
stats Stats
|
||||
safebrowsingCache = gcache.New(defaultCacheSize).LRU().Expiration(defaultCacheTime).Build()
|
||||
parentalCache = gcache.New(defaultCacheSize).LRU().Expiration(defaultCacheTime).Build()
|
||||
safebrowsingCache gcache.Cache
|
||||
parentalCache gcache.Cache
|
||||
)
|
||||
|
||||
// search result
|
||||
// Result holds state of hostname check
|
||||
type Result struct {
|
||||
IsFiltered bool
|
||||
Reason Reason
|
||||
Rule string
|
||||
IsFiltered bool `json:",omitempty"` // True if the host name is filtered
|
||||
Reason Reason `json:",omitempty"` // Reason for blocking / unblocking
|
||||
Rule string `json:",omitempty"` // Original rule text
|
||||
Ip net.IP `json:",omitempty"` // Not nil only in the case of a hosts file syntax
|
||||
FilterID int64 `json:",omitempty"` // Filter ID the rule belongs to
|
||||
}
|
||||
|
||||
// Matched can be used to see if any match at all was found, no matter filtered or not
|
||||
func (r Reason) Matched() bool {
|
||||
return r != NotFilteredNotFound
|
||||
}
|
||||
|
||||
// CheckHost tries to match host against rules, then safebrowsing and parental if they are enabled
|
||||
func (d *Dnsfilter) CheckHost(host string) (Result, error) {
|
||||
// sometimes DNS clients will try to resolve ".", which in turns transforms into "" when it reaches here
|
||||
// sometimes DNS clients will try to resolve ".", which is a request to get root servers
|
||||
if host == "" {
|
||||
return Result{Reason: FilteredInvalid}, nil
|
||||
return Result{Reason: NotFilteredNotFound}, nil
|
||||
}
|
||||
host = strings.ToLower(host)
|
||||
|
||||
// try filter lists first
|
||||
result, err := d.matchHost(host)
|
||||
@@ -185,30 +205,43 @@ func (d *Dnsfilter) CheckHost(host string) (Result, error) {
|
||||
//
|
||||
|
||||
type rulesTable struct {
|
||||
rulesByShortcut map[string][]*Rule
|
||||
rulesLeftovers []*Rule
|
||||
rulesByHost map[string]*rule
|
||||
rulesByShortcut map[string][]*rule
|
||||
rulesLeftovers []*rule
|
||||
sync.RWMutex
|
||||
}
|
||||
|
||||
func newRulesTable() *rulesTable {
|
||||
return &rulesTable{
|
||||
rulesByShortcut: make(map[string][]*Rule),
|
||||
rulesLeftovers: make([]*Rule, 0),
|
||||
rulesByHost: make(map[string]*rule),
|
||||
rulesByShortcut: make(map[string][]*rule),
|
||||
rulesLeftovers: make([]*rule, 0),
|
||||
}
|
||||
}
|
||||
|
||||
func (r *rulesTable) Add(rule *Rule) {
|
||||
func (r *rulesTable) Add(rule *rule) {
|
||||
r.Lock()
|
||||
if len(rule.shortcut) == shortcutLength && enableFastLookup {
|
||||
|
||||
if rule.ip != nil {
|
||||
// Hosts syntax
|
||||
r.rulesByHost[rule.text] = rule
|
||||
} else if //noinspection GoBoolExpressions
|
||||
len(rule.shortcut) == shortcutLength && enableFastLookup {
|
||||
|
||||
// Adblock syntax with a shortcut
|
||||
r.rulesByShortcut[rule.shortcut] = append(r.rulesByShortcut[rule.shortcut], rule)
|
||||
} else {
|
||||
|
||||
// Adblock syntax -- too short to have a shortcut
|
||||
r.rulesLeftovers = append(r.rulesLeftovers, rule)
|
||||
}
|
||||
r.Unlock()
|
||||
}
|
||||
|
||||
func (r *rulesTable) matchByHost(host string) (Result, error) {
|
||||
res, err := r.searchShortcuts(host)
|
||||
|
||||
// First: examine the hosts-syntax rules
|
||||
res, err := r.searchByHost(host)
|
||||
if err != nil {
|
||||
return res, err
|
||||
}
|
||||
@@ -216,6 +249,16 @@ func (r *rulesTable) matchByHost(host string) (Result, error) {
|
||||
return res, nil
|
||||
}
|
||||
|
||||
// Second: examine the adblock-syntax rules with shortcuts
|
||||
res, err = r.searchShortcuts(host)
|
||||
if err != nil {
|
||||
return res, err
|
||||
}
|
||||
if res.Reason.Matched() {
|
||||
return res, nil
|
||||
}
|
||||
|
||||
// Third: examine the others
|
||||
res, err = r.searchLeftovers(host)
|
||||
if err != nil {
|
||||
return res, err
|
||||
@@ -227,6 +270,17 @@ func (r *rulesTable) matchByHost(host string) (Result, error) {
|
||||
return Result{}, nil
|
||||
}
|
||||
|
||||
func (r *rulesTable) searchByHost(host string) (Result, error) {
|
||||
|
||||
rule, ok := r.rulesByHost[host]
|
||||
|
||||
if ok {
|
||||
return rule.match(host)
|
||||
}
|
||||
|
||||
return Result{}, nil
|
||||
}
|
||||
|
||||
func (r *rulesTable) searchShortcuts(host string) (Result, error) {
|
||||
// check in shortcuts first
|
||||
for i := 0; i < len(host); i++ {
|
||||
@@ -292,7 +346,7 @@ func findOptionIndex(text string) int {
|
||||
return -1
|
||||
}
|
||||
|
||||
func (rule *Rule) extractOptions() error {
|
||||
func (rule *rule) extractOptions() error {
|
||||
optIndex := findOptionIndex(rule.text)
|
||||
if optIndex == 0 { // starts with $
|
||||
return ErrInvalidSyntax
|
||||
@@ -330,7 +384,7 @@ func (rule *Rule) extractOptions() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (rule *Rule) parseOptions() error {
|
||||
func (rule *rule) parseOptions() error {
|
||||
err := rule.extractOptions()
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -351,7 +405,7 @@ func (rule *Rule) parseOptions() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (rule *Rule) extractShortcut() {
|
||||
func (rule *rule) extractShortcut() {
|
||||
// regex rules have no shortcuts
|
||||
if rule.text[0] == '/' && rule.text[len(rule.text)-1] == '/' {
|
||||
return
|
||||
@@ -376,14 +430,23 @@ func (rule *Rule) extractShortcut() {
|
||||
rule.shortcut = strings.ToLower(longestField)
|
||||
}
|
||||
|
||||
func (rule *Rule) compile() error {
|
||||
func (rule *rule) compile() error {
|
||||
rule.RLock()
|
||||
isCompiled := rule.compiled != nil
|
||||
isCompiled := rule.isSuffix || rule.compiled != nil
|
||||
rule.RUnlock()
|
||||
if isCompiled {
|
||||
return nil
|
||||
}
|
||||
|
||||
isSuffix, suffix := getSuffix(rule.text)
|
||||
if isSuffix {
|
||||
rule.Lock()
|
||||
rule.isSuffix = isSuffix
|
||||
rule.suffix = suffix
|
||||
rule.Unlock()
|
||||
return nil
|
||||
}
|
||||
|
||||
expr, err := ruleToRegexp(rule.text)
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -401,23 +464,46 @@ func (rule *Rule) compile() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (rule *Rule) match(host string) (Result, error) {
|
||||
// Checks if the rule matches the specified host and returns a corresponding Result object
|
||||
func (rule *rule) match(host string) (Result, error) {
|
||||
res := Result{}
|
||||
|
||||
if rule.ip != nil && rule.text == host {
|
||||
// This is a hosts-syntax rule -- just check that the hostname matches and return the result
|
||||
return Result{
|
||||
IsFiltered: true,
|
||||
Reason: FilteredBlackList,
|
||||
Rule: rule.originalText,
|
||||
Ip: rule.ip,
|
||||
FilterID: rule.listID,
|
||||
}, nil
|
||||
}
|
||||
|
||||
err := rule.compile()
|
||||
if err != nil {
|
||||
return res, err
|
||||
}
|
||||
rule.RLock()
|
||||
matched := rule.compiled.MatchString(host)
|
||||
matched := false
|
||||
if rule.isSuffix {
|
||||
if host == rule.suffix {
|
||||
matched = true
|
||||
} else if strings.HasSuffix(host, "."+rule.suffix) {
|
||||
matched = true
|
||||
}
|
||||
} else {
|
||||
matched = rule.compiled.MatchString(host)
|
||||
}
|
||||
rule.RUnlock()
|
||||
if matched {
|
||||
res.Reason = FilteredBlackList
|
||||
res.IsFiltered = true
|
||||
res.FilterID = rule.listID
|
||||
res.Rule = rule.originalText
|
||||
if rule.isWhitelist {
|
||||
res.Reason = NotFilteredWhiteList
|
||||
res.IsFiltered = false
|
||||
}
|
||||
res.Rule = rule.text
|
||||
}
|
||||
return res, nil
|
||||
}
|
||||
@@ -439,7 +525,7 @@ func getCachedReason(cache gcache.Cache, host string) (result Result, isFound bo
|
||||
|
||||
// since it can be something else, validate that it belongs to proper type
|
||||
cachedValue, ok := rawValue.(Result)
|
||||
if ok == false {
|
||||
if !ok {
|
||||
// this is not our type -- error
|
||||
text := "SHOULD NOT HAPPEN: entry with invalid type was found in lookup cache"
|
||||
log.Println(text)
|
||||
@@ -455,7 +541,7 @@ func hostnameToHashParam(host string, addslash bool) (string, map[string]bool) {
|
||||
var hashparam bytes.Buffer
|
||||
hashes := map[string]bool{}
|
||||
tld, icann := publicsuffix.PublicSuffix(host)
|
||||
if icann == false {
|
||||
if !icann {
|
||||
// private suffixes like cloudfront.net
|
||||
tld = ""
|
||||
}
|
||||
@@ -487,6 +573,10 @@ func hostnameToHashParam(host string, addslash bool) (string, map[string]bool) {
|
||||
}
|
||||
|
||||
func (d *Dnsfilter) checkSafeBrowsing(host string) (Result, error) {
|
||||
// prevent recursion -- checking the host of safebrowsing server makes no sense
|
||||
if host == d.config.safeBrowsingServer {
|
||||
return Result{}, nil
|
||||
}
|
||||
format := func(hashparam string) string {
|
||||
url := fmt.Sprintf(defaultSafebrowsingURL, d.config.safeBrowsingServer, hashparam)
|
||||
return url
|
||||
@@ -516,21 +606,29 @@ func (d *Dnsfilter) checkSafeBrowsing(host string) (Result, error) {
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
if safebrowsingCache == nil {
|
||||
safebrowsingCache = gcache.New(defaultCacheSize).LRU().Expiration(defaultCacheTime).Build()
|
||||
}
|
||||
result, err := d.lookupCommon(host, &stats.Safebrowsing, safebrowsingCache, true, format, handleBody)
|
||||
return result, err
|
||||
}
|
||||
|
||||
func (d *Dnsfilter) checkParental(host string) (Result, error) {
|
||||
format2 := func(hashparam string) string {
|
||||
url := fmt.Sprintf(defaultParentalURL, hashparam, d.config.parentalSensitivity)
|
||||
// prevent recursion -- checking the host of parental safety server makes no sense
|
||||
if host == d.config.parentalServer {
|
||||
return Result{}, nil
|
||||
}
|
||||
format := func(hashparam string) string {
|
||||
url := fmt.Sprintf(defaultParentalURL, d.config.parentalServer, hashparam, d.config.parentalSensitivity)
|
||||
return url
|
||||
}
|
||||
handleBody2 := func(body []byte, hashes map[string]bool) (Result, error) {
|
||||
handleBody := func(body []byte, hashes map[string]bool) (Result, error) {
|
||||
// parse json
|
||||
var m []struct {
|
||||
Blocked bool `json:"blocked"`
|
||||
ClientTTL int `json:"clientTtl"`
|
||||
Reason string `json:"reason"`
|
||||
Hash string `json:"hash"`
|
||||
}
|
||||
err := json.Unmarshal(body, &m)
|
||||
if err != nil {
|
||||
@@ -542,6 +640,9 @@ func (d *Dnsfilter) checkParental(host string) (Result, error) {
|
||||
result := Result{}
|
||||
|
||||
for i := range m {
|
||||
if !hashes[m[i].Hash] {
|
||||
continue
|
||||
}
|
||||
if m[i].Blocked {
|
||||
result.IsFiltered = true
|
||||
result.Reason = FilteredParental
|
||||
@@ -551,7 +652,10 @@ func (d *Dnsfilter) checkParental(host string) (Result, error) {
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
result, err := d.lookupCommon(host, &stats.Parental, parentalCache, false, format2, handleBody2)
|
||||
if parentalCache == nil {
|
||||
parentalCache = gcache.New(defaultCacheSize).LRU().Expiration(defaultCacheTime).Build()
|
||||
}
|
||||
result, err := d.lookupCommon(host, &stats.Parental, parentalCache, false, format, handleBody)
|
||||
return result, err
|
||||
}
|
||||
|
||||
@@ -563,7 +667,7 @@ func (d *Dnsfilter) lookupCommon(host string, lookupstats *LookupStats, cache gc
|
||||
// check cache
|
||||
cachedValue, isFound, err := getCachedReason(cache, host)
|
||||
if isFound {
|
||||
atomic.AddUint64(&stats.Safebrowsing.CacheHits, 1)
|
||||
atomic.AddUint64(&lookupstats.CacheHits, 1)
|
||||
return cachedValue, nil
|
||||
}
|
||||
if err != nil {
|
||||
@@ -601,7 +705,10 @@ func (d *Dnsfilter) lookupCommon(host string, lookupstats *LookupStats, cache gc
|
||||
switch {
|
||||
case resp.StatusCode == 204:
|
||||
// empty result, save cache
|
||||
cache.Set(host, Result{})
|
||||
err = cache.Set(host, Result{})
|
||||
if err != nil {
|
||||
return Result{}, err
|
||||
}
|
||||
return Result{}, nil
|
||||
case resp.StatusCode != 200:
|
||||
// error, don't save cache
|
||||
@@ -614,7 +721,10 @@ func (d *Dnsfilter) lookupCommon(host string, lookupstats *LookupStats, cache gc
|
||||
return Result{}, err
|
||||
}
|
||||
|
||||
cache.Set(host, result)
|
||||
err = cache.Set(host, result)
|
||||
if err != nil {
|
||||
return Result{}, err
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
@@ -623,27 +733,34 @@ func (d *Dnsfilter) lookupCommon(host string, lookupstats *LookupStats, cache gc
|
||||
//
|
||||
|
||||
// AddRule adds a rule, checking if it is a valid rule first and if it wasn't added already
|
||||
func (d *Dnsfilter) AddRule(input string, filterListID uint32) error {
|
||||
func (d *Dnsfilter) AddRule(input string, filterListID int64) error {
|
||||
input = strings.TrimSpace(input)
|
||||
d.storageMutex.RLock()
|
||||
_, exists := d.storage[input]
|
||||
d.storageMutex.RUnlock()
|
||||
if exists {
|
||||
// already added
|
||||
return ErrInvalidSyntax
|
||||
return ErrAlreadyExists
|
||||
}
|
||||
|
||||
if !isValidRule(input) {
|
||||
return ErrInvalidSyntax
|
||||
}
|
||||
|
||||
rule := Rule{
|
||||
// First, check if this is a hosts-syntax rule
|
||||
if d.parseEtcHosts(input, filterListID) {
|
||||
// This is a valid hosts-syntax rule, no need for further parsing
|
||||
return nil
|
||||
}
|
||||
|
||||
// Start parsing the rule
|
||||
rule := rule{
|
||||
text: input, // will be modified
|
||||
originalText: input,
|
||||
listID: filterListID,
|
||||
}
|
||||
|
||||
// mark rule as whitelist if it starts with @@
|
||||
// Mark rule as whitelist if it starts with @@
|
||||
if strings.HasPrefix(rule.text, "@@") {
|
||||
rule.isWhitelist = true
|
||||
rule.text = rule.text[2:]
|
||||
@@ -656,6 +773,7 @@ func (d *Dnsfilter) AddRule(input string, filterListID uint32) error {
|
||||
|
||||
rule.extractShortcut()
|
||||
|
||||
//noinspection GoBoolExpressions
|
||||
if !enableDelayedCompilation {
|
||||
err := rule.compile()
|
||||
if err != nil {
|
||||
@@ -671,12 +789,44 @@ func (d *Dnsfilter) AddRule(input string, filterListID uint32) error {
|
||||
}
|
||||
|
||||
d.storageMutex.Lock()
|
||||
d.storage[input] = &rule
|
||||
d.storage[input] = true
|
||||
d.storageMutex.Unlock()
|
||||
destination.Add(&rule)
|
||||
return nil
|
||||
}
|
||||
|
||||
// Parses the hosts-syntax rules. Returns false if the input string is not of hosts-syntax.
|
||||
func (d *Dnsfilter) parseEtcHosts(input string, filterListID int64) bool {
|
||||
// Strip the trailing comment
|
||||
ruleText := input
|
||||
if pos := strings.IndexByte(ruleText, '#'); pos != -1 {
|
||||
ruleText = ruleText[0:pos]
|
||||
}
|
||||
fields := strings.Fields(ruleText)
|
||||
if len(fields) < 2 {
|
||||
return false
|
||||
}
|
||||
addr := net.ParseIP(fields[0])
|
||||
if addr == nil {
|
||||
return false
|
||||
}
|
||||
|
||||
d.storageMutex.Lock()
|
||||
d.storage[input] = true
|
||||
d.storageMutex.Unlock()
|
||||
|
||||
for _, host := range fields[1:] {
|
||||
rule := rule{
|
||||
text: host,
|
||||
originalText: input,
|
||||
listID: filterListID,
|
||||
ip: addr,
|
||||
}
|
||||
d.blackList.Add(&rule)
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// matchHost is a low-level way to check only if hostname is filtered by rules, skipping expensive safebrowsing and parental lookups
|
||||
func (d *Dnsfilter) matchHost(host string) (Result, error) {
|
||||
lists := []*rulesTable{
|
||||
@@ -701,10 +851,11 @@ func (d *Dnsfilter) matchHost(host string) (Result, error) {
|
||||
// lifecycle helper functions
|
||||
//
|
||||
|
||||
// New creates properly initialized DNS Filter that is ready to be used
|
||||
func New() *Dnsfilter {
|
||||
d := new(Dnsfilter)
|
||||
|
||||
d.storage = make(map[string]*Rule)
|
||||
d.storage = make(map[string]bool)
|
||||
d.important = newRulesTable()
|
||||
d.whiteList = newRulesTable()
|
||||
d.blackList = newRulesTable()
|
||||
@@ -723,21 +874,29 @@ func New() *Dnsfilter {
|
||||
Timeout: defaultHTTPTimeout,
|
||||
}
|
||||
d.config.safeBrowsingServer = defaultSafebrowsingServer
|
||||
d.config.parentalServer = defaultParentalServer
|
||||
|
||||
return d
|
||||
}
|
||||
|
||||
// Destroy is optional if you want to tidy up goroutines without waiting for them to die off
|
||||
// right now it closes idle HTTP connections if there are any
|
||||
func (d *Dnsfilter) Destroy() {
|
||||
d.transport.CloseIdleConnections()
|
||||
if d != nil && d.transport != nil {
|
||||
d.transport.CloseIdleConnections()
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
// config manipulation helpers
|
||||
//
|
||||
|
||||
// EnableSafeBrowsing turns on checking hostnames in malware/phishing database
|
||||
func (d *Dnsfilter) EnableSafeBrowsing() {
|
||||
d.config.safeBrowsingEnabled = true
|
||||
}
|
||||
|
||||
// EnableParental turns on checking hostnames for containing adult content
|
||||
func (d *Dnsfilter) EnableParental(sensitivity int) error {
|
||||
switch sensitivity {
|
||||
case 3, 10, 13, 17:
|
||||
@@ -749,10 +908,13 @@ func (d *Dnsfilter) EnableParental(sensitivity int) error {
|
||||
}
|
||||
}
|
||||
|
||||
// EnableSafeSearch turns on enforcing safesearch in search engines
|
||||
// only used in coredns plugin and requires caller to use SafeSearchDomain()
|
||||
func (d *Dnsfilter) EnableSafeSearch() {
|
||||
d.config.safeSearchEnabled = true
|
||||
}
|
||||
|
||||
// SetSafeBrowsingServer lets you optionally change hostname of safesearch lookup
|
||||
func (d *Dnsfilter) SetSafeBrowsingServer(host string) {
|
||||
if len(host) == 0 {
|
||||
d.config.safeBrowsingServer = defaultSafebrowsingServer
|
||||
@@ -761,38 +923,35 @@ func (d *Dnsfilter) SetSafeBrowsingServer(host string) {
|
||||
}
|
||||
}
|
||||
|
||||
// SetHTTPTimeout lets you optionally change timeout during lookups
|
||||
func (d *Dnsfilter) SetHTTPTimeout(t time.Duration) {
|
||||
d.client.Timeout = t
|
||||
}
|
||||
|
||||
// ResetHTTPTimeout resets lookup timeouts
|
||||
func (d *Dnsfilter) ResetHTTPTimeout() {
|
||||
d.client.Timeout = defaultHTTPTimeout
|
||||
}
|
||||
|
||||
// SafeSearchDomain returns replacement address for search engine
|
||||
func (d *Dnsfilter) SafeSearchDomain(host string) (string, bool) {
|
||||
if d.config.safeSearchEnabled == false {
|
||||
return "", false
|
||||
if d.config.safeSearchEnabled {
|
||||
val, ok := safeSearchDomains[host]
|
||||
return val, ok
|
||||
}
|
||||
val, ok := safeSearchDomains[host]
|
||||
return val, ok
|
||||
return "", false
|
||||
}
|
||||
|
||||
//
|
||||
// stats
|
||||
//
|
||||
|
||||
// GetStats return dns filtering stats since startup
|
||||
func (d *Dnsfilter) GetStats() Stats {
|
||||
return stats
|
||||
}
|
||||
|
||||
// Count returns number of rules added to filter
|
||||
func (d *Dnsfilter) Count() int {
|
||||
return len(d.storage)
|
||||
}
|
||||
|
||||
//
|
||||
// cache control, right now needed only for tests
|
||||
//
|
||||
func purgeCaches() {
|
||||
safebrowsingCache.Purge()
|
||||
parentalCache.Purge()
|
||||
}
|
||||
|
||||
@@ -1,8 +1,14 @@
|
||||
package dnsfilter
|
||||
|
||||
import (
|
||||
"archive/zip"
|
||||
"bytes"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"path"
|
||||
"runtime/pprof"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
@@ -11,9 +17,175 @@ import (
|
||||
"os"
|
||||
"runtime"
|
||||
|
||||
"github.com/shirou/gopsutil/process"
|
||||
"go.uber.org/goleak"
|
||||
)
|
||||
|
||||
// first in file because it must be run first
|
||||
func TestLotsOfRulesMemoryUsage(t *testing.T) {
|
||||
start := getRSS()
|
||||
trace("RSS before loading rules - %d kB\n", start/1024)
|
||||
dumpMemProfile(_Func() + "1.pprof")
|
||||
|
||||
d := NewForTest()
|
||||
defer d.Destroy()
|
||||
err := loadTestRules(d)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
|
||||
afterLoad := getRSS()
|
||||
trace("RSS after loading rules - %d kB (%d kB diff)\n", afterLoad/1024, (afterLoad-start)/1024)
|
||||
dumpMemProfile(_Func() + "2.pprof")
|
||||
|
||||
tests := []struct {
|
||||
host string
|
||||
match bool
|
||||
}{
|
||||
{"asdasdasd_adsajdasda_asdasdjashdkasdasdasdasd_adsajdasda_asdasdjashdkasd.thisistesthost.com", false},
|
||||
{"asdasdasd_adsajdasda_asdasdjashdkasdasdasdasd_adsajdasda_asdasdjashdkasd.ad.doubleclick.net", true},
|
||||
}
|
||||
for _, testcase := range tests {
|
||||
ret, err := d.CheckHost(testcase.host)
|
||||
if err != nil {
|
||||
t.Errorf("Error while matching host %s: %s", testcase.host, err)
|
||||
}
|
||||
if !ret.IsFiltered && ret.IsFiltered != testcase.match {
|
||||
t.Errorf("Expected hostname %s to not match", testcase.host)
|
||||
}
|
||||
if ret.IsFiltered && ret.IsFiltered != testcase.match {
|
||||
t.Errorf("Expected hostname %s to match", testcase.host)
|
||||
}
|
||||
}
|
||||
afterMatch := getRSS()
|
||||
trace("RSS after matching - %d kB (%d kB diff)\n", afterMatch/1024, (afterMatch-afterLoad)/1024)
|
||||
dumpMemProfile(_Func() + "3.pprof")
|
||||
}
|
||||
|
||||
func getRSS() uint64 {
|
||||
proc, err := process.NewProcess(int32(os.Getpid()))
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
minfo, err := proc.MemoryInfo()
|
||||
return minfo.RSS
|
||||
}
|
||||
|
||||
func dumpMemProfile(name string) {
|
||||
runtime.GC()
|
||||
f, err := os.Create(name)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
defer f.Close()
|
||||
runtime.GC() // update the stats before writing them
|
||||
err = pprof.WriteHeapProfile(f)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
|
||||
const topHostsFilename = "../tests/top-1m.csv"
|
||||
|
||||
func fetchTopHostsFromNet() {
|
||||
trace("Fetching top hosts from network")
|
||||
resp, err := http.Get("http://s3-us-west-1.amazonaws.com/umbrella-static/top-1m.csv.zip")
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
trace("Reading zipfile body")
|
||||
zipfile, err := ioutil.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
trace("Opening zipfile")
|
||||
r, err := zip.NewReader(bytes.NewReader(zipfile), int64(len(zipfile)))
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
if len(r.File) != 1 {
|
||||
panic(fmt.Errorf("zipfile must have only one entry: %+v", r))
|
||||
}
|
||||
f := r.File[0]
|
||||
trace("Unpacking file %s from zipfile", f.Name)
|
||||
rc, err := f.Open()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
trace("Reading file %s contents", f.Name)
|
||||
body, err := ioutil.ReadAll(rc)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
rc.Close()
|
||||
|
||||
trace("Writing file %s contents to disk", f.Name)
|
||||
err = ioutil.WriteFile(topHostsFilename+".tmp", body, 0644)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
err = os.Rename(topHostsFilename+".tmp", topHostsFilename)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
|
||||
func getTopHosts() {
|
||||
// if file doesn't exist, fetch it
|
||||
if _, err := os.Stat(topHostsFilename); os.IsNotExist(err) {
|
||||
// file does not exist, fetch it
|
||||
fetchTopHostsFromNet()
|
||||
}
|
||||
}
|
||||
|
||||
func TestLotsOfRulesLotsOfHostsMemoryUsage(t *testing.T) {
|
||||
start := getRSS()
|
||||
trace("RSS before loading rules - %d kB\n", start/1024)
|
||||
dumpMemProfile(_Func() + "1.pprof")
|
||||
|
||||
d := NewForTest()
|
||||
defer d.Destroy()
|
||||
mustLoadTestRules(d)
|
||||
trace("Have %d rules", d.Count())
|
||||
|
||||
afterLoad := getRSS()
|
||||
trace("RSS after loading rules - %d kB (%d kB diff)\n", afterLoad/1024, (afterLoad-start)/1024)
|
||||
dumpMemProfile(_Func() + "2.pprof")
|
||||
|
||||
getTopHosts()
|
||||
hostnames, err := os.Open(topHostsFilename)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer hostnames.Close()
|
||||
afterHosts := getRSS()
|
||||
trace("RSS after loading hosts - %d kB (%d kB diff)\n", afterHosts/1024, (afterHosts-afterLoad)/1024)
|
||||
dumpMemProfile(_Func() + "2.pprof")
|
||||
|
||||
{
|
||||
scanner := bufio.NewScanner(hostnames)
|
||||
for scanner.Scan() {
|
||||
line := scanner.Text()
|
||||
records := strings.Split(line, ",")
|
||||
ret, err := d.CheckHost(records[1] + "." + records[1])
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
if ret.Reason.Matched() {
|
||||
// log.Printf("host \"%s\" mathed. Rule \"%s\", reason: %v", host, ret.Rule, ret.Reason)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
afterMatch := getRSS()
|
||||
trace("RSS after matching - %d kB (%d kB diff)\n", afterMatch/1024, (afterMatch-afterLoad)/1024)
|
||||
dumpMemProfile(_Func() + "3.pprof")
|
||||
}
|
||||
|
||||
func TestRuleToRegexp(t *testing.T) {
|
||||
tests := []struct {
|
||||
rule string
|
||||
@@ -23,7 +195,7 @@ func TestRuleToRegexp(t *testing.T) {
|
||||
{"/doubleclick/", "doubleclick", nil},
|
||||
{"/", "", ErrInvalidSyntax},
|
||||
{`|double*?.+[]|(){}#$\|`, `^double.*\?\.\+\[\]\|\(\)\{\}\#\$\\$`, nil},
|
||||
{`||doubleclick.net^`, `^([a-z0-9-_.]+\.)?doubleclick\.net([^ a-zA-Z0-9.%]|$)`, nil},
|
||||
{`||doubleclick.net^`, `(?:^|\.)doubleclick\.net$`, nil},
|
||||
}
|
||||
for _, testcase := range tests {
|
||||
converted, err := ruleToRegexp(testcase.rule)
|
||||
@@ -36,6 +208,38 @@ func TestRuleToRegexp(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestSuffixRule(t *testing.T) {
|
||||
for _, testcase := range []struct {
|
||||
rule string
|
||||
isSuffix bool
|
||||
suffix string
|
||||
}{
|
||||
{`||doubleclick.net^`, true, `doubleclick.net`}, // entire string or subdomain match
|
||||
{`||doubleclick.net|`, true, `doubleclick.net`}, // entire string or subdomain match
|
||||
{`|doubleclick.net^`, false, ``}, // TODO: ends with doubleclick.net
|
||||
{`*doubleclick.net^`, false, ``}, // TODO: ends with doubleclick.net
|
||||
{`doubleclick.net^`, false, ``}, // TODO: ends with doubleclick.net
|
||||
{`|*doubleclick.net^`, false, ``}, // TODO: ends with doubleclick.net
|
||||
{`||*doubleclick.net^`, false, ``}, // TODO: ends with doubleclick.net
|
||||
{`||*doubleclick.net|`, false, ``}, // TODO: ends with doubleclick.net
|
||||
{`||*doublec*lick.net^`, false, ``}, // has a wildcard inside, has to be regexp
|
||||
{`||*doublec|lick.net^`, false, ``}, // has a special symbol inside, has to be regexp
|
||||
{`/abracadabra/`, false, ``}, // regexp, not anchored
|
||||
{`/abracadabra$/`, false, ``}, // TODO: simplify simple suffix regexes
|
||||
} {
|
||||
isSuffix, suffix := getSuffix(testcase.rule)
|
||||
if testcase.isSuffix != isSuffix {
|
||||
t.Errorf("Results do not match for \"%s\": got %v expected %v", testcase.rule, isSuffix, testcase.isSuffix)
|
||||
continue
|
||||
}
|
||||
if testcase.isSuffix && testcase.suffix != suffix {
|
||||
t.Errorf("Result suffix does not match for \"%s\": got \"%s\" expected \"%s\"", testcase.rule, suffix, testcase.suffix)
|
||||
continue
|
||||
}
|
||||
// trace("\"%s\": %v: %s", testcase.rule, isSuffix, suffix)
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
// helper functions
|
||||
//
|
||||
@@ -57,7 +261,7 @@ func (d *Dnsfilter) checkAddRule(t *testing.T, rule string) {
|
||||
func (d *Dnsfilter) checkAddRuleFail(t *testing.T, rule string) {
|
||||
t.Helper()
|
||||
err := d.AddRule(rule, 0)
|
||||
if err == ErrInvalidSyntax {
|
||||
if err == ErrInvalidSyntax || err == ErrAlreadyExists {
|
||||
return
|
||||
}
|
||||
if err != nil {
|
||||
@@ -77,6 +281,20 @@ func (d *Dnsfilter) checkMatch(t *testing.T, hostname string) {
|
||||
}
|
||||
}
|
||||
|
||||
func (d *Dnsfilter) checkMatchIp(t *testing.T, hostname string, ip string) {
|
||||
t.Helper()
|
||||
ret, err := d.CheckHost(hostname)
|
||||
if err != nil {
|
||||
t.Errorf("Error while matching host %s: %s", hostname, err)
|
||||
}
|
||||
if !ret.IsFiltered {
|
||||
t.Errorf("Expected hostname %s to match", hostname)
|
||||
}
|
||||
if ret.Ip == nil || ret.Ip.String() != ip {
|
||||
t.Errorf("Expected ip %s to match, actual: %v", ip, ret.Ip)
|
||||
}
|
||||
}
|
||||
|
||||
func (d *Dnsfilter) checkMatchEmpty(t *testing.T, hostname string) {
|
||||
t.Helper()
|
||||
ret, err := d.CheckHost(hostname)
|
||||
@@ -100,7 +318,7 @@ func loadTestRules(d *Dnsfilter) error {
|
||||
for scanner.Scan() {
|
||||
rule := scanner.Text()
|
||||
err = d.AddRule(rule, 0)
|
||||
if err == ErrInvalidSyntax {
|
||||
if err == ErrInvalidSyntax || err == ErrAlreadyExists {
|
||||
continue
|
||||
}
|
||||
if err != nil {
|
||||
@@ -112,6 +330,13 @@ func loadTestRules(d *Dnsfilter) error {
|
||||
return err
|
||||
}
|
||||
|
||||
func mustLoadTestRules(d *Dnsfilter) {
|
||||
err := loadTestRules(d)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
|
||||
func NewForTest() *Dnsfilter {
|
||||
d := New()
|
||||
purgeCaches()
|
||||
@@ -124,7 +349,9 @@ func NewForTest() *Dnsfilter {
|
||||
func TestSanityCheck(t *testing.T) {
|
||||
d := NewForTest()
|
||||
defer d.Destroy()
|
||||
|
||||
d.checkAddRule(t, "||doubleclick.net^")
|
||||
d.checkMatch(t, "doubleclick.net")
|
||||
d.checkMatch(t, "www.doubleclick.net")
|
||||
d.checkMatchEmpty(t, "nodoubleclick.net")
|
||||
d.checkMatchEmpty(t, "doubleclick.net.ru")
|
||||
@@ -132,6 +359,86 @@ func TestSanityCheck(t *testing.T) {
|
||||
d.checkAddRuleFail(t, "lkfaojewhoawehfwacoefawr$@#$@3413841384")
|
||||
}
|
||||
|
||||
func TestEtcHostsMatching(t *testing.T) {
|
||||
d := NewForTest()
|
||||
defer d.Destroy()
|
||||
|
||||
addr := "216.239.38.120"
|
||||
text := fmt.Sprintf(" %s google.com www.google.com # enforce google's safesearch ", addr)
|
||||
|
||||
d.checkAddRule(t, text)
|
||||
d.checkMatchIp(t, "google.com", addr)
|
||||
d.checkMatchIp(t, "www.google.com", addr)
|
||||
d.checkMatchEmpty(t, "subdomain.google.com")
|
||||
d.checkMatchEmpty(t, "example.org")
|
||||
}
|
||||
|
||||
func TestSuffixMatching1(t *testing.T) {
|
||||
d := NewForTest()
|
||||
defer d.Destroy()
|
||||
|
||||
d.checkAddRule(t, "||doubleclick.net^")
|
||||
d.checkMatch(t, "doubleclick.net")
|
||||
d.checkMatch(t, "www.doubleclick.net")
|
||||
d.checkMatchEmpty(t, "nodoubleclick.net")
|
||||
d.checkMatchEmpty(t, "doubleclick.net.ru")
|
||||
}
|
||||
|
||||
func TestSuffixMatching2(t *testing.T) {
|
||||
d := NewForTest()
|
||||
defer d.Destroy()
|
||||
|
||||
d.checkAddRule(t, "|doubleclick.net^")
|
||||
d.checkMatch(t, "doubleclick.net")
|
||||
d.checkMatchEmpty(t, "www.doubleclick.net")
|
||||
d.checkMatchEmpty(t, "nodoubleclick.net")
|
||||
d.checkMatchEmpty(t, "doubleclick.net.ru")
|
||||
}
|
||||
|
||||
func TestSuffixMatching3(t *testing.T) {
|
||||
d := NewForTest()
|
||||
defer d.Destroy()
|
||||
|
||||
d.checkAddRule(t, "doubleclick.net^")
|
||||
d.checkMatch(t, "doubleclick.net")
|
||||
d.checkMatch(t, "www.doubleclick.net")
|
||||
d.checkMatch(t, "nodoubleclick.net")
|
||||
d.checkMatchEmpty(t, "doubleclick.net.ru")
|
||||
}
|
||||
|
||||
func TestSuffixMatching4(t *testing.T) {
|
||||
d := NewForTest()
|
||||
defer d.Destroy()
|
||||
|
||||
d.checkAddRule(t, "*doubleclick.net^")
|
||||
d.checkMatch(t, "doubleclick.net")
|
||||
d.checkMatch(t, "www.doubleclick.net")
|
||||
d.checkMatch(t, "nodoubleclick.net")
|
||||
d.checkMatchEmpty(t, "doubleclick.net.ru")
|
||||
}
|
||||
|
||||
func TestSuffixMatching5(t *testing.T) {
|
||||
d := NewForTest()
|
||||
defer d.Destroy()
|
||||
|
||||
d.checkAddRule(t, "|*doubleclick.net^")
|
||||
d.checkMatch(t, "doubleclick.net")
|
||||
d.checkMatch(t, "www.doubleclick.net")
|
||||
d.checkMatch(t, "nodoubleclick.net")
|
||||
d.checkMatchEmpty(t, "doubleclick.net.ru")
|
||||
}
|
||||
|
||||
func TestSuffixMatching6(t *testing.T) {
|
||||
d := NewForTest()
|
||||
defer d.Destroy()
|
||||
|
||||
d.checkAddRule(t, "||*doubleclick.net^")
|
||||
d.checkMatch(t, "doubleclick.net")
|
||||
d.checkMatch(t, "www.doubleclick.net")
|
||||
d.checkMatch(t, "nodoubleclick.net")
|
||||
d.checkMatchEmpty(t, "doubleclick.net.ru")
|
||||
}
|
||||
|
||||
func TestCount(t *testing.T) {
|
||||
d := NewForTest()
|
||||
defer d.Destroy()
|
||||
@@ -167,6 +474,15 @@ func TestDnsFilterWhitelist(t *testing.T) {
|
||||
d.checkMatch(t, "example.org")
|
||||
d.checkMatchEmpty(t, "test.example.org")
|
||||
d.checkMatchEmpty(t, "test.test.example.org")
|
||||
|
||||
d.checkAddRule(t, "||googleadapis.l.google.com^|")
|
||||
d.checkMatch(t, "googleadapis.l.google.com")
|
||||
d.checkMatch(t, "test.googleadapis.l.google.com")
|
||||
|
||||
d.checkAddRule(t, "@@||googleadapis.l.google.com|")
|
||||
d.checkMatchEmpty(t, "googleadapis.l.google.com")
|
||||
d.checkMatchEmpty(t, "test.googleadapis.l.google.com")
|
||||
|
||||
}
|
||||
|
||||
func TestDnsFilterImportant(t *testing.T) {
|
||||
@@ -217,52 +533,6 @@ func TestAddRuleFail(t *testing.T) {
|
||||
d.checkAddRuleFail(t, "lkfaojewhoawehfwacoefawr$@#$@3413841384")
|
||||
}
|
||||
|
||||
func printMemStats(r runtime.MemStats) {
|
||||
fmt.Printf("Alloc: %.2f, HeapAlloc: %.2f Mb, Sys: %.2f Mb, HeapSys: %.2f Mb\n",
|
||||
float64(r.Alloc)/1024.0/1024.0, float64(r.HeapAlloc)/1024.0/1024.0,
|
||||
float64(r.Sys)/1024.0/1024.0, float64(r.HeapSys)/1024.0/1024.0)
|
||||
}
|
||||
|
||||
func TestLotsOfRulesMemoryUsage(t *testing.T) {
|
||||
var start, afterLoad, end runtime.MemStats
|
||||
runtime.GC()
|
||||
runtime.ReadMemStats(&start)
|
||||
fmt.Printf("Memory usage before loading rules - %d kB alloc, %d kB sys\n", start.Alloc/1024, start.Sys/1024)
|
||||
|
||||
d := NewForTest()
|
||||
defer d.Destroy()
|
||||
err := loadTestRules(d)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
runtime.GC()
|
||||
runtime.ReadMemStats(&afterLoad)
|
||||
fmt.Printf("Memory usage after loading rules - %d kB alloc, %d kB sys\n", afterLoad.Alloc/1024, afterLoad.Sys/1024)
|
||||
|
||||
tests := []struct {
|
||||
host string
|
||||
match bool
|
||||
}{
|
||||
{"asdasdasd_adsajdasda_asdasdjashdkasdasdasdasd_adsajdasda_asdasdjashdkasd.thisistesthost.com", false},
|
||||
{"asdasdasd_adsajdasda_asdasdjashdkasdasdasdasd_adsajdasda_asdasdjashdkasd.ad.doubleclick.net", true},
|
||||
}
|
||||
for _, testcase := range tests {
|
||||
ret, err := d.CheckHost(testcase.host)
|
||||
if err != nil {
|
||||
t.Errorf("Error while matching host %s: %s", testcase.host, err)
|
||||
}
|
||||
if ret.IsFiltered == false && ret.IsFiltered != testcase.match {
|
||||
t.Errorf("Expected hostname %s to not match", testcase.host)
|
||||
}
|
||||
if ret.IsFiltered == true && ret.IsFiltered != testcase.match {
|
||||
t.Errorf("Expected hostname %s to match", testcase.host)
|
||||
}
|
||||
}
|
||||
runtime.GC()
|
||||
runtime.ReadMemStats(&end)
|
||||
fmt.Printf("Memory usage after matching - %d kB alloc, %d kB sys\n", afterLoad.Alloc/1024, afterLoad.Sys/1024)
|
||||
}
|
||||
|
||||
func TestSafeBrowsing(t *testing.T) {
|
||||
testCases := []string{
|
||||
"",
|
||||
@@ -356,6 +626,8 @@ func TestParentalControl(t *testing.T) {
|
||||
if stats.Parental.Requests != l {
|
||||
t.Errorf("Parental lookup negative cache is not working")
|
||||
}
|
||||
|
||||
d.checkMatchEmpty(t, "api.jquery.com")
|
||||
}
|
||||
|
||||
func TestSafeSearch(t *testing.T) {
|
||||
@@ -385,43 +657,44 @@ var regexRules = []string{"/example\\.org/", "@@||test.example.org^"}
|
||||
var maskRules = []string{"test*.example.org^", "exam*.com"}
|
||||
|
||||
var tests = []struct {
|
||||
testname string
|
||||
rules []string
|
||||
hostname string
|
||||
result bool
|
||||
testname string
|
||||
rules []string
|
||||
hostname string
|
||||
isFiltered bool
|
||||
reason Reason
|
||||
}{
|
||||
{"sanity", []string{"||doubleclick.net^"}, "www.doubleclick.net", true},
|
||||
{"sanity", []string{"||doubleclick.net^"}, "nodoubleclick.net", false},
|
||||
{"sanity", []string{"||doubleclick.net^"}, "doubleclick.net.ru", false},
|
||||
{"sanity", []string{"||doubleclick.net^"}, "wmconvirus.narod.ru", false},
|
||||
{"blocking", blockingRules, "example.org", true},
|
||||
{"blocking", blockingRules, "test.example.org", true},
|
||||
{"blocking", blockingRules, "test.test.example.org", true},
|
||||
{"blocking", blockingRules, "testexample.org", false},
|
||||
{"blocking", blockingRules, "onemoreexample.org", false},
|
||||
{"whitelist", whitelistRules, "example.org", true},
|
||||
{"whitelist", whitelistRules, "test.example.org", false},
|
||||
{"whitelist", whitelistRules, "test.test.example.org", false},
|
||||
{"whitelist", whitelistRules, "testexample.org", false},
|
||||
{"whitelist", whitelistRules, "onemoreexample.org", false},
|
||||
{"important", importantRules, "example.org", false},
|
||||
{"important", importantRules, "test.example.org", true},
|
||||
{"important", importantRules, "test.test.example.org", true},
|
||||
{"important", importantRules, "testexample.org", false},
|
||||
{"important", importantRules, "onemoreexample.org", false},
|
||||
{"regex", regexRules, "example.org", true},
|
||||
{"regex", regexRules, "test.example.org", false},
|
||||
{"regex", regexRules, "test.test.example.org", false},
|
||||
{"regex", regexRules, "testexample.org", true},
|
||||
{"regex", regexRules, "onemoreexample.org", true},
|
||||
{"mask", maskRules, "test.example.org", true},
|
||||
{"mask", maskRules, "test2.example.org", true},
|
||||
{"mask", maskRules, "example.com", true},
|
||||
{"mask", maskRules, "exampleeee.com", true},
|
||||
{"mask", maskRules, "onemoreexamsite.com", true},
|
||||
{"mask", maskRules, "example.org", false},
|
||||
{"mask", maskRules, "testexample.org", false},
|
||||
{"mask", maskRules, "example.co.uk", false},
|
||||
{"sanity", []string{"||doubleclick.net^"}, "www.doubleclick.net", true, FilteredBlackList},
|
||||
{"sanity", []string{"||doubleclick.net^"}, "nodoubleclick.net", false, NotFilteredNotFound},
|
||||
{"sanity", []string{"||doubleclick.net^"}, "doubleclick.net.ru", false, NotFilteredNotFound},
|
||||
{"sanity", []string{"||doubleclick.net^"}, "wmconvirus.narod.ru", false, NotFilteredNotFound},
|
||||
{"blocking", blockingRules, "example.org", true, FilteredBlackList},
|
||||
{"blocking", blockingRules, "test.example.org", true, FilteredBlackList},
|
||||
{"blocking", blockingRules, "test.test.example.org", true, FilteredBlackList},
|
||||
{"blocking", blockingRules, "testexample.org", false, NotFilteredNotFound},
|
||||
{"blocking", blockingRules, "onemoreexample.org", false, NotFilteredNotFound},
|
||||
{"whitelist", whitelistRules, "example.org", true, FilteredBlackList},
|
||||
{"whitelist", whitelistRules, "test.example.org", false, NotFilteredWhiteList},
|
||||
{"whitelist", whitelistRules, "test.test.example.org", false, NotFilteredWhiteList},
|
||||
{"whitelist", whitelistRules, "testexample.org", false, NotFilteredNotFound},
|
||||
{"whitelist", whitelistRules, "onemoreexample.org", false, NotFilteredNotFound},
|
||||
{"important", importantRules, "example.org", false, NotFilteredWhiteList},
|
||||
{"important", importantRules, "test.example.org", true, FilteredBlackList},
|
||||
{"important", importantRules, "test.test.example.org", true, FilteredBlackList},
|
||||
{"important", importantRules, "testexample.org", false, NotFilteredNotFound},
|
||||
{"important", importantRules, "onemoreexample.org", false, NotFilteredNotFound},
|
||||
{"regex", regexRules, "example.org", true, FilteredBlackList},
|
||||
{"regex", regexRules, "test.example.org", false, NotFilteredWhiteList},
|
||||
{"regex", regexRules, "test.test.example.org", false, NotFilteredWhiteList},
|
||||
{"regex", regexRules, "testexample.org", true, FilteredBlackList},
|
||||
{"regex", regexRules, "onemoreexample.org", true, FilteredBlackList},
|
||||
{"mask", maskRules, "test.example.org", true, FilteredBlackList},
|
||||
{"mask", maskRules, "test2.example.org", true, FilteredBlackList},
|
||||
{"mask", maskRules, "example.com", true, FilteredBlackList},
|
||||
{"mask", maskRules, "exampleeee.com", true, FilteredBlackList},
|
||||
{"mask", maskRules, "onemoreexamsite.com", true, FilteredBlackList},
|
||||
{"mask", maskRules, "example.org", false, NotFilteredNotFound},
|
||||
{"mask", maskRules, "testexample.org", false, NotFilteredNotFound},
|
||||
{"mask", maskRules, "example.co.uk", false, NotFilteredNotFound},
|
||||
}
|
||||
|
||||
func TestMatching(t *testing.T) {
|
||||
@@ -439,8 +712,11 @@ func TestMatching(t *testing.T) {
|
||||
if err != nil {
|
||||
t.Errorf("Error while matching host %s: %s", test.hostname, err)
|
||||
}
|
||||
if ret.IsFiltered != test.result {
|
||||
t.Errorf("Hostname %s has wrong result (%v must be %v)", test.hostname, ret, test.result)
|
||||
if ret.IsFiltered != test.isFiltered {
|
||||
t.Errorf("Hostname %s has wrong result (%v must be %v)", test.hostname, ret.IsFiltered, test.isFiltered)
|
||||
}
|
||||
if ret.Reason != test.reason {
|
||||
t.Errorf("Hostname %s has wrong reason (%v must be %v)", test.hostname, ret.Reason.String(), test.reason.String())
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -457,6 +733,7 @@ func BenchmarkAddRule(b *testing.B) {
|
||||
err := d.AddRule(rule, 0)
|
||||
switch err {
|
||||
case nil:
|
||||
case ErrAlreadyExists: // ignore rules which were already added
|
||||
case ErrInvalidSyntax: // ignore invalid syntax
|
||||
default:
|
||||
b.Fatalf("Error while adding rule %s: %s", rule, err)
|
||||
@@ -476,6 +753,7 @@ func BenchmarkAddRuleParallel(b *testing.B) {
|
||||
}
|
||||
switch err {
|
||||
case nil:
|
||||
case ErrAlreadyExists: // ignore rules which were already added
|
||||
case ErrInvalidSyntax: // ignore invalid syntax
|
||||
default:
|
||||
b.Fatalf("Error while adding rule %s: %s", rule, err)
|
||||
@@ -569,6 +847,80 @@ func BenchmarkLotsOfRulesMatchParallel(b *testing.B) {
|
||||
})
|
||||
}
|
||||
|
||||
func BenchmarkLotsOfRulesLotsOfHosts(b *testing.B) {
|
||||
d := NewForTest()
|
||||
defer d.Destroy()
|
||||
mustLoadTestRules(d)
|
||||
|
||||
getTopHosts()
|
||||
hostnames, err := os.Open(topHostsFilename)
|
||||
if err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
defer hostnames.Close()
|
||||
|
||||
scanner := bufio.NewScanner(hostnames)
|
||||
b.ResetTimer()
|
||||
for n := 0; n < b.N; n++ {
|
||||
havedata := scanner.Scan()
|
||||
if !havedata {
|
||||
hostnames.Seek(0, 0)
|
||||
scanner = bufio.NewScanner(hostnames)
|
||||
havedata = scanner.Scan()
|
||||
}
|
||||
if !havedata {
|
||||
b.Fatal(scanner.Err())
|
||||
}
|
||||
line := scanner.Text()
|
||||
records := strings.Split(line, ",")
|
||||
ret, err := d.CheckHost(records[1] + "." + records[1])
|
||||
if err != nil {
|
||||
b.Error(err)
|
||||
}
|
||||
if ret.Reason.Matched() {
|
||||
// log.Printf("host \"%s\" mathed. Rule \"%s\", reason: %v", host, ret.Rule, ret.Reason)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkLotsOfRulesLotsOfHostsParallel(b *testing.B) {
|
||||
d := NewForTest()
|
||||
defer d.Destroy()
|
||||
mustLoadTestRules(d)
|
||||
|
||||
getTopHosts()
|
||||
|
||||
b.ResetTimer()
|
||||
b.RunParallel(func(pb *testing.PB) {
|
||||
hostnames, err := os.Open(topHostsFilename)
|
||||
if err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
defer hostnames.Close()
|
||||
scanner := bufio.NewScanner(hostnames)
|
||||
for pb.Next() {
|
||||
havedata := scanner.Scan()
|
||||
if !havedata {
|
||||
hostnames.Seek(0, 0)
|
||||
scanner = bufio.NewScanner(hostnames)
|
||||
havedata = scanner.Scan()
|
||||
}
|
||||
if !havedata {
|
||||
b.Fatal(scanner.Err())
|
||||
}
|
||||
line := scanner.Text()
|
||||
records := strings.Split(line, ",")
|
||||
ret, err := d.CheckHost(records[1] + "." + records[1])
|
||||
if err != nil {
|
||||
b.Error(err)
|
||||
}
|
||||
if ret.Reason.Matched() {
|
||||
// log.Printf("host \"%s\" mathed. Rule \"%s\", reason: %v", host, ret.Rule, ret.Reason)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func BenchmarkSafeBrowsing(b *testing.B) {
|
||||
d := NewForTest()
|
||||
defer d.Destroy()
|
||||
@@ -638,3 +990,36 @@ func BenchmarkSafeSearchParallel(b *testing.B) {
|
||||
func TestMain(m *testing.M) {
|
||||
goleak.VerifyTestMain(m)
|
||||
}
|
||||
|
||||
//
|
||||
// helper functions for debugging and testing
|
||||
//
|
||||
func purgeCaches() {
|
||||
if safebrowsingCache != nil {
|
||||
safebrowsingCache.Purge()
|
||||
}
|
||||
if parentalCache != nil {
|
||||
parentalCache.Purge()
|
||||
}
|
||||
}
|
||||
|
||||
func _Func() string {
|
||||
pc := make([]uintptr, 10) // at least 1 entry needed
|
||||
runtime.Callers(2, pc)
|
||||
f := runtime.FuncForPC(pc[0])
|
||||
return path.Base(f.Name())
|
||||
}
|
||||
|
||||
func trace(format string, args ...interface{}) {
|
||||
pc := make([]uintptr, 10) // at least 1 entry needed
|
||||
runtime.Callers(2, pc)
|
||||
f := runtime.FuncForPC(pc[0])
|
||||
var buf strings.Builder
|
||||
buf.WriteString(fmt.Sprintf("%s(): ", path.Base(f.Name())))
|
||||
text := fmt.Sprintf(format, args...)
|
||||
buf.WriteString(text)
|
||||
if len(text) == 0 || text[len(text)-1] != '\n' {
|
||||
buf.WriteRune('\n')
|
||||
}
|
||||
fmt.Print(buf.String())
|
||||
}
|
||||
|
||||
@@ -1,9 +1,6 @@
|
||||
package dnsfilter
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"path"
|
||||
"runtime"
|
||||
"strings"
|
||||
"sync/atomic"
|
||||
)
|
||||
@@ -22,11 +19,17 @@ func isValidRule(rule string) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
// Filter out all sorts of cosmetic rules:
|
||||
// https://kb.adguard.com/en/general/how-to-create-your-own-ad-filters#cosmetic-rules
|
||||
masks := []string{
|
||||
"##",
|
||||
"#@#",
|
||||
"#?#",
|
||||
"#@?#",
|
||||
"#$#",
|
||||
"#@$#",
|
||||
"#?$#",
|
||||
"#@?$#",
|
||||
"$$",
|
||||
"$@$",
|
||||
"#%#",
|
||||
@@ -49,33 +52,9 @@ func updateMax(valuePtr *int64, maxPtr *int64) {
|
||||
break
|
||||
}
|
||||
swapped := atomic.CompareAndSwapInt64(maxPtr, max, current)
|
||||
if swapped == true {
|
||||
if swapped {
|
||||
break
|
||||
}
|
||||
// swapping failed because value has changed after reading, try again
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
// helper functions for debugging and testing
|
||||
//
|
||||
func _Func() string {
|
||||
pc := make([]uintptr, 10) // at least 1 entry needed
|
||||
runtime.Callers(2, pc)
|
||||
f := runtime.FuncForPC(pc[0])
|
||||
return path.Base(f.Name())
|
||||
}
|
||||
|
||||
func trace(format string, args ...interface{}) {
|
||||
pc := make([]uintptr, 10) // at least 1 entry needed
|
||||
runtime.Callers(2, pc)
|
||||
f := runtime.FuncForPC(pc[0])
|
||||
var buf strings.Builder
|
||||
buf.WriteString(fmt.Sprintf("%s(): ", path.Base(f.Name())))
|
||||
text := fmt.Sprintf(format, args...)
|
||||
buf.WriteString(text)
|
||||
if len(text) == 0 || text[len(text)-1] != '\n' {
|
||||
buf.WriteRune('\n')
|
||||
}
|
||||
fmt.Print(buf.String())
|
||||
}
|
||||
|
||||
@@ -5,8 +5,8 @@ import (
|
||||
)
|
||||
|
||||
func ruleToRegexp(rule string) (string, error) {
|
||||
const hostStart = "^([a-z0-9-_.]+\\.)?"
|
||||
const hostEnd = "([^ a-zA-Z0-9.%]|$)"
|
||||
const hostStart = `(?:^|\.)`
|
||||
const hostEnd = `$`
|
||||
|
||||
// empty or short rule -- do nothing
|
||||
if !isValidRule(rule) {
|
||||
@@ -49,3 +49,43 @@ func ruleToRegexp(rule string) (string, error) {
|
||||
|
||||
return sb.String(), nil
|
||||
}
|
||||
|
||||
// handle suffix rule ||example.com^ -- either entire string is example.com or *.example.com
|
||||
func getSuffix(rule string) (bool, string) {
|
||||
// if starts with / and ends with /, it's already a regexp
|
||||
// TODO: if a regexp is simple `/abracadabra$/`, then simplify it maybe?
|
||||
if rule[0] == '/' && rule[len(rule)-1] == '/' {
|
||||
return false, ""
|
||||
}
|
||||
|
||||
// must start with ||
|
||||
if rule[0] != '|' || rule[1] != '|' {
|
||||
return false, ""
|
||||
}
|
||||
rule = rule[2:]
|
||||
|
||||
// suffix rule must end with ^ or |
|
||||
lastChar := rule[len(rule)-1]
|
||||
if lastChar != '^' && lastChar != '|' {
|
||||
return false, ""
|
||||
}
|
||||
// last char was checked, eat it
|
||||
rule = rule[:len(rule)-1]
|
||||
|
||||
// it might also end with ^|
|
||||
if rule[len(rule)-1] == '^' {
|
||||
rule = rule[:len(rule)-1]
|
||||
}
|
||||
|
||||
// check that it doesn't have any special characters inside
|
||||
for _, r := range rule {
|
||||
switch r {
|
||||
case '|':
|
||||
return false, ""
|
||||
case '*':
|
||||
return false, ""
|
||||
}
|
||||
}
|
||||
|
||||
return true, rule
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user