summaryrefslogtreecommitdiffhomepage
path: root/modules/luci-mod-system/luasrc/view/admin_system
diff options
context:
space:
mode:
authorJo-Philipp Wich <jo@mein.io>2018-11-20 10:50:03 +0100
committerJo-Philipp Wich <jo@mein.io>2018-11-20 11:01:50 +0100
commit447f0c81713e567706c970d5c8fdea732dac52d4 (patch)
tree8160ab608be83ffe85dfc529c7ad0f5a0a5141d3 /modules/luci-mod-system/luasrc/view/admin_system
parentfc87173e1faf00b5e12c223f400b18a69abac852 (diff)
luci-mod-system: restructure administration pages
Split password, dropbear and SSH key configuration into separate pages in order to improve the form layout and to simplify the code. Signed-off-by: Jo-Philipp Wich <jo@mein.io>
Diffstat (limited to 'modules/luci-mod-system/luasrc/view/admin_system')
-rw-r--r--modules/luci-mod-system/luasrc/view/admin_system/password.htm70
-rw-r--r--modules/luci-mod-system/luasrc/view/admin_system/sshkeys.htm245
2 files changed, 315 insertions, 0 deletions
diff --git a/modules/luci-mod-system/luasrc/view/admin_system/password.htm b/modules/luci-mod-system/luasrc/view/admin_system/password.htm
new file mode 100644
index 0000000000..3aff9c981c
--- /dev/null
+++ b/modules/luci-mod-system/luasrc/view/admin_system/password.htm
@@ -0,0 +1,70 @@
+<%+header%>
+
+<script type="application/javascript">//<![CDATA[
+ function submitPassword(ev) {
+ var pw1 = document.body.querySelector('[name="pw1"]'),
+ pw2 = document.body.querySelector('[name="pw2"]');
+
+ if (!pw1.value.length || !pw2.value.length)
+ return;
+
+ if (pw1.value === pw2.value) {
+ showModal('<%:Change login password%>',
+ E('p', { class: 'spinning' }, '<%:Changing password…%>'));
+
+ (new XHR()).post('<%=url("admin/system/admin/password/json")%>',
+ { token: '<%=token%>', password: pw1.value },
+ function() {
+ showModal('<%:Change login password%>', [
+ E('div', _('The system password has been successfully changed.')),
+ E('div', { 'class': 'right' },
+ E('div', { class: 'btn', click: hideModal }, '<%:Dismiss%>'))
+ ]);
+
+ pw1.value = pw2.value = '';
+ });
+ }
+ else {
+ showModal('<%:Change login password%>', [
+ E('div', { class: 'alert-message warning' },
+ _('Given password confirmation did not match, password not changed!')),
+ E('div', { 'class': 'right' },
+ E('div', { class: 'btn', click: hideModal }, '<%:Dismiss%>'))
+ ]);
+ }
+ }
+//]]></script>
+
+<input type="password" aria-hidden="true" style="position:absolute; left:-10000px" />
+
+<div class="cbi-map">
+ <h2><%:Router Password%></h2>
+
+ <div class="cbi-section-descr">
+ <%:Changes the administrator password for accessing the device%>
+ </div>
+
+ <div class="cbi-section-node">
+ <div class="cbi-value">
+ <label class="cbi-value-title" for="image"><%:Password%></label>
+ <div class="cbi-value-field">
+ <input type="password" name="pw1" /><!--
+ --><button class="cbi-button cbi-button-neutral" title="<%:Reveal/hide password%>" aria-label="<%:Reveal/hide password%>" onclick="var e = this.previousElementSibling; e.type = (e.type === 'password') ? 'text' : 'password'">∗</button>
+ </div>
+ </div>
+
+ <div class="cbi-value">
+ <label class="cbi-value-title" for="image"><%:Confirmation%></label>
+ <div class="cbi-value-field">
+ <input type="password" name="pw2" /><!--
+ --><button class="cbi-button cbi-button-neutral" title="<%:Reveal/hide password%>" aria-label="<%:Reveal/hide password%>" onclick="var e = this.previousElementSibling; e.type = (e.type === 'password') ? 'text' : 'password'">∗</button>
+ </div>
+ </div>
+ </div>
+</div>
+
+<div class="cbi-page-actions">
+ <button class="btn cbi-button-apply" onclick="submitPassword(event)"><%:Save%></button>
+</div>
+
+<%+footer%>
diff --git a/modules/luci-mod-system/luasrc/view/admin_system/sshkeys.htm b/modules/luci-mod-system/luasrc/view/admin_system/sshkeys.htm
new file mode 100644
index 0000000000..acf008adf3
--- /dev/null
+++ b/modules/luci-mod-system/luasrc/view/admin_system/sshkeys.htm
@@ -0,0 +1,245 @@
+<%+header%>
+
+<style type="text/css">
+ .cbi-dynlist {
+ max-width: 100%;
+ }
+</style>
+
+<script type="application/javascript">//<![CDATA[
+ SSHPubkeyDecoder.prototype = {
+ lengthDecode: function(s, off)
+ {
+ var l = (s.charCodeAt(off++) << 24) |
+ (s.charCodeAt(off++) << 16) |
+ (s.charCodeAt(off++) << 8) |
+ s.charCodeAt(off++);
+
+ if (l < 0 || (off + l) > s.length)
+ return -1;
+
+ return l;
+ },
+
+ decode: function(s)
+ {
+ var parts = s.split(/\s+/);
+ if (parts.length < 2)
+ return null;
+
+ var key = null;
+ try { key = atob(parts[1]); } catch(e) {}
+ if (!key)
+ return null;
+
+ var off, len;
+
+ off = 0;
+ len = this.lengthDecode(key, off);
+
+ if (len <= 0)
+ return null;
+
+ var type = key.substr(off + 4, len);
+ if (type !== parts[0])
+ return null;
+
+ off += 4 + len;
+
+ var len1 = off < key.length ? this.lengthDecode(key, off) : 0;
+ if (len1 <= 0)
+ return null;
+
+ var curve = null;
+ if (type.indexOf('ecdsa-sha2-') === 0) {
+ curve = key.substr(off + 4, len1);
+
+ if (!len1 || type.substr(11) !== curve)
+ return null;
+
+ type = 'ecdsa-sha2';
+ curve = curve.replace(/^nistp(\d+)$/, 'NIST P-$1');
+ }
+
+ off += 4 + len1;
+
+ var len2 = off < key.length ? this.lengthDecode(key, off) : 0;
+ if (len2 < 0)
+ return null;
+
+ if (len1 & 1)
+ len1--;
+
+ if (len2 & 1)
+ len2--;
+
+ var comment = parts.slice(2).join(' '),
+ fprint = parts[1].length > 68 ? parts[1].substr(0, 33) + '…' + parts[1].substr(-34) : parts[1];
+
+ switch (type)
+ {
+ case 'ssh-rsa':
+ return { type: 'RSA', bits: len2 * 8, comment: comment, fprint: fprint };
+
+ case 'ssh-dss':
+ return { type: 'DSA', bits: len1 * 8, comment: comment, fprint: fprint };
+
+ case 'ssh-ed25519':
+ return { type: 'ECDH', curve: 'Curve25519', comment: comment, fprint: fprint };
+
+ case 'ecdsa-sha2':
+ return { type: 'ECDSA', curve: curve, comment: comment, fprint: fprint };
+
+ default:
+ return null;
+ }
+ }
+ };
+
+ function SSHPubkeyDecoder() {}
+
+ function renderKeys(keys) {
+ var list = document.querySelector('.cbi-dynlist[name="sshkeys"]'),
+ decoder = new SSHPubkeyDecoder();
+
+ while (!matchesElem(list.firstElementChild, '.add-item'))
+ list.removeChild(list.firstElementChild);
+
+ keys.forEach(function(key) {
+ var pubkey = decoder.decode(key);
+ if (pubkey)
+ list.insertBefore(E('div', {
+ class: 'item',
+ click: removeKey,
+ 'data-key': key
+ }, [
+ E('strong', pubkey.comment || _('Unnamed key')), E('br'),
+ E('small', [
+ '%s, %s'.format(pubkey.type, pubkey.curve || _('%d Bit').format(pubkey.bits)),
+ E('br'), E('code', pubkey.fprint)
+ ])
+ ]), list.lastElementChild);
+ });
+
+ if (list.firstElementChild === list.lastElementChild)
+ list.insertBefore(E('p', _('No public keys present yet.')), list.lastElementChild);
+ }
+
+ function saveKeys(keys) {
+ showModal('<%:Add key%>', E('div', { class: 'spinning' }, _('Saving keys…')));
+ (new XHR()).post('<%=url("admin/system/admin/sshkeys/json")%>', { token: '<%=token%>', keys: JSON.stringify(keys) }, function(xhr, keys) {
+ renderKeys(keys);
+ hideModal();
+ });
+ }
+
+ function addKey(ev) {
+ var decoder = new SSHPubkeyDecoder(),
+ list = findParent(ev.target, '.cbi-dynlist'),
+ input = list.querySelector('input[type="text"]'),
+ key = input.value.trim(),
+ pubkey = decoder.decode(key),
+ keys = [];
+
+ if (!key.length)
+ return;
+
+ list.querySelectorAll('.item').forEach(function(item) {
+ keys.push(item.getAttribute('data-key'));
+ });
+
+ if (keys.indexOf(key) !== -1) {
+ showModal('<%:Add key%>', [
+ E('div', { class: 'alert-message warning' }, _('The given SSH public key has already been added.')),
+ E('div', { class: 'right' }, E('div', { class: 'btn', click: hideModal }, _('Close')))
+ ]);
+ }
+ else if (!pubkey) {
+ showModal('<%:Add key%>', [
+ E('div', { class: 'alert-message warning' }, _('The given SSH public key is invalid. Please supply proper public RSA or ECDSA keys.')),
+ E('div', { class: 'right' }, E('div', { class: 'btn', click: hideModal }, _('Close')))
+ ]);
+ }
+ else {
+ keys.push(key);
+ saveKeys(keys);
+ input.value = '';
+ }
+ }
+
+ function removeKey(ev) {
+ var list = findParent(ev.target, '.cbi-dynlist'),
+ delkey = ev.target.getAttribute('data-key'),
+ keys = [];
+
+ list.querySelectorAll('.item').forEach(function(item) {
+ var key = item.getAttribute('data-key');
+ if (key !== delkey)
+ keys.push(key);
+ });
+
+ showModal('<%:Delete key%>', [
+ E('div', _('Do you really want to delete the following SSH key?')),
+ E('pre', delkey),
+ E('div', { class: 'right' }, [
+ E('div', { class: 'btn', click: hideModal }, _('Cancel')),
+ ' ',
+ E('div', { class: 'btn danger', click: function(ev) { saveKeys(keys) } }, _('Delete key')),
+ ])
+ ]);
+ }
+
+ function dragKey(ev) {
+ ev.stopPropagation();
+ ev.preventDefault();
+ ev.dataTransfer.dropEffect = 'copy';
+ }
+
+ function dropKey(ev) {
+ var file = ev.dataTransfer.files[0],
+ input = ev.currentTarget.querySelector('input[type="text"]'),
+ reader = new FileReader();
+
+ if (file) {
+ reader.onload = function(rev) {
+ input.value = rev.target.result.trim();
+ addKey(ev);
+ input.value = '';
+ };
+
+ reader.readAsText(file);
+ }
+
+ ev.stopPropagation();
+ ev.preventDefault();
+ }
+
+ window.addEventListener('dragover', function(ev) { ev.preventDefault() });
+ window.addEventListener('drop', function(ev) { ev.preventDefault() });
+
+ requestAnimationFrame(function() {
+ XHR.get('<%=url("admin/system/admin/sshkeys/json")%>', null, function(xhr, keys) {
+ renderKeys(keys);
+ });
+ });
+//]]></script>
+
+<div class="cbi-map">
+ <h2><%:SSH-Keys%></h2>
+
+ <div class="cbi-section-descr">
+ <%_Public keys allow for the passwordless SSH logins with a higher security compared to the use of plain passwords. In order to upload a new key to the device, paste an OpenSSH compatible public key line or drag a <code>.pub</code> file into the input field.%>
+ </div>
+
+ <div class="cbi-section-node">
+ <div class="cbi-dynlist" name="sshkeys">
+ <p class="spinning"><%:Loading SSH keys…%></p>
+ <div class="add-item" ondragover="dragKey(event)" ondrop="dropKey(event)">
+ <input class="cbi-input-text" type="text" placeholder="<%:Paste or drag SSH key file…%>" onkeydown="if (event.keyCode === 13) addKey(event)" /><!--
+ --><div class="cbi-button" onclick="addKey(event)"><%:Add key%></div>
+ </div>
+ </div>
+ </div>
+</div>
+
+<%+footer%>