123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603 |
- 'use strict';
- 'require fs';
- 'require dom';
- 'require view';
- 'require form';
- 'require ui';
- 'require tools.widgets as widgets';
- var domparser = new DOMParser();
- var QRCODE_VARIABLES = ['KEY_BASE64', 'KEY', 'HMAC_KEY_BASE64', 'HMAC_KEY'];
- var INVALID_KEYS = ['__CHANGEME__', 'CHANGEME'];
- function setOptionValue(map, section_id, option, value) {
- var option = L.toArray(map.lookupOption(option, section_id))[0];
- var uiEl = option ? option.getUIElement(section_id) : null;
- if (uiEl)
- uiEl.setValue(value);
- }
- function lines(content) {
- return content.split(/\r?\n/);
- }
- function parseLine(rawLine) {
- if (rawLine[0] != '#' && rawLine[0] != ';') {
- var line = rawLine.split(/ ([^;]*)/, 2);
- if (line.length == 2) {
- var key = line[0].trim();
- var value = line[1].trim();
- if (key && value)
- return [key, value];
- }
- }
- return null;
- }
- function parseKeys(content) {
- var l = lines(content);
- var keys = {};
- for (var i = 0; i < l.length; i++) {
- var p = l[i].split(/:(.*)/, 2);
- if (p.length == 2)
- keys[p[0].trim()] = p[1].trim();
- }
- return keys;
- }
- var KeyTypeValue = form.ListValue.extend({
- __init__: function() {
- this.super('__init__', arguments);
- this.hidden = false;
- },
- cfgvalue: function(section_id) {
- for (var i = 0; i < this.keylist.length; i++) {
- var value = this.map.data.get(
- this.uciconfig || this.section.uciconfig || this.map.config,
- this.ucisection || section_id,
- this.keylist[i]
- );
- if (value)
- return this.keylist[i];
- }
- return this.keylist[0];
- },
- render: function(section_id, option_index, cfgvalue) {
- return this.super('render', arguments)
- .then(L.bind(function(el) {
- // Use direct style to hide, because class .hidden
- // is used by this.isActive(). We want full functionality,
- // but hidden field
- if (this.hidden)
- el.style.display = 'none';
- return el;
- }, this));
- },
- remove: function() {
- // Ignore
- },
- write: function() {
- // Ignore
- },
- });
- var YNValue = form.Flag.extend({
- __init__: function() {
- this.super('__init__', arguments);
- this.enabled = 'Y';
- this.disabled = 'N';
- this.default = 'N';
- },
-
- cfgvalue: function(section_id) {
- var value = this.super('cfgvalue', arguments);
- return value ? String(value).toUpperCase() : value;
- },
- parse: function(section_id) {
- var active = this.isActive(section_id),
- cval = this.cfgvalue(section_id),
- fval = active ? this.formvalue(section_id) : null;
- if (String(fval).toUpperCase() != cval) {
- if (fval == 'Y')
- return Promise.resolve(this.write(section_id, fval));
- else if (cval !== undefined)
- return Promise.resolve(this.remove(section_id));
- }
- },
- });
- var QrCodeValue = form.DummyValue.extend({
- __init__: function() {
- this.super('__init__', arguments);
- this.needsRefresh = {};
- this.components = [];
- QRCODE_VARIABLES.forEach(L.bind(function(option) {
- this.components.push(option);
- var dep = {};
- dep[option] = /.+/;
- this.depends(dep);
- }, this));
- },
- cfgQrCode: function(section_id) {
- var qr = [];
- for (var i = 0; i < this.components.length; i++) {
- var value = this.map.data.get(
- this.uciconfig || this.section.uciconfig || this.map.config,
- this.ucisection || section_id,
- this.components[i]
- );
- if (value)
- qr.push(this.components[i] + ':' + value);
- }
- return qr ? qr.join(' ') : null;
- },
- formQrCode: function(section_id) {
- var qr = [];
- for (var i = 0; i < this.components.length; i++) {
- var value = null;
- var uiEl = L.toArray(this.map.lookupOption(this.components[i], section_id))[0];
- if (uiEl) {
- if (uiEl.isActive(section_id))
- value = uiEl.formvalue(section_id);
- }
- if (value)
- qr.push(this.components[i] + ':' + value);
- }
- return qr ? qr.join(' ') : null;
- },
- onchange: function(ev, section_id) {
- if (this.needsRefresh[section_id] !== undefined)
- this.needsRefresh[section_id] = true;
- else {
- this.refresh(section_id);
- }
- },
- refresh: function(section_id) {
- var qrcode = this.formQrCode(section_id);
- var formvalue = this.formvalue(section_id);
- if (formvalue != qrcode) {
- this.getUIElement(section_id).setValue(qrcode);
- var uiEl = document.getElementById(this.cbid(section_id));
- if (uiEl) {
- var contentEl = uiEl.nextSibling;
- if (contentEl.childNodes.length == 1) {
- dom.append(contentEl, E('em', { 'class': 'spinning', }, [ _('Loading…') ]));
- }
- this.needsRefresh[section_id] = false;
- // Render QR code
- return this.renderSvg(qrcode)
- .then(L.bind(function(svgEl) {
- dom.content(contentEl, svgEl || E('div'));
- var needsAnotherRefresh = this.needsRefresh[section_id];
- delete this.needsRefresh[section_id];
- if (needsAnotherRefresh) {
- this.refresh(section_id);
- }
- }, this)).finally(L.bind(function() {
- if (this.needsRefresh[section_id] === undefined) {
- if (contentEl.childNodes.length == 2)
- contentEl.removeChild(contentEl.lastChild);
- delete this.needsRefresh[section_id];
- }
- }, this)).catch(L.error);
- }
- }
- // Nothing to render
- return Promise.resolve(null);
- },
- renderWidget: function(section_id) {
- var qrcode = this.cfgQrCode(section_id);
- return this.renderSvg(qrcode)
- .then(L.bind(function(svgEl) {
- var uiEl = new ui.Hiddenfield(qrcode, { id: this.cbid(section_id) });
- return E([
- uiEl.render(),
- E('div', {}, svgEl || E('div'))
- ]);
- }, this));
- },
- qrEncodeSvg: function(qrcode) {
- return fs.exec('/usr/bin/qrencode', ['--type', 'svg', '--inline', '-o', '-', qrcode])
- .then(function(response) {
- return response.stdout;
- });
- },
- renderSvg: function(qrcode) {
- if (qrcode)
- return this.qrEncodeSvg(qrcode)
- .then(function(rawsvg) {
- return domparser.parseFromString(rawsvg, 'image/svg+xml')
- .querySelector('svg');
- });
- else
- return Promise.resolve(null);
- },
- });
- var GenerateButton = form.Button.extend({
- __init__: function() {
- this.super('__init__', arguments);
- this.onclick = L.bind(this.generateKeys, this);
- this.keytypes = {};
- },
- keytype: function(key, regex) {
- this.keytypes[key] = regex;
- },
- qrcode: function(option) {
- this.qrcode = option;
- },
- generateKeys: function(ev, section_id) {
- return fs.exec('/usr/sbin/fwknopd', ['--key-gen'])
- .then(function(response) { return parseKeys(response.stdout); })
- .then(L.bind(this.applyKeys, this, section_id))
- .catch(L.error);
- },
- applyKeys: function(section_id, keys) {
- for (var key in keys) {
- setOptionValue(this.map, section_id, key, keys[key]);
- for (var type in this.keytypes) {
- if (this.keytypes[type].test(key))
- setOptionValue(this.map, section_id, type, key);
- }
- }
- // Force update of dependencies (element visibility)
- this.map.checkDepends();
- // Refresh QR code
- var option = L.toArray(this.map.lookupOption(this.qrcode, section_id))[0];
- if (option)
- return option.refresh(section_id);
- else
- return Promise.resolve(null);
- },
- });
- var ParseButton = form.Button.extend({
- __init__: function() {
- this.super('__init__', arguments);
- this.onclick = L.bind(this.parseAccessConf, this);
- },
- parseAccessConf: function() {
- this.stanzas = [];
- var ctx = {
- processLine: L.bind(this.processAccessLine, this),
- remainingLines: [],
- stanzas: {
- last: {},
- all: []
- }
- };
- return fs.read('/etc/fwknop/access.conf')
- .then(L.bind(this.parseFile, this, ctx))
- .then(L.bind(function() {
- if (ctx.stanzas.all.length > 0)
- return this.renderStanzas(ctx.stanzas.all)
- .then(function(topEl) {
- var dlg = ui.showModal(_('Firewall Knock Operator Daemon'), [
- topEl,
- E('button', {
- 'class': 'cbi-button cbi-button-neutral',
- 'click': ui.hideModal
- }, _('Close'))
- ], 'cbi-modal');
- dlg.querySelector('button').focus();
- dlg.parentNode.scrollTop = 0;
- });
- else {
- var dlg = ui.showModal(_('Firewall Knock Operator Daemon'), [
- E('p', _("No stanza found.")),
- E('button', {
- 'class': 'cbi-button cbi-button-neutral',
- 'click': ui.hideModal
- }, _('Close'))
- ]);
- dlg.querySelector('button').focus();
- }
- }, this))
- .catch(function(err) {
- L.error(err);
- });
- },
- parseFile: function(ctx, content) {
- ctx.remainingLines.unshift.apply(ctx.remainingLines, lines(content));
- return this.parseLines(ctx);
- },
- parseFolder: function(ctx, folder, entries) {
- // Parse and process files in order
- var parseJobs = [];
- var parsedLines = [];
- entries.sort(function(el1, el2) {
- return (el1.name > el2.name) ? 1
- : (el1.name < el2.name) ? -1
- : 0;
- });
- entries.forEach(L.bind(function(entry) {
- var ctxLines = [];
- parsedLines.unshift(ctxLines);
- parseJobs.push(fs.read(folder + '/' + entry.name)
- .then(function(content) {
- ctxLines.push.apply(ctxLines, lines(content));
- }));
- }, this));
- return Promise.all(parseJobs)
- .then(L.bind(function(ctx) {
- parsedLines.forEach(function(lines) {
- ctx.remainingLines.unshift.apply(ctx.remainingLines, lines);
- });
- }, this, ctx))
- .then(L.bind(this.parseLines, this, ctx));
- },
- parseLines: function(ctx) {
- while (ctx.remainingLines.length > 0) {
- var line = parseLine(ctx.remainingLines.shift());
- if (line) {
- var result = ctx.processLine.call(this, ctx, line[0], line[1]);
- if (result)
- return result;
- }
- }
- },
- processAccessLine: function(ctx, key, value) {
- if (key.endsWith(':')) {
- key = key.slice(0, -1);
- }
- if (key == "%include") {
- return fs.read(value)
- .then(L.bind(this.parseFile, this, ctx));
- } else if (key == "%include_folder") {
- return fs.list(value)
- .then(L.bind(this.parseFolder, this, ctx, value));
- } else if (key == "%include_keys") {
- var keysCtx = {
- processLine: L.bind(this.processKeysLine, this),
- remainingLines: [],
- stanzas: ctx.stanzas
- };
- return fs.read(value)
- .then(L.bind(this.parseFile, this, keysCtx))
- .then(L.bind(this.parseLines, this, ctx));
- } else {
- if (key == 'SOURCE') {
- ctx.stanzas.last = {};
- ctx.stanzas.all.push(ctx.stanzas.last);
- }
- ctx.stanzas.last[key] = value;
- }
- },
- processKeysLine: function(ctx, key, value) {
- // Simplification - accept only KEY arguments
- if (ctx.stanzas.last && key.match(/KEY/))
- ctx.stanzas.last[key] = value;
- },
- renderStanzas: function(stanzas) {
- var config = {};
- config.access = stanzas;
- var m, s, o;
- m = new form.JSONMap(config, null, _('Custom configuration read from /etc/fwknop/access.conf.'));
- m.readonly = true;
- // set the access.conf settings
- s = m.section(form.TypedSection, 'access', _('access.conf stanzas'));
- s.anonymous = true;
- o = s.option(QrCodeValue, 'qr', _('QR code'), ('QR code to configure fwknopd Android application.'));
- o = s.option(form.Value, 'SOURCE', 'SOURCE');
- o = s.option(form.Value, 'DESTINATION', 'DESTINATION');
- o = s.option(form.Value, 'KEY', 'KEY');
- o.depends('keytype', 'KEY');
- o.validate = function(section_id, value) {
- return (String(value).length > 0 && !INVALID_KEYS.includes(value)) ? true : _('The symmetric key has to be specified.');
- }
- o = s.option(form.Value, 'KEY_BASE64', 'KEY_BASE64');
- o.depends('keytype', 'KEY_BASE64');
- o.validate = function(section_id, value) {
- return (String(value).length > 0 && !INVALID_KEYS.includes(value)) ? true : _('The symmetric key has to be specified.');
- }
- o = s.option(KeyTypeValue, 'keytype');
- o.value('KEY', _('Normal key'));
- o.value('KEY_BASE64', _('Base64 key'));
- o.hidden = true;
- o = s.option(form.Value, 'HMAC_KEY', 'HMAC_KEY');
- o.depends('hkeytype', 'HMAC_KEY');
- o.validate = function(section_id, value) {
- return (String(value).length > 0 && !INVALID_KEYS.includes(value)) ? true : _('The HMAC authentication key has to be specified.');
- }
- o = s.option(form.Value, 'HMAC_KEY_BASE64', 'HMAC_KEY_BASE64');
- o.depends('hkeytype', 'HMAC_KEY_BASE64');
- o.validate = function(section_id, value) {
- return (String(value).length > 0 && !INVALID_KEYS.includes(value)) ? true : _('The HMAC authentication key has to be specified.');
- }
- o = s.option(KeyTypeValue, 'hkeytype');
- o.value('HMAC_KEY', _('Normal key'));
- o.value('HMAC_KEY_BASE64', _('Base64 key'));
- o.hidden = true;
- return m.load()
- .then(L.bind(m.render, m));
- }
- });
- return view.extend({
- load: function() {
- return Promise.all([
- L.resolveDefault(fs.stat('/etc/fwknop/access.conf'))
- ]);
- },
- render: function(results) {
- var has_access_conf = results[0];
- var m, s, o;
- m = new form.Map('fwknopd', _('Firewall Knock Operator Daemon'));
- s = m.section(form.TypedSection, 'global', _('Enable Uci/Luci control'));
- s.anonymous = true;
- s.option(form.Flag, 'uci_enabled', _('Enable config overwrite'), _('When unchecked, the config files in /etc/fwknopd will be used as is, ignoring any settings here.'));
- if ( has_access_conf ) {
- o = s.option(ParseButton, 'parse', _('Custom configuration'), _('Parses the /etc/fwknop/access.conf file (and \
- included files/folders/keys) and generates QR codes for all found \
- stanzas. Handles only files in /etc/fwknop folder due to access rights \
- restrictions.'));
- o.inputtitle = _("Show access.conf QR codes");
- }
- s = m.section(form.TypedSection, 'network', _('Network configuration'));
- s.anonymous = true;
- o = s.option(widgets.NetworkSelect, 'network', _('Network'), _('The network on which the daemon listens. The daemon \
- is automatically started when the network is up-and-running. This option \
- has precedence over “PCAP_INTF” option.'));
- o.unpecified = true;
- o.nocreate = true;
- o.rmempty = true;
- // set the access.conf settings
- s = m.section(form.TypedSection, 'access', _('access.conf stanzas'));
- s.anonymous = true;
- s.addremove = true;
- var qrCode = s.option(QrCodeValue, 'qr', _('QR code'), ('QR code to configure fwknopd Android application.'));
- o = s.option(form.Value, 'SOURCE', 'SOURCE', _('The source address from which the SPA packet will be accepted. The string “ANY” is \
- also accepted if a valid SPA packet should be honored from any source IP. \
- Networks should be specified in CIDR notation (e.g. “192.168.10.0/24”), \
- and individual IP addresses can be specified as well. Multiple entries \
- are comma-separated.'));
- o.validate = function(section_id, value) {
- return String(value).length > 0 ? true : _('The source address has to be specified.');
- }
- s.option(form.Value, 'DESTINATION', 'DESTINATION', _('The destination address for which the SPA packet will be accepted. The \
- string “ANY” is also accepted if a valid SPA packet should be honored to any \
- destination IP. Networks should be specified in CIDR notation \
- (e.g. “192.168.10.0/24”), and individual IP addresses can be specified as well. \
- Multiple entries are comma-separated.'));
- o = s.option(GenerateButton, 'keys', _('Generate keys'), _('Generates the symmetric key used for decrypting an incoming \
- SPA packet, that is encrypted by the fwknop client with Rijndael block cipher, \
- and HMAC authentication key used to verify the authenticity of the incoming SPA \
- packet before the packet is decrypted.'));
- o.inputtitle = _("Generate Keys");
- o.keytype('keytype', /^KEY/);
- o.keytype('hkeytype', /^HMAC_KEY/);
- o.qrcode('qr');
- o = s.option(form.Value, 'KEY', 'KEY', _('Define the symmetric key used for decrypting an incoming SPA \
- packet that is encrypted by the fwknop client with Rijndael.'));
- o.depends('keytype', 'KEY');
- o.onchange = L.bind(qrCode.onchange, qrCode);
- o.validate = function(section_id, value) {
- return (String(value).length > 0 && !INVALID_KEYS.includes(value)) ? true : _('The symmetric key has to be specified.');
- }
- o = s.option(form.Value, 'KEY_BASE64', 'KEY_BASE64', _('Define the symmetric key (in Base64 encoding) used for \
- decrypting an incoming SPA packet that is encrypted by the fwknop client \
- with Rijndael.'));
- o.depends('keytype', 'KEY_BASE64');
- o.onchange = L.bind(qrCode.onchange, qrCode);
- o.validate = function(section_id, value) {
- return (String(value).length > 0 && !INVALID_KEYS.includes(value)) ? true : _('The symmetric key has to be specified.');
- }
- o = s.option(KeyTypeValue, 'keytype', _('Key type'));
- o.value('KEY', _('Normal key'));
- o.value('KEY_BASE64', _('Base64 key'));
- o.onchange = L.bind(qrCode.onchange, qrCode);
- o = s.option(form.Value, 'HMAC_KEY', 'HMAC_KEY', _('Define the HMAC authentication key used for verifying \
- the authenticity of the SPA packet before the packet is decrypted.'));
- o.depends('hkeytype', 'HMAC_KEY');
- o.onchange = L.bind(qrCode.onchange, qrCode);
- o.validate = function(section_id, value) {
- return (String(value).length > 0 && !INVALID_KEYS.includes(value)) ? true : _('The HMAC authentication key has to be specified.');
- }
- o = s.option(form.Value, 'HMAC_KEY_BASE64', 'HMAC_KEY_BASE64', _('Define the HMAC authentication key \
- (in Base64 encoding) used for verifying the authenticity of the SPA \
- packet before the packet is decrypted.'));
- o.depends('hkeytype', 'HMAC_KEY_BASE64');
- o.onchange = L.bind(qrCode.onchange, qrCode);
- o.validate = function(section_id, value) {
- return (String(value).length > 0 && !INVALID_KEYS.includes(value)) ? true : _('The HMAC authentication key has to be specified.');
- }
- o = s.option(KeyTypeValue, 'hkeytype', _('HMAC key type'));
- o.value('HMAC_KEY', _('Normal key'));
- o.value('HMAC_KEY_BASE64', _('Base64 key'));
- o.onchange = L.bind(qrCode.onchange, qrCode);
- o = s.option(form.Value, 'OPEN_PORTS', 'OPEN_PORTS', _('Define a set of ports and protocols (tcp or udp) that will be opened if a valid knock sequence is seen. \
- If this entry is not set, fwknopd will attempt to honor any proto/port request specified in the SPA data \
- (unless of it matches any “RESTRICT_PORTS” entries). Multiple entries are comma-separated.'));
- o.placeholder = "protocol/port,...";
- o = s.option(form.Value, 'RESTRICT_PORTS', 'RESTRICT_PORTS', _('Define a set of ports and protocols (tcp or udp) that are explicitly not allowed \
- regardless of the validity of the incoming SPA packet. Multiple entries are comma-separated.'));
- o.placeholder = "protocol/port,...";
- o = s.option(form.Value, 'FW_ACCESS_TIMEOUT', 'FW_ACCESS_TIMEOUT', _('Define the length of time access will be granted by fwknopd through the firewall after a \
- valid knock sequence from a source IP address. If “FW_ACCESS_TIMEOUT” is not set then the default \
- timeout of 30 seconds will automatically be set.'));
- o.placeholder = "30";
- s.option(YNValue, 'REQUIRE_SOURCE_ADDRESS', 'REQUIRE_SOURCE_ADDRESS', _('Force all SPA packets to contain a real IP address within the encrypted data. \
- This makes it impossible to use the -s command line argument on the fwknop client command line, so either -R \
- has to be used to automatically resolve the external address (if the client behind a NAT) or the client must \
- know the external IP and set it via the -a argument.'));
- s.option(YNValue, 'ENABLE_CMD_EXEC', 'ENABLE_CMD_EXEC', _('This instructs fwknopd to accept complete commands that are contained within an authorization packet. \
- Any such command will be executed on the fwknopd server as the user specified by the “CMD_EXEC_USER” or as the user \
- that started fwknopd if that is not set.'));
- s = m.section(form.TypedSection, 'config', _('fwknopd.conf config options'));
- s.anonymous=true;
- s.option(form.Value, 'MAX_SPA_PACKET_AGE', 'MAX_SPA_PACKET_AGE', _('Maximum age in seconds that an SPA packet will be accepted. Defaults to 120 seconds.'));
- s.option(form.Value, 'PCAP_INTF', 'PCAP_INTF', _('Specify the ethernet interface on which fwknopd will sniff packets.'));
- s.option(YNValue, 'ENABLE_IPT_FORWARDING', 'ENABLE_IPT_FORWARDING', _('Allow SPA clients to request access to services through an iptables firewall instead of just to it.'));
- s.option(YNValue, 'ENABLE_NAT_DNS', 'ENABLE_NAT_DNS', _('Allow SPA clients to request forwarding destination by DNS name.'));
- return m.render();
- }
- });
|