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
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"crypto/tls"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"net"
|
||||
"net/http"
|
||||
"os"
|
||||
"os/exec"
|
||||
"os/signal"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"syscall"
|
||||
|
||||
|
@ -45,15 +49,6 @@ func main() {
|
|||
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(args)
|
||||
}
|
||||
|
@ -83,6 +78,18 @@ func run(args options) {
|
|||
}
|
||||
|
||||
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
|
||||
err := upgradeConfig()
|
||||
|
@ -161,54 +168,7 @@ func run(args options) {
|
|||
httpsServer.cond = sync.NewCond(&httpsServer.Mutex)
|
||||
|
||||
// for https, we have a separate goroutine loop
|
||||
go func() {
|
||||
for { // this is an endless loop
|
||||
httpsServer.cond.L.Lock()
|
||||
// this mechanism doesn't let us through until all conditions are ment
|
||||
for config.TLS.Enabled == false || config.TLS.PortHTTPS == 0 || config.TLS.PrivateKey == "" || config.TLS.CertificateChain == "" { // sleep until necessary data is supplied
|
||||
httpsServer.cond.Wait()
|
||||
}
|
||||
address := net.JoinHostPort(config.BindHost, strconv.Itoa(config.TLS.PortHTTPS))
|
||||
// validate current TLS config and update warnings (it could have been loaded from file)
|
||||
data := validateCertificates(config.TLS.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)
|
||||
}
|
||||
}
|
||||
}()
|
||||
go httpServerLoop()
|
||||
|
||||
// this loop is used as an ability to change listening host and/or port
|
||||
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
|
||||
func writePIDFile(fn string) bool {
|
||||
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() {
|
||||
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",
|
||||
"compression-webpack-plugin": "^1.1.11",
|
||||
"copy-webpack-plugin": "^4.6.0",
|
||||
"css-loader": "^0.28.11",
|
||||
"css-loader": "^2.1.1",
|
||||
"eslint": "^4.19.1",
|
||||
"eslint-config-airbnb-base": "^12.1.0",
|
||||
"eslint-config-react-app": "^2.1.0",
|
||||
|
|
|
@ -257,5 +257,7 @@
|
|||
"reset_settings": "Reset settings",
|
||||
"update_announcement": "AdGuard Home {{version}} is now available! <0>Click here<\/0> for more info.",
|
||||
"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());
|
||||
}
|
||||
};
|
||||
|
||||
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
|
||||
INSTALL_GET_ADDRESSES = { path: 'install/get_addresses', method: 'GET' };
|
||||
INSTALL_CONFIGURE = { path: 'install/configure', method: 'POST' };
|
||||
INSTALL_CHECK_CONFIG = { path: 'install/check_config', method: 'POST' };
|
||||
|
||||
getDefaultAddresses() {
|
||||
const { path, method } = this.INSTALL_GET_ADDRESSES;
|
||||
|
@ -365,6 +366,15 @@ export default class Api {
|
|||
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
|
||||
TLS_STATUS = { path: 'tls/status', method: 'GET' };
|
||||
TLS_CONFIG = { path: 'tls/configure', method: 'POST' };
|
||||
|
|
|
@ -55,6 +55,8 @@ class Controls extends Component {
|
|||
invalid
|
||||
|| pristine
|
||||
|| install.processingSubmit
|
||||
|| install.dns.status
|
||||
|| install.web.status
|
||||
}
|
||||
>
|
||||
<Trans>next</Trans>
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import React from 'react';
|
||||
import React, { Component, Fragment } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import PropTypes from 'prop-types';
|
||||
import { Field, reduxForm, formValueSelector } from 'redux-form';
|
||||
|
@ -30,10 +30,25 @@ const toNumber = value => value && parseInt(value, 10);
|
|||
const renderInterfaces = (interfaces => (
|
||||
Object.keys(interfaces).map((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 isDown = flags && flags.includes('down');
|
||||
|
||||
if (isDown) {
|
||||
return (
|
||||
<option value={ip} key={name} disabled>
|
||||
<Fragment>
|
||||
{name} - {ip} (<Trans>down</Trans>)
|
||||
</Fragment>
|
||||
</option>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<option value={ip} key={name}>
|
||||
|
@ -46,141 +61,191 @@ const renderInterfaces = (interfaces => (
|
|||
})
|
||||
));
|
||||
|
||||
let Settings = (props) => {
|
||||
const {
|
||||
handleSubmit,
|
||||
webIp,
|
||||
webPort,
|
||||
dnsIp,
|
||||
dnsPort,
|
||||
interfaces,
|
||||
invalid,
|
||||
webWarning,
|
||||
dnsWarning,
|
||||
} = props;
|
||||
class Settings extends Component {
|
||||
componentDidMount() {
|
||||
const { web, dns } = this.props.config;
|
||||
|
||||
return (
|
||||
<form className="setup__step" onSubmit={handleSubmit}>
|
||||
<div className="setup__group">
|
||||
<div className="setup__subtitle">
|
||||
<Trans>install_settings_title</Trans>
|
||||
</div>
|
||||
<div className="row">
|
||||
<div className="col-8">
|
||||
<div className="form-group">
|
||||
<label>
|
||||
<Trans>install_settings_listen</Trans>
|
||||
</label>
|
||||
<Field
|
||||
name="web.ip"
|
||||
component="select"
|
||||
className="form-control custom-select"
|
||||
>
|
||||
<option value={ALL_INTERFACES_IP}>
|
||||
<Trans>install_settings_all_interfaces</Trans>
|
||||
</option>
|
||||
{renderInterfaces(interfaces)}
|
||||
</Field>
|
||||
this.props.validateForm({
|
||||
web,
|
||||
dns,
|
||||
});
|
||||
}
|
||||
|
||||
render() {
|
||||
const {
|
||||
handleSubmit,
|
||||
handleChange,
|
||||
handleAutofix,
|
||||
webIp,
|
||||
webPort,
|
||||
dnsIp,
|
||||
dnsPort,
|
||||
interfaces,
|
||||
invalid,
|
||||
config,
|
||||
} = this.props;
|
||||
const {
|
||||
status: webStatus,
|
||||
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 className="col-4">
|
||||
<div className="form-group">
|
||||
<label>
|
||||
<Trans>install_settings_port</Trans>
|
||||
</label>
|
||||
<Field
|
||||
name="web.port"
|
||||
component={renderField}
|
||||
type="number"
|
||||
className="form-control"
|
||||
placeholder="80"
|
||||
validate={[port, required]}
|
||||
normalize={toNumber}
|
||||
<div className="setup__desc">
|
||||
<Trans>install_settings_interface_link</Trans>
|
||||
<div className="mt-1">
|
||||
<AddressList
|
||||
interfaces={interfaces}
|
||||
address={webIp}
|
||||
port={webPort}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="setup__desc">
|
||||
<Trans>install_settings_interface_link</Trans>
|
||||
<div className="mt-1">
|
||||
<AddressList
|
||||
interfaces={interfaces}
|
||||
address={webIp}
|
||||
port={webPort}
|
||||
/>
|
||||
<div className="setup__group">
|
||||
<div className="setup__subtitle">
|
||||
<Trans>install_settings_dns</Trans>
|
||||
</div>
|
||||
{webWarning &&
|
||||
<div className="text-danger mt-2">
|
||||
{webWarning}
|
||||
<div className="row">
|
||||
<div className="col-8">
|
||||
<div className="form-group">
|
||||
<label>
|
||||
<Trans>install_settings_listen</Trans>
|
||||
</label>
|
||||
<Field
|
||||
name="dns.ip"
|
||||
component="select"
|
||||
className="form-control custom-select"
|
||||
onChange={handleChange}
|
||||
>
|
||||
<option value={ALL_INTERFACES_IP}>
|
||||
<Trans>install_settings_all_interfaces</Trans>
|
||||
</option>
|
||||
{renderInterfaces(interfaces)}
|
||||
</Field>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
<div className="setup__group">
|
||||
<div className="setup__subtitle">
|
||||
<Trans>install_settings_dns</Trans>
|
||||
</div>
|
||||
<div className="row">
|
||||
<div className="col-8">
|
||||
<div className="form-group">
|
||||
<label>
|
||||
<Trans>install_settings_listen</Trans>
|
||||
</label>
|
||||
<Field
|
||||
name="dns.ip"
|
||||
component="select"
|
||||
className="form-control custom-select"
|
||||
>
|
||||
<option value={ALL_INTERFACES_IP}>
|
||||
<Trans>install_settings_all_interfaces</Trans>
|
||||
</option>
|
||||
{renderInterfaces(interfaces)}
|
||||
</Field>
|
||||
<div className="col-4">
|
||||
<div className="form-group">
|
||||
<label>
|
||||
<Trans>install_settings_port</Trans>
|
||||
</label>
|
||||
<Field
|
||||
name="dns.port"
|
||||
component={renderField}
|
||||
type="number"
|
||||
className="form-control"
|
||||
placeholder="80"
|
||||
validate={[port, required]}
|
||||
normalize={toNumber}
|
||||
onChange={handleChange}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="col-12">
|
||||
{dnsStatus &&
|
||||
<div className="setup__error text-danger">
|
||||
{dnsStatus}
|
||||
{isDnsFixAvailable &&
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-secondary btn-sm ml-2"
|
||||
onClick={() => handleAutofix('dns', dnsIp, dnsPort)}
|
||||
>
|
||||
<Trans>fix</Trans>
|
||||
</button>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
<div className="col-4">
|
||||
<div className="form-group">
|
||||
<label>
|
||||
<Trans>install_settings_port</Trans>
|
||||
</label>
|
||||
<Field
|
||||
name="dns.port"
|
||||
component={renderField}
|
||||
type="number"
|
||||
className="form-control"
|
||||
placeholder="80"
|
||||
validate={[port, required]}
|
||||
normalize={toNumber}
|
||||
<div className="setup__desc">
|
||||
<Trans>install_settings_dns_desc</Trans>
|
||||
<div className="mt-1">
|
||||
<AddressList
|
||||
interfaces={interfaces}
|
||||
address={dnsIp}
|
||||
port={dnsPort}
|
||||
isDns={true}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="setup__desc">
|
||||
<Trans>install_settings_dns_desc</Trans>
|
||||
<div className="mt-1">
|
||||
<AddressList
|
||||
interfaces={interfaces}
|
||||
address={dnsIp}
|
||||
port={dnsPort}
|
||||
isDns={true}
|
||||
/>
|
||||
</div>
|
||||
{dnsWarning &&
|
||||
<div className="text-danger mt-2">
|
||||
{dnsWarning}
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
<Controls invalid={invalid} />
|
||||
</form>
|
||||
);
|
||||
};
|
||||
<Controls invalid={invalid} />
|
||||
</form>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Settings.propTypes = {
|
||||
handleSubmit: PropTypes.func.isRequired,
|
||||
handleChange: PropTypes.func,
|
||||
handleAutofix: PropTypes.func,
|
||||
validateForm: PropTypes.func,
|
||||
webIp: PropTypes.string.isRequired,
|
||||
dnsIp: PropTypes.string.isRequired,
|
||||
config: PropTypes.object.isRequired,
|
||||
webPort: PropTypes.oneOfType([
|
||||
PropTypes.string,
|
||||
PropTypes.number,
|
||||
|
@ -189,8 +254,6 @@ Settings.propTypes = {
|
|||
PropTypes.string,
|
||||
PropTypes.number,
|
||||
]),
|
||||
webWarning: PropTypes.string.isRequired,
|
||||
dnsWarning: PropTypes.string.isRequired,
|
||||
interfaces: PropTypes.object.isRequired,
|
||||
invalid: PropTypes.bool.isRequired,
|
||||
initialValues: PropTypes.object,
|
||||
|
@ -198,7 +261,7 @@ Settings.propTypes = {
|
|||
|
||||
const selector = formValueSelector('install');
|
||||
|
||||
Settings = connect((state) => {
|
||||
const SettingsForm = connect((state) => {
|
||||
const webIp = selector(state, 'web.ip');
|
||||
const webPort = selector(state, 'web.port');
|
||||
const dnsIp = selector(state, 'dns.ip');
|
||||
|
@ -219,4 +282,4 @@ export default flow([
|
|||
destroyOnUnmount: false,
|
||||
forceUnregisterOnUnmount: true,
|
||||
}),
|
||||
])(Settings);
|
||||
])(SettingsForm);
|
||||
|
|
|
@ -115,3 +115,7 @@
|
|||
padding-left: 30px;
|
||||
padding-right: 30px;
|
||||
}
|
||||
|
||||
.setup__error {
|
||||
margin: -5px 0 5px;
|
||||
}
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import React, { Component, Fragment } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import PropTypes from 'prop-types';
|
||||
import debounce from 'lodash/debounce';
|
||||
|
||||
import * as actionCreators from '../../actions/install';
|
||||
import { getWebAddress } from '../../helpers/helpers';
|
||||
|
@ -8,6 +9,7 @@ import {
|
|||
INSTALL_FIRST_STEP,
|
||||
INSTALL_TOTAL_STEPS,
|
||||
ALL_INTERFACES_IP,
|
||||
DEBOUNCE_TIMEOUT,
|
||||
} from '../../helpers/constants';
|
||||
|
||||
import Loading from '../../components/ui/Loading';
|
||||
|
@ -34,6 +36,30 @@ class Setup extends Component {
|
|||
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) => {
|
||||
let address = getWebAddress(ip, port);
|
||||
|
||||
|
@ -63,11 +89,13 @@ class Setup extends Component {
|
|||
case 2:
|
||||
return (
|
||||
<Settings
|
||||
config={config}
|
||||
initialValues={config}
|
||||
interfaces={interfaces}
|
||||
webWarning={config.web.warning}
|
||||
dnsWarning={config.dns.warning}
|
||||
onSubmit={this.nextStep}
|
||||
onChange={this.handleFormChange}
|
||||
validateForm={this.handleFormChange}
|
||||
handleAutofix={this.handleAutofix}
|
||||
/>
|
||||
);
|
||||
case 3:
|
||||
|
@ -116,6 +144,7 @@ class Setup extends Component {
|
|||
Setup.propTypes = {
|
||||
getDefaultAddresses: PropTypes.func.isRequired,
|
||||
setAllSettings: PropTypes.func.isRequired,
|
||||
checkConfig: PropTypes.func.isRequired,
|
||||
nextStep: PropTypes.func.isRequired,
|
||||
prevStep: PropTypes.func.isRequired,
|
||||
install: PropTypes.object.isRequired,
|
||||
|
|
|
@ -10,10 +10,13 @@ const install = handleActions({
|
|||
[actions.getDefaultAddressesRequest]: state => ({ ...state, processingDefault: true }),
|
||||
[actions.getDefaultAddressesFailure]: state => ({ ...state, processingDefault: false }),
|
||||
[actions.getDefaultAddressesSuccess]: (state, { payload }) => {
|
||||
const values = payload;
|
||||
values.web.ip = state.web.ip;
|
||||
values.dns.ip = state.dns.ip;
|
||||
const newState = { ...state, ...values, processingDefault: false };
|
||||
const { interfaces } = payload;
|
||||
const web = { ...state.web, port: payload.web_port };
|
||||
const dns = { ...state.dns, port: payload.dns_port };
|
||||
|
||||
const newState = {
|
||||
...state, web, dns, interfaces, processingDefault: false,
|
||||
};
|
||||
return newState;
|
||||
},
|
||||
|
||||
|
@ -23,19 +26,34 @@ const install = handleActions({
|
|||
[actions.setAllSettingsRequest]: state => ({ ...state, processingSubmit: true }),
|
||||
[actions.setAllSettingsFailure]: 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,
|
||||
processingDefault: true,
|
||||
processingSubmit: false,
|
||||
processingCheck: false,
|
||||
web: {
|
||||
ip: '0.0.0.0',
|
||||
port: 80,
|
||||
warning: '',
|
||||
status: '',
|
||||
can_autofix: false,
|
||||
},
|
||||
dns: {
|
||||
ip: '0.0.0.0',
|
||||
port: 53,
|
||||
warning: '',
|
||||
status: '',
|
||||
can_autofix: false,
|
||||
},
|
||||
interfaces: {},
|
||||
});
|
||||
|
|
112
control.go
112
control.go
|
@ -2,7 +2,6 @@ package main
|
|||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"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
|
||||
// --------------
|
||||
|
@ -1097,11 +990,6 @@ func handleDOH(w http.ResponseWriter, r *http.Request) {
|
|||
// ------------------------
|
||||
// registration of handlers
|
||||
// ------------------------
|
||||
func registerInstallHandlers() {
|
||||
http.HandleFunc("/control/install/get_addresses", preInstall(ensureGET(handleInstallGetAddresses)))
|
||||
http.HandleFunc("/control/install/configure", preInstall(ensurePOST(handleInstallConfigure)))
|
||||
}
|
||||
|
||||
func registerControlHandlers() {
|
||||
http.HandleFunc("/control/status", postInstall(optionalAuth(ensureGET(handleStatus))))
|
||||
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"
|
||||
"strconv"
|
||||
"strings"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"github.com/AdguardTeam/dnsproxy/upstream"
|
||||
|
@ -346,6 +347,31 @@ func customDialContext(ctx context.Context, network, addr string) (net.Conn, err
|
|||
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
|
||||
// ---------------------
|
||||
|
|
|
@ -739,6 +739,26 @@ paths:
|
|||
description: OK
|
||||
schema:
|
||||
$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:
|
||||
post:
|
||||
tags:
|
||||
|
@ -1320,17 +1340,18 @@ definitions:
|
|||
type: "integer"
|
||||
format: "int32"
|
||||
example: 53
|
||||
warning:
|
||||
type: "string"
|
||||
example: "Cannot bind to this port"
|
||||
AddressesInfo:
|
||||
type: "object"
|
||||
description: "AdGuard Home addresses configuration"
|
||||
properties:
|
||||
dns:
|
||||
$ref: "#/definitions/AddressInfo"
|
||||
web:
|
||||
$ref: "#/definitions/AddressInfo"
|
||||
dns_port:
|
||||
type: "integer"
|
||||
format: "int32"
|
||||
example: 53
|
||||
web_port:
|
||||
type: "integer"
|
||||
format: "int32"
|
||||
example: 80
|
||||
interfaces:
|
||||
type: "object"
|
||||
description: "Network interfaces dictionary (key is the interface name)"
|
||||
|
@ -1353,6 +1374,43 @@ definitions:
|
|||
items:
|
||||
$ref: "#/definitions/Client"
|
||||
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:
|
||||
type: "object"
|
||||
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.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" {
|
||||
cleanupService()
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue