-- vim: ft=terra
-- provides the functionality of the `parsav` utility that controls `parsavd`
local pstr = lib.mem.ptr(int8)
local ctloptions = lib.cmdparse({
version = {'V', 'display information about the binary build and exit'};
verbose = {'v', 'increase logging verbosity', inc=1};
quiet = {'q', 'do not print to standard out'};
help = {'h', 'display this list'};
backend_file = {'B', 'init from specified backend file', consume=1};
backend = {'b', 'operate on only the selected backend'};
instance_id = {'i', 'specify the instance to control by name', consume=1};
instance_serial = {'I', 'specify the instance to control by serial', consume=1};
all = {'A', 'affect all running instances/backends'};
}, { subcmd = 1 })
local pbasic = lib.cmdparse {
help = {'h', 'display this list'}
}
local subcmds = {
}
local ctlcmds = {
{ 'start', 'start a new instance of the server' };
{ 'stop', 'stop a running instance' };
{ 'ls', 'list all running instances' };
{ 'attach', 'capture log output from a running instance' };
{ 'db', 'set up and manage the database' };
{ 'user', 'manage users, privileges, and credentials'};
{ 'mkroot <handle>', 'establish a new root user with the given handle' };
{ 'actor <xid> purge-all', 'remove all traces of a user from the database (except local user credentials -- use \27[1mauth all purge\27[m to prevent a user from accessing the instance)' };
{ 'actor <xid> create', 'instantiate a new actor' };
{ 'actor <xid> bestow <epithet>', 'bestow an epithet upon an actor' };
{ 'conf', 'manage the server configuration'};
{ 'serv dl', 'initiate an update cycle over foreign actors' };
{ 'tl', 'print the current local timeline to standard out' };
{ 'be pgsql setup-auth (managed|unmanaged)', '(PGSQL backends) select the authentication strategy to use' };
}
local cmdhelp = function(tbl)
local str = '\ncommands:\n'
for _, v in ipairs(tbl) do
str = str .. string.format (
' \27[1m%s\27[m: %s\n',
v[1]
:gsub('([%(%)|%[%]])', '\27[34m%1\27[;1m')
:gsub('(<.->)','\27[36m%1\27[;1m'),
v[2]
)
end
return str
end
local struct idelegate {
emperor: &lib.ipc.emperor
all: bool
src: &lib.store.source
srv: &lib.srv.overlord
sid: uint64
iname: rawstring
}
idelegate.metamethods.__methodmissing = macro(function(meth, self, ...)
local expr = {...}
local rt
for _,f in pairs(lib.store.backend.entries) do
local fn = f.field or f[1]
local ft = f.type or f[2]
if fn == meth then rt = ft.type.returntype break end
end
return quote
var r: rt
if self.all or (self.srv ~= nil and self.srv.sources.ct == 1)
then r=self.srv:[meth]([expr])
elseif self.src ~= nil then r=self.src:[meth]([expr])
else lib.bail('no data source specified')
end
in r end
end)
terra idelegate:ipc_send(cmd: lib.ipc.cmd.t, operand: uint64)
var emp = self.emperor
var acks: lib.mem.ptr(lib.ipc.ack)
if self.sid == 0 and self.iname == nil then
if not self.all and emp:countpeers() > 1 then
lib.bail('either specify the instance to control or pass --all to control all instances')
end
acks = emp:mallack()
emp:decree(0,nil, cmd, operand, &acks(0)) -- TODO targeting
else
acks = lib.mem.heapa(lib.ipc.ack, 1)
if not emp:decree(self.sid, self.iname, cmd, operand, &acks(0)) then
acks:free()
end
end
return acks
end
local terra gensec(sdest: rawstring)
var dest = [&uint8](sdest)
lib.crypt.spray(dest,64)
for i=0,64 do dest[i] = dest[i] % (0x7e - 0x20) + 0x20 end
dest[64] = 0
end
local terra pwset(dlg: idelegate, buf: &(int8[33]), uid: uint64, reset: bool)
lib.dbg('generating temporary password')
var tmppw = [&uint8](&(buf[0]))
lib.crypt.spray(tmppw,32) tmppw[32] = 0
for i=0,32 do
tmppw[i] = tmppw[i] % (10 + 26*2)
if tmppw[i] >= 36 then
tmppw[i] = tmppw[i] + (0x61 - 36)
elseif tmppw[i] >= 10 then
tmppw[i] = tmppw[i] + (0x41 - 10)
else tmppw[i] = tmppw[i] + 0x30 end
end
lib.dbg('assigning temporary password')
dlg:auth_create_pw(uid, reset, pstr {
ptr = [rawstring](tmppw), ct = 32
})
end
local terra ipc_report(acks: lib.mem.ptr(lib.ipc.ack), rep: rawstring)
var decbuf: int8[21]
for i=0,acks.ct do
var num = lib.math.decstr(acks(i).clid, &decbuf[20])
if acks(i).success then
lib.report('instance #',num,' reports successful ',rep)
else
lib.report('instance #',num,' reports failed ',rep)
end
end
end
local emp = lib.ipc.global_emperor
local terra entry_mgtool(argc: int, argv: &rawstring): int
if argc < 1 then lib.bail('bad invocation!') end
lib.noise.init(2)
[lib.init]
var srv: lib.srv.overlord
var dlg = idelegate {
emperor = &emp, srv = &srv;
src = nil, sid = 0, iname = nil, all = false;
}
var mode: ctloptions
mode:parse(argc,argv) defer mode:free()
if mode.version then version() return 0 end
if mode.help then
[ lib.emit(false, 1, 'usage: ', `argv[0], ' ', ctloptions.helptxt.flags, ' <cmd> [<args>…]', ctloptions.helptxt.opts, cmdhelp(ctlcmds)) ]
return 0
end
if mode.quiet then lib.noise.level = 0 end
var cnf: rawstring
if mode.backend_file ~= nil
then cnf = @mode.backend_file
else cnf = lib.proc.getenv('parsav_backend_file')
end
if cnf == nil then cnf = [config.prefix_conf .. "/backend.conf"] end
if mode.all then dlg.all = true else
-- iterate through and pick the right backend, if one is indicated
end
if mode.instance_id ~= nil and mode.instance_serial ~= nil then
lib.bail('conflicting flags passed')
end
if mode.instance_id ~= nil then
dlg.iname = @mode.instance_id
elseif mode.instance_serial ~= nil then
-- decode serial
end
if mode.arglist.ct == 0 then lib.bail('no command') return 1 end
if lib.str.cmp(mode.arglist(0),'start') ~= 0 then
-- hack to save us some pain around forking
emp = lib.ipc.emperor.mk(false)
end
defer emp:release()
if lib.str.cmp(mode.arglist(0),'attach') == 0 then
elseif lib.str.cmp(mode.arglist(0),'start') == 0 then
var smode: lib.cmdparse {
help = {'h', 'display this list'};
log = {'l', 'send server\'s logging output to a file', consume=1};
pid = {'p', 'report PID of launched process'};
instance_serial = {'s', 'report the instance serial of the launched process'};
keep_attached = {'k', 'don\'t detach from the calling terminal until a successful startup (or ever, if passed twice)', inc=1};
}
smode:parse(mode.arglist.ct,mode.arglist.ptr)
if smode.help then
[ lib.emit(false, 1, 'usage: ', `argv[0], ' start ', smode.type.helptxt.flags, ' [<instance args>…] [-- <instance flags>…]', smode.type.helptxt.opts) ]
return 1
end
var lsr = lib.ipc.listener.mk()
var chpid = lib.proc.fork()
if chpid == 0 then
lsr:release()
var chargv = lib.mem.heapa(rawstring, smode.arglist.ct + 2)
chargv(0) = '-'
for i = 1, chargv.ct - 1 do
chargv(i) = smode.arglist(i-1)
end
chargv(chargv.ct-1) = nil
if smode.keep_attached == 0 then
lib.io.close(0) lib.io.close(1) lib.io.close(2)
end
lib.proc.exec([config.prefix_bin .. '/parsavd'], chargv.ptr)
lib.proc.execp([config.prefix_bin .. '/parsavd'], chargv.ptr)
lib.ipc.notify_parent(lib.ipc.signals.state_fail_find)
lib.bail('cannot find parsav program')
-- chargv:free()
else
lib.report('starting parsav daemon')
while true do
var sig = lsr:block()
if sig.system then lib.dbg('got system signal') end
if sig.from == chpid then
if sig.system and sig.sig == lib.ipc.signals.sys_child then
lib.warn('parsavd failed to start')
return 0
elseif sig.sig == lib.ipc.signals.notify_state_change and
sig.event == lib.ipc.signals.state_success then
lib.report('parsavd successfully started')
return 0
elseif sig.sig == lib.ipc.signals.notify_state_change and
sig.event == lib.ipc.signals.state_fail_find then
lib.bail('parsavd could not be found')
else lib.warn('got unrecognized signal, ignoring')
end
end
end
lsr:release() -- just because i feel distinctly uncomfortable leaving it out
end
elseif lib.str.cmp(mode.arglist(0),'stop') == 0 then
if mode.arglist.ct ~= 1 then goto cmderr end
var acks = dlg:ipc_send(lib.ipc.cmd.stop, 0)
if acks:ref() then
ipc_report(acks, 'step-down')
acks:free()
end
elseif lib.str.cmp(mode.arglist(0),'ls') == 0 then
if mode.arglist.ct ~= 1 then goto cmderr end
if dlg.sid == 0 and dlg.iname == nil then dlg.all = true end
var acks = dlg:ipc_send(lib.ipc.cmd.enumerate, 0)
if acks:ref() then
for i=0,acks.ct do
var decbuf: int8[21]
var num = lib.math.decstr(acks(i).clid, &decbuf[20])
[ lib.emit(true,1, '\27[1m(', `num, ')\27[m ',`&acks(i).iname[0],' \27[1;32mactive\27[m') ]
end
acks:free()
else lib.bail('no active instances') end
else
if lib.str.cmp(mode.arglist(0),'db') == 0 then
var dbmode: pbasic dbmode:parse(mode.arglist.ct, &mode.arglist(0))
if dbmode.help then
[ lib.emit(false, 1, 'usage: ', `argv[0], ' db ', dbmode.type.helptxt.flags, ' <cmd> [<args>…]', dbmode.type.helptxt.opts, cmdhelp {
{ 'db init <domain>', 'initialize backend databases (or a single specified database) with the necessary schema and structures for the given FQDN' };
{ 'db vacuum', 'delete old remote content from the database' };
{ 'db extract (<artifact>|<post>/<attachment number>)', 'extracts an attachment artifact from the database and prints it to standard out' };
{ 'db excise (<artifact>|<post>/<attachment number>)', 'removes an undesirable artifact from the database' };
{ 'db obliterate [<confirmation code>]', 'completely purge all parsav-related content and structure from the database, destroying all user content (requires confirmation)' };
{ 'db insert', 'reads a file from standard in and inserts it into the attachment database, printing the resulting ID' };
}) ]
return 1
end
if dbmode.arglist.ct < 1 then goto cmderr end
srv:setup(cnf)
if lib.str.cmp(dbmode.arglist(0),'init') == 0 and dbmode.arglist.ct == 2 then
lib.report('initializing new database structure for domain ', dbmode.arglist(1))
if dlg:dbsetup() then
srv:conprep(lib.store.prepmode.conf)
dlg:conf_set('instance-name', dbmode.arglist(1))
do var sec: int8[65] gensec(&sec[0])
dlg:conf_set('server-secret', &sec[0])
end
lib.report('database setup complete; use mkroot to create an administrative user')
else lib.bail('initialization process interrupted') end
elseif lib.str.cmp(dbmode.arglist(0),'obliterate') == 0 then
var confirmstrs = array(
'alpha', 'beta', 'gamma', 'delta', 'epsilon', 'eta', 'nu', 'kappa',
'emerald', 'carnelian', 'sapphire', 'ruby', 'amethyst'
)
var cfmstr: int8[64] cfmstr[0] = 0
var tdx = lib.osclock.time(nil) / 60
for i=0,3 do
if i ~= 0 then lib.str.cat(&cfmstr[0], '-') end
lib.str.cat(&cfmstr[0], confirmstrs[(tdx ^ (173*i)) % [confirmstrs.type.N]])
end
if dbmode.arglist.ct == 1 then
lib.bail('you are attempting to completely obliterate all data! make sure you have selected your target correctly. if you really want to do this, pass the confirmation string ', &cfmstr[0])
elseif dbmode.arglist.ct == 2 then
if lib.str.cmp(dbmode.arglist(1), cfmstr) == 0 then
lib.warn('completely obliterating all data!')
dlg:obliterate_everything()
elseif lib.str.cmp(dbmode.arglist(1), 'print-confirmation-string') == 0 then
lib.io.send(1, cfmstr, lib.str.sz(cfmstr))
else
lib.bail('you passed an incorrect confirmation string; pass ', &cfmstr[0], ' if you really want to destroy everything')
end
else goto cmderr end
else goto cmderr end
elseif lib.str.cmp(mode.arglist(0),'be') == 0 then
srv:setup(cnf)
elseif lib.str.cmp(mode.arglist(0),'conf') == 0 then
srv:setup(cnf)
srv:conprep(lib.store.prepmode.conf)
var cfmode: lib.cmdparse {
help = {'h','display this list'};
no_notify = {'n', "don't instruct the server to refresh its configuration cache after making changes; useful for \"transactional\" configuration changes."};
}
cfmode:parse(mode.arglist.ct, &mode.arglist(0))
if cfmode.help then
[ lib.emit(false, 1, 'usage: ', `argv[0], ' conf ', cfmode.type.helptxt.flags, ' <cmd> [<args>…]', cfmode.type.helptxt.opts, cmdhelp {
{ 'conf set <setting> <value>', 'add or a change a server configuration parameter to the database' };
{ 'conf get <setting>', 'report the value of a server setting' };
{ 'conf reset <setting>', 'reset a server setting to its default value' };
{ 'conf refresh', 'instruct an instance to refresh its configuration cache' };
{ 'conf chsec', 'reset the server secret, invalidating all authentication cookies' };
}) ]
return 1
end
if cfmode.arglist.ct < 1 then goto cmderr end
if cfmode.arglist.ct == 1 then
if lib.str.cmp(cfmode.arglist(0),'chsec') == 0 then
var sec: int8[65] gensec(&sec[0])
dlg:conf_set('server-secret', &sec[0])
lib.report('server secret reset')
elseif lib.str.cmp(cfmode.arglist(0),'refresh') == 0 then
cfmode.no_notify = false -- duh
else goto cmderr end
elseif cfmode.arglist.ct == 3 and
lib.str.cmp(cfmode.arglist(0),'set') == 0 then
dlg:conf_set(cfmode.arglist(1),cfmode.arglist(2))
lib.report('parameter set')
else goto cmderr end
-- successful commands fall through
if not cfmode.no_notify then
dlg:ipc_send(lib.ipc.cmd.cfgrefresh,0)
end
else
srv:setup(cnf)
srv:conprep(lib.store.prepmode.full)
if lib.str.cmp(mode.arglist(0),'mkroot') == 0 then
var cfmode: pbasic cfmode:parse(mode.arglist.ct, &mode.arglist(0))
if cfmode.help then
[ lib.emit(false, 1, 'usage: ', `argv[0], ' mkroot ', cfmode.type.helptxt.flags, ' <handle>', cfmode.type.helptxt.opts) ]
return 1
end
if cfmode.arglist.ct == 1 then
var am = dlg:conf_get('credential-store')
var mg: bool
if (not am) or am:cmp(lib.str.plit 'managed') then
mg = true
elseif am:cmp(lib.str.plit 'unmanaged') then
lib.warn('credential store is unmanaged; you will need to create credentials for the new root user manually!')
mg = false
else lib.bail('unknown credential store mode "',{am.ptr,am.ct},'"; should be either "managed" or "unmanaged"') end
var kbuf: uint8[lib.crypt.const.maxdersz]
var root = lib.store.actor.mk(&kbuf[0])
root.handle = cfmode.arglist(0)
var epithets = array(
'root', 'god', 'regional jehovah', 'titan king',
'king of olympus', 'cyberpharaoh', 'electric ellimist',
"rampaging c'tan", 'deathless tweetlord', 'postmaster',
'faerie queene', 'lord of the posts', 'ruthless cybercrat',
'general secretary', 'commissar', 'kwisatz haderach',
'dedicated hyperturing'
-- feel free to add more
)
root.epithet = epithets[lib.crypt.random(intptr,0,[epithets.type.N])]
root.rights.powers:fill() -- grant omnipotence
root.rights.rank = 1
var ruid = dlg:actor_create(&root)
dlg:conf_set('master',root.handle)
lib.report('created new administrator')
if mg then
var tmppw: int8[33]
pwset(dlg, &tmppw, ruid, false)
lib.report('temporary root pw: ', {&tmppw[0], 32})
end
else goto cmderr end
elseif lib.str.cmp(mode.arglist(0),'user') == 0 then
var umode: pbasic umode:parse(mode.arglist.ct, &mode.arglist(0))
if umode.help then
[ lib.emit(false, 1, 'usage: ', `argv[0], ' user ', umode.type.helptxt.flags, ' <handle> <cmd> [<args>…]', umode.type.helptxt.opts, cmdhelp {
{ 'user <handle> auth <type> new', '(where applicable, managed auth only) create a new authentication token of the given type for a user' };
{ 'user <handle> auth <type> reset', '(where applicable, managed auth only) delete all of a user\'s authentication tokens of the given type and issue a new one' };
{ 'user <handle> auth (<type>|all) purge', 'delete all credentials that would allow this user to log in (where possible)' };
{ 'user <handle> (grant|revoke) (<priv>|all)', 'grant or revoke a specific power to or from a user' };
{ 'user <handle> emasculate', 'strip all administrative powers from a user' };
{ 'user <handle> forgive', 'restore all default powers to a user' };
{ 'user <handle> suspend [<timespec>]', '(e.g. \27[1muser jokester suspend 5d 6h 7m 3s\27[m to suspend "jokester" for five days, six hours, seven minutes, and three seconds) suspend a user'};
}) ]
return 1
end
if umode.arglist.ct >= 3 then
var grant = lib.str.cmp(umode.arglist(1),'grant') == 0
var handle = umode.arglist(0)
var usr = dlg:actor_fetch_xid(pstr {ptr=handle, ct=lib.str.sz(handle)})
if grant or lib.str.cmp(umode.arglist(1),'revoke') == 0 then
if not usr then lib.bail('unknown handle') end
var newprivs = usr.ptr.rights.powers
var map = array([lib.store.privmap])
if umode.arglist.ct == 3 and lib.str.cmp(umode.arglist(2),'all') == 0 then
if grant
then newprivs:fill()
else newprivs:clear()
end
else
for i=2,umode.arglist.ct do
var priv = umode.arglist(i)
for j=0,[map.type.N] do
var p = map[j]
if p.name:cmp_raw(priv) then
if grant then
lib.dbg('enabling power ', {p.name.ptr,p.name.ct})
newprivs = newprivs + p.priv
else
lib.dbg('disabling power ', {p.name.ptr,p.name.ct})
newprivs = newprivs - p.priv
end
break
end
end
end
end
usr.ptr.rights.powers = newprivs
dlg:actor_save_privs(usr.ptr)
elseif lib.str.cmp(umode.arglist(1),'auth') == 0 and umode.arglist.ct == 4 then
var reset = lib.str.cmp(umode.arglist(3),'reset') == 0
if reset or lib.str.cmp(umode.arglist(3),'new') == 0 then
-- FIXME enable resetting pws for users who have
-- not logged in yet
if not usr then lib.bail('unknown handle') end
if lib.str.cmp(umode.arglist(2),'pw') == 0 then
var tmppw: int8[33]
pwset(dlg, &tmppw, usr.ptr.id, reset)
lib.report('new temporary password for ',usr.ptr.handle,': ', {&tmppw[0], 32})
else lib.bail('unknown credential type') end
elseif lib.str.cmp(umode.arglist(3),'purge') == 0 then
var uid: uint64 = 0
if usr:ref() then uid = usr(0).id end
if lib.str.cmp(umode.arglist(2),'pw') == 0 then
dlg:auth_purge_pw(uid, handle)
elseif lib.str.cmp(umode.arglist(2),'otp') == 0 then
dlg:auth_purge_otp(uid, handle)
elseif lib.str.cmp(umode.arglist(2),'trust') == 0 then
dlg:auth_purge_trust(uid, handle)
else lib.bail('unknown credential type') end
else goto cmderr end
else goto cmderr end
else goto cmderr end
elseif lib.str.cmp(mode.arglist(0),'actor') == 0 then
elseif lib.str.cmp(mode.arglist(0),'tl') == 0 then
elseif lib.str.cmp(mode.arglist(0),'serv') == 0 then
else goto cmderr end
end
end
do return 0 end
::cmderr:: lib.bail('invalid command')
end
return entry_mgtool