Merge pull request #42 from itdoginfo/fix/many-sni-support

fix: add missing URL decoding for semicolon
This commit is contained in:
itdoginfo
2025-02-22 14:52:03 +03:00
committed by GitHub
2 changed files with 79 additions and 54 deletions

View File

@@ -5,6 +5,29 @@
'require network'; 'require network';
'require fs'; 'require fs';
// Add helper function for safe command execution with timeout
async function safeExec(command, args = [], timeout = 3000) {
try {
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), timeout);
const result = await Promise.race([
fs.exec(command, args),
new Promise((_, reject) => {
controller.signal.addEventListener('abort', () => {
reject(new Error('Command execution timed out'));
});
})
]);
clearTimeout(timeoutId);
return result;
} catch (error) {
console.warn(`Command execution failed or timed out: ${command} ${args.join(' ')}`);
return { stdout: '', stderr: error.message };
}
}
function formatDiagnosticOutput(output) { function formatDiagnosticOutput(output) {
if (typeof output !== 'string') return ''; if (typeof output !== 'string') return '';
return output.trim() return output.trim()
@@ -53,7 +76,7 @@ function createConfigSection(section, map, network) {
o.rows = 5; o.rows = 5;
o.ucisection = s.section; o.ucisection = s.section;
o.load = function (section_id) { o.load = function (section_id) {
return fs.exec('/etc/init.d/podkop', ['get_proxy_label', section_id]).then(res => { return safeExec('/etc/init.d/podkop', ['get_proxy_label', section_id]).then(res => {
if (res.stdout) { if (res.stdout) {
try { try {
const decodedLabel = decodeURIComponent(res.stdout.trim()); const decodedLabel = decodeURIComponent(res.stdout.trim());
@@ -457,7 +480,7 @@ return view.extend({
`); `);
const m = new form.Map('podkop', _('Podkop configuration'), null, ['main', 'extra']); const m = new form.Map('podkop', _('Podkop configuration'), null, ['main', 'extra']);
fs.exec('/etc/init.d/podkop', ['show_version']).then(res => { safeExec('/etc/init.d/podkop', ['show_version']).then(res => {
if (res.stdout) m.title = _('Podkop') + ' v' + res.stdout.trim(); if (res.stdout) m.title = _('Podkop') + ' v' + res.stdout.trim();
}); });
@@ -512,7 +535,7 @@ return view.extend({
o.default = '1.1.1.1'; o.default = '1.1.1.1';
o.rmempty = false; o.rmempty = false;
o.ucisection = 'main'; o.ucisection = 'main';
o.validate = function(section_id, value) { o.validate = function (section_id, value) {
if (!value) { if (!value) {
return _('DNS server address cannot be empty'); return _('DNS server address cannot be empty');
} }
@@ -560,7 +583,7 @@ return view.extend({
o.default = '/tmp/cache.db'; o.default = '/tmp/cache.db';
o.rmempty = false; o.rmempty = false;
o.ucisection = 'main'; o.ucisection = 'main';
o.validate = function(section_id, value) { o.validate = function (section_id, value) {
if (!value) { if (!value) {
return _('Cache file path cannot be empty'); return _('Cache file path cannot be empty');
} }
@@ -630,19 +653,23 @@ return view.extend({
podkopStatus.running ? podkopStatus.running ?
E('button', { E('button', {
'class': 'btn cbi-button-remove', 'class': 'btn cbi-button-remove',
'click': () => fs.exec('/etc/init.d/podkop', ['stop']).then(() => location.reload()) 'click': () => safeExec('/etc/init.d/podkop', ['stop']).then(() => location.reload())
}, _('Stop Podkop')) : }, _('Stop Podkop')) :
E('button', { E('button', {
'class': 'btn cbi-button-apply', 'class': 'btn cbi-button-apply',
'click': () => fs.exec('/etc/init.d/podkop', ['start']).then(() => location.reload()) 'click': () => safeExec('/etc/init.d/podkop', ['start']).then(() => location.reload())
}, _('Start Podkop')), }, _('Start Podkop')),
E('button', { E('button', {
'class': 'btn cbi-button-apply', 'class': 'btn cbi-button-apply',
'click': () => fs.exec('/etc/init.d/podkop', ['restart']).then(() => location.reload()) 'click': () => safeExec('/etc/init.d/podkop', ['restart']).then(() => location.reload())
}, _('Restart Podkop')), }, _('Restart Podkop')),
E('button', {
'class': 'btn cbi-button-' + (podkopStatus.enabled ? 'remove' : 'apply'),
'click': () => safeExec('/etc/init.d/podkop', [podkopStatus.enabled ? 'disable' : 'enable']).then(() => location.reload())
}, podkopStatus.enabled ? _('Disable Podkop') : _('Enable Podkop')),
E('button', { E('button', {
'class': 'btn', 'class': 'btn',
'click': () => fs.exec('/etc/init.d/podkop', ['show_config']).then(res => { 'click': () => safeExec('/etc/init.d/podkop', ['show_config']).then(res => {
const formattedOutput = formatDiagnosticOutput(res.stdout || _('No output')); const formattedOutput = formatDiagnosticOutput(res.stdout || _('No output'));
ui.showModal(_('Podkop Configuration'), [ ui.showModal(_('Podkop Configuration'), [
E('div', { style: 'max-height: 70vh; overflow-y: auto; margin: 1em 0; padding: 1.5em; background: #f8f9fa; border: 1px solid #e9ecef; border-radius: 4px; font-family: monospace; white-space: pre-wrap; word-wrap: break-word; line-height: 1.5; font-size: 14px;' }, [ E('div', { style: 'max-height: 70vh; overflow-y: auto; margin: 1em 0; padding: 1.5em; background: #f8f9fa; border: 1px solid #e9ecef; border-radius: 4px; font-family: monospace; white-space: pre-wrap; word-wrap: break-word; line-height: 1.5; font-size: 14px;' }, [
@@ -673,7 +700,7 @@ return view.extend({
}, _('Show Config')), }, _('Show Config')),
E('button', { E('button', {
'class': 'btn', 'class': 'btn',
'click': () => fs.exec('/etc/init.d/podkop', ['check_logs']).then(res => { 'click': () => safeExec('/etc/init.d/podkop', ['check_logs']).then(res => {
const formattedOutput = formatDiagnosticOutput(res.stdout || _('No output')); const formattedOutput = formatDiagnosticOutput(res.stdout || _('No output'));
ui.showModal(_('Podkop Logs'), [ ui.showModal(_('Podkop Logs'), [
E('div', { style: 'max-height: 70vh; overflow-y: auto; margin: 1em 0; padding: 1.5em; background: #f8f9fa; border: 1px solid #e9ecef; border-radius: 4px; font-family: monospace; white-space: pre-wrap; word-wrap: break-word; line-height: 1.5; font-size: 14px;' }, [ E('div', { style: 'max-height: 70vh; overflow-y: auto; margin: 1em 0; padding: 1.5em; background: #f8f9fa; border: 1px solid #e9ecef; border-radius: 4px; font-family: monospace; white-space: pre-wrap; word-wrap: break-word; line-height: 1.5; font-size: 14px;' }, [
@@ -717,7 +744,7 @@ return view.extend({
E('div', { 'class': 'btn-group', 'style': 'display: flex; flex-direction: column; gap: 8px;' }, [ E('div', { 'class': 'btn-group', 'style': 'display: flex; flex-direction: column; gap: 8px;' }, [
E('button', { E('button', {
'class': 'btn', 'class': 'btn',
'click': () => fs.exec('/etc/init.d/podkop', ['show_sing_box_config']).then(res => { 'click': () => safeExec('/etc/init.d/podkop', ['show_sing_box_config']).then(res => {
const formattedOutput = formatDiagnosticOutput(res.stdout || _('No output')); const formattedOutput = formatDiagnosticOutput(res.stdout || _('No output'));
ui.showModal(_('Sing-box Configuration'), [ ui.showModal(_('Sing-box Configuration'), [
E('div', { style: 'max-height: 70vh; overflow-y: auto; margin: 1em 0; padding: 1.5em; background: #f8f9fa; border: 1px solid #e9ecef; border-radius: 4px; font-family: monospace; white-space: pre-wrap; word-wrap: break-word; line-height: 1.5; font-size: 14px;' }, [ E('div', { style: 'max-height: 70vh; overflow-y: auto; margin: 1em 0; padding: 1.5em; background: #f8f9fa; border: 1px solid #e9ecef; border-radius: 4px; font-family: monospace; white-space: pre-wrap; word-wrap: break-word; line-height: 1.5; font-size: 14px;' }, [
@@ -748,7 +775,7 @@ return view.extend({
}, _('Show Config')), }, _('Show Config')),
E('button', { E('button', {
'class': 'btn', 'class': 'btn',
'click': () => fs.exec('/etc/init.d/podkop', ['check_sing_box_logs']).then(res => { 'click': () => safeExec('/etc/init.d/podkop', ['check_sing_box_logs']).then(res => {
const formattedOutput = formatDiagnosticOutput(res.stdout || _('No output')); const formattedOutput = formatDiagnosticOutput(res.stdout || _('No output'));
ui.showModal(_('Sing-box Logs'), [ ui.showModal(_('Sing-box Logs'), [
E('div', { style: 'max-height: 70vh; overflow-y: auto; margin: 1em 0; padding: 1.5em; background: #f8f9fa; border: 1px solid #e9ecef; border-radius: 4px; font-family: monospace; white-space: pre-wrap; word-wrap: break-word; line-height: 1.5; font-size: 14px;' }, [ E('div', { style: 'max-height: 70vh; overflow-y: auto; margin: 1em 0; padding: 1.5em; background: #f8f9fa; border: 1px solid #e9ecef; border-radius: 4px; font-family: monospace; white-space: pre-wrap; word-wrap: break-word; line-height: 1.5; font-size: 14px;' }, [
@@ -779,7 +806,7 @@ return view.extend({
}, _('View Logs')), }, _('View Logs')),
E('button', { E('button', {
'class': 'btn', 'class': 'btn',
'click': () => fs.exec('/etc/init.d/podkop', ['check_sing_box_connections']).then(res => { 'click': () => safeExec('/etc/init.d/podkop', ['check_sing_box_connections']).then(res => {
const formattedOutput = formatDiagnosticOutput(res.stdout || _('No output')); const formattedOutput = formatDiagnosticOutput(res.stdout || _('No output'));
ui.showModal(_('Active Connections'), [ ui.showModal(_('Active Connections'), [
E('div', { style: 'max-height: 70vh; overflow-y: auto; margin: 1em 0; padding: 1.5em; background: #f8f9fa; border: 1px solid #e9ecef; border-radius: 4px; font-family: monospace; white-space: pre-wrap; word-wrap: break-word; line-height: 1.5; font-size: 14px;' }, [ E('div', { style: 'max-height: 70vh; overflow-y: auto; margin: 1em 0; padding: 1.5em; background: #f8f9fa; border: 1px solid #e9ecef; border-radius: 4px; font-family: monospace; white-space: pre-wrap; word-wrap: break-word; line-height: 1.5; font-size: 14px;' }, [
@@ -818,7 +845,7 @@ return view.extend({
E('div', { 'class': 'btn-group', 'style': 'display: flex; flex-direction: column; gap: 8px;' }, [ E('div', { 'class': 'btn-group', 'style': 'display: flex; flex-direction: column; gap: 8px;' }, [
E('button', { E('button', {
'class': 'btn', 'class': 'btn',
'click': () => fs.exec('/etc/init.d/podkop', ['check_nft']).then(res => { 'click': () => safeExec('/etc/init.d/podkop', ['check_nft']).then(res => {
const formattedOutput = formatDiagnosticOutput(res.stdout || _('No output')); const formattedOutput = formatDiagnosticOutput(res.stdout || _('No output'));
ui.showModal(_('NFT Rules'), [ ui.showModal(_('NFT Rules'), [
E('div', { style: 'max-height: 70vh; overflow-y: auto; margin: 1em 0; padding: 1.5em; background: #f8f9fa; border: 1px solid #e9ecef; border-radius: 4px; font-family: monospace; white-space: pre-wrap; word-wrap: break-word; line-height: 1.5; font-size: 14px;' }, [ E('div', { style: 'max-height: 70vh; overflow-y: auto; margin: 1em 0; padding: 1.5em; background: #f8f9fa; border: 1px solid #e9ecef; border-radius: 4px; font-family: monospace; white-space: pre-wrap; word-wrap: break-word; line-height: 1.5; font-size: 14px;' }, [
@@ -849,7 +876,7 @@ return view.extend({
}, _('Check NFT Rules')), }, _('Check NFT Rules')),
E('button', { E('button', {
'class': 'btn', 'class': 'btn',
'click': () => fs.exec('/etc/init.d/podkop', ['check_dnsmasq']).then(res => { 'click': () => safeExec('/etc/init.d/podkop', ['check_dnsmasq']).then(res => {
const formattedOutput = formatDiagnosticOutput(res.stdout || _('No output')); const formattedOutput = formatDiagnosticOutput(res.stdout || _('No output'));
ui.showModal(_('DNSMasq Configuration'), [ ui.showModal(_('DNSMasq Configuration'), [
E('div', { style: 'max-height: 70vh; overflow-y: auto; margin: 1em 0; padding: 1.5em; background: #f8f9fa; border: 1px solid #e9ecef; border-radius: 4px; font-family: monospace; white-space: pre-wrap; word-wrap: break-word; line-height: 1.5; font-size: 14px;' }, [ E('div', { style: 'max-height: 70vh; overflow-y: auto; margin: 1em 0; padding: 1.5em; background: #f8f9fa; border: 1px solid #e9ecef; border-radius: 4px; font-family: monospace; white-space: pre-wrap; word-wrap: break-word; line-height: 1.5; font-size: 14px;' }, [
@@ -880,7 +907,7 @@ return view.extend({
}, _('Check DNSMasq')), }, _('Check DNSMasq')),
E('button', { E('button', {
'class': 'btn', 'class': 'btn',
'click': () => fs.exec('/etc/init.d/podkop', ['list_update']).then(res => { 'click': () => safeExec('/etc/init.d/podkop', ['list_update']).then(res => {
const formattedOutput = formatDiagnosticOutput(res.stdout || _('No output')); const formattedOutput = formatDiagnosticOutput(res.stdout || _('No output'));
ui.showModal(_('Lists Update Results'), [ ui.showModal(_('Lists Update Results'), [
E('div', { style: 'max-height: 70vh; overflow-y: auto; margin: 1em 0; padding: 1.5em; background: #f8f9fa; border: 1px solid #e9ecef; border-radius: 4px; font-family: monospace; white-space: pre-wrap; word-wrap: break-word; line-height: 1.5; font-size: 14px;' }, [ E('div', { style: 'max-height: 70vh; overflow-y: auto; margin: 1em 0; padding: 1.5em; background: #f8f9fa; border: 1px solid #e9ecef; border-radius: 4px; font-family: monospace; white-space: pre-wrap; word-wrap: break-word; line-height: 1.5; font-size: 14px;' }, [
@@ -983,12 +1010,12 @@ return view.extend({
system, system,
fakeipStatus fakeipStatus
] = await Promise.all([ ] = await Promise.all([
fs.exec('/etc/init.d/podkop', ['get_status']), safeExec('/etc/init.d/podkop', ['get_status']),
fs.exec('/etc/init.d/podkop', ['get_sing_box_status']), safeExec('/etc/init.d/podkop', ['get_sing_box_status']),
fs.exec('/etc/init.d/podkop', ['show_version']), safeExec('/etc/init.d/podkop', ['show_version']),
fs.exec('/etc/init.d/podkop', ['show_luci_version']), safeExec('/etc/init.d/podkop', ['show_luci_version']),
fs.exec('/etc/init.d/podkop', ['show_sing_box_version']), safeExec('/etc/init.d/podkop', ['show_sing_box_version']),
fs.exec('/etc/init.d/podkop', ['show_system_info']), safeExec('/etc/init.d/podkop', ['show_system_info']),
checkFakeIP() checkFakeIP()
]); ]);

View File

@@ -212,8 +212,9 @@ main() {
config_get proxy_string "main" "proxy_string" config_get proxy_string "main" "proxy_string"
config_get interface "main" "interface" config_get interface "main" "interface"
config_get outbound_json "main" "outbound_json"
if [ -n "$proxy_string" ] || [ -n "$interface" ]; then if [ -n "$proxy_string" ] || [ -n "$interface" ] || [ -n "$outbound_json" ]; then
config_get_bool dont_touch_dhcp "main" "dont_touch_dhcp" "0" config_get_bool dont_touch_dhcp "main" "dont_touch_dhcp" "0"
if [ "$dont_touch_dhcp" -eq 0 ]; then if [ "$dont_touch_dhcp" -eq 0 ]; then
dnsmasq_add_resolver dnsmasq_add_resolver
@@ -798,7 +799,7 @@ sing_box_outdound() {
config_get outbound_json $section "outbound_json" config_get outbound_json $section "outbound_json"
if [ -n "$outbound_json" ]; then if [ -n "$outbound_json" ]; then
log "Using JSON outbound configuration" log "Using JSON outbound configuration"
sing_box_config_outbound_json "$outbound_json" sing_box_config_outbound_json "$outbound_json" "$section"
else else
log "Missing outbound JSON configuration" log "Missing outbound JSON configuration"
return return
@@ -880,37 +881,34 @@ sing_box_config_check() {
sing_box_config_outbound_json() { sing_box_config_outbound_json() {
local json_config="$1" local json_config="$1"
local listen_port="$2" local section="$2"
cat > /tmp/base_config.json << EOF # Create new object with tag first, then merge with the rest of the config
{ local modified_config=$(echo "$json_config" | jq --arg section "$section" \
"log": { 'del(.tag) | {"tag": $section} + .')
"level": "warn"
}, jq --argjson outbound "$modified_config" \
"inbounds": [ --arg section "$section" \
{ '. |
"type": "tproxy", .outbounds |= (
"listen": "::", map(
"listen_port": $listen_port, if .tag == $section then
"tcp_fast_open": true, $outbound
"udp_fragment": true else . end
}, ) +
{ (
"tag": "dns-in", if (map(select(.tag == $section)) | length) == 0 then
"type": "direct", [$outbound]
"listen": "127.0.0.42", else [] end
"listen_port": 53 )
} )' $SING_BOX_CONFIG >/tmp/sing-box-config-tmp.json && mv /tmp/sing-box-config-tmp.json $SING_BOX_CONFIG
],
"outbounds": [],
"route": {
"auto_detect_interface": true
}
}
EOF
jq --argjson outbound "$json_config" '.outbounds += [$outbound]' /tmp/base_config.json > $SING_BOX_CONFIG if [ $? -eq 0 ]; then
rm -f /tmp/base_config.json log "Outbound config updated successfully"
else
log "Error: Invalid JSON config generated"
return 1
fi
} }
@@ -985,7 +983,7 @@ sing_box_config_vless() {
get_param() { get_param() {
local param="$1" local param="$1"
local value=$(echo "$STRING" | sed -n "s/.*[?&]$param=\([^&?#]*\).*/\1/p") local value=$(echo "$STRING" | sed -n "s/.*[?&]$param=\([^&?#]*\).*/\1/p")
value=$(echo "$value" | sed 's/%2F/\//g; s/%2C/,/g; s/%3D/=/g; s/%2B/+/g; s/%20/ /g' | tr -d '\n' | tr -d '\r') value=$(echo "$value" | sed 's/%2F/\//g; s/%2C/,/g; s/%3D/=/g; s/%2B/+/g; s/%20/ /g; s/%3B/;/g' | tr -d '\n' | tr -d '\r')
echo "$value" echo "$value"
} }
@@ -1823,7 +1821,7 @@ get_status() {
fi fi
# Check if service is running # Check if service is running
if pgrep -f "sing-box" >/dev/null; then if pgrep -f "podkop" >/dev/null; then
running=1 running=1
fi fi