all: sync with master; upd chlog
This commit is contained in:
parent
b40bbf0260
commit
5f6fbe8e08
|
@ -109,7 +109,8 @@
|
||||||
CHANNEL=${bamboo.channel}\
|
CHANNEL=${bamboo.channel}\
|
||||||
GPG_KEY_PASSPHRASE=${bamboo.gpgPassword}\
|
GPG_KEY_PASSPHRASE=${bamboo.gpgPassword}\
|
||||||
FRONTEND_PREBUILT=1\
|
FRONTEND_PREBUILT=1\
|
||||||
VERBOSE=1\
|
PARALLELISM=1\
|
||||||
|
VERBOSE=2\
|
||||||
build-release
|
build-release
|
||||||
# TODO(a.garipov): Use more fine-grained artifact rules.
|
# TODO(a.garipov): Use more fine-grained artifact rules.
|
||||||
'artifacts':
|
'artifacts':
|
||||||
|
@ -239,18 +240,12 @@
|
||||||
;;
|
;;
|
||||||
esac
|
esac
|
||||||
|
|
||||||
# Ignore errors from the Snapstore upload script, because it seems to
|
|
||||||
# have a lot of issues recently.
|
|
||||||
#
|
|
||||||
# TODO(a.garipov): Stop ignoring those errors once they fix the issues.
|
|
||||||
#
|
|
||||||
# See https://forum.snapcraft.io/t/unable-to-upload-promote-snaps-to-edge/33120.
|
|
||||||
env\
|
env\
|
||||||
SNAPCRAFT_CHANNEL="$snapchannel"\
|
SNAPCRAFT_CHANNEL="$snapchannel"\
|
||||||
SNAPCRAFT_EMAIL="${bamboo.snapcraftEmail}"\
|
SNAPCRAFT_EMAIL="${bamboo.snapcraftEmail}"\
|
||||||
SNAPCRAFT_MACAROON="${bamboo.snapcraftMacaroonPassword}"\
|
SNAPCRAFT_MACAROON="${bamboo.snapcraftMacaroonPassword}"\
|
||||||
SNAPCRAFT_UBUNTU_DISCHARGE="${bamboo.snapcraftUbuntuDischargePassword}"\
|
SNAPCRAFT_UBUNTU_DISCHARGE="${bamboo.snapcraftUbuntuDischargePassword}"\
|
||||||
../bamboo-deploy-publisher/deploy.sh adguard-home-snap || :
|
../bamboo-deploy-publisher/deploy.sh adguard-home-snap
|
||||||
'final-tasks':
|
'final-tasks':
|
||||||
- 'clean'
|
- 'clean'
|
||||||
'requirements':
|
'requirements':
|
||||||
|
|
|
@ -298,6 +298,9 @@
|
||||||
"blocking_mode_nxdomain": "NXDOMAIN: Respond with NXDOMAIN code",
|
"blocking_mode_nxdomain": "NXDOMAIN: Respond with NXDOMAIN code",
|
||||||
"blocking_mode_null_ip": "Null IP: Respond with zero IP address (0.0.0.0 for A; :: for AAAA)",
|
"blocking_mode_null_ip": "Null IP: Respond with zero IP address (0.0.0.0 for A; :: for AAAA)",
|
||||||
"blocking_mode_custom_ip": "Custom IP: Respond with a manually set IP address",
|
"blocking_mode_custom_ip": "Custom IP: Respond with a manually set IP address",
|
||||||
|
"theme_auto": "Auto",
|
||||||
|
"theme_light": "Light",
|
||||||
|
"theme_dark": "Dark",
|
||||||
"upstream_dns_client_desc": "If you keep this field empty, AdGuard Home will use the servers configured in the <0>DNS settings</0>.",
|
"upstream_dns_client_desc": "If you keep this field empty, AdGuard Home will use the servers configured in the <0>DNS settings</0>.",
|
||||||
"tracker_source": "Tracker source",
|
"tracker_source": "Tracker source",
|
||||||
"source_label": "Source",
|
"source_label": "Source",
|
||||||
|
|
|
@ -41,6 +41,12 @@ export const setTlsConfig = (config) => async (dispatch, getState) => {
|
||||||
response.certificate_chain = atob(response.certificate_chain);
|
response.certificate_chain = atob(response.certificate_chain);
|
||||||
response.private_key = atob(response.private_key);
|
response.private_key = atob(response.private_key);
|
||||||
|
|
||||||
|
if (values.enabled && values.force_https && window.location.protocol === 'http:') {
|
||||||
|
window.location.reload();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
redirectToCurrentProtocol(response, httpPort);
|
||||||
|
|
||||||
const dnsStatus = await apiClient.getGlobalStatus();
|
const dnsStatus = await apiClient.getGlobalStatus();
|
||||||
if (dnsStatus) {
|
if (dnsStatus) {
|
||||||
dispatch(dnsStatusSuccess(dnsStatus));
|
dispatch(dnsStatusSuccess(dnsStatus));
|
||||||
|
@ -48,7 +54,6 @@ export const setTlsConfig = (config) => async (dispatch, getState) => {
|
||||||
|
|
||||||
dispatch(setTlsConfigSuccess(response));
|
dispatch(setTlsConfigSuccess(response));
|
||||||
dispatch(addSuccessToast('encryption_config_saved'));
|
dispatch(addSuccessToast('encryption_config_saved'));
|
||||||
redirectToCurrentProtocol(response, httpPort);
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
dispatch(addErrorToast({ error }));
|
dispatch(addErrorToast({ error }));
|
||||||
dispatch(setTlsConfigFailure());
|
dispatch(setTlsConfigFailure());
|
||||||
|
|
|
@ -363,18 +363,18 @@ export const changeLanguage = (lang) => async (dispatch) => {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getLanguageRequest = createAction('GET_LANGUAGE_REQUEST');
|
export const changeThemeRequest = createAction('CHANGE_THEME_REQUEST');
|
||||||
export const getLanguageFailure = createAction('GET_LANGUAGE_FAILURE');
|
export const changeThemeFailure = createAction('CHANGE_THEME_FAILURE');
|
||||||
export const getLanguageSuccess = createAction('GET_LANGUAGE_SUCCESS');
|
export const changeThemeSuccess = createAction('CHANGE_THEME_SUCCESS');
|
||||||
|
|
||||||
export const getLanguage = () => async (dispatch) => {
|
export const changeTheme = (theme) => async (dispatch) => {
|
||||||
dispatch(getLanguageRequest());
|
dispatch(changeThemeRequest());
|
||||||
try {
|
try {
|
||||||
const langSettings = await apiClient.getCurrentLanguage();
|
await apiClient.changeTheme({ theme });
|
||||||
dispatch(getLanguageSuccess(langSettings.language));
|
dispatch(changeThemeSuccess({ theme }));
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
dispatch(addErrorToast({ error }));
|
dispatch(addErrorToast({ error }));
|
||||||
dispatch(getLanguageFailure());
|
dispatch(changeThemeFailure());
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -1,8 +1,12 @@
|
||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
|
|
||||||
import { getPathWithQueryString } from '../helpers/helpers';
|
import { getPathWithQueryString } from '../helpers/helpers';
|
||||||
import { QUERY_LOGS_PAGE_LIMIT, HTML_PAGES, R_PATH_LAST_PART } from '../helpers/constants';
|
import {
|
||||||
|
QUERY_LOGS_PAGE_LIMIT, HTML_PAGES, R_PATH_LAST_PART, THEMES,
|
||||||
|
} from '../helpers/constants';
|
||||||
import { BASE_URL } from '../../constants';
|
import { BASE_URL } from '../../constants';
|
||||||
|
import i18n from '../i18n';
|
||||||
|
import { LANGUAGES } from '../helpers/twosky';
|
||||||
|
|
||||||
class Api {
|
class Api {
|
||||||
baseUrl = BASE_URL;
|
baseUrl = BASE_URL;
|
||||||
|
@ -224,21 +228,21 @@ class Api {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Language
|
// Language
|
||||||
CURRENT_LANGUAGE = { path: 'i18n/current_language', method: 'GET' };
|
|
||||||
|
|
||||||
CHANGE_LANGUAGE = { path: 'i18n/change_language', method: 'POST' };
|
async changeLanguage(config) {
|
||||||
|
const profile = await this.getProfile();
|
||||||
|
profile.language = config.language;
|
||||||
|
|
||||||
getCurrentLanguage() {
|
return this.setProfile(profile);
|
||||||
const { path, method } = this.CURRENT_LANGUAGE;
|
|
||||||
return this.makeRequest(path, method);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
changeLanguage(config) {
|
// Theme
|
||||||
const { path, method } = this.CHANGE_LANGUAGE;
|
|
||||||
const parameters = {
|
async changeTheme(config) {
|
||||||
data: config,
|
const profile = await this.getProfile();
|
||||||
};
|
profile.theme = config.theme;
|
||||||
return this.makeRequest(path, method, parameters);
|
|
||||||
|
return this.setProfile(profile);
|
||||||
}
|
}
|
||||||
|
|
||||||
// DHCP
|
// DHCP
|
||||||
|
@ -571,11 +575,24 @@ class Api {
|
||||||
// Profile
|
// Profile
|
||||||
GET_PROFILE = { path: 'profile', method: 'GET' };
|
GET_PROFILE = { path: 'profile', method: 'GET' };
|
||||||
|
|
||||||
|
UPDATE_PROFILE = { path: 'profile/update', method: 'PUT' };
|
||||||
|
|
||||||
getProfile() {
|
getProfile() {
|
||||||
const { path, method } = this.GET_PROFILE;
|
const { path, method } = this.GET_PROFILE;
|
||||||
return this.makeRequest(path, method);
|
return this.makeRequest(path, method);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
setProfile(data) {
|
||||||
|
const theme = data.theme ? data.theme : THEMES.auto;
|
||||||
|
const defaultLanguage = i18n.language ? i18n.language : LANGUAGES.en;
|
||||||
|
const language = data.language ? data.language : defaultLanguage;
|
||||||
|
|
||||||
|
const { path, method } = this.UPDATE_PROFILE;
|
||||||
|
const config = { data: { theme, language } };
|
||||||
|
|
||||||
|
return this.makeRequest(path, method, config);
|
||||||
|
}
|
||||||
|
|
||||||
// DNS config
|
// DNS config
|
||||||
GET_DNS_CONFIG = { path: 'dns_info', method: 'GET' };
|
GET_DNS_CONFIG = { path: 'dns_info', method: 'GET' };
|
||||||
|
|
||||||
|
|
|
@ -1,4 +1,26 @@
|
||||||
:root {
|
:root {
|
||||||
|
--bgcolor: #f5f7fb;
|
||||||
|
--mcolor: #495057;
|
||||||
|
--scolor: rgba(74, 74, 74, 0.7);
|
||||||
|
--border-color: rgba(0, 40, 100, 0.12);
|
||||||
|
--header-bgcolor: #fff;
|
||||||
|
--card-bgcolor: #fff;
|
||||||
|
--card-border-color: rgba(0, 40, 100, 0.12);
|
||||||
|
--ctrl-bgcolor: #fff;
|
||||||
|
--ctrl-select-bgcolor: rgba(69, 79, 94, 0.12);
|
||||||
|
--ctrl-dropdown-color: #212529;
|
||||||
|
--ctrl-dropdown-bgcolor-focus: #f8f9fa;
|
||||||
|
--ctrl-dropdown-color-focus: #16181b;
|
||||||
|
--btn-success-bgcolor: #5eba00;
|
||||||
|
--form-disabled-bgcolor: #f8f9fa;
|
||||||
|
--form-disabled-color: #495057;
|
||||||
|
--rt-nodata-bgcolor: rgba(255,255,255,0.8);
|
||||||
|
--rt-nodata-color: rgba(0,0,0,0.5);
|
||||||
|
--modal-overlay-bgcolor: rgba(255, 255, 255, 0.75);
|
||||||
|
--logs__table-bgcolor: #fff;
|
||||||
|
--logs__row--blue-bgcolor: #e5effd;
|
||||||
|
--logs__row--white-bgcolor: #fff;
|
||||||
|
--detailed-info-color: #888888;
|
||||||
--yellow-pale: rgba(247, 181, 0, 0.1);
|
--yellow-pale: rgba(247, 181, 0, 0.1);
|
||||||
--green79: #67b279;
|
--green79: #67b279;
|
||||||
--gray-a5: #a5a5a5;
|
--gray-a5: #a5a5a5;
|
||||||
|
@ -8,6 +30,32 @@
|
||||||
--font-size-disable-autozoom: 1rem;
|
--font-size-disable-autozoom: 1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[data-theme="dark"] {
|
||||||
|
--bgcolor: #131313;
|
||||||
|
--mcolor: #e6e6e6;
|
||||||
|
--scolor: #a5a5a5;
|
||||||
|
--header-bgcolor: #131313;
|
||||||
|
--border-color: #222;
|
||||||
|
--card-bgcolor: #1c1c1c;
|
||||||
|
--card-border-color: #3d3d3d;
|
||||||
|
--ctrl-bgcolor: #1c1c1c;
|
||||||
|
--ctrl-select-bgcolor: #3d3d3d;
|
||||||
|
--ctrl-dropdown-color: #fff;
|
||||||
|
--ctrl-dropdown-bgcolor-focus: #000;
|
||||||
|
--ctrl-dropdown-color-focus: #fff;
|
||||||
|
--btn-success-bgcolor: #67b279;
|
||||||
|
--form-disabled-bgcolor: #3d3d3d;
|
||||||
|
--form-disabled-color: #a5a5a5;
|
||||||
|
--logs__text-color: #f3f3f3;
|
||||||
|
--rt-nodata-bgcolor: #1c1c1c;
|
||||||
|
--rt-nodata-color: #fff;
|
||||||
|
--modal-overlay-bgcolor: #1c1c1c;
|
||||||
|
--logs__table-bgcolor: #3d3d3d;
|
||||||
|
--logs__row--blue-bgcolor: #467fcf;
|
||||||
|
--logs__row--white-bgcolor: #1c1c1c;
|
||||||
|
--detailed-info-color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
body {
|
body {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
|
|
|
@ -20,8 +20,13 @@ import EncryptionTopline from '../ui/EncryptionTopline';
|
||||||
import Icons from '../ui/Icons';
|
import Icons from '../ui/Icons';
|
||||||
import i18n from '../../i18n';
|
import i18n from '../../i18n';
|
||||||
import Loading from '../ui/Loading';
|
import Loading from '../ui/Loading';
|
||||||
import { FILTERS_URLS, MENU_URLS, SETTINGS_URLS } from '../../helpers/constants';
|
import {
|
||||||
import { getLogsUrlParams, setHtmlLangAttr } from '../../helpers/helpers';
|
FILTERS_URLS,
|
||||||
|
MENU_URLS,
|
||||||
|
SETTINGS_URLS,
|
||||||
|
THEMES,
|
||||||
|
} from '../../helpers/constants';
|
||||||
|
import { getLogsUrlParams, setHtmlLangAttr, setUITheme } from '../../helpers/helpers';
|
||||||
import Header from '../Header';
|
import Header from '../Header';
|
||||||
import { changeLanguage, getDnsStatus } from '../../actions';
|
import { changeLanguage, getDnsStatus } from '../../actions';
|
||||||
|
|
||||||
|
@ -109,6 +114,7 @@ const App = () => {
|
||||||
isCoreRunning,
|
isCoreRunning,
|
||||||
isUpdateAvailable,
|
isUpdateAvailable,
|
||||||
processing,
|
processing,
|
||||||
|
theme,
|
||||||
} = useSelector((state) => state.dashboard, shallowEqual);
|
} = useSelector((state) => state.dashboard, shallowEqual);
|
||||||
|
|
||||||
const { processing: processingEncryption } = useSelector((
|
const { processing: processingEncryption } = useSelector((
|
||||||
|
@ -138,6 +144,41 @@ const App = () => {
|
||||||
setLanguage();
|
setLanguage();
|
||||||
}, [language]);
|
}, [language]);
|
||||||
|
|
||||||
|
const handleAutoTheme = (e, accountTheme) => {
|
||||||
|
if (accountTheme !== THEMES.auto) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (e.matches) {
|
||||||
|
setUITheme(THEMES.dark);
|
||||||
|
} else {
|
||||||
|
setUITheme(THEMES.light);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (theme !== THEMES.auto) {
|
||||||
|
setUITheme(theme);
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const colorSchemeMedia = window.matchMedia('(prefers-color-scheme: dark)');
|
||||||
|
const prefersDark = colorSchemeMedia.matches;
|
||||||
|
setUITheme(prefersDark ? THEMES.dark : THEMES.light);
|
||||||
|
|
||||||
|
if (colorSchemeMedia.addEventListener !== undefined) {
|
||||||
|
colorSchemeMedia.addEventListener('change', (e) => {
|
||||||
|
handleAutoTheme(e, theme);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
// Deprecated addListener for older versions of Safari.
|
||||||
|
colorSchemeMedia.addListener((e) => {
|
||||||
|
handleAutoTheme(e, theme);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, [theme]);
|
||||||
|
|
||||||
const reloadPage = () => {
|
const reloadPage = () => {
|
||||||
window.location.reload();
|
window.location.reload();
|
||||||
};
|
};
|
||||||
|
|
|
@ -47,7 +47,7 @@
|
||||||
width: 250px;
|
width: 250px;
|
||||||
height: 100vh;
|
height: 100vh;
|
||||||
transition: transform 0.3s ease;
|
transition: transform 0.3s ease;
|
||||||
background-color: #fff;
|
background-color: var(--header-bgcolor);
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -4,7 +4,8 @@
|
||||||
box-shadow: 2px 4px 8px rgba(0, 0, 0, 0.2);
|
box-shadow: 2px 4px 8px rgba(0, 0, 0, 0.2);
|
||||||
border-radius: 4px !important;
|
border-radius: 4px !important;
|
||||||
pointer-events: auto !important;
|
pointer-events: auto !important;
|
||||||
background-color: var(--white);
|
background-color: var(--ctrl-bgcolor);
|
||||||
|
color: var(--scolor);
|
||||||
z-index: 102;
|
z-index: 102;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
max-height: 100%;
|
max-height: 100%;
|
||||||
|
|
|
@ -155,7 +155,7 @@ const Form = (props) => {
|
||||||
name={FORM_NAMES.search}
|
name={FORM_NAMES.search}
|
||||||
component={renderFilterField}
|
component={renderFilterField}
|
||||||
type="text"
|
type="text"
|
||||||
className={classNames('form-control--search form-control--transparent', className)}
|
className={classNames('form-control form-control--search form-control--transparent', className)}
|
||||||
placeholder={t('domain_or_client')}
|
placeholder={t('domain_or_client')}
|
||||||
tooltip={t('query_log_strict_search')}
|
tooltip={t('query_log_strict_search')}
|
||||||
onClearInputClick={onInputClear}
|
onClearInputClick={onInputClear}
|
||||||
|
|
|
@ -31,7 +31,7 @@
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
font-size: 1rem;
|
font-size: 1rem;
|
||||||
font-family: var(--font-family-sans-serif);
|
font-family: var(--font-family-sans-serif);
|
||||||
color: var(--gray-4d);
|
color: var(--logs__text-color);
|
||||||
letter-spacing: 0;
|
letter-spacing: 0;
|
||||||
line-height: 1.5rem;
|
line-height: 1.5rem;
|
||||||
}
|
}
|
||||||
|
@ -48,7 +48,7 @@
|
||||||
.detailed-info {
|
.detailed-info {
|
||||||
font-size: 0.8rem;
|
font-size: 0.8rem;
|
||||||
line-height: 1.4;
|
line-height: 1.4;
|
||||||
color: #888888;
|
color: var(--detailed-info-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
.logs__text--link {
|
.logs__text--link {
|
||||||
|
@ -103,14 +103,12 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.form-control--search {
|
.form-control--search {
|
||||||
box-shadow: 0 1px 0 #ddd;
|
|
||||||
padding: 0 2.5rem;
|
padding: 0 2.5rem;
|
||||||
height: 2.25rem;
|
height: 2.25rem;
|
||||||
flex-grow: 1;
|
flex-grow: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
.form-control--transparent {
|
.form-control--transparent {
|
||||||
border: 0 solid transparent !important;
|
|
||||||
background-color: transparent !important;
|
background-color: transparent !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -174,10 +172,8 @@
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
|
width: 2.5rem;
|
||||||
--size: 2.5rem;
|
height: 2.5rem;
|
||||||
width: var(--size);
|
|
||||||
height: var(--size);
|
|
||||||
padding: 0;
|
padding: 0;
|
||||||
margin-left: 0.9375rem;
|
margin-left: 0.9375rem;
|
||||||
background-color: transparent;
|
background-color: transparent;
|
||||||
|
@ -373,7 +369,7 @@
|
||||||
|
|
||||||
/* QUERY_STATUS_COLORS */
|
/* QUERY_STATUS_COLORS */
|
||||||
.logs__row--blue {
|
.logs__row--blue {
|
||||||
background-color: var(--blue);
|
background-color: var(--logs__row--blue-bgcolor);
|
||||||
}
|
}
|
||||||
|
|
||||||
.logs__row--green {
|
.logs__row--green {
|
||||||
|
@ -385,7 +381,7 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.logs__row--white {
|
.logs__row--white {
|
||||||
background-color: var(--white);
|
background-color: var(--logs__row--white-bgcolor);
|
||||||
}
|
}
|
||||||
|
|
||||||
.logs__row--yellow {
|
.logs__row--yellow {
|
||||||
|
@ -393,8 +389,8 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.logs__no-data {
|
.logs__no-data {
|
||||||
color: var(--gray-4d);
|
color: var(--mcolor);
|
||||||
background-color: var(--white80);
|
background-color: var(--logs__table-bgcolor);
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
|
@ -407,7 +403,7 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.logs__table {
|
.logs__table {
|
||||||
background-color: var(--white);
|
background-color: var(--logs__table-bgcolor);
|
||||||
border: 0;
|
border: 0;
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
min-height: 43rem;
|
min-height: 43rem;
|
||||||
|
@ -474,7 +470,7 @@
|
||||||
|
|
||||||
.filteringRules__filter {
|
.filteringRules__filter {
|
||||||
font-style: italic;
|
font-style: italic;
|
||||||
font-weight: normal;
|
font-weight: 400;
|
||||||
margin-bottom: 1rem;
|
margin-bottom: 1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -11,12 +11,13 @@ import Select from 'react-select';
|
||||||
import i18n from '../../../i18n';
|
import i18n from '../../../i18n';
|
||||||
import Tabs from '../../ui/Tabs';
|
import Tabs from '../../ui/Tabs';
|
||||||
import Examples from '../Dns/Upstream/Examples';
|
import Examples from '../Dns/Upstream/Examples';
|
||||||
import { toggleAllServices } from '../../../helpers/helpers';
|
import { toggleAllServices, trimLinesAndRemoveEmpty } from '../../../helpers/helpers';
|
||||||
import {
|
import {
|
||||||
renderInputField,
|
renderInputField,
|
||||||
renderGroupField,
|
renderGroupField,
|
||||||
CheckboxField,
|
CheckboxField,
|
||||||
renderServiceField,
|
renderServiceField,
|
||||||
|
renderTextareaField,
|
||||||
} from '../../../helpers/form';
|
} from '../../../helpers/form';
|
||||||
import { validateClientId, validateRequiredValue } from '../../../helpers/validators';
|
import { validateClientId, validateRequiredValue } from '../../../helpers/validators';
|
||||||
import { CLIENT_ID_LINK, FORM_NAME } from '../../../helpers/constants';
|
import { CLIENT_ID_LINK, FORM_NAME } from '../../../helpers/constants';
|
||||||
|
@ -230,10 +231,11 @@ let Form = (props) => {
|
||||||
<Field
|
<Field
|
||||||
id="upstreams"
|
id="upstreams"
|
||||||
name="upstreams"
|
name="upstreams"
|
||||||
component="textarea"
|
component={renderTextareaField}
|
||||||
type="text"
|
type="text"
|
||||||
className="form-control form-control--textarea mb-5"
|
className="form-control form-control--textarea mb-5"
|
||||||
placeholder={t('upstream_dns')}
|
placeholder={t('upstream_dns')}
|
||||||
|
normalizeOnBlur={trimLinesAndRemoveEmpty}
|
||||||
/>
|
/>
|
||||||
<Examples />
|
<Examples />
|
||||||
</div>,
|
</div>,
|
||||||
|
|
|
@ -77,7 +77,7 @@
|
||||||
.form__desc {
|
.form__desc {
|
||||||
margin-top: 10px;
|
margin-top: 10px;
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
color: rgba(74, 74, 74, 0.7);
|
color: var(--scolor);
|
||||||
}
|
}
|
||||||
|
|
||||||
.form__desc--top {
|
.form__desc--top {
|
||||||
|
|
|
@ -107,5 +107,5 @@
|
||||||
.checkbox__label-subtitle {
|
.checkbox__label-subtitle {
|
||||||
display: block;
|
display: block;
|
||||||
line-height: 1.5;
|
line-height: 1.5;
|
||||||
color: rgba(74, 74, 74, 0.7);
|
color: var(--scolor);
|
||||||
}
|
}
|
||||||
|
|
|
@ -18,6 +18,11 @@
|
||||||
align-items: center;
|
align-items: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.footer__column--theme {
|
||||||
|
min-width: 220px;
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
.footer__column--language {
|
.footer__column--language {
|
||||||
min-width: 220px;
|
min-width: 220px;
|
||||||
margin-bottom: 0;
|
margin-bottom: 0;
|
||||||
|
@ -49,6 +54,11 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.footer__column--language {
|
.footer__column--language {
|
||||||
|
min-width: initial;
|
||||||
|
margin-left: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer__column--theme {
|
||||||
min-width: initial;
|
min-width: initial;
|
||||||
margin-left: auto;
|
margin-left: auto;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,8 +1,9 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { useDispatch, useSelector } from 'react-redux';
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
|
|
||||||
import { REPOSITORY, PRIVACY_POLICY_LINK } from '../../helpers/constants';
|
import { REPOSITORY, PRIVACY_POLICY_LINK, THEMES } from '../../helpers/constants';
|
||||||
import { LANGUAGES } from '../../helpers/twosky';
|
import { LANGUAGES } from '../../helpers/twosky';
|
||||||
import i18n from '../../i18n';
|
import i18n from '../../i18n';
|
||||||
|
|
||||||
|
@ -10,6 +11,7 @@ import Version from './Version';
|
||||||
import './Footer.css';
|
import './Footer.css';
|
||||||
import './Select.css';
|
import './Select.css';
|
||||||
import { setHtmlLangAttr } from '../../helpers/helpers';
|
import { setHtmlLangAttr } from '../../helpers/helpers';
|
||||||
|
import { changeTheme } from '../../actions';
|
||||||
|
|
||||||
const linksData = [
|
const linksData = [
|
||||||
{
|
{
|
||||||
|
@ -29,6 +31,11 @@ const linksData = [
|
||||||
|
|
||||||
const Footer = () => {
|
const Footer = () => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
const dispatch = useDispatch();
|
||||||
|
|
||||||
|
const currentTheme = useSelector((state) => (state.dashboard ? state.dashboard.theme : 'auto'));
|
||||||
|
const profileName = useSelector((state) => (state.dashboard ? state.dashboard.name : ''));
|
||||||
|
const isLoggedIn = profileName !== '';
|
||||||
|
|
||||||
const getYear = () => {
|
const getYear = () => {
|
||||||
const today = new Date();
|
const today = new Date();
|
||||||
|
@ -41,6 +48,11 @@ const Footer = () => {
|
||||||
setHtmlLangAttr(value);
|
setHtmlLangAttr(value);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const onThemeChanged = (event) => {
|
||||||
|
const { value } = event.target;
|
||||||
|
dispatch(changeTheme(value));
|
||||||
|
};
|
||||||
|
|
||||||
const renderCopyright = () => <div className="footer__column">
|
const renderCopyright = () => <div className="footer__column">
|
||||||
<div className="footer__copyright">
|
<div className="footer__copyright">
|
||||||
{t('copyright')} © {getYear()}{' '}
|
{t('copyright')} © {getYear()}{' '}
|
||||||
|
@ -58,6 +70,25 @@ const Footer = () => {
|
||||||
{t(name)}
|
{t(name)}
|
||||||
</a>);
|
</a>);
|
||||||
|
|
||||||
|
const renderThemeSelect = (currentTheme, isLoggedIn) => {
|
||||||
|
if (!isLoggedIn) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
return <select
|
||||||
|
className="form-control select select--theme"
|
||||||
|
value={currentTheme}
|
||||||
|
onChange={onThemeChanged}
|
||||||
|
>
|
||||||
|
{Object.values(THEMES)
|
||||||
|
.map((theme) => (
|
||||||
|
<option key={theme} value={theme}>
|
||||||
|
{t(`theme_${theme}`)}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</select>;
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<footer className="footer">
|
<footer className="footer">
|
||||||
|
@ -66,6 +97,9 @@ const Footer = () => {
|
||||||
<div className="footer__column footer__column--links">
|
<div className="footer__column footer__column--links">
|
||||||
{renderLinks(linksData)}
|
{renderLinks(linksData)}
|
||||||
</div>
|
</div>
|
||||||
|
<div className="footer__column footer__column--theme">
|
||||||
|
{renderThemeSelect(currentTheme, isLoggedIn)}
|
||||||
|
</div>
|
||||||
<div className="footer__column footer__column--language">
|
<div className="footer__column footer__column--language">
|
||||||
<select
|
<select
|
||||||
className="form-control select select--language"
|
className="form-control select select--language"
|
||||||
|
|
|
@ -11,6 +11,7 @@
|
||||||
.ReactModal__Overlay--after-open {
|
.ReactModal__Overlay--after-open {
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
transition: opacity 150ms ease-out;
|
transition: opacity 150ms ease-out;
|
||||||
|
background-color: var(--modal-overlay-bgcolor) !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.ReactModal__Content {
|
.ReactModal__Content {
|
||||||
|
|
|
@ -13,6 +13,26 @@
|
||||||
overflow: visible;
|
overflow: visible;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.ReactTable .rt-noData {
|
||||||
|
color: var(--rt-nodata-color);
|
||||||
|
background-color: var(--rt-nodata-bgcolor);
|
||||||
|
}
|
||||||
|
|
||||||
|
.ReactTable .-loading {
|
||||||
|
color: var(--rt-nodata-color);
|
||||||
|
background-color: var(--rt-nodata-bgcolor);
|
||||||
|
}
|
||||||
|
|
||||||
|
.ReactTable .-pagination input, .ReactTable .-pagination select {
|
||||||
|
color: var(--rt-nodata-color);
|
||||||
|
background-color: var(--rt-nodata-bgcolor);
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-theme=dark] .ReactTable .-pagination .-btn {
|
||||||
|
color: var(--scolor);
|
||||||
|
background-color: var(--ctrl-bgcolor);
|
||||||
|
}
|
||||||
|
|
||||||
.rt-tr-group.logs__row--red {
|
.rt-tr-group.logs__row--red {
|
||||||
background-color: rgba(223, 56, 18, 0.05);
|
background-color: rgba(223, 56, 18, 0.05);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,8 +1,25 @@
|
||||||
|
.select.select--theme {
|
||||||
|
height: 45px;
|
||||||
|
padding: 0 32px 2px 11px;
|
||||||
|
outline: 0;
|
||||||
|
border-color: var(--ctrl-select-bgcolor);
|
||||||
|
background-image: url("./svg/chevron-down.svg");
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
background-position: right 9px center;
|
||||||
|
background-size: 17px 20px;
|
||||||
|
appearance: none;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.select--theme::-ms-expand {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
|
||||||
.select.select--language {
|
.select.select--language {
|
||||||
height: 45px;
|
height: 45px;
|
||||||
padding: 0 32px 2px 33px;
|
padding: 0 32px 2px 33px;
|
||||||
outline: 0;
|
outline: 0;
|
||||||
border-color: rgba(69, 79, 94, 0.12);
|
border-color: var(--ctrl-select-bgcolor);
|
||||||
background-image: url("./svg/globe.svg"), url("./svg/chevron-down.svg");
|
background-image: url("./svg/globe.svg"), url("./svg/chevron-down.svg");
|
||||||
background-repeat: no-repeat, no-repeat;
|
background-repeat: no-repeat, no-repeat;
|
||||||
background-position: left 11px center, right 9px center;
|
background-position: left 11px center, right 9px center;
|
||||||
|
@ -16,8 +33,9 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.basic-multi-select .select__control {
|
.basic-multi-select .select__control {
|
||||||
border: 1px solid rgba(0, 40, 100, 0.12);
|
border: 1px solid var(--card-border-color);;
|
||||||
border-radius: 3px;
|
border-radius: 3px;
|
||||||
|
background-color: var(--ctrl-bgcolor);
|
||||||
}
|
}
|
||||||
|
|
||||||
.basic-multi-select .select__control:hover {
|
.basic-multi-select .select__control:hover {
|
||||||
|
@ -36,4 +54,16 @@
|
||||||
|
|
||||||
.basic-multi-select .select__menu {
|
.basic-multi-select .select__menu {
|
||||||
z-index: 3;
|
z-index: 3;
|
||||||
|
background-color: var(--ctrl-bgcolor);
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-theme=dark] .basic-multi-select .select__option:hover,
|
||||||
|
[data-theme=dark] .basic-multi-select .select__option--is-focused,
|
||||||
|
[data-theme=dark] .basic-multi-select .select__option--is-focused:hover {
|
||||||
|
background-color: var(--ctrl-select-bgcolor);
|
||||||
|
color: var(--ctrl-dropdown-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-theme=dark] .select__multi-value__remove svg {
|
||||||
|
filter: invert(1);
|
||||||
}
|
}
|
||||||
|
|
|
@ -85,9 +85,9 @@ body {
|
||||||
font-size: 0.9375rem;
|
font-size: 0.9375rem;
|
||||||
font-weight: 400;
|
font-weight: 400;
|
||||||
line-height: 1.5;
|
line-height: 1.5;
|
||||||
color: #495057;
|
color: var(--mcolor);
|
||||||
text-align: left;
|
text-align: left;
|
||||||
background-color: #f5f7fb;
|
background-color: var(--bgcolor);
|
||||||
}
|
}
|
||||||
|
|
||||||
[tabindex="-1"]:focus {
|
[tabindex="-1"]:focus {
|
||||||
|
@ -1943,10 +1943,10 @@ pre code {
|
||||||
padding: 0.375rem 0.75rem;
|
padding: 0.375rem 0.75rem;
|
||||||
font-size: 0.9375rem;
|
font-size: 0.9375rem;
|
||||||
line-height: 1.6;
|
line-height: 1.6;
|
||||||
color: #495057;
|
color: var(--mcolor);
|
||||||
background-color: #fff;
|
background-color: var(--card-bgcolor);
|
||||||
background-clip: padding-box;
|
background-clip: padding-box;
|
||||||
border: 1px solid rgba(0, 40, 100, 0.12);
|
border: 1px solid var(--card-border-color);
|
||||||
border-radius: 3px;
|
border-radius: 3px;
|
||||||
transition: border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out;
|
transition: border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out;
|
||||||
}
|
}
|
||||||
|
@ -1957,8 +1957,8 @@ pre code {
|
||||||
}
|
}
|
||||||
|
|
||||||
.form-control:focus {
|
.form-control:focus {
|
||||||
color: #495057;
|
color: var(--mcolor);
|
||||||
background-color: #fff;
|
background-color: var(--ctrl-bgcolor);
|
||||||
border-color: #1991eb;
|
border-color: #1991eb;
|
||||||
outline: 0;
|
outline: 0;
|
||||||
box-shadow: 0 0 0 2px rgba(70, 127, 207, 0.25);
|
box-shadow: 0 0 0 2px rgba(70, 127, 207, 0.25);
|
||||||
|
@ -1991,7 +1991,8 @@ pre code {
|
||||||
|
|
||||||
.form-control:disabled,
|
.form-control:disabled,
|
||||||
.form-control[readonly] {
|
.form-control[readonly] {
|
||||||
background-color: #f8f9fa;
|
background-color: var(--form-disabled-bgcolor);
|
||||||
|
color: var(--form-disabled-color);
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -2580,7 +2581,7 @@ fieldset:disabled a.btn {
|
||||||
|
|
||||||
.btn-success {
|
.btn-success {
|
||||||
color: #fff;
|
color: #fff;
|
||||||
background-color: #5eba00;
|
background-color: var(--btn-success-bgcolor);
|
||||||
border-color: #5eba00;
|
border-color: #5eba00;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -3244,7 +3245,7 @@ tbody.collapse.show {
|
||||||
color: #495057;
|
color: #495057;
|
||||||
text-align: left;
|
text-align: left;
|
||||||
list-style: none;
|
list-style: none;
|
||||||
background-color: #fff;
|
background-color: var(--ctrl-bgcolor);
|
||||||
background-clip: padding-box;
|
background-clip: padding-box;
|
||||||
border: 1px solid rgba(0, 40, 100, 0.12);
|
border: 1px solid rgba(0, 40, 100, 0.12);
|
||||||
border-radius: 3px;
|
border-radius: 3px;
|
||||||
|
@ -3348,7 +3349,7 @@ tbody.collapse.show {
|
||||||
padding: 0.25rem 1.5rem;
|
padding: 0.25rem 1.5rem;
|
||||||
clear: both;
|
clear: both;
|
||||||
font-weight: 400;
|
font-weight: 400;
|
||||||
color: #212529;
|
color: var(--ctrl-dropdown-color);
|
||||||
text-align: inherit;
|
text-align: inherit;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
background-color: transparent;
|
background-color: transparent;
|
||||||
|
@ -3357,9 +3358,9 @@ tbody.collapse.show {
|
||||||
|
|
||||||
.dropdown-item:hover,
|
.dropdown-item:hover,
|
||||||
.dropdown-item:focus {
|
.dropdown-item:focus {
|
||||||
color: #16181b;
|
color: var(--ctrl-dropdown-color-focus);
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
background-color: #f8f9fa;
|
background-color: var(--ctrl-dropdown-bgcolor-focus);
|
||||||
}
|
}
|
||||||
|
|
||||||
.dropdown-item.active,
|
.dropdown-item.active,
|
||||||
|
@ -3794,11 +3795,11 @@ tbody.collapse.show {
|
||||||
height: 2.375rem;
|
height: 2.375rem;
|
||||||
padding: 0.5rem 1.75rem 0.5rem 0.75rem;
|
padding: 0.5rem 1.75rem 0.5rem 0.75rem;
|
||||||
line-height: 1.5;
|
line-height: 1.5;
|
||||||
color: #495057;
|
color: var(--mcolor);
|
||||||
vertical-align: middle;
|
vertical-align: middle;
|
||||||
background: #fff url("data:image/svg+xml;base64,PHN2ZyB4bWxucz0naHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmcnIHZpZXdCb3g9JzAgMCAxMCA1Jz48cGF0aCBmaWxsPScjOTk5JyBkPSdNMCAwTDEwIDBMNSA1TDAgMCcvPjwvc3ZnPg==") no-repeat right 0.75rem center;
|
background: var(--card-bgcolor) url("data:image/svg+xml;base64,PHN2ZyB4bWxucz0naHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmcnIHZpZXdCb3g9JzAgMCAxMCA1Jz48cGF0aCBmaWxsPScjOTk5JyBkPSdNMCAwTDEwIDBMNSA1TDAgMCcvPjwvc3ZnPg==") no-repeat right 0.75rem center;
|
||||||
background-size: 8px 10px;
|
background-size: 8px 10px;
|
||||||
border: 1px solid rgba(0, 40, 100, 0.12);
|
border: 1px solid var(--card-border-color);
|
||||||
border-radius: 3px;
|
border-radius: 3px;
|
||||||
-webkit-appearance: none;
|
-webkit-appearance: none;
|
||||||
-moz-appearance: none;
|
-moz-appearance: none;
|
||||||
|
@ -4469,9 +4470,9 @@ tbody.collapse.show {
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
word-wrap: break-word;
|
word-wrap: break-word;
|
||||||
background-color: #fff;
|
background-color: var(--card-bgcolor);
|
||||||
background-clip: border-box;
|
background-clip: border-box;
|
||||||
border: 1px solid rgba(0, 40, 100, 0.12);
|
border: 1px solid var(--card-border-color);
|
||||||
border-radius: 3px;
|
border-radius: 3px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -5475,9 +5476,9 @@ button.close {
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
pointer-events: auto;
|
pointer-events: auto;
|
||||||
background-color: #fff;
|
background-color: var(--card-bgcolor);
|
||||||
background-clip: padding-box;
|
background-clip: padding-box;
|
||||||
border: 1px solid rgba(0, 0, 0, 0.2);
|
border: 1px solid var(--card-border-color);
|
||||||
border-radius: 3px;
|
border-radius: 3px;
|
||||||
outline: 0;
|
outline: 0;
|
||||||
}
|
}
|
||||||
|
@ -10268,8 +10269,8 @@ body.fixed-header .page {
|
||||||
.header {
|
.header {
|
||||||
padding-top: 0.75rem;
|
padding-top: 0.75rem;
|
||||||
padding-bottom: 0.75rem;
|
padding-bottom: 0.75rem;
|
||||||
background: #fff;
|
background: var(--header-bgcolor);
|
||||||
border-bottom: 1px solid rgba(0, 40, 100, 0.12);
|
border-bottom: 1px solid var(--border-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
body.fixed-header .header {
|
body.fixed-header .header {
|
||||||
|
@ -10325,6 +10326,10 @@ body.fixed-header .header {
|
||||||
width: auto;
|
width: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[data-theme=dark] .header-brand-img {
|
||||||
|
filter:invert(1);
|
||||||
|
}
|
||||||
|
|
||||||
.header-avatar {
|
.header-avatar {
|
||||||
width: 2rem;
|
width: 2rem;
|
||||||
height: 2rem;
|
height: 2rem;
|
||||||
|
@ -10382,8 +10387,8 @@ body.fixed-header .header {
|
||||||
}
|
}
|
||||||
|
|
||||||
.footer {
|
.footer {
|
||||||
background: #fff;
|
background: var(--card-bgcolor);
|
||||||
border-top: 1px solid rgba(0, 40, 100, 0.12);
|
border-top: 1px solid var(--card-border-color);
|
||||||
font-size: 0.875rem;
|
font-size: 0.875rem;
|
||||||
padding: 1.25rem 0;
|
padding: 1.25rem 0;
|
||||||
color: #9aa0ac;
|
color: #9aa0ac;
|
||||||
|
@ -13686,13 +13691,17 @@ Card alert
|
||||||
content: "";
|
content: "";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[data-theme=dark] .dropdown-menu-arrow:before {
|
||||||
|
border-bottom-color: var(--ctrl-bgcolor);
|
||||||
|
}
|
||||||
|
|
||||||
.dropdown-menu-arrow:after {
|
.dropdown-menu-arrow:after {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: -5px;
|
top: -5px;
|
||||||
left: 12px;
|
left: 12px;
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
border-right: 5px solid transparent;
|
border-right: 5px solid transparent;
|
||||||
border-bottom: 5px solid #fff;
|
border-bottom: 5px solid var(--ctrl-bgcolor);
|
||||||
border-left: 5px solid transparent;
|
border-left: 5px solid transparent;
|
||||||
content: "";
|
content: "";
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,6 +4,11 @@
|
||||||
box-shadow: 1px 1px 6px rgba(0, 0, 0, 0.2);
|
box-shadow: 1px 1px 6px rgba(0, 0, 0, 0.2);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[data-theme=dark] .tooltip-container {
|
||||||
|
background-color: var(--ctrl-select-bgcolor);
|
||||||
|
color: var(--mcolor);
|
||||||
|
}
|
||||||
|
|
||||||
.tooltip-custom--narrow {
|
.tooltip-custom--narrow {
|
||||||
max-width: 14rem;
|
max-width: 14rem;
|
||||||
}
|
}
|
||||||
|
|
|
@ -227,6 +227,14 @@ export const BLOCKING_MODES = {
|
||||||
custom_ip: 'custom_ip',
|
custom_ip: 'custom_ip',
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Note that translation strings contain these modes (theme_CONSTANT)
|
||||||
|
// i.e. theme_auto, theme_light.
|
||||||
|
export const THEMES = {
|
||||||
|
auto: 'auto',
|
||||||
|
dark: 'dark',
|
||||||
|
light: 'light',
|
||||||
|
};
|
||||||
|
|
||||||
export const WHOIS_ICONS = {
|
export const WHOIS_ICONS = {
|
||||||
location: 'location',
|
location: 'location',
|
||||||
orgname: 'network',
|
orgname: 'network',
|
||||||
|
|
|
@ -670,6 +670,15 @@ export const setHtmlLangAttr = (language) => {
|
||||||
window.document.documentElement.lang = language;
|
window.document.documentElement.lang = language;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets UI theme.
|
||||||
|
*
|
||||||
|
* @param theme
|
||||||
|
*/
|
||||||
|
export const setUITheme = (theme) => {
|
||||||
|
document.body.dataset.theme = theme;
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param values {object}
|
* @param values {object}
|
||||||
* @returns {object}
|
* @returns {object}
|
||||||
|
|
|
@ -112,14 +112,6 @@ const dashboard = handleActions(
|
||||||
return newState;
|
return newState;
|
||||||
},
|
},
|
||||||
|
|
||||||
[actions.getLanguageSuccess]: (state, { payload }) => {
|
|
||||||
const newState = {
|
|
||||||
...state,
|
|
||||||
language: payload,
|
|
||||||
};
|
|
||||||
return newState;
|
|
||||||
},
|
|
||||||
|
|
||||||
[actions.getClientsRequest]: (state) => ({
|
[actions.getClientsRequest]: (state) => ({
|
||||||
...state,
|
...state,
|
||||||
processingClients: true,
|
processingClients: true,
|
||||||
|
@ -148,8 +140,13 @@ const dashboard = handleActions(
|
||||||
[actions.getProfileSuccess]: (state, { payload }) => ({
|
[actions.getProfileSuccess]: (state, { payload }) => ({
|
||||||
...state,
|
...state,
|
||||||
name: payload.name,
|
name: payload.name,
|
||||||
|
theme: payload.theme,
|
||||||
processingProfile: false,
|
processingProfile: false,
|
||||||
}),
|
}),
|
||||||
|
[actions.changeThemeSuccess]: (state, { payload }) => ({
|
||||||
|
...state,
|
||||||
|
theme: payload.theme,
|
||||||
|
}),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
processing: true,
|
processing: true,
|
||||||
|
@ -168,6 +165,7 @@ const dashboard = handleActions(
|
||||||
autoClients: [],
|
autoClients: [],
|
||||||
supportedTags: [],
|
supportedTags: [],
|
||||||
name: '',
|
name: '',
|
||||||
|
theme: 'auto',
|
||||||
checkUpdateFlag: false,
|
checkUpdateFlag: false,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
|
@ -530,14 +530,14 @@ func validateBlockingMode(mode BlockingMode, blockingIPv4, blockingIPv6 net.IP)
|
||||||
// prepareInternalProxy initializes the DNS proxy that is used for internal DNS
|
// prepareInternalProxy initializes the DNS proxy that is used for internal DNS
|
||||||
// queries, such as public clients PTR resolving and updater hostname resolving.
|
// queries, such as public clients PTR resolving and updater hostname resolving.
|
||||||
func (s *Server) prepareInternalProxy() (err error) {
|
func (s *Server) prepareInternalProxy() (err error) {
|
||||||
|
srvConf := s.conf
|
||||||
conf := &proxy.Config{
|
conf := &proxy.Config{
|
||||||
CacheEnabled: true,
|
CacheEnabled: true,
|
||||||
CacheSizeBytes: 4096,
|
CacheSizeBytes: 4096,
|
||||||
UpstreamConfig: s.conf.UpstreamConfig,
|
UpstreamConfig: srvConf.UpstreamConfig,
|
||||||
MaxGoroutines: int(s.conf.MaxGoroutines),
|
MaxGoroutines: int(s.conf.MaxGoroutines),
|
||||||
}
|
}
|
||||||
|
|
||||||
srvConf := s.conf
|
|
||||||
setProxyUpstreamMode(
|
setProxyUpstreamMode(
|
||||||
conf,
|
conf,
|
||||||
srvConf.AllServers,
|
srvConf.AllServers,
|
||||||
|
|
|
@ -2,6 +2,7 @@ package filtering
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bufio"
|
"bufio"
|
||||||
|
"bytes"
|
||||||
"fmt"
|
"fmt"
|
||||||
"hash/crc32"
|
"hash/crc32"
|
||||||
"io"
|
"io"
|
||||||
|
@ -12,6 +13,7 @@ import (
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/AdguardTeam/AdGuardHome/internal/aghalg"
|
||||||
"github.com/AdguardTeam/golibs/errors"
|
"github.com/AdguardTeam/golibs/errors"
|
||||||
"github.com/AdguardTeam/golibs/log"
|
"github.com/AdguardTeam/golibs/log"
|
||||||
"github.com/AdguardTeam/golibs/stringutil"
|
"github.com/AdguardTeam/golibs/stringutil"
|
||||||
|
@ -97,14 +99,15 @@ func (d *DNSFilter) filterSetProperties(
|
||||||
filt.URL,
|
filt.URL,
|
||||||
)
|
)
|
||||||
|
|
||||||
defer func(oldURL, oldName string, oldEnabled bool, oldUpdated time.Time) {
|
defer func(oldURL, oldName string, oldEnabled bool, oldUpdated time.Time, oldRulesCount int) {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
filt.URL = oldURL
|
filt.URL = oldURL
|
||||||
filt.Name = oldName
|
filt.Name = oldName
|
||||||
filt.Enabled = oldEnabled
|
filt.Enabled = oldEnabled
|
||||||
filt.LastUpdated = oldUpdated
|
filt.LastUpdated = oldUpdated
|
||||||
|
filt.RulesCount = oldRulesCount
|
||||||
}
|
}
|
||||||
}(filt.URL, filt.Name, filt.Enabled, filt.LastUpdated)
|
}(filt.URL, filt.Name, filt.Enabled, filt.LastUpdated, filt.RulesCount)
|
||||||
|
|
||||||
filt.Name = newList.Name
|
filt.Name = newList.Name
|
||||||
|
|
||||||
|
@ -134,8 +137,8 @@ func (d *DNSFilter) filterSetProperties(
|
||||||
// TODO(e.burkov): The validation of the contents of the new URL is
|
// TODO(e.burkov): The validation of the contents of the new URL is
|
||||||
// currently skipped if the rule list is disabled. This makes it
|
// currently skipped if the rule list is disabled. This makes it
|
||||||
// possible to set a bad rules source, but the validation should still
|
// possible to set a bad rules source, but the validation should still
|
||||||
// kick in when the filter is enabled. Consider making changing this
|
// kick in when the filter is enabled. Consider changing this behavior
|
||||||
// behavior to be stricter.
|
// to be stricter.
|
||||||
filt.unload()
|
filt.unload()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -269,10 +272,10 @@ func (d *DNSFilter) periodicallyRefreshFilters() {
|
||||||
// already going on.
|
// already going on.
|
||||||
//
|
//
|
||||||
// TODO(e.burkov): Get rid of the concurrency pattern which requires the
|
// TODO(e.burkov): Get rid of the concurrency pattern which requires the
|
||||||
// sync.Mutex.TryLock.
|
// [sync.Mutex.TryLock].
|
||||||
func (d *DNSFilter) tryRefreshFilters(block, allow, force bool) (updated int, isNetworkErr, ok bool) {
|
func (d *DNSFilter) tryRefreshFilters(block, allow, force bool) (updated int, isNetworkErr, ok bool) {
|
||||||
if ok = d.refreshLock.TryLock(); !ok {
|
if ok = d.refreshLock.TryLock(); !ok {
|
||||||
return 0, false, ok
|
return 0, false, false
|
||||||
}
|
}
|
||||||
defer d.refreshLock.Unlock()
|
defer d.refreshLock.Unlock()
|
||||||
|
|
||||||
|
@ -427,52 +430,124 @@ func (d *DNSFilter) refreshFiltersIntl(block, allow, force bool) (int, bool) {
|
||||||
return updNum, false
|
return updNum, false
|
||||||
}
|
}
|
||||||
|
|
||||||
// Allows printable UTF-8 text with CR, LF, TAB characters
|
// isPrintableText returns true if data is printable UTF-8 text with CR, LF, TAB
|
||||||
func isPrintableText(data []byte, len int) bool {
|
// characters.
|
||||||
for i := 0; i < len; i++ {
|
//
|
||||||
c := data[i]
|
// TODO(e.burkov): Investigate the purpose of this and improve the
|
||||||
|
// implementation. Perhaps, use something from the unicode package.
|
||||||
|
func isPrintableText(data string) (ok bool) {
|
||||||
|
for _, c := range []byte(data) {
|
||||||
if (c >= ' ' && c != 0x7f) || c == '\n' || c == '\r' || c == '\t' {
|
if (c >= ' ' && c != 0x7f) || c == '\n' || c == '\r' || c == '\t' {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
// A helper function that parses filter contents and returns a number of rules and a filter name (if there's any)
|
// scanLinesWithBreak is essentially a [bufio.ScanLines] which keeps trailing
|
||||||
func (d *DNSFilter) parseFilterContents(file io.Reader) (int, uint32, string) {
|
// line breaks.
|
||||||
rulesCount := 0
|
func scanLinesWithBreak(data []byte, atEOF bool) (advance int, token []byte, err error) {
|
||||||
name := ""
|
if atEOF && len(data) == 0 {
|
||||||
seenTitle := false
|
return 0, nil, nil
|
||||||
r := bufio.NewReader(file)
|
}
|
||||||
checksum := uint32(0)
|
|
||||||
|
if i := bytes.IndexByte(data, '\n'); i >= 0 {
|
||||||
|
return i + 1, data[0 : i+1], nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if atEOF {
|
||||||
|
return len(data), data, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Request more data.
|
||||||
|
return 0, nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// parseFilter copies filter's content from src to dst and returns the number of
|
||||||
|
// rules, name, number of bytes written, checksum, and title of the parsed list.
|
||||||
|
// dst must not be nil.
|
||||||
|
func (d *DNSFilter) parseFilter(
|
||||||
|
src io.Reader,
|
||||||
|
dst io.Writer,
|
||||||
|
) (rulesNum, written int, checksum uint32, title string, err error) {
|
||||||
|
scanner := bufio.NewScanner(src)
|
||||||
|
scanner.Split(scanLinesWithBreak)
|
||||||
|
|
||||||
|
titleFound := false
|
||||||
|
for n := 0; scanner.Scan(); written += n {
|
||||||
|
line := scanner.Text()
|
||||||
|
var isRule bool
|
||||||
|
var likelyTitle string
|
||||||
|
isRule, likelyTitle, err = d.parseFilterLine(line, !titleFound, written == 0)
|
||||||
|
if err != nil {
|
||||||
|
return 0, written, 0, "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
if isRule {
|
||||||
|
rulesNum++
|
||||||
|
} else if likelyTitle != "" {
|
||||||
|
title, titleFound = likelyTitle, true
|
||||||
|
}
|
||||||
|
|
||||||
for {
|
|
||||||
line, err := r.ReadString('\n')
|
|
||||||
checksum = crc32.Update(checksum, crc32.IEEETable, []byte(line))
|
checksum = crc32.Update(checksum, crc32.IEEETable, []byte(line))
|
||||||
|
|
||||||
line = strings.TrimSpace(line)
|
n, err = dst.Write([]byte(line))
|
||||||
if len(line) == 0 {
|
|
||||||
//
|
|
||||||
} else if line[0] == '!' {
|
|
||||||
m := d.filterTitleRegexp.FindAllStringSubmatch(line, -1)
|
|
||||||
if len(m) > 0 && len(m[0]) >= 2 && !seenTitle {
|
|
||||||
name = m[0][1]
|
|
||||||
seenTitle = true
|
|
||||||
}
|
|
||||||
|
|
||||||
} else if line[0] == '#' {
|
|
||||||
//
|
|
||||||
} else {
|
|
||||||
rulesCount++
|
|
||||||
}
|
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
break
|
return 0, written, 0, "", fmt.Errorf("writing filter line: %w", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return rulesCount, checksum, name
|
if err = scanner.Err(); err != nil {
|
||||||
|
return 0, written, 0, "", fmt.Errorf("scanning filter contents: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return rulesNum, written, checksum, title, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// parseFilterLine returns true if the passed line is a rule. line is
|
||||||
|
// considered a rule if it's not a comment and contains no title.
|
||||||
|
func (d *DNSFilter) parseFilterLine(
|
||||||
|
line string,
|
||||||
|
lookForTitle bool,
|
||||||
|
testHTML bool,
|
||||||
|
) (isRule bool, title string, err error) {
|
||||||
|
if !isPrintableText(line) {
|
||||||
|
return false, "", errors.Error("filter contains non-printable characters")
|
||||||
|
}
|
||||||
|
|
||||||
|
line = strings.TrimSpace(line)
|
||||||
|
if line == "" || line[0] == '#' {
|
||||||
|
return false, "", nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if testHTML && isHTML(line) {
|
||||||
|
return false, "", errors.Error("data is HTML, not plain text")
|
||||||
|
}
|
||||||
|
|
||||||
|
if line[0] == '!' && lookForTitle {
|
||||||
|
match := d.filterTitleRegexp.FindStringSubmatch(line)
|
||||||
|
if len(match) > 1 {
|
||||||
|
title = match[1]
|
||||||
|
}
|
||||||
|
|
||||||
|
return false, title, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return true, "", nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// isHTML returns true if the line contains HTML tags instead of plain text.
|
||||||
|
// line shouldn have no leading space symbols.
|
||||||
|
//
|
||||||
|
// TODO(ameshkov): It actually gives too much false-positives. Perhaps, just
|
||||||
|
// check if trimmed string begins with angle bracket.
|
||||||
|
func isHTML(line string) (ok bool) {
|
||||||
|
line = strings.ToLower(line)
|
||||||
|
|
||||||
|
return strings.HasPrefix(line, "<html") || strings.HasPrefix(line, "<!doctype")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Perform upgrade on a filter and update LastUpdated value
|
// Perform upgrade on a filter and update LastUpdated value
|
||||||
|
@ -485,57 +560,10 @@ func (d *DNSFilter) update(filter *FilterYAML) (bool, error) {
|
||||||
log.Error("os.Chtimes(): %v", e)
|
log.Error("os.Chtimes(): %v", e)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return b, err
|
return b, err
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *DNSFilter) read(reader io.Reader, tmpFile *os.File, filter *FilterYAML) (int, error) {
|
|
||||||
htmlTest := true
|
|
||||||
firstChunk := make([]byte, 4*1024)
|
|
||||||
firstChunkLen := 0
|
|
||||||
buf := make([]byte, 64*1024)
|
|
||||||
total := 0
|
|
||||||
for {
|
|
||||||
n, err := reader.Read(buf)
|
|
||||||
total += n
|
|
||||||
|
|
||||||
if htmlTest {
|
|
||||||
num := len(firstChunk) - firstChunkLen
|
|
||||||
if n < num {
|
|
||||||
num = n
|
|
||||||
}
|
|
||||||
copied := copy(firstChunk[firstChunkLen:], buf[:num])
|
|
||||||
firstChunkLen += copied
|
|
||||||
|
|
||||||
if firstChunkLen == len(firstChunk) || err == io.EOF {
|
|
||||||
if !isPrintableText(firstChunk, firstChunkLen) {
|
|
||||||
return total, fmt.Errorf("data contains non-printable characters")
|
|
||||||
}
|
|
||||||
|
|
||||||
s := strings.ToLower(string(firstChunk))
|
|
||||||
if strings.Contains(s, "<html") || strings.Contains(s, "<!doctype") {
|
|
||||||
return total, fmt.Errorf("data is HTML, not plain text")
|
|
||||||
}
|
|
||||||
|
|
||||||
htmlTest = false
|
|
||||||
firstChunk = nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
_, err2 := tmpFile.Write(buf[:n])
|
|
||||||
if err2 != nil {
|
|
||||||
return total, err2
|
|
||||||
}
|
|
||||||
|
|
||||||
if err == io.EOF {
|
|
||||||
return total, nil
|
|
||||||
}
|
|
||||||
if err != nil {
|
|
||||||
log.Printf("Couldn't fetch filter contents from URL %s, skipping: %s", filter.URL, err)
|
|
||||||
return total, err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// finalizeUpdate closes and gets rid of temporary file f with filter's content
|
// finalizeUpdate closes and gets rid of temporary file f with filter's content
|
||||||
// according to updated. It also saves new values of flt's name, rules number
|
// according to updated. It also saves new values of flt's name, rules number
|
||||||
// and checksum if sucсeeded.
|
// and checksum if sucсeeded.
|
||||||
|
@ -552,7 +580,8 @@ func (d *DNSFilter) finalizeUpdate(
|
||||||
// Close the file before renaming it because it's required on Windows.
|
// Close the file before renaming it because it's required on Windows.
|
||||||
//
|
//
|
||||||
// See https://github.com/adguardTeam/adGuardHome/issues/1553.
|
// See https://github.com/adguardTeam/adGuardHome/issues/1553.
|
||||||
if err = file.Close(); err != nil {
|
err = file.Close()
|
||||||
|
if err != nil {
|
||||||
return fmt.Errorf("closing temporary file: %w", err)
|
return fmt.Errorf("closing temporary file: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -564,38 +593,18 @@ func (d *DNSFilter) finalizeUpdate(
|
||||||
|
|
||||||
log.Printf("saving filter %d contents to: %s", flt.ID, flt.Path(d.DataDir))
|
log.Printf("saving filter %d contents to: %s", flt.ID, flt.Path(d.DataDir))
|
||||||
|
|
||||||
if err = os.Rename(tmpFileName, flt.Path(d.DataDir)); err != nil {
|
// Don't use renamio or maybe packages, since those will require loading the
|
||||||
|
// whole filter content to the memory on Windows.
|
||||||
|
err = os.Rename(tmpFileName, flt.Path(d.DataDir))
|
||||||
|
if err != nil {
|
||||||
return errors.WithDeferred(err, os.Remove(tmpFileName))
|
return errors.WithDeferred(err, os.Remove(tmpFileName))
|
||||||
}
|
}
|
||||||
|
|
||||||
flt.Name = stringutil.Coalesce(flt.Name, name)
|
flt.Name, flt.checksum, flt.RulesCount = aghalg.Coalesce(flt.Name, name), cs, rnum
|
||||||
flt.checksum = cs
|
|
||||||
flt.RulesCount = rnum
|
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// processUpdate copies filter's content from src to dst and returns the name,
|
|
||||||
// rules number, and checksum for it. It also returns the number of bytes read
|
|
||||||
// from src.
|
|
||||||
func (d *DNSFilter) processUpdate(
|
|
||||||
src io.Reader,
|
|
||||||
dst *os.File,
|
|
||||||
flt *FilterYAML,
|
|
||||||
) (name string, rnum int, cs uint32, n int, err error) {
|
|
||||||
if n, err = d.read(src, dst, flt); err != nil {
|
|
||||||
return "", 0, 0, 0, err
|
|
||||||
}
|
|
||||||
|
|
||||||
if _, err = dst.Seek(0, io.SeekStart); err != nil {
|
|
||||||
return "", 0, 0, 0, err
|
|
||||||
}
|
|
||||||
|
|
||||||
rnum, cs, name = d.parseFilterContents(dst)
|
|
||||||
|
|
||||||
return name, rnum, cs, n, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// updateIntl updates the flt rewriting it's actual file. It returns true if
|
// updateIntl updates the flt rewriting it's actual file. It returns true if
|
||||||
// the actual update has been performed.
|
// the actual update has been performed.
|
||||||
func (d *DNSFilter) updateIntl(flt *FilterYAML) (ok bool, err error) {
|
func (d *DNSFilter) updateIntl(flt *FilterYAML) (ok bool, err error) {
|
||||||
|
@ -612,31 +621,21 @@ func (d *DNSFilter) updateIntl(flt *FilterYAML) (ok bool, err error) {
|
||||||
}
|
}
|
||||||
defer func() {
|
defer func() {
|
||||||
err = errors.WithDeferred(err, d.finalizeUpdate(tmpFile, flt, ok, name, rnum, cs))
|
err = errors.WithDeferred(err, d.finalizeUpdate(tmpFile, flt, ok, name, rnum, cs))
|
||||||
ok = ok && err == nil
|
if ok && err == nil {
|
||||||
if ok {
|
|
||||||
log.Printf("updated filter %d: %d bytes, %d rules", flt.ID, n, rnum)
|
log.Printf("updated filter %d: %d bytes, %d rules", flt.ID, n, rnum)
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
|
||||||
// Change the default 0o600 permission to something more acceptable by
|
// Change the default 0o600 permission to something more acceptable by end
|
||||||
// end users.
|
// users.
|
||||||
//
|
//
|
||||||
// See https://github.com/AdguardTeam/AdGuardHome/issues/3198.
|
// See https://github.com/AdguardTeam/AdGuardHome/issues/3198.
|
||||||
if err = tmpFile.Chmod(0o644); err != nil {
|
if err = tmpFile.Chmod(0o644); err != nil {
|
||||||
return false, fmt.Errorf("changing file mode: %w", err)
|
return false, fmt.Errorf("changing file mode: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
var r io.Reader
|
var rc io.ReadCloser
|
||||||
if filepath.IsAbs(flt.URL) {
|
if !filepath.IsAbs(flt.URL) {
|
||||||
var file io.ReadCloser
|
|
||||||
file, err = os.Open(flt.URL)
|
|
||||||
if err != nil {
|
|
||||||
return false, fmt.Errorf("open file: %w", err)
|
|
||||||
}
|
|
||||||
defer func() { err = errors.WithDeferred(err, file.Close()) }()
|
|
||||||
|
|
||||||
r = file
|
|
||||||
} else {
|
|
||||||
var resp *http.Response
|
var resp *http.Response
|
||||||
resp, err = d.HTTPClient.Get(flt.URL)
|
resp, err = d.HTTPClient.Get(flt.URL)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -649,24 +648,30 @@ func (d *DNSFilter) updateIntl(flt *FilterYAML) (ok bool, err error) {
|
||||||
if resp.StatusCode != http.StatusOK {
|
if resp.StatusCode != http.StatusOK {
|
||||||
log.Printf("got status code %d from %s, skip", resp.StatusCode, flt.URL)
|
log.Printf("got status code %d from %s, skip", resp.StatusCode, flt.URL)
|
||||||
|
|
||||||
return false, fmt.Errorf("got status code != 200: %d", resp.StatusCode)
|
return false, fmt.Errorf("got status code %d, want %d", resp.StatusCode, http.StatusOK)
|
||||||
}
|
}
|
||||||
|
|
||||||
r = resp.Body
|
rc = resp.Body
|
||||||
|
} else {
|
||||||
|
rc, err = os.Open(flt.URL)
|
||||||
|
if err != nil {
|
||||||
|
return false, fmt.Errorf("open file: %w", err)
|
||||||
|
}
|
||||||
|
defer func() { err = errors.WithDeferred(err, rc.Close()) }()
|
||||||
}
|
}
|
||||||
|
|
||||||
name, rnum, cs, n, err = d.processUpdate(r, tmpFile, flt)
|
rnum, n, cs, name, err = d.parseFilter(rc, tmpFile)
|
||||||
|
|
||||||
return cs != flt.checksum, err
|
return cs != flt.checksum && err == nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// loads filter contents from the file in dataDir
|
// loads filter contents from the file in dataDir
|
||||||
func (d *DNSFilter) load(filter *FilterYAML) (err error) {
|
func (d *DNSFilter) load(flt *FilterYAML) (err error) {
|
||||||
filterFilePath := filter.Path(d.DataDir)
|
fileName := flt.Path(d.DataDir)
|
||||||
|
|
||||||
log.Tracef("filtering: loading filter %d from %s", filter.ID, filterFilePath)
|
log.Debug("filtering: loading filter %d from %s", flt.ID, fileName)
|
||||||
|
|
||||||
file, err := os.Open(filterFilePath)
|
file, err := os.Open(fileName)
|
||||||
if errors.Is(err, os.ErrNotExist) {
|
if errors.Is(err, os.ErrNotExist) {
|
||||||
// Do nothing, file doesn't exist.
|
// Do nothing, file doesn't exist.
|
||||||
return nil
|
return nil
|
||||||
|
@ -680,13 +685,14 @@ func (d *DNSFilter) load(filter *FilterYAML) (err error) {
|
||||||
return fmt.Errorf("getting filter file stat: %w", err)
|
return fmt.Errorf("getting filter file stat: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Tracef("filtering: File %s, id %d, length %d", filterFilePath, filter.ID, st.Size())
|
log.Debug("filtering: file %s, id %d, length %d", fileName, flt.ID, st.Size())
|
||||||
|
|
||||||
rulesCount, checksum, _ := d.parseFilterContents(file)
|
rulesCount, _, checksum, _, err := d.parseFilter(file, io.Discard)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("parsing filter file: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
filter.RulesCount = rulesCount
|
flt.RulesCount, flt.checksum, flt.LastUpdated = rulesCount, checksum, st.ModTime()
|
||||||
filter.checksum = checksum
|
|
||||||
filter.LastUpdated = st.ModTime()
|
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,33 +4,23 @@ import (
|
||||||
"io/fs"
|
"io/fs"
|
||||||
"net"
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/netip"
|
|
||||||
"net/url"
|
"net/url"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/AdguardTeam/golibs/netutil"
|
"github.com/AdguardTeam/AdGuardHome/internal/aghhttp"
|
||||||
"github.com/AdguardTeam/golibs/testutil"
|
"github.com/AdguardTeam/golibs/testutil"
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
)
|
)
|
||||||
|
|
||||||
// serveFiltersLocally is a helper that concurrently listens on a free port to
|
// serveHTTPLocally starts a new HTTP server, that handles its index with h. It
|
||||||
// respond with fltContent. It also gracefully closes the listener when the
|
// also gracefully closes the listener when the test under t finishes.
|
||||||
// test under t finishes.
|
func serveHTTPLocally(t *testing.T, h http.Handler) (urlStr string) {
|
||||||
func serveFiltersLocally(t *testing.T, fltContent []byte) (ipp netip.AddrPort) {
|
|
||||||
t.Helper()
|
t.Helper()
|
||||||
|
|
||||||
h := http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
|
|
||||||
pt := testutil.PanicT{}
|
|
||||||
|
|
||||||
n, werr := w.Write(fltContent)
|
|
||||||
require.NoError(pt, werr)
|
|
||||||
require.Equal(pt, len(fltContent), n)
|
|
||||||
})
|
|
||||||
|
|
||||||
l, err := net.Listen("tcp", ":0")
|
l, err := net.Listen("tcp", ":0")
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
@ -38,9 +28,26 @@ func serveFiltersLocally(t *testing.T, fltContent []byte) (ipp netip.AddrPort) {
|
||||||
testutil.CleanupAndRequireSuccess(t, l.Close)
|
testutil.CleanupAndRequireSuccess(t, l.Close)
|
||||||
|
|
||||||
addr := l.Addr()
|
addr := l.Addr()
|
||||||
require.IsType(t, new(net.TCPAddr), addr)
|
require.IsType(t, (*net.TCPAddr)(nil), addr)
|
||||||
|
|
||||||
return netip.AddrPortFrom(netutil.IPv4Localhost(), uint16(addr.(*net.TCPAddr).Port))
|
return (&url.URL{
|
||||||
|
Scheme: aghhttp.SchemeHTTP,
|
||||||
|
Host: addr.String(),
|
||||||
|
}).String()
|
||||||
|
}
|
||||||
|
|
||||||
|
// serveFiltersLocally is a helper that concurrently listens on a free port to
|
||||||
|
// respond with fltContent.
|
||||||
|
func serveFiltersLocally(t *testing.T, fltContent []byte) (urlStr string) {
|
||||||
|
t.Helper()
|
||||||
|
|
||||||
|
return serveHTTPLocally(t, http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
|
||||||
|
pt := testutil.PanicT{}
|
||||||
|
|
||||||
|
n, werr := w.Write(fltContent)
|
||||||
|
require.NoError(pt, werr)
|
||||||
|
require.Equal(pt, len(fltContent), n)
|
||||||
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestFilters(t *testing.T) {
|
func TestFilters(t *testing.T) {
|
||||||
|
@ -65,10 +72,7 @@ func TestFilters(t *testing.T) {
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
f := &FilterYAML{
|
f := &FilterYAML{
|
||||||
URL: (&url.URL{
|
URL: addr,
|
||||||
Scheme: "http",
|
|
||||||
Host: addr.String(),
|
|
||||||
}).String(),
|
|
||||||
}
|
}
|
||||||
|
|
||||||
updateAndAssert := func(t *testing.T, want require.BoolAssertionFunc, wantRulesCount int) {
|
updateAndAssert := func(t *testing.T, want require.BoolAssertionFunc, wantRulesCount int) {
|
||||||
|
@ -103,11 +107,7 @@ func TestFilters(t *testing.T) {
|
||||||
anotherContent := []byte(`||example.com^`)
|
anotherContent := []byte(`||example.com^`)
|
||||||
oldURL := f.URL
|
oldURL := f.URL
|
||||||
|
|
||||||
ipp := serveFiltersLocally(t, anotherContent)
|
f.URL = serveFiltersLocally(t, anotherContent)
|
||||||
f.URL = (&url.URL{
|
|
||||||
Scheme: "http",
|
|
||||||
Host: ipp.String(),
|
|
||||||
}).String()
|
|
||||||
t.Cleanup(func() { f.URL = oldURL })
|
t.Cleanup(func() { f.URL = oldURL })
|
||||||
|
|
||||||
updateAndAssert(t, require.True, 1)
|
updateAndAssert(t, require.True, 1)
|
||||||
|
|
|
@ -190,6 +190,8 @@ type DNSFilter struct {
|
||||||
|
|
||||||
// filterTitleRegexp is the regular expression to retrieve a name of a
|
// filterTitleRegexp is the regular expression to retrieve a name of a
|
||||||
// filter list.
|
// filter list.
|
||||||
|
//
|
||||||
|
// TODO(e.burkov): Don't use regexp for such a simple text processing task.
|
||||||
filterTitleRegexp *regexp.Regexp
|
filterTitleRegexp *regexp.Regexp
|
||||||
|
|
||||||
hostCheckers []hostChecker
|
hostCheckers []hostChecker
|
||||||
|
|
|
@ -5,7 +5,6 @@ import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/http/httptest"
|
"net/http/httptest"
|
||||||
"net/url"
|
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
@ -30,11 +29,7 @@ func TestDNSFilter_handleFilteringSetURL(t *testing.T) {
|
||||||
endpoint: &badRulesEndpoint,
|
endpoint: &badRulesEndpoint,
|
||||||
content: []byte(`<html></html>`),
|
content: []byte(`<html></html>`),
|
||||||
}} {
|
}} {
|
||||||
ipp := serveFiltersLocally(t, rulesSource.content)
|
*rulesSource.endpoint = serveFiltersLocally(t, rulesSource.content)
|
||||||
*rulesSource.endpoint = (&url.URL{
|
|
||||||
Scheme: "http",
|
|
||||||
Host: ipp.String(),
|
|
||||||
}).String()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
testCases := []struct {
|
testCases := []struct {
|
||||||
|
|
|
@ -106,6 +106,8 @@ type configuration struct {
|
||||||
ProxyURL string `yaml:"http_proxy"`
|
ProxyURL string `yaml:"http_proxy"`
|
||||||
// Language is a two-letter ISO 639-1 language code.
|
// Language is a two-letter ISO 639-1 language code.
|
||||||
Language string `yaml:"language"`
|
Language string `yaml:"language"`
|
||||||
|
// Theme is a UI theme for current user.
|
||||||
|
Theme Theme `yaml:"theme"`
|
||||||
// DebugPProf defines if the profiling HTTP handler will listen on :6060.
|
// DebugPProf defines if the profiling HTTP handler will listen on :6060.
|
||||||
DebugPProf bool `yaml:"debug_pprof"`
|
DebugPProf bool `yaml:"debug_pprof"`
|
||||||
|
|
||||||
|
@ -322,6 +324,7 @@ var config = &configuration{
|
||||||
},
|
},
|
||||||
OSConfig: &osConfig{},
|
OSConfig: &osConfig{},
|
||||||
SchemaVersion: currentSchemaVersion,
|
SchemaVersion: currentSchemaVersion,
|
||||||
|
Theme: ThemeAuto,
|
||||||
}
|
}
|
||||||
|
|
||||||
// getConfigFilename returns path to the current config file
|
// getConfigFilename returns path to the current config file
|
||||||
|
|
|
@ -149,19 +149,6 @@ func handleStatus(w http.ResponseWriter, r *http.Request) {
|
||||||
_ = aghhttp.WriteJSONResponse(w, r, resp)
|
_ = aghhttp.WriteJSONResponse(w, r, resp)
|
||||||
}
|
}
|
||||||
|
|
||||||
type profileJSON struct {
|
|
||||||
Name string `json:"name"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func handleGetProfile(w http.ResponseWriter, r *http.Request) {
|
|
||||||
u := Context.auth.getCurrentUser(r)
|
|
||||||
resp := &profileJSON{
|
|
||||||
Name: u.Name,
|
|
||||||
}
|
|
||||||
|
|
||||||
_ = aghhttp.WriteJSONResponse(w, r, resp)
|
|
||||||
}
|
|
||||||
|
|
||||||
// ------------------------
|
// ------------------------
|
||||||
// registration of handlers
|
// registration of handlers
|
||||||
// ------------------------
|
// ------------------------
|
||||||
|
@ -172,6 +159,7 @@ func registerControlHandlers() {
|
||||||
Context.mux.HandleFunc("/control/version.json", postInstall(optionalAuth(handleGetVersionJSON)))
|
Context.mux.HandleFunc("/control/version.json", postInstall(optionalAuth(handleGetVersionJSON)))
|
||||||
httpRegister(http.MethodPost, "/control/update", handleUpdate)
|
httpRegister(http.MethodPost, "/control/update", handleUpdate)
|
||||||
httpRegister(http.MethodGet, "/control/profile", handleGetProfile)
|
httpRegister(http.MethodGet, "/control/profile", handleGetProfile)
|
||||||
|
httpRegister(http.MethodPut, "/control/profile/update", handlePutProfile)
|
||||||
|
|
||||||
// No auth is necessary for DoH/DoT configurations
|
// No auth is necessary for DoH/DoT configurations
|
||||||
Context.mux.HandleFunc("/apple/doh.mobileconfig", postInstall(handleMobileConfigDoH))
|
Context.mux.HandleFunc("/apple/doh.mobileconfig", postInstall(handleMobileConfigDoH))
|
||||||
|
|
|
@ -123,7 +123,7 @@ func handleUpdate(w http.ResponseWriter, r *http.Request) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
err = Context.updater.Update()
|
err = Context.updater.Update(false)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
aghhttp.Error(r, w, http.StatusInternalServerError, "%s", err)
|
aghhttp.Error(r, w, http.StatusInternalServerError, "%s", err)
|
||||||
|
|
||||||
|
|
|
@ -9,7 +9,9 @@ import (
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
|
||||||
"github.com/AdguardTeam/AdGuardHome/internal/aghalg"
|
"github.com/AdguardTeam/AdGuardHome/internal/aghalg"
|
||||||
|
"github.com/AdguardTeam/AdGuardHome/internal/aghhttp"
|
||||||
"github.com/AdguardTeam/AdGuardHome/internal/aghnet"
|
"github.com/AdguardTeam/AdGuardHome/internal/aghnet"
|
||||||
|
"github.com/AdguardTeam/AdGuardHome/internal/dhcpd"
|
||||||
"github.com/AdguardTeam/AdGuardHome/internal/dnsforward"
|
"github.com/AdguardTeam/AdGuardHome/internal/dnsforward"
|
||||||
"github.com/AdguardTeam/AdGuardHome/internal/filtering"
|
"github.com/AdguardTeam/AdGuardHome/internal/filtering"
|
||||||
"github.com/AdguardTeam/AdGuardHome/internal/querylog"
|
"github.com/AdguardTeam/AdGuardHome/internal/querylog"
|
||||||
|
@ -39,17 +41,13 @@ func onConfigModified() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// initDNSServer creates an instance of the dnsforward.Server
|
// initDNS updates all the fields of the [Context] needed to initialize the DNS
|
||||||
// Please note that we must do it even if we don't start it
|
// server and initializes it at last. It also must not be called unless
|
||||||
// so that we had access to the query log and the stats
|
// [config] and [Context] are initialized.
|
||||||
func initDNSServer() (err error) {
|
func initDNS() (err error) {
|
||||||
baseDir := Context.getDataDir()
|
baseDir := Context.getDataDir()
|
||||||
|
|
||||||
var anonFunc aghnet.IPMutFunc
|
anonymizer := config.anonymizer()
|
||||||
if config.DNS.AnonymizeClientIP {
|
|
||||||
anonFunc = querylog.AnonymizeIP
|
|
||||||
}
|
|
||||||
anonymizer := aghnet.NewIPMut(anonFunc)
|
|
||||||
|
|
||||||
statsConf := stats.Config{
|
statsConf := stats.Config{
|
||||||
Filename: filepath.Join(baseDir, "stats.db"),
|
Filename: filepath.Join(baseDir, "stats.db"),
|
||||||
|
@ -82,34 +80,46 @@ func initDNSServer() (err error) {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
var privateNets netutil.SubnetSet
|
tlsConf := &tlsConfigSettings{}
|
||||||
switch len(config.DNS.PrivateNets) {
|
Context.tls.WriteDiskConfig(tlsConf)
|
||||||
case 0:
|
|
||||||
// Use an optimized locally-served matcher.
|
return initDNSServer(
|
||||||
privateNets = netutil.SubnetSetFunc(netutil.IsLocallyServed)
|
Context.filters,
|
||||||
case 1:
|
Context.stats,
|
||||||
privateNets, err = netutil.ParseSubnet(config.DNS.PrivateNets[0])
|
Context.queryLog,
|
||||||
if err != nil {
|
Context.dhcpServer,
|
||||||
return fmt.Errorf("preparing the set of private subnets: %w", err)
|
anonymizer,
|
||||||
}
|
httpRegister,
|
||||||
default:
|
tlsConf,
|
||||||
var nets []*net.IPNet
|
)
|
||||||
nets, err = netutil.ParseSubnets(config.DNS.PrivateNets...)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("preparing the set of private subnets: %w", err)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
privateNets = netutil.SliceSubnetSet(nets)
|
// initDNSServer initializes the [context.dnsServer]. To only use the internal
|
||||||
|
// proxy, none of the arguments are required, but tlsConf still must not be nil,
|
||||||
|
// in other cases all the arguments also must not be nil. It also must not be
|
||||||
|
// called unless [config] and [Context] are initialized.
|
||||||
|
func initDNSServer(
|
||||||
|
filters *filtering.DNSFilter,
|
||||||
|
sts stats.Interface,
|
||||||
|
qlog querylog.QueryLog,
|
||||||
|
dhcpSrv dhcpd.Interface,
|
||||||
|
anonymizer *aghnet.IPMut,
|
||||||
|
httpReg aghhttp.RegisterFunc,
|
||||||
|
tlsConf *tlsConfigSettings,
|
||||||
|
) (err error) {
|
||||||
|
privateNets, err := parseSubnetSet(config.DNS.PrivateNets)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("preparing set of private subnets: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
p := dnsforward.DNSCreateParams{
|
p := dnsforward.DNSCreateParams{
|
||||||
DNSFilter: Context.filters,
|
DNSFilter: filters,
|
||||||
Stats: Context.stats,
|
Stats: sts,
|
||||||
QueryLog: Context.queryLog,
|
QueryLog: qlog,
|
||||||
PrivateNets: privateNets,
|
PrivateNets: privateNets,
|
||||||
Anonymizer: anonymizer,
|
Anonymizer: anonymizer,
|
||||||
LocalDomain: config.DHCP.LocalDomainName,
|
LocalDomain: config.DHCP.LocalDomainName,
|
||||||
DHCPServer: Context.dhcpServer,
|
DHCPServer: dhcpSrv,
|
||||||
}
|
}
|
||||||
|
|
||||||
Context.dnsServer, err = dnsforward.NewServer(p)
|
Context.dnsServer, err = dnsforward.NewServer(p)
|
||||||
|
@ -120,15 +130,15 @@ func initDNSServer() (err error) {
|
||||||
}
|
}
|
||||||
|
|
||||||
Context.clients.dnsServer = Context.dnsServer
|
Context.clients.dnsServer = Context.dnsServer
|
||||||
var dnsConfig dnsforward.ServerConfig
|
|
||||||
dnsConfig, err = generateServerConfig()
|
dnsConf, err := generateServerConfig(tlsConf, httpReg)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
closeDNSServer()
|
closeDNSServer()
|
||||||
|
|
||||||
return fmt.Errorf("generateServerConfig: %w", err)
|
return fmt.Errorf("generateServerConfig: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
err = Context.dnsServer.Prepare(&dnsConfig)
|
err = Context.dnsServer.Prepare(&dnsConf)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
closeDNSServer()
|
closeDNSServer()
|
||||||
|
|
||||||
|
@ -146,6 +156,32 @@ func initDNSServer() (err error) {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// parseSubnetSet parses a slice of subnets. If the slice is empty, it returns
|
||||||
|
// a subnet set that matches all locally served networks, see
|
||||||
|
// [netutil.IsLocallyServed].
|
||||||
|
func parseSubnetSet(nets []string) (s netutil.SubnetSet, err error) {
|
||||||
|
switch len(nets) {
|
||||||
|
case 0:
|
||||||
|
// Use an optimized function-based matcher.
|
||||||
|
return netutil.SubnetSetFunc(netutil.IsLocallyServed), nil
|
||||||
|
case 1:
|
||||||
|
s, err = netutil.ParseSubnet(nets[0])
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return s, nil
|
||||||
|
default:
|
||||||
|
var nets []*net.IPNet
|
||||||
|
nets, err = netutil.ParseSubnets(config.DNS.PrivateNets...)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return netutil.SliceSubnetSet(nets), nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func isRunning() bool {
|
func isRunning() bool {
|
||||||
return Context.dnsServer != nil && Context.dnsServer.IsRunning()
|
return Context.dnsServer != nil && Context.dnsServer.IsRunning()
|
||||||
}
|
}
|
||||||
|
@ -193,7 +229,10 @@ func ipsToUDPAddrs(ips []netip.Addr, port int) (udpAddrs []*net.UDPAddr) {
|
||||||
return udpAddrs
|
return udpAddrs
|
||||||
}
|
}
|
||||||
|
|
||||||
func generateServerConfig() (newConf dnsforward.ServerConfig, err error) {
|
func generateServerConfig(
|
||||||
|
tlsConf *tlsConfigSettings,
|
||||||
|
httpReg aghhttp.RegisterFunc,
|
||||||
|
) (newConf dnsforward.ServerConfig, err error) {
|
||||||
dnsConf := config.DNS
|
dnsConf := config.DNS
|
||||||
hosts := aghalg.CoalesceSlice(dnsConf.BindHosts, []netip.Addr{netutil.IPv4Localhost()})
|
hosts := aghalg.CoalesceSlice(dnsConf.BindHosts, []netip.Addr{netutil.IPv4Localhost()})
|
||||||
newConf = dnsforward.ServerConfig{
|
newConf = dnsforward.ServerConfig{
|
||||||
|
@ -201,12 +240,10 @@ func generateServerConfig() (newConf dnsforward.ServerConfig, err error) {
|
||||||
TCPListenAddrs: ipsToTCPAddrs(hosts, dnsConf.Port),
|
TCPListenAddrs: ipsToTCPAddrs(hosts, dnsConf.Port),
|
||||||
FilteringConfig: dnsConf.FilteringConfig,
|
FilteringConfig: dnsConf.FilteringConfig,
|
||||||
ConfigModified: onConfigModified,
|
ConfigModified: onConfigModified,
|
||||||
HTTPRegister: httpRegister,
|
HTTPRegister: httpReg,
|
||||||
OnDNSRequest: onDNSRequest,
|
OnDNSRequest: onDNSRequest,
|
||||||
}
|
}
|
||||||
|
|
||||||
tlsConf := tlsConfigSettings{}
|
|
||||||
Context.tls.WriteDiskConfig(&tlsConf)
|
|
||||||
if tlsConf.Enabled {
|
if tlsConf.Enabled {
|
||||||
newConf.TLSConfig = tlsConf.TLSConfig
|
newConf.TLSConfig = tlsConf.TLSConfig
|
||||||
newConf.TLSConfig.ServerName = tlsConf.ServerName
|
newConf.TLSConfig.ServerName = tlsConf.ServerName
|
||||||
|
@ -224,7 +261,7 @@ func generateServerConfig() (newConf dnsforward.ServerConfig, err error) {
|
||||||
}
|
}
|
||||||
|
|
||||||
if tlsConf.PortDNSCrypt != 0 {
|
if tlsConf.PortDNSCrypt != 0 {
|
||||||
newConf.DNSCryptConfig, err = newDNSCrypt(hosts, tlsConf)
|
newConf.DNSCryptConfig, err = newDNSCrypt(hosts, *tlsConf)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
// Don't wrap the error, because it's already
|
// Don't wrap the error, because it's already
|
||||||
// wrapped by newDNSCrypt.
|
// wrapped by newDNSCrypt.
|
||||||
|
@ -413,7 +450,11 @@ func startDNSServer() error {
|
||||||
|
|
||||||
func reconfigureDNSServer() (err error) {
|
func reconfigureDNSServer() (err error) {
|
||||||
var newConf dnsforward.ServerConfig
|
var newConf dnsforward.ServerConfig
|
||||||
newConf, err = generateServerConfig()
|
|
||||||
|
tlsConf := &tlsConfigSettings{}
|
||||||
|
Context.tls.WriteDiskConfig(tlsConf)
|
||||||
|
|
||||||
|
newConf, err = generateServerConfig(tlsConf, httpRegister)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("generating forwarding dns server config: %w", err)
|
return fmt.Errorf("generating forwarding dns server config: %w", err)
|
||||||
}
|
}
|
||||||
|
|
|
@ -455,6 +455,10 @@ func run(opts options, clientBuildFS fs.FS) {
|
||||||
err = setupConfig(opts)
|
err = setupConfig(opts)
|
||||||
fatalOnError(err)
|
fatalOnError(err)
|
||||||
|
|
||||||
|
// TODO(e.burkov): This could be made earlier, probably as the option's
|
||||||
|
// effect.
|
||||||
|
cmdlineUpdate(opts)
|
||||||
|
|
||||||
if !Context.firstRun {
|
if !Context.firstRun {
|
||||||
// Save the updated config
|
// Save the updated config
|
||||||
err = config.write()
|
err = config.write()
|
||||||
|
@ -522,7 +526,7 @@ func run(opts options, clientBuildFS fs.FS) {
|
||||||
fatalOnError(err)
|
fatalOnError(err)
|
||||||
|
|
||||||
if !Context.firstRun {
|
if !Context.firstRun {
|
||||||
err = initDNSServer()
|
err = initDNS()
|
||||||
fatalOnError(err)
|
fatalOnError(err)
|
||||||
|
|
||||||
Context.tls.start()
|
Context.tls.start()
|
||||||
|
@ -543,20 +547,24 @@ func run(opts options, clientBuildFS fs.FS) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO(a.garipov): This could be made much earlier and could be done on
|
|
||||||
// the first run as well, but to achieve this we need to bypass requests
|
|
||||||
// over dnsforward resolver.
|
|
||||||
cmdlineUpdate(opts)
|
|
||||||
|
|
||||||
Context.web.Start()
|
Context.web.Start()
|
||||||
|
|
||||||
// wait indefinitely for other go-routines to complete their job
|
// wait indefinitely for other go-routines to complete their job
|
||||||
select {}
|
select {}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (c *configuration) anonymizer() (ipmut *aghnet.IPMut) {
|
||||||
|
var anonFunc aghnet.IPMutFunc
|
||||||
|
if c.DNS.AnonymizeClientIP {
|
||||||
|
anonFunc = querylog.AnonymizeIP
|
||||||
|
}
|
||||||
|
|
||||||
|
return aghnet.NewIPMut(anonFunc)
|
||||||
|
}
|
||||||
|
|
||||||
// startMods initializes and starts the DNS server after installation.
|
// startMods initializes and starts the DNS server after installation.
|
||||||
func startMods() error {
|
func startMods() (err error) {
|
||||||
err := initDNSServer()
|
err = initDNS()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
@ -927,8 +935,8 @@ func getHTTPProxy(_ *http.Request) (*url.URL, error) {
|
||||||
|
|
||||||
// jsonError is a generic JSON error response.
|
// jsonError is a generic JSON error response.
|
||||||
//
|
//
|
||||||
// TODO(a.garipov): Merge together with the implementations in .../dhcpd and
|
// TODO(a.garipov): Merge together with the implementations in [dhcpd] and other
|
||||||
// other packages after refactoring the web handler registering.
|
// packages after refactoring the web handler registering.
|
||||||
type jsonError struct {
|
type jsonError struct {
|
||||||
// Message is the error message, an opaque string.
|
// Message is the error message, an opaque string.
|
||||||
Message string `json:"message"`
|
Message string `json:"message"`
|
||||||
|
@ -940,30 +948,40 @@ func cmdlineUpdate(opts options) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Info("starting update")
|
// Initialize the DNS server to use the internal resolver which the updater
|
||||||
|
// needs to be able to resolve the update source hostname.
|
||||||
|
//
|
||||||
|
// TODO(e.burkov): We could probably initialize the internal resolver
|
||||||
|
// separately.
|
||||||
|
err := initDNSServer(nil, nil, nil, nil, nil, nil, &tlsConfigSettings{})
|
||||||
|
fatalOnError(err)
|
||||||
|
|
||||||
if Context.firstRun {
|
log.Info("cmdline update: performing update")
|
||||||
log.Info("update not allowed on first run")
|
|
||||||
|
|
||||||
os.Exit(0)
|
updater := Context.updater
|
||||||
}
|
info, err := updater.VersionInfo(true)
|
||||||
|
|
||||||
_, err := Context.updater.VersionInfo(true)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
vcu := Context.updater.VersionCheckURL()
|
vcu := updater.VersionCheckURL()
|
||||||
log.Error("getting version info from %s: %s", vcu, err)
|
log.Error("getting version info from %s: %s", vcu, err)
|
||||||
|
|
||||||
os.Exit(0)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
if Context.updater.NewVersion() == "" {
|
if info.NewVersion == version.Version() {
|
||||||
log.Info("no updates available")
|
log.Info("no updates available")
|
||||||
|
|
||||||
os.Exit(0)
|
os.Exit(0)
|
||||||
}
|
}
|
||||||
|
|
||||||
err = Context.updater.Update()
|
err = updater.Update(Context.firstRun)
|
||||||
fatalOnError(err)
|
fatalOnError(err)
|
||||||
|
|
||||||
|
err = restartService()
|
||||||
|
if err != nil {
|
||||||
|
log.Debug("restarting service: %s", err)
|
||||||
|
log.Info("AdGuard Home was not installed as a service. " +
|
||||||
|
"Please restart running instances of AdGuardHome manually.")
|
||||||
|
}
|
||||||
|
|
||||||
os.Exit(0)
|
os.Exit(0)
|
||||||
}
|
}
|
||||||
|
|
|
@ -54,6 +54,7 @@ type languageJSON struct {
|
||||||
Language string `json:"language"`
|
Language string `json:"language"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO(d.kolyshev): Deprecated, remove it later.
|
||||||
func handleI18nCurrentLanguage(w http.ResponseWriter, r *http.Request) {
|
func handleI18nCurrentLanguage(w http.ResponseWriter, r *http.Request) {
|
||||||
log.Printf("home: language is %s", config.Language)
|
log.Printf("home: language is %s", config.Language)
|
||||||
|
|
||||||
|
@ -62,6 +63,7 @@ func handleI18nCurrentLanguage(w http.ResponseWriter, r *http.Request) {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO(d.kolyshev): Deprecated, remove it later.
|
||||||
func handleI18nChangeLanguage(w http.ResponseWriter, r *http.Request) {
|
func handleI18nChangeLanguage(w http.ResponseWriter, r *http.Request) {
|
||||||
if aghhttp.WriteTextPlainDeprecated(w, r) {
|
if aghhttp.WriteTextPlainDeprecated(w, r) {
|
||||||
return
|
return
|
||||||
|
|
|
@ -229,7 +229,7 @@ var cmdLineOpts = []cmdLineOpt{{
|
||||||
updateNoValue: func(o options) (options, error) { o.performUpdate = true; return o, nil },
|
updateNoValue: func(o options) (options, error) { o.performUpdate = true; return o, nil },
|
||||||
effect: nil,
|
effect: nil,
|
||||||
serialize: func(o options) (val string, ok bool) { return "", o.performUpdate },
|
serialize: func(o options) (val string, ok bool) { return "", o.performUpdate },
|
||||||
description: "Update application and exit.",
|
description: "Update the current binary and restart the service in case it's installed.",
|
||||||
longName: "update",
|
longName: "update",
|
||||||
shortName: "",
|
shortName: "",
|
||||||
}, {
|
}, {
|
||||||
|
|
|
@ -0,0 +1,102 @@
|
||||||
|
package home
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/AdguardTeam/AdGuardHome/internal/aghhttp"
|
||||||
|
"github.com/AdguardTeam/golibs/log"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Theme is an enum of all allowed UI themes.
|
||||||
|
type Theme string
|
||||||
|
|
||||||
|
// Allowed [Theme] values.
|
||||||
|
//
|
||||||
|
// Keep in sync with client/src/helpers/constants.js.
|
||||||
|
const (
|
||||||
|
ThemeAuto Theme = "auto"
|
||||||
|
ThemeLight Theme = "light"
|
||||||
|
ThemeDark Theme = "dark"
|
||||||
|
)
|
||||||
|
|
||||||
|
// UnmarshalText implements [encoding.TextUnmarshaler] interface for *Theme.
|
||||||
|
func (t *Theme) UnmarshalText(b []byte) (err error) {
|
||||||
|
switch string(b) {
|
||||||
|
case "auto":
|
||||||
|
*t = ThemeAuto
|
||||||
|
case "dark":
|
||||||
|
*t = ThemeDark
|
||||||
|
case "light":
|
||||||
|
*t = ThemeLight
|
||||||
|
default:
|
||||||
|
return fmt.Errorf("invalid theme %q, supported: %q, %q, %q", b, ThemeAuto, ThemeDark, ThemeLight)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// profileJSON is an object for /control/profile and /control/profile/update
|
||||||
|
// endpoints.
|
||||||
|
type profileJSON struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
Language string `json:"language"`
|
||||||
|
Theme Theme `json:"theme"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// handleGetProfile is the handler for GET /control/profile endpoint.
|
||||||
|
func handleGetProfile(w http.ResponseWriter, r *http.Request) {
|
||||||
|
u := Context.auth.getCurrentUser(r)
|
||||||
|
|
||||||
|
var resp profileJSON
|
||||||
|
func() {
|
||||||
|
config.RLock()
|
||||||
|
defer config.RUnlock()
|
||||||
|
|
||||||
|
resp = profileJSON{
|
||||||
|
Name: u.Name,
|
||||||
|
Language: config.Language,
|
||||||
|
Theme: config.Theme,
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
_ = aghhttp.WriteJSONResponse(w, r, resp)
|
||||||
|
}
|
||||||
|
|
||||||
|
// handlePutProfile is the handler for PUT /control/profile/update endpoint.
|
||||||
|
func handlePutProfile(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if aghhttp.WriteTextPlainDeprecated(w, r) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
profileReq := &profileJSON{}
|
||||||
|
err := json.NewDecoder(r.Body).Decode(profileReq)
|
||||||
|
if err != nil {
|
||||||
|
aghhttp.Error(r, w, http.StatusBadRequest, "reading req: %s", err)
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
lang := profileReq.Language
|
||||||
|
if !allowedLanguages.Has(lang) {
|
||||||
|
aghhttp.Error(r, w, http.StatusBadRequest, "unknown language: %q", lang)
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
theme := profileReq.Theme
|
||||||
|
|
||||||
|
func() {
|
||||||
|
config.Lock()
|
||||||
|
defer config.Unlock()
|
||||||
|
|
||||||
|
config.Language = lang
|
||||||
|
config.Theme = theme
|
||||||
|
log.Printf("home: language is set to %s", lang)
|
||||||
|
log.Printf("home: theme is set to %s", theme)
|
||||||
|
}()
|
||||||
|
|
||||||
|
onConfigModified()
|
||||||
|
aghhttp.OK(w)
|
||||||
|
}
|
|
@ -159,6 +159,38 @@ func sendSigReload() {
|
||||||
log.Debug("service: sent signal to pid %d", pid)
|
log.Debug("service: sent signal to pid %d", pid)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// restartService restarts the service. It returns error if the service is not
|
||||||
|
// running.
|
||||||
|
func restartService() (err error) {
|
||||||
|
// Call chooseSystem explicitly to introduce OpenBSD support for service
|
||||||
|
// package. It's a noop for other GOOS values.
|
||||||
|
chooseSystem()
|
||||||
|
|
||||||
|
pwd, err := os.Getwd()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("getting current directory: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
svcConfig := &service.Config{
|
||||||
|
Name: serviceName,
|
||||||
|
DisplayName: serviceDisplayName,
|
||||||
|
Description: serviceDescription,
|
||||||
|
WorkingDirectory: pwd,
|
||||||
|
}
|
||||||
|
configureService(svcConfig)
|
||||||
|
|
||||||
|
var s service.Service
|
||||||
|
if s, err = service.New(&program{}, svcConfig); err != nil {
|
||||||
|
return fmt.Errorf("initializing service: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err = svcAction(s, "restart"); err != nil {
|
||||||
|
return fmt.Errorf("restarting service: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
// handleServiceControlAction one of the possible control actions:
|
// handleServiceControlAction one of the possible control actions:
|
||||||
//
|
//
|
||||||
// - install: Installs a service/daemon.
|
// - install: Installs a service/daemon.
|
||||||
|
|
|
@ -7,6 +7,8 @@ import (
|
||||||
"github.com/kardianos/service"
|
"github.com/kardianos/service"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// chooseSystem checks the current system detected and substitutes it with local
|
||||||
|
// implementation if needed.
|
||||||
func chooseSystem() {
|
func chooseSystem() {
|
||||||
sys := service.ChosenSystem()
|
sys := service.ChosenSystem()
|
||||||
// By default, package service uses the SysV system if it cannot detect
|
// By default, package service uses the SysV system if it cannot detect
|
||||||
|
|
|
@ -30,6 +30,8 @@ import (
|
||||||
// sysVersion is the version of local service.System interface implementation.
|
// sysVersion is the version of local service.System interface implementation.
|
||||||
const sysVersion = "openbsd-runcom"
|
const sysVersion = "openbsd-runcom"
|
||||||
|
|
||||||
|
// chooseSystem checks the current system detected and substitutes it with local
|
||||||
|
// implementation if needed.
|
||||||
func chooseSystem() {
|
func chooseSystem() {
|
||||||
service.ChooseSystem(openbsdSystem{})
|
service.ChooseSystem(openbsdSystem{})
|
||||||
}
|
}
|
||||||
|
|
|
@ -180,7 +180,7 @@ func withRecovered(orig *error) {
|
||||||
// type check
|
// type check
|
||||||
var _ Interface = (*StatsCtx)(nil)
|
var _ Interface = (*StatsCtx)(nil)
|
||||||
|
|
||||||
// Start implements the Interface interface for *StatsCtx.
|
// Start implements the [Interface] interface for *StatsCtx.
|
||||||
func (s *StatsCtx) Start() {
|
func (s *StatsCtx) Start() {
|
||||||
s.initWeb()
|
s.initWeb()
|
||||||
|
|
||||||
|
|
|
@ -61,7 +61,7 @@ func (u *Updater) VersionInfo(forceRecheck bool) (vi VersionInfo, err error) {
|
||||||
return VersionInfo{}, fmt.Errorf("updater: HTTP GET %s: %w", vcu, err)
|
return VersionInfo{}, fmt.Errorf("updater: HTTP GET %s: %w", vcu, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
u.prevCheckTime = time.Now()
|
u.prevCheckTime = now
|
||||||
u.prevCheckResult, u.prevCheckError = u.parseVersionResponse(body)
|
u.prevCheckResult, u.prevCheckError = u.parseVersionResponse(body)
|
||||||
|
|
||||||
return u.prevCheckResult, u.prevCheckError
|
return u.prevCheckResult, u.prevCheckError
|
||||||
|
@ -92,7 +92,11 @@ func (u *Updater) parseVersionResponse(data []byte) (VersionInfo, error) {
|
||||||
info.AnnouncementURL = versionJSON["announcement_url"]
|
info.AnnouncementURL = versionJSON["announcement_url"]
|
||||||
|
|
||||||
packageURL, ok := u.downloadURL(versionJSON)
|
packageURL, ok := u.downloadURL(versionJSON)
|
||||||
info.CanAutoUpdate = aghalg.BoolToNullBool(ok && info.NewVersion != u.version)
|
if !ok {
|
||||||
|
return info, fmt.Errorf("version.json: packageURL not found")
|
||||||
|
}
|
||||||
|
|
||||||
|
info.CanAutoUpdate = aghalg.BoolToNullBool(info.NewVersion != u.version)
|
||||||
|
|
||||||
u.newVersion = info.NewVersion
|
u.newVersion = info.NewVersion
|
||||||
u.packageURL = packageURL
|
u.packageURL = packageURL
|
||||||
|
|
|
@ -104,49 +104,58 @@ func NewUpdater(conf *Config) *Updater {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update performs the auto-update.
|
// Update performs the auto-update. It returns an error if the update failed.
|
||||||
func (u *Updater) Update() (err error) {
|
// If firstRun is true, it assumes the configuration file doesn't exist.
|
||||||
|
func (u *Updater) Update(firstRun bool) (err error) {
|
||||||
u.mu.Lock()
|
u.mu.Lock()
|
||||||
defer u.mu.Unlock()
|
defer u.mu.Unlock()
|
||||||
|
|
||||||
log.Info("updater: updating")
|
log.Info("updater: updating")
|
||||||
defer func() { log.Info("updater: finished; errors: %v", err) }()
|
defer func() {
|
||||||
|
if err != nil {
|
||||||
|
log.Error("updater: failed: %v", err)
|
||||||
|
} else {
|
||||||
|
log.Info("updater: finished")
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
execPath, err := os.Executable()
|
execPath, err := os.Executable()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return fmt.Errorf("getting executable path: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
err = u.prepare(execPath)
|
err = u.prepare(execPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return fmt.Errorf("preparing: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
defer u.clean()
|
defer u.clean()
|
||||||
|
|
||||||
err = u.downloadPackageFile(u.packageURL, u.packageName)
|
err = u.downloadPackageFile()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return fmt.Errorf("downloading package file: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
err = u.unpack()
|
err = u.unpack()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return fmt.Errorf("unpacking: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if !firstRun {
|
||||||
err = u.check()
|
err = u.check()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return fmt.Errorf("checking config: %w", err)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
err = u.backup()
|
err = u.backup(firstRun)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return fmt.Errorf("making backup: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
err = u.replace()
|
err = u.replace()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return fmt.Errorf("replacing: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
|
@ -174,7 +183,7 @@ func (u *Updater) prepare(exePath string) (err error) {
|
||||||
|
|
||||||
_, pkgNameOnly := filepath.Split(u.packageURL)
|
_, pkgNameOnly := filepath.Split(u.packageURL)
|
||||||
if pkgNameOnly == "" {
|
if pkgNameOnly == "" {
|
||||||
return fmt.Errorf("invalid PackageURL")
|
return fmt.Errorf("invalid PackageURL: %q", u.packageURL)
|
||||||
}
|
}
|
||||||
|
|
||||||
u.packageName = filepath.Join(u.updateDir, pkgNameOnly)
|
u.packageName = filepath.Join(u.updateDir, pkgNameOnly)
|
||||||
|
@ -204,6 +213,7 @@ func (u *Updater) prepare(exePath string) (err error) {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// unpack extracts the files from the downloaded archive.
|
||||||
func (u *Updater) unpack() error {
|
func (u *Updater) unpack() error {
|
||||||
var err error
|
var err error
|
||||||
_, pkgNameOnly := filepath.Split(u.packageURL)
|
_, pkgNameOnly := filepath.Split(u.packageURL)
|
||||||
|
@ -228,38 +238,48 @@ func (u *Updater) unpack() error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// check returns an error if the configuration file couldn't be used with the
|
||||||
|
// version of AdGuard Home just downloaded.
|
||||||
func (u *Updater) check() error {
|
func (u *Updater) check() error {
|
||||||
log.Debug("updater: checking configuration")
|
log.Debug("updater: checking configuration")
|
||||||
|
|
||||||
err := copyFile(u.confName, filepath.Join(u.updateDir, "AdGuardHome.yaml"))
|
err := copyFile(u.confName, filepath.Join(u.updateDir, "AdGuardHome.yaml"))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("copyFile() failed: %w", err)
|
return fmt.Errorf("copyFile() failed: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
cmd := exec.Command(u.updateExeName, "--check-config")
|
cmd := exec.Command(u.updateExeName, "--check-config")
|
||||||
err = cmd.Run()
|
err = cmd.Run()
|
||||||
if err != nil || cmd.ProcessState.ExitCode() != 0 {
|
if err != nil || cmd.ProcessState.ExitCode() != 0 {
|
||||||
return fmt.Errorf("exec.Command(): %s %d", err, cmd.ProcessState.ExitCode())
|
return fmt.Errorf("exec.Command(): %s %d", err, cmd.ProcessState.ExitCode())
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (u *Updater) backup() error {
|
// backup makes a backup of the current configuration and supporting files. It
|
||||||
|
// ignores the configuration file if firstRun is true.
|
||||||
|
func (u *Updater) backup(firstRun bool) (err error) {
|
||||||
log.Debug("updater: backing up current configuration")
|
log.Debug("updater: backing up current configuration")
|
||||||
_ = os.Mkdir(u.backupDir, 0o755)
|
_ = os.Mkdir(u.backupDir, 0o755)
|
||||||
err := copyFile(u.confName, filepath.Join(u.backupDir, "AdGuardHome.yaml"))
|
if !firstRun {
|
||||||
|
err = copyFile(u.confName, filepath.Join(u.backupDir, "AdGuardHome.yaml"))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("copyFile() failed: %w", err)
|
return fmt.Errorf("copyFile() failed: %w", err)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
wd := u.workDir
|
wd := u.workDir
|
||||||
err = copySupportingFiles(u.unpackedFiles, wd, u.backupDir)
|
err = copySupportingFiles(u.unpackedFiles, wd, u.backupDir)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("copySupportingFiles(%s, %s) failed: %s",
|
return fmt.Errorf("copySupportingFiles(%s, %s) failed: %s", wd, u.backupDir, err)
|
||||||
wd, u.backupDir, err)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// replace moves the current executable with the updated one and also copies the
|
||||||
|
// supporting files.
|
||||||
func (u *Updater) replace() error {
|
func (u *Updater) replace() error {
|
||||||
err := copySupportingFiles(u.unpackedFiles, u.updateDir, u.workDir)
|
err := copySupportingFiles(u.unpackedFiles, u.updateDir, u.workDir)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -287,6 +307,7 @@ func (u *Updater) replace() error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// clean removes the temporary directory itself and all it's contents.
|
||||||
func (u *Updater) clean() {
|
func (u *Updater) clean() {
|
||||||
_ = os.RemoveAll(u.updateDir)
|
_ = os.RemoveAll(u.updateDir)
|
||||||
}
|
}
|
||||||
|
@ -297,9 +318,9 @@ func (u *Updater) clean() {
|
||||||
const MaxPackageFileSize = 32 * 1024 * 1024
|
const MaxPackageFileSize = 32 * 1024 * 1024
|
||||||
|
|
||||||
// Download package file and save it to disk
|
// Download package file and save it to disk
|
||||||
func (u *Updater) downloadPackageFile(url, filename string) (err error) {
|
func (u *Updater) downloadPackageFile() (err error) {
|
||||||
var resp *http.Response
|
var resp *http.Response
|
||||||
resp, err = u.client.Get(url)
|
resp, err = u.client.Get(u.packageURL)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("http request failed: %w", err)
|
return fmt.Errorf("http request failed: %w", err)
|
||||||
}
|
}
|
||||||
|
@ -321,7 +342,7 @@ func (u *Updater) downloadPackageFile(url, filename string) (err error) {
|
||||||
_ = os.Mkdir(u.updateDir, 0o755)
|
_ = os.Mkdir(u.updateDir, 0o755)
|
||||||
|
|
||||||
log.Debug("updater: saving package to file")
|
log.Debug("updater: saving package to file")
|
||||||
err = os.WriteFile(filename, body, 0o644)
|
err = os.WriteFile(u.packageName, body, 0o644)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("os.WriteFile() failed: %w", err)
|
return fmt.Errorf("os.WriteFile() failed: %w", err)
|
||||||
}
|
}
|
||||||
|
|
|
@ -136,10 +136,10 @@ func TestUpdate(t *testing.T) {
|
||||||
u.packageURL = fakeURL.String()
|
u.packageURL = fakeURL.String()
|
||||||
|
|
||||||
require.NoError(t, u.prepare(exePath))
|
require.NoError(t, u.prepare(exePath))
|
||||||
require.NoError(t, u.downloadPackageFile(u.packageURL, u.packageName))
|
require.NoError(t, u.downloadPackageFile())
|
||||||
require.NoError(t, u.unpack())
|
require.NoError(t, u.unpack())
|
||||||
// require.NoError(t, u.check())
|
// require.NoError(t, u.check())
|
||||||
require.NoError(t, u.backup())
|
require.NoError(t, u.backup(false))
|
||||||
require.NoError(t, u.replace())
|
require.NoError(t, u.replace())
|
||||||
|
|
||||||
u.clean()
|
u.clean()
|
||||||
|
@ -215,10 +215,10 @@ func TestUpdateWindows(t *testing.T) {
|
||||||
u.packageURL = fakeURL.String()
|
u.packageURL = fakeURL.String()
|
||||||
|
|
||||||
require.NoError(t, u.prepare(exePath))
|
require.NoError(t, u.prepare(exePath))
|
||||||
require.NoError(t, u.downloadPackageFile(u.packageURL, u.packageName))
|
require.NoError(t, u.downloadPackageFile())
|
||||||
require.NoError(t, u.unpack())
|
require.NoError(t, u.unpack())
|
||||||
// assert.Nil(t, u.check())
|
// assert.Nil(t, u.check())
|
||||||
require.NoError(t, u.backup())
|
require.NoError(t, u.backup(false))
|
||||||
require.NoError(t, u.replace())
|
require.NoError(t, u.replace())
|
||||||
|
|
||||||
u.clean()
|
u.clean()
|
||||||
|
|
|
@ -6,6 +6,33 @@
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## v0.107.22: API changes
|
||||||
|
|
||||||
|
### `POST /control/i18n/change_language` is deprecated
|
||||||
|
|
||||||
|
Use `PUT /control/profile/update`.
|
||||||
|
|
||||||
|
### `GET /control/i18n/current_language` is deprecated
|
||||||
|
|
||||||
|
Use `GET /control/profile`.
|
||||||
|
|
||||||
|
* The `/control/profile` HTTP API has been changed.
|
||||||
|
|
||||||
|
* The new `PUT /control/profile/update` HTTP API allows user info updates.
|
||||||
|
|
||||||
|
These `control/profile/update` and `control/profile` APIs accept and return a
|
||||||
|
JSON object with the following format:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"name":"user name",
|
||||||
|
"language": "en",
|
||||||
|
"theme": "auto"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
## v0.107.20: API Changes
|
## v0.107.20: API Changes
|
||||||
|
|
||||||
### `POST /control/cache_clear`
|
### `POST /control/cache_clear`
|
||||||
|
|
|
@ -962,6 +962,9 @@
|
||||||
'description': 'OK.'
|
'description': 'OK.'
|
||||||
'/i18n/change_language':
|
'/i18n/change_language':
|
||||||
'post':
|
'post':
|
||||||
|
'deprecated': true
|
||||||
|
'description': >
|
||||||
|
Deprecated: Use `PUT /control/profile` instead.
|
||||||
'tags':
|
'tags':
|
||||||
- 'i18n'
|
- 'i18n'
|
||||||
'operationId': 'changeLanguage'
|
'operationId': 'changeLanguage'
|
||||||
|
@ -980,6 +983,9 @@
|
||||||
'description': 'OK.'
|
'description': 'OK.'
|
||||||
'/i18n/current_language':
|
'/i18n/current_language':
|
||||||
'get':
|
'get':
|
||||||
|
'deprecated': true
|
||||||
|
'description': >
|
||||||
|
Deprecated: Use `GET /control/profile` instead.
|
||||||
'tags':
|
'tags':
|
||||||
- 'i18n'
|
- 'i18n'
|
||||||
'operationId': 'currentLanguage'
|
'operationId': 'currentLanguage'
|
||||||
|
@ -1145,6 +1151,20 @@
|
||||||
'responses':
|
'responses':
|
||||||
'302':
|
'302':
|
||||||
'description': 'OK.'
|
'description': 'OK.'
|
||||||
|
'/profile/update':
|
||||||
|
'put':
|
||||||
|
'tags':
|
||||||
|
- 'global'
|
||||||
|
'operationId': 'updateProfile'
|
||||||
|
'summary': 'Updates current user info'
|
||||||
|
'requestBody':
|
||||||
|
'content':
|
||||||
|
'application/json':
|
||||||
|
'schema':
|
||||||
|
'$ref': '#/components/schemas/ProfileInfo'
|
||||||
|
'responses':
|
||||||
|
'200':
|
||||||
|
'description': 'OK'
|
||||||
'/profile':
|
'/profile':
|
||||||
'get':
|
'get':
|
||||||
'tags':
|
'tags':
|
||||||
|
@ -2335,6 +2355,19 @@
|
||||||
'properties':
|
'properties':
|
||||||
'name':
|
'name':
|
||||||
'type': 'string'
|
'type': 'string'
|
||||||
|
'language':
|
||||||
|
'type': 'string'
|
||||||
|
'theme':
|
||||||
|
'type': 'string'
|
||||||
|
'description': 'Interface theme'
|
||||||
|
'enum':
|
||||||
|
- 'auto'
|
||||||
|
- 'dark'
|
||||||
|
- 'light'
|
||||||
|
'required':
|
||||||
|
- 'name'
|
||||||
|
- 'language'
|
||||||
|
- 'theme'
|
||||||
'Client':
|
'Client':
|
||||||
'type': 'object'
|
'type': 'object'
|
||||||
'description': 'Client information.'
|
'description': 'Client information.'
|
||||||
|
|
|
@ -85,7 +85,7 @@ in
|
||||||
esac
|
esac
|
||||||
readonly docker_image_full_name docker_tags
|
readonly docker_image_full_name docker_tags
|
||||||
|
|
||||||
# Copy the binaries into a new directory under new names, so that it's eaiser to
|
# Copy the binaries into a new directory under new names, so that it's easier to
|
||||||
# COPY them later. DO NOT remove the trailing underscores. See file
|
# COPY them later. DO NOT remove the trailing underscores. See file
|
||||||
# scripts/make/Dockerfile.
|
# scripts/make/Dockerfile.
|
||||||
dist_docker="${dist_dir}/docker"
|
dist_docker="${dist_dir}/docker"
|
||||||
|
|
|
@ -7,22 +7,18 @@
|
||||||
# Experienced readers may find it overly verbose.
|
# Experienced readers may find it overly verbose.
|
||||||
|
|
||||||
# The default verbosity level is 0. Show log messages if the caller requested
|
# The default verbosity level is 0. Show log messages if the caller requested
|
||||||
# verbosity level greater than 0. Show every command that is run if the
|
# verbosity level greater than 0. Show the environment and every command that
|
||||||
# verbosity level is greater than 1. Show the environment if the verbosity
|
# is run if the verbosity level is greater than 1. Otherwise, print nothing.
|
||||||
# level is greater than 2. Otherwise, print nothing.
|
|
||||||
#
|
#
|
||||||
# The level of verbosity for the build script is the same minus one level. See
|
# The level of verbosity for the build script is the same minus one level. See
|
||||||
# below in build().
|
# below in build().
|
||||||
verbose="${VERBOSE:-0}"
|
verbose="${VERBOSE:-0}"
|
||||||
readonly verbose
|
readonly verbose
|
||||||
|
|
||||||
if [ "$verbose" -gt '2' ]
|
if [ "$verbose" -gt '1' ]
|
||||||
then
|
then
|
||||||
env
|
env
|
||||||
set -x
|
set -x
|
||||||
elif [ "$verbose" -gt '1' ]
|
|
||||||
then
|
|
||||||
set -x
|
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# By default, sign the packages, but allow users to skip that step.
|
# By default, sign the packages, but allow users to skip that step.
|
||||||
|
@ -188,9 +184,6 @@ build() {
|
||||||
#
|
#
|
||||||
# Set GOARM and GOMIPS to an empty string if $build_arm and $build_mips are
|
# Set GOARM and GOMIPS to an empty string if $build_arm and $build_mips are
|
||||||
# the zero value by removing the hyphen as if it's a prefix.
|
# the zero value by removing the hyphen as if it's a prefix.
|
||||||
#
|
|
||||||
# Don't use quotes with $build_par because we want an empty space if
|
|
||||||
# parallelism wasn't set.
|
|
||||||
env\
|
env\
|
||||||
GOARCH="$build_arch"\
|
GOARCH="$build_arch"\
|
||||||
GOARM="${build_arm#-}"\
|
GOARM="${build_arm#-}"\
|
||||||
|
|
Loading…
Reference in New Issue