'use strict'; 'require ui'; 'require form'; 'require network'; 'require firewall'; 'require fs'; /** * Get users found in `/etc/passwd`. * @returns {string[]} */ function getUsers() { return fs.lines('/etc/passwd').then(function(lines) { return lines.map(function(line) { return line.split(/:/)[0] }); }); } /** * Get users found in `/etc/group`. * @returns {string[]} */ function getGroups() { return fs.lines('/etc/group').then(function(lines) { return lines.map(function(line) { return line.split(/:/)[0] }); }); } /** * Get bridge devices or Layer 3 devices of a network object. * @param {object} network * @returns {string[]} */ function getDevices(network) { if (network.isBridge()) { var devices = network.getDevices(); return devices ? devices : []; } else { return L.toArray(network.getL3Device()); } } var CBIZoneSelect = form.ListValue.extend({ __name__: 'CBI.ZoneSelect', load(section_id) { return Promise.all([ firewall.getZones(), network.getNetworks() ]).then(L.bind(function(zn) { this.zones = zn[0]; this.networks = zn[1]; return this.super('load', section_id); }, this)); }, filter(section_id, value) { return true; }, lookupZone(name) { return this.zones.filter(function(zone) { return zone.getName() == name })[0]; }, lookupNetwork(name) { return this.networks.filter(function(network) { return network.getName() == name })[0]; }, renderWidget(section_id, option_index, cfgvalue) { const values = L.toArray((cfgvalue != null) ? cfgvalue : this.default); let isOutputOnly = false; const choices = {}; let datatype_str = 'ucifw4zonename'; if (!L.hasSystemFeature('firewall4')) datatype_str = `and(${datatype_str},maxlength(11))`; if (this.allowany && this.nocreate) datatype_str = `or(${datatype_str},"*")`; if (this.multiple) datatype_str = `list(${datatype_str})`; if (this.option == 'dest') { for (let c of this.section.children) { const opt = c; if (opt.option == 'src') { const val = opt.cfgvalue(section_id) || opt.default; isOutputOnly = (val == null || val == ''); break; } } this.title = isOutputOnly ? _('Output zone') : _('Destination zone'); } if (this.allowlocal) { choices[''] = E('span', { 'class': 'zonebadge', 'style': firewall.getZoneColorStyle(null) }, [ E('strong', _('Device')), (this.allowany || this.allowlocal) ? E('span', ' (%s)'.format(this.option != 'dest' ? _('output') : _('input'))) : '' ]); } else if (!this.multiple && (this.rmempty || this.optional)) { choices[''] = E('span', { 'class': 'zonebadge', 'style': firewall.getZoneColorStyle(null) }, E('em', _('unspecified'))); } if (this.allowany) { choices['*'] = E('span', { 'class': 'zonebadge', 'style': firewall.getZoneColorStyle(null) }, [ E('strong', _('Any zone')), (this.allowany && this.allowlocal && !isOutputOnly) ? E('span', ' (%s)'.format(_('forward'))) : '' ]); } for (let zone of this.zones) { const name = zone.getName(); const networks = zone.getNetworks(); const ifaces = []; if (!this.filter(section_id, name)) continue; for (let n of networks) { const network = this.lookupNetwork(n); if (!network) continue; const span = E('span', { 'class': 'ifacebadge' + (network.isUp() ? ' ifacebadge-active' : '') }, network.getName() + ': '); const devices = getDevices(network); for (let d of devices) { span.appendChild(E('img', { 'title': d.getI18n(), 'src': L.resource('icons/%s%s.svg'.format(d.getType(), d.isUp() ? '' : '_disabled')) })); } if (!devices.length) span.appendChild(E('em', _('(empty)'))); ifaces.push(span); } if (!ifaces.length) ifaces.push(E('em', _('(empty)'))); choices[name] = E('span', { 'class': 'zonebadge', 'style': firewall.getZoneColorStyle(zone) }, [ E('strong', name) ].concat(ifaces)); } const widget = new ui.Dropdown(values, choices, { id: this.cbid(section_id), sort: true, multiple: this.multiple, optional: this.optional || this.rmempty, disabled: (this.readonly != null) ? this.readonly : this.map.readonly, select_placeholder: E('em', _('unspecified')), display_items: this.display_size || this.size || 3, dropdown_items: this.dropdown_size || this.size || 5, validate: L.bind(this.validate, this, section_id), datatype: datatype_str, create: !this.nocreate, create_markup: '' + '
  • ' + '' + '{{value}}: ('+_('create')+')' + '' + '
  • ' }); const elem = widget.render(); if (this.option == 'src') { elem.addEventListener('cbi-dropdown-change', L.bind(function(ev) { const opt = this.map.lookupOption('dest', section_id); const val = ev.detail.instance.getValue(); if (opt == null) return; const cbid = opt[0].cbid(section_id); const label = document.querySelector('label[for="widget.%s"]'.format(cbid)); const node = document.getElementById(cbid); L.dom.content(label, val == '' ? _('Output zone') : _('Destination zone')); if (val == '') { if (L.dom.callClassMethod(node, 'getValue') == '') L.dom.callClassMethod(node, 'setValue', '*'); const emptyval = node.querySelector('[data-value=""]'); const anyval = node.querySelector('[data-value="*"]'); L.dom.content(anyval.querySelector('span'), E('strong', _('Any zone'))); if (emptyval != null) emptyval.parentNode.removeChild(emptyval); } else { const anyval = node.querySelector('[data-value="*"]') || ''; let emptyval = node.querySelector('[data-value=""]') || ''; if (emptyval == null && anyval) { emptyval = anyval.cloneNode(true); emptyval.removeAttribute('display'); emptyval.removeAttribute('selected'); emptyval.setAttribute('data-value', ''); } if (opt[0]?.allowlocal && emptyval) L.dom.content(emptyval.querySelector('span'), [ E('strong', _('Device')), E('span', ' (%s)'.format(_('input'))) ]); if (opt[0]?.allowany && anyval && emptyval) { L.dom.content(anyval.querySelector('span'), [ E('strong', _('Any zone')), E('span', ' (%s)'.format(_('forward'))) ]); anyval.parentNode.insertBefore(emptyval, anyval); } } }, this)); } else if (isOutputOnly) { const emptyval = elem.querySelector('[data-value=""]'); emptyval.parentNode.removeChild(emptyval); } return elem; }, }); var CBIZoneForwards = form.DummyValue.extend({ __name__: 'CBI.ZoneForwards', load(section_id) { return Promise.all([ firewall.getDefaults(), firewall.getZones(), network.getNetworks(), network.getDevices() ]).then(L.bind(function(dznd) { this.defaults = dznd[0]; this.zones = dznd[1]; this.networks = dznd[2]; this.devices = dznd[3]; return this.super('load', section_id); }, this)); }, renderZone(zone) { const name = zone.getName(); const networks = zone.getNetworks(); const devices = zone.getDevices(); const subnets = zone.getSubnets(); const ifaces = []; for (let n of networks) { const network = this.networks.filter(function(net) { return net.getName() == n })[0]; if (!network) continue; const span = E('span', { 'class': 'ifacebadge' + (network.isUp() ? ' ifacebadge-active' : '') }, network.getName() + ': '); const subdevs = getDevices(network); for (let s of subdevs) { span.appendChild(E('img', { 'title': s.getI18n(), 'src': L.resource('icons/%s%s.svg'.format(s.getType(), s.isUp() ? '' : '_disabled')) })); } if (!subdevs.length) span.appendChild(E('em', _('(empty)'))); ifaces.push(span); } for (let d of devices) { const device = this.devices.filter(function(dev) { return dev.getName() == d })[0]; const title = device ? device.getI18n() : _('Absent Interface'); const type = device ? device.getType() : 'ethernet'; const up = device ? device.isUp() : false; ifaces.push(E('span', { 'class': 'ifacebadge' }, [ E('img', { 'title': title, 'src': L.resource('icons/%s%s.svg'.format(type, up ? '' : '_disabled')) }), device ? device.getName() : d ])); } if (subnets.length > 0) ifaces.push(E('span', { 'class': 'ifacebadge' }, [ '{ %s }'.format(subnets.join('; ')) ])); if (!ifaces.length) ifaces.push(E('span', { 'class': 'ifacebadge' }, E('em', _('(empty)')))); return E('label', { 'class': 'zonebadge cbi-tooltip-container', 'style': firewall.getZoneColorStyle(zone) }, [ E('strong', name), E('div', { 'class': 'cbi-tooltip' }, ifaces) ]); }, renderWidget(section_id, option_index, cfgvalue) { const value = (cfgvalue != null) ? cfgvalue : this.default; const zone = this.zones.filter(function(z) { return z.getName() == value })[0]; if (!zone) return E([]); const forwards = zone.getForwardingsBy('src'); const dzones = []; for (var i = 0; i < forwards.length; i++) { const dzone = forwards[i].getDestinationZone(); if (!dzone) continue; dzones.push(this.renderZone(dzone)); } if (!dzones.length) dzones.push(E('label', { 'class': 'zonebadge zonebadge-empty' }, E('strong', this.defaults.getForward()))); else dzones.push(E('label', { 'class': 'zonebadge zonebadge-empty' }, E('strong', '%s %s'.format(this.defaults.getForward(), ('all others'))))); return E('div', { 'class': 'zone-forwards' }, [ E('div', { 'class': 'zone-src' }, this.renderZone(zone)), E('span', '⇒'), E('div', { 'class': 'zone-dest' }, dzones) ]); }, }); const CBIIPSelect = form.ListValue.extend({ __name__: 'CBI.IPSelect', load(section_id) { return network.getDevices().then(L.bind(function(devices) { this.devices = devices; return this.super('load', section_id); }, this)); }, filter(section_id, value) { return true; }, renderIfaceBadge(device, ip) { return E('div', {}, [ ip, ' ', E('span', { 'class': 'ifacebadge', }, [ device.getName(), E('img', { 'title': device.getI18n(), 'src': L.resource('icons/%s%s.svg'.format(device.getType(), device.isUp() ? '' : '_disabled')) }) ]), ]); }, renderWidget(section_id, option_index, cfgvalue) { let values = L.toArray((cfgvalue != null) ? cfgvalue : this.default); const choices = {}; const checked = {}; for (const val of values) checked[val] = true; values = []; if (!this.multiple && (this.rmempty || this.optional)) choices[''] = E('em', _('unspecified')); for (const device of (this.devices || [])) { const name = device.getName(); if (name == this.exclude || !this.filter(section_id, name)) continue; if (name == 'loopback' && !this.loopback) continue; if (this.novirtual && device.isVirtual()) continue; for (const ip of [...device.getIPAddrs(), ...device.getIP6Addrs()]) { const iponly = ip.split('/')?.[0] if (checked[iponly]) values.push(iponly); choices[iponly] = this.renderIfaceBadge(device, iponly); } } const widget = new ui.Dropdown(this.multiple ? values : values[0], choices, { id: this.cbid(section_id), sort: true, multiple: this.multiple, optional: this.optional || this.rmempty, disabled: (this.readonly != null) ? this.readonly : this.map.readonly, select_placeholder: E('em', _('unspecified')), display_items: this.display_size || this.size || 2, dropdown_items: this.dropdown_size || this.size || 5, datatype: this.multiple ? 'list(ipaddr)' : 'ipaddr', validate: L.bind(this.validate, this, section_id), create: false, }); return widget.render(); }, textvalue(section_id) { const cfgvalue = this.cfgvalue(section_id); const values = L.toArray((cfgvalue != null) ? cfgvalue : this.default); const rv = E([]); for (const device of (this.devices || [])) { for (const ip of [...device.getIPAddrs(), ...device.getIP6Addrs()]) { const iponly = ip.split('/')[0]; if (values.indexOf(iponly) === -1) continue; if (rv.childNodes.length) rv.appendChild(document.createTextNode(' ')); rv.appendChild(this.renderIfaceBadge(device, iponly)); } } if (!rv.firstChild) rv.appendChild(E('em', _('unspecified'))); return rv; }, }); var CBINetworkSelect = form.ListValue.extend({ __name__: 'CBI.NetworkSelect', load(section_id) { return network.getNetworks().then(L.bind(function(networks) { this.networks = networks; return this.super('load', section_id); }, this)); }, filter(section_id, value) { return true; }, renderIfaceBadge(network) { const span = E('span', { 'class': 'ifacebadge' }, network.getName() + ': '); const devices = getDevices(network); for (let d of devices) { span.appendChild(E('img', { 'title': d.getI18n(), 'src': L.resource('icons/%s%s.svg'.format(d.getType(), d.isUp() ? '' : '_disabled')) })); } if (!devices.length) { span.appendChild(E('em', { 'class': 'hide-close' }, _('(no interfaces attached)'))); span.appendChild(E('em', { 'class': 'hide-open' }, '-')); } return span; }, renderWidget(section_id, option_index, cfgvalue) { let values = L.toArray((cfgvalue != null) ? cfgvalue : this.default); const choices = {}; const checked = {}; for (var i = 0; i < values.length; i++) checked[values[i]] = true; values = []; if (!this.multiple && (this.rmempty || this.optional)) choices[''] = E('em', _('unspecified')); for (let network of this.networks) { const name = network.getName(); if (name == this.exclude || !this.filter(section_id, name)) continue; if (name == 'loopback' && !this.loopback) continue; if (this.novirtual && network.isVirtual()) continue; if (checked[name]) values.push(name); choices[name] = this.renderIfaceBadge(network); } const widget = new ui.Dropdown(this.multiple ? values : values[0], choices, { id: this.cbid(section_id), sort: true, multiple: this.multiple, optional: this.optional || this.rmempty, disabled: (this.readonly != null) ? this.readonly : this.map.readonly, select_placeholder: E('em', _('unspecified')), display_items: this.display_size || this.size || 3, dropdown_items: this.dropdown_size || this.size || 5, datatype: this.multiple ? 'list(uciname)' : 'uciname', validate: L.bind(this.validate, this, section_id), create: !this.nocreate, create_markup: '' + '
  • ' + '' + '{{value}}: ('+_('create')+')' + '' + '
  • ' }); return widget.render(); }, textvalue(section_id) { const cfgvalue = this.cfgvalue(section_id); const values = L.toArray((cfgvalue != null) ? cfgvalue : this.default); const rv = E([]); for (let network of this.networks) { const name = network.getName(); if (values.indexOf(name) == -1) continue; if (rv.length) L.dom.append(rv, ' '); L.dom.append(rv, this.renderIfaceBadge(network)); } if (!rv.firstChild) rv.appendChild(E('em', _('unspecified'))); return rv; }, }); var CBIDeviceSelect = form.ListValue.extend({ __name__: 'CBI.DeviceSelect', load(section_id) { return Promise.all([ network.getDevices(), this.noaliases ? null : network.getNetworks() ]).then(L.bind(function(data) { this.devices = data[0]; this.networks = data[1]; return this.super('load', section_id); }, this)); }, filter(section_id, value) { return true; }, renderWidget(section_id, option_index, cfgvalue) { var values = L.toArray((cfgvalue != null) ? cfgvalue : this.default), choices = {}, checked = {}, order = []; for (var i = 0; i < values.length; i++) checked[values[i]] = true; values = []; if (!this.multiple && (this.rmempty || this.optional)) choices[''] = E('em', _('unspecified')); for (let device of this.devices) { const name = device.getName(); const type = device.getType(); if (name == 'lo' || name == this.exclude || !this.filter(section_id, name)) continue; if (this.noaliases && type == 'alias') continue; if (this.nobridges && type == 'bridge') continue; if (this.noinactive && device.isUp() == false) continue; const item = E([ E('img', { 'title': device.getI18n(), 'src': L.resource('icons/%s%s.svg'.format(type, device.isUp() ? '' : '_disabled')) }), E('span', { 'class': 'hide-open' }, [ name ]), E('span', { 'class': 'hide-close'}, [ device.getI18n() ]) ]); const networks = device.getNetworks(); if (networks.length > 0) L.dom.append(item.lastChild, [ ' (', networks.map(function(n) { return n.getName() }).join(', '), ')' ]); if (checked[name]) values.push(name); choices[name] = item; order.push(name); } if (this.networks != null) { for (let net of this.networks) { const device = network.instantiateDevice('@%s'.format(net.getName()), net); const name = device.getName(); if (name == '@loopback' || name == this.exclude || !this.filter(section_id, name)) continue; if (this.noinactive && net.isUp() == false) continue; const item = E([ E('img', { 'title': device.getI18n(), 'src': L.resource('icons/alias%s.svg'.format(device.isUp() ? '' : '_disabled')) }), E('span', { 'class': 'hide-open' }, [ name ]), E('span', { 'class': 'hide-close'}, [ device.getI18n() ]) ]); if (checked[name]) values.push(name); choices[name] = item; order.push(name); } } if (this.includeips) { this.devices.forEach(net_dev => { ['getIPAddrs', 'getIP6Addrs'].forEach(fn => { net_dev[fn]().forEach(addr => { const name = addr.split('/')[0]; if (checked[name]) values.push(name); choices[name] = E([], [name, ' (', E('strong', net_dev.getName()), ')']); order.push(name); }); }); }); } if (!this.nocreate) { const keys = Object.keys(checked).sort(L.naturalCompare); for (let k of keys) { if (choices.hasOwnProperty(k)) continue; choices[k] = E([ E('img', { 'title': _('Absent Interface'), 'src': L.resource('icons/ethernet_disabled.svg') }), E('span', { 'class': 'hide-open' }, [ k ]), E('span', { 'class': 'hide-close'}, [ '%s: "%h"'.format(_('Absent Interface'), k) ]) ]); values.push(k); order.push(k); } } const widget = new ui.Dropdown(this.multiple ? values : values[0], choices, { id: this.cbid(section_id), sort: order, multiple: this.multiple, optional: this.optional || this.rmempty, disabled: (this.readonly != null) ? this.readonly : this.map.readonly, select_placeholder: E('em', _('unspecified')), display_items: this.display_size || this.size || 3, dropdown_items: this.dropdown_size || this.size || 5, validate: L.bind(this.validate, this, section_id), create: !this.nocreate, create_markup: '' + '
  • ' + '' + '{{value}}' + ''+_('Custom Interface')+': "{{value}}"' + '
  • ' }); return widget.render(); }, }); var CBIUserSelect = form.ListValue.extend({ __name__: 'CBI.UserSelect', load(section_id) { return getUsers().then(L.bind(function(users) { delete this.keylist; delete this.vallist; for (var i = 0; i < users.length; i++) { this.value(users[i]); } return this.super('load', section_id); }, this)); }, filter(section_id, value) { return true; }, }); var CBIGroupSelect = form.ListValue.extend({ __name__: 'CBI.GroupSelect', load(section_id) { return getGroups().then(L.bind(function(groups) { for (var i = 0; i < groups.length; i++) { this.value(groups[i]); } return this.super('load', section_id); }, this)); }, filter(section_id, value) { return true; }, }); return L.Class.extend({ ZoneSelect: CBIZoneSelect, ZoneForwards: CBIZoneForwards, IPSelect: CBIIPSelect, NetworkSelect: CBINetworkSelect, DeviceSelect: CBIDeviceSelect, UserSelect: CBIUserSelect, GroupSelect: CBIGroupSelect, });