Compare commits
158 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0f7235f217 | ||
|
|
db67fb6c6a | ||
|
|
087d2f68c2 | ||
|
|
02fa39226c | ||
|
|
edfa104710 | ||
|
|
7a124213e5 | ||
|
|
0a2a7ca630 | ||
|
|
1f164c7005 | ||
|
|
44f224d69e | ||
|
|
22469bb83b | ||
|
|
10a0873bc8 | ||
|
|
a165410c9f | ||
|
|
ec5e2be31f | ||
|
|
6d3099acd3 | ||
|
|
66c670c6ff | ||
|
|
c2a31f9503 | ||
|
|
466f553bbe | ||
|
|
ddb1bc0fee | ||
|
|
a36630e5a8 | ||
|
|
3958330560 | ||
|
|
86ba6d4332 | ||
|
|
b2364e465f | ||
|
|
a3b8d4d923 | ||
|
|
3454bf9243 | ||
|
|
64a4443d0c | ||
|
|
d24b78db0e | ||
|
|
f7150e6a19 | ||
|
|
85046abb15 | ||
|
|
454e26db7f | ||
|
|
b74438bf83 | ||
|
|
7d40d3bfea | ||
|
|
6261fb79ab | ||
|
|
27bffef940 | ||
|
|
450e2ac549 | ||
|
|
6ac466e430 | ||
|
|
f7d88f6976 | ||
|
|
7f5ac19b59 | ||
|
|
54f6710b8f | ||
|
|
757bb7285a | ||
|
|
af041bcbd7 | ||
|
|
cf53653cfa | ||
|
|
1d09ff0562 | ||
|
|
c93cb43db8 | ||
|
|
276d87a218 | ||
|
|
fcf609ac1e | ||
|
|
e4532a27cd | ||
|
|
302a11a6a3 | ||
|
|
b8d9ca942c | ||
|
|
df9864ec00 | ||
|
|
3baa6919dc | ||
|
|
02db488b30 | ||
|
|
821ad3edd9 | ||
|
|
d18c222b1a | ||
|
|
36ffcf7d22 | ||
|
|
147344afa3 | ||
|
|
1abd9da27d | ||
|
|
d9e70f5244 | ||
|
|
a1ceb83da0 | ||
|
|
a12f01793f | ||
|
|
b1fbd7c40c | ||
|
|
2976726f99 | ||
|
|
6f2503a09f | ||
|
|
a8384c004e | ||
|
|
49b91b4fc9 | ||
|
|
fa47fa3f9c | ||
|
|
763b986955 | ||
|
|
342699d933 | ||
|
|
fd593f5282 | ||
|
|
725aeeb910 | ||
|
|
564a41d598 | ||
|
|
c3204664c3 | ||
|
|
626c1ae753 | ||
|
|
cc366495d3 | ||
|
|
0d405c0af8 | ||
|
|
c038e4cf14 | ||
|
|
a83bc5eeeb | ||
|
|
702db84e39 | ||
|
|
9cc824d852 | ||
|
|
8a8c7329f7 | ||
|
|
cbef338592 | ||
|
|
bd2c4269db | ||
|
|
f40141bbbc | ||
|
|
c7b5830336 | ||
|
|
bb34381a0d | ||
|
|
68a4cc597f | ||
|
|
22d3c38df2 | ||
|
|
22c7efd2d1 | ||
|
|
eb159e6997 | ||
|
|
8bf76c331d | ||
|
|
4bb7b654ab | ||
|
|
3f89335ed2 | ||
|
|
8f7aff93d7 | ||
|
|
5fb7e44e79 | ||
|
|
6a7b1aba8b | ||
|
|
218f51092c | ||
|
|
9f75146eab | ||
|
|
6ab8aa4da1 | ||
|
|
386886cec2 | ||
|
|
5b29cae133 | ||
|
|
4df8868787 | ||
|
|
517ebc0251 | ||
|
|
f25639f1fc | ||
|
|
f23507a554 | ||
|
|
b9df476c5d | ||
|
|
e2579c72bd | ||
|
|
ac8f703407 | ||
|
|
9ad4bba9ab | ||
|
|
452c930dd0 | ||
|
|
fdd0f594fb | ||
|
|
00e1b6ca08 | ||
|
|
dece393d6a | ||
|
|
aa2d942783 | ||
|
|
e3ee7a0c3e | ||
|
|
70e3299567 | ||
|
|
967517316f | ||
|
|
24f582d36d | ||
|
|
9cffe865ec | ||
|
|
cb3f7f2834 | ||
|
|
096a959987 | ||
|
|
5ec747b30b | ||
|
|
829415da5b | ||
|
|
3396d68019 | ||
|
|
bd68bf2e25 | ||
|
|
9644f79a03 | ||
|
|
36e273dfd5 | ||
|
|
068072bc5a | ||
|
|
b72ca4d127 | ||
|
|
28440fc3ac | ||
|
|
6d14ec18ac | ||
|
|
5fd35254a8 | ||
|
|
3ee8051e97 | ||
|
|
2dd6ea5161 | ||
|
|
788e91a51e | ||
|
|
d4fcef8d04 | ||
|
|
392c7b6ee1 | ||
|
|
7bb40bca0f | ||
|
|
f20cb65189 | ||
|
|
d5f6dd1a46 | ||
|
|
0f28a989e9 | ||
|
|
d918e5b418 | ||
|
|
3a0f608402 | ||
|
|
cd2dd00da3 | ||
|
|
07ffcbec3d | ||
|
|
b3461d37ca | ||
|
|
2178546e7b | ||
|
|
24ae61de3e | ||
|
|
68f9ec70fb | ||
|
|
17aa46c4d2 | ||
|
|
a45f0c519e | ||
|
|
2cb2b3585f | ||
|
|
d24f208f98 | ||
|
|
6ac9509d64 | ||
|
|
7d2df26335 | ||
|
|
ae403fb137 | ||
|
|
e1bb89c393 | ||
|
|
c4e67690f4 | ||
|
|
f6023b395e | ||
|
|
58868b75af |
353
AGHTechDoc.md
353
AGHTechDoc.md
@@ -9,10 +9,24 @@ Contents:
|
||||
* "Check configuration" command
|
||||
* Disable DNSStubListener
|
||||
* "Apply configuration" command
|
||||
* Updating
|
||||
* Get version command
|
||||
* Update command
|
||||
* Device Names and Per-client Settings
|
||||
* Per-client settings
|
||||
* Get list of clients
|
||||
* Add client
|
||||
* Update client
|
||||
* Delete client
|
||||
* Enable DHCP server
|
||||
* "Show DHCP status" command
|
||||
* "Check DHCP" command
|
||||
* "Enable DHCP" command
|
||||
* Static IP check/set
|
||||
* Add a static lease
|
||||
* DNS access settings
|
||||
* List access settings
|
||||
* Set access settings
|
||||
|
||||
|
||||
## First startup
|
||||
@@ -187,6 +201,103 @@ On error, server responds with code 400 or 500. In this case UI should show err
|
||||
ERROR MESSAGE
|
||||
|
||||
|
||||
## Updating
|
||||
|
||||
Algorithm of an update by command:
|
||||
|
||||
* UI requests the latest version information from Server
|
||||
* Server requests information from Internet; stores the data in cache for several hours; sends data to UI
|
||||
* If UI sees that a new version is available, it shows notification message and "Update Now" button
|
||||
* When user clicks on "Update Now" button, UI sends Update command to Server
|
||||
* UI shows "Please wait, AGH is being updated..." message
|
||||
* Server performs an update:
|
||||
* Use working directory from `--work-dir` if necessary
|
||||
* Download new package for the current OS and CPU
|
||||
* Unpack the package to a temporary directory `update-vXXX`
|
||||
* Copy the current configuration file to the directory we unpacked new AGH to
|
||||
* Check configuration compatibility by executing `./AGH --check-config`. If this command fails, we won't be able to update.
|
||||
* Create `backup-vXXX` directory and copy the current configuration file there
|
||||
* Copy supporting files (README, LICENSE, etc.) to backup directory
|
||||
* Copy supporting files from the update directory to the current directory
|
||||
* Move the current binary file to backup directory
|
||||
* Note: if power fails here, AGH won't be able to start at system boot. Administrator has to fix it manually
|
||||
* Move new binary file to the current directory
|
||||
* Send response to UI
|
||||
* Stop all tasks, including DNS server, DHCP server, HTTP server
|
||||
* If AGH is running as a service, use service control functionality to restart
|
||||
* If AGH is not running as a service, use the current process arguments to start a new process
|
||||
* Exit process
|
||||
* UI resends Get Status command until Server responds to it with the new version. This means that Server is successfully restarted after update.
|
||||
* UI reloads itself
|
||||
|
||||
|
||||
### Get version command
|
||||
|
||||
On receiving this request server downloads version.json data from github and stores it in cache for several hours.
|
||||
|
||||
Example of version.json data:
|
||||
|
||||
{
|
||||
"version": "v0.95-hotfix",
|
||||
"announcement": "AdGuard Home v0.95-hotfix is now available!",
|
||||
"announcement_url": "",
|
||||
"download_windows_amd64": "",
|
||||
"download_windows_386": "",
|
||||
"download_darwin_amd64": "",
|
||||
"download_linux_amd64": "",
|
||||
"download_linux_386": "",
|
||||
"download_linux_arm": "",
|
||||
"download_linux_arm64": "",
|
||||
"download_linux_mips": "",
|
||||
"download_linux_mipsle": "",
|
||||
"selfupdate_min_version": "v0.0"
|
||||
}
|
||||
|
||||
Server can only auto-update if the current version is equal or higher than `selfupdate_min_version`.
|
||||
|
||||
Request:
|
||||
|
||||
GET /control/version.json
|
||||
|
||||
Response:
|
||||
|
||||
200 OK
|
||||
|
||||
{
|
||||
"new_version": "v0.95",
|
||||
"announcement": "AdGuard Home v0.95 is now available!",
|
||||
"announcement_url": "http://...",
|
||||
"can_autoupdate": true
|
||||
}
|
||||
|
||||
If `can_autoupdate` is true, then the server can automatically upgrade to a new version.
|
||||
|
||||
Response with empty body:
|
||||
|
||||
200 OK
|
||||
|
||||
It means that update check is disabled by user. UI should do nothing.
|
||||
|
||||
|
||||
### Update command
|
||||
|
||||
Perform an update procedure to the latest available version
|
||||
|
||||
Request:
|
||||
|
||||
POST /control/update
|
||||
|
||||
Response:
|
||||
|
||||
200 OK
|
||||
|
||||
Error response:
|
||||
|
||||
500
|
||||
|
||||
UI shows error message "Auto-update has failed"
|
||||
|
||||
|
||||
## Enable DHCP server
|
||||
|
||||
Algorithm:
|
||||
@@ -201,6 +312,38 @@ Algorithm:
|
||||
* UI shows the status
|
||||
|
||||
|
||||
### "Show DHCP status" command
|
||||
|
||||
Request:
|
||||
|
||||
GET /control/dhcp/status
|
||||
|
||||
Response:
|
||||
|
||||
200 OK
|
||||
|
||||
{
|
||||
"config":{
|
||||
"enabled":false,
|
||||
"interface_name":"...",
|
||||
"gateway_ip":"...",
|
||||
"subnet_mask":"...",
|
||||
"range_start":"...",
|
||||
"range_end":"...",
|
||||
"lease_duration":60,
|
||||
"icmp_timeout_msec":0
|
||||
},
|
||||
"leases":[
|
||||
{"ip":"...","mac":"...","hostname":"...","expires":"..."}
|
||||
...
|
||||
],
|
||||
"static_leases":[
|
||||
{"ip":"...","mac":"...","hostname":"..."}
|
||||
...
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
### "Check DHCP" command
|
||||
|
||||
Request:
|
||||
@@ -325,3 +468,213 @@ Step 2.
|
||||
If we would set a different IP address, we'd need to replace the IP address for the current network configuration. But currently this step isn't necessary.
|
||||
|
||||
ip addr replace dev eth0 192.168.0.1/24
|
||||
|
||||
|
||||
### Add a static lease
|
||||
|
||||
Request:
|
||||
|
||||
POST /control/dhcp/add_static_lease
|
||||
|
||||
{
|
||||
"mac":"...",
|
||||
"ip":"...",
|
||||
"hostname":"..."
|
||||
}
|
||||
|
||||
Response:
|
||||
|
||||
200 OK
|
||||
|
||||
|
||||
### Remove a static lease
|
||||
|
||||
Request:
|
||||
|
||||
POST /control/dhcp/remove_static_lease
|
||||
|
||||
{
|
||||
"mac":"...",
|
||||
"ip":"...",
|
||||
"hostname":"..."
|
||||
}
|
||||
|
||||
Response:
|
||||
|
||||
200 OK
|
||||
|
||||
|
||||
## Device Names and Per-client Settings
|
||||
|
||||
When a client requests information from DNS server, he's identified by IP address.
|
||||
Administrator can set a name for a client with a known IP and also override global settings for this client. The name is used to improve readability of DNS logs: client's name is shown in UI next to its IP address. The names are loaded from 3 sources:
|
||||
* automatically from "/etc/hosts" file. It's a list of `IP<->Name` entries which is loaded once on AGH startup from "/etc/hosts" file.
|
||||
* automatically using rDNS. It's a list of `IP<->Name` entries which is added in runtime using rDNS mechanism when a client first makes a DNS request.
|
||||
* manually configured via UI. It's a list of client's names and their settings which is loaded from configuration file and stored on disk.
|
||||
|
||||
### Per-client settings
|
||||
|
||||
UI provides means to manage the list of known clients (List/Add/Update/Delete) and their settings. These settings are stored in configuration file as an array of objects.
|
||||
|
||||
Notes:
|
||||
|
||||
* `name`, `ip` and `mac` values are unique.
|
||||
|
||||
* `ip` & `mac` values can't be set both at the same time.
|
||||
|
||||
* If `mac` is set and DHCP server is enabled, IP is taken from DHCP lease table.
|
||||
|
||||
* If `use_global_settings` is true, then DNS responses for this client are processed and filtered using global settings.
|
||||
|
||||
* If `use_global_settings` is false, then the client-specific settings are used to override (disable) global settings. For example, if global setting `parental_enabled` is true, then per-client setting `parental_enabled:false` can disable Parental Control for this specific client.
|
||||
|
||||
|
||||
### Get list of clients
|
||||
|
||||
Request:
|
||||
|
||||
GET /control/clients
|
||||
|
||||
Response:
|
||||
|
||||
200 OK
|
||||
|
||||
{
|
||||
clients: [
|
||||
{
|
||||
name: "client1"
|
||||
ip: "..."
|
||||
mac: "..."
|
||||
use_global_settings: true
|
||||
filtering_enabled: false
|
||||
parental_enabled: false
|
||||
safebrowsing_enabled: false
|
||||
safesearch_enabled: false
|
||||
}
|
||||
]
|
||||
auto_clients: [
|
||||
{
|
||||
name: "host"
|
||||
ip: "..."
|
||||
source: "etc/hosts" || "rDNS"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
### Add client
|
||||
|
||||
Request:
|
||||
|
||||
POST /control/clients/add
|
||||
|
||||
{
|
||||
name: "client1"
|
||||
ip: "..."
|
||||
mac: "..."
|
||||
use_global_settings: true
|
||||
filtering_enabled: false
|
||||
parental_enabled: false
|
||||
safebrowsing_enabled: false
|
||||
safesearch_enabled: false
|
||||
}
|
||||
|
||||
Response:
|
||||
|
||||
200 OK
|
||||
|
||||
Error response (Client already exists):
|
||||
|
||||
400
|
||||
|
||||
|
||||
### Update client
|
||||
|
||||
Request:
|
||||
|
||||
POST /control/clients/update
|
||||
|
||||
{
|
||||
name: "client1"
|
||||
data: {
|
||||
name: "client1"
|
||||
ip: "..."
|
||||
mac: "..."
|
||||
use_global_settings: true
|
||||
filtering_enabled: false
|
||||
parental_enabled: false
|
||||
safebrowsing_enabled: false
|
||||
safesearch_enabled: false
|
||||
}
|
||||
}
|
||||
|
||||
Response:
|
||||
|
||||
200 OK
|
||||
|
||||
Error response (Client not found):
|
||||
|
||||
400
|
||||
|
||||
|
||||
### Delete client
|
||||
|
||||
Request:
|
||||
|
||||
POST /control/clients/delete
|
||||
|
||||
{
|
||||
name: "client1"
|
||||
}
|
||||
|
||||
Response:
|
||||
|
||||
200 OK
|
||||
|
||||
Error response (Client not found):
|
||||
|
||||
400
|
||||
|
||||
|
||||
## DNS access settings
|
||||
|
||||
There are low-level settings that can block undesired DNS requests. "Blocking" means not responding to request.
|
||||
|
||||
There are 3 types of access settings:
|
||||
* allowed_clients: Only these clients are allowed to make DNS requests.
|
||||
* disallowed_clients: These clients are not allowed to make DNS requests.
|
||||
* blocked_hosts: These hosts are not allowed to be resolved by a DNS request.
|
||||
|
||||
|
||||
### List access settings
|
||||
|
||||
Request:
|
||||
|
||||
GET /control/access/list
|
||||
|
||||
Response:
|
||||
|
||||
200 OK
|
||||
|
||||
{
|
||||
allowed_clients: ["127.0.0.1", ...]
|
||||
disallowed_clients: ["127.0.0.1", ...]
|
||||
blocked_hosts: ["host.com", ...]
|
||||
}
|
||||
|
||||
|
||||
### Set access settings
|
||||
|
||||
Request:
|
||||
|
||||
POST /control/access/set
|
||||
|
||||
{
|
||||
allowed_clients: ["127.0.0.1", ...]
|
||||
disallowed_clients: ["127.0.0.1", ...]
|
||||
blocked_hosts: ["host.com", ...]
|
||||
}
|
||||
|
||||
Response:
|
||||
|
||||
200 OK
|
||||
|
||||
18
Dockerfile
18
Dockerfile
@@ -11,14 +11,22 @@ 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
|
||||
RUN apk --no-cache --update add ca-certificates libcap && \
|
||||
rm -rf /var/cache/apk/* && \
|
||||
mkdir -p /opt/adguardhome/conf /opt/adguardhome/work && \
|
||||
chown -R nobody: /opt/adguardhome
|
||||
|
||||
COPY --from=build /src/AdGuardHome/AdGuardHome /opt/adguardhome/AdGuardHome
|
||||
COPY --from=build --chown=nobody:nogroup /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
|
||||
RUN setcap 'cap_net_bind_service=+eip' /opt/adguardhome/AdGuardHome
|
||||
|
||||
EXPOSE 53/tcp 53/udp 67/udp 68/udp 80/tcp 443/tcp 853/tcp 3000/tcp
|
||||
|
||||
VOLUME ["/opt/adguardhome/conf", "/opt/adguardhome/work"]
|
||||
|
||||
WORKDIR /opt/adguardhome/work
|
||||
|
||||
#USER nobody
|
||||
|
||||
ENTRYPOINT ["/opt/adguardhome/AdGuardHome"]
|
||||
CMD ["-c", "/opt/adguardhome/conf/AdGuardHome.yaml", "-w", "/opt/adguardhome/work"]
|
||||
CMD ["-c", "/opt/adguardhome/conf/AdGuardHome.yaml", "-w", "/opt/adguardhome/work", "--no-check-update"]
|
||||
|
||||
@@ -2,15 +2,22 @@ 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
|
||||
RUN apk --no-cache --update add ca-certificates libcap && \
|
||||
rm -rf /var/cache/apk/* && \
|
||||
mkdir -p /opt/adguardhome/conf /opt/adguardhome/work && \
|
||||
chown -R nobody: /opt/adguardhome
|
||||
|
||||
COPY --chown=nobody:nogroup ./AdGuardHome /opt/adguardhome/AdGuardHome
|
||||
|
||||
COPY ./AdGuardHome /opt/adguardhome/AdGuardHome
|
||||
RUN setcap 'cap_net_bind_service=+eip' /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
|
||||
EXPOSE 53/tcp 53/udp 67/udp 68/udp 80/tcp 443/tcp 853/tcp 3000/tcp
|
||||
|
||||
VOLUME ["/opt/adguardhome/conf", "/opt/adguardhome/work"]
|
||||
|
||||
WORKDIR /opt/adguardhome/work
|
||||
|
||||
#USER nobody
|
||||
|
||||
ENTRYPOINT ["/opt/adguardhome/AdGuardHome"]
|
||||
CMD ["-h", "0.0.0.0", "-c", "/opt/adguardhome/conf/AdGuardHome.yaml", "-w", "/opt/adguardhome/work"]
|
||||
CMD ["-h", "0.0.0.0", "-c", "/opt/adguardhome/conf/AdGuardHome.yaml", "-w", "/opt/adguardhome/work", "--no-check-update"]
|
||||
|
||||
3
Makefile
3
Makefile
@@ -4,6 +4,7 @@ NATIVE_GOARCH = $(shell unset GOARCH; go env GOARCH)
|
||||
GOPATH := $(shell go env GOPATH)
|
||||
JSFILES = $(shell find client -path client/node_modules -prune -o -type f -name '*.js')
|
||||
STATIC = build/static/index.html
|
||||
CHANNEL ?= release
|
||||
|
||||
TARGET=AdGuardHome
|
||||
|
||||
@@ -22,7 +23,7 @@ $(STATIC): $(JSFILES) client/node_modules
|
||||
$(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)"
|
||||
CGO_ENABLED=0 go build -ldflags="-s -w -X main.VersionString=$(GIT_VERSION) -X main.updateChannel=$(CHANNEL)" -asmflags="-trimpath=$(PWD)" -gcflags="-trimpath=$(PWD)"
|
||||
PATH=$(GOPATH)/bin:$(PATH) packr clean
|
||||
|
||||
clean:
|
||||
|
||||
107
README.md
107
README.md
@@ -44,9 +44,15 @@ AdGuard Home is a network-wide software for blocking ads & tracking. After you s
|
||||
It operates as a DNS server that re-routes tracking domains to a "black hole," thus preventing your devices from connecting to those servers. It's based on software we use for our public [AdGuard DNS](https://adguard.com/en/adguard-dns/overview.html) servers -- both share a lot of common code.
|
||||
|
||||
* [Getting Started](#getting-started)
|
||||
* [Comparing AdGuard Home to other solutions](#comparison)
|
||||
* [How is this different from public AdGuard DNS servers?](#comparison-adguard-dns)
|
||||
* [How does AdGuard Home compare to Pi-Hole](#comparison-pi-hole)
|
||||
* [How does AdGuard Home compare to traditional ad blockers](#comparison-adblock)
|
||||
* [How to build from source](#how-to-build)
|
||||
* [Contributing](#contributing)
|
||||
* [Reporting issues](#reporting-issues)
|
||||
* [Test unstable versions](#test-unstable-versions)
|
||||
* [Reporting issues](#reporting-issues)
|
||||
* [Help with translations](#translate)
|
||||
* [Acknowledgments](#acknowledgments)
|
||||
|
||||
<a id="getting-started"></a>
|
||||
@@ -63,6 +69,52 @@ Alternatively, you can use our [official Docker image](https://hub.docker.com/r/
|
||||
* [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="comparison"></a>
|
||||
## Comparing AdGuard Home to other solutions
|
||||
|
||||
<a id="comparison-adguard-dns"></a>
|
||||
### How is this different from public AdGuard DNS servers?
|
||||
|
||||
Running your own AdGuard Home server allows you to do much more than using a public DNS server. It's a completely different level. See for yourself:
|
||||
|
||||
* Choose what exactly will the server block or not block.
|
||||
* Monitor your network activity.
|
||||
* Add your own custom filtering rules.
|
||||
* **Most importantly, this is your own server, and you are the only one who's in control.**
|
||||
|
||||
<a id="comparison-pi-hole"></a>
|
||||
### How does AdGuard Home compare to Pi-Hole
|
||||
|
||||
At this point, AdGuard Home has a lot in common with Pi-Hole. Both block ads and trackers using "DNS sinkholing" method, and both allow customizing what's blocked.
|
||||
|
||||
> We're not going to stop here. DNS sinkholing is not a bad starting point, but this is just the beginning.
|
||||
|
||||
AdGuard Home provides a lot of features out-of-the-box with no need to install and configure additional software. We want it to be simple to the point when even casual users can set it up with minimal effort.
|
||||
|
||||
> Disclaimer: some of the listed features can be added to Pi-Hole by installing additional software or by manually using SSH terminal and reconfiguring one of the utilities Pi-Hole consists of. However, in our opinion, this cannot be legitimately counted as a Pi-Hole's feature.
|
||||
|
||||
| Feature | AdGuard Home | Pi-Hole |
|
||||
|-------------------------------------------------------------------------|--------------|--------------------------------------------------------|
|
||||
| Blocking ads and trackers | ✅ | ✅ |
|
||||
| Customizing blocklists | ✅ | ✅ |
|
||||
| Built-in DHCP server | ✅ | ✅ |
|
||||
| HTTPS for the Admin interface | ✅ | Kind of, but you'll need to manually configure lighthttp |
|
||||
| Encrypted DNS upstream servers (DNS-over-HTTPS, DNS-over-TLS, DNSCrypt) | ✅ | ❌ (requires additional software) |
|
||||
| Cross-platform | ✅ | ❌ (not natively, only via Docker) |
|
||||
| Running as a DNS-over-HTTPS or DNS-over-TLS server | ✅ | ❌ (requires additional software) |
|
||||
| Blocking phishing and malware domains | ✅ | ❌ |
|
||||
| Parental control (blocking adult domains) | ✅ | ❌ |
|
||||
| Force Safe search on search engines | ✅ | ❌ |
|
||||
| Per-client (device) configuration | ✅ | ❌ |
|
||||
| Access settings (choose who can use AGH DNS) | ✅ | ❌ |
|
||||
|
||||
<a id="comparison-adblock"></a>
|
||||
### How does AdGuard Home compare to traditional ad blockers
|
||||
|
||||
It depends.
|
||||
|
||||
"DNS sinkholing" is capable of blocking a big percentage of ads, but it lacks flexibility and power of traditional ad blockers. You can get a good impression about the difference between these methods by reading [this article](https://adguard.com/en/blog/adguard-vs-adaway-dns66/). It compares AdGuard for Android (a traditional ad blocker) to hosts-level ad blockers (which are almost identical to DNS-based blockers in their capabilities). However, this level of protection is enough for some users.
|
||||
|
||||
<a id="how-to-build"></a>
|
||||
## How to build from source
|
||||
|
||||
@@ -89,12 +141,47 @@ cd AdGuardHome
|
||||
make
|
||||
```
|
||||
|
||||
#### (For devs) Upload translations
|
||||
```
|
||||
node upload.js
|
||||
```
|
||||
|
||||
#### (For devs) Download translations
|
||||
```
|
||||
node download.js
|
||||
```
|
||||
|
||||
<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
|
||||
<a id="test-unstable-versions"></a>
|
||||
### Test unstable versions
|
||||
|
||||
There are two options how you can install an unstable version.
|
||||
You can either install a beta version of AdGuard Home which we update periodically,
|
||||
or you can use the Docker image from the `edge` tag, which is synced with the repo master branch.
|
||||
|
||||
* [Docker Hub](https://hub.docker.com/r/adguard/adguardhome)
|
||||
* Beta builds
|
||||
* [Rapsberry Pi (32-bit ARM)](https://static.adguard.com/adguardhome/beta/AdGuardHome_linux_arm.tar.gz)
|
||||
* [MacOS](https://static.adguard.com/adguardhome/beta/AdGuardHome_MacOS.zip)
|
||||
* [Windows 64-bit](https://static.adguard.com/adguardhome/beta/AdGuardHome_Windows_amd64.zip)
|
||||
* [Windows 32-bit](https://static.adguard.com/adguardhome/beta/AdGuardHome_Windows_386.zip)
|
||||
* [Linux 64-bit](https://static.adguard.com/adguardhome/beta/AdGuardHome_linux_amd64.tar.gz)
|
||||
* [Linux 32-bit](https://static.adguard.com/adguardhome/beta/AdGuardHome_linux_386.tar.gz)
|
||||
* [64-bit ARM](https://static.adguard.com/adguardhome/beta/AdGuardHome_linux_arm64.tar.gz)
|
||||
* [MIPS](https://static.adguard.com/adguardhome/beta/AdGuardHome_linux_mips.tar.gz)
|
||||
* [MIPSLE](https://static.adguard.com/adguardhome/beta/AdGuardHome_linux_mipsle.tar.gz)
|
||||
|
||||
<a id="reporting-issues"></a>
|
||||
### Report 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="translate"></a>
|
||||
### Help with 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
|
||||
|
||||
@@ -118,21 +205,6 @@ Example of `oneskyapp.json`
|
||||
}
|
||||
```
|
||||
|
||||
#### Upload translations
|
||||
```
|
||||
node upload.js
|
||||
```
|
||||
|
||||
#### Download translations
|
||||
```
|
||||
node download.js
|
||||
```
|
||||
|
||||
<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
|
||||
|
||||
@@ -145,6 +217,7 @@ This software wouldn't have been possible without:
|
||||
* [go-yaml](https://github.com/go-yaml/yaml)
|
||||
* [service](https://godoc.org/github.com/kardianos/service)
|
||||
* [dnsproxy](https://github.com/AdguardTeam/dnsproxy)
|
||||
* [urlfilter](https://github.com/AdguardTeam/urlfilter)
|
||||
* [Node.js](https://nodejs.org/) and it's libraries:
|
||||
* [React.js](https://reactjs.org)
|
||||
* [Tabler](https://github.com/tabler/tabler)
|
||||
|
||||
99
app.go
99
app.go
@@ -2,6 +2,7 @@ package main
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"fmt"
|
||||
"io"
|
||||
@@ -17,6 +18,7 @@ import (
|
||||
"strings"
|
||||
"sync"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"github.com/AdguardTeam/golibs/log"
|
||||
"github.com/NYTimes/gziphandler"
|
||||
@@ -25,11 +27,19 @@ import (
|
||||
|
||||
// VersionString will be set through ldflags, contains current version
|
||||
var VersionString = "undefined"
|
||||
|
||||
// updateChannel can be set via ldflags
|
||||
var updateChannel = "release"
|
||||
var versionCheckURL = "https://static.adguard.com/adguardhome/" + updateChannel + "/version.json"
|
||||
|
||||
const versionCheckPeriod = time.Hour * 8
|
||||
|
||||
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
|
||||
shutdown bool // if TRUE, don't restart the server
|
||||
}
|
||||
var pidFileName string // PID file name. Empty if no PID file was created.
|
||||
|
||||
@@ -71,11 +81,13 @@ func run(args options) {
|
||||
enableTLS13()
|
||||
|
||||
// print the first message after logger is configured
|
||||
log.Printf("AdGuard Home, version %s\n", VersionString)
|
||||
log.Printf("AdGuard Home, version %s, channel %s\n", VersionString, updateChannel)
|
||||
log.Debug("Current working directory is %s", config.ourWorkingDir)
|
||||
if args.runningAsService {
|
||||
log.Info("AdGuard Home is running as a service")
|
||||
}
|
||||
config.runningAsService = args.runningAsService
|
||||
config.disableUpdate = args.disableUpdate
|
||||
|
||||
config.firstRun = detectFirstRun()
|
||||
if config.firstRun {
|
||||
@@ -91,16 +103,24 @@ func run(args options) {
|
||||
os.Exit(0)
|
||||
}()
|
||||
|
||||
// Do the upgrade if necessary
|
||||
err := upgradeConfig()
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
clientsInit()
|
||||
|
||||
// parse from config file
|
||||
err = parseConfig()
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
if !config.firstRun {
|
||||
// Do the upgrade if necessary
|
||||
err := upgradeConfig()
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
err = parseConfig()
|
||||
if err != nil {
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
if args.checkConfig {
|
||||
log.Info("Configuration file is OK")
|
||||
os.Exit(0)
|
||||
}
|
||||
}
|
||||
|
||||
if (runtime.GOOS == "linux" || runtime.GOOS == "darwin") &&
|
||||
@@ -118,10 +138,12 @@ func run(args options) {
|
||||
|
||||
loadFilters()
|
||||
|
||||
// Save the updated config
|
||||
err = config.write()
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
if !config.firstRun {
|
||||
// Save the updated config
|
||||
err := config.write()
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
// Init the DNS server instance before registering HTTP handlers
|
||||
@@ -129,7 +151,7 @@ func run(args options) {
|
||||
initDNSServer(dnsBaseDir)
|
||||
|
||||
if !config.firstRun {
|
||||
err = startDNSServer()
|
||||
err := startDNSServer()
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
@@ -171,7 +193,7 @@ func run(args options) {
|
||||
go httpServerLoop()
|
||||
|
||||
// this loop is used as an ability to change listening host and/or port
|
||||
for {
|
||||
for !httpsServer.shutdown {
|
||||
printHTTPAddresses("http")
|
||||
|
||||
// we need to have new instance, because after Shutdown() the Server is not usable
|
||||
@@ -186,10 +208,13 @@ func run(args options) {
|
||||
}
|
||||
// We use ErrServerClosed as a sign that we need to rebind on new address, so go back to the start of the loop
|
||||
}
|
||||
|
||||
// wait indefinitely for other go-routines to complete their job
|
||||
select {}
|
||||
}
|
||||
|
||||
func httpServerLoop() {
|
||||
for {
|
||||
for !httpsServer.shutdown {
|
||||
httpsServer.cond.L.Lock()
|
||||
// this mechanism doesn't let us through until all conditions are met
|
||||
for config.TLS.Enabled == false ||
|
||||
@@ -367,11 +392,21 @@ func cleanup() {
|
||||
}
|
||||
}
|
||||
|
||||
// Stop HTTP server, possibly waiting for all active connections to be closed
|
||||
func stopHTTPServer() {
|
||||
httpsServer.shutdown = true
|
||||
if httpsServer.server != nil {
|
||||
httpsServer.server.Shutdown(context.TODO())
|
||||
}
|
||||
httpServer.Shutdown(context.TODO())
|
||||
}
|
||||
|
||||
// This function is called before application exits
|
||||
func cleanupAlways() {
|
||||
if len(pidFileName) != 0 {
|
||||
os.Remove(pidFileName)
|
||||
}
|
||||
log.Info("Stopped")
|
||||
}
|
||||
|
||||
// command-line arguments
|
||||
@@ -383,6 +418,8 @@ type options struct {
|
||||
bindPort int // port to serve HTTP pages on
|
||||
logFile string // Path to the log file. If empty, write to stdout. If "syslog", writes to syslog
|
||||
pidFile string // File name to save PID to
|
||||
checkConfig bool // Check configuration and exit
|
||||
disableUpdate bool // If set, don't check for updates
|
||||
|
||||
// service control action (see service.ControlAction array + "status" command)
|
||||
serviceControlAction string
|
||||
@@ -403,25 +440,27 @@ func loadOptions() options {
|
||||
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) {
|
||||
{"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) {
|
||||
{"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) {
|
||||
{"logfile", "l", "Path to log file. If empty: write to stdout; if 'syslog': write to system log", func(value string) {
|
||||
o.logFile = value
|
||||
}, nil},
|
||||
{"pidfile", "", "File name to save PID to", func(value string) { o.pidFile = value }, nil},
|
||||
{"verbose", "v", "enable verbose output", nil, func() { o.verbose = true }},
|
||||
{"help", "", "print this help", nil, func() {
|
||||
{"pidfile", "", "Path to a file where PID is stored", func(value string) { o.pidFile = value }, nil},
|
||||
{"check-config", "", "Check configuration and exit", nil, func() { o.checkConfig = true }},
|
||||
{"no-check-update", "", "Don't check for updates", nil, func() { o.disableUpdate = true }},
|
||||
{"verbose", "v", "Enable verbose output", nil, func() { o.verbose = true }},
|
||||
{"help", "", "Print this help", nil, func() {
|
||||
printHelp()
|
||||
os.Exit(64)
|
||||
}},
|
||||
@@ -431,10 +470,14 @@ func loadOptions() options {
|
||||
fmt.Printf("%s [options]\n\n", os.Args[0])
|
||||
fmt.Printf("Options:\n")
|
||||
for _, opt := range opts {
|
||||
val := ""
|
||||
if opt.callbackWithValue != nil {
|
||||
val = " VALUE"
|
||||
}
|
||||
if opt.shortName != "" {
|
||||
fmt.Printf(" -%s, %-30s %s\n", opt.shortName, "--"+opt.longName, opt.description)
|
||||
fmt.Printf(" -%s, %-30s %s\n", opt.shortName, "--"+opt.longName+val, opt.description)
|
||||
} else {
|
||||
fmt.Printf(" %-34s %s\n", "--"+opt.longName, opt.description)
|
||||
fmt.Printf(" %-34s %s\n", "--"+opt.longName+val, opt.description)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 15 KiB |
BIN
client/public/favicon.png
Normal file
BIN
client/public/favicon.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.3 KiB |
@@ -5,7 +5,7 @@
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
|
||||
<meta name="theme-color" content="#000000">
|
||||
<meta name="google" content="notranslate">
|
||||
<link rel="shortcut icon" href="favicon.ico">
|
||||
<link rel="icon" type="image/png" href="favicon.png" sizes="48x48">
|
||||
<title>AdGuard Home</title>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
|
||||
<meta name="theme-color" content="#000000">
|
||||
<meta name="google" content="notranslate">
|
||||
<link rel="shortcut icon" href="favicon.ico">
|
||||
<link rel="icon" type="image/png" href="favicon.png" sizes="48x48">
|
||||
<title>Setup AdGuard Home</title>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
@@ -15,10 +15,12 @@
|
||||
"dhcp_not_found": "It is safe to enable the built-in DHCP server - we didn't find any active DHCP servers on the network. However, we encourage you to re-check it manually as our automatic test currently doesn't give 100% guarantee.",
|
||||
"dhcp_found": "An active DHCP server is found on the network. It is not safe to enable the built-in DHCP server.",
|
||||
"dhcp_leases": "DHCP leases",
|
||||
"dhcp_static_leases": "DHCP static 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_mac_format": "Invalid MAC format",
|
||||
"form_error_positive": "Must be greater than 0",
|
||||
"dhcp_form_gateway_input": "Gateway IP",
|
||||
"dhcp_form_subnet_input": "Subnet mask",
|
||||
@@ -35,7 +37,14 @@
|
||||
"dhcp_warning": "If you want to enable DHCP server anyway, make sure that there is no other active DHCP server in your network. Otherwise, it can break the Internet for connected devices!",
|
||||
"dhcp_error": "We could not determine whether there is another DHCP server in the network.",
|
||||
"dhcp_static_ip_error": "In order to use DHCP server a static IP address must be set. We failed to determine if this network interface is configured using static IP address. Please set a static IP address manually.",
|
||||
"dhcp_dynamic_ip_found": "Your system uses dynamic IP address configuration for interface <0>{{interfaceName}}</0>. In order to use DHCP server a static IP address must be set. Your current IP address is <0>{{ipAddress}}</0>. We will automatically set this IP address as static if you press Enable DHCP button.",
|
||||
"dhcp_dynamic_ip_found": "Your system uses dynamic IP address configuration for interface <0>{{interfaceName}}<\/0>. In order to use DHCP server a static IP address must be set. Your current IP address is <0>{{ipAddress}}<\/0>. We will automatically set this IP address as static if you press Enable DHCP button.",
|
||||
"dhcp_lease_added": "Static lease \"{{key}}\" successfully added",
|
||||
"dhcp_lease_deleted": "Static lease \"{{key}}\" successfully deleted",
|
||||
"dhcp_new_static_lease": "New static lease",
|
||||
"dhcp_static_leases_not_found": "No DHCP static leases found",
|
||||
"dhcp_add_static_lease": "Add static lease",
|
||||
"delete_confirm": "Are you sure you want to delete \"{{key}}\"?",
|
||||
"form_enter_hostname": "Enter hostname",
|
||||
"error_details": "Error details",
|
||||
"back": "Back",
|
||||
"dashboard": "Dashboard",
|
||||
@@ -87,6 +96,10 @@
|
||||
"no_servers_specified": "No servers specified",
|
||||
"no_settings": "No settings",
|
||||
"general_settings": "General settings",
|
||||
"dns_settings": "DNS settings",
|
||||
"encryption_settings": "Encryption settings",
|
||||
"dhcp_settings": "DHCP settings",
|
||||
"client_settings": "Client settings",
|
||||
"upstream_dns": "Upstream DNS servers",
|
||||
"upstream_dns_hint": "If you keep this field empty, AdGuard Home will use <a href='https:\/\/1.1.1.1\/' target='_blank'>Cloudflare DNS<\/a> as an upstream.",
|
||||
"test_upstream_btn": "Test upstreams",
|
||||
@@ -105,6 +118,7 @@
|
||||
"rules_count_table_header": "Rules count",
|
||||
"last_time_updated_table_header": "Last time updated",
|
||||
"actions_table_header": "Actions",
|
||||
"edit_table_action": "Edit",
|
||||
"delete_table_action": "Delete",
|
||||
"filters_and_hosts": "Filters and hosts blocklists",
|
||||
"filters_and_hosts_hint": "AdGuard Home understands basic adblock rules and hosts files syntax.",
|
||||
@@ -259,5 +273,44 @@
|
||||
"setup_guide": "Setup guide",
|
||||
"dns_addresses": "DNS addresses",
|
||||
"down": "Down",
|
||||
"fix": "Fix"
|
||||
"fix": "Fix",
|
||||
"dns_providers": "Here is a <0>list of known DNS providers<\/0> to choose from.",
|
||||
"update_now": "Update now",
|
||||
"update_failed": "Auto-update failed. Please <a href='https:\/\/github.com\/AdguardTeam\/AdGuardHome\/wiki\/Getting-Started#update'>follow the steps<\/a> to update manually.",
|
||||
"processing_update": "Please wait, AdGuard Home is being updated",
|
||||
"clients_title": "Clients",
|
||||
"clients_desc": "Configure devices connected to AdGuard Home",
|
||||
"settings_global": "Global",
|
||||
"settings_custom": "Custom",
|
||||
"table_client": "Client",
|
||||
"table_name": "Name",
|
||||
"save_btn": "Save",
|
||||
"client_add": "Add Client",
|
||||
"client_new": "New Client",
|
||||
"client_edit": "Edit Client",
|
||||
"client_identifier": "Identifier",
|
||||
"ip_address": "IP address",
|
||||
"client_identifier_desc": "Clients can be identified by the IP address or MAC address. Please note, that using MAC as identifier is possible only if AdGuard Home is also a <0>DHCP server<\/0>",
|
||||
"form_enter_ip": "Enter IP",
|
||||
"form_enter_mac": "Enter MAC",
|
||||
"form_client_name": "Enter client name",
|
||||
"client_global_settings": "Use global settings",
|
||||
"client_deleted": "Client \"{{key}}\" successfully deleted",
|
||||
"client_added": "Client \"{{key}}\" successfully added",
|
||||
"client_updated": "Client \"{{key}}\" successfully updated",
|
||||
"table_statistics": "Requests count (last 24 hours)",
|
||||
"clients_not_found": "No clients found",
|
||||
"client_confirm_delete": "Are you sure you want to delete client \"{{key}}\"?",
|
||||
"filter_confirm_delete": "Are you sure you want to delete filter?",
|
||||
"auto_clients_title": "Clients (runtime)",
|
||||
"auto_clients_desc": "Data on the clients that use AdGuard Home, but not stored in the configuration",
|
||||
"access_title": "Access settings",
|
||||
"access_desc": "Here you can configure access rules for the AdGuard Home DNS server.",
|
||||
"access_allowed_title": "Allowed clients",
|
||||
"access_allowed_desc": "A list of CIDR or IP addresses. If configured, AdGuard Home will accept requests from these IP addresses only.",
|
||||
"access_disallowed_title": "Disallowed clients",
|
||||
"access_disallowed_desc": "A list of CIDR or IP addresses. If configured, AdGuard Home will drop requests from these IP addresses.",
|
||||
"access_blocked_title": "Blocked domains",
|
||||
"access_blocked_desc": "Don't confuse this with filters. AdGuard Home will drop DNS queries with these domains in query's question.",
|
||||
"access_settings_saved": "Access settings successfully saved"
|
||||
}
|
||||
@@ -2,37 +2,50 @@
|
||||
"example_upstream_reserved": "puede especificar el DNS de subida <0>para un dominio espec\u00edfico<\/0>",
|
||||
"upstream_parallel": "Usar consultas paralelas para acelerar la resoluci\u00f3n al consultar simult\u00e1neamente a todos los servidores de subida",
|
||||
"bootstrap_dns": "Servidores DNS de arranque",
|
||||
"bootstrap_dns_desc": "Los servidores DNS de arranque se utilizan para resolver las direcciones IP de los resolutores DoH\/DoT que especifique como DNS de subida.",
|
||||
"bootstrap_dns_desc": "Los servidores DNS de arranque se utilizan para resolver las direcciones IP de los resolutores DoH\/DoT que usted especifique como DNS de subida.",
|
||||
"url_added_successfully": "URL a\u00f1adida correctamente",
|
||||
"check_dhcp_servers": "Compruebe si hay servidores DHCP",
|
||||
"check_dhcp_servers": "Comprobar si hay servidores DHCP",
|
||||
"save_config": "Guardar configuraci\u00f3n",
|
||||
"enabled_dhcp": "Servidor DHCP habilitado",
|
||||
"disabled_dhcp": "Servidor DHCP deshabilitado",
|
||||
"dhcp_title": "Servidor DHCP (experimental)",
|
||||
"dhcp_description": "Si su router 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": "Algunos servidores DHCP activos se encuentran 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_disable": "Deshabilitar servidor DHCP",
|
||||
"dhcp_not_found": "Es seguro habilitar el servidor DHCP incorporado. No se ha encontrado ning\u00fan servidor DHCP activo en la red, sin embargo le recomendamos que lo vuelva a comprobar manualmente, ya que nuestra prueba autom\u00e1tica no ofrece actualmente una garant\u00eda del 100%.",
|
||||
"dhcp_found": "Un servidor DHCP activo se encuentra en la red. No es seguro habilitar el servidor DHCP incorporado.",
|
||||
"dhcp_leases": "Asignaciones DHCP",
|
||||
"dhcp_static_leases": "DHCP static leases",
|
||||
"dhcp_leases_not_found": "No se han encontrado asignaciones 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_mac_format": "Formato MAC no v\u00e1lido",
|
||||
"form_error_positive": "Debe ser mayor que 0",
|
||||
"dhcp_form_gateway_input": "IP de puerta de enlace",
|
||||
"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_form_lease_title": "Tiempo de asignaci\u00f3n DHCP (en segundos)",
|
||||
"dhcp_form_lease_input": "Duraci\u00f3n de asignaci\u00f3n",
|
||||
"dhcp_interface_select": "Seleccione la interfaz DHCP",
|
||||
"dhcp_hardware_address": "Direcci\u00f3n MAC",
|
||||
"dhcp_ip_addresses": "Direcciones IP",
|
||||
"dhcp_table_hostname": "Nombre del host",
|
||||
"dhcp_table_expires": "Expira",
|
||||
"dhcp_warning": "Si desea habilitar el servidor DHCP incorporado, aseg\u00farese de que no hay otro servidor DHCP activo. \u00a1De lo contrario, puede dejar sin Internet a los dispositivos conectados!",
|
||||
"dhcp_warning": "Si de todos modos desea habilitar el servidor DHCP, aseg\u00farese de que no hay otro servidor DHCP activo en su red. \u00a1De lo contrario, puede dejar sin Internet a los dispositivos conectados!",
|
||||
"dhcp_error": "No pudimos determinar si hay otro servidor DHCP en la red.",
|
||||
"dhcp_static_ip_error": "Para poder utilizar el servidor DHCP se debe establecer una direcci\u00f3n IP est\u00e1tica. No hemos podido determinar si esta interfaz de red est\u00e1 configurada utilizando una direcci\u00f3n IP est\u00e1tica. Por favor establezca una direcci\u00f3n IP est\u00e1tica manualmente.",
|
||||
"dhcp_dynamic_ip_found": "Su sistema utiliza la configuraci\u00f3n de direcci\u00f3n IP din\u00e1mica para la interfaz <0>{{interfaceName}}<\/0>. Para poder utilizar el servidor DHCP se debe establecer una direcci\u00f3n IP est\u00e1tica. Su direcci\u00f3n IP actual es <0>{{ipAddress}}<\/0>. Si presiona el bot\u00f3n Habilitar servidor DHCP, estableceremos autom\u00e1ticamente esta direcci\u00f3n IP como est\u00e1tica.",
|
||||
"dhcp_lease_added": "Static lease \"{{key}}\" successfully added",
|
||||
"dhcp_lease_deleted": "Static lease \"{{key}}\" successfully deleted",
|
||||
"dhcp_new_static_lease": "New static lease",
|
||||
"dhcp_static_leases_not_found": "No DHCP static leases found",
|
||||
"dhcp_add_static_lease": "Add static lease",
|
||||
"delete_confirm": "Are you sure you want to delete \"{{key}}\"?",
|
||||
"form_enter_hostname": "Enter hostname",
|
||||
"error_details": "Detalles del error",
|
||||
"back": "Atr\u00e1s",
|
||||
"dashboard": "Panel de control",
|
||||
"settings": "Configuraci\u00f3n",
|
||||
@@ -46,6 +59,7 @@
|
||||
"copyright": "Copyright",
|
||||
"homepage": "P\u00e1gina de inicio",
|
||||
"report_an_issue": "Reportar un error",
|
||||
"privacy_policy": "Pol\u00edtica de privacidad",
|
||||
"enable_protection": "Habilitar protecci\u00f3n",
|
||||
"enabled_protection": "Protecci\u00f3n habilitada",
|
||||
"disable_protection": "Deshabilitar protecci\u00f3n",
|
||||
@@ -54,19 +68,19 @@
|
||||
"dns_query": "Consultas DNS",
|
||||
"blocked_by": "Bloqueado por filtros",
|
||||
"stats_malware_phishing": "Malware\/phishing bloqueado",
|
||||
"stats_adult": "Sitios para adultos bloqueado",
|
||||
"stats_adult": "Sitios web para adultos bloqueado",
|
||||
"stats_query_domain": "Dominios m\u00e1s consultados",
|
||||
"for_last_24_hours": "en las \u00faltimas 24 horas",
|
||||
"no_domains_found": "Dominios no encontrados",
|
||||
"requests_count": "N\u00famero de solicitudes",
|
||||
"no_domains_found": "No se han encontrado dominios",
|
||||
"requests_count": "N\u00famero de peticiones",
|
||||
"top_blocked_domains": "Dominios m\u00e1s bloqueados",
|
||||
"top_clients": "Clientes m\u00e1s frecuentes",
|
||||
"no_clients_found": "No hay clientes",
|
||||
"no_clients_found": "No se han encontrado clientes",
|
||||
"general_statistics": "Estad\u00edsticas generales",
|
||||
"number_of_dns_query_24_hours": "N\u00famero de consultas DNS procesadas durante las \u00faltimas 24 horas",
|
||||
"number_of_dns_query_blocked_24_hours": "N\u00famero de peticiones DNS bloqueadas por los filtros de publicidad y listas de bloqueo de hosts",
|
||||
"number_of_dns_query_blocked_24_hours_by_sec": "N\u00famero de peticiones DNS bloqueadas por el m\u00f3dulo de navegaci\u00f3n segura de AdGuard",
|
||||
"number_of_dns_query_blocked_24_hours_adult": "N\u00famero de sitios para adultos bloqueado",
|
||||
"number_of_dns_query_blocked_24_hours": "N\u00famero de peticiones DNS bloqueadas por los filtros y listas de bloqueo de hosts",
|
||||
"number_of_dns_query_blocked_24_hours_by_sec": "N\u00famero de peticiones DNS bloqueadas por el m\u00f3dulo de seguridad de navegaci\u00f3n de AdGuard",
|
||||
"number_of_dns_query_blocked_24_hours_adult": "N\u00famero de sitios web para adultos bloqueado",
|
||||
"enforced_save_search": "B\u00fasquedas seguras forzadas",
|
||||
"number_of_dns_query_to_safe_search": "N\u00famero de peticiones DNS a los motores de b\u00fasqueda para los que se aplic\u00f3 la b\u00fasqueda segura forzada",
|
||||
"average_processing_time": "Tiempo promedio de procesamiento",
|
||||
@@ -74,14 +88,18 @@
|
||||
"block_domain_use_filters_and_hosts": "Bloquear dominios usando filtros y archivos hosts",
|
||||
"filters_block_toggle_hint": "Puede configurar las reglas de bloqueo en la configuraci\u00f3n de <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 una API de b\u00fasqueda amigable con la privacidad para realizar la comprobaci\u00f3n: solo se env\u00eda al servidor un prefijo corto de 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 la API de b\u00fasqueda amigable con la privacidad para realizar la comprobaci\u00f3n: solo se env\u00eda al servidor un prefijo corto del nombre de dominio con hash SHA256.",
|
||||
"use_adguard_parental": "Usar el control parental de AdGuard",
|
||||
"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.",
|
||||
"use_adguard_parental_hint": "AdGuard Home comprobar\u00e1 si el dominio contiene materiales para adultos. Utiliza la misma API amigable con la privacidad del 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.",
|
||||
"enforce_save_search_hint": "AdGuard Home puede forzar la b\u00fasqueda segura en los siguientes motores de b\u00fasqueda: Google, YouTube, Bing, DuckDuckGo y Yandex.",
|
||||
"no_servers_specified": "No hay servidores especificados",
|
||||
"no_settings": "Sin configuraci\u00f3n",
|
||||
"general_settings": "Configuraci\u00f3n general",
|
||||
"dns_settings": "DNS settings",
|
||||
"encryption_settings": "Encryption settings",
|
||||
"dhcp_settings": "DHCP settings",
|
||||
"client_settings": "Client settings",
|
||||
"upstream_dns": "Servidores DNS de subida",
|
||||
"upstream_dns_hint": "Si mantiene este campo vac\u00edo, AdGuard Home utilizar\u00e1 <a href='https:\/\/1.1.1.1\/' target='_blank'>Cloudflare DNS<\/a> como DNS de subida. Utilice el prefijo tls:\/\/ para los servidores DNS mediante TLS.",
|
||||
"test_upstream_btn": "Probar DNS de subida",
|
||||
@@ -100,19 +118,20 @@
|
||||
"rules_count_table_header": "N\u00famero de reglas",
|
||||
"last_time_updated_table_header": "\u00daltima actualizaci\u00f3n",
|
||||
"actions_table_header": "Acciones",
|
||||
"edit_table_action": "Editar",
|
||||
"delete_table_action": "Eliminar",
|
||||
"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.",
|
||||
"filters_and_hosts_hint": "AdGuard Home entiende las reglas b\u00e1sicas de bloqueo y la sintaxis de los archivos hosts.",
|
||||
"no_filters_added": "No hay filtros a\u00f1adidos",
|
||||
"add_filter_btn": "A\u00f1adir filtro",
|
||||
"cancel_btn": "Cancelar",
|
||||
"enter_name_hint": "Ingrese el nombre",
|
||||
"enter_url_hint": "Ingrese la URL",
|
||||
"check_updates_btn": "Buscar actualizaciones",
|
||||
"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": "Ingrese una regla en una l\u00ednea. Puede utilizar reglas de bloqueo de anuncios o la sintaxis de archivos de hosts.",
|
||||
"new_filter_btn": "Nueva suscripci\u00f3n a filtro",
|
||||
"enter_valid_filter_url": "Ingrese una URL v\u00e1lida para suscribirse a un filtro o archivo hosts.",
|
||||
"custom_filter_rules": "Reglas de filtrado personalizado",
|
||||
"custom_filter_rules_hint": "Ingrese una regla por l\u00ednea. Puede utilizar reglas de bloqueo o la sintaxis de los archivos hosts.",
|
||||
"examples_title": "Ejemplos",
|
||||
"example_meaning_filter_block": "bloquea el acceso al dominio ejemplo.org\ny a todos sus subdominios",
|
||||
"example_meaning_filter_whitelist": "desbloquea el acceso al dominio ejemplo.org y a todos sus subdominios",
|
||||
@@ -129,7 +148,7 @@
|
||||
"all_filters_up_to_date_toast": "Todos los filtros ya est\u00e1n actualizados",
|
||||
"updated_upstream_dns_toast": "Servidores DNS de subida actualizados",
|
||||
"dns_test_ok_toast": "Los servidores DNS especificados funcionan correctamente",
|
||||
"dns_test_not_ok_toast": "Servidor \"{{key}}\": no puede ser usado, por favor, revise si lo ha escrito correctamente",
|
||||
"dns_test_not_ok_toast": "Servidor \"{{key}}\": no se puede utilizar, por favor revise si lo ha escrito correctamente",
|
||||
"unblock_btn": "Desbloquear",
|
||||
"block_btn": "Bloquear",
|
||||
"time_table_header": "Hora",
|
||||
@@ -164,7 +183,7 @@
|
||||
"filter_label": "Filtro",
|
||||
"unknown_filter": "Filtro desconocido {{filterId}}",
|
||||
"install_welcome_title": "\u00a1Bienvenido a AdGuard Home!",
|
||||
"install_welcome_desc": "AdGuard Home es un servidor DNS de bloqueo de anuncios y rastreadores en toda la red. Su prop\u00f3sito es permitirle controlar toda su red y todos sus dispositivos, y no requiere el uso de un programa del lado del cliente.",
|
||||
"install_welcome_desc": "AdGuard Home es un servidor DNS para bloqueo de anuncios y rastreadores a nivel de red. Su prop\u00f3sito es permitirle controlar toda su red y todos sus dispositivos, y no requiere el uso de un programa del lado del cliente.",
|
||||
"install_settings_title": "Interfaz web de administraci\u00f3n",
|
||||
"install_settings_listen": "Interfaz de escucha",
|
||||
"install_settings_port": "Puerto",
|
||||
@@ -174,7 +193,7 @@
|
||||
"install_settings_dns_desc": "Deber\u00e1 configurar sus dispositivos o router para usar el servidor DNS en las siguientes direcciones:",
|
||||
"install_settings_all_interfaces": "Todas las interfaces",
|
||||
"install_auth_title": "Autenticaci\u00f3n",
|
||||
"install_auth_desc": "Se recomienda encarecidamente configurar la autenticaci\u00f3n por contrase\u00f1a para la interfaz web del administraci\u00f3n de AdGuard Home. Incluso si solo es accesible en su red local, es importante que est\u00e9 protegido contra el acceso no autorizado.",
|
||||
"install_auth_desc": "Se recomienda encarecidamente configurar la autenticaci\u00f3n por contrase\u00f1a para la interfaz web de administraci\u00f3n de AdGuard Home. Incluso si solo es accesible en su red local, es importante que est\u00e9 protegido contra el acceso no autorizado.",
|
||||
"install_auth_username": "Usuario",
|
||||
"install_auth_password": "Contrase\u00f1a",
|
||||
"install_auth_confirm": "Confirmar contrase\u00f1a",
|
||||
@@ -187,7 +206,7 @@
|
||||
"install_submit_desc": "El proceso de configuraci\u00f3n ha finalizado y est\u00e1 listo para comenzar a usar AdGuard Home.",
|
||||
"install_devices_router": "Router",
|
||||
"install_devices_router_desc": "Esta configuraci\u00f3n cubrir\u00e1 autom\u00e1ticamente todos los dispositivos conectados a su router dom\u00e9stico y no necesitar\u00e1 configurar cada uno de ellos manualmente.",
|
||||
"install_devices_address": "El servidor DNS de AdGuard Home est\u00e1 escuchando las siguientes direcciones",
|
||||
"install_devices_address": "El servidor DNS de AdGuard Home est\u00e1 escuchando en las siguientes direcciones",
|
||||
"install_devices_router_list_1": "Abra las preferencias de su router. Por lo general, puede acceder a \u00e9l desde su navegador a trav\u00e9s de una URL (como http:\/\/192.168.0.1\/ o http:\/\/192.168.1.1\/). Se le puede pedir que ingrese la contrase\u00f1a. Si no lo recuerda, a menudo puede restablecer la contrase\u00f1a presionando un bot\u00f3n en el router. Algunos routers requieren una aplicaci\u00f3n espec\u00edfica, que en ese caso ya deber\u00eda estar instalada en su computadora\/tel\u00e9fono.",
|
||||
"install_devices_router_list_2": "Busque la configuraci\u00f3n de DHCP\/DNS. Busque las letras DNS junto a un campo que permita ingresar dos o tres grupos de n\u00fameros, cada uno dividido en cuatro grupos de uno a tres d\u00edgitos.",
|
||||
"install_devices_router_list_3": "Ingrese las direcciones de su servidor AdGuard Home all\u00ed.",
|
||||
@@ -209,13 +228,13 @@
|
||||
"install_devices_ios_list_1": "En la pantalla de inicio, pulse en Configuraci\u00f3n.",
|
||||
"install_devices_ios_list_2": "Elija Wi-Fi en el men\u00fa de la izquierda (es imposible configurar DNS para redes m\u00f3viles).",
|
||||
"install_devices_ios_list_3": "Pulse sobre el nombre de la red activa en ese momento.",
|
||||
"install_devices_ios_list_4": "En ese campo DNS ingrese las direcciones de su servidor AdGuard Home.",
|
||||
"install_devices_ios_list_4": "En el campo DNS ingrese las direcciones de su servidor AdGuard Home.",
|
||||
"get_started": "Comenzar",
|
||||
"next": "Siguiente",
|
||||
"open_dashboard": "Abrir panel de control",
|
||||
"install_saved": "Guardado correctamente",
|
||||
"encryption_title": "Cifrado",
|
||||
"encryption_desc": "Soporte para cifrado (HTTPS\/TLS) tanto para DNS como para la interfaz web de administraci\u00f3n",
|
||||
"encryption_desc": "Soporte de cifrado (HTTPS\/TLS) tanto para DNS como para la interfaz web de administraci\u00f3n",
|
||||
"encryption_config_saved": "Configuraci\u00f3n de cifrado guardado",
|
||||
"encryption_server": "Nombre del servidor",
|
||||
"encryption_server_enter": "Ingrese su nombre de dominio",
|
||||
@@ -224,7 +243,7 @@
|
||||
"encryption_redirect_desc": "Si est\u00e1 marcado, AdGuard Home redireccionar\u00e1 autom\u00e1ticamente de HTTP a las direcciones HTTPS.",
|
||||
"encryption_https": "Puerto HTTPS",
|
||||
"encryption_https_desc": "Si el puerto HTTPS est\u00e1 configurado, la interfaz de administraci\u00f3n de AdGuard Home ser\u00e1 accesible a trav\u00e9s de HTTPS, y tambi\u00e9n proporcionar\u00e1 DNS mediante HTTPS en la ubicaci\u00f3n '\/dns-query'.",
|
||||
"encryption_dot": "Puerto para DNS mediante TLS",
|
||||
"encryption_dot": "Puerto DNS mediante TLS",
|
||||
"encryption_dot_desc": "Si este puerto est\u00e1 configurado, AdGuard Home ejecutar\u00e1 un servidor DNS mediante TLS en este puerto.",
|
||||
"encryption_certificates": "Certificados",
|
||||
"encryption_certificates_desc": "Para utilizar el cifrado, debe proporcionar una cadena de certificados SSL v\u00e1lida para su dominio. Puede obtener un certificado gratuito en <0>{{link}}<\/0> o puede comprarlo en una de las autoridades de certificaci\u00f3n de confianza.",
|
||||
@@ -233,7 +252,7 @@
|
||||
"encryption_expire": "Expira",
|
||||
"encryption_key": "Clave privada",
|
||||
"encryption_key_input": "Copie\/pegue aqu\u00ed su clave privada codificada PEM para su certificado.",
|
||||
"encryption_enable": "Habilitar el cifrado (HTTPS, DNS mediante HTTPS y DNS mediante TLS)",
|
||||
"encryption_enable": "Habilitar cifrado (HTTPS, DNS mediante HTTPS y DNS mediante TLS)",
|
||||
"encryption_enable_desc": "Si el cifrado est\u00e1 habilitado, la interfaz de administraci\u00f3n de AdGuard Home funcionar\u00e1 a trav\u00e9s de HTTPS, y el servidor DNS escuchar\u00e1 las peticiones DNS mediante HTTPS y DNS mediante TLS.",
|
||||
"encryption_chain_valid": "La cadena de certificado es v\u00e1lida",
|
||||
"encryption_chain_invalid": "La cadena de certificado no es v\u00e1lida",
|
||||
@@ -245,12 +264,53 @@
|
||||
"encryption_reset": "\u00bfEst\u00e1 seguro de que desea restablecer la configuraci\u00f3n de cifrado?",
|
||||
"topline_expiring_certificate": "Su certificado SSL est\u00e1 a punto de expirar. Actualice la <0>configuraci\u00f3n del cifrado<\/0>.",
|
||||
"topline_expired_certificate": "Su certificado SSL ha expirado. Actualice la <0>configuraci\u00f3n del cifrado<\/0>.",
|
||||
"form_error_port_range": "Ingrese el valor del puerto en el rango de 80 - 65535",
|
||||
"form_error_port_range": "Ingrese el valor del puerto en el rango de 80 a 65535",
|
||||
"form_error_port_unsafe": "Este es un puerto inseguro",
|
||||
"form_error_equal": "No deber\u00eda ser igual",
|
||||
"form_error_password": "La contrase\u00f1a no coincide",
|
||||
"reset_settings": "Restablecer configuraci\u00f3n",
|
||||
"update_announcement": "\u00a1AdGuard Home {{version}} ya est\u00e1 disponible! <0>Haga clic aqu\u00ed<\/0> para m\u00e1s informaci\u00f3n.",
|
||||
"setup_guide": "Gu\u00eda de configuraci\u00f3n",
|
||||
"dns_addresses": "Direcciones DNS"
|
||||
"dns_addresses": "Direcciones DNS",
|
||||
"down": "Abajo",
|
||||
"fix": "Corregir",
|
||||
"dns_providers": "Aqu\u00ed hay una <0>lista de proveedores DNS<\/0> conocidos para elegir.",
|
||||
"update_now": "Actualizar ahora",
|
||||
"update_failed": "Error en la actualizaci\u00f3n autom\u00e1tica. Por favor <a href='https:\/\/github.com\/AdguardTeam\/AdGuardHome\/wiki\/Getting-Started#update'>siga los pasos<\/a> para actualizar manualmente.",
|
||||
"processing_update": "Por favor espere, AdGuard Home se est\u00e1 actualizando",
|
||||
"clients_title": "Clientes",
|
||||
"clients_desc": "Configurar dispositivos conectados con AdGuard Home",
|
||||
"settings_global": "Global",
|
||||
"settings_custom": "Personalizado",
|
||||
"table_client": "Cliente",
|
||||
"table_name": "Nombre",
|
||||
"save_btn": "Guardar",
|
||||
"client_add": "A\u00f1adir cliente",
|
||||
"client_new": "Cliente nuevo",
|
||||
"client_edit": "Editar cliente",
|
||||
"client_identifier": "Identificador",
|
||||
"ip_address": "Direcci\u00f3n IP",
|
||||
"client_identifier_desc": "Los clientes pueden ser identificados por la direcci\u00f3n IP o MAC. Tenga en cuenta que el uso de MAC como identificador solo es posible si AdGuard Home tambi\u00e9n es un <0>servidor DHCP<\/0>.",
|
||||
"form_enter_ip": "Ingresar IP",
|
||||
"form_enter_mac": "Ingresar MAC",
|
||||
"form_client_name": "Ingrese el nombre del cliente",
|
||||
"client_global_settings": "Usar configuraci\u00f3n global",
|
||||
"client_deleted": "Cliente \"{{key}}\" eliminado correctamente",
|
||||
"client_added": "Cliente \"{{key}}\" a\u00f1adido correctamente",
|
||||
"client_updated": "Cliente \"{{key}}\" actualizado correctamente",
|
||||
"table_statistics": "N\u00famero de peticiones (\u00faltimas 24 horas)",
|
||||
"clients_not_found": "No se han encontrado clientes",
|
||||
"client_confirm_delete": "\u00bfEst\u00e1 seguro de que desea eliminar el cliente \"{{key}}\"?",
|
||||
"filter_confirm_delete": "Are you sure you want to delete filter?",
|
||||
"auto_clients_title": "Clientes (activos)",
|
||||
"auto_clients_desc": "Datos de los clientes que utilizan AdGuard Home pero que no est\u00e1n almacenados en la configuraci\u00f3n",
|
||||
"access_title": "Access settings",
|
||||
"access_desc": "Here you can configure access rules for the AdGuard Home DNS server.",
|
||||
"access_allowed_title": "Allowed clients",
|
||||
"access_allowed_desc": "A list of CIDR or IP addresses. If configured, AdGuard Home will accept requests from these IP addresses only.",
|
||||
"access_disallowed_title": "Disallowed clients",
|
||||
"access_disallowed_desc": "A list of CIDR or IP addresses. If configured, AdGuard Home will drop requests from these IP addresses.",
|
||||
"access_blocked_title": "Blocked domains",
|
||||
"access_blocked_desc": "Don't confuse this with filters. AdGuard Home will drop DNS queries with these domains in query's question.",
|
||||
"access_settings_saved": "Access settings successfully saved"
|
||||
}
|
||||
@@ -15,10 +15,12 @@
|
||||
"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_static_leases": "DHCP static leases",
|
||||
"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_mac_format": "Invalid MAC format",
|
||||
"form_error_positive": "Deve ser maior que 0",
|
||||
"dhcp_form_gateway_input": "IP do gateway",
|
||||
"dhcp_form_subnet_input": "M\u00e1scara de sub-rede",
|
||||
@@ -32,7 +34,18 @@
|
||||
"dhcp_ip_addresses": "Endere\u00e7o de IP",
|
||||
"dhcp_table_hostname": "Hostname",
|
||||
"dhcp_table_expires": "Expira",
|
||||
"dhcp_warning": "Se voc\u00ea quiser ativar o servidor DHCP interno, certifique-se que n\u00e3o h\u00e1 outro servidor DHCP ativo. Caso contr\u00e1rio, isso pode impedir que outros dispositivos conectem \u00e1 internet!",
|
||||
"dhcp_warning": "Se voc\u00ea quiser ativar o servidor DHCP, verifique se n\u00e3o h\u00e1 outro servidor DHCP ativo na sua rede. Caso contr\u00e1rio, a internet pode parar de funcionar para outros dispositivos conectados!",
|
||||
"dhcp_error": "N\u00e3o foi poss\u00edvel determinar se existe outro servidor DHCP na rede.",
|
||||
"dhcp_static_ip_error": "Para usar o servidor DHCP, voc\u00ea deve definir um endere\u00e7o IP est\u00e1tico. N\u00e3o conseguimos determinar se essa interface de rede est\u00e1 configurada usando o endere\u00e7o de IP est\u00e1tico. Por favor, defina um endere\u00e7o IP est\u00e1tico manualmente.",
|
||||
"dhcp_dynamic_ip_found": "Seu sistema usa a configura\u00e7\u00e3o de endere\u00e7o IP din\u00e2mico para a interface <0>{{interfaceName}}<\/0>. Para usar o servidor DHCP, voc\u00ea deve definir um endere\u00e7o de IP est\u00e1tico. Seu endere\u00e7o IP atual \u00e9 <0> {{ipAddress}} <\/ 0>. Vamos definir automaticamente este endere\u00e7o IP como est\u00e1tico se voc\u00ea pressionar o bot\u00e3o Ativar DHCP.",
|
||||
"dhcp_lease_added": "Static lease \"{{key}}\" successfully added",
|
||||
"dhcp_lease_deleted": "Static lease \"{{key}}\" successfully deleted",
|
||||
"dhcp_new_static_lease": "New static lease",
|
||||
"dhcp_static_leases_not_found": "No DHCP static leases found",
|
||||
"dhcp_add_static_lease": "Add static lease",
|
||||
"delete_confirm": "Are you sure you want to delete \"{{key}}\"?",
|
||||
"form_enter_hostname": "Enter hostname",
|
||||
"error_details": "Detalhes do erro",
|
||||
"back": "Voltar",
|
||||
"dashboard": "Painel",
|
||||
"settings": "Configura\u00e7\u00f5es",
|
||||
@@ -46,6 +59,7 @@
|
||||
"copyright": "Copyright",
|
||||
"homepage": "P\u00e1gina inicial",
|
||||
"report_an_issue": "Reportar um problema",
|
||||
"privacy_policy": "Pol\u00edtica de privacidade",
|
||||
"enable_protection": "Ativar prote\u00e7\u00e3o",
|
||||
"enabled_protection": "Prote\u00e7\u00e3o ativada",
|
||||
"disable_protection": "Desativar prote\u00e7\u00e3o",
|
||||
@@ -82,6 +96,10 @@
|
||||
"no_servers_specified": "Nenhum servidor especificado",
|
||||
"no_settings": "N\u00e3o configurado",
|
||||
"general_settings": "Configura\u00e7\u00f5es gerais",
|
||||
"dns_settings": "DNS settings",
|
||||
"encryption_settings": "Encryption settings",
|
||||
"dhcp_settings": "DHCP settings",
|
||||
"client_settings": "Client settings",
|
||||
"upstream_dns": "Servidores DNS upstream",
|
||||
"upstream_dns_hint": "Se voc\u00ea deixar este campo vazio, o AdGuard Home ir\u00e1 usar o<a href='https:\/\/1.1.1.1\/' target='_blank'>DNS da Cloudflare<\/a> como upstream.",
|
||||
"test_upstream_btn": "Testar upstreams",
|
||||
@@ -100,6 +118,7 @@
|
||||
"rules_count_table_header": "Quantidade de regras",
|
||||
"last_time_updated_table_header": "\u00daltima atualiza\u00e7\u00e3o",
|
||||
"actions_table_header": "A\u00e7\u00f5es",
|
||||
"edit_table_action": "Edit",
|
||||
"delete_table_action": "Excluir",
|
||||
"filters_and_hosts": "Filtros e listas de bloqueio de hosts",
|
||||
"filters_and_hosts_hint": "O AdGuard Home entende regras b\u00e1sicas de bloqueio de an\u00fancios e a sintaxe de arquivos de hosts.",
|
||||
@@ -252,5 +271,46 @@
|
||||
"reset_settings": "Redefinir configura\u00e7\u00f5es",
|
||||
"update_announcement": "AdGuard Home {{version}} est\u00e1 dispon\u00edvel!<0>Clique aqui<\/0> para mais informa\u00e7\u00f5es.",
|
||||
"setup_guide": "Guia de configura\u00e7\u00e3o",
|
||||
"dns_addresses": "Endere\u00e7os DNS"
|
||||
"dns_addresses": "Endere\u00e7os DNS",
|
||||
"down": "Caiu",
|
||||
"fix": "Corrigido",
|
||||
"dns_providers": "Aqui est\u00e1 uma <0>lista de provedores de DNS conhecidos<\/0> para escolher.",
|
||||
"update_now": "Update now",
|
||||
"update_failed": "Auto-update failed. Please <a href='https:\/\/github.com\/AdguardTeam\/AdGuardHome\/wiki\/Getting-Started#update'>follow the steps<\/a> to update manually.",
|
||||
"processing_update": "Please wait, AdGuard Home is being updated",
|
||||
"clients_title": "Clients",
|
||||
"clients_desc": "Configure devices connected to AdGuard Home",
|
||||
"settings_global": "Global",
|
||||
"settings_custom": "Custom",
|
||||
"table_client": "Client",
|
||||
"table_name": "Name",
|
||||
"save_btn": "Save",
|
||||
"client_add": "Add Client",
|
||||
"client_new": "New Client",
|
||||
"client_edit": "Edit Client",
|
||||
"client_identifier": "Identifier",
|
||||
"ip_address": "IP address",
|
||||
"client_identifier_desc": "Clients can be identified by the IP address or MAC address. Please note, that using MAC as identifier is possible only if AdGuard Home is also a <0>DHCP server<\/0>",
|
||||
"form_enter_ip": "Enter IP",
|
||||
"form_enter_mac": "Enter MAC",
|
||||
"form_client_name": "Enter client name",
|
||||
"client_global_settings": "Use global settings",
|
||||
"client_deleted": "Client \"{{key}}\" successfully deleted",
|
||||
"client_added": "Client \"{{key}}\" successfully added",
|
||||
"client_updated": "Client \"{{key}}\" successfully updated",
|
||||
"table_statistics": "Requests count (last 24 hours)",
|
||||
"clients_not_found": "No clients found",
|
||||
"client_confirm_delete": "Are you sure you want to delete client \"{{key}}\"?",
|
||||
"filter_confirm_delete": "Are you sure you want to delete filter?",
|
||||
"auto_clients_title": "Clients (runtime)",
|
||||
"auto_clients_desc": "Data on the clients that use AdGuard Home, but not stored in the configuration",
|
||||
"access_title": "Access settings",
|
||||
"access_desc": "Here you can configure access rules for the AdGuard Home DNS server.",
|
||||
"access_allowed_title": "Allowed clients",
|
||||
"access_allowed_desc": "A list of CIDR or IP addresses. If configured, AdGuard Home will accept requests from these IP addresses only.",
|
||||
"access_disallowed_title": "Disallowed clients",
|
||||
"access_disallowed_desc": "A list of CIDR or IP addresses. If configured, AdGuard Home will drop requests from these IP addresses.",
|
||||
"access_blocked_title": "Blocked domains",
|
||||
"access_blocked_desc": "Don't confuse this with filters. AdGuard Home will drop DNS queries with these domains in query's question.",
|
||||
"access_settings_saved": "Access settings successfully saved"
|
||||
}
|
||||
@@ -1,25 +1,27 @@
|
||||
{
|
||||
"example_upstream_reserved": "\u60a8\u53ef\u660e\u78ba\u6307\u5b9a<0>\u7528\u65bc\u7279\u5b9a\u7684\u7db2\u57df<\/0>\u4e4bDNS\u4e0a\u6e38",
|
||||
"example_upstream_reserved": "\u60a8\u53ef\u660e\u78ba\u6307\u5b9a<0>\u7528\u65bc\u7279\u5b9a\u7684\u7db2\u57df<\/0>\u4e4b DNS \u4e0a\u6e38",
|
||||
"upstream_parallel": "\u900f\u904e\u540c\u6642\u5730\u67e5\u8a62\u6240\u6709\u4e0a\u6e38\u7684\u4f3a\u670d\u5668\uff0c\u4f7f\u7528\u4e26\u884c\u7684\u67e5\u8a62\u4ee5\u52a0\u901f\u89e3\u6790",
|
||||
"bootstrap_dns": "\u81ea\u6211\u555f\u52d5\uff08Bootstrap\uff09DNS \u4f3a\u670d\u5668",
|
||||
"bootstrap_dns_desc": "\u81ea\u6211\u555f\u52d5\uff08Bootstrap\uff09DNS\u4f3a\u670d\u5668\u88ab\u7528\u65bc\u89e3\u6790\u60a8\u660e\u78ba\u6307\u5b9a\u4f5c\u70ba\u4e0a\u6e38\u7684DoH\/DoT\u89e3\u6790\u5668\u4e4bIP\u4f4d\u5740\u3002",
|
||||
"bootstrap_dns_desc": "\u81ea\u6211\u555f\u52d5\uff08Bootstrap\uff09DNS \u4f3a\u670d\u5668\u88ab\u7528\u65bc\u89e3\u6790\u60a8\u660e\u78ba\u6307\u5b9a\u4f5c\u70ba\u4e0a\u6e38\u7684 DoH\/DoT \u89e3\u6790\u5668\u4e4b IP \u4f4d\u5740\u3002",
|
||||
"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_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\u7528 AdGuard \u81ea\u8eab\u5167\u5efa\u7684 DHCP \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_not_found": "\u555f\u7528\u5167\u5efa\u7684\u52d5\u614b\u4e3b\u6a5f\u8a2d\u5b9a\u5354\u5b9a\uff08DHCP\uff09\u4f3a\u670d\u5668\u70ba\u5b89\u5168\u7684 - \u65bc\u8a72\u7db2\u8def\u4e0a\uff0c\u6211\u5011\u672a\u767c\u73fe\u4efb\u4f55\u73fe\u884c\u7684 DHCP \u4f3a\u670d\u5668\u3002\u7136\u800c\uff0c\u6211\u5011\u9f13\u52f5\u60a8\u624b\u52d5\u5730\u91cd\u65b0\u6aa2\u67e5\u5b83\uff0c\u56e0\u70ba\u6211\u5011\u7684\u81ea\u52d5\u4e4b\u6e2c\u8a66\u76ee\u524d\u4e0d\u4e88 100\uff05 \u4fdd\u8b49\u3002",
|
||||
"dhcp_found": "\u65bc\u8a72\u7db2\u8def\u4e0a\uff0c\u4e00\u500b\u73fe\u884c\u7684\u52d5\u614b\u4e3b\u6a5f\u8a2d\u5b9a\u5354\u5b9a\uff08DHCP\uff09\u4f3a\u670d\u5668\u88ab\u767c\u73fe\u3002\u555f\u7528\u5167\u5efa\u7684 DHCP \u4f3a\u670d\u5668\u70ba\u4e0d\u5b89\u5168\u7684\u3002",
|
||||
"dhcp_leases": "\u52d5\u614b\u4e3b\u6a5f\u8a2d\u5b9a\u5354\u5b9a\uff08DHCP\uff09\u79df\u8cc3",
|
||||
"dhcp_static_leases": "DHCP static leases",
|
||||
"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",
|
||||
"form_error_ip_format": "\u7121\u6548\u7684 IPv4 \u683c\u5f0f",
|
||||
"form_error_mac_format": "\u7121\u6548\u7684\u5a92\u9ad4\u5b58\u53d6\u63a7\u5236\uff08MAC\uff09\u683c\u5f0f",
|
||||
"form_error_positive": "\u5fc5\u9808\u5927\u65bc 0",
|
||||
"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",
|
||||
@@ -32,7 +34,18 @@
|
||||
"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",
|
||||
"dhcp_warning": "\u5982\u679c\u60a8\u7121\u8ad6\u5982\u4f55\u60f3\u8981\u555f\u7528\u52d5\u614b\u4e3b\u6a5f\u8a2d\u5b9a\u5354\u5b9a\uff08DHCP\uff09\u4f3a\u670d\u5668\uff0c\u78ba\u4fdd\u5728\u60a8\u7684\u7db2\u8def\u7121\u5176\u5b83\u73fe\u884c\u7684 DHCP \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",
|
||||
"dhcp_error": "\u6211\u5011\u7121\u6cd5\u78ba\u5b9a\u5728\u8a72\u7db2\u8def\u662f\u5426\u6709\u53e6\u5916\u7684\u52d5\u614b\u4e3b\u6a5f\u8a2d\u5b9a\u5354\u5b9a\uff08DHCP\uff09\u4f3a\u670d\u5668\u3002",
|
||||
"dhcp_static_ip_error": "\u70ba\u4e86\u4f7f\u7528\u52d5\u614b\u4e3b\u6a5f\u8a2d\u5b9a\u5354\u5b9a\uff08DHCP\uff09\u4f3a\u670d\u5668\uff0c\u975c\u614b IP \u4f4d\u5740\u5fc5\u9808\u88ab\u8a2d\u5b9a\u3002\u6211\u5011\u672a\u80fd\u78ba\u5b9a\u8a72\u7db2\u8def\u4ecb\u9762\u662f\u5426\u88ab\u914d\u7f6e\u4f7f\u7528\u975c\u614b IP \u4f4d\u5740\u3002\u8acb\u624b\u52d5\u5730\u8a2d\u5b9a\u975c\u614b IP \u4f4d\u5740\u3002",
|
||||
"dhcp_dynamic_ip_found": "\u60a8\u7684\u7cfb\u7d71\u4f7f\u7528\u52d5\u614b IP \u4f4d\u5740\u914d\u7f6e\u4f9b\u4ecb\u9762 <0>{{interfaceName}}<\/0>\u3002\u70ba\u4e86\u4f7f\u7528\u52d5\u614b\u4e3b\u6a5f\u8a2d\u5b9a\u5354\u5b9a\uff08DHCP\uff09\u4f3a\u670d\u5668\uff0c\u975c\u614bIP\u4f4d\u5740\u5fc5\u9808\u88ab\u8a2d\u5b9a\u3002\u60a8\u73fe\u884c\u7684 IP \u4f4d\u5740\u70ba <0>{{ipAddress}}<\/0>\u3002\u5982\u679c\u60a8\u6309\u555f\u7528 DHCP \u6309\u9215\uff0c\u6211\u5011\u5c07\u81ea\u52d5\u5730\u8a2d\u5b9a\u6b64 IP \u4f4d\u5740\u4f5c\u70ba\u975c\u614b\u3002",
|
||||
"dhcp_lease_added": "Static lease \"{{key}}\" successfully added",
|
||||
"dhcp_lease_deleted": "Static lease \"{{key}}\" successfully deleted",
|
||||
"dhcp_new_static_lease": "New static lease",
|
||||
"dhcp_static_leases_not_found": "No DHCP static leases found",
|
||||
"dhcp_add_static_lease": "Add static lease",
|
||||
"delete_confirm": "Are you sure you want to delete \"{{key}}\"?",
|
||||
"form_enter_hostname": "Enter hostname",
|
||||
"error_details": "\u932f\u8aa4\u7d30\u7bc0",
|
||||
"back": "\u8fd4\u56de",
|
||||
"dashboard": "\u5100\u8868\u677f",
|
||||
"settings": "\u8a2d\u5b9a",
|
||||
@@ -46,6 +59,7 @@
|
||||
"copyright": "\u7248\u6b0a",
|
||||
"homepage": "\u9996\u9801",
|
||||
"report_an_issue": "\u5831\u544a\u554f\u984c",
|
||||
"privacy_policy": "\u96b1\u79c1\u653f\u7b56",
|
||||
"enable_protection": "\u555f\u7528\u9632\u8b77",
|
||||
"enabled_protection": "\u5df2\u555f\u7528\u9632\u8b77",
|
||||
"disable_protection": "\u7981\u7528\u9632\u8b77",
|
||||
@@ -56,34 +70,38 @@
|
||||
"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",
|
||||
"for_last_24_hours": "\u5728\u6700\u8fd1\u7684 24 \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_24_hours": "\u5728\u6700\u8fd1\u7684 24 \u5c0f\u6642\u5167\u5df2\u8655\u7406\u7684 DNS \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\u7684 DNS \u8acb\u6c42\u4e4b\u6578\u91cf",
|
||||
"number_of_dns_query_blocked_24_hours_by_sec": "\u5df2\u88ab AdGuard \u700f\u89bd\u5b89\u5168\u6a21\u7d44\u5c01\u9396\u7684 DNS \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",
|
||||
"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\u7684 DNS \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",
|
||||
"average_processing_time_hint": "\u65bc\u8655\u7406\u4e00\u9805 DNS \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",
|
||||
"use_adguard_browsing_sec": "\u4f7f\u7528 AdGuard \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\u540d SHA256 \u96dc\u6e4a\u7684\u77ed\u524d\u7db4\u88ab\u50b3\u9001\u5230\u4f3a\u670d\u5668\u3002",
|
||||
"use_adguard_parental": "\u4f7f\u7528 AdGuard \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",
|
||||
"enforce_save_search_hint": "AdGuard Home \u53ef\u5728\u4e0b\u5217\u7684\u641c\u5c0b\u5f15\u64ce\uff1aGoogle\u3001YouTube\u3001Bing\u3001DuckDuckGo \u548c Yandex \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\u5c07\u8a72\u6b04\u4f4d\u7559\u7a7a\uff0cAdGuard Home\u5c07\u4f7f\u7528<a href='https:\/\/1.1.1.1\/' target='_blank'>Cloudflare DNS<\/a>\u4f5c\u70ba\u4e0a\u6e38\u3002",
|
||||
"dns_settings": "DNS settings",
|
||||
"encryption_settings": "Encryption settings",
|
||||
"dhcp_settings": "DHCP settings",
|
||||
"client_settings": "Clients settings",
|
||||
"upstream_dns": "\u4e0a\u6e38\u7684 DNS \u4f3a\u670d\u5668",
|
||||
"upstream_dns_hint": "\u5982\u679c\u60a8\u5c07\u8a72\u6b04\u4f4d\u7559\u7a7a\uff0cAdGuard Home \u5c07\u4f7f\u7528 <a href='https:\/\/1.1.1.1\/' target='_blank'>Cloudflare DNS<\/a> \u4f5c\u70ba\u4e0a\u6e38\u3002",
|
||||
"test_upstream_btn": "\u6e2c\u8a66\u4e0a\u884c\u8cc7\u6599\u6d41",
|
||||
"apply_btn": "\u5957\u7528",
|
||||
"disabled_filtering_toast": "\u5df2\u7981\u7528\u904e\u6ffe",
|
||||
@@ -100,9 +118,10 @@
|
||||
"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",
|
||||
"edit_table_action": "\u7de8\u8f2f",
|
||||
"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",
|
||||
"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",
|
||||
@@ -114,21 +133,21 @@
|
||||
"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_meaning_filter_block": "\u5c01\u9396\u81f3 example.org \u7db2\u57df\u53ca\u5176\u6240\u6709\u7684\u5b50\u7db2\u57df\u4e4b\u5b58\u53d6",
|
||||
"example_meaning_filter_whitelist": "\u89e3\u9664\u5c01\u9396\u81f3 example.org \u7db2\u57df\u53ca\u5176\u6240\u6709\u7684\u5b50\u7db2\u57df\u4e4b\u5b58\u53d6",
|
||||
"example_meaning_host_block": "AdGuard Home \u73fe\u5728\u5c07\u5c0d example.org \u7db2\u57df\u8fd4\u56de 127.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\u7b26\u7684\u7db2\u57df\u4e4b\u5b58\u53d6",
|
||||
"example_upstream_regular": "\u4e00\u822c\u7684 DNS\uff08\u900f\u904eUDP\uff09",
|
||||
"example_upstream_regular": "\u4e00\u822c\u7684 DNS\uff08\u900f\u904e UDP\uff09",
|
||||
"example_upstream_dot": "\u52a0\u5bc6\u7684 <0>DNS-over-TLS<\/0>",
|
||||
"example_upstream_doh": "\u52a0\u5bc6\u7684 <0>DNS-over-HTTPS<\/0>",
|
||||
"example_upstream_sdns": "\u60a8\u53ef\u4f7f\u7528\u95dc\u65bc <1>DNSCrypt<\/1> \u6216 <2>DNS-over-HTTPS<\/2> \u89e3\u6790\u5668\u4e4b <0>DNS \u6233\u8a18<\/0>",
|
||||
"example_upstream_tcp": "\u4e00\u822c\u7684 DNS\uff08\u900f\u904eTCP\uff09",
|
||||
"example_upstream_tcp": "\u4e00\u822c\u7684 DNS\uff08\u900f\u904e TCP\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",
|
||||
"updated_upstream_dns_toast": "\u5df2\u66f4\u65b0\u4e0a\u6e38\u7684 DNS \u4f3a\u670d\u5668",
|
||||
"dns_test_ok_toast": "\u5df2\u660e\u78ba\u6307\u5b9a\u7684 DNS \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",
|
||||
@@ -145,7 +164,7 @@
|
||||
"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",
|
||||
"last_dns_queries": "\u6700\u8fd1\u7684 5000 \u7b46 DNS \u67e5\u8a62",
|
||||
"previous_btn": "\u4e0a\u4e00\u9801",
|
||||
"next_btn": "\u4e0b\u4e00\u9801",
|
||||
"loading_table_status": "\u6b63\u5728\u8f09\u5165...",
|
||||
@@ -163,18 +182,18 @@
|
||||
"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_welcome_title": "\u6b61\u8fce\u81f3 AdGuard Home\uff01",
|
||||
"install_welcome_desc": "AdGuard Home \u662f\u5168\u7db2\u8def\u7bc4\u570d\u5ee3\u544a\u548c\u8ffd\u8e64\u5668\u5c01\u9396\u7684 DNS \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",
|
||||
"install_settings_interface_link": "\u60a8\u7684 AdGuard 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_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\u4e4b DNS \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_desc": "\u88ab\u975e\u5e38\u5efa\u8b70\u914d\u7f6e\u5c6c\u65bc\u60a8\u7684 AdGuard 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",
|
||||
@@ -182,50 +201,50 @@
|
||||
"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_devices_desc": "\u70ba\u4f7f AdGuard 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_submit_desc": "\u8a72\u8a2d\u7f6e\u7a0b\u5e8f\u88ab\u5b8c\u6210\uff0c\u4e14\u60a8\u6e96\u5099\u597d\u958b\u59cb\u4f7f\u7528 AdGuard 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_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_router_list_2": "\u627e\u5230 DHCP\/DNS \u8a2d\u5b9a\u3002\u5c0b\u627e\u7dca\u9130\u8457\u5141\u8a31\u5169\u7d44\u6216\u4e09\u7d44\u6578\u5b57\u96c6\u7684\u6b04\u4f4d\u4e4b DNS \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\u7684 AdGuard Home \u4f3a\u670d\u5668\u4f4d\u5740\u3002",
|
||||
"install_devices_windows_list_1": "\u901a\u904e\u958b\u59cb\u529f\u80fd\u8868\u6216 Windows \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_windows_list_6": "\u9078\u64c7\u4f7f\u7528\u4e0b\u5217\u7684 DNS \u4f3a\u670d\u5668\u4f4d\u5740\uff0c\u7136\u5f8c\u8f38\u5165\u60a8\u7684 AdGuard Home \u4f3a\u670d\u5668\u4f4d\u5740\u3002",
|
||||
"install_devices_macos_list_1": "\u65bc Apple \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_macos_list_4": "\u9078\u64c7\u8a72 DNS \u5206\u9801\uff0c\u7136\u5f8c\u8f38\u5165\u60a8\u7684 AdGuard Home \u4f3a\u670d\u5668\u4f4d\u5740\u3002",
|
||||
"install_devices_android_list_1": "\u5f9e Android \u9078\u55ae\u4e3b\u756b\u9762\u4e2d\uff0c\u8f15\u89f8\u8a2d\u5b9a\u3002",
|
||||
"install_devices_android_list_2": "\u65bc\u8a72\u9078\u55ae\u4e0a\u8f15\u89f8 Wi-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\u7684 DNS\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_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\u7684 Android DNS \u8a2d\u5b9a\uff0c\u60a8\u5c07\u9700\u8981\u628a IP \u8a2d\u5b9a\u5f9e DHCP \u8f49\u63db\u6210\u975c\u614b\u3002",
|
||||
"install_devices_android_list_5": "\u4f7f\u8a2d\u5b9a DNS 1 \u548c DNS 2 \u503c\u66f4\u6539\u6210\u60a8\u7684 AdGuard 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_2": "\u5728\u5de6\u5074\u7684\u9078\u55ae\u4e2d\u9078\u64c7 Wi-Fi\uff08\u4e0d\u53ef\u80fd\u70ba\u884c\u52d5\u7db2\u8def\u914d\u7f6e DNS\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",
|
||||
"install_devices_ios_list_4": "\u5728\u8a72 DNS \u6b04\u4f4d\u4e2d\uff0c\u8f38\u5165\u60a8\u7684 AdGuard 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",
|
||||
"encryption_title": "\u52a0\u5bc6",
|
||||
"encryption_desc": "\u52a0\u5bc6\uff08HTTPS\/TLS\uff09\u652f\u63f4\u4f9bDNS\u548c\u7ba1\u7406\u54e1\u7db2\u9801\u4ecb\u9762\u5169\u8005",
|
||||
"encryption_desc": "\u52a0\u5bc6\uff08HTTPS\/TLS\uff09\u652f\u63f4\u4f9b DNS \u548c\u7ba1\u7406\u54e1\u7db2\u9801\u4ecb\u9762\u5169\u8005",
|
||||
"encryption_config_saved": "\u52a0\u5bc6\u914d\u7f6e\u5df2\u88ab\u5132\u5b58",
|
||||
"encryption_server": "\u4f3a\u670d\u5668\u540d\u7a31",
|
||||
"encryption_server_enter": "\u8f38\u5165\u60a8\u7684\u57df\u540d",
|
||||
"encryption_server_desc": "\u70ba\u4e86\u4f7f\u7528HTTPS\uff0c\u60a8\u9700\u8981\u8f38\u5165\u8207\u60a8\u7684\u5b89\u5168\u901a\u8a0a\u7aef\u5c64\uff08SSL\uff09\u6191\u8b49\u76f8\u7b26\u7684\u4f3a\u670d\u5668\u540d\u7a31\u3002",
|
||||
"encryption_redirect": "\u81ea\u52d5\u5730\u91cd\u5b9a\u5411\u5230HTTPS",
|
||||
"encryption_redirect_desc": "\u5982\u679c\u88ab\u52fe\u9078\uff0cAdGuard Home\u5c07\u81ea\u52d5\u5730\u91cd\u5b9a\u5411\u60a8\u5f9eHTTP\u5230HTTPS\u4f4d\u5740\u3002",
|
||||
"encryption_server_desc": "\u70ba\u4e86\u4f7f\u7528 HTTPS\uff0c\u60a8\u9700\u8981\u8f38\u5165\u8207\u60a8\u7684\u5b89\u5168\u901a\u8a0a\u7aef\u5c64\uff08SSL\uff09\u6191\u8b49\u76f8\u7b26\u7684\u4f3a\u670d\u5668\u540d\u7a31\u3002",
|
||||
"encryption_redirect": "\u81ea\u52d5\u5730\u91cd\u65b0\u5c0e\u5411\u5230 HTTPS",
|
||||
"encryption_redirect_desc": "\u5982\u679c\u88ab\u52fe\u9078\uff0cAdGuard Home \u5c07\u81ea\u52d5\u5730\u91cd\u65b0\u5c0e\u5411\u60a8\u5f9e HTTP \u5230 HTTPS \u4f4d\u5740\u3002",
|
||||
"encryption_https": "HTTPS \u9023\u63a5\u57e0",
|
||||
"encryption_https_desc": "\u5982\u679cHTTPS\u9023\u63a5\u57e0\u88ab\u914d\u7f6e\uff0cAdGuard Home\u7ba1\u7406\u54e1\u4ecb\u9762\u900f\u904eHTTPS\u5c07\u70ba\u53ef\u5b58\u53d6\u7684\uff0c\u4e14\u5b83\u4e5f\u5c07\u65bc '\/dns-query' \u4f4d\u7f6e\u4e0a\u63d0\u4f9bDNS-over-HTTPS\u3002",
|
||||
"encryption_https_desc": "\u5982\u679c HTTPS \u9023\u63a5\u57e0\u88ab\u914d\u7f6e\uff0cAdGuard Home \u7ba1\u7406\u54e1\u4ecb\u9762\u900f\u904e HTTPS \u5c07\u70ba\u53ef\u5b58\u53d6\u7684\uff0c\u4e14\u5b83\u4e5f\u5c07\u65bc '\/dns-query' \u4f4d\u7f6e\u4e0a\u63d0\u4f9b DNS-over-HTTPS\u3002",
|
||||
"encryption_dot": "DNS-over-TLS \u9023\u63a5\u57e0",
|
||||
"encryption_dot_desc": "\u5982\u679c\u8a72\u9023\u63a5\u57e0\u88ab\u914d\u7f6e\uff0cAdGuard Home\u5c07\u65bc\u6b64\u9023\u63a5\u57e0\u4e0a\u904b\u884cDNS-over-TLS\u4f3a\u670d\u5668\u3002",
|
||||
"encryption_dot_desc": "\u5982\u679c\u8a72\u9023\u63a5\u57e0\u88ab\u914d\u7f6e\uff0cAdGuard Home \u5c07\u65bc\u6b64\u9023\u63a5\u57e0\u4e0a\u904b\u884c DNS-over-TLS \u4f3a\u670d\u5668\u3002",
|
||||
"encryption_certificates": "\u6191\u8b49",
|
||||
"encryption_certificates_desc": "\u70ba\u4e86\u4f7f\u7528\u52a0\u5bc6\uff0c\u60a8\u9700\u8981\u63d0\u4f9b\u6709\u6548\u7684\u5b89\u5168\u901a\u8a0a\u7aef\u5c64\uff08SSL\uff09\u6191\u8b49\u93c8\u7d50\u4f9b\u60a8\u7684\u7db2\u57df\u3002\u65bc <0>{{link}}<\/0> \u4e0a\u60a8\u53ef\u53d6\u5f97\u514d\u8cbb\u7684\u6191\u8b49\u6216\u60a8\u53ef\u5f9e\u53d7\u4fe1\u4efb\u7684\u6191\u8b49\u6388\u6b0a\u55ae\u4f4d\u4e4b\u4e00\u8cfc\u8cb7\u5b83\u3002",
|
||||
"encryption_certificates_input": "\u65bc\u6b64\u8907\u88fd\/\u8cbc\u4e0a\u60a8\u7684\u96b1\u79c1\u589e\u5f37\u90f5\u4ef6\u7de8\u78bc\u4e4b\uff08PEM-encoded\uff09\u6191\u8b49\u3002",
|
||||
@@ -233,8 +252,8 @@
|
||||
"encryption_expire": "\u5230\u671f",
|
||||
"encryption_key": "\u79c1\u5bc6\u91d1\u9470",
|
||||
"encryption_key_input": "\u65bc\u6b64\u8907\u88fd\/\u8cbc\u4e0a\u60a8\u7684\u96b1\u79c1\u589e\u5f37\u90f5\u4ef6\u7de8\u78bc\u4e4b\uff08PEM-encoded\uff09\u79c1\u5bc6\u91d1\u9470\u4f9b\u60a8\u7684\u6191\u8b49\u3002",
|
||||
"encryption_enable": "\u555f\u7528\u52a0\u5bc6\uff08HTTPS\u3001DNS-over-HTTPS\u548cDNS-over-TLS\uff09",
|
||||
"encryption_enable_desc": "\u5982\u679c\u52a0\u5bc6\u88ab\u555f\u7528\uff0cAdGuard Home\u7ba1\u7406\u54e1\u4ecb\u9762\u900f\u904eHTTPS\u5c07\u904b\u4f5c\uff0c\u4e14\u8a72DNS\u4f3a\u670d\u5668\u5c07\u7559\u5fc3\u76e3\u807d\u900f\u904eDNS-over-HTTPS\u548cDNS-over-TLS\u4e4b\u8acb\u6c42\u3002",
|
||||
"encryption_enable": "\u555f\u7528\u52a0\u5bc6\uff08HTTPS\u3001DNS-over-HTTPS \u548c DNS-over-TLS\uff09",
|
||||
"encryption_enable_desc": "\u5982\u679c\u52a0\u5bc6\u88ab\u555f\u7528\uff0cAdGuard Home \u7ba1\u7406\u54e1\u4ecb\u9762\u900f\u904e HTTPS \u5c07\u904b\u4f5c\uff0c\u4e14\u8a72 DNS \u4f3a\u670d\u5668\u5c07\u7559\u5fc3\u76e3\u807d\u900f\u904e DNS-over-HTTPS \u548c DNS-over-TLS \u4e4b\u8acb\u6c42\u3002",
|
||||
"encryption_chain_valid": "\u6191\u8b49\u93c8\u7d50\u70ba\u6709\u6548\u7684",
|
||||
"encryption_chain_invalid": "\u6191\u8b49\u93c8\u7d50\u70ba\u7121\u6548\u7684",
|
||||
"encryption_key_valid": "\u6b64\u70ba\u6709\u6548\u7684 {{type}} \u79c1\u5bc6\u91d1\u9470",
|
||||
@@ -245,12 +264,53 @@
|
||||
"encryption_reset": "\u60a8\u78ba\u5b9a\u60a8\u60f3\u8981\u91cd\u7f6e\u52a0\u5bc6\u8a2d\u5b9a\u55ce\uff1f",
|
||||
"topline_expiring_certificate": "\u60a8\u7684\u5b89\u5168\u901a\u8a0a\u7aef\u5c64\uff08SSL\uff09\u6191\u8b49\u5373\u5c07\u5230\u671f\u3002\u66f4\u65b0<0>\u52a0\u5bc6\u8a2d\u5b9a<\/0>\u3002",
|
||||
"topline_expired_certificate": "\u60a8\u7684\u5b89\u5168\u901a\u8a0a\u7aef\u5c64\uff08SSL\uff09\u6191\u8b49\u70ba\u5df2\u5230\u671f\u7684\u3002\u66f4\u65b0<0>\u52a0\u5bc6\u8a2d\u5b9a<\/0>\u3002",
|
||||
"form_error_port_range": "\u572880-65535\u4e4b\u7bc4\u570d\u5167\u8f38\u5165\u9023\u63a5\u57e0\u503c",
|
||||
"form_error_port_range": "\u5728 80-65535 \u4e4b\u7bc4\u570d\u5167\u8f38\u5165\u9023\u63a5\u57e0\u503c",
|
||||
"form_error_port_unsafe": "\u6b64\u70ba\u4e0d\u5b89\u5168\u7684\u9023\u63a5\u57e0",
|
||||
"form_error_equal": "\u4e0d\u61c9\u70ba\u76f8\u7b49\u7684",
|
||||
"form_error_password": "\u4e0d\u76f8\u7b26\u7684\u5bc6\u78bc",
|
||||
"reset_settings": "\u91cd\u7f6e\u8a2d\u5b9a",
|
||||
"update_announcement": "AdGuard Home {{version}} \u73fe\u70ba\u53ef\u7528\u7684\uff01\u95dc\u65bc\u66f4\u591a\u7684\u8cc7\u8a0a\uff0c<0>\u9ede\u64ca\u9019\u88e1<\/0>\u3002",
|
||||
"setup_guide": "\u5b89\u88dd\u6307\u5357",
|
||||
"dns_addresses": "DNS \u4f4d\u5740"
|
||||
"dns_addresses": "DNS \u4f4d\u5740",
|
||||
"down": "\u505c\u6b62\u904b\u4f5c\u7684",
|
||||
"fix": "\u4fee\u5fa9",
|
||||
"dns_providers": "\u9019\u88e1\u662f\u4e00\u500b\u5f9e\u4e2d\u9078\u64c7\u4e4b<0>\u5df2\u77e5\u7684 DNS \u4f9b\u61c9\u5546\u4e4b\u6e05\u55ae<\/0>\u3002",
|
||||
"update_now": "\u7acb\u5373\u66f4\u65b0",
|
||||
"update_failed": "Auto-update failed. Please <a href='https:\/\/github.com\/AdguardTeam\/AdGuardHome\/wiki\/Getting-Started#update'>follow the steps<\/a> to update manually.",
|
||||
"processing_update": "Please wait, AdGuard Home is being updated",
|
||||
"clients_title": "\u7528\u6236\u7aef",
|
||||
"clients_desc": "Configure devices connected to AdGuard Home",
|
||||
"settings_global": "Global",
|
||||
"settings_custom": "Custom",
|
||||
"table_client": "\u7528\u6236\u7aef",
|
||||
"table_name": "\u540d\u7a31",
|
||||
"save_btn": "\u5132\u5b58",
|
||||
"client_add": "\u589e\u52a0\u7528\u6236\u7aef",
|
||||
"client_new": "\u65b0\u7684\u7528\u6236\u7aef",
|
||||
"client_edit": "\u7de8\u8f2f\u7528\u6236\u7aef",
|
||||
"client_identifier": "Identifier",
|
||||
"ip_address": "IP \u4f4d\u5740",
|
||||
"client_identifier_desc": "Clients can be identified by the IP address or MAC address. Please note, that using MAC as identifier is possible only if AdGuard Home is also a <0>DHCP server<\/0>",
|
||||
"form_enter_ip": "\u8f38\u5165 IP",
|
||||
"form_enter_mac": "\u8f38\u5165\u5a92\u9ad4\u5b58\u53d6\u63a7\u5236\uff08MAC\uff09",
|
||||
"form_client_name": "\u8f38\u5165\u7528\u6236\u7aef\u540d\u7a31",
|
||||
"client_global_settings": "Use global settings",
|
||||
"client_deleted": "Client \"{{key}}\" successfully deleted",
|
||||
"client_added": "Client \"{{key}}\" successfully added",
|
||||
"client_updated": "Client \"{{key}}\" successfully updated",
|
||||
"table_statistics": "Requests count (last 24 hours)",
|
||||
"clients_not_found": "No clients found",
|
||||
"client_confirm_delete": "Are you sure you want to delete client \"{{key}}\"?",
|
||||
"filter_confirm_delete": "Are you sure you want to delete filter?",
|
||||
"auto_clients_title": "Clients (runtime)",
|
||||
"auto_clients_desc": "Data on the clients that use AdGuard Home, but not stored in the configuration",
|
||||
"access_title": "Access settings",
|
||||
"access_desc": "Here you can configure access rules for the AdGuard Home DNS server.",
|
||||
"access_allowed_title": "Allowed clients",
|
||||
"access_allowed_desc": "A list of CIDR or IP addresses. If configured, AdGuard Home will accept requests from these IP addresses only.",
|
||||
"access_disallowed_title": "Disallowed clients",
|
||||
"access_disallowed_desc": "A list of CIDR or IP addresses. If configured, AdGuard Home will drop requests from these IP addresses.",
|
||||
"access_blocked_title": "Blocked domains",
|
||||
"access_blocked_desc": "Don't confuse this with filters. AdGuard Home will drop DNS queries with these domains in query's question.",
|
||||
"access_settings_saved": "Access settings successfully saved"
|
||||
}
|
||||
45
client/src/actions/access.js
Normal file
45
client/src/actions/access.js
Normal file
@@ -0,0 +1,45 @@
|
||||
import { createAction } from 'redux-actions';
|
||||
import Api from '../api/Api';
|
||||
import { addErrorToast, addSuccessToast } from './index';
|
||||
import { normalizeTextarea } from '../helpers/helpers';
|
||||
|
||||
const apiClient = new Api();
|
||||
|
||||
export const getAccessListRequest = createAction('GET_ACCESS_LIST_REQUEST');
|
||||
export const getAccessListFailure = createAction('GET_ACCESS_LIST_FAILURE');
|
||||
export const getAccessListSuccess = createAction('GET_ACCESS_LIST_SUCCESS');
|
||||
|
||||
export const getAccessList = () => async (dispatch) => {
|
||||
dispatch(getAccessListRequest());
|
||||
try {
|
||||
const data = await apiClient.getAccessList();
|
||||
dispatch(getAccessListSuccess(data));
|
||||
} catch (error) {
|
||||
dispatch(addErrorToast({ error }));
|
||||
dispatch(getAccessListFailure());
|
||||
}
|
||||
};
|
||||
|
||||
export const setAccessListRequest = createAction('SET_ACCESS_LIST_REQUEST');
|
||||
export const setAccessListFailure = createAction('SET_ACCESS_LIST_FAILURE');
|
||||
export const setAccessListSuccess = createAction('SET_ACCESS_LIST_SUCCESS');
|
||||
|
||||
export const setAccessList = config => async (dispatch) => {
|
||||
dispatch(setAccessListRequest());
|
||||
try {
|
||||
const { allowed_clients, disallowed_clients, blocked_hosts } = config;
|
||||
|
||||
const values = {
|
||||
allowed_clients: (allowed_clients && normalizeTextarea(allowed_clients)) || [],
|
||||
disallowed_clients: (disallowed_clients && normalizeTextarea(disallowed_clients)) || [],
|
||||
blocked_hosts: (blocked_hosts && normalizeTextarea(blocked_hosts)) || [],
|
||||
};
|
||||
|
||||
await apiClient.setAccessList(values);
|
||||
dispatch(setAccessListSuccess());
|
||||
dispatch(addSuccessToast('access_settings_saved'));
|
||||
} catch (error) {
|
||||
dispatch(addErrorToast({ error }));
|
||||
dispatch(setAccessListFailure());
|
||||
}
|
||||
};
|
||||
84
client/src/actions/clients.js
Normal file
84
client/src/actions/clients.js
Normal file
@@ -0,0 +1,84 @@
|
||||
import { createAction } from 'redux-actions';
|
||||
import { t } from 'i18next';
|
||||
import Api from '../api/Api';
|
||||
import { addErrorToast, addSuccessToast, getClients } from './index';
|
||||
import { CLIENT_ID } from '../helpers/constants';
|
||||
|
||||
const apiClient = new Api();
|
||||
|
||||
export const toggleClientModal = createAction('TOGGLE_CLIENT_MODAL');
|
||||
|
||||
export const addClientRequest = createAction('ADD_CLIENT_REQUEST');
|
||||
export const addClientFailure = createAction('ADD_CLIENT_FAILURE');
|
||||
export const addClientSuccess = createAction('ADD_CLIENT_SUCCESS');
|
||||
|
||||
export const addClient = config => async (dispatch) => {
|
||||
dispatch(addClientRequest());
|
||||
try {
|
||||
let data;
|
||||
if (config.identifier === CLIENT_ID.MAC) {
|
||||
const { ip, identifier, ...values } = config;
|
||||
|
||||
data = { ...values };
|
||||
} else {
|
||||
const { mac, identifier, ...values } = config;
|
||||
|
||||
data = { ...values };
|
||||
}
|
||||
|
||||
await apiClient.addClient(data);
|
||||
dispatch(addClientSuccess());
|
||||
dispatch(toggleClientModal());
|
||||
dispatch(addSuccessToast(t('client_added', { key: config.name })));
|
||||
dispatch(getClients());
|
||||
} catch (error) {
|
||||
dispatch(addErrorToast({ error }));
|
||||
dispatch(addClientFailure());
|
||||
}
|
||||
};
|
||||
|
||||
export const deleteClientRequest = createAction('DELETE_CLIENT_REQUEST');
|
||||
export const deleteClientFailure = createAction('DELETE_CLIENT_FAILURE');
|
||||
export const deleteClientSuccess = createAction('DELETE_CLIENT_SUCCESS');
|
||||
|
||||
export const deleteClient = config => async (dispatch) => {
|
||||
dispatch(deleteClientRequest());
|
||||
try {
|
||||
await apiClient.deleteClient(config);
|
||||
dispatch(deleteClientSuccess());
|
||||
dispatch(addSuccessToast(t('client_deleted', { key: config.name })));
|
||||
dispatch(getClients());
|
||||
} catch (error) {
|
||||
dispatch(addErrorToast({ error }));
|
||||
dispatch(deleteClientFailure());
|
||||
}
|
||||
};
|
||||
|
||||
export const updateClientRequest = createAction('UPDATE_CLIENT_REQUEST');
|
||||
export const updateClientFailure = createAction('UPDATE_CLIENT_FAILURE');
|
||||
export const updateClientSuccess = createAction('UPDATE_CLIENT_SUCCESS');
|
||||
|
||||
export const updateClient = (config, name) => async (dispatch) => {
|
||||
dispatch(updateClientRequest());
|
||||
try {
|
||||
let data;
|
||||
if (config.identifier === CLIENT_ID.MAC) {
|
||||
const { ip, identifier, ...values } = config;
|
||||
|
||||
data = { name, data: { ...values } };
|
||||
} else {
|
||||
const { mac, identifier, ...values } = config;
|
||||
|
||||
data = { name, data: { ...values } };
|
||||
}
|
||||
|
||||
await apiClient.updateClient(data);
|
||||
dispatch(updateClientSuccess());
|
||||
dispatch(toggleClientModal());
|
||||
dispatch(addSuccessToast(t('client_updated', { key: name })));
|
||||
dispatch(getClients());
|
||||
} catch (error) {
|
||||
dispatch(addErrorToast({ error }));
|
||||
dispatch(updateClientFailure());
|
||||
}
|
||||
};
|
||||
@@ -2,15 +2,17 @@ import { createAction } from 'redux-actions';
|
||||
import round from 'lodash/round';
|
||||
import { t } from 'i18next';
|
||||
import { showLoading, hideLoading } from 'react-redux-loading-bar';
|
||||
import axios from 'axios';
|
||||
|
||||
import { normalizeHistory, normalizeFilteringStatus, normalizeLogs, normalizeTextarea } from '../helpers/helpers';
|
||||
import { SETTINGS_NAMES } from '../helpers/constants';
|
||||
import { normalizeHistory, normalizeFilteringStatus, normalizeLogs, normalizeTextarea, sortClients } from '../helpers/helpers';
|
||||
import { SETTINGS_NAMES, CHECK_TIMEOUT } from '../helpers/constants';
|
||||
import Api from '../api/Api';
|
||||
|
||||
const apiClient = new Api();
|
||||
|
||||
export const addErrorToast = createAction('ADD_ERROR_TOAST');
|
||||
export const addSuccessToast = createAction('ADD_SUCCESS_TOAST');
|
||||
export const addNoticeToast = createAction('ADD_NOTICE_TOAST');
|
||||
export const removeToast = createAction('REMOVE_TOAST');
|
||||
|
||||
export const toggleSettingStatus = createAction('SETTING_STATUS_TOGGLE');
|
||||
@@ -154,6 +156,62 @@ export const getVersion = () => async (dispatch) => {
|
||||
}
|
||||
};
|
||||
|
||||
export const getUpdateRequest = createAction('GET_UPDATE_REQUEST');
|
||||
export const getUpdateFailure = createAction('GET_UPDATE_FAILURE');
|
||||
export const getUpdateSuccess = createAction('GET_UPDATE_SUCCESS');
|
||||
|
||||
export const getUpdate = () => async (dispatch, getState) => {
|
||||
const { dnsVersion } = getState().dashboard;
|
||||
|
||||
dispatch(getUpdateRequest());
|
||||
try {
|
||||
await apiClient.getUpdate();
|
||||
|
||||
const checkUpdate = async (attempts) => {
|
||||
let count = attempts || 1;
|
||||
let timeout;
|
||||
|
||||
if (count > 60) {
|
||||
dispatch(addNoticeToast({ error: 'update_failed' }));
|
||||
dispatch(getUpdateFailure());
|
||||
return false;
|
||||
}
|
||||
|
||||
const rmTimeout = t => t && clearTimeout(t);
|
||||
const setRecursiveTimeout = (time, ...args) => setTimeout(
|
||||
checkUpdate,
|
||||
time,
|
||||
...args,
|
||||
);
|
||||
|
||||
axios.get('control/status')
|
||||
.then((response) => {
|
||||
rmTimeout(timeout);
|
||||
if (response && response.status === 200) {
|
||||
const responseVersion = response.data && response.data.version;
|
||||
|
||||
if (dnsVersion !== responseVersion) {
|
||||
dispatch(getUpdateSuccess());
|
||||
window.location.reload(true);
|
||||
}
|
||||
}
|
||||
timeout = setRecursiveTimeout(CHECK_TIMEOUT, count += 1);
|
||||
})
|
||||
.catch(() => {
|
||||
rmTimeout(timeout);
|
||||
timeout = setRecursiveTimeout(CHECK_TIMEOUT, count += 1);
|
||||
});
|
||||
|
||||
return false;
|
||||
};
|
||||
|
||||
checkUpdate();
|
||||
} catch (error) {
|
||||
dispatch(addNoticeToast({ error: 'update_failed' }));
|
||||
dispatch(getUpdateFailure());
|
||||
}
|
||||
};
|
||||
|
||||
export const getClientsRequest = createAction('GET_CLIENTS_REQUEST');
|
||||
export const getClientsFailure = createAction('GET_CLIENTS_FAILURE');
|
||||
export const getClientsSuccess = createAction('GET_CLIENTS_SUCCESS');
|
||||
@@ -161,14 +219,41 @@ export const getClientsSuccess = createAction('GET_CLIENTS_SUCCESS');
|
||||
export const getClients = () => async (dispatch) => {
|
||||
dispatch(getClientsRequest());
|
||||
try {
|
||||
const clients = await apiClient.getGlobalClients();
|
||||
dispatch(getClientsSuccess(clients));
|
||||
const data = await apiClient.getClients();
|
||||
const sortedClients = data.clients && sortClients(data.clients);
|
||||
const sortedAutoClients = data.auto_clients && sortClients(data.auto_clients);
|
||||
|
||||
dispatch(getClientsSuccess({
|
||||
clients: sortedClients || [],
|
||||
autoClients: sortedAutoClients || [],
|
||||
}));
|
||||
} catch (error) {
|
||||
dispatch(addErrorToast({ error }));
|
||||
dispatch(getClientsFailure());
|
||||
}
|
||||
};
|
||||
|
||||
export const getTopStatsRequest = createAction('GET_TOP_STATS_REQUEST');
|
||||
export const getTopStatsFailure = createAction('GET_TOP_STATS_FAILURE');
|
||||
export const getTopStatsSuccess = createAction('GET_TOP_STATS_SUCCESS');
|
||||
|
||||
export const getTopStats = () => async (dispatch, getState) => {
|
||||
dispatch(getTopStatsRequest());
|
||||
const timer = setInterval(async () => {
|
||||
const state = getState();
|
||||
if (state.dashboard.isCoreRunning) {
|
||||
clearInterval(timer);
|
||||
try {
|
||||
const stats = await apiClient.getGlobalStatsTop();
|
||||
dispatch(getTopStatsSuccess(stats));
|
||||
} catch (error) {
|
||||
dispatch(addErrorToast({ error }));
|
||||
dispatch(getTopStatsFailure(error));
|
||||
}
|
||||
}
|
||||
}, 100);
|
||||
};
|
||||
|
||||
export const dnsStatusRequest = createAction('DNS_STATUS_REQUEST');
|
||||
export const dnsStatusFailure = createAction('DNS_STATUS_FAILURE');
|
||||
export const dnsStatusSuccess = createAction('DNS_STATUS_SUCCESS');
|
||||
@@ -180,6 +265,7 @@ export const getDnsStatus = () => async (dispatch) => {
|
||||
dispatch(dnsStatusSuccess(dnsStatus));
|
||||
dispatch(getVersion());
|
||||
dispatch(getClients());
|
||||
dispatch(getTopStats());
|
||||
} catch (error) {
|
||||
dispatch(addErrorToast({ error }));
|
||||
dispatch(initSettingsFailure());
|
||||
@@ -237,27 +323,6 @@ export const getStats = () => async (dispatch) => {
|
||||
}
|
||||
};
|
||||
|
||||
export const getTopStatsRequest = createAction('GET_TOP_STATS_REQUEST');
|
||||
export const getTopStatsFailure = createAction('GET_TOP_STATS_FAILURE');
|
||||
export const getTopStatsSuccess = createAction('GET_TOP_STATS_SUCCESS');
|
||||
|
||||
export const getTopStats = () => async (dispatch, getState) => {
|
||||
dispatch(getTopStatsRequest());
|
||||
const timer = setInterval(async () => {
|
||||
const state = getState();
|
||||
if (state.dashboard.isCoreRunning) {
|
||||
clearInterval(timer);
|
||||
try {
|
||||
const stats = await apiClient.getGlobalStatsTop();
|
||||
dispatch(getTopStatsSuccess(stats));
|
||||
} catch (error) {
|
||||
dispatch(addErrorToast({ error }));
|
||||
dispatch(getTopStatsFailure(error));
|
||||
}
|
||||
}
|
||||
}, 100);
|
||||
};
|
||||
|
||||
export const getLogsRequest = createAction('GET_LOGS_REQUEST');
|
||||
export const getLogsFailure = createAction('GET_LOGS_FAILURE');
|
||||
export const getLogsSuccess = createAction('GET_LOGS_SUCCESS');
|
||||
@@ -603,41 +668,18 @@ 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());
|
||||
}
|
||||
dispatch(findActiveDhcp(values.interface_name));
|
||||
try {
|
||||
await apiClient.setDhcpConfig(updatedConfig);
|
||||
dispatch(setDhcpConfigSuccess(updatedConfig));
|
||||
dispatch(addSuccessToast('dhcp_config_saved'));
|
||||
} catch (error) {
|
||||
dispatch(addErrorToast({ error }));
|
||||
dispatch(setDhcpConfigFailure());
|
||||
}
|
||||
};
|
||||
|
||||
@@ -645,40 +687,60 @@ 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) => {
|
||||
export const toggleDhcp = values => async (dispatch) => {
|
||||
dispatch(toggleDhcpRequest());
|
||||
let config = { ...values, enabled: false };
|
||||
let successMessage = 'disabled_dhcp';
|
||||
|
||||
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 (!values.enabled) {
|
||||
config = { ...values, enabled: true };
|
||||
successMessage = 'enabled_dhcp';
|
||||
dispatch(findActiveDhcp(values.interface_name));
|
||||
}
|
||||
|
||||
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());
|
||||
}
|
||||
try {
|
||||
await apiClient.setDhcpConfig(config);
|
||||
dispatch(toggleDhcpSuccess());
|
||||
dispatch(addSuccessToast(successMessage));
|
||||
} catch (error) {
|
||||
dispatch(addErrorToast({ error }));
|
||||
dispatch(toggleDhcpFailure());
|
||||
}
|
||||
};
|
||||
|
||||
export const toggleLeaseModal = createAction('TOGGLE_LEASE_MODAL');
|
||||
|
||||
export const addStaticLeaseRequest = createAction('ADD_STATIC_LEASE_REQUEST');
|
||||
export const addStaticLeaseFailure = createAction('ADD_STATIC_LEASE_FAILURE');
|
||||
export const addStaticLeaseSuccess = createAction('ADD_STATIC_LEASE_SUCCESS');
|
||||
|
||||
export const addStaticLease = config => async (dispatch) => {
|
||||
dispatch(addStaticLeaseRequest());
|
||||
try {
|
||||
const name = config.hostname || config.ip;
|
||||
await apiClient.addStaticLease(config);
|
||||
dispatch(addStaticLeaseSuccess(config));
|
||||
dispatch(addSuccessToast(t('dhcp_lease_added', { key: name })));
|
||||
dispatch(toggleLeaseModal());
|
||||
} catch (error) {
|
||||
dispatch(addErrorToast({ error }));
|
||||
dispatch(addStaticLeaseFailure());
|
||||
}
|
||||
};
|
||||
|
||||
export const removeStaticLeaseRequest = createAction('REMOVE_STATIC_LEASE_REQUEST');
|
||||
export const removeStaticLeaseFailure = createAction('REMOVE_STATIC_LEASE_FAILURE');
|
||||
export const removeStaticLeaseSuccess = createAction('REMOVE_STATIC_LEASE_SUCCESS');
|
||||
|
||||
export const removeStaticLease = config => async (dispatch) => {
|
||||
dispatch(removeStaticLeaseRequest());
|
||||
try {
|
||||
const name = config.hostname || config.ip;
|
||||
await apiClient.removeStaticLease(config);
|
||||
dispatch(removeStaticLeaseSuccess(config));
|
||||
dispatch(addSuccessToast(t('dhcp_lease_deleted', { key: name })));
|
||||
} catch (error) {
|
||||
dispatch(addErrorToast({ error }));
|
||||
dispatch(removeStaticLeaseFailure());
|
||||
}
|
||||
};
|
||||
|
||||
@@ -39,7 +39,7 @@ export default class Api {
|
||||
GLOBAL_VERSION = { path: 'version.json', method: 'GET' };
|
||||
GLOBAL_ENABLE_PROTECTION = { path: 'enable_protection', method: 'POST' };
|
||||
GLOBAL_DISABLE_PROTECTION = { path: 'disable_protection', method: 'POST' };
|
||||
GLOBAL_CLIENTS = { path: 'clients', method: 'GET' }
|
||||
GLOBAL_UPDATE = { path: 'update', method: 'POST' };
|
||||
|
||||
restartGlobalFiltering() {
|
||||
const { path, method } = this.GLOBAL_RESTART;
|
||||
@@ -140,8 +140,8 @@ export default class Api {
|
||||
return this.makeRequest(path, method);
|
||||
}
|
||||
|
||||
getGlobalClients() {
|
||||
const { path, method } = this.GLOBAL_CLIENTS;
|
||||
getUpdate() {
|
||||
const { path, method } = this.GLOBAL_UPDATE;
|
||||
return this.makeRequest(path, method);
|
||||
}
|
||||
|
||||
@@ -188,15 +188,14 @@ export default class Api {
|
||||
return this.makeRequest(path, method, config);
|
||||
}
|
||||
|
||||
removeFilter(url) {
|
||||
removeFilter(config) {
|
||||
const { path, method } = this.FILTERING_REMOVE_FILTER;
|
||||
const parameter = 'url';
|
||||
const requestBody = `${parameter}=${url}`;
|
||||
const config = {
|
||||
data: requestBody,
|
||||
header: { 'Content-Type': 'text/plain' },
|
||||
const parameters = {
|
||||
data: config,
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
};
|
||||
return this.makeRequest(path, method, config);
|
||||
|
||||
return this.makeRequest(path, method, parameters);
|
||||
}
|
||||
|
||||
setRules(rules) {
|
||||
@@ -318,6 +317,8 @@ export default class Api {
|
||||
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' };
|
||||
DHCP_ADD_STATIC_LEASE = { path: 'dhcp/add_static_lease', method: 'POST' };
|
||||
DHCP_REMOVE_STATIC_LEASE = { path: 'dhcp/remove_static_lease', method: 'POST' };
|
||||
|
||||
getDhcpStatus() {
|
||||
const { path, method } = this.DHCP_STATUS;
|
||||
@@ -347,6 +348,24 @@ export default class Api {
|
||||
return this.makeRequest(path, method, parameters);
|
||||
}
|
||||
|
||||
addStaticLease(config) {
|
||||
const { path, method } = this.DHCP_ADD_STATIC_LEASE;
|
||||
const parameters = {
|
||||
data: config,
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
};
|
||||
return this.makeRequest(path, method, parameters);
|
||||
}
|
||||
|
||||
removeStaticLease(config) {
|
||||
const { path, method } = this.DHCP_REMOVE_STATIC_LEASE;
|
||||
const parameters = {
|
||||
data: config,
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
};
|
||||
return this.makeRequest(path, method, parameters);
|
||||
}
|
||||
|
||||
// Installation
|
||||
INSTALL_GET_ADDRESSES = { path: 'install/get_addresses', method: 'GET' };
|
||||
INSTALL_CONFIGURE = { path: 'install/configure', method: 'POST' };
|
||||
@@ -402,4 +421,60 @@ export default class Api {
|
||||
};
|
||||
return this.makeRequest(path, method, parameters);
|
||||
}
|
||||
|
||||
// Per-client settings
|
||||
GET_CLIENTS = { path: 'clients', method: 'GET' };
|
||||
ADD_CLIENT = { path: 'clients/add', method: 'POST' };
|
||||
DELETE_CLIENT = { path: 'clients/delete', method: 'POST' };
|
||||
UPDATE_CLIENT = { path: 'clients/update', method: 'POST' };
|
||||
|
||||
getClients() {
|
||||
const { path, method } = this.GET_CLIENTS;
|
||||
return this.makeRequest(path, method);
|
||||
}
|
||||
|
||||
addClient(config) {
|
||||
const { path, method } = this.ADD_CLIENT;
|
||||
const parameters = {
|
||||
data: config,
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
};
|
||||
return this.makeRequest(path, method, parameters);
|
||||
}
|
||||
|
||||
deleteClient(config) {
|
||||
const { path, method } = this.DELETE_CLIENT;
|
||||
const parameters = {
|
||||
data: config,
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
};
|
||||
return this.makeRequest(path, method, parameters);
|
||||
}
|
||||
|
||||
updateClient(config) {
|
||||
const { path, method } = this.UPDATE_CLIENT;
|
||||
const parameters = {
|
||||
data: config,
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
};
|
||||
return this.makeRequest(path, method, parameters);
|
||||
}
|
||||
|
||||
// DNS access settings
|
||||
ACCESS_LIST = { path: 'access/list', method: 'GET' };
|
||||
ACCESS_SET = { path: 'access/set', method: 'POST' };
|
||||
|
||||
getAccessList() {
|
||||
const { path, method } = this.ACCESS_LIST;
|
||||
return this.makeRequest(path, method);
|
||||
}
|
||||
|
||||
setAccessList(config) {
|
||||
const { path, method } = this.ACCESS_SET;
|
||||
const parameters = {
|
||||
data: config,
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
};
|
||||
return this.makeRequest(path, method, parameters);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,13 +13,21 @@ import Header from '../../containers/Header';
|
||||
import Dashboard from '../../containers/Dashboard';
|
||||
import Settings from '../../containers/Settings';
|
||||
import Filters from '../../containers/Filters';
|
||||
|
||||
import Dns from '../../containers/Dns';
|
||||
import Encryption from '../../containers/Encryption';
|
||||
import Dhcp from '../../containers/Dhcp';
|
||||
import Clients from '../../containers/Clients';
|
||||
|
||||
import Logs from '../../containers/Logs';
|
||||
import SetupGuide from '../../containers/SetupGuide';
|
||||
import Toasts from '../Toasts';
|
||||
import Footer from '../ui/Footer';
|
||||
import Status from '../ui/Status';
|
||||
import UpdateTopline from '../ui/UpdateTopline';
|
||||
import UpdateOverlay from '../ui/UpdateOverlay';
|
||||
import EncryptionTopline from '../ui/EncryptionTopline';
|
||||
import Icons from '../ui/Icons';
|
||||
import i18n from '../../i18n';
|
||||
|
||||
class App extends Component {
|
||||
@@ -37,6 +45,10 @@ class App extends Component {
|
||||
this.props.enableDns();
|
||||
};
|
||||
|
||||
handleUpdate = () => {
|
||||
this.props.getUpdate();
|
||||
};
|
||||
|
||||
setLanguage = () => {
|
||||
const { processing, language } = this.props.dashboard;
|
||||
|
||||
@@ -49,49 +61,58 @@ class App extends Component {
|
||||
i18n.on('languageChanged', (lang) => {
|
||||
this.props.changeLanguage(lang);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
render() {
|
||||
const { dashboard, encryption } = this.props;
|
||||
const updateAvailable =
|
||||
!dashboard.processingVersions &&
|
||||
dashboard.isCoreRunning &&
|
||||
dashboard.isUpdateAvailable;
|
||||
!dashboard.processingVersions && dashboard.isCoreRunning && dashboard.isUpdateAvailable;
|
||||
|
||||
return (
|
||||
<HashRouter hashType='noslash'>
|
||||
<HashRouter hashType="noslash">
|
||||
<Fragment>
|
||||
{updateAvailable &&
|
||||
<UpdateTopline
|
||||
url={dashboard.announcementUrl}
|
||||
version={dashboard.version}
|
||||
/>
|
||||
}
|
||||
{!encryption.processing &&
|
||||
{updateAvailable && (
|
||||
<Fragment>
|
||||
<UpdateTopline
|
||||
url={dashboard.announcementUrl}
|
||||
version={dashboard.newVersion}
|
||||
canAutoUpdate={dashboard.canAutoUpdate}
|
||||
getUpdate={this.handleUpdate}
|
||||
processingUpdate={dashboard.processingUpdate}
|
||||
/>
|
||||
<UpdateOverlay processingUpdate={dashboard.processingUpdate} />
|
||||
</Fragment>
|
||||
)}
|
||||
{!encryption.processing && (
|
||||
<EncryptionTopline notAfter={encryption.not_after} />
|
||||
}
|
||||
)}
|
||||
<LoadingBar className="loading-bar" updateTime={1000} />
|
||||
<Route component={Header} />
|
||||
<div className="container container--wrap">
|
||||
{!dashboard.processing && !dashboard.isCoreRunning &&
|
||||
{!dashboard.processing && !dashboard.isCoreRunning && (
|
||||
<div className="row row-cards">
|
||||
<div className="col-lg-12">
|
||||
<Status handleStatusChange={this.handleStatusChange} />
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
{!dashboard.processing && dashboard.isCoreRunning &&
|
||||
)}
|
||||
{!dashboard.processing && dashboard.isCoreRunning && (
|
||||
<Fragment>
|
||||
<Route path="/" exact component={Dashboard} />
|
||||
<Route path="/settings" component={Settings} />
|
||||
<Route path="/dns" component={Dns} />
|
||||
<Route path="/encryption" component={Encryption} />
|
||||
<Route path="/dhcp" component={Dhcp} />
|
||||
<Route path="/clients" component={Clients} />
|
||||
<Route path="/filters" component={Filters} />
|
||||
<Route path="/logs" component={Logs} />
|
||||
<Route path="/guide" component={SetupGuide} />
|
||||
</Fragment>
|
||||
}
|
||||
)}
|
||||
</div>
|
||||
<Footer />
|
||||
<Toasts />
|
||||
<Icons />
|
||||
</Fragment>
|
||||
</HashRouter>
|
||||
);
|
||||
@@ -100,6 +121,7 @@ class App extends Component {
|
||||
|
||||
App.propTypes = {
|
||||
getDnsStatus: PropTypes.func,
|
||||
getUpdate: PropTypes.func,
|
||||
enableDns: PropTypes.func,
|
||||
dashboard: PropTypes.object,
|
||||
isCoreRunning: PropTypes.bool,
|
||||
|
||||
@@ -24,7 +24,8 @@ class Clients extends Component {
|
||||
Header: 'IP',
|
||||
accessor: 'ip',
|
||||
Cell: ({ value }) => {
|
||||
const clientName = getClientName(this.props.clients, value);
|
||||
const clientName = getClientName(this.props.clients, value)
|
||||
|| getClientName(this.props.autoClients, value);
|
||||
let client;
|
||||
|
||||
if (clientName) {
|
||||
@@ -79,6 +80,7 @@ Clients.propTypes = {
|
||||
dnsQueries: PropTypes.number.isRequired,
|
||||
refreshButton: PropTypes.node.isRequired,
|
||||
clients: PropTypes.array.isRequired,
|
||||
autoClients: PropTypes.array.isRequired,
|
||||
t: PropTypes.func,
|
||||
};
|
||||
|
||||
|
||||
@@ -21,6 +21,7 @@ class Dashboard extends Component {
|
||||
this.props.getStats();
|
||||
this.props.getStatsHistory();
|
||||
this.props.getTopStats();
|
||||
this.props.getClients();
|
||||
}
|
||||
|
||||
getToggleFilteringButton = () => {
|
||||
@@ -96,6 +97,7 @@ class Dashboard extends Component {
|
||||
refreshButton={refreshButton}
|
||||
topClients={dashboard.topStats.top_clients}
|
||||
clients={dashboard.clients}
|
||||
autoClients={dashboard.autoClients}
|
||||
/>
|
||||
</div>
|
||||
<div className="col-lg-6">
|
||||
@@ -131,6 +133,7 @@ Dashboard.propTypes = {
|
||||
isCoreRunning: PropTypes.bool,
|
||||
getFiltering: PropTypes.func,
|
||||
toggleProtection: PropTypes.func,
|
||||
getClients: PropTypes.func,
|
||||
processingProtection: PropTypes.bool,
|
||||
t: PropTypes.func,
|
||||
};
|
||||
|
||||
@@ -1,13 +0,0 @@
|
||||
.remove-icon {
|
||||
position: relative;
|
||||
top: 2px;
|
||||
display: inline-block;
|
||||
width: 20px;
|
||||
height: 18px;
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
.remove-icon:hover {
|
||||
cursor: pointer;
|
||||
opacity: 1;
|
||||
}
|
||||
@@ -6,7 +6,6 @@ import Modal from '../ui/Modal';
|
||||
import PageTitle from '../ui/PageTitle';
|
||||
import Card from '../ui/Card';
|
||||
import UserRules from './UserRules';
|
||||
import './Filters.css';
|
||||
|
||||
class Filters extends Component {
|
||||
componentDidMount() {
|
||||
@@ -33,6 +32,13 @@ class Filters extends Component {
|
||||
);
|
||||
};
|
||||
|
||||
handleDelete = (url) => {
|
||||
// eslint-disable-next-line no-alert
|
||||
if (window.confirm(this.props.t('filter_confirm_delete'))) {
|
||||
this.props.removeFilter({ url });
|
||||
}
|
||||
}
|
||||
|
||||
columns = [{
|
||||
Header: <Trans>enabled_table_header</Trans>,
|
||||
accessor: 'enabled',
|
||||
@@ -59,7 +65,18 @@ class Filters extends Component {
|
||||
}, {
|
||||
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)}/>),
|
||||
Cell: ({ value }) => (
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-icon btn-outline-secondary btn-sm"
|
||||
onClick={() => this.handleDelete(value)}
|
||||
title={this.props.t('delete_table_action')}
|
||||
>
|
||||
<svg className="icons">
|
||||
<use xlinkHref="#delete" />
|
||||
</svg>
|
||||
</button>
|
||||
),
|
||||
className: 'text-center',
|
||||
width: 80,
|
||||
sortable: false,
|
||||
|
||||
@@ -16,11 +16,13 @@
|
||||
stroke: #9aa0ac;
|
||||
}
|
||||
|
||||
.nav-tabs .nav-link.active .nav-icon {
|
||||
.nav-tabs .nav-link.active .nav-icon,
|
||||
.nav-tabs .nav-item.show .nav-icon {
|
||||
stroke: #66b574;
|
||||
}
|
||||
|
||||
.nav-tabs .nav-link.active:hover .nav-icon {
|
||||
.nav-tabs .nav-link.active:hover .nav-icon,
|
||||
.nav-tabs .nav-item.show:hover .nav-icon {
|
||||
stroke: #58a273;
|
||||
}
|
||||
|
||||
@@ -87,6 +89,12 @@
|
||||
height: 32px;
|
||||
}
|
||||
|
||||
.nav-tabs .nav-item.show .nav-link {
|
||||
color: #66b574;
|
||||
background-color: #fff;
|
||||
border-bottom-color: #66b574;
|
||||
}
|
||||
|
||||
@media screen and (min-width: 992px) {
|
||||
.header {
|
||||
padding: 0;
|
||||
|
||||
@@ -5,6 +5,9 @@ import enhanceWithClickOutside from 'react-click-outside';
|
||||
import classnames from 'classnames';
|
||||
import { Trans, withNamespaces } from 'react-i18next';
|
||||
|
||||
import { SETTINGS_URLS } from '../../helpers/constants';
|
||||
import Dropdown from '../ui/Dropdown';
|
||||
|
||||
class Menu extends Component {
|
||||
handleClickOutside = () => {
|
||||
this.props.closeMenu();
|
||||
@@ -14,49 +17,86 @@ class Menu extends Component {
|
||||
this.props.toggleMenuOpen();
|
||||
};
|
||||
|
||||
getActiveClassForSettings = () => {
|
||||
const { pathname } = this.props.location;
|
||||
const isSettingsPage = SETTINGS_URLS.some(item => item === pathname);
|
||||
|
||||
return isSettingsPage ? 'active' : '';
|
||||
};
|
||||
|
||||
render() {
|
||||
const menuClass = classnames({
|
||||
'col-lg-6 mobile-menu': true,
|
||||
'mobile-menu--active': this.props.isMenuOpen,
|
||||
});
|
||||
|
||||
const dropdownControlClass = `nav-link ${this.getActiveClassForSettings()}`;
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
<div className={menuClass}>
|
||||
<ul className="nav nav-tabs border-0 flex-column flex-lg-row flex-nowrap">
|
||||
<li className="nav-item border-bottom d-lg-none" onClick={this.toggleMenu}>
|
||||
<div className="nav-link nav-link--back">
|
||||
<svg className="nav-icon" fill="none" height="24" strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" viewBox="0 0 24 24" width="24" xmlns="http://www.w3.org/2000/svg"><path d="m19 12h-14"/><path d="m12 19-7-7 7-7"/></svg>
|
||||
<svg className="nav-icon">
|
||||
<use xlinkHref="#back" />
|
||||
</svg>
|
||||
<Trans>back</Trans>
|
||||
</div>
|
||||
</li>
|
||||
<li className="nav-item">
|
||||
<NavLink to="/" exact={true} className="nav-link">
|
||||
<svg className="nav-icon" fill="none" height="24" stroke="#9aa0ac" strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" viewBox="0 0 24 24" width="24" xmlns="http://www.w3.org/2000/svg"><path d="m3 9 9-7 9 7v11a2 2 0 0 1 -2 2h-14a2 2 0 0 1 -2-2z"/><path d="m9 22v-10h6v10"/></svg>
|
||||
<svg className="nav-icon">
|
||||
<use xlinkHref="#dashboard" />
|
||||
</svg>
|
||||
<Trans>dashboard</Trans>
|
||||
</NavLink>
|
||||
</li>
|
||||
<li className="nav-item">
|
||||
<NavLink to="/settings" className="nav-link">
|
||||
<svg className="nav-icon" fill="none" height="24" strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" viewBox="0 0 24 24" width="24" xmlns="http://www.w3.org/2000/svg"><circle cx="12" cy="12" r="3"/><path d="m19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1 0 2.83 2 2 0 0 1 -2.83 0l-.06-.06a1.65 1.65 0 0 0 -1.82-.33 1.65 1.65 0 0 0 -1 1.51v.17a2 2 0 0 1 -2 2 2 2 0 0 1 -2-2v-.09a1.65 1.65 0 0 0 -1.08-1.51 1.65 1.65 0 0 0 -1.82.33l-.06.06a2 2 0 0 1 -2.83 0 2 2 0 0 1 0-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0 -1.51-1h-.17a2 2 0 0 1 -2-2 2 2 0 0 1 2-2h.09a1.65 1.65 0 0 0 1.51-1.08 1.65 1.65 0 0 0 -.33-1.82l-.06-.06a2 2 0 0 1 0-2.83 2 2 0 0 1 2.83 0l.06.06a1.65 1.65 0 0 0 1.82.33h.08a1.65 1.65 0 0 0 1-1.51v-.17a2 2 0 0 1 2-2 2 2 0 0 1 2 2v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 0 2 2 0 0 1 0 2.83l-.06.06a1.65 1.65 0 0 0 -.33 1.82v.08a1.65 1.65 0 0 0 1.51 1h.17a2 2 0 0 1 2 2 2 2 0 0 1 -2 2h-.09a1.65 1.65 0 0 0 -1.51 1z"/></svg>
|
||||
<Trans>settings</Trans>
|
||||
</NavLink>
|
||||
</li>
|
||||
<Dropdown
|
||||
label={this.props.t('settings')}
|
||||
baseClassName="dropdown nav-item"
|
||||
controlClassName={dropdownControlClass}
|
||||
icon="settings"
|
||||
>
|
||||
<Fragment>
|
||||
<NavLink to="/settings" className="dropdown-item">
|
||||
<Trans>general_settings</Trans>
|
||||
</NavLink>
|
||||
<NavLink to="/dns" className="dropdown-item">
|
||||
<Trans>dns_settings</Trans>
|
||||
</NavLink>
|
||||
<NavLink to="/encryption" className="dropdown-item">
|
||||
<Trans>encryption_settings</Trans>
|
||||
</NavLink>
|
||||
<NavLink to="/clients" className="dropdown-item">
|
||||
<Trans>client_settings</Trans>
|
||||
</NavLink>
|
||||
<NavLink to="/dhcp" className="dropdown-item">
|
||||
<Trans>dhcp_settings</Trans>
|
||||
</NavLink>
|
||||
</Fragment>
|
||||
</Dropdown>
|
||||
<li className="nav-item">
|
||||
<NavLink to="/filters" className="nav-link">
|
||||
<svg className="nav-icon" fill="none" height="24" strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" viewBox="0 0 24 24" width="24" xmlns="http://www.w3.org/2000/svg"><path d="m22 3h-20l8 9.46v6.54l4 2v-8.54z"/></svg>
|
||||
<svg className="nav-icon">
|
||||
<use xlinkHref="#filters" />
|
||||
</svg>
|
||||
<Trans>filters</Trans>
|
||||
</NavLink>
|
||||
</li>
|
||||
<li className="nav-item">
|
||||
<NavLink to="/logs" className="nav-link">
|
||||
<svg className="nav-icon" fill="none" height="24" strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" viewBox="0 0 24 24" width="24" xmlns="http://www.w3.org/2000/svg"><path d="m14 2h-8a2 2 0 0 0 -2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2v-12z"/><path d="m14 2v6h6"/><path d="m16 13h-8"/><path d="m16 17h-8"/><path d="m10 9h-1-1"/></svg>
|
||||
<svg className="nav-icon">
|
||||
<use xlinkHref="#log" />
|
||||
</svg>
|
||||
<Trans>query_log</Trans>
|
||||
</NavLink>
|
||||
</li>
|
||||
<li className="nav-item">
|
||||
<NavLink to="/guide" href="/guide" className="nav-link">
|
||||
<svg className="nav-icon" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="#66b574" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><circle cx="12" cy="12" r="10"></circle><path d="M9.09 9a3 3 0 0 1 5.83 1c0 2-3 3-3 3"></path><line x1="12" y1="17" x2="12" y2="17"></line></svg>
|
||||
<NavLink to="/guide" className="nav-link">
|
||||
<svg className="nav-icon">
|
||||
<use xlinkHref="#setup" />
|
||||
</svg>
|
||||
<Trans>setup_guide</Trans>
|
||||
</NavLink>
|
||||
</li>
|
||||
@@ -71,6 +111,8 @@ Menu.propTypes = {
|
||||
isMenuOpen: PropTypes.bool,
|
||||
closeMenu: PropTypes.func,
|
||||
toggleMenuOpen: PropTypes.func,
|
||||
location: PropTypes.object,
|
||||
t: PropTypes.func,
|
||||
};
|
||||
|
||||
export default withNamespaces()(enhanceWithClickOutside(Menu));
|
||||
|
||||
@@ -5,6 +5,10 @@
|
||||
min-height: 26px;
|
||||
}
|
||||
|
||||
.logs__row--center {
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.logs__row--overflow {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
@@ -196,7 +196,8 @@ class Logs extends Component {
|
||||
Cell: (row) => {
|
||||
const { reason } = row.original;
|
||||
const isFiltered = row ? reason.indexOf('Filtered') === 0 : false;
|
||||
const clientName = getClientName(dashboard.clients, row.value);
|
||||
const clientName = getClientName(dashboard.clients, row.value)
|
||||
|| getClientName(dashboard.autoClients, row.value);
|
||||
let client;
|
||||
|
||||
if (clientName) {
|
||||
|
||||
118
client/src/components/Settings/Clients/AutoClients.js
Normal file
118
client/src/components/Settings/Clients/AutoClients.js
Normal file
@@ -0,0 +1,118 @@
|
||||
import React, { Component } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { withNamespaces } from 'react-i18next';
|
||||
import ReactTable from 'react-table';
|
||||
|
||||
import { CLIENT_ID } from '../../../helpers/constants';
|
||||
import Card from '../../ui/Card';
|
||||
|
||||
class AutoClients extends Component {
|
||||
getClient = (name, clients) => {
|
||||
const client = clients.find(item => name === item.name);
|
||||
|
||||
if (client) {
|
||||
const identifier = client.mac ? CLIENT_ID.MAC : CLIENT_ID.IP;
|
||||
|
||||
return {
|
||||
identifier,
|
||||
use_global_settings: true,
|
||||
...client,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
identifier: 'ip',
|
||||
use_global_settings: true,
|
||||
};
|
||||
};
|
||||
|
||||
getStats = (ip, stats) => {
|
||||
if (stats && stats.top_clients) {
|
||||
return stats.top_clients[ip];
|
||||
}
|
||||
|
||||
return '';
|
||||
};
|
||||
|
||||
cellWrap = ({ value }) => (
|
||||
<div className="logs__row logs__row--overflow">
|
||||
<span className="logs__text" title={value}>
|
||||
{value}
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
|
||||
columns = [
|
||||
{
|
||||
Header: this.props.t('table_client'),
|
||||
accessor: 'ip',
|
||||
Cell: this.cellWrap,
|
||||
},
|
||||
{
|
||||
Header: this.props.t('table_name'),
|
||||
accessor: 'name',
|
||||
Cell: this.cellWrap,
|
||||
},
|
||||
{
|
||||
Header: this.props.t('source_label'),
|
||||
accessor: 'source',
|
||||
Cell: this.cellWrap,
|
||||
},
|
||||
{
|
||||
Header: this.props.t('table_statistics'),
|
||||
accessor: 'statistics',
|
||||
Cell: (row) => {
|
||||
const clientIP = row.original.ip;
|
||||
const clientStats = clientIP && this.getStats(clientIP, this.props.topStats);
|
||||
|
||||
if (clientStats) {
|
||||
return (
|
||||
<div className="logs__row">
|
||||
<div className="logs__text" title={clientStats}>
|
||||
{clientStats}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return '–';
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
render() {
|
||||
const { t, autoClients } = this.props;
|
||||
|
||||
return (
|
||||
<Card
|
||||
title={t('auto_clients_title')}
|
||||
subtitle={t('auto_clients_desc')}
|
||||
bodyType="card-body box-body--settings"
|
||||
>
|
||||
<ReactTable
|
||||
data={autoClients || []}
|
||||
columns={this.columns}
|
||||
className="-striped -highlight card-table-overflow"
|
||||
showPagination={true}
|
||||
defaultPageSize={10}
|
||||
minRows={5}
|
||||
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('clients_not_found')}
|
||||
/>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
AutoClients.propTypes = {
|
||||
t: PropTypes.func.isRequired,
|
||||
autoClients: PropTypes.array.isRequired,
|
||||
topStats: PropTypes.object.isRequired,
|
||||
};
|
||||
|
||||
export default withNamespaces()(AutoClients);
|
||||
260
client/src/components/Settings/Clients/ClientsTable.js
Normal file
260
client/src/components/Settings/Clients/ClientsTable.js
Normal file
@@ -0,0 +1,260 @@
|
||||
import React, { Component, Fragment } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { Trans, withNamespaces } from 'react-i18next';
|
||||
import ReactTable from 'react-table';
|
||||
|
||||
import { MODAL_TYPE, CLIENT_ID } from '../../../helpers/constants';
|
||||
import Card from '../../ui/Card';
|
||||
import Modal from './Modal';
|
||||
|
||||
class ClientsTable extends Component {
|
||||
handleFormAdd = (values) => {
|
||||
this.props.addClient(values);
|
||||
};
|
||||
|
||||
handleFormUpdate = (values, name) => {
|
||||
this.props.updateClient(values, name);
|
||||
};
|
||||
|
||||
handleSubmit = (values) => {
|
||||
if (this.props.modalType === MODAL_TYPE.EDIT) {
|
||||
this.handleFormUpdate(values, this.props.modalClientName);
|
||||
} else {
|
||||
this.handleFormAdd(values);
|
||||
}
|
||||
};
|
||||
|
||||
cellWrap = ({ value }) => (
|
||||
<div className="logs__row logs__row--overflow">
|
||||
<span className="logs__text" title={value}>
|
||||
{value}
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
|
||||
getClient = (name, clients) => {
|
||||
const client = clients.find(item => name === item.name);
|
||||
|
||||
if (client) {
|
||||
const identifier = client.mac ? CLIENT_ID.MAC : CLIENT_ID.IP;
|
||||
|
||||
return {
|
||||
identifier,
|
||||
use_global_settings: true,
|
||||
...client,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
identifier: CLIENT_ID.IP,
|
||||
use_global_settings: true,
|
||||
};
|
||||
};
|
||||
|
||||
getStats = (ip, stats) => {
|
||||
if (stats && stats.top_clients) {
|
||||
return stats.top_clients[ip];
|
||||
}
|
||||
|
||||
return '';
|
||||
};
|
||||
|
||||
handleDelete = (data) => {
|
||||
// eslint-disable-next-line no-alert
|
||||
if (window.confirm(this.props.t('client_confirm_delete', { key: data.name }))) {
|
||||
this.props.deleteClient(data);
|
||||
}
|
||||
};
|
||||
|
||||
columns = [
|
||||
{
|
||||
Header: this.props.t('table_client'),
|
||||
accessor: 'ip',
|
||||
Cell: (row) => {
|
||||
if (row.original && row.original.mac) {
|
||||
return (
|
||||
<div className="logs__row logs__row--overflow">
|
||||
<span className="logs__text" title={row.original.mac}>
|
||||
{row.original.mac} <em>(MAC)</em>
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
} else if (row.value) {
|
||||
return (
|
||||
<div className="logs__row logs__row--overflow">
|
||||
<span className="logs__text" title={row.value}>
|
||||
{row.value} <em>(IP)</em>
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return '';
|
||||
},
|
||||
},
|
||||
{
|
||||
Header: this.props.t('table_name'),
|
||||
accessor: 'name',
|
||||
Cell: this.cellWrap,
|
||||
},
|
||||
{
|
||||
Header: this.props.t('settings'),
|
||||
accessor: 'use_global_settings',
|
||||
Cell: ({ value }) => {
|
||||
const title = value ? (
|
||||
<Trans>settings_global</Trans>
|
||||
) : (
|
||||
<Trans>settings_custom</Trans>
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="logs__row logs__row--overflow">
|
||||
<div className="logs__text" title={title}>
|
||||
{title}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
Header: this.props.t('table_statistics'),
|
||||
accessor: 'statistics',
|
||||
Cell: (row) => {
|
||||
const clientIP = row.original.ip;
|
||||
const clientStats = clientIP && this.getStats(clientIP, this.props.topStats);
|
||||
|
||||
if (clientStats) {
|
||||
return (
|
||||
<div className="logs__row">
|
||||
<div className="logs__text" title={clientStats}>
|
||||
{clientStats}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return '–';
|
||||
},
|
||||
},
|
||||
{
|
||||
Header: this.props.t('actions_table_header'),
|
||||
accessor: 'actions',
|
||||
maxWidth: 150,
|
||||
Cell: (row) => {
|
||||
const clientName = row.original.name;
|
||||
const {
|
||||
toggleClientModal, processingDeleting, processingUpdating, t,
|
||||
} = this.props;
|
||||
|
||||
return (
|
||||
<div className="logs__row logs__row--center">
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-icon btn-outline-primary btn-sm mr-2"
|
||||
onClick={() =>
|
||||
toggleClientModal({
|
||||
type: MODAL_TYPE.EDIT,
|
||||
name: clientName,
|
||||
})
|
||||
}
|
||||
disabled={processingUpdating}
|
||||
title={t('edit_table_action')}
|
||||
>
|
||||
<svg className="icons">
|
||||
<use xlinkHref="#edit" />
|
||||
</svg>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-icon btn-outline-secondary btn-sm"
|
||||
onClick={() => this.handleDelete({ name: clientName })}
|
||||
disabled={processingDeleting}
|
||||
title={t('delete_table_action')}
|
||||
>
|
||||
<svg className="icons">
|
||||
<use xlinkHref="#delete" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
render() {
|
||||
const {
|
||||
t,
|
||||
clients,
|
||||
isModalOpen,
|
||||
modalType,
|
||||
modalClientName,
|
||||
toggleClientModal,
|
||||
processingAdding,
|
||||
processingUpdating,
|
||||
} = this.props;
|
||||
|
||||
const currentClientData = this.getClient(modalClientName, clients);
|
||||
|
||||
return (
|
||||
<Card
|
||||
title={t('clients_title')}
|
||||
subtitle={t('clients_desc')}
|
||||
bodyType="card-body box-body--settings"
|
||||
>
|
||||
<Fragment>
|
||||
<ReactTable
|
||||
data={clients || []}
|
||||
columns={this.columns}
|
||||
className="-striped -highlight card-table-overflow"
|
||||
showPagination={true}
|
||||
defaultPageSize={10}
|
||||
minRows={5}
|
||||
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('clients_not_found')}
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-success btn-standard mt-3"
|
||||
onClick={() => toggleClientModal(MODAL_TYPE.ADD)}
|
||||
disabled={processingAdding}
|
||||
>
|
||||
<Trans>client_add</Trans>
|
||||
</button>
|
||||
|
||||
<Modal
|
||||
isModalOpen={isModalOpen}
|
||||
modalType={modalType}
|
||||
toggleClientModal={toggleClientModal}
|
||||
currentClientData={currentClientData}
|
||||
handleSubmit={this.handleSubmit}
|
||||
processingAdding={processingAdding}
|
||||
processingUpdating={processingUpdating}
|
||||
/>
|
||||
</Fragment>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
ClientsTable.propTypes = {
|
||||
t: PropTypes.func.isRequired,
|
||||
clients: PropTypes.array.isRequired,
|
||||
topStats: PropTypes.object.isRequired,
|
||||
toggleClientModal: PropTypes.func.isRequired,
|
||||
deleteClient: PropTypes.func.isRequired,
|
||||
addClient: PropTypes.func.isRequired,
|
||||
updateClient: PropTypes.func.isRequired,
|
||||
isModalOpen: PropTypes.bool.isRequired,
|
||||
modalType: PropTypes.string.isRequired,
|
||||
modalClientName: PropTypes.string.isRequired,
|
||||
processingAdding: PropTypes.bool.isRequired,
|
||||
processingDeleting: PropTypes.bool.isRequired,
|
||||
processingUpdating: PropTypes.bool.isRequired,
|
||||
};
|
||||
|
||||
export default withNamespaces()(ClientsTable);
|
||||
217
client/src/components/Settings/Clients/Form.js
Normal file
217
client/src/components/Settings/Clients/Form.js
Normal file
@@ -0,0 +1,217 @@
|
||||
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 { renderField, renderSelectField, ipv4, mac, required } from '../../../helpers/form';
|
||||
import { CLIENT_ID } from '../../../helpers/constants';
|
||||
|
||||
let Form = (props) => {
|
||||
const {
|
||||
t,
|
||||
handleSubmit,
|
||||
reset,
|
||||
pristine,
|
||||
submitting,
|
||||
clientIdentifier,
|
||||
useGlobalSettings,
|
||||
toggleClientModal,
|
||||
processingAdding,
|
||||
processingUpdating,
|
||||
} = props;
|
||||
|
||||
return (
|
||||
<form onSubmit={handleSubmit}>
|
||||
<div className="modal-body">
|
||||
<div className="form__group">
|
||||
<div className="form-inline mb-3">
|
||||
<strong className="mr-3">
|
||||
<Trans>client_identifier</Trans>
|
||||
</strong>
|
||||
<label className="mr-3">
|
||||
<Field
|
||||
name="identifier"
|
||||
component={renderField}
|
||||
type="radio"
|
||||
className="form-control mr-2"
|
||||
value="ip"
|
||||
/>{' '}
|
||||
<Trans>ip_address</Trans>
|
||||
</label>
|
||||
<label>
|
||||
<Field
|
||||
name="identifier"
|
||||
component={renderField}
|
||||
type="radio"
|
||||
className="form-control mr-2"
|
||||
value="mac"
|
||||
/>{' '}
|
||||
MAC
|
||||
</label>
|
||||
</div>
|
||||
{clientIdentifier === CLIENT_ID.IP && (
|
||||
<div className="form__group">
|
||||
<Field
|
||||
id="ip"
|
||||
name="ip"
|
||||
component={renderField}
|
||||
type="text"
|
||||
className="form-control"
|
||||
placeholder={t('form_enter_ip')}
|
||||
validate={[ipv4, required]}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
{clientIdentifier === CLIENT_ID.MAC && (
|
||||
<div className="form__group">
|
||||
<Field
|
||||
id="mac"
|
||||
name="mac"
|
||||
component={renderField}
|
||||
type="text"
|
||||
className="form-control"
|
||||
placeholder={t('form_enter_mac')}
|
||||
validate={[mac, required]}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<div className="form__desc">
|
||||
<Trans
|
||||
components={[
|
||||
<a href="#settings_dhcp" key="0">
|
||||
link
|
||||
</a>,
|
||||
]}
|
||||
>
|
||||
client_identifier_desc
|
||||
</Trans>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="form__group">
|
||||
<Field
|
||||
id="name"
|
||||
name="name"
|
||||
component={renderField}
|
||||
type="text"
|
||||
className="form-control"
|
||||
placeholder={t('form_client_name')}
|
||||
validate={[required]}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="mb-4">
|
||||
<strong>
|
||||
<Trans>settings</Trans>
|
||||
</strong>
|
||||
</div>
|
||||
|
||||
<div className="form__group">
|
||||
<Field
|
||||
name="use_global_settings"
|
||||
type="checkbox"
|
||||
component={renderSelectField}
|
||||
placeholder={t('client_global_settings')}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="form__group">
|
||||
<Field
|
||||
name="filtering_enabled"
|
||||
type="checkbox"
|
||||
component={renderSelectField}
|
||||
placeholder={t('block_domain_use_filters_and_hosts')}
|
||||
disabled={useGlobalSettings}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="form__group">
|
||||
<Field
|
||||
name="safebrowsing_enabled"
|
||||
type="checkbox"
|
||||
component={renderSelectField}
|
||||
placeholder={t('use_adguard_browsing_sec')}
|
||||
disabled={useGlobalSettings}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="form__group">
|
||||
<Field
|
||||
name="parental_enabled"
|
||||
type="checkbox"
|
||||
component={renderSelectField}
|
||||
placeholder={t('use_adguard_parental')}
|
||||
disabled={useGlobalSettings}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="form__group">
|
||||
<Field
|
||||
name="safesearch_enabled"
|
||||
type="checkbox"
|
||||
component={renderSelectField}
|
||||
placeholder={t('enforce_safe_search')}
|
||||
disabled={useGlobalSettings}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="modal-footer">
|
||||
<div className="btn-list">
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-secondary btn-standard"
|
||||
disabled={submitting}
|
||||
onClick={() => {
|
||||
reset();
|
||||
toggleClientModal();
|
||||
}}
|
||||
>
|
||||
<Trans>cancel_btn</Trans>
|
||||
</button>
|
||||
<button
|
||||
type="submit"
|
||||
className="btn btn-success btn-standard"
|
||||
disabled={submitting || pristine || processingAdding || processingUpdating}
|
||||
>
|
||||
<Trans>save_btn</Trans>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
);
|
||||
};
|
||||
|
||||
Form.propTypes = {
|
||||
pristine: PropTypes.bool.isRequired,
|
||||
handleSubmit: PropTypes.func.isRequired,
|
||||
reset: PropTypes.func.isRequired,
|
||||
submitting: PropTypes.bool.isRequired,
|
||||
toggleClientModal: PropTypes.func.isRequired,
|
||||
clientIdentifier: PropTypes.string,
|
||||
useGlobalSettings: PropTypes.bool,
|
||||
t: PropTypes.func.isRequired,
|
||||
processingAdding: PropTypes.bool.isRequired,
|
||||
processingUpdating: PropTypes.bool.isRequired,
|
||||
};
|
||||
|
||||
const selector = formValueSelector('clientForm');
|
||||
|
||||
Form = connect((state) => {
|
||||
const clientIdentifier = selector(state, 'identifier');
|
||||
const useGlobalSettings = selector(state, 'use_global_settings');
|
||||
return {
|
||||
clientIdentifier,
|
||||
useGlobalSettings,
|
||||
};
|
||||
})(Form);
|
||||
|
||||
export default flow([
|
||||
withNamespaces(),
|
||||
reduxForm({
|
||||
form: 'clientForm',
|
||||
enableReinitialize: true,
|
||||
}),
|
||||
])(Form);
|
||||
64
client/src/components/Settings/Clients/Modal.js
Normal file
64
client/src/components/Settings/Clients/Modal.js
Normal file
@@ -0,0 +1,64 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { Trans, withNamespaces } from 'react-i18next';
|
||||
import ReactModal from 'react-modal';
|
||||
|
||||
import { MODAL_TYPE } from '../../../helpers/constants';
|
||||
import Form from './Form';
|
||||
|
||||
const Modal = (props) => {
|
||||
const {
|
||||
isModalOpen,
|
||||
modalType,
|
||||
currentClientData,
|
||||
handleSubmit,
|
||||
toggleClientModal,
|
||||
processingAdding,
|
||||
processingUpdating,
|
||||
} = props;
|
||||
|
||||
return (
|
||||
<ReactModal
|
||||
className="Modal__Bootstrap modal-dialog modal-dialog-centered modal-dialog--clients"
|
||||
closeTimeoutMS={0}
|
||||
isOpen={isModalOpen}
|
||||
onRequestClose={() => toggleClientModal()}
|
||||
>
|
||||
<div className="modal-content">
|
||||
<div className="modal-header">
|
||||
<h4 className="modal-title">
|
||||
{modalType === MODAL_TYPE.EDIT ? (
|
||||
<Trans>client_edit</Trans>
|
||||
) : (
|
||||
<Trans>client_new</Trans>
|
||||
)}
|
||||
</h4>
|
||||
<button type="button" className="close" onClick={() => toggleClientModal()}>
|
||||
<span className="sr-only">Close</span>
|
||||
</button>
|
||||
</div>
|
||||
<Form
|
||||
initialValues={{
|
||||
...currentClientData,
|
||||
}}
|
||||
onSubmit={handleSubmit}
|
||||
toggleClientModal={toggleClientModal}
|
||||
processingAdding={processingAdding}
|
||||
processingUpdating={processingUpdating}
|
||||
/>
|
||||
</div>
|
||||
</ReactModal>
|
||||
);
|
||||
};
|
||||
|
||||
Modal.propTypes = {
|
||||
isModalOpen: PropTypes.bool.isRequired,
|
||||
modalType: PropTypes.string.isRequired,
|
||||
currentClientData: PropTypes.object.isRequired,
|
||||
handleSubmit: PropTypes.func.isRequired,
|
||||
toggleClientModal: PropTypes.func.isRequired,
|
||||
processingAdding: PropTypes.bool.isRequired,
|
||||
processingUpdating: PropTypes.bool.isRequired,
|
||||
};
|
||||
|
||||
export default withNamespaces()(Modal);
|
||||
69
client/src/components/Settings/Clients/index.js
Normal file
69
client/src/components/Settings/Clients/index.js
Normal file
@@ -0,0 +1,69 @@
|
||||
import React, { Component, Fragment } from 'react';
|
||||
import { withNamespaces } from 'react-i18next';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import ClientsTable from './ClientsTable';
|
||||
import AutoClients from './AutoClients';
|
||||
import PageTitle from '../../ui/PageTitle';
|
||||
import Loading from '../../ui/Loading';
|
||||
|
||||
class Clients extends Component {
|
||||
componentDidMount() {
|
||||
this.props.getClients();
|
||||
}
|
||||
|
||||
render() {
|
||||
const {
|
||||
t,
|
||||
dashboard,
|
||||
clients,
|
||||
addClient,
|
||||
updateClient,
|
||||
deleteClient,
|
||||
toggleClientModal,
|
||||
} = this.props;
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
<PageTitle title={t('client_settings')} />
|
||||
{(dashboard.processingTopStats || dashboard.processingClients) && <Loading />}
|
||||
{!dashboard.processingTopStats && !dashboard.processingClients && (
|
||||
<Fragment>
|
||||
<ClientsTable
|
||||
clients={dashboard.clients}
|
||||
topStats={dashboard.topStats}
|
||||
isModalOpen={clients.isModalOpen}
|
||||
modalClientName={clients.modalClientName}
|
||||
modalType={clients.modalType}
|
||||
addClient={addClient}
|
||||
updateClient={updateClient}
|
||||
deleteClient={deleteClient}
|
||||
toggleClientModal={toggleClientModal}
|
||||
processingAdding={clients.processingAdding}
|
||||
processingDeleting={clients.processingDeleting}
|
||||
processingUpdating={clients.processingUpdating}
|
||||
/>
|
||||
<AutoClients
|
||||
autoClients={dashboard.autoClients}
|
||||
topStats={dashboard.topStats}
|
||||
/>
|
||||
</Fragment>
|
||||
)}
|
||||
</Fragment>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Clients.propTypes = {
|
||||
t: PropTypes.func.isRequired,
|
||||
dashboard: PropTypes.object.isRequired,
|
||||
clients: PropTypes.object.isRequired,
|
||||
toggleClientModal: PropTypes.func.isRequired,
|
||||
deleteClient: PropTypes.func.isRequired,
|
||||
addClient: PropTypes.func.isRequired,
|
||||
updateClient: PropTypes.func.isRequired,
|
||||
getClients: PropTypes.func.isRequired,
|
||||
topStats: PropTypes.object,
|
||||
};
|
||||
|
||||
export default withNamespaces()(Clients);
|
||||
@@ -1,22 +1,97 @@
|
||||
import React from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import PropTypes from 'prop-types';
|
||||
import { Field, reduxForm } from 'redux-form';
|
||||
import { withNamespaces } from 'react-i18next';
|
||||
import { Field, reduxForm, formValueSelector } from 'redux-form';
|
||||
import { Trans, withNamespaces } from 'react-i18next';
|
||||
import flow from 'lodash/flow';
|
||||
|
||||
import { renderField, required, ipv4, isPositive, toNumber } from '../../../helpers/form';
|
||||
|
||||
const Form = (props) => {
|
||||
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 Form = (props) => {
|
||||
const {
|
||||
t,
|
||||
handleSubmit,
|
||||
submitting,
|
||||
invalid,
|
||||
enabled,
|
||||
interfaces,
|
||||
interfaceValue,
|
||||
processingConfig,
|
||||
processingInterfaces,
|
||||
} = props;
|
||||
|
||||
return (
|
||||
<form onSubmit={handleSubmit}>
|
||||
{!processingInterfaces && 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"
|
||||
validate={[required]}
|
||||
>
|
||||
<option value="" disabled={enabled}>
|
||||
{t('dhcp_interface_select')}
|
||||
</option>
|
||||
{renderInterfaces(interfaces)}
|
||||
</Field>
|
||||
</div>
|
||||
</div>
|
||||
{interfaceValue &&
|
||||
<div className="col-sm-12 col-md-6">
|
||||
{interfaces[interfaceValue] &&
|
||||
renderInterfaceValues(interfaces[interfaceValue])}
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
<hr/>
|
||||
<div className="row">
|
||||
<div className="col-lg-6">
|
||||
<div className="form__group form__group--settings">
|
||||
@@ -101,11 +176,24 @@ Form.propTypes = {
|
||||
submitting: PropTypes.bool,
|
||||
invalid: PropTypes.bool,
|
||||
interfaces: PropTypes.object,
|
||||
interfaceValue: PropTypes.string,
|
||||
initialValues: PropTypes.object,
|
||||
processingConfig: PropTypes.bool,
|
||||
processingInterfaces: PropTypes.bool,
|
||||
enabled: PropTypes.bool,
|
||||
t: PropTypes.func,
|
||||
};
|
||||
|
||||
|
||||
const selector = formValueSelector('dhcpForm');
|
||||
|
||||
Form = connect((state) => {
|
||||
const interfaceValue = selector(state, 'interface_name');
|
||||
return {
|
||||
interfaceValue,
|
||||
};
|
||||
})(Form);
|
||||
|
||||
export default flow([
|
||||
withNamespaces(),
|
||||
reduxForm({ form: 'dhcpForm' }),
|
||||
|
||||
@@ -1,114 +0,0 @@
|
||||
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">
|
||||
{interfaces[interfaceValue] &&
|
||||
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);
|
||||
96
client/src/components/Settings/Dhcp/StaticLeases/Form.js
Normal file
96
client/src/components/Settings/Dhcp/StaticLeases/Form.js
Normal file
@@ -0,0 +1,96 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { Field, reduxForm } from 'redux-form';
|
||||
import { Trans, withNamespaces } from 'react-i18next';
|
||||
import flow from 'lodash/flow';
|
||||
|
||||
import { renderField, ipv4, mac, required } from '../../../../helpers/form';
|
||||
|
||||
const Form = (props) => {
|
||||
const {
|
||||
t,
|
||||
handleSubmit,
|
||||
reset,
|
||||
pristine,
|
||||
submitting,
|
||||
toggleLeaseModal,
|
||||
processingAdding,
|
||||
} = props;
|
||||
|
||||
return (
|
||||
<form onSubmit={handleSubmit}>
|
||||
<div className="modal-body">
|
||||
<div className="form__group">
|
||||
<Field
|
||||
id="mac"
|
||||
name="mac"
|
||||
component={renderField}
|
||||
type="text"
|
||||
className="form-control"
|
||||
placeholder={t('form_enter_mac')}
|
||||
validate={[required, mac]}
|
||||
/>
|
||||
</div>
|
||||
<div className="form__group">
|
||||
<Field
|
||||
id="ip"
|
||||
name="ip"
|
||||
component={renderField}
|
||||
type="text"
|
||||
className="form-control"
|
||||
placeholder={t('form_enter_ip')}
|
||||
validate={[required, ipv4]}
|
||||
/>
|
||||
</div>
|
||||
<div className="form__group">
|
||||
<Field
|
||||
id="hostname"
|
||||
name="hostname"
|
||||
component={renderField}
|
||||
type="text"
|
||||
className="form-control"
|
||||
placeholder={t('form_enter_hostname')}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="modal-footer">
|
||||
<div className="btn-list">
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-secondary btn-standard"
|
||||
disabled={submitting}
|
||||
onClick={() => {
|
||||
reset();
|
||||
toggleLeaseModal();
|
||||
}}
|
||||
>
|
||||
<Trans>cancel_btn</Trans>
|
||||
</button>
|
||||
<button
|
||||
type="submit"
|
||||
className="btn btn-success btn-standard"
|
||||
disabled={submitting || pristine || processingAdding}
|
||||
>
|
||||
<Trans>save_btn</Trans>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
);
|
||||
};
|
||||
|
||||
Form.propTypes = {
|
||||
pristine: PropTypes.bool.isRequired,
|
||||
handleSubmit: PropTypes.func.isRequired,
|
||||
reset: PropTypes.func.isRequired,
|
||||
submitting: PropTypes.bool.isRequired,
|
||||
toggleLeaseModal: PropTypes.func.isRequired,
|
||||
processingAdding: PropTypes.bool.isRequired,
|
||||
t: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
export default flow([
|
||||
withNamespaces(),
|
||||
reduxForm({ form: 'leaseForm' }),
|
||||
])(Form);
|
||||
49
client/src/components/Settings/Dhcp/StaticLeases/Modal.js
Normal file
49
client/src/components/Settings/Dhcp/StaticLeases/Modal.js
Normal file
@@ -0,0 +1,49 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { Trans, withNamespaces } from 'react-i18next';
|
||||
import ReactModal from 'react-modal';
|
||||
|
||||
import Form from './Form';
|
||||
|
||||
const Modal = (props) => {
|
||||
const {
|
||||
isModalOpen,
|
||||
handleSubmit,
|
||||
toggleLeaseModal,
|
||||
processingAdding,
|
||||
} = props;
|
||||
|
||||
return (
|
||||
<ReactModal
|
||||
className="Modal__Bootstrap modal-dialog modal-dialog-centered modal-dialog--clients"
|
||||
closeTimeoutMS={0}
|
||||
isOpen={isModalOpen}
|
||||
onRequestClose={() => toggleLeaseModal()}
|
||||
>
|
||||
<div className="modal-content">
|
||||
<div className="modal-header">
|
||||
<h4 className="modal-title">
|
||||
<Trans>dhcp_new_static_lease</Trans>
|
||||
</h4>
|
||||
<button type="button" className="close" onClick={() => toggleLeaseModal()}>
|
||||
<span className="sr-only">Close</span>
|
||||
</button>
|
||||
</div>
|
||||
<Form
|
||||
onSubmit={handleSubmit}
|
||||
toggleLeaseModal={toggleLeaseModal}
|
||||
processingAdding={processingAdding}
|
||||
/>
|
||||
</div>
|
||||
</ReactModal>
|
||||
);
|
||||
};
|
||||
|
||||
Modal.propTypes = {
|
||||
isModalOpen: PropTypes.bool.isRequired,
|
||||
handleSubmit: PropTypes.func.isRequired,
|
||||
toggleLeaseModal: PropTypes.func.isRequired,
|
||||
processingAdding: PropTypes.bool.isRequired,
|
||||
};
|
||||
|
||||
export default withNamespaces()(Modal);
|
||||
112
client/src/components/Settings/Dhcp/StaticLeases/index.js
Normal file
112
client/src/components/Settings/Dhcp/StaticLeases/index.js
Normal file
@@ -0,0 +1,112 @@
|
||||
import React, { Component, Fragment } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import ReactTable from 'react-table';
|
||||
import { Trans, withNamespaces } from 'react-i18next';
|
||||
|
||||
import Modal from './Modal';
|
||||
|
||||
class StaticLeases extends Component {
|
||||
cellWrap = ({ value }) => (
|
||||
<div className="logs__row logs__row--overflow">
|
||||
<span className="logs__text" title={value}>
|
||||
{value}
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
|
||||
handleSubmit = (data) => {
|
||||
this.props.addStaticLease(data);
|
||||
}
|
||||
|
||||
handleDelete = (ip, mac, hostname = '') => {
|
||||
const name = hostname || ip;
|
||||
// eslint-disable-next-line no-alert
|
||||
if (window.confirm(this.props.t('delete_confirm', { key: name }))) {
|
||||
this.props.removeStaticLease({ ip, mac, hostname });
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
const {
|
||||
isModalOpen,
|
||||
toggleLeaseModal,
|
||||
processingAdding,
|
||||
processingDeleting,
|
||||
staticLeases,
|
||||
t,
|
||||
} = this.props;
|
||||
return (
|
||||
<Fragment>
|
||||
<ReactTable
|
||||
data={staticLeases || []}
|
||||
columns={[
|
||||
{
|
||||
Header: 'MAC',
|
||||
accessor: 'mac',
|
||||
Cell: this.cellWrap,
|
||||
},
|
||||
{
|
||||
Header: 'IP',
|
||||
accessor: 'ip',
|
||||
Cell: this.cellWrap,
|
||||
},
|
||||
{
|
||||
Header: <Trans>dhcp_table_hostname</Trans>,
|
||||
accessor: 'hostname',
|
||||
Cell: this.cellWrap,
|
||||
},
|
||||
{
|
||||
Header: <Trans>actions_table_header</Trans>,
|
||||
accessor: 'actions',
|
||||
maxWidth: 150,
|
||||
Cell: (row) => {
|
||||
const { ip, mac, hostname } = row.original;
|
||||
|
||||
return (
|
||||
<div className="logs__row logs__row--center">
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-icon btn-outline-secondary btn-sm"
|
||||
title={t('delete_table_action')}
|
||||
disabled={processingDeleting}
|
||||
onClick={() =>
|
||||
this.handleDelete(ip, mac, hostname)
|
||||
}
|
||||
>
|
||||
<svg className="icons">
|
||||
<use xlinkHref="#delete" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
},
|
||||
]}
|
||||
showPagination={false}
|
||||
noDataText={t('dhcp_static_leases_not_found')}
|
||||
className="-striped -highlight card-table-overflow"
|
||||
minRows={6}
|
||||
/>
|
||||
<Modal
|
||||
isModalOpen={isModalOpen}
|
||||
toggleLeaseModal={toggleLeaseModal}
|
||||
handleSubmit={this.handleSubmit}
|
||||
processingAdding={processingAdding}
|
||||
/>
|
||||
</Fragment>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
StaticLeases.propTypes = {
|
||||
staticLeases: PropTypes.array.isRequired,
|
||||
isModalOpen: PropTypes.bool.isRequired,
|
||||
toggleLeaseModal: PropTypes.func.isRequired,
|
||||
removeStaticLease: PropTypes.func.isRequired,
|
||||
addStaticLease: PropTypes.func.isRequired,
|
||||
processingAdding: PropTypes.bool.isRequired,
|
||||
processingDeleting: PropTypes.bool.isRequired,
|
||||
t: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
export default withNamespaces()(StaticLeases);
|
||||
@@ -6,18 +6,27 @@ import { Trans, withNamespaces } from 'react-i18next';
|
||||
import { DHCP_STATUS_RESPONSE } from '../../../helpers/constants';
|
||||
import Form from './Form';
|
||||
import Leases from './Leases';
|
||||
import Interface from './Interface';
|
||||
import StaticLeases from './StaticLeases/index';
|
||||
import Card from '../../ui/Card';
|
||||
import Accordion from '../../ui/Accordion';
|
||||
import PageTitle from '../../ui/PageTitle';
|
||||
import Loading from '../../ui/Loading';
|
||||
|
||||
class Dhcp extends Component {
|
||||
componentDidMount() {
|
||||
this.props.getDhcpStatus();
|
||||
this.props.getDhcpInterfaces();
|
||||
}
|
||||
|
||||
handleFormSubmit = (values) => {
|
||||
this.props.setDhcpConfig(values);
|
||||
if (values.interface_name) {
|
||||
this.props.setDhcpConfig(values);
|
||||
}
|
||||
};
|
||||
|
||||
handleToggle = (config) => {
|
||||
this.props.toggleDhcp(config);
|
||||
}
|
||||
};
|
||||
|
||||
getToggleDhcpButton = () => {
|
||||
const {
|
||||
@@ -52,17 +61,13 @@ class Dhcp extends Component {
|
||||
className="btn btn-standard mr-2 btn-success"
|
||||
onClick={() => this.handleToggle(config)}
|
||||
disabled={
|
||||
!filledConfig
|
||||
|| !check
|
||||
|| otherDhcpFound
|
||||
|| processingDhcp
|
||||
|| processingConfig
|
||||
!filledConfig || !check || otherDhcpFound || processingDhcp || processingConfig
|
||||
}
|
||||
>
|
||||
<Trans>dhcp_enable</Trans>
|
||||
</button>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
getActiveDhcpMessage = (t, check) => {
|
||||
const { found } = check.otherServer;
|
||||
@@ -93,7 +98,7 @@ class Dhcp extends Component {
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
getDhcpWarning = (check) => {
|
||||
if (check.otherServer.found === DHCP_STATUS_RESPONSE.NO) {
|
||||
@@ -105,7 +110,7 @@ class Dhcp extends Component {
|
||||
<Trans>dhcp_warning</Trans>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
getStaticIpWarning = (t, check, interfaceName) => {
|
||||
if (check.staticIP.static === DHCP_STATUS_RESPONSE.ERROR) {
|
||||
@@ -119,21 +124,19 @@ class Dhcp extends Component {
|
||||
</Accordion>
|
||||
</div>
|
||||
</div>
|
||||
<hr className="mt-4 mb-4"/>
|
||||
<hr className="mt-4 mb-4" />
|
||||
</Fragment>
|
||||
);
|
||||
} else if (
|
||||
check.staticIP.static === DHCP_STATUS_RESPONSE.NO
|
||||
&& check.staticIP.ip
|
||||
&& interfaceName
|
||||
check.staticIP.static === DHCP_STATUS_RESPONSE.NO &&
|
||||
check.staticIP.ip &&
|
||||
interfaceName
|
||||
) {
|
||||
return (
|
||||
<Fragment>
|
||||
<div className="text-secondary mb-2">
|
||||
<Trans
|
||||
components={[
|
||||
<strong key="0">example</strong>,
|
||||
]}
|
||||
components={[<strong key="0">example</strong>]}
|
||||
values={{
|
||||
interfaceName,
|
||||
ipAddress: check.staticIP.ip,
|
||||
@@ -142,13 +145,13 @@ class Dhcp extends Component {
|
||||
dhcp_dynamic_ip_found
|
||||
</Trans>
|
||||
</div>
|
||||
<hr className="mt-4 mb-4"/>
|
||||
<hr className="mt-4 mb-4" />
|
||||
</Fragment>
|
||||
);
|
||||
}
|
||||
|
||||
return '';
|
||||
}
|
||||
};
|
||||
|
||||
render() {
|
||||
const { t, dhcp } = this.props;
|
||||
@@ -156,69 +159,101 @@ class Dhcp extends Component {
|
||||
'btn btn-primary btn-standard': true,
|
||||
'btn btn-primary btn-standard btn-loading': dhcp.processingStatus,
|
||||
});
|
||||
const {
|
||||
enabled,
|
||||
interface_name,
|
||||
...values
|
||||
} = dhcp.config;
|
||||
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.enabled
|
||||
|| !dhcp.config.interface_name
|
||||
|| dhcp.processingConfig
|
||||
}
|
||||
>
|
||||
<Trans>check_dhcp_servers</Trans>
|
||||
</button>
|
||||
</div>
|
||||
{!enabled && dhcp.check &&
|
||||
<Fragment>
|
||||
{this.getStaticIpWarning(t, dhcp.check, interface_name)}
|
||||
{this.getActiveDhcpMessage(t, dhcp.check)}
|
||||
{this.getDhcpWarning(dhcp.check)}
|
||||
</Fragment>
|
||||
}
|
||||
</Fragment>
|
||||
}
|
||||
</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} />
|
||||
<PageTitle title={t('dhcp_settings')} />
|
||||
{(dhcp.processing || dhcp.processingInterfaces) && <Loading />}
|
||||
{!dhcp.processing && !dhcp.processingInterfaces && (
|
||||
<Fragment>
|
||||
<Card
|
||||
title={t('dhcp_title')}
|
||||
subtitle={t('dhcp_description')}
|
||||
bodyType="card-body box-body--settings"
|
||||
>
|
||||
<div className="dhcp">
|
||||
<Fragment>
|
||||
<Form
|
||||
onSubmit={this.handleFormSubmit}
|
||||
initialValues={{
|
||||
interface_name,
|
||||
...values,
|
||||
}}
|
||||
interfaces={dhcp.interfaces}
|
||||
processingConfig={dhcp.processingConfig}
|
||||
processingInterfaces={dhcp.processingInterfaces}
|
||||
enabled={enabled}
|
||||
/>
|
||||
<hr />
|
||||
<div className="card-actions mb-3">
|
||||
{this.getToggleDhcpButton()}
|
||||
<button
|
||||
type="button"
|
||||
className={statusButtonClass}
|
||||
onClick={() =>
|
||||
this.props.findActiveDhcp(interface_name)
|
||||
}
|
||||
disabled={
|
||||
enabled || !interface_name || dhcp.processingConfig
|
||||
}
|
||||
>
|
||||
<Trans>check_dhcp_servers</Trans>
|
||||
</button>
|
||||
</div>
|
||||
{!enabled && dhcp.check && (
|
||||
<Fragment>
|
||||
{this.getStaticIpWarning(t, dhcp.check, interface_name)}
|
||||
{this.getActiveDhcpMessage(t, dhcp.check)}
|
||||
{this.getDhcpWarning(dhcp.check)}
|
||||
</Fragment>
|
||||
)}
|
||||
</Fragment>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
}
|
||||
</Card>
|
||||
{dhcp.config.enabled && (
|
||||
<Fragment>
|
||||
<Card
|
||||
title={t('dhcp_leases')}
|
||||
bodyType="card-body box-body--settings"
|
||||
>
|
||||
<div className="row">
|
||||
<div className="col">
|
||||
<Leases leases={dhcp.leases} />
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
<Card
|
||||
title={t('dhcp_static_leases')}
|
||||
bodyType="card-body box-body--settings"
|
||||
>
|
||||
<div className="row">
|
||||
<div className="col-12">
|
||||
<StaticLeases
|
||||
staticLeases={dhcp.staticLeases}
|
||||
isModalOpen={dhcp.isModalOpen}
|
||||
addStaticLease={this.props.addStaticLease}
|
||||
removeStaticLease={this.props.removeStaticLease}
|
||||
toggleLeaseModal={this.props.toggleLeaseModal}
|
||||
processingAdding={dhcp.processingAdding}
|
||||
processingDeleting={dhcp.processingDeleting}
|
||||
/>
|
||||
</div>
|
||||
<div className="col-12">
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-success btn-standard mt-3"
|
||||
onClick={() => this.props.toggleLeaseModal()}
|
||||
>
|
||||
<Trans>dhcp_add_static_lease</Trans>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
</Fragment>
|
||||
)}
|
||||
</Fragment>
|
||||
)}
|
||||
</Fragment>
|
||||
);
|
||||
}
|
||||
@@ -230,7 +265,10 @@ Dhcp.propTypes = {
|
||||
getDhcpStatus: PropTypes.func,
|
||||
setDhcpConfig: PropTypes.func,
|
||||
findActiveDhcp: PropTypes.func,
|
||||
handleSubmit: PropTypes.func,
|
||||
addStaticLease: PropTypes.func,
|
||||
removeStaticLease: PropTypes.func,
|
||||
toggleLeaseModal: PropTypes.func,
|
||||
getDhcpInterfaces: PropTypes.func,
|
||||
t: PropTypes.func,
|
||||
};
|
||||
|
||||
|
||||
80
client/src/components/Settings/Dns/Access/Form.js
Normal file
80
client/src/components/Settings/Dns/Access/Form.js
Normal file
@@ -0,0 +1,80 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { Field, reduxForm } from 'redux-form';
|
||||
import { Trans, withNamespaces } from 'react-i18next';
|
||||
import flow from 'lodash/flow';
|
||||
|
||||
const Form = (props) => {
|
||||
const { handleSubmit, submitting, invalid } = props;
|
||||
|
||||
return (
|
||||
<form onSubmit={handleSubmit}>
|
||||
<div className="form__group mb-5">
|
||||
<label className="form__label form__label--with-desc" htmlFor="allowed_clients">
|
||||
<Trans>access_allowed_title</Trans>
|
||||
</label>
|
||||
<div className="form__desc form__desc--top">
|
||||
<Trans>access_allowed_desc</Trans>
|
||||
</div>
|
||||
<Field
|
||||
id="allowed_clients"
|
||||
name="allowed_clients"
|
||||
component="textarea"
|
||||
type="text"
|
||||
className="form-control form-control--textarea"
|
||||
/>
|
||||
</div>
|
||||
<div className="form__group mb-5">
|
||||
<label className="form__label form__label--with-desc" htmlFor="disallowed_clients">
|
||||
<Trans>access_disallowed_title</Trans>
|
||||
</label>
|
||||
<div className="form__desc form__desc--top">
|
||||
<Trans>access_disallowed_desc</Trans>
|
||||
</div>
|
||||
<Field
|
||||
id="disallowed_clients"
|
||||
name="disallowed_clients"
|
||||
component="textarea"
|
||||
type="text"
|
||||
className="form-control form-control--textarea"
|
||||
/>
|
||||
</div>
|
||||
<div className="form__group mb-5">
|
||||
<label className="form__label form__label--with-desc" htmlFor="blocked_hosts">
|
||||
<Trans>access_blocked_title</Trans>
|
||||
</label>
|
||||
<div className="form__desc form__desc--top">
|
||||
<Trans>access_blocked_desc</Trans>
|
||||
</div>
|
||||
<Field
|
||||
id="blocked_hosts"
|
||||
name="blocked_hosts"
|
||||
component="textarea"
|
||||
type="text"
|
||||
className="form-control form-control--textarea"
|
||||
/>
|
||||
</div>
|
||||
<div className="card-actions">
|
||||
<div className="btn-list">
|
||||
<button
|
||||
type="submit"
|
||||
className="btn btn-success btn-standard"
|
||||
disabled={submitting || invalid}
|
||||
>
|
||||
<Trans>save_config</Trans>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
);
|
||||
};
|
||||
|
||||
Form.propTypes = {
|
||||
handleSubmit: PropTypes.func,
|
||||
submitting: PropTypes.bool,
|
||||
invalid: PropTypes.bool,
|
||||
initialValues: PropTypes.object,
|
||||
t: PropTypes.func,
|
||||
};
|
||||
|
||||
export default flow([withNamespaces(), reduxForm({ form: 'accessForm' })])(Form);
|
||||
43
client/src/components/Settings/Dns/Access/index.js
Normal file
43
client/src/components/Settings/Dns/Access/index.js
Normal file
@@ -0,0 +1,43 @@
|
||||
import React, { Component } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { withNamespaces } from 'react-i18next';
|
||||
|
||||
import Form from './Form';
|
||||
import Card from '../../../ui/Card';
|
||||
|
||||
class Access extends Component {
|
||||
handleFormSubmit = (values) => {
|
||||
this.props.setAccessList(values);
|
||||
};
|
||||
|
||||
render() {
|
||||
const { t, access } = this.props;
|
||||
|
||||
const {
|
||||
processing,
|
||||
processingSet,
|
||||
...values
|
||||
} = access;
|
||||
|
||||
return (
|
||||
<Card
|
||||
title={t('access_title')}
|
||||
subtitle={t('access_desc')}
|
||||
bodyType="card-body box-body--settings"
|
||||
>
|
||||
<Form
|
||||
initialValues={values}
|
||||
onSubmit={this.handleFormSubmit}
|
||||
/>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Access.propTypes = {
|
||||
access: PropTypes.object.isRequired,
|
||||
setAccessList: PropTypes.func.isRequired,
|
||||
t: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
export default withNamespaces()(Access);
|
||||
@@ -4,17 +4,38 @@ import { Trans, withNamespaces } from 'react-i18next';
|
||||
|
||||
const Examples = props => (
|
||||
<div className="list leading-loose">
|
||||
<p>
|
||||
<Trans
|
||||
components={[
|
||||
<a
|
||||
href="https://kb.adguard.com/general/dns-providers"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
key="0"
|
||||
>
|
||||
DNS providers
|
||||
</a>,
|
||||
]}
|
||||
>
|
||||
dns_providers
|
||||
</Trans>
|
||||
</p>
|
||||
<Trans>examples_title</Trans>:
|
||||
<ol className="leading-loose">
|
||||
<li>
|
||||
<code>1.1.1.1</code> - { props.t('example_upstream_regular') }
|
||||
<code>1.1.1.1</code> - {props.t('example_upstream_regular')}
|
||||
</li>
|
||||
<li>
|
||||
<code>tls://1dot1dot1dot1.cloudflare-dns.com</code> –
|
||||
<span>
|
||||
<Trans
|
||||
components={[
|
||||
<a href="https://en.wikipedia.org/wiki/DNS_over_TLS" target="_blank" rel="noopener noreferrer" key="0">
|
||||
<a
|
||||
href="https://en.wikipedia.org/wiki/DNS_over_TLS"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
key="0"
|
||||
>
|
||||
DNS-over-TLS
|
||||
</a>,
|
||||
]}
|
||||
@@ -28,7 +49,12 @@ const Examples = props => (
|
||||
<span>
|
||||
<Trans
|
||||
components={[
|
||||
<a href="https://en.wikipedia.org/wiki/DNS_over_HTTPS" target="_blank" rel="noopener noreferrer" key="0">
|
||||
<a
|
||||
href="https://en.wikipedia.org/wiki/DNS_over_HTTPS"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
key="0"
|
||||
>
|
||||
DNS-over-HTTPS
|
||||
</a>,
|
||||
]}
|
||||
@@ -45,13 +71,28 @@ const Examples = props => (
|
||||
<span>
|
||||
<Trans
|
||||
components={[
|
||||
<a href="https://dnscrypt.info/stamps/" target="_blank" rel="noopener noreferrer" key="0">
|
||||
<a
|
||||
href="https://dnscrypt.info/stamps/"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
key="0"
|
||||
>
|
||||
DNS Stamps
|
||||
</a>,
|
||||
<a href="https://dnscrypt.info/" target="_blank" rel="noopener noreferrer" key="1">
|
||||
<a
|
||||
href="https://dnscrypt.info/"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
key="1"
|
||||
>
|
||||
DNSCrypt
|
||||
</a>,
|
||||
<a href="https://en.wikipedia.org/wiki/DNS_over_HTTPS" target="_blank" rel="noopener noreferrer" key="2">
|
||||
<a
|
||||
href="https://en.wikipedia.org/wiki/DNS_over_HTTPS"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
key="2"
|
||||
>
|
||||
DNS-over-HTTPS
|
||||
</a>,
|
||||
]}
|
||||
@@ -65,7 +106,12 @@ const Examples = props => (
|
||||
<span>
|
||||
<Trans
|
||||
components={[
|
||||
<a href="https://github.com/AdguardTeam/AdGuardHome/wiki/Configuration#upstreams-for-domains" target="_blank" rel="noopener noreferrer" key="0">
|
||||
<a
|
||||
href="https://github.com/AdguardTeam/AdGuardHome/wiki/Configuration#upstreams-for-domains"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
key="0"
|
||||
>
|
||||
Link
|
||||
</a>,
|
||||
]}
|
||||
@@ -6,7 +6,7 @@ import { Trans, withNamespaces } from 'react-i18next';
|
||||
import flow from 'lodash/flow';
|
||||
import classnames from 'classnames';
|
||||
|
||||
import { renderSelectField } from '../../../helpers/form';
|
||||
import { renderSelectField } from '../../../../helpers/form';
|
||||
import Examples from './Examples';
|
||||
|
||||
let Form = (props) => {
|
||||
@@ -58,11 +58,11 @@ let Form = (props) => {
|
||||
</div>
|
||||
<div className="col-12">
|
||||
<Examples />
|
||||
<hr/>
|
||||
<hr />
|
||||
</div>
|
||||
<div className="col-12">
|
||||
<div className="form__group">
|
||||
<label className="form__label" htmlFor="bootstrap_dns">
|
||||
<label className="form__label form__label--with-desc" htmlFor="bootstrap_dns">
|
||||
<Trans>bootstrap_dns</Trans>
|
||||
</label>
|
||||
<div className="form__desc form__desc--top">
|
||||
@@ -84,11 +84,13 @@ let Form = (props) => {
|
||||
<button
|
||||
type="button"
|
||||
className={testButtonClass}
|
||||
onClick={() => testUpstream({
|
||||
upstream_dns: upstreamDns,
|
||||
bootstrap_dns: bootstrapDns,
|
||||
all_servers: allServers,
|
||||
})}
|
||||
onClick={() =>
|
||||
testUpstream({
|
||||
upstream_dns: upstreamDns,
|
||||
bootstrap_dns: bootstrapDns,
|
||||
all_servers: allServers,
|
||||
})
|
||||
}
|
||||
disabled={!upstreamDns || processingTestUpstream}
|
||||
>
|
||||
<Trans>test_upstream_btn</Trans>
|
||||
@@ -97,10 +99,7 @@ let Form = (props) => {
|
||||
type="submit"
|
||||
className="btn btn-success btn-standard"
|
||||
disabled={
|
||||
submitting
|
||||
|| invalid
|
||||
|| processingSetUpstream
|
||||
|| processingTestUpstream
|
||||
submitting || invalid || processingSetUpstream || processingTestUpstream
|
||||
}
|
||||
>
|
||||
<Trans>apply_btn</Trans>
|
||||
@@ -140,5 +139,7 @@ Form = connect((state) => {
|
||||
|
||||
export default flow([
|
||||
withNamespaces(),
|
||||
reduxForm({ form: 'upstreamForm' }),
|
||||
reduxForm({
|
||||
form: 'upstreamForm',
|
||||
}),
|
||||
])(Form);
|
||||
@@ -3,7 +3,7 @@ import PropTypes from 'prop-types';
|
||||
import { withNamespaces } from 'react-i18next';
|
||||
|
||||
import Form from './Form';
|
||||
import Card from '../../ui/Card';
|
||||
import Card from '../../../ui/Card';
|
||||
|
||||
class Upstream extends Component {
|
||||
handleSubmit = (values) => {
|
||||
@@ -12,7 +12,7 @@ class Upstream extends Component {
|
||||
|
||||
handleTest = (values) => {
|
||||
this.props.testUpstream(values);
|
||||
}
|
||||
};
|
||||
|
||||
render() {
|
||||
const {
|
||||
@@ -26,8 +26,8 @@ class Upstream extends Component {
|
||||
|
||||
return (
|
||||
<Card
|
||||
title={ t('upstream_dns') }
|
||||
subtitle={ t('upstream_dns_hint') }
|
||||
title={t('upstream_dns')}
|
||||
subtitle={t('upstream_dns_hint')}
|
||||
bodyType="card-body box-body--settings"
|
||||
>
|
||||
<div className="row">
|
||||
60
client/src/components/Settings/Dns/index.js
Normal file
60
client/src/components/Settings/Dns/index.js
Normal file
@@ -0,0 +1,60 @@
|
||||
import React, { Component, Fragment } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { withNamespaces } from 'react-i18next';
|
||||
|
||||
import Upstream from './Upstream';
|
||||
import Access from './Access';
|
||||
import PageTitle from '../../ui/PageTitle';
|
||||
import Loading from '../../ui/Loading';
|
||||
|
||||
class Dns extends Component {
|
||||
componentDidMount() {
|
||||
this.props.getAccessList();
|
||||
}
|
||||
|
||||
render() {
|
||||
const {
|
||||
t,
|
||||
dashboard,
|
||||
settings,
|
||||
access,
|
||||
setAccessList,
|
||||
testUpstream,
|
||||
setUpstream,
|
||||
} = this.props;
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
<PageTitle title={t('dns_settings')} />
|
||||
{(dashboard.processing || access.processing) && <Loading />}
|
||||
{!dashboard.processing && !access.processing && (
|
||||
<Fragment>
|
||||
<Upstream
|
||||
upstreamDns={dashboard.upstreamDns}
|
||||
bootstrapDns={dashboard.bootstrapDns}
|
||||
allServers={dashboard.allServers}
|
||||
processingTestUpstream={settings.processingTestUpstream}
|
||||
processingSetUpstream={settings.processingSetUpstream}
|
||||
setUpstream={setUpstream}
|
||||
testUpstream={testUpstream}
|
||||
/>
|
||||
<Access access={access} setAccessList={setAccessList} />
|
||||
</Fragment>
|
||||
)}
|
||||
</Fragment>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Dns.propTypes = {
|
||||
dashboard: PropTypes.object.isRequired,
|
||||
settings: PropTypes.object.isRequired,
|
||||
setUpstream: PropTypes.func.isRequired,
|
||||
testUpstream: PropTypes.func.isRequired,
|
||||
getAccessList: PropTypes.func.isRequired,
|
||||
setAccessList: PropTypes.func.isRequired,
|
||||
access: PropTypes.object.isRequired,
|
||||
t: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
export default withNamespaces()(Dns);
|
||||
@@ -66,14 +66,15 @@ let Form = (props) => {
|
||||
setTlsConfig,
|
||||
} = props;
|
||||
|
||||
const isSavingDisabled = invalid
|
||||
|| submitting
|
||||
|| processingConfig
|
||||
|| processingValidate
|
||||
|| (isEnabled && (!privateKey || !certificateChain))
|
||||
|| (privateKey && !valid_key)
|
||||
|| (certificateChain && !valid_cert)
|
||||
|| (privateKey && certificateChain && !valid_pair);
|
||||
const isSavingDisabled =
|
||||
invalid ||
|
||||
submitting ||
|
||||
processingConfig ||
|
||||
processingValidate ||
|
||||
(isEnabled && (!privateKey || !certificateChain)) ||
|
||||
(privateKey && !valid_key) ||
|
||||
(certificateChain && !valid_cert) ||
|
||||
(privateKey && certificateChain && !valid_pair);
|
||||
|
||||
return (
|
||||
<form onSubmit={handleSubmit}>
|
||||
@@ -91,7 +92,7 @@ let Form = (props) => {
|
||||
<div className="form__desc">
|
||||
<Trans>encryption_enable_desc</Trans>
|
||||
</div>
|
||||
<hr/>
|
||||
<hr />
|
||||
</div>
|
||||
<div className="col-12">
|
||||
<label className="form__label" htmlFor="server_name">
|
||||
@@ -180,13 +181,20 @@ let Form = (props) => {
|
||||
<div className="row">
|
||||
<div className="col-12">
|
||||
<div className="form__group form__group--settings">
|
||||
<label className="form__label form__label--bold" htmlFor="certificate_chain">
|
||||
<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>]}
|
||||
components={[
|
||||
<a href="https://letsencrypt.org/" key="0">
|
||||
link
|
||||
</a>,
|
||||
]}
|
||||
>
|
||||
encryption_certificates_desc
|
||||
</Trans>
|
||||
@@ -202,49 +210,52 @@ let Form = (props) => {
|
||||
disabled={!isEnabled}
|
||||
/>
|
||||
<div className="form__status">
|
||||
{certificateChain &&
|
||||
{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 ?
|
||||
<li
|
||||
className={valid_chain ? 'text-success' : 'text-danger'}
|
||||
>
|
||||
{valid_chain ? (
|
||||
<Trans>encryption_chain_valid</Trans>
|
||||
: <Trans>encryption_chain_invalid</Trans>
|
||||
}
|
||||
) : (
|
||||
<Trans>encryption_chain_invalid</Trans>
|
||||
)}
|
||||
</li>
|
||||
{valid_cert &&
|
||||
{valid_cert && (
|
||||
<Fragment>
|
||||
{subject &&
|
||||
{subject && (
|
||||
<li>
|
||||
<Trans>encryption_subject</Trans>:
|
||||
{subject}
|
||||
</li>
|
||||
}
|
||||
{issuer &&
|
||||
)}
|
||||
{issuer && (
|
||||
<li>
|
||||
<Trans>encryption_issuer</Trans>:
|
||||
{issuer}
|
||||
</li>
|
||||
}
|
||||
{not_after && not_after !== EMPTY_DATE &&
|
||||
)}
|
||||
{not_after && not_after !== EMPTY_DATE && (
|
||||
<li>
|
||||
<Trans>encryption_expire</Trans>:
|
||||
{format(not_after, 'YYYY-MM-DD HH:mm:ss')}
|
||||
</li>
|
||||
}
|
||||
{dns_names &&
|
||||
)}
|
||||
{dns_names && (
|
||||
<li>
|
||||
<Trans>encryption_hostnames</Trans>:
|
||||
{dns_names}
|
||||
</li>
|
||||
}
|
||||
)}
|
||||
</Fragment>
|
||||
}
|
||||
)}
|
||||
</ul>
|
||||
</Fragment>
|
||||
}
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -266,35 +277,34 @@ let Form = (props) => {
|
||||
disabled={!isEnabled}
|
||||
/>
|
||||
<div className="form__status">
|
||||
{privateKey &&
|
||||
{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 ?
|
||||
{valid_key ? (
|
||||
<Trans values={{ type: key_type }}>
|
||||
encryption_key_valid
|
||||
</Trans>
|
||||
: <Trans values={{ type: key_type }}>
|
||||
) : (
|
||||
<Trans values={{ type: key_type }}>
|
||||
encryption_key_invalid
|
||||
</Trans>
|
||||
}
|
||||
)}
|
||||
</li>
|
||||
</ul>
|
||||
</Fragment>
|
||||
}
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{warning_validation &&
|
||||
{warning_validation && (
|
||||
<div className="col-12">
|
||||
<p className="text-danger">
|
||||
{warning_validation}
|
||||
</p>
|
||||
<p className="text-danger">{warning_validation}</p>
|
||||
</div>
|
||||
}
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="btn-list mt-2">
|
||||
|
||||
@@ -6,11 +6,17 @@ import debounce from 'lodash/debounce';
|
||||
import { DEBOUNCE_TIMEOUT } from '../../../helpers/constants';
|
||||
import Form from './Form';
|
||||
import Card from '../../ui/Card';
|
||||
import PageTitle from '../../ui/PageTitle';
|
||||
import Loading from '../../ui/Loading';
|
||||
|
||||
class Encryption extends Component {
|
||||
componentDidMount() {
|
||||
if (this.props.encryption.enabled) {
|
||||
this.props.validateTlsConfig(this.props.encryption);
|
||||
const { getTlsStatus, validateTlsConfig, encryption } = this.props;
|
||||
|
||||
getTlsStatus();
|
||||
|
||||
if (encryption.enabled) {
|
||||
validateTlsConfig(encryption);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -36,7 +42,9 @@ class Encryption extends Component {
|
||||
|
||||
return (
|
||||
<div className="encryption">
|
||||
{encryption &&
|
||||
<PageTitle title={t('encryption_settings')} />
|
||||
{encryption.processing && <Loading />}
|
||||
{!encryption.processing && (
|
||||
<Card
|
||||
title={t('encryption_title')}
|
||||
subtitle={t('encryption_desc')}
|
||||
@@ -58,7 +66,7 @@ class Encryption extends Component {
|
||||
{...this.props.encryption}
|
||||
/>
|
||||
</Card>
|
||||
}
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -63,6 +63,10 @@
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.form__label--with-desc {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.form__status {
|
||||
margin-top: 10px;
|
||||
font-size: 14px;
|
||||
@@ -76,3 +80,11 @@
|
||||
.encryption__list li {
|
||||
list-style: inside;
|
||||
}
|
||||
|
||||
.btn-icon {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
}
|
||||
|
||||
@@ -1,13 +1,12 @@
|
||||
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';
|
||||
import Card from '../ui/Card';
|
||||
|
||||
import './Settings.css';
|
||||
|
||||
class Settings extends Component {
|
||||
@@ -36,9 +35,6 @@ class Settings extends Component {
|
||||
|
||||
componentDidMount() {
|
||||
this.props.initSettings(this.settings);
|
||||
this.props.getDhcpStatus();
|
||||
this.props.getDhcpInterfaces();
|
||||
this.props.getTlsStatus();
|
||||
}
|
||||
|
||||
renderSettings = (settings) => {
|
||||
@@ -46,58 +42,41 @@ class Settings extends Component {
|
||||
return Object.keys(settings).map((key) => {
|
||||
const setting = settings[key];
|
||||
const { enabled } = setting;
|
||||
return (<Checkbox
|
||||
key={key}
|
||||
{...settings[key]}
|
||||
handleChange={() => this.props.toggleSetting(key, enabled)}
|
||||
/>);
|
||||
return (
|
||||
<Checkbox
|
||||
key={key}
|
||||
{...settings[key]}
|
||||
handleChange={() => this.props.toggleSetting(key, enabled)}
|
||||
/>
|
||||
);
|
||||
});
|
||||
}
|
||||
return (
|
||||
<div><Trans>no_settings</Trans></div>
|
||||
<div>
|
||||
<Trans>no_settings</Trans>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
render() {
|
||||
const { settings, dashboard, t } = this.props;
|
||||
const { settings, t } = this.props;
|
||||
return (
|
||||
<Fragment>
|
||||
<PageTitle title={ t('settings') } />
|
||||
<PageTitle title={t('general_settings')} />
|
||||
{settings.processing && <Loading />}
|
||||
{!settings.processing &&
|
||||
{!settings.processing && (
|
||||
<div className="content">
|
||||
<div className="row">
|
||||
<div className="col-md-12">
|
||||
<Card title={ t('general_settings') } bodyType="card-body box-body--settings">
|
||||
<Card bodyType="card-body box-body--settings">
|
||||
<div className="form">
|
||||
{this.renderSettings(settings.settingsList)}
|
||||
</div>
|
||||
</Card>
|
||||
<Upstream
|
||||
upstreamDns={dashboard.upstreamDns}
|
||||
bootstrapDns={dashboard.bootstrapDns}
|
||||
allServers={dashboard.allServers}
|
||||
setUpstream={this.props.setUpstream}
|
||||
testUpstream={this.props.testUpstream}
|
||||
processingTestUpstream={settings.processingTestUpstream}
|
||||
processingSetUpstream={settings.processingSetUpstream}
|
||||
/>
|
||||
<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>
|
||||
}
|
||||
)}
|
||||
</Fragment>
|
||||
);
|
||||
}
|
||||
@@ -108,8 +87,6 @@ Settings.propTypes = {
|
||||
settings: PropTypes.object,
|
||||
settingsList: PropTypes.object,
|
||||
toggleSetting: PropTypes.func,
|
||||
handleUpstreamChange: PropTypes.func,
|
||||
setUpstream: PropTypes.func,
|
||||
t: PropTypes.func,
|
||||
};
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
position: fixed;
|
||||
right: 24px;
|
||||
bottom: 24px;
|
||||
z-index: 103;
|
||||
z-index: 105;
|
||||
width: 345px;
|
||||
}
|
||||
|
||||
@@ -32,6 +32,12 @@
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.toast__content a {
|
||||
font-weight: 600;
|
||||
color: #fff;
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.toast__dismiss {
|
||||
display: block;
|
||||
flex: 0 0 auto;
|
||||
|
||||
@@ -4,7 +4,7 @@ import { Trans, withNamespaces } from 'react-i18next';
|
||||
|
||||
class Toast extends Component {
|
||||
componentDidMount() {
|
||||
const timeout = this.props.type === 'error' ? 30000 : 5000;
|
||||
const timeout = this.props.type === 'success' ? 5000 : 30000;
|
||||
|
||||
setTimeout(() => {
|
||||
this.props.removeToast(this.props.id);
|
||||
@@ -15,13 +15,25 @@ class Toast extends Component {
|
||||
return false;
|
||||
}
|
||||
|
||||
showMessage(t, type, message) {
|
||||
if (type === 'notice') {
|
||||
return <span dangerouslySetInnerHTML={{ __html: t(message) }} />;
|
||||
}
|
||||
|
||||
return <Trans>{message}</Trans>;
|
||||
}
|
||||
|
||||
render() {
|
||||
const {
|
||||
type, id, t, message,
|
||||
} = this.props;
|
||||
|
||||
return (
|
||||
<div className={`toast toast--${this.props.type}`}>
|
||||
<div className={`toast toast--${type}`}>
|
||||
<p className="toast__content">
|
||||
<Trans>{this.props.message}</Trans>
|
||||
{this.showMessage(t, type, message)}
|
||||
</p>
|
||||
<button className="toast__dismiss" onClick={() => this.props.removeToast(this.props.id)}>
|
||||
<button className="toast__dismiss" onClick={() => this.props.removeToast(id)}>
|
||||
<svg stroke="#fff" fill="none" width="20" height="20" strokeWidth="2" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path d="m18 6-12 12"/><path d="m6 6 12 12"/></svg>
|
||||
</button>
|
||||
</div>
|
||||
@@ -30,6 +42,7 @@ class Toast extends Component {
|
||||
}
|
||||
|
||||
Toast.propTypes = {
|
||||
t: PropTypes.func.isRequired,
|
||||
id: PropTypes.string.isRequired,
|
||||
message: PropTypes.string.isRequired,
|
||||
type: PropTypes.string.isRequired,
|
||||
|
||||
8
client/src/components/ui/Dropdown.css
Normal file
8
client/src/components/ui/Dropdown.css
Normal file
@@ -0,0 +1,8 @@
|
||||
.dropdown-item.active,
|
||||
.dropdown-item:active {
|
||||
background-color: #66b574;
|
||||
}
|
||||
|
||||
.dropdown-menu {
|
||||
cursor: default;
|
||||
}
|
||||
89
client/src/components/ui/Dropdown.js
Normal file
89
client/src/components/ui/Dropdown.js
Normal file
@@ -0,0 +1,89 @@
|
||||
import React, { Component } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import classnames from 'classnames';
|
||||
import { withNamespaces } from 'react-i18next';
|
||||
import enhanceWithClickOutside from 'react-click-outside';
|
||||
|
||||
import './Dropdown.css';
|
||||
|
||||
class Dropdown extends Component {
|
||||
state = {
|
||||
isOpen: false,
|
||||
};
|
||||
|
||||
toggleDropdown = () => {
|
||||
this.setState(prevState => ({ isOpen: !prevState.isOpen }));
|
||||
};
|
||||
|
||||
hideDropdown = () => {
|
||||
this.setState({ isOpen: false });
|
||||
};
|
||||
|
||||
handleClickOutside = () => {
|
||||
if (this.state.isOpen) {
|
||||
this.hideDropdown();
|
||||
}
|
||||
};
|
||||
|
||||
render() {
|
||||
const {
|
||||
label,
|
||||
controlClassName,
|
||||
menuClassName,
|
||||
baseClassName,
|
||||
icon,
|
||||
children,
|
||||
} = this.props;
|
||||
|
||||
const { isOpen } = this.state;
|
||||
|
||||
const dropdownClass = classnames({
|
||||
[baseClassName]: true,
|
||||
show: isOpen,
|
||||
});
|
||||
|
||||
const dropdownMenuClass = classnames({
|
||||
[menuClassName]: true,
|
||||
show: isOpen,
|
||||
});
|
||||
|
||||
const ariaSettings = isOpen ? 'true' : 'false';
|
||||
|
||||
return (
|
||||
<div className={dropdownClass}>
|
||||
<a
|
||||
className={controlClassName}
|
||||
aria-expanded={ariaSettings}
|
||||
onClick={this.toggleDropdown}
|
||||
>
|
||||
{icon && (
|
||||
<svg className="nav-icon">
|
||||
<use xlinkHref={`#${icon}`} />
|
||||
</svg>
|
||||
)}
|
||||
{label}
|
||||
</a>
|
||||
<div className={dropdownMenuClass} onClick={this.hideDropdown}>
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Dropdown.defaultProps = {
|
||||
baseClassName: 'dropdown',
|
||||
menuClassName: 'dropdown-menu dropdown-menu-arrow',
|
||||
controlClassName: '',
|
||||
};
|
||||
|
||||
Dropdown.propTypes = {
|
||||
label: PropTypes.string.isRequired,
|
||||
children: PropTypes.node.isRequired,
|
||||
controlClassName: PropTypes.node.isRequired,
|
||||
menuClassName: PropTypes.string.isRequired,
|
||||
baseClassName: PropTypes.string.isRequired,
|
||||
icon: PropTypes.string,
|
||||
};
|
||||
|
||||
export default withNamespaces()(enhanceWithClickOutside(Dropdown));
|
||||
5
client/src/components/ui/Icons.css
Normal file
5
client/src/components/ui/Icons.css
Normal file
@@ -0,0 +1,5 @@
|
||||
.icons {
|
||||
display: inline-block;
|
||||
vertical-align: middle;
|
||||
height: 100%;
|
||||
}
|
||||
@@ -1,5 +1,7 @@
|
||||
import React from 'react';
|
||||
|
||||
import './Icons.css';
|
||||
|
||||
const Icons = () => (
|
||||
<svg xmlns="http://www.w3.org/2000/svg" className="hidden">
|
||||
<symbol id="android" viewBox="0 0 14 16" fill="currentColor">
|
||||
@@ -21,6 +23,38 @@ const Icons = () => (
|
||||
<symbol id="router" viewBox="0 0 30 30" fill="currentColor">
|
||||
<path d="M17.646 2.332a1 1 0 0 0-.697 1.719 6.984 6.984 0 0 1 0 9.898 1 1 0 1 0 1.414 1.414c3.507-3.506 3.507-9.22 0-12.726a1 1 0 0 0-.717-.305zm-12.662.654A1 1 0 0 0 4 4v14a2 2 0 0 0-2 2v4a2 2 0 0 0 2 2h22a2 2 0 0 0 2-2v-4a2 2 0 0 0-2-2H12V9a1 1 0 0 0-1.016-1.014A1 1 0 0 0 10 9v9H6V4a1 1 0 0 0-1.016-1.014zm9.834 2.176a1 1 0 0 0-.697 1.717 2.985 2.985 0 0 1 0 4.242 1 1 0 1 0 1.414 1.414 5.014 5.014 0 0 0 0-7.07 1 1 0 0 0-.717-.303zM5 21a1 1 0 1 1 0 2 1 1 0 0 1 0-2zm4 0a1 1 0 1 1 0 2 1 1 0 0 1 0-2z" />
|
||||
</symbol>
|
||||
|
||||
<symbol id="edit" viewBox="0 0 24 24" stroke="currentColor" fill="none" strokeLinecap="round" strokeLinejoin="round" strokeWidth="2">
|
||||
<path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"/><path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z"/>
|
||||
</symbol>
|
||||
|
||||
<symbol id="delete" viewBox="0 0 24 24" stroke="currentColor" fill="none" strokeLinecap="round" strokeLinejoin="round" strokeWidth="2">
|
||||
<path d="m3 6h2 16"/><path d="m19 6v14a2 2 0 0 1 -2 2h-10a2 2 0 0 1 -2-2v-14m3 0v-2a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"/><path d="m10 11v6"/><path d="m14 11v6"/>
|
||||
</symbol>
|
||||
|
||||
<symbol id="back" viewBox="0 0 24 24" stroke="currentColor" fill="none" strokeLinecap="round" strokeLinejoin="round" strokeWidth="2">
|
||||
<path d="m19 12h-14"/><path d="m12 19-7-7 7-7"/>
|
||||
</symbol>
|
||||
|
||||
<symbol id="dashboard" viewBox="0 0 24 24" stroke="currentColor" fill="none" strokeLinecap="round" strokeLinejoin="round" strokeWidth="2">
|
||||
<path d="m3 9 9-7 9 7v11a2 2 0 0 1 -2 2h-14a2 2 0 0 1 -2-2z"/><path d="m9 22v-10h6v10"/>
|
||||
</symbol>
|
||||
|
||||
<symbol id="filters" viewBox="0 0 24 24" stroke="currentColor" fill="none" strokeLinecap="round" strokeLinejoin="round" strokeWidth="2">
|
||||
<path d="m22 3h-20l8 9.46v6.54l4 2v-8.54z"/>
|
||||
</symbol>
|
||||
|
||||
<symbol id="log" viewBox="0 0 24 24" stroke="currentColor" fill="none" strokeLinecap="round" strokeLinejoin="round" strokeWidth="2">
|
||||
<path d="m14 2h-8a2 2 0 0 0 -2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2v-12z"/><path d="m14 2v6h6"/><path d="m16 13h-8"/><path d="m16 17h-8"/><path d="m10 9h-1-1"/>
|
||||
</symbol>
|
||||
|
||||
<symbol id="setup" viewBox="0 0 24 24" stroke="currentColor" fill="none" strokeLinecap="round" strokeLinejoin="round" strokeWidth="2">
|
||||
<circle cx="12" cy="12" r="10"></circle><path d="M9.09 9a3 3 0 0 1 5.83 1c0 2-3 3-3 3"></path><line x1="12" y1="17" x2="12" y2="17"></line>
|
||||
</symbol>
|
||||
|
||||
<symbol id="settings" viewBox="0 0 24 24" stroke="currentColor" fill="none" strokeLinecap="round" strokeLinejoin="round" strokeWidth="2">
|
||||
<circle cx="12" cy="12" r="3"/><path d="m19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1 0 2.83 2 2 0 0 1 -2.83 0l-.06-.06a1.65 1.65 0 0 0 -1.82-.33 1.65 1.65 0 0 0 -1 1.51v.17a2 2 0 0 1 -2 2 2 2 0 0 1 -2-2v-.09a1.65 1.65 0 0 0 -1.08-1.51 1.65 1.65 0 0 0 -1.82.33l-.06.06a2 2 0 0 1 -2.83 0 2 2 0 0 1 0-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0 -1.51-1h-.17a2 2 0 0 1 -2-2 2 2 0 0 1 2-2h.09a1.65 1.65 0 0 0 1.51-1.08 1.65 1.65 0 0 0 -.33-1.82l-.06-.06a2 2 0 0 1 0-2.83 2 2 0 0 1 2.83 0l.06.06a1.65 1.65 0 0 0 1.82.33h.08a1.65 1.65 0 0 0 1-1.51v-.17a2 2 0 0 1 2-2 2 2 0 0 1 2 2v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 0 2 2 0 0 1 0 2.83l-.06.06a1.65 1.65 0 0 0 -.33 1.82v.08a1.65 1.65 0 0 0 1.51 1h.17a2 2 0 0 1 2 2 2 2 0 0 1 -2 2h-.09a1.65 1.65 0 0 0 -1.51 1z"/>
|
||||
</symbol>
|
||||
</svg>
|
||||
);
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
overflow-x: hidden;
|
||||
overflow-y: auto;
|
||||
background-color: rgba(0, 0, 0, 0.5);
|
||||
z-index: 1;
|
||||
z-index: 104;
|
||||
}
|
||||
|
||||
.ReactModal__Overlay--after-open {
|
||||
@@ -38,3 +38,9 @@
|
||||
border: none;
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
@media (min-width: 576px) {
|
||||
.modal-dialog--clients {
|
||||
max-width: 650px;
|
||||
}
|
||||
}
|
||||
|
||||
40
client/src/components/ui/Overlay.css
Normal file
40
client/src/components/ui/Overlay.css
Normal file
@@ -0,0 +1,40 @@
|
||||
.overlay {
|
||||
display: none;
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
z-index: 110;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 20px;
|
||||
font-size: 28px;
|
||||
font-weight: 600;
|
||||
text-align: center;
|
||||
background-color: rgba(255, 255, 255, 0.8);
|
||||
}
|
||||
|
||||
.overlay--visible {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.overlay__loading {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
margin-bottom: 20px;
|
||||
background-image: url("data:image/svg+xml;charset=utf-8,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20viewBox%3D%220%200%2047.6%2047.6%22%20height%3D%22100%25%22%20width%3D%22100%25%22%3E%3Cpath%20opacity%3D%22.235%22%20fill%3D%22%23979797%22%20d%3D%22M44.4%2011.9l-5.2%203c1.5%202.6%202.4%205.6%202.4%208.9%200%209.8-8%2017.8-17.8%2017.8-6.6%200-12.3-3.6-15.4-8.9l-5.2%203C7.3%2042.8%2015%2047.6%2023.8%2047.6c13.1%200%2023.8-10.7%2023.8-23.8%200-4.3-1.2-8.4-3.2-11.9z%22%2F%3E%3Cpath%20fill%3D%22%2366b574%22%20d%3D%22M3.2%2035.7C0%2030.2-.8%2023.8.8%2017.6%202.5%2011.5%206.4%206.4%2011.9%203.2%2017.4%200%2023.8-.8%2030%20.8c6.1%201.6%2011.3%205.6%2014.4%2011.1l-5.2%203c-2.4-4.1-6.2-7.1-10.8-8.3C23.8%205.4%2019%206%2014.9%208.4s-7.1%206.2-8.3%2010.8c-1.2%204.6-.6%209.4%201.8%2013.5l-5.2%203z%22%2F%3E%3C%2Fsvg%3E");
|
||||
will-change: transform;
|
||||
animation: clockwise 2s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes clockwise {
|
||||
0% {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
|
||||
100% {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
26
client/src/components/ui/UpdateOverlay.js
Normal file
26
client/src/components/ui/UpdateOverlay.js
Normal file
@@ -0,0 +1,26 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { Trans, withNamespaces } from 'react-i18next';
|
||||
import classnames from 'classnames';
|
||||
|
||||
import './Overlay.css';
|
||||
|
||||
const UpdateOverlay = (props) => {
|
||||
const overlayClass = classnames({
|
||||
overlay: true,
|
||||
'overlay--visible': props.processingUpdate,
|
||||
});
|
||||
|
||||
return (
|
||||
<div className={overlayClass}>
|
||||
<div className="overlay__loading"></div>
|
||||
<Trans>processing_update</Trans>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
UpdateOverlay.propTypes = {
|
||||
processingUpdate: PropTypes.bool,
|
||||
};
|
||||
|
||||
export default withNamespaces()(UpdateOverlay);
|
||||
@@ -1,4 +1,4 @@
|
||||
import React from 'react';
|
||||
import React, { Fragment } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { Trans, withNamespaces } from 'react-i18next';
|
||||
|
||||
@@ -6,22 +6,37 @@ 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>
|
||||
<Fragment>
|
||||
<Trans
|
||||
values={{ version: props.version }}
|
||||
components={[
|
||||
<a href={props.url} target="_blank" rel="noopener noreferrer" key="0">
|
||||
Click here
|
||||
</a>,
|
||||
]}
|
||||
>
|
||||
update_announcement
|
||||
</Trans>
|
||||
{props.canAutoUpdate &&
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-sm btn-primary ml-3"
|
||||
onClick={props.getUpdate}
|
||||
disabled={props.processingUpdate}
|
||||
>
|
||||
<Trans>update_now</Trans>
|
||||
</button>
|
||||
}
|
||||
</Fragment>
|
||||
</Topline>
|
||||
);
|
||||
|
||||
UpdateTopline.propTypes = {
|
||||
version: PropTypes.string.isRequired,
|
||||
version: PropTypes.string,
|
||||
url: PropTypes.string.isRequired,
|
||||
canAutoUpdate: PropTypes.bool,
|
||||
getUpdate: PropTypes.func,
|
||||
processingUpdate: PropTypes.bool,
|
||||
};
|
||||
|
||||
export default withNamespaces()(UpdateTopline);
|
||||
|
||||
26
client/src/containers/Clients.js
Normal file
26
client/src/containers/Clients.js
Normal file
@@ -0,0 +1,26 @@
|
||||
import { connect } from 'react-redux';
|
||||
import { getClients } from '../actions';
|
||||
import { addClient, updateClient, deleteClient, toggleClientModal } from '../actions/clients';
|
||||
import Clients from '../components/Settings/Clients';
|
||||
|
||||
const mapStateToProps = (state) => {
|
||||
const { dashboard, clients } = state;
|
||||
const props = {
|
||||
dashboard,
|
||||
clients,
|
||||
};
|
||||
return props;
|
||||
};
|
||||
|
||||
const mapDispatchToProps = {
|
||||
getClients,
|
||||
addClient,
|
||||
updateClient,
|
||||
deleteClient,
|
||||
toggleClientModal,
|
||||
};
|
||||
|
||||
export default connect(
|
||||
mapStateToProps,
|
||||
mapDispatchToProps,
|
||||
)(Clients);
|
||||
36
client/src/containers/Dhcp.js
Normal file
36
client/src/containers/Dhcp.js
Normal file
@@ -0,0 +1,36 @@
|
||||
import { connect } from 'react-redux';
|
||||
import {
|
||||
toggleDhcp,
|
||||
getDhcpStatus,
|
||||
getDhcpInterfaces,
|
||||
setDhcpConfig,
|
||||
findActiveDhcp,
|
||||
toggleLeaseModal,
|
||||
addStaticLease,
|
||||
removeStaticLease,
|
||||
} from '../actions';
|
||||
import Dhcp from '../components/Settings/Dhcp';
|
||||
|
||||
const mapStateToProps = (state) => {
|
||||
const { dhcp } = state;
|
||||
const props = {
|
||||
dhcp,
|
||||
};
|
||||
return props;
|
||||
};
|
||||
|
||||
const mapDispatchToProps = {
|
||||
toggleDhcp,
|
||||
getDhcpStatus,
|
||||
getDhcpInterfaces,
|
||||
setDhcpConfig,
|
||||
findActiveDhcp,
|
||||
toggleLeaseModal,
|
||||
addStaticLease,
|
||||
removeStaticLease,
|
||||
};
|
||||
|
||||
export default connect(
|
||||
mapStateToProps,
|
||||
mapDispatchToProps,
|
||||
)(Dhcp);
|
||||
27
client/src/containers/Dns.js
Normal file
27
client/src/containers/Dns.js
Normal file
@@ -0,0 +1,27 @@
|
||||
import { connect } from 'react-redux';
|
||||
import { handleUpstreamChange, setUpstream, testUpstream } from '../actions';
|
||||
import { getAccessList, setAccessList } from '../actions/access';
|
||||
import Dns from '../components/Settings/Dns';
|
||||
|
||||
const mapStateToProps = (state) => {
|
||||
const { dashboard, settings, access } = state;
|
||||
const props = {
|
||||
dashboard,
|
||||
settings,
|
||||
access,
|
||||
};
|
||||
return props;
|
||||
};
|
||||
|
||||
const mapDispatchToProps = {
|
||||
handleUpstreamChange,
|
||||
setUpstream,
|
||||
testUpstream,
|
||||
getAccessList,
|
||||
setAccessList,
|
||||
};
|
||||
|
||||
export default connect(
|
||||
mapStateToProps,
|
||||
mapDispatchToProps,
|
||||
)(Dns);
|
||||
22
client/src/containers/Encryption.js
Normal file
22
client/src/containers/Encryption.js
Normal file
@@ -0,0 +1,22 @@
|
||||
import { connect } from 'react-redux';
|
||||
import { getTlsStatus, setTlsConfig, validateTlsConfig } from '../actions/encryption';
|
||||
import Encryption from '../components/Settings/Encryption';
|
||||
|
||||
const mapStateToProps = (state) => {
|
||||
const { encryption } = state;
|
||||
const props = {
|
||||
encryption,
|
||||
};
|
||||
return props;
|
||||
};
|
||||
|
||||
const mapDispatchToProps = {
|
||||
getTlsStatus,
|
||||
setTlsConfig,
|
||||
validateTlsConfig,
|
||||
};
|
||||
|
||||
export default connect(
|
||||
mapStateToProps,
|
||||
mapDispatchToProps,
|
||||
)(Encryption);
|
||||
@@ -1,36 +1,11 @@
|
||||
import { connect } from 'react-redux';
|
||||
import {
|
||||
initSettings,
|
||||
toggleSetting,
|
||||
handleUpstreamChange,
|
||||
setUpstream,
|
||||
testUpstream,
|
||||
addErrorToast,
|
||||
toggleDhcp,
|
||||
getDhcpStatus,
|
||||
getDhcpInterfaces,
|
||||
setDhcpConfig,
|
||||
findActiveDhcp,
|
||||
} from '../actions';
|
||||
import {
|
||||
getTlsStatus,
|
||||
setTlsConfig,
|
||||
validateTlsConfig,
|
||||
} from '../actions/encryption';
|
||||
import { initSettings, toggleSetting } from '../actions';
|
||||
import Settings from '../components/Settings';
|
||||
|
||||
const mapStateToProps = (state) => {
|
||||
const {
|
||||
settings,
|
||||
dashboard,
|
||||
dhcp,
|
||||
encryption,
|
||||
} = state;
|
||||
const { settings } = state;
|
||||
const props = {
|
||||
settings,
|
||||
dashboard,
|
||||
dhcp,
|
||||
encryption,
|
||||
};
|
||||
return props;
|
||||
};
|
||||
@@ -38,18 +13,6 @@ const mapStateToProps = (state) => {
|
||||
const mapDispatchToProps = {
|
||||
initSettings,
|
||||
toggleSetting,
|
||||
handleUpstreamChange,
|
||||
setUpstream,
|
||||
testUpstream,
|
||||
addErrorToast,
|
||||
toggleDhcp,
|
||||
getDhcpStatus,
|
||||
getDhcpInterfaces,
|
||||
setDhcpConfig,
|
||||
findActiveDhcp,
|
||||
getTlsStatus,
|
||||
setTlsConfig,
|
||||
validateTlsConfig,
|
||||
};
|
||||
|
||||
export default connect(
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
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 R_MAC = /^((([a-fA-F0-9][a-fA-F0-9]+[-]){5}|([a-fA-F0-9][a-fA-F0-9]+[:]){5})([a-fA-F0-9][a-fA-F0-9])$)|(^([a-fA-F0-9][a-fA-F0-9][a-fA-F0-9][a-fA-F0-9]+[.]){2}([a-fA-F0-9][a-fA-F0-9][a-fA-F0-9][a-fA-F0-9]))$/g;
|
||||
|
||||
export const STATS_NAMES = {
|
||||
avg_processing_time: 'average_processing_time',
|
||||
@@ -19,7 +20,8 @@ export const STATUS_COLORS = {
|
||||
|
||||
export const REPOSITORY = {
|
||||
URL: 'https://github.com/AdguardTeam/AdGuardHome',
|
||||
TRACKERS_DB: 'https://github.com/AdguardTeam/AdGuardHome/tree/master/client/src/helpers/trackers/adguard.json',
|
||||
TRACKERS_DB:
|
||||
'https://github.com/AdguardTeam/AdGuardHome/tree/master/client/src/helpers/trackers/adguard.json',
|
||||
};
|
||||
|
||||
export const PRIVACY_POLICY_LINK = 'https://adguard.com/privacy/home.html';
|
||||
@@ -165,3 +167,15 @@ export const DHCP_STATUS_RESPONSE = {
|
||||
NO: 'no',
|
||||
ERROR: 'error',
|
||||
};
|
||||
|
||||
export const MODAL_TYPE = {
|
||||
ADD: 'add',
|
||||
EDIT: 'edit',
|
||||
};
|
||||
|
||||
export const CLIENT_ID = {
|
||||
MAC: 'mac',
|
||||
IP: 'ip',
|
||||
};
|
||||
|
||||
export const SETTINGS_URLS = ['/encryption', '/dhcp', '/dns', '/settings', '/clients'];
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import React, { Fragment } from 'react';
|
||||
import { Trans } from 'react-i18next';
|
||||
|
||||
import { R_IPV4, UNSAFE_PORTS } from '../helpers/constants';
|
||||
import { R_IPV4, R_MAC, UNSAFE_PORTS } from '../helpers/constants';
|
||||
|
||||
export const renderField = ({
|
||||
input, id, className, placeholder, type, disabled, meta: { touched, error },
|
||||
@@ -55,6 +55,13 @@ export const ipv4 = (value) => {
|
||||
return false;
|
||||
};
|
||||
|
||||
export const mac = (value) => {
|
||||
if (value && !new RegExp(R_MAC).test(value)) {
|
||||
return <Trans>form_error_mac_format</Trans>;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
export const isPositive = (value) => {
|
||||
if ((value || value === 0) && (value <= 0)) {
|
||||
return <Trans>form_error_positive</Trans>;
|
||||
|
||||
@@ -208,3 +208,20 @@ export const getClientName = (clients, ip) => {
|
||||
const client = clients.find(item => ip === item.ip);
|
||||
return (client && client.name) || '';
|
||||
};
|
||||
|
||||
export const sortClients = (clients) => {
|
||||
const compare = (a, b) => {
|
||||
const nameA = a.name.toUpperCase();
|
||||
const nameB = b.name.toUpperCase();
|
||||
|
||||
if (nameA > nameB) {
|
||||
return 1;
|
||||
} else if (nameA < nameB) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
return 0;
|
||||
};
|
||||
|
||||
return clients.sort(compare);
|
||||
};
|
||||
|
||||
@@ -63,11 +63,19 @@ const renderInterfaces = (interfaces => (
|
||||
|
||||
class Settings extends Component {
|
||||
componentDidMount() {
|
||||
const { web, dns } = this.props.config;
|
||||
const {
|
||||
webIp, webPort, dnsIp, dnsPort,
|
||||
} = this.props;
|
||||
|
||||
this.props.validateForm({
|
||||
web,
|
||||
dns,
|
||||
web: {
|
||||
ip: webIp,
|
||||
port: webPort,
|
||||
},
|
||||
dns: {
|
||||
ip: dnsIp,
|
||||
port: dnsPort,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
44
client/src/reducers/access.js
Normal file
44
client/src/reducers/access.js
Normal file
@@ -0,0 +1,44 @@
|
||||
import { handleActions } from 'redux-actions';
|
||||
|
||||
import * as actions from '../actions/access';
|
||||
|
||||
const access = handleActions(
|
||||
{
|
||||
[actions.getAccessListRequest]: state => ({ ...state, processing: true }),
|
||||
[actions.getAccessListFailure]: state => ({ ...state, processing: false }),
|
||||
[actions.getAccessListSuccess]: (state, { payload }) => {
|
||||
const {
|
||||
allowed_clients,
|
||||
disallowed_clients,
|
||||
blocked_hosts,
|
||||
} = payload;
|
||||
const newState = {
|
||||
...state,
|
||||
allowed_clients: (allowed_clients && allowed_clients.join('\n')) || '',
|
||||
disallowed_clients: (disallowed_clients && disallowed_clients.join('\n')) || '',
|
||||
blocked_hosts: (blocked_hosts && blocked_hosts.join('\n')) || '',
|
||||
processing: false,
|
||||
};
|
||||
return newState;
|
||||
},
|
||||
|
||||
[actions.setAccessListRequest]: state => ({ ...state, processingSet: true }),
|
||||
[actions.setAccessListFailure]: state => ({ ...state, processingSet: false }),
|
||||
[actions.setAccessListSuccess]: (state) => {
|
||||
const newState = {
|
||||
...state,
|
||||
processingSet: false,
|
||||
};
|
||||
return newState;
|
||||
},
|
||||
},
|
||||
{
|
||||
processing: true,
|
||||
processingSet: false,
|
||||
allowed_clients: '',
|
||||
disallowed_clients: '',
|
||||
blocked_hosts: '',
|
||||
},
|
||||
);
|
||||
|
||||
export default access;
|
||||
63
client/src/reducers/clients.js
Normal file
63
client/src/reducers/clients.js
Normal file
@@ -0,0 +1,63 @@
|
||||
import { handleActions } from 'redux-actions';
|
||||
|
||||
import * as actions from '../actions/clients';
|
||||
|
||||
const clients = handleActions({
|
||||
[actions.addClientRequest]: state => ({ ...state, processingAdding: true }),
|
||||
[actions.addClientFailure]: state => ({ ...state, processingAdding: false }),
|
||||
[actions.addClientSuccess]: (state) => {
|
||||
const newState = {
|
||||
...state,
|
||||
processingAdding: false,
|
||||
};
|
||||
return newState;
|
||||
},
|
||||
|
||||
[actions.deleteClientRequest]: state => ({ ...state, processingDeleting: true }),
|
||||
[actions.deleteClientFailure]: state => ({ ...state, processingDeleting: false }),
|
||||
[actions.deleteClientSuccess]: (state) => {
|
||||
const newState = {
|
||||
...state,
|
||||
processingDeleting: false,
|
||||
};
|
||||
return newState;
|
||||
},
|
||||
|
||||
[actions.updateClientRequest]: state => ({ ...state, processingUpdating: true }),
|
||||
[actions.updateClientFailure]: state => ({ ...state, processingUpdating: false }),
|
||||
[actions.updateClientSuccess]: (state) => {
|
||||
const newState = {
|
||||
...state,
|
||||
processingUpdating: false,
|
||||
};
|
||||
return newState;
|
||||
},
|
||||
|
||||
[actions.toggleClientModal]: (state, { payload }) => {
|
||||
if (payload) {
|
||||
const newState = {
|
||||
...state,
|
||||
modalType: payload.type || '',
|
||||
modalClientName: payload.name || '',
|
||||
isModalOpen: !state.isModalOpen,
|
||||
};
|
||||
return newState;
|
||||
}
|
||||
|
||||
const newState = {
|
||||
...state,
|
||||
isModalOpen: !state.isModalOpen,
|
||||
};
|
||||
return newState;
|
||||
},
|
||||
}, {
|
||||
processing: true,
|
||||
processingAdding: false,
|
||||
processingDeleting: false,
|
||||
processingUpdating: false,
|
||||
isModalOpen: false,
|
||||
modalClientName: '',
|
||||
modalType: '',
|
||||
});
|
||||
|
||||
export default clients;
|
||||
@@ -7,6 +7,8 @@ import versionCompare from '../helpers/versionCompare';
|
||||
import * as actions from '../actions';
|
||||
import toasts from './toasts';
|
||||
import encryption from './encryption';
|
||||
import clients from './clients';
|
||||
import access from './access';
|
||||
|
||||
const settings = handleActions({
|
||||
[actions.initSettingsRequest]: state => ({ ...state, processing: true }),
|
||||
@@ -122,16 +124,18 @@ const dashboard = handleActions({
|
||||
[actions.getVersionSuccess]: (state, { payload }) => {
|
||||
const currentVersion = state.dnsVersion === 'undefined' ? 0 : state.dnsVersion;
|
||||
|
||||
if (versionCompare(currentVersion, payload.version) === -1) {
|
||||
if (payload && versionCompare(currentVersion, payload.new_version) === -1) {
|
||||
const {
|
||||
version,
|
||||
announcement_url: announcementUrl,
|
||||
new_version: newVersion,
|
||||
can_autoupdate: canAutoUpdate,
|
||||
} = payload;
|
||||
|
||||
const newState = {
|
||||
...state,
|
||||
version,
|
||||
announcementUrl,
|
||||
newVersion,
|
||||
canAutoUpdate,
|
||||
isUpdateAvailable: true,
|
||||
};
|
||||
return newState;
|
||||
@@ -140,6 +144,13 @@ const dashboard = handleActions({
|
||||
return state;
|
||||
},
|
||||
|
||||
[actions.getUpdateRequest]: state => ({ ...state, processingUpdate: true }),
|
||||
[actions.getUpdateFailure]: state => ({ ...state, processingUpdate: false }),
|
||||
[actions.getUpdateSuccess]: (state) => {
|
||||
const newState = { ...state, processingUpdate: false };
|
||||
return newState;
|
||||
},
|
||||
|
||||
[actions.getFilteringRequest]: state => ({ ...state, processingFiltering: true }),
|
||||
[actions.getFilteringFailure]: state => ({ ...state, processingFiltering: false }),
|
||||
[actions.getFilteringSuccess]: (state, { payload }) => {
|
||||
@@ -173,7 +184,8 @@ const dashboard = handleActions({
|
||||
[actions.getClientsSuccess]: (state, { payload }) => {
|
||||
const newState = {
|
||||
...state,
|
||||
clients: payload,
|
||||
clients: payload.clients,
|
||||
autoClients: payload.autoClients,
|
||||
processingClients: false,
|
||||
};
|
||||
return newState;
|
||||
@@ -187,6 +199,7 @@ const dashboard = handleActions({
|
||||
processingVersion: true,
|
||||
processingFiltering: true,
|
||||
processingClients: true,
|
||||
processingUpdate: false,
|
||||
upstreamDns: '',
|
||||
bootstrapDns: '',
|
||||
allServers: false,
|
||||
@@ -197,6 +210,8 @@ const dashboard = handleActions({
|
||||
dnsAddresses: [],
|
||||
dnsVersion: '',
|
||||
clients: [],
|
||||
autoClients: [],
|
||||
topStats: [],
|
||||
});
|
||||
|
||||
const queryLogs = handleActions({
|
||||
@@ -271,11 +286,18 @@ const dhcp = handleActions({
|
||||
[actions.getDhcpStatusRequest]: state => ({ ...state, processing: true }),
|
||||
[actions.getDhcpStatusFailure]: state => ({ ...state, processing: false }),
|
||||
[actions.getDhcpStatusSuccess]: (state, { payload }) => {
|
||||
const {
|
||||
static_leases: staticLeases,
|
||||
...values
|
||||
} = payload;
|
||||
|
||||
const newState = {
|
||||
...state,
|
||||
...payload,
|
||||
staticLeases,
|
||||
processing: false,
|
||||
...values,
|
||||
};
|
||||
|
||||
return newState;
|
||||
},
|
||||
|
||||
@@ -328,17 +350,62 @@ const dhcp = handleActions({
|
||||
const newState = { ...state, config: newConfig, processingConfig: false };
|
||||
return newState;
|
||||
},
|
||||
|
||||
[actions.toggleLeaseModal]: (state) => {
|
||||
const newState = {
|
||||
...state,
|
||||
isModalOpen: !state.isModalOpen,
|
||||
};
|
||||
return newState;
|
||||
},
|
||||
|
||||
[actions.addStaticLeaseRequest]: state => ({ ...state, processingAdding: true }),
|
||||
[actions.addStaticLeaseFailure]: state => ({ ...state, processingAdding: false }),
|
||||
[actions.addStaticLeaseSuccess]: (state, { payload }) => {
|
||||
const {
|
||||
ip, mac, hostname,
|
||||
} = payload;
|
||||
const newLease = {
|
||||
ip,
|
||||
mac,
|
||||
hostname: hostname || '',
|
||||
};
|
||||
const leases = [...state.staticLeases, newLease];
|
||||
const newState = {
|
||||
...state,
|
||||
staticLeases: leases,
|
||||
processingAdding: false,
|
||||
};
|
||||
return newState;
|
||||
},
|
||||
|
||||
[actions.removeStaticLeaseRequest]: state => ({ ...state, processingDeleting: true }),
|
||||
[actions.removeStaticLeaseFailure]: state => ({ ...state, processingDeleting: false }),
|
||||
[actions.removeStaticLeaseSuccess]: (state, { payload }) => {
|
||||
const leaseToRemove = payload.ip;
|
||||
const leases = state.staticLeases.filter(item => item.ip !== leaseToRemove);
|
||||
const newState = {
|
||||
...state,
|
||||
staticLeases: leases,
|
||||
processingDeleting: false,
|
||||
};
|
||||
return newState;
|
||||
},
|
||||
}, {
|
||||
processing: true,
|
||||
processingStatus: false,
|
||||
processingInterfaces: false,
|
||||
processingDhcp: false,
|
||||
processingConfig: false,
|
||||
processingAdding: false,
|
||||
processingDeleting: false,
|
||||
config: {
|
||||
enabled: false,
|
||||
},
|
||||
check: null,
|
||||
leases: [],
|
||||
staticLeases: [],
|
||||
isModalOpen: false,
|
||||
});
|
||||
|
||||
export default combineReducers({
|
||||
@@ -349,6 +416,8 @@ export default combineReducers({
|
||||
toasts,
|
||||
dhcp,
|
||||
encryption,
|
||||
clients,
|
||||
access,
|
||||
loadingBar: loadingBarReducer,
|
||||
form: formReducer,
|
||||
});
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { handleActions } from 'redux-actions';
|
||||
import nanoid from 'nanoid';
|
||||
|
||||
import { addErrorToast, addSuccessToast, removeToast } from '../actions';
|
||||
import { addErrorToast, addSuccessToast, addNoticeToast, removeToast } from '../actions';
|
||||
|
||||
const toasts = handleActions({
|
||||
[addErrorToast]: (state, { payload }) => {
|
||||
@@ -24,6 +24,16 @@ const toasts = handleActions({
|
||||
const newState = { ...state, notices: [...state.notices, successToast] };
|
||||
return newState;
|
||||
},
|
||||
[addNoticeToast]: (state, { payload }) => {
|
||||
const noticeToast = {
|
||||
id: nanoid(),
|
||||
message: payload.error.toString(),
|
||||
type: 'notice',
|
||||
};
|
||||
|
||||
const newState = { ...state, notices: [...state.notices, noticeToast] };
|
||||
return newState;
|
||||
},
|
||||
[removeToast]: (state, { payload }) => {
|
||||
const filtered = state.notices.filter(notice => notice.id !== payload);
|
||||
const newState = { ...state, notices: filtered };
|
||||
|
||||
2
client/webpack.common.js
vendored
2
client/webpack.common.js
vendored
@@ -12,7 +12,7 @@ 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 FAVICON_PATH = path.resolve(RESOURCES_PATH, 'public/favicon.ico');
|
||||
const FAVICON_PATH = path.resolve(RESOURCES_PATH, 'public/favicon.png');
|
||||
|
||||
const PUBLIC_PATH = path.resolve(__dirname, '../build/static');
|
||||
|
||||
|
||||
436
clients.go
436
clients.go
@@ -2,32 +2,264 @@ package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net"
|
||||
"net/http"
|
||||
"os"
|
||||
"runtime"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/AdguardTeam/golibs/log"
|
||||
)
|
||||
|
||||
// Client information
|
||||
type Client struct {
|
||||
IP string
|
||||
Name string
|
||||
//Source source // Hosts file / User settings / DHCP
|
||||
IP string
|
||||
MAC string
|
||||
Name string
|
||||
UseOwnSettings bool // false: use global settings
|
||||
FilteringEnabled bool
|
||||
SafeSearchEnabled bool
|
||||
SafeBrowsingEnabled bool
|
||||
ParentalEnabled bool
|
||||
}
|
||||
|
||||
type clientJSON struct {
|
||||
IP string `json:"ip"`
|
||||
Name string `json:"name"`
|
||||
IP string `json:"ip"`
|
||||
MAC string `json:"mac"`
|
||||
Name string `json:"name"`
|
||||
UseGlobalSettings bool `json:"use_global_settings"`
|
||||
FilteringEnabled bool `json:"filtering_enabled"`
|
||||
ParentalEnabled bool `json:"parental_enabled"`
|
||||
SafeSearchEnabled bool `json:"safebrowsing_enabled"`
|
||||
SafeBrowsingEnabled bool `json:"safesearch_enabled"`
|
||||
}
|
||||
|
||||
var clients []Client
|
||||
var clientsFilled bool
|
||||
type clientSource uint
|
||||
|
||||
const (
|
||||
ClientSourceHostsFile clientSource = 0 // from /etc/hosts
|
||||
ClientSourceRDNS clientSource = 1 // from rDNS
|
||||
)
|
||||
|
||||
// ClientHost information
|
||||
type ClientHost struct {
|
||||
Host string
|
||||
Source clientSource
|
||||
}
|
||||
|
||||
type clientsContainer struct {
|
||||
list map[string]*Client
|
||||
ipIndex map[string]*Client
|
||||
ipHost map[string]ClientHost // IP -> Hostname
|
||||
lock sync.Mutex
|
||||
}
|
||||
|
||||
var clients clientsContainer
|
||||
|
||||
// Initialize clients container
|
||||
func clientsInit() {
|
||||
if clients.list != nil {
|
||||
log.Fatal("clients.list != nil")
|
||||
}
|
||||
clients.list = make(map[string]*Client)
|
||||
clients.ipIndex = make(map[string]*Client)
|
||||
clients.ipHost = make(map[string]ClientHost)
|
||||
|
||||
clientsAddFromHostsFile()
|
||||
}
|
||||
|
||||
func clientsGetList() map[string]*Client {
|
||||
return clients.list
|
||||
}
|
||||
|
||||
func clientExists(ip string) bool {
|
||||
clients.lock.Lock()
|
||||
defer clients.lock.Unlock()
|
||||
|
||||
_, ok := clients.ipIndex[ip]
|
||||
if ok {
|
||||
return true
|
||||
}
|
||||
|
||||
_, ok = clients.ipHost[ip]
|
||||
return ok
|
||||
}
|
||||
|
||||
// Search for a client by IP
|
||||
func clientFind(ip string) (Client, bool) {
|
||||
clients.lock.Lock()
|
||||
defer clients.lock.Unlock()
|
||||
|
||||
c, ok := clients.ipIndex[ip]
|
||||
if ok {
|
||||
return *c, true
|
||||
}
|
||||
|
||||
for _, c = range clients.list {
|
||||
if len(c.MAC) != 0 {
|
||||
mac, err := net.ParseMAC(c.MAC)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
ipAddr := dhcpServer.FindIPbyMAC(mac)
|
||||
if ipAddr == nil {
|
||||
continue
|
||||
}
|
||||
if ip == ipAddr.String() {
|
||||
return *c, true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return Client{}, false
|
||||
}
|
||||
|
||||
// Check if Client object's fields are correct
|
||||
func clientCheck(c *Client) error {
|
||||
if len(c.Name) == 0 {
|
||||
return fmt.Errorf("Invalid Name")
|
||||
}
|
||||
|
||||
if (len(c.IP) == 0 && len(c.MAC) == 0) ||
|
||||
(len(c.IP) != 0 && len(c.MAC) != 0) {
|
||||
return fmt.Errorf("IP or MAC required")
|
||||
}
|
||||
|
||||
if len(c.IP) != 0 {
|
||||
ip := net.ParseIP(c.IP)
|
||||
if ip == nil {
|
||||
return fmt.Errorf("Invalid IP")
|
||||
}
|
||||
c.IP = ip.String()
|
||||
} else {
|
||||
_, err := net.ParseMAC(c.MAC)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Invalid MAC: %s", err)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Add a new client object
|
||||
// Return true: success; false: client exists.
|
||||
func clientAdd(c Client) (bool, error) {
|
||||
e := clientCheck(&c)
|
||||
if e != nil {
|
||||
return false, e
|
||||
}
|
||||
|
||||
clients.lock.Lock()
|
||||
defer clients.lock.Unlock()
|
||||
|
||||
// check Name index
|
||||
_, ok := clients.list[c.Name]
|
||||
if ok {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
// check IP index
|
||||
if len(c.IP) != 0 {
|
||||
c2, ok := clients.ipIndex[c.IP]
|
||||
if ok {
|
||||
return false, fmt.Errorf("Another client uses the same IP address: %s", c2.Name)
|
||||
}
|
||||
}
|
||||
|
||||
clients.list[c.Name] = &c
|
||||
if len(c.IP) != 0 {
|
||||
clients.ipIndex[c.IP] = &c
|
||||
}
|
||||
|
||||
log.Tracef("'%s': '%s' | '%s' -> [%d]", c.Name, c.IP, c.MAC, len(clients.list))
|
||||
return true, nil
|
||||
}
|
||||
|
||||
// Remove a client
|
||||
func clientDel(name string) bool {
|
||||
clients.lock.Lock()
|
||||
defer clients.lock.Unlock()
|
||||
|
||||
c, ok := clients.list[name]
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
|
||||
delete(clients.list, name)
|
||||
delete(clients.ipIndex, c.IP)
|
||||
return true
|
||||
}
|
||||
|
||||
// Update a client
|
||||
func clientUpdate(name string, c Client) error {
|
||||
err := clientCheck(&c)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
clients.lock.Lock()
|
||||
defer clients.lock.Unlock()
|
||||
|
||||
old, ok := clients.list[name]
|
||||
if !ok {
|
||||
return fmt.Errorf("Client not found")
|
||||
}
|
||||
|
||||
// check Name index
|
||||
if old.Name != c.Name {
|
||||
_, ok = clients.list[c.Name]
|
||||
if ok {
|
||||
return fmt.Errorf("Client already exists")
|
||||
}
|
||||
}
|
||||
|
||||
// check IP index
|
||||
if old.IP != c.IP && len(c.IP) != 0 {
|
||||
c2, ok := clients.ipIndex[c.IP]
|
||||
if ok {
|
||||
return fmt.Errorf("Another client uses the same IP address: %s", c2.Name)
|
||||
}
|
||||
}
|
||||
|
||||
// update Name index
|
||||
if old.Name != c.Name {
|
||||
delete(clients.list, old.Name)
|
||||
}
|
||||
clients.list[c.Name] = &c
|
||||
|
||||
// update IP index
|
||||
if old.IP != c.IP {
|
||||
delete(clients.ipIndex, old.IP)
|
||||
}
|
||||
if len(c.IP) != 0 {
|
||||
clients.ipIndex[c.IP] = &c
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func clientAddHost(ip, host string, source clientSource) (bool, error) {
|
||||
clients.lock.Lock()
|
||||
defer clients.lock.Unlock()
|
||||
|
||||
// check index
|
||||
_, ok := clients.ipHost[ip]
|
||||
if ok {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
clients.ipHost[ip] = ClientHost{
|
||||
Host: host,
|
||||
Source: source,
|
||||
}
|
||||
log.Tracef("'%s': '%s' -> [%d]", host, ip, len(clients.ipHost))
|
||||
return true, nil
|
||||
}
|
||||
|
||||
// Parse system 'hosts' file and fill clients array
|
||||
func fillClientInfo() {
|
||||
func clientsAddFromHostsFile() {
|
||||
hostsFn := "/etc/hosts"
|
||||
if runtime.GOOS == "windows" {
|
||||
hostsFn = os.ExpandEnv("$SystemRoot\\system32\\drivers\\etc\\hosts")
|
||||
@@ -40,6 +272,7 @@ func fillClientInfo() {
|
||||
}
|
||||
|
||||
lines := strings.Split(string(d), "\n")
|
||||
n := 0
|
||||
for _, ln := range lines {
|
||||
ln = strings.TrimSpace(ln)
|
||||
if len(ln) == 0 || ln[0] == '#' {
|
||||
@@ -51,33 +284,71 @@ func fillClientInfo() {
|
||||
continue
|
||||
}
|
||||
|
||||
var c Client
|
||||
c.IP = fields[0]
|
||||
c.Name = fields[1]
|
||||
clients = append(clients, c)
|
||||
log.Tracef("%s -> %s", c.IP, c.Name)
|
||||
ok, e := clientAddHost(fields[0], fields[1], ClientSourceHostsFile)
|
||||
if e != nil {
|
||||
log.Tracef("%s", e)
|
||||
}
|
||||
if ok {
|
||||
n++
|
||||
}
|
||||
}
|
||||
|
||||
log.Info("Added %d client aliases from %s", len(clients), hostsFn)
|
||||
clientsFilled = true
|
||||
log.Info("Added %d client aliases from %s", n, hostsFn)
|
||||
}
|
||||
|
||||
type clientHostJSON struct {
|
||||
IP string `json:"ip"`
|
||||
Name string `json:"name"`
|
||||
Source string `json:"source"`
|
||||
}
|
||||
|
||||
type clientListJSON struct {
|
||||
Clients []clientJSON `json:"clients"`
|
||||
AutoClients []clientHostJSON `json:"auto_clients"`
|
||||
}
|
||||
|
||||
// respond with information about configured clients
|
||||
func handleGetClients(w http.ResponseWriter, r *http.Request) {
|
||||
log.Tracef("%s %v", r.Method, r.URL)
|
||||
|
||||
if !clientsFilled {
|
||||
fillClientInfo()
|
||||
}
|
||||
data := clientListJSON{}
|
||||
|
||||
data := []clientJSON{}
|
||||
for _, c := range clients {
|
||||
clients.lock.Lock()
|
||||
for _, c := range clients.list {
|
||||
cj := clientJSON{
|
||||
IP: c.IP,
|
||||
Name: c.Name,
|
||||
IP: c.IP,
|
||||
MAC: c.MAC,
|
||||
Name: c.Name,
|
||||
UseGlobalSettings: !c.UseOwnSettings,
|
||||
FilteringEnabled: c.FilteringEnabled,
|
||||
ParentalEnabled: c.ParentalEnabled,
|
||||
SafeSearchEnabled: c.SafeSearchEnabled,
|
||||
SafeBrowsingEnabled: c.SafeBrowsingEnabled,
|
||||
}
|
||||
data = append(data, cj)
|
||||
|
||||
if len(c.MAC) != 0 {
|
||||
hwAddr, _ := net.ParseMAC(c.MAC)
|
||||
ipAddr := dhcpServer.FindIPbyMAC(hwAddr)
|
||||
if ipAddr != nil {
|
||||
cj.IP = ipAddr.String()
|
||||
}
|
||||
}
|
||||
|
||||
data.Clients = append(data.Clients, cj)
|
||||
}
|
||||
for ip, ch := range clients.ipHost {
|
||||
cj := clientHostJSON{
|
||||
IP: ip,
|
||||
Name: ch.Host,
|
||||
}
|
||||
cj.Source = "etc/hosts"
|
||||
if ch.Source == ClientSourceRDNS {
|
||||
cj.Source = "rDNS"
|
||||
}
|
||||
data.AutoClients = append(data.AutoClients, cj)
|
||||
}
|
||||
clients.lock.Unlock()
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
e := json.NewEncoder(w).Encode(data)
|
||||
if e != nil {
|
||||
@@ -86,7 +357,126 @@ func handleGetClients(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
}
|
||||
|
||||
// Convert JSON object to Client object
|
||||
func jsonToClient(cj clientJSON) (*Client, error) {
|
||||
c := Client{
|
||||
IP: cj.IP,
|
||||
MAC: cj.MAC,
|
||||
Name: cj.Name,
|
||||
UseOwnSettings: !cj.UseGlobalSettings,
|
||||
FilteringEnabled: cj.FilteringEnabled,
|
||||
ParentalEnabled: cj.ParentalEnabled,
|
||||
SafeSearchEnabled: cj.SafeSearchEnabled,
|
||||
SafeBrowsingEnabled: cj.SafeBrowsingEnabled,
|
||||
}
|
||||
return &c, nil
|
||||
}
|
||||
|
||||
// Add a new client
|
||||
func handleAddClient(w http.ResponseWriter, r *http.Request) {
|
||||
log.Tracef("%s %v", r.Method, r.URL)
|
||||
body, err := ioutil.ReadAll(r.Body)
|
||||
if err != nil {
|
||||
httpError(w, http.StatusBadRequest, "failed to read request body: %s", err)
|
||||
return
|
||||
}
|
||||
|
||||
cj := clientJSON{}
|
||||
err = json.Unmarshal(body, &cj)
|
||||
if err != nil {
|
||||
httpError(w, http.StatusBadRequest, "JSON parse: %s", err)
|
||||
return
|
||||
}
|
||||
|
||||
c, err := jsonToClient(cj)
|
||||
if err != nil {
|
||||
httpError(w, http.StatusBadRequest, "%s", err)
|
||||
return
|
||||
}
|
||||
ok, err := clientAdd(*c)
|
||||
if err != nil {
|
||||
httpError(w, http.StatusBadRequest, "%s", err)
|
||||
return
|
||||
}
|
||||
if !ok {
|
||||
httpError(w, http.StatusBadRequest, "Client already exists")
|
||||
return
|
||||
}
|
||||
|
||||
_ = writeAllConfigsAndReloadDNS()
|
||||
returnOK(w)
|
||||
}
|
||||
|
||||
// Remove client
|
||||
func handleDelClient(w http.ResponseWriter, r *http.Request) {
|
||||
log.Tracef("%s %v", r.Method, r.URL)
|
||||
body, err := ioutil.ReadAll(r.Body)
|
||||
if err != nil {
|
||||
httpError(w, http.StatusBadRequest, "failed to read request body: %s", err)
|
||||
return
|
||||
}
|
||||
|
||||
cj := clientJSON{}
|
||||
err = json.Unmarshal(body, &cj)
|
||||
if err != nil || len(cj.Name) == 0 {
|
||||
httpError(w, http.StatusBadRequest, "JSON parse: %s", err)
|
||||
return
|
||||
}
|
||||
|
||||
if !clientDel(cj.Name) {
|
||||
httpError(w, http.StatusBadRequest, "Client not found")
|
||||
return
|
||||
}
|
||||
|
||||
_ = writeAllConfigsAndReloadDNS()
|
||||
returnOK(w)
|
||||
}
|
||||
|
||||
type clientUpdateJSON struct {
|
||||
Name string `json:"name"`
|
||||
Data clientJSON `json:"data"`
|
||||
}
|
||||
|
||||
// Update client's properties
|
||||
func handleUpdateClient(w http.ResponseWriter, r *http.Request) {
|
||||
log.Tracef("%s %v", r.Method, r.URL)
|
||||
body, err := ioutil.ReadAll(r.Body)
|
||||
if err != nil {
|
||||
httpError(w, http.StatusBadRequest, "failed to read request body: %s", err)
|
||||
return
|
||||
}
|
||||
|
||||
var dj clientUpdateJSON
|
||||
err = json.Unmarshal(body, &dj)
|
||||
if err != nil {
|
||||
httpError(w, http.StatusBadRequest, "JSON parse: %s", err)
|
||||
return
|
||||
}
|
||||
if len(dj.Name) == 0 {
|
||||
httpError(w, http.StatusBadRequest, "Invalid request")
|
||||
return
|
||||
}
|
||||
|
||||
c, err := jsonToClient(dj.Data)
|
||||
if err != nil {
|
||||
httpError(w, http.StatusBadRequest, "%s", err)
|
||||
return
|
||||
}
|
||||
|
||||
err = clientUpdate(dj.Name, *c)
|
||||
if err != nil {
|
||||
httpError(w, http.StatusBadRequest, "%s", err)
|
||||
return
|
||||
}
|
||||
|
||||
_ = writeAllConfigsAndReloadDNS()
|
||||
returnOK(w)
|
||||
}
|
||||
|
||||
// RegisterClientsHandlers registers HTTP handlers
|
||||
func RegisterClientsHandlers() {
|
||||
http.HandleFunc("/control/clients", postInstall(optionalAuth(ensureGET(handleGetClients))))
|
||||
http.HandleFunc("/control/clients/add", postInstall(optionalAuth(ensurePOST(handleAddClient))))
|
||||
http.HandleFunc("/control/clients/delete", postInstall(optionalAuth(ensurePOST(handleDelClient))))
|
||||
http.HandleFunc("/control/clients/update", postInstall(optionalAuth(ensurePOST(handleUpdateClient))))
|
||||
}
|
||||
|
||||
122
clients_test.go
Normal file
122
clients_test.go
Normal file
@@ -0,0 +1,122 @@
|
||||
package main
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestClients(t *testing.T) {
|
||||
var c Client
|
||||
var e error
|
||||
var b bool
|
||||
|
||||
clientsInit()
|
||||
|
||||
// add
|
||||
c = Client{
|
||||
IP: "1.1.1.1",
|
||||
Name: "client1",
|
||||
}
|
||||
b, e = clientAdd(c)
|
||||
if !b || e != nil {
|
||||
t.Fatalf("clientAdd #1")
|
||||
}
|
||||
|
||||
// add #2
|
||||
c = Client{
|
||||
IP: "2.2.2.2",
|
||||
Name: "client2",
|
||||
}
|
||||
b, e = clientAdd(c)
|
||||
if !b || e != nil {
|
||||
t.Fatalf("clientAdd #2")
|
||||
}
|
||||
|
||||
// failed add - name in use
|
||||
c = Client{
|
||||
IP: "1.2.3.5",
|
||||
Name: "client1",
|
||||
}
|
||||
b, _ = clientAdd(c)
|
||||
if b {
|
||||
t.Fatalf("clientAdd - name in use")
|
||||
}
|
||||
|
||||
// failed add - ip in use
|
||||
c = Client{
|
||||
IP: "2.2.2.2",
|
||||
Name: "client3",
|
||||
}
|
||||
b, e = clientAdd(c)
|
||||
if b || e == nil {
|
||||
t.Fatalf("clientAdd - ip in use")
|
||||
}
|
||||
|
||||
// get
|
||||
if clientExists("1.2.3.4") {
|
||||
t.Fatalf("clientExists")
|
||||
}
|
||||
if !clientExists("1.1.1.1") {
|
||||
t.Fatalf("clientExists #1")
|
||||
}
|
||||
if !clientExists("2.2.2.2") {
|
||||
t.Fatalf("clientExists #2")
|
||||
}
|
||||
|
||||
// failed update - no such name
|
||||
c.IP = "1.2.3.0"
|
||||
c.Name = "client3"
|
||||
if clientUpdate("client3", c) == nil {
|
||||
t.Fatalf("clientUpdate")
|
||||
}
|
||||
|
||||
// failed update - name in use
|
||||
c.IP = "1.2.3.0"
|
||||
c.Name = "client2"
|
||||
if clientUpdate("client1", c) == nil {
|
||||
t.Fatalf("clientUpdate - name in use")
|
||||
}
|
||||
|
||||
// failed update - ip in use
|
||||
c.IP = "2.2.2.2"
|
||||
c.Name = "client1"
|
||||
if clientUpdate("client1", c) == nil {
|
||||
t.Fatalf("clientUpdate - ip in use")
|
||||
}
|
||||
|
||||
// update
|
||||
c.IP = "1.1.1.2"
|
||||
c.Name = "client1"
|
||||
if clientUpdate("client1", c) != nil {
|
||||
t.Fatalf("clientUpdate")
|
||||
}
|
||||
|
||||
// get after update
|
||||
if clientExists("1.1.1.1") || !clientExists("1.1.1.2") {
|
||||
t.Fatalf("clientExists - get after update")
|
||||
}
|
||||
|
||||
// failed remove - no such name
|
||||
if clientDel("client3") {
|
||||
t.Fatalf("clientDel - no such name")
|
||||
}
|
||||
|
||||
// remove
|
||||
if !clientDel("client1") || clientExists("1.1.1.2") {
|
||||
t.Fatalf("clientDel")
|
||||
}
|
||||
|
||||
// add host client
|
||||
b, e = clientAddHost("1.1.1.1", "host", ClientSourceHostsFile)
|
||||
if !b || e != nil {
|
||||
t.Fatalf("clientAddHost")
|
||||
}
|
||||
|
||||
// failed add - ip exists
|
||||
b, e = clientAddHost("1.1.1.1", "host", ClientSourceHostsFile)
|
||||
if b || e != nil {
|
||||
t.Fatalf("clientAddHost - ip exists")
|
||||
}
|
||||
|
||||
// get
|
||||
if !clientExists("1.1.1.1") {
|
||||
t.Fatalf("clientAddHost")
|
||||
}
|
||||
}
|
||||
92
config.go
92
config.go
@@ -27,12 +27,30 @@ type logSettings struct {
|
||||
Verbose bool `yaml:"verbose"` // If true, verbose logging is enabled
|
||||
}
|
||||
|
||||
type clientObject struct {
|
||||
Name string `yaml:"name"`
|
||||
IP string `yaml:"ip"`
|
||||
MAC string `yaml:"mac"`
|
||||
UseGlobalSettings bool `yaml:"use_global_settings"`
|
||||
FilteringEnabled bool `yaml:"filtering_enabled"`
|
||||
ParentalEnabled bool `yaml:"parental_enabled"`
|
||||
SafeSearchEnabled bool `yaml:"safebrowsing_enabled"`
|
||||
SafeBrowsingEnabled bool `yaml:"safesearch_enabled"`
|
||||
}
|
||||
|
||||
// configuration is loaded from YAML
|
||||
// field ordering is important -- yaml fields will mirror ordering from here
|
||||
type configuration struct {
|
||||
// Raw file data to avoid re-reading of configuration file
|
||||
// It's reset after config is parsed
|
||||
fileData []byte
|
||||
|
||||
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
|
||||
// runningAsService flag is set to true when options are passed from the service runner
|
||||
runningAsService bool
|
||||
disableUpdate bool // If set, don't check for updates
|
||||
|
||||
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
|
||||
@@ -47,6 +65,9 @@ type configuration struct {
|
||||
UserRules []string `yaml:"user_rules"`
|
||||
DHCP dhcpd.ServerConfig `yaml:"dhcp"`
|
||||
|
||||
// Note: this array is filled only before file read/write and then it's cleared
|
||||
Clients []clientObject `yaml:"clients"`
|
||||
|
||||
logSettings `yaml:",inline"`
|
||||
|
||||
sync.RWMutex `yaml:"-"`
|
||||
@@ -113,9 +134,10 @@ var config = configuration{
|
||||
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
|
||||
ProtectionEnabled: true, // whether or not use any of dnsfilter features
|
||||
FilteringEnabled: true, // whether or not use filter lists
|
||||
BlockingMode: "nxdomain", // mode how to answer filtered requests
|
||||
BlockedResponseTTL: 10, // in seconds
|
||||
QueryLogEnabled: true,
|
||||
Ratelimit: 20,
|
||||
RefuseAny: true,
|
||||
@@ -173,7 +195,7 @@ func (c *configuration) getConfigFilename() string {
|
||||
func getLogSettings() logSettings {
|
||||
l := logSettings{}
|
||||
yamlFile, err := readConfigFile()
|
||||
if err != nil || yamlFile == nil {
|
||||
if err != nil {
|
||||
return l
|
||||
}
|
||||
err = yaml.Unmarshal(yamlFile, &l)
|
||||
@@ -189,19 +211,33 @@ func parseConfig() error {
|
||||
log.Debug("Reading config file: %s", configFile)
|
||||
yamlFile, err := readConfigFile()
|
||||
if err != nil {
|
||||
log.Error("Couldn't read config file: %s", err)
|
||||
return err
|
||||
}
|
||||
if yamlFile == nil {
|
||||
log.Error("YAML file doesn't exist, skipping it")
|
||||
return nil
|
||||
}
|
||||
config.fileData = nil
|
||||
err = yaml.Unmarshal(yamlFile, &config)
|
||||
if err != nil {
|
||||
log.Error("Couldn't parse config file: %s", err)
|
||||
return err
|
||||
}
|
||||
|
||||
for _, cy := range config.Clients {
|
||||
cli := Client{
|
||||
Name: cy.Name,
|
||||
IP: cy.IP,
|
||||
MAC: cy.MAC,
|
||||
UseOwnSettings: !cy.UseGlobalSettings,
|
||||
FilteringEnabled: cy.FilteringEnabled,
|
||||
ParentalEnabled: cy.ParentalEnabled,
|
||||
SafeSearchEnabled: cy.SafeSearchEnabled,
|
||||
SafeBrowsingEnabled: cy.SafeBrowsingEnabled,
|
||||
}
|
||||
_, err = clientAdd(cli)
|
||||
if err != nil {
|
||||
log.Tracef("clientAdd: %s", err)
|
||||
}
|
||||
}
|
||||
config.Clients = nil
|
||||
|
||||
// Deduplicate filters
|
||||
deduplicateFilters()
|
||||
|
||||
@@ -212,25 +248,47 @@ func parseConfig() error {
|
||||
|
||||
// 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
|
||||
if len(config.fileData) != 0 {
|
||||
return config.fileData, nil
|
||||
}
|
||||
return ioutil.ReadFile(configFile)
|
||||
|
||||
configFile := config.getConfigFilename()
|
||||
d, err := ioutil.ReadFile(configFile)
|
||||
if err != nil {
|
||||
log.Error("Couldn't read config file %s: %s", configFile, err)
|
||||
return nil, err
|
||||
}
|
||||
return d, nil
|
||||
}
|
||||
|
||||
// Saves configuration to the YAML file and also saves the user filter contents to a file
|
||||
func (c *configuration) write() error {
|
||||
c.Lock()
|
||||
defer c.Unlock()
|
||||
if config.firstRun {
|
||||
log.Debug("Silently refusing to write config because first run and not configured yet")
|
||||
return nil
|
||||
|
||||
clientsList := clientsGetList()
|
||||
for _, cli := range clientsList {
|
||||
ip := cli.IP
|
||||
if len(cli.MAC) != 0 {
|
||||
ip = ""
|
||||
}
|
||||
cy := clientObject{
|
||||
Name: cli.Name,
|
||||
IP: ip,
|
||||
MAC: cli.MAC,
|
||||
UseGlobalSettings: !cli.UseOwnSettings,
|
||||
FilteringEnabled: cli.FilteringEnabled,
|
||||
ParentalEnabled: cli.ParentalEnabled,
|
||||
SafeSearchEnabled: cli.SafeSearchEnabled,
|
||||
SafeBrowsingEnabled: cli.SafeBrowsingEnabled,
|
||||
}
|
||||
config.Clients = append(config.Clients, cy)
|
||||
}
|
||||
|
||||
configFile := config.getConfigFilename()
|
||||
log.Debug("Writing YAML file: %s", configFile)
|
||||
yamlText, err := yaml.Marshal(&config)
|
||||
config.Clients = nil
|
||||
if err != nil {
|
||||
log.Error("Couldn't generate YAML file: %s", err)
|
||||
return err
|
||||
|
||||
66
control.go
66
control.go
@@ -31,9 +31,6 @@ var versionCheckLastTime time.Time
|
||||
|
||||
var protocols = []string{"tls://", "https://", "tcp://", "sdns://"}
|
||||
|
||||
const versionCheckURL = "https://adguardteam.github.io/AdGuardHome/version.json"
|
||||
const versionCheckPeriod = time.Hour * 8
|
||||
|
||||
var transport = &http.Transport{
|
||||
DialContext: customDialContext,
|
||||
}
|
||||
@@ -557,42 +554,6 @@ func checkDNS(input string, bootstrap []string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func handleGetVersionJSON(w http.ResponseWriter, r *http.Request) {
|
||||
log.Tracef("%s %v", r.Method, r.URL)
|
||||
now := time.Now()
|
||||
if now.Sub(versionCheckLastTime) <= versionCheckPeriod && len(versionCheckJSON) != 0 {
|
||||
// return cached copy
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.Write(versionCheckJSON)
|
||||
return
|
||||
}
|
||||
|
||||
resp, err := client.Get(versionCheckURL)
|
||||
if err != nil {
|
||||
httpError(w, http.StatusBadGateway, "Couldn't get version check json from %s: %T %s\n", versionCheckURL, err, err)
|
||||
return
|
||||
}
|
||||
if resp != nil && resp.Body != nil {
|
||||
defer resp.Body.Close()
|
||||
}
|
||||
|
||||
// read the body entirely
|
||||
body, err := ioutil.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
httpError(w, http.StatusBadGateway, "Couldn't read response body from %s: %s", versionCheckURL, err)
|
||||
return
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
_, err = w.Write(body)
|
||||
if err != nil {
|
||||
httpError(w, http.StatusInternalServerError, "Couldn't write body: %s", err)
|
||||
}
|
||||
|
||||
versionCheckLastTime = now
|
||||
versionCheckJSON = body
|
||||
}
|
||||
|
||||
// ---------
|
||||
// filtering
|
||||
// ---------
|
||||
@@ -712,19 +673,18 @@ func handleFilteringAddURL(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
func handleFilteringRemoveURL(w http.ResponseWriter, r *http.Request) {
|
||||
log.Tracef("%s %v", r.Method, r.URL)
|
||||
parameters, err := parseParametersFromBody(r.Body)
|
||||
|
||||
type request struct {
|
||||
URL string `json:"url"`
|
||||
}
|
||||
req := request{}
|
||||
err := json.NewDecoder(r.Body).Decode(&req)
|
||||
if err != nil {
|
||||
httpError(w, http.StatusBadRequest, "failed to parse parameters from body: %s", err)
|
||||
httpError(w, http.StatusBadRequest, "Failed to parse request body json: %s", err)
|
||||
return
|
||||
}
|
||||
|
||||
url, ok := parameters["url"]
|
||||
if !ok {
|
||||
http.Error(w, "URL parameter was not specified", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
if valid := govalidator.IsRequestURL(url); !valid {
|
||||
if valid := govalidator.IsRequestURL(req.URL); !valid {
|
||||
http.Error(w, "URL parameter is not valid request URL", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
@@ -733,7 +693,7 @@ func handleFilteringRemoveURL(w http.ResponseWriter, r *http.Request) {
|
||||
config.Lock()
|
||||
newFilters := config.Filters[:0]
|
||||
for _, filter := range config.Filters {
|
||||
if filter.URL != url {
|
||||
if filter.URL != req.URL {
|
||||
newFilters = append(newFilters, filter)
|
||||
} else {
|
||||
// Remove the filter file
|
||||
@@ -871,7 +831,7 @@ func handleParentalEnable(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
sensitivity, ok := parameters["sensitivity"]
|
||||
if !ok {
|
||||
http.Error(w, "URL parameter was not specified", 400)
|
||||
http.Error(w, "Sensitivity parameter was not specified", 400)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -1006,6 +966,7 @@ func registerControlHandlers() {
|
||||
http.HandleFunc("/control/stats_history", postInstall(optionalAuth(ensureGET(handleStatsHistory))))
|
||||
http.HandleFunc("/control/stats_reset", postInstall(optionalAuth(ensurePOST(handleStatsReset))))
|
||||
http.HandleFunc("/control/version.json", postInstall(optionalAuth(handleGetVersionJSON)))
|
||||
http.HandleFunc("/control/update", postInstall(optionalAuth(ensurePOST(handleUpdate))))
|
||||
http.HandleFunc("/control/filtering/enable", postInstall(optionalAuth(ensurePOST(handleFilteringEnable))))
|
||||
http.HandleFunc("/control/filtering/disable", postInstall(optionalAuth(ensurePOST(handleFilteringDisable))))
|
||||
http.HandleFunc("/control/filtering/add_url", postInstall(optionalAuth(ensurePOST(handleFilteringAddURL))))
|
||||
@@ -1028,6 +989,11 @@ func registerControlHandlers() {
|
||||
http.HandleFunc("/control/dhcp/interfaces", postInstall(optionalAuth(ensureGET(handleDHCPInterfaces))))
|
||||
http.HandleFunc("/control/dhcp/set_config", postInstall(optionalAuth(ensurePOST(handleDHCPSetConfig))))
|
||||
http.HandleFunc("/control/dhcp/find_active_dhcp", postInstall(optionalAuth(ensurePOST(handleDHCPFindActiveServer))))
|
||||
http.HandleFunc("/control/dhcp/add_static_lease", postInstall(optionalAuth(ensurePOST(handleDHCPAddStaticLease))))
|
||||
http.HandleFunc("/control/dhcp/remove_static_lease", postInstall(optionalAuth(ensurePOST(handleDHCPRemoveStaticLease))))
|
||||
|
||||
http.HandleFunc("/control/access/list", postInstall(optionalAuth(ensureGET(handleAccessList))))
|
||||
http.HandleFunc("/control/access/set", postInstall(optionalAuth(ensurePOST(handleAccessSet))))
|
||||
|
||||
RegisterTLSHandlers()
|
||||
RegisterClientsHandlers()
|
||||
|
||||
87
control_access.go
Normal file
87
control_access.go
Normal file
@@ -0,0 +1,87 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net"
|
||||
"net/http"
|
||||
|
||||
"github.com/AdguardTeam/golibs/log"
|
||||
)
|
||||
|
||||
type accessListJSON struct {
|
||||
AllowedClients []string `json:"allowed_clients"`
|
||||
DisallowedClients []string `json:"disallowed_clients"`
|
||||
BlockedHosts []string `json:"blocked_hosts"`
|
||||
}
|
||||
|
||||
func handleAccessList(w http.ResponseWriter, r *http.Request) {
|
||||
log.Tracef("%s %v", r.Method, r.URL)
|
||||
|
||||
controlLock.Lock()
|
||||
j := accessListJSON{
|
||||
AllowedClients: config.DNS.AllowedClients,
|
||||
DisallowedClients: config.DNS.DisallowedClients,
|
||||
BlockedHosts: config.DNS.BlockedHosts,
|
||||
}
|
||||
controlLock.Unlock()
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
err := json.NewEncoder(w).Encode(j)
|
||||
if err != nil {
|
||||
httpError(w, http.StatusInternalServerError, "json.Encode: %s", err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func checkIPCIDRArray(src []string) error {
|
||||
for _, s := range src {
|
||||
ip := net.ParseIP(s)
|
||||
if ip != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
_, _, err := net.ParseCIDR(s)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func handleAccessSet(w http.ResponseWriter, r *http.Request) {
|
||||
log.Tracef("%s %v", r.Method, r.URL)
|
||||
|
||||
j := accessListJSON{}
|
||||
err := json.NewDecoder(r.Body).Decode(&j)
|
||||
if err != nil {
|
||||
httpError(w, http.StatusBadRequest, "json.Decode: %s", err)
|
||||
return
|
||||
}
|
||||
|
||||
err = checkIPCIDRArray(j.AllowedClients)
|
||||
if err == nil {
|
||||
err = checkIPCIDRArray(j.DisallowedClients)
|
||||
}
|
||||
if err != nil {
|
||||
httpError(w, http.StatusBadRequest, "%s", err)
|
||||
return
|
||||
}
|
||||
|
||||
config.Lock()
|
||||
config.DNS.AllowedClients = j.AllowedClients
|
||||
config.DNS.DisallowedClients = j.DisallowedClients
|
||||
config.DNS.BlockedHosts = j.BlockedHosts
|
||||
config.Unlock()
|
||||
|
||||
log.Tracef("Update access lists: %d, %d, %d",
|
||||
len(j.AllowedClients), len(j.DisallowedClients), len(j.BlockedHosts))
|
||||
|
||||
err = writeAllConfigsAndReloadDNS()
|
||||
if err != nil {
|
||||
httpError(w, http.StatusBadRequest, "%s", err)
|
||||
return
|
||||
}
|
||||
|
||||
returnOK(w)
|
||||
}
|
||||
511
control_update.go
Normal file
511
control_update.go
Normal file
@@ -0,0 +1,511 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"archive/tar"
|
||||
"archive/zip"
|
||||
"compress/gzip"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strings"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"github.com/AdguardTeam/golibs/log"
|
||||
)
|
||||
|
||||
// Convert version.json data to our JSON response
|
||||
func getVersionResp(data []byte) []byte {
|
||||
versionJSON := make(map[string]interface{})
|
||||
err := json.Unmarshal(data, &versionJSON)
|
||||
if err != nil {
|
||||
log.Error("version.json: %s", err)
|
||||
return []byte{}
|
||||
}
|
||||
|
||||
ret := make(map[string]interface{})
|
||||
ret["can_autoupdate"] = false
|
||||
|
||||
var ok1, ok2, ok3 bool
|
||||
ret["new_version"], ok1 = versionJSON["version"].(string)
|
||||
ret["announcement"], ok2 = versionJSON["announcement"].(string)
|
||||
ret["announcement_url"], ok3 = versionJSON["announcement_url"].(string)
|
||||
selfUpdateMinVersion, ok4 := versionJSON["selfupdate_min_version"].(string)
|
||||
if !ok1 || !ok2 || !ok3 || !ok4 {
|
||||
log.Error("version.json: invalid data")
|
||||
return []byte{}
|
||||
}
|
||||
|
||||
_, ok := versionJSON[fmt.Sprintf("download_%s_%s", runtime.GOOS, runtime.GOARCH)]
|
||||
if ok && ret["new_version"] != VersionString && VersionString >= selfUpdateMinVersion {
|
||||
ret["can_autoupdate"] = true
|
||||
}
|
||||
|
||||
d, _ := json.Marshal(ret)
|
||||
return d
|
||||
}
|
||||
|
||||
// Get the latest available version from the Internet
|
||||
func handleGetVersionJSON(w http.ResponseWriter, r *http.Request) {
|
||||
log.Tracef("%s %v", r.Method, r.URL)
|
||||
|
||||
if config.disableUpdate {
|
||||
log.Tracef("New app version check is disabled by user")
|
||||
return
|
||||
}
|
||||
|
||||
now := time.Now()
|
||||
controlLock.Lock()
|
||||
cached := now.Sub(versionCheckLastTime) <= versionCheckPeriod && len(versionCheckJSON) != 0
|
||||
data := versionCheckJSON
|
||||
controlLock.Unlock()
|
||||
|
||||
if cached {
|
||||
// return cached copy
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.Write(getVersionResp(data))
|
||||
return
|
||||
}
|
||||
|
||||
resp, err := client.Get(versionCheckURL)
|
||||
if err != nil {
|
||||
httpError(w, http.StatusBadGateway, "Couldn't get version check json from %s: %T %s\n", versionCheckURL, err, err)
|
||||
return
|
||||
}
|
||||
if resp != nil && resp.Body != nil {
|
||||
defer resp.Body.Close()
|
||||
}
|
||||
|
||||
// read the body entirely
|
||||
body, err := ioutil.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
httpError(w, http.StatusBadGateway, "Couldn't read response body from %s: %s", versionCheckURL, err)
|
||||
return
|
||||
}
|
||||
|
||||
controlLock.Lock()
|
||||
versionCheckLastTime = now
|
||||
versionCheckJSON = body
|
||||
controlLock.Unlock()
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
_, err = w.Write(getVersionResp(body))
|
||||
if err != nil {
|
||||
httpError(w, http.StatusInternalServerError, "Couldn't write body: %s", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Copy file on disk
|
||||
func copyFile(src, dst string) error {
|
||||
d, e := ioutil.ReadFile(src)
|
||||
if e != nil {
|
||||
return e
|
||||
}
|
||||
e = ioutil.WriteFile(dst, d, 0644)
|
||||
if e != nil {
|
||||
return e
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type updateInfo struct {
|
||||
pkgURL string // URL for the new package
|
||||
pkgName string // Full path to package file
|
||||
newVer string // New version string
|
||||
updateDir string // Full path to the directory containing unpacked files from the new package
|
||||
backupDir string // Full path to backup directory
|
||||
configName string // Full path to the current configuration file
|
||||
updateConfigName string // Full path to the configuration file to check by the new binary
|
||||
curBinName string // Full path to the current executable file
|
||||
bkpBinName string // Full path to the current executable file in backup directory
|
||||
newBinName string // Full path to the new executable file
|
||||
}
|
||||
|
||||
// Fill in updateInfo object
|
||||
func getUpdateInfo(jsonData []byte) (*updateInfo, error) {
|
||||
var u updateInfo
|
||||
|
||||
workDir := config.ourWorkingDir
|
||||
|
||||
versionJSON := make(map[string]interface{})
|
||||
err := json.Unmarshal(jsonData, &versionJSON)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("JSON parse: %s", err)
|
||||
}
|
||||
|
||||
u.pkgURL = versionJSON[fmt.Sprintf("download_%s_%s", runtime.GOOS, runtime.GOARCH)].(string)
|
||||
u.newVer = versionJSON["version"].(string)
|
||||
if len(u.pkgURL) == 0 || len(u.newVer) == 0 {
|
||||
return nil, fmt.Errorf("Invalid JSON")
|
||||
}
|
||||
|
||||
if u.newVer == VersionString {
|
||||
return nil, fmt.Errorf("No need to update")
|
||||
}
|
||||
|
||||
u.updateDir = filepath.Join(workDir, fmt.Sprintf("agh-update-%s", u.newVer))
|
||||
u.backupDir = filepath.Join(workDir, fmt.Sprintf("agh-backup-%s", VersionString))
|
||||
|
||||
_, pkgFileName := filepath.Split(u.pkgURL)
|
||||
if len(pkgFileName) == 0 {
|
||||
return nil, fmt.Errorf("Invalid JSON")
|
||||
}
|
||||
u.pkgName = filepath.Join(u.updateDir, pkgFileName)
|
||||
|
||||
u.configName = config.getConfigFilename()
|
||||
u.updateConfigName = filepath.Join(u.updateDir, "AdGuardHome", "AdGuardHome.yaml")
|
||||
if strings.HasSuffix(pkgFileName, ".zip") {
|
||||
u.updateConfigName = filepath.Join(u.updateDir, "AdGuardHome.yaml")
|
||||
}
|
||||
|
||||
binName := "AdGuardHome"
|
||||
if runtime.GOOS == "windows" {
|
||||
binName = "AdGuardHome.exe"
|
||||
}
|
||||
u.curBinName = filepath.Join(workDir, binName)
|
||||
u.bkpBinName = filepath.Join(u.backupDir, binName)
|
||||
u.newBinName = filepath.Join(u.updateDir, "AdGuardHome", binName)
|
||||
if strings.HasSuffix(pkgFileName, ".zip") {
|
||||
u.newBinName = filepath.Join(u.updateDir, binName)
|
||||
}
|
||||
|
||||
return &u, nil
|
||||
}
|
||||
|
||||
// Unpack all files from .zip file to the specified directory
|
||||
// Existing files are overwritten
|
||||
// Return the list of files (not directories) written
|
||||
func zipFileUnpack(zipfile, outdir string) ([]string, error) {
|
||||
|
||||
r, err := zip.OpenReader(zipfile)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("zip.OpenReader(): %s", err)
|
||||
}
|
||||
defer r.Close()
|
||||
|
||||
var files []string
|
||||
var err2 error
|
||||
var zr io.ReadCloser
|
||||
for _, zf := range r.File {
|
||||
zr, err = zf.Open()
|
||||
if err != nil {
|
||||
err2 = fmt.Errorf("zip file Open(): %s", err)
|
||||
break
|
||||
}
|
||||
|
||||
fi := zf.FileInfo()
|
||||
if len(fi.Name()) == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
fn := filepath.Join(outdir, fi.Name())
|
||||
|
||||
if fi.IsDir() {
|
||||
err = os.Mkdir(fn, fi.Mode())
|
||||
if err != nil && !os.IsExist(err) {
|
||||
err2 = fmt.Errorf("os.Mkdir(): %s", err)
|
||||
break
|
||||
}
|
||||
log.Tracef("created directory %s", fn)
|
||||
continue
|
||||
}
|
||||
|
||||
f, err := os.OpenFile(fn, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, fi.Mode())
|
||||
if err != nil {
|
||||
err2 = fmt.Errorf("os.OpenFile(): %s", err)
|
||||
break
|
||||
}
|
||||
_, err = io.Copy(f, zr)
|
||||
if err != nil {
|
||||
f.Close()
|
||||
err2 = fmt.Errorf("io.Copy(): %s", err)
|
||||
break
|
||||
}
|
||||
f.Close()
|
||||
|
||||
log.Tracef("created file %s", fn)
|
||||
files = append(files, fi.Name())
|
||||
}
|
||||
|
||||
zr.Close()
|
||||
return files, err2
|
||||
}
|
||||
|
||||
// Unpack all files from .tar.gz file to the specified directory
|
||||
// Existing files are overwritten
|
||||
// Return the list of files (not directories) written
|
||||
func targzFileUnpack(tarfile, outdir string) ([]string, error) {
|
||||
|
||||
f, err := os.Open(tarfile)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("os.Open(): %s", err)
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
gzReader, err := gzip.NewReader(f)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("gzip.NewReader(): %s", err)
|
||||
}
|
||||
|
||||
var files []string
|
||||
var err2 error
|
||||
tarReader := tar.NewReader(gzReader)
|
||||
for {
|
||||
header, err := tarReader.Next()
|
||||
if err == io.EOF {
|
||||
err2 = nil
|
||||
break
|
||||
}
|
||||
if err != nil {
|
||||
err2 = fmt.Errorf("tarReader.Next(): %s", err)
|
||||
break
|
||||
}
|
||||
if len(header.Name) == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
fn := filepath.Join(outdir, header.Name)
|
||||
|
||||
if header.Typeflag == tar.TypeDir {
|
||||
err = os.Mkdir(fn, os.FileMode(header.Mode&0777))
|
||||
if err != nil && !os.IsExist(err) {
|
||||
err2 = fmt.Errorf("os.Mkdir(%s): %s", fn, err)
|
||||
break
|
||||
}
|
||||
log.Tracef("created directory %s", fn)
|
||||
continue
|
||||
} else if header.Typeflag != tar.TypeReg {
|
||||
log.Tracef("%s: unknown file type %d, skipping", header.Name, header.Typeflag)
|
||||
continue
|
||||
}
|
||||
|
||||
f, err := os.OpenFile(fn, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, os.FileMode(header.Mode&0777))
|
||||
if err != nil {
|
||||
err2 = fmt.Errorf("os.OpenFile(%s): %s", fn, err)
|
||||
break
|
||||
}
|
||||
_, err = io.Copy(f, tarReader)
|
||||
if err != nil {
|
||||
f.Close()
|
||||
err2 = fmt.Errorf("io.Copy(): %s", err)
|
||||
break
|
||||
}
|
||||
f.Close()
|
||||
|
||||
log.Tracef("created file %s", fn)
|
||||
files = append(files, header.Name)
|
||||
}
|
||||
|
||||
gzReader.Close()
|
||||
return files, err2
|
||||
}
|
||||
|
||||
func copySupportingFiles(files []string, srcdir, dstdir string, useSrcNameOnly, useDstNameOnly bool) error {
|
||||
for _, f := range files {
|
||||
_, name := filepath.Split(f)
|
||||
if name == "AdGuardHome" || name == "AdGuardHome.exe" || name == "AdGuardHome.yaml" {
|
||||
continue
|
||||
}
|
||||
|
||||
src := filepath.Join(srcdir, f)
|
||||
if useSrcNameOnly {
|
||||
src = filepath.Join(srcdir, name)
|
||||
}
|
||||
|
||||
dst := filepath.Join(dstdir, f)
|
||||
if useDstNameOnly {
|
||||
dst = filepath.Join(dstdir, name)
|
||||
}
|
||||
|
||||
err := copyFile(src, dst)
|
||||
if err != nil && !os.IsNotExist(err) {
|
||||
return err
|
||||
}
|
||||
|
||||
log.Tracef("Copied: %s -> %s", src, dst)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Download package file and save it to disk
|
||||
func getPackageFile(u *updateInfo) error {
|
||||
resp, err := client.Get(u.pkgURL)
|
||||
if err != nil {
|
||||
return fmt.Errorf("HTTP request failed: %s", err)
|
||||
}
|
||||
if resp != nil && resp.Body != nil {
|
||||
defer resp.Body.Close()
|
||||
}
|
||||
|
||||
log.Tracef("Reading HTTP body")
|
||||
body, err := ioutil.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return fmt.Errorf("ioutil.ReadAll() failed: %s", err)
|
||||
}
|
||||
|
||||
log.Tracef("Saving package to file")
|
||||
err = ioutil.WriteFile(u.pkgName, body, 0644)
|
||||
if err != nil {
|
||||
return fmt.Errorf("ioutil.WriteFile() failed: %s", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Perform an update procedure
|
||||
func doUpdate(u *updateInfo) error {
|
||||
log.Info("Updating from %s to %s. URL:%s Package:%s",
|
||||
VersionString, u.newVer, u.pkgURL, u.pkgName)
|
||||
|
||||
_ = os.Mkdir(u.updateDir, 0755)
|
||||
|
||||
var err error
|
||||
err = getPackageFile(u)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
log.Tracef("Unpacking the package")
|
||||
_, file := filepath.Split(u.pkgName)
|
||||
var files []string
|
||||
if strings.HasSuffix(file, ".zip") {
|
||||
files, err = zipFileUnpack(u.pkgName, u.updateDir)
|
||||
if err != nil {
|
||||
return fmt.Errorf("zipFileUnpack() failed: %s", err)
|
||||
}
|
||||
} else if strings.HasSuffix(file, ".tar.gz") {
|
||||
files, err = targzFileUnpack(u.pkgName, u.updateDir)
|
||||
if err != nil {
|
||||
return fmt.Errorf("targzFileUnpack() failed: %s", err)
|
||||
}
|
||||
} else {
|
||||
return fmt.Errorf("Unknown package extension")
|
||||
}
|
||||
|
||||
log.Tracef("Checking configuration")
|
||||
err = copyFile(u.configName, u.updateConfigName)
|
||||
if err != nil {
|
||||
return fmt.Errorf("copyFile() failed: %s", err)
|
||||
}
|
||||
cmd := exec.Command(u.newBinName, "--check-config")
|
||||
err = cmd.Run()
|
||||
if err != nil || cmd.ProcessState.ExitCode() != 0 {
|
||||
return fmt.Errorf("exec.Command(): %s %d", err, cmd.ProcessState.ExitCode())
|
||||
}
|
||||
|
||||
log.Tracef("Backing up the current configuration")
|
||||
_ = os.Mkdir(u.backupDir, 0755)
|
||||
err = copyFile(u.configName, filepath.Join(u.backupDir, "AdGuardHome.yaml"))
|
||||
if err != nil {
|
||||
return fmt.Errorf("copyFile() failed: %s", err)
|
||||
}
|
||||
|
||||
// ./README.md -> backup/README.md
|
||||
err = copySupportingFiles(files, config.ourWorkingDir, u.backupDir, true, true)
|
||||
if err != nil {
|
||||
return fmt.Errorf("copySupportingFiles(%s, %s) failed: %s",
|
||||
config.ourWorkingDir, u.backupDir, err)
|
||||
}
|
||||
|
||||
// update/[AdGuardHome/]README.md -> ./README.md
|
||||
err = copySupportingFiles(files, u.updateDir, config.ourWorkingDir, false, true)
|
||||
if err != nil {
|
||||
return fmt.Errorf("copySupportingFiles(%s, %s) failed: %s",
|
||||
u.updateDir, config.ourWorkingDir, err)
|
||||
}
|
||||
|
||||
log.Tracef("Renaming: %s -> %s", u.curBinName, u.bkpBinName)
|
||||
err = os.Rename(u.curBinName, u.bkpBinName)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if runtime.GOOS == "windows" {
|
||||
// rename fails with "File in use" error
|
||||
err = copyFile(u.newBinName, u.curBinName)
|
||||
} else {
|
||||
err = os.Rename(u.newBinName, u.curBinName)
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
log.Tracef("Renamed: %s -> %s", u.newBinName, u.curBinName)
|
||||
|
||||
_ = os.Remove(u.pkgName)
|
||||
_ = os.RemoveAll(u.updateDir)
|
||||
return nil
|
||||
}
|
||||
|
||||
// Complete an update procedure
|
||||
func finishUpdate(u *updateInfo) {
|
||||
log.Info("Stopping all tasks")
|
||||
cleanup()
|
||||
stopHTTPServer()
|
||||
cleanupAlways()
|
||||
|
||||
if runtime.GOOS == "windows" {
|
||||
|
||||
if config.runningAsService {
|
||||
// Note:
|
||||
// we can't restart the service via "kardianos/service" package - it kills the process first
|
||||
// we can't start a new instance - Windows doesn't allow it
|
||||
cmd := exec.Command("cmd", "/c", "net stop AdGuardHome & net start AdGuardHome")
|
||||
err := cmd.Start()
|
||||
if err != nil {
|
||||
log.Fatalf("exec.Command() failed: %s", err)
|
||||
}
|
||||
os.Exit(0)
|
||||
}
|
||||
|
||||
cmd := exec.Command(u.curBinName, os.Args[1:]...)
|
||||
log.Info("Restarting: %v", cmd.Args)
|
||||
cmd.Stdin = os.Stdin
|
||||
cmd.Stdout = os.Stdout
|
||||
cmd.Stderr = os.Stderr
|
||||
err := cmd.Start()
|
||||
if err != nil {
|
||||
log.Fatalf("exec.Command() failed: %s", err)
|
||||
}
|
||||
os.Exit(0)
|
||||
|
||||
} else {
|
||||
|
||||
log.Info("Restarting: %v", os.Args)
|
||||
err := syscall.Exec(u.curBinName, os.Args, os.Environ())
|
||||
if err != nil {
|
||||
log.Fatalf("syscall.Exec() failed: %s", err)
|
||||
}
|
||||
// Unreachable code
|
||||
}
|
||||
}
|
||||
|
||||
// Perform an update procedure to the latest available version
|
||||
func handleUpdate(w http.ResponseWriter, r *http.Request) {
|
||||
log.Tracef("%s %v", r.Method, r.URL)
|
||||
|
||||
if len(versionCheckJSON) == 0 {
|
||||
httpError(w, http.StatusBadRequest, "/update request isn't allowed now")
|
||||
return
|
||||
}
|
||||
|
||||
u, err := getUpdateInfo(versionCheckJSON)
|
||||
if err != nil {
|
||||
httpError(w, http.StatusInternalServerError, "%s", err)
|
||||
return
|
||||
}
|
||||
|
||||
err = doUpdate(u)
|
||||
if err != nil {
|
||||
httpError(w, http.StatusInternalServerError, "%s", err)
|
||||
return
|
||||
}
|
||||
|
||||
returnOK(w)
|
||||
|
||||
time.Sleep(time.Second) // wait (hopefully) until response is sent (not sure whether it's really necessary)
|
||||
go finishUpdate(u)
|
||||
}
|
||||
54
control_update_test.go
Normal file
54
control_update_test.go
Normal file
@@ -0,0 +1,54 @@
|
||||
// +build ignore
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"os"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestDoUpdate(t *testing.T) {
|
||||
config.DNS.Port = 0
|
||||
config.ourWorkingDir = "."
|
||||
u := updateInfo{
|
||||
pkgURL: "https://github.com/AdguardTeam/AdGuardHome/releases/download/v0.95/AdGuardHome_v0.95_linux_amd64.tar.gz",
|
||||
pkgName: "./AdGuardHome_v0.95_linux_amd64.tar.gz",
|
||||
newVer: "v0.95",
|
||||
updateDir: "./agh-update-v0.95",
|
||||
backupDir: "./agh-backup-v0.94",
|
||||
configName: "./AdGuardHome.yaml",
|
||||
updateConfigName: "./agh-update-v0.95/AdGuardHome/AdGuardHome.yaml",
|
||||
curBinName: "./AdGuardHome",
|
||||
bkpBinName: "./agh-backup-v0.94/AdGuardHome",
|
||||
newBinName: "./agh-update-v0.95/AdGuardHome/AdGuardHome",
|
||||
}
|
||||
e := doUpdate(&u)
|
||||
if e != nil {
|
||||
t.Fatalf("FAILED: %s", e)
|
||||
}
|
||||
os.RemoveAll(u.backupDir)
|
||||
}
|
||||
|
||||
func TestTargzFileUnpack(t *testing.T) {
|
||||
fn := "./dist/AdGuardHome_v0.95_linux_amd64.tar.gz"
|
||||
outdir := "./test-unpack"
|
||||
_ = os.Mkdir(outdir, 0755)
|
||||
files, e := targzFileUnpack(fn, outdir)
|
||||
if e != nil {
|
||||
t.Fatalf("FAILED: %s", e)
|
||||
}
|
||||
t.Logf("%v", files)
|
||||
os.RemoveAll(outdir)
|
||||
}
|
||||
|
||||
func TestZipFileUnpack(t *testing.T) {
|
||||
fn := "./dist/AdGuardHome_v0.95_Windows_amd64.zip"
|
||||
outdir := "./test-unpack"
|
||||
_ = os.Mkdir(outdir, 0755)
|
||||
files, e := zipFileUnpack(fn, outdir)
|
||||
if e != nil {
|
||||
t.Fatalf("FAILED: %s", e)
|
||||
}
|
||||
t.Logf("%v", files)
|
||||
os.RemoveAll(outdir)
|
||||
}
|
||||
141
dhcp.go
141
dhcp.go
@@ -20,23 +20,33 @@ import (
|
||||
|
||||
var dhcpServer = dhcpd.Server{}
|
||||
|
||||
// []dhcpd.Lease -> JSON
|
||||
func convertLeases(inputLeases []dhcpd.Lease, includeExpires bool) []map[string]string {
|
||||
leases := []map[string]string{}
|
||||
for _, l := range inputLeases {
|
||||
lease := map[string]string{
|
||||
"mac": l.HWAddr.String(),
|
||||
"ip": l.IP.String(),
|
||||
"hostname": l.Hostname,
|
||||
}
|
||||
|
||||
if includeExpires {
|
||||
lease["expires"] = l.Expiry.Format(time.RFC3339)
|
||||
}
|
||||
|
||||
leases = append(leases, lease)
|
||||
}
|
||||
return leases
|
||||
}
|
||||
|
||||
func handleDHCPStatus(w http.ResponseWriter, r *http.Request) {
|
||||
log.Tracef("%s %v", r.Method, r.URL)
|
||||
rawLeases := dhcpServer.Leases()
|
||||
leases := []map[string]string{}
|
||||
for i := range rawLeases {
|
||||
lease := map[string]string{
|
||||
"mac": rawLeases[i].HWAddr.String(),
|
||||
"ip": rawLeases[i].IP.String(),
|
||||
"hostname": rawLeases[i].Hostname,
|
||||
"expires": rawLeases[i].Expiry.Format(time.RFC3339),
|
||||
}
|
||||
leases = append(leases, lease)
|
||||
|
||||
}
|
||||
leases := convertLeases(dhcpServer.Leases(), true)
|
||||
staticLeases := convertLeases(dhcpServer.StaticLeases(), false)
|
||||
status := map[string]interface{}{
|
||||
"config": config.DHCP,
|
||||
"leases": leases,
|
||||
"config": config.DHCP,
|
||||
"leases": leases,
|
||||
"static_leases": staticLeases,
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
@@ -47,20 +57,43 @@ func handleDHCPStatus(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
}
|
||||
|
||||
type leaseJSON struct {
|
||||
HWAddr string `json:"mac"`
|
||||
IP string `json:"ip"`
|
||||
Hostname string `json:"hostname"`
|
||||
}
|
||||
|
||||
type dhcpServerConfigJSON struct {
|
||||
dhcpd.ServerConfig `json:",inline"`
|
||||
StaticLeases []leaseJSON `json:"static_leases"`
|
||||
}
|
||||
|
||||
func handleDHCPSetConfig(w http.ResponseWriter, r *http.Request) {
|
||||
log.Tracef("%s %v", r.Method, r.URL)
|
||||
newconfig := dhcpd.ServerConfig{}
|
||||
newconfig := dhcpServerConfigJSON{}
|
||||
err := json.NewDecoder(r.Body).Decode(&newconfig)
|
||||
if err != nil {
|
||||
httpError(w, http.StatusBadRequest, "Failed to parse new DHCP config json: %s", err)
|
||||
return
|
||||
}
|
||||
|
||||
err = dhcpServer.CheckConfig(newconfig.ServerConfig)
|
||||
if err != nil {
|
||||
httpError(w, http.StatusBadRequest, "Invalid DHCP configuration: %s", err)
|
||||
return
|
||||
}
|
||||
|
||||
err = dhcpServer.Stop()
|
||||
if err != nil {
|
||||
log.Error("failed to stop the DHCP server: %s", err)
|
||||
}
|
||||
|
||||
err = dhcpServer.Init(newconfig.ServerConfig)
|
||||
if err != nil {
|
||||
httpError(w, http.StatusBadRequest, "Invalid DHCP configuration: %s", err)
|
||||
return
|
||||
}
|
||||
|
||||
if newconfig.Enabled {
|
||||
|
||||
staticIP, err := hasStaticIP(newconfig.InterfaceName)
|
||||
@@ -72,14 +105,14 @@ func handleDHCPSetConfig(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
}
|
||||
|
||||
err = dhcpServer.Start(&newconfig)
|
||||
err = dhcpServer.Start()
|
||||
if err != nil {
|
||||
httpError(w, http.StatusBadRequest, "Failed to start DHCP server: %s", err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
config.DHCP = newconfig
|
||||
config.DHCP = newconfig.ServerConfig
|
||||
httpUpdateConfigReloadDNSReturnOK(w, r)
|
||||
}
|
||||
|
||||
@@ -333,12 +366,80 @@ func setStaticIP(ifaceName string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func handleDHCPAddStaticLease(w http.ResponseWriter, r *http.Request) {
|
||||
log.Tracef("%s %v", r.Method, r.URL)
|
||||
|
||||
lj := leaseJSON{}
|
||||
err := json.NewDecoder(r.Body).Decode(&lj)
|
||||
if err != nil {
|
||||
httpError(w, http.StatusBadRequest, "json.Decode: %s", err)
|
||||
return
|
||||
}
|
||||
|
||||
ip := parseIPv4(lj.IP)
|
||||
if ip == nil {
|
||||
httpError(w, http.StatusBadRequest, "invalid IP")
|
||||
return
|
||||
}
|
||||
|
||||
mac, _ := net.ParseMAC(lj.HWAddr)
|
||||
|
||||
lease := dhcpd.Lease{
|
||||
IP: ip,
|
||||
HWAddr: mac,
|
||||
Hostname: lj.Hostname,
|
||||
}
|
||||
err = dhcpServer.AddStaticLease(lease)
|
||||
if err != nil {
|
||||
httpError(w, http.StatusBadRequest, "%s", err)
|
||||
return
|
||||
}
|
||||
returnOK(w)
|
||||
}
|
||||
|
||||
func handleDHCPRemoveStaticLease(w http.ResponseWriter, r *http.Request) {
|
||||
log.Tracef("%s %v", r.Method, r.URL)
|
||||
|
||||
lj := leaseJSON{}
|
||||
err := json.NewDecoder(r.Body).Decode(&lj)
|
||||
if err != nil {
|
||||
httpError(w, http.StatusBadRequest, "json.Decode: %s", err)
|
||||
return
|
||||
}
|
||||
|
||||
ip := parseIPv4(lj.IP)
|
||||
if ip == nil {
|
||||
httpError(w, http.StatusBadRequest, "invalid IP")
|
||||
return
|
||||
}
|
||||
|
||||
mac, _ := net.ParseMAC(lj.HWAddr)
|
||||
|
||||
lease := dhcpd.Lease{
|
||||
IP: ip,
|
||||
HWAddr: mac,
|
||||
Hostname: lj.Hostname,
|
||||
}
|
||||
err = dhcpServer.RemoveStaticLease(lease)
|
||||
if err != nil {
|
||||
httpError(w, http.StatusBadRequest, "%s", err)
|
||||
return
|
||||
}
|
||||
returnOK(w)
|
||||
}
|
||||
|
||||
func startDHCPServer() error {
|
||||
if !config.DHCP.Enabled {
|
||||
// not enabled, don't do anything
|
||||
return nil
|
||||
}
|
||||
err := dhcpServer.Start(&config.DHCP)
|
||||
|
||||
err := dhcpServer.Init(config.DHCP)
|
||||
if err != nil {
|
||||
return errorx.Decorate(err, "Couldn't init DHCP server")
|
||||
}
|
||||
|
||||
err = dhcpServer.Start()
|
||||
if err != nil {
|
||||
return errorx.Decorate(err, "Couldn't start DHCP server")
|
||||
}
|
||||
@@ -350,10 +451,6 @@ func stopDHCPServer() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
if !dhcpServer.Enabled {
|
||||
return nil
|
||||
}
|
||||
|
||||
err := dhcpServer.Stop()
|
||||
if err != nil {
|
||||
return errorx.Decorate(err, "Couldn't stop DHCP server")
|
||||
|
||||
19
dhcpd/db.go
19
dhcpd/db.go
@@ -23,8 +23,20 @@ type leaseJSON struct {
|
||||
Expiry int64 `json:"exp"`
|
||||
}
|
||||
|
||||
// Safe version of dhcp4.IPInRange()
|
||||
func ipInRange(start, stop, ip net.IP) bool {
|
||||
if len(start) != len(stop) ||
|
||||
len(start) != len(ip) {
|
||||
return false
|
||||
}
|
||||
return dhcp4.IPInRange(start, stop, ip)
|
||||
}
|
||||
|
||||
// Load lease table from DB
|
||||
func (s *Server) dbLoad() {
|
||||
s.leases = nil
|
||||
s.IPpool = make(map[[4]byte]net.HardwareAddr)
|
||||
|
||||
data, err := ioutil.ReadFile(dbFilename)
|
||||
if err != nil {
|
||||
if !os.IsNotExist(err) {
|
||||
@@ -40,13 +52,12 @@ func (s *Server) dbLoad() {
|
||||
return
|
||||
}
|
||||
|
||||
s.leases = nil
|
||||
s.IPpool = make(map[[4]byte]net.HardwareAddr)
|
||||
|
||||
numLeases := len(obj)
|
||||
for i := range obj {
|
||||
|
||||
if !dhcp4.IPInRange(s.leaseStart, s.leaseStop, obj[i].IP) {
|
||||
if obj[i].Expiry != leaseExpireStatic &&
|
||||
!ipInRange(s.leaseStart, s.leaseStop, obj[i].IP) {
|
||||
|
||||
log.Tracef("Skipping a lease with IP %s: not within current IP range", obj[i].IP)
|
||||
continue
|
||||
}
|
||||
|
||||
206
dhcpd/dhcpd.go
206
dhcpd/dhcpd.go
@@ -14,6 +14,7 @@ import (
|
||||
)
|
||||
|
||||
const defaultDiscoverTime = time.Second * 3
|
||||
const leaseExpireStatic = 1
|
||||
|
||||
// Lease contains the necessary information about a DHCP lease
|
||||
// field ordering is important -- yaml fields will mirror ordering from here
|
||||
@@ -21,7 +22,10 @@ type Lease struct {
|
||||
HWAddr net.HardwareAddr `json:"mac" yaml:"hwaddr"`
|
||||
IP net.IP `json:"ip"`
|
||||
Hostname string `json:"hostname"`
|
||||
Expiry time.Time `json:"expires"`
|
||||
|
||||
// Lease expiration time
|
||||
// 1: static lease
|
||||
Expiry time.Time `json:"expires"`
|
||||
}
|
||||
|
||||
// ServerConfig - DHCP server configuration
|
||||
@@ -53,6 +57,7 @@ type Server struct {
|
||||
|
||||
// leases
|
||||
leases []*Lease
|
||||
leasesLock sync.RWMutex
|
||||
leaseStart net.IP // parsed from config RangeStart
|
||||
leaseStop net.IP // parsed from config RangeEnd
|
||||
leaseTime time.Duration // parsed from config LeaseDuration
|
||||
@@ -61,8 +66,7 @@ type Server struct {
|
||||
// IP address pool -- if entry is in the pool, then it's attached to a lease
|
||||
IPpool map[[4]byte]net.HardwareAddr
|
||||
|
||||
ServerConfig
|
||||
sync.RWMutex
|
||||
conf ServerConfig
|
||||
}
|
||||
|
||||
// Print information about the available network interfaces
|
||||
@@ -75,62 +79,65 @@ func printInterfaces() {
|
||||
log.Info("Available network interfaces: %s", buf.String())
|
||||
}
|
||||
|
||||
// Start will listen on port 67 and serve DHCP requests.
|
||||
// Even though config can be nil, it is not optional (at least for now), since there are no default values (yet).
|
||||
func (s *Server) Start(config *ServerConfig) error {
|
||||
if config != nil {
|
||||
s.ServerConfig = *config
|
||||
}
|
||||
// CheckConfig checks the configuration
|
||||
func (s *Server) CheckConfig(config ServerConfig) error {
|
||||
tmpServer := Server{}
|
||||
return tmpServer.setConfig(config)
|
||||
}
|
||||
|
||||
iface, err := net.InterfaceByName(s.InterfaceName)
|
||||
// Init checks the configuration and initializes the server
|
||||
func (s *Server) Init(config ServerConfig) error {
|
||||
err := s.setConfig(config)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
s.dbLoad()
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Server) setConfig(config ServerConfig) error {
|
||||
s.conf = config
|
||||
|
||||
iface, err := net.InterfaceByName(config.InterfaceName)
|
||||
if err != nil {
|
||||
s.closeConn() // in case it was already started
|
||||
printInterfaces()
|
||||
return wrapErrPrint(err, "Couldn't find interface by name %s", s.InterfaceName)
|
||||
return wrapErrPrint(err, "Couldn't find interface by name %s", config.InterfaceName)
|
||||
}
|
||||
|
||||
// get ipv4 address of an interface
|
||||
s.ipnet = getIfaceIPv4(iface)
|
||||
if s.ipnet == nil {
|
||||
s.closeConn() // in case it was already started
|
||||
return wrapErrPrint(err, "Couldn't find IPv4 address of interface %s %+v", s.InterfaceName, iface)
|
||||
return wrapErrPrint(err, "Couldn't find IPv4 address of interface %s %+v", config.InterfaceName, iface)
|
||||
}
|
||||
|
||||
if s.LeaseDuration == 0 {
|
||||
if config.LeaseDuration == 0 {
|
||||
s.leaseTime = time.Hour * 2
|
||||
s.LeaseDuration = uint(s.leaseTime.Seconds())
|
||||
} else {
|
||||
s.leaseTime = time.Second * time.Duration(s.LeaseDuration)
|
||||
s.leaseTime = time.Second * time.Duration(config.LeaseDuration)
|
||||
}
|
||||
|
||||
s.leaseStart, err = parseIPv4(s.RangeStart)
|
||||
s.leaseStart, err = parseIPv4(config.RangeStart)
|
||||
if err != nil {
|
||||
|
||||
s.closeConn() // in case it was already started
|
||||
return wrapErrPrint(err, "Failed to parse range start address %s", s.RangeStart)
|
||||
return wrapErrPrint(err, "Failed to parse range start address %s", config.RangeStart)
|
||||
}
|
||||
|
||||
s.leaseStop, err = parseIPv4(s.RangeEnd)
|
||||
s.leaseStop, err = parseIPv4(config.RangeEnd)
|
||||
if err != nil {
|
||||
s.closeConn() // in case it was already started
|
||||
return wrapErrPrint(err, "Failed to parse range end address %s", s.RangeEnd)
|
||||
return wrapErrPrint(err, "Failed to parse range end address %s", config.RangeEnd)
|
||||
}
|
||||
|
||||
subnet, err := parseIPv4(s.SubnetMask)
|
||||
subnet, err := parseIPv4(config.SubnetMask)
|
||||
if err != nil {
|
||||
s.closeConn() // in case it was already started
|
||||
return wrapErrPrint(err, "Failed to parse subnet mask %s", s.SubnetMask)
|
||||
return wrapErrPrint(err, "Failed to parse subnet mask %s", config.SubnetMask)
|
||||
}
|
||||
|
||||
// if !bytes.Equal(subnet, s.ipnet.Mask) {
|
||||
// s.closeConn() // in case it was already started
|
||||
// return wrapErrPrint(err, "specified subnet mask %s does not meatch interface %s subnet mask %s", s.SubnetMask, s.InterfaceName, s.ipnet.Mask)
|
||||
// }
|
||||
|
||||
router, err := parseIPv4(s.GatewayIP)
|
||||
router, err := parseIPv4(config.GatewayIP)
|
||||
if err != nil {
|
||||
s.closeConn() // in case it was already started
|
||||
return wrapErrPrint(err, "Failed to parse gateway IP %s", s.GatewayIP)
|
||||
return wrapErrPrint(err, "Failed to parse gateway IP %s", config.GatewayIP)
|
||||
}
|
||||
|
||||
s.leaseOptions = dhcp4.Options{
|
||||
@@ -139,12 +146,21 @@ func (s *Server) Start(config *ServerConfig) error {
|
||||
dhcp4.OptionDomainNameServer: s.ipnet.IP,
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Start will listen on port 67 and serve DHCP requests.
|
||||
func (s *Server) Start() error {
|
||||
|
||||
// TODO: don't close if interface and addresses are the same
|
||||
if s.conn != nil {
|
||||
s.closeConn()
|
||||
}
|
||||
|
||||
s.dbLoad()
|
||||
iface, err := net.InterfaceByName(s.conf.InterfaceName)
|
||||
if err != nil {
|
||||
return wrapErrPrint(err, "Couldn't find interface by name %s", s.conf.InterfaceName)
|
||||
}
|
||||
|
||||
c, err := newFilterConn(*iface, ":67") // it has to be bound to 0.0.0.0:67, otherwise it won't see DHCP discover/request packets
|
||||
if err != nil {
|
||||
@@ -229,9 +245,9 @@ func (s *Server) reserveLease(p dhcp4.Packet) (*Lease, error) {
|
||||
log.Tracef("Assigning IP address %s to %s (lease for %s expired at %s)",
|
||||
s.leases[i].IP, hwaddr, s.leases[i].HWAddr, s.leases[i].Expiry)
|
||||
lease.IP = s.leases[i].IP
|
||||
s.Lock()
|
||||
s.leasesLock.Lock()
|
||||
s.leases[i] = lease
|
||||
s.Unlock()
|
||||
s.leasesLock.Unlock()
|
||||
|
||||
s.reserveIP(lease.IP, hwaddr)
|
||||
return lease, nil
|
||||
@@ -239,9 +255,9 @@ func (s *Server) reserveLease(p dhcp4.Packet) (*Lease, error) {
|
||||
|
||||
log.Tracef("Assigning to %s IP address %s", hwaddr, ip.String())
|
||||
lease.IP = ip
|
||||
s.Lock()
|
||||
s.leasesLock.Lock()
|
||||
s.leases = append(s.leases, lease)
|
||||
s.Unlock()
|
||||
s.leasesLock.Unlock()
|
||||
return lease, nil
|
||||
}
|
||||
|
||||
@@ -261,7 +277,7 @@ func (s *Server) findLease(p dhcp4.Packet) *Lease {
|
||||
func (s *Server) findExpiredLease() int {
|
||||
now := time.Now().Unix()
|
||||
for i, lease := range s.leases {
|
||||
if lease.Expiry.Unix() <= now {
|
||||
if lease.Expiry.Unix() <= now && lease.Expiry.Unix() != leaseExpireStatic {
|
||||
return i
|
||||
}
|
||||
}
|
||||
@@ -269,11 +285,6 @@ func (s *Server) findExpiredLease() int {
|
||||
}
|
||||
|
||||
func (s *Server) findFreeIP(hwaddr net.HardwareAddr) (net.IP, error) {
|
||||
// if IP pool is nil, lazy initialize it
|
||||
if s.IPpool == nil {
|
||||
s.IPpool = make(map[[4]byte]net.HardwareAddr)
|
||||
}
|
||||
|
||||
// go from start to end, find unreserved IP
|
||||
var foundIP net.IP
|
||||
for i := 0; i < dhcp4.IPRange(s.leaseStart, s.leaseStop); i++ {
|
||||
@@ -361,7 +372,7 @@ func (s *Server) ServeDHCP(p dhcp4.Packet, msgType dhcp4.MessageType, options dh
|
||||
// Return TRUE if it doesn't reply, which probably means that the IP is available
|
||||
func (s *Server) addrAvailable(target net.IP) bool {
|
||||
|
||||
if s.ICMPTimeout == 0 {
|
||||
if s.conf.ICMPTimeout == 0 {
|
||||
return true
|
||||
}
|
||||
|
||||
@@ -372,7 +383,7 @@ func (s *Server) addrAvailable(target net.IP) bool {
|
||||
}
|
||||
|
||||
pinger.SetPrivileged(true)
|
||||
pinger.Timeout = time.Duration(s.ICMPTimeout) * time.Millisecond
|
||||
pinger.Timeout = time.Duration(s.conf.ICMPTimeout) * time.Millisecond
|
||||
pinger.Count = 1
|
||||
reply := false
|
||||
pinger.OnRecv = func(pkt *ping.Packet) {
|
||||
@@ -395,11 +406,11 @@ func (s *Server) addrAvailable(target net.IP) bool {
|
||||
func (s *Server) blacklistLease(lease *Lease) {
|
||||
hw := make(net.HardwareAddr, 6)
|
||||
s.reserveIP(lease.IP, hw)
|
||||
s.Lock()
|
||||
s.leasesLock.Lock()
|
||||
lease.HWAddr = hw
|
||||
lease.Hostname = ""
|
||||
lease.Expiry = time.Now().Add(s.leaseTime)
|
||||
s.Unlock()
|
||||
s.leasesLock.Unlock()
|
||||
}
|
||||
|
||||
// Return TRUE if DHCP packet is correct
|
||||
@@ -516,21 +527,103 @@ func (s *Server) handleDecline(p dhcp4.Packet, options dhcp4.Options) dhcp4.Pack
|
||||
return nil
|
||||
}
|
||||
|
||||
// AddStaticLease adds a static lease (thread-safe)
|
||||
func (s *Server) AddStaticLease(l Lease) error {
|
||||
if s.IPpool == nil {
|
||||
return fmt.Errorf("DHCP server isn't started")
|
||||
}
|
||||
|
||||
if len(l.IP) != 4 {
|
||||
return fmt.Errorf("Invalid IP")
|
||||
}
|
||||
if len(l.HWAddr) != 6 {
|
||||
return fmt.Errorf("Invalid MAC")
|
||||
}
|
||||
l.Expiry = time.Unix(leaseExpireStatic, 0)
|
||||
|
||||
s.leasesLock.Lock()
|
||||
defer s.leasesLock.Unlock()
|
||||
|
||||
if s.findReservedHWaddr(l.IP) != nil {
|
||||
return fmt.Errorf("IP is already used")
|
||||
}
|
||||
s.leases = append(s.leases, &l)
|
||||
s.reserveIP(l.IP, l.HWAddr)
|
||||
s.dbStore()
|
||||
return nil
|
||||
}
|
||||
|
||||
// RemoveStaticLease removes a static lease (thread-safe)
|
||||
func (s *Server) RemoveStaticLease(l Lease) error {
|
||||
if s.IPpool == nil {
|
||||
return fmt.Errorf("DHCP server isn't started")
|
||||
}
|
||||
|
||||
if len(l.IP) != 4 {
|
||||
return fmt.Errorf("Invalid IP")
|
||||
}
|
||||
if len(l.HWAddr) != 6 {
|
||||
return fmt.Errorf("Invalid MAC")
|
||||
}
|
||||
|
||||
s.leasesLock.Lock()
|
||||
defer s.leasesLock.Unlock()
|
||||
|
||||
if s.findReservedHWaddr(l.IP) == nil {
|
||||
return fmt.Errorf("Lease not found")
|
||||
}
|
||||
|
||||
var newLeases []*Lease
|
||||
for _, lease := range s.leases {
|
||||
if bytes.Equal(lease.IP.To4(), l.IP) {
|
||||
if !bytes.Equal(lease.HWAddr, l.HWAddr) ||
|
||||
lease.Hostname != l.Hostname {
|
||||
return fmt.Errorf("Lease not found")
|
||||
}
|
||||
continue
|
||||
}
|
||||
newLeases = append(newLeases, lease)
|
||||
}
|
||||
s.leases = newLeases
|
||||
s.unreserveIP(l.IP)
|
||||
s.dbStore()
|
||||
return nil
|
||||
}
|
||||
|
||||
// Leases returns the list of current DHCP leases (thread-safe)
|
||||
func (s *Server) Leases() []Lease {
|
||||
var result []Lease
|
||||
now := time.Now().Unix()
|
||||
s.RLock()
|
||||
s.leasesLock.RLock()
|
||||
for _, lease := range s.leases {
|
||||
if lease.Expiry.Unix() > now {
|
||||
result = append(result, *lease)
|
||||
}
|
||||
}
|
||||
s.RUnlock()
|
||||
s.leasesLock.RUnlock()
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
// StaticLeases returns the list of statically-configured DHCP leases (thread-safe)
|
||||
func (s *Server) StaticLeases() []Lease {
|
||||
s.leasesLock.Lock()
|
||||
if s.IPpool == nil {
|
||||
s.dbLoad()
|
||||
}
|
||||
s.leasesLock.Unlock()
|
||||
|
||||
var result []Lease
|
||||
s.leasesLock.RLock()
|
||||
for _, lease := range s.leases {
|
||||
if lease.Expiry.Unix() == 1 {
|
||||
result = append(result, *lease)
|
||||
}
|
||||
}
|
||||
s.leasesLock.RUnlock()
|
||||
return result
|
||||
}
|
||||
|
||||
// Print information about the current leases
|
||||
func (s *Server) printLeases() {
|
||||
log.Tracef("Leases:")
|
||||
@@ -540,10 +633,23 @@ func (s *Server) printLeases() {
|
||||
}
|
||||
}
|
||||
|
||||
// FindIPbyMAC finds an IP address by MAC address in the currently active DHCP leases
|
||||
func (s *Server) FindIPbyMAC(mac net.HardwareAddr) net.IP {
|
||||
now := time.Now().Unix()
|
||||
s.leasesLock.RLock()
|
||||
defer s.leasesLock.RUnlock()
|
||||
for _, l := range s.leases {
|
||||
if l.Expiry.Unix() > now && bytes.Equal(mac, l.HWAddr) {
|
||||
return l.IP
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Reset internal state
|
||||
func (s *Server) reset() {
|
||||
s.Lock()
|
||||
s.leasesLock.Lock()
|
||||
s.leases = nil
|
||||
s.Unlock()
|
||||
s.leasesLock.Unlock()
|
||||
s.IPpool = make(map[[4]byte]net.HardwareAddr)
|
||||
}
|
||||
|
||||
@@ -26,6 +26,7 @@ func TestDHCP(t *testing.T) {
|
||||
var lease *Lease
|
||||
var opt dhcp4.Options
|
||||
|
||||
s.reset()
|
||||
s.leaseStart = []byte{1, 1, 1, 1}
|
||||
s.leaseStop = []byte{1, 1, 1, 2}
|
||||
s.leaseTime = 5 * time.Second
|
||||
@@ -132,6 +133,7 @@ func TestDB(t *testing.T) {
|
||||
var hw1, hw2 net.HardwareAddr
|
||||
var lease *Lease
|
||||
|
||||
s.reset()
|
||||
s.leaseStart = []byte{1, 1, 1, 1}
|
||||
s.leaseStop = []byte{1, 1, 1, 2}
|
||||
s.leaseTime = 5 * time.Second
|
||||
|
||||
@@ -54,7 +54,11 @@ func main() {
|
||||
GatewayIP: "192.168.7.1",
|
||||
}
|
||||
log.Printf("Starting DHCP server")
|
||||
err = server.Start(&config)
|
||||
err = server.Init(config)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
err = server.Start()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
@@ -66,12 +70,12 @@ func main() {
|
||||
panic(err)
|
||||
}
|
||||
log.Printf("Starting DHCP server")
|
||||
err = server.Start(&config)
|
||||
err = server.Start()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
log.Printf("Starting DHCP server while it's already running")
|
||||
err = server.Start(&config)
|
||||
err = server.Start()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
175
dns.go
175
dns.go
@@ -4,16 +4,36 @@ import (
|
||||
"fmt"
|
||||
"net"
|
||||
"os"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/AdguardTeam/AdGuardHome/dnsfilter"
|
||||
"github.com/AdguardTeam/AdGuardHome/dnsforward"
|
||||
"github.com/AdguardTeam/dnsproxy/proxy"
|
||||
"github.com/AdguardTeam/dnsproxy/upstream"
|
||||
"github.com/AdguardTeam/golibs/log"
|
||||
"github.com/joomcode/errorx"
|
||||
"github.com/miekg/dns"
|
||||
)
|
||||
|
||||
var dnsServer *dnsforward.Server
|
||||
|
||||
const (
|
||||
rdnsTimeout = 3 * time.Second // max time to wait for rDNS response
|
||||
)
|
||||
|
||||
type dnsContext struct {
|
||||
rdnsChannel chan string // pass data from DNS request handling thread to rDNS thread
|
||||
// contains IP addresses of clients to be resolved by rDNS
|
||||
// if IP address couldn't be resolved, it stays here forever to prevent further attempts to resolve the same IP
|
||||
rdnsIP map[string]bool
|
||||
rdnsLock sync.Mutex // synchronize access to rdnsIP
|
||||
upstream upstream.Upstream // Upstream object for our own DNS server
|
||||
}
|
||||
|
||||
var dnsctx dnsContext
|
||||
|
||||
// initDNSServer creates an instance of the dnsforward.Server
|
||||
// Please note that we must do it even if we don't start it
|
||||
// so that we had access to the query log and the stats
|
||||
@@ -24,23 +44,141 @@ func initDNSServer(baseDir string) {
|
||||
}
|
||||
|
||||
dnsServer = dnsforward.NewServer(baseDir)
|
||||
|
||||
bindhost := config.DNS.BindHost
|
||||
if config.DNS.BindHost == "0.0.0.0" {
|
||||
bindhost = "127.0.0.1"
|
||||
}
|
||||
resolverAddress := fmt.Sprintf("%s:%d", bindhost, config.DNS.Port)
|
||||
opts := upstream.Options{
|
||||
Timeout: rdnsTimeout,
|
||||
}
|
||||
dnsctx.upstream, err = upstream.AddressToUpstream(resolverAddress, opts)
|
||||
if err != nil {
|
||||
log.Error("upstream.AddressToUpstream: %s", err)
|
||||
return
|
||||
}
|
||||
dnsctx.rdnsIP = make(map[string]bool)
|
||||
dnsctx.rdnsChannel = make(chan string, 256)
|
||||
go asyncRDNSLoop()
|
||||
}
|
||||
|
||||
func isRunning() bool {
|
||||
return dnsServer != nil && dnsServer.IsRunning()
|
||||
}
|
||||
|
||||
func beginAsyncRDNS(ip string) {
|
||||
if clientExists(ip) {
|
||||
return
|
||||
}
|
||||
|
||||
// add IP to rdnsIP, if not exists
|
||||
dnsctx.rdnsLock.Lock()
|
||||
defer dnsctx.rdnsLock.Unlock()
|
||||
_, ok := dnsctx.rdnsIP[ip]
|
||||
if ok {
|
||||
return
|
||||
}
|
||||
dnsctx.rdnsIP[ip] = true
|
||||
|
||||
log.Tracef("Adding %s for rDNS resolve", ip)
|
||||
select {
|
||||
case dnsctx.rdnsChannel <- ip:
|
||||
//
|
||||
default:
|
||||
log.Tracef("rDNS queue is full")
|
||||
}
|
||||
}
|
||||
|
||||
// Use rDNS to get hostname by IP address
|
||||
func resolveRDNS(ip string) string {
|
||||
log.Tracef("Resolving host for %s", ip)
|
||||
|
||||
req := dns.Msg{}
|
||||
req.Id = dns.Id()
|
||||
req.RecursionDesired = true
|
||||
req.Question = []dns.Question{
|
||||
{
|
||||
Qtype: dns.TypePTR,
|
||||
Qclass: dns.ClassINET,
|
||||
},
|
||||
}
|
||||
var err error
|
||||
req.Question[0].Name, err = dns.ReverseAddr(ip)
|
||||
if err != nil {
|
||||
log.Debug("Error while calling dns.ReverseAddr(%s): %s", ip, err)
|
||||
return ""
|
||||
}
|
||||
|
||||
resp, err := dnsctx.upstream.Exchange(&req)
|
||||
if err != nil {
|
||||
log.Error("Error while making an rDNS lookup for %s: %s", ip, err)
|
||||
return ""
|
||||
}
|
||||
if len(resp.Answer) != 1 {
|
||||
log.Debug("No answer for rDNS lookup of %s", ip)
|
||||
return ""
|
||||
}
|
||||
ptr, ok := resp.Answer[0].(*dns.PTR)
|
||||
if !ok {
|
||||
log.Error("not a PTR response for %s", ip)
|
||||
return ""
|
||||
}
|
||||
|
||||
log.Tracef("PTR response for %s: %s", ip, ptr.String())
|
||||
if strings.HasSuffix(ptr.Ptr, ".") {
|
||||
ptr.Ptr = ptr.Ptr[:len(ptr.Ptr)-1]
|
||||
}
|
||||
|
||||
return ptr.Ptr
|
||||
}
|
||||
|
||||
// Wait for a signal and then synchronously resolve hostname by IP address
|
||||
// Add the hostname:IP pair to "Clients" array
|
||||
func asyncRDNSLoop() {
|
||||
for {
|
||||
var ip string
|
||||
ip = <-dnsctx.rdnsChannel
|
||||
|
||||
host := resolveRDNS(ip)
|
||||
if len(host) == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
dnsctx.rdnsLock.Lock()
|
||||
delete(dnsctx.rdnsIP, ip)
|
||||
dnsctx.rdnsLock.Unlock()
|
||||
|
||||
_, _ = clientAddHost(ip, host, ClientSourceRDNS)
|
||||
}
|
||||
}
|
||||
|
||||
func onDNSRequest(d *proxy.DNSContext) {
|
||||
qType := d.Req.Question[0].Qtype
|
||||
if qType != dns.TypeA && qType != dns.TypeAAAA {
|
||||
return
|
||||
}
|
||||
|
||||
ip := dnsforward.GetIPString(d.Addr)
|
||||
if ip == "" {
|
||||
// This would be quite weird if we get here
|
||||
return
|
||||
}
|
||||
|
||||
beginAsyncRDNS(ip)
|
||||
}
|
||||
|
||||
func generateServerConfig() dnsforward.ServerConfig {
|
||||
filters := []dnsfilter.Filter{}
|
||||
userFilter := userFilter()
|
||||
filters = append(filters, dnsfilter.Filter{
|
||||
ID: userFilter.ID,
|
||||
Rules: userFilter.Rules,
|
||||
ID: userFilter.ID,
|
||||
Data: userFilter.Data,
|
||||
})
|
||||
for _, filter := range config.Filters {
|
||||
filters = append(filters, dnsfilter.Filter{
|
||||
ID: filter.ID,
|
||||
Rules: filter.Rules,
|
||||
ID: filter.ID,
|
||||
Data: filter.Data,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -70,9 +208,33 @@ func generateServerConfig() dnsforward.ServerConfig {
|
||||
newconfig.Upstreams = upstreamConfig.Upstreams
|
||||
newconfig.DomainsReservedUpstreams = upstreamConfig.DomainReservedUpstreams
|
||||
newconfig.AllServers = config.DNS.AllServers
|
||||
newconfig.FilterHandler = applyClientSettings
|
||||
newconfig.OnDNSRequest = onDNSRequest
|
||||
return newconfig
|
||||
}
|
||||
|
||||
// If a client has his own settings, apply them
|
||||
func applyClientSettings(clientAddr string, setts *dnsfilter.RequestFilteringSettings) {
|
||||
c, ok := clientFind(clientAddr)
|
||||
if !ok || !c.UseOwnSettings {
|
||||
return
|
||||
}
|
||||
|
||||
log.Debug("Using settings for client with IP %s", clientAddr)
|
||||
if !c.FilteringEnabled {
|
||||
setts.FilteringEnabled = false
|
||||
}
|
||||
if !c.SafeSearchEnabled {
|
||||
setts.SafeSearchEnabled = false
|
||||
}
|
||||
if !c.SafeBrowsingEnabled {
|
||||
setts.SafeBrowsingEnabled = false
|
||||
}
|
||||
if !c.ParentalEnabled {
|
||||
setts.ParentalEnabled = false
|
||||
}
|
||||
}
|
||||
|
||||
func startDNSServer() error {
|
||||
if isRunning() {
|
||||
return fmt.Errorf("unable to start forwarding DNS server: Already running")
|
||||
@@ -84,6 +246,11 @@ func startDNSServer() error {
|
||||
return errorx.Decorate(err, "Couldn't start forwarding DNS server")
|
||||
}
|
||||
|
||||
top := dnsServer.GetStatsTop()
|
||||
for k := range top.Clients {
|
||||
beginAsyncRDNS(k)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
13
dns_test.go
Normal file
13
dns_test.go
Normal file
@@ -0,0 +1,13 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestResolveRDNS(t *testing.T) {
|
||||
config.DNS.BindHost = "1.1.1.1"
|
||||
initDNSServer(".")
|
||||
if r := resolveRDNS("1.1.1.1"); r != "one.one.one.one" {
|
||||
t.Errorf("resolveRDNS(): %s", r)
|
||||
}
|
||||
}
|
||||
@@ -11,15 +11,15 @@ import (
|
||||
"io/ioutil"
|
||||
"net"
|
||||
"net/http"
|
||||
"regexp"
|
||||
"strings"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"github.com/AdguardTeam/dnsproxy/upstream"
|
||||
"github.com/AdguardTeam/golibs/log"
|
||||
"github.com/AdguardTeam/urlfilter"
|
||||
"github.com/bluele/gcache"
|
||||
"github.com/miekg/dns"
|
||||
"golang.org/x/net/publicsuffix"
|
||||
)
|
||||
|
||||
@@ -30,28 +30,31 @@ const defaultHTTPTimeout = 5 * time.Minute
|
||||
const defaultHTTPMaxIdleConnections = 100
|
||||
|
||||
const defaultSafebrowsingServer = "sb.adtidy.org"
|
||||
const defaultSafebrowsingURL = "http://%s/safebrowsing-lookup-hash.html?prefixes=%s"
|
||||
const defaultSafebrowsingURL = "%s://%s/safebrowsing-lookup-hash.html?prefixes=%s"
|
||||
const defaultParentalServer = "pctrl.adguard.com"
|
||||
const defaultParentalURL = "http://%s/check-parental-control-hash?prefixes=%s&sensitivity=%d"
|
||||
const defaultParentalURL = "%s://%s/check-parental-control-hash?prefixes=%s&sensitivity=%d"
|
||||
const maxDialCacheSize = 2 // the number of host names for safebrowsing and parental control
|
||||
|
||||
// ErrInvalidSyntax is returned by AddRule when the rule is invalid
|
||||
var ErrInvalidSyntax = errors.New("dnsfilter: invalid rule syntax")
|
||||
|
||||
// ErrAlreadyExists is returned by AddRule when the rule was already added to the filter
|
||||
var ErrAlreadyExists = errors.New("dnsfilter: rule was already added")
|
||||
|
||||
const shortcutLength = 6 // used for rule search optimization, 6 hits the sweet spot
|
||||
|
||||
const enableFastLookup = true // flag for debugging, must be true in production for faster performance
|
||||
const enableDelayedCompilation = true // flag for debugging, must be true in production for faster performance
|
||||
// Custom filtering settings
|
||||
type RequestFilteringSettings struct {
|
||||
FilteringEnabled bool
|
||||
SafeSearchEnabled bool
|
||||
SafeBrowsingEnabled bool
|
||||
ParentalEnabled bool
|
||||
}
|
||||
|
||||
// Config allows you to configure DNS filtering with New() or just change variables directly.
|
||||
type Config struct {
|
||||
ParentalSensitivity int `yaml:"parental_sensitivity"` // must be either 3, 10, 13 or 17
|
||||
ParentalEnabled bool `yaml:"parental_enabled"`
|
||||
SafeSearchEnabled bool `yaml:"safesearch_enabled"`
|
||||
SafeBrowsingEnabled bool `yaml:"safebrowsing_enabled"`
|
||||
ResolverAddress string // DNS server address
|
||||
FilteringTempFilename string `yaml:"filtering_temp_filename"` // temporary file for storing unused filtering rules
|
||||
ParentalSensitivity int `yaml:"parental_sensitivity"` // must be either 3, 10, 13 or 17
|
||||
ParentalEnabled bool `yaml:"parental_enabled"`
|
||||
UsePlainHTTP bool `yaml:"-"` // use plain HTTP for requests to parental and safe browsing servers
|
||||
SafeSearchEnabled bool `yaml:"safesearch_enabled"`
|
||||
SafeBrowsingEnabled bool `yaml:"safebrowsing_enabled"`
|
||||
ResolverAddress string // DNS server address
|
||||
|
||||
// Filtering callback function
|
||||
FilterHandler func(clientAddr string, settings *RequestFilteringSettings) `yaml:"-"`
|
||||
}
|
||||
|
||||
type privateConfig struct {
|
||||
@@ -59,33 +62,6 @@ type privateConfig struct {
|
||||
safeBrowsingServer string // access via methods
|
||||
}
|
||||
|
||||
type rule struct {
|
||||
text string // text without @@ decorators or $ options
|
||||
shortcut string // for speeding up lookup
|
||||
originalText string // original text for reporting back to applications
|
||||
ip net.IP // IP address (for the case when we're matching a hosts file)
|
||||
|
||||
// options
|
||||
options []string // optional options after $
|
||||
|
||||
// parsed options
|
||||
apps []string
|
||||
isWhitelist bool
|
||||
isImportant bool
|
||||
|
||||
// user-supplied data
|
||||
listID int64
|
||||
|
||||
// suffix matching
|
||||
isSuffix bool
|
||||
suffix string
|
||||
|
||||
// compiled regexp
|
||||
compiled *regexp.Regexp
|
||||
|
||||
sync.RWMutex
|
||||
}
|
||||
|
||||
// LookupStats store stats collected during safebrowsing or parental checks
|
||||
type LookupStats struct {
|
||||
Requests uint64 // number of HTTP requests that were sent
|
||||
@@ -103,13 +79,8 @@ type Stats struct {
|
||||
|
||||
// Dnsfilter holds added rules and performs hostname matches against the rules
|
||||
type Dnsfilter struct {
|
||||
storage map[string]bool // rule storage, not used for matching, just for filtering out duplicates
|
||||
storageMutex sync.RWMutex
|
||||
|
||||
// rules are checked against these lists in the order defined here
|
||||
important *rulesTable // more important than whitelist and is checked first
|
||||
whiteList *rulesTable // more important than blacklist
|
||||
blackList *rulesTable
|
||||
rulesStorage *urlfilter.RulesStorage
|
||||
filteringEngine *urlfilter.DNSEngine
|
||||
|
||||
// HTTP lookups for safebrowsing and parental
|
||||
client http.Client // handle for http client -- single instance as recommended by docs
|
||||
@@ -121,8 +92,8 @@ type Dnsfilter struct {
|
||||
|
||||
// Filter represents a filter list
|
||||
type Filter struct {
|
||||
ID int64 `json:"id"` // auto-assigned when filter is added (see nextFilterID), json by default keeps ID uppercase but we need lowercase
|
||||
Rules []string `json:"-" yaml:"-"` // not in yaml or json
|
||||
ID int64 `json:"id"` // auto-assigned when filter is added (see nextFilterID), json by default keeps ID uppercase but we need lowercase
|
||||
Data []byte `json:"-" yaml:"-"` // List of rules divided by '\n'
|
||||
}
|
||||
|
||||
//go:generate stringer -type=Reason
|
||||
@@ -157,13 +128,12 @@ const (
|
||||
// these variables need to survive coredns reload
|
||||
var (
|
||||
stats Stats
|
||||
dialCache gcache.Cache // "host" -> "IP" cache for safebrowsing and parental control servers
|
||||
safebrowsingCache gcache.Cache
|
||||
parentalCache gcache.Cache
|
||||
safeSearchCache gcache.Cache
|
||||
)
|
||||
|
||||
var resolverAddr string // DNS server address
|
||||
|
||||
// Result holds state of hostname check
|
||||
type Result struct {
|
||||
IsFiltered bool `json:",omitempty"` // True if the host name is filtered
|
||||
@@ -179,24 +149,41 @@ func (r Reason) Matched() bool {
|
||||
}
|
||||
|
||||
// CheckHost tries to match host against rules, then safebrowsing and parental if they are enabled
|
||||
func (d *Dnsfilter) CheckHost(host string) (Result, error) {
|
||||
func (d *Dnsfilter) CheckHost(host string, qtype uint16, clientAddr string) (Result, error) {
|
||||
// sometimes DNS clients will try to resolve ".", which is a request to get root servers
|
||||
if host == "" {
|
||||
return Result{Reason: NotFilteredNotFound}, nil
|
||||
}
|
||||
host = strings.ToLower(host)
|
||||
|
||||
// try filter lists first
|
||||
result, err := d.matchHost(host)
|
||||
if err != nil {
|
||||
return result, err
|
||||
// prevent recursion
|
||||
if host == d.parentalServer || host == d.safeBrowsingServer {
|
||||
return Result{}, nil
|
||||
}
|
||||
if result.Reason.Matched() {
|
||||
return result, nil
|
||||
|
||||
var setts RequestFilteringSettings
|
||||
setts.FilteringEnabled = true
|
||||
setts.SafeSearchEnabled = d.SafeSearchEnabled
|
||||
setts.SafeBrowsingEnabled = d.SafeBrowsingEnabled
|
||||
setts.ParentalEnabled = d.ParentalEnabled
|
||||
if len(clientAddr) != 0 && d.FilterHandler != nil {
|
||||
d.FilterHandler(clientAddr, &setts)
|
||||
}
|
||||
|
||||
var result Result
|
||||
var err error
|
||||
// try filter lists first
|
||||
if setts.FilteringEnabled {
|
||||
result, err = d.matchHost(host, qtype)
|
||||
if err != nil {
|
||||
return result, err
|
||||
}
|
||||
if result.Reason.Matched() {
|
||||
return result, nil
|
||||
}
|
||||
}
|
||||
|
||||
// check safeSearch if no match
|
||||
if d.SafeSearchEnabled {
|
||||
if setts.SafeSearchEnabled {
|
||||
result, err = d.checkSafeSearch(host)
|
||||
if err != nil {
|
||||
log.Printf("Failed to safesearch HTTP lookup, ignoring check: %v", err)
|
||||
@@ -209,7 +196,7 @@ func (d *Dnsfilter) CheckHost(host string) (Result, error) {
|
||||
}
|
||||
|
||||
// check safebrowsing if no match
|
||||
if d.SafeBrowsingEnabled {
|
||||
if setts.SafeBrowsingEnabled {
|
||||
result, err = d.checkSafeBrowsing(host)
|
||||
if err != nil {
|
||||
// failed to do HTTP lookup -- treat it as if we got empty response, but don't save cache
|
||||
@@ -222,7 +209,7 @@ func (d *Dnsfilter) CheckHost(host string) (Result, error) {
|
||||
}
|
||||
|
||||
// check parental if no match
|
||||
if d.ParentalEnabled {
|
||||
if setts.ParentalEnabled {
|
||||
result, err = d.checkParental(host)
|
||||
if err != nil {
|
||||
// failed to do HTTP lookup -- treat it as if we got empty response, but don't save cache
|
||||
@@ -238,308 +225,6 @@ func (d *Dnsfilter) CheckHost(host string) (Result, error) {
|
||||
return Result{}, nil
|
||||
}
|
||||
|
||||
//
|
||||
// rules table
|
||||
//
|
||||
|
||||
type rulesTable struct {
|
||||
rulesByHost map[string]*rule
|
||||
rulesByShortcut map[string][]*rule
|
||||
rulesLeftovers []*rule
|
||||
sync.RWMutex
|
||||
}
|
||||
|
||||
func newRulesTable() *rulesTable {
|
||||
return &rulesTable{
|
||||
rulesByHost: make(map[string]*rule),
|
||||
rulesByShortcut: make(map[string][]*rule),
|
||||
rulesLeftovers: make([]*rule, 0),
|
||||
}
|
||||
}
|
||||
|
||||
func (r *rulesTable) Add(rule *rule) {
|
||||
r.Lock()
|
||||
if rule.ip != nil {
|
||||
// Hosts syntax
|
||||
r.rulesByHost[rule.text] = rule
|
||||
} else if len(rule.shortcut) == shortcutLength && enableFastLookup {
|
||||
// Adblock syntax with a shortcut
|
||||
r.rulesByShortcut[rule.shortcut] = append(r.rulesByShortcut[rule.shortcut], rule)
|
||||
} else {
|
||||
// Adblock syntax -- too short to have a shortcut
|
||||
r.rulesLeftovers = append(r.rulesLeftovers, rule)
|
||||
}
|
||||
r.Unlock()
|
||||
}
|
||||
|
||||
func (r *rulesTable) matchByHost(host string) (Result, error) {
|
||||
// First: examine the hosts-syntax rules
|
||||
res, err := r.searchByHost(host)
|
||||
if err != nil {
|
||||
return res, err
|
||||
}
|
||||
if res.Reason.Matched() {
|
||||
return res, nil
|
||||
}
|
||||
|
||||
// Second: examine the adblock-syntax rules with shortcuts
|
||||
res, err = r.searchShortcuts(host)
|
||||
if err != nil {
|
||||
return res, err
|
||||
}
|
||||
if res.Reason.Matched() {
|
||||
return res, nil
|
||||
}
|
||||
|
||||
// Third: examine the others
|
||||
res, err = r.searchLeftovers(host)
|
||||
if err != nil {
|
||||
return res, err
|
||||
}
|
||||
if res.Reason.Matched() {
|
||||
return res, nil
|
||||
}
|
||||
|
||||
return Result{}, nil
|
||||
}
|
||||
|
||||
func (r *rulesTable) searchByHost(host string) (Result, error) {
|
||||
rule, ok := r.rulesByHost[host]
|
||||
|
||||
if ok {
|
||||
return rule.match(host)
|
||||
}
|
||||
|
||||
return Result{}, nil
|
||||
}
|
||||
|
||||
func (r *rulesTable) searchShortcuts(host string) (Result, error) {
|
||||
// check in shortcuts first
|
||||
for i := 0; i < len(host); i++ {
|
||||
shortcut := host[i:]
|
||||
if len(shortcut) > shortcutLength {
|
||||
shortcut = shortcut[:shortcutLength]
|
||||
}
|
||||
if len(shortcut) != shortcutLength {
|
||||
continue
|
||||
}
|
||||
rules, ok := r.rulesByShortcut[shortcut]
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
for _, rule := range rules {
|
||||
res, err := rule.match(host)
|
||||
// error? stop search
|
||||
if err != nil {
|
||||
return res, err
|
||||
}
|
||||
// matched? stop search
|
||||
if res.Reason.Matched() {
|
||||
return res, err
|
||||
}
|
||||
// continue otherwise
|
||||
}
|
||||
}
|
||||
return Result{}, nil
|
||||
}
|
||||
|
||||
func (r *rulesTable) searchLeftovers(host string) (Result, error) {
|
||||
for _, rule := range r.rulesLeftovers {
|
||||
res, err := rule.match(host)
|
||||
// error? stop search
|
||||
if err != nil {
|
||||
return res, err
|
||||
}
|
||||
// matched? stop search
|
||||
if res.Reason.Matched() {
|
||||
return res, err
|
||||
}
|
||||
// continue otherwise
|
||||
}
|
||||
return Result{}, nil
|
||||
}
|
||||
|
||||
func findOptionIndex(text string) int {
|
||||
for i, r := range text {
|
||||
// ignore non-$
|
||||
if r != '$' {
|
||||
continue
|
||||
}
|
||||
// ignore `\$`
|
||||
if i > 0 && text[i-1] == '\\' {
|
||||
continue
|
||||
}
|
||||
// ignore `$/`
|
||||
if i > len(text) && text[i+1] == '/' {
|
||||
continue
|
||||
}
|
||||
return i + 1
|
||||
}
|
||||
return -1
|
||||
}
|
||||
|
||||
func (rule *rule) extractOptions() error {
|
||||
optIndex := findOptionIndex(rule.text)
|
||||
if optIndex == 0 { // starts with $
|
||||
return ErrInvalidSyntax
|
||||
}
|
||||
if optIndex == len(rule.text) { // ends with $
|
||||
return ErrInvalidSyntax
|
||||
}
|
||||
if optIndex < 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
optionsStr := rule.text[optIndex:]
|
||||
rule.text = rule.text[:optIndex-1] // remove options from text
|
||||
|
||||
begin := 0
|
||||
i := 0
|
||||
for i = 0; i < len(optionsStr); i++ {
|
||||
switch optionsStr[i] {
|
||||
case ',':
|
||||
if i > 0 {
|
||||
// it might be escaped, if so, ignore
|
||||
if optionsStr[i-1] == '\\' {
|
||||
break // from switch, not for loop
|
||||
}
|
||||
}
|
||||
rule.options = append(rule.options, optionsStr[begin:i])
|
||||
begin = i + 1
|
||||
}
|
||||
}
|
||||
if begin != i {
|
||||
// there's still an option remaining
|
||||
rule.options = append(rule.options, optionsStr[begin:])
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (rule *rule) parseOptions() error {
|
||||
err := rule.extractOptions()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, option := range rule.options {
|
||||
switch {
|
||||
case option == "important":
|
||||
rule.isImportant = true
|
||||
case strings.HasPrefix(option, "app="):
|
||||
option = strings.TrimPrefix(option, "app=")
|
||||
rule.apps = strings.Split(option, "|")
|
||||
default:
|
||||
return ErrInvalidSyntax
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (rule *rule) extractShortcut() {
|
||||
// regex rules have no shortcuts
|
||||
if rule.text[0] == '/' && rule.text[len(rule.text)-1] == '/' {
|
||||
return
|
||||
}
|
||||
|
||||
fields := strings.FieldsFunc(rule.text, func(r rune) bool {
|
||||
switch r {
|
||||
case '*', '^', '|':
|
||||
return true
|
||||
}
|
||||
return false
|
||||
})
|
||||
longestField := ""
|
||||
for _, field := range fields {
|
||||
if len(field) > len(longestField) {
|
||||
longestField = field
|
||||
}
|
||||
}
|
||||
if len(longestField) > shortcutLength {
|
||||
longestField = longestField[:shortcutLength]
|
||||
}
|
||||
rule.shortcut = strings.ToLower(longestField)
|
||||
}
|
||||
|
||||
func (rule *rule) compile() error {
|
||||
rule.RLock()
|
||||
isCompiled := rule.isSuffix || rule.compiled != nil
|
||||
rule.RUnlock()
|
||||
if isCompiled {
|
||||
return nil
|
||||
}
|
||||
|
||||
isSuffix, suffix := getSuffix(rule.text)
|
||||
if isSuffix {
|
||||
rule.Lock()
|
||||
rule.isSuffix = isSuffix
|
||||
rule.suffix = suffix
|
||||
rule.Unlock()
|
||||
return nil
|
||||
}
|
||||
|
||||
expr, err := ruleToRegexp(rule.text)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
compiled, err := regexp.Compile(expr)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
rule.Lock()
|
||||
rule.compiled = compiled
|
||||
rule.Unlock()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Checks if the rule matches the specified host and returns a corresponding Result object
|
||||
func (rule *rule) match(host string) (Result, error) {
|
||||
res := Result{}
|
||||
|
||||
if rule.ip != nil && rule.text == host {
|
||||
// This is a hosts-syntax rule -- just check that the hostname matches and return the result
|
||||
return Result{
|
||||
IsFiltered: true,
|
||||
Reason: FilteredBlackList,
|
||||
Rule: rule.originalText,
|
||||
IP: rule.ip,
|
||||
FilterID: rule.listID,
|
||||
}, nil
|
||||
}
|
||||
|
||||
err := rule.compile()
|
||||
if err != nil {
|
||||
return res, err
|
||||
}
|
||||
rule.RLock()
|
||||
matched := false
|
||||
if rule.isSuffix {
|
||||
if host == rule.suffix {
|
||||
matched = true
|
||||
} else if strings.HasSuffix(host, "."+rule.suffix) {
|
||||
matched = true
|
||||
}
|
||||
} else {
|
||||
matched = rule.compiled.MatchString(host)
|
||||
}
|
||||
rule.RUnlock()
|
||||
if matched {
|
||||
res.Reason = FilteredBlackList
|
||||
res.IsFiltered = true
|
||||
res.FilterID = rule.listID
|
||||
res.Rule = rule.originalText
|
||||
if rule.isWhitelist {
|
||||
res.Reason = NotFilteredWhiteList
|
||||
res.IsFiltered = false
|
||||
}
|
||||
}
|
||||
return res, nil
|
||||
}
|
||||
|
||||
func getCachedReason(cache gcache.Cache, host string) (result Result, isFound bool, err error) {
|
||||
isFound = false // not found yet
|
||||
|
||||
@@ -674,12 +359,12 @@ func (d *Dnsfilter) checkSafeBrowsing(host string) (Result, error) {
|
||||
defer timer.LogElapsed("SafeBrowsing HTTP lookup for %s", host)
|
||||
}
|
||||
|
||||
// prevent recursion -- checking the host of safebrowsing server makes no sense
|
||||
if host == d.safeBrowsingServer {
|
||||
return Result{}, nil
|
||||
}
|
||||
format := func(hashparam string) string {
|
||||
url := fmt.Sprintf(defaultSafebrowsingURL, d.safeBrowsingServer, hashparam)
|
||||
schema := "https"
|
||||
if d.UsePlainHTTP {
|
||||
schema = "http"
|
||||
}
|
||||
url := fmt.Sprintf(defaultSafebrowsingURL, schema, d.safeBrowsingServer, hashparam)
|
||||
return url
|
||||
}
|
||||
handleBody := func(body []byte, hashes map[string]bool) (Result, error) {
|
||||
@@ -720,12 +405,12 @@ func (d *Dnsfilter) checkParental(host string) (Result, error) {
|
||||
defer timer.LogElapsed("Parental HTTP lookup for %s", host)
|
||||
}
|
||||
|
||||
// prevent recursion -- checking the host of parental safety server makes no sense
|
||||
if host == d.parentalServer {
|
||||
return Result{}, nil
|
||||
}
|
||||
format := func(hashparam string) string {
|
||||
url := fmt.Sprintf(defaultParentalURL, d.parentalServer, hashparam, d.ParentalSensitivity)
|
||||
schema := "https"
|
||||
if d.UsePlainHTTP {
|
||||
schema = "http"
|
||||
}
|
||||
url := fmt.Sprintf(defaultParentalURL, schema, d.parentalServer, hashparam, d.ParentalSensitivity)
|
||||
return url
|
||||
}
|
||||
handleBody := func(body []byte, hashes map[string]bool) (Result, error) {
|
||||
@@ -842,135 +527,77 @@ func (d *Dnsfilter) lookupCommon(host string, lookupstats *LookupStats, cache gc
|
||||
// Adding rule and matching against the rules
|
||||
//
|
||||
|
||||
// AddRules is a convinience function to add an array of filters in one call
|
||||
func (d *Dnsfilter) AddRules(filters []Filter) error {
|
||||
for _, f := range filters {
|
||||
for _, rule := range f.Rules {
|
||||
err := d.AddRule(rule, f.ID)
|
||||
if err == ErrAlreadyExists || err == ErrInvalidSyntax {
|
||||
continue
|
||||
}
|
||||
if err != nil {
|
||||
log.Printf("Cannot add rule %s: %s", rule, err)
|
||||
// Just ignore invalid rules
|
||||
continue
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// AddRule adds a rule, checking if it is a valid rule first and if it wasn't added already
|
||||
func (d *Dnsfilter) AddRule(input string, filterListID int64) error {
|
||||
input = strings.TrimSpace(input)
|
||||
d.storageMutex.RLock()
|
||||
_, exists := d.storage[input]
|
||||
d.storageMutex.RUnlock()
|
||||
if exists {
|
||||
// already added
|
||||
return ErrAlreadyExists
|
||||
}
|
||||
|
||||
if !isValidRule(input) {
|
||||
return ErrInvalidSyntax
|
||||
}
|
||||
|
||||
// First, check if this is a hosts-syntax rule
|
||||
if d.parseEtcHosts(input, filterListID) {
|
||||
// This is a valid hosts-syntax rule, no need for further parsing
|
||||
return nil
|
||||
}
|
||||
|
||||
// Start parsing the rule
|
||||
r := rule{
|
||||
text: input, // will be modified
|
||||
originalText: input,
|
||||
listID: filterListID,
|
||||
}
|
||||
|
||||
// Mark rule as whitelist if it starts with @@
|
||||
if strings.HasPrefix(r.text, "@@") {
|
||||
r.isWhitelist = true
|
||||
r.text = r.text[2:]
|
||||
}
|
||||
|
||||
err := r.parseOptions()
|
||||
// Initialize urlfilter objects
|
||||
func (d *Dnsfilter) initFiltering(filters map[int]string) error {
|
||||
var err error
|
||||
d.rulesStorage, err = urlfilter.NewRuleStorage(d.FilteringTempFilename)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
r.extractShortcut()
|
||||
|
||||
if !enableDelayedCompilation {
|
||||
err := r.compile()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
destination := d.blackList
|
||||
if r.isImportant {
|
||||
destination = d.important
|
||||
} else if r.isWhitelist {
|
||||
destination = d.whiteList
|
||||
}
|
||||
|
||||
d.storageMutex.Lock()
|
||||
d.storage[input] = true
|
||||
d.storageMutex.Unlock()
|
||||
destination.Add(&r)
|
||||
d.filteringEngine = urlfilter.NewDNSEngine(filters, d.rulesStorage)
|
||||
return nil
|
||||
}
|
||||
|
||||
// Parses the hosts-syntax rules. Returns false if the input string is not of hosts-syntax.
|
||||
func (d *Dnsfilter) parseEtcHosts(input string, filterListID int64) bool {
|
||||
// Strip the trailing comment
|
||||
ruleText := input
|
||||
if pos := strings.IndexByte(ruleText, '#'); pos != -1 {
|
||||
ruleText = ruleText[0:pos]
|
||||
}
|
||||
fields := strings.Fields(ruleText)
|
||||
if len(fields) < 2 {
|
||||
return false
|
||||
}
|
||||
addr := net.ParseIP(fields[0])
|
||||
if addr == nil {
|
||||
return false
|
||||
}
|
||||
|
||||
d.storageMutex.Lock()
|
||||
d.storage[input] = true
|
||||
d.storageMutex.Unlock()
|
||||
|
||||
for _, host := range fields[1:] {
|
||||
r := rule{
|
||||
text: host,
|
||||
originalText: input,
|
||||
listID: filterListID,
|
||||
ip: addr,
|
||||
}
|
||||
d.blackList.Add(&r)
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// matchHost is a low-level way to check only if hostname is filtered by rules, skipping expensive safebrowsing and parental lookups
|
||||
func (d *Dnsfilter) matchHost(host string) (Result, error) {
|
||||
lists := []*rulesTable{
|
||||
d.important,
|
||||
d.whiteList,
|
||||
d.blackList,
|
||||
func (d *Dnsfilter) matchHost(host string, qtype uint16) (Result, error) {
|
||||
if d.filteringEngine == nil {
|
||||
return Result{}, nil
|
||||
}
|
||||
|
||||
for _, table := range lists {
|
||||
res, err := table.matchByHost(host)
|
||||
if err != nil {
|
||||
return res, err
|
||||
}
|
||||
if res.Reason.Matched() {
|
||||
rules, ok := d.filteringEngine.Match(host)
|
||||
if !ok {
|
||||
return Result{}, nil
|
||||
}
|
||||
|
||||
log.Tracef("%d rules matched for host '%s'", len(rules), host)
|
||||
|
||||
for _, rule := range rules {
|
||||
|
||||
log.Tracef("Found rule for host '%s': '%s' list_id: %d",
|
||||
host, rule.Text(), rule.GetFilterListID())
|
||||
|
||||
res := Result{}
|
||||
res.Reason = FilteredBlackList
|
||||
res.IsFiltered = true
|
||||
res.FilterID = int64(rule.GetFilterListID())
|
||||
res.Rule = rule.Text()
|
||||
|
||||
if netRule, ok := rule.(*urlfilter.NetworkRule); ok {
|
||||
|
||||
if netRule.Whitelist {
|
||||
res.Reason = NotFilteredWhiteList
|
||||
res.IsFiltered = false
|
||||
}
|
||||
return res, nil
|
||||
|
||||
} else if hostRule, ok := rule.(*urlfilter.HostRule); ok {
|
||||
|
||||
if qtype == dns.TypeA && hostRule.IP.To4() != nil {
|
||||
// either IPv4 or IPv4-mapped IPv6 address
|
||||
res.IP = hostRule.IP.To4()
|
||||
return res, nil
|
||||
|
||||
} else if qtype == dns.TypeAAAA {
|
||||
ip4 := hostRule.IP.To4()
|
||||
if ip4 == nil {
|
||||
res.IP = hostRule.IP
|
||||
return res, nil
|
||||
}
|
||||
if bytes.Equal(ip4, []byte{0, 0, 0, 0}) {
|
||||
// send IP="::" response for a rule "0.0.0.0 blockdomain"
|
||||
res.IP = net.IPv6zero
|
||||
return res, nil
|
||||
}
|
||||
}
|
||||
continue
|
||||
|
||||
} else {
|
||||
log.Tracef("Rule type is unsupported: '%s' list_id: %d",
|
||||
rule.Text(), rule.GetFilterListID())
|
||||
}
|
||||
}
|
||||
|
||||
return Result{}, nil
|
||||
}
|
||||
|
||||
@@ -978,56 +605,96 @@ func (d *Dnsfilter) matchHost(host string) (Result, error) {
|
||||
// lifecycle helper functions
|
||||
//
|
||||
|
||||
// Connect to a remote server resolving hostname using our own DNS server
|
||||
func customDialContext(ctx context.Context, network, addr string) (net.Conn, error) {
|
||||
log.Tracef("network:%v addr:%v", network, addr)
|
||||
// Return TRUE if this host's IP should be cached
|
||||
func (d *Dnsfilter) shouldBeInDialCache(host string) bool {
|
||||
return host == d.safeBrowsingServer ||
|
||||
host == d.parentalServer
|
||||
}
|
||||
|
||||
host, port, err := net.SplitHostPort(addr)
|
||||
// Search for an IP address by host name
|
||||
func searchInDialCache(host string) string {
|
||||
rawValue, err := dialCache.Get(host)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return ""
|
||||
}
|
||||
|
||||
dialer := &net.Dialer{
|
||||
Timeout: time.Minute * 5,
|
||||
}
|
||||
ip, _ := rawValue.(string)
|
||||
log.Debug("Found in cache: %s -> %s", host, ip)
|
||||
return ip
|
||||
}
|
||||
|
||||
if net.ParseIP(host) != nil {
|
||||
con, err := dialer.DialContext(ctx, network, addr)
|
||||
return con, err
|
||||
// Add "hostname" -> "IP address" entry to cache
|
||||
func addToDialCache(host, ip string) {
|
||||
err := dialCache.Set(host, ip)
|
||||
if err != nil {
|
||||
log.Debug("dialCache.Set: %s", err)
|
||||
}
|
||||
log.Debug("Added to cache: %s -> %s", host, ip)
|
||||
}
|
||||
|
||||
r := upstream.NewResolver(resolverAddr, 30*time.Second)
|
||||
addrs, e := r.LookupIPAddr(ctx, host)
|
||||
log.Tracef("LookupIPAddr: %s: %v", host, addrs)
|
||||
if e != nil {
|
||||
return nil, e
|
||||
}
|
||||
type dialFunctionType func(ctx context.Context, network, addr string) (net.Conn, error)
|
||||
|
||||
var firstErr error
|
||||
firstErr = nil
|
||||
for _, a := range addrs {
|
||||
addr = fmt.Sprintf("%s:%s", a.String(), port)
|
||||
con, err := dialer.DialContext(ctx, network, addr)
|
||||
// Connect to a remote server resolving hostname using our own DNS server
|
||||
func (d *Dnsfilter) createCustomDialContext(resolverAddr string) dialFunctionType {
|
||||
return func(ctx context.Context, network, addr string) (net.Conn, error) {
|
||||
log.Tracef("network:%v addr:%v", network, addr)
|
||||
|
||||
host, port, err := net.SplitHostPort(addr)
|
||||
if err != nil {
|
||||
if firstErr == nil {
|
||||
firstErr = err
|
||||
}
|
||||
continue
|
||||
return nil, err
|
||||
}
|
||||
return con, err
|
||||
|
||||
dialer := &net.Dialer{
|
||||
Timeout: time.Minute * 5,
|
||||
}
|
||||
|
||||
if net.ParseIP(host) != nil {
|
||||
con, err := dialer.DialContext(ctx, network, addr)
|
||||
return con, err
|
||||
}
|
||||
|
||||
cache := d.shouldBeInDialCache(host)
|
||||
if cache {
|
||||
ip := searchInDialCache(host)
|
||||
if len(ip) != 0 {
|
||||
addr = fmt.Sprintf("%s:%s", ip, port)
|
||||
return dialer.DialContext(ctx, network, addr)
|
||||
}
|
||||
}
|
||||
|
||||
r := upstream.NewResolver(resolverAddr, 30*time.Second)
|
||||
addrs, e := r.LookupIPAddr(ctx, host)
|
||||
log.Tracef("LookupIPAddr: %s: %v", host, addrs)
|
||||
if e != nil {
|
||||
return nil, e
|
||||
}
|
||||
|
||||
var firstErr error
|
||||
firstErr = nil
|
||||
for _, a := range addrs {
|
||||
addr = fmt.Sprintf("%s:%s", a.String(), port)
|
||||
con, err := dialer.DialContext(ctx, network, addr)
|
||||
if err != nil {
|
||||
if firstErr == nil {
|
||||
firstErr = err
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
if cache {
|
||||
addToDialCache(host, a.String())
|
||||
}
|
||||
|
||||
return con, err
|
||||
}
|
||||
return nil, firstErr
|
||||
}
|
||||
return nil, firstErr
|
||||
}
|
||||
|
||||
// New creates properly initialized DNS Filter that is ready to be used
|
||||
func New(c *Config) *Dnsfilter {
|
||||
func New(c *Config, filters map[int]string) *Dnsfilter {
|
||||
d := new(Dnsfilter)
|
||||
|
||||
d.storage = make(map[string]bool)
|
||||
d.important = newRulesTable()
|
||||
d.whiteList = newRulesTable()
|
||||
d.blackList = newRulesTable()
|
||||
|
||||
// Customize the Transport to have larger connection pool,
|
||||
// We are not (re)using http.DefaultTransport because of race conditions found by tests
|
||||
d.transport = &http.Transport{
|
||||
@@ -1039,8 +706,8 @@ func New(c *Config) *Dnsfilter {
|
||||
ExpectContinueTimeout: 1 * time.Second,
|
||||
}
|
||||
if c != nil && len(c.ResolverAddress) != 0 {
|
||||
resolverAddr = c.ResolverAddress
|
||||
d.transport.DialContext = customDialContext
|
||||
dialCache = gcache.New(maxDialCacheSize).LRU().Expiration(defaultCacheTime).Build()
|
||||
d.transport.DialContext = d.createCustomDialContext(c.ResolverAddress)
|
||||
}
|
||||
d.client = http.Client{
|
||||
Transport: d.transport,
|
||||
@@ -1052,6 +719,15 @@ func New(c *Config) *Dnsfilter {
|
||||
d.Config = *c
|
||||
}
|
||||
|
||||
if filters != nil {
|
||||
err := d.initFiltering(filters)
|
||||
if err != nil {
|
||||
log.Error("Can't initialize filtering subsystem: %s", err)
|
||||
d.Destroy()
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
return d
|
||||
}
|
||||
|
||||
@@ -1061,6 +737,11 @@ func (d *Dnsfilter) Destroy() {
|
||||
if d != nil && d.transport != nil {
|
||||
d.transport.CloseIdleConnections()
|
||||
}
|
||||
|
||||
if d.rulesStorage != nil {
|
||||
d.rulesStorage.Close()
|
||||
d.rulesStorage = nil
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
@@ -1103,8 +784,3 @@ func (d *Dnsfilter) SafeSearchDomain(host string) (string, bool) {
|
||||
func (d *Dnsfilter) GetStats() Stats {
|
||||
return stats
|
||||
}
|
||||
|
||||
// Count returns number of rules added to filter
|
||||
func (d *Dnsfilter) Count() int {
|
||||
return len(d.storage)
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,49 +1,9 @@
|
||||
package dnsfilter
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"sync/atomic"
|
||||
)
|
||||
|
||||
func isValidRule(rule string) bool {
|
||||
if len(rule) < 4 {
|
||||
return false
|
||||
}
|
||||
if rule[0] == '!' {
|
||||
return false
|
||||
}
|
||||
if rule[0] == '#' {
|
||||
return false
|
||||
}
|
||||
if strings.HasPrefix(rule, "[Adblock") {
|
||||
return false
|
||||
}
|
||||
|
||||
// Filter out all sorts of cosmetic rules:
|
||||
// https://kb.adguard.com/en/general/how-to-create-your-own-ad-filters#cosmetic-rules
|
||||
masks := []string{
|
||||
"##",
|
||||
"#@#",
|
||||
"#?#",
|
||||
"#@?#",
|
||||
"#$#",
|
||||
"#@$#",
|
||||
"#?$#",
|
||||
"#@?$#",
|
||||
"$$",
|
||||
"$@$",
|
||||
"#%#",
|
||||
"#@%#",
|
||||
}
|
||||
for _, mask := range masks {
|
||||
if strings.Contains(rule, mask) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
func updateMax(valuePtr *int64, maxPtr *int64) {
|
||||
for {
|
||||
current := atomic.LoadInt64(valuePtr)
|
||||
|
||||
@@ -1,91 +0,0 @@
|
||||
package dnsfilter
|
||||
|
||||
import (
|
||||
"strings"
|
||||
)
|
||||
|
||||
func ruleToRegexp(rule string) (string, error) {
|
||||
const hostStart = `(?:^|\.)`
|
||||
const hostEnd = `$`
|
||||
|
||||
// empty or short rule -- do nothing
|
||||
if !isValidRule(rule) {
|
||||
return "", ErrInvalidSyntax
|
||||
}
|
||||
|
||||
// if starts with / and ends with /, it's already a regexp, just strip the slashes
|
||||
if rule[0] == '/' && rule[len(rule)-1] == '/' {
|
||||
return rule[1 : len(rule)-1], nil
|
||||
}
|
||||
|
||||
var sb strings.Builder
|
||||
|
||||
if rule[0] == '|' && rule[1] == '|' {
|
||||
sb.WriteString(hostStart)
|
||||
rule = rule[2:]
|
||||
}
|
||||
|
||||
for i, r := range rule {
|
||||
switch {
|
||||
case r == '?' || r == '.' || r == '+' || r == '[' || r == ']' || r == '(' || r == ')' || r == '{' || r == '}' || r == '#' || r == '\\' || r == '$':
|
||||
sb.WriteRune('\\')
|
||||
sb.WriteRune(r)
|
||||
case r == '|' && i == 0:
|
||||
// | at start and it's not || at start
|
||||
sb.WriteRune('^')
|
||||
case r == '|' && i == len(rule)-1:
|
||||
// | at end
|
||||
sb.WriteRune('$')
|
||||
case r == '|' && i != 0 && i != len(rule)-1:
|
||||
sb.WriteString(`\|`)
|
||||
case r == '*':
|
||||
sb.WriteString(`.*`)
|
||||
case r == '^':
|
||||
sb.WriteString(hostEnd)
|
||||
default:
|
||||
sb.WriteRune(r)
|
||||
}
|
||||
}
|
||||
|
||||
return sb.String(), nil
|
||||
}
|
||||
|
||||
// handle suffix rule ||example.com^ -- either entire string is example.com or *.example.com
|
||||
func getSuffix(rule string) (bool, string) {
|
||||
// if starts with / and ends with /, it's already a regexp
|
||||
// TODO: if a regexp is simple `/abracadabra$/`, then simplify it maybe?
|
||||
if rule[0] == '/' && rule[len(rule)-1] == '/' {
|
||||
return false, ""
|
||||
}
|
||||
|
||||
// must start with ||
|
||||
if rule[0] != '|' || rule[1] != '|' {
|
||||
return false, ""
|
||||
}
|
||||
rule = rule[2:]
|
||||
|
||||
// suffix rule must end with ^ or |
|
||||
lastChar := rule[len(rule)-1]
|
||||
if lastChar != '^' && lastChar != '|' {
|
||||
return false, ""
|
||||
}
|
||||
// last char was checked, eat it
|
||||
rule = rule[:len(rule)-1]
|
||||
|
||||
// it might also end with ^|
|
||||
if rule[len(rule)-1] == '^' {
|
||||
rule = rule[:len(rule)-1]
|
||||
}
|
||||
|
||||
// check that it doesn't have any special characters inside
|
||||
for _, r := range rule {
|
||||
switch r {
|
||||
case '|':
|
||||
return false, ""
|
||||
case '*':
|
||||
return false, ""
|
||||
}
|
||||
}
|
||||
|
||||
return true, rule
|
||||
}
|
||||
@@ -43,8 +43,14 @@ type Server struct {
|
||||
stats *stats // General server statistics
|
||||
once sync.Once
|
||||
|
||||
AllowedClients map[string]bool // IP addresses of whitelist clients
|
||||
DisallowedClients map[string]bool // IP addresses of clients that should be blocked
|
||||
AllowedClientsIPNet []net.IPNet // CIDRs of whitelist clients
|
||||
DisallowedClientsIPNet []net.IPNet // CIDRs of clients that should be blocked
|
||||
BlockedHosts map[string]bool // hosts that should be blocked
|
||||
|
||||
sync.RWMutex
|
||||
ServerConfig
|
||||
conf ServerConfig
|
||||
}
|
||||
|
||||
// NewServer creates a new instance of the dnsforward.Server
|
||||
@@ -61,6 +67,7 @@ func NewServer(baseDir string) *Server {
|
||||
type FilteringConfig struct {
|
||||
ProtectionEnabled bool `yaml:"protection_enabled"` // whether or not use any of dnsfilter features
|
||||
FilteringEnabled bool `yaml:"filtering_enabled"` // whether or not use filter lists
|
||||
BlockingMode string `yaml:"blocking_mode"` // mode how to answer filtered requests
|
||||
BlockedResponseTTL uint32 `yaml:"blocked_response_ttl"` // if 0, then default is used (3600)
|
||||
QueryLogEnabled bool `yaml:"querylog_enabled"` // if true, query log is enabled
|
||||
Ratelimit int `yaml:"ratelimit"` // max number of requests per second from a given IP (0 to disable)
|
||||
@@ -69,6 +76,10 @@ type FilteringConfig struct {
|
||||
BootstrapDNS []string `yaml:"bootstrap_dns"` // a list of bootstrap DNS for DoH and DoT (plain DNS only)
|
||||
AllServers bool `yaml:"all_servers"` // if true, parallel queries to all configured upstream servers are enabled
|
||||
|
||||
AllowedClients []string `yaml:"allowed_clients"` // IP addresses of whitelist clients
|
||||
DisallowedClients []string `yaml:"disallowed_clients"` // IP addresses of clients that should be blocked
|
||||
BlockedHosts []string `yaml:"blocked_hosts"` // hosts that should be blocked
|
||||
|
||||
dnsfilter.Config `yaml:",inline"`
|
||||
}
|
||||
|
||||
@@ -87,6 +98,7 @@ type ServerConfig struct {
|
||||
Upstreams []upstream.Upstream // Configured upstreams
|
||||
DomainsReservedUpstreams map[string][]upstream.Upstream // Map of domains and lists of configured upstreams
|
||||
Filters []dnsfilter.Filter // A list of filters to use
|
||||
OnDNSRequest func(d *proxy.DNSContext)
|
||||
|
||||
FilteringConfig
|
||||
TLSConfig
|
||||
@@ -119,10 +131,38 @@ func (s *Server) Start(config *ServerConfig) error {
|
||||
return s.startInternal(config)
|
||||
}
|
||||
|
||||
func convertArrayToMap(dst *map[string]bool, src []string) {
|
||||
*dst = make(map[string]bool)
|
||||
for _, s := range src {
|
||||
(*dst)[s] = true
|
||||
}
|
||||
}
|
||||
|
||||
// Split array of IP or CIDR into 2 containers for fast search
|
||||
func processIPCIDRArray(dst *map[string]bool, dstIPNet *[]net.IPNet, src []string) error {
|
||||
*dst = make(map[string]bool)
|
||||
|
||||
for _, s := range src {
|
||||
ip := net.ParseIP(s)
|
||||
if ip != nil {
|
||||
(*dst)[s] = true
|
||||
continue
|
||||
}
|
||||
|
||||
_, ipnet, err := net.ParseCIDR(s)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
*dstIPNet = append(*dstIPNet, *ipnet)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// startInternal starts without locking
|
||||
func (s *Server) startInternal(config *ServerConfig) error {
|
||||
if config != nil {
|
||||
s.ServerConfig = *config
|
||||
s.conf = *config
|
||||
}
|
||||
|
||||
if s.dnsFilter != nil || s.dnsProxy != nil {
|
||||
@@ -157,21 +197,34 @@ func (s *Server) startInternal(config *ServerConfig) error {
|
||||
})
|
||||
|
||||
proxyConfig := proxy.Config{
|
||||
UDPListenAddr: s.UDPListenAddr,
|
||||
TCPListenAddr: s.TCPListenAddr,
|
||||
Ratelimit: s.Ratelimit,
|
||||
RatelimitWhitelist: s.RatelimitWhitelist,
|
||||
RefuseAny: s.RefuseAny,
|
||||
UDPListenAddr: s.conf.UDPListenAddr,
|
||||
TCPListenAddr: s.conf.TCPListenAddr,
|
||||
Ratelimit: s.conf.Ratelimit,
|
||||
RatelimitWhitelist: s.conf.RatelimitWhitelist,
|
||||
RefuseAny: s.conf.RefuseAny,
|
||||
CacheEnabled: true,
|
||||
Upstreams: s.Upstreams,
|
||||
DomainsReservedUpstreams: s.DomainsReservedUpstreams,
|
||||
Handler: s.handleDNSRequest,
|
||||
AllServers: s.AllServers,
|
||||
Upstreams: s.conf.Upstreams,
|
||||
DomainsReservedUpstreams: s.conf.DomainsReservedUpstreams,
|
||||
BeforeRequestHandler: s.beforeRequestHandler,
|
||||
RequestHandler: s.handleDNSRequest,
|
||||
AllServers: s.conf.AllServers,
|
||||
}
|
||||
|
||||
if s.TLSListenAddr != nil && s.CertificateChain != "" && s.PrivateKey != "" {
|
||||
proxyConfig.TLSListenAddr = s.TLSListenAddr
|
||||
keypair, err := tls.X509KeyPair([]byte(s.CertificateChain), []byte(s.PrivateKey))
|
||||
err = processIPCIDRArray(&s.AllowedClients, &s.AllowedClientsIPNet, s.conf.AllowedClients)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = processIPCIDRArray(&s.DisallowedClients, &s.DisallowedClientsIPNet, s.conf.DisallowedClients)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
convertArrayToMap(&s.BlockedHosts, s.conf.BlockedHosts)
|
||||
|
||||
if s.conf.TLSListenAddr != nil && s.conf.CertificateChain != "" && s.conf.PrivateKey != "" {
|
||||
proxyConfig.TLSListenAddr = s.conf.TLSListenAddr
|
||||
keypair, err := tls.X509KeyPair([]byte(s.conf.CertificateChain), []byte(s.conf.PrivateKey))
|
||||
if err != nil {
|
||||
return errorx.Decorate(err, "Failed to parse TLS keypair")
|
||||
}
|
||||
@@ -201,14 +254,20 @@ func (s *Server) startInternal(config *ServerConfig) error {
|
||||
// Initializes the DNS filter
|
||||
func (s *Server) initDNSFilter() error {
|
||||
log.Tracef("Creating dnsfilter")
|
||||
s.dnsFilter = dnsfilter.New(&s.Config)
|
||||
// add rules only if they are enabled
|
||||
if s.FilteringEnabled {
|
||||
err := s.dnsFilter.AddRules(s.Filters)
|
||||
if err != nil {
|
||||
return errorx.Decorate(err, "could not initialize dnsfilter")
|
||||
|
||||
var filters map[int]string
|
||||
filters = nil
|
||||
if s.conf.FilteringEnabled {
|
||||
filters = make(map[int]string)
|
||||
for _, f := range s.conf.Filters {
|
||||
filters[int(f.ID)] = string(f.Data)
|
||||
}
|
||||
}
|
||||
|
||||
s.dnsFilter = dnsfilter.New(&s.conf.Config, filters)
|
||||
if s.dnsFilter == nil {
|
||||
return fmt.Errorf("could not initialize dnsfilter")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -235,7 +294,7 @@ func (s *Server) stopInternal() error {
|
||||
}
|
||||
|
||||
// flush remainder to file
|
||||
return s.queryLog.flushLogBuffer()
|
||||
return s.queryLog.flushLogBuffer(true)
|
||||
}
|
||||
|
||||
// IsRunning returns true if the DNS server is running
|
||||
@@ -313,10 +372,75 @@ func (s *Server) GetStatsHistory(timeUnit time.Duration, startTime time.Time, en
|
||||
return s.stats.getStatsHistory(timeUnit, startTime, endTime)
|
||||
}
|
||||
|
||||
// Return TRUE if this client should be blocked
|
||||
func (s *Server) isBlockedIP(ip string) bool {
|
||||
if len(s.AllowedClients) != 0 || len(s.AllowedClientsIPNet) != 0 {
|
||||
_, ok := s.AllowedClients[ip]
|
||||
if ok {
|
||||
return false
|
||||
}
|
||||
|
||||
if len(s.AllowedClientsIPNet) != 0 {
|
||||
ipAddr := net.ParseIP(ip)
|
||||
for _, ipnet := range s.AllowedClientsIPNet {
|
||||
if ipnet.Contains(ipAddr) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
_, ok := s.DisallowedClients[ip]
|
||||
if ok {
|
||||
return true
|
||||
}
|
||||
|
||||
if len(s.DisallowedClientsIPNet) != 0 {
|
||||
ipAddr := net.ParseIP(ip)
|
||||
for _, ipnet := range s.DisallowedClientsIPNet {
|
||||
if ipnet.Contains(ipAddr) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// Return TRUE if this domain should be blocked
|
||||
func (s *Server) isBlockedDomain(host string) bool {
|
||||
_, ok := s.BlockedHosts[host]
|
||||
return ok
|
||||
}
|
||||
|
||||
func (s *Server) beforeRequestHandler(p *proxy.Proxy, d *proxy.DNSContext) (bool, error) {
|
||||
ip, _, _ := net.SplitHostPort(d.Addr.String())
|
||||
if s.isBlockedIP(ip) {
|
||||
log.Tracef("Client IP %s is blocked by settings", ip)
|
||||
return false, nil
|
||||
}
|
||||
|
||||
if len(d.Req.Question) == 1 {
|
||||
host := strings.TrimSuffix(d.Req.Question[0].Name, ".")
|
||||
if s.isBlockedDomain(host) {
|
||||
log.Tracef("Domain %s is blocked by settings", host)
|
||||
return false, nil
|
||||
}
|
||||
}
|
||||
|
||||
return true, nil
|
||||
}
|
||||
|
||||
// handleDNSRequest filters the incoming DNS requests and writes them to the query log
|
||||
func (s *Server) handleDNSRequest(p *proxy.Proxy, d *proxy.DNSContext) error {
|
||||
start := time.Now()
|
||||
|
||||
if s.conf.OnDNSRequest != nil {
|
||||
s.conf.OnDNSRequest(d)
|
||||
}
|
||||
|
||||
// use dnsfilter before cache -- changed settings or filters would require cache invalidation otherwise
|
||||
res, err := s.filterDNSRequest(d)
|
||||
if err != nil {
|
||||
@@ -335,11 +459,11 @@ func (s *Server) handleDNSRequest(p *proxy.Proxy, d *proxy.DNSContext) error {
|
||||
msg := d.Req
|
||||
|
||||
// don't log ANY request if refuseAny is enabled
|
||||
if len(msg.Question) >= 1 && msg.Question[0].Qtype == dns.TypeANY && s.RefuseAny {
|
||||
if len(msg.Question) >= 1 && msg.Question[0].Qtype == dns.TypeANY && s.conf.RefuseAny {
|
||||
shouldLog = false
|
||||
}
|
||||
|
||||
if s.QueryLogEnabled && shouldLog {
|
||||
if s.conf.QueryLogEnabled && shouldLog {
|
||||
elapsed := time.Since(start)
|
||||
upstreamAddr := ""
|
||||
if d.Upstream != nil {
|
||||
@@ -360,7 +484,7 @@ func (s *Server) filterDNSRequest(d *proxy.DNSContext) (*dnsfilter.Result, error
|
||||
host := strings.TrimSuffix(msg.Question[0].Name, ".")
|
||||
|
||||
s.RLock()
|
||||
protectionEnabled := s.ProtectionEnabled
|
||||
protectionEnabled := s.conf.ProtectionEnabled
|
||||
dnsFilter := s.dnsFilter
|
||||
s.RUnlock()
|
||||
|
||||
@@ -371,7 +495,11 @@ func (s *Server) filterDNSRequest(d *proxy.DNSContext) (*dnsfilter.Result, error
|
||||
var res dnsfilter.Result
|
||||
var err error
|
||||
|
||||
res, err = dnsFilter.CheckHost(host)
|
||||
clientAddr := ""
|
||||
if d.Addr != nil {
|
||||
clientAddr, _, _ = net.SplitHostPort(d.Addr.String())
|
||||
}
|
||||
res, err = dnsFilter.CheckHost(host, d.Req.Question[0].Qtype, clientAddr)
|
||||
if err != nil {
|
||||
// Return immediately if there's an error
|
||||
return nil, errorx.Decorate(err, "dnsfilter failed to check host '%s'", host)
|
||||
@@ -387,7 +515,7 @@ func (s *Server) filterDNSRequest(d *proxy.DNSContext) (*dnsfilter.Result, error
|
||||
func (s *Server) genDNSFilterMessage(d *proxy.DNSContext, result *dnsfilter.Result) *dns.Msg {
|
||||
m := d.Req
|
||||
|
||||
if m.Question[0].Qtype != dns.TypeA {
|
||||
if m.Question[0].Qtype != dns.TypeA && m.Question[0].Qtype != dns.TypeAAAA {
|
||||
return s.genNXDomain(m)
|
||||
}
|
||||
|
||||
@@ -398,7 +526,25 @@ func (s *Server) genDNSFilterMessage(d *proxy.DNSContext, result *dnsfilter.Resu
|
||||
return s.genBlockedHost(m, parentalBlockHost, d)
|
||||
default:
|
||||
if result.IP != nil {
|
||||
return s.genARecord(m, result.IP)
|
||||
if m.Question[0].Qtype == dns.TypeA {
|
||||
return s.genARecord(m, result.IP)
|
||||
} else if m.Question[0].Qtype == dns.TypeAAAA {
|
||||
return s.genAAAARecord(m, result.IP)
|
||||
}
|
||||
|
||||
// empty response
|
||||
resp := dns.Msg{}
|
||||
resp.SetReply(m)
|
||||
return &resp
|
||||
}
|
||||
|
||||
if s.conf.BlockingMode == "null_ip" {
|
||||
switch m.Question[0].Qtype {
|
||||
case dns.TypeA:
|
||||
return s.genARecord(m, []byte{0, 0, 0, 0})
|
||||
case dns.TypeAAAA:
|
||||
return s.genAAAARecord(m, net.IPv6zero)
|
||||
}
|
||||
}
|
||||
|
||||
return s.genNXDomain(m)
|
||||
@@ -415,15 +561,41 @@ func (s *Server) genServerFailure(request *dns.Msg) *dns.Msg {
|
||||
func (s *Server) genARecord(request *dns.Msg, ip net.IP) *dns.Msg {
|
||||
resp := dns.Msg{}
|
||||
resp.SetReply(request)
|
||||
answer, err := dns.NewRR(fmt.Sprintf("%s %d A %s", request.Question[0].Name, s.BlockedResponseTTL, ip.String()))
|
||||
if err != nil {
|
||||
log.Printf("Couldn't generate A record for replacement host '%s': %s", ip.String(), err)
|
||||
return s.genServerFailure(request)
|
||||
}
|
||||
resp.Answer = append(resp.Answer, answer)
|
||||
resp.Answer = append(resp.Answer, s.genAAnswer(request, ip))
|
||||
return &resp
|
||||
}
|
||||
|
||||
func (s *Server) genAAAARecord(request *dns.Msg, ip net.IP) *dns.Msg {
|
||||
resp := dns.Msg{}
|
||||
resp.SetReply(request)
|
||||
resp.Answer = append(resp.Answer, s.genAAAAAnswer(request, ip))
|
||||
return &resp
|
||||
}
|
||||
|
||||
func (s *Server) genAAnswer(req *dns.Msg, ip net.IP) *dns.A {
|
||||
answer := new(dns.A)
|
||||
answer.Hdr = dns.RR_Header{
|
||||
Name: req.Question[0].Name,
|
||||
Rrtype: dns.TypeA,
|
||||
Ttl: s.conf.BlockedResponseTTL,
|
||||
Class: dns.ClassINET,
|
||||
}
|
||||
answer.A = ip
|
||||
return answer
|
||||
}
|
||||
|
||||
func (s *Server) genAAAAAnswer(req *dns.Msg, ip net.IP) *dns.AAAA {
|
||||
answer := new(dns.AAAA)
|
||||
answer.Hdr = dns.RR_Header{
|
||||
Name: req.Question[0].Name,
|
||||
Rrtype: dns.TypeAAAA,
|
||||
Ttl: s.conf.BlockedResponseTTL,
|
||||
Class: dns.ClassINET,
|
||||
}
|
||||
answer.AAAA = ip
|
||||
return answer
|
||||
}
|
||||
|
||||
func (s *Server) genBlockedHost(request *dns.Msg, newAddr string, d *proxy.DNSContext) *dns.Msg {
|
||||
// look up the hostname, TODO: cache
|
||||
replReq := dns.Msg{}
|
||||
@@ -484,7 +656,7 @@ func (s *Server) genSOA(request *dns.Msg) []dns.RR {
|
||||
Hdr: dns.RR_Header{
|
||||
Name: zone,
|
||||
Rrtype: dns.TypeSOA,
|
||||
Ttl: s.BlockedResponseTTL,
|
||||
Ttl: s.conf.BlockedResponseTTL,
|
||||
Class: dns.ClassINET,
|
||||
},
|
||||
Mbox: "hostmaster.", // zone will be appended later if it's not empty or "."
|
||||
|
||||
@@ -15,12 +15,10 @@ import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/AdguardTeam/dnsproxy/proxy"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
||||
"github.com/AdguardTeam/AdGuardHome/dnsfilter"
|
||||
"github.com/AdguardTeam/dnsproxy/proxy"
|
||||
"github.com/miekg/dns"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -86,7 +84,7 @@ func TestDotServer(t *testing.T) {
|
||||
s := createTestServer(t)
|
||||
defer removeDataDir(t)
|
||||
|
||||
s.TLSConfig = TLSConfig{
|
||||
s.conf.TLSConfig = TLSConfig{
|
||||
TLSListenAddr: &net.TCPAddr{Port: 0},
|
||||
CertificateChain: string(certPem),
|
||||
PrivateKey: string(keyPem),
|
||||
@@ -149,7 +147,7 @@ func TestServerRace(t *testing.T) {
|
||||
|
||||
func TestSafeSearch(t *testing.T) {
|
||||
s := createTestServer(t)
|
||||
s.SafeSearchEnabled = true
|
||||
s.conf.SafeSearchEnabled = true
|
||||
defer removeDataDir(t)
|
||||
err := s.Start(nil)
|
||||
if err != nil {
|
||||
@@ -177,7 +175,7 @@ func TestSafeSearch(t *testing.T) {
|
||||
|
||||
ip := ips[0]
|
||||
for _, i := range ips {
|
||||
if len(i) == net.IPv6len && i.To4() != nil {
|
||||
if i.To4() != nil {
|
||||
ip = i
|
||||
break
|
||||
}
|
||||
@@ -293,6 +291,55 @@ func TestBlockedRequest(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestNullBlockedRequest(t *testing.T) {
|
||||
s := createTestServer(t)
|
||||
s.conf.FilteringConfig.BlockingMode = "null_ip"
|
||||
defer removeDataDir(t)
|
||||
err := s.Start(nil)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to start server: %s", err)
|
||||
}
|
||||
addr := s.dnsProxy.Addr(proxy.ProtoUDP)
|
||||
|
||||
//
|
||||
// Null filter blocking
|
||||
//
|
||||
req := dns.Msg{}
|
||||
req.Id = dns.Id()
|
||||
req.RecursionDesired = true
|
||||
req.Question = []dns.Question{
|
||||
{Name: "null.example.org.", Qtype: dns.TypeA, Qclass: dns.ClassINET},
|
||||
}
|
||||
|
||||
reply, err := dns.Exchange(&req, addr.String())
|
||||
if err != nil {
|
||||
t.Fatalf("Couldn't talk to server %s: %s", addr, err)
|
||||
}
|
||||
if len(reply.Answer) != 1 {
|
||||
t.Fatalf("DNS server %s returned reply with wrong number of answers - %d", addr, len(reply.Answer))
|
||||
}
|
||||
if a, ok := reply.Answer[0].(*dns.A); ok {
|
||||
if !net.IPv4zero.Equal(a.A) {
|
||||
t.Fatalf("DNS server %s returned wrong answer instead of 0.0.0.0: %v", addr, a.A)
|
||||
}
|
||||
} else {
|
||||
t.Fatalf("DNS server %s returned wrong answer type instead of A: %v", addr, reply.Answer[0])
|
||||
}
|
||||
|
||||
// check query log and stats
|
||||
log := s.GetQueryLog()
|
||||
assert.Equal(t, 1, len(log), "Log size")
|
||||
stats := s.GetStatsTop()
|
||||
assert.Equal(t, 1, len(stats.Domains), "Top domains length")
|
||||
assert.Equal(t, 1, len(stats.Blocked), "Top blocked length")
|
||||
assert.Equal(t, 1, len(stats.Clients), "Top clients length")
|
||||
|
||||
err = s.Stop()
|
||||
if err != nil {
|
||||
t.Fatalf("DNS server failed to stop: %s", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBlockedByHosts(t *testing.T) {
|
||||
s := createTestServer(t)
|
||||
defer removeDataDir(t)
|
||||
@@ -402,21 +449,18 @@ func TestBlockedBySafeBrowsing(t *testing.T) {
|
||||
|
||||
func createTestServer(t *testing.T) *Server {
|
||||
s := NewServer(createDataDir(t))
|
||||
s.UDPListenAddr = &net.UDPAddr{Port: 0}
|
||||
s.TCPListenAddr = &net.TCPAddr{Port: 0}
|
||||
s.conf.UDPListenAddr = &net.UDPAddr{Port: 0}
|
||||
s.conf.TCPListenAddr = &net.TCPAddr{Port: 0}
|
||||
|
||||
s.QueryLogEnabled = true
|
||||
s.FilteringConfig.FilteringEnabled = true
|
||||
s.FilteringConfig.ProtectionEnabled = true
|
||||
s.FilteringConfig.SafeBrowsingEnabled = true
|
||||
s.Filters = make([]dnsfilter.Filter, 0)
|
||||
s.conf.QueryLogEnabled = true
|
||||
s.conf.FilteringConfig.FilteringEnabled = true
|
||||
s.conf.FilteringConfig.ProtectionEnabled = true
|
||||
s.conf.FilteringConfig.SafeBrowsingEnabled = true
|
||||
s.conf.Filters = make([]dnsfilter.Filter, 0)
|
||||
|
||||
rules := []string{
|
||||
"||nxdomain.example.org^",
|
||||
"127.0.0.1 host.example.org",
|
||||
}
|
||||
filter := dnsfilter.Filter{ID: 1, Rules: rules}
|
||||
s.Filters = append(s.Filters, filter)
|
||||
rules := "||nxdomain.example.org^\n||null.example.org^\n127.0.0.1 host.example.org\n"
|
||||
filter := dnsfilter.Filter{ID: 1, Data: []byte(rules)}
|
||||
s.conf.Filters = append(s.conf.Filters, filter)
|
||||
return s
|
||||
}
|
||||
|
||||
@@ -578,3 +622,72 @@ func publicKey(priv interface{}) interface{} {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func TestIsBlockedIPAllowed(t *testing.T) {
|
||||
s := createTestServer(t)
|
||||
s.conf.AllowedClients = []string{"1.1.1.1", "2.2.0.0/16"}
|
||||
|
||||
err := s.Start(nil)
|
||||
defer removeDataDir(t)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to start server: %s", err)
|
||||
}
|
||||
|
||||
if s.isBlockedIP("1.1.1.1") {
|
||||
t.Fatalf("isBlockedIP")
|
||||
}
|
||||
if !s.isBlockedIP("1.1.1.2") {
|
||||
t.Fatalf("isBlockedIP")
|
||||
}
|
||||
if s.isBlockedIP("2.2.1.1") {
|
||||
t.Fatalf("isBlockedIP")
|
||||
}
|
||||
if !s.isBlockedIP("2.3.1.1") {
|
||||
t.Fatalf("isBlockedIP")
|
||||
}
|
||||
}
|
||||
|
||||
func TestIsBlockedIPDisallowed(t *testing.T) {
|
||||
s := createTestServer(t)
|
||||
s.conf.DisallowedClients = []string{"1.1.1.1", "2.2.0.0/16"}
|
||||
|
||||
err := s.Start(nil)
|
||||
defer removeDataDir(t)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to start server: %s", err)
|
||||
}
|
||||
|
||||
if !s.isBlockedIP("1.1.1.1") {
|
||||
t.Fatalf("isBlockedIP")
|
||||
}
|
||||
if s.isBlockedIP("1.1.1.2") {
|
||||
t.Fatalf("isBlockedIP")
|
||||
}
|
||||
if !s.isBlockedIP("2.2.1.1") {
|
||||
t.Fatalf("isBlockedIP")
|
||||
}
|
||||
if s.isBlockedIP("2.3.1.1") {
|
||||
t.Fatalf("isBlockedIP")
|
||||
}
|
||||
}
|
||||
|
||||
func TestIsBlockedIPBlockedDomain(t *testing.T) {
|
||||
s := createTestServer(t)
|
||||
s.conf.BlockedHosts = []string{"host1", "host2"}
|
||||
|
||||
err := s.Start(nil)
|
||||
defer removeDataDir(t)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to start server: %s", err)
|
||||
}
|
||||
|
||||
if !s.isBlockedDomain("host1") {
|
||||
t.Fatalf("isBlockedDomain")
|
||||
}
|
||||
if !s.isBlockedDomain("host2") {
|
||||
t.Fatalf("isBlockedDomain")
|
||||
}
|
||||
if s.isBlockedDomain("host3") {
|
||||
t.Fatalf("isBlockedDomain")
|
||||
}
|
||||
}
|
||||
|
||||
14
dnsforward/helpers.go
Normal file
14
dnsforward/helpers.go
Normal file
@@ -0,0 +1,14 @@
|
||||
package dnsforward
|
||||
|
||||
import "net"
|
||||
|
||||
// GetIPString is a helper function that extracts IP address from net.Addr
|
||||
func GetIPString(addr net.Addr) string {
|
||||
switch addr := addr.(type) {
|
||||
case *net.UDPAddr:
|
||||
return addr.IP.String()
|
||||
case *net.TCPAddr:
|
||||
return addr.IP.String()
|
||||
}
|
||||
return ""
|
||||
}
|
||||
@@ -30,6 +30,8 @@ type queryLog struct {
|
||||
|
||||
logBufferLock sync.RWMutex
|
||||
logBuffer []*logEntry
|
||||
fileFlushLock sync.Mutex // synchronize a file-flushing goroutine and main thread
|
||||
flushPending bool // don't start another goroutine while the previous one is still running
|
||||
|
||||
queryLogCache []*logEntry
|
||||
queryLogLock sync.RWMutex
|
||||
@@ -59,7 +61,7 @@ func (l *queryLog) logRequest(question *dns.Msg, answer *dns.Msg, result *dnsfil
|
||||
var q []byte
|
||||
var a []byte
|
||||
var err error
|
||||
ip := getIPString(addr)
|
||||
ip := GetIPString(addr)
|
||||
|
||||
if question != nil {
|
||||
q, err = question.Pack()
|
||||
@@ -91,13 +93,15 @@ func (l *queryLog) logRequest(question *dns.Msg, answer *dns.Msg, result *dnsfil
|
||||
IP: ip,
|
||||
Upstream: upstream,
|
||||
}
|
||||
var flushBuffer []*logEntry
|
||||
|
||||
l.logBufferLock.Lock()
|
||||
l.logBuffer = append(l.logBuffer, &entry)
|
||||
if len(l.logBuffer) >= logBufferCap {
|
||||
flushBuffer = l.logBuffer
|
||||
l.logBuffer = nil
|
||||
needFlush := false
|
||||
if !l.flushPending {
|
||||
needFlush = len(l.logBuffer) >= logBufferCap
|
||||
if needFlush {
|
||||
l.flushPending = true
|
||||
}
|
||||
}
|
||||
l.logBufferLock.Unlock()
|
||||
l.queryLogLock.Lock()
|
||||
@@ -116,15 +120,10 @@ func (l *queryLog) logRequest(question *dns.Msg, answer *dns.Msg, result *dnsfil
|
||||
}
|
||||
|
||||
// if buffer needs to be flushed to disk, do it now
|
||||
if len(flushBuffer) > 0 {
|
||||
if needFlush {
|
||||
// write to file
|
||||
// do it in separate goroutine -- we are stalling DNS response this whole time
|
||||
go func() {
|
||||
err := l.flushToFile(flushBuffer)
|
||||
if err != nil {
|
||||
log.Printf("Failed to flush the query log: %s", err)
|
||||
}
|
||||
}()
|
||||
go l.flushLogBuffer(false)
|
||||
}
|
||||
|
||||
return &entry
|
||||
@@ -245,14 +244,3 @@ func answerToMap(a *dns.Msg) []map[string]interface{} {
|
||||
|
||||
return answers
|
||||
}
|
||||
|
||||
// getIPString is a helper function that extracts IP address from net.Addr
|
||||
func getIPString(addr net.Addr) string {
|
||||
switch addr := addr.(type) {
|
||||
case *net.UDPAddr:
|
||||
return addr.IP.String()
|
||||
case *net.TCPAddr:
|
||||
return addr.IP.String()
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
@@ -20,11 +20,20 @@ var (
|
||||
const enableGzip = false
|
||||
|
||||
// flushLogBuffer flushes the current buffer to file and resets the current buffer
|
||||
func (l *queryLog) flushLogBuffer() error {
|
||||
func (l *queryLog) flushLogBuffer(fullFlush bool) error {
|
||||
l.fileFlushLock.Lock()
|
||||
defer l.fileFlushLock.Unlock()
|
||||
|
||||
// flush remainder to file
|
||||
l.logBufferLock.Lock()
|
||||
needFlush := len(l.logBuffer) >= logBufferCap
|
||||
if !needFlush && !fullFlush {
|
||||
l.logBufferLock.Unlock()
|
||||
return nil
|
||||
}
|
||||
flushBuffer := l.logBuffer
|
||||
l.logBuffer = nil
|
||||
l.flushPending = false
|
||||
l.logBufferLock.Unlock()
|
||||
err := l.flushToFile(flushBuffer)
|
||||
if err != nil {
|
||||
@@ -37,6 +46,7 @@ func (l *queryLog) flushLogBuffer() error {
|
||||
// flushToFile saves the specified log entries to the query log file
|
||||
func (l *queryLog) flushToFile(buffer []*logEntry) error {
|
||||
if len(buffer) == 0 {
|
||||
log.Debug("querylog: there's nothing to write to a file")
|
||||
return nil
|
||||
}
|
||||
start := time.Now()
|
||||
|
||||
26
filter.go
26
filter.go
@@ -35,13 +35,12 @@ type filter struct {
|
||||
|
||||
// Creates a helper object for working with the user rules
|
||||
func userFilter() filter {
|
||||
return filter{
|
||||
f := filter{
|
||||
// User filter always has constant ID=0
|
||||
Enabled: true,
|
||||
Filter: dnsfilter.Filter{
|
||||
Rules: config.UserRules,
|
||||
},
|
||||
}
|
||||
f.Filter.Data = []byte(strings.Join(config.UserRules, "\n"))
|
||||
return f
|
||||
}
|
||||
|
||||
// Enable or disable a filter
|
||||
@@ -242,7 +241,7 @@ func refreshFiltersIfNecessary(force bool) int {
|
||||
log.Info("Updated filter #%d. Rules: %d -> %d",
|
||||
f.ID, f.RulesCount, uf.RulesCount)
|
||||
f.Name = uf.Name
|
||||
f.Rules = uf.Rules
|
||||
f.Data = uf.Data
|
||||
f.RulesCount = uf.RulesCount
|
||||
f.checksum = uf.checksum
|
||||
updateCount++
|
||||
@@ -261,7 +260,7 @@ func refreshFiltersIfNecessary(force bool) int {
|
||||
}
|
||||
|
||||
// A helper function that parses filter contents and returns a number of rules and a filter name (if there's any)
|
||||
func parseFilterContents(contents []byte) (int, string, []string) {
|
||||
func parseFilterContents(contents []byte) (int, string) {
|
||||
lines := strings.Split(string(contents), "\n")
|
||||
rulesCount := 0
|
||||
name := ""
|
||||
@@ -286,7 +285,7 @@ func parseFilterContents(contents []byte) (int, string, []string) {
|
||||
}
|
||||
}
|
||||
|
||||
return rulesCount, name, lines
|
||||
return rulesCount, name
|
||||
}
|
||||
|
||||
// Perform upgrade on a filter
|
||||
@@ -327,13 +326,13 @@ func (filter *filter) update() (bool, error) {
|
||||
}
|
||||
|
||||
// Extract filter name and count number of rules
|
||||
rulesCount, filterName, rules := parseFilterContents(body)
|
||||
rulesCount, filterName := parseFilterContents(body)
|
||||
log.Printf("Filter %d has been updated: %d bytes, %d rules", filter.ID, len(body), rulesCount)
|
||||
if filterName != "" {
|
||||
filter.Name = filterName
|
||||
}
|
||||
filter.RulesCount = rulesCount
|
||||
filter.Rules = rules
|
||||
filter.Data = body
|
||||
filter.checksum = checksum
|
||||
|
||||
return true, nil
|
||||
@@ -343,9 +342,8 @@ func (filter *filter) update() (bool, error) {
|
||||
func (filter *filter) save() error {
|
||||
filterFilePath := filter.Path()
|
||||
log.Printf("Saving filter %d contents to: %s", filter.ID, filterFilePath)
|
||||
body := []byte(strings.Join(filter.Rules, "\n"))
|
||||
|
||||
err := file.SafeWrite(filterFilePath, body)
|
||||
err := file.SafeWrite(filterFilePath, filter.Data)
|
||||
|
||||
// update LastUpdated field after saving the file
|
||||
filter.LastUpdated = filter.LastTimeUpdated()
|
||||
@@ -368,10 +366,10 @@ func (filter *filter) load() error {
|
||||
}
|
||||
|
||||
log.Tracef("File %s, id %d, length %d", filterFilePath, filter.ID, len(filterFileContents))
|
||||
rulesCount, _, rules := parseFilterContents(filterFileContents)
|
||||
rulesCount, _ := parseFilterContents(filterFileContents)
|
||||
|
||||
filter.RulesCount = rulesCount
|
||||
filter.Rules = rules
|
||||
filter.Data = filterFileContents
|
||||
filter.checksum = crc32.ChecksumIEEE(filterFileContents)
|
||||
filter.LastUpdated = filter.LastTimeUpdated()
|
||||
|
||||
@@ -380,7 +378,7 @@ func (filter *filter) load() error {
|
||||
|
||||
// Clear filter rules
|
||||
func (filter *filter) unload() {
|
||||
filter.Rules = []string{}
|
||||
filter.Data = nil
|
||||
filter.RulesCount = 0
|
||||
}
|
||||
|
||||
|
||||
14
go.mod
14
go.mod
@@ -3,26 +3,22 @@ module github.com/AdguardTeam/AdGuardHome
|
||||
go 1.12
|
||||
|
||||
require (
|
||||
github.com/AdguardTeam/dnsproxy v0.12.0
|
||||
github.com/AdguardTeam/dnsproxy v0.14.0
|
||||
github.com/AdguardTeam/golibs v0.1.3
|
||||
github.com/AdguardTeam/urlfilter v0.3.0
|
||||
github.com/NYTimes/gziphandler v1.1.1
|
||||
github.com/StackExchange/wmi v0.0.0-20180725035823-b12b22c5341f // indirect
|
||||
github.com/bluele/gcache v0.0.0-20190203144525-2016d595ccb0
|
||||
github.com/go-ole/go-ole v1.2.1 // indirect
|
||||
github.com/go-test/deep v1.0.1
|
||||
github.com/gobuffalo/packr v1.19.0
|
||||
github.com/joomcode/errorx v0.1.0
|
||||
github.com/kardianos/osext v0.0.0-20170510131534-ae77be60afb1 // indirect
|
||||
github.com/kardianos/service v0.0.0-20181115005516-4c239ee84e7b
|
||||
github.com/krolaw/dhcp4 v0.0.0-20180925202202-7cead472c414
|
||||
github.com/miekg/dns v1.1.1
|
||||
github.com/shirou/gopsutil v2.18.10+incompatible
|
||||
github.com/shirou/w32 v0.0.0-20160930032740-bb4de0191aa4 // indirect
|
||||
github.com/miekg/dns v1.1.8
|
||||
github.com/sparrc/go-ping v0.0.0-20181106165434-ef3ab45e41b0
|
||||
github.com/stretchr/testify v1.3.0
|
||||
go.uber.org/goleak v0.10.0
|
||||
golang.org/x/net v0.0.0-20190119204137-ed066c81e75e
|
||||
golang.org/x/sys v0.0.0-20190122071731-054c452bb702
|
||||
golang.org/x/net v0.0.0-20190424112056-4829fb13d2c6
|
||||
golang.org/x/sys v0.0.0-20190424160641-4347357a82bc
|
||||
gopkg.in/asaskevich/govalidator.v4 v4.0.0-20160518190739-766470278477
|
||||
gopkg.in/yaml.v2 v2.2.1
|
||||
)
|
||||
|
||||
65
go.sum
65
go.sum
@@ -1,20 +1,25 @@
|
||||
github.com/AdguardTeam/dnsproxy v0.12.0 h1:BPgv2PlH2u4xakFcaW4EqU3Visk1BNidrqGSgxe5Qzg=
|
||||
github.com/AdguardTeam/dnsproxy v0.12.0/go.mod h1:lcZM2QPwcWGEL3pz8RYy06nQdbjj4pr+94H45jnVSHg=
|
||||
github.com/AdguardTeam/dnsproxy v0.14.0 h1:ubB5031Oc8TfOWxRpYYDx0Lt181jiNGOfiOgEN5VJys=
|
||||
github.com/AdguardTeam/dnsproxy v0.14.0/go.mod h1:50//JYIOMRnQnq0GQhvg516seqb5vjjyMIk+Z3RYy/s=
|
||||
github.com/AdguardTeam/golibs v0.1.2/go.mod h1:b0XkhgIcn2TxwX6C5AQMtpIFAgjPehNgxJErWkwA3ko=
|
||||
github.com/AdguardTeam/golibs v0.1.3 h1:hmapdTtMtIk3T8eQDwTOLdqZLGDKNKk9325uC8z12xg=
|
||||
github.com/AdguardTeam/golibs v0.1.3/go.mod h1:b0XkhgIcn2TxwX6C5AQMtpIFAgjPehNgxJErWkwA3ko=
|
||||
github.com/AdguardTeam/urlfilter v0.3.0 h1:WNd3uZEYWwxylUuA8QS6V5DqHNsVFw3ZD/E2rd5HGpo=
|
||||
github.com/AdguardTeam/urlfilter v0.3.0/go.mod h1:9xfZ6R2vB8LlT8G9LxtbNhDsbr/xybUOSwmJvpXhl/c=
|
||||
github.com/NYTimes/gziphandler v1.1.1 h1:ZUDjpQae29j0ryrS0u/B8HZfJBtBQHjqw2rQ2cqUQ3I=
|
||||
github.com/NYTimes/gziphandler v1.1.1/go.mod h1:n/CVRwUEOgIxrgPvAQhUUr9oeUtvrhMomdKFjzJNB0c=
|
||||
github.com/StackExchange/wmi v0.0.0-20180725035823-b12b22c5341f h1:5ZfJxyXo8KyX8DgGXC5B7ILL8y51fci/qYz2B4j8iLY=
|
||||
github.com/StackExchange/wmi v0.0.0-20180725035823-b12b22c5341f/go.mod h1:3eOhrUMpNV+6aFIbp5/iudMxNCF27Vw2OZgy4xEx0Fg=
|
||||
github.com/StackExchange/wmi v0.0.0-20181212234831-e0a55b97c705 h1:UUppSQnhf4Yc6xGxSkoQpPhb7RVzuv5Nb1mwJ5VId9s=
|
||||
github.com/StackExchange/wmi v0.0.0-20181212234831-e0a55b97c705/go.mod h1:3eOhrUMpNV+6aFIbp5/iudMxNCF27Vw2OZgy4xEx0Fg=
|
||||
github.com/aead/chacha20 v0.0.0-20180709150244-8b13a72661da h1:KjTM2ks9d14ZYCvmHS9iAKVt9AyzRSqNU1qabPih5BY=
|
||||
github.com/aead/chacha20 v0.0.0-20180709150244-8b13a72661da/go.mod h1:eHEWzANqSiWQsof+nXEI9bUVUyV6F53Fp89EuCh2EAA=
|
||||
github.com/aead/poly1305 v0.0.0-20180717145839-3fee0db0b635 h1:52m0LGchQBBVqJRyYYufQuIbVqRawmubW3OFGqK1ekw=
|
||||
github.com/aead/poly1305 v0.0.0-20180717145839-3fee0db0b635/go.mod h1:lmLxL+FV291OopO93Bwf9fQLQeLyt33VJRUg5VJ30us=
|
||||
github.com/ameshkov/dnscrypt v1.0.6 h1:55wfnNF8c4E3JXDNlwPl2Pbs7UPPIh+kI6KK3THqYS0=
|
||||
github.com/ameshkov/dnscrypt v1.0.6/go.mod h1:ZvT9LaNaJfDNXKIbkYFf24HUgHuQR6MNT6nwVvN4jMQ=
|
||||
github.com/ameshkov/dnscrypt v1.0.7 h1:7LS9wiC/6c00H3ZdZOlwQSYGTJvs12g5ui9D1VSZ2aQ=
|
||||
github.com/ameshkov/dnscrypt v1.0.7/go.mod h1:rA74ASZ0j4JqPWaiN64hN97QXJ/zu5Kb2xgn295VzWQ=
|
||||
github.com/ameshkov/dnsstamps v1.0.1 h1:LhGvgWDzhNJh+kBQd/AfUlq1vfVe109huiXw4JhnPug=
|
||||
github.com/ameshkov/dnsstamps v1.0.1/go.mod h1:Ii3eUu73dx4Vw5O4wjzmT5+lkCwovjzaEZZ4gKyIH5A=
|
||||
github.com/ameshkov/goproxy v0.0.0-20190328085534-e9f6fabc24d4/go.mod h1:tKA6C/1BQYejT7L6ZX0klDrqloYenYETv3BCk8xCbrQ=
|
||||
github.com/asaskevich/govalidator v0.0.0-20180720115003-f9ffefc3facf h1:eg0MeVzsP1G42dRafH3vf+al2vQIJU0YHX+1Tw87oco=
|
||||
github.com/asaskevich/govalidator v0.0.0-20180720115003-f9ffefc3facf/go.mod h1:lB+ZfQJz7igIIfQNfa7Ml4HSf2uFQQRzpGGRXenZAgY=
|
||||
github.com/beefsack/go-rate v0.0.0-20180408011153-efa7637bb9b6 h1:KXlsf+qt/X5ttPGEjR0tPH1xaWWoKBEg9Q1THAj2h3I=
|
||||
github.com/beefsack/go-rate v0.0.0-20180408011153-efa7637bb9b6/go.mod h1:6YNgTHLutezwnBvyneBbwvB8C82y3dcoOj5EQJIdGXA=
|
||||
github.com/bluele/gcache v0.0.0-20190203144525-2016d595ccb0 h1:vUdUwmQLnT/yuk8PsDhhMVkrfr4aMdcv/0GWzIqOjEY=
|
||||
@@ -22,8 +27,8 @@ github.com/bluele/gcache v0.0.0-20190203144525-2016d595ccb0/go.mod h1:8c4/i2Vlov
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/go-ole/go-ole v1.2.1 h1:2lOsA72HgjxAuMlKpFiCbHTvu44PIVkZ5hqm3RSdI/E=
|
||||
github.com/go-ole/go-ole v1.2.1/go.mod h1:7FAglXiTm7HKlQRDeOQ6ZNUHidzCWXuZWq/1dTyBNF8=
|
||||
github.com/go-ole/go-ole v1.2.4 h1:nNBDSCOigTSiarFpYE9J/KtEA1IOW4CNeqT9TQDqCxI=
|
||||
github.com/go-ole/go-ole v1.2.4/go.mod h1:XCwSNxSkXRo4vlyPy93sltvi/qJq0jqQhjqQNIwKuxM=
|
||||
github.com/go-test/deep v1.0.1 h1:UQhStjbkDClarlmv0am7OXXO4/GaPdCGiUiMTvi28sg=
|
||||
github.com/go-test/deep v1.0.1/go.mod h1:wGDj63lr65AM2AQyKZd/NYHGb0R+1RLqB8NKt3aSFNA=
|
||||
github.com/gobuffalo/envy v1.6.7 h1:XMZGuFqTupAXhZTriQ+qO38QvNOSU/0rl3hEPCFci/4=
|
||||
@@ -32,6 +37,9 @@ github.com/gobuffalo/packd v0.0.0-20181031195726-c82734870264 h1:roWyi0eEdiFreSq
|
||||
github.com/gobuffalo/packd v0.0.0-20181031195726-c82734870264/go.mod h1:Yf2toFaISlyQrr5TfO3h6DB9pl9mZRmyvBGQb/aQ/pI=
|
||||
github.com/gobuffalo/packr v1.19.0 h1:3UDmBDxesCOPF8iZdMDBBWKfkBoYujIMIZePnobqIUI=
|
||||
github.com/gobuffalo/packr v1.19.0/go.mod h1:MstrNkfCQhd5o+Ct4IJ0skWlxN8emOq8DsoT1G98VIU=
|
||||
github.com/google/pprof v0.0.0-20190309163659-77426154d546/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
|
||||
github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
|
||||
github.com/inconshreveable/go-vhost v0.0.0-20160627193104-06d84117953b/go.mod h1:aA6DnFhALT3zH0y+A39we+zbrdMC2N0X/q21e6FI0LU=
|
||||
github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
|
||||
github.com/jessevdk/go-flags v1.4.0 h1:4IU2WS7AumrZ/40jfhf4QVDMsQwqA7VEHozFRrGARJA=
|
||||
github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI=
|
||||
@@ -47,16 +55,17 @@ github.com/krolaw/dhcp4 v0.0.0-20180925202202-7cead472c414 h1:6wnYc2S/lVM7BvR32B
|
||||
github.com/krolaw/dhcp4 v0.0.0-20180925202202-7cead472c414/go.mod h1:0AqAH3ZogsCrvrtUpvc6EtVKbc3w6xwZhkvGLuqyi3o=
|
||||
github.com/markbates/oncer v0.0.0-20181014194634-05fccaae8fc4 h1:Mlji5gkcpzkqTROyE4ZxZ8hN7osunMb2RuGVrbvMvCc=
|
||||
github.com/markbates/oncer v0.0.0-20181014194634-05fccaae8fc4/go.mod h1:Ld9puTsIW75CHf65OeIOkyKbteujpZVXDpWK6YGZbxE=
|
||||
github.com/miekg/dns v1.1.1 h1:DVkblRdiScEnEr0LR9nTnEQqHYycjkXW9bOjd+2EL2o=
|
||||
github.com/miekg/dns v1.1.1/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg=
|
||||
github.com/miekg/dns v1.1.8 h1:1QYRAKU3lN5cRfLCkPU08hwvLJFhvjP6MqNMmQz6ZVI=
|
||||
github.com/miekg/dns v1.1.8/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg=
|
||||
github.com/patrickmn/go-cache v2.1.0+incompatible h1:HRMgzkcYKYpi3C8ajMPV8OFXaaRUnok+kx1WdO15EQc=
|
||||
github.com/patrickmn/go-cache v2.1.0+incompatible/go.mod h1:3Qf8kWWT7OJRJbdiICTKqZju1ZixQ/KpMGzzAfe6+WQ=
|
||||
github.com/pkg/errors v0.8.0 h1:WdK/asTD0HN+q6hsWO3/vpuAkAr+tw6aNJNDFFf0+qw=
|
||||
github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/shirou/gopsutil v2.18.10+incompatible h1:cy84jW6EVRPa5g9HAHrlbxMSIjBhDSX0OFYyMYminYs=
|
||||
github.com/shirou/gopsutil v2.18.10+incompatible/go.mod h1:5b4v6he4MtMOwMlS0TUMTu2PcXUg8+E1lC7eC3UO/RA=
|
||||
github.com/rogpeppe/go-charset v0.0.0-20180617210344-2471d30d28b4/go.mod h1:qgYeAmZ5ZIpBWTGllZSQnw97Dj+woV0toclVaRGI8pc=
|
||||
github.com/shirou/gopsutil v2.18.12+incompatible h1:1eaJvGomDnH74/5cF4CTmTbLHAriGFsTZppLXDX93OM=
|
||||
github.com/shirou/gopsutil v2.18.12+incompatible/go.mod h1:5b4v6he4MtMOwMlS0TUMTu2PcXUg8+E1lC7eC3UO/RA=
|
||||
github.com/shirou/w32 v0.0.0-20160930032740-bb4de0191aa4 h1:udFKJ0aHUL60LboW/A+DfgoHVedieIzIXE8uylPue0U=
|
||||
github.com/shirou/w32 v0.0.0-20160930032740-bb4de0191aa4/go.mod h1:qsXQc7+bwAM3Q1u/4XEfrquwF8Lw7D7y5cD8CuHnfIc=
|
||||
github.com/sparrc/go-ping v0.0.0-20181106165434-ef3ab45e41b0 h1:mu7brOsdaH5Dqf93vdch+mr/0To8Sgc+yInt/jE/RJM=
|
||||
@@ -70,28 +79,48 @@ github.com/stretchr/testify v1.2.2 h1:bSDNvY7ZPG5RlJ8otE/7V6gMiyenm9RtJ7IUVIAoJ1
|
||||
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
||||
github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q=
|
||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||
go.uber.org/goleak v0.10.0 h1:G3eWbSNIskeRqtsN/1uI5B+eP73y3JUuBsv9AZjehb4=
|
||||
go.uber.org/goleak v0.10.0/go.mod h1:VCZuO8V8mFPlL0F5J5GK1rtHV3DrFcQ1R8ryq7FK0aI=
|
||||
golang.org/x/arch v0.0.0-20190312162104-788fe5ffcd8c/go.mod h1:flIaEI6LNU6xOCD5PaJvn9wGP0agmIOqjrtsKGRguv4=
|
||||
golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9 h1:mKdxBk7AujPs8kU4m80U72y/zjbZ3UcXC7dClwKbUI0=
|
||||
golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
||||
golang.org/x/crypto v0.0.0-20190122013713-64072686203f h1:u1CmMhe3a44hy8VIgpInORnI01UVaUYheqR7x9BxT3c=
|
||||
golang.org/x/crypto v0.0.0-20190122013713-64072686203f/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2 h1:VklqNMn3ovrHsnt90PveolxSbWFaJdECFbxSq0Mqo2M=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20190422183909-d864b10871cd h1:sMHc2rZHuzQmrbVoSpt9HgerkXPyIeCSO6k0zUMGfFk=
|
||||
golang.org/x/crypto v0.0.0-20190422183909-d864b10871cd/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||
golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
|
||||
golang.org/x/image v0.0.0-20190516052701-61b8692d9a5c/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
|
||||
golang.org/x/mobile v0.0.0-20190509164839-32b2708ab171/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o=
|
||||
golang.org/x/net v0.0.0-20181102091132-c10e9556a7bc/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20190119204137-ed066c81e75e h1:MDa3fSUp6MdYHouVmCCNz/zaH2a6CRcxY3VhT/K3C5Q=
|
||||
golang.org/x/net v0.0.0-20190119204137-ed066c81e75e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20190313220215-9f648a60d977 h1:actzWV6iWn3GLqN8dZjzsB+CLt+gaV2+wsxroxiQI8I=
|
||||
golang.org/x/net v0.0.0-20190313220215-9f648a60d977/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190424112056-4829fb13d2c6 h1:FP8hkuE6yUEaJnK7O2eTuejKWwW+Rhfj80dQ2JcKxCU=
|
||||
golang.org/x/net v0.0.0-20190424112056-4829fb13d2c6/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f h1:wMNYb4v58l5UBM7MYRLPG6ZhfOqbKu7X5eyFl8ZhKvA=
|
||||
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f h1:Bl/8QSvNqXvPGPGXa2z5xUTmV7VDcZyvRZ+QQXkXTZQ=
|
||||
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4 h1:YUO/7uOKsKeq9UokNS62b8FYywz3ker1l1vDZRCRefw=
|
||||
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58 h1:8gQV6CLnAEikrhgkHFbMAEhagSSnXWGV915qUMm9mrU=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sys v0.0.0-20190122071731-054c452bb702 h1:Lk4tbZFnlyPgV+sLgTw5yGfzrlOn9kx4vSombi2FFlY=
|
||||
golang.org/x/sys v0.0.0-20190122071731-054c452bb702/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a h1:1BGLXjeY4akVXGgbC9HugT3Jv3hCI0z56oJR5vAMgBU=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190424160641-4347357a82bc h1:ULV59IIHLrmESQT7EqC104GKra36T4CqHvPeEqR6v8M=
|
||||
golang.org/x/sys v0.0.0-20190424160641-4347357a82bc/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.1 h1:nsUiJHvm6yOoRozW9Tz0siNk9sHieLzR+w814Ihse3A=
|
||||
golang.org/x/text v0.3.1/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
gopkg.in/asaskevich/govalidator.v4 v4.0.0-20160518190739-766470278477 h1:5xUJw+lg4zao9W4HIDzlFbMYgSgtvNVHh00MEHvbGpQ=
|
||||
gopkg.in/asaskevich/govalidator.v4 v4.0.0-20160518190739-766470278477/go.mod h1:QDV1vrFSrowdoOba0UM8VJPUZONT7dnfdLsM+GG53Z8=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/yaml.v2 v2.2.1 h1:mUhvW9EsL+naU5Q3cakzfE91YhliOondGd6ZrsDBHQE=
|
||||
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4=
|
||||
|
||||
24
helpers.go
24
helpers.go
@@ -2,6 +2,7 @@ package main
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
@@ -140,7 +141,9 @@ func preInstallHandler(handler http.Handler) http.Handler {
|
||||
// it also enforces HTTPS if it is enabled and configured
|
||||
func postInstall(handler func(http.ResponseWriter, *http.Request)) func(http.ResponseWriter, *http.Request) {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
if config.firstRun && !strings.HasPrefix(r.URL.Path, "/install.") {
|
||||
if config.firstRun &&
|
||||
!strings.HasPrefix(r.URL.Path, "/install.") &&
|
||||
r.URL.Path != "/favicon.png" {
|
||||
http.Redirect(w, r, "/install.html", http.StatusSeeOther) // should not be cacheable
|
||||
return
|
||||
}
|
||||
@@ -318,7 +321,7 @@ func customDialContext(ctx context.Context, network, addr string) (net.Conn, err
|
||||
Timeout: time.Minute * 5,
|
||||
}
|
||||
|
||||
if net.ParseIP(host) != nil {
|
||||
if net.ParseIP(host) != nil || config.DNS.Port == 0 {
|
||||
con, err := dialer.DialContext(ctx, network, addr)
|
||||
return con, err
|
||||
}
|
||||
@@ -338,7 +341,7 @@ func customDialContext(ctx context.Context, network, addr string) (net.Conn, err
|
||||
var firstErr error
|
||||
firstErr = nil
|
||||
for _, a := range addrs {
|
||||
addr = fmt.Sprintf("%s:%s", a.String(), port)
|
||||
addr = net.JoinHostPort(a.String(), port)
|
||||
con, err := dialer.DialContext(ctx, network, addr)
|
||||
if err != nil {
|
||||
if firstErr == nil {
|
||||
@@ -385,3 +388,18 @@ func _Func() string {
|
||||
f := runtime.FuncForPC(pc[0])
|
||||
return path.Base(f.Name())
|
||||
}
|
||||
|
||||
// Parse input string and return IPv4 address
|
||||
func parseIPv4(s string) net.IP {
|
||||
ip := net.ParseIP(s)
|
||||
if ip == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
v4InV6Prefix := []byte{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0xff, 0xff}
|
||||
if !bytes.Equal(ip[0:12], v4InV6Prefix) {
|
||||
return nil
|
||||
}
|
||||
|
||||
return ip.To4()
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@ swagger: '2.0'
|
||||
info:
|
||||
title: 'AdGuard Home'
|
||||
description: 'AdGuard Home REST API. Admin web interface is built on top of this REST API.'
|
||||
version: 0.95.0
|
||||
version: 0.96.0
|
||||
schemes:
|
||||
- http
|
||||
basePath: /control
|
||||
@@ -144,13 +144,24 @@ paths:
|
||||
- 'application/json'
|
||||
responses:
|
||||
200:
|
||||
description: 'Version info'
|
||||
description: 'Version info. If response message is empty, UI does not show a version update message.'
|
||||
schema:
|
||||
$ref: "#/definitions/VersionInfo"
|
||||
500:
|
||||
description: 'Cannot write answer'
|
||||
502:
|
||||
description: 'Cannot retrieve the version.json file contents'
|
||||
/update:
|
||||
post:
|
||||
tags:
|
||||
- global
|
||||
operationId: beginUpdate
|
||||
summary: 'Begin auto-upgrade procedure'
|
||||
responses:
|
||||
200:
|
||||
description: OK
|
||||
500:
|
||||
description: Failed
|
||||
|
||||
# --------------------------------------------------
|
||||
# Query log methods
|
||||
@@ -376,6 +387,42 @@ paths:
|
||||
schema:
|
||||
$ref: "#/definitions/DhcpSearchResult"
|
||||
|
||||
/dhcp/add_static_lease:
|
||||
post:
|
||||
tags:
|
||||
- dhcp
|
||||
operationId: dhcpAddStaticLease
|
||||
summary: "Adds a static lease"
|
||||
consumes:
|
||||
- application/json
|
||||
parameters:
|
||||
- in: "body"
|
||||
name: "body"
|
||||
required: true
|
||||
schema:
|
||||
$ref: "#/definitions/DhcpStaticLease"
|
||||
responses:
|
||||
200:
|
||||
description: OK
|
||||
|
||||
/dhcp/remove_static_lease:
|
||||
post:
|
||||
tags:
|
||||
- dhcp
|
||||
operationId: dhcpRemoveStaticLease
|
||||
summary: "Removes a static lease"
|
||||
consumes:
|
||||
- application/json
|
||||
parameters:
|
||||
- in: "body"
|
||||
name: "body"
|
||||
required: true
|
||||
schema:
|
||||
$ref: "#/definitions/DhcpStaticLease"
|
||||
responses:
|
||||
200:
|
||||
description: OK
|
||||
|
||||
# --------------------------------------------------
|
||||
# Filtering status methods
|
||||
# --------------------------------------------------
|
||||
@@ -413,41 +460,37 @@ paths:
|
||||
description: OK
|
||||
|
||||
/filtering/add_url:
|
||||
put:
|
||||
post:
|
||||
tags:
|
||||
- filtering
|
||||
operationId: filteringAddURL
|
||||
summary: 'Add filter URL'
|
||||
consumes:
|
||||
- text/plain
|
||||
- application/json
|
||||
parameters:
|
||||
- in: body
|
||||
name: url
|
||||
description: 'URL containing filtering rules'
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
example: 'url=https://filters.adtidy.org/windows/filters/15.txt'
|
||||
- in: "body"
|
||||
name: "body"
|
||||
required: true
|
||||
schema:
|
||||
$ref: "#/definitions/AddUrlRequest"
|
||||
responses:
|
||||
200:
|
||||
description: OK
|
||||
|
||||
/filtering/remove_url:
|
||||
delete:
|
||||
post:
|
||||
tags:
|
||||
- filtering
|
||||
operationId: filteringRemoveURL
|
||||
summary: 'Remove filter URL'
|
||||
consumes:
|
||||
- text/plain
|
||||
- application/json
|
||||
parameters:
|
||||
- in: body
|
||||
name: url
|
||||
description: 'Previously added URL containing filtering rules'
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
example: 'url=https://filters.adtidy.org/windows/filters/15.txt'
|
||||
- in: "body"
|
||||
name: "body"
|
||||
required: true
|
||||
schema:
|
||||
$ref: "#/definitions/RemoveUrlRequest"
|
||||
responses:
|
||||
200:
|
||||
description: OK
|
||||
@@ -519,7 +562,7 @@ paths:
|
||||
description: OK with how many filters were actually updated
|
||||
|
||||
/filtering/set_rules:
|
||||
put:
|
||||
post:
|
||||
tags:
|
||||
- filtering
|
||||
operationId: filteringSetRules
|
||||
@@ -687,6 +730,54 @@ paths:
|
||||
schema:
|
||||
$ref: "#/definitions/Clients"
|
||||
|
||||
/clients/add:
|
||||
post:
|
||||
tags:
|
||||
- clients
|
||||
operationId: clientsAdd
|
||||
summary: 'Add a new client'
|
||||
parameters:
|
||||
- in: "body"
|
||||
name: "body"
|
||||
required: true
|
||||
schema:
|
||||
$ref: "#/definitions/Client"
|
||||
responses:
|
||||
200:
|
||||
description: OK
|
||||
|
||||
/clients/delete:
|
||||
post:
|
||||
tags:
|
||||
- clients
|
||||
operationId: clientsDelete
|
||||
summary: 'Remove a client'
|
||||
parameters:
|
||||
- in: "body"
|
||||
name: "body"
|
||||
required: true
|
||||
schema:
|
||||
$ref: "#/definitions/ClientDelete"
|
||||
responses:
|
||||
200:
|
||||
description: OK
|
||||
|
||||
/clients/update:
|
||||
post:
|
||||
tags:
|
||||
- clients
|
||||
operationId: clientsUpdate
|
||||
summary: 'Update client information'
|
||||
parameters:
|
||||
- in: "body"
|
||||
name: "body"
|
||||
required: true
|
||||
schema:
|
||||
$ref: "#/definitions/ClientUpdate"
|
||||
responses:
|
||||
200:
|
||||
description: OK
|
||||
|
||||
# --------------------------------------------------
|
||||
# I18N methods
|
||||
# --------------------------------------------------
|
||||
@@ -906,17 +997,8 @@ definitions:
|
||||
VersionInfo:
|
||||
type: "object"
|
||||
description: "Information about the latest available version of AdGuard Home"
|
||||
required:
|
||||
- "version"
|
||||
- "announcement"
|
||||
- "announcement_url"
|
||||
- "download_darwin_amd64"
|
||||
- "download_linux_amd64"
|
||||
- "download_linux_386"
|
||||
- "download_linux_arm"
|
||||
- "selfupdate_min_version"
|
||||
properties:
|
||||
version:
|
||||
new_version:
|
||||
type: "string"
|
||||
example: "v0.9"
|
||||
announcement:
|
||||
@@ -925,21 +1007,8 @@ definitions:
|
||||
announcement_url:
|
||||
type: "string"
|
||||
example: "https://github.com/AdguardTeam/AdGuardHome/releases/tag/v0.9"
|
||||
download_darwin_amd64:
|
||||
type: "string"
|
||||
example: "https://github.com/AdguardTeam/AdGuardHome/releases/download/v0.9/AdGuardHome_v0.9_MacOS.zip"
|
||||
download_linux_amd64:
|
||||
type: "string"
|
||||
example: "https://github.com/AdguardTeam/AdGuardHome/releases/download/v0.9/AdGuardHome_v0.9_linux_amd64.tar.gz"
|
||||
download_linux_386:
|
||||
type: "string"
|
||||
example: "https://github.com/AdguardTeam/AdGuardHome/releases/download/v0.9/AdGuardHome_v0.9_linux_386.tar.gz"
|
||||
download_linux_arm:
|
||||
type: "string"
|
||||
example: "https://github.com/AdguardTeam/AdGuardHome/releases/download/v0.9/AdGuardHome_v0.9_linux_arm.tar.gz"
|
||||
selfupdate_min_version:
|
||||
type: "string"
|
||||
example: "v0.0"
|
||||
can_autoupdate:
|
||||
type: "boolean"
|
||||
Stats:
|
||||
type: "object"
|
||||
description: "General server stats for the last 24 hours"
|
||||
@@ -1115,7 +1184,7 @@ definitions:
|
||||
properties:
|
||||
mac:
|
||||
type: "string"
|
||||
example: "001109b3b3b8"
|
||||
example: "00:11:09:b3:b3:b8"
|
||||
ip:
|
||||
type: "string"
|
||||
example: "192.168.1.22"
|
||||
@@ -1126,6 +1195,24 @@ definitions:
|
||||
type: "string"
|
||||
format: "date-time"
|
||||
example: "2017-07-21T17:32:28Z"
|
||||
DhcpStaticLease:
|
||||
type: "object"
|
||||
description: "DHCP static lease information"
|
||||
required:
|
||||
- "mac"
|
||||
- "ip"
|
||||
- "hostname"
|
||||
- "expires"
|
||||
properties:
|
||||
mac:
|
||||
type: "string"
|
||||
example: "00:11:09:b3:b3:b8"
|
||||
ip:
|
||||
type: "string"
|
||||
example: "192.168.1.22"
|
||||
hostname:
|
||||
type: "string"
|
||||
example: "dell"
|
||||
DhcpStatus:
|
||||
type: "object"
|
||||
description: "Built-in DHCP server configuration and status"
|
||||
@@ -1139,6 +1226,10 @@ definitions:
|
||||
type: "array"
|
||||
items:
|
||||
$ref: "#/definitions/DhcpLease"
|
||||
static_leases :
|
||||
type: "array"
|
||||
items:
|
||||
$ref: "#/definitions/DhcpStaticLease"
|
||||
DhcpSearchResult:
|
||||
type: "object"
|
||||
description: "Information about a DHCP server discovered in the current network"
|
||||
@@ -1195,6 +1286,24 @@ definitions:
|
||||
type:
|
||||
type: "string"
|
||||
example: "A"
|
||||
AddUrlRequest:
|
||||
type: "object"
|
||||
description: "/add_url request data"
|
||||
properties:
|
||||
name:
|
||||
type: "string"
|
||||
url:
|
||||
description: "URL containing filtering rules"
|
||||
type: "string"
|
||||
example: "https://filters.adtidy.org/windows/filters/15.txt"
|
||||
RemoveUrlRequest:
|
||||
type: "object"
|
||||
description: "/remove_url request data"
|
||||
properties:
|
||||
url:
|
||||
description: "Previously added URL containing filtering rules"
|
||||
type: "string"
|
||||
example: "https://filters.adtidy.org/windows/filters/15.txt"
|
||||
QueryLogItem:
|
||||
type: "object"
|
||||
description: "Query log item"
|
||||
@@ -1388,11 +1497,65 @@ definitions:
|
||||
type: "string"
|
||||
description: "Name"
|
||||
example: "localhost"
|
||||
mac:
|
||||
type: "string"
|
||||
use_global_settings:
|
||||
type: "boolean"
|
||||
filtering_enabled:
|
||||
type: "boolean"
|
||||
parental_enabled:
|
||||
type: "boolean"
|
||||
safebrowsing_enabled:
|
||||
type: "boolean"
|
||||
safesearch_enabled:
|
||||
type: "boolean"
|
||||
ClientAuto:
|
||||
type: "object"
|
||||
description: "Auto-Client information"
|
||||
properties:
|
||||
ip:
|
||||
type: "string"
|
||||
description: "IP address"
|
||||
example: "127.0.0.1"
|
||||
name:
|
||||
type: "string"
|
||||
description: "Name"
|
||||
example: "localhost"
|
||||
source:
|
||||
type: "string"
|
||||
description: "The source of this information"
|
||||
example: "etc/hosts"
|
||||
ClientUpdate:
|
||||
type: "object"
|
||||
description: "Client update request"
|
||||
properties:
|
||||
name:
|
||||
type: "string"
|
||||
data:
|
||||
$ref: "#/definitions/Client"
|
||||
ClientDelete:
|
||||
type: "object"
|
||||
description: "Client delete request"
|
||||
properties:
|
||||
name:
|
||||
type: "string"
|
||||
Clients:
|
||||
type: "object"
|
||||
properties:
|
||||
clients:
|
||||
$ref: "#/definitions/ClientsArray"
|
||||
auto_clients:
|
||||
$ref: "#/definitions/ClientsAutoArray"
|
||||
ClientsArray:
|
||||
type: "array"
|
||||
items:
|
||||
$ref: "#/definitions/Client"
|
||||
description: "Clients array"
|
||||
ClientsAutoArray:
|
||||
type: "array"
|
||||
items:
|
||||
$ref: "#/definitions/ClientAuto"
|
||||
description: "Auto-Clients array"
|
||||
CheckConfigRequest:
|
||||
type: "object"
|
||||
description: "Configuration to be checked"
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user