Index: backend/pgsql.t ================================================================== --- backend/pgsql.t +++ backend/pgsql.t @@ -60,14 +60,37 @@ (a.origin is null and $1::text = a.handle or $1::text = ('@' || a.handle)) ]]; }; + + actor_create = { + params = { + rawstring, rawstring, uint64, lib.store.timepoint, + rawstring, rawstring, lib.mem.ptr(uint8), + rawstring, uint16, uint32 + }; + sql = [[ + insert into parsav_actors ( + nym,handle, + origin,knownsince, + bio,avataruri,key, + title,rank,quota + ) values ($1::text, $2::text, + case when $3::bigint = 0 then null + else $3::bigint end, + to_timestamp($4::bigint), + $5::bigint, $6::bigint, $7::bytea, + $8::text, $9::smallint, $10::integer + ) returning id + ]]; + }; + actor_auth_pw = { params = {pstring,rawstring,pstring,lib.store.inet}, sql = [[ - select a.aid from parsav_auth as a + select a.aid, a.uid, a.name from parsav_auth as a left join parsav_actors as u on u.id = a.uid where (a.uid is null or u.handle = $1::text or ( a.uid = 0 and a.name = $1::text )) and (a.kind = 'trust' or (a.kind = $2::text and a.cred = $3::bytea)) and @@ -163,10 +186,16 @@ where au.aid = $1::bigint and au.blacklist = false and (au.netmask is null or au.netmask >> $2::inet) ]]; }; + + actor_powers_fetch = { + params = {uint64}, sql = [[ + select key, allow from parsav_rights where actor = $1::bigint + ]] + }; post_create = { params = {uint64, rawstring, rawstring, rawstring}, sql = [[ insert into parsav_posts ( author, subject, acl, body, @@ -215,25 +244,32 @@ end terra pqr:cols() return lib.pq.PQnfields(self.res) end terra pqr:string(row: intptr, col: intptr) -- not to be exported!! if self:null(row,col) then return nil end var v = lib.pq.PQgetvalue(self.res, row, col) --- var r: lib.mem.ptr(int8) --- r.ct = lib.str.sz(v) --- r.ptr = v return v +end +terra pqr:_string(row: intptr, col: intptr) -- not to be exported!! + if self:null(row,col) then return pstring.null() end + return pstring { + ptr = lib.pq.PQgetvalue (self.res, row, col); + ct = lib.pq.PQgetlength(self.res, row, col); + } end terra pqr:bin(row: intptr, col: intptr) -- not to be exported!! DO NOT FREE + if self:null(row,col) then return [lib.mem.ptr(uint8)].null() end return [lib.mem.ptr(uint8)] { ptr = [&uint8](lib.pq.PQgetvalue(self.res, row, col)); ct = lib.pq.PQgetlength(self.res, row, col); } end terra pqr:String(row: intptr, col: intptr) -- suitable to be exported - if self:null(row,col) then return [lib.mem.ptr(int8)] {ptr=nil,ct=0} end - var s = [lib.mem.ptr(int8)] { ptr = lib.str.dup(self:string(row,col)) } - s.ct = lib.pq.PQgetlength(self.res, row, col) + if self:null(row,col) then return pstring.null() end + var s = pstring { + ptr = lib.str.dup(self:string(row,col)); + ct = lib.pq.PQgetlength(self.res, row, col); + } return s end terra pqr:bool(row: intptr, col: intptr) var v = lib.pq.PQgetvalue(self.res, row, col) if @v == 0x01 then return true else return false end @@ -423,10 +459,19 @@ end if r:null(row,3) then a.ptr.origin = 0 else a.ptr.origin = r:int(uint64,row,3) end return a end + +local privmap = {} +do local struct pt { name:pstring, priv:lib.store.powerset } +for k,v in pairs(lib.store.powerset.members) do + privmap[#privmap + 1] = quote + var ps: lib.store.powerset ps:clear() + (ps.[v] << true) + in pt {name = lib.str.plit(v), priv = ps} end +end end local checksha = function(src, hash, origin, username, pw) local validate = function(kind, cred, credlen) return quote var r = queries.actor_auth_pw.exec( @@ -435,12 +480,14 @@ kind, [lib.mem.ptr(int8)] {ptr=[&int8](cred), ct=credlen}, origin) if r.sz > 0 then -- found a record! stop here var aid = r:int(uint64, 0,0) + var uid = r:int(uint64, 0,1) + var name = r:String(0,2) r:free() - return aid + return aid, uid, name end end end local out = symbol(uint8[64]) @@ -569,20 +616,20 @@ actor_auth_pw = [terra( src: &lib.store.source, ip: lib.store.inet, username: lib.mem.ptr(int8), cred: lib.mem.ptr(int8) - ): uint64 + ): {uint64, uint64, pstring} [ checksha(`src, 256, ip, username, cred) ] -- most common [ checksha(`src, 512, ip, username, cred) ] -- most secure [ checksha(`src, 384, ip, username, cred) ] -- weird [ checksha(`src, 224, ip, username, cred) ] -- weirdest -- TODO: check pbkdf2-hmac -- TODO: check OTP - return 0 + return 0, 0, pstring.null() end]; actor_stats = [terra(src: &lib.store.source, uid: uint64) var r = queries.actor_stats.exec(src, uid) if r.sz == 0 then lib.bail('error fetching actor stats!') end @@ -654,8 +701,42 @@ var ret: lib.mem.ptr(lib.mem.ptr(lib.store.post)) ret:init(r.sz) for i=0,r.sz do ret.ptr[i] = row_to_post(&r, i) end -- MUST FREE ALL return ret end]; + + actor_powers_fetch = [terra( + src: &lib.store.source, + uid: uint64 + ): lib.store.powerset + var powers = lib.store.rights_default().powers + var map = array([privmap]) + var r = queries.actor_powers_fetch.exec(src, uid) + + for i=0, r.sz do + for j=0, [map.type.N] do + var pn = r:_string(i,0) + if map[j].name:cmp(pn) then + if r:bool(i,1) + then powers = powers + map[j].priv + else powers = powers - map[j].priv + end + end + end + end + + return powers + end]; + + actor_create = [terra( + src: &lib.store.source, + ac: &lib.store.actor + ): uint64 + var r = queries.actor_create.exec(src,ac.nym, ac.handle, ac.origin, ac.knownsince, ac.bio, ac.avatar, ac.key, ac.title, ac.rights.rank, ac.rights.quota) + if r.sz == 0 then lib.bail('failed to create actor!') end + return r:int(uint64,0,0) + end]; + + actor_auth_register_uid = nil; -- not necessary for view-based auth } return b Index: common.lua ================================================================== --- common.lua +++ common.lua @@ -94,10 +94,15 @@ has = function(haystack,needle,eq) eq = eq or function(a,b) return a == b end for k,v in pairs(haystack) do if eq(needle,v) then return k end end + end; + keys = function(ary) + local kt = {} + for k,v in pairs(ary) do kt[#kt+1] = k end + return kt end; ingest = function(f) local h = io.open(f, 'r') if h == nil then return nil end local txt = f:read('*a') f:close() Index: config.lua ================================================================== --- config.lua +++ config.lua @@ -29,10 +29,14 @@ os.getenv('NIX_STORE') and 'nixos', '')); tgttrip = default('parsav_arch_triple'); -- target triple, used in xcomp tgtcpu = default('parsav_arch_cpu'); -- target cpu, used in xcomp tgthf = u.tobool(default('parsav_arch_armhf',true)); + doc = { + online = u.tobool(default('parsav_online_documentation',true)); + offline = u.tobool(default('parsav_offline_documentation',true)); + }; outform = default('parsav_emit_type', 'o'); endian = default('parsav_arch_endian', 'little'); build = { id = u.rndstr(6); release = u.ingest('release'); @@ -46,10 +50,13 @@ {'default-avatar.webp', 'image/webp'}; {'padlock.webp', 'image/webp'}; {'warn.webp', 'image/webp'}; }; } +if os.getenv('parsav_let_me_be_an_idiot') == "i know what i'm doing" then + conf.braingeniousmode = true -- SOUND GENERAL QUARTERS +end if u.ping '.fslckout' or u.ping '_FOSSIL_' then if u.ping '_FOSSIL_' then default_os = 'windows' end conf.build.branch = u.exec { 'fossil', 'branch', 'current' } conf.build.checkout = (u.exec { 'fossil', 'sql', [[select value from localdb.vvar where name = 'checkout-hash']] Index: crypt.t ================================================================== --- crypt.t +++ crypt.t @@ -9,16 +9,18 @@ return code and 0xFF80 end; toobig = -lib.pk.MBEDTLS_ERR_RSA_OUTPUT_TOO_LARGE; } const.maxpemsz = math.floor((const.keybits / 8)*6.4) + 128 -- idk why this formula works but it basically seems to +const.maxdersz = const.maxpemsz -- FIXME this is a safe value but obvs not the correct one local ctx = lib.pk.mbedtls_pk_context local struct hashalg { id: uint8 bytes: intptr } local m = { pemfile = uint8[const.maxpemsz]; + const = const; algsz = { sha1 = 160/8; sha256 = 256/8; sha512 = 512/8; sha384 = 384/8; @@ -63,10 +65,18 @@ return lib.pk.mbedtls_pk_write_pubkey_pem(key, buf, const.maxpemsz) == 0 else return lib.pk.mbedtls_pk_write_key_pem(key, buf, const.maxpemsz) == 0 end end + +terra m.der(pub: bool, key: &ctx, buf: &uint8): intptr + if pub then + return lib.pk.mbedtls_pk_write_pubkey_der(key, buf, const.maxdersz) + else + return lib.pk.mbedtls_pk_write_key_der(key, buf, const.maxdersz) + end +end m.destroy = lib.dispatch { [ctx] = function(v) return `lib.pk.mbedtls_pk_free(&v) end; [false] = function(ptr) return `ptr:free() end; ADDED doc/acl.md Index: doc/acl.md ================================================================== --- doc/acl.md +++ doc/acl.md @@ -0,0 +1,36 @@ +# access control + +to help limit who can see your post (and how far it is propagated), `parsav` uses **ACL expressions**. this is roughly equivalent to scoping in pleroma or GNU social terms. an ACL expression consists of one or more space-separated terms, each of which match a certain set of users. a term can be negated by prefixing it with `~`, a tilde character, so `~all` matches nobody, and `~followed` matches users you do not follow. + +* **all**: matches any and all users +* **local**: matches users who belong to this instance +* **mutuals**: matches users you follow who also follow you +* **followed**: matches users you follow +* **followers**: matches users who follow you +* **groupies**: matches users who follow you, but whom you do not follow +* **mentioned**: matches users who are mentioned in the post +* **staff**: matches instance staff (equivalent to `~%0`) +* **admin**: matches the individual named as the instance administrator, if any +* **@**`handle`: matches the user `handle` +* **+**`circle`: matches users you have categorized under `circle` +* **#**`room`: matches users who are members of `room` +* **%**`rank`: matches users of `rank` or higher (e.g. `%3` matches users of rank 3, 2, and 1). as a special case, `%0` matches ordinary users +* **#**`room`**%**`rank`: matches users who hold `rank` in `room` +* **<**`title`**>**: matches peers of the net who have been created `title` by the sovereign +* **#**`room`**<**`title`**>**: matches peers of the chat who have been created `title` by `room` staff + +to evaluate an ACL expression, `parsav` reads each term from start to finish. for each term, it considers whether it describes the user who is attempting to access the content. if the term matches, its policy is applied and the expression completes. if the term doesn't match, the server proceeds on to the next term and the process repeats until it finds a matching term or runs out of terms, applying the fallback policy. + +**policy** is whether a term grants or denies access. the default term policy is **allow**, but you can control the policy with the keywords `allow` and `deny`. if a term finishes evaluating without any match being found, a fallback policy is applied; this fallback is the opposite of whatever the current policy is. this sounds confusing but makes ACL expressions much more intuitive; `allow @bob` and `deny trent` do exactly what you'd expect — the former allows bob and only bob in; the latter denies access only to trent, but grants access to the rest of the world. + +expressions must contain at least one term to be valid. if they consist only of policy keywords, they will be rejected. + +in effect, this all means that an ACL expression can be treated as a simple list of who is allowed to view your post. for instance, an expression of `local` means only local users can view it. however, much more complex expressions are possible. + +* `deny groupies allow +illuminati`: permits access to the illuminati, but excluding those members who are groupies +* `+illuminati deny groupies`: allows access to everyone but groupies (unless they're in the illuminati) +* `@eve @alice@nowhere.tld deny @bob @trent@witches.live`: grants access to eve and alice, but locks out bob and trent +* ` #4th-intl`: restricts the post to the eyes of the Fourth International's secret cabal of anointed comrades and the grand dukes of the Empire +* `deny ~%3`: blocks a post from being seen by anyone with a staff rank level below 3 + +**limitations:** to inhibit potential denial-of-service attacks, ACL expressions can be a maximum of 128 characters, can contain at most 16 words, and cannot trigger queries against other servers. all information needed to evaluate an ACL expression must be known locally. this is particularly relevant with respect to rooms. ADDED doc/invocation.md Index: doc/invocation.md ================================================================== --- doc/invocation.md +++ doc/invocation.md @@ -0,0 +1,1 @@ +# daemon invocation ADDED doc/load.lua Index: doc/load.lua ================================================================== --- doc/load.lua +++ doc/load.lua @@ -0,0 +1,23 @@ +local path = ... +local sources = { +-- user section + acl = {title = 'access control lists'}; +-- admin section + --overview = {title = 'server overview', priv = 'config'}; + invocation = {title = 'daemon invocation', priv = 'config'}; + --backends = {title = 'storage backends', priv = 'config'}; + --pgsql = {title = 'pgsql', priv = 'config', parent = 'backends'}; +} + +local util = dofile 'common.lua' +local ingest = function(filename) + return (util.exec { 'cmark', '--smart', '--unsafe', (path..'/'..filename) }):gsub('\n','') +end + +local doc = {} +for n,meta in pairs(sources) do doc[n] = { + name = n; + text = ingest(n .. '.md'); + meta = meta; +} end +return doc Index: http.t ================================================================== --- http.t +++ http.t @@ -1,8 +1,8 @@ -- vim: ft=terra local m = {} -local util = dofile('common.lua') +local util = lib.util m.method = lib.enum { 'get', 'post', 'head', 'options', 'put', 'delete' } m.mime = lib.enum { 'html'; -- default 'json'; Index: mem.t ================================================================== --- mem.t +++ mem.t @@ -54,13 +54,10 @@ t.ptr_basetype = ty local recurse = false --if ty:isstruct() then --if ty.methods.free then recurse = true end --end - t.metamethods.__not = macro(function(self) - return `self.ptr - end) if dyn then t.methods = { free = terra(self: &t): bool [recurse and quote self.ptr:free() @@ -106,10 +103,14 @@ terra t:advance(n: intptr) self.ptr = self.ptr + n self.ct = self.ct - n return self.ptr end + terra t.methods.null(): t return t { ptr = nil, ct = 0 } end -- maybe should be a macro? + terra t:ref() return self.ptr ~= nil end + t.metamethods.__not = macro(function(self) return `not self:ref() end) + t.metamethods.__apply = macro(function(self,idx) return `self.ptr[idx] end) if not ty:isstruct() then terra t:cmp_raw(other: &ty) for i=0, self.ct do if self.ptr[i] ~= other[i] then return false end end Index: parsav.md ================================================================== --- parsav.md +++ parsav.md @@ -7,17 +7,21 @@ * postgresql ## dependencies -* mongoose -* json-c -* mbedtls -* **postgresql backend:** - * postgresql-libs +* runtime + * mongoose + * json-c + * mbedtls + * **postgresql backend:** + * postgresql-libs +* compile-time + * cmark (commonmark implementation), for transformation of the help files, whose source is in commonmark. online documentation transforms these into html and embeds them in the binary; cmark is also used to to produce the troff source which is used to build the offline documentation. disable with `parsav_online_documentation=no parsav_offline_documentation=no` + * troff implementation (tested with groff but as far as i know we don't need any groff-specific extensions) to produce PDFs and manpages from the cmark-generated intermediate forms. disable with `parsav_offline_documentation=no` -additional build-time dependencies are necessary if you are building directly from trunk, rather than from a release tarball that includes certain build artifacts which need to be embedded in the binary: +additional preconfigure dependencies are necessary if you are building directly from trunk, rather than from a release tarball that includes certain build artifacts which need to be embedded in the binary: * inkscape, for rendering out UI graphics * cwebp (libwebp package), for transforming inkscape PNGs to webp * sassc, for compiling the SCSS stylesheet into its final CSS @@ -30,10 +34,12 @@ ## building first, either install any missing dependencies as shared libraries, or build them as static libraries with the command `make dep.$LIBRARY`. as a shortcut, `make dep` will build all dependencies as static libraries. note that if the build system finds a static version of a library in the `lib/` folder, it will use that instead of any system library. note that these commands require GNU make (it may be installed as `gmake` on your system), although this is a fairly soft dependency -- if you really need to build it on BSD make, you can probably translate it with a minute or so of work; you'll just have to do some of the various gmake functions' work manually. this may be worthwhile if you're packaging for a BSD. postgresql-libs must be installed systemwide, as `parsav` does not currently provide for statically compiling and linking it + +if you use nixos and wish to build the pdf documentation, you're going to have to do a bit of extra work (but you're used to that, aren't you). for some incomprehensible reason, the groff package on nix is split up, seemingly randomly, with many crucial output devices relegated to the "perl" output of the package, which is not installed by default (and `nix-env -iA nixos.groff.perl` doesn't work either; i don't know why either). you'll have to instantiate and install the outputs directly by path, e.g. `nix-env -i /nix/store/*groff*/` to get everything you need into your profile. alas, the battle is not over: you also need to change the environment variables `GROFF_FONT_PATH` and `GROFF_TMAC_PATH` to point at the `font` and `tmac` subdirs of `~/.nix-profile/share/groff/$groff_version/`. once this is done, invoking `groff -Tpdf` will work as expected. ## configuring the `parsav` configuration is comprised of two components: the backends list and the config store. the backends list is a simple text file that tells `parsav` which data sources to draw from. the config store is a key-value store which contains the rest of the server's configuration, and is loaded from the backends. the configuration store can be spread across the backends; backends will be checked for configuration keys according to the order in which they are listed. changes to the configuration store affect parsav in real time; you only need to restart the server if you make a change to the backend list. @@ -66,19 +72,25 @@ ) `aid` is a unique value identifying the authentication method. it must be deterministic -- values based on time of creation or a hash of `uid`+`kind`+`cred` are ideal. `uid` is the identifier of the user the row specifies credentials for. `kind` is a string indicating the credential type, and `cred` is the content of that credential.for the meaning of these fields and use of this structure, see **authentication** below. ## authentication -in the most basic case, an authentication record would be something like `{uid = 123, kind = "pw-sha512", cred = "12bf90…a10e"}`. but `parsav` is not restricted to username-password authentication, and in addition to various hashing styles, it also will support more esoteric forms of authentcation. any individual user can have as many auth rows as she likes. there is also a `restrict` field, which is normally null, but can be specified in order to restrict a particular credential to certain operations, such as posting tweets or updating a bio. `blacklist` indicates that any attempt to authenticate that matches this row will be denied, regardless of whether it matches other rows. if `netmask` is present, this authentication will only succeed if it comes from the specified IP mask. +in the most basic case, an authentication record would be something like `{uid = 123, kind = "pw-sha512", cred = "\x12bf90…a10e"::bytea}`. but `parsav` is not restricted to username-password authentication, and in addition to various hashing styles, it also will support more esoteric forms of authentcation. any individual user can have as many auth rows as she likes. there is also a `restrict` field, which is normally null, but can be specified in order to restrict a particular credential to certain operations, such as posting tweets or updating a bio. `blacklist` indicates that any attempt to authenticate that matches this row will be denied, regardless of whether it matches other rows. if `netmask` is present, this authentication will only succeed if it comes from the specified IP mask. `uid` can also be `0` (not null, which matches any user!), indicating that there is not yet a record in `parsav_actors` for this account. if this is the case, `name` must contain the handle of the account to be created when someone attempts to log in with this credential. whether `name` is used in the authentication process depends on whether the authentication method accepts a username. all rows with the same `uid` *must* have the same `name`. -below is a full list of authentication types we intend to support. a checked box indicates the scheme has been implemented. +below is a full list of authentication types we intend/hope to one day support. contributors should consider this a to-do list. a checked box indicates the scheme has been implemented. * ☑ pw-sha{512,384,256,224}: an ordinary password, hashed with the appropriate algorithm * ☐ pw-{sha1,md5,clear} (insecure, must be manually enabled at compile time with the config variable `parsav_let_me_be_a_dumbass="i know what i'm doing"`) * ☐ pw-pbkdf2-hmac-sha{…}: a password hashed with the Password-Based Key Derivation Function 2 instead of plain SHA2 +* ☐ pw-extern-ldap: try to authenticate by binding against an LDAP server +* ☐ pw-extern-cyrus: try to authenticate against saslauthd +* ☐ pw-extern-dovecot: try to authenticate against a dovecot SASL socket +* ☐ pw-extern-krb5: abuse MIT kerberos as a password verifier +* ☐ pw-extern-imap: abuse an email server as a password verifier +* (extra credit) ☐ pw-extern-radius: verify a user against a radius server * ☐ api-digest-sha{…}: a value that can be hashed with the current epoch to derive a temporary access key without logging in. these are used for API calls, sent in the header `X-API-Key`. * ☐ otp-time-sha1: a TOTP PSK: the first two bytes represent the step, the third byte the OTP length, and the remaining ten bytes the secret key * ☐ tls-cert-fp: a fingerprint of a client certificate * ☐ tls-cert-ca: a value of the form `fp/key=value` where a client certificate with the property `key=value` (e.g. `uid=cyberlord19`) signed by a certificate authority matching the given fingerprint `fp` can authenticate the user * ☐ challenge-rsa-sha256: an RSA public key. the user is presented with a challenge and must sign it with the corresponding private key using SHA256. @@ -97,9 +109,9 @@ * plain text/filesystem storage * lmdb * sqlite3 * generic odbc * lua -* ldap?? possibly just for users +* ldap for auth (and maybe actors?) * cdb (for static content, maybe?) * mariadb/mysql * the various nosql horrors, e.g. redis, mongo, and so on Index: parsav.t ================================================================== --- parsav.t +++ parsav.t @@ -3,11 +3,11 @@ local util = dofile('common.lua') local buildopts, buildargs = util.parseargs{...} config = dofile('config.lua') lib = { - init = {}; + init = {}, util = util; load = function(lst) for _, l in pairs(lst) do local path = {} for m in l:gmatch('([^:]+)') do path[#path+1]=m end local tgt = lib @@ -47,11 +47,12 @@ else code[#code+1] = quote var n = v in lib.io.send(2, n, lib.str.sz(n)) end end end - if nl then code[#code+1] = `lib.io.send(fd, '\n', 1) 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 end; emitv = function(nl,fd,...) local vec = {} local defs = {} @@ -75,11 +76,12 @@ end ct = ct or #str end vec[#vec + 1] = `[lib.uio.iovec]{iov_base = [&opaque](str), iov_len = ct} end - if nl then vec[#vec + 1] = `[lib.uio.iovec]{iov_base = [&opaque]('\n'), iov_len = 1} end + if nl == true then vec[#vec + 1] = `[lib.uio.iovec]{iov_base = [&opaque]('\n'), iov_len = 1} + elseif nl then vec[#vec + 1] = `[lib.uio.iovec]{iov_base = [&opaque](nl), iov_len = [#nl]} end return quote [defs] var strs = array( [vec] ) in lib.uio.writev(fd, strs, [#vec]) end end; @@ -176,11 +178,14 @@ end end local defrep = function(level,n,code) return macro(function(...) - local q = lib.emit(true, 2, noise_header(code,n), ...) + local fn = (...).filename + local ln = tostring((...).linenumber) + local dbgtag = string.format('\27[35m · \27[34m%s:\27[1m%s\27[m\n', fn,ln) + local q = lib.emit(level < 3 and true or dbgtag, 2, noise_header(code,n), ...) return quote if noise >= level then timehdr(); [q] end end end); end lib.dbg = defrep(3,'debug', '32') lib.report = defrep(2,'info', '35') @@ -262,10 +267,40 @@ q = quote [q] new._store[i] = self._store[i] or other._store[i] end end return quote [q] in new end + end) + set.metamethods.__and = macro(function(self,other) + local new = symbol(set) + local q = quote var [new] new:clear() end + for i = 0, bytes - 1 do + q = quote [q] + new._store[i] = self._store[i] and other._store[i] + end + end + return quote [q] in new end + end) + set.metamethods.__not = macro(function(self) + local new = symbol(set) + local q = quote var [new] new:clear() end + for i = 0, bytes - 1 do + q = quote [q] + new._store[i] = not self._store[i] + end + end + return quote [q] in new end + end) + set.metamethods.__sub = macro(function(self,other) + local new = symbol(set) + local q = quote var [new] new:clear() end + for i = 0, bytes - 1 do + q = quote [q] + new._store[i] = self._store[i] and (not other._store[i]) + end + end + return quote [q] in new end end) bit.metamethods.__cast = function(from,to,e) local q = quote var s = e in (s._set._store[s._v/8] and (1 << s._v % 8)) end if to == bit then error('casting to bit is not meaningful') @@ -312,10 +347,11 @@ do local collate = function(path,f, ...) return loadfile(path..'/'..f..'.lua')(path, ...) end data = { + doc = collate('doc','load'); view = collate('view','load'); static = {}; stmap = global(lib.mem.ref(int8)[#config.embeds]); -- array of pointers to static content } end for i,e in ipairs(config.embeds) do local v = e[1] @@ -339,10 +375,11 @@ 'render:profile'; 'render:userpage'; 'render:compose'; 'render:tweet'; 'render:timeline'; + 'render:docpage'; 'route'; } do local p = string.format('parsav: %s\nbuilt on %s\n', config.build.str, config.build.when) ADDED render/docpage.t Index: render/docpage.t ================================================================== --- render/docpage.t +++ render/docpage.t @@ -0,0 +1,119 @@ +-- vim: ft=terra +local page = lib.srv.convo.page +local pstr = lib.mem.ptr(int8) +local pref = lib.mem.ref(int8) +local P = lib.str.plit +local R = lib.str.lit + +local topics = lib.util.keys(data.doc) +local topicidxt = {} +table.sort(topics) -- because deterministic builds are good +local branches = {} +for i,k in pairs(topics) do + topicidxt[k] = i + local par = data.doc[k].meta.parent + if par then + branches[par] = branches[par] or {} + local br = branches[par] + br[#br+1] = k + end +end + +local struct pgpair { + content:page name:pref title:pref + priv:lib.store.powerset parent:intptr +} +local pages = symbol(pgpair[#topics]) +local allpages = {} + + +for i,v in ipairs(topics) do + local t = data.doc[v] + local par = 0 + if t.meta.parent then par = topicidxt[t.meta.parent] end + local restrict = symbol(lib.store.powerset) + local setbits = quote restrict:clear() end + if t.meta.priv then + if type(t.meta.priv) ~= 'table' then t.meta.priv = {t.meta.priv} end + for _,v in pairs(t.meta.priv) do + setbits = quote [setbits]; (restrict.[v] << true) end + end + end + allpages[i] = quote var [restrict]; [setbits] in pgpair { + name = R(v); + parent = par; + priv = restrict; + title = R(t.meta.title); + content = page { + title = ['documentation :: ' .. t.meta.title]; + body = [ t.text ]; + class = P'doc article'; + }; + } end +end + +local terra +showpage(co: &lib.srv.convo, id: pref) + var [pages] = array([allpages]) + for i=0,[pages.type.N] do + if pages[i].name:cmp(id) then + co:stdpage(pages[i].content) + return + end + end -- else + co:complain(404,'not found', 'no help article with that identifier is available') +end + +local terra +pushbranches(list: &lib.str.acc, idx: intptr, ps: lib.store.powerset): {} + var [pages] = array([allpages]) + var started = false + for i=0,[pages.type.N] do + if pages[i].parent == idx+1 and (pages[i].priv:sz() == 0 or + (ps and pages[i].priv):sz() > 0) then + if not started then + started = true + list:lpush('') end +end + +local terra +render_docpage(co: &lib.srv.convo, pg: pref) + var nullprivs: lib.store.powerset nullprivs:clear() + if not pg then -- display index + var list: lib.str.acc list:compose('') + + var bp = list:finalize() + co:stdpage(page { + title = 'documentation'; + body = bp; + class = P'doc listing'; + }) + bp:free() + else showpage(co, pg) end +end + +return render_docpage 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 configure help log out') + t:lpush('">profile configure docs log out') else - t:lpush('help log in') + t:lpush('docs log in') end return t:finalize() end return render_nav Index: render/profile.t ================================================================== --- render/profile.t +++ render/profile.t @@ -17,10 +17,11 @@ aux:push('control',17) end auxp = aux:finalize() else aux:compose('remote follow') + auxp = aux:finalize() end var avistr: lib.str.acc if actor.origin == 0 then avistr:compose('/avi/',actor.handle) end var timestr: int8[26] lib.osclock.ctime_r(&actor.knownsince, ×tr[0]) Index: render/userpage.t ================================================================== --- render/userpage.t +++ render/userpage.t @@ -6,22 +6,15 @@ ti:compose('my profile') else ti:compose('profile :: ', actor.handle) end var pftxt = lib.render.profile(co,actor) defer pftxt:free() - - var doc = data.view.docskel { - instance = co.srv.cfg.instance; - title = ti:finalize(); - body = pftxt; + var tiptr = ti:finalize() + co:stdpage([lib.srv.convo.page] { + title = tiptr; body = pftxt; class = lib.str.plit 'profile'; - navlinks = co.navbar; - } + }) - var hdrs = array( - lib.http.header { 'Content-Type', 'text/html; charset=UTF-8' } - ) - doc:send(co.con,200,[lib.mem.ptr(lib.http.header)] {ct = 1, ptr = &hdrs[0]}) - doc.title:free() + tiptr:free() end return render_userpage Index: route.t ================================================================== --- route.t +++ route.t @@ -160,11 +160,20 @@ end end terra http.timeline(co: &lib.srv.convo, mode: hpath) lib.render.timeline(co,lib.trn(mode.ptr == nil, rstring{ptr=nil}, mode.ptr[1])) - return +end + +terra http.documentation(co: &lib.srv.convo, path: hpath) + if path.ct == 2 then + lib.render.docpage(co,path(1)) + elseif path.ct == 1 then + lib.render.docpage(co, rstring.null()) + else + co:complain(404, 'no such documentation', 'invalid documentation URL') + end end do local branches = quote end local filename, flen = symbol(&int8), symbol(intptr) local page = symbol(lib.http.page) @@ -205,11 +214,10 @@ -- entry points terra r.dispatch_http(co: &lib.srv.convo, uri: lib.mem.ptr(int8), meth: method.t) lib.dbg('handling URI of form ', {uri.ptr,uri.ct}) co.navbar = lib.render.nav(co) - lib.dbg('got nav ', {co.navbar.ptr,co.navbar.ct}, "||", co.navbar.ptr) -- some routes are non-hierarchical, and can be resolved with a simple strcmp -- we run through those first before giving up and parsing the URI if uri.ptr[0] ~= @'/' then co:complain(404, 'what the hell', 'how did you do that') return @@ -253,12 +261,15 @@ var path = lib.http.hier(uri) defer path:free() if path.ptr[0]:cmp(lib.str.lit('user')) then http.actor_profile_uid(co, path, meth) elseif path.ptr[0]:cmp(lib.str.lit('tl')) then http.timeline(co, path) + elseif path.ptr[0]:cmp(lib.str.lit('doc')) then + if meth ~= method.get and meth ~= method.head then goto wrongmeth end + http.documentation(co, path) else goto notfound end return end ::wrongmeth:: co:complain(405, 'method not allowed', 'that method is not meaningful for this endpoint') do return end ::notfound:: co:complain(404, 'not found', 'no such resource available') do return end end Index: schema.sql ================================================================== --- schema.sql +++ schema.sql @@ -60,22 +60,26 @@ drop table if exists parsav_rights cascade; create table parsav_rights ( key text, actor bigint references parsav_actors(id) on delete cascade, - allow boolean, + allow boolean not null, + scope bigint, -- for future expansion primary key (key,actor) ); insert into parsav_actors (handle,rank,quota) values (:'admin',1,0); insert into parsav_rights (actor,key,allow) select (select id from parsav_actors where handle=:'admin'), a.column1, a.column2 from (values - ('ban',true), + ('purge',true), ('config',true), ('censor',true), ('suspend',true), + ('cred',true), + ('elevate',true), + ('demote',true), ('rebrand',true) ) as a; drop table if exists parsav_posts cascade; create table parsav_posts ( Index: srv.t ================================================================== --- srv.t +++ srv.t @@ -1,8 +1,9 @@ -- vim: ft=terra -local util = dofile 'common.lua' +local util = lib.util local secmode = lib.enum { 'public', 'private', 'lockdown', 'isolate' } +local pstring = lib.mem.ptr(int8) local struct srv local struct cfgcache { secret: lib.mem.ptr(int8) instance: lib.mem.ptr(int8) overlord: &srv @@ -50,21 +51,22 @@ break end end local r = symbol(rt) + local succ = label() if tk == primary then return quote var [r] for i=0,self.sources.ct do var src = self.sources.ptr + i if src.handle ~= nil and src.backend.[meth] ~= nil then r = src:[meth]([expr]) - goto success + goto [succ] end end lib.bail(['no active backends provide critical capability ' .. meth .. '!']) - ::success::; + ::[succ]::; in r end else local ok, empty if tk == ptr then ok = `r.ptr ~= nil empty = `[rt]{ptr=nil,ct=0} @@ -165,10 +167,32 @@ }) body.title:free() body.body:free() end + +struct convo.page { + title: pstring + body: pstring + class: pstring +} + +terra convo:stdpage(pg: convo.page) + var doc = data.view.docskel { + instance = self.srv.cfg.instance; + title = pg.title; + body = pg.body; + class = pg.class; + navlinks = self.navbar; + } + + var hdrs = array( + lib.http.header { 'Content-Type', 'text/html; charset=UTF-8' } + ) + + doc:send(self.con,200,[lib.mem.ptr(lib.http.header)] {ct = [hdrs.type.N], ptr = &hdrs[0]}) +end -- CALL ONLY ONCE PER VAR terra convo:postv(name: rawstring) if self.varbuf.ptr == nil then self.varbuf = lib.mem.heapa(int8, self.msg.body.len + self.msg.query.len) @@ -346,11 +370,14 @@ end ::nocookie::; end if co.aid ~= 0 then var sess, usr = co.srv:actor_session_fetch(co.aid, peer) - if sess.ok == false then co.aid = 0 else co.who = usr.ptr end + if sess.ok == false then co.aid = 0 else + co.who = usr.ptr + co.who.rights.powers = server:actor_powers_fetch(co.who.id) + end end var uridec = lib.mem.heapa(int8, msg.uri.len) defer uridec:free() var urideclen = lib.net.mg_url_decode(msg.uri.ptr, msg.uri.len, uridec.ptr, uridec.ct, 1) @@ -483,19 +510,57 @@ terra srv:actor_auth_how(ip: lib.store.inet, usn: rawstring) var cs: lib.store.credset cs:clear() var ok = false for i=0,self.sources.ct do - var set, iok = self.sources.ptr[i]:actor_auth_how(ip, usn) + var set, iok = self.sources(i):actor_auth_how(ip, usn) if iok then cs = cs + set ok = iok end end return cs, ok end +terra srv:actor_auth_pw(ip: lib.store.inet, user: pstring, pw: pstring): uint64 + for i=0,self.sources.ct do + if self.sources(i).backend ~= nil and + self.sources(i).backend.actor_auth_pw ~= nil then + var aid,uid,newhnd = self.sources(i):actor_auth_pw(ip,user,pw) + if aid ~= 0 then + if uid == 0 then + lib.dbg('new user just logged in, creating account entry') + var kbuf: uint8[lib.crypt.const.maxdersz] + var newkp = lib.crypt.genkp() + var privsz = lib.crypt.der(false,&newkp,&kbuf[0]) + var na = lib.store.actor { + id = 0; nym = nil; handle = newhnd.ptr; + origin = 0; bio = nil; avatar = nil; + knownsince = lib.osclock.time(nil); + rights = lib.store.rights_default(); + title = nil, key = [lib.mem.ptr(uint8)] { + ptr = &kbuf[0], ct = privsz + }; + } + var newuid: uint64 + if self.sources(i).backend.actor_create ~= nil then + newuid = self.sources(i):actor_create(&na) + else newuid = self:actor_create(&na) end + + if self.sources(i).backend.actor_auth_register_uid ~= nil then + self.sources(i):actor_auth_register_uid(aid,newuid) + end + end + return aid + end + end + end + + return 0 +end + +--9twh8y94i5c1qqr7hxu20fyd terra cfgcache.methods.load :: {&cfgcache} -> {} terra cfgcache:init(o: &srv) self.overlord = o self:load() end Index: static/style.scss ================================================================== --- static/style.scss +++ static/style.scss @@ -214,10 +214,11 @@ min-height: calc(100vh - 1.1in); margin-top: 0; margin-bottom: 0; padding: 0 0.4in; padding-top: 1.1in; + padding-bottom: 0.1in; background-color: adjust-color($color, $lightness: -45%, $alpha: 0.4); border: { left: 1px solid black; right: 1px solid black; } @@ -465,5 +466,11 @@ } a[href].rawlink { @extend %teletype; } + +body.doc main { + @extend %serif; + li { margin-top: 0.05in; } + li:first-child { margin-top: 0; } +} Index: store.t ================================================================== --- store.t +++ store.t @@ -32,11 +32,12 @@ return self.purge() or self.censor() or self.suspend() or self.elevate() or self.demote() or self.rebrand() or self.cred() end -local str = rawstring --lib.mem.ptr(int8) +local str = rawstring +local pstr = lib.mem.ptr(int8) struct m.source struct m.rights { rank: uint16 -- lower = more powerful except 0 = regular user @@ -63,10 +64,11 @@ id: uint64 nym: str handle: str origin: uint64 bio: str + title: str avatar: str knownsince: m.timepoint rights: m.rights key: lib.mem.ptr(uint8) @@ -171,12 +173,12 @@ conf_get: {&m.source, rawstring} -> lib.mem.ptr(int8) conf_set: {&m.source, rawstring, rawstring} -> {} conf_reset: {&m.source, rawstring} -> {} - actor_save: {&m.source, m.actor} -> bool - actor_create: {&m.source, m.actor} -> bool + actor_save: {&m.source, &m.actor} -> bool + actor_create: {&m.source, &m.actor} -> uint64 actor_fetch_xid: {&m.source, lib.mem.ptr(int8)} -> lib.mem.ptr(m.actor) actor_fetch_uid: {&m.source, uint64} -> lib.mem.ptr(m.actor) actor_notif_fetch_uid: {&m.source, uint64} -> lib.mem.ptr(m.notif) actor_enum: {&m.source} -> lib.mem.ptr(&m.actor) actor_enum_local: {&m.source} -> lib.mem.ptr(&m.actor) @@ -185,31 +187,41 @@ actor_auth_how: {&m.source, m.inet, rawstring} -> {m.credset, bool} -- returns a set of auth method categories that are available for a -- given user from a certain origin -- origin: inet -- username: rawstring - actor_auth_otp: {&m.source, m.inet, rawstring, rawstring} -> uint64 - actor_auth_pw: {&m.source, m.inet, lib.mem.ptr(int8), lib.mem.ptr(int8) } -> uint64 + actor_auth_otp: {&m.source, m.inet, rawstring, rawstring} + -> {uint64, uint64, pstr} + actor_auth_pw: {&m.source, m.inet, lib.mem.ptr(int8), lib.mem.ptr(int8) } + -> {uint64, uint64, pstr} -- handles password-based logins against hashed passwords -- origin: inet -- handle: rawstring -- token: rawstring - actor_auth_tls: {&m.source, m.inet, rawstring} -> uint64 + actor_auth_tls: {&m.source, m.inet, rawstring} + -> {uint64, uint64, pstr} -- handles implicit authentication performed as part of an TLS connection -- origin: inet -- fingerprint: rawstring actor_auth_api: {&m.source, m.inet, rawstring, rawstring} -> uint64 + -> {uint64, uint64, pstr} -- handles API authentication -- origin: inet -- handle: rawstring -- key: rawstring (X-API-Key) actor_auth_record_fetch: {&m.source, uint64} -> lib.mem.ptr(m.auth) + actor_powers_fetch: {&m.source, uint64} -> m.powerset actor_session_fetch: {&m.source, uint64, m.inet} -> {lib.stat(m.auth), lib.mem.ptr(m.actor)} -- retrieves an auth record + actor combo suitable by AID suitable -- for determining session validity & caps -- aid: uint64 -- origin: inet + actor_auth_register_uid: {&m.source, uint64, uint64} -> {} + -- notifies the backend module of the UID that has been assigned for + -- an authentication ID + -- aid: uint64 + -- uid: uint64 actor_conf_str: cnf(rawstring, lib.mem.ptr(int8)) actor_conf_int: cnf(intptr, lib.stat(intptr)) post_save: {&m.source, &m.post} -> {} Index: str.t ================================================================== --- str.t +++ str.t @@ -1,8 +1,8 @@ -- vim: ft=terra -- string.t: string classes -local util = dofile('common.lua') +local util = lib.util local pstr = lib.mem.ptr(int8) local pref = lib.mem.ref(int8) local m = { sz = terralib.externfunction('strlen', rawstring -> intptr); @@ -176,10 +176,12 @@ end) m.acc.methods.lpush = macro(function(self,str) return `self:push([str:asvalue()], [#(str:asvalue())]) end) m.acc.methods.ppush = terra(self: &m.acc, str: lib.mem.ptr(int8)) + self:push(str.ptr, str.ct) return self end; +m.acc.methods.rpush = terra(self: &m.acc, str: lib.mem.ref(int8)) self:push(str.ptr, str.ct) return self end; m.acc.methods.merge = terra(self: &m.acc, str: lib.mem.ptr(int8)) self:push(str.ptr, str.ct) str:free() return self end; m.acc.methods.compose = macro(function(self, ...) local minlen = 0 Index: tpl.t ================================================================== --- tpl.t +++ tpl.t @@ -1,11 +1,11 @@ -- vim: ft=terra -- string template generator: -- returns a function that fills out a template -- with the strings given -local util = dofile 'common.lua' +local util = lib.util local m = {} function m.mk(tplspec) local str if type(tplspec) == 'string' then str = tplspec tplspec = {} @@ -23,29 +23,44 @@ if magic[c] then return '%' .. c end end) local last = 1 local fields = {} local segs = {} + local docs = {} local constlen = 0 -- strip out all irrelevant whitespace to tidy things up -- TODO: find way to exclude
 tags?
 	str = str:gsub('[\n^]%s+','')
 	str = str:gsub('%s+[\n$]','')
 	str = str:gsub('\n','')
 	str = str:gsub(' ?', file)
+	end)
 	for start, key, stop in string.gmatch(str,'()'..tplchar..'(%w+)()') do
 		if string.sub(str,start-1,start-1) ~= '\\' then
 			segs[#segs+1] = string.sub(str,last,start-1)
 			fields[#segs] = key
 			last = stop
 		end
 	end
 	segs[#segs+1] = string.sub(str,last)
+
 	for i, s in ipairs(segs) do
 		segs[i] = string.gsub(s, '\\'..tplchar, tplchar_o)
 		constlen = constlen + string.len(segs[i])
 	end
+
+	for n,d in pairs(docs) do
+		local html = string.format(
+			'', n, d.text
+		)
+		segs[#segs] = segs[#segs] .. html
+		constlen = constlen + #html
+	end
+	
 
 	local runningtally = symbol(intptr)
 	local tallyup = {quote
 		var [runningtally] = 1 + constlen
 	end}

Index: view/compose.tpl
==================================================================
--- view/compose.tpl
+++ view/compose.tpl
@@ -1,41 +1,6 @@
 
- - ? + @?acl
- -