423 lines
12 KiB
Lua
Executable File
423 lines
12 KiB
Lua
Executable File
#!/usr/bin/lua
|
|
|
|
local uci = require('simple-uci').cursor()
|
|
local ip = require 'luci.ip' -- luci-lib-ip
|
|
local fetch = require 'luci.httpclient'
|
|
local json = require 'luci.jsonc'
|
|
local ecdsa = require 'gluon.ecdsa'
|
|
local site = require 'gluon.site'
|
|
|
|
local pretty_hostname = require 'pretty_hostname'
|
|
local hostname = pretty_hostname.get(uci)
|
|
|
|
local manapi = site.manman.api()
|
|
local mankey = site.manman.key()
|
|
|
|
-- CLI lib from olsrd-cli (TODO: move to it's own lib package)
|
|
-- DO NOT EDIT HERE, JUST COPY FROM THERE
|
|
|
|
function printf(...)
|
|
print(string.format(...))
|
|
end
|
|
|
|
-- Print contents of `tbl`, with indentation.
|
|
-- `indent` sets the initial level of indentation.
|
|
-- src https://gist.github.com/xytis/5361405
|
|
function tprint (tbl, indent)
|
|
if not indent then indent = 0 end
|
|
for k, v in pairs(tbl) do
|
|
formatting = string.rep(' ', indent) .. k .. ': '
|
|
if type(v) == 'table' then
|
|
print(formatting)
|
|
tprint(v, indent + 1)
|
|
else
|
|
print(formatting .. tostring(v))
|
|
end
|
|
end
|
|
end
|
|
|
|
-- src https://stackoverflow.com/a/24823383/3990041
|
|
function table.slice(tbl, first, last, step)
|
|
local sliced = {}
|
|
|
|
for i = first or 1, last or #tbl, step or 1 do
|
|
sliced[#sliced+1] = tbl[i]
|
|
end
|
|
|
|
return sliced
|
|
end
|
|
|
|
-- CLI lib
|
|
|
|
function exec_cmd(args, sub)
|
|
if sub[args[1]] == nil then
|
|
return cmd_err('does not exist')
|
|
else
|
|
local cmd = sub[args[1]]
|
|
if cmd[3] ~= nil and #args > 1 then
|
|
exec_cmd(table.slice(args, 2), cmd[3])
|
|
else
|
|
cmd[1](unpack(table.slice(args, 2)))
|
|
end
|
|
end
|
|
end
|
|
|
|
function list_cmd(level, sub)
|
|
for key, cmd in pairs(sub) do
|
|
printf('%s%s: %s', string.rep(' ', level), key, cmd[2])
|
|
if cmd[3] ~= nil then
|
|
list_cmd(level + 1, cmd[3])
|
|
end
|
|
end
|
|
end
|
|
|
|
function show_help()
|
|
printf('Usage: %s <command>', arg[0])
|
|
list_cmd(1, sub)
|
|
end
|
|
|
|
function cmd_err(msg, no_show_help)
|
|
-- since argv0 is at... well... 0... even though this is lua...
|
|
--- ...slice just returns arg without argv0 as the for starts at 1
|
|
printf('Error: Command "%s" %s', table.concat(table.slice(arg), ' '), msg)
|
|
if not no_show_help then
|
|
printf('')
|
|
show_help()
|
|
end
|
|
os.exit(2)
|
|
end
|
|
|
|
function dummy()
|
|
cmd_err('requires a subcommand')
|
|
end
|
|
|
|
-- ///
|
|
|
|
-- NOTE: these will have mesh_ appended for static-ip
|
|
local mappings = {
|
|
wifi = 'radio0',
|
|
tunnel = 'vpn',
|
|
eth = 'other',
|
|
lan = 'other',
|
|
wan = 'uplink',
|
|
single = 'uplink',
|
|
}
|
|
|
|
-- https://stackoverflow.com/a/1647577/3990041
|
|
function string:split(pat)
|
|
pat = pat or '%s+'
|
|
local st, g = 1, self:gmatch("()("..pat..")")
|
|
local function getter(segs, seps, sep, cap1, ...)
|
|
st = sep and seps + #sep
|
|
return self:sub(segs, (seps or 0) - 1), cap1 or sep, ...
|
|
end
|
|
return function() if st then return getter(st, g()) end end
|
|
end
|
|
|
|
-- https://gist.github.com/Uradamus/10323382
|
|
local function shuffle(tbl)
|
|
for i = #tbl, 2, -1 do
|
|
local j = math.random(i)
|
|
tbl[i], tbl[j] = tbl[j], tbl[i]
|
|
end
|
|
return tbl
|
|
end
|
|
|
|
local function fetch_signed_json(remote, url)
|
|
print('GET ' .. url)
|
|
|
|
local code, res, result = fetch.request_raw(remote .. url)
|
|
|
|
if code < 1 then
|
|
print('E: failed to fetch')
|
|
return 1
|
|
end
|
|
|
|
if code == 404 then
|
|
print('E: location does not exist')
|
|
return 2
|
|
end
|
|
|
|
if code ~= 200 then
|
|
print('E: got status code ' .. code)
|
|
return 1
|
|
end
|
|
|
|
-- cloudflare's reverse proxies send http chunked responses with chunk sizes
|
|
-- for whatever reasons the chunk size gets smashed into the result
|
|
-- this is a hack to fish it out, it is irrelevant on unaffected reverse proxies
|
|
local j_start = result:find('{')
|
|
local j_end = (result:reverse()):find("}")
|
|
result = string.sub(result, j_start, (1 - j_end) > 0 and (1 - j_end) or nil)
|
|
|
|
local sig = res.headers['x-ecdsa']
|
|
local ts = res.headers['x-ecdsa-ts']
|
|
|
|
if not sig or not ts then
|
|
print('E: provided response is not signed')
|
|
return 1
|
|
end
|
|
|
|
local data = ts .. '@' .. result
|
|
if not ecdsa.verify(data, sig, mankey) then
|
|
print('E: signature invalid or not signed with expected key')
|
|
return 1
|
|
end
|
|
|
|
local obj = json.parse(result)
|
|
|
|
if obj == nil then
|
|
print('E: failed to parse json data')
|
|
return 1
|
|
end
|
|
|
|
return false, obj
|
|
end
|
|
|
|
local function do_manman_sync()
|
|
if not uci:get('gluon-manman-sync', 'sync', 'location_id') and not uci:get('gluon-manman-sync', 'sync', 'location') then
|
|
print('E: manman location missing')
|
|
return 2
|
|
end
|
|
|
|
-- check manman reachability, abort if not reachable
|
|
|
|
local working_remote
|
|
|
|
for _, remote in ipairs(shuffle(manapi)) do
|
|
if not working_remote then -- don't try other remotes if we got one that works
|
|
print('Trying remote ' .. remote)
|
|
|
|
local success, a, b, c = pcall(function() return fetch.request_raw(remote .. '/') end)
|
|
if not success then
|
|
print('E: couldnt reach manman: ' .. a)
|
|
else
|
|
if a ~= 200 then
|
|
print('E: couldnt reach manman - unexpected fetch result', a, b, c)
|
|
else
|
|
working_remote = remote
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
if not working_remote then
|
|
print('E: couldnt reach any manapi server, giving up')
|
|
return 1
|
|
end
|
|
|
|
-- try to fetch data
|
|
print('Fetching manman data...')
|
|
local location_id = uci:get('gluon-manman-sync', 'sync', 'location_id')
|
|
local location = uci:get('gluon-manman-sync', 'sync', 'location')
|
|
|
|
if not location_id and location then
|
|
local err, resp = fetch_signed_json(working_remote, '/find_location_by_name/' .. location)
|
|
if err then
|
|
return err
|
|
end
|
|
|
|
if not resp.id then
|
|
printf('E: location %s is invalid (404)', location)
|
|
return 2
|
|
end
|
|
|
|
uci:set('gluon-manman-sync', 'sync', 'location_id', resp.id)
|
|
location_id = resp.id
|
|
end
|
|
|
|
local err, location = fetch_signed_json(working_remote, '/location/show/' .. location_id)
|
|
if err then
|
|
return err
|
|
end
|
|
|
|
print('Syncing with location ' .. location.location.name)
|
|
|
|
uci:set('gluon-manman-sync', 'sync', 'location', location.location.name)
|
|
|
|
if uci:get('gluon-manman-sync', 'sync', 'last_data') and json.stringify(location) == uci:get('gluon-manman-sync', 'sync', 'last_data') then
|
|
print('Nothing changed, skipping sync')
|
|
return 0
|
|
end
|
|
|
|
local local_router_id
|
|
for id, _ in string.split(hostname, '-') do
|
|
if id then
|
|
local_router_id = id
|
|
end
|
|
end
|
|
|
|
local local_node_name = uci:get('gluon-manman-sync', 'sync', 'node') or local_router_id
|
|
local local_node = uci:get('gluon-manman-sync', 'sync', 'node_id') or local_router_id
|
|
local node
|
|
local should_hostname
|
|
|
|
if #location.nodes > 1 then
|
|
for _, potential_node in ipairs(location.nodes) do
|
|
if (local_node ~= nil and tostring(potential_node.id) == local_node) or (local_node_name ~= nil and potential_node.name == local_node_name) then
|
|
node = potential_node
|
|
should_hostname = location.location.name .. '-' .. node.name
|
|
end
|
|
end
|
|
else
|
|
node = location.nodes[1]
|
|
should_hostname = location.location.name
|
|
end
|
|
|
|
-- save node data to update name and persist ID
|
|
uci:set('gluon-manman-sync', 'sync', 'node_id', node.id)
|
|
uci:set('gluon-manman-sync', 'sync', 'node', node.name)
|
|
|
|
if node == nil then
|
|
print('E: unable to find matching node (selector "' .. node .. '")')
|
|
return 2
|
|
end
|
|
|
|
print('Syncing data for node ' .. node.name)
|
|
|
|
if hostname ~= should_hostname then
|
|
print('Renaming node to ' .. should_hostname)
|
|
pretty_hostname.set(uci, should_hostname)
|
|
end
|
|
|
|
local owner = uci:get_first('gluon-node-info', 'owner')
|
|
uci:set('gluon-node-info', owner, 'contact', location.administrator.email)
|
|
local _location = uci:get_first('gluon-node-info', 'location')
|
|
uci:set('gluon-node-info', _location, 'share_location', '1')
|
|
uci:set('gluon-node-info', _location, 'latitude', location.location.lat)
|
|
uci:set('gluon-node-info', _location, 'longitude', location.location.long)
|
|
|
|
-- check if anything changed since last time
|
|
-- if yes, apply changes and do gluon-reload
|
|
|
|
local has_changes = false
|
|
|
|
-- Use this when changing something that needs a reload and/or rollback (not the hostname)
|
|
local function set(a, b, c, d, isbool)
|
|
local curval
|
|
|
|
if isbool then
|
|
curval = uci:get_bool(a, b, c)
|
|
else
|
|
curval = uci:get(a, b, c)
|
|
end
|
|
|
|
if curval ~= d then
|
|
uci:set(a, b, c, d)
|
|
print(' Value', a, b, c, 'changed to', d, 'was', curval)
|
|
has_changes = true
|
|
else
|
|
print(' Value', a, b, c, 'unchanged', d)
|
|
end
|
|
end
|
|
|
|
local has_tunnel = false
|
|
|
|
for _, net in ipairs(node.interfaces) do
|
|
local net_name = net.name
|
|
if net_name == 'tunnel' or net_name == 'vpn' or net_name == 'mesh_vpn' then
|
|
has_tunnel = true
|
|
end
|
|
local net_mapped = mappings[net_name] or net_name
|
|
if not string.find(net_mapped, '_') then
|
|
net_mapped = 'mesh_' .. net_mapped
|
|
end
|
|
|
|
local cidr = ip.new(net.ip, net.netmask):string()
|
|
|
|
print('Syncing ' .. net_name .. ' as ' .. net_mapped .. ' to ' .. cidr)
|
|
set('gluon-static-ip', net_mapped, 'ip4', cidr)
|
|
end
|
|
|
|
print('Syncing mesh vpn: ' .. (has_tunnel and 'on' or 'off'))
|
|
set('gluon', 'mesh_vpn', 'enabled', has_tunnel, true)
|
|
|
|
uci:set('gluon-manman-sync', 'sync', 'last_data', json.stringify(location))
|
|
|
|
uci:save('system')
|
|
uci:save('gluon')
|
|
uci:save('gluon-manman-sync')
|
|
uci:save('gluon-static-ip')
|
|
uci:save('gluon-node-info')
|
|
os.execute('exec uci commit')
|
|
|
|
if has_changes then
|
|
print('Applying changes...')
|
|
os.execute('exec gluon-reconfigure')
|
|
os.execute('exec gluon-reload')
|
|
else
|
|
print('No settings changes, no reason to reload')
|
|
end
|
|
end
|
|
|
|
local function maybe_sync()
|
|
if uci:get_bool('gluon-manman-sync', 'sync', 'enabled') then
|
|
do_manman_sync()
|
|
else
|
|
print('manman-sync not enabled, skipping')
|
|
end
|
|
end
|
|
|
|
local function enable_sync(location, node)
|
|
if not uci:get('gluon-manman-sync', 'sync', 'location') and not uci:get('gluon-manman-sync', 'sync', 'location_id') and not location then
|
|
cmd_err('requires at least a location (No location was found in config)', true)
|
|
end
|
|
|
|
if location then
|
|
printf('Config location %s', location)
|
|
uci:set('gluon-manman-sync', 'sync', 'location', location)
|
|
-- clear ID, gets fetched from manman-sync
|
|
uci:set('gluon-manman-sync', 'sync', 'location_id', nil)
|
|
end
|
|
|
|
if node then
|
|
printf('Config node %s', node)
|
|
uci:set('gluon-manman-sync', 'sync', 'node', node)
|
|
-- clear ID, gets fetched from manman-sync
|
|
uci:set('gluon-manman-sync', 'sync', 'node_id', nil)
|
|
end
|
|
|
|
if not uci:get_bool('gluon-manman-sync', 'sync', 'enabled') then
|
|
uci:set('gluon-manman-sync', 'sync', 'enabled', true)
|
|
print('Enabled sync.')
|
|
print('Trigger with manman-sync sync')
|
|
else
|
|
print('Sync already enabled.')
|
|
end
|
|
|
|
uci:save('gluon-manman-sync')
|
|
end
|
|
|
|
local function disable_sync()
|
|
if uci:get_bool('gluon-manman-sync', 'sync', 'enabled') then
|
|
uci:set('gluon-manman-sync', 'sync', 'enabled', false)
|
|
print('Disabled sync.')
|
|
uci:save('gluon-manman-sync')
|
|
end
|
|
end
|
|
|
|
local function show_info()
|
|
tprint({
|
|
enabled = uci:get_bool('gluon-manman-sync', 'sync', 'enabled'),
|
|
location = string.format('%s (id %s)',
|
|
uci:get('gluon-manman-sync', 'sync', 'location') or '[none]',
|
|
uci:get('gluon-manman-sync', 'sync', 'location_id') or '[will autodetect]'
|
|
),
|
|
node = string.format('%s (id %s)',
|
|
uci:get('gluon-manman-sync', 'sync', 'node') or '[none - use only available or error]',
|
|
uci:get('gluon-manman-sync', 'sync', 'node_id') or '[will autodetect]'
|
|
),
|
|
})
|
|
end
|
|
|
|
sub = {
|
|
info = { show_info, 'Show manman-sync configuration' },
|
|
help = { show_help, 'Show help' },
|
|
sync = { maybe_sync, 'Trigger a manman-sync' },
|
|
force_sync = { do_manman_sync, 'Trigger a manman-sync, despite being disabled' },
|
|
enable = { enable_sync, 'Enable sync. [<location>] [<node>]' },
|
|
disable = { disable_sync, 'Disable sync.' }
|
|
}
|
|
|
|
exec_cmd(table.slice(arg), sub)
|