"use strict"; function makeFieldset() { return document.createElement('fieldset'); } function makeLegend(...legend) { let ret = document.createElement('legend'); ret.append(...legend); return ret; } function makeButton(label, onClick) { let ret = document.createElement('button'); ret.setAttribute('type', 'button'); ret.append(label); ret.addEventListener('click', onClick); return ret; } function makeText(text) { return document.createTextNode(text); } function makeInput(id, attrs) { let ret = document.createElement('input'); ret.setAttribute('id', id); for (const [attr, value] of Object.entries(attrs)) { ret.setAttribute(attr, value); } return ret; } function makeLabel(label, forId) { let ret = document.createElement('label'); ret.setAttribute('for', forId); ret.innerHTML = label; return ret; } class L3Section { constructor(legend, presetname, ...inputs) { this.legend = legend; this.presetname = presetname; this.inputs = inputs; } node() { let fieldset = makeFieldset(); let legend = this.legend; if (this.presetname instanceof HTMLElement) { legend = this.presetname; } fieldset.append(makeLegend(legend)); let sep = undefined; for (const input of this.inputs) { fieldset.append(input.node()); } return fieldset; } render() { const options = this.inputs .flatMap(input => input.option()) .filter(option => !!option); if (options.length == 0) { return undefined; } var compiledopts = new Map(); for (const option of options) { let c = compiledopts.get(option.optionName) ?? []; c.push(option.value); compiledopts.set(option.optionName, c); } let optstrs = []; for (const [opt, values] of compiledopts) { if (values.length == 1) { optstrs.push(`option ${opt} '${values[0]}'`); } else { for (const value of values) { optstrs.push(`list ${opt} '${value}'`); } } } let sectionname = ""; if (typeof this.presetname === 'string') { sectionname = ` '${this.presetname}'`; } else if (this.presetname instanceof HTMLElement && this.presetname.tagName === 'INPUT' && this.presetname.value) { sectionname = ` '${this.presetname.value}'`; } return `config ${this.legend}${sectionname}\n\t` + optstrs.join('\n\t'); } } class L3MultiSection { constructor(legend, template) { this.legend = legend; this.template = template; this.sections = []; this.nsections = 0; } node() { let fieldset = makeFieldset(); fieldset.append( makeLegend( makeButton(`+ ${this.legend}`, () => fieldset.appendChild(this.#makeSection())), ) ); return fieldset; } #makeSection() { const idsuffix = `-${this.nsections++}`; let newsection = this.template(idsuffix); this.sections.push(newsection); let nodes = newsection.node(); let del = makeButton('Remove', () => this.#removeSection(newsection, nodes)); del.classList.add('del'); nodes.append(del); return nodes; } #removeSection(del, nodes) { this.sections = this.sections.filter(section => section != del); nodes.remove(); } render() { return this.sections.map(section => section.render()).filter(section => !!section) } } class L3Input { constructor(label, id, optionName, attrs, datalist) { this.label = label; this.id = id; this.optionName = optionName; this.attrs = attrs; this.datalist = datalist this.input = undefined; } node() { let div = document.createElement('div') if (this.label) { div.appendChild(makeLabel(this.label, this.id)); } this.input = makeInput(this.id, this.attrs); div.appendChild(this.input); if (this.datalist) { let datalist = document.createElement('datalist'); let datalistid = this.id + '-datalist'; datalist.setAttribute('id', datalistid); this.input.setAttribute('list', datalistid); for (const value of this.datalist) { let option = document.createElement('option'); option.setAttribute('value', value); datalist.append(option); } div.appendChild(datalist); } return div; } option() { switch (this.input.type) { case 'radio': case 'checkbox': if (!this.input.checked || this.input.value == 'undefined') return undefined; break; default: if (!this.input.value) return undefined; break; } const ret = { optionName: this.optionName, value: this.input.value, }; return ret; } } class L3Select { constructor(label, id, optionName, attrs, ...optionList) { this.label = label; this.id = id; this.optionName = optionName; this.attrs = attrs; this.optionList = optionList; this.input = undefined; } node() { let div = document.createElement('div') if (this.label) { div.appendChild(makeLabel(this.label, this.id)); } this.input = document.createElement('select'); this.input.setAttribute('id', this.id); for (const [attr, value] of Object.entries(this.attrs)) { this.input.setAttribute(attr, value); } div.appendChild(this.input); for (const [optgrouplabel, options] of this.optionList) { let target = this.input; if (optgrouplabel != '') { target = document.createElement('optgroup'); target.setAttribute('label', optgrouplabel); this.input.appendChild(target); } for (const [label, value] of options) { let option = document.createElement('option'); option.setAttribute('value', value); option.appendChild(makeText(label)); target.appendChild(option); } } return div; } option() { if (this.input.value == 'undefined') { return undefined; } const ret = { optionName: this.optionName, value: this.input.value, }; return ret; } } class L3MultiInput { constructor(label, template) { this.label = label; this.template = template; this.inputs = []; this.ninputs = 0; } node() { let fieldset = makeFieldset(); fieldset.append( makeLegend( makeButton(`+ ${this.label}`, () => fieldset.append(this.#makeInput())), ) ); return fieldset; } #makeInput() { const idsuffix = `-${this.ninputs++}`; let newinput = this.template(idsuffix); this.inputs.push(newinput); let div = newinput.node(); let delbutton = document.createElement('button'); delbutton.setAttribute('type', 'button'); delbutton.classList.add('del'); delbutton.innerHTML = '-'; delbutton.addEventListener('click', () => this.#removeInput(newinput, div)); div.prepend(delbutton); return div; } #removeInput(del, div) { this.inputs = this.inputs.filter(input => input != del); div.remove(); } option() { return this.inputs.map(input => input.option()); } } class L3Config { constructor() { this.sections = []; } addSection(section) { this.sections.push(section) } handleUpdate() { renderConfig(this.sections.flatMap(section => section.render()).filter(section => !!section)); } node() { return this.sections.map(section => section.node()) } } function initForm() { let l3configinput = document.getElementById('l3configinput'); let form = document.createElement('form'); l3configinput.appendChild(form); const label5ghz = (chan) => [`${chan} (${5000+chan*5}MHz)`, chan.toString()] const label2ghz = (chan) => [`${chan} (${2407+chan*5}MHz)`, chan.toString()] const UNII1Channels = [36, 40, 44, 48].map(label5ghz); const UNII2Channels = [52, 56, 60, 64].map(label5ghz); const UNII2EChannels = [100, 104, 108, 112, 116, 120, 124, 128, 132, 136, 140].map(label5ghz); const Channels5GHz = [...UNII1Channels, ...UNII2Channels, ...UNII2EChannels]; const Channels2GHz = Array(13).fill().map((_, chan) => [`${chan+1} (${2412 + chan*5}MHz)`, (chan+1).toString()]); const Channels2GHzOFDM = [1,5,9,13].map(label2ghz); const Channels2GHzDSSS = [1,6,11].map(label2ghz); const l3cfg = new L3Config(); l3cfg.addSection(new L3Section('gateway', 'meta', new L3Input('Router Name','gatewayName', 'name', {type: 'search', placeholder: 'Router Name'}), new L3MultiInput('Router IPv4 Address', function(idsuffix) { return new L3Input(undefined,'gatewayRouterIP4'+idsuffix, 'router_ip', {type: 'search', maxlength: 15, placeholder: 'Router IPv4 Address'}); }), new L3MultiInput('Router IPv6 Address', function(idsuffix) { return new L3Input(undefined,'gatewayRouterIP6'+idsuffix, 'router_ip6', {type: 'text', maxlength: 39, placeholder: 'Router IPv6 Address'}); }), new L3Input(undefined, 'gatewayConfigVersion', 'config_version', {type: 'hidden', value: 2, disabled: ''}), )); l3cfg.addSection(new L3Section('dns', undefined, new L3Select('Preset','dnsAnycastSelect', 'server', {name: 'anycast'}, ['', [ ['Anycast DNS', 'fd43:5602:29bd:ffff:1:1:1:1'], ['Anycast DNS64', 'fd43:5602:29bd:ffff:1:1:1:64'], ['---', undefined], ]], ), new L3MultiInput('Custom DNS Server', function(idsuffix) { return new L3Input(undefined,'dnsOther'+idsuffix, 'server', {type: 'text', maxlength: 39, placeholder: 'Custom DNS Server'}); }), )); l3cfg.addSection(new L3Section('wan', undefined, new L3Input('Use VLAN','babelpeerVLAN', 'vlan', {type: 'number', min: 1, max: 4094}), new L3Input('Use Interface directly','babelpeerIFACE', 'iface', {type: 'text', placeholder: 'eth0, eth0.4, ...'}), )); l3cfg.addSection(new L3Section('client', undefined, new L3Input('Use VLAN','clientVLAN', 'vlan', {type: 'number', min: 1, max: 4094}), new L3Input('Use Interface directly','clientIFACE', 'iface', {type: 'text', placeholder: 'eth0, eth0.4, ...'}), new L3MultiInput('Client IPv6 Subnet', function(idsuffix) { return new L3Input(undefined,'clientIP6Addr'+idsuffix, 'ip6addr', {type: 'text', maxlength: 39, placeholder: 'IPv6 CIDR, 2a0b:f4c0:XX:YYYY::/64, fd43:5602:29bd:XXXX::/64,...'}); }), new L3MultiInput('Client IPv4 Subnet', function(idsuffix) { return new L3Input(undefined,'clientIPAddr'+idsuffix, 'ipaddr', {type: 'text', maxlength: 15, placeholder: 'IPv4 CIDR, 10.XX.YY.ZZ/24'}); }), new L3Input('IPv4 SNAT','clientSNAT', 'snat', {type: 'checkbox', value: 1}), new L3Input('DHCP Start Address','clientDHCPStart', 'dhcp_start', {type: 'text', maxlength: 15, placeholder: 'IPv4 Address, 10.XX.YY.10'}), new L3Input('DHCP Number of Addresses','clientDHCPLimit', 'dhcp_limit', {type: 'number', min: 1, max: 65535}), new L3Input('Wifi ESSID','clientESSID', 'essid', {type: 'text'}), new L3Select('Wifi 2.4 GHz Channel','client2GHZ', 'chan2ghz', {name: 'chan2ghz'}, ['', [['---', undefined]]], ['Non-Overlapping Channels (OFDM)', Channels2GHzOFDM], ['Non-Overlapping Channels (DSSS)', Channels2GHzDSSS], ['All Channels', Channels2GHz], ), new L3Select('Wifi 5 GHz Channel','client5GHZ', 'chan5ghz', {name: 'chan5ghz'}, ['', [['---', undefined]]], ['U-NII-1 Channels (Indoor)', UNII1Channels], ['U-NII-2 Channels (Indoor + DFS)', UNII2Channels], ['U-NII-2E Channels (Indoor + Outdoor + DFS)', UNII2EChannels], ), )); l3cfg.addSection(new L3MultiSection('VLAN', function(idsuffix) { return new L3Section('vlan', makeInput('vlanSectionName'+idsuffix, {type: 'number', min: 1, max: 4094, required: '', placeholder: '1..4094'}), new L3Input('Comment','vlanComment'+idsuffix, 'comment', {type: 'text'}, ['client', 'wan']), new L3Input('Ports','vlanPorts'+idsuffix, 'ports', {type: 'text', placeholder: 'eth0:*, eth1.4:u, ...'}), ); })); l3cfg.addSection(new L3MultiSection('Direct Babel Peering', function(idsuffix) { return new L3Section('babelpeer', makeInput('babelpeerSectionName'+idsuffix, {type: 'text'}), new L3Input('Use VLAN','babelpeerVLAN'+idsuffix, 'vlan', {type: 'number', min: 1, max: 4094}), new L3Input('Use Interface directly','babelpeerIFACE'+idsuffix, 'iface', {type: 'text', placeholder: 'eth0, eth0.4, ...'}), new L3Select('Babel Interface Type','babelpeerType'+idsuffix, 'type', {name: 'type'}, ['', [ ['Default', undefined], ['Wired', 'wired'], ['Wireless', 'wireless'], ]], ), new L3Input('rxcost','babelpeerRXCost'+idsuffix, 'rxcost', {type: 'number', min: 96, max: 65535, placeholder: 'LAN: 96, Tunnel: 4096, ...'}), ); })); l3cfg.addSection(new L3MultiSection('WireGuard Peering', function(idsuffix) { return new L3Section('wireguardpeer', makeInput('wireguardpeerSectionName'+idsuffix, {type: 'text'}), new L3Input('Endpoint Host', 'wireguardpeerEPHost'+idsuffix, 'endpoint_host', {type: 'text', required: ''}, ['peering.nue3.fff.community','ff1.zbau.f3netze.de','nsvm.f3netze.de','2a0b:f4c0:400::']), new L3Input('Endpoint Port','wireguardpeerEPPort'+idsuffix, 'endpoint_port', {type: 'number', required: '', min:1, max: 65535}), new L3Input('Persistent Keepalive','wireguardpeerPersistentKeepalive', 'persistent_keepalive', {type: 'number', min: 0, max: 65535, placeholder: '1 - 65535 Seconds...'}), new L3Input('Remote Public Key', 'wireguardpeerRemotePubKey'+idsuffix, 'remote_public_key', {type: 'text', minlength: 44, maxlength: 44, required: '', placeholder: 'base64 encoded public key'}), new L3Input('Local Private Key', 'wireguardpeerLocalPrivKey'+idsuffix, 'local_private_key', {type: 'text', minlength: 44, maxlength: 44, placeholder: 'base64 encoded private key'}), new L3Input('rxcost','wireguardpeerRXCost'+idsuffix, 'rxcost', {type: 'number', min: 96, max: 65535, value: 4096, placeholder: '4096'}), new L3Input('mtu','wireguardpeerMTU'+idsuffix, 'mtu', {type: 'number', min: 1280, max: 65535, value: 1412, placeholder: '1412'}), ); })); form.addEventListener('input', () => l3cfg.handleUpdate()); form.replaceChildren(...l3cfg.node()) renderConfig(l3cfg.sections.flatMap(section => section.render()).filter(section => !!section)); } function renderConfig(sections) { let l3configoutput = document.getElementById('l3configoutput'); let l3configoutputpre = document.createElement('pre'); let code = document.createElement('code'); l3configoutputpre.appendChild(code); code.innerHTML += sections.join('\n\n'); l3configoutput.replaceChildren(l3configoutputpre); } initForm();