#!/usr/bin/lua --[[ Part of AREDN® -- Used for creating Amateur Radio Emergency Data Networks Copyright (C) 2021 Tim Wilkinson Original Perl Copyright (C) 2015 Conrad Lara See Contributors file for additional contributors This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation version 3 of the License. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . Additional Terms: Additional use restrictions exist on the AREDN® trademark and logo. See AREDNLicense.txt for more info. Attributions to the AREDN® Project must be retained in the source code. If importing this code into a new or existing project attribution to the AREDN® project must be added to the source code. You must not misrepresent the origin of the material contained within. Modified versions must be modified to attribute to the original source and be marked in reasonable ways as differentiate it from the original version --]] require("nixio") require("aredn.hardware") require("aredn.http") require("aredn.utils") require("aredn.html") require("uci") require("aredn.info") require("aredn.olsr") local html = aredn.html local cursor = uci.cursor() local config = aredn.info.get_nvram("config") local node = aredn.info.get_nvram("node") if not node or node == "" then node = "NOCALL" end local tactical = aredn.info.get_nvram("tactical") if not tactical then tactical = "" end -- post_data local parms = {} if os.getenv("REQUEST_METHOD") == "POST" then require('luci.http') local request = luci.http.Request(nixio.getenv(), function() local v = io.read(1024) if not v then io.close() end return v end ) parms = request:formvalue() end local function validate_service_name(name) if not name or name == "" or name:match("[:-\"|<>]") then return false else return true end end local function validate_service_protocol(proto) if not proto or proto == "" or proto:match("[:-\"|<>]") or not proto:match("^%w+") then return false else return true end end local function validate_service_suffix(suffix) if not suffix or suffix:match("[:-\"|<>]") or not suffix:match("^[%w/?&._=#-]*$") then return false else return true end end local mac_pattern = "%x%x:%x%x:%x%x:%x%x:%x%x:%x%x" local mac_wildcard = "*:*:*:*:*:*" -- accepts nonempty strings local function validate_nonempty_matcher(matcher) return matcher ~= "" end -- accepts MAC addresses with optional bytewise wildcard substitution local function validate_mac_matcher(matcher) return matcher:gsub("%x%x", "*") == mac_wildcard end -- accepts sequences of one or more hexadecimal digit pairs separated by colons local function validate_hexbytes_matcher(matcher) return matcher:gsub("[XY]", "Z"):gsub("%x%x:", "X"):gsub("%x%x", "Y"):match("^X*Y$") end -- accepts host names with optional trailing wildcard local function validate_host_matcher(matcher) return matcher:match("^%w[%w.-]*%*?$") end -- accepts Net-ASCII (RFC 854) with escapes for supported control characters local function validate_net_ascii(matcher) local no_escapes = matcher:gsub("\\[\\benrt]", "") return not no_escapes:match("[^ -Z^-~%[%]]") end local FORCE_SUPPORTED_HINT = "Send option even when not requested by client" local FORCE_UNSUPPORTED_HINT = "Only options with an explicit tag can be sent always." local dhcp_tag_conds = { { key = "vendorclass", name = "Vendor Class", validator = validate_nonempty_matcher, hint = "all or part of the class value sent by the client", jsPlaceholder = "subpart of class string", }, { key = "userclass", name = "User Class", validator = validate_nonempty_matcher, hint = "all or part of the class value sent by the client", jsPlaceholder = "subpart of class string", }, { key = "mac", name = "MAC Address", validator = validate_mac_matcher, hint = "six bytes, each either two hexadecimal digits or a single asterisk, separated by colons", jsPattern = [[^(?:(?:[0-9a-fA-F]{2}|\\*):){5}(?:[0-9a-fA-F]{2}|\\*)$]], jsPlaceholder = "*:*:*:*:*:*", }, { key = "circuitid", name = "Agent Circuit ID", validator = validate_hexbytes_matcher, hint = "one or more bytes, each two hexadecimal digits, separated by colons", jsPattern = [[^(?:(?:[0-9a-fA-F]{2}|\\*):)*(?:[0-9a-fA-F]{2}|\\*)$]], jsPlaceholder = "12:3a:bc", }, { key = "remoteid", name = "Agent Remote ID", validator = validate_hexbytes_matcher, hint = "one or more bytes, each two hexadecimal digits, separated by colons", jsPattern = [[^(?:(?:[0-9a-fA-F]{2}|\\*):)*(?:[0-9a-fA-F]{2}|\\*)$]], jsPlaceholder = "12:3a:bc", }, { key = "subscriberid", name = "Subscriber-ID", validator = validate_net_ascii, hint = [[ASCII printing characters and \\ escapes; use \\\\ \\b \\e \\n \\r or \\t for \\, BS, ESC, LF, CR, or HT, respectively]], jsPattern = [[^(?:[ -\\[\\]-~]|\\\\[\\\\benrt])+$]], jsPlaceholder = "ASCII string\\\\r\\\\n& escapes", }, } local dhcp_tag_validators = {} for _, cond in ipairs(dhcp_tag_conds) do dhcp_tag_validators[cond.key] = { validator = cond.validator, hint = cond.hint, jsPattern = cond.jsPattern, jsPlaceholder = cond.jsPlaceholder} end local function html_safe(s) return s:gsub("&", "&"):gsub(">", ">"):gsub("<", "<") end local serv_err = {} local function serverr(msg) serv_err[#serv_err + 1] = msg:gsub(">", ">"):gsub("<", "<") end local port_err = {} local function porterr(msg) port_err[#port_err + 1] = msg end local dmz_err = {} local function dmzerr(msg) dmz_err[#dmz_err + 1] = msg end local dhcp_err = {} local function dhcperr(msg) dhcp_err[#dhcp_err + 1] = msg end local dhcptag_err = {} local function dhcptagerr(msg) dhcptag_err[#dhcptag_err + 1] = msg end local dhcpopt_err = {} local function dhcpopterr(msg) dhcpopt_err[#dhcpopt_err + 1] = msg end local alias_err = {} local function aliaserr(msg) alias_err[#alias_err + 1] = msg end local errors = {} local function err(msg) errors[#errors + 1] = msg end local hidden = {} local function hide(m) hidden[#hidden + 1] = m end local hosts = {} local addrs = {} local macs = {} if config == "" or nixio.fs.stat("/tmp/reboot-required") then http_header() html.header(node .. " setup", true) html.print("
") html.alert_banner() html.navbar_admin("ports") html.print("") html.print("

") if config == "" then html.print("This page is not available until the configuration has been set.") else html.print("The specified configuration is invalid, try flushing your browser cache or reboot the mesh node.") end html.print("
") html.footer(); html.print("") http_footer() os.exit() end -- check for dmz mode local dmz_mode = tonumber(cursor:get("aredn", "@dmz[0]", "mode")) if not dmz_mode then dmz_mode = 2 end local lanip, lanbcast, lanmask = aredn.hardware.get_interface_ip4(aredn.hardware.get_iface_name("lan")) local lannet_d = nixio.bit.band(ip_to_decimal(lanip), ip_to_decimal(lanmask)) local tmpdir = "/tmp/web/ports" if not parms.reload then remove_all(tmpdir) end nixio.fs.mkdir(tmpdir) local fsuffix = dmz_mode == 0 and ".nat" or ".dmz" local portfile = "/etc/config.mesh/_setup.ports" .. fsuffix local dhcpfile = "/etc/config.mesh/_setup.dhcp" .. fsuffix local dhcptagsfile = "/etc/config.mesh/_setup.dhcptags" .. fsuffix local dhcpoptionsfile = "/etc/config.mesh/_setup.dhcpoptions" .. fsuffix local servfile = "/etc/config.mesh/_setup.services" .. fsuffix local aliasfile = "/etc/config.mesh/aliases" .. fsuffix -- if a reset or a first time page load -- read the data from the config files if parms.button_reset or not parms.reload then local i = 0 if nixio.fs.stat(portfile) then for line in io.lines(portfile) do if not (line:match("^%s*#") or line:match("^%s*$")) then local k, v = line:match("(%S+)%s+=%s+(%S+)") if v then parms[k] = v else local intf, type, out, ip, _in, enable = line:match("(.*):(.*):(.*):(.*):(.*):(.*)") if intf then i = i + 1 parms["port" .. i .. "_intf"] = intf parms["port" .. i .. "_type"] = type parms["port" .. i .. "_out"] = out parms["port" .. i .. "_ip"] = ip parms["port" .. i .. "_in"] = _in parms["port" .. i .. "_enable"] = enable end end end end end parms.port_num = i -- set dhcp reservations -- ip addresses are stored as offsets from the lan network address i = 0 if nixio.fs.stat(dhcpfile) then for line in io.lines(dhcpfile) do if not (line:match("^%s*#") or line:match("^%s*$")) then local a, b, x = line:match("(%S*)%s+(%S*)%s+(.*)") if x then local c, d = x:match("(%S*)%s+(%S*)") if not c then c = x d = "" end i = i + 1 local prefix = "dhcp" .. i .. "_" parms[prefix .. "mac"] = a parms[prefix .. "ip"] = decimal_to_ip(lannet_d + tonumber(b)) parms[prefix .. "host"] = c parms[prefix .. "noprop"] = d end end end end parms.dhcp_num = i -- set dhcp tags i = 0 if nixio.fs.stat(dhcptagsfile) then for line in io.lines(dhcptagsfile) do if not (line:match("^%s*#") or line:match("^%s*$")) then local name, condition, pattern = line:match("(%S+)%s+(%S+)%s+(.*)") if pattern then i = i + 1 local prefix = "dhcptag" .. i .. "_" parms[prefix .. "name"] = name parms[prefix .. "cond"] = condition parms[prefix .. "pat"] = pattern end end end end parms.dhcptags_num = i -- set dhcp options i = 0 if nixio.fs.stat(dhcpoptionsfile) then for line in io.lines(dhcpoptionsfile) do if not (line:match("^%s*#") or line:match("^%s*$")) then local tag, force, opt_num, opt_val = line:match("(%S*)%s+(%w+)%s+(%d+)%s+(.*)") if force then i = i + 1 local prefix = "dhcpopt" .. i .. "_" parms[prefix .. "tag"] = tag parms[prefix .. "force"] = force parms[prefix .. "num"] = opt_num parms[prefix .. "val"] = opt_val end end end end parms.dhcpoptions_num = i -- services i = 0 if nixio.fs.stat(servfile) then for line in io.lines(servfile) do if not (line:match("^%s*#") or line:match("^%s*$")) then local a, b, c, d, x = line:match("([^|]*)|([^|]*)|([^|]*)|([^|]*)|(.*)") if x then local e, f = x:match("(.*)|(.*)") if not e then e = x f = "" end i = i + 1 local prefix = "serv" .. i .. "_" parms[prefix .. "name"] = a parms[prefix .. "link"] = b parms[prefix .. "proto"] = c parms[prefix .. "host"] = d parms[prefix .. "port"] = e parms[prefix .. "suffix"] = f end end end end parms.serv_num = i -- aliases i = 0 if nixio.fs.stat(aliasfile) then for line in io.lines(aliasfile) do if not (line:match("^%s*#") or line:match("^%s*$")) then local a, b = line:match("(.*)%s+(.*)") if b then i = i + 1 parms["alias" .. i .. "_ip"] = a parms["alias" .. i .. "_host"] = b end end end end parms.alias_num = i -- sanitize the 'add' values parms.port_add_intf = dmz_mode ~= 0 and "wan" or "wifi" parms.port_add_type = "tcp" if not parms.dmz_ip then parms.dmz_ip = "" end parms.port_add_out = "" parms.port_add_ip = "" parms.port_add_in = "" parms.dhcp_add_host = "" parms.dhcp_add_ip = "" parms.dhcp_add_mac = "" parms.dhcp_add_noprop = "" parms.dhcptag_add_name = "" parms.dhcptag_add_cond = "" parms.dhcptag_add_pat = "" parms.dhcpopt_add_tag = "" parms.dhcpopt_add_num = "" parms.dhcpopt_add_val = "" parms.serv_add_name = "" parms.serv_add_proto = "" parms.serv_add_host = "" parms.serv_add_port = "" parms.serv_add_suffix = "" parms.alias_add_host = "" parms.alias_add_ip = "" end local dhcp_start = tonumber(cursor:get("dhcp", "@dhcp[0]", "start")) local dhcp_limit = tonumber(cursor:get("dhcp", "@dhcp[0]", "limit")) local dhcp_end = dhcp_start + dhcp_limit - 1 -- load and validate the ports local function make_addable_list(max_row) local list = {} for i = 1,max_row do list[#list + 1] = i end list[#list + 1] = "_add" return list end local list = make_addable_list(parms.port_num) local port_num = 0 local usedports = {} local f = io.open(tmpdir .. "/ports", "w") if f then local vars = { "_intf", "_type", "_out", "_ip", "_in" } for _, val in ipairs(list) do for _ = 1,1 -- a non-loop so we can break out to continue the parent loop do for _, var in ipairs(vars) do local varname = "port" .. val .. var local v = parms[varname] if v then v = v:gsub("^%s+", ""):gsub("%s+$", "") else v = "" end parms[varname] = v _G[var] = v end local varname = "port" .. val .. "_enable" if not parms[varname] then parms[varname] = "0" end _enable = parms[varname] if val == "_add" then _enable = "1" end local continue = false if val == "_add" then if not ((_out ~= "" or _ip ~= "" or _in ~= "") and (parms.port_add or parms.button_save)) then break end else if parms["port" .. val .. "_del"] then break end end if val == "_add" and parms.button_save then porterr(val .. " this rule must be added or cleared out before saving changes") break end if _out:match("-") then if validate_port_range(_out) then _in = _out:match("^%s*(%d+)") else porterr(val .. "'" .. _out .. "' is not a valid port range") end else if _out == "" then porterr(val .. " an outside port is required") elseif not validate_port(_out) then porterr(val .. "'" .. _out .. "' is not a valid port") end end if _ip == "" then porterr(val .. " an address must be selected") elseif not validate_ip(_ip) then porterr(val .. "'" .. _ip .. "' is not a valid address") end if _in == "" then porterr(val .. "a LAN port is required") elseif not validate_port(_in) then porterr(val .. "'" .. _in .. "' is not a valid port") end if val == "_add" and #port_err > 0 then break end -- commit the data for this rule port_num = port_num + 1 usedports[_out] = true if _type ~= "tcp" and _type ~= "udp" then _type = "both" end f:write(_intf .. ":" .. _type .. ":" .. _out .. ":" .. _ip .. ":" .. _in .. ":" .. _enable .. "\n") for _, var in ipairs(vars) do parms["port" .. port_num .. var] = _G[var] end if val == "_add" then parms.port_add_intf = "wifi" parms.port_add_out = "" parms.port_add_ip = "" parms.port_add_in = "" end end end if parms.dmz_ip and parms.dmz_ip ~= "" then f:write("dmz_ip = " .. parms.dmz_ip .. "\n") if not validate_ip(parms.dmz_ip) then dmzerr(parms.dmz_ip .. " is not a valid address") end end f:close() end parms.port_num = port_num -- load and validate the dhcp reservations local list = make_addable_list(parms.dhcp_num) local dhcp_num = 0 for _, val in ipairs(list) do for _ = 1,1 do local host = parms["dhcp" .. val .. "_host"] local ip = parms["dhcp" .. val .. "_ip"] local mac = parms["dhcp" .. val .. "_mac"] local noprop = parms["dhcp" .. val .. "_noprop"] if not noprop then noprop = "" end local foundhost = false if val == "_add" then if host ~= "" then local pattern = "%s" .. host .. "%s" for line in aredn.olsr.getHostAsLines() do if line:lower():match(pattern) then foundhost = true dhcperr(val .. [[ Warning! ']] .. host .. [[ is already in use!
Please choose another hostname.
Prefixing the hostname with your callsign will help prevent duplicates on the network. ]]) break end end end if not ((host ~= "" or ip ~= "" or mac ~= "" or foundhost) and (parms.dhcp_add or parms.button_save)) then break end elseif parms["dhcp" .. val .. "_del"] then break end if val == "_add" and parms.button_save then dhcperr(val .. " this reservation must be added or cleared before saving changes") break end if validate_hostname(host) then if not foundhost then local lhost = host:lower() if lhost == node:lower() or lhost == tactical:lower() then dhcperr(val .. " hostname '" .. host .. "' is already in use") end for key, _ in pairs(hosts) do if key:lower() == lhost then dhcperr(val .. " hostname '" .. host .. "' is already in use") break end end end else if host then dhcperr(val .. " '" .. host .. "' is not a valid hostname") else dhcperr(val .. " a hostname is required") end end if validate_ip(ip) then if addrs[ip] then dhcperr(val .. " '" .. ip .. "' is already in use") elseif ip == lanip or not validate_same_subnet(ip, lanip, lanmask) or not validate_ip_netmask(ip, lanmask) then dhcperr(val .. " '" .. ip .. "' is not a valid LAN address") end else if ip then dhcperr(val .. " '" .. ip .. "' is not a valid address") else dhcperr(val .. " an IP Address must be selected") end end if mac:match(mac_pattern) then if macs[mac] then dhcperr(val .. " MAC " .. mac .. " is already in use") end else if mac then dhcperr(val .. " '" .. mac .. "' is not a valid mac address") else dhcperr(val .. " a MAC address is required") end end if val == "_add" and #dhcp_err > 0 then break end dhcp_num = dhcp_num + 1 local prefix = "dhcp" .. dhcp_num .. "_" parms[prefix .. "host"] = host parms[prefix .. "ip"] = ip parms[prefix .. "mac"] = mac parms[prefix .. "noprop"] = noprop hosts[host] = true addrs[ip] = true macs[mac] = true if val == "_add" then parms.dhcp_add_host = "" parms.dhcp_add_ip = "" parms.dhcp_add_mac = "" parms.dhcp_add_noprop = "" end end end -- add existing leases for lease, lval in pairs(parms) do local n = lease:match("^lease(%d+)_add$") if n and lval ~= "" then local found = false for k, v in pairs(parms) do if k:match("dhcp%d+_mac") then if v == parms["lease" .. n .. "_mac"] then found = true break end end end if not found then dhcp_num = dhcp_num + 1 local prefix1 = "lease" .. n .. "_" local prefix2 = "dhcp" .. dhcp_num .. "_" local host = parms[prefix1 .. "host"] local ip = parms[prefix1 .. "ip"] local mac = parms[prefix1 .. "mac"] local noprop = parms[prefix1 .. "noprop"] parms[prefix2 .. "host"] = host parms[prefix2 .. "ip"] = ip parms[prefix2 .. "mac"] = mac parms[prefix2 .. "noprop"] = noprop and noprop or "0" local lhost = host:lower() if lhost == node:lower() or lhost == tactical:lower() then dhcperr(dhcp_num .. " hostname '" .. host .. "' is already in use") end for key, _ in pairs(hosts) do if key:lower() == lhost then dhcperr(dhcp_num .. " hostname '" .. host .. "' is already in use") break end end if addrs[ip] then dhcperr(dhcp_num .. " is already in use") end if macs[mac] then dhcperr(dhcp_num .. " MAC " .. mac .. " is already in use") end end end end parms.dhcp_num = dhcp_num dhcphosts = {} dhcphosts[lanip] = "localhost" -- replace "blank" dhcp hostname and save the dhcp info into the tmpdir local f = io.open(tmpdir .. "/dhcp", "w") if f then local nn = 1 for i = 1,parms.dhcp_num do if parms["dhcp" .. i .. "_host"] == "*" then while hosts["noname" .. nn] do nn = nn + 1 end parms["dhcp" .. i .. "_host"] = "noname" .. nn hosts["noname" .. nn] = true end f:write(parms["dhcp" .. i .. "_mac"] .. " " .. (ip_to_decimal(parms["dhcp" .. i .. "_ip"]) - lannet_d) .. " " .. parms["dhcp" .. i .. "_host"] .. " " .. parms["dhcp" .. i .. "_noprop"] .. "\n") if not (dhcphosts[parms["dhcp" .. i .. "_ip"]]) then dhcphosts[parms["dhcp" .. i .. "_ip"]] = parms["dhcp" .. i .. "_host"] end end f:close() end local function write_temp_records(filename, group_name, record_count, fields) local f = io.open(tmpdir .. "/" .. filename, "w") if f then for i = 1,record_count do local prefix = group_name .. i .. "_" local record = {} for _, field in ipairs(fields) do table.insert(record, parms[prefix .. field]) end f:write(table.concat(record, " ") .. "\n") end f:close() end end -- DHCP tags local list = make_addable_list(parms.dhcptags_num) local dhcptags_num = 0 for _, val in ipairs(list) do for _ = 1,1 do local prefix = "dhcptag" .. val .. "_" local name = parms[prefix .. "name"] local cond_key = parms[prefix .. "cond"] local matcher = parms[prefix .. "pat"] if val == "_add" then if not ((name ~= "" or cond_key ~= "" or matcher ~= "") and (parms.dhcptag_add or parms.button_save)) then break end elseif parms[prefix .. "del"] then break end if val == "_add" and parms.button_save then dhcptagerr(val .. " this tag must be added or cleared out before saving changes") break end if name and not name:match("^%w+$") then dhcptagerr(val .. [[ Warning! The tag name "]] .. html_safe(name) .. [[" is invalid; tag names must contain only letters and digits.]]) break end if cond_key == "" then dhcptagerr(val .. [[ Warning! Please choose a client parameter to match.]]) break end if matcher == "" then dhcptagerr(val .. [[ Warning! Please supply a value to match to the client parameter.]]) break else local tag_validator = dhcp_tag_validators[cond_key] if not tag_validator then dhcptagerr(string.format([[%s Warning! "%s" is not a known DHCP parameter.]], val, cond_key)) break end if not tag_validator.validator(matcher) then dhcptagerr(string.format([[Line %s: Warning! "%s" is not a valid match for %s; must be %s]], val, html_safe(matcher), cond_key, (tag_validator.hint:gsub("\\\\", "\\")))) break end end if val == "_add" and #dhcptag_err > 0 and dhcptag_err[#dhcptag_err]:match("^" .. val .. " ") then break end dhcptags_num = dhcptags_num + 1 prefix = "dhcptag" .. dhcptags_num .. "_" parms[prefix .. "name"] = name parms[prefix .. "cond"] = cond_key parms[prefix .. "pat"] = matcher if val == "_add" then parms.dhcptag_add_name = "" parms.dhcptag_add_cond = "" parms.dhcptag_add_pat = "" end end end -- write to temp file write_temp_records("dhcptags", "dhcptag", dhcptags_num, {"name", "cond", "pat"}) parms.dhcptags_num = dhcptags_num -- DHCP options local list = make_addable_list(parms.dhcpoptions_num) local dhcpoptions_num = 0 for _, val in ipairs(list) do for _ = 1,1 do local prefix = "dhcpopt" .. val .. "_" local tag = parms[prefix .. "tag"] local force = parms[prefix .. "force"] if force ~= "force" then force = "onrequest" end local opt_num = parms[prefix .. "num"] local opt_val = parms[prefix .. "val"] if val == "_add" then if not ((tag ~= "" or opt_num ~= "" or opt_val ~= "") and (parms.dhcpopt_add or parms.button_save)) then break end elseif parms[prefix .. "del"] then break end if val == "_add" and parms.button_save then dhcpopterr(val .. " this option must be added or cleared out before saving changes") break end if tag ~= "" and not tag:match("^%w+$") then dhcpopterr(val .. [[ Warning! The tag name "]] .. html_safe(tag) .. [[" is invalid; tag names must contain only letters and digits.]]) break end local nnum = tonumber(opt_num) if not nnum or nnum < 1 or nnum > 254 then dhcpopterr(val .. [[ Warning! Option number must be an integer in the range [1..254].]]) break end -- TODO check opt_val for suitability for opt_num if opt_val == "" then dhcpopterr(val .. [[ Warning! Please supply a value to send for this option.]]) break end if val == "_add" and #dhcpopt_err > 0 and dhcpopt_err[#dhcpopt_err]:match("^" .. val .. " ") then --if val == "_add" and #dhcpopt_err > 0 then break end dhcpoptions_num = dhcpoptions_num + 1 prefix = "dhcpopt" .. dhcpoptions_num .. "_" parms[prefix .. "tag"] = tag parms[prefix .. "force"] = force parms[prefix .. "num"] = opt_num parms[prefix .. "val"] = opt_val if val == "_add" then parms.dhcpopt_add_tag = "" parms.dhcpopt_add_force = "" parms.dhcpopt_add_num = "" parms.dhcpopt_add_val = "" end end end -- write to temp file write_temp_records("dhcpoptions", "dhcpopt", dhcpoptions_num, {"tag", "force", "num", "val"}) parms.dhcpoptions_num = dhcpoptions_num -- aliases local list = make_addable_list(parms.alias_num) local alias_num = 0 for _, val in ipairs(list) do for _ = 1,1 do local foundhost = false local host = parms["alias" .. val .. "_host"] local ip = parms["alias" .. val .. "_ip"] -- if adding aliases check the name is not already in use, -- also check that it dos not contain anything that will be weird on the mesh if val == "_add" then if host ~= "" then local pattern = "%s" .. host .. "%s" for line in aredn.olsr.getHostAsLines() do if line:lower():match(pattern) then foundhost = true aliaserr(val .. [[ Warning! ']] .. host .. [[ is already in use!
Please choose another hostname.
Prefixing the hostname with your callsign will help prevent duplicates on the network. ]]) break end end if not host:match("^%*%.") and (host:match("_") or not host:match("^[%w%-%.]+$")) then aliaserr(val .. " Warning! The alias name: '" .. host .."' is invalid") end end if not ((host ~= "" or ip ~= "" or foundhost) and (parms.alias_add or parms.button_save)) then break end elseif parms["alias" .. val .. "_del"] then break end if val == "_add" and parms.button_save then aliaserr(val .. " this alias must be added or cleared out before saving changes") break end if val == "_add" and #alias_err > 0 and alias_err[#alias_err]:match("^" .. val .. " ") then break end alias_num = alias_num + 1 parms["alias" .. alias_num .. "_host"] = host parms["alias" .. alias_num .. "_ip"] = ip hosts[host] = true if val == "_add" then parms.alias_add_host = "" parms.alias_add_ip = "" end end end -- write to temp file write_temp_records("aliases", "alias", alias_num, {"ip", "host"}) parms.alias_num = alias_num -- load and validate services local list = make_addable_list(parms.serv_num) local serv_num = 0 hosts[""] = true hosts[node] = true usedports[""] = true local servicenames = {} local vars = { "name", "link", "proto", "host", "port", "suffix" } local f = io.open(tmpdir .. "/services", "w") if f then for _, val in ipairs(list) do for _ = 1,1 do for _, var in ipairs(vars) do local varname = "serv" .. val .. "_" .. var local v = parms[varname] if v then v = v:gsub("^%s+", ""):gsub("%s+$", "") else v = "" end parms[varname] = v _G[var] = v end if link == "" then link = "0" end if dmz_mode == 0 then host = node end -- remove services that have had their host or port deleted if val ~= "_add" and not (dmz_mode == 0 and true or hosts[host]) then break end if val == "_add" then if not ((name ~= "" or proto ~= "" or port ~= "" or suffix ~= "") and (parms.serv_add or parms.button_save)) then break end else if parms["serv" .. val .. "_del"] or not (name ~= "" or proto ~= "" or port ~= "" or suffix ~= "") then break end end if val == "_add" and parms.button_save then serverr(val .. " this service must be added or cleared out before saving changes") break end if name == "" then serverr(val .. " a name is required") elseif not validate_service_name(name) then serverr(val .. " '" .. name .. "' is not a valid service name") elseif servicenames[name] then serverr(val .. " the name '" .. name .. "' is already in use") end if link == "1" then if proto == "" then proto = "http" end parms["serv" .. val .. "_proto"] = proto if not validate_service_protocol(proto) then serverr(val .. " '" .. proto .. "' is not a valid protocol") elseif port == "" then serverr(val .. " a port number is required") elseif not validate_port(port) then serverr(val .. " '" .. port .. "' is not a valid port") elseif not validate_service_suffix(suffix) then serverr(val .. " '" .. suffix .. "' is not a valid service suffix") end elseif val == "_add" then proto = "" port = "" suffix = "" end if val == "_add" and #serv_err > 0 then break end -- commit the data for this service serv_num = serv_num + 1 servicenames[name] = true f:write(name .. "|" .. link .. "|" .. proto .. "|" .. host .. "|" .. port .. "|" .. suffix .. "\n") for _, var in ipairs(vars) do parms["serv" .. serv_num .. "_" .. var] = _G[var] end if val == "_add" then parms.serv_add_name = "" parms.serv_add_link = "" parms.serv_add_proto = "" parms.serv_add_host = "" parms.serv_add_port = "" parms.serv_add_suffix = "" end end end f:close() end parms.serv_num = serv_num -- save configuration if parms.button_save and not (#port_err > 0 or #dhcp_err > 0 or #dhcptag_err > 0 or #dhcpopt_err > 0 or #dmz_err > 0 or #serv_err > 0 or #alias_err > 0) then filecopy(tmpdir .. "/ports", portfile) filecopy(tmpdir .. "/dhcp", dhcpfile) filecopy(tmpdir .. "/dhcptags", dhcptagsfile) filecopy(tmpdir .. "/dhcpoptions", dhcpoptionsfile) filecopy(tmpdir .. "/services", servfile) filecopy(tmpdir .. "/aliases", aliasfile) os.remove("/tmp/service-validation-state") if os.execute("/usr/local/bin/node-setup > /dev/null 2>&1") ~= 0 then err("problem with configuration") else os.execute("/usr/local/bin/restart-services.sh dnsmasq firewall olsrd > /dev/null 2>&1") end end -- generate the page http_header() html.header(node .. " setup", false) do local function generateValidatorCaseJS(cond) local caseTab = {' case "', cond.key, '":\n newPlaceholder = "', cond.jsPlaceholder, '";\n newTitle = "', cond.hint} if cond.jsPattern then table.insert(caseTab, '";\n newPattern = "') table.insert(caseTab, cond.jsPattern) end table.insert(caseTab, '";\n break;\n') return table.concat(caseTab, '') end -- Note: toggleField comes from https://jsfiddle.net/6nq7w/4/ local scriptTab = {[[ ]]) html.print(table.concat(scriptTab, '')) end html.print("") html.print("
") html.alert_banner() html.print("
") html.navbar_admin("ports") html.print("") -- control buttons html.print([[]]) hide("") -- messages if parms.button_save then if #port_err > 0 or #dhcp_err > 0 or #dhcptag_err > 0 or #dhcpopt_err > 0 or #dmz_err > 0 or #serv_err > 0 then html.print("") elseif #errors > 0 then html.print("") else html.print("") end html.print("") end -- everything else local js_mac_pattern = [[ pattern='^(?:[0-9a-fA-F]{2}:){5}[0-9a-fA-F]{2}$' ]] local js_host_pattern = [[ pattern='^[a-zA-Z0-9][a-zA-Z0-9\-]*$' ]] local js_alias_pattern = [[ pattern='^(\*\.|)?[a-zA-Z0-9][a-zA-Z0-9\-]*$' ]] local js_port_range_pattern = [[ pattern='^\s*\d{1,5}(?:\s*-\s*\d{1,5})?\s*$' ]] local function print_heading_vsep() html.print("") end local function print_new_entry_vsep(val, list, columns) if val == "_add" and #list > 1 then html.print("") end end -- mark input fields of already-added entries as "required" local function require_existing(val) return val == "_add" and "" or " required " end local function print_errors(error_list) for _, e in ipairs(error_list) do html.print("") end end local function print_reservations() html.print("
Help          
 
Configuration NOT saved!
Configuration saved, however:
") for _, err in ipairs(errors) do html.print(err .. "
") end html.print("
Configuration saved and is now active.
 
" .. e .. "
") print_heading_vsep() html.print("") if dmz_mode ~= 0 then html.print("") else html.print("") end print_heading_vsep() local list = make_addable_list(parms.dhcp_num) local mac_list = {} for _, val in ipairs(list) do local host = parms["dhcp" .. val .. "_host"] local ip = parms["dhcp" .. val .. "_ip"] local mac = parms["dhcp" .. val .. "_mac"]:lower() local noprop = parms["dhcp" .. val .. "_noprop"] mac_list[mac] = true print_new_entry_vsep(val, list, 5) html.print("") html.print("") html.print("") if dmz_mode ~= 0 then html.print("") else html.print("") end html.print("") while #dhcp_err > 0 and dhcp_err[1]:match("^" .. val .. " ") do html.print("") table.remove(dhcp_err, 1) end html.print("") end html.print("") html.print("\n") local i = 0 if nixio.fs.stat("/tmp/dhcp.leases") then for line in io.lines("/tmp/dhcp.leases") do i = i + 1 local _, mac, ip, host = line:match("(%S+)%s+(%S+)%s+(%S+)%s+(%S+)") html.print("") html.print("") html.print("") hide("") hide("") hide("") end end if i == 0 then html.print("") end html.print("
DHCP Address Reservations
HostnameIP AddressMAC AddressDo Not
Propagate
 
" .. dhcp_err[1]:gsub("^%S+ ", "") .. "
 
Current DHCP Leases
" .. host .. "" .. ip .. "" .. mac .. " 
there are no active leases
") end local function print_forwarding() html.print("") html.print("") html.print("") html.print("") html.print("") html.print("") html.print("") local list = make_addable_list(parms.port_num) local vars = { "_intf", "_type", "_out", "_ip", "_in", "_enable", "_adv", "_link", "_proto", "_suffix", "_name" } for _, val in ipairs(list) do for _, var in ipairs(vars) do _G[var] = parms["port" .. val .. var] end print_new_entry_vsep(val, list, 6) html.print("") hide("") -- port forwarding settings html.print("") html.print("") html.print("") html.print("") html.print("") html.print("") -- display any errors while #port_err > 0 and port_err[1]:match("^" .. val .. " ") do local err = port_err[1]:gsub("^%S+ ", "") html.print("") table.remove(port_err, 1) end html.print("") end -- dmz server for nat mode if dmz_mode == 0 then html.print("") html.print("") html.print("") print_errors(dmz_err) end html.print("
Port Forwarding
InterfaceTypeOutside PortLAN IPLAN Port 
 
" .. err .. "
DMZ Server  
") end local function print_services() local activesvc = nil if nixio.fs.stat("/var/etc/olsrd.conf") then activesvc = {} for line in io.lines("/var/etc/olsrd.conf") do local plparam = line:match([[^%s*(PlParam "service" ".*")%s*$]]) if plparam then activesvc[plparam] = true end end end html.print("") if not (dmz_mode ~= 0 or parms.port_num ~= 0 or parms.dmz_ip) then if dmz_mode ~= 0 then html.print("") else html.print("") end html.print("") html.print("
Advertised Services
 
 

none
") return; end if dmz_mode ~= 0 then html.print("") html.print("NameLinkURL") html.print("") else html.print("NameLinkURL

") end local list = make_addable_list(parms.serv_num) local vars = { "_name", "_link", "_proto", "_host", "_port", "_suffix" } for _, val in ipairs(list) do for _, var in ipairs(vars) do _G[var] = parms["serv" .. val .. var] end if dmz_mode == 0 then _host = node parms["serv" .. val .. "_host"] = node end print_new_entry_vsep(val, list, 4) html.print("") html.print("") html.print("") html.print("") if dmz_mode ~= 0 then html.print("://") else html.print("://" .. _host .. "") end html.print(": / ") html.print(" ") else html.print("serv" .. val .. "_del value='Del ' title='Remove this service'>") end local svc = string.format([[PlParam "service" "%s://%s:%s/%s|tcp|%s"]], (_link == "1" and _proto or "http"), _host, (_link == "1" and _port or "0"), _suffix, _name) if val ~= "_add" and activesvc and not activesvc[svc] then html.print("") end html.print("") -- display any errors while #serv_err > 0 and serv_err[1]:match("^" .. val .. " ") do html.print("" .. serv_err[1]:gsub("^%S+ ", "") .. "") table.remove(serv_err, 1) end if _link ~= "1" and val ~= "_add" then hide("") hide("") hide("") hide("") end html.print("") end html.print("") end local function print_aliases() html.print("") html.print("") html.print("") html.print("") local list = make_addable_list(parms.alias_num) for _, val in ipairs(list) do local host = parms["alias" .. val .. "_host"] local ip = parms["alias" .. val .. "_ip"] print_new_entry_vsep(val, list , 4) html.print("") html.print("") html.print("") html.print("") end for _, e in ipairs(alias_err) do html.print("") end html.print("
DNS Aliases
Alias NameIP Address
      
" .. e:gsub("^%S+ ", "") .. "
") end local function print_dhcp_cond_selector(row, selected_key, allow_unset, patname) local field_name = "dhcptag" .. row .. "_cond" html.print("") end local function dhcp_tag_used(tag_name) if tag_name == "" then return false end for i = 1,parms.dhcpoptions_num do if tag_name == parms["dhcpopt" .. i .. "_tag"] then return true end end return false end local function print_dhcp_tags() html.print("") print_heading_vsep() html.print("") local list = make_addable_list(parms.dhcptags_num) for _, val in ipairs(list) do local prefix = "dhcptag" .. val .. "_" local name = parms[prefix .. "name"] local cond_key = parms[prefix .. "cond"] local matcher = parms[prefix .. "pat"] print_new_entry_vsep(val, list, 4) html.print("") local patname = prefix .. "pat" print_dhcp_cond_selector(val, cond_key, val == "_add", patname) local t = {"") html.print(table.concat(t, '')) html.print("") end print_errors(dhcptag_err) html.print("
Tags for Advanced DHCP Options
Set a Tag NamedWhen Client'sMatches
 
") end local function get_dhcp_tag_names() local tag_hash = {} local names = {} for val = 1,parms.dhcptags_num do local name = parms["dhcptag" .. val .. "_name"] if name and not tag_hash[name] then tag_hash[name] = 0 table.insert(names, name) end end table.sort(names) return names end local function print_dhcp_tag_selector(row, tag_name, all_tags) local prefix = "dhcpopt" .. row .. "_" local field_name = prefix .. "tag" html.print("") end local function print_dhcp_option_selector(known_options, row, opt_num) local field_name = "dhcpopt" .. row .. "_num" -- custom-value option adapted from https://jsfiddle.net/6nq7w/4/ html.print("]]) end -- load all known DHCP option names local function load_known_options() local known_options = {} local optlist = io.popen("/usr/sbin/dnsmasq --help dhcp") if optlist then for line in optlist:lines() do local num, name = line:match("%s*(%d+)%s+(%S+)") if name then table.insert(known_options, {num = tonumber(num), name = name}) end end optlist:close() end return known_options end local function print_dhcp_options() html.print("") print_heading_vsep() html.print("") local known_options = load_known_options() local all_tags = get_dhcp_tag_names() local list = make_addable_list(parms.dhcpoptions_num) for _, val in ipairs(list) do local prefix = "dhcpopt" .. val .. "_" local forceAttr = prefix .. "force" local tag = parms[prefix .. "tag"] or "" local force = tag == "" and "onrequest" or parms[forceAttr] -- force is unsupported for untagged options local opt_num = tonumber(parms[prefix .. "num"]) local opt_val = parms[prefix .. "val"] or "" print_new_entry_vsep(val, list, 5) html.print("") print_dhcp_tag_selector(val, tag, all_tags) html.print("") print_dhcp_option_selector(known_options, val, opt_num) html.print("") html.print("") end print_errors(dhcpopt_err) html.print("
Advanced DHCP Options
For TagAlwaysSend DHCP OptionWith Value
 
") end html.print("") html.print("") html.print("") html.print("") html.print("") html.print("") html.print("") html.print("") html.print("") html.print("") html.print("") html.print("
") if dmz_mode ~= 0 then print_reservations() else print_forwarding() end html.print("     ") print_services() html.print("

") print_dhcp_tags() html.print("     ") print_dhcp_options() html.print("

") if dmz_mode ~= 0 then print_forwarding() else print_reservations() end html.print("     ") print_aliases() html.print("
") html.print("") html.footer() html.print(" ") hide("") hide("") hide("") hide("") hide("") hide("") for _, h in ipairs(hidden) do html.print(h) end html.print("
") http_footer()