mirror of
https://github.com/kiddin9/op-packages.git
synced 2026-11-06 20:14:10 +08:00
🍓 Sync 2026-09-15 02:13:06
This commit is contained in:
1 parent
0e5393594c
commit
1dd96ac2bb
146 files changed
+26541
-508
No files matched your search
@@ -0,0 +1,32 @@
|
||||
include $(TOPDIR)/rules.mk
|
||||
|
||||
PKG_NAME:=luci-app-frpc-next
|
||||
PKG_VERSION:=1.0.0
|
||||
PKG_RELEASE:=1
|
||||
|
||||
PKG_LICENSE:=MIT
|
||||
PKG_MAINTAINER:=Dante <dante@seckv.com>
|
||||
|
||||
LUCI_TITLE:=LuCI support for frp client (v0.68.x/v0.69.x+)
|
||||
LUCI_DEPENDS:=+frpc-next +curl
|
||||
LUCI_PKGARCH:=all
|
||||
|
||||
include $(TOPDIR)/feeds/luci/luci.mk
|
||||
|
||||
define Package/$(PKG_NAME)
|
||||
$(call Package/luci/webtemplate)
|
||||
TITLE:=$(LUCI_TITLE)
|
||||
DEPENDS:=$(LUCI_DEPENDS)
|
||||
PROVIDES:=luci-app-frpc
|
||||
CONFLICTS:=luci-app-frpc
|
||||
endef
|
||||
|
||||
define Package/$(PKG_NAME)/postinst
|
||||
#!/bin/sh
|
||||
[ -n "$${IPKG_INSTROOT}" ] || {
|
||||
chmod 0755 /etc/init.d/frpc /etc/hotplug.d/iface/99-frpc 2>/dev/null || true
|
||||
}
|
||||
exit 0
|
||||
endef
|
||||
|
||||
# call BuildPackage - OpenWrt buildroot signature
|
||||
@@ -0,0 +1,44 @@
|
||||
'use strict';
|
||||
'require view';
|
||||
'require rpc';
|
||||
'require poll';
|
||||
|
||||
const callFrpcLog = rpc.declare({
|
||||
object: 'luci.frpc',
|
||||
method: 'get_log'
|
||||
});
|
||||
|
||||
return view.extend({
|
||||
handleSaveApply: null,
|
||||
handleSave: null,
|
||||
handleReset: null,
|
||||
|
||||
render: function() {
|
||||
const area = E('textarea', {
|
||||
id: 'frpc_log_content',
|
||||
style: 'width:100%; height:500px; background:#f4f4f4; color:#333; padding:10px; border:1px solid #ccc; border-radius:3px; font-family:monospace; font-size:12px; overflow-y:auto; white-space:pre-wrap; word-break:break-all; margin-top:10px;',
|
||||
readonly: 'readonly'
|
||||
}, _('Loading logs...'));
|
||||
|
||||
poll.add(() => this.updateLogs(), 5);
|
||||
|
||||
return E('div', { 'class': 'cbi-map' }, [
|
||||
E('h2', {}, _('Frp Client Title') + ' - ' + _('Log')),
|
||||
E('div', { 'class': 'cbi-map-descr' }, _('Real-time background system logs of the active frpc process, helping you diagnose server handshake and protocol errors.')),
|
||||
E('fieldset', { 'class': 'cbi-section' }, [
|
||||
E('legend', {}, _('Logs')),
|
||||
E('div', { 'class': 'cbi-section-node' }, area)
|
||||
])
|
||||
]);
|
||||
},
|
||||
|
||||
updateLogs: function() {
|
||||
return callFrpcLog().then(function(res) {
|
||||
const area = document.getElementById('frpc_log_content');
|
||||
if (area) {
|
||||
area.value = res && res.log ? res.log : _('No logs available...');
|
||||
area.scrollTop = area.scrollHeight;
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,294 @@
|
||||
'use strict';
|
||||
'require view';
|
||||
'require rpc';
|
||||
'require poll';
|
||||
'require ui';
|
||||
|
||||
const callFrpcStatus = rpc.declare({
|
||||
object: 'luci.frpc',
|
||||
method: 'get_status'
|
||||
});
|
||||
|
||||
const callAdoptProcess = rpc.declare({
|
||||
object: 'luci.frpc',
|
||||
method: 'adopt_process'
|
||||
});
|
||||
|
||||
const callServiceAction = rpc.declare({
|
||||
object: 'luci.frpc',
|
||||
method: 'service_action',
|
||||
params: [ 'action' ]
|
||||
});
|
||||
|
||||
const callRuntimeInfo = rpc.declare({
|
||||
object: 'luci.frpc',
|
||||
method: 'get_runtime_info'
|
||||
});
|
||||
|
||||
function renderState(status) {
|
||||
let text = _('Unknown');
|
||||
let color = '#999';
|
||||
|
||||
if (status.state === 'managed') {
|
||||
text = _('Running');
|
||||
color = 'green';
|
||||
} else if (status.state === 'unmanaged') {
|
||||
text = _('Running (Unmanaged)');
|
||||
color = 'orange';
|
||||
} else if (status.state === 'stopped') {
|
||||
text = _('Stopped');
|
||||
color = 'red';
|
||||
}
|
||||
|
||||
if (status.pid)
|
||||
text += ' [PID: ' + status.pid + ']';
|
||||
|
||||
return { text, color };
|
||||
}
|
||||
|
||||
function flattenProxies(statusObj) {
|
||||
const rows = [];
|
||||
if (!statusObj || typeof statusObj !== 'object')
|
||||
return rows;
|
||||
|
||||
Object.keys(statusObj).forEach((ptype) => {
|
||||
const arr = statusObj[ptype];
|
||||
if (!Array.isArray(arr))
|
||||
return;
|
||||
arr.forEach((p) => {
|
||||
if (!p || typeof p !== 'object')
|
||||
return;
|
||||
rows.push({
|
||||
name: p.name || '-',
|
||||
type: p.type || ptype || '-',
|
||||
status: p.status || '-',
|
||||
local: p.local_addr || '-',
|
||||
remote: p.remote_addr || '-',
|
||||
err: p.err || ''
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
return rows;
|
||||
}
|
||||
|
||||
return view.extend({
|
||||
handleSaveApply: null,
|
||||
handleSave: null,
|
||||
handleReset: null,
|
||||
|
||||
load: function() {
|
||||
return Promise.all([ callFrpcStatus(), callRuntimeInfo() ]);
|
||||
},
|
||||
|
||||
render: function(data) {
|
||||
const status = data && data[0] ? data[0] : {};
|
||||
const runtime = data && data[1] ? data[1] : {};
|
||||
const stateInfo = renderState(status || {});
|
||||
|
||||
const container = E('div', { 'class': 'cbi-map' }, [
|
||||
E('h2', {}, _('Frp Client Title') + ' - ' + _('Overview')),
|
||||
E('div', { 'class': 'cbi-map-descr' }, _('Overview of the local frpc service state, active process PID, loaded config files, and detailed online proxy information.'))
|
||||
]);
|
||||
|
||||
// Service Status Section (Aligned with lucky style)
|
||||
const statusSection = E('fieldset', { 'class': 'cbi-section' }, [
|
||||
E('legend', {}, _('Service Status')),
|
||||
E('div', { 'class': 'cbi-section-node' }, [
|
||||
E('table', { 'class': 'table cbi-section-table' }, [
|
||||
E('tr', { 'class': 'tr' }, [
|
||||
E('td', { 'class': 'td left', 'width': '33%' }, _('Current Status')),
|
||||
E('td', { 'class': 'td left' }, [
|
||||
E('span', { id: 'frpc-status-text', 'style': 'font-weight:bold; color:' + stateInfo.color + '; margin-right:10px;' }, stateInfo.text),
|
||||
E('button', {
|
||||
id: 'frpc-restart-btn',
|
||||
'class': 'btn cbi-button cbi-button-apply',
|
||||
'style': 'display:' + ((status.state === 'managed' || status.state === 'unmanaged') ? 'inline-block' : 'none') + ';',
|
||||
click: ui.createHandlerFn(this, function(ev) {
|
||||
if (!ev || !ev.currentTarget) return;
|
||||
const btn = ev.currentTarget;
|
||||
btn.disabled = true;
|
||||
ui.addNotification(null, E('p', _('Restarting FRP Client, please wait...')), 'info');
|
||||
return callServiceAction('restart').then(function(res) {
|
||||
btn.disabled = false;
|
||||
if (res && res.success) {
|
||||
ui.addNotification(null, E('p', _('Service restarted successfully.')), 'info');
|
||||
} else {
|
||||
ui.addNotification(null, E('p', _('Failed to restart service.')), 'error');
|
||||
}
|
||||
}).catch(function(err) {
|
||||
btn.disabled = false;
|
||||
ui.addNotification(null, E('p', _('An error occurred: ') + err.message), 'error');
|
||||
});
|
||||
})
|
||||
}, _('Restart'))
|
||||
])
|
||||
]),
|
||||
E('tr', { 'class': 'tr' }, [
|
||||
E('td', { 'class': 'td left' }, _('Generated Runtime Config')),
|
||||
E('td', { 'class': 'td left', id: 'frpc-runtime-config' }, status.runtime_config || '-')
|
||||
]),
|
||||
E('tr', { 'class': 'tr' }, [
|
||||
E('td', { 'class': 'td left' }, _('Configured Proxies')),
|
||||
E('td', { 'class': 'td left', id: 'frpc-proxy-count' }, String(status.proxy_count || 0))
|
||||
]),
|
||||
E('tr', { 'class': 'tr' }, [
|
||||
E('td', { 'class': 'td left' }, _('frpc Version')),
|
||||
E('td', { 'class': 'td left', id: 'frpc-version' }, status.version || '-')
|
||||
])
|
||||
])
|
||||
])
|
||||
]);
|
||||
container.appendChild(statusSection);
|
||||
|
||||
const proxyRows = flattenProxies(runtime.status);
|
||||
const runtimeSection = E('fieldset', {
|
||||
'class': 'cbi-section',
|
||||
id: 'frpc-runtime-section',
|
||||
style: (status.state === 'managed') ? '' : 'display:none'
|
||||
}, [
|
||||
E('legend', {}, _('Runtime Details')),
|
||||
E('div', { 'class': 'cbi-section-node' }, [
|
||||
E('table', { 'class': 'table cbi-section-table' }, [
|
||||
E('tr', { 'class': 'tr' }, [
|
||||
E('td', { 'class': 'td left', 'width': '33%' }, _('Web Panel')),
|
||||
E('td', { 'class': 'td left' }, [
|
||||
E('span', { id: 'frpc-web-api' }, [
|
||||
runtime.web_url ? E('a', {
|
||||
'class': 'btn cbi-button',
|
||||
href: runtime.web_url,
|
||||
target: '_blank',
|
||||
rel: 'noopener noreferrer'
|
||||
}, _('Open')) : '-'
|
||||
])
|
||||
])
|
||||
]),
|
||||
E('tr', { 'class': 'tr' }, [
|
||||
E('td', { 'class': 'td left' }, _('Connected Server')),
|
||||
E('td', { 'class': 'td left' }, [
|
||||
E('span', { id: 'frpc-connected-server' }, (runtime.server_addr && runtime.server_port) ? (runtime.server_addr + ':' + runtime.server_port) : '-'),
|
||||
E('div', { 'class': 'cbi-value-description', style: 'margin: 4px 0 0 0;' }, _('Connect to FRP server'))
|
||||
])
|
||||
]),
|
||||
E('tr', { 'class': 'tr' }, [
|
||||
E('td', { 'class': 'td left' }, _('Proxy Runtime List')),
|
||||
E('td', { 'class': 'td left' }, [
|
||||
E('div', { 'class': 'cbi-value-description', style: 'margin: 0 0 6px 0;' }, _('Running proxy list')),
|
||||
E('div', {
|
||||
id: 'frpc-proxy-list-wrap',
|
||||
style: 'max-height:220px; overflow:auto;'
|
||||
}, proxyRows.length > 0 ? E('table', { 'class': 'table cbi-section-table' }, [
|
||||
E('tr', { 'class': 'tr table-titles' }, [
|
||||
E('th', { 'class': 'th' }, _('Name')),
|
||||
E('th', { 'class': 'th' }, _('Type')),
|
||||
E('th', { 'class': 'th' }, _('Status')),
|
||||
E('th', { 'class': 'th' }, _('Local')),
|
||||
E('th', { 'class': 'th' }, _('Remote')),
|
||||
E('th', { 'class': 'th' }, _('Error'))
|
||||
]),
|
||||
...proxyRows.map((row) => E('tr', { 'class': 'tr' }, [
|
||||
E('td', { 'class': 'td' }, row.name),
|
||||
E('td', { 'class': 'td' }, row.type),
|
||||
E('td', { 'class': 'td' }, row.status),
|
||||
E('td', { 'class': 'td' }, row.local),
|
||||
E('td', { 'class': 'td' }, row.remote),
|
||||
E('td', { 'class': 'td' }, row.err || '-')
|
||||
]))
|
||||
]) : E('div', {}, _('No running proxies')))
|
||||
])
|
||||
])
|
||||
])
|
||||
])
|
||||
]);
|
||||
container.appendChild(runtimeSection);
|
||||
|
||||
poll.add(() => Promise.all([callFrpcStatus(), callRuntimeInfo()]).then((res) => {
|
||||
const s = res[0] || {};
|
||||
const r = res[1] || {};
|
||||
const next = renderState(s);
|
||||
|
||||
const statusNode = document.getElementById('frpc-status-text');
|
||||
if (statusNode) {
|
||||
statusNode.textContent = next.text;
|
||||
statusNode.style.color = next.color;
|
||||
}
|
||||
|
||||
const restartBtnNode = document.getElementById('frpc-restart-btn');
|
||||
if (restartBtnNode) {
|
||||
if (s.state === 'managed' || s.state === 'unmanaged') {
|
||||
restartBtnNode.style.display = 'inline-block';
|
||||
} else {
|
||||
restartBtnNode.style.display = 'none';
|
||||
}
|
||||
}
|
||||
|
||||
const runtimeConfigNode = document.getElementById('frpc-runtime-config');
|
||||
if (runtimeConfigNode)
|
||||
runtimeConfigNode.textContent = s.runtime_config || '-';
|
||||
|
||||
const proxyCountNode = document.getElementById('frpc-proxy-count');
|
||||
if (proxyCountNode)
|
||||
proxyCountNode.textContent = String(s.proxy_count || 0);
|
||||
|
||||
const versionNode = document.getElementById('frpc-version');
|
||||
if (versionNode)
|
||||
versionNode.textContent = s.version || '-';
|
||||
|
||||
const runtimeSectionNode = document.getElementById('frpc-runtime-section');
|
||||
if (runtimeSectionNode)
|
||||
runtimeSectionNode.style.display = (s.state === 'managed') ? '' : 'none';
|
||||
|
||||
const webNode = document.getElementById('frpc-web-api');
|
||||
if (webNode) {
|
||||
if (r.web_url) {
|
||||
webNode.innerHTML = '';
|
||||
webNode.appendChild(E('a', {
|
||||
'class': 'btn cbi-button',
|
||||
href: r.web_url,
|
||||
target: '_blank',
|
||||
rel: 'noopener noreferrer'
|
||||
}, _('Open')));
|
||||
} else {
|
||||
webNode.textContent = '-';
|
||||
}
|
||||
}
|
||||
|
||||
const serverNode = document.getElementById('frpc-connected-server');
|
||||
if (serverNode)
|
||||
serverNode.textContent = (r.server_addr && r.server_port) ? (r.server_addr + ':' + r.server_port) : '-';
|
||||
|
||||
const listWrap = document.getElementById('frpc-proxy-list-wrap');
|
||||
if (listWrap) {
|
||||
const rows = flattenProxies(r.status);
|
||||
listWrap.innerHTML = '';
|
||||
if (rows.length === 0) {
|
||||
listWrap.appendChild(E('div', {}, _('No running proxies')));
|
||||
} else {
|
||||
const table = E('table', { 'class': 'table cbi-section-table' }, [
|
||||
E('tr', { 'class': 'tr table-titles' }, [
|
||||
E('th', { 'class': 'th' }, _('Name')),
|
||||
E('th', { 'class': 'th' }, _('Type')),
|
||||
E('th', { 'class': 'th' }, _('Status')),
|
||||
E('th', { 'class': 'th' }, _('Local')),
|
||||
E('th', { 'class': 'th' }, _('Remote')),
|
||||
E('th', { 'class': 'th' }, _('Error'))
|
||||
])
|
||||
]);
|
||||
rows.forEach((row) => {
|
||||
table.appendChild(E('tr', { 'class': 'tr' }, [
|
||||
E('td', { 'class': 'td' }, row.name),
|
||||
E('td', { 'class': 'td' }, row.type),
|
||||
E('td', { 'class': 'td' }, row.status),
|
||||
E('td', { 'class': 'td' }, row.local),
|
||||
E('td', { 'class': 'td' }, row.remote),
|
||||
E('td', { 'class': 'td' }, row.err || '-')
|
||||
]));
|
||||
});
|
||||
listWrap.appendChild(table);
|
||||
}
|
||||
}
|
||||
}), 5);
|
||||
|
||||
return container;
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,276 @@
|
||||
'use strict';
|
||||
'require view';
|
||||
'require rpc';
|
||||
'require ui';
|
||||
|
||||
const callGetConfig = rpc.declare({
|
||||
object: 'luci.frpc',
|
||||
method: 'get_config'
|
||||
});
|
||||
|
||||
const callSaveConfig = rpc.declare({
|
||||
object: 'luci.frpc',
|
||||
method: 'save_config',
|
||||
params: [ 'config' ]
|
||||
});
|
||||
|
||||
const callValidateConfig = rpc.declare({
|
||||
object: 'luci.frpc',
|
||||
method: 'validate_config',
|
||||
params: [ 'config' ]
|
||||
});
|
||||
|
||||
const callStatus = rpc.declare({
|
||||
object: 'luci.frpc',
|
||||
method: 'get_status'
|
||||
});
|
||||
|
||||
const callSetEnabled = rpc.declare({
|
||||
object: 'luci.frpc',
|
||||
method: 'set_enabled',
|
||||
params: [ 'enabled' ]
|
||||
});
|
||||
|
||||
const callServiceAction = rpc.declare({
|
||||
object: 'luci.frpc',
|
||||
method: 'service_action',
|
||||
params: [ 'action' ]
|
||||
});
|
||||
|
||||
function parseErrorPosition(errText) {
|
||||
const s = String(errText || '');
|
||||
let m = s.match(/line\s+(\d+)\s*[,;:]?\s*(?:col|column)\s+(\d+)/i);
|
||||
if (m)
|
||||
return { line: +m[1], col: +m[2] };
|
||||
|
||||
m = s.match(/at\s+line\s+(\d+)\s*[,;:]?\s*column\s+(\d+)/i);
|
||||
if (m)
|
||||
return { line: +m[1], col: +m[2] };
|
||||
|
||||
m = s.match(/line\s+(\d+)/i);
|
||||
if (m)
|
||||
return { line: +m[1], col: null };
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
return view.extend({
|
||||
_validateTimer: null,
|
||||
_errorLine: null,
|
||||
|
||||
injectStyle: function() {
|
||||
if (document.getElementById('frpc_editor_style'))
|
||||
return;
|
||||
|
||||
const css = [
|
||||
'.frpc-editor-wrap{border:1px solid #ccc;border-radius:4px;overflow:hidden;background:#fff;max-width:900px;box-sizing:border-box;}',
|
||||
'.frpc-editor-main{display:flex;min-height:400px;max-height:65vh;}',
|
||||
'.frpc-editor-gutter{width:48px;background:#f7f7f7;color:#888;border-right:1px solid #e5e5e5;overflow:hidden;font:13px/1.5 monospace;padding:12px 6px;text-align:right;user-select:none;box-sizing:border-box;}',
|
||||
'.frpc-editor-gutter-line{height:1.5em;line-height:1.5;}',
|
||||
'.frpc-editor-gutter-line.err{background:#ffe8e8;color:#b00020;font-weight:700;}',
|
||||
'.frpc-editor-input-wrap{flex:1;background:#fff;position:relative;}',
|
||||
'.frpc-editor-input{width:100%;min-height:100%;box-sizing:border-box;border:0;outline:none;resize:vertical;overflow-wrap:anywhere;white-space:pre;word-break:keep-all;font:13px/1.5 monospace;padding:12px;margin:0;display:block;}'
|
||||
].join('\n');
|
||||
|
||||
document.head.appendChild(E('style', { id: 'frpc_editor_style' }, css));
|
||||
},
|
||||
|
||||
refreshEditor: function() {
|
||||
const ta = document.getElementById('frpc_config');
|
||||
const gutter = document.getElementById('frpc_editor_gutter');
|
||||
if (!ta || !gutter)
|
||||
return;
|
||||
|
||||
const text = ta.value || '';
|
||||
const lineCount = Math.max(1, text.split('\n').length);
|
||||
gutter.innerHTML = '';
|
||||
for (let i = 1; i <= lineCount; i++) {
|
||||
gutter.appendChild(E('div', {
|
||||
'class': 'frpc-editor-gutter-line' + (this._errorLine === i ? ' err' : '')
|
||||
}, String(i)));
|
||||
}
|
||||
},
|
||||
|
||||
renderEditor: function(configText) {
|
||||
const gutter = E('div', { id: 'frpc_editor_gutter', 'class': 'frpc-editor-gutter' });
|
||||
const ta = E('textarea', {
|
||||
id: 'frpc_config',
|
||||
class: 'frpc-editor-input',
|
||||
spellcheck: 'false',
|
||||
wrap: 'off'
|
||||
}, configText || '');
|
||||
|
||||
ta.addEventListener('keydown', function(ev) {
|
||||
if (ev.keyCode === 9) { // Tab key
|
||||
ev.preventDefault();
|
||||
const start = this.selectionStart;
|
||||
const end = this.selectionEnd;
|
||||
const val = this.value;
|
||||
this.value = val.substring(0, start) + ' ' + val.substring(end);
|
||||
this.selectionStart = this.selectionEnd = start + 2;
|
||||
}
|
||||
});
|
||||
|
||||
ta.addEventListener('input', L.bind(function() {
|
||||
this.refreshEditor();
|
||||
this.scheduleValidate();
|
||||
}, this));
|
||||
|
||||
ta.addEventListener('scroll', function() {
|
||||
gutter.scrollTop = ta.scrollTop;
|
||||
});
|
||||
|
||||
window.setTimeout(L.bind(function() { this.refreshEditor(); }, this), 0);
|
||||
|
||||
const inputWrap = E('div', { 'class': 'frpc-editor-input-wrap' }, [ ta ]);
|
||||
|
||||
return E('div', { 'class': 'frpc-editor-wrap' }, [
|
||||
E('div', { 'class': 'frpc-editor-main' }, [ gutter, inputWrap ])
|
||||
]);
|
||||
},
|
||||
|
||||
setValidateHint: function(ok, message, pos) {
|
||||
const hint = document.getElementById('frpc_validate_hint');
|
||||
if (!hint)
|
||||
return;
|
||||
|
||||
hint.style.color = ok ? 'green' : 'red';
|
||||
hint.textContent = message;
|
||||
this._errorLine = (!ok && pos && pos.line) ? pos.line : null;
|
||||
this.refreshEditor();
|
||||
},
|
||||
|
||||
runValidate: function() {
|
||||
const content = document.getElementById('frpc_config')?.value || '';
|
||||
if (!content.trim()) {
|
||||
this.setValidateHint(true, _('Configuration is empty.'));
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
return callValidateConfig(content).then(L.bind(function(res) {
|
||||
if (res && res.success) {
|
||||
this.setValidateHint(true, _('Configuration is valid.'));
|
||||
return;
|
||||
}
|
||||
|
||||
const err = (res && res.error) ? String(res.error).trim() : _('Unknown error');
|
||||
const pos = parseErrorPosition(err);
|
||||
let msg = _('Verification failed: ') + err;
|
||||
if (pos && pos.line)
|
||||
msg += ' (' + _('Line') + ' ' + pos.line + (pos.col ? ', ' + _('Column') + ' ' + pos.col : '') + ')';
|
||||
this.setValidateHint(false, msg, pos);
|
||||
}, this));
|
||||
},
|
||||
|
||||
scheduleValidate: function() {
|
||||
if (this._validateTimer)
|
||||
window.clearTimeout(this._validateTimer);
|
||||
|
||||
this._validateTimer = window.setTimeout(L.bind(function() {
|
||||
this.runValidate().catch(function() {});
|
||||
}, this), 700);
|
||||
},
|
||||
|
||||
load: function() {
|
||||
return Promise.all([ callGetConfig(), callStatus() ]);
|
||||
},
|
||||
|
||||
render: function(data) {
|
||||
this.injectStyle();
|
||||
const cfg = data && data[0] ? data[0] : {};
|
||||
const st = data && data[1] ? data[1] : {};
|
||||
const configText = cfg.config || '';
|
||||
const enabled = (st.enabled === 1 || st.enabled === true);
|
||||
|
||||
window.setTimeout(L.bind(function() {
|
||||
this.runValidate().catch(function() {});
|
||||
}, this), 0);
|
||||
|
||||
return E('div', { 'class': 'cbi-map' }, [
|
||||
E('h2', {}, _('Frp Client Title') + ' - ' + _('Settings')),
|
||||
E('div', { 'class': 'cbi-map-descr' }, _('Configure the Frp client parameters. Complete the server link details and local port forwarding rules to build an intranet penetration channel.')),
|
||||
E('div', { 'class': 'cbi-section' }, [
|
||||
E('h3', { 'class': 'cbi-section-title' }, _('Service Control')),
|
||||
E('div', { 'class': 'cbi-value' }, [
|
||||
E('label', { 'class': 'cbi-value-title' }, _('Enabled')),
|
||||
E('div', { 'class': 'cbi-value-field' }, [
|
||||
E('input', {
|
||||
id: 'frpc_enabled',
|
||||
type: 'checkbox',
|
||||
checked: enabled ? 'checked' : null
|
||||
})
|
||||
])
|
||||
])
|
||||
]),
|
||||
E('div', { 'class': 'cbi-section' }, [
|
||||
E('h3', { 'class': 'cbi-section-title' }, _('Configuration Editor')),
|
||||
this.renderEditor(configText)
|
||||
]),
|
||||
E('div', { 'class': 'cbi-section frpc-editor-buttons' }, [
|
||||
E('button', {
|
||||
'class': 'btn cbi-button cbi-button-apply',
|
||||
click: ui.createHandlerFn(this, function() {
|
||||
return this.runValidate();
|
||||
})
|
||||
}, _('Verify Configuration')),
|
||||
E('span', { id: 'frpc_validate_hint', style: 'margin-left:12px;' }, '')
|
||||
])
|
||||
]);
|
||||
},
|
||||
|
||||
validateAndSave: function() {
|
||||
const configText = document.getElementById('frpc_config')?.value || '';
|
||||
const enabledCheckbox = document.getElementById('frpc_enabled');
|
||||
const on = enabledCheckbox && enabledCheckbox.checked ? 1 : 0;
|
||||
|
||||
ui.showModal(null, [ E('p', { 'class': 'spinning' }, [ _('Applying changes...') ]) ]);
|
||||
|
||||
return callValidateConfig(configText).then(L.bind(function(res) {
|
||||
if (!res || !res.success) {
|
||||
ui.hideModal();
|
||||
const err = (res && res.error) ? String(res.error).trim() : _('Unknown error');
|
||||
this.setValidateHint(false, _('Verification failed: ') + err, parseErrorPosition(err));
|
||||
ui.addNotification(null, E('pre', { style: 'white-space:pre-wrap; margin:0;' }, _('frpc config verify failed:\n') + err), 'danger');
|
||||
throw new Error('frpc verify failed');
|
||||
}
|
||||
|
||||
this.setValidateHint(true, _('Configuration is valid.'));
|
||||
|
||||
return callSetEnabled(on).then(L.bind(function(setRes) {
|
||||
if (!setRes || !setRes.success) {
|
||||
ui.hideModal();
|
||||
ui.addNotification(null, E('p', _('Failed to update service enable state.')), 'danger');
|
||||
throw new Error('frpc set enabled failed');
|
||||
}
|
||||
|
||||
return callSaveConfig(configText).then(L.bind(function(saveRes) {
|
||||
ui.hideModal();
|
||||
if (saveRes && saveRes.success) {
|
||||
if (on === 0) {
|
||||
return callServiceAction('stop');
|
||||
} else {
|
||||
if (saveRes.applied === false) {
|
||||
ui.addNotification(null, E('p', _('Configuration saved, but service is not running or reload failed. Please force restart service.')), 'warning');
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
ui.addNotification(null, E('p', _('Configuration save failed: ') + (saveRes && saveRes.error ? saveRes.error : _('Unknown error'))), 'danger');
|
||||
throw new Error('frpc config save failed');
|
||||
}, this));
|
||||
}, this));
|
||||
}, this)).catch(function(err) {
|
||||
ui.hideModal();
|
||||
throw err;
|
||||
});
|
||||
},
|
||||
|
||||
handleSave: function() {
|
||||
return this.validateAndSave();
|
||||
},
|
||||
|
||||
handleSaveApply: function() {
|
||||
return this.validateAndSave();
|
||||
}
|
||||
});
|
||||
Symlink
+1
@@ -0,0 +1 @@
|
||||
zh_Hans
|
||||
@@ -0,0 +1,342 @@
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: luci-app-frpc\n"
|
||||
"PO-Revision-Date: 2026-05-19 12:00+0800\n"
|
||||
"Last-Translator: Dante <dante@seckv.com>\n"
|
||||
"Language-Team: Chinese (Simplified)\n"
|
||||
"Language: zh_Hans\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Transfer-Encoding: 8bit\n"
|
||||
|
||||
msgid "Service Status"
|
||||
msgstr "服务运行状态"
|
||||
|
||||
msgid "Current Status"
|
||||
msgstr "当前状态"
|
||||
|
||||
msgid "Adopting process, please wait..."
|
||||
msgstr "正在接管进程,请稍候..."
|
||||
|
||||
msgid "Kill and Adopt Rogue Process"
|
||||
msgstr "杀死并接管孤儿进程"
|
||||
|
||||
msgid "Proxy Health"
|
||||
msgstr "穿透隧道健康度"
|
||||
|
||||
msgid "Configuration View (/var/etc/frpc.toml)"
|
||||
msgstr "运行配置查看 (/var/etc/frpc.toml)"
|
||||
|
||||
msgid "Overview"
|
||||
msgstr "概览"
|
||||
|
||||
msgid "Unknown"
|
||||
msgstr "未知"
|
||||
|
||||
msgid "Running"
|
||||
msgstr "运行中"
|
||||
|
||||
msgid "Running (Unmanaged)"
|
||||
msgstr "运行中 (非托管)"
|
||||
|
||||
msgid "Stopped"
|
||||
msgstr "已停止"
|
||||
|
||||
msgid "Name"
|
||||
msgstr "名称"
|
||||
|
||||
msgid "Type"
|
||||
msgstr "类型"
|
||||
|
||||
msgid "Status"
|
||||
msgstr "状态"
|
||||
|
||||
msgid "Today Traffic (In/Out)"
|
||||
msgstr "今日流量 (入/出)"
|
||||
|
||||
msgid "Error Message"
|
||||
msgstr "报错信息"
|
||||
|
||||
msgid "No data (Service stopped or API not configured)"
|
||||
msgstr "暂无数据 (服务未启动或 API 未配置)"
|
||||
|
||||
msgid "No proxies configured"
|
||||
msgstr "没有配置任何隧道"
|
||||
|
||||
msgid "Frp Client"
|
||||
msgstr "Frp客户端"
|
||||
|
||||
msgid "Open frp Console"
|
||||
msgstr "打开 frp 控制台"
|
||||
|
||||
msgid "Global Settings"
|
||||
msgstr "全局配置"
|
||||
|
||||
msgid "Basic Settings"
|
||||
msgstr "基本设置"
|
||||
|
||||
msgid "Admin API (Required for Hot Reload)"
|
||||
msgstr "管理 API (热重载必须)"
|
||||
|
||||
msgid "Recommended 0.0.0.0 for LAN access, 127.0.0.1 for local only."
|
||||
msgstr "推荐设置 0.0.0.0 以便从 LAN 访问,127.0.0.1 仅限本地访问"
|
||||
|
||||
msgid "Log Settings"
|
||||
msgstr "日志设置"
|
||||
|
||||
msgid "Enable"
|
||||
msgstr "启用"
|
||||
|
||||
msgid "Server Address"
|
||||
msgstr "服务器地址"
|
||||
|
||||
msgid "Server Port"
|
||||
msgstr "服务器端口"
|
||||
|
||||
msgid "Default 7000"
|
||||
msgstr "默认 7000"
|
||||
|
||||
msgid "Token"
|
||||
msgstr "令牌"
|
||||
|
||||
msgid "Transport Protocol"
|
||||
msgstr "传输协议"
|
||||
|
||||
msgid "Admin Address"
|
||||
msgstr "管理地址"
|
||||
|
||||
msgid "Recommended 127.0.0.1"
|
||||
msgstr "推荐 127.0.0.1"
|
||||
|
||||
msgid "Admin Port"
|
||||
msgstr "管理端口"
|
||||
|
||||
msgid "Admin User"
|
||||
msgstr "管理用户名"
|
||||
|
||||
msgid "Admin Password"
|
||||
msgstr "管理密码"
|
||||
|
||||
msgid "Log File"
|
||||
msgstr "日志路径"
|
||||
|
||||
msgid "Leave empty to use system log"
|
||||
msgstr "留空则输出到系统日志"
|
||||
|
||||
msgid "Log Level"
|
||||
msgstr "日志级别"
|
||||
|
||||
msgid "Max Days"
|
||||
msgstr "保留天数"
|
||||
|
||||
msgid "Proxies"
|
||||
msgstr "穿透列表"
|
||||
|
||||
msgid "Local IP"
|
||||
msgstr "本地 IP"
|
||||
|
||||
msgid "Local Port"
|
||||
msgstr "本地端口"
|
||||
|
||||
msgid "Remote Port"
|
||||
msgstr "远程端口"
|
||||
|
||||
msgid "Custom Domains"
|
||||
msgstr "自定义域名"
|
||||
|
||||
msgid "Use Encryption"
|
||||
msgstr "开启加密"
|
||||
|
||||
msgid "Use Compression"
|
||||
msgstr "开启压缩"
|
||||
|
||||
msgid "No logs available..."
|
||||
msgstr "暂无日志..."
|
||||
|
||||
msgid "Real-time Logs"
|
||||
msgstr "实时日志"
|
||||
|
||||
msgid "Log Viewer"
|
||||
msgstr "日志查看"
|
||||
|
||||
msgid "Frp Client Overview"
|
||||
msgstr "Frp客户端概览"
|
||||
|
||||
msgid "Overview of frpc runtime state, generated config, and proxy count."
|
||||
msgstr "展示 frpc 运行状态、生成配置路径与代理数量。"
|
||||
|
||||
msgid "Generated Runtime Config"
|
||||
msgstr "运行配置"
|
||||
|
||||
msgid "Configured Proxies"
|
||||
msgstr "代理数量"
|
||||
|
||||
msgid "Frp Client Settings"
|
||||
msgstr "Frp客户端设置"
|
||||
|
||||
msgid "Configuration saved successfully."
|
||||
msgstr "配置保存成功。"
|
||||
|
||||
msgid "Configuration save failed: "
|
||||
msgstr "配置保存失败:"
|
||||
|
||||
msgid "Unknown error"
|
||||
msgstr "未知错误"
|
||||
|
||||
msgid "Log"
|
||||
msgstr "日志"
|
||||
|
||||
msgid "Verify Configuration"
|
||||
msgstr "校验客户端配置"
|
||||
|
||||
msgid "Configuration is empty."
|
||||
msgstr "配置为空。"
|
||||
|
||||
msgid "Configuration is valid."
|
||||
msgstr "配置校验通过。"
|
||||
|
||||
msgid "Verification failed: "
|
||||
msgstr "配置校验失败:"
|
||||
|
||||
msgid "Service Status"
|
||||
msgstr "服务状态"
|
||||
|
||||
msgid "Logs"
|
||||
msgstr "日志"
|
||||
|
||||
msgid "Frp Client overview of runtime state, generated config, and proxy count."
|
||||
msgstr "Frp Client 运行状态概览,包含生成配置路径和代理数量。"
|
||||
|
||||
msgid "Single TOML editor with syntax highlight, line numbers and error position hints."
|
||||
msgstr "单一 TOML 编辑器,支持语法高亮、行号和错误位置提示。"
|
||||
|
||||
msgid "Line"
|
||||
msgstr "行"
|
||||
|
||||
msgid "Column"
|
||||
msgstr "列"
|
||||
|
||||
msgid "frpc Version"
|
||||
msgstr "frpc 版本"
|
||||
|
||||
|
||||
msgid "frpc config verify failed:\n"
|
||||
msgstr "frpc 配置校验失败:\n"
|
||||
|
||||
msgid "Frp Client Log"
|
||||
msgstr "Frp客户端日志"
|
||||
|
||||
|
||||
msgid "Service Control"
|
||||
msgstr "服务控制"
|
||||
|
||||
msgid "Enabled"
|
||||
msgstr "启用"
|
||||
|
||||
msgid "Actions"
|
||||
msgstr "操作"
|
||||
|
||||
msgid "Start"
|
||||
msgstr "启动"
|
||||
|
||||
msgid "Stop"
|
||||
msgstr "停止"
|
||||
|
||||
msgid "Restart"
|
||||
msgstr "重启"
|
||||
|
||||
msgid "Service enable state updated."
|
||||
msgstr "服务启用状态已更新。"
|
||||
|
||||
msgid "Failed to update service enable state."
|
||||
msgstr "更新服务启用状态失败。"
|
||||
|
||||
msgid "Configuration Editor"
|
||||
msgstr "配置修改"
|
||||
|
||||
msgid "Runtime Details"
|
||||
msgstr "运行详情"
|
||||
|
||||
msgid "Web API"
|
||||
msgstr "Web API"
|
||||
|
||||
msgid "Connected Server"
|
||||
msgstr "连接服务器"
|
||||
|
||||
msgid "Web API Status JSON"
|
||||
msgstr "Web API 状态 JSON"
|
||||
|
||||
msgid "Proxy Runtime List"
|
||||
msgstr "代理运行列表"
|
||||
|
||||
msgid "Local"
|
||||
msgstr "本地地址"
|
||||
|
||||
msgid "Remote"
|
||||
msgstr "远程地址"
|
||||
|
||||
msgid "Error"
|
||||
msgstr "错误"
|
||||
|
||||
msgid "No running proxies"
|
||||
msgstr "暂无运行中的代理"
|
||||
|
||||
msgid "Connect to FRP server"
|
||||
msgstr "连接FRP服务端"
|
||||
|
||||
msgid "Running proxy list"
|
||||
msgstr "运行的代理列表"
|
||||
|
||||
msgid "Web Panel"
|
||||
msgstr "Web面板"
|
||||
|
||||
msgid "Loading logs..."
|
||||
msgstr "日志加载中..."
|
||||
|
||||
msgid "Showing the last 100 lines of frpc service logs."
|
||||
msgstr "显示 frpc 服务最近 100 行日志。"
|
||||
|
||||
msgid "Open"
|
||||
msgstr "打开"
|
||||
|
||||
msgid "Failed to apply service state."
|
||||
msgstr "应用服务状态失败。"
|
||||
|
||||
msgid "Restarting service, please wait..."
|
||||
msgstr "正在重启服务,请稍候..."
|
||||
|
||||
msgid "Restart and Take Over"
|
||||
msgstr "重启并接管"
|
||||
|
||||
msgid "Restart Service"
|
||||
msgstr "重启服务"
|
||||
|
||||
msgid "Force Restart"
|
||||
msgstr "强制重启"
|
||||
|
||||
msgid "Force restarting service, please wait..."
|
||||
msgstr "正在强制重启服务,请稍候..."
|
||||
|
||||
msgid "Hide Preview"
|
||||
msgstr "关闭预览"
|
||||
|
||||
msgid "Configuration saved and applied successfully."
|
||||
msgstr "配置已保存并生效。"
|
||||
|
||||
msgid "Configuration saved, but apply failed. Please force restart service."
|
||||
msgstr "配置已保存,但生效失败,请强制重启服务。"
|
||||
|
||||
msgid "Configuration saved, but service is not running or reload failed. Please force restart service."
|
||||
msgstr "配置已保存,但服务未运行或重载失败,请强制重启服务。"
|
||||
|
||||
msgid "Overview of the local frpc service state, active process PID, loaded config files, and detailed online proxy information."
|
||||
msgstr "展示 frpc 客户端本地服务运行状态、活动进程 PID、当前加载的配置文件以及在线穿透隧道的实时统计与健康度参数。"
|
||||
|
||||
msgid "Configure the Frp client parameters. Complete the server link details and local port forwarding rules to build an intranet penetration channel."
|
||||
msgstr "配置 Frp 客户端的核心参数。支持一键式服务状态启用控制,并提供具备语法高亮、实时语法校验、行号提示的专业级 TOML 配置文件编辑器。"
|
||||
|
||||
msgid "Real-time background system logs of the active frpc process, helping you diagnose server handshake and protocol errors."
|
||||
msgstr "实时查询当前活跃 frpc 进程的后台系统日志,方便您快速诊断与 frp 服务端的握手协议及穿透连接异常。"
|
||||
|
||||
msgid "Frp Client Title"
|
||||
msgstr "Frp 客户端"
|
||||
@@ -0,0 +1,2 @@
|
||||
config frpc 'main'
|
||||
option enabled '0'
|
||||
+25
@@ -0,0 +1,25 @@
|
||||
#!/bin/sh
|
||||
|
||||
[ "$ACTION" = "ifup" ] || exit 0
|
||||
[ "$INTERFACE" = "loopback" ] && exit 0
|
||||
|
||||
# Skip if frpc is not enabled
|
||||
[ "$(uci -q get frpc.main.enabled)" = "1" ] || exit 0
|
||||
|
||||
# Skip if frpc process is already running
|
||||
if pidof /usr/bin/frpc >/dev/null 2>&1 || pidof frpc >/dev/null 2>&1; then
|
||||
exit 0
|
||||
fi
|
||||
|
||||
LOCK=/tmp/frpc_hotplug.lock
|
||||
|
||||
# Debounce: avoid duplicate concurrent starts within 5 seconds
|
||||
if [ -f "$LOCK" ]; then
|
||||
ts=$(cat "$LOCK" 2>/dev/null)
|
||||
[ -n "$ts" ] && [ $(( $(date +%s) - ts )) -lt 5 ] && exit 0
|
||||
fi
|
||||
|
||||
date +%s > "$LOCK"
|
||||
|
||||
logger -t frpc-hotplug "${INTERFACE:-$DEVICE} ifup: frpc is enabled but not running, starting service"
|
||||
/etc/init.d/frpc start >/dev/null 2>&1 &
|
||||
Executable
+50
@@ -0,0 +1,50 @@
|
||||
#!/bin/sh /etc/rc.common
|
||||
|
||||
START=90
|
||||
STOP=10
|
||||
USE_PROCD=1
|
||||
|
||||
PROG=/usr/bin/frpc
|
||||
CONF_FILE=/etc/config/frpc.toml
|
||||
|
||||
is_enabled() {
|
||||
local enabled
|
||||
config_load frpc
|
||||
config_get_bool enabled "main" "enabled" 0
|
||||
[ "$enabled" -eq 1 ]
|
||||
}
|
||||
|
||||
start_service() {
|
||||
is_enabled || return 0
|
||||
|
||||
if [ ! -s "$CONF_FILE" ]; then
|
||||
logger -t frpc "Config file missing or empty: $CONF_FILE"
|
||||
return 1
|
||||
fi
|
||||
|
||||
procd_open_instance
|
||||
procd_set_param command "$PROG" -c "$CONF_FILE"
|
||||
procd_set_param respawn
|
||||
procd_set_param stdout 1
|
||||
procd_set_param stderr 1
|
||||
procd_close_instance
|
||||
}
|
||||
|
||||
reload_service() {
|
||||
if ! is_enabled; then
|
||||
stop
|
||||
return 0
|
||||
fi
|
||||
|
||||
if [ ! -s "$CONF_FILE" ]; then
|
||||
logger -t frpc "Config file missing or empty: $CONF_FILE"
|
||||
stop
|
||||
return 1
|
||||
fi
|
||||
|
||||
restart
|
||||
}
|
||||
|
||||
service_triggers() {
|
||||
procd_add_reload_trigger "frpc"
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
{
|
||||
"admin/services/frpc": {
|
||||
"title": "Frp Client",
|
||||
"order": 90,
|
||||
"action": {
|
||||
"type": "firstchild"
|
||||
},
|
||||
"depends": {
|
||||
"acl": [ "luci-app-frpc" ],
|
||||
"uci": { "frpc": true }
|
||||
}
|
||||
},
|
||||
"admin/services/frpc/overview": {
|
||||
"title": "Overview",
|
||||
"order": 10,
|
||||
"action": {
|
||||
"type": "view",
|
||||
"path": "frpc/overview"
|
||||
}
|
||||
},
|
||||
"admin/services/frpc/settings": {
|
||||
"title": "Settings",
|
||||
"order": 20,
|
||||
"action": {
|
||||
"type": "view",
|
||||
"path": "frpc/settings"
|
||||
}
|
||||
},
|
||||
"admin/services/frpc/log": {
|
||||
"title": "Log",
|
||||
"order": 30,
|
||||
"action": {
|
||||
"type": "view",
|
||||
"path": "frpc/log"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
{
|
||||
"luci-app-frpc": {
|
||||
"description": "Grant access to frpc RPC methods",
|
||||
"read": {
|
||||
"ubus": {
|
||||
"luci.frpc": [
|
||||
"get_status",
|
||||
"get_runtime_info",
|
||||
"get_log",
|
||||
"get_config"
|
||||
]
|
||||
},
|
||||
"uci": [
|
||||
"frpc"
|
||||
]
|
||||
},
|
||||
"write": {
|
||||
"ubus": {
|
||||
"luci.frpc": [
|
||||
"adopt_process",
|
||||
"validate_config",
|
||||
"set_enabled",
|
||||
"service_action",
|
||||
"save_config"
|
||||
]
|
||||
},
|
||||
"uci": [
|
||||
"frpc"
|
||||
],
|
||||
"file": {
|
||||
"/etc/config/frpc.toml": [ "read", "write" ],
|
||||
"/var/etc/frpc.toml": [ "read" ],
|
||||
"/var/log/frpc.log": [ "read" ]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,283 @@
|
||||
'use strict';
|
||||
|
||||
import { connect } from 'ubus';
|
||||
import * as fs from 'fs';
|
||||
|
||||
const PROCD_SERVICE_NAME = 'frpc';
|
||||
const CONFIG_FILE = '/etc/config/frpc.toml';
|
||||
const RUNTIME_CONFIG_FILE = '/etc/config/frpc.toml';
|
||||
|
||||
function get_procd_status() {
|
||||
let u = connect();
|
||||
if (!u)
|
||||
return { running: false };
|
||||
|
||||
let res = u.call('service', 'list', { name: PROCD_SERVICE_NAME });
|
||||
if (res && res[PROCD_SERVICE_NAME] && res[PROCD_SERVICE_NAME].instances) {
|
||||
let inst = res[PROCD_SERVICE_NAME].instances.instance1;
|
||||
if (inst && inst.running)
|
||||
return { running: true, pid: inst.pid };
|
||||
}
|
||||
|
||||
return { running: false };
|
||||
}
|
||||
|
||||
function get_rogue_pid(managed_pid) {
|
||||
let p = fs.popen('pgrep -x frpc', 'r');
|
||||
if (!p)
|
||||
return null;
|
||||
|
||||
let pid_str = p.read('all');
|
||||
p.close();
|
||||
|
||||
if (!pid_str)
|
||||
return null;
|
||||
|
||||
let pids = pid_str.trim().split('\n');
|
||||
for (let i = 0; i < length(pids); i++) {
|
||||
let pid = pids[i];
|
||||
if (pid != '' && (managed_pid == null || pid != (managed_pid + '')))
|
||||
return pid;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
function count_proxies(config) {
|
||||
if (!config)
|
||||
return 0;
|
||||
|
||||
let parts = split(config, '[[proxies]]');
|
||||
return (parts && length(parts) > 0) ? (length(parts) - 1) : 0;
|
||||
}
|
||||
|
||||
function verify_config(config) {
|
||||
fs.writefile('/tmp/frpc_test.toml', config || '');
|
||||
let p = fs.popen("/usr/bin/frpc verify -c /tmp/frpc_test.toml 2>&1", 'r');
|
||||
if (!p) {
|
||||
fs.unlink('/tmp/frpc_test.toml');
|
||||
return { success: false, error: 'failed to validate config' };
|
||||
}
|
||||
|
||||
let output = p.read('all') || '';
|
||||
let code = p.close();
|
||||
fs.unlink('/tmp/frpc_test.toml');
|
||||
|
||||
if (code !== 0)
|
||||
return { success: false, error: output || 'verify failed' };
|
||||
|
||||
return { success: true };
|
||||
}
|
||||
|
||||
function get_frpc_version() {
|
||||
system("/bin/sh -c '/usr/bin/frpc --version >/tmp/frpc_version.out 2>&1 || /usr/bin/frpc -v >/tmp/frpc_version.out 2>&1'");
|
||||
let out = fs.readfile('/tmp/frpc_version.out') || '';
|
||||
fs.unlink('/tmp/frpc_version.out');
|
||||
|
||||
if (!out)
|
||||
return '';
|
||||
|
||||
let line = split(out, '\n')[0] || '';
|
||||
if (!line)
|
||||
return '';
|
||||
line = replace(line, /^[ \t\r\n]+/, '');
|
||||
line = replace(line, /[ \t\r\n]+$/, '');
|
||||
return line;
|
||||
}
|
||||
|
||||
function get_enabled() {
|
||||
let p = fs.popen("/bin/sh -c \"uci -q get frpc.main.enabled 2>/dev/null || echo 0\"", 'r');
|
||||
if (!p)
|
||||
return 0;
|
||||
let out = p.read('all') || '0';
|
||||
p.close();
|
||||
out = replace(out, /^[ \t\r\n]+/, '');
|
||||
out = replace(out, /[ \t\r\n]+$/, '');
|
||||
return (out === '1') ? 1 : 0;
|
||||
}
|
||||
|
||||
function toml_get(key, defv) {
|
||||
let p = fs.popen("/bin/sh -c \"grep -F '" + key + "' " + CONFIG_FILE + " 2>/dev/null | head -n1 | cut -d= -f2-\"", 'r');
|
||||
if (!p)
|
||||
return defv;
|
||||
|
||||
let out = p.read('all') || '';
|
||||
p.close();
|
||||
out = replace(out, /^[ \t\r\n]+/, '');
|
||||
out = replace(out, /[ \t\r\n]+$/, '');
|
||||
out = replace(out, /^\"/, '');
|
||||
out = replace(out, /\"$/, '');
|
||||
return out ? out : defv;
|
||||
}
|
||||
|
||||
function get_runtime_info() {
|
||||
let status = get_procd_status();
|
||||
if (!status.running)
|
||||
return { running: false };
|
||||
|
||||
let addr = toml_get('webServer.addr', '127.0.0.1');
|
||||
let port = toml_get('webServer.port', '7400');
|
||||
let user = toml_get('webServer.user', '');
|
||||
let pass = toml_get('webServer.password', '');
|
||||
let auth = (user && pass) ? ("-u '" + user + ":" + pass + "' ") : '';
|
||||
|
||||
system("/bin/sh -c \"curl -s " + auth + "http://" + addr + ":" + port + "/api/status >/tmp/frpc_web_status.json 2>/tmp/frpc_web_err\"");
|
||||
system("/bin/sh -c \"curl -s " + auth + "http://" + addr + ":" + port + "/api/config >/tmp/frpc_web_config.txt 2>>/tmp/frpc_web_err\"");
|
||||
|
||||
let raw_status = fs.readfile('/tmp/frpc_web_status.json') || '';
|
||||
let raw_config = fs.readfile('/tmp/frpc_web_config.txt') || '';
|
||||
let raw_err = fs.readfile('/tmp/frpc_web_err') || '';
|
||||
fs.unlink('/tmp/frpc_web_status.json');
|
||||
fs.unlink('/tmp/frpc_web_config.txt');
|
||||
fs.unlink('/tmp/frpc_web_err');
|
||||
|
||||
let status_obj = {};
|
||||
if (raw_status) {
|
||||
try { status_obj = json(raw_status); } catch (e) {}
|
||||
}
|
||||
|
||||
let server_addr = '';
|
||||
let server_port = '';
|
||||
if (raw_config) {
|
||||
let m1 = match(raw_config, /serverAddr[ \t]*=[ \t]*\"([^\"]+)\"/);
|
||||
let m2 = match(raw_config, /serverPort[ \t]*=[ \t]*([0-9]+)/);
|
||||
if (m1 && length(m1) > 1) server_addr = m1[1];
|
||||
if (m2 && length(m2) > 1) server_port = m2[1];
|
||||
}
|
||||
|
||||
let lan_ip = '';
|
||||
let u = connect();
|
||||
if (u) {
|
||||
let lan_st = u.call('network.interface.lan', 'status', {});
|
||||
if (lan_st && lan_st['ipv4-address'] && length(lan_st['ipv4-address']) > 0)
|
||||
lan_ip = lan_st['ipv4-address'][0].address || '';
|
||||
}
|
||||
|
||||
let web_host = addr;
|
||||
if (addr == '0.0.0.0' || addr == '127.0.0.1' || addr == '::')
|
||||
web_host = lan_ip || '127.0.0.1';
|
||||
let web_url = 'http://' + web_host + ':' + port;
|
||||
|
||||
return {
|
||||
running: true,
|
||||
web_addr: addr,
|
||||
web_port: int(port),
|
||||
web_url: web_url,
|
||||
server_addr: server_addr,
|
||||
server_port: server_port ? int(server_port) : 0,
|
||||
raw_status: raw_status,
|
||||
status: status_obj,
|
||||
error: raw_err
|
||||
};
|
||||
}
|
||||
|
||||
const methods = {
|
||||
get_status: {
|
||||
call: function() {
|
||||
let procd_st = get_procd_status();
|
||||
let rogue_pid = get_rogue_pid(procd_st.pid);
|
||||
|
||||
let state = 'stopped';
|
||||
if (procd_st.running)
|
||||
state = 'managed';
|
||||
else if (rogue_pid)
|
||||
state = 'unmanaged';
|
||||
|
||||
let config = fs.readfile(CONFIG_FILE) || '';
|
||||
|
||||
return {
|
||||
state: state,
|
||||
pid: procd_st.pid || rogue_pid,
|
||||
config_exists: !!fs.stat(CONFIG_FILE),
|
||||
runtime_config: RUNTIME_CONFIG_FILE,
|
||||
proxy_count: count_proxies(config),
|
||||
version: get_frpc_version(),
|
||||
enabled: get_enabled()
|
||||
};
|
||||
}
|
||||
},
|
||||
|
||||
adopt_process: {
|
||||
call: function() {
|
||||
fs.popen('killall -9 frpc', 'r').close();
|
||||
let rc = system('/etc/init.d/frpc start >/dev/null 2>&1');
|
||||
return { success: (rc === 0) };
|
||||
}
|
||||
},
|
||||
|
||||
get_log: {
|
||||
call: function() {
|
||||
let p = fs.popen('logread -e frpc | tail -n 100', 'r');
|
||||
if (!p)
|
||||
return { log: '' };
|
||||
let log = p.read('all');
|
||||
p.close();
|
||||
return { log: log || '' };
|
||||
}
|
||||
},
|
||||
|
||||
get_runtime_info: {
|
||||
call: function() {
|
||||
return get_runtime_info();
|
||||
}
|
||||
},
|
||||
|
||||
get_config: {
|
||||
call: function() {
|
||||
if (!fs.stat(CONFIG_FILE))
|
||||
return { config: '' };
|
||||
return { config: fs.readfile(CONFIG_FILE) || '' };
|
||||
}
|
||||
},
|
||||
|
||||
validate_config: {
|
||||
args: { config: '' },
|
||||
call: function(req) {
|
||||
let config = req.args?.config ?? '';
|
||||
return verify_config(config);
|
||||
}
|
||||
},
|
||||
|
||||
set_enabled: {
|
||||
args: { enabled: 0 },
|
||||
call: function(req) {
|
||||
let enabled = (int(req.args?.enabled ?? 0) === 1) ? '1' : '0';
|
||||
let cmd = "/bin/sh -c \"uci -q set frpc.main.enabled=" + enabled + " && uci -q commit frpc\"";
|
||||
let rc = system(cmd);
|
||||
return { success: (rc === 0), enabled: int(enabled) };
|
||||
}
|
||||
},
|
||||
|
||||
service_action: {
|
||||
args: { action: '' },
|
||||
call: function(req) {
|
||||
let action = req.args?.action ?? '';
|
||||
if (action != 'start' && action != 'stop' && action != 'restart' && action != 'reload')
|
||||
return { success: false, error: 'invalid action' };
|
||||
let rc = system('/etc/init.d/frpc ' + action + ' >/dev/null 2>&1');
|
||||
return { success: (rc === 0), applied: (rc === 0) };
|
||||
}
|
||||
},
|
||||
|
||||
save_config: {
|
||||
args: { config: '' },
|
||||
call: function(req) {
|
||||
let config = req.args?.config ?? '';
|
||||
let vr = verify_config(config);
|
||||
if (!vr.success)
|
||||
return vr;
|
||||
|
||||
if (!fs.writefile(CONFIG_FILE, config))
|
||||
return { success: false, error: 'unable to write config file (/etc/config/frpc.toml)' };
|
||||
|
||||
let rc = system('/etc/init.d/frpc reload >/dev/null 2>&1');
|
||||
return {
|
||||
success: true,
|
||||
saved: true,
|
||||
applied: (rc === 0)
|
||||
};
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
return { 'luci.frpc': methods };
|
||||
Reference in new issue
Block a user