Overview
| Comment: | first steps towards litepub support |
|---|---|
| Downloads: | Tarball | ZIP archive | SQL archive |
| Timelines: | family | ancestors | descendants | both | trunk |
| Files: | files | file ages | folders |
| SHA3-256: |
26937ca853a6373e44a47f9114b99bca |
| User & Date: | lexi on 2021-01-25 12:40:08 |
| Other Links: | manifest | tags |
Context
|
2021-01-25
| ||
| 12:41 | commit missing file check-in: bd5794c0cc user: lexi tags: trunk | |
| 12:40 | first steps towards litepub support check-in: 26937ca853 user: lexi tags: trunk | |
|
2021-01-24
| ||
| 23:18 | enable webfinger check-in: 64ae6724c2 user: lexi tags: trunk | |
Changes
Added api/lp/actor.t version [b336ed6430].
1 +-- vim: ft=terra 2 +local tpl = lib.tpl.mk { 3 + sigil = '%'; 4 + body = [[{ 5 + "@context": "https://%+domain/s/litepub.jsonld", 6 + "type": "Person", 7 + "id": "%lpid", 8 + "url": "https://%+domain/@%+handle", 9 + "preferredUsername": %$handle, 10 + "name": %$nym, 11 + "summary": %$desc, 12 + "alsoKnownAs": ["https://%+domain/@%+handle"], 13 + "publicKey": { 14 + "id": "%lpid#ident-rsa", 15 + "owner": "%lpid", 16 + "publicKeyPem": %rsa 17 + }, 18 + "icon": { 19 + "type": "Image", 20 + "url": "https://%+domain%+avi" 21 + }, 22 + "capabilities": { "acceptsChatMessages": false }, 23 + "discoverable": true, 24 + "manuallyApprovedFollowers": %locked, 25 + "inbox": "https://%+domain/api/lp/inbox/user/%uid", 26 + "outbox": "https://%+domain/api/lp/outbox/user/%uid", 27 + "followers": "https://%+domain/api/lp/rel/%uid/followers", 28 + "following": "https://%+domain/api/lp/rel/%uid/following" 29 + }]]; 30 +} 31 + 32 +local pstr = lib.str.t 33 +terra cs(s: rawstring) return pstr {s, lib.str.sz(s)} end 34 + 35 +local terra 36 +api_lp_actor(co: &lib.srv.convo, actor: &lib.store.actor) 37 + var lpid = co:stra(64) 38 + lpid:lpush'https://':ppush(co.srv.cfg.domain):lpush'/user/':shpush(actor.id) 39 + var uid = co:stra(32) uid:shpush(actor.id) -- dumb hack bc lazy FIXME 40 + 41 + var body = tpl { 42 + domain = co.srv.cfg.domain; 43 + uid = uid:finalize(); 44 + lpid = lpid:finalize(); 45 + handle = cs(actor.handle); 46 + nym = cs(actor.nym); 47 + desc = cs(actor.bio); 48 + avi = cs(actor.avatar); 49 + rsa = ''; 50 + locked = 'false'; 51 + } 52 + 53 + co:json(body:poolstr(&co.srv.pool)) 54 +end 55 +return api_lp_actor
Added api/lp/outbox.t version [7ed4c4752c].
1 +-- vim: ft=terra 2 +local pstr = lib.str.t 3 + 4 +local terra 5 +lp_outbox(co: &lib.srv.convo, uri: pstr, here: lib.mem.ptr(lib.str.ref)) 6 + var path = lib.str.qesc(&co.srv.pool,uri,false) 7 + var json = co:stra(512) 8 + json:lpush '{"@context": "https://':ppush(co.srv.cfg.domain):lpush'/s/litepub.jsonld","id":"https://' 9 + :ppush(co.srv.cfg.domain):ppush(path) 10 + :lpush '"' 11 + var at = co:pgetv('at') 12 + lib.dbg('api path ', 13 + {here(0).ptr,here(0).ct}, ' / ', 14 + {here(1).ptr,here(1).ct}) 15 + json:lpush',"current":"https://':ppush(co.srv.cfg.domain):ppush(path):lpush'?at=top"' 16 + if not at then 17 + json:lpush',"type":"OrderedCollection","first":"https://':ppush(co.srv.cfg.domain):ppush(path):lpush'?at=top"' 18 + else 19 + if here(0):cmp 'user' and here.ct > 1 then 20 + var uid, uidok = lib.math.shorthand.parse(here(1).ptr, here(1).ct) 21 + if not uidok then goto e404 end 22 + var user = co.srv:actor_fetch_uid(uid) 23 + if not user then goto e404 end 24 + var time: lib.store.timepoint 25 + if at:cmp('top') then 26 + time = lib.osclock.time(nil) 27 + else 28 + var tp, ok = lib.math.decparse(at) 29 + if ok then time = tp end 30 + end 31 + lib.io.fmt('from-time: %llu\n', time) 32 + var posts = co.srv:post_enum_author_uid(uid, lib.store.range { 33 + mode = 1; -- time -> idx 34 + from_time = time; 35 + to_idx = 65; 36 + }) 37 + var oldest = time 38 + json:lpush',"partOf":"https://':ppush(co.srv.cfg.domain):ppush(path) 39 + :lpush'","type":"CollectionPage","orderedItems":[' 40 + if posts.sz > 0 then defer posts:free() 41 + for i=0, lib.math.smallest(posts.sz,64) do 42 + if i~=0 then json:lpush',' end 43 + json:ppush(lib.api.lp.tweet(co,posts(i).ptr,true)) 44 + oldest = lib.math.smallest(posts(i)().posted, oldest) 45 + end 46 + end 47 + json:lpush'],"totalItems":':ipush(posts.sz) 48 + if oldest ~= time and oldest > 0 and posts.sz > 64 then 49 + json:lpush',"next":"https://':ppush(co.srv.cfg.domain):ppush(path) 50 + :lpush'?at=':ipush(oldest-1):lpush'"' 51 + end 52 + 53 + else goto e404 end -- TODO 54 + end 55 + json:lpush[[}]] 56 + co:json(json:finalize()) 57 + do return end 58 + ::e404:: do co:fail(404) return end 59 +end 60 + 61 +return lp_outbox
Added api/lp/tweet.t version [e0805dcd2f].
1 +-- vim: ft=terra 2 +local pstr = lib.str.t 3 + 4 +local obj = lib.tpl.mk [[{ 5 + "\@context": "https://@+domain/s/litepub.jsonld", 6 + "type": "Note", 7 + "id": "https://@+domain/post/@^pid", 8 + "content": @$html, 9 + "source": @$raw, 10 + "attributedTo": "https://@+domain/user/@^uid", 11 + "published": "@pubtime" 12 + @extra 13 +}]] 14 + 15 +local wrap = lib.tpl.mk [[{ 16 + "\@context": "https://@+domain/s/litepub.jsonld", 17 + "type": "@kind", 18 + "actor": "https://@+domain/user/@^uid", 19 + "published": "@$pubtime", 20 + "id": "https://@+domain/api/lp/act/@^aid", 21 + "object": @obj 22 +}]] 23 + 24 +local terra 25 +lp_tweet(co: &lib.srv.convo, p: &lib.store.post, act_wrap: bool) 26 + 27 + var tweet = (obj { 28 + domain = co.srv.cfg.domain, uid = p.author, pid = p.id; 29 + html = lib.smackdown.html(&co.srv.pool, p.body, false); 30 + raw = p.body, pubtime = '', extra = ''; 31 + }):poolstr(&co.srv.pool) 32 + 33 + if act_wrap then 34 + return (wrap { 35 + domain = co.srv.cfg.domain; 36 + kind = lib.trn(p.rtdby == 0, 'Create', 'Announce'); 37 + uid = lib.trn(p.rtdby == 0, p.author, p.rtdby); 38 + aid = lib.trn(p.rtdby == 0, p.id, p.rtact); 39 + pubtime = '', obj = tweet; 40 + }):poolstr(&co.srv.pool) 41 + else 42 + return tweet 43 + end 44 +end 45 + 46 +return lp_tweet
Added api/webfinger.t version [c64d390bdc].
1 +-- vim: ft=terra 2 +wftpl = lib.tpl.mk [[{ 3 + "subject": @$subj, 4 + "aliases": [ @$href, @$pfp ], 5 + "links": [ 6 + { "rel": "self", "type": "application/activity+json", "href": @$href }, 7 + { "rel": "self", 8 + "type": "application/ld+json; profile=\"https://www.w3.org/ns/activitystreams\"", "href": @$href }, 9 + { "rel": "http://webfinger.net/rel/profile-page", 10 + "type": "text/html", "href": @$pfp } 11 + ] 12 +}]] 13 + 14 +local terra 15 +webfinger(co: &lib.srv.convo) 16 + var res = co:pgetv('resource') 17 + if (not res) or not res:startswith 'acct:' then goto err end 18 + 19 + var acct = res + 5 20 + var svp = lib.str.find(acct, '@') 21 + if svp:ref() then 22 + acct.ct = (svp.ptr - acct.ptr) 23 + svp:advance(1) 24 + if not svp:cmp(co.srv.cfg.domain) then goto err end 25 + end 26 + 27 + var actor = co.srv:actor_fetch_xid(acct) 28 + if not actor then goto err end 29 + do defer actor:free() 30 + if actor().origin ~= 0 then goto err end 31 + var href = co:stra(64) 32 + var pfp = co:stra(64) 33 + href:lpush'https://':ppush(co.srv.cfg.domain):lpush'/user/':shpush(actor().id) 34 + pfp:lpush'https://':ppush(co.srv.cfg.domain):lpush'/@':ppush(acct) 35 + 36 + var tp = wftpl { 37 + subj = res; 38 + href = href:finalize(); 39 + pfp = pfp:finalize(); 40 + } 41 + co:json(tp:poolstr(&co.srv.pool)) 42 + 43 + return 44 + end 45 + -- error conditions 46 + ::err:: do co:json('{}') return end 47 +end 48 +return webfinger
Modified config.lua from [74b801cbf1] to [f9545a425d].
62 62 {'icon.svg', 'image/svg+xml'}; 63 63 {'padlock.svg', 'image/svg+xml'}; 64 64 {'warn.svg', 'image/svg+xml'}; 65 65 {'query.webp', 'image/webp'}; 66 66 {'reply.webp', 'image/webp'}; 67 67 {'file.webp', 'image/webp'}; 68 68 {'follow.webp', 'image/webp'}; 69 + 70 + {'litepub.jsonld', 'application/ld+json; charset=utf-8'}; 69 71 -- keep in mind before you add anything to this list: these are not 70 72 -- just files parsav can access, they are files that are *kept in 71 73 -- memory* for fast access the entire time parsav is running, and 72 74 -- which need to be loaded into memory before the program can even 73 75 -- start. it's imperative to keep these as small and few in number 74 76 -- as is realistically possible. 75 77 };
Modified math.t from [12858be43d] to [60b64e2aea].
1 1 -- vim: ft=terra 2 2 local m = { 3 3 shorthand = {maxlen = 14}; 4 - ll = { 5 - ctpop_u8 = terralib.intrinsic('llvm.ctpop.i8', uint8 -> uint8); 6 - }; 7 4 } 5 +m.shorthand.t = int8[m.shorthand.maxlen] 8 6 9 7 local pstring = lib.mem.ptr(int8) 10 8 11 9 -- swap in place -- faster on little endian 12 10 m.netswap_ip = macro(function(ty, src, dest) 13 11 if ty:astype().type ~= 'integer' then error('bad type') end 14 12 local bytes = ty:astype().bytes ................................................................................ 64 62 var o = n 65 63 for i=0,fac do n = n * o end 66 64 return n 67 65 end 68 66 69 67 terra m.shorthand.gen(val: uint64, dest: rawstring): ptrdiff 70 68 var lst = "0123456789-ABCDEFGHIJKLMNOPQRSTUVWXYZ:abcdefghijklmnopqrstuvwxyz" 71 - var buf: int8[m.shorthand.maxlen] 69 + var buf: m.shorthand.t 72 70 var ptr = [&int8](buf) 73 71 while val ~= 0 do 74 72 var v = val % 64 75 73 @ptr = lst[v] 76 74 ptr = ptr + 1 77 75 val = val / 64 78 76 end
Modified parsav.t from [dd405c9e82] to [1d36a792dc].
13 13 for m in l:gmatch('([^:]+)') do path[#path+1]=m end 14 14 local tgt = lib 15 15 for i=1,#path-1 do 16 16 if tgt[path[i]] == nil then tgt[path[i]] = {} end 17 17 tgt = tgt[path[i]] 18 18 end 19 19 local chunk = terralib.loadfile(l:gsub(':','/') .. '.t') 20 - if chunk ~= nil then 20 + if chunk ~= nil then 21 21 tgt[path[#path]:gsub('-','_')] = chunk() 22 22 print(' \27[1m[ \27[32mok\27[;1m ]\27[m') 23 23 else 24 24 print(' \27[1m[\27[31mfail\27[;1m]\27[m') 25 25 os.exit(2) 26 26 end 27 27 end ................................................................................ 106 106 trn = macro(function(cond, i, e) 107 107 return quote 108 108 var c: bool = [cond] 109 109 var r: i.tree.type 110 110 if c == true then r = i else r = e end 111 111 in r end 112 112 end); 113 + typeof = macro(function(exp) return exp.tree.type end); 113 114 coalesce = macro(function(...) 114 115 local args = {...} 115 116 local ty = args[1].tree.type 116 117 local val = symbol(ty) 117 118 local empty 118 119 if ty.ptr_basetype then empty = `[ty]{ptr=nil,ct=0} 119 120 elseif ty.type == 'integer' then empty = `0 ................................................................................ 496 497 497 498 'render:conf:profile'; 498 499 'render:conf:circles'; 499 500 'render:conf:sec'; 500 501 'render:conf:users'; 501 502 'render:conf:avi'; 502 503 'render:conf'; 504 + 505 + 'api:lp:actor'; 506 + 'api:lp:tweet'; 507 + 'api:lp:outbox'; 508 + 'api:webfinger'; 503 509 'route'; 504 510 } 505 511 506 512 do 507 513 local p = string.format('parsav: %s\nbuilt on %s\n', config.build.str, config.build.when) 508 514 terra version() lib.io.send(1, p, [#p]) end 509 515 end
Modified render/profile.t from [c63a8aaea6] to [74b4c77645].
195 195 end 196 196 197 197 if relationship.recip.follow() then 198 198 comments:lpush('<li style="--co:30">follows you</li>') 199 199 end 200 200 201 201 var circpanel: lib.str.acc 202 + var circstr = pstr.null() 202 203 if co.aid ~= 0 then 203 204 circpanel = co:stra(128) 204 205 var allcircs = co.srv:circle_search(&co.srv.pool, co.who.id, 0) 205 206 if allcircs:ref() then 206 207 var mycircs = co.srv:circle_memberships_uid(&co.srv.pool, co.who.id, actor.id) 207 208 for i=0, allcircs.ct do 208 209 circpanel:lpush '<label><input type="checkbox" name="circle" value="' ................................................................................ 215 216 end 216 217 end 217 218 circpanel:lpush '> ' 218 219 :ppush(allcircs(i).name) 219 220 :lpush '</label>' 220 221 end 221 222 end 223 + circstr = circpanel:finalize() 222 224 end 223 225 224 226 var profile = data.view.profile { 225 227 nym = fullname; 226 228 bio = bio; 227 229 xid = cs(actor.xid); 228 230 avatar = cs(actor.avatar); ................................................................................ 231 233 nfollowers = sn_followers, nmutuals = sn_mutuals; 232 234 tweetday = cs(timestr); 233 235 timephrase = lib.trn(actor.origin == 0, pstr 'joined', pstr 'known since'); 234 236 235 237 remarks = ''; 236 238 237 239 auxbtn = auxp; 238 - circles = circpanel:finalize(); 240 + circles = circstr; 239 241 relations = relbtns:finalize(); 240 242 sanctions = sancbtns:finalize(); 241 243 } 242 244 if comments.sz > 0 then profile.remarks = comments:finalize() end 243 245 244 246 var ret = profile:poolstr(&co.srv.pool) 245 247 -- auxp:free() 246 248 --if actor.bio ~= nil then bio:free() end 247 249 --if comments.sz > 0 then profile.remarks:free() end 248 250 return ret 249 251 end 250 252 251 253 return render_profile
Modified route.t from [325df84c8e] to [db07af2616].
93 93 if not go or go(0) ~= @'/' then 94 94 lib.render.user_page(co, actor, &rel) 95 95 else 96 96 co:reroute(go.ptr) 97 97 end 98 98 end 99 99 100 -terra http.actor_profile_xid(co: &lib.srv.convo, uri: lib.mem.ptr(int8), meth: method.t) 100 +terra http.actor_dispatch_mime(co: &lib.srv.convo, actor: &lib.store.actor) 101 + if co:matchmime(lib.http.mime.html) then 102 + http.actor_profile(co,actor,co.method) 103 + elseif co:matchmime(lib.http.mime.json) then 104 + lib.api.lp.actor(co, actor) 105 + else co:fail(406) end 106 +end 107 + 108 +terra http.actor_profile_xid(co: &lib.srv.convo, uri: lib.mem.ptr(int8)) 101 109 var handle = [lib.mem.ptr(int8)] { ptr = &uri.ptr[2], ct = 0 } 102 110 for i=2,uri.ct do 103 111 if uri.ptr[i] == @'/' or uri.ptr[i] == 0 then handle.ct = i - 2 break end 104 112 end 105 113 if handle.ct == 0 then 106 114 handle.ct = uri.ct - 2 107 115 uri:advance(uri.ct) ................................................................................ 114 122 var actor = co.srv:actor_fetch_xid(handle) 115 123 if actor.ptr == nil then 116 124 co:complain(404,'no such user','no such user known to this server') 117 125 return 118 126 end 119 127 defer actor:free() 120 128 121 - http.actor_profile(co,actor.ptr,meth) 129 + http.actor_dispatch_mime(co, actor.ptr) 122 130 end 123 131 124 132 terra http.actor_profile_uid ( 125 133 co: &lib.srv.convo, 126 - path: lib.mem.ptr(lib.mem.ref(int8)), 127 - meth: method.t 134 + path: lib.mem.ptr(lib.mem.ref(int8)) 128 135 ) 129 136 if path.ct < 2 then 130 137 co:complain(404,'bad url','invalid user url') 131 138 return 132 139 end 133 140 134 141 var uid, ok = lib.math.shorthand.parse(path.ptr[1].ptr, path.ptr[1].ct) ................................................................................ 140 147 var actor = co.srv:actor_fetch_uid(uid) 141 148 if actor.ptr == nil then 142 149 co:complain(404, 'no such user', 'no user by that ID is known to this instance') 143 150 return 144 151 end 145 152 defer actor:free() 146 153 147 - http.actor_profile(co,actor.ptr,meth) 154 + http.actor_dispatch_mime(co, actor.ptr) 148 155 end 149 156 150 157 terra http.login_form(co: &lib.srv.convo, meth: method.t) 151 158 if meth_get(meth) then 152 159 -- request a username 153 160 lib.render.login(co, nil, nil, pstring.null()) 154 161 elseif meth == method.post then ................................................................................ 225 232 lib.render.login(co, nil, nil, 'authentication failure') 226 233 else 227 234 co:installkey('/',aid) 228 235 end 229 236 end 230 237 if act.ptr ~= nil and fakeact == false then act:free() end 231 238 else 232 - ::wrongmeth:: co:complain(405, 'method not allowed', 'that method is not meaningful for this endpoint') do return end 239 + ::wrongmeth:: co:fail(405) do return end 233 240 end 234 241 return 235 242 end 236 243 237 244 terra http.post_compose(co: &lib.srv.convo, meth: method.t) 238 245 if not co:assertpow('post') then return end 239 246 --if co.who.rights.powers.post() == false then ................................................................................ 242 249 if meth_get(meth) then 243 250 lib.render.compose(co, nil, nil) 244 251 elseif meth == method.post then 245 252 var text, textlen = co:postv("post") 246 253 var acl, acllen = co:postv("acl") 247 254 var subj, subjlen = co:postv("subject") 248 255 if text == nil or acl == nil then 249 - co:complain(405, 'invalid post', 'every post must have at least body text and an ACL') 256 + co:complain(400, 'invalid post', 'every post must have at least body text and an ACL') 250 257 return 251 258 end 252 259 if subj == nil then subj = '' end 253 260 254 261 var p = lib.store.post { 255 262 author = co.who.id, acl = acl; 256 263 body = text, subject = subj; ................................................................................ 404 411 end 405 412 406 413 if not post then goto badurl end 407 414 408 415 lib.render.tweet_page(co, path, post.ptr) 409 416 do return end 410 417 411 - ::badurl:: do co:complain(404, 'invalid URL', 'this URL does not reference extant content or functionality') return end 412 - ::badop :: do co:complain(405, 'invalid operation', 'the operation you have attempted on this post is not meaningful') return end 413 - ::noauth:: do co:complain(401, 'unauthorized', 'you have not supplied the necessary credentials to perform this operation') return end 418 + ::noauth:: do co:fail(401) return end 419 + ::badurl:: do co:fail(404) return end 420 + ::badop :: do co:fail(405) return end 414 421 end 415 422 416 423 local terra 417 424 credsec_for_uid(co: &lib.srv.convo, uid: uint64) 418 425 var act = co:ppostv('act') 419 426 if not act then return true end 420 427 lib.dbg('handling credential action') ................................................................................ 921 928 do defer data:free() defer mime:free() 922 929 co:bytestream(mime,data) 923 930 return end 924 931 925 932 ::e404:: do co:complain(404, 'artifact not found', 'no such artifact has been uploaded to this instance') return end 926 933 end 927 934 928 -local json = {} 929 - 930 -do wftpl = lib.tpl.mk [[{ 931 - "subject": @$subj, 932 - "links": [ 933 - { "rel": "self", "type": "application/ld+json", "href": @$href } 934 - ] 935 - }]] 936 - terra json.webfinger(co: &lib.srv.convo) 937 - var res = co:pgetv('resource') 938 - if (not res) or not res:startswith 'acct:' then goto err end 939 - 940 - -- technically we should look this user up in the database to make sure 941 - -- they actually exist, buuut that's costly and i doubt that's actually 942 - -- necessary for webfinger to do its job. so we cheat and just do string 943 - -- munging so lookups are as cheap as possible. TODO make sure this works 944 - -- in practice and doesn't cause any weird security problems 945 - var acct = res + 5 946 - var svp = lib.str.find(acct, '@') 947 - if svp:ref() then 948 - acct.ct = (svp.ptr - acct.ptr) 949 - svp:advance(1) 950 - if not svp:cmp(co.srv.cfg.domain) then goto err end 951 - end 952 - var tp = wftpl { 953 - subj = res; 954 - href = co:qstr('https://', co.srv.cfg.domain, '/@', acct); 955 - } 956 - co:json(tp:poolstr(&co.srv.pool)) 957 - 958 - do return end -- error conditions 959 - ::err:: do co:json('{}') return end 960 - end 961 -end 962 935 963 936 -- entry points 964 -terra r.dispatch_http(co: &lib.srv.convo, uri: lib.mem.ptr(int8), meth: method.t) 937 +terra r.dispatch_http(co: &lib.srv.convo, uri: lib.mem.ptr(int8)) 965 938 lib.dbg('handling URI of form ', {uri.ptr,uri.ct}) 966 939 co.navbar = lib.render.nav(co) 967 940 -- some routes are non-hierarchical, and can be resolved with a simple strcmp 968 941 -- we run through those first before giving up and parsing the URI 942 + var meth = co.method -- TODO unfuck this legacy bat shit 969 943 if uri.ptr == nil or uri.ptr[0] ~= @'/' then 970 944 co:complain(404, 'what the hell', 'how did you do that') 971 945 elseif uri.ct == 1 then -- root 972 946 if (co.srv.cfg.pol_sec == lib.srv.secmode.private or 973 947 co.srv.cfg.pol_sec == lib.srv.secmode.lockdown) and co.aid == 0 then 974 948 http.login_form(co, meth) 975 949 else http.timeline(co, hpath {ptr=nil,ct=0}) end 976 950 elseif uri.ptr[1] == @'@' then 977 - http.actor_profile_xid(co, uri, meth) 951 + http.actor_profile_xid(co, uri) 978 952 elseif uri.ptr[1] == @'s' and uri.ptr[2] == @'/' and uri.ct > 3 then 979 953 if not meth_get(meth) then goto wrongmeth end 980 954 if not http.static_content(co, uri.ptr + 3, uri.ct - 3) then goto notfound end 981 955 elseif lib.str.ncmp('/avi/', uri.ptr, 5) == 0 then 982 956 http.local_avatar(co, [lib.mem.ptr(int8)] {ptr = uri.ptr + 5, ct = uri.ct - 5}) 983 957 elseif lib.str.ncmp('/file/', uri.ptr, 6) == 0 then 984 958 http.file_serve_raw(co, [lib.mem.ptr(int8)] {ptr = uri.ptr + 6, ct = uri.ct - 6}) ................................................................................ 997 971 if co.aid == 0 998 972 then goto notfound 999 973 else co:reroute_cookie('/','auth=; Path=/') 1000 974 end 1001 975 else -- hierarchical routes 1002 976 var path = lib.http.hier(&co.srv.pool, uri) --defer path:free() 1003 977 if path.ct > 1 and path(0):cmp('user') then 1004 - http.actor_profile_uid(co, path, meth) 978 + http.actor_profile_uid(co, path) 1005 979 elseif path.ct > 1 and path(0):cmp('post') then 1006 980 http.tweet_page(co, path, meth) 1007 981 elseif path(0):cmp('tl') then 1008 982 http.timeline(co, path) 1009 983 elseif path(0):cmp('.well-known') then 1010 984 if path(1):cmp('webfinger') then 1011 - json.webfinger(co) 985 + if not co:matchmime(lib.http.mime.json) then goto nacc end 986 + lib.api.webfinger(co) 1012 987 end 988 + elseif path(0):cmp('api') then 989 + if path(1):cmp('parsav') then -- native API 990 + elseif path(1):cmp('v1') then -- mastodon client api :/ 991 + elseif path(1):cmp('lp') then -- litepub endpoints 992 + if path(2):cmp('outbox') then 993 + lib.api.lp.outbox(co,uri,path + 3) 994 + elseif path(2):cmp('inbox') then 995 + end 996 + else goto notfound end 1013 997 elseif path(0):cmp('media') then 1014 998 if co.aid == 0 then goto unauth end 1015 999 http.media_manager(co, path, meth, co.who.id) 1016 1000 elseif path(0):cmp('doc') then 1017 1001 if not meth_get(meth) then goto wrongmeth end 1018 1002 http.documentation(co, path) 1019 1003 elseif path(0):cmp('conf') then 1020 1004 if co.aid == 0 then goto unauth end 1021 1005 http.configure(co,path,meth) 1022 1006 else goto notfound end 1023 1007 end 1024 1008 do return end 1025 1009 1026 - ::wrongmeth:: co:complain(405, 'method not allowed', 'that method is not meaningful for this endpoint') do return end 1027 - ::notfound:: co:complain(404, 'not found', 'no such resource available') do return end 1028 - ::unauth:: co:complain(401, 'unauthorized', 'this content is not available at your clearance level') do return end 1010 + ::wrongmeth:: co:fail(405) do return end 1011 + ::nacc :: co:fail(406) do return end 1012 + ::notfound :: co:fail(404) do return end 1013 + ::unauth :: co:fail(401) do return end 1029 1014 end
Modified srv.t from [557555fd0b] to [68c9cc33d4].
1 1 -- vim: ft=terra 2 2 local util = lib.util 3 3 local secmode = lib.enum { 'public', 'private', 'lockdown', 'isolate' } 4 4 local pstring = lib.mem.ptr(int8) 5 +local mimetypes = { 6 + {'html', 'text/html'}; 7 + {'json', 'application/json'}; 8 + {'json', 'application/activity+json'}; 9 + {'json', 'application/ld+json'}; 10 + {'mkdown', 'text/markdown'}; 11 + {'text', 'text/plain'}; 12 + {'ansi', 'text/x-ansi'}; 13 +} 14 + 5 15 local struct srv 6 16 local struct cfgcache { 7 17 secret: pstring 8 18 pol_sec: secmode.t 9 19 pol_reg: bool 10 20 pol_autoherald: bool 11 21 credmgd: bool ................................................................................ 178 188 179 189 local usrdefs = { 180 190 str = { 181 191 ['acl-follow' ] = {cfgfld = 'usrdef_pol_follow', fallback = 'local'}; 182 192 ['acl-follow-req'] = {cfgfld = 'usrdef_pol_follow_req', fallback = 'all'}; 183 193 }; 184 194 } 195 + 196 +terra convo:matchmime(mime: lib.http.mime.t): bool 197 + return self.reqtype == [lib.http.mime.none] 198 + or self.reqtype == mime 199 +end 185 200 186 201 terra convo:usercfg_str(uid: uint64, setting: pstring): pstring 187 202 var set = self.srv:actor_conf_str_get(&self.srv.pool, uid, setting) 188 203 if not set then 189 204 [(function() 190 205 local q = quote return pstring.null() end 191 206 for key, dfl in pairs(usrdefs.str) do ................................................................................ 316 331 if not lockdown then lockhdr = "" end 317 332 lib.net.mg_printf(self.con, "HTTP/1.1 200 OK\r\nContent-Type: %.*s\r\nContent-Length: %llu\r\n%sX-Content-Options: nosniff\r\n\r\n", mime.ct, mime.ptr, data.ct + 2, lockhdr) 318 333 lib.net.mg_send(self.con, data.ptr, data.ct) 319 334 lib.net.mg_send(self.con, '\r\n', 2) 320 335 end 321 336 322 337 terra convo:json(data: pstring) 323 - self:bytestream_trusted(false, 'application/ld+json', data:blob()) 338 + self:bytestream_trusted(false, 'application/activity+json; charset=utf-8', data:blob()) 324 339 end 325 340 326 341 terra convo:bytestream(mime: pstring, data: lib.mem.ptr(uint8)) 327 342 -- TODO this is not a satisfactory solution; it's a bandaid on a gaping 328 343 -- chest wound. ultimately we need to compile a whitelist of safe mime 329 344 -- types as part of mimelib, but that is no small task. for now, this 330 345 -- will keep the patient from immediately bleeding out ................................................................................ 370 385 p = p + lib.session.cookie_gen(self.srv.cfg.secret, aid, lib.osclock.time(nil), p) 371 386 lib.dbg('sending cookie ',{&sesskey[0],15}) 372 387 p = lib.str.ncpy(p, '; Path=/', 9) 373 388 end 374 389 self:reroute_cookie(dest, &sesskey[0]) 375 390 end 376 391 392 +terra convo:stra(sz: intptr) -- convenience function 393 + var s: lib.str.acc 394 + s:pool(&self.srv.pool,sz) 395 + return s 396 +end 397 + 398 +convo.methods.qstr = macro(function(self, ...) -- convenience string builder 399 + local exp = {...} 400 + return `lib.str.acc{}:pcompose(&self.srv.pool, [exp]):finalize() 401 +end) 402 + 377 403 terra convo:complain(code: uint16, title: rawstring, msg: rawstring) 378 404 if msg == nil then msg = "i'm sorry, dave. i can't let you do that" end 379 405 380 - var ti: lib.str.acc ti:compose('error :: ', title) 381 - var bo: lib.str.acc bo:compose('<div class="message"><img class="icon" src="/s/warn.svg"><h1>',title,'</h1><p>',msg,'</p></div>') 382 - var body = [convo.page] { 383 - title = ti:finalize(); 384 - body = bo:finalize(); 385 - class = 'error'; 386 - cache = false; 387 - } 388 - 389 - self:statpage(code, body) 390 - 391 - body.title:free() 392 - body.body:free() 406 + if self:matchmime(lib.http.mime.html) then 407 + var body = [convo.page] { 408 + title = self:qstr('error :: ', title); 409 + body = self:qstr('<div class="message"><img class="icon" src="/s/warn.svg"><h1>',title,'</h1><p>',msg,'</p></div>'); 410 + class = 'error'; 411 + cache = false; 412 + } 413 + 414 + self:statpage(code, body) 415 + else 416 + var pg = lib.http.page { respcode = code, body = pstring.null() } 417 + var ctt = lib.http.mime.none 418 + if self:matchmime(lib.http.mime.json) then ctt = lib.http.mime.json 419 + pg.body = ([lib.tpl.mk'{"_parsav_error":@$ekind, "_parsav_error_desc":@$edesc}'] 420 + {ekind = title, edesc = msg}):poolstr(&self.srv.pool) 421 + elseif self:matchmime(lib.http.mime.text) then ctt = lib.http.mime.text 422 + pg.body = self:qstr('error: ',title,'\n',msg) 423 + elseif self:matchmime(lib.http.mime.mkdown) then ctt = lib.http.mime.mkdown 424 + pg.body = self:qstr('# error :: ',title,'\n\n',msg) 425 + elseif self:matchmime(lib.http.mime.ansi) then ctt = lib.http.mime.ansi 426 + pg.body = self:qstr('\27[1;31merror :: ',title,'\27[m\n',msg) 427 + end 428 + var cthdr = lib.http.header { 'Content-Type', 'text/plain' } 429 + if ctt == lib.http.mime.none then 430 + pg.headers.ct = 0 431 + else 432 + pg.headers = lib.typeof(pg.headers) { &cthdr, 1 } 433 + switch ctt do 434 + case [ctt.type](lib.http.mime.json) then 435 + cthdr.value = 'application/json' 436 + end 437 + escape 438 + for i,v in ipairs(mimetypes) do local key,mime = v[1],v[2] 439 + if key ~= 'json' then 440 + emit quote case [ctt.type](lib.http.mime.[key]) then cthdr.value = [mime] end end 441 + end 442 + end 443 + end 444 + end 445 + end 446 + pg:send(self.con) 447 + end 448 +end 449 + 450 +terra convo:fail(code: uint16) 451 + switch code do 452 + escape 453 + local stderrors = { 454 + {400, 'bad request', "the action you have attempted on this resource is not meaningful"}; 455 + {401, 'unauthorized', "this resource is not available at your clearance level"}; 456 + {403, 'forbidden', "we can neither confirm nor deny the existence of this resource"}; 457 + {404, 'resource not found', "that resource is not extant on or known to this server"}; 458 + {405, 'method not allowed', "the method you have attempted on this resource is not meaningful"}; 459 + {406, 'not acceptable', "none of the suggested content types are a viable representation of this resource"}; 460 + {500, 'internal server error', "parsav did a fucksy wucksy"}; 461 + } 462 + 463 + for i,v in ipairs(stderrors) do 464 + emit quote case uint16([v[1]]) then 465 + self:complain([v]) 466 + end end 467 + end 468 + end 469 + else self:complain(500,'unknown error','an unrecognized error was thrown. this is a bug') 470 + end 393 471 end 394 472 395 473 terra convo:confirm(title: pstring, msg: pstring, cancel: pstring) 396 474 var conf = data.view.confirm { 397 475 title = title; 398 476 query = msg; 399 477 cancel = cancel; ................................................................................ 405 483 class = 'query'; 406 484 body = body; cache = false; 407 485 } 408 486 self:stdpage(cf) 409 487 --cf.title:free() 410 488 end 411 489 412 -terra convo:stra(sz: intptr) -- convenience function 413 - var s: lib.str.acc 414 - s:pool(&self.srv.pool,sz) 415 - return s 416 -end 417 - 418 -convo.methods.qstr = macro(function(self, ...) -- convenience string builder 419 - local exp = {...} 420 - return `lib.str.acc{}:pcompose(&self.srv.pool, [exp]):finalize() 421 -end) 422 - 423 490 convo.methods.assertpow = macro(function(self, pow) 424 491 return quote 425 492 var ok = true 426 493 if self.aid == 0 or self.who.rights.powers.[pow:asvalue()]() == false then 427 494 ok = false 428 495 self:complain(403,'insufficient privileges',['you lack the <strong>'..pow:asvalue()..'</strong> power and cannot perform this action']) 429 496 end ................................................................................ 504 571 end 505 572 terra convo:pgetv(name: rawstring) 506 573 var s,l = self:getv(name) 507 574 return pstring { ptr = s, ct = l } 508 575 end 509 576 510 577 local route = {} -- these are defined in route.t, as they need access to renderers 511 -terra route.dispatch_http :: {&convo, lib.mem.ptr(int8), lib.http.method.t} -> {} 512 - 513 -local mimetypes = { 514 - {'html', 'text/html'}; 515 - {'json', 'application/json'}; 516 - {'json', 'application/ld+json'}; 517 - {'json', 'application/activity+json'}; 518 - {'mkdown', 'text/markdown'}; 519 - {'text', 'text/plain'}; 520 - {'ansi', 'text/x-ansi'}; 521 -} 578 +terra route.dispatch_http :: {&convo, lib.mem.ptr(int8)} -> {} 522 579 523 580 local mimevar = symbol(lib.mem.ref(int8)) 524 581 local mimeneg = `lib.http.mime.none 525 582 526 583 for i, t in ipairs(mimetypes) do 527 584 local name, mime = t[1], t[2] 528 585 mimeneg = quote ................................................................................ 821 878 end 822 879 bsr:free() 823 880 upmap:free() 824 881 end 825 882 end 826 883 end 827 884 828 - route.dispatch_http(&co, uri, co.method) 885 + route.dispatch_http(&co, uri) 829 886 830 887 ::fail:: 831 888 if co.uploads.run > 0 then 832 889 for i=0,co.uploads.sz do 833 890 co.uploads(i).filename:free() 834 891 co.uploads(i).field:free() 835 892 end
Modified str.t from [6c699847d5] to [072253bf19].
180 180 end 181 181 if self.buf ~= nil and self.space > 0 then 182 182 lib.mem.heapf(self.buf) 183 183 end 184 184 end; 185 185 186 186 terra m.acc:crush() 187 - --lib.dbg('crushing string accumulator') 188 187 if self.pool ~= nil then return self end -- no point unless at end of buffer 188 + --lib.dbg('crushing string accumulator', &self.buf[0]) 189 189 self.buf = [rawstring](lib.mem.heapr_raw(self.buf, self.sz)) 190 190 self.space = self.sz 191 191 return self 192 192 end; 193 193 194 194 terra m.acc:finalize() 195 195 --lib.dbg('finalizing string accumulator') ................................................................................ 567 567 var add, cont = disemvowel_codepoint(cur) 568 568 if add:ref() then acc:ppush(add) end 569 569 cur = cont 570 570 end 571 571 return acc:finalize() 572 572 end 573 573 574 -terra m.qesc(pool: &lib.mem.pool, str: m.t): m.t 574 +terra m.qesc(pool: &lib.mem.pool, str: m.t, wrap: bool): m.t 575 575 -- escape double-quotes 576 576 var a: m.acc a:pool(pool, str.ct + str.ct/2) 577 - a:lpush '"' 577 + if wrap then a:lpush '"' end 578 578 for i=0, str.ct do 579 579 if str(i) == @'"' then a:lpush '\\"' 580 580 elseif str(i) == @'\\' then a:lpush '\\\\' 581 581 elseif str(i) < 0x20 then -- for json 582 582 var hex = lib.math.hexbyte(str(i)) 583 583 a:lpush('\\u00'):push(&hex[0], 2) 584 584 else a:push(str.ptr + i,1) end 585 585 end 586 - a:lpush '"' 586 + if wrap then a:lpush '"' end 587 587 return a:finalize() 588 588 end 589 589 590 590 return m
Modified tpl.t from [ce74a1083d] to [2f4ebceee0].
34 34 str = str:gsub('%s+[\n$]','') 35 35 str = str:gsub('\n','') 36 36 str = str:gsub('</a><a ','</a> <a ') -- keep nav links from getting smooshed 37 37 str = str:gsub(tplchar .. '%?([-%w]+)', function(file) 38 38 if not docs[file] then docs[file] = data.doc[file] end 39 39 return string.format('<a href="#help-%s" class="help">?</a>', file) 40 40 end) 41 - for start, mode, key, stop in string.gmatch(str,'()'..tplchar..'([:!$]?)([-a-zA-Z0-9_]+)()') do 41 + for start, mode, key, stop in string.gmatch(str,'()'..tplchar..'([+:!$#%^]?)([-a-zA-Z0-9_]+)()') do 42 42 if string.sub(str,start-1,start-1) ~= '\\' then 43 43 segs[#segs+1] = string.sub(str,last,start-1) 44 44 fields[#segs] = { key = key:gsub('-','_'), mode = (mode ~= '' and mode or nil) } 45 45 last = stop 46 46 end 47 47 end 48 48 segs[#segs+1] = string.sub(str,last) ................................................................................ 65 65 local tallyup = {quote 66 66 var [runningtally] = 1 + constlen 67 67 end} 68 68 local rec = terralib.types.newstruct(string.format('template<%s>',tplspec.id or '')) 69 69 local symself = symbol(&rec) 70 70 do local kfac = {} 71 71 local sanmode = {} 72 + local types = { ['^'] = uint64, ['#'] = uint64 } 73 + local recmap = {} 72 74 for afterseg,fld in ipairs(fields) do 73 75 if not kfac[fld.key] then 74 76 rec.entries[#rec.entries + 1] = { 75 77 field = fld.key; 76 - type = lib.mem.ptr(int8); 78 + type = types[fld.mode] or pstr; 77 79 } 80 + recmap[fld.key] = rec.entries[#rec.entries] 78 81 end 79 82 kfac[fld.key] = (kfac[fld.key] or 0) + 1 80 83 sanmode[fld.key] = fld.mode == ':' and 6 81 84 or fld.mode == '!' and 5 82 - or fld.mode == '$' and 2 or 1 85 + or (fld.mode == '$' or fld.mode == '+') and 2 86 + or fld.mode == '^' and lib.math.shorthand.maxlen 87 + or fld.mode == '#' and 20 88 + or 1 83 89 end 84 90 for key, fac in pairs(kfac) do 85 91 local sanfac = sanmode[key] 86 - 87 - tallyup[#tallyup + 1] = quote 88 - [runningtally] = [runningtally] + ([symself].[key].ct)*fac*sanfac 92 + if recmap[key].type ~= pstr then 93 + tallyup[#tallyup + 1] = quote 94 + [runningtally] = [runningtally] + fac*sanfac 95 + end 96 + else 97 + tallyup[#tallyup + 1] = quote 98 + [runningtally] = [runningtally] + ([symself].[key].ct)*fac*sanfac 99 + end 89 100 end 90 101 end 91 102 end 92 103 93 104 local copiers = {} 94 105 local senders = {} 95 106 local appenders = {} ................................................................................ 102 113 copiers[#copiers+1] = quote [cpypos] = lib.mem.cpy([cpypos], [&opaque]([seg]), [#seg]) end 103 114 senders[#senders+1] = quote lib.net.mg_send([destcon], [seg], [#seg]) end 104 115 appenders[#appenders+1] = quote [accumulator]:push([seg], [#seg]) end 105 116 if fields[idx] and fields[idx].mode then 106 117 local f = fields[idx] 107 118 local fp = `symself.[f.key] 108 119 local sanexp 109 - if f.mode == '$' then 110 - sanexp = `lib.str.qesc(pool, fp) 111 - else 112 - sanexp = `lib.html.sanitize(pool, fp, [f.mode == ':']) 113 - end 120 + local nulexp 121 + if f.mode == '$' then sanexp = `lib.str.qesc(pool, fp, true) 122 + elseif f.mode == '+' then sanexp = `lib.str.qesc(pool, fp, false) 123 + elseif f.mode == '#' then 124 + sanexp = quote 125 + var ibuf: int8[21] 126 + var ptr = lib.math.decstr(fp, &ibuf[20]) 127 + in pstr {ptr=ptr, ct=&ibuf[20] - ptr} end 128 + elseif f.mode == '^' then 129 + 130 + sanexp = quote 131 + var shbuf: lib.math.shorthand.t 132 + var len = lib.math.shorthand.gen(fp, &shbuf[0]) 133 + in pstr {ptr=&shbuf[0],ct=len} end 134 + else sanexp = `lib.html.sanitize(pool, fp, [f.mode == ':']) end 135 + if f.mode == '^' or f.mode == '#' then nulexp = `true 136 + else nulexp = `fp.ct > 0 end 114 137 copiers[#copiers+1] = quote 115 - if fp.ct > 0 then 138 + if [nulexp] then 116 139 var san = sanexp 117 140 [cpypos] = lib.mem.cpy([cpypos], [&opaque](san.ptr), san.ct) 118 141 --san:free() 119 142 end 120 143 end 121 144 senders[#senders+1] = quote 122 - if fp.ct > 0 then 145 + if [nulexp] then 123 146 var san = sanexp 124 147 lib.net.mg_send([destcon], san.ptr, san.ct) 125 148 --san:free() 126 149 end 127 150 end 128 151 appenders[#appenders+1] = quote 129 - if fp.ct > 0 then 152 + if [nulexp] then 130 153 var san = sanexp 131 154 [accumulator]:ppush(san) 132 155 --san:free() 133 156 end 134 157 end 135 158 elseif fields[idx] then 136 159 local f = fields[idx]