Started work on refactor

Started work on moveing each endpoint into it's own file.
This commit is contained in:
Robin Malley 2020-12-20 08:16:23 +00:00
parent 86a14e9d62
commit 2cd10b6968
35 changed files with 1319 additions and 92 deletions

93
src/lua/cache.lua Normal file
View File

@ -0,0 +1,93 @@
local sql = require("lsqlite3")
local queries = require("queries")
local util = require("util")
local ret = {}
local stmnt_cache, stmnt_insert_cache
local oldconfigure = configure
function configure(...)
local cache = sqlassert(sql.open_memory())
--A cache table to store rendered pages that do not need to be
--rerendered. In theory this could OOM the program eventually and start
--swapping to disk. TODO: fixme
assert(cache:exec([[
CREATE TABLE IF NOT EXISTS cache (
path TEXT PRIMARY KEY,
data BLOB,
updated INTEGER,
dirty INTEGER
);
]]))
stmnt_cache = cache:prepare([[
SELECT data
FROM cache
WHERE
path = :path AND
((dirty = 0) OR (strftime('%s','now') - updated) < 20)
;
]])
stmnt_insert_cache = cache:prepare([[
INSERT OR REPLACE INTO cache (
path, data, updated, dirty
) VALUES (
:path, :data, strftime('%s','now'), 0
);
]])
stmnt_dirty_cache = cache:prepare([[
UPDATE OR IGNORE cache
SET dirty = 1
WHERE path = :path;
]])
return oldconfigure(...)
end
--Render a page, with cacheing. If you need to dirty a cache, call dirty_cache()
function ret.render(pagename,callback)
print("Running render...")
stmnt_cache:bind_names{path=pagename}
local err = util.do_sql(stmnt_cache)
if err == sql.DONE then
stmnt_cache:reset()
--page is not cached
elseif err == sql.ROW then
print("Cache hit:" .. pagename)
data = stmnt_cache:get_values()
stmnt_cache:reset()
return data[1]
else --sql.ERROR or sql.MISUSE
error("Failed to check cache for page " .. pagename)
end
--We didn't have the paged cached, render it
print("Cache miss, running function")
local text = callback()
print("Saving data...")
--And save the data back into the cache
stmnt_insert_cache:bind_names{
path=pagename,
data=text,
}
err = util.do_sql(stmnt_insert_cache)
if err == sql.ERROR or err == sql.MISUSE then
error("Failed to update cache for page " .. pagename)
end
stmnt_insert_cache:reset()
return text
end
function ret.dirty(url)
print("Dirtying cache:",url)
stmnt_dirty_cache:bind_names{
path = url
}
err = util.do_sql(stmnt_dirty_cache)
stmnt_dirty_cache:reset()
end
function ret.close()
end
return ret

16
src/lua/claim_get.lua Normal file
View File

@ -0,0 +1,16 @@
local cache = require("cache")
local config = require("config")
local pages = require("pages")
local function claim_get(req)
--Get the page to claim a name
local cachestr = string.format("%s/_claim",config.domain)
local text = cache.render(cachestr,function()
print("cache miss, rendering claim page")
return pages.claim{err=""}
end)
http_response(req,200,text)
end
return claim_get

66
src/lua/claim_post.lua Normal file
View File

@ -0,0 +1,66 @@
local sql = require("lsqlite3")
local pages = require("pages")
local db = require("db")
local queries = require("queries")
local stmnt_author_create
local oldconfigure = configure
function configure(...)
stmnt_author_create = assert(db.conn:prepare(queries.insert_author))
return oldconfigure(...)
end
local function claim_post(req)
--Actually claim a name
http_request_populate_post(req)
local name = assert(http_argument_get_string(req,"user"))
local text
--What in the world, Kore should be rejecting names that
--are not lower case & no symbols, but some still get through somehow.
if not name:match("^[a-z0-9]*$") then
print("Bad username:",name)
text = pages.claim{
err = "Usernames must match ^[a-z0-9]{1,30}$"
}
http_response(req,200,text)
return
end
local rngf = assert(io.open("/dev/urandom","rb"))
local passlength = string.byte(rngf:read(1)) + 64
local salt = rngf:read(64)
local password = rngf:read(passlength)
rngf:close()
local hash = sha3(salt .. password)
stmnt_author_create:bind_names{
name = name,
}
stmnt_author_create:bind_blob(2,salt)
stmnt_author_create:bind_blob(3,hash)
local err = do_sql(stmnt_author_create)
if err == sql.DONE then
--We sucessfully made athe new author
local id = stmnt_author_create:last_insert_rowid()
stmnt_author_create:reset()
--Give them a file back
http_response_header(req,"Content-Type","application/octet-stream")
http_response_header(req,"Content-Disposition","attachment; filename=\"" .. name .. "." .. domain .. ".passfile\"")
local session = start_session(id)
text = password
elseif err == sql.CONSTRAINT then
--If the creation failed, they probably just tried
--to use a name that was already taken
text = pages.claim {
err = "Failed to claim. That name may already be taken."
}
elseif err == sql.ERROR or err == sql.MISUSE then
--This is bad though
text = pages.claim {
err = "Failed to claim"
}
end
stmnt_author_create:reset()
end
return claim_post

5
src/lua/config.lua Normal file
View File

@ -0,0 +1,5 @@
return {
domain = "test.monster:8888",
production = false,
}

48
src/lua/db.lua Normal file
View File

@ -0,0 +1,48 @@
local sql = require("lsqlite3")
local queries = require("queries")
local db = {}
local oldconfigure = configure
function configure(...)
db.conn = sqlassert(sql.open("data/posts.db"))
--Create sql tables
assert(db.conn:exec(queries.create_table_authors))
--Create a fake "anonymous" user, so we don't run into trouble
--so that no one runs into trouble being able to paste under this account.
assert(db.conn:exec(queries.insert_anon_author))
--If/when an author delets their account, all posts
--and comments by that author are also deleted (on
--delete cascade) this is intentional. This also
--means that all comments by other users on a post
--an author makes will also be deleted.
--
--Post text uses zlib compression
assert(db.conn:exec(queries.create_table_posts))
--Store the raw text so people can download it later, maybe
--we can use it for "download as image" or "download as pdf"
--in the future too. Stil stored zlib compressed
assert(db.conn:exec(queries.create_table_raw_text))
--Maybe we want to store images one day?
assert(db.conn:exec(queries.create_table_images))
--Comments on a post
assert(db.conn:exec(queries.create_table_comments))
--Tags for a post
assert(db.conn:exec(queries.create_table_tags))
--Index for tags
assert(db.conn:exec(queries.create_index_tags))
--Store a cookie for logged in users. Logged in users can edit
--their own posts, and edit their biographies.
assert(db:exec(queries.create_table_session))
print("Created db tables")
return configure(...)
end
function db.close()
db.conn:close()
end
return db

64
src/lua/edit_get.lua Normal file
View File

@ -0,0 +1,64 @@
local zlib = require("zlib")
local sql = require("lsqlite3")
local db = require("db")
local queries = require("queries")
local util = require("util")
local pages = require("pages")
local tags = require("tags")
local stmnt_edit
local oldconfigure = configure
function configure(...)
stmnt_edit = assert(db.conn:prepare(queries.select_edit))
return configure(...)
end
local function edit_get(req)
local host = http_request_get_host(req)
local path = http_request_get_path(req)
local author, authorid = get_session(req)
http_request_populate_qs(req)
local story = assert(http_argument_get_string(req,"story"))
local story_id = decode_id(story)
local ret
print("we want to edit story:",story)
--Check that the logged in user is the owner of the story
--sql-side. If we're not the owner, we'll get 0 rows back.
stmnt_edit:bind_names{
postid = story_id,
authorid = author_id
}
local err = util.do_sql(stmnt_edit)
if err == sql.DONE then
--No rows, we're probably not the owner (it might
--also be because there's no such story)
ret = pages.cantedit{
path = story,
}
stmnt_edit:reset()
http_response(req,200,ret)
return
end
assert(err == sql.ROW)
local data = stmnt_edit:get_values()
local txt_compressed, markup, isanon, title = unpack(data)
local text = zlib.decompress(txt_compressed)
local tags = tags.get(story_id)
local tags_txt = table.concat(tags,";")
stmnt_edit:reset()
ret = pages.edit{
title = title,
text = text,
markup = markup,
user = author,
isanon = isanon == 1,
domain = domain,
story = story_id,
err = "",
tags = tags_txt
}
http_response(req,200,ret)
end

85
src/lua/edit_post.lua Normal file
View File

@ -0,0 +1,85 @@
local sql = require("lsqlite3")
local zlib = require("zlib")
local queries = require("queries")
local pages = require("pages")
local parsers = require("parsers")
local util = require("util")
local tagslib = require("tags")
local cache = require("cache")
local config = require("config")
local stmnt_author_of, stmnt_update_raw, stmnt_update
local oldconfigure = configure
function configure(...)
stmnt_author_of = assert(db.conn:prepare(queries.select_author_of_post))
stmnt_update_raw = assert(db.conn:prepare(queries.update_raw))
stmnt_update = assert(db.conn:prepare(queries.update_post))
return oldconfigure(...)
end
local function edit_post(req)
local host = http_request_get_host(req)
local path = http_request_get_path(req)
local author, author_id = get_session(req)
http_request_populate_post(req)
local storyid = tonumber(assert(http_argument_get_string(req,"story")))
local title = assert(http_argument_get_string(req,"title"))
local text = assert(http_argument_get_string(req,"text"))
local pasteas = assert(http_argument_get_string(req,"pasteas"))
local markup = assert(http_argument_get_string(req,"markup"))
local tags_str = http_argument_get_string(req,"tags")
stmnt_author_of:bind_names{
id = storyid
}
local err = do_sql(stmnt_author_of)
if err ~= sql.ROW then
stmnt_author_of:reset()
error("No author found for story:" .. storyid)
end
local data = stmnt_author_of:get_values()
stmnt_author_of:reset()
local realauthor = data[1]
assert(realauthor == author_id) --Make sure the author of the story is the currently logged in user
local parsed = parsers[markup](text)
local compr_raw = zlib.compress(text)
local compr = zlib.compress(parsed)
local tags = {}
if tags_str then
tags = util.parse_tags(tags_str)
end
assert(stmnt_update_raw:bind_blob(1,compr_raw) == sql.OK)
assert(stmnt_update_raw:bind(2,markup) == sql.OK)
assert(stmnt_update_raw:bind(3,storyid) == sql.OK)
assert(util.do_sql(stmnt_update_raw) == sql.DONE, "Failed to update raw")
stmnt_update_raw:reset()
assert(stmnt_update:bind(1,title) == sql.OK)
assert(stmnt_update:bind_blob(2,compr) == sql.OK)
assert(stmnt_update:bind(3,pasteas == "anonymous" and 1 or 0) == sql.OK)
assert(stmnt_update:bind(4,storyid) == sql.OK)
assert(do_sql(stmnt_update) == sql.DONE, "Failed to update text")
stmnt_update:reset()
tagslib.set(storyid,tags)
--[[
assert(stmnt_drop_tags:bind_names{postid = storyid} == sql.OK)
do_sql(stmnt_drop_tags)
stmnt_drop_tags:reset()
for _,tag in pairs(tags) do
print("Looking at tag",tag)
assert(stmnt_ins_tag:bind(1,storyid) == sql.OK)
assert(stmnt_ins_tag:bind(2,tag) == sql.OK)
err = do_sql(stmnt_ins_tag)
stmnt_ins_tag:reset()
end
]]
local id_enc = util.encode_id(storyid)
local loc = string.format("https://%s/%s",config.domain,id_enc)
cache.dirty(string.format("%s/%s",config.domain,id_enc)) -- This place to read this post
cache.dirty(string.format("%s",config.domain)) -- The site index (ex, if the author changed the paste from their's to "Anonymous", the cache should reflect that).
cache.dirty(string.format("%s.%s",author,config.domain)) -- The author's index, same reasoning as above.
http_response_header(req,"Location",loc)
http_response(req,303,"")
return
end

110
src/lua/index_get.lua Normal file
View File

@ -0,0 +1,110 @@
local sql = require("lsqlite3")
local cache = require("cache")
local queries = require("queries")
local db = require("db")
local util = require("util")
local config = require("config")
local pages = require("pages")
local stmnt_index, stmnt_author, stmnt_author_bio
local oldconfigure = configure
function configure(...)
stmnt_index = assert(db.conn:prepare(queries.select_site_index))
--TODO: actually let authors edit their bio
stmnt_author_bio = assert(db.conn:prepare([[
SELECT authors.biography FROM authors WHERE authors.name = :author;
]]))
stmnt_author = assert(db.conn:prepare(queries.select_author_index))
return configure(...)
end
local function get_site_home(req)
print("Cache miss, rendering index")
stmnt_index:bind_names{}
local err = util.do_sql(stmnt_index)
local latest = {}
--err may be sql.ROW or sql.DONE if we don't have any stories yet
while err == sql.ROW do
local data = stmnt_index:get_values()
local storytags = tags.get(data[1])
table.insert(latest,{
url = encode_id(data[1]),
title = data[2],
isanon = data[3] == 1,
posted = os.date("%B %d %Y",tonumber(data[4])),
author = data[5],
tags = storytags,
})
err = stmnt_index:step()
end
stmnt_index:reset()
return pages.index{
domain = config.domain,
stories = latest
}
end
local function get_author_home(req)
local host = http_request_get_host(req)
local subdomain = host:match("([^\\.]+)")
stmnt_author_bio:bind_names{author=subdomain}
local err = do_sql(stmnt_author_bio)
if err == sql.DONE then
print("No such author")
stmnt_author_bio:reset()
return pages.noauthor{
author = subdomain
}
end
print("err:",err)
assert(err == sql.ROW,"failed to get author:" .. subdomain .. " error:" .. tostring(err))
local data = stmnt_author_bio:get_values()
local bio = data[1]
stmnt_author_bio:reset()
print("Getting author's stories")
stmnt_author:bind_names{author=subdomain}
err = do_sql(stmnt_author)
print("err:",err)
local stories = {}
while err == sql.ROW do
local data = stmnt_author:get_values()
local id, title, time = unpack(data)
local tags = get_tags(id)
table.insert(stories,{
url = encode_id(id),
title = title,
posted = os.date("%B %d %Y",tonumber(time)),
tags = tags,
})
err = stmnt_author:step()
end
stmnt_author:reset()
return pages.author_index{
domain=config.domain,
author=subdomain,
stories=stories,
bio=bio
}
end
local function index_get(req)
local method = http_method_text(req)
local host = http_request_get_host(req)
local path = http_request_get_path(req)
--Default home page
local subdomain = host:match("([^\\.]+)")
local text
if host == config.domain then
local cachepath = string.format("%s",config.domain),
text = cache.render(cachepath, function()
return get_site_home(req)
end)
else --author home page
local cachepath = string.format("%s.%s",subdomain,config.domain)
text = cache.render(cachepath, function()
return get_author_home(req)
end)
end
end

View File

@ -1,19 +1,34 @@
print("Really fast print from init.lua") print("Really fast print from init.lua")
--Luarocks libraries
local et = require("etlua") local et = require("etlua")
local sql = require("lsqlite3") local sql = require("lsqlite3")
local zlib = require("zlib") local zlib = require("zlib")
if PRODUCTION then --smr code
local cache = require("cache")
local pages = require("pages")
local util = require("util")
local config = require("config")
local db = require("db")
if config.production then
local function print() end --squash prints local function print() end --squash prints
end end
--[[
local parser_names = {"plain","imageboard"} local parser_names = {"plain","imageboard"}
local parsers = {} local parsers = {}
for _,v in pairs(parser_names) do for _,v in pairs(parser_names) do
parsers[v] = require("parser_" .. v) parsers[v] = require("parser_" .. v)
end end
local db,cache --databases ]]
local domain = "test.monster:8888" --The domain to write links as --pages
read_get = require("read_get")
read_post = require("read_post")
preview_post = require("preview_post")
--local db,cache --databases
--local domain = "test.monster:8888" --The domain to write links as
--[[
local pagenames = { local pagenames = {
"index", "index",
"author_index", "author_index",
@ -36,7 +51,9 @@ for k,v in pairs(pagenames) do
pages[v] = assert(et.compile(f:read("*a"))) pages[v] = assert(et.compile(f:read("*a")))
f:close() f:close()
end end
]]
--[=[
local queries = {} local queries = {}
--These are all loaded during startup, won't affect ongoing performance. --These are all loaded during startup, won't affect ongoing performance.
setmetatable(queries,{ setmetatable(queries,{
@ -47,13 +64,13 @@ setmetatable(queries,{
return ret return ret
end end
}) })
]=]
---sql queries ---sql queries
local stmnt_index, stmnt_author_index, stmnt_read, stmnt_paste, stmnt_raw local --[[stmnt_index,]] stmnt_author_index, stmnt_read, stmnt_paste, stmnt_raw
local stmnt_update_views local stmnt_update_views
local stmnt_ins_tag, stmnt_drop_tags, stmnt_get_tags local stmnt_ins_tag, stmnt_drop_tags, stmnt_get_tags
local stmnt_author_create, stmnt_author_acct, stmnt_author_bio local stmnt_author_create, stmnt_author_acct, stmnt_author_bio
local stmnt_cache, stmnt_insert_cache, stmnt_dirty_cache --local stmnt_cache, stmnt_insert_cache, stmnt_dirty_cache
local stmnt_get_session, stmnt_insert_session local stmnt_get_session, stmnt_insert_session
local stmnt_edit, stmnt_update, stmnt_update_raw, stmnt_author_of local stmnt_edit, stmnt_update, stmnt_update_raw, stmnt_author_of
local stmnt_comments, stmnt_comment_insert local stmnt_comments, stmnt_comment_insert
@ -79,6 +96,7 @@ local function decodeentities(capture)
end end
end end
--[[
local function sqlassert(...) local function sqlassert(...)
local r,errcode,err = ... local r,errcode,err = ...
if not r then if not r then
@ -86,6 +104,7 @@ local function sqlassert(...)
end end
return r return r
end end
]]
local function sqlbind(stmnt,call,position,data) local function sqlbind(stmnt,call,position,data)
assert(call == "bind" or call == "bind_blob","Bad bind call, call was:" .. call) assert(call == "bind" or call == "bind_blob","Bad bind call, call was:" .. call)
@ -99,22 +118,22 @@ end
print("Hello from init.lua") print("Hello from init.lua")
function configure() function configure()
db = sqlassert(sql.open("data/posts.db")) --db = sqlassert(sql.open("data/posts.db"))
--db = sqlassert(sql.open_memory()) ----db = sqlassert(sql.open_memory())
cache = sqlassert(sql.open_memory()) --cache = sqlassert(sql.open_memory())
print("Compiled pages...")
--Test that compression works --Test that compression works. For some reason, the zlib library
--fails if this is done as a one-liner
local msg = "test message" local msg = "test message"
local one = zlib.compress(msg) local one = zlib.compress(msg)
local two = zlib.decompress(one) local two = zlib.decompress(one)
--For some reason, the zlib library fails if this is done as a oneliner
assert(two == msg, "zlib not working as expected") assert(two == msg, "zlib not working as expected")
--Create sql tables --Create sql tables
assert(db:exec(queries.create_table_authors)) --assert(db:exec(queries.create_table_authors))
--Create a fake "anonymous" user, so we don't run into trouble --Create a fake "anonymous" user, so we don't run into trouble
--so that no one runs into touble being able to paste under this account. --so that no one runs into touble being able to paste under this account.
assert(db:exec(queries.insert_anon_author)) --assert(db:exec(queries.insert_anon_author))
--If/when an author delets their account, all posts --If/when an author delets their account, all posts
--and comments by that author are also deleted (on --and comments by that author are also deleted (on
--delete cascade) this is intentional. This also --delete cascade) this is intentional. This also
@ -122,23 +141,24 @@ function configure()
--an author makes will also be deleted. --an author makes will also be deleted.
-- --
--Post text uses zlib compression --Post text uses zlib compression
assert(db:exec(queries.create_table_posts)) --assert(db:exec(queries.create_table_posts))
--Store the raw text so people can download it later, maybe --Store the raw text so people can download it later, maybe
--we can use it for "download as image" or "download as pdf" --we can use it for "download as image" or "download as pdf"
--in the future too. Stil stored zlib compressed --in the future too. Stil stored zlib compressed
assert(db:exec(queries.create_table_raw_text)) --assert(db:exec(queries.create_table_raw_text))
assert(db:exec(queries.create_table_images)) --assert(db:exec(queries.create_table_images))
assert(db:exec(queries.create_table_comments)) --assert(db:exec(queries.create_table_comments))
assert(db:exec(queries.create_table_tags)) --assert(db:exec(queries.create_table_tags))
assert(db:exec(queries.create_index_tags)) --assert(db:exec(queries.create_index_tags))
--Store a cookie for logged in users. Logged in users can edit --Store a cookie for logged in users. Logged in users can edit
--their own posts. --their own posts.
assert(db:exec(queries.create_table_session)) --assert(db:exec(queries.create_table_session))
print("Created db tables") --print("Created db tables")
--A cache table to store rendered pages that do not need to be --A cache table to store rendered pages that do not need to be
--rerendered. In theory this could OOM the program eventually and start --rerendered. In theory this could OOM the program eventually and start
--swapping to disk. TODO: fixme --swapping to disk. TODO: fixme
--[=[
assert(cache:exec([[ assert(cache:exec([[
CREATE TABLE IF NOT EXISTS cache ( CREATE TABLE IF NOT EXISTS cache (
path TEXT PRIMARY KEY, path TEXT PRIMARY KEY,
@ -147,62 +167,71 @@ function configure()
dirty INTEGER dirty INTEGER
); );
]])) ]]))
]=]
--Select the data we need to display the on the front page --Select the data we need to display the on the front page
stmnt_index = assert(db:prepare(queries.select_site_index)) --stmnt_index = assert(db:prepare(queries.select_site_index))
--Select the data we need to read a story (and maybe display an edit --Select the data we need to read a story (and maybe display an edit
--button --button
stmnt_read = assert(db:prepare(queries.select_post)) --stmnt_read = assert(db:prepare(queries.select_post))
--Update the view counter when someone reads a story --Update the view counter when someone reads a story
stmnt_update_views = assert(db:prepare(queries.update_views)) --stmnt_update_views = assert(db:prepare(queries.update_views))
--Retreive comments on a story --Retreive comments on a story
stmnt_comments = assert(db:prepare(queries.select_comments)) --stmnt_comments = assert(db:prepare(queries.select_comments))
--Add a new comment to a story --Add a new comment to a story
stmnt_comment_insert = assert(db:prepare(queries.insert_comment)) --stmnt_comment_insert = assert(db:prepare(queries.insert_comment))
--TODO: actually let authors edit their bio --TODO: actually let authors edit their bio
stmnt_author_bio = assert(db:prepare([[ --[=[stmnt_author_bio = assert(db.conn:prepare([[
SELECT authors.biography FROM authors WHERE authors.name = :author; SELECT authors.biography FROM authors WHERE authors.name = :author;
]])) ]]))
]=]
--Get the author of a story, used to check when editing that the --Get the author of a story, used to check when editing that the
--author really owns the story they're trying to edit --author really owns the story they're trying to edit
stmnt_author_of = assert(db:prepare(queries.select_author_of_post)) --stmnt_author_of = assert(db:prepare(queries.select_author_of_post))
--Get the data we need to display a particular author's latest --Get the data we need to display a particular author's latest
--stories --stories
stmnt_author = assert(db:prepare(queries.select_author_index)) --stmnt_author = assert(db:prepare(queries.select_author_index))
--Get the data we need to check if someone can log in --Get the data we need to check if someone can log in
--[=[
stmnt_author_acct = assert(db:prepare([[ stmnt_author_acct = assert(db:prepare([[
SELECT id, salt, passhash FROM authors WHERE name = :name; SELECT id, salt, passhash FROM authors WHERE name = :name;
]])) ]]))
]=]
--Create a new author on the site --Create a new author on the site
stmnt_author_create = assert(db:prepare(queries.insert_author)) --stmnt_author_create = assert(db:prepare(queries.insert_author))
--[=[
stmnt_author_login = assert(db:prepare([[ stmnt_author_login = assert(db:prepare([[
SELECT name, passhash FROM authors WHERE name = :name; SELECT name, passhash FROM authors WHERE name = :name;
]])) ]]))
]=]
--Create a new post --Create a new post
stmnt_paste = assert(db:prepare(queries.insert_post)) stmnt_paste = assert(db:prepare(queries.insert_post))
--Keep a copy of the plain text of a post so we can edit it later --Keep a copy of the plain text of a post so we can edit it later
--It might also be useful for migrations, if that ever needs to happen --It might also be useful for migrations, if that ever needs to happen
stmnt_raw = assert(db:prepare(queries.insert_raw)) stmnt_raw = assert(db:prepare(queries.insert_raw))
--Tags for a story --Tags for a story
--[[
stmnt_ins_tag = assert(db:prepare(queries.insert_tag)) stmnt_ins_tag = assert(db:prepare(queries.insert_tag))
stmnt_get_tags = assert(db:prepare(queries.select_tags)) stmnt_get_tags = assert(db:prepare(queries.select_tags))
stmnt_drop_tags = assert(db:prepare(queries.delete_tags)) stmnt_drop_tags = assert(db:prepare(queries.delete_tags))
]]
--Get the data we need to display the edit screen --Get the data we need to display the edit screen
stmnt_edit = assert(db:prepare(queries.select_edit)) stmnt_edit = assert(db:prepare(queries.select_edit))
--Get the data we need when someone wants to download a paste --Get the data we need when someone wants to download a paste
stmnt_download = assert(db:prepare(queries.select_download)) stmnt_download = assert(db:prepare(queries.select_download))
--When we update a post, store the plaintext again --When we update a post, store the plaintext again
stmnt_update_raw = assert(db:prepare(queries.update_raw)) --stmnt_update_raw = assert(db:prepare(queries.update_raw))
--Should we really reset the update time every time someone makes a post? --Should we really reset the update time every time someone makes a post?
--Someone could keep their story on the front page by just editing it a lot. --Someone could keep their story on the front page by just editing it a lot.
--If it gets abused I can disable it I guess. --If it gets abused I can disable it I guess.
stmnt_update = assert(db:prepare(queries.update_post)) --stmnt_update = assert(db:prepare(queries.update_post))
--Check sessions for login support --Check sessions for login support
stmnt_insert_session = assert(db:prepare(queries.insert_session)) stmnt_insert_session = assert(db:prepare(queries.insert_session))
stmnt_get_session = assert(db:prepare(queries.select_valid_sessions)) stmnt_get_session = assert(db:prepare(queries.select_valid_sessions))
--Search by tag name --Search by tag name
stmnt_search = assert(db:prepare(queries.select_post_tags)) stmnt_search = assert(db:prepare(queries.select_post_tags))
--only refresh pages at most once every 10 seconds --only refresh pages at most once every 10 seconds
--[=[
stmnt_cache = cache:prepare([[ stmnt_cache = cache:prepare([[
SELECT data SELECT data
FROM cache FROM cache
@ -223,6 +252,7 @@ function configure()
SET dirty = 1 SET dirty = 1
WHERE path = :path; WHERE path = :path;
]]) ]])
]=]
--[=[ --[=[
]=] ]=]
print("finished running configure()") print("finished running configure()")
@ -296,6 +326,7 @@ local function get_tags(id)
until false until false
end end
--[=[
local function dirty_cache(url) local function dirty_cache(url)
print("Dirtying cache:",url) print("Dirtying cache:",url)
stmnt_dirty_cache:bind_names{ stmnt_dirty_cache:bind_names{
@ -304,6 +335,7 @@ local function dirty_cache(url)
err = do_sql(stmnt_dirty_cache) err = do_sql(stmnt_dirty_cache)
stmnt_dirty_cache:reset() stmnt_dirty_cache:reset()
end end
]=]
--[[ --[[
@ -356,6 +388,7 @@ local function get_session(req)
return author,authorid return author,authorid
end end
--[=[
--Render a page, with cacheing. If you need to dirty a cache, call dirty_cache() --Render a page, with cacheing. If you need to dirty a cache, call dirty_cache()
local function render(pagename,callback) local function render(pagename,callback)
print("Running render...") print("Running render...")
@ -388,7 +421,9 @@ local function render(pagename,callback)
stmnt_insert_cache:reset() stmnt_insert_cache:reset()
return text return text
end end
]=]
--[=[
--[[Parses a semicolon seperated string into it's parts, trims whitespace, lowercases, and capitalizes the first letter. Tags will not be empty. Returns an array of tags]] --[[Parses a semicolon seperated string into it's parts, trims whitespace, lowercases, and capitalizes the first letter. Tags will not be empty. Returns an array of tags]]
local function parse_tags(str) local function parse_tags(str)
local tags = {} local tags = {}
@ -404,6 +439,7 @@ local function parse_tags(str)
end end
return tags return tags
end end
]=]
function home(req) function home(req)
print("Hello from lua!") print("Hello from lua!")
@ -414,7 +450,7 @@ function home(req)
local text local text
if host == domain then if host == domain then
--Default home page --Default home page
text = render(string.format("%s",domain),function() text = cache.render(string.format("%s",domain),function()
print("Cache miss, rendering index") print("Cache miss, rendering index")
stmnt_index:bind_names{} stmnt_index:bind_names{}
local err = do_sql(stmnt_index) local err = do_sql(stmnt_index)
@ -442,7 +478,7 @@ function home(req)
else else
--Home page for an author --Home page for an author
local subdomain = host:match("([^\\.]+)") local subdomain = host:match("([^\\.]+)")
text = render(string.format("%s.%s",subdomain,domain),function() text = cache.render(string.format("%s.%s",subdomain,domain),function()
print("Cache miss, rendering author:" .. subdomain) print("Cache miss, rendering author:" .. subdomain)
stmnt_author_bio:bind_names{author=subdomain} stmnt_author_bio:bind_names{author=subdomain}
local err = do_sql(stmnt_author_bio) local err = do_sql(stmnt_author_bio)
@ -505,7 +541,7 @@ function claim(req)
local text local text
if method == "GET" then if method == "GET" then
--Get the page to claim a name --Get the page to claim a name
text = render(string.format("%s/_claim",domain),function() text = cache.render(string.format("%s/_claim",domain),function()
print("cache miss, rendering claim page") print("cache miss, rendering claim page")
return pages.claim{err=""} return pages.claim{err=""}
end) end)
@ -578,7 +614,7 @@ function paste(req)
return return
else else
--For an anonymous user --For an anonymous user
ret = render(string.format("%s/_paste",host),function() ret = cache.render(string.format("%s/_paste",host),function()
print("Cache missing, rendering post page") print("Cache missing, rendering post page")
return pages.paste{ return pages.paste{
domain = domain, domain = domain,
@ -686,8 +722,8 @@ function paste(req)
local loc = string.format("https://%s/%s",domain,url) local loc = string.format("https://%s/%s",domain,url)
http_response_header(req,"Location",loc) http_response_header(req,"Location",loc)
http_response(req,303,"") http_response(req,303,"")
dirty_cache(string.format("%s/%s",domain,url)) cache.dirty(string.format("%s/%s",domain,url))
dirty_cache(string.format("%s",domain)) cache.dirty(string.format("%s",domain))
return return
elseif err == sql.ERROR or err == sql.MISUSE then elseif err == sql.ERROR or err == sql.MISUSE then
ret = "Failed to paste: " .. tostring(err) ret = "Failed to paste: " .. tostring(err)
@ -748,9 +784,9 @@ function paste(req)
end end
http_response_header(req,"Location",loc) http_response_header(req,"Location",loc)
http_response(req,303,"") http_response(req,303,"")
dirty_cache(string.format("%s.%s",author,domain)) cache.dirty(string.format("%s.%s",author,domain))
dirty_cache(string.format("%s/%s",domain,url)) cache.dirty(string.format("%s/%s",domain,url))
dirty_cache(string.format("%s",domain)) cache.dirty(string.format("%s",domain))
return return
elseif err == sql.ERROR or err == sql.MISUSE then elseif err == sql.ERROR or err == sql.MISUSE then
ret = "Failed to paste: " .. tostring(err) ret = "Failed to paste: " .. tostring(err)
@ -778,7 +814,7 @@ local function read_story(host,path,idp,show_comments,iam)
} }
print("update:",do_sql(stmnt_update_views)) print("update:",do_sql(stmnt_update_views))
stmnt_update_views:reset() stmnt_update_views:reset()
dirty_cache(cachestr) cache.dirty(cachestr)
print("cachestr was:",cachestr) print("cachestr was:",cachestr)
local readstoryf = function() local readstoryf = function()
stmnt_read:bind_names{ stmnt_read:bind_names{
@ -830,7 +866,7 @@ local function read_story(host,path,idp,show_comments,iam)
--which is not the user's, from whoever loaded the cache last) to fix this bug, don't cache --which is not the user's, from whoever loaded the cache last) to fix this bug, don't cache
--pages when the user is logged in. All non-logged-in users can see the same page no problem. --pages when the user is logged in. All non-logged-in users can see the same page no problem.
if not iam then if not iam then
return render(cachestr,readstoryf) return cache.render(cachestr,readstoryf)
else else
return readstoryf() return readstoryf()
end end
@ -841,6 +877,8 @@ function read(req)
local path = http_request_get_path(req) local path = http_request_get_path(req)
local method = http_method_text(req) local method = http_method_text(req)
if method == "GET" then if method == "GET" then
read_get(req)
--[=[
local idp = string.sub(path,2)--remove leading "/" local idp = string.sub(path,2)--remove leading "/"
assert(string.len(path) > 0,"Tried to read 0-length story id") assert(string.len(path) > 0,"Tried to read 0-length story id")
local author, authorid = get_session(req) local author, authorid = get_session(req)
@ -902,7 +940,10 @@ function read(req)
assert(text) assert(text)
http_response(req,200,text) http_response(req,200,text)
return return
]=]
elseif method == "POST" then elseif method == "POST" then
read_post(req)
--[=[
--We're posting a comment --We're posting a comment
http_request_populate_post(req) http_request_populate_post(req)
http_populate_cookies(req) http_populate_cookies(req)
@ -928,11 +969,12 @@ function read(req)
http_response(req,500,"Internal error, failed to post comment. Go back and try again.") http_response(req,500,"Internal error, failed to post comment. Go back and try again.")
else else
--When we post a comment, we need to dirty the cache for the "comments displayed" page. --When we post a comment, we need to dirty the cache for the "comments displayed" page.
dirty_cache(string.format("%s%s?comments=1",host,path)) cache.dirty(string.format("%s%s?comments=1",host,path))
local redir = string.format("https://%s%s?comments=1", domain, path) local redir = string.format("https://%s%s?comments=1", domain, path)
http_response_header(req,"Location",redir) http_response_header(req,"Location",redir)
http_response(req,303,"") http_response(req,303,"")
end end
]=]
end end
end end
@ -949,7 +991,7 @@ function login(req)
local text local text
if method == "GET" then if method == "GET" then
--Just give them the login page --Just give them the login page
text = render(string.format("%s/_login",domain),function() text = cache.render(string.format("%s/_login",domain),function()
return pages.login{ return pages.login{
err = "", err = "",
} }
@ -1093,9 +1135,9 @@ function edit(req)
end end
local id_enc = encode_id(storyid) local id_enc = encode_id(storyid)
local loc = string.format("https://%s/%s",domain,id_enc) local loc = string.format("https://%s/%s",domain,id_enc)
dirty_cache(string.format("%s/%s",domain,id_enc)) -- This place to read this post cache.dirty(string.format("%s/%s",domain,id_enc)) -- This place to read this post
dirty_cache(string.format("%s",domain)) -- The site index (ex, if the author changed the paste from their's to "Anonymous", the cache should reflect that). cache.dirty(string.format("%s",domain)) -- The site index (ex, if the author changed the paste from their's to "Anonymous", the cache should reflect that).
dirty_cache(string.format("%s.%s",author,domain)) -- The author's index, same reasoning as above. cache.dirty(string.format("%s.%s",author,domain)) -- The author's index, same reasoning as above.
http_response_header(req,"Location",loc) http_response_header(req,"Location",loc)
http_response(req,303,"") http_response(req,303,"")
return return
@ -1112,10 +1154,10 @@ end
function teardown() function teardown()
print("Exiting...") print("Exiting...")
if db then if db then
db:close() db.close()
end end
if cache then if cache then
cache:close() cache.close()
end end
print("Finished lua teardown") print("Finished lua teardown")
end end
@ -1127,6 +1169,7 @@ function download(req)
http_request_populate_qs(req) http_request_populate_qs(req)
local story = assert(http_argument_get_string(req,"story")) local story = assert(http_argument_get_string(req,"story"))
local story_id = decode_id(story) local story_id = decode_id(story)
print("Downloading", story_id)
stmnt_download:bind_names{ stmnt_download:bind_names{
postid = story_id postid = story_id
} }
@ -1137,6 +1180,7 @@ function download(req)
stmnt_download:reset() stmnt_download:reset()
return return
end end
assert(err == sql.ROW, "after doing download sql, result was not a row, was:" .. tostring(err))
local txt_compressed, title = unpack(stmnt_download:get_values()) local txt_compressed, title = unpack(stmnt_download:get_values())
local text = zlib.decompress(txt_compressed) local text = zlib.decompress(txt_compressed)
stmnt_download:reset() stmnt_download:reset()
@ -1147,6 +1191,8 @@ function download(req)
end end
function preview(req) function preview(req)
preview_post(req)
--[[
print("We want to preview a paste!") print("We want to preview a paste!")
local host = http_request_get_host(req) local host = http_request_get_host(req)
local path = http_request_get_path(req) local path = http_request_get_path(req)
@ -1170,6 +1216,7 @@ function preview(req)
tags = tags, tags = tags,
} }
http_response(req,200,ret) http_response(req,200,ret)
]]
end end
function search(req) function search(req)

11
src/lua/login_get.lua Normal file
View File

@ -0,0 +1,11 @@
local config = require("config")
local function login_get(req)
--Just give them the login page
return cache.render(string.format("%s/_login",domain),function()
return pages.login{
err = "",
}
end)
end

58
src/lua/login_post.lua Normal file
View File

@ -0,0 +1,58 @@
local sql = require("lsqlite3")
local db = require("db")
local util = require("util")
local stmnt_author_acct
local oldconfigure = configure
function configure(...)
--Get the data we need to check if someone can log in
stmnt_author_acct = assert(db.conn:prepare([[
SELECT id, salt, passhash FROM authors WHERE name = :name;
]]))
return configure(...)
end
local function login_post(req)
--Try to log in
http_populate_multipart_form(req)
local name = assert(http_argument_get_string(req,"user"))
local pass = assert(http_file_get(req,"pass"))
stmnt_author_acct:bind_names{
name = name
}
local text
local err = util.do_sql(stmnt_author_acct)
if err == sql.ROW then
local id, salt, passhash = unpack(stmnt_author_acct:get_values())
stmnt_author_acct:reset()
local todigest = salt .. pass
local hash = sha3(todigest)
if hash == passhash then
local session = start_session(id)
http_response_cookie(req,"session",session,"/",0,0)
local loc = string.format("https://%s.%s",name,domain)
http_response_header(req,"Location",loc)
http_response(req,303,"")
return
else
text = pages.login{
err = "Incorrect username or password"
}
end
elseif err == sql.DONE then --Allows user enumeration, do we want this?
--Probably not a problem since all passwords are forced to be "good"
stmnt_author_acct:reset()
text = pages.login{
err = "Failed to find user:" .. name
}
else
stmnt_author_acct:reset()
error("Other sql error during login")
end
http_response(req,200,text)
end
return login_post

25
src/lua/pages.lua Normal file
View File

@ -0,0 +1,25 @@
local et = require("etlua")
local pagenames = {
"index",
"author_index",
"claim",
"paste",
"edit",
"read",
"nostory",
"cantedit",
"noauthor",
"login",
"author_paste",
"author_edit",
"search",
}
local pages = {}
for k,v in pairs(pagenames) do
print("Compiling page:",v)
local f = assert(io.open("pages/" .. v .. ".etlua","r"))
pages[v] = assert(et.compile(f:read("*a")))
f:close()
end
return pages

7
src/lua/parsers.lua Normal file
View File

@ -0,0 +1,7 @@
local parser_names = {"plain","imageboard"}
local parsers = {}
for _,v in pairs(parser_names) do
parsers[v] = require("parser_" .. v)
end
return parsers

81
src/lua/paste_get.lua Normal file
View File

@ -0,0 +1,81 @@
local config = require("config")
local function paste_get(req)
--Get the paste page
local host = http_request_get_host(req)
local text
local author,_ = get_session(req)
if host == config.domain and author then
http_response_header(req,"Location",string.format("https://%s.%s/_paste",author,config.domain))
http_response(req,303,"")
return
elseif host == config.domain and author == nil then
text = cache.render(string.format("%s/_paste",host),function()
print("Cache missing, rendering post page")
return pages.paste{
domain = domain,
err = "",
}
end)
http_response(req,200,text)
elseif host ~= config.domain and author then
elseif host ~= config.domain and author == nil then
else
error(string.format(
"Unable to find a good case for paste:%s,%s,%s",
host,
config.domain,
author
))
end
if host == config.domain then
local author,_ = get_session(req)
if author then
http_response_header(req,"Location",string.format("https://%s.%s/_paste",author,domain))
http_response(req,303,"")
return
else
--For an anonymous user
ret = cache.render(string.format("%s/_paste",host),function()
print("Cache missing, rendering post page")
return pages.paste{
domain = domain,
err = "",
}
end)
end
else
--Or for someone that's logged in
print("Looks like a logged in user wants to paste!")
local subdomain = host:match("([^%.]+)")
local author,_ = get_session(req)
print("subdomain:",subdomain,"author:",author)
--If they try to paste as an author, but are on the
--wrong subdomain, or or not logged in, redirect them
--to the right place. Their own subdomain for authors
--or the anonymous paste page for not logged in users.
if author == nil then
http_response_header(req,"Location","https://"..domain.."/_paste")
http_response(req,303,"")
return
end
if author ~= subdomain then
http_response_header(req,"Location",string.format("https://%s.%s/_paste",author,domain))
http_response(req,303,"")
return
end
assert(author == subdomain,"someone wants to paste as someone else")
--We're where we want to be, serve up this users's
--paste page. No cache, because how often is a user
--going to paste?
ret = pages.author_paste{
domain = domain,
user = author,
text = "",
err = "",
}
end
end

32
src/lua/preview_post.lua Normal file
View File

@ -0,0 +1,32 @@
local parsers = require("parsers")
local tags = require("tags")
local util = require("util")
local pages = require("pages")
local function preview_post(req)
print("We want to preview a paste!")
local host = http_request_get_host(req)
local path = http_request_get_path(req)
http_request_populate_post(req)
local title = assert(http_argument_get_string(req,"title"))
local text = assert(http_argument_get_string(req,"text"))
local markup = assert(http_argument_get_string(req,"markup"))
local tag_str = http_argument_get_string(req,"tags")
local tags = {}
if tag_str then
tags = util.parse_tags(tag_str)
end
print("title:",title,"text:",text,"markup:",markup)
local parsed = parsers[markup](text)
local ret = pages.read{
domain = domain,
title = title,
author = "preview",
idp = "preview",
text = parsed,
tags = tags,
}
http_response(req,200,ret)
end
return preview_post

13
src/lua/queries.lua Normal file
View File

@ -0,0 +1,13 @@
local queries = {}
setmetatable(queries,{
__index = function(self,key)
local f = assert(io.open("sql/" .. key .. ".sql","r"))
local ret = f:read("*a")
f:close()
return ret
end
})
return queries

140
src/lua/read_get.lua Normal file
View File

@ -0,0 +1,140 @@
local sql = require("sqlite3")
local session = require("session")
local tags = require("tags")
local db = require("db")
local queries = require("queries")
local stmnt_read, stmnt_update_views, stmnt_comments
local oldconfigure = configure
function configure(...)
stmnt_read = assert(db.conn:prepare(queries.select_post))
stmnt_update_views = assert(db.conn:prepare(queries.update_views))
stmnt_comments = assert(db.conn:prepare(queries.select_comments))
return configure(...)
end
--[[
Increases a story's hit counter by 1
]]
local function add_view(storyid)
stmnt_update_views:bind_names{
id = storyid
}
local err = do_sql(stmnt_update_views)
assert(err == sql.DONE, "Failed to update view counter:"..tostring(err))
stmnt_update_views:reset()
end
--[[
Populates ps with story settings, returns true if story was found,
or nil if it wasn't
]]
local function populate_ps_story(req,ps)
--Make sure our story exists
stmnt_read:bind_names{
id = ps.storyid
}
local err = do_sql(stmnt_read)
if err == sql.DONE then
--We got no story
stmnt_read:reset()
return nil
end
--If we've made it here, we have a story. Populate our settings
--with title, text, ect.
assert(err == sql.ROW)
add_view(storyid)
local title, storytext, tauthor, isanon, authorname, views = unpack(
stmnt_read:get_values()
)
ps.title = title
ps.storytext = zlib.decompress(storytext)
ps.tauthor = tauthor
ps.isanon = isanon == 1
ps.authorname = authorname
ps.views = views
stmnt_read:reset()
--Tags
ps.tags = tags.get(id)
return true
end
--[[
Get the comments for a story
]]
local function get_comments(req,ps)
end
--[[
The author is viewing their own story, give them an edit button
]]
local function read_get_author(req,storyid,author,authorid,comments)
end
--[[
An author is viewing a story, allow them to post comments as themselves
]]
local function read_get_loggedin(req,ps)
if ps.tauthor == ps.authorid then
--The story exists and we're logged in as the
--owner, display the edit button
return read_get_author(req,ps)
end
return pages.read(ps)
end
local function read_get(req)
--Pages settings
local ps = {
host = http_request_get_host(req),
path = http_request_get_path(req),
method = http_method_text(req),
}
--Get our story id
assert(string.len(ps.path) > 0,"Tried to read 0-length story id")
local idp = string.sub(ps.path,2)--remove leading "/"
ps.storyid = util.decode_id(idp)
--If we're logged in, set author and authorid
local author, authorid = session.get(req)
if author and authorid then
ps.author = author
ps.authorid = authorid
end
--If we need to show comments
http_request_populate_qs(req)
ps.show_comments = http_argument_get_string(req,"comments")
if ps.show_comments then
ps.comments = get_comments(req,ps)
end
--normal story display
if (not ps.author) then
local cachestr = string.format("%s%s%s",
ps.host,
ps.path,
ps.show_comments and "?comments=1" or ""
)
local text = cache.render(cachestr,function()
populate_ps_story(req,ps)
return pages.read(ps)
end)
else --we are logged in, don't cache
populate_ps_story(req,ps)
ps.owner = (ps.author == ps.tauthor)
text = pages.read(ps)
end
assert(text)
http_response(req,200,text)
return
end
return read_get

53
src/lua/read_post.lua Normal file
View File

@ -0,0 +1,53 @@
local sql = require("sqlite3")
local cache = require("cache")
local session = require("session")
local util = require("util")
local db = require("db")
local queries = require("queries")
local config = require("config")
local stmnt_comment_insert
local oldconfigure = configure
function configure(...)
stmnt_comment_insert = assert(db.conn:prepare(queries.insert_comment))
return oldconfigure(...)
end
local function read_post(req)
local host = http_request_get_host(req)
local path = http_request_get_path(req)
--We're posting a comment
http_request_populate_post(req)
http_populate_cookies(req)
local author, authorid = session.get(req)
local comment_text = assert(http_argument_get_string(req,"text"))
local pasteas = assert(http_argument_get_string(req,"postas"))
local idp = string.sub(path,2)--remove leading "/"
local id = util.decode_id(idp)
local isanon = 1
--Even if an author is logged in, they may post their comment anonymously
if author and pasteas ~= "Anonymous" then
isanon = 0
end
stmnt_comment_insert:bind_names{
postid=id,
authorid = author and authorid or -1,
isanon = isanon,
comment_text = comment_text,
}
local err = util.do_sql(stmnt_comment_insert)
stmnt_comment_insert:reset()
if err ~= sql.DONE then
http_response(req,500,"Internal error, failed to post comment. Go back and try again.")
else
--When we post a comment, we need to dirty the cache for the "comments displayed" page.
cache.dirty(string.format("%s%s?comments=1",host,path))
local redir = string.format("https://%s%s?comments=1", domain, path)
http_response_header(req,"Location",redir)
http_response(req,303,"")
end
end
return read_post

2
src/lua/render.lua Normal file
View File

@ -0,0 +1,2 @@

67
src/lua/session.lua Normal file
View File

@ -0,0 +1,67 @@
local sql = require("lsqlite3")
local db = require("db")
local util = require("util")
local oldconfigure = configure
local stmnt_get_session, stmnt_insert_session
function configure(...)
stmnt_get_session = assert(db:prepare(queries.select_valid_sessions))
return oldconfigure(...)
end
local session = {}
--[[
Retreive the name and authorid of the logged in person,
or nil+error message if not logged in
]]
function session.get(req)
http_populate_cookies(req)
local sessionid = http_request_cookie(req,"session")
if sessionid == nil then
return nil, "No session cookie passed by client"
end
stmnt_get_session:bind_names{
key = sessionid
}
local err = util.do_sql(stmnt_get_session)
if err ~= sql.ROW then
return nil, "No such session by logged in users"
end
print("get session err:",err)
local data = stmnt_get_session:get_values()
stmnt_get_session:reset()
local author = data[1]
local authorid = data[2]
return author,authorid
end
--[[
Start a session for someone who logged in
]]
local function start_session(who)
local rngf = assert(io.open("/dev/urandom","rb"))
local session_t = {}
for i = 1,64 do
local r = string.byte(rngf:read(1))
local s = string.char((r % 26) + 65)
table.insert(session_t,s)
end
local session = table.concat(session_t)
rngf:close()
print("sessionid:",session)
print("authorid:",who)
stmnt_insert_session:bind_names{
sessionid = session,
authorid = who
}
local err = util.do_sql(stmnt_insert_session)
stmnt_insert_session:reset()
print("Err:",err)
assert(err == sql.DONE)
return session
end
return session

54
src/lua/tags.lua Normal file
View File

@ -0,0 +1,54 @@
local db = require("db")
local queries = require("queries")
local util = require("util")
local tags = {}
local stmnt_get_tags, stmnt_ins_tag, stmnt_drop_tags
local oldconfigure = configure
function configure(...)
--Tags for a story
stmnt_ins_tag = assert(db.conn:prepare(queries.insert_tag))
stmnt_get_tags = assert(db.conn:prepare(queries.select_tags))
stmnt_drop_tags = assert(db.conn:prepare(queries.delete_tags))
return configure(...)
end
function tags.get(id)
local ret = {}
stmnt_get_tags:bind_names{
id = id
}
local err
repeat
err = stmnt_get_tags:step()
if err == sql.BUSY then
coroutine.yield()
elseif err == sql.ROW then
table.insert(ret,stmnt_get_tags:get_value(0))
elseif err == sql.DONE then
stmnt_get_tags:reset()
return ret
else
error(string.format("Failed to get tags for story %d : %d", id, err))
end
until false
end
function tags.set(storyid,tags)
assert(stmnt_drop_tags:bind_names{postid = storyid} == sql.OK)
util.do_sql(stmnt_drop_tags)
stmnt_drop_tags:reset()
for _,tag in pairs(tags) do
print("Looking at tag",tag)
assert(stmnt_ins_tag:bind(1,storyid) == sql.OK)
assert(stmnt_ins_tag:bind(2,tag) == sql.OK)
err = util.do_sql(stmnt_ins_tag)
stmnt_ins_tag:reset()
end
end
return tags

103
src/lua/util.lua Normal file
View File

@ -0,0 +1,103 @@
local util = {}
--[[
Runs an sql query and receives the 3 arguments back, prints a nice error
message on fail, and returns true on success.
]]
function util.sqlassert(...)
local r,errcode,err = ...
if not r then
error(string.format("%d: %s",errcode, err))
end
return r
end
--[[
Continuously tries to perform an sql statement until it goes through
]]
function util.do_sql(stmnt)
if not stmnt then error("No statement",2) end
local err
repeat
err = stmnt:step()
print("After stepping, err is", err)
if err == sql.BUSY then
coroutine.yield()
end
until(err ~= sql.BUSY)
return err
end
--see https://perishablepress.com/stop-using-unsafe-characters-in-urls/
--no underscore because we use that for our operative pages
local url_characters =
[[abcdefghijklmnopqrstuvwxyz]]..
[[ABCDEFGHIJKLMNOPQRSTUVWXYZ]]..
[[0123456789]]..
[[$-+!*'(),]]
local url_characters_rev = {}
for i = 1,string.len(url_characters) do
url_characters_rev[string.sub(url_characters,i,i)] = i
end
--[[
Encode a number to a shorter HTML-safe url path
]]
function util.encode_id(number)
local result = {}
local charlen = string.len(url_characters)
repeat
local pos = (number % charlen) + 1
number = math.floor(number / charlen)
table.insert(result,string.sub(url_characters,pos,pos))
until number == 0
return table.concat(result)
end
--[[
Given a short HTML-safe url path, convert it to a storyid
]]
function util.decode_id(s)
local res, id = pcall(function()
local n = 0
local charlen = string.len(url_characters)
for i = 1,string.len(s) do
local char = string.sub(s,i,i)
local pos = url_characters_rev[char] - 1
n = n + (pos*math.pow(charlen,i-1))
end
return n
end)
if res then
return id
else
error("Failed to decode id:" .. s)
end
end
--[[
Parses a semicolon seperated string into it's parts:
1. seperates by semicolon
2. trims whitespace
3. lowercases
4. capitalizes the first letter.
Returns an array of zero or more strings.
There is no blank tag, parsing "one;two;;three" will yield
{"one","two","three"}
]]
function util.parse_tags(str)
local tags = {}
for tag in string.gmatch(str,"([^;]+)") do
assert(tag, "Found a nil or false tag in:" .. str)
local tag_trimmed = string.match(tag,"%s*(.*)%s*")
local tag_lower = string.lower(tag_trimmed)
local tag_capitalized = string.gsub(tag_lower,"^.",string.upper)
assert(tag_capitalized, "After processing tag:" .. tag .. " it was falsey.")
if string.len(tag_capitalized) > 0 then
table.insert(tags, tag_capitalized)
end
end
return tags
end
return util

View File

@ -1,51 +1,49 @@
<% assert(author,"No author specified") %> <% assert(author,"No author specified") %> <% assert(bio,"No bio included") %> <!DOCTYPE html> <html lang="en"> <head> <meta charset="utf-8">
<% assert(bio,"No bio included") %>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>&#x1f351;</title> <title>&#x1f351;</title>
<link href="/_css/milligram.css" rel="stylesheet"> <link href="/_css/milligram.css" rel="stylesheet">
<link href="/_css/style.css" rel="stylesheet"> <link href="/_css/style.css" rel="stylesheet">
</head> </head>
<body class="container"> <body class="container">
<h1 class="title"> <main class="wrapper">
<a href="https://<%= author %>.<%= domain %>"><%= author %></a>.<a href="https://<%= domain %>"><%= domain %></a> <h1 class="title">
</h1> <a href="https://<%= author %>.<%= domain %>"><%= author %></a>.<a href="https://<%= domain %>"><%= domain %></a>
</h1>
<div class="content"> <div class="container">
<%= bio %> <a href="/_paste" class="button">New paste</a>
</div> </div>
<div class="content"> <div class="content">
<% if #stories == 0 then %> <%= bio %>
This author has not made any pastes yet. </div>
<% else %> <div class="content">
<table> <% if #stories == 0 then %>
<% for k,v in pairs(stories) do %> This author has not made any pastes yet.
<tr><td> <% else %>
<a href="<%= v.url %>"> <table>
<%- v.title %> <% for k,v in pairs(stories) do %>
</a> <tr><td>
</td><td> <a href="<%= v.url %>">
By <a href="https://<%= author %>.<%= domain %>"><%= author %></a> <%- v.title %>
</td><td> </a>
<ul class="row tag-list"> </td><td>
<% for i = 1,math.min(#v.tags, 5) do %> By <a href="https://<%= author %>.<%= domain %>"><%= author %></a>
<li><a class="tag button button-outline" href="https://<%= domain %>/_search?tag=<%= v.tags[i] %>"><%= v.tags[i] %></a></li> </td><td>
<% end %> <ul class="row tag-list">
</ul> <% for i = 1,math.min(#v.tags, 5) do %>
</td><td> <li><a class="tag button button-outline" href="https://<%= domain %>/_search?tag=<%= v.tags[i] %>"><%= v.tags[i] %></a></li>
<%= v.posted %> <% end %>
</td></tr> </ul>
</td><td>
<%= v.posted %>
</td></tr>
<% end %>
</table>
<% end %> <% end %>
</table> </div>
<% end %>
</div>
<footer class="footer"> <footer class="footer">
</footer> </footer>
</main>
</body> </body>
<body> <body>

View File

@ -12,6 +12,7 @@
#include "libkore.h" #include "libkore.h"
#include "libcrypto.h" #include "libcrypto.h"
#include <dirent.h> #include <dirent.h>
#include <kore/seccomp.h>
int home(struct http_request *); int home(struct http_request *);
int post_story(struct http_request *); int post_story(struct http_request *);
@ -38,6 +39,15 @@ lua_State *L;
static / _claim claim static / _claim claim
*/ */
/*Allow seccomp things for luajit and sqlite*/
KORE_SECCOMP_FILTER("app",
KORE_SYSCALL_ALLOW(pread64),
KORE_SYSCALL_ALLOW(pwrite64),
KORE_SYSCALL_ALLOW(fdatasync),
KORE_SYSCALL_ALLOW(unlinkat),
KORE_SYSCALL_ALLOW(mremap)
);
int int
errhandeler(lua_State *L){ errhandeler(lua_State *L){
printf("Error: %s\n",lua_tostring(L,1)); printf("Error: %s\n",lua_tostring(L,1));
@ -66,8 +76,10 @@ do_lua(struct http_request *req, const char *name){
printf("About to pcall\n"); printf("About to pcall\n");
int err = lua_pcall(L,1,0,-3); int err = lua_pcall(L,1,0,-3);
if(err != LUA_OK){ if(err != LUA_OK){
size_t retlen;
const char *ret = lua_tolstring(L,-1,&retlen);
printf("Failed to run %s: %s\n",name,lua_tostring(L,-1)); printf("Failed to run %s: %s\n",name,lua_tostring(L,-1));
http_response(req, 500, NULL, 0); http_response(req, 500, ret, retlen);
lua_pop(L,lua_gettop(L)); lua_pop(L,lua_gettop(L));
return (KORE_RESULT_OK); return (KORE_RESULT_OK);
} }

View File

@ -1,3 +1,12 @@
/*
If/when an author delets their account, all posts
and comments by that author are also deleted (on
delete cascade) this is intentional. This also
means that all comments by other users on a post
an author makes will also be deleted.
Post text uses zlib compression
*/
CREATE TABLE IF NOT EXISTS posts ( CREATE TABLE IF NOT EXISTS posts (
id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
post_text BLOB, post_text BLOB,

View File

@ -1,3 +1,8 @@
/*
Store the raw text so people can download it later, maybe
we can use it for "download as image" or "download as pdf"
in the future too. Stil stored zlib compressed
*/
CREATE TABLE IF NOT EXISTS raw_text ( CREATE TABLE IF NOT EXISTS raw_text (
id INTEGER PRIMARY KEY REFERENCES posts(id) ON DELETE CASCADE, id INTEGER PRIMARY KEY REFERENCES posts(id) ON DELETE CASCADE,
post_text BLOB, post_text BLOB,

View File

@ -1,3 +1,8 @@
/*
Store a cookie for logged in users. Logged in users can edit
their own posts.
*/
CREATE TABLE IF NOT EXISTS sessions ( CREATE TABLE IF NOT EXISTS sessions (
key TEXT PRIMARY KEY, key TEXT PRIMARY KEY,
author REFERENCES authors(id) ON DELETE CASCADE, author REFERENCES authors(id) ON DELETE CASCADE,

View File

@ -1,3 +1,7 @@
/*
Create a fake "anonymous" user, so
that no one runs into touble being able to paste under this account.
*/
INSERT OR IGNORE INTO authors ( INSERT OR IGNORE INTO authors (
id, id,
name, name,

View File

@ -1,3 +1,4 @@
/* Add a new comment to a story */
INSERT INTO comments( INSERT INTO comments(
postid, postid,
author, author,

View File

@ -1,3 +1,5 @@
/* Get the data we need to display a particular author's latest stories */
SELECT SELECT
posts.id, posts.id,
posts.post_title, posts.post_title,

View File

@ -1,3 +1,7 @@
/*
Get the author of a story, used to check when editing that the
author really owns the story they're trying to edit
*/
SELECT SELECT
authors.id, authors.id,
authors.name authors.name

View File

@ -1,3 +1,4 @@
/* Retreive comments on a story */
SELECT SELECT
authors.name, authors.name,
comments.isanon, comments.isanon,

View File

@ -1,3 +1,7 @@
/*
Select the data we need to read a story (and maybe display an edit button)
*/
SELECT SELECT
post_title, post_title,
post_text, post_text,

View File

@ -1,3 +1,4 @@
/* Select the data we need to display the on the front page */
SELECT SELECT
posts.id, posts.id,
posts.post_title, posts.post_title,

View File

@ -1 +1,2 @@
/* Update the view counter when someone reads a story */
UPDATE posts SET views = views + 1 WHERE id = :id; UPDATE posts SET views = views + 1 WHERE id = :id;