ADDED api/lp/actor.t
Index: api/lp/actor.t
==================================================================
--- api/lp/actor.t
+++ api/lp/actor.t
@@ -0,0 +1,55 @@
+-- vim: ft=terra
+local tpl = lib.tpl.mk {
+ sigil = '%';
+ body = [[{
+ "@context": "https://%+domain/s/litepub.jsonld",
+ "type": "Person",
+ "id": "%lpid",
+ "url": "https://%+domain/@%+handle",
+ "preferredUsername": %$handle,
+ "name": %$nym,
+ "summary": %$desc,
+ "alsoKnownAs": ["https://%+domain/@%+handle"],
+ "publicKey": {
+ "id": "%lpid#ident-rsa",
+ "owner": "%lpid",
+ "publicKeyPem": %rsa
+ },
+ "icon": {
+ "type": "Image",
+ "url": "https://%+domain%+avi"
+ },
+ "capabilities": { "acceptsChatMessages": false },
+ "discoverable": true,
+ "manuallyApprovedFollowers": %locked,
+ "inbox": "https://%+domain/api/lp/inbox/user/%uid",
+ "outbox": "https://%+domain/api/lp/outbox/user/%uid",
+ "followers": "https://%+domain/api/lp/rel/%uid/followers",
+ "following": "https://%+domain/api/lp/rel/%uid/following"
+ }]];
+}
+
+local pstr = lib.str.t
+terra cs(s: rawstring) return pstr {s, lib.str.sz(s)} end
+
+local terra
+api_lp_actor(co: &lib.srv.convo, actor: &lib.store.actor)
+ var lpid = co:stra(64)
+ lpid:lpush'https://':ppush(co.srv.cfg.domain):lpush'/user/':shpush(actor.id)
+ var uid = co:stra(32) uid:shpush(actor.id) -- dumb hack bc lazy FIXME
+
+ var body = tpl {
+ domain = co.srv.cfg.domain;
+ uid = uid:finalize();
+ lpid = lpid:finalize();
+ handle = cs(actor.handle);
+ nym = cs(actor.nym);
+ desc = cs(actor.bio);
+ avi = cs(actor.avatar);
+ rsa = '';
+ locked = 'false';
+ }
+
+ co:json(body:poolstr(&co.srv.pool))
+end
+return api_lp_actor
ADDED api/lp/outbox.t
Index: api/lp/outbox.t
==================================================================
--- api/lp/outbox.t
+++ api/lp/outbox.t
@@ -0,0 +1,61 @@
+-- vim: ft=terra
+local pstr = lib.str.t
+
+local terra
+lp_outbox(co: &lib.srv.convo, uri: pstr, here: lib.mem.ptr(lib.str.ref))
+ var path = lib.str.qesc(&co.srv.pool,uri,false)
+ var json = co:stra(512)
+ json:lpush '{"@context": "https://':ppush(co.srv.cfg.domain):lpush'/s/litepub.jsonld","id":"https://'
+ :ppush(co.srv.cfg.domain):ppush(path)
+ :lpush '"'
+ var at = co:pgetv('at')
+ lib.dbg('api path ',
+ {here(0).ptr,here(0).ct}, ' / ',
+ {here(1).ptr,here(1).ct})
+ json:lpush',"current":"https://':ppush(co.srv.cfg.domain):ppush(path):lpush'?at=top"'
+ if not at then
+ json:lpush',"type":"OrderedCollection","first":"https://':ppush(co.srv.cfg.domain):ppush(path):lpush'?at=top"'
+ else
+ if here(0):cmp 'user' and here.ct > 1 then
+ var uid, uidok = lib.math.shorthand.parse(here(1).ptr, here(1).ct)
+ if not uidok then goto e404 end
+ var user = co.srv:actor_fetch_uid(uid)
+ if not user then goto e404 end
+ var time: lib.store.timepoint
+ if at:cmp('top') then
+ time = lib.osclock.time(nil)
+ else
+ var tp, ok = lib.math.decparse(at)
+ if ok then time = tp end
+ end
+ lib.io.fmt('from-time: %llu\n', time)
+ var posts = co.srv:post_enum_author_uid(uid, lib.store.range {
+ mode = 1; -- time -> idx
+ from_time = time;
+ to_idx = 65;
+ })
+ var oldest = time
+ json:lpush',"partOf":"https://':ppush(co.srv.cfg.domain):ppush(path)
+ :lpush'","type":"CollectionPage","orderedItems":['
+ if posts.sz > 0 then defer posts:free()
+ for i=0, lib.math.smallest(posts.sz,64) do
+ if i~=0 then json:lpush',' end
+ json:ppush(lib.api.lp.tweet(co,posts(i).ptr,true))
+ oldest = lib.math.smallest(posts(i)().posted, oldest)
+ end
+ end
+ json:lpush'],"totalItems":':ipush(posts.sz)
+ if oldest ~= time and oldest > 0 and posts.sz > 64 then
+ json:lpush',"next":"https://':ppush(co.srv.cfg.domain):ppush(path)
+ :lpush'?at=':ipush(oldest-1):lpush'"'
+ end
+
+ else goto e404 end -- TODO
+ end
+ json:lpush[[}]]
+ co:json(json:finalize())
+ do return end
+ ::e404:: do co:fail(404) return end
+end
+
+return lp_outbox
ADDED api/lp/tweet.t
Index: api/lp/tweet.t
==================================================================
--- api/lp/tweet.t
+++ api/lp/tweet.t
@@ -0,0 +1,46 @@
+-- vim: ft=terra
+local pstr = lib.str.t
+
+local obj = lib.tpl.mk [[{
+ "\@context": "https://@+domain/s/litepub.jsonld",
+ "type": "Note",
+ "id": "https://@+domain/post/@^pid",
+ "content": @$html,
+ "source": @$raw,
+ "attributedTo": "https://@+domain/user/@^uid",
+ "published": "@pubtime"
+ @extra
+}]]
+
+local wrap = lib.tpl.mk [[{
+ "\@context": "https://@+domain/s/litepub.jsonld",
+ "type": "@kind",
+ "actor": "https://@+domain/user/@^uid",
+ "published": "@$pubtime",
+ "id": "https://@+domain/api/lp/act/@^aid",
+ "object": @obj
+}]]
+
+local terra
+lp_tweet(co: &lib.srv.convo, p: &lib.store.post, act_wrap: bool)
+
+ var tweet = (obj {
+ domain = co.srv.cfg.domain, uid = p.author, pid = p.id;
+ html = lib.smackdown.html(&co.srv.pool, p.body, false);
+ raw = p.body, pubtime = '', extra = '';
+ }):poolstr(&co.srv.pool)
+
+ if act_wrap then
+ return (wrap {
+ domain = co.srv.cfg.domain;
+ kind = lib.trn(p.rtdby == 0, 'Create', 'Announce');
+ uid = lib.trn(p.rtdby == 0, p.author, p.rtdby);
+ aid = lib.trn(p.rtdby == 0, p.id, p.rtact);
+ pubtime = '', obj = tweet;
+ }):poolstr(&co.srv.pool)
+ else
+ return tweet
+ end
+end
+
+return lp_tweet
ADDED api/webfinger.t
Index: api/webfinger.t
==================================================================
--- api/webfinger.t
+++ api/webfinger.t
@@ -0,0 +1,48 @@
+-- vim: ft=terra
+wftpl = lib.tpl.mk [[{
+ "subject": @$subj,
+ "aliases": [ @$href, @$pfp ],
+ "links": [
+ { "rel": "self", "type": "application/activity+json", "href": @$href },
+ { "rel": "self",
+ "type": "application/ld+json; profile=\"https://www.w3.org/ns/activitystreams\"", "href": @$href },
+ { "rel": "http://webfinger.net/rel/profile-page",
+ "type": "text/html", "href": @$pfp }
+ ]
+}]]
+
+local terra
+webfinger(co: &lib.srv.convo)
+ var res = co:pgetv('resource')
+ if (not res) or not res:startswith 'acct:' then goto err end
+
+ var acct = res + 5
+ var svp = lib.str.find(acct, '@')
+ if svp:ref() then
+ acct.ct = (svp.ptr - acct.ptr)
+ svp:advance(1)
+ if not svp:cmp(co.srv.cfg.domain) then goto err end
+ end
+
+ var actor = co.srv:actor_fetch_xid(acct)
+ if not actor then goto err end
+ do defer actor:free()
+ if actor().origin ~= 0 then goto err end
+ var href = co:stra(64)
+ var pfp = co:stra(64)
+ href:lpush'https://':ppush(co.srv.cfg.domain):lpush'/user/':shpush(actor().id)
+ pfp:lpush'https://':ppush(co.srv.cfg.domain):lpush'/@':ppush(acct)
+
+ var tp = wftpl {
+ subj = res;
+ href = href:finalize();
+ pfp = pfp:finalize();
+ }
+ co:json(tp:poolstr(&co.srv.pool))
+
+ return
+ end
+ -- error conditions
+ ::err:: do co:json('{}') return end
+end
+return webfinger
Index: config.lua
==================================================================
--- config.lua
+++ config.lua
@@ -64,10 +64,12 @@
{'warn.svg', 'image/svg+xml'};
{'query.webp', 'image/webp'};
{'reply.webp', 'image/webp'};
{'file.webp', 'image/webp'};
{'follow.webp', 'image/webp'};
+
+ {'litepub.jsonld', 'application/ld+json; charset=utf-8'};
-- keep in mind before you add anything to this list: these are not
-- just files parsav can access, they are files that are *kept in
-- memory* for fast access the entire time parsav is running, and
-- which need to be loaded into memory before the program can even
-- start. it's imperative to keep these as small and few in number
Index: math.t
==================================================================
--- math.t
+++ math.t
@@ -1,12 +1,10 @@
-- vim: ft=terra
local m = {
shorthand = {maxlen = 14};
- ll = {
- ctpop_u8 = terralib.intrinsic('llvm.ctpop.i8', uint8 -> uint8);
- };
}
+m.shorthand.t = int8[m.shorthand.maxlen]
local pstring = lib.mem.ptr(int8)
-- swap in place -- faster on little endian
m.netswap_ip = macro(function(ty, src, dest)
@@ -66,11 +64,11 @@
return n
end
terra m.shorthand.gen(val: uint64, dest: rawstring): ptrdiff
var lst = "0123456789-ABCDEFGHIJKLMNOPQRSTUVWXYZ:abcdefghijklmnopqrstuvwxyz"
- var buf: int8[m.shorthand.maxlen]
+ var buf: m.shorthand.t
var ptr = [&int8](buf)
while val ~= 0 do
var v = val % 64
@ptr = lst[v]
ptr = ptr + 1
Index: parsav.t
==================================================================
--- parsav.t
+++ parsav.t
@@ -15,11 +15,11 @@
for i=1,#path-1 do
if tgt[path[i]] == nil then tgt[path[i]] = {} end
tgt = tgt[path[i]]
end
local chunk = terralib.loadfile(l:gsub(':','/') .. '.t')
- if chunk ~= nil then
+ if chunk ~= nil then
tgt[path[#path]:gsub('-','_')] = chunk()
print(' \27[1m[ \27[32mok\27[;1m ]\27[m')
else
print(' \27[1m[\27[31mfail\27[;1m]\27[m')
os.exit(2)
@@ -108,10 +108,11 @@
var c: bool = [cond]
var r: i.tree.type
if c == true then r = i else r = e end
in r end
end);
+ typeof = macro(function(exp) return exp.tree.type end);
coalesce = macro(function(...)
local args = {...}
local ty = args[1].tree.type
local val = symbol(ty)
local empty
@@ -498,10 +499,15 @@
'render:conf:circles';
'render:conf:sec';
'render:conf:users';
'render:conf:avi';
'render:conf';
+
+ 'api:lp:actor';
+ 'api:lp:tweet';
+ 'api:lp:outbox';
+ 'api:webfinger';
'route';
}
do
local p = string.format('parsav: %s\nbuilt on %s\n', config.build.str, config.build.when)
Index: render/profile.t
==================================================================
--- render/profile.t
+++ render/profile.t
@@ -197,10 +197,11 @@
if relationship.recip.follow() then
comments:lpush('
follows you')
end
var circpanel: lib.str.acc
+ var circstr = pstr.null()
if co.aid ~= 0 then
circpanel = co:stra(128)
var allcircs = co.srv:circle_search(&co.srv.pool, co.who.id, 0)
if allcircs:ref() then
var mycircs = co.srv:circle_memberships_uid(&co.srv.pool, co.who.id, actor.id)
@@ -217,10 +218,11 @@
circpanel:lpush '> '
:ppush(allcircs(i).name)
:lpush ''
end
end
+ circstr = circpanel:finalize()
end
var profile = data.view.profile {
nym = fullname;
bio = bio;
@@ -233,11 +235,11 @@
timephrase = lib.trn(actor.origin == 0, pstr 'joined', pstr 'known since');
remarks = '';
auxbtn = auxp;
- circles = circpanel:finalize();
+ circles = circstr;
relations = relbtns:finalize();
sanctions = sancbtns:finalize();
}
if comments.sz > 0 then profile.remarks = comments:finalize() end
Index: route.t
==================================================================
--- route.t
+++ route.t
@@ -95,11 +95,19 @@
else
co:reroute(go.ptr)
end
end
-terra http.actor_profile_xid(co: &lib.srv.convo, uri: lib.mem.ptr(int8), meth: method.t)
+terra http.actor_dispatch_mime(co: &lib.srv.convo, actor: &lib.store.actor)
+ if co:matchmime(lib.http.mime.html) then
+ http.actor_profile(co,actor,co.method)
+ elseif co:matchmime(lib.http.mime.json) then
+ lib.api.lp.actor(co, actor)
+ else co:fail(406) end
+end
+
+terra http.actor_profile_xid(co: &lib.srv.convo, uri: lib.mem.ptr(int8))
var handle = [lib.mem.ptr(int8)] { ptr = &uri.ptr[2], ct = 0 }
for i=2,uri.ct do
if uri.ptr[i] == @'/' or uri.ptr[i] == 0 then handle.ct = i - 2 break end
end
if handle.ct == 0 then
@@ -116,17 +124,16 @@
co:complain(404,'no such user','no such user known to this server')
return
end
defer actor:free()
- http.actor_profile(co,actor.ptr,meth)
+ http.actor_dispatch_mime(co, actor.ptr)
end
terra http.actor_profile_uid (
co: &lib.srv.convo,
- path: lib.mem.ptr(lib.mem.ref(int8)),
- meth: method.t
+ path: lib.mem.ptr(lib.mem.ref(int8))
)
if path.ct < 2 then
co:complain(404,'bad url','invalid user url')
return
end
@@ -142,11 +149,11 @@
co:complain(404, 'no such user', 'no user by that ID is known to this instance')
return
end
defer actor:free()
- http.actor_profile(co,actor.ptr,meth)
+ http.actor_dispatch_mime(co, actor.ptr)
end
terra http.login_form(co: &lib.srv.convo, meth: method.t)
if meth_get(meth) then
-- request a username
@@ -227,11 +234,11 @@
co:installkey('/',aid)
end
end
if act.ptr ~= nil and fakeact == false then act:free() end
else
- ::wrongmeth:: co:complain(405, 'method not allowed', 'that method is not meaningful for this endpoint') do return end
+ ::wrongmeth:: co:fail(405) do return end
end
return
end
terra http.post_compose(co: &lib.srv.convo, meth: method.t)
@@ -244,11 +251,11 @@
elseif meth == method.post then
var text, textlen = co:postv("post")
var acl, acllen = co:postv("acl")
var subj, subjlen = co:postv("subject")
if text == nil or acl == nil then
- co:complain(405, 'invalid post', 'every post must have at least body text and an ACL')
+ co:complain(400, 'invalid post', 'every post must have at least body text and an ACL')
return
end
if subj == nil then subj = '' end
var p = lib.store.post {
@@ -406,13 +413,13 @@
if not post then goto badurl end
lib.render.tweet_page(co, path, post.ptr)
do return end
- ::badurl:: do co:complain(404, 'invalid URL', 'this URL does not reference extant content or functionality') return end
- ::badop :: do co:complain(405, 'invalid operation', 'the operation you have attempted on this post is not meaningful') return end
- ::noauth:: do co:complain(401, 'unauthorized', 'you have not supplied the necessary credentials to perform this operation') return end
+ ::noauth:: do co:fail(401) return end
+ ::badurl:: do co:fail(404) return end
+ ::badop :: do co:fail(405) return end
end
local terra
credsec_for_uid(co: &lib.srv.convo, uid: uint64)
var act = co:ppostv('act')
@@ -923,60 +930,27 @@
return end
::e404:: do co:complain(404, 'artifact not found', 'no such artifact has been uploaded to this instance') return end
end
-local json = {}
-
-do wftpl = lib.tpl.mk [[{
- "subject": @$subj,
- "links": [
- { "rel": "self", "type": "application/ld+json", "href": @$href }
- ]
- }]]
- terra json.webfinger(co: &lib.srv.convo)
- var res = co:pgetv('resource')
- if (not res) or not res:startswith 'acct:' then goto err end
-
- -- technically we should look this user up in the database to make sure
- -- they actually exist, buuut that's costly and i doubt that's actually
- -- necessary for webfinger to do its job. so we cheat and just do string
- -- munging so lookups are as cheap as possible. TODO make sure this works
- -- in practice and doesn't cause any weird security problems
- var acct = res + 5
- var svp = lib.str.find(acct, '@')
- if svp:ref() then
- acct.ct = (svp.ptr - acct.ptr)
- svp:advance(1)
- if not svp:cmp(co.srv.cfg.domain) then goto err end
- end
- var tp = wftpl {
- subj = res;
- href = co:qstr('https://', co.srv.cfg.domain, '/@', acct);
- }
- co:json(tp:poolstr(&co.srv.pool))
-
- do return end -- error conditions
- ::err:: do co:json('{}') return end
- end
-end
-- entry points
-terra r.dispatch_http(co: &lib.srv.convo, uri: lib.mem.ptr(int8), meth: method.t)
+terra r.dispatch_http(co: &lib.srv.convo, uri: lib.mem.ptr(int8))
lib.dbg('handling URI of form ', {uri.ptr,uri.ct})
co.navbar = lib.render.nav(co)
-- some routes are non-hierarchical, and can be resolved with a simple strcmp
-- we run through those first before giving up and parsing the URI
+ var meth = co.method -- TODO unfuck this legacy bat shit
if uri.ptr == nil or uri.ptr[0] ~= @'/' then
co:complain(404, 'what the hell', 'how did you do that')
elseif uri.ct == 1 then -- root
if (co.srv.cfg.pol_sec == lib.srv.secmode.private or
co.srv.cfg.pol_sec == lib.srv.secmode.lockdown) and co.aid == 0 then
http.login_form(co, meth)
else http.timeline(co, hpath {ptr=nil,ct=0}) end
elseif uri.ptr[1] == @'@' then
- http.actor_profile_xid(co, uri, meth)
+ http.actor_profile_xid(co, uri)
elseif uri.ptr[1] == @'s' and uri.ptr[2] == @'/' and uri.ct > 3 then
if not meth_get(meth) then goto wrongmeth end
if not http.static_content(co, uri.ptr + 3, uri.ct - 3) then goto notfound end
elseif lib.str.ncmp('/avi/', uri.ptr, 5) == 0 then
http.local_avatar(co, [lib.mem.ptr(int8)] {ptr = uri.ptr + 5, ct = uri.ct - 5})
@@ -999,19 +973,29 @@
else co:reroute_cookie('/','auth=; Path=/')
end
else -- hierarchical routes
var path = lib.http.hier(&co.srv.pool, uri) --defer path:free()
if path.ct > 1 and path(0):cmp('user') then
- http.actor_profile_uid(co, path, meth)
+ http.actor_profile_uid(co, path)
elseif path.ct > 1 and path(0):cmp('post') then
http.tweet_page(co, path, meth)
elseif path(0):cmp('tl') then
http.timeline(co, path)
elseif path(0):cmp('.well-known') then
if path(1):cmp('webfinger') then
- json.webfinger(co)
+ if not co:matchmime(lib.http.mime.json) then goto nacc end
+ lib.api.webfinger(co)
end
+ elseif path(0):cmp('api') then
+ if path(1):cmp('parsav') then -- native API
+ elseif path(1):cmp('v1') then -- mastodon client api :/
+ elseif path(1):cmp('lp') then -- litepub endpoints
+ if path(2):cmp('outbox') then
+ lib.api.lp.outbox(co,uri,path + 3)
+ elseif path(2):cmp('inbox') then
+ end
+ else goto notfound end
elseif path(0):cmp('media') then
if co.aid == 0 then goto unauth end
http.media_manager(co, path, meth, co.who.id)
elseif path(0):cmp('doc') then
if not meth_get(meth) then goto wrongmeth end
@@ -1021,9 +1005,10 @@
http.configure(co,path,meth)
else goto notfound end
end
do return end
- ::wrongmeth:: co:complain(405, 'method not allowed', 'that method is not meaningful for this endpoint') do return end
- ::notfound:: co:complain(404, 'not found', 'no such resource available') do return end
- ::unauth:: co:complain(401, 'unauthorized', 'this content is not available at your clearance level') do return end
+ ::wrongmeth:: co:fail(405) do return end
+ ::nacc :: co:fail(406) do return end
+ ::notfound :: co:fail(404) do return end
+ ::unauth :: co:fail(401) do return end
end
Index: srv.t
==================================================================
--- srv.t
+++ srv.t
@@ -1,9 +1,19 @@
-- vim: ft=terra
local util = lib.util
local secmode = lib.enum { 'public', 'private', 'lockdown', 'isolate' }
local pstring = lib.mem.ptr(int8)
+local mimetypes = {
+ {'html', 'text/html'};
+ {'json', 'application/json'};
+ {'json', 'application/activity+json'};
+ {'json', 'application/ld+json'};
+ {'mkdown', 'text/markdown'};
+ {'text', 'text/plain'};
+ {'ansi', 'text/x-ansi'};
+}
+
local struct srv
local struct cfgcache {
secret: pstring
pol_sec: secmode.t
pol_reg: bool
@@ -180,10 +190,15 @@
str = {
['acl-follow' ] = {cfgfld = 'usrdef_pol_follow', fallback = 'local'};
['acl-follow-req'] = {cfgfld = 'usrdef_pol_follow_req', fallback = 'all'};
};
}
+
+terra convo:matchmime(mime: lib.http.mime.t): bool
+ return self.reqtype == [lib.http.mime.none]
+ or self.reqtype == mime
+end
terra convo:usercfg_str(uid: uint64, setting: pstring): pstring
var set = self.srv:actor_conf_str_get(&self.srv.pool, uid, setting)
if not set then
[(function()
@@ -318,11 +333,11 @@
lib.net.mg_send(self.con, data.ptr, data.ct)
lib.net.mg_send(self.con, '\r\n', 2)
end
terra convo:json(data: pstring)
- self:bytestream_trusted(false, 'application/ld+json', data:blob())
+ self:bytestream_trusted(false, 'application/activity+json; charset=utf-8', data:blob())
end
terra convo:bytestream(mime: pstring, data: lib.mem.ptr(uint8))
-- TODO this is not a satisfactory solution; it's a bandaid on a gaping
-- chest wound. ultimately we need to compile a whitelist of safe mime
@@ -372,26 +387,89 @@
p = lib.str.ncpy(p, '; Path=/', 9)
end
self:reroute_cookie(dest, &sesskey[0])
end
+terra convo:stra(sz: intptr) -- convenience function
+ var s: lib.str.acc
+ s:pool(&self.srv.pool,sz)
+ return s
+end
+
+convo.methods.qstr = macro(function(self, ...) -- convenience string builder
+ local exp = {...}
+ return `lib.str.acc{}:pcompose(&self.srv.pool, [exp]):finalize()
+end)
+
terra convo:complain(code: uint16, title: rawstring, msg: rawstring)
if msg == nil then msg = "i'm sorry, dave. i can't let you do that" end
- var ti: lib.str.acc ti:compose('error :: ', title)
- var bo: lib.str.acc bo:compose('
',title,'
',msg,'
')
- var body = [convo.page] {
- title = ti:finalize();
- body = bo:finalize();
- class = 'error';
- cache = false;
- }
-
- self:statpage(code, body)
-
- body.title:free()
- body.body:free()
+ if self:matchmime(lib.http.mime.html) then
+ var body = [convo.page] {
+ title = self:qstr('error :: ', title);
+ body = self:qstr('
',title,'
',msg,'
');
+ class = 'error';
+ cache = false;
+ }
+
+ self:statpage(code, body)
+ else
+ var pg = lib.http.page { respcode = code, body = pstring.null() }
+ var ctt = lib.http.mime.none
+ if self:matchmime(lib.http.mime.json) then ctt = lib.http.mime.json
+ pg.body = ([lib.tpl.mk'{"_parsav_error":@$ekind, "_parsav_error_desc":@$edesc}']
+ {ekind = title, edesc = msg}):poolstr(&self.srv.pool)
+ elseif self:matchmime(lib.http.mime.text) then ctt = lib.http.mime.text
+ pg.body = self:qstr('error: ',title,'\n',msg)
+ elseif self:matchmime(lib.http.mime.mkdown) then ctt = lib.http.mime.mkdown
+ pg.body = self:qstr('# error :: ',title,'\n\n',msg)
+ elseif self:matchmime(lib.http.mime.ansi) then ctt = lib.http.mime.ansi
+ pg.body = self:qstr('\27[1;31merror :: ',title,'\27[m\n',msg)
+ end
+ var cthdr = lib.http.header { 'Content-Type', 'text/plain' }
+ if ctt == lib.http.mime.none then
+ pg.headers.ct = 0
+ else
+ pg.headers = lib.typeof(pg.headers) { &cthdr, 1 }
+ switch ctt do
+ case [ctt.type](lib.http.mime.json) then
+ cthdr.value = 'application/json'
+ end
+ escape
+ for i,v in ipairs(mimetypes) do local key,mime = v[1],v[2]
+ if key ~= 'json' then
+ emit quote case [ctt.type](lib.http.mime.[key]) then cthdr.value = [mime] end end
+ end
+ end
+ end
+ end
+ end
+ pg:send(self.con)
+ end
+end
+
+terra convo:fail(code: uint16)
+ switch code do
+ escape
+ local stderrors = {
+ {400, 'bad request', "the action you have attempted on this resource is not meaningful"};
+ {401, 'unauthorized', "this resource is not available at your clearance level"};
+ {403, 'forbidden', "we can neither confirm nor deny the existence of this resource"};
+ {404, 'resource not found', "that resource is not extant on or known to this server"};
+ {405, 'method not allowed', "the method you have attempted on this resource is not meaningful"};
+ {406, 'not acceptable', "none of the suggested content types are a viable representation of this resource"};
+ {500, 'internal server error', "parsav did a fucksy wucksy"};
+ }
+
+ for i,v in ipairs(stderrors) do
+ emit quote case uint16([v[1]]) then
+ self:complain([v])
+ end end
+ end
+ end
+ else self:complain(500,'unknown error','an unrecognized error was thrown. this is a bug')
+ end
end
terra convo:confirm(title: pstring, msg: pstring, cancel: pstring)
var conf = data.view.confirm {
title = title;
@@ -407,21 +485,10 @@
}
self:stdpage(cf)
--cf.title:free()
end
-terra convo:stra(sz: intptr) -- convenience function
- var s: lib.str.acc
- s:pool(&self.srv.pool,sz)
- return s
-end
-
-convo.methods.qstr = macro(function(self, ...) -- convenience string builder
- local exp = {...}
- return `lib.str.acc{}:pcompose(&self.srv.pool, [exp]):finalize()
-end)
-
convo.methods.assertpow = macro(function(self, pow)
return quote
var ok = true
if self.aid == 0 or self.who.rights.powers.[pow:asvalue()]() == false then
ok = false
@@ -506,21 +573,11 @@
var s,l = self:getv(name)
return pstring { ptr = s, ct = l }
end
local route = {} -- these are defined in route.t, as they need access to renderers
-terra route.dispatch_http :: {&convo, lib.mem.ptr(int8), lib.http.method.t} -> {}
-
-local mimetypes = {
- {'html', 'text/html'};
- {'json', 'application/json'};
- {'json', 'application/ld+json'};
- {'json', 'application/activity+json'};
- {'mkdown', 'text/markdown'};
- {'text', 'text/plain'};
- {'ansi', 'text/x-ansi'};
-}
+terra route.dispatch_http :: {&convo, lib.mem.ptr(int8)} -> {}
local mimevar = symbol(lib.mem.ref(int8))
local mimeneg = `lib.http.mime.none
for i, t in ipairs(mimetypes) do
@@ -823,11 +880,11 @@
upmap:free()
end
end
end
- route.dispatch_http(&co, uri, co.method)
+ route.dispatch_http(&co, uri)
::fail::
if co.uploads.run > 0 then
for i=0,co.uploads.sz do
co.uploads(i).filename:free()
Index: str.t
==================================================================
--- str.t
+++ str.t
@@ -182,12 +182,12 @@
lib.mem.heapf(self.buf)
end
end;
terra m.acc:crush()
- --lib.dbg('crushing string accumulator')
if self.pool ~= nil then return self end -- no point unless at end of buffer
+ --lib.dbg('crushing string accumulator', &self.buf[0])
self.buf = [rawstring](lib.mem.heapr_raw(self.buf, self.sz))
self.space = self.sz
return self
end;
@@ -569,22 +569,22 @@
cur = cont
end
return acc:finalize()
end
-terra m.qesc(pool: &lib.mem.pool, str: m.t): m.t
+terra m.qesc(pool: &lib.mem.pool, str: m.t, wrap: bool): m.t
-- escape double-quotes
var a: m.acc a:pool(pool, str.ct + str.ct/2)
- a:lpush '"'
+ if wrap then a:lpush '"' end
for i=0, str.ct do
if str(i) == @'"' then a:lpush '\\"'
elseif str(i) == @'\\' then a:lpush '\\\\'
elseif str(i) < 0x20 then -- for json
var hex = lib.math.hexbyte(str(i))
a:lpush('\\u00'):push(&hex[0], 2)
else a:push(str.ptr + i,1) end
end
- a:lpush '"'
+ if wrap then a:lpush '"' end
return a:finalize()
end
return m
Index: tpl.t
==================================================================
--- tpl.t
+++ tpl.t
@@ -36,11 +36,11 @@
str = str:gsub(' ?', file)
end)
- for start, mode, key, stop in string.gmatch(str,'()'..tplchar..'([:!$]?)([-a-zA-Z0-9_]+)()') do
+ for start, mode, key, stop in string.gmatch(str,'()'..tplchar..'([+:!$#%^]?)([-a-zA-Z0-9_]+)()') do
if string.sub(str,start-1,start-1) ~= '\\' then
segs[#segs+1] = string.sub(str,last,start-1)
fields[#segs] = { key = key:gsub('-','_'), mode = (mode ~= '' and mode or nil) }
last = stop
end
@@ -67,27 +67,38 @@
end}
local rec = terralib.types.newstruct(string.format('template<%s>',tplspec.id or ''))
local symself = symbol(&rec)
do local kfac = {}
local sanmode = {}
+ local types = { ['^'] = uint64, ['#'] = uint64 }
+ local recmap = {}
for afterseg,fld in ipairs(fields) do
if not kfac[fld.key] then
rec.entries[#rec.entries + 1] = {
field = fld.key;
- type = lib.mem.ptr(int8);
+ type = types[fld.mode] or pstr;
}
+ recmap[fld.key] = rec.entries[#rec.entries]
end
kfac[fld.key] = (kfac[fld.key] or 0) + 1
sanmode[fld.key] = fld.mode == ':' and 6
or fld.mode == '!' and 5
- or fld.mode == '$' and 2 or 1
+ or (fld.mode == '$' or fld.mode == '+') and 2
+ or fld.mode == '^' and lib.math.shorthand.maxlen
+ or fld.mode == '#' and 20
+ or 1
end
for key, fac in pairs(kfac) do
local sanfac = sanmode[key]
-
- tallyup[#tallyup + 1] = quote
- [runningtally] = [runningtally] + ([symself].[key].ct)*fac*sanfac
+ if recmap[key].type ~= pstr then
+ tallyup[#tallyup + 1] = quote
+ [runningtally] = [runningtally] + fac*sanfac
+ end
+ else
+ tallyup[#tallyup + 1] = quote
+ [runningtally] = [runningtally] + ([symself].[key].ct)*fac*sanfac
+ end
end
end
end
local copiers = {}
@@ -104,31 +115,43 @@
appenders[#appenders+1] = quote [accumulator]:push([seg], [#seg]) end
if fields[idx] and fields[idx].mode then
local f = fields[idx]
local fp = `symself.[f.key]
local sanexp
- if f.mode == '$' then
- sanexp = `lib.str.qesc(pool, fp)
- else
- sanexp = `lib.html.sanitize(pool, fp, [f.mode == ':'])
- end
+ local nulexp
+ if f.mode == '$' then sanexp = `lib.str.qesc(pool, fp, true)
+ elseif f.mode == '+' then sanexp = `lib.str.qesc(pool, fp, false)
+ elseif f.mode == '#' then
+ sanexp = quote
+ var ibuf: int8[21]
+ var ptr = lib.math.decstr(fp, &ibuf[20])
+ in pstr {ptr=ptr, ct=&ibuf[20] - ptr} end
+ elseif f.mode == '^' then
+
+ sanexp = quote
+ var shbuf: lib.math.shorthand.t
+ var len = lib.math.shorthand.gen(fp, &shbuf[0])
+ in pstr {ptr=&shbuf[0],ct=len} end
+ else sanexp = `lib.html.sanitize(pool, fp, [f.mode == ':']) end
+ if f.mode == '^' or f.mode == '#' then nulexp = `true
+ else nulexp = `fp.ct > 0 end
copiers[#copiers+1] = quote
- if fp.ct > 0 then
+ if [nulexp] then
var san = sanexp
[cpypos] = lib.mem.cpy([cpypos], [&opaque](san.ptr), san.ct)
--san:free()
end
end
senders[#senders+1] = quote
- if fp.ct > 0 then
+ if [nulexp] then
var san = sanexp
lib.net.mg_send([destcon], san.ptr, san.ct)
--san:free()
end
end
appenders[#appenders+1] = quote
- if fp.ct > 0 then
+ if [nulexp] then
var san = sanexp
[accumulator]:ppush(san)
--san:free()
end
end