Merge: Installation wizard #685
* commit '79b0fac01a544e35207420fed78e1c2a63d428d8': * control: move /install handlers to a separate file + add technical document * app: move code for http server loop to a separate function * client: fixed getDefaultAddresses structure - client: npm audit fix * client: validate form on load * client: installation wizard additional checks * update openapi.yaml + service install: a post-install guide of what to do next * control: /install/configure: validate port number * control: /install/configure: reset configuration back to its current state on error + control: /install/*: test TCP port availability for DNS server + control: /install/check_config: Check and deactivate DNSStubListener * control: /install/configure: refactor + control: add /install/check_config handler * control: /install/get_addresses: don't check if ports are available + app: unix, windows: require root user on first launch * setRlimit(): move OS-specific code to separate files
This commit is contained in:
commit
69c5f175e8
|
@ -0,0 +1,327 @@
|
||||||
|
# AdGuard Home Technical Document
|
||||||
|
|
||||||
|
The document describes technical details and internal algorithms of AdGuard Home.
|
||||||
|
|
||||||
|
Contents:
|
||||||
|
* First startup
|
||||||
|
* Installation wizard
|
||||||
|
* "Get install settings" command
|
||||||
|
* "Check configuration" command
|
||||||
|
* Disable DNSStubListener
|
||||||
|
* "Apply configuration" command
|
||||||
|
* Enable DHCP server
|
||||||
|
* "Check DHCP" command
|
||||||
|
* "Enable DHCP" command
|
||||||
|
* Static IP check/set
|
||||||
|
|
||||||
|
|
||||||
|
## First startup
|
||||||
|
|
||||||
|
The first application startup is detected when there's no .yaml configuration file.
|
||||||
|
|
||||||
|
We check if the user is root, otherwise we fail with an error.
|
||||||
|
|
||||||
|
Web server is started up on port 3000 and automatically redirects requests to `/` to Installation wizard.
|
||||||
|
|
||||||
|
After Installation wizard steps are completed, we write configuration to a file and start normal operation.
|
||||||
|
|
||||||
|
|
||||||
|
## Installation wizard
|
||||||
|
|
||||||
|
This is the collection of UI screens that are shown to a user on first application startup.
|
||||||
|
|
||||||
|
The screens are:
|
||||||
|
|
||||||
|
1. Welcome
|
||||||
|
2. Set up network interface and listening ports for Web and DNS servers
|
||||||
|
3. Set up administrator username and password
|
||||||
|
4. Configuration complete
|
||||||
|
5. Done
|
||||||
|
|
||||||
|
Algorithm:
|
||||||
|
|
||||||
|
Screen 2:
|
||||||
|
* UI asks server for initial information and shows it
|
||||||
|
* User edits the default settings, clicks on "Next" button
|
||||||
|
* UI asks server to check new settings
|
||||||
|
* Server searches for the known issues
|
||||||
|
* UI shows information about the known issues and the means to fix them
|
||||||
|
* Server applies automatic fixes of the known issues on command from UI
|
||||||
|
|
||||||
|
Screen 3:
|
||||||
|
* UI asks server to apply the configuration
|
||||||
|
* Server restarts DNS server
|
||||||
|
|
||||||
|
|
||||||
|
### "Get install settings" command
|
||||||
|
|
||||||
|
Request:
|
||||||
|
|
||||||
|
GET /control/install/get_addresses
|
||||||
|
|
||||||
|
Response:
|
||||||
|
|
||||||
|
200 OK
|
||||||
|
|
||||||
|
{
|
||||||
|
"web_port":80,
|
||||||
|
"dns_port":53,
|
||||||
|
"interfaces":{
|
||||||
|
"enp2s0":{"name":"enp2s0","mtu":1500,"hardware_address":"","ip_addresses":["",""],"flags":"up|broadcast|multicast"},
|
||||||
|
"lo":{"name":"lo","mtu":65536,"hardware_address":"","ip_addresses":["127.0.0.1","::1"],"flags":"up|loopback"},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
If `interfaces.flags` doesn't contain `up` flag, UI must show `(Down)` status next to its IP address in interfaces selector.
|
||||||
|
|
||||||
|
|
||||||
|
### "Check configuration" command
|
||||||
|
|
||||||
|
Request:
|
||||||
|
|
||||||
|
POST /control/install/check_config
|
||||||
|
|
||||||
|
{
|
||||||
|
"web":{"port":80,"ip":"192.168.11.33"},
|
||||||
|
"dns":{"port":53,"ip":"127.0.0.1","autofix":false},
|
||||||
|
}
|
||||||
|
|
||||||
|
Server should check whether a port is available only in case it itself isn't already listening on that port.
|
||||||
|
|
||||||
|
Server replies on success:
|
||||||
|
|
||||||
|
200 OK
|
||||||
|
|
||||||
|
{
|
||||||
|
"web":{"status":""},
|
||||||
|
"dns":{"status":""},
|
||||||
|
}
|
||||||
|
|
||||||
|
Server replies on error:
|
||||||
|
|
||||||
|
200 OK
|
||||||
|
|
||||||
|
{
|
||||||
|
"web":{"status":"ERROR MESSAGE"},
|
||||||
|
"dns":{"status":"ERROR MESSAGE", "can_autofix": true|false},
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
### Disable DNSStubListener
|
||||||
|
|
||||||
|
On Linux, if 53 port is not available, server performs several additional checks to determine if the issue can be fixed automatically.
|
||||||
|
|
||||||
|
#### Phase 1
|
||||||
|
|
||||||
|
Request:
|
||||||
|
|
||||||
|
POST /control/install/check_config
|
||||||
|
|
||||||
|
{
|
||||||
|
"dns":{"port":53,"ip":"127.0.0.1","autofix":false}
|
||||||
|
}
|
||||||
|
|
||||||
|
Check if DNSStubListener is enabled:
|
||||||
|
|
||||||
|
systemctl is-enabled systemd-resolved
|
||||||
|
|
||||||
|
Check if DNSStubListener is active:
|
||||||
|
|
||||||
|
grep -E '#?DNSStubListener=yes' /etc/systemd/resolved.conf
|
||||||
|
|
||||||
|
If the issue can be fixed automatically, server replies with `"can_autofix":true`
|
||||||
|
|
||||||
|
200 OK
|
||||||
|
|
||||||
|
{
|
||||||
|
"dns":{"status":"ERROR MESSAGE", "can_autofix":true},
|
||||||
|
}
|
||||||
|
|
||||||
|
In this case UI shows "Fix" button next to error message.
|
||||||
|
|
||||||
|
#### Phase 2
|
||||||
|
|
||||||
|
If user clicks on "Fix" button, UI sends request to perform an automatic fix
|
||||||
|
|
||||||
|
POST /control/install/check_config
|
||||||
|
|
||||||
|
{
|
||||||
|
"dns":{"port":53,"ip":"127.0.0.1","autofix":true},
|
||||||
|
}
|
||||||
|
|
||||||
|
Deactivate (save backup as `resolved.conf.orig`) and stop DNSStubListener:
|
||||||
|
|
||||||
|
sed -r -i.orig 's/#?DNSStubListener=yes/DNSStubListener=no/g' /etc/systemd/resolved.conf
|
||||||
|
systemctl reload-or-restart systemd-resolved
|
||||||
|
|
||||||
|
Server replies:
|
||||||
|
|
||||||
|
200 OK
|
||||||
|
|
||||||
|
{
|
||||||
|
"dns":{"status":""},
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
### "Apply configuration" command
|
||||||
|
|
||||||
|
Request:
|
||||||
|
|
||||||
|
POST /control/install/configure
|
||||||
|
|
||||||
|
{
|
||||||
|
"web":{"port":80,"ip":"192.168.11.33"},
|
||||||
|
"dns":{"port":53,"ip":"127.0.0.1"},
|
||||||
|
"username":"u",
|
||||||
|
"password":"p",
|
||||||
|
}
|
||||||
|
|
||||||
|
Server checks the parameters once again, restarts DNS server, replies:
|
||||||
|
|
||||||
|
200 OK
|
||||||
|
|
||||||
|
On error, server responds with code 400 or 500. In this case UI should show error message and reset to the beginning.
|
||||||
|
|
||||||
|
400 Bad Request
|
||||||
|
|
||||||
|
ERROR MESSAGE
|
||||||
|
|
||||||
|
|
||||||
|
## Enable DHCP server
|
||||||
|
|
||||||
|
Algorithm:
|
||||||
|
|
||||||
|
* UI shows DHCP configuration screen with "Enabled DHCP" button disabled, and "Check DHCP" button enabled
|
||||||
|
* User clicks on "Check DHCP"; UI sends request to server
|
||||||
|
* Server may fail to detect whether there is another DHCP server working in the network. In this case UI shows a warning.
|
||||||
|
* Server may detect that a dynamic IP configuration is used for this interface. In this case UI shows a warning.
|
||||||
|
* UI enables "Enable DHCP" button
|
||||||
|
* User clicks on "Enable DHCP"; UI sends request to server
|
||||||
|
* Server sets a static IP (if necessary), enables DHCP server, sends the status back to UI
|
||||||
|
* UI shows the status
|
||||||
|
|
||||||
|
|
||||||
|
### "Check DHCP" command
|
||||||
|
|
||||||
|
Request:
|
||||||
|
|
||||||
|
POST /control/dhcp/find_active_dhcp
|
||||||
|
|
||||||
|
vboxnet0
|
||||||
|
|
||||||
|
Response:
|
||||||
|
|
||||||
|
200 OK
|
||||||
|
|
||||||
|
{
|
||||||
|
"other_server": {
|
||||||
|
"found": "yes|no|error",
|
||||||
|
"error": "Error message", // set if found=error
|
||||||
|
},
|
||||||
|
"static_ip": {
|
||||||
|
"static": "yes|no|error",
|
||||||
|
"ip": "<Current dynamic IP address>", // set if static=no
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
If `other_server.found` is:
|
||||||
|
* `no`: everything is fine - there is no other DHCP server
|
||||||
|
* `yes`: we found another DHCP server. UI shows a warning.
|
||||||
|
* `error`: we failed to determine whether there's another DHCP server. `other_server.error` contains error details. UI shows a warning.
|
||||||
|
|
||||||
|
If `static_ip.static` is:
|
||||||
|
* `yes`: everything is fine - server uses static IP address.
|
||||||
|
|
||||||
|
* `no`: `static_ip.ip` contains the current dynamic IP address which we may set as static. In this case UI shows a warning:
|
||||||
|
|
||||||
|
Your system uses dynamic IP address configuration for interface <CURRENT INTERFACE NAME>. In order to use DHCP server a static IP address must be set. Your current IP address is <static_ip.ip>. We will automatically set this IP address as static if you press Enable DHCP button.
|
||||||
|
|
||||||
|
* `error`: this means that the server failed to check for a static IP. In this case UI shows a warning:
|
||||||
|
|
||||||
|
In order to use DHCP server a static IP address must be set. We failed to determine if this network interface is configured using static IP address. Please set a static IP address manually.
|
||||||
|
|
||||||
|
|
||||||
|
### "Enable DHCP" command
|
||||||
|
|
||||||
|
Request:
|
||||||
|
|
||||||
|
POST /control/dhcp/set_config
|
||||||
|
|
||||||
|
{
|
||||||
|
"enabled":true,
|
||||||
|
"interface_name":"vboxnet0",
|
||||||
|
"gateway_ip":"192.169.56.1",
|
||||||
|
"subnet_mask":"255.255.255.0",
|
||||||
|
"range_start":"192.169.56.3",
|
||||||
|
"range_end":"192.169.56.3",
|
||||||
|
"lease_duration":60,
|
||||||
|
"icmp_timeout_msec":0
|
||||||
|
}
|
||||||
|
|
||||||
|
Response:
|
||||||
|
|
||||||
|
200 OK
|
||||||
|
|
||||||
|
OK
|
||||||
|
|
||||||
|
|
||||||
|
### Static IP check/set
|
||||||
|
|
||||||
|
Before enabling DHCP server we have to make sure the network interface we use has a static IP configured.
|
||||||
|
|
||||||
|
#### Phase 1
|
||||||
|
|
||||||
|
On Debian systems DHCP is configured by `/etc/dhcpcd.conf`.
|
||||||
|
|
||||||
|
To detect if a static IP is used currently we search for line
|
||||||
|
|
||||||
|
interface eth0
|
||||||
|
|
||||||
|
and then look for line
|
||||||
|
|
||||||
|
static ip_address=...
|
||||||
|
|
||||||
|
If the interface already has a static IP, everything is set up, we don't have to change anything.
|
||||||
|
|
||||||
|
To get the current IP address along with netmask we execute
|
||||||
|
|
||||||
|
ip -oneline -family inet address show eth0
|
||||||
|
|
||||||
|
which will print:
|
||||||
|
|
||||||
|
2: eth0 inet 192.168.0.1/24 brd 192.168.0.255 scope global eth0\ valid_lft forever preferred_lft forever
|
||||||
|
|
||||||
|
To get the current gateway address:
|
||||||
|
|
||||||
|
ip route show dev enp2s0
|
||||||
|
|
||||||
|
which will print:
|
||||||
|
|
||||||
|
default via 192.168.0.1 proto dhcp metric 100
|
||||||
|
|
||||||
|
|
||||||
|
#### Phase 2
|
||||||
|
|
||||||
|
This method only works on Raspbian.
|
||||||
|
|
||||||
|
On Ubuntu DHCP for a network interface can't be disabled via `dhcpcd.conf`. This must be configured in `/etc/netplan/01-netcfg.yaml`.
|
||||||
|
|
||||||
|
Fedora doesn't use `dhcpcd.conf` configuration at all.
|
||||||
|
|
||||||
|
Step 1.
|
||||||
|
|
||||||
|
To set a static IP address we add these lines to `dhcpcd.conf`:
|
||||||
|
|
||||||
|
interface eth0
|
||||||
|
static ip_address=192.168.0.1/24
|
||||||
|
static routers=192.168.0.1
|
||||||
|
static domain_name_servers=192.168.0.1
|
||||||
|
|
||||||
|
* Don't set 'routers' if we couldn't find gateway IP
|
||||||
|
* Set 'domain_name_servers' equal to our IP
|
||||||
|
|
||||||
|
Step 2.
|
||||||
|
|
||||||
|
If we would set a different IP address, we'd need to replace the IP address for the current network configuration. But currently this step isn't necessary.
|
||||||
|
|
||||||
|
ip addr replace dev eth0 192.168.0.1/24
|
169
app.go
169
app.go
|
@ -1,16 +1,20 @@
|
||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bufio"
|
||||||
"crypto/tls"
|
"crypto/tls"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"io"
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
"net"
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
|
"os/exec"
|
||||||
"os/signal"
|
"os/signal"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"runtime"
|
"runtime"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
"syscall"
|
"syscall"
|
||||||
|
|
||||||
|
@ -45,15 +49,6 @@ func main() {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
signalChannel := make(chan os.Signal)
|
|
||||||
signal.Notify(signalChannel, syscall.SIGINT, syscall.SIGTERM, syscall.SIGHUP, syscall.SIGQUIT)
|
|
||||||
go func() {
|
|
||||||
<-signalChannel
|
|
||||||
cleanup()
|
|
||||||
cleanupAlways()
|
|
||||||
os.Exit(0)
|
|
||||||
}()
|
|
||||||
|
|
||||||
// run the protection
|
// run the protection
|
||||||
run(args)
|
run(args)
|
||||||
}
|
}
|
||||||
|
@ -83,6 +78,18 @@ func run(args options) {
|
||||||
}
|
}
|
||||||
|
|
||||||
config.firstRun = detectFirstRun()
|
config.firstRun = detectFirstRun()
|
||||||
|
if config.firstRun {
|
||||||
|
requireAdminRights()
|
||||||
|
}
|
||||||
|
|
||||||
|
signalChannel := make(chan os.Signal)
|
||||||
|
signal.Notify(signalChannel, syscall.SIGINT, syscall.SIGTERM, syscall.SIGHUP, syscall.SIGQUIT)
|
||||||
|
go func() {
|
||||||
|
<-signalChannel
|
||||||
|
cleanup()
|
||||||
|
cleanupAlways()
|
||||||
|
os.Exit(0)
|
||||||
|
}()
|
||||||
|
|
||||||
// Do the upgrade if necessary
|
// Do the upgrade if necessary
|
||||||
err := upgradeConfig()
|
err := upgradeConfig()
|
||||||
|
@ -161,54 +168,7 @@ func run(args options) {
|
||||||
httpsServer.cond = sync.NewCond(&httpsServer.Mutex)
|
httpsServer.cond = sync.NewCond(&httpsServer.Mutex)
|
||||||
|
|
||||||
// for https, we have a separate goroutine loop
|
// for https, we have a separate goroutine loop
|
||||||
go func() {
|
go httpServerLoop()
|
||||||
for { // this is an endless loop
|
|
||||||
httpsServer.cond.L.Lock()
|
|
||||||
// this mechanism doesn't let us through until all conditions are ment
|
|
||||||
for config.TLS.Enabled == false || config.TLS.PortHTTPS == 0 || config.TLS.PrivateKey == "" || config.TLS.CertificateChain == "" { // sleep until necessary data is supplied
|
|
||||||
httpsServer.cond.Wait()
|
|
||||||
}
|
|
||||||
address := net.JoinHostPort(config.BindHost, strconv.Itoa(config.TLS.PortHTTPS))
|
|
||||||
// validate current TLS config and update warnings (it could have been loaded from file)
|
|
||||||
data := validateCertificates(config.TLS.CertificateChain, config.TLS.PrivateKey, config.TLS.ServerName)
|
|
||||||
if !data.ValidPair {
|
|
||||||
cleanupAlways()
|
|
||||||
log.Fatal(data.WarningValidation)
|
|
||||||
}
|
|
||||||
config.Lock()
|
|
||||||
config.TLS.tlsConfigStatus = data // update warnings
|
|
||||||
config.Unlock()
|
|
||||||
|
|
||||||
// prepare certs for HTTPS server
|
|
||||||
// important -- they have to be copies, otherwise changing the contents in config.TLS will break encryption for in-flight requests
|
|
||||||
certchain := make([]byte, len(config.TLS.CertificateChain))
|
|
||||||
copy(certchain, []byte(config.TLS.CertificateChain))
|
|
||||||
privatekey := make([]byte, len(config.TLS.PrivateKey))
|
|
||||||
copy(privatekey, []byte(config.TLS.PrivateKey))
|
|
||||||
cert, err := tls.X509KeyPair(certchain, privatekey)
|
|
||||||
if err != nil {
|
|
||||||
cleanupAlways()
|
|
||||||
log.Fatal(err)
|
|
||||||
}
|
|
||||||
httpsServer.cond.L.Unlock()
|
|
||||||
|
|
||||||
// prepare HTTPS server
|
|
||||||
httpsServer.server = &http.Server{
|
|
||||||
Addr: address,
|
|
||||||
TLSConfig: &tls.Config{
|
|
||||||
Certificates: []tls.Certificate{cert},
|
|
||||||
MinVersion: tls.VersionTLS12,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
printHTTPAddresses("https")
|
|
||||||
err = httpsServer.server.ListenAndServeTLS("", "")
|
|
||||||
if err != http.ErrServerClosed {
|
|
||||||
cleanupAlways()
|
|
||||||
log.Fatal(err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
// this loop is used as an ability to change listening host and/or port
|
// this loop is used as an ability to change listening host and/or port
|
||||||
for {
|
for {
|
||||||
|
@ -228,6 +188,89 @@ func run(args options) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func httpServerLoop() {
|
||||||
|
for {
|
||||||
|
httpsServer.cond.L.Lock()
|
||||||
|
// this mechanism doesn't let us through until all conditions are met
|
||||||
|
for config.TLS.Enabled == false ||
|
||||||
|
config.TLS.PortHTTPS == 0 ||
|
||||||
|
config.TLS.PrivateKey == "" ||
|
||||||
|
config.TLS.CertificateChain == "" { // sleep until necessary data is supplied
|
||||||
|
httpsServer.cond.Wait()
|
||||||
|
}
|
||||||
|
address := net.JoinHostPort(config.BindHost, strconv.Itoa(config.TLS.PortHTTPS))
|
||||||
|
// validate current TLS config and update warnings (it could have been loaded from file)
|
||||||
|
data := validateCertificates(config.TLS.CertificateChain, config.TLS.PrivateKey, config.TLS.ServerName)
|
||||||
|
if !data.ValidPair {
|
||||||
|
cleanupAlways()
|
||||||
|
log.Fatal(data.WarningValidation)
|
||||||
|
}
|
||||||
|
config.Lock()
|
||||||
|
config.TLS.tlsConfigStatus = data // update warnings
|
||||||
|
config.Unlock()
|
||||||
|
|
||||||
|
// prepare certs for HTTPS server
|
||||||
|
// important -- they have to be copies, otherwise changing the contents in config.TLS will break encryption for in-flight requests
|
||||||
|
certchain := make([]byte, len(config.TLS.CertificateChain))
|
||||||
|
copy(certchain, []byte(config.TLS.CertificateChain))
|
||||||
|
privatekey := make([]byte, len(config.TLS.PrivateKey))
|
||||||
|
copy(privatekey, []byte(config.TLS.PrivateKey))
|
||||||
|
cert, err := tls.X509KeyPair(certchain, privatekey)
|
||||||
|
if err != nil {
|
||||||
|
cleanupAlways()
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
httpsServer.cond.L.Unlock()
|
||||||
|
|
||||||
|
// prepare HTTPS server
|
||||||
|
httpsServer.server = &http.Server{
|
||||||
|
Addr: address,
|
||||||
|
TLSConfig: &tls.Config{
|
||||||
|
Certificates: []tls.Certificate{cert},
|
||||||
|
MinVersion: tls.VersionTLS12,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
printHTTPAddresses("https")
|
||||||
|
err = httpsServer.server.ListenAndServeTLS("", "")
|
||||||
|
if err != http.ErrServerClosed {
|
||||||
|
cleanupAlways()
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if the current user has root (administrator) rights
|
||||||
|
// and if not, ask and try to run as root
|
||||||
|
func requireAdminRights() {
|
||||||
|
admin, _ := haveAdminRights()
|
||||||
|
if admin {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if runtime.GOOS == "windows" {
|
||||||
|
log.Fatal("This is the first launch of AdGuard Home. You must run it as Administrator.")
|
||||||
|
|
||||||
|
} else {
|
||||||
|
log.Error("This is the first launch of AdGuard Home. You must run it as root.")
|
||||||
|
|
||||||
|
_, _ = io.WriteString(os.Stdout, "Do you want to start AdGuard Home as root user? [y/n] ")
|
||||||
|
stdin := bufio.NewReader(os.Stdin)
|
||||||
|
buf, _ := stdin.ReadString('\n')
|
||||||
|
buf = strings.TrimSpace(buf)
|
||||||
|
if buf != "y" {
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd := exec.Command("sudo", os.Args...)
|
||||||
|
cmd.Stdin = os.Stdin
|
||||||
|
cmd.Stdout = os.Stdout
|
||||||
|
cmd.Stderr = os.Stderr
|
||||||
|
_ = cmd.Run()
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Write PID to a file
|
// Write PID to a file
|
||||||
func writePIDFile(fn string) bool {
|
func writePIDFile(fn string) bool {
|
||||||
data := fmt.Sprintf("%d", os.Getpid())
|
data := fmt.Sprintf("%d", os.Getpid())
|
||||||
|
@ -311,18 +354,6 @@ func enableTLS13() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set user-specified limit of how many fd's we can use
|
|
||||||
// https://github.com/AdguardTeam/AdGuardHome/issues/659
|
|
||||||
func setRlimit(val uint) {
|
|
||||||
var rlim syscall.Rlimit
|
|
||||||
rlim.Max = uint64(val)
|
|
||||||
rlim.Cur = uint64(val)
|
|
||||||
err := syscall.Setrlimit(syscall.RLIMIT_NOFILE, &rlim)
|
|
||||||
if err != nil {
|
|
||||||
log.Error("Setrlimit() failed: %v", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func cleanup() {
|
func cleanup() {
|
||||||
log.Info("Stopping AdGuard Home")
|
log.Info("Stopping AdGuard Home")
|
||||||
|
|
||||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -48,7 +48,7 @@
|
||||||
"clean-webpack-plugin": "^0.1.19",
|
"clean-webpack-plugin": "^0.1.19",
|
||||||
"compression-webpack-plugin": "^1.1.11",
|
"compression-webpack-plugin": "^1.1.11",
|
||||||
"copy-webpack-plugin": "^4.6.0",
|
"copy-webpack-plugin": "^4.6.0",
|
||||||
"css-loader": "^0.28.11",
|
"css-loader": "^2.1.1",
|
||||||
"eslint": "^4.19.1",
|
"eslint": "^4.19.1",
|
||||||
"eslint-config-airbnb-base": "^12.1.0",
|
"eslint-config-airbnb-base": "^12.1.0",
|
||||||
"eslint-config-react-app": "^2.1.0",
|
"eslint-config-react-app": "^2.1.0",
|
||||||
|
|
|
@ -257,5 +257,7 @@
|
||||||
"reset_settings": "Reset settings",
|
"reset_settings": "Reset settings",
|
||||||
"update_announcement": "AdGuard Home {{version}} is now available! <0>Click here<\/0> for more info.",
|
"update_announcement": "AdGuard Home {{version}} is now available! <0>Click here<\/0> for more info.",
|
||||||
"setup_guide": "Setup guide",
|
"setup_guide": "Setup guide",
|
||||||
"dns_addresses": "DNS addresses"
|
"dns_addresses": "DNS addresses",
|
||||||
|
"down": "Down",
|
||||||
|
"fix": "Fix"
|
||||||
}
|
}
|
|
@ -44,3 +44,18 @@ export const setAllSettings = values => async (dispatch) => {
|
||||||
dispatch(prevStep());
|
dispatch(prevStep());
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const checkConfigRequest = createAction('CHECK_CONFIG_REQUEST');
|
||||||
|
export const checkConfigFailure = createAction('CHECK_CONFIG_FAILURE');
|
||||||
|
export const checkConfigSuccess = createAction('CHECK_CONFIG_SUCCESS');
|
||||||
|
|
||||||
|
export const checkConfig = values => async (dispatch) => {
|
||||||
|
dispatch(checkConfigRequest());
|
||||||
|
try {
|
||||||
|
const check = await apiClient.checkConfig(values);
|
||||||
|
dispatch(checkConfigSuccess(check));
|
||||||
|
} catch (error) {
|
||||||
|
dispatch(addErrorToast({ error }));
|
||||||
|
dispatch(checkConfigFailure());
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
|
@ -350,6 +350,7 @@ export default class Api {
|
||||||
// Installation
|
// Installation
|
||||||
INSTALL_GET_ADDRESSES = { path: 'install/get_addresses', method: 'GET' };
|
INSTALL_GET_ADDRESSES = { path: 'install/get_addresses', method: 'GET' };
|
||||||
INSTALL_CONFIGURE = { path: 'install/configure', method: 'POST' };
|
INSTALL_CONFIGURE = { path: 'install/configure', method: 'POST' };
|
||||||
|
INSTALL_CHECK_CONFIG = { path: 'install/check_config', method: 'POST' };
|
||||||
|
|
||||||
getDefaultAddresses() {
|
getDefaultAddresses() {
|
||||||
const { path, method } = this.INSTALL_GET_ADDRESSES;
|
const { path, method } = this.INSTALL_GET_ADDRESSES;
|
||||||
|
@ -365,6 +366,15 @@ export default class Api {
|
||||||
return this.makeRequest(path, method, parameters);
|
return this.makeRequest(path, method, parameters);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
checkConfig(config) {
|
||||||
|
const { path, method } = this.INSTALL_CHECK_CONFIG;
|
||||||
|
const parameters = {
|
||||||
|
data: config,
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
};
|
||||||
|
return this.makeRequest(path, method, parameters);
|
||||||
|
}
|
||||||
|
|
||||||
// DNS-over-HTTPS and DNS-over-TLS
|
// DNS-over-HTTPS and DNS-over-TLS
|
||||||
TLS_STATUS = { path: 'tls/status', method: 'GET' };
|
TLS_STATUS = { path: 'tls/status', method: 'GET' };
|
||||||
TLS_CONFIG = { path: 'tls/configure', method: 'POST' };
|
TLS_CONFIG = { path: 'tls/configure', method: 'POST' };
|
||||||
|
|
|
@ -55,6 +55,8 @@ class Controls extends Component {
|
||||||
invalid
|
invalid
|
||||||
|| pristine
|
|| pristine
|
||||||
|| install.processingSubmit
|
|| install.processingSubmit
|
||||||
|
|| install.dns.status
|
||||||
|
|| install.web.status
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<Trans>next</Trans>
|
<Trans>next</Trans>
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import React from 'react';
|
import React, { Component, Fragment } from 'react';
|
||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import { Field, reduxForm, formValueSelector } from 'redux-form';
|
import { Field, reduxForm, formValueSelector } from 'redux-form';
|
||||||
|
@ -30,10 +30,25 @@ const toNumber = value => value && parseInt(value, 10);
|
||||||
const renderInterfaces = (interfaces => (
|
const renderInterfaces = (interfaces => (
|
||||||
Object.keys(interfaces).map((item) => {
|
Object.keys(interfaces).map((item) => {
|
||||||
const option = interfaces[item];
|
const option = interfaces[item];
|
||||||
const { name } = option;
|
const {
|
||||||
|
name,
|
||||||
|
ip_addresses,
|
||||||
|
flags,
|
||||||
|
} = option;
|
||||||
|
|
||||||
if (option.ip_addresses && option.ip_addresses.length > 0) {
|
if (option && ip_addresses && ip_addresses.length > 0) {
|
||||||
const ip = getInterfaceIp(option);
|
const ip = getInterfaceIp(option);
|
||||||
|
const isDown = flags && flags.includes('down');
|
||||||
|
|
||||||
|
if (isDown) {
|
||||||
|
return (
|
||||||
|
<option value={ip} key={name} disabled>
|
||||||
|
<Fragment>
|
||||||
|
{name} - {ip} (<Trans>down</Trans>)
|
||||||
|
</Fragment>
|
||||||
|
</option>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<option value={ip} key={name}>
|
<option value={ip} key={name}>
|
||||||
|
@ -46,141 +61,191 @@ const renderInterfaces = (interfaces => (
|
||||||
})
|
})
|
||||||
));
|
));
|
||||||
|
|
||||||
let Settings = (props) => {
|
class Settings extends Component {
|
||||||
const {
|
componentDidMount() {
|
||||||
handleSubmit,
|
const { web, dns } = this.props.config;
|
||||||
webIp,
|
|
||||||
webPort,
|
|
||||||
dnsIp,
|
|
||||||
dnsPort,
|
|
||||||
interfaces,
|
|
||||||
invalid,
|
|
||||||
webWarning,
|
|
||||||
dnsWarning,
|
|
||||||
} = props;
|
|
||||||
|
|
||||||
return (
|
this.props.validateForm({
|
||||||
<form className="setup__step" onSubmit={handleSubmit}>
|
web,
|
||||||
<div className="setup__group">
|
dns,
|
||||||
<div className="setup__subtitle">
|
});
|
||||||
<Trans>install_settings_title</Trans>
|
}
|
||||||
</div>
|
|
||||||
<div className="row">
|
render() {
|
||||||
<div className="col-8">
|
const {
|
||||||
<div className="form-group">
|
handleSubmit,
|
||||||
<label>
|
handleChange,
|
||||||
<Trans>install_settings_listen</Trans>
|
handleAutofix,
|
||||||
</label>
|
webIp,
|
||||||
<Field
|
webPort,
|
||||||
name="web.ip"
|
dnsIp,
|
||||||
component="select"
|
dnsPort,
|
||||||
className="form-control custom-select"
|
interfaces,
|
||||||
>
|
invalid,
|
||||||
<option value={ALL_INTERFACES_IP}>
|
config,
|
||||||
<Trans>install_settings_all_interfaces</Trans>
|
} = this.props;
|
||||||
</option>
|
const {
|
||||||
{renderInterfaces(interfaces)}
|
status: webStatus,
|
||||||
</Field>
|
can_autofix: isWebFixAvailable,
|
||||||
|
} = config.web;
|
||||||
|
const {
|
||||||
|
status: dnsStatus,
|
||||||
|
can_autofix: isDnsFixAvailable,
|
||||||
|
} = config.dns;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<form className="setup__step" onSubmit={handleSubmit}>
|
||||||
|
<div className="setup__group">
|
||||||
|
<div className="setup__subtitle">
|
||||||
|
<Trans>install_settings_title</Trans>
|
||||||
|
</div>
|
||||||
|
<div className="row">
|
||||||
|
<div className="col-8">
|
||||||
|
<div className="form-group">
|
||||||
|
<label>
|
||||||
|
<Trans>install_settings_listen</Trans>
|
||||||
|
</label>
|
||||||
|
<Field
|
||||||
|
name="web.ip"
|
||||||
|
component="select"
|
||||||
|
className="form-control custom-select"
|
||||||
|
onChange={handleChange}
|
||||||
|
>
|
||||||
|
<option value={ALL_INTERFACES_IP}>
|
||||||
|
<Trans>install_settings_all_interfaces</Trans>
|
||||||
|
</option>
|
||||||
|
{renderInterfaces(interfaces)}
|
||||||
|
</Field>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="col-4">
|
||||||
|
<div className="form-group">
|
||||||
|
<label>
|
||||||
|
<Trans>install_settings_port</Trans>
|
||||||
|
</label>
|
||||||
|
<Field
|
||||||
|
name="web.port"
|
||||||
|
component={renderField}
|
||||||
|
type="number"
|
||||||
|
className="form-control"
|
||||||
|
placeholder="80"
|
||||||
|
validate={[port, required]}
|
||||||
|
normalize={toNumber}
|
||||||
|
onChange={handleChange}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</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={() => handleAutofix('web', webIp, webPort)}
|
||||||
|
>
|
||||||
|
<Trans>fix</Trans>
|
||||||
|
</button>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="col-4">
|
<div className="setup__desc">
|
||||||
<div className="form-group">
|
<Trans>install_settings_interface_link</Trans>
|
||||||
<label>
|
<div className="mt-1">
|
||||||
<Trans>install_settings_port</Trans>
|
<AddressList
|
||||||
</label>
|
interfaces={interfaces}
|
||||||
<Field
|
address={webIp}
|
||||||
name="web.port"
|
port={webPort}
|
||||||
component={renderField}
|
|
||||||
type="number"
|
|
||||||
className="form-control"
|
|
||||||
placeholder="80"
|
|
||||||
validate={[port, required]}
|
|
||||||
normalize={toNumber}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="setup__desc">
|
<div className="setup__group">
|
||||||
<Trans>install_settings_interface_link</Trans>
|
<div className="setup__subtitle">
|
||||||
<div className="mt-1">
|
<Trans>install_settings_dns</Trans>
|
||||||
<AddressList
|
|
||||||
interfaces={interfaces}
|
|
||||||
address={webIp}
|
|
||||||
port={webPort}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
{webWarning &&
|
<div className="row">
|
||||||
<div className="text-danger mt-2">
|
<div className="col-8">
|
||||||
{webWarning}
|
<div className="form-group">
|
||||||
|
<label>
|
||||||
|
<Trans>install_settings_listen</Trans>
|
||||||
|
</label>
|
||||||
|
<Field
|
||||||
|
name="dns.ip"
|
||||||
|
component="select"
|
||||||
|
className="form-control custom-select"
|
||||||
|
onChange={handleChange}
|
||||||
|
>
|
||||||
|
<option value={ALL_INTERFACES_IP}>
|
||||||
|
<Trans>install_settings_all_interfaces</Trans>
|
||||||
|
</option>
|
||||||
|
{renderInterfaces(interfaces)}
|
||||||
|
</Field>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
}
|
<div className="col-4">
|
||||||
</div>
|
<div className="form-group">
|
||||||
</div>
|
<label>
|
||||||
<div className="setup__group">
|
<Trans>install_settings_port</Trans>
|
||||||
<div className="setup__subtitle">
|
</label>
|
||||||
<Trans>install_settings_dns</Trans>
|
<Field
|
||||||
</div>
|
name="dns.port"
|
||||||
<div className="row">
|
component={renderField}
|
||||||
<div className="col-8">
|
type="number"
|
||||||
<div className="form-group">
|
className="form-control"
|
||||||
<label>
|
placeholder="80"
|
||||||
<Trans>install_settings_listen</Trans>
|
validate={[port, required]}
|
||||||
</label>
|
normalize={toNumber}
|
||||||
<Field
|
onChange={handleChange}
|
||||||
name="dns.ip"
|
/>
|
||||||
component="select"
|
</div>
|
||||||
className="form-control custom-select"
|
</div>
|
||||||
>
|
<div className="col-12">
|
||||||
<option value={ALL_INTERFACES_IP}>
|
{dnsStatus &&
|
||||||
<Trans>install_settings_all_interfaces</Trans>
|
<div className="setup__error text-danger">
|
||||||
</option>
|
{dnsStatus}
|
||||||
{renderInterfaces(interfaces)}
|
{isDnsFixAvailable &&
|
||||||
</Field>
|
<button
|
||||||
|
type="button"
|
||||||
|
className="btn btn-secondary btn-sm ml-2"
|
||||||
|
onClick={() => handleAutofix('dns', dnsIp, dnsPort)}
|
||||||
|
>
|
||||||
|
<Trans>fix</Trans>
|
||||||
|
</button>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="col-4">
|
<div className="setup__desc">
|
||||||
<div className="form-group">
|
<Trans>install_settings_dns_desc</Trans>
|
||||||
<label>
|
<div className="mt-1">
|
||||||
<Trans>install_settings_port</Trans>
|
<AddressList
|
||||||
</label>
|
interfaces={interfaces}
|
||||||
<Field
|
address={dnsIp}
|
||||||
name="dns.port"
|
port={dnsPort}
|
||||||
component={renderField}
|
isDns={true}
|
||||||
type="number"
|
|
||||||
className="form-control"
|
|
||||||
placeholder="80"
|
|
||||||
validate={[port, required]}
|
|
||||||
normalize={toNumber}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="setup__desc">
|
<Controls invalid={invalid} />
|
||||||
<Trans>install_settings_dns_desc</Trans>
|
</form>
|
||||||
<div className="mt-1">
|
);
|
||||||
<AddressList
|
}
|
||||||
interfaces={interfaces}
|
}
|
||||||
address={dnsIp}
|
|
||||||
port={dnsPort}
|
|
||||||
isDns={true}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
{dnsWarning &&
|
|
||||||
<div className="text-danger mt-2">
|
|
||||||
{dnsWarning}
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<Controls invalid={invalid} />
|
|
||||||
</form>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
Settings.propTypes = {
|
Settings.propTypes = {
|
||||||
handleSubmit: PropTypes.func.isRequired,
|
handleSubmit: PropTypes.func.isRequired,
|
||||||
|
handleChange: PropTypes.func,
|
||||||
|
handleAutofix: PropTypes.func,
|
||||||
|
validateForm: PropTypes.func,
|
||||||
webIp: PropTypes.string.isRequired,
|
webIp: PropTypes.string.isRequired,
|
||||||
dnsIp: PropTypes.string.isRequired,
|
dnsIp: PropTypes.string.isRequired,
|
||||||
|
config: PropTypes.object.isRequired,
|
||||||
webPort: PropTypes.oneOfType([
|
webPort: PropTypes.oneOfType([
|
||||||
PropTypes.string,
|
PropTypes.string,
|
||||||
PropTypes.number,
|
PropTypes.number,
|
||||||
|
@ -189,8 +254,6 @@ Settings.propTypes = {
|
||||||
PropTypes.string,
|
PropTypes.string,
|
||||||
PropTypes.number,
|
PropTypes.number,
|
||||||
]),
|
]),
|
||||||
webWarning: PropTypes.string.isRequired,
|
|
||||||
dnsWarning: PropTypes.string.isRequired,
|
|
||||||
interfaces: PropTypes.object.isRequired,
|
interfaces: PropTypes.object.isRequired,
|
||||||
invalid: PropTypes.bool.isRequired,
|
invalid: PropTypes.bool.isRequired,
|
||||||
initialValues: PropTypes.object,
|
initialValues: PropTypes.object,
|
||||||
|
@ -198,7 +261,7 @@ Settings.propTypes = {
|
||||||
|
|
||||||
const selector = formValueSelector('install');
|
const selector = formValueSelector('install');
|
||||||
|
|
||||||
Settings = connect((state) => {
|
const SettingsForm = connect((state) => {
|
||||||
const webIp = selector(state, 'web.ip');
|
const webIp = selector(state, 'web.ip');
|
||||||
const webPort = selector(state, 'web.port');
|
const webPort = selector(state, 'web.port');
|
||||||
const dnsIp = selector(state, 'dns.ip');
|
const dnsIp = selector(state, 'dns.ip');
|
||||||
|
@ -219,4 +282,4 @@ export default flow([
|
||||||
destroyOnUnmount: false,
|
destroyOnUnmount: false,
|
||||||
forceUnregisterOnUnmount: true,
|
forceUnregisterOnUnmount: true,
|
||||||
}),
|
}),
|
||||||
])(Settings);
|
])(SettingsForm);
|
||||||
|
|
|
@ -115,3 +115,7 @@
|
||||||
padding-left: 30px;
|
padding-left: 30px;
|
||||||
padding-right: 30px;
|
padding-right: 30px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.setup__error {
|
||||||
|
margin: -5px 0 5px;
|
||||||
|
}
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
import React, { Component, Fragment } from 'react';
|
import React, { Component, Fragment } from 'react';
|
||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
|
import debounce from 'lodash/debounce';
|
||||||
|
|
||||||
import * as actionCreators from '../../actions/install';
|
import * as actionCreators from '../../actions/install';
|
||||||
import { getWebAddress } from '../../helpers/helpers';
|
import { getWebAddress } from '../../helpers/helpers';
|
||||||
|
@ -8,6 +9,7 @@ import {
|
||||||
INSTALL_FIRST_STEP,
|
INSTALL_FIRST_STEP,
|
||||||
INSTALL_TOTAL_STEPS,
|
INSTALL_TOTAL_STEPS,
|
||||||
ALL_INTERFACES_IP,
|
ALL_INTERFACES_IP,
|
||||||
|
DEBOUNCE_TIMEOUT,
|
||||||
} from '../../helpers/constants';
|
} from '../../helpers/constants';
|
||||||
|
|
||||||
import Loading from '../../components/ui/Loading';
|
import Loading from '../../components/ui/Loading';
|
||||||
|
@ -34,6 +36,30 @@ class Setup extends Component {
|
||||||
this.props.setAllSettings(values);
|
this.props.setAllSettings(values);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
handleFormChange = debounce((values) => {
|
||||||
|
if (values && values.web.port && values.dns.port) {
|
||||||
|
this.props.checkConfig(values);
|
||||||
|
}
|
||||||
|
}, DEBOUNCE_TIMEOUT);
|
||||||
|
|
||||||
|
handleAutofix = (type, ip, port) => {
|
||||||
|
const data = {
|
||||||
|
ip,
|
||||||
|
port,
|
||||||
|
autofix: true,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (type === 'web') {
|
||||||
|
this.props.checkConfig({
|
||||||
|
web: { ...data },
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
this.props.checkConfig({
|
||||||
|
dns: { ...data },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
openDashboard = (ip, port) => {
|
openDashboard = (ip, port) => {
|
||||||
let address = getWebAddress(ip, port);
|
let address = getWebAddress(ip, port);
|
||||||
|
|
||||||
|
@ -63,11 +89,13 @@ class Setup extends Component {
|
||||||
case 2:
|
case 2:
|
||||||
return (
|
return (
|
||||||
<Settings
|
<Settings
|
||||||
|
config={config}
|
||||||
initialValues={config}
|
initialValues={config}
|
||||||
interfaces={interfaces}
|
interfaces={interfaces}
|
||||||
webWarning={config.web.warning}
|
|
||||||
dnsWarning={config.dns.warning}
|
|
||||||
onSubmit={this.nextStep}
|
onSubmit={this.nextStep}
|
||||||
|
onChange={this.handleFormChange}
|
||||||
|
validateForm={this.handleFormChange}
|
||||||
|
handleAutofix={this.handleAutofix}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
case 3:
|
case 3:
|
||||||
|
@ -116,6 +144,7 @@ class Setup extends Component {
|
||||||
Setup.propTypes = {
|
Setup.propTypes = {
|
||||||
getDefaultAddresses: PropTypes.func.isRequired,
|
getDefaultAddresses: PropTypes.func.isRequired,
|
||||||
setAllSettings: PropTypes.func.isRequired,
|
setAllSettings: PropTypes.func.isRequired,
|
||||||
|
checkConfig: PropTypes.func.isRequired,
|
||||||
nextStep: PropTypes.func.isRequired,
|
nextStep: PropTypes.func.isRequired,
|
||||||
prevStep: PropTypes.func.isRequired,
|
prevStep: PropTypes.func.isRequired,
|
||||||
install: PropTypes.object.isRequired,
|
install: PropTypes.object.isRequired,
|
||||||
|
|
|
@ -10,10 +10,13 @@ const install = handleActions({
|
||||||
[actions.getDefaultAddressesRequest]: state => ({ ...state, processingDefault: true }),
|
[actions.getDefaultAddressesRequest]: state => ({ ...state, processingDefault: true }),
|
||||||
[actions.getDefaultAddressesFailure]: state => ({ ...state, processingDefault: false }),
|
[actions.getDefaultAddressesFailure]: state => ({ ...state, processingDefault: false }),
|
||||||
[actions.getDefaultAddressesSuccess]: (state, { payload }) => {
|
[actions.getDefaultAddressesSuccess]: (state, { payload }) => {
|
||||||
const values = payload;
|
const { interfaces } = payload;
|
||||||
values.web.ip = state.web.ip;
|
const web = { ...state.web, port: payload.web_port };
|
||||||
values.dns.ip = state.dns.ip;
|
const dns = { ...state.dns, port: payload.dns_port };
|
||||||
const newState = { ...state, ...values, processingDefault: false };
|
|
||||||
|
const newState = {
|
||||||
|
...state, web, dns, interfaces, processingDefault: false,
|
||||||
|
};
|
||||||
return newState;
|
return newState;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -23,19 +26,34 @@ const install = handleActions({
|
||||||
[actions.setAllSettingsRequest]: state => ({ ...state, processingSubmit: true }),
|
[actions.setAllSettingsRequest]: state => ({ ...state, processingSubmit: true }),
|
||||||
[actions.setAllSettingsFailure]: state => ({ ...state, processingSubmit: false }),
|
[actions.setAllSettingsFailure]: state => ({ ...state, processingSubmit: false }),
|
||||||
[actions.setAllSettingsSuccess]: state => ({ ...state, processingSubmit: false }),
|
[actions.setAllSettingsSuccess]: state => ({ ...state, processingSubmit: false }),
|
||||||
|
|
||||||
|
[actions.checkConfigRequest]: state => ({ ...state, processingCheck: true }),
|
||||||
|
[actions.checkConfigFailure]: state => ({ ...state, processingCheck: false }),
|
||||||
|
[actions.checkConfigSuccess]: (state, { payload }) => {
|
||||||
|
const web = { ...state.web, ...payload.web };
|
||||||
|
const dns = { ...state.dns, ...payload.dns };
|
||||||
|
|
||||||
|
const newState = {
|
||||||
|
...state, web, dns, processingCheck: false,
|
||||||
|
};
|
||||||
|
return newState;
|
||||||
|
},
|
||||||
}, {
|
}, {
|
||||||
step: INSTALL_FIRST_STEP,
|
step: INSTALL_FIRST_STEP,
|
||||||
processingDefault: true,
|
processingDefault: true,
|
||||||
processingSubmit: false,
|
processingSubmit: false,
|
||||||
|
processingCheck: false,
|
||||||
web: {
|
web: {
|
||||||
ip: '0.0.0.0',
|
ip: '0.0.0.0',
|
||||||
port: 80,
|
port: 80,
|
||||||
warning: '',
|
status: '',
|
||||||
|
can_autofix: false,
|
||||||
},
|
},
|
||||||
dns: {
|
dns: {
|
||||||
ip: '0.0.0.0',
|
ip: '0.0.0.0',
|
||||||
port: 53,
|
port: 53,
|
||||||
warning: '',
|
status: '',
|
||||||
|
can_autofix: false,
|
||||||
},
|
},
|
||||||
interfaces: {},
|
interfaces: {},
|
||||||
});
|
});
|
||||||
|
|
112
control.go
112
control.go
|
@ -2,7 +2,6 @@ package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"context"
|
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
|
@ -970,112 +969,6 @@ func handleSafeSearchStatus(w http.ResponseWriter, r *http.Request) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
type ipport struct {
|
|
||||||
IP string `json:"ip,omitempty"`
|
|
||||||
Port int `json:"port"`
|
|
||||||
Warning string `json:"warning"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type firstRunData struct {
|
|
||||||
Web ipport `json:"web"`
|
|
||||||
DNS ipport `json:"dns"`
|
|
||||||
Username string `json:"username,omitempty"`
|
|
||||||
Password string `json:"password,omitempty"`
|
|
||||||
Interfaces map[string]interface{} `json:"interfaces"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func handleInstallGetAddresses(w http.ResponseWriter, r *http.Request) {
|
|
||||||
log.Tracef("%s %v", r.Method, r.URL)
|
|
||||||
data := firstRunData{}
|
|
||||||
|
|
||||||
// find out if port 80 is available -- if not, fall back to 3000
|
|
||||||
if checkPortAvailable("", 80) == nil {
|
|
||||||
data.Web.Port = 80
|
|
||||||
} else {
|
|
||||||
data.Web.Port = 3000
|
|
||||||
}
|
|
||||||
|
|
||||||
// find out if port 53 is available -- if not, show a big warning
|
|
||||||
data.DNS.Port = 53
|
|
||||||
if checkPacketPortAvailable("", 53) != nil {
|
|
||||||
data.DNS.Warning = "Port 53 is not available for binding -- this will make DNS clients unable to contact AdGuard Home."
|
|
||||||
}
|
|
||||||
|
|
||||||
ifaces, err := getValidNetInterfacesForWeb()
|
|
||||||
if err != nil {
|
|
||||||
httpError(w, http.StatusInternalServerError, "Couldn't get interfaces: %s", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
data.Interfaces = make(map[string]interface{})
|
|
||||||
for _, iface := range ifaces {
|
|
||||||
data.Interfaces[iface.Name] = iface
|
|
||||||
}
|
|
||||||
|
|
||||||
w.Header().Set("Content-Type", "application/json")
|
|
||||||
err = json.NewEncoder(w).Encode(data)
|
|
||||||
if err != nil {
|
|
||||||
httpError(w, http.StatusInternalServerError, "Unable to marshal default addresses to json: %s", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func handleInstallConfigure(w http.ResponseWriter, r *http.Request) {
|
|
||||||
log.Tracef("%s %v", r.Method, r.URL)
|
|
||||||
newSettings := firstRunData{}
|
|
||||||
err := json.NewDecoder(r.Body).Decode(&newSettings)
|
|
||||||
if err != nil {
|
|
||||||
httpError(w, http.StatusBadRequest, "Failed to parse new config json: %s", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
restartHTTP := true
|
|
||||||
if config.BindHost == newSettings.Web.IP && config.BindPort == newSettings.Web.Port {
|
|
||||||
// no need to rebind
|
|
||||||
restartHTTP = false
|
|
||||||
}
|
|
||||||
|
|
||||||
// validate that hosts and ports are bindable
|
|
||||||
if restartHTTP {
|
|
||||||
err = checkPortAvailable(newSettings.Web.IP, newSettings.Web.Port)
|
|
||||||
if err != nil {
|
|
||||||
httpError(w, http.StatusBadRequest, "Impossible to listen on IP:port %s due to %s", net.JoinHostPort(newSettings.Web.IP, strconv.Itoa(newSettings.Web.Port)), err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
err = checkPacketPortAvailable(newSettings.DNS.IP, newSettings.DNS.Port)
|
|
||||||
if err != nil {
|
|
||||||
httpError(w, http.StatusBadRequest, "Impossible to listen on IP:port %s due to %s", net.JoinHostPort(newSettings.DNS.IP, strconv.Itoa(newSettings.DNS.Port)), err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
config.firstRun = false
|
|
||||||
config.BindHost = newSettings.Web.IP
|
|
||||||
config.BindPort = newSettings.Web.Port
|
|
||||||
config.DNS.BindHost = newSettings.DNS.IP
|
|
||||||
config.DNS.Port = newSettings.DNS.Port
|
|
||||||
config.AuthName = newSettings.Username
|
|
||||||
config.AuthPass = newSettings.Password
|
|
||||||
|
|
||||||
if config.DNS.Port != 0 {
|
|
||||||
err = startDNSServer()
|
|
||||||
if err != nil {
|
|
||||||
httpError(w, http.StatusInternalServerError, "Couldn't start DNS server: %s", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
httpUpdateConfigReloadDNSReturnOK(w, r)
|
|
||||||
// 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 {
|
|
||||||
go func() {
|
|
||||||
httpServer.Shutdown(context.TODO())
|
|
||||||
}()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// --------------
|
// --------------
|
||||||
// DNS-over-HTTPS
|
// DNS-over-HTTPS
|
||||||
// --------------
|
// --------------
|
||||||
|
@ -1097,11 +990,6 @@ func handleDOH(w http.ResponseWriter, r *http.Request) {
|
||||||
// ------------------------
|
// ------------------------
|
||||||
// registration of handlers
|
// registration of handlers
|
||||||
// ------------------------
|
// ------------------------
|
||||||
func registerInstallHandlers() {
|
|
||||||
http.HandleFunc("/control/install/get_addresses", preInstall(ensureGET(handleInstallGetAddresses)))
|
|
||||||
http.HandleFunc("/control/install/configure", preInstall(ensurePOST(handleInstallConfigure)))
|
|
||||||
}
|
|
||||||
|
|
||||||
func registerControlHandlers() {
|
func registerControlHandlers() {
|
||||||
http.HandleFunc("/control/status", postInstall(optionalAuth(ensureGET(handleStatus))))
|
http.HandleFunc("/control/status", postInstall(optionalAuth(ensureGET(handleStatus))))
|
||||||
http.HandleFunc("/control/enable_protection", postInstall(optionalAuth(ensurePOST(handleProtectionEnable))))
|
http.HandleFunc("/control/enable_protection", postInstall(optionalAuth(ensurePOST(handleProtectionEnable))))
|
||||||
|
|
|
@ -0,0 +1,276 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"net"
|
||||||
|
"net/http"
|
||||||
|
"os/exec"
|
||||||
|
"strconv"
|
||||||
|
|
||||||
|
"github.com/AdguardTeam/golibs/log"
|
||||||
|
)
|
||||||
|
|
||||||
|
type firstRunData struct {
|
||||||
|
WebPort int `json:"web_port"`
|
||||||
|
DNSPort int `json:"dns_port"`
|
||||||
|
Interfaces map[string]interface{} `json:"interfaces"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get initial installation settings
|
||||||
|
func handleInstallGetAddresses(w http.ResponseWriter, r *http.Request) {
|
||||||
|
log.Tracef("%s %v", r.Method, r.URL)
|
||||||
|
data := firstRunData{}
|
||||||
|
data.WebPort = 80
|
||||||
|
data.DNSPort = 53
|
||||||
|
|
||||||
|
ifaces, err := getValidNetInterfacesForWeb()
|
||||||
|
if err != nil {
|
||||||
|
httpError(w, http.StatusInternalServerError, "Couldn't get interfaces: %s", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
data.Interfaces = make(map[string]interface{})
|
||||||
|
for _, iface := range ifaces {
|
||||||
|
data.Interfaces[iface.Name] = iface
|
||||||
|
}
|
||||||
|
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
err = json.NewEncoder(w).Encode(data)
|
||||||
|
if err != nil {
|
||||||
|
httpError(w, http.StatusInternalServerError, "Unable to marshal default addresses to json: %s", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type checkConfigReqEnt struct {
|
||||||
|
Port int `json:"port"`
|
||||||
|
IP string `json:"ip"`
|
||||||
|
Autofix bool `json:"autofix"`
|
||||||
|
}
|
||||||
|
type checkConfigReq struct {
|
||||||
|
Web checkConfigReqEnt `json:"web"`
|
||||||
|
DNS checkConfigReqEnt `json:"dns"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type checkConfigRespEnt struct {
|
||||||
|
Status string `json:"status"`
|
||||||
|
CanAutofix bool `json:"can_autofix"`
|
||||||
|
}
|
||||||
|
type checkConfigResp struct {
|
||||||
|
Web checkConfigRespEnt `json:"web"`
|
||||||
|
DNS checkConfigRespEnt `json:"dns"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if ports are available, respond with results
|
||||||
|
func handleInstallCheckConfig(w http.ResponseWriter, r *http.Request) {
|
||||||
|
log.Tracef("%s %v", r.Method, r.URL)
|
||||||
|
reqData := checkConfigReq{}
|
||||||
|
respData := checkConfigResp{}
|
||||||
|
err := json.NewDecoder(r.Body).Decode(&reqData)
|
||||||
|
if err != nil {
|
||||||
|
httpError(w, http.StatusBadRequest, "Failed to parse 'check_config' JSON data: %s", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if reqData.Web.Port != 0 && reqData.Web.Port != config.BindPort {
|
||||||
|
err = checkPortAvailable(reqData.Web.IP, reqData.Web.Port)
|
||||||
|
if err != nil {
|
||||||
|
respData.Web.Status = fmt.Sprintf("%v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if reqData.DNS.Port != 0 {
|
||||||
|
err = checkPacketPortAvailable(reqData.DNS.IP, reqData.DNS.Port)
|
||||||
|
|
||||||
|
if errorIsAddrInUse(err) {
|
||||||
|
canAutofix := checkDNSStubListener()
|
||||||
|
if canAutofix && reqData.DNS.Autofix {
|
||||||
|
|
||||||
|
err = disableDNSStubListener()
|
||||||
|
if err != nil {
|
||||||
|
log.Error("Couldn't disable DNSStubListener: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = checkPacketPortAvailable(reqData.DNS.IP, reqData.DNS.Port)
|
||||||
|
canAutofix = false
|
||||||
|
}
|
||||||
|
|
||||||
|
respData.DNS.CanAutofix = canAutofix
|
||||||
|
}
|
||||||
|
|
||||||
|
if err == nil {
|
||||||
|
err = checkPortAvailable(reqData.DNS.IP, reqData.DNS.Port)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
respData.DNS.Status = fmt.Sprintf("%v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
err = json.NewEncoder(w).Encode(respData)
|
||||||
|
if err != nil {
|
||||||
|
httpError(w, http.StatusInternalServerError, "Unable to marshal JSON: %s", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if DNSStubListener is active
|
||||||
|
func checkDNSStubListener() bool {
|
||||||
|
cmd := exec.Command("systemctl", "is-enabled", "systemd-resolved")
|
||||||
|
log.Tracef("executing %s %v", cmd.Path, cmd.Args)
|
||||||
|
_, err := cmd.Output()
|
||||||
|
if err != nil || cmd.ProcessState.ExitCode() != 0 {
|
||||||
|
log.Error("command %s has failed: %v code:%d",
|
||||||
|
cmd.Path, err, cmd.ProcessState.ExitCode())
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd = exec.Command("grep", "-E", "#?DNSStubListener=yes", "/etc/systemd/resolved.conf")
|
||||||
|
log.Tracef("executing %s %v", cmd.Path, cmd.Args)
|
||||||
|
_, err = cmd.Output()
|
||||||
|
if err != nil || cmd.ProcessState.ExitCode() != 0 {
|
||||||
|
log.Error("command %s has failed: %v code:%d",
|
||||||
|
cmd.Path, err, cmd.ProcessState.ExitCode())
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// Deactivate DNSStubListener
|
||||||
|
func disableDNSStubListener() error {
|
||||||
|
cmd := exec.Command("sed", "-r", "-i.orig", "s/#?DNSStubListener=yes/DNSStubListener=no/g", "/etc/systemd/resolved.conf")
|
||||||
|
log.Tracef("executing %s %v", cmd.Path, cmd.Args)
|
||||||
|
_, err := cmd.Output()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if cmd.ProcessState.ExitCode() != 0 {
|
||||||
|
return fmt.Errorf("process %s exited with an error: %d",
|
||||||
|
cmd.Path, cmd.ProcessState.ExitCode())
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd = exec.Command("systemctl", "reload-or-restart", "systemd-resolved")
|
||||||
|
log.Tracef("executing %s %v", cmd.Path, cmd.Args)
|
||||||
|
_, err = cmd.Output()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if cmd.ProcessState.ExitCode() != 0 {
|
||||||
|
return fmt.Errorf("process %s exited with an error: %d",
|
||||||
|
cmd.Path, cmd.ProcessState.ExitCode())
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type applyConfigReqEnt struct {
|
||||||
|
IP string `json:"ip"`
|
||||||
|
Port int `json:"port"`
|
||||||
|
}
|
||||||
|
type applyConfigReq struct {
|
||||||
|
Web applyConfigReqEnt `json:"web"`
|
||||||
|
DNS applyConfigReqEnt `json:"dns"`
|
||||||
|
Username string `json:"username"`
|
||||||
|
Password string `json:"password"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Copy installation parameters between two configuration objects
|
||||||
|
func copyInstallSettings(dst *configuration, src *configuration) {
|
||||||
|
dst.BindHost = src.BindHost
|
||||||
|
dst.BindPort = src.BindPort
|
||||||
|
dst.DNS.BindHost = src.DNS.BindHost
|
||||||
|
dst.DNS.Port = src.DNS.Port
|
||||||
|
dst.AuthName = src.AuthName
|
||||||
|
dst.AuthPass = src.AuthPass
|
||||||
|
}
|
||||||
|
|
||||||
|
// Apply new configuration, start DNS server, restart Web server
|
||||||
|
func handleInstallConfigure(w http.ResponseWriter, r *http.Request) {
|
||||||
|
log.Tracef("%s %v", r.Method, r.URL)
|
||||||
|
newSettings := applyConfigReq{}
|
||||||
|
err := json.NewDecoder(r.Body).Decode(&newSettings)
|
||||||
|
if err != nil {
|
||||||
|
httpError(w, http.StatusBadRequest, "Failed to parse 'configure' JSON: %s", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if newSettings.Web.Port == 0 || newSettings.DNS.Port == 0 {
|
||||||
|
httpError(w, http.StatusBadRequest, "port value can't be 0")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
restartHTTP := true
|
||||||
|
if config.BindHost == newSettings.Web.IP && config.BindPort == newSettings.Web.Port {
|
||||||
|
// no need to rebind
|
||||||
|
restartHTTP = false
|
||||||
|
}
|
||||||
|
|
||||||
|
// validate that hosts and ports are bindable
|
||||||
|
if restartHTTP {
|
||||||
|
err = checkPortAvailable(newSettings.Web.IP, newSettings.Web.Port)
|
||||||
|
if err != nil {
|
||||||
|
httpError(w, http.StatusBadRequest, "Impossible to listen on IP:port %s due to %s",
|
||||||
|
net.JoinHostPort(newSettings.Web.IP, strconv.Itoa(newSettings.Web.Port)), err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
err = checkPacketPortAvailable(newSettings.DNS.IP, newSettings.DNS.Port)
|
||||||
|
if err != nil {
|
||||||
|
httpError(w, http.StatusBadRequest, "%s", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
err = checkPortAvailable(newSettings.DNS.IP, newSettings.DNS.Port)
|
||||||
|
if err != nil {
|
||||||
|
httpError(w, http.StatusBadRequest, "%s", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var curConfig configuration
|
||||||
|
copyInstallSettings(&curConfig, &config)
|
||||||
|
|
||||||
|
config.firstRun = false
|
||||||
|
config.BindHost = newSettings.Web.IP
|
||||||
|
config.BindPort = newSettings.Web.Port
|
||||||
|
config.DNS.BindHost = newSettings.DNS.IP
|
||||||
|
config.DNS.Port = newSettings.DNS.Port
|
||||||
|
config.AuthName = newSettings.Username
|
||||||
|
config.AuthPass = newSettings.Password
|
||||||
|
|
||||||
|
err = startDNSServer()
|
||||||
|
if err != nil {
|
||||||
|
config.firstRun = true
|
||||||
|
copyInstallSettings(&config, &curConfig)
|
||||||
|
httpError(w, http.StatusInternalServerError, "Couldn't start DNS server: %s", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
err = config.write()
|
||||||
|
if err != nil {
|
||||||
|
config.firstRun = true
|
||||||
|
copyInstallSettings(&config, &curConfig)
|
||||||
|
httpError(w, http.StatusInternalServerError, "Couldn't write config: %s", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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 {
|
||||||
|
go func() {
|
||||||
|
httpServer.Shutdown(context.TODO())
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
|
||||||
|
returnOK(w)
|
||||||
|
}
|
||||||
|
|
||||||
|
func registerInstallHandlers() {
|
||||||
|
http.HandleFunc("/control/install/get_addresses", preInstall(ensureGET(handleInstallGetAddresses)))
|
||||||
|
http.HandleFunc("/control/install/check_config", preInstall(ensurePOST(handleInstallCheckConfig)))
|
||||||
|
http.HandleFunc("/control/install/configure", preInstall(ensurePOST(handleInstallConfigure)))
|
||||||
|
}
|
26
helpers.go
26
helpers.go
|
@ -15,6 +15,7 @@ import (
|
||||||
"runtime"
|
"runtime"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
"syscall"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/AdguardTeam/dnsproxy/upstream"
|
"github.com/AdguardTeam/dnsproxy/upstream"
|
||||||
|
@ -346,6 +347,31 @@ func customDialContext(ctx context.Context, network, addr string) (net.Conn, err
|
||||||
return nil, firstErr
|
return nil, firstErr
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// check if error is "address already in use"
|
||||||
|
func errorIsAddrInUse(err error) bool {
|
||||||
|
errOpError, ok := err.(*net.OpError)
|
||||||
|
if !ok {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
errSyscallError, ok := errOpError.Err.(*os.SyscallError)
|
||||||
|
if !ok {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
errErrno, ok := errSyscallError.Err.(syscall.Errno)
|
||||||
|
if !ok {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if runtime.GOOS == "windows" {
|
||||||
|
const WSAEADDRINUSE = 10048
|
||||||
|
return errErrno == WSAEADDRINUSE
|
||||||
|
}
|
||||||
|
|
||||||
|
return errErrno == syscall.EADDRINUSE
|
||||||
|
}
|
||||||
|
|
||||||
// ---------------------
|
// ---------------------
|
||||||
// debug logging helpers
|
// debug logging helpers
|
||||||
// ---------------------
|
// ---------------------
|
||||||
|
|
|
@ -739,6 +739,26 @@ paths:
|
||||||
description: OK
|
description: OK
|
||||||
schema:
|
schema:
|
||||||
$ref: "#/definitions/AddressesInfo"
|
$ref: "#/definitions/AddressesInfo"
|
||||||
|
/install/check_config:
|
||||||
|
post:
|
||||||
|
tags:
|
||||||
|
- install
|
||||||
|
operationId: installCheckConfig
|
||||||
|
summary: "Checks configuration"
|
||||||
|
parameters:
|
||||||
|
- in: "body"
|
||||||
|
name: "body"
|
||||||
|
description: "Configuration to be checked"
|
||||||
|
required: true
|
||||||
|
schema:
|
||||||
|
$ref: "#/definitions/CheckConfigRequest"
|
||||||
|
responses:
|
||||||
|
200:
|
||||||
|
description: OK
|
||||||
|
schema:
|
||||||
|
$ref: "#/definitions/CheckConfigResponse"
|
||||||
|
400:
|
||||||
|
description: "Failed to parse JSON or cannot listen on the specified address"
|
||||||
/install/configure:
|
/install/configure:
|
||||||
post:
|
post:
|
||||||
tags:
|
tags:
|
||||||
|
@ -1320,17 +1340,18 @@ definitions:
|
||||||
type: "integer"
|
type: "integer"
|
||||||
format: "int32"
|
format: "int32"
|
||||||
example: 53
|
example: 53
|
||||||
warning:
|
|
||||||
type: "string"
|
|
||||||
example: "Cannot bind to this port"
|
|
||||||
AddressesInfo:
|
AddressesInfo:
|
||||||
type: "object"
|
type: "object"
|
||||||
description: "AdGuard Home addresses configuration"
|
description: "AdGuard Home addresses configuration"
|
||||||
properties:
|
properties:
|
||||||
dns:
|
dns_port:
|
||||||
$ref: "#/definitions/AddressInfo"
|
type: "integer"
|
||||||
web:
|
format: "int32"
|
||||||
$ref: "#/definitions/AddressInfo"
|
example: 53
|
||||||
|
web_port:
|
||||||
|
type: "integer"
|
||||||
|
format: "int32"
|
||||||
|
example: 80
|
||||||
interfaces:
|
interfaces:
|
||||||
type: "object"
|
type: "object"
|
||||||
description: "Network interfaces dictionary (key is the interface name)"
|
description: "Network interfaces dictionary (key is the interface name)"
|
||||||
|
@ -1353,6 +1374,43 @@ definitions:
|
||||||
items:
|
items:
|
||||||
$ref: "#/definitions/Client"
|
$ref: "#/definitions/Client"
|
||||||
description: "Clients array"
|
description: "Clients array"
|
||||||
|
CheckConfigRequest:
|
||||||
|
type: "object"
|
||||||
|
description: "Configuration to be checked"
|
||||||
|
properties:
|
||||||
|
dns:
|
||||||
|
$ref: "#/definitions/CheckConfigRequestInfo"
|
||||||
|
web:
|
||||||
|
$ref: "#/definitions/CheckConfigRequestInfo"
|
||||||
|
CheckConfigRequestInfo:
|
||||||
|
type: "object"
|
||||||
|
properties:
|
||||||
|
ip:
|
||||||
|
type: "string"
|
||||||
|
example: "127.0.0.1"
|
||||||
|
port:
|
||||||
|
type: "integer"
|
||||||
|
format: "int32"
|
||||||
|
example: 53
|
||||||
|
autofix:
|
||||||
|
type: "boolean"
|
||||||
|
example: false
|
||||||
|
CheckConfigResponse:
|
||||||
|
type: "object"
|
||||||
|
properties:
|
||||||
|
dns:
|
||||||
|
$ref: "#/definitions/CheckConfigResponseInfo"
|
||||||
|
web:
|
||||||
|
$ref: "#/definitions/CheckConfigResponseInfo"
|
||||||
|
CheckConfigResponseInfo:
|
||||||
|
type: "object"
|
||||||
|
properties:
|
||||||
|
status:
|
||||||
|
type: "string"
|
||||||
|
example: ""
|
||||||
|
can_autofix:
|
||||||
|
type: "boolean"
|
||||||
|
example: false
|
||||||
InitialConfiguration:
|
InitialConfiguration:
|
||||||
type: "object"
|
type: "object"
|
||||||
description: "AdGuard Home initial configuration (for the first-install wizard)"
|
description: "AdGuard Home initial configuration (for the first-install wizard)"
|
||||||
|
|
|
@ -0,0 +1,27 @@
|
||||||
|
// +build aix darwin dragonfly freebsd linux netbsd openbsd solaris
|
||||||
|
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"syscall"
|
||||||
|
|
||||||
|
"github.com/AdguardTeam/golibs/log"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Set user-specified limit of how many fd's we can use
|
||||||
|
// https://github.com/AdguardTeam/AdGuardHome/issues/659
|
||||||
|
func setRlimit(val uint) {
|
||||||
|
var rlim syscall.Rlimit
|
||||||
|
rlim.Max = uint64(val)
|
||||||
|
rlim.Cur = uint64(val)
|
||||||
|
err := syscall.Setrlimit(syscall.RLIMIT_NOFILE, &rlim)
|
||||||
|
if err != nil {
|
||||||
|
log.Error("Setrlimit() failed: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if the current user has root (administrator) rights
|
||||||
|
func haveAdminRights() (bool, error) {
|
||||||
|
return os.Getuid() == 0, nil
|
||||||
|
}
|
|
@ -0,0 +1,28 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import "golang.org/x/sys/windows"
|
||||||
|
|
||||||
|
// Set user-specified limit of how many fd's we can use
|
||||||
|
func setRlimit(val uint) {
|
||||||
|
}
|
||||||
|
|
||||||
|
func haveAdminRights() (bool, error) {
|
||||||
|
var token windows.Token
|
||||||
|
h, _ := windows.GetCurrentProcess()
|
||||||
|
err := windows.OpenProcessToken(h, windows.TOKEN_QUERY, &token)
|
||||||
|
if err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
|
||||||
|
info := make([]byte, 4)
|
||||||
|
var returnedLen uint32
|
||||||
|
err = windows.GetTokenInformation(token, windows.TokenElevation, &info[0], uint32(len(info)), &returnedLen)
|
||||||
|
token.Close()
|
||||||
|
if err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
if info[0] == 0 {
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
return true, nil
|
||||||
|
}
|
|
@ -108,6 +108,15 @@ func handleServiceControlAction(action string) {
|
||||||
log.Fatalf("Failed to start the service: %s", err)
|
log.Fatalf("Failed to start the service: %s", err)
|
||||||
}
|
}
|
||||||
log.Printf("Service has been started")
|
log.Printf("Service has been started")
|
||||||
|
|
||||||
|
if detectFirstRun() {
|
||||||
|
log.Printf(`Almost ready!
|
||||||
|
AdGuard Home is successfully installed and will automatically start on boot.
|
||||||
|
There are a few more things that must be configured before you can use it.
|
||||||
|
Click on the link below and follow the Installation Wizard steps to finish setup.`)
|
||||||
|
printHTTPAddresses("http")
|
||||||
|
}
|
||||||
|
|
||||||
} else if action == "uninstall" {
|
} else if action == "uninstall" {
|
||||||
cleanupService()
|
cleanupService()
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue