From 7697754a73ae847d1ab68b7f52389d6790d8eb5c Mon Sep 17 00:00:00 2001 From: Ivan K Date: Sat, 22 Feb 2025 12:45:25 +0300 Subject: [PATCH] refactor: replace fs.exec with safeExec for command execution with timeout --- .../resources/view/podkop/podkop.js | 63 +++++++++++++------ podkop/files/etc/init.d/podkop | 2 +- 2 files changed, 44 insertions(+), 21 deletions(-) diff --git a/luci-app-podkop/htdocs/luci-static/resources/view/podkop/podkop.js b/luci-app-podkop/htdocs/luci-static/resources/view/podkop/podkop.js index 3a71052..96f359f 100644 --- a/luci-app-podkop/htdocs/luci-static/resources/view/podkop/podkop.js +++ b/luci-app-podkop/htdocs/luci-static/resources/view/podkop/podkop.js @@ -5,6 +5,29 @@ 'require network'; '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) { if (typeof output !== 'string') return ''; return output.trim() @@ -53,7 +76,7 @@ function createConfigSection(section, map, network) { o.rows = 5; o.ucisection = s.section; 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) { try { const decodedLabel = decodeURIComponent(res.stdout.trim()); @@ -457,7 +480,7 @@ return view.extend({ `); 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(); }); @@ -577,23 +600,23 @@ return view.extend({ podkopStatus.running ? E('button', { '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')) : E('button', { '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')), E('button', { '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')), E('button', { 'class': 'btn cbi-button-' + (podkopStatus.enabled ? 'remove' : 'apply'), - 'click': () => fs.exec('/etc/init.d/podkop', [podkopStatus.enabled ? 'disable' : 'enable']).then(() => location.reload()) + 'click': () => safeExec('/etc/init.d/podkop', [podkopStatus.enabled ? 'disable' : 'enable']).then(() => location.reload()) }, podkopStatus.enabled ? _('Disable Podkop') : _('Enable Podkop')), E('button', { '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')); 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;' }, [ @@ -624,7 +647,7 @@ return view.extend({ }, _('Show Config')), E('button', { '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')); 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;' }, [ @@ -668,7 +691,7 @@ return view.extend({ E('div', { 'class': 'btn-group', 'style': 'display: flex; flex-direction: column; gap: 8px;' }, [ E('button', { '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')); 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;' }, [ @@ -699,7 +722,7 @@ return view.extend({ }, _('Show Config')), E('button', { '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')); 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;' }, [ @@ -730,7 +753,7 @@ return view.extend({ }, _('View Logs')), E('button', { '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')); 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;' }, [ @@ -769,7 +792,7 @@ return view.extend({ E('div', { 'class': 'btn-group', 'style': 'display: flex; flex-direction: column; gap: 8px;' }, [ E('button', { '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')); 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;' }, [ @@ -800,7 +823,7 @@ return view.extend({ }, _('Check NFT Rules')), E('button', { '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')); 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;' }, [ @@ -831,7 +854,7 @@ return view.extend({ }, _('Check DNSMasq')), E('button', { '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')); 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;' }, [ @@ -934,12 +957,12 @@ return view.extend({ system, fakeipStatus ] = await Promise.all([ - fs.exec('/etc/init.d/podkop', ['get_status']), - fs.exec('/etc/init.d/podkop', ['get_sing_box_status']), - fs.exec('/etc/init.d/podkop', ['show_version']), - fs.exec('/etc/init.d/podkop', ['show_luci_version']), - fs.exec('/etc/init.d/podkop', ['show_sing_box_version']), - fs.exec('/etc/init.d/podkop', ['show_system_info']), + safeExec('/etc/init.d/podkop', ['get_status']), + safeExec('/etc/init.d/podkop', ['get_sing_box_status']), + safeExec('/etc/init.d/podkop', ['show_version']), + safeExec('/etc/init.d/podkop', ['show_luci_version']), + safeExec('/etc/init.d/podkop', ['show_sing_box_version']), + safeExec('/etc/init.d/podkop', ['show_system_info']), checkFakeIP() ]); diff --git a/podkop/files/etc/init.d/podkop b/podkop/files/etc/init.d/podkop index 37e87ed..e9b61cd 100755 --- a/podkop/files/etc/init.d/podkop +++ b/podkop/files/etc/init.d/podkop @@ -1814,7 +1814,7 @@ get_status() { fi # Check if service is running - if pgrep -f "sing-box" >/dev/null; then + if pgrep -f "podkop" >/dev/null; then running=1 fi