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