mirror of
https://github.com/kiddin9/op-packages.git
synced 2026-11-01 08:03:56 +08:00
🌈 Sync 2026-07-29 14:47:30
This commit is contained in:
1 parent
d31b90c3d6
commit
9419456fd2
48 files changed
+13901
-3399
No files matched your search
@@ -12,6 +12,158 @@
|
||||
'require uci';
|
||||
'require ui';
|
||||
|
||||
function pad2(value) {
|
||||
value = (value == null) ? '' : String(value).trim();
|
||||
return value.length === 1 ? '0' + value : value;
|
||||
}
|
||||
|
||||
function normalizeTimePart(value, min, max) {
|
||||
value = parseInt(String(value || '').trim(), 10);
|
||||
if (isNaN(value))
|
||||
value = min;
|
||||
value = Math.max(min, Math.min(max, value));
|
||||
return pad2(value);
|
||||
}
|
||||
|
||||
function parseCron(value) {
|
||||
const matched = String(value || '').trim().match(/^(\d{1,2})\s+(\d{1,2})\s+\*\s+\*\s+([0-7*])$/);
|
||||
if (!matched)
|
||||
return null;
|
||||
|
||||
const minute = +matched[1],
|
||||
hour = +matched[2];
|
||||
if (minute < 0 || minute > 59 || hour < 0 || hour > 23)
|
||||
return null;
|
||||
|
||||
return {
|
||||
minute: normalizeTimePart(matched[1], 0, 59),
|
||||
hour: normalizeTimePart(matched[2], 0, 23),
|
||||
day: matched[3] === '*' ? '*' : (matched[3] === '7' ? '0' : matched[3])
|
||||
};
|
||||
}
|
||||
|
||||
function buildCron(day, hour, minute) {
|
||||
return [
|
||||
normalizeTimePart(minute, 0, 59),
|
||||
normalizeTimePart(hour, 0, 23),
|
||||
'*',
|
||||
'*',
|
||||
String(day || '*')
|
||||
].join(' ');
|
||||
}
|
||||
|
||||
function alignCronEditorRow(wrap) {
|
||||
let field = wrap?.parentNode;
|
||||
|
||||
while (field && !field.classList?.contains('cbi-value-field'))
|
||||
field = field.parentNode;
|
||||
|
||||
let row = field?.parentNode;
|
||||
while (row && !row.classList?.contains('cbi-value'))
|
||||
row = row.parentNode;
|
||||
|
||||
if (!row || !field)
|
||||
return false;
|
||||
|
||||
if (wrap._dailyRow) {
|
||||
wrap._dailyRow.style.display = row.style.display;
|
||||
return true;
|
||||
}
|
||||
|
||||
let dailyRow = E('div', { 'class': 'cbi-value homeproxy-cron-daily-row' }, [
|
||||
E('div', { 'class': 'cbi-value-title' }, _('Update time (daily)')),
|
||||
E('div', { 'class': 'cbi-value-field' }, [ wrap._dailyField ])
|
||||
]);
|
||||
|
||||
wrap._dailyRow = dailyRow;
|
||||
row.parentNode.insertBefore(dailyRow, row.nextSibling);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
function watchCronEditorRow(wrap) {
|
||||
const align = () => alignCronEditorRow(wrap);
|
||||
[ 0, 50, 200, 500, 1000, 2000 ].forEach(delay => window.setTimeout(align, delay));
|
||||
|
||||
if (typeof MutationObserver !== 'undefined' && document?.body) {
|
||||
const observer = new MutationObserver(() => {
|
||||
if (align())
|
||||
observer.disconnect();
|
||||
});
|
||||
|
||||
observer.observe(document.body, { childList: true, subtree: true });
|
||||
window.setTimeout(() => observer.disconnect(), 3000);
|
||||
}
|
||||
}
|
||||
|
||||
function renderCronEditor(input) {
|
||||
if (!input)
|
||||
return null;
|
||||
|
||||
const parsed = parseCron(input.value);
|
||||
input.type = 'text';
|
||||
input.readOnly = true;
|
||||
input.tabIndex = -1;
|
||||
input.style.position = 'absolute';
|
||||
input.style.opacity = '0';
|
||||
input.style.pointerEvents = 'none';
|
||||
input.style.width = '1px';
|
||||
input.style.height = '1px';
|
||||
input.style.minWidth = '1px';
|
||||
input.style.maxWidth = '1px';
|
||||
input.style.padding = '0';
|
||||
input.style.border = '0';
|
||||
|
||||
let day = E('select', { 'class': 'cbi-input-select', 'style': 'width: 12em !important; min-width: 12em !important; max-width: 12em !important; box-sizing: border-box;' }),
|
||||
hour = E('select', { 'class': 'cbi-input-select', 'style': 'width: 4.25em !important; min-width: 4.25em !important; max-width: 4.25em !important; box-sizing: border-box;' }),
|
||||
minute = E('select', { 'class': 'cbi-input-select', 'style': 'width: 4.25em !important; min-width: 4.25em !important; max-width: 4.25em !important; box-sizing: border-box;' }),
|
||||
dailyField = E('div', { 'style': 'display: flex; align-items: center; gap: .25em; width: 9.5em;' }, [
|
||||
hour,
|
||||
E('span', ':'),
|
||||
minute
|
||||
]),
|
||||
wrap = E('div', { 'class': 'homeproxy-cron-editor' }, [ day ]);
|
||||
|
||||
wrap._dailyField = dailyField;
|
||||
|
||||
for (let i = 0; i < 24; i++)
|
||||
hour.appendChild(E('option', { value: pad2(i) }, pad2(i)));
|
||||
|
||||
for (let i = 0; i < 60; i++)
|
||||
minute.appendChild(E('option', { value: pad2(i) }, pad2(i)));
|
||||
|
||||
[
|
||||
['*', _('Every day')],
|
||||
['1', _('Every Monday')],
|
||||
['2', _('Every Tuesday')],
|
||||
['3', _('Every Wednesday')],
|
||||
['4', _('Every Thursday')],
|
||||
['5', _('Every Friday')],
|
||||
['6', _('Every Saturday')],
|
||||
['0', _('Every Sunday')]
|
||||
].forEach(([value, label]) => day.appendChild(E('option', { value }, label)));
|
||||
|
||||
day.value = parsed ? parsed.day : '*';
|
||||
hour.value = parsed ? parsed.hour : '00';
|
||||
minute.value = parsed ? parsed.minute : '00';
|
||||
input.value = buildCron(day.value, hour.value, minute.value);
|
||||
input.setAttribute('value', input.value);
|
||||
|
||||
const sync = () => {
|
||||
input.value = buildCron(day.value, hour.value, minute.value);
|
||||
input.setAttribute('value', input.value);
|
||||
input.dispatchEvent(new Event('input', { bubbles: true }));
|
||||
input.dispatchEvent(new Event('change', { bubbles: true }));
|
||||
};
|
||||
|
||||
day.addEventListener('change', sync);
|
||||
hour.addEventListener('change', sync);
|
||||
minute.addEventListener('change', sync);
|
||||
wrap.appendChild(input);
|
||||
window.setTimeout(() => watchCronEditorRow(wrap), 0);
|
||||
return wrap;
|
||||
}
|
||||
|
||||
return baseclass.extend({
|
||||
dns_strategy: {
|
||||
'': _('Default'),
|
||||
@@ -86,6 +238,29 @@ return baseclass.extend({
|
||||
}
|
||||
}),
|
||||
|
||||
CBIMultiValue: form.MultiValue.extend({
|
||||
__name__: 'CBI.HomeProxyMultiValue',
|
||||
|
||||
renderWidget(section_id, _option_index, cfgvalue) {
|
||||
let value = (cfgvalue != null) ? cfgvalue : this.default,
|
||||
choices = this.transformChoices() || {},
|
||||
widget = new ui.Dropdown(L.toArray(value), choices, {
|
||||
id: this.cbid(section_id),
|
||||
sort: this.keylist,
|
||||
multiple: true,
|
||||
optional: this.optional || this.rmempty,
|
||||
select_placeholder: this.placeholder,
|
||||
create: this.create,
|
||||
display_items: this.display_size ?? this.size ?? 3,
|
||||
dropdown_items: this.dropdown_size ?? this.size ?? -1,
|
||||
validate: this.getValidator(section_id),
|
||||
disabled: (this.readonly != null) ? this.readonly : this.map.readonly
|
||||
});
|
||||
|
||||
return widget.render();
|
||||
}
|
||||
}),
|
||||
|
||||
calcStringMD5(e) {
|
||||
/* Thanks to https://stackoverflow.com/a/41602636 */
|
||||
let h = (a, b) => {
|
||||
@@ -185,6 +360,49 @@ return baseclass.extend({
|
||||
return L.resolveDefault(callGetSingBoxFeatures(), {});
|
||||
},
|
||||
|
||||
renderCronSelector(/* ... */) {
|
||||
if (!this._homeproxyCronOriginalFormvalue) {
|
||||
this._homeproxyCronOriginalFormvalue = this.formvalue;
|
||||
this.formvalue = function(section_id) {
|
||||
let cbid = this.cbid(section_id),
|
||||
ids = [ cbid, 'widget.' + cbid ],
|
||||
roots = [ document.getElementById('modal_overlay'), document ],
|
||||
input = null;
|
||||
|
||||
for (let root of roots) {
|
||||
if (!root)
|
||||
continue;
|
||||
|
||||
for (let el of root.querySelectorAll('input')) {
|
||||
if (ids.includes(el.id) || ids.includes(el.name)) {
|
||||
input = el;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (input)
|
||||
break;
|
||||
}
|
||||
|
||||
let value = input?.value ||
|
||||
this._homeproxyCronOriginalFormvalue?.call(this, section_id) ||
|
||||
this.default ||
|
||||
'0 0 * * *';
|
||||
|
||||
return parseCron(value) ? value : (this.default || '0 0 * * *');
|
||||
};
|
||||
}
|
||||
|
||||
let node = form.Value.prototype.renderWidget.apply(this, arguments),
|
||||
editor = renderCronEditor(node.querySelector('input'));
|
||||
|
||||
return editor || node;
|
||||
},
|
||||
|
||||
renderCronSelectorRow(/* ... */) {
|
||||
return form.Value.prototype.render.apply(this, arguments);
|
||||
},
|
||||
|
||||
generateRand(type, length) {
|
||||
let byteArr;
|
||||
if (['base64', 'hex'].includes(type))
|
||||
@@ -208,6 +426,9 @@ return baseclass.extend({
|
||||
},
|
||||
|
||||
loadDefaultLabel(uciconfig, ucisection) {
|
||||
if (!ucisection)
|
||||
return '';
|
||||
|
||||
let label = uci.get(uciconfig, ucisection, 'label');
|
||||
if (label) {
|
||||
return label;
|
||||
@@ -218,32 +439,127 @@ return baseclass.extend({
|
||||
},
|
||||
|
||||
loadModalTitle(title, addtitle, uciconfig, ucisection) {
|
||||
if (!ucisection)
|
||||
return addtitle;
|
||||
|
||||
let label = uci.get(uciconfig, ucisection, 'label');
|
||||
return label ? title + ' » ' + label : addtitle;
|
||||
},
|
||||
|
||||
normalizeSectionId(label, prefix) {
|
||||
let section_id = (label || '').trim().replace(/[^A-Za-z0-9_]/g, '_').replace(/^_+|_+$/g, '');
|
||||
return (prefix || 'cfg') + '_' + (section_id || 'section');
|
||||
},
|
||||
|
||||
renderSectionAdd(section, extra_class) {
|
||||
let el = form.GridSection.prototype.renderSectionAdd.apply(section, [ extra_class ]),
|
||||
nameEl = el.querySelector('.cbi-section-create-name');
|
||||
ui.addValidator(nameEl, 'uciname', true, (v) => {
|
||||
let button = el.querySelector('.cbi-section-create > .cbi-button-add');
|
||||
let uciconfig = section.uciconfig || section.map.config;
|
||||
nameEl = el.querySelector('.cbi-section-create-name'),
|
||||
button = el.querySelector('.cbi-section-create > .cbi-button-add'),
|
||||
uciconfig = section.uciconfig || section.map.config,
|
||||
sectiontype = section.sectiontype,
|
||||
labelEl = E('input', {
|
||||
'type': 'text',
|
||||
'class': nameEl.className
|
||||
}),
|
||||
hintEl = E('div', { 'class': 'cbi-value-description' });
|
||||
|
||||
if (!v) {
|
||||
nameEl.style.display = 'none';
|
||||
nameEl.parentNode.insertBefore(labelEl, nameEl);
|
||||
nameEl.parentNode.appendChild(hintEl);
|
||||
button.disabled = true;
|
||||
|
||||
const syncSectionName = () => {
|
||||
let label = (labelEl.value || '').trim();
|
||||
hintEl.textContent = '';
|
||||
|
||||
if (!label) {
|
||||
nameEl.value = '';
|
||||
button.disabled = true;
|
||||
return true;
|
||||
} else if (uci.get(uciconfig, v)) {
|
||||
button.disabled = true;
|
||||
return _('Expecting: %s').format(_('unique UCI identifier'));
|
||||
} else {
|
||||
button.disabled = null;
|
||||
return true;
|
||||
return;
|
||||
}
|
||||
}, 'blur', 'keyup');
|
||||
|
||||
let duplicate = false;
|
||||
uci.sections(uciconfig, sectiontype, (res) => {
|
||||
if ((res.label || res['.name']) === label)
|
||||
duplicate = true;
|
||||
});
|
||||
|
||||
if (duplicate) {
|
||||
nameEl.value = '';
|
||||
button.disabled = true;
|
||||
hintEl.textContent = _('Expecting: %s').format(_('unique value'));
|
||||
return;
|
||||
}
|
||||
|
||||
let normalized = (label || '').trim().replace(/[^A-Za-z0-9_]/g, '_').replace(/^_+|_+$/g, '') || 'section',
|
||||
section_id = (sectiontype || 'cfg') + '_' + normalized,
|
||||
suffix = 1;
|
||||
while (uci.get(uciconfig, section_id))
|
||||
section_id = (sectiontype || 'cfg') + '_' + normalized + '_' + suffix++;
|
||||
|
||||
nameEl.value = section_id;
|
||||
nameEl.dataset.sectionId = section_id;
|
||||
nameEl.dataset.sectionLabel = label;
|
||||
button.disabled = null;
|
||||
};
|
||||
|
||||
labelEl.addEventListener('input', syncSectionName);
|
||||
labelEl.addEventListener('blur', syncSectionName);
|
||||
|
||||
button.addEventListener('click', () => {
|
||||
syncSectionName();
|
||||
|
||||
let label = nameEl.dataset.sectionLabel,
|
||||
section_id = nameEl.dataset.sectionId;
|
||||
|
||||
window.setTimeout(() => {
|
||||
if (label && section_id && uci.get(uciconfig, section_id))
|
||||
uci.set(uciconfig, section_id, 'label', label);
|
||||
}, 0);
|
||||
});
|
||||
|
||||
return el;
|
||||
},
|
||||
|
||||
installCloseButtonText() {
|
||||
if (window.__homeproxyCloseButtonText)
|
||||
return;
|
||||
|
||||
window.__homeproxyCloseButtonText = true;
|
||||
|
||||
let fix = (root) => {
|
||||
for (let el of (root || document).querySelectorAll('button, .btn, input[type="button"], input[type="submit"]')) {
|
||||
let text = (el.textContent || el.value || '').trim();
|
||||
if (text !== 'Dismiss' && text !== '\u5ffd\u7565')
|
||||
continue;
|
||||
|
||||
if ('value' in el && !el.textContent.trim())
|
||||
el.value = _('Close');
|
||||
else
|
||||
el.textContent = _('Close');
|
||||
}
|
||||
};
|
||||
|
||||
let observe = () => {
|
||||
if (!document.body)
|
||||
return;
|
||||
|
||||
fix(document);
|
||||
new MutationObserver((mutations) => {
|
||||
for (let mutation of mutations)
|
||||
for (let node of mutation.addedNodes)
|
||||
if (node.nodeType === 1)
|
||||
fix(node);
|
||||
fix(document);
|
||||
}).observe(document.body, { childList: true, subtree: true });
|
||||
};
|
||||
|
||||
if (document.body)
|
||||
observe();
|
||||
else
|
||||
document.addEventListener('DOMContentLoaded', observe, { once: true });
|
||||
},
|
||||
|
||||
uploadCertificate(_option, type, filename, ev) {
|
||||
const callWriteCertificate = rpc.declare({
|
||||
object: 'luci.homeproxy',
|
||||
@@ -264,6 +580,44 @@ return baseclass.extend({
|
||||
.catch((e) => { ui.addNotification(null, E('p', e.message)) });
|
||||
},
|
||||
|
||||
uploadPanel(_option, ev) {
|
||||
const callInstallPanel = rpc.declare({
|
||||
object: 'luci.homeproxy',
|
||||
method: 'clash_api_install_panel',
|
||||
expect: { '': {} }
|
||||
});
|
||||
|
||||
const normalizeMessage = (message) => {
|
||||
switch (message) {
|
||||
case 'panel_backup_failed':
|
||||
return _('Backup panel failed.');
|
||||
case 'panel_restore_failed':
|
||||
return _('Restore panel failed.');
|
||||
case 'empty_panel_zip':
|
||||
return _('Panel ZIP file is empty.');
|
||||
case 'unzip_unavailable':
|
||||
return _('The unzip command is unavailable.');
|
||||
case 'invalid_panel_zip':
|
||||
return _('Invalid panel ZIP file.');
|
||||
case 'panel_install_failed':
|
||||
return _('Install panel failed.');
|
||||
default:
|
||||
return message || _('unknown error');
|
||||
}
|
||||
};
|
||||
|
||||
return ui.uploadFile('/tmp/homeproxy_panel.tmp', ev.target)
|
||||
.then(L.bind((_btn, res) => {
|
||||
return L.resolveDefault(callInstallPanel(), {}).then((ret) => {
|
||||
if (ret.result === true)
|
||||
ui.addNotification(null, E('p', _('Your %s was successfully uploaded. Size: %sB.').format(_('panel ZIP package'), res.size)), 'info');
|
||||
else
|
||||
ui.addNotification(null, E('p', _('Failed to upload %s, error: %s.').format(_('panel ZIP package'), normalizeMessage(ret.error))), 'danger');
|
||||
});
|
||||
}, this, ev.target))
|
||||
.catch((e) => { ui.addNotification(null, E('p', e.message || e), 'danger') });
|
||||
},
|
||||
|
||||
validateBase64Key(length, section_id, value) {
|
||||
/* Thanks to luci-proto-wireguard */
|
||||
if (section_id && value)
|
||||
@@ -304,7 +658,7 @@ return baseclass.extend({
|
||||
if (section_id) {
|
||||
if (!value)
|
||||
return _('Expecting: %s').format(_('non-empty value'));
|
||||
if (ucioption === 'node' && value === 'urltest')
|
||||
if (ucioption === 'node' && (value === 'urltest' || value === 'selector'))
|
||||
return true;
|
||||
|
||||
let duplicate = false;
|
||||
|
||||
File diff suppressed because it is too large.
Load diff
@@ -7,6 +7,7 @@
|
||||
'use strict';
|
||||
'require form';
|
||||
'require fs';
|
||||
'require rpc';
|
||||
'require uci';
|
||||
'require ui';
|
||||
'require view';
|
||||
@@ -14,11 +15,207 @@
|
||||
'require homeproxy as hp';
|
||||
'require tools.widgets as widgets';
|
||||
|
||||
const callNodeLatencyTest = rpc.declare({
|
||||
object: 'luci.homeproxy',
|
||||
method: 'node_latency_test',
|
||||
params: ['node', 'mode'],
|
||||
expect: { '': {} }
|
||||
});
|
||||
|
||||
const callRCInit = rpc.declare({
|
||||
object: 'rc',
|
||||
method: 'init',
|
||||
params: ['name', 'action'],
|
||||
expect: { '': {} }
|
||||
});
|
||||
|
||||
function normalizeList(value) {
|
||||
return Array.isArray(value) ? value : (value ? [value] : []);
|
||||
}
|
||||
|
||||
function getSubscriptionInfo(config) {
|
||||
const urls = normalizeList(uci.get(config, 'subscription', 'subscription_url')),
|
||||
names = normalizeList(uci.get(config, 'subscription', 'subscription_name')),
|
||||
info = [];
|
||||
|
||||
for (let i = 0; i < urls.length; i++) {
|
||||
const url = urls[i],
|
||||
cleanUrl = String(url || '').replace(/#.*$/, ''),
|
||||
title = String(names[i] || '').trim();
|
||||
|
||||
if (!url)
|
||||
continue;
|
||||
|
||||
info.push({
|
||||
hash: hp.calcStringMD5(cleanUrl),
|
||||
title,
|
||||
url
|
||||
});
|
||||
}
|
||||
|
||||
return info;
|
||||
}
|
||||
|
||||
const SubscriptionList = form.Value.extend({
|
||||
__name__: 'CBI.HomeProxySubscriptionList',
|
||||
|
||||
cfgvalue(section_id) {
|
||||
return {
|
||||
urls: normalizeList(uci.get('homeproxy', section_id, 'subscription_url')),
|
||||
names: normalizeList(uci.get('homeproxy', section_id, 'subscription_name'))
|
||||
};
|
||||
},
|
||||
|
||||
renderWidget(section_id, _option_index, cfgvalue) {
|
||||
const value = cfgvalue || { urls: [], names: [] },
|
||||
rows = E('div', {
|
||||
id: this.cbid(section_id),
|
||||
'class': 'homeproxy-subscription-list',
|
||||
'style': 'display: flex; flex-direction: column; gap: .45em; width: min(100%, 42em);'
|
||||
});
|
||||
|
||||
rows.appendChild(E('div', {
|
||||
'class': 'homeproxy-subscription-header',
|
||||
'style': 'display: grid; grid-template-columns: minmax(8em, 13em) minmax(16em, 1fr) auto; gap: .35em; color: var(--text-muted, #6b7280); font-size: 90%;'
|
||||
}, [
|
||||
E('span', {}, [ _('\u8ba2\u9605\u540d\u79f0') ]),
|
||||
E('span', {}, [ _('\u8ba2\u9605\u5730\u5740') ]),
|
||||
E('span')
|
||||
]));
|
||||
|
||||
const addRow = (name, url) => {
|
||||
const row = E('div', {
|
||||
'class': 'homeproxy-subscription-row',
|
||||
'style': 'display: grid; grid-template-columns: minmax(8em, 13em) minmax(16em, 1fr) auto; gap: .35em; align-items: stretch;'
|
||||
}, [
|
||||
E('input', {
|
||||
'type': 'text',
|
||||
'class': 'cbi-input-text subscription-name',
|
||||
'placeholder': _('\u8ba2\u9605\u540d\u79f0'),
|
||||
'value': name || '',
|
||||
'style': 'width: 100%; box-sizing: border-box;'
|
||||
}),
|
||||
E('input', {
|
||||
'type': 'text',
|
||||
'class': 'cbi-input-text subscription-url',
|
||||
'placeholder': _('\u8ba2\u9605\u5730\u5740'),
|
||||
'value': url || '',
|
||||
'style': 'width: 100%; box-sizing: border-box;'
|
||||
}),
|
||||
E('button', {
|
||||
'type': 'button',
|
||||
'class': 'cbi-button cbi-button-remove',
|
||||
'click': () => {
|
||||
row.remove();
|
||||
if (!rows.querySelector('.homeproxy-subscription-row'))
|
||||
addRow('', '');
|
||||
}
|
||||
}, [ '-' ])
|
||||
]);
|
||||
|
||||
if (rows._addButton)
|
||||
rows.insertBefore(row, rows._addButton);
|
||||
else
|
||||
rows.appendChild(row);
|
||||
};
|
||||
|
||||
for (let i = 0; i < Math.max(value.urls.length, 1); i++)
|
||||
addRow(value.names[i], value.urls[i]);
|
||||
|
||||
rows._addButton = E('button', {
|
||||
'type': 'button',
|
||||
'class': 'cbi-button cbi-button-add',
|
||||
'style': 'width: 3em;',
|
||||
'click': () => addRow('', '')
|
||||
}, [ '+' ]);
|
||||
rows.appendChild(rows._addButton);
|
||||
|
||||
return rows;
|
||||
},
|
||||
|
||||
formvalue(section_id) {
|
||||
const node = document.getElementById(this.cbid(section_id)),
|
||||
names = [],
|
||||
urls = [];
|
||||
|
||||
node?.querySelectorAll('.homeproxy-subscription-row').forEach((row) => {
|
||||
const name = row.querySelector('.subscription-name')?.value.trim() || '',
|
||||
url = row.querySelector('.subscription-url')?.value.trim() || '';
|
||||
|
||||
if (!name && !url)
|
||||
return;
|
||||
|
||||
names.push(name);
|
||||
urls.push(url);
|
||||
});
|
||||
|
||||
return { names, urls };
|
||||
},
|
||||
|
||||
validate(_section_id, value) {
|
||||
const names = value?.names || [],
|
||||
urls = value?.urls || [],
|
||||
usedNames = {},
|
||||
usedUrls = {};
|
||||
|
||||
if (!names.length || !urls.length)
|
||||
return _('\u8ba2\u9605\u540d\u79f0\u548c\u8ba2\u9605\u5730\u5740\u4e3a\u5fc5\u586b\u9879');
|
||||
|
||||
for (let i = 0; i < Math.max(names.length, urls.length); i++) {
|
||||
const name = String(names[i] || '').trim(),
|
||||
url = String(urls[i] || '').trim(),
|
||||
normalizedUrl = url.replace(/#.*$/, '');
|
||||
|
||||
if (!name || !url)
|
||||
return _('\u8ba2\u9605\u540d\u79f0\u548c\u8ba2\u9605\u5730\u5740\u5fc5\u987b\u6210\u5bf9\u586b\u5199');
|
||||
|
||||
if (usedNames[name])
|
||||
return _('\u8ba2\u9605\u540d\u79f0\u4e0d\u80fd\u91cd\u590d');
|
||||
usedNames[name] = true;
|
||||
|
||||
if (usedUrls[normalizedUrl])
|
||||
return _('\u8ba2\u9605\u5730\u5740\u4e0d\u80fd\u91cd\u590d');
|
||||
usedUrls[normalizedUrl] = true;
|
||||
|
||||
try {
|
||||
let parsed = new URL(url);
|
||||
if (!parsed.hostname)
|
||||
return _('Expecting: %s').format(_('valid URL'));
|
||||
} catch(e) {
|
||||
return _('Expecting: %s').format(_('valid URL'));
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
},
|
||||
|
||||
write(section_id, value) {
|
||||
if (value?.urls?.length) {
|
||||
uci.set('homeproxy', section_id, 'subscription_url', value.urls);
|
||||
uci.set('homeproxy', section_id, 'subscription_name', value.names);
|
||||
} else {
|
||||
uci.unset('homeproxy', section_id, 'subscription_url');
|
||||
uci.unset('homeproxy', section_id, 'subscription_name');
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
function allowInsecureConfirm(ev, _section_id, value) {
|
||||
if (value === '1' && !confirm(_('Are you sure to allow insecure?')))
|
||||
ev.target.firstElementChild.checked = null;
|
||||
}
|
||||
|
||||
function normalizeHysteriaHoppingPort(mport) {
|
||||
if (!mport)
|
||||
return null;
|
||||
|
||||
const ports = mport.split(',')
|
||||
.map((p) => p.trim())
|
||||
.filter((p) => p.length)
|
||||
.map((p) => (/^\d+$/.test(p) ? `${p}:${p}` : p.replace('-', ':')));
|
||||
return ports.length ? ports : null;
|
||||
}
|
||||
|
||||
function parseShareLink(uri, features) {
|
||||
let config, url, params;
|
||||
|
||||
@@ -29,6 +226,11 @@ function parseShareLink(uri, features) {
|
||||
/* https://github.com/anytls/anytls-go/blob/v0.0.8/docs/uri_scheme.md */
|
||||
url = new URL('http://' + uri[1]);
|
||||
params = url.searchParams;
|
||||
let anytls_fp = params.get('fp');
|
||||
if (['none', 'disable', 'disabled'].includes(anytls_fp))
|
||||
anytls_fp = null;
|
||||
else if (!anytls_fp)
|
||||
anytls_fp = 'chrome';
|
||||
|
||||
/* Check if password exists */
|
||||
if (!url.username)
|
||||
@@ -42,7 +244,8 @@ function parseShareLink(uri, features) {
|
||||
password: url.username ? decodeURIComponent(url.username) : null,
|
||||
tls: '1',
|
||||
tls_sni: params.get('sni'),
|
||||
tls_insecure: (params.get('insecure') === '1') ? '1' : '0'
|
||||
tls_insecure: (params.get('insecure') === '1' || params.get('allowInsecure') === '1') ? '1' : '0',
|
||||
tls_utls: features.with_utls ? anytls_fp : null
|
||||
};
|
||||
|
||||
break;
|
||||
@@ -75,6 +278,7 @@ function parseShareLink(uri, features) {
|
||||
type: 'hysteria',
|
||||
address: url.hostname,
|
||||
port: url.port || '80',
|
||||
hysteria_hopping_port: normalizeHysteriaHoppingPort(params.get('mport')),
|
||||
hysteria_protocol: params.get('protocol') || 'udp',
|
||||
hysteria_auth_type: params.get('auth') ? 'string' : null,
|
||||
hysteria_auth_payload: params.get('auth'),
|
||||
@@ -82,9 +286,9 @@ function parseShareLink(uri, features) {
|
||||
hysteria_down_mbps: params.get('downmbps'),
|
||||
hysteria_up_mbps: params.get('upmbps'),
|
||||
tls: '1',
|
||||
tls_insecure: (params.get('insecure') === '1' || params.get('allowInsecure') === '1') ? '1' : '0',
|
||||
tls_sni: params.get('peer'),
|
||||
tls_alpn: params.get('alpn'),
|
||||
tls_insecure: (params.get('insecure') === '1') ? '1' : '0'
|
||||
tls_alpn: params.get('alpn')
|
||||
};
|
||||
|
||||
break;
|
||||
@@ -105,11 +309,12 @@ function parseShareLink(uri, features) {
|
||||
password: url.username ? (
|
||||
decodeURIComponent(url.username + (url.password ? (':' + url.password) : ''))
|
||||
) : null,
|
||||
hysteria_hopping_port: normalizeHysteriaHoppingPort(params.get('mport')),
|
||||
hysteria_obfs_type: params.get('obfs'),
|
||||
hysteria_obfs_password: params.get('obfs-password'),
|
||||
tls: '1',
|
||||
tls_sni: params.get('sni'),
|
||||
tls_insecure: params.get('insecure') ? '1' : '0'
|
||||
tls_insecure: (params.get('insecure') === '1' || params.get('allowInsecure') === '1') ? '1' : '0',
|
||||
tls_sni: params.get('sni')
|
||||
};
|
||||
|
||||
break;
|
||||
@@ -212,6 +417,7 @@ function parseShareLink(uri, features) {
|
||||
password: decodeURIComponent(url.username),
|
||||
transport: params.get('type') !== 'tcp' ? params.get('type') : null,
|
||||
tls: '1',
|
||||
tls_insecure: (params.get('insecure') === '1' || params.get('allowInsecure') === '1') ? '1' : '0',
|
||||
tls_sni: params.get('sni')
|
||||
};
|
||||
switch (params.get('type')) {
|
||||
@@ -249,6 +455,7 @@ function parseShareLink(uri, features) {
|
||||
tuic_congestion_control: params.get('congestion_control'),
|
||||
tuic_udp_relay_mode: params.get('udp_relay_mode'),
|
||||
tls: '1',
|
||||
tls_insecure: (params.get('insecure') === '1' || params.get('allowInsecure') === '1') ? '1' : '0',
|
||||
tls_sni: params.get('sni'),
|
||||
tls_alpn: params.get('alpn') ? decodeURIComponent(params.get('alpn')).split(',') : null
|
||||
};
|
||||
@@ -276,6 +483,7 @@ function parseShareLink(uri, features) {
|
||||
uuid: url.username,
|
||||
transport: params.get('type') !== 'tcp' ? params.get('type') : null,
|
||||
tls: ['tls', 'xtls', 'reality'].includes(params.get('security')) ? '1' : '0',
|
||||
tls_insecure: (params.get('insecure') === '1' || params.get('allowInsecure') === '1') ? '1' : '0',
|
||||
tls_sni: params.get('sni'),
|
||||
tls_alpn: params.get('alpn') ? decodeURIComponent(params.get('alpn')).split(',') : null,
|
||||
tls_reality: (params.get('security') === 'reality') ? '1' : '0',
|
||||
@@ -388,13 +596,378 @@ function parseShareLink(uri, features) {
|
||||
return config;
|
||||
}
|
||||
|
||||
function renderNodeSettings(section, data, features, main_node, routing_mode) {
|
||||
const NODE_LATENCY_ROW_STATES = Object.freeze({
|
||||
UNTESTED: 'untested',
|
||||
TESTING: 'testing',
|
||||
SUCCESS: 'success',
|
||||
TIMEOUT: 'timeout',
|
||||
ERROR: 'error'
|
||||
});
|
||||
|
||||
const NODE_LATENCY_ROW_STATE_VALUES = Object.freeze(Object.values(NODE_LATENCY_ROW_STATES));
|
||||
const NODE_LATENCY_TEST_MODES = Object.freeze([ 'icmp', 'rp' ]);
|
||||
|
||||
function remapLegacyNodeLatencyTestMode(mode) {
|
||||
return mode === 'tcp' ? 'icmp' : mode;
|
||||
}
|
||||
|
||||
function normalizeNodeLatencyTestMode(mode) {
|
||||
mode = remapLegacyNodeLatencyTestMode(mode);
|
||||
return NODE_LATENCY_TEST_MODES.includes(mode) ? mode : 'icmp';
|
||||
}
|
||||
|
||||
function resolveNodeLatencyResultByMode(result, mode) {
|
||||
let active_mode = normalizeNodeLatencyTestMode(mode || result?.mode);
|
||||
let active_latency_ms = (result && result.latency_ms != null) ? result.latency_ms : null;
|
||||
let active_error = (result && result.error) ? result.error : null;
|
||||
let active_state = result?.state;
|
||||
|
||||
if (active_mode === 'icmp') {
|
||||
if (result && result.icmp_latency_ms != null)
|
||||
active_latency_ms = result.icmp_latency_ms;
|
||||
if (result && result.icmp_error)
|
||||
active_error = result.icmp_error;
|
||||
}
|
||||
|
||||
if (active_error?.code?.endsWith('_timeout'))
|
||||
active_state = NODE_LATENCY_ROW_STATES.TIMEOUT;
|
||||
else if (active_error)
|
||||
active_state = NODE_LATENCY_ROW_STATES.ERROR;
|
||||
else if (active_latency_ms != null)
|
||||
active_state = NODE_LATENCY_ROW_STATES.SUCCESS;
|
||||
|
||||
return {
|
||||
mode: active_mode,
|
||||
state: active_state,
|
||||
latency_ms: active_latency_ms,
|
||||
error: active_error
|
||||
};
|
||||
}
|
||||
|
||||
function getNodeLatencyErrorText(error_code) {
|
||||
switch (error_code) {
|
||||
case 'backend_execution failed':
|
||||
return _('Backend execution failed');
|
||||
case 'icmp_failed':
|
||||
return _('ICMP Failed');
|
||||
case 'icmp_probe_unavailable':
|
||||
return _('ICMP Probe unavailable');
|
||||
case 'icmp_timeout':
|
||||
return _('ICMP Timeout');
|
||||
case 'invalid_node':
|
||||
return _('Invalid node');
|
||||
case 'invalid_response':
|
||||
return _('Invalid response');
|
||||
case 'probe_failed':
|
||||
return _('Probe failed');
|
||||
case 'rpc_failed':
|
||||
return _('RPC failed');
|
||||
default:
|
||||
return _('Error');
|
||||
}
|
||||
}
|
||||
|
||||
function getNodeLatencyStatusText(row_state) {
|
||||
if (!row_state)
|
||||
return _('Not Tested');
|
||||
|
||||
switch (row_state.state) {
|
||||
case NODE_LATENCY_ROW_STATES.TESTING:
|
||||
return _('Testing');
|
||||
case NODE_LATENCY_ROW_STATES.SUCCESS:
|
||||
return (row_state.latency_ms != null) ? _('%s ms').format(row_state.latency_ms) : _('Success');
|
||||
case NODE_LATENCY_ROW_STATES.TIMEOUT:
|
||||
return _('Timeout');
|
||||
case NODE_LATENCY_ROW_STATES.ERROR:
|
||||
return getNodeLatencyErrorText(row_state.error?.code);
|
||||
case NODE_LATENCY_ROW_STATES.UNTESTED:
|
||||
default:
|
||||
return _('Not Tested');
|
||||
}
|
||||
}
|
||||
|
||||
function getNodeLatencyStatusStyle(row_state) {
|
||||
if (!row_state)
|
||||
return 'color:gray';
|
||||
|
||||
switch (row_state.state) {
|
||||
case NODE_LATENCY_ROW_STATES.TESTING:
|
||||
return 'color:#0a84ff';
|
||||
case NODE_LATENCY_ROW_STATES.SUCCESS:
|
||||
return 'color:green';
|
||||
case NODE_LATENCY_ROW_STATES.TIMEOUT:
|
||||
return 'color:#ff8c00';
|
||||
case NODE_LATENCY_ROW_STATES.ERROR:
|
||||
return 'color:red';
|
||||
case NODE_LATENCY_ROW_STATES.UNTESTED:
|
||||
default:
|
||||
return 'color:gray';
|
||||
}
|
||||
}
|
||||
|
||||
function renderNodeLatencyStatus(row_state) {
|
||||
return '<strong style="%s">%s</strong>'.format(getNodeLatencyStatusStyle(row_state), getNodeLatencyStatusText(row_state));
|
||||
}
|
||||
|
||||
function renderNodeLatencyStatusNode(row_state, attrs) {
|
||||
return E('strong', Object.assign({
|
||||
'style': getNodeLatencyStatusStyle(row_state)
|
||||
}, attrs || {}), [ getNodeLatencyStatusText(row_state) ]);
|
||||
}
|
||||
|
||||
function getNodeLatencyActionTitle(row_state) {
|
||||
return (row_state?.state === NODE_LATENCY_ROW_STATES.TESTING) ? _('Testing') : _('Test');
|
||||
}
|
||||
|
||||
function parseNodeLatencySectionId(widget_id) {
|
||||
let matched = String(widget_id || '').match(/^cbi-homeproxy-(.*)-_test_latency$/);
|
||||
return matched ? matched[1] : null;
|
||||
}
|
||||
|
||||
function parseActiveSubscriptionTabId(value) {
|
||||
let matched = String(value || '').match(/(?:^|[^\w])(sub_[0-9a-f]+)(?:$|[^\w])/i);
|
||||
return matched ? matched[1] : null;
|
||||
}
|
||||
|
||||
function parseActiveNodeListTabId(value) {
|
||||
let matched = String(value || '').match(/(?:^|[^\w])(node|subscription|sub_[0-9a-f]+)(?:$|[^\w])/i);
|
||||
return matched ? matched[1].toLowerCase() : null;
|
||||
}
|
||||
|
||||
function getActiveNodeListTabId(root_el) {
|
||||
let active_tab = null;
|
||||
let active_tab_nodes = root_el.querySelectorAll('.cbi-tabmenu li.cbi-tab-active, .cbi-tabmenu li.active, .cbi-tabmenu [aria-selected="true"]');
|
||||
|
||||
for (let node of active_tab_nodes) {
|
||||
let tab_id = parseActiveNodeListTabId(node?.getAttribute('data-tab'))
|
||||
|| parseActiveNodeListTabId(node?.dataset?.tab)
|
||||
|| parseActiveNodeListTabId(node?.getAttribute('id'));
|
||||
if (!tab_id && node?.querySelector)
|
||||
tab_id = parseActiveNodeListTabId(node.querySelector('a')?.getAttribute('href'))
|
||||
|| parseActiveNodeListTabId(node.querySelector('a')?.getAttribute('data-tab'));
|
||||
|
||||
if (tab_id) {
|
||||
active_tab = tab_id;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!active_tab) {
|
||||
let fallback_tab_nodes = root_el.querySelectorAll('.cbi-tabmenu li.cbi-tab[data-tab], .cbi-tabmenu li[data-tab]:not(.cbi-tab-disabled)');
|
||||
|
||||
for (let node of fallback_tab_nodes) {
|
||||
let tab_id = parseActiveNodeListTabId(node?.getAttribute('data-tab')) || parseActiveNodeListTabId(node?.dataset?.tab);
|
||||
if (tab_id) {
|
||||
active_tab = tab_id;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return active_tab;
|
||||
}
|
||||
|
||||
function shouldShowBulkLatencyButton(root_el) {
|
||||
let active_tab = getActiveNodeListTabId(root_el);
|
||||
|
||||
if (!active_tab)
|
||||
return false;
|
||||
|
||||
return !!parseActiveSubscriptionTabId(active_tab);
|
||||
}
|
||||
|
||||
function getActiveSubscriptionTabId(root_el) {
|
||||
let active_tab = getActiveNodeListTabId(root_el);
|
||||
return parseActiveSubscriptionTabId(active_tab);
|
||||
}
|
||||
|
||||
function getActiveSubscriptionTabContainer(root_el, active_tab) {
|
||||
if (!active_tab)
|
||||
return null;
|
||||
|
||||
let candidates = [];
|
||||
for (let selector of [
|
||||
'.cbi-tabcontainer[data-tab="%s"]'.format(active_tab),
|
||||
'[data-tab="%s"]'.format(active_tab),
|
||||
'[id="tab.%s"]'.format(active_tab),
|
||||
'[id="tab-%s"]'.format(active_tab),
|
||||
'[id="%s"]'.format(active_tab)
|
||||
]) {
|
||||
candidates = candidates.concat(Array.from(root_el.querySelectorAll(selector)));
|
||||
}
|
||||
|
||||
return candidates.find((node) => !node.closest('.cbi-tabmenu') && node.querySelector('div[id$="-_test_latency"]')) || null;
|
||||
}
|
||||
|
||||
function getActiveSubscriptionLatencySectionIds(root_el) {
|
||||
let active_tab = getActiveSubscriptionTabId(root_el);
|
||||
let active_container = getActiveSubscriptionTabContainer(root_el, active_tab);
|
||||
if (!active_container)
|
||||
return [];
|
||||
|
||||
let widgets = Array.from(active_container.querySelectorAll('div[id$="-_test_latency"]'));
|
||||
return widgets.map((node) => parseNodeLatencySectionId(node.id)).filter((sid) => !!sid);
|
||||
}
|
||||
|
||||
function createNodeLatencyRowStateModel() {
|
||||
let rows = Object.create(null);
|
||||
|
||||
const buildRowState = (state, result, mode) => {
|
||||
let resolved_result = resolveNodeLatencyResultByMode(result, mode);
|
||||
|
||||
return {
|
||||
state: state,
|
||||
mode: resolved_result.mode,
|
||||
latency_ms: resolved_result.latency_ms,
|
||||
icmp_latency_ms: (result && result.icmp_latency_ms != null) ? result.icmp_latency_ms : null,
|
||||
measured_at: (result && result.measured_at) ? result.measured_at : null,
|
||||
error: resolved_result.error,
|
||||
icmp_error: (result && result.icmp_error) ? result.icmp_error : null
|
||||
};
|
||||
};
|
||||
|
||||
const normalizeState = (state) => {
|
||||
if (NODE_LATENCY_ROW_STATE_VALUES.includes(state))
|
||||
return state;
|
||||
|
||||
return NODE_LATENCY_ROW_STATES.ERROR;
|
||||
};
|
||||
|
||||
const ensure = (section_id) => {
|
||||
if (!section_id)
|
||||
return buildRowState(NODE_LATENCY_ROW_STATES.UNTESTED, null, 'icmp');
|
||||
|
||||
if (!rows[section_id])
|
||||
rows[section_id] = buildRowState(NODE_LATENCY_ROW_STATES.UNTESTED, null, 'icmp');
|
||||
|
||||
return rows[section_id];
|
||||
};
|
||||
|
||||
return {
|
||||
states: NODE_LATENCY_ROW_STATES,
|
||||
|
||||
get: (section_id) => ensure(section_id),
|
||||
|
||||
set: (section_id, state, result, mode) => {
|
||||
if (!section_id)
|
||||
return null;
|
||||
|
||||
rows[section_id] = buildRowState(normalizeState(state), result, mode);
|
||||
return rows[section_id];
|
||||
},
|
||||
|
||||
setFromProbeResult: (section_id, result, mode) => {
|
||||
if (!section_id)
|
||||
return null;
|
||||
|
||||
let resolved_result = resolveNodeLatencyResultByMode(result, mode);
|
||||
let state = resolved_result.state ? normalizeState(resolved_result.state) : NODE_LATENCY_ROW_STATES.ERROR;
|
||||
rows[section_id] = buildRowState(state, result, mode);
|
||||
return rows[section_id];
|
||||
},
|
||||
|
||||
clear: (section_id) => {
|
||||
if (!section_id)
|
||||
return;
|
||||
|
||||
delete rows[section_id];
|
||||
},
|
||||
|
||||
reset: () => {
|
||||
rows = Object.create(null);
|
||||
},
|
||||
|
||||
snapshot: () => rows
|
||||
};
|
||||
}
|
||||
|
||||
function renderNodeSettings(section, data, features, main_node, routing_mode, node_latency_row_state) {
|
||||
let s = section, o;
|
||||
if (typeof globalThis !== 'undefined') {
|
||||
globalThis.__hpNodeLatencySections = globalThis.__hpNodeLatencySections || {};
|
||||
globalThis.__hpNodeLatencyTrigger = function(section_id) {
|
||||
let target = globalThis.__hpNodeLatencySections?.[section_id];
|
||||
return target ? target.handleNodeLatencyTest(section_id) : false;
|
||||
};
|
||||
}
|
||||
s.rowcolors = true;
|
||||
s.sortable = true;
|
||||
s.nodescriptions = true;
|
||||
s.max_cols = 7;
|
||||
s.modaltitle = L.bind(hp.loadModalTitle, this, _('Node'), _('Add a node'), data[0]);
|
||||
s.sectiontitle = L.bind(hp.loadDefaultLabel, this, data[0]);
|
||||
s.node_latency_row_state = node_latency_row_state;
|
||||
s.getNodeLatencyRowState = function(section_id) {
|
||||
return node_latency_row_state.get(section_id);
|
||||
}
|
||||
s.getNodeLatencyTestMode = function() {
|
||||
let mode_option = this.map.lookupOption('latency_test_mode', 'subscription')?.[0];
|
||||
let mode_value = mode_option ? mode_option.formvalue('subscription') : uci.get(data[0], 'subscription', 'latency_test_mode');
|
||||
|
||||
return normalizeNodeLatencyTestMode(mode_value);
|
||||
}
|
||||
s.syncNodeLatencyTestMode = function(mode) {
|
||||
let normalized_mode = normalizeNodeLatencyTestMode(mode);
|
||||
let saved_mode_value = uci.get(data[0], 'subscription', 'latency_test_mode');
|
||||
let saved_mode = normalizeNodeLatencyTestMode(saved_mode_value);
|
||||
|
||||
if (saved_mode === normalized_mode && saved_mode_value !== 'tcp')
|
||||
return Promise.resolve(normalized_mode);
|
||||
|
||||
uci.set(data[0], 'subscription', 'latency_test_mode', normalized_mode);
|
||||
return uci.save().then(() => normalized_mode);
|
||||
}
|
||||
s.setNodeLatencyRowState = function(section_id, state, result, mode) {
|
||||
return node_latency_row_state.set(section_id, state, result, mode);
|
||||
}
|
||||
s.setNodeLatencyRowStateFromProbeResult = function(section_id, result, mode) {
|
||||
return node_latency_row_state.setFromProbeResult(section_id, result, mode);
|
||||
}
|
||||
s.refreshNodeLatencyRow = function(section_id) {
|
||||
let row_state = this.getNodeLatencyRowState(section_id);
|
||||
let latency_id = 'cbi-%s-%s-_latency'.format(data[0], section_id);
|
||||
let test_id = 'cbi-%s-%s-_test_latency'.format(data[0], section_id);
|
||||
|
||||
let status_widget = this.map.findElement('id', latency_id);
|
||||
if (status_widget)
|
||||
status_widget.innerHTML = renderNodeLatencyStatus(row_state);
|
||||
|
||||
let test_widget = this.map.findElement('id', test_id);
|
||||
let test_button = test_widget ? test_widget.querySelector('button') : null;
|
||||
if (test_button) {
|
||||
test_button.textContent = getNodeLatencyActionTitle(row_state);
|
||||
test_button.disabled = (row_state.state === NODE_LATENCY_ROW_STATES.TESTING) ? true : null;
|
||||
}
|
||||
}
|
||||
|
||||
s.handleNodeLatencyTest = function(section_id) {
|
||||
let mode = this.getNodeLatencyTestMode();
|
||||
this.setNodeLatencyRowState(section_id, NODE_LATENCY_ROW_STATES.TESTING, null, mode);
|
||||
this.refreshNodeLatencyRow(section_id);
|
||||
|
||||
return this.syncNodeLatencyTestMode(mode).then((mode) => L.resolveDefault(callNodeLatencyTest(section_id, mode), null).then((result) => {
|
||||
if (result?.node === section_id)
|
||||
this.setNodeLatencyRowStateFromProbeResult(section_id, result, mode);
|
||||
else
|
||||
this.setNodeLatencyRowState(section_id, NODE_LATENCY_ROW_STATES.ERROR, {
|
||||
mode: mode,
|
||||
error: {
|
||||
code: 'invalid_response',
|
||||
message: 'unexpected node latency response'
|
||||
}
|
||||
}, mode);
|
||||
})).catch((err) => {
|
||||
this.setNodeLatencyRowState(section_id, NODE_LATENCY_ROW_STATES.ERROR, {
|
||||
mode: mode,
|
||||
error: {
|
||||
code: 'rpc_failed',
|
||||
message: String(err)
|
||||
}
|
||||
}, mode);
|
||||
}).then(() => {
|
||||
this.refreshNodeLatencyRow(section_id);
|
||||
});
|
||||
}
|
||||
|
||||
if (routing_mode !== 'custom') {
|
||||
o = s.option(form.Button, '_apply', _('Apply'));
|
||||
@@ -419,6 +992,41 @@ function renderNodeSettings(section, data, features, main_node, routing_mode) {
|
||||
}
|
||||
}
|
||||
|
||||
o = s.option(form.DummyValue, '_latency', _('Latency'));
|
||||
o.rawhtml = true;
|
||||
o.modalonly = false;
|
||||
o.textvalue = function(section_id) {
|
||||
return E('span', { 'id': 'cbi-%s-%s-_latency'.format(data[0], section_id) }, [
|
||||
renderNodeLatencyStatusNode(this.section.getNodeLatencyRowState(section_id))
|
||||
]);
|
||||
}
|
||||
|
||||
o = s.option(form.Button, '_test_latency', _('Test'));
|
||||
o.editable = true;
|
||||
o.modalonly = false;
|
||||
o.inputstyle = 'action';
|
||||
o.inputtitle = function(section_id) {
|
||||
return getNodeLatencyActionTitle(this.section.getNodeLatencyRowState(section_id));
|
||||
}
|
||||
o.renderWidget = function(section_id, _option_index, cfgvalue) {
|
||||
let hiddenEl = new ui.Hiddenfield((cfgvalue != null) ? cfgvalue : '', {
|
||||
id: this.cbid(section_id)
|
||||
});
|
||||
let outputEl = E('output', { 'for': this.cbid(section_id) });
|
||||
let row_state = s.getNodeLatencyRowState(section_id);
|
||||
|
||||
if (typeof globalThis !== 'undefined' && globalThis.__hpNodeLatencySections)
|
||||
globalThis.__hpNodeLatencySections[section_id] = s;
|
||||
|
||||
outputEl.appendChild(E('button', {
|
||||
'class': 'cbi-button cbi-button-action',
|
||||
'onclick': 'return globalThis.__hpNodeLatencyTrigger(%s);'.format(JSON.stringify(section_id)),
|
||||
'disabled': (row_state.state === NODE_LATENCY_ROW_STATES.TESTING) ? true : null
|
||||
}, [ getNodeLatencyActionTitle(row_state) ]));
|
||||
|
||||
return E([ outputEl, hiddenEl.render() ]);
|
||||
}
|
||||
|
||||
o = s.option(form.Value, 'label', _('Label'));
|
||||
o.load = L.bind(hp.loadDefaultLabel, this, data[0]);
|
||||
o.validate = L.bind(hp.validateUniqueValue, this, data[0], 'node', 'label');
|
||||
@@ -443,6 +1051,7 @@ function renderNodeSettings(section, data, features, main_node, routing_mode) {
|
||||
o.value('wireguard', _('WireGuard'));
|
||||
o.value('vless', _('VLESS'));
|
||||
o.value('vmess', _('VMess'));
|
||||
o.default = 'direct';
|
||||
o.rmempty = false;
|
||||
|
||||
o = s.option(form.Value, 'address', _('Address'));
|
||||
@@ -493,15 +1102,6 @@ function renderNodeSettings(section, data, features, main_node, routing_mode) {
|
||||
}
|
||||
o.modalonly = true;
|
||||
|
||||
/* Direct config */
|
||||
o = s.option(form.ListValue, 'proxy_protocol', _('Proxy protocol'),
|
||||
_('Write proxy protocol in the connection header.'));
|
||||
o.value('', _('Disable'));
|
||||
o.value('1', _('v1'));
|
||||
o.value('2', _('v2'));
|
||||
o.depends('type', 'direct');
|
||||
o.modalonly = true;
|
||||
|
||||
/* AnyTLS config start */
|
||||
o = s.option(form.Value, 'anytls_idle_session_check_interval', _('Idle session check interval'),
|
||||
_('Interval checking for idle sessions, in seconds.'));
|
||||
@@ -746,7 +1346,7 @@ function renderNodeSettings(section, data, features, main_node, routing_mode) {
|
||||
o.modalonly = true;
|
||||
|
||||
o = s.option(form.ListValue, 'vmess_encrypt', _('Encrypt method'));
|
||||
o.value('auto');
|
||||
o.value('auto', _('Default'));
|
||||
o.value('none');
|
||||
o.value('zero');
|
||||
o.value('aes-128-gcm');
|
||||
@@ -1194,19 +1794,18 @@ return view.extend({
|
||||
},
|
||||
|
||||
render(data) {
|
||||
let m, s, o, ss, so;
|
||||
let m, s, o, ss, so, subscription_list_option;
|
||||
let main_node = uci.get(data[0], 'config', 'main_node');
|
||||
let routing_mode = uci.get(data[0], 'config', 'routing_mode');
|
||||
let features = data[1];
|
||||
let node_latency_row_state = createNodeLatencyRowStateModel();
|
||||
|
||||
hp.installCloseButtonText();
|
||||
|
||||
this.node_latency_row_state = node_latency_row_state;
|
||||
|
||||
/* Cache subscription information, it will be called multiple times */
|
||||
let subinfo = [];
|
||||
for (let suburl of (uci.get(data[0], 'subscription', 'subscription_url') || [])) {
|
||||
const url = new URL(suburl);
|
||||
const urlhash = hp.calcStringMD5(suburl.replace(/#.*$/, ''));
|
||||
const title = url.hash ? decodeURIComponent(url.hash.slice(1)) : url.hostname;
|
||||
subinfo.push({ 'hash': urlhash, 'title': title });
|
||||
}
|
||||
let subinfo = getSubscriptionInfo(data[0]);
|
||||
|
||||
m = new form.Map('homeproxy', _('Edit nodes'));
|
||||
|
||||
@@ -1216,7 +1815,7 @@ return view.extend({
|
||||
/* User nodes start */
|
||||
s.tab('node', _('Nodes'));
|
||||
o = s.taboption('node', form.SectionValue, '_node', form.GridSection, 'node');
|
||||
ss = renderNodeSettings(o.subsection, data, features, main_node, routing_mode);
|
||||
ss = renderNodeSettings(o.subsection, data, features, main_node, routing_mode, node_latency_row_state);
|
||||
ss.addremove = true;
|
||||
ss.filter = function(section_id) {
|
||||
for (let info of subinfo)
|
||||
@@ -1287,24 +1886,7 @@ return view.extend({
|
||||
])
|
||||
}
|
||||
ss.renderSectionAdd = function(/* ... */) {
|
||||
let el = form.GridSection.prototype.renderSectionAdd.apply(this, arguments),
|
||||
nameEl = el.querySelector('.cbi-section-create-name');
|
||||
|
||||
ui.addValidator(nameEl, 'uciname', true, (v) => {
|
||||
let button = el.querySelector('.cbi-section-create > .cbi-button-add');
|
||||
let uciconfig = this.uciconfig || this.map.config;
|
||||
|
||||
if (!v) {
|
||||
button.disabled = true;
|
||||
return true;
|
||||
} else if (uci.get(uciconfig, v)) {
|
||||
button.disabled = true;
|
||||
return _('Expecting: %s').format(_('unique UCI identifier'));
|
||||
} else {
|
||||
button.disabled = null;
|
||||
return true;
|
||||
}
|
||||
}, 'blur', 'keyup');
|
||||
let el = hp.renderSectionAdd(this, arguments[0]);
|
||||
|
||||
el.appendChild(E('button', {
|
||||
'class': 'cbi-button cbi-button-add',
|
||||
@@ -1319,9 +1901,9 @@ return view.extend({
|
||||
|
||||
/* Subscription nodes start */
|
||||
for (const info of subinfo) {
|
||||
s.tab('sub_' + info.hash, _('Sub (%s)').format(info.title));
|
||||
s.tab('sub_' + info.hash, info.title ? _('Sub (%s)').format(info.title) : _('Sub'));
|
||||
o = s.taboption('sub_' + info.hash, form.SectionValue, '_sub_' + info.hash, form.GridSection, 'node');
|
||||
ss = renderNodeSettings(o.subsection, data, features, main_node, routing_mode);
|
||||
ss = renderNodeSettings(o.subsection, data, features, main_node, routing_mode, node_latency_row_state);
|
||||
ss.filter = function(section_id) {
|
||||
return (uci.get(data[0], section_id, 'grouphash') === info.hash);
|
||||
}
|
||||
@@ -1336,32 +1918,35 @@ return view.extend({
|
||||
_('Auto update subscriptions and geodata.'));
|
||||
o.rmempty = false;
|
||||
|
||||
o = s.taboption('subscription', form.ListValue, 'auto_update_time', _('Update time'));
|
||||
for (let i = 0; i < 24; i++)
|
||||
o.value(i, i + ':00');
|
||||
o.default = '2';
|
||||
o.depends('auto_update', '1');
|
||||
o = s.taboption('subscription', form.Value, 'auto_update_time', _('Update time (weekly)'));
|
||||
o.renderWidget = function() {
|
||||
return hp.renderCronSelector.apply(this, arguments);
|
||||
};
|
||||
o.default = '0 0 * * *';
|
||||
o.rmempty = false;
|
||||
o.validate = function(section_id, value) {
|
||||
value = value || this.formvalue(section_id);
|
||||
const matched = String(value || '').trim().match(/^(\d{1,2})\s+(\d{1,2})\s+\*\s+\*\s+([0-7*])$/);
|
||||
if (!matched || +matched[1] > 59 || +matched[2] > 23)
|
||||
return _('Expecting: %s').format(_('valid cron expression'));
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
o = s.taboption('subscription', form.Flag, 'update_via_proxy', _('Update via proxy'),
|
||||
_('Update subscriptions via proxy.'));
|
||||
o.rmempty = false;
|
||||
|
||||
o = s.taboption('subscription', form.DynamicList, 'subscription_url', _('Subscription URL-s'),
|
||||
_('Support Hysteria, Shadowsocks, Trojan, v2rayN (VMess), and XTLS (VLESS) online configuration delivery standard.'));
|
||||
o.validate = function(section_id, value) {
|
||||
if (section_id && value) {
|
||||
try {
|
||||
let url = new URL(value);
|
||||
if (!url.hostname)
|
||||
return _('Expecting: %s').format(_('valid URL'));
|
||||
}
|
||||
catch(e) {
|
||||
return _('Expecting: %s').format(_('valid URL'));
|
||||
}
|
||||
}
|
||||
o = s.taboption('subscription', form.ListValue, 'latency_test_mode', _('Latency test mode'),
|
||||
_('Choose which method node latency tests use.'));
|
||||
o.value('icmp', _('ICMP'));
|
||||
o.value('rp', _('RP'));
|
||||
o.default = 'icmp';
|
||||
o.rmempty = false;
|
||||
|
||||
return true;
|
||||
}
|
||||
o = s.taboption('subscription', SubscriptionList, 'subscription_url', _('Subscriptions'),
|
||||
_('Support Hysteria, Shadowsocks, Trojan, v2rayN (VMess), and XTLS (VLESS) online configuration delivery standard.'));
|
||||
subscription_list_option = o;
|
||||
|
||||
o = s.taboption('subscription', form.ListValue, 'filter_nodes', _('Filter nodes'),
|
||||
_('Drop/keep specific nodes from subscriptions.'));
|
||||
@@ -1396,8 +1981,16 @@ return view.extend({
|
||||
o.inputstyle = 'apply';
|
||||
o.inputtitle = _('Save current settings');
|
||||
o.onclick = function() {
|
||||
const validation = subscription_list_option.validate('subscription', subscription_list_option.formvalue('subscription'));
|
||||
if (validation !== true) {
|
||||
ui.addNotification(null, E('p', validation));
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
return this.map.save(null, true).then(() => {
|
||||
ui.changes.apply(true);
|
||||
return ui.changes.apply(true);
|
||||
}).then(() => {
|
||||
return callRCInit('homeproxy', 'reload');
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1460,6 +2053,68 @@ return view.extend({
|
||||
}
|
||||
/* Subscriptions settings end */
|
||||
|
||||
return m.render();
|
||||
return m.render().then((el) => {
|
||||
let bulk_testing = false;
|
||||
let tabmenu_observer = null;
|
||||
let bulk_button = E('button', {
|
||||
'class': 'cbi-button cbi-button-action hp-bulk-latency-test',
|
||||
'click': ui.createHandlerFn(this, async () => {
|
||||
if (bulk_testing || typeof globalThis === 'undefined' || !globalThis.__hpNodeLatencyTrigger)
|
||||
return false;
|
||||
|
||||
let section_ids = getActiveSubscriptionLatencySectionIds(el);
|
||||
if (!section_ids.length)
|
||||
return false;
|
||||
|
||||
bulk_testing = true;
|
||||
bulk_button.textContent = _('Testing Current List...');
|
||||
bulk_button.disabled = true;
|
||||
|
||||
for (let sid of section_ids)
|
||||
await globalThis.__hpNodeLatencyTrigger(sid);
|
||||
|
||||
bulk_testing = false;
|
||||
bulk_button.textContent = _('Test Current List');
|
||||
bulk_button.disabled = false;
|
||||
return true;
|
||||
})
|
||||
}, [ _('Test Current List') ]);
|
||||
|
||||
let updateBulkButtonVisibility = () => {
|
||||
bulk_button.style.display = shouldShowBulkLatencyButton(el) ? '' : 'none';
|
||||
};
|
||||
|
||||
let attachBulkButton = () => {
|
||||
let actions = (el.parentElement ? el.parentElement.querySelector('.cbi-page-actions') : null) || document.querySelector('.cbi-page-actions');
|
||||
if (!actions)
|
||||
return false;
|
||||
|
||||
if (!actions.querySelector('.hp-bulk-latency-test'))
|
||||
actions.insertBefore(bulk_button, actions.firstChild);
|
||||
|
||||
updateBulkButtonVisibility();
|
||||
|
||||
if (!tabmenu_observer) {
|
||||
let tabmenu = el.querySelector('.cbi-tabmenu');
|
||||
if (tabmenu) {
|
||||
tabmenu_observer = new MutationObserver(() => updateBulkButtonVisibility());
|
||||
tabmenu_observer.observe(tabmenu, {
|
||||
subtree: true,
|
||||
attributes: true,
|
||||
attributeFilter: [ 'class', 'aria-selected' ]
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
if (!attachBulkButton()) {
|
||||
window.setTimeout(attachBulkButton, 0);
|
||||
window.setTimeout(attachBulkButton, 300);
|
||||
}
|
||||
|
||||
return el;
|
||||
});
|
||||
}
|
||||
});
|
||||
@@ -126,6 +126,8 @@ return view.extend({
|
||||
let m, s, o;
|
||||
let features = data[1];
|
||||
|
||||
hp.installCloseButtonText();
|
||||
|
||||
m = new form.Map('homeproxy', _('HomeProxy Server'),
|
||||
_('The modern ImmortalWrt proxy platform for ARM64/AMD64.'));
|
||||
|
||||
@@ -188,6 +190,7 @@ return view.extend({
|
||||
o.value('tuic', _('Tuic'));
|
||||
o.value('vless', _('VLESS'));
|
||||
o.value('vmess', _('VMess'));
|
||||
o.default = 'mixed';
|
||||
o.rmempty = false;
|
||||
|
||||
o = s.option(form.Value, 'address', _('Listen address'));
|
||||
@@ -637,10 +640,10 @@ return view.extend({
|
||||
o.modalonly = true;
|
||||
|
||||
o = s.option(form.ListValue, 'tls_dns01_provider', _('DNS provider'));
|
||||
o.value('alidns', _('Alibaba Cloud DNS'));
|
||||
o.value('cloudflare', _('Cloudflare'));
|
||||
o.value('alidns', _('AliYun'));
|
||||
o.value('cfdns', _('CloudFlare'));
|
||||
o.depends('tls_dns01_challenge', '1');
|
||||
o.default = 'cloudflare';
|
||||
o.default = 'cfdns';
|
||||
o.rmempty = false;
|
||||
o.modalonly = true;
|
||||
|
||||
@@ -663,7 +666,7 @@ return view.extend({
|
||||
|
||||
o = s.option(form.Value, 'tls_dns01_cf_api_token', _('API token'));
|
||||
o.password = true;
|
||||
o.depends('tls_dns01_provider', 'cloudflare');
|
||||
o.depends('tls_dns01_provider', 'cfdns');
|
||||
o.rmempty = false;
|
||||
o.modalonly = true;
|
||||
|
||||
|
||||
@@ -14,6 +14,8 @@
|
||||
'require ui';
|
||||
'require view';
|
||||
|
||||
'require homeproxy as hp';
|
||||
|
||||
/* Thanks to luci-app-aria2 */
|
||||
const css = ' \
|
||||
#log_textarea { \
|
||||
@@ -27,6 +29,53 @@ const css = ' \
|
||||
} \
|
||||
.description { \
|
||||
background-color: #33ccff; \
|
||||
} \
|
||||
.homeproxy-connect-grid { \
|
||||
display: grid; \
|
||||
grid-template-columns: repeat(auto-fit, minmax(210px, 1fr));\
|
||||
gap: 12px; \
|
||||
} \
|
||||
.homeproxy-connect-card { \
|
||||
display: grid; \
|
||||
grid-template-columns: 44px 1fr; \
|
||||
gap: 10px; \
|
||||
align-items: center; \
|
||||
min-height: 64px; \
|
||||
padding: 8px 10px; \
|
||||
border-radius: 6px; \
|
||||
background: #fff; \
|
||||
box-shadow: 0 1px 4px rgba(0, 0, 0, .08);\
|
||||
} \
|
||||
.homeproxy-connect-icon { \
|
||||
display: flex; \
|
||||
align-items: center; \
|
||||
justify-content: center; \
|
||||
width: 38px; \
|
||||
height: 38px; \
|
||||
border-radius: 50%; \
|
||||
color: #fff; \
|
||||
font-weight: 700; \
|
||||
font-size: 15px; \
|
||||
} \
|
||||
.homeproxy-connect-info { \
|
||||
min-height: 42px; \
|
||||
padding: 7px 12px; \
|
||||
border-radius: 8px; \
|
||||
background: #e9edf2; \
|
||||
} \
|
||||
.homeproxy-connect-title { \
|
||||
color: #7d8aa0; \
|
||||
font-weight: 700; \
|
||||
font-size: 13px; \
|
||||
line-height: 1.2; \
|
||||
} \
|
||||
.homeproxy-connect-result { \
|
||||
margin-top: 2px; \
|
||||
color: #ff4d35; \
|
||||
font-weight: 700; \
|
||||
font-size: 13px; \
|
||||
cursor: pointer; \
|
||||
line-height: 1.2; \
|
||||
}';
|
||||
|
||||
const hp_dir = '/var/run/homeproxy';
|
||||
@@ -44,13 +93,13 @@ function getConnStat(o, site) {
|
||||
'class': 'btn cbi-button cbi-button-action',
|
||||
'click': ui.createHandlerFn(this, () => {
|
||||
return L.resolveDefault(callConnStat(site), {}).then((ret) => {
|
||||
let ele = o.default.firstElementChild.nextElementSibling;
|
||||
let ele = o.default.firstElementChild.nextElementSibling;
|
||||
if (ret.result) {
|
||||
ele.style.setProperty('color', 'green');
|
||||
ele.innerHTML = _('passed');
|
||||
ele.innerHTML = _('passed');
|
||||
} else {
|
||||
ele.style.setProperty('color', 'red');
|
||||
ele.innerHTML = _('failed');
|
||||
ele.innerHTML = _('failed');
|
||||
}
|
||||
});
|
||||
})
|
||||
@@ -60,6 +109,53 @@ function getConnStat(o, site) {
|
||||
]);
|
||||
}
|
||||
|
||||
function renderConnCards() {
|
||||
const callConnStat = rpc.declare({
|
||||
object: 'luci.homeproxy',
|
||||
method: 'connection_check',
|
||||
params: ['site'],
|
||||
expect: { '': {} }
|
||||
});
|
||||
|
||||
const items = [
|
||||
{ site: 'baidu', title: _('Baidu Connection'), icon: 'B', color: '#ff7a00' },
|
||||
{ site: 'google', title: _('Google Connection'), icon: 'G', color: '#4285f4' },
|
||||
{ site: 'github', title: _('GitHub Connection'), icon: 'GH', color: '#7b3bb3' },
|
||||
{ site: 'youtube', title: _('YouTube Connection'), icon: 'YT', color: '#ff0033' }
|
||||
];
|
||||
|
||||
return E('div', { 'class': 'homeproxy-connect-grid' }, items.map((item) => {
|
||||
let result = E('div', {
|
||||
'class': 'homeproxy-connect-result',
|
||||
'click': ui.createHandlerFn(this, () => {
|
||||
result.textContent = _('Checking...');
|
||||
result.style.setProperty('color', '#f0ad4e');
|
||||
|
||||
return L.resolveDefault(callConnStat(item.site), {}).then((ret) => {
|
||||
if (ret.result && ret.latency != null) {
|
||||
result.style.setProperty('color', 'green');
|
||||
result.textContent = _('%s ms').format(ret.latency);
|
||||
} else {
|
||||
result.style.setProperty('color', '#ff4d35');
|
||||
result.textContent = _('failed');
|
||||
}
|
||||
});
|
||||
})
|
||||
}, [ _('Click to check') ]);
|
||||
|
||||
return E('div', { 'class': 'homeproxy-connect-card' }, [
|
||||
E('div', {
|
||||
'class': 'homeproxy-connect-icon',
|
||||
'style': 'background:%s'.format(item.color)
|
||||
}, [ item.icon ]),
|
||||
E('div', { 'class': 'homeproxy-connect-info' }, [
|
||||
E('div', { 'class': 'homeproxy-connect-title' }, [ item.title ]),
|
||||
result
|
||||
])
|
||||
]);
|
||||
}));
|
||||
}
|
||||
|
||||
function getResVersion(o, type) {
|
||||
const callResVersion = rpc.declare({
|
||||
object: 'luci.homeproxy',
|
||||
@@ -227,16 +323,16 @@ return view.extend({
|
||||
render() {
|
||||
let m, s, o;
|
||||
|
||||
hp.installCloseButtonText();
|
||||
|
||||
m = new form.Map('homeproxy');
|
||||
|
||||
s = m.section(form.NamedSection, 'config', 'homeproxy', _('Connection check'));
|
||||
s.anonymous = true;
|
||||
|
||||
o = s.option(form.DummyValue, '_check_baidu', _('BaiDu'));
|
||||
o.cfgvalue = L.bind(getConnStat, this, o, 'baidu');
|
||||
|
||||
o = s.option(form.DummyValue, '_check_google', _('Google'));
|
||||
o.cfgvalue = L.bind(getConnStat, this, o, 'google');
|
||||
o = s.option(form.DummyValue, '_connection_check');
|
||||
o.rawhtml = true;
|
||||
o.render = renderConnCards;
|
||||
|
||||
s = m.section(form.NamedSection, 'config', 'homeproxy', _('Resources management'));
|
||||
s.anonymous = true;
|
||||
|
||||
Reference in new issue
Block a user