diff options
Diffstat (limited to 'modules')
15 files changed, 490 insertions, 334 deletions
diff --git a/modules/luci-base/htdocs/luci-static/resources/form.js b/modules/luci-base/htdocs/luci-static/resources/form.js index f0629d0ca7..0630ceec86 100644 --- a/modules/luci-base/htdocs/luci-static/resources/form.js +++ b/modules/luci-base/htdocs/luci-static/resources/form.js @@ -4,6 +4,173 @@ var scope = this; +var CBIJSONConfig = Class.extend({ + __init__: function(data) { + data = Object.assign({}, data); + + this.data = {}; + + var num_sections = 0, + section_ids = []; + + for (var sectiontype in data) { + if (!data.hasOwnProperty(sectiontype)) + continue; + + if (L.isObject(data[sectiontype])) { + this.data[sectiontype] = Object.assign(data[sectiontype], { + '.anonymous': false, + '.name': sectiontype, + '.type': sectiontype + }); + + section_ids.push(sectiontype); + num_sections++; + } + else if (Array.isArray(data[sectiontype])) { + for (var i = 0, index = 0; i < data[sectiontype].length; i++) { + var item = data[sectiontype][i], + anonymous, name; + + if (!L.isObject(item)) + continue; + + if (typeof(item['.name']) == 'string') { + name = item['.name']; + anonymous = false; + } + else { + name = sectiontype + num_sections; + anonymous = true; + } + + if (!this.data.hasOwnProperty(name)) + section_ids.push(name); + + this.data[name] = Object.assign(item, { + '.index': num_sections++, + '.anonymous': anonymous, + '.name': name, + '.type': sectiontype + }); + } + } + } + + section_ids.sort(L.bind(function(a, b) { + var indexA = (this.data[a]['.index'] != null) ? +this.data[a]['.index'] : 9999, + indexB = (this.data[b]['.index'] != null) ? +this.data[b]['.index'] : 9999; + + if (indexA != indexB) + return (indexA - indexB); + + return (a > b); + }, this)); + + for (var i = 0; i < section_ids.length; i++) + this.data[section_ids[i]]['.index'] = i; + }, + + load: function() { + return Promise.resolve(this.data); + }, + + save: function() { + return Promise.resolve(); + }, + + get: function(config, section, option) { + if (section == null) + return null; + + if (option == null) + return this.data[section]; + + if (!this.data.hasOwnProperty(section)) + return null; + + var value = this.data[section][option]; + + if (Array.isArray(value)) + return value; + + if (value != null) + return String(value); + + return null; + }, + + set: function(config, section, option, value) { + if (section == null || option == null || option.charAt(0) == '.') + return; + + if (!this.data.hasOwnProperty(section)) + return; + + if (value == null) + delete this.data[section][option]; + else if (Array.isArray(value)) + this.data[section][option] = value; + else + this.data[section][option] = String(value); + }, + + unset: function(config, section, option) { + return this.set(config, section, option, null); + }, + + sections: function(config, sectiontype, callback) { + var rv = []; + + for (var section_id in this.data) + if (sectiontype == null || this.data[section_id]['.type'] == sectiontype) + rv.push(this.data[section_id]); + + rv.sort(function(a, b) { return a['.index'] - b['.index'] }); + + if (typeof(callback) == 'function') + for (var i = 0; i < rv.length; i++) + callback.call(this, rv[i], rv[i]['.name']); + + return rv; + }, + + add: function(config, sectiontype, sectionname) { + var num_sections_type = 0, next_index = 0; + + for (var name in this.data) { + num_sections_type += (this.data[name]['.type'] == sectiontype); + next_index = Math.max(next_index, this.data[name]['.index']); + } + + var section_id = sectionname || sectiontype + num_sections_type; + + if (!this.data.hasOwnProperty(section_id)) { + this.data[section_id] = { + '.name': section_id, + '.type': sectiontype, + '.anonymous': (sectionname == null), + '.index': next_index + 1 + }; + } + + return section_id; + }, + + remove: function(config, section) { + if (this.data.hasOwnProperty(section)) + delete this.data[section]; + }, + + resolveSID: function(config, section_id) { + return section_id; + }, + + move: function(config, section_id1, section_id2, after) { + return uci.move.apply(this, [config, section_id1, section_id2, after]); + } +}); + var CBINode = Class.extend({ __init__: function(title, description) { this.title = title || ''; @@ -83,6 +250,7 @@ var CBIMap = CBINode.extend({ this.config = config; this.parsechain = [ config ]; + this.data = uci; }, findElements: function(/* ... */) { @@ -118,7 +286,7 @@ var CBIMap = CBINode.extend({ }, load: function() { - return uci.load(this.parsechain || [ this.config ]) + return this.data.load(this.parsechain || [ this.config ]) .then(this.loadChildren.bind(this)); }, @@ -137,7 +305,7 @@ var CBIMap = CBINode.extend({ return this.parse() .then(cb) - .then(uci.save.bind(uci)) + .then(this.data.save.bind(this.data)) .then(this.load.bind(this)) .catch(function(e) { if (!silent) @@ -228,6 +396,16 @@ var CBIMap = CBINode.extend({ } }); +var CBIJSONMap = CBIMap.extend({ + __init__: function(data /*, ... */) { + this.super('__init__', this.varargs(arguments, 1, 'json')); + + this.config = 'json'; + this.parsechain = [ 'json' ]; + this.data = new CBIJSONConfig(data); + } +}); + var CBIAbstractSection = CBINode.extend({ __init__: function(map, sectionType /*, ... */) { this.super('__init__', this.varargs(arguments, 2)); @@ -543,7 +721,7 @@ var CBIAbstractValue = CBINode.extend({ if (section_id == null) L.error('TypeError', 'Section ID required'); - return uci.get( + return this.map.data.get( this.uciconfig || this.section.uciconfig || this.map.config, this.ucisection || section_id, this.ucioption || this.option); @@ -631,7 +809,7 @@ var CBIAbstractValue = CBINode.extend({ }, write: function(section_id, formvalue) { - return uci.set( + return this.map.data.set( this.uciconfig || this.section.uciconfig || this.map.config, this.ucisection || section_id, this.ucioption || this.option, @@ -639,7 +817,7 @@ var CBIAbstractValue = CBINode.extend({ }, remove: function(section_id) { - return uci.unset( + return this.map.data.unset( this.uciconfig || this.section.uciconfig || this.map.config, this.ucisection || section_id, this.ucioption || this.option); @@ -650,7 +828,7 @@ var CBITypedSection = CBIAbstractSection.extend({ __name__: 'CBI.TypedSection', cfgsections: function() { - return uci.sections(this.uciconfig || this.map.config, this.sectiontype) + return this.map.data.sections(this.uciconfig || this.map.config, this.sectiontype) .map(function(s) { return s['.name'] }) .filter(L.bind(this.filter, this)); }, @@ -658,14 +836,14 @@ var CBITypedSection = CBIAbstractSection.extend({ handleAdd: function(ev, name) { var config_name = this.uciconfig || this.map.config; - uci.add(config_name, this.sectiontype, name); + this.map.data.add(config_name, this.sectiontype, name); return this.map.save(null, true); }, handleRemove: function(section_id, ev) { var config_name = this.uciconfig || this.map.config; - uci.remove(config_name, section_id); + this.map.data.remove(config_name, section_id); return this.map.save(null, true); }, @@ -998,7 +1176,7 @@ var CBITableSection = CBITypedSection.extend({ 'title': btn_title || _('Delete'), 'class': 'cbi-button cbi-button-remove', 'click': L.ui.createHandlerFn(this, function(sid, ev) { - uci.remove(config_name, sid); + this.map.data.remove(config_name, sid); return this.map.save(null, true); }, section_id) }, [ btn_title || _('Delete') ]) @@ -1082,7 +1260,7 @@ var CBITableSection = CBITypedSection.extend({ sid2 = s.targetNode.getAttribute('data-sid'); s.node.parentNode.insertBefore(s.node, ref_node); - uci.move(config_name, sid1, sid2, after); + this.map.data.move(config_name, sid1, sid2, after); } scope.dragState = null; @@ -1178,7 +1356,7 @@ var CBIGridSection = CBITableSection.extend({ handleAdd: function(ev) { var config_name = this.uciconfig || this.map.config, - section_id = uci.add(config_name, this.sectiontype); + section_id = this.map.data.add(config_name, this.sectiontype); this.addedSection = section_id; return this.renderMoreOptionsModal(section_id); @@ -1193,7 +1371,7 @@ var CBIGridSection = CBITableSection.extend({ var config_name = this.uciconfig || this.map.config; if (this.addedSection != null) { - uci.remove(config_name, this.addedSection); + this.map.data.remove(config_name, this.addedSection); this.addedSection = null; } @@ -1273,7 +1451,7 @@ var CBINamedSection = CBIAbstractSection.extend({ var section_id = this.section, config_name = this.uciconfig || this.map.config; - uci.add(config_name, this.sectiontype, section_id); + this.map.data.add(config_name, this.sectiontype, section_id); return this.map.save(null, true); }, @@ -1281,7 +1459,7 @@ var CBINamedSection = CBIAbstractSection.extend({ var section_id = this.section, config_name = this.uciconfig || this.map.config; - uci.remove(config_name, section_id); + this.map.data.remove(config_name, section_id); return this.map.save(null, true); }, @@ -1337,7 +1515,7 @@ var CBINamedSection = CBIAbstractSection.extend({ section_id = this.section; return Promise.all([ - uci.get(config_name, section_id), + this.map.data.get(config_name, section_id), this.renderUCISection(section_id) ]).then(this.renderContents.bind(this)); } @@ -1737,6 +1915,7 @@ var CBISectionValue = CBIValue.extend({ return L.Class.extend({ Map: CBIMap, + JSONMap: CBIJSONMap, AbstractSection: CBIAbstractSection, AbstractValue: CBIAbstractValue, diff --git a/modules/luci-base/htdocs/luci-static/resources/luci.js b/modules/luci-base/htdocs/luci-static/resources/luci.js index bcc6870bd2..0b7ec6ea86 100644 --- a/modules/luci-base/htdocs/luci-static/resources/luci.js +++ b/modules/luci-base/htdocs/luci-static/resources/luci.js @@ -1337,23 +1337,22 @@ }, addFooter: function() { - var footer = E([]), - mc = document.getElementById('maincontent'); + var footer = E([]); - if (mc.querySelector('.cbi-map')) { + if (this.handleSaveApply || this.handleSave || this.handleReset) { footer.appendChild(E('div', { 'class': 'cbi-page-actions' }, [ - E('button', { + this.handleSaveApply ? E('button', { 'class': 'cbi-button cbi-button-apply', 'click': L.ui.createHandlerFn(this, 'handleSaveApply') - }, _('Save & Apply')), ' ', - E('button', { + }, _('Save & Apply')) : '', ' ', + this.handleSave ? E('button', { 'class': 'cbi-button cbi-button-save', 'click': L.ui.createHandlerFn(this, 'handleSave') - }, _('Save')), ' ', - E('button', { + }, _('Save')) : '', ' ', + this.handleReset ? E('button', { 'class': 'cbi-button cbi-button-reset', 'click': L.ui.createHandlerFn(this, 'handleReset') - }, _('Reset')) + }, _('Reset')) : '' ])); } diff --git a/modules/luci-base/htdocs/luci-static/resources/network.js b/modules/luci-base/htdocs/luci-static/resources/network.js index 525b7c9f19..d0282ad01f 100644 --- a/modules/luci-base/htdocs/luci-static/resources/network.js +++ b/modules/luci-base/htdocs/luci-static/resources/network.js @@ -463,7 +463,9 @@ function initNetworkState(refresh) { if (a.family == 'packet') { s.netdevs[name].flags = a.flags; s.netdevs[name].stats = a.data; - s.netdevs[name].macaddr = a.addr; + + if (a.addr != null && a.addr != '00:00:00:00:00:00' && a.addr.length == 17) + s.netdevs[name].macaddr = a.addr; } else if (a.family == 'inet') { s.netdevs[name].ipaddrs.push(a.addr + '/' + a.netmask); @@ -1756,10 +1758,16 @@ Device = L.Class.extend({ this.network = network; }, - _ubus: function(field) { - var dump = _state.devices[this.ifname] || {}; + _devstate: function(/* ... */) { + var rv = this.dev; + + for (var i = 0; i < arguments.length; i++) + if (L.isObject(rv)) + rv = rv[arguments[i]]; + else + return null; - return (field != null ? dump[field] : dump); + return rv; }, getName: function() { @@ -1767,24 +1775,21 @@ Device = L.Class.extend({ }, getMAC: function() { - var mac = (this.dev != null ? this.dev.macaddr : null); - if (mac == null) - mac = this._ubus('macaddr'); - + var mac = this._devstate('macaddr'); return mac ? mac.toUpperCase() : null; }, getMTU: function() { - return this.dev ? this.dev.mtu : null; + return this._devstate('mtu'); }, getIPAddrs: function() { - var addrs = (this.dev != null ? this.dev.ipaddrs : null); + var addrs = this._devstate('ipaddrs'); return (Array.isArray(addrs) ? addrs : []); }, getIP6Addrs: function() { - var addrs = (this.dev != null ? this.dev.ip6addrs : null); + var addrs = this._devstate('ip6addrs'); return (Array.isArray(addrs) ? addrs : []); }, @@ -1874,7 +1879,7 @@ Device = L.Class.extend({ }, isUp: function() { - var up = this._ubus('up'); + var up = this._devstate('flags', 'up'); if (up == null) up = (this.getType() == 'alias'); @@ -1887,26 +1892,26 @@ Device = L.Class.extend({ }, isBridgePort: function() { - return (this.dev != null && this.dev.bridge != null); + return (this._devstate('bridge') != null); }, getTXBytes: function() { - var stat = this._ubus('statistics'); + var stat = this._devstate('stats'); return (stat != null ? stat.tx_bytes || 0 : 0); }, getRXBytes: function() { - var stat = this._ubus('statistics'); + var stat = this._devstate('stats'); return (stat != null ? stat.rx_bytes || 0 : 0); }, getTXPackets: function() { - var stat = this._ubus('statistics'); + var stat = this._devstate('stats'); return (stat != null ? stat.tx_packets || 0 : 0); }, getRXPackets: function() { - var stat = this._ubus('statistics'); + var stat = this._devstate('stats'); return (stat != null ? stat.rx_packets || 0 : 0); }, diff --git a/modules/luci-base/root/usr/libexec/rpcd/luci b/modules/luci-base/root/usr/libexec/rpcd/luci index 99c172a96b..fb15dab6a5 100755 --- a/modules/luci-base/root/usr/libexec/rpcd/luci +++ b/modules/luci-base/root/usr/libexec/rpcd/luci @@ -396,6 +396,7 @@ local methods = { rv.zram = fs.access("/sys/class/zram-control") rv.sysntpd = fs.readlink("/usr/sbin/ntpd") and true rv.ipv6 = fs.access("/proc/net/ipv6_route") + rv.dropbear = fs.access("/usr/sbin/dropbear") local wifi_features = { "eap", "11n", "11ac", "11r", "11w", "acs", "sae", "owe", "suiteb192" } @@ -575,6 +576,20 @@ local methods = { return { error = err } end end + }, + + setPassword = { + args = { username = "root", password = "password" }, + call = function(args) + local util = require "luci.util" + return { + result = (os.execute("(echo %s; sleep 1; echo %s) | passwd %s >/dev/null 2>&1" %{ + luci.util.shellquote(args.password), + luci.util.shellquote(args.password), + luci.util.shellquote(args.username) + }) == 0) + } + end } } diff --git a/modules/luci-base/root/usr/share/rpcd/acl.d/luci-base.json b/modules/luci-base/root/usr/share/rpcd/acl.d/luci-base.json index d364508c27..32cb10596b 100644 --- a/modules/luci-base/root/usr/share/rpcd/acl.d/luci-base.json +++ b/modules/luci-base/root/usr/share/rpcd/acl.d/luci-base.json @@ -24,6 +24,7 @@ "/": [ "list" ], "/*": [ "list" ], "/etc/crontabs/root": [ "read" ], + "/etc/dropbear/authorized_keys": [ "read" ], "/etc/rc.local": [ "read" ], "/proc/sys/kernel/hostname": [ "read" ] }, @@ -42,13 +43,14 @@ "cgi-io": [ "upload", "/etc/luci-uploads/*" ], "file": { "/etc/crontabs/root": [ "write" ], + "/etc/dropbear/authorized_keys": [ "write" ], "/etc/luci-uploads/*": [ "write" ], "/etc/rc.local": [ "write" ] }, "ubus": { "file": [ "write", "remove" ], "iwinfo": [ "scan" ], - "luci": [ "setInitAction", "setLocaltime" ], + "luci": [ "setInitAction", "setLocaltime", "setPassword" ], "uci": [ "add", "apply", "confirm", "delete", "order", "set", "rename" ] }, "uci": [ "*" ] diff --git a/modules/luci-mod-network/htdocs/luci-static/resources/view/network/interfaces.js b/modules/luci-mod-network/htdocs/luci-static/resources/view/network/interfaces.js index c79deea27a..624718dd84 100644 --- a/modules/luci-mod-network/htdocs/luci-static/resources/view/network/interfaces.js +++ b/modules/luci-mod-network/htdocs/luci-static/resources/view/network/interfaces.js @@ -65,7 +65,7 @@ function render_status(node, ifc, with_device) { ipaddrs = changecount ? [] : ifc.getIPAddrs(), ip6addrs = changecount ? [] : ifc.getIP6Addrs(), errors = ifc.getErrors(), - maindev = ifc.getDevice(), + maindev = ifc.getL3Device() || ifc.getDevice(), macaddr = maindev ? maindev.getMAC() : null; return L.itemlist(node, [ diff --git a/modules/luci-mod-system/htdocs/luci-static/resources/view/system/crontab.js b/modules/luci-mod-system/htdocs/luci-static/resources/view/system/crontab.js index c8baa47229..286155790a 100644 --- a/modules/luci-mod-system/htdocs/luci-static/resources/view/system/crontab.js +++ b/modules/luci-mod-system/htdocs/luci-static/resources/view/system/crontab.js @@ -40,13 +40,10 @@ return L.view.extend({ E('p', {}, _('This is the system crontab in which scheduled tasks can be defined.') + _('<br/>Note: you need to manually restart the cron service if the crontab file was empty before editing.')), - E('p', {}, E('textarea', { 'style': 'width:100%', 'rows': 10 }, crontab != null ? crontab : '')), - E('div', { 'class': 'right' }, [ - E('button', { - 'class': 'btn cbi-button-positive important', - 'click': L.ui.createHandlerFn(this, 'handleSave') - }, _('Save')) - ]) + E('p', {}, E('textarea', { 'style': 'width:100%', 'rows': 10 }, crontab != null ? crontab : '')) ]); - } + }, + + handleSaveApply: null, + handleReset: null }); diff --git a/modules/luci-mod-system/htdocs/luci-static/resources/view/system/dropbear.js b/modules/luci-mod-system/htdocs/luci-static/resources/view/system/dropbear.js new file mode 100644 index 0000000000..7a8b1428d5 --- /dev/null +++ b/modules/luci-mod-system/htdocs/luci-static/resources/view/system/dropbear.js @@ -0,0 +1,42 @@ +'use strict'; +'require form'; +'require tools.widgets as widgets'; + +return L.view.extend({ + render: function() { + var m, s, o; + + m = new form.Map('dropbear', _('SSH Access'), _('Dropbear offers <abbr title="Secure Shell">SSH</abbr> network shell access and an integrated <abbr title="Secure Copy">SCP</abbr> server')); + + s = m.section(form.TypedSection, 'dropbear', _('Dropbear Instance')); + s.anonymous = true; + s.addremove = true; + s.addbtntitle = _('Add instance'); + + o = s.option(widgets.NetworkSelect, 'Interface', _('Interface'), _('Listen only on the given interface or, if unspecified, on all')); + o.nocreate = true; + o.unspecified = true; + + o = s.option(form.Value, 'Port', _('Port')); + o.datatype = 'port'; + o.placeholder = 22; + + o = s.option(form.Flag, 'PasswordAuth', _('Password authentication'), _('Allow <abbr title="Secure Shell">SSH</abbr> password authentication')); + o.enabled = 'on'; + o.disabled = 'off'; + o.default = o.enabled; + o.rmempty = false; + + o = s.option(form.Flag, 'RootPasswordAuth', _('Allow root logins with password'), _('Allow the <em>root</em> user to login with password')); + o.enabled = 'on'; + o.disabled = 'off'; + o.default = o.enabled; + + o = s.option(form.Flag, 'GatewayPorts', _('Gateway Ports'), _('Allow remote hosts to connect to local SSH forwarded ports')); + o.enabled = 'on'; + o.disabled = 'off'; + o.default = o.disabled; + + return m.render(); + } +}); diff --git a/modules/luci-mod-system/htdocs/luci-static/resources/view/system/password.js b/modules/luci-mod-system/htdocs/luci-static/resources/view/system/password.js index 7a79d7e2da..6c5ffa1b26 100644 --- a/modules/luci-mod-system/htdocs/luci-static/resources/view/system/password.js +++ b/modules/luci-mod-system/htdocs/luci-static/resources/view/system/password.js @@ -1,31 +1,94 @@ -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) { - L.showModal(_('Change login password'), - E('p', { class: 'spinning' }, _('Changing password…'))); - - L.post('admin/system/admin/password/json', { 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: L.hideModal }, _('Dismiss'))) - ]); - - pw1.value = pw2.value = ''; - }); - } - else { - L.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: L.hideModal }, _('Dismiss'))) - ]); +'use strict'; +'require form'; +'require rpc'; + +var formData = { + password: { + pw1: null, + pw2: null } -} +}; + +var callSetPassword = rpc.declare({ + object: 'luci', + method: 'setPassword', + params: [ 'username', 'password' ], + expect: { result: false } +}); + +return L.view.extend({ + checkPassword: function(section_id, value) { + var strength = document.querySelector('.cbi-value-description'), + strongRegex = new RegExp("^(?=.{8,})(?=.*[A-Z])(?=.*[a-z])(?=.*[0-9])(?=.*\\W).*$", "g"), + mediumRegex = new RegExp("^(?=.{7,})(((?=.*[A-Z])(?=.*[a-z]))|((?=.*[A-Z])(?=.*[0-9]))|((?=.*[a-z])(?=.*[0-9]))).*$", "g"), + enoughRegex = new RegExp("(?=.{6,}).*", "g"); + + if (strength && value.length) { + if (false == enoughRegex.test(value)) + strength.innerHTML = '%s: <span style="color:red">%s</span>'.format(_('Password strength'), _('More Characters')); + else if (strongRegex.test(value)) + strength.innerHTML = '%s: <span style="color:green">%s</span>'.format(_('Password strength'), _('Strong')); + else if (mediumRegex.test(value)) + strength.innerHTML = '%s: <span style="color:orange">%s</span>'.format(_('Password strength'), _('Medium')); + else + strength.innerHTML = '%s: <span style="color:red">%s</span>'.format(_('Password strength'), _('Weak')); + } + + return true; + }, + + render: function() { + var m, s, o; + + m = new form.JSONMap(formData, _('Router Password'), _('Changes the administrator password for accessing the device')); + s = m.section(form.NamedSection, 'password', 'password'); + + o = s.option(form.Value, 'pw1', _('Password')); + o.password = true; + o.validate = this.checkPassword; + + o = s.option(form.Value, 'pw2', _('Confirmation'), ' '); + o.password = true; + o.renderWidget = function(/* ... */) { + var node = form.Value.prototype.renderWidget.apply(this, arguments); + + node.childNodes[1].addEventListener('keydown', function(ev) { + if (ev.keyCode == 13 && !ev.currentTarget.classList.contains('cbi-input-invalid')) + document.querySelector('.cbi-button-save').click(); + }); + + return node; + }; + + return m.render(); + }, + + handleSave: function() { + var map = document.querySelector('.cbi-map'); + + return L.dom.callClassMethod(map, 'save').then(function() { + if (formData.password.pw1 == null || formData.password.pw1.length == 0) + return; + + if (formData.password.pw1 != formData.password.pw2) { + L.ui.addNotification(null, E('p', _('Given password confirmation did not match, password not changed!')), 'danger'); + return; + } + + return callSetPassword('root', formData.password.pw1).then(function(success) { + if (success) + L.ui.addNotification(null, E('p', _('The system password has been successfully changed.')), 'info'); + else + L.ui.addNotification(null, E('p', _('Failed to change the system password.')), 'danger'); + + formData.password.pw1 = null; + formData.password.pw2 = null; + + L.dom.callClassMethod(map, 'render'); + }); + }); + }, + + handleSaveApply: null, + handleReset: null +}); diff --git a/modules/luci-mod-system/htdocs/luci-static/resources/view/system/sshkeys.js b/modules/luci-mod-system/htdocs/luci-static/resources/view/system/sshkeys.js index d298b3be98..a68cb6b0bf 100644 --- a/modules/luci-mod-system/htdocs/luci-static/resources/view/system/sshkeys.js +++ b/modules/luci-mod-system/htdocs/luci-static/resources/view/system/sshkeys.js @@ -1,4 +1,7 @@ -SSHPubkeyDecoder.prototype = { +'use strict'; +'require rpc'; + +var SSHPubkeyDecoder = L.Class.singleton({ lengthDecode: function(s, off) { var l = (s.charCodeAt(off++) << 24) | @@ -85,19 +88,29 @@ SSHPubkeyDecoder.prototype = { return null; } } -}; +}); -function SSHPubkeyDecoder() {} +var callFileRead = rpc.declare({ + object: 'file', + method: 'read', + params: [ 'path' ], + expect: { data: '' } +}); + +var callFileWrite = rpc.declare({ + object: 'file', + method: 'write', + params: [ 'path', 'data' ] +}); function renderKeys(keys) { - var list = document.querySelector('.cbi-dynlist[name="sshkeys"]'), - decoder = new SSHPubkeyDecoder(); + var list = document.querySelector('.cbi-dynlist[name="sshkeys"]'); while (!matchesElem(list.firstElementChild, '.add-item')) list.removeChild(list.firstElementChild); keys.forEach(function(key) { - var pubkey = decoder.decode(key); + var pubkey = SSHPubkeyDecoder.decode(key); if (pubkey) list.insertBefore(E('div', { class: 'item', @@ -117,19 +130,16 @@ function renderKeys(keys) { } function saveKeys(keys) { - L.showModal(_('Add key'), E('div', { class: 'spinning' }, _('Saving keys…'))); - L.post('admin/system/admin/sshkeys/json', { keys: JSON.stringify(keys) }, function(xhr, keys) { - renderKeys(keys); - L.hideModal(); - }); + return callFileWrite('/etc/dropbear/authorized_keys', keys.join('\n') + '\n') + .then(renderKeys.bind(this, keys)) + .then(L.ui.hideModal); } function addKey(ev) { - var decoder = new SSHPubkeyDecoder(), - list = findParent(ev.target, '.cbi-dynlist'), + var list = findParent(ev.target, '.cbi-dynlist'), input = list.querySelector('input[type="text"]'), key = input.value.trim(), - pubkey = decoder.decode(key), + pubkey = SSHPubkeyDecoder.decode(key), keys = []; if (!key.length) @@ -140,21 +150,26 @@ function addKey(ev) { }); if (keys.indexOf(key) !== -1) { - L.showModal(_('Add key'), [ + L.ui.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: L.hideModal }, _('Close'))) ]); } else if (!pubkey) { - L.showModal(_('Add key'), [ + L.ui.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: L.hideModal }, _('Close'))) ]); } else { keys.push(key); - saveKeys(keys); input.value = ''; + + return saveKeys(keys).then(function() { + var added = list.querySelector('[data-key="%s"]'.format(key)); + if (added) + added.classList.add('flash'); + }); } } @@ -175,7 +190,7 @@ function removeKey(ev) { E('div', { class: 'right' }, [ E('div', { class: 'btn', click: L.hideModal }, _('Cancel')), ' ', - E('div', { class: 'btn danger', click: function(ev) { saveKeys(keys) } }, _('Delete key')), + E('div', { class: 'btn danger', click: L.ui.createHandlerFn(this, saveKeys, keys) }, _('Delete key')), ]) ]); } @@ -205,11 +220,67 @@ function dropKey(ev) { ev.preventDefault(); } -window.addEventListener('dragover', function(ev) { ev.preventDefault() }); -window.addEventListener('drop', function(ev) { ev.preventDefault() }); +function handleWindowDragDropIgnore(ev) { + ev.preventDefault() +} -requestAnimationFrame(function() { - L.get('admin/system/admin/sshkeys/json', null, function(xhr, keys) { - renderKeys(keys); - }); +return L.view.extend({ + load: function() { + return callFileRead('/etc/dropbear/authorized_keys').then(function(data) { + return (data || '').split(/\n/).map(function(line) { + return line.trim(); + }).filter(function(line) { + return line.match(/^ssh-/) != null; + }); + }); + }, + + render: function(keys) { + var list = E('div', { 'class': 'cbi-dynlist', 'dragover': dragKey, 'drop': dropKey }, [ + E('div', { 'class': 'add-item' }, [ + E('input', { + 'class': 'cbi-input-text', + 'type': 'text', + 'placeholder': _('Paste or drag SSH key file…') , + 'keydown': function(ev) { if (ev.keyCode === 13) addKey(ev) } + }), + E('button', { + 'class': 'cbi-button', + 'click': L.ui.createHandlerFn(this, addKey) + }, _('Add key')) + ]) + ]); + + keys.forEach(L.bind(function(key) { + var pubkey = SSHPubkeyDecoder.decode(key); + if (pubkey) + list.insertBefore(E('div', { + class: 'item', + click: L.ui.createHandlerFn(this, 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); + }, this)); + + if (list.firstElementChild === list.lastElementChild) + list.insertBefore(E('p', _('No public keys present yet.')), list.lastElementChild); + + window.addEventListener('dragover', handleWindowDragDropIgnore); + window.addEventListener('drop', handleWindowDragDropIgnore); + + return E('div', {}, [ + E('h2', _('SSH-Keys')), + E('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.')), + E('div', { 'class': 'cbi-section-node' }, list) + ]); + }, + + handleSaveApply: null, + handleSave: null, + handleReset: null }); diff --git a/modules/luci-mod-system/htdocs/luci-static/resources/view/system/startup.js b/modules/luci-mod-system/htdocs/luci-static/resources/view/system/startup.js index 3c026e0889..365e6c8ed8 100644 --- a/modules/luci-mod-system/htdocs/luci-static/resources/view/system/startup.js +++ b/modules/luci-mod-system/htdocs/luci-static/resources/view/system/startup.js @@ -55,7 +55,7 @@ return L.view.extend({ }, this, name, !isEnabled, ev.currentTarget.parentNode)); }, - handleSave: function(ev) { + handleRcLocalSave: function(ev) { var value = (document.querySelector('textarea').value || '').trim().replace(/\r\n/g, '\n') + '\n'; return this.callFileWrite('/etc/rc.local', value).then(function(rc) { @@ -126,10 +126,10 @@ return L.view.extend({ E('div', { 'data-tab': 'rc', 'data-tab-title': _('Local Startup') }, [ E('p', {}, _('This is the content of /etc/rc.local. Insert your own commands here (in front of \'exit 0\') to execute them at the end of the boot process.')), E('p', {}, E('textarea', { 'style': 'width:100%', 'rows': 20 }, rcLocal != null ? rcLocal : '')), - E('div', { 'class': 'right' }, [ + E('div', { 'class': 'cbi-page-actions' }, [ E('button', { - 'class': 'btn cbi-button-positive important', - 'click': L.ui.createHandlerFn(this, 'handleSave') + 'class': 'btn cbi-button-save', + 'click': L.ui.createHandlerFn(this, 'handleRcLocalSave') }, _('Save')) ]) ]) @@ -139,5 +139,9 @@ return L.view.extend({ L.ui.tabs.initTabGroup(view.lastElementChild.childNodes); return view; - } + }, + + handleSaveApply: null, + handleSave: null, + handleReset: null }); diff --git a/modules/luci-mod-system/luasrc/controller/admin/system.lua b/modules/luci-mod-system/luasrc/controller/admin/system.lua index b9785994ad..0ce08d10bd 100644 --- a/modules/luci-mod-system/luasrc/controller/admin/system.lua +++ b/modules/luci-mod-system/luasrc/controller/admin/system.lua @@ -12,13 +12,11 @@ function index() entry({"admin", "system", "ntp_restart"}, call("action_ntp_restart"), nil).leaf = true entry({"admin", "system", "admin"}, firstchild(), _("Administration"), 2) - entry({"admin", "system", "admin", "password"}, template("admin_system/password"), _("Router Password"), 1) - entry({"admin", "system", "admin", "password", "json"}, post("action_password")) + entry({"admin", "system", "admin", "password"}, view("system/password"), _("Router Password"), 1) if fs.access("/etc/config/dropbear") then - entry({"admin", "system", "admin", "dropbear"}, cbi("admin_system/dropbear"), _("SSH Access"), 2) - entry({"admin", "system", "admin", "sshkeys"}, template("admin_system/sshkeys"), _("SSH-Keys"), 3) - entry({"admin", "system", "admin", "sshkeys", "json"}, post_on({ keys = true }, "action_sshkeys")) + entry({"admin", "system", "admin", "dropbear"}, view("system/dropbear"), _("SSH Access"), 2) + entry({"admin", "system", "admin", "sshkeys"}, view("system/sshkeys"), _("SSH-Keys"), 3) end entry({"admin", "system", "startup"}, view("system/startup"), _("Startup"), 45) @@ -282,67 +280,6 @@ function action_reset() http.redirect(luci.dispatcher.build_url('admin/system/flashops')) end -function action_password() - local password = luci.http.formvalue("password") - if not password then - luci.http.status(400, "Bad Request") - return - end - - luci.http.prepare_content("application/json") - luci.http.write_json({ code = luci.sys.user.setpasswd("root", password) }) -end - -function action_sshkeys() - local keys = luci.http.formvalue("keys") - if keys then - keys = luci.jsonc.parse(keys) - if not keys or type(keys) ~= "table" then - luci.http.status(400, "Bad Request") - return - end - - local fd, err = io.open("/etc/dropbear/authorized_keys", "w") - if not fd then - luci.http.status(503, err) - return - end - - local _, k - for _, k in ipairs(keys) do - if type(k) == "string" and k:match("^%w+%-") then - fd:write(k) - fd:write("\n") - end - end - - fd:close() - end - - local fd, err = io.open("/etc/dropbear/authorized_keys", "r") - if not fd then - luci.http.status(503, err) - return - end - - local rv = {} - while true do - local ln = fd:read("*l") - if not ln then - break - elseif ln:match("^[%w%-]+%s+[A-Za-z0-9+/=]+$") or - ln:match("^[%w%-]+%s+[A-Za-z0-9+/=]+%s") - then - rv[#rv+1] = ln - end - end - - fd:close() - - luci.http.prepare_content("application/json") - luci.http.write_json(rv) -end - function action_reboot() luci.sys.reboot() end diff --git a/modules/luci-mod-system/luasrc/model/cbi/admin_system/dropbear.lua b/modules/luci-mod-system/luasrc/model/cbi/admin_system/dropbear.lua deleted file mode 100644 index 1a1695d2be..0000000000 --- a/modules/luci-mod-system/luasrc/model/cbi/admin_system/dropbear.lua +++ /dev/null @@ -1,53 +0,0 @@ --- Copyright 2008 Steven Barth <steven@midlink.org> --- Copyright 2011-2018 Jo-Philipp Wich <jo@mein.io> --- Licensed to the public under the Apache License 2.0. - -m = Map("dropbear", translate("SSH Access"), - translate("Dropbear offers <abbr title=\"Secure Shell\">SSH</abbr> network shell access and an integrated <abbr title=\"Secure Copy\">SCP</abbr> server")) -m.apply_on_parse = true - -s = m:section(TypedSection, "dropbear", translate("Dropbear Instance")) -s.anonymous = true -s.addremove = true - - -ni = s:option(Value, "Interface", translate("Interface"), - translate("Listen only on the given interface or, if unspecified, on all")) - -ni.template = "cbi/network_netlist" -ni.nocreate = true -ni.unspecified = true - - -pt = s:option(Value, "Port", translate("Port"), - translate("Specifies the listening port of this <em>Dropbear</em> instance")) - -pt.datatype = "port" -pt.default = 22 - - -pa = s:option(Flag, "PasswordAuth", translate("Password authentication"), - translate("Allow <abbr title=\"Secure Shell\">SSH</abbr> password authentication")) - -pa.enabled = "on" -pa.disabled = "off" -pa.default = pa.enabled -pa.rmempty = false - - -ra = s:option(Flag, "RootPasswordAuth", translate("Allow root logins with password"), - translate("Allow the <em>root</em> user to login with password")) - -ra.enabled = "on" -ra.disabled = "off" -ra.default = ra.enabled - - -gp = s:option(Flag, "GatewayPorts", translate("Gateway ports"), - translate("Allow remote hosts to connect to local SSH forwarded ports")) - -gp.enabled = "on" -gp.disabled = "off" -gp.default = gp.disabled - -return m diff --git a/modules/luci-mod-system/luasrc/view/admin_system/password.htm b/modules/luci-mod-system/luasrc/view/admin_system/password.htm deleted file mode 100644 index 6ca02a83c1..0000000000 --- a/modules/luci-mod-system/luasrc/view/admin_system/password.htm +++ /dev/null @@ -1,59 +0,0 @@ -<%+header%> - -<input type="password" aria-hidden="true" style="position:absolute; left:-10000px" /> - -<script type="text/javascript"> -function checkPassword() { - var pw1 = document.body.querySelector('[name="pw1"]'); - var view = document.getElementById("passstrength"); - - var strongRegex = new RegExp("^(?=.{8,})(?=.*[A-Z])(?=.*[a-z])(?=.*[0-9])(?=.*\\W).*$", "g"); - var mediumRegex = new RegExp("^(?=.{7,})(((?=.*[A-Z])(?=.*[a-z]))|((?=.*[A-Z])(?=.*[0-9]))|((?=.*[a-z])(?=.*[0-9]))).*$", "g"); - var enoughRegex = new RegExp("(?=.{6,}).*", "g"); - if (false == enoughRegex.test(pw1.value)) { - view.innerHTML = '<%:Password strength%>: <span style="color:red"><%:More Characters%></span>'; - } else if (strongRegex.test(pw1.value)) { - view.innerHTML = '<%:Password strength%>: <span style="color:green"><%:Strong%></span>'; - } else if (mediumRegex.test(pw1.value)) { - view.innerHTML = '<%:Password strength%>: <span style="color:orange"><%:Medium%></span>'; - } else { - view.innerHTML = '<%:Password strength%>: <span style="color:red"><%:Weak%></span>'; - } - return true; -} -</script> - -<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" onkeyup="checkPassword()"/><!-- - --><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" onkeydown="if (event.keyCode === 13) submitPassword(event)" /><!-- - --><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 id="passstrength" class="cbi-value-description"></div> - </div> - </div> - </div> -</div> - -<div class="cbi-page-actions"> - <button class="btn cbi-button-apply" onclick="submitPassword(event)"><%:Save%></button> -</div> - -<script type="application/javascript" src="<%=resource%>/view/system/password.js"></script> - -<%+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 deleted file mode 100644 index ac453f3f6c..0000000000 --- a/modules/luci-mod-system/luasrc/view/admin_system/sshkeys.htm +++ /dev/null @@ -1,46 +0,0 @@ -<%+header%> - -<style type="text/css"> - .cbi-dynlist { - max-width: 100%; - } - - .cbi-dynlist .item > small { - display: block; - direction: rtl; - overflow: hidden; - text-align: left; - } - - .cbi-dynlist .item > small > code { - direction: ltr; - white-space: nowrap; - unicode-bidi: bidi-override; - } - - @media screen and (-ms-high-contrast: active), (-ms-high-contrast: none) { - .cbi-dynlist .item > small { direction: ltr } - } -</style> - -<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)" /> - <button class="cbi-button" onclick="addKey(event)"><%:Add key%></button> - </div> - </div> - </div> -</div> - -<script type="application/javascript" src="<%=resource%>/view/system/sshkeys.js"></script> - -<%+footer%> |