Index: backend/pgsql.t ================================================================== --- backend/pgsql.t +++ backend/pgsql.t @@ -121,11 +121,11 @@ params = {}, sql = [[ select id, nym, handle, origin, bio, null::text, rank, quota, key, epithet, knownsince::bigint, '@' || handle, - invites + invites, avatarid from parsav_actors where origin is null order by nullif(rank,0) nulls last, handle ]]; }; @@ -504,10 +504,17 @@ params = {uint64, uint64}, cmd = true, sql = [[ delete from parsav_artifact_claims where uid = $1::bigint and rid = $2::bigint ]]; + }; + artifact_collect_garbage = { + params = {}, cmd = true, sql = [[ + delete from parsav_artifacts where + id not in (select rid from parsav_artifact_claims) and + content is not null -- avoid stepping on toes of ban mech + ]]; }; artifact_excise_forget = { -- delete the blasted thing and pretend it never existed params = {uint64}, cmd=true, sql = [[ delete from parsav_artifacts where id = $1::bigint @@ -948,10 +955,11 @@ a.ptr.rights = lib.store.rights_default(); a.ptr.rights.rank = r:int(uint16, row, 6); a.ptr.rights.quota = r:int(uint32, row, 7); a.ptr.rights.invites = r:int(uint32, row, 12); a.ptr.knownsince = r:int(int64,row, 10); + a.ptr.avatarid = r:int(uint64,row, 13); if r:null(row,8) then a.ptr.key.ct = 0 a.ptr.key.ptr = nil else a.ptr.key = r:bin(row,8) end @@ -1502,15 +1510,18 @@ var ret = lib.mem.heapa([lib.mem.ptr(lib.store.auth)], r.sz) for i=0, r.sz do var kind = r:_string(i, 1) var comment = r:_string(i, 2) var a = [ lib.str.encapsulate(lib.store.auth, { - kind = {`kind.ptr, `kind.ct}; - comment = {`comment.ptr, `comment.ct}; + kind = {`kind.ptr, `kind.ct+1}; + comment = {`comment.ptr, `comment.ct+1}; }) ] a.ptr.aid = r:int(uint64, i, 0) - a.ptr.netmask = r:cidr(i, 3) + if r:null(i,3) + then a.ptr.netmask.pv = 0 + else a.ptr.netmask = r:cidr(i, 3) + end a.ptr.blacklist = r:bool(i, 4) ret.ptr[i] = a end return ret end]; @@ -1597,10 +1608,19 @@ desc: pstring, folder: pstring ): {} queries.artifact_expropriate.exec(src,uid,artifact,desc,folder, lib.osclock.time(nil)) end]; + + artifact_disclaim = [terra( + src: &lib.store.source, + uid: uint64, + artifact: uint64 + ) + queries.artifact_disclaim.exec(src,uid,artifact) + queries.artifact_collect_garbage.exec(src) -- TODO add a config option to change GC strategies, instead of just always running a cycle after an artifact is disclaimed, which is not very efficient + end]; artifact_enum_uid = [terra( src: &lib.store.source, uid: uint64, folder: pstring Index: backend/schema/pgsql-views.sql ================================================================== --- backend/schema/pgsql-views.sql +++ backend/schema/pgsql-views.sql @@ -56,11 +56,12 @@ quota integer, key bytea, epithet text, knownsince bigint, xid text, - invites integer + invites integer, + avatarid bigint ); create or replace function pg_temp.parsavpg_translate_actor(parsav_actors) returns pg_temp.parsavpg_intern_actor as $$ @@ -69,11 +70,11 @@ ($1).avataruri, ($1).rank, ($1).quota, ($1).key, ($1).epithet, ($1).knownsince::bigint, coalesce(($1).handle || '@' || (select domain from parsav_servers as s where s.id = ($1).origin), '@' || ($1).handle) as xid, - ($1).invites + ($1).invites, ($1).avatarid $$ language sql; --drop type if exists pg_temp.parsavpg_intern_post; create type pg_temp.parsavpg_intern_post as ( -- order is crucially important, and must match the order used Index: parsav.md ================================================================== --- parsav.md +++ parsav.md @@ -23,13 +23,13 @@ * inkscape, for rendering out some of the UI graphics that can't be represented with standard svg * cwebp (libwebp package), for transforming inkscape PNGs to webp * sassc, for compiling the SCSS stylesheet into its final CSS -all builds require terra, which, unfortunately, requires installing an older version of llvm, v9 at the latest (which i develop parsav under). with any luck, your distro will be clever enough to package terra and its dependencies properly (it's trivial on nix, tho you'll need to tweak the terra expression to select a more recent llvm package); Arch Linux is one of those distros which is not so clever, and whose (AUR) terra package is totally broken. due to these unfortunate circumstances, terra is distributed not just in source form, but also in the the form of LLVM IR. distributions will also be made in the form of tarballed object code and assembly listings for various common platforms, currently including x86-32/64, arm7hf, aarch64, riscv, mips32/64, and ppc64/64le. +all builds require terra, which, unfortunately, requires installing an older version of llvm, v9 at the latest (which i develop parsav under). with any luck, your distro will be clever enough to package terra and its dependencies properly (it's trivial on nix, tho you'll need to tweak the terra expression to select a more recent llvm package); Arch Linux is one of those distros which is not so clever, and whose (AUR) terra package is totally broken. due to these unfortunate circumstances, terra is distributed not just in source form, but also in the the form of LLVM IR. -i've noticed that terra (at least with llvm9) seems to get a bit cantankerous and trigger llvm to fail with bizarre errors when you try to cross-compile parsav from x86-64 to any other platform, even x86-32. i don't know if this problem exists on other architectures or in what form, but as a workaround, the current cross-compile process consists of generating LLVM IR (ostensibly for x86-64, though this is in reality an architecture-independent language), and then compiling that down to an object file with llc. this is an enormous hassle; hopefully the terra (or llvm?) people will fix this eventually. +i've noticed that terra (at least with llvm9) seems to get a bit cantankerous and trigger llvm to fail with bizarre errors when you try to cross-compile parsav from x86-64 to any other platform, even x86-32. i don't know if this problem exists on other architectures or in what form. as a workaround, i've tried generating LLVM IR (ostensibly for x86-64, though this is in reality an architecture-independent language), and then compiling that down to an object file with llc. it doesn't work. the generated binaries seem to run but they crash with bizarre errors and are impossible to debug, as llc refuses to include debug symbols. for these reasons, parsav will (almost certainly) not run on any architecture besides x86-64, at least until terra and/or llvm are fixed. also note that, while parsav has a flag to build with ASAN, ASAN has proven unusable for most purposes as it routinely reports false positive buffer-heap-overflows. if you figure out how to defuckulate this, i will be overjoyed. ## building Index: parsav.t ================================================================== --- parsav.t +++ parsav.t @@ -41,27 +41,37 @@ return (tbl[false])(v,...) end) end; emit_unitary = function(nl,fd,...) local code = {} + local defs = {} for i,v in ipairs{...} do - if type(v) == 'string' or type(v) == 'number' then - local str = tostring(v) - code[#code+1] = `lib.io.send(2, str, [#str]) - elseif type(v) == 'table' and #v == 2 then - code[#code+1] = `lib.io.send(2, [v[1]], [v[2]]) - elseif v.tree:is 'constant' then - local str = tostring(v:asvalue()) - code[#code+1] = `lib.io.send(2, str, [#str]) + local str, ct + if type(v) == 'table' and v.tree and not (v.tree:is 'constant') then + if v.tree.type.convertible == 'tuple' then + str = `v._0 + ct = `v._1 + else + local n = symbol(v.tree.type) + defs[#defs + 1] = quote var [n] = v end + str = n + ct = `lib.str.sz(n) + end else - code[#code+1] = quote var n = v in - lib.io.send(2, n, lib.str.sz(n)) end + if type(v) == 'string' or type(v) == 'number' then + str = tostring(v) + else--if v.tree:is 'constant' then + str = tostring(v:asvalue()) + end + ct = ct or #str end + + code[#code+1] = `lib.io.send(fd, str, ct) end if nl == true then code[#code+1] = `lib.io.send(fd, '\n', 1) elseif nl then code[#code+1] = `lib.io.send(fd, nl, [#nl]) end - return code + return quote [defs] in [code] end end; emitv = function(nl,fd,...) local vec = {} local defs = {} for i,v in ipairs{...} do @@ -162,11 +172,10 @@ } if config.posix then lib.uio = terralib.includec 'sys/uio.h'; lib.emit = lib.emitv -- use more efficient call where available else lib.emit = lib.emit_unitary end - lib.noise = { level = global(uint8,1); starttime = global(lib.osclock.time_t); lasttime = global(lib.osclock.time_t); Index: render/conf.t ================================================================== --- render/conf.t +++ render/conf.t @@ -4,11 +4,11 @@ local mappings = { {url = 'profile', title = 'account profile', render = 'profile'}; {url = 'avi', title = 'avatar', render = 'avatar'}; {url = 'ui', title = 'user interface', render = 'ui'}; - {url = 'sec', title = 'security', render = 'sec'}; + {url = 'sec', title = 'security', render = 'sec_overlay'}; {url = 'rel', title = 'relationships', render = 'rel'}; {url = 'qnt', title = 'quarantine', render = 'quarantine'}; {url = 'acl', title = 'access control shortcuts', render = 'acl'}; {url = 'rooms', title = 'chatrooms', render = 'rooms'}; {url = 'circles', title = 'circles', render = 'circles'}; @@ -31,13 +31,19 @@ if lib.render.conf[m.render] then invoker = quote if path(1):cmp(lib.str.lit([m.url])) then var body = [lib.render.conf[m.render]] (co, path) var a: lib.str.acc a:init(body.ct+48) - a:lpush(['

' .. m.title .. '

']):ppush(body) - panel = a:finalize() - body:free() + if not body then + a:lpush(['

' .. m.title .. ' :: error

' .. + '

the requested resource is not available.

']) + panel = a:finalize() + else + a:lpush(['

' .. m.title .. '

']):ppush(body) + panel = a:finalize() + body:free() + end else [invoker] end end end end Index: render/conf/sec.t ================================================================== --- render/conf/sec.t +++ render/conf/sec.t @@ -1,25 +1,69 @@ -- vim: ft=terra local pstr = lib.mem.ptr(int8) local pref = lib.mem.ref(int8) + local terra -render_conf_sec(co: &lib.srv.convo, path: lib.mem.ptr(pref)): pstr - var time: lib.store.timepoint = co.who.source:auth_sigtime_user_fetch(co.who.id) +render_conf_sec(co: &lib.srv.convo, uid: uint64): pstr + var time: lib.store.timepoint = co.who.source:auth_sigtime_user_fetch(uid) var tstr: int8[26] lib.osclock.ctime_r(&time, &tstr[0]) var body = data.view.conf_sec { lastreset = pstr { ptr = &tstr[0], ct = lib.str.sz(&tstr[0]) } } if co.srv.cfg.credmgd then + var new = co:pgetv('new') var a: lib.str.acc a:init(768) - body:append(&a) - var credmgr = data.view.conf_sec_credmg { - credlist = '' - } - credmgr:append(&a) + if not new then + body:append(&a) + var credmgr = data.view.conf_sec_credmg { + credlist = pstr{'',0}; + } + var creds = co.srv:auth_enum_uid(uid) + if creds.ct > 0 then defer creds:free() + var cl: lib.str.acc cl:init(256) + for i=0, creds.ct do var c = creds(i).ptr + if not c.blacklist then + cl:lpush('') + end + end + credmgr.credlist = cl:finalize() + end + credmgr:append(&a) + if credmgr.credlist.ct > 0 then credmgr.credlist:free() end + elseif new:cmp(lib.str.plit'pw') then + var d: data.view.conf_sec_pwnew + var time = lib.osclock.time(nil) + var timestr: int8[26] lib.osclock.ctime_r(&time, ×tr[0]) + var cmt: lib.str.acc + cmt:init(48):lpush('enrolled over http on '):push(×tr[0],0) + d.comment = cmt:finalize() + + var st = d:tostr() + d.comment:free() + return st + elseif new:cmp(lib.str.plit'challenge') then + -- we're going to break the rules a bit and do database munging from + -- the rendering code, because doing otherwise in this case would be + -- genuinely nightmarish + elseif new:cmp(lib.str.plit'otp') then + elseif new:cmp(lib.str.plit'api') then + else return pstr.null() end return a:finalize() else return body:tostr() end end + +terra lib.render.conf.sec_overlay +(co: &lib.srv.convo, path: lib.mem.ptr(pref)): pstr + -- render the credential panel for the current user, allowing + -- it to be reused in the administration UI + return render_conf_sec(co,co.who.id) +end + return render_conf_sec Index: render/conf/users.t ================================================================== --- render/conf/users.t +++ render/conf/users.t @@ -105,10 +105,15 @@ end if xXx then a:lpush('_xXx') end end + +local terra +suggest_domain(a: &lib.str.acc) + var tlds = array('tld','club','town','space','xxx') +end local push_num_field = macro(function(acc,name,lbl,min,max,value,disable) name = name:asvalue() lbl = lbl:asvalue() local start = '
' @@ -156,114 +161,139 @@ local push_radio = input_pusher('radio',false,true) local mode_local, mode_remote, mode_staff, mode_peers, mode_peons, mode_all = 0,1,2,3,4,5 local terra render_conf_users(co: &lib.srv.convo, path: lib.mem.ptr(pref)): pstr - if path.ct == 3 then + if path.ct >= 3 then var uid, ok = lib.math.shorthand.parse(path(2).ptr,path(2).ct) if not ok then goto e404 end var user = co.srv:actor_fetch_uid(uid) -- FIXME allow xids as well, for manual queries if not user then goto e404 end defer user:free() if not co.who:overpowers(user.ptr) then goto e403 end - var cinp: lib.str.acc cinp:init(256) - var clnk: lib.str.acc clnk:init(512) - cinp:lpush('
') - if user.ptr.rights.rank > 0 and (co.who.rights.powers.elevate() or co.who.rights.powers.demote()) then - var max = co.who.rights.rank - if not co.who.rights.powers.elevate() then max = user.ptr.rights.rank end - var min = co.srv.cfg.nranks - if not co.who.rights.powers.demote() then min = user.ptr.rights.rank end - - push_num_field(cinp, 'rank', 'rank', max, min, user.ptr.rights.rank, user.ptr.id == co.who.id) - end - if co.who.rights.powers.herald() then - var sanitized: pstr - if user.ptr.epithet == nil - then sanitized = pstr {ptr='', ct=0} - else sanitized = lib.html.sanitize(cs(user.ptr.epithet),true) + if path.ct == 4 then + if path(3):cmp(lib.str.lit'cred') then + var pg: lib.str.acc pg:init(1024) + pg:lpush('
editing credentials for user '):push(user(0).xid,0):lpush('
') + var credmgr = lib.render.conf.sec(co, uid) + pg:ppush(credmgr) + credmgr:free() + return pg:finalize() + else goto e404 end + elseif path.ct == 3 then + var cinp: lib.str.acc cinp:init(256) + cinp:lpush('
') + if user.ptr.rights.rank > 0 and (co.who.rights.powers.elevate() or co.who.rights.powers.demote()) then + var max = co.who.rights.rank + if not co.who.rights.powers.elevate() then max = user.ptr.rights.rank end + var min = co.srv.cfg.nranks + if not co.who.rights.powers.demote() then min = user.ptr.rights.rank end + + push_num_field(cinp, 'rank', 'rank', max, min, user.ptr.rights.rank, user.ptr.id == co.who.id) + end + if co.who.rights.powers.herald() then + var sanitized: pstr + if user.ptr.epithet == nil + then sanitized = pstr {ptr='', ct=0} + else sanitized = lib.html.sanitize(cs(user.ptr.epithet),true) + end + cinp:lpush('
') + if user.ptr.epithet ~= nil then sanitized:free() end + end + if co.who.rights.powers.invite() or co.who.rights.powers.discipline() then + var min: uint32 = 0 + if not (co.who.rights.powers.discipline() or + co.who.rights.powers.demote() and co.who.rights.powers.invite()) + then min = user.ptr.rights.invites end + var max: uint32 = co.srv.cfg.maxinvites + if not co.who.rights.powers.invite() then max = user.ptr.rights.invites end + + push_num_field(cinp, 'invites', 'invites', min, max, user.ptr.rights.invites, false) + end + if co.who.rights.powers.elevate() or co.who.rights.powers.demote() then + var max: uint32 = 5000 + if not co.who.rights.powers.elevate() then max = user.ptr.rights.quota end + var min: uint32 = 0 + if not co.who.rights.powers.demote() then min = user.ptr.rights.quota end + + push_num_field(cinp, 'quota', 'quota', min, max, user.ptr.rights.quota, user.ptr.id == co.who.id and co.who.rights.rank ~= 1) end - cinp:lpush('
') - if user.ptr.epithet ~= nil then sanitized:free() end - end - if co.who.rights.powers.invite() or co.who.rights.powers.discipline() then - var min: uint32 = 0 - if not (co.who.rights.powers.discipline() or - co.who.rights.powers.demote() and co.who.rights.powers.invite()) - then min = user.ptr.rights.invites end - var max: uint32 = co.srv.cfg.maxinvites - if not co.who.rights.powers.invite() then max = user.ptr.rights.invites end + cinp:lpush('
') + + if user.ptr.id ~= co.who.id and + ((user.ptr.rights.rank == 0 and co.who.rights.powers.elevate()) or + (user.ptr.rights.rank > 0 and co.who.rights.powers.demote())) then + push_checkbox(&cinp, 'staff', pstr.null(), 'site staff member', user.ptr.rights.rank > 0, true, pstr.null()) + end - push_num_field(cinp, 'invites', 'invites', min, max, user.ptr.rights.invites, false) - end - if co.who.rights.powers.elevate() or co.who.rights.powers.demote() then - var max: uint32 = 5000 - if not co.who.rights.powers.elevate() then max = user.ptr.rights.quota end - var min: uint32 = 0 - if not co.who.rights.powers.demote() then min = user.ptr.rights.quota end + cinp:lpush('
') - push_num_field(cinp, 'quota', 'quota', min, max, user.ptr.rights.quota, user.ptr.id == co.who.id and co.who.rights.rank ~= 1) - end - cinp:lpush('
') - - if user.ptr.id ~= co.who.id and - ((user.ptr.rights.rank == 0 and co.who.rights.powers.elevate()) or - (user.ptr.rights.rank > 0 and co.who.rights.powers.demote())) then - push_checkbox(&cinp, 'staff', pstr.null(), 'site staff member', user.ptr.rights.rank > 0, true, pstr.null()) - end - - cinp:lpush('
') - - if (co.who.rights.powers.elevate() or - co.who.rights.powers.demote()) and user.ptr.id ~= co.who.id then - var map = array([lib.store.privmap]) - cinp:lpush('
powers
') - for i=0, [map.type.N] do - if (co.who.rights.powers and map[i].priv):sz() > 0 then - var on = (user.ptr.rights.powers and map[i].priv):sz() > 0 - var enabled = ( on and co.who.rights.powers.demote() ) or - ((not on) and co.who.rights.powers.elevate()) - var namea: lib.str.acc namea:compose('power-', map[i].name) - var name = namea:finalize() - push_pickbox(&cinp, name, pstr.null(), map[i].name, on, enabled, pstr.null()) - name:free() + if (co.who.rights.powers.elevate() or + co.who.rights.powers.demote()) and user.ptr.id ~= co.who.id then + var map = array([lib.store.privmap]) + cinp:lpush('
powers
') + for i=0, [map.type.N] do + if (co.who.rights.powers and map[i].priv):sz() > 0 then + var on = (user.ptr.rights.powers and map[i].priv):sz() > 0 + var enabled = ( on and co.who.rights.powers.demote() ) or + ((not on) and co.who.rights.powers.elevate()) + var namea: lib.str.acc namea:compose('power-', map[i].name) + var name = namea:finalize() + push_pickbox(&cinp, name, pstr.null(), map[i].name, on, enabled, pstr.null()) + name:free() + end end + cinp:lpush('
') + end + + if co.who.id ~= uid and co.who.rights.powers.purge() then + var purgeconf: lib.str.acc purgeconf:init(48) + var purgestrs = array( + 'alpha', 'beta', 'gamma', 'delta', 'epsilon', 'eta', 'nu', 'kappa', + 'emerald', 'carnelian', 'sapphire', 'ruby', 'amethyst', 'glory', + 'hope', 'grace', 'pearl', 'carnation', 'rose', 'peony', 'poppy' + ) + for i=0,3 do + purgeconf:push(purgestrs[lib.crypt.random(intptr,0,[purgestrs.type.N])],0) + if i ~= 2 then purgeconf:lpush('-') end end - cinp:lpush('
') - end + cinp:lpush('
purge account

you have the authority to destroy this account and all its associated content irreversibly and irretrievably. if you really wish to apply such an extreme sanction, enter the confirmation string '):push(purgeconf.buf,purgeconf.sz):lpush(' below and press the “alter” button to begin the process.

') + purgeconf:free() + end + + -- TODO black mark system? e.g. resolution option for badthink reports + -- adds a black mark to the offending user; they can be automatically banned + -- or brought up for review after a certain number of offenses; possibly lower + -- set of default privs for marked users - -- TODO black mark system? e.g. resolution option for badthink reports - -- adds a black mark to the offending user; they can be automatically banned - -- or brought up for review after a certain number of offenses; possibly lower - -- set of default privs for marked users + var cinpp = cinp:finalize() defer cinpp:free() + var unym: lib.str.acc unym:init(64) + unym:lpush('') + lib.render.nym(user.ptr,0,&unym,false) + unym:lpush('') + var ctlbox = data.view.conf_user_ctl { + name = unym:finalize(); + inputcontent = cinpp; + btns = pstr{'',0}; + } + if co.who.id ~= uid and co.who.rights.powers.cred() then + ctlbox.btns = lib.str.acc{}:compose('security & credentials'):finalize() + end + var pg: lib.str.acc pg:init(512) + ctlbox:append(&pg) + ctlbox.name:free() + if ctlbox.btns.ct > 0 then ctlbox.btns:free() end - var cinpp = cinp:finalize() defer cinpp:free() - var clnkp: pstr - if clnk.sz > 0 then clnkp = clnk:finalize() else - clnk:free() - clnkp = pstr { ptr='', ct=0 } + return pg:finalize() end - var unym: lib.str.acc unym:init(64) - unym:lpush('') - lib.render.nym(user.ptr,0,&unym,false) - unym:lpush('') - var pg = data.view.conf_user_ctl { - name = unym:finalize(); - inputcontent = cinpp; - linkcontent = clnkp; - } - var ret = pg:tostr() - pg.name:free() - if clnkp.ct > 0 then clnkp:free() end - return ret else var modes = array(P'local', P'remote', P'staff', P'titled', P'peons', P'all') var idbuf: int8[lib.math.shorthand.maxlen] var ulst: lib.str.acc ulst:init(256) var mode: uint8 = mode_local Index: render/nav.t ================================================================== --- render/nav.t +++ render/nav.t @@ -5,12 +5,12 @@ if co.who ~= nil or co.srv.cfg.pol_sec == lib.srv.secmode.public then t:lpush(' timeline') end if co.who ~= nil then t:lpush(' compose profile media configure docs log out notices') + t:lpush('">profile media configure docs log out notices') else t:lpush(' docs log in') end return t:finalize() end return render_nav Index: render/tweet.t ================================================================== --- render/tweet.t +++ render/tweet.t @@ -33,13 +33,10 @@ if p.rtdby ~= 0 and retweeter == nil then retweeter = co.actorcache:insert(co.srv:actor_fetch_uid(p.rtdby)).ptr end ::foundauth:: - var avistr: lib.str.acc if author.origin == 0 then - avistr:compose('/avi/',author.handle) - end var timestr: int8[26] lib.osclock.ctime_r(&p.posted, ×tr[0]) for i=0,26 do if timestr[i] == @'\n' then timestr[i] = 0 break end end -- 🙄 var bhtml = lib.smackdown.html([lib.mem.ptr(int8)] {ptr=p.body,ct=0},false) defer bhtml:free() Index: route.t ================================================================== --- route.t +++ route.t @@ -286,10 +286,41 @@ ::badurl:: do co:complain(404, 'invalid URL', 'this URL does not reference extant content or functionality') return end ::badop :: do co:complain(405, 'invalid operation', 'the operation you have attempted on this post is not meaningful') return end ::noauth:: do co:complain(401, 'unauthorized', 'you have not supplied the necessary credentials to perform this operation') return end end + +local terra +credsec_for_uid(co: &lib.srv.convo, uid: uint64) + var act = co:ppostv('act') + if act:cmp(lib.str.plit 'invalidate') then + lib.dbg('setting user\'s cookie validation time to now') + co.who.source:auth_sigtime_user_alter(uid, lib.osclock.time(nil)) + -- the current session has been invalidated as well, so we need to immediately install a new authentication cookie with the same aid so the user doesn't need to log back in all over again + co:installkey('/conf/sec',co.aid) + return + elseif act:cmp(lib.str.plit 'newcred') then + var cmt = co:ppostv('comment') + var pw = co:ppostv('newpw') + if pw:ref() then + var cpw = co:ppostv('rptpw') + if not pw:cmp(cpw) then + co:complain(400,'enrollment failure','the passwords you supplied do not match') + return + end + co.srv:auth_attach_pw(uid, false, pw, cmt) + co:reroute('?') + return + else + var key = co:ppostv('newkey') + if key:ref() then + + end + end + end + co:complain(400,'bad request','the operation you have requested is not meaningful in this context') +end terra http.configure(co: &lib.srv.convo, path: hpath, meth: method.t) var msg = pstring.null() -- first things first, do priv checks if path.ct >= 1 then @@ -347,28 +378,43 @@ msg = lib.str.plit 'profile changes saved' --user_refresh = true -- not really necessary here, actually elseif path(1):cmp(lib.str.lit 'sec') then - var act = co:ppostv('act') - if act:cmp(lib.str.plit 'invalidate') then - lib.dbg('setting user\'s cookie validation time to now') - co.who.source:auth_sigtime_user_alter(co.who.id, lib.osclock.time(nil)) - -- the current session has been invalidated as well, so we need to immediately install a new authentication cookie with the same aid so the user doesn't need to log back in all over again - co:installkey('/conf/sec',co.aid) - return - end + credsec_for_uid(co, co.who.id) elseif path(1):cmp(lib.str.lit 'users') then if path.ct >= 3 then var userid, ok = lib.math.shorthand.parse(path(2).ptr, path(2).ct) if ok then var usr = co.srv:actor_fetch_uid(userid) if usr:ref() then defer usr:free() if not co.who:overpowers(usr.ptr) then goto nopriv end end end - elseif path.ct == 2 then + elseif path.ct == 2 and meth == method.post then + var act = co:ppostv('act') + if act:cmp(lib.str.plit'create') then + var newname = co:ppostv('handle') + if not newname or not lib.store.actor.handle_validate(newname.ptr) then + co:complain(400,'invalid handle','the handle you have requested is not valid') + end + var tu = co.srv:actor_fetch_xid(newname) + if tu:ref() then tu:free() + co:complain(409,'handle clash','that handle conflicts with one that already exists') + return + end + var kbuf: uint8[lib.crypt.const.maxdersz] + var na = lib.store.actor.mk(&kbuf[0]) + na.handle = newname.ptr + var newuid = co.srv:actor_create(&na) + var shid: int8[lib.math.shorthand.maxlen] + var shidlen = lib.math.shorthand.gen(newuid, &shid[0]) + var url = lib.str.acc{}:compose('/conf/users/',pstring{&shid[0],shidlen}):finalize() defer url:free() + co:reroute(url.ptr) + return + elseif act:cmp(lib.str.plit'inst') then + else goto badop end end end if user_refresh then -- refresh the user info for the renderer var usr = co.srv:actor_fetch_uid(co.who.id) @@ -382,11 +428,12 @@ end end lib.render.conf(co,path,msg) do return end - ::nopriv:: co:complain(403,'insufficient privileges','you do not have the necessary powers to perform this action') + ::nopriv:: do co:complain(403,'insufficient privileges','you do not have the necessary powers to perform this action') return end + ::badop:: do co:complain(400,'bad request','the operation you have requested is not meaningful in this context') return end end terra http.user_notices(co: &lib.srv.convo, meth: method.t) if meth == method.post then var act = co:ppostv('act') @@ -401,16 +448,12 @@ do return end ::badop:: do co:complain(405, 'invalid operation', 'the operation you have attempted on this post is not meaningful') return end end -terra http.media_manager(co: &lib.srv.convo, path: hpath, meth: method.t) - if meth == method.post then - goto badop - end - - if path.ct == 2 and path(1):cmp(lib.str.lit'upload') and co.who.rights.powers.artifact() then +terra http.media_manager(co: &lib.srv.convo, path: hpath, meth: method.t, uid: uint64) + if co.aid ~= 0 and co.who.id == uid and path.ct == 2 and path(1):cmp(lib.str.lit'upload') and co.who.rights.powers.artifact() then if meth == method.get then var view = data.view.media_upload { folders = '' } var pg = view:tostr() defer pg:free() @@ -455,13 +498,31 @@ var url = lib.str.acc{}:compose('/media/a/',pstring{&idbuf[0],idlen}):finalize() co:reroute(url.ptr) url:free() else goto badop end + elseif co.aid ~= 0 and path.ct == 4 and path(1):cmp(lib.str.lit'a') and meth==method.post then + var act = co:ppostv('act') + if not act or not act:cmp(lib.str.plit'confirm') then goto badop end + var artid, aok = lib.math.shorthand.parse(path(2).ptr,path(2).ct) + if not aok then goto e404 end + var art = co.srv:artifact_fetch(uid,artid) + if not art then goto e404 end + defer art:free() + + if path(3):cmp(lib.str.lit'avi') then + -- user wants to set avatar + co.who.avatarid = artid + co.srv:actor_save(co.who) + co:reroute('/conf/avi') + elseif path(3):cmp(lib.str.lit'del') then + co.srv:artifact_disclaim(co.who.id, artid) + co:reroute('/media') + else goto badop end else if meth == method.post then goto badop end - lib.render.media_gallery(co,path,co.who.id,nil) + lib.render.media_gallery(co,path,uid,nil) end do return end ::badop:: do co:complain(405, 'invalid operation', 'the operation you have attempted on this post is not meaningful') return end ::e404:: do co:complain(404, 'artifact not found', 'no such artifact has been uploaded by this user') return end @@ -503,36 +564,33 @@ end terra http.local_avatar(co: &lib.srv.convo, handle: lib.mem.ptr(int8)) -- TODO retrieve user avatars - co:reroute('/s/default-avatar.webp') + var usr = co.srv:actor_fetch_xid(handle) + if not usr then + goto default end + if usr(0).origin == 0 then + if usr(0).avatarid == 0 then goto default end + var avi, mime = co.srv:artifact_load(usr(0).avatarid) + if not avi then goto default end + defer avi:free() defer mime:free() + co:bytestream(mime,avi) + else + co:reroute(usr(0).avatar) + end + do return end + ::default:: co:reroute('/s/default-avatar.webp') end terra http.file_serve_raw(co: &lib.srv.convo, id: lib.mem.ptr(int8)) var id, idok = lib.math.shorthand.parse(id.ptr, id.ct) if not idok then goto e404 end var data, mime = co.srv:artifact_load(id) if not data then goto e404 end do defer data:free() defer mime:free() - var safemime = mime - -- TODO this is not a satisfactory solution; it's a bandaid on a gaping - -- chest wound. ultimately we need to compile a whitelist of safe mime - -- types as part of mimelib, but that is no small task. for now, this - -- will keep the patient from immediately bleeding out - if mime:cmp(lib.str.plit'text/html') or - mime:cmp(lib.str.plit'text/xml') or - mime:cmp(lib.str.plit'application/xhtml+xml') or - mime:cmp(lib.str.plit'application/vnd.wap.xhtml+xml') - then -- danger will robinson - safemime = lib.str.plit'text/plain' - elseif mime:cmp(lib.str.plit'application/x-shockwave-flash') then - safemime = lib.str.plit'application/octet-stream' - end - lib.net.mg_printf(co.con, "HTTP/1.1 200 OK\r\nContent-Type: %.*s\r\nContent-Length: %llu\r\nContent-Security-Policy: sandbox; default-src 'none'; form-action 'none'; navigate-to 'none';\r\nX-Content-Options: nosniff\r\n\r\n", safemime.ct, safemime.ptr, data.ct + 2) - lib.net.mg_send(co.con, data.ptr, data.ct) - lib.net.mg_send(co.con, '\r\n', 2) + co:bytestream(mime,data) return end ::e404:: do co:complain(404, 'artifact not found', 'no such artifact has been uploaded to this instance') return end end @@ -582,11 +640,11 @@ http.tweet_page(co, path, meth) elseif path(0):cmp(lib.str.lit('tl')) then http.timeline(co, path) elseif path(0):cmp(lib.str.lit('media')) then if co.aid == 0 then goto unauth end - http.media_manager(co, path, meth) + http.media_manager(co, path, meth, co.who.id) elseif path(0):cmp(lib.str.lit('doc')) then if not meth_get(meth) then goto wrongmeth end http.documentation(co, path) elseif path(0):cmp(lib.str.lit('conf')) then if co.aid == 0 then goto unauth end Index: srv.t ================================================================== --- srv.t +++ srv.t @@ -230,10 +230,29 @@ }) end end terra convo:stdpage(pg: convo.page) self:statpage(200, pg) end + +terra convo:bytestream(mime: pstring, data: lib.mem.ptr(uint8)) + -- TODO this is not a satisfactory solution; it's a bandaid on a gaping + -- chest wound. ultimately we need to compile a whitelist of safe mime + -- types as part of mimelib, but that is no small task. for now, this + -- will keep the patient from immediately bleeding out + if mime:cmp(lib.str.plit'text/html') or + mime:cmp(lib.str.plit'text/xml') or + mime:cmp(lib.str.plit'application/xhtml+xml') or + mime:cmp(lib.str.plit'application/vnd.wap.xhtml+xml') + then -- danger will robinson + mime = lib.str.plit'text/plain' + elseif mime:cmp(lib.str.plit'application/x-shockwave-flash') then + mime = lib.str.plit'application/octet-stream' + end + lib.net.mg_printf(self.con, "HTTP/1.1 200 OK\r\nContent-Type: %.*s\r\nContent-Length: %llu\r\nContent-Security-Policy: sandbox; default-src 'none'; form-action 'none'; navigate-to 'none';\r\nX-Content-Options: nosniff\r\n\r\n", mime.ct, mime.ptr, data.ct + 2) + lib.net.mg_send(self.con, data.ptr, data.ct) + lib.net.mg_send(self.con, '\r\n', 2) +end terra convo:reroute_cookie(dest: rawstring, cookie: rawstring) var hdrs = array( lib.http.header { key = 'Content-Type', value = 'text/html; charset=UTF-8' }, lib.http.header { key = 'Location', value = dest }, Index: static/style.scss ================================================================== --- static/style.scss +++ static/style.scss @@ -633,10 +633,19 @@ menu { all: unset; display: block; } body.conf main { display: grid; grid-template-columns: 2in 1fr; grid-template-rows: max-content 1fr; + div.context { + border-radius: 4px; + text-align: center; + background: tone(-53%); + box-shadow: 0 1px 0 1px tone(-55%); + border: 1px solid tone(-20%); + font-style: italic; + padding: 0.1in; + } > menu { @extend %navmenu; } > .panel { grid-column: 2/3; grid-row: 1/3; padding-left: 0.15in; > h1 { Index: store.t ================================================================== --- store.t +++ store.t @@ -165,10 +165,11 @@ return m.actor { id = 0; nym = nil; handle = nil; origin = 0; bio = nil; avatar = nil; knownsince = lib.osclock.time(nil); rights = m.rights_default(); + avatarid = 0; epithet = nil, key = [lib.mem.ptr(uint8)] { ptr = &kbuf[0], ct = privsz }; } end @@ -409,11 +410,12 @@ actor_rel_destroy: {&m.source, uint16, uint64, uint64} -> {} actor_rel_calc: {&m.source, uint64, uint64} -> m.relationship auth_enum_uid: {&m.source, uint64} -> lib.mem.lstptr(m.auth) auth_enum_handle: {&m.source, rawstring} -> lib.mem.lstptr(m.auth) - auth_attach_pw: {&m.source, uint64, bool, pstr, pstr} -> {} + auth_attach_pw: {&m.source, uint64, bool, pstr, pstr} -> {} + auth_attach_key: {&m.source, uint64, bool, pstr, pstr} -> {} -- uid: uint64 -- reset: bool (delete other passwords?) -- pw: pstring -- comment: pstring auth_purge_pw: {&m.source, uint64, rawstring} -> {} Index: view/conf-sec-credmg.tpl ================================================================== --- view/conf-sec-credmg.tpl +++ view/conf-sec-credmg.tpl @@ -1,19 +1,19 @@
-

your account can currently be accessed with the credentials listed below. if you fear a credential has been compromised, you can revoke or reset it.

+

this account can currently be accessed with the credentials listed below. if you fear a credential has been compromised, you can revoke or reset it.


-
-

you can associate extra credentials with your account. you can also limit how much of your authority these credentials can be used to exercise — for instance, it might be useful to create API keys that can read your timeline, but not post as you or access any administrative powers you may have. if you don't select a capability set, the credential will be able to wield the full scope of your powers.

+ +

you can associate extra credentials with this account. you can also limit how much of this account’s authority these credentials can be used to exercise — for instance, it might be useful to create API keys that can read the account timeline, but not post as the account owner or access any of his administrative powers. if you don't select a capability set, the credential will be able to wield the full scope of the associated account‘s powers.

@@ -25,11 +25,11 @@
- - - - -
+ + + + +
ADDED view/conf-sec-pwnew.tpl Index: view/conf-sec-pwnew.tpl ================================================================== --- view/conf-sec-pwnew.tpl +++ view/conf-sec-pwnew.tpl @@ -0,0 +1,18 @@ +
+
+ + +
+
+ + +
+
+ + +
+ + + cancel + +
Index: view/conf-sec.tpl ================================================================== --- view/conf-sec.tpl +++ view/conf-sec.tpl @@ -1,10 +1,10 @@
-

if you are concerned that your account may have been compromised, you can terminate all other login sessions by invalidating their session cookies. note that this will not have any effect on API tokens; these must be revoked separately!

+

if you are concerned that this account may have been compromised, you can terminate conflicting login sessions by invalidating their session cookies. note that this will not have any effect on API tokens; these must be revoked separately!

@lastreset
Index: view/conf-user-ctl.tpl ================================================================== --- view/conf-user-ctl.tpl +++ view/conf-user-ctl.tpl @@ -2,8 +2,10 @@
@name
@inputcontent - + + + @btns + -@linkcontent Index: view/load.lua ================================================================== --- view/load.lua +++ view/load.lua @@ -21,10 +21,11 @@ 'conf'; 'conf-profile'; 'conf-sec'; 'conf-sec-credmg'; + 'conf-sec-pwnew'; 'conf-user-ctl'; } local ingest = function(filename) local hnd = io.open(path..'/'..filename)