parsav  Check-in [bbfea467bf]

Overview
Comment:permissions work now
Downloads: Tarball | ZIP archive | SQL archive
Timelines: family | ancestors | descendants | both | trunk
Files: files | file ages | folders
SHA3-256: bbfea467bf467820010635f449f683a1400fbb42537efd99a1816b71a36e4146
User & Date: lexi on 2020-12-27 02:31:30
Other Links: manifest | tags
Context
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
Changes

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, &timestr[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>&lt;</strong><em>title</em><strong>&gt;</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>&lt;</strong><em>title</em><strong>&gt;</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>&lt;grand duke&gt; #4th-intl&lt;comrade&gt;</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>