mirror of
https://github.com/openwrt/luci.git
synced 2026-10-14 06:56:45 +04:00
luci-app-weechat: add new application
Add a LuCI web interface for the WeeChat IRC client.
- Status page: service state and version, boot autostart toggle and
start, stop and restart through the rc and service ubus objects, with
read-only handling for users without write access
- IRC Servers page: grid with modal tabs for connection, identity,
authentication (none, NickServ, SASL PLAIN, SCRAM-SHA-256 and
SCRAM-SHA-512) and auto-join channels with optional keys
- Relay page: relay port and listen address, TLS, password, chat
logging and a link to the hosted Glowing Bear web client. After a
certificate is generated, a running WeeChat is restarted if the saved
settings use it.
- rpcd ucode plugin "weechat", which runs as root. It reports the
daemon version, enables the service by committing only option
enabled, and checks or generates a self-signed relay certificate with
an EC key in /etc/weechat/tls, preferring openssl over px5g. The
bundle is installed as root:weechat 0640, so the daemon can read but
not replace it. Certificates at other paths are left to the user.
- The forms refuse the values that the init script of the weechat
package rejects.
- ACL limited to the weechat UCI config and the methods above
Signed-off-by: Josef Schlehofer <pepe.schlehofer@gmail.com>
(cherry picked from commit a29ca05b02)
This commit is contained in:
10 files changed
+2414
No files matched your search
@@ -0,0 +1,13 @@
|
||||
# This is free software, licensed under the Apache License, Version 2.0 .
|
||||
|
||||
include $(TOPDIR)/rules.mk
|
||||
|
||||
LUCI_TITLE:=LuCI support for WeeChat IRC client
|
||||
LUCI_DEPENDS:=+luci-base +weechat +rpcd-mod-ucode
|
||||
|
||||
PKG_LICENSE:=Apache-2.0
|
||||
PKG_MAINTAINER:=Josef Schlehofer <pepe.schlehofer@gmail.com>
|
||||
|
||||
include ../../luci.mk
|
||||
|
||||
# call BuildPackage - OpenWrt buildroot signature
|
||||
@@ -0,0 +1,27 @@
|
||||
'use strict';
|
||||
'require baseclass';
|
||||
|
||||
return baseclass.extend({
|
||||
parseAutoJoin: function(val) {
|
||||
if (!val || !val.trim())
|
||||
return { channels: [], keys: [] };
|
||||
|
||||
var trimmed = val.trim();
|
||||
var spaceIdx = trimmed.search(/\s/);
|
||||
if (spaceIdx === -1) {
|
||||
return {
|
||||
channels: trimmed.split(',').map(function(c) { return c.trim(); }).filter(Boolean),
|
||||
keys: []
|
||||
};
|
||||
}
|
||||
|
||||
var firstPart = trimmed.substring(0, spaceIdx).trim();
|
||||
var rest = trimmed.substring(spaceIdx + 1).trim();
|
||||
|
||||
/* Keys are positional, so keep empty ones to stay aligned with channels */
|
||||
return {
|
||||
channels: firstPart.split(',').map(function(c) { return c.trim(); }).filter(Boolean),
|
||||
keys: rest.length ? rest.split(',').map(function(k) { return k.trim(); }) : []
|
||||
};
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,412 @@
|
||||
'use strict';
|
||||
'require view';
|
||||
'require form';
|
||||
'require rpc';
|
||||
'require uci';
|
||||
'require ui';
|
||||
|
||||
var callGetCertStatus = rpc.declare({
|
||||
object: 'weechat',
|
||||
method: 'get_certificate_status',
|
||||
params: [ 'path' ],
|
||||
expect: { '': {} }
|
||||
});
|
||||
|
||||
var callGenerateCert = rpc.declare({
|
||||
object: 'weechat',
|
||||
method: 'generate_certificate',
|
||||
params: [ 'cn', 'san', 'path' ],
|
||||
expect: { '': {} }
|
||||
});
|
||||
|
||||
var callServiceList = rpc.declare({
|
||||
object: 'service',
|
||||
method: 'list',
|
||||
params: [ 'name' ],
|
||||
expect: { '': {} }
|
||||
});
|
||||
|
||||
var callRcInit = rpc.declare({
|
||||
object: 'rc',
|
||||
method: 'init',
|
||||
params: [ 'name', 'action' ]
|
||||
});
|
||||
|
||||
/* Mirrors check_path() in the init script, which refuses to start otherwise */
|
||||
function validatePath(value, allowPercent) {
|
||||
if (!value)
|
||||
return true;
|
||||
if (value.charAt(0) !== '/' || /[\r\n]/.test(value) || /(^|\/)\.\.?(\/|$)/.test(value))
|
||||
return _('Must be an absolute path without . or .. components');
|
||||
if (value !== value.trim())
|
||||
return _('Must not start or end with a space');
|
||||
if (value.indexOf('${') !== -1)
|
||||
return _('Must not contain "${"');
|
||||
if (!allowPercent && value.indexOf('%') !== -1)
|
||||
return _('Must not contain "%"');
|
||||
return true;
|
||||
}
|
||||
|
||||
/* WeeChat only reads the certificate when it starts, so restart a running relay */
|
||||
function restartIfRunning() {
|
||||
return callServiceList('weechat').then(function(res) {
|
||||
var instances = (res && res.weechat && res.weechat.instances) || {};
|
||||
var running = Object.keys(instances).some(function(k) { return instances[k].running; });
|
||||
|
||||
if (!running)
|
||||
return false;
|
||||
|
||||
return callRcInit('weechat', 'restart').then(function(ret) {
|
||||
if (ret)
|
||||
throw new Error(_('Command failed with return code %d').format(ret));
|
||||
return true;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
return view.extend({
|
||||
load: function() {
|
||||
return uci.load('weechat');
|
||||
},
|
||||
|
||||
render: function() {
|
||||
var m, s, o;
|
||||
var defaultPath = '/etc/weechat/tls/relay.pem';
|
||||
var refreshCertStatus = null;
|
||||
|
||||
/* Only certificates in this directory are checked and generated here */
|
||||
function isManagedPath(path) {
|
||||
return /^\/etc\/weechat\/tls\/[A-Za-z0-9._-]+\.pem$/.test(path) && path.indexOf('..') === -1;
|
||||
}
|
||||
|
||||
/* IPv6 literals come with brackets, which are not valid in a SAN */
|
||||
var hostname = (window.location.hostname || 'OpenWrt').replace(/^\[(.*)\]$/, '$1');
|
||||
var defaultSan = hostname;
|
||||
|
||||
m = new form.Map('weechat', _('WeeChat - Relay'),
|
||||
_('Configure the WeeChat relay and chat logging.'));
|
||||
|
||||
/* Relay */
|
||||
s = m.section(form.NamedSection, 'weechat', 'weechat', _('Relay'));
|
||||
|
||||
var optRelayEnabled = s.option(form.Flag, 'relay_enabled', _('Enable Relay Server'));
|
||||
optRelayEnabled.default = optRelayEnabled.disabled;
|
||||
optRelayEnabled.rmempty = false;
|
||||
|
||||
o = s.option(form.Value, 'relay_address', _('Listen Address'),
|
||||
_('IP address to bind (leave empty for all interfaces). The address must exist when WeeChat starts.'));
|
||||
o.datatype = 'ipaddr("nomask")';
|
||||
o.placeholder = _('All interfaces');
|
||||
/* WeeChat cannot bind to it and the init script refuses it */
|
||||
o.validate = function(section_id, value) {
|
||||
var m = (value || '').match(/(?:^|:)(\d+\.\d+\.\d+\.\d+)$/);
|
||||
if (m && m[1].split('.').some(function(p) { return p.length > 1 && p.charAt(0) === '0'; }))
|
||||
return _('Leading zeros are not allowed in an IPv4 address');
|
||||
return true;
|
||||
};
|
||||
o.rmempty = true;
|
||||
o.depends('relay_enabled', '1');
|
||||
|
||||
o = s.option(form.Value, 'relay_port', _('Port'),
|
||||
_('Port for relay connections (WebSocket).'));
|
||||
o.datatype = 'and(port,min(1))';
|
||||
o.default = '9000';
|
||||
o.placeholder = '9000';
|
||||
o.depends('relay_enabled', '1');
|
||||
|
||||
/* Security */
|
||||
s = m.section(form.NamedSection, 'weechat', 'weechat', _('Security'));
|
||||
|
||||
o = s.option(form.Flag, 'relay_tls', _('Enable TLS'),
|
||||
_('Encrypt relay connections with TLS.'));
|
||||
o.default = o.disabled;
|
||||
|
||||
var optCertKey = s.option(form.Value, 'relay_cert_key', _('Certificate'),
|
||||
_('PEM file containing the certificate and private key. It must be readable by the weechat user.'));
|
||||
optCertKey.placeholder = defaultPath;
|
||||
optCertKey.rmempty = true;
|
||||
optCertKey.depends('relay_tls', '1');
|
||||
optCertKey.validate = function(section_id, value) {
|
||||
return validatePath(value, true);
|
||||
};
|
||||
optCertKey.onchange = function() {
|
||||
if (refreshCertStatus)
|
||||
refreshCertStatus();
|
||||
};
|
||||
|
||||
/* An empty field means the default path, as in the init script */
|
||||
function getActiveCertPath() {
|
||||
var val = optCertKey.formvalue('weechat');
|
||||
if (val == null)
|
||||
val = uci.get('weechat', 'weechat', 'relay_cert_key');
|
||||
return (val || '').trim() || defaultPath;
|
||||
}
|
||||
|
||||
/* Certificate status widget */
|
||||
var optCertStatus = s.option(form.DummyValue, '_cert_status', _('Certificate status'));
|
||||
optCertStatus.depends('relay_tls', '1');
|
||||
|
||||
optCertStatus.renderWidget = function(section_id, option_index, cfgvalue) {
|
||||
var container = E('div', { 'class': 'cbi-value-field' });
|
||||
var isReadonly = (this.readonly != null) ? this.readonly : this.map.readonly;
|
||||
|
||||
function updateWidget(path) {
|
||||
while (container.firstChild)
|
||||
container.removeChild(container.firstChild);
|
||||
|
||||
if (!isManagedPath(path)) {
|
||||
container.appendChild(E('div', { 'class': 'alert-message notice' },
|
||||
_('This certificate is managed outside of LuCI, so it is not checked here. The service refuses to start if the file does not exist.')));
|
||||
return;
|
||||
}
|
||||
|
||||
container.appendChild(E('em', {}, _('Checking certificate status…')));
|
||||
|
||||
callGetCertStatus(path || defaultPath).then(function(res) {
|
||||
while (container.firstChild)
|
||||
container.removeChild(container.firstChild);
|
||||
|
||||
var elements = [];
|
||||
|
||||
if (res && res.exists && res.has_cert && res.has_key) {
|
||||
elements.push(E('div', {}, [
|
||||
E('span', { 'class': 'label success' }, [ '● ', _('Certificate and key present') ])
|
||||
]));
|
||||
|
||||
elements.push(E('p', { 'class': 'cbi-value-description' }, [
|
||||
E('strong', {}, _('File: ')),
|
||||
E('code', {}, res.path)
|
||||
]));
|
||||
|
||||
elements.push(E('button', {
|
||||
'class': 'btn cbi-button cbi-button-action',
|
||||
'type': 'button',
|
||||
'disabled': isReadonly || null,
|
||||
'click': function(ev) {
|
||||
ev.preventDefault();
|
||||
showGenerateDialog(getActiveCertPath(), res.generator_available);
|
||||
}
|
||||
}, _('Generate new certificate')));
|
||||
} else if (res && res.exists && (!res.has_cert || !res.has_key)) {
|
||||
elements.push(E('div', { 'class': 'alert-message warning' },
|
||||
_('Certificate file exists but is incomplete (must contain both certificate and private key).')));
|
||||
|
||||
elements.push(E('button', {
|
||||
'class': 'btn cbi-button cbi-button-action',
|
||||
'type': 'button',
|
||||
'disabled': isReadonly || null,
|
||||
'click': function(ev) {
|
||||
ev.preventDefault();
|
||||
showGenerateDialog(getActiveCertPath(), res.generator_available);
|
||||
}
|
||||
}, _('Generate self-signed certificate')));
|
||||
} else {
|
||||
elements.push(E('div', { 'class': 'alert-message warning' },
|
||||
_('TLS is enabled but no certificate file was found.')));
|
||||
|
||||
elements.push(E('button', {
|
||||
'class': 'btn cbi-button cbi-button-action',
|
||||
'type': 'button',
|
||||
'disabled': isReadonly || null,
|
||||
'click': function(ev) {
|
||||
ev.preventDefault();
|
||||
showGenerateDialog(getActiveCertPath(), res && res.generator_available);
|
||||
}
|
||||
}, _('Generate self-signed certificate')));
|
||||
}
|
||||
|
||||
if (res && res.generator_available === false) {
|
||||
elements.push(E('div', { 'class': 'alert-message warning' },
|
||||
_('Certificate generation requires the %s or %s utility.').format('px5g', 'openssl')));
|
||||
}
|
||||
|
||||
for (var i = 0; i < elements.length; i++)
|
||||
container.appendChild(elements[i]);
|
||||
}).catch(function(err) {
|
||||
while (container.firstChild)
|
||||
container.removeChild(container.firstChild);
|
||||
container.appendChild(E('div', { 'class': 'alert-message error' },
|
||||
_('Unable to verify certificate status: %s').format(err.message || err)));
|
||||
});
|
||||
}
|
||||
|
||||
function showGenerateDialog(targetPath, generatorAvailable) {
|
||||
var inputCn, inputSan, btnGenerate, btnCancel, msgBox;
|
||||
|
||||
var modalContent = [
|
||||
E('div', { 'class': 'alert-message notice' }, [
|
||||
E('p', {},
|
||||
_('A self-signed certificate will be generated for WeeChat Relay at %s.').format(targetPath)),
|
||||
E('p', {},
|
||||
E('strong', {}, _('Self-signed certificates provide encryption but are not trusted automatically by browsers.'))),
|
||||
E('p', {},
|
||||
_('For Internet-facing deployments, use a certificate issued by a trusted CA.'))
|
||||
]),
|
||||
|
||||
E('h5', {}, _('Certificate identity')),
|
||||
|
||||
E('div', { 'class': 'cbi-value' }, [
|
||||
E('label', { 'class': 'cbi-value-title' }, _('Common name')),
|
||||
E('div', { 'class': 'cbi-value-field' }, [
|
||||
inputCn = E('input', {
|
||||
'type': 'text',
|
||||
'class': 'cbi-input-text',
|
||||
'value': hostname,
|
||||
'placeholder': 'router.local'
|
||||
}),
|
||||
E('div', { 'class': 'cbi-value-description' },
|
||||
_('Host name identifying this router.'))
|
||||
])
|
||||
]),
|
||||
|
||||
E('div', { 'class': 'cbi-value' }, [
|
||||
E('label', { 'class': 'cbi-value-title' }, _('Subject alternative names')),
|
||||
E('div', { 'class': 'cbi-value-field' }, [
|
||||
inputSan = E('input', {
|
||||
'type': 'text',
|
||||
'class': 'cbi-input-text',
|
||||
'value': defaultSan,
|
||||
'placeholder': 'router.local, 192.168.1.1'
|
||||
}),
|
||||
E('div', { 'class': 'cbi-value-description' },
|
||||
_('Comma-separated list of host names and IP addresses used to reach the relay.'))
|
||||
])
|
||||
]),
|
||||
|
||||
msgBox = E('div', {}),
|
||||
|
||||
E('div', { 'class': 'right' }, [
|
||||
btnCancel = E('button', {
|
||||
'class': 'btn cbi-button cbi-button-neutral',
|
||||
'click': ui.hideModal
|
||||
}, _('Cancel')),
|
||||
' ',
|
||||
btnGenerate = E('button', {
|
||||
'class': 'btn cbi-button cbi-button-action',
|
||||
'click': function(ev) {
|
||||
ev.preventDefault();
|
||||
var cnVal = inputCn.value.trim();
|
||||
var sanVal = inputSan.value.trim();
|
||||
|
||||
if (!cnVal) {
|
||||
ui.addNotification(null, E('p', _('Please enter a valid Common Name.')), 'warning');
|
||||
return;
|
||||
}
|
||||
|
||||
btnGenerate.disabled = true;
|
||||
btnCancel.disabled = true;
|
||||
btnGenerate.textContent = _('Generating…');
|
||||
|
||||
while (msgBox.firstChild)
|
||||
msgBox.removeChild(msgBox.firstChild);
|
||||
msgBox.appendChild(E('p', { 'class': 'spinning' }, _('Generating self-signed certificate…')));
|
||||
|
||||
callGenerateCert(cnVal, sanVal, targetPath).then(function(res) {
|
||||
if (res && res.success) {
|
||||
ui.hideModal();
|
||||
updateWidget(getActiveCertPath());
|
||||
/* Only the saved relay settings decide which certificate WeeChat loads */
|
||||
(res.in_use ? restartIfRunning() : Promise.resolve(null)).then(function(restarted) {
|
||||
var msg;
|
||||
if (restarted === true)
|
||||
msg = _('Self-signed certificate generated for %s. WeeChat was restarted to use it.');
|
||||
else if (restarted === false)
|
||||
msg = _('Self-signed certificate generated for %s. WeeChat uses it from its next start.');
|
||||
else
|
||||
msg = _('Self-signed certificate generated for %s. Save and apply TLS with this certificate to use it.');
|
||||
ui.addNotification(null, E('p', msg.format(cnVal)), 'info');
|
||||
}).catch(function(err) {
|
||||
ui.addNotification(null, E('p',
|
||||
_('Self-signed certificate generated for %s, but WeeChat could not be restarted: %s').format(cnVal, err.message || err)),
|
||||
'warning');
|
||||
});
|
||||
} else {
|
||||
btnGenerate.disabled = false;
|
||||
btnCancel.disabled = false;
|
||||
btnGenerate.textContent = _('Generate');
|
||||
while (msgBox.firstChild)
|
||||
msgBox.removeChild(msgBox.firstChild);
|
||||
msgBox.appendChild(E('div', { 'class': 'alert-message error' },
|
||||
res && res.error ? res.error : _('Certificate generation failed.')));
|
||||
}
|
||||
}).catch(function(err) {
|
||||
btnGenerate.disabled = false;
|
||||
btnCancel.disabled = false;
|
||||
btnGenerate.textContent = _('Generate');
|
||||
while (msgBox.firstChild)
|
||||
msgBox.removeChild(msgBox.firstChild);
|
||||
msgBox.appendChild(E('div', { 'class': 'alert-message error' },
|
||||
err.message || err));
|
||||
});
|
||||
}
|
||||
}, _('Generate'))
|
||||
])
|
||||
];
|
||||
|
||||
if (generatorAvailable === false) {
|
||||
btnGenerate.disabled = true;
|
||||
msgBox.appendChild(E('div', { 'class': 'alert-message error' },
|
||||
_('Cannot generate certificate: neither %s nor %s is installed on this router.').format('px5g', 'openssl')));
|
||||
}
|
||||
|
||||
ui.showModal(_('Generate certificate?'), modalContent);
|
||||
}
|
||||
|
||||
refreshCertStatus = function() {
|
||||
updateWidget(getActiveCertPath());
|
||||
};
|
||||
|
||||
/* The field is not in the page yet while rendering, start from its stored value */
|
||||
updateWidget(String(optCertKey.cfgvalue(section_id) || '').trim() || defaultPath);
|
||||
|
||||
return container;
|
||||
};
|
||||
|
||||
o = s.option(form.Value, 'relay_password', _('Relay Password'),
|
||||
_('Password required by clients to connect.'));
|
||||
o.password = true;
|
||||
o.validate = function(section_id, value) {
|
||||
var formVal = optRelayEnabled ? optRelayEnabled.formvalue(section_id) : null;
|
||||
var isEnabled = (formVal !== null) ? (formVal === '1') : (uci.get('weechat', section_id, 'relay_enabled') === '1');
|
||||
if (isEnabled && (!value || !value.trim())) {
|
||||
return _('Relay password is required when relay server is enabled.');
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
/* Logging */
|
||||
s = m.section(form.NamedSection, 'weechat', 'weechat', _('Logging'));
|
||||
|
||||
o = s.option(form.Flag, 'log_enabled', _('Enable Logging'),
|
||||
_('Record chat logs to disk. Disabled by default to protect flash memory.'));
|
||||
o.default = o.disabled;
|
||||
|
||||
o = s.option(form.Value, 'log_dir', _('Log Directory'),
|
||||
_('Directory where WeeChat stores chat logs. The default /var/log/weechat is created automatically, any other directory must exist and be writable by the weechat user.'));
|
||||
o.placeholder = '/var/log/weechat';
|
||||
o.depends('log_enabled', '1');
|
||||
o.validate = function(section_id, value) {
|
||||
return validatePath(value, false);
|
||||
};
|
||||
|
||||
/* Web Client */
|
||||
s = m.section(form.NamedSection, 'weechat', 'weechat', _('Web Client'));
|
||||
|
||||
o = s.option(form.DummyValue, '_gb_info');
|
||||
o.cfgvalue = function() {
|
||||
return E('div', {}, [
|
||||
E('p', { 'style': 'margin-bottom: 12px; line-height: 1.5;' },
|
||||
_('Glowing Bear is a web client that connects to WeeChat using the relay configured above.')),
|
||||
E('a', {
|
||||
'class': 'btn cbi-button cbi-button-action',
|
||||
'href': 'https://www.glowing-bear.org/',
|
||||
'target': '_blank',
|
||||
'rel': 'noreferrer noopener',
|
||||
'style': 'margin-bottom: 6px; display: inline-block;'
|
||||
}, [ _('Open Glowing Bear (Hosted)'), ' ↗' ])
|
||||
]);
|
||||
};
|
||||
|
||||
return m.render();
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,376 @@
|
||||
'use strict';
|
||||
'require view';
|
||||
'require form';
|
||||
'require uci';
|
||||
'require ui';
|
||||
'require tools.weechat as weechatTools';
|
||||
|
||||
/* WeeChat evaluates these options, the init script refuses "${" in them */
|
||||
function validateNoExpr(section_id, value) {
|
||||
if (value && value.indexOf('${') !== -1)
|
||||
return _('Must not contain "${"');
|
||||
return true;
|
||||
}
|
||||
|
||||
function checkAutoJoinRows(rows) {
|
||||
for (var i = 0; i < rows.length; i++) {
|
||||
var chanInput = rows[i].querySelector('.chan-input');
|
||||
var keyInput = rows[i].querySelector('.key-input');
|
||||
var chan = (chanInput ? chanInput.value : '').trim();
|
||||
var key = (keyInput ? keyInput.value : '').trim();
|
||||
|
||||
if (!chan && key)
|
||||
return _('Channel key cannot be specified without a channel name');
|
||||
if (chan && (chan.indexOf(',') !== -1 || /\s/.test(chan)))
|
||||
return _('Channel name "%s" cannot contain commas or spaces').format(chan);
|
||||
if (key && (key.indexOf(',') !== -1 || /\s/.test(key)))
|
||||
return _('Channel key cannot contain commas or spaces');
|
||||
if (validateNoExpr(null, chan) !== true || validateNoExpr(null, key) !== true)
|
||||
return _('Channels and keys must not contain "${"');
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
var CBIAutoJoin = form.Value.extend({
|
||||
validate: function(section_id, value) {
|
||||
var el = document.getElementById(this.cbid(section_id));
|
||||
if (!el || !el.parentNode)
|
||||
return true;
|
||||
|
||||
var res = checkAutoJoinRows(el.parentNode.querySelectorAll('tr[data-channel-row]'));
|
||||
|
||||
/* The modal shows no message for this widget, so show it here */
|
||||
var box = el.parentNode.querySelector('.autojoin-error');
|
||||
if (box) {
|
||||
box.textContent = (res === true) ? '' : res;
|
||||
box.style.display = (res === true) ? 'none' : '';
|
||||
}
|
||||
|
||||
return res;
|
||||
},
|
||||
|
||||
/*
|
||||
* The value lives in a hidden field, which never runs validation on its
|
||||
* own, so let parse() see the result of validate() through these.
|
||||
*/
|
||||
isValid: function(section_id) {
|
||||
return this.validate(section_id, this.formvalue(section_id)) === true;
|
||||
},
|
||||
|
||||
getValidationError: function(section_id) {
|
||||
var res = this.validate(section_id, this.formvalue(section_id));
|
||||
return (res === true) ? '' : res;
|
||||
},
|
||||
|
||||
textvalue: function(section_id) {
|
||||
var val = this.cfgvalue(section_id);
|
||||
var parsed = weechatTools.parseAutoJoin(val);
|
||||
if (parsed.channels.length === 0)
|
||||
return null;
|
||||
|
||||
var nodes = [];
|
||||
parsed.channels.forEach(function(chan, idx) {
|
||||
if (idx > 0)
|
||||
nodes.push(', ');
|
||||
nodes.push(E('code', {}, chan));
|
||||
});
|
||||
return E('span', {}, nodes);
|
||||
},
|
||||
|
||||
renderWidget: function(section_id, option_index, cfgvalue) {
|
||||
var isReadonly = (this.readonly != null) ? this.readonly : this.map.readonly;
|
||||
var hidden = new ui.Hiddenfield((cfgvalue != null) ? cfgvalue : '', {
|
||||
id: this.cbid(section_id)
|
||||
});
|
||||
|
||||
var table = E('table', { 'class': 'table cbi-section-table' }, [
|
||||
E('tr', { 'class': 'tr cbi-section-table-titles' }, [
|
||||
E('th', { 'class': 'th cbi-section-table-cell' }, _('Channel')),
|
||||
E('th', { 'class': 'th cbi-section-table-cell' }, _('Key')),
|
||||
E('th', { 'class': 'th cbi-section-table-cell cbi-section-actions' }, '')
|
||||
])
|
||||
]);
|
||||
|
||||
function updateValue() {
|
||||
var rows = table.querySelectorAll('tr[data-channel-row]');
|
||||
var chansWithKeys = [];
|
||||
var chansWithoutKeys = [];
|
||||
|
||||
rows.forEach(function(row) {
|
||||
var chanInput = row.querySelector('.chan-input');
|
||||
var keyInput = row.querySelector('.key-input');
|
||||
var chan = (chanInput ? chanInput.value : '').trim();
|
||||
var key = (keyInput ? keyInput.value : '').trim();
|
||||
|
||||
if (!chan)
|
||||
return;
|
||||
|
||||
if (key)
|
||||
chansWithKeys.push({ chan: chan, key: key });
|
||||
else
|
||||
chansWithoutKeys.push(chan);
|
||||
});
|
||||
|
||||
var allChans = chansWithKeys.map(function(item) { return item.chan; }).concat(chansWithoutKeys);
|
||||
var allKeys = chansWithKeys.map(function(item) { return item.key; });
|
||||
var result = allChans.join(',');
|
||||
if (allKeys.length > 0)
|
||||
result += ' ' + allKeys.join(',');
|
||||
|
||||
hidden.setValue(result);
|
||||
}
|
||||
|
||||
function createRow(channel, key) {
|
||||
var chanInput = E('input', {
|
||||
'type': 'text',
|
||||
'class': 'cbi-input-text chan-input',
|
||||
'placeholder': '#openwrt',
|
||||
'value': channel || '',
|
||||
'disabled': isReadonly || null
|
||||
});
|
||||
|
||||
chanInput.addEventListener('input', function() {
|
||||
if (chanInput.value.indexOf(',') !== -1 || /\s/.test(chanInput.value)) {
|
||||
chanInput.classList.add('cbi-input-invalid');
|
||||
chanInput.title = _('Enter a single channel per row without commas or spaces');
|
||||
} else {
|
||||
chanInput.classList.remove('cbi-input-invalid');
|
||||
chanInput.removeAttribute('title');
|
||||
}
|
||||
updateValue();
|
||||
});
|
||||
|
||||
var keyInput = E('input', {
|
||||
'type': 'password',
|
||||
'class': 'cbi-input-text key-input',
|
||||
'placeholder': _('Optional key'),
|
||||
'value': key || '',
|
||||
'disabled': isReadonly || null
|
||||
});
|
||||
|
||||
keyInput.addEventListener('input', function() {
|
||||
if (keyInput.value.indexOf(',') !== -1 || /\s/.test(keyInput.value)) {
|
||||
keyInput.classList.add('cbi-input-invalid');
|
||||
keyInput.title = _('Channel key cannot contain commas or spaces');
|
||||
} else {
|
||||
keyInput.classList.remove('cbi-input-invalid');
|
||||
keyInput.removeAttribute('title');
|
||||
}
|
||||
updateValue();
|
||||
});
|
||||
|
||||
var btnRemove = E('button', {
|
||||
'type': 'button',
|
||||
'class': 'btn cbi-button cbi-button-remove',
|
||||
'title': _('Remove'),
|
||||
'disabled': isReadonly || null
|
||||
}, '✕');
|
||||
|
||||
var row = E('tr', {
|
||||
'class': 'tr cbi-section-table-row',
|
||||
'data-channel-row': ''
|
||||
}, [
|
||||
E('td', { 'class': 'td cbi-section-table-cell' }, chanInput),
|
||||
E('td', { 'class': 'td cbi-section-table-cell' }, keyInput),
|
||||
E('td', { 'class': 'td cbi-section-table-cell cbi-section-actions' }, btnRemove)
|
||||
]);
|
||||
|
||||
btnRemove.addEventListener('click', function() {
|
||||
row.remove();
|
||||
updateValue();
|
||||
});
|
||||
|
||||
return row;
|
||||
}
|
||||
|
||||
var parsed = weechatTools.parseAutoJoin(cfgvalue != null ? cfgvalue : '');
|
||||
if (parsed.channels.length > 0) {
|
||||
parsed.channels.forEach(function(chan, idx) {
|
||||
table.appendChild(createRow(chan, parsed.keys[idx] || ''));
|
||||
});
|
||||
}
|
||||
else {
|
||||
table.appendChild(createRow('', ''));
|
||||
}
|
||||
|
||||
var btnAdd = E('button', {
|
||||
'type': 'button',
|
||||
'class': 'btn cbi-button cbi-button-add',
|
||||
'disabled': isReadonly || null
|
||||
}, _('Add channel'));
|
||||
|
||||
btnAdd.addEventListener('click', function() {
|
||||
var newRow = createRow('', '');
|
||||
table.appendChild(newRow);
|
||||
var input = newRow.querySelector('.chan-input');
|
||||
if (input)
|
||||
input.focus();
|
||||
});
|
||||
|
||||
return E('div', { 'class': 'cbi-value-field' }, [
|
||||
table,
|
||||
btnAdd,
|
||||
E('div', { 'class': 'alert-message error autojoin-error', 'style': 'display: none' }),
|
||||
hidden.render()
|
||||
]);
|
||||
}
|
||||
});
|
||||
|
||||
return view.extend({
|
||||
render: function() {
|
||||
var m, s, o;
|
||||
|
||||
m = new form.Map('weechat', _('WeeChat - IRC Servers'),
|
||||
_('Configure IRC server connections, authentication, and auto-join channels.'));
|
||||
|
||||
s = m.section(form.GridSection, 'server', _('IRC Servers'));
|
||||
s.addremove = true;
|
||||
s.anonymous = false;
|
||||
s.addbtntitle = _('Add Server');
|
||||
s.nodescriptions = true;
|
||||
|
||||
s.sectiontitle = function(section_id) {
|
||||
return section_id;
|
||||
};
|
||||
|
||||
/* Define Tabs */
|
||||
s.tab('connection', _('Connection'));
|
||||
s.tab('identity', _('Identity'));
|
||||
s.tab('auth', _('Authentication'));
|
||||
s.tab('channels', _('Channels'));
|
||||
|
||||
/* Connection settings (table columns + modal Connection tab) */
|
||||
o = s.taboption('connection', form.Value, 'address', _('Server Hostname'),
|
||||
_('Hostname or IP address of the IRC server.'));
|
||||
o.placeholder = 'irc.oftc.net';
|
||||
o.datatype = 'host';
|
||||
o.rmempty = false;
|
||||
|
||||
o = s.taboption('connection', form.Value, 'port', _('Port'));
|
||||
o.datatype = 'and(port,min(1))';
|
||||
o.default = '6697';
|
||||
o.placeholder = '6697';
|
||||
|
||||
o = s.taboption('connection', form.Flag, 'ssl', _('Use TLS'));
|
||||
o.default = o.enabled;
|
||||
o.rmempty = false;
|
||||
|
||||
o = s.taboption('connection', form.ListValue, 'ipv6', _('IPv6 Support'),
|
||||
_('IPv6 connection mode for this IRC server.'));
|
||||
o.value('auto', _('Auto'));
|
||||
o.value('force', _('Force IPv6'));
|
||||
o.value('disable', _('Disable IPv6'));
|
||||
o.default = 'auto';
|
||||
o.textvalue = function(section_id) {
|
||||
var val = this.cfgvalue(section_id) || this.default;
|
||||
switch (val) {
|
||||
case 'force': return _('Force IPv6');
|
||||
case 'disable': return _('Disabled');
|
||||
case 'auto':
|
||||
default:
|
||||
return _('Auto');
|
||||
}
|
||||
};
|
||||
|
||||
/* Channels (table column + modal Channels tab) */
|
||||
o = s.taboption('channels', CBIAutoJoin, 'autojoin', _('Auto-join Channels'),
|
||||
_('Channels to join automatically after connecting to this server.'));
|
||||
o.rmempty = true;
|
||||
|
||||
/* Auto-connect & Enabled flags (table columns + modal Connection tab) */
|
||||
o = s.taboption('connection', form.Flag, 'autoconnect', _('Auto-connect'),
|
||||
_('Connect automatically when WeeChat starts.'));
|
||||
o.default = o.enabled;
|
||||
o.rmempty = false;
|
||||
o.editable = true;
|
||||
|
||||
o = s.taboption('connection', form.Flag, 'enabled', _('Enabled'));
|
||||
o.default = o.enabled;
|
||||
o.rmempty = false;
|
||||
o.editable = true;
|
||||
|
||||
/* Tab 2: Identity */
|
||||
o = s.taboption('identity', form.Value, 'nicks', _('Nicknames'),
|
||||
_('Comma-separated list of nicknames to try.'));
|
||||
o.placeholder = 'weechat,weechat_';
|
||||
o.modalonly = true;
|
||||
o.validate = validateNoExpr;
|
||||
|
||||
o = s.taboption('identity', form.Value, 'username', _('Username'));
|
||||
o.placeholder = 'weechat';
|
||||
o.modalonly = true;
|
||||
o.validate = validateNoExpr;
|
||||
|
||||
o = s.taboption('identity', form.Value, 'realname', _('Real Name'));
|
||||
o.placeholder = 'WeeChat User';
|
||||
o.modalonly = true;
|
||||
o.validate = validateNoExpr;
|
||||
|
||||
/* Tab 3: Authentication */
|
||||
o = s.taboption('auth', form.ListValue, 'auth_method', _('Authentication Method'),
|
||||
_('Select the authentication method required by the IRC network.'));
|
||||
o.value('none', _('None'));
|
||||
o.value('nickserv', _('NickServ (/msg nickserv identify)'));
|
||||
o.value('sasl_plain', _('SASL PLAIN'));
|
||||
o.value('sasl_scram_sha_256', _('SASL SCRAM-SHA-256'));
|
||||
o.value('sasl_scram_sha_512', _('SASL SCRAM-SHA-512'));
|
||||
o.default = 'none';
|
||||
o.modalonly = true;
|
||||
|
||||
o = s.taboption('auth', form.Value, 'sasl_username', _('SASL Username'),
|
||||
_('Account username if different from nickname.'));
|
||||
o.depends('auth_method', 'sasl_plain');
|
||||
o.depends('auth_method', 'sasl_scram_sha_256');
|
||||
o.depends('auth_method', 'sasl_scram_sha_512');
|
||||
o.modalonly = true;
|
||||
/* Same default as the init script: the first nickname, then the username */
|
||||
o.validate = function(section_id, value) {
|
||||
var nick = (this.section.formvalue(section_id, 'nicks') || '').split(',')[0];
|
||||
var account = value || nick || this.section.formvalue(section_id, 'username') || '';
|
||||
if (!account.trim())
|
||||
return _('Enter a SASL username or a nickname');
|
||||
return validateNoExpr(section_id, value);
|
||||
};
|
||||
|
||||
o = s.taboption('auth', form.Value, 'password', _('Password'));
|
||||
o.password = true;
|
||||
o.depends('auth_method', 'nickserv');
|
||||
o.depends('auth_method', 'sasl_plain');
|
||||
o.depends('auth_method', 'sasl_scram_sha_256');
|
||||
o.depends('auth_method', 'sasl_scram_sha_512');
|
||||
o.modalonly = true;
|
||||
o.validate = function(section_id, value) {
|
||||
var method = this.section.formvalue(section_id, 'auth_method');
|
||||
|
||||
if (!method || method === 'none')
|
||||
return true;
|
||||
if (!value || !value.trim())
|
||||
return _('A password is required for this authentication method');
|
||||
/* WeeChat replaces these in the NickServ command after decoding the password */
|
||||
if (method === 'nickserv' && /\$(nick|channel|server)/.test(value))
|
||||
return _('A NickServ password cannot contain $nick, $channel or $server');
|
||||
return true;
|
||||
};
|
||||
|
||||
return m.render().then(L.bind(function(nodes) {
|
||||
var target = null;
|
||||
var hashMatch = (window.location.hash || '').match(/#(?:edit|server)=([^&]+)/);
|
||||
if (hashMatch) {
|
||||
target = decodeURIComponent(hashMatch[1]);
|
||||
} else {
|
||||
var params = new URLSearchParams(window.location.search);
|
||||
target = params.get('server') || params.get('edit');
|
||||
}
|
||||
|
||||
if (target && (m.data.get('weechat', target) || uci.get('weechat', target))) {
|
||||
if (window.history && window.history.replaceState) {
|
||||
var cleanSearch = window.location.search ? window.location.search.replace(/[?&](?:server|edit)=[^&]*/g, '').replace(/^&/, '?') : '';
|
||||
window.history.replaceState(null, '', window.location.pathname + cleanSearch);
|
||||
}
|
||||
s.renderMoreOptionsModal(target);
|
||||
}
|
||||
|
||||
return nodes;
|
||||
}, this));
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,394 @@
|
||||
'use strict';
|
||||
'require view';
|
||||
'require rpc';
|
||||
'require uci';
|
||||
'require ui';
|
||||
'require poll';
|
||||
'require tools.weechat as weechatTools';
|
||||
|
||||
function renderEnabledState(isEnabled) {
|
||||
return [
|
||||
isEnabled
|
||||
? E('span', { 'class': 'label success', 'style': 'margin-right: 8px;' }, _('Enabled'))
|
||||
: E('span', { 'class': 'label danger', 'style': 'margin-right: 8px;' }, _('Disabled')),
|
||||
E('span', { 'class': 'cbi-value-description' },
|
||||
isEnabled ? _('Service is allowed to start.') : _('Service is disabled in configuration.'))
|
||||
];
|
||||
}
|
||||
|
||||
function getServiceStatus(serviceData) {
|
||||
var isRunning = false,
|
||||
pid = null;
|
||||
|
||||
if (serviceData && serviceData.weechat && serviceData.weechat.instances) {
|
||||
var instances = serviceData.weechat.instances;
|
||||
for (var inst in instances) {
|
||||
if (instances[inst].running) {
|
||||
isRunning = true;
|
||||
pid = instances[inst].pid;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return { running: isRunning, pid: pid };
|
||||
}
|
||||
|
||||
return view.extend({
|
||||
callServiceList: rpc.declare({
|
||||
object: 'service',
|
||||
method: 'list',
|
||||
params: ['name'],
|
||||
expect: { '': {} }
|
||||
}),
|
||||
|
||||
callRcList: rpc.declare({
|
||||
object: 'rc',
|
||||
method: 'list',
|
||||
expect: { '': {} }
|
||||
}),
|
||||
|
||||
callRcInit: rpc.declare({
|
||||
object: 'rc',
|
||||
method: 'init',
|
||||
params: ['name', 'action']
|
||||
}),
|
||||
|
||||
callDaemonInfo: rpc.declare({
|
||||
object: 'weechat',
|
||||
method: 'get_daemon_info',
|
||||
expect: { '': {} }
|
||||
}),
|
||||
|
||||
callEnableService: rpc.declare({
|
||||
object: 'weechat',
|
||||
method: 'enable_service',
|
||||
expect: { success: false }
|
||||
}),
|
||||
|
||||
/*
|
||||
* The init script only starts WeeChat with option enabled '1'. Let the
|
||||
* backend commit just that option, uci.apply() would apply every change
|
||||
* staged in this session, for any config.
|
||||
*/
|
||||
ensureEnabled: function() {
|
||||
if (uci.get('weechat', 'weechat', 'enabled') === '1')
|
||||
return Promise.resolve();
|
||||
|
||||
return this.callEnableService().then(function(success) {
|
||||
if (!success)
|
||||
throw new Error(_('Could not enable the service in /etc/config/weechat'));
|
||||
|
||||
var cell = document.getElementById('weechat-service-enabled');
|
||||
if (cell) {
|
||||
while (cell.firstChild)
|
||||
cell.removeChild(cell.firstChild);
|
||||
renderEnabledState(true).forEach(function(node) { cell.appendChild(node); });
|
||||
}
|
||||
|
||||
uci.unload('weechat');
|
||||
return uci.load('weechat');
|
||||
});
|
||||
},
|
||||
|
||||
load: function() {
|
||||
return Promise.all([
|
||||
uci.load('weechat'),
|
||||
this.callServiceList('weechat'),
|
||||
this.callRcList(),
|
||||
L.resolveDefault(this.callDaemonInfo(), {})
|
||||
]);
|
||||
},
|
||||
|
||||
handleServiceAction: function(statusBadge, pidLabel, btnStart, btnRestart, btnStop, isReadonly, action) {
|
||||
btnStart.disabled = true;
|
||||
btnRestart.disabled = true;
|
||||
btnStop.disabled = true;
|
||||
|
||||
var ensureEnabled = (action === 'start') ? this.ensureEnabled() : Promise.resolve();
|
||||
|
||||
return ensureEnabled.then(L.bind(function() {
|
||||
return this.callRcInit('weechat', action);
|
||||
}, this)).then(L.bind(function(ret) {
|
||||
if (ret)
|
||||
throw new Error(_('Command failed with return code %d').format(ret));
|
||||
|
||||
return new Promise(function(resolve) {
|
||||
window.setTimeout(resolve, 1000);
|
||||
});
|
||||
}, this)).then(L.bind(function() {
|
||||
return this.updateServiceStatus(statusBadge, pidLabel, btnStart, btnRestart, btnStop, isReadonly);
|
||||
}, this)).catch(function(e) {
|
||||
ui.addNotification(null, E('p', _('Failed to execute "/etc/init.d/weechat %s": %s').format(action, e.message || e)));
|
||||
});
|
||||
},
|
||||
|
||||
handleToggleAutostart: function(enable, autostartBadge, autostartBtn, isReadonly, ev) {
|
||||
var action = enable ? 'enable' : 'disable';
|
||||
autostartBtn.disabled = true;
|
||||
|
||||
var prepare = enable ? this.ensureEnabled() : Promise.resolve();
|
||||
|
||||
return prepare.then(L.bind(function() {
|
||||
return this.callRcInit('weechat', action);
|
||||
}, this)).then(L.bind(function(ret) {
|
||||
if (ret)
|
||||
throw new Error(_('Command failed with return code %d').format(ret));
|
||||
|
||||
autostartBadge.className = 'label ' + (enable ? 'success' : '');
|
||||
autostartBadge.textContent = enable ? _('Enabled') : _('Disabled');
|
||||
autostartBtn.textContent = enable ? _('Disable Autostart') : _('Enable Autostart');
|
||||
autostartBtn.disabled = isReadonly ? true : false;
|
||||
autostartBtn.onclick = isReadonly ? null : ui.createHandlerFn(this, 'handleToggleAutostart', !enable, autostartBadge, autostartBtn, isReadonly);
|
||||
}, this)).catch(function(e) {
|
||||
autostartBtn.disabled = false;
|
||||
ui.addNotification(null, E('p', _('Failed to execute "/etc/init.d/weechat %s": %s').format(action, e.message || e)));
|
||||
});
|
||||
},
|
||||
|
||||
updateServiceStatus: function(statusBadge, pidLabel, btnStart, btnRestart, btnStop, isReadonly) {
|
||||
return this.callServiceList('weechat').then(function(res) {
|
||||
var st = getServiceStatus(res);
|
||||
var isRunning = st.running;
|
||||
var pid = st.pid;
|
||||
|
||||
statusBadge.className = 'label ' + (isRunning ? 'success' : 'danger');
|
||||
statusBadge.textContent = isRunning ? _('Running') : _('Stopped');
|
||||
pidLabel.textContent = (isRunning && pid) ? ('PID: ' + pid) : '';
|
||||
|
||||
btnStart.disabled = (isRunning || isReadonly);
|
||||
btnRestart.disabled = (!isRunning || isReadonly);
|
||||
btnStop.disabled = (!isRunning || isReadonly);
|
||||
});
|
||||
},
|
||||
|
||||
render: function(data) {
|
||||
var serviceData = data[1] || {},
|
||||
rcData = data[2] || {},
|
||||
daemonInfo = data[3] || {},
|
||||
isReadonly = !L.hasViewPermission(),
|
||||
st = getServiceStatus(serviceData),
|
||||
isRunning = st.running,
|
||||
pid = st.pid,
|
||||
isAutostart = !!(rcData && rcData.weechat && rcData.weechat.enabled),
|
||||
isEnabled = (uci.get('weechat', 'weechat', 'enabled') === '1'),
|
||||
version = (daemonInfo && daemonInfo.version) ? daemonInfo.version : '-';
|
||||
|
||||
var statusBadge = E('span', {
|
||||
'class': 'label ' + (isRunning ? 'success' : 'danger'),
|
||||
'style': 'padding: 4px 10px; font-size: 90%; font-weight: bold; border-radius: 4px;'
|
||||
}, isRunning ? _('Running') : _('Stopped'));
|
||||
|
||||
var pidLabel = E('span', {
|
||||
'class': 'cbi-value-description',
|
||||
'style': 'margin-left: 8px; font-weight: normal;'
|
||||
}, (isRunning && pid) ? ('PID: ' + pid) : '');
|
||||
|
||||
var autostartBadge = E('span', {
|
||||
'class': 'label ' + (isAutostart ? 'success' : ''),
|
||||
'style': 'margin-right: 12px;'
|
||||
}, isAutostart ? _('Enabled') : _('Disabled'));
|
||||
|
||||
var autostartBtn = E('button', {
|
||||
'class': 'btn cbi-button cbi-button-action',
|
||||
'disabled': isReadonly ? 'disabled' : null
|
||||
}, isAutostart ? _('Disable Autostart') : _('Enable Autostart'));
|
||||
|
||||
if (!isReadonly) {
|
||||
autostartBtn.onclick = ui.createHandlerFn(this, 'handleToggleAutostart', !isAutostart, autostartBadge, autostartBtn, isReadonly);
|
||||
}
|
||||
|
||||
var btnStart = E('button', {
|
||||
'class': 'btn cbi-button cbi-button-action weechat-btn-control',
|
||||
'disabled': (isRunning || isReadonly) ? 'disabled' : null
|
||||
}, _('Start'));
|
||||
|
||||
var btnRestart = E('button', {
|
||||
'class': 'btn cbi-button cbi-button-reload weechat-btn-control',
|
||||
'disabled': (!isRunning || isReadonly) ? 'disabled' : null
|
||||
}, _('Restart'));
|
||||
|
||||
var btnStop = E('button', {
|
||||
'class': 'btn cbi-button cbi-button-negative weechat-btn-control',
|
||||
'disabled': (!isRunning || isReadonly) ? 'disabled' : null
|
||||
}, _('Stop'));
|
||||
|
||||
if (!isReadonly) {
|
||||
btnStart.addEventListener('click',
|
||||
ui.createHandlerFn(this, 'handleServiceAction',
|
||||
statusBadge, pidLabel, btnStart, btnRestart, btnStop, isReadonly, 'start'));
|
||||
btnRestart.addEventListener('click',
|
||||
ui.createHandlerFn(this, 'handleServiceAction',
|
||||
statusBadge, pidLabel, btnStart, btnRestart, btnStop, isReadonly, 'restart'));
|
||||
btnStop.addEventListener('click',
|
||||
ui.createHandlerFn(this, 'handleServiceAction',
|
||||
statusBadge, pidLabel, btnStart, btnRestart, btnStop, isReadonly, 'stop'));
|
||||
}
|
||||
|
||||
var relayEnabled = (uci.get('weechat', 'weechat', 'relay_enabled') === '1'),
|
||||
relayPort = uci.get('weechat', 'weechat', 'relay_port') || '9000',
|
||||
relayTls = (uci.get('weechat', 'weechat', 'relay_tls') === '1');
|
||||
|
||||
var servers = uci.sections('weechat', 'server'),
|
||||
enabledCount = 0;
|
||||
|
||||
servers.forEach(function(srv) {
|
||||
if (srv.enabled !== '0' && srv.enabled !== false && srv.enabled !== 'off')
|
||||
enabledCount++;
|
||||
});
|
||||
|
||||
var relayStatusNode;
|
||||
if (relayEnabled) {
|
||||
var clientBtn = E('a', {
|
||||
'class': 'btn cbi-button cbi-button-neutral',
|
||||
'href': 'https://www.glowing-bear.org/',
|
||||
'target': '_blank',
|
||||
'rel': 'noreferrer noopener',
|
||||
'style': 'display: inline-block; margin-top: 6px;'
|
||||
}, [ _('Open Web Client (Hosted)'), ' ↗' ]);
|
||||
|
||||
relayStatusNode = E('div', {}, [
|
||||
E('div', {}, [
|
||||
E('span', { 'class': 'label success' }, _('Enabled'))
|
||||
]),
|
||||
E('div', { 'class': 'cbi-value-description', 'style': 'margin-top: 4px;' },
|
||||
_('Port %s · TLS %s').format(relayPort, relayTls ? _('enabled') : _('disabled'))),
|
||||
E('div', { 'style': 'margin-top: 6px;' }, clientBtn)
|
||||
]);
|
||||
} else {
|
||||
relayStatusNode = E('div', {}, [
|
||||
E('span', { 'class': 'label' }, _('Disabled'))
|
||||
]);
|
||||
}
|
||||
|
||||
var ircStatusNode = E('div', {}, [
|
||||
E('span', {},
|
||||
_('%d configured · %d enabled').format(servers.length, enabledCount))
|
||||
]);
|
||||
|
||||
var serverRows = [];
|
||||
if (servers.length === 0) {
|
||||
serverRows.push(E('tr', { 'class': 'tr cbi-section-table-row' }, [
|
||||
E('td', { 'colspan': 8, 'class': 'td cbi-section-table-cell center' },
|
||||
_('No IRC servers configured. Go to Servers tab to add one.'))
|
||||
]));
|
||||
} else {
|
||||
servers.forEach(function(s) {
|
||||
var isSrvEnabled = (s.enabled !== '0' && s.enabled !== false && s.enabled !== 'off');
|
||||
var isSrvSsl = (s.ssl !== '0' && s.ssl !== false && s.ssl !== 'off');
|
||||
var parsed = weechatTools.parseAutoJoin(s.autojoin);
|
||||
var chans = parsed.channels;
|
||||
|
||||
var chanNodes = [];
|
||||
if (chans.length > 0) {
|
||||
chans.forEach(function(chan) {
|
||||
chanNodes.push(E('code', {
|
||||
'style': 'margin: 2px 4px 2px 0; display: inline-block; padding: 2px 6px; border-radius: 3px;'
|
||||
}, chan));
|
||||
});
|
||||
} else {
|
||||
chanNodes.push(E('em', { 'class': 'cbi-value-description' }, _('no channels')));
|
||||
}
|
||||
|
||||
serverRows.push(E('tr', { 'class': 'tr cbi-section-table-row' }, [
|
||||
E('td', { 'class': 'td cbi-section-table-cell' },
|
||||
isSrvEnabled
|
||||
? E('span', { 'class': 'label success' }, _('Enabled'))
|
||||
: E('span', { 'class': 'label' }, _('Disabled'))),
|
||||
E('td', { 'class': 'td cbi-section-table-cell' }, E('strong', {}, s['.name'])),
|
||||
E('td', { 'class': 'td cbi-section-table-cell' }, s.address || '-'),
|
||||
E('td', { 'class': 'td cbi-section-table-cell' }, s.port || '6697'),
|
||||
E('td', { 'class': 'td cbi-section-table-cell' }, isSrvSsl ? _('Yes') : _('No')),
|
||||
E('td', { 'class': 'td cbi-section-table-cell' }, s.nicks || s.username || '-'),
|
||||
E('td', { 'class': 'td cbi-section-table-cell' }, chanNodes),
|
||||
E('td', { 'class': 'td cbi-section-table-cell cbi-section-actions', 'style': 'text-align: right; white-space: nowrap;' }, [
|
||||
E('a', {
|
||||
'class': 'btn cbi-button cbi-button-edit',
|
||||
'href': L.url('admin/services/weechat/servers') + '?server=' + encodeURIComponent(s['.name']) + '#edit=' + encodeURIComponent(s['.name']),
|
||||
'title': _('Edit server'),
|
||||
'aria-label': _('Edit server')
|
||||
}, '✎')
|
||||
])
|
||||
]));
|
||||
});
|
||||
}
|
||||
|
||||
poll.add(L.bind(function() {
|
||||
return this.updateServiceStatus(statusBadge, pidLabel, btnStart, btnRestart, btnStop, isReadonly);
|
||||
}, this), 5);
|
||||
|
||||
return E('div', { 'class': 'cbi-map' }, [
|
||||
E('link', { 'rel': 'stylesheet', 'href': L.resource('view/weechat/weechat.css') }),
|
||||
|
||||
E('h2', {}, _('WeeChat - Status')),
|
||||
E('div', { 'class': 'cbi-map-descr' },
|
||||
_('WeeChat runs as a headless daemon on your router. It keeps you connected to IRC networks and exposes a secure Relay API for web or mobile clients.')),
|
||||
|
||||
/* Two cards grid */
|
||||
E('div', { 'class': 'weechat-status-grid' }, [
|
||||
/* Card 1: Service Status */
|
||||
E('div', { 'class': 'weechat-panel' }, [
|
||||
E('h3', {}, _('Service Status')),
|
||||
E('table', {}, [
|
||||
E('tr', { 'class': 'tr' }, [
|
||||
E('td', { 'class': 'td left', 'style': 'width: 35%;' }, _('WeeChat Daemon')),
|
||||
E('td', { 'class': 'td left' }, [ statusBadge, pidLabel ])
|
||||
]),
|
||||
E('tr', { 'class': 'tr' }, [
|
||||
E('td', { 'class': 'td left' }, _('Version')),
|
||||
E('td', { 'class': 'td left' }, E('span', {}, version))
|
||||
]),
|
||||
E('tr', { 'class': 'tr' }, [
|
||||
E('td', { 'class': 'td left' }, _('Service enabled')),
|
||||
E('td', { 'class': 'td left', 'id': 'weechat-service-enabled' }, renderEnabledState(isEnabled))
|
||||
]),
|
||||
E('tr', { 'class': 'tr' }, [
|
||||
E('td', { 'class': 'td left' }, _('Autostart at boot')),
|
||||
E('td', { 'class': 'td left' }, [ autostartBadge, autostartBtn ])
|
||||
]),
|
||||
E('tr', { 'class': 'tr' }, [
|
||||
E('td', { 'class': 'td left' }, _('Service control')),
|
||||
E('td', { 'class': 'td left' }, [ btnStart, btnRestart, btnStop ])
|
||||
])
|
||||
])
|
||||
]),
|
||||
|
||||
/* Card 2: Overview */
|
||||
E('div', { 'class': 'weechat-panel' }, [
|
||||
E('h3', {}, _('Overview')),
|
||||
E('table', {}, [
|
||||
E('tr', { 'class': 'tr' }, [
|
||||
E('td', { 'class': 'td left', 'style': 'width: 30%;' }, _('Relay server')),
|
||||
E('td', { 'class': 'td left' }, relayStatusNode)
|
||||
]),
|
||||
E('tr', { 'class': 'tr' }, [
|
||||
E('td', { 'class': 'td left' }, _('IRC servers')),
|
||||
E('td', { 'class': 'td left' }, ircStatusNode)
|
||||
])
|
||||
])
|
||||
])
|
||||
]),
|
||||
|
||||
/* Configured IRC Servers Table */
|
||||
E('div', { 'class': 'weechat-servers-panel' }, [
|
||||
E('h3', {}, _('Configured IRC Servers')),
|
||||
E('table', { 'class': 'table cbi-section-table' }, [
|
||||
E('tr', { 'class': 'tr cbi-section-table-titles' }, [
|
||||
E('th', { 'class': 'th cbi-section-table-cell', 'style': 'width: 10%; white-space: nowrap;' }, _('Status')),
|
||||
E('th', { 'class': 'th cbi-section-table-cell', 'style': 'width: 12%; white-space: nowrap;' }, _('Server')),
|
||||
E('th', { 'class': 'th cbi-section-table-cell', 'style': 'width: 20%; white-space: nowrap;' }, _('Hostname')),
|
||||
E('th', { 'class': 'th cbi-section-table-cell', 'style': 'width: 7%; white-space: nowrap;' }, _('Port')),
|
||||
E('th', { 'class': 'th cbi-section-table-cell', 'style': 'width: 7%; white-space: nowrap;' }, _('TLS')),
|
||||
E('th', { 'class': 'th cbi-section-table-cell', 'style': 'width: 13%; white-space: nowrap;' }, _('Nicknames')),
|
||||
E('th', { 'class': 'th cbi-section-table-cell', 'style': 'width: 26%;' }, _('Channels')),
|
||||
E('th', { 'class': 'th cbi-section-table-cell cbi-section-actions', 'style': 'width: 5%; text-align: right;' }, '')
|
||||
])
|
||||
].concat(serverRows))
|
||||
])
|
||||
]);
|
||||
},
|
||||
|
||||
handleSaveApply: null,
|
||||
handleSave: null,
|
||||
handleReset: null
|
||||
});
|
||||
@@ -0,0 +1,65 @@
|
||||
.weechat-status-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
gap: 1.25rem;
|
||||
margin-bottom: 1.25rem;
|
||||
}
|
||||
|
||||
@media (max-width: 840px) {
|
||||
.weechat-status-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
.weechat-panel {
|
||||
border: 1px solid rgba(128, 128, 128, 0.22);
|
||||
border-radius: 6px;
|
||||
padding: 16px 20px;
|
||||
background: rgba(128, 128, 128, 0.02);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.weechat-panel h3 {
|
||||
margin-top: 0;
|
||||
margin-bottom: 14px;
|
||||
padding-bottom: 10px;
|
||||
border-bottom: 1px solid rgba(128, 128, 128, 0.15);
|
||||
font-size: 1.15rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.weechat-panel table {
|
||||
width: 100%;
|
||||
margin-bottom: 0;
|
||||
border-collapse: collapse;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.weechat-panel td {
|
||||
padding: 10px 4px;
|
||||
vertical-align: middle;
|
||||
border: none;
|
||||
}
|
||||
|
||||
.weechat-servers-panel {
|
||||
border: 1px solid rgba(128, 128, 128, 0.22);
|
||||
border-radius: 6px;
|
||||
padding: 16px 20px;
|
||||
background: rgba(128, 128, 128, 0.02);
|
||||
margin-top: 0.5rem;
|
||||
}
|
||||
|
||||
.weechat-servers-panel h3 {
|
||||
margin-top: 0;
|
||||
margin-bottom: 14px;
|
||||
padding-bottom: 10px;
|
||||
border-bottom: 1px solid rgba(128, 128, 128, 0.15);
|
||||
font-size: 1.15rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.weechat-btn-control {
|
||||
min-width: 72px;
|
||||
margin-right: 6px;
|
||||
}
|
||||
@@ -0,0 +1,665 @@
|
||||
msgid ""
|
||||
msgstr "Content-Type: text/plain; charset=UTF-8"
|
||||
|
||||
#: applications/luci-app-weechat/htdocs/luci-static/resources/view/weechat/status.js:266
|
||||
msgid "%d configured · %d enabled"
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-weechat/htdocs/luci-static/resources/view/weechat/servers.js:351
|
||||
msgid "A NickServ password cannot contain $nick, $channel or $server"
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-weechat/htdocs/luci-static/resources/view/weechat/servers.js:348
|
||||
msgid "A password is required for this authentication method"
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-weechat/htdocs/luci-static/resources/view/weechat/relay.js:239
|
||||
msgid "A self-signed certificate will be generated for WeeChat Relay at %s."
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-weechat/htdocs/luci-static/resources/view/weechat/servers.js:321
|
||||
msgid "Account username if different from nickname."
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-weechat/htdocs/luci-static/resources/view/weechat/servers.js:229
|
||||
msgid "Add Server"
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-weechat/htdocs/luci-static/resources/view/weechat/servers.js:200
|
||||
msgid "Add channel"
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-weechat/htdocs/luci-static/resources/view/weechat/relay.js:99
|
||||
msgid "All interfaces"
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-weechat/htdocs/luci-static/resources/view/weechat/servers.js:239
|
||||
msgid "Authentication"
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-weechat/htdocs/luci-static/resources/view/weechat/servers.js:310
|
||||
msgid "Authentication Method"
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-weechat/htdocs/luci-static/resources/view/weechat/servers.js:260
|
||||
#: applications/luci-app-weechat/htdocs/luci-static/resources/view/weechat/servers.js:271
|
||||
msgid "Auto"
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-weechat/htdocs/luci-static/resources/view/weechat/servers.js:281
|
||||
msgid "Auto-connect"
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-weechat/htdocs/luci-static/resources/view/weechat/servers.js:276
|
||||
msgid "Auto-join Channels"
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-weechat/htdocs/luci-static/resources/view/weechat/status.js:346
|
||||
msgid "Autostart at boot"
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-weechat/htdocs/luci-static/resources/view/weechat/relay.js:282
|
||||
msgid "Cancel"
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-weechat/htdocs/luci-static/resources/view/weechat/relay.js:349
|
||||
msgid ""
|
||||
"Cannot generate certificate: neither %s nor %s is installed on this router."
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-weechat/htdocs/luci-static/resources/view/weechat/relay.js:124
|
||||
msgid "Certificate"
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-weechat/htdocs/luci-static/resources/view/weechat/relay.js:173
|
||||
msgid "Certificate and key present"
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-weechat/htdocs/luci-static/resources/view/weechat/relay.js:192
|
||||
msgid ""
|
||||
"Certificate file exists but is incomplete (must contain both certificate and "
|
||||
"private key)."
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-weechat/htdocs/luci-static/resources/view/weechat/relay.js:330
|
||||
msgid "Certificate generation failed."
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-weechat/htdocs/luci-static/resources/view/weechat/relay.js:220
|
||||
msgid "Certificate generation requires the %s or %s utility."
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-weechat/htdocs/luci-static/resources/view/weechat/relay.js:246
|
||||
msgid "Certificate identity"
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-weechat/htdocs/luci-static/resources/view/weechat/relay.js:146
|
||||
msgid "Certificate status"
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-weechat/htdocs/luci-static/resources/view/weechat/servers.js:88
|
||||
msgid "Channel"
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-weechat/htdocs/luci-static/resources/view/weechat/servers.js:23
|
||||
msgid "Channel key cannot be specified without a channel name"
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-weechat/htdocs/luci-static/resources/view/weechat/servers.js:27
|
||||
#: applications/luci-app-weechat/htdocs/luci-static/resources/view/weechat/servers.js:154
|
||||
msgid "Channel key cannot contain commas or spaces"
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-weechat/htdocs/luci-static/resources/view/weechat/servers.js:25
|
||||
msgid "Channel name \"%s\" cannot contain commas or spaces"
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-weechat/htdocs/luci-static/resources/view/weechat/servers.js:240
|
||||
#: applications/luci-app-weechat/htdocs/luci-static/resources/view/weechat/status.js:383
|
||||
msgid "Channels"
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-weechat/htdocs/luci-static/resources/view/weechat/servers.js:29
|
||||
msgid "Channels and keys must not contain \"${\""
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-weechat/htdocs/luci-static/resources/view/weechat/servers.js:277
|
||||
msgid "Channels to join automatically after connecting to this server."
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-weechat/htdocs/luci-static/resources/view/weechat/relay.js:163
|
||||
msgid "Checking certificate status…"
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-weechat/htdocs/luci-static/resources/view/weechat/relay.js:272
|
||||
msgid ""
|
||||
"Comma-separated list of host names and IP addresses used to reach the relay."
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-weechat/htdocs/luci-static/resources/view/weechat/servers.js:294
|
||||
msgid "Comma-separated list of nicknames to try."
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-weechat/htdocs/luci-static/resources/view/weechat/relay.js:61
|
||||
#: applications/luci-app-weechat/htdocs/luci-static/resources/view/weechat/status.js:114
|
||||
#: applications/luci-app-weechat/htdocs/luci-static/resources/view/weechat/status.js:136
|
||||
msgid "Command failed with return code %d"
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-weechat/htdocs/luci-static/resources/view/weechat/relay.js:249
|
||||
msgid "Common name"
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-weechat/htdocs/luci-static/resources/view/weechat/servers.js:224
|
||||
msgid ""
|
||||
"Configure IRC server connections, authentication, and auto-join channels."
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-weechat/htdocs/luci-static/resources/view/weechat/relay.js:87
|
||||
msgid "Configure the WeeChat relay and chat logging."
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-weechat/htdocs/luci-static/resources/view/weechat/status.js:374
|
||||
msgid "Configured IRC Servers"
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-weechat/htdocs/luci-static/resources/view/weechat/servers.js:282
|
||||
msgid "Connect automatically when WeeChat starts."
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-weechat/htdocs/luci-static/resources/view/weechat/servers.js:237
|
||||
msgid "Connection"
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-weechat/htdocs/luci-static/resources/view/weechat/status.js:80
|
||||
msgid "Could not enable the service in /etc/config/weechat"
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-weechat/htdocs/luci-static/resources/view/weechat/relay.js:385
|
||||
msgid ""
|
||||
"Directory where WeeChat stores chat logs. The default /var/log/weechat is "
|
||||
"created automatically, any other directory must exist and be writable by the "
|
||||
"weechat user."
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-weechat/htdocs/luci-static/resources/view/weechat/status.js:140
|
||||
#: applications/luci-app-weechat/htdocs/luci-static/resources/view/weechat/status.js:195
|
||||
msgid "Disable Autostart"
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-weechat/htdocs/luci-static/resources/view/weechat/servers.js:262
|
||||
msgid "Disable IPv6"
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-weechat/htdocs/luci-static/resources/view/weechat/servers.js:268
|
||||
#: applications/luci-app-weechat/htdocs/luci-static/resources/view/weechat/status.js:13
|
||||
#: applications/luci-app-weechat/htdocs/luci-static/resources/view/weechat/status.js:139
|
||||
#: applications/luci-app-weechat/htdocs/luci-static/resources/view/weechat/status.js:190
|
||||
#: applications/luci-app-weechat/htdocs/luci-static/resources/view/weechat/status.js:260
|
||||
#: applications/luci-app-weechat/htdocs/luci-static/resources/view/weechat/status.js:297
|
||||
msgid "Disabled"
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-weechat/htdocs/luci-static/resources/view/weechat/status.js:308
|
||||
#: applications/luci-app-weechat/htdocs/luci-static/resources/view/weechat/status.js:309
|
||||
msgid "Edit server"
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-weechat/htdocs/luci-static/resources/view/weechat/status.js:140
|
||||
#: applications/luci-app-weechat/htdocs/luci-static/resources/view/weechat/status.js:195
|
||||
msgid "Enable Autostart"
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-weechat/htdocs/luci-static/resources/view/weechat/relay.js:380
|
||||
msgid "Enable Logging"
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-weechat/htdocs/luci-static/resources/view/weechat/relay.js:92
|
||||
msgid "Enable Relay Server"
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-weechat/htdocs/luci-static/resources/view/weechat/relay.js:120
|
||||
msgid "Enable TLS"
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-weechat/htdocs/luci-static/resources/view/weechat/servers.js:287
|
||||
#: applications/luci-app-weechat/htdocs/luci-static/resources/view/weechat/status.js:12
|
||||
#: applications/luci-app-weechat/htdocs/luci-static/resources/view/weechat/status.js:139
|
||||
#: applications/luci-app-weechat/htdocs/luci-static/resources/view/weechat/status.js:190
|
||||
#: applications/luci-app-weechat/htdocs/luci-static/resources/view/weechat/status.js:252
|
||||
#: applications/luci-app-weechat/htdocs/luci-static/resources/view/weechat/status.js:296
|
||||
msgid "Enabled"
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-weechat/htdocs/luci-static/resources/view/weechat/relay.js:121
|
||||
msgid "Encrypt relay connections with TLS."
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-weechat/htdocs/luci-static/resources/view/weechat/servers.js:331
|
||||
msgid "Enter a SASL username or a nickname"
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-weechat/htdocs/luci-static/resources/view/weechat/servers.js:135
|
||||
msgid "Enter a single channel per row without commas or spaces"
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-weechat/htdocs/luci-static/resources/view/weechat/status.js:122
|
||||
#: applications/luci-app-weechat/htdocs/luci-static/resources/view/weechat/status.js:145
|
||||
msgid "Failed to execute \"/etc/init.d/weechat %s\": %s"
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-weechat/htdocs/luci-static/resources/view/weechat/relay.js:177
|
||||
msgid "File:"
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-weechat/htdocs/luci-static/resources/view/weechat/relay.js:243
|
||||
msgid ""
|
||||
"For Internet-facing deployments, use a certificate issued by a trusted CA."
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-weechat/htdocs/luci-static/resources/view/weechat/servers.js:261
|
||||
#: applications/luci-app-weechat/htdocs/luci-static/resources/view/weechat/servers.js:267
|
||||
msgid "Force IPv6"
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-weechat/htdocs/luci-static/resources/view/weechat/relay.js:326
|
||||
#: applications/luci-app-weechat/htdocs/luci-static/resources/view/weechat/relay.js:335
|
||||
#: applications/luci-app-weechat/htdocs/luci-static/resources/view/weechat/relay.js:342
|
||||
msgid "Generate"
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-weechat/htdocs/luci-static/resources/view/weechat/relay.js:352
|
||||
msgid "Generate certificate?"
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-weechat/htdocs/luci-static/resources/view/weechat/relay.js:189
|
||||
msgid "Generate new certificate"
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-weechat/htdocs/luci-static/resources/view/weechat/relay.js:202
|
||||
#: applications/luci-app-weechat/htdocs/luci-static/resources/view/weechat/relay.js:215
|
||||
msgid "Generate self-signed certificate"
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-weechat/htdocs/luci-static/resources/view/weechat/relay.js:302
|
||||
msgid "Generating self-signed certificate…"
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-weechat/htdocs/luci-static/resources/view/weechat/relay.js:298
|
||||
msgid "Generating…"
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-weechat/htdocs/luci-static/resources/view/weechat/relay.js:399
|
||||
msgid ""
|
||||
"Glowing Bear is a web client that connects to WeeChat using the relay "
|
||||
"configured above."
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-weechat/root/usr/share/rpcd/acl.d/luci-app-weechat.json:3
|
||||
msgid "Grant access for luci-app-weechat"
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-weechat/htdocs/luci-static/resources/view/weechat/relay.js:258
|
||||
msgid "Host name identifying this router."
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-weechat/htdocs/luci-static/resources/view/weechat/status.js:379
|
||||
msgid "Hostname"
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-weechat/htdocs/luci-static/resources/view/weechat/servers.js:244
|
||||
msgid "Hostname or IP address of the IRC server."
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-weechat/htdocs/luci-static/resources/view/weechat/relay.js:97
|
||||
msgid ""
|
||||
"IP address to bind (leave empty for all interfaces). The address must exist "
|
||||
"when WeeChat starts."
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-weechat/htdocs/luci-static/resources/view/weechat/servers.js:258
|
||||
msgid "IPv6 Support"
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-weechat/htdocs/luci-static/resources/view/weechat/servers.js:259
|
||||
msgid "IPv6 connection mode for this IRC server."
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-weechat/htdocs/luci-static/resources/view/weechat/servers.js:226
|
||||
msgid "IRC Servers"
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-weechat/htdocs/luci-static/resources/view/weechat/status.js:365
|
||||
msgid "IRC servers"
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-weechat/htdocs/luci-static/resources/view/weechat/servers.js:238
|
||||
msgid "Identity"
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-weechat/htdocs/luci-static/resources/view/weechat/servers.js:89
|
||||
msgid "Key"
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-weechat/htdocs/luci-static/resources/view/weechat/relay.js:104
|
||||
msgid "Leading zeros are not allowed in an IPv4 address"
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-weechat/htdocs/luci-static/resources/view/weechat/relay.js:96
|
||||
msgid "Listen Address"
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-weechat/htdocs/luci-static/resources/view/weechat/relay.js:384
|
||||
msgid "Log Directory"
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-weechat/htdocs/luci-static/resources/view/weechat/relay.js:378
|
||||
msgid "Logging"
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-weechat/htdocs/luci-static/resources/view/weechat/relay.js:40
|
||||
msgid "Must be an absolute path without . or .. components"
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-weechat/htdocs/luci-static/resources/view/weechat/relay.js:44
|
||||
#: applications/luci-app-weechat/htdocs/luci-static/resources/view/weechat/servers.js:11
|
||||
msgid "Must not contain \"${\""
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-weechat/htdocs/luci-static/resources/view/weechat/relay.js:46
|
||||
msgid "Must not contain \"%\""
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-weechat/htdocs/luci-static/resources/view/weechat/relay.js:42
|
||||
msgid "Must not start or end with a space"
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-weechat/htdocs/luci-static/resources/view/weechat/servers.js:313
|
||||
msgid "NickServ (/msg nickserv identify)"
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-weechat/htdocs/luci-static/resources/view/weechat/servers.js:293
|
||||
#: applications/luci-app-weechat/htdocs/luci-static/resources/view/weechat/status.js:382
|
||||
msgid "Nicknames"
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-weechat/htdocs/luci-static/resources/view/weechat/status.js:301
|
||||
msgid "No"
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-weechat/htdocs/luci-static/resources/view/weechat/status.js:273
|
||||
msgid "No IRC servers configured. Go to Servers tab to add one."
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-weechat/htdocs/luci-static/resources/view/weechat/servers.js:312
|
||||
msgid "None"
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-weechat/htdocs/luci-static/resources/view/weechat/relay.js:406
|
||||
msgid "Open Glowing Bear (Hosted)"
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-weechat/htdocs/luci-static/resources/view/weechat/status.js:248
|
||||
msgid "Open Web Client (Hosted)"
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-weechat/htdocs/luci-static/resources/view/weechat/servers.js:146
|
||||
msgid "Optional key"
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-weechat/htdocs/luci-static/resources/view/weechat/status.js:358
|
||||
msgid "Overview"
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-weechat/htdocs/luci-static/resources/view/weechat/relay.js:125
|
||||
msgid ""
|
||||
"PEM file containing the certificate and private key. It must be readable by "
|
||||
"the weechat user."
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-weechat/htdocs/luci-static/resources/view/weechat/servers.js:335
|
||||
msgid "Password"
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-weechat/htdocs/luci-static/resources/view/weechat/relay.js:366
|
||||
msgid "Password required by clients to connect."
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-weechat/htdocs/luci-static/resources/view/weechat/relay.js:292
|
||||
msgid "Please enter a valid Common Name."
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-weechat/htdocs/luci-static/resources/view/weechat/relay.js:110
|
||||
#: applications/luci-app-weechat/htdocs/luci-static/resources/view/weechat/servers.js:249
|
||||
#: applications/luci-app-weechat/htdocs/luci-static/resources/view/weechat/status.js:380
|
||||
msgid "Port"
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-weechat/htdocs/luci-static/resources/view/weechat/status.js:255
|
||||
msgid "Port %s · TLS %s"
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-weechat/htdocs/luci-static/resources/view/weechat/relay.js:111
|
||||
msgid "Port for relay connections (WebSocket)."
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-weechat/htdocs/luci-static/resources/view/weechat/servers.js:304
|
||||
msgid "Real Name"
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-weechat/htdocs/luci-static/resources/view/weechat/relay.js:381
|
||||
msgid "Record chat logs to disk. Disabled by default to protect flash memory."
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-weechat/htdocs/luci-static/resources/view/weechat/relay.js:90
|
||||
#: applications/luci-app-weechat/root/usr/share/luci/menu.d/luci-app-weechat.json:30
|
||||
msgid "Relay"
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-weechat/htdocs/luci-static/resources/view/weechat/relay.js:365
|
||||
msgid "Relay Password"
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-weechat/htdocs/luci-static/resources/view/weechat/relay.js:372
|
||||
msgid "Relay password is required when relay server is enabled."
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-weechat/htdocs/luci-static/resources/view/weechat/status.js:361
|
||||
msgid "Relay server"
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-weechat/htdocs/luci-static/resources/view/weechat/servers.js:165
|
||||
msgid "Remove"
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-weechat/htdocs/luci-static/resources/view/weechat/status.js:209
|
||||
msgid "Restart"
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-weechat/htdocs/luci-static/resources/view/weechat/status.js:156
|
||||
#: applications/luci-app-weechat/htdocs/luci-static/resources/view/weechat/status.js:180
|
||||
msgid "Running"
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-weechat/htdocs/luci-static/resources/view/weechat/servers.js:314
|
||||
msgid "SASL PLAIN"
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-weechat/htdocs/luci-static/resources/view/weechat/servers.js:315
|
||||
msgid "SASL SCRAM-SHA-256"
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-weechat/htdocs/luci-static/resources/view/weechat/servers.js:316
|
||||
msgid "SASL SCRAM-SHA-512"
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-weechat/htdocs/luci-static/resources/view/weechat/servers.js:320
|
||||
msgid "SASL Username"
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-weechat/htdocs/luci-static/resources/view/weechat/relay.js:118
|
||||
msgid "Security"
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-weechat/htdocs/luci-static/resources/view/weechat/servers.js:311
|
||||
msgid "Select the authentication method required by the IRC network."
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-weechat/htdocs/luci-static/resources/view/weechat/relay.js:320
|
||||
msgid ""
|
||||
"Self-signed certificate generated for %s, but WeeChat could not be "
|
||||
"restarted: %s"
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-weechat/htdocs/luci-static/resources/view/weechat/relay.js:316
|
||||
msgid ""
|
||||
"Self-signed certificate generated for %s. Save and apply TLS with this "
|
||||
"certificate to use it."
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-weechat/htdocs/luci-static/resources/view/weechat/relay.js:314
|
||||
msgid ""
|
||||
"Self-signed certificate generated for %s. WeeChat uses it from its next "
|
||||
"start."
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-weechat/htdocs/luci-static/resources/view/weechat/relay.js:312
|
||||
msgid ""
|
||||
"Self-signed certificate generated for %s. WeeChat was restarted to use it."
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-weechat/htdocs/luci-static/resources/view/weechat/relay.js:241
|
||||
msgid ""
|
||||
"Self-signed certificates provide encryption but are not trusted "
|
||||
"automatically by browsers."
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-weechat/htdocs/luci-static/resources/view/weechat/status.js:378
|
||||
msgid "Server"
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-weechat/htdocs/luci-static/resources/view/weechat/servers.js:243
|
||||
msgid "Server Hostname"
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-weechat/root/usr/share/luci/menu.d/luci-app-weechat.json:22
|
||||
msgid "Servers"
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-weechat/htdocs/luci-static/resources/view/weechat/status.js:331
|
||||
msgid "Service Status"
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-weechat/htdocs/luci-static/resources/view/weechat/status.js:350
|
||||
msgid "Service control"
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-weechat/htdocs/luci-static/resources/view/weechat/status.js:342
|
||||
msgid "Service enabled"
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-weechat/htdocs/luci-static/resources/view/weechat/status.js:15
|
||||
msgid "Service is allowed to start."
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-weechat/htdocs/luci-static/resources/view/weechat/status.js:15
|
||||
msgid "Service is disabled in configuration."
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-weechat/htdocs/luci-static/resources/view/weechat/status.js:204
|
||||
msgid "Start"
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-weechat/htdocs/luci-static/resources/view/weechat/status.js:377
|
||||
#: applications/luci-app-weechat/root/usr/share/luci/menu.d/luci-app-weechat.json:14
|
||||
msgid "Status"
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-weechat/htdocs/luci-static/resources/view/weechat/status.js:214
|
||||
msgid "Stop"
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-weechat/htdocs/luci-static/resources/view/weechat/status.js:156
|
||||
#: applications/luci-app-weechat/htdocs/luci-static/resources/view/weechat/status.js:180
|
||||
msgid "Stopped"
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-weechat/htdocs/luci-static/resources/view/weechat/relay.js:263
|
||||
msgid "Subject alternative names"
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-weechat/htdocs/luci-static/resources/view/weechat/status.js:381
|
||||
msgid "TLS"
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-weechat/htdocs/luci-static/resources/view/weechat/relay.js:205
|
||||
msgid "TLS is enabled but no certificate file was found."
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-weechat/htdocs/luci-static/resources/view/weechat/relay.js:159
|
||||
msgid ""
|
||||
"This certificate is managed outside of LuCI, so it is not checked here. The "
|
||||
"service refuses to start if the file does not exist."
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-weechat/htdocs/luci-static/resources/view/weechat/relay.js:229
|
||||
msgid "Unable to verify certificate status: %s"
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-weechat/htdocs/luci-static/resources/view/weechat/servers.js:254
|
||||
msgid "Use TLS"
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-weechat/htdocs/luci-static/resources/view/weechat/servers.js:299
|
||||
msgid "Username"
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-weechat/htdocs/luci-static/resources/view/weechat/status.js:338
|
||||
msgid "Version"
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-weechat/htdocs/luci-static/resources/view/weechat/relay.js:393
|
||||
msgid "Web Client"
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-weechat/root/usr/share/luci/menu.d/luci-app-weechat.json:3
|
||||
msgid "WeeChat"
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-weechat/htdocs/luci-static/resources/view/weechat/servers.js:223
|
||||
msgid "WeeChat - IRC Servers"
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-weechat/htdocs/luci-static/resources/view/weechat/relay.js:86
|
||||
msgid "WeeChat - Relay"
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-weechat/htdocs/luci-static/resources/view/weechat/status.js:323
|
||||
msgid "WeeChat - Status"
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-weechat/htdocs/luci-static/resources/view/weechat/status.js:334
|
||||
msgid "WeeChat Daemon"
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-weechat/htdocs/luci-static/resources/view/weechat/status.js:325
|
||||
msgid ""
|
||||
"WeeChat runs as a headless daemon on your router. It keeps you connected to "
|
||||
"IRC networks and exposes a secure Relay API for web or mobile clients."
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-weechat/htdocs/luci-static/resources/view/weechat/status.js:301
|
||||
msgid "Yes"
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-weechat/htdocs/luci-static/resources/view/weechat/status.js:255
|
||||
msgid "disabled"
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-weechat/htdocs/luci-static/resources/view/weechat/status.js:255
|
||||
msgid "enabled"
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-weechat/htdocs/luci-static/resources/view/weechat/status.js:290
|
||||
msgid "no channels"
|
||||
msgstr ""
|
||||
@@ -0,0 +1,37 @@
|
||||
{
|
||||
"admin/services/weechat": {
|
||||
"title": "WeeChat",
|
||||
"order": 60,
|
||||
"action": {
|
||||
"type": "firstchild"
|
||||
},
|
||||
"depends": {
|
||||
"acl": [ "luci-app-weechat" ],
|
||||
"uci": { "weechat": true }
|
||||
}
|
||||
},
|
||||
"admin/services/weechat/status": {
|
||||
"title": "Status",
|
||||
"order": 10,
|
||||
"action": {
|
||||
"type": "view",
|
||||
"path": "weechat/status"
|
||||
}
|
||||
},
|
||||
"admin/services/weechat/servers": {
|
||||
"title": "Servers",
|
||||
"order": 20,
|
||||
"action": {
|
||||
"type": "view",
|
||||
"path": "weechat/servers"
|
||||
}
|
||||
},
|
||||
"admin/services/weechat/relay": {
|
||||
"title": "Relay",
|
||||
"order": 30,
|
||||
"action": {
|
||||
"type": "view",
|
||||
"path": "weechat/relay"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
{
|
||||
"luci-app-weechat": {
|
||||
"description": "Grant access for luci-app-weechat",
|
||||
"read": {
|
||||
"uci": [
|
||||
"weechat"
|
||||
],
|
||||
"ubus": {
|
||||
"service": [
|
||||
"list"
|
||||
],
|
||||
"rc": [
|
||||
"list"
|
||||
],
|
||||
"weechat": [
|
||||
"get_certificate_status",
|
||||
"get_daemon_info"
|
||||
]
|
||||
}
|
||||
},
|
||||
"write": {
|
||||
"uci": [
|
||||
"weechat"
|
||||
],
|
||||
"ubus": {
|
||||
"rc": [
|
||||
"init"
|
||||
],
|
||||
"weechat": [
|
||||
"enable_service",
|
||||
"generate_certificate"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,389 @@
|
||||
#!/usr/bin/env ucode
|
||||
'use strict';
|
||||
|
||||
import { access, lstat, readfile, unlink, rename, chmod, chown, mkdir, mkdtemp, open, popen } from 'fs';
|
||||
import { cursor } from 'uci';
|
||||
|
||||
/*
|
||||
* The weechat init script keeps /etc/weechat and /etc/weechat/tls owned by
|
||||
* root and not writable by the daemon. Certificates are only written there,
|
||||
* so the daemon cannot redirect any of the paths used below.
|
||||
*/
|
||||
const BASE_DIR = '/etc/weechat';
|
||||
const TLS_DIR = BASE_DIR + '/tls';
|
||||
const DEFAULT_CERT = TLS_DIR + '/relay.pem';
|
||||
|
||||
function find_px5g() {
|
||||
if (access('/usr/sbin/px5g', 'x')) {
|
||||
return '/usr/sbin/px5g';
|
||||
}
|
||||
if (access('/usr/bin/px5g', 'x')) {
|
||||
return '/usr/bin/px5g';
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function find_openssl() {
|
||||
if (access('/usr/bin/openssl', 'x')) {
|
||||
return '/usr/bin/openssl';
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function px5g_supports_addext(px5g_bin) {
|
||||
if (!px5g_bin)
|
||||
return false;
|
||||
let p = popen(px5g_bin + ' 2>&1', 'r');
|
||||
let out = p ? p.read('all') : '';
|
||||
if (p)
|
||||
p.close();
|
||||
// px5g-wolfssl outputs: "PX5G X.509 Certificate Generator Utilit using WolfSSL"
|
||||
// and does not support -addext
|
||||
if (index(out, 'WolfSSL') >= 0)
|
||||
return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
/* Without the s flag, ^ and $ also match at line breaks, so reject them first */
|
||||
function single_line(val) {
|
||||
return type(val) == 'string' && index(val, '\n') < 0 && index(val, '\r') < 0;
|
||||
}
|
||||
|
||||
function valid_cert_path(path) {
|
||||
return single_line(path) &&
|
||||
match(path, /^\/etc\/weechat\/tls\/[a-zA-Z0-9._-]+\.pem$/) &&
|
||||
index(path, '..') < 0;
|
||||
}
|
||||
|
||||
function root_owned_dir(path) {
|
||||
let st = lstat(path);
|
||||
return !!(st && st.type == 'directory' && st.uid == 0 && !(st.mode & 0022));
|
||||
}
|
||||
|
||||
function normalize_san(entry) {
|
||||
if (!single_line(entry))
|
||||
return null;
|
||||
|
||||
let item = trim(entry);
|
||||
if (!length(item))
|
||||
return null;
|
||||
|
||||
let kind = null;
|
||||
if (match(item, /^IP:/i)) {
|
||||
kind = 'IP';
|
||||
item = trim(substr(item, 3));
|
||||
} else if (match(item, /^DNS:/i)) {
|
||||
kind = 'DNS';
|
||||
item = trim(substr(item, 4));
|
||||
}
|
||||
|
||||
let ip = (kind != 'DNS') ? iptoarr(item) : null;
|
||||
if (ip)
|
||||
return { type: 'IP', value: arrtoip(ip), bytes: ip };
|
||||
if (kind == 'IP')
|
||||
return null;
|
||||
|
||||
if (match(item, /^[a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(\.[a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$/))
|
||||
return { type: 'DNS', value: item };
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/* DER encoding of a SAN entry, GeneralName [7] for IP and [2] for DNS */
|
||||
function san_der(san) {
|
||||
let bytes = (san.type == 'IP') ? san.bytes : map(split(san.value, ''), (c) => ord(c));
|
||||
let tag = (san.type == 'IP') ? 0x87 : 0x82;
|
||||
let hdr = (length(bytes) < 128) ? [ tag, length(bytes) ] : [ tag, 0x81, length(bytes) ];
|
||||
return chr(...hdr, ...bytes);
|
||||
}
|
||||
|
||||
/*
|
||||
* px5g-mbedtls up to PKG_RELEASE 11 writes every IP entry with a length of
|
||||
* 16 bytes, so an IPv4 entry turns into a wrong IPv6 address, and other px5g
|
||||
* builds might ignore -addext. Look for each SAN entry in the certificate.
|
||||
*/
|
||||
function cert_has_sans(cert_pem, sans) {
|
||||
let der = b64dec(replace(cert_pem, /-----[A-Z ]+-----|[ \t\r\n]/g, ''));
|
||||
if (der == null)
|
||||
return false;
|
||||
|
||||
for (let san in sans)
|
||||
if (index(der, san_der(san)) < 0)
|
||||
return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
function cleanup_temp_dir(tmp_dir) {
|
||||
if (tmp_dir && match(tmp_dir, /^\/tmp\/weechat-cert\.[A-Za-z0-9]+$/)) {
|
||||
system(['rm', '-rf', tmp_dir]);
|
||||
}
|
||||
}
|
||||
|
||||
function failure(msg) {
|
||||
return { success: false, error: msg };
|
||||
}
|
||||
|
||||
/* Whether the saved config makes the relay use this certificate */
|
||||
function cert_in_use(path) {
|
||||
let ctx = cursor();
|
||||
return ctx.get('weechat', 'weechat', 'relay_enabled') == '1' &&
|
||||
ctx.get('weechat', 'weechat', 'relay_tls') == '1' &&
|
||||
(ctx.get('weechat', 'weechat', 'relay_cert_key') || DEFAULT_CERT) == path;
|
||||
}
|
||||
|
||||
const methods = {
|
||||
/*
|
||||
* The init script only starts WeeChat with option enabled '1'. Commit
|
||||
* just that option, LuCI itself can only apply all staged changes.
|
||||
*/
|
||||
enable_service: {
|
||||
call: function(req) {
|
||||
let ctx = cursor();
|
||||
if (!ctx.get('weechat', 'weechat'))
|
||||
ctx.set('weechat', 'weechat', 'weechat');
|
||||
ctx.set('weechat', 'weechat', 'enabled', '1');
|
||||
return { success: !!ctx.commit('weechat') };
|
||||
}
|
||||
},
|
||||
get_daemon_info: {
|
||||
call: function(req) {
|
||||
let version = null;
|
||||
let p = popen('/usr/bin/weechat-headless -v 2>/dev/null', 'r');
|
||||
if (p) {
|
||||
version = trim(p.read('all'));
|
||||
p.close();
|
||||
}
|
||||
if (!length(version)) {
|
||||
p = popen('/usr/bin/weechat -v 2>/dev/null', 'r');
|
||||
if (p) {
|
||||
version = trim(p.read('all'));
|
||||
p.close();
|
||||
}
|
||||
}
|
||||
return {
|
||||
version: length(version) ? version : null
|
||||
};
|
||||
}
|
||||
},
|
||||
get_certificate_status: {
|
||||
args: { path: 'path' },
|
||||
call: function(req) {
|
||||
let path = req?.args?.path || DEFAULT_CERT;
|
||||
if (!valid_cert_path(path)) {
|
||||
return {
|
||||
exists: false,
|
||||
error: 'Invalid path. Must be located under /etc/weechat/tls/.'
|
||||
};
|
||||
}
|
||||
|
||||
let tool_available = !!(find_px5g() || find_openssl());
|
||||
|
||||
let st = lstat(path);
|
||||
if (!st || st.type != 'file' || st.size == 0) {
|
||||
return {
|
||||
exists: false,
|
||||
path: path,
|
||||
generator_available: tool_available
|
||||
};
|
||||
}
|
||||
|
||||
let content = readfile(path);
|
||||
if (!content) {
|
||||
return {
|
||||
exists: true,
|
||||
path: path,
|
||||
readable: false,
|
||||
generator_available: tool_available
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
exists: true,
|
||||
path: path,
|
||||
readable: true,
|
||||
has_key: !!match(content, /-----BEGIN ([A-Z0-9]+ )?PRIVATE KEY-----/),
|
||||
has_cert: !!match(content, /-----BEGIN CERTIFICATE-----/),
|
||||
generator_available: tool_available
|
||||
};
|
||||
}
|
||||
},
|
||||
|
||||
generate_certificate: {
|
||||
args: { cn: 'cn', san: 'san', path: 'path' },
|
||||
call: function(req) {
|
||||
let args = req?.args || {};
|
||||
let path = args.path || DEFAULT_CERT;
|
||||
|
||||
if (!valid_cert_path(path))
|
||||
return failure('Invalid target path. Must be located under /etc/weechat/tls/.');
|
||||
|
||||
let cn = (type(args.cn) == 'string') ? trim(args.cn) : '';
|
||||
if (!length(cn)) {
|
||||
cn = 'OpenWrt';
|
||||
}
|
||||
/* ':' allows an IPv6 address, which the dialog pre-fills over IPv6 */
|
||||
if (!single_line(cn) || !match(cn, /^[a-zA-Z0-9.:_-]+$/))
|
||||
return failure('Invalid Common Name format.');
|
||||
|
||||
let san_raw = args.san;
|
||||
let san_list = [];
|
||||
if (type(san_raw) == 'array') {
|
||||
san_list = san_raw;
|
||||
} else if (type(san_raw) == 'string') {
|
||||
san_list = split(san_raw, ',');
|
||||
}
|
||||
|
||||
let sans = [];
|
||||
for (let entry in san_list) {
|
||||
if (type(entry) != 'string' || !length(trim(entry)))
|
||||
continue;
|
||||
let san = normalize_san(entry);
|
||||
if (!san)
|
||||
return failure('Invalid Subject Alternative Name (SAN): ' + trim(entry));
|
||||
push(sans, san);
|
||||
}
|
||||
|
||||
let px5g_bin = find_px5g();
|
||||
let openssl_bin = find_openssl();
|
||||
|
||||
if (!px5g_bin && !openssl_bin)
|
||||
return failure('Neither px5g nor openssl certificate generator is installed.');
|
||||
|
||||
/*
|
||||
* A certificate needs at least one SAN, default to the CN. px5g-wolfssl
|
||||
* cannot add any, it puts the CN in as a DNS name itself.
|
||||
*/
|
||||
let with_san = !!(openssl_bin || px5g_supports_addext(px5g_bin));
|
||||
if (!length(sans) && with_san) {
|
||||
let san = normalize_san(cn);
|
||||
if (!san)
|
||||
return failure('Please enter a Subject Alternative Name.');
|
||||
push(sans, san);
|
||||
}
|
||||
|
||||
if (!root_owned_dir(BASE_DIR))
|
||||
return failure(BASE_DIR + ' must be a directory owned by root. Restart the WeeChat service to fix its permissions.');
|
||||
|
||||
if (!lstat(TLS_DIR)) {
|
||||
if (!mkdir(TLS_DIR, 0755) || !chown(TLS_DIR, 0, 0) || !chmod(TLS_DIR, 0755))
|
||||
return failure('Failed to create directory: ' + TLS_DIR);
|
||||
}
|
||||
if (!root_owned_dir(TLS_DIR))
|
||||
return failure(TLS_DIR + ' must be a directory owned by root.');
|
||||
|
||||
let st_path = lstat(path);
|
||||
if (st_path && st_path.type != 'file')
|
||||
return failure('Target exists and is not a regular file: ' + path);
|
||||
|
||||
let tmp_dir = mkdtemp('/tmp/weechat-cert.XXXXXX');
|
||||
if (!tmp_dir)
|
||||
return failure('Failed to create secure temporary directory.');
|
||||
|
||||
let key_file = tmp_dir + '/key.pem';
|
||||
let cert_file = tmp_dir + '/cert.pem';
|
||||
let subj = sprintf('/C=ZZ/ST=Somewhere/L=Unknown/O=OpenWrt/CN=%s', cn);
|
||||
/*
|
||||
* rpcd waits for this call, so use an EC key: generating an RSA
|
||||
* key can block it for a long time on slow routers.
|
||||
*/
|
||||
let key_opts = [ '-newkey', 'ec', '-pkeyopt', 'ec_paramgen_curve:P-256' ];
|
||||
let used_px5g = false;
|
||||
let cmd;
|
||||
|
||||
if (openssl_bin) {
|
||||
let san_str = join(',', map(sans, (san) => san.type + ':' + san.value));
|
||||
|
||||
cmd = [
|
||||
openssl_bin, 'req', '-x509', '-nodes',
|
||||
'-days', '397',
|
||||
...key_opts,
|
||||
'-keyout', key_file,
|
||||
'-out', cert_file,
|
||||
'-subj', subj,
|
||||
'-addext', 'extendedKeyUsage=serverAuth',
|
||||
'-addext', 'subjectAltName=' + san_str
|
||||
];
|
||||
} else if (px5g_supports_addext(px5g_bin)) {
|
||||
cmd = [
|
||||
px5g_bin, 'selfsigned',
|
||||
'-days', '397',
|
||||
...key_opts,
|
||||
'-keyout', key_file,
|
||||
'-out', cert_file,
|
||||
'-subj', subj,
|
||||
'-addext', 'extendedKeyUsage=serverAuth'
|
||||
];
|
||||
// Keep the subjectAltName options last, older px5g-mbedtls
|
||||
// hangs when any other option follows one of them.
|
||||
for (let san in sans)
|
||||
push(cmd, '-addext', 'subjectAltName=' + san.type + ':' + san.value);
|
||||
used_px5g = true;
|
||||
} else {
|
||||
if (length(sans) > 0) {
|
||||
cleanup_temp_dir(tmp_dir);
|
||||
return failure('The installed px5g (WolfSSL) does not support Subject Alternative Names (-addext). Please install openssl-util or px5g-mbedtls.');
|
||||
}
|
||||
/* It would put the address in as a DNS name, which no client accepts */
|
||||
if (iptoarr(cn)) {
|
||||
cleanup_temp_dir(tmp_dir);
|
||||
return failure('The installed px5g (WolfSSL) cannot put an IP address into a certificate. Please install openssl-util or px5g-mbedtls, or use a host name as Common Name.');
|
||||
}
|
||||
cmd = [
|
||||
px5g_bin, 'selfsigned',
|
||||
'-days', '397',
|
||||
...key_opts,
|
||||
'-keyout', key_file,
|
||||
'-out', cert_file,
|
||||
'-subj', subj
|
||||
];
|
||||
}
|
||||
|
||||
let ret = system(cmd);
|
||||
let key_content = (ret == 0) ? readfile(key_file) : null;
|
||||
let cert_content = (ret == 0) ? readfile(cert_file) : null;
|
||||
cleanup_temp_dir(tmp_dir);
|
||||
|
||||
if (!key_content || !cert_content)
|
||||
return failure('Certificate generation failed.');
|
||||
|
||||
if (used_px5g && !cert_has_sans(cert_content, sans)) {
|
||||
if (length(filter(sans, (san) => san.type == 'IP')))
|
||||
return failure('The installed px5g writes invalid IP addresses into certificates. Please install openssl-util, update px5g-mbedtls or use host names only.');
|
||||
return failure('The installed px5g did not write the Subject Alternative Names. Please install openssl-util or px5g-mbedtls.');
|
||||
}
|
||||
|
||||
let combined = key_content;
|
||||
if (substr(combined, -1) != '\n') {
|
||||
combined += '\n';
|
||||
}
|
||||
combined += cert_content;
|
||||
|
||||
// Readable by the daemon, but only root can replace it
|
||||
let tmp_file = path + '.tmp';
|
||||
unlink(tmp_file);
|
||||
|
||||
let f = open(tmp_file, 'wx', 0640);
|
||||
if (!f)
|
||||
return failure('Failed to write certificate bundle.');
|
||||
let written = f.write(combined);
|
||||
f.close();
|
||||
|
||||
if (written != length(combined) ||
|
||||
!chown(tmp_file, 'root', 'weechat') ||
|
||||
!chmod(tmp_file, 0640) ||
|
||||
!rename(tmp_file, path)) {
|
||||
unlink(tmp_file);
|
||||
return failure('Failed to install certificate to: ' + path);
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
path: path,
|
||||
in_use: cert_in_use(path),
|
||||
message: 'Certificate generated successfully.'
|
||||
};
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
return { 'weechat': methods };
|
||||
Reference in new issue
Block a user