Compare commits
284 Commits
v0.1
...
v0.9-hotfi
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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
|
||||
9
.gitignore
vendored
9
.gitignore
vendored
@@ -1,7 +1,12 @@
|
||||
/AdguardDNS
|
||||
/AdguardDNS.yaml
|
||||
.DS_Store
|
||||
.vscode
|
||||
debug
|
||||
/AdGuardHome
|
||||
/AdGuardHome.yaml
|
||||
/build/
|
||||
/client/node_modules/
|
||||
/coredns
|
||||
/Corefile
|
||||
/dnsfilter.txt
|
||||
/querylog.json
|
||||
/querylog.json.1
|
||||
20
.travis.yml
Normal file
20
.travis.yml
Normal file
@@ -0,0 +1,20 @@
|
||||
language: go
|
||||
sudo: false
|
||||
go:
|
||||
- 1.10.x
|
||||
- 1.11.x
|
||||
- 1.x
|
||||
|
||||
os:
|
||||
- linux
|
||||
- osx
|
||||
|
||||
install:
|
||||
- go get -v -d -t ./...
|
||||
- npm --prefix client install
|
||||
|
||||
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.9"
|
||||
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.9"
|
||||
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.9"
|
||||
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}
|
||||
46
Makefile
46
Makefile
@@ -1,33 +1,41 @@
|
||||
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 := $(mkfile_dir)/build/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
|
||||
mkdir -p $(GOPATH)/src/github.com/AdguardTeam
|
||||
if [ ! -h $(GOPATH)/src/github.com/AdguardTeam/AdGuardHome ]; then rm -rf $(GOPATH)/src/github.com/AdguardTeam/AdGuardHome && ln -fs $(mkfile_dir) $(GOPATH)/src/github.com/AdguardTeam/AdGuardHome; fi
|
||||
GOPATH=$(GOPATH) go get -v -d .
|
||||
GOPATH=$(GOPATH) GOOS=$(NATIVE_GOOS) GOARCH=$(NATIVE_GOARCH) go get -v github.com/gobuffalo/packr/...
|
||||
mkdir -p $(GOPATH)/src/github.com/AdguardTeam/AdGuardHome/build/static ## work around packr bug
|
||||
cd $(GOPATH)/src/github.com/prometheus/client_golang && git reset --hard v0.8.0
|
||||
perl -0777 -p -i.bak -e 's/pprofOnce.Do\(func\(\) {(.*)}\)/\1/ms' $(GOPATH)/src/github.com/coredns/coredns/plugin/pprof/setup.go
|
||||
perl -0777 -p -i.bak -e 's/c.OnShutdown/c.OnRestart/' $(GOPATH)/src/github.com/coredns/coredns/plugin/pprof/setup.go
|
||||
GOPATH=$(GOPATH) PATH=$(GOPATH)/bin:$(PATH) packr build -ldflags="-X main.VersionString=$(GIT_VERSION)" -o $(TARGET)
|
||||
|
||||
clean:
|
||||
rm -vf coredns AdguardDNS
|
||||
$(MAKE) cleanfast
|
||||
rm -rf build
|
||||
rm -rf client/node_modules
|
||||
|
||||
cleanfast:
|
||||
rm -f $(TARGET)
|
||||
|
||||
172
README.md
172
README.md
@@ -1,66 +1,166 @@
|
||||
# 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.9-hotfix1_MacOS.zip](https://github.com/AdguardTeam/AdGuardHome/releases/download/v0.9-hotfix1/AdGuardHome_v0.9-hotfix1_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.9-hotfix1_linux_amd64.tar.gz](https://github.com/AdguardTeam/AdGuardHome/releases/download/v0.9-hotfix1/AdGuardHome_v0.9-hotfix1_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.9-hotfix1_linux_386.tar.gz](https://github.com/AdguardTeam/AdGuardHome/releases/download/v0.9-hotfix1/AdGuardHome_v0.9-hotfix1_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.9-hotfix1_linux_arm.tar.gz](https://github.com/AdguardTeam/AdGuardHome/releases/download/v0.9-hotfix1/AdGuardHome_v0.9-hotfix1_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)
|
||||
* `upstream_dns` — List of upstream DNS servers
|
||||
* `filters` — List of filters, each filter has the following values:
|
||||
* `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:
|
||||
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
|
||||
|
||||
DNS works on port 53, which requires superuser privileges. Therefore, you need to run it with sudo:
|
||||
```bash
|
||||
sudo ./AdguardDNS
|
||||
```
|
||||
|
||||
Now open the browser and point it to http://localhost:3000/ to control AdGuard DNS server.
|
||||
|
||||
## 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
|
||||
```
|
||||
|
||||
If the file does not exist, create it and put these two lines down.
|
||||
|
||||
## 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
|
||||
|
||||
## 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.
|
||||
|
||||
148
app.go
148
app.go
@@ -1,6 +1,7 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"fmt"
|
||||
"log"
|
||||
"net"
|
||||
@@ -10,13 +11,14 @@ 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()
|
||||
@@ -26,6 +28,8 @@ func main() {
|
||||
config.ourBinaryDir = filepath.Dir(executable)
|
||||
}
|
||||
|
||||
doConfigRename := true
|
||||
|
||||
// config can be specified, which reads options from there, but other command line flags have to override config values
|
||||
// therefore, we must do it manually instead of using a lib
|
||||
{
|
||||
@@ -94,10 +98,25 @@ func main() {
|
||||
}
|
||||
}
|
||||
if configFilename != nil {
|
||||
// config was manually specified, don't do anything
|
||||
doConfigRename = false
|
||||
config.ourConfigFilename = *configFilename
|
||||
}
|
||||
|
||||
if doConfigRename {
|
||||
err := renameOldConfigIfNeccessary()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
|
||||
err := askUsernamePasswordIfPossible()
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
// parse from config file
|
||||
err := parseConfig()
|
||||
err = parseConfig()
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
@@ -109,6 +128,11 @@ func main() {
|
||||
}
|
||||
}
|
||||
|
||||
// eat all args so that coredns can start happily
|
||||
if len(os.Args) > 1 {
|
||||
os.Args = os.Args[:1]
|
||||
}
|
||||
|
||||
err := writeConfig()
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
@@ -116,13 +140,129 @@ func main() {
|
||||
|
||||
address := net.JoinHostPort(config.BindHost, strconv.Itoa(config.BindPort))
|
||||
|
||||
runStatsCollectors()
|
||||
runFilterRefreshers()
|
||||
|
||||
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
|
||||
}
|
||||
return "", nil
|
||||
}
|
||||
|
||||
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 := 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
|
||||
}
|
||||
|
||||
func renameOldConfigIfNeccessary() error {
|
||||
oldConfigFile := filepath.Join(config.ourBinaryDir, "AdguardDNS.yaml")
|
||||
_, err := os.Stat(oldConfigFile)
|
||||
if os.IsNotExist(err) {
|
||||
// do nothing, file doesn't exist
|
||||
trace("File %s doesn't exist, nothing to do", oldConfigFile)
|
||||
return nil
|
||||
}
|
||||
|
||||
newConfigFile := filepath.Join(config.ourBinaryDir, config.ourConfigFilename)
|
||||
_, err = os.Stat(newConfigFile)
|
||||
if !os.IsNotExist(err) {
|
||||
// do nothing, file doesn't exist
|
||||
trace("File %s already exists, will not overwrite", newConfigFile)
|
||||
return nil
|
||||
}
|
||||
|
||||
err = os.Rename(oldConfigFile, newConfigFile)
|
||||
if err != nil {
|
||||
log.Printf("Failed to rename %s to %s: %s", oldConfigFile, newConfigFile, err)
|
||||
return 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"
|
||||
}
|
||||
}
|
||||
|
||||
16614
client/package-lock.json
generated
vendored
Normal file
16614
client/package-lock.json
generated
vendored
Normal file
File diff suppressed because it is too large
Load Diff
14
client/package.json
vendored
14
client/package.json
vendored
@@ -3,31 +3,34 @@
|
||||
"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",
|
||||
"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-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",
|
||||
"tiny-version-compare": "^0.9.1",
|
||||
"whatwg-fetch": "2.0.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
@@ -41,6 +44,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>
|
||||
|
||||
@@ -1,50 +1,70 @@
|
||||
import { createAction } from 'redux-actions';
|
||||
import round from 'lodash/round';
|
||||
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';
|
||||
await apiClient.disableFiltering();
|
||||
} else {
|
||||
successMessage = 'Enabled filtering';
|
||||
await apiClient.enableFiltering();
|
||||
}
|
||||
dispatch(toggleSettingStatus({ settingKey }));
|
||||
break;
|
||||
case 'safebrowsing':
|
||||
if (status) {
|
||||
successMessage = 'Disabled safebrowsing';
|
||||
await apiClient.disableSafebrowsing();
|
||||
} else {
|
||||
successMessage = 'Enabled safebrowsing';
|
||||
await apiClient.enableSafebrowsing();
|
||||
}
|
||||
dispatch(toggleSettingStatus({ settingKey }));
|
||||
break;
|
||||
case 'parental':
|
||||
if (status) {
|
||||
successMessage = 'Disabled parental control';
|
||||
await apiClient.disableParentalControl();
|
||||
} else {
|
||||
successMessage = 'Enabled parental control';
|
||||
await apiClient.enableParentalControl();
|
||||
}
|
||||
dispatch(toggleSettingStatus({ settingKey }));
|
||||
break;
|
||||
case 'safesearch':
|
||||
if (status) {
|
||||
successMessage = 'Disabled safe search';
|
||||
await apiClient.disableSafesearch();
|
||||
} else {
|
||||
successMessage = 'Enabled safe search';
|
||||
await apiClient.enableSafesearch();
|
||||
}
|
||||
dispatch(toggleSettingStatus({ settingKey }));
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
dispatch(addSuccessToast(successMessage));
|
||||
} catch (error) {
|
||||
dispatch(addErrorToast({ error }));
|
||||
}
|
||||
};
|
||||
|
||||
@@ -73,11 +93,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 +148,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 +163,7 @@ export const enableDns = () => async (dispatch) => {
|
||||
await apiClient.startGlobalFiltering();
|
||||
dispatch(enableDnsSuccess());
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
dispatch(addErrorToast({ error }));
|
||||
dispatch(enableDnsFailure());
|
||||
}
|
||||
};
|
||||
@@ -118,8 +178,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 +199,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 +246,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 +268,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 = 'disabled';
|
||||
} else {
|
||||
toggleMethod = apiClient.enableQueryLog.bind(apiClient);
|
||||
successMessage = 'enabled';
|
||||
}
|
||||
try {
|
||||
await toggleMethod();
|
||||
dispatch(addSuccessToast(`Query log ${successMessage}`));
|
||||
dispatch(toggleLogStatusSuccess());
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
dispatch(addErrorToast({ error }));
|
||||
dispatch(toggleLogStatusFailure());
|
||||
}
|
||||
};
|
||||
@@ -214,10 +293,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 the custom filtering rules'));
|
||||
dispatch(setRulesSuccess());
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
dispatch(addErrorToast({ error }));
|
||||
dispatch(setRulesFailure());
|
||||
}
|
||||
};
|
||||
@@ -232,7 +315,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 +341,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 +352,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 are already up-to-date'));
|
||||
} 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 +389,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 +398,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 +422,7 @@ export const removeFilter = url => async (dispatch) => {
|
||||
dispatch(removeFilterSuccess(url));
|
||||
dispatch(getFilteringStatus());
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
dispatch(addErrorToast({ error }));
|
||||
dispatch(removeFilterFailure());
|
||||
}
|
||||
};
|
||||
@@ -344,7 +441,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 +456,38 @@ export const setUpstream = url => async (dispatch) => {
|
||||
dispatch(setUpstreamRequest());
|
||||
try {
|
||||
await apiClient.setUpstream(url);
|
||||
dispatch(addSuccessToast('Updated the upstream DNS servers'));
|
||||
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: `Server "${key}": could not be used, please check that you've written it correctly` }));
|
||||
}
|
||||
return message;
|
||||
});
|
||||
|
||||
if (testMessages.every(message => message === 'OK')) {
|
||||
dispatch(addSuccessToast('Specified DNS servers are working correctly'));
|
||||
}
|
||||
|
||||
dispatch(testUpstreamSuccess());
|
||||
} catch (error) {
|
||||
dispatch(addErrorToast({ error }));
|
||||
dispatch(testUpstreamFailure());
|
||||
}
|
||||
};
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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,12 +14,14 @@ 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';
|
||||
|
||||
class App extends Component {
|
||||
componentDidMount() {
|
||||
this.props.getDnsStatus();
|
||||
this.props.getVersion();
|
||||
}
|
||||
|
||||
handleStatusChange = () => {
|
||||
@@ -27,9 +30,21 @@ class App extends Component {
|
||||
|
||||
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 +64,7 @@ class App extends Component {
|
||||
}
|
||||
</div>
|
||||
<Footer />
|
||||
<Toasts />
|
||||
</Fragment>
|
||||
</HashRouter>
|
||||
);
|
||||
@@ -60,6 +76,8 @@ App.propTypes = {
|
||||
enableDns: PropTypes.func,
|
||||
dashboard: PropTypes.object,
|
||||
isCoreRunning: PropTypes.bool,
|
||||
error: PropTypes.string,
|
||||
getVersion: PropTypes.func,
|
||||
};
|
||||
|
||||
export default App;
|
||||
|
||||
@@ -1,34 +1,76 @@
|
||||
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 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" title={value}>
|
||||
<div className="logs__text">
|
||||
{value}
|
||||
</div>
|
||||
{trackerData && <Popover data={trackerData} />}
|
||||
</div>
|
||||
);
|
||||
},
|
||||
}, {
|
||||
Header: 'Requests count',
|
||||
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() {
|
||||
return (
|
||||
<Card title="Top blocked domains" subtitle="for the 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="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,
|
||||
};
|
||||
|
||||
export default Clients;
|
||||
export default BlockedDomains;
|
||||
|
||||
@@ -1,34 +1,63 @@
|
||||
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 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>),
|
||||
}, {
|
||||
Header: 'Requests count',
|
||||
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() {
|
||||
return (
|
||||
<Card title="Top clients" subtitle="for the 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="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,
|
||||
};
|
||||
|
||||
export default Clients;
|
||||
|
||||
@@ -4,14 +4,16 @@ import PropTypes from 'prop-types';
|
||||
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="General statistics" subtitle="for the 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" />
|
||||
<Tooltip text="A number of DNS quieries processed for the last 24 hours" type={tooltipType} />
|
||||
</td>
|
||||
<td className="text-right">
|
||||
<span className="text-muted">
|
||||
@@ -21,8 +23,8 @@ const Counters = props => (
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
Blocked by filters
|
||||
<Tooltip text="A number of DNS requests blocked by filters" />
|
||||
Blocked by <a href="#filters">Filters</a>
|
||||
<Tooltip text="A number of DNS requests blocked by adblock filters and hosts blocklists" type={tooltipType} />
|
||||
</td>
|
||||
<td className="text-right">
|
||||
<span className="text-muted">
|
||||
@@ -33,7 +35,7 @@ const Counters = props => (
|
||||
<tr>
|
||||
<td>
|
||||
Blocked malware/phishing
|
||||
<Tooltip text="A number of DNS requests blocked" />
|
||||
<Tooltip text="A number of DNS requests blocked by the AdGuard browsing security module" type={tooltipType} />
|
||||
</td>
|
||||
<td className="text-right">
|
||||
<span className="text-muted">
|
||||
@@ -44,7 +46,7 @@ const Counters = props => (
|
||||
<tr>
|
||||
<td>
|
||||
Blocked adult websites
|
||||
<Tooltip text="A number of adult websites blocked" />
|
||||
<Tooltip text="A number of adult websites blocked" type={tooltipType} />
|
||||
</td>
|
||||
<td className="text-right">
|
||||
<span className="text-muted">
|
||||
@@ -55,7 +57,7 @@ const Counters = props => (
|
||||
<tr>
|
||||
<td>
|
||||
Enforced safe search
|
||||
<Tooltip text="A number of DNS requests to search engines for which Safe Search was enforced" />
|
||||
<Tooltip text="A number of DNS requests to search engines for which Safe Search was enforced" type={tooltipType} />
|
||||
</td>
|
||||
<td className="text-right">
|
||||
<span className="text-muted">
|
||||
@@ -66,7 +68,7 @@ const Counters = props => (
|
||||
<tr>
|
||||
<td>
|
||||
Average processing time
|
||||
<Tooltip text="Average time in milliseconds on processing a DNS request" />
|
||||
<Tooltip text="Average time in milliseconds on processing a DNS request" type={tooltipType} />
|
||||
</td>
|
||||
<td className="text-right">
|
||||
<span className="text-muted">
|
||||
@@ -86,7 +88,7 @@ Counters.propTypes = {
|
||||
replacedParental: PropTypes.number.isRequired,
|
||||
replacedSafesearch: PropTypes.number.isRequired,
|
||||
avgProcessingTime: PropTypes.number.isRequired,
|
||||
refreshButton: PropTypes.node,
|
||||
refreshButton: PropTypes.node.isRequired,
|
||||
};
|
||||
|
||||
export default 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,78 @@
|
||||
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 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" title={value}>
|
||||
<div className="logs__text">
|
||||
{value}
|
||||
</div>
|
||||
{trackerData && <Popover data={trackerData} />}
|
||||
</div>
|
||||
);
|
||||
},
|
||||
}, {
|
||||
Header: 'Requests count',
|
||||
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() {
|
||||
return (
|
||||
<Card title="Top queried domains" subtitle="for the 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="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,
|
||||
};
|
||||
|
||||
export default QueriedDomains;
|
||||
|
||||
@@ -1,61 +1,109 @@
|
||||
import React from 'react';
|
||||
import { ResponsiveLine } from '@nivo/line';
|
||||
import React, { Component } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
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 bodyType="card-wrap">
|
||||
<div className="card-body-stats">
|
||||
<div className="card-value card-value-stats text-blue">
|
||||
{dnsQueries}
|
||||
</div>
|
||||
<div className="card-title-stats">
|
||||
DNS Queries
|
||||
</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 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">
|
||||
Blocked by <a href="#filters">Filters</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 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">
|
||||
Blocked malware/phishing
|
||||
</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 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">
|
||||
Blocked adult websites
|
||||
</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;
|
||||
|
||||
@@ -10,14 +10,31 @@ 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' : 'Enable';
|
||||
const buttonClass = protectionEnabled ? 'btn-gray' : 'btn-success';
|
||||
|
||||
return (
|
||||
<button type="button" className={`btn btn-sm mr-2 ${buttonClass}`} onClick={() => this.props.toggleProtection(protectionEnabled)}>
|
||||
{buttonText} protection
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
const { dashboard } = this.props;
|
||||
const dashboardProcessing =
|
||||
@@ -26,15 +43,14 @@ class Dashboard extends Component {
|
||||
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()}>Refresh statistics</button>;
|
||||
const refreshButton = <button type="button" className="btn btn-outline-primary btn-sm card-refresh" onClick={() => this.getAllStats()} />;
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
<PageTitle title="Dashboard">
|
||||
<div className="page-title__actions">
|
||||
{disableButton}
|
||||
{this.getToggleFilteringButton()}
|
||||
{refreshFullButton}
|
||||
</div>
|
||||
</PageTitle>
|
||||
@@ -46,6 +62,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 +86,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 +102,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 +120,10 @@ 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,
|
||||
};
|
||||
|
||||
export default Dashboard;
|
||||
|
||||
@@ -20,14 +20,14 @@ export default class UserRules extends Component {
|
||||
subtitle="Enter one rule on a line. You can use either adblock rules or hosts files syntax."
|
||||
>
|
||||
<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...
|
||||
Apply
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
@@ -44,7 +44,7 @@ export default class UserRules extends Component {
|
||||
domain and all its subdomains
|
||||
</li>
|
||||
<li>
|
||||
<code>example.org 127.0.0.1</code> - AdGuard DNS will now return
|
||||
<code>127.0.0.1 example.org</code> - AdGuard Home will now return
|
||||
127.0.0.1 address for the example.org domain (but not its subdomains).
|
||||
</li>
|
||||
<li>
|
||||
|
||||
@@ -39,17 +39,19 @@ class Filters extends Component {
|
||||
width: 90,
|
||||
className: 'text-center',
|
||||
}, {
|
||||
Header: 'Filter name',
|
||||
Header: 'Name',
|
||||
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: 'Filter URL',
|
||||
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',
|
||||
accessor: 'rulesCount',
|
||||
className: 'text-center',
|
||||
}, {
|
||||
Header: 'Last time update',
|
||||
Header: 'Last time updated',
|
||||
accessor: 'lastUpdated',
|
||||
className: 'text-center',
|
||||
}, {
|
||||
@@ -71,8 +73,8 @@ class Filters extends Component {
|
||||
<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="Filters and hosts blocklists"
|
||||
subtitle="AdGuard Home understands basic adblock rules and hosts files syntax."
|
||||
>
|
||||
<ReactTable
|
||||
data={filters}
|
||||
@@ -102,7 +104,7 @@ class Filters extends Component {
|
||||
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."
|
||||
inputDescription="Enter a valid URL to a filter subscription or a hosts file."
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -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) {
|
||||
@@ -103,7 +107,7 @@
|
||||
|
||||
.nav-version {
|
||||
padding: 0;
|
||||
font-size: 0.9rem;
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -4,6 +4,8 @@ import PropTypes from 'prop-types';
|
||||
import enhanceWithClickOutside from 'react-click-outside';
|
||||
import classnames from 'classnames';
|
||||
|
||||
import { REPOSITORY } from '../../helpers/constants';
|
||||
|
||||
class Menu extends Component {
|
||||
handleClickOutside = () => {
|
||||
this.props.closeMenu();
|
||||
@@ -53,6 +55,12 @@ class Menu extends Component {
|
||||
Query Log
|
||||
</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>
|
||||
FAQ
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</Fragment>
|
||||
|
||||
@@ -5,7 +5,12 @@ export default function Version(props) {
|
||||
const { dnsVersion, dnsAddress, dnsPort } = props;
|
||||
return (
|
||||
<div className="nav-version">
|
||||
v.{dnsVersion} / address: {dnsAddress}:{dnsPort}
|
||||
<div className="nav-version__text">
|
||||
version: <span className="nav-version__value">{dnsVersion}</span>
|
||||
</div>
|
||||
<div className="nav-version__text">
|
||||
address: <span className="nav-version__value">{dnsAddress}:{dnsPort}</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -26,8 +26,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 +42,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'}
|
||||
{dashboard.protectionEnabled ? 'ON' : 'OFF'}
|
||||
</span>
|
||||
}
|
||||
</div>
|
||||
|
||||
@@ -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,25 @@
|
||||
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 { 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 Tooltip from '../ui/Tooltip';
|
||||
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 +29,189 @@ class Logs extends Component {
|
||||
}
|
||||
}
|
||||
|
||||
getLogs = () => {
|
||||
// get logs on initialization if queryLogIsEnabled
|
||||
if (this.props.dashboard.queryLogEnabled) {
|
||||
this.props.getLogs();
|
||||
}
|
||||
}
|
||||
|
||||
renderTooltip(isFiltered, rule) {
|
||||
if (rule) {
|
||||
return (isFiltered && <Tooltip text={rule}/>);
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
toggleBlocking = (type, domain) => {
|
||||
const { userRules } = this.props.filtering;
|
||||
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(`Rule removed from the custom filtering rules: ${blockingRule}`);
|
||||
} else if (!userRules.match(preparedUnblockingRule)) {
|
||||
this.props.setRules(`${userRules}${lineEnding}${unblockingRule}\n`);
|
||||
this.props.addSuccessToast(`Rule added to the custom filtering rules: ${unblockingRule}`);
|
||||
}
|
||||
|
||||
this.props.getFilteringStatus();
|
||||
}
|
||||
|
||||
renderBlockingButton(isFiltered, domain) {
|
||||
const buttonClass = isFiltered ? 'btn-outline-secondary' : 'btn-outline-danger';
|
||||
const buttonText = isFiltered ? 'Unblock' : 'Block';
|
||||
|
||||
return (
|
||||
<div className="logs__action">
|
||||
<button
|
||||
type="button"
|
||||
className={`btn btn-sm ${buttonClass}`}
|
||||
onClick={() => this.toggleBlocking(buttonText.toLowerCase(), domain)}
|
||||
>
|
||||
{buttonText}
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
renderLogs(logs) {
|
||||
const columns = [{
|
||||
Header: 'Time',
|
||||
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',
|
||||
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',
|
||||
accessor: 'type',
|
||||
maxWidth: 100,
|
||||
maxWidth: 60,
|
||||
}, {
|
||||
Header: 'Response',
|
||||
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;
|
||||
|
||||
if (isFiltered) {
|
||||
return (
|
||||
<div className="logs__row">
|
||||
{this.renderTooltip(isFiltered, rule)}
|
||||
<span className="logs__text" title={parsedFilteredReason}>
|
||||
{parsedFilteredReason}
|
||||
</span>
|
||||
</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>));
|
||||
return (
|
||||
<div className="logs__row">
|
||||
{this.renderTooltip(isFiltered, rule)}
|
||||
<ul className="list-unstyled">{liNodes}</ul>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
return 'Empty';
|
||||
return (
|
||||
<div className="logs__row">
|
||||
{this.renderTooltip(isFiltered, rule)}
|
||||
<span>Empty</span>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
}];
|
||||
filterMethod: (filter, row) => {
|
||||
if (filter.value === 'filtered') {
|
||||
// eslint-disable-next-line no-underscore-dangle
|
||||
return row._original.reason.indexOf('Filtered') === 0;
|
||||
}
|
||||
return true;
|
||||
},
|
||||
Filter: ({ filter, onChange }) =>
|
||||
<select
|
||||
onChange={event => onChange(event.target.value)}
|
||||
className="form-control"
|
||||
value={filter ? filter.value : 'all'}
|
||||
>
|
||||
<option value="all">Show all</option>
|
||||
<option value="filtered">Show filtered</option>
|
||||
</select>,
|
||||
}, {
|
||||
Header: '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"
|
||||
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 {};
|
||||
}
|
||||
return {
|
||||
className: (rowInfo.original.reason.indexOf('Filtered') === 0 ? 'red' : ''),
|
||||
};
|
||||
}}
|
||||
/>);
|
||||
}
|
||||
return undefined;
|
||||
@@ -79,34 +226,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)}
|
||||
>Disable log</button>
|
||||
<button
|
||||
className="btn btn-primary btn-sm mr-2"
|
||||
type="submit"
|
||||
onClick={this.handleDownloadButton}
|
||||
>Download log file</button>
|
||||
<button
|
||||
className="btn btn-outline-primary btn-sm"
|
||||
type="submit"
|
||||
onClick={this.getLogs}
|
||||
>Refresh</button>
|
||||
</Fragment>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<button
|
||||
className="btn btn-success btn-sm mr-2"
|
||||
type="submit"
|
||||
onClick={() => this.props.toggleLogStatus(queryLogEnabled)}
|
||||
>Enable log</button>
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
const { queryLogs, dashboard } = this.props;
|
||||
const { queryLogEnabled } = dashboard;
|
||||
return (
|
||||
<div>
|
||||
<PageTitle title="Query Log" subtitle="DNS queries log" />
|
||||
<Fragment>
|
||||
<PageTitle title="Query Log" subtitle="Last 5000 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 +283,11 @@ 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,
|
||||
};
|
||||
|
||||
export default 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,5 +1,6 @@
|
||||
import React, { Component } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import classnames from 'classnames';
|
||||
import Card from '../ui/Card';
|
||||
|
||||
export default class Upstream extends Component {
|
||||
@@ -13,22 +14,38 @@ 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,
|
||||
});
|
||||
|
||||
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."
|
||||
subtitle="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."
|
||||
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}
|
||||
>
|
||||
Test upstreams
|
||||
</button>
|
||||
<button
|
||||
className="btn btn-success btn-standart"
|
||||
type="submit"
|
||||
@@ -46,7 +63,9 @@ 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,
|
||||
};
|
||||
|
||||
@@ -17,17 +17,17 @@ export default class Settings extends Component {
|
||||
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.',
|
||||
subtitle: '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.',
|
||||
},
|
||||
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.',
|
||||
subtitle: 'AdGuard Home will check if domain contains adult materials. It uses the same privacy-friendly API as the browsing security web service.',
|
||||
},
|
||||
safesearch: {
|
||||
enabled: false,
|
||||
title: 'Enforce safe search',
|
||||
subtitle: 'AdGuard DNS can enforce safe search in the major search engines: Google, Bing, Yandex.',
|
||||
subtitle: 'AdGuard Home can enforce safe search in the following search engines: Google, Bing, Yandex.',
|
||||
},
|
||||
};
|
||||
|
||||
@@ -36,11 +36,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: 'No servers specified' });
|
||||
}
|
||||
};
|
||||
|
||||
renderSettings = (settings) => {
|
||||
@@ -61,7 +69,8 @@ export default class Settings extends Component {
|
||||
}
|
||||
|
||||
render() {
|
||||
const { settings, upstream } = this.props;
|
||||
const { settings } = this.props;
|
||||
const { upstreamDns } = this.props.dashboard;
|
||||
return (
|
||||
<Fragment>
|
||||
<PageTitle title="Settings" />
|
||||
@@ -76,9 +85,11 @@ export default class Settings extends Component {
|
||||
</div>
|
||||
</Card>
|
||||
<Upstream
|
||||
upstream={upstream}
|
||||
upstreamDns={upstreamDns}
|
||||
processingTestUpstream={settings.processingTestUpstream}
|
||||
handleUpstreamChange={this.handleUpstreamChange}
|
||||
handleUpstreamSubmit={this.handleUpstreamSubmit}
|
||||
handleUpstreamTest={this.handleUpstreamTest}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
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: 10;
|
||||
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;
|
||||
}
|
||||
38
client/src/components/Toasts/Toast.js
Normal file
38
client/src/components/Toasts/Toast.js
Normal file
@@ -0,0 +1,38 @@
|
||||
import React, { Component } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
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">
|
||||
{this.props.message}
|
||||
</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 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,48 @@
|
||||
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 {
|
||||
color: #9aa0ac;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.card-body-stats {
|
||||
position: relative;
|
||||
flex: 1 1 auto;
|
||||
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: "%";
|
||||
}
|
||||
|
||||
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,4 +1,5 @@
|
||||
import React, { Component } from 'react';
|
||||
import { REPOSITORY } from '../../helpers/constants';
|
||||
|
||||
class Footer extends Component {
|
||||
getYear = () => {
|
||||
@@ -10,22 +11,25 @@ class Footer extends Component {
|
||||
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>
|
||||
<div className="row align-items-center flex-row">
|
||||
<div className="col-12 col-lg-auto mt-3 mt-lg-0 text-center">
|
||||
© AdGuard {this.getYear()}
|
||||
<div className="row align-items-center justify-content-center">
|
||||
<div className="col-auto">
|
||||
Copyright © {this.getYear()} <a href="https://adguard.com/">AdGuard</a>
|
||||
</div>
|
||||
<div className="col-auto">
|
||||
<ul className="list-inline text-center mb-0">
|
||||
<li className="list-inline-item">
|
||||
<a href={REPOSITORY.URL} target="_blank" rel="noopener noreferrer">Homepage</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div className="col-auto">
|
||||
<a href={`${REPOSITORY.URL}/issues/new`} className="btn btn-outline-primary btn-sm" target="_blank" rel="noopener noreferrer">
|
||||
Report an issue
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
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: 0,
|
||||
}}
|
||||
axisLeft={{
|
||||
tickSize: 0,
|
||||
tickPadding: 0,
|
||||
}}
|
||||
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;
|
||||
@@ -7,11 +7,14 @@ import './Modal.css';
|
||||
|
||||
ReactModal.setAppElement('#root');
|
||||
|
||||
const initialState = {
|
||||
url: '',
|
||||
name: '',
|
||||
isUrlValid: false,
|
||||
};
|
||||
|
||||
export default class Modal extends Component {
|
||||
state = {
|
||||
url: '',
|
||||
isUrlValid: false,
|
||||
};
|
||||
state = initialState;
|
||||
|
||||
// eslint-disable-next-line
|
||||
isUrlValid = url => {
|
||||
@@ -27,33 +30,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="Enter name" onChange={this.handleNameChange} />
|
||||
<input type="text" className={inputUrlClass} placeholder="Enter URL" onChange={this.handleUrlChange} />
|
||||
{inputDescription &&
|
||||
<div className="description">
|
||||
{inputDescription}
|
||||
@@ -68,21 +86,21 @@ export default class Modal extends Component {
|
||||
);
|
||||
};
|
||||
|
||||
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,7 +110,7 @@ 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-secondary" onClick={this.closeModal}>Cancel</button>
|
||||
<button type="button" className="btn btn-success" onClick={this.handleNext} disabled={isValidForSubmit}>Add filter</button>
|
||||
</div>
|
||||
}
|
||||
|
||||
85
client/src/components/ui/Popover.css
Normal file
85
client/src/components/ui/Popover.css
Normal file
@@ -0,0 +1,85 @@
|
||||
.popover-wrap {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.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;
|
||||
min-width: 275px;
|
||||
bottom: calc(100% + 3px);
|
||||
left: 50%;
|
||||
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: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__list-title {
|
||||
margin-bottom: 3px;
|
||||
}
|
||||
|
||||
.popover__list-item {
|
||||
margin-bottom: 2px;
|
||||
}
|
||||
|
||||
.popover__list-item:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.popover__link {
|
||||
color: #66b586;
|
||||
}
|
||||
|
||||
.popover__link:hover,
|
||||
.popover__link:focus {
|
||||
color: #66b586;
|
||||
}
|
||||
54
client/src/components/ui/Popover.js
Normal file
54
client/src/components/ui/Popover.js
Normal file
@@ -0,0 +1,54 @@
|
||||
import React, { Component } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
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">
|
||||
Source: <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">
|
||||
Name: <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">
|
||||
Found in the known domains database.
|
||||
</div>
|
||||
{tracker}
|
||||
<div className="popover__list-item">
|
||||
Category: <strong>{categoryName}</strong>
|
||||
</div>
|
||||
{source}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Popover.propTypes = {
|
||||
data: PropTypes.object.isRequired,
|
||||
};
|
||||
|
||||
export default Popover;
|
||||
@@ -1,4 +1,13 @@
|
||||
.ReactTable .rt-th,
|
||||
.ReactTable .rt-td {
|
||||
padding: 10px 15px;
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
.ReactTable .rt-tbody {
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
.rt-tr-group .red {
|
||||
background-color: #fff4f2;
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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: 206px;
|
||||
}
|
||||
|
||||
@@ -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,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,22 @@
|
||||
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: 'Blocked adult websites',
|
||||
replaced_safebrowsing: 'Blocked malware/phishing',
|
||||
replaced_safesearch: 'Enforced safe 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',
|
||||
};
|
||||
|
||||
@@ -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,9 @@ export const normalizeLogs = logs => logs.map((log) => {
|
||||
time,
|
||||
question,
|
||||
answer: response,
|
||||
reason,
|
||||
client,
|
||||
rule,
|
||||
} = log;
|
||||
const { host: domain, type } = question;
|
||||
const responsesArray = response ? response.map((response) => {
|
||||
@@ -21,20 +26,26 @@ export const normalizeLogs = logs => logs.map((log) => {
|
||||
return `${type}: ${value} (ttl=${ttl})`;
|
||||
}) : [];
|
||||
return {
|
||||
time: formatTime(time),
|
||||
time,
|
||||
domain,
|
||||
type,
|
||||
response: responsesArray,
|
||||
reason,
|
||||
client,
|
||||
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 {
|
||||
@@ -63,3 +74,12 @@ export const normalizeFilteringStatus = (filteringStatus) => {
|
||||
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(' ');
|
||||
|
||||
77
client/src/helpers/trackers/adguard.json
Normal file
77
client/src/helpers/trackers/adguard.json
Normal file
@@ -0,0 +1,77 @@
|
||||
{
|
||||
"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/"
|
||||
}
|
||||
},
|
||||
"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"
|
||||
}
|
||||
}
|
||||
103
client/src/helpers/trackers/trackers.js
Normal file
103
client/src/helpers/trackers/trackers.js
Normal file
@@ -0,0 +1,103 @@
|
||||
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
|
||||
*/
|
||||
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
@@ -1,5 +1,8 @@
|
||||
import { combineReducers } from 'redux';
|
||||
import { handleActions } from 'redux-actions';
|
||||
import { loadingBarReducer } from 'react-redux-loading-bar';
|
||||
import versionCompare from 'tiny-version-compare';
|
||||
import nanoid from 'nanoid';
|
||||
|
||||
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,8 @@ const dashboard = handleActions({
|
||||
dns_port: dnsPort,
|
||||
dns_address: dnsAddress,
|
||||
querylog_enabled: queryLogEnabled,
|
||||
upstream_dns: upstreamDns,
|
||||
protection_enabled: protectionEnabled,
|
||||
} = payload;
|
||||
const newState = {
|
||||
...state,
|
||||
@@ -53,6 +58,8 @@ const dashboard = handleActions({
|
||||
dnsPort,
|
||||
dnsAddress,
|
||||
queryLogEnabled,
|
||||
upstreamDns: upstreamDns.join('\n'),
|
||||
protectionEnabled,
|
||||
};
|
||||
return newState;
|
||||
},
|
||||
@@ -98,12 +105,56 @@ 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 };
|
||||
},
|
||||
}, {
|
||||
processing: true,
|
||||
isCoreRunning: false,
|
||||
processingTopStats: true,
|
||||
processingStats: true,
|
||||
logStatusProcessing: false,
|
||||
processingVersion: true,
|
||||
processingFiltering: true,
|
||||
upstreamDns: [],
|
||||
protectionEnabled: false,
|
||||
});
|
||||
|
||||
const queryLogs = handleActions({
|
||||
@@ -172,9 +223,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
56
config.go
56
config.go
@@ -21,44 +21,48 @@ type configuration struct {
|
||||
|
||||
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"`
|
||||
|
||||
sync.Mutex `yaml:"-"`
|
||||
sync.RWMutex `yaml:"-"`
|
||||
}
|
||||
|
||||
type coreDNSConfig struct {
|
||||
Port int `yaml:"port"`
|
||||
binaryFile string
|
||||
coreFile string
|
||||
FilterFile string `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"`
|
||||
Pprof string `yaml:"pprof"`
|
||||
Pprof string `yaml:"-"`
|
||||
Cache string `yaml:"-"`
|
||||
Prometheus string `yaml:"-"`
|
||||
UpstreamDNS []string `yaml:"upstream_dns"`
|
||||
Cache string `yaml:"cache"`
|
||||
Prometheus string `yaml:"prometheus"`
|
||||
}
|
||||
|
||||
type filter struct {
|
||||
Enabled bool `json:"enabled"`
|
||||
URL string `json:"url"`
|
||||
Name string `json:"name" yaml:"name"`
|
||||
Enabled bool `json:"enabled"`
|
||||
RulesCount int `json:"rules_count" yaml:"-"`
|
||||
Name string `json:"name" yaml:"-"`
|
||||
contents []byte
|
||||
LastUpdated time.Time `json:"last_updated" yaml:"-"`
|
||||
}
|
||||
|
||||
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",
|
||||
BindPort: 3000,
|
||||
BindHost: "127.0.0.1",
|
||||
CoreDNS: coreDNSConfig{
|
||||
@@ -66,15 +70,20 @@ var config = configuration{
|
||||
binaryFile: "coredns", // only filename, no path
|
||||
coreFile: "Corefile", // only filename, no path
|
||||
FilterFile: "dnsfilter.txt", // only filename, no path
|
||||
ProtectionEnabled: true,
|
||||
FilteringEnabled: true,
|
||||
SafeBrowsingEnabled: true,
|
||||
SafeBrowsingEnabled: false,
|
||||
BlockedResponseTTL: 10, // in seconds
|
||||
QueryLogEnabled: true,
|
||||
UpstreamDNS: defaultDNS,
|
||||
Cache: "cache",
|
||||
Prometheus: "prometheus :9153",
|
||||
},
|
||||
Filters: []filter{
|
||||
{Enabled: true, URL: "https://filters.adtidy.org/windows/filters/15.txt"},
|
||||
{Enabled: true, URL: "https://adguardteam.github.io/AdGuardSDNSFilter/Filters/filter.txt"},
|
||||
{Enabled: false, URL: "https://adaway.org/hosts.txt", Name: "AdAway"},
|
||||
{Enabled: false, URL: "https://hosts-file.net/ad_servers.txt", Name: "hpHosts - Ad and Tracking servers only"},
|
||||
{Enabled: false, URL: "http://www.malwaredomainlist.com/hostslist/hosts.txt", Name: "MalwareDomainList.com Hosts List"},
|
||||
},
|
||||
}
|
||||
|
||||
@@ -108,11 +117,16 @@ func writeConfig() error {
|
||||
log.Printf("Couldn't generate YAML file: %s", err)
|
||||
return err
|
||||
}
|
||||
err = ioutil.WriteFile(configfile, yamlText, 0644)
|
||||
err = ioutil.WriteFile(configfile+".tmp", yamlText, 0644)
|
||||
if err != nil {
|
||||
log.Printf("Couldn't write YAML config: %s", err)
|
||||
return err
|
||||
}
|
||||
err = os.Rename(configfile+".tmp", configfile)
|
||||
if err != nil {
|
||||
log.Printf("Couldn't rename YAML config: %s", err)
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -127,10 +141,14 @@ func writeCoreDNSConfig() error {
|
||||
log.Printf("Couldn't generate DNS config: %s", err)
|
||||
return err
|
||||
}
|
||||
err = ioutil.WriteFile(corefile, []byte(configtext), 0644)
|
||||
err = ioutil.WriteFile(corefile+".tmp", []byte(configtext), 0644)
|
||||
if err != nil {
|
||||
log.Printf("Couldn't write DNS config: %s", err)
|
||||
}
|
||||
err = os.Rename(corefile+".tmp", corefile)
|
||||
if err != nil {
|
||||
log.Printf("Couldn't rename DNS config: %s", err)
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -148,14 +166,18 @@ func writeAllConfigs() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
const coreDNSConfigTemplate = `. {
|
||||
{{if .FilteringEnabled}}dnsfilter {{.FilterFile}} {
|
||||
const coreDNSConfigTemplate = `.:{{.Port}} {
|
||||
{{if .ProtectionEnabled}}dnsfilter {{if .FilteringEnabled}}{{.FilterFile}}{{end}} {
|
||||
{{if .SafeBrowsingEnabled}}safebrowsing{{end}}
|
||||
{{if .ParentalEnabled}}parental {{.ParentalSensitivity}}{{end}}
|
||||
{{if .SafeSearchEnabled}}safesearch{{end}}
|
||||
{{if .QueryLogEnabled}}querylog{{end}}
|
||||
blocked_ttl {{.BlockedResponseTTL}}
|
||||
}{{end}}
|
||||
{{.Pprof}}
|
||||
hosts {
|
||||
fallthrough
|
||||
}
|
||||
{{if .UpstreamDNS}}forward . {{range .UpstreamDNS}}{{.}} {{end}}{{end}}
|
||||
{{.Cache}}
|
||||
{{.Prometheus}}
|
||||
@@ -173,8 +195,10 @@ func generateCoreDNSConfigText() (string, error) {
|
||||
}
|
||||
|
||||
var configBytes bytes.Buffer
|
||||
temporaryConfig := config.CoreDNS
|
||||
temporaryConfig.FilterFile = filepath.Join(config.ourBinaryDir, config.CoreDNS.FilterFile)
|
||||
// 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
|
||||
|
||||
844
control.go
844
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/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",
|
||||
"dnsfilter",
|
||||
"dnstap",
|
||||
"chaos",
|
||||
"loadbalance",
|
||||
"cache",
|
||||
"rewrite",
|
||||
"dnssec",
|
||||
"autopath",
|
||||
"template",
|
||||
"hosts",
|
||||
"file",
|
||||
"auto",
|
||||
"secondary",
|
||||
"loop",
|
||||
"forward",
|
||||
"proxy",
|
||||
"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
|
||||
}
|
||||
err = writeFilterFile()
|
||||
if err != nil {
|
||||
errortext := fmt.Errorf("Couldn't write filter file: %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,68 @@ func init() {
|
||||
})
|
||||
}
|
||||
|
||||
type Plugin struct {
|
||||
type cacheEntry struct {
|
||||
answer []dns.RR
|
||||
lastUpdated time.Time
|
||||
}
|
||||
|
||||
var (
|
||||
lookupCacheTime = time.Minute * 30
|
||||
lookupCache = map[string]cacheEntry{}
|
||||
)
|
||||
|
||||
type plugSettings struct {
|
||||
SafeBrowsingBlockHost string
|
||||
ParentalBlockHost string
|
||||
QueryLogEnabled bool
|
||||
BlockedTTL uint32 // in seconds, default 3600
|
||||
}
|
||||
|
||||
type plug struct {
|
||||
d *dnsfilter.Dnsfilter
|
||||
Next plugin.Handler
|
||||
upstream upstream.Upstream
|
||||
hosts map[string]net.IP
|
||||
settings plugSettings
|
||||
|
||||
SafeBrowsingBlockHost string
|
||||
ParentalBlockHost string
|
||||
QueryLogEnabled bool
|
||||
sync.RWMutex
|
||||
}
|
||||
|
||||
var defaultPlugin = Plugin{
|
||||
var defaultPluginSettings = plugSettings{
|
||||
SafeBrowsingBlockHost: "safebrowsing.block.dns.adguard.com",
|
||||
ParentalBlockHost: "family.block.dns.adguard.com",
|
||||
BlockedTTL: 3600, // in seconds
|
||||
}
|
||||
|
||||
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(),
|
||||
hosts: make(map[string]net.IP),
|
||||
}
|
||||
|
||||
var filterFileName string
|
||||
filterFileNames := []string{}
|
||||
for c.Next() {
|
||||
args := c.RemainingArgs()
|
||||
if len(args) == 0 {
|
||||
// must have at least one argument
|
||||
return nil, c.ArgErr()
|
||||
if len(args) > 0 {
|
||||
filterFileNames = append(filterFileNames, args...)
|
||||
}
|
||||
filterFileName = args[0]
|
||||
for c.NextBlock() {
|
||||
switch c.Val() {
|
||||
case "safebrowsing":
|
||||
d.d.EnableSafeBrowsing()
|
||||
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()
|
||||
p.d.EnableSafeSearch()
|
||||
case "parental":
|
||||
if !c.NextArg() {
|
||||
return nil, c.ArgErr()
|
||||
@@ -120,7 +111,7 @@ func setupPlugin(c *caddy.Controller) (*Plugin, error) {
|
||||
if err != nil {
|
||||
return nil, c.ArgErr()
|
||||
}
|
||||
err = d.d.EnableParental(sensitivity)
|
||||
err = p.d.EnableParental(sensitivity)
|
||||
if err != nil {
|
||||
return nil, c.ArgErr()
|
||||
}
|
||||
@@ -128,87 +119,119 @@ 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()
|
||||
}
|
||||
blockttl, err := strconv.ParseUint(c.Val(), 10, 32)
|
||||
if err != nil {
|
||||
return nil, c.ArgErr()
|
||||
}
|
||||
p.settings.BlockedTTL = uint32(blockttl)
|
||||
case "querylog":
|
||||
d.QueryLogEnabled = true
|
||||
once.Do(func() {
|
||||
go startQueryLogServer() // TODO: how to handle errors?
|
||||
})
|
||||
p.settings.QueryLogEnabled = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
file, err := os.Open(filterFileName)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer file.Close()
|
||||
log.Printf("filterFileNames = %+v", filterFileNames)
|
||||
|
||||
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
|
||||
}
|
||||
for i, filterFileName := range filterFileNames {
|
||||
file, err := os.Open(filterFileName)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
count := 0
|
||||
scanner := bufio.NewScanner(file)
|
||||
for scanner.Scan() {
|
||||
text := scanner.Text()
|
||||
if p.parseEtcHosts(text) {
|
||||
continue
|
||||
}
|
||||
err = p.d.AddRule(text, uint32(i))
|
||||
if err == dnsfilter.ErrInvalidSyntax {
|
||||
continue
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
count++
|
||||
}
|
||||
log.Printf("Added %d rules from %s", count, filterFileName)
|
||||
|
||||
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 {
|
||||
func (p *plug) parseEtcHosts(text string) bool {
|
||||
if pos := strings.IndexByte(text, '#'); pos != -1 {
|
||||
text = text[0:pos]
|
||||
}
|
||||
@@ -221,17 +244,31 @@ func (d *Plugin) parseEtcHosts(text string) bool {
|
||||
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
|
||||
// debug logging for duplicate values, pretty common if you subscribe to many hosts files
|
||||
// if val, ok := p.hosts[host]; ok {
|
||||
// log.Printf("warning: host %s already has value %s, will overwrite it with %s", host, val, addr)
|
||||
// }
|
||||
p.hosts[host] = addr
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func (d *Plugin) OnShutdown() error {
|
||||
d.d.Destroy()
|
||||
d.d = nil
|
||||
func (p *plug) onShutdown() error {
|
||||
p.Lock()
|
||||
p.d.Destroy()
|
||||
p.d = nil
|
||||
p.Unlock()
|
||||
return 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
|
||||
}
|
||||
|
||||
@@ -239,7 +276,7 @@ type statsFunc func(ch interface{}, name string, text string, value float64, val
|
||||
|
||||
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 +285,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 +297,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)
|
||||
@@ -295,20 +337,29 @@ func (d *Plugin) replaceHostWithValAndReply(ctx context.Context, w dns.ResponseW
|
||||
records = append(records, result)
|
||||
} else {
|
||||
// this is a domain name, need to look it up
|
||||
req := new(dns.Msg)
|
||||
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())
|
||||
if err != nil {
|
||||
log.Printf("Got error %s\n", err)
|
||||
return dns.RcodeServerFailure, fmt.Errorf("plugin/dnsfilter: %s", err)
|
||||
}
|
||||
if result != nil {
|
||||
for _, answer := range result.Answer {
|
||||
answer.Header().Name = question.Name
|
||||
cacheentry := lookupCache[val]
|
||||
if time.Since(cacheentry.lastUpdated) > lookupCacheTime {
|
||||
req := new(dns.Msg)
|
||||
req.SetQuestion(dns.Fqdn(val), question.Qtype)
|
||||
req.RecursionDesired = true
|
||||
reqstate := request.Request{W: w, Req: req, Context: ctx}
|
||||
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)
|
||||
}
|
||||
records = result.Answer
|
||||
if result != nil {
|
||||
for _, answer := range result.Answer {
|
||||
answer.Header().Name = question.Name
|
||||
}
|
||||
records = result.Answer
|
||||
cacheentry.answer = result.Answer
|
||||
cacheentry.lastUpdated = time.Now()
|
||||
lookupCache[val] = cacheentry
|
||||
}
|
||||
} else {
|
||||
// get from cache
|
||||
records = cacheentry.answer
|
||||
}
|
||||
}
|
||||
m := new(dns.Msg)
|
||||
@@ -327,9 +378,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 +388,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 +412,127 @@ 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 DNS request with != 1 questions")
|
||||
}
|
||||
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}
|
||||
p.RUnlock()
|
||||
return rcode, dnsfilter.Result{Reason: dnsfilter.FilteredSafeSearch}, err
|
||||
}
|
||||
p.RUnlock()
|
||||
|
||||
// is it in hosts?
|
||||
if val, ok := d.hosts[host]; ok {
|
||||
if val, ok := p.hosts[host]; ok {
|
||||
// it is, if it's a loopback host, reply with NXDOMAIN
|
||||
if val.IsLoopback() {
|
||||
rcode, err := writeNXdomain(ctx, w, r)
|
||||
// TODO: research if it's better than 127.0.0.1
|
||||
if false && val.IsLoopback() {
|
||||
rcode, err := p.writeNXdomain(ctx, w, r)
|
||||
if err != nil {
|
||||
return rcode, err, dnsfilter.Result{}
|
||||
return rcode, dnsfilter.Result{}, err
|
||||
}
|
||||
return rcode, err, dnsfilter.Result{Reason: dnsfilter.FilteredInvalid}
|
||||
return rcode, dnsfilter.Result{Reason: dnsfilter.FilteredInvalid}, err
|
||||
}
|
||||
// it's not a loopback host, replace it with value specified
|
||||
rcode, err := d.replaceHostWithValAndReply(ctx, w, r, host, val.String(), question)
|
||||
rcode, err := p.replaceHostWithValAndReply(ctx, w, r, host, val.String(), question)
|
||||
if err != nil {
|
||||
return rcode, err, dnsfilter.Result{}
|
||||
return rcode, dnsfilter.Result{}, err
|
||||
}
|
||||
return rcode, err, dnsfilter.Result{Reason: dnsfilter.FilteredSafeSearch}
|
||||
// TODO: This must be handled in the dnsfilter and not here!
|
||||
rule := val.String() + " " + host
|
||||
return rcode, dnsfilter.Result{Reason: dnsfilter.FilteredBlackList, Rule: rule}, err
|
||||
}
|
||||
|
||||
// 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{}
|
||||
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:
|
||||
// return NXdomain
|
||||
rcode, err := p.writeNXdomain(ctx, w, r)
|
||||
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
|
||||
}
|
||||
|
||||
// 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{}
|
||||
} 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
|
||||
}
|
||||
|
||||
// blacklist
|
||||
if result.IsFiltered == true && result.Reason == dnsfilter.FilteredBlackList {
|
||||
rcode, err := writeNXdomain(ctx, w, r)
|
||||
if err != nil {
|
||||
return rcode, err, dnsfilter.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 +566,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,7 +20,8 @@ func TestSetup(t *testing.T) {
|
||||
config string
|
||||
failing bool
|
||||
}{
|
||||
{`dnsfilter`, true},
|
||||
{`dnsfilter`, false},
|
||||
{`dnsfilter /dev/nonexistent/abcdef`, true},
|
||||
{`dnsfilter ../tests/dns.txt`, false},
|
||||
{`dnsfilter ../tests/dns.txt { safebrowsing }`, false},
|
||||
{`dnsfilter ../tests/dns.txt { parental }`, true},
|
||||
@@ -46,10 +47,10 @@ func TestEtcHostsParse(t *testing.T) {
|
||||
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)
|
||||
}
|
||||
|
||||
@@ -80,10 +81,10 @@ func TestEtcHostsFilter(t *testing.T) {
|
||||
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)
|
||||
}
|
||||
|
||||
@@ -126,11 +127,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
|
||||
queryLogAPIPort = "8618" // 8618 is sha512sum of "querylog" then each byte summed
|
||||
)
|
||||
|
||||
var (
|
||||
logBufferLock sync.RWMutex
|
||||
logBuffer []*logEntry
|
||||
|
||||
queryLogCache []*logEntry
|
||||
queryLogLock sync.RWMutex
|
||||
queryLogTime time.Time
|
||||
)
|
||||
|
||||
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()
|
||||
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{}{
|
||||
"reason": entry.Result.Reason.String(),
|
||||
"elapsed_ms": 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
|
||||
}
|
||||
|
||||
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],
|
||||
@@ -125,22 +221,20 @@ func handler(w http.ResponseWriter, r *http.Request) {
|
||||
if err != nil {
|
||||
errortext := fmt.Sprintf("Unable to write response json: %s", err)
|
||||
log.Println(errortext)
|
||||
http.Error(w, errortext, 500)
|
||||
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)
|
||||
}
|
||||
}
|
||||
@@ -4,7 +4,6 @@ import (
|
||||
"errors"
|
||||
"log"
|
||||
"strconv"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
// ratelimiting and per-ip buckets
|
||||
@@ -29,8 +28,8 @@ 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)
|
||||
@@ -44,7 +43,7 @@ func (p *Plugin) ServeDNS(ctx context.Context, w dns.ResponseWriter, r *dns.Msg)
|
||||
return plugin.NextOrFailure(p.Name(), p.Next, ctx, w, r)
|
||||
}
|
||||
|
||||
func (p *Plugin) allowRequest(ip string) (bool, error) {
|
||||
func (p *plug) allowRequest(ip string) (bool, error) {
|
||||
if _, found := tokenBuckets.Get(ip); !found {
|
||||
tokenBuckets.Set(ip, rate.New(p.ratelimit, time.Second), time.Hour)
|
||||
}
|
||||
@@ -59,7 +58,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,7 +79,7 @@ func init() {
|
||||
})
|
||||
}
|
||||
|
||||
type Plugin struct {
|
||||
type plug struct {
|
||||
Next plugin.Handler
|
||||
|
||||
// configuration for creating above
|
||||
@@ -88,7 +87,7 @@ type Plugin struct {
|
||||
}
|
||||
|
||||
func setup(c *caddy.Controller) error {
|
||||
p := &Plugin{ratelimit: defaultRatelimit}
|
||||
p := &plug{ratelimit: defaultRatelimit}
|
||||
config := dnsserver.GetConfig(c)
|
||||
|
||||
for c.Next() {
|
||||
@@ -109,22 +108,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: "ratelimit",
|
||||
@@ -134,9 +131,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" }
|
||||
|
||||
@@ -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")
|
||||
@@ -41,9 +41,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 +54,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 +63,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 +86,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"
|
||||
)
|
||||
|
||||
|
||||
@@ -16,6 +16,7 @@ import (
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
_ "github.com/benburkert/dns/init"
|
||||
"github.com/bluele/gcache"
|
||||
"golang.org/x/net/publicsuffix"
|
||||
)
|
||||
@@ -28,9 +29,13 @@ 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 rule is invalid
|
||||
var ErrInvalidSyntax = errors.New("dnsfilter: invalid rule syntax")
|
||||
|
||||
// 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,15 +43,16 @@ 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
|
||||
@@ -55,19 +61,24 @@ type Rule struct {
|
||||
options []string // optional options after $
|
||||
|
||||
// parsed options
|
||||
apps []string
|
||||
isWhitelist bool
|
||||
isImportant bool
|
||||
apps []string
|
||||
|
||||
// user-supplied data
|
||||
listID uint32
|
||||
|
||||
// suffix matching
|
||||
isSuffix bool
|
||||
suffix string
|
||||
|
||||
// compiled regexp
|
||||
compiled *regexp.Regexp
|
||||
|
||||
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 +86,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 +94,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]*rule // rule storage, not used for matching, needs to be key->value
|
||||
storageMutex sync.RWMutex
|
||||
|
||||
// rules are checked against these lists in the order defined here
|
||||
@@ -94,12 +106,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 +131,29 @@ 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"`
|
||||
Reason Reason `json:",omitempty"`
|
||||
Rule string `json:",omitempty"`
|
||||
}
|
||||
|
||||
// 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,19 +199,19 @@ func (d *Dnsfilter) CheckHost(host string) (Result, error) {
|
||||
//
|
||||
|
||||
type rulesTable struct {
|
||||
rulesByShortcut map[string][]*Rule
|
||||
rulesLeftovers []*Rule
|
||||
rulesByShortcut map[string][]*rule
|
||||
rulesLeftovers []*rule
|
||||
sync.RWMutex
|
||||
}
|
||||
|
||||
func newRulesTable() *rulesTable {
|
||||
return &rulesTable{
|
||||
rulesByShortcut: make(map[string][]*Rule),
|
||||
rulesLeftovers: make([]*Rule, 0),
|
||||
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 {
|
||||
r.rulesByShortcut[rule.shortcut] = append(r.rulesByShortcut[rule.shortcut], rule)
|
||||
@@ -292,7 +306,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 +344,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 +365,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 +390,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,14 +424,23 @@ func (rule *Rule) compile() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (rule *Rule) match(host string) (Result, error) {
|
||||
func (rule *rule) match(host string) (Result, error) {
|
||||
res := Result{}
|
||||
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
|
||||
@@ -439,7 +471,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 +487,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 +519,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 +552,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 +586,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 +598,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 +613,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 +651,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 +667,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
|
||||
}
|
||||
|
||||
@@ -637,7 +693,7 @@ func (d *Dnsfilter) AddRule(input string, filterListID uint32) error {
|
||||
return ErrInvalidSyntax
|
||||
}
|
||||
|
||||
rule := Rule{
|
||||
rule := rule{
|
||||
text: input, // will be modified
|
||||
originalText: input,
|
||||
listID: filterListID,
|
||||
@@ -701,10 +757,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]*rule)
|
||||
d.important = newRulesTable()
|
||||
d.whiteList = newRulesTable()
|
||||
d.blackList = newRulesTable()
|
||||
@@ -723,21 +780,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 +814,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 +829,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
|
||||
//
|
||||
@@ -112,6 +316,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 +335,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 +345,72 @@ func TestSanityCheck(t *testing.T) {
|
||||
d.checkAddRuleFail(t, "lkfaojewhoawehfwacoefawr$@#$@3413841384")
|
||||
}
|
||||
|
||||
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()
|
||||
@@ -217,52 +496,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 +589,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 +620,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 +675,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())
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -569,6 +808,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 +951,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"
|
||||
)
|
||||
@@ -49,33 +46,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,38 @@ 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]
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
181
helpers.go
181
helpers.go
@@ -3,9 +3,12 @@ package main
|
||||
import (
|
||||
"bufio"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"sort"
|
||||
"os"
|
||||
"path"
|
||||
"runtime"
|
||||
"strings"
|
||||
)
|
||||
|
||||
@@ -48,142 +51,44 @@ func ensureDELETE(handler func(http.ResponseWriter, *http.Request)) func(http.Re
|
||||
return ensure("DELETE", handler)
|
||||
}
|
||||
|
||||
// --------------------------
|
||||
// helper functions for stats
|
||||
// --------------------------
|
||||
func computeRate(input []float64) []float64 {
|
||||
output := make([]float64, 0)
|
||||
for i := len(input) - 2; i >= 0; i-- {
|
||||
value := input[i]
|
||||
diff := value - input[i+1]
|
||||
output = append([]float64{diff}, output...)
|
||||
}
|
||||
return output
|
||||
}
|
||||
|
||||
func generateMapFromSnap(snap statsSnapshot) map[string]interface{} {
|
||||
var avgProcessingTime float64
|
||||
if snap.processingTimeCount > 0 {
|
||||
avgProcessingTime = snap.processingTimeSum / snap.processingTimeCount
|
||||
}
|
||||
|
||||
result := map[string]interface{}{
|
||||
"dns_queries": snap.totalRequests,
|
||||
"blocked_filtering": snap.filteredLists,
|
||||
"replaced_safebrowsing": snap.filteredSafebrowsing,
|
||||
"replaced_safesearch": snap.filteredSafesearch,
|
||||
"replaced_parental": snap.filteredParental,
|
||||
"avg_processing_time": avgProcessingTime,
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
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 := computeRate(stats.processingTimeCount[start:end])
|
||||
sum := computeRate(stats.processingTimeSum[start:end])
|
||||
for i := 0; i < len(count); i++ {
|
||||
var avg float64
|
||||
if count[i] != 0 {
|
||||
avg = sum[i] / count[i]
|
||||
avg *= 1000
|
||||
func optionalAuth(handler func(http.ResponseWriter, *http.Request)) func(http.ResponseWriter, *http.Request) {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
if config.AuthName == "" || config.AuthPass == "" {
|
||||
handler(w, r)
|
||||
return
|
||||
}
|
||||
avgProcessingTime = append(avgProcessingTime, avg)
|
||||
}
|
||||
|
||||
result := map[string]interface{}{
|
||||
"dns_queries": computeRate(stats.totalRequests[start:end]),
|
||||
"blocked_filtering": computeRate(stats.filteredLists[start:end]),
|
||||
"replaced_safebrowsing": computeRate(stats.filteredSafebrowsing[start:end]),
|
||||
"replaced_safesearch": computeRate(stats.filteredSafesearch[start:end]),
|
||||
"replaced_parental": computeRate(stats.filteredParental[start:end]),
|
||||
"avg_processing_time": avgProcessingTime,
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func produceTop(m map[string]int, top int) map[string]int {
|
||||
toMarshal := map[string]int{}
|
||||
topKeys := sortByValue(m)
|
||||
for i, k := range topKeys {
|
||||
if i == top {
|
||||
break
|
||||
user, pass, ok := r.BasicAuth()
|
||||
if !ok || user != config.AuthName || pass != config.AuthPass {
|
||||
w.Header().Set("WWW-Authenticate", `Basic realm="dnsfilter"`)
|
||||
w.WriteHeader(http.StatusUnauthorized)
|
||||
w.Write([]byte("Unauthorised.\n"))
|
||||
return
|
||||
}
|
||||
toMarshal[k] = m[k]
|
||||
handler(w, r)
|
||||
}
|
||||
return toMarshal
|
||||
}
|
||||
|
||||
// -------------------------------------
|
||||
// helper functions for querylog parsing
|
||||
// -------------------------------------
|
||||
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
|
||||
type authHandler struct {
|
||||
handler http.Handler
|
||||
}
|
||||
|
||||
func getHost(entry map[string]interface{}) string {
|
||||
q, ok := entry["question"]
|
||||
if !ok {
|
||||
return ""
|
||||
func (a *authHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
if config.AuthName == "" || config.AuthPass == "" {
|
||||
a.handler.ServeHTTP(w, r)
|
||||
return
|
||||
}
|
||||
question, ok := q.(map[string]interface{})
|
||||
if !ok {
|
||||
return ""
|
||||
user, pass, ok := r.BasicAuth()
|
||||
if !ok || user != config.AuthName || pass != config.AuthPass {
|
||||
w.Header().Set("WWW-Authenticate", `Basic realm="dnsfilter"`)
|
||||
w.WriteHeader(http.StatusUnauthorized)
|
||||
w.Write([]byte("Unauthorised.\n"))
|
||||
return
|
||||
}
|
||||
h, ok := question["host"]
|
||||
if !ok {
|
||||
return ""
|
||||
}
|
||||
host, ok := h.(string)
|
||||
if !ok {
|
||||
return ""
|
||||
}
|
||||
return host
|
||||
a.handler.ServeHTTP(w, r)
|
||||
}
|
||||
|
||||
func getReason(entry map[string]interface{}) string {
|
||||
r, ok := entry["reason"]
|
||||
if !ok {
|
||||
return ""
|
||||
}
|
||||
reason, ok := r.(string)
|
||||
if !ok {
|
||||
return ""
|
||||
}
|
||||
return reason
|
||||
}
|
||||
|
||||
func getClient(entry map[string]interface{}) string {
|
||||
c, ok := entry["client"]
|
||||
if !ok {
|
||||
return ""
|
||||
}
|
||||
client, ok := c.(string)
|
||||
if !ok {
|
||||
return ""
|
||||
}
|
||||
return client
|
||||
func optionalAuthHandler(handler http.Handler) http.Handler {
|
||||
return &authHandler{handler}
|
||||
}
|
||||
|
||||
// -------------------------------------------------
|
||||
@@ -208,3 +113,27 @@ func parseParametersFromBody(r io.Reader) (map[string]string, error) {
|
||||
|
||||
return parameters, nil
|
||||
}
|
||||
|
||||
// ---------------------
|
||||
// debug logging helpers
|
||||
// ---------------------
|
||||
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.Fprint(os.Stderr, buf.String())
|
||||
}
|
||||
|
||||
@@ -29,7 +29,7 @@
|
||||
|
||||
| Description | Value |
|
||||
| -------------- | ------------ |
|
||||
| Version of AdGuard DNS server:| (e.g. v1.0)
|
||||
| Version of AdGuard Home server:| (e.g. v1.0)
|
||||
| How did you setup DNS configuration:| (System/Router/IoT)
|
||||
| If it's a router or IoT, please write device model:| (e.g. Raspberry Pi 3 Model B)
|
||||
| Operating system and version:| (e.g. Ubuntu 18.04.1)
|
||||
|
||||
98
openapi.yaml
98
openapi.yaml
@@ -1,7 +1,7 @@
|
||||
swagger: '2.0'
|
||||
info:
|
||||
title: 'AdGuard DNS'
|
||||
description: 'Control AdGuard DNS server with this API'
|
||||
title: 'AdGuard Home'
|
||||
description: 'Control AdGuard Home server with this API'
|
||||
version: 0.0.0
|
||||
basePath: /control
|
||||
schemes:
|
||||
@@ -25,39 +25,41 @@ tags:
|
||||
name: safesearch
|
||||
description: 'Enforce family-friendly results in search engines'
|
||||
paths:
|
||||
/start:
|
||||
post:
|
||||
tags:
|
||||
- global
|
||||
operationId: start
|
||||
summary: 'Start DNS server'
|
||||
responses:
|
||||
200:
|
||||
description: OK
|
||||
/stop:
|
||||
post:
|
||||
tags:
|
||||
- global
|
||||
operationId: stop
|
||||
summary: 'Stop DNS server'
|
||||
responses:
|
||||
200:
|
||||
description: OK
|
||||
/restart:
|
||||
post:
|
||||
tags:
|
||||
- global
|
||||
operationId: restart
|
||||
summary: 'Restart DNS server'
|
||||
responses:
|
||||
200:
|
||||
description: OK
|
||||
/status:
|
||||
get:
|
||||
tags:
|
||||
- global
|
||||
operationId: status
|
||||
summary: 'Get DNS server status'
|
||||
responses:
|
||||
200:
|
||||
description: OK
|
||||
examples:
|
||||
application/json:
|
||||
dns_address: 127.0.0.1
|
||||
dns_port: 53
|
||||
protection_enabled: true
|
||||
querylog_enabled: true
|
||||
running: true
|
||||
upstream_dns:
|
||||
- 1.1.1.1
|
||||
- 1.0.0.1
|
||||
version: "v0.1"
|
||||
/enable_protection:
|
||||
post:
|
||||
tags:
|
||||
-global
|
||||
operationId: enableProtection
|
||||
summary: "Enable protection (turns on dnsfilter module in coredns)"
|
||||
responses:
|
||||
200:
|
||||
description: OK
|
||||
/disable_protection:
|
||||
post:
|
||||
tags:
|
||||
-global
|
||||
operationId: disableProtection
|
||||
summary: "Disable protection (turns off filtering, sb, parental, safesearch temporarily by disabling dnsfilter module in coredns)"
|
||||
responses:
|
||||
200:
|
||||
description: OK
|
||||
@@ -160,6 +162,35 @@ paths:
|
||||
responses:
|
||||
200:
|
||||
description: OK
|
||||
/test_upstream_dns:
|
||||
post:
|
||||
tags:
|
||||
- global
|
||||
operationId: testUpstreamDNS
|
||||
summary: 'Test upstream DNS'
|
||||
consumes:
|
||||
- text/plain
|
||||
parameters:
|
||||
- in: body
|
||||
name: upstream
|
||||
description: 'Upstream servers, separated by newline or space, port is optional after colon'
|
||||
schema:
|
||||
type: string
|
||||
example: |
|
||||
1.1.1.1
|
||||
1.0.0.1
|
||||
8.8.8.8 8.8.4.4
|
||||
192.168.1.104:53535
|
||||
responses:
|
||||
200:
|
||||
description: 'Status of testing each requested server, with "OK" meaning that server works, any other text means an error.'
|
||||
examples:
|
||||
application/json:
|
||||
1.1.1.1: OK
|
||||
1.0.0.1: OK
|
||||
8.8.8.8: OK
|
||||
8.8.4.4: OK
|
||||
"192.168.1.104:53535": "Couldn't communicate with DNS server"
|
||||
/stats_top:
|
||||
get:
|
||||
tags:
|
||||
@@ -277,6 +308,15 @@ paths:
|
||||
- 123
|
||||
- 123
|
||||
- 123
|
||||
/stats_reset:
|
||||
post:
|
||||
tags:
|
||||
-global
|
||||
operationId: statsReset
|
||||
summary: "Reset all statistics to zeroes"
|
||||
responses:
|
||||
200:
|
||||
description: OK
|
||||
/filtering/enable:
|
||||
post:
|
||||
tags:
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
#!/bin/bash
|
||||
set -e -x -o pipefail
|
||||
echo "executing $0"
|
||||
@@ -1,3 +0,0 @@
|
||||
#!/bin/bash
|
||||
set -e -x -o pipefail
|
||||
echo "executing $0"
|
||||
@@ -1,3 +0,0 @@
|
||||
#!/bin/bash
|
||||
set -e -x -o pipefail
|
||||
echo "executing $0"
|
||||
@@ -1,3 +0,0 @@
|
||||
#!/bin/bash
|
||||
set -e -x -o pipefail
|
||||
echo "executing $0"
|
||||
2
scripts/whotracksme/.gitignore
vendored
Normal file
2
scripts/whotracksme/.gitignore
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
node_modules
|
||||
whotracksme.json
|
||||
12
scripts/whotracksme/README.md
Normal file
12
scripts/whotracksme/README.md
Normal file
@@ -0,0 +1,12 @@
|
||||
## Whotracks.me database converter
|
||||
|
||||
A simple script that converts the Ghostery/Cliqz trackers database to a json format.
|
||||
|
||||
### Usage
|
||||
|
||||
```
|
||||
yarn install
|
||||
node index.js
|
||||
```
|
||||
|
||||
You'll find the output in the `whotracksmedb.json` file.
|
||||
76
scripts/whotracksme/index.js
Normal file
76
scripts/whotracksme/index.js
Normal file
@@ -0,0 +1,76 @@
|
||||
const fs = require('fs');
|
||||
const sqlite3 = require('sqlite3').verbose();
|
||||
const downloadFileSync = require('download-file-sync');
|
||||
|
||||
const INPUT_SQL_URL = 'https://raw.githubusercontent.com/cliqz-oss/whotracks.me/master/whotracksme/data/assets/trackerdb.sql';
|
||||
const OUTPUT_PATH = 'whotracksme.json';
|
||||
|
||||
console.log('Downloading ' + INPUT_SQL_URL);
|
||||
let trackersDbSql = downloadFileSync(INPUT_SQL_URL).toString();
|
||||
|
||||
let transformToSqlite = function(sql) {
|
||||
sql = sql.trim();
|
||||
|
||||
if (sql.indexOf("CREATE TABLE") >= 0) {
|
||||
sql = sql.replace(/UNIQUE/g, '');
|
||||
}
|
||||
|
||||
return sql;
|
||||
}
|
||||
|
||||
let whotracksme = {
|
||||
timeUpdated: new Date().toISOString(),
|
||||
categories: {},
|
||||
trackers: {},
|
||||
trackerDomains: {}
|
||||
};
|
||||
|
||||
console.log('Initializing the in-memory trackers database');
|
||||
let db = new sqlite3.Database(':memory:');
|
||||
db.serialize(function() {
|
||||
trackersDbSql.split(/;\s*$/gm).forEach(function(sql) {
|
||||
sql = transformToSqlite(sql);
|
||||
db.run(sql, function() {});
|
||||
});
|
||||
|
||||
db.each("SELECT * FROM categories", function(err, row) {
|
||||
if (err) {
|
||||
console.error(err);
|
||||
return;
|
||||
}
|
||||
|
||||
whotracksme.categories[row.id] = row.name;
|
||||
});
|
||||
|
||||
db.each("SELECT * FROM trackers", function(err, row) {
|
||||
if (err) {
|
||||
console.error(err);
|
||||
return;
|
||||
}
|
||||
|
||||
whotracksme.trackers[row.id] = {
|
||||
"name": row.name,
|
||||
"categoryId": row.category_id,
|
||||
"url": row.website_url
|
||||
};
|
||||
});
|
||||
|
||||
db.each("SELECT * FROM tracker_domains", function(err, row) {
|
||||
if (err) {
|
||||
console.error(err);
|
||||
return;
|
||||
}
|
||||
|
||||
whotracksme.trackerDomains[row.domain] = row.tracker;
|
||||
});
|
||||
});
|
||||
|
||||
db.close(function(err) {
|
||||
if (err) {
|
||||
console.error(err);
|
||||
return;
|
||||
}
|
||||
|
||||
fs.writeFileSync(OUTPUT_PATH, JSON.stringify(whotracksme, 0, 4));
|
||||
console.log('Trackers json file has been updated: ' + OUTPUT_PATH);
|
||||
});
|
||||
15
scripts/whotracksme/package.json
Normal file
15
scripts/whotracksme/package.json
Normal file
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"name": "whotracksme-converter",
|
||||
"version": "1.0.0",
|
||||
"description": "",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
"test": "echo \"Error: no test specified\" && exit 1"
|
||||
},
|
||||
"author": "",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"download-file-sync": "^1.0.4",
|
||||
"sqlite3": "^4.0.2"
|
||||
}
|
||||
}
|
||||
674
scripts/whotracksme/yarn.lock
Normal file
674
scripts/whotracksme/yarn.lock
Normal file
@@ -0,0 +1,674 @@
|
||||
# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
|
||||
# yarn lockfile v1
|
||||
|
||||
|
||||
abbrev@1:
|
||||
version "1.1.1"
|
||||
resolved "https://registry.yarnpkg.com/abbrev/-/abbrev-1.1.1.tgz#f8f2c887ad10bf67f634f005b6987fed3179aac8"
|
||||
|
||||
ajv@^5.3.0:
|
||||
version "5.5.2"
|
||||
resolved "https://registry.yarnpkg.com/ajv/-/ajv-5.5.2.tgz#73b5eeca3fab653e3d3f9422b341ad42205dc965"
|
||||
dependencies:
|
||||
co "^4.6.0"
|
||||
fast-deep-equal "^1.0.0"
|
||||
fast-json-stable-stringify "^2.0.0"
|
||||
json-schema-traverse "^0.3.0"
|
||||
|
||||
ansi-regex@^2.0.0:
|
||||
version "2.1.1"
|
||||
resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-2.1.1.tgz#c3b33ab5ee360d86e0e628f0468ae7ef27d654df"
|
||||
|
||||
ansi-regex@^3.0.0:
|
||||
version "3.0.0"
|
||||
resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-3.0.0.tgz#ed0317c322064f79466c02966bddb605ab37d998"
|
||||
|
||||
aproba@^1.0.3:
|
||||
version "1.2.0"
|
||||
resolved "https://registry.yarnpkg.com/aproba/-/aproba-1.2.0.tgz#6802e6264efd18c790a1b0d517f0f2627bf2c94a"
|
||||
|
||||
are-we-there-yet@~1.1.2:
|
||||
version "1.1.5"
|
||||
resolved "https://registry.yarnpkg.com/are-we-there-yet/-/are-we-there-yet-1.1.5.tgz#4b35c2944f062a8bfcda66410760350fe9ddfc21"
|
||||
dependencies:
|
||||
delegates "^1.0.0"
|
||||
readable-stream "^2.0.6"
|
||||
|
||||
asn1@~0.2.3:
|
||||
version "0.2.4"
|
||||
resolved "https://registry.yarnpkg.com/asn1/-/asn1-0.2.4.tgz#8d2475dfab553bb33e77b54e59e880bb8ce23136"
|
||||
dependencies:
|
||||
safer-buffer "~2.1.0"
|
||||
|
||||
assert-plus@1.0.0, assert-plus@^1.0.0:
|
||||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/assert-plus/-/assert-plus-1.0.0.tgz#f12e0f3c5d77b0b1cdd9146942e4e96c1e4dd525"
|
||||
|
||||
asynckit@^0.4.0:
|
||||
version "0.4.0"
|
||||
resolved "https://registry.yarnpkg.com/asynckit/-/asynckit-0.4.0.tgz#c79ed97f7f34cb8f2ba1bc9790bcc366474b4b79"
|
||||
|
||||
aws-sign2@~0.7.0:
|
||||
version "0.7.0"
|
||||
resolved "https://registry.yarnpkg.com/aws-sign2/-/aws-sign2-0.7.0.tgz#b46e890934a9591f2d2f6f86d7e6a9f1b3fe76a8"
|
||||
|
||||
aws4@^1.8.0:
|
||||
version "1.8.0"
|
||||
resolved "https://registry.yarnpkg.com/aws4/-/aws4-1.8.0.tgz#f0e003d9ca9e7f59c7a508945d7b2ef9a04a542f"
|
||||
|
||||
balanced-match@^1.0.0:
|
||||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.0.tgz#89b4d199ab2bee49de164ea02b89ce462d71b767"
|
||||
|
||||
bcrypt-pbkdf@^1.0.0:
|
||||
version "1.0.2"
|
||||
resolved "https://registry.yarnpkg.com/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz#a4301d389b6a43f9b67ff3ca11a3f6637e360e9e"
|
||||
dependencies:
|
||||
tweetnacl "^0.14.3"
|
||||
|
||||
brace-expansion@^1.1.7:
|
||||
version "1.1.11"
|
||||
resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.11.tgz#3c7fcbf529d87226f3d2f52b966ff5271eb441dd"
|
||||
dependencies:
|
||||
balanced-match "^1.0.0"
|
||||
concat-map "0.0.1"
|
||||
|
||||
caseless@~0.12.0:
|
||||
version "0.12.0"
|
||||
resolved "https://registry.yarnpkg.com/caseless/-/caseless-0.12.0.tgz#1b681c21ff84033c826543090689420d187151dc"
|
||||
|
||||
chownr@^1.0.1:
|
||||
version "1.1.1"
|
||||
resolved "https://registry.yarnpkg.com/chownr/-/chownr-1.1.1.tgz#54726b8b8fff4df053c42187e801fb4412df1494"
|
||||
|
||||
co@^4.6.0:
|
||||
version "4.6.0"
|
||||
resolved "https://registry.yarnpkg.com/co/-/co-4.6.0.tgz#6ea6bdf3d853ae54ccb8e47bfa0bf3f9031fb184"
|
||||
|
||||
code-point-at@^1.0.0:
|
||||
version "1.1.0"
|
||||
resolved "https://registry.yarnpkg.com/code-point-at/-/code-point-at-1.1.0.tgz#0d070b4d043a5bea33a2f1a40e2edb3d9a4ccf77"
|
||||
|
||||
combined-stream@1.0.6:
|
||||
version "1.0.6"
|
||||
resolved "http://registry.npmjs.org/combined-stream/-/combined-stream-1.0.6.tgz#723e7df6e801ac5613113a7e445a9b69cb632818"
|
||||
dependencies:
|
||||
delayed-stream "~1.0.0"
|
||||
|
||||
combined-stream@~1.0.6:
|
||||
version "1.0.7"
|
||||
resolved "https://registry.yarnpkg.com/combined-stream/-/combined-stream-1.0.7.tgz#2d1d24317afb8abe95d6d2c0b07b57813539d828"
|
||||
dependencies:
|
||||
delayed-stream "~1.0.0"
|
||||
|
||||
concat-map@0.0.1:
|
||||
version "0.0.1"
|
||||
resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b"
|
||||
|
||||
console-control-strings@^1.0.0, console-control-strings@~1.1.0:
|
||||
version "1.1.0"
|
||||
resolved "https://registry.yarnpkg.com/console-control-strings/-/console-control-strings-1.1.0.tgz#3d7cf4464db6446ea644bf4b39507f9851008e8e"
|
||||
|
||||
core-util-is@1.0.2, core-util-is@~1.0.0:
|
||||
version "1.0.2"
|
||||
resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.2.tgz#b5fd54220aa2bc5ab57aab7140c940754503c1a7"
|
||||
|
||||
dashdash@^1.12.0:
|
||||
version "1.14.1"
|
||||
resolved "https://registry.yarnpkg.com/dashdash/-/dashdash-1.14.1.tgz#853cfa0f7cbe2fed5de20326b8dd581035f6e2f0"
|
||||
dependencies:
|
||||
assert-plus "^1.0.0"
|
||||
|
||||
debug@^2.1.2:
|
||||
version "2.6.9"
|
||||
resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f"
|
||||
dependencies:
|
||||
ms "2.0.0"
|
||||
|
||||
deep-extend@^0.6.0:
|
||||
version "0.6.0"
|
||||
resolved "https://registry.yarnpkg.com/deep-extend/-/deep-extend-0.6.0.tgz#c4fa7c95404a17a9c3e8ca7e1537312b736330ac"
|
||||
|
||||
delayed-stream@~1.0.0:
|
||||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/delayed-stream/-/delayed-stream-1.0.0.tgz#df3ae199acadfb7d440aaae0b29e2272b24ec619"
|
||||
|
||||
delegates@^1.0.0:
|
||||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/delegates/-/delegates-1.0.0.tgz#84c6e159b81904fdca59a0ef44cd870d31250f9a"
|
||||
|
||||
detect-libc@^1.0.2:
|
||||
version "1.0.3"
|
||||
resolved "https://registry.yarnpkg.com/detect-libc/-/detect-libc-1.0.3.tgz#fa137c4bd698edf55cd5cd02ac559f91a4c4ba9b"
|
||||
|
||||
download-file-sync@^1.0.4:
|
||||
version "1.0.4"
|
||||
resolved "https://registry.yarnpkg.com/download-file-sync/-/download-file-sync-1.0.4.tgz#d3e3c543f836f41039455b9034c72e355b036019"
|
||||
|
||||
ecc-jsbn@~0.1.1:
|
||||
version "0.1.2"
|
||||
resolved "https://registry.yarnpkg.com/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz#3a83a904e54353287874c564b7549386849a98c9"
|
||||
dependencies:
|
||||
jsbn "~0.1.0"
|
||||
safer-buffer "^2.1.0"
|
||||
|
||||
extend@~3.0.2:
|
||||
version "3.0.2"
|
||||
resolved "https://registry.yarnpkg.com/extend/-/extend-3.0.2.tgz#f8b1136b4071fbd8eb140aff858b1019ec2915fa"
|
||||
|
||||
extsprintf@1.3.0:
|
||||
version "1.3.0"
|
||||
resolved "https://registry.yarnpkg.com/extsprintf/-/extsprintf-1.3.0.tgz#96918440e3041a7a414f8c52e3c574eb3c3e1e05"
|
||||
|
||||
extsprintf@^1.2.0:
|
||||
version "1.4.0"
|
||||
resolved "https://registry.yarnpkg.com/extsprintf/-/extsprintf-1.4.0.tgz#e2689f8f356fad62cca65a3a91c5df5f9551692f"
|
||||
|
||||
fast-deep-equal@^1.0.0:
|
||||
version "1.1.0"
|
||||
resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-1.1.0.tgz#c053477817c86b51daa853c81e059b733d023614"
|
||||
|
||||
fast-json-stable-stringify@^2.0.0:
|
||||
version "2.0.0"
|
||||
resolved "https://registry.yarnpkg.com/fast-json-stable-stringify/-/fast-json-stable-stringify-2.0.0.tgz#d5142c0caee6b1189f87d3a76111064f86c8bbf2"
|
||||
|
||||
forever-agent@~0.6.1:
|
||||
version "0.6.1"
|
||||
resolved "https://registry.yarnpkg.com/forever-agent/-/forever-agent-0.6.1.tgz#fbc71f0c41adeb37f96c577ad1ed42d8fdacca91"
|
||||
|
||||
form-data@~2.3.2:
|
||||
version "2.3.2"
|
||||
resolved "https://registry.yarnpkg.com/form-data/-/form-data-2.3.2.tgz#4970498be604c20c005d4f5c23aecd21d6b49099"
|
||||
dependencies:
|
||||
asynckit "^0.4.0"
|
||||
combined-stream "1.0.6"
|
||||
mime-types "^2.1.12"
|
||||
|
||||
fs-minipass@^1.2.5:
|
||||
version "1.2.5"
|
||||
resolved "https://registry.yarnpkg.com/fs-minipass/-/fs-minipass-1.2.5.tgz#06c277218454ec288df77ada54a03b8702aacb9d"
|
||||
dependencies:
|
||||
minipass "^2.2.1"
|
||||
|
||||
fs.realpath@^1.0.0:
|
||||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f"
|
||||
|
||||
gauge@~2.7.3:
|
||||
version "2.7.4"
|
||||
resolved "https://registry.yarnpkg.com/gauge/-/gauge-2.7.4.tgz#2c03405c7538c39d7eb37b317022e325fb018bf7"
|
||||
dependencies:
|
||||
aproba "^1.0.3"
|
||||
console-control-strings "^1.0.0"
|
||||
has-unicode "^2.0.0"
|
||||
object-assign "^4.1.0"
|
||||
signal-exit "^3.0.0"
|
||||
string-width "^1.0.1"
|
||||
strip-ansi "^3.0.1"
|
||||
wide-align "^1.1.0"
|
||||
|
||||
getpass@^0.1.1:
|
||||
version "0.1.7"
|
||||
resolved "https://registry.yarnpkg.com/getpass/-/getpass-0.1.7.tgz#5eff8e3e684d569ae4cb2b1282604e8ba62149fa"
|
||||
dependencies:
|
||||
assert-plus "^1.0.0"
|
||||
|
||||
glob@^7.0.5:
|
||||
version "7.1.3"
|
||||
resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.3.tgz#3960832d3f1574108342dafd3a67b332c0969df1"
|
||||
dependencies:
|
||||
fs.realpath "^1.0.0"
|
||||
inflight "^1.0.4"
|
||||
inherits "2"
|
||||
minimatch "^3.0.4"
|
||||
once "^1.3.0"
|
||||
path-is-absolute "^1.0.0"
|
||||
|
||||
har-schema@^2.0.0:
|
||||
version "2.0.0"
|
||||
resolved "https://registry.yarnpkg.com/har-schema/-/har-schema-2.0.0.tgz#a94c2224ebcac04782a0d9035521f24735b7ec92"
|
||||
|
||||
har-validator@~5.1.0:
|
||||
version "5.1.0"
|
||||
resolved "https://registry.yarnpkg.com/har-validator/-/har-validator-5.1.0.tgz#44657f5688a22cfd4b72486e81b3a3fb11742c29"
|
||||
dependencies:
|
||||
ajv "^5.3.0"
|
||||
har-schema "^2.0.0"
|
||||
|
||||
has-unicode@^2.0.0:
|
||||
version "2.0.1"
|
||||
resolved "https://registry.yarnpkg.com/has-unicode/-/has-unicode-2.0.1.tgz#e0e6fe6a28cf51138855e086d1691e771de2a8b9"
|
||||
|
||||
http-signature@~1.2.0:
|
||||
version "1.2.0"
|
||||
resolved "https://registry.yarnpkg.com/http-signature/-/http-signature-1.2.0.tgz#9aecd925114772f3d95b65a60abb8f7c18fbace1"
|
||||
dependencies:
|
||||
assert-plus "^1.0.0"
|
||||
jsprim "^1.2.2"
|
||||
sshpk "^1.7.0"
|
||||
|
||||
iconv-lite@^0.4.4:
|
||||
version "0.4.24"
|
||||
resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.24.tgz#2022b4b25fbddc21d2f524974a474aafe733908b"
|
||||
dependencies:
|
||||
safer-buffer ">= 2.1.2 < 3"
|
||||
|
||||
ignore-walk@^3.0.1:
|
||||
version "3.0.1"
|
||||
resolved "https://registry.yarnpkg.com/ignore-walk/-/ignore-walk-3.0.1.tgz#a83e62e7d272ac0e3b551aaa82831a19b69f82f8"
|
||||
dependencies:
|
||||
minimatch "^3.0.4"
|
||||
|
||||
inflight@^1.0.4:
|
||||
version "1.0.6"
|
||||
resolved "https://registry.yarnpkg.com/inflight/-/inflight-1.0.6.tgz#49bd6331d7d02d0c09bc910a1075ba8165b56df9"
|
||||
dependencies:
|
||||
once "^1.3.0"
|
||||
wrappy "1"
|
||||
|
||||
inherits@2, inherits@~2.0.3:
|
||||
version "2.0.3"
|
||||
resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.3.tgz#633c2c83e3da42a502f52466022480f4208261de"
|
||||
|
||||
ini@~1.3.0:
|
||||
version "1.3.5"
|
||||
resolved "https://registry.yarnpkg.com/ini/-/ini-1.3.5.tgz#eee25f56db1c9ec6085e0c22778083f596abf927"
|
||||
|
||||
is-fullwidth-code-point@^1.0.0:
|
||||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz#ef9e31386f031a7f0d643af82fde50c457ef00cb"
|
||||
dependencies:
|
||||
number-is-nan "^1.0.0"
|
||||
|
||||
is-fullwidth-code-point@^2.0.0:
|
||||
version "2.0.0"
|
||||
resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz#a3b30a5c4f199183167aaab93beefae3ddfb654f"
|
||||
|
||||
is-typedarray@~1.0.0:
|
||||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/is-typedarray/-/is-typedarray-1.0.0.tgz#e479c80858df0c1b11ddda6940f96011fcda4a9a"
|
||||
|
||||
isarray@~1.0.0:
|
||||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/isarray/-/isarray-1.0.0.tgz#bb935d48582cba168c06834957a54a3e07124f11"
|
||||
|
||||
isstream@~0.1.2:
|
||||
version "0.1.2"
|
||||
resolved "https://registry.yarnpkg.com/isstream/-/isstream-0.1.2.tgz#47e63f7af55afa6f92e1500e690eb8b8529c099a"
|
||||
|
||||
jsbn@~0.1.0:
|
||||
version "0.1.1"
|
||||
resolved "https://registry.yarnpkg.com/jsbn/-/jsbn-0.1.1.tgz#a5e654c2e5a2deb5f201d96cefbca80c0ef2f513"
|
||||
|
||||
json-schema-traverse@^0.3.0:
|
||||
version "0.3.1"
|
||||
resolved "https://registry.yarnpkg.com/json-schema-traverse/-/json-schema-traverse-0.3.1.tgz#349a6d44c53a51de89b40805c5d5e59b417d3340"
|
||||
|
||||
json-schema@0.2.3:
|
||||
version "0.2.3"
|
||||
resolved "https://registry.yarnpkg.com/json-schema/-/json-schema-0.2.3.tgz#b480c892e59a2f05954ce727bd3f2a4e882f9e13"
|
||||
|
||||
json-stringify-safe@~5.0.1:
|
||||
version "5.0.1"
|
||||
resolved "https://registry.yarnpkg.com/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz#1296a2d58fd45f19a0f6ce01d65701e2c735b6eb"
|
||||
|
||||
jsprim@^1.2.2:
|
||||
version "1.4.1"
|
||||
resolved "https://registry.yarnpkg.com/jsprim/-/jsprim-1.4.1.tgz#313e66bc1e5cc06e438bc1b7499c2e5c56acb6a2"
|
||||
dependencies:
|
||||
assert-plus "1.0.0"
|
||||
extsprintf "1.3.0"
|
||||
json-schema "0.2.3"
|
||||
verror "1.10.0"
|
||||
|
||||
mime-db@~1.36.0:
|
||||
version "1.36.0"
|
||||
resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.36.0.tgz#5020478db3c7fe93aad7bbcc4dcf869c43363397"
|
||||
|
||||
mime-types@^2.1.12, mime-types@~2.1.19:
|
||||
version "2.1.20"
|
||||
resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.20.tgz#930cb719d571e903738520f8470911548ca2cc19"
|
||||
dependencies:
|
||||
mime-db "~1.36.0"
|
||||
|
||||
minimatch@^3.0.4:
|
||||
version "3.0.4"
|
||||
resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.0.4.tgz#5166e286457f03306064be5497e8dbb0c3d32083"
|
||||
dependencies:
|
||||
brace-expansion "^1.1.7"
|
||||
|
||||
minimist@0.0.8:
|
||||
version "0.0.8"
|
||||
resolved "http://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz#857fcabfc3397d2625b8228262e86aa7a011b05d"
|
||||
|
||||
minimist@^1.2.0:
|
||||
version "1.2.0"
|
||||
resolved "http://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz#a35008b20f41383eec1fb914f4cd5df79a264284"
|
||||
|
||||
minipass@^2.2.1, minipass@^2.3.3:
|
||||
version "2.3.4"
|
||||
resolved "https://registry.yarnpkg.com/minipass/-/minipass-2.3.4.tgz#4768d7605ed6194d6d576169b9e12ef71e9d9957"
|
||||
dependencies:
|
||||
safe-buffer "^5.1.2"
|
||||
yallist "^3.0.0"
|
||||
|
||||
minizlib@^1.1.0:
|
||||
version "1.1.1"
|
||||
resolved "https://registry.yarnpkg.com/minizlib/-/minizlib-1.1.1.tgz#6734acc045a46e61d596a43bb9d9cd326e19cc42"
|
||||
dependencies:
|
||||
minipass "^2.2.1"
|
||||
|
||||
mkdirp@^0.5.0, mkdirp@^0.5.1:
|
||||
version "0.5.1"
|
||||
resolved "http://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz#30057438eac6cf7f8c4767f38648d6697d75c903"
|
||||
dependencies:
|
||||
minimist "0.0.8"
|
||||
|
||||
ms@2.0.0:
|
||||
version "2.0.0"
|
||||
resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8"
|
||||
|
||||
nan@~2.10.0:
|
||||
version "2.10.0"
|
||||
resolved "http://registry.npmjs.org/nan/-/nan-2.10.0.tgz#96d0cd610ebd58d4b4de9cc0c6828cda99c7548f"
|
||||
|
||||
needle@^2.2.1:
|
||||
version "2.2.4"
|
||||
resolved "https://registry.yarnpkg.com/needle/-/needle-2.2.4.tgz#51931bff82533b1928b7d1d69e01f1b00ffd2a4e"
|
||||
dependencies:
|
||||
debug "^2.1.2"
|
||||
iconv-lite "^0.4.4"
|
||||
sax "^1.2.4"
|
||||
|
||||
node-pre-gyp@^0.10.3:
|
||||
version "0.10.3"
|
||||
resolved "https://registry.yarnpkg.com/node-pre-gyp/-/node-pre-gyp-0.10.3.tgz#3070040716afdc778747b61b6887bf78880b80fc"
|
||||
dependencies:
|
||||
detect-libc "^1.0.2"
|
||||
mkdirp "^0.5.1"
|
||||
needle "^2.2.1"
|
||||
nopt "^4.0.1"
|
||||
npm-packlist "^1.1.6"
|
||||
npmlog "^4.0.2"
|
||||
rc "^1.2.7"
|
||||
rimraf "^2.6.1"
|
||||
semver "^5.3.0"
|
||||
tar "^4"
|
||||
|
||||
nopt@^4.0.1:
|
||||
version "4.0.1"
|
||||
resolved "https://registry.yarnpkg.com/nopt/-/nopt-4.0.1.tgz#d0d4685afd5415193c8c7505602d0d17cd64474d"
|
||||
dependencies:
|
||||
abbrev "1"
|
||||
osenv "^0.1.4"
|
||||
|
||||
npm-bundled@^1.0.1:
|
||||
version "1.0.5"
|
||||
resolved "https://registry.yarnpkg.com/npm-bundled/-/npm-bundled-1.0.5.tgz#3c1732b7ba936b3a10325aef616467c0ccbcc979"
|
||||
|
||||
npm-packlist@^1.1.6:
|
||||
version "1.1.12"
|
||||
resolved "https://registry.yarnpkg.com/npm-packlist/-/npm-packlist-1.1.12.tgz#22bde2ebc12e72ca482abd67afc51eb49377243a"
|
||||
dependencies:
|
||||
ignore-walk "^3.0.1"
|
||||
npm-bundled "^1.0.1"
|
||||
|
||||
npmlog@^4.0.2:
|
||||
version "4.1.2"
|
||||
resolved "https://registry.yarnpkg.com/npmlog/-/npmlog-4.1.2.tgz#08a7f2a8bf734604779a9efa4ad5cc717abb954b"
|
||||
dependencies:
|
||||
are-we-there-yet "~1.1.2"
|
||||
console-control-strings "~1.1.0"
|
||||
gauge "~2.7.3"
|
||||
set-blocking "~2.0.0"
|
||||
|
||||
number-is-nan@^1.0.0:
|
||||
version "1.0.1"
|
||||
resolved "https://registry.yarnpkg.com/number-is-nan/-/number-is-nan-1.0.1.tgz#097b602b53422a522c1afb8790318336941a011d"
|
||||
|
||||
oauth-sign@~0.9.0:
|
||||
version "0.9.0"
|
||||
resolved "https://registry.yarnpkg.com/oauth-sign/-/oauth-sign-0.9.0.tgz#47a7b016baa68b5fa0ecf3dee08a85c679ac6455"
|
||||
|
||||
object-assign@^4.1.0:
|
||||
version "4.1.1"
|
||||
resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863"
|
||||
|
||||
once@^1.3.0:
|
||||
version "1.4.0"
|
||||
resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1"
|
||||
dependencies:
|
||||
wrappy "1"
|
||||
|
||||
os-homedir@^1.0.0:
|
||||
version "1.0.2"
|
||||
resolved "https://registry.yarnpkg.com/os-homedir/-/os-homedir-1.0.2.tgz#ffbc4988336e0e833de0c168c7ef152121aa7fb3"
|
||||
|
||||
os-tmpdir@^1.0.0:
|
||||
version "1.0.2"
|
||||
resolved "https://registry.yarnpkg.com/os-tmpdir/-/os-tmpdir-1.0.2.tgz#bbe67406c79aa85c5cfec766fe5734555dfa1274"
|
||||
|
||||
osenv@^0.1.4:
|
||||
version "0.1.5"
|
||||
resolved "https://registry.yarnpkg.com/osenv/-/osenv-0.1.5.tgz#85cdfafaeb28e8677f416e287592b5f3f49ea410"
|
||||
dependencies:
|
||||
os-homedir "^1.0.0"
|
||||
os-tmpdir "^1.0.0"
|
||||
|
||||
path-is-absolute@^1.0.0:
|
||||
version "1.0.1"
|
||||
resolved "https://registry.yarnpkg.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f"
|
||||
|
||||
performance-now@^2.1.0:
|
||||
version "2.1.0"
|
||||
resolved "https://registry.yarnpkg.com/performance-now/-/performance-now-2.1.0.tgz#6309f4e0e5fa913ec1c69307ae364b4b377c9e7b"
|
||||
|
||||
process-nextick-args@~2.0.0:
|
||||
version "2.0.0"
|
||||
resolved "https://registry.yarnpkg.com/process-nextick-args/-/process-nextick-args-2.0.0.tgz#a37d732f4271b4ab1ad070d35508e8290788ffaa"
|
||||
|
||||
psl@^1.1.24:
|
||||
version "1.1.29"
|
||||
resolved "https://registry.yarnpkg.com/psl/-/psl-1.1.29.tgz#60f580d360170bb722a797cc704411e6da850c67"
|
||||
|
||||
punycode@^1.4.1:
|
||||
version "1.4.1"
|
||||
resolved "https://registry.yarnpkg.com/punycode/-/punycode-1.4.1.tgz#c0d5a63b2718800ad8e1eb0fa5269c84dd41845e"
|
||||
|
||||
qs@~6.5.2:
|
||||
version "6.5.2"
|
||||
resolved "https://registry.yarnpkg.com/qs/-/qs-6.5.2.tgz#cb3ae806e8740444584ef154ce8ee98d403f3e36"
|
||||
|
||||
rc@^1.2.7:
|
||||
version "1.2.8"
|
||||
resolved "https://registry.yarnpkg.com/rc/-/rc-1.2.8.tgz#cd924bf5200a075b83c188cd6b9e211b7fc0d3ed"
|
||||
dependencies:
|
||||
deep-extend "^0.6.0"
|
||||
ini "~1.3.0"
|
||||
minimist "^1.2.0"
|
||||
strip-json-comments "~2.0.1"
|
||||
|
||||
readable-stream@^2.0.6:
|
||||
version "2.3.6"
|
||||
resolved "http://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz#b11c27d88b8ff1fbe070643cf94b0c79ae1b0aaf"
|
||||
dependencies:
|
||||
core-util-is "~1.0.0"
|
||||
inherits "~2.0.3"
|
||||
isarray "~1.0.0"
|
||||
process-nextick-args "~2.0.0"
|
||||
safe-buffer "~5.1.1"
|
||||
string_decoder "~1.1.1"
|
||||
util-deprecate "~1.0.1"
|
||||
|
||||
request@^2.87.0:
|
||||
version "2.88.0"
|
||||
resolved "https://registry.yarnpkg.com/request/-/request-2.88.0.tgz#9c2fca4f7d35b592efe57c7f0a55e81052124fef"
|
||||
dependencies:
|
||||
aws-sign2 "~0.7.0"
|
||||
aws4 "^1.8.0"
|
||||
caseless "~0.12.0"
|
||||
combined-stream "~1.0.6"
|
||||
extend "~3.0.2"
|
||||
forever-agent "~0.6.1"
|
||||
form-data "~2.3.2"
|
||||
har-validator "~5.1.0"
|
||||
http-signature "~1.2.0"
|
||||
is-typedarray "~1.0.0"
|
||||
isstream "~0.1.2"
|
||||
json-stringify-safe "~5.0.1"
|
||||
mime-types "~2.1.19"
|
||||
oauth-sign "~0.9.0"
|
||||
performance-now "^2.1.0"
|
||||
qs "~6.5.2"
|
||||
safe-buffer "^5.1.2"
|
||||
tough-cookie "~2.4.3"
|
||||
tunnel-agent "^0.6.0"
|
||||
uuid "^3.3.2"
|
||||
|
||||
rimraf@^2.6.1:
|
||||
version "2.6.2"
|
||||
resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.6.2.tgz#2ed8150d24a16ea8651e6d6ef0f47c4158ce7a36"
|
||||
dependencies:
|
||||
glob "^7.0.5"
|
||||
|
||||
safe-buffer@^5.0.1, safe-buffer@^5.1.2, safe-buffer@~5.1.0, safe-buffer@~5.1.1:
|
||||
version "5.1.2"
|
||||
resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d"
|
||||
|
||||
"safer-buffer@>= 2.1.2 < 3", safer-buffer@^2.0.2, safer-buffer@^2.1.0, safer-buffer@~2.1.0:
|
||||
version "2.1.2"
|
||||
resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a"
|
||||
|
||||
sax@^1.2.4:
|
||||
version "1.2.4"
|
||||
resolved "https://registry.yarnpkg.com/sax/-/sax-1.2.4.tgz#2816234e2378bddc4e5354fab5caa895df7100d9"
|
||||
|
||||
semver@^5.3.0:
|
||||
version "5.6.0"
|
||||
resolved "https://registry.yarnpkg.com/semver/-/semver-5.6.0.tgz#7e74256fbaa49c75aa7c7a205cc22799cac80004"
|
||||
|
||||
set-blocking@~2.0.0:
|
||||
version "2.0.0"
|
||||
resolved "https://registry.yarnpkg.com/set-blocking/-/set-blocking-2.0.0.tgz#045f9782d011ae9a6803ddd382b24392b3d890f7"
|
||||
|
||||
signal-exit@^3.0.0:
|
||||
version "3.0.2"
|
||||
resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.2.tgz#b5fdc08f1287ea1178628e415e25132b73646c6d"
|
||||
|
||||
sqlite3@^4.0.2:
|
||||
version "4.0.2"
|
||||
resolved "https://registry.yarnpkg.com/sqlite3/-/sqlite3-4.0.2.tgz#1bbeb68b03ead5d499e42a3a1b140064791c5a64"
|
||||
dependencies:
|
||||
nan "~2.10.0"
|
||||
node-pre-gyp "^0.10.3"
|
||||
request "^2.87.0"
|
||||
|
||||
sshpk@^1.7.0:
|
||||
version "1.15.1"
|
||||
resolved "https://registry.yarnpkg.com/sshpk/-/sshpk-1.15.1.tgz#b79a089a732e346c6e0714830f36285cd38191a2"
|
||||
dependencies:
|
||||
asn1 "~0.2.3"
|
||||
assert-plus "^1.0.0"
|
||||
bcrypt-pbkdf "^1.0.0"
|
||||
dashdash "^1.12.0"
|
||||
ecc-jsbn "~0.1.1"
|
||||
getpass "^0.1.1"
|
||||
jsbn "~0.1.0"
|
||||
safer-buffer "^2.0.2"
|
||||
tweetnacl "~0.14.0"
|
||||
|
||||
string-width@^1.0.1:
|
||||
version "1.0.2"
|
||||
resolved "https://registry.yarnpkg.com/string-width/-/string-width-1.0.2.tgz#118bdf5b8cdc51a2a7e70d211e07e2b0b9b107d3"
|
||||
dependencies:
|
||||
code-point-at "^1.0.0"
|
||||
is-fullwidth-code-point "^1.0.0"
|
||||
strip-ansi "^3.0.0"
|
||||
|
||||
"string-width@^1.0.2 || 2":
|
||||
version "2.1.1"
|
||||
resolved "https://registry.yarnpkg.com/string-width/-/string-width-2.1.1.tgz#ab93f27a8dc13d28cac815c462143a6d9012ae9e"
|
||||
dependencies:
|
||||
is-fullwidth-code-point "^2.0.0"
|
||||
strip-ansi "^4.0.0"
|
||||
|
||||
string_decoder@~1.1.1:
|
||||
version "1.1.1"
|
||||
resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.1.1.tgz#9cf1611ba62685d7030ae9e4ba34149c3af03fc8"
|
||||
dependencies:
|
||||
safe-buffer "~5.1.0"
|
||||
|
||||
strip-ansi@^3.0.0, strip-ansi@^3.0.1:
|
||||
version "3.0.1"
|
||||
resolved "http://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz#6a385fb8853d952d5ff05d0e8aaf94278dc63dcf"
|
||||
dependencies:
|
||||
ansi-regex "^2.0.0"
|
||||
|
||||
strip-ansi@^4.0.0:
|
||||
version "4.0.0"
|
||||
resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-4.0.0.tgz#a8479022eb1ac368a871389b635262c505ee368f"
|
||||
dependencies:
|
||||
ansi-regex "^3.0.0"
|
||||
|
||||
strip-json-comments@~2.0.1:
|
||||
version "2.0.1"
|
||||
resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-2.0.1.tgz#3c531942e908c2697c0ec344858c286c7ca0a60a"
|
||||
|
||||
tar@^4:
|
||||
version "4.4.6"
|
||||
resolved "https://registry.yarnpkg.com/tar/-/tar-4.4.6.tgz#63110f09c00b4e60ac8bcfe1bf3c8660235fbc9b"
|
||||
dependencies:
|
||||
chownr "^1.0.1"
|
||||
fs-minipass "^1.2.5"
|
||||
minipass "^2.3.3"
|
||||
minizlib "^1.1.0"
|
||||
mkdirp "^0.5.0"
|
||||
safe-buffer "^5.1.2"
|
||||
yallist "^3.0.2"
|
||||
|
||||
tough-cookie@~2.4.3:
|
||||
version "2.4.3"
|
||||
resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-2.4.3.tgz#53f36da3f47783b0925afa06ff9f3b165280f781"
|
||||
dependencies:
|
||||
psl "^1.1.24"
|
||||
punycode "^1.4.1"
|
||||
|
||||
tunnel-agent@^0.6.0:
|
||||
version "0.6.0"
|
||||
resolved "https://registry.yarnpkg.com/tunnel-agent/-/tunnel-agent-0.6.0.tgz#27a5dea06b36b04a0a9966774b290868f0fc40fd"
|
||||
dependencies:
|
||||
safe-buffer "^5.0.1"
|
||||
|
||||
tweetnacl@^0.14.3, tweetnacl@~0.14.0:
|
||||
version "0.14.5"
|
||||
resolved "https://registry.yarnpkg.com/tweetnacl/-/tweetnacl-0.14.5.tgz#5ae68177f192d4456269d108afa93ff8743f4f64"
|
||||
|
||||
util-deprecate@~1.0.1:
|
||||
version "1.0.2"
|
||||
resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf"
|
||||
|
||||
uuid@^3.3.2:
|
||||
version "3.3.2"
|
||||
resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.3.2.tgz#1b4af4955eb3077c501c23872fc6513811587131"
|
||||
|
||||
verror@1.10.0:
|
||||
version "1.10.0"
|
||||
resolved "https://registry.yarnpkg.com/verror/-/verror-1.10.0.tgz#3a105ca17053af55d6e270c1f8288682e18da400"
|
||||
dependencies:
|
||||
assert-plus "^1.0.0"
|
||||
core-util-is "1.0.2"
|
||||
extsprintf "^1.2.0"
|
||||
|
||||
wide-align@^1.1.0:
|
||||
version "1.1.3"
|
||||
resolved "https://registry.yarnpkg.com/wide-align/-/wide-align-1.1.3.tgz#ae074e6bdc0c14a431e804e624549c633b000457"
|
||||
dependencies:
|
||||
string-width "^1.0.2 || 2"
|
||||
|
||||
wrappy@1:
|
||||
version "1.0.2"
|
||||
resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f"
|
||||
|
||||
yallist@^3.0.0, yallist@^3.0.2:
|
||||
version "3.0.2"
|
||||
resolved "https://registry.yarnpkg.com/yallist/-/yallist-3.0.2.tgz#8452b4bb7e83c7c188d8041c1a837c773d6d8bb9"
|
||||
218
stats.go
218
stats.go
@@ -1,218 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
"syscall"
|
||||
"time"
|
||||
)
|
||||
|
||||
type periodicStats struct {
|
||||
totalRequests []float64
|
||||
|
||||
filteredTotal []float64
|
||||
filteredLists []float64
|
||||
filteredSafebrowsing []float64
|
||||
filteredSafesearch []float64
|
||||
filteredParental []float64
|
||||
|
||||
processingTimeSum []float64
|
||||
processingTimeCount []float64
|
||||
|
||||
lastRotate time.Time // last time this data was rotated
|
||||
}
|
||||
|
||||
type statsSnapshot struct {
|
||||
totalRequests float64
|
||||
|
||||
filteredTotal float64
|
||||
filteredLists float64
|
||||
filteredSafebrowsing float64
|
||||
filteredSafesearch float64
|
||||
filteredParental float64
|
||||
|
||||
processingTimeSum float64
|
||||
processingTimeCount float64
|
||||
}
|
||||
|
||||
type statsCollection struct {
|
||||
perSecond periodicStats
|
||||
perMinute periodicStats
|
||||
perHour periodicStats
|
||||
perDay periodicStats
|
||||
lastsnap statsSnapshot
|
||||
}
|
||||
|
||||
var statistics statsCollection
|
||||
|
||||
var client = &http.Client{
|
||||
Timeout: time.Second * 30,
|
||||
}
|
||||
|
||||
const statsHistoryElements = 60 + 1 // +1 for calculating delta
|
||||
|
||||
var requestCountTotalRegex = regexp.MustCompile(`^coredns_dns_request_count_total`)
|
||||
var requestDurationSecondsSum = regexp.MustCompile(`^coredns_dns_request_duration_seconds_sum`)
|
||||
var requestDurationSecondsCount = regexp.MustCompile(`^coredns_dns_request_duration_seconds_count`)
|
||||
|
||||
func initPeriodicStats(stats *periodicStats) {
|
||||
stats.totalRequests = make([]float64, statsHistoryElements)
|
||||
stats.filteredTotal = make([]float64, statsHistoryElements)
|
||||
stats.filteredLists = make([]float64, statsHistoryElements)
|
||||
stats.filteredSafebrowsing = make([]float64, statsHistoryElements)
|
||||
stats.filteredSafesearch = make([]float64, statsHistoryElements)
|
||||
stats.filteredParental = make([]float64, statsHistoryElements)
|
||||
stats.processingTimeSum = make([]float64, statsHistoryElements)
|
||||
stats.processingTimeCount = make([]float64, statsHistoryElements)
|
||||
}
|
||||
|
||||
func init() {
|
||||
initPeriodicStats(&statistics.perSecond)
|
||||
initPeriodicStats(&statistics.perMinute)
|
||||
initPeriodicStats(&statistics.perHour)
|
||||
initPeriodicStats(&statistics.perDay)
|
||||
}
|
||||
|
||||
func runStatsCollectors() {
|
||||
go statsCollector(time.Second)
|
||||
}
|
||||
|
||||
func statsCollector(t time.Duration) {
|
||||
for range time.Tick(t) {
|
||||
collectStats()
|
||||
}
|
||||
}
|
||||
|
||||
func isConnRefused(err error) bool {
|
||||
if err != nil {
|
||||
if uerr, ok := err.(*url.Error); ok {
|
||||
if noerr, ok := uerr.Err.(*net.OpError); ok {
|
||||
if scerr, ok := noerr.Err.(*os.SyscallError); ok {
|
||||
if scerr.Err == syscall.ECONNREFUSED {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func sliceRotate(slice *[]float64) {
|
||||
a := (*slice)[:len(*slice)-1]
|
||||
*slice = append([]float64{0}, a...)
|
||||
}
|
||||
|
||||
func statsRotate(stats *periodicStats, now time.Time) {
|
||||
sliceRotate(&stats.totalRequests)
|
||||
sliceRotate(&stats.filteredTotal)
|
||||
sliceRotate(&stats.filteredLists)
|
||||
sliceRotate(&stats.filteredSafebrowsing)
|
||||
sliceRotate(&stats.filteredSafesearch)
|
||||
sliceRotate(&stats.filteredParental)
|
||||
sliceRotate(&stats.processingTimeSum)
|
||||
sliceRotate(&stats.processingTimeCount)
|
||||
stats.lastRotate = now
|
||||
}
|
||||
|
||||
func handleValue(input string, target *float64) {
|
||||
value, err := strconv.ParseFloat(input, 64)
|
||||
if err != nil {
|
||||
log.Println("Failed to parse number input:", err)
|
||||
return
|
||||
}
|
||||
*target = value
|
||||
}
|
||||
|
||||
// called every second, accumulates stats for each second, minute, hour and day
|
||||
func collectStats() {
|
||||
now := time.Now()
|
||||
// rotate each second
|
||||
// NOTE: since we are called every second, always rotate, otherwise aliasing problems cause the rotation to skip
|
||||
if true {
|
||||
statsRotate(&statistics.perSecond, now)
|
||||
}
|
||||
// if minute elapsed, rotate
|
||||
if now.Sub(statistics.perMinute.lastRotate).Minutes() >= 1 {
|
||||
statsRotate(&statistics.perMinute, now)
|
||||
}
|
||||
// if hour elapsed, rotate
|
||||
if now.Sub(statistics.perHour.lastRotate).Hours() >= 1 {
|
||||
statsRotate(&statistics.perHour, now)
|
||||
}
|
||||
// if day elapsed, rotate
|
||||
if now.Sub(statistics.perDay.lastRotate).Hours()/24.0 >= 1 {
|
||||
statsRotate(&statistics.perDay, now)
|
||||
}
|
||||
|
||||
// grab HTTP from prometheus
|
||||
resp, err := client.Get("http://127.0.0.1:9153/metrics")
|
||||
if resp != nil && resp.Body != nil {
|
||||
defer resp.Body.Close()
|
||||
}
|
||||
if err != nil {
|
||||
if isConnRefused(err) == false {
|
||||
log.Printf("Couldn't get coredns metrics: %T %s\n", err, err)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// read the body entirely
|
||||
body, err := ioutil.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
log.Println("Couldn't read response body:", err)
|
||||
return
|
||||
}
|
||||
|
||||
// handle body
|
||||
scanner := bufio.NewScanner(strings.NewReader(string(body)))
|
||||
for scanner.Scan() {
|
||||
line := scanner.Text()
|
||||
// ignore comments
|
||||
if line[0] == '#' {
|
||||
continue
|
||||
}
|
||||
splitted := strings.Split(line, " ")
|
||||
switch {
|
||||
case splitted[0] == "coredns_dnsfilter_filtered_total":
|
||||
handleValue(splitted[1], &statistics.lastsnap.filteredTotal)
|
||||
case splitted[0] == "coredns_dnsfilter_filtered_lists_total":
|
||||
handleValue(splitted[1], &statistics.lastsnap.filteredLists)
|
||||
case splitted[0] == "coredns_dnsfilter_filtered_safebrowsing_total":
|
||||
handleValue(splitted[1], &statistics.lastsnap.filteredSafebrowsing)
|
||||
case splitted[0] == "coredns_dnsfilter_filtered_parental_total":
|
||||
handleValue(splitted[1], &statistics.lastsnap.filteredParental)
|
||||
case requestCountTotalRegex.MatchString(splitted[0]):
|
||||
handleValue(splitted[1], &statistics.lastsnap.totalRequests)
|
||||
case requestDurationSecondsSum.MatchString(splitted[0]):
|
||||
handleValue(splitted[1], &statistics.lastsnap.processingTimeSum)
|
||||
case requestDurationSecondsCount.MatchString(splitted[0]):
|
||||
handleValue(splitted[1], &statistics.lastsnap.processingTimeCount)
|
||||
}
|
||||
}
|
||||
|
||||
// put the snap into per-second, per-minute, per-hour and per-day
|
||||
assignSnapToStats(&statistics.perSecond)
|
||||
assignSnapToStats(&statistics.perMinute)
|
||||
assignSnapToStats(&statistics.perHour)
|
||||
assignSnapToStats(&statistics.perDay)
|
||||
}
|
||||
|
||||
func assignSnapToStats(stats *periodicStats) {
|
||||
stats.totalRequests[0] = statistics.lastsnap.totalRequests
|
||||
stats.filteredTotal[0] = statistics.lastsnap.filteredTotal
|
||||
stats.filteredLists[0] = statistics.lastsnap.filteredLists
|
||||
stats.filteredSafebrowsing[0] = statistics.lastsnap.filteredSafebrowsing
|
||||
stats.filteredSafesearch[0] = statistics.lastsnap.filteredSafesearch
|
||||
stats.filteredParental[0] = statistics.lastsnap.filteredParental
|
||||
stats.processingTimeSum[0] = statistics.lastsnap.processingTimeSum
|
||||
stats.processingTimeCount[0] = statistics.lastsnap.processingTimeCount
|
||||
}
|
||||
10
version.json
Normal file
10
version.json
Normal file
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"version": "v0.9",
|
||||
"announcement": "AdGuard Home v0.9 is now available!",
|
||||
"announcement_url": "https://github.com/AdguardTeam/AdGuardHome/releases/tag/v0.9",
|
||||
"download_darwin_amd64": "https://github.com/AdguardTeam/AdGuardHome/releases/download/v0.9/AdGuardHome_v0.9_MacOS.zip",
|
||||
"download_linux_amd64": "https://github.com/AdguardTeam/AdGuardHome/releases/download/v0.9/AdGuardHome_v0.9_linux_amd64.tar.gz",
|
||||
"download_linux_386": "https://github.com/AdguardTeam/AdGuardHome/releases/download/v0.9/AdGuardHome_v0.9_linux_386.tar.gz",
|
||||
"download_linux_arm": "https://github.com/AdguardTeam/AdGuardHome/releases/download/v0.9/AdGuardHome_v0.9_linux_arm.tar.gz",
|
||||
"selfupdate_min_version": "v0.0"
|
||||
}
|
||||
Reference in New Issue
Block a user