Merge pull request #145 in DNS/adguard-dns from feature/425 to master
* commit '5ca33e44d897d08117e10b248fe9dbe25d3b31f8': (45 commits) Fix object spread Demote some log.printf into log.tracef Makefile -- no need for go get -d . anymore npm 6.7.0 unconditionally modifies package-lock.json. Commit those changes. /install/configure -- Don't fail if HTTP listen host and port don't change /install/get_addresses -- don't send link-local addresses Increase button width Disable button on submit Common reducer for toasts Check if ip_addresses exist in the interface Fix data race found by tests -- https://travis-ci.org/AdguardTeam/AdGuardHome/jobs/489674061#L970 Minor cleanup, added strings, added more information to response when error occurs ingnore Shutdown by golangci Fixed custom select arrow Remove unused icons get rid of go-spew and cleanup go.mod from unused packages Hide 80 web port Default listening to 0.0.0.0 for first-time setup Move installation of /install handlers into a separate optional function /install/configure -- Rebind HTTP server when we get new host and port ...
This commit is contained in:
commit
6b6eacaa2b
|
@ -51,5 +51,6 @@ issues:
|
|||
- .safeBrowsingServer. is unused
|
||||
# errcheck
|
||||
- Error return value of .s.closeConn. is not checked
|
||||
- Error return value of ..*.Shutdown.
|
||||
# goconst
|
||||
- string .forcesafesearch.google.com. has 3 occurrences
|
1
Makefile
1
Makefile
|
@ -20,7 +20,6 @@ $(STATIC): $(JSFILES) client/node_modules
|
|||
npm --prefix client run build-prod
|
||||
|
||||
$(TARGET): $(STATIC) *.go dhcpd/*.go dnsfilter/*.go dnsforward/*.go
|
||||
go get -d .
|
||||
GOOS=$(NATIVE_GOOS) GOARCH=$(NATIVE_GOARCH) GO111MODULE=off go get -v github.com/gobuffalo/packr/...
|
||||
PATH=$(GOPATH)/bin:$(PATH) packr -z
|
||||
CGO_ENABLED=0 go build -ldflags="-s -w -X main.VersionString=$(GIT_VERSION)" -asmflags="-trimpath=$(PWD)" -gcflags="-trimpath=$(PWD)"
|
||||
|
|
122
app.go
122
app.go
|
@ -1,7 +1,6 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"fmt"
|
||||
stdlog "log"
|
||||
"net"
|
||||
|
@ -17,11 +16,11 @@ import (
|
|||
"github.com/gobuffalo/packr"
|
||||
|
||||
"github.com/hmage/golibs/log"
|
||||
"golang.org/x/crypto/ssh/terminal"
|
||||
)
|
||||
|
||||
// VersionString will be set through ldflags, contains current version
|
||||
var VersionString = "undefined"
|
||||
var httpServer *http.Server
|
||||
|
||||
const (
|
||||
// Used in config to indicate that syslog or eventlog (win) should be used for logger output
|
||||
|
@ -67,18 +66,15 @@ func run(args options) {
|
|||
|
||||
// print the first message after logger is configured
|
||||
log.Printf("AdGuard Home, version %s\n", VersionString)
|
||||
log.Printf("Current working directory is %s", config.ourBinaryDir)
|
||||
log.Tracef("Current working directory is %s", config.ourBinaryDir)
|
||||
if args.runningAsService {
|
||||
log.Printf("AdGuard Home is running as a service")
|
||||
}
|
||||
|
||||
err := askUsernamePasswordIfPossible()
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
config.firstRun = detectFirstRun()
|
||||
|
||||
// Do the upgrade if necessary
|
||||
err = upgradeConfig()
|
||||
err := upgradeConfig()
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
@ -121,6 +117,7 @@ func run(args options) {
|
|||
log.Fatal(err)
|
||||
}
|
||||
|
||||
if !config.firstRun {
|
||||
err = startDNSServer()
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
|
@ -130,6 +127,7 @@ func run(args options) {
|
|||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
// Update filters we've just loaded right away, don't wait for periodic update timer
|
||||
go func() {
|
||||
|
@ -145,13 +143,33 @@ func run(args options) {
|
|||
|
||||
// Initialize and run the admin Web interface
|
||||
box := packr.NewBox("build/static")
|
||||
http.Handle("/", optionalAuthHandler(http.FileServer(box)))
|
||||
// if not configured, redirect / to /install.html, otherwise redirect /install.html to /
|
||||
http.Handle("/", postInstallHandler(optionalAuthHandler(http.FileServer(box))))
|
||||
registerControlHandlers()
|
||||
|
||||
// add handlers for /install paths, we only need them when we're not configured yet
|
||||
if config.firstRun {
|
||||
log.Printf("This is the first launch of AdGuard Home, redirecting everything to /install.html ")
|
||||
http.Handle("/install.html", preInstallHandler(http.FileServer(box)))
|
||||
registerInstallHandlers()
|
||||
}
|
||||
|
||||
// this loop is used as an ability to change listening host and/or port
|
||||
for {
|
||||
address := net.JoinHostPort(config.BindHost, strconv.Itoa(config.BindPort))
|
||||
URL := fmt.Sprintf("http://%s", address)
|
||||
log.Println("Go to " + URL)
|
||||
log.Fatal(http.ListenAndServe(address, nil))
|
||||
// we need to have new instance, because after Shutdown() the Server is not usable
|
||||
httpServer = &http.Server{
|
||||
Addr: address,
|
||||
}
|
||||
err := httpServer.ListenAndServe()
|
||||
if err != http.ErrServerClosed {
|
||||
log.Fatal(err)
|
||||
os.Exit(1)
|
||||
}
|
||||
// We use ErrServerClosed as a sign that we need to rebind on new address, so go back to the start of the loop
|
||||
}
|
||||
}
|
||||
|
||||
// initWorkingDir initializes the ourBinaryDir (basically, we use it as a working dir)
|
||||
|
@ -222,14 +240,6 @@ func cleanup() {
|
|||
}
|
||||
}
|
||||
|
||||
func getInput() (string, error) {
|
||||
scanner := bufio.NewScanner(os.Stdin)
|
||||
scanner.Scan()
|
||||
text := scanner.Text()
|
||||
err := scanner.Err()
|
||||
return text, err
|
||||
}
|
||||
|
||||
// command-line arguments
|
||||
type options struct {
|
||||
verbose bool // is verbose logging enabled
|
||||
|
@ -318,79 +328,3 @@ func loadOptions() options {
|
|||
|
||||
return o
|
||||
}
|
||||
|
||||
func promptAndGet(prompt string) (string, error) {
|
||||
for {
|
||||
fmt.Print(prompt)
|
||||
input, err := getInput()
|
||||
if err != nil {
|
||||
log.Printf("Failed to get input, aborting: %s", err)
|
||||
return "", err
|
||||
}
|
||||
if len(input) != 0 {
|
||||
return input, nil
|
||||
}
|
||||
// try again
|
||||
}
|
||||
}
|
||||
|
||||
func promptAndGetPassword(prompt string) (string, error) {
|
||||
for {
|
||||
fmt.Print(prompt)
|
||||
password, err := terminal.ReadPassword(int(os.Stdin.Fd()))
|
||||
fmt.Print("\n")
|
||||
if err != nil {
|
||||
log.Printf("Failed to get input, aborting: %s", err)
|
||||
return "", err
|
||||
}
|
||||
if len(password) != 0 {
|
||||
return string(password), nil
|
||||
}
|
||||
// try again
|
||||
}
|
||||
}
|
||||
|
||||
func askUsernamePasswordIfPossible() error {
|
||||
configFile := config.getConfigFilename()
|
||||
_, err := os.Stat(configFile)
|
||||
if !os.IsNotExist(err) {
|
||||
// do nothing, file exists
|
||||
return nil
|
||||
}
|
||||
if !terminal.IsTerminal(int(os.Stdin.Fd())) {
|
||||
return nil // do nothing
|
||||
}
|
||||
if !terminal.IsTerminal(int(os.Stdout.Fd())) {
|
||||
return nil // do nothing
|
||||
}
|
||||
fmt.Printf("Would you like to set user/password for the web interface authentication (yes/no)?\n")
|
||||
yesno, err := promptAndGet("Please type 'yes' or 'no': ")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if yesno[0] != 'y' && yesno[0] != 'Y' {
|
||||
return nil
|
||||
}
|
||||
username, err := promptAndGet("Please enter the username: ")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
password, err := promptAndGetPassword("Please enter the password: ")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
password2, err := promptAndGetPassword("Please enter password again: ")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if password2 != password {
|
||||
fmt.Printf("Passwords do not match! Aborting\n")
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
config.AuthName = username
|
||||
config.AuthPass = password
|
||||
return nil
|
||||
}
|
||||
|
|
|
@ -45,9 +45,7 @@
|
|||
}],
|
||||
"class-methods-use-this": "off",
|
||||
"no-shadow": "off",
|
||||
"camelcase": ["error", {
|
||||
"properties": "never"
|
||||
}],
|
||||
"camelcase": "off",
|
||||
"no-console": ["warn", { "allow": ["warn", "error"] }],
|
||||
"import/no-extraneous-dependencies": ["error", { "devDependencies": true }],
|
||||
"import/prefer-default-export": "off"
|
||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -73,7 +73,7 @@
|
|||
"uglifyjs-webpack-plugin": "^1.2.7",
|
||||
"url-loader": "^1.0.1",
|
||||
"webpack": "3.8.1",
|
||||
"webpack-dev-server": "2.9.4",
|
||||
"webpack-dev-server": "^3.1.14",
|
||||
"webpack-merge": "^4.1.3"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,16 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
|
||||
<meta name="theme-color" content="#000000">
|
||||
<link rel="shortcut icon" href="https://adguard.com/img/favicons/favicon.ico">
|
||||
<title>Setup AdGuard Home</title>
|
||||
</head>
|
||||
<body>
|
||||
<noscript>
|
||||
You need to enable JavaScript to run this app.
|
||||
</noscript>
|
||||
<div id="root"></div>
|
||||
</body>
|
||||
</html>
|
|
@ -28,6 +28,7 @@
|
|||
"dhcp_ip_addresses": "IP addresses",
|
||||
"dhcp_table_hostname": "Hostname",
|
||||
"dhcp_table_expires": "Expires",
|
||||
"dhcp_warning": "If you want to enable the built-in DHCP server, make sure that there is no other active DHCP server. Otherwise, it can break the internet for connected devices!",
|
||||
"back": "Back",
|
||||
"dashboard": "Dashboard",
|
||||
"settings": "Settings",
|
||||
|
@ -157,5 +158,57 @@
|
|||
"category_label": "Category",
|
||||
"rule_label": "Rule",
|
||||
"filter_label": "Filter",
|
||||
"unknown_filter": "Unknown filter {{filterId}}"
|
||||
"unknown_filter": "Unknown filter {{filterId}}",
|
||||
"install_welcome_title": "Welcome to AdGuard Home!",
|
||||
"install_welcome_desc": "AdGuard Home is a network-wide ad-and-tracker blocking DNS server. Its purpose is to let you control your entire network and all your devices, and it does not require using a client-side program.",
|
||||
"install_settings_title": "Admin Web Interface",
|
||||
"install_settings_listen": "Listen interface",
|
||||
"install_settings_port": "Port",
|
||||
"install_settings_interface_link": "Your AdGuard Home admin web interface will be available on the following addresses:",
|
||||
"form_error_port": "Enter valid port value",
|
||||
"install_settings_dns": "DNS server",
|
||||
"install_settings_dns_desc": "You will need to configure your devices or router to use the DNS server on the following addresses:",
|
||||
"install_settings_all_interfaces": "All interfaces",
|
||||
"install_auth_title": "Authentication",
|
||||
"install_auth_desc": "It is highly recommended to configure password authentication to your AdGuard Home admin web interface. Even if it is accessible only in your local network, it is still important to have it protected from unrestricted access.",
|
||||
"install_auth_username": "Username",
|
||||
"install_auth_password": "Password",
|
||||
"install_auth_confirm": "Confirm password",
|
||||
"install_auth_username_enter": "Enter username",
|
||||
"install_auth_password_enter": "Enter password",
|
||||
"install_step": "Step",
|
||||
"install_devices_title": "Configure your devices",
|
||||
"install_devices_desc": "In order for AdGuard Home to start working, you need to configure your devices to use it.",
|
||||
"install_submit_title": "Congratulations!",
|
||||
"install_submit_desc": "The setup procedure is finished and you are ready to start using AdGuard Home.",
|
||||
"install_devices_router": "Router",
|
||||
"install_devices_router_desc": "This setup will automatically cover all the devices connected to your home router and you will not need to configure each of them manually.",
|
||||
"install_devices_address": "AdGuard Home DNS server is listening to the following addresses",
|
||||
"install_devices_router_list_1": "Open the preferences for your router. Usually, you can access it from your browser via a URL (like http://192.168.0.1/ or http://192.168.1.1/). You may be asked to enter the password. If you don't remember it, you can often reset the password by pressing a button on the router itself. Some routers require a specific application, which in that case should be already installed on your computer/phone.",
|
||||
"install_devices_router_list_2": "Find the DHCP/DNS settings. Look for the DNS letters next to a field which allows two or three sets of numbers, each broken into four groups of one to three digits.",
|
||||
"install_devices_router_list_3": "Enter your AdGuard Home server addresses there.",
|
||||
"install_devices_windows_list_1": "Open Control Panel through Start menu or Windows search.",
|
||||
"install_devices_windows_list_2": "Go to Network and Internet category and then to Network and Sharing Center.",
|
||||
"install_devices_windows_list_3": "On the left side of the screen find Change adapter settings and click on it.",
|
||||
"install_devices_windows_list_4": "Select your active connection, right-click on it and choose Properties.",
|
||||
"install_devices_windows_list_5": "Find Internet Protocol Version 4 (TCP/IP) in the list, select it and then click on Properties again.",
|
||||
"install_devices_windows_list_6": "Choose Use the following DNS server addresses and enter your AdGuard Home server addresses.",
|
||||
"install_devices_macos_list_1": "Click on Apple icon and go to System Preferences.",
|
||||
"install_devices_macos_list_2": "Click on Network.",
|
||||
"install_devices_macos_list_3": "Select the first connection in your list and click Advanced.",
|
||||
"install_devices_macos_list_4": "Select the DNS tab and enter your AdGuard Home server addresses.",
|
||||
"install_devices_android_list_1": "From the Android Menu home screen, tap Settings.",
|
||||
"install_devices_android_list_2": "Tap Wi-Fi on the menu. The screen listing all of the available networks will be shown (it is impossible to set custom DNS for mobile connection).",
|
||||
"install_devices_android_list_3": "Long press the network you're connected to, and tap Modify Network.",
|
||||
"install_devices_android_list_4": "On some devices, you may need to check the box for Advanced to see further settings. To adjust your Android DNS settings, you will need to switch the IP settings from DHCP to Static.",
|
||||
"install_devices_android_list_5": "Change set DNS 1 and DNS 2 values to your AdGuard Home server addresses.",
|
||||
"install_devices_ios_list_1": "From the home screen, tap Settings.",
|
||||
"install_devices_ios_list_2": "Choose Wi-Fi in the left menu (it is impossible to configure DNS for mobile networks).",
|
||||
"install_devices_ios_list_3": "Tap on the name of the currently active network.",
|
||||
"install_devices_ios_list_4": "In the DNS field enter your AdGuard Home server addresses.",
|
||||
"get_started": "Get Started",
|
||||
"next": "Next",
|
||||
"open_dashboard": "Open Dashboard",
|
||||
"install_saved": "All settings saved",
|
||||
"form_error_password": "Password mismatched"
|
||||
}
|
|
@ -4,6 +4,7 @@ import { t } from 'i18next';
|
|||
import { showLoading, hideLoading } from 'react-redux-loading-bar';
|
||||
|
||||
import { normalizeHistory, normalizeFilteringStatus, normalizeLogs } from '../helpers/helpers';
|
||||
import { SETTINGS_NAMES } from '../helpers/constants';
|
||||
import Api from '../api/Api';
|
||||
|
||||
const apiClient = new Api();
|
||||
|
@ -18,9 +19,8 @@ export const showSettingsFailure = createAction('SETTINGS_FAILURE_SHOW');
|
|||
export const toggleSetting = (settingKey, status) => async (dispatch) => {
|
||||
let successMessage = '';
|
||||
try {
|
||||
// TODO move setting keys to constants
|
||||
switch (settingKey) {
|
||||
case 'filtering':
|
||||
case SETTINGS_NAMES.filtering:
|
||||
if (status) {
|
||||
successMessage = 'disabled_filtering_toast';
|
||||
await apiClient.disableFiltering();
|
||||
|
@ -30,7 +30,7 @@ export const toggleSetting = (settingKey, status) => async (dispatch) => {
|
|||
}
|
||||
dispatch(toggleSettingStatus({ settingKey }));
|
||||
break;
|
||||
case 'safebrowsing':
|
||||
case SETTINGS_NAMES.safebrowsing:
|
||||
if (status) {
|
||||
successMessage = 'disabled_safe_browsing_toast';
|
||||
await apiClient.disableSafebrowsing();
|
||||
|
@ -40,7 +40,7 @@ export const toggleSetting = (settingKey, status) => async (dispatch) => {
|
|||
}
|
||||
dispatch(toggleSettingStatus({ settingKey }));
|
||||
break;
|
||||
case 'parental':
|
||||
case SETTINGS_NAMES.parental:
|
||||
if (status) {
|
||||
successMessage = 'disabled_parental_toast';
|
||||
await apiClient.disableParentalControl();
|
||||
|
@ -50,7 +50,7 @@ export const toggleSetting = (settingKey, status) => async (dispatch) => {
|
|||
}
|
||||
dispatch(toggleSettingStatus({ settingKey }));
|
||||
break;
|
||||
case 'safesearch':
|
||||
case SETTINGS_NAMES.safesearch:
|
||||
if (status) {
|
||||
successMessage = 'disabled_safe_search_toast';
|
||||
await apiClient.disableSafesearch();
|
||||
|
@ -434,7 +434,6 @@ export const downloadQueryLogRequest = createAction('DOWNLOAD_QUERY_LOG_REQUEST'
|
|||
export const downloadQueryLogFailure = createAction('DOWNLOAD_QUERY_LOG_FAILURE');
|
||||
export const downloadQueryLogSuccess = createAction('DOWNLOAD_QUERY_LOG_SUCCESS');
|
||||
|
||||
// TODO create some common flasher with all server errors
|
||||
export const downloadQueryLog = () => async (dispatch) => {
|
||||
let data;
|
||||
dispatch(downloadQueryLogRequest());
|
||||
|
@ -573,20 +572,24 @@ export const setDhcpConfigSuccess = createAction('SET_DHCP_CONFIG_SUCCESS');
|
|||
export const setDhcpConfigFailure = createAction('SET_DHCP_CONFIG_FAILURE');
|
||||
|
||||
// TODO rewrite findActiveDhcp part
|
||||
export const setDhcpConfig = config => async (dispatch) => {
|
||||
export const setDhcpConfig = values => async (dispatch, getState) => {
|
||||
const { config } = getState().dhcp;
|
||||
const updatedConfig = { ...config, ...values };
|
||||
dispatch(setDhcpConfigRequest());
|
||||
try {
|
||||
if (config.interface_name) {
|
||||
if (values.interface_name) {
|
||||
dispatch(findActiveDhcpRequest());
|
||||
try {
|
||||
const activeDhcp = await apiClient.findActiveDhcp(config.interface_name);
|
||||
const activeDhcp = await apiClient.findActiveDhcp(values.interface_name);
|
||||
dispatch(findActiveDhcpSuccess(activeDhcp));
|
||||
|
||||
if (!activeDhcp.found) {
|
||||
await apiClient.setDhcpConfig(config);
|
||||
try {
|
||||
await apiClient.setDhcpConfig(updatedConfig);
|
||||
dispatch(setDhcpConfigSuccess(updatedConfig));
|
||||
dispatch(addSuccessToast('dhcp_config_saved'));
|
||||
dispatch(setDhcpConfigSuccess());
|
||||
dispatch(getDhcpStatus());
|
||||
} catch (error) {
|
||||
dispatch(addErrorToast({ error }));
|
||||
dispatch(setDhcpConfigFailure());
|
||||
}
|
||||
} else {
|
||||
dispatch(addErrorToast({ error: 'dhcp_found' }));
|
||||
}
|
||||
|
@ -595,15 +598,15 @@ export const setDhcpConfig = config => async (dispatch) => {
|
|||
dispatch(findActiveDhcpFailure());
|
||||
}
|
||||
} else {
|
||||
await apiClient.setDhcpConfig(config);
|
||||
try {
|
||||
await apiClient.setDhcpConfig(updatedConfig);
|
||||
dispatch(setDhcpConfigSuccess(updatedConfig));
|
||||
dispatch(addSuccessToast('dhcp_config_saved'));
|
||||
dispatch(setDhcpConfigSuccess());
|
||||
dispatch(getDhcpStatus());
|
||||
}
|
||||
} catch (error) {
|
||||
dispatch(addErrorToast({ error }));
|
||||
dispatch(setDhcpConfigFailure());
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export const toggleDhcpRequest = createAction('TOGGLE_DHCP_REQUEST');
|
||||
|
@ -615,11 +618,10 @@ export const toggleDhcp = config => async (dispatch) => {
|
|||
dispatch(toggleDhcpRequest());
|
||||
|
||||
if (config.enabled) {
|
||||
dispatch(addSuccessToast('disabled_dhcp'));
|
||||
try {
|
||||
await apiClient.setDhcpConfig({ ...config, enabled: false });
|
||||
dispatch(toggleDhcpSuccess());
|
||||
dispatch(getDhcpStatus());
|
||||
dispatch(addSuccessToast('disabled_dhcp'));
|
||||
} catch (error) {
|
||||
dispatch(addErrorToast({ error }));
|
||||
dispatch(toggleDhcpFailure());
|
||||
|
@ -634,12 +636,11 @@ export const toggleDhcp = config => async (dispatch) => {
|
|||
try {
|
||||
await apiClient.setDhcpConfig({ ...config, enabled: true });
|
||||
dispatch(toggleDhcpSuccess());
|
||||
dispatch(getDhcpStatus());
|
||||
dispatch(addSuccessToast('enabled_dhcp'));
|
||||
} catch (error) {
|
||||
dispatch(addErrorToast({ error }));
|
||||
dispatch(toggleDhcpFailure());
|
||||
}
|
||||
dispatch(addSuccessToast('enabled_dhcp'));
|
||||
} else {
|
||||
dispatch(addErrorToast({ error: 'dhcp_found' }));
|
||||
}
|
||||
|
|
|
@ -0,0 +1,46 @@
|
|||
import { createAction } from 'redux-actions';
|
||||
import Api from '../api/Api';
|
||||
import { addErrorToast, addSuccessToast } from './index';
|
||||
|
||||
const apiClient = new Api();
|
||||
|
||||
export const nextStep = createAction('NEXT_STEP');
|
||||
export const prevStep = createAction('PREV_STEP');
|
||||
|
||||
export const getDefaultAddressesRequest = createAction('GET_DEFAULT_ADDRESSES_REQUEST');
|
||||
export const getDefaultAddressesFailure = createAction('GET_DEFAULT_ADDRESSES_FAILURE');
|
||||
export const getDefaultAddressesSuccess = createAction('GET_DEFAULT_ADDRESSES_SUCCESS');
|
||||
|
||||
export const getDefaultAddresses = () => async (dispatch) => {
|
||||
dispatch(getDefaultAddressesRequest());
|
||||
try {
|
||||
const addresses = await apiClient.getDefaultAddresses();
|
||||
dispatch(getDefaultAddressesSuccess(addresses));
|
||||
} catch (error) {
|
||||
dispatch(addErrorToast({ error }));
|
||||
dispatch(getDefaultAddressesFailure());
|
||||
}
|
||||
};
|
||||
|
||||
export const setAllSettingsRequest = createAction('SET_ALL_SETTINGS_REQUEST');
|
||||
export const setAllSettingsFailure = createAction('SET_ALL_SETTINGS_FAILURE');
|
||||
export const setAllSettingsSuccess = createAction('SET_ALL_SETTINGS_SUCCESS');
|
||||
|
||||
export const setAllSettings = values => async (dispatch) => {
|
||||
dispatch(setAllSettingsRequest());
|
||||
try {
|
||||
const {
|
||||
confirm_password,
|
||||
...config
|
||||
} = values;
|
||||
|
||||
await apiClient.setAllSettings(config);
|
||||
dispatch(setAllSettingsSuccess());
|
||||
dispatch(addSuccessToast('install_saved'));
|
||||
dispatch(nextStep());
|
||||
} catch (error) {
|
||||
dispatch(addErrorToast({ error }));
|
||||
dispatch(setAllSettingsFailure());
|
||||
dispatch(prevStep());
|
||||
}
|
||||
};
|
|
@ -336,4 +336,22 @@ export default class Api {
|
|||
};
|
||||
return this.makeRequest(path, method, parameters);
|
||||
}
|
||||
|
||||
// Installation
|
||||
INSTALL_GET_ADDRESSES = { path: 'install/get_addresses', method: 'GET' };
|
||||
INSTALL_CONFIGURE = { path: 'install/configure', method: 'POST' };
|
||||
|
||||
getDefaultAddresses() {
|
||||
const { path, method } = this.INSTALL_GET_ADDRESSES;
|
||||
return this.makeRequest(path, method);
|
||||
}
|
||||
|
||||
setAllSettings(config) {
|
||||
const { path, method } = this.INSTALL_CONFIGURE;
|
||||
const parameters = {
|
||||
data: config,
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
};
|
||||
return this.makeRequest(path, method, parameters);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
font-family: sans-serif;
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Helvetica Neue", Arial, sans-serif;
|
||||
}
|
||||
|
||||
.status {
|
||||
|
@ -26,3 +26,7 @@ body {
|
|||
height: 3px;
|
||||
background: linear-gradient(45deg, rgba(99, 125, 120, 1) 0%, rgba(88, 177, 101, 1) 100%);
|
||||
}
|
||||
|
||||
.hidden {
|
||||
display: none;
|
||||
}
|
||||
|
|
|
@ -25,7 +25,7 @@ class UserRules extends Component {
|
|||
<textarea className="form-control form-control--textarea-large" value={this.props.userRules} onChange={this.handleChange} />
|
||||
<div className="card-actions">
|
||||
<button
|
||||
className="btn btn-success btn-standart"
|
||||
className="btn btn-success btn-standard"
|
||||
type="submit"
|
||||
onClick={this.handleSubmit}
|
||||
>
|
||||
|
|
|
@ -84,7 +84,7 @@ class Filters extends Component {
|
|||
columns={this.columns}
|
||||
showPagination={true}
|
||||
defaultPageSize={10}
|
||||
minRows={4} // TODO find out what to show if rules.length is 0
|
||||
minRows={4}
|
||||
// Text
|
||||
previousText={ t('previous_btn') }
|
||||
nextText={ t('next_btn') }
|
||||
|
@ -96,14 +96,14 @@ class Filters extends Component {
|
|||
/>
|
||||
<div className="card-actions">
|
||||
<button
|
||||
className="btn btn-success btn-standart mr-2"
|
||||
className="btn btn-success btn-standard mr-2"
|
||||
type="submit"
|
||||
onClick={this.props.toggleFilteringModal}
|
||||
>
|
||||
<Trans>add_filter_btn</Trans>
|
||||
</button>
|
||||
<button
|
||||
className="btn btn-primary btn-standart"
|
||||
className="btn btn-primary btn-standard"
|
||||
type="submit"
|
||||
onClick={this.props.refreshFilters}
|
||||
disabled={processingRefreshFilters}
|
||||
|
|
|
@ -6,7 +6,7 @@ import { Trans, withNamespaces } from 'react-i18next';
|
|||
|
||||
import Menu from './Menu';
|
||||
import Version from './Version';
|
||||
import logo from './logo.svg';
|
||||
import logo from '../ui/svg/logo.svg';
|
||||
import './Header.css';
|
||||
|
||||
class Header extends Component {
|
||||
|
|
|
@ -48,8 +48,9 @@ const Form = (props) => {
|
|||
const {
|
||||
t,
|
||||
handleSubmit,
|
||||
pristine,
|
||||
submitting,
|
||||
invalid,
|
||||
processingConfig,
|
||||
} = props;
|
||||
|
||||
return (
|
||||
|
@ -124,8 +125,8 @@ const Form = (props) => {
|
|||
|
||||
<button
|
||||
type="submit"
|
||||
className="btn btn-success btn-standart"
|
||||
disabled={pristine || submitting}
|
||||
className="btn btn-success btn-standard"
|
||||
disabled={submitting || invalid || processingConfig}
|
||||
>
|
||||
{t('save_config')}
|
||||
</button>
|
||||
|
@ -135,11 +136,11 @@ const Form = (props) => {
|
|||
|
||||
Form.propTypes = {
|
||||
handleSubmit: PropTypes.func,
|
||||
pristine: PropTypes.bool,
|
||||
submitting: PropTypes.bool,
|
||||
invalid: PropTypes.bool,
|
||||
interfaces: PropTypes.object,
|
||||
processing: PropTypes.bool,
|
||||
initialValues: PropTypes.object,
|
||||
processingConfig: PropTypes.bool,
|
||||
t: PropTypes.func,
|
||||
};
|
||||
|
||||
|
|
|
@ -13,17 +13,14 @@ class Dhcp extends Component {
|
|||
this.props.setDhcpConfig(values);
|
||||
};
|
||||
|
||||
handleFormChange = (value) => {
|
||||
this.props.setDhcpConfig(value);
|
||||
}
|
||||
|
||||
handleToggle = (config) => {
|
||||
this.props.toggleDhcp(config);
|
||||
this.props.findActiveDhcp(config.interface_name);
|
||||
}
|
||||
|
||||
getToggleDhcpButton = () => {
|
||||
const { config, active, processingDhcp } = this.props.dhcp;
|
||||
const {
|
||||
config, active, processingDhcp, processingConfig,
|
||||
} = this.props.dhcp;
|
||||
const activeDhcpFound = active && active.found;
|
||||
const filledConfig = Object.keys(config).every((key) => {
|
||||
if (key === 'enabled') {
|
||||
|
@ -37,9 +34,9 @@ class Dhcp extends Component {
|
|||
return (
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-standart mr-2 btn-gray"
|
||||
className="btn btn-standard mr-2 btn-gray"
|
||||
onClick={() => this.props.toggleDhcp(config)}
|
||||
disabled={processingDhcp}
|
||||
disabled={processingDhcp || processingConfig}
|
||||
>
|
||||
<Trans>dhcp_disable</Trans>
|
||||
</button>
|
||||
|
@ -49,9 +46,14 @@ class Dhcp extends Component {
|
|||
return (
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-standart mr-2 btn-success"
|
||||
className="btn btn-standard mr-2 btn-success"
|
||||
onClick={() => this.handleToggle(config)}
|
||||
disabled={!filledConfig || activeDhcpFound || processingDhcp}
|
||||
disabled={
|
||||
!filledConfig
|
||||
|| activeDhcpFound
|
||||
|| processingDhcp
|
||||
|| processingConfig
|
||||
}
|
||||
>
|
||||
<Trans>dhcp_enable</Trans>
|
||||
</button>
|
||||
|
@ -64,14 +66,14 @@ class Dhcp extends Component {
|
|||
if (active) {
|
||||
if (active.error) {
|
||||
return (
|
||||
<div className="text-danger">
|
||||
<div className="text-danger mb-2">
|
||||
{active.error}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
<div className="mb-2">
|
||||
{active.found ? (
|
||||
<div className="text-danger">
|
||||
<Trans>dhcp_found</Trans>
|
||||
|
@ -81,7 +83,7 @@ class Dhcp extends Component {
|
|||
<Trans>dhcp_not_found</Trans>
|
||||
</div>
|
||||
)}
|
||||
</Fragment>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -91,9 +93,14 @@ class Dhcp extends Component {
|
|||
render() {
|
||||
const { t, dhcp } = this.props;
|
||||
const statusButtonClass = classnames({
|
||||
'btn btn-primary btn-standart': true,
|
||||
'btn btn-primary btn-standart btn-loading': dhcp.processingStatus,
|
||||
'btn btn-primary btn-standard': true,
|
||||
'btn btn-primary btn-standard btn-loading': dhcp.processingStatus,
|
||||
});
|
||||
const {
|
||||
enabled,
|
||||
interface_name,
|
||||
...values
|
||||
} = dhcp.config;
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
|
@ -102,17 +109,17 @@ class Dhcp extends Component {
|
|||
{!dhcp.processing &&
|
||||
<Fragment>
|
||||
<Interface
|
||||
onChange={this.handleFormChange}
|
||||
initialValues={dhcp.config}
|
||||
onChange={this.handleFormSubmit}
|
||||
initialValues={{ interface_name }}
|
||||
interfaces={dhcp.interfaces}
|
||||
processing={dhcp.processingInterfaces}
|
||||
enabled={dhcp.config.enabled}
|
||||
/>
|
||||
<Form
|
||||
onSubmit={this.handleFormSubmit}
|
||||
initialValues={dhcp.config}
|
||||
initialValues={{ ...values }}
|
||||
interfaces={dhcp.interfaces}
|
||||
processing={dhcp.processingInterfaces}
|
||||
processingConfig={dhcp.processingConfig}
|
||||
/>
|
||||
<hr/>
|
||||
<div className="card-actions mb-3">
|
||||
|
@ -123,12 +130,18 @@ class Dhcp extends Component {
|
|||
onClick={() =>
|
||||
this.props.findActiveDhcp(dhcp.config.interface_name)
|
||||
}
|
||||
disabled={!dhcp.config.interface_name}
|
||||
disabled={
|
||||
!dhcp.config.interface_name
|
||||
|| dhcp.processingConfig
|
||||
}
|
||||
>
|
||||
<Trans>check_dhcp_servers</Trans>
|
||||
</button>
|
||||
</div>
|
||||
{this.getActiveDhcpMessage()}
|
||||
<div className="text-danger">
|
||||
<Trans>dhcp_warning</Trans>
|
||||
</div>
|
||||
</Fragment>
|
||||
}
|
||||
</div>
|
||||
|
|
|
@ -11,7 +11,7 @@
|
|||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.btn-standart {
|
||||
.btn-standard {
|
||||
padding-left: 20px;
|
||||
padding-right: 20px;
|
||||
}
|
||||
|
|
|
@ -21,8 +21,8 @@ class Upstream extends Component {
|
|||
|
||||
render() {
|
||||
const testButtonClass = classnames({
|
||||
'btn btn-primary btn-standart mr-2': true,
|
||||
'btn btn-primary btn-standart mr-2 btn-loading': this.props.processingTestUpstream,
|
||||
'btn btn-primary btn-standard mr-2': true,
|
||||
'btn btn-primary btn-standard mr-2 btn-loading': this.props.processingTestUpstream,
|
||||
});
|
||||
const { t } = this.props;
|
||||
|
||||
|
@ -49,7 +49,7 @@ class Upstream extends Component {
|
|||
<Trans>test_upstream_btn</Trans>
|
||||
</button>
|
||||
<button
|
||||
className="btn btn-success btn-standart"
|
||||
className="btn btn-success btn-standard"
|
||||
type="submit"
|
||||
onClick={this.handleSubmit}
|
||||
>
|
||||
|
|
File diff suppressed because one or more lines are too long
|
@ -0,0 +1,41 @@
|
|||
import React, { Component } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import classnames from 'classnames';
|
||||
|
||||
class Tab extends Component {
|
||||
handleClick = () => {
|
||||
this.props.onClick(this.props.label);
|
||||
}
|
||||
|
||||
render() {
|
||||
const {
|
||||
activeTab,
|
||||
label,
|
||||
} = this.props;
|
||||
|
||||
const tabClass = classnames({
|
||||
tab__control: true,
|
||||
'tab__control--active': activeTab === label,
|
||||
});
|
||||
|
||||
return (
|
||||
<div
|
||||
className={tabClass}
|
||||
onClick={this.handleClick}
|
||||
>
|
||||
<svg className="tab__icon">
|
||||
<use xlinkHref={`#${label.toLowerCase()}`} />
|
||||
</svg>
|
||||
{label}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Tab.propTypes = {
|
||||
activeTab: PropTypes.string.isRequired,
|
||||
label: PropTypes.string.isRequired,
|
||||
onClick: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
export default Tab;
|
|
@ -3783,7 +3783,7 @@ tbody.collapse.show {
|
|||
line-height: 1.5;
|
||||
color: #495057;
|
||||
vertical-align: middle;
|
||||
background: #fff url("data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 10 5'%3E%3Cpath fill='#999' d='M0 0L10 0L5 5L0 0'/%3E%3C/svg%3E") no-repeat right 0.75rem center;
|
||||
background: #fff url("data:image/svg+xml;base64,PHN2ZyB4bWxucz0naHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmcnIHZpZXdCb3g9JzAgMCAxMCA1Jz48cGF0aCBmaWxsPScjOTk5JyBkPSdNMCAwTDEwIDBMNSA1TDAgMCcvPjwvc3ZnPg==") no-repeat right 0.75rem center;
|
||||
background-size: 8px 10px;
|
||||
border: 1px solid rgba(0, 40, 100, 0.12);
|
||||
border-radius: 3px;
|
||||
|
|
|
@ -0,0 +1,51 @@
|
|||
.tabs__controls {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 15px;
|
||||
padding: 15px 0;
|
||||
border-bottom: 1px solid #e8e8e8;
|
||||
}
|
||||
|
||||
.tab__control {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
min-width: 70px;
|
||||
font-size: 13px;
|
||||
color: #555555;
|
||||
cursor: pointer;
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
.tab__control:hover,
|
||||
.tab__control:focus {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.tab__control--active {
|
||||
font-weight: 700;
|
||||
color: #4a4a4a;
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.tab__title {
|
||||
margin-bottom: 10px;
|
||||
font-size: 16px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.tab__icon {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
margin-bottom: 6px;
|
||||
fill: #4a4a4a;
|
||||
}
|
||||
|
||||
.tab__text {
|
||||
line-height: 1.7;
|
||||
}
|
||||
|
||||
.tab__text li,
|
||||
.tab__text p {
|
||||
margin-bottom: 5px;
|
||||
}
|
|
@ -0,0 +1,59 @@
|
|||
import React, { Component } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import Tab from './Tab';
|
||||
import './Tabs.css';
|
||||
|
||||
class Tabs extends Component {
|
||||
state = {
|
||||
activeTab: this.props.children[0].props.label,
|
||||
};
|
||||
|
||||
onClickTabControl = (tab) => {
|
||||
this.setState({ activeTab: tab });
|
||||
}
|
||||
|
||||
render() {
|
||||
const {
|
||||
props: {
|
||||
children,
|
||||
},
|
||||
state: {
|
||||
activeTab,
|
||||
},
|
||||
} = this;
|
||||
|
||||
return (
|
||||
<div className="tabs">
|
||||
<div className="tabs__controls">
|
||||
{children.map((child) => {
|
||||
const { label } = child.props;
|
||||
|
||||
return (
|
||||
<Tab
|
||||
key={label}
|
||||
label={label}
|
||||
activeTab={activeTab}
|
||||
onClick={this.onClickTabControl}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
<div className="tabs__content">
|
||||
{children.map((child) => {
|
||||
if (child.props.label !== activeTab) {
|
||||
return false;
|
||||
}
|
||||
return child.props.children;
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Tabs.propTypes = {
|
||||
children: PropTypes.array.isRequired,
|
||||
};
|
||||
|
||||
export default Tabs;
|
Before Width: | Height: | Size: 4.0 KiB After Width: | Height: | Size: 4.0 KiB |
|
@ -60,3 +60,16 @@ export const LANGUAGES = [
|
|||
name: '正體中文',
|
||||
},
|
||||
];
|
||||
|
||||
export const INSTALL_FIRST_STEP = 1;
|
||||
export const INSTALL_TOTAL_STEPS = 5;
|
||||
|
||||
export const SETTINGS_NAMES = {
|
||||
filtering: 'filtering',
|
||||
safebrowsing: 'safebrowsing',
|
||||
parental: 'parental',
|
||||
safesearch: 'safesearch',
|
||||
};
|
||||
|
||||
export const STANDARD_DNS_PORT = 53;
|
||||
export const STANDARD_WEB_PORT = 80;
|
||||
|
|
|
@ -4,7 +4,7 @@ import subHours from 'date-fns/sub_hours';
|
|||
import addHours from 'date-fns/add_hours';
|
||||
import round from 'lodash/round';
|
||||
|
||||
import { STATS_NAMES } from './constants';
|
||||
import { STATS_NAMES, STANDARD_DNS_PORT, STANDARD_WEB_PORT } from './constants';
|
||||
|
||||
export const formatTime = (time) => {
|
||||
const parsedTime = dateParse(time);
|
||||
|
@ -85,3 +85,58 @@ export const getPercent = (amount, number) => {
|
|||
};
|
||||
|
||||
export const captitalizeWords = text => text.split(/[ -_]/g).map(str => str.charAt(0).toUpperCase() + str.substr(1)).join(' ');
|
||||
|
||||
export const getInterfaceIp = (option) => {
|
||||
const onlyIPv6 = option.ip_addresses.every(ip => ip.includes(':'));
|
||||
let interfaceIP = option.ip_addresses[0];
|
||||
|
||||
if (!onlyIPv6) {
|
||||
option.ip_addresses.forEach((ip) => {
|
||||
if (!ip.includes(':')) {
|
||||
interfaceIP = ip;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return interfaceIP;
|
||||
};
|
||||
|
||||
export const getIpList = (interfaces) => {
|
||||
let list = [];
|
||||
|
||||
Object.keys(interfaces).forEach((item) => {
|
||||
list = [...list, ...interfaces[item].ip_addresses];
|
||||
});
|
||||
|
||||
return list.sort();
|
||||
};
|
||||
|
||||
export const getDnsAddress = (ip, port = '') => {
|
||||
const isStandardDnsPort = port === STANDARD_DNS_PORT;
|
||||
let address = ip;
|
||||
|
||||
if (port) {
|
||||
if (ip.includes(':') && !isStandardDnsPort) {
|
||||
address = `[${ip}]:${port}`;
|
||||
} else if (!isStandardDnsPort) {
|
||||
address = `${ip}:${port}`;
|
||||
}
|
||||
}
|
||||
|
||||
return address;
|
||||
};
|
||||
|
||||
export const getWebAddress = (ip, port = '') => {
|
||||
const isStandardWebPort = port === STANDARD_WEB_PORT;
|
||||
let address = `http://${ip}`;
|
||||
|
||||
if (port) {
|
||||
if (ip.includes(':') && !isStandardWebPort) {
|
||||
address = `http://[${ip}]:${port}`;
|
||||
} else if (!isStandardWebPort) {
|
||||
address = `http://${ip}:${port}`;
|
||||
}
|
||||
}
|
||||
|
||||
return address;
|
||||
};
|
||||
|
|
|
@ -0,0 +1,60 @@
|
|||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { getIpList, getDnsAddress, getWebAddress } from '../../helpers/helpers';
|
||||
|
||||
const AddressList = (props) => {
|
||||
let webAddress = getWebAddress(props.address, props.port);
|
||||
let dnsAddress = getDnsAddress(props.address, props.port);
|
||||
|
||||
if (props.address === '0.0.0.0') {
|
||||
return getIpList(props.interfaces).map((ip) => {
|
||||
webAddress = getWebAddress(ip, props.port);
|
||||
dnsAddress = getDnsAddress(ip, props.port);
|
||||
|
||||
if (props.isDns) {
|
||||
return (
|
||||
<li key={ip}>
|
||||
<strong>
|
||||
{dnsAddress}
|
||||
</strong>
|
||||
</li>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<li key={ip}>
|
||||
<a href={webAddress}>
|
||||
{webAddress}
|
||||
</a>
|
||||
</li>
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
if (props.isDns) {
|
||||
return (
|
||||
<strong>
|
||||
{dnsAddress}
|
||||
</strong>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<a href={webAddress}>
|
||||
{webAddress}
|
||||
</a>
|
||||
);
|
||||
};
|
||||
|
||||
AddressList.propTypes = {
|
||||
interfaces: PropTypes.object.isRequired,
|
||||
address: PropTypes.string.isRequired,
|
||||
port: PropTypes.oneOfType([
|
||||
PropTypes.string,
|
||||
PropTypes.number,
|
||||
]),
|
||||
isDns: PropTypes.bool,
|
||||
};
|
||||
|
||||
export default AddressList;
|
|
@ -0,0 +1,108 @@
|
|||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { Field, reduxForm } from 'redux-form';
|
||||
import { withNamespaces, Trans } from 'react-i18next';
|
||||
import flow from 'lodash/flow';
|
||||
|
||||
import i18n from '../../i18n';
|
||||
import Controls from './Controls';
|
||||
import renderField from './renderField';
|
||||
|
||||
const required = (value) => {
|
||||
if (value || value === 0) {
|
||||
return false;
|
||||
}
|
||||
return <Trans>form_error_required</Trans>;
|
||||
};
|
||||
|
||||
const validate = (values) => {
|
||||
const errors = {};
|
||||
|
||||
if (values.confirm_password !== values.password) {
|
||||
errors.confirm_password = i18n.t('form_error_password');
|
||||
}
|
||||
|
||||
return errors;
|
||||
};
|
||||
|
||||
const Auth = (props) => {
|
||||
const {
|
||||
handleSubmit,
|
||||
pristine,
|
||||
invalid,
|
||||
t,
|
||||
} = props;
|
||||
|
||||
return (
|
||||
<form className="setup__step" onSubmit={handleSubmit}>
|
||||
<div className="setup__group">
|
||||
<div className="setup__subtitle">
|
||||
<Trans>install_auth_title</Trans>
|
||||
</div>
|
||||
<p className="setup__desc">
|
||||
<Trans>install_auth_desc</Trans>
|
||||
</p>
|
||||
<div className="form-group">
|
||||
<label>
|
||||
<Trans>install_auth_username</Trans>
|
||||
</label>
|
||||
<Field
|
||||
name="username"
|
||||
component={renderField}
|
||||
type="text"
|
||||
className="form-control"
|
||||
placeholder={ t('install_auth_username_enter') }
|
||||
validate={[required]}
|
||||
autoComplete="username"
|
||||
/>
|
||||
</div>
|
||||
<div className="form-group">
|
||||
<label>
|
||||
<Trans>install_auth_password</Trans>
|
||||
</label>
|
||||
<Field
|
||||
name="password"
|
||||
component={renderField}
|
||||
type="password"
|
||||
className="form-control"
|
||||
placeholder={ t('install_auth_password_enter') }
|
||||
validate={[required]}
|
||||
autoComplete="new-password"
|
||||
/>
|
||||
</div>
|
||||
<div className="form-group">
|
||||
<label>
|
||||
<Trans>install_auth_confirm</Trans>
|
||||
</label>
|
||||
<Field
|
||||
name="confirm_password"
|
||||
component={renderField}
|
||||
type="password"
|
||||
className="form-control"
|
||||
placeholder={ t('install_auth_confirm') }
|
||||
validate={[required]}
|
||||
autoComplete="new-password"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<Controls pristine={pristine} invalid={invalid} />
|
||||
</form>
|
||||
);
|
||||
};
|
||||
|
||||
Auth.propTypes = {
|
||||
handleSubmit: PropTypes.func.isRequired,
|
||||
pristine: PropTypes.bool.isRequired,
|
||||
invalid: PropTypes.bool.isRequired,
|
||||
t: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
export default flow([
|
||||
withNamespaces(),
|
||||
reduxForm({
|
||||
form: 'install',
|
||||
destroyOnUnmount: false,
|
||||
forceUnregisterOnUnmount: true,
|
||||
validate,
|
||||
}),
|
||||
])(Auth);
|
|
@ -0,0 +1,113 @@
|
|||
import React, { Component } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import PropTypes from 'prop-types';
|
||||
import { Trans } from 'react-i18next';
|
||||
|
||||
import * as actionCreators from '../../actions/install';
|
||||
|
||||
class Controls extends Component {
|
||||
renderPrevButton(step) {
|
||||
switch (step) {
|
||||
case 2:
|
||||
case 3:
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-secondary btn-lg setup__button"
|
||||
onClick={this.props.prevStep}
|
||||
>
|
||||
<Trans>back</Trans>
|
||||
</button>
|
||||
);
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
renderNextButton(step) {
|
||||
switch (step) {
|
||||
case 1:
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-success btn-lg setup__button"
|
||||
onClick={this.props.nextStep}
|
||||
>
|
||||
<Trans>get_started</Trans>
|
||||
</button>
|
||||
);
|
||||
case 2:
|
||||
case 3:
|
||||
return (
|
||||
<button
|
||||
type="submit"
|
||||
className="btn btn-success btn-lg setup__button"
|
||||
disabled={
|
||||
this.props.invalid
|
||||
|| this.props.pristine
|
||||
|| this.props.install.processingSubmit
|
||||
}
|
||||
>
|
||||
<Trans>next</Trans>
|
||||
</button>
|
||||
);
|
||||
case 4:
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-success btn-lg setup__button"
|
||||
onClick={this.props.nextStep}
|
||||
>
|
||||
<Trans>next</Trans>
|
||||
</button>
|
||||
);
|
||||
case 5:
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-success btn-lg setup__button"
|
||||
onClick={() => this.props.openDashboard(this.props.address)}
|
||||
>
|
||||
<Trans>open_dashboard</Trans>
|
||||
</button>
|
||||
);
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
const { install } = this.props;
|
||||
|
||||
return (
|
||||
<div className="setup__nav">
|
||||
<div className="btn-list">
|
||||
{this.renderPrevButton(install.step)}
|
||||
{this.renderNextButton(install.step)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Controls.propTypes = {
|
||||
install: PropTypes.object.isRequired,
|
||||
nextStep: PropTypes.func,
|
||||
prevStep: PropTypes.func,
|
||||
openDashboard: PropTypes.func,
|
||||
submitting: PropTypes.bool,
|
||||
invalid: PropTypes.bool,
|
||||
pristine: PropTypes.bool,
|
||||
address: PropTypes.string,
|
||||
};
|
||||
|
||||
const mapStateToProps = (state) => {
|
||||
const { install } = state;
|
||||
const props = { install };
|
||||
return props;
|
||||
};
|
||||
|
||||
export default connect(
|
||||
mapStateToProps,
|
||||
actionCreators,
|
||||
)(Controls);
|
|
@ -0,0 +1,134 @@
|
|||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { connect } from 'react-redux';
|
||||
import { reduxForm, formValueSelector } from 'redux-form';
|
||||
import { Trans, withNamespaces } from 'react-i18next';
|
||||
import flow from 'lodash/flow';
|
||||
|
||||
import Tabs from '../../components/ui/Tabs';
|
||||
import Icons from '../../components/ui/Icons';
|
||||
import Controls from './Controls';
|
||||
import AddressList from './AddressList';
|
||||
|
||||
let Devices = props => (
|
||||
<div className="setup__step">
|
||||
<div className="setup__group">
|
||||
<div className="setup__subtitle">
|
||||
<Trans>install_devices_title</Trans>
|
||||
</div>
|
||||
<div className="setup__desc">
|
||||
<Trans>install_devices_desc</Trans>
|
||||
<div className="mt-1">
|
||||
<Trans>install_devices_address</Trans>:
|
||||
</div>
|
||||
<div className="mt-1">
|
||||
<AddressList
|
||||
interfaces={props.interfaces}
|
||||
address={props.dnsIp}
|
||||
port={props.dnsPort}
|
||||
isDns={true}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<Icons />
|
||||
<Tabs>
|
||||
<div label="Router">
|
||||
<div className="tab__title">
|
||||
<Trans>install_devices_router</Trans>
|
||||
</div>
|
||||
<div className="tab__text">
|
||||
<p><Trans>install_devices_router_desc</Trans></p>
|
||||
<ol>
|
||||
<li><Trans>install_devices_router_list_1</Trans></li>
|
||||
<li><Trans>install_devices_router_list_2</Trans></li>
|
||||
<li><Trans>install_devices_router_list_3</Trans></li>
|
||||
</ol>
|
||||
</div>
|
||||
</div>
|
||||
<div label="Windows">
|
||||
<div className="tab__title">
|
||||
Windows
|
||||
</div>
|
||||
<div className="tab__text">
|
||||
<ol>
|
||||
<li><Trans>install_devices_windows_list_1</Trans></li>
|
||||
<li><Trans>install_devices_windows_list_2</Trans></li>
|
||||
<li><Trans>install_devices_windows_list_3</Trans></li>
|
||||
<li><Trans>install_devices_windows_list_4</Trans></li>
|
||||
<li><Trans>install_devices_windows_list_5</Trans></li>
|
||||
<li><Trans>install_devices_windows_list_6</Trans></li>
|
||||
</ol>
|
||||
</div>
|
||||
</div>
|
||||
<div label="macOS">
|
||||
<div className="tab__title">
|
||||
macOS
|
||||
</div>
|
||||
<div className="tab__text">
|
||||
<ol>
|
||||
<li><Trans>install_devices_macos_list_1</Trans></li>
|
||||
<li><Trans>install_devices_macos_list_2</Trans></li>
|
||||
<li><Trans>install_devices_macos_list_3</Trans></li>
|
||||
<li><Trans>install_devices_macos_list_4</Trans></li>
|
||||
</ol>
|
||||
</div>
|
||||
</div>
|
||||
<div label="Android">
|
||||
<div className="tab__title">
|
||||
Android
|
||||
</div>
|
||||
<div className="tab__text">
|
||||
<ol>
|
||||
<li><Trans>install_devices_android_list_1</Trans></li>
|
||||
<li><Trans>install_devices_android_list_2</Trans></li>
|
||||
<li><Trans>install_devices_android_list_3</Trans></li>
|
||||
<li><Trans>install_devices_android_list_4</Trans></li>
|
||||
<li><Trans>install_devices_android_list_5</Trans></li>
|
||||
</ol>
|
||||
</div>
|
||||
</div>
|
||||
<div label="iOS">
|
||||
<div className="tab__title">
|
||||
iOS
|
||||
</div>
|
||||
<div className="tab__text">
|
||||
<ol>
|
||||
<li><Trans>install_devices_ios_list_1</Trans></li>
|
||||
<li><Trans>install_devices_ios_list_2</Trans></li>
|
||||
<li><Trans>install_devices_ios_list_3</Trans></li>
|
||||
<li><Trans>install_devices_ios_list_4</Trans></li>
|
||||
</ol>
|
||||
</div>
|
||||
</div>
|
||||
</Tabs>
|
||||
</div>
|
||||
<Controls />
|
||||
</div>
|
||||
);
|
||||
|
||||
Devices.propTypes = {
|
||||
interfaces: PropTypes.object.isRequired,
|
||||
dnsIp: PropTypes.string.isRequired,
|
||||
dnsPort: PropTypes.number.isRequired,
|
||||
};
|
||||
|
||||
const selector = formValueSelector('install');
|
||||
|
||||
Devices = connect((state) => {
|
||||
const dnsIp = selector(state, 'dns.ip');
|
||||
const dnsPort = selector(state, 'dns.port');
|
||||
|
||||
return {
|
||||
dnsIp,
|
||||
dnsPort,
|
||||
};
|
||||
})(Devices);
|
||||
|
||||
export default flow([
|
||||
withNamespaces(),
|
||||
reduxForm({
|
||||
form: 'install',
|
||||
destroyOnUnmount: false,
|
||||
forceUnregisterOnUnmount: true,
|
||||
}),
|
||||
])(Devices);
|
|
@ -0,0 +1,23 @@
|
|||
import React, { Component } from 'react';
|
||||
import { Trans, withNamespaces } from 'react-i18next';
|
||||
import Controls from './Controls';
|
||||
|
||||
class Greeting extends Component {
|
||||
render() {
|
||||
return (
|
||||
<div className="setup__step">
|
||||
<div className="setup__group">
|
||||
<h1 className="setup__title">
|
||||
<Trans>install_welcome_title</Trans>
|
||||
</h1>
|
||||
<p className="setup__desc text-center">
|
||||
<Trans>install_welcome_desc</Trans>
|
||||
</p>
|
||||
</div>
|
||||
<Controls />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default withNamespaces()(Greeting);
|
|
@ -0,0 +1,25 @@
|
|||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { Trans, withNamespaces } from 'react-i18next';
|
||||
|
||||
import { INSTALL_TOTAL_STEPS } from '../../helpers/constants';
|
||||
|
||||
const getProgressPercent = step => (step / INSTALL_TOTAL_STEPS) * 100;
|
||||
|
||||
const Progress = props => (
|
||||
<div className="setup__progress">
|
||||
<Trans>install_step</Trans> {props.step}/{INSTALL_TOTAL_STEPS}
|
||||
<div className="setup__progress-wrap">
|
||||
<div
|
||||
className="setup__progress-inner"
|
||||
style={{ width: `${getProgressPercent(props.step)}%` }}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
Progress.propTypes = {
|
||||
step: PropTypes.number.isRequired,
|
||||
};
|
||||
|
||||
export default withNamespaces()(Progress);
|
|
@ -0,0 +1,221 @@
|
|||
import React from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import PropTypes from 'prop-types';
|
||||
import { Field, reduxForm, formValueSelector } from 'redux-form';
|
||||
import { Trans, withNamespaces } from 'react-i18next';
|
||||
import flow from 'lodash/flow';
|
||||
|
||||
import Controls from './Controls';
|
||||
import AddressList from './AddressList';
|
||||
import renderField from './renderField';
|
||||
import { getInterfaceIp } from '../../helpers/helpers';
|
||||
|
||||
const required = (value) => {
|
||||
if (value || value === 0) {
|
||||
return false;
|
||||
}
|
||||
return <Trans>form_error_required</Trans>;
|
||||
};
|
||||
|
||||
const port = (value) => {
|
||||
if (value < 1 || value > 65535) {
|
||||
return <Trans>form_error_port</Trans>;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
const toNumber = value => value && parseInt(value, 10);
|
||||
|
||||
const renderInterfaces = (interfaces => (
|
||||
Object.keys(interfaces).map((item) => {
|
||||
const option = interfaces[item];
|
||||
const { name } = option;
|
||||
|
||||
if (option.ip_addresses && option.ip_addresses.length > 0) {
|
||||
const ip = getInterfaceIp(option);
|
||||
|
||||
return (
|
||||
<option value={ip} key={name}>
|
||||
{name} - {ip}
|
||||
</option>
|
||||
);
|
||||
}
|
||||
|
||||
return false;
|
||||
})
|
||||
));
|
||||
|
||||
let Settings = (props) => {
|
||||
const {
|
||||
handleSubmit,
|
||||
webIp,
|
||||
webPort,
|
||||
dnsIp,
|
||||
dnsPort,
|
||||
interfaces,
|
||||
invalid,
|
||||
webWarning,
|
||||
dnsWarning,
|
||||
} = props;
|
||||
|
||||
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="0.0.0.0">
|
||||
<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}
|
||||
/>
|
||||
</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>
|
||||
{webWarning &&
|
||||
<div className="text-danger mt-2">
|
||||
{webWarning}
|
||||
</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="0.0.0.0">
|
||||
<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="dns.port"
|
||||
component={renderField}
|
||||
type="number"
|
||||
className="form-control"
|
||||
placeholder="80"
|
||||
validate={[port, required]}
|
||||
normalize={toNumber}
|
||||
/>
|
||||
</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>
|
||||
);
|
||||
};
|
||||
|
||||
Settings.propTypes = {
|
||||
handleSubmit: PropTypes.func.isRequired,
|
||||
webIp: PropTypes.string.isRequired,
|
||||
dnsIp: PropTypes.string.isRequired,
|
||||
webPort: PropTypes.oneOfType([
|
||||
PropTypes.string,
|
||||
PropTypes.number,
|
||||
]),
|
||||
dnsPort: PropTypes.oneOfType([
|
||||
PropTypes.string,
|
||||
PropTypes.number,
|
||||
]),
|
||||
webWarning: PropTypes.string.isRequired,
|
||||
dnsWarning: PropTypes.string.isRequired,
|
||||
interfaces: PropTypes.object.isRequired,
|
||||
invalid: PropTypes.bool.isRequired,
|
||||
initialValues: PropTypes.object,
|
||||
};
|
||||
|
||||
const selector = formValueSelector('install');
|
||||
|
||||
Settings = connect((state) => {
|
||||
const webIp = selector(state, 'web.ip');
|
||||
const webPort = selector(state, 'web.port');
|
||||
const dnsIp = selector(state, 'dns.ip');
|
||||
const dnsPort = selector(state, 'dns.port');
|
||||
|
||||
return {
|
||||
webIp,
|
||||
webPort,
|
||||
dnsIp,
|
||||
dnsPort,
|
||||
};
|
||||
})(Settings);
|
||||
|
||||
export default flow([
|
||||
withNamespaces(),
|
||||
reduxForm({
|
||||
form: 'install',
|
||||
destroyOnUnmount: false,
|
||||
forceUnregisterOnUnmount: true,
|
||||
}),
|
||||
])(Settings);
|
|
@ -0,0 +1,117 @@
|
|||
.setup {
|
||||
min-height: calc(100vh - 80px);
|
||||
line-height: 1.48;
|
||||
}
|
||||
|
||||
@media screen and (min-width: 768px) {
|
||||
.setup {
|
||||
padding: 50px 0;
|
||||
}
|
||||
}
|
||||
|
||||
.setup__container {
|
||||
max-width: 650px;
|
||||
margin: 0 auto;
|
||||
padding: 30px 20px;
|
||||
line-height: 1.6;
|
||||
background-color: #fff;
|
||||
box-shadow: 0 1px 4px rgba(74, 74, 74, .36);
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
@media screen and (min-width: 768px) {
|
||||
.setup__container {
|
||||
width: 650px;
|
||||
padding: 40px 30px;
|
||||
}
|
||||
}
|
||||
|
||||
.setup__logo {
|
||||
display: block;
|
||||
margin: 0 auto 40px;
|
||||
max-width: 140px;
|
||||
}
|
||||
|
||||
.setup__nav {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.setup__step {
|
||||
margin-bottom: 25px;
|
||||
}
|
||||
|
||||
.setup__title {
|
||||
margin-bottom: 30px;
|
||||
font-size: 28px;
|
||||
text-align: center;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.setup__subtitle {
|
||||
margin-bottom: 10px;
|
||||
font-size: 17px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.setup__desc {
|
||||
margin-bottom: 20px;
|
||||
font-size: 15px;
|
||||
}
|
||||
|
||||
.setup__group {
|
||||
margin-bottom: 35px;
|
||||
}
|
||||
|
||||
.setup__group:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.setup__progress {
|
||||
font-size: 13px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.setup__progress-wrap {
|
||||
height: 4px;
|
||||
margin: 20px -20px -30px -20px;
|
||||
overflow: hidden;
|
||||
background-color: #eaeaea;
|
||||
border-radius: 0 0 3px 3px;
|
||||
}
|
||||
|
||||
@media screen and (min-width: 768px) {
|
||||
.setup__progress-wrap {
|
||||
margin: 20px -30px -40px -30px;
|
||||
}
|
||||
}
|
||||
|
||||
.setup__progress-inner {
|
||||
width: 0;
|
||||
height: 100%;
|
||||
font-size: 1.2rem;
|
||||
line-height: 20px;
|
||||
color: #fff;
|
||||
text-align: center;
|
||||
box-shadow: inset 0 -1px 0 rgba(0, 0, 0, .15);
|
||||
transition: width 0.6s ease;
|
||||
background: linear-gradient(45deg, rgba(99, 125, 120, 1) 0%, rgba(88, 177, 101, 1) 100%);
|
||||
}
|
||||
|
||||
.btn-standard {
|
||||
padding-left: 20px;
|
||||
padding-right: 20px;
|
||||
}
|
||||
|
||||
.form__message {
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
.form__message--error {
|
||||
color: #cd201f;
|
||||
}
|
||||
|
||||
.setup__button {
|
||||
min-width: 120px;
|
||||
padding-left: 30px;
|
||||
padding-right: 30px;
|
||||
}
|
|
@ -0,0 +1,57 @@
|
|||
import React from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import PropTypes from 'prop-types';
|
||||
import { reduxForm, formValueSelector } from 'redux-form';
|
||||
import { Trans, withNamespaces } from 'react-i18next';
|
||||
import flow from 'lodash/flow';
|
||||
|
||||
import Controls from './Controls';
|
||||
import { getWebAddress } from '../../helpers/helpers';
|
||||
|
||||
let Submit = props => (
|
||||
<div className="setup__step">
|
||||
<div className="setup__group">
|
||||
<h1 className="setup__title">
|
||||
<Trans>install_submit_title</Trans>
|
||||
</h1>
|
||||
<p className="setup__desc">
|
||||
<Trans>install_submit_desc</Trans>
|
||||
</p>
|
||||
</div>
|
||||
<Controls
|
||||
openDashboard={props.openDashboard}
|
||||
address={getWebAddress(props.webIp, props.webPort)}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
||||
Submit.propTypes = {
|
||||
webIp: PropTypes.string.isRequired,
|
||||
webPort: PropTypes.number.isRequired,
|
||||
handleSubmit: PropTypes.func.isRequired,
|
||||
pristine: PropTypes.bool.isRequired,
|
||||
submitting: PropTypes.bool.isRequired,
|
||||
openDashboard: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
const selector = formValueSelector('install');
|
||||
|
||||
Submit = connect((state) => {
|
||||
const webIp = selector(state, 'web.ip');
|
||||
const webPort = selector(state, 'web.port');
|
||||
|
||||
return {
|
||||
webIp,
|
||||
webPort,
|
||||
};
|
||||
})(Submit);
|
||||
|
||||
|
||||
export default flow([
|
||||
withNamespaces(),
|
||||
reduxForm({
|
||||
form: 'install',
|
||||
destroyOnUnmount: false,
|
||||
forceUnregisterOnUnmount: true,
|
||||
}),
|
||||
])(Submit);
|
|
@ -0,0 +1,125 @@
|
|||
import React, { Component, Fragment } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import * as actionCreators from '../../actions/install';
|
||||
import { INSTALL_FIRST_STEP, INSTALL_TOTAL_STEPS } from '../../helpers/constants';
|
||||
|
||||
import Loading from '../../components/ui/Loading';
|
||||
import Greeting from './Greeting';
|
||||
import Settings from './Settings';
|
||||
import Auth from './Auth';
|
||||
import Devices from './Devices';
|
||||
import Submit from './Submit';
|
||||
import Progress from './Progress';
|
||||
|
||||
import Toasts from '../../components/Toasts';
|
||||
import Footer from '../../components/ui/Footer';
|
||||
import logo from '../../components/ui/svg/logo.svg';
|
||||
|
||||
import './Setup.css';
|
||||
import '../../components/ui/Tabler.css';
|
||||
|
||||
class Setup extends Component {
|
||||
componentDidMount() {
|
||||
this.props.getDefaultAddresses();
|
||||
}
|
||||
|
||||
handleFormSubmit = (values) => {
|
||||
this.props.setAllSettings(values);
|
||||
};
|
||||
|
||||
openDashboard = (address) => {
|
||||
window.location.replace(address);
|
||||
}
|
||||
|
||||
nextStep = () => {
|
||||
if (this.props.install.step < INSTALL_TOTAL_STEPS) {
|
||||
this.props.nextStep();
|
||||
}
|
||||
}
|
||||
|
||||
prevStep = () => {
|
||||
if (this.props.install.step > INSTALL_FIRST_STEP) {
|
||||
this.props.prevStep();
|
||||
}
|
||||
}
|
||||
|
||||
renderPage(step, config, interfaces) {
|
||||
switch (step) {
|
||||
case 1:
|
||||
return <Greeting />;
|
||||
case 2:
|
||||
return (
|
||||
<Settings
|
||||
initialValues={config}
|
||||
interfaces={interfaces}
|
||||
webWarning={config.web.warning}
|
||||
dnsWarning={config.dns.warning}
|
||||
onSubmit={this.nextStep}
|
||||
/>
|
||||
);
|
||||
case 3:
|
||||
return (
|
||||
<Auth onSubmit={this.handleFormSubmit} />
|
||||
);
|
||||
case 4:
|
||||
return <Devices interfaces={interfaces} />;
|
||||
case 5:
|
||||
return <Submit openDashboard={this.openDashboard} />;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
const {
|
||||
processingDefault,
|
||||
step,
|
||||
web,
|
||||
dns,
|
||||
interfaces,
|
||||
} = this.props.install;
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
{processingDefault && <Loading />}
|
||||
{!processingDefault &&
|
||||
<Fragment>
|
||||
<div className="setup">
|
||||
<div className="setup__container">
|
||||
<img src={logo} className="setup__logo" alt="logo" />
|
||||
{this.renderPage(step, { web, dns }, interfaces)}
|
||||
<Progress step={step} />
|
||||
</div>
|
||||
</div>
|
||||
<Footer />
|
||||
<Toasts />
|
||||
</Fragment>
|
||||
}
|
||||
</Fragment>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Setup.propTypes = {
|
||||
getDefaultAddresses: PropTypes.func.isRequired,
|
||||
setAllSettings: PropTypes.func.isRequired,
|
||||
nextStep: PropTypes.func.isRequired,
|
||||
prevStep: PropTypes.func.isRequired,
|
||||
install: PropTypes.object.isRequired,
|
||||
step: PropTypes.number,
|
||||
web: PropTypes.object,
|
||||
dns: PropTypes.object,
|
||||
};
|
||||
|
||||
const mapStateToProps = (state) => {
|
||||
const { install, toasts } = state;
|
||||
const props = { install, toasts };
|
||||
return props;
|
||||
};
|
||||
|
||||
export default connect(
|
||||
mapStateToProps,
|
||||
actionCreators,
|
||||
)(Setup);
|
|
@ -0,0 +1,19 @@
|
|||
import React, { Fragment } from 'react';
|
||||
|
||||
const renderField = ({
|
||||
input, className, placeholder, type, disabled, autoComplete, meta: { touched, error },
|
||||
}) => (
|
||||
<Fragment>
|
||||
<input
|
||||
{...input}
|
||||
placeholder={placeholder}
|
||||
type={type}
|
||||
className={className}
|
||||
disabled={disabled}
|
||||
autoComplete={autoComplete}
|
||||
/>
|
||||
{!disabled && touched && (error && <span className="form__message form__message--error">{error}</span>)}
|
||||
</Fragment>
|
||||
);
|
||||
|
||||
export default renderField;
|
|
@ -0,0 +1,18 @@
|
|||
import React from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import { Provider } from 'react-redux';
|
||||
|
||||
import '../components/App/index.css';
|
||||
import '../components/ui/ReactTable.css';
|
||||
import configureStore from '../configureStore';
|
||||
import reducers from '../reducers/install';
|
||||
import '../i18n';
|
||||
import Setup from './Setup';
|
||||
|
||||
const store = configureStore(reducers, {}); // set initial state
|
||||
ReactDOM.render(
|
||||
<Provider store={store}>
|
||||
<Setup />
|
||||
</Provider>,
|
||||
document.getElementById('root'),
|
||||
);
|
|
@ -1,11 +1,11 @@
|
|||
import { combineReducers } from 'redux';
|
||||
import { handleActions } from 'redux-actions';
|
||||
import { loadingBarReducer } from 'react-redux-loading-bar';
|
||||
import nanoid from 'nanoid';
|
||||
import { reducer as formReducer } from 'redux-form';
|
||||
import versionCompare from '../helpers/versionCompare';
|
||||
|
||||
import * as actions from '../actions';
|
||||
import toasts from './toasts';
|
||||
|
||||
const settings = handleActions({
|
||||
[actions.initSettingsRequest]: state => ({ ...state, processing: true }),
|
||||
|
@ -241,34 +241,6 @@ const filtering = handleActions({
|
|||
userRules: '',
|
||||
});
|
||||
|
||||
const toasts = handleActions({
|
||||
[actions.addErrorToast]: (state, { payload }) => {
|
||||
const errorToast = {
|
||||
id: nanoid(),
|
||||
message: payload.error.toString(),
|
||||
type: 'error',
|
||||
};
|
||||
|
||||
const newState = { ...state, notices: [...state.notices, errorToast] };
|
||||
return newState;
|
||||
},
|
||||
[actions.addSuccessToast]: (state, { payload }) => {
|
||||
const successToast = {
|
||||
id: nanoid(),
|
||||
message: payload,
|
||||
type: 'success',
|
||||
};
|
||||
|
||||
const newState = { ...state, notices: [...state.notices, successToast] };
|
||||
return newState;
|
||||
},
|
||||
[actions.removeToast]: (state, { payload }) => {
|
||||
const filtered = state.notices.filter(notice => notice.id !== payload);
|
||||
const newState = { ...state, notices: filtered };
|
||||
return newState;
|
||||
},
|
||||
}, { notices: [] });
|
||||
|
||||
const dhcp = handleActions({
|
||||
[actions.getDhcpStatusRequest]: state => ({ ...state, processing: true }),
|
||||
[actions.getDhcpStatusFailure]: state => ({ ...state, processing: false }),
|
||||
|
@ -308,11 +280,21 @@ const dhcp = handleActions({
|
|||
const newState = { ...state, config: newConfig, processingDhcp: false };
|
||||
return newState;
|
||||
},
|
||||
|
||||
[actions.setDhcpConfigRequest]: state => ({ ...state, processingConfig: true }),
|
||||
[actions.setDhcpConfigFailure]: state => ({ ...state, processingConfig: false }),
|
||||
[actions.setDhcpConfigSuccess]: (state, { payload }) => {
|
||||
const { config } = state;
|
||||
const newConfig = { ...config, ...payload };
|
||||
const newState = { ...state, config: newConfig, processingConfig: false };
|
||||
return newState;
|
||||
},
|
||||
}, {
|
||||
processing: true,
|
||||
processingStatus: false,
|
||||
processingInterfaces: false,
|
||||
processingDhcp: false,
|
||||
processingConfig: false,
|
||||
config: {
|
||||
enabled: false,
|
||||
},
|
||||
|
|
|
@ -0,0 +1,47 @@
|
|||
import { combineReducers } from 'redux';
|
||||
import { handleActions } from 'redux-actions';
|
||||
import { reducer as formReducer } from 'redux-form';
|
||||
|
||||
import * as actions from '../actions/install';
|
||||
import toasts from './toasts';
|
||||
import { INSTALL_FIRST_STEP } from '../helpers/constants';
|
||||
|
||||
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 };
|
||||
return newState;
|
||||
},
|
||||
|
||||
[actions.nextStep]: state => ({ ...state, step: state.step + 1 }),
|
||||
[actions.prevStep]: state => ({ ...state, step: state.step - 1 }),
|
||||
|
||||
[actions.setAllSettingsRequest]: state => ({ ...state, processingSubmit: true }),
|
||||
[actions.setAllSettingsFailure]: state => ({ ...state, processingSubmit: false }),
|
||||
[actions.setAllSettingsSuccess]: state => ({ ...state, processingSubmit: false }),
|
||||
}, {
|
||||
step: INSTALL_FIRST_STEP,
|
||||
processingDefault: true,
|
||||
processingSubmit: false,
|
||||
web: {
|
||||
ip: '0.0.0.0',
|
||||
port: 80,
|
||||
warning: '',
|
||||
},
|
||||
dns: {
|
||||
ip: '0.0.0.0',
|
||||
port: 53,
|
||||
warning: '',
|
||||
},
|
||||
interfaces: {},
|
||||
});
|
||||
|
||||
export default combineReducers({
|
||||
install,
|
||||
toasts,
|
||||
form: formReducer,
|
||||
});
|
|
@ -0,0 +1,34 @@
|
|||
import { handleActions } from 'redux-actions';
|
||||
import nanoid from 'nanoid';
|
||||
|
||||
import { addErrorToast, addSuccessToast, removeToast } from '../actions';
|
||||
|
||||
const toasts = handleActions({
|
||||
[addErrorToast]: (state, { payload }) => {
|
||||
const errorToast = {
|
||||
id: nanoid(),
|
||||
message: payload.error.toString(),
|
||||
type: 'error',
|
||||
};
|
||||
|
||||
const newState = { ...state, notices: [...state.notices, errorToast] };
|
||||
return newState;
|
||||
},
|
||||
[addSuccessToast]: (state, { payload }) => {
|
||||
const successToast = {
|
||||
id: nanoid(),
|
||||
message: payload,
|
||||
type: 'success',
|
||||
};
|
||||
|
||||
const newState = { ...state, notices: [...state.notices, successToast] };
|
||||
return newState;
|
||||
},
|
||||
[removeToast]: (state, { payload }) => {
|
||||
const filtered = state.notices.filter(notice => notice.id !== payload);
|
||||
const newState = { ...state, notices: filtered };
|
||||
return newState;
|
||||
},
|
||||
}, { notices: [] });
|
||||
|
||||
export default toasts;
|
|
@ -8,7 +8,9 @@ const CleanWebpackPlugin = require('clean-webpack-plugin');
|
|||
|
||||
const RESOURCES_PATH = path.resolve(__dirname);
|
||||
const ENTRY_REACT = path.resolve(RESOURCES_PATH, 'src/index.js');
|
||||
const ENTRY_INSTALL = path.resolve(RESOURCES_PATH, 'src/install/index.js');
|
||||
const HTML_PATH = path.resolve(RESOURCES_PATH, 'public/index.html');
|
||||
const HTML_INSTALL_PATH = path.resolve(RESOURCES_PATH, 'public/install.html');
|
||||
|
||||
const PUBLIC_PATH = path.resolve(__dirname, '../build/static');
|
||||
|
||||
|
@ -16,7 +18,8 @@ const config = {
|
|||
target: 'web',
|
||||
context: RESOURCES_PATH,
|
||||
entry: {
|
||||
bundle: ENTRY_REACT,
|
||||
main: ENTRY_REACT,
|
||||
install: ENTRY_INSTALL,
|
||||
},
|
||||
output: {
|
||||
path: PUBLIC_PATH,
|
||||
|
@ -101,8 +104,16 @@ const config = {
|
|||
new HtmlWebpackPlugin({
|
||||
inject: true,
|
||||
cache: false,
|
||||
chunks: ['main'],
|
||||
template: HTML_PATH,
|
||||
}),
|
||||
new HtmlWebpackPlugin({
|
||||
inject: true,
|
||||
cache: false,
|
||||
chunks: ['install'],
|
||||
filename: 'install.html',
|
||||
template: HTML_INSTALL_PATH,
|
||||
}),
|
||||
new ExtractTextPlugin({
|
||||
filename: '[name].[contenthash].css',
|
||||
}),
|
||||
|
|
|
@ -2,7 +2,6 @@ const merge = require('webpack-merge');
|
|||
const common = require('./webpack.common.js');
|
||||
|
||||
module.exports = merge(common, {
|
||||
devtool: 'inline-source-map',
|
||||
module: {
|
||||
rules: [{
|
||||
test: /\.js$/,
|
||||
|
|
11
config.go
11
config.go
|
@ -29,6 +29,7 @@ type logSettings struct {
|
|||
type configuration struct {
|
||||
ourConfigFilename string // Config filename (can be overridden via the command line arguments)
|
||||
ourBinaryDir 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
|
||||
|
||||
BindHost string `yaml:"bind_host"`
|
||||
BindPort int `yaml:"bind_port"`
|
||||
|
@ -63,7 +64,7 @@ var defaultDNS = []string{"tls://1.1.1.1", "tls://1.0.0.1"}
|
|||
var config = configuration{
|
||||
ourConfigFilename: "AdGuardHome.yaml",
|
||||
BindPort: 3000,
|
||||
BindHost: "127.0.0.1",
|
||||
BindHost: "0.0.0.0",
|
||||
DNS: dnsConfig{
|
||||
BindHost: "0.0.0.0",
|
||||
Port: 53,
|
||||
|
@ -114,7 +115,7 @@ func getLogSettings() logSettings {
|
|||
// parseConfig loads configuration from the YAML file
|
||||
func parseConfig() error {
|
||||
configFile := config.getConfigFilename()
|
||||
log.Printf("Reading YAML file: %s", configFile)
|
||||
log.Tracef("Reading YAML file: %s", configFile)
|
||||
yamlFile, err := readConfigFile()
|
||||
if err != nil {
|
||||
log.Printf("Couldn't read config file: %s", err)
|
||||
|
@ -152,8 +153,12 @@ func readConfigFile() ([]byte, error) {
|
|||
func (c *configuration) write() error {
|
||||
c.Lock()
|
||||
defer c.Unlock()
|
||||
if config.firstRun {
|
||||
log.Tracef("Silently refusing to write config because first run and not configured yet")
|
||||
return nil
|
||||
}
|
||||
configFile := config.getConfigFilename()
|
||||
log.Printf("Writing YAML file: %s", configFile)
|
||||
log.Tracef("Writing YAML file: %s", configFile)
|
||||
yamlText, err := yaml.Marshal(&config)
|
||||
if err != nil {
|
||||
log.Printf("Couldn't generate YAML file: %s", err)
|
||||
|
|
223
control.go
223
control.go
|
@ -1,6 +1,7 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
|
@ -693,42 +694,188 @@ func handleSafeSearchStatus(w http.ResponseWriter, r *http.Request) {
|
|||
}
|
||||
}
|
||||
|
||||
func registerControlHandlers() {
|
||||
http.HandleFunc("/control/status", optionalAuth(ensureGET(handleStatus)))
|
||||
http.HandleFunc("/control/enable_protection", optionalAuth(ensurePOST(handleProtectionEnable)))
|
||||
http.HandleFunc("/control/disable_protection", optionalAuth(ensurePOST(handleProtectionDisable)))
|
||||
http.HandleFunc("/control/querylog", optionalAuth(ensureGET(dnsforward.HandleQueryLog)))
|
||||
http.HandleFunc("/control/querylog_enable", optionalAuth(ensurePOST(handleQueryLogEnable)))
|
||||
http.HandleFunc("/control/querylog_disable", optionalAuth(ensurePOST(handleQueryLogDisable)))
|
||||
http.HandleFunc("/control/set_upstream_dns", optionalAuth(ensurePOST(handleSetUpstreamDNS)))
|
||||
http.HandleFunc("/control/test_upstream_dns", optionalAuth(ensurePOST(handleTestUpstreamDNS)))
|
||||
http.HandleFunc("/control/i18n/change_language", optionalAuth(ensurePOST(handleI18nChangeLanguage)))
|
||||
http.HandleFunc("/control/i18n/current_language", optionalAuth(ensureGET(handleI18nCurrentLanguage)))
|
||||
http.HandleFunc("/control/stats_top", optionalAuth(ensureGET(dnsforward.HandleStatsTop)))
|
||||
http.HandleFunc("/control/stats", optionalAuth(ensureGET(dnsforward.HandleStats)))
|
||||
http.HandleFunc("/control/stats_history", optionalAuth(ensureGET(dnsforward.HandleStatsHistory)))
|
||||
http.HandleFunc("/control/stats_reset", optionalAuth(ensurePOST(dnsforward.HandleStatsReset)))
|
||||
http.HandleFunc("/control/version.json", optionalAuth(handleGetVersionJSON))
|
||||
http.HandleFunc("/control/filtering/enable", optionalAuth(ensurePOST(handleFilteringEnable)))
|
||||
http.HandleFunc("/control/filtering/disable", optionalAuth(ensurePOST(handleFilteringDisable)))
|
||||
http.HandleFunc("/control/filtering/add_url", optionalAuth(ensurePUT(handleFilteringAddURL)))
|
||||
http.HandleFunc("/control/filtering/remove_url", optionalAuth(ensureDELETE(handleFilteringRemoveURL)))
|
||||
http.HandleFunc("/control/filtering/enable_url", optionalAuth(ensurePOST(handleFilteringEnableURL)))
|
||||
http.HandleFunc("/control/filtering/disable_url", optionalAuth(ensurePOST(handleFilteringDisableURL)))
|
||||
http.HandleFunc("/control/filtering/refresh", optionalAuth(ensurePOST(handleFilteringRefresh)))
|
||||
http.HandleFunc("/control/filtering/status", optionalAuth(ensureGET(handleFilteringStatus)))
|
||||
http.HandleFunc("/control/filtering/set_rules", optionalAuth(ensurePUT(handleFilteringSetRules)))
|
||||
http.HandleFunc("/control/safebrowsing/enable", optionalAuth(ensurePOST(handleSafeBrowsingEnable)))
|
||||
http.HandleFunc("/control/safebrowsing/disable", optionalAuth(ensurePOST(handleSafeBrowsingDisable)))
|
||||
http.HandleFunc("/control/safebrowsing/status", optionalAuth(ensureGET(handleSafeBrowsingStatus)))
|
||||
http.HandleFunc("/control/parental/enable", optionalAuth(ensurePOST(handleParentalEnable)))
|
||||
http.HandleFunc("/control/parental/disable", optionalAuth(ensurePOST(handleParentalDisable)))
|
||||
http.HandleFunc("/control/parental/status", optionalAuth(ensureGET(handleParentalStatus)))
|
||||
http.HandleFunc("/control/safesearch/enable", optionalAuth(ensurePOST(handleSafeSearchEnable)))
|
||||
http.HandleFunc("/control/safesearch/disable", optionalAuth(ensurePOST(handleSafeSearchDisable)))
|
||||
http.HandleFunc("/control/safesearch/status", optionalAuth(ensureGET(handleSafeSearchStatus)))
|
||||
http.HandleFunc("/control/dhcp/status", optionalAuth(ensureGET(handleDHCPStatus)))
|
||||
http.HandleFunc("/control/dhcp/interfaces", optionalAuth(ensureGET(handleDHCPInterfaces)))
|
||||
http.HandleFunc("/control/dhcp/set_config", optionalAuth(ensurePOST(handleDHCPSetConfig)))
|
||||
http.HandleFunc("/control/dhcp/find_active_dhcp", optionalAuth(ensurePOST(handleDHCPFindActiveServer)))
|
||||
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) {
|
||||
data := firstRunData{}
|
||||
ifaces, err := getValidNetInterfaces()
|
||||
if err != nil {
|
||||
httpError(w, http.StatusInternalServerError, "Couldn't get interfaces: %s", err)
|
||||
return
|
||||
}
|
||||
if len(ifaces) == 0 {
|
||||
httpError(w, http.StatusServiceUnavailable, "Couldn't find any legible interface, plase try again later")
|
||||
return
|
||||
}
|
||||
|
||||
// fill out the fields
|
||||
|
||||
// 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."
|
||||
}
|
||||
|
||||
data.Interfaces = make(map[string]interface{})
|
||||
for _, iface := range ifaces {
|
||||
addrs, err := iface.Addrs()
|
||||
if err != nil {
|
||||
httpError(w, http.StatusInternalServerError, "Failed to get addresses for interface %s: %s", iface.Name, err)
|
||||
return
|
||||
}
|
||||
|
||||
jsonIface := netInterface{
|
||||
Name: iface.Name,
|
||||
MTU: iface.MTU,
|
||||
HardwareAddr: iface.HardwareAddr.String(),
|
||||
}
|
||||
|
||||
if iface.Flags != 0 {
|
||||
jsonIface.Flags = iface.Flags.String()
|
||||
}
|
||||
|
||||
// we don't want link-local addresses in json, so skip them
|
||||
for _, addr := range addrs {
|
||||
ipnet, ok := addr.(*net.IPNet)
|
||||
if !ok {
|
||||
// not an IPNet, should not happen
|
||||
httpError(w, http.StatusInternalServerError, "SHOULD NOT HAPPEN: got iface.Addrs() element %s that is not net.IPNet, it is %T", addr, addr)
|
||||
return
|
||||
}
|
||||
// ignore link-local
|
||||
if ipnet.IP.IsLinkLocalUnicast() {
|
||||
continue
|
||||
}
|
||||
jsonIface.Addresses = append(jsonIface.Addresses, ipnet.IP.String())
|
||||
}
|
||||
if len(jsonIface.Addresses) != 0 {
|
||||
data.Interfaces[iface.Name] = jsonIface
|
||||
}
|
||||
}
|
||||
|
||||
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) {
|
||||
newSettings := firstRunData{}
|
||||
err := json.NewDecoder(r.Body).Decode(&newSettings)
|
||||
if err != nil {
|
||||
httpError(w, http.StatusBadRequest, "Failed to parse new DHCP 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())
|
||||
}()
|
||||
}
|
||||
}
|
||||
|
||||
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))))
|
||||
http.HandleFunc("/control/disable_protection", postInstall(optionalAuth(ensurePOST(handleProtectionDisable))))
|
||||
http.HandleFunc("/control/querylog", postInstall(optionalAuth(ensureGET(dnsforward.HandleQueryLog))))
|
||||
http.HandleFunc("/control/querylog_enable", postInstall(optionalAuth(ensurePOST(handleQueryLogEnable))))
|
||||
http.HandleFunc("/control/querylog_disable", postInstall(optionalAuth(ensurePOST(handleQueryLogDisable))))
|
||||
http.HandleFunc("/control/set_upstream_dns", postInstall(optionalAuth(ensurePOST(handleSetUpstreamDNS))))
|
||||
http.HandleFunc("/control/test_upstream_dns", postInstall(optionalAuth(ensurePOST(handleTestUpstreamDNS))))
|
||||
http.HandleFunc("/control/i18n/change_language", postInstall(optionalAuth(ensurePOST(handleI18nChangeLanguage))))
|
||||
http.HandleFunc("/control/i18n/current_language", postInstall(optionalAuth(ensureGET(handleI18nCurrentLanguage))))
|
||||
http.HandleFunc("/control/stats_top", postInstall(optionalAuth(ensureGET(dnsforward.HandleStatsTop))))
|
||||
http.HandleFunc("/control/stats", postInstall(optionalAuth(ensureGET(dnsforward.HandleStats))))
|
||||
http.HandleFunc("/control/stats_history", postInstall(optionalAuth(ensureGET(dnsforward.HandleStatsHistory))))
|
||||
http.HandleFunc("/control/stats_reset", postInstall(optionalAuth(ensurePOST(dnsforward.HandleStatsReset))))
|
||||
http.HandleFunc("/control/version.json", postInstall(optionalAuth(handleGetVersionJSON)))
|
||||
http.HandleFunc("/control/filtering/enable", postInstall(optionalAuth(ensurePOST(handleFilteringEnable))))
|
||||
http.HandleFunc("/control/filtering/disable", postInstall(optionalAuth(ensurePOST(handleFilteringDisable))))
|
||||
http.HandleFunc("/control/filtering/add_url", postInstall(optionalAuth(ensurePUT(handleFilteringAddURL))))
|
||||
http.HandleFunc("/control/filtering/remove_url", postInstall(optionalAuth(ensureDELETE(handleFilteringRemoveURL))))
|
||||
http.HandleFunc("/control/filtering/enable_url", postInstall(optionalAuth(ensurePOST(handleFilteringEnableURL))))
|
||||
http.HandleFunc("/control/filtering/disable_url", postInstall(optionalAuth(ensurePOST(handleFilteringDisableURL))))
|
||||
http.HandleFunc("/control/filtering/refresh", postInstall(optionalAuth(ensurePOST(handleFilteringRefresh))))
|
||||
http.HandleFunc("/control/filtering/status", postInstall(optionalAuth(ensureGET(handleFilteringStatus))))
|
||||
http.HandleFunc("/control/filtering/set_rules", postInstall(optionalAuth(ensurePUT(handleFilteringSetRules))))
|
||||
http.HandleFunc("/control/safebrowsing/enable", postInstall(optionalAuth(ensurePOST(handleSafeBrowsingEnable))))
|
||||
http.HandleFunc("/control/safebrowsing/disable", postInstall(optionalAuth(ensurePOST(handleSafeBrowsingDisable))))
|
||||
http.HandleFunc("/control/safebrowsing/status", postInstall(optionalAuth(ensureGET(handleSafeBrowsingStatus))))
|
||||
http.HandleFunc("/control/parental/enable", postInstall(optionalAuth(ensurePOST(handleParentalEnable))))
|
||||
http.HandleFunc("/control/parental/disable", postInstall(optionalAuth(ensurePOST(handleParentalDisable))))
|
||||
http.HandleFunc("/control/parental/status", postInstall(optionalAuth(ensureGET(handleParentalStatus))))
|
||||
http.HandleFunc("/control/safesearch/enable", postInstall(optionalAuth(ensurePOST(handleSafeSearchEnable))))
|
||||
http.HandleFunc("/control/safesearch/disable", postInstall(optionalAuth(ensurePOST(handleSafeSearchDisable))))
|
||||
http.HandleFunc("/control/safesearch/status", postInstall(optionalAuth(ensureGET(handleSafeSearchStatus))))
|
||||
http.HandleFunc("/control/dhcp/status", postInstall(optionalAuth(ensureGET(handleDHCPStatus))))
|
||||
http.HandleFunc("/control/dhcp/interfaces", postInstall(optionalAuth(ensureGET(handleDHCPInterfaces))))
|
||||
http.HandleFunc("/control/dhcp/set_config", postInstall(optionalAuth(ensurePOST(handleDHCPSetConfig))))
|
||||
http.HandleFunc("/control/dhcp/find_active_dhcp", postInstall(optionalAuth(ensurePOST(handleDHCPFindActiveServer))))
|
||||
}
|
||||
|
|
62
dhcp.go
62
dhcp.go
|
@ -70,50 +70,54 @@ func handleDHCPSetConfig(w http.ResponseWriter, r *http.Request) {
|
|||
func handleDHCPInterfaces(w http.ResponseWriter, r *http.Request) {
|
||||
response := map[string]interface{}{}
|
||||
|
||||
ifaces, err := net.Interfaces()
|
||||
ifaces, err := getValidNetInterfaces()
|
||||
if err != nil {
|
||||
httpError(w, http.StatusInternalServerError, "Couldn't get list of interfaces: %s", err)
|
||||
httpError(w, http.StatusInternalServerError, "Couldn't get interfaces: %s", err)
|
||||
return
|
||||
}
|
||||
|
||||
type responseInterface struct {
|
||||
Name string `json:"name"`
|
||||
MTU int `json:"mtu"`
|
||||
HardwareAddr string `json:"hardware_address"`
|
||||
Addresses []string `json:"ip_addresses"`
|
||||
}
|
||||
|
||||
for i := range ifaces {
|
||||
if ifaces[i].Flags&net.FlagLoopback != 0 {
|
||||
for _, iface := range ifaces {
|
||||
if iface.Flags&net.FlagLoopback != 0 {
|
||||
// it's a loopback, skip it
|
||||
continue
|
||||
}
|
||||
if ifaces[i].Flags&net.FlagBroadcast == 0 {
|
||||
if iface.Flags&net.FlagBroadcast == 0 {
|
||||
// this interface doesn't support broadcast, skip it
|
||||
continue
|
||||
}
|
||||
if ifaces[i].Flags&net.FlagPointToPoint != 0 {
|
||||
// this interface is ppp, don't do dhcp over it
|
||||
continue
|
||||
}
|
||||
iface := responseInterface{
|
||||
Name: ifaces[i].Name,
|
||||
MTU: ifaces[i].MTU,
|
||||
HardwareAddr: ifaces[i].HardwareAddr.String(),
|
||||
}
|
||||
addrs, errAddrs := ifaces[i].Addrs()
|
||||
if errAddrs != nil {
|
||||
httpError(w, http.StatusInternalServerError, "Failed to get addresses for interface %v: %s", ifaces[i].Name, errAddrs)
|
||||
addrs, err := iface.Addrs()
|
||||
if err != nil {
|
||||
httpError(w, http.StatusInternalServerError, "Failed to get addresses for interface %s: %s", iface.Name, err)
|
||||
return
|
||||
}
|
||||
for _, addr := range addrs {
|
||||
iface.Addresses = append(iface.Addresses, addr.String())
|
||||
|
||||
jsonIface := netInterface{
|
||||
Name: iface.Name,
|
||||
MTU: iface.MTU,
|
||||
HardwareAddr: iface.HardwareAddr.String(),
|
||||
}
|
||||
if len(iface.Addresses) == 0 {
|
||||
// this interface has no addresses, skip it
|
||||
|
||||
if iface.Flags != 0 {
|
||||
jsonIface.Flags = iface.Flags.String()
|
||||
}
|
||||
// we don't want link-local addresses in json, so skip them
|
||||
for _, addr := range addrs {
|
||||
ipnet, ok := addr.(*net.IPNet)
|
||||
if !ok {
|
||||
// not an IPNet, should not happen
|
||||
httpError(w, http.StatusInternalServerError, "SHOULD NOT HAPPEN: got iface.Addrs() element %s that is not net.IPNet, it is %T", addr, addr)
|
||||
return
|
||||
}
|
||||
// ignore link-local
|
||||
if ipnet.IP.IsLinkLocalUnicast() {
|
||||
continue
|
||||
}
|
||||
response[ifaces[i].Name] = iface
|
||||
jsonIface.Addresses = append(jsonIface.Addresses, ipnet.IP.String())
|
||||
}
|
||||
if len(jsonIface.Addresses) != 0 {
|
||||
response[iface.Name] = jsonIface
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
err = json.NewEncoder(w).Encode(response)
|
||||
|
|
|
@ -31,11 +31,11 @@ func getIfaceIPv4(iface *net.Interface) *net.IPNet {
|
|||
}
|
||||
|
||||
if ipnet.IP.To4() == nil {
|
||||
log.Printf("Got IP that is not IPv4: %v", ipnet.IP)
|
||||
log.Tracef("Got IP that is not IPv4: %v", ipnet.IP)
|
||||
continue
|
||||
}
|
||||
|
||||
log.Printf("Got IP that is IPv4: %v", ipnet.IP)
|
||||
log.Tracef("Got IP that is IPv4: %v", ipnet.IP)
|
||||
return &net.IPNet{
|
||||
IP: ipnet.IP.To4(),
|
||||
Mask: ipnet.Mask,
|
||||
|
|
|
@ -892,15 +892,16 @@ func New(c *Config) *Dnsfilter {
|
|||
d.whiteList = newRulesTable()
|
||||
d.blackList = newRulesTable()
|
||||
|
||||
// Customize the Transport to have larger connection pool
|
||||
defaultRoundTripper := http.DefaultTransport
|
||||
defaultTransportPointer, ok := defaultRoundTripper.(*http.Transport)
|
||||
if !ok {
|
||||
panic(fmt.Sprintf("defaultRoundTripper not an *http.Transport"))
|
||||
// Customize the Transport to have larger connection pool,
|
||||
// We are not (re)using http.DefaultTransport because of race conditions found by tests
|
||||
d.transport = &http.Transport{
|
||||
Proxy: http.ProxyFromEnvironment,
|
||||
MaxIdleConns: defaultHTTPMaxIdleConnections, // default 100
|
||||
MaxIdleConnsPerHost: defaultHTTPMaxIdleConnections, // default 2
|
||||
IdleConnTimeout: 90 * time.Second,
|
||||
TLSHandshakeTimeout: 10 * time.Second,
|
||||
ExpectContinueTimeout: 1 * time.Second,
|
||||
}
|
||||
d.transport = defaultTransportPointer // dereference it to get a copy of the struct that the pointer points to
|
||||
d.transport.MaxIdleConns = defaultHTTPMaxIdleConnections // default 100
|
||||
d.transport.MaxIdleConnsPerHost = defaultHTTPMaxIdleConnections // default 2
|
||||
d.client = http.Client{
|
||||
Transport: d.transport,
|
||||
Timeout: defaultHTTPTimeout,
|
||||
|
|
|
@ -110,7 +110,7 @@ func (s *Server) startInternal(config *ServerConfig) error {
|
|||
return err
|
||||
}
|
||||
|
||||
log.Printf("Loading stats from querylog")
|
||||
log.Tracef("Loading stats from querylog")
|
||||
err = fillStatsFromQueryLog()
|
||||
if err != nil {
|
||||
return errorx.Decorate(err, "failed to load stats from querylog")
|
||||
|
@ -152,7 +152,7 @@ func (s *Server) startInternal(config *ServerConfig) error {
|
|||
|
||||
// Initializes the DNS filter
|
||||
func (s *Server) initDNSFilter() error {
|
||||
log.Printf("Creating dnsfilter")
|
||||
log.Tracef("Creating dnsfilter")
|
||||
s.dnsFilter = dnsfilter.New(&s.Config)
|
||||
// add rules only if they are enabled
|
||||
if s.FilteringEnabled {
|
||||
|
|
|
@ -228,7 +228,7 @@ func (filter *filter) load() error {
|
|||
}
|
||||
|
||||
filterFilePath := filter.Path()
|
||||
log.Printf("Loading filter %d contents to: %s", filter.ID, filterFilePath)
|
||||
log.Tracef("Loading filter %d contents to: %s", filter.ID, filterFilePath)
|
||||
|
||||
if _, err := os.Stat(filterFilePath); os.IsNotExist(err) {
|
||||
// do nothing, file doesn't exist
|
||||
|
@ -240,7 +240,7 @@ func (filter *filter) load() error {
|
|||
return err
|
||||
}
|
||||
|
||||
log.Printf("File %s, id %d, length %d", filterFilePath, filter.ID, len(filterFileContents))
|
||||
log.Tracef("File %s, id %d, length %d", filterFilePath, filter.ID, len(filterFileContents))
|
||||
rulesCount, _, rules := parseFilterContents(filterFileContents)
|
||||
|
||||
filter.RulesCount = rulesCount
|
||||
|
|
1
go.mod
1
go.mod
|
@ -16,7 +16,6 @@ require (
|
|||
github.com/shirou/gopsutil v2.18.10+incompatible
|
||||
github.com/shirou/w32 v0.0.0-20160930032740-bb4de0191aa4 // indirect
|
||||
go.uber.org/goleak v0.10.0
|
||||
golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9
|
||||
golang.org/x/net v0.0.0-20181220203305-927f97764cc3
|
||||
golang.org/x/sys v0.0.0-20181228144115-9a3f9b0469bb
|
||||
gopkg.in/asaskevich/govalidator.v4 v4.0.0-20160518190739-766470278477
|
||||
|
|
134
helpers.go
134
helpers.go
|
@ -3,13 +3,16 @@ package main
|
|||
import (
|
||||
"bufio"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"net"
|
||||
"net/http"
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
|
@ -84,24 +87,78 @@ type authHandler struct {
|
|||
}
|
||||
|
||||
func (a *authHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
if config.AuthName == "" || config.AuthPass == "" {
|
||||
a.handler.ServeHTTP(w, r)
|
||||
return
|
||||
}
|
||||
user, pass, ok := r.BasicAuth()
|
||||
if !ok || user != config.AuthName || pass != config.AuthPass {
|
||||
w.Header().Set("WWW-Authenticate", `Basic realm="dnsfilter"`)
|
||||
w.WriteHeader(http.StatusUnauthorized)
|
||||
w.Write([]byte("Unauthorised.\n"))
|
||||
return
|
||||
}
|
||||
a.handler.ServeHTTP(w, r)
|
||||
optionalAuth(a.handler.ServeHTTP)(w, r)
|
||||
}
|
||||
|
||||
func optionalAuthHandler(handler http.Handler) http.Handler {
|
||||
return &authHandler{handler}
|
||||
}
|
||||
|
||||
// -------------------
|
||||
// first run / install
|
||||
// -------------------
|
||||
func detectFirstRun() bool {
|
||||
configfile := config.ourConfigFilename
|
||||
if !filepath.IsAbs(configfile) {
|
||||
configfile = filepath.Join(config.ourBinaryDir, config.ourConfigFilename)
|
||||
}
|
||||
_, err := os.Stat(configfile)
|
||||
if !os.IsNotExist(err) {
|
||||
// do nothing, file exists
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// preInstall lets the handler run only if firstRun is true, no redirects
|
||||
func preInstall(handler func(http.ResponseWriter, *http.Request)) func(http.ResponseWriter, *http.Request) {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
if !config.firstRun {
|
||||
// if it's not first run, don't let users access it (for example /install.html when configuration is done)
|
||||
http.Error(w, http.StatusText(http.StatusForbidden), http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
handler(w, r)
|
||||
}
|
||||
}
|
||||
|
||||
// preInstallStruct wraps preInstall into a struct that can be returned as an interface where necessary
|
||||
type preInstallHandlerStruct struct {
|
||||
handler http.Handler
|
||||
}
|
||||
|
||||
func (p *preInstallHandlerStruct) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
preInstall(p.handler.ServeHTTP)(w, r)
|
||||
}
|
||||
|
||||
// preInstallHandler returns http.Handler interface for preInstall wrapper
|
||||
func preInstallHandler(handler http.Handler) http.Handler {
|
||||
return &preInstallHandlerStruct{handler}
|
||||
}
|
||||
|
||||
// postInstall lets the handler run only if firstRun is false, and redirects to /install.html otherwise
|
||||
func postInstall(handler func(http.ResponseWriter, *http.Request)) func(http.ResponseWriter, *http.Request) {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
if config.firstRun && !strings.HasPrefix(r.URL.Path, "/install.") {
|
||||
http.Redirect(w, r, "/install.html", http.StatusSeeOther) // should not be cacheable
|
||||
return
|
||||
}
|
||||
handler(w, r)
|
||||
}
|
||||
}
|
||||
|
||||
type postInstallHandlerStruct struct {
|
||||
handler http.Handler
|
||||
}
|
||||
|
||||
func (p *postInstallHandlerStruct) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
postInstall(p.handler.ServeHTTP)(w, r)
|
||||
}
|
||||
|
||||
func postInstallHandler(handler http.Handler) http.Handler {
|
||||
return &postInstallHandlerStruct{handler}
|
||||
}
|
||||
|
||||
// -------------------------------------------------
|
||||
// helper functions for parsing parameters from body
|
||||
// -------------------------------------------------
|
||||
|
@ -125,6 +182,59 @@ func parseParametersFromBody(r io.Reader) (map[string]string, error) {
|
|||
return parameters, nil
|
||||
}
|
||||
|
||||
// ------------------
|
||||
// network interfaces
|
||||
// ------------------
|
||||
type netInterface struct {
|
||||
Name string `json:"name"`
|
||||
MTU int `json:"mtu"`
|
||||
HardwareAddr string `json:"hardware_address"`
|
||||
Addresses []string `json:"ip_addresses"`
|
||||
Flags string `json:"flags"`
|
||||
}
|
||||
|
||||
// getValidNetInterfaces returns interfaces that are eligible for DNS and/or DHCP
|
||||
// invalid interface is a ppp interface or the one that doesn't allow broadcasts
|
||||
func getValidNetInterfaces() ([]net.Interface, error) {
|
||||
ifaces, err := net.Interfaces()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("Couldn't get list of interfaces: %s", err)
|
||||
}
|
||||
|
||||
netIfaces := []net.Interface{}
|
||||
|
||||
for i := range ifaces {
|
||||
if ifaces[i].Flags&net.FlagPointToPoint != 0 {
|
||||
// this interface is ppp, we're not interested in this one
|
||||
continue
|
||||
}
|
||||
|
||||
iface := ifaces[i]
|
||||
netIfaces = append(netIfaces, iface)
|
||||
}
|
||||
|
||||
return netIfaces, nil
|
||||
}
|
||||
|
||||
// checkPortAvailable is not a cheap test to see if the port is bindable, because it's actually doing the bind momentarily
|
||||
func checkPortAvailable(host string, port int) error {
|
||||
ln, err := net.Listen("tcp", net.JoinHostPort(host, strconv.Itoa(port)))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
ln.Close()
|
||||
return nil
|
||||
}
|
||||
|
||||
func checkPacketPortAvailable(host string, port int) error {
|
||||
ln, err := net.ListenPacket("udp", net.JoinHostPort(host, strconv.Itoa(port)))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
ln.Close()
|
||||
return err
|
||||
}
|
||||
|
||||
// ---------------------
|
||||
// debug logging helpers
|
||||
// ---------------------
|
||||
|
|
|
@ -34,7 +34,7 @@ func upgradeConfig() error {
|
|||
}
|
||||
|
||||
schemaVersionInterface, ok := diskConfig["schema_version"]
|
||||
log.Printf("%s(): got schema version %v", _Func(), schemaVersionInterface)
|
||||
log.Tracef("got schema version %v", schemaVersionInterface)
|
||||
if !ok {
|
||||
// no schema version, set it to 0
|
||||
schemaVersionInterface = 0
|
||||
|
|
Loading…
Reference in New Issue