Merge: Update by command from UI
Close #428 * commit '70e329956776cc381fdb28805375d5b2f0e22dbf': * openapi: update * client: add link to the update error * client: add update timeout * client: add error message if update failed + client: handle update * go linter * control: /version.json: use new JSON format + set config.runningAsService * app: --help: more pretty help info + app: add --check-config command-line argument * app: optimize config file reading + /control/update handler * control: don't use custom resolver for tests + doc: Update algorithm - control: fix race in /control/version.json handler
This commit is contained in:
commit
aa2d942783
|
@ -9,6 +9,9 @@ Contents:
|
||||||
* "Check configuration" command
|
* "Check configuration" command
|
||||||
* Disable DNSStubListener
|
* Disable DNSStubListener
|
||||||
* "Apply configuration" command
|
* "Apply configuration" command
|
||||||
|
* Updating
|
||||||
|
* Get version command
|
||||||
|
* Update command
|
||||||
* Enable DHCP server
|
* Enable DHCP server
|
||||||
* "Check DHCP" command
|
* "Check DHCP" command
|
||||||
* "Enable DHCP" command
|
* "Enable DHCP" command
|
||||||
|
@ -187,6 +190,92 @@ On error, server responds with code 400 or 500. In this case UI should show err
|
||||||
ERROR MESSAGE
|
ERROR MESSAGE
|
||||||
|
|
||||||
|
|
||||||
|
## Updating
|
||||||
|
|
||||||
|
Algorithm of an update by command:
|
||||||
|
|
||||||
|
* UI requests the latest version information from Server
|
||||||
|
* Server requests information from Internet; stores the data in cache for several hours; sends data to UI
|
||||||
|
* If UI sees that a new version is available, it shows notification message and "Update Now" button
|
||||||
|
* When user clicks on "Update Now" button, UI sends Update command to Server
|
||||||
|
* UI shows "Please wait, AGH is being updated..." message
|
||||||
|
* Server performs an update:
|
||||||
|
* Use working directory from `--work-dir` if necessary
|
||||||
|
* Download new package for the current OS and CPU
|
||||||
|
* Unpack the package to a temporary directory `update-vXXX`
|
||||||
|
* Copy the current configuration file to the directory we unpacked new AGH to
|
||||||
|
* Check configuration compatibility by executing `./AGH --check-config`. If this command fails, we won't be able to update.
|
||||||
|
* Create `backup-vXXX` directory and copy the current configuration file there
|
||||||
|
* Stop all tasks, including DNS server, DHCP server, HTTP server
|
||||||
|
* Move the current binary file to backup directory
|
||||||
|
* Note: if power fails here, AGH won't be able to start at system boot. Administrator has to fix it manually
|
||||||
|
* Move new binary file to the current directory
|
||||||
|
* If AGH is running as a service, use service control functionality to restart
|
||||||
|
* If AGH is not running as a service, use the current process arguments to start a new process
|
||||||
|
* Exit process
|
||||||
|
* UI resends Get Status command until Server responds to it with the new version. This means that Server is successfully restarted after update.
|
||||||
|
* UI reloads itself
|
||||||
|
|
||||||
|
|
||||||
|
### Get version command
|
||||||
|
|
||||||
|
On receiving this request server downloads version.json data from github and stores it in cache for several hours.
|
||||||
|
|
||||||
|
Example of version.json data:
|
||||||
|
|
||||||
|
{
|
||||||
|
"version": "v0.95-hotfix",
|
||||||
|
"announcement": "AdGuard Home v0.95-hotfix is now available!",
|
||||||
|
"announcement_url": "",
|
||||||
|
"download_windows_amd64": "",
|
||||||
|
"download_windows_386": "",
|
||||||
|
"download_darwin_amd64": "",
|
||||||
|
"download_linux_amd64": "",
|
||||||
|
"download_linux_386": "",
|
||||||
|
"download_linux_arm": "",
|
||||||
|
"download_linux_arm64": "",
|
||||||
|
"download_linux_mips": "",
|
||||||
|
"download_linux_mipsle": "",
|
||||||
|
"selfupdate_min_version": "v0.0"
|
||||||
|
}
|
||||||
|
|
||||||
|
Request:
|
||||||
|
|
||||||
|
GET /control/version.json
|
||||||
|
|
||||||
|
Response:
|
||||||
|
|
||||||
|
200 OK
|
||||||
|
|
||||||
|
{
|
||||||
|
"new_version": "v0.95",
|
||||||
|
"announcement": "AdGuard Home v0.95 is now available!",
|
||||||
|
"announcement_url": "http://...",
|
||||||
|
"can_autoupdate": true
|
||||||
|
}
|
||||||
|
|
||||||
|
If `can_autoupdate` is true, then the server can automatically upgrade to a new version.
|
||||||
|
|
||||||
|
|
||||||
|
### Update command
|
||||||
|
|
||||||
|
Perform an update procedure to the latest available version
|
||||||
|
|
||||||
|
Request:
|
||||||
|
|
||||||
|
POST /control/update
|
||||||
|
|
||||||
|
Response:
|
||||||
|
|
||||||
|
200 OK
|
||||||
|
|
||||||
|
Error response:
|
||||||
|
|
||||||
|
500
|
||||||
|
|
||||||
|
UI shows error message "Auto-update has failed"
|
||||||
|
|
||||||
|
|
||||||
## Enable DHCP server
|
## Enable DHCP server
|
||||||
|
|
||||||
Algorithm:
|
Algorithm:
|
||||||
|
|
83
app.go
83
app.go
|
@ -2,6 +2,7 @@ package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bufio"
|
"bufio"
|
||||||
|
"context"
|
||||||
"crypto/tls"
|
"crypto/tls"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
|
@ -30,6 +31,7 @@ var httpsServer struct {
|
||||||
server *http.Server
|
server *http.Server
|
||||||
cond *sync.Cond // reacts to config.TLS.Enabled, PortHTTPS, CertificateChain and PrivateKey
|
cond *sync.Cond // reacts to config.TLS.Enabled, PortHTTPS, CertificateChain and PrivateKey
|
||||||
sync.Mutex // protects config.TLS
|
sync.Mutex // protects config.TLS
|
||||||
|
shutdown bool // if TRUE, don't restart the server
|
||||||
}
|
}
|
||||||
var pidFileName string // PID file name. Empty if no PID file was created.
|
var pidFileName string // PID file name. Empty if no PID file was created.
|
||||||
|
|
||||||
|
@ -76,6 +78,7 @@ func run(args options) {
|
||||||
if args.runningAsService {
|
if args.runningAsService {
|
||||||
log.Info("AdGuard Home is running as a service")
|
log.Info("AdGuard Home is running as a service")
|
||||||
}
|
}
|
||||||
|
config.runningAsService = args.runningAsService
|
||||||
|
|
||||||
config.firstRun = detectFirstRun()
|
config.firstRun = detectFirstRun()
|
||||||
if config.firstRun {
|
if config.firstRun {
|
||||||
|
@ -91,16 +94,22 @@ func run(args options) {
|
||||||
os.Exit(0)
|
os.Exit(0)
|
||||||
}()
|
}()
|
||||||
|
|
||||||
// Do the upgrade if necessary
|
if !config.firstRun {
|
||||||
err := upgradeConfig()
|
// Do the upgrade if necessary
|
||||||
if err != nil {
|
err := upgradeConfig()
|
||||||
log.Fatal(err)
|
if err != nil {
|
||||||
}
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
// parse from config file
|
err = parseConfig()
|
||||||
err = parseConfig()
|
if err != nil {
|
||||||
if err != nil {
|
os.Exit(1)
|
||||||
log.Fatal(err)
|
}
|
||||||
|
|
||||||
|
if args.checkConfig {
|
||||||
|
log.Info("Configuration file is OK")
|
||||||
|
os.Exit(0)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (runtime.GOOS == "linux" || runtime.GOOS == "darwin") &&
|
if (runtime.GOOS == "linux" || runtime.GOOS == "darwin") &&
|
||||||
|
@ -118,10 +127,12 @@ func run(args options) {
|
||||||
|
|
||||||
loadFilters()
|
loadFilters()
|
||||||
|
|
||||||
// Save the updated config
|
if !config.firstRun {
|
||||||
err = config.write()
|
// Save the updated config
|
||||||
if err != nil {
|
err := config.write()
|
||||||
log.Fatal(err)
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Init the DNS server instance before registering HTTP handlers
|
// Init the DNS server instance before registering HTTP handlers
|
||||||
|
@ -129,7 +140,7 @@ func run(args options) {
|
||||||
initDNSServer(dnsBaseDir)
|
initDNSServer(dnsBaseDir)
|
||||||
|
|
||||||
if !config.firstRun {
|
if !config.firstRun {
|
||||||
err = startDNSServer()
|
err := startDNSServer()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatal(err)
|
log.Fatal(err)
|
||||||
}
|
}
|
||||||
|
@ -171,7 +182,7 @@ func run(args options) {
|
||||||
go httpServerLoop()
|
go httpServerLoop()
|
||||||
|
|
||||||
// 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 !httpsServer.shutdown {
|
||||||
printHTTPAddresses("http")
|
printHTTPAddresses("http")
|
||||||
|
|
||||||
// we need to have new instance, because after Shutdown() the Server is not usable
|
// we need to have new instance, because after Shutdown() the Server is not usable
|
||||||
|
@ -186,10 +197,13 @@ func run(args options) {
|
||||||
}
|
}
|
||||||
// We use ErrServerClosed as a sign that we need to rebind on new address, so go back to the start of the loop
|
// We use ErrServerClosed as a sign that we need to rebind on new address, so go back to the start of the loop
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// wait indefinitely for other go-routines to complete their job
|
||||||
|
select {}
|
||||||
}
|
}
|
||||||
|
|
||||||
func httpServerLoop() {
|
func httpServerLoop() {
|
||||||
for {
|
for !httpsServer.shutdown {
|
||||||
httpsServer.cond.L.Lock()
|
httpsServer.cond.L.Lock()
|
||||||
// this mechanism doesn't let us through until all conditions are met
|
// this mechanism doesn't let us through until all conditions are met
|
||||||
for config.TLS.Enabled == false ||
|
for config.TLS.Enabled == false ||
|
||||||
|
@ -367,6 +381,15 @@ func cleanup() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Stop HTTP server, possibly waiting for all active connections to be closed
|
||||||
|
func stopHTTPServer() {
|
||||||
|
httpsServer.shutdown = true
|
||||||
|
if httpsServer.server != nil {
|
||||||
|
httpsServer.server.Shutdown(context.TODO())
|
||||||
|
}
|
||||||
|
httpServer.Shutdown(context.TODO())
|
||||||
|
}
|
||||||
|
|
||||||
// This function is called before application exits
|
// This function is called before application exits
|
||||||
func cleanupAlways() {
|
func cleanupAlways() {
|
||||||
if len(pidFileName) != 0 {
|
if len(pidFileName) != 0 {
|
||||||
|
@ -384,6 +407,7 @@ type options struct {
|
||||||
bindPort int // port to serve HTTP pages on
|
bindPort int // port to serve HTTP pages on
|
||||||
logFile string // Path to the log file. If empty, write to stdout. If "syslog", writes to syslog
|
logFile string // Path to the log file. If empty, write to stdout. If "syslog", writes to syslog
|
||||||
pidFile string // File name to save PID to
|
pidFile string // File name to save PID to
|
||||||
|
checkConfig bool // Check configuration and exit
|
||||||
|
|
||||||
// service control action (see service.ControlAction array + "status" command)
|
// service control action (see service.ControlAction array + "status" command)
|
||||||
serviceControlAction string
|
serviceControlAction string
|
||||||
|
@ -404,25 +428,26 @@ func loadOptions() options {
|
||||||
callbackWithValue func(value string)
|
callbackWithValue func(value string)
|
||||||
callbackNoValue func()
|
callbackNoValue func()
|
||||||
}{
|
}{
|
||||||
{"config", "c", "path to the config file", func(value string) { o.configFilename = value }, nil},
|
{"config", "c", "Path to the config file", func(value string) { o.configFilename = value }, nil},
|
||||||
{"work-dir", "w", "path to the working directory", func(value string) { o.workDir = value }, nil},
|
{"work-dir", "w", "Path to the working directory", func(value string) { o.workDir = value }, nil},
|
||||||
{"host", "h", "host address to bind HTTP server on", func(value string) { o.bindHost = value }, nil},
|
{"host", "h", "Host address to bind HTTP server on", func(value string) { o.bindHost = value }, nil},
|
||||||
{"port", "p", "port to serve HTTP pages on", func(value string) {
|
{"port", "p", "Port to serve HTTP pages on", func(value string) {
|
||||||
v, err := strconv.Atoi(value)
|
v, err := strconv.Atoi(value)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic("Got port that is not a number")
|
panic("Got port that is not a number")
|
||||||
}
|
}
|
||||||
o.bindPort = v
|
o.bindPort = v
|
||||||
}, nil},
|
}, nil},
|
||||||
{"service", "s", "service control action: status, install, uninstall, start, stop, restart", func(value string) {
|
{"service", "s", "Service control action: status, install, uninstall, start, stop, restart", func(value string) {
|
||||||
o.serviceControlAction = value
|
o.serviceControlAction = value
|
||||||
}, nil},
|
}, nil},
|
||||||
{"logfile", "l", "path to the log file. If empty, writes to stdout, if 'syslog' -- system log", func(value string) {
|
{"logfile", "l", "Path to log file. If empty: write to stdout; if 'syslog': write to system log", func(value string) {
|
||||||
o.logFile = value
|
o.logFile = value
|
||||||
}, nil},
|
}, nil},
|
||||||
{"pidfile", "", "File name to save PID to", func(value string) { o.pidFile = value }, nil},
|
{"pidfile", "", "Path to a file where PID is stored", func(value string) { o.pidFile = value }, nil},
|
||||||
{"verbose", "v", "enable verbose output", nil, func() { o.verbose = true }},
|
{"check-config", "", "Check configuration and exit", nil, func() { o.checkConfig = true }},
|
||||||
{"help", "", "print this help", nil, func() {
|
{"verbose", "v", "Enable verbose output", nil, func() { o.verbose = true }},
|
||||||
|
{"help", "", "Print this help", nil, func() {
|
||||||
printHelp()
|
printHelp()
|
||||||
os.Exit(64)
|
os.Exit(64)
|
||||||
}},
|
}},
|
||||||
|
@ -432,10 +457,14 @@ func loadOptions() options {
|
||||||
fmt.Printf("%s [options]\n\n", os.Args[0])
|
fmt.Printf("%s [options]\n\n", os.Args[0])
|
||||||
fmt.Printf("Options:\n")
|
fmt.Printf("Options:\n")
|
||||||
for _, opt := range opts {
|
for _, opt := range opts {
|
||||||
|
val := ""
|
||||||
|
if opt.callbackWithValue != nil {
|
||||||
|
val = " VALUE"
|
||||||
|
}
|
||||||
if opt.shortName != "" {
|
if opt.shortName != "" {
|
||||||
fmt.Printf(" -%s, %-30s %s\n", opt.shortName, "--"+opt.longName, opt.description)
|
fmt.Printf(" -%s, %-30s %s\n", opt.shortName, "--"+opt.longName+val, opt.description)
|
||||||
} else {
|
} else {
|
||||||
fmt.Printf(" %-34s %s\n", "--"+opt.longName, opt.description)
|
fmt.Printf(" %-34s %s\n", "--"+opt.longName+val, opt.description)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -260,5 +260,8 @@
|
||||||
"dns_addresses": "DNS addresses",
|
"dns_addresses": "DNS addresses",
|
||||||
"down": "Down",
|
"down": "Down",
|
||||||
"fix": "Fix",
|
"fix": "Fix",
|
||||||
"dns_providers": "Here is a <0>list of known DNS providers</0> to choose from."
|
"dns_providers": "Here is a <0>list of known DNS providers</0> to choose from.",
|
||||||
|
"update_now": "Update now",
|
||||||
|
"update_failed": "Auto-update failed. Please <a href='https://github.com/AdguardTeam/AdGuardHome/wiki/Getting-Started#update'>follow the steps<\/a> to update manually.",
|
||||||
|
"processing_update": "Please wait, AdGuard Home is being updated"
|
||||||
}
|
}
|
|
@ -2,15 +2,17 @@ import { createAction } from 'redux-actions';
|
||||||
import round from 'lodash/round';
|
import round from 'lodash/round';
|
||||||
import { t } from 'i18next';
|
import { t } from 'i18next';
|
||||||
import { showLoading, hideLoading } from 'react-redux-loading-bar';
|
import { showLoading, hideLoading } from 'react-redux-loading-bar';
|
||||||
|
import axios from 'axios';
|
||||||
|
|
||||||
import { normalizeHistory, normalizeFilteringStatus, normalizeLogs, normalizeTextarea } from '../helpers/helpers';
|
import { normalizeHistory, normalizeFilteringStatus, normalizeLogs, normalizeTextarea } from '../helpers/helpers';
|
||||||
import { SETTINGS_NAMES } from '../helpers/constants';
|
import { SETTINGS_NAMES, CHECK_TIMEOUT } from '../helpers/constants';
|
||||||
import Api from '../api/Api';
|
import Api from '../api/Api';
|
||||||
|
|
||||||
const apiClient = new Api();
|
const apiClient = new Api();
|
||||||
|
|
||||||
export const addErrorToast = createAction('ADD_ERROR_TOAST');
|
export const addErrorToast = createAction('ADD_ERROR_TOAST');
|
||||||
export const addSuccessToast = createAction('ADD_SUCCESS_TOAST');
|
export const addSuccessToast = createAction('ADD_SUCCESS_TOAST');
|
||||||
|
export const addNoticeToast = createAction('ADD_NOTICE_TOAST');
|
||||||
export const removeToast = createAction('REMOVE_TOAST');
|
export const removeToast = createAction('REMOVE_TOAST');
|
||||||
|
|
||||||
export const toggleSettingStatus = createAction('SETTING_STATUS_TOGGLE');
|
export const toggleSettingStatus = createAction('SETTING_STATUS_TOGGLE');
|
||||||
|
@ -154,6 +156,56 @@ export const getVersion = () => async (dispatch) => {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const getUpdateRequest = createAction('GET_UPDATE_REQUEST');
|
||||||
|
export const getUpdateFailure = createAction('GET_UPDATE_FAILURE');
|
||||||
|
export const getUpdateSuccess = createAction('GET_UPDATE_SUCCESS');
|
||||||
|
|
||||||
|
export const getUpdate = () => async (dispatch) => {
|
||||||
|
dispatch(getUpdateRequest());
|
||||||
|
try {
|
||||||
|
await apiClient.getUpdate();
|
||||||
|
|
||||||
|
const checkUpdate = async (attempts) => {
|
||||||
|
let count = attempts || 1;
|
||||||
|
let timeout;
|
||||||
|
|
||||||
|
if (count > 60) {
|
||||||
|
dispatch(addNoticeToast({ error: 'update_failed' }));
|
||||||
|
dispatch(getUpdateFailure());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const rmTimeout = t => t && clearTimeout(t);
|
||||||
|
const setRecursiveTimeout = (time, ...args) => setTimeout(
|
||||||
|
checkUpdate,
|
||||||
|
time,
|
||||||
|
...args,
|
||||||
|
);
|
||||||
|
|
||||||
|
axios.get('control/status')
|
||||||
|
.then((response) => {
|
||||||
|
rmTimeout(timeout);
|
||||||
|
if (response) {
|
||||||
|
dispatch(getUpdateSuccess());
|
||||||
|
window.location.reload(true);
|
||||||
|
}
|
||||||
|
timeout = setRecursiveTimeout(CHECK_TIMEOUT, count += 1);
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
rmTimeout(timeout);
|
||||||
|
timeout = setRecursiveTimeout(CHECK_TIMEOUT, count += 1);
|
||||||
|
});
|
||||||
|
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
|
||||||
|
checkUpdate();
|
||||||
|
} catch (error) {
|
||||||
|
dispatch(addNoticeToast({ error: 'update_failed' }));
|
||||||
|
dispatch(getUpdateFailure());
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
export const getClientsRequest = createAction('GET_CLIENTS_REQUEST');
|
export const getClientsRequest = createAction('GET_CLIENTS_REQUEST');
|
||||||
export const getClientsFailure = createAction('GET_CLIENTS_FAILURE');
|
export const getClientsFailure = createAction('GET_CLIENTS_FAILURE');
|
||||||
export const getClientsSuccess = createAction('GET_CLIENTS_SUCCESS');
|
export const getClientsSuccess = createAction('GET_CLIENTS_SUCCESS');
|
||||||
|
|
|
@ -40,6 +40,8 @@ export default class Api {
|
||||||
GLOBAL_ENABLE_PROTECTION = { path: 'enable_protection', method: 'POST' };
|
GLOBAL_ENABLE_PROTECTION = { path: 'enable_protection', method: 'POST' };
|
||||||
GLOBAL_DISABLE_PROTECTION = { path: 'disable_protection', method: 'POST' };
|
GLOBAL_DISABLE_PROTECTION = { path: 'disable_protection', method: 'POST' };
|
||||||
GLOBAL_CLIENTS = { path: 'clients', method: 'GET' }
|
GLOBAL_CLIENTS = { path: 'clients', method: 'GET' }
|
||||||
|
GLOBAL_CLIENTS = { path: 'clients', method: 'GET' };
|
||||||
|
GLOBAL_UPDATE = { path: 'update', method: 'POST' };
|
||||||
|
|
||||||
restartGlobalFiltering() {
|
restartGlobalFiltering() {
|
||||||
const { path, method } = this.GLOBAL_RESTART;
|
const { path, method } = this.GLOBAL_RESTART;
|
||||||
|
@ -145,6 +147,11 @@ export default class Api {
|
||||||
return this.makeRequest(path, method);
|
return this.makeRequest(path, method);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getUpdate() {
|
||||||
|
const { path, method } = this.GLOBAL_UPDATE;
|
||||||
|
return this.makeRequest(path, method);
|
||||||
|
}
|
||||||
|
|
||||||
// Filtering
|
// Filtering
|
||||||
FILTERING_STATUS = { path: 'filtering/status', method: 'GET' };
|
FILTERING_STATUS = { path: 'filtering/status', method: 'GET' };
|
||||||
FILTERING_ENABLE = { path: 'filtering/enable', method: 'POST' };
|
FILTERING_ENABLE = { path: 'filtering/enable', method: 'POST' };
|
||||||
|
|
|
@ -19,6 +19,7 @@ import Toasts from '../Toasts';
|
||||||
import Footer from '../ui/Footer';
|
import Footer from '../ui/Footer';
|
||||||
import Status from '../ui/Status';
|
import Status from '../ui/Status';
|
||||||
import UpdateTopline from '../ui/UpdateTopline';
|
import UpdateTopline from '../ui/UpdateTopline';
|
||||||
|
import UpdateOverlay from '../ui/UpdateOverlay';
|
||||||
import EncryptionTopline from '../ui/EncryptionTopline';
|
import EncryptionTopline from '../ui/EncryptionTopline';
|
||||||
import i18n from '../../i18n';
|
import i18n from '../../i18n';
|
||||||
|
|
||||||
|
@ -37,6 +38,10 @@ class App extends Component {
|
||||||
this.props.enableDns();
|
this.props.enableDns();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
handleUpdate = () => {
|
||||||
|
this.props.getUpdate();
|
||||||
|
}
|
||||||
|
|
||||||
setLanguage = () => {
|
setLanguage = () => {
|
||||||
const { processing, language } = this.props.dashboard;
|
const { processing, language } = this.props.dashboard;
|
||||||
|
|
||||||
|
@ -62,10 +67,16 @@ class App extends Component {
|
||||||
<HashRouter hashType='noslash'>
|
<HashRouter hashType='noslash'>
|
||||||
<Fragment>
|
<Fragment>
|
||||||
{updateAvailable &&
|
{updateAvailable &&
|
||||||
<UpdateTopline
|
<Fragment>
|
||||||
url={dashboard.announcementUrl}
|
<UpdateTopline
|
||||||
version={dashboard.version}
|
url={dashboard.announcementUrl}
|
||||||
/>
|
version={dashboard.newVersion}
|
||||||
|
canAutoUpdate={dashboard.canAutoUpdate}
|
||||||
|
getUpdate={this.handleUpdate}
|
||||||
|
processingUpdate={dashboard.processingUpdate}
|
||||||
|
/>
|
||||||
|
<UpdateOverlay processingUpdate={dashboard.processingUpdate} />
|
||||||
|
</Fragment>
|
||||||
}
|
}
|
||||||
{!encryption.processing &&
|
{!encryption.processing &&
|
||||||
<EncryptionTopline notAfter={encryption.not_after} />
|
<EncryptionTopline notAfter={encryption.not_after} />
|
||||||
|
@ -100,6 +111,7 @@ class App extends Component {
|
||||||
|
|
||||||
App.propTypes = {
|
App.propTypes = {
|
||||||
getDnsStatus: PropTypes.func,
|
getDnsStatus: PropTypes.func,
|
||||||
|
getUpdate: PropTypes.func,
|
||||||
enableDns: PropTypes.func,
|
enableDns: PropTypes.func,
|
||||||
dashboard: PropTypes.object,
|
dashboard: PropTypes.object,
|
||||||
isCoreRunning: PropTypes.bool,
|
isCoreRunning: PropTypes.bool,
|
||||||
|
|
|
@ -32,6 +32,12 @@
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.toast__content a {
|
||||||
|
font-weight: 600;
|
||||||
|
color: #fff;
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
|
||||||
.toast__dismiss {
|
.toast__dismiss {
|
||||||
display: block;
|
display: block;
|
||||||
flex: 0 0 auto;
|
flex: 0 0 auto;
|
||||||
|
|
|
@ -4,7 +4,7 @@ import { Trans, withNamespaces } from 'react-i18next';
|
||||||
|
|
||||||
class Toast extends Component {
|
class Toast extends Component {
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
const timeout = this.props.type === 'error' ? 30000 : 5000;
|
const timeout = this.props.type === 'success' ? 5000 : 30000;
|
||||||
|
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
this.props.removeToast(this.props.id);
|
this.props.removeToast(this.props.id);
|
||||||
|
@ -15,13 +15,25 @@ class Toast extends Component {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
showMessage(t, type, message) {
|
||||||
|
if (type === 'notice') {
|
||||||
|
return <span dangerouslySetInnerHTML={{ __html: t(message) }} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
return <Trans>{message}</Trans>;
|
||||||
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
|
const {
|
||||||
|
type, id, t, message,
|
||||||
|
} = this.props;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={`toast toast--${this.props.type}`}>
|
<div className={`toast toast--${type}`}>
|
||||||
<p className="toast__content">
|
<p className="toast__content">
|
||||||
<Trans>{this.props.message}</Trans>
|
{this.showMessage(t, type, message)}
|
||||||
</p>
|
</p>
|
||||||
<button className="toast__dismiss" onClick={() => this.props.removeToast(this.props.id)}>
|
<button className="toast__dismiss" onClick={() => this.props.removeToast(id)}>
|
||||||
<svg stroke="#fff" fill="none" width="20" height="20" strokeWidth="2" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path d="m18 6-12 12"/><path d="m6 6 12 12"/></svg>
|
<svg stroke="#fff" fill="none" width="20" height="20" strokeWidth="2" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path d="m18 6-12 12"/><path d="m6 6 12 12"/></svg>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
@ -30,6 +42,7 @@ class Toast extends Component {
|
||||||
}
|
}
|
||||||
|
|
||||||
Toast.propTypes = {
|
Toast.propTypes = {
|
||||||
|
t: PropTypes.func.isRequired,
|
||||||
id: PropTypes.string.isRequired,
|
id: PropTypes.string.isRequired,
|
||||||
message: PropTypes.string.isRequired,
|
message: PropTypes.string.isRequired,
|
||||||
type: PropTypes.string.isRequired,
|
type: PropTypes.string.isRequired,
|
||||||
|
|
|
@ -0,0 +1,40 @@
|
||||||
|
.overlay {
|
||||||
|
display: none;
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
z-index: 110;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 20px;
|
||||||
|
font-size: 28px;
|
||||||
|
font-weight: 600;
|
||||||
|
text-align: center;
|
||||||
|
background-color: rgba(255, 255, 255, 0.8);
|
||||||
|
}
|
||||||
|
|
||||||
|
.overlay--visible {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
.overlay__loading {
|
||||||
|
width: 40px;
|
||||||
|
height: 40px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
background-image: url("data:image/svg+xml;charset=utf-8,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20viewBox%3D%220%200%2047.6%2047.6%22%20height%3D%22100%25%22%20width%3D%22100%25%22%3E%3Cpath%20opacity%3D%22.235%22%20fill%3D%22%23979797%22%20d%3D%22M44.4%2011.9l-5.2%203c1.5%202.6%202.4%205.6%202.4%208.9%200%209.8-8%2017.8-17.8%2017.8-6.6%200-12.3-3.6-15.4-8.9l-5.2%203C7.3%2042.8%2015%2047.6%2023.8%2047.6c13.1%200%2023.8-10.7%2023.8-23.8%200-4.3-1.2-8.4-3.2-11.9z%22%2F%3E%3Cpath%20fill%3D%22%2366b574%22%20d%3D%22M3.2%2035.7C0%2030.2-.8%2023.8.8%2017.6%202.5%2011.5%206.4%206.4%2011.9%203.2%2017.4%200%2023.8-.8%2030%20.8c6.1%201.6%2011.3%205.6%2014.4%2011.1l-5.2%203c-2.4-4.1-6.2-7.1-10.8-8.3C23.8%205.4%2019%206%2014.9%208.4s-7.1%206.2-8.3%2010.8c-1.2%204.6-.6%209.4%201.8%2013.5l-5.2%203z%22%2F%3E%3C%2Fsvg%3E");
|
||||||
|
will-change: transform;
|
||||||
|
animation: clockwise 2s linear infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes clockwise {
|
||||||
|
0% {
|
||||||
|
transform: rotate(0deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
100% {
|
||||||
|
transform: rotate(360deg);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,26 @@
|
||||||
|
import React from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import { Trans, withNamespaces } from 'react-i18next';
|
||||||
|
import classnames from 'classnames';
|
||||||
|
|
||||||
|
import './Overlay.css';
|
||||||
|
|
||||||
|
const UpdateOverlay = (props) => {
|
||||||
|
const overlayClass = classnames({
|
||||||
|
overlay: true,
|
||||||
|
'overlay--visible': props.processingUpdate,
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={overlayClass}>
|
||||||
|
<div className="overlay__loading"></div>
|
||||||
|
<Trans>processing_update</Trans>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
UpdateOverlay.propTypes = {
|
||||||
|
processingUpdate: PropTypes.bool,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default withNamespaces()(UpdateOverlay);
|
|
@ -1,4 +1,4 @@
|
||||||
import React from 'react';
|
import React, { Fragment } from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import { Trans, withNamespaces } from 'react-i18next';
|
import { Trans, withNamespaces } from 'react-i18next';
|
||||||
|
|
||||||
|
@ -6,22 +6,37 @@ import Topline from './Topline';
|
||||||
|
|
||||||
const UpdateTopline = props => (
|
const UpdateTopline = props => (
|
||||||
<Topline type="info">
|
<Topline type="info">
|
||||||
<Trans
|
<Fragment>
|
||||||
values={{ version: props.version }}
|
<Trans
|
||||||
components={[
|
values={{ version: props.version }}
|
||||||
<a href={props.url} target="_blank" rel="noopener noreferrer" key="0">
|
components={[
|
||||||
Click here
|
<a href={props.url} target="_blank" rel="noopener noreferrer" key="0">
|
||||||
</a>,
|
Click here
|
||||||
]}
|
</a>,
|
||||||
>
|
]}
|
||||||
update_announcement
|
>
|
||||||
</Trans>
|
update_announcement
|
||||||
|
</Trans>
|
||||||
|
{props.canAutoUpdate &&
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="btn btn-sm btn-primary ml-3"
|
||||||
|
onClick={props.getUpdate}
|
||||||
|
disabled={props.processingUpdate}
|
||||||
|
>
|
||||||
|
<Trans>update_now</Trans>
|
||||||
|
</button>
|
||||||
|
}
|
||||||
|
</Fragment>
|
||||||
</Topline>
|
</Topline>
|
||||||
);
|
);
|
||||||
|
|
||||||
UpdateTopline.propTypes = {
|
UpdateTopline.propTypes = {
|
||||||
version: PropTypes.string.isRequired,
|
version: PropTypes.string,
|
||||||
url: PropTypes.string.isRequired,
|
url: PropTypes.string.isRequired,
|
||||||
|
canAutoUpdate: PropTypes.bool,
|
||||||
|
getUpdate: PropTypes.func,
|
||||||
|
processingUpdate: PropTypes.bool,
|
||||||
};
|
};
|
||||||
|
|
||||||
export default withNamespaces()(UpdateTopline);
|
export default withNamespaces()(UpdateTopline);
|
||||||
|
|
|
@ -126,12 +126,16 @@ const dashboard = handleActions({
|
||||||
const {
|
const {
|
||||||
version,
|
version,
|
||||||
announcement_url: announcementUrl,
|
announcement_url: announcementUrl,
|
||||||
|
new_version: newVersion,
|
||||||
|
can_autoupdate: canAutoUpdate,
|
||||||
} = payload;
|
} = payload;
|
||||||
|
|
||||||
const newState = {
|
const newState = {
|
||||||
...state,
|
...state,
|
||||||
version,
|
version,
|
||||||
announcementUrl,
|
announcementUrl,
|
||||||
|
newVersion,
|
||||||
|
canAutoUpdate,
|
||||||
isUpdateAvailable: true,
|
isUpdateAvailable: true,
|
||||||
};
|
};
|
||||||
return newState;
|
return newState;
|
||||||
|
@ -140,6 +144,13 @@ const dashboard = handleActions({
|
||||||
return state;
|
return state;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
[actions.getUpdateRequest]: state => ({ ...state, processingUpdate: true }),
|
||||||
|
[actions.getUpdateFailure]: state => ({ ...state, processingUpdate: false }),
|
||||||
|
[actions.getUpdateSuccess]: (state) => {
|
||||||
|
const newState = { ...state, processingUpdate: false };
|
||||||
|
return newState;
|
||||||
|
},
|
||||||
|
|
||||||
[actions.getFilteringRequest]: state => ({ ...state, processingFiltering: true }),
|
[actions.getFilteringRequest]: state => ({ ...state, processingFiltering: true }),
|
||||||
[actions.getFilteringFailure]: state => ({ ...state, processingFiltering: false }),
|
[actions.getFilteringFailure]: state => ({ ...state, processingFiltering: false }),
|
||||||
[actions.getFilteringSuccess]: (state, { payload }) => {
|
[actions.getFilteringSuccess]: (state, { payload }) => {
|
||||||
|
@ -187,6 +198,7 @@ const dashboard = handleActions({
|
||||||
processingVersion: true,
|
processingVersion: true,
|
||||||
processingFiltering: true,
|
processingFiltering: true,
|
||||||
processingClients: true,
|
processingClients: true,
|
||||||
|
processingUpdate: false,
|
||||||
upstreamDns: '',
|
upstreamDns: '',
|
||||||
bootstrapDns: '',
|
bootstrapDns: '',
|
||||||
allServers: false,
|
allServers: false,
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import { handleActions } from 'redux-actions';
|
import { handleActions } from 'redux-actions';
|
||||||
import nanoid from 'nanoid';
|
import nanoid from 'nanoid';
|
||||||
|
|
||||||
import { addErrorToast, addSuccessToast, removeToast } from '../actions';
|
import { addErrorToast, addSuccessToast, addNoticeToast, removeToast } from '../actions';
|
||||||
|
|
||||||
const toasts = handleActions({
|
const toasts = handleActions({
|
||||||
[addErrorToast]: (state, { payload }) => {
|
[addErrorToast]: (state, { payload }) => {
|
||||||
|
@ -24,6 +24,16 @@ const toasts = handleActions({
|
||||||
const newState = { ...state, notices: [...state.notices, successToast] };
|
const newState = { ...state, notices: [...state.notices, successToast] };
|
||||||
return newState;
|
return newState;
|
||||||
},
|
},
|
||||||
|
[addNoticeToast]: (state, { payload }) => {
|
||||||
|
const noticeToast = {
|
||||||
|
id: nanoid(),
|
||||||
|
message: payload.error.toString(),
|
||||||
|
type: 'notice',
|
||||||
|
};
|
||||||
|
|
||||||
|
const newState = { ...state, notices: [...state.notices, noticeToast] };
|
||||||
|
return newState;
|
||||||
|
},
|
||||||
[removeToast]: (state, { payload }) => {
|
[removeToast]: (state, { payload }) => {
|
||||||
const filtered = state.notices.filter(notice => notice.id !== payload);
|
const filtered = state.notices.filter(notice => notice.id !== payload);
|
||||||
const newState = { ...state, notices: filtered };
|
const newState = { ...state, notices: filtered };
|
||||||
|
|
39
config.go
39
config.go
|
@ -30,9 +30,15 @@ type logSettings struct {
|
||||||
// configuration is loaded from YAML
|
// configuration is loaded from YAML
|
||||||
// field ordering is important -- yaml fields will mirror ordering from here
|
// field ordering is important -- yaml fields will mirror ordering from here
|
||||||
type configuration struct {
|
type configuration struct {
|
||||||
|
// Raw file data to avoid re-reading of configuration file
|
||||||
|
// It's reset after config is parsed
|
||||||
|
fileData []byte
|
||||||
|
|
||||||
ourConfigFilename string // Config filename (can be overridden via the command line arguments)
|
ourConfigFilename string // Config filename (can be overridden via the command line arguments)
|
||||||
ourWorkingDir string // Location of our directory, used to protect against CWD being somewhere else
|
ourWorkingDir string // Location of our directory, used to protect against CWD being somewhere else
|
||||||
firstRun bool // if set to true, don't run any services except HTTP web inteface, and serve only first-run html
|
firstRun bool // if set to true, don't run any services except HTTP web inteface, and serve only first-run html
|
||||||
|
// runningAsService flag is set to true when options are passed from the service runner
|
||||||
|
runningAsService bool
|
||||||
|
|
||||||
BindHost string `yaml:"bind_host"` // BindHost is the IP address of the HTTP server to bind to
|
BindHost string `yaml:"bind_host"` // BindHost is the IP address of the HTTP server to bind to
|
||||||
BindPort int `yaml:"bind_port"` // BindPort is the port the HTTP server
|
BindPort int `yaml:"bind_port"` // BindPort is the port the HTTP server
|
||||||
|
@ -113,10 +119,10 @@ var config = configuration{
|
||||||
BindHost: "0.0.0.0",
|
BindHost: "0.0.0.0",
|
||||||
Port: 53,
|
Port: 53,
|
||||||
FilteringConfig: dnsforward.FilteringConfig{
|
FilteringConfig: dnsforward.FilteringConfig{
|
||||||
ProtectionEnabled: true, // whether or not use any of dnsfilter features
|
ProtectionEnabled: true, // whether or not use any of dnsfilter features
|
||||||
FilteringEnabled: true, // whether or not use filter lists
|
FilteringEnabled: true, // whether or not use filter lists
|
||||||
BlockingMode: "nxdomain", // mode how to answer filtered requests
|
BlockingMode: "nxdomain", // mode how to answer filtered requests
|
||||||
BlockedResponseTTL: 10, // in seconds
|
BlockedResponseTTL: 10, // in seconds
|
||||||
QueryLogEnabled: true,
|
QueryLogEnabled: true,
|
||||||
Ratelimit: 20,
|
Ratelimit: 20,
|
||||||
RefuseAny: true,
|
RefuseAny: true,
|
||||||
|
@ -174,7 +180,7 @@ func (c *configuration) getConfigFilename() string {
|
||||||
func getLogSettings() logSettings {
|
func getLogSettings() logSettings {
|
||||||
l := logSettings{}
|
l := logSettings{}
|
||||||
yamlFile, err := readConfigFile()
|
yamlFile, err := readConfigFile()
|
||||||
if err != nil || yamlFile == nil {
|
if err != nil {
|
||||||
return l
|
return l
|
||||||
}
|
}
|
||||||
err = yaml.Unmarshal(yamlFile, &l)
|
err = yaml.Unmarshal(yamlFile, &l)
|
||||||
|
@ -190,13 +196,9 @@ func parseConfig() error {
|
||||||
log.Debug("Reading config file: %s", configFile)
|
log.Debug("Reading config file: %s", configFile)
|
||||||
yamlFile, err := readConfigFile()
|
yamlFile, err := readConfigFile()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error("Couldn't read config file: %s", err)
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if yamlFile == nil {
|
config.fileData = nil
|
||||||
log.Error("YAML file doesn't exist, skipping it")
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
err = yaml.Unmarshal(yamlFile, &config)
|
err = yaml.Unmarshal(yamlFile, &config)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error("Couldn't parse config file: %s", err)
|
log.Error("Couldn't parse config file: %s", err)
|
||||||
|
@ -213,22 +215,23 @@ func parseConfig() error {
|
||||||
|
|
||||||
// readConfigFile reads config file contents if it exists
|
// readConfigFile reads config file contents if it exists
|
||||||
func readConfigFile() ([]byte, error) {
|
func readConfigFile() ([]byte, error) {
|
||||||
configFile := config.getConfigFilename()
|
if len(config.fileData) != 0 {
|
||||||
if _, err := os.Stat(configFile); os.IsNotExist(err) {
|
return config.fileData, nil
|
||||||
// do nothing, file doesn't exist
|
|
||||||
return nil, nil
|
|
||||||
}
|
}
|
||||||
return ioutil.ReadFile(configFile)
|
|
||||||
|
configFile := config.getConfigFilename()
|
||||||
|
d, err := ioutil.ReadFile(configFile)
|
||||||
|
if err != nil {
|
||||||
|
log.Error("Couldn't read config file %s: %s", configFile, err)
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return d, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Saves configuration to the YAML file and also saves the user filter contents to a file
|
// Saves configuration to the YAML file and also saves the user filter contents to a file
|
||||||
func (c *configuration) write() error {
|
func (c *configuration) write() error {
|
||||||
c.Lock()
|
c.Lock()
|
||||||
defer c.Unlock()
|
defer c.Unlock()
|
||||||
if config.firstRun {
|
|
||||||
log.Debug("Silently refusing to write config because first run and not configured yet")
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
configFile := config.getConfigFilename()
|
configFile := config.getConfigFilename()
|
||||||
log.Debug("Writing YAML file: %s", configFile)
|
log.Debug("Writing YAML file: %s", configFile)
|
||||||
yamlText, err := yaml.Marshal(&config)
|
yamlText, err := yaml.Marshal(&config)
|
||||||
|
|
37
control.go
37
control.go
|
@ -557,42 +557,6 @@ func checkDNS(input string, bootstrap []string) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func handleGetVersionJSON(w http.ResponseWriter, r *http.Request) {
|
|
||||||
log.Tracef("%s %v", r.Method, r.URL)
|
|
||||||
now := time.Now()
|
|
||||||
if now.Sub(versionCheckLastTime) <= versionCheckPeriod && len(versionCheckJSON) != 0 {
|
|
||||||
// return cached copy
|
|
||||||
w.Header().Set("Content-Type", "application/json")
|
|
||||||
w.Write(versionCheckJSON)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
resp, err := client.Get(versionCheckURL)
|
|
||||||
if err != nil {
|
|
||||||
httpError(w, http.StatusBadGateway, "Couldn't get version check json from %s: %T %s\n", versionCheckURL, err, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if resp != nil && resp.Body != nil {
|
|
||||||
defer resp.Body.Close()
|
|
||||||
}
|
|
||||||
|
|
||||||
// read the body entirely
|
|
||||||
body, err := ioutil.ReadAll(resp.Body)
|
|
||||||
if err != nil {
|
|
||||||
httpError(w, http.StatusBadGateway, "Couldn't read response body from %s: %s", versionCheckURL, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
w.Header().Set("Content-Type", "application/json")
|
|
||||||
_, err = w.Write(body)
|
|
||||||
if err != nil {
|
|
||||||
httpError(w, http.StatusInternalServerError, "Couldn't write body: %s", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
versionCheckLastTime = now
|
|
||||||
versionCheckJSON = body
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---------
|
// ---------
|
||||||
// filtering
|
// filtering
|
||||||
// ---------
|
// ---------
|
||||||
|
@ -1006,6 +970,7 @@ func registerControlHandlers() {
|
||||||
http.HandleFunc("/control/stats_history", postInstall(optionalAuth(ensureGET(handleStatsHistory))))
|
http.HandleFunc("/control/stats_history", postInstall(optionalAuth(ensureGET(handleStatsHistory))))
|
||||||
http.HandleFunc("/control/stats_reset", postInstall(optionalAuth(ensurePOST(handleStatsReset))))
|
http.HandleFunc("/control/stats_reset", postInstall(optionalAuth(ensurePOST(handleStatsReset))))
|
||||||
http.HandleFunc("/control/version.json", postInstall(optionalAuth(handleGetVersionJSON)))
|
http.HandleFunc("/control/version.json", postInstall(optionalAuth(handleGetVersionJSON)))
|
||||||
|
http.HandleFunc("/control/update", postInstall(optionalAuth(ensurePOST(handleUpdate))))
|
||||||
http.HandleFunc("/control/filtering/enable", postInstall(optionalAuth(ensurePOST(handleFilteringEnable))))
|
http.HandleFunc("/control/filtering/enable", postInstall(optionalAuth(ensurePOST(handleFilteringEnable))))
|
||||||
http.HandleFunc("/control/filtering/disable", postInstall(optionalAuth(ensurePOST(handleFilteringDisable))))
|
http.HandleFunc("/control/filtering/disable", postInstall(optionalAuth(ensurePOST(handleFilteringDisable))))
|
||||||
http.HandleFunc("/control/filtering/add_url", postInstall(optionalAuth(ensurePOST(handleFilteringAddURL))))
|
http.HandleFunc("/control/filtering/add_url", postInstall(optionalAuth(ensurePOST(handleFilteringAddURL))))
|
||||||
|
|
|
@ -0,0 +1,371 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"archive/zip"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"io/ioutil"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
"path/filepath"
|
||||||
|
"runtime"
|
||||||
|
"strings"
|
||||||
|
"syscall"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/AdguardTeam/golibs/log"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Convert version.json data to our JSON response
|
||||||
|
func getVersionResp(data []byte) []byte {
|
||||||
|
versionJSON := make(map[string]interface{})
|
||||||
|
err := json.Unmarshal(data, &versionJSON)
|
||||||
|
if err != nil {
|
||||||
|
log.Error("version.json: %s", err)
|
||||||
|
return []byte{}
|
||||||
|
}
|
||||||
|
|
||||||
|
ret := make(map[string]interface{})
|
||||||
|
ret["can_autoupdate"] = false
|
||||||
|
|
||||||
|
var ok1, ok2, ok3 bool
|
||||||
|
ret["new_version"], ok1 = versionJSON["version"].(string)
|
||||||
|
ret["announcement"], ok2 = versionJSON["announcement"].(string)
|
||||||
|
ret["announcement_url"], ok3 = versionJSON["announcement_url"].(string)
|
||||||
|
if !ok1 || !ok2 || !ok3 {
|
||||||
|
log.Error("version.json: invalid data")
|
||||||
|
return []byte{}
|
||||||
|
}
|
||||||
|
|
||||||
|
_, ok := versionJSON[fmt.Sprintf("download_%s_%s", runtime.GOOS, runtime.GOARCH)]
|
||||||
|
if ok && ret["new_version"] != VersionString {
|
||||||
|
ret["can_autoupdate"] = true
|
||||||
|
}
|
||||||
|
|
||||||
|
d, _ := json.Marshal(ret)
|
||||||
|
return d
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the latest available version from the Internet
|
||||||
|
func handleGetVersionJSON(w http.ResponseWriter, r *http.Request) {
|
||||||
|
log.Tracef("%s %v", r.Method, r.URL)
|
||||||
|
|
||||||
|
now := time.Now()
|
||||||
|
controlLock.Lock()
|
||||||
|
cached := now.Sub(versionCheckLastTime) <= versionCheckPeriod && len(versionCheckJSON) != 0
|
||||||
|
data := versionCheckJSON
|
||||||
|
controlLock.Unlock()
|
||||||
|
|
||||||
|
if cached {
|
||||||
|
// return cached copy
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
w.Write(getVersionResp(data))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
resp, err := client.Get(versionCheckURL)
|
||||||
|
if err != nil {
|
||||||
|
httpError(w, http.StatusBadGateway, "Couldn't get version check json from %s: %T %s\n", versionCheckURL, err, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if resp != nil && resp.Body != nil {
|
||||||
|
defer resp.Body.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
// read the body entirely
|
||||||
|
body, err := ioutil.ReadAll(resp.Body)
|
||||||
|
if err != nil {
|
||||||
|
httpError(w, http.StatusBadGateway, "Couldn't read response body from %s: %s", versionCheckURL, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
controlLock.Lock()
|
||||||
|
versionCheckLastTime = now
|
||||||
|
versionCheckJSON = body
|
||||||
|
controlLock.Unlock()
|
||||||
|
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
_, err = w.Write(getVersionResp(body))
|
||||||
|
if err != nil {
|
||||||
|
httpError(w, http.StatusInternalServerError, "Couldn't write body: %s", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Copy file on disk
|
||||||
|
func copyFile(src, dst string) error {
|
||||||
|
d, e := ioutil.ReadFile(src)
|
||||||
|
if e != nil {
|
||||||
|
return e
|
||||||
|
}
|
||||||
|
e = ioutil.WriteFile(dst, d, 0644)
|
||||||
|
if e != nil {
|
||||||
|
return e
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type updateInfo struct {
|
||||||
|
pkgURL string // URL for the new package
|
||||||
|
pkgName string // Full path to package file
|
||||||
|
newVer string // New version string
|
||||||
|
updateDir string // Full path to the directory containing unpacked files from the new package
|
||||||
|
backupDir string // Full path to backup directory
|
||||||
|
configName string // Full path to the current configuration file
|
||||||
|
updateConfigName string // Full path to the configuration file to check by the new binary
|
||||||
|
curBinName string // Full path to the current executable file
|
||||||
|
bkpBinName string // Full path to the current executable file in backup directory
|
||||||
|
newBinName string // Full path to the new executable file
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fill in updateInfo object
|
||||||
|
func getUpdateInfo(jsonData []byte) (*updateInfo, error) {
|
||||||
|
var u updateInfo
|
||||||
|
|
||||||
|
workDir := config.ourWorkingDir
|
||||||
|
|
||||||
|
versionJSON := make(map[string]interface{})
|
||||||
|
err := json.Unmarshal(jsonData, &versionJSON)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("JSON parse: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
u.pkgURL = versionJSON[fmt.Sprintf("download_%s_%s", runtime.GOOS, runtime.GOARCH)].(string)
|
||||||
|
u.newVer = versionJSON["version"].(string)
|
||||||
|
if len(u.pkgURL) == 0 || len(u.newVer) == 0 {
|
||||||
|
return nil, fmt.Errorf("Invalid JSON")
|
||||||
|
}
|
||||||
|
|
||||||
|
if u.newVer == VersionString {
|
||||||
|
return nil, fmt.Errorf("No need to update")
|
||||||
|
}
|
||||||
|
|
||||||
|
_, pkgFileName := filepath.Split(u.pkgURL)
|
||||||
|
if len(pkgFileName) == 0 {
|
||||||
|
return nil, fmt.Errorf("Invalid JSON")
|
||||||
|
}
|
||||||
|
u.pkgName = filepath.Join(workDir, pkgFileName)
|
||||||
|
|
||||||
|
u.updateDir = filepath.Join(workDir, fmt.Sprintf("update-%s", u.newVer))
|
||||||
|
u.backupDir = filepath.Join(workDir, fmt.Sprintf("backup-%s", VersionString))
|
||||||
|
u.configName = config.getConfigFilename()
|
||||||
|
u.updateConfigName = filepath.Join(u.updateDir, "AdGuardHome", "AdGuardHome.yaml")
|
||||||
|
if strings.HasSuffix(pkgFileName, ".zip") {
|
||||||
|
u.updateConfigName = filepath.Join(u.updateDir, "AdGuardHome.yaml")
|
||||||
|
}
|
||||||
|
|
||||||
|
binName := "AdGuardHome"
|
||||||
|
if runtime.GOOS == "windows" {
|
||||||
|
binName = "AdGuardHome.exe"
|
||||||
|
}
|
||||||
|
u.curBinName = filepath.Join(workDir, binName)
|
||||||
|
u.bkpBinName = filepath.Join(u.backupDir, binName)
|
||||||
|
u.newBinName = filepath.Join(u.updateDir, "AdGuardHome", binName)
|
||||||
|
if strings.HasSuffix(pkgFileName, ".zip") {
|
||||||
|
u.newBinName = filepath.Join(u.updateDir, binName)
|
||||||
|
}
|
||||||
|
|
||||||
|
return &u, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Unpack all files from .zip file to the specified directory
|
||||||
|
func zipFileUnpack(zipfile, outdir string) error {
|
||||||
|
r, err := zip.OpenReader(zipfile)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("zip.OpenReader(): %s", err)
|
||||||
|
}
|
||||||
|
defer r.Close()
|
||||||
|
|
||||||
|
for _, zf := range r.File {
|
||||||
|
zr, err := zf.Open()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("zip file Open(): %s", err)
|
||||||
|
}
|
||||||
|
fi := zf.FileInfo()
|
||||||
|
fn := filepath.Join(outdir, fi.Name())
|
||||||
|
|
||||||
|
if fi.IsDir() {
|
||||||
|
err = os.Mkdir(fn, fi.Mode())
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("zip file Read(): %s", err)
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
f, err := os.OpenFile(fn, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, fi.Mode())
|
||||||
|
if err != nil {
|
||||||
|
zr.Close()
|
||||||
|
return fmt.Errorf("os.OpenFile(): %s", err)
|
||||||
|
}
|
||||||
|
_, err = io.Copy(f, zr)
|
||||||
|
if err != nil {
|
||||||
|
zr.Close()
|
||||||
|
return fmt.Errorf("io.Copy(): %s", err)
|
||||||
|
}
|
||||||
|
zr.Close()
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Unpack all files from .tar.gz file to the specified directory
|
||||||
|
func targzFileUnpack(tarfile, outdir string) error {
|
||||||
|
cmd := exec.Command("tar", "zxf", tarfile, "-C", outdir)
|
||||||
|
log.Tracef("Unpacking: %v", cmd.Args)
|
||||||
|
_, err := cmd.Output()
|
||||||
|
if err != nil || cmd.ProcessState.ExitCode() != 0 {
|
||||||
|
return fmt.Errorf("exec.Command() failed: %s", err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Perform an update procedure
|
||||||
|
func doUpdate(u *updateInfo) error {
|
||||||
|
log.Info("Updating from %s to %s. URL:%s Package:%s",
|
||||||
|
VersionString, u.newVer, u.pkgURL, u.pkgName)
|
||||||
|
|
||||||
|
resp, err := client.Get(u.pkgURL)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("HTTP request failed: %s", err)
|
||||||
|
}
|
||||||
|
if resp != nil && resp.Body != nil {
|
||||||
|
defer resp.Body.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Tracef("Reading HTTP body")
|
||||||
|
body, err := ioutil.ReadAll(resp.Body)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("ioutil.ReadAll() failed: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Tracef("Saving package to file")
|
||||||
|
err = ioutil.WriteFile(u.pkgName, body, 0644)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("ioutil.WriteFile() failed: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Tracef("Unpacking the package")
|
||||||
|
_ = os.Mkdir(u.updateDir, 0755)
|
||||||
|
_, file := filepath.Split(u.pkgName)
|
||||||
|
if strings.HasSuffix(file, ".zip") {
|
||||||
|
err = zipFileUnpack(u.pkgName, u.updateDir)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("zipFileUnpack() failed: %s", err)
|
||||||
|
}
|
||||||
|
} else if strings.HasSuffix(file, ".tar.gz") {
|
||||||
|
err = targzFileUnpack(u.pkgName, u.updateDir)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("zipFileUnpack() failed: %s", err)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return fmt.Errorf("Unknown package extension")
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Tracef("Checking configuration")
|
||||||
|
err = copyFile(u.configName, u.updateConfigName)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("copyFile() failed: %s", err)
|
||||||
|
}
|
||||||
|
cmd := exec.Command(u.newBinName, "--check-config")
|
||||||
|
err = cmd.Run()
|
||||||
|
if err != nil || cmd.ProcessState.ExitCode() != 0 {
|
||||||
|
return fmt.Errorf("exec.Command(): %s %d", err, cmd.ProcessState.ExitCode())
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Tracef("Backing up the current configuration")
|
||||||
|
_ = os.Mkdir(u.backupDir, 0755)
|
||||||
|
err = copyFile(u.configName, filepath.Join(u.backupDir, "AdGuardHome.yaml"))
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("copyFile() failed: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Tracef("Renaming: %s -> %s", u.curBinName, u.bkpBinName)
|
||||||
|
err = os.Rename(u.curBinName, u.bkpBinName)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if runtime.GOOS == "windows" {
|
||||||
|
// rename fails with "File in use" error
|
||||||
|
err = copyFile(u.newBinName, u.curBinName)
|
||||||
|
} else {
|
||||||
|
err = os.Rename(u.newBinName, u.curBinName)
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
log.Tracef("Renamed: %s -> %s", u.newBinName, u.curBinName)
|
||||||
|
|
||||||
|
_ = os.Remove(u.pkgName)
|
||||||
|
// _ = os.RemoveAll(u.updateDir)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Complete an update procedure
|
||||||
|
func finishUpdate(u *updateInfo) {
|
||||||
|
log.Info("Stopping all tasks")
|
||||||
|
cleanup()
|
||||||
|
stopHTTPServer()
|
||||||
|
cleanupAlways()
|
||||||
|
|
||||||
|
if runtime.GOOS == "windows" {
|
||||||
|
|
||||||
|
if config.runningAsService {
|
||||||
|
// Note:
|
||||||
|
// we can't restart the service via "kardianos/service" package - it kills the process first
|
||||||
|
// we can't start a new instance - Windows doesn't allow it
|
||||||
|
cmd := exec.Command("cmd", "/c", "net stop AdGuardHome & net start AdGuardHome")
|
||||||
|
err := cmd.Start()
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("exec.Command() failed: %s", err)
|
||||||
|
}
|
||||||
|
os.Exit(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd := exec.Command(u.curBinName, os.Args[1:]...)
|
||||||
|
log.Info("Restarting: %v", cmd.Args)
|
||||||
|
cmd.Stdin = os.Stdin
|
||||||
|
cmd.Stdout = os.Stdout
|
||||||
|
cmd.Stderr = os.Stderr
|
||||||
|
err := cmd.Start()
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("exec.Command() failed: %s", err)
|
||||||
|
}
|
||||||
|
os.Exit(0)
|
||||||
|
|
||||||
|
} else {
|
||||||
|
|
||||||
|
log.Info("Restarting: %v", os.Args)
|
||||||
|
err := syscall.Exec(u.curBinName, os.Args, os.Environ())
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("syscall.Exec() failed: %s", err)
|
||||||
|
}
|
||||||
|
// Unreachable code
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Perform an update procedure to the latest available version
|
||||||
|
func handleUpdate(w http.ResponseWriter, r *http.Request) {
|
||||||
|
log.Tracef("%s %v", r.Method, r.URL)
|
||||||
|
|
||||||
|
if len(versionCheckJSON) == 0 {
|
||||||
|
httpError(w, http.StatusBadRequest, "/update request isn't allowed now")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
u, err := getUpdateInfo(versionCheckJSON)
|
||||||
|
if err != nil {
|
||||||
|
httpError(w, http.StatusInternalServerError, "%s", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
err = doUpdate(u)
|
||||||
|
if err != nil {
|
||||||
|
httpError(w, http.StatusInternalServerError, "%s", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
returnOK(w)
|
||||||
|
|
||||||
|
time.Sleep(time.Second) // wait (hopefully) until response is sent (not sure whether it's really necessary)
|
||||||
|
go finishUpdate(u)
|
||||||
|
}
|
|
@ -0,0 +1,39 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func testDoUpdate(t *testing.T) {
|
||||||
|
config.DNS.Port = 0
|
||||||
|
u := updateInfo{
|
||||||
|
pkgURL: "https://github.com/AdguardTeam/AdGuardHome/releases/download/v0.95/AdGuardHome_v0.95_linux_amd64.tar.gz",
|
||||||
|
pkgName: "./AdGuardHome_v0.95_linux_amd64.tar.gz",
|
||||||
|
newVer: "v0.95",
|
||||||
|
updateDir: "./update-v0.95",
|
||||||
|
backupDir: "./backup-v0.94",
|
||||||
|
configName: "./AdGuardHome.yaml",
|
||||||
|
updateConfigName: "./update-v0.95/AdGuardHome/AdGuardHome.yaml",
|
||||||
|
curBinName: "./AdGuardHome",
|
||||||
|
bkpBinName: "./backup-v0.94/AdGuardHome",
|
||||||
|
newBinName: "./update-v0.95/AdGuardHome/AdGuardHome",
|
||||||
|
}
|
||||||
|
e := doUpdate(&u)
|
||||||
|
if e != nil {
|
||||||
|
t.Fatalf("FAILED: %s", e)
|
||||||
|
}
|
||||||
|
os.RemoveAll(u.backupDir)
|
||||||
|
os.RemoveAll(u.updateDir)
|
||||||
|
}
|
||||||
|
|
||||||
|
func testZipFileUnpack(t *testing.T) {
|
||||||
|
fn := "./dist/AdGuardHome_v0.95_Windows_amd64.zip"
|
||||||
|
outdir := "./test-unpack"
|
||||||
|
_ = os.Mkdir(outdir, 0755)
|
||||||
|
e := zipFileUnpack(fn, outdir)
|
||||||
|
if e != nil {
|
||||||
|
t.Fatalf("FAILED: %s", e)
|
||||||
|
}
|
||||||
|
os.RemoveAll(outdir)
|
||||||
|
}
|
|
@ -318,7 +318,7 @@ func customDialContext(ctx context.Context, network, addr string) (net.Conn, err
|
||||||
Timeout: time.Minute * 5,
|
Timeout: time.Minute * 5,
|
||||||
}
|
}
|
||||||
|
|
||||||
if net.ParseIP(host) != nil {
|
if net.ParseIP(host) != nil || config.DNS.Port == 0 {
|
||||||
con, err := dialer.DialContext(ctx, network, addr)
|
con, err := dialer.DialContext(ctx, network, addr)
|
||||||
return con, err
|
return con, err
|
||||||
}
|
}
|
||||||
|
|
|
@ -151,6 +151,17 @@ paths:
|
||||||
description: 'Cannot write answer'
|
description: 'Cannot write answer'
|
||||||
502:
|
502:
|
||||||
description: 'Cannot retrieve the version.json file contents'
|
description: 'Cannot retrieve the version.json file contents'
|
||||||
|
/update:
|
||||||
|
post:
|
||||||
|
tags:
|
||||||
|
- global
|
||||||
|
operationId: beginUpdate
|
||||||
|
summary: 'Begin auto-upgrade procedure'
|
||||||
|
responses:
|
||||||
|
200:
|
||||||
|
description: OK
|
||||||
|
500:
|
||||||
|
description: Failed
|
||||||
|
|
||||||
# --------------------------------------------------
|
# --------------------------------------------------
|
||||||
# Query log methods
|
# Query log methods
|
||||||
|
@ -906,17 +917,8 @@ definitions:
|
||||||
VersionInfo:
|
VersionInfo:
|
||||||
type: "object"
|
type: "object"
|
||||||
description: "Information about the latest available version of AdGuard Home"
|
description: "Information about the latest available version of AdGuard Home"
|
||||||
required:
|
|
||||||
- "version"
|
|
||||||
- "announcement"
|
|
||||||
- "announcement_url"
|
|
||||||
- "download_darwin_amd64"
|
|
||||||
- "download_linux_amd64"
|
|
||||||
- "download_linux_386"
|
|
||||||
- "download_linux_arm"
|
|
||||||
- "selfupdate_min_version"
|
|
||||||
properties:
|
properties:
|
||||||
version:
|
new_version:
|
||||||
type: "string"
|
type: "string"
|
||||||
example: "v0.9"
|
example: "v0.9"
|
||||||
announcement:
|
announcement:
|
||||||
|
@ -925,21 +927,8 @@ definitions:
|
||||||
announcement_url:
|
announcement_url:
|
||||||
type: "string"
|
type: "string"
|
||||||
example: "https://github.com/AdguardTeam/AdGuardHome/releases/tag/v0.9"
|
example: "https://github.com/AdguardTeam/AdGuardHome/releases/tag/v0.9"
|
||||||
download_darwin_amd64:
|
can_autoupdate:
|
||||||
type: "string"
|
type: "boolean"
|
||||||
example: "https://github.com/AdguardTeam/AdGuardHome/releases/download/v0.9/AdGuardHome_v0.9_MacOS.zip"
|
|
||||||
download_linux_amd64:
|
|
||||||
type: "string"
|
|
||||||
example: "https://github.com/AdguardTeam/AdGuardHome/releases/download/v0.9/AdGuardHome_v0.9_linux_amd64.tar.gz"
|
|
||||||
download_linux_386:
|
|
||||||
type: "string"
|
|
||||||
example: "https://github.com/AdguardTeam/AdGuardHome/releases/download/v0.9/AdGuardHome_v0.9_linux_386.tar.gz"
|
|
||||||
download_linux_arm:
|
|
||||||
type: "string"
|
|
||||||
example: "https://github.com/AdguardTeam/AdGuardHome/releases/download/v0.9/AdGuardHome_v0.9_linux_arm.tar.gz"
|
|
||||||
selfupdate_min_version:
|
|
||||||
type: "string"
|
|
||||||
example: "v0.0"
|
|
||||||
Stats:
|
Stats:
|
||||||
type: "object"
|
type: "object"
|
||||||
description: "General server stats for the last 24 hours"
|
description: "General server stats for the last 24 hours"
|
||||||
|
|
12
upgrade.go
12
upgrade.go
|
@ -2,7 +2,6 @@ package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"io/ioutil"
|
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
|
||||||
|
@ -16,21 +15,15 @@ const currentSchemaVersion = 3 // used for upgrading from old configs to new con
|
||||||
// Performs necessary upgrade operations if needed
|
// Performs necessary upgrade operations if needed
|
||||||
func upgradeConfig() error {
|
func upgradeConfig() error {
|
||||||
// read a config file into an interface map, so we can manipulate values without losing any
|
// read a config file into an interface map, so we can manipulate values without losing any
|
||||||
configFile := config.getConfigFilename()
|
|
||||||
if _, err := os.Stat(configFile); os.IsNotExist(err) {
|
|
||||||
log.Printf("config file %s does not exist, nothing to upgrade", configFile)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
diskConfig := map[string]interface{}{}
|
diskConfig := map[string]interface{}{}
|
||||||
body, err := ioutil.ReadFile(configFile)
|
body, err := readConfigFile()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("Couldn't read config file '%s': %s", configFile, err)
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
err = yaml.Unmarshal(body, &diskConfig)
|
err = yaml.Unmarshal(body, &diskConfig)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("Couldn't parse config file '%s': %s", configFile, err)
|
log.Printf("Couldn't parse config file: %s", err)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -87,6 +80,7 @@ func upgradeConfigSchema(oldVersion int, diskConfig *map[string]interface{}) err
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
config.fileData = body
|
||||||
err = file.SafeWrite(configFile, body)
|
err = file.SafeWrite(configFile, body)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("Couldn't save YAML config: %s", err)
|
log.Printf("Couldn't save YAML config: %s", err)
|
||||||
|
|
Loading…
Reference in New Issue