| Comment: | permissions work now |
|---|---|
| Downloads: | Tarball | ZIP archive | SQL archive |
| Timelines: | family | ancestors | descendants | both | trunk |
| Files: | files | file ages | folders |
| SHA3-256: |
bbfea467bf467820010635f449f683a1 |
| User & Date: | lexi on 2020-12-27 02:31:30 |
| Other Links: | manifest | tags |
|
2020-12-27
| ||
| 04:08 | look ma, im tweetin check-in: 8f954221a1 user: lexi tags: trunk | |
| 02:31 | permissions work now check-in: bbfea467bf user: lexi tags: trunk | |
|
2020-12-25
| ||
| 23:37 | iteration and important api adjustments check-in: f9559a83fc user: lexi tags: trunk | |
Modified backend/pgsql.t from [1740166796] to [36b1398523].
58 58 where $1::text = (a.handle || '@' || domain) or 59 59 $1::text = ('@' || a.handle || '@' || domain) or 60 60 (a.origin is null and 61 61 $1::text = a.handle or 62 62 $1::text = ('@' || a.handle)) 63 63 ]]; 64 64 }; 65 + 66 + actor_create = { 67 + params = { 68 + rawstring, rawstring, uint64, lib.store.timepoint, 69 + rawstring, rawstring, lib.mem.ptr(uint8), 70 + rawstring, uint16, uint32 71 + }; 72 + sql = [[ 73 + insert into parsav_actors ( 74 + nym,handle, 75 + origin,knownsince, 76 + bio,avataruri,key, 77 + title,rank,quota 78 + ) values ($1::text, $2::text, 79 + case when $3::bigint = 0 then null 80 + else $3::bigint end, 81 + to_timestamp($4::bigint), 82 + $5::bigint, $6::bigint, $7::bytea, 83 + $8::text, $9::smallint, $10::integer 84 + ) returning id 85 + ]]; 86 + }; 87 + 65 88 66 89 actor_auth_pw = { 67 90 params = {pstring,rawstring,pstring,lib.store.inet}, sql = [[ 68 - select a.aid from parsav_auth as a 91 + select a.aid, a.uid, a.name from parsav_auth as a 69 92 left join parsav_actors as u on u.id = a.uid 70 93 where (a.uid is null or u.handle = $1::text or ( 71 94 a.uid = 0 and a.name = $1::text 72 95 )) and 73 96 (a.kind = 'trust' or (a.kind = $2::text and a.cred = $3::bytea)) and 74 97 (a.netmask is null or a.netmask >> $4::inet) 75 98 order by blacklist desc limit 1 ................................................................................ 161 184 left join parsav_actors a on au.uid = a.id 162 185 left join parsav_servers s on a.origin = s.id 163 186 164 187 where au.aid = $1::bigint and au.blacklist = false and 165 188 (au.netmask is null or au.netmask >> $2::inet) 166 189 ]]; 167 190 }; 191 + 192 + actor_powers_fetch = { 193 + params = {uint64}, sql = [[ 194 + select key, allow from parsav_rights where actor = $1::bigint 195 + ]] 196 + }; 168 197 169 198 post_create = { 170 199 params = {uint64, rawstring, rawstring, rawstring}, sql = [[ 171 200 insert into parsav_posts ( 172 201 author, subject, acl, body, 173 202 posted, discovered, 174 203 circles, mentions ................................................................................ 213 242 terra pqr:len(row: intptr, col: intptr) 214 243 return lib.pq.PQgetlength(self.res, row, col) 215 244 end 216 245 terra pqr:cols() return lib.pq.PQnfields(self.res) end 217 246 terra pqr:string(row: intptr, col: intptr) -- not to be exported!! 218 247 if self:null(row,col) then return nil end 219 248 var v = lib.pq.PQgetvalue(self.res, row, col) 220 --- var r: lib.mem.ptr(int8) 221 --- r.ct = lib.str.sz(v) 222 --- r.ptr = v 223 249 return v 250 +end 251 +terra pqr:_string(row: intptr, col: intptr) -- not to be exported!! 252 + if self:null(row,col) then return pstring.null() end 253 + return pstring { 254 + ptr = lib.pq.PQgetvalue (self.res, row, col); 255 + ct = lib.pq.PQgetlength(self.res, row, col); 256 + } 224 257 end 225 258 terra pqr:bin(row: intptr, col: intptr) -- not to be exported!! DO NOT FREE 259 + if self:null(row,col) then return [lib.mem.ptr(uint8)].null() end 226 260 return [lib.mem.ptr(uint8)] { 227 261 ptr = [&uint8](lib.pq.PQgetvalue(self.res, row, col)); 228 262 ct = lib.pq.PQgetlength(self.res, row, col); 229 263 } 230 264 end 231 265 terra pqr:String(row: intptr, col: intptr) -- suitable to be exported 232 - if self:null(row,col) then return [lib.mem.ptr(int8)] {ptr=nil,ct=0} end 233 - var s = [lib.mem.ptr(int8)] { ptr = lib.str.dup(self:string(row,col)) } 234 - s.ct = lib.pq.PQgetlength(self.res, row, col) 266 + if self:null(row,col) then return pstring.null() end 267 + var s = pstring { 268 + ptr = lib.str.dup(self:string(row,col)); 269 + ct = lib.pq.PQgetlength(self.res, row, col); 270 + } 235 271 return s 236 272 end 237 273 terra pqr:bool(row: intptr, col: intptr) 238 274 var v = lib.pq.PQgetvalue(self.res, row, col) 239 275 if @v == 0x01 then return true else return false end 240 276 end 241 277 terra pqr:cidr(row: intptr, col: intptr) ................................................................................ 421 457 else 422 458 a.ptr.key = r:bin(row,8) 423 459 end 424 460 if r:null(row,3) then a.ptr.origin = 0 425 461 else a.ptr.origin = r:int(uint64,row,3) end 426 462 return a 427 463 end 464 + 465 +local privmap = {} 466 +do local struct pt { name:pstring, priv:lib.store.powerset } 467 +for k,v in pairs(lib.store.powerset.members) do 468 + privmap[#privmap + 1] = quote 469 + var ps: lib.store.powerset ps:clear() 470 + (ps.[v] << true) 471 + in pt {name = lib.str.plit(v), priv = ps} end 472 +end end 428 473 429 474 local checksha = function(src, hash, origin, username, pw) 430 475 local validate = function(kind, cred, credlen) 431 476 return quote 432 477 var r = queries.actor_auth_pw.exec( 433 478 [&lib.store.source](src), 434 479 username, 435 480 kind, 436 481 [lib.mem.ptr(int8)] {ptr=[&int8](cred), ct=credlen}, 437 482 origin) 438 483 if r.sz > 0 then -- found a record! stop here 439 484 var aid = r:int(uint64, 0,0) 485 + var uid = r:int(uint64, 0,1) 486 + var name = r:String(0,2) 440 487 r:free() 441 - return aid 488 + return aid, uid, name 442 489 end 443 490 end 444 491 end 445 492 446 493 local out = symbol(uint8[64]) 447 494 local vdrs = {} 448 495 ................................................................................ 567 614 end]; 568 615 569 616 actor_auth_pw = [terra( 570 617 src: &lib.store.source, 571 618 ip: lib.store.inet, 572 619 username: lib.mem.ptr(int8), 573 620 cred: lib.mem.ptr(int8) 574 - ): uint64 621 + ): {uint64, uint64, pstring} 575 622 576 623 [ checksha(`src, 256, ip, username, cred) ] -- most common 577 624 [ checksha(`src, 512, ip, username, cred) ] -- most secure 578 625 [ checksha(`src, 384, ip, username, cred) ] -- weird 579 626 [ checksha(`src, 224, ip, username, cred) ] -- weirdest 580 627 581 628 -- TODO: check pbkdf2-hmac 582 629 -- TODO: check OTP 583 - return 0 630 + return 0, 0, pstring.null() 584 631 end]; 585 632 586 633 actor_stats = [terra(src: &lib.store.source, uid: uint64) 587 634 var r = queries.actor_stats.exec(src, uid) 588 635 if r.sz == 0 then lib.bail('error fetching actor stats!') end 589 636 var s: lib.store.actor_stats 590 637 s.posts = r:int(uint64, 0, 0) ................................................................................ 652 699 end 653 700 654 701 var ret: lib.mem.ptr(lib.mem.ptr(lib.store.post)) ret:init(r.sz) 655 702 for i=0,r.sz do ret.ptr[i] = row_to_post(&r, i) end -- MUST FREE ALL 656 703 657 704 return ret 658 705 end]; 706 + 707 + actor_powers_fetch = [terra( 708 + src: &lib.store.source, 709 + uid: uint64 710 + ): lib.store.powerset 711 + var powers = lib.store.rights_default().powers 712 + var map = array([privmap]) 713 + var r = queries.actor_powers_fetch.exec(src, uid) 714 + 715 + for i=0, r.sz do 716 + for j=0, [map.type.N] do 717 + var pn = r:_string(i,0) 718 + if map[j].name:cmp(pn) then 719 + if r:bool(i,1) 720 + then powers = powers + map[j].priv 721 + else powers = powers - map[j].priv 722 + end 723 + end 724 + end 725 + end 726 + 727 + return powers 728 + end]; 729 + 730 + actor_create = [terra( 731 + src: &lib.store.source, 732 + ac: &lib.store.actor 733 + ): uint64 734 + 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) 735 + if r.sz == 0 then lib.bail('failed to create actor!') end 736 + return r:int(uint64,0,0) 737 + end]; 738 + 739 + actor_auth_register_uid = nil; -- not necessary for view-based auth 659 740 } 660 741 661 742 return b
Modified common.lua from [3d397d42e6] to [e762fc8997].
92 92 for _, v in pairs(b) do a[#a+1] = v end 93 93 end; 94 94 has = function(haystack,needle,eq) 95 95 eq = eq or function(a,b) return a == b end 96 96 for k,v in pairs(haystack) do 97 97 if eq(needle,v) then return k end 98 98 end 99 + end; 100 + keys = function(ary) 101 + local kt = {} 102 + for k,v in pairs(ary) do kt[#kt+1] = k end 103 + return kt 99 104 end; 100 105 ingest = function(f) 101 106 local h = io.open(f, 'r') 102 107 if h == nil then return nil end 103 108 local txt = f:read('*a') f:close() 104 109 return chomp(txt) 105 110 end;
Modified config.lua from [dc89401662] to [29834379ec].
27 27 dist = default('parsav_dist', coalesce( 28 28 os.getenv('NIX_PATH') and 'nixos', 29 29 os.getenv('NIX_STORE') and 'nixos', 30 30 '')); 31 31 tgttrip = default('parsav_arch_triple'); -- target triple, used in xcomp 32 32 tgtcpu = default('parsav_arch_cpu'); -- target cpu, used in xcomp 33 33 tgthf = u.tobool(default('parsav_arch_armhf',true)); 34 + doc = { 35 + online = u.tobool(default('parsav_online_documentation',true)); 36 + offline = u.tobool(default('parsav_offline_documentation',true)); 37 + }; 34 38 outform = default('parsav_emit_type', 'o'); 35 39 endian = default('parsav_arch_endian', 'little'); 36 40 build = { 37 41 id = u.rndstr(6); 38 42 release = u.ingest('release'); 39 43 when = os.date(); 40 44 }; ................................................................................ 44 48 embeds = { 45 49 {'style.css', 'text/css'}; 46 50 {'default-avatar.webp', 'image/webp'}; 47 51 {'padlock.webp', 'image/webp'}; 48 52 {'warn.webp', 'image/webp'}; 49 53 }; 50 54 } 55 +if os.getenv('parsav_let_me_be_an_idiot') == "i know what i'm doing" then 56 + conf.braingeniousmode = true -- SOUND GENERAL QUARTERS 57 +end 51 58 if u.ping '.fslckout' or u.ping '_FOSSIL_' then 52 59 if u.ping '_FOSSIL_' then default_os = 'windows' end 53 60 conf.build.branch = u.exec { 'fossil', 'branch', 'current' } 54 61 conf.build.checkout = (u.exec { 'fossil', 'sql', 55 62 [[select value from localdb.vvar where name = 'checkout-hash']] 56 63 }):gsub("^'(.*)'$", '%1') 57 64 end
Modified crypt.t from [48369b50e0] to [bf3957f4f4].
7 7 rawcode = terra(code: int) 8 8 if code < 0 then code = -code end 9 9 return code and 0xFF80 10 10 end; 11 11 toobig = -lib.pk.MBEDTLS_ERR_RSA_OUTPUT_TOO_LARGE; 12 12 } 13 13 const.maxpemsz = math.floor((const.keybits / 8)*6.4) + 128 -- idk why this formula works but it basically seems to 14 +const.maxdersz = const.maxpemsz -- FIXME this is a safe value but obvs not the correct one 14 15 15 16 local ctx = lib.pk.mbedtls_pk_context 16 17 17 18 local struct hashalg { id: uint8 bytes: intptr } 18 19 local m = { 19 20 pemfile = uint8[const.maxpemsz]; 21 + const = const; 20 22 algsz = { 21 23 sha1 = 160/8; 22 24 sha256 = 256/8; 23 25 sha512 = 512/8; 24 26 sha384 = 384/8; 25 27 sha224 = 224/8; 26 28 } ................................................................................ 61 63 terra m.pem(pub: bool, key: &ctx, buf: &uint8): bool 62 64 if pub then 63 65 return lib.pk.mbedtls_pk_write_pubkey_pem(key, buf, const.maxpemsz) == 0 64 66 else 65 67 return lib.pk.mbedtls_pk_write_key_pem(key, buf, const.maxpemsz) == 0 66 68 end 67 69 end 70 + 71 +terra m.der(pub: bool, key: &ctx, buf: &uint8): intptr 72 + if pub then 73 + return lib.pk.mbedtls_pk_write_pubkey_der(key, buf, const.maxdersz) 74 + else 75 + return lib.pk.mbedtls_pk_write_key_der(key, buf, const.maxdersz) 76 + end 77 +end 68 78 69 79 m.destroy = lib.dispatch { 70 80 [ctx] = function(v) return `lib.pk.mbedtls_pk_free(&v) end; 71 81 72 82 [false] = function(ptr) return `ptr:free() end; 73 83 } 74 84
Added doc/acl.md version [ea4e893c12].
1 +# access control 2 + 3 +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. 4 + 5 +* **all**: matches any and all users 6 +* **local**: matches users who belong to this instance 7 +* **mutuals**: matches users you follow who also follow you 8 +* **followed**: matches users you follow 9 +* **followers**: matches users who follow you 10 +* **groupies**: matches users who follow you, but whom you do not follow 11 +* **mentioned**: matches users who are mentioned in the post 12 +* **staff**: matches instance staff (equivalent to `~%0`) 13 +* **admin**: matches the individual named as the instance administrator, if any 14 +* **@**`handle`: matches the user `handle` 15 +* **+**`circle`: matches users you have categorized under `circle` 16 +* **#**`room`: matches users who are members of `room` 17 +* **%**`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 18 +* **#**`room`**%**`rank`: matches users who hold `rank` in `room` 19 +* **<**`title`**>**: matches peers of the net who have been created `title` by the sovereign 20 +* **#**`room`**<**`title`**>**: matches peers of the chat who have been created `title` by `room` staff 21 + 22 +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. 23 + 24 +**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. 25 + 26 +expressions must contain at least one term to be valid. if they consist only of policy keywords, they will be rejected. 27 + 28 +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. 29 + 30 +* `deny groupies allow +illuminati`: permits access to the illuminati, but excluding those members who are groupies 31 +* `+illuminati deny groupies`: allows access to everyone but groupies (unless they're in the illuminati) 32 +* `@eve @alice@nowhere.tld deny @bob @trent@witches.live`: grants access to eve and alice, but locks out bob and trent 33 +* `<grand duke> #4th-intl<comrade>`: restricts the post to the eyes of the Fourth International's secret cabal of anointed comrades and the grand dukes of the Empire 34 +* `deny ~%3`: blocks a post from being seen by anyone with a staff rank level below 3 35 + 36 +**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 version [2a91b27d48].
1 +# daemon invocation
Added doc/load.lua version [68f6a9a498].
1 +local path = ... 2 +local sources = { 3 +-- user section 4 + acl = {title = 'access control lists'}; 5 +-- admin section 6 + --overview = {title = 'server overview', priv = 'config'}; 7 + invocation = {title = 'daemon invocation', priv = 'config'}; 8 + --backends = {title = 'storage backends', priv = 'config'}; 9 + --pgsql = {title = 'pgsql', priv = 'config', parent = 'backends'}; 10 +} 11 + 12 +local util = dofile 'common.lua' 13 +local ingest = function(filename) 14 + return (util.exec { 'cmark', '--smart', '--unsafe', (path..'/'..filename) }):gsub('\n','') 15 +end 16 + 17 +local doc = {} 18 +for n,meta in pairs(sources) do doc[n] = { 19 + name = n; 20 + text = ingest(n .. '.md'); 21 + meta = meta; 22 +} end 23 +return doc
Modified http.t from [654249752e] to [7092b409fb].
1 1 -- vim: ft=terra 2 2 local m = {} 3 -local util = dofile('common.lua') 3 +local util = lib.util 4 4 5 5 m.method = lib.enum { 'get', 'post', 'head', 'options', 'put', 'delete' } 6 6 m.mime = lib.enum { 7 7 'html'; -- default 8 8 'json'; 9 9 'mkdown'; 10 10 'text';
Modified mem.t from [15de92d877] to [a177326f1c].
52 52 {'ct', intptr}; 53 53 } 54 54 t.ptr_basetype = ty 55 55 local recurse = false 56 56 --if ty:isstruct() then 57 57 --if ty.methods.free then recurse = true end 58 58 --end 59 - t.metamethods.__not = macro(function(self) 60 - return `self.ptr 61 - end) 62 59 if dyn then 63 60 t.methods = { 64 61 free = terra(self: &t): bool 65 62 [recurse and quote 66 63 self.ptr:free() 67 64 end or {}] 68 65 if self.ct > 0 then ................................................................................ 104 101 end 105 102 end 106 103 terra t:advance(n: intptr) 107 104 self.ptr = self.ptr + n 108 105 self.ct = self.ct - n 109 106 return self.ptr 110 107 end 108 + terra t.methods.null(): t return t { ptr = nil, ct = 0 } end -- maybe should be a macro? 109 + terra t:ref() return self.ptr ~= nil end 110 + t.metamethods.__not = macro(function(self) return `not self:ref() end) 111 + t.metamethods.__apply = macro(function(self,idx) return `self.ptr[idx] end) 111 112 if not ty:isstruct() then 112 113 terra t:cmp_raw(other: &ty) 113 114 for i=0, self.ct do 114 115 if self.ptr[i] ~= other[i] then return false end 115 116 end 116 117 return true 117 118 end
Modified parsav.md from [eb5d145ae6] to [ae23203c4b].
5 5 ## backends 6 6 parsav is designed to be storage-agnostic, and can draw data from multiple backends at a time. backends can be enabled or disabled at compile time to avoid unnecessary dependencies. 7 7 8 8 * postgresql 9 9 10 10 ## dependencies 11 11 12 -* mongoose 13 -* json-c 14 -* mbedtls 15 -* **postgresql backend:** 16 - * postgresql-libs 12 +* runtime 13 + * mongoose 14 + * json-c 15 + * mbedtls 16 + * **postgresql backend:** 17 + * postgresql-libs 18 +* compile-time 19 + * 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` 20 + * 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` 17 21 18 -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: 22 +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: 19 23 20 24 * inkscape, for rendering out UI graphics 21 25 * cwebp (libwebp package), for transforming inkscape PNGs to webp 22 26 * sassc, for compiling the SCSS stylesheet into its final CSS 23 27 24 28 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. 25 29 ................................................................................ 28 32 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. 29 33 30 34 ## building 31 35 32 36 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. 33 37 34 38 postgresql-libs must be installed systemwide, as `parsav` does not currently provide for statically compiling and linking it 39 + 40 +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. 35 41 36 42 ## configuring 37 43 38 44 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. 39 45 40 46 eventually, we'll add a command-line tool `parsav-cfg` to enable easy modification of the configuration store from the command line; for now, you'll need to modify the database by hand or use the online administration menu. the schema.sql file contains commands to prompt for various important values like the name of your administrative user. 41 47 ................................................................................ 64 70 netmask cidr, 65 71 blacklist bool 66 72 ) 67 73 68 74 `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. 69 75 70 76 ## authentication 71 -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. 77 +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. 72 78 73 79 `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`. 74 80 75 -below is a full list of authentication types we intend to support. a checked box indicates the scheme has been implemented. 81 +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. 76 82 77 83 * ☑ pw-sha{512,384,256,224}: an ordinary password, hashed with the appropriate algorithm 78 84 * ☐ 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"`) 79 85 * ☐ pw-pbkdf2-hmac-sha{…}: a password hashed with the Password-Based Key Derivation Function 2 instead of plain SHA2 86 +* ☐ pw-extern-ldap: try to authenticate by binding against an LDAP server 87 +* ☐ pw-extern-cyrus: try to authenticate against saslauthd 88 +* ☐ pw-extern-dovecot: try to authenticate against a dovecot SASL socket 89 +* ☐ pw-extern-krb5: abuse MIT kerberos as a password verifier 90 +* ☐ pw-extern-imap: abuse an email server as a password verifier 91 +* (extra credit) ☐ pw-extern-radius: verify a user against a radius server 80 92 * ☐ 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`. 81 93 * ☐ 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 82 94 * ☐ tls-cert-fp: a fingerprint of a client certificate 83 95 * ☐ 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 84 96 * ☐ 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. 85 97 * ☐ challenge-ecc-sha256: a Curve25519 public key. the user is presented with a challenge and must sign it with the corresponding private key using SHA256. 86 98 * ☐ challenge-ecc448-sha256: a Curve448 public key. the user is presented with a challenge and must sign it with the corresponding private key using SHA256. ................................................................................ 95 107 parsav needs more storage backends, as it currently supports only postgres. some possibilities, in order of priority, are: 96 108 97 109 * plain text/filesystem storage 98 110 * lmdb 99 111 * sqlite3 100 112 * generic odbc 101 113 * lua 102 -* ldap?? possibly just for users 114 +* ldap for auth (and maybe actors?) 103 115 * cdb (for static content, maybe?) 104 116 * mariadb/mysql 105 117 * the various nosql horrors, e.g. redis, mongo, and so on
Modified parsav.t from [579976ae23] to [8c471232dd].
1 1 -- vim: ft=terra 2 2 3 3 local util = dofile('common.lua') 4 4 local buildopts, buildargs = util.parseargs{...} 5 5 config = dofile('config.lua') 6 6 7 7 lib = { 8 - init = {}; 8 + init = {}, util = util; 9 9 load = function(lst) 10 10 for _, l in pairs(lst) do 11 11 local path = {} 12 12 for m in l:gmatch('([^:]+)') do path[#path+1]=m end 13 13 local tgt = lib 14 14 for i=1,#path-1 do 15 15 if tgt[path[i]] == nil then tgt[path[i]] = {} end ................................................................................ 45 45 local str = tostring(v:asvalue()) 46 46 code[#code+1] = `lib.io.send(2, str, [#str]) 47 47 else 48 48 code[#code+1] = quote var n = v in 49 49 lib.io.send(2, n, lib.str.sz(n)) end 50 50 end 51 51 end 52 - if nl then code[#code+1] = `lib.io.send(fd, '\n', 1) end 52 + if nl == true then code[#code+1] = `lib.io.send(fd, '\n', 1) 53 + elseif nl then code[#code+1] = `lib.io.send(fd, nl, [#nl]) end 53 54 return code 54 55 end; 55 56 emitv = function(nl,fd,...) 56 57 local vec = {} 57 58 local defs = {} 58 59 for i,v in ipairs{...} do 59 60 local str, ct ................................................................................ 73 74 else--if v.tree:is 'constant' then 74 75 str = tostring(v:asvalue()) 75 76 end 76 77 ct = ct or #str 77 78 end 78 79 vec[#vec + 1] = `[lib.uio.iovec]{iov_base = [&opaque](str), iov_len = ct} 79 80 end 80 - if nl then vec[#vec + 1] = `[lib.uio.iovec]{iov_base = [&opaque]('\n'), iov_len = 1} end 81 + if nl == true then vec[#vec + 1] = `[lib.uio.iovec]{iov_base = [&opaque]('\n'), iov_len = 1} 82 + elseif nl then vec[#vec + 1] = `[lib.uio.iovec]{iov_base = [&opaque](nl), iov_len = [#nl]} end 81 83 return quote 82 84 [defs] 83 85 var strs = array( [vec] ) 84 86 in lib.uio.writev(fd, strs, [#vec]) end 85 87 end; 86 88 trn = macro(function(cond, i, e) 87 89 return quote ................................................................................ 174 176 var dfs = arrayof(int8, 0x30 + diff/10, 0x30 + diff%10, 0x20, 0) 175 177 [ lib.emit(false, 2, ' \27[36m+', `&dfs[0]) ] 176 178 end 177 179 end 178 180 179 181 local defrep = function(level,n,code) 180 182 return macro(function(...) 181 - local q = lib.emit(true, 2, noise_header(code,n), ...) 183 + local fn = (...).filename 184 + local ln = tostring((...).linenumber) 185 + local dbgtag = string.format('\27[35m · \27[34m%s:\27[1m%s\27[m\n', fn,ln) 186 + local q = lib.emit(level < 3 and true or dbgtag, 2, noise_header(code,n), ...) 182 187 return quote if noise >= level then timehdr(); [q] end end 183 188 end); 184 189 end 185 190 lib.dbg = defrep(3,'debug', '32') 186 191 lib.report = defrep(2,'info', '35') 187 192 lib.warn = defrep(1,'warn', '33') 188 193 lib.bail = macro(function(...) ................................................................................ 260 265 local q = quote var [new] new:clear() end 261 266 for i = 0, bytes - 1 do 262 267 q = quote [q] 263 268 new._store[i] = self._store[i] or other._store[i] 264 269 end 265 270 end 266 271 return quote [q] in new end 272 + end) 273 + set.metamethods.__and = macro(function(self,other) 274 + local new = symbol(set) 275 + local q = quote var [new] new:clear() end 276 + for i = 0, bytes - 1 do 277 + q = quote [q] 278 + new._store[i] = self._store[i] and other._store[i] 279 + end 280 + end 281 + return quote [q] in new end 282 + end) 283 + set.metamethods.__not = macro(function(self) 284 + local new = symbol(set) 285 + local q = quote var [new] new:clear() end 286 + for i = 0, bytes - 1 do 287 + q = quote [q] 288 + new._store[i] = not self._store[i] 289 + end 290 + end 291 + return quote [q] in new end 292 + end) 293 + set.metamethods.__sub = macro(function(self,other) 294 + local new = symbol(set) 295 + local q = quote var [new] new:clear() end 296 + for i = 0, bytes - 1 do 297 + q = quote [q] 298 + new._store[i] = self._store[i] and (not other._store[i]) 299 + end 300 + end 301 + return quote [q] in new end 267 302 end) 268 303 bit.metamethods.__cast = function(from,to,e) 269 304 local q = quote var s = e 270 305 in (s._set._store[s._v/8] and (1 << s._v % 8)) end 271 306 if to == bit then error('casting to bit is not meaningful') 272 307 elseif to == bool then return `([q] ~= 0) 273 308 elseif to:isintegral() then return q ................................................................................ 310 345 311 346 lib.cmdparse = terralib.loadfile('cmdparse.t')() 312 347 313 348 do local collate = function(path,f, ...) 314 349 return loadfile(path..'/'..f..'.lua')(path, ...) 315 350 end 316 351 data = { 352 + doc = collate('doc','load'); 317 353 view = collate('view','load'); 318 354 static = {}; 319 355 stmap = global(lib.mem.ref(int8)[#config.embeds]); -- array of pointers to static content 320 356 } end 321 357 for i,e in ipairs(config.embeds) do local v = e[1] 322 358 local fh = io.open('static/' .. v,'r') 323 359 if fh == nil then error('static file ' .. v .. ' missing') end ................................................................................ 337 373 'render:nav'; 338 374 'render:login'; 339 375 'render:profile'; 340 376 'render:userpage'; 341 377 'render:compose'; 342 378 'render:tweet'; 343 379 'render:timeline'; 380 + 'render:docpage'; 344 381 'route'; 345 382 } 346 383 347 384 do 348 385 local p = string.format('parsav: %s\nbuilt on %s\n', config.build.str, config.build.when) 349 386 terra version() lib.io.send(1, p, [#p]) end 350 387 end
Added render/docpage.t version [7e1168b387].
1 +-- vim: ft=terra 2 +local page = lib.srv.convo.page 3 +local pstr = lib.mem.ptr(int8) 4 +local pref = lib.mem.ref(int8) 5 +local P = lib.str.plit 6 +local R = lib.str.lit 7 + 8 +local topics = lib.util.keys(data.doc) 9 +local topicidxt = {} 10 +table.sort(topics) -- because deterministic builds are good 11 +local branches = {} 12 +for i,k in pairs(topics) do 13 + topicidxt[k] = i 14 + local par = data.doc[k].meta.parent 15 + if par then 16 + branches[par] = branches[par] or {} 17 + local br = branches[par] 18 + br[#br+1] = k 19 + end 20 +end 21 + 22 +local struct pgpair { 23 + content:page name:pref title:pref 24 + priv:lib.store.powerset parent:intptr 25 +} 26 +local pages = symbol(pgpair[#topics]) 27 +local allpages = {} 28 + 29 + 30 +for i,v in ipairs(topics) do 31 + local t = data.doc[v] 32 + local par = 0 33 + if t.meta.parent then par = topicidxt[t.meta.parent] end 34 + local restrict = symbol(lib.store.powerset) 35 + local setbits = quote restrict:clear() end 36 + if t.meta.priv then 37 + if type(t.meta.priv) ~= 'table' then t.meta.priv = {t.meta.priv} end 38 + for _,v in pairs(t.meta.priv) do 39 + setbits = quote [setbits]; (restrict.[v] << true) end 40 + end 41 + end 42 + allpages[i] = quote var [restrict]; [setbits] in pgpair { 43 + name = R(v); 44 + parent = par; 45 + priv = restrict; 46 + title = R(t.meta.title); 47 + content = page { 48 + title = ['documentation :: ' .. t.meta.title]; 49 + body = [ t.text ]; 50 + class = P'doc article'; 51 + }; 52 + } end 53 +end 54 + 55 +local terra 56 +showpage(co: &lib.srv.convo, id: pref) 57 + var [pages] = array([allpages]) 58 + for i=0,[pages.type.N] do 59 + if pages[i].name:cmp(id) then 60 + co:stdpage(pages[i].content) 61 + return 62 + end 63 + end -- else 64 + co:complain(404,'not found', 'no help article with that identifier is available') 65 +end 66 + 67 +local terra 68 +pushbranches(list: &lib.str.acc, idx: intptr, ps: lib.store.powerset): {} 69 + var [pages] = array([allpages]) 70 + var started = false 71 + for i=0,[pages.type.N] do 72 + if pages[i].parent == idx+1 and (pages[i].priv:sz() == 0 or 73 + (ps and pages[i].priv):sz() > 0) then 74 + if not started then 75 + started = true 76 + list:lpush('<ul>') 77 + end 78 + list:lpush('<li><a href="/doc/'):rpush(pages[i].name):lpush('">') 79 + :rpush(pages[i].title):lpush('</a>') 80 + pushbranches(list, i, ps) 81 + list:lpush('</li>') 82 + end 83 + end 84 + if started then list:lpush('</ul>') end 85 +end 86 + 87 +local terra 88 +render_docpage(co: &lib.srv.convo, pg: pref) 89 + var nullprivs: lib.store.powerset nullprivs:clear() 90 + if not pg then -- display index 91 + var list: lib.str.acc list:compose('<ul>') 92 + var [pages] = array([allpages]) 93 + for i=0,[pages.type.N] do 94 + if pages[i].parent == 0 and (pages[i].priv:sz() == 0 or 95 + (co.aid ~= 0 and (co.who.rights.powers 96 + and pages[i].priv):sz() > 0)) then 97 + list:lpush('<li><a href="/doc/'):rpush(pages[i].name):lpush('">') 98 + :rpush(pages[i].title):lpush('</a>') 99 + if co.aid ~= 0 then 100 + pushbranches(&list, i, co.who.rights.powers) 101 + else 102 + pushbranches(&list, i, nullprivs) 103 + end 104 + list:lpush('</li>') 105 + end 106 + end 107 + list:lpush('</ul>') 108 + 109 + var bp = list:finalize() 110 + co:stdpage(page { 111 + title = 'documentation'; 112 + body = bp; 113 + class = P'doc listing'; 114 + }) 115 + bp:free() 116 + else showpage(co, pg) end 117 +end 118 + 119 +return render_docpage
Modified render/nav.t from [450b73f673] to [27572ae99b].
3 3 render_nav(co: &lib.srv.convo) 4 4 var t: lib.str.acc t:init(64) 5 5 if co.who ~= nil or co.srv.cfg.pol_sec == lib.srv.secmode.public then 6 6 t:lpush('<a href="/">timeline</a>') 7 7 end 8 8 if co.who ~= nil then 9 9 t:lpush('<a href="/compose">compose</a> <a href="/'):push(co.who.xid,0) 10 - t:lpush('">profile</a> <a href="/conf">configure</a> <a href="/help">help</a> <a href="/logout">log out</a>') 10 + t:lpush('">profile</a> <a href="/conf">configure</a> <a href="/doc">docs</a> <a href="/logout">log out</a>') 11 11 else 12 - t:lpush('<a href="/help">help</a> <a href="/login">log in</a>') 12 + t:lpush('<a href="/doc">docs</a> <a href="/login">log in</a>') 13 13 end 14 14 return t:finalize() 15 15 end 16 16 return render_nav
Modified render/profile.t from [30c7a0b6c6] to [d063b74f92].
15 15 actor.xid, '/chat">chat</a>') 16 16 if co.who.rights.powers:affect_users() then 17 17 aux:push('<a href="/',11):push(actor.xid,0):push('/ctl">control</a>',17) 18 18 end 19 19 auxp = aux:finalize() 20 20 else 21 21 aux:compose('<a href="/', actor.xid, '/follow">remote follow</a>') 22 + auxp = aux:finalize() 22 23 end 23 24 var avistr: lib.str.acc if actor.origin == 0 then 24 25 avistr:compose('/avi/',actor.handle) 25 26 end 26 27 var timestr: int8[26] lib.osclock.ctime_r(&actor.knownsince, ×tr[0]) 27 28 28 29 var strfbuf: int8[28*4]
Modified render/userpage.t from [7b79f9904c] to [9774faf032].
4 4 var ti: lib.str.acc 5 5 if co.aid ~= 0 and co.who.id == actor.id then 6 6 ti:compose('my profile') 7 7 else 8 8 ti:compose('profile :: ', actor.handle) 9 9 end 10 10 var pftxt = lib.render.profile(co,actor) defer pftxt:free() 11 - 12 - var doc = data.view.docskel { 13 - instance = co.srv.cfg.instance; 14 - title = ti:finalize(); 15 - body = pftxt; 11 + var tiptr = ti:finalize() 12 + co:stdpage([lib.srv.convo.page] { 13 + title = tiptr; body = pftxt; 16 14 class = lib.str.plit 'profile'; 17 - navlinks = co.navbar; 18 - } 15 + }) 19 16 20 - var hdrs = array( 21 - lib.http.header { 'Content-Type', 'text/html; charset=UTF-8' } 22 - ) 23 - doc:send(co.con,200,[lib.mem.ptr(lib.http.header)] {ct = 1, ptr = &hdrs[0]}) 24 - doc.title:free() 17 + tiptr:free() 25 18 end 26 19 27 20 return render_userpage
Modified route.t from [4d69f275c0] to [d6af263481].
158 158 var redirto: lib.str.acc redirto:compose('/post/',{idbuf,idlen}) defer redirto:free() 159 159 co:reroute(redirto.buf) 160 160 end 161 161 end 162 162 163 163 terra http.timeline(co: &lib.srv.convo, mode: hpath) 164 164 lib.render.timeline(co,lib.trn(mode.ptr == nil, rstring{ptr=nil}, mode.ptr[1])) 165 - return 165 +end 166 + 167 +terra http.documentation(co: &lib.srv.convo, path: hpath) 168 + if path.ct == 2 then 169 + lib.render.docpage(co,path(1)) 170 + elseif path.ct == 1 then 171 + lib.render.docpage(co, rstring.null()) 172 + else 173 + co:complain(404, 'no such documentation', 'invalid documentation URL') 174 + end 166 175 end 167 176 168 177 do local branches = quote end 169 178 local filename, flen = symbol(&int8), symbol(intptr) 170 179 local page = symbol(lib.http.page) 171 180 local send = label() 172 181 local storage = data.stmap ................................................................................ 203 212 co:reroute('/s/default-avatar.webp') 204 213 end 205 214 206 215 -- entry points 207 216 terra r.dispatch_http(co: &lib.srv.convo, uri: lib.mem.ptr(int8), meth: method.t) 208 217 lib.dbg('handling URI of form ', {uri.ptr,uri.ct}) 209 218 co.navbar = lib.render.nav(co) 210 - lib.dbg('got nav ', {co.navbar.ptr,co.navbar.ct}, "||", co.navbar.ptr) 211 219 -- some routes are non-hierarchical, and can be resolved with a simple strcmp 212 220 -- we run through those first before giving up and parsing the URI 213 221 if uri.ptr[0] ~= @'/' then 214 222 co:complain(404, 'what the hell', 'how did you do that') 215 223 return 216 224 elseif uri.ct == 1 then -- root 217 225 if (co.srv.cfg.pol_sec == lib.srv.secmode.private or ................................................................................ 251 259 return 252 260 else -- hierarchical routes 253 261 var path = lib.http.hier(uri) defer path:free() 254 262 if path.ptr[0]:cmp(lib.str.lit('user')) then 255 263 http.actor_profile_uid(co, path, meth) 256 264 elseif path.ptr[0]:cmp(lib.str.lit('tl')) then 257 265 http.timeline(co, path) 266 + elseif path.ptr[0]:cmp(lib.str.lit('doc')) then 267 + if meth ~= method.get and meth ~= method.head then goto wrongmeth end 268 + http.documentation(co, path) 258 269 else goto notfound end 259 270 return 260 271 end 261 272 262 273 ::wrongmeth:: co:complain(405, 'method not allowed', 'that method is not meaningful for this endpoint') do return end 263 274 ::notfound:: co:complain(404, 'not found', 'no such resource available') do return end 264 275 end
Modified schema.sql from [6bf306c986] to [097969b0cb].
58 58 ); 59 59 60 60 drop table if exists parsav_rights cascade; 61 61 create table parsav_rights ( 62 62 key text, 63 63 actor bigint references parsav_actors(id) 64 64 on delete cascade, 65 - allow boolean, 65 + allow boolean not null, 66 + scope bigint, -- for future expansion 66 67 67 68 primary key (key,actor) 68 69 ); 69 70 70 71 insert into parsav_actors (handle,rank,quota) values (:'admin',1,0); 71 72 insert into parsav_rights (actor,key,allow) 72 73 select (select id from parsav_actors where handle=:'admin'), a.column1, a.column2 from (values 73 - ('ban',true), 74 + ('purge',true), 74 75 ('config',true), 75 76 ('censor',true), 76 77 ('suspend',true), 78 + ('cred',true), 79 + ('elevate',true), 80 + ('demote',true), 77 81 ('rebrand',true) 78 82 ) as a; 79 83 80 84 drop table if exists parsav_posts cascade; 81 85 create table parsav_posts ( 82 86 id bigint primary key default (1+random()*(2^63-1))::bigint, 83 87 author bigint references parsav_actors(id)
Modified srv.t from [d14bb4b6ce] to [8c264568c9].
1 1 -- vim: ft=terra 2 -local util = dofile 'common.lua' 2 +local util = lib.util 3 3 local secmode = lib.enum { 'public', 'private', 'lockdown', 'isolate' } 4 +local pstring = lib.mem.ptr(int8) 4 5 local struct srv 5 6 local struct cfgcache { 6 7 secret: lib.mem.ptr(int8) 7 8 instance: lib.mem.ptr(int8) 8 9 overlord: &srv 9 10 pol_sec: secmode.t 10 11 pol_reg: bool ................................................................................ 48 49 elseif rt.stat_basetype then tk = stat 49 50 elseif rt.ptr_basetype then tk = ptr end 50 51 break 51 52 end 52 53 end 53 54 54 55 local r = symbol(rt) 56 + local succ = label() 55 57 if tk == primary then 56 58 return quote 57 59 var [r] 58 60 for i=0,self.sources.ct do var src = self.sources.ptr + i 59 61 if src.handle ~= nil and src.backend.[meth] ~= nil then 60 62 r = src:[meth]([expr]) 61 - goto success 63 + goto [succ] 62 64 end 63 65 end 64 66 lib.bail(['no active backends provide critical capability ' .. meth .. '!']) 65 - ::success::; 67 + ::[succ]::; 66 68 in r end 67 69 else local ok, empty 68 70 if tk == ptr then 69 71 ok = `r.ptr ~= nil 70 72 empty = `[rt]{ptr=nil,ct=0} 71 73 elseif tk == stat then 72 74 ok = `r.ok == true ................................................................................ 163 165 body:send(self.con, code, [lib.mem.ptr(lib.http.header)] { 164 166 ptr = &hdrs[0], ct = [hdrs.type.N] 165 167 }) 166 168 167 169 body.title:free() 168 170 body.body:free() 169 171 end 172 + 173 +struct convo.page { 174 + title: pstring 175 + body: pstring 176 + class: pstring 177 +} 178 + 179 +terra convo:stdpage(pg: convo.page) 180 + var doc = data.view.docskel { 181 + instance = self.srv.cfg.instance; 182 + title = pg.title; 183 + body = pg.body; 184 + class = pg.class; 185 + navlinks = self.navbar; 186 + } 187 + 188 + var hdrs = array( 189 + lib.http.header { 'Content-Type', 'text/html; charset=UTF-8' } 190 + ) 191 + 192 + doc:send(self.con,200,[lib.mem.ptr(lib.http.header)] {ct = [hdrs.type.N], ptr = &hdrs[0]}) 193 +end 170 194 171 195 -- CALL ONLY ONCE PER VAR 172 196 terra convo:postv(name: rawstring) 173 197 if self.varbuf.ptr == nil then 174 198 self.varbuf = lib.mem.heapa(int8, self.msg.body.len + self.msg.query.len) 175 199 self.vbofs = self.varbuf.ptr 176 200 end ................................................................................ 344 368 lib.osclock.time(nil)) 345 369 if aid ~= 0 then co.aid = aid end 346 370 end ::nocookie::; 347 371 end 348 372 349 373 if co.aid ~= 0 then 350 374 var sess, usr = co.srv:actor_session_fetch(co.aid, peer) 351 - if sess.ok == false then co.aid = 0 else co.who = usr.ptr end 375 + if sess.ok == false then co.aid = 0 else 376 + co.who = usr.ptr 377 + co.who.rights.powers = server:actor_powers_fetch(co.who.id) 378 + end 352 379 end 353 380 354 381 var uridec = lib.mem.heapa(int8, msg.uri.len) defer uridec:free() 355 382 var urideclen = lib.net.mg_url_decode(msg.uri.ptr, msg.uri.len, uridec.ptr, uridec.ct, 1) 356 383 357 384 var uri = uridec 358 385 if urideclen == -1 then ................................................................................ 481 508 return stats 482 509 end 483 510 484 511 terra srv:actor_auth_how(ip: lib.store.inet, usn: rawstring) 485 512 var cs: lib.store.credset cs:clear() 486 513 var ok = false 487 514 for i=0,self.sources.ct do 488 - var set, iok = self.sources.ptr[i]:actor_auth_how(ip, usn) 515 + var set, iok = self.sources(i):actor_auth_how(ip, usn) 489 516 if iok then 490 517 cs = cs + set 491 518 ok = iok 492 519 end 493 520 end 494 521 return cs, ok 495 522 end 496 523 524 +terra srv:actor_auth_pw(ip: lib.store.inet, user: pstring, pw: pstring): uint64 525 + for i=0,self.sources.ct do 526 + if self.sources(i).backend ~= nil and 527 + self.sources(i).backend.actor_auth_pw ~= nil then 528 + var aid,uid,newhnd = self.sources(i):actor_auth_pw(ip,user,pw) 529 + if aid ~= 0 then 530 + if uid == 0 then 531 + lib.dbg('new user just logged in, creating account entry') 532 + var kbuf: uint8[lib.crypt.const.maxdersz] 533 + var newkp = lib.crypt.genkp() 534 + var privsz = lib.crypt.der(false,&newkp,&kbuf[0]) 535 + var na = lib.store.actor { 536 + id = 0; nym = nil; handle = newhnd.ptr; 537 + origin = 0; bio = nil; avatar = nil; 538 + knownsince = lib.osclock.time(nil); 539 + rights = lib.store.rights_default(); 540 + title = nil, key = [lib.mem.ptr(uint8)] { 541 + ptr = &kbuf[0], ct = privsz 542 + }; 543 + } 544 + var newuid: uint64 545 + if self.sources(i).backend.actor_create ~= nil then 546 + newuid = self.sources(i):actor_create(&na) 547 + else newuid = self:actor_create(&na) end 548 + 549 + if self.sources(i).backend.actor_auth_register_uid ~= nil then 550 + self.sources(i):actor_auth_register_uid(aid,newuid) 551 + end 552 + end 553 + return aid 554 + end 555 + end 556 + end 557 + 558 + return 0 559 +end 560 + 561 +--9twh8y94i5c1qqr7hxu20fyd 497 562 terra cfgcache.methods.load :: {&cfgcache} -> {} 498 563 terra cfgcache:init(o: &srv) 499 564 self.overlord = o 500 565 self:load() 501 566 end 502 567 503 568 srv.methods.start = terra(self: &srv, befile: rawstring)
Modified static/style.scss from [1dafd96ed6] to [9520b92c84].
212 212 display: block; 213 213 position: relative; 214 214 min-height: calc(100vh - 1.1in); 215 215 margin-top: 0; 216 216 margin-bottom: 0; 217 217 padding: 0 0.4in; 218 218 padding-top: 1.1in; 219 + padding-bottom: 0.1in; 219 220 background-color: adjust-color($color, $lightness: -45%, $alpha: 0.4); 220 221 border: { 221 222 left: 1px solid black; 222 223 right: 1px solid black; 223 224 } 224 225 } 225 226 ................................................................................ 463 464 background: linear-gradient(to left, tone(-55%,-0.5), transparent); 464 465 } 465 466 } 466 467 467 468 a[href].rawlink { 468 469 @extend %teletype; 469 470 } 471 + 472 +body.doc main { 473 + @extend %serif; 474 + li { margin-top: 0.05in; } 475 + li:first-child { margin-top: 0; } 476 +}
Modified store.t from [4782518865] to [5b8778f17f].
30 30 31 31 terra m.powerset:affect_users() 32 32 return self.purge() or self.censor() or self.suspend() or 33 33 self.elevate() or self.demote() or self.rebrand() or 34 34 self.cred() 35 35 end 36 36 37 -local str = rawstring --lib.mem.ptr(int8) 37 +local str = rawstring 38 +local pstr = lib.mem.ptr(int8) 38 39 39 40 struct m.source 40 41 41 42 struct m.rights { 42 43 rank: uint16 -- lower = more powerful except 0 = regular user 43 44 -- creating staff automatically assigns rank immediately below you 44 45 quota: uint32 -- # of allowed tweets per day; 0 = no limit ................................................................................ 61 62 62 63 struct m.actor { 63 64 id: uint64 64 65 nym: str 65 66 handle: str 66 67 origin: uint64 67 68 bio: str 69 + title: str 68 70 avatar: str 69 71 knownsince: m.timepoint 70 72 rights: m.rights 71 73 key: lib.mem.ptr(uint8) 72 74 73 75 -- ephemera 74 76 xid: str ................................................................................ 169 171 open: &m.source -> &opaque 170 172 close: &m.source -> {} 171 173 172 174 conf_get: {&m.source, rawstring} -> lib.mem.ptr(int8) 173 175 conf_set: {&m.source, rawstring, rawstring} -> {} 174 176 conf_reset: {&m.source, rawstring} -> {} 175 177 176 - actor_save: {&m.source, m.actor} -> bool 177 - actor_create: {&m.source, m.actor} -> bool 178 + actor_save: {&m.source, &m.actor} -> bool 179 + actor_create: {&m.source, &m.actor} -> uint64 178 180 actor_fetch_xid: {&m.source, lib.mem.ptr(int8)} -> lib.mem.ptr(m.actor) 179 181 actor_fetch_uid: {&m.source, uint64} -> lib.mem.ptr(m.actor) 180 182 actor_notif_fetch_uid: {&m.source, uint64} -> lib.mem.ptr(m.notif) 181 183 actor_enum: {&m.source} -> lib.mem.ptr(&m.actor) 182 184 actor_enum_local: {&m.source} -> lib.mem.ptr(&m.actor) 183 185 actor_stats: {&m.source, uint64} -> m.actor_stats 184 186 185 187 actor_auth_how: {&m.source, m.inet, rawstring} -> {m.credset, bool} 186 188 -- returns a set of auth method categories that are available for a 187 189 -- given user from a certain origin 188 190 -- origin: inet 189 191 -- username: rawstring 190 - actor_auth_otp: {&m.source, m.inet, rawstring, rawstring} -> uint64 191 - actor_auth_pw: {&m.source, m.inet, lib.mem.ptr(int8), lib.mem.ptr(int8) } -> uint64 192 + actor_auth_otp: {&m.source, m.inet, rawstring, rawstring} 193 + -> {uint64, uint64, pstr} 194 + actor_auth_pw: {&m.source, m.inet, lib.mem.ptr(int8), lib.mem.ptr(int8) } 195 + -> {uint64, uint64, pstr} 192 196 -- handles password-based logins against hashed passwords 193 197 -- origin: inet 194 198 -- handle: rawstring 195 199 -- token: rawstring 196 - actor_auth_tls: {&m.source, m.inet, rawstring} -> uint64 200 + actor_auth_tls: {&m.source, m.inet, rawstring} 201 + -> {uint64, uint64, pstr} 197 202 -- handles implicit authentication performed as part of an TLS connection 198 203 -- origin: inet 199 204 -- fingerprint: rawstring 200 205 actor_auth_api: {&m.source, m.inet, rawstring, rawstring} -> uint64 206 + -> {uint64, uint64, pstr} 201 207 -- handles API authentication 202 208 -- origin: inet 203 209 -- handle: rawstring 204 210 -- key: rawstring (X-API-Key) 205 211 actor_auth_record_fetch: {&m.source, uint64} -> lib.mem.ptr(m.auth) 212 + actor_powers_fetch: {&m.source, uint64} -> m.powerset 206 213 actor_session_fetch: {&m.source, uint64, m.inet} -> {lib.stat(m.auth), lib.mem.ptr(m.actor)} 207 214 -- retrieves an auth record + actor combo suitable by AID suitable 208 215 -- for determining session validity & caps 209 216 -- aid: uint64 210 217 -- origin: inet 218 + actor_auth_register_uid: {&m.source, uint64, uint64} -> {} 219 + -- notifies the backend module of the UID that has been assigned for 220 + -- an authentication ID 221 + -- aid: uint64 222 + -- uid: uint64 211 223 212 224 actor_conf_str: cnf(rawstring, lib.mem.ptr(int8)) 213 225 actor_conf_int: cnf(intptr, lib.stat(intptr)) 214 226 215 227 post_save: {&m.source, &m.post} -> {} 216 228 post_create: {&m.source, &m.post} -> uint64 217 229 actor_post_fetch_uid: {&m.source, uint64, m.range} -> lib.mem.ptr(m.post)
Modified str.t from [c5ea2451a4] to [5af1afba76].
1 1 -- vim: ft=terra 2 2 -- string.t: string classes 3 -local util = dofile('common.lua') 3 +local util = lib.util 4 4 local pstr = lib.mem.ptr(int8) 5 5 local pref = lib.mem.ref(int8) 6 6 7 7 local m = { 8 8 sz = terralib.externfunction('strlen', rawstring -> intptr); 9 9 cmp = terralib.externfunction('strcmp', {rawstring, rawstring} -> int); 10 10 ncmp = terralib.externfunction('strncmp', {rawstring, rawstring, intptr} -> int); ................................................................................ 174 174 return `[lib.mem.ptr(int8)] {ptr = nil, ct = 0} 175 175 end 176 176 end) 177 177 178 178 m.acc.methods.lpush = macro(function(self,str) 179 179 return `self:push([str:asvalue()], [#(str:asvalue())]) end) 180 180 m.acc.methods.ppush = terra(self: &m.acc, str: lib.mem.ptr(int8)) 181 + self:push(str.ptr, str.ct) return self end; 182 +m.acc.methods.rpush = terra(self: &m.acc, str: lib.mem.ref(int8)) 181 183 self:push(str.ptr, str.ct) return self end; 182 184 m.acc.methods.merge = terra(self: &m.acc, str: lib.mem.ptr(int8)) 183 185 self:push(str.ptr, str.ct) str:free() return self end; 184 186 m.acc.methods.compose = macro(function(self, ...) 185 187 local minlen = 0 186 188 local pstrs = {} 187 189 for i,v in ipairs{...} do
Modified tpl.t from [3e776df34f] to [ba911a8ebc].
1 1 -- vim: ft=terra 2 2 -- string template generator: 3 3 -- returns a function that fills out a template 4 4 -- with the strings given 5 5 6 -local util = dofile 'common.lua' 6 +local util = lib.util 7 7 local m = {} 8 8 function m.mk(tplspec) 9 9 local str 10 10 if type(tplspec) == 'string' 11 11 then str = tplspec tplspec = {} 12 12 else str = tplspec.body 13 13 end ................................................................................ 21 21 tplchar_o = string.gsub(tplchar_o,'%%','%%%%') 22 22 tplchar = string.gsub(tplchar, '.', function(c) 23 23 if magic[c] then return '%' .. c end 24 24 end) 25 25 local last = 1 26 26 local fields = {} 27 27 local segs = {} 28 + local docs = {} 28 29 local constlen = 0 29 30 -- strip out all irrelevant whitespace to tidy things up 30 31 -- TODO: find way to exclude <pre> tags? 31 32 str = str:gsub('[\n^]%s+','') 32 33 str = str:gsub('%s+[\n$]','') 33 34 str = str:gsub('\n','') 34 35 str = str:gsub('</a><a ','</a> <a ') -- keep nav links from getting smooshed 36 + str = str:gsub(tplchar .. '%?([-%w]+)', function(file) 37 + if not docs[file] then docs[file] = data.doc[file] end 38 + return string.format('<a href="#help-%s" class="help">?</a>', file) 39 + end) 35 40 for start, key, stop in string.gmatch(str,'()'..tplchar..'(%w+)()') do 36 41 if string.sub(str,start-1,start-1) ~= '\\' then 37 42 segs[#segs+1] = string.sub(str,last,start-1) 38 43 fields[#segs] = key 39 44 last = stop 40 45 end 41 46 end 42 47 segs[#segs+1] = string.sub(str,last) 48 + 43 49 for i, s in ipairs(segs) do 44 50 segs[i] = string.gsub(s, '\\'..tplchar, tplchar_o) 45 51 constlen = constlen + string.len(segs[i]) 46 52 end 53 + 54 + for n,d in pairs(docs) do 55 + local html = string.format( 56 + '<div id="help-%s" class="modal"> <a href="#0">close</a> <div>%s</div></div>', n, d.text 57 + ) 58 + segs[#segs] = segs[#segs] .. html 59 + constlen = constlen + #html 60 + end 61 + 47 62 48 63 local runningtally = symbol(intptr) 49 64 local tallyup = {quote 50 65 var [runningtally] = 1 + constlen 51 66 end} 52 67 local rec = terralib.types.newstruct(string.format('template<%s>',tplspec.id or '')) 53 68 local symself = symbol(&rec)
Modified view/compose.tpl from [09c6180294] to [bb642e2999].
1 1 <form class="compose" method="post"> 2 2 <img src="/avi/@handle"> 3 3 <textarea autofocus name="post" placeholder="it was a dark and stormy night…">@content</textarea> 4 - <input required type="text" name="acl" class="acl" value="@acl"> 5 - <a href="#aclhelp" class="help">?</a> 4 + <input required type="text" name="acl" class="acl" value="@acl"> @?acl 6 5 <button type="submit">commit</button> 7 6 </form> 8 - 9 -<div id="aclhelp" class="modal"> <a href="#0">close</a> <div> 10 - <p>to control who can see your post (and how far it is propagated), <code>parsav</code> uses <strong>ACL expressions</strong>. 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 <code>~</code>, a tilde character, so <code>~all</code> matches nobody, and <code>~followed</code> matches users you do not follow.</p> 11 - <ul> 12 - <li><strong>all</strong>: matches any and all users</li> 13 - <li><strong>local</strong>: matches users who belong to this instance</li> 14 - <li><strong>mutuals</strong>: matches users you follow who also follow you</li> 15 - <li><strong>followed</strong>: matches users you follow</li> 16 - <li><strong>followers</strong>: matches users who follow you</li> 17 - <li><strong>groupies</strong>: matches users who follow you, but whom you do not follow</li> 18 - <li><strong>mentioned</strong>: matches users who are mentioned in the post</li> 19 - <li><strong>staff</strong>: matches instance staff (equivalent to <code>~%0</code>)</li> 20 - <li><strong>admin</strong>: matches the individual named as the instance administrator, if any</li> 21 - <li><strong>\@</strong><em>handle</em>: matches the user <em>handle</em></li> 22 - <li><strong>+</strong><em>circle</em>: matches users you have categorized under <em>circle</em></li> 23 - <li><strong>#</strong><em>room</em>: matches users who are members of <em>room</em></li> 24 - <li><strong>%</strong><em>rank</em>: matches users of <em>rank</em> or higher (e.g. <code>%3</code> matches users of rank 3, 2, and 1). as a special case, <code>%0</code> matches ordinary users</li> 25 - <li><strong>#</strong><em>room</em><strong>%</strong><em>rank</em>: matches users who hold <em>rank</em> in <em>room</em></li> 26 - <li><strong><</strong><em>title</em><strong>></strong>: matches peers of the net who have been created <em>title</em> by the sovereign</li> 27 - <li><strong>#</strong><em>room</em><strong><</strong><em>title</em><strong>></strong>: matches peers of the chat who have been created <em>title</em> by <em>room</em> staff</li> 28 - </ul> 29 - <p>to evaluate an ACL expression, <code>parsav</code> 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.</p> 30 - <p><strong>policy</strong> is whether a term grants or denies access. the default term policy is <strong>allow</strong>, but you can control the policy with the keywords <code>allow</code> and <code>deny</code>. 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; <code>allow \@bob</code> and <code>deny trent</code> do exactly what you'd expect &em; the former allows bob and only bob in; the latter denies access only to trent, but grants access to the rest of the world.</p> 31 - <p>expressions must contain at least one term to be valid. if they consist only of policy keywords, they will be rejected.</p> 32 - <p>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 <code>local</code> means only local users can view it. however, much more complex expressions are possible.</p> 33 - <ul> 34 - <li><code>deny groupies allow +illuminati</code>: permits access to the illuminati, but excluding those members who are groupies</li> 35 - <li><code>+illuminati deny groupies</code>: allows access to everyone but groupies (unless they're in the illuminati)</li> 36 - <li><code>\@eve \@alice\@nowhere.tld deny \@bob \@trent\@witches.live</code>: grants access to eve and alice, but locks out bob and trent</li> 37 - <li><code><grand duke> #4th-intl<comrade></code>: restricts the post to the eyes of the Fourth International's secret cabal of anointed comrades and the grand dukes of the Empire</li> 38 - <li><code>deny ~%3</code>: blocks a post from being seen by anyone with a staff rank level below 3</li> 39 - </ul> 40 - <p><strong>limitations:</strong> 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.</p> 41 -</div></div>