⛄ Sync 2026-07-31 00:26:24

This commit is contained in:
github-actions[bot] committed 2026-07-31 00:26:24 +08:00
1 parent 47d0743808
commit b44a4364c4
125 files changed
+3954 -140418

No files matched your search

@@ -6,19 +6,114 @@
'use strict';
'require form';
'require fs';
'require rpc';
'require uci';
'require ui';
'require view';
'require homeproxy as hp';
'require homeproxy.diagnostics as hpDiagnostics';
'require homeproxy.tcping as hpTcping';
'require tools.widgets as widgets';
const callNodeReferences = rpc.declare({
object: 'luci.homeproxy_node_tools',
method: 'node_references',
params: [ 'node_id' ],
expect: { '': {} }
});
const callRemoveSubscriptionNodes = rpc.declare({
object: 'luci.homeproxy_node_tools',
method: 'remove_subscription_nodes',
params: [ 'node_ids' ],
expect: { '': {} }
});
const callUpdateSubscriptions = rpc.declare({
object: 'luci.homeproxy_node_tools',
method: 'update_subscriptions',
expect: { '': {} }
});
const callUpdateSubscriptionsStatus = rpc.declare({
object: 'luci.homeproxy_node_tools',
method: 'update_subscriptions_status',
expect: { '': {} }
});
const SUBSCRIPTION_UPDATE_POLL_INTERVAL = 1000;
const SUBSCRIPTION_UPDATE_POLL_LIMIT = 180;
function subscriptionRpcErrorMessage(err, fallback) {
let message = err?.message || err;
if (message != null)
message = String(message);
if (!message || message === 'Unknown error.' || message === '未知错误。')
message = fallback;
let lower = (message || '').toLowerCase();
if (lower.includes('access denied') ||
lower.includes('permission denied') ||
lower.includes('ubus') ||
lower.includes('rpc') ||
lower.includes('session')) {
return _('HomeProxy was just upgraded or rpcd has restarted. The current page session may be expired. Refresh the page or sign in again before retrying.');
}
return message || fallback;
}
function allowInsecureConfirm(ev, _section_id, value) {
if (value === '1' && !confirm(_('Are you sure to allow insecure?')))
ev.target.firstElementChild.checked = null;
}
function showNodeReferenceNotice(section_id) {
return L.resolveDefault(callNodeReferences(section_id), { result: false, error: _('Unknown error.') }).then((res) => {
if (!res.result) {
ui.addNotification(null, E('p', res.error || _('Failed to check node references. Please try again later.')), 'warning');
return true;
}
let refs = res.references || [];
if (!refs.length)
return false;
let label = uci.get('homeproxy', section_id, 'label') || section_id;
ui.addNotification(null, E('div', [
E('p', _('Node %s is still referenced by these settings. Remove the references before deleting the node.').format(label)),
E('ul', refs.map((ref) => E('li', ref?.label || ref?.scope || '')))
]), 'warning');
return true;
}).catch((err) => {
ui.addNotification(null, E('p', _('Failed to check node references. Please try again later.')), 'warning');
return true;
});
}
function wait(ms) {
return new Promise((resolve) => window.setTimeout(resolve, ms));
}
function pollSubscriptionUpdateStatus(attempt) {
return callUpdateSubscriptionsStatus().catch((err) => {
throw new Error(subscriptionRpcErrorMessage(err, _('Failed to read subscription update status.')));
}).then((res) => {
if (!res.result)
throw new Error(res.error || _('Failed to read subscription update status.'));
if (!res.completed) {
if (attempt >= SUBSCRIPTION_UPDATE_POLL_LIMIT)
throw new Error(_('Timed out waiting for subscription update to finish.'));
return wait(SUBSCRIPTION_UPDATE_POLL_INTERVAL).then(() => pollSubscriptionUpdateStatus(attempt + 1));
}
return res;
});
}
function parseShareLink(uri, features) {
let config, url, params;
@@ -455,6 +550,27 @@ function renderNodeSettings(section, data, features, main_node, routing_mode) {
o.depends({'type': 'direct', '!reverse': true});
o.rmempty = false;
o = s.option(form.DummyValue, '_tcping_delay', '延迟');
o.editable = true;
o.rmempty = true;
o.renderWidget = function(section_id) {
hpTcping.ensureStyle();
return E('span', {
id: hpTcping.nodeId(section_id),
class: 'homeproxy-node-tcping homeproxy-latency-pill',
role: 'button',
tabindex: '0',
title: '点击测速',
click: (ev) => hpTcping.runNode(section_id, ev),
keydown: (ev) => {
if (ev.key === 'Enter' || ev.key === ' ') {
ev.preventDefault();
return hpTcping.runNode(section_id, ev);
}
}
}, '-');
}
o = s.option(form.Value, 'username', _('Username'));
o.depends('type', 'http');
o.depends('type', 'socks');
@@ -638,6 +754,69 @@ function renderNodeSettings(section, data, features, main_node, routing_mode) {
o.depends('shadowsocks_plugin', 'obfs-local');
o.depends('shadowsocks_plugin', 'v2ray-plugin');
o.modalonly = true;
o = s.option(form.Flag, 'shadowtls_enabled', _('Enable ShadowTLS'));
o.depends('type', 'shadowsocks');
o.rmempty = false;
o.load = function(section_id) {
let enabled = uci.get(data[0], section_id, 'shadowtls_enabled');
if (enabled != null)
return enabled;
return uci.get(data[0], section_id, 'shadowtls_address') ? '1' : '0';
}
o.modalonly = true;
o = s.option(form.Value, 'shadowtls_address', _('ShadowTLS address'));
o.datatype = 'host';
o.depends({'type': 'shadowsocks', 'shadowtls_enabled': '1'});
o.validate = function(section_id, value) {
if (section_id) {
let type = this.section.formvalue(section_id, 'type');
let enabled = this.section.formvalue(section_id, 'shadowtls_enabled');
if (type === 'shadowsocks' && enabled === '1' && !value)
return _('Cannot be empty');
}
return true;
}
o.modalonly = true;
o = s.option(form.Value, 'shadowtls_port', _('ShadowTLS port'));
o.datatype = 'port';
o.placeholder = '443';
o.depends({'type': 'shadowsocks', 'shadowtls_enabled': '1'});
o.validate = function(section_id, value) {
if (section_id) {
let type = this.section.formvalue(section_id, 'type');
let enabled = this.section.formvalue(section_id, 'shadowtls_enabled');
if (type === 'shadowsocks' && enabled === '1' && !value)
return _('Cannot be empty');
}
return true;
}
o.modalonly = true;
o = s.option(form.Value, 'shadowtls_password', _('ShadowTLS password'));
o.password = true;
o.depends({'type': 'shadowsocks', 'shadowtls_enabled': '1'});
o.validate = function(section_id, value) {
if (section_id) {
let type = this.section.formvalue(section_id, 'type');
let enabled = this.section.formvalue(section_id, 'shadowtls_enabled');
if (type === 'shadowsocks' && enabled === '1' && !value)
return _('Cannot be empty');
}
return true;
}
o.modalonly = true;
o = s.option(form.Value, 'shadowtls_sni', _('ShadowTLS masquerade SNI'));
o.datatype = 'hostname';
o.depends({'type': 'shadowsocks', 'shadowtls_enabled': '1'});
o.modalonly = true;
/* Shadowsocks config end */
/* ShadowTLS config */
@@ -647,6 +826,7 @@ function renderNodeSettings(section, data, features, main_node, routing_mode) {
o.value('3', _('v3'));
o.default = '1';
o.depends('type', 'shadowtls');
o.depends({'type': 'shadowsocks', 'shadowtls_enabled': '1'});
o.rmempty = false;
o.modalonly = true;
@@ -1189,7 +1369,8 @@ return view.extend({
load() {
return Promise.all([
uci.load('homeproxy'),
hp.getBuiltinFeatures()
hp.getBuiltinFeatures(),
hpDiagnostics.load()
]);
},
@@ -1198,6 +1379,7 @@ return view.extend({
let main_node = uci.get(data[0], 'config', 'main_node');
let routing_mode = uci.get(data[0], 'config', 'routing_mode');
let features = data[1];
hpDiagnostics.show(data[2]);
/* Cache subscription information, it will be called multiple times */
let subinfo = [];
@@ -1208,6 +1390,48 @@ return view.extend({
subinfo.push({ 'hash': urlhash, 'title': title });
}
let isSubscriptionGrouphash = function(grouphash) {
for (let info of subinfo)
if (info.hash === grouphash)
return true;
return false;
};
let collectNodeSections = function(grouphash) {
let sections = [];
uci.sections(data[0], 'node', (res) => {
if (grouphash ? res.grouphash === grouphash : !isSubscriptionGrouphash(res.grouphash))
sections.push(res['.name']);
});
return sections;
};
let addTcpingButton = function(tab, option, grouphash) {
o = s.taboption(tab, form.DummyValue, option, '');
o.rawhtml = true;
o.renderWidget = function() {
hpTcping.ensureStyle();
return E('div', {
'class': 'homeproxy-tcping-toolbar',
'style': 'display:flex; align-items:center; gap:.75em; flex-wrap:wrap;'
}, [
E('button', {
'class': 'cbi-button cbi-button-action homeproxy-tcping-button',
'title': '测速',
'type': 'button',
'click': (ev) => hpTcping.runNodes(collectNodeSections(grouphash), ev)
}, [ hpTcping.renderIcon('homeproxy-tcping-icon') ]),
E('span', {
'class': 'homeproxy-tcping-help',
'style': 'color:var(--text-color-medium, #777); line-height:1.4;'
}, hpTcping.helpText)
]);
};
};
m = new form.Map('homeproxy', _('Edit nodes'));
s = m.section(form.NamedSection, 'subscription', 'homeproxy');
@@ -1215,9 +1439,18 @@ return view.extend({
/* Node settings start */
/* User nodes start */
s.tab('node', _('Nodes'));
addTcpingButton('node', '_tcping_node');
o = s.taboption('node', form.SectionValue, '_node', form.GridSection, 'node');
ss = renderNodeSettings(o.subsection, data, features, main_node, routing_mode);
ss.addremove = true;
ss.handleRemove = function(section_id, ev) {
return showNodeReferenceNotice(section_id).then((blocked) => {
if (blocked)
return Promise.resolve();
return form.GridSection.prototype.handleRemove.apply(this, [ section_id, ev ]);
});
};
ss.filter = function(section_id) {
for (let info of subinfo)
if (info.hash === uci.get(data[0], section_id, 'grouphash'))
@@ -1320,6 +1553,7 @@ return view.extend({
/* Subscription nodes start */
for (const info of subinfo) {
s.tab('sub_' + info.hash, _('Sub (%s)').format(info.title));
addTcpingButton('sub_' + info.hash, '_tcping_' + info.hash, info.hash);
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.filter = function(section_id) {
@@ -1386,6 +1620,11 @@ return view.extend({
o.rmempty = false;
o.onchange = allowInsecureConfirm;
o = s.taboption('subscription', form.Flag, 'allow_unsupported_tls_pin_fallback', _('Allow unsupported certificate pin fallback'),
_('When a subscription node uses a server certificate fingerprint but the current sing-box version cannot express it, skip the node by default. Enable this only if you explicitly accept falling back to TLS insecure mode for compatibility.'));
o.rmempty = false;
o.onchange = allowInsecureConfirm;
o = s.taboption('subscription', form.ListValue, 'packet_encoding', _('Default packet encoding'));
o.value('', _('none'));
o.value('packetaddr', _('packet addr (v2ray-core v5+)'));
@@ -1413,10 +1652,35 @@ return view.extend({
}
}
o.onclick = function() {
return fs.exec_direct('/etc/homeproxy/scripts/update_subscriptions.uc').then((res) => {
return location.reload();
ui.showModal(_('Updating subscriptions...'), [
E('p', _('Subscription update is running. The page will refresh automatically when it finishes.'))
]);
return callUpdateSubscriptions().catch((err) => {
return {
result: false,
error: subscriptionRpcErrorMessage(err, _('An error occurred while updating subscriptions.'))
};
}).then((res) => {
if (!res.result) {
ui.hideModal();
ui.addNotification(null, E('p', res.error || _('An error occurred while updating subscriptions.')), 'warning');
return this.map.reset();
}
return pollSubscriptionUpdateStatus(0).then((status) => {
ui.hideModal();
if (!status.update_result) {
ui.addNotification(null, E('p', status.error || _('An error occurred while updating subscriptions.')), 'warning');
return this.map.reset();
}
return location.reload();
});
}).catch((err) => {
ui.addNotification(null, E('p', _('An error occurred during updating subscriptions: %s').format(err)));
ui.hideModal();
ui.addNotification(null, E('p', subscriptionRpcErrorMessage(err, _('An error occurred while updating subscriptions.'))), 'warning');
return this.map.reset();
});
}
@@ -1444,19 +1708,17 @@ return view.extend({
subnodes = subnodes.concat(res['.name'])
});
for (let i in subnodes)
uci.remove(data[0], subnodes[i]);
return L.resolveDefault(callRemoveSubscriptionNodes(subnodes), { result: false, error: _('Unknown error.') }).then((res) => {
if (!res.result)
throw new Error(res.error || _('Unknown error.'));
if (subnodes.includes(uci.get(data[0], 'config', 'main_node')))
uci.set(data[0], 'config', 'main_node', 'nil');
if (subnodes.includes(uci.get(data[0], 'config', 'main_udp_node')))
uci.set(data[0], 'config', 'main_udp_node', 'nil');
this.inputtitle = _('%s nodes removed').format(subnodes.length);
this.readonly = true;
return this.map.save(null, true);
this.inputtitle = _('%s nodes removed').format(res.removed || 0);
this.readonly = true;
return location.reload();
}).catch((err) => {
ui.addNotification(null, E('p', _('An error occurred during removing subscription nodes: %s').format(err.message || err)));
return this.map.reset();
});
}
/* Subscriptions settings end */