Compare commits
365 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
aa30728cda | ||
|
|
71ab95f12f | ||
|
|
c71d6ed433 | ||
|
|
77348e746f | ||
|
|
27c33b2fa9 | ||
|
|
86279f19b0 | ||
|
|
3d901a82ad | ||
|
|
d351ed82c1 | ||
|
|
8e13f22aa5 | ||
|
|
d0f4f22e0d | ||
|
|
4bbc503709 | ||
|
|
1b305d94a7 | ||
|
|
a7478255a1 | ||
|
|
84604e292b | ||
|
|
a04923a4f3 | ||
|
|
1da954fa97 | ||
|
|
ad4b58472f | ||
|
|
4e1c1618cb | ||
|
|
3916f1073d | ||
|
|
623c3bba09 | ||
|
|
e8898811fe | ||
|
|
71df659dc9 | ||
|
|
158f2f6100 | ||
|
|
8e993cd788 | ||
|
|
12f8590228 | ||
|
|
2814c393ad | ||
|
|
37431735fd | ||
|
|
251beb24d3 | ||
|
|
37a1a98c49 | ||
|
|
5ac775aa4a | ||
|
|
c53a132072 | ||
|
|
8e7ceec1a1 | ||
|
|
89446fccd5 | ||
|
|
4f45f2c3e3 | ||
|
|
9c8e4c64ea | ||
|
|
2c2295c161 | ||
|
|
a2dd7c32d5 | ||
|
|
b3f33b4b0b | ||
|
|
de08b53ae1 | ||
|
|
a60eeb55f1 | ||
|
|
9d4b829fb6 | ||
|
|
1515c353f8 | ||
|
|
f0536b6347 | ||
|
|
340a4fb58e | ||
|
|
e873149bee | ||
|
|
77793e5f21 | ||
|
|
24154f0033 | ||
|
|
8c406427af | ||
|
|
885e4e16c8 | ||
|
|
0b7f0396de | ||
|
|
cca6998efe | ||
|
|
3c374b5940 | ||
|
|
ba103f9825 | ||
|
|
2748d4c889 | ||
|
|
b8c0ed9335 | ||
|
|
ff012cf0a3 | ||
|
|
2b0addd505 | ||
|
|
2de0f82bbc | ||
|
|
1fc5f15aaa | ||
|
|
954d923975 | ||
|
|
05cce8b107 | ||
|
|
d44f68e844 | ||
|
|
cb97c221fd | ||
|
|
81bb4aea78 | ||
|
|
8da90a7f4a | ||
|
|
b4b800565c | ||
|
|
e8280c60d8 | ||
|
|
571be68733 | ||
|
|
bdec98f18e | ||
|
|
28df187012 | ||
|
|
f0569af367 | ||
|
|
e2956cae82 | ||
|
|
110434c2d5 | ||
|
|
f417f6257f | ||
|
|
1d2958f4aa | ||
|
|
3e67c8d79a | ||
|
|
57a33654f7 | ||
|
|
30050bf278 | ||
|
|
5cbaeb82a8 | ||
|
|
876bec5a65 | ||
|
|
4b4faad9e8 | ||
|
|
c061bec6d8 | ||
|
|
229ef78085 | ||
|
|
0aeca6bbf5 | ||
|
|
35b5f4b48b | ||
|
|
0d3aa00956 | ||
|
|
cb9ffe4de9 | ||
|
|
351673c060 | ||
|
|
4a14c199d8 | ||
|
|
1dd548c36c | ||
|
|
d42718465d | ||
|
|
93847bd309 | ||
|
|
4da55dc2aa | ||
|
|
3d3e0784ea | ||
|
|
3898309778 | ||
|
|
c19416bf8e | ||
|
|
c025c845d2 | ||
|
|
c5b1105fc1 | ||
|
|
38869b22a6 | ||
|
|
ab11c912db | ||
|
|
7451eb1346 | ||
|
|
8725c1df7a | ||
|
|
0820983d81 | ||
|
|
a5b61459cc | ||
|
|
dd3621bcf6 | ||
|
|
571370ab16 | ||
|
|
e33c8a3cde | ||
|
|
0d5f24927c | ||
|
|
27ea739cfd | ||
|
|
899b26725e | ||
|
|
26f2207b5c | ||
|
|
6d7d10ec38 | ||
|
|
c1f6da2b52 | ||
|
|
a40ddb094b | ||
|
|
b477b67428 | ||
|
|
cd9db6440b | ||
|
|
9ff420bb52 | ||
|
|
9a03190a62 | ||
|
|
6b6eacaa2b | ||
|
|
5ca33e44d8 | ||
|
|
68c8a4d484 | ||
|
|
6e5731ab02 | ||
|
|
548f539566 | ||
|
|
853582dade | ||
|
|
3a94080491 | ||
|
|
ba161e9a6f | ||
|
|
91eaf72051 | ||
|
|
826529e73e | ||
|
|
c466f8cc73 | ||
|
|
f9d1948f6a | ||
|
|
bb8d7c37bb | ||
|
|
f2d7f8161b | ||
|
|
39b2e345c3 | ||
|
|
a7a38413fe | ||
|
|
fe671152c2 | ||
|
|
ba678ffa82 | ||
|
|
672ff33879 | ||
|
|
398312cd80 | ||
|
|
06a28a461d | ||
|
|
31b855f9ab | ||
|
|
f379d34813 | ||
|
|
5abe5af707 | ||
|
|
daae040f9c | ||
|
|
f2b3c3a14c | ||
|
|
d3e81c47f6 | ||
|
|
c14aff3dba | ||
|
|
d97c426646 | ||
|
|
34e14930de | ||
|
|
924afea22b | ||
|
|
302c3a767a | ||
|
|
c494e17df5 | ||
|
|
7c25c0febe | ||
|
|
5f7fc0f041 | ||
|
|
beb94741cf | ||
|
|
24be7ce4ed | ||
|
|
6e41897323 | ||
|
|
b5e7237169 | ||
|
|
7e95ce9136 | ||
|
|
a7416f9c34 | ||
|
|
2bd4840ba5 | ||
|
|
5349ec76fd | ||
|
|
71259c5f19 | ||
|
|
f21aebd1cf | ||
|
|
c36a7895ad | ||
|
|
5fed5c0718 | ||
|
|
f437d53c1c | ||
|
|
bfe25ba014 | ||
|
|
a8cdc5b01c | ||
|
|
0fbfa057b1 | ||
|
|
93ea27077f | ||
|
|
aab8da4c7c | ||
|
|
448a6caeb8 | ||
|
|
a4dc4c61d8 | ||
|
|
277415124e | ||
|
|
b216475c20 | ||
|
|
c776ad21b7 | ||
|
|
b56dcc9de1 | ||
|
|
05cab6fde0 | ||
|
|
09b49d0145 | ||
|
|
98bfb82787 | ||
|
|
d238e1feb3 | ||
|
|
911250cfbe | ||
|
|
4b4cb99b30 | ||
|
|
0161509b5f | ||
|
|
ec6b1f7c42 | ||
|
|
69a75fbcaa | ||
|
|
a0157e39c6 | ||
|
|
d078851246 | ||
|
|
c9d627ea71 | ||
|
|
297a1c7fa5 | ||
|
|
f1c3fecfb2 | ||
|
|
79eff5f260 | ||
|
|
8d5d37c281 | ||
|
|
e1bb428a6a | ||
|
|
f1b6da93cf | ||
|
|
61f4b6f1ae | ||
|
|
f678eaf9c0 | ||
|
|
607089cd25 | ||
|
|
df94d76a8b | ||
|
|
8294bb1c7c | ||
|
|
ec157ac4ea | ||
|
|
c4ba284964 | ||
|
|
f3a97ed7ab | ||
|
|
d90da5d540 | ||
|
|
6cd93139fd | ||
|
|
246f726115 | ||
|
|
5a6dc34ec0 | ||
|
|
ddcfe7c4bf | ||
|
|
eb71f3ed8f | ||
|
|
fd629be6e6 | ||
|
|
ce1aaea4ca | ||
|
|
fa8c038bc1 | ||
|
|
9fdf946fc0 | ||
|
|
fd8860a389 | ||
|
|
cbe83e2053 | ||
|
|
b0c4d88d54 | ||
|
|
ec0b8c687a | ||
|
|
4d3f1b83a6 | ||
|
|
368e2d1ebd | ||
|
|
568784b992 | ||
|
|
243603e04c | ||
|
|
d8802a9709 | ||
|
|
7463e54258 | ||
|
|
7acb107cbf | ||
|
|
86d79ae232 | ||
|
|
fedfc3a1fd | ||
|
|
bf15a40248 | ||
|
|
4efa30edc4 | ||
|
|
7ab03e9335 | ||
|
|
55a7ff7447 | ||
|
|
a7e0f66492 | ||
|
|
f312575da4 | ||
|
|
8fc5aebf12 | ||
|
|
03effab345 | ||
|
|
f868fdbf7a | ||
|
|
1b7db49062 | ||
|
|
f5e7eed447 | ||
|
|
6fd9af3c60 | ||
|
|
4aea91a70c | ||
|
|
8b4a1ca713 | ||
|
|
73f71364b3 | ||
|
|
712493aafd | ||
|
|
1270bbad1a | ||
|
|
c073f9db7b | ||
|
|
87b3c92f71 | ||
|
|
9fa85a5c48 | ||
|
|
52b81a27fb | ||
|
|
39bc55e430 | ||
|
|
59adad4d53 | ||
|
|
a74c2248fb | ||
|
|
d46b65f982 | ||
|
|
96fbf7f134 | ||
|
|
9294c9ecb2 | ||
|
|
dd21f497e3 | ||
|
|
390883126c | ||
|
|
fb24447915 | ||
|
|
fcf7b2185e | ||
|
|
b91c829f4c | ||
|
|
7106a8eb35 | ||
|
|
09702c724e | ||
|
|
4623817894 | ||
|
|
413bc75320 | ||
|
|
1b84a9233d | ||
|
|
aed87ce741 | ||
|
|
2652ed34b1 | ||
|
|
cc96593ebf | ||
|
|
3ade62301b | ||
|
|
62606db1af | ||
|
|
8227970d39 | ||
|
|
374a0dc2e5 | ||
|
|
2bc1d737cc | ||
|
|
bac2c39107 | ||
|
|
0a977fee87 | ||
|
|
e711f6e5fe | ||
|
|
9fe9baf7f4 | ||
|
|
b195080012 | ||
|
|
3d17907966 | ||
|
|
45626b139d | ||
|
|
b30b6b1d66 | ||
|
|
6e6c321871 | ||
|
|
6addc04b97 | ||
|
|
717a58a872 | ||
|
|
1c89e1df32 | ||
|
|
5c4ec62d96 | ||
|
|
69a387547d | ||
|
|
8411de8887 | ||
|
|
b5121c5754 | ||
|
|
253d8a4016 | ||
|
|
2ba5cb48b2 | ||
|
|
e056fb2eb9 | ||
|
|
8fb6f92753 | ||
|
|
e5c1211e17 | ||
|
|
217124cb3b | ||
|
|
15f3c82238 | ||
|
|
c82a5ac0cb | ||
|
|
250cc0ec0f | ||
|
|
3ad4b2864d | ||
|
|
0f5dd661f5 | ||
|
|
ff1c19cac5 | ||
|
|
2a1059107a | ||
|
|
609523a59c | ||
|
|
e31905864b | ||
|
|
bb6c596b22 | ||
|
|
2745223dbf | ||
|
|
b847866310 | ||
|
|
f6942213c8 | ||
|
|
478ce03386 | ||
|
|
15f0dee719 | ||
|
|
7ddc71006b | ||
|
|
b0149972cc | ||
|
|
9b43e07d7f | ||
|
|
e357620740 | ||
|
|
052f975762 | ||
|
|
e5d2f883ac | ||
|
|
8396dc2fdb | ||
|
|
09fb539875 | ||
|
|
be4b65fdca | ||
|
|
0a4627f4f0 | ||
|
|
0502ef6cc7 | ||
|
|
2281b60ebb | ||
|
|
7d2e39ed52 | ||
|
|
e26837d9e8 | ||
|
|
3ecc0ee24b | ||
|
|
057db71f3b | ||
|
|
ce615e1855 | ||
|
|
87c54ebd4c | ||
|
|
a6e0a17454 | ||
|
|
9089122b56 | ||
|
|
e0286ee85d | ||
|
|
31f77af534 | ||
|
|
0d1478b635 | ||
|
|
d27fd0488d | ||
|
|
9c4b791621 | ||
|
|
9d87ae95e6 | ||
|
|
8316d39b42 | ||
|
|
7120f551c8 | ||
|
|
e4a3564706 | ||
|
|
4eb122e973 | ||
|
|
feabc21864 | ||
|
|
a904f85e61 | ||
|
|
584f441141 | ||
|
|
7944f23d95 | ||
|
|
639b34c7d1 | ||
|
|
ea1353422f | ||
|
|
5a548be16c | ||
|
|
39eccc62b1 | ||
|
|
ea25510a08 | ||
|
|
45ae984f3b | ||
|
|
2012e707d0 | ||
|
|
942cde79bd | ||
|
|
c37c3e0459 | ||
|
|
cab73c0d68 | ||
|
|
58129543de | ||
|
|
504aaddc32 | ||
|
|
6257ff123f | ||
|
|
aa3f3e2c43 | ||
|
|
70c5afd6a5 | ||
|
|
701fd10c1c | ||
|
|
6cb991fe7f | ||
|
|
ec7efcc9d6 | ||
|
|
489c29b472 | ||
|
|
5609e47c28 | ||
|
|
8796a52c09 | ||
|
|
12a8011fb3 | ||
|
|
47e2a1004d |
8
.codecov.yml
Normal file
8
.codecov.yml
Normal file
@@ -0,0 +1,8 @@
|
||||
coverage:
|
||||
status:
|
||||
project:
|
||||
default:
|
||||
target: 40%
|
||||
threshold: null
|
||||
patch: false
|
||||
changes: false
|
||||
16
.gitignore
vendored
16
.gitignore
vendored
@@ -1,20 +1,20 @@
|
||||
.DS_Store
|
||||
.vscode
|
||||
.idea
|
||||
debug
|
||||
/.vscode
|
||||
/.idea
|
||||
/AdGuardHome
|
||||
/AdGuardHome.exe
|
||||
/AdGuardHome.yaml
|
||||
/AdGuardHome.log
|
||||
/data/
|
||||
/build/
|
||||
/dist/
|
||||
/client/node_modules/
|
||||
/coredns
|
||||
/Corefile
|
||||
/dnsfilter.txt
|
||||
/querylog.json
|
||||
/querylog.json.1
|
||||
/scripts/translations/node_modules
|
||||
/scripts/translations/oneskyapp.json
|
||||
/a_main-packr.go
|
||||
|
||||
# Test output
|
||||
dnsfilter/dnsfilter.TestLotsOfRules*.pprof
|
||||
tests/top-1m.csv
|
||||
dnsfilter/tests/top-1m.csv
|
||||
dnsfilter/tests/dnsfilter.TestLotsOfRules*.pprof
|
||||
56
.golangci.yml
Normal file
56
.golangci.yml
Normal file
@@ -0,0 +1,56 @@
|
||||
# options for analysis running
|
||||
run:
|
||||
# default concurrency is a available CPU number
|
||||
concurrency: 4
|
||||
|
||||
# timeout for analysis, e.g. 30s, 5m, default is 1m
|
||||
deadline: 2m
|
||||
|
||||
# which files to skip: they will be analyzed, but issues from them
|
||||
# won't be reported. Default value is empty list, but there is
|
||||
# no need to include all autogenerated files, we confidently recognize
|
||||
# autogenerated files. If it's not please let us know.
|
||||
skip-files:
|
||||
- ".*generated.*"
|
||||
- dnsfilter/rule_to_regexp.go
|
||||
|
||||
|
||||
# all available settings of specific linters
|
||||
linters-settings:
|
||||
errcheck:
|
||||
# [deprecated] comma-separated list of pairs of the form pkg:regex
|
||||
# the regex is used to ignore names within pkg. (default "fmt:.*").
|
||||
# see https://github.com/kisielk/errcheck#the-deprecated-method for details
|
||||
ignore: fmt:.*,net:SetReadDeadline,net/http:^Write
|
||||
gocyclo:
|
||||
min-complexity: 20
|
||||
lll:
|
||||
line-length: 200
|
||||
|
||||
linters:
|
||||
enable-all: true
|
||||
disable:
|
||||
- interfacer
|
||||
- gocritic
|
||||
- scopelint
|
||||
- gochecknoglobals
|
||||
- gochecknoinits
|
||||
- prealloc
|
||||
- maligned
|
||||
- goconst # disabled until it's possible to configure
|
||||
fast: true
|
||||
|
||||
issues:
|
||||
# List of regexps of issue texts to exclude, empty list by default.
|
||||
# But independently from this option we use default exclude patterns,
|
||||
# it can be disabled by `exclude-use-default: false`. To list all
|
||||
# excluded by default patterns execute `golangci-lint run --help`
|
||||
exclude:
|
||||
# structcheck cannot detect usages while they're there
|
||||
- .parentalServer. is unused
|
||||
- .safeBrowsingServer. is unused
|
||||
# errcheck
|
||||
- Error return value of .s.closeConn. is not checked
|
||||
- Error return value of ..*.Shutdown.
|
||||
# goconst
|
||||
- string .forcesafesearch.google.com. has 3 occurrences
|
||||
29
.gometalinter.json
Normal file
29
.gometalinter.json
Normal file
@@ -0,0 +1,29 @@
|
||||
{
|
||||
"Vendor": true,
|
||||
"Test": true,
|
||||
"Deadline": "2m",
|
||||
"Sort": ["linter", "severity", "path", "line"],
|
||||
"Exclude": [
|
||||
".*generated.*",
|
||||
"dnsfilter/rule_to_regexp.go"
|
||||
],
|
||||
"EnableGC": true,
|
||||
"Linters": {
|
||||
"nakedret": {
|
||||
"Command": "nakedret",
|
||||
"Pattern": "^(?P<path>.*?\\.go):(?P<line>\\d+)\\s*(?P<message>.*)$"
|
||||
}
|
||||
},
|
||||
"WarnUnmatchedDirective": true,
|
||||
|
||||
"EnableAll": true,
|
||||
"DisableAll": false,
|
||||
"Disable": [
|
||||
"maligned",
|
||||
"goconst",
|
||||
"vetshadow"
|
||||
],
|
||||
|
||||
"Cyclo": 20,
|
||||
"LineLength": 200
|
||||
}
|
||||
77
.travis.yml
77
.travis.yml
@@ -1,8 +1,19 @@
|
||||
language: go
|
||||
sudo: false
|
||||
|
||||
go:
|
||||
- 1.11.x
|
||||
- 1.x
|
||||
os:
|
||||
- linux
|
||||
- osx
|
||||
|
||||
before_install:
|
||||
- nvm install node
|
||||
- npm install -g npm
|
||||
|
||||
install:
|
||||
- npm --prefix client install
|
||||
|
||||
cache:
|
||||
directories:
|
||||
@@ -10,17 +21,61 @@ cache:
|
||||
- $HOME/gopath/pkg/mod
|
||||
- $HOME/Library/Caches/go-build
|
||||
|
||||
os:
|
||||
- linux
|
||||
- osx
|
||||
|
||||
install:
|
||||
- go get -v -d -t ./...
|
||||
- npm --prefix client install
|
||||
- go env
|
||||
|
||||
script:
|
||||
- (cd `go env GOPATH`/src/github.com/prometheus/client_golang && git checkout -q v0.8.0)
|
||||
- go test ./...
|
||||
- node -v
|
||||
- npm -v
|
||||
# Run tests
|
||||
- go test -race -v -bench=. -coverprofile=coverage.txt -covermode=atomic ./...
|
||||
# Make
|
||||
- make build/static/index.html
|
||||
- make
|
||||
|
||||
after_success:
|
||||
- bash <(curl -s https://codecov.io/bash)
|
||||
|
||||
matrix:
|
||||
include:
|
||||
# Release build configuration
|
||||
- name: release
|
||||
go:
|
||||
- 1.11.x
|
||||
os:
|
||||
- linux
|
||||
|
||||
script:
|
||||
- node -v
|
||||
- npm -v
|
||||
# Run tests just in case
|
||||
- go test -race -v -bench=. ./...
|
||||
# Prepare releases
|
||||
- ./release.sh
|
||||
- ls -l dist
|
||||
|
||||
deploy:
|
||||
provider: releases
|
||||
api_key: $GITHUB_TOKEN
|
||||
file:
|
||||
- dist/AdGuardHome_*
|
||||
on:
|
||||
repo: AdguardTeam/AdGuardHome
|
||||
tags: true
|
||||
draft: true
|
||||
file_glob: true
|
||||
skip_cleanup: true
|
||||
|
||||
- name: docker
|
||||
if: type != pull_request AND (branch = master OR tag IS present)
|
||||
go:
|
||||
- 1.11.x
|
||||
os:
|
||||
- linux
|
||||
services:
|
||||
- docker
|
||||
before_script:
|
||||
- nvm install node
|
||||
- npm install -g npm
|
||||
script:
|
||||
- docker login -u="$DOCKER_USER" -p="$DOCKER_PASSWORD"
|
||||
- ./build_docker.sh
|
||||
after_script:
|
||||
- docker images
|
||||
|
||||
24
Dockerfile
Normal file
24
Dockerfile
Normal file
@@ -0,0 +1,24 @@
|
||||
FROM golang:alpine AS build
|
||||
|
||||
RUN apk add --update git make build-base npm && \
|
||||
rm -rf /var/cache/apk/*
|
||||
|
||||
WORKDIR /src/AdGuardHome
|
||||
COPY . /src/AdGuardHome
|
||||
RUN make
|
||||
|
||||
FROM alpine:latest
|
||||
LABEL maintainer="AdGuard Team <devteam@adguard.com>"
|
||||
|
||||
# Update CA certs
|
||||
RUN apk --no-cache --update add ca-certificates && \
|
||||
rm -rf /var/cache/apk/* && mkdir -p /opt/adguardhome
|
||||
|
||||
COPY --from=build /src/AdGuardHome/AdGuardHome /opt/adguardhome/AdGuardHome
|
||||
|
||||
EXPOSE 53/tcp 53/udp 67/tcp 67/udp 68/tcp 68/udp 80/tcp 443/tcp 853/tcp 853/udp 3000/tcp
|
||||
|
||||
VOLUME ["/opt/adguardhome/conf", "/opt/adguardhome/work"]
|
||||
|
||||
ENTRYPOINT ["/opt/adguardhome/AdGuardHome"]
|
||||
CMD ["-h", "0.0.0.0", "-c", "/opt/adguardhome/conf/AdGuardHome.yaml", "-w", "/opt/adguardhome/work"]
|
||||
@@ -1,48 +0,0 @@
|
||||
FROM easypi/alpine-arm:latest
|
||||
LABEL maintainer="Erik Rogers <erik.rogers@live.com>"
|
||||
|
||||
# AdGuard version
|
||||
ARG ADGUARD_VERSION="0.91"
|
||||
ENV ADGUARD_VERSION $ADGUARD_VERSION
|
||||
|
||||
# AdGuard architecture and package info
|
||||
ARG ADGUARD_ARCH="linux_arm"
|
||||
ENV ADGUARD_ARCH ${ADGUARD_ARCH}
|
||||
ENV ADGUARD_PACKAGE "AdGuardHome_v${ADGUARD_VERSION}_${ADGUARD_ARCH}"
|
||||
|
||||
# AdGuard release info
|
||||
ARG ADGUARD_ARCHIVE="${ADGUARD_PACKAGE}.tar.gz"
|
||||
ENV ADGUARD_ARCHIVE ${ADGUARD_ARCHIVE}
|
||||
ARG ADGUARD_RELEASE="https://github.com/AdguardTeam/AdGuardHome/releases/download/v${ADGUARD_VERSION}/${ADGUARD_ARCHIVE}"
|
||||
ENV ADGUARD_RELEASE ${ADGUARD_RELEASE}
|
||||
|
||||
# AdGuard directory
|
||||
ARG ADGUARD_DIR="/data/adguard"
|
||||
ENV ADGUARD_DIR ${ADGUARD_DIR}
|
||||
|
||||
# Update CA certs and download AdGuard binaries
|
||||
RUN apk --no-cache --update add ca-certificates \
|
||||
&& cd /tmp \
|
||||
&& wget ${ADGUARD_RELEASE} \
|
||||
&& tar xvf ${ADGUARD_ARCHIVE} \
|
||||
&& mkdir -p "${ADGUARD_DIR}" \
|
||||
&& cp "AdGuardHome/AdGuardHome" "${ADGUARD_DIR}" \
|
||||
&& chmod +x "${ADGUARD_DIR}/AdGuardHome" \
|
||||
&& rm -rf "AdGuardHome" \
|
||||
&& rm ${ADGUARD_ARCHIVE}
|
||||
|
||||
# Expose DNS port 53
|
||||
EXPOSE 53
|
||||
|
||||
# Expose UI port 3000
|
||||
ARG ADGUARD_UI_HOST="0.0.0.0"
|
||||
ENV ADGUARD_UI_HOST ${ADGUARD_UI_HOST}
|
||||
ARG ADGUARD_UI_PORT="3000"
|
||||
ENV ADGUARD_UI_PORT ${ADGUARD_UI_PORT}
|
||||
|
||||
EXPOSE ${ADGUARD_UI_PORT}
|
||||
|
||||
# Run AdGuardHome
|
||||
WORKDIR ${ADGUARD_DIR}
|
||||
VOLUME ${ADGUARD_DIR}
|
||||
ENTRYPOINT ./AdGuardHome --host ${ADGUARD_UI_HOST} --port ${ADGUARD_UI_PORT}
|
||||
@@ -1,48 +0,0 @@
|
||||
FROM alpine:latest
|
||||
LABEL maintainer="Erik Rogers <erik.rogers@live.com>"
|
||||
|
||||
# AdGuard version
|
||||
ARG ADGUARD_VERSION="0.91"
|
||||
ENV ADGUARD_VERSION $ADGUARD_VERSION
|
||||
|
||||
# AdGuard architecture and package info
|
||||
ARG ADGUARD_ARCH="linux_386"
|
||||
ENV ADGUARD_ARCH ${ADGUARD_ARCH}
|
||||
ENV ADGUARD_PACKAGE "AdGuardHome_v${ADGUARD_VERSION}_${ADGUARD_ARCH}"
|
||||
|
||||
# AdGuard release info
|
||||
ARG ADGUARD_ARCHIVE="${ADGUARD_PACKAGE}.tar.gz"
|
||||
ENV ADGUARD_ARCHIVE ${ADGUARD_ARCHIVE}
|
||||
ARG ADGUARD_RELEASE="https://github.com/AdguardTeam/AdGuardHome/releases/download/v${ADGUARD_VERSION}/${ADGUARD_ARCHIVE}"
|
||||
ENV ADGUARD_RELEASE ${ADGUARD_RELEASE}
|
||||
|
||||
# AdGuard directory
|
||||
ARG ADGUARD_DIR="/data/adguard"
|
||||
ENV ADGUARD_DIR ${ADGUARD_DIR}
|
||||
|
||||
# Update CA certs and download AdGuard binaries
|
||||
RUN apk --no-cache --update add ca-certificates \
|
||||
&& cd /tmp \
|
||||
&& wget ${ADGUARD_RELEASE} \
|
||||
&& tar xvf ${ADGUARD_ARCHIVE} \
|
||||
&& mkdir -p "${ADGUARD_DIR}" \
|
||||
&& cp "AdGuardHome/AdGuardHome" "${ADGUARD_DIR}" \
|
||||
&& chmod +x "${ADGUARD_DIR}/AdGuardHome" \
|
||||
&& rm -rf "AdGuardHome" \
|
||||
&& rm ${ADGUARD_ARCHIVE}
|
||||
|
||||
# Expose DNS port 53
|
||||
EXPOSE 53
|
||||
|
||||
# Expose UI port 3000
|
||||
ARG ADGUARD_UI_HOST="0.0.0.0"
|
||||
ENV ADGUARD_UI_HOST ${ADGUARD_UI_HOST}
|
||||
ARG ADGUARD_UI_PORT="3000"
|
||||
ENV ADGUARD_UI_PORT ${ADGUARD_UI_PORT}
|
||||
|
||||
EXPOSE ${ADGUARD_UI_PORT}
|
||||
|
||||
# Run AdGuardHome
|
||||
WORKDIR ${ADGUARD_DIR}
|
||||
VOLUME ${ADGUARD_DIR}
|
||||
ENTRYPOINT ./AdGuardHome --host ${ADGUARD_UI_HOST} --port ${ADGUARD_UI_PORT}
|
||||
@@ -1,48 +0,0 @@
|
||||
FROM alpine:latest
|
||||
LABEL maintainer="Erik Rogers <erik.rogers@live.com>"
|
||||
|
||||
# AdGuard version
|
||||
ARG ADGUARD_VERSION="0.91"
|
||||
ENV ADGUARD_VERSION $ADGUARD_VERSION
|
||||
|
||||
# AdGuard architecture and package info
|
||||
ARG ADGUARD_ARCH="linux_amd64"
|
||||
ENV ADGUARD_ARCH ${ADGUARD_ARCH}
|
||||
ENV ADGUARD_PACKAGE "AdGuardHome_v${ADGUARD_VERSION}_${ADGUARD_ARCH}"
|
||||
|
||||
# AdGuard release info
|
||||
ARG ADGUARD_ARCHIVE="${ADGUARD_PACKAGE}.tar.gz"
|
||||
ENV ADGUARD_ARCHIVE ${ADGUARD_ARCHIVE}
|
||||
ARG ADGUARD_RELEASE="https://github.com/AdguardTeam/AdGuardHome/releases/download/v${ADGUARD_VERSION}/${ADGUARD_ARCHIVE}"
|
||||
ENV ADGUARD_RELEASE ${ADGUARD_RELEASE}
|
||||
|
||||
# AdGuard directory
|
||||
ARG ADGUARD_DIR="/data/adguard"
|
||||
ENV ADGUARD_DIR ${ADGUARD_DIR}
|
||||
|
||||
# Update CA certs and download AdGuard binaries
|
||||
RUN apk --no-cache --update add ca-certificates \
|
||||
&& cd /tmp \
|
||||
&& wget ${ADGUARD_RELEASE} \
|
||||
&& tar xvf ${ADGUARD_ARCHIVE} \
|
||||
&& mkdir -p "${ADGUARD_DIR}" \
|
||||
&& cp "AdGuardHome/AdGuardHome" "${ADGUARD_DIR}" \
|
||||
&& chmod +x "${ADGUARD_DIR}/AdGuardHome" \
|
||||
&& rm -rf "AdGuardHome" \
|
||||
&& rm ${ADGUARD_ARCHIVE}
|
||||
|
||||
# Expose DNS port 53
|
||||
EXPOSE 53
|
||||
|
||||
# Expose UI port 3000
|
||||
ARG ADGUARD_UI_HOST="0.0.0.0"
|
||||
ENV ADGUARD_UI_HOST ${ADGUARD_UI_HOST}
|
||||
ARG ADGUARD_UI_PORT="3000"
|
||||
ENV ADGUARD_UI_PORT ${ADGUARD_UI_PORT}
|
||||
|
||||
EXPOSE ${ADGUARD_UI_PORT}
|
||||
|
||||
# Run AdGuardHome
|
||||
WORKDIR ${ADGUARD_DIR}
|
||||
VOLUME ${ADGUARD_DIR}
|
||||
ENTRYPOINT ./AdGuardHome --host ${ADGUARD_UI_HOST} --port ${ADGUARD_UI_PORT}
|
||||
16
Dockerfile.travis
Normal file
16
Dockerfile.travis
Normal file
@@ -0,0 +1,16 @@
|
||||
FROM alpine:latest
|
||||
LABEL maintainer="AdGuard Team <devteam@adguard.com>"
|
||||
|
||||
# Update CA certs
|
||||
RUN apk --no-cache --update add ca-certificates && \
|
||||
rm -rf /var/cache/apk/* && mkdir -p /opt/adguardhome
|
||||
|
||||
|
||||
COPY ./AdGuardHome /opt/adguardhome/AdGuardHome
|
||||
|
||||
EXPOSE 53/tcp 53/udp 67/tcp 67/udp 68/tcp 68/udp 80/tcp 443/tcp 853/tcp 853/udp 3000/tcp
|
||||
|
||||
VOLUME ["/opt/adguardhome/conf", "/opt/adguardhome/work"]
|
||||
|
||||
ENTRYPOINT ["/opt/adguardhome/AdGuardHome"]
|
||||
CMD ["-h", "0.0.0.0", "-c", "/opt/adguardhome/conf/AdGuardHome.yaml", "-w", "/opt/adguardhome/work"]
|
||||
8
Makefile
8
Makefile
@@ -19,9 +19,11 @@ client/node_modules: client/package.json client/package-lock.json
|
||||
$(STATIC): $(JSFILES) client/node_modules
|
||||
npm --prefix client run build-prod
|
||||
|
||||
$(TARGET): $(STATIC) *.go coredns_plugin/*.go dnsfilter/*.go
|
||||
GOPATH=$(GOPATH) GOOS=$(NATIVE_GOOS) GOARCH=$(NATIVE_GOARCH) GO111MODULE=off go get -v github.com/gobuffalo/packr/...
|
||||
GOPATH=$(GOPATH) PATH=$(GOPATH)/bin:$(PATH) packr build -ldflags="-X main.VersionString=$(GIT_VERSION)" -asmflags="-trimpath=$(PWD)" -gcflags="-trimpath=$(PWD)" -o $(TARGET)
|
||||
$(TARGET): $(STATIC) *.go dhcpd/*.go dnsfilter/*.go dnsforward/*.go
|
||||
GOOS=$(NATIVE_GOOS) GOARCH=$(NATIVE_GOARCH) GO111MODULE=off go get -v github.com/gobuffalo/packr/...
|
||||
PATH=$(GOPATH)/bin:$(PATH) packr -z
|
||||
CGO_ENABLED=0 go build -ldflags="-s -w -X main.VersionString=$(GIT_VERSION)" -asmflags="-trimpath=$(PWD)" -gcflags="-trimpath=$(PWD)"
|
||||
PATH=$(GOPATH)/bin:$(PATH) packr clean
|
||||
|
||||
clean:
|
||||
$(MAKE) cleanfast
|
||||
|
||||
132
README.md
132
README.md
@@ -11,11 +11,21 @@
|
||||
<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>
|
||||
<a href="https://twitter.com/AdGuard">Twitter</a> |
|
||||
<a href="https://t.me/adguard_en">Telegram</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://codecov.io/github/AdguardTeam/AdGuardHome?branch=master">
|
||||
<img src="https://img.shields.io/codecov/c/github/AdguardTeam/AdGuardHome/master.svg" alt="Code Coverage" />
|
||||
</a>
|
||||
<a href="https://goreportcard.com/report/AdguardTeam/AdGuardHome">
|
||||
<img src="https://goreportcard.com/badge/github.com/AdguardTeam/AdGuardHome" alt="Go Report Card" />
|
||||
</a>
|
||||
<a href="https://golangci.com/r/github.com/AdguardTeam/AdGuardHome">
|
||||
<img src="https://golangci.com/badges/github.com/AdguardTeam/AdGuardHome.svg" alt="GolangCI" />
|
||||
</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>
|
||||
@@ -29,93 +39,31 @@
|
||||
|
||||
<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?
|
||||
It operates as a DNS server that re-routes tracking domains to a "black hole," thus preventing your devices from connecting to those servers. It's based on software we use for our public [AdGuard DNS](https://adguard.com/en/adguard-dns/overview.html) servers -- both share a lot of common code.
|
||||
|
||||
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.
|
||||
* [Getting Started](#getting-started)
|
||||
* [How to build from source](#how-to-build)
|
||||
* [Contributing](#contributing)
|
||||
* [Reporting issues](#reporting-issues)
|
||||
* [Acknowledgments](#acknowledgments)
|
||||
|
||||
## How is this different from public AdGuard DNS servers?
|
||||
<a id="getting-started"></a>
|
||||
## Getting Started
|
||||
|
||||
Running your own AdGuard Home server allows you to do much more than using a public DNS server.
|
||||
Please read the [Getting Started](https://github.com/AdguardTeam/AdGuardHome/wiki/Getting-Started) article on our Wiki to learn how to install AdGuard Home, and how to configure your devices to use it.
|
||||
|
||||
* Choose what exactly will the server block or not block;
|
||||
* Monitor your network activity;
|
||||
* Add your own custom filtering rules;
|
||||
Alternatively, you can use our [official Docker image](https://hub.docker.com/r/adguard/adguardhome).
|
||||
|
||||
In the future, AdGuard Home is supposed to become more than just a DNS server.
|
||||
### Guides
|
||||
|
||||
## Installation
|
||||
|
||||
### Mac
|
||||
|
||||
Download this file: [AdGuardHome_v0.91_MacOS.zip](https://github.com/AdguardTeam/AdGuardHome/releases/download/v0.91/AdGuardHome_v0.91_MacOS.zip), then unpack it and follow ["How to run"](#how-to-run) instructions below.
|
||||
|
||||
### Linux 64-bit Intel
|
||||
|
||||
Download this file: [AdGuardHome_v0.91_linux_amd64.tar.gz](https://github.com/AdguardTeam/AdGuardHome/releases/download/v0.91/AdGuardHome_v0.91_linux_amd64.tar.gz), then unpack it and follow ["How to run"](#how-to-run) instructions below.
|
||||
|
||||
### Linux 32-bit Intel
|
||||
|
||||
Download this file: [AdGuardHome_v0.91_linux_386.tar.gz](https://github.com/AdguardTeam/AdGuardHome/releases/download/v0.91/AdGuardHome_v0.91_linux_386.tar.gz), then unpack it and follow ["How to run"](#how-to-run) instructions below.
|
||||
|
||||
### Raspberry Pi (32-bit ARM)
|
||||
|
||||
Download this file: [AdGuardHome_v0.91_linux_arm.tar.gz](https://github.com/AdguardTeam/AdGuardHome/releases/download/v0.91/AdGuardHome_v0.91_linux_arm.tar.gz), then unpack it and follow ["How to run"](#how-to-run) instructions below.
|
||||
|
||||
## How to run
|
||||
|
||||
DNS works on port 53, which requires superuser privileges. Therefore, you need to run it with `sudo` in terminal:
|
||||
|
||||
```bash
|
||||
sudo ./AdGuardHome
|
||||
```
|
||||
|
||||
Now open the browser and navigate to http://localhost:3000/ to control your AdGuard Home service.
|
||||
|
||||
### Running without superuser
|
||||
|
||||
You can run AdGuard Home without superuser privileges, but you need to instruct it to use a different port rather than 53. You can do that by editing `AdGuardHome.yaml` and finding these two lines:
|
||||
|
||||
```yaml
|
||||
coredns:
|
||||
port: 53
|
||||
```
|
||||
|
||||
You can change port 53 to anything above 1024 to avoid requiring superuser privileges.
|
||||
|
||||
If the file does not exist, create it in the same folder, type these two lines down and save.
|
||||
|
||||
### Additional configuration
|
||||
|
||||
Upon the first execution, a file named `AdGuardHome.yaml` will be created, with default values written in it. You can modify the file while your AdGuard Home service is not running. Otherwise, any changes to the file will be lost because the running program will overwrite them.
|
||||
|
||||
Settings are stored in [YAML format](https://en.wikipedia.org/wiki/YAML), possible parameters that you can configure are listed below:
|
||||
|
||||
* `bind_host` — Web interface IP address to listen on
|
||||
* `bind_port` — Web interface IP port to listen on
|
||||
* `auth_name` — Web interface optional authorization username
|
||||
* `auth_pass` — Web interface optional authorization password
|
||||
* `coredns` — CoreDNS configuration section
|
||||
* `port` — DNS server port to listen on
|
||||
* `filtering_enabled` — Filtering of DNS requests based on filter lists
|
||||
* `safebrowsing_enabled` — Filtering of DNS requests based on safebrowsing
|
||||
* `safesearch_enabled` — Enforcing "Safe search" option for search engines, when possible
|
||||
* `parental_enabled` — Parental control-based DNS requests filtering
|
||||
* `parental_sensitivity` — Age group for parental control-based filtering, must be either 3, 10, 13 or 17
|
||||
* `querylog_enabled` — Query logging (also used to calculate top 50 clients, blocked domains and requested domains for statistic purposes)
|
||||
* `bootstrap_dns` — DNS server used for initial hostnames resolution in case if upstream is DoH or DoT with a hostname
|
||||
* `upstream_dns` — List of upstream DNS servers
|
||||
* `filters` — List of filters, each filter has the following values:
|
||||
* `ID` - filter ID (must be unique)
|
||||
* `url` — URL pointing to the filter contents (filtering rules)
|
||||
* `enabled` — Current filter's status (enabled/disabled)
|
||||
* `user_rules` — User-specified filtering rules
|
||||
|
||||
Removing an entry from settings file will reset it to the default value. Deleting the file will reset all settings to the default values.
|
||||
* [Configuration](https://github.com/AdguardTeam/AdGuardHome/wiki/Configuration)
|
||||
* [AdGuard Home as a DNS-over-HTTPS or DNS-over-TLS server](https://github.com/AdguardTeam/AdGuardHome/wiki/Encryption)
|
||||
* [How to install and run AdGuard Home on Raspberry Pi](https://github.com/AdguardTeam/AdGuardHome/wiki/Raspberry-Pi)
|
||||
* [How to install and run AdGuard Home on a Virtual Private Server](https://github.com/AdguardTeam/AdGuardHome/wiki/VPS)
|
||||
|
||||
<a id="how-to-build"></a>
|
||||
## How to build from source
|
||||
|
||||
### Prerequisites
|
||||
@@ -123,7 +71,7 @@ Removing an entry from settings file will reset it to the default value. Deletin
|
||||
You will need:
|
||||
|
||||
* [go](https://golang.org/dl/) v1.11 or later.
|
||||
* [node.js](https://nodejs.org/en/download/)
|
||||
* [node.js](https://nodejs.org/en/download/) v10 or later.
|
||||
|
||||
You can either install it via the provided links or use [brew.sh](https://brew.sh/) if you're on Mac:
|
||||
|
||||
@@ -141,8 +89,17 @@ cd AdGuardHome
|
||||
make
|
||||
```
|
||||
|
||||
<a id="contributing"></a>
|
||||
## Contributing
|
||||
|
||||
You are welcome to fork this repository, make your changes and submit a pull request — https://github.com/AdguardTeam/AdGuardHome/pulls
|
||||
|
||||
### How to update translations
|
||||
|
||||
If you want to help with AdGuard Home translations, please learn more about translating AdGuard products here: https://kb.adguard.com/en/general/adguard-translations
|
||||
|
||||
Here is a direct link to AdGuard Home project: http://translate.adguard.com/collaboration/project?id=153384
|
||||
|
||||
Before updating translations you need to install dependencies:
|
||||
```
|
||||
cd scripts/translations
|
||||
@@ -171,32 +128,29 @@ node upload.js
|
||||
node download.js
|
||||
```
|
||||
|
||||
## Contributing
|
||||
|
||||
You are welcome to fork this repository, make your changes and submit a pull request — https://github.com/AdguardTeam/AdGuardHome/pulls
|
||||
|
||||
If you want to help with AdGuard Home translations, please learn more about translating AdGuard products here: https://kb.adguard.com/en/general/adguard-translations
|
||||
|
||||
Here is a direct link to AdGuard Home project: http://translate.adguard.com/collaboration/project?id=153384
|
||||
|
||||
<a id="reporting-issues"></a>
|
||||
## Reporting issues
|
||||
|
||||
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.
|
||||
|
||||
<a id="acknowledgments"></a>
|
||||
## 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)
|
||||
* [service](https://godoc.org/github.com/kardianos/service)
|
||||
* [dnsproxy](https://github.com/AdguardTeam/dnsproxy)
|
||||
* [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)
|
||||
|
||||
You might have seen that [CoreDNS](https://coredns.io) was mentioned here before — we've stopped using it in AdGuardHome. While we still use it on our servers for [AdGuard DNS](https://adguard.com/adguard-dns/overview.html) service, it seemed like an overkill for Home as it impeded with Home features that we plan to implement.
|
||||
|
||||
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.
|
||||
|
||||
655
app.go
655
app.go
@@ -1,340 +1,427 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"crypto/tls"
|
||||
"fmt"
|
||||
"log"
|
||||
stdlog "log"
|
||||
"net"
|
||||
"net/http"
|
||||
"os"
|
||||
"os/signal"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strconv"
|
||||
"sync"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"github.com/gobuffalo/packr"
|
||||
"golang.org/x/crypto/ssh/terminal"
|
||||
|
||||
"github.com/hmage/golibs/log"
|
||||
)
|
||||
|
||||
// VersionString will be set through ldflags, contains current version
|
||||
var VersionString = "undefined"
|
||||
var httpServer *http.Server
|
||||
var httpsServer struct {
|
||||
server *http.Server
|
||||
cond *sync.Cond // reacts to config.TLS.Enabled, PortHTTPS, CertificateChain and PrivateKey
|
||||
sync.Mutex // protects config.TLS
|
||||
}
|
||||
|
||||
const (
|
||||
// Used in config to indicate that syslog or eventlog (win) should be used for logger output
|
||||
configSyslog = "syslog"
|
||||
)
|
||||
|
||||
// main is the entry point
|
||||
func main() {
|
||||
log.Printf("AdGuard Home web interface backend, version %s\n", VersionString)
|
||||
box := packr.NewBox("build/static")
|
||||
{
|
||||
executable, err := os.Executable()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
executableName := filepath.Base(executable)
|
||||
if executableName == "AdGuardHome" {
|
||||
// Binary build
|
||||
config.ourBinaryDir = filepath.Dir(executable)
|
||||
} else {
|
||||
// Most likely we're debugging -- using current working directory in this case
|
||||
workDir, _ := os.Getwd()
|
||||
config.ourBinaryDir = workDir
|
||||
}
|
||||
log.Printf("Current working directory is %s", config.ourBinaryDir)
|
||||
}
|
||||
|
||||
// config can be specified, which reads options from there, but other command line flags have to override config values
|
||||
// therefore, we must do it manually instead of using a lib
|
||||
{
|
||||
var configFilename *string
|
||||
var bindHost *string
|
||||
var bindPort *int
|
||||
var opts = []struct {
|
||||
longName string
|
||||
shortName string
|
||||
description string
|
||||
callback func(value string)
|
||||
}{
|
||||
{"config", "c", "path to config file", func(value string) { configFilename = &value }},
|
||||
{"host", "h", "host address to bind HTTP server on", func(value string) { bindHost = &value }},
|
||||
{"port", "p", "port to serve HTTP pages on", func(value string) {
|
||||
v, err := strconv.Atoi(value)
|
||||
if err != nil {
|
||||
panic("Got port that is not a number")
|
||||
}
|
||||
bindPort = &v
|
||||
}},
|
||||
{"help", "h", "print this help", nil},
|
||||
}
|
||||
printHelp := func() {
|
||||
fmt.Printf("Usage:\n\n")
|
||||
fmt.Printf("%s [options]\n\n", os.Args[0])
|
||||
fmt.Printf("Options:\n")
|
||||
for _, opt := range opts {
|
||||
fmt.Printf(" -%s, %-30s %s\n", opt.shortName, "--"+opt.longName, opt.description)
|
||||
}
|
||||
}
|
||||
for i := 1; i < len(os.Args); i++ {
|
||||
v := os.Args[i]
|
||||
// short-circuit for help
|
||||
if v == "--help" || v == "-h" {
|
||||
printHelp()
|
||||
os.Exit(64)
|
||||
}
|
||||
knownParam := false
|
||||
for _, opt := range opts {
|
||||
if v == "--"+opt.longName {
|
||||
if i+1 > len(os.Args) {
|
||||
log.Printf("ERROR: Got %s without argument\n", v)
|
||||
os.Exit(64)
|
||||
}
|
||||
i++
|
||||
opt.callback(os.Args[i])
|
||||
knownParam = true
|
||||
break
|
||||
}
|
||||
if v == "-"+opt.shortName {
|
||||
if i+1 > len(os.Args) {
|
||||
log.Printf("ERROR: Got %s without argument\n", v)
|
||||
os.Exit(64)
|
||||
}
|
||||
i++
|
||||
opt.callback(os.Args[i])
|
||||
knownParam = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !knownParam {
|
||||
log.Printf("ERROR: unknown option %v\n", v)
|
||||
printHelp()
|
||||
os.Exit(64)
|
||||
}
|
||||
}
|
||||
if configFilename != nil {
|
||||
config.ourConfigFilename = *configFilename
|
||||
}
|
||||
args := loadOptions()
|
||||
|
||||
err := askUsernamePasswordIfPossible()
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
// parse from config file
|
||||
err = parseConfig()
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
// override bind host/port from the console
|
||||
if bindHost != nil {
|
||||
config.BindHost = *bindHost
|
||||
}
|
||||
if bindPort != nil {
|
||||
config.BindPort = *bindPort
|
||||
}
|
||||
if args.serviceControlAction != "" {
|
||||
handleServiceControlAction(args.serviceControlAction)
|
||||
return
|
||||
}
|
||||
|
||||
// Eat all args so that coredns can start happily
|
||||
if len(os.Args) > 1 {
|
||||
os.Args = os.Args[:1]
|
||||
signalChannel := make(chan os.Signal)
|
||||
signal.Notify(signalChannel, syscall.SIGINT, syscall.SIGTERM, syscall.SIGHUP, syscall.SIGQUIT)
|
||||
go func() {
|
||||
<-signalChannel
|
||||
cleanup()
|
||||
os.Exit(0)
|
||||
}()
|
||||
|
||||
// run the protection
|
||||
run(args)
|
||||
}
|
||||
|
||||
// run initializes configuration and runs the AdGuard Home
|
||||
// run is a blocking method and it won't exit until the service is stopped!
|
||||
func run(args options) {
|
||||
// config file path can be overridden by command-line arguments:
|
||||
if args.configFilename != "" {
|
||||
config.ourConfigFilename = args.configFilename
|
||||
}
|
||||
|
||||
// configure working dir and config path
|
||||
initWorkingDir(args)
|
||||
|
||||
// configure log level and output
|
||||
configureLogger(args)
|
||||
|
||||
// print the first message after logger is configured
|
||||
log.Printf("AdGuard Home, version %s\n", VersionString)
|
||||
log.Tracef("Current working directory is %s", config.ourWorkingDir)
|
||||
if args.runningAsService {
|
||||
log.Printf("AdGuard Home is running as a service")
|
||||
}
|
||||
|
||||
config.firstRun = detectFirstRun()
|
||||
|
||||
// Do the upgrade if necessary
|
||||
err := upgradeConfig()
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
// Save the updated config
|
||||
err = writeConfig()
|
||||
// parse from config file
|
||||
err = parseConfig()
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
// override bind host/port from the console
|
||||
if args.bindHost != "" {
|
||||
config.BindHost = args.bindHost
|
||||
}
|
||||
if args.bindPort != 0 {
|
||||
config.BindPort = args.bindPort
|
||||
}
|
||||
|
||||
// Load filters from the disk
|
||||
// And if any filter has zero ID, assign a new one
|
||||
for i := range config.Filters {
|
||||
filter := &config.Filters[i]
|
||||
filter := &config.Filters[i] // otherwise we're operating on a copy
|
||||
if filter.ID == 0 {
|
||||
filter.ID = assignUniqueFilterID()
|
||||
}
|
||||
err = filter.load()
|
||||
if err != nil {
|
||||
// This is okay for the first start, the filter will be loaded later
|
||||
log.Printf("Couldn't load filter %d contents due to %s", filter.ID, err)
|
||||
// clear LastUpdated so it gets fetched right away
|
||||
}
|
||||
|
||||
if len(filter.Rules) == 0 {
|
||||
filter.LastUpdated = time.Time{}
|
||||
}
|
||||
}
|
||||
|
||||
address := net.JoinHostPort(config.BindHost, strconv.Itoa(config.BindPort))
|
||||
|
||||
runFiltersUpdatesTimer()
|
||||
|
||||
http.Handle("/", optionalAuthHandler(http.FileServer(box)))
|
||||
registerControlHandlers()
|
||||
|
||||
err = startDNSServer()
|
||||
// Save the updated config
|
||||
err = config.write()
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
URL := fmt.Sprintf("http://%s", address)
|
||||
log.Println("Go to " + URL)
|
||||
log.Fatal(http.ListenAndServe(address, nil))
|
||||
}
|
||||
// Init the DNS server instance before registering HTTP handlers
|
||||
dnsBaseDir := filepath.Join(config.ourWorkingDir, dataDir)
|
||||
initDNSServer(dnsBaseDir)
|
||||
|
||||
func getInput() (string, error) {
|
||||
scanner := bufio.NewScanner(os.Stdin)
|
||||
scanner.Scan()
|
||||
text := scanner.Text()
|
||||
err := scanner.Err()
|
||||
return text, err
|
||||
}
|
||||
|
||||
func promptAndGet(prompt string) (string, error) {
|
||||
for {
|
||||
fmt.Printf(prompt)
|
||||
input, err := getInput()
|
||||
if err != nil {
|
||||
log.Printf("Failed to get input, aborting: %s", err)
|
||||
return "", err
|
||||
}
|
||||
if len(input) != 0 {
|
||||
return input, nil
|
||||
}
|
||||
// try again
|
||||
}
|
||||
}
|
||||
|
||||
func promptAndGetPassword(prompt string) (string, error) {
|
||||
for {
|
||||
fmt.Printf(prompt)
|
||||
password, err := terminal.ReadPassword(int(os.Stdin.Fd()))
|
||||
fmt.Printf("\n")
|
||||
if err != nil {
|
||||
log.Printf("Failed to get input, aborting: %s", err)
|
||||
return "", err
|
||||
}
|
||||
if len(password) != 0 {
|
||||
return string(password), nil
|
||||
}
|
||||
// try again
|
||||
}
|
||||
}
|
||||
|
||||
func askUsernamePasswordIfPossible() error {
|
||||
configfile := config.ourConfigFilename
|
||||
if !filepath.IsAbs(configfile) {
|
||||
configfile = filepath.Join(config.ourBinaryDir, config.ourConfigFilename)
|
||||
}
|
||||
_, err := os.Stat(configfile)
|
||||
if !os.IsNotExist(err) {
|
||||
// do nothing, file exists
|
||||
trace("File %s exists, won't ask for password", configfile)
|
||||
return nil
|
||||
}
|
||||
if !terminal.IsTerminal(int(os.Stdin.Fd())) {
|
||||
return nil // do nothing
|
||||
}
|
||||
if !terminal.IsTerminal(int(os.Stdout.Fd())) {
|
||||
return nil // do nothing
|
||||
}
|
||||
fmt.Printf("Would you like to set user/password for the web interface authentication (yes/no)?\n")
|
||||
yesno, err := promptAndGet("Please type 'yes' or 'no': ")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if yesno[0] != 'y' && yesno[0] != 'Y' {
|
||||
return nil
|
||||
}
|
||||
username, err := promptAndGet("Please enter the username: ")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
password, err := promptAndGetPassword("Please enter the password: ")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
password2, err := promptAndGetPassword("Please enter password again: ")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if password2 != password {
|
||||
fmt.Printf("Passwords do not match! Aborting\n")
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
config.AuthName = username
|
||||
config.AuthPass = password
|
||||
return nil
|
||||
}
|
||||
|
||||
// Performs necessary upgrade operations if needed
|
||||
func upgradeConfig() error {
|
||||
|
||||
if config.SchemaVersion == SchemaVersion {
|
||||
// No upgrade, do nothing
|
||||
return nil
|
||||
}
|
||||
|
||||
if config.SchemaVersion > SchemaVersion {
|
||||
// Unexpected -- the config file is newer than we expect
|
||||
return fmt.Errorf("configuration file is supposed to be used with a newer version of AdGuard Home, schema=%d", config.SchemaVersion)
|
||||
}
|
||||
|
||||
// Perform upgrade operations for each consecutive version upgrade
|
||||
for oldVersion, newVersion := config.SchemaVersion, config.SchemaVersion+1; newVersion <= SchemaVersion; {
|
||||
|
||||
err := upgradeConfigSchema(oldVersion, newVersion)
|
||||
if !config.firstRun {
|
||||
err = startDNSServer()
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
// Increment old and new versions
|
||||
oldVersion++
|
||||
newVersion++
|
||||
}
|
||||
|
||||
// Save the current schema version
|
||||
config.SchemaVersion = SchemaVersion
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Upgrade from oldVersion to newVersion
|
||||
func upgradeConfigSchema(oldVersion int, newVersion int) error {
|
||||
|
||||
if oldVersion == 0 && newVersion == 1 {
|
||||
log.Printf("Updating schema from %d to %d", oldVersion, newVersion)
|
||||
|
||||
// The first schema upgrade:
|
||||
// Added "ID" field to "filter" -- we need to populate this field now
|
||||
// Added "config.ourDataDir" -- where we will now store filters contents
|
||||
for i := range config.Filters {
|
||||
|
||||
filter := &config.Filters[i] // otherwise we will be operating on a copy
|
||||
|
||||
// Set the filter ID
|
||||
log.Printf("Seting ID=%d for filter %s", NextFilterId, filter.URL)
|
||||
filter.ID = NextFilterId
|
||||
NextFilterId++
|
||||
|
||||
// Forcibly update the filter
|
||||
_, err := filter.update(true)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
// Saving it to the filters dir now
|
||||
err = filter.save()
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
// No more "dnsfilter.txt", filters are now loaded from config.ourDataDir/filters/
|
||||
dnsFilterPath := filepath.Join(config.ourBinaryDir, "dnsfilter.txt")
|
||||
_, err := os.Stat(dnsFilterPath)
|
||||
if !os.IsNotExist(err) {
|
||||
log.Printf("Deleting %s as we don't need it anymore", dnsFilterPath)
|
||||
err = os.Remove(dnsFilterPath)
|
||||
if err != nil {
|
||||
log.Printf("Cannot remove %s due to %s", dnsFilterPath, err)
|
||||
}
|
||||
err = startDHCPServer()
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
// Update filters we've just loaded right away, don't wait for periodic update timer
|
||||
go func() {
|
||||
refreshFiltersIfNecessary(false)
|
||||
// Save the updated config
|
||||
err := config.write()
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
}()
|
||||
// Schedule automatic filters updates
|
||||
go periodicallyRefreshFilters()
|
||||
|
||||
// Initialize and run the admin Web interface
|
||||
box := packr.NewBox("build/static")
|
||||
// if not configured, redirect / to /install.html, otherwise redirect /install.html to /
|
||||
http.Handle("/", postInstallHandler(optionalAuthHandler(http.FileServer(box))))
|
||||
registerControlHandlers()
|
||||
|
||||
// add handlers for /install paths, we only need them when we're not configured yet
|
||||
if config.firstRun {
|
||||
log.Printf("This is the first launch of AdGuard Home, redirecting everything to /install.html ")
|
||||
http.Handle("/install.html", preInstallHandler(http.FileServer(box)))
|
||||
registerInstallHandlers()
|
||||
}
|
||||
|
||||
httpsServer.cond = sync.NewCond(&httpsServer.Mutex)
|
||||
|
||||
// for https, we have a separate goroutine loop
|
||||
go func() {
|
||||
for { // this is an endless loop
|
||||
httpsServer.cond.L.Lock()
|
||||
// this mechanism doesn't let us through until all conditions are ment
|
||||
for config.TLS.Enabled == false || config.TLS.PortHTTPS == 0 || config.TLS.PrivateKey == "" || config.TLS.CertificateChain == "" { // sleep until necessary data is supplied
|
||||
httpsServer.cond.Wait()
|
||||
}
|
||||
address := net.JoinHostPort(config.BindHost, strconv.Itoa(config.TLS.PortHTTPS))
|
||||
// validate current TLS config and update warnings (it could have been loaded from file)
|
||||
data := validateCertificates(config.TLS)
|
||||
if !data.usable {
|
||||
log.Fatal(data.WarningValidation)
|
||||
os.Exit(1)
|
||||
}
|
||||
config.Lock()
|
||||
config.TLS = data // update warnings
|
||||
config.Unlock()
|
||||
|
||||
// prepare certs for HTTPS server
|
||||
// important -- they have to be copies, otherwise changing the contents in config.TLS will break encryption for in-flight requests
|
||||
certchain := make([]byte, len(config.TLS.CertificateChain))
|
||||
copy(certchain, []byte(config.TLS.CertificateChain))
|
||||
privatekey := make([]byte, len(config.TLS.PrivateKey))
|
||||
copy(privatekey, []byte(config.TLS.PrivateKey))
|
||||
cert, err := tls.X509KeyPair(certchain, privatekey)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
os.Exit(1)
|
||||
}
|
||||
httpsServer.cond.L.Unlock()
|
||||
|
||||
// prepare HTTPS server
|
||||
httpsServer.server = &http.Server{
|
||||
Addr: address,
|
||||
TLSConfig: &tls.Config{
|
||||
Certificates: []tls.Certificate{cert},
|
||||
},
|
||||
}
|
||||
|
||||
printHTTPAddresses("https")
|
||||
err = httpsServer.server.ListenAndServeTLS("", "")
|
||||
if err != http.ErrServerClosed {
|
||||
log.Fatal(err)
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
// this loop is used as an ability to change listening host and/or port
|
||||
for {
|
||||
printHTTPAddresses("http")
|
||||
|
||||
// we need to have new instance, because after Shutdown() the Server is not usable
|
||||
address := net.JoinHostPort(config.BindHost, strconv.Itoa(config.BindPort))
|
||||
httpServer = &http.Server{
|
||||
Addr: address,
|
||||
}
|
||||
err := httpServer.ListenAndServe()
|
||||
if err != http.ErrServerClosed {
|
||||
log.Fatal(err)
|
||||
os.Exit(1)
|
||||
}
|
||||
// We use ErrServerClosed as a sign that we need to rebind on new address, so go back to the start of the loop
|
||||
}
|
||||
}
|
||||
|
||||
// initWorkingDir initializes the ourWorkingDir
|
||||
// if no command-line arguments specified, we use the directory where our binary file is located
|
||||
func initWorkingDir(args options) {
|
||||
exec, err := os.Executable()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
if args.workDir != "" {
|
||||
// If there is a custom config file, use it's directory as our working dir
|
||||
config.ourWorkingDir = args.workDir
|
||||
} else {
|
||||
config.ourWorkingDir = filepath.Dir(exec)
|
||||
}
|
||||
}
|
||||
|
||||
// configureLogger configures logger level and output
|
||||
func configureLogger(args options) {
|
||||
ls := getLogSettings()
|
||||
|
||||
// command-line arguments can override config settings
|
||||
if args.verbose {
|
||||
ls.Verbose = true
|
||||
}
|
||||
if args.logFile != "" {
|
||||
ls.LogFile = args.logFile
|
||||
}
|
||||
|
||||
log.Verbose = ls.Verbose
|
||||
|
||||
if args.runningAsService && ls.LogFile == "" && runtime.GOOS == "windows" {
|
||||
// When running as a Windows service, use eventlog by default if nothing else is configured
|
||||
// Otherwise, we'll simply loose the log output
|
||||
ls.LogFile = configSyslog
|
||||
}
|
||||
|
||||
if ls.LogFile == "" {
|
||||
return
|
||||
}
|
||||
|
||||
if ls.LogFile == configSyslog {
|
||||
// Use syslog where it is possible and eventlog on Windows
|
||||
err := configureSyslog()
|
||||
if err != nil {
|
||||
log.Fatalf("cannot initialize syslog: %s", err)
|
||||
}
|
||||
} else {
|
||||
logFilePath := filepath.Join(config.ourWorkingDir, ls.LogFile)
|
||||
file, err := os.OpenFile(logFilePath, os.O_WRONLY|os.O_CREATE|os.O_APPEND, 0755)
|
||||
if err != nil {
|
||||
log.Fatalf("cannot create a log file: %s", err)
|
||||
}
|
||||
stdlog.SetOutput(file)
|
||||
}
|
||||
}
|
||||
|
||||
func cleanup() {
|
||||
log.Printf("Stopping AdGuard Home")
|
||||
|
||||
err := stopDNSServer()
|
||||
if err != nil {
|
||||
log.Printf("Couldn't stop DNS server: %s", err)
|
||||
}
|
||||
err = stopDHCPServer()
|
||||
if err != nil {
|
||||
log.Printf("Couldn't stop DHCP server: %s", err)
|
||||
}
|
||||
}
|
||||
|
||||
// command-line arguments
|
||||
type options struct {
|
||||
verbose bool // is verbose logging enabled
|
||||
configFilename string // path to the config file
|
||||
workDir string // path to the working directory where we will store the filters data and the querylog
|
||||
bindHost string // host address to bind HTTP server on
|
||||
bindPort int // port to serve HTTP pages on
|
||||
logFile string // Path to the log file. If empty, write to stdout. If "syslog", writes to syslog
|
||||
|
||||
// service control action (see service.ControlAction array + "status" command)
|
||||
serviceControlAction string
|
||||
|
||||
// runningAsService flag is set to true when options are passed from the service runner
|
||||
runningAsService bool
|
||||
}
|
||||
|
||||
// loadOptions reads command line arguments and initializes configuration
|
||||
func loadOptions() options {
|
||||
o := options{}
|
||||
|
||||
var printHelp func()
|
||||
var opts = []struct {
|
||||
longName string
|
||||
shortName string
|
||||
description string
|
||||
callbackWithValue func(value string)
|
||||
callbackNoValue func()
|
||||
}{
|
||||
{"config", "c", "path to the config file", func(value string) { o.configFilename = value }, nil},
|
||||
{"work-dir", "w", "path to the working directory", func(value string) { o.workDir = value }, nil},
|
||||
{"host", "h", "host address to bind HTTP server on", func(value string) { o.bindHost = value }, nil},
|
||||
{"port", "p", "port to serve HTTP pages on", func(value string) {
|
||||
v, err := strconv.Atoi(value)
|
||||
if err != nil {
|
||||
panic("Got port that is not a number")
|
||||
}
|
||||
o.bindPort = v
|
||||
}, nil},
|
||||
{"service", "s", "service control action: status, install, uninstall, start, stop, restart", func(value string) {
|
||||
o.serviceControlAction = value
|
||||
}, nil},
|
||||
{"logfile", "l", "path to the log file. If empty, writes to stdout, if 'syslog' -- system log", func(value string) {
|
||||
o.logFile = value
|
||||
}, nil},
|
||||
{"verbose", "v", "enable verbose output", nil, func() { o.verbose = true }},
|
||||
{"help", "", "print this help", nil, func() {
|
||||
printHelp()
|
||||
os.Exit(64)
|
||||
}},
|
||||
}
|
||||
printHelp = func() {
|
||||
fmt.Printf("Usage:\n\n")
|
||||
fmt.Printf("%s [options]\n\n", os.Args[0])
|
||||
fmt.Printf("Options:\n")
|
||||
for _, opt := range opts {
|
||||
if opt.shortName != "" {
|
||||
fmt.Printf(" -%s, %-30s %s\n", opt.shortName, "--"+opt.longName, opt.description)
|
||||
} else {
|
||||
fmt.Printf(" %-34s %s\n", "--"+opt.longName, opt.description)
|
||||
}
|
||||
}
|
||||
}
|
||||
for i := 1; i < len(os.Args); i++ {
|
||||
v := os.Args[i]
|
||||
knownParam := false
|
||||
for _, opt := range opts {
|
||||
if v == "--"+opt.longName || (opt.shortName != "" && v == "-"+opt.shortName) {
|
||||
if opt.callbackWithValue != nil {
|
||||
if i+1 >= len(os.Args) {
|
||||
log.Printf("ERROR: Got %s without argument\n", v)
|
||||
os.Exit(64)
|
||||
}
|
||||
i++
|
||||
opt.callbackWithValue(os.Args[i])
|
||||
} else if opt.callbackNoValue != nil {
|
||||
opt.callbackNoValue()
|
||||
}
|
||||
knownParam = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !knownParam {
|
||||
log.Printf("ERROR: unknown option %v\n", v)
|
||||
printHelp()
|
||||
os.Exit(64)
|
||||
}
|
||||
}
|
||||
|
||||
return o
|
||||
}
|
||||
|
||||
// prints IP addresses which user can use to open the admin interface
|
||||
// proto is either "http" or "https"
|
||||
func printHTTPAddresses(proto string) {
|
||||
var address string
|
||||
|
||||
if proto == "https" && config.TLS.ServerName != "" {
|
||||
if config.TLS.PortHTTPS == 443 {
|
||||
log.Printf("Go to https://%s", config.TLS.ServerName)
|
||||
} else {
|
||||
log.Printf("Go to https://%s:%d", config.TLS.ServerName, config.TLS.PortHTTPS)
|
||||
}
|
||||
} else if config.BindHost == "0.0.0.0" {
|
||||
log.Println("AdGuard Home is available on the following addresses:")
|
||||
ifaces, err := getValidNetInterfacesForWeb()
|
||||
if err != nil {
|
||||
// That's weird, but we'll ignore it
|
||||
address = net.JoinHostPort(config.BindHost, strconv.Itoa(config.BindPort))
|
||||
log.Printf("Go to %s://%s", proto, address)
|
||||
return
|
||||
}
|
||||
|
||||
for _, iface := range ifaces {
|
||||
address = net.JoinHostPort(iface.Addresses[0], strconv.Itoa(config.BindPort))
|
||||
log.Printf("Go to %s://%s", proto, address)
|
||||
}
|
||||
} else {
|
||||
address = net.JoinHostPort(config.BindHost, strconv.Itoa(config.BindPort))
|
||||
log.Printf("Go to %s://%s", proto, address)
|
||||
}
|
||||
}
|
||||
|
||||
74
build_docker.sh
Executable file
74
build_docker.sh
Executable file
@@ -0,0 +1,74 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
set -eE
|
||||
set -o pipefail
|
||||
set -x
|
||||
|
||||
DOCKERFILE="Dockerfile.travis"
|
||||
IMAGE_NAME="adguard/adguardhome"
|
||||
|
||||
if [[ "${TRAVIS_BRANCH}" == "master" ]]
|
||||
then
|
||||
VERSION="edge"
|
||||
else
|
||||
VERSION=`git describe --abbrev=4 --dirty --always --tags`
|
||||
fi
|
||||
|
||||
build_image() {
|
||||
from="$(awk '$1 == toupper("FROM") { print $2 }' ${DOCKERFILE})"
|
||||
|
||||
# See https://hub.docker.com/r/multiarch/alpine/tags
|
||||
case "${GOARCH}" in
|
||||
arm64)
|
||||
alpineArch='arm64-edge'
|
||||
imageArch='arm64'
|
||||
;;
|
||||
arm)
|
||||
alpineArch='armhf-edge'
|
||||
imageArch='armhf'
|
||||
;;
|
||||
386)
|
||||
alpineArch='i386-edge'
|
||||
imageArch='i386'
|
||||
;;
|
||||
amd64)
|
||||
alpineArch='amd64-edge'
|
||||
;;
|
||||
*)
|
||||
alpineArch='amd64-edge'
|
||||
;;
|
||||
esac
|
||||
|
||||
if [[ "${GOOS}" == "linux" ]] && [[ "${GOARCH}" == "amd64" ]]
|
||||
then
|
||||
image="${IMAGE_NAME}:${VERSION}"
|
||||
else
|
||||
image="${IMAGE_NAME}:${imageArch}-${VERSION}"
|
||||
fi
|
||||
|
||||
make cleanfast; CGO_DISABLED=1 make
|
||||
|
||||
docker pull "multiarch/alpine:${alpineArch}"
|
||||
docker tag "multiarch/alpine:${alpineArch}" "$from"
|
||||
docker build -t "${image}" -f ${DOCKERFILE} .
|
||||
docker push ${image}
|
||||
if [[ "${VERSION}" != "edge" ]]
|
||||
then
|
||||
latest=${image/$VERSION/latest}
|
||||
docker tag "${image}" "${latest}"
|
||||
docker push ${latest}
|
||||
docker rmi ${latest}
|
||||
fi
|
||||
docker rmi "$from"
|
||||
}
|
||||
|
||||
# prepare qemu
|
||||
docker run --rm --privileged multiarch/qemu-user-static:register --reset
|
||||
|
||||
make clean
|
||||
|
||||
# Prepare releases
|
||||
GOOS=linux GOARCH=amd64 build_image
|
||||
GOOS=linux GOARCH=386 build_image
|
||||
GOOS=linux GOARCH=arm GOARM=6 build_image
|
||||
GOOS=linux GOARCH=arm64 GOARM=6 build_image
|
||||
4
client/.eslintrc
vendored
4
client/.eslintrc
vendored
@@ -45,9 +45,7 @@
|
||||
}],
|
||||
"class-methods-use-this": "off",
|
||||
"no-shadow": "off",
|
||||
"camelcase": ["error", {
|
||||
"properties": "never"
|
||||
}],
|
||||
"camelcase": "off",
|
||||
"no-console": ["warn", { "allow": ["warn", "error"] }],
|
||||
"import/no-extraneous-dependencies": ["error", { "devDependencies": true }],
|
||||
"import/prefer-default-export": "off"
|
||||
|
||||
7183
client/package-lock.json
generated
vendored
7183
client/package-lock.json
generated
vendored
File diff suppressed because it is too large
Load Diff
12
client/package.json
vendored
12
client/package.json
vendored
@@ -16,7 +16,7 @@
|
||||
"file-saver": "^1.3.8",
|
||||
"i18next": "^12.0.0",
|
||||
"i18next-browser-languagedetector": "^2.2.3",
|
||||
"lodash": "^4.17.10",
|
||||
"lodash": "^4.17.11",
|
||||
"nanoid": "^1.2.3",
|
||||
"prop-types": "^15.6.1",
|
||||
"react": "^16.4.0",
|
||||
@@ -31,15 +31,14 @@
|
||||
"react-transition-group": "^2.4.0",
|
||||
"redux": "^4.0.0",
|
||||
"redux-actions": "^2.4.0",
|
||||
"redux-form": "^7.4.2",
|
||||
"redux-thunk": "^2.3.0",
|
||||
"svg-url-loader": "^2.3.2",
|
||||
"whatwg-fetch": "2.0.3"
|
||||
"svg-url-loader": "^2.3.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"autoprefixer": "^8.6.3",
|
||||
"babel-core": "6.26.0",
|
||||
"babel-eslint": "^8.2.3",
|
||||
"babel-jest": "20.0.3",
|
||||
"babel-loader": "7.1.2",
|
||||
"babel-plugin-transform-runtime": "^6.23.0",
|
||||
"babel-preset-env": "^1.7.0",
|
||||
@@ -59,7 +58,6 @@
|
||||
"extract-text-webpack-plugin": "^3.0.2",
|
||||
"file-loader": "1.1.5",
|
||||
"html-webpack-plugin": "^3.2.0",
|
||||
"jest": "20.0.4",
|
||||
"postcss-flexbugs-fixes": "3.2.0",
|
||||
"postcss-import": "^11.1.0",
|
||||
"postcss-loader": "^2.1.5",
|
||||
@@ -67,12 +65,12 @@
|
||||
"postcss-preset-env": "^5.1.0",
|
||||
"postcss-svg": "^2.4.0",
|
||||
"style-loader": "^0.21.0",
|
||||
"stylelint": "9.2.1",
|
||||
"stylelint": "^9.10.1",
|
||||
"stylelint-webpack-plugin": "0.10.4",
|
||||
"uglifyjs-webpack-plugin": "^1.2.7",
|
||||
"url-loader": "^1.0.1",
|
||||
"webpack": "3.8.1",
|
||||
"webpack-dev-server": "2.9.4",
|
||||
"webpack-dev-server": "^3.1.14",
|
||||
"webpack-merge": "^4.1.3"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<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 Home</title>
|
||||
</head>
|
||||
<body>
|
||||
<noscript>
|
||||
You need to enable JavaScript to run this app.
|
||||
</noscript>
|
||||
<div id="root"></div>
|
||||
</body>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<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 Home</title>
|
||||
</head>
|
||||
<body>
|
||||
<noscript>
|
||||
You need to enable JavaScript to run this app.
|
||||
</noscript>
|
||||
<div id="root"></div>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
16
client/public/install.html
Normal file
16
client/public/install.html
Normal file
@@ -0,0 +1,16 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<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>Setup AdGuard Home</title>
|
||||
</head>
|
||||
<body>
|
||||
<noscript>
|
||||
You need to enable JavaScript to run this app.
|
||||
</noscript>
|
||||
<div id="root"></div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,4 +1,34 @@
|
||||
{
|
||||
"url_added_successfully": "Url added successfully",
|
||||
"check_dhcp_servers": "Check for DHCP servers",
|
||||
"save_config": "Save config",
|
||||
"enabled_dhcp": "DHCP server enabled",
|
||||
"disabled_dhcp": "DHCP server disabled",
|
||||
"dhcp_title": "DHCP server (experimental!)",
|
||||
"dhcp_description": "If your router does not provide DHCP settings, you can use AdGuard's own built-in DHCP server.",
|
||||
"dhcp_enable": "Enable DHCP server",
|
||||
"dhcp_disable": "Disable DHCP server",
|
||||
"dhcp_not_found": "No active DHCP servers found on the network. It is safe to enable the built-in DHCP server.",
|
||||
"dhcp_found": "Some active DHCP servers found on the network. It is not safe to enable the built-in DHCP server.",
|
||||
"dhcp_leases": "DHCP leases",
|
||||
"dhcp_leases_not_found": "No DHCP leases found",
|
||||
"dhcp_config_saved": "Saved DHCP server config",
|
||||
"form_error_required": "Required field",
|
||||
"form_error_ip_format": "Invalid IPv4 format",
|
||||
"form_error_positive": "Must be greater than 0",
|
||||
"dhcp_form_gateway_input": "Gateway IP",
|
||||
"dhcp_form_subnet_input": "Subnet mask",
|
||||
"dhcp_form_range_title": "Range of IP addresses",
|
||||
"dhcp_form_range_start": "Range start",
|
||||
"dhcp_form_range_end": "Range end",
|
||||
"dhcp_form_lease_title": "DHCP lease time (in seconds)",
|
||||
"dhcp_form_lease_input": "Lease duration",
|
||||
"dhcp_interface_select": "Select DHCP interface",
|
||||
"dhcp_hardware_address": "Hardware address",
|
||||
"dhcp_ip_addresses": "IP addresses",
|
||||
"dhcp_table_hostname": "Hostname",
|
||||
"dhcp_table_expires": "Expires",
|
||||
"dhcp_warning": "If you want to enable the built-in DHCP server, make sure that there is no other active DHCP server. Otherwise, it can break the internet for connected devices!",
|
||||
"back": "Back",
|
||||
"dashboard": "Dashboard",
|
||||
"settings": "Settings",
|
||||
@@ -18,7 +48,7 @@
|
||||
"disabled_protection": "Disabled protection",
|
||||
"refresh_statics": "Refresh statistics",
|
||||
"dns_query": "DNS Queries",
|
||||
"blocked_by": "Blocked by",
|
||||
"blocked_by": "Blocked by Filters",
|
||||
"stats_malware_phishing": "Blocked malware\/phishing",
|
||||
"stats_adult": "Blocked adult websites",
|
||||
"stats_query_domain": "Top queried domains",
|
||||
@@ -86,9 +116,11 @@
|
||||
"example_comment": "! Here goes a comment",
|
||||
"example_comment_meaning": "just a comment",
|
||||
"example_comment_hash": "# Also a comment",
|
||||
"example_regex_meaning": "block access to the domains matching the specified regular expression",
|
||||
"example_upstream_regular": "regular DNS (over UDP)",
|
||||
"example_upstream_dot": "encrypted <a href='https:\/\/en.wikipedia.org\/wiki\/DNS_over_TLS' target='_blank'>DNS-over-TLS<\/a>",
|
||||
"example_upstream_doh": "encrypted <a href='https:\/\/en.wikipedia.org\/wiki\/DNS_over_HTTPS' target='_blank'>DNS-over-HTTPS<\/a>",
|
||||
"example_upstream_sdns": "you can use <a href='https:\/\/dnscrypt.info\/stamps\/' target='_blank'>DNS Stamps<\/a> for <a href='https:\/\/dnscrypt.info\/' target='_blank'>DNSCrypt<\/a> or <a href='https:\/\/en.wikipedia.org\/wiki\/DNS_over_HTTPS' target='_blank'>DNS-over-HTTPS<\/a> resolvers",
|
||||
"example_upstream_tcp": "regular DNS (over TCP)",
|
||||
"all_filters_up_to_date_toast": "All filters are already up-to-date",
|
||||
"updated_upstream_dns_toast": "Updated the upstream DNS servers",
|
||||
@@ -100,6 +132,7 @@
|
||||
"domain_name_table_header": "Domain name",
|
||||
"type_table_header": "Type",
|
||||
"response_table_header": "Response",
|
||||
"client_table_header": "Client",
|
||||
"empty_response_status": "Empty",
|
||||
"show_all_filter_type": "Show all",
|
||||
"show_filtered_type": "Show filtered",
|
||||
@@ -124,5 +157,94 @@
|
||||
"found_in_known_domain_db": "Found in the known domains database.",
|
||||
"category_label": "Category",
|
||||
"rule_label": "Rule",
|
||||
"filter_label": "Filter"
|
||||
"filter_label": "Filter",
|
||||
"unknown_filter": "Unknown filter {{filterId}}",
|
||||
"install_welcome_title": "Welcome to AdGuard Home!",
|
||||
"install_welcome_desc": "AdGuard Home is a network-wide ad-and-tracker blocking DNS server. Its purpose is to let you control your entire network and all your devices, and it does not require using a client-side program.",
|
||||
"install_settings_title": "Admin Web Interface",
|
||||
"install_settings_listen": "Listen interface",
|
||||
"install_settings_port": "Port",
|
||||
"install_settings_interface_link": "Your AdGuard Home admin web interface will be available on the following addresses:",
|
||||
"form_error_port": "Enter valid port value",
|
||||
"install_settings_dns": "DNS server",
|
||||
"install_settings_dns_desc": "You will need to configure your devices or router to use the DNS server on the following addresses:",
|
||||
"install_settings_all_interfaces": "All interfaces",
|
||||
"install_auth_title": "Authentication",
|
||||
"install_auth_desc": "It is highly recommended to configure password authentication to your AdGuard Home admin web interface. Even if it is accessible only in your local network, it is still important to have it protected from unrestricted access.",
|
||||
"install_auth_username": "Username",
|
||||
"install_auth_password": "Password",
|
||||
"install_auth_confirm": "Confirm password",
|
||||
"install_auth_username_enter": "Enter username",
|
||||
"install_auth_password_enter": "Enter password",
|
||||
"install_step": "Step",
|
||||
"install_devices_title": "Configure your devices",
|
||||
"install_devices_desc": "In order for AdGuard Home to start working, you need to configure your devices to use it.",
|
||||
"install_submit_title": "Congratulations!",
|
||||
"install_submit_desc": "The setup procedure is finished and you are ready to start using AdGuard Home.",
|
||||
"install_devices_router": "Router",
|
||||
"install_devices_router_desc": "This setup will automatically cover all the devices connected to your home router and you will not need to configure each of them manually.",
|
||||
"install_devices_address": "AdGuard Home DNS server is listening to the following addresses",
|
||||
"install_devices_router_list_1": "Open the preferences for your router. Usually, you can access it from your browser via a URL (like http:\/\/192.168.0.1\/ or http:\/\/192.168.1.1\/). You may be asked to enter the password. If you don't remember it, you can often reset the password by pressing a button on the router itself. Some routers require a specific application, which in that case should be already installed on your computer\/phone.",
|
||||
"install_devices_router_list_2": "Find the DHCP\/DNS settings. Look for the DNS letters next to a field which allows two or three sets of numbers, each broken into four groups of one to three digits.",
|
||||
"install_devices_router_list_3": "Enter your AdGuard Home server addresses there.",
|
||||
"install_devices_windows_list_1": "Open Control Panel through Start menu or Windows search.",
|
||||
"install_devices_windows_list_2": "Go to Network and Internet category and then to Network and Sharing Center.",
|
||||
"install_devices_windows_list_3": "On the left side of the screen find Change adapter settings and click on it.",
|
||||
"install_devices_windows_list_4": "Select your active connection, right-click on it and choose Properties.",
|
||||
"install_devices_windows_list_5": "Find Internet Protocol Version 4 (TCP\/IP) in the list, select it and then click on Properties again.",
|
||||
"install_devices_windows_list_6": "Choose Use the following DNS server addresses and enter your AdGuard Home server addresses.",
|
||||
"install_devices_macos_list_1": "Click on Apple icon and go to System Preferences.",
|
||||
"install_devices_macos_list_2": "Click on Network.",
|
||||
"install_devices_macos_list_3": "Select the first connection in your list and click Advanced.",
|
||||
"install_devices_macos_list_4": "Select the DNS tab and enter your AdGuard Home server addresses.",
|
||||
"install_devices_android_list_1": "From the Android Menu home screen, tap Settings.",
|
||||
"install_devices_android_list_2": "Tap Wi-Fi on the menu. The screen listing all of the available networks will be shown (it is impossible to set custom DNS for mobile connection).",
|
||||
"install_devices_android_list_3": "Long press the network you're connected to, and tap Modify Network.",
|
||||
"install_devices_android_list_4": "On some devices, you may need to check the box for Advanced to see further settings. To adjust your Android DNS settings, you will need to switch the IP settings from DHCP to Static.",
|
||||
"install_devices_android_list_5": "Change set DNS 1 and DNS 2 values to your AdGuard Home server addresses.",
|
||||
"install_devices_ios_list_1": "From the home screen, tap Settings.",
|
||||
"install_devices_ios_list_2": "Choose Wi-Fi in the left menu (it is impossible to configure DNS for mobile networks).",
|
||||
"install_devices_ios_list_3": "Tap on the name of the currently active network.",
|
||||
"install_devices_ios_list_4": "In the DNS field enter your AdGuard Home server addresses.",
|
||||
"get_started": "Get Started",
|
||||
"next": "Next",
|
||||
"open_dashboard": "Open Dashboard",
|
||||
"install_saved": "Saved successfully",
|
||||
"encryption_title": "Encryption",
|
||||
"encryption_desc": "Encryption (HTTPS/TLS) support for both DNS and admin web interface",
|
||||
"encryption_config_saved": "Encryption config saved",
|
||||
"encryption_server": "Server name",
|
||||
"encryption_server_enter": "Enter your domain name",
|
||||
"encryption_server_desc": "In order to use HTTPS, you need yo enter the server name that matches your SSL certificate.",
|
||||
"encryption_redirect": "Redirect to HTTPS automatically",
|
||||
"encryption_redirect_desc": "If checked, AdGuard Home will automatically redirect you from HTTP to HTTPS addresses.",
|
||||
"encryption_https": "HTTPS port",
|
||||
"encryption_https_desc": "If HTTPS port is configured, AdGuard Home admin interface will be accessible via HTTPS, and it will also provide DNS-over-HTTPS on '\/dns-query' location.",
|
||||
"encryption_dot": "DNS-over-TLS port",
|
||||
"encryption_dot_desc": "If this port is configured, AdGuard Home will run a DNS-over-TLS server on this port.",
|
||||
"encryption_certificates": "Certificates",
|
||||
"encryption_certificates_desc": "In order to use encryption, you need to provide a valid SSL certificates chain for your domain. You can get a free certificate on <0>{{link}}</0> or you can buy it from one of the trusted Certificate Authorities.",
|
||||
"encryption_certificates_input": "Copy/paste your PEM-encoded cerificates here.",
|
||||
"encryption_status": "Status",
|
||||
"encryption_expire": "Expires",
|
||||
"encryption_key": "Private key",
|
||||
"encryption_key_input": "Copy/paste your PEM-encoded private key for your cerficate here.",
|
||||
"encryption_enable": "Enable Encryption (HTTPS, DNS-over-HTTPS, and DNS-over-TLS)",
|
||||
"encryption_enable_desc": "If encryption is enabled, AdGuard Home admin interface will work over HTTPS, and the DNS server will listen for requests over DNS-over-HTTPS and DNS-over-TLS.",
|
||||
"encryption_chain_valid": "Certificate chain is valid",
|
||||
"encryption_chain_invalid": "Certificate chain is invalid",
|
||||
"encryption_key_valid": "This is a valid {{type}} private key",
|
||||
"encryption_key_invalid": "This is an invalid {{type}} private key",
|
||||
"encryption_subject": "Subject",
|
||||
"encryption_issuer": "Issuer",
|
||||
"encryption_hostnames": "Hostnames",
|
||||
"encryption_reset": "Are you sure you want to reset encryption settings?",
|
||||
"topline_expiring_certificate": "Your SSL certificate is about to expire. Update <0>Encryption settings</0>.",
|
||||
"topline_expired_certificate": "Your SSL certificate is expired. Update <0>Encryption settings</0>.",
|
||||
"form_error_port_range": "Enter port value in the range of 80-65535",
|
||||
"form_error_port_unsafe": "This is an unsafe port",
|
||||
"form_error_equal": "Shouldn't be equal",
|
||||
"form_error_password": "Password mismatched",
|
||||
"reset_settings": "Reset settings",
|
||||
"update_announcement": "AdGuard Home {{version}} is now available! <0>Click here</0> for more info."
|
||||
}
|
||||
@@ -1,4 +1,32 @@
|
||||
{
|
||||
"check_dhcp_servers": "Compruebe si hay servidores DHCP",
|
||||
"save_config": "Guardar config",
|
||||
"enabled_dhcp": "Servidor DHCP habilitado",
|
||||
"disabled_dhcp": "Servidor DHCP deshabilitado",
|
||||
"dhcp_title": "Servidor DHCP (experimental)",
|
||||
"dhcp_description": "Si su enrutador no proporciona la configuraci\u00f3n DHCP, puede utilizar el propio servidor DHCP incorporado de AdGuard.",
|
||||
"dhcp_enable": "Habilitar servidor DHCP",
|
||||
"dhcp_disable": "Deshabilitar el servidor DHCP",
|
||||
"dhcp_not_found": "No se han encontrado servidores DHCP activos en la red. Es seguro habilitar el servidor DHCP incorporado.",
|
||||
"dhcp_found": "Se encontraron servidores DHCP activos encontrados en la red. No es seguro habilitar el servidor DHCP incorporado.",
|
||||
"dhcp_leases": "concesi\u00f3nes DHCP",
|
||||
"dhcp_leases_not_found": "No se encontraron concesi\u00f3nes DHCP",
|
||||
"dhcp_config_saved": "Configuraci\u00f3n del servidor DHCP guardada",
|
||||
"form_error_required": "Campo obligatorio",
|
||||
"form_error_ip_format": "Formato IPv4 no v\u00e1lido",
|
||||
"form_error_positive": "Debe ser mayor que 0",
|
||||
"dhcp_form_gateway_input": "IP de acceso",
|
||||
"dhcp_form_subnet_input": "M\u00e1scara de subred",
|
||||
"dhcp_form_range_title": "Rango de direcciones IP",
|
||||
"dhcp_form_range_start": "Inicio de rango",
|
||||
"dhcp_form_range_end": "Final de rango",
|
||||
"dhcp_form_lease_title": "Tiempo de concesi\u00f3n DHCP (en segundos)",
|
||||
"dhcp_form_lease_input": "duraci\u00f3n de la concesi\u00f3n",
|
||||
"dhcp_interface_select": "Seleccione la interfaz DHCP",
|
||||
"dhcp_hardware_address": "Direcci\u00f3n de hardware",
|
||||
"dhcp_ip_addresses": "Direcciones IP",
|
||||
"dhcp_table_hostname": "Hostname",
|
||||
"dhcp_table_expires": "Expira",
|
||||
"back": "Atr\u00e1s",
|
||||
"dashboard": "Tablero de rendimiento",
|
||||
"settings": "Ajustes",
|
||||
@@ -9,42 +37,42 @@
|
||||
"address": "direcci\u00f3n",
|
||||
"on": "Activado",
|
||||
"off": "Desactivado",
|
||||
"copyright": "Copyright",
|
||||
"copyright": "Derechos de autor",
|
||||
"homepage": "P\u00e1gina de inicio",
|
||||
"report_an_issue": "Reportar el error",
|
||||
"report_an_issue": "Reportar un error",
|
||||
"enable_protection": "Activar la protecci\u00f3n",
|
||||
"enabled_protection": "Protecci\u00f3n activada",
|
||||
"disable_protection": "Desactivar protecci\u00f3n",
|
||||
"disabled_protection": "Protecci\u00f3n desactivada",
|
||||
"refresh_statics": "Renovas estad\u00edsticas",
|
||||
"dns_query": "DNS Queries",
|
||||
"blocked_by": "Bloqueado por",
|
||||
"refresh_statics": "Restablecer estad\u00edsticas",
|
||||
"dns_query": "Consultas DNS",
|
||||
"blocked_by": "Bloqueado por filtros",
|
||||
"stats_malware_phishing": "Malware\/phishing bloqueado",
|
||||
"stats_adult": "Contenido adulto bloqueado",
|
||||
"stats_adult": "Contenido para adultos bloqueado",
|
||||
"stats_query_domain": "Dominios m\u00e1s solicitados",
|
||||
"for_last_24_hours": "en las \u00faltimas 24 horas",
|
||||
"no_domains_found": "No dominios encontrados",
|
||||
"no_domains_found": "Dominios no encontrados",
|
||||
"requests_count": "N\u00famero de solicitudes",
|
||||
"top_blocked_domains": "Dominios m\u00e1s bloqueados",
|
||||
"top_clients": "Clientes m\u00e1s populares",
|
||||
"no_clients_found": "No hay clientes",
|
||||
"general_statistics": "Estad\u00edstica general",
|
||||
"general_statistics": "Estad\u00edsticas generales",
|
||||
"number_of_dns_query_24_hours": "Una serie de consultas DNS procesadas durante las \u00faltimas 24 horas",
|
||||
"number_of_dns_query_blocked_24_hours": "El n\u00famero de solicitudes de DNS bloqueadas por los filtros y listas de block",
|
||||
"number_of_dns_query_blocked_24_hours_by_sec": "El n\u00famero de solicitudes de DNS bloqueadas por el m\u00f3dulo de navegaci\u00f3n segura de AdGuard",
|
||||
"number_of_dns_query_blocked_24_hours_adult": "El n\u00famero de sitios para adultos bloqueados",
|
||||
"number_of_dns_query_blocked_24_hours": "El n\u00famero de solicitudes de DNS bloqueadas por los filtros de publicidad y listas de bloqueo de hosts",
|
||||
"number_of_dns_query_blocked_24_hours_by_sec": "Un n\u00famero de solicitudes de DNS bloqueadas por el m\u00f3dulo de navegaci\u00f3n segura de AdGuard",
|
||||
"number_of_dns_query_blocked_24_hours_adult": "Un n\u00famero de sitios para adultos bloqueados",
|
||||
"enforced_save_search": "B\u00fasqueda segura forzada",
|
||||
"number_of_dns_query_to_safe_search": "Una serie de solicitudes de DNS a los motores de b\u00fasqueda para los que se aplic\u00f3 la B\u00fasqueda Segura",
|
||||
"average_processing_time": "Tiempo medio de tratamiento",
|
||||
"average_processing_time_hint": "Tiempo medio en milisegundos al procesar una solicitud DNS",
|
||||
"average_processing_time": "Tiempo promedio de procesamiento",
|
||||
"average_processing_time_hint": "Tiempo promedio en milisegundos al procesar una solicitud DNS",
|
||||
"block_domain_use_filters_and_hosts": "Bloquear dominios usando filtros y archivos hosts",
|
||||
"filters_block_toggle_hint": "Puede configurar las reglas de bloqueo en los ajustes <a href='#filters'>Filtros<\/a>.",
|
||||
"use_adguard_browsing_sec": "Usar el servicio web de Seguridad de navegaci\u00f3n de AdGuard",
|
||||
"use_adguard_browsing_sec_hint": "AdGuard Home comprobar\u00e1 si el dominio est\u00e1 en la lista negra del servicio web de seguridad de navegaci\u00f3n. Utilizar\u00e1 la API de b\u00fasqueda de privacidad para realizar la comprobaci\u00f3n: s\u00f3lo se env\u00eda al servidor un prefijo corto del hash del nombre de dominio SHA256.",
|
||||
"use_adguard_browsing_sec_hint": "AdGuard Home comprobar\u00e1 si el dominio est\u00e1 en la lista negra del servicio web de seguridad de navegaci\u00f3n. Utilizar\u00e1 una API de b\u00fasqueda amigable con la privacidad para realizar la comprobaci\u00f3n: s\u00f3lo se env\u00eda al servidor un prefijo corto del hash del nombre de dominio SHA256.",
|
||||
"use_adguard_parental": "Usar Control Parental de AdGuard ",
|
||||
"use_adguard_parental_hint": "AdGuard Home comprobar\u00e1 si el dominio contiene materiales para adultos. Utiliza la misma API de privacidad que el servicio web de seguridad de navegaci\u00f3n.",
|
||||
"enforce_safe_search": "Enforzar b\u00fasqueda segura",
|
||||
"enforce_save_search_hint": "AdGuard Home puede hacer cumplir la b\u00fasqueda segura en los siguientes motores de b\u00fasqueda: Google, Youtube, Bing y Yandex.",
|
||||
"use_adguard_parental_hint": "AdGuard Home comprobar\u00e1 si el dominio contiene materiales para adultos. Utiliza la misma API amigable con la privacidad que el servicio web de seguridad de navegaci\u00f3n.",
|
||||
"enforce_safe_search": "Forzar b\u00fasqueda segura",
|
||||
"enforce_save_search_hint": "AdGuard Home puede forzar la b\u00fasqueda segura en los siguientes motores de b\u00fasqueda: Google, Youtube, Bing y Yandex.",
|
||||
"no_servers_specified": "No hay servidores especificados",
|
||||
"no_settings": "No hay ajustes",
|
||||
"general_settings": "Ajustes generales",
|
||||
@@ -67,7 +95,7 @@
|
||||
"last_time_updated_table_header": "\u00daltima actualizaci\u00f3n",
|
||||
"actions_table_header": "Acciones",
|
||||
"delete_table_action": "Eliminar",
|
||||
"filters_and_hosts": "Filtros y hosts blocklists",
|
||||
"filters_and_hosts": "Filtros y listas de bloqueo de hosts",
|
||||
"filters_and_hosts_hint": "AdGuard Home entiende reglas b\u00e1sicas de bloqueo y la sintaxis de los archivos de hosts.",
|
||||
"no_filters_added": "No hay filtros agregados",
|
||||
"add_filter_btn": "Agregar filtro",
|
||||
@@ -75,19 +103,24 @@
|
||||
"enter_name_hint": "Ingresar nombre",
|
||||
"enter_url_hint": "Ingresar URL",
|
||||
"check_updates_btn": "Revisar si hay actualizaciones",
|
||||
"new_filter_btn": "Nueva suscripci\u00f3n al filtro",
|
||||
"enter_valid_filter_url": "Ingrese el URL v\u00e1lido para suscribirse o un archivo hosts.",
|
||||
"new_filter_btn": "Nueva suscripci\u00f3n de filtro",
|
||||
"enter_valid_filter_url": "Ingrese una URL v\u00e1lida para suscribirse o un archivo de hosts.",
|
||||
"custom_filter_rules": "Personalizar reglas del filtrado",
|
||||
"custom_filter_rules_hint": "Introduzca una regla en una l\u00ednea. Puede utilizar reglas de bloqueo de anuncios o sintaxis de archivos de hosts.",
|
||||
"examples_title": "Ejemplos",
|
||||
"example_meaning_filter_block": "bloquear acceso para el dominio ejemplo.org\ny todos sus subdominios ",
|
||||
"example_meaning_filter_whitelist": "desbloquear el acceso para el dominio ejemplo.org y sus subdominios",
|
||||
"example_meaning_filter_block": "bloquear acceso al dominio ejemplo.org\ny a todos sus subdominios",
|
||||
"example_meaning_filter_whitelist": "desbloquear el acceso al dominio ejemplo.org y a sus subdominios",
|
||||
"example_meaning_host_block": "AdGuard Home regresar\u00e1 la direcci\u00f3n 127.0.0.1 para el dominio ejemplo.org (pero no para sus subdominios).",
|
||||
"example_comment": "! Aqu\u00ed va el comentario",
|
||||
"example_comment": "! Aqu\u00ed va un comentario",
|
||||
"example_comment_meaning": "solo un comentario",
|
||||
"example_comment_hash": "# Tambi\u00e9n un comentario",
|
||||
"example_upstream_regular": "DNS regular (a trav\u00e9s de UDP)",
|
||||
"example_upstream_dot": "encriptado <a href='https:\/\/en.wikipedia.org\/wiki\/DNS_over_TLS' target='_blank'>DNS-a-trav\u00e9s-de-TLS<\/a>",
|
||||
"example_upstream_doh": "encriptado <a href='https:\/\/en.wikipedia.org\/wiki\/DNS_over_HTTPS' target='_blank'>DNS-a-trav\u00e9s-de-TLS<\/a>",
|
||||
"example_upstream_sdns": "puedes usar <a href='https:\/\/dnscrypt.info\/stamps\/' target='_blank'>DNS Stamps<\/a> para <a href='https:\/\/dnscrypt.info\/' target='_blank'>DNSCrypt<\/a> o <a href='https:\/\/en.wikipedia.org\/wiki\/DNS_over_HTTPS' target='_blank'>DNS-over-HTTPS<\/a> resolutores",
|
||||
"example_upstream_tcp": "DNS regular (a trav\u00e9s de TCP)",
|
||||
"all_filters_up_to_date_toast": "Todos los filtros son actualizados",
|
||||
"updated_upstream_dns_toast": "Servidores DNS upstream son actualizados",
|
||||
"updated_upstream_dns_toast": "Servidores DNS upstream actualizados",
|
||||
"dns_test_ok_toast": "Servidores DNS especificados funcionan correctamente",
|
||||
"dns_test_not_ok_toast": "Servidor \"{{key}}\": no puede ser usado, por favor, revise si lo ha escrito correctamente",
|
||||
"unblock_btn": "Desbloquear",
|
||||
@@ -96,13 +129,14 @@
|
||||
"domain_name_table_header": "Nombre de dominio",
|
||||
"type_table_header": "Tipo",
|
||||
"response_table_header": "Respuesta",
|
||||
"client_table_header": "Cliente",
|
||||
"empty_response_status": "Vac\u00edo",
|
||||
"show_all_filter_type": "Mostrar todo",
|
||||
"show_filtered_type": "Mostrar filtrados",
|
||||
"no_logs_found": "No se han encontrado registros",
|
||||
"disabled_log_btn": "Desactivar registro",
|
||||
"download_log_file_btn": "Descargar el archivo de registro",
|
||||
"refresh_btn": "Renovar",
|
||||
"refresh_btn": "Refrescar",
|
||||
"enabled_log_btn": "Activar registro",
|
||||
"last_dns_queries": "\u00daltimas 500 solicitudes de DNS",
|
||||
"previous_btn": "Anterior",
|
||||
@@ -120,5 +154,6 @@
|
||||
"found_in_known_domain_db": "Encontrado en la base de datos de dominios conocidos.",
|
||||
"category_label": "Categor\u00eda",
|
||||
"rule_label": "Regla",
|
||||
"filter_label": "Filtro"
|
||||
"filter_label": "Filtro",
|
||||
"unknown_filter": "Filtro desconocido {{filterId}}"
|
||||
}
|
||||
@@ -1,4 +1,33 @@
|
||||
{
|
||||
"url_added_successfully": "Url ajout\u00e9e",
|
||||
"check_dhcp_servers": "Rechercher les serveurs DHCP",
|
||||
"save_config": "Sauvegarder la configuration",
|
||||
"enabled_dhcp": "Serveur DHCP activ\u00e9",
|
||||
"disabled_dhcp": "Serveur DHCP d\u00e9sactiv\u00e9",
|
||||
"dhcp_title": "Serveur DHCP (experimental !)",
|
||||
"dhcp_description": "Si votre routeur ne fonctionne pas avec les r\u00e9glages DHCP, vous pouvez utiliser le serveur DHCP par d\u00e9faut d'AdGuard.",
|
||||
"dhcp_enable": "Activer le serveur DHCP",
|
||||
"dhcp_disable": "D\u00e9sactiver le serveur DHCP",
|
||||
"dhcp_not_found": "Aucun serveur DHCP actif trouv\u00e9 sur le r\u00e9seau. Vous pouvez activer le serveur DHCP int\u00e9gr\u00e9.",
|
||||
"dhcp_found": "Il y a plusieurs serveurs DHCP actifs sur le r\u00e9seau. Ce n'est pas prudent d'activer le serveur DHCP int\u00e9gr\u00e9 en ce moment.",
|
||||
"dhcp_leases": "Locations des serveurs DHCP",
|
||||
"dhcp_leases_not_found": "Aucune location des serveurs DHCP trouv\u00e9e",
|
||||
"dhcp_config_saved": "La configuration du serveur DHCP est sauvegard\u00e9e",
|
||||
"form_error_required": "Champ requis",
|
||||
"form_error_ip_format": "Format IPv4 invalide",
|
||||
"form_error_positive": "Doit \u00eatre sup\u00e9rieur \u00e0 0\u001c",
|
||||
"dhcp_form_gateway_input": "IP de la passerelle",
|
||||
"dhcp_form_subnet_input": "Masque de sous-r\u00e9seau",
|
||||
"dhcp_form_range_title": "Rang\u00e9e des adresses IP",
|
||||
"dhcp_form_range_start": "D\u00e9but de la rang\u00e9e",
|
||||
"dhcp_form_range_end": "Fin de la rang\u00e9e",
|
||||
"dhcp_form_lease_title": "P\u00e9riode de location du serveur DHCP (secondes)",
|
||||
"dhcp_form_lease_input": "Dur\u00e9e de la location",
|
||||
"dhcp_interface_select": "S\u00e9lectionner l'interface du serveur DHCP",
|
||||
"dhcp_hardware_address": "Adresse de la machine",
|
||||
"dhcp_ip_addresses": "Adresses IP",
|
||||
"dhcp_table_hostname": "Nom de machine",
|
||||
"dhcp_table_expires": "Expire le",
|
||||
"back": "Retour",
|
||||
"dashboard": "Tableau de bord",
|
||||
"settings": "Param\u00e8tres",
|
||||
@@ -18,7 +47,7 @@
|
||||
"disabled_protection": "Protection d\u00e9sactiv\u00e9e",
|
||||
"refresh_statics": "Renouveler les statistiques",
|
||||
"dns_query": "Requ\u00eates\u001c DNS",
|
||||
"blocked_by": "Bloqu\u00e9 par",
|
||||
"blocked_by": "Bloqu\u00e9 par Filtres",
|
||||
"stats_malware_phishing": "Tentative de malware\/hamme\u00e7onnage bloqu\u00e9e",
|
||||
"stats_adult": "Sites \u00e0 contenu adulte bloqu\u00e9s",
|
||||
"stats_query_domain": "Domaines les plus recherch\u00e9s",
|
||||
@@ -49,7 +78,7 @@
|
||||
"no_settings": "Pas de param\u00e8tres",
|
||||
"general_settings": "Param\u00e8tres g\u00e9n\u00e9raux",
|
||||
"upstream_dns": "Serveurs DNS upstream",
|
||||
"upstream_dns_hint": "Si vous laisses ce champ vide, AdGuard Home va utiliser <a href='https:\/\/1.1.1.1\/' target='_blank'>Cloudflare DNS<\/a> somme upstream. Utilisez le pr\u00e9fixe tls:\/\/ pour DNS via les serveurs TLS .",
|
||||
"upstream_dns_hint": "Si vous laissez ce champ vide, AdGuard Home va utiliser <a href='https:\/\/1.1.1.1\/' target='_blank'>Cloudflare DNS<\/a> somme upstream. Utilisez le pr\u00e9fixe tls:\/\/ pour DNS via les serveurs TLS .",
|
||||
"test_upstream_btn": "Tester les upstreams",
|
||||
"apply_btn": "Appliquer",
|
||||
"disabled_filtering_toast": "Filtrage d\u00e9sactiv\u00e9",
|
||||
@@ -86,16 +115,22 @@
|
||||
"example_comment": "! Voici comment ajouter une d\u00e9scription",
|
||||
"example_comment_meaning": "commentaire",
|
||||
"example_comment_hash": "# Et comme \u00e7a aussi on peut laisser des commentaires",
|
||||
"example_upstream_regular": "DNS classique (au-dessus de UDP)",
|
||||
"example_upstream_dot": "<a href='https:\/\/en.wikipedia.org\/wiki\/DNS_over_TLS' target='_blank'>DNS-au-dessus-de-TLS<\/a> chiffr\u00e9",
|
||||
"example_upstream_doh": "<a href='https:\/\/en.wikipedia.org\/wiki\/DNS_over_HTTPS' target='_blank'>DNS-au-dessus-de-HTTPS<\/a> chiffr\u00e9",
|
||||
"example_upstream_sdns": "vous pouvez utiliser <a href='https:\/\/dnscrypt.info\/stamps\/' target='_blank'>DNS Stamps<\/a> pour <a href='https:\/\/dnscrypt.info\/' target='_blank'>DNSCrypt<\/a> ou les resolveurs <a href='https:\/\/en.wikipedia.org\/wiki\/DNS_over_HTTPS' target='_blank'>DNS-au-dessus-de-HTTPS<\/a>",
|
||||
"example_upstream_tcp": "DNS classique (au-dessus de TCP)",
|
||||
"all_filters_up_to_date_toast": "Tous les filtres sont mis \u00e0 jour",
|
||||
"updated_upstream_dns_toast": "Les serveurs DNS upstream sont mis \u00e0 jour",
|
||||
"dns_test_ok_toast": "Les serveurs DNS sp\u00e9cifi\u00e9s fonctionnent de mani\u00e8re incorrecte",
|
||||
"dns_test_not_ok_toast": "Server \"{{key}}\": could not be used, please check that you've written it correctly",
|
||||
"dns_test_not_ok_toast": "Impossible d'utiliser le serveur \"{{key}}\": veuillez v\u00e9rifier si le nom saisi est bien correct",
|
||||
"unblock_btn": "D\u00e9bloquer",
|
||||
"block_btn": "Bloquer",
|
||||
"time_table_header": "Temps",
|
||||
"domain_name_table_header": "Nom de domaine",
|
||||
"type_table_header": "Type",
|
||||
"response_table_header": "R\u00e9ponse",
|
||||
"client_table_header": "Client",
|
||||
"empty_response_status": "Vide",
|
||||
"show_all_filter_type": "Montrer tout",
|
||||
"show_filtered_type": "Montrer les sites filtr\u00e9s",
|
||||
@@ -120,5 +155,6 @@
|
||||
"found_in_known_domain_db": "Trouv\u00e9 dans la base de donn\u00e9es des domaines connus",
|
||||
"category_label": "Cat\u00e9gorie",
|
||||
"rule_label": "R\u00e8gle",
|
||||
"filter_label": "Filtre"
|
||||
"filter_label": "Filtre",
|
||||
"unknown_filter": "Filtre inconnu {{filterId}}"
|
||||
}
|
||||
@@ -1,74 +1,102 @@
|
||||
{
|
||||
"check_dhcp_servers": "DHCP\u30b5\u30fc\u30d0\u3092\u30c1\u30a7\u30c3\u30af\u3059\u308b",
|
||||
"save_config": "\u8a2d\u5b9a\u3092\u4fdd\u5b58\u3059\u308b",
|
||||
"enabled_dhcp": "DHCP\u30b5\u30fc\u30d0\u3092\u6709\u52b9\u306b\u3057\u307e\u3057\u305f",
|
||||
"disabled_dhcp": "DHCP\u30b5\u30fc\u30d0\u3092\u7121\u52b9\u306b\u3057\u307e\u3057\u305f",
|
||||
"dhcp_title": "DHCP\u30b5\u30fc\u30d0\uff08\u5b9f\u9a13\u7684\uff01\uff09",
|
||||
"dhcp_description": "\u3042\u306a\u305f\u306e\u30eb\u30fc\u30bf\u304cDHCP\u306e\u8a2d\u5b9a\u3092\u63d0\u4f9b\u3057\u3066\u3044\u306a\u3044\u306e\u306a\u3089\u3001AdGuard\u306b\u5185\u8535\u3055\u308c\u3066\u3044\u308bDHCP\u30b5\u30fc\u30d0\u3092\u5229\u7528\u3067\u304d\u307e\u3059\u3002",
|
||||
"dhcp_enable": "DHCP\u30b5\u30fc\u30d0\u3092\u6709\u52b9\u306b\u3059\u308b",
|
||||
"dhcp_disable": "DHCP\u30b5\u30fc\u30d0\u3092\u7121\u52b9\u306b\u3059\u308b",
|
||||
"dhcp_not_found": "\u30cd\u30c3\u30c8\u30ef\u30fc\u30af\u5185\u306b\u52d5\u4f5c\u3057\u3066\u3044\u308bDHCP\u30b5\u30fc\u30d0\u306f\u3042\u308a\u307e\u305b\u3093\u3002\u5185\u8535\u3055\u308c\u3066\u3044\u308bDHCP\u30b5\u30fc\u30d0\u3092\u6709\u52b9\u306b\u3057\u3066\u3082\u5b89\u5168\u3067\u3059\u3002",
|
||||
"dhcp_found": "\u30cd\u30c3\u30c8\u30ef\u30fc\u30af\u5185\u306b\u52d5\u4f5c\u3057\u3066\u3044\u308bDHCP\u30b5\u30fc\u30d0\u3092\u898b\u3064\u3051\u307e\u3057\u305f\u3002\u5185\u81d3\u3055\u308c\u3066\u3044\u308bDHCP\u30b5\u30fc\u30d0\u3092\u6709\u52b9\u306b\u3059\u308b\u306b\u306f\u5b89\u5168\u3067\u306f\u3042\u308a\u307e\u305b\u3093\u3002",
|
||||
"dhcp_leases": "DHCP\u5272\u5f53",
|
||||
"dhcp_leases_not_found": "DHCP\u5272\u5f53\u306f\u3042\u308a\u307e\u305b\u3093",
|
||||
"dhcp_config_saved": "DHCP\u30b5\u30fc\u30d0\u306e\u8a2d\u5b9a\u3092\u4fdd\u5b58\u3057\u307e\u3057\u305f",
|
||||
"form_error_required": "\u5fc5\u9808\u9805\u76ee\u3067\u3059",
|
||||
"form_error_ip_format": "IPv4\u30d5\u30a9\u30fc\u30de\u30c3\u30c8\u3067\u306f\u3042\u308a\u307e\u305b\u3093",
|
||||
"form_error_positive": "0\u3088\u308a\u5927\u304d\u3044\u5fc5\u8981\u304c\u3042\u308a\u307e\u3059",
|
||||
"dhcp_form_gateway_input": "\u30b2\u30fc\u30c8\u30a6\u30a7\u30a4IP",
|
||||
"dhcp_form_subnet_input": "\u30b5\u30d6\u30cd\u30c3\u30c8\u30de\u30b9\u30af",
|
||||
"dhcp_form_range_title": "IP\u30a2\u30c9\u30ec\u30b9\u306e\u7bc4\u56f2",
|
||||
"dhcp_form_range_start": "\u7bc4\u56f2\u306e\u958b\u59cb",
|
||||
"dhcp_form_range_end": "\u7bc4\u56f2\u306e\u7d42\u4e86",
|
||||
"dhcp_form_lease_title": "DHCP\u5272\u5f53\u6642\u9593\uff08\u79d2\u5358\u4f4d\uff09",
|
||||
"dhcp_form_lease_input": "\u5272\u5f53\u671f\u9593",
|
||||
"dhcp_interface_select": "DHCP\u30a4\u30f3\u30bf\u30d5\u30a7\u30fc\u30b9\u306e\u9078\u629e",
|
||||
"dhcp_hardware_address": "MAC\u30a2\u30c9\u30ec\u30b9",
|
||||
"dhcp_ip_addresses": "IP\u30a2\u30c9\u30ec\u30b9",
|
||||
"dhcp_table_hostname": "\u30db\u30b9\u30c8\u540d",
|
||||
"dhcp_table_expires": "\u6709\u52b9\u671f\u9650",
|
||||
"back": "\u623b\u308b",
|
||||
"dashboard": "\u30c0\u30c3\u30b7\u30e5\u30dc\u30fc\u30c9",
|
||||
"settings": "\u8a2d\u5b9a",
|
||||
"filters": "\u30d5\u30a3\u30eb\u30bf",
|
||||
"query_log": "\u30af\u30a8\u30ea\u30fb\u30ed\u30b0",
|
||||
"faq": "FAQ",
|
||||
"faq": "\u3088\u304f\u3042\u308b\u8cea\u554f",
|
||||
"version": "\u30d0\u30fc\u30b8\u30e7\u30f3",
|
||||
"address": "\u30a2\u30c9\u30ec\u30b9",
|
||||
"on": "ON",
|
||||
"off": "OFF",
|
||||
"on": "\u30aa\u30f3",
|
||||
"off": "\u30aa\u30d5",
|
||||
"copyright": "\u8457\u4f5c\u6a29",
|
||||
"homepage": "\u30db\u30fc\u30e0\u30da\u30fc\u30b8",
|
||||
"report_an_issue": "\u554f\u984c\u3092\u5831\u544a\u3059\u308b",
|
||||
"enable_protection": "\u4fdd\u8b77\u3092\u6709\u52b9\u306b\u3059\u308b",
|
||||
"enabled_protection": "\u4fdd\u8b77\u6709\u52b9",
|
||||
"enabled_protection": "\u4fdd\u8b77\u3092\u6709\u52b9\u306b\u3057\u307e\u3057\u305f",
|
||||
"disable_protection": "\u4fdd\u8b77\u3092\u7121\u52b9\u306b\u3059\u308b",
|
||||
"disabled_protection": "\u4fdd\u8b77\u7121\u52b9",
|
||||
"refresh_statics": "\u7d71\u8a08\u30c7\u30fc\u30bf\u3092\u66f4\u65b0\u3059\u308b",
|
||||
"disabled_protection": "\u4fdd\u8b77\u3092\u7121\u52b9\u306b\u3057\u307e\u3057\u305f",
|
||||
"refresh_statics": "\u7d71\u8a08\u30c7\u30fc\u30bf\u3092\u6700\u65b0\u306b\u3059\u308b",
|
||||
"dns_query": "DNS\u30af\u30a8\u30ea",
|
||||
"blocked_by": "\u306b\u3088\u3063\u3066\u30d6\u30ed\u30c3\u30af\u6e08\u307f",
|
||||
"stats_malware_phishing": "\u30d6\u30ed\u30c3\u30af\u6e08\u307f\u30de\u30eb\u30a6\u30a7\u30a2\uff0f\u30d5\u30a3\u30c3\u30b7\u30f3\u30b0",
|
||||
"stats_adult": "\u30d6\u30ed\u30c3\u30af\u6e08\u307f\u30a2\u30c0\u30eb\u30c8\u30a6\u30a7\u30d6\u30b5\u30a4\u30c8",
|
||||
"stats_query_domain": "\u983b\u7e41\u306b\u554f\u3044\u5408\u308f\u305b\u3055\u308c\u308b\u30c9\u30e1\u30a4\u30f3",
|
||||
"for_last_24_hours": "\u904e\u53bb24\u6642\u9593\u5185",
|
||||
"no_domains_found": "\u30c9\u30e1\u30a4\u30f3\u898b\u3064\u304b\u308a\u307e\u305b\u3093\u3067\u3057\u305f",
|
||||
"blocked_by": "\u30d5\u30a3\u30eb\u30bf\u306b\u30d6\u30ed\u30c3\u30af\u3055\u308c\u305fDNS\u30af\u30a8\u30ea",
|
||||
"stats_malware_phishing": "\u30d6\u30ed\u30c3\u30af\u3055\u308c\u305f\u30de\u30eb\u30a6\u30a7\u30a2\uff0f\u30d5\u30a3\u30c3\u30b7\u30f3\u30b0",
|
||||
"stats_adult": "\u30d6\u30ed\u30c3\u30af\u3055\u308c\u305f\u30a2\u30c0\u30eb\u30c8\u30a6\u30a7\u30d6\u30b5\u30a4\u30c8",
|
||||
"stats_query_domain": "\u6700\u3082\u554f\u5408\u305b\u3055\u308c\u305f\u30c9\u30e1\u30a4\u30f3",
|
||||
"for_last_24_hours": "\u904e\u53bb24\u6642\u9593\u4ee5\u5185",
|
||||
"no_domains_found": "\u30c9\u30e1\u30a4\u30f3\u60c5\u5831\u306f\u3042\u308a\u307e\u305b\u3093",
|
||||
"requests_count": "\u30ea\u30af\u30a8\u30b9\u30c8\u6570",
|
||||
"top_blocked_domains": "\u6700\u3082\u30d6\u30ed\u30c3\u30af\u3055\u308c\u308b\u30c9\u30e1\u30a4\u30f3",
|
||||
"top_blocked_domains": "\u6700\u3082\u30d6\u30ed\u30c3\u30af\u3055\u308c\u305f\u30c9\u30e1\u30a4\u30f3",
|
||||
"top_clients": "\u30c8\u30c3\u30d7\u30af\u30e9\u30a4\u30a2\u30f3\u30c8",
|
||||
"no_clients_found": "\u30af\u30e9\u30a4\u30a2\u30f3\u30c8\u898b\u3064\u304b\u308a\u307e\u305b\u3093\u3067\u3057\u305f",
|
||||
"general_statistics": "\u4e00\u822c\u7d71\u8a08",
|
||||
"no_clients_found": "\u30af\u30e9\u30a4\u30a2\u30f3\u30c8\u60c5\u5831\u306f\u3042\u308a\u307e\u305b\u3093",
|
||||
"general_statistics": "\u5168\u822c\u7684\u306a\u7d71\u8a08",
|
||||
"number_of_dns_query_24_hours": "\u904e\u53bb24\u6642\u9593\u306b\u51e6\u7406\u3055\u308c\u305fDNS\u30af\u30a8\u30ea\u306e\u6570",
|
||||
"number_of_dns_query_blocked_24_hours": "\u5e83\u544a\u30d6\u30ed\u30c3\u30af\u30d5\u30a3\u30eb\u30bf\u3068\u30db\u30b9\u30c8\u30d6\u30ed\u30c3\u30af\u30ea\u30b9\u30c8\u306b\u3088\u3063\u3066\u30d6\u30ed\u30c3\u30af\u3055\u308c\u305fDNS\u30ea\u30af\u30a8\u30b9\u30c8\u306e\u6570",
|
||||
"number_of_dns_query_blocked_24_hours": "\u5e83\u544a\u30d6\u30ed\u30c3\u30af\u30d5\u30a3\u30eb\u30bf\u3068hosts\u30d6\u30ed\u30c3\u30af\u30ea\u30b9\u30c8\u306b\u3088\u3063\u3066\u30d6\u30ed\u30c3\u30af\u3055\u308c\u305fDNS\u30ea\u30af\u30a8\u30b9\u30c8\u306e\u6570",
|
||||
"number_of_dns_query_blocked_24_hours_by_sec": "AdGuard\u30d6\u30e9\u30a6\u30b8\u30f3\u30b0\u30bb\u30ad\u30e5\u30ea\u30c6\u30a3\u30e2\u30b8\u30e5\u30fc\u30eb\u306b\u3088\u3063\u3066\u30d6\u30ed\u30c3\u30af\u3055\u308c\u305fDNS\u30ea\u30af\u30a8\u30b9\u30c8\u306e\u6570",
|
||||
"number_of_dns_query_blocked_24_hours_adult": "\u30d6\u30ed\u30c3\u30af\u6e08\u307f\u6210\u4eba\u5411\u3051\u30a6\u30a7\u30d6\u30b5\u30a4\u30c8\u306e\u6570",
|
||||
"enforced_save_search": "\u30bb\u30fc\u30d5\u30b5\u30fc\u30c1\u9069\u7528\u6e08\u307f",
|
||||
"number_of_dns_query_to_safe_search": "\u30bb\u30fc\u30d5\u30b5\u30fc\u30c1\u304c\u9069\u7528\u3055\u308c\u305f\u691c\u7d22\u30a8\u30f3\u30b8\u30f3\u306b\u5bfe\u3059\u308bDNS\u30ea\u30af\u30a8\u30b9\u30c8\u306e\u6570",
|
||||
"number_of_dns_query_blocked_24_hours_adult": "\u30d6\u30ed\u30c3\u30af\u3055\u308c\u305f\u30a2\u30c0\u30eb\u30c8\u30a6\u30a7\u30d6\u30b5\u30a4\u30c8\u306e\u6570",
|
||||
"enforced_save_search": "\u5f37\u5236\u3055\u308c\u305f\u30bb\u30fc\u30d5\u30b5\u30fc\u30c1",
|
||||
"number_of_dns_query_to_safe_search": "\u30bb\u30fc\u30d5\u30b5\u30fc\u30c1\u304c\u5f37\u5236\u3055\u308c\u305f\u691c\u7d22\u30a8\u30f3\u30b8\u30f3\u306b\u5bfe\u3059\u308bDNS\u30ea\u30af\u30a8\u30b9\u30c8\u306e\u6570",
|
||||
"average_processing_time": "\u5e73\u5747\u51e6\u7406\u6642\u9593",
|
||||
"average_processing_time_hint": "DNS\u30ea\u30af\u30a8\u30b9\u30c8\u306e\u51e6\u7406\u306b\u304b\u304b\u308b\u5e73\u5747\u6642\u9593\uff08\u30df\u30ea\u79d2\u5358\u4f4d\uff09",
|
||||
"block_domain_use_filters_and_hosts": "\u30d5\u30a3\u30eb\u30bf\u3068\u30db\u30b9\u30c8\u30d5\u30a1\u30a4\u30eb\u3092\u4f7f\u7528\u3057\u3066\u30c9\u30e1\u30a4\u30f3\u3092\u30d6\u30ed\u30c3\u30af\u3059\u308b",
|
||||
"filters_block_toggle_hint": "<a href='#filters'>\u30d5\u30a3\u30eb\u30bf<\/a>\u306e\u8a2d\u5b9a\u3067\u30d6\u30ed\u30c3\u30ad\u30f3\u30b0\u30eb\u30fc\u30eb\u3092\u8a2d\u5b9a\u3059\u308b\u3053\u3068\u304c\u3067\u304d\u307e\u3059\u3002",
|
||||
"use_adguard_browsing_sec": "AdGuard\u30d6\u30e9\u30a6\u30b8\u30f3\u30b0\u30bb\u30ad\u30e5\u30ea\u30c6\u30a3Web\u30b5\u30fc\u30d3\u30b9\u3092\u4f7f\u7528\u3059\u308b",
|
||||
"use_adguard_browsing_sec_hint": "AdGuard Home\u306f\u3001\u30d6\u30e9\u30a6\u30b8\u30f3\u30b0\u30bb\u30ad\u30e5\u30ea\u30c6\u30a3Web\u30b5\u30fc\u30d3\u30b9\u306b\u3088\u3063\u3066\u30c9\u30e1\u30a4\u30f3\u304c\u30d6\u30e9\u30c3\u30af\u30ea\u30b9\u30c8\u306b\u767b\u9332\u3055\u308c\u3066\u3044\u308b\u304b\u3069\u3046\u304b\u3092\u78ba\u8a8d\u3057\u307e\u3059\u3002 \u3053\u308c\u306f\u30d7\u30e9\u30a4\u30d0\u30b7\u30fc\u3092\u8003\u616e\u3057\u305fAPI\u3092\u4f7f\u7528\u3057\u3066\u30c1\u30a7\u30c3\u30af\u3092\u5b9f\u884c\u3057\u307e\u3059\u3002\u30c9\u30e1\u30a4\u30f3\u540dSHA256\u30cf\u30c3\u30b7\u30e5\u306e\u77ed\u3044\u30d7\u30ec\u30d5\u30a3\u30c3\u30af\u30b9\u306e\u307f\u304c\u30b5\u30fc\u30d0\u30fc\u306b\u9001\u4fe1\u3055\u308c\u307e\u3059\u3002",
|
||||
"use_adguard_parental": "AdGuard\u30da\u30a2\u30ec\u30f3\u30bf\u30eb\u30b3\u30f3\u30c8\u30ed\u30fc\u30ebWeb\u30b5\u30fc\u30d3\u30b9\u3092\u4f7f\u7528\u3059\u308b",
|
||||
"use_adguard_parental_hint": "AdGuard Home\u306f\u3001\u30c9\u30e1\u30a4\u30f3\u306b\u30a2\u30c0\u30eb\u30c8\u30b3\u30f3\u30c6\u30f3\u30c4\u304c\u542b\u307e\u308c\u3066\u3044\u308b\u304b\u3069\u3046\u304b\u3092\u78ba\u8a8d\u3057\u307e\u3059\u3002 \u30d6\u30e9\u30a6\u30b8\u30f3\u30b0\u30bb\u30ad\u30e5\u30ea\u30c6\u30a3Web\u30b5\u30fc\u30d3\u30b9\u3068\u540c\u3058\u30d7\u30e9\u30a4\u30d0\u30b7\u30fc\u306b\u512a\u3057\u3044API\u3092\u4f7f\u7528\u3057\u307e\u3059\u3002",
|
||||
"enforce_safe_search": "\u30bb\u30fc\u30d5\u30b5\u30fc\u30c1\u3092\u9069\u7528\u3059\u308b",
|
||||
"enforce_save_search_hint": "AdGuard Home\u306f\u3001Google\u3001Youtube\u3001Bing\u3001Yandex\u306e\u691c\u7d22\u30a8\u30f3\u30b8\u30f3\u3067\u30bb\u30fc\u30d5\u30b5\u30fc\u30c1\u3092\u9069\u7528\u3067\u304d\u307e\u3059\u3002",
|
||||
"no_servers_specified": "\u30b5\u30fc\u30d0\u30fc\u304c\u6307\u5b9a\u3055\u308c\u3066\u3044\u307e\u305b\u3093",
|
||||
"block_domain_use_filters_and_hosts": "\u30d5\u30a3\u30eb\u30bf\u3068hosts\u30d5\u30a1\u30a4\u30eb\u3092\u4f7f\u7528\u3057\u3066\u30c9\u30e1\u30a4\u30f3\u3092\u30d6\u30ed\u30c3\u30af\u3059\u308b",
|
||||
"filters_block_toggle_hint": "<a href='#filters'>\u30d5\u30a3\u30eb\u30bf<\/a>\u306e\u8a2d\u5b9a\u3067\u30d6\u30ed\u30c3\u30af\u3059\u308b\u30eb\u30fc\u30eb\u3092\u8a2d\u5b9a\u3059\u308b\u3053\u3068\u304c\u3067\u304d\u307e\u3059\u3002",
|
||||
"use_adguard_browsing_sec": "AdGuard\u30d6\u30e9\u30a6\u30b8\u30f3\u30b0\u30bb\u30ad\u30e5\u30ea\u30c6\u30a3\u30fb\u30a6\u30a7\u30d6\u30b5\u30fc\u30d3\u30b9\u3092\u4f7f\u7528\u3059\u308b",
|
||||
"use_adguard_browsing_sec_hint": "AdGuard Home\u306f\u3001\u30d6\u30e9\u30a6\u30b8\u30f3\u30b0\u30bb\u30ad\u30e5\u30ea\u30c6\u30a3\u30fb\u30a6\u30a7\u30d6\u30b5\u30fc\u30d3\u30b9\u306b\u3088\u3063\u3066\u30c9\u30e1\u30a4\u30f3\u304c\u30d6\u30e9\u30c3\u30af\u30ea\u30b9\u30c8\u306b\u767b\u9332\u3055\u308c\u3066\u3044\u308b\u304b\u3069\u3046\u304b\u3092\u78ba\u8a8d\u3057\u307e\u3059\u3002 \u3053\u308c\u306f\u30d7\u30e9\u30a4\u30d0\u30b7\u30fc\u3092\u8003\u616e\u3057\u305fAPI\u3092\u4f7f\u7528\u3057\u3066\u30c1\u30a7\u30c3\u30af\u3092\u5b9f\u884c\u3057\u307e\u3059\u3002\u30c9\u30e1\u30a4\u30f3\u540dSHA256\u30cf\u30c3\u30b7\u30e5\u306e\u77ed\u3044\u30d7\u30ec\u30d5\u30a3\u30c3\u30af\u30b9\u306e\u307f\u304c\u30b5\u30fc\u30d0\u306b\u9001\u4fe1\u3055\u308c\u307e\u3059\u3002",
|
||||
"use_adguard_parental": "AdGuard\u30da\u30a2\u30ec\u30f3\u30bf\u30eb\u30b3\u30f3\u30c8\u30ed\u30fc\u30eb\u30fb\u30a6\u30a7\u30d6\u30b5\u30fc\u30d3\u30b9\u3092\u4f7f\u7528\u3059\u308b",
|
||||
"use_adguard_parental_hint": "AdGuard Home\u306f\u3001\u30c9\u30e1\u30a4\u30f3\u306b\u30a2\u30c0\u30eb\u30c8\u30b3\u30f3\u30c6\u30f3\u30c4\u304c\u542b\u307e\u308c\u3066\u3044\u308b\u304b\u3069\u3046\u304b\u3092\u78ba\u8a8d\u3057\u307e\u3059\u3002 \u30d6\u30e9\u30a6\u30b8\u30f3\u30b0\u30bb\u30ad\u30e5\u30ea\u30c6\u30a3\u30fb\u30a6\u30a7\u30d6\u30b5\u30fc\u30d3\u30b9\u3068\u540c\u3058\u30d7\u30e9\u30a4\u30d0\u30b7\u30fc\u306b\u512a\u3057\u3044API\u3092\u4f7f\u7528\u3057\u307e\u3059\u3002",
|
||||
"enforce_safe_search": "\u30bb\u30fc\u30d5\u30b5\u30fc\u30c1\u3092\u5f37\u5236\u3059\u308b",
|
||||
"enforce_save_search_hint": "AdGuard Home\u306f\u3001Google\u3001Youtube\u3001Bing\u3001Yandex\u306e\u691c\u7d22\u30a8\u30f3\u30b8\u30f3\u3067\u30bb\u30fc\u30d5\u30b5\u30fc\u30c1\u3092\u5f37\u5236\u3067\u304d\u307e\u3059\u3002",
|
||||
"no_servers_specified": "\u30b5\u30fc\u30d0\u304c\u6307\u5b9a\u3055\u308c\u3066\u3044\u307e\u305b\u3093",
|
||||
"no_settings": "\u8a2d\u5b9a\u306a\u3057",
|
||||
"general_settings": "\u4e00\u822c\u8a2d\u5b9a",
|
||||
"upstream_dns": "\u30a2\u30c3\u30d7\u30b9\u30c8\u30ea\u30fc\u30e0DNS\u30b5\u30fc\u30d0\u30fc",
|
||||
"upstream_dns_hint": "\u3053\u306e\u30d5\u30a3\u30fc\u30eb\u30c9\u3092\u7a7a\u306e\u307e\u307e\u306b\u3059\u308b\u3068\u3001AdGuard Home\u306f<a href='https:\/\/1.1.1.1\/' target='_blank'>Cloudflare DNS<\/a>\u3092\u30a2\u30c3\u30d7\u30b9\u30c8\u30ea\u30fc\u30e0\u3068\u3057\u3066\u4f7f\u7528\u3057\u307e\u3059\u3002 TLS\u30b5\u30fc\u30d0\u30fc\u7d4c\u7531\u306eDNS\u306b\u306f\u3001\uff62tls:\/\/\u300d\u30d7\u30ec\u30d5\u30a3\u30c3\u30af\u30b9\u3092\u4f7f\u7528\u3057\u3066\u304f\u3060\u3055\u3044\u3002",
|
||||
"test_upstream_btn": "\u30a2\u30c3\u30d7\u30b9\u30c8\u30ea\u30fc\u30e0\u30b5\u30fc\u30d0\u30fc\u30c6\u30b9\u30c8",
|
||||
"apply_btn": "\u9069\u7528",
|
||||
"disabled_filtering_toast": "\u30d5\u30a3\u30eb\u30bf\u30ea\u30f3\u30b0\u7121\u52b9",
|
||||
"enabled_filtering_toast": "\u30d5\u30a3\u30eb\u30bf\u30ea\u30f3\u30b0\u6709\u52b9",
|
||||
"disabled_safe_browsing_toast": "\u30bb\u30fc\u30d5\u30d6\u30e9\u30a6\u30b8\u30f3\u30b0\u7121\u52b9",
|
||||
"enabled_safe_browsing_toast": "\u30bb\u30fc\u30d5\u30d6\u30e9\u30a6\u30b8\u30f3\u30b0\u6709\u52b9",
|
||||
"disabled_parental_toast": "\u30da\u30a2\u30ec\u30f3\u30bf\u30eb\u30b3\u30f3\u30c8\u30ed\u30fc\u30eb\u7121\u52b9",
|
||||
"enabled_parental_toast": "\u30da\u30a2\u30ec\u30f3\u30bf\u30eb\u30b3\u30f3\u30c8\u30ed\u30fc\u30eb\u6709\u52b9",
|
||||
"disabled_safe_search_toast": "\u30bb\u30fc\u30d5\u30b5\u30fc\u30c1\u7121\u52b9",
|
||||
"enabled_save_search_toast": "\u30bb\u30fc\u30d5\u30b5\u30fc\u30c1\u6709\u52b9",
|
||||
"upstream_dns": "\u4e0a\u6d41DNS\u30b5\u30fc\u30d0",
|
||||
"upstream_dns_hint": "\u3053\u306e\u30d5\u30a3\u30fc\u30eb\u30c9\u3092\u672a\u5165\u529b\u306e\u307e\u307e\u306b\u3059\u308b\u3068\u3001AdGuard Home\u306f\u4e0a\u6d41\u3068\u3057\u3066<a href='https:\/\/1.1.1.1\/' target='_blank'>Cloudflare DNS<\/a>\u3092\u4f7f\u7528\u3057\u307e\u3059\u3002DNS over TLS\u30b5\u30fc\u30d0\u306b\u306f\u3001\uff62tls:\/\/\u300d\u30d7\u30ec\u30d5\u30a3\u30c3\u30af\u30b9\u3092\u4f7f\u7528\u3057\u3066\u304f\u3060\u3055\u3044\u3002",
|
||||
"test_upstream_btn": "\u4e0a\u6d41\u30b5\u30fc\u30d0\u3092\u30c6\u30b9\u30c8\u3059\u308b",
|
||||
"apply_btn": "\u9069\u7528\u3059\u308b",
|
||||
"disabled_filtering_toast": "\u30d5\u30a3\u30eb\u30bf\u30ea\u30f3\u30b0\u3092\u7121\u52b9\u306b\u3057\u307e\u3057\u305f",
|
||||
"enabled_filtering_toast": "\u30d5\u30a3\u30eb\u30bf\u30ea\u30f3\u30b0\u3092\u6709\u52b9\u306b\u3057\u307e\u3057\u305f",
|
||||
"disabled_safe_browsing_toast": "\u30bb\u30fc\u30d5\u30d6\u30e9\u30a6\u30b8\u30f3\u30b0\u3092\u7121\u52b9\u306b\u3057\u307e\u3057\u305f",
|
||||
"enabled_safe_browsing_toast": "\u30bb\u30fc\u30d5\u30d6\u30e9\u30a6\u30b8\u30f3\u30b0\u3092\u6709\u52b9\u306b\u3057\u307e\u3057\u305f",
|
||||
"disabled_parental_toast": "\u30da\u30a2\u30ec\u30f3\u30bf\u30eb\u30b3\u30f3\u30c8\u30ed\u30fc\u30eb\u3092\u7121\u52b9\u306b\u3057\u307e\u3057\u305f",
|
||||
"enabled_parental_toast": "\u30da\u30a2\u30ec\u30f3\u30bf\u30eb\u30b3\u30f3\u30c8\u30ed\u30fc\u30eb\u3092\u6709\u52b9\u306b\u3057\u307e\u3057\u305f",
|
||||
"disabled_safe_search_toast": "\u30bb\u30fc\u30d5\u30b5\u30fc\u30c1\u3092\u7121\u52b9\u306b\u3057\u307e\u3057\u305f",
|
||||
"enabled_save_search_toast": "\u30bb\u30fc\u30d5\u30b5\u30fc\u30c1\u3092\u6709\u52b9\u306b\u3057\u307e\u3057\u305f",
|
||||
"enabled_table_header": "\u6709\u52b9",
|
||||
"name_table_header": "\u540d\u79f0",
|
||||
"filter_url_table_header": "\u30d5\u30a3\u30eb\u30bf\u306eURL",
|
||||
"rules_count_table_header": "\u30eb\u30fc\u30eb\u6570",
|
||||
"last_time_updated_table_header": "\u6700\u7d42\u66f4\u65b0\u65e5",
|
||||
"last_time_updated_table_header": "\u6700\u7d42\u66f4\u65b0\u6642\u523b",
|
||||
"actions_table_header": "\u64cd\u4f5c",
|
||||
"delete_table_action": "\u524a\u9664",
|
||||
"filters_and_hosts": "\u30d5\u30a3\u30eb\u30bf\u3068\u30db\u30b9\u30c8\u30d6\u30ed\u30c3\u30af\u30ea\u30b9\u30c8",
|
||||
"filters_and_hosts_hint": "AdGuard Home\u306f\u3001\u57fa\u672c\u7684\u306a\u5e83\u544a\u30d6\u30ed\u30c3\u30af\u30eb\u30fc\u30eb\u3068\u30db\u30b9\u30c8\u30d5\u30a1\u30a4\u30eb\u306e\u69cb\u6587\u3092\u7406\u89e3\u3057\u3066\u3044\u307e\u3059\u3002",
|
||||
"delete_table_action": "\u524a\u9664\u3059\u308b",
|
||||
"filters_and_hosts": "\u30d5\u30a3\u30eb\u30bf\u3068hosts\u30d6\u30ed\u30c3\u30af\u30ea\u30b9\u30c8",
|
||||
"filters_and_hosts_hint": "AdGuard Home\u306f\u3001\u57fa\u672c\u7684\u306a\u5e83\u544a\u30d6\u30ed\u30c3\u30af\u30eb\u30fc\u30eb\u3068hosts\u30d5\u30a1\u30a4\u30eb\u306e\u69cb\u6587\u3092\u7406\u89e3\u3057\u307e\u3059\u3002",
|
||||
"no_filters_added": "\u30d5\u30a3\u30eb\u30bf\u306f\u8ffd\u52a0\u3055\u308c\u307e\u305b\u3093\u3067\u3057\u305f",
|
||||
"add_filter_btn": "\u30d5\u30a3\u30eb\u30bf\u3092\u8ffd\u52a0\u3059\u308b",
|
||||
"cancel_btn": "\u30ad\u30e3\u30f3\u30bb\u30eb",
|
||||
@@ -76,49 +104,56 @@
|
||||
"enter_url_hint": "URL\u3092\u5165\u529b",
|
||||
"check_updates_btn": "\u30a2\u30c3\u30d7\u30c7\u30fc\u30c8\u3092\u78ba\u8a8d\u3059\u308b",
|
||||
"new_filter_btn": "\u65b0\u3057\u3044\u30d5\u30a3\u30eb\u30bf\u30fb\u30b5\u30d6\u30b9\u30af\u30ea\u30d7\u30b7\u30e7\u30f3",
|
||||
"enter_valid_filter_url": "\u30d5\u30a3\u30eb\u30bf\u30fb\u30b5\u30d6\u30b9\u30af\u30ea\u30d7\u30b7\u30e7\u30f3\u304a\u3088\u3073\u30db\u30b9\u30c8\u30d5\u30a1\u30a4\u30eb\u306e\u6709\u52b9\u306aURL\u3092\u5165\u529b\u3057\u3066\u304f\u3060\u3055\u3044\u3002",
|
||||
"enter_valid_filter_url": "\u30d5\u30a3\u30eb\u30bf\u30fb\u30b5\u30d6\u30b9\u30af\u30ea\u30d7\u30b7\u30e7\u30f3\u3082\u3057\u304f\u306fhosts\u30d5\u30a1\u30a4\u30eb\u306e\u6709\u52b9\u306aURL\u3092\u5165\u529b\u3057\u3066\u304f\u3060\u3055\u3044\u3002",
|
||||
"custom_filter_rules": "\u30ab\u30b9\u30bf\u30e0\u30fb\u30d5\u30a3\u30eb\u30bf\u30ea\u30f3\u30b0\u30eb\u30fc\u30eb",
|
||||
"custom_filter_rules_hint": "1\u3064\u306e\u884c\u306b1\u3064\u306e\u30eb\u30fc\u30eb\u3092\u5165\u529b\u3057\u3066\u304f\u3060\u3055\u3044\u3002 \u5e83\u544a\u30d6\u30ed\u30c3\u30af\u30eb\u30fc\u30eb\u3084\u30db\u30b9\u30c8\u30d5\u30a1\u30a4\u30eb\u69cb\u6587\u3092\u4f7f\u7528\u3067\u304d\u307e\u3059\u3002",
|
||||
"custom_filter_rules_hint": "1\u3064\u306e\u884c\u306b1\u3064\u306e\u30eb\u30fc\u30eb\u3092\u5165\u529b\u3057\u3066\u304f\u3060\u3055\u3044\u3002 \u5e83\u544a\u30d6\u30ed\u30c3\u30af\u30eb\u30fc\u30eb\u3084hosts\u30d5\u30a1\u30a4\u30eb\u69cb\u6587\u3092\u4f7f\u7528\u3067\u304d\u307e\u3059\u3002",
|
||||
"examples_title": "\u4f8b",
|
||||
"example_meaning_filter_block": "example.org\u30c9\u30e1\u30a4\u30f3\u3068\u305d\u306e\u3059\u3079\u3066\u306e\u30b5\u30d6\u30c9\u30e1\u30a4\u30f3\u3078\u306e\u30a2\u30af\u30bb\u30b9\u3092\u30d6\u30ed\u30c3\u30af\u3059\u308b",
|
||||
"example_meaning_filter_whitelist": "example.org\u30c9\u30e1\u30a4\u30f3\u3068\u305d\u306e\u3059\u3079\u3066\u306e\u30b5\u30d6\u30c9\u30e1\u30a4\u30f3\u3078\u306e\u30a2\u30af\u30bb\u30b9\u306e\u30d6\u30ed\u30c3\u30af\u3092\u89e3\u9664\u3059\u308b",
|
||||
"example_meaning_host_block": "AdGuard Home\u306f\u3001example.org\u30c9\u30e1\u30a4\u30f3\uff08\u30b5\u30d6\u30c9\u30e1\u30a4\u30f3\u306f\u9664\u304f\uff09\u306b\u5bfe\u3057\u3066127.0.0.1\u306e\u30a2\u30c9\u30ec\u30b9\u3092\u8fd4\u3059\u3088\u3046\u306b\u306a\u308a\u307e\u3057\u305f\u3002",
|
||||
"example_meaning_host_block": "AdGuard Home\u306f\u3001example.org\u30c9\u30e1\u30a4\u30f3\uff08\u30b5\u30d6\u30c9\u30e1\u30a4\u30f3\u3092\u9664\u304f\uff09\u306b\u5bfe\u3057\u3066127.0.0.1\u306e\u30a2\u30c9\u30ec\u30b9\u3092\u8fd4\u3059\u3088\u3046\u306b\u306a\u308a\u307e\u3059\u3002",
|
||||
"example_comment": "! \u3053\u3053\u306b\u306f\u30b3\u30e1\u30f3\u30c8\u304c\u5165\u308a\u307e\u3059",
|
||||
"example_comment_meaning": "\u305f\u3060\u306e\u30b3\u30e1\u30f3\u30c8",
|
||||
"example_comment_hash": "# \u3053\u3053\u3082\u30b3\u30e1\u30f3\u30c8",
|
||||
"all_filters_up_to_date_toast": "\u30d5\u30a3\u30eb\u30bf\u306f\u65e2\u306b\u3059\u3079\u3066\u6700\u65b0\u3067\u3059",
|
||||
"updated_upstream_dns_toast": "\u30a2\u30c3\u30d7\u30b9\u30c8\u30ea\u30fc\u30e0DNS\u30b5\u30fc\u30d0\u30fc\u3092\u66f4\u65b0\u3057\u307e\u3057\u305f",
|
||||
"dns_test_ok_toast": "\u6307\u5b9a\u3055\u308c\u305fDNS\u30b5\u30fc\u30d0\u30fc\u306f\u6b63\u3057\u304f\u52d5\u4f5c\u3057\u3066\u3044\u307e\u3059",
|
||||
"example_comment_meaning": "\u305f\u3060\u306e\u30b3\u30e1\u30f3\u30c8\u3067\u3059",
|
||||
"example_comment_hash": "# \u3053\u3053\u3082\u30b3\u30e1\u30f3\u30c8\u3067\u3059",
|
||||
"example_upstream_regular": "\u901a\u5e38\u306eDNS\uff08UDP\u3067\u306e\u554f\u3044\u5408\u308f\u305b\uff09",
|
||||
"example_upstream_dot": "\u6697\u53f7\u5316\u3055\u308c\u3066\u3044\u308b <a href='https:\/\/en.wikipedia.org\/wiki\/DNS_over_TLS' target='_blank'>DNS-over-TLS<\/a>",
|
||||
"example_upstream_doh": "\u6697\u53f7\u5316\u3055\u308c\u3066\u3044\u308b <a href='https:\/\/en.wikipedia.org\/wiki\/DNS_over_HTTPS' target='_blank'>DNS-over-HTTPS<\/a>",
|
||||
"example_upstream_sdns": "<a href='https:\/\/dnscrypt.info\/' target='_blank'>DNSCrypt<\/a> \u307e\u305f\u306f <a href='https:\/\/en.wikipedia.org\/wiki\/DNS_over_HTTPS' target='_blank'>DNS-over-HTTPS<\/a> \u30ea\u30be\u30eb\u30d0\u306e\u305f\u3081\u306b <a href='https:\/\/dnscrypt.info\/stamps\/' target='_blank'>DNS Stamps<\/a> \u3092\u4f7f\u3048\u307e\u3059",
|
||||
"example_upstream_tcp": "\u901a\u5e38\u306eDNS\uff08TCP\u3067\u306e\u554f\u3044\u5408\u308f\u305b\uff09",
|
||||
"all_filters_up_to_date_toast": "\u3059\u3079\u3066\u306e\u30d5\u30a3\u30eb\u30bf\u306f\u65e2\u306b\u6700\u65b0\u3067\u3059",
|
||||
"updated_upstream_dns_toast": "\u4e0a\u6d41DNS\u30b5\u30fc\u30d0\u3092\u66f4\u65b0\u3057\u307e\u3057\u305f",
|
||||
"dns_test_ok_toast": "\u6307\u5b9a\u3055\u308c\u305fDNS\u30b5\u30fc\u30d0\u306f\u6b63\u3057\u304f\u52d5\u4f5c\u3057\u3066\u3044\u307e\u3059",
|
||||
"dns_test_not_ok_toast": "\u30b5\u30fc\u30d0 \"{{key}}\": \u4f7f\u7528\u3067\u304d\u307e\u305b\u3093\u3067\u3057\u305f\u3002\u6b63\u3057\u304f\u5165\u529b\u3055\u308c\u3066\u3044\u308b\u304b\u3069\u3046\u304b\u3092\u78ba\u8a8d\u3057\u3066\u304f\u3060\u3055\u3044",
|
||||
"unblock_btn": "\u30d6\u30ed\u30c3\u30af\u89e3\u9664",
|
||||
"block_btn": "\u30d6\u30ed\u30c3\u30af",
|
||||
"block_btn": "\u30d6\u30ed\u30c3\u30af\u3059\u308b",
|
||||
"time_table_header": "\u6642\u523b",
|
||||
"domain_name_table_header": "\u30c9\u30e1\u30a4\u30f3\u540d",
|
||||
"type_table_header": "\u7a2e\u985e",
|
||||
"response_table_header": "\u5fdc\u7b54",
|
||||
"empty_response_status": "\u7a7a",
|
||||
"show_all_filter_type": "\u5168\u3066\u8868\u793a",
|
||||
"show_filtered_type": "\u30d5\u30a3\u30eb\u30bf\u30ea\u30f3\u30b0\u6e08\u307f\u3092\u8868\u793a",
|
||||
"no_logs_found": "\u30ed\u30b0\u898b\u3064\u304b\u308a\u307e\u305b\u3093\u3067\u3057\u305f",
|
||||
"client_table_header": "\u30af\u30e9\u30a4\u30a2\u30f3\u30c8",
|
||||
"empty_response_status": "\u672a\u5b9a\u7fa9",
|
||||
"show_all_filter_type": "\u3059\u3079\u3066\u8868\u793a",
|
||||
"show_filtered_type": "\u30d5\u30a3\u30eb\u30bf\u3055\u308c\u305f\u30ed\u30b0\u3092\u8868\u793a",
|
||||
"no_logs_found": "\u30ed\u30b0\u306f\u3042\u308a\u307e\u305b\u3093",
|
||||
"disabled_log_btn": "\u30ed\u30b0\u3092\u7121\u52b9\u306b\u3059\u308b",
|
||||
"download_log_file_btn": "\u30ed\u30b0\u30d5\u30a1\u30a4\u30eb\u3092\u30c0\u30a6\u30f3\u30ed\u30fc\u30c9",
|
||||
"refresh_btn": "\u66f4\u65b0",
|
||||
"download_log_file_btn": "\u30ed\u30b0\u30d5\u30a1\u30a4\u30eb\u3092\u30c0\u30a6\u30f3\u30ed\u30fc\u30c9\u3059\u308b",
|
||||
"refresh_btn": "\u6700\u65b0\u306b\u3059\u308b",
|
||||
"enabled_log_btn": "\u30ed\u30b0\u3092\u6709\u52b9\u306b\u3059\u308b",
|
||||
"last_dns_queries": "\u6700\u7d42\uff15\uff10\uff10\uff10\u672c\u306eDNS\u30af\u30a8\u30ea",
|
||||
"previous_btn": "\u524d",
|
||||
"last_dns_queries": "\u6700\u65b05000\u4ef6\u5206\u306eDNS\u30af\u30a8\u30ea",
|
||||
"previous_btn": "\u524d\u3078",
|
||||
"next_btn": "\u6b21\u3078",
|
||||
"loading_table_status": "\u8aad\u307f\u8fbc\u307f\u4e2d\u2026",
|
||||
"page_table_footer_text": "\u30da\u30fc\u30b8",
|
||||
"of_table_footer_text": "\uff0f",
|
||||
"rows_table_footer_text": "\u884c",
|
||||
"updated_custom_filtering_toast": "\u30ab\u30b9\u30bf\u30e0\u30fb\u30d5\u30a3\u30eb\u30bf\u30ea\u30f3\u30b0\u30eb\u30fc\u30eb\u3092\u66f4\u65b0\u3057\u307e\u3057\u305f",
|
||||
"rule_removed_from_custom_filtering_toast": "\u30ab\u30b9\u30bf\u30e0\u30fb\u30d5\u30a3\u30eb\u30bf\u30ea\u30f3\u30b0\u30eb\u30fc\u30eb\u304b\u3089\u30eb\u30fc\u30eb\u3092\u9664\u53bb\u3057\u307e\u3057\u305f",
|
||||
"rule_added_to_custom_filtering_toast": "\u30ab\u30b9\u30bf\u30e0\u30fb\u30d5\u30a3\u30eb\u30bf\u30ea\u30f3\u30b0\u30eb\u30fc\u30eb\u306b\u30eb\u30fc\u30eb\u3092\u8ffd\u52a0\u3057\u307e\u3057\u305f",
|
||||
"query_log_disabled_toast": "\u30af\u30a8\u30ea\u30fb\u30ed\u30b0\u7121\u52b9",
|
||||
"query_log_enabled_toast": "\u30af\u30a8\u30ea\u30fb\u30ed\u30b0\u6709\u52b9",
|
||||
"rule_removed_from_custom_filtering_toast": "\u30eb\u30fc\u30eb\u3092\u30ab\u30b9\u30bf\u30e0\u30fb\u30d5\u30a3\u30eb\u30bf\u30ea\u30f3\u30b0\u30eb\u30fc\u30eb\u304b\u3089\u9664\u53bb\u3057\u307e\u3057\u305f",
|
||||
"rule_added_to_custom_filtering_toast": "\u30eb\u30fc\u30eb\u3092\u30ab\u30b9\u30bf\u30e0\u30fb\u30d5\u30a3\u30eb\u30bf\u30ea\u30f3\u30b0\u30eb\u30fc\u30eb\u306b\u8ffd\u52a0\u3057\u307e\u3057\u305f",
|
||||
"query_log_disabled_toast": "\u30af\u30a8\u30ea\u30fb\u30ed\u30b0\u3092\u7121\u52b9\u306b\u3057\u307e\u3057\u305f",
|
||||
"query_log_enabled_toast": "\u30af\u30a8\u30ea\u30fb\u30ed\u30b0\u3092\u6709\u52b9\u306b\u3057\u307e\u3057\u305f",
|
||||
"source_label": "\u30bd\u30fc\u30b9",
|
||||
"found_in_known_domain_db": "\u65e2\u77e5\u306e\u30c9\u30e1\u30a4\u30f3\u30c7\u30fc\u30bf\u30d9\u30fc\u30b9\u306b\u898b\u3064\u304b\u308a\u307e\u3057\u305f\u3002",
|
||||
"category_label": "\u30ab\u30c6\u30b4\u30ea",
|
||||
"rule_label": "\u30eb\u30fc\u30eb",
|
||||
"filter_label": "\u30d5\u30a3\u30eb\u30bf"
|
||||
"filter_label": "\u30d5\u30a3\u30eb\u30bf",
|
||||
"unknown_filter": "\u4e0d\u660e\u306a\u30d5\u30a3\u30eb\u30bf {{filterId}}"
|
||||
}
|
||||
@@ -1,4 +1,33 @@
|
||||
{
|
||||
"url_added_successfully": "Url adicionada com sucesso",
|
||||
"check_dhcp_servers": "Verifique se h\u00e1 servidores DHCP",
|
||||
"save_config": "Salvar configura\u00e7\u00e3o",
|
||||
"enabled_dhcp": "Servidor DHCP ativado",
|
||||
"disabled_dhcp": "Servidor DHCP desativado",
|
||||
"dhcp_title": "Servidor DHCP (experimental)",
|
||||
"dhcp_description": "Se o seu roteador n\u00e3o fornecer configura\u00e7\u00f5es de DHCP, voc\u00ea poder\u00e1 usar o servidor DHCP integrado do AdGuard.",
|
||||
"dhcp_enable": "Ativar servidor DHCP",
|
||||
"dhcp_disable": "Desativar servidor DHCP",
|
||||
"dhcp_not_found": "Nenhum servidor DHCP ativo foi encontrado na sua rede. \u00c9 seguro ativar o servidor DHCP integrado.",
|
||||
"dhcp_found": "Foram encontrados servidores DHCP ativos na rede. N\u00e3o \u00e9 seguro ativar o servidor DHCP integrado.",
|
||||
"dhcp_leases": "Concess\u00f5es DHCP",
|
||||
"dhcp_leases_not_found": "Nenhuma concess\u00e3o DHCP encontrada",
|
||||
"dhcp_config_saved": "Salvar configura\u00e7\u00f5es do servidor DHCP",
|
||||
"form_error_required": "Campo obrigat\u00f3rio",
|
||||
"form_error_ip_format": "formato de endere\u00e7o IPv4 inv\u00e1lido",
|
||||
"form_error_positive": "Deve ser maior que 0",
|
||||
"dhcp_form_gateway_input": "IP do gateway",
|
||||
"dhcp_form_subnet_input": "M\u00e1scara de sub-rede",
|
||||
"dhcp_form_range_title": "Faixa de endere\u00e7os IP",
|
||||
"dhcp_form_range_start": "In\u00edcio da faixa",
|
||||
"dhcp_form_range_end": "Final da faixa",
|
||||
"dhcp_form_lease_title": "Tempo de concess\u00e3o do DHCP (em segundos)",
|
||||
"dhcp_form_lease_input": "Dura\u00e7\u00e3o da concess\u00e3o",
|
||||
"dhcp_interface_select": "Selecione a interface DHCP",
|
||||
"dhcp_hardware_address": "Endere\u00e7o de hardware",
|
||||
"dhcp_ip_addresses": "Endere\u00e7o de IP",
|
||||
"dhcp_table_hostname": "Hostname",
|
||||
"dhcp_table_expires": "Expira",
|
||||
"back": "Voltar",
|
||||
"dashboard": "Painel",
|
||||
"settings": "Configura\u00e7\u00f5es",
|
||||
@@ -18,7 +47,7 @@
|
||||
"disabled_protection": "Prote\u00e7\u00e3o desativada",
|
||||
"refresh_statics": "Atualizar estat\u00edsticas",
|
||||
"dns_query": "Consultas de DNS",
|
||||
"blocked_by": "Bloqueador por",
|
||||
"blocked_by": "Bloqueador por filtros",
|
||||
"stats_malware_phishing": "Bloqueado malware\/phishing",
|
||||
"stats_adult": "Bloqueado sites adultos",
|
||||
"stats_query_domain": "Principais dom\u00ednios consultados",
|
||||
@@ -86,6 +115,11 @@
|
||||
"example_comment": "! Aqui vai um coment\u00e1rio",
|
||||
"example_comment_meaning": "apenas um coment\u00e1rio",
|
||||
"example_comment_hash": "# Tamb\u00e9m um coment\u00e1rio",
|
||||
"example_upstream_regular": "DNS regular (atrav\u00e9s do UDP)",
|
||||
"example_upstream_dot": "DNS criptografado <a href='https:\/\/en.wikipedia.org\/wiki\/DNS_over_TLS' target='_blank'>atrav\u00e9s do TLS<\/a>",
|
||||
"example_upstream_doh": "DNS criptografado <a href='https:\/\/en.wikipedia.org\/wiki\/DNS_over_HTTPS' target='_blank'>atrav\u00e9s do HTTPS<\/a>",
|
||||
"example_upstream_sdns": "Voc\u00ea pode usar <a href='https:\/\/dnscrypt.info\/stamps\/' target='_blank'>DNS Stamps<\/a>para o<a href='https:\/\/dnscrypt.info\/' target='_blank'>DNSCrypt<\/a>ou usar resolvedores<a href='https:\/\/en.wikipedia.org\/wiki\/DNS_over_HTTPS' target='_blank'>DNS-sobre-HTTPS<\/a>",
|
||||
"example_upstream_tcp": "DNS regular (atrav\u00e9s do TCP)",
|
||||
"all_filters_up_to_date_toast": "Todos os filtros j\u00e1 est\u00e3o atualizados",
|
||||
"updated_upstream_dns_toast": "Atualizado os servidores DNS upstream",
|
||||
"dns_test_ok_toast": "Os servidores DNS especificados est\u00e3o funcionando corretamente",
|
||||
@@ -96,6 +130,7 @@
|
||||
"domain_name_table_header": "Nome de dom\u00ednio",
|
||||
"type_table_header": "Tipo",
|
||||
"response_table_header": "Resposta",
|
||||
"client_table_header": "Cliente",
|
||||
"empty_response_status": "Vazio",
|
||||
"show_all_filter_type": "Mostrar todos",
|
||||
"show_filtered_type": "Mostrar filtrados",
|
||||
@@ -120,5 +155,6 @@
|
||||
"found_in_known_domain_db": "Encontrado no banco de dados de dom\u00ednios conhecidos.",
|
||||
"category_label": "Categoria",
|
||||
"rule_label": "Regra",
|
||||
"filter_label": "Filtro"
|
||||
"filter_label": "Filtro",
|
||||
"unknown_filter": "Filtro desconhecido {{filterId}}"
|
||||
}
|
||||
@@ -1,4 +1,32 @@
|
||||
{
|
||||
"check_dhcp_servers": "\u041f\u0440\u043e\u0432\u0435\u0440\u0438\u0442\u044c DHCP-\u0441\u0435\u0440\u0432\u0435\u0440\u044b",
|
||||
"save_config": "\u0421\u043e\u0445\u0440\u0430\u043d\u0438\u0442\u044c \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044e",
|
||||
"enabled_dhcp": "DHCP-\u0441\u0435\u0440\u0432\u0435\u0440 \u0432\u043a\u043b\u044e\u0447\u0435\u043d",
|
||||
"disabled_dhcp": "DHCP-\u0441\u0435\u0440\u0432\u0435\u0440 \u043e\u0442\u043a\u043b\u044e\u0447\u0435\u043d",
|
||||
"dhcp_title": "DHCP-\u0441\u0435\u0440\u0432\u0435\u0440 (\u044d\u043a\u0441\u043f\u0435\u0440\u0438\u043c\u0435\u043d\u0442\u0430\u043b\u044c\u043d\u044b\u0439!)",
|
||||
"dhcp_description": "\u0415\u0441\u043b\u0438 \u0432\u0430\u0448 \u0440\u043e\u0443\u0442\u0435\u0440 \u043d\u0435 \u043f\u0440\u0435\u0434\u043e\u0441\u0442\u0430\u0432\u043b\u044f\u0435\u0442 \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 DHCP, \u0432\u044b \u043c\u043e\u0436\u0435\u0442\u0435 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u044c \u0441\u043e\u0431\u0441\u0442\u0432\u0435\u043d\u043d\u044b\u0439 \u0432\u0441\u0442\u0440\u043e\u0435\u043d\u043d\u044b\u0439 DHCP-\u0441\u0435\u0440\u0432\u0435\u0440 AdGuard.",
|
||||
"dhcp_enable": "\u0412\u043a\u043b\u044e\u0447\u0438\u0442\u044c DHCP-\u0441\u0435\u0440\u0432\u0435\u0440",
|
||||
"dhcp_disable": "\u041e\u0442\u043a\u043b\u044e\u0447\u0438\u0442\u044c DHCP-\u0441\u0435\u0440\u0432\u0435\u0440",
|
||||
"dhcp_not_found": "\u0410\u043a\u0442\u0438\u0432\u043d\u044b\u0435 DHCP-\u0441\u0435\u0440\u0432\u0435\u0440\u044b \u0432 \u0441\u0435\u0442\u0438 \u043d\u0435 \u043d\u0430\u0439\u0434\u0435\u043d\u044b. \u0412\u044b \u043c\u043e\u0436\u0435\u0442\u0435 \u0431\u0435\u0437\u043e\u043f\u0430\u0441\u043d\u043e \u0432\u043a\u043b\u044e\u0447\u0438\u0442\u044c \u0432\u0441\u0442\u0440\u043e\u0435\u043d\u043d\u044b\u0439 \u0441\u0435\u0440\u0432\u0435\u0440 DHCP.",
|
||||
"dhcp_found": "\u041d\u0435\u043a\u043e\u0442\u043e\u0440\u044b\u0435 \u0430\u043a\u0442\u0438\u0432\u043d\u044b\u0435 DHCP-\u0441\u0435\u0440\u0432\u0435\u0440\u044b \u043d\u0430\u0439\u0434\u0435\u043d\u044b \u0432 \u0441\u0435\u0442\u0438. \u0412\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0435 \u0432\u0441\u0442\u0440\u043e\u0435\u043d\u043d\u043e\u0433\u043e DHCP-\u0441\u0435\u0440\u0432\u0435\u0440\u0430 \u043d\u0435\u0431\u0435\u0437\u043e\u043f\u0430\u0441\u043d\u043e.",
|
||||
"dhcp_leases": "\u0410\u0440\u0435\u043d\u0434\u0430 DHCP",
|
||||
"dhcp_leases_not_found": "\u0410\u0440\u0435\u043d\u0434\u0430 DHCP \u043d\u0435 \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0435\u043d\u0430",
|
||||
"dhcp_config_saved": "\u0421\u043e\u0445\u0440\u0430\u043d\u0435\u043d\u043d\u0430\u044f \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044f DHCP-\u0441\u0435\u0440\u0432\u0435\u0440\u0430",
|
||||
"form_error_required": "\u041e\u0431\u044f\u0437\u0430\u0442\u0435\u043b\u044c\u043d\u043e\u0435 \u043f\u043e\u043b\u0435",
|
||||
"form_error_ip_format": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0439 \u0444\u043e\u0440\u043c\u0430\u0442 IPv4",
|
||||
"form_error_positive": "\u0414\u043e\u043b\u0436\u043d\u043e \u0431\u044b\u0442\u044c \u0431\u043e\u043b\u044c\u0448\u0435 0",
|
||||
"dhcp_form_gateway_input": "IP-\u0430\u0434\u0440\u0435\u0441 \u0448\u043b\u044e\u0437\u0430",
|
||||
"dhcp_form_subnet_input": "\u041c\u0430\u0441\u043a\u0430 \u043f\u043e\u0434\u0441\u0435\u0442\u0438",
|
||||
"dhcp_form_range_title": "\u0414\u0438\u0430\u043f\u0430\u0437\u043e\u043d IP-\u0430\u0434\u0440\u0435\u0441\u043e\u0432",
|
||||
"dhcp_form_range_start": "\u041d\u0430\u0447\u0430\u043b\u043e \u0434\u0438\u0430\u043f\u0430\u0437\u043e\u043d\u0430",
|
||||
"dhcp_form_range_end": "\u041a\u043e\u043d\u0435\u0446 \u0434\u0438\u0430\u043f\u0430\u0437\u043e\u043d\u0430",
|
||||
"dhcp_form_lease_title": "\u0412\u0440\u0435\u043c\u044f \u0430\u0440\u0435\u043d\u0434\u044b DHCP (\u0432 \u0441\u0435\u043a\u0443\u043d\u0434\u0430\u0445)",
|
||||
"dhcp_form_lease_input": "\u0421\u0440\u043e\u043a \u0430\u0440\u0435\u043d\u0434\u044b",
|
||||
"dhcp_interface_select": "\u0412\u044b\u0431\u0440\u0430\u0442\u044c \u0438\u043d\u0442\u0435\u0440\u0444\u0435\u0439\u0441 DHCP",
|
||||
"dhcp_hardware_address": "\u0410\u043f\u043f\u0430\u0440\u0430\u0442\u043d\u044b\u0439 \u0430\u0434\u0440\u0435\u0441",
|
||||
"dhcp_ip_addresses": "IP-\u0430\u0434\u0440\u0435\u0441\u0430",
|
||||
"dhcp_table_hostname": "\u0418\u043c\u044f \u0445\u043e\u0441\u0442\u0430",
|
||||
"dhcp_table_expires": "\u0418\u0441\u0442\u0435\u043a\u0430\u0435\u0442",
|
||||
"back": "\u041d\u0430\u0437\u0430\u0434",
|
||||
"dashboard": "\u041f\u0430\u043d\u0435\u043b\u044c \u0443\u043f\u0440\u0430\u0432\u043b\u0435\u043d\u0438\u044f",
|
||||
"settings": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438",
|
||||
@@ -18,7 +46,7 @@
|
||||
"disabled_protection": "\u0417\u0430\u0449\u0438\u0442\u0430 \u0432\u044b\u043a\u043b.",
|
||||
"refresh_statics": "\u041e\u0431\u043d\u043e\u0432\u0438\u0442\u044c \u0441\u0442\u0430\u0442\u0438\u0441\u0442\u0438\u043a\u0443",
|
||||
"dns_query": "DNS-\u0437\u0430\u043f\u0440\u043e\u0441\u044b",
|
||||
"blocked_by": "\u0417\u0430\u0431\u043b\u043e\u043a\u0438\u0440\u043e\u0432\u0430\u043d\u043e ",
|
||||
"blocked_by": "\u0417\u0430\u0431\u043b\u043e\u043a\u0438\u0440\u043e\u0432\u0430\u043d\u043e \u0444\u0438\u043b\u044c\u0442\u0440\u0430\u043c\u0438",
|
||||
"stats_malware_phishing": "\u0417\u0430\u0431\u043b\u043e\u043a\u0438\u0440\u043e\u0432\u0430\u043d\u043d\u044b\u0435 \u0432\u0440\u0435\u0434\u043e\u043d\u043e\u0441\u043d\u044b\u0435 \u0438 \u0444\u0438\u0448\u0438\u043d\u0433\u043e\u0432\u044b\u0435 \u0441\u0430\u0439\u0442\u044b",
|
||||
"stats_adult": "\u0417\u0430\u0431\u043b\u043e\u043a\u0438\u0440\u043e\u0432\u0430\u043d\u043d\u044b\u0435 \"\u0432\u0437\u0440\u043e\u0441\u043b\u044b\u0435\" \u0441\u0430\u0439\u0442\u044b",
|
||||
"stats_query_domain": "\u0427\u0430\u0441\u0442\u043e \u0437\u0430\u043f\u0440\u0430\u0448\u0438\u0432\u0430\u0435\u043c\u044b\u0435 \u0434\u043e\u043c\u0435\u043d\u044b",
|
||||
@@ -77,7 +105,7 @@
|
||||
"check_updates_btn": "\u041f\u0440\u043e\u0432\u0435\u0440\u0438\u0442\u044c \u043e\u0431\u043d\u043e\u0432\u043b\u0435\u043d\u0438\u044f",
|
||||
"new_filter_btn": "\u0414\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u0438\u0435 \u043d\u043e\u0432\u043e\u0433\u043e \u0444\u0438\u043b\u044c\u0442\u0440\u0430",
|
||||
"enter_valid_filter_url": "\u0412\u0432\u0435\u0434\u0438\u0442\u0435 \u0434\u0435\u0439\u0441\u0442\u0432\u0443\u044e\u0449\u0438\u0439 URL \u0434\u043b\u044f \u043f\u043e\u0434\u043f\u0438\u0441\u043a\u0438 \u043d\u0430 \u0444\u0438\u043b\u044c\u0442\u0440 \u0438\u043b\u0438 \u0444\u0430\u0439\u043b hosts.",
|
||||
"custom_filter_rules": "\u041F\u043E\u043B\u044C\u0437\u043E\u0432\u0430\u0442\u0435\u043B\u044C\u0441\u043A\u0438\u0439 \u0444\u0438\u043B\u044C\u0442\u0440",
|
||||
"custom_filter_rules": "\u041f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044c\u0441\u043a\u043e\u0435 \u043f\u0440\u0430\u0432\u0438\u043b\u043e \u0444\u0438\u043b\u044c\u0442\u0440\u0430\u0446\u0438\u0438",
|
||||
"custom_filter_rules_hint": "\u0412\u0432\u043e\u0434\u0438\u0442\u0435 \u043f\u043e \u043e\u0434\u043d\u043e\u043c\u0443 \u043f\u0440\u0430\u0432\u0438\u043b\u0443 \u043d\u0430 \u0441\u0442\u0440\u043e\u0447\u043a\u0443. \u0412\u044b \u043c\u043e\u0436\u0435\u0442\u0435 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u044c \u043f\u0440\u0430\u0432\u0438\u043b\u0430 \u0431\u043b\u043e\u043a\u0438\u0440\u043e\u0432\u043a\u0438 \u0438\u043b\u0438 \u0441\u0438\u043d\u0442\u0430\u043a\u0441\u0438\u0441 \u0444\u0430\u0439\u043b\u043e\u0432 hosts.",
|
||||
"examples_title": "\u041f\u0440\u0438\u043c\u0435\u0440\u044b",
|
||||
"example_meaning_filter_block": "\u0437\u0430\u0431\u043b\u043e\u043a\u0438\u0440\u043e\u0432\u0430\u0442\u044c \u0434\u043e\u0441\u0442\u0443\u043f \u043a \u0434\u043e\u043c\u0435\u043d\u0443 example.org \u0438 \u0432\u0441\u0435\u043c \u0435\u0433\u043e \u043f\u043e\u0434\u0434\u043e\u043c\u0435\u043d\u0430\u043c",
|
||||
@@ -89,6 +117,7 @@
|
||||
"example_upstream_regular": "\u043e\u0431\u044b\u0447\u043d\u044b\u0439 DNS (\u043f\u043e\u0432\u0435\u0440\u0445 UDP)",
|
||||
"example_upstream_dot": "\u0437\u0430\u0448\u0438\u0444\u0440\u043e\u0432\u0430\u043d\u043d\u044b\u0439 <a href='https:\/\/en.wikipedia.org\/wiki\/DNS_over_TLS' target='_blank'>DNS-\u043f\u043e\u0432\u0435\u0440\u0445-TLS<\/a>",
|
||||
"example_upstream_doh": "\u0437\u0430\u0448\u0438\u0444\u0440\u043e\u0432\u0430\u043d\u043d\u044b\u0439 <a href='https:\/\/en.wikipedia.org\/wiki\/DNS_over_HTTPS' target='_blank'>DNS-\u043f\u043e\u0432\u0435\u0440\u0445-HTTPS<\/a>",
|
||||
"example_upstream_sdns": "\u0432\u044b \u043c\u043e\u0436\u0435\u0442\u0435 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u044c <a href='https:\/\/dnscrypt.info\/stamps\/' target='_blank'>DNS Stamps<\/a> \u0434\u043b\u044f <a href='https:\/\/dnscrypt.info\/' target='_blank'>DNSCrypt<\/a> \u0438\u043b\u0438 <a href='https:\/\/en.wikipedia.org\/wiki\/DNS_over_HTTPS' target='_blank'>DNS-over-HTTPS<\/a> \u0440\u0435\u0437\u043e\u043b\u0432\u0435\u0440\u043e\u0432",
|
||||
"example_upstream_tcp": "\u043e\u0431\u044b\u0447\u043d\u044b\u0439 DNS (\u043f\u043e\u0432\u0435\u0440\u0445 TCP)",
|
||||
"all_filters_up_to_date_toast": "\u0412\u0441\u0435 \u0444\u0438\u043b\u044c\u0442\u0440\u044b \u043e\u0431\u043d\u043e\u0432\u043b\u0435\u043d\u044b",
|
||||
"updated_upstream_dns_toast": "Upstream DNS-\u0441\u0435\u0440\u0432\u0435\u0440\u044b \u043e\u0431\u043d\u043e\u0432\u043b\u0435\u043d\u044b",
|
||||
@@ -100,6 +129,7 @@
|
||||
"domain_name_table_header": "\u0414\u043e\u043c\u0435\u043d",
|
||||
"type_table_header": "\u0422\u0438\u043f",
|
||||
"response_table_header": "\u041e\u0442\u0432\u0435\u0442",
|
||||
"client_table_header": "\u041a\u043b\u0438\u0435\u043d\u0442",
|
||||
"empty_response_status": "\u041f\u0443\u0441\u0442\u043e",
|
||||
"show_all_filter_type": "\u041f\u043e\u043a\u0430\u0437\u0430\u0442\u044c \u0432\u0441\u0435",
|
||||
"show_filtered_type": "\u041f\u043e\u043a\u0430\u0437\u0430\u0442\u044c \u043e\u0442\u0444\u0438\u043b\u044c\u0442\u0440\u043e\u0432\u0430\u043d\u043d\u044b\u0435",
|
||||
@@ -124,5 +154,6 @@
|
||||
"found_in_known_domain_db": "\u041d\u0430\u0439\u0434\u0435\u043d \u0432 \u0431\u0430\u0437\u0435 \u0438\u0437\u0432\u0435\u0441\u0442\u043d\u044b\u0445 \u0434\u043e\u043c\u0435\u043d\u043e\u0432.",
|
||||
"category_label": "\u041a\u0430\u0442\u0435\u0433\u043e\u0440\u0438\u044f",
|
||||
"rule_label": "\u041f\u0440\u0430\u0432\u0438\u043b\u043e",
|
||||
"filter_label": "\u0424\u0438\u043b\u044c\u0442\u0440"
|
||||
"filter_label": "\u0424\u0438\u043b\u044c\u0442\u0440",
|
||||
"unknown_filter": "\u041d\u0435\u0438\u0437\u0432\u0435\u0441\u0442\u043d\u044b\u0439 \u0444\u0438\u043b\u044c\u0442\u0440 {{filterId}}"
|
||||
}
|
||||
@@ -1,4 +1,33 @@
|
||||
{
|
||||
"url_added_successfully": "URL tillagd utan fel",
|
||||
"check_dhcp_servers": "Letar efter DHCP-servrar",
|
||||
"save_config": "Spara inst\u00e4llningar",
|
||||
"enabled_dhcp": "DHCP-server aktiverad",
|
||||
"disabled_dhcp": "Dhcp-server avaktiverad",
|
||||
"dhcp_title": "DHCP-server (experimentell)",
|
||||
"dhcp_description": "Om din router inte har inst\u00e4llningar f\u00f6r DHCP kan du anv\u00e4nda AdGuards inbyggda server.",
|
||||
"dhcp_enable": "Aktivera DHCP.-server",
|
||||
"dhcp_disable": "Avaktivera DHCP-server",
|
||||
"dhcp_not_found": "Ingen aktiv DHCP-server hittades i n\u00e4tverkat.",
|
||||
"dhcp_found": "N\u00e5gra aktiva DHCP-servar uppt\u00e4cktes. Det \u00e4r inte s\u00e4kert att aktivera inbyggda DHCP-servrar.",
|
||||
"dhcp_leases": "DHCP-lease",
|
||||
"dhcp_leases_not_found": "Ingen DHCP-lease hittad",
|
||||
"dhcp_config_saved": "Sparade inst\u00e4llningar f\u00f6r DHCP-servern",
|
||||
"form_error_required": "Obligatoriskt f\u00e4lt",
|
||||
"form_error_ip_format": "Ogiltigt IPv4-format",
|
||||
"form_error_positive": "M\u00e5ste vara st\u00f6rre \u00e4n noll",
|
||||
"dhcp_form_gateway_input": "Gateway-IP",
|
||||
"dhcp_form_subnet_input": "Subnetmask",
|
||||
"dhcp_form_range_title": "IP-adressgr\u00e4nser",
|
||||
"dhcp_form_range_start": "Startgr\u00e4ns",
|
||||
"dhcp_form_range_end": "Gr\u00e4nsslut",
|
||||
"dhcp_form_lease_title": "DHCP-leasetid (i sekunder)",
|
||||
"dhcp_form_lease_input": "Leasetid",
|
||||
"dhcp_interface_select": "V\u00e4lj DHCP-gr\u00e4nssnitt",
|
||||
"dhcp_hardware_address": "H\u00e5rdvaruadress",
|
||||
"dhcp_ip_addresses": "IP-adresser",
|
||||
"dhcp_table_hostname": "V\u00e4rdnamn",
|
||||
"dhcp_table_expires": "Utg\u00e5r",
|
||||
"back": "Tiilbaka",
|
||||
"dashboard": "Kontrollpanel",
|
||||
"settings": "Inst\u00e4llningar",
|
||||
@@ -18,7 +47,7 @@
|
||||
"disabled_protection": "Kopplade bort skydd",
|
||||
"refresh_statics": "Uppdatera statistik",
|
||||
"dns_query": "DNS-f\u00f6rfr\u00e5gningar",
|
||||
"blocked_by": "Blockerat av",
|
||||
"blocked_by": "Blockerat av filter",
|
||||
"stats_malware_phishing": "Blockerad skadekod\/phising",
|
||||
"stats_adult": "Blockerade vuxensajter",
|
||||
"stats_query_domain": "Mest efters\u00f6kta dom\u00e4ner",
|
||||
@@ -86,6 +115,11 @@
|
||||
"example_comment": "! H\u00e4r kommer en kommentar",
|
||||
"example_comment_meaning": "Endast en kommentar",
|
||||
"example_comment_hash": "# Ocks\u00e5 en kommentar",
|
||||
"example_upstream_regular": "vanlig DNS (\u00f6ver UDP)",
|
||||
"example_upstream_dot": "krypterat <a href='https:\/\/en.wikipedia.org\/wiki\/DNS_over_TLS' target='_blank'>DNS-over-TLS<\/a>",
|
||||
"example_upstream_doh": "krypterat <a href='https:\/\/en.wikipedia.org\/wiki\/DNS_over_HTTPS' target='_blank'>DNS-over-HTTPS<\/a>",
|
||||
"example_upstream_sdns": "Du kan anv\u00e4nda <a href='https:\/\/dnscrypt.info\/stamps\/' target='_blank'>DNS-stamps<\/a> f\u00f6r <a href='https:\/\/dnscrypt.info\/' target='_blank'>DNSCrypt<\/a> eller <a href='https:\/\/en.wikipedia.org\/wiki\/DNS_over_HTTPS' target='_blank'>DNS-\u00f6ver-HTTPS<\/a>\n-resolvers",
|
||||
"example_upstream_tcp": "vanlig DNS (\u00f6ver UDP)",
|
||||
"all_filters_up_to_date_toast": "Alla filter \u00e4r redan aktuella",
|
||||
"updated_upstream_dns_toast": "Uppdaterade uppstr\u00f6ms-dns-servrar",
|
||||
"dns_test_ok_toast": "Angivna DNS servrar fungerar korrekt",
|
||||
@@ -96,7 +130,8 @@
|
||||
"domain_name_table_header": "Dom\u00e4nnamn",
|
||||
"type_table_header": "Typ",
|
||||
"response_table_header": "Svar",
|
||||
"empty_response_status": "Empty",
|
||||
"client_table_header": "Klient",
|
||||
"empty_response_status": "Tomt",
|
||||
"show_all_filter_type": "Visa alla",
|
||||
"show_filtered_type": "Visa filtrerade",
|
||||
"no_logs_found": "Inga logga funna",
|
||||
@@ -120,5 +155,6 @@
|
||||
"found_in_known_domain_db": "Hittad i dom\u00e4ndatabas.",
|
||||
"category_label": "Kategori",
|
||||
"rule_label": "Regel",
|
||||
"filter_label": "Filter"
|
||||
"filter_label": "Filter",
|
||||
"unknown_filter": "Ok\u00e4nt filter {{filterId}}"
|
||||
}
|
||||
@@ -1,4 +1,30 @@
|
||||
{
|
||||
"check_dhcp_servers": "Ki\u1ec3m tra m\u00e1y ch\u1ee7 DHCP",
|
||||
"save_config": "L\u01b0u thi\u1ebft l\u1eadp",
|
||||
"enabled_dhcp": "M\u00e1y ch\u1ee7 DHCP \u0111\u00e3 k\u00edch ho\u1ea1t",
|
||||
"disabled_dhcp": "M\u00e1y ch\u1ee7 DHCP \u0111\u00e3 t\u1eaft",
|
||||
"dhcp_title": "M\u00e1y ch\u1ee7 DHCP (th\u1eed nghi\u1ec7m!)",
|
||||
"dhcp_description": "N\u1ebfu b\u1ed9 \u0111\u1ecbnh tuy\u1ebfn kh\u00f4ng tr\u1ee3 c\u00e0i \u0111\u1eb7t DHCP, b\u1ea1n c\u00f3 th\u1ec3 d\u00f9ng m\u00e1y ch\u1ee7 DHCP d\u1ef1ng s\u1eb5n c\u1ee7a AdGuard",
|
||||
"dhcp_enable": "B\u1eadt m\u00e1y ch\u1ee7 DHCP",
|
||||
"dhcp_disable": "T\u1eaft m\u00e1y ch\u1ee7 DHCP",
|
||||
"dhcp_not_found": "Kh\u00f4ng c\u00f3 m\u00e1y ch\u1ee7 DHCP n\u00e0o \u0111\u01b0\u1ee3c t\u00ecm th\u1ea5y trong m\u1ea1ng. C\u00f3 th\u1ec3 b\u1eadt m\u00e1y ch\u1ee7 DHCP m\u1ed9t c\u00e1ch an to\u00e0n",
|
||||
"dhcp_found": "\u0110\u00e3 t\u00ecm th\u1ea5y m\u00e1y ch\u1ee7 DHCP trong m\u1ea1ng. C\u00f3 th\u1ec3 c\u00f3 r\u1ee7i ro n\u1ebfu k\u00edch ho\u1ea1t m\u00e1y ch\u1ee7 DHCP d\u1ef1ng s\u1eb5n",
|
||||
"dhcp_leases": "DHCP leases",
|
||||
"dhcp_leases_not_found": "No DHCP leases found",
|
||||
"dhcp_config_saved": "Saved DHCP server config",
|
||||
"form_error_required": "Required field",
|
||||
"form_error_ip_format": "Invalid IPv4 format",
|
||||
"form_error_positive": "Ph\u1ea3i l\u1edbn h\u01a1n 0",
|
||||
"dhcp_form_gateway_input": "Gateway IP",
|
||||
"dhcp_form_subnet_input": "Subnet mask",
|
||||
"dhcp_form_range_title": "Range of IP addresses",
|
||||
"dhcp_form_range_start": "Range start",
|
||||
"dhcp_form_range_end": "IP k\u1ebft th\u00fac",
|
||||
"dhcp_form_lease_title": "DHCP lease time (in seconds)",
|
||||
"dhcp_form_lease_input": "Lease duration",
|
||||
"dhcp_interface_select": "Ch\u1ecdn m\u1ed9t card m\u1ea1ng",
|
||||
"dhcp_hardware_address": "Hardware address",
|
||||
"dhcp_ip_addresses": "IP addresses",
|
||||
"back": "Quay l\u1ea1i",
|
||||
"dashboard": "T\u1ed5ng quan",
|
||||
"settings": "C\u00e0i \u0111\u1eb7t",
|
||||
@@ -18,7 +44,7 @@
|
||||
"disabled_protection": "\u0110\u00e3 t\u1eaft b\u1ea3o v\u1ec7",
|
||||
"refresh_statics": "L\u00e0m m\u1edbi th\u1ed1ng k\u00ea",
|
||||
"dns_query": "Truy v\u1ea5n DNS",
|
||||
"blocked_by": "Ch\u1eb7n b\u1edfi",
|
||||
"blocked_by": "Ch\u1eb7n b\u1edfi b\u1ed9 l\u1ecdc",
|
||||
"stats_malware_phishing": "M\u00e3 \u0111\u1ed9c\/l\u1eeba \u0111\u1ea3o \u0111\u00e3 ch\u1eb7n",
|
||||
"stats_adult": "Website ng\u01b0\u1eddi l\u1edbn \u0111\u00e3 ch\u1eb7n",
|
||||
"stats_query_domain": "T\u00ean mi\u1ec1n truy v\u1ea5n nhi\u1ec1u",
|
||||
@@ -86,6 +112,11 @@
|
||||
"example_comment": "! \u0110\u00e2y l\u00e0 m\u1ed9t ch\u00fa th\u00edch",
|
||||
"example_comment_meaning": "Ch\u1ec9 l\u00e0 m\u1ed9t ch\u00fa th\u00edch",
|
||||
"example_comment_hash": "# C\u0169ng l\u00e0 m\u1ed9t ch\u00fa th\u00edch",
|
||||
"example_upstream_regular": "DNS th\u00f4ng th\u01b0\u1eddng (d\u00f9ng UDP)",
|
||||
"example_upstream_dot": "\u0111\u01b0\u1ee3c m\u00e3 ho\u00e1 <a href='https:\/\/en.wikipedia.org\/wiki\/DNS_over_TLS' target='_blank'>DNS-over-TLS<\/a>",
|
||||
"example_upstream_doh": "\u0111\u01b0\u1ee3c m\u00e3 ho\u00e1 <a href='https:\/\/en.wikipedia.org\/wiki\/DNS_over_HTTPS' target='_blank'>DNS-over-HTTPS<\/a>",
|
||||
"example_upstream_sdns": "b\u1ea1n c\u00f3 th\u1ec3 s\u1eed d\u1ee5ng <a href='https:\/\/dnscrypt.info\/stamps\/' target='_blank'>DNS Stamps<\/a> for <a href='https:\/\/dnscrypt.info\/' target='_blank'>DNSCrypt<\/a> ho\u1eb7c<a href='https:\/\/en.wikipedia.org\/wiki\/DNS_over_HTTPS' target='_blank'>DNS-over-HTTPS<\/a> ",
|
||||
"example_upstream_tcp": "DNS th\u00f4ng th\u01b0\u1eddng(d\u00f9ng TCP)",
|
||||
"all_filters_up_to_date_toast": "T\u1ea5t c\u1ea3 b\u1ed9 l\u1ecdc \u0111\u00e3 \u0111\u01b0\u1ee3c c\u1eadp nh\u1eadt",
|
||||
"updated_upstream_dns_toast": "\u0110\u00e3 c\u1eadp nh\u1eadt m\u00e1y ch\u1ee7 DNS t\u00ecm ki\u1ebfm",
|
||||
"dns_test_ok_toast": "M\u00e1y ch\u1ee7 DNS c\u00f3 th\u1ec3 s\u1eed d\u1ee5ng",
|
||||
@@ -96,9 +127,10 @@
|
||||
"domain_name_table_header": "T\u00ean mi\u1ec1n",
|
||||
"type_table_header": "Lo\u1ea1i",
|
||||
"response_table_header": "Ph\u1ea3n h\u1ed3i",
|
||||
"client_table_header": "Ng\u01b0\u1eddi d\u00f9ng cu\u1ed1i",
|
||||
"empty_response_status": "R\u1ed7ng",
|
||||
"show_all_filter_type": "Hi\u1ec7n t\u1ea5t c\u1ea3",
|
||||
"show_filtered_type": "Ch\u1ec9 hi\u1ec7n \u0111\u00e3 ch\u1eb7n",
|
||||
"show_filtered_type": "Ch\u1ec9 hi\u1ec7n \u0111\u00e3 l\u1ecdc",
|
||||
"no_logs_found": "Kh\u00f4ng c\u00f3 l\u1ecbch s\u1eed truy v\u1ea5n",
|
||||
"disabled_log_btn": "T\u1eaft l\u1ecbch s\u1eed truy v\u1ea5n",
|
||||
"download_log_file_btn": "T\u1ea3i t\u1eadp tin l\u1ecbch s\u1eed truy v\u1ea5n",
|
||||
@@ -120,5 +152,7 @@
|
||||
"found_in_known_domain_db": "T\u00ecm th\u1ea5y trong c\u01a1 s\u1edf d\u1eef li\u1ec7u t\u00ean mi\u1ec1n",
|
||||
"category_label": "Th\u1ec3 lo\u1ea1i",
|
||||
"rule_label": "Quy t\u1eafc",
|
||||
"filter_label": "B\u1ed9 l\u1ecdc"
|
||||
"filter_label": "B\u1ed9 l\u1ecdc",
|
||||
"url_added_successfully": "Th\u00eam b\u1ed9 l\u1ecdc th\u00e0nh c\u00f4ng",
|
||||
"unknown_filter": "B\u1ed9 l\u1ecdc kh\u00f4ng r\u00f5 {{filterId}}"
|
||||
}
|
||||
214
client/src/__locales/zh-tw.json
Normal file
214
client/src/__locales/zh-tw.json
Normal file
@@ -0,0 +1,214 @@
|
||||
{
|
||||
"url_added_successfully": "\u7db2\u5740\u88ab\u6210\u529f\u5730\u52a0\u5165",
|
||||
"check_dhcp_servers": "\u6aa2\u67e5\u52d5\u614b\u4e3b\u6a5f\u8a2d\u5b9a\u5354\u5b9a\uff08DHCP\uff09\u4f3a\u670d\u5668",
|
||||
"save_config": "\u5132\u5b58\u914d\u7f6e",
|
||||
"enabled_dhcp": "\u52d5\u614b\u4e3b\u6a5f\u8a2d\u5b9a\u5354\u5b9a\uff08DHCP\uff09\u4f3a\u670d\u5668\u88ab\u555f\u7528",
|
||||
"disabled_dhcp": "\u52d5\u614b\u4e3b\u6a5f\u8a2d\u5b9a\u5354\u5b9a\uff08DHCP\uff09\u4f3a\u670d\u5668\u88ab\u7981\u7528",
|
||||
"dhcp_title": "\u52d5\u614b\u4e3b\u6a5f\u8a2d\u5b9a\u5354\u5b9a\uff08DHCP\uff09\u4f3a\u670d\u5668\uff08\u5be6\u9a57\u6027\u7684\uff01\uff09",
|
||||
"dhcp_description": "\u5982\u679c\u60a8\u7684\u8def\u7531\u5668\u672a\u63d0\u4f9b\u52d5\u614b\u4e3b\u6a5f\u8a2d\u5b9a\u5354\u5b9a\uff08DHCP\uff09\u8a2d\u5b9a\uff0c\u60a8\u53ef\u4f7f\u7528AdGuard\u81ea\u8eab\u5167\u5efa\u7684DHCP\u4f3a\u670d\u5668\u3002",
|
||||
"dhcp_enable": "\u555f\u7528\u52d5\u614b\u4e3b\u6a5f\u8a2d\u5b9a\u5354\u5b9a\uff08DHCP\uff09\u4f3a\u670d\u5668",
|
||||
"dhcp_disable": "\u7981\u7528\u52d5\u614b\u4e3b\u6a5f\u8a2d\u5b9a\u5354\u5b9a\uff08DHCP\uff09\u4f3a\u670d\u5668",
|
||||
"dhcp_not_found": "\u65bc\u8a72\u7db2\u8def\u4e0a\u7121\u73fe\u884c\u7684\u52d5\u614b\u4e3b\u6a5f\u8a2d\u5b9a\u5354\u5b9a\uff08DHCP\uff09\u4f3a\u670d\u5668\u88ab\u767c\u73fe\u3002\u555f\u7528\u5167\u5efa\u7684DHCP\u4f3a\u670d\u5668\u70ba\u5b89\u5168\u7684\u3002",
|
||||
"dhcp_found": "\u65bc\u8a72\u7db2\u8def\u4e0a\u67d0\u4e9b\u73fe\u884c\u7684\u52d5\u614b\u4e3b\u6a5f\u8a2d\u5b9a\u5354\u5b9a\uff08DHCP\uff09\u4f3a\u670d\u5668\u88ab\u767c\u73fe\u3002\u555f\u7528\u5167\u5efa\u7684DHCP\u4f3a\u670d\u5668\u70ba\u4e0d\u5b89\u5168\u7684\u3002",
|
||||
"dhcp_leases": "\u52d5\u614b\u4e3b\u6a5f\u8a2d\u5b9a\u5354\u5b9a\uff08DHCP\uff09\u79df\u8cc3",
|
||||
"dhcp_leases_not_found": "\u7121\u5df2\u767c\u73fe\u4e4b\u52d5\u614b\u4e3b\u6a5f\u8a2d\u5b9a\u5354\u5b9a\uff08DHCP\uff09\u79df\u8cc3",
|
||||
"dhcp_config_saved": "\u5df2\u5132\u5b58\u52d5\u614b\u4e3b\u6a5f\u8a2d\u5b9a\u5354\u5b9a\uff08DHCP\uff09\u4f3a\u670d\u5668\u914d\u7f6e",
|
||||
"form_error_required": "\u5fc5\u586b\u7684\u6b04\u4f4d",
|
||||
"form_error_ip_format": "\u7121\u6548\u7684IPv4\u683c\u5f0f",
|
||||
"form_error_positive": "\u5fc5\u9808\u5927\u65bc0",
|
||||
"dhcp_form_gateway_input": "\u9598\u9053 IP",
|
||||
"dhcp_form_subnet_input": "\u5b50\u7db2\u8def\u906e\u7f69",
|
||||
"dhcp_form_range_title": "IP \u4f4d\u5740\u7bc4\u570d",
|
||||
"dhcp_form_range_start": "\u7bc4\u570d\u958b\u59cb",
|
||||
"dhcp_form_range_end": "\u7bc4\u570d\u7d50\u675f",
|
||||
"dhcp_form_lease_title": "\u52d5\u614b\u4e3b\u6a5f\u8a2d\u5b9a\u5354\u5b9a\uff08DHCP\uff09\u79df\u8cc3\u6642\u9593\uff08\u4ee5\u79d2\u6578\uff09",
|
||||
"dhcp_form_lease_input": "\u79df\u8cc3\u6301\u7e8c\u6642\u9593",
|
||||
"dhcp_interface_select": "\u9078\u64c7\u52d5\u614b\u4e3b\u6a5f\u8a2d\u5b9a\u5354\u5b9a\uff08DHCP\uff09\u4ecb\u9762",
|
||||
"dhcp_hardware_address": "\u786c\u9ad4\u4f4d\u5740",
|
||||
"dhcp_ip_addresses": "IP \u4f4d\u5740",
|
||||
"dhcp_table_hostname": "\u4e3b\u6a5f\u540d\u7a31",
|
||||
"dhcp_table_expires": "\u5230\u671f",
|
||||
"dhcp_warning": "\u5982\u679c\u60a8\u60f3\u8981\u555f\u7528\u5167\u5efa\u7684\u52d5\u614b\u4e3b\u6a5f\u8a2d\u5b9a\u5354\u5b9a\uff08DHCP\uff09\u4f3a\u670d\u5668\uff0c\u78ba\u4fdd\u7121\u5176\u5b83\u73fe\u884c\u7684DHCP\u4f3a\u670d\u5668\u3002\u5426\u5247\uff0c\u5b83\u53ef\u80fd\u6703\u7834\u58de\u4f9b\u5df2\u9023\u7dda\u7684\u88dd\u7f6e\u4e4b\u7db2\u969b\u7db2\u8def\uff01",
|
||||
"back": "\u8fd4\u56de",
|
||||
"dashboard": "\u5100\u8868\u677f",
|
||||
"settings": "\u8a2d\u5b9a",
|
||||
"filters": "\u904e\u6ffe\u5668",
|
||||
"query_log": "\u67e5\u8a62\u8a18\u9304",
|
||||
"faq": "\u5e38\u898b\u554f\u7b54\u96c6",
|
||||
"version": "\u7248\u672c",
|
||||
"address": "\u4f4d\u5740",
|
||||
"on": "\u958b\u8457",
|
||||
"off": "\u95dc\u8457",
|
||||
"copyright": "\u7248\u6b0a",
|
||||
"homepage": "\u9996\u9801",
|
||||
"report_an_issue": "\u5831\u544a\u554f\u984c",
|
||||
"enable_protection": "\u555f\u7528\u9632\u8b77",
|
||||
"enabled_protection": "\u5df2\u555f\u7528\u9632\u8b77",
|
||||
"disable_protection": "\u7981\u7528\u9632\u8b77",
|
||||
"disabled_protection": "\u5df2\u7981\u7528\u9632\u8b77",
|
||||
"refresh_statics": "\u91cd\u65b0\u6574\u7406\u7d71\u8a08\u8cc7\u6599",
|
||||
"dns_query": "DNS \u67e5\u8a62",
|
||||
"blocked_by": "\u5df2\u88ab\u904e\u6ffe\u5668\u5c01\u9396",
|
||||
"stats_malware_phishing": "\u5df2\u5c01\u9396\u7684\u60e1\u610f\u8edf\u9ad4\/\u7db2\u8def\u91e3\u9b5a",
|
||||
"stats_adult": "\u5df2\u5c01\u9396\u7684\u6210\u4eba\u7db2\u7ad9",
|
||||
"stats_query_domain": "\u71b1\u9580\u5df2\u67e5\u8a62\u7684\u7db2\u57df",
|
||||
"for_last_24_hours": "\u5728\u6700\u8fd1\u768424\u5c0f\u6642\u5167",
|
||||
"no_domains_found": "\u7121\u5df2\u767c\u73fe\u4e4b\u7db2\u57df",
|
||||
"requests_count": "\u8acb\u6c42\u7e3d\u6578",
|
||||
"top_blocked_domains": "\u71b1\u9580\u5df2\u5c01\u9396\u7684\u7db2\u57df",
|
||||
"top_clients": "\u71b1\u9580\u7528\u6236\u7aef",
|
||||
"no_clients_found": "\u7121\u5df2\u767c\u73fe\u4e4b\u7528\u6236\u7aef",
|
||||
"general_statistics": "\u4e00\u822c\u7684\u7d71\u8a08\u8cc7\u6599",
|
||||
"number_of_dns_query_24_hours": "\u5728\u6700\u8fd1\u768424\u5c0f\u6642\u5167\u5df2\u8655\u7406\u7684DNS\u67e5\u8a62\u4e4b\u6578\u91cf",
|
||||
"number_of_dns_query_blocked_24_hours": "\u5df2\u88ab\u5ee3\u544a\u5c01\u9396\u904e\u6ffe\u5668\u548c\u4e3b\u6a5f\u5c01\u9396\u6e05\u55ae\u5c01\u9396\u7684DNS\u8acb\u6c42\u4e4b\u6578\u91cf",
|
||||
"number_of_dns_query_blocked_24_hours_by_sec": "\u5df2\u88abAdGuard\u700f\u89bd\u5b89\u5168\u6a21\u7d44\u5c01\u9396\u7684DNS\u8acb\u6c42\u4e4b\u6578\u91cf",
|
||||
"number_of_dns_query_blocked_24_hours_adult": "\u5df2\u5c01\u9396\u7684\u6210\u4eba\u7db2\u7ad9\u4e4b\u6578\u91cf",
|
||||
"enforced_save_search": "\u5df2\u5f37\u5236\u57f7\u884c\u7684\u5b89\u5168\u641c\u5c0b",
|
||||
"number_of_dns_query_to_safe_search": "\u5c0d\u65bc\u90a3\u4e9b\u5b89\u5168\u641c\u5c0b\u5df2\u88ab\u5f37\u5236\u57f7\u884c\u4e4b\u5c6c\u65bc\u641c\u5c0b\u5f15\u64ce\u7684DNS\u8acb\u6c42\u4e4b\u6578\u91cf",
|
||||
"average_processing_time": "\u5e73\u5747\u7684\u8655\u7406\u6642\u9593",
|
||||
"average_processing_time_hint": "\u65bc\u8655\u7406\u4e00\u9805DNS\u8acb\u6c42\u4e0a\u4ee5\u6beb\u79d2\uff08ms\uff09\u8a08\u4e4b\u5e73\u5747\u7684\u6642\u9593",
|
||||
"block_domain_use_filters_and_hosts": "\u900f\u904e\u904e\u6ffe\u5668\u548c\u4e3b\u6a5f\u6a94\u6848\u5c01\u9396\u7db2\u57df",
|
||||
"filters_block_toggle_hint": "\u60a8\u53ef\u5728<a href='#filters'>\u904e\u6ffe\u5668<\/a>\u8a2d\u5b9a\u4e2d\u8a2d\u7f6e\u5c01\u9396\u898f\u5247\u3002",
|
||||
"use_adguard_browsing_sec": "\u4f7f\u7528AdGuard\u700f\u89bd\u5b89\u5168\u7db2\u8def\u670d\u52d9",
|
||||
"use_adguard_browsing_sec_hint": "AdGuard Home\u5c07\u6aa2\u67e5\u7db2\u57df\u662f\u5426\u88ab\u700f\u89bd\u5b89\u5168\u7db2\u8def\u670d\u52d9\u5217\u5165\u9ed1\u540d\u55ae\u3002\u5b83\u5c07\u4f7f\u7528\u53cb\u597d\u7684\u96b1\u79c1\u67e5\u627e\u61c9\u7528\u7a0b\u5f0f\u4ecb\u9762\uff08API\uff09\u4ee5\u57f7\u884c\u6aa2\u67e5\uff1a\u50c5\u57df\u540dSHA256\u96dc\u6e4a\u7684\u77ed\u524d\u7db4\u88ab\u50b3\u9001\u5230\u4f3a\u670d\u5668\u3002",
|
||||
"use_adguard_parental": "\u4f7f\u7528AdGuard\u5bb6\u9577\u76e3\u63a7\u4e4b\u7db2\u8def\u670d\u52d9",
|
||||
"use_adguard_parental_hint": "AdGuard Home\u5c07\u6aa2\u67e5\u7db2\u57df\u662f\u5426\u5305\u542b\u6210\u4eba\u8cc7\u6599\u3002\u5b83\u4f7f\u7528\u5982\u540c\u700f\u89bd\u5b89\u5168\u7db2\u8def\u670d\u52d9\u4e00\u6a23\u4e4b\u53cb\u597d\u7684\u96b1\u79c1\u61c9\u7528\u7a0b\u5f0f\u4ecb\u9762\uff08API\uff09\u3002",
|
||||
"enforce_safe_search": "\u5f37\u5236\u57f7\u884c\u5b89\u5168\u641c\u5c0b",
|
||||
"enforce_save_search_hint": "AdGuard Home\u53ef\u5728\u4e0b\u5217\u7684\u641c\u5c0b\u5f15\u64ce\uff1aGoogle\u3001YouTube\u3001Bing\u548cYandex\u4e2d\u5f37\u5236\u57f7\u884c\u5b89\u5168\u641c\u5c0b\u3002",
|
||||
"no_servers_specified": "\u7121\u5df2\u660e\u78ba\u6307\u5b9a\u7684\u4f3a\u670d\u5668",
|
||||
"no_settings": "\u7121\u8a2d\u5b9a",
|
||||
"general_settings": "\u4e00\u822c\u7684\u8a2d\u5b9a",
|
||||
"upstream_dns": "\u4e0a\u6e38\u7684DNS\u4f3a\u670d\u5668",
|
||||
"upstream_dns_hint": "\u5982\u679c\u60a8\u4fdd\u7559\u8a72\u6b04\u4f4d\u7a7a\u767d\u7684\uff0cAdGuard Home\u5c07\u4f7f\u7528<a href='https:\/\/1.1.1.1\/' target='_blank'>Cloudflare DNS<\/a>\u4f5c\u70ba\u4e0a\u6e38\u3002\u5c0d\u65bcDNS over TLS\u4f3a\u670d\u5668\u4f7f\u7528 tls:\/\/ \u524d\u7db4\u3002",
|
||||
"test_upstream_btn": "\u6e2c\u8a66\u4e0a\u884c\u8cc7\u6599\u6d41",
|
||||
"apply_btn": "\u5957\u7528",
|
||||
"disabled_filtering_toast": "\u5df2\u7981\u7528\u904e\u6ffe",
|
||||
"enabled_filtering_toast": "\u5df2\u555f\u7528\u904e\u6ffe",
|
||||
"disabled_safe_browsing_toast": "\u5df2\u7981\u7528\u5b89\u5168\u700f\u89bd",
|
||||
"enabled_safe_browsing_toast": "\u5df2\u555f\u7528\u5b89\u5168\u700f\u89bd",
|
||||
"disabled_parental_toast": "\u5df2\u7981\u7528\u5bb6\u9577\u76e3\u63a7",
|
||||
"enabled_parental_toast": "\u5df2\u555f\u7528\u5bb6\u9577\u76e3\u63a7",
|
||||
"disabled_safe_search_toast": "\u5df2\u7981\u7528\u5b89\u5168\u641c\u5c0b",
|
||||
"enabled_save_search_toast": "\u5df2\u555f\u7528\u5b89\u5168\u641c\u5c0b",
|
||||
"enabled_table_header": "\u5df2\u555f\u7528\u7684",
|
||||
"name_table_header": "\u540d\u7a31",
|
||||
"filter_url_table_header": "\u904e\u6ffe\u5668\u7db2\u5740",
|
||||
"rules_count_table_header": "\u898f\u5247\u7e3d\u6578",
|
||||
"last_time_updated_table_header": "\u6700\u8fd1\u7684\u66f4\u65b0\u6642\u9593",
|
||||
"actions_table_header": "\u884c\u52d5",
|
||||
"delete_table_action": "\u522a\u9664",
|
||||
"filters_and_hosts": "\u904e\u6ffe\u5668\u548c\u4e3b\u6a5f\u5c01\u9396\u6e05\u55ae",
|
||||
"filters_and_hosts_hint": "AdGuard Home\u61c2\u5f97\u57fa\u672c\u7684\u5ee3\u544a\u5c01\u9396\u898f\u5247\u548c\u4e3b\u6a5f\u6a94\u6848\u8a9e\u6cd5\u3002",
|
||||
"no_filters_added": "\u7121\u5df2\u52a0\u5165\u7684\u904e\u6ffe\u5668",
|
||||
"add_filter_btn": "\u589e\u52a0\u904e\u6ffe\u5668",
|
||||
"cancel_btn": "\u53d6\u6d88",
|
||||
"enter_name_hint": "\u8f38\u5165\u540d\u7a31",
|
||||
"enter_url_hint": "\u8f38\u5165\u7db2\u5740",
|
||||
"check_updates_btn": "\u6aa2\u67e5\u66f4\u65b0",
|
||||
"new_filter_btn": "\u65b0\u7684\u904e\u6ffe\u5668\u8a02\u95b1",
|
||||
"enter_valid_filter_url": "\u8f38\u5165\u95dc\u65bc\u904e\u6ffe\u5668\u8a02\u95b1\u6216\u4e3b\u6a5f\u6a94\u6848\u4e4b\u6709\u6548\u7684\u7db2\u5740\u3002",
|
||||
"custom_filter_rules": "\u81ea\u8a02\u7684\u904e\u6ffe\u898f\u5247",
|
||||
"custom_filter_rules_hint": "\u65bc\u4e00\u884c\u4e0a\u8f38\u5165\u4e00\u500b\u898f\u5247\u3002\u60a8\u53ef\u4f7f\u7528\u5ee3\u544a\u5c01\u9396\u898f\u5247\u6216\u4e3b\u6a5f\u6a94\u6848\u8a9e\u6cd5\u3002",
|
||||
"examples_title": "\u7bc4\u4f8b",
|
||||
"example_meaning_filter_block": "\u5c01\u9396\u81f3example.org\u7db2\u57df\u53ca\u5176\u6240\u6709\u7684\u5b50\u7db2\u57df\u4e4b\u5b58\u53d6",
|
||||
"example_meaning_filter_whitelist": "\u89e3\u9664\u5c01\u9396\u81f3example.org\u7db2\u57df\u53ca\u5176\u6240\u6709\u7684\u5b50\u7db2\u57df\u4e4b\u5b58\u53d6",
|
||||
"example_meaning_host_block": "AdGuard Home\u73fe\u5728\u5c07\u5c0dexample.org\u7db2\u57df\u8fd4\u56de127.0.0.1\u4f4d\u5740\uff08\u4f46\u975e\u5176\u5b50\u7db2\u57df\uff09\u3002",
|
||||
"example_comment": "! \u770b\uff0c\u4e00\u500b\u8a3b\u89e3",
|
||||
"example_comment_meaning": "\u53ea\u662f\u4e00\u500b\u8a3b\u89e3",
|
||||
"example_comment_hash": "# \u4e5f\u662f\u4e00\u500b\u8a3b\u89e3",
|
||||
"example_regex_meaning": "\u5c01\u9396\u81f3\u8207\u5df2\u660e\u78ba\u6307\u5b9a\u7684\u898f\u5247\u904b\u7b97\u5f0f\uff08Regular Expression\uff09\u76f8\u914d\u7684\u7db2\u57df\u4e4b\u5b58\u53d6",
|
||||
"example_upstream_regular": "\u4e00\u822c\u7684 DNS\uff08\u900f\u904eUDP\uff09",
|
||||
"example_upstream_dot": "\u52a0\u5bc6\u7684 <a href='https:\/\/en.wikipedia.org\/wiki\/DNS_over_TLS' target='_blank'>DNS-over-TLS<\/a>",
|
||||
"example_upstream_doh": "\u52a0\u5bc6\u7684 <a href='https:\/\/en.wikipedia.org\/wiki\/DNS_over_HTTPS' target='_blank'>DNS-over-HTTPS <\/a>",
|
||||
"example_upstream_sdns": "\u60a8\u53ef\u4f7f\u7528\u95dc\u65bc <a href='https:\/\/dnscrypt.info\/' target='_blank'>DNSCrypt<\/a> \u6216 <a href='https:\/\/en.wikipedia.org\/wiki\/DNS_over_HTTPS' target='_blank'>DNS-over-HTTPS<\/a> \u89e3\u6790\u5668\u4e4b <a href='https:\/\/dnscrypt.info\/stamps\/' target='_blank'>DNS \u6233\u8a18<\/a>",
|
||||
"example_upstream_tcp": "\u4e00\u822c\u7684 DNS\uff08\u900f\u904eTCP\uff09",
|
||||
"all_filters_up_to_date_toast": "\u6240\u6709\u7684\u904e\u6ffe\u5668\u5df2\u662f\u6700\u65b0\u7684",
|
||||
"updated_upstream_dns_toast": "\u5df2\u66f4\u65b0\u4e0a\u6e38\u7684DNS\u4f3a\u670d\u5668",
|
||||
"dns_test_ok_toast": "\u5df2\u660e\u78ba\u6307\u5b9a\u7684DNS\u4f3a\u670d\u5668\u6b63\u5728\u6b63\u78ba\u5730\u904b\u4f5c",
|
||||
"dns_test_not_ok_toast": "\u4f3a\u670d\u5668 \"{{key}}\"\uff1a\u7121\u6cd5\u88ab\u4f7f\u7528\uff0c\u8acb\u6aa2\u67e5\u60a8\u5df2\u6b63\u78ba\u5730\u586b\u5beb\u5b83",
|
||||
"unblock_btn": "\u89e3\u9664\u5c01\u9396",
|
||||
"block_btn": "\u5c01\u9396",
|
||||
"time_table_header": "\u6642\u9593",
|
||||
"domain_name_table_header": "\u57df\u540d",
|
||||
"type_table_header": "\u985e\u578b",
|
||||
"response_table_header": "\u56de\u61c9",
|
||||
"client_table_header": "\u7528\u6236\u7aef",
|
||||
"empty_response_status": "\u7a7a\u767d\u7684",
|
||||
"show_all_filter_type": "\u986f\u793a\u5168\u90e8",
|
||||
"show_filtered_type": "\u986f\u793a\u5df2\u904e\u6ffe\u7684",
|
||||
"no_logs_found": "\u7121\u5df2\u767c\u73fe\u4e4b\u8a18\u9304",
|
||||
"disabled_log_btn": "\u7981\u7528\u8a18\u9304",
|
||||
"download_log_file_btn": "\u4e0b\u8f09\u8a18\u9304\u6a94\u6848",
|
||||
"refresh_btn": "\u91cd\u65b0\u6574\u7406",
|
||||
"enabled_log_btn": "\u555f\u7528\u8a18\u9304",
|
||||
"last_dns_queries": "\u6700\u8fd1\u76845000\u7b46DNS\u67e5\u8a62",
|
||||
"previous_btn": "\u4e0a\u4e00\u9801",
|
||||
"next_btn": "\u4e0b\u4e00\u9801",
|
||||
"loading_table_status": "\u6b63\u5728\u8f09\u5165...",
|
||||
"page_table_footer_text": "\u9801\u9762",
|
||||
"of_table_footer_text": "\u4e4b",
|
||||
"rows_table_footer_text": "\u5217",
|
||||
"updated_custom_filtering_toast": "\u5df2\u66f4\u65b0\u81ea\u8a02\u7684\u904e\u6ffe\u898f\u5247",
|
||||
"rule_removed_from_custom_filtering_toast": "\u898f\u5247\u5f9e\u81ea\u8a02\u7684\u904e\u6ffe\u898f\u5247\u4e2d\u88ab\u79fb\u9664",
|
||||
"rule_added_to_custom_filtering_toast": "\u898f\u5247\u88ab\u52a0\u81f3\u81ea\u8a02\u7684\u904e\u6ffe\u898f\u5247\u4e2d",
|
||||
"query_log_disabled_toast": "\u67e5\u8a62\u8a18\u9304\u88ab\u7981\u7528",
|
||||
"query_log_enabled_toast": "\u67e5\u8a62\u8a18\u9304\u88ab\u555f\u7528",
|
||||
"source_label": "\u4f86\u6e90",
|
||||
"found_in_known_domain_db": "\u5728\u5df2\u77e5\u7684\u57df\u540d\u8cc7\u6599\u5eab\u4e2d\u88ab\u767c\u73fe\u3002",
|
||||
"category_label": "\u985e\u5225",
|
||||
"rule_label": "\u898f\u5247",
|
||||
"filter_label": "\u904e\u6ffe\u5668",
|
||||
"unknown_filter": "\u672a\u77e5\u7684\u904e\u6ffe\u5668 {{filterId}}",
|
||||
"install_welcome_title": "\u6b61\u8fce\u81f3AdGuard Home\uff01",
|
||||
"install_welcome_desc": "AdGuard Home\u662f\u5168\u7db2\u8def\u7bc4\u570d\u5ee3\u544a\u548c\u8ffd\u8e64\u5668\u5c01\u9396\u7684DNS\u4f3a\u670d\u5668\u3002\u5b83\u7684\u76ee\u7684\u70ba\u8b93\u60a8\u63a7\u5236\u60a8\u6574\u500b\u7684\u7db2\u8def\u548c\u6240\u6709\u60a8\u7684\u88dd\u7f6e\uff0c\u4e14\u4e0d\u9700\u8981\u4f7f\u7528\u7528\u6236\u7aef\u7a0b\u5f0f\u3002",
|
||||
"install_settings_title": "\u7ba1\u7406\u54e1\u7db2\u9801\u4ecb\u9762",
|
||||
"install_settings_listen": "\u76e3\u807d\u4ecb\u9762",
|
||||
"install_settings_port": "\u9023\u63a5\u57e0",
|
||||
"install_settings_interface_link": "\u60a8\u7684AdGuard Home\u7ba1\u7406\u54e1\u7db2\u9801\u4ecb\u9762\u5c07\u65bc\u4e0b\u5217\u7684\u4f4d\u5740\u4e0a\u70ba\u53ef\u7528\u7684\uff1a",
|
||||
"form_error_port": "\u8f38\u5165\u6709\u6548\u7684\u9023\u63a5\u57e0\u503c",
|
||||
"install_settings_dns": "DNS \u4f3a\u670d\u5668",
|
||||
"install_settings_dns_desc": "\u60a8\u5c07\u9700\u8981\u914d\u7f6e\u60a8\u7684\u88dd\u7f6e\u6216\u8def\u7531\u5668\u4ee5\u4f7f\u7528\u65bc\u4e0b\u5217\u7684\u4f4d\u5740\u4e0a\u4e4bDNS\u4f3a\u670d\u5668\uff1a",
|
||||
"install_settings_all_interfaces": "\u6240\u6709\u7684\u4ecb\u9762",
|
||||
"install_auth_title": "\u9a57\u8b49",
|
||||
"install_auth_desc": "\u88ab\u975e\u5e38\u5efa\u8b70\u914d\u7f6e\u5c6c\u65bc\u60a8\u7684AdGuard Home\u7ba1\u7406\u54e1\u7db2\u9801\u4ecb\u9762\u4e4b\u5bc6\u78bc\u9a57\u8b49\u3002\u5373\u4f7f\u5b83\u50c5\u5728\u60a8\u7684\u5340\u57df\u7db2\u8def\u4e2d\u70ba\u53ef\u5b58\u53d6\u7684\uff0c\u8b93\u5b83\u53d7\u4fdd\u8b77\u514d\u65bc\u4e0d\u53d7\u9650\u5236\u7684\u5b58\u53d6\u70ba\u4ecd\u7136\u91cd\u8981\u7684\u3002",
|
||||
"install_auth_username": "\u7528\u6236\u540d",
|
||||
"install_auth_password": "\u5bc6\u78bc",
|
||||
"install_auth_confirm": "\u78ba\u8a8d\u5bc6\u78bc",
|
||||
"install_auth_username_enter": "\u8f38\u5165\u7528\u6236\u540d",
|
||||
"install_auth_password_enter": "\u8f38\u5165\u5bc6\u78bc",
|
||||
"install_step": "\u6b65\u9a5f",
|
||||
"install_devices_title": "\u914d\u7f6e\u60a8\u7684\u88dd\u7f6e",
|
||||
"install_devices_desc": "\u70ba\u4f7fAdGuard Home\u958b\u59cb\u904b\u4f5c\uff0c\u60a8\u9700\u8981\u914d\u7f6e\u60a8\u7684\u88dd\u7f6e\u4ee5\u4f7f\u7528\u5b83\u3002",
|
||||
"install_submit_title": "\u606d\u559c\uff01",
|
||||
"install_submit_desc": "\u8a72\u8a2d\u7f6e\u7a0b\u5e8f\u88ab\u5b8c\u6210\uff0c\u4e14\u60a8\u6e96\u5099\u597d\u958b\u59cb\u4f7f\u7528AdGuard Home\u3002",
|
||||
"install_devices_router": "\u8def\u7531\u5668",
|
||||
"install_devices_router_desc": "\u8a72\u8a2d\u7f6e\u5c07\u81ea\u52d5\u5730\u6db5\u84cb\u88ab\u9023\u7dda\u81f3\u60a8\u7684\u5bb6\u5ead\u8def\u7531\u5668\u4e4b\u6240\u6709\u7684\u88dd\u7f6e\uff0c\u4e14\u60a8\u5c07\u7121\u9700\u624b\u52d5\u5730\u914d\u7f6e\u5b83\u5011\u6bcf\u500b\u3002",
|
||||
"install_devices_address": "AdGuard Home DNS\u4f3a\u670d\u5668\u6b63\u5728\u76e3\u807d\u4e0b\u5217\u7684\u4f4d\u5740",
|
||||
"install_devices_router_list_1": "\u958b\u555f\u95dc\u65bc\u60a8\u7684\u8def\u7531\u5668\u4e4b\u504f\u597d\u8a2d\u5b9a\u3002\u901a\u5e38\u5730\uff0c\u60a8\u53ef\u900f\u904e\u7db2\u5740\uff08\u5982 http:\/\/192.168.0.1\/ \u6216 http:\/\/192.168.1.1\/\uff09\u5f9e\u60a8\u7684\u700f\u89bd\u5668\u4e2d\u5b58\u53d6\u5b83\u3002\u60a8\u53ef\u80fd\u88ab\u8981\u6c42\u8f38\u5165\u8a72\u5bc6\u78bc\u3002\u5982\u679c\u60a8\u4e0d\u8a18\u5f97\u5b83\uff0c\u60a8\u7d93\u5e38\u53ef\u900f\u904e\u6309\u58d3\u65bc\u8a72\u8def\u7531\u5668\u672c\u8eab\u4e0a\u7684\u6309\u9215\u4f86\u91cd\u7f6e\u5bc6\u78bc\u3002\u67d0\u4e9b\u8def\u7531\u5668\u9700\u8981\u7279\u5b9a\u7684\u61c9\u7528\u7a0b\u5f0f\uff0c\u65e2\u7136\u5982\u6b64\u5176\u61c9\u5df2\u88ab\u5b89\u88dd\u65bc\u60a8\u7684\u96fb\u8166\/\u624b\u6a5f\u4e0a\u3002",
|
||||
"install_devices_router_list_2": "\u627e\u5230DHCP\/DNS\u8a2d\u5b9a\u3002\u5c0b\u627e\u7dca\u9130\u8457\u5141\u8a31\u5169\u7d44\u6216\u4e09\u7d44\u6578\u5b57\u96c6\u7684\u6b04\u4f4d\u4e4bDNS\u5b57\u6bcd\uff0c\u6bcf\u7d44\u88ab\u62c6\u6210\u56db\u500b\u542b\u6709\u4e00\u81f3\u4e09\u500b\u6578\u5b57\u7684\u7fa4\u96c6\u3002",
|
||||
"install_devices_router_list_3": "\u5728\u90a3\u88e1\u8f38\u5165\u60a8\u7684AdGuard Home\u4f3a\u670d\u5668\u4f4d\u5740\u3002",
|
||||
"install_devices_windows_list_1": "\u901a\u904e\u958b\u59cb\u529f\u80fd\u8868\u6216Windows \u641c\u5c0b\uff0c\u958b\u555f\u63a7\u5236\u53f0\u3002",
|
||||
"install_devices_windows_list_2": "\u53bb\u7db2\u8def\u548c\u7db2\u969b\u7db2\u8def\u985e\u5225\uff0c\u7136\u5f8c\u53bb\u7db2\u8def\u548c\u5171\u7528\u4e2d\u5fc3\u3002",
|
||||
"install_devices_windows_list_3": "\u65bc\u756b\u9762\u4e4b\u5de6\u5074\u4e0a\u627e\u5230\u8b8a\u66f4\u4ecb\u9762\u5361\u8a2d\u5b9a\u4e26\u65bc\u5b83\u4e0a\u9ede\u64ca\u3002",
|
||||
"install_devices_windows_list_4": "\u9078\u64c7\u60a8\u73fe\u884c\u7684\u9023\u7dda\uff0c\u65bc\u5b83\u4e0a\u9ede\u64ca\u6ed1\u9f20\u53f3\u9375\uff0c\u7136\u5f8c\u9078\u64c7\u5167\u5bb9\u3002",
|
||||
"install_devices_windows_list_5": "\u5728\u6e05\u55ae\u4e2d\u627e\u5230\u7db2\u969b\u7db2\u8def\u901a\u8a0a\u5354\u5b9a\u7b2c 4 \u7248\uff08TCP\/IPv4\uff09\uff0c\u9078\u64c7\u5b83\uff0c\u7136\u5f8c\u518d\u6b21\u65bc\u5167\u5bb9\u4e0a\u9ede\u64ca\u3002",
|
||||
"install_devices_windows_list_6": "\u9078\u64c7\u4f7f\u7528\u4e0b\u5217\u7684DNS\u4f3a\u670d\u5668\u4f4d\u5740\uff0c\u7136\u5f8c\u8f38\u5165\u60a8\u7684AdGuard Home\u4f3a\u670d\u5668\u4f4d\u5740\u3002",
|
||||
"install_devices_macos_list_1": "\u65bcApple\u5716\u50cf\u4e0a\u9ede\u64ca\uff0c\u7136\u5f8c\u53bb\u7cfb\u7d71\u504f\u597d\u8a2d\u5b9a\u3002",
|
||||
"install_devices_macos_list_2": "\u65bc\u7db2\u8def\u4e0a\u9ede\u64ca\u3002",
|
||||
"install_devices_macos_list_3": "\u9078\u64c7\u5728\u60a8\u7684\u6e05\u55ae\u4e2d\u4e4b\u9996\u8981\u7684\u9023\u7dda\uff0c\u7136\u5f8c\u9ede\u64ca\u9032\u968e\u7684\u3002",
|
||||
"install_devices_macos_list_4": "\u9078\u64c7\u8a72DNS\u5206\u9801\uff0c\u7136\u5f8c\u8f38\u5165\u60a8\u7684AdGuard Home\u4f3a\u670d\u5668\u4f4d\u5740\u3002",
|
||||
"install_devices_android_list_1": "\u5f9eAndroid\u9078\u55ae\u4e3b\u756b\u9762\u4e2d\uff0c\u8f15\u89f8\u8a2d\u5b9a\u3002",
|
||||
"install_devices_android_list_2": "\u65bc\u8a72\u9078\u55ae\u4e0a\u8f15\u89f8Wi-Fi\u3002\u6b63\u5728\u5217\u51fa\u6240\u6709\u53ef\u7528\u7684\u7db2\u8def\u4e4b\u756b\u9762\u5c07\u88ab\u986f\u793a\uff08\u4e0d\u53ef\u80fd\u70ba\u884c\u52d5\u9023\u7dda\u8a2d\u5b9a\u81ea\u8a02\u7684DNS\uff09\u3002",
|
||||
"install_devices_android_list_3": "\u9577\u6309\u60a8\u6240\u9023\u7dda\u81f3\u7684\u7db2\u8def\uff0c\u7136\u5f8c\u8f15\u89f8\u4fee\u6539\u7db2\u8def\u3002",
|
||||
"install_devices_android_list_4": "\u65bc\u67d0\u4e9b\u88dd\u7f6e\u4e0a\uff0c\u60a8\u53ef\u80fd\u9700\u8981\u6aa2\u67e5\u95dc\u65bc\u9032\u968e\u7684\u65b9\u6846\u4ee5\u67e5\u770b\u9032\u4e00\u6b65\u7684\u8a2d\u5b9a\u3002\u70ba\u4e86\u8abf\u6574\u60a8\u7684Android DNS\u8a2d\u5b9a\uff0c\u60a8\u5c07\u9700\u8981\u628aIP \u8a2d\u5b9a\u5f9eDHCP\u8f49\u63db\u6210\u975c\u614b\u3002",
|
||||
"install_devices_android_list_5": "\u4f7f\u8a2d\u5b9aDNS 1\u548cDNS 2\u503c\u66f4\u6539\u6210\u60a8\u7684AdGuard Home\u4f3a\u670d\u5668\u4f4d\u5740\u3002",
|
||||
"install_devices_ios_list_1": "\u5f9e\u4e3b\u756b\u9762\u4e2d\uff0c\u8f15\u89f8\u8a2d\u5b9a\u3002",
|
||||
"install_devices_ios_list_2": "\u5728\u5de6\u5074\u7684\u9078\u55ae\u4e2d\u9078\u64c7Wi-Fi\uff08\u4e0d\u53ef\u80fd\u70ba\u884c\u52d5\u7db2\u8def\u914d\u7f6eDNS\uff09\u3002",
|
||||
"install_devices_ios_list_3": "\u65bc\u76ee\u524d\u73fe\u884c\u7684\u7db2\u8def\u4e4b\u540d\u7a31\u4e0a\u8f15\u89f8\u3002",
|
||||
"install_devices_ios_list_4": "\u5728\u8a72DNS\u6b04\u4f4d\u4e2d\uff0c\u8f38\u5165\u60a8\u7684AdGuard Home\u4f3a\u670d\u5668\u4f4d\u5740\u3002",
|
||||
"get_started": "\u958b\u59cb\u5427",
|
||||
"next": "\u4e0b\u4e00\u6b65",
|
||||
"open_dashboard": "\u958b\u555f\u5100\u8868\u677f",
|
||||
"install_saved": "\u5df2\u6210\u529f\u5730\u5132\u5b58",
|
||||
"form_error_password": "\u4e0d\u76f8\u7b26\u7684\u5bc6\u78bc"
|
||||
}
|
||||
73
client/src/actions/encryption.js
Normal file
73
client/src/actions/encryption.js
Normal file
@@ -0,0 +1,73 @@
|
||||
import { createAction } from 'redux-actions';
|
||||
import Api from '../api/Api';
|
||||
import { addErrorToast, addSuccessToast } from './index';
|
||||
import { redirectToCurrentProtocol } from '../helpers/helpers';
|
||||
|
||||
const apiClient = new Api();
|
||||
|
||||
export const getTlsStatusRequest = createAction('GET_TLS_STATUS_REQUEST');
|
||||
export const getTlsStatusFailure = createAction('GET_TLS_STATUS_FAILURE');
|
||||
export const getTlsStatusSuccess = createAction('GET_TLS_STATUS_SUCCESS');
|
||||
|
||||
export const getTlsStatus = () => async (dispatch) => {
|
||||
dispatch(getTlsStatusRequest());
|
||||
try {
|
||||
const status = await apiClient.getTlsStatus();
|
||||
status.certificate_chain = atob(status.certificate_chain);
|
||||
status.private_key = atob(status.private_key);
|
||||
|
||||
dispatch(getTlsStatusSuccess(status));
|
||||
} catch (error) {
|
||||
dispatch(addErrorToast({ error }));
|
||||
dispatch(getTlsStatusFailure());
|
||||
}
|
||||
};
|
||||
|
||||
export const setTlsConfigRequest = createAction('SET_TLS_CONFIG_REQUEST');
|
||||
export const setTlsConfigFailure = createAction('SET_TLS_CONFIG_FAILURE');
|
||||
export const setTlsConfigSuccess = createAction('SET_TLS_CONFIG_SUCCESS');
|
||||
|
||||
export const setTlsConfig = config => async (dispatch, getState) => {
|
||||
dispatch(setTlsConfigRequest());
|
||||
try {
|
||||
const { httpPort } = getState().dashboard;
|
||||
const values = { ...config };
|
||||
values.certificate_chain = btoa(values.certificate_chain);
|
||||
values.private_key = btoa(values.private_key);
|
||||
values.port_https = values.port_https || 0;
|
||||
values.port_dns_over_tls = values.port_dns_over_tls || 0;
|
||||
|
||||
const response = await apiClient.setTlsConfig(values);
|
||||
response.certificate_chain = atob(response.certificate_chain);
|
||||
response.private_key = atob(response.private_key);
|
||||
dispatch(setTlsConfigSuccess(response));
|
||||
dispatch(addSuccessToast('encryption_config_saved'));
|
||||
redirectToCurrentProtocol(response, httpPort);
|
||||
} catch (error) {
|
||||
dispatch(addErrorToast({ error }));
|
||||
dispatch(setTlsConfigFailure());
|
||||
}
|
||||
};
|
||||
|
||||
export const validateTlsConfigRequest = createAction('VALIDATE_TLS_CONFIG_REQUEST');
|
||||
export const validateTlsConfigFailure = createAction('VALIDATE_TLS_CONFIG_FAILURE');
|
||||
export const validateTlsConfigSuccess = createAction('VALIDATE_TLS_CONFIG_SUCCESS');
|
||||
|
||||
export const validateTlsConfig = config => async (dispatch) => {
|
||||
dispatch(validateTlsConfigRequest());
|
||||
try {
|
||||
const values = { ...config };
|
||||
values.certificate_chain = btoa(values.certificate_chain);
|
||||
values.private_key = btoa(values.private_key);
|
||||
values.port_https = values.port_https || 0;
|
||||
values.port_dns_over_tls = values.port_dns_over_tls || 0;
|
||||
|
||||
const response = await apiClient.validateTlsConfig(values);
|
||||
response.certificate_chain = atob(response.certificate_chain);
|
||||
response.private_key = atob(response.private_key);
|
||||
dispatch(validateTlsConfigSuccess(response));
|
||||
} catch (error) {
|
||||
dispatch(addErrorToast({ error }));
|
||||
dispatch(validateTlsConfigFailure());
|
||||
}
|
||||
};
|
||||
@@ -4,6 +4,7 @@ import { t } from 'i18next';
|
||||
import { showLoading, hideLoading } from 'react-redux-loading-bar';
|
||||
|
||||
import { normalizeHistory, normalizeFilteringStatus, normalizeLogs } from '../helpers/helpers';
|
||||
import { SETTINGS_NAMES } from '../helpers/constants';
|
||||
import Api from '../api/Api';
|
||||
|
||||
const apiClient = new Api();
|
||||
@@ -18,9 +19,8 @@ export const showSettingsFailure = createAction('SETTINGS_FAILURE_SHOW');
|
||||
export const toggleSetting = (settingKey, status) => async (dispatch) => {
|
||||
let successMessage = '';
|
||||
try {
|
||||
// TODO move setting keys to constants
|
||||
switch (settingKey) {
|
||||
case 'filtering':
|
||||
case SETTINGS_NAMES.filtering:
|
||||
if (status) {
|
||||
successMessage = 'disabled_filtering_toast';
|
||||
await apiClient.disableFiltering();
|
||||
@@ -30,7 +30,7 @@ export const toggleSetting = (settingKey, status) => async (dispatch) => {
|
||||
}
|
||||
dispatch(toggleSettingStatus({ settingKey }));
|
||||
break;
|
||||
case 'safebrowsing':
|
||||
case SETTINGS_NAMES.safebrowsing:
|
||||
if (status) {
|
||||
successMessage = 'disabled_safe_browsing_toast';
|
||||
await apiClient.disableSafebrowsing();
|
||||
@@ -40,7 +40,7 @@ export const toggleSetting = (settingKey, status) => async (dispatch) => {
|
||||
}
|
||||
dispatch(toggleSettingStatus({ settingKey }));
|
||||
break;
|
||||
case 'parental':
|
||||
case SETTINGS_NAMES.parental:
|
||||
if (status) {
|
||||
successMessage = 'disabled_parental_toast';
|
||||
await apiClient.disableParentalControl();
|
||||
@@ -50,7 +50,7 @@ export const toggleSetting = (settingKey, status) => async (dispatch) => {
|
||||
}
|
||||
dispatch(toggleSettingStatus({ settingKey }));
|
||||
break;
|
||||
case 'safesearch':
|
||||
case SETTINGS_NAMES.safesearch:
|
||||
if (status) {
|
||||
successMessage = 'disabled_safe_search_toast';
|
||||
await apiClient.disableSafesearch();
|
||||
@@ -352,11 +352,11 @@ export const refreshFiltersFailure = createAction('FILTERING_REFRESH_FAILURE');
|
||||
export const refreshFiltersSuccess = createAction('FILTERING_REFRESH_SUCCESS');
|
||||
|
||||
export const refreshFilters = () => async (dispatch) => {
|
||||
dispatch(refreshFiltersRequest);
|
||||
dispatch(refreshFiltersRequest());
|
||||
dispatch(showLoading());
|
||||
try {
|
||||
const refreshText = await apiClient.refreshFilters();
|
||||
dispatch(refreshFiltersSuccess);
|
||||
dispatch(refreshFiltersSuccess());
|
||||
|
||||
if (refreshText.includes('OK')) {
|
||||
if (refreshText.includes('OK 0')) {
|
||||
@@ -434,7 +434,6 @@ export const downloadQueryLogRequest = createAction('DOWNLOAD_QUERY_LOG_REQUEST'
|
||||
export const downloadQueryLogFailure = createAction('DOWNLOAD_QUERY_LOG_FAILURE');
|
||||
export const downloadQueryLogSuccess = createAction('DOWNLOAD_QUERY_LOG_SUCCESS');
|
||||
|
||||
// TODO create some common flasher with all server errors
|
||||
export const downloadQueryLog = () => async (dispatch) => {
|
||||
let data;
|
||||
dispatch(downloadQueryLogRequest());
|
||||
@@ -522,3 +521,132 @@ export const getLanguage = () => async (dispatch) => {
|
||||
dispatch(getLanguageFailure());
|
||||
}
|
||||
};
|
||||
|
||||
export const getDhcpStatusRequest = createAction('GET_DHCP_STATUS_REQUEST');
|
||||
export const getDhcpStatusSuccess = createAction('GET_DHCP_STATUS_SUCCESS');
|
||||
export const getDhcpStatusFailure = createAction('GET_DHCP_STATUS_FAILURE');
|
||||
|
||||
export const getDhcpStatus = () => async (dispatch) => {
|
||||
dispatch(getDhcpStatusRequest());
|
||||
try {
|
||||
const status = await apiClient.getDhcpStatus();
|
||||
dispatch(getDhcpStatusSuccess(status));
|
||||
} catch (error) {
|
||||
dispatch(addErrorToast({ error }));
|
||||
dispatch(getDhcpStatusFailure());
|
||||
}
|
||||
};
|
||||
|
||||
export const getDhcpInterfacesRequest = createAction('GET_DHCP_INTERFACES_REQUEST');
|
||||
export const getDhcpInterfacesSuccess = createAction('GET_DHCP_INTERFACES_SUCCESS');
|
||||
export const getDhcpInterfacesFailure = createAction('GET_DHCP_INTERFACES_FAILURE');
|
||||
|
||||
export const getDhcpInterfaces = () => async (dispatch) => {
|
||||
dispatch(getDhcpInterfacesRequest());
|
||||
try {
|
||||
const interfaces = await apiClient.getDhcpInterfaces();
|
||||
dispatch(getDhcpInterfacesSuccess(interfaces));
|
||||
} catch (error) {
|
||||
dispatch(addErrorToast({ error }));
|
||||
dispatch(getDhcpInterfacesFailure());
|
||||
}
|
||||
};
|
||||
|
||||
export const findActiveDhcpRequest = createAction('FIND_ACTIVE_DHCP_REQUEST');
|
||||
export const findActiveDhcpSuccess = createAction('FIND_ACTIVE_DHCP_SUCCESS');
|
||||
export const findActiveDhcpFailure = createAction('FIND_ACTIVE_DHCP_FAILURE');
|
||||
|
||||
export const findActiveDhcp = name => async (dispatch) => {
|
||||
dispatch(findActiveDhcpRequest());
|
||||
try {
|
||||
const activeDhcp = await apiClient.findActiveDhcp(name);
|
||||
dispatch(findActiveDhcpSuccess(activeDhcp));
|
||||
} catch (error) {
|
||||
dispatch(addErrorToast({ error }));
|
||||
dispatch(findActiveDhcpFailure());
|
||||
}
|
||||
};
|
||||
|
||||
export const setDhcpConfigRequest = createAction('SET_DHCP_CONFIG_REQUEST');
|
||||
export const setDhcpConfigSuccess = createAction('SET_DHCP_CONFIG_SUCCESS');
|
||||
export const setDhcpConfigFailure = createAction('SET_DHCP_CONFIG_FAILURE');
|
||||
|
||||
// TODO rewrite findActiveDhcp part
|
||||
export const setDhcpConfig = values => async (dispatch, getState) => {
|
||||
const { config } = getState().dhcp;
|
||||
const updatedConfig = { ...config, ...values };
|
||||
dispatch(setDhcpConfigRequest());
|
||||
if (values.interface_name) {
|
||||
dispatch(findActiveDhcpRequest());
|
||||
try {
|
||||
const activeDhcp = await apiClient.findActiveDhcp(values.interface_name);
|
||||
dispatch(findActiveDhcpSuccess(activeDhcp));
|
||||
if (!activeDhcp.found) {
|
||||
try {
|
||||
await apiClient.setDhcpConfig(updatedConfig);
|
||||
dispatch(setDhcpConfigSuccess(updatedConfig));
|
||||
dispatch(addSuccessToast('dhcp_config_saved'));
|
||||
} catch (error) {
|
||||
dispatch(addErrorToast({ error }));
|
||||
dispatch(setDhcpConfigFailure());
|
||||
}
|
||||
} else {
|
||||
dispatch(addErrorToast({ error: 'dhcp_found' }));
|
||||
}
|
||||
} catch (error) {
|
||||
dispatch(addErrorToast({ error }));
|
||||
dispatch(findActiveDhcpFailure());
|
||||
}
|
||||
} else {
|
||||
try {
|
||||
await apiClient.setDhcpConfig(updatedConfig);
|
||||
dispatch(setDhcpConfigSuccess(updatedConfig));
|
||||
dispatch(addSuccessToast('dhcp_config_saved'));
|
||||
} catch (error) {
|
||||
dispatch(addErrorToast({ error }));
|
||||
dispatch(setDhcpConfigFailure());
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export const toggleDhcpRequest = createAction('TOGGLE_DHCP_REQUEST');
|
||||
export const toggleDhcpFailure = createAction('TOGGLE_DHCP_FAILURE');
|
||||
export const toggleDhcpSuccess = createAction('TOGGLE_DHCP_SUCCESS');
|
||||
|
||||
// TODO rewrite findActiveDhcp part
|
||||
export const toggleDhcp = config => async (dispatch) => {
|
||||
dispatch(toggleDhcpRequest());
|
||||
|
||||
if (config.enabled) {
|
||||
try {
|
||||
await apiClient.setDhcpConfig({ ...config, enabled: false });
|
||||
dispatch(toggleDhcpSuccess());
|
||||
dispatch(addSuccessToast('disabled_dhcp'));
|
||||
} catch (error) {
|
||||
dispatch(addErrorToast({ error }));
|
||||
dispatch(toggleDhcpFailure());
|
||||
}
|
||||
} else {
|
||||
dispatch(findActiveDhcpRequest());
|
||||
try {
|
||||
const activeDhcp = await apiClient.findActiveDhcp(config.interface_name);
|
||||
dispatch(findActiveDhcpSuccess(activeDhcp));
|
||||
|
||||
if (!activeDhcp.found) {
|
||||
try {
|
||||
await apiClient.setDhcpConfig({ ...config, enabled: true });
|
||||
dispatch(toggleDhcpSuccess());
|
||||
dispatch(addSuccessToast('enabled_dhcp'));
|
||||
} catch (error) {
|
||||
dispatch(addErrorToast({ error }));
|
||||
dispatch(toggleDhcpFailure());
|
||||
}
|
||||
} else {
|
||||
dispatch(addErrorToast({ error: 'dhcp_found' }));
|
||||
}
|
||||
} catch (error) {
|
||||
dispatch(addErrorToast({ error }));
|
||||
dispatch(findActiveDhcpFailure());
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
46
client/src/actions/install.js
Normal file
46
client/src/actions/install.js
Normal file
@@ -0,0 +1,46 @@
|
||||
import { createAction } from 'redux-actions';
|
||||
import Api from '../api/Api';
|
||||
import { addErrorToast, addSuccessToast } from './index';
|
||||
|
||||
const apiClient = new Api();
|
||||
|
||||
export const nextStep = createAction('NEXT_STEP');
|
||||
export const prevStep = createAction('PREV_STEP');
|
||||
|
||||
export const getDefaultAddressesRequest = createAction('GET_DEFAULT_ADDRESSES_REQUEST');
|
||||
export const getDefaultAddressesFailure = createAction('GET_DEFAULT_ADDRESSES_FAILURE');
|
||||
export const getDefaultAddressesSuccess = createAction('GET_DEFAULT_ADDRESSES_SUCCESS');
|
||||
|
||||
export const getDefaultAddresses = () => async (dispatch) => {
|
||||
dispatch(getDefaultAddressesRequest());
|
||||
try {
|
||||
const addresses = await apiClient.getDefaultAddresses();
|
||||
dispatch(getDefaultAddressesSuccess(addresses));
|
||||
} catch (error) {
|
||||
dispatch(addErrorToast({ error }));
|
||||
dispatch(getDefaultAddressesFailure());
|
||||
}
|
||||
};
|
||||
|
||||
export const setAllSettingsRequest = createAction('SET_ALL_SETTINGS_REQUEST');
|
||||
export const setAllSettingsFailure = createAction('SET_ALL_SETTINGS_FAILURE');
|
||||
export const setAllSettingsSuccess = createAction('SET_ALL_SETTINGS_SUCCESS');
|
||||
|
||||
export const setAllSettings = values => async (dispatch) => {
|
||||
dispatch(setAllSettingsRequest());
|
||||
try {
|
||||
const {
|
||||
confirm_password,
|
||||
...config
|
||||
} = values;
|
||||
|
||||
await apiClient.setAllSettings(config);
|
||||
dispatch(setAllSettingsSuccess());
|
||||
dispatch(addSuccessToast('install_saved'));
|
||||
dispatch(nextStep());
|
||||
} catch (error) {
|
||||
dispatch(addErrorToast({ error }));
|
||||
dispatch(setAllSettingsFailure());
|
||||
dispatch(prevStep());
|
||||
}
|
||||
};
|
||||
@@ -15,7 +15,11 @@ export default class Api {
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
throw new Error(`${this.baseUrl}/${path} | ${error.response.data} | ${error.response.status}`);
|
||||
const errorPath = `${this.baseUrl}/${path}`;
|
||||
if (error.response) {
|
||||
throw new Error(`${errorPath} | ${error.response.data} | ${error.response.status}`);
|
||||
}
|
||||
throw new Error(`${errorPath} | ${error.message ? error.message : error}`);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -302,4 +306,84 @@ export default class Api {
|
||||
};
|
||||
return this.makeRequest(path, method, parameters);
|
||||
}
|
||||
|
||||
// DHCP
|
||||
DHCP_STATUS = { path: 'dhcp/status', method: 'GET' };
|
||||
DHCP_SET_CONFIG = { path: 'dhcp/set_config', method: 'POST' };
|
||||
DHCP_FIND_ACTIVE = { path: 'dhcp/find_active_dhcp', method: 'POST' };
|
||||
DHCP_INTERFACES = { path: 'dhcp/interfaces', method: 'GET' };
|
||||
|
||||
getDhcpStatus() {
|
||||
const { path, method } = this.DHCP_STATUS;
|
||||
return this.makeRequest(path, method);
|
||||
}
|
||||
|
||||
getDhcpInterfaces() {
|
||||
const { path, method } = this.DHCP_INTERFACES;
|
||||
return this.makeRequest(path, method);
|
||||
}
|
||||
|
||||
setDhcpConfig(config) {
|
||||
const { path, method } = this.DHCP_SET_CONFIG;
|
||||
const parameters = {
|
||||
data: config,
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
};
|
||||
return this.makeRequest(path, method, parameters);
|
||||
}
|
||||
|
||||
findActiveDhcp(name) {
|
||||
const { path, method } = this.DHCP_FIND_ACTIVE;
|
||||
const parameters = {
|
||||
data: name,
|
||||
headers: { 'Content-Type': 'text/plain' },
|
||||
};
|
||||
return this.makeRequest(path, method, parameters);
|
||||
}
|
||||
|
||||
// Installation
|
||||
INSTALL_GET_ADDRESSES = { path: 'install/get_addresses', method: 'GET' };
|
||||
INSTALL_CONFIGURE = { path: 'install/configure', method: 'POST' };
|
||||
|
||||
getDefaultAddresses() {
|
||||
const { path, method } = this.INSTALL_GET_ADDRESSES;
|
||||
return this.makeRequest(path, method);
|
||||
}
|
||||
|
||||
setAllSettings(config) {
|
||||
const { path, method } = this.INSTALL_CONFIGURE;
|
||||
const parameters = {
|
||||
data: config,
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
};
|
||||
return this.makeRequest(path, method, parameters);
|
||||
}
|
||||
|
||||
// DNS-over-HTTPS and DNS-over-TLS
|
||||
TLS_STATUS = { path: 'tls/status', method: 'GET' };
|
||||
TLS_CONFIG = { path: 'tls/configure', method: 'POST' };
|
||||
TLS_VALIDATE = { path: 'tls/validate', method: 'POST' };
|
||||
|
||||
getTlsStatus() {
|
||||
const { path, method } = this.TLS_STATUS;
|
||||
return this.makeRequest(path, method);
|
||||
}
|
||||
|
||||
setTlsConfig(config) {
|
||||
const { path, method } = this.TLS_CONFIG;
|
||||
const parameters = {
|
||||
data: config,
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
};
|
||||
return this.makeRequest(path, method, parameters);
|
||||
}
|
||||
|
||||
validateTlsConfig(config) {
|
||||
const { path, method } = this.TLS_VALIDATE;
|
||||
const parameters = {
|
||||
data: config,
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
};
|
||||
return this.makeRequest(path, method, parameters);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
font-family: sans-serif;
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Helvetica Neue", Arial, sans-serif;
|
||||
}
|
||||
|
||||
.status {
|
||||
@@ -19,8 +19,14 @@ body {
|
||||
}
|
||||
|
||||
.loading-bar {
|
||||
position: absolute;
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
z-index: 103;
|
||||
height: 3px;
|
||||
background: linear-gradient(45deg, rgba(99, 125, 120, 1) 0%, rgba(88, 177, 101, 1) 100%);
|
||||
}
|
||||
|
||||
.hidden {
|
||||
display: none;
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import React, { Component, Fragment } from 'react';
|
||||
import { HashRouter, Route } from 'react-router-dom';
|
||||
import PropTypes from 'prop-types';
|
||||
import { withNamespaces } from 'react-i18next';
|
||||
import LoadingBar from 'react-redux-loading-bar';
|
||||
|
||||
import 'react-table/react-table.css';
|
||||
@@ -16,7 +17,8 @@ import Logs from '../../containers/Logs';
|
||||
import Footer from '../ui/Footer';
|
||||
import Toasts from '../Toasts';
|
||||
import Status from '../ui/Status';
|
||||
import Update from '../ui/Update';
|
||||
import UpdateTopline from '../ui/UpdateTopline';
|
||||
import EncryptionTopline from '../ui/EncryptionTopline';
|
||||
import i18n from '../../i18n';
|
||||
|
||||
class App extends Component {
|
||||
@@ -50,7 +52,7 @@ class App extends Component {
|
||||
}
|
||||
|
||||
render() {
|
||||
const { dashboard } = this.props;
|
||||
const { dashboard, encryption } = this.props;
|
||||
const updateAvailable =
|
||||
!dashboard.processingVersions &&
|
||||
dashboard.isCoreRunning &&
|
||||
@@ -60,11 +62,14 @@ class App extends Component {
|
||||
<HashRouter hashType='noslash'>
|
||||
<Fragment>
|
||||
{updateAvailable &&
|
||||
<Update
|
||||
announcement={dashboard.announcement}
|
||||
announcementUrl={dashboard.announcementUrl}
|
||||
<UpdateTopline
|
||||
url={dashboard.announcementUrl}
|
||||
version={dashboard.version}
|
||||
/>
|
||||
}
|
||||
{!encryption.processing &&
|
||||
<EncryptionTopline notAfter={encryption.not_after} />
|
||||
}
|
||||
<LoadingBar className="loading-bar" updateTime={1000} />
|
||||
<Route component={Header} />
|
||||
<div className="container container--wrap">
|
||||
@@ -100,6 +105,7 @@ App.propTypes = {
|
||||
error: PropTypes.string,
|
||||
getVersion: PropTypes.func,
|
||||
changeLanguage: PropTypes.func,
|
||||
encryption: PropTypes.object,
|
||||
};
|
||||
|
||||
export default App;
|
||||
export default withNamespaces()(App);
|
||||
|
||||
@@ -24,7 +24,9 @@ const Counters = props => (
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<Trans>blocked_by</Trans> <a href="#filters"><Trans>filters</Trans></a>
|
||||
<a href="#filters">
|
||||
<Trans>blocked_by</Trans>
|
||||
</a>
|
||||
<Tooltip text={ props.t('number_of_dns_query_blocked_24_hours') } type={tooltipType} />
|
||||
</td>
|
||||
<td className="text-right">
|
||||
@@ -90,7 +92,7 @@ Counters.propTypes = {
|
||||
replacedSafesearch: PropTypes.number.isRequired,
|
||||
avgProcessingTime: PropTypes.number.isRequired,
|
||||
refreshButton: PropTypes.node.isRequired,
|
||||
t: PropTypes.func,
|
||||
t: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
export default withNamespaces()(Counters);
|
||||
|
||||
@@ -49,7 +49,9 @@ class Statistics extends Component {
|
||||
{getPercent(dnsQueries, blockedFiltering)}
|
||||
</div>
|
||||
<div className="card-title-stats">
|
||||
<Trans>blocked_by</Trans><a href="#filters"> <Trans>filters</Trans></a>
|
||||
<a href="#filters">
|
||||
<Trans>blocked_by</Trans>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<div className="card-chart-bg">
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import React, { Component, Fragment } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import 'whatwg-fetch';
|
||||
import { Trans, withNamespaces } from 'react-i18next';
|
||||
|
||||
import Statistics from './Statistics';
|
||||
@@ -25,12 +24,17 @@ class Dashboard extends Component {
|
||||
}
|
||||
|
||||
getToggleFilteringButton = () => {
|
||||
const { protectionEnabled } = this.props.dashboard;
|
||||
const { protectionEnabled, processingProtection } = this.props.dashboard;
|
||||
const buttonText = protectionEnabled ? 'disable_protection' : 'enable_protection';
|
||||
const buttonClass = protectionEnabled ? 'btn-gray' : 'btn-success';
|
||||
|
||||
return (
|
||||
<button type="button" className={`btn btn-sm mr-2 ${buttonClass}`} onClick={() => this.props.toggleProtection(protectionEnabled)}>
|
||||
<button
|
||||
type="button"
|
||||
className={`btn btn-sm mr-2 ${buttonClass}`}
|
||||
onClick={() => this.props.toggleProtection(protectionEnabled)}
|
||||
disabled={processingProtection}
|
||||
>
|
||||
<Trans>{buttonText}</Trans>
|
||||
</button>
|
||||
);
|
||||
@@ -125,6 +129,7 @@ Dashboard.propTypes = {
|
||||
isCoreRunning: PropTypes.bool,
|
||||
getFiltering: PropTypes.func,
|
||||
toggleProtection: PropTypes.func,
|
||||
processingProtection: PropTypes.bool,
|
||||
t: PropTypes.func,
|
||||
};
|
||||
|
||||
|
||||
@@ -25,7 +25,7 @@ class UserRules extends Component {
|
||||
<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"
|
||||
className="btn btn-success btn-standard"
|
||||
type="submit"
|
||||
onClick={this.handleSubmit}
|
||||
>
|
||||
@@ -52,6 +52,9 @@ class UserRules extends Component {
|
||||
<li>
|
||||
<code>{ t('example_comment_hash') }</code> - { t('example_comment_meaning') }
|
||||
</li>
|
||||
<li>
|
||||
<code>/REGEX/</code> - { t('example_regex_meaning') }
|
||||
</li>
|
||||
</ol>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
@@ -34,30 +34,30 @@ class Filters extends Component {
|
||||
};
|
||||
|
||||
columns = [{
|
||||
Header: this.props.t('enabled_table_header'),
|
||||
Header: <Trans>enabled_table_header</Trans>,
|
||||
accessor: 'enabled',
|
||||
Cell: this.renderCheckbox,
|
||||
width: 90,
|
||||
className: 'text-center',
|
||||
}, {
|
||||
Header: this.props.t('name_table_header'),
|
||||
Header: <Trans>name_table_header</Trans>,
|
||||
accessor: 'name',
|
||||
Cell: ({ value }) => (<div className="logs__row logs__row--overflow"><span className="logs__text" title={value}>{value}</span></div>),
|
||||
}, {
|
||||
Header: this.props.t('filter_url_table_header'),
|
||||
Header: <Trans>filter_url_table_header</Trans>,
|
||||
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: this.props.t('rules_count_table_header'),
|
||||
Header: <Trans>rules_count_table_header</Trans>,
|
||||
accessor: 'rulesCount',
|
||||
className: 'text-center',
|
||||
Cell: props => props.value.toLocaleString(),
|
||||
}, {
|
||||
Header: this.props.t('last_time_updated_table_header'),
|
||||
Header: <Trans>last_time_updated_table_header</Trans>,
|
||||
accessor: 'lastUpdated',
|
||||
className: 'text-center',
|
||||
}, {
|
||||
Header: this.props.t('actions_table_header'),
|
||||
Header: <Trans>actions_table_header</Trans>,
|
||||
accessor: 'url',
|
||||
Cell: ({ value }) => (<span title={ this.props.t('delete_table_action') } className='remove-icon fe fe-trash-2' onClick={() => this.props.removeFilter(value)}/>),
|
||||
className: 'text-center',
|
||||
@@ -68,7 +68,7 @@ class Filters extends Component {
|
||||
|
||||
render() {
|
||||
const { t } = this.props;
|
||||
const { filters, userRules } = this.props.filtering;
|
||||
const { filters, userRules, processingRefreshFilters } = this.props.filtering;
|
||||
return (
|
||||
<div>
|
||||
<PageTitle title={ t('filters') } />
|
||||
@@ -82,13 +82,34 @@ class Filters extends Component {
|
||||
<ReactTable
|
||||
data={filters}
|
||||
columns={this.columns}
|
||||
showPagination={false}
|
||||
showPagination={true}
|
||||
defaultPageSize={10}
|
||||
minRows={4}
|
||||
// Text
|
||||
previousText={ t('previous_btn') }
|
||||
nextText={ t('next_btn') }
|
||||
loadingText={ t('loading_table_status') }
|
||||
pageText={ t('page_table_footer_text') }
|
||||
ofText={ t('of_table_footer_text') }
|
||||
rowsText={ t('rows_table_footer_text') }
|
||||
noDataText={ t('no_filters_added') }
|
||||
minRows={4} // TODO find out what to show if rules.length is 0
|
||||
/>
|
||||
<div className="card-actions">
|
||||
<button className="btn btn-success btn-standart mr-2" type="submit" onClick={this.props.toggleFilteringModal}><Trans>add_filter_btn</Trans></button>
|
||||
<button className="btn btn-primary btn-standart" type="submit" onClick={this.props.refreshFilters}><Trans>check_updates_btn</Trans></button>
|
||||
<button
|
||||
className="btn btn-success btn-standard mr-2"
|
||||
type="submit"
|
||||
onClick={this.props.toggleFilteringModal}
|
||||
>
|
||||
<Trans>add_filter_btn</Trans>
|
||||
</button>
|
||||
<button
|
||||
className="btn btn-primary btn-standard"
|
||||
type="submit"
|
||||
onClick={this.props.refreshFilters}
|
||||
disabled={processingRefreshFilters}
|
||||
>
|
||||
<Trans>check_updates_btn</Trans>
|
||||
</button>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
@@ -106,6 +127,7 @@ class Filters extends Component {
|
||||
toggleModal={this.props.toggleFilteringModal}
|
||||
addFilter={this.props.addFilter}
|
||||
isFilterAdded={this.props.filtering.isFilterAdded}
|
||||
processingAddFilter={this.props.filtering.processingAddFilter}
|
||||
title={ t('new_filter_btn') }
|
||||
inputDescription={ t('enter_valid_filter_url') }
|
||||
/>
|
||||
@@ -122,6 +144,8 @@ Filters.propTypes = {
|
||||
filters: PropTypes.array,
|
||||
isFilteringModalOpen: PropTypes.bool.isRequired,
|
||||
isFilterAdded: PropTypes.bool,
|
||||
processingAddFilter: PropTypes.bool,
|
||||
processingRefreshFilters: PropTypes.bool,
|
||||
}),
|
||||
removeFilter: PropTypes.func.isRequired,
|
||||
toggleFilterStatus: PropTypes.func.isRequired,
|
||||
|
||||
@@ -6,13 +6,12 @@ import { Trans, withNamespaces } from 'react-i18next';
|
||||
|
||||
import Menu from './Menu';
|
||||
import Version from './Version';
|
||||
import logo from './logo.svg';
|
||||
import logo from '../ui/svg/logo.svg';
|
||||
import './Header.css';
|
||||
|
||||
class Header extends Component {
|
||||
state = {
|
||||
isMenuOpen: false,
|
||||
isDropdownOpen: false,
|
||||
};
|
||||
|
||||
toggleMenuOpen = () => {
|
||||
@@ -25,6 +24,7 @@ class Header extends Component {
|
||||
|
||||
render() {
|
||||
const { dashboard } = this.props;
|
||||
const { isMenuOpen } = this.state;
|
||||
const badgeClass = classnames({
|
||||
'badge dns-status': true,
|
||||
'badge-success': dashboard.protectionEnabled,
|
||||
@@ -52,7 +52,7 @@ class Header extends Component {
|
||||
</div>
|
||||
<Menu
|
||||
location={this.props.location}
|
||||
isMenuOpen={this.state.isMenuOpen}
|
||||
isMenuOpen={isMenuOpen}
|
||||
toggleMenuOpen={this.toggleMenuOpen}
|
||||
closeMenu={this.closeMenu}
|
||||
/>
|
||||
|
||||
@@ -77,6 +77,7 @@ class Logs extends Component {
|
||||
type="button"
|
||||
className={`btn btn-sm ${buttonClass}`}
|
||||
onClick={() => this.toggleBlocking(buttonType, domain)}
|
||||
disabled={this.props.filtering.processingRules}
|
||||
>
|
||||
<Trans>{buttonText}</Trans>
|
||||
</button>
|
||||
@@ -131,7 +132,14 @@ class Logs extends Component {
|
||||
} else {
|
||||
const filterItem = Object.keys(filters)
|
||||
.filter(key => filters[key].id === filterId);
|
||||
filterName = filters[filterItem].name;
|
||||
|
||||
if (typeof filterItem !== 'undefined' && typeof filters[filterItem] !== 'undefined') {
|
||||
filterName = filters[filterItem].name;
|
||||
}
|
||||
|
||||
if (!filterName) {
|
||||
filterName = t('unknown_filter', { filterId });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -182,7 +190,7 @@ class Logs extends Component {
|
||||
<option value="filtered">{ t('show_filtered_type') }</option>
|
||||
</select>,
|
||||
}, {
|
||||
Header: t('Client'),
|
||||
Header: t('client_table_header'),
|
||||
accessor: 'client',
|
||||
maxWidth: 250,
|
||||
Cell: (row) => {
|
||||
@@ -262,7 +270,7 @@ class Logs extends Component {
|
||||
saveAs(dataBlob, DOWNLOAD_LOG_FILENAME);
|
||||
};
|
||||
|
||||
renderButtons(queryLogEnabled) {
|
||||
renderButtons(queryLogEnabled, logStatusProcessing) {
|
||||
if (queryLogEnabled) {
|
||||
return (
|
||||
<Fragment>
|
||||
@@ -270,6 +278,7 @@ class Logs extends Component {
|
||||
className="btn btn-gray btn-sm mr-2"
|
||||
type="submit"
|
||||
onClick={() => this.props.toggleLogStatus(queryLogEnabled)}
|
||||
disabled={logStatusProcessing}
|
||||
><Trans>disabled_log_btn</Trans></button>
|
||||
<button
|
||||
className="btn btn-primary btn-sm mr-2"
|
||||
@@ -290,6 +299,7 @@ class Logs extends Component {
|
||||
className="btn btn-success btn-sm mr-2"
|
||||
type="submit"
|
||||
onClick={() => this.props.toggleLogStatus(queryLogEnabled)}
|
||||
disabled={logStatusProcessing}
|
||||
><Trans>enabled_log_btn</Trans></button>
|
||||
);
|
||||
}
|
||||
@@ -301,7 +311,7 @@ class Logs extends Component {
|
||||
<Fragment>
|
||||
<PageTitle title={ t('query_log') } subtitle={ t('last_dns_queries') }>
|
||||
<div className="page-title__actions">
|
||||
{this.renderButtons(queryLogEnabled)}
|
||||
{this.renderButtons(queryLogEnabled, dashboard.logStatusProcessing)}
|
||||
</div>
|
||||
</PageTitle>
|
||||
<Card>
|
||||
@@ -325,6 +335,8 @@ Logs.propTypes = {
|
||||
userRules: PropTypes.string,
|
||||
setRules: PropTypes.func,
|
||||
addSuccessToast: PropTypes.func,
|
||||
processingRules: PropTypes.bool,
|
||||
logStatusProcessing: PropTypes.bool,
|
||||
t: PropTypes.func,
|
||||
};
|
||||
|
||||
|
||||
112
client/src/components/Settings/Dhcp/Form.js
Normal file
112
client/src/components/Settings/Dhcp/Form.js
Normal file
@@ -0,0 +1,112 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { Field, reduxForm } from 'redux-form';
|
||||
import { withNamespaces } from 'react-i18next';
|
||||
import flow from 'lodash/flow';
|
||||
|
||||
import { renderField, required, ipv4, isPositive, toNumber } from '../../../helpers/form';
|
||||
|
||||
const Form = (props) => {
|
||||
const {
|
||||
t,
|
||||
handleSubmit,
|
||||
submitting,
|
||||
invalid,
|
||||
processingConfig,
|
||||
} = props;
|
||||
|
||||
return (
|
||||
<form onSubmit={handleSubmit}>
|
||||
<div className="row">
|
||||
<div className="col-lg-6">
|
||||
<div className="form__group form__group--settings">
|
||||
<label>{t('dhcp_form_gateway_input')}</label>
|
||||
<Field
|
||||
name="gateway_ip"
|
||||
component={renderField}
|
||||
type="text"
|
||||
className="form-control"
|
||||
placeholder={t('dhcp_form_gateway_input')}
|
||||
validate={[ipv4, required]}
|
||||
/>
|
||||
</div>
|
||||
<div className="form__group form__group--settings">
|
||||
<label>{t('dhcp_form_subnet_input')}</label>
|
||||
<Field
|
||||
name="subnet_mask"
|
||||
component={renderField}
|
||||
type="text"
|
||||
className="form-control"
|
||||
placeholder={t('dhcp_form_subnet_input')}
|
||||
validate={[ipv4, required]}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="col-lg-6">
|
||||
<div className="form__group form__group--settings">
|
||||
<div className="row">
|
||||
<div className="col-12">
|
||||
<label>{t('dhcp_form_range_title')}</label>
|
||||
</div>
|
||||
<div className="col">
|
||||
<Field
|
||||
name="range_start"
|
||||
component={renderField}
|
||||
type="text"
|
||||
className="form-control"
|
||||
placeholder={t('dhcp_form_range_start')}
|
||||
validate={[ipv4, required]}
|
||||
/>
|
||||
</div>
|
||||
<div className="col">
|
||||
<Field
|
||||
name="range_end"
|
||||
component={renderField}
|
||||
type="text"
|
||||
className="form-control"
|
||||
placeholder={t('dhcp_form_range_end')}
|
||||
validate={[ipv4, required]}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="form__group form__group--settings">
|
||||
<label>{t('dhcp_form_lease_title')}</label>
|
||||
<Field
|
||||
name="lease_duration"
|
||||
component={renderField}
|
||||
type="number"
|
||||
className="form-control"
|
||||
placeholder={t('dhcp_form_lease_input')}
|
||||
validate={[required, isPositive]}
|
||||
normalize={toNumber}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button
|
||||
type="submit"
|
||||
className="btn btn-success btn-standard"
|
||||
disabled={submitting || invalid || processingConfig}
|
||||
>
|
||||
{t('save_config')}
|
||||
</button>
|
||||
</form>
|
||||
);
|
||||
};
|
||||
|
||||
Form.propTypes = {
|
||||
handleSubmit: PropTypes.func,
|
||||
submitting: PropTypes.bool,
|
||||
invalid: PropTypes.bool,
|
||||
interfaces: PropTypes.object,
|
||||
initialValues: PropTypes.object,
|
||||
processingConfig: PropTypes.bool,
|
||||
t: PropTypes.func,
|
||||
};
|
||||
|
||||
export default flow([
|
||||
withNamespaces(),
|
||||
reduxForm({ form: 'dhcpForm' }),
|
||||
])(Form);
|
||||
113
client/src/components/Settings/Dhcp/Interface.js
Normal file
113
client/src/components/Settings/Dhcp/Interface.js
Normal file
@@ -0,0 +1,113 @@
|
||||
import React from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import PropTypes from 'prop-types';
|
||||
import { Field, reduxForm, formValueSelector } from 'redux-form';
|
||||
import { withNamespaces, Trans } from 'react-i18next';
|
||||
import flow from 'lodash/flow';
|
||||
|
||||
const renderInterfaces = (interfaces => (
|
||||
Object.keys(interfaces).map((item) => {
|
||||
const option = interfaces[item];
|
||||
const { name } = option;
|
||||
const onlyIPv6 = option.ip_addresses.every(ip => ip.includes(':'));
|
||||
let interfaceIP = option.ip_addresses[0];
|
||||
|
||||
if (!onlyIPv6) {
|
||||
option.ip_addresses.forEach((ip) => {
|
||||
if (!ip.includes(':')) {
|
||||
interfaceIP = ip;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return (
|
||||
<option value={name} key={name} disabled={onlyIPv6}>
|
||||
{name} - {interfaceIP}
|
||||
</option>
|
||||
);
|
||||
})
|
||||
));
|
||||
|
||||
const renderInterfaceValues = (interfaceValues => (
|
||||
<ul className="list-unstyled mt-1 mb-0">
|
||||
<li>
|
||||
<span className="interface__title">MTU: </span>
|
||||
{interfaceValues.mtu}
|
||||
</li>
|
||||
<li>
|
||||
<span className="interface__title"><Trans>dhcp_hardware_address</Trans>: </span>
|
||||
{interfaceValues.hardware_address}
|
||||
</li>
|
||||
<li>
|
||||
<span className="interface__title"><Trans>dhcp_ip_addresses</Trans>: </span>
|
||||
{
|
||||
interfaceValues.ip_addresses
|
||||
.map(ip => <span key={ip} className="interface__ip">{ip}</span>)
|
||||
}
|
||||
</li>
|
||||
</ul>
|
||||
));
|
||||
|
||||
let Interface = (props) => {
|
||||
const {
|
||||
t,
|
||||
handleChange,
|
||||
interfaces,
|
||||
processing,
|
||||
interfaceValue,
|
||||
enabled,
|
||||
} = props;
|
||||
|
||||
return (
|
||||
<form>
|
||||
{!processing && interfaces &&
|
||||
<div className="row">
|
||||
<div className="col-sm-12 col-md-6">
|
||||
<div className="form__group form__group--settings">
|
||||
<label>{t('dhcp_interface_select')}</label>
|
||||
<Field
|
||||
name="interface_name"
|
||||
component="select"
|
||||
className="form-control custom-select"
|
||||
onChange={handleChange}
|
||||
>
|
||||
<option value="" disabled={enabled}>{t('dhcp_interface_select')}</option>
|
||||
{renderInterfaces(interfaces)}
|
||||
</Field>
|
||||
</div>
|
||||
</div>
|
||||
{interfaceValue &&
|
||||
<div className="col-sm-12 col-md-6">
|
||||
{renderInterfaceValues(interfaces[interfaceValue])}
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
<hr/>
|
||||
</form>
|
||||
);
|
||||
};
|
||||
|
||||
Interface.propTypes = {
|
||||
handleChange: PropTypes.func,
|
||||
interfaces: PropTypes.object,
|
||||
processing: PropTypes.bool,
|
||||
interfaceValue: PropTypes.string,
|
||||
initialValues: PropTypes.object,
|
||||
enabled: PropTypes.bool,
|
||||
t: PropTypes.func,
|
||||
};
|
||||
|
||||
const selector = formValueSelector('dhcpInterface');
|
||||
|
||||
Interface = connect((state) => {
|
||||
const interfaceValue = selector(state, 'interface_name');
|
||||
return {
|
||||
interfaceValue,
|
||||
};
|
||||
})(Interface);
|
||||
|
||||
export default flow([
|
||||
withNamespaces(),
|
||||
reduxForm({ form: 'dhcpInterface' }),
|
||||
])(Interface);
|
||||
36
client/src/components/Settings/Dhcp/Leases.js
Normal file
36
client/src/components/Settings/Dhcp/Leases.js
Normal file
@@ -0,0 +1,36 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import ReactTable from 'react-table';
|
||||
import { Trans, withNamespaces } from 'react-i18next';
|
||||
|
||||
const columns = [{
|
||||
Header: 'MAC',
|
||||
accessor: 'mac',
|
||||
}, {
|
||||
Header: 'IP',
|
||||
accessor: 'ip',
|
||||
}, {
|
||||
Header: <Trans>dhcp_table_hostname</Trans>,
|
||||
accessor: 'hostname',
|
||||
}, {
|
||||
Header: <Trans>dhcp_table_expires</Trans>,
|
||||
accessor: 'expires',
|
||||
}];
|
||||
|
||||
const Leases = props => (
|
||||
<ReactTable
|
||||
data={props.leases || []}
|
||||
columns={columns}
|
||||
showPagination={false}
|
||||
noDataText={ props.t('dhcp_leases_not_found') }
|
||||
minRows={6}
|
||||
className="-striped -highlight card-table-overflow"
|
||||
/>
|
||||
);
|
||||
|
||||
Leases.propTypes = {
|
||||
leases: PropTypes.array,
|
||||
t: PropTypes.func,
|
||||
};
|
||||
|
||||
export default withNamespaces()(Leases);
|
||||
173
client/src/components/Settings/Dhcp/index.js
Normal file
173
client/src/components/Settings/Dhcp/index.js
Normal file
@@ -0,0 +1,173 @@
|
||||
import React, { Component, Fragment } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import classnames from 'classnames';
|
||||
import { Trans, withNamespaces } from 'react-i18next';
|
||||
|
||||
import Form from './Form';
|
||||
import Leases from './Leases';
|
||||
import Interface from './Interface';
|
||||
import Card from '../../ui/Card';
|
||||
|
||||
class Dhcp extends Component {
|
||||
handleFormSubmit = (values) => {
|
||||
this.props.setDhcpConfig(values);
|
||||
};
|
||||
|
||||
handleToggle = (config) => {
|
||||
this.props.toggleDhcp(config);
|
||||
}
|
||||
|
||||
getToggleDhcpButton = () => {
|
||||
const {
|
||||
config, active, processingDhcp, processingConfig,
|
||||
} = this.props.dhcp;
|
||||
const activeDhcpFound = active && active.found;
|
||||
const filledConfig = Object.keys(config).every((key) => {
|
||||
if (key === 'enabled') {
|
||||
return true;
|
||||
}
|
||||
|
||||
return config[key];
|
||||
});
|
||||
|
||||
if (config.enabled) {
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-standard mr-2 btn-gray"
|
||||
onClick={() => this.props.toggleDhcp(config)}
|
||||
disabled={processingDhcp || processingConfig}
|
||||
>
|
||||
<Trans>dhcp_disable</Trans>
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-standard mr-2 btn-success"
|
||||
onClick={() => this.handleToggle(config)}
|
||||
disabled={
|
||||
!filledConfig
|
||||
|| activeDhcpFound
|
||||
|| processingDhcp
|
||||
|| processingConfig
|
||||
}
|
||||
>
|
||||
<Trans>dhcp_enable</Trans>
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
getActiveDhcpMessage = () => {
|
||||
const { active } = this.props.dhcp;
|
||||
|
||||
if (active) {
|
||||
if (active.error) {
|
||||
return (
|
||||
<div className="text-danger mb-2">
|
||||
{active.error}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="mb-2">
|
||||
{active.found ? (
|
||||
<div className="text-danger">
|
||||
<Trans>dhcp_found</Trans>
|
||||
</div>
|
||||
) : (
|
||||
<div className="text-secondary">
|
||||
<Trans>dhcp_not_found</Trans>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return '';
|
||||
}
|
||||
|
||||
render() {
|
||||
const { t, dhcp } = this.props;
|
||||
const statusButtonClass = classnames({
|
||||
'btn btn-primary btn-standard': true,
|
||||
'btn btn-primary btn-standard btn-loading': dhcp.processingStatus,
|
||||
});
|
||||
const {
|
||||
enabled,
|
||||
interface_name,
|
||||
...values
|
||||
} = dhcp.config;
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
<Card title={ t('dhcp_title') } subtitle={ t('dhcp_description') } bodyType="card-body box-body--settings">
|
||||
<div className="dhcp">
|
||||
{!dhcp.processing &&
|
||||
<Fragment>
|
||||
<Interface
|
||||
onChange={this.handleFormSubmit}
|
||||
initialValues={{ interface_name }}
|
||||
interfaces={dhcp.interfaces}
|
||||
processing={dhcp.processingInterfaces}
|
||||
enabled={dhcp.config.enabled}
|
||||
/>
|
||||
<Form
|
||||
onSubmit={this.handleFormSubmit}
|
||||
initialValues={{ ...values }}
|
||||
interfaces={dhcp.interfaces}
|
||||
processingConfig={dhcp.processingConfig}
|
||||
/>
|
||||
<hr/>
|
||||
<div className="card-actions mb-3">
|
||||
{this.getToggleDhcpButton()}
|
||||
<button
|
||||
type="button"
|
||||
className={statusButtonClass}
|
||||
onClick={() =>
|
||||
this.props.findActiveDhcp(dhcp.config.interface_name)
|
||||
}
|
||||
disabled={
|
||||
!dhcp.config.interface_name
|
||||
|| dhcp.processingConfig
|
||||
}
|
||||
>
|
||||
<Trans>check_dhcp_servers</Trans>
|
||||
</button>
|
||||
</div>
|
||||
{this.getActiveDhcpMessage()}
|
||||
<div className="text-danger">
|
||||
<Trans>dhcp_warning</Trans>
|
||||
</div>
|
||||
</Fragment>
|
||||
}
|
||||
</div>
|
||||
</Card>
|
||||
{!dhcp.processing && dhcp.config.enabled &&
|
||||
<Card title={ t('dhcp_leases') } bodyType="card-body box-body--settings">
|
||||
<div className="row">
|
||||
<div className="col">
|
||||
<Leases leases={dhcp.leases} />
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
}
|
||||
</Fragment>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Dhcp.propTypes = {
|
||||
dhcp: PropTypes.object,
|
||||
toggleDhcp: PropTypes.func,
|
||||
getDhcpStatus: PropTypes.func,
|
||||
setDhcpConfig: PropTypes.func,
|
||||
findActiveDhcp: PropTypes.func,
|
||||
handleSubmit: PropTypes.func,
|
||||
t: PropTypes.func,
|
||||
};
|
||||
|
||||
export default withNamespaces()(Dhcp);
|
||||
364
client/src/components/Settings/Encryption/Form.js
Normal file
364
client/src/components/Settings/Encryption/Form.js
Normal file
@@ -0,0 +1,364 @@
|
||||
import React, { Fragment } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import PropTypes from 'prop-types';
|
||||
import { Field, reduxForm, formValueSelector } from 'redux-form';
|
||||
import { Trans, withNamespaces } from 'react-i18next';
|
||||
import flow from 'lodash/flow';
|
||||
import format from 'date-fns/format';
|
||||
|
||||
import { renderField, renderSelectField, toNumber, port, isSafePort } from '../../../helpers/form';
|
||||
import { EMPTY_DATE } from '../../../helpers/constants';
|
||||
import i18n from '../../../i18n';
|
||||
|
||||
const validate = (values) => {
|
||||
const errors = {};
|
||||
|
||||
if (values.port_dns_over_tls && values.port_https) {
|
||||
if (values.port_dns_over_tls === values.port_https) {
|
||||
errors.port_dns_over_tls = i18n.t('form_error_equal');
|
||||
errors.port_https = i18n.t('form_error_equal');
|
||||
}
|
||||
}
|
||||
|
||||
return errors;
|
||||
};
|
||||
|
||||
const clearFields = (change, setTlsConfig, t) => {
|
||||
const fields = {
|
||||
private_key: '',
|
||||
certificate_chain: '',
|
||||
port_https: 443,
|
||||
port_dns_over_tls: 853,
|
||||
server_name: '',
|
||||
force_https: false,
|
||||
enabled: false,
|
||||
};
|
||||
// eslint-disable-next-line no-alert
|
||||
if (window.confirm(t('encryption_reset'))) {
|
||||
Object.keys(fields).forEach(field => change(field, fields[field]));
|
||||
setTlsConfig(fields);
|
||||
}
|
||||
};
|
||||
|
||||
let Form = (props) => {
|
||||
const {
|
||||
t,
|
||||
handleSubmit,
|
||||
handleChange,
|
||||
isEnabled,
|
||||
certificateChain,
|
||||
privateKey,
|
||||
change,
|
||||
invalid,
|
||||
submitting,
|
||||
processingConfig,
|
||||
processingValidate,
|
||||
not_after,
|
||||
valid_chain,
|
||||
valid_key,
|
||||
valid_cert,
|
||||
dns_names,
|
||||
key_type,
|
||||
issuer,
|
||||
subject,
|
||||
warning_validation,
|
||||
setTlsConfig,
|
||||
} = props;
|
||||
|
||||
return (
|
||||
<form onSubmit={handleSubmit}>
|
||||
<div className="row">
|
||||
<div className="col-12">
|
||||
<div className="form__group form__group--settings">
|
||||
<Field
|
||||
name="enabled"
|
||||
type="checkbox"
|
||||
component={renderSelectField}
|
||||
placeholder={t('encryption_enable')}
|
||||
onChange={handleChange}
|
||||
/>
|
||||
</div>
|
||||
<div className="form__desc">
|
||||
<Trans>encryption_enable_desc</Trans>
|
||||
</div>
|
||||
<hr/>
|
||||
</div>
|
||||
<div className="col-12">
|
||||
<label className="form__label" htmlFor="server_name">
|
||||
<Trans>encryption_server</Trans>
|
||||
</label>
|
||||
</div>
|
||||
<div className="col-lg-6">
|
||||
<div className="form__group form__group--settings">
|
||||
<Field
|
||||
id="server_name"
|
||||
name="server_name"
|
||||
component={renderField}
|
||||
type="text"
|
||||
className="form-control"
|
||||
placeholder={t('encryption_server_enter')}
|
||||
onChange={handleChange}
|
||||
disabled={!isEnabled}
|
||||
/>
|
||||
<div className="form__desc">
|
||||
<Trans>encryption_server_desc</Trans>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="col-lg-6">
|
||||
<div className="form__group form__group--settings">
|
||||
<Field
|
||||
name="force_https"
|
||||
type="checkbox"
|
||||
component={renderSelectField}
|
||||
placeholder={t('encryption_redirect')}
|
||||
onChange={handleChange}
|
||||
disabled={!isEnabled}
|
||||
/>
|
||||
<div className="form__desc">
|
||||
<Trans>encryption_redirect_desc</Trans>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="row">
|
||||
<div className="col-lg-6">
|
||||
<div className="form__group form__group--settings">
|
||||
<label className="form__label" htmlFor="port_https">
|
||||
<Trans>encryption_https</Trans>
|
||||
</label>
|
||||
<Field
|
||||
id="port_https"
|
||||
name="port_https"
|
||||
component={renderField}
|
||||
type="number"
|
||||
className="form-control"
|
||||
placeholder={t('encryption_https')}
|
||||
validate={[port, isSafePort]}
|
||||
normalize={toNumber}
|
||||
onChange={handleChange}
|
||||
disabled={!isEnabled}
|
||||
/>
|
||||
<div className="form__desc">
|
||||
<Trans>encryption_https_desc</Trans>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="col-lg-6">
|
||||
<div className="form__group form__group--settings">
|
||||
<label className="form__label" htmlFor="port_dns_over_tls">
|
||||
<Trans>encryption_dot</Trans>
|
||||
</label>
|
||||
<Field
|
||||
id="port_dns_over_tls"
|
||||
name="port_dns_over_tls"
|
||||
component={renderField}
|
||||
type="number"
|
||||
className="form-control"
|
||||
placeholder={t('encryption_dot')}
|
||||
validate={[port]}
|
||||
normalize={toNumber}
|
||||
onChange={handleChange}
|
||||
disabled={!isEnabled}
|
||||
/>
|
||||
<div className="form__desc">
|
||||
<Trans>encryption_dot_desc</Trans>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="row">
|
||||
<div className="col-12">
|
||||
<div className="form__group form__group--settings">
|
||||
<label className="form__label form__label--bold" htmlFor="certificate_chain">
|
||||
<Trans>encryption_certificates</Trans>
|
||||
</label>
|
||||
<div className="form__desc form__desc--top">
|
||||
<Trans
|
||||
values={{ link: 'letsencrypt.org' }}
|
||||
components={[<a href="https://letsencrypt.org/" key="0">link</a>]}
|
||||
>
|
||||
encryption_certificates_desc
|
||||
</Trans>
|
||||
</div>
|
||||
<Field
|
||||
id="certificate_chain"
|
||||
name="certificate_chain"
|
||||
component="textarea"
|
||||
type="text"
|
||||
className="form-control form-control--textarea"
|
||||
placeholder={t('encryption_certificates_input')}
|
||||
onChange={handleChange}
|
||||
disabled={!isEnabled}
|
||||
/>
|
||||
<div className="form__status">
|
||||
{certificateChain &&
|
||||
<Fragment>
|
||||
<div className="form__label form__label--bold">
|
||||
<Trans>encryption_status</Trans>:
|
||||
</div>
|
||||
<ul className="encryption__list">
|
||||
<li className={valid_chain ? 'text-success' : 'text-danger'}>
|
||||
{valid_chain ?
|
||||
<Trans>encryption_chain_valid</Trans>
|
||||
: <Trans>encryption_chain_invalid</Trans>
|
||||
}
|
||||
</li>
|
||||
{valid_cert &&
|
||||
<Fragment>
|
||||
{subject &&
|
||||
<li>
|
||||
<Trans>encryption_subject</Trans>:
|
||||
{subject}
|
||||
</li>
|
||||
}
|
||||
{issuer &&
|
||||
<li>
|
||||
<Trans>encryption_issuer</Trans>:
|
||||
{issuer}
|
||||
</li>
|
||||
}
|
||||
{not_after && not_after !== EMPTY_DATE &&
|
||||
<li>
|
||||
<Trans>encryption_expire</Trans>:
|
||||
{format(not_after, 'YYYY-MM-DD HH:mm:ss')}
|
||||
</li>
|
||||
}
|
||||
{dns_names &&
|
||||
<li>
|
||||
<Trans>encryption_hostnames</Trans>:
|
||||
{dns_names}
|
||||
</li>
|
||||
}
|
||||
</Fragment>
|
||||
}
|
||||
</ul>
|
||||
</Fragment>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="row">
|
||||
<div className="col-12">
|
||||
<div className="form__group form__group--settings">
|
||||
<label className="form__label form__label--bold" htmlFor="private_key">
|
||||
<Trans>encryption_key</Trans>
|
||||
</label>
|
||||
<Field
|
||||
id="private_key"
|
||||
name="private_key"
|
||||
component="textarea"
|
||||
type="text"
|
||||
className="form-control form-control--textarea"
|
||||
placeholder="Copy/paste your PEM-encoded private key for your cerficate here."
|
||||
onChange={handleChange}
|
||||
disabled={!isEnabled}
|
||||
/>
|
||||
<div className="form__status">
|
||||
{privateKey &&
|
||||
<Fragment>
|
||||
<div className="form__label form__label--bold">
|
||||
<Trans>encryption_status</Trans>:
|
||||
</div>
|
||||
<ul className="encryption__list">
|
||||
<li className={valid_key ? 'text-success' : 'text-danger'}>
|
||||
{valid_key ?
|
||||
<Trans values={{ type: key_type }}>
|
||||
encryption_key_valid
|
||||
</Trans>
|
||||
: <Trans values={{ type: key_type }}>
|
||||
encryption_key_invalid
|
||||
</Trans>
|
||||
}
|
||||
</li>
|
||||
</ul>
|
||||
</Fragment>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{warning_validation &&
|
||||
<div className="col-12">
|
||||
<p className="text-danger">
|
||||
{warning_validation}
|
||||
</p>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
|
||||
<div className="btn-list mt-2">
|
||||
<button
|
||||
type="submit"
|
||||
className="btn btn-success btn-standart"
|
||||
disabled={
|
||||
invalid
|
||||
|| submitting
|
||||
|| processingConfig
|
||||
|| processingValidate
|
||||
|| (isEnabled && (!privateKey || !certificateChain))
|
||||
|| (privateKey && !valid_key)
|
||||
|| (certificateChain && !valid_cert)
|
||||
}
|
||||
>
|
||||
<Trans>save_config</Trans>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-secondary btn-standart"
|
||||
disabled={submitting || processingConfig}
|
||||
onClick={() => clearFields(change, setTlsConfig, t)}
|
||||
>
|
||||
<Trans>reset_settings</Trans>
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
);
|
||||
};
|
||||
|
||||
Form.propTypes = {
|
||||
handleSubmit: PropTypes.func.isRequired,
|
||||
handleChange: PropTypes.func,
|
||||
isEnabled: PropTypes.bool.isRequired,
|
||||
certificateChain: PropTypes.string.isRequired,
|
||||
privateKey: PropTypes.string.isRequired,
|
||||
change: PropTypes.func.isRequired,
|
||||
submitting: PropTypes.bool.isRequired,
|
||||
invalid: PropTypes.bool.isRequired,
|
||||
initialValues: PropTypes.object.isRequired,
|
||||
processingConfig: PropTypes.bool.isRequired,
|
||||
processingValidate: PropTypes.bool.isRequired,
|
||||
status_key: PropTypes.string,
|
||||
not_after: PropTypes.string,
|
||||
warning_validation: PropTypes.string,
|
||||
valid_chain: PropTypes.bool,
|
||||
valid_key: PropTypes.bool,
|
||||
valid_cert: PropTypes.bool,
|
||||
dns_names: PropTypes.string,
|
||||
key_type: PropTypes.string,
|
||||
issuer: PropTypes.string,
|
||||
subject: PropTypes.string,
|
||||
t: PropTypes.func.isRequired,
|
||||
setTlsConfig: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
const selector = formValueSelector('encryptionForm');
|
||||
|
||||
Form = connect((state) => {
|
||||
const isEnabled = selector(state, 'enabled');
|
||||
const certificateChain = selector(state, 'certificate_chain');
|
||||
const privateKey = selector(state, 'private_key');
|
||||
return {
|
||||
isEnabled,
|
||||
certificateChain,
|
||||
privateKey,
|
||||
};
|
||||
})(Form);
|
||||
|
||||
export default flow([
|
||||
withNamespaces(),
|
||||
reduxForm({
|
||||
form: 'encryptionForm',
|
||||
validate,
|
||||
}),
|
||||
])(Form);
|
||||
72
client/src/components/Settings/Encryption/index.js
Normal file
72
client/src/components/Settings/Encryption/index.js
Normal file
@@ -0,0 +1,72 @@
|
||||
import React, { Component } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { withNamespaces } from 'react-i18next';
|
||||
import debounce from 'lodash/debounce';
|
||||
|
||||
import { DEBOUNCE_TIMEOUT } from '../../../helpers/constants';
|
||||
import Form from './Form';
|
||||
import Card from '../../ui/Card';
|
||||
|
||||
class Encryption extends Component {
|
||||
componentDidMount() {
|
||||
this.props.validateTlsConfig(this.props.encryption);
|
||||
}
|
||||
|
||||
handleFormSubmit = (values) => {
|
||||
this.props.setTlsConfig(values);
|
||||
};
|
||||
|
||||
handleFormChange = debounce((values) => {
|
||||
this.props.validateTlsConfig(values);
|
||||
}, DEBOUNCE_TIMEOUT);
|
||||
|
||||
render() {
|
||||
const { encryption, t } = this.props;
|
||||
const {
|
||||
enabled,
|
||||
server_name,
|
||||
force_https,
|
||||
port_https,
|
||||
port_dns_over_tls,
|
||||
certificate_chain,
|
||||
private_key,
|
||||
} = encryption;
|
||||
|
||||
return (
|
||||
<div className="encryption">
|
||||
{encryption &&
|
||||
<Card
|
||||
title={t('encryption_title')}
|
||||
subtitle={t('encryption_desc')}
|
||||
bodyType="card-body box-body--settings"
|
||||
>
|
||||
<Form
|
||||
initialValues={{
|
||||
enabled,
|
||||
server_name,
|
||||
force_https,
|
||||
port_https,
|
||||
port_dns_over_tls,
|
||||
certificate_chain,
|
||||
private_key,
|
||||
}}
|
||||
onSubmit={this.handleFormSubmit}
|
||||
onChange={this.handleFormChange}
|
||||
setTlsConfig={this.props.setTlsConfig}
|
||||
{...this.props.encryption}
|
||||
/>
|
||||
</Card>
|
||||
}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Encryption.propTypes = {
|
||||
setTlsConfig: PropTypes.func.isRequired,
|
||||
validateTlsConfig: PropTypes.func.isRequired,
|
||||
encryption: PropTypes.object.isRequired,
|
||||
t: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
export default withNamespaces()(Encryption);
|
||||
@@ -1,4 +1,5 @@
|
||||
.form__group {
|
||||
position: relative;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
@@ -6,7 +7,11 @@
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.btn-standart {
|
||||
.form__group--settings:last-child {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.btn-standard {
|
||||
padding-left: 20px;
|
||||
padding-right: 20px;
|
||||
}
|
||||
@@ -18,3 +23,56 @@
|
||||
.form-control--textarea-large {
|
||||
min-height: 240px;
|
||||
}
|
||||
|
||||
.form__message {
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
.form__message--error {
|
||||
color: #cd201f;
|
||||
}
|
||||
|
||||
.interface__title {
|
||||
font-size: 13px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.interface__ip:after {
|
||||
content: ", ";
|
||||
}
|
||||
|
||||
.interface__ip:last-child:after {
|
||||
content: "";
|
||||
}
|
||||
|
||||
.dhcp {
|
||||
min-height: 450px;
|
||||
}
|
||||
|
||||
.form__desc {
|
||||
margin-top: 10px;
|
||||
font-size: 13px;
|
||||
color: rgba(74, 74, 74, 0.7);
|
||||
}
|
||||
|
||||
.form__desc--top {
|
||||
margin: 0 0 8px;
|
||||
}
|
||||
|
||||
.form__label--bold {
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.form__status {
|
||||
margin-top: 10px;
|
||||
font-size: 14px;
|
||||
line-height: 1.7;
|
||||
}
|
||||
|
||||
.encryption__list {
|
||||
padding-left: 0;
|
||||
}
|
||||
|
||||
.encryption__list li {
|
||||
list-style: inside;
|
||||
}
|
||||
|
||||
@@ -21,8 +21,8 @@ class Upstream extends Component {
|
||||
|
||||
render() {
|
||||
const testButtonClass = classnames({
|
||||
'btn btn-primary btn-standart mr-2': true,
|
||||
'btn btn-primary btn-standart mr-2 btn-loading': this.props.processingTestUpstream,
|
||||
'btn btn-primary btn-standard mr-2': true,
|
||||
'btn btn-primary btn-standard mr-2 btn-loading': this.props.processingTestUpstream,
|
||||
});
|
||||
const { t } = this.props;
|
||||
|
||||
@@ -49,7 +49,7 @@ class Upstream extends Component {
|
||||
<Trans>test_upstream_btn</Trans>
|
||||
</button>
|
||||
<button
|
||||
className="btn btn-success btn-standart"
|
||||
className="btn btn-success btn-standard"
|
||||
type="submit"
|
||||
onClick={this.handleSubmit}
|
||||
>
|
||||
@@ -73,6 +73,9 @@ class Upstream extends Component {
|
||||
<li>
|
||||
<code>tcp://1.1.1.1</code> - { t('example_upstream_tcp') }
|
||||
</li>
|
||||
<li>
|
||||
<code>sdns://...</code> - <span dangerouslySetInnerHTML={{ __html: t('example_upstream_sdns') }} />
|
||||
</li>
|
||||
</ol>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -2,6 +2,8 @@ import React, { Component, Fragment } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { withNamespaces, Trans } from 'react-i18next';
|
||||
import Upstream from './Upstream';
|
||||
import Dhcp from './Dhcp';
|
||||
import Encryption from './Encryption';
|
||||
import Checkbox from '../ui/Checkbox';
|
||||
import Loading from '../ui/Loading';
|
||||
import PageTitle from '../ui/PageTitle';
|
||||
@@ -34,6 +36,9 @@ class Settings extends Component {
|
||||
|
||||
componentDidMount() {
|
||||
this.props.initSettings(this.settings);
|
||||
this.props.getDhcpStatus();
|
||||
this.props.getDhcpInterfaces();
|
||||
this.props.getTlsStatus();
|
||||
}
|
||||
|
||||
handleUpstreamChange = (value) => {
|
||||
@@ -92,6 +97,18 @@ class Settings extends Component {
|
||||
handleUpstreamSubmit={this.handleUpstreamSubmit}
|
||||
handleUpstreamTest={this.handleUpstreamTest}
|
||||
/>
|
||||
<Encryption
|
||||
encryption={this.props.encryption}
|
||||
setTlsConfig={this.props.setTlsConfig}
|
||||
validateTlsConfig={this.props.validateTlsConfig}
|
||||
/>
|
||||
<Dhcp
|
||||
dhcp={this.props.dhcp}
|
||||
toggleDhcp={this.props.toggleDhcp}
|
||||
getDhcpStatus={this.props.getDhcpStatus}
|
||||
findActiveDhcp={this.props.findActiveDhcp}
|
||||
setDhcpConfig={this.props.setDhcpConfig}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -22,6 +22,11 @@
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.checkbox--form .checkbox__label:before {
|
||||
top: 2px;
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
.checkbox__label {
|
||||
position: relative;
|
||||
display: flex;
|
||||
@@ -68,14 +73,19 @@
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.checkbox__input:checked+.checkbox__label:before {
|
||||
.checkbox__input:checked + .checkbox__label:before {
|
||||
background-image: url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAxMi4zIDkuMiIgZmlsbD0ibm9uZSIgc3Ryb2tlPSIjMDAwIiBzdHJva2UtbGluZWNhcD0icm91bmQiPjxwYXRoIGQ9Ik0xMS44IDAuNUw1LjMgOC41IDAuNSA0LjIiLz48L3N2Zz4=);
|
||||
}
|
||||
|
||||
.checkbox__input:focus+.checkbox__label:before {
|
||||
.checkbox__input:focus + .checkbox__label:before {
|
||||
box-shadow: 0 0 1px 1px rgba(74, 74, 74, 0.32);
|
||||
}
|
||||
|
||||
.checkbox__input:disabled + .checkbox__label {
|
||||
opacity: 0.6;
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.checkbox__label-text {
|
||||
max-width: 515px;
|
||||
line-height: 1.5;
|
||||
|
||||
43
client/src/components/ui/EncryptionTopline.js
Normal file
43
client/src/components/ui/EncryptionTopline.js
Normal file
@@ -0,0 +1,43 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { Trans, withNamespaces } from 'react-i18next';
|
||||
import isAfter from 'date-fns/is_after';
|
||||
import addDays from 'date-fns/add_days';
|
||||
|
||||
import Topline from './Topline';
|
||||
import { EMPTY_DATE } from '../../helpers/constants';
|
||||
|
||||
const EncryptionTopline = (props) => {
|
||||
if (props.notAfter === EMPTY_DATE) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const isAboutExpire = isAfter(addDays(Date.now(), 30), props.notAfter);
|
||||
const isExpired = isAfter(Date.now(), props.notAfter);
|
||||
|
||||
if (isExpired) {
|
||||
return (
|
||||
<Topline type="danger">
|
||||
<Trans components={[<a href="#settings" key="0">link</a>]}>
|
||||
topline_expired_certificate
|
||||
</Trans>
|
||||
</Topline>
|
||||
);
|
||||
} else if (isAboutExpire) {
|
||||
return (
|
||||
<Topline type="warning">
|
||||
<Trans components={[<a href="#settings" key="0">link</a>]}>
|
||||
topline_expiring_certificate
|
||||
</Trans>
|
||||
</Topline>
|
||||
);
|
||||
}
|
||||
|
||||
return false;
|
||||
};
|
||||
|
||||
EncryptionTopline.propTypes = {
|
||||
notAfter: PropTypes.string.isRequired,
|
||||
};
|
||||
|
||||
export default withNamespaces()(EncryptionTopline);
|
||||
@@ -23,7 +23,7 @@ class Footer extends Component {
|
||||
<div className="footer__row">
|
||||
<div className="footer__column">
|
||||
<div className="footer__copyright">
|
||||
<Trans>copyright</Trans> © {this.getYear()} <a href="https://adguard.com/">AdGuard</a>
|
||||
<Trans>copyright</Trans> © {this.getYear()} <a href="https://adguard.com/">AdGuard</a>
|
||||
</div>
|
||||
</div>
|
||||
<div className="footer__column">
|
||||
|
||||
27
client/src/components/ui/Icons.js
Normal file
27
client/src/components/ui/Icons.js
Normal file
File diff suppressed because one or more lines are too long
@@ -55,6 +55,7 @@ class Modal extends Component {
|
||||
isOpen,
|
||||
title,
|
||||
inputDescription,
|
||||
processingAddFilter,
|
||||
} = this.props;
|
||||
const { isUrlValid, url, name } = this.state;
|
||||
const inputUrlClass = classnames({
|
||||
@@ -71,8 +72,8 @@ class Modal extends Component {
|
||||
if (!this.props.isFilterAdded) {
|
||||
return (
|
||||
<React.Fragment>
|
||||
<input type="text" className={inputNameClass} placeholder={ this.props.t('enter_name_hint') } onChange={this.handleNameChange} />
|
||||
<input type="text" className={inputUrlClass} placeholder={ this.props.t('enter_url_hint') } onChange={this.handleUrlChange} />
|
||||
<input type="text" className={inputNameClass} placeholder={this.props.t('enter_name_hint')} onChange={this.handleNameChange} />
|
||||
<input type="text" className={inputUrlClass} placeholder={this.props.t('enter_url_hint')} onChange={this.handleUrlChange} />
|
||||
{inputDescription &&
|
||||
<div className="description">
|
||||
{inputDescription}
|
||||
@@ -82,7 +83,7 @@ class Modal extends Component {
|
||||
}
|
||||
return (
|
||||
<div className="description">
|
||||
<Trans>Url added successfully</Trans>
|
||||
<Trans>url_added_successfully</Trans>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -93,7 +94,7 @@ class Modal extends Component {
|
||||
<ReactModal
|
||||
className="Modal__Bootstrap modal-dialog modal-dialog-centered"
|
||||
closeTimeoutMS={0}
|
||||
isOpen={ isOpen }
|
||||
isOpen={isOpen}
|
||||
onRequestClose={this.closeModal}
|
||||
>
|
||||
<div className="modal-content">
|
||||
@@ -106,14 +107,26 @@ class Modal extends Component {
|
||||
</button>
|
||||
</div>
|
||||
<div className="modal-body">
|
||||
{ renderBody()}
|
||||
{renderBody()}
|
||||
</div>
|
||||
{
|
||||
!this.props.isFilterAdded &&
|
||||
<div className="modal-footer">
|
||||
<button type="button" className="btn btn-secondary" onClick={this.closeModal}><Trans>cancel_btn</Trans></button>
|
||||
<button type="button" className="btn btn-success" onClick={this.handleNext} disabled={isValidForSubmit}><Trans>add_filter_btn</Trans></button>
|
||||
</div>
|
||||
{!this.props.isFilterAdded &&
|
||||
<div className="modal-footer">
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-secondary"
|
||||
onClick={this.closeModal}
|
||||
>
|
||||
<Trans>cancel_btn</Trans>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-success"
|
||||
onClick={this.handleNext}
|
||||
disabled={isValidForSubmit || processingAddFilter}
|
||||
>
|
||||
<Trans>add_filter_btn</Trans>
|
||||
</button>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</ReactModal>
|
||||
@@ -128,6 +141,7 @@ Modal.propTypes = {
|
||||
inputDescription: PropTypes.string,
|
||||
addFilter: PropTypes.func.isRequired,
|
||||
isFilterAdded: PropTypes.bool,
|
||||
processingAddFilter: PropTypes.bool,
|
||||
t: PropTypes.func,
|
||||
};
|
||||
|
||||
|
||||
41
client/src/components/ui/Tab.js
Normal file
41
client/src/components/ui/Tab.js
Normal file
@@ -0,0 +1,41 @@
|
||||
import React, { Component } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import classnames from 'classnames';
|
||||
|
||||
class Tab extends Component {
|
||||
handleClick = () => {
|
||||
this.props.onClick(this.props.label);
|
||||
}
|
||||
|
||||
render() {
|
||||
const {
|
||||
activeTab,
|
||||
label,
|
||||
} = this.props;
|
||||
|
||||
const tabClass = classnames({
|
||||
tab__control: true,
|
||||
'tab__control--active': activeTab === label,
|
||||
});
|
||||
|
||||
return (
|
||||
<div
|
||||
className={tabClass}
|
||||
onClick={this.handleClick}
|
||||
>
|
||||
<svg className="tab__icon">
|
||||
<use xlinkHref={`#${label.toLowerCase()}`} />
|
||||
</svg>
|
||||
{label}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Tab.propTypes = {
|
||||
activeTab: PropTypes.string.isRequired,
|
||||
label: PropTypes.string.isRequired,
|
||||
onClick: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
export default Tab;
|
||||
@@ -3783,7 +3783,7 @@ tbody.collapse.show {
|
||||
line-height: 1.5;
|
||||
color: #495057;
|
||||
vertical-align: middle;
|
||||
background: #fff url("data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 10 5'%3E%3Cpath fill='#999' d='M0 0L10 0L5 5L0 0'/%3E%3C/svg%3E") no-repeat right 0.75rem center;
|
||||
background: #fff url("data:image/svg+xml;base64,PHN2ZyB4bWxucz0naHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmcnIHZpZXdCb3g9JzAgMCAxMCA1Jz48cGF0aCBmaWxsPScjOTk5JyBkPSdNMCAwTDEwIDBMNSA1TDAgMCcvPjwvc3ZnPg==") no-repeat right 0.75rem center;
|
||||
background-size: 8px 10px;
|
||||
border: 1px solid rgba(0, 40, 100, 0.12);
|
||||
border-radius: 3px;
|
||||
|
||||
51
client/src/components/ui/Tabs.css
Normal file
51
client/src/components/ui/Tabs.css
Normal file
@@ -0,0 +1,51 @@
|
||||
.tabs__controls {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 15px;
|
||||
padding: 15px 0;
|
||||
border-bottom: 1px solid #e8e8e8;
|
||||
}
|
||||
|
||||
.tab__control {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
min-width: 70px;
|
||||
font-size: 13px;
|
||||
color: #555555;
|
||||
cursor: pointer;
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
.tab__control:hover,
|
||||
.tab__control:focus {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.tab__control--active {
|
||||
font-weight: 700;
|
||||
color: #4a4a4a;
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.tab__title {
|
||||
margin-bottom: 10px;
|
||||
font-size: 16px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.tab__icon {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
margin-bottom: 6px;
|
||||
fill: #4a4a4a;
|
||||
}
|
||||
|
||||
.tab__text {
|
||||
line-height: 1.7;
|
||||
}
|
||||
|
||||
.tab__text li,
|
||||
.tab__text p {
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
59
client/src/components/ui/Tabs.js
Normal file
59
client/src/components/ui/Tabs.js
Normal file
@@ -0,0 +1,59 @@
|
||||
import React, { Component } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import Tab from './Tab';
|
||||
import './Tabs.css';
|
||||
|
||||
class Tabs extends Component {
|
||||
state = {
|
||||
activeTab: this.props.children[0].props.label,
|
||||
};
|
||||
|
||||
onClickTabControl = (tab) => {
|
||||
this.setState({ activeTab: tab });
|
||||
}
|
||||
|
||||
render() {
|
||||
const {
|
||||
props: {
|
||||
children,
|
||||
},
|
||||
state: {
|
||||
activeTab,
|
||||
},
|
||||
} = this;
|
||||
|
||||
return (
|
||||
<div className="tabs">
|
||||
<div className="tabs__controls">
|
||||
{children.map((child) => {
|
||||
const { label } = child.props;
|
||||
|
||||
return (
|
||||
<Tab
|
||||
key={label}
|
||||
label={label}
|
||||
activeTab={activeTab}
|
||||
onClick={this.onClickTabControl}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
<div className="tabs__content">
|
||||
{children.map((child) => {
|
||||
if (child.props.label !== activeTab) {
|
||||
return false;
|
||||
}
|
||||
return child.props.children;
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Tabs.propTypes = {
|
||||
children: PropTypes.array.isRequired,
|
||||
};
|
||||
|
||||
export default Tabs;
|
||||
@@ -1,4 +1,4 @@
|
||||
.update {
|
||||
.topline {
|
||||
position: relative;
|
||||
z-index: 102;
|
||||
margin-bottom: 0;
|
||||
19
client/src/components/ui/Topline.js
Normal file
19
client/src/components/ui/Topline.js
Normal file
@@ -0,0 +1,19 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import './Topline.css';
|
||||
|
||||
const Topline = props => (
|
||||
<div className={`alert alert-${props.type} topline`}>
|
||||
<div className="container">
|
||||
{props.children}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
Topline.propTypes = {
|
||||
children: PropTypes.node.isRequired,
|
||||
type: PropTypes.string.isRequired,
|
||||
};
|
||||
|
||||
export default Topline;
|
||||
@@ -1,19 +0,0 @@
|
||||
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;
|
||||
27
client/src/components/ui/UpdateTopline.js
Normal file
27
client/src/components/ui/UpdateTopline.js
Normal file
@@ -0,0 +1,27 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { Trans, withNamespaces } from 'react-i18next';
|
||||
|
||||
import Topline from './Topline';
|
||||
|
||||
const UpdateTopline = props => (
|
||||
<Topline type="info">
|
||||
<Trans
|
||||
values={{ version: props.version }}
|
||||
components={[
|
||||
<a href={props.url} target="_blank" rel="noopener noreferrer" key="0">
|
||||
Click here
|
||||
</a>,
|
||||
]}
|
||||
>
|
||||
update_announcement
|
||||
</Trans>
|
||||
</Topline>
|
||||
);
|
||||
|
||||
UpdateTopline.propTypes = {
|
||||
version: PropTypes.string.isRequired,
|
||||
url: PropTypes.string.isRequired,
|
||||
};
|
||||
|
||||
export default withNamespaces()(UpdateTopline);
|
||||
|
Before Width: | Height: | Size: 4.0 KiB After Width: | Height: | Size: 4.0 KiB |
@@ -3,8 +3,8 @@ import * as actionCreators from '../actions';
|
||||
import App from '../components/App';
|
||||
|
||||
const mapStateToProps = (state) => {
|
||||
const { dashboard } = state;
|
||||
const props = { dashboard };
|
||||
const { dashboard, encryption } = state;
|
||||
const props = { dashboard, encryption };
|
||||
return props;
|
||||
};
|
||||
|
||||
|
||||
@@ -1,10 +1,37 @@
|
||||
import { connect } from 'react-redux';
|
||||
import { initSettings, toggleSetting, handleUpstreamChange, setUpstream, testUpstream, addErrorToast } from '../actions';
|
||||
import {
|
||||
initSettings,
|
||||
toggleSetting,
|
||||
handleUpstreamChange,
|
||||
setUpstream,
|
||||
testUpstream,
|
||||
addErrorToast,
|
||||
toggleDhcp,
|
||||
getDhcpStatus,
|
||||
getDhcpInterfaces,
|
||||
setDhcpConfig,
|
||||
findActiveDhcp,
|
||||
} from '../actions';
|
||||
import {
|
||||
getTlsStatus,
|
||||
setTlsConfig,
|
||||
validateTlsConfig,
|
||||
} from '../actions/encryption';
|
||||
import Settings from '../components/Settings';
|
||||
|
||||
const mapStateToProps = (state) => {
|
||||
const { settings, dashboard } = state;
|
||||
const props = { settings, dashboard };
|
||||
const {
|
||||
settings,
|
||||
dashboard,
|
||||
dhcp,
|
||||
encryption,
|
||||
} = state;
|
||||
const props = {
|
||||
settings,
|
||||
dashboard,
|
||||
dhcp,
|
||||
encryption,
|
||||
};
|
||||
return props;
|
||||
};
|
||||
|
||||
@@ -15,6 +42,14 @@ const mapDispatchToProps = {
|
||||
setUpstream,
|
||||
testUpstream,
|
||||
addErrorToast,
|
||||
toggleDhcp,
|
||||
getDhcpStatus,
|
||||
getDhcpInterfaces,
|
||||
setDhcpConfig,
|
||||
findActiveDhcp,
|
||||
getTlsStatus,
|
||||
setTlsConfig,
|
||||
validateTlsConfig,
|
||||
};
|
||||
|
||||
export default connect(
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
export const R_URL_REQUIRES_PROTOCOL = /^https?:\/\/\w[\w_\-.]*\.[a-z]{2,8}[^\s]*$/;
|
||||
export const R_IPV4 = /^(?:(?:^|\.)(?:2(?:5[0-5]|[0-4]\d)|1?\d?\d)){4}$/g;
|
||||
|
||||
export const STATS_NAMES = {
|
||||
avg_processing_time: 'average_processing_time',
|
||||
@@ -54,4 +55,95 @@ export const LANGUAGES = [
|
||||
key: 'ja',
|
||||
name: '日本語',
|
||||
},
|
||||
{
|
||||
key: 'zh-tw',
|
||||
name: '正體中文',
|
||||
},
|
||||
];
|
||||
|
||||
export const INSTALL_FIRST_STEP = 1;
|
||||
export const INSTALL_TOTAL_STEPS = 5;
|
||||
|
||||
export const SETTINGS_NAMES = {
|
||||
filtering: 'filtering',
|
||||
safebrowsing: 'safebrowsing',
|
||||
parental: 'parental',
|
||||
safesearch: 'safesearch',
|
||||
};
|
||||
|
||||
export const STANDARD_DNS_PORT = 53;
|
||||
export const STANDARD_WEB_PORT = 80;
|
||||
export const STANDARD_HTTPS_PORT = 443;
|
||||
|
||||
export const EMPTY_DATE = '0001-01-01T00:00:00Z';
|
||||
|
||||
export const DEBOUNCE_TIMEOUT = 300;
|
||||
export const CHECK_TIMEOUT = 1000;
|
||||
export const STOP_TIMEOUT = 10000;
|
||||
|
||||
export const UNSAFE_PORTS = [
|
||||
1,
|
||||
7,
|
||||
9,
|
||||
11,
|
||||
13,
|
||||
15,
|
||||
17,
|
||||
19,
|
||||
20,
|
||||
21,
|
||||
22,
|
||||
23,
|
||||
25,
|
||||
37,
|
||||
42,
|
||||
43,
|
||||
53,
|
||||
77,
|
||||
79,
|
||||
87,
|
||||
95,
|
||||
101,
|
||||
102,
|
||||
103,
|
||||
104,
|
||||
109,
|
||||
110,
|
||||
111,
|
||||
113,
|
||||
115,
|
||||
117,
|
||||
119,
|
||||
123,
|
||||
135,
|
||||
139,
|
||||
143,
|
||||
179,
|
||||
389,
|
||||
465,
|
||||
512,
|
||||
513,
|
||||
514,
|
||||
515,
|
||||
526,
|
||||
530,
|
||||
531,
|
||||
532,
|
||||
540,
|
||||
556,
|
||||
563,
|
||||
587,
|
||||
601,
|
||||
636,
|
||||
993,
|
||||
995,
|
||||
2049,
|
||||
3659,
|
||||
4045,
|
||||
6000,
|
||||
6665,
|
||||
6666,
|
||||
6667,
|
||||
6668,
|
||||
6669,
|
||||
];
|
||||
|
||||
79
client/src/helpers/form.js
Normal file
79
client/src/helpers/form.js
Normal file
@@ -0,0 +1,79 @@
|
||||
import React, { Fragment } from 'react';
|
||||
import { Trans } from 'react-i18next';
|
||||
|
||||
import { R_IPV4, UNSAFE_PORTS } from '../helpers/constants';
|
||||
|
||||
export const renderField = ({
|
||||
input, id, className, placeholder, type, disabled, meta: { touched, error },
|
||||
}) => (
|
||||
<Fragment>
|
||||
<input
|
||||
{...input}
|
||||
id={id}
|
||||
placeholder={placeholder}
|
||||
type={type}
|
||||
className={className}
|
||||
disabled={disabled}
|
||||
/>
|
||||
{!disabled && touched && (error && <span className="form__message form__message--error">{error}</span>)}
|
||||
</Fragment>
|
||||
);
|
||||
|
||||
export const renderSelectField = ({
|
||||
input, placeholder, disabled, meta: { touched, error },
|
||||
}) => (
|
||||
<Fragment>
|
||||
<label className="checkbox checkbox--form">
|
||||
<span className="checkbox__marker"/>
|
||||
<input
|
||||
{...input}
|
||||
type="checkbox"
|
||||
className="checkbox__input"
|
||||
disabled={disabled}
|
||||
/>
|
||||
<span className="checkbox__label">
|
||||
<span className="checkbox__label-text">
|
||||
<span className="checkbox__label-title">{placeholder}</span>
|
||||
</span>
|
||||
</span>
|
||||
</label>
|
||||
{!disabled && touched && (error && <span className="form__message form__message--error">{error}</span>)}
|
||||
</Fragment>
|
||||
);
|
||||
|
||||
export const required = (value) => {
|
||||
if (value || value === 0) {
|
||||
return false;
|
||||
}
|
||||
return <Trans>form_error_required</Trans>;
|
||||
};
|
||||
|
||||
export const ipv4 = (value) => {
|
||||
if (value && !new RegExp(R_IPV4).test(value)) {
|
||||
return <Trans>form_error_ip_format</Trans>;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
export const isPositive = (value) => {
|
||||
if ((value || value === 0) && (value <= 0)) {
|
||||
return <Trans>form_error_positive</Trans>;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
export const port = (value) => {
|
||||
if ((value || value === 0) && (value < 80 || value > 65535)) {
|
||||
return <Trans>form_error_port_range</Trans>;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
export const isSafePort = (value) => {
|
||||
if (UNSAFE_PORTS.includes(value)) {
|
||||
return <Trans>form_error_port_unsafe</Trans>;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
export const toNumber = value => value && parseInt(value, 10);
|
||||
@@ -3,8 +3,15 @@ import dateFormat from 'date-fns/format';
|
||||
import subHours from 'date-fns/sub_hours';
|
||||
import addHours from 'date-fns/add_hours';
|
||||
import round from 'lodash/round';
|
||||
import axios from 'axios';
|
||||
|
||||
import { STATS_NAMES } from './constants';
|
||||
import {
|
||||
STATS_NAMES,
|
||||
STANDARD_DNS_PORT,
|
||||
STANDARD_WEB_PORT,
|
||||
STANDARD_HTTPS_PORT,
|
||||
CHECK_TIMEOUT,
|
||||
} from './constants';
|
||||
|
||||
export const formatTime = (time) => {
|
||||
const parsedTime = dateParse(time);
|
||||
@@ -85,3 +92,112 @@ export const getPercent = (amount, number) => {
|
||||
};
|
||||
|
||||
export const captitalizeWords = text => text.split(/[ -_]/g).map(str => str.charAt(0).toUpperCase() + str.substr(1)).join(' ');
|
||||
|
||||
export const getInterfaceIp = (option) => {
|
||||
const onlyIPv6 = option.ip_addresses.every(ip => ip.includes(':'));
|
||||
let interfaceIP = option.ip_addresses[0];
|
||||
|
||||
if (!onlyIPv6) {
|
||||
option.ip_addresses.forEach((ip) => {
|
||||
if (!ip.includes(':')) {
|
||||
interfaceIP = ip;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return interfaceIP;
|
||||
};
|
||||
|
||||
export const getIpList = (interfaces) => {
|
||||
let list = [];
|
||||
|
||||
Object.keys(interfaces).forEach((item) => {
|
||||
list = [...list, ...interfaces[item].ip_addresses];
|
||||
});
|
||||
|
||||
return list.sort();
|
||||
};
|
||||
|
||||
export const getDnsAddress = (ip, port = '') => {
|
||||
const isStandardDnsPort = port === STANDARD_DNS_PORT;
|
||||
let address = ip;
|
||||
|
||||
if (port) {
|
||||
if (ip.includes(':') && !isStandardDnsPort) {
|
||||
address = `[${ip}]:${port}`;
|
||||
} else if (!isStandardDnsPort) {
|
||||
address = `${ip}:${port}`;
|
||||
}
|
||||
}
|
||||
|
||||
return address;
|
||||
};
|
||||
|
||||
export const getWebAddress = (ip, port = '') => {
|
||||
const isStandardWebPort = port === STANDARD_WEB_PORT;
|
||||
let address = `http://${ip}`;
|
||||
|
||||
if (port) {
|
||||
if (ip.includes(':') && !isStandardWebPort) {
|
||||
address = `http://[${ip}]:${port}`;
|
||||
} else if (!isStandardWebPort) {
|
||||
address = `http://${ip}:${port}`;
|
||||
}
|
||||
}
|
||||
|
||||
return address;
|
||||
};
|
||||
|
||||
export const checkRedirect = (url, attempts) => {
|
||||
let count = attempts || 1;
|
||||
|
||||
if (count > 10) {
|
||||
window.location.replace(url);
|
||||
return false;
|
||||
}
|
||||
|
||||
const rmTimeout = t => t && clearTimeout(t);
|
||||
const setRecursiveTimeout = (time, ...args) => setTimeout(
|
||||
checkRedirect,
|
||||
time,
|
||||
...args,
|
||||
);
|
||||
|
||||
let timeout;
|
||||
|
||||
axios.get(url)
|
||||
.then((response) => {
|
||||
rmTimeout(timeout);
|
||||
if (response) {
|
||||
window.location.replace(url);
|
||||
return;
|
||||
}
|
||||
timeout = setRecursiveTimeout(CHECK_TIMEOUT, url, count += 1);
|
||||
})
|
||||
.catch((error) => {
|
||||
rmTimeout(timeout);
|
||||
if (error.response) {
|
||||
window.location.replace(url);
|
||||
return;
|
||||
}
|
||||
timeout = setRecursiveTimeout(CHECK_TIMEOUT, url, count += 1);
|
||||
});
|
||||
|
||||
return false;
|
||||
};
|
||||
|
||||
export const redirectToCurrentProtocol = (values, httpPort = 80) => {
|
||||
const {
|
||||
protocol, hostname, hash, port,
|
||||
} = window.location;
|
||||
const { enabled, port_https } = values;
|
||||
const httpsPort = port_https !== STANDARD_HTTPS_PORT ? `:${port_https}` : '';
|
||||
|
||||
if (protocol !== 'https:' && enabled && port_https) {
|
||||
checkRedirect(`https://${hostname}${httpsPort}/${hash}`);
|
||||
} else if (protocol === 'https:' && enabled && port_https && port_https !== parseInt(port, 10)) {
|
||||
checkRedirect(`https://${hostname}${httpsPort}/${hash}`);
|
||||
} else if (protocol === 'https:' && (!enabled || !port_https)) {
|
||||
window.location.replace(`http://${hostname}:${httpPort}/${hash}`);
|
||||
}
|
||||
};
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -11,6 +11,7 @@ import fr from './__locales/fr.json';
|
||||
import ja from './__locales/ja.json';
|
||||
import sv from './__locales/sv.json';
|
||||
import ptBR from './__locales/pt-br.json';
|
||||
import zhTW from './__locales/zh-tw.json';
|
||||
|
||||
const resources = {
|
||||
en: {
|
||||
@@ -37,6 +38,9 @@ const resources = {
|
||||
'pt-BR': {
|
||||
translation: ptBR,
|
||||
},
|
||||
'zh-TW': {
|
||||
translation: zhTW,
|
||||
},
|
||||
};
|
||||
|
||||
i18n
|
||||
|
||||
60
client/src/install/Setup/AddressList.js
Normal file
60
client/src/install/Setup/AddressList.js
Normal file
@@ -0,0 +1,60 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { getIpList, getDnsAddress, getWebAddress } from '../../helpers/helpers';
|
||||
|
||||
const AddressList = (props) => {
|
||||
let webAddress = getWebAddress(props.address, props.port);
|
||||
let dnsAddress = getDnsAddress(props.address, props.port);
|
||||
|
||||
if (props.address === '0.0.0.0') {
|
||||
return getIpList(props.interfaces).map((ip) => {
|
||||
webAddress = getWebAddress(ip, props.port);
|
||||
dnsAddress = getDnsAddress(ip, props.port);
|
||||
|
||||
if (props.isDns) {
|
||||
return (
|
||||
<li key={ip}>
|
||||
<strong>
|
||||
{dnsAddress}
|
||||
</strong>
|
||||
</li>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<li key={ip}>
|
||||
<a href={webAddress}>
|
||||
{webAddress}
|
||||
</a>
|
||||
</li>
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
if (props.isDns) {
|
||||
return (
|
||||
<strong>
|
||||
{dnsAddress}
|
||||
</strong>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<a href={webAddress}>
|
||||
{webAddress}
|
||||
</a>
|
||||
);
|
||||
};
|
||||
|
||||
AddressList.propTypes = {
|
||||
interfaces: PropTypes.object.isRequired,
|
||||
address: PropTypes.string.isRequired,
|
||||
port: PropTypes.oneOfType([
|
||||
PropTypes.string,
|
||||
PropTypes.number,
|
||||
]),
|
||||
isDns: PropTypes.bool,
|
||||
};
|
||||
|
||||
export default AddressList;
|
||||
108
client/src/install/Setup/Auth.js
Normal file
108
client/src/install/Setup/Auth.js
Normal file
@@ -0,0 +1,108 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { Field, reduxForm } from 'redux-form';
|
||||
import { withNamespaces, Trans } from 'react-i18next';
|
||||
import flow from 'lodash/flow';
|
||||
|
||||
import i18n from '../../i18n';
|
||||
import Controls from './Controls';
|
||||
import renderField from './renderField';
|
||||
|
||||
const required = (value) => {
|
||||
if (value || value === 0) {
|
||||
return false;
|
||||
}
|
||||
return <Trans>form_error_required</Trans>;
|
||||
};
|
||||
|
||||
const validate = (values) => {
|
||||
const errors = {};
|
||||
|
||||
if (values.confirm_password !== values.password) {
|
||||
errors.confirm_password = i18n.t('form_error_password');
|
||||
}
|
||||
|
||||
return errors;
|
||||
};
|
||||
|
||||
const Auth = (props) => {
|
||||
const {
|
||||
handleSubmit,
|
||||
pristine,
|
||||
invalid,
|
||||
t,
|
||||
} = props;
|
||||
|
||||
return (
|
||||
<form className="setup__step" onSubmit={handleSubmit}>
|
||||
<div className="setup__group">
|
||||
<div className="setup__subtitle">
|
||||
<Trans>install_auth_title</Trans>
|
||||
</div>
|
||||
<p className="setup__desc">
|
||||
<Trans>install_auth_desc</Trans>
|
||||
</p>
|
||||
<div className="form-group">
|
||||
<label>
|
||||
<Trans>install_auth_username</Trans>
|
||||
</label>
|
||||
<Field
|
||||
name="username"
|
||||
component={renderField}
|
||||
type="text"
|
||||
className="form-control"
|
||||
placeholder={ t('install_auth_username_enter') }
|
||||
validate={[required]}
|
||||
autoComplete="username"
|
||||
/>
|
||||
</div>
|
||||
<div className="form-group">
|
||||
<label>
|
||||
<Trans>install_auth_password</Trans>
|
||||
</label>
|
||||
<Field
|
||||
name="password"
|
||||
component={renderField}
|
||||
type="password"
|
||||
className="form-control"
|
||||
placeholder={ t('install_auth_password_enter') }
|
||||
validate={[required]}
|
||||
autoComplete="new-password"
|
||||
/>
|
||||
</div>
|
||||
<div className="form-group">
|
||||
<label>
|
||||
<Trans>install_auth_confirm</Trans>
|
||||
</label>
|
||||
<Field
|
||||
name="confirm_password"
|
||||
component={renderField}
|
||||
type="password"
|
||||
className="form-control"
|
||||
placeholder={ t('install_auth_confirm') }
|
||||
validate={[required]}
|
||||
autoComplete="new-password"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<Controls pristine={pristine} invalid={invalid} />
|
||||
</form>
|
||||
);
|
||||
};
|
||||
|
||||
Auth.propTypes = {
|
||||
handleSubmit: PropTypes.func.isRequired,
|
||||
pristine: PropTypes.bool.isRequired,
|
||||
invalid: PropTypes.bool.isRequired,
|
||||
t: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
export default flow([
|
||||
withNamespaces(),
|
||||
reduxForm({
|
||||
form: 'install',
|
||||
destroyOnUnmount: false,
|
||||
forceUnregisterOnUnmount: true,
|
||||
validate,
|
||||
}),
|
||||
])(Auth);
|
||||
113
client/src/install/Setup/Controls.js
Normal file
113
client/src/install/Setup/Controls.js
Normal file
@@ -0,0 +1,113 @@
|
||||
import React, { Component } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import PropTypes from 'prop-types';
|
||||
import { Trans } from 'react-i18next';
|
||||
|
||||
import * as actionCreators from '../../actions/install';
|
||||
|
||||
class Controls extends Component {
|
||||
renderPrevButton(step) {
|
||||
switch (step) {
|
||||
case 2:
|
||||
case 3:
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-secondary btn-lg setup__button"
|
||||
onClick={this.props.prevStep}
|
||||
>
|
||||
<Trans>back</Trans>
|
||||
</button>
|
||||
);
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
renderNextButton(step) {
|
||||
switch (step) {
|
||||
case 1:
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-success btn-lg setup__button"
|
||||
onClick={this.props.nextStep}
|
||||
>
|
||||
<Trans>get_started</Trans>
|
||||
</button>
|
||||
);
|
||||
case 2:
|
||||
case 3:
|
||||
return (
|
||||
<button
|
||||
type="submit"
|
||||
className="btn btn-success btn-lg setup__button"
|
||||
disabled={
|
||||
this.props.invalid
|
||||
|| this.props.pristine
|
||||
|| this.props.install.processingSubmit
|
||||
}
|
||||
>
|
||||
<Trans>next</Trans>
|
||||
</button>
|
||||
);
|
||||
case 4:
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-success btn-lg setup__button"
|
||||
onClick={this.props.nextStep}
|
||||
>
|
||||
<Trans>next</Trans>
|
||||
</button>
|
||||
);
|
||||
case 5:
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-success btn-lg setup__button"
|
||||
onClick={() => this.props.openDashboard(this.props.address)}
|
||||
>
|
||||
<Trans>open_dashboard</Trans>
|
||||
</button>
|
||||
);
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
const { install } = this.props;
|
||||
|
||||
return (
|
||||
<div className="setup__nav">
|
||||
<div className="btn-list">
|
||||
{this.renderPrevButton(install.step)}
|
||||
{this.renderNextButton(install.step)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Controls.propTypes = {
|
||||
install: PropTypes.object.isRequired,
|
||||
nextStep: PropTypes.func,
|
||||
prevStep: PropTypes.func,
|
||||
openDashboard: PropTypes.func,
|
||||
submitting: PropTypes.bool,
|
||||
invalid: PropTypes.bool,
|
||||
pristine: PropTypes.bool,
|
||||
address: PropTypes.string,
|
||||
};
|
||||
|
||||
const mapStateToProps = (state) => {
|
||||
const { install } = state;
|
||||
const props = { install };
|
||||
return props;
|
||||
};
|
||||
|
||||
export default connect(
|
||||
mapStateToProps,
|
||||
actionCreators,
|
||||
)(Controls);
|
||||
134
client/src/install/Setup/Devices.js
Normal file
134
client/src/install/Setup/Devices.js
Normal file
@@ -0,0 +1,134 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { connect } from 'react-redux';
|
||||
import { reduxForm, formValueSelector } from 'redux-form';
|
||||
import { Trans, withNamespaces } from 'react-i18next';
|
||||
import flow from 'lodash/flow';
|
||||
|
||||
import Tabs from '../../components/ui/Tabs';
|
||||
import Icons from '../../components/ui/Icons';
|
||||
import Controls from './Controls';
|
||||
import AddressList from './AddressList';
|
||||
|
||||
let Devices = props => (
|
||||
<div className="setup__step">
|
||||
<div className="setup__group">
|
||||
<div className="setup__subtitle">
|
||||
<Trans>install_devices_title</Trans>
|
||||
</div>
|
||||
<div className="setup__desc">
|
||||
<Trans>install_devices_desc</Trans>
|
||||
<div className="mt-1">
|
||||
<Trans>install_devices_address</Trans>:
|
||||
</div>
|
||||
<div className="mt-1">
|
||||
<AddressList
|
||||
interfaces={props.interfaces}
|
||||
address={props.dnsIp}
|
||||
port={props.dnsPort}
|
||||
isDns={true}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<Icons />
|
||||
<Tabs>
|
||||
<div label="Router">
|
||||
<div className="tab__title">
|
||||
<Trans>install_devices_router</Trans>
|
||||
</div>
|
||||
<div className="tab__text">
|
||||
<p><Trans>install_devices_router_desc</Trans></p>
|
||||
<ol>
|
||||
<li><Trans>install_devices_router_list_1</Trans></li>
|
||||
<li><Trans>install_devices_router_list_2</Trans></li>
|
||||
<li><Trans>install_devices_router_list_3</Trans></li>
|
||||
</ol>
|
||||
</div>
|
||||
</div>
|
||||
<div label="Windows">
|
||||
<div className="tab__title">
|
||||
Windows
|
||||
</div>
|
||||
<div className="tab__text">
|
||||
<ol>
|
||||
<li><Trans>install_devices_windows_list_1</Trans></li>
|
||||
<li><Trans>install_devices_windows_list_2</Trans></li>
|
||||
<li><Trans>install_devices_windows_list_3</Trans></li>
|
||||
<li><Trans>install_devices_windows_list_4</Trans></li>
|
||||
<li><Trans>install_devices_windows_list_5</Trans></li>
|
||||
<li><Trans>install_devices_windows_list_6</Trans></li>
|
||||
</ol>
|
||||
</div>
|
||||
</div>
|
||||
<div label="macOS">
|
||||
<div className="tab__title">
|
||||
macOS
|
||||
</div>
|
||||
<div className="tab__text">
|
||||
<ol>
|
||||
<li><Trans>install_devices_macos_list_1</Trans></li>
|
||||
<li><Trans>install_devices_macos_list_2</Trans></li>
|
||||
<li><Trans>install_devices_macos_list_3</Trans></li>
|
||||
<li><Trans>install_devices_macos_list_4</Trans></li>
|
||||
</ol>
|
||||
</div>
|
||||
</div>
|
||||
<div label="Android">
|
||||
<div className="tab__title">
|
||||
Android
|
||||
</div>
|
||||
<div className="tab__text">
|
||||
<ol>
|
||||
<li><Trans>install_devices_android_list_1</Trans></li>
|
||||
<li><Trans>install_devices_android_list_2</Trans></li>
|
||||
<li><Trans>install_devices_android_list_3</Trans></li>
|
||||
<li><Trans>install_devices_android_list_4</Trans></li>
|
||||
<li><Trans>install_devices_android_list_5</Trans></li>
|
||||
</ol>
|
||||
</div>
|
||||
</div>
|
||||
<div label="iOS">
|
||||
<div className="tab__title">
|
||||
iOS
|
||||
</div>
|
||||
<div className="tab__text">
|
||||
<ol>
|
||||
<li><Trans>install_devices_ios_list_1</Trans></li>
|
||||
<li><Trans>install_devices_ios_list_2</Trans></li>
|
||||
<li><Trans>install_devices_ios_list_3</Trans></li>
|
||||
<li><Trans>install_devices_ios_list_4</Trans></li>
|
||||
</ol>
|
||||
</div>
|
||||
</div>
|
||||
</Tabs>
|
||||
</div>
|
||||
<Controls />
|
||||
</div>
|
||||
);
|
||||
|
||||
Devices.propTypes = {
|
||||
interfaces: PropTypes.object.isRequired,
|
||||
dnsIp: PropTypes.string.isRequired,
|
||||
dnsPort: PropTypes.number.isRequired,
|
||||
};
|
||||
|
||||
const selector = formValueSelector('install');
|
||||
|
||||
Devices = connect((state) => {
|
||||
const dnsIp = selector(state, 'dns.ip');
|
||||
const dnsPort = selector(state, 'dns.port');
|
||||
|
||||
return {
|
||||
dnsIp,
|
||||
dnsPort,
|
||||
};
|
||||
})(Devices);
|
||||
|
||||
export default flow([
|
||||
withNamespaces(),
|
||||
reduxForm({
|
||||
form: 'install',
|
||||
destroyOnUnmount: false,
|
||||
forceUnregisterOnUnmount: true,
|
||||
}),
|
||||
])(Devices);
|
||||
19
client/src/install/Setup/Greeting.js
Normal file
19
client/src/install/Setup/Greeting.js
Normal file
@@ -0,0 +1,19 @@
|
||||
import React from 'react';
|
||||
import { Trans, withNamespaces } from 'react-i18next';
|
||||
import Controls from './Controls';
|
||||
|
||||
const Greeting = () => (
|
||||
<div className="setup__step">
|
||||
<div className="setup__group">
|
||||
<h1 className="setup__title">
|
||||
<Trans>install_welcome_title</Trans>
|
||||
</h1>
|
||||
<p className="setup__desc text-center">
|
||||
<Trans>install_welcome_desc</Trans>
|
||||
</p>
|
||||
</div>
|
||||
<Controls />
|
||||
</div>
|
||||
);
|
||||
|
||||
export default withNamespaces()(Greeting);
|
||||
25
client/src/install/Setup/Progress.js
Normal file
25
client/src/install/Setup/Progress.js
Normal file
@@ -0,0 +1,25 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { Trans, withNamespaces } from 'react-i18next';
|
||||
|
||||
import { INSTALL_TOTAL_STEPS } from '../../helpers/constants';
|
||||
|
||||
const getProgressPercent = step => (step / INSTALL_TOTAL_STEPS) * 100;
|
||||
|
||||
const Progress = props => (
|
||||
<div className="setup__progress">
|
||||
<Trans>install_step</Trans> {props.step}/{INSTALL_TOTAL_STEPS}
|
||||
<div className="setup__progress-wrap">
|
||||
<div
|
||||
className="setup__progress-inner"
|
||||
style={{ width: `${getProgressPercent(props.step)}%` }}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
Progress.propTypes = {
|
||||
step: PropTypes.number.isRequired,
|
||||
};
|
||||
|
||||
export default withNamespaces()(Progress);
|
||||
221
client/src/install/Setup/Settings.js
Normal file
221
client/src/install/Setup/Settings.js
Normal file
@@ -0,0 +1,221 @@
|
||||
import React from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import PropTypes from 'prop-types';
|
||||
import { Field, reduxForm, formValueSelector } from 'redux-form';
|
||||
import { Trans, withNamespaces } from 'react-i18next';
|
||||
import flow from 'lodash/flow';
|
||||
|
||||
import Controls from './Controls';
|
||||
import AddressList from './AddressList';
|
||||
import renderField from './renderField';
|
||||
import { getInterfaceIp } from '../../helpers/helpers';
|
||||
|
||||
const required = (value) => {
|
||||
if (value || value === 0) {
|
||||
return false;
|
||||
}
|
||||
return <Trans>form_error_required</Trans>;
|
||||
};
|
||||
|
||||
const port = (value) => {
|
||||
if (value < 1 || value > 65535) {
|
||||
return <Trans>form_error_port</Trans>;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
const toNumber = value => value && parseInt(value, 10);
|
||||
|
||||
const renderInterfaces = (interfaces => (
|
||||
Object.keys(interfaces).map((item) => {
|
||||
const option = interfaces[item];
|
||||
const { name } = option;
|
||||
|
||||
if (option.ip_addresses && option.ip_addresses.length > 0) {
|
||||
const ip = getInterfaceIp(option);
|
||||
|
||||
return (
|
||||
<option value={ip} key={name}>
|
||||
{name} - {ip}
|
||||
</option>
|
||||
);
|
||||
}
|
||||
|
||||
return false;
|
||||
})
|
||||
));
|
||||
|
||||
let Settings = (props) => {
|
||||
const {
|
||||
handleSubmit,
|
||||
webIp,
|
||||
webPort,
|
||||
dnsIp,
|
||||
dnsPort,
|
||||
interfaces,
|
||||
invalid,
|
||||
webWarning,
|
||||
dnsWarning,
|
||||
} = props;
|
||||
|
||||
return (
|
||||
<form className="setup__step" onSubmit={handleSubmit}>
|
||||
<div className="setup__group">
|
||||
<div className="setup__subtitle">
|
||||
<Trans>install_settings_title</Trans>
|
||||
</div>
|
||||
<div className="row">
|
||||
<div className="col-8">
|
||||
<div className="form-group">
|
||||
<label>
|
||||
<Trans>install_settings_listen</Trans>
|
||||
</label>
|
||||
<Field
|
||||
name="web.ip"
|
||||
component="select"
|
||||
className="form-control custom-select"
|
||||
>
|
||||
<option value="0.0.0.0">
|
||||
<Trans>install_settings_all_interfaces</Trans>
|
||||
</option>
|
||||
{renderInterfaces(interfaces)}
|
||||
</Field>
|
||||
</div>
|
||||
</div>
|
||||
<div className="col-4">
|
||||
<div className="form-group">
|
||||
<label>
|
||||
<Trans>install_settings_port</Trans>
|
||||
</label>
|
||||
<Field
|
||||
name="web.port"
|
||||
component={renderField}
|
||||
type="number"
|
||||
className="form-control"
|
||||
placeholder="80"
|
||||
validate={[port, required]}
|
||||
normalize={toNumber}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="setup__desc">
|
||||
<Trans>install_settings_interface_link</Trans>
|
||||
<div className="mt-1">
|
||||
<AddressList
|
||||
interfaces={interfaces}
|
||||
address={webIp}
|
||||
port={webPort}
|
||||
/>
|
||||
</div>
|
||||
{webWarning &&
|
||||
<div className="text-danger mt-2">
|
||||
{webWarning}
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
<div className="setup__group">
|
||||
<div className="setup__subtitle">
|
||||
<Trans>install_settings_dns</Trans>
|
||||
</div>
|
||||
<div className="row">
|
||||
<div className="col-8">
|
||||
<div className="form-group">
|
||||
<label>
|
||||
<Trans>install_settings_listen</Trans>
|
||||
</label>
|
||||
<Field
|
||||
name="dns.ip"
|
||||
component="select"
|
||||
className="form-control custom-select"
|
||||
>
|
||||
<option value="0.0.0.0">
|
||||
<Trans>install_settings_all_interfaces</Trans>
|
||||
</option>
|
||||
{renderInterfaces(interfaces)}
|
||||
</Field>
|
||||
</div>
|
||||
</div>
|
||||
<div className="col-4">
|
||||
<div className="form-group">
|
||||
<label>
|
||||
<Trans>install_settings_port</Trans>
|
||||
</label>
|
||||
<Field
|
||||
name="dns.port"
|
||||
component={renderField}
|
||||
type="number"
|
||||
className="form-control"
|
||||
placeholder="80"
|
||||
validate={[port, required]}
|
||||
normalize={toNumber}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="setup__desc">
|
||||
<Trans>install_settings_dns_desc</Trans>
|
||||
<div className="mt-1">
|
||||
<AddressList
|
||||
interfaces={interfaces}
|
||||
address={dnsIp}
|
||||
port={dnsPort}
|
||||
isDns={true}
|
||||
/>
|
||||
</div>
|
||||
{dnsWarning &&
|
||||
<div className="text-danger mt-2">
|
||||
{dnsWarning}
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
<Controls invalid={invalid} />
|
||||
</form>
|
||||
);
|
||||
};
|
||||
|
||||
Settings.propTypes = {
|
||||
handleSubmit: PropTypes.func.isRequired,
|
||||
webIp: PropTypes.string.isRequired,
|
||||
dnsIp: PropTypes.string.isRequired,
|
||||
webPort: PropTypes.oneOfType([
|
||||
PropTypes.string,
|
||||
PropTypes.number,
|
||||
]),
|
||||
dnsPort: PropTypes.oneOfType([
|
||||
PropTypes.string,
|
||||
PropTypes.number,
|
||||
]),
|
||||
webWarning: PropTypes.string.isRequired,
|
||||
dnsWarning: PropTypes.string.isRequired,
|
||||
interfaces: PropTypes.object.isRequired,
|
||||
invalid: PropTypes.bool.isRequired,
|
||||
initialValues: PropTypes.object,
|
||||
};
|
||||
|
||||
const selector = formValueSelector('install');
|
||||
|
||||
Settings = connect((state) => {
|
||||
const webIp = selector(state, 'web.ip');
|
||||
const webPort = selector(state, 'web.port');
|
||||
const dnsIp = selector(state, 'dns.ip');
|
||||
const dnsPort = selector(state, 'dns.port');
|
||||
|
||||
return {
|
||||
webIp,
|
||||
webPort,
|
||||
dnsIp,
|
||||
dnsPort,
|
||||
};
|
||||
})(Settings);
|
||||
|
||||
export default flow([
|
||||
withNamespaces(),
|
||||
reduxForm({
|
||||
form: 'install',
|
||||
destroyOnUnmount: false,
|
||||
forceUnregisterOnUnmount: true,
|
||||
}),
|
||||
])(Settings);
|
||||
117
client/src/install/Setup/Setup.css
Normal file
117
client/src/install/Setup/Setup.css
Normal file
@@ -0,0 +1,117 @@
|
||||
.setup {
|
||||
min-height: calc(100vh - 80px);
|
||||
line-height: 1.48;
|
||||
}
|
||||
|
||||
@media screen and (min-width: 768px) {
|
||||
.setup {
|
||||
padding: 50px 0;
|
||||
}
|
||||
}
|
||||
|
||||
.setup__container {
|
||||
max-width: 650px;
|
||||
margin: 0 auto;
|
||||
padding: 30px 20px;
|
||||
line-height: 1.6;
|
||||
background-color: #fff;
|
||||
box-shadow: 0 1px 4px rgba(74, 74, 74, 0.36);
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
@media screen and (min-width: 768px) {
|
||||
.setup__container {
|
||||
width: 650px;
|
||||
padding: 40px 30px;
|
||||
}
|
||||
}
|
||||
|
||||
.setup__logo {
|
||||
display: block;
|
||||
margin: 0 auto 40px;
|
||||
max-width: 140px;
|
||||
}
|
||||
|
||||
.setup__nav {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.setup__step {
|
||||
margin-bottom: 25px;
|
||||
}
|
||||
|
||||
.setup__title {
|
||||
margin-bottom: 30px;
|
||||
font-size: 28px;
|
||||
text-align: center;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.setup__subtitle {
|
||||
margin-bottom: 10px;
|
||||
font-size: 17px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.setup__desc {
|
||||
margin-bottom: 20px;
|
||||
font-size: 15px;
|
||||
}
|
||||
|
||||
.setup__group {
|
||||
margin-bottom: 35px;
|
||||
}
|
||||
|
||||
.setup__group:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.setup__progress {
|
||||
font-size: 13px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.setup__progress-wrap {
|
||||
height: 4px;
|
||||
margin: 20px -20px -30px -20px;
|
||||
overflow: hidden;
|
||||
background-color: #eaeaea;
|
||||
border-radius: 0 0 3px 3px;
|
||||
}
|
||||
|
||||
@media screen and (min-width: 768px) {
|
||||
.setup__progress-wrap {
|
||||
margin: 20px -30px -40px -30px;
|
||||
}
|
||||
}
|
||||
|
||||
.setup__progress-inner {
|
||||
width: 0;
|
||||
height: 100%;
|
||||
font-size: 1.2rem;
|
||||
line-height: 20px;
|
||||
color: #fff;
|
||||
text-align: center;
|
||||
box-shadow: inset 0 -1px 0 rgba(0, 0, 0, 0.15);
|
||||
transition: width 0.6s ease;
|
||||
background: linear-gradient(45deg, rgba(99, 125, 120, 1) 0%, rgba(88, 177, 101, 1) 100%);
|
||||
}
|
||||
|
||||
.btn-standard {
|
||||
padding-left: 20px;
|
||||
padding-right: 20px;
|
||||
}
|
||||
|
||||
.form__message {
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
.form__message--error {
|
||||
color: #cd201f;
|
||||
}
|
||||
|
||||
.setup__button {
|
||||
min-width: 120px;
|
||||
padding-left: 30px;
|
||||
padding-right: 30px;
|
||||
}
|
||||
57
client/src/install/Setup/Submit.js
Normal file
57
client/src/install/Setup/Submit.js
Normal file
@@ -0,0 +1,57 @@
|
||||
import React from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import PropTypes from 'prop-types';
|
||||
import { reduxForm, formValueSelector } from 'redux-form';
|
||||
import { Trans, withNamespaces } from 'react-i18next';
|
||||
import flow from 'lodash/flow';
|
||||
|
||||
import Controls from './Controls';
|
||||
import { getWebAddress } from '../../helpers/helpers';
|
||||
|
||||
let Submit = props => (
|
||||
<div className="setup__step">
|
||||
<div className="setup__group">
|
||||
<h1 className="setup__title">
|
||||
<Trans>install_submit_title</Trans>
|
||||
</h1>
|
||||
<p className="setup__desc">
|
||||
<Trans>install_submit_desc</Trans>
|
||||
</p>
|
||||
</div>
|
||||
<Controls
|
||||
openDashboard={props.openDashboard}
|
||||
address={getWebAddress(props.webIp, props.webPort)}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
||||
Submit.propTypes = {
|
||||
webIp: PropTypes.string.isRequired,
|
||||
webPort: PropTypes.number.isRequired,
|
||||
handleSubmit: PropTypes.func.isRequired,
|
||||
pristine: PropTypes.bool.isRequired,
|
||||
submitting: PropTypes.bool.isRequired,
|
||||
openDashboard: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
const selector = formValueSelector('install');
|
||||
|
||||
Submit = connect((state) => {
|
||||
const webIp = selector(state, 'web.ip');
|
||||
const webPort = selector(state, 'web.port');
|
||||
|
||||
return {
|
||||
webIp,
|
||||
webPort,
|
||||
};
|
||||
})(Submit);
|
||||
|
||||
|
||||
export default flow([
|
||||
withNamespaces(),
|
||||
reduxForm({
|
||||
form: 'install',
|
||||
destroyOnUnmount: false,
|
||||
forceUnregisterOnUnmount: true,
|
||||
}),
|
||||
])(Submit);
|
||||
125
client/src/install/Setup/index.js
Normal file
125
client/src/install/Setup/index.js
Normal file
@@ -0,0 +1,125 @@
|
||||
import React, { Component, Fragment } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import * as actionCreators from '../../actions/install';
|
||||
import { INSTALL_FIRST_STEP, INSTALL_TOTAL_STEPS } from '../../helpers/constants';
|
||||
|
||||
import Loading from '../../components/ui/Loading';
|
||||
import Greeting from './Greeting';
|
||||
import Settings from './Settings';
|
||||
import Auth from './Auth';
|
||||
import Devices from './Devices';
|
||||
import Submit from './Submit';
|
||||
import Progress from './Progress';
|
||||
|
||||
import Toasts from '../../components/Toasts';
|
||||
import Footer from '../../components/ui/Footer';
|
||||
import logo from '../../components/ui/svg/logo.svg';
|
||||
|
||||
import './Setup.css';
|
||||
import '../../components/ui/Tabler.css';
|
||||
|
||||
class Setup extends Component {
|
||||
componentDidMount() {
|
||||
this.props.getDefaultAddresses();
|
||||
}
|
||||
|
||||
handleFormSubmit = (values) => {
|
||||
this.props.setAllSettings(values);
|
||||
};
|
||||
|
||||
openDashboard = (address) => {
|
||||
window.location.replace(address);
|
||||
}
|
||||
|
||||
nextStep = () => {
|
||||
if (this.props.install.step < INSTALL_TOTAL_STEPS) {
|
||||
this.props.nextStep();
|
||||
}
|
||||
}
|
||||
|
||||
prevStep = () => {
|
||||
if (this.props.install.step > INSTALL_FIRST_STEP) {
|
||||
this.props.prevStep();
|
||||
}
|
||||
}
|
||||
|
||||
renderPage(step, config, interfaces) {
|
||||
switch (step) {
|
||||
case 1:
|
||||
return <Greeting />;
|
||||
case 2:
|
||||
return (
|
||||
<Settings
|
||||
initialValues={config}
|
||||
interfaces={interfaces}
|
||||
webWarning={config.web.warning}
|
||||
dnsWarning={config.dns.warning}
|
||||
onSubmit={this.nextStep}
|
||||
/>
|
||||
);
|
||||
case 3:
|
||||
return (
|
||||
<Auth onSubmit={this.handleFormSubmit} />
|
||||
);
|
||||
case 4:
|
||||
return <Devices interfaces={interfaces} />;
|
||||
case 5:
|
||||
return <Submit openDashboard={this.openDashboard} />;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
const {
|
||||
processingDefault,
|
||||
step,
|
||||
web,
|
||||
dns,
|
||||
interfaces,
|
||||
} = this.props.install;
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
{processingDefault && <Loading />}
|
||||
{!processingDefault &&
|
||||
<Fragment>
|
||||
<div className="setup">
|
||||
<div className="setup__container">
|
||||
<img src={logo} className="setup__logo" alt="logo" />
|
||||
{this.renderPage(step, { web, dns }, interfaces)}
|
||||
<Progress step={step} />
|
||||
</div>
|
||||
</div>
|
||||
<Footer />
|
||||
<Toasts />
|
||||
</Fragment>
|
||||
}
|
||||
</Fragment>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Setup.propTypes = {
|
||||
getDefaultAddresses: PropTypes.func.isRequired,
|
||||
setAllSettings: PropTypes.func.isRequired,
|
||||
nextStep: PropTypes.func.isRequired,
|
||||
prevStep: PropTypes.func.isRequired,
|
||||
install: PropTypes.object.isRequired,
|
||||
step: PropTypes.number,
|
||||
web: PropTypes.object,
|
||||
dns: PropTypes.object,
|
||||
};
|
||||
|
||||
const mapStateToProps = (state) => {
|
||||
const { install, toasts } = state;
|
||||
const props = { install, toasts };
|
||||
return props;
|
||||
};
|
||||
|
||||
export default connect(
|
||||
mapStateToProps,
|
||||
actionCreators,
|
||||
)(Setup);
|
||||
19
client/src/install/Setup/renderField.js
Normal file
19
client/src/install/Setup/renderField.js
Normal file
@@ -0,0 +1,19 @@
|
||||
import React, { Fragment } from 'react';
|
||||
|
||||
const renderField = ({
|
||||
input, className, placeholder, type, disabled, autoComplete, meta: { touched, error },
|
||||
}) => (
|
||||
<Fragment>
|
||||
<input
|
||||
{...input}
|
||||
placeholder={placeholder}
|
||||
type={type}
|
||||
className={className}
|
||||
disabled={disabled}
|
||||
autoComplete={autoComplete}
|
||||
/>
|
||||
{!disabled && touched && (error && <span className="form__message form__message--error">{error}</span>)}
|
||||
</Fragment>
|
||||
);
|
||||
|
||||
export default renderField;
|
||||
18
client/src/install/index.js
Normal file
18
client/src/install/index.js
Normal file
@@ -0,0 +1,18 @@
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import { Provider } from 'react-redux';
|
||||
|
||||
import '../components/App/index.css';
|
||||
import '../components/ui/ReactTable.css';
|
||||
import configureStore from '../configureStore';
|
||||
import reducers from '../reducers/install';
|
||||
import '../i18n';
|
||||
import Setup from './Setup';
|
||||
|
||||
const store = configureStore(reducers, {}); // set initial state
|
||||
ReactDOM.render(
|
||||
<Provider store={store}>
|
||||
<Setup />
|
||||
</Provider>,
|
||||
document.getElementById('root'),
|
||||
);
|
||||
81
client/src/reducers/encryption.js
Normal file
81
client/src/reducers/encryption.js
Normal file
@@ -0,0 +1,81 @@
|
||||
import { handleActions } from 'redux-actions';
|
||||
|
||||
import * as actions from '../actions/encryption';
|
||||
|
||||
const encryption = handleActions({
|
||||
[actions.getTlsStatusRequest]: state => ({ ...state, processing: true }),
|
||||
[actions.getTlsStatusFailure]: state => ({ ...state, processing: false }),
|
||||
[actions.getTlsStatusSuccess]: (state, { payload }) => {
|
||||
const newState = {
|
||||
...state,
|
||||
...payload,
|
||||
processing: false,
|
||||
};
|
||||
return newState;
|
||||
},
|
||||
|
||||
[actions.setTlsConfigRequest]: state => ({ ...state, processingConfig: true }),
|
||||
[actions.setTlsConfigFailure]: state => ({ ...state, processingConfig: false }),
|
||||
[actions.setTlsConfigSuccess]: (state, { payload }) => {
|
||||
const newState = {
|
||||
...state,
|
||||
...payload,
|
||||
processingConfig: false,
|
||||
};
|
||||
return newState;
|
||||
},
|
||||
|
||||
[actions.validateTlsConfigRequest]: state => ({ ...state, processingValidate: true }),
|
||||
[actions.validateTlsConfigFailure]: state => ({ ...state, processingValidate: false }),
|
||||
[actions.validateTlsConfigSuccess]: (state, { payload }) => {
|
||||
const {
|
||||
issuer = '',
|
||||
key_type = '',
|
||||
not_after = '',
|
||||
not_before = '',
|
||||
subject = '',
|
||||
warning_validation = '',
|
||||
dns_names = '',
|
||||
...values
|
||||
} = payload;
|
||||
|
||||
const newState = {
|
||||
...state,
|
||||
...values,
|
||||
issuer,
|
||||
key_type,
|
||||
not_after,
|
||||
not_before,
|
||||
subject,
|
||||
warning_validation,
|
||||
dns_names,
|
||||
processingValidate: false,
|
||||
};
|
||||
return newState;
|
||||
},
|
||||
}, {
|
||||
processing: true,
|
||||
processingConfig: false,
|
||||
processingValidate: false,
|
||||
enabled: false,
|
||||
dns_names: null,
|
||||
force_https: false,
|
||||
issuer: '',
|
||||
key_type: '',
|
||||
not_after: '',
|
||||
not_before: '',
|
||||
port_dns_over_tls: '',
|
||||
port_https: '',
|
||||
subject: '',
|
||||
valid_chain: false,
|
||||
valid_key: false,
|
||||
valid_cert: false,
|
||||
status_cert: '',
|
||||
status_key: '',
|
||||
certificate_chain: '',
|
||||
private_key: '',
|
||||
server_name: '',
|
||||
warning_validation: '',
|
||||
});
|
||||
|
||||
export default encryption;
|
||||
@@ -1,10 +1,12 @@
|
||||
import { combineReducers } from 'redux';
|
||||
import { handleActions } from 'redux-actions';
|
||||
import { loadingBarReducer } from 'react-redux-loading-bar';
|
||||
import nanoid from 'nanoid';
|
||||
import { reducer as formReducer } from 'redux-form';
|
||||
import versionCompare from '../helpers/versionCompare';
|
||||
|
||||
import * as actions from '../actions';
|
||||
import toasts from './toasts';
|
||||
import encryption from './encryption';
|
||||
|
||||
const settings = handleActions({
|
||||
[actions.initSettingsRequest]: state => ({ ...state, processing: true }),
|
||||
@@ -35,6 +37,7 @@ const settings = handleActions({
|
||||
processing: true,
|
||||
processingTestUpstream: false,
|
||||
processingSetUpstream: false,
|
||||
processingDhcpStatus: false,
|
||||
});
|
||||
|
||||
const dashboard = handleActions({
|
||||
@@ -50,6 +53,7 @@ const dashboard = handleActions({
|
||||
upstream_dns: upstreamDns,
|
||||
protection_enabled: protectionEnabled,
|
||||
language,
|
||||
http_port: httpPort,
|
||||
} = payload;
|
||||
const newState = {
|
||||
...state,
|
||||
@@ -62,6 +66,7 @@ const dashboard = handleActions({
|
||||
upstreamDns: upstreamDns.join('\n'),
|
||||
protectionEnabled,
|
||||
language,
|
||||
httpPort,
|
||||
};
|
||||
return newState;
|
||||
},
|
||||
@@ -115,13 +120,13 @@ const dashboard = handleActions({
|
||||
|
||||
if (versionCompare(currentVersion, payload.version) === -1) {
|
||||
const {
|
||||
announcement,
|
||||
version,
|
||||
announcement_url: announcementUrl,
|
||||
} = payload;
|
||||
|
||||
const newState = {
|
||||
...state,
|
||||
announcement,
|
||||
version,
|
||||
announcementUrl,
|
||||
isUpdateAvailable: true,
|
||||
};
|
||||
@@ -138,8 +143,14 @@ const dashboard = handleActions({
|
||||
return newState;
|
||||
},
|
||||
|
||||
[actions.toggleProtectionRequest]: state => ({ ...state, processingProtection: true }),
|
||||
[actions.toggleProtectionFailure]: state => ({ ...state, processingProtection: false }),
|
||||
[actions.toggleProtectionSuccess]: (state) => {
|
||||
const newState = { ...state, protectionEnabled: !state.protectionEnabled };
|
||||
const newState = {
|
||||
...state,
|
||||
protectionEnabled: !state.protectionEnabled,
|
||||
processingProtection: false,
|
||||
};
|
||||
return newState;
|
||||
},
|
||||
|
||||
@@ -162,6 +173,8 @@ const dashboard = handleActions({
|
||||
processingFiltering: true,
|
||||
upstreamDns: [],
|
||||
protectionEnabled: false,
|
||||
processingProtection: false,
|
||||
httpPort: 80,
|
||||
});
|
||||
|
||||
const queryLogs = handleActions({
|
||||
@@ -226,37 +239,72 @@ const filtering = handleActions({
|
||||
isFilteringModalOpen: false,
|
||||
processingFilters: false,
|
||||
processingRules: false,
|
||||
processingAddFilter: false,
|
||||
processingRefreshFilters: false,
|
||||
filters: [],
|
||||
userRules: '',
|
||||
});
|
||||
|
||||
const toasts = handleActions({
|
||||
[actions.addErrorToast]: (state, { payload }) => {
|
||||
const errorToast = {
|
||||
id: nanoid(),
|
||||
message: payload.error.toString(),
|
||||
type: 'error',
|
||||
const dhcp = handleActions({
|
||||
[actions.getDhcpStatusRequest]: state => ({ ...state, processing: true }),
|
||||
[actions.getDhcpStatusFailure]: state => ({ ...state, processing: false }),
|
||||
[actions.getDhcpStatusSuccess]: (state, { payload }) => {
|
||||
const newState = {
|
||||
...state,
|
||||
...payload,
|
||||
processing: false,
|
||||
};
|
||||
|
||||
const newState = { ...state, notices: [...state.notices, errorToast] };
|
||||
return newState;
|
||||
},
|
||||
[actions.addSuccessToast]: (state, { payload }) => {
|
||||
const successToast = {
|
||||
id: nanoid(),
|
||||
message: payload,
|
||||
type: 'success',
|
||||
|
||||
[actions.getDhcpInterfacesRequest]: state => ({ ...state, processingInterfaces: true }),
|
||||
[actions.getDhcpInterfacesFailure]: state => ({ ...state, processingInterfaces: false }),
|
||||
[actions.getDhcpInterfacesSuccess]: (state, { payload }) => {
|
||||
const newState = {
|
||||
...state,
|
||||
interfaces: payload,
|
||||
processingInterfaces: false,
|
||||
};
|
||||
return newState;
|
||||
},
|
||||
|
||||
const newState = { ...state, notices: [...state.notices, successToast] };
|
||||
[actions.findActiveDhcpRequest]: state => ({ ...state, processingStatus: true }),
|
||||
[actions.findActiveDhcpFailure]: state => ({ ...state, processingStatus: false }),
|
||||
[actions.findActiveDhcpSuccess]: (state, { payload }) => ({
|
||||
...state,
|
||||
active: payload,
|
||||
processingStatus: false,
|
||||
}),
|
||||
|
||||
[actions.toggleDhcpRequest]: state => ({ ...state, processingDhcp: true }),
|
||||
[actions.toggleDhcpFailure]: state => ({ ...state, processingDhcp: false }),
|
||||
[actions.toggleDhcpSuccess]: (state) => {
|
||||
const { config } = state;
|
||||
const newConfig = { ...config, enabled: !config.enabled };
|
||||
const newState = { ...state, config: newConfig, processingDhcp: false };
|
||||
return newState;
|
||||
},
|
||||
[actions.removeToast]: (state, { payload }) => {
|
||||
const filtered = state.notices.filter(notice => notice.id !== payload);
|
||||
const newState = { ...state, notices: filtered };
|
||||
|
||||
[actions.setDhcpConfigRequest]: state => ({ ...state, processingConfig: true }),
|
||||
[actions.setDhcpConfigFailure]: state => ({ ...state, processingConfig: false }),
|
||||
[actions.setDhcpConfigSuccess]: (state, { payload }) => {
|
||||
const { config } = state;
|
||||
const newConfig = { ...config, ...payload };
|
||||
const newState = { ...state, config: newConfig, processingConfig: false };
|
||||
return newState;
|
||||
},
|
||||
}, { notices: [] });
|
||||
}, {
|
||||
processing: true,
|
||||
processingStatus: false,
|
||||
processingInterfaces: false,
|
||||
processingDhcp: false,
|
||||
processingConfig: false,
|
||||
config: {
|
||||
enabled: false,
|
||||
},
|
||||
active: null,
|
||||
leases: [],
|
||||
});
|
||||
|
||||
export default combineReducers({
|
||||
settings,
|
||||
@@ -264,5 +312,8 @@ export default combineReducers({
|
||||
queryLogs,
|
||||
filtering,
|
||||
toasts,
|
||||
dhcp,
|
||||
encryption,
|
||||
loadingBar: loadingBarReducer,
|
||||
form: formReducer,
|
||||
});
|
||||
|
||||
47
client/src/reducers/install.js
Normal file
47
client/src/reducers/install.js
Normal file
@@ -0,0 +1,47 @@
|
||||
import { combineReducers } from 'redux';
|
||||
import { handleActions } from 'redux-actions';
|
||||
import { reducer as formReducer } from 'redux-form';
|
||||
|
||||
import * as actions from '../actions/install';
|
||||
import toasts from './toasts';
|
||||
import { INSTALL_FIRST_STEP } from '../helpers/constants';
|
||||
|
||||
const install = handleActions({
|
||||
[actions.getDefaultAddressesRequest]: state => ({ ...state, processingDefault: true }),
|
||||
[actions.getDefaultAddressesFailure]: state => ({ ...state, processingDefault: false }),
|
||||
[actions.getDefaultAddressesSuccess]: (state, { payload }) => {
|
||||
const values = payload;
|
||||
values.web.ip = state.web.ip;
|
||||
values.dns.ip = state.dns.ip;
|
||||
const newState = { ...state, ...values, processingDefault: false };
|
||||
return newState;
|
||||
},
|
||||
|
||||
[actions.nextStep]: state => ({ ...state, step: state.step + 1 }),
|
||||
[actions.prevStep]: state => ({ ...state, step: state.step - 1 }),
|
||||
|
||||
[actions.setAllSettingsRequest]: state => ({ ...state, processingSubmit: true }),
|
||||
[actions.setAllSettingsFailure]: state => ({ ...state, processingSubmit: false }),
|
||||
[actions.setAllSettingsSuccess]: state => ({ ...state, processingSubmit: false }),
|
||||
}, {
|
||||
step: INSTALL_FIRST_STEP,
|
||||
processingDefault: true,
|
||||
processingSubmit: false,
|
||||
web: {
|
||||
ip: '0.0.0.0',
|
||||
port: 80,
|
||||
warning: '',
|
||||
},
|
||||
dns: {
|
||||
ip: '0.0.0.0',
|
||||
port: 53,
|
||||
warning: '',
|
||||
},
|
||||
interfaces: {},
|
||||
});
|
||||
|
||||
export default combineReducers({
|
||||
install,
|
||||
toasts,
|
||||
form: formReducer,
|
||||
});
|
||||
34
client/src/reducers/toasts.js
Normal file
34
client/src/reducers/toasts.js
Normal file
@@ -0,0 +1,34 @@
|
||||
import { handleActions } from 'redux-actions';
|
||||
import nanoid from 'nanoid';
|
||||
|
||||
import { addErrorToast, addSuccessToast, removeToast } from '../actions';
|
||||
|
||||
const toasts = handleActions({
|
||||
[addErrorToast]: (state, { payload }) => {
|
||||
const errorToast = {
|
||||
id: nanoid(),
|
||||
message: payload.error.toString(),
|
||||
type: 'error',
|
||||
};
|
||||
|
||||
const newState = { ...state, notices: [...state.notices, errorToast] };
|
||||
return newState;
|
||||
},
|
||||
[addSuccessToast]: (state, { payload }) => {
|
||||
const successToast = {
|
||||
id: nanoid(),
|
||||
message: payload,
|
||||
type: 'success',
|
||||
};
|
||||
|
||||
const newState = { ...state, notices: [...state.notices, successToast] };
|
||||
return newState;
|
||||
},
|
||||
[removeToast]: (state, { payload }) => {
|
||||
const filtered = state.notices.filter(notice => notice.id !== payload);
|
||||
const newState = { ...state, notices: filtered };
|
||||
return newState;
|
||||
},
|
||||
}, { notices: [] });
|
||||
|
||||
export default toasts;
|
||||
13
client/webpack.common.js
vendored
13
client/webpack.common.js
vendored
@@ -8,7 +8,9 @@ const CleanWebpackPlugin = require('clean-webpack-plugin');
|
||||
|
||||
const RESOURCES_PATH = path.resolve(__dirname);
|
||||
const ENTRY_REACT = path.resolve(RESOURCES_PATH, 'src/index.js');
|
||||
const ENTRY_INSTALL = path.resolve(RESOURCES_PATH, 'src/install/index.js');
|
||||
const HTML_PATH = path.resolve(RESOURCES_PATH, 'public/index.html');
|
||||
const HTML_INSTALL_PATH = path.resolve(RESOURCES_PATH, 'public/install.html');
|
||||
|
||||
const PUBLIC_PATH = path.resolve(__dirname, '../build/static');
|
||||
|
||||
@@ -16,7 +18,8 @@ const config = {
|
||||
target: 'web',
|
||||
context: RESOURCES_PATH,
|
||||
entry: {
|
||||
bundle: ENTRY_REACT,
|
||||
main: ENTRY_REACT,
|
||||
install: ENTRY_INSTALL,
|
||||
},
|
||||
output: {
|
||||
path: PUBLIC_PATH,
|
||||
@@ -101,8 +104,16 @@ const config = {
|
||||
new HtmlWebpackPlugin({
|
||||
inject: true,
|
||||
cache: false,
|
||||
chunks: ['main'],
|
||||
template: HTML_PATH,
|
||||
}),
|
||||
new HtmlWebpackPlugin({
|
||||
inject: true,
|
||||
cache: false,
|
||||
chunks: ['install'],
|
||||
filename: 'install.html',
|
||||
template: HTML_INSTALL_PATH,
|
||||
}),
|
||||
new ExtractTextPlugin({
|
||||
filename: '[name].[contenthash].css',
|
||||
}),
|
||||
|
||||
1
client/webpack.dev.js
vendored
1
client/webpack.dev.js
vendored
@@ -2,7 +2,6 @@ const merge = require('webpack-merge');
|
||||
const common = require('./webpack.common.js');
|
||||
|
||||
module.exports = merge(common, {
|
||||
devtool: 'inline-source-map',
|
||||
module: {
|
||||
rules: [{
|
||||
test: /\.js$/,
|
||||
|
||||
403
config.go
403
config.go
@@ -1,158 +1,175 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"sync"
|
||||
"text/template"
|
||||
"time"
|
||||
|
||||
"gopkg.in/yaml.v2"
|
||||
"github.com/AdguardTeam/AdGuardHome/dhcpd"
|
||||
"github.com/AdguardTeam/AdGuardHome/dnsfilter"
|
||||
"github.com/AdguardTeam/AdGuardHome/dnsforward"
|
||||
"github.com/hmage/golibs/log"
|
||||
yaml "gopkg.in/yaml.v2"
|
||||
)
|
||||
|
||||
// Current schema version. We compare it with the value from
|
||||
// the configuration file and perform necessary upgrade operations if needed
|
||||
const SchemaVersion = 1
|
||||
const (
|
||||
dataDir = "data" // data storage
|
||||
filterDir = "filters" // cache location for downloaded filters, it's under DataDir
|
||||
)
|
||||
|
||||
// Directory where we'll store all downloaded filters contents
|
||||
const FiltersDir = "filters"
|
||||
|
||||
// User filter ID is always 0
|
||||
const UserFilterId = 0
|
||||
|
||||
// Just a counter that we use for incrementing the filter ID
|
||||
var NextFilterId = time.Now().Unix()
|
||||
// logSettings
|
||||
type logSettings struct {
|
||||
LogFile string `yaml:"log_file"` // Path to the log file. If empty, write to stdout. If "syslog", writes to syslog
|
||||
Verbose bool `yaml:"verbose"` // If true, verbose logging is enabled
|
||||
}
|
||||
|
||||
// configuration is loaded from YAML
|
||||
// field ordering is important -- yaml fields will mirror ordering from here
|
||||
type configuration struct {
|
||||
// Config filename (can be overriden via the command line arguments)
|
||||
ourConfigFilename string
|
||||
// Basically, this is our working directory
|
||||
ourBinaryDir string
|
||||
// Directory to store data (i.e. filters contents)
|
||||
ourDataDir string
|
||||
ourConfigFilename string // Config filename (can be overridden via the command line arguments)
|
||||
ourWorkingDir string // Location of our directory, used to protect against CWD being somewhere else
|
||||
firstRun bool // if set to true, don't run any services except HTTP web inteface, and serve only first-run html
|
||||
|
||||
// Schema version of the config file. This value is used when performing the app updates.
|
||||
SchemaVersion int `yaml:"schema_version"`
|
||||
BindHost string `yaml:"bind_host"`
|
||||
BindPort int `yaml:"bind_port"`
|
||||
AuthName string `yaml:"auth_name"`
|
||||
AuthPass string `yaml:"auth_pass"`
|
||||
CoreDNS coreDNSConfig `yaml:"coredns"`
|
||||
Filters []filter `yaml:"filters"`
|
||||
UserRules []string `yaml:"user_rules"`
|
||||
Language string `yaml:"language"` // two-letter ISO 639-1 language code
|
||||
BindHost string `yaml:"bind_host"` // BindHost is the IP address of the HTTP server to bind to
|
||||
BindPort int `yaml:"bind_port"` // BindPort is the port the HTTP server
|
||||
AuthName string `yaml:"auth_name"` // AuthName is the basic auth username
|
||||
AuthPass string `yaml:"auth_pass"` // AuthPass is the basic auth password
|
||||
Language string `yaml:"language"` // two-letter ISO 639-1 language code
|
||||
DNS dnsConfig `yaml:"dns"`
|
||||
TLS tlsConfig `yaml:"tls"`
|
||||
Filters []filter `yaml:"filters"`
|
||||
UserRules []string `yaml:"user_rules"`
|
||||
DHCP dhcpd.ServerConfig `yaml:"dhcp"`
|
||||
|
||||
logSettings `yaml:",inline"`
|
||||
|
||||
sync.RWMutex `yaml:"-"`
|
||||
|
||||
SchemaVersion int `yaml:"schema_version"` // keeping last so that users will be less tempted to change it -- used when upgrading between versions
|
||||
}
|
||||
|
||||
type coreDnsFilter struct {
|
||||
ID int64 `yaml:"-"`
|
||||
Path string `yaml:"-"`
|
||||
}
|
||||
// field ordering is important -- yaml fields will mirror ordering from here
|
||||
type dnsConfig struct {
|
||||
BindHost string `yaml:"bind_host"`
|
||||
Port int `yaml:"port"`
|
||||
|
||||
type coreDNSConfig struct {
|
||||
binaryFile string
|
||||
coreFile string
|
||||
Filters []coreDnsFilter `yaml:"-"`
|
||||
Port int `yaml:"port"`
|
||||
ProtectionEnabled bool `yaml:"protection_enabled"`
|
||||
FilteringEnabled bool `yaml:"filtering_enabled"`
|
||||
SafeBrowsingEnabled bool `yaml:"safebrowsing_enabled"`
|
||||
SafeSearchEnabled bool `yaml:"safesearch_enabled"`
|
||||
ParentalEnabled bool `yaml:"parental_enabled"`
|
||||
ParentalSensitivity int `yaml:"parental_sensitivity"`
|
||||
BlockedResponseTTL int `yaml:"blocked_response_ttl"`
|
||||
QueryLogEnabled bool `yaml:"querylog_enabled"`
|
||||
Ratelimit int `yaml:"ratelimit"`
|
||||
RefuseAny bool `yaml:"refuse_any"`
|
||||
Pprof string `yaml:"-"`
|
||||
Cache string `yaml:"-"`
|
||||
Prometheus string `yaml:"-"`
|
||||
BootstrapDNS string `yaml:"bootstrap_dns"`
|
||||
UpstreamDNS []string `yaml:"upstream_dns"`
|
||||
}
|
||||
dnsforward.FilteringConfig `yaml:",inline"`
|
||||
|
||||
type filter struct {
|
||||
ID int64 `json:"id" yaml:"id"` // auto-assigned when filter is added (see NextFilterId)
|
||||
URL string `json:"url"`
|
||||
Name string `json:"name" yaml:"name"`
|
||||
Enabled bool `json:"enabled"`
|
||||
RulesCount int `json:"rulesCount" yaml:"-"`
|
||||
contents []byte
|
||||
LastUpdated time.Time `json:"lastUpdated" yaml:"last_updated"`
|
||||
UpstreamDNS []string `yaml:"upstream_dns"`
|
||||
}
|
||||
|
||||
var defaultDNS = []string{"tls://1.1.1.1", "tls://1.0.0.1"}
|
||||
|
||||
type tlsConfigSettings struct {
|
||||
Enabled bool `yaml:"enabled" json:"enabled"` // Enabled is the encryption (DOT/DOH/HTTPS) status
|
||||
ServerName string `yaml:"server_name" json:"server_name,omitempty"` // ServerName is the hostname of your HTTPS/TLS server
|
||||
ForceHTTPS bool `yaml:"force_https" json:"force_https,omitempty"` // ForceHTTPS: if true, forces HTTP->HTTPS redirect
|
||||
PortHTTPS int `yaml:"port_https" json:"port_https,omitempty"` // HTTPS port. If 0, HTTPS will be disabled
|
||||
PortDNSOverTLS int `yaml:"port_dns_over_tls" json:"port_dns_over_tls,omitempty"` // DNS-over-TLS port. If 0, DOT will be disabled
|
||||
|
||||
dnsforward.TLSConfig `yaml:",inline" json:",inline"`
|
||||
}
|
||||
|
||||
// field ordering is not important -- these are for API and are recalculated on each run
|
||||
type tlsConfigStatus struct {
|
||||
ValidCert bool `yaml:"-" json:"valid_cert"` // ValidCert is true if the specified certificates chain is a valid chain of X509 certificates
|
||||
ValidChain bool `yaml:"-" json:"valid_chain"` // ValidChain is true if the specified certificates chain is verified and issued by a known CA
|
||||
Subject string `yaml:"-" json:"subject,omitempty"` // Subject is the subject of the first certificate in the chain
|
||||
Issuer string `yaml:"-" json:"issuer,omitempty"` // Issuer is the issuer of the first certificate in the chain
|
||||
NotBefore time.Time `yaml:"-" json:"not_before,omitempty"` // NotBefore is the NotBefore field of the first certificate in the chain
|
||||
NotAfter time.Time `yaml:"-" json:"not_after,omitempty"` // NotAfter is the NotAfter field of the first certificate in the chain
|
||||
DNSNames []string `yaml:"-" json:"dns_names"` // DNSNames is the value of SubjectAltNames field of the first certificate in the chain
|
||||
|
||||
// key status
|
||||
ValidKey bool `yaml:"-" json:"valid_key"` // ValidKey is true if the key is a valid private key
|
||||
KeyType string `yaml:"-" json:"key_type,omitempty"` // KeyType is one of RSA or ECDSA
|
||||
|
||||
// is usable? set by validator
|
||||
usable bool
|
||||
|
||||
// warnings
|
||||
WarningValidation string `yaml:"-" json:"warning_validation,omitempty"` // WarningValidation is a validation warning message with the issue description
|
||||
}
|
||||
|
||||
// field ordering is important -- yaml fields will mirror ordering from here
|
||||
type tlsConfig struct {
|
||||
tlsConfigSettings `yaml:",inline" json:",inline"`
|
||||
tlsConfigStatus `yaml:"-" json:",inline"`
|
||||
}
|
||||
|
||||
// initialize to default values, will be changed later when reading config or parsing command line
|
||||
var config = configuration{
|
||||
ourConfigFilename: "AdGuardHome.yaml",
|
||||
ourDataDir: "data",
|
||||
BindPort: 3000,
|
||||
BindHost: "127.0.0.1",
|
||||
CoreDNS: coreDNSConfig{
|
||||
Port: 53,
|
||||
binaryFile: "coredns", // only filename, no path
|
||||
coreFile: "Corefile", // only filename, no path
|
||||
ProtectionEnabled: true,
|
||||
FilteringEnabled: true,
|
||||
SafeBrowsingEnabled: false,
|
||||
BlockedResponseTTL: 10, // in seconds
|
||||
QueryLogEnabled: true,
|
||||
Ratelimit: 20,
|
||||
RefuseAny: true,
|
||||
BootstrapDNS: "8.8.8.8:53",
|
||||
UpstreamDNS: defaultDNS,
|
||||
Cache: "cache",
|
||||
Prometheus: "prometheus :9153",
|
||||
BindHost: "0.0.0.0",
|
||||
DNS: dnsConfig{
|
||||
BindHost: "0.0.0.0",
|
||||
Port: 53,
|
||||
FilteringConfig: dnsforward.FilteringConfig{
|
||||
ProtectionEnabled: true, // whether or not use any of dnsfilter features
|
||||
FilteringEnabled: true, // whether or not use filter lists
|
||||
BlockedResponseTTL: 10, // in seconds
|
||||
QueryLogEnabled: true,
|
||||
Ratelimit: 20,
|
||||
RefuseAny: true,
|
||||
BootstrapDNS: "8.8.8.8:53",
|
||||
},
|
||||
UpstreamDNS: defaultDNS,
|
||||
},
|
||||
TLS: tlsConfig{
|
||||
tlsConfigSettings: tlsConfigSettings{
|
||||
PortHTTPS: 443,
|
||||
PortDNSOverTLS: 853, // needs to be passed through to dnsproxy
|
||||
},
|
||||
},
|
||||
Filters: []filter{
|
||||
{ID: 1, Enabled: true, URL: "https://adguardteam.github.io/AdGuardSDNSFilter/Filters/filter.txt", Name: "AdGuard Simplified Domain Names filter"},
|
||||
{ID: 2, Enabled: false, URL: "https://adaway.org/hosts.txt", Name: "AdAway"},
|
||||
{ID: 3, Enabled: false, URL: "https://hosts-file.net/ad_servers.txt", Name: "hpHosts - Ad and Tracking servers only"},
|
||||
{ID: 4, Enabled: false, URL: "http://www.malwaredomainlist.com/hostslist/hosts.txt", Name: "MalwareDomainList.com Hosts List"},
|
||||
{Filter: dnsfilter.Filter{ID: 1}, Enabled: true, URL: "https://adguardteam.github.io/AdGuardSDNSFilter/Filters/filter.txt", Name: "AdGuard Simplified Domain Names filter"},
|
||||
{Filter: dnsfilter.Filter{ID: 2}, Enabled: false, URL: "https://adaway.org/hosts.txt", Name: "AdAway"},
|
||||
{Filter: dnsfilter.Filter{ID: 3}, Enabled: false, URL: "https://hosts-file.net/ad_servers.txt", Name: "hpHosts - Ad and Tracking servers only"},
|
||||
{Filter: dnsfilter.Filter{ID: 4}, Enabled: false, URL: "http://www.malwaredomainlist.com/hostslist/hosts.txt", Name: "MalwareDomainList.com Hosts List"},
|
||||
},
|
||||
SchemaVersion: currentSchemaVersion,
|
||||
}
|
||||
|
||||
// Creates a helper object for working with the user rules
|
||||
func getUserFilter() filter {
|
||||
|
||||
// TODO: This should be calculated when UserRules are set
|
||||
var contents []byte
|
||||
for _, rule := range config.UserRules {
|
||||
contents = append(contents, []byte(rule)...)
|
||||
contents = append(contents, '\n')
|
||||
// getConfigFilename returns path to the current config file
|
||||
func (c *configuration) getConfigFilename() string {
|
||||
configFile := config.ourConfigFilename
|
||||
if !filepath.IsAbs(configFile) {
|
||||
configFile = filepath.Join(config.ourWorkingDir, config.ourConfigFilename)
|
||||
}
|
||||
|
||||
userFilter := filter{
|
||||
// User filter always has constant ID=0
|
||||
ID: UserFilterId,
|
||||
contents: contents,
|
||||
Enabled: true,
|
||||
}
|
||||
|
||||
return userFilter
|
||||
return configFile
|
||||
}
|
||||
|
||||
// Loads configuration from the YAML file
|
||||
// getLogSettings reads logging settings from the config file.
|
||||
// we do it in a separate method in order to configure logger before the actual configuration is parsed and applied.
|
||||
func getLogSettings() logSettings {
|
||||
l := logSettings{}
|
||||
yamlFile, err := readConfigFile()
|
||||
if err != nil || yamlFile == nil {
|
||||
return l
|
||||
}
|
||||
err = yaml.Unmarshal(yamlFile, &l)
|
||||
if err != nil {
|
||||
log.Printf("Couldn't get logging settings from the configuration: %s", err)
|
||||
}
|
||||
return l
|
||||
}
|
||||
|
||||
// parseConfig loads configuration from the YAML file
|
||||
func parseConfig() error {
|
||||
configFile := filepath.Join(config.ourBinaryDir, config.ourConfigFilename)
|
||||
log.Printf("Reading YAML file: %s", configFile)
|
||||
if _, err := os.Stat(configFile); os.IsNotExist(err) {
|
||||
// do nothing, file doesn't exist
|
||||
log.Printf("YAML file doesn't exist, skipping: %s", configFile)
|
||||
return nil
|
||||
}
|
||||
yamlFile, err := ioutil.ReadFile(configFile)
|
||||
configFile := config.getConfigFilename()
|
||||
log.Printf("Reading config file: %s", configFile)
|
||||
yamlFile, err := readConfigFile()
|
||||
if err != nil {
|
||||
log.Printf("Couldn't read config file: %s", err)
|
||||
return err
|
||||
}
|
||||
if yamlFile == nil {
|
||||
log.Printf("YAML file doesn't exist, skipping it")
|
||||
return nil
|
||||
}
|
||||
err = yaml.Unmarshal(yamlFile, &config)
|
||||
if err != nil {
|
||||
log.Printf("Couldn't parse config file: %s", err)
|
||||
@@ -160,47 +177,55 @@ func parseConfig() error {
|
||||
}
|
||||
|
||||
// Deduplicate filters
|
||||
{
|
||||
i := 0 // output index, used for deletion later
|
||||
urls := map[string]bool{}
|
||||
for _, filter := range config.Filters {
|
||||
if _, ok := urls[filter.URL]; !ok {
|
||||
// we didn't see it before, keep it
|
||||
urls[filter.URL] = true // remember the URL
|
||||
config.Filters[i] = filter
|
||||
i++
|
||||
}
|
||||
}
|
||||
// all entries we want to keep are at front, delete the rest
|
||||
config.Filters = config.Filters[:i]
|
||||
}
|
||||
deduplicateFilters()
|
||||
|
||||
// Set the next filter ID to max(filter.ID) + 1
|
||||
for i := range config.Filters {
|
||||
if NextFilterId < config.Filters[i].ID {
|
||||
NextFilterId = config.Filters[i].ID + 1
|
||||
}
|
||||
}
|
||||
updateUniqueFilterID(config.Filters)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// readConfigFile reads config file contents if it exists
|
||||
func readConfigFile() ([]byte, error) {
|
||||
configFile := config.getConfigFilename()
|
||||
if _, err := os.Stat(configFile); os.IsNotExist(err) {
|
||||
// do nothing, file doesn't exist
|
||||
return nil, nil
|
||||
}
|
||||
return ioutil.ReadFile(configFile)
|
||||
}
|
||||
|
||||
// Saves configuration to the YAML file and also saves the user filter contents to a file
|
||||
func writeConfig() error {
|
||||
configFile := filepath.Join(config.ourBinaryDir, config.ourConfigFilename)
|
||||
log.Printf("Writing YAML file: %s", configFile)
|
||||
func (c *configuration) write() error {
|
||||
c.Lock()
|
||||
defer c.Unlock()
|
||||
if config.firstRun {
|
||||
log.Tracef("Silently refusing to write config because first run and not configured yet")
|
||||
return nil
|
||||
}
|
||||
configFile := config.getConfigFilename()
|
||||
log.Tracef("Writing YAML file: %s", configFile)
|
||||
yamlText, err := yaml.Marshal(&config)
|
||||
if err != nil {
|
||||
log.Printf("Couldn't generate YAML file: %s", err)
|
||||
return err
|
||||
}
|
||||
err = writeFileSafe(configFile, yamlText)
|
||||
err = safeWriteFile(configFile, yamlText)
|
||||
if err != nil {
|
||||
log.Printf("Couldn't save YAML config: %s", err)
|
||||
return err
|
||||
}
|
||||
|
||||
userFilter := getUserFilter()
|
||||
return nil
|
||||
}
|
||||
|
||||
func writeAllConfigs() error {
|
||||
err := config.write()
|
||||
if err != nil {
|
||||
log.Printf("Couldn't write config: %s", err)
|
||||
return err
|
||||
}
|
||||
|
||||
userFilter := userFilter()
|
||||
err = userFilter.save()
|
||||
if err != nil {
|
||||
log.Printf("Couldn't save the user filter: %s", err)
|
||||
@@ -209,107 +234,3 @@ func writeConfig() error {
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// --------------
|
||||
// coredns config
|
||||
// --------------
|
||||
func writeCoreDNSConfig() error {
|
||||
coreFile := filepath.Join(config.ourBinaryDir, config.CoreDNS.coreFile)
|
||||
log.Printf("Writing DNS config: %s", coreFile)
|
||||
configText, err := generateCoreDNSConfigText()
|
||||
if err != nil {
|
||||
log.Printf("Couldn't generate DNS config: %s", err)
|
||||
return err
|
||||
}
|
||||
err = writeFileSafe(coreFile, []byte(configText))
|
||||
if err != nil {
|
||||
log.Printf("Couldn't save DNS config: %s", err)
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func writeAllConfigs() error {
|
||||
err := writeConfig()
|
||||
if err != nil {
|
||||
log.Printf("Couldn't write our config: %s", err)
|
||||
return err
|
||||
}
|
||||
err = writeCoreDNSConfig()
|
||||
if err != nil {
|
||||
log.Printf("Couldn't write DNS config: %s", err)
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
const coreDNSConfigTemplate = `.:{{.Port}} {
|
||||
{{if .ProtectionEnabled}}dnsfilter {
|
||||
{{if .SafeBrowsingEnabled}}safebrowsing{{end}}
|
||||
{{if .ParentalEnabled}}parental {{.ParentalSensitivity}}{{end}}
|
||||
{{if .SafeSearchEnabled}}safesearch{{end}}
|
||||
{{if .QueryLogEnabled}}querylog{{end}}
|
||||
blocked_ttl {{.BlockedResponseTTL}}
|
||||
{{if .FilteringEnabled}}
|
||||
{{range .Filters}}
|
||||
filter {{.ID}} "{{.Path}}"
|
||||
{{end}}
|
||||
{{end}}
|
||||
}{{end}}
|
||||
{{.Pprof}}
|
||||
{{if .RefuseAny}}refuseany{{end}}
|
||||
{{if gt .Ratelimit 0}}ratelimit {{.Ratelimit}}{{end}}
|
||||
hosts {
|
||||
fallthrough
|
||||
}
|
||||
{{if .UpstreamDNS}}upstream {{range .UpstreamDNS}}{{.}} {{end}} { bootstrap {{.BootstrapDNS}} }{{end}}
|
||||
{{.Cache}}
|
||||
{{.Prometheus}}
|
||||
}
|
||||
`
|
||||
|
||||
var removeEmptyLines = regexp.MustCompile("([\t ]*\n)+")
|
||||
|
||||
// generate CoreDNS config text
|
||||
func generateCoreDNSConfigText() (string, error) {
|
||||
t, err := template.New("config").Parse(coreDNSConfigTemplate)
|
||||
if err != nil {
|
||||
log.Printf("Couldn't generate DNS config: %s", err)
|
||||
return "", err
|
||||
}
|
||||
|
||||
var configBytes bytes.Buffer
|
||||
temporaryConfig := config.CoreDNS
|
||||
|
||||
// fill the list of filters
|
||||
filters := make([]coreDnsFilter, 0)
|
||||
|
||||
// first of all, append the user filter
|
||||
userFilter := getUserFilter()
|
||||
|
||||
if len(userFilter.contents) > 0 {
|
||||
filters = append(filters, coreDnsFilter{ID: userFilter.ID, Path: userFilter.getFilterFilePath()})
|
||||
}
|
||||
|
||||
// then go through other filters
|
||||
for i := range config.Filters {
|
||||
filter := &config.Filters[i]
|
||||
|
||||
if filter.Enabled && len(filter.contents) > 0 {
|
||||
filters = append(filters, coreDnsFilter{ID: filter.ID, Path: filter.getFilterFilePath()})
|
||||
}
|
||||
}
|
||||
temporaryConfig.Filters = filters
|
||||
|
||||
// run the template
|
||||
err = t.Execute(&configBytes, &temporaryConfig)
|
||||
if err != nil {
|
||||
log.Printf("Couldn't generate DNS config: %s", err)
|
||||
return "", err
|
||||
}
|
||||
configText := configBytes.String()
|
||||
|
||||
// remove empty lines from generated config
|
||||
configText = removeEmptyLines.ReplaceAllString(configText, "\n")
|
||||
return configText, nil
|
||||
}
|
||||
|
||||
1250
control.go
1250
control.go
File diff suppressed because it is too large
Load Diff
132
coredns.go
132
coredns.go
@@ -1,132 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sync" // Include all plugins.
|
||||
|
||||
_ "github.com/AdguardTeam/AdGuardHome/coredns_plugin"
|
||||
_ "github.com/AdguardTeam/AdGuardHome/coredns_plugin/ratelimit"
|
||||
_ "github.com/AdguardTeam/AdGuardHome/coredns_plugin/refuseany"
|
||||
_ "github.com/AdguardTeam/AdGuardHome/upstream"
|
||||
"github.com/coredns/coredns/core/dnsserver"
|
||||
"github.com/coredns/coredns/coremain"
|
||||
_ "github.com/coredns/coredns/plugin/auto"
|
||||
_ "github.com/coredns/coredns/plugin/autopath"
|
||||
_ "github.com/coredns/coredns/plugin/bind"
|
||||
_ "github.com/coredns/coredns/plugin/cache"
|
||||
_ "github.com/coredns/coredns/plugin/chaos"
|
||||
_ "github.com/coredns/coredns/plugin/debug"
|
||||
_ "github.com/coredns/coredns/plugin/dnssec"
|
||||
_ "github.com/coredns/coredns/plugin/dnstap"
|
||||
_ "github.com/coredns/coredns/plugin/erratic"
|
||||
_ "github.com/coredns/coredns/plugin/errors"
|
||||
_ "github.com/coredns/coredns/plugin/file"
|
||||
_ "github.com/coredns/coredns/plugin/forward"
|
||||
_ "github.com/coredns/coredns/plugin/health"
|
||||
_ "github.com/coredns/coredns/plugin/hosts"
|
||||
_ "github.com/coredns/coredns/plugin/loadbalance"
|
||||
_ "github.com/coredns/coredns/plugin/log"
|
||||
_ "github.com/coredns/coredns/plugin/loop"
|
||||
_ "github.com/coredns/coredns/plugin/metadata"
|
||||
_ "github.com/coredns/coredns/plugin/metrics"
|
||||
_ "github.com/coredns/coredns/plugin/nsid"
|
||||
_ "github.com/coredns/coredns/plugin/pprof"
|
||||
_ "github.com/coredns/coredns/plugin/proxy"
|
||||
_ "github.com/coredns/coredns/plugin/reload"
|
||||
_ "github.com/coredns/coredns/plugin/rewrite"
|
||||
_ "github.com/coredns/coredns/plugin/root"
|
||||
_ "github.com/coredns/coredns/plugin/secondary"
|
||||
_ "github.com/coredns/coredns/plugin/template"
|
||||
_ "github.com/coredns/coredns/plugin/tls"
|
||||
_ "github.com/coredns/coredns/plugin/whoami"
|
||||
_ "github.com/mholt/caddy/onevent"
|
||||
)
|
||||
|
||||
// Directives are registered in the order they should be
|
||||
// executed.
|
||||
//
|
||||
// Ordering is VERY important. Every plugin will
|
||||
// feel the effects of all other plugin below
|
||||
// (after) them during a request, but they must not
|
||||
// care what plugin above them are doing.
|
||||
|
||||
var directives = []string{
|
||||
"metadata",
|
||||
"tls",
|
||||
"reload",
|
||||
"nsid",
|
||||
"root",
|
||||
"bind",
|
||||
"debug",
|
||||
"health",
|
||||
"pprof",
|
||||
"prometheus",
|
||||
"errors",
|
||||
"log",
|
||||
"refuseany",
|
||||
"ratelimit",
|
||||
"dnsfilter",
|
||||
"dnstap",
|
||||
"chaos",
|
||||
"loadbalance",
|
||||
"cache",
|
||||
"rewrite",
|
||||
"dnssec",
|
||||
"autopath",
|
||||
"template",
|
||||
"hosts",
|
||||
"file",
|
||||
"auto",
|
||||
"secondary",
|
||||
"loop",
|
||||
"forward",
|
||||
"proxy",
|
||||
"upstream",
|
||||
"erratic",
|
||||
"whoami",
|
||||
"on",
|
||||
}
|
||||
|
||||
func init() {
|
||||
dnsserver.Directives = directives
|
||||
}
|
||||
|
||||
var (
|
||||
isCoreDNSRunningLock sync.Mutex
|
||||
isCoreDNSRunning = false
|
||||
)
|
||||
|
||||
func isRunning() bool {
|
||||
isCoreDNSRunningLock.Lock()
|
||||
value := isCoreDNSRunning
|
||||
isCoreDNSRunningLock.Unlock()
|
||||
return value
|
||||
}
|
||||
|
||||
func startDNSServer() error {
|
||||
isCoreDNSRunningLock.Lock()
|
||||
if isCoreDNSRunning {
|
||||
isCoreDNSRunningLock.Unlock()
|
||||
return fmt.Errorf("Unable to start coreDNS: Already running")
|
||||
}
|
||||
isCoreDNSRunning = true
|
||||
isCoreDNSRunningLock.Unlock()
|
||||
|
||||
configpath := filepath.Join(config.ourBinaryDir, config.CoreDNS.coreFile)
|
||||
os.Args = os.Args[:1]
|
||||
os.Args = append(os.Args, "-conf")
|
||||
os.Args = append(os.Args, configpath)
|
||||
|
||||
err := writeCoreDNSConfig()
|
||||
if err != nil {
|
||||
errortext := fmt.Errorf("Unable to write coredns config: %s", err)
|
||||
log.Println(errortext)
|
||||
return errortext
|
||||
}
|
||||
|
||||
go coremain.Run()
|
||||
return nil
|
||||
}
|
||||
@@ -1,559 +0,0 @@
|
||||
package dnsfilter
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"errors"
|
||||
"fmt"
|
||||
"log"
|
||||
"net"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
"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"
|
||||
"golang.org/x/net/context"
|
||||
)
|
||||
|
||||
var defaultSOA = &dns.SOA{
|
||||
// values copied from verisign's nonexistent .com domain
|
||||
// their exact values are not important in our use case because they are used for domain transfers between primary/secondary DNS servers
|
||||
Refresh: 1800,
|
||||
Retry: 900,
|
||||
Expire: 604800,
|
||||
Minttl: 86400,
|
||||
}
|
||||
|
||||
func init() {
|
||||
caddy.RegisterPlugin("dnsfilter", caddy.Plugin{
|
||||
ServerType: "dns",
|
||||
Action: setup,
|
||||
})
|
||||
}
|
||||
|
||||
type plugFilter struct {
|
||||
ID int64
|
||||
Path string
|
||||
}
|
||||
|
||||
type plugSettings struct {
|
||||
SafeBrowsingBlockHost string
|
||||
ParentalBlockHost string
|
||||
QueryLogEnabled bool
|
||||
BlockedTTL uint32 // in seconds, default 3600
|
||||
Filters []plugFilter
|
||||
}
|
||||
|
||||
type plug struct {
|
||||
d *dnsfilter.Dnsfilter
|
||||
Next plugin.Handler
|
||||
upstream upstream.Upstream
|
||||
settings plugSettings
|
||||
|
||||
sync.RWMutex
|
||||
}
|
||||
|
||||
var defaultPluginSettings = plugSettings{
|
||||
SafeBrowsingBlockHost: "safebrowsing.block.dns.adguard.com",
|
||||
ParentalBlockHost: "family.block.dns.adguard.com",
|
||||
BlockedTTL: 3600, // in seconds
|
||||
Filters: make([]plugFilter, 0),
|
||||
}
|
||||
|
||||
//
|
||||
// coredns handling functions
|
||||
//
|
||||
func setupPlugin(c *caddy.Controller) (*plug, error) {
|
||||
// create new Plugin and copy default values
|
||||
p := &plug{
|
||||
settings: defaultPluginSettings,
|
||||
d: dnsfilter.New(),
|
||||
}
|
||||
|
||||
log.Println("Initializing the CoreDNS plugin")
|
||||
|
||||
for c.Next() {
|
||||
for c.NextBlock() {
|
||||
blockValue := c.Val()
|
||||
switch blockValue {
|
||||
case "safebrowsing":
|
||||
log.Println("Browsing security service is enabled")
|
||||
p.d.EnableSafeBrowsing()
|
||||
if c.NextArg() {
|
||||
if len(c.Val()) == 0 {
|
||||
return nil, c.ArgErr()
|
||||
}
|
||||
p.d.SetSafeBrowsingServer(c.Val())
|
||||
}
|
||||
case "safesearch":
|
||||
log.Println("Safe search is enabled")
|
||||
p.d.EnableSafeSearch()
|
||||
case "parental":
|
||||
if !c.NextArg() {
|
||||
return nil, c.ArgErr()
|
||||
}
|
||||
sensitivity, err := strconv.Atoi(c.Val())
|
||||
if err != nil {
|
||||
return nil, c.ArgErr()
|
||||
}
|
||||
|
||||
log.Println("Parental control is enabled")
|
||||
err = p.d.EnableParental(sensitivity)
|
||||
if err != nil {
|
||||
return nil, c.ArgErr()
|
||||
}
|
||||
if c.NextArg() {
|
||||
if len(c.Val()) == 0 {
|
||||
return nil, c.ArgErr()
|
||||
}
|
||||
p.settings.ParentalBlockHost = c.Val()
|
||||
}
|
||||
case "blocked_ttl":
|
||||
if !c.NextArg() {
|
||||
return nil, c.ArgErr()
|
||||
}
|
||||
blockedTtl, err := strconv.ParseUint(c.Val(), 10, 32)
|
||||
if err != nil {
|
||||
return nil, c.ArgErr()
|
||||
}
|
||||
log.Printf("Blocked request TTL is %d", blockedTtl)
|
||||
p.settings.BlockedTTL = uint32(blockedTtl)
|
||||
case "querylog":
|
||||
log.Println("Query log is enabled")
|
||||
p.settings.QueryLogEnabled = true
|
||||
case "filter":
|
||||
if !c.NextArg() {
|
||||
return nil, c.ArgErr()
|
||||
}
|
||||
|
||||
filterId, err := strconv.ParseInt(c.Val(), 10, 64)
|
||||
if err != nil {
|
||||
return nil, c.ArgErr()
|
||||
}
|
||||
if !c.NextArg() {
|
||||
return nil, c.ArgErr()
|
||||
}
|
||||
filterPath := c.Val()
|
||||
|
||||
// Initialize filter and add it to the list
|
||||
p.settings.Filters = append(p.settings.Filters, plugFilter{
|
||||
ID: filterId,
|
||||
Path: filterPath,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for _, filter := range p.settings.Filters {
|
||||
log.Printf("Loading rules from %s", filter.Path)
|
||||
|
||||
file, err := os.Open(filter.Path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
//noinspection GoDeferInLoop
|
||||
defer file.Close()
|
||||
|
||||
count := 0
|
||||
scanner := bufio.NewScanner(file)
|
||||
for scanner.Scan() {
|
||||
text := scanner.Text()
|
||||
|
||||
err = p.d.AddRule(text, filter.ID)
|
||||
if err == dnsfilter.ErrAlreadyExists || err == dnsfilter.ErrInvalidSyntax {
|
||||
continue
|
||||
}
|
||||
if err != nil {
|
||||
log.Printf("Cannot add rule %s: %s", text, err)
|
||||
// Just ignore invalid rules
|
||||
continue
|
||||
}
|
||||
count++
|
||||
}
|
||||
log.Printf("Added %d rules from filter ID=%d", count, filter.ID)
|
||||
|
||||
if err = scanner.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
log.Printf("Loading stats from querylog")
|
||||
err := fillStatsFromQueryLog()
|
||||
if err != nil {
|
||||
log.Printf("Failed to load stats from querylog: %s", err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
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 p, nil
|
||||
}
|
||||
|
||||
func setup(c *caddy.Controller) error {
|
||||
p, err := setupPlugin(c)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
config := dnsserver.GetConfig(c)
|
||||
config.AddPlugin(func(next plugin.Handler) plugin.Handler {
|
||||
p.Next = next
|
||||
return p
|
||||
})
|
||||
|
||||
c.OnStartup(func() error {
|
||||
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(p.onShutdown)
|
||||
c.OnFinalShutdown(p.onFinalShutdown)
|
||||
|
||||
return 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
|
||||
}
|
||||
|
||||
type statsFunc func(ch interface{}, name string, text string, value float64, valueType prometheus.ValueType)
|
||||
|
||||
//noinspection GoUnusedParameter
|
||||
func doDesc(ch interface{}, name string, text string, value float64, valueType prometheus.ValueType) {
|
||||
realch, ok := ch.(chan<- *prometheus.Desc)
|
||||
if !ok {
|
||||
log.Printf("Couldn't convert ch to chan<- *prometheus.Desc\n")
|
||||
return
|
||||
}
|
||||
realch <- prometheus.NewDesc(name, text, nil, nil)
|
||||
}
|
||||
|
||||
func doMetric(ch interface{}, name string, text string, value float64, valueType prometheus.ValueType) {
|
||||
realch, ok := ch.(chan<- prometheus.Metric)
|
||||
if !ok {
|
||||
log.Printf("Couldn't convert ch to chan<- prometheus.Metric\n")
|
||||
return
|
||||
}
|
||||
desc := prometheus.NewDesc(name, text, nil, nil)
|
||||
realch <- prometheus.MustNewConstMetric(desc, valueType, value)
|
||||
}
|
||||
|
||||
func gen(ch interface{}, doFunc statsFunc, name string, text string, value float64, valueType prometheus.ValueType) {
|
||||
doFunc(ch, name, text, value, valueType)
|
||||
}
|
||||
|
||||
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 (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()
|
||||
}
|
||||
|
||||
// Describe is called by prometheus handler to know stat types
|
||||
func (p *plug) Describe(ch chan<- *prometheus.Desc) {
|
||||
p.doStats(ch, doDesc)
|
||||
}
|
||||
|
||||
// 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) // debug logging
|
||||
if addr != nil {
|
||||
// this is an IP address, return it
|
||||
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)
|
||||
}
|
||||
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 := 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)
|
||||
}
|
||||
if result != nil {
|
||||
for _, answer := range result.Answer {
|
||||
answer.Header().Name = question.Name
|
||||
}
|
||||
records = result.Answer
|
||||
}
|
||||
}
|
||||
m := new(dns.Msg)
|
||||
m.SetReply(r)
|
||||
m.Authoritative, m.RecursionAvailable, m.Compress = true, true, true
|
||||
m.Answer = append(m.Answer, records...)
|
||||
state := request.Request{W: w, Req: r, Context: ctx}
|
||||
state.SizeAndDo(m)
|
||||
err := state.W.WriteMsg(m)
|
||||
if err != nil {
|
||||
log.Printf("Got error %s\n", err)
|
||||
return dns.RcodeServerFailure, fmt.Errorf("plugin/dnsfilter: %s", err)
|
||||
}
|
||||
return dns.RcodeSuccess, nil
|
||||
}
|
||||
|
||||
// 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 (p *plug) genSOA(r *dns.Msg) []dns.RR {
|
||||
zone := r.Question[0].Name
|
||||
header := dns.RR_Header{Name: zone, Rrtype: dns.TypeSOA, Ttl: p.settings.BlockedTTL, Class: dns.ClassINET}
|
||||
|
||||
Mbox := "hostmaster."
|
||||
if zone[0] != '.' {
|
||||
Mbox += zone
|
||||
}
|
||||
Ns := "fake-for-negative-caching.adguard.com."
|
||||
|
||||
soa := *defaultSOA
|
||||
soa.Hdr = header
|
||||
soa.Mbox = Mbox
|
||||
soa.Ns = Ns
|
||||
soa.Serial = 100500 // faster than uint32(time.Now().Unix())
|
||||
return []dns.RR{&soa}
|
||||
}
|
||||
|
||||
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 = p.genSOA(r)
|
||||
|
||||
state.SizeAndDo(m)
|
||||
err := state.W.WriteMsg(m)
|
||||
if err != nil {
|
||||
log.Printf("Got error %s\n", err)
|
||||
return dns.RcodeServerFailure, err
|
||||
}
|
||||
return dns.RcodeNameError, nil
|
||||
}
|
||||
|
||||
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, dnsfilter.Result{}, fmt.Errorf("got a DNS request with more than one Question")
|
||||
}
|
||||
for _, question := range r.Question {
|
||||
host := strings.ToLower(strings.TrimSuffix(question.Name, "."))
|
||||
// is it a safesearch domain?
|
||||
p.RLock()
|
||||
if val, ok := p.d.SafeSearchDomain(host); ok {
|
||||
rcode, err := p.replaceHostWithValAndReply(ctx, w, r, host, val, question)
|
||||
if err != nil {
|
||||
p.RUnlock()
|
||||
return rcode, dnsfilter.Result{}, err
|
||||
}
|
||||
p.RUnlock()
|
||||
return rcode, dnsfilter.Result{Reason: dnsfilter.FilteredSafeSearch}, err
|
||||
}
|
||||
p.RUnlock()
|
||||
|
||||
// needs to be filtered instead
|
||||
p.RLock()
|
||||
result, err := p.d.CheckHost(host)
|
||||
if err != nil {
|
||||
log.Printf("plugin/dnsfilter: %s\n", err)
|
||||
p.RUnlock()
|
||||
return dns.RcodeServerFailure, dnsfilter.Result{}, fmt.Errorf("plugin/dnsfilter: %s", err)
|
||||
}
|
||||
p.RUnlock()
|
||||
|
||||
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:
|
||||
|
||||
if result.Ip == nil {
|
||||
// return NXDomain
|
||||
rcode, err := p.writeNXdomain(ctx, w, r)
|
||||
if err != nil {
|
||||
return rcode, dnsfilter.Result{}, err
|
||||
}
|
||||
return rcode, result, err
|
||||
} else {
|
||||
// This is a hosts-syntax rule
|
||||
rcode, err := p.replaceHostWithValAndReply(ctx, w, r, host, result.Ip.String(), question)
|
||||
if err != nil {
|
||||
return rcode, dnsfilter.Result{}, err
|
||||
}
|
||||
return rcode, result, err
|
||||
}
|
||||
case dnsfilter.FilteredInvalid:
|
||||
// return NXdomain
|
||||
rcode, err := p.writeNXdomain(ctx, w, r)
|
||||
if err != nil {
|
||||
return rcode, dnsfilter.Result{}, err
|
||||
}
|
||||
return rcode, result, err
|
||||
default:
|
||||
log.Printf("SHOULD NOT HAPPEN -- got unknown reason for filtering host \"%s\": %v, %+v", host, result.Reason, result)
|
||||
}
|
||||
} 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
rcode, err := plugin.NextOrFailure(p.Name(), p.Next, ctx, w, r)
|
||||
return rcode, dnsfilter.Result{}, err
|
||||
}
|
||||
|
||||
// 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, result, err := p.serveDNSInternal(ctx, rrw, r)
|
||||
if rcode > 0 {
|
||||
// actually send the answer if we have one
|
||||
answer := new(dns.Msg)
|
||||
answer.SetRcode(r, rcode)
|
||||
state.SizeAndDo(answer)
|
||||
err = w.WriteMsg(answer)
|
||||
if err != nil {
|
||||
return dns.RcodeServerFailure, err
|
||||
}
|
||||
}
|
||||
|
||||
// increment counters
|
||||
switch {
|
||||
case err != nil:
|
||||
errorsTotal.Inc()
|
||||
case result.Reason == dnsfilter.FilteredBlackList:
|
||||
filtered.Inc()
|
||||
filteredLists.Inc()
|
||||
case result.Reason == dnsfilter.FilteredSafeBrowsing:
|
||||
filtered.Inc()
|
||||
filteredSafebrowsing.Inc()
|
||||
case result.Reason == dnsfilter.FilteredParental:
|
||||
filtered.Inc()
|
||||
filteredParental.Inc()
|
||||
case result.Reason == dnsfilter.FilteredInvalid:
|
||||
filtered.Inc()
|
||||
filteredInvalid.Inc()
|
||||
case result.Reason == dnsfilter.FilteredSafeSearch:
|
||||
// the request was passsed through but not filtered, don't increment filtered
|
||||
safesearch.Inc()
|
||||
case result.Reason == dnsfilter.NotFilteredWhiteList:
|
||||
whitelisted.Inc()
|
||||
case result.Reason == dnsfilter.NotFilteredNotFound:
|
||||
// do nothing
|
||||
case result.Reason == dnsfilter.NotFilteredError:
|
||||
text := "SHOULD NOT HAPPEN: got DNSFILTER_NOTFILTERED_ERROR without err != nil!"
|
||||
log.Println(text)
|
||||
err = errors.New(text)
|
||||
rcode = dns.RcodeServerFailure
|
||||
}
|
||||
|
||||
// log
|
||||
elapsed := time.Since(start)
|
||||
elapsedTime.Observe(elapsed.Seconds())
|
||||
if p.settings.QueryLogEnabled {
|
||||
logRequest(r, rrw.Msg, result, time.Since(start), ip)
|
||||
}
|
||||
return rcode, err
|
||||
}
|
||||
|
||||
// Name returns name of the plugin as seen in Corefile and plugin.cfg
|
||||
func (p *plug) Name() string { return "dnsfilter" }
|
||||
|
||||
var onceHook sync.Once
|
||||
var onceQueryLog sync.Once
|
||||
@@ -1,131 +0,0 @@
|
||||
package dnsfilter
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net"
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/coredns/coredns/plugin"
|
||||
"github.com/coredns/coredns/plugin/pkg/dnstest"
|
||||
"github.com/coredns/coredns/plugin/test"
|
||||
"github.com/mholt/caddy"
|
||||
"github.com/miekg/dns"
|
||||
)
|
||||
|
||||
func TestSetup(t *testing.T) {
|
||||
for i, testcase := range []struct {
|
||||
config string
|
||||
failing bool
|
||||
}{
|
||||
{`dnsfilter`, false},
|
||||
{`dnsfilter {
|
||||
filter 0 /dev/nonexistent/abcdef
|
||||
}`, true},
|
||||
{`dnsfilter {
|
||||
filter 0 ../tests/dns.txt
|
||||
}`, false},
|
||||
{`dnsfilter {
|
||||
safebrowsing
|
||||
filter 0 ../tests/dns.txt
|
||||
}`, false},
|
||||
{`dnsfilter {
|
||||
parental
|
||||
filter 0 ../tests/dns.txt
|
||||
}`, true},
|
||||
} {
|
||||
c := caddy.NewTestController("dns", testcase.config)
|
||||
err := setup(c)
|
||||
if err != nil {
|
||||
if !testcase.failing {
|
||||
t.Fatalf("Test #%d expected no errors, but got: %v", i, err)
|
||||
}
|
||||
continue
|
||||
}
|
||||
if testcase.failing {
|
||||
t.Fatalf("Test #%d expected to fail but it didn't", i)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestEtcHostsFilter(t *testing.T) {
|
||||
text := []byte("127.0.0.1 doubleclick.net\n" + "127.0.0.1 example.org example.net www.example.org www.example.net")
|
||||
tmpfile, err := ioutil.TempFile("", "")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, err = tmpfile.Write(text); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err = tmpfile.Close(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
defer os.Remove(tmpfile.Name())
|
||||
|
||||
configText := fmt.Sprintf("dnsfilter {\nfilter 0 %s\n}", tmpfile.Name())
|
||||
c := caddy.NewTestController("dns", configText)
|
||||
p, err := setupPlugin(c)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
p.Next = zeroTTLBackend()
|
||||
|
||||
ctx := context.TODO()
|
||||
|
||||
for _, testcase := range []struct {
|
||||
host string
|
||||
filtered bool
|
||||
}{
|
||||
{"www.doubleclick.net", false},
|
||||
{"doubleclick.net", true},
|
||||
{"www2.example.org", false},
|
||||
{"www2.example.net", false},
|
||||
{"test.www.example.org", false},
|
||||
{"test.www.example.net", false},
|
||||
{"example.org", true},
|
||||
{"example.net", true},
|
||||
{"www.example.org", true},
|
||||
{"www.example.net", true},
|
||||
} {
|
||||
req := new(dns.Msg)
|
||||
req.SetQuestion(testcase.host+".", dns.TypeA)
|
||||
|
||||
resp := test.ResponseWriter{}
|
||||
rrw := dnstest.NewRecorder(&resp)
|
||||
rcode, err := p.ServeDNS(ctx, rrw, req)
|
||||
if err != nil {
|
||||
t.Fatalf("ServeDNS returned error: %s", err)
|
||||
}
|
||||
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)
|
||||
}
|
||||
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 && testcase.filtered != filtered {
|
||||
t.Fatalf("Host %s expected to be not filtered, instead it is filtered", testcase.host)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func zeroTTLBackend() plugin.Handler {
|
||||
return plugin.HandlerFunc(func(ctx context.Context, w dns.ResponseWriter, r *dns.Msg) (int, error) {
|
||||
m := new(dns.Msg)
|
||||
m.SetReply(r)
|
||||
m.Response, m.RecursionAvailable = true, true
|
||||
|
||||
m.Answer = []dns.RR{test.A("example.org. 0 IN A 127.0.0.53")}
|
||||
w.WriteMsg(m)
|
||||
return dns.RcodeSuccess, nil
|
||||
})
|
||||
}
|
||||
@@ -1,410 +0,0 @@
|
||||
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
|
||||
}
|
||||
@@ -1,240 +0,0 @@
|
||||
package dnsfilter
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
"path"
|
||||
"runtime"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/AdguardTeam/AdGuardHome/dnsfilter"
|
||||
"github.com/coredns/coredns/plugin/pkg/response"
|
||||
"github.com/miekg/dns"
|
||||
)
|
||||
|
||||
const (
|
||||
logBufferCap = 5000 // maximum capacity of logBuffer before it's flushed to disk
|
||||
queryLogTimeLimit = time.Hour * 24 // how far in the past we care about querylogs
|
||||
queryLogRotationPeriod = time.Hour * 24 // rotate the log every 24 hours
|
||||
queryLogFileName = "querylog.json" // .gz added during compression
|
||||
queryLogSize = 5000 // maximum API response for /querylog
|
||||
queryLogTopSize = 500 // Keep in memory only top N values
|
||||
)
|
||||
|
||||
var (
|
||||
logBufferLock sync.RWMutex
|
||||
logBuffer []*logEntry
|
||||
|
||||
queryLogCache []*logEntry
|
||||
queryLogLock sync.RWMutex
|
||||
)
|
||||
|
||||
type logEntry struct {
|
||||
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 logRequest(question *dns.Msg, answer *dns.Msg, result dnsfilter.Result, elapsed time.Duration, ip string) {
|
||||
var q []byte
|
||||
var a []byte
|
||||
var err error
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
//noinspection GoUnusedParameter
|
||||
func HandleQueryLog(w http.ResponseWriter, r *http.Request) {
|
||||
queryLogLock.RLock()
|
||||
values := make([]*logEntry, len(queryLogCache))
|
||||
copy(values, queryLogCache)
|
||||
queryLogLock.RUnlock()
|
||||
|
||||
// reverse it so that newest is first
|
||||
for left, right := 0, len(values)-1; left < right; left, right = left+1, right-1 {
|
||||
values[left], values[right] = values[right], values[left]
|
||||
}
|
||||
|
||||
var data = []map[string]interface{}{}
|
||||
for _, 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(),
|
||||
"elapsedMs": strconv.FormatFloat(entry.Elapsed.Seconds()*1000, 'f', -1, 64),
|
||||
"time": entry.Time.Format(time.RFC3339),
|
||||
"client": entry.IP,
|
||||
}
|
||||
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(),
|
||||
}
|
||||
}
|
||||
|
||||
if a != nil {
|
||||
status, _ := response.Typify(a, time.Now().UTC())
|
||||
jsonEntry["status"] = status.String()
|
||||
}
|
||||
if len(entry.Result.Rule) > 0 {
|
||||
jsonEntry["rule"] = entry.Result.Rule
|
||||
jsonEntry["filterId"] = entry.Result.FilterID
|
||||
}
|
||||
|
||||
if a != nil && len(a.Answer) > 0 {
|
||||
var answers = []map[string]interface{}{}
|
||||
for _, k := range a.Answer {
|
||||
header := k.Header()
|
||||
answer := map[string]interface{}{
|
||||
"type": dns.TypeToString[header.Rrtype],
|
||||
"ttl": header.Ttl,
|
||||
}
|
||||
// try most common record types
|
||||
switch v := k.(type) {
|
||||
case *dns.A:
|
||||
answer["value"] = v.A
|
||||
case *dns.AAAA:
|
||||
answer["value"] = v.AAAA
|
||||
case *dns.MX:
|
||||
answer["value"] = fmt.Sprintf("%v %v", v.Preference, v.Mx)
|
||||
case *dns.CNAME:
|
||||
answer["value"] = v.Target
|
||||
case *dns.NS:
|
||||
answer["value"] = v.Ns
|
||||
case *dns.SPF:
|
||||
answer["value"] = v.Txt
|
||||
case *dns.TXT:
|
||||
answer["value"] = v.Txt
|
||||
case *dns.PTR:
|
||||
answer["value"] = v.Ptr
|
||||
case *dns.SOA:
|
||||
answer["value"] = fmt.Sprintf("%v %v %v %v %v %v %v", v.Ns, v.Mbox, v.Serial, v.Refresh, v.Retry, v.Expire, v.Minttl)
|
||||
case *dns.CAA:
|
||||
answer["value"] = fmt.Sprintf("%v %v \"%v\"", v.Flag, v.Tag, v.Value)
|
||||
case *dns.HINFO:
|
||||
answer["value"] = fmt.Sprintf("\"%v\" \"%v\"", v.Cpu, v.Os)
|
||||
case *dns.RRSIG:
|
||||
answer["value"] = fmt.Sprintf("%v %v %v %v %v %v %v %v %v", dns.TypeToString[v.TypeCovered], v.Algorithm, v.Labels, v.OrigTtl, v.Expiration, v.Inception, v.KeyTag, v.SignerName, v.Signature)
|
||||
default:
|
||||
// type unknown, marshall it as-is
|
||||
answer["value"] = v
|
||||
}
|
||||
answers = append(answers, answer)
|
||||
}
|
||||
jsonEntry["answer"] = answers
|
||||
}
|
||||
|
||||
data = append(data, jsonEntry)
|
||||
}
|
||||
|
||||
jsonVal, err := json.Marshal(data)
|
||||
if err != nil {
|
||||
errorText := fmt.Sprintf("Couldn't marshal data into json: %s", err)
|
||||
log.Println(errorText)
|
||||
http.Error(w, errorText, http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
_, err = w.Write(jsonVal)
|
||||
if err != nil {
|
||||
errorText := fmt.Sprintf("Unable to write response json: %s", err)
|
||||
log.Println(errorText)
|
||||
http.Error(w, errorText, http.StatusInternalServerError)
|
||||
}
|
||||
}
|
||||
|
||||
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())
|
||||
}
|
||||
@@ -1,386 +0,0 @@
|
||||
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)
|
||||
}
|
||||
}
|
||||
@@ -1,184 +0,0 @@
|
||||
package ratelimit
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"log"
|
||||
"sort"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
// ratelimiting and per-ip buckets
|
||||
"github.com/beefsack/go-rate"
|
||||
"github.com/patrickmn/go-cache"
|
||||
|
||||
// coredns plugin
|
||||
"github.com/coredns/coredns/core/dnsserver"
|
||||
"github.com/coredns/coredns/plugin"
|
||||
"github.com/coredns/coredns/plugin/metrics"
|
||||
"github.com/coredns/coredns/plugin/pkg/dnstest"
|
||||
"github.com/coredns/coredns/request"
|
||||
"github.com/mholt/caddy"
|
||||
"github.com/miekg/dns"
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
"golang.org/x/net/context"
|
||||
)
|
||||
|
||||
const defaultRatelimit = 30
|
||||
const defaultResponseSize = 1000
|
||||
|
||||
var (
|
||||
tokenBuckets = cache.New(time.Hour, time.Hour)
|
||||
)
|
||||
|
||||
// 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)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
if !allow {
|
||||
ratelimited.Inc()
|
||||
return 0, nil
|
||||
}
|
||||
|
||||
// Record response to get status code and size of the reply.
|
||||
rw := dnstest.NewRecorder(w)
|
||||
status, err := plugin.NextOrFailure(p.Name(), p.Next, ctx, rw, r)
|
||||
|
||||
size := rw.Len
|
||||
|
||||
if size > defaultResponseSize && state.Proto() == "udp" {
|
||||
// For large UDP responses we call allowRequest more times
|
||||
// The exact number of times depends on the response size
|
||||
for i := 0; i < size/defaultResponseSize; i++ {
|
||||
p.allowRequest(ip)
|
||||
}
|
||||
}
|
||||
|
||||
return status, err
|
||||
}
|
||||
|
||||
func (p *plug) allowRequest(ip string) (bool, error) {
|
||||
|
||||
if len(p.whitelist) > 0 {
|
||||
i := sort.SearchStrings(p.whitelist, ip)
|
||||
|
||||
if i < len(p.whitelist) && p.whitelist[i] == ip {
|
||||
return true, nil
|
||||
}
|
||||
}
|
||||
|
||||
if _, found := tokenBuckets.Get(ip); !found {
|
||||
tokenBuckets.Set(ip, rate.New(p.ratelimit, time.Second), time.Hour)
|
||||
}
|
||||
|
||||
value, found := tokenBuckets.Get(ip)
|
||||
if !found {
|
||||
// should not happen since we've just inserted it
|
||||
text := "SHOULD NOT HAPPEN: just-inserted ratelimiter disappeared"
|
||||
log.Println(text)
|
||||
err := errors.New(text)
|
||||
return true, err
|
||||
}
|
||||
|
||||
rl, ok := value.(*rate.RateLimiter)
|
||||
if !ok {
|
||||
text := "SHOULD NOT HAPPEN: non-bool entry found in safebrowsing lookup cache"
|
||||
log.Println(text)
|
||||
err := errors.New(text)
|
||||
return true, err
|
||||
}
|
||||
|
||||
allow, _ := rl.Try()
|
||||
return allow, nil
|
||||
}
|
||||
|
||||
//
|
||||
// helper functions
|
||||
//
|
||||
func init() {
|
||||
caddy.RegisterPlugin("ratelimit", caddy.Plugin{
|
||||
ServerType: "dns",
|
||||
Action: setup,
|
||||
})
|
||||
}
|
||||
|
||||
type plug struct {
|
||||
Next plugin.Handler
|
||||
|
||||
// configuration for creating above
|
||||
ratelimit int // in requests per second per IP
|
||||
whitelist []string // a list of whitelisted IP addresses
|
||||
}
|
||||
|
||||
func setupPlugin(c *caddy.Controller) (*plug, error) {
|
||||
|
||||
p := &plug{ratelimit: defaultRatelimit}
|
||||
|
||||
for c.Next() {
|
||||
args := c.RemainingArgs()
|
||||
if len(args) > 0 {
|
||||
ratelimit, err := strconv.Atoi(args[0])
|
||||
if err != nil {
|
||||
return nil, c.ArgErr()
|
||||
}
|
||||
p.ratelimit = ratelimit
|
||||
}
|
||||
for c.NextBlock() {
|
||||
switch c.Val() {
|
||||
case "whitelist":
|
||||
p.whitelist = c.RemainingArgs()
|
||||
|
||||
if len(p.whitelist) > 0 {
|
||||
sort.Strings(p.whitelist)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return p, nil
|
||||
}
|
||||
|
||||
func setup(c *caddy.Controller) error {
|
||||
p, err := setupPlugin(c)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
config := dnsserver.GetConfig(c)
|
||||
config.AddPlugin(func(next plugin.Handler) plugin.Handler {
|
||||
p.Next = next
|
||||
return p
|
||||
})
|
||||
|
||||
c.OnStartup(func() error {
|
||||
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 {
|
||||
return prometheus.NewCounter(prometheus.CounterOpts{
|
||||
Namespace: plugin.Namespace,
|
||||
Subsystem: "ratelimit",
|
||||
Name: name,
|
||||
Help: help,
|
||||
})
|
||||
}
|
||||
|
||||
var (
|
||||
ratelimited = newDNSCounter("dropped_total", "Count of requests that have been dropped because of rate limit")
|
||||
)
|
||||
|
||||
// Name returns name of the plugin as seen in Corefile and plugin.cfg
|
||||
func (p *plug) Name() string { return "ratelimit" }
|
||||
@@ -1,82 +0,0 @@
|
||||
package ratelimit
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/mholt/caddy"
|
||||
)
|
||||
|
||||
func TestSetup(t *testing.T) {
|
||||
for i, testcase := range []struct {
|
||||
config string
|
||||
failing bool
|
||||
}{
|
||||
{`ratelimit`, false},
|
||||
{`ratelimit 100`, false},
|
||||
{`ratelimit {
|
||||
whitelist 127.0.0.1
|
||||
}`, false},
|
||||
{`ratelimit 50 {
|
||||
whitelist 127.0.0.1 176.103.130.130
|
||||
}`, false},
|
||||
{`ratelimit test`, true},
|
||||
} {
|
||||
c := caddy.NewTestController("dns", testcase.config)
|
||||
err := setup(c)
|
||||
if err != nil {
|
||||
if !testcase.failing {
|
||||
t.Fatalf("Test #%d expected no errors, but got: %v", i, err)
|
||||
}
|
||||
continue
|
||||
}
|
||||
if testcase.failing {
|
||||
t.Fatalf("Test #%d expected to fail but it didn't", i)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestRatelimiting(t *testing.T) {
|
||||
|
||||
// rate limit is 1 per sec
|
||||
c := caddy.NewTestController("dns", `ratelimit 1`)
|
||||
p, err := setupPlugin(c)
|
||||
|
||||
if err != nil {
|
||||
t.Fatal("Failed to initialize the plugin")
|
||||
}
|
||||
|
||||
allowed, err := p.allowRequest("127.0.0.1")
|
||||
|
||||
if err != nil || !allowed {
|
||||
t.Fatal("First request must have been allowed")
|
||||
}
|
||||
|
||||
allowed, err = p.allowRequest("127.0.0.1")
|
||||
|
||||
if err != nil || allowed {
|
||||
t.Fatal("Second request must have been ratelimited")
|
||||
}
|
||||
}
|
||||
|
||||
func TestWhitelist(t *testing.T) {
|
||||
|
||||
// rate limit is 1 per sec
|
||||
c := caddy.NewTestController("dns", `ratelimit 1 { whitelist 127.0.0.2 127.0.0.1 127.0.0.125 }`)
|
||||
p, err := setupPlugin(c)
|
||||
|
||||
if err != nil {
|
||||
t.Fatal("Failed to initialize the plugin")
|
||||
}
|
||||
|
||||
allowed, err := p.allowRequest("127.0.0.1")
|
||||
|
||||
if err != nil || !allowed {
|
||||
t.Fatal("First request must have been allowed")
|
||||
}
|
||||
|
||||
allowed, err = p.allowRequest("127.0.0.1")
|
||||
|
||||
if err != nil || !allowed {
|
||||
t.Fatal("Second request must have been allowed due to whitelist")
|
||||
}
|
||||
}
|
||||
@@ -1,91 +0,0 @@
|
||||
package refuseany
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
|
||||
"github.com/coredns/coredns/core/dnsserver"
|
||||
"github.com/coredns/coredns/plugin"
|
||||
"github.com/coredns/coredns/plugin/metrics"
|
||||
"github.com/coredns/coredns/request"
|
||||
"github.com/mholt/caddy"
|
||||
"github.com/miekg/dns"
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
"golang.org/x/net/context"
|
||||
)
|
||||
|
||||
type plug struct {
|
||||
Next plugin.Handler
|
||||
}
|
||||
|
||||
// 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")
|
||||
}
|
||||
|
||||
q := r.Question[0]
|
||||
if q.Qtype == dns.TypeANY {
|
||||
state := request.Request{W: w, Req: r, Context: ctx}
|
||||
rcode := dns.RcodeNotImplemented
|
||||
|
||||
m := new(dns.Msg)
|
||||
m.SetRcode(r, rcode)
|
||||
state.SizeAndDo(m)
|
||||
err := state.W.WriteMsg(m)
|
||||
if err != nil {
|
||||
log.Printf("Got error %s\n", err)
|
||||
return dns.RcodeServerFailure, err
|
||||
}
|
||||
return rcode, nil
|
||||
}
|
||||
|
||||
return plugin.NextOrFailure(p.Name(), p.Next, ctx, w, r)
|
||||
}
|
||||
|
||||
func init() {
|
||||
caddy.RegisterPlugin("refuseany", caddy.Plugin{
|
||||
ServerType: "dns",
|
||||
Action: setup,
|
||||
})
|
||||
}
|
||||
|
||||
func setup(c *caddy.Controller) error {
|
||||
p := &plug{}
|
||||
config := dnsserver.GetConfig(c)
|
||||
|
||||
config.AddPlugin(func(next plugin.Handler) plugin.Handler {
|
||||
p.Next = next
|
||||
return p
|
||||
})
|
||||
|
||||
c.OnStartup(func() error {
|
||||
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 {
|
||||
return prometheus.NewCounter(prometheus.CounterOpts{
|
||||
Namespace: plugin.Namespace,
|
||||
Subsystem: "refuseany",
|
||||
Name: name,
|
||||
Help: help,
|
||||
})
|
||||
}
|
||||
|
||||
var (
|
||||
ratelimited = newDNSCounter("refusedany_total", "Count of ANY requests that have been dropped")
|
||||
)
|
||||
|
||||
// Name returns name of the plugin as seen in Corefile and plugin.cfg
|
||||
func (p *plug) Name() string { return "refuseany" }
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user