From 93a597fce485ab9537a481a38aeca9f7b1745023 Mon Sep 17 00:00:00 2001
From: Simon Zolin <s.zolin@adguard.com>
Date: Fri, 17 Apr 2020 14:40:13 +0300
Subject: [PATCH 01/37] - querylog: fix get data Close #1589

Squashed commit of the following:

commit 83d1d15f06a67746feb0e82fb71d5ea02f86ab69
Author: Simon Zolin <s.zolin@adguard.com>
Date:   Fri Apr 17 14:11:00 2020 +0300

    - querylog: fix get data
---
 querylog/qlog.go | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/querylog/qlog.go b/querylog/qlog.go
index f8792d8f..247bb519 100644
--- a/querylog/qlog.go
+++ b/querylog/qlog.go
@@ -238,7 +238,7 @@ func (l *queryLog) getData(params getDataParams) map[string]interface{} {
 	entries := append(memoryEntries, fileEntries...)
 	if len(entries) > getDataLimit {
 		// remove extra records
-		entries = entries[(len(entries) - getDataLimit):]
+		entries = entries[:getDataLimit]
 	}
 	if len(entries) == getDataLimit {
 		// change the "oldest" value here.

From 9b7c118103cf1033394ce1721276d3e90f262049 Mon Sep 17 00:00:00 2001
From: Simon Zolin <s.zolin@adguard.com>
Date: Tue, 21 Apr 2020 11:08:59 +0300
Subject: [PATCH 02/37] * dnsproxy v0.26.2 Close #1588

Squashed commit of the following:

commit a6eedb9883737d93332b59e9de69fbf07b8f81d4
Author: Simon Zolin <s.zolin@adguard.com>
Date:   Mon Apr 20 18:33:37 2020 +0300

    * dnsproxy v0.26.2
---
 go.mod | 2 +-
 go.sum | 4 ++--
 2 files changed, 3 insertions(+), 3 deletions(-)

diff --git a/go.mod b/go.mod
index c5f10d71..06bb22f5 100644
--- a/go.mod
+++ b/go.mod
@@ -3,7 +3,7 @@ module github.com/AdguardTeam/AdGuardHome
 go 1.14
 
 require (
-	github.com/AdguardTeam/dnsproxy v0.26.1
+	github.com/AdguardTeam/dnsproxy v0.26.2
 	github.com/AdguardTeam/golibs v0.4.2
 	github.com/AdguardTeam/urlfilter v0.10.0
 	github.com/NYTimes/gziphandler v1.1.1
diff --git a/go.sum b/go.sum
index a50d167e..bab761e2 100644
--- a/go.sum
+++ b/go.sum
@@ -1,5 +1,5 @@
-github.com/AdguardTeam/dnsproxy v0.26.1 h1:ZdrrEuNlMqiBlJpcDStitO3m6VBGCb1AP7ndaor2I6E=
-github.com/AdguardTeam/dnsproxy v0.26.1/go.mod h1:hOYFV9TW+pd5XKYz7KZf2FFD8SvSPqjyGTxUae86s58=
+github.com/AdguardTeam/dnsproxy v0.26.2 h1:VaFDlhoIbhPtnpcQ/sBNVgvpMQSvMotLS+9ZzB/xuhE=
+github.com/AdguardTeam/dnsproxy v0.26.2/go.mod h1:hOYFV9TW+pd5XKYz7KZf2FFD8SvSPqjyGTxUae86s58=
 github.com/AdguardTeam/golibs v0.4.0 h1:4VX6LoOqFe9p9Gf55BeD8BvJD6M6RDYmgEiHrENE9KU=
 github.com/AdguardTeam/golibs v0.4.0/go.mod h1:skKsDKIBB7kkFflLJBpfGX+G8QFTx0WKUzB6TIgtUj4=
 github.com/AdguardTeam/golibs v0.4.2 h1:7M28oTZFoFwNmp8eGPb3ImmYbxGaJLyQXeIFVHjME0o=

From 7f224658e0b3b0fb4799ffa44a943792d08bce41 Mon Sep 17 00:00:00 2001
From: Andrey Meshkov <am@adguard.com>
Date: Tue, 21 Apr 2020 15:05:13 +0300
Subject: [PATCH 03/37] *: moved docker files to the packaging folder

---
 .travis.yml                               |  2 +-
 Makefile                                  |  8 ++++++++
 build_docker.sh                           |  2 +-
 release.sh => build_release.sh            |  0
 Dockerfile => packaging/docker/Dockerfile |  0
 packaging/docker/Dockerfile.hub           | 23 +++++++++++++++++++++++
 packaging/docker/README.md                |  6 ++++++
 7 files changed, 39 insertions(+), 2 deletions(-)
 rename release.sh => build_release.sh (100%)
 rename Dockerfile => packaging/docker/Dockerfile (100%)
 create mode 100644 packaging/docker/Dockerfile.hub
 create mode 100644 packaging/docker/README.md

diff --git a/.travis.yml b/.travis.yml
index fd369c2e..d6fc01cc 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -86,7 +86,7 @@ matrix:
         - node -v
         - npm -v
         # Prepare releases
-        - ./release.sh
+        - ./build_release.sh
         - ls -l dist
 
       deploy:
diff --git a/Makefile b/Makefile
index 74934562..ff32c957 100644
--- a/Makefile
+++ b/Makefile
@@ -5,6 +5,9 @@ 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
+DOCKER_IMAGE_DEV_NAME=adguardhome-dev
+DOCKERFILE=packaging/docker/Dockerfile
+DOCKERFILE_HUB=packaging/docker/Dockerfile.travis
 
 TARGET=AdGuardHome
 
@@ -26,6 +29,11 @@ $(TARGET): $(STATIC) *.go home/*.go dhcpd/*.go dnsfilter/*.go dnsforward/*.go
 	CGO_ENABLED=0 go build -ldflags="-s -w -X main.version=$(GIT_VERSION) -X main.channel=$(CHANNEL) -X main.goarm=$(GOARM)" -asmflags="-trimpath=$(PWD)" -gcflags="-trimpath=$(PWD)"
 	PATH=$(GOPATH)/bin:$(PATH) packr clean
 
+docker:
+	docker build -t "$(DOCKER_IMAGE_DEV_NAME)" -f "$(DOCKERFILE)" .
+	@echo Now you can run the docker image:
+	@echo docker run --name "$(DOCKER_IMAGE_DEV_NAME)" -p 53:53/tcp -p 53:53/udp -p 3000:3000/tcp $(DOCKER_IMAGE_DEV_NAME)
+
 clean:
 	$(MAKE) cleanfast
 	rm -rf build
diff --git a/build_docker.sh b/build_docker.sh
index 1123d269..81173574 100755
--- a/build_docker.sh
+++ b/build_docker.sh
@@ -4,7 +4,7 @@ set -eE
 set -o pipefail
 set -x
 
-DOCKERFILE="Dockerfile.travis"
+DOCKERFILE="packaging/docker/Dockerfile.hub"
 IMAGE_NAME="adguard/adguardhome"
 
 if [[ "${TRAVIS_BRANCH}" == "master" ]]
diff --git a/release.sh b/build_release.sh
similarity index 100%
rename from release.sh
rename to build_release.sh
diff --git a/Dockerfile b/packaging/docker/Dockerfile
similarity index 100%
rename from Dockerfile
rename to packaging/docker/Dockerfile
diff --git a/packaging/docker/Dockerfile.hub b/packaging/docker/Dockerfile.hub
new file mode 100644
index 00000000..0e11574e
--- /dev/null
+++ b/packaging/docker/Dockerfile.hub
@@ -0,0 +1,23 @@
+FROM alpine:latest
+LABEL maintainer="AdGuard Team <devteam@adguard.com>"
+
+# Update CA certs
+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
+
+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 ["-h", "0.0.0.0", "-c", "/opt/adguardhome/conf/AdGuardHome.yaml", "-w", "/opt/adguardhome/work", "--no-check-update"]
diff --git a/packaging/docker/README.md b/packaging/docker/README.md
new file mode 100644
index 00000000..d44ec143
--- /dev/null
+++ b/packaging/docker/README.md
@@ -0,0 +1,6 @@
+## Docker images
+
+* `Dockerfile` is used for local development. Build it using `make docker` command.
+
+* `Dockerfile.huub` is used to publish AdGuard images to Docker Hub: https://hub.docker.com/r/adguard/adguardhome
+    Check out `build_docker.sh` for the details.
\ No newline at end of file

From 0cb876a71c6c9460263b9d92661e5f7aac5a393a Mon Sep 17 00:00:00 2001
From: Andrey Meshkov <am@adguard.com>
Date: Tue, 21 Apr 2020 16:25:29 +0300
Subject: [PATCH 04/37] *: added snapcraft build configuration

---
 .travis.yml                   | 16 ++++++-
 build_snap.sh                 | 81 +++++++++++++++++++++++++++++++++++
 packaging/snap/README.md      |  5 +++
 packaging/snap/snapcraft.yaml | 31 ++++++++++++++
 4 files changed, 132 insertions(+), 1 deletion(-)
 create mode 100755 build_snap.sh
 create mode 100644 packaging/snap/README.md
 create mode 100644 packaging/snap/snapcraft.yaml

diff --git a/.travis.yml b/.travis.yml
index d6fc01cc..a4f01db4 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -118,4 +118,18 @@ matrix:
         - docker login -u="$DOCKER_USER" -p="$DOCKER_PASSWORD"
         - ./build_docker.sh
       after_script:
-        - docker images    
+        - docker images
+
+    # Snapcraft build configuration
+    - if: repo = AdguardTeam/AdGuardHome
+    - name: snapcraft
+      # if: type != pull_request AND (branch = master OR tag IS present) AND repo = AdguardTeam/AdGuardHome
+      if: branch = snapcraft
+      go:
+        - 1.14.x
+      os:
+        - linux
+      services:
+        - docker
+      script:
+        - ./build_snap.sh
diff --git a/build_snap.sh b/build_snap.sh
new file mode 100755
index 00000000..2d305a52
--- /dev/null
+++ b/build_snap.sh
@@ -0,0 +1,81 @@
+#!/usr/bin/env bash
+
+set -eE
+set -o pipefail
+set -x
+
+BUILDER_IMAGE="adguard/snapcraft:1.0"
+SNAPCRAFT_TMPL="packaging/snap/snapcraft.yaml"
+SNAP_NAME="adguardhometest"
+VERSION=`git describe --abbrev=4 --dirty --always --tags`
+
+if [[ "${TRAVIS_BRANCH}" == "master" ]]
+then
+  CHANNEL="edge"
+else
+  CHANNEL="release"
+fi
+
+# Launchpad oauth tokens data is necessary to run snapcraft remote-build
+#
+# Here's an instruction on how to generate launchpad OAuth tokens:
+# https://uci.readthedocs.io/en/latest/oauth.html
+#
+# Launchpad credentials are necessary to run snapcraft remote-build command
+echo "[1]
+consumer_key = ${LAUNCHPAD_KEY}
+consumer_secret =
+access_token = ${LAUNCHPAD_ACCESS_TOKEN}
+access_secret = ${LAUNCHPAD_ACCESS_SECRET}
+" > launchpad_credentials
+
+# Snapcraft login data
+# It can be exported using snapcraft export-login command
+echo "[login.ubuntu.com]
+macaroon = ${SNAPCRAFT_MACAROON}
+unbound_discharge = ${SNAPCRAFT_UBUNTU_DISCHARGE}
+email = ${SNAPCRAFT_EMAIL}" > snapcraft_login
+
+# Prepare the snap configuration
+cp ${SNAPCRAFT_TMPL} ./snapcraft.yaml
+sed -i.bak 's/dev_version/'"${VERSION}"'/g' ./snapcraft.yaml
+
+build_snap() {
+    # Run the build
+    docker run -it -v $(pwd):/build \
+        -v $(pwd)/launchpad_credentials:/root/.local/share/snapcraft/provider/launchpad/credentials:ro \
+        ${BUILDER_IMAGE} \
+        snapcraft remote-build --build-on=${ARCH} --launchpad-accept-public-upload
+}
+
+publish_snap() {
+    # Check that the snap file exists
+    snapFile="${SNAP_NAME}_${VERSION}_${ARCH}.snap"
+    if [ ! -f ${snapFile} ]; then
+       echo "Snap file ${snapFile} not found!"
+       exit 1
+    fi
+
+    # Login and publish the snap
+    docker run -it -v $(pwd):/build \
+        ${BUILDER_IMAGE} \
+        sh -c "snapcraft login --with=/build/snapcraft_login && snapcraft push --release=${CHANNEL} /build/${snapFile}"
+}
+
+# Build snaps
+ARCH=i386 build_snap
+ARCH=arm64 build_snap
+ARCH=armhf build_snap
+ARCH=amd64 build_snap
+
+# Publish snaps
+ARCH=i386 publish_snap
+ARCH=arm64 publish_snap
+ARCH=armhf publish_snap
+ARCH=amd64 publish_snap
+
+# Clean up
+rm launchpad_credentials
+rm snapcraft.yaml
+rm snapcraft.yaml.bak
+rm snapcraft_login
\ No newline at end of file
diff --git a/packaging/snap/README.md b/packaging/snap/README.md
new file mode 100644
index 00000000..764100ff
--- /dev/null
+++ b/packaging/snap/README.md
@@ -0,0 +1,5 @@
+## Snapcraft
+
+Configuration for our snap.
+
+Check out `build_snap.sh` for more details.
\ No newline at end of file
diff --git a/packaging/snap/snapcraft.yaml b/packaging/snap/snapcraft.yaml
new file mode 100644
index 00000000..ba7a47d0
--- /dev/null
+++ b/packaging/snap/snapcraft.yaml
@@ -0,0 +1,31 @@
+name: adguardhometest
+base: core18
+version: 'dev_version'
+summary: Network-wide ads & trackers blocking DNS server
+description: |
+  AdGuard Home is a network-wide software for blocking ads & tracking. After
+  you set it up, it'll cover ALL your home devices, and you don't need any
+  client-side software for that.
+  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 servers -- both share a lot
+  of common code.
+grade: stable
+confinement: strict
+
+parts:
+  adguard-home:
+    plugin: make
+    source: .
+    build-snaps: [ node/13/stable, go ]
+    build-packages: [ git, build-essential ]
+    override-build: |
+      make clean
+      make
+      cp AdGuardHome ${SNAPCRAFT_PART_INSTALL}/
+apps:
+  adguard-home:
+    command: AdGuardHome -c ${SNAP_COMMON}/AdGuardHome.yaml -w ${SNAP_DATA} --no-check-update
+    plugs: [ network-bind ]
+    daemon: simple
+    restart-condition: always
\ No newline at end of file

From 3fdd3f7cec9f75433c1ed2ea30dd023980092ffc Mon Sep 17 00:00:00 2001
From: Andrey Meshkov <am@adguard.com>
Date: Tue, 21 Apr 2020 17:23:29 +0300
Subject: [PATCH 05/37] *: travis - print log on failuer

---
 .travis.yml | 3 +++
 1 file changed, 3 insertions(+)

diff --git a/.travis.yml b/.travis.yml
index a4f01db4..39a7bf28 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -133,3 +133,6 @@ matrix:
         - docker
       script:
         - ./build_snap.sh
+      after_failure:
+        # replace snap name
+        - cat adguardhometest_*.txt
\ No newline at end of file

From 3ae6043748bb9c396723f83a8206426908bf5ba7 Mon Sep 17 00:00:00 2001
From: Andrey Meshkov <am@adguard.com>
Date: Wed, 22 Apr 2020 13:40:13 +0300
Subject: [PATCH 06/37] *: remove snap from travis

---
 .travis.yml   | 17 -----------------
 build_snap.sh | 12 ++++++++++--
 2 files changed, 10 insertions(+), 19 deletions(-)

diff --git a/.travis.yml b/.travis.yml
index 39a7bf28..b2e5a2dd 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -119,20 +119,3 @@ matrix:
         - ./build_docker.sh
       after_script:
         - docker images
-
-    # Snapcraft build configuration
-    - if: repo = AdguardTeam/AdGuardHome
-    - name: snapcraft
-      # if: type != pull_request AND (branch = master OR tag IS present) AND repo = AdguardTeam/AdGuardHome
-      if: branch = snapcraft
-      go:
-        - 1.14.x
-      os:
-        - linux
-      services:
-        - docker
-      script:
-        - ./build_snap.sh
-      after_failure:
-        # replace snap name
-        - cat adguardhometest_*.txt
\ No newline at end of file
diff --git a/build_snap.sh b/build_snap.sh
index 2d305a52..89da6fbe 100755
--- a/build_snap.sh
+++ b/build_snap.sh
@@ -16,6 +16,12 @@ else
   CHANNEL="release"
 fi
 
+# If bash is interactive, set `-it` parameter for docker run
+INTERACTIVE=""
+if [ -t 0 ] ; then
+    INTERACTIVE="-it"
+fi
+
 # Launchpad oauth tokens data is necessary to run snapcraft remote-build
 #
 # Here's an instruction on how to generate launchpad OAuth tokens:
@@ -42,7 +48,8 @@ sed -i.bak 's/dev_version/'"${VERSION}"'/g' ./snapcraft.yaml
 
 build_snap() {
     # Run the build
-    docker run -it -v $(pwd):/build \
+    docker run ${INTERACTIVE} --rm  \
+        -v $(pwd):/build \
         -v $(pwd)/launchpad_credentials:/root/.local/share/snapcraft/provider/launchpad/credentials:ro \
         ${BUILDER_IMAGE} \
         snapcraft remote-build --build-on=${ARCH} --launchpad-accept-public-upload
@@ -57,7 +64,8 @@ publish_snap() {
     fi
 
     # Login and publish the snap
-    docker run -it -v $(pwd):/build \
+    docker run ${INTERACTIVE} --rm \
+        -v $(pwd):/build \
         ${BUILDER_IMAGE} \
         sh -c "snapcraft login --with=/build/snapcraft_login && snapcraft push --release=${CHANNEL} /build/${snapFile}"
 }

From 093617e6bd4e64f3c9a5df740af286623473e301 Mon Sep 17 00:00:00 2001
From: Andrey Meshkov <am@adguard.com>
Date: Wed, 22 Apr 2020 15:07:17 +0300
Subject: [PATCH 07/37] *: update build_snap.sh

---
 build_snap.sh | 228 +++++++++++++++++++++++++++++++++++++++++---------
 1 file changed, 190 insertions(+), 38 deletions(-)

diff --git a/build_snap.sh b/build_snap.sh
index 89da6fbe..49e05071 100755
--- a/build_snap.sh
+++ b/build_snap.sh
@@ -7,6 +7,7 @@ set -x
 BUILDER_IMAGE="adguard/snapcraft:1.0"
 SNAPCRAFT_TMPL="packaging/snap/snapcraft.yaml"
 SNAP_NAME="adguardhometest"
+LAUNCHPAD_CREDENTIALS_DIR=".local/share/snapcraft/provider/launchpad"
 VERSION=`git describe --abbrev=4 --dirty --always --tags`
 
 if [[ "${TRAVIS_BRANCH}" == "master" ]]
@@ -22,40 +23,127 @@ if [ -t 0 ] ; then
     INTERACTIVE="-it"
 fi
 
-# Launchpad oauth tokens data is necessary to run snapcraft remote-build
-#
-# Here's an instruction on how to generate launchpad OAuth tokens:
-# https://uci.readthedocs.io/en/latest/oauth.html
-#
-# Launchpad credentials are necessary to run snapcraft remote-build command
-echo "[1]
-consumer_key = ${LAUNCHPAD_KEY}
-consumer_secret =
-access_token = ${LAUNCHPAD_ACCESS_TOKEN}
-access_secret = ${LAUNCHPAD_ACCESS_SECRET}
-" > launchpad_credentials
+function usage() {
+    cat <<EOF
+    Usage: ${0##*/} command [options]
 
-# Snapcraft login data
-# It can be exported using snapcraft export-login command
-echo "[login.ubuntu.com]
-macaroon = ${SNAPCRAFT_MACAROON}
-unbound_discharge = ${SNAPCRAFT_UBUNTU_DISCHARGE}
-email = ${SNAPCRAFT_EMAIL}" > snapcraft_login
+    Please note that in order for the builds to work properly, you need to setup some env variables.
 
-# Prepare the snap configuration
-cp ${SNAPCRAFT_TMPL} ./snapcraft.yaml
-sed -i.bak 's/dev_version/'"${VERSION}"'/g' ./snapcraft.yaml
+    These are necessary for "remote-build' command.
+    Read this doc on how to generate them: https://uci.readthedocs.io/en/latest/oauth.html
+
+        * LAUNCHPAD_KEY -- launchpad CI key
+        * LAUNCHPAD_ACCESS_TOKEN -- launchpad access token
+        * LAUNCHPAD_ACCESS_SECRET -- launchpad access secret
+
+    These are necessary for snapcraft publish command to work.
+    They can be exported using "snapcraft export-login"
+
+        * SNAPCRAFT_MACAROON
+        * SNAPCRAFT_UBUNTU_DISCHARGE
+        * SNAPCRAFT_EMAIL
+
+    Examples:
+        ${0##*/} build-docker - builds snaps using remote-build inside a Docker environment
+        ${0##*/} build - builds snaps using remote-build
+        ${0##*/} publish-docker-beta - publishes snaps to the beta channel using Docker environment
+        ${0##*/} publish-docker-release - publishes snaps to the release channel using Docker environment
+        ${0##*/} publish-beta - publishes snaps to the beta channel
+        ${0##*/} publish-release - publishes snaps to the release channel
+        ${0##*/} cleanup - clean up temporary files that were created by the builds
+EOF
+    exit 1
+}
+
+#######################################
+# helper functions
+#######################################
+
+function prepare() {
+    # Launchpad oauth tokens data is necessary to run snapcraft remote-build
+    #
+    # Here's an instruction on how to generate launchpad OAuth tokens:
+    # https://uci.readthedocs.io/en/latest/oauth.html
+    #
+    # Launchpad credentials are necessary to run snapcraft remote-build command
+    echo "[1]
+    consumer_key = ${LAUNCHPAD_KEY}
+    consumer_secret =
+    access_token = ${LAUNCHPAD_ACCESS_TOKEN}
+    access_secret = ${LAUNCHPAD_ACCESS_SECRET}
+    " > launchpad_credentials
+
+    # Snapcraft login data
+    # It can be exported using snapcraft export-login command
+    echo "[login.ubuntu.com]
+    macaroon = ${SNAPCRAFT_MACAROON}
+    unbound_discharge = ${SNAPCRAFT_UBUNTU_DISCHARGE}
+    email = ${SNAPCRAFT_EMAIL}" > snapcraft_login
+
+    # Prepare the snap configuration
+    cp ${SNAPCRAFT_TMPL} ./snapcraft.yaml
+    sed -i.bak 's/dev_version/'"${VERSION}"'/g' ./snapcraft.yaml
+    rm -f snapcraft.yaml.bak
+}
 
 build_snap() {
-    # Run the build
+    # prepare credentials
+    prepare
+
+    # copy them to the directory where snapcraft will be able to read them
+    mkdir -p ~/${LAUNCHPAD_CREDENTIALS_DIR}
+    cp -f snapcraft_login ~/${LAUNCHPAD_CREDENTIALS_DIR}/credentials
+    chmod 600 ~/${LAUNCHPAD_CREDENTIALS_DIR}/credentials
+
+    # run the build
+    snapcraft remote-build --build-on=${ARCH} --launchpad-accept-public-upload
+
+    # remove the credentials - we don't need them anymore
+    rm -rf ~/${LAUNCHPAD_CREDENTIALS_DIR}
+
+    # cleanup credentials
+    cleanup
+}
+
+build_snap_docker() {
+    # prepare credentials
+    prepare
+
     docker run ${INTERACTIVE} --rm  \
         -v $(pwd):/build \
-        -v $(pwd)/launchpad_credentials:/root/.local/share/snapcraft/provider/launchpad/credentials:ro \
+        -v $(pwd)/launchpad_credentials:/root/${LAUNCHPAD_CREDENTIALS_DIR}/credentials:ro \
         ${BUILDER_IMAGE} \
         snapcraft remote-build --build-on=${ARCH} --launchpad-accept-public-upload
+
+    # cleanup credentials
+    cleanup
 }
 
 publish_snap() {
+    # prepare credentials
+    prepare
+
+    # Check that the snap file exists
+    snapFile="${SNAP_NAME}_${VERSION}_${ARCH}.snap"
+    if [ ! -f ${snapFile} ]; then
+       echo "Snap file ${snapFile} not found!"
+       exit 1
+    fi
+
+    # Login if necessary
+    snapcraft login --with=snapcraft_login
+
+    # Push to the channel
+    snapcraft push --release=${CHANNEL} ${snapFile}
+
+    # cleanup credentials
+    cleanup
+}
+
+publish_snap_docker() {
+    # prepare credentials
+    prepare
+
     # Check that the snap file exists
     snapFile="${SNAP_NAME}_${VERSION}_${ARCH}.snap"
     if [ ! -f ${snapFile} ]; then
@@ -68,22 +156,86 @@ publish_snap() {
         -v $(pwd):/build \
         ${BUILDER_IMAGE} \
         sh -c "snapcraft login --with=/build/snapcraft_login && snapcraft push --release=${CHANNEL} /build/${snapFile}"
+
+    # cleanup credentials
+    cleanup
 }
 
-# Build snaps
-ARCH=i386 build_snap
-ARCH=arm64 build_snap
-ARCH=armhf build_snap
-ARCH=amd64 build_snap
+#######################################
+# main functions
+#######################################
 
-# Publish snaps
-ARCH=i386 publish_snap
-ARCH=arm64 publish_snap
-ARCH=armhf publish_snap
-ARCH=amd64 publish_snap
+build() {
+    ARCH=i386 build_snap
+    ARCH=arm64 build_snap
+    ARCH=armhf build_snap
+    ARCH=amd64 build_snap
+}
 
-# Clean up
-rm launchpad_credentials
-rm snapcraft.yaml
-rm snapcraft.yaml.bak
-rm snapcraft_login
\ No newline at end of file
+build_docker() {
+    ARCH=i386 build_snap_docker
+    ARCH=arm64 build_snap_docker
+    ARCH=armhf build_snap_docker
+    ARCH=amd64 build_snap_docker
+}
+
+publish_docker() {
+    if [[ -z $1 ]]; then
+        echo "No channel specified"
+        exit 1
+    fi
+    CHANNEL="${1}"
+    if [ "$CHANNEL" != "release" ] && [ "$CHANNEL" != "beta" ]; then
+        echo "$CHANNEL is an invalid value for the update channel!"
+        exit 1
+    fi
+
+    ARCH=i386 publish_snap_docker
+    ARCH=arm64 publish_snap_docker
+    ARCH=armhf publish_snap_docker
+    ARCH=amd64 publish_snap_docker
+}
+
+publish() {
+    if [[ -z $1 ]]; then
+        echo "No channel specified"
+        exit 1
+    fi
+    CHANNEL="${1}"
+    if [ "$CHANNEL" != "release" ] && [ "$CHANNEL" != "beta" ]; then
+        echo "$CHANNEL is an invalid value for the update channel!"
+        exit 1
+    fi
+
+    ARCH=i386 publish_snap
+    ARCH=arm64 publish_snap
+    ARCH=armhf publish_snap
+    ARCH=amd64 publish_snap
+}
+
+cleanup() {
+    rm -f launchpad_credentials
+    rm -f snapcraft.yaml
+    rm -f snapcraft.yaml.bak
+    rm -f snapcraft_login
+}
+
+#######################################
+# main
+#######################################
+if [[ -z $1 || $1 == "--help" || $1 == "-h" ]]; then
+    usage
+fi
+
+case "$1" in
+"build-docker") build_docker ;;
+"build") build ;;
+"publish-docker-beta") publish_docker beta ;;
+"publish-docker-release") publish_docker release ;;
+"publish-beta") publish beta ;;
+"publish-release") publish release ;;
+"cleanup") cleanup ;;
+*) usage ;;
+esac
+
+exit 0
\ No newline at end of file

From 9671050e5b520e3cf979b113f19e554e6402a3c6 Mon Sep 17 00:00:00 2001
From: Simon Zolin <s.zolin@adguard.com>
Date: Wed, 22 Apr 2020 16:00:26 +0300
Subject: [PATCH 08/37] + config: add "debug_pprof" setting

Squashed commit of the following:

commit 96d185cf4acc55b21a00d10072e0a641ef7655b8
Merge: 3b75a8cb 9b7c1181
Author: Simon Zolin <s.zolin@adguard.com>
Date:   Wed Apr 22 15:57:50 2020 +0300

    Merge remote-tracking branch 'origin/master' into add-pprof

commit 3b75a8cbec5d72be8865a56bfd7ebb8b0673c3bc
Author: Simon Zolin <s.zolin@adguard.com>
Date:   Wed Apr 22 13:52:36 2020 +0300

    * use localhost:6060 for pprof

commit f66f2fbd7409b98cd9f7d297c268fca998f85e3b
Author: Simon Zolin <s.zolin@adguard.com>
Date:   Wed Apr 22 12:46:25 2020 +0300

    minor

commit 6e38712fb5542f612675858eb957efdefc38f9b0
Author: Simon Zolin <s.zolin@adguard.com>
Date:   Wed Apr 22 11:59:39 2020 +0300

    use separate HTTP server for pprof

commit 988d95b5fad22f536bf9204b6b96f3697cf9a589
Author: Simon Zolin <s.zolin@adguard.com>
Date:   Tue Apr 21 17:04:04 2020 +0300

    minor

commit 90ee6e9753be2af49467687cdf71c35b3943b78b
Author: Simon Zolin <s.zolin@adguard.com>
Date:   Tue Apr 21 16:57:38 2020 +0300

    * use modified version of pprof

commit 413002220fe0717950539a8b7e6b0f31cef31bb8
Author: Simon Zolin <s.zolin@adguard.com>
Date:   Mon Apr 20 16:35:21 2020 +0300

    minor

commit d4655d3849de1d9fe97efdb7f18fc21d5ac19eda
Author: Simon Zolin <s.zolin@adguard.com>
Date:   Mon Apr 20 15:57:57 2020 +0300

    + config: add "enable_pprof" setting
---
 .golangci.yml  |   5 +-
 home/config.go |   1 +
 home/home.go   |  10 ++
 util/pprof.go  | 352 +++++++++++++++++++++++++++++++++++++++++++++++++
 4 files changed, 367 insertions(+), 1 deletion(-)
 create mode 100644 util/pprof.go

diff --git a/.golangci.yml b/.golangci.yml
index 341a62d4..60b7ee1c 100644
--- a/.golangci.yml
+++ b/.golangci.yml
@@ -13,6 +13,7 @@ run:
   skip-files:
     - ".*generated.*"
     - dnsfilter/rule_to_regexp.go
+    - util/pprof.go
     - ".*_test.go"
 
 
@@ -65,5 +66,7 @@ issues:
     - Error return value of ..*.Shutdown.
     # goconst
     - string .forcesafesearch.google.com. has 3 occurrences
+    # gosec: Profiling endpoint is automatically exposed on /debug/pprof
+    - G108
     # gosec: Subprocess launched with function call as argument or cmd arguments
-    - G204
\ No newline at end of file
+    - G204
diff --git a/home/config.go b/home/config.go
index 2d78b42c..35940ebd 100644
--- a/home/config.go
+++ b/home/config.go
@@ -45,6 +45,7 @@ type configuration struct {
 	ProxyURL     string `yaml:"http_proxy"`    // Proxy address for our HTTP client
 	Language     string `yaml:"language"`      // two-letter ISO 639-1 language code
 	RlimitNoFile uint   `yaml:"rlimit_nofile"` // Maximum number of opened fd's per process (0: default)
+	DebugPProf   bool   `yaml:"debug_pprof"`   // Enable pprof HTTP server on port 6060
 
 	// TTL for a web session (in hours)
 	// An active session is automatically refreshed once a day.
diff --git a/home/home.go b/home/home.go
index 98ad8841..2340ca6c 100644
--- a/home/home.go
+++ b/home/home.go
@@ -242,6 +242,16 @@ func run(args options) {
 		if err != nil {
 			log.Fatal(err)
 		}
+
+		if config.DebugPProf {
+			mux := http.NewServeMux()
+			util.PProfRegisterWebHandlers(mux)
+			go func() {
+				log.Info("pprof: listening on localhost:6060")
+				err := http.ListenAndServe("localhost:6060", mux)
+				log.Error("Error while running the pprof server: %s", err)
+			}()
+		}
 	}
 
 	err := os.MkdirAll(Context.getDataDir(), 0755)
diff --git a/util/pprof.go b/util/pprof.go
new file mode 100644
index 00000000..031e6c78
--- /dev/null
+++ b/util/pprof.go
@@ -0,0 +1,352 @@
+// Modified pprof package
+// The problem with the mainstream package is that it registers HTTP handlers
+//  inside init() function.
+// This behaviour makes it impossible to enable pprof on demand in runtime.
+// But this package has a separate function for this -
+//  PProfRegisterWebHandlers().
+
+// Copyright 2010 The Go Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+// Package pprof serves via its HTTP server runtime profiling data
+// in the format expected by the pprof visualization tool.
+//
+// The package is typically only imported for the side effect of
+// registering its HTTP handlers.
+// The handled paths all begin with /debug/pprof/.
+//
+// To use pprof, link this package into your program:
+//	import _ "net/http/pprof"
+//
+// If your application is not already running an http server, you
+// need to start one. Add "net/http" and "log" to your imports and
+// the following code to your main function:
+//
+// 	go func() {
+// 		log.Println(http.ListenAndServe("localhost:6060", nil))
+// 	}()
+//
+// If you are not using DefaultServeMux, you will have to register handlers
+// with the mux you are using.
+//
+// Then use the pprof tool to look at the heap profile:
+//
+//	go tool pprof http://localhost:6060/debug/pprof/heap
+//
+// Or to look at a 30-second CPU profile:
+//
+//	go tool pprof http://localhost:6060/debug/pprof/profile?seconds=30
+//
+// Or to look at the goroutine blocking profile, after calling
+// runtime.SetBlockProfileRate in your program:
+//
+//	go tool pprof http://localhost:6060/debug/pprof/block
+//
+// Or to collect a 5-second execution trace:
+//
+//	wget http://localhost:6060/debug/pprof/trace?seconds=5
+//
+// Or to look at the holders of contended mutexes, after calling
+// runtime.SetMutexProfileFraction in your program:
+//
+//	go tool pprof http://localhost:6060/debug/pprof/mutex
+//
+// To view all available profiles, open http://localhost:6060/debug/pprof/
+// in your browser.
+//
+// For a study of the facility in action, visit
+//
+//	https://blog.golang.org/2011/06/profiling-go-programs.html
+//
+package util
+
+import (
+	"bufio"
+	"bytes"
+	"fmt"
+	"html/template"
+	"io"
+	"log"
+	"net/http"
+	"os"
+	"runtime"
+	"runtime/pprof"
+	"runtime/trace"
+	"sort"
+	"strconv"
+	"strings"
+	"time"
+)
+
+// PProfRegisterWebHandlers - register HTTP handlers for pprof
+func PProfRegisterWebHandlers(mux *http.ServeMux) {
+	mux.HandleFunc("/debug/pprof/", Index)
+	mux.HandleFunc("/debug/pprof/cmdline", Cmdline)
+	mux.HandleFunc("/debug/pprof/profile", Profile)
+	mux.HandleFunc("/debug/pprof/symbol", Symbol)
+	mux.HandleFunc("/debug/pprof/trace", Trace)
+}
+
+// Cmdline responds with the running program's
+// command line, with arguments separated by NUL bytes.
+// The package initialization registers it as /debug/pprof/cmdline.
+func Cmdline(w http.ResponseWriter, r *http.Request) {
+	w.Header().Set("X-Content-Type-Options", "nosniff")
+	w.Header().Set("Content-Type", "text/plain; charset=utf-8")
+	fmt.Fprintf(w, strings.Join(os.Args, "\x00"))
+}
+
+func sleep(w http.ResponseWriter, d time.Duration) {
+	var clientGone <-chan bool
+	if cn, ok := w.(http.CloseNotifier); ok {
+		clientGone = cn.CloseNotify()
+	}
+	select {
+	case <-time.After(d):
+	case <-clientGone:
+	}
+}
+
+func durationExceedsWriteTimeout(r *http.Request, seconds float64) bool {
+	srv, ok := r.Context().Value(http.ServerContextKey).(*http.Server)
+	return ok && srv.WriteTimeout != 0 && seconds >= srv.WriteTimeout.Seconds()
+}
+
+func serveError(w http.ResponseWriter, status int, txt string) {
+	w.Header().Set("Content-Type", "text/plain; charset=utf-8")
+	w.Header().Set("X-Go-Pprof", "1")
+	w.Header().Del("Content-Disposition")
+	w.WriteHeader(status)
+	fmt.Fprintln(w, txt)
+}
+
+// Profile responds with the pprof-formatted cpu profile.
+// Profiling lasts for duration specified in seconds GET parameter, or for 30 seconds if not specified.
+// The package initialization registers it as /debug/pprof/profile.
+func Profile(w http.ResponseWriter, r *http.Request) {
+	w.Header().Set("X-Content-Type-Options", "nosniff")
+	sec, err := strconv.ParseInt(r.FormValue("seconds"), 10, 64)
+	if sec <= 0 || err != nil {
+		sec = 30
+	}
+
+	if durationExceedsWriteTimeout(r, float64(sec)) {
+		serveError(w, http.StatusBadRequest, "profile duration exceeds server's WriteTimeout")
+		return
+	}
+
+	// Set Content Type assuming StartCPUProfile will work,
+	// because if it does it starts writing.
+	w.Header().Set("Content-Type", "application/octet-stream")
+	w.Header().Set("Content-Disposition", `attachment; filename="profile"`)
+	if err := pprof.StartCPUProfile(w); err != nil {
+		// StartCPUProfile failed, so no writes yet.
+		serveError(w, http.StatusInternalServerError,
+			fmt.Sprintf("Could not enable CPU profiling: %s", err))
+		return
+	}
+	sleep(w, time.Duration(sec)*time.Second)
+	pprof.StopCPUProfile()
+}
+
+// Trace responds with the execution trace in binary form.
+// Tracing lasts for duration specified in seconds GET parameter, or for 1 second if not specified.
+// The package initialization registers it as /debug/pprof/trace.
+func Trace(w http.ResponseWriter, r *http.Request) {
+	w.Header().Set("X-Content-Type-Options", "nosniff")
+	sec, err := strconv.ParseFloat(r.FormValue("seconds"), 64)
+	if sec <= 0 || err != nil {
+		sec = 1
+	}
+
+	if durationExceedsWriteTimeout(r, sec) {
+		serveError(w, http.StatusBadRequest, "profile duration exceeds server's WriteTimeout")
+		return
+	}
+
+	// Set Content Type assuming trace.Start will work,
+	// because if it does it starts writing.
+	w.Header().Set("Content-Type", "application/octet-stream")
+	w.Header().Set("Content-Disposition", `attachment; filename="trace"`)
+	if err := trace.Start(w); err != nil {
+		// trace.Start failed, so no writes yet.
+		serveError(w, http.StatusInternalServerError,
+			fmt.Sprintf("Could not enable tracing: %s", err))
+		return
+	}
+	sleep(w, time.Duration(sec*float64(time.Second)))
+	trace.Stop()
+}
+
+// Symbol looks up the program counters listed in the request,
+// responding with a table mapping program counters to function names.
+// The package initialization registers it as /debug/pprof/symbol.
+func Symbol(w http.ResponseWriter, r *http.Request) {
+	w.Header().Set("X-Content-Type-Options", "nosniff")
+	w.Header().Set("Content-Type", "text/plain; charset=utf-8")
+
+	// We have to read the whole POST body before
+	// writing any output. Buffer the output here.
+	var buf bytes.Buffer
+
+	// We don't know how many symbols we have, but we
+	// do have symbol information. Pprof only cares whether
+	// this number is 0 (no symbols available) or > 0.
+	fmt.Fprintf(&buf, "num_symbols: 1\n")
+
+	var b *bufio.Reader
+	if r.Method == "POST" {
+		b = bufio.NewReader(r.Body)
+	} else {
+		b = bufio.NewReader(strings.NewReader(r.URL.RawQuery))
+	}
+
+	for {
+		word, err := b.ReadSlice('+')
+		if err == nil {
+			word = word[0 : len(word)-1] // trim +
+		}
+		pc, _ := strconv.ParseUint(string(word), 0, 64)
+		if pc != 0 {
+			f := runtime.FuncForPC(uintptr(pc))
+			if f != nil {
+				fmt.Fprintf(&buf, "%#x %s\n", pc, f.Name())
+			}
+		}
+
+		// Wait until here to check for err; the last
+		// symbol will have an err because it doesn't end in +.
+		if err != nil {
+			if err != io.EOF {
+				fmt.Fprintf(&buf, "reading request: %v\n", err)
+			}
+			break
+		}
+	}
+
+	w.Write(buf.Bytes())
+}
+
+// Handler returns an HTTP handler that serves the named profile.
+func Handler(name string) http.Handler {
+	return handler(name)
+}
+
+type handler string
+
+func (name handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
+	w.Header().Set("X-Content-Type-Options", "nosniff")
+	p := pprof.Lookup(string(name))
+	if p == nil {
+		serveError(w, http.StatusNotFound, "Unknown profile")
+		return
+	}
+	gc, _ := strconv.Atoi(r.FormValue("gc"))
+	if name == "heap" && gc > 0 {
+		runtime.GC()
+	}
+	debug, _ := strconv.Atoi(r.FormValue("debug"))
+	if debug != 0 {
+		w.Header().Set("Content-Type", "text/plain; charset=utf-8")
+	} else {
+		w.Header().Set("Content-Type", "application/octet-stream")
+		w.Header().Set("Content-Disposition", fmt.Sprintf(`attachment; filename="%s"`, name))
+	}
+	p.WriteTo(w, debug)
+}
+
+var profileDescriptions = map[string]string{
+	"allocs":       "A sampling of all past memory allocations",
+	"block":        "Stack traces that led to blocking on synchronization primitives",
+	"cmdline":      "The command line invocation of the current program",
+	"goroutine":    "Stack traces of all current goroutines",
+	"heap":         "A sampling of memory allocations of live objects. You can specify the gc GET parameter to run GC before taking the heap sample.",
+	"mutex":        "Stack traces of holders of contended mutexes",
+	"profile":      "CPU profile. You can specify the duration in the seconds GET parameter. After you get the profile file, use the go tool pprof command to investigate the profile.",
+	"threadcreate": "Stack traces that led to the creation of new OS threads",
+	"trace":        "A trace of execution of the current program. You can specify the duration in the seconds GET parameter. After you get the trace file, use the go tool trace command to investigate the trace.",
+}
+
+// Index responds with the pprof-formatted profile named by the request.
+// For example, "/debug/pprof/heap" serves the "heap" profile.
+// Index responds to a request for "/debug/pprof/" with an HTML page
+// listing the available profiles.
+func Index(w http.ResponseWriter, r *http.Request) {
+	if strings.HasPrefix(r.URL.Path, "/debug/pprof/") {
+		name := strings.TrimPrefix(r.URL.Path, "/debug/pprof/")
+		if name != "" {
+			handler(name).ServeHTTP(w, r)
+			return
+		}
+	}
+
+	type profile struct {
+		Name  string
+		Href  string
+		Desc  string
+		Count int
+	}
+	var profiles []profile
+	for _, p := range pprof.Profiles() {
+		profiles = append(profiles, profile{
+			Name:  p.Name(),
+			Href:  p.Name() + "?debug=1",
+			Desc:  profileDescriptions[p.Name()],
+			Count: p.Count(),
+		})
+	}
+
+	// Adding other profiles exposed from within this package
+	for _, p := range []string{"cmdline", "profile", "trace"} {
+		profiles = append(profiles, profile{
+			Name: p,
+			Href: p,
+			Desc: profileDescriptions[p],
+		})
+	}
+
+	sort.Slice(profiles, func(i, j int) bool {
+		return profiles[i].Name < profiles[j].Name
+	})
+
+	if err := indexTmpl.Execute(w, profiles); err != nil {
+		log.Print(err)
+	}
+}
+
+var indexTmpl = template.Must(template.New("index").Parse(`<html>
+<head>
+<title>/debug/pprof/</title>
+<style>
+.profile-name{
+	display:inline-block;
+	width:6rem;
+}
+</style>
+</head>
+<body>
+/debug/pprof/<br>
+<br>
+Types of profiles available:
+<table>
+<thead><td>Count</td><td>Profile</td></thead>
+{{range .}}
+	<tr>
+	<td>{{.Count}}</td><td><a href={{.Href}}>{{.Name}}</a></td>
+	</tr>
+{{end}}
+</table>
+<a href="goroutine?debug=2">full goroutine stack dump</a>
+<br/>
+<p>
+Profile Descriptions:
+<ul>
+{{range .}}
+<li><div class=profile-name>{{.Name}}:</div> {{.Desc}}</li>
+{{end}}
+</ul>
+</p>
+</body>
+</html>
+`))

From daf9af9a224931135863a8be9ba46545148fd372 Mon Sep 17 00:00:00 2001
From: Andrey Meshkov <am@adguard.com>
Date: Wed, 22 Apr 2020 17:48:37 +0300
Subject: [PATCH 09/37] *: rename resulting snapfile -- remove version from it

---
 build_snap.sh | 45 +++++++++++++++++++++++++++++++++++++--------
 1 file changed, 37 insertions(+), 8 deletions(-)

diff --git a/build_snap.sh b/build_snap.sh
index 49e05071..4e9a00bd 100755
--- a/build_snap.sh
+++ b/build_snap.sh
@@ -8,13 +8,10 @@ BUILDER_IMAGE="adguard/snapcraft:1.0"
 SNAPCRAFT_TMPL="packaging/snap/snapcraft.yaml"
 SNAP_NAME="adguardhometest"
 LAUNCHPAD_CREDENTIALS_DIR=".local/share/snapcraft/provider/launchpad"
-VERSION=`git describe --abbrev=4 --dirty --always --tags`
 
-if [[ "${TRAVIS_BRANCH}" == "master" ]]
-then
-  CHANNEL="edge"
-else
-  CHANNEL="release"
+if [[ -z ${VERSION} ]]; then
+    VERSION=`git describe --abbrev=4 --dirty --always --tags`
+    echo "VERSION env variable is not set, getting it from git: ${VERSION}"
 fi
 
 # If bash is interactive, set `-it` parameter for docker run
@@ -60,6 +57,18 @@ EOF
 #######################################
 
 function prepare() {
+    if [ -z ${LAUNCHPAD_KEY} ] || [ -z ${LAUNCHPAD_ACCESS_TOKEN} ] || [ -z ${LAUNCHPAD_ACCESS_SECRET} ]; then
+        echo "Launchpad oauth tokens are not set, exiting"
+        usage
+        exit 1
+    fi
+
+    if [ -z ${SNAPCRAFT_MACAROON} ] || [ -z ${SNAPCRAFT_UBUNTU_DISCHARGE} ] || [ -z ${SNAPCRAFT_EMAIL} ]; then
+        echo "Snapcraft auth params are not set, exiting"
+        usage
+        exit 1
+    fi
+
     # Launchpad oauth tokens data is necessary to run snapcraft remote-build
     #
     # Here's an instruction on how to generate launchpad OAuth tokens:
@@ -101,6 +110,9 @@ build_snap() {
     # remove the credentials - we don't need them anymore
     rm -rf ~/${LAUNCHPAD_CREDENTIALS_DIR}
 
+    # remove version from the file name
+    rename_snap_file
+
     # cleanup credentials
     cleanup
 }
@@ -115,16 +127,33 @@ build_snap_docker() {
         ${BUILDER_IMAGE} \
         snapcraft remote-build --build-on=${ARCH} --launchpad-accept-public-upload
 
+    # remove version from the file name
+    rename_snap_file
+
     # cleanup credentials
     cleanup
 }
 
+rename_snap_file() {
+    # In order to make working with snaps easier later on
+    # we remove version from the file name
+
+    # Check that the snap file exists
+    snapFile="${SNAP_NAME}_${VERSION}_${ARCH}.snap"
+    if [ ! -f ${snapFile} ]; then
+       echo "Snap file ${snapFile} not found!"
+       exit 1
+    fi
+
+    mv -f ${snapFile} "${SNAP_NAME}_${ARCH}.snap"
+}
+
 publish_snap() {
     # prepare credentials
     prepare
 
     # Check that the snap file exists
-    snapFile="${SNAP_NAME}_${VERSION}_${ARCH}.snap"
+    snapFile="${SNAP_NAME}_${ARCH}.snap"
     if [ ! -f ${snapFile} ]; then
        echo "Snap file ${snapFile} not found!"
        exit 1
@@ -145,7 +174,7 @@ publish_snap_docker() {
     prepare
 
     # Check that the snap file exists
-    snapFile="${SNAP_NAME}_${VERSION}_${ARCH}.snap"
+    snapFile="${SNAP_NAME}_${ARCH}.snap"
     if [ ! -f ${snapFile} ]; then
        echo "Snap file ${snapFile} not found!"
        exit 1

From 47160c16d9f5c2226f33af52a503e5826c1748ec Mon Sep 17 00:00:00 2001
From: Andrey Meshkov <am@adguard.com>
Date: Wed, 22 Apr 2020 18:39:47 +0300
Subject: [PATCH 10/37] *: cleanup

---
 build_snap.sh | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/build_snap.sh b/build_snap.sh
index 4e9a00bd..8879fd22 100755
--- a/build_snap.sh
+++ b/build_snap.sh
@@ -57,13 +57,13 @@ EOF
 #######################################
 
 function prepare() {
-    if [ -z ${LAUNCHPAD_KEY} ] || [ -z ${LAUNCHPAD_ACCESS_TOKEN} ] || [ -z ${LAUNCHPAD_ACCESS_SECRET} ]; then
+    if [ -z "${LAUNCHPAD_KEY}" ] || [ -z "${LAUNCHPAD_ACCESS_TOKEN}" ] || [ -z "${LAUNCHPAD_ACCESS_SECRET}" ]; then
         echo "Launchpad oauth tokens are not set, exiting"
         usage
         exit 1
     fi
 
-    if [ -z ${SNAPCRAFT_MACAROON} ] || [ -z ${SNAPCRAFT_UBUNTU_DISCHARGE} ] || [ -z ${SNAPCRAFT_EMAIL} ]; then
+    if [ -z "${SNAPCRAFT_MACAROON}" ] || [ -z "${SNAPCRAFT_UBUNTU_DISCHARGE}" ] || [ -z "${SNAPCRAFT_EMAIL}" ]; then
         echo "Snapcraft auth params are not set, exiting"
         usage
         exit 1

From 054980bc8b9cab4ac8dd5421d724f421cd046583 Mon Sep 17 00:00:00 2001
From: Andrey Meshkov <am@adguard.com>
Date: Wed, 22 Apr 2020 18:50:24 +0300
Subject: [PATCH 11/37] *: fix typo

---
 packaging/docker/README.md | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/packaging/docker/README.md b/packaging/docker/README.md
index d44ec143..0d7ff06f 100644
--- a/packaging/docker/README.md
+++ b/packaging/docker/README.md
@@ -2,5 +2,5 @@
 
 * `Dockerfile` is used for local development. Build it using `make docker` command.
 
-* `Dockerfile.huub` is used to publish AdGuard images to Docker Hub: https://hub.docker.com/r/adguard/adguardhome
+* `Dockerfile.hub` is used to publish AdGuard images to Docker Hub: https://hub.docker.com/r/adguard/adguardhome
     Check out `build_docker.sh` for the details.
\ No newline at end of file

From e2ddc82d70ecff9ed2ddedb68bffaf7937a21370 Mon Sep 17 00:00:00 2001
From: Simon Zolin <s.zolin@adguard.com>
Date: Wed, 22 Apr 2020 19:14:04 +0300
Subject: [PATCH 12/37] + DNS: add fastest_addr setting

* API: /dns_info, /dns_config: add "parallel_requests" instead of "all_servers" from /set_upstreams_config
* API: /status: removed fields

#715

Squashed commit of the following:

commit 7dd913bd336ecbaa7419b998d0bf913d89702fe6
Merge: 43706970 8170955a
Author: Simon Zolin <s.zolin@adguard.com>
Date:   Wed Apr 22 19:09:36 2020 +0300

    Merge remote-tracking branch 'origin/master' into 715-fastest-addr

commit 437069702a3e91e0b066e4b22b08cdc02ff19eaf
Author: Simon Zolin <s.zolin@adguard.com>
Date:   Wed Apr 22 19:08:55 2020 +0300

    minor

commit 9e713df80c5bf113c98794c0a20915c756a76938
Merge: e3bf4037 9b7c1181
Author: Simon Zolin <s.zolin@adguard.com>
Date:   Tue Apr 21 16:02:03 2020 +0300

    Merge remote-tracking branch 'origin/master' into 715-fastest-addr

commit e3bf4037f49198e42bde55305d6f9077341b556a
Author: Simon Zolin <s.zolin@adguard.com>
Date:   Tue Apr 21 15:40:49 2020 +0300

    minor

commit d6e6a823c5e51acc061b2850d362772efcb827e1
Author: Simon Zolin <s.zolin@adguard.com>
Date:   Fri Apr 17 17:56:24 2020 +0300

    * API changes

    . removed POST /set_upstreams_config
    . removed fields from GET /status: bootstrap_dns, upstream_dns, all_servers
    . added new fields to /dns_config and /dns_info

commit 237a452d09cc48ff8f00e81c7fd35e7828bea835
Author: Simon Zolin <s.zolin@adguard.com>
Date:   Fri Apr 17 16:43:13 2020 +0300

    * API: /dns_info, /dns_config: add "parallel_requests" instead of "all_servers" from /set_upstreams_config

commit 9976723b9725ed19e0cce152d1d1198b13c4acc1
Author: Simon Zolin <s.zolin@adguard.com>
Date:   Mon Mar 23 10:28:25 2020 +0300

    openapi

commit 6f8ea16c6332606f29095b0094d71e8a91798f82
Merge: 36e4d4e8 c8285c41
Author: Simon Zolin <s.zolin@adguard.com>
Date:   Fri Mar 20 19:18:48 2020 +0300

    Merge remote-tracking branch 'origin/master' into 715-fastest-addr

commit 36e4d4e82cadeaba5a11313f0d69d66a0924c342
Author: Simon Zolin <s.zolin@adguard.com>
Date:   Fri Mar 20 18:13:43 2020 +0300

    + DNS: add fastest_addr setting
---
 AGHTechDoc.md                 | 10 ++++
 dnsforward/dnsforward.go      |  3 ++
 dnsforward/dnsforward_http.go | 94 ++++++++++++++++++-----------------
 home/control.go               |  3 --
 openapi/CHANGELOG.md          | 64 ++++++++++++++++++++++++
 openapi/openapi.yaml          | 57 +++++++++------------
 6 files changed, 148 insertions(+), 83 deletions(-)

diff --git a/AGHTechDoc.md b/AGHTechDoc.md
index 4c8e0fda..5a3b001c 100644
--- a/AGHTechDoc.md
+++ b/AGHTechDoc.md
@@ -882,6 +882,9 @@ Response:
 	200 OK
 
 	{
+		"upstream_dns": ["tls://...", ...],
+		"bootstrap_dns": ["1.2.3.4", ...],
+
 		"protection_enabled": true | false,
 		"ratelimit": 1234,
 		"blocking_mode": "default" | "nxdomain" | "null_ip" | "custom_ip",
@@ -890,6 +893,8 @@ Response:
 		"edns_cs_enabled": true | false,
 		"dnssec_enabled": true | false
 		"disable_ipv6": true | false,
+		"fastest_addr": true | false, // use Fastest Address algorithm
+		"parallel_requests": true | false, // send DNS requests to all upstream servers at once
 	}
 
 
@@ -900,6 +905,9 @@ Request:
 	POST /control/dns_config
 
 	{
+		"upstream_dns": ["tls://...", ...],
+		"bootstrap_dns": ["1.2.3.4", ...],
+
 		"protection_enabled": true | false,
 		"ratelimit": 1234,
 		"blocking_mode": "default" | "nxdomain" | "null_ip" | "custom_ip",
@@ -908,6 +916,8 @@ Request:
 		"edns_cs_enabled": true | false,
 		"dnssec_enabled": true | false
 		"disable_ipv6": true | false,
+		"fastest_addr": true | false, // use Fastest Address algorithm
+		"parallel_requests": true | false, // send DNS requests to all upstream servers at once
 	}
 
 Response:
diff --git a/dnsforward/dnsforward.go b/dnsforward/dnsforward.go
index b191a966..4f630afb 100644
--- a/dnsforward/dnsforward.go
+++ b/dnsforward/dnsforward.go
@@ -141,6 +141,8 @@ type FilteringConfig struct {
 	// Respond with an empty answer to all AAAA requests
 	AAAADisabled bool `yaml:"aaaa_disabled"`
 
+	FastestAddrAlgo bool `yaml:"fastest_addr"` // use Fastest Address algorithm
+
 	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
@@ -305,6 +307,7 @@ func (s *Server) Prepare(config *ServerConfig) error {
 		RequestHandler:           s.handleDNSRequest,
 		AllServers:               s.conf.AllServers,
 		EnableEDNSClientSubnet:   s.conf.EnableEDNSClientSubnet,
+		FindFastestAddr:          s.conf.FastestAddrAlgo,
 	}
 
 	intlProxyConfig := proxy.Config{
diff --git a/dnsforward/dnsforward_http.go b/dnsforward/dnsforward_http.go
index 414b728b..4658cbbf 100644
--- a/dnsforward/dnsforward_http.go
+++ b/dnsforward/dnsforward_http.go
@@ -22,6 +22,9 @@ func httpError(r *http.Request, w http.ResponseWriter, code int, format string,
 }
 
 type dnsConfigJSON struct {
+	Upstreams  []string `json:"upstream_dns"`
+	Bootstraps []string `json:"bootstrap_dns"`
+
 	ProtectionEnabled bool   `json:"protection_enabled"`
 	RateLimit         uint32 `json:"ratelimit"`
 	BlockingMode      string `json:"blocking_mode"`
@@ -30,11 +33,16 @@ type dnsConfigJSON struct {
 	EDNSCSEnabled     bool   `json:"edns_cs_enabled"`
 	DNSSECEnabled     bool   `json:"dnssec_enabled"`
 	DisableIPv6       bool   `json:"disable_ipv6"`
+	FastestAddr       bool   `json:"fastest_addr"`
+	ParallelRequests  bool   `json:"parallel_requests"`
 }
 
 func (s *Server) handleGetConfig(w http.ResponseWriter, r *http.Request) {
 	resp := dnsConfigJSON{}
 	s.RLock()
+	resp.Upstreams = stringArrayDup(s.conf.UpstreamDNS)
+	resp.Bootstraps = stringArrayDup(s.conf.BootstrapDNS)
+
 	resp.ProtectionEnabled = s.conf.ProtectionEnabled
 	resp.BlockingMode = s.conf.BlockingMode
 	resp.BlockingIPv4 = s.conf.BlockingIPv4
@@ -43,6 +51,8 @@ func (s *Server) handleGetConfig(w http.ResponseWriter, r *http.Request) {
 	resp.EDNSCSEnabled = s.conf.EnableEDNSClientSubnet
 	resp.DNSSECEnabled = s.conf.EnableDNSSEC
 	resp.DisableIPv6 = s.conf.AAAADisabled
+	resp.FastestAddr = s.conf.FastestAddrAlgo
+	resp.ParallelRequests = s.conf.AllServers
 	s.RUnlock()
 
 	js, err := json.Marshal(resp)
@@ -75,6 +85,7 @@ func checkBlockingMode(req dnsConfigJSON) bool {
 	return true
 }
 
+// nolint(gocyclo) - we need to check each JSON field separately
 func (s *Server) handleSetConfig(w http.ResponseWriter, r *http.Request) {
 	req := dnsConfigJSON{}
 	js, err := jsonutil.DecodeObject(&req, r.Body)
@@ -83,6 +94,25 @@ func (s *Server) handleSetConfig(w http.ResponseWriter, r *http.Request) {
 		return
 	}
 
+	if js.Exists("upstream_dns") {
+		if len(req.Upstreams) != 0 {
+			err = ValidateUpstreams(req.Upstreams)
+			if err != nil {
+				httpError(r, w, http.StatusBadRequest, "wrong upstreams specification: %s", err)
+				return
+			}
+		}
+	}
+
+	if js.Exists("bootstrap_dns") {
+		for _, host := range req.Bootstraps {
+			if err := checkPlainDNS(host); err != nil {
+				httpError(r, w, http.StatusBadRequest, "%s can not be used as bootstrap dns cause: %s", host, err)
+				return
+			}
+		}
+	}
+
 	if js.Exists("blocking_mode") && !checkBlockingMode(req) {
 		httpError(r, w, http.StatusBadRequest, "blocking_mode: incorrect value")
 		return
@@ -91,6 +121,16 @@ func (s *Server) handleSetConfig(w http.ResponseWriter, r *http.Request) {
 	restart := false
 	s.Lock()
 
+	if js.Exists("upstream_dns") {
+		s.conf.UpstreamDNS = req.Upstreams
+		restart = true
+	}
+
+	if js.Exists("bootstrap_dns") {
+		s.conf.BootstrapDNS = req.Bootstraps
+		restart = true
+	}
+
 	if js.Exists("protection_enabled") {
 		s.conf.ProtectionEnabled = req.ProtectionEnabled
 	}
@@ -129,6 +169,14 @@ func (s *Server) handleSetConfig(w http.ResponseWriter, r *http.Request) {
 		s.conf.AAAADisabled = req.DisableIPv6
 	}
 
+	if js.Exists("fastest_addr") {
+		s.conf.FastestAddrAlgo = req.FastestAddr
+	}
+
+	if js.Exists("parallel_requests") {
+		s.conf.AllServers = req.ParallelRequests
+	}
+
 	s.Unlock()
 	s.conf.ConfigModified()
 
@@ -144,51 +192,6 @@ func (s *Server) handleSetConfig(w http.ResponseWriter, r *http.Request) {
 type upstreamJSON struct {
 	Upstreams    []string `json:"upstream_dns"`  // Upstreams
 	BootstrapDNS []string `json:"bootstrap_dns"` // Bootstrap DNS
-	AllServers   bool     `json:"all_servers"`   // --all-servers param for dnsproxy
-}
-
-func (s *Server) handleSetUpstreamConfig(w http.ResponseWriter, r *http.Request) {
-	req := upstreamJSON{}
-	err := json.NewDecoder(r.Body).Decode(&req)
-	if err != nil {
-		httpError(r, w, http.StatusBadRequest, "Failed to parse new upstreams config json: %s", err)
-		return
-	}
-
-	if len(req.Upstreams) != 0 {
-		err = ValidateUpstreams(req.Upstreams)
-		if err != nil {
-			httpError(r, w, http.StatusBadRequest, "wrong upstreams specification: %s", err)
-			return
-		}
-	}
-
-	newconf := FilteringConfig{}
-	newconf.UpstreamDNS = req.Upstreams
-
-	// bootstrap servers are plain DNS only
-	for _, host := range req.BootstrapDNS {
-		if err := checkPlainDNS(host); err != nil {
-			httpError(r, w, http.StatusBadRequest, "%s can not be used as bootstrap dns cause: %s", host, err)
-			return
-		}
-	}
-	newconf.BootstrapDNS = req.BootstrapDNS
-
-	newconf.AllServers = req.AllServers
-
-	s.Lock()
-	s.conf.UpstreamDNS = newconf.UpstreamDNS
-	s.conf.BootstrapDNS = newconf.BootstrapDNS
-	s.conf.AllServers = newconf.AllServers
-	s.Unlock()
-	s.conf.ConfigModified()
-
-	err = s.Reconfigure(nil)
-	if err != nil {
-		httpError(r, w, http.StatusInternalServerError, "%s", err)
-		return
-	}
 }
 
 // ValidateUpstreams validates each upstream and returns an error if any upstream is invalid or if there are no default upstreams specified
@@ -399,7 +402,6 @@ func (s *Server) handleDOH(w http.ResponseWriter, r *http.Request) {
 func (s *Server) registerHandlers() {
 	s.conf.HTTPRegister("GET", "/control/dns_info", s.handleGetConfig)
 	s.conf.HTTPRegister("POST", "/control/dns_config", s.handleSetConfig)
-	s.conf.HTTPRegister("POST", "/control/set_upstreams_config", s.handleSetUpstreamConfig)
 	s.conf.HTTPRegister("POST", "/control/test_upstream_dns", s.handleTestUpstreamDNS)
 
 	s.conf.HTTPRegister("GET", "/control/access/list", s.handleAccessList)
diff --git a/home/control.go b/home/control.go
index 4d1dbe0c..310b9f20 100644
--- a/home/control.go
+++ b/home/control.go
@@ -55,9 +55,6 @@ func handleStatus(w http.ResponseWriter, r *http.Request) {
 		"language":      config.Language,
 
 		"protection_enabled": c.ProtectionEnabled,
-		"bootstrap_dns":      c.BootstrapDNS,
-		"upstream_dns":       c.UpstreamDNS,
-		"all_servers":        c.AllServers,
 	}
 
 	jsonVal, err := json.Marshal(data)
diff --git a/openapi/CHANGELOG.md b/openapi/CHANGELOG.md
index 877c1f7c..193b69fc 100644
--- a/openapi/CHANGELOG.md
+++ b/openapi/CHANGELOG.md
@@ -1,6 +1,70 @@
 # AdGuard Home API Change Log
 
 
+## v0.102: API changes
+
+### API: Get general status: GET /control/status
+
+* Removed "upstream_dns", "bootstrap_dns", "all_servers" parameters
+
+### API: Get DNS general settings: GET /control/dns_info
+
+* Added "parallel_requests", "upstream_dns", "bootstrap_dns" parameters
+
+Request:
+
+	GET /control/dns_info
+
+Response:
+
+	200 OK
+
+	{
+		"upstream_dns": ["tls://...", ...],
+		"bootstrap_dns": ["1.2.3.4", ...],
+
+		"protection_enabled": true | false,
+		"ratelimit": 1234,
+		"blocking_mode": "default" | "nxdomain" | "null_ip" | "custom_ip",
+		"blocking_ipv4": "1.2.3.4",
+		"blocking_ipv6": "1:2:3::4",
+		"edns_cs_enabled": true | false,
+		"dnssec_enabled": true | false
+		"disable_ipv6": true | false,
+		"fastest_addr": true | false, // use Fastest Address algorithm
+		"parallel_requests": true | false, // send DNS requests to all upstream servers at once
+	}
+
+### API: Set DNS general settings: POST /control/dns_config
+
+* Added "parallel_requests", "upstream_dns", "bootstrap_dns" parameters
+* removed /control/set_upstreams_config method
+
+Request:
+
+	POST /control/dns_config
+
+	{
+		"upstream_dns": ["tls://...", ...],
+		"bootstrap_dns": ["1.2.3.4", ...],
+
+		"protection_enabled": true | false,
+		"ratelimit": 1234,
+		"blocking_mode": "default" | "nxdomain" | "null_ip" | "custom_ip",
+		"blocking_ipv4": "1.2.3.4",
+		"blocking_ipv6": "1:2:3::4",
+		"edns_cs_enabled": true | false,
+		"dnssec_enabled": true | false
+		"disable_ipv6": true | false,
+		"fastest_addr": true | false, // use Fastest Address algorithm
+		"parallel_requests": true | false, // send DNS requests to all upstream servers at once
+	}
+
+Response:
+
+	200 OK
+
+
 ## v0.101: API changes
 
 ### API: Refresh filters: POST /control/filtering/refresh
diff --git a/openapi/openapi.yaml b/openapi/openapi.yaml
index 9653af5a..a9791c4d 100644
--- a/openapi/openapi.yaml
+++ b/openapi/openapi.yaml
@@ -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.101'
+    version: '0.102'
 schemes:
     - http
 basePath: /control
@@ -99,25 +99,6 @@ paths:
                 200:
                     description: OK
 
-    /set_upstreams_config:
-        post:
-            tags:
-                - global
-            operationId: setUpstreamsConfig
-            summary: "Updates the current upstreams configuration"
-            consumes:
-                - application/json
-            parameters:
-                - in: "body"
-                  name: "body"
-                  description: "Upstreams configuration JSON"
-                  required: true
-                  schema:
-                      $ref: "#/definitions/UpstreamsConfig"
-            responses:
-                200:
-                    description: OK
-
     /test_upstream_dns:
         post:
             tags:
@@ -1072,16 +1053,6 @@ definitions:
                 type: "boolean"
             running:
                 type: "boolean"
-            bootstrap_dns:
-                type: "string"
-                example: "8.8.8.8:53"
-            upstream_dns:
-                type: "array"
-                items:
-                    type: "string"
-                example:
-                  - "tls://1.1.1.1"
-                  - "tls://1.0.0.1"
             version:
                 type: "string"
                 example: "0.1"
@@ -1093,6 +1064,22 @@ definitions:
         type: "object"
         description: "Query log configuration"
         properties:
+            bootstrap_dns:
+                type: "array"
+                description: 'Bootstrap servers, port is optional after colon. Empty value will reset it to default values'
+                items:
+                    type: "string"
+                example:
+                    - "8.8.8.8:53"
+                    - "1.1.1.1:53"
+            upstream_dns:
+                type: "array"
+                description: 'Upstream servers, port is optional after colon. Empty value will reset it to default values'
+                items:
+                    type: "string"
+                example:
+                    - "tls://1.1.1.1"
+                    - "tls://1.0.0.1"
             protection_enabled:
                 type: "boolean"
             ratelimit:
@@ -1112,6 +1099,11 @@ definitions:
                 type: "boolean"
             dnssec_enabled:
                 type: "boolean"
+            fastest_addr:
+                type: "boolean"
+            parallel_requests:
+                type: "boolean"
+                description: "If true, parallel queries to all configured upstream servers are enabled"
 
     UpstreamsConfig:
         type: "object"
@@ -1119,7 +1111,6 @@ definitions:
         required:
             - "bootstrap_dns"
             - "upstream_dns"
-            - "all_servers"
         properties:
             bootstrap_dns:
                 type: "array"
@@ -1137,9 +1128,7 @@ definitions:
                 example:
                     - "tls://1.1.1.1"
                     - "tls://1.0.0.1"
-            all_servers:
-                type: "boolean"
-                description: "If true, parallel queries to all configured upstream servers are enabled"
+
     Filter:
         type: "object"
         description: "Filter subscription info"

From 0ffc0965dc38eb02a09c7a14a8fc8f821cfb0893 Mon Sep 17 00:00:00 2001
From: Artem Baskal <a.baskal@adguard.com>
Date: Wed, 22 Apr 2020 19:32:07 +0300
Subject: [PATCH 13/37] + client: add fastest_addr setting

Squashed commit of the following:

commit e47fae25f7bac950bfb452fc8f18b9c0865b08ba
Merge: a23285ec e2ddc82d
Author: Simon Zolin <s.zolin@adguard.com>
Date:   Wed Apr 22 19:16:01 2020 +0300

    Merge remote-tracking branch 'origin/master' into 715

commit a23285ec3ace78fe4ce19122a51ecf3e6cdd942c
Author: ArtemBaskal <a.baskal@adguard.com>
Date:   Wed Apr 22 18:30:30 2020 +0300

    Review changes

commit f80d62a0d2038ff9d070ae9e9c77c33b92232d9c
Author: ArtemBaskal <a.baskal@adguard.com>
Date:   Tue Apr 21 16:37:42 2020 +0300

    + client: Add fastest addr option

commit 9e713df80c5bf113c98794c0a20915c756a76938
Merge: e3bf4037 9b7c1181
Author: Simon Zolin <s.zolin@adguard.com>
Date:   Tue Apr 21 16:02:03 2020 +0300

    Merge remote-tracking branch 'origin/master' into 715-fastest-addr

commit e3bf4037f49198e42bde55305d6f9077341b556a
Author: Simon Zolin <s.zolin@adguard.com>
Date:   Tue Apr 21 15:40:49 2020 +0300

    minor

commit d6e6a823c5e51acc061b2850d362772efcb827e1
Author: Simon Zolin <s.zolin@adguard.com>
Date:   Fri Apr 17 17:56:24 2020 +0300

    * API changes

    . removed POST /set_upstreams_config
    . removed fields from GET /status: bootstrap_dns, upstream_dns, all_servers
    . added new fields to /dns_config and /dns_info

commit 237a452d09cc48ff8f00e81c7fd35e7828bea835
Author: Simon Zolin <s.zolin@adguard.com>
Date:   Fri Apr 17 16:43:13 2020 +0300

    * API: /dns_info, /dns_config: add "parallel_requests" instead of "all_servers" from /set_upstreams_config

commit 9976723b9725ed19e0cce152d1d1198b13c4acc1
Author: Simon Zolin <s.zolin@adguard.com>
Date:   Mon Mar 23 10:28:25 2020 +0300

    openapi

commit 6f8ea16c6332606f29095b0094d71e8a91798f82
Merge: 36e4d4e8 c8285c41
Author: Simon Zolin <s.zolin@adguard.com>
Date:   Fri Mar 20 19:18:48 2020 +0300

    Merge remote-tracking branch 'origin/master' into 715-fastest-addr

commit 36e4d4e82cadeaba5a11313f0d69d66a0924c342
Author: Simon Zolin <s.zolin@adguard.com>
Date:   Fri Mar 20 18:13:43 2020 +0300

    + DNS: add fastest_addr setting
---
 client/src/__locales/en.json                  |   7 +-
 client/src/actions/dnsConfig.js               |  23 ++-
 client/src/actions/index.js                   |  37 +----
 client/src/api/Api.js                         |  10 --
 .../components/Settings/Dns/Config/Form.js    | 153 +++++++++--------
 .../components/Settings/Dns/Upstream/Form.js  | 154 ++++++++++--------
 .../components/Settings/Dns/Upstream/index.js |  25 +--
 client/src/components/Settings/Dns/index.js   |  27 +--
 client/src/components/ui/Checkbox.css         |   1 +
 client/src/containers/Dns.js                  |   5 +-
 client/src/helpers/form.js                    |  39 ++---
 client/src/reducers/dnsConfig.js              |   4 +
 client/src/reducers/index.js                  |  44 +----
 13 files changed, 233 insertions(+), 296 deletions(-)

diff --git a/client/src/__locales/en.json b/client/src/__locales/en.json
index a30e5d0b..71d9daaf 100644
--- a/client/src/__locales/en.json
+++ b/client/src/__locales/en.json
@@ -1,7 +1,8 @@
 {
     "client_settings": "Client settings",
-    "example_upstream_reserved": "you can specify DNS upstream <0>for a specific domain(s)</0>",
-    "upstream_parallel": "Use parallel queries to speed up resolving by simultaneously querying all upstream servers",
+    "example_upstream_reserved": "You can specify DNS upstream <0>for the specific domain(s)</0>",
+    "upstream_parallel": "Use parallel requests to speed up resolving by simultaneously querying all upstream servers",
+    "parallel_requests": "Parallel requests",
     "bootstrap_dns": "Bootstrap DNS servers",
     "bootstrap_dns_desc": "Bootstrap DNS servers are used to resolve IP addresses of the DoH/DoT resolvers you specify as upstreams.",
     "check_dhcp_servers": "Check for DHCP servers",
@@ -453,6 +454,8 @@
     "example_rewrite_wildcard": "rewrite responses for all <0>example.org</0> subdomains.",
     "disable_ipv6": "Disable IPv6",
     "disable_ipv6_desc": "If this feature is enabled, all DNS queries for IPv6 addresses (type AAAA) will be dropped.",
+    "fastest_addr": "Fastest IP address",
+    "fastest_addr_desc": "Query all DNS servers and return the fastest IP address among all responses",
     "autofix_warning_text": "If you click \"Fix\", AdGuard Home will configure your system to use AdGuard Home DNS server.",
     "autofix_warning_list": "It will perform these tasks: <0>Deactivate system DNSStubListener</0> <0>Set DNS server address to 127.0.0.1</0> <0>Replace symbolic link target of /etc/resolv.conf with /run/systemd/resolve/resolv.conf</0> <0>Stop DNSStubListener (reload systemd-resolved service)</0>",
     "autofix_warning_result": "As a result all DNS requests from your system will be processed by AdGuard Home by default.",
diff --git a/client/src/actions/dnsConfig.js b/client/src/actions/dnsConfig.js
index 1976613e..c7a4dc2f 100644
--- a/client/src/actions/dnsConfig.js
+++ b/client/src/actions/dnsConfig.js
@@ -2,6 +2,7 @@ import { createAction } from 'redux-actions';
 
 import apiClient from '../api/Api';
 import { addErrorToast, addSuccessToast } from './index';
+import { normalizeTextarea } from '../helpers/helpers';
 
 export const getDnsConfigRequest = createAction('GET_DNS_CONFIG_REQUEST');
 export const getDnsConfigFailure = createAction('GET_DNS_CONFIG_FAILURE');
@@ -25,8 +26,26 @@ export const setDnsConfigSuccess = createAction('SET_DNS_CONFIG_SUCCESS');
 export const setDnsConfig = config => async (dispatch) => {
     dispatch(setDnsConfigRequest());
     try {
-        await apiClient.setDnsConfig(config);
-        dispatch(addSuccessToast('config_successfully_saved'));
+        const data = { ...config };
+
+        let hasDnsSettings = false;
+        if (Object.prototype.hasOwnProperty.call(data, 'bootstrap_dns')) {
+            data.bootstrap_dns = normalizeTextarea(config.bootstrap_dns);
+            hasDnsSettings = true;
+        }
+        if (Object.prototype.hasOwnProperty.call(data, 'upstream_dns')) {
+            data.upstream_dns = normalizeTextarea(config.upstream_dns);
+            hasDnsSettings = true;
+        }
+
+        await apiClient.setDnsConfig(data);
+
+        if (hasDnsSettings) {
+            dispatch(addSuccessToast('updated_upstream_dns_toast'));
+        } else {
+            dispatch(addSuccessToast('config_successfully_saved'));
+        }
+
         dispatch(setDnsConfigSuccess(config));
     } catch (error) {
         dispatch(addErrorToast({ error }));
diff --git a/client/src/actions/index.js b/client/src/actions/index.js
index 0d212b1b..0cff2116 100644
--- a/client/src/actions/index.js
+++ b/client/src/actions/index.js
@@ -244,6 +244,7 @@ export const getDnsStatus = () => async (dispatch) => {
         dispatch(dnsStatusFailure());
         window.location.reload(true);
     };
+
     const handleRequestSuccess = (response) => {
         const dnsStatus = response.data;
         const { running } = dnsStatus;
@@ -265,42 +266,6 @@ export const getDnsStatus = () => async (dispatch) => {
     }
 };
 
-export const getDnsSettingsRequest = createAction('GET_DNS_SETTINGS_REQUEST');
-export const getDnsSettingsFailure = createAction('GET_DNS_SETTINGS_FAILURE');
-export const getDnsSettingsSuccess = createAction('GET_DNS_SETTINGS_SUCCESS');
-
-export const getDnsSettings = () => async (dispatch) => {
-    dispatch(getDnsSettingsRequest());
-    try {
-        const dnsStatus = await apiClient.getGlobalStatus();
-        dispatch(getDnsSettingsSuccess(dnsStatus));
-    } catch (error) {
-        dispatch(addErrorToast({ error }));
-        dispatch(getDnsSettingsFailure());
-    }
-};
-
-export const handleUpstreamChange = createAction('HANDLE_UPSTREAM_CHANGE');
-export const setUpstreamRequest = createAction('SET_UPSTREAM_REQUEST');
-export const setUpstreamFailure = createAction('SET_UPSTREAM_FAILURE');
-export const setUpstreamSuccess = createAction('SET_UPSTREAM_SUCCESS');
-
-export const setUpstream = config => async (dispatch) => {
-    dispatch(setUpstreamRequest());
-    try {
-        const values = { ...config };
-        values.bootstrap_dns = normalizeTextarea(values.bootstrap_dns);
-        values.upstream_dns = normalizeTextarea(values.upstream_dns);
-
-        await apiClient.setUpstream(values);
-        dispatch(addSuccessToast('updated_upstream_dns_toast'));
-        dispatch(setUpstreamSuccess(config));
-    } catch (error) {
-        dispatch(addErrorToast({ error }));
-        dispatch(setUpstreamFailure());
-    }
-};
-
 export const testUpstreamRequest = createAction('TEST_UPSTREAM_REQUEST');
 export const testUpstreamFailure = createAction('TEST_UPSTREAM_FAILURE');
 export const testUpstreamSuccess = createAction('TEST_UPSTREAM_SUCCESS');
diff --git a/client/src/api/Api.js b/client/src/api/Api.js
index 94a05b19..f678d7c3 100644
--- a/client/src/api/Api.js
+++ b/client/src/api/Api.js
@@ -32,7 +32,6 @@ class Api {
 
     // Global methods
     GLOBAL_STATUS = { path: 'status', method: 'GET' };
-    GLOBAL_SET_UPSTREAM_DNS = { path: 'set_upstreams_config', method: 'POST' };
     GLOBAL_TEST_UPSTREAM_DNS = { path: 'test_upstream_dns', method: 'POST' };
     GLOBAL_VERSION = { path: 'version.json', method: 'POST' };
     GLOBAL_UPDATE = { path: 'update', method: 'POST' };
@@ -42,15 +41,6 @@ class Api {
         return this.makeRequest(path, method);
     }
 
-    setUpstream(url) {
-        const { path, method } = this.GLOBAL_SET_UPSTREAM_DNS;
-        const config = {
-            data: url,
-            headers: { 'Content-Type': 'application/json' },
-        };
-        return this.makeRequest(path, method, config);
-    }
-
     testUpstream(servers) {
         const { path, method } = this.GLOBAL_TEST_UPSTREAM_DNS;
         const config = {
diff --git a/client/src/components/Settings/Dns/Config/Form.js b/client/src/components/Settings/Dns/Config/Form.js
index 1db400e8..46fae81e 100644
--- a/client/src/components/Settings/Dns/Config/Form.js
+++ b/client/src/components/Settings/Dns/Config/Form.js
@@ -17,26 +17,55 @@ import {
 } from '../../../../helpers/form';
 import { BLOCKING_MODES } from '../../../../helpers/constants';
 
-const getFields = (processing, t) => Object.values(BLOCKING_MODES).map(mode => (
-    <Field
-        key={mode}
-        name="blocking_mode"
-        type="radio"
-        component={renderRadioField}
-        value={mode}
-        placeholder={t(mode)}
-        disabled={processing}
-    />
-));
+const checkboxes = [{
+    name: 'edns_cs_enabled',
+    placeholder: 'edns_enable',
+    subtitle: 'edns_cs_desc',
+},
+{
+    name: 'dnssec_enabled',
+    placeholder: 'dnssec_enable',
+    subtitle: 'dnssec_enable_desc',
+},
+{
+    name: 'disable_ipv6',
+    placeholder: 'disable_ipv6',
+    subtitle: 'disable_ipv6_desc',
+}];
+
+const customIps = [{
+    description: 'blocking_ipv4_desc',
+    name: 'blocking_ipv4',
+    validateIp: ipv4,
+},
+{
+    description: 'blocking_ipv6_desc',
+    name: 'blocking_ipv6',
+    validateIp: ipv6,
+}];
+
+const getFields = (processing, t) => Object.values(BLOCKING_MODES)
+    .map(mode => (
+        <Field
+            key={mode}
+            name="blocking_mode"
+            type="radio"
+            component={renderRadioField}
+            value={mode}
+            placeholder={t(mode)}
+            disabled={processing}
+        />
+    ));
 
 let Form = ({
     handleSubmit, submitting, invalid, processing, blockingMode, t,
-}) => (
+}) =>
     <form onSubmit={handleSubmit}>
         <div className="row">
             <div className="col-12 col-sm-6">
                 <div className="form__group form__group--settings">
-                    <label htmlFor="ratelimit" className="form__label form__label--with-desc">
+                    <label htmlFor="ratelimit"
+                           className="form__label form__label--with-desc">
                         <Trans>rate_limit</Trans>
                     </label>
                     <div className="form__desc form__desc--top">
@@ -53,53 +82,31 @@ let Form = ({
                     />
                 </div>
             </div>
-            <div className="col-12">
-                <div className="form__group form__group--settings">
-                    <Field
-                        name="edns_cs_enabled"
-                        type="checkbox"
-                        component={renderSelectField}
-                        placeholder={t('edns_enable')}
-                        disabled={processing}
-                        subtitle={t('edns_cs_desc')}
-                    />
-                </div>
-            </div>
-            <div className="col-12">
-                <div className="form__group form__group--settings">
-                    <Field
-                        name="dnssec_enabled"
-                        type="checkbox"
-                        component={renderSelectField}
-                        placeholder={t('dnssec_enable')}
-                        disabled={processing}
-                        subtitle={t('dnssec_enable_desc')}
-                    />
-                </div>
-            </div>
-            <div className="col-12">
-                <div className="form__group form__group--settings">
-                    <Field
-                        name="disable_ipv6"
-                        type="checkbox"
-                        component={renderSelectField}
-                        placeholder={t('disable_ipv6')}
-                        disabled={processing}
-                        subtitle={t('disable_ipv6_desc')}
-                    />
-                </div>
-            </div>
+            {checkboxes.map(({ name, placeholder, subtitle }) =>
+                <div className="col-12" key={name}>
+                    <div className="form__group form__group--settings">
+                        <Field
+                            name={name}
+                            type="checkbox"
+                            component={renderSelectField}
+                            placeholder={t(placeholder)}
+                            disabled={processing}
+                            subtitle={t(subtitle)}
+                        />
+                    </div>
+                </div>)}
             <div className="col-12">
                 <div className="form__group form__group--settings mb-4">
                     <label className="form__label form__label--with-desc">
                         <Trans>blocking_mode</Trans>
                     </label>
                     <div className="form__desc form__desc--top">
-                        {Object.values(BLOCKING_MODES).map(mode => (
-                            <li key={mode}>
-                                <Trans >{`blocking_mode_${mode}`}</Trans>
-                            </li>
-                        ))}
+                        {Object.values(BLOCKING_MODES)
+                            .map(mode => (
+                                <li key={mode}>
+                                    <Trans>{`blocking_mode_${mode}`}</Trans>
+                                </li>
+                            ))}
                     </div>
                     <div className="custom-controls-stacked">
                         {getFields(processing, t)}
@@ -108,40 +115,27 @@ let Form = ({
             </div>
             {blockingMode === BLOCKING_MODES.custom_ip && (
                 <Fragment>
-                    <div className="col-12 col-sm-6">
+                    {customIps.map(({
+                        description,
+                        name,
+                        validateIp,
+                    }) => <div className="col-12 col-sm-6" key={name}>
                         <div className="form__group form__group--settings">
-                            <label htmlFor="blocking_ipv4" className="form__label form__label--with-desc">
-                                <Trans>blocking_ipv4</Trans>
+                            <label className="form__label form__label--with-desc"
+                                   htmlFor={name}><Trans>{name}</Trans>
                             </label>
                             <div className="form__desc form__desc--top">
-                                <Trans>blocking_ipv4_desc</Trans>
+                                <Trans>{description}</Trans>
                             </div>
                             <Field
-                                name="blocking_ipv4"
+                                name={name}
                                 component={renderInputField}
                                 className="form-control"
                                 placeholder={t('form_enter_ip')}
-                                validate={[ipv4, required]}
+                                validate={[validateIp, required]}
                             />
                         </div>
-                    </div>
-                    <div className="col-12 col-sm-6">
-                        <div className="form__group form__group--settings">
-                            <label htmlFor="ip_address" className="form__label form__label--with-desc">
-                                <Trans>blocking_ipv6</Trans>
-                            </label>
-                            <div className="form__desc form__desc--top">
-                                <Trans>blocking_ipv6_desc</Trans>
-                            </div>
-                            <Field
-                                name="blocking_ipv6"
-                                component={renderInputField}
-                                className="form-control"
-                                placeholder={t('form_enter_ip')}
-                                validate={[ipv6, required]}
-                            />
-                        </div>
-                    </div>
+                    </div>)}
                 </Fragment>
             )}
         </div>
@@ -152,8 +146,7 @@ let Form = ({
         >
             <Trans>save_btn</Trans>
         </button>
-    </form>
-);
+    </form>;
 
 Form.propTypes = {
     blockingMode: PropTypes.string.isRequired,
diff --git a/client/src/components/Settings/Dns/Upstream/Form.js b/client/src/components/Settings/Dns/Upstream/Form.js
index e6164818..961ac252 100644
--- a/client/src/components/Settings/Dns/Upstream/Form.js
+++ b/client/src/components/Settings/Dns/Upstream/Form.js
@@ -6,21 +6,50 @@ import { Trans, withNamespaces } from 'react-i18next';
 import flow from 'lodash/flow';
 import classnames from 'classnames';
 
-import { renderSelectField } from '../../../../helpers/form';
 import Examples from './Examples';
+import { renderSelectField } from '../../../../helpers/form';
+
+const getInputFields = (parallel_requests_selected, fastest_addr_selected) => [{
+    // eslint-disable-next-line react/display-name
+    getTitle: () => <label className="form__label" htmlFor="upstream_dns">
+        <Trans>upstream_dns</Trans>
+    </label>,
+    name: 'upstream_dns',
+    type: 'text',
+    component: 'textarea',
+    className: 'form-control form-control--textarea',
+    placeholder: 'upstream_dns',
+},
+{
+    name: 'parallel_requests',
+    placeholder: 'parallel_requests',
+    component: renderSelectField,
+    type: 'checkbox',
+    subtitle: 'upstream_parallel',
+    disabled: fastest_addr_selected,
+},
+{
+    name: 'fastest_addr',
+    placeholder: 'fastest_addr',
+    component: renderSelectField,
+    type: 'checkbox',
+    subtitle: 'fastest_addr_desc',
+    disabled: parallel_requests_selected,
+}];
 
 let Form = (props) => {
     const {
         t,
         handleSubmit,
         testUpstream,
-        upstreamDns,
-        bootstrapDns,
-        allServers,
         submitting,
         invalid,
-        processingSetUpstream,
+        processingSetConfig,
         processingTestUpstream,
+        fastest_addr,
+        parallel_requests,
+        upstream_dns,
+        bootstrap_dns,
     } = props;
 
     const testButtonClass = classnames({
@@ -28,61 +57,49 @@ let Form = (props) => {
         'btn btn-primary btn-standard mr-2 btn-loading': processingTestUpstream,
     });
 
+    const INPUT_FIELDS = getInputFields(parallel_requests, fastest_addr);
+
     return (
         <form onSubmit={handleSubmit}>
             <div className="row">
-                <div className="col-12">
-                    <div className="form__group form__group--settings">
-                        <label className="form__label" htmlFor="upstream_dns">
-                            <Trans>upstream_dns</Trans>
-                        </label>
-                        <Field
-                            id="upstream_dns"
-                            name="upstream_dns"
-                            component="textarea"
-                            type="text"
-                            className="form-control form-control--textarea"
-                            placeholder={t('upstream_dns')}
-                            disabled={processingSetUpstream || processingTestUpstream}
-                        />
-                    </div>
-                </div>
-                <div className="col-12">
-                    <div className="form__group form__group--settings">
-                        <Field
-                            name="all_servers"
-                            type="checkbox"
-                            component={renderSelectField}
-                            placeholder={t('upstream_parallel')}
-                            disabled={processingSetUpstream}
-                        />
-                    </div>
-                </div>
+                {INPUT_FIELDS.map(({
+                    name, component, type, className, placeholder, getTitle, subtitle, disabled,
+                }) => <div className="col-12 mb-4" key={name}>
+                    {typeof getTitle === 'function' && getTitle()}
+                    <Field
+                        id={name}
+                        name={name}
+                        component={component}
+                        type={type}
+                        className={className}
+                        placeholder={t(placeholder)}
+                        subtitle={t(subtitle)}
+                        disabled={processingSetConfig || processingTestUpstream || disabled}
+                    />
+                </div>)}
                 <div className="col-12">
                     <Examples />
                     <hr />
                 </div>
-                <div className="col-12">
-                    <div className="form__group">
-                        <label
-                            className="form__label form__label--with-desc"
-                            htmlFor="bootstrap_dns"
-                        >
-                            <Trans>bootstrap_dns</Trans>
-                        </label>
-                        <div className="form__desc form__desc--top">
-                            <Trans>bootstrap_dns_desc</Trans>
-                        </div>
-                        <Field
-                            id="bootstrap_dns"
-                            name="bootstrap_dns"
-                            component="textarea"
-                            type="text"
-                            className="form-control form-control--textarea form-control--textarea-small"
-                            placeholder={t('bootstrap_dns')}
-                            disabled={processingSetUpstream}
-                        />
+                <div className="col-12 mb-4">
+                    <label
+                        className="form__label form__label--with-desc"
+                        htmlFor="bootstrap_dns"
+                    >
+                        <Trans>bootstrap_dns</Trans>
+                    </label>
+                    <div className="form__desc form__desc--top">
+                        <Trans>bootstrap_dns_desc</Trans>
                     </div>
+                    <Field
+                        id="bootstrap_dns"
+                        name="bootstrap_dns"
+                        component="textarea"
+                        type="text"
+                        className="form-control form-control--textarea form-control--textarea-small"
+                        placeholder={t('bootstrap_dns')}
+                        disabled={processingSetConfig}
+                    />
                 </div>
             </div>
             <div className="card-actions">
@@ -92,12 +109,11 @@ let Form = (props) => {
                         className={testButtonClass}
                         onClick={() =>
                             testUpstream({
-                                upstream_dns: upstreamDns,
-                                bootstrap_dns: bootstrapDns,
-                                all_servers: allServers,
+                                upstream_dns,
+                                bootstrap_dns,
                             })
                         }
-                        disabled={!upstreamDns || processingTestUpstream}
+                        disabled={!upstream_dns || processingTestUpstream}
                     >
                         <Trans>test_upstream_btn</Trans>
                     </button>
@@ -105,7 +121,7 @@ let Form = (props) => {
                         type="submit"
                         className="btn btn-success btn-standard"
                         disabled={
-                            submitting || invalid || processingSetUpstream || processingTestUpstream
+                            submitting || invalid || processingSetConfig || processingTestUpstream
                         }
                     >
                         <Trans>apply_btn</Trans>
@@ -122,24 +138,28 @@ Form.propTypes = {
     submitting: PropTypes.bool,
     invalid: PropTypes.bool,
     initialValues: PropTypes.object,
-    upstreamDns: PropTypes.string,
-    bootstrapDns: PropTypes.string,
-    allServers: PropTypes.bool,
+    upstream_dns: PropTypes.string,
+    bootstrap_dns: PropTypes.string,
+    fastest_addr: PropTypes.bool,
+    parallel_requests: PropTypes.bool,
     processingTestUpstream: PropTypes.bool,
-    processingSetUpstream: PropTypes.bool,
+    processingSetConfig: PropTypes.bool,
     t: PropTypes.func,
 };
 
 const selector = formValueSelector('upstreamForm');
 
 Form = connect((state) => {
-    const upstreamDns = selector(state, 'upstream_dns');
-    const bootstrapDns = selector(state, 'bootstrap_dns');
-    const allServers = selector(state, 'all_servers');
+    const upstream_dns = selector(state, 'upstream_dns');
+    const bootstrap_dns = selector(state, 'bootstrap_dns');
+    const fastest_addr = selector(state, 'fastest_addr');
+    const parallel_requests = selector(state, 'parallel_requests');
+
     return {
-        upstreamDns,
-        bootstrapDns,
-        allServers,
+        upstream_dns,
+        bootstrap_dns,
+        fastest_addr,
+        parallel_requests,
     };
 })(Form);
 
diff --git a/client/src/components/Settings/Dns/Upstream/index.js b/client/src/components/Settings/Dns/Upstream/index.js
index 292c2cfd..62a93795 100644
--- a/client/src/components/Settings/Dns/Upstream/index.js
+++ b/client/src/components/Settings/Dns/Upstream/index.js
@@ -7,7 +7,7 @@ import Card from '../../../ui/Card';
 
 class Upstream extends Component {
     handleSubmit = (values) => {
-        this.props.setUpstream(values);
+        this.props.setDnsConfig(values);
     };
 
     handleTest = (values) => {
@@ -17,11 +17,14 @@ class Upstream extends Component {
     render() {
         const {
             t,
-            upstreamDns: upstream_dns,
-            bootstrapDns: bootstrap_dns,
-            allServers: all_servers,
-            processingSetUpstream,
             processingTestUpstream,
+            dnsConfig: {
+                upstream_dns,
+                bootstrap_dns,
+                fastest_addr,
+                parallel_requests,
+                processingSetConfig,
+            },
         } = this.props;
 
         return (
@@ -36,12 +39,13 @@ class Upstream extends Component {
                             initialValues={{
                                 upstream_dns,
                                 bootstrap_dns,
-                                all_servers,
+                                fastest_addr,
+                                parallel_requests,
                             }}
                             testUpstream={this.handleTest}
                             onSubmit={this.handleSubmit}
                             processingTestUpstream={processingTestUpstream}
-                            processingSetUpstream={processingSetUpstream}
+                            processingSetConfig={processingSetConfig}
                         />
                     </div>
                 </div>
@@ -51,14 +55,11 @@ class Upstream extends Component {
 }
 
 Upstream.propTypes = {
-    upstreamDns: PropTypes.string,
-    bootstrapDns: PropTypes.string,
-    allServers: PropTypes.bool,
-    setUpstream: PropTypes.func.isRequired,
     testUpstream: PropTypes.func.isRequired,
-    processingSetUpstream: PropTypes.bool.isRequired,
     processingTestUpstream: PropTypes.bool.isRequired,
     t: PropTypes.func.isRequired,
+    dnsConfig: PropTypes.object.isRequired,
+    setDnsConfig: PropTypes.func.isRequired,
 };
 
 export default withNamespaces()(Upstream);
diff --git a/client/src/components/Settings/Dns/index.js b/client/src/components/Settings/Dns/index.js
index aae1fdab..85a4bcf5 100644
--- a/client/src/components/Settings/Dns/index.js
+++ b/client/src/components/Settings/Dns/index.js
@@ -10,7 +10,6 @@ import Loading from '../../ui/Loading';
 
 class Dns extends Component {
     componentDidMount() {
-        this.props.getDnsSettings();
         this.props.getAccessList();
         this.props.getDnsConfig();
     }
@@ -18,59 +17,45 @@ class Dns extends Component {
     render() {
         const {
             t,
-            dashboard,
             settings,
             access,
             setAccessList,
             testUpstream,
-            setUpstream,
             dnsConfig,
             setDnsConfig,
         } = this.props;
 
-        const isDataLoading = dashboard.processingDnsSettings
-            || access.processing
-            || dnsConfig.processingGetConfig;
-        const isDataReady = !dashboard.processingDnsSettings
-            && !access.processing
-            && !dnsConfig.processingGetConfig;
+        const isDataLoading = access.processing || dnsConfig.processingGetConfig;
 
         return (
             <Fragment>
                 <PageTitle title={t('dns_settings')} />
-                {isDataLoading && <Loading />}
-                {isDataReady && (
+                {isDataLoading ?
+                    <Loading /> :
                     <Fragment>
                         <Config
                             dnsConfig={dnsConfig}
                             setDnsConfig={setDnsConfig}
                         />
                         <Upstream
-                            upstreamDns={dashboard.upstreamDns}
-                            bootstrapDns={dashboard.bootstrapDns}
-                            allServers={dashboard.allServers}
                             processingTestUpstream={settings.processingTestUpstream}
-                            processingSetUpstream={settings.processingSetUpstream}
-                            setUpstream={setUpstream}
                             testUpstream={testUpstream}
+                            dnsConfig={dnsConfig}
+                            setDnsConfig={setDnsConfig}
                         />
                         <Access access={access} setAccessList={setAccessList} />
-                    </Fragment>
-                )}
+                    </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,
-    getDnsSettings: PropTypes.func.isRequired,
     dnsConfig: PropTypes.object.isRequired,
     setDnsConfig: PropTypes.func.isRequired,
     getDnsConfig: PropTypes.func.isRequired,
diff --git a/client/src/components/ui/Checkbox.css b/client/src/components/ui/Checkbox.css
index ff657898..bab88c79 100644
--- a/client/src/components/ui/Checkbox.css
+++ b/client/src/components/ui/Checkbox.css
@@ -83,6 +83,7 @@
 
 .checkbox__input:disabled + .checkbox__label {
     cursor: default;
+    color: var(--gray);
 }
 
 .checkbox__input:disabled + .checkbox__label:before {
diff --git a/client/src/containers/Dns.js b/client/src/containers/Dns.js
index f32e1510..961c2f3e 100644
--- a/client/src/containers/Dns.js
+++ b/client/src/containers/Dns.js
@@ -1,5 +1,5 @@
 import { connect } from 'react-redux';
-import { handleUpstreamChange, setUpstream, testUpstream, getDnsSettings } from '../actions';
+import { testUpstream } from '../actions';
 import { getAccessList, setAccessList } from '../actions/access';
 import {
     getRewritesList,
@@ -25,8 +25,6 @@ const mapStateToProps = (state) => {
 };
 
 const mapDispatchToProps = {
-    handleUpstreamChange,
-    setUpstream,
     testUpstream,
     getAccessList,
     setAccessList,
@@ -34,7 +32,6 @@ const mapDispatchToProps = {
     addRewrite,
     deleteRewrite,
     toggleRewritesModal,
-    getDnsSettings,
     getDnsConfig,
     setDnsConfig,
 };
diff --git a/client/src/helpers/form.js b/client/src/helpers/form.js
index d075feee..97c0daa2 100644
--- a/client/src/helpers/form.js
+++ b/client/src/helpers/form.js
@@ -117,29 +117,30 @@ export const renderSelectField = ({
     placeholder,
     subtitle,
     disabled,
+    onClick,
     modifier = 'checkbox--form',
     meta: { touched, error },
 }) => (
-    <Fragment>
-        <label className={`checkbox ${modifier}`}>
-            <span className="checkbox__marker" />
-            <input {...input} type="checkbox" className="checkbox__input" disabled={disabled} />
-            <span className="checkbox__label">
-                <span className="checkbox__label-text checkbox__label-text--long">
-                    <span className="checkbox__label-title">{placeholder}</span>
-                    {subtitle && (
-                        <span
-                            className="checkbox__label-subtitle"
-                            dangerouslySetInnerHTML={{ __html: subtitle }}
-                        />
-                    )}
+        <Fragment>
+            <label className={`checkbox ${modifier}`} onClick={onClick}>
+                <span className="checkbox__marker" />
+                <input {...input} type="checkbox" className="checkbox__input" disabled={disabled} />
+                <span className="checkbox__label">
+                    <span className="checkbox__label-text checkbox__label-text--long">
+                        <span className="checkbox__label-title">{placeholder}</span>
+                        {subtitle && (
+                            <span
+                                className="checkbox__label-subtitle"
+                                dangerouslySetInnerHTML={{ __html: subtitle }}
+                            />
+                        )}
+                    </span>
                 </span>
-            </span>
-        </label>
-        {!disabled &&
-        touched &&
-        (error && <span className="form__message form__message--error">{error}</span>)}
-    </Fragment>
+            </label>
+            {!disabled &&
+            touched &&
+            (error && <span className="form__message form__message--error">{error}</span>)}
+        </Fragment>
 );
 
 export const renderServiceField = ({
diff --git a/client/src/reducers/dnsConfig.js b/client/src/reducers/dnsConfig.js
index ad45e631..9f55bee7 100644
--- a/client/src/reducers/dnsConfig.js
+++ b/client/src/reducers/dnsConfig.js
@@ -15,6 +15,8 @@ const dnsConfig = handleActions(
             const {
                 blocking_ipv4,
                 blocking_ipv6,
+                upstream_dns,
+                bootstrap_dns,
                 ...values
             } = payload;
 
@@ -23,6 +25,8 @@ const dnsConfig = handleActions(
                 ...values,
                 blocking_ipv4: blocking_ipv4 || DEFAULT_BLOCKING_IPV4,
                 blocking_ipv6: blocking_ipv6 || DEFAULT_BLOCKING_IPV6,
+                upstream_dns: (upstream_dns && upstream_dns.join('\n')) || '',
+                bootstrap_dns: (bootstrap_dns && bootstrap_dns.join('\n')) || '',
                 processingGetConfig: false,
             };
         },
diff --git a/client/src/reducers/index.js b/client/src/reducers/index.js
index 046fce67..245cf17d 100644
--- a/client/src/reducers/index.js
+++ b/client/src/reducers/index.js
@@ -35,14 +35,6 @@ const settings = handleActions(
             const newSettingsList = { ...settingsList, [settingKey]: newSetting };
             return { ...state, settingsList: newSettingsList };
         },
-        [actions.setUpstreamRequest]: state => ({ ...state, processingSetUpstream: true }),
-        [actions.setUpstreamFailure]: state => ({ ...state, processingSetUpstream: false }),
-        [actions.setUpstreamSuccess]: (state, { payload }) => ({
-            ...state,
-            ...payload,
-            processingSetUpstream: false,
-        }),
-
         [actions.testUpstreamRequest]: state => ({ ...state, processingTestUpstream: true }),
         [actions.testUpstreamFailure]: state => ({ ...state, processingTestUpstream: false }),
         [actions.testUpstreamSuccess]: state => ({ ...state, processingTestUpstream: false }),
@@ -50,7 +42,6 @@ const settings = handleActions(
     {
         processing: true,
         processingTestUpstream: false,
-        processingSetUpstream: false,
         processingDhcpStatus: false,
         settingsList: {},
     },
@@ -67,12 +58,9 @@ const dashboard = handleActions(
                 version,
                 dns_port: dnsPort,
                 dns_addresses: dnsAddresses,
-                upstream_dns: upstreamDns,
-                bootstrap_dns: bootstrapDns,
-                all_servers: allServers,
                 protection_enabled: protectionEnabled,
-                language,
                 http_port: httpPort,
+                language,
             } = payload;
             const newState = {
                 ...state,
@@ -81,9 +69,6 @@ const dashboard = handleActions(
                 dnsVersion: version,
                 dnsPort,
                 dnsAddresses,
-                upstreamDns: (upstreamDns && upstreamDns.join('\n')) || '',
-                bootstrapDns: (bootstrapDns && bootstrapDns.join('\n')) || '',
-                allServers,
                 protectionEnabled,
                 language,
                 httpPort,
@@ -138,11 +123,6 @@ const dashboard = handleActions(
             return newState;
         },
 
-        [actions.handleUpstreamChange]: (state, { payload }) => {
-            const { upstreamDns } = payload;
-            return { ...state, upstreamDns };
-        },
-
         [actions.getLanguageSuccess]: (state, { payload }) => {
             const newState = { ...state, language: payload };
             return newState;
@@ -159,24 +139,6 @@ const dashboard = handleActions(
             return newState;
         },
 
-        [actions.getDnsSettingsRequest]: state => ({ ...state, processingDnsSettings: true }),
-        [actions.getDnsSettingsFailure]: state => ({ ...state, processingDnsSettings: false }),
-        [actions.getDnsSettingsSuccess]: (state, { payload }) => {
-            const {
-                upstream_dns: upstreamDns,
-                bootstrap_dns: bootstrapDns,
-                all_servers: allServers,
-            } = payload;
-
-            return {
-                ...state,
-                allServers,
-                upstreamDns: (upstreamDns && upstreamDns.join('\n')) || '',
-                bootstrapDns: (bootstrapDns && bootstrapDns.join('\n')) || '',
-                processingDnsSettings: false,
-            };
-        },
-
         [actions.getProfileRequest]: state => ({ ...state, processingProfile: true }),
         [actions.getProfileFailure]: state => ({ ...state, processingProfile: false }),
         [actions.getProfileSuccess]: (state, { payload }) => ({
@@ -191,11 +153,7 @@ const dashboard = handleActions(
         processingVersion: true,
         processingClients: true,
         processingUpdate: false,
-        processingDnsSettings: true,
         processingProfile: true,
-        upstreamDns: '',
-        bootstrapDns: '',
-        allServers: false,
         protectionEnabled: false,
         processingProtection: false,
         httpPort: 80,

From 4153d973ecac72ed2e87f4606d163a9db311b949 Mon Sep 17 00:00:00 2001
From: Andrey Meshkov <am@adguard.com>
Date: Wed, 22 Apr 2020 21:02:38 +0300
Subject: [PATCH 14/37] update snap script, added temp files to .gitignore

---
 .gitignore    | 7 +++++++
 build_snap.sh | 5 +++--
 2 files changed, 10 insertions(+), 2 deletions(-)

diff --git a/.gitignore b/.gitignore
index 78d61cdf..7b15eb29 100644
--- a/.gitignore
+++ b/.gitignore
@@ -17,3 +17,10 @@ coverage.txt
 # Test output
 dnsfilter/tests/top-1m.csv
 dnsfilter/tests/dnsfilter.TestLotsOfRules*.pprof
+
+# Snapcraft build temporary files
+*.snap
+launchpad_credentials
+snapcraft_login
+snapcraft.yaml
+snapcraft.yaml.bak
\ No newline at end of file
diff --git a/build_snap.sh b/build_snap.sh
index 8879fd22..525b210b 100755
--- a/build_snap.sh
+++ b/build_snap.sh
@@ -260,9 +260,10 @@ case "$1" in
 "build-docker") build_docker ;;
 "build") build ;;
 "publish-docker-beta") publish_docker beta ;;
-"publish-docker-release") publish_docker release ;;
+"publish-docker-release") publish_docker stable ;;
 "publish-beta") publish beta ;;
-"publish-release") publish release ;;
+"publish-release") publish stable ;;
+"prepare") prepare ;;
 "cleanup") cleanup ;;
 *) usage ;;
 esac

From e2ee2d48df1f165a3d6b8e6e5fe6c5ce5d20c0b0 Mon Sep 17 00:00:00 2001
From: Andrey Meshkov <am@adguard.com>
Date: Wed, 22 Apr 2020 21:57:25 +0300
Subject: [PATCH 15/37] Allow to build a specific snap architecture

---
 build_snap.sh | 30 ++++++++++++++++++++----------
 1 file changed, 20 insertions(+), 10 deletions(-)

diff --git a/build_snap.sh b/build_snap.sh
index 525b210b..07e61b53 100755
--- a/build_snap.sh
+++ b/build_snap.sh
@@ -195,17 +195,27 @@ publish_snap_docker() {
 #######################################
 
 build() {
-    ARCH=i386 build_snap
-    ARCH=arm64 build_snap
-    ARCH=armhf build_snap
-    ARCH=amd64 build_snap
+    if [[ -n "$1" ]]; then
+        echo "ARCH is set to $1"
+        ARCH=$1 build_snap
+    else
+        ARCH=i386 build_snap
+        ARCH=arm64 build_snap
+        ARCH=armhf build_snap
+        ARCH=amd64 build_snap
+    fi
 }
 
 build_docker() {
-    ARCH=i386 build_snap_docker
-    ARCH=arm64 build_snap_docker
-    ARCH=armhf build_snap_docker
-    ARCH=amd64 build_snap_docker
+    if [[ -n "$1" ]]; then
+        echo "ARCH is set to $1"
+        ARCH=$1 build_snap_docker
+    else
+        ARCH=i386 build_snap_docker
+        ARCH=arm64 build_snap_docker
+        ARCH=armhf build_snap_docker
+        ARCH=amd64 build_snap_docker
+    fi
 }
 
 publish_docker() {
@@ -257,8 +267,8 @@ if [[ -z $1 || $1 == "--help" || $1 == "-h" ]]; then
 fi
 
 case "$1" in
-"build-docker") build_docker ;;
-"build") build ;;
+"build-docker") build_docker $2 ;;
+"build") build $2 ;;
 "publish-docker-beta") publish_docker beta ;;
 "publish-docker-release") publish_docker stable ;;
 "publish-beta") publish beta ;;

From 1041aa8aff48a8815eae7f9c437c052a452a2331 Mon Sep 17 00:00:00 2001
From: Andrey Meshkov <am@adguard.com>
Date: Thu, 23 Apr 2020 00:27:03 +0300
Subject: [PATCH 16/37] fix stable snap publishing

---
 build_snap.sh | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/build_snap.sh b/build_snap.sh
index 07e61b53..412cba4e 100755
--- a/build_snap.sh
+++ b/build_snap.sh
@@ -224,7 +224,7 @@ publish_docker() {
         exit 1
     fi
     CHANNEL="${1}"
-    if [ "$CHANNEL" != "release" ] && [ "$CHANNEL" != "beta" ]; then
+    if [ "$CHANNEL" != "stable" ] && [ "$CHANNEL" != "beta" ]; then
         echo "$CHANNEL is an invalid value for the update channel!"
         exit 1
     fi
@@ -241,7 +241,7 @@ publish() {
         exit 1
     fi
     CHANNEL="${1}"
-    if [ "$CHANNEL" != "release" ] && [ "$CHANNEL" != "beta" ]; then
+    if [ "$CHANNEL" != "stable" ] && [ "$CHANNEL" != "beta" ]; then
         echo "$CHANNEL is an invalid value for the update channel!"
         exit 1
     fi

From 63d525c4d43c060f8c0f678c0da394e41a3ed90f Mon Sep 17 00:00:00 2001
From: Andrey Meshkov <am@adguard.com>
Date: Thu, 23 Apr 2020 01:05:31 +0300
Subject: [PATCH 17/37] upd snap yaml

---
 packaging/snap/snapcraft.yaml | 1 +
 1 file changed, 1 insertion(+)

diff --git a/packaging/snap/snapcraft.yaml b/packaging/snap/snapcraft.yaml
index ba7a47d0..e810aab3 100644
--- a/packaging/snap/snapcraft.yaml
+++ b/packaging/snap/snapcraft.yaml
@@ -6,6 +6,7 @@ description: |
   AdGuard Home is a network-wide software for blocking ads & tracking. After
   you set it up, it'll cover ALL your home devices, and you don't need any
   client-side software for that.
+
   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 servers -- both share a lot

From 4889f2d00a2153c6b78d3b92cf050d3b0ad5c763 Mon Sep 17 00:00:00 2001
From: Archive5 <63988538+Archive5@users.noreply.github.com>
Date: Mon, 20 Apr 2020 15:52:45 -0700
Subject: [PATCH 18/37] * blocked_services.go: Update blocked services
 component

rearrange

rearrange
---
 dnsfilter/blocked_services.go | 39 ++++++++++++++++++++++++++++-------
 1 file changed, 31 insertions(+), 8 deletions(-)

diff --git a/dnsfilter/blocked_services.go b/dnsfilter/blocked_services.go
index f11eed5a..1c77d1bc 100644
--- a/dnsfilter/blocked_services.go
+++ b/dnsfilter/blocked_services.go
@@ -24,6 +24,7 @@ var serviceRulesArray = []svc{
 		"||facebook.com^",
 		"||facebook.net^",
 		"||fbcdn.net^",
+		"||accountkit.com^",
 		"||fb.me^",
 		"||fb.com^",
 		"||fbsbx.com^",
@@ -31,7 +32,7 @@ var serviceRulesArray = []svc{
 		"||facebookcorewwwi.onion^",
 		"||fbcdn.com^",
 	}},
-	{"twitter", []string{"||twitter.com^", "||t.co^", "||twimg.com^"}},
+	{"twitter", []string{"||twitter.com^", "||twttr.com^", "||t.co^", "||twimg.com^"}},
 	{"youtube", []string{
 		"||youtube.com^",
 		"||ytimg.com^",
@@ -40,17 +41,31 @@ var serviceRulesArray = []svc{
 		"||youtubei.googleapis.com^",
 		"||youtube-nocookie.com^",
 	}},
-	{"twitch", []string{"||twitch.tv^", "||ttvnw.net^"}},
-	{"netflix", []string{"||nflxext.com^", "||netflix.com^"}},
+	{"twitch", []string{"||twitch.tv^", "||ttvnw.net^", "||jtvnw.net^", "||twitchcdn.net^"}},
+	{"netflix", []string{"||nflxext.com^", "||netflix.com^", "||nflximg.net^", "||nflxvideo.net^"}},
 	{"instagram", []string{"||instagram.com^", "||cdninstagram.com^"}},
-	{"snapchat", []string{"||snapchat.com^", "||sc-cdn.net^", "||impala-media-production.s3.amazonaws.com^"}},
+	{"snapchat", []string{
+		"||snapchat.com^",
+		"||sc-cdn.net^",
+		"||snap-dev.net^",
+		"||snapkit.co",
+		"||snapads.com^",
+		"||impala-media-production.s3.amazonaws.com^",
+	}},
 	{"discord", []string{"||discord.gg^", "||discordapp.net^", "||discordapp.com^", "||discord.media^"}},
 	{"ok", []string{"||ok.ru^"}},
-	{"skype", []string{"||skype.com^"}},
-	{"vk", []string{"||vk.com^"}},
+	{"skype", []string{"||skype.com^", "||skypeassets.com^"}},
+	{"vk", []string{"||vk.com^", "||userapi.com^", "||vk-cdn.net^", "||vkuservideo.net^"}},
 	{"origin", []string{"||origin.com^", "||signin.ea.com^", "||accounts.ea.com^"}},
-	{"steam", []string{"||steam.com^", "||steampowered.com^"}},
-	{"epic_games", []string{"||epicgames.com^"}},
+	{"steam", []string{
+		"||steam.com^",
+		"||steampowered.com^",
+		"||steamcommunity.com^",
+		"||steamstatic.com^",
+		"||steamstore-a.akamaihd.net^",
+		"||steamcdn-a.akamaihd.net^",
+	}},
+	{"epic_games", []string{"||epicgames.com^", "||easyanticheat.net^", "||easy.ac^", "||eac-cdn.com^"}},
 	{"reddit", []string{"||reddit.com^", "||redditstatic.com^", "||redditmedia.com^", "||redd.it^"}},
 	{"mail_ru", []string{"||mail.ru^"}},
 	{"cloudflare", []string{
@@ -72,7 +87,13 @@ var serviceRulesArray = []svc{
 	{"amazon", []string{
 		"||amazon.com^",
 		"||media-amazon.com^",
+		"||primevideo.com^",
+		"||amazontrust.com^",
 		"||images-amazon.com^",
+		"||ssl-images-amazon.com^",
+		"||amazonpay.com^",
+		"||amazonpay.in^",
+		"||amazon-adsystem.com^",
 		"||a2z.com^",
 		"||amazon.ae^",
 		"||amazon.ca^",
@@ -88,6 +109,7 @@ var serviceRulesArray = []svc{
 		"||amazon.co.jp^",
 		"||amazon.com.mx^",
 		"||amazon.co.uk^",
+		"||createspace.com^",
 	}},
 	{"ebay", []string{
 		"||ebay.com^",
@@ -119,6 +141,7 @@ var serviceRulesArray = []svc{
 	{"tiktok", []string{
 		"||tiktok.com^",
 		"||tiktokcdn.com^",
+		"||musical.ly^",
 		"||snssdk.com^",
 		"||amemv.com^",
 		"||toutiao.com^",

From 4d73a0148e8671c839416eba4398eb13504e9cc0 Mon Sep 17 00:00:00 2001
From: Andrey Meshkov <am@adguard.com>
Date: Thu, 23 Apr 2020 16:18:58 +0300
Subject: [PATCH 19/37] change snap name to adguard-home

---
 build_snap.sh                 | 2 +-
 packaging/snap/snapcraft.yaml | 2 +-
 2 files changed, 2 insertions(+), 2 deletions(-)

diff --git a/build_snap.sh b/build_snap.sh
index 412cba4e..7150b394 100755
--- a/build_snap.sh
+++ b/build_snap.sh
@@ -6,7 +6,7 @@ set -x
 
 BUILDER_IMAGE="adguard/snapcraft:1.0"
 SNAPCRAFT_TMPL="packaging/snap/snapcraft.yaml"
-SNAP_NAME="adguardhometest"
+SNAP_NAME="adguard-home"
 LAUNCHPAD_CREDENTIALS_DIR=".local/share/snapcraft/provider/launchpad"
 
 if [[ -z ${VERSION} ]]; then
diff --git a/packaging/snap/snapcraft.yaml b/packaging/snap/snapcraft.yaml
index e810aab3..5d1c2280 100644
--- a/packaging/snap/snapcraft.yaml
+++ b/packaging/snap/snapcraft.yaml
@@ -1,4 +1,4 @@
-name: adguardhometest
+name: adguard-home
 base: core18
 version: 'dev_version'
 summary: Network-wide ads & trackers blocking DNS server

From 25361836bf5ff357e59e70235d7370431611184c Mon Sep 17 00:00:00 2001
From: Andrey Meshkov <am@adguard.com>
Date: Fri, 24 Apr 2020 01:15:53 +0300
Subject: [PATCH 20/37] *: snap: don't use SNAP_COMMON

---
 packaging/snap/snapcraft.yaml | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/packaging/snap/snapcraft.yaml b/packaging/snap/snapcraft.yaml
index 5d1c2280..1d6c5274 100644
--- a/packaging/snap/snapcraft.yaml
+++ b/packaging/snap/snapcraft.yaml
@@ -26,7 +26,7 @@ parts:
       cp AdGuardHome ${SNAPCRAFT_PART_INSTALL}/
 apps:
   adguard-home:
-    command: AdGuardHome -c ${SNAP_COMMON}/AdGuardHome.yaml -w ${SNAP_DATA} --no-check-update
+    command: AdGuardHome -w ${SNAP_DATA} --no-check-update
     plugs: [ network-bind ]
     daemon: simple
     restart-condition: always
\ No newline at end of file

From 490784c285acc6ae32994062e3d9d1db73639713 Mon Sep 17 00:00:00 2001
From: Simon Zolin <s.zolin@adguard.com>
Date: Fri, 24 Apr 2020 10:25:46 +0300
Subject: [PATCH 21/37] dnsproxy v0.26.3

---
 go.mod | 2 +-
 go.sum | 4 ++--
 2 files changed, 3 insertions(+), 3 deletions(-)

diff --git a/go.mod b/go.mod
index 06bb22f5..8a39908f 100644
--- a/go.mod
+++ b/go.mod
@@ -3,7 +3,7 @@ module github.com/AdguardTeam/AdGuardHome
 go 1.14
 
 require (
-	github.com/AdguardTeam/dnsproxy v0.26.2
+	github.com/AdguardTeam/dnsproxy v0.26.3
 	github.com/AdguardTeam/golibs v0.4.2
 	github.com/AdguardTeam/urlfilter v0.10.0
 	github.com/NYTimes/gziphandler v1.1.1
diff --git a/go.sum b/go.sum
index bab761e2..be1e5861 100644
--- a/go.sum
+++ b/go.sum
@@ -1,5 +1,5 @@
-github.com/AdguardTeam/dnsproxy v0.26.2 h1:VaFDlhoIbhPtnpcQ/sBNVgvpMQSvMotLS+9ZzB/xuhE=
-github.com/AdguardTeam/dnsproxy v0.26.2/go.mod h1:hOYFV9TW+pd5XKYz7KZf2FFD8SvSPqjyGTxUae86s58=
+github.com/AdguardTeam/dnsproxy v0.26.3 h1:SHvJS3xnIPkJwHqP5tQa2PTqesHgoUJeeWZQw2seXiY=
+github.com/AdguardTeam/dnsproxy v0.26.3/go.mod h1:hOYFV9TW+pd5XKYz7KZf2FFD8SvSPqjyGTxUae86s58=
 github.com/AdguardTeam/golibs v0.4.0 h1:4VX6LoOqFe9p9Gf55BeD8BvJD6M6RDYmgEiHrENE9KU=
 github.com/AdguardTeam/golibs v0.4.0/go.mod h1:skKsDKIBB7kkFflLJBpfGX+G8QFTx0WKUzB6TIgtUj4=
 github.com/AdguardTeam/golibs v0.4.2 h1:7M28oTZFoFwNmp8eGPb3ImmYbxGaJLyQXeIFVHjME0o=

From b5d437c92af5b9f516212e0f8dbba5e88fcce8b8 Mon Sep 17 00:00:00 2001
From: Andrey Meshkov <am@adguard.com>
Date: Fri, 24 Apr 2020 12:00:20 +0300
Subject: [PATCH 22/37] *: snapfile for edge channel builds

---
 .gitignore     |  1 -
 build_snap.sh  |  1 +
 snapcraft.yaml | 35 +++++++++++++++++++++++++++++++++++
 3 files changed, 36 insertions(+), 1 deletion(-)
 create mode 100644 snapcraft.yaml

diff --git a/.gitignore b/.gitignore
index 7b15eb29..77c43738 100644
--- a/.gitignore
+++ b/.gitignore
@@ -22,5 +22,4 @@ dnsfilter/tests/dnsfilter.TestLotsOfRules*.pprof
 *.snap
 launchpad_credentials
 snapcraft_login
-snapcraft.yaml
 snapcraft.yaml.bak
\ No newline at end of file
diff --git a/build_snap.sh b/build_snap.sh
index 7150b394..f44b950c 100755
--- a/build_snap.sh
+++ b/build_snap.sh
@@ -257,6 +257,7 @@ cleanup() {
     rm -f snapcraft.yaml
     rm -f snapcraft.yaml.bak
     rm -f snapcraft_login
+    git checkout snapcraft.yaml
 }
 
 #######################################
diff --git a/snapcraft.yaml b/snapcraft.yaml
new file mode 100644
index 00000000..d2f4f375
--- /dev/null
+++ b/snapcraft.yaml
@@ -0,0 +1,35 @@
+# Note that this snapcraft.yaml file is used for automatic Edge channel builds ONLY!
+# We use packaging/snap/snapcraft.yaml for beta and release builds
+# Check out build_snap.sh for more details
+name: adguard-home
+base: core18
+version: 'edge'
+summary: Network-wide ads & trackers blocking DNS server
+description: |
+  AdGuard Home is a network-wide software for blocking ads & tracking. After
+  you set it up, it'll cover ALL your home devices, and you don't need any
+  client-side software for that.
+
+  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 servers -- both share a lot
+  of common code.
+grade: stable
+confinement: strict
+
+parts:
+  adguard-home:
+    plugin: make
+    source: .
+    build-snaps: [ node/13/stable, go ]
+    build-packages: [ git, build-essential ]
+    override-build: |
+      make clean
+      make
+      cp AdGuardHome ${SNAPCRAFT_PART_INSTALL}/
+apps:
+  adguard-home:
+    command: AdGuardHome -w ${SNAP_DATA} --no-check-update
+    plugs: [ network-bind ]
+    daemon: simple
+    restart-condition: always
\ No newline at end of file

From c7a2cbe04ef1b4418f774cc589eaf9c05ff1c650 Mon Sep 17 00:00:00 2001
From: Andrey Meshkov <am@adguard.com>
Date: Fri, 24 Apr 2020 12:28:00 +0300
Subject: [PATCH 23/37] *(global): limit architectures list for edge build

---
 snapcraft.yaml | 3 +++
 1 file changed, 3 insertions(+)

diff --git a/snapcraft.yaml b/snapcraft.yaml
index d2f4f375..cda2f165 100644
--- a/snapcraft.yaml
+++ b/snapcraft.yaml
@@ -17,6 +17,9 @@ description: |
 grade: stable
 confinement: strict
 
+architectures:
+  - build-on: [ amd64, i386, arm64, armhf ]
+
 parts:
   adguard-home:
     plugin: make

From 44353821e6bf99839dac15f0049a3335c0376258 Mon Sep 17 00:00:00 2001
From: Simon Zolin <s.zolin@adguard.com>
Date: Fri, 24 Apr 2020 14:04:36 +0300
Subject: [PATCH 24/37] * TestAuth: improve test

---
 home/auth_test.go | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/home/auth_test.go b/home/auth_test.go
index 38f826ec..a4829b93 100644
--- a/home/auth_test.go
+++ b/home/auth_test.go
@@ -47,7 +47,7 @@ func TestAuth(t *testing.T) {
 
 	// add session with TTL = 2 sec
 	s = session{}
-	s.expire = uint32(now + 2)
+	s.expire = uint32(time.Now().UTC().Unix() + 2)
 	a.addSession(sess, &s)
 	assert.True(t, a.CheckSession(sessStr) == 0)
 
@@ -59,7 +59,7 @@ func TestAuth(t *testing.T) {
 	// the session is still alive
 	assert.True(t, a.CheckSession(sessStr) == 0)
 	// reset our expiration time because CheckSession() has just updated it
-	s.expire = uint32(now + 2)
+	s.expire = uint32(time.Now().UTC().Unix() + 2)
 	a.storeSession(sess, &s)
 	a.Close()
 

From 9ce2a66c0e05d4525411e46db73df3cf3f52c1d0 Mon Sep 17 00:00:00 2001
From: Andrey Meshkov <am@adguard.com>
Date: Fri, 24 Apr 2020 15:08:58 +0300
Subject: [PATCH 25/37] *(documentation): added Snap Store to documentation

---
 README.md | 8 +++++++-
 1 file changed, 7 insertions(+), 1 deletion(-)

diff --git a/README.md b/README.md
index 99575e34..c44dddef 100644
--- a/README.md
+++ b/README.md
@@ -29,6 +29,9 @@
     <a href="https://github.com/AdguardTeam/AdGuardHome/releases">
         <img src="https://img.shields.io/github/release/AdguardTeam/AdGuardHome/all.svg" alt="Latest release" />
     </a>
+    <a href="https://snapcraft.io/adguard-home">
+        <img alt="adguard-home" src="https://snapcraft.io/adguard-home/badge.svg" />
+    </a>
 </p>
 
 <br />
@@ -59,7 +62,9 @@ It operates as a DNS server that re-routes tracking domains to a "black hole," t
 <a id="getting-started"></a>
 ## Getting Started
 
-Please read the [Getting Started](https://github.com/AdguardTeam/AdGuardHome/wiki/Getting-Started) article on our Wiki to learn how to install AdGuard Home, and how to configure your devices to use it.
+Please read the **[Getting Started](https://github.com/AdguardTeam/AdGuardHome/wiki/Getting-Started)** article on our Wiki to learn how to install AdGuard Home, and how to configure your devices to use it.
+
+If you're running **Linux**, there's a secure and easy way to install AdGuard Home - you can get it from the [Snap Store](https://snapcraft.io/adguard-home).
 
 Alternatively, you can use our [official Docker image](https://hub.docker.com/r/adguard/adguardhome). 
 
@@ -69,6 +74,7 @@ Alternatively, you can use our [official Docker image](https://hub.docker.com/r/
 * [AdGuard Home as a DNS-over-HTTPS or DNS-over-TLS server](https://github.com/AdguardTeam/AdGuardHome/wiki/Encryption)
 * [How to install and run AdGuard Home on Raspberry Pi](https://github.com/AdguardTeam/AdGuardHome/wiki/Raspberry-Pi)
 * [How to install and run AdGuard Home on a Virtual Private Server](https://github.com/AdguardTeam/AdGuardHome/wiki/VPS)
+* [How to write your own hosts blocklists properly](https://github.com/AdguardTeam/AdGuardHome/wiki/Hosts-Blocklists)
 
 ### API
 

From 8ad9422a4809a29f1f0e157250d67c9806ecb77f Mon Sep 17 00:00:00 2001
From: Andrey Meshkov <am@adguard.com>
Date: Fri, 24 Apr 2020 15:13:24 +0300
Subject: [PATCH 26/37] *(documentation): added info about snap edge/beta
 channels

---
 README.md | 11 +++++++----
 1 file changed, 7 insertions(+), 4 deletions(-)

diff --git a/README.md b/README.md
index c44dddef..48d78b9a 100644
--- a/README.md
+++ b/README.md
@@ -171,11 +171,12 @@ You are welcome to fork this repository, make your changes and submit a pull req
 <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.
+There are three options how you can install an unstable version.
+
+1. You can either install a beta version of AdGuard Home which we update periodically.
+2. You can use the Docker image from the `edge` tag, which is synced with the repo master branch.
+3. You can install AdGuard Home from `beta` or `edge` channels on the Snap Store.
 
-* [Docker Hub](https://hub.docker.com/r/adguard/adguardhome)
 * Beta builds
     * [Raspberry Pi (32-bit ARMv6)](https://static.adguard.com/adguardhome/beta/AdGuardHome_linux_arm.tar.gz)
     * [MacOS](https://static.adguard.com/adguardhome/beta/AdGuardHome_MacOS.zip)
@@ -188,6 +189,8 @@ or you can use the Docker image from the `edge` tag, which is synced with the re
     * [Linux 32-bit ARMv5](https://static.adguard.com/adguardhome/beta/AdGuardHome_linux_armv5.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)
+* [Docker Hub](https://hub.docker.com/r/adguard/adguardhome)
+* [Snap Store](https://snapcraft.io/adguard-home)
 
 <a id="reporting-issues"></a>
 ### Report issues

From e24143a1962c16763cb73f4e41b30117bcf0d80c Mon Sep 17 00:00:00 2001
From: Simon Zolin <s.zolin@adguard.com>
Date: Fri, 24 Apr 2020 15:50:57 +0300
Subject: [PATCH 27/37] - Web: flush the bufferred response data before
 performing global operations

---
 home/control_install.go | 7 +++++--
 home/control_update.go  | 4 +++-
 home/tls.go             | 5 ++++-
 3 files changed, 12 insertions(+), 4 deletions(-)

diff --git a/home/control_install.go b/home/control_install.go
index 4a39983f..864ad96b 100644
--- a/home/control_install.go
+++ b/home/control_install.go
@@ -351,6 +351,11 @@ func (web *Web) handleInstallConfigure(w http.ResponseWriter, r *http.Request) {
 
 	registerControlHandlers()
 
+	returnOK(w)
+	if f, ok := w.(http.Flusher); ok {
+		f.Flush()
+	}
+
 	// this needs to be done in a goroutine because Shutdown() is a blocking call, and it will block
 	// until all requests are finished, and _we_ are inside a request right now, so it will block indefinitely
 	if restartHTTP {
@@ -358,8 +363,6 @@ func (web *Web) handleInstallConfigure(w http.ResponseWriter, r *http.Request) {
 			_ = Context.web.httpServer.Shutdown(context.TODO())
 		}()
 	}
-
-	returnOK(w)
 }
 
 func (web *Web) registerInstallHandlers() {
diff --git a/home/control_update.go b/home/control_update.go
index b0080e44..6115fac1 100644
--- a/home/control_update.go
+++ b/home/control_update.go
@@ -548,7 +548,9 @@ func handleUpdate(w http.ResponseWriter, r *http.Request) {
 	}
 
 	returnOK(w)
+	if f, ok := w.(http.Flusher); ok {
+		f.Flush()
+	}
 
-	time.Sleep(time.Second) // wait (hopefully) until response is sent (not sure whether it's really necessary)
 	go finishUpdate(u)
 }
diff --git a/home/tls.go b/home/tls.go
index 1e6267fe..5977f859 100644
--- a/home/tls.go
+++ b/home/tls.go
@@ -279,11 +279,14 @@ func (t *TLSMod) handleTLSConfigure(w http.ResponseWriter, r *http.Request) {
 		tlsConfigStatus:   t.status,
 	}
 	marshalTLS(w, data2)
+	if f, ok := w.(http.Flusher); ok {
+		f.Flush()
+	}
+
 	// this needs to be done in a goroutine because Shutdown() is a blocking call, and it will block
 	// until all requests are finished, and _we_ are inside a request right now, so it will block indefinitely
 	if restartHTTPS {
 		go func() {
-			time.Sleep(time.Second) // TODO: could not find a way to reliably know that data was fully sent to client by https server, so we wait a bit to let response through before closing the server
 			Context.web.TLSConfigChanged(data)
 		}()
 	}

From 3b0914715ee0be2a5b6236729b9976193db9bb17 Mon Sep 17 00:00:00 2001
From: ArtemBaskal <a.baskal@adguard.com>
Date: Fri, 24 Apr 2020 16:51:44 +0300
Subject: [PATCH 28/37] + client: Switch places for "Upstream DNS servers" and
 "DNS servers configuration"

---
 client/package-lock.json                    | 843 ++++++++++++++------
 client/src/components/Settings/Dns/index.js |   8 +-
 2 files changed, 606 insertions(+), 245 deletions(-)

diff --git a/client/package-lock.json b/client/package-lock.json
index 0f020587..6c73efd3 100644
--- a/client/package-lock.json
+++ b/client/package-lock.json
@@ -167,9 +167,9 @@
           }
         },
         "minimist": {
-          "version": "1.2.0",
-          "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz",
-          "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=",
+          "version": "1.2.5",
+          "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz",
+          "integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==",
           "dev": true
         },
         "ms": {
@@ -778,9 +778,9 @@
       }
     },
     "acorn": {
-      "version": "5.7.2",
-      "resolved": "https://registry.npmjs.org/acorn/-/acorn-5.7.2.tgz",
-      "integrity": "sha512-cJrKCNcr2kv8dlDnbw+JPUGjHZzo4myaxOLmpOX8a+rgX94YeTcTMv/LFJUSByRpc+i4GgVnnhLxvMu/2Y+rqw==",
+      "version": "5.7.4",
+      "resolved": "https://registry.npmjs.org/acorn/-/acorn-5.7.4.tgz",
+      "integrity": "sha512-1D++VG7BhrtvQpNbBzovKNc1FLGGEE/oGe7b9xJm/RFHMBeUaUGpluV9RLjZa47YFdPcDAenEYuq9pQPcMdLJg==",
       "dev": true
     },
     "acorn-dynamic-import": {
@@ -2259,8 +2259,7 @@
         },
         "kind-of": {
           "version": "6.0.2",
-          "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.2.tgz",
-          "integrity": "sha512-s5kLOcnH0XqDO+FvuaLX8DDjZ18CGFk7VygH40QoKPUQhW4e2rvM0rwUq0t8IQDOwYSeLK01U90OjzBTme2QqA==",
+          "resolved": "",
           "dev": true
         }
       }
@@ -3057,8 +3056,7 @@
         },
         "kind-of": {
           "version": "6.0.2",
-          "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.2.tgz",
-          "integrity": "sha512-s5kLOcnH0XqDO+FvuaLX8DDjZ18CGFk7VygH40QoKPUQhW4e2rvM0rwUq0t8IQDOwYSeLK01U90OjzBTme2QqA==",
+          "resolved": "",
           "dev": true
         },
         "micromatch": {
@@ -3208,6 +3206,17 @@
       "integrity": "sha1-bqa989hTrlTMuOR7+gvz+QMfsYQ=",
       "dev": true
     },
+    "coa": {
+      "version": "2.0.2",
+      "resolved": "https://registry.npmjs.org/coa/-/coa-2.0.2.tgz",
+      "integrity": "sha512-q5/jG+YQnSy4nRTV4F7lPepBJZ8qBNJJDBuJdoejDyLXgmL7IEo+Le2JDZudFTFt7mrCqIRaSjws4ygRCTCAXA==",
+      "dev": true,
+      "requires": {
+        "@types/q": "^1.5.1",
+        "chalk": "^2.4.1",
+        "q": "^1.1.2"
+      }
+    },
     "code-point-at": {
       "version": "1.1.0",
       "resolved": "https://registry.npmjs.org/code-point-at/-/code-point-at-1.1.0.tgz",
@@ -3679,8 +3688,7 @@
         },
         "minimist": {
           "version": "1.2.0",
-          "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz",
-          "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=",
+          "resolved": "",
           "dev": true
         },
         "normalize-path": {
@@ -3734,6 +3742,22 @@
         "nth-check": "~1.0.1"
       }
     },
+    "css-select-base-adapter": {
+      "version": "0.1.1",
+      "resolved": "https://registry.npmjs.org/css-select-base-adapter/-/css-select-base-adapter-0.1.1.tgz",
+      "integrity": "sha512-jQVeeRG70QI08vSTwf1jHxp74JoZsr2XSgETae8/xC8ovSnL2WF87GTLO86Sbwdt2lK4Umg4HnnwMO4YF3Ce7w==",
+      "dev": true
+    },
+    "css-tree": {
+      "version": "1.0.0-alpha.37",
+      "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-1.0.0-alpha.37.tgz",
+      "integrity": "sha512-DMxWJg0rnz7UgxKT0Q1HU/L9BeJI0M6ksor0OgqOnF+aRCDWg/N2641HmVyU9KVIu0OVVWOb2IpC9A+BJRnejg==",
+      "dev": true,
+      "requires": {
+        "mdn-data": "2.0.4",
+        "source-map": "^0.6.1"
+      }
+    },
     "css-what": {
       "version": "2.1.0",
       "resolved": "https://registry.npmjs.org/css-what/-/css-what-2.1.0.tgz",
@@ -3752,6 +3776,33 @@
       "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==",
       "dev": true
     },
+    "csso": {
+      "version": "4.0.3",
+      "resolved": "https://registry.npmjs.org/csso/-/csso-4.0.3.tgz",
+      "integrity": "sha512-NL3spysxUkcrOgnpsT4Xdl2aiEiBG6bXswAABQVHcMrfjjBisFOKwLDOmf4wf32aPdcJws1zds2B0Rg+jqMyHQ==",
+      "dev": true,
+      "requires": {
+        "css-tree": "1.0.0-alpha.39"
+      },
+      "dependencies": {
+        "css-tree": {
+          "version": "1.0.0-alpha.39",
+          "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-1.0.0-alpha.39.tgz",
+          "integrity": "sha512-7UvkEYgBAHRG9Nt980lYxjsTrCyHFN53ky3wVsDkiMdVqylqRt+Zc+jm5qw7/qyOvN2dHSYtX0e4MbCCExSvnA==",
+          "dev": true,
+          "requires": {
+            "mdn-data": "2.0.6",
+            "source-map": "^0.6.1"
+          }
+        },
+        "mdn-data": {
+          "version": "2.0.6",
+          "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.6.tgz",
+          "integrity": "sha512-rQvjv71olwNHgiTbfPZFkJtjNMciWgswYeciZhtvWLO8bmX3TnhyA62I6sTWOyZssWHJJjY6/KiWwqQsWWsqOA==",
+          "dev": true
+        }
+      }
+    },
     "csstype": {
       "version": "2.6.8",
       "resolved": "https://registry.npmjs.org/csstype/-/csstype-2.6.8.tgz",
@@ -4024,9 +4075,9 @@
           "dev": true
         },
         "kind-of": {
-          "version": "6.0.2",
-          "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.2.tgz",
-          "integrity": "sha512-s5kLOcnH0XqDO+FvuaLX8DDjZ18CGFk7VygH40QoKPUQhW4e2rvM0rwUq0t8IQDOwYSeLK01U90OjzBTme2QqA==",
+          "version": "6.0.3",
+          "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz",
+          "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==",
           "dev": true
         }
       }
@@ -4918,6 +4969,12 @@
         "acorn-jsx": "^3.0.0"
       }
     },
+    "esprima": {
+      "version": "4.0.1",
+      "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz",
+      "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==",
+      "dev": true
+    },
     "esquery": {
       "version": "1.0.1",
       "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.0.1.tgz",
@@ -5231,8 +5288,7 @@
         },
         "kind-of": {
           "version": "6.0.2",
-          "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.2.tgz",
-          "integrity": "sha512-s5kLOcnH0XqDO+FvuaLX8DDjZ18CGFk7VygH40QoKPUQhW4e2rvM0rwUq0t8IQDOwYSeLK01U90OjzBTme2QqA==",
+          "resolved": "",
           "dev": true
         }
       }
@@ -6226,9 +6282,9 @@
       },
       "dependencies": {
         "kind-of": {
-          "version": "6.0.2",
-          "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.2.tgz",
-          "integrity": "sha512-s5kLOcnH0XqDO+FvuaLX8DDjZ18CGFk7VygH40QoKPUQhW4e2rvM0rwUq0t8IQDOwYSeLK01U90OjzBTme2QqA==",
+          "version": "6.0.3",
+          "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz",
+          "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==",
           "dev": true
         }
       }
@@ -6268,20 +6324,12 @@
       "dev": true
     },
     "gonzales-pe": {
-      "version": "4.2.3",
-      "resolved": "https://registry.npmjs.org/gonzales-pe/-/gonzales-pe-4.2.3.tgz",
-      "integrity": "sha512-Kjhohco0esHQnOiqqdJeNz/5fyPkOMD/d6XVjwTAoPGUFh0mCollPUTUTa2OZy4dYNAqlPIQdTiNzJTWdd9Htw==",
+      "version": "4.3.0",
+      "resolved": "https://registry.npmjs.org/gonzales-pe/-/gonzales-pe-4.3.0.tgz",
+      "integrity": "sha512-otgSPpUmdWJ43VXyiNgEYE4luzHCL2pz4wQ0OnDluC6Eg4Ko3Vexy/SrSynglw/eR+OhkzmqFCZa/OFa/RgAOQ==",
       "dev": true,
       "requires": {
-        "minimist": "1.1.x"
-      },
-      "dependencies": {
-        "minimist": {
-          "version": "1.1.3",
-          "resolved": "http://registry.npmjs.org/minimist/-/minimist-1.1.3.tgz",
-          "integrity": "sha1-O+39kaktOQFvz6ocaB6Pqhoe/ag=",
-          "dev": true
-        }
+        "minimist": "^1.2.5"
       }
     },
     "graceful-fs": {
@@ -6324,6 +6372,12 @@
       "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz",
       "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0="
     },
+    "has-symbols": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.1.tgz",
+      "integrity": "sha512-PLcsoqu++dmEIZB+6totNFKq/7Do+Z0u4oT0zKOJNl3lYK6vGwwu2hjHs+68OEZbTjiUE9bgOABXbP/GvrS0Kg==",
+      "dev": true
+    },
     "has-value": {
       "version": "1.0.0",
       "resolved": "https://registry.npmjs.org/has-value/-/has-value-1.0.0.tgz",
@@ -6890,9 +6944,9 @@
           "dev": true
         },
         "kind-of": {
-          "version": "6.0.2",
-          "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.2.tgz",
-          "integrity": "sha512-s5kLOcnH0XqDO+FvuaLX8DDjZ18CGFk7VygH40QoKPUQhW4e2rvM0rwUq0t8IQDOwYSeLK01U90OjzBTme2QqA==",
+          "version": "6.0.3",
+          "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz",
+          "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==",
           "dev": true
         },
         "micromatch": {
@@ -7589,6 +7643,16 @@
       "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
       "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="
     },
+    "js-yaml": {
+      "version": "3.13.1",
+      "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.13.1.tgz",
+      "integrity": "sha512-YfbcO7jXDdyj0DGxYVSlSeQNHbD7XPWvrVWeVUujrQEoZzWJIRrCPoyk6kL6IAjAG2IolMK4T0hNUe0HOUs5Jw==",
+      "dev": true,
+      "requires": {
+        "argparse": "^1.0.7",
+        "esprima": "^4.0.0"
+      }
+    },
     "jsesc": {
       "version": "1.3.0",
       "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-1.3.0.tgz",
@@ -7706,13 +7770,13 @@
       }
     },
     "loader-fs-cache": {
-      "version": "1.0.1",
-      "resolved": "https://registry.npmjs.org/loader-fs-cache/-/loader-fs-cache-1.0.1.tgz",
-      "integrity": "sha1-VuC/CL2XCLJqdltoUJhAyN7J/bw=",
+      "version": "1.0.3",
+      "resolved": "https://registry.npmjs.org/loader-fs-cache/-/loader-fs-cache-1.0.3.tgz",
+      "integrity": "sha512-ldcgZpjNJj71n+2Mf6yetz+c9bM4xpKtNds4LbqXzU/PTdeAX0g3ytnU1AJMEcTk2Lex4Smpe3Q/eCTsvUBxbA==",
       "dev": true,
       "requires": {
         "find-cache-dir": "^0.1.1",
-        "mkdirp": "0.5.1"
+        "mkdirp": "^0.5.1"
       },
       "dependencies": {
         "find-cache-dir": {
@@ -7978,9 +8042,9 @@
       }
     },
     "mdn-data": {
-      "version": "1.1.4",
-      "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-1.1.4.tgz",
-      "integrity": "sha512-FSYbp3lyKjyj3E7fMl6rYvUdX0FBXaluGqlFoYESWQlyUTq8R+wp0rkFxoYFqZlHCvsUXGjyJmLQSnXToYhOSA==",
+      "version": "2.0.4",
+      "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.4.tgz",
+      "integrity": "sha512-iV3XNKw06j5Q7mi6h+9vbx23Tv7JkjEVgKHW4pimwyDGWm0OIQntJJ+u1C6mg6mK1EaTv42XQ7w76yuzH7M2cA==",
       "dev": true
     },
     "media-typer": {
@@ -8070,9 +8134,9 @@
       },
       "dependencies": {
         "kind-of": {
-          "version": "6.0.2",
-          "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.2.tgz",
-          "integrity": "sha512-s5kLOcnH0XqDO+FvuaLX8DDjZ18CGFk7VygH40QoKPUQhW4e2rvM0rwUq0t8IQDOwYSeLK01U90OjzBTme2QqA==",
+          "version": "6.0.3",
+          "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz",
+          "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==",
           "dev": true
         }
       }
@@ -8136,9 +8200,9 @@
       }
     },
     "minimist": {
-      "version": "0.0.8",
-      "resolved": "http://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz",
-      "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=",
+      "version": "1.2.5",
+      "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz",
+      "integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==",
       "dev": true
     },
     "minimist-options": {
@@ -8191,12 +8255,12 @@
       }
     },
     "mkdirp": {
-      "version": "0.5.1",
-      "resolved": "http://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz",
-      "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=",
+      "version": "0.5.5",
+      "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.5.tgz",
+      "integrity": "sha512-NKmAlESf6jMGym1++R0Ra7wvhV+wFW63FaSOFPwRahvea0gMUcGUhVeAg/0BC0wiv9ih5NYPB1Wn1UEI1/L+xQ==",
       "dev": true,
       "requires": {
-        "minimist": "0.0.8"
+        "minimist": "^1.2.5"
       }
     },
     "move-concurrently": {
@@ -8284,9 +8348,9 @@
           "dev": true
         },
         "kind-of": {
-          "version": "6.0.2",
-          "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.2.tgz",
-          "integrity": "sha512-s5kLOcnH0XqDO+FvuaLX8DDjZ18CGFk7VygH40QoKPUQhW4e2rvM0rwUq0t8IQDOwYSeLK01U90OjzBTme2QqA==",
+          "version": "6.0.3",
+          "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz",
+          "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==",
           "dev": true
         }
       }
@@ -8489,6 +8553,12 @@
       "integrity": "sha512-05KzQ70lSeGSrZJQXE5wNDiTkBJDlUT/myi6RX9dVIvz7a7Qh4oH93BQdiPMn27nldYvVQCKMUaM83AfizZlsQ==",
       "dev": true
     },
+    "object-inspect": {
+      "version": "1.7.0",
+      "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.7.0.tgz",
+      "integrity": "sha512-a7pEHdh1xKIAgTySUGgLMx/xwDZskN1Ud6egYYN3EdRW4ZMPNEDUTF+hwy2LUC+Bl+SyLXANnwz/jyh/qutKUw==",
+      "dev": true
+    },
     "object-keys": {
       "version": "1.0.12",
       "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.0.12.tgz",
@@ -8512,6 +8582,18 @@
         }
       }
     },
+    "object.assign": {
+      "version": "4.1.0",
+      "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.0.tgz",
+      "integrity": "sha512-exHJeq6kBKj58mqGyTQ9DFvrZC/eR6OwxzoM9YRoGBqrXYonaFyGiFMuc9VZrXf7DarreEwMpurG3dd+CNyW5w==",
+      "dev": true,
+      "requires": {
+        "define-properties": "^1.1.2",
+        "function-bind": "^1.1.1",
+        "has-symbols": "^1.0.0",
+        "object-keys": "^1.0.11"
+      }
+    },
     "object.getownpropertydescriptors": {
       "version": "2.0.3",
       "resolved": "https://registry.npmjs.org/object.getownpropertydescriptors/-/object.getownpropertydescriptors-2.0.3.tgz",
@@ -8539,6 +8621,80 @@
         }
       }
     },
+    "object.values": {
+      "version": "1.1.1",
+      "resolved": "https://registry.npmjs.org/object.values/-/object.values-1.1.1.tgz",
+      "integrity": "sha512-WTa54g2K8iu0kmS/us18jEmdv1a4Wi//BZ/DTVYEcH0XhLM5NYdpDHja3gt57VrZLcNAO2WGA+KpWsDBaHt6eA==",
+      "dev": true,
+      "requires": {
+        "define-properties": "^1.1.3",
+        "es-abstract": "^1.17.0-next.1",
+        "function-bind": "^1.1.1",
+        "has": "^1.0.3"
+      },
+      "dependencies": {
+        "es-abstract": {
+          "version": "1.17.5",
+          "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.17.5.tgz",
+          "integrity": "sha512-BR9auzDbySxOcfog0tLECW8l28eRGpDpU3Dm3Hp4q/N+VtLTmyj4EUN088XZWQDW/hzj6sYRDXeOFsaAODKvpg==",
+          "dev": true,
+          "requires": {
+            "es-to-primitive": "^1.2.1",
+            "function-bind": "^1.1.1",
+            "has": "^1.0.3",
+            "has-symbols": "^1.0.1",
+            "is-callable": "^1.1.5",
+            "is-regex": "^1.0.5",
+            "object-inspect": "^1.7.0",
+            "object-keys": "^1.1.1",
+            "object.assign": "^4.1.0",
+            "string.prototype.trimleft": "^2.1.1",
+            "string.prototype.trimright": "^2.1.1"
+          }
+        },
+        "es-to-primitive": {
+          "version": "1.2.1",
+          "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.2.1.tgz",
+          "integrity": "sha512-QCOllgZJtaUo9miYBcLChTUaHNjJF3PYs1VidD7AwiEj1kYxKeQTctLAezAOH5ZKRH0g2IgPn6KwB4IT8iRpvA==",
+          "dev": true,
+          "requires": {
+            "is-callable": "^1.1.4",
+            "is-date-object": "^1.0.1",
+            "is-symbol": "^1.0.2"
+          }
+        },
+        "is-callable": {
+          "version": "1.1.5",
+          "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.1.5.tgz",
+          "integrity": "sha512-ESKv5sMCJB2jnHTWZ3O5itG+O128Hsus4K4Qh1h2/cgn2vbgnLSVqfV46AeJA9D5EeeLa9w81KUXMtn34zhX+Q==",
+          "dev": true
+        },
+        "is-regex": {
+          "version": "1.0.5",
+          "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.0.5.tgz",
+          "integrity": "sha512-vlKW17SNq44owv5AQR3Cq0bQPEb8+kF3UKZ2fiZNOWtztYE5i0CzCZxFDwO58qAOWtxdBRVO/V5Qin1wjCqFYQ==",
+          "dev": true,
+          "requires": {
+            "has": "^1.0.3"
+          }
+        },
+        "is-symbol": {
+          "version": "1.0.3",
+          "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.0.3.tgz",
+          "integrity": "sha512-OwijhaRSgqvhm/0ZdAcXNZt9lYdKFpcRDT5ULUuYXPoT794UNOdU+gpT6Rzo7b4V2HUl/op6GqY894AZwv9faQ==",
+          "dev": true,
+          "requires": {
+            "has-symbols": "^1.0.1"
+          }
+        },
+        "object-keys": {
+          "version": "1.1.1",
+          "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz",
+          "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==",
+          "dev": true
+        }
+      }
+    },
     "obuf": {
       "version": "1.1.2",
       "resolved": "https://registry.npmjs.org/obuf/-/obuf-1.1.2.tgz",
@@ -8879,30 +9035,39 @@
       "dev": true
     },
     "portfinder": {
-      "version": "1.0.20",
-      "resolved": "https://registry.npmjs.org/portfinder/-/portfinder-1.0.20.tgz",
-      "integrity": "sha512-Yxe4mTyDzTd59PZJY4ojZR8F+E5e97iq2ZOHPz3HDgSvYC5siNad2tLooQ5y5QHyQhc3xVqvyk/eNA3wuoa7Sw==",
+      "version": "1.0.25",
+      "resolved": "https://registry.npmjs.org/portfinder/-/portfinder-1.0.25.tgz",
+      "integrity": "sha512-6ElJnHBbxVA1XSLgBp7G1FiCkQdlqGzuF7DswL5tcea+E8UpuvPU7beVAjjRwCioTS9ZluNbu+ZyRvgTsmqEBg==",
       "dev": true,
       "requires": {
-        "async": "^1.5.2",
-        "debug": "^2.2.0",
-        "mkdirp": "0.5.x"
+        "async": "^2.6.2",
+        "debug": "^3.1.1",
+        "mkdirp": "^0.5.1"
       },
       "dependencies": {
         "async": {
-          "version": "1.5.2",
-          "resolved": "https://registry.npmjs.org/async/-/async-1.5.2.tgz",
-          "integrity": "sha1-7GphrlZIDAw8skHJVhjiCJL5Zyo=",
-          "dev": true
-        },
-        "debug": {
-          "version": "2.6.9",
-          "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
-          "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
+          "version": "2.6.3",
+          "resolved": "https://registry.npmjs.org/async/-/async-2.6.3.tgz",
+          "integrity": "sha512-zflvls11DCy+dQWzTW2dzuilv8Z5X/pjfmZOWba6TNIVDm+2UDaJmXSOXlasHKfNBs8oo3M0aT50fDEWfKZjXg==",
           "dev": true,
           "requires": {
-            "ms": "2.0.0"
+            "lodash": "^4.17.14"
           }
+        },
+        "debug": {
+          "version": "3.2.6",
+          "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.6.tgz",
+          "integrity": "sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ==",
+          "dev": true,
+          "requires": {
+            "ms": "^2.1.1"
+          }
+        },
+        "ms": {
+          "version": "2.1.2",
+          "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
+          "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==",
+          "dev": true
         }
       }
     },
@@ -10028,171 +10193,6 @@
         "postcss-value-parser": "3.x",
         "svgo": "1.x",
         "xmldoc": "1.x"
-      },
-      "dependencies": {
-        "coa": {
-          "version": "2.0.2",
-          "resolved": "https://registry.npmjs.org/coa/-/coa-2.0.2.tgz",
-          "integrity": "sha512-q5/jG+YQnSy4nRTV4F7lPepBJZ8qBNJJDBuJdoejDyLXgmL7IEo+Le2JDZudFTFt7mrCqIRaSjws4ygRCTCAXA==",
-          "dev": true,
-          "requires": {
-            "@types/q": "^1.5.1",
-            "chalk": "^2.4.1",
-            "q": "^1.1.2"
-          }
-        },
-        "css-select": {
-          "version": "2.0.2",
-          "resolved": "https://registry.npmjs.org/css-select/-/css-select-2.0.2.tgz",
-          "integrity": "sha512-dSpYaDVoWaELjvZ3mS6IKZM/y2PMPa/XYoEfYNZePL4U/XgyxZNroHEHReDx/d+VgXh9VbCTtFqLkFbmeqeaRQ==",
-          "dev": true,
-          "requires": {
-            "boolbase": "^1.0.0",
-            "css-what": "^2.1.2",
-            "domutils": "^1.7.0",
-            "nth-check": "^1.0.2"
-          },
-          "dependencies": {
-            "css-what": {
-              "version": "2.1.3",
-              "resolved": "https://registry.npmjs.org/css-what/-/css-what-2.1.3.tgz",
-              "integrity": "sha512-a+EPoD+uZiNfh+5fxw2nO9QwFa6nJe2Or35fGY6Ipw1R3R4AGz1d1TEZrCegvw2YTmZ0jXirGYlzxxpYSHwpEg==",
-              "dev": true
-            },
-            "domutils": {
-              "version": "1.7.0",
-              "resolved": "https://registry.npmjs.org/domutils/-/domutils-1.7.0.tgz",
-              "integrity": "sha512-Lgd2XcJ/NjEw+7tFvfKxOzCYKZsdct5lczQ2ZaQY8Djz7pfAD3Gbp8ySJWtreII/vDlMVmxwa6pHmdxIYgttDg==",
-              "dev": true,
-              "requires": {
-                "dom-serializer": "0",
-                "domelementtype": "1"
-              }
-            },
-            "nth-check": {
-              "version": "1.0.2",
-              "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-1.0.2.tgz",
-              "integrity": "sha512-WeBOdju8SnzPN5vTUJYxYUxLeXpCaVP5i5e0LF8fg7WORF2Wd7wFX/pk0tYZk7s8T+J7VLy0Da6J1+wCT0AtHg==",
-              "dev": true,
-              "requires": {
-                "boolbase": "~1.0.0"
-              }
-            }
-          }
-        },
-        "csso": {
-          "version": "3.5.1",
-          "resolved": "https://registry.npmjs.org/csso/-/csso-3.5.1.tgz",
-          "integrity": "sha512-vrqULLffYU1Q2tLdJvaCYbONStnfkfimRxXNaGjxMldI0C7JPBC4rB1RyjhfdZ4m1frm8pM9uRPKH3d2knZ8gg==",
-          "dev": true,
-          "requires": {
-            "css-tree": "1.0.0-alpha.29"
-          },
-          "dependencies": {
-            "css-tree": {
-              "version": "1.0.0-alpha.29",
-              "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-1.0.0-alpha.29.tgz",
-              "integrity": "sha512-sRNb1XydwkW9IOci6iB2xmy8IGCj6r/fr+JWitvJ2JxQRPzN3T4AGGVWCMlVmVwM1gtgALJRmGIlWv5ppnGGkg==",
-              "dev": true,
-              "requires": {
-                "mdn-data": "~1.1.0",
-                "source-map": "^0.5.3"
-              }
-            }
-          }
-        },
-        "esprima": {
-          "version": "4.0.1",
-          "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz",
-          "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==",
-          "dev": true
-        },
-        "js-yaml": {
-          "version": "3.13.1",
-          "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.13.1.tgz",
-          "integrity": "sha512-YfbcO7jXDdyj0DGxYVSlSeQNHbD7XPWvrVWeVUujrQEoZzWJIRrCPoyk6kL6IAjAG2IolMK4T0hNUe0HOUs5Jw==",
-          "dev": true,
-          "requires": {
-            "argparse": "^1.0.7",
-            "esprima": "^4.0.0"
-          }
-        },
-        "source-map": {
-          "version": "0.5.7",
-          "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz",
-          "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=",
-          "dev": true
-        },
-        "svgo": {
-          "version": "1.2.2",
-          "resolved": "https://registry.npmjs.org/svgo/-/svgo-1.2.2.tgz",
-          "integrity": "sha512-rAfulcwp2D9jjdGu+0CuqlrAUin6bBWrpoqXWwKDZZZJfXcUXQSxLJOFJCQCSA0x0pP2U0TxSlJu2ROq5Bq6qA==",
-          "dev": true,
-          "requires": {
-            "chalk": "^2.4.1",
-            "coa": "^2.0.2",
-            "css-select": "^2.0.0",
-            "css-select-base-adapter": "^0.1.1",
-            "css-tree": "1.0.0-alpha.28",
-            "css-url-regex": "^1.1.0",
-            "csso": "^3.5.1",
-            "js-yaml": "^3.13.1",
-            "mkdirp": "~0.5.1",
-            "object.values": "^1.1.0",
-            "sax": "~1.2.4",
-            "stable": "^0.1.8",
-            "unquote": "~1.1.1",
-            "util.promisify": "~1.0.0"
-          },
-          "dependencies": {
-            "css-select-base-adapter": {
-              "version": "0.1.1",
-              "resolved": "https://registry.npmjs.org/css-select-base-adapter/-/css-select-base-adapter-0.1.1.tgz",
-              "integrity": "sha512-jQVeeRG70QI08vSTwf1jHxp74JoZsr2XSgETae8/xC8ovSnL2WF87GTLO86Sbwdt2lK4Umg4HnnwMO4YF3Ce7w==",
-              "dev": true
-            },
-            "css-tree": {
-              "version": "1.0.0-alpha.28",
-              "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-1.0.0-alpha.28.tgz",
-              "integrity": "sha512-joNNW1gCp3qFFzj4St6zk+Wh/NBv0vM5YbEreZk0SD4S23S+1xBKb6cLDg2uj4P4k/GUMlIm6cKIDqIG+vdt0w==",
-              "dev": true,
-              "requires": {
-                "mdn-data": "~1.1.0",
-                "source-map": "^0.5.3"
-              }
-            },
-            "css-url-regex": {
-              "version": "1.1.0",
-              "resolved": "https://registry.npmjs.org/css-url-regex/-/css-url-regex-1.1.0.tgz",
-              "integrity": "sha1-g4NCMMyfdMRX3lnuvRVD/uuDt+w=",
-              "dev": true
-            },
-            "object.values": {
-              "version": "1.1.0",
-              "resolved": "https://registry.npmjs.org/object.values/-/object.values-1.1.0.tgz",
-              "integrity": "sha512-8mf0nKLAoFX6VlNVdhGj31SVYpaNFtUnuoOXWyFEstsWRgU837AK+JYM0iAxwkSzGRbwn8cbFmgbyxj1j4VbXg==",
-              "dev": true,
-              "requires": {
-                "define-properties": "^1.1.3",
-                "es-abstract": "^1.12.0",
-                "function-bind": "^1.1.1",
-                "has": "^1.0.3"
-              }
-            },
-            "stable": {
-              "version": "0.1.8",
-              "resolved": "https://registry.npmjs.org/stable/-/stable-0.1.8.tgz",
-              "integrity": "sha512-ji9qxRnOVfcuLDySj9qzhGSEFVobyt1kIOSkj1qZzYLzq7Tos/oUUWvotUPQLlrsidqsK6tBH89Bc9kL5zHA6w==",
-              "dev": true
-            },
-            "unquote": {
-              "version": "1.1.1",
-              "resolved": "https://registry.npmjs.org/unquote/-/unquote-1.1.1.tgz",
-              "integrity": "sha1-j97XMk7G6IoP+LkF58CYzcCG1UQ=",
-              "dev": true
-            }
-          }
-        }
       }
     },
     "postcss-syntax": {
@@ -11525,8 +11525,7 @@
         },
         "kind-of": {
           "version": "6.0.2",
-          "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.2.tgz",
-          "integrity": "sha512-s5kLOcnH0XqDO+FvuaLX8DDjZ18CGFk7VygH40QoKPUQhW4e2rvM0rwUq0t8IQDOwYSeLK01U90OjzBTme2QqA==",
+          "resolved": "",
           "dev": true
         }
       }
@@ -11772,6 +11771,12 @@
         "safe-buffer": "^5.1.1"
       }
     },
+    "stable": {
+      "version": "0.1.8",
+      "resolved": "https://registry.npmjs.org/stable/-/stable-0.1.8.tgz",
+      "integrity": "sha512-ji9qxRnOVfcuLDySj9qzhGSEFVobyt1kIOSkj1qZzYLzq7Tos/oUUWvotUPQLlrsidqsK6tBH89Bc9kL5zHA6w==",
+      "dev": true
+    },
     "state-toggle": {
       "version": "1.0.1",
       "resolved": "https://registry.npmjs.org/state-toggle/-/state-toggle-1.0.1.tgz",
@@ -11871,6 +11876,296 @@
         }
       }
     },
+    "string.prototype.trimend": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.1.tgz",
+      "integrity": "sha512-LRPxFUaTtpqYsTeNKaFOw3R4bxIzWOnbQ837QfBylo8jIxtcbK/A/sMV7Q+OAV/vWo+7s25pOE10KYSjaSO06g==",
+      "dev": true,
+      "requires": {
+        "define-properties": "^1.1.3",
+        "es-abstract": "^1.17.5"
+      },
+      "dependencies": {
+        "es-abstract": {
+          "version": "1.17.5",
+          "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.17.5.tgz",
+          "integrity": "sha512-BR9auzDbySxOcfog0tLECW8l28eRGpDpU3Dm3Hp4q/N+VtLTmyj4EUN088XZWQDW/hzj6sYRDXeOFsaAODKvpg==",
+          "dev": true,
+          "requires": {
+            "es-to-primitive": "^1.2.1",
+            "function-bind": "^1.1.1",
+            "has": "^1.0.3",
+            "has-symbols": "^1.0.1",
+            "is-callable": "^1.1.5",
+            "is-regex": "^1.0.5",
+            "object-inspect": "^1.7.0",
+            "object-keys": "^1.1.1",
+            "object.assign": "^4.1.0",
+            "string.prototype.trimleft": "^2.1.1",
+            "string.prototype.trimright": "^2.1.1"
+          }
+        },
+        "es-to-primitive": {
+          "version": "1.2.1",
+          "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.2.1.tgz",
+          "integrity": "sha512-QCOllgZJtaUo9miYBcLChTUaHNjJF3PYs1VidD7AwiEj1kYxKeQTctLAezAOH5ZKRH0g2IgPn6KwB4IT8iRpvA==",
+          "dev": true,
+          "requires": {
+            "is-callable": "^1.1.4",
+            "is-date-object": "^1.0.1",
+            "is-symbol": "^1.0.2"
+          }
+        },
+        "is-callable": {
+          "version": "1.1.5",
+          "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.1.5.tgz",
+          "integrity": "sha512-ESKv5sMCJB2jnHTWZ3O5itG+O128Hsus4K4Qh1h2/cgn2vbgnLSVqfV46AeJA9D5EeeLa9w81KUXMtn34zhX+Q==",
+          "dev": true
+        },
+        "is-regex": {
+          "version": "1.0.5",
+          "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.0.5.tgz",
+          "integrity": "sha512-vlKW17SNq44owv5AQR3Cq0bQPEb8+kF3UKZ2fiZNOWtztYE5i0CzCZxFDwO58qAOWtxdBRVO/V5Qin1wjCqFYQ==",
+          "dev": true,
+          "requires": {
+            "has": "^1.0.3"
+          }
+        },
+        "is-symbol": {
+          "version": "1.0.3",
+          "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.0.3.tgz",
+          "integrity": "sha512-OwijhaRSgqvhm/0ZdAcXNZt9lYdKFpcRDT5ULUuYXPoT794UNOdU+gpT6Rzo7b4V2HUl/op6GqY894AZwv9faQ==",
+          "dev": true,
+          "requires": {
+            "has-symbols": "^1.0.1"
+          }
+        },
+        "object-keys": {
+          "version": "1.1.1",
+          "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz",
+          "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==",
+          "dev": true
+        }
+      }
+    },
+    "string.prototype.trimleft": {
+      "version": "2.1.2",
+      "resolved": "https://registry.npmjs.org/string.prototype.trimleft/-/string.prototype.trimleft-2.1.2.tgz",
+      "integrity": "sha512-gCA0tza1JBvqr3bfAIFJGqfdRTyPae82+KTnm3coDXkZN9wnuW3HjGgN386D7hfv5CHQYCI022/rJPVlqXyHSw==",
+      "dev": true,
+      "requires": {
+        "define-properties": "^1.1.3",
+        "es-abstract": "^1.17.5",
+        "string.prototype.trimstart": "^1.0.0"
+      },
+      "dependencies": {
+        "es-abstract": {
+          "version": "1.17.5",
+          "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.17.5.tgz",
+          "integrity": "sha512-BR9auzDbySxOcfog0tLECW8l28eRGpDpU3Dm3Hp4q/N+VtLTmyj4EUN088XZWQDW/hzj6sYRDXeOFsaAODKvpg==",
+          "dev": true,
+          "requires": {
+            "es-to-primitive": "^1.2.1",
+            "function-bind": "^1.1.1",
+            "has": "^1.0.3",
+            "has-symbols": "^1.0.1",
+            "is-callable": "^1.1.5",
+            "is-regex": "^1.0.5",
+            "object-inspect": "^1.7.0",
+            "object-keys": "^1.1.1",
+            "object.assign": "^4.1.0",
+            "string.prototype.trimleft": "^2.1.1",
+            "string.prototype.trimright": "^2.1.1"
+          }
+        },
+        "es-to-primitive": {
+          "version": "1.2.1",
+          "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.2.1.tgz",
+          "integrity": "sha512-QCOllgZJtaUo9miYBcLChTUaHNjJF3PYs1VidD7AwiEj1kYxKeQTctLAezAOH5ZKRH0g2IgPn6KwB4IT8iRpvA==",
+          "dev": true,
+          "requires": {
+            "is-callable": "^1.1.4",
+            "is-date-object": "^1.0.1",
+            "is-symbol": "^1.0.2"
+          }
+        },
+        "is-callable": {
+          "version": "1.1.5",
+          "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.1.5.tgz",
+          "integrity": "sha512-ESKv5sMCJB2jnHTWZ3O5itG+O128Hsus4K4Qh1h2/cgn2vbgnLSVqfV46AeJA9D5EeeLa9w81KUXMtn34zhX+Q==",
+          "dev": true
+        },
+        "is-regex": {
+          "version": "1.0.5",
+          "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.0.5.tgz",
+          "integrity": "sha512-vlKW17SNq44owv5AQR3Cq0bQPEb8+kF3UKZ2fiZNOWtztYE5i0CzCZxFDwO58qAOWtxdBRVO/V5Qin1wjCqFYQ==",
+          "dev": true,
+          "requires": {
+            "has": "^1.0.3"
+          }
+        },
+        "is-symbol": {
+          "version": "1.0.3",
+          "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.0.3.tgz",
+          "integrity": "sha512-OwijhaRSgqvhm/0ZdAcXNZt9lYdKFpcRDT5ULUuYXPoT794UNOdU+gpT6Rzo7b4V2HUl/op6GqY894AZwv9faQ==",
+          "dev": true,
+          "requires": {
+            "has-symbols": "^1.0.1"
+          }
+        },
+        "object-keys": {
+          "version": "1.1.1",
+          "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz",
+          "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==",
+          "dev": true
+        }
+      }
+    },
+    "string.prototype.trimright": {
+      "version": "2.1.2",
+      "resolved": "https://registry.npmjs.org/string.prototype.trimright/-/string.prototype.trimright-2.1.2.tgz",
+      "integrity": "sha512-ZNRQ7sY3KroTaYjRS6EbNiiHrOkjihL9aQE/8gfQ4DtAC/aEBRHFJa44OmoWxGGqXuJlfKkZW4WcXErGr+9ZFg==",
+      "dev": true,
+      "requires": {
+        "define-properties": "^1.1.3",
+        "es-abstract": "^1.17.5",
+        "string.prototype.trimend": "^1.0.0"
+      },
+      "dependencies": {
+        "es-abstract": {
+          "version": "1.17.5",
+          "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.17.5.tgz",
+          "integrity": "sha512-BR9auzDbySxOcfog0tLECW8l28eRGpDpU3Dm3Hp4q/N+VtLTmyj4EUN088XZWQDW/hzj6sYRDXeOFsaAODKvpg==",
+          "dev": true,
+          "requires": {
+            "es-to-primitive": "^1.2.1",
+            "function-bind": "^1.1.1",
+            "has": "^1.0.3",
+            "has-symbols": "^1.0.1",
+            "is-callable": "^1.1.5",
+            "is-regex": "^1.0.5",
+            "object-inspect": "^1.7.0",
+            "object-keys": "^1.1.1",
+            "object.assign": "^4.1.0",
+            "string.prototype.trimleft": "^2.1.1",
+            "string.prototype.trimright": "^2.1.1"
+          }
+        },
+        "es-to-primitive": {
+          "version": "1.2.1",
+          "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.2.1.tgz",
+          "integrity": "sha512-QCOllgZJtaUo9miYBcLChTUaHNjJF3PYs1VidD7AwiEj1kYxKeQTctLAezAOH5ZKRH0g2IgPn6KwB4IT8iRpvA==",
+          "dev": true,
+          "requires": {
+            "is-callable": "^1.1.4",
+            "is-date-object": "^1.0.1",
+            "is-symbol": "^1.0.2"
+          }
+        },
+        "is-callable": {
+          "version": "1.1.5",
+          "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.1.5.tgz",
+          "integrity": "sha512-ESKv5sMCJB2jnHTWZ3O5itG+O128Hsus4K4Qh1h2/cgn2vbgnLSVqfV46AeJA9D5EeeLa9w81KUXMtn34zhX+Q==",
+          "dev": true
+        },
+        "is-regex": {
+          "version": "1.0.5",
+          "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.0.5.tgz",
+          "integrity": "sha512-vlKW17SNq44owv5AQR3Cq0bQPEb8+kF3UKZ2fiZNOWtztYE5i0CzCZxFDwO58qAOWtxdBRVO/V5Qin1wjCqFYQ==",
+          "dev": true,
+          "requires": {
+            "has": "^1.0.3"
+          }
+        },
+        "is-symbol": {
+          "version": "1.0.3",
+          "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.0.3.tgz",
+          "integrity": "sha512-OwijhaRSgqvhm/0ZdAcXNZt9lYdKFpcRDT5ULUuYXPoT794UNOdU+gpT6Rzo7b4V2HUl/op6GqY894AZwv9faQ==",
+          "dev": true,
+          "requires": {
+            "has-symbols": "^1.0.1"
+          }
+        },
+        "object-keys": {
+          "version": "1.1.1",
+          "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz",
+          "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==",
+          "dev": true
+        }
+      }
+    },
+    "string.prototype.trimstart": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.1.tgz",
+      "integrity": "sha512-XxZn+QpvrBI1FOcg6dIpxUPgWCPuNXvMD72aaRaUQv1eD4e/Qy8i/hFTe0BUmD60p/QA6bh1avmuPTfNjqVWRw==",
+      "dev": true,
+      "requires": {
+        "define-properties": "^1.1.3",
+        "es-abstract": "^1.17.5"
+      },
+      "dependencies": {
+        "es-abstract": {
+          "version": "1.17.5",
+          "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.17.5.tgz",
+          "integrity": "sha512-BR9auzDbySxOcfog0tLECW8l28eRGpDpU3Dm3Hp4q/N+VtLTmyj4EUN088XZWQDW/hzj6sYRDXeOFsaAODKvpg==",
+          "dev": true,
+          "requires": {
+            "es-to-primitive": "^1.2.1",
+            "function-bind": "^1.1.1",
+            "has": "^1.0.3",
+            "has-symbols": "^1.0.1",
+            "is-callable": "^1.1.5",
+            "is-regex": "^1.0.5",
+            "object-inspect": "^1.7.0",
+            "object-keys": "^1.1.1",
+            "object.assign": "^4.1.0",
+            "string.prototype.trimleft": "^2.1.1",
+            "string.prototype.trimright": "^2.1.1"
+          }
+        },
+        "es-to-primitive": {
+          "version": "1.2.1",
+          "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.2.1.tgz",
+          "integrity": "sha512-QCOllgZJtaUo9miYBcLChTUaHNjJF3PYs1VidD7AwiEj1kYxKeQTctLAezAOH5ZKRH0g2IgPn6KwB4IT8iRpvA==",
+          "dev": true,
+          "requires": {
+            "is-callable": "^1.1.4",
+            "is-date-object": "^1.0.1",
+            "is-symbol": "^1.0.2"
+          }
+        },
+        "is-callable": {
+          "version": "1.1.5",
+          "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.1.5.tgz",
+          "integrity": "sha512-ESKv5sMCJB2jnHTWZ3O5itG+O128Hsus4K4Qh1h2/cgn2vbgnLSVqfV46AeJA9D5EeeLa9w81KUXMtn34zhX+Q==",
+          "dev": true
+        },
+        "is-regex": {
+          "version": "1.0.5",
+          "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.0.5.tgz",
+          "integrity": "sha512-vlKW17SNq44owv5AQR3Cq0bQPEb8+kF3UKZ2fiZNOWtztYE5i0CzCZxFDwO58qAOWtxdBRVO/V5Qin1wjCqFYQ==",
+          "dev": true,
+          "requires": {
+            "has": "^1.0.3"
+          }
+        },
+        "is-symbol": {
+          "version": "1.0.3",
+          "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.0.3.tgz",
+          "integrity": "sha512-OwijhaRSgqvhm/0ZdAcXNZt9lYdKFpcRDT5ULUuYXPoT794UNOdU+gpT6Rzo7b4V2HUl/op6GqY894AZwv9faQ==",
+          "dev": true,
+          "requires": {
+            "has-symbols": "^1.0.1"
+          }
+        },
+        "object-keys": {
+          "version": "1.1.1",
+          "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz",
+          "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==",
+          "dev": true
+        }
+      }
+    },
     "string_decoder": {
       "version": "1.1.1",
       "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz",
@@ -12572,9 +12867,9 @@
           "dev": true
         },
         "kind-of": {
-          "version": "6.0.2",
-          "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.2.tgz",
-          "integrity": "sha512-s5kLOcnH0XqDO+FvuaLX8DDjZ18CGFk7VygH40QoKPUQhW4e2rvM0rwUq0t8IQDOwYSeLK01U90OjzBTme2QqA==",
+          "version": "6.0.3",
+          "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz",
+          "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==",
           "dev": true
         },
         "micromatch": {
@@ -12645,6 +12940,66 @@
         }
       }
     },
+    "svgo": {
+      "version": "1.3.2",
+      "resolved": "https://registry.npmjs.org/svgo/-/svgo-1.3.2.tgz",
+      "integrity": "sha512-yhy/sQYxR5BkC98CY7o31VGsg014AKLEPxdfhora76l36hD9Rdy5NZA/Ocn6yayNPgSamYdtX2rFJdcv07AYVw==",
+      "dev": true,
+      "requires": {
+        "chalk": "^2.4.1",
+        "coa": "^2.0.2",
+        "css-select": "^2.0.0",
+        "css-select-base-adapter": "^0.1.1",
+        "css-tree": "1.0.0-alpha.37",
+        "csso": "^4.0.2",
+        "js-yaml": "^3.13.1",
+        "mkdirp": "~0.5.1",
+        "object.values": "^1.1.0",
+        "sax": "~1.2.4",
+        "stable": "^0.1.8",
+        "unquote": "~1.1.1",
+        "util.promisify": "~1.0.0"
+      },
+      "dependencies": {
+        "css-select": {
+          "version": "2.1.0",
+          "resolved": "https://registry.npmjs.org/css-select/-/css-select-2.1.0.tgz",
+          "integrity": "sha512-Dqk7LQKpwLoH3VovzZnkzegqNSuAziQyNZUcrdDM401iY+R5NkGBXGmtO05/yaXQziALuPogeG0b7UAgjnTJTQ==",
+          "dev": true,
+          "requires": {
+            "boolbase": "^1.0.0",
+            "css-what": "^3.2.1",
+            "domutils": "^1.7.0",
+            "nth-check": "^1.0.2"
+          }
+        },
+        "css-what": {
+          "version": "3.2.1",
+          "resolved": "https://registry.npmjs.org/css-what/-/css-what-3.2.1.tgz",
+          "integrity": "sha512-WwOrosiQTvyms+Ti5ZC5vGEK0Vod3FTt1ca+payZqvKuGJF+dq7bG63DstxtN0dpm6FxY27a/zS3Wten+gEtGw==",
+          "dev": true
+        },
+        "domutils": {
+          "version": "1.7.0",
+          "resolved": "https://registry.npmjs.org/domutils/-/domutils-1.7.0.tgz",
+          "integrity": "sha512-Lgd2XcJ/NjEw+7tFvfKxOzCYKZsdct5lczQ2ZaQY8Djz7pfAD3Gbp8ySJWtreII/vDlMVmxwa6pHmdxIYgttDg==",
+          "dev": true,
+          "requires": {
+            "dom-serializer": "0",
+            "domelementtype": "1"
+          }
+        },
+        "nth-check": {
+          "version": "1.0.2",
+          "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-1.0.2.tgz",
+          "integrity": "sha512-WeBOdju8SnzPN5vTUJYxYUxLeXpCaVP5i5e0LF8fg7WORF2Wd7wFX/pk0tYZk7s8T+J7VLy0Da6J1+wCT0AtHg==",
+          "dev": true,
+          "requires": {
+            "boolbase": "~1.0.0"
+          }
+        }
+      }
+    },
     "symbol-observable": {
       "version": "1.2.0",
       "resolved": "https://registry.npmjs.org/symbol-observable/-/symbol-observable-1.2.0.tgz",
@@ -13041,6 +13396,12 @@
       "integrity": "sha1-sr9O6FFKrmFltIF4KdIbLvSZBOw=",
       "dev": true
     },
+    "unquote": {
+      "version": "1.1.1",
+      "resolved": "https://registry.npmjs.org/unquote/-/unquote-1.1.1.tgz",
+      "integrity": "sha1-j97XMk7G6IoP+LkF58CYzcCG1UQ=",
+      "dev": true
+    },
     "unset-value": {
       "version": "1.0.0",
       "resolved": "https://registry.npmjs.org/unset-value/-/unset-value-1.0.0.tgz",
diff --git a/client/src/components/Settings/Dns/index.js b/client/src/components/Settings/Dns/index.js
index 85a4bcf5..8eb6e5a4 100644
--- a/client/src/components/Settings/Dns/index.js
+++ b/client/src/components/Settings/Dns/index.js
@@ -33,16 +33,16 @@ class Dns extends Component {
                 {isDataLoading ?
                     <Loading /> :
                     <Fragment>
-                        <Config
-                            dnsConfig={dnsConfig}
-                            setDnsConfig={setDnsConfig}
-                        />
                         <Upstream
                             processingTestUpstream={settings.processingTestUpstream}
                             testUpstream={testUpstream}
                             dnsConfig={dnsConfig}
                             setDnsConfig={setDnsConfig}
                         />
+                        <Config
+                            dnsConfig={dnsConfig}
+                            setDnsConfig={setDnsConfig}
+                        />
                         <Access access={access} setAccessList={setAccessList} />
                     </Fragment>}
             </Fragment>

From 80c3112ab3cf28c75fc7ededf8b917915c32d538 Mon Sep 17 00:00:00 2001
From: Simon Zolin <s.zolin@adguard.com>
Date: Fri, 24 Apr 2020 16:54:37 +0300
Subject: [PATCH 29/37] * client: router guide: add more info

---
 client/src/__locales/en.json      | 1 +
 client/src/components/ui/Guide.js | 3 +++
 2 files changed, 4 insertions(+)

diff --git a/client/src/__locales/en.json b/client/src/__locales/en.json
index 71d9daaf..ffbadfbd 100644
--- a/client/src/__locales/en.json
+++ b/client/src/__locales/en.json
@@ -257,6 +257,7 @@
     "install_devices_router_list_1": "Open the preferences for your router. Usually, you can access it from your browser via a URL (like http://192.168.0.1/ or http://192.168.1.1/). You may be asked to enter the password. If you don't remember it, you can often reset the password by pressing a button on the router itself. Some routers require a specific application, which in that case should be already installed on your computer/phone.",
     "install_devices_router_list_2": "Find the DHCP/DNS settings. Look for the DNS letters next to a field which allows two or three sets of numbers, each broken into four groups of one to three digits.",
     "install_devices_router_list_3": "Enter your AdGuard Home server addresses there.",
+    "install_devices_router_list_4": "You can't set a custom DNS server on some types of routers. In this case it may help if you set up AdGuard Home as a DHCP server. Otherwise, you should search for the manual on how to customize DNS servers for your particular router model.",
     "install_devices_windows_list_1": "Open Control Panel through Start menu or Windows search.",
     "install_devices_windows_list_2": "Go to Network and Internet category and then to Network and Sharing Center.",
     "install_devices_windows_list_3": "On the left side of the screen find Change adapter settings and click on it.",
diff --git a/client/src/components/ui/Guide.js b/client/src/components/ui/Guide.js
index bc18366a..cbb0035b 100644
--- a/client/src/components/ui/Guide.js
+++ b/client/src/components/ui/Guide.js
@@ -34,6 +34,9 @@ const Guide = (props) => {
                             <li>
                                 <Trans>install_devices_router_list_3</Trans>
                             </li>
+                            <li>
+                                <Trans>install_devices_router_list_4</Trans>
+                            </li>
                         </ol>
                     </div>
                 </div>

From 08b033dd04bfe7a5bc9cd32fd2d5fe588e13d013 Mon Sep 17 00:00:00 2001
From: Andrey Meshkov <am@adguard.com>
Date: Sun, 26 Apr 2020 19:16:10 +0300
Subject: [PATCH 30/37] *(dnsforward): upgrade dnsproxy to v0.27.0

---
 go.mod | 2 +-
 go.sum | 4 ++--
 2 files changed, 3 insertions(+), 3 deletions(-)

diff --git a/go.mod b/go.mod
index 8a39908f..f2d7aa1c 100644
--- a/go.mod
+++ b/go.mod
@@ -3,7 +3,7 @@ module github.com/AdguardTeam/AdGuardHome
 go 1.14
 
 require (
-	github.com/AdguardTeam/dnsproxy v0.26.3
+	github.com/AdguardTeam/dnsproxy v0.27.0
 	github.com/AdguardTeam/golibs v0.4.2
 	github.com/AdguardTeam/urlfilter v0.10.0
 	github.com/NYTimes/gziphandler v1.1.1
diff --git a/go.sum b/go.sum
index be1e5861..d5fef41f 100644
--- a/go.sum
+++ b/go.sum
@@ -1,5 +1,5 @@
-github.com/AdguardTeam/dnsproxy v0.26.3 h1:SHvJS3xnIPkJwHqP5tQa2PTqesHgoUJeeWZQw2seXiY=
-github.com/AdguardTeam/dnsproxy v0.26.3/go.mod h1:hOYFV9TW+pd5XKYz7KZf2FFD8SvSPqjyGTxUae86s58=
+github.com/AdguardTeam/dnsproxy v0.27.0 h1:Xl8v3Fwm4t/eFHpJemEUUh+GHb7getPLZvdpcX6fJ20=
+github.com/AdguardTeam/dnsproxy v0.27.0/go.mod h1:hOYFV9TW+pd5XKYz7KZf2FFD8SvSPqjyGTxUae86s58=
 github.com/AdguardTeam/golibs v0.4.0 h1:4VX6LoOqFe9p9Gf55BeD8BvJD6M6RDYmgEiHrENE9KU=
 github.com/AdguardTeam/golibs v0.4.0/go.mod h1:skKsDKIBB7kkFflLJBpfGX+G8QFTx0WKUzB6TIgtUj4=
 github.com/AdguardTeam/golibs v0.4.2 h1:7M28oTZFoFwNmp8eGPb3ImmYbxGaJLyQXeIFVHjME0o=

From 14ffd1a3f52049931b21abe4ebce5bd22ece715b Mon Sep 17 00:00:00 2001
From: Simon Zolin <s.zolin@adguard.com>
Date: Mon, 27 Apr 2020 13:21:16 +0300
Subject: [PATCH 31/37] Merge: - blocked-services: settings were reset on
 restart Close #1624

Squashed commit of the following:

commit eecc91ca1f9d062c27702a9c07da74da673bef05
Merge: ec53dbeb 26f78dcc
Author: Simon Zolin <s.zolin@adguard.com>
Date:   Mon Apr 27 13:15:58 2020 +0300

    Merge remote-tracking branch 'origin/master' into fix-blocked-svcs

commit ec53dbebdc2fbd2ff94f939d2bd8fb07b9dd1bc8
Author: Simon Zolin <s.zolin@adguard.com>
Date:   Mon Apr 27 12:58:50 2020 +0300

    minor

commit 1e3c20ed02151965ebaca55ac4f25a951a772062
Author: Simon Zolin <s.zolin@adguard.com>
Date:   Mon Apr 27 12:53:37 2020 +0300

    - blocked-services: settings were reset on restart

    broken by:
     0789e4b20dea132f5d66ee18f7d1fbd05c10b014
     * refactor: move blocked-services functions to dnsfilter
---
 dnsfilter/dnsfilter.go | 7 +++++--
 home/home.go           | 5 +++++
 2 files changed, 10 insertions(+), 2 deletions(-)

diff --git a/dnsfilter/dnsfilter.go b/dnsfilter/dnsfilter.go
index 60a32d39..82b56187 100644
--- a/dnsfilter/dnsfilter.go
+++ b/dnsfilter/dnsfilter.go
@@ -628,6 +628,11 @@ func makeResult(rule rules.Rule, reason Reason) Result {
 	return res
 }
 
+// InitModule() - manually initialize blocked services map
+func InitModule() {
+	initBlockedServices()
+}
+
 // New creates properly initialized DNS Filter that is ready to be used
 func New(c *Config, blockFilters []Filter) *Dnsfilter {
 
@@ -677,8 +682,6 @@ func New(c *Config, blockFilters []Filter) *Dnsfilter {
 	}
 	d.BlockedServices = bsvcs
 
-	initBlockedServices()
-
 	if blockFilters != nil {
 		err := d.initFiltering(nil, blockFilters)
 		if err != nil {
diff --git a/home/home.go b/home/home.go
index 2340ca6c..ebc25af4 100644
--- a/home/home.go
+++ b/home/home.go
@@ -208,6 +208,11 @@ func run(args options) {
 		}
 	}
 
+	// 'clients' module uses 'dnsfilter' module's static data (dnsfilter.BlockedSvcKnown()),
+	//  so we have to initialize dnsfilter's static data first,
+	//  but also avoid relying on automatic Go init() function
+	dnsfilter.InitModule()
+
 	config.DHCP.WorkDir = Context.workDir
 	config.DHCP.HTTPRegister = httpRegister
 	config.DHCP.ConfigModified = onConfigModified

From c0ebf9e793dea3b5a9c1cbf1627839ca56ea7497 Mon Sep 17 00:00:00 2001
From: Andrey Meshkov <am@adguard.com>
Date: Mon, 27 Apr 2020 16:18:35 +0300
Subject: [PATCH 32/37] *: more badges to the god of badges

---
 README.md | 7 +++++++
 1 file changed, 7 insertions(+)

diff --git a/README.md b/README.md
index 48d78b9a..9cf5953e 100644
--- a/README.md
+++ b/README.md
@@ -26,12 +26,19 @@
     <a href="https://golangci.com/r/github.com/AdguardTeam/AdGuardHome">
       <img src="https://golangci.com/badges/github.com/AdguardTeam/AdGuardHome.svg" alt="GolangCI" />
     </a>
+    <br />
     <a href="https://github.com/AdguardTeam/AdGuardHome/releases">
         <img src="https://img.shields.io/github/release/AdguardTeam/AdGuardHome/all.svg" alt="Latest release" />
     </a>
     <a href="https://snapcraft.io/adguard-home">
         <img alt="adguard-home" src="https://snapcraft.io/adguard-home/badge.svg" />
     </a>
+    <a href="https://hub.docker.com/r/adguard/adguardhome">
+        <img alt="Docker Pulls" src="https://img.shields.io/docker/pulls/adguard/adguardhome.svg?maxAge=604800" />
+    </a>
+    <a href="https://hub.docker.com/r/adguard/adguardhome">
+        <img alt="Docker Stars" src="https://img.shields.io/docker/stars/adguard/adguardhome.svg?maxAge=604800" />
+    </a>
 </p>
 
 <br />

From b33653ec48a613df24d1768aab56a5c505affd33 Mon Sep 17 00:00:00 2001
From: Simon Zolin <s.zolin@adguard.com>
Date: Mon, 27 Apr 2020 17:24:55 +0300
Subject: [PATCH 33/37] + rewrites: support deeper level wildcards - select the
 more specific one

---
 dnsfilter/dnsfilter_test.go |  90 -------------------------
 dnsfilter/rewrites.go       |  24 +++++--
 dnsfilter/rewrites_test.go  | 127 ++++++++++++++++++++++++++++++++++++
 3 files changed, 145 insertions(+), 96 deletions(-)
 create mode 100644 dnsfilter/rewrites_test.go

diff --git a/dnsfilter/dnsfilter_test.go b/dnsfilter/dnsfilter_test.go
index 532e1c3c..6515b53d 100644
--- a/dnsfilter/dnsfilter_test.go
+++ b/dnsfilter/dnsfilter_test.go
@@ -532,96 +532,6 @@ func TestClientSettings(t *testing.T) {
 	assert.True(t, r.IsFiltered && r.Reason == FilteredBlockedService)
 }
 
-func TestRewrites(t *testing.T) {
-	d := Dnsfilter{}
-	// CNAME, A, AAAA
-	d.Rewrites = []RewriteEntry{
-		RewriteEntry{"somecname", "somehost.com", 0, nil},
-		RewriteEntry{"somehost.com", "0.0.0.0", 0, nil},
-
-		RewriteEntry{"host.com", "1.2.3.4", 0, nil},
-		RewriteEntry{"host.com", "1.2.3.5", 0, nil},
-		RewriteEntry{"host.com", "1:2:3::4", 0, nil},
-		RewriteEntry{"www.host.com", "host.com", 0, nil},
-	}
-	d.prepareRewrites()
-	r := d.processRewrites("host2.com")
-	assert.Equal(t, NotFilteredNotFound, r.Reason)
-
-	r = d.processRewrites("www.host.com")
-	assert.Equal(t, ReasonRewrite, r.Reason)
-	assert.Equal(t, "host.com", r.CanonName)
-	assert.True(t, len(r.IPList) == 3)
-	assert.True(t, r.IPList[0].Equal(net.ParseIP("1.2.3.4")))
-	assert.True(t, r.IPList[1].Equal(net.ParseIP("1.2.3.5")))
-	assert.True(t, r.IPList[2].Equal(net.ParseIP("1:2:3::4")))
-
-	// wildcard
-	d.Rewrites = []RewriteEntry{
-		RewriteEntry{"host.com", "1.2.3.4", 0, nil},
-		RewriteEntry{"*.host.com", "1.2.3.5", 0, nil},
-	}
-	d.prepareRewrites()
-	r = d.processRewrites("host.com")
-	assert.Equal(t, ReasonRewrite, r.Reason)
-	assert.True(t, r.IPList[0].Equal(net.ParseIP("1.2.3.4")))
-
-	r = d.processRewrites("www.host.com")
-	assert.Equal(t, ReasonRewrite, r.Reason)
-	assert.True(t, r.IPList[0].Equal(net.ParseIP("1.2.3.5")))
-
-	r = d.processRewrites("www.host2.com")
-	assert.Equal(t, NotFilteredNotFound, r.Reason)
-
-	// override a wildcard
-	d.Rewrites = []RewriteEntry{
-		RewriteEntry{"a.host.com", "1.2.3.4", 0, nil},
-		RewriteEntry{"*.host.com", "1.2.3.5", 0, nil},
-	}
-	d.prepareRewrites()
-	r = d.processRewrites("a.host.com")
-	assert.Equal(t, ReasonRewrite, r.Reason)
-	assert.True(t, len(r.IPList) == 1)
-	assert.True(t, r.IPList[0].Equal(net.ParseIP("1.2.3.4")))
-
-	// wildcard + CNAME
-	d.Rewrites = []RewriteEntry{
-		RewriteEntry{"host.com", "1.2.3.4", 0, nil},
-		RewriteEntry{"*.host.com", "host.com", 0, nil},
-	}
-	d.prepareRewrites()
-	r = d.processRewrites("www.host.com")
-	assert.Equal(t, ReasonRewrite, r.Reason)
-	assert.Equal(t, "host.com", r.CanonName)
-	assert.True(t, r.IPList[0].Equal(net.ParseIP("1.2.3.4")))
-
-	// 2 CNAMEs
-	d.Rewrites = []RewriteEntry{
-		RewriteEntry{"b.host.com", "a.host.com", 0, nil},
-		RewriteEntry{"a.host.com", "host.com", 0, nil},
-		RewriteEntry{"host.com", "1.2.3.4", 0, nil},
-	}
-	d.prepareRewrites()
-	r = d.processRewrites("b.host.com")
-	assert.Equal(t, ReasonRewrite, r.Reason)
-	assert.Equal(t, "host.com", r.CanonName)
-	assert.True(t, len(r.IPList) == 1)
-	assert.True(t, r.IPList[0].Equal(net.ParseIP("1.2.3.4")))
-
-	// 2 CNAMEs + wildcard
-	d.Rewrites = []RewriteEntry{
-		RewriteEntry{"b.host.com", "a.host.com", 0, nil},
-		RewriteEntry{"a.host.com", "x.somehost.com", 0, nil},
-		RewriteEntry{"*.somehost.com", "1.2.3.4", 0, nil},
-	}
-	d.prepareRewrites()
-	r = d.processRewrites("b.host.com")
-	assert.Equal(t, ReasonRewrite, r.Reason)
-	assert.Equal(t, "x.somehost.com", r.CanonName)
-	assert.True(t, len(r.IPList) == 1)
-	assert.True(t, r.IPList[0].Equal(net.ParseIP("1.2.3.4")))
-}
-
 func prepareTestDir() string {
 	const dir = "./agh-test"
 	_ = os.RemoveAll(dir)
diff --git a/dnsfilter/rewrites.go b/dnsfilter/rewrites.go
index 029d3332..cbc02a16 100644
--- a/dnsfilter/rewrites.go
+++ b/dnsfilter/rewrites.go
@@ -42,7 +42,10 @@ func (a rewritesArray) Len() int { return len(a) }
 
 func (a rewritesArray) Swap(i, j int) { a[i], a[j] = a[j], a[i] }
 
-// Priority: CNAME, A/AAAA;  exact, wildcard.
+// Priority:
+//  . CNAME > A/AAAA;
+//  . exact > wildcard;
+//  . higher level wildcard > lower level wildcard
 func (a rewritesArray) Less(i, j int) bool {
 	if a[i].Type == dns.TypeCNAME && a[j].Type != dns.TypeCNAME {
 		return false
@@ -50,13 +53,18 @@ func (a rewritesArray) Less(i, j int) bool {
 		return true
 	}
 
-	if isWildcard(a[i].Domain) && !isWildcard(a[j].Domain) {
-		return false
-	} else if !isWildcard(a[i].Domain) && isWildcard(a[j].Domain) {
-		return true
+	if isWildcard(a[i].Domain) {
+		if !isWildcard(a[j].Domain) {
+			return false
+		}
+	} else {
+		if isWildcard(a[j].Domain) {
+			return true
+		}
 	}
 
-	return i < j
+	// both are wildcards
+	return len(a[i].Domain) > len(a[j].Domain)
 }
 
 // Prepare entry for use
@@ -86,6 +94,7 @@ func (d *Dnsfilter) prepareRewrites() {
 // Get the list of matched rewrite entries.
 // Priority: CNAME, A/AAAA;  exact, wildcard.
 // If matched exactly, don't return wildcard entries.
+// If matched by several wildcards, select the more specific one
 func findRewrites(a []RewriteEntry, host string) []RewriteEntry {
 	rr := rewritesArray{}
 	for _, r := range a {
@@ -111,7 +120,10 @@ func findRewrites(a []RewriteEntry, host string) []RewriteEntry {
 				break
 			}
 		}
+	} else {
+		rr = rr[:1]
 	}
+
 	return rr
 }
 
diff --git a/dnsfilter/rewrites_test.go b/dnsfilter/rewrites_test.go
new file mode 100644
index 00000000..6da3e0f9
--- /dev/null
+++ b/dnsfilter/rewrites_test.go
@@ -0,0 +1,127 @@
+package dnsfilter
+
+import (
+	"net"
+	"testing"
+
+	"github.com/stretchr/testify/assert"
+)
+
+func TestRewrites(t *testing.T) {
+	d := Dnsfilter{}
+	// CNAME, A, AAAA
+	d.Rewrites = []RewriteEntry{
+		RewriteEntry{"somecname", "somehost.com", 0, nil},
+		RewriteEntry{"somehost.com", "0.0.0.0", 0, nil},
+
+		RewriteEntry{"host.com", "1.2.3.4", 0, nil},
+		RewriteEntry{"host.com", "1.2.3.5", 0, nil},
+		RewriteEntry{"host.com", "1:2:3::4", 0, nil},
+		RewriteEntry{"www.host.com", "host.com", 0, nil},
+	}
+	d.prepareRewrites()
+	r := d.processRewrites("host2.com")
+	assert.Equal(t, NotFilteredNotFound, r.Reason)
+
+	r = d.processRewrites("www.host.com")
+	assert.Equal(t, ReasonRewrite, r.Reason)
+	assert.Equal(t, "host.com", r.CanonName)
+	assert.True(t, len(r.IPList) == 3)
+	assert.True(t, r.IPList[0].Equal(net.ParseIP("1.2.3.4")))
+	assert.True(t, r.IPList[1].Equal(net.ParseIP("1.2.3.5")))
+	assert.True(t, r.IPList[2].Equal(net.ParseIP("1:2:3::4")))
+
+	// wildcard
+	d.Rewrites = []RewriteEntry{
+		RewriteEntry{"host.com", "1.2.3.4", 0, nil},
+		RewriteEntry{"*.host.com", "1.2.3.5", 0, nil},
+	}
+	d.prepareRewrites()
+	r = d.processRewrites("host.com")
+	assert.Equal(t, ReasonRewrite, r.Reason)
+	assert.True(t, r.IPList[0].Equal(net.ParseIP("1.2.3.4")))
+
+	r = d.processRewrites("www.host.com")
+	assert.Equal(t, ReasonRewrite, r.Reason)
+	assert.True(t, r.IPList[0].Equal(net.ParseIP("1.2.3.5")))
+
+	r = d.processRewrites("www.host2.com")
+	assert.Equal(t, NotFilteredNotFound, r.Reason)
+
+	// override a wildcard
+	d.Rewrites = []RewriteEntry{
+		RewriteEntry{"a.host.com", "1.2.3.4", 0, nil},
+		RewriteEntry{"*.host.com", "1.2.3.5", 0, nil},
+	}
+	d.prepareRewrites()
+	r = d.processRewrites("a.host.com")
+	assert.Equal(t, ReasonRewrite, r.Reason)
+	assert.True(t, len(r.IPList) == 1)
+	assert.True(t, r.IPList[0].Equal(net.ParseIP("1.2.3.4")))
+
+	// wildcard + CNAME
+	d.Rewrites = []RewriteEntry{
+		RewriteEntry{"host.com", "1.2.3.4", 0, nil},
+		RewriteEntry{"*.host.com", "host.com", 0, nil},
+	}
+	d.prepareRewrites()
+	r = d.processRewrites("www.host.com")
+	assert.Equal(t, ReasonRewrite, r.Reason)
+	assert.Equal(t, "host.com", r.CanonName)
+	assert.True(t, r.IPList[0].Equal(net.ParseIP("1.2.3.4")))
+
+	// 2 CNAMEs
+	d.Rewrites = []RewriteEntry{
+		RewriteEntry{"b.host.com", "a.host.com", 0, nil},
+		RewriteEntry{"a.host.com", "host.com", 0, nil},
+		RewriteEntry{"host.com", "1.2.3.4", 0, nil},
+	}
+	d.prepareRewrites()
+	r = d.processRewrites("b.host.com")
+	assert.Equal(t, ReasonRewrite, r.Reason)
+	assert.Equal(t, "host.com", r.CanonName)
+	assert.True(t, len(r.IPList) == 1)
+	assert.True(t, r.IPList[0].Equal(net.ParseIP("1.2.3.4")))
+
+	// 2 CNAMEs + wildcard
+	d.Rewrites = []RewriteEntry{
+		RewriteEntry{"b.host.com", "a.host.com", 0, nil},
+		RewriteEntry{"a.host.com", "x.somehost.com", 0, nil},
+		RewriteEntry{"*.somehost.com", "1.2.3.4", 0, nil},
+	}
+	d.prepareRewrites()
+	r = d.processRewrites("b.host.com")
+	assert.Equal(t, ReasonRewrite, r.Reason)
+	assert.Equal(t, "x.somehost.com", r.CanonName)
+	assert.True(t, len(r.IPList) == 1)
+	assert.True(t, r.IPList[0].Equal(net.ParseIP("1.2.3.4")))
+}
+
+func TestRewritesLevels(t *testing.T) {
+	d := Dnsfilter{}
+	// exact host, wildcard L2, wildcard L3
+	d.Rewrites = []RewriteEntry{
+		RewriteEntry{"host.com", "1.1.1.1", 0, nil},
+		RewriteEntry{"*.host.com", "2.2.2.2", 0, nil},
+		RewriteEntry{"*.sub.host.com", "3.3.3.3", 0, nil},
+	}
+	d.prepareRewrites()
+
+	// match exact
+	r := d.processRewrites("host.com")
+	assert.Equal(t, ReasonRewrite, r.Reason)
+	assert.Equal(t, 1, len(r.IPList))
+	assert.Equal(t, "1.1.1.1", r.IPList[0].String())
+
+	// match L2
+	r = d.processRewrites("sub.host.com")
+	assert.Equal(t, ReasonRewrite, r.Reason)
+	assert.Equal(t, 1, len(r.IPList))
+	assert.Equal(t, "2.2.2.2", r.IPList[0].String())
+
+	// match L3
+	r = d.processRewrites("my.sub.host.com")
+	assert.Equal(t, ReasonRewrite, r.Reason)
+	assert.Equal(t, 1, len(r.IPList))
+	assert.Equal(t, "3.3.3.3", r.IPList[0].String())
+}

From 2837502a7b379b261004d691460792dc61f61492 Mon Sep 17 00:00:00 2001
From: Artem Baskal <a.baskal@adguard.com>
Date: Wed, 29 Apr 2020 19:09:00 +0300
Subject: [PATCH 34/37] Merge: + client: Make default table height 100%

Squashed commit of the following:

commit d6b07ae070b5ec826dbe3e226e326f9d52b8c7d1
Author: ArtemBaskal <a.baskal@adguard.com>
Date:   Wed Apr 29 16:57:22 2020 +0300

    Limit dasboard tables height

commit 6b42f7a1e3cb82e1b792dd352717a3ffa9566b4b
Author: ArtemBaskal <a.baskal@adguard.com>
Date:   Wed Apr 29 16:44:08 2020 +0300

    + client: Make default table height 100%
---
 client/package-lock.json                          | 15 ++++++++++-----
 client/src/components/Dashboard/BlockedDomains.js |  2 +-
 client/src/components/Dashboard/Clients.js        |  2 +-
 client/src/components/Dashboard/QueriedDomains.js |  2 +-
 client/src/components/ui/Card.css                 |  5 +++++
 5 files changed, 18 insertions(+), 8 deletions(-)

diff --git a/client/package-lock.json b/client/package-lock.json
index 6c73efd3..874c9e1e 100644
--- a/client/package-lock.json
+++ b/client/package-lock.json
@@ -2259,7 +2259,8 @@
         },
         "kind-of": {
           "version": "6.0.2",
-          "resolved": "",
+          "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.2.tgz",
+          "integrity": "sha512-s5kLOcnH0XqDO+FvuaLX8DDjZ18CGFk7VygH40QoKPUQhW4e2rvM0rwUq0t8IQDOwYSeLK01U90OjzBTme2QqA==",
           "dev": true
         }
       }
@@ -3056,7 +3057,8 @@
         },
         "kind-of": {
           "version": "6.0.2",
-          "resolved": "",
+          "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.2.tgz",
+          "integrity": "sha512-s5kLOcnH0XqDO+FvuaLX8DDjZ18CGFk7VygH40QoKPUQhW4e2rvM0rwUq0t8IQDOwYSeLK01U90OjzBTme2QqA==",
           "dev": true
         },
         "micromatch": {
@@ -3688,7 +3690,8 @@
         },
         "minimist": {
           "version": "1.2.0",
-          "resolved": "",
+          "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz",
+          "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=",
           "dev": true
         },
         "normalize-path": {
@@ -5288,7 +5291,8 @@
         },
         "kind-of": {
           "version": "6.0.2",
-          "resolved": "",
+          "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.2.tgz",
+          "integrity": "sha512-s5kLOcnH0XqDO+FvuaLX8DDjZ18CGFk7VygH40QoKPUQhW4e2rvM0rwUq0t8IQDOwYSeLK01U90OjzBTme2QqA==",
           "dev": true
         }
       }
@@ -11525,7 +11529,8 @@
         },
         "kind-of": {
           "version": "6.0.2",
-          "resolved": "",
+          "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.2.tgz",
+          "integrity": "sha512-s5kLOcnH0XqDO+FvuaLX8DDjZ18CGFk7VygH40QoKPUQhW4e2rvM0rwUq0t8IQDOwYSeLK01U90OjzBTme2QqA==",
           "dev": true
         }
       }
diff --git a/client/src/components/Dashboard/BlockedDomains.js b/client/src/components/Dashboard/BlockedDomains.js
index 3823d52d..0bb61f4c 100644
--- a/client/src/components/Dashboard/BlockedDomains.js
+++ b/client/src/components/Dashboard/BlockedDomains.js
@@ -58,7 +58,7 @@ const BlockedDomains = ({
                 noDataText={t('no_domains_found')}
                 minRows={6}
                 defaultPageSize={100}
-                className="-highlight card-table-overflow stats__table"
+                className="-highlight card-table-overflow--limited stats__table"
             />
         </Card>
     );
diff --git a/client/src/components/Dashboard/Clients.js b/client/src/components/Dashboard/Clients.js
index 0a9ec903..f0b73ffe 100644
--- a/client/src/components/Dashboard/Clients.js
+++ b/client/src/components/Dashboard/Clients.js
@@ -119,7 +119,7 @@ const Clients = ({
             noDataText={t('no_clients_found')}
             minRows={6}
             defaultPageSize={100}
-            className="-highlight card-table-overflow clients__table"
+            className="-highlight card-table-overflow--limited clients__table"
             getTrProps={(_state, rowInfo) => {
                 if (!rowInfo) {
                     return {};
diff --git a/client/src/components/Dashboard/QueriedDomains.js b/client/src/components/Dashboard/QueriedDomains.js
index 85c39cfb..8bc7674a 100644
--- a/client/src/components/Dashboard/QueriedDomains.js
+++ b/client/src/components/Dashboard/QueriedDomains.js
@@ -59,7 +59,7 @@ const QueriedDomains = ({
             noDataText={t('no_domains_found')}
             minRows={6}
             defaultPageSize={100}
-            className="-highlight card-table-overflow stats__table"
+            className="-highlight card-table-overflow--limited stats__table"
         />
     </Card>
 );
diff --git a/client/src/components/ui/Card.css b/client/src/components/ui/Card.css
index 577fa60a..e5840f7f 100644
--- a/client/src/components/ui/Card.css
+++ b/client/src/components/ui/Card.css
@@ -10,6 +10,11 @@
 }
 
 .card-table-overflow {
+    overflow-y: auto;
+    max-height: 100%;
+}
+
+.card-table-overflow--limited {
     overflow-y: auto;
     max-height: 280px;
 }

From efc69047a172074bf7ef5d1271f30d1eb0dba90c Mon Sep 17 00:00:00 2001
From: Simon Zolin <s.zolin@adguard.com>
Date: Thu, 30 Apr 2020 10:57:07 +0300
Subject: [PATCH 35/37] dnsproxy v0.27.1

---
 go.mod | 2 +-
 go.sum | 4 ++--
 2 files changed, 3 insertions(+), 3 deletions(-)

diff --git a/go.mod b/go.mod
index f2d7aa1c..85c12c02 100644
--- a/go.mod
+++ b/go.mod
@@ -3,7 +3,7 @@ module github.com/AdguardTeam/AdGuardHome
 go 1.14
 
 require (
-	github.com/AdguardTeam/dnsproxy v0.27.0
+	github.com/AdguardTeam/dnsproxy v0.27.1
 	github.com/AdguardTeam/golibs v0.4.2
 	github.com/AdguardTeam/urlfilter v0.10.0
 	github.com/NYTimes/gziphandler v1.1.1
diff --git a/go.sum b/go.sum
index d5fef41f..636d419e 100644
--- a/go.sum
+++ b/go.sum
@@ -1,5 +1,5 @@
-github.com/AdguardTeam/dnsproxy v0.27.0 h1:Xl8v3Fwm4t/eFHpJemEUUh+GHb7getPLZvdpcX6fJ20=
-github.com/AdguardTeam/dnsproxy v0.27.0/go.mod h1:hOYFV9TW+pd5XKYz7KZf2FFD8SvSPqjyGTxUae86s58=
+github.com/AdguardTeam/dnsproxy v0.27.1 h1:CQ3vtGSNbHNeYkxC6pALwugTSssP2MnsjdxkvVMzEp4=
+github.com/AdguardTeam/dnsproxy v0.27.1/go.mod h1:hOYFV9TW+pd5XKYz7KZf2FFD8SvSPqjyGTxUae86s58=
 github.com/AdguardTeam/golibs v0.4.0 h1:4VX6LoOqFe9p9Gf55BeD8BvJD6M6RDYmgEiHrENE9KU=
 github.com/AdguardTeam/golibs v0.4.0/go.mod h1:skKsDKIBB7kkFflLJBpfGX+G8QFTx0WKUzB6TIgtUj4=
 github.com/AdguardTeam/golibs v0.4.2 h1:7M28oTZFoFwNmp8eGPb3ImmYbxGaJLyQXeIFVHjME0o=

From 89920bc518b6d8bcc7bf754b0b768ad3bf49de9b Mon Sep 17 00:00:00 2001
From: Artem Baskal <a.baskal@adguard.com>
Date: Thu, 30 Apr 2020 18:59:14 +0300
Subject: [PATCH 36/37] Merge: + client: Hide dns autofix warning text

Squashed commit of the following:

commit e99192ac85400bcce09ca8d73ceef0224f003e0c
Author: ArtemBaskal <a.baskal@adguard.com>
Date:   Thu Apr 30 16:03:20 2020 +0300

    Show autofix warning conditionally

commit e5658fc3aaee449a49bee76063033dc62e61c722
Author: ArtemBaskal <a.baskal@adguard.com>
Date:   Wed Apr 29 19:18:02 2020 +0300

    + client: Add wiki link on instllation screen
---
 client/src/install/Setup/Settings.js | 79 ++++++++++++++--------------
 1 file changed, 40 insertions(+), 39 deletions(-)

diff --git a/client/src/install/Setup/Settings.js b/client/src/install/Setup/Settings.js
index 876aa05b..1c31143f 100644
--- a/client/src/install/Setup/Settings.js
+++ b/client/src/install/Setup/Settings.js
@@ -217,19 +217,19 @@ class Settings extends Component {
                         </div>
                         <div className="col-12">
                             {webStatus &&
-                                <div className="setup__error text-danger">
-                                    {webStatus}
-                                    {isWebFixAvailable &&
-                                        <button
-                                            type="button"
-                                            className="btn btn-secondary btn-sm ml-2"
-                                            onClick={() => this.handleAutofix('web')}
-                                        >
-                                            <Trans>fix</Trans>
-                                        </button>
-                                    }
-                                    <hr className="divider--small" />
-                                </div>
+                            <div className="setup__error text-danger">
+                                {webStatus}
+                                {isWebFixAvailable &&
+                                <button
+                                    type="button"
+                                    className="btn btn-secondary btn-sm ml-2"
+                                    onClick={() => this.handleAutofix('web')}
+                                >
+                                    <Trans>fix</Trans>
+                                </button>
+                                }
+                                <hr className="divider--small" />
+                            </div>
                             }
                         </div>
                     </div>
@@ -287,32 +287,33 @@ class Settings extends Component {
                         </div>
                         <div className="col-12">
                             {dnsStatus &&
-                                <Fragment>
-                                    <div className="setup__error text-danger">
-                                        {dnsStatus}
-                                        {isDnsFixAvailable &&
-                                            <button
-                                                type="button"
-                                                className="btn btn-secondary btn-sm ml-2"
-                                                onClick={() => this.handleAutofix('dns')}
-                                            >
-                                                <Trans>fix</Trans>
-                                            </button>
-                                        }
-                                    </div>
-                                    <div className="text-muted mb-2">
-                                        <p className="mb-1">
-                                            <Trans>autofix_warning_text</Trans>
-                                        </p>
-                                        <Trans components={[<li key="0">text</li>]}>
-                                            autofix_warning_list
-                                        </Trans>
-                                        <p className="mb-1">
-                                            <Trans>autofix_warning_result</Trans>
-                                        </p>
-                                    </div>
-                                    <hr className="divider--small" />
-                                </Fragment>
+                            <Fragment>
+                                <div className="setup__error text-danger">
+                                    {dnsStatus}
+                                    {isDnsFixAvailable &&
+                                    <button
+                                        type="button"
+                                        className="btn btn-secondary btn-sm ml-2"
+                                        onClick={() => this.handleAutofix('dns')}
+                                    >
+                                        <Trans>fix</Trans>
+                                    </button>
+                                    }
+                                </div>
+                                {isDnsFixAvailable &&
+                                <div className="text-muted mb-2">
+                                    <p className="mb-1">
+                                        <Trans>autofix_warning_text</Trans>
+                                    </p>
+                                    <Trans components={[<li key="0">text</li>]}>
+                                        autofix_warning_list
+                                    </Trans>
+                                    <p className="mb-1">
+                                        <Trans>autofix_warning_result</Trans>
+                                    </p>
+                                </div>}
+                                <hr className="divider--small" />
+                            </Fragment>
                             }
                         </div>
                     </div>

From f250fc16a2869ad96475c939ec54f062144a0516 Mon Sep 17 00:00:00 2001
From: Alan Pope <alan.pope@canonical.com>
Date: Fri, 1 May 2020 20:55:34 +0100
Subject: [PATCH 37/37] Correct architecture builds

The current syntax means it will build on one of the architectures to run on all. That would be useful if it was an arch-independent snap, such as a shell script. But this contains arch-specific binaries. Using the syntax here, will get four separate builds (one per arch) which is the desired outcome.
---
 snapcraft.yaml | 7 +++++--
 1 file changed, 5 insertions(+), 2 deletions(-)

diff --git a/snapcraft.yaml b/snapcraft.yaml
index cda2f165..c5cda82b 100644
--- a/snapcraft.yaml
+++ b/snapcraft.yaml
@@ -18,7 +18,10 @@ grade: stable
 confinement: strict
 
 architectures:
-  - build-on: [ amd64, i386, arm64, armhf ]
+  - build-on: amd64
+  - build-on: armhf
+  - build-on: i386
+  - build-on: arm64
 
 parts:
   adguard-home:
@@ -35,4 +38,4 @@ apps:
     command: AdGuardHome -w ${SNAP_DATA} --no-check-update
     plugs: [ network-bind ]
     daemon: simple
-    restart-condition: always
\ No newline at end of file
+    restart-condition: always