From 2cd10b6968fc26e519244ff33bed8f69d86b74b3 Mon Sep 17 00:00:00 2001 From: Robin Malley Date: Sun, 20 Dec 2020 08:16:23 +0000 Subject: [PATCH 1/3] Started work on refactor Started work on moveing each endpoint into it's own file. --- src/lua/cache.lua | 93 +++++++++++++++++++ src/lua/claim_get.lua | 16 ++++ src/lua/claim_post.lua | 66 +++++++++++++ src/lua/config.lua | 5 + src/lua/db.lua | 48 ++++++++++ src/lua/edit_get.lua | 64 +++++++++++++ src/lua/edit_post.lua | 85 +++++++++++++++++ src/lua/index_get.lua | 110 ++++++++++++++++++++++ src/lua/init.lua | 149 ++++++++++++++++++++---------- src/lua/login_get.lua | 11 +++ src/lua/login_post.lua | 58 ++++++++++++ src/lua/pages.lua | 25 +++++ src/lua/parsers.lua | 7 ++ src/lua/paste_get.lua | 81 ++++++++++++++++ src/lua/preview_post.lua | 32 +++++++ src/lua/queries.lua | 13 +++ src/lua/read_get.lua | 140 ++++++++++++++++++++++++++++ src/lua/read_post.lua | 53 +++++++++++ src/lua/render.lua | 2 + src/lua/session.lua | 67 ++++++++++++++ src/lua/tags.lua | 54 +++++++++++ src/lua/util.lua | 103 +++++++++++++++++++++ src/pages/author_index.etlua | 78 ++++++++-------- src/smr.c | 14 ++- src/sql/create_table_posts.sql | 9 ++ src/sql/create_table_raw_text.sql | 5 + src/sql/create_table_session.sql | 5 + src/sql/insert_anon_author.sql | 4 + src/sql/insert_comment.sql | 1 + src/sql/select_author_index.sql | 2 + src/sql/select_author_of_post.sql | 4 + src/sql/select_comments.sql | 1 + src/sql/select_post.sql | 4 + src/sql/select_site_index.sql | 1 + src/sql/update_views.sql | 1 + 35 files changed, 1319 insertions(+), 92 deletions(-) create mode 100644 src/lua/cache.lua create mode 100644 src/lua/claim_get.lua create mode 100644 src/lua/claim_post.lua create mode 100644 src/lua/config.lua create mode 100644 src/lua/db.lua create mode 100644 src/lua/edit_get.lua create mode 100644 src/lua/edit_post.lua create mode 100644 src/lua/index_get.lua create mode 100644 src/lua/login_get.lua create mode 100644 src/lua/login_post.lua create mode 100644 src/lua/pages.lua create mode 100644 src/lua/parsers.lua create mode 100644 src/lua/paste_get.lua create mode 100644 src/lua/preview_post.lua create mode 100644 src/lua/queries.lua create mode 100644 src/lua/read_get.lua create mode 100644 src/lua/read_post.lua create mode 100644 src/lua/render.lua create mode 100644 src/lua/session.lua create mode 100644 src/lua/tags.lua create mode 100644 src/lua/util.lua diff --git a/src/lua/cache.lua b/src/lua/cache.lua new file mode 100644 index 0000000..6290cef --- /dev/null +++ b/src/lua/cache.lua @@ -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 diff --git a/src/lua/claim_get.lua b/src/lua/claim_get.lua new file mode 100644 index 0000000..cd13079 --- /dev/null +++ b/src/lua/claim_get.lua @@ -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 diff --git a/src/lua/claim_post.lua b/src/lua/claim_post.lua new file mode 100644 index 0000000..a950fe8 --- /dev/null +++ b/src/lua/claim_post.lua @@ -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 diff --git a/src/lua/config.lua b/src/lua/config.lua new file mode 100644 index 0000000..804038f --- /dev/null +++ b/src/lua/config.lua @@ -0,0 +1,5 @@ + +return { + domain = "test.monster:8888", + production = false, +} diff --git a/src/lua/db.lua b/src/lua/db.lua new file mode 100644 index 0000000..f8dee8c --- /dev/null +++ b/src/lua/db.lua @@ -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 diff --git a/src/lua/edit_get.lua b/src/lua/edit_get.lua new file mode 100644 index 0000000..600e0e1 --- /dev/null +++ b/src/lua/edit_get.lua @@ -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 diff --git a/src/lua/edit_post.lua b/src/lua/edit_post.lua new file mode 100644 index 0000000..7bb093e --- /dev/null +++ b/src/lua/edit_post.lua @@ -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 diff --git a/src/lua/index_get.lua b/src/lua/index_get.lua new file mode 100644 index 0000000..7e65fe0 --- /dev/null +++ b/src/lua/index_get.lua @@ -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 diff --git a/src/lua/init.lua b/src/lua/init.lua index 2a971f7..9bb7161 100644 --- a/src/lua/init.lua +++ b/src/lua/init.lua @@ -1,19 +1,34 @@ + print("Really fast print from init.lua") +--Luarocks libraries local et = require("etlua") local sql = require("lsqlite3") 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 end +--[[ local parser_names = {"plain","imageboard"} local parsers = {} for _,v in pairs(parser_names) do parsers[v] = require("parser_" .. v) 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 = { "index", "author_index", @@ -36,7 +51,9 @@ for k,v in pairs(pagenames) do pages[v] = assert(et.compile(f:read("*a"))) f:close() end +]] +--[=[ local queries = {} --These are all loaded during startup, won't affect ongoing performance. setmetatable(queries,{ @@ -47,13 +64,13 @@ setmetatable(queries,{ return ret end }) - +]=] ---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_ins_tag, stmnt_drop_tags, stmnt_get_tags 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_edit, stmnt_update, stmnt_update_raw, stmnt_author_of local stmnt_comments, stmnt_comment_insert @@ -79,6 +96,7 @@ local function decodeentities(capture) end end +--[[ local function sqlassert(...) local r,errcode,err = ... if not r then @@ -86,6 +104,7 @@ local function sqlassert(...) end return r end +]] local function sqlbind(stmnt,call,position,data) assert(call == "bind" or call == "bind_blob","Bad bind call, call was:" .. call) @@ -99,22 +118,22 @@ end print("Hello from init.lua") function configure() - db = sqlassert(sql.open("data/posts.db")) - --db = sqlassert(sql.open_memory()) - cache = sqlassert(sql.open_memory()) - print("Compiled pages...") - --Test that compression works + --db = sqlassert(sql.open("data/posts.db")) + ----db = sqlassert(sql.open_memory()) + --cache = sqlassert(sql.open_memory()) + + --Test that compression works. For some reason, the zlib library + --fails if this is done as a one-liner local msg = "test message" local one = zlib.compress(msg) 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") --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 --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 --and comments by that author are also deleted (on --delete cascade) this is intentional. This also @@ -122,23 +141,24 @@ function configure() --an author makes will also be deleted. -- --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 --we can use it for "download as image" or "download as pdf" --in the future too. Stil stored zlib compressed - assert(db:exec(queries.create_table_raw_text)) - assert(db:exec(queries.create_table_images)) - assert(db:exec(queries.create_table_comments)) - assert(db:exec(queries.create_table_tags)) - assert(db:exec(queries.create_index_tags)) + --assert(db:exec(queries.create_table_raw_text)) + --assert(db:exec(queries.create_table_images)) + --assert(db:exec(queries.create_table_comments)) + --assert(db:exec(queries.create_table_tags)) + --assert(db:exec(queries.create_index_tags)) --Store a cookie for logged in users. Logged in users can edit --their own posts. - assert(db:exec(queries.create_table_session)) - print("Created db tables") + --assert(db:exec(queries.create_table_session)) + --print("Created db tables") --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, @@ -147,62 +167,71 @@ function configure() dirty INTEGER ); ]])) + ]=] --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 --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 - stmnt_update_views = assert(db:prepare(queries.update_views)) + --stmnt_update_views = assert(db:prepare(queries.update_views)) --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 - 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 - stmnt_author_bio = assert(db:prepare([[ + --[=[stmnt_author_bio = assert(db.conn:prepare([[ SELECT authors.biography FROM authors WHERE authors.name = :author; ]])) + ]=] --Get the author of a story, used to check when editing that the --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 --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 + --[=[ stmnt_author_acct = assert(db:prepare([[ SELECT id, salt, passhash FROM authors WHERE name = :name; ]])) + ]=] --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([[ SELECT name, passhash FROM authors WHERE name = :name; ]])) + ]=] --Create a new 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 --It might also be useful for migrations, if that ever needs to happen stmnt_raw = assert(db:prepare(queries.insert_raw)) --Tags for a story + --[[ stmnt_ins_tag = assert(db:prepare(queries.insert_tag)) stmnt_get_tags = assert(db:prepare(queries.select_tags)) stmnt_drop_tags = assert(db:prepare(queries.delete_tags)) + ]] --Get the data we need to display the edit screen stmnt_edit = assert(db:prepare(queries.select_edit)) --Get the data we need when someone wants to download a paste stmnt_download = assert(db:prepare(queries.select_download)) --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? --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. - stmnt_update = assert(db:prepare(queries.update_post)) + --stmnt_update = assert(db:prepare(queries.update_post)) --Check sessions for login support stmnt_insert_session = assert(db:prepare(queries.insert_session)) stmnt_get_session = assert(db:prepare(queries.select_valid_sessions)) --Search by tag name stmnt_search = assert(db:prepare(queries.select_post_tags)) --only refresh pages at most once every 10 seconds + --[=[ stmnt_cache = cache:prepare([[ SELECT data FROM cache @@ -223,6 +252,7 @@ function configure() SET dirty = 1 WHERE path = :path; ]]) + ]=] --[=[ ]=] print("finished running configure()") @@ -296,6 +326,7 @@ local function get_tags(id) until false end +--[=[ local function dirty_cache(url) print("Dirtying cache:",url) stmnt_dirty_cache:bind_names{ @@ -304,6 +335,7 @@ local function dirty_cache(url) err = do_sql(stmnt_dirty_cache) stmnt_dirty_cache:reset() end +]=] --[[ @@ -356,6 +388,7 @@ local function get_session(req) return author,authorid end +--[=[ --Render a page, with cacheing. If you need to dirty a cache, call dirty_cache() local function render(pagename,callback) print("Running render...") @@ -388,7 +421,9 @@ local function render(pagename,callback) stmnt_insert_cache:reset() return text 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]] local function parse_tags(str) local tags = {} @@ -404,6 +439,7 @@ local function parse_tags(str) end return tags end +]=] function home(req) print("Hello from lua!") @@ -414,7 +450,7 @@ function home(req) local text if host == domain then --Default home page - text = render(string.format("%s",domain),function() + text = cache.render(string.format("%s",domain),function() print("Cache miss, rendering index") stmnt_index:bind_names{} local err = do_sql(stmnt_index) @@ -442,7 +478,7 @@ function home(req) else --Home page for an author 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) stmnt_author_bio:bind_names{author=subdomain} local err = do_sql(stmnt_author_bio) @@ -505,7 +541,7 @@ function claim(req) local text if method == "GET" then --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") return pages.claim{err=""} end) @@ -578,7 +614,7 @@ function paste(req) return else --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") return pages.paste{ domain = domain, @@ -686,8 +722,8 @@ function paste(req) local loc = string.format("https://%s/%s",domain,url) http_response_header(req,"Location",loc) http_response(req,303,"") - dirty_cache(string.format("%s/%s",domain,url)) - dirty_cache(string.format("%s",domain)) + cache.dirty(string.format("%s/%s",domain,url)) + cache.dirty(string.format("%s",domain)) return elseif err == sql.ERROR or err == sql.MISUSE then ret = "Failed to paste: " .. tostring(err) @@ -748,9 +784,9 @@ function paste(req) end http_response_header(req,"Location",loc) http_response(req,303,"") - dirty_cache(string.format("%s.%s",author,domain)) - dirty_cache(string.format("%s/%s",domain,url)) - dirty_cache(string.format("%s",domain)) + cache.dirty(string.format("%s.%s",author,domain)) + cache.dirty(string.format("%s/%s",domain,url)) + cache.dirty(string.format("%s",domain)) return elseif err == sql.ERROR or err == sql.MISUSE then 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)) stmnt_update_views:reset() - dirty_cache(cachestr) + cache.dirty(cachestr) print("cachestr was:",cachestr) local readstoryf = function() 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 --pages when the user is logged in. All non-logged-in users can see the same page no problem. if not iam then - return render(cachestr,readstoryf) + return cache.render(cachestr,readstoryf) else return readstoryf() end @@ -841,6 +877,8 @@ function read(req) local path = http_request_get_path(req) local method = http_method_text(req) if method == "GET" then + read_get(req) + --[=[ local idp = string.sub(path,2)--remove leading "/" assert(string.len(path) > 0,"Tried to read 0-length story id") local author, authorid = get_session(req) @@ -902,7 +940,10 @@ function read(req) assert(text) http_response(req,200,text) return + ]=] elseif method == "POST" then + read_post(req) + --[=[ --We're posting a comment http_request_populate_post(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.") else --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) http_response_header(req,"Location",redir) http_response(req,303,"") end + ]=] end end @@ -949,7 +991,7 @@ function login(req) local text if method == "GET" then --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{ err = "", } @@ -1093,9 +1135,9 @@ function edit(req) end local id_enc = encode_id(storyid) 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 - 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). - dirty_cache(string.format("%s.%s",author,domain)) -- The author's index, same reasoning as above. + cache.dirty(string.format("%s/%s",domain,id_enc)) -- This place to read this post + 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). + cache.dirty(string.format("%s.%s",author,domain)) -- The author's index, same reasoning as above. http_response_header(req,"Location",loc) http_response(req,303,"") return @@ -1112,10 +1154,10 @@ end function teardown() print("Exiting...") if db then - db:close() + db.close() end if cache then - cache:close() + cache.close() end print("Finished lua teardown") end @@ -1127,6 +1169,7 @@ function download(req) http_request_populate_qs(req) local story = assert(http_argument_get_string(req,"story")) local story_id = decode_id(story) + print("Downloading", story_id) stmnt_download:bind_names{ postid = story_id } @@ -1137,6 +1180,7 @@ function download(req) stmnt_download:reset() return 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 text = zlib.decompress(txt_compressed) stmnt_download:reset() @@ -1147,6 +1191,8 @@ function download(req) end function preview(req) + preview_post(req) + --[[ print("We want to preview a paste!") local host = http_request_get_host(req) local path = http_request_get_path(req) @@ -1170,6 +1216,7 @@ function preview(req) tags = tags, } http_response(req,200,ret) + ]] end function search(req) diff --git a/src/lua/login_get.lua b/src/lua/login_get.lua new file mode 100644 index 0000000..4ad9c90 --- /dev/null +++ b/src/lua/login_get.lua @@ -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 diff --git a/src/lua/login_post.lua b/src/lua/login_post.lua new file mode 100644 index 0000000..d8cc902 --- /dev/null +++ b/src/lua/login_post.lua @@ -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 diff --git a/src/lua/pages.lua b/src/lua/pages.lua new file mode 100644 index 0000000..e492cad --- /dev/null +++ b/src/lua/pages.lua @@ -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 diff --git a/src/lua/parsers.lua b/src/lua/parsers.lua new file mode 100644 index 0000000..1f679c9 --- /dev/null +++ b/src/lua/parsers.lua @@ -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 diff --git a/src/lua/paste_get.lua b/src/lua/paste_get.lua new file mode 100644 index 0000000..440f722 --- /dev/null +++ b/src/lua/paste_get.lua @@ -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 diff --git a/src/lua/preview_post.lua b/src/lua/preview_post.lua new file mode 100644 index 0000000..b7886be --- /dev/null +++ b/src/lua/preview_post.lua @@ -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 diff --git a/src/lua/queries.lua b/src/lua/queries.lua new file mode 100644 index 0000000..3d4e500 --- /dev/null +++ b/src/lua/queries.lua @@ -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 diff --git a/src/lua/read_get.lua b/src/lua/read_get.lua new file mode 100644 index 0000000..2fbed09 --- /dev/null +++ b/src/lua/read_get.lua @@ -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 diff --git a/src/lua/read_post.lua b/src/lua/read_post.lua new file mode 100644 index 0000000..81907da --- /dev/null +++ b/src/lua/read_post.lua @@ -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 diff --git a/src/lua/render.lua b/src/lua/render.lua new file mode 100644 index 0000000..139597f --- /dev/null +++ b/src/lua/render.lua @@ -0,0 +1,2 @@ + + diff --git a/src/lua/session.lua b/src/lua/session.lua new file mode 100644 index 0000000..7912d01 --- /dev/null +++ b/src/lua/session.lua @@ -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 diff --git a/src/lua/tags.lua b/src/lua/tags.lua new file mode 100644 index 0000000..9612c66 --- /dev/null +++ b/src/lua/tags.lua @@ -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 diff --git a/src/lua/util.lua b/src/lua/util.lua new file mode 100644 index 0000000..a798141 --- /dev/null +++ b/src/lua/util.lua @@ -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 diff --git a/src/pages/author_index.etlua b/src/pages/author_index.etlua index 8654483..98b8f76 100644 --- a/src/pages/author_index.etlua +++ b/src/pages/author_index.etlua @@ -1,51 +1,49 @@ -<% assert(author,"No author specified") %> -<% assert(bio,"No bio included") %> - - - - - +<% assert(author,"No author specified") %> <% assert(bio,"No bio included") %> 🍑 -

- <%= author %>.<%= domain %> -

- -
- <%= bio %> -
-
- <% if #stories == 0 then %> - This author has not made any pastes yet. - <% else %> - - <% for k,v in pairs(stories) do %> - +
+

+ <%= author %>.<%= domain %> +

+
+ New paste +
+
+ <%= bio %> +
+
+ <% if #stories == 0 then %> + This author has not made any pastes yet. + <% else %> +
- - <%- v.title %> - - - By <%= author %> - - - - <%= v.posted %> -
+ <% for k,v in pairs(stories) do %> + + <% end %> +
+ + <%- v.title %> + + + By <%= author %> + + + + <%= v.posted %> +
<% end %> - - <% end %> -
+ - + diff --git a/src/smr.c b/src/smr.c index 6030602..3e14a5c 100644 --- a/src/smr.c +++ b/src/smr.c @@ -12,6 +12,7 @@ #include "libkore.h" #include "libcrypto.h" #include +#include int home(struct http_request *); int post_story(struct http_request *); @@ -38,6 +39,15 @@ lua_State *L; 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 errhandeler(lua_State *L){ 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"); int err = lua_pcall(L,1,0,-3); 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)); - http_response(req, 500, NULL, 0); + http_response(req, 500, ret, retlen); lua_pop(L,lua_gettop(L)); return (KORE_RESULT_OK); } diff --git a/src/sql/create_table_posts.sql b/src/sql/create_table_posts.sql index 76cc858..1f6f5dc 100644 --- a/src/sql/create_table_posts.sql +++ b/src/sql/create_table_posts.sql @@ -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 ( id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, post_text BLOB, diff --git a/src/sql/create_table_raw_text.sql b/src/sql/create_table_raw_text.sql index a424f9a..5ca8c51 100644 --- a/src/sql/create_table_raw_text.sql +++ b/src/sql/create_table_raw_text.sql @@ -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 ( id INTEGER PRIMARY KEY REFERENCES posts(id) ON DELETE CASCADE, post_text BLOB, diff --git a/src/sql/create_table_session.sql b/src/sql/create_table_session.sql index 28e5f11..e4f4c8c 100644 --- a/src/sql/create_table_session.sql +++ b/src/sql/create_table_session.sql @@ -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 ( key TEXT PRIMARY KEY, author REFERENCES authors(id) ON DELETE CASCADE, diff --git a/src/sql/insert_anon_author.sql b/src/sql/insert_anon_author.sql index b2ff0e6..0036f83 100644 --- a/src/sql/insert_anon_author.sql +++ b/src/sql/insert_anon_author.sql @@ -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 ( id, name, diff --git a/src/sql/insert_comment.sql b/src/sql/insert_comment.sql index 0adf012..3986b31 100644 --- a/src/sql/insert_comment.sql +++ b/src/sql/insert_comment.sql @@ -1,3 +1,4 @@ +/* Add a new comment to a story */ INSERT INTO comments( postid, author, diff --git a/src/sql/select_author_index.sql b/src/sql/select_author_index.sql index ac536f8..319da48 100644 --- a/src/sql/select_author_index.sql +++ b/src/sql/select_author_index.sql @@ -1,3 +1,5 @@ +/* Get the data we need to display a particular author's latest stories */ + SELECT posts.id, posts.post_title, diff --git a/src/sql/select_author_of_post.sql b/src/sql/select_author_of_post.sql index 8527b90..e875211 100644 --- a/src/sql/select_author_of_post.sql +++ b/src/sql/select_author_of_post.sql @@ -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 authors.id, authors.name diff --git a/src/sql/select_comments.sql b/src/sql/select_comments.sql index 05b5419..d147f79 100644 --- a/src/sql/select_comments.sql +++ b/src/sql/select_comments.sql @@ -1,3 +1,4 @@ +/* Retreive comments on a story */ SELECT authors.name, comments.isanon, diff --git a/src/sql/select_post.sql b/src/sql/select_post.sql index c614596..9e36fdc 100644 --- a/src/sql/select_post.sql +++ b/src/sql/select_post.sql @@ -1,3 +1,7 @@ +/* +Select the data we need to read a story (and maybe display an edit button) +*/ + SELECT post_title, post_text, diff --git a/src/sql/select_site_index.sql b/src/sql/select_site_index.sql index 024b692..99d8abc 100644 --- a/src/sql/select_site_index.sql +++ b/src/sql/select_site_index.sql @@ -1,3 +1,4 @@ +/* Select the data we need to display the on the front page */ SELECT posts.id, posts.post_title, diff --git a/src/sql/update_views.sql b/src/sql/update_views.sql index 3af22f7..685f6fe 100644 --- a/src/sql/update_views.sql +++ b/src/sql/update_views.sql @@ -1 +1,2 @@ +/* Update the view counter when someone reads a story */ UPDATE posts SET views = views + 1 WHERE id = :id; From a640096bdd6e69f2bef6c0421bf6adc32d79cbb4 Mon Sep 17 00:00:00 2001 From: Robin Malley Date: Mon, 21 Dec 2020 04:22:22 +0000 Subject: [PATCH 2/3] More work on refactor --- Makefile | 9 ++ spec/home_spec.lua | 28 +++++ spec/pages_sanity_spec.lua | 106 ++++++++++++++++ src/lua/cache.lua | 15 +-- src/lua/claim_post.lua | 18 ++- src/lua/db.lua | 10 +- src/lua/download_get.lua | 45 +++++++ src/lua/edit_get.lua | 14 ++- src/lua/edit_post.lua | 10 +- src/lua/endpoints/claim_get.lua | 16 +++ src/lua/endpoints/claim_post.lua | 74 +++++++++++ src/lua/endpoints/download_get.lua | 45 +++++++ src/lua/endpoints/edit_get.lua | 68 ++++++++++ src/lua/endpoints/edit_post.lua | 89 +++++++++++++ src/lua/endpoints/index_get.lua | 115 +++++++++++++++++ src/lua/endpoints/login_get.lua | 17 +++ src/lua/endpoints/login_post.lua | 61 +++++++++ src/lua/endpoints/paste_get.lua | 96 ++++++++++++++ src/lua/endpoints/paste_post.lua | 194 +++++++++++++++++++++++++++++ src/lua/endpoints/preview_post.lua | 33 +++++ src/lua/endpoints/read_get.lua | 169 +++++++++++++++++++++++++ src/lua/endpoints/read_post.lua | 53 ++++++++ src/lua/endpoints/search_get.lua | 59 +++++++++ src/lua/index_get.lua | 21 ++-- src/lua/init.lua | 137 +++++++++++++++----- src/lua/login_get.lua | 8 +- src/lua/login_post.lua | 11 +- src/lua/paste_get.lua | 25 +++- src/lua/paste_post.lua | 194 +++++++++++++++++++++++++++++ src/lua/preview_post.lua | 3 +- src/lua/read_get.lua | 63 +++++++--- src/lua/read_post.lua | 2 +- src/lua/search_get.lua | 59 +++++++++ src/lua/session.lua | 7 +- src/lua/tags.lua | 8 +- src/lua/util.lua | 22 +++- src/pages/read.etlua | 2 +- src/smr.c | 29 +++-- 38 files changed, 1829 insertions(+), 106 deletions(-) create mode 100644 spec/home_spec.lua create mode 100644 spec/pages_sanity_spec.lua create mode 100644 src/lua/download_get.lua create mode 100644 src/lua/endpoints/claim_get.lua create mode 100644 src/lua/endpoints/claim_post.lua create mode 100644 src/lua/endpoints/download_get.lua create mode 100644 src/lua/endpoints/edit_get.lua create mode 100644 src/lua/endpoints/edit_post.lua create mode 100644 src/lua/endpoints/index_get.lua create mode 100644 src/lua/endpoints/login_get.lua create mode 100644 src/lua/endpoints/login_post.lua create mode 100644 src/lua/endpoints/paste_get.lua create mode 100644 src/lua/endpoints/paste_post.lua create mode 100644 src/lua/endpoints/preview_post.lua create mode 100644 src/lua/endpoints/read_get.lua create mode 100644 src/lua/endpoints/read_post.lua create mode 100644 src/lua/endpoints/search_get.lua create mode 100644 src/lua/paste_post.lua create mode 100644 src/lua/search_get.lua diff --git a/Makefile b/Makefile index c2496bb..7f8ed35 100644 --- a/Makefile +++ b/Makefile @@ -11,10 +11,13 @@ domain=test.monster lua_files=$(shell find src/lua -type f) src_files=$(shell find src -type f) $(shell find conf -type f) sql_files=$(shell find src/sql -type f) +test_files=$(shell find spec -type f) +built_tests=$(test_files:%=$(chroot_dir)%) built_files=$(lua_files:src/lua/%.lua=$(chroot_dir)%.lua) page_files=$(shell find src/pages -type f) built_pages=$(page_files:src/pages/%.etlua=$(chroot_dir)pages/%.etlua) built_sql=$(sql_files:src/sql/%.sql=$(chroot_dir)sql/%.sql) +built=$(built_files) $(built_sql) $(built_pages) $(built_tests) all: $(chroot_dir) smr.so $(built_files) $(built_pages) $(built_sql) echo $(built_files) @@ -69,5 +72,11 @@ $(built_pages): $(chroot_dir)pages/%.etlua : src/pages/%.etlua $(built_sql): $(chroot_dir)sql/%.sql : src/sql/%.sql cp $^ $@ +$(built_tests) : $(chroot_dir)% : % + cp $^ $@ + smr.so : $(src_files) kodev build + +test : $(built) + cd kore_chroot && busted diff --git a/spec/home_spec.lua b/spec/home_spec.lua new file mode 100644 index 0000000..01f120e --- /dev/null +++ b/spec/home_spec.lua @@ -0,0 +1,28 @@ +--[[ +Test the home page +]] + + + + +describe("smr",function() + describe("site home page",function() + it("detours configure",function() + local s = {} + local c = false + function configure(...) + local args = {...} + if args[1] == s then + c = true + end + end + local oldconfigure = configure + local index_get = require("index_get") + configure(s) + assert(c) + end) + end) + describe("author home page",function() + + end) +end) diff --git a/spec/pages_sanity_spec.lua b/spec/pages_sanity_spec.lua new file mode 100644 index 0000000..35b38da --- /dev/null +++ b/spec/pages_sanity_spec.lua @@ -0,0 +1,106 @@ + +local pages = { + index = { + route = "/", + name = "home", + methods = { + GET={} + } + }, + paste = { + route = "/_paste", + name = "post_story", + methods = { + GET={}, + POST={} + } + }, + edit = { + route = "/_edit", + name = "edit", + methods = { + GET={}, + POST={}, + } + }, + --TODO:bio + login = { + route = "/_login", + name = "login", + methods = { + GET={}, + POST={}, + } + }, + claim = { + route = "/_claim", + name = "claim", + methods = { + GET = {}, + POST = {} + } + }, + download = { + route = "/_download", + name = "download", + methods = { + GET = {}, + } + }, + preview = { + route = "/_preview", + name = "preview", + methods = { + POST = {}, + } + }, + search = { + route = "/_search", + name = "search", + methods = { + GET = {}, + } + } + +} + +local request_stub_m = { +} +function http_response(req,errcode,str) + s = true +end +function http_request_get_host(reqstub) + return "localhost:8888" +end +function http_request_populate_post(reqstub) + reqstub.post_populated = true +end + +describe("smr",function() + for name, obj in pairs(pages) do + describe("endpoint " .. name,function() + for method,parameters in pairs(obj.methods) do + describe("method " .. method,function() + local fname = string.format("%s_%s",name,string.lower(method)) + it("should be named appropriately",function() + local f = assert(io.open(fname .. ".lua","r")) + end) + it("should run without errors",function() + require(fname) + end) + it("should return a function",function() + local pagefunc = assert(require(fname)) + assert(type(pagefunc) == "function") + end) + it("calls http_response()",function() + local pagefunc = require(fname) + local s = false + local reqstub = {} + pagefunc(reqstub) + end) + + end) + end + end) + end +end) diff --git a/src/lua/cache.lua b/src/lua/cache.lua index 6290cef..4880135 100644 --- a/src/lua/cache.lua +++ b/src/lua/cache.lua @@ -9,7 +9,7 @@ local stmnt_cache, stmnt_insert_cache local oldconfigure = configure function configure(...) - local cache = sqlassert(sql.open_memory()) + local cache = util.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 @@ -21,26 +21,26 @@ function configure(...) dirty INTEGER ); ]])) - stmnt_cache = cache:prepare([[ + stmnt_cache = assert(cache:prepare([[ SELECT data FROM cache WHERE path = :path AND ((dirty = 0) OR (strftime('%s','now') - updated) < 20) ; - ]]) - stmnt_insert_cache = cache:prepare([[ + ]])) + stmnt_insert_cache = assert(cache:prepare([[ INSERT OR REPLACE INTO cache ( path, data, updated, dirty ) VALUES ( :path, :data, strftime('%s','now'), 0 ); - ]]) - stmnt_dirty_cache = cache:prepare([[ + ]])) + stmnt_dirty_cache = assert(cache:prepare([[ UPDATE OR IGNORE cache SET dirty = 1 WHERE path = :path; - ]]) + ]])) return oldconfigure(...) end @@ -74,6 +74,7 @@ function ret.render(pagename,callback) error("Failed to update cache for page " .. pagename) end stmnt_insert_cache:reset() + print("returning text from cache.render:",text) return text end diff --git a/src/lua/claim_post.lua b/src/lua/claim_post.lua index a950fe8..ae4d149 100644 --- a/src/lua/claim_post.lua +++ b/src/lua/claim_post.lua @@ -3,13 +3,16 @@ local sql = require("lsqlite3") local pages = require("pages") local db = require("db") local queries = require("queries") +local util = require("util") +local sessionlib = require("session") +local config = require("config") local stmnt_author_create local oldconfigure = configure function configure(...) - stmnt_author_create = assert(db.conn:prepare(queries.insert_author)) + stmnt_author_create = util.sqlassert(db.conn:prepare(queries.insert_author)) return oldconfigure(...) end @@ -39,16 +42,20 @@ local function claim_post(req) } stmnt_author_create:bind_blob(2,salt) stmnt_author_create:bind_blob(3,hash) - local err = do_sql(stmnt_author_create) + local err = util.do_sql(stmnt_author_create) if err == sql.DONE then - --We sucessfully made athe new author + print("success") + --We sucessfully made the 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) + http_response_header(req,"Content-Disposition","attachment; filename=\"" .. name .. "." .. config.domain .. ".passfile\"") + local session = sessionlib.start(id) text = password + print("session started, about to send password:",text) + http_response(req,200,text) + return elseif err == sql.CONSTRAINT then --If the creation failed, they probably just tried --to use a name that was already taken @@ -62,5 +69,6 @@ local function claim_post(req) } end stmnt_author_create:reset() + http_response(req,200,text) end return claim_post diff --git a/src/lua/db.lua b/src/lua/db.lua index f8dee8c..3810e3e 100644 --- a/src/lua/db.lua +++ b/src/lua/db.lua @@ -2,11 +2,13 @@ local sql = require("lsqlite3") local queries = require("queries") +local util = require("util") local db = {} local oldconfigure = configure +db.conn = util.sqlassert(sql.open("data/posts.db")) function configure(...) - db.conn = sqlassert(sql.open("data/posts.db")) + --db.conn = sqlassert(sql.open("data/posts.db")) --Create sql tables assert(db.conn:exec(queries.create_table_authors)) @@ -35,14 +37,16 @@ function configure(...) 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)) + assert(db.conn:exec(queries.create_table_session)) print("Created db tables") - return configure(...) + return oldconfigure(...) end +configure() function db.close() db.conn:close() end + return db diff --git a/src/lua/download_get.lua b/src/lua/download_get.lua new file mode 100644 index 0000000..48f4593 --- /dev/null +++ b/src/lua/download_get.lua @@ -0,0 +1,45 @@ +local sql = require("lsqlite3") +local zlib = require("zlib") + +local db = require("db") +local queries = require("queries") +local util = require("util") +local pages = require("pages") + +local stmnt_download +local oldconfigure = configure +function configure(...) + stmnt_download = assert(db.conn:prepare(queries.select_download)) + return oldconfigure(...) +end + +local function download_get(req) + local host = http_request_get_host(req) + local path = http_request_get_path(req) + print("host:",host,"path:",path) + http_request_populate_qs(req) + local story = assert(http_argument_get_string(req,"story")) + local story_id = util.decode_id(story) + print("Downloading", story_id) + stmnt_download:bind_names{ + postid = story_id + } + local err = util.do_sql(stmnt_download) + if err == sql.DONE then + --No rows, story not found + http_responose(req,404,pages.nostory{path=story}) + stmnt_download:reset() + return + 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 text = zlib.decompress(txt_compressed) + stmnt_download:reset() + http_response_header(req,"Content-Type","application/octet-stream") + local nicetitle = title:gsub("%W","_") + http_response_header(req,"Content-Disposition","attachment; filename=\"" .. nicetitle .. ".txt\"") + http_response(req,200,text) + +end + +return download_get diff --git a/src/lua/edit_get.lua b/src/lua/edit_get.lua index 600e0e1..1881a58 100644 --- a/src/lua/edit_get.lua +++ b/src/lua/edit_get.lua @@ -6,22 +6,24 @@ local queries = require("queries") local util = require("util") local pages = require("pages") local tags = require("tags") +local session = require("session") +local config = require("config") local stmnt_edit local oldconfigure = configure function configure(...) stmnt_edit = assert(db.conn:prepare(queries.select_edit)) - return configure(...) + return oldconfigure(...) 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) + local author, authorid = session.get(req) http_request_populate_qs(req) local story = assert(http_argument_get_string(req,"story")) - local story_id = decode_id(story) + local story_id = util.decode_id(story) local ret print("we want to edit story:",story) @@ -29,7 +31,7 @@ local function edit_get(req) --sql-side. If we're not the owner, we'll get 0 rows back. stmnt_edit:bind_names{ postid = story_id, - authorid = author_id + authorid = authorid } local err = util.do_sql(stmnt_edit) if err == sql.DONE then @@ -55,10 +57,12 @@ local function edit_get(req) markup = markup, user = author, isanon = isanon == 1, - domain = domain, + domain = config.domain, story = story_id, err = "", tags = tags_txt } http_response(req,200,ret) end + +return edit_get diff --git a/src/lua/edit_post.lua b/src/lua/edit_post.lua index 7bb093e..0e612d8 100644 --- a/src/lua/edit_post.lua +++ b/src/lua/edit_post.lua @@ -1,6 +1,7 @@ local sql = require("lsqlite3") local zlib = require("zlib") +local db = require("db") local queries = require("queries") local pages = require("pages") local parsers = require("parsers") @@ -8,6 +9,7 @@ local util = require("util") local tagslib = require("tags") local cache = require("cache") local config = require("config") +local session = require("session") local stmnt_author_of, stmnt_update_raw, stmnt_update @@ -22,7 +24,7 @@ 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) + local author, author_id = session.get(req) http_request_populate_post(req) local storyid = tonumber(assert(http_argument_get_string(req,"story"))) @@ -34,7 +36,7 @@ local function edit_post(req) stmnt_author_of:bind_names{ id = storyid } - local err = do_sql(stmnt_author_of) + local err = util.do_sql(stmnt_author_of) if err ~= sql.ROW then stmnt_author_of:reset() error("No author found for story:" .. storyid) @@ -59,7 +61,7 @@ local function edit_post(req) 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") + assert(util.do_sql(stmnt_update) == sql.DONE, "Failed to update text") stmnt_update:reset() tagslib.set(storyid,tags) --[[ @@ -83,3 +85,5 @@ local function edit_post(req) http_response(req,303,"") return end + +return edit_post diff --git a/src/lua/endpoints/claim_get.lua b/src/lua/endpoints/claim_get.lua new file mode 100644 index 0000000..cd13079 --- /dev/null +++ b/src/lua/endpoints/claim_get.lua @@ -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 diff --git a/src/lua/endpoints/claim_post.lua b/src/lua/endpoints/claim_post.lua new file mode 100644 index 0000000..ae4d149 --- /dev/null +++ b/src/lua/endpoints/claim_post.lua @@ -0,0 +1,74 @@ +local sql = require("lsqlite3") + +local pages = require("pages") +local db = require("db") +local queries = require("queries") +local util = require("util") +local sessionlib = require("session") +local config = require("config") + +local stmnt_author_create + +local oldconfigure = configure +function configure(...) + + stmnt_author_create = util.sqlassert(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 = util.do_sql(stmnt_author_create) + if err == sql.DONE then + print("success") + --We sucessfully made the 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 .. "." .. config.domain .. ".passfile\"") + local session = sessionlib.start(id) + text = password + print("session started, about to send password:",text) + http_response(req,200,text) + return + 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() + http_response(req,200,text) +end +return claim_post diff --git a/src/lua/endpoints/download_get.lua b/src/lua/endpoints/download_get.lua new file mode 100644 index 0000000..48f4593 --- /dev/null +++ b/src/lua/endpoints/download_get.lua @@ -0,0 +1,45 @@ +local sql = require("lsqlite3") +local zlib = require("zlib") + +local db = require("db") +local queries = require("queries") +local util = require("util") +local pages = require("pages") + +local stmnt_download +local oldconfigure = configure +function configure(...) + stmnt_download = assert(db.conn:prepare(queries.select_download)) + return oldconfigure(...) +end + +local function download_get(req) + local host = http_request_get_host(req) + local path = http_request_get_path(req) + print("host:",host,"path:",path) + http_request_populate_qs(req) + local story = assert(http_argument_get_string(req,"story")) + local story_id = util.decode_id(story) + print("Downloading", story_id) + stmnt_download:bind_names{ + postid = story_id + } + local err = util.do_sql(stmnt_download) + if err == sql.DONE then + --No rows, story not found + http_responose(req,404,pages.nostory{path=story}) + stmnt_download:reset() + return + 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 text = zlib.decompress(txt_compressed) + stmnt_download:reset() + http_response_header(req,"Content-Type","application/octet-stream") + local nicetitle = title:gsub("%W","_") + http_response_header(req,"Content-Disposition","attachment; filename=\"" .. nicetitle .. ".txt\"") + http_response(req,200,text) + +end + +return download_get diff --git a/src/lua/endpoints/edit_get.lua b/src/lua/endpoints/edit_get.lua new file mode 100644 index 0000000..1881a58 --- /dev/null +++ b/src/lua/endpoints/edit_get.lua @@ -0,0 +1,68 @@ +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 session = require("session") +local config = require("config") + +local stmnt_edit +local oldconfigure = configure +function configure(...) + stmnt_edit = assert(db.conn:prepare(queries.select_edit)) + return oldconfigure(...) +end + +local function edit_get(req) + local host = http_request_get_host(req) + local path = http_request_get_path(req) + local author, authorid = session.get(req) + + http_request_populate_qs(req) + local story = assert(http_argument_get_string(req,"story")) + local story_id = util.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 = authorid + } + 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 = config.domain, + story = story_id, + err = "", + tags = tags_txt + } + http_response(req,200,ret) +end + +return edit_get diff --git a/src/lua/endpoints/edit_post.lua b/src/lua/endpoints/edit_post.lua new file mode 100644 index 0000000..0e612d8 --- /dev/null +++ b/src/lua/endpoints/edit_post.lua @@ -0,0 +1,89 @@ +local sql = require("lsqlite3") +local zlib = require("zlib") + +local db = require("db") +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 session = require("session") + +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 = session.get(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 = util.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(util.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 + +return edit_post diff --git a/src/lua/endpoints/index_get.lua b/src/lua/endpoints/index_get.lua new file mode 100644 index 0000000..104d09b --- /dev/null +++ b/src/lua/endpoints/index_get.lua @@ -0,0 +1,115 @@ +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 libtags = require("tags") + +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 oldconfigure(...) +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 = libtags.get(data[1]) + table.insert(latest,{ + url = util.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 = util.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 = util.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 = libtags.get(id) + table.insert(stories,{ + url = util.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 + assert(text) + http_response(req,200,text) +end + +return index_get diff --git a/src/lua/endpoints/login_get.lua b/src/lua/endpoints/login_get.lua new file mode 100644 index 0000000..e9d279d --- /dev/null +++ b/src/lua/endpoints/login_get.lua @@ -0,0 +1,17 @@ +local config = require("config") +local cache = require("cache") +local config = require("config") +local pages = require("pages") + + +local function login_get(req) + --Just give them the login page + local ret = cache.render(string.format("%s/_login",config.domain),function() + return pages.login{ + err = "", + } + end) + http_response(req,200,ret) +end + +return login_get diff --git a/src/lua/endpoints/login_post.lua b/src/lua/endpoints/login_post.lua new file mode 100644 index 0000000..46d7504 --- /dev/null +++ b/src/lua/endpoints/login_post.lua @@ -0,0 +1,61 @@ +local sql = require("lsqlite3") + +local db = require("db") +local util = require("util") +local session = require("session") +local config = require("config") +local pages = require("pages") + +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 oldconfigure(...) +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 mysession = session.start(id) + http_response_cookie(req,"session",mysession,"/",0,0) + local loc = string.format("https://%s.%s",name,config.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 diff --git a/src/lua/endpoints/paste_get.lua b/src/lua/endpoints/paste_get.lua new file mode 100644 index 0000000..b371a0b --- /dev/null +++ b/src/lua/endpoints/paste_get.lua @@ -0,0 +1,96 @@ +local config = require("config") +local session = require("session") +local pages = require("pages") +local cache = require("cache") + +local function paste_get(req) + --Get the paste page + local host = http_request_get_host(req) + local text + local author,_ = session.get(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 = config.domain, + err = "", + } + end) + http_response(req,200,text) + elseif host ~= config.domain and author then + text = pages.author_paste{ + domain = config.domain, + user = author, + err = "", + text="", + } + elseif host ~= config.domain and author == nil then + http_response_header(req,"Location",string.format("https://%s/_paste",config.domain)) + http_response(req,303,"") + else + error(string.format( + "Unable to find a good case for paste:%s,%s,%s", + host, + config.domain, + author + )) + end + assert(text) + http_response(req,200,text) + --[=[ + 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,_ = session.get(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 + +return paste_get diff --git a/src/lua/endpoints/paste_post.lua b/src/lua/endpoints/paste_post.lua new file mode 100644 index 0000000..243cf6e --- /dev/null +++ b/src/lua/endpoints/paste_post.lua @@ -0,0 +1,194 @@ +local sql = require("lsqlite3") +local zlib = require("zlib") + +local util = require("util") +local parsers = require("parsers") +local config = require("config") +local queries = require("queries") +local db = require("db") +local cache = require("cache") +local tags = require("tags") +local session = require("session") + +local stmnt_raw,stmnt_paste + +local oldconfigure = configure +function configure(...) + stmnt_paste = assert(db.conn:prepare(queries.insert_post)) + stmnt_raw = assert(db.conn:prepare(queries.insert_raw)) + return oldconfigure(...) +end + +local function anon_paste(req,ps) + --Public paste + --[[ + This doesn't actually do much for IPv4 addresses, + since there are only 32 bits of address. Someone who + got a copy of the database could + just generate all 2^32 hashes and look up who posted + what. Use IPv6, Tor or I2P where possible. (but then I + guess it's harder to ban spammers... hmm..) + ]] + --local ip = http_request_get_ip(req) + --local iphash = sha3(ip) + --Don't store this information for now, until I come up + --with a more elegent solution. + + util.sqlbind(stmnt_paste,"bind_blob",1,ps.text) + --assert(stmnt_paste:bind_blob(1,text) == sql.OK) + util.sqlbind(stmnt_paste,"bind",2,ps.title) + --assert(stmnt_paste:bind(2,esctitle) == sql.OK) + util.sqlbind(stmnt_paste,"bind",3,-1) + --assert(stmnt_paste:bind(3,-1) == sql.OK) + util.sqlbind(stmnt_paste,"bind",4,true) + --assert(stmnt_paste:bind(4,true) == sql.OK) + util.sqlbind(stmnt_paste,"bind_blob",5,"") + --assert(stmnt_paste:bind_blob(5,"") == sql.OK) + err = util.do_sql(stmnt_paste) + stmnt_paste:reset() + if err == sql.DONE then + local rowid = stmnt_paste:last_insert_rowid() + assert(stmnt_raw:bind(1,rowid) == sql.OK) + assert(stmnt_raw:bind_blob(2,ps.raw) == sql.OK) + assert(stmnt_raw:bind(3,ps.markup) == sql.OK) + err = util.do_sql(stmnt_raw) + stmnt_raw:reset() + if err ~= sql.DONE then + print("Failed to save raw text, but paste still went though") + end + tags.set(rowid,ps.tags) + --[[ + for _,tag in pairs(ps.tags) do + print("tag 1:",stmnt_ins_tag:bind(1,rowid)) + print("Looking at tag",tag) + print("tag 2:",stmnt_ins_tag:bind(2,tag)) + err = util.do_sql(stmnt_ins_tag) + stmnt_ins_tag:reset() + end + ]] + local url = util.encode_id(rowid) + local loc = string.format("https://%s/%s",config.domain,url) + http_response_header(req,"Location",loc) + http_response(req,303,"") + cache.dirty(string.format("%s/%s",config.domain,url)) + cache.dirty(string.format("%s",config.domain)) + return + elseif err == sql.ERROR or err == sql.MISUSE then + ret = "Failed to paste: " .. tostring(err) + else + error("Error pasting:" .. tostring(err)) + end + stmnt_paste:reset() +end +local function author_paste(req,ps) + --Author paste + local author, authorid = session.get(req) + if author == nil then + ret = pages.author_paste{ + domain = domain, + author = subdomain, + err = "You are not logged in, you must be logged in to post as " .. subdomain .. ".", + text = text + } + end + local asanon = assert(http_argument_get_string(req,"pasteas")) + --No need to check if the author is posting to the + --"right" sudomain, just post it to the one they have + --the session key for. + assert(stmnt_paste:bind_blob(1,ps.text) == sql.OK) + assert(stmnt_paste:bind(2,ps.title) == sql.OK) + assert(stmnt_paste:bind(3,authorid) == sql.OK) + if asanon == "anonymous" then + assert(stmnt_paste:bind(4,true) == sql.OK) + else + assert(stmnt_paste:bind(4,false) == sql.OK) + end + assert(stmnt_paste:bind_blob(5,"") == sql.OK) + err = util.do_sql(stmnt_paste) + stmnt_paste:reset() + if err == sql.DONE then + local rowid = stmnt_paste:last_insert_rowid() + assert(stmnt_raw:bind(1,rowid) == sql.OK) + assert(stmnt_raw:bind_blob(2,ps.raw) == sql.OK) + assert(stmnt_raw:bind(3,ps.markup) == sql.OK) + err = util.do_sql(stmnt_raw) + stmnt_raw:reset() + if err ~= sql.DONE then + print("Failed to save raw text, but paste still went through") + end + tags.set(rowid,ps.tags) + --[[ + for _,tag in pairs(ps.tags) do + print("tag 1:",stmnt_ins_tag:bind(1,rowid)) + print("Looking at tag",tag) + print("tag 2:",stmnt_ins_tag:bind(2,tag)) + err = do_sql(stmnt_ins_tag) + stmnt_ins_tag:reset() + end + ]] + local url = util.encode_id(rowid) + local loc + if asanon == "anonymous" then + loc = string.format("https://%s/%s",config.domain,url) + else + loc = string.format("https://%s.%s/%s",author,config.domain,url) + end + http_response_header(req,"Location",loc) + http_response(req,303,"") + cache.dirty(string.format("%s.%s",author,config.domain)) + cache.dirty(string.format("%s/%s",config.domain,url)) + cache.dirty(string.format("%s",config.domain)) + return + elseif err == sql.ERROR or err == sql.MISUSE then + ret = "Failed to paste: " .. tostring(err) + else + error("Error pasting:",err) + end + stmnt_paste:reset() + +end +local function decodeentities(capture) + local n = tonumber(capture,16) + local c = string.char(n) + if escapes[c] then + return escapes[c] + else + return c + end +end +local function paste_post(req) + local host = http_request_get_host(req) + local path = http_request_get_path(req) + + local ps = {} + --We're creatinga new paste + http_request_populate_post(req) + local title = assert(http_argument_get_string(req,"title")) + local text = assert(http_argument_get_string(req,"text")) + ps.markup = assert(http_argument_get_string(req,"markup")) + local tag_str = http_argument_get_string(req,"tags") + ps.tags = {} + if tag_str then + ps.tags = util.parse_tags(tag_str) + end + local pasteas + ps.raw = zlib.compress(text) + text = string.gsub(text,"%%(%x%x)",decodeentities) + text = parsers[ps.markup](text) + assert(text,"Failed to parse text") + text = zlib.compress(text) + assert(text,"Failed to compress text") + ps.text = text + local esctitle = string.gsub(title,"%%(%x%x)",decodeentities) + --Always sanatize the title with the plain parser. no markup + --in the title. + ps.title = parsers.plain(title) + if host == config.domain then + anon_paste(req,ps) + else + author_paste(req,ps) + end +end +--assert(ret) +--http_response(req,200,ret) +return paste_post diff --git a/src/lua/endpoints/preview_post.lua b/src/lua/endpoints/preview_post.lua new file mode 100644 index 0000000..e3cb8da --- /dev/null +++ b/src/lua/endpoints/preview_post.lua @@ -0,0 +1,33 @@ +local parsers = require("parsers") +local tags = require("tags") +local util = require("util") +local pages = require("pages") +local config = require("config") + +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 = config.domain, + title = title, + author = "preview", + idp = "preview", + text = parsed, + tags = tags, + } + http_response(req,200,ret) +end + +return preview_post diff --git a/src/lua/endpoints/read_get.lua b/src/lua/endpoints/read_get.lua new file mode 100644 index 0000000..3874b9a --- /dev/null +++ b/src/lua/endpoints/read_get.lua @@ -0,0 +1,169 @@ +local sql = require("sqlite3") + +local session = require("session") +local tags = require("tags") +local db = require("db") +local queries = require("queries") +local util = require("util") +local cache = require("cache") +local pages = require("pages") +local config = require("config") + +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 oldconfigure(...) +end + + +--[[ +Increases a story's hit counter by 1 +]] +local function add_view(storyid) + stmnt_update_views:bind_names{ + id = storyid + } + local err = util.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 = util.do_sql(stmnt_read) + if err == sql.DONE then + --We got no story + stmnt_read:reset() + error("No story by this name",ps.storyid) + return + end + --If we've made it here, we have a story. Populate our settings + --with title, text, ect. + assert(err == sql.ROW) + local title, storytext, tauthor, isanon, authorname, views = unpack( + stmnt_read:get_values() + ) + ps.title = title + ps.text = zlib.decompress(storytext) + ps.tauthor = tauthor + ps.isanon = isanon == 1 + ps.author = authorname + ps.views = views + stmnt_read:reset() + --Tags + ps.tags = tags.get(ps.storyid) + return true +end + +--[[ +Get the comments for a story +]] +local function get_comments(req,ps) + stmnt_comments:bind_names{ + id = ps.storyid + } + err = util.do_sql(stmnt_comments) + local comments = {} + while err ~= sql.DONE do + local com_author, com_isanon, com_text = unpack(stmnt_comments:get_values()) + table.insert(comments,{ + author = com_author, + isanon = com_isanon == 1, --int to boolean + text = com_text + }) + err = stmnt_comments:step() + end + stmnt_comments:reset() + return comments +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 = { + domain = config.domain, + host = http_request_get_host(req), + path = http_request_get_path(req), + method = http_method_text(req), + } + print("reading", ps.path) + + --Get our story id + assert(string.len(ps.path) > 0,"Tried to read 0-length story id") + ps.idp = string.sub(ps.path,2)--remove leading "/" + ps.storyid = util.decode_id(ps.idp) + add_view(ps.storyid) + + --If we're logged in, set author and authorid + local author, authorid = session.get(req) + if author and authorid then + ps.loggedauthor = author + ps.iam = author + ps.loggedauthorid = 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 + + local text + --normal story display + if (not ps.loggedauthor) then + print("not author") + local cachestr = string.format("%s%s%s", + ps.host, + ps.path, + ps.show_comments and "?comments=1" or "" + ) + text = cache.render(cachestr,function() + populate_ps_story(req,ps) + local output = pages.read(ps) + assert(output,"failed to read page:" .. cachestr) + return output + end) + else --we are logged in, don't cache + print("is author") + populate_ps_story(req,ps) + print("tauthor was", ps.tauthor, "while author was:",ps.author) + ps.owner = (ps.loggedauthorid == ps.tauthor) + text = pages.read(ps) + end + assert(text) + http_response(req,200,text) + return +end + +return read_get diff --git a/src/lua/endpoints/read_post.lua b/src/lua/endpoints/read_post.lua new file mode 100644 index 0000000..2fde5ef --- /dev/null +++ b/src/lua/endpoints/read_post.lua @@ -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", config.domain, path) + http_response_header(req,"Location",redir) + http_response(req,303,"") + end + +end +return read_post diff --git a/src/lua/endpoints/search_get.lua b/src/lua/endpoints/search_get.lua new file mode 100644 index 0000000..4c340a3 --- /dev/null +++ b/src/lua/endpoints/search_get.lua @@ -0,0 +1,59 @@ +local sql = require("lsqlite3") + +local db = require("db") +local queries = require("queries") +local util = require("util") +local libtags = require("tags") +local pages = require("pages") +local config = require("config") + +local stmnt_search +local oldconfigure = configure +function configure(...) + stmnt_search = assert(db.conn:prepare(queries.select_post_tags)) + return oldconfigure(...) +end + +local function search_get(req) + local host = http_request_get_host(req) + local path = http_request_get_path(req) + http_request_populate_qs(req) + local tag = http_argument_get_string(req,"tag") + if tag then + stmnt_search:bind_names{ + tag = tag + } + local results = {} + local err + repeat + err = stmnt_search:step() + if err == sql.BUSY then + coroutine.yield() + elseif err == sql.ROW then + local id, title, anon, time, author = unpack(stmnt_search:get_values()) + local idp = util.encode_id(id) + local tags = libtags.get(id) + table.insert(results,{ + id = idp, + title = title, + anon = anon, + time = os.date("%B %d %Y",tonumber(time)), + author = author, + tags = tags + }) + elseif err == sql.DONE then + stmnt_search:reset() + else + error("Failed to search, sql error:" .. tostring(err)) + end + until err == sql.DONE + local ret = pages.search{ + domain = config.domain, + results = results, + tag = tag, + } + http_response(req,200,ret) + end +end + +return search_get diff --git a/src/lua/index_get.lua b/src/lua/index_get.lua index 7e65fe0..104d09b 100644 --- a/src/lua/index_get.lua +++ b/src/lua/index_get.lua @@ -6,6 +6,7 @@ local db = require("db") local util = require("util") local config = require("config") local pages = require("pages") +local libtags = require("tags") local stmnt_index, stmnt_author, stmnt_author_bio @@ -17,7 +18,7 @@ function configure(...) SELECT authors.biography FROM authors WHERE authors.name = :author; ]])) stmnt_author = assert(db.conn:prepare(queries.select_author_index)) - return configure(...) + return oldconfigure(...) end local function get_site_home(req) @@ -28,9 +29,9 @@ local function get_site_home(req) --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]) + local storytags = libtags.get(data[1]) table.insert(latest,{ - url = encode_id(data[1]), + url = util.encode_id(data[1]), title = data[2], isanon = data[3] == 1, posted = os.date("%B %d %Y",tonumber(data[4])), @@ -49,7 +50,7 @@ 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) + local err = util.do_sql(stmnt_author_bio) if err == sql.DONE then print("No such author") stmnt_author_bio:reset() @@ -64,15 +65,15 @@ local function get_author_home(req) stmnt_author_bio:reset() print("Getting author's stories") stmnt_author:bind_names{author=subdomain} - err = do_sql(stmnt_author) + err = util.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) + local tags = libtags.get(id) table.insert(stories,{ - url = encode_id(id), + url = util.encode_id(id), title = title, posted = os.date("%B %d %Y",tonumber(time)), tags = tags, @@ -97,7 +98,7 @@ local function index_get(req) local subdomain = host:match("([^\\.]+)") local text if host == config.domain then - local cachepath = string.format("%s",config.domain), + local cachepath = string.format("%s",config.domain) text = cache.render(cachepath, function() return get_site_home(req) end) @@ -107,4 +108,8 @@ local function index_get(req) return get_author_home(req) end) end + assert(text) + http_response(req,200,text) end + +return index_get diff --git a/src/lua/init.lua b/src/lua/init.lua index 9bb7161..ec457e1 100644 --- a/src/lua/init.lua +++ b/src/lua/init.lua @@ -6,6 +6,9 @@ local et = require("etlua") local sql = require("lsqlite3") local zlib = require("zlib") +--stubs for overloading +function configure(...) end + --smr code local cache = require("cache") local pages = require("pages") @@ -22,10 +25,39 @@ for _,v in pairs(parser_names) do parsers[v] = require("parser_" .. v) end ]] +local endpoint_names = { + read = {"get","post"}, + preview = {"post"}, + index = {"get"}, + paste = {"get","post"}, + download = {"get"}, + login = {"get","post"}, + edit = {"get","post"}, + claim = {"get","post"}, + search = {"get"}, +} +local endpoints = {} +for name, methods in pairs(endpoint_names) do + for _,method in pairs(methods) do + local epn = string.format("%s_%s",name,method) + endpoints[epn] = require("endpoints." .. epn) + end +end --pages read_get = require("read_get") read_post = require("read_post") preview_post = require("preview_post") +index_get = require("index_get") +paste_get = require("paste_get") +paste_post = require("paste_post") +download_get = require("download_get") +login_get = require("login_get") +login_post = require("login_post") +edit_get = require("edit_get") +edit_post = require("edit_post") +claim_get = require("claim_get") +claim_post = require("claim_post") +search_get = require("search_get") --local db,cache --databases --local domain = "test.monster:8888" --The domain to write links as --[[ @@ -117,7 +149,8 @@ end print("Hello from init.lua") -function configure() +local oldconfigure = configure +function configure(...) --db = sqlassert(sql.open("data/posts.db")) ----db = sqlassert(sql.open_memory()) --cache = sqlassert(sql.open_memory()) @@ -128,7 +161,7 @@ function configure() local one = zlib.compress(msg) local two = zlib.decompress(one) assert(two == msg, "zlib not working as expected") - + oldconfigure(...) --Create sql tables --assert(db:exec(queries.create_table_authors)) --Create a fake "anonymous" user, so we don't run into trouble @@ -205,10 +238,10 @@ function configure() ]])) ]=] --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 --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 --[[ stmnt_ins_tag = assert(db:prepare(queries.insert_tag)) @@ -216,9 +249,9 @@ function configure() stmnt_drop_tags = assert(db:prepare(queries.delete_tags)) ]] --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 - 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 --stmnt_update_raw = assert(db:prepare(queries.update_raw)) --Should we really reset the update time every time someone makes a post? @@ -226,10 +259,10 @@ function configure() --If it gets abused I can disable it I guess. --stmnt_update = assert(db:prepare(queries.update_post)) --Check sessions for login support - stmnt_insert_session = assert(db:prepare(queries.insert_session)) - stmnt_get_session = assert(db:prepare(queries.select_valid_sessions)) + --stmnt_insert_session = assert(db:prepare(queries.insert_session)) + --stmnt_get_session = assert(db:prepare(queries.select_valid_sessions)) --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 --[=[ stmnt_cache = cache:prepare([[ @@ -445,6 +478,10 @@ function home(req) print("Hello from lua!") print("Method:", http_method_text(req)) local method = http_method_text(req) + if method == "GET" then + endpoints.index_get(req) + end + --[=[ local host = http_request_get_host(req) local path = http_request_get_path(req) local text @@ -522,6 +559,7 @@ function home(req) end assert(text) http_response(req,200,text) + ]=] end --We prevent people from changing their password file, this way we don't really @@ -530,6 +568,7 @@ end --a while, but whatever. function claim(req) local method = http_method_text(req) + --[[ local host = http_request_get_host(req) local path = http_request_get_path(req) if host ~= domain then @@ -539,13 +578,19 @@ function claim(req) end assert(host == domain) local text + ]] if method == "GET" then + endpoints.claim_get(req) + --[=[ --Get the page to claim a name text = cache.render(string.format("%s/_claim",domain),function() print("cache miss, rendering claim page") return pages.claim{err=""} end) + ]=] elseif method == "POST" then + endpoints.claim_post(req) + --[=[ --Actually claim a name http_request_populate_post(req) local name = assert(http_argument_get_string(req,"user")) @@ -593,18 +638,21 @@ function claim(req) } end stmnt_author_create:reset() + ]=] end - assert(text) - http_response(req,200,text) + --assert(text) + --http_response(req,200,text) end function paste(req) - local host = http_request_get_host(req) - local path = http_request_get_path(req) + --local host = http_request_get_host(req) + --local path = http_request_get_path(req) local method = http_method_text(req) - local err - local ret + --local err + --local ret if method == "GET" then + endpoints.paste_get(req) + --[=[ --Get the paste page if host == domain then local author,_ = get_session(req) @@ -654,7 +702,10 @@ function paste(req) err = "", } end + ]=] elseif method == "POST" then + endpoints.paste_post(req) + --[=[ --We're creatinga new paste http_request_populate_post(req) local title = assert(http_argument_get_string(req,"title")) @@ -795,9 +846,11 @@ function paste(req) end stmnt_paste:reset() end + ]=] end - assert(ret) - http_response(req,200,ret) + + --assert(ret) + --http_response(req,200,ret) end --A helper function for below @@ -873,11 +926,11 @@ local function read_story(host,path,idp,show_comments,iam) end function read(req) - local host = http_request_get_host(req) - local path = http_request_get_path(req) + --local host = http_request_get_host(req) + --local path = http_request_get_path(req) local method = http_method_text(req) if method == "GET" then - read_get(req) + endpoints.read_get(req) --[=[ local idp = string.sub(path,2)--remove leading "/" assert(string.len(path) > 0,"Tried to read 0-length story id") @@ -942,7 +995,7 @@ function read(req) return ]=] elseif method == "POST" then - read_post(req) + endpoints.read_post(req) --[=[ --We're posting a comment http_request_populate_post(req) @@ -979,9 +1032,10 @@ function read(req) end function login(req) - local host = http_request_get_host(req) - local path = http_request_get_path(req) + --local host = http_request_get_host(req) + --local path = http_request_get_path(req) local method = http_method_text(req) + --[[ if host ~= domain then --Don't allow logging into subdomains, I guess http_response_header(req,"Location",string.format("https://%s/_login",domain)) @@ -989,14 +1043,20 @@ function login(req) return end local text + ]] if method == "GET" then + endpoints.login_get(req) + --[=[ --Just give them the login page text = cache.render(string.format("%s/_login",domain),function() return pages.login{ err = "", } end) + ]=] elseif method == "POST" then + endpoints.login_post(req) + --[=[ --Try to log in http_populate_multipart_form(req) local name = assert(http_argument_get_string(req,"user")) @@ -1032,19 +1092,22 @@ function login(req) stmnt_author_acct:reset() error("Other sql error during login") end + ]=] end - assert(text) - http_response(req,200,text) + --assert(text) + --http_response(req,200,text) end --Edit a story function edit(req) - local host = http_request_get_host(req) - local path = http_request_get_path(req) + --local host = http_request_get_host(req) + --local path = http_request_get_path(req) local method = http_method_text(req) - local author, author_id = get_session(req) - local ret + --local author, author_id = get_session(req) + --local ret if method == "GET" then + endpoints.edit_get(req) + --[=[ http_request_populate_qs(req) local story = assert(http_argument_get_string(req,"story")) local story_id = decode_id(story) @@ -1084,7 +1147,10 @@ function edit(req) err = "", tags = tags_txt } + ]=] elseif method == "POST" then + endpoints.edit_post(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")) @@ -1141,9 +1207,10 @@ function edit(req) http_response_header(req,"Location",loc) http_response(req,303,"") return + ]=] end - assert(ret) - http_response(req,200,ret) + --assert(ret) + --http_response(req,200,ret) end --TODO @@ -1163,6 +1230,8 @@ function teardown() end function download(req) + endpoints.download_get(req) + --[[ local host = http_request_get_host(req) local path = http_request_get_path(req) print("host:",host,"path:",path) @@ -1188,10 +1257,11 @@ function download(req) local nicetitle = title:gsub("%W","_") http_response_header(req,"Content-Disposition","attachment; filename=\"" .. nicetitle .. ".txt\"") http_response(req,200,text) + ]] end function preview(req) - preview_post(req) + endpoints.preview_post(req) --[[ print("We want to preview a paste!") local host = http_request_get_host(req) @@ -1220,6 +1290,8 @@ function preview(req) end function search(req) + endpoints.search_get(req) + --[=[ local host = http_request_get_host(req) local path = http_request_get_path(req) http_request_populate_qs(req) @@ -1259,6 +1331,7 @@ function search(req) } http_response(req,200,ret) end + ]=] end print("Done with init.lua") diff --git a/src/lua/login_get.lua b/src/lua/login_get.lua index 4ad9c90..e9d279d 100644 --- a/src/lua/login_get.lua +++ b/src/lua/login_get.lua @@ -1,11 +1,17 @@ local config = require("config") +local cache = require("cache") +local config = require("config") +local pages = require("pages") local function login_get(req) --Just give them the login page - return cache.render(string.format("%s/_login",domain),function() + local ret = cache.render(string.format("%s/_login",config.domain),function() return pages.login{ err = "", } end) + http_response(req,200,ret) end + +return login_get diff --git a/src/lua/login_post.lua b/src/lua/login_post.lua index d8cc902..46d7504 100644 --- a/src/lua/login_post.lua +++ b/src/lua/login_post.lua @@ -2,6 +2,9 @@ local sql = require("lsqlite3") local db = require("db") local util = require("util") +local session = require("session") +local config = require("config") +local pages = require("pages") local stmnt_author_acct @@ -12,7 +15,7 @@ function configure(...) SELECT id, salt, passhash FROM authors WHERE name = :name; ]])) - return configure(...) + return oldconfigure(...) end local function login_post(req) @@ -31,9 +34,9 @@ local function login_post(req) 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) + local mysession = session.start(id) + http_response_cookie(req,"session",mysession,"/",0,0) + local loc = string.format("https://%s.%s",name,config.domain) http_response_header(req,"Location",loc) http_response(req,303,"") return diff --git a/src/lua/paste_get.lua b/src/lua/paste_get.lua index 440f722..b371a0b 100644 --- a/src/lua/paste_get.lua +++ b/src/lua/paste_get.lua @@ -1,10 +1,13 @@ local config = require("config") +local session = require("session") +local pages = require("pages") +local cache = require("cache") local function paste_get(req) --Get the paste page local host = http_request_get_host(req) local text - local author,_ = get_session(req) + local author,_ = session.get(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,"") @@ -13,15 +16,21 @@ local function paste_get(req) text = cache.render(string.format("%s/_paste",host),function() print("Cache missing, rendering post page") return pages.paste{ - domain = domain, + domain = config.domain, err = "", } end) http_response(req,200,text) elseif host ~= config.domain and author then - + text = pages.author_paste{ + domain = config.domain, + user = author, + err = "", + text="", + } elseif host ~= config.domain and author == nil then - + http_response_header(req,"Location",string.format("https://%s/_paste",config.domain)) + http_response(req,303,"") else error(string.format( "Unable to find a good case for paste:%s,%s,%s", @@ -30,6 +39,9 @@ local function paste_get(req) author )) end + assert(text) + http_response(req,200,text) + --[=[ if host == config.domain then local author,_ = get_session(req) if author then @@ -51,7 +63,7 @@ local function paste_get(req) --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) + local author,_ = session.get(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 @@ -78,4 +90,7 @@ local function paste_get(req) err = "", } end + ]=] end + +return paste_get diff --git a/src/lua/paste_post.lua b/src/lua/paste_post.lua new file mode 100644 index 0000000..243cf6e --- /dev/null +++ b/src/lua/paste_post.lua @@ -0,0 +1,194 @@ +local sql = require("lsqlite3") +local zlib = require("zlib") + +local util = require("util") +local parsers = require("parsers") +local config = require("config") +local queries = require("queries") +local db = require("db") +local cache = require("cache") +local tags = require("tags") +local session = require("session") + +local stmnt_raw,stmnt_paste + +local oldconfigure = configure +function configure(...) + stmnt_paste = assert(db.conn:prepare(queries.insert_post)) + stmnt_raw = assert(db.conn:prepare(queries.insert_raw)) + return oldconfigure(...) +end + +local function anon_paste(req,ps) + --Public paste + --[[ + This doesn't actually do much for IPv4 addresses, + since there are only 32 bits of address. Someone who + got a copy of the database could + just generate all 2^32 hashes and look up who posted + what. Use IPv6, Tor or I2P where possible. (but then I + guess it's harder to ban spammers... hmm..) + ]] + --local ip = http_request_get_ip(req) + --local iphash = sha3(ip) + --Don't store this information for now, until I come up + --with a more elegent solution. + + util.sqlbind(stmnt_paste,"bind_blob",1,ps.text) + --assert(stmnt_paste:bind_blob(1,text) == sql.OK) + util.sqlbind(stmnt_paste,"bind",2,ps.title) + --assert(stmnt_paste:bind(2,esctitle) == sql.OK) + util.sqlbind(stmnt_paste,"bind",3,-1) + --assert(stmnt_paste:bind(3,-1) == sql.OK) + util.sqlbind(stmnt_paste,"bind",4,true) + --assert(stmnt_paste:bind(4,true) == sql.OK) + util.sqlbind(stmnt_paste,"bind_blob",5,"") + --assert(stmnt_paste:bind_blob(5,"") == sql.OK) + err = util.do_sql(stmnt_paste) + stmnt_paste:reset() + if err == sql.DONE then + local rowid = stmnt_paste:last_insert_rowid() + assert(stmnt_raw:bind(1,rowid) == sql.OK) + assert(stmnt_raw:bind_blob(2,ps.raw) == sql.OK) + assert(stmnt_raw:bind(3,ps.markup) == sql.OK) + err = util.do_sql(stmnt_raw) + stmnt_raw:reset() + if err ~= sql.DONE then + print("Failed to save raw text, but paste still went though") + end + tags.set(rowid,ps.tags) + --[[ + for _,tag in pairs(ps.tags) do + print("tag 1:",stmnt_ins_tag:bind(1,rowid)) + print("Looking at tag",tag) + print("tag 2:",stmnt_ins_tag:bind(2,tag)) + err = util.do_sql(stmnt_ins_tag) + stmnt_ins_tag:reset() + end + ]] + local url = util.encode_id(rowid) + local loc = string.format("https://%s/%s",config.domain,url) + http_response_header(req,"Location",loc) + http_response(req,303,"") + cache.dirty(string.format("%s/%s",config.domain,url)) + cache.dirty(string.format("%s",config.domain)) + return + elseif err == sql.ERROR or err == sql.MISUSE then + ret = "Failed to paste: " .. tostring(err) + else + error("Error pasting:" .. tostring(err)) + end + stmnt_paste:reset() +end +local function author_paste(req,ps) + --Author paste + local author, authorid = session.get(req) + if author == nil then + ret = pages.author_paste{ + domain = domain, + author = subdomain, + err = "You are not logged in, you must be logged in to post as " .. subdomain .. ".", + text = text + } + end + local asanon = assert(http_argument_get_string(req,"pasteas")) + --No need to check if the author is posting to the + --"right" sudomain, just post it to the one they have + --the session key for. + assert(stmnt_paste:bind_blob(1,ps.text) == sql.OK) + assert(stmnt_paste:bind(2,ps.title) == sql.OK) + assert(stmnt_paste:bind(3,authorid) == sql.OK) + if asanon == "anonymous" then + assert(stmnt_paste:bind(4,true) == sql.OK) + else + assert(stmnt_paste:bind(4,false) == sql.OK) + end + assert(stmnt_paste:bind_blob(5,"") == sql.OK) + err = util.do_sql(stmnt_paste) + stmnt_paste:reset() + if err == sql.DONE then + local rowid = stmnt_paste:last_insert_rowid() + assert(stmnt_raw:bind(1,rowid) == sql.OK) + assert(stmnt_raw:bind_blob(2,ps.raw) == sql.OK) + assert(stmnt_raw:bind(3,ps.markup) == sql.OK) + err = util.do_sql(stmnt_raw) + stmnt_raw:reset() + if err ~= sql.DONE then + print("Failed to save raw text, but paste still went through") + end + tags.set(rowid,ps.tags) + --[[ + for _,tag in pairs(ps.tags) do + print("tag 1:",stmnt_ins_tag:bind(1,rowid)) + print("Looking at tag",tag) + print("tag 2:",stmnt_ins_tag:bind(2,tag)) + err = do_sql(stmnt_ins_tag) + stmnt_ins_tag:reset() + end + ]] + local url = util.encode_id(rowid) + local loc + if asanon == "anonymous" then + loc = string.format("https://%s/%s",config.domain,url) + else + loc = string.format("https://%s.%s/%s",author,config.domain,url) + end + http_response_header(req,"Location",loc) + http_response(req,303,"") + cache.dirty(string.format("%s.%s",author,config.domain)) + cache.dirty(string.format("%s/%s",config.domain,url)) + cache.dirty(string.format("%s",config.domain)) + return + elseif err == sql.ERROR or err == sql.MISUSE then + ret = "Failed to paste: " .. tostring(err) + else + error("Error pasting:",err) + end + stmnt_paste:reset() + +end +local function decodeentities(capture) + local n = tonumber(capture,16) + local c = string.char(n) + if escapes[c] then + return escapes[c] + else + return c + end +end +local function paste_post(req) + local host = http_request_get_host(req) + local path = http_request_get_path(req) + + local ps = {} + --We're creatinga new paste + http_request_populate_post(req) + local title = assert(http_argument_get_string(req,"title")) + local text = assert(http_argument_get_string(req,"text")) + ps.markup = assert(http_argument_get_string(req,"markup")) + local tag_str = http_argument_get_string(req,"tags") + ps.tags = {} + if tag_str then + ps.tags = util.parse_tags(tag_str) + end + local pasteas + ps.raw = zlib.compress(text) + text = string.gsub(text,"%%(%x%x)",decodeentities) + text = parsers[ps.markup](text) + assert(text,"Failed to parse text") + text = zlib.compress(text) + assert(text,"Failed to compress text") + ps.text = text + local esctitle = string.gsub(title,"%%(%x%x)",decodeentities) + --Always sanatize the title with the plain parser. no markup + --in the title. + ps.title = parsers.plain(title) + if host == config.domain then + anon_paste(req,ps) + else + author_paste(req,ps) + end +end +--assert(ret) +--http_response(req,200,ret) +return paste_post diff --git a/src/lua/preview_post.lua b/src/lua/preview_post.lua index b7886be..e3cb8da 100644 --- a/src/lua/preview_post.lua +++ b/src/lua/preview_post.lua @@ -2,6 +2,7 @@ local parsers = require("parsers") local tags = require("tags") local util = require("util") local pages = require("pages") +local config = require("config") local function preview_post(req) print("We want to preview a paste!") @@ -19,7 +20,7 @@ local function preview_post(req) print("title:",title,"text:",text,"markup:",markup) local parsed = parsers[markup](text) local ret = pages.read{ - domain = domain, + domain = config.domain, title = title, author = "preview", idp = "preview", diff --git a/src/lua/read_get.lua b/src/lua/read_get.lua index 2fbed09..3874b9a 100644 --- a/src/lua/read_get.lua +++ b/src/lua/read_get.lua @@ -4,6 +4,10 @@ local session = require("session") local tags = require("tags") local db = require("db") local queries = require("queries") +local util = require("util") +local cache = require("cache") +local pages = require("pages") +local config = require("config") local stmnt_read, stmnt_update_views, stmnt_comments @@ -12,7 +16,7 @@ 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(...) + return oldconfigure(...) end @@ -23,7 +27,7 @@ local function add_view(storyid) stmnt_update_views:bind_names{ id = storyid } - local err = do_sql(stmnt_update_views) + local err = util.do_sql(stmnt_update_views) assert(err == sql.DONE, "Failed to update view counter:"..tostring(err)) stmnt_update_views:reset() end @@ -37,28 +41,28 @@ local function populate_ps_story(req,ps) stmnt_read:bind_names{ id = ps.storyid } - local err = do_sql(stmnt_read) + local err = util.do_sql(stmnt_read) if err == sql.DONE then --We got no story stmnt_read:reset() - return nil + error("No story by this name",ps.storyid) + return 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.text = zlib.decompress(storytext) ps.tauthor = tauthor ps.isanon = isanon == 1 - ps.authorname = authorname + ps.author = authorname ps.views = views stmnt_read:reset() --Tags - ps.tags = tags.get(id) + ps.tags = tags.get(ps.storyid) return true end @@ -66,7 +70,22 @@ end Get the comments for a story ]] local function get_comments(req,ps) - + stmnt_comments:bind_names{ + id = ps.storyid + } + err = util.do_sql(stmnt_comments) + local comments = {} + while err ~= sql.DONE do + local com_author, com_isanon, com_text = unpack(stmnt_comments:get_values()) + table.insert(comments,{ + author = com_author, + isanon = com_isanon == 1, --int to boolean + text = com_text + }) + err = stmnt_comments:step() + end + stmnt_comments:reset() + return comments end --[[ @@ -92,21 +111,25 @@ end local function read_get(req) --Pages settings local ps = { + domain = config.domain, host = http_request_get_host(req), path = http_request_get_path(req), method = http_method_text(req), } + print("reading", ps.path) --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) + ps.idp = string.sub(ps.path,2)--remove leading "/" + ps.storyid = util.decode_id(ps.idp) + add_view(ps.storyid) --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 + ps.loggedauthor = author + ps.iam = author + ps.loggedauthorid = authorid end --If we need to show comments @@ -116,20 +139,26 @@ local function read_get(req) ps.comments = get_comments(req,ps) end + local text --normal story display - if (not ps.author) then + if (not ps.loggedauthor) then + print("not author") local cachestr = string.format("%s%s%s", ps.host, ps.path, ps.show_comments and "?comments=1" or "" ) - local text = cache.render(cachestr,function() + text = cache.render(cachestr,function() populate_ps_story(req,ps) - return pages.read(ps) + local output = pages.read(ps) + assert(output,"failed to read page:" .. cachestr) + return output end) else --we are logged in, don't cache + print("is author") populate_ps_story(req,ps) - ps.owner = (ps.author == ps.tauthor) + print("tauthor was", ps.tauthor, "while author was:",ps.author) + ps.owner = (ps.loggedauthorid == ps.tauthor) text = pages.read(ps) end assert(text) diff --git a/src/lua/read_post.lua b/src/lua/read_post.lua index 81907da..2fde5ef 100644 --- a/src/lua/read_post.lua +++ b/src/lua/read_post.lua @@ -44,7 +44,7 @@ local function read_post(req) 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) + local redir = string.format("https://%s%s?comments=1", config.domain, path) http_response_header(req,"Location",redir) http_response(req,303,"") end diff --git a/src/lua/search_get.lua b/src/lua/search_get.lua new file mode 100644 index 0000000..4c340a3 --- /dev/null +++ b/src/lua/search_get.lua @@ -0,0 +1,59 @@ +local sql = require("lsqlite3") + +local db = require("db") +local queries = require("queries") +local util = require("util") +local libtags = require("tags") +local pages = require("pages") +local config = require("config") + +local stmnt_search +local oldconfigure = configure +function configure(...) + stmnt_search = assert(db.conn:prepare(queries.select_post_tags)) + return oldconfigure(...) +end + +local function search_get(req) + local host = http_request_get_host(req) + local path = http_request_get_path(req) + http_request_populate_qs(req) + local tag = http_argument_get_string(req,"tag") + if tag then + stmnt_search:bind_names{ + tag = tag + } + local results = {} + local err + repeat + err = stmnt_search:step() + if err == sql.BUSY then + coroutine.yield() + elseif err == sql.ROW then + local id, title, anon, time, author = unpack(stmnt_search:get_values()) + local idp = util.encode_id(id) + local tags = libtags.get(id) + table.insert(results,{ + id = idp, + title = title, + anon = anon, + time = os.date("%B %d %Y",tonumber(time)), + author = author, + tags = tags + }) + elseif err == sql.DONE then + stmnt_search:reset() + else + error("Failed to search, sql error:" .. tostring(err)) + end + until err == sql.DONE + local ret = pages.search{ + domain = config.domain, + results = results, + tag = tag, + } + http_response(req,200,ret) + end +end + +return search_get diff --git a/src/lua/session.lua b/src/lua/session.lua index 7912d01..b6a5c4a 100644 --- a/src/lua/session.lua +++ b/src/lua/session.lua @@ -2,12 +2,13 @@ local sql = require("lsqlite3") local db = require("db") local util = require("util") +local queries = require("queries") local oldconfigure = configure local stmnt_get_session, stmnt_insert_session function configure(...) - stmnt_get_session = assert(db:prepare(queries.select_valid_sessions)) - + stmnt_get_session = assert(db.conn:prepare(queries.select_valid_sessions)) + stmnt_insert_session = assert(db.conn:prepare(queries.insert_session)) return oldconfigure(...) end @@ -41,7 +42,7 @@ end --[[ Start a session for someone who logged in ]] -local function start_session(who) +function session.start(who) local rngf = assert(io.open("/dev/urandom","rb")) local session_t = {} for i = 1,64 do diff --git a/src/lua/tags.lua b/src/lua/tags.lua index 9612c66..9ee026b 100644 --- a/src/lua/tags.lua +++ b/src/lua/tags.lua @@ -1,3 +1,5 @@ +local sql = require("lsqlite3") + local db = require("db") local queries = require("queries") local util = require("util") @@ -12,7 +14,7 @@ function configure(...) stmnt_get_tags = assert(db.conn:prepare(queries.select_tags)) stmnt_drop_tags = assert(db.conn:prepare(queries.delete_tags)) - return configure(...) + return oldconfigure(...) end @@ -41,6 +43,7 @@ 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() + local err for _,tag in pairs(tags) do print("Looking at tag",tag) assert(stmnt_ins_tag:bind(1,storyid) == sql.OK) @@ -48,6 +51,9 @@ function tags.set(storyid,tags) err = util.do_sql(stmnt_ins_tag) stmnt_ins_tag:reset() end + if err ~= sql.DONE then + print("Failed to save tags, but paste and raw still went through") + end end diff --git a/src/lua/util.lua b/src/lua/util.lua index a798141..32da51f 100644 --- a/src/lua/util.lua +++ b/src/lua/util.lua @@ -1,4 +1,6 @@ +local sql = require("lsqlite3") + local util = {} --[[ @@ -19,16 +21,34 @@ 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 + local i = 0 repeat err = stmnt:step() print("After stepping, err is", err) if err == sql.BUSY then + i = i + 1 coroutine.yield() end - until(err ~= sql.BUSY) + until(err ~= sql.BUSY or i > 10) + assert(i < 10, "Database busy") return err end +--[[ +Binds an argument to as statement with nice error reporting on failure +stmnt :: sql.stmnt - the prepared sql statemnet +call :: string - a string "bind" or "bind_blob" +position :: number - the argument position to bind to +data :: string - The data to bind +]] +function util.sqlbind(stmnt,call,position,data) + assert(call == "bind" or call == "bind_blob","Bad bind call, call was:" .. call) + local f = stmnt[call](stmnt,position,data) + if f ~= sql.OK then + error(string.format("Failed to %s at %d with %q: %s", call, position, data, db:errmsg()),2) + end +end + --see https://perishablepress.com/stop-using-unsafe-characters-in-urls/ --no underscore because we use that for our operative pages local url_characters = diff --git a/src/pages/read.etlua b/src/pages/read.etlua index e1d3fa7..5224b19 100644 --- a/src/pages/read.etlua +++ b/src/pages/read.etlua @@ -22,7 +22,7 @@ <%- title %>

- <% if isanon then -%> + <% if isanon or author == nil then -%> By Anonymous <% else -%> By <%= author %> diff --git a/src/smr.c b/src/smr.c index 3e14a5c..c1486e9 100644 --- a/src/smr.c +++ b/src/smr.c @@ -45,18 +45,31 @@ KORE_SECCOMP_FILTER("app", KORE_SYSCALL_ALLOW(pwrite64), KORE_SYSCALL_ALLOW(fdatasync), KORE_SYSCALL_ALLOW(unlinkat), - KORE_SYSCALL_ALLOW(mremap) + KORE_SYSCALL_ALLOW(mremap), + KORE_SYSCALL_ALLOW(newfstatat) ); int errhandeler(lua_State *L){ - printf("Error: %s\n",lua_tostring(L,1)); - lua_getglobal(L,"debug"); - lua_getglobal(L,"print"); - lua_getfield(L,-2,"traceback"); - lua_call(L,0,1); - lua_call(L,1,0); - lua_pop(L,1); + printf("Error: %s\n",lua_tostring(L,1));//"error" + lua_getglobal(L,"debug");//"error",{debug} + lua_getglobal(L,"print");//"error",{debug},print() + lua_getfield(L,-2,"traceback");//"error",{debug},print(),traceback() + lua_call(L,0,1);//"error",{debug},print(),"traceback" + lua_call(L,1,0);//"error",{debug} + printf("Called print()\n"); + lua_getfield(L,-1,"traceback");//"error",{debug},traceback() + printf("got traceback\n"); + lua_call(L,0,1);//"error",{debug},"traceback" + lua_pushstring(L,"\n"); + printf("called traceback\n"); + lua_pushvalue(L,-4);//"error",{debug},"traceback","error" + printf("pushed error\n"); + lua_concat(L,3);//"error",{debug},"traceback .. error" + printf("concated\n"); + int ref = luaL_ref(L,LUA_REGISTRYINDEX);//"error",{debug} + lua_pop(L,2);// + lua_rawgeti(L,LUA_REGISTRYINDEX,ref);//"traceback .. error" return 1; } From a45d0787a253492149e17fb4efb952958d292c1f Mon Sep 17 00:00:00 2001 From: Robin Malley Date: Mon, 21 Dec 2020 23:32:29 +0000 Subject: [PATCH 3/3] Organize lua endpoints Move all endpoints under the src/lua/endpoints folder, and delete the previous location. --- Makefile | 1 + src/lua/claim_get.lua | 16 - src/lua/claim_post.lua | 74 -- src/lua/download_get.lua | 45 -- src/lua/edit_get.lua | 68 -- src/lua/edit_post.lua | 89 --- src/lua/endpoints/read_get.lua | 12 +- src/lua/index_get.lua | 115 --- src/lua/init.lua | 1207 +------------------------------- src/lua/login_get.lua | 17 - src/lua/login_post.lua | 61 -- src/lua/parser_search.lua | 8 + src/lua/paste_get.lua | 96 --- src/lua/paste_post.lua | 194 ----- src/lua/preview_post.lua | 33 - src/lua/read_get.lua | 169 ----- src/lua/read_post.lua | 53 -- src/lua/render.lua | 2 - src/lua/search_get.lua | 59 -- src/pages/index.etlua | 3 + src/pages/read.etlua | 2 +- 21 files changed, 25 insertions(+), 2299 deletions(-) delete mode 100644 src/lua/claim_get.lua delete mode 100644 src/lua/claim_post.lua delete mode 100644 src/lua/download_get.lua delete mode 100644 src/lua/edit_get.lua delete mode 100644 src/lua/edit_post.lua delete mode 100644 src/lua/index_get.lua delete mode 100644 src/lua/login_get.lua delete mode 100644 src/lua/login_post.lua create mode 100644 src/lua/parser_search.lua delete mode 100644 src/lua/paste_get.lua delete mode 100644 src/lua/paste_post.lua delete mode 100644 src/lua/preview_post.lua delete mode 100644 src/lua/read_get.lua delete mode 100644 src/lua/read_post.lua delete mode 100644 src/lua/render.lua delete mode 100644 src/lua/search_get.lua diff --git a/Makefile b/Makefile index 7f8ed35..668d388 100644 --- a/Makefile +++ b/Makefile @@ -34,6 +34,7 @@ $(chroot_dir): apk-tools-static-$(version).apk mkdir -p $(chroot_dir)/pages mkdir -p $(chroot_dir)/sql mkdir -p $(chroot_dir)/data + mkdir -p $(chroot_dir)/endpoints #cd $(chroot_dir) && tar -xvzf ../apk-tools-static-*.apk #cd $(chroot_dir) && sudo ./sbin/apk.static -X $(mirror)latest-stable/main -U --allow-untrusted --root $(chroot_dir) --no-cache --initdb add alpine-base #ln -s /dev/urandom $(chroot_dir)/dev/random #Prevent an attacker with access to the chroot from exhausting our entropy pool and causing a dos diff --git a/src/lua/claim_get.lua b/src/lua/claim_get.lua deleted file mode 100644 index cd13079..0000000 --- a/src/lua/claim_get.lua +++ /dev/null @@ -1,16 +0,0 @@ -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 diff --git a/src/lua/claim_post.lua b/src/lua/claim_post.lua deleted file mode 100644 index ae4d149..0000000 --- a/src/lua/claim_post.lua +++ /dev/null @@ -1,74 +0,0 @@ -local sql = require("lsqlite3") - -local pages = require("pages") -local db = require("db") -local queries = require("queries") -local util = require("util") -local sessionlib = require("session") -local config = require("config") - -local stmnt_author_create - -local oldconfigure = configure -function configure(...) - - stmnt_author_create = util.sqlassert(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 = util.do_sql(stmnt_author_create) - if err == sql.DONE then - print("success") - --We sucessfully made the 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 .. "." .. config.domain .. ".passfile\"") - local session = sessionlib.start(id) - text = password - print("session started, about to send password:",text) - http_response(req,200,text) - return - 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() - http_response(req,200,text) -end -return claim_post diff --git a/src/lua/download_get.lua b/src/lua/download_get.lua deleted file mode 100644 index 48f4593..0000000 --- a/src/lua/download_get.lua +++ /dev/null @@ -1,45 +0,0 @@ -local sql = require("lsqlite3") -local zlib = require("zlib") - -local db = require("db") -local queries = require("queries") -local util = require("util") -local pages = require("pages") - -local stmnt_download -local oldconfigure = configure -function configure(...) - stmnt_download = assert(db.conn:prepare(queries.select_download)) - return oldconfigure(...) -end - -local function download_get(req) - local host = http_request_get_host(req) - local path = http_request_get_path(req) - print("host:",host,"path:",path) - http_request_populate_qs(req) - local story = assert(http_argument_get_string(req,"story")) - local story_id = util.decode_id(story) - print("Downloading", story_id) - stmnt_download:bind_names{ - postid = story_id - } - local err = util.do_sql(stmnt_download) - if err == sql.DONE then - --No rows, story not found - http_responose(req,404,pages.nostory{path=story}) - stmnt_download:reset() - return - 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 text = zlib.decompress(txt_compressed) - stmnt_download:reset() - http_response_header(req,"Content-Type","application/octet-stream") - local nicetitle = title:gsub("%W","_") - http_response_header(req,"Content-Disposition","attachment; filename=\"" .. nicetitle .. ".txt\"") - http_response(req,200,text) - -end - -return download_get diff --git a/src/lua/edit_get.lua b/src/lua/edit_get.lua deleted file mode 100644 index 1881a58..0000000 --- a/src/lua/edit_get.lua +++ /dev/null @@ -1,68 +0,0 @@ -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 session = require("session") -local config = require("config") - -local stmnt_edit -local oldconfigure = configure -function configure(...) - stmnt_edit = assert(db.conn:prepare(queries.select_edit)) - return oldconfigure(...) -end - -local function edit_get(req) - local host = http_request_get_host(req) - local path = http_request_get_path(req) - local author, authorid = session.get(req) - - http_request_populate_qs(req) - local story = assert(http_argument_get_string(req,"story")) - local story_id = util.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 = authorid - } - 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 = config.domain, - story = story_id, - err = "", - tags = tags_txt - } - http_response(req,200,ret) -end - -return edit_get diff --git a/src/lua/edit_post.lua b/src/lua/edit_post.lua deleted file mode 100644 index 0e612d8..0000000 --- a/src/lua/edit_post.lua +++ /dev/null @@ -1,89 +0,0 @@ -local sql = require("lsqlite3") -local zlib = require("zlib") - -local db = require("db") -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 session = require("session") - -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 = session.get(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 = util.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(util.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 - -return edit_post diff --git a/src/lua/endpoints/read_get.lua b/src/lua/endpoints/read_get.lua index 3874b9a..1b5d4e7 100644 --- a/src/lua/endpoints/read_get.lua +++ b/src/lua/endpoints/read_get.lua @@ -45,8 +45,8 @@ local function populate_ps_story(req,ps) if err == sql.DONE then --We got no story stmnt_read:reset() - error("No story by this name",ps.storyid) - return + print("No story by this name",ps.storyid) + return false end --If we've made it here, we have a story. Populate our settings --with title, text, ect. @@ -149,14 +149,18 @@ local function read_get(req) ps.show_comments and "?comments=1" or "" ) text = cache.render(cachestr,function() - populate_ps_story(req,ps) + if not populate_ps_story(req,ps) then + return pages.nostory(ps) + end local output = pages.read(ps) assert(output,"failed to read page:" .. cachestr) return output end) else --we are logged in, don't cache print("is author") - populate_ps_story(req,ps) + if not populate_ps_story(req,ps) then + return pages.nostory(ps) + end print("tauthor was", ps.tauthor, "while author was:",ps.author) ps.owner = (ps.loggedauthorid == ps.tauthor) text = pages.read(ps) diff --git a/src/lua/index_get.lua b/src/lua/index_get.lua deleted file mode 100644 index 104d09b..0000000 --- a/src/lua/index_get.lua +++ /dev/null @@ -1,115 +0,0 @@ -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 libtags = require("tags") - -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 oldconfigure(...) -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 = libtags.get(data[1]) - table.insert(latest,{ - url = util.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 = util.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 = util.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 = libtags.get(id) - table.insert(stories,{ - url = util.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 - assert(text) - http_response(req,200,text) -end - -return index_get diff --git a/src/lua/init.lua b/src/lua/init.lua index ec457e1..22983dd 100644 --- a/src/lua/init.lua +++ b/src/lua/init.lua @@ -15,16 +15,8 @@ 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 -end ---[[ -local parser_names = {"plain","imageboard"} -local parsers = {} -for _,v in pairs(parser_names) do - parsers[v] = require("parser_" .. v) -end -]] + +--Pages local endpoint_names = { read = {"get","post"}, preview = {"post"}, @@ -43,118 +35,10 @@ for name, methods in pairs(endpoint_names) do endpoints[epn] = require("endpoints." .. epn) end end ---pages -read_get = require("read_get") -read_post = require("read_post") -preview_post = require("preview_post") -index_get = require("index_get") -paste_get = require("paste_get") -paste_post = require("paste_post") -download_get = require("download_get") -login_get = require("login_get") -login_post = require("login_post") -edit_get = require("edit_get") -edit_post = require("edit_post") -claim_get = require("claim_get") -claim_post = require("claim_post") -search_get = require("search_get") ---local db,cache --databases ---local domain = "test.monster:8888" --The domain to write links as ---[[ -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 -]] - ---[=[ -local queries = {} ---These are all loaded during startup, won't affect ongoing performance. -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 -}) -]=] ----sql queries -local --[[stmnt_index,]] stmnt_author_index, stmnt_read, stmnt_paste, stmnt_raw -local stmnt_update_views -local stmnt_ins_tag, stmnt_drop_tags, stmnt_get_tags -local stmnt_author_create, stmnt_author_acct, stmnt_author_bio ---local stmnt_cache, stmnt_insert_cache, stmnt_dirty_cache -local stmnt_get_session, stmnt_insert_session -local stmnt_edit, stmnt_update, stmnt_update_raw, stmnt_author_of -local stmnt_comments, stmnt_comment_insert -local stmnt_search ---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 -local function decodeentities(capture) - local n = tonumber(capture,16) - local c = string.char(n) - if escapes[c] then - return escapes[c] - else - return c - end -end - ---[[ -local function sqlassert(...) - local r,errcode,err = ... - if not r then - error(string.format("%d: %s",errcode, err)) - end - return r -end -]] - -local function sqlbind(stmnt,call,position,data) - assert(call == "bind" or call == "bind_blob","Bad bind call, call was:" .. call) - local f = stmnt[call](stmnt,position,data) - if f ~= sql.OK then - error(string.format("Failed to %s at %d with %q: %s", call, position, data, db:errmsg()),2) - end -end - - print("Hello from init.lua") local oldconfigure = configure function configure(...) - --db = sqlassert(sql.open("data/posts.db")) - ----db = sqlassert(sql.open_memory()) - --cache = sqlassert(sql.open_memory()) - --Test that compression works. For some reason, the zlib library --fails if this is done as a one-liner local msg = "test message" @@ -162,404 +46,14 @@ function configure(...) local two = zlib.decompress(one) assert(two == msg, "zlib not working as expected") oldconfigure(...) - --Create sql tables - --assert(db:exec(queries.create_table_authors)) - --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. - --assert(db: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: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:exec(queries.create_table_raw_text)) - --assert(db:exec(queries.create_table_images)) - --assert(db:exec(queries.create_table_comments)) - --assert(db:exec(queries.create_table_tags)) - --assert(db:exec(queries.create_index_tags)) - --Store a cookie for logged in users. Logged in users can edit - --their own posts. - --assert(db:exec(queries.create_table_session)) - --print("Created db tables") - - --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 - ); - ]])) - ]=] - - --Select the data we need to display the on the front page - --stmnt_index = assert(db:prepare(queries.select_site_index)) - --Select the data we need to read a story (and maybe display an edit - --button - --stmnt_read = assert(db:prepare(queries.select_post)) - --Update the view counter when someone reads a story - --stmnt_update_views = assert(db:prepare(queries.update_views)) - --Retreive comments on a story - --stmnt_comments = assert(db:prepare(queries.select_comments)) - --Add a new comment to a story - --stmnt_comment_insert = assert(db:prepare(queries.insert_comment)) - --TODO: actually let authors edit their bio - --[=[stmnt_author_bio = assert(db.conn:prepare([[ - SELECT authors.biography FROM authors WHERE authors.name = :author; - ]])) - ]=] - --Get the author of a story, used to check when editing that the - --author really owns the story they're trying to edit - --stmnt_author_of = assert(db:prepare(queries.select_author_of_post)) - --Get the data we need to display a particular author's latest - --stories - --stmnt_author = assert(db:prepare(queries.select_author_index)) - --Get the data we need to check if someone can log in - --[=[ - stmnt_author_acct = assert(db:prepare([[ - SELECT id, salt, passhash FROM authors WHERE name = :name; - ]])) - ]=] - --Create a new author on the site - --stmnt_author_create = assert(db:prepare(queries.insert_author)) - --[=[ - stmnt_author_login = assert(db:prepare([[ - SELECT name, passhash FROM authors WHERE name = :name; - ]])) - ]=] - --Create a new 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 - --It might also be useful for migrations, if that ever needs to happen - --stmnt_raw = assert(db:prepare(queries.insert_raw)) - --Tags for a story - --[[ - stmnt_ins_tag = assert(db:prepare(queries.insert_tag)) - stmnt_get_tags = assert(db:prepare(queries.select_tags)) - stmnt_drop_tags = assert(db:prepare(queries.delete_tags)) - ]] - --Get the data we need to display the edit screen - --stmnt_edit = assert(db:prepare(queries.select_edit)) - --Get the data we need when someone wants to download a paste - --stmnt_download = assert(db:prepare(queries.select_download)) - --When we update a post, store the plaintext again - --stmnt_update_raw = assert(db:prepare(queries.update_raw)) - --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. - --If it gets abused I can disable it I guess. - --stmnt_update = assert(db:prepare(queries.update_post)) - --Check sessions for login support - --stmnt_insert_session = assert(db:prepare(queries.insert_session)) - --stmnt_get_session = assert(db:prepare(queries.select_valid_sessions)) - --Search by tag name - --stmnt_search = assert(db:prepare(queries.select_post_tags)) - --only refresh pages at most once every 10 seconds - --[=[ - 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; - ]]) - ]=] - --[=[ - ]=] - print("finished running configure()") end print("Created configure function") ---[[ -find a string url for a number -]] - -local function 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 - -local function 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 - -local function 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 - -local function get_tags(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 - ---[=[ -local function dirty_cache(url) - print("Dirtying cache:",url) - stmnt_dirty_cache:bind_names{ - path = url - } - err = do_sql(stmnt_dirty_cache) - stmnt_dirty_cache:reset() -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 = do_sql(stmnt_insert_session) - stmnt_insert_session:reset() - print("Err:",err) - assert(err == sql.DONE) - return session -end ---[[ -Retreive the name and authorid of the logged in person, -or nil+error message if not logged in -]] -local function get_session(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 = 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 - ---[=[ ---Render a page, with cacheing. If you need to dirty a cache, call dirty_cache() -local function render(pagename,callback) - print("Running render...") - stmnt_cache:bind_names{path=pagename} - local err = 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 = 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 -]=] - ---[=[ ---[[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 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,"^%w",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 -]=] - function home(req) - print("Hello from lua!") - print("Method:", http_method_text(req)) local method = http_method_text(req) if method == "GET" then endpoints.index_get(req) end - --[=[ - local host = http_request_get_host(req) - local path = http_request_get_path(req) - local text - if host == domain then - --Default home page - text = cache.render(string.format("%s",domain),function() - print("Cache miss, rendering index") - stmnt_index:bind_names{} - local err = 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 tags = get_tags(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 = tags, - }) - err = stmnt_index:step() - end - stmnt_index:reset() - return pages.index{ - domain = domain, - stories = latest - } - end) - else - --Home page for an author - local subdomain = host:match("([^\\.]+)") - text = cache.render(string.format("%s.%s",subdomain,domain),function() - print("Cache miss, rendering author:" .. subdomain) - 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=domain, - author=subdomain, - stories=stories, - bio=bio - } - end) - end - assert(text) - http_response(req,200,text) - ]=] end --We prevent people from changing their password file, this way we don't really @@ -568,654 +62,54 @@ end --a while, but whatever. function claim(req) local method = http_method_text(req) - --[[ - local host = http_request_get_host(req) - local path = http_request_get_path(req) - if host ~= domain then - http_response_header(req,"Location",string.format("https://%s/_claim",domain)) - http_response(req,303,"") - return - end - assert(host == domain) - local text - ]] if method == "GET" then endpoints.claim_get(req) - --[=[ - --Get the page to claim a name - text = cache.render(string.format("%s/_claim",domain),function() - print("cache miss, rendering claim page") - return pages.claim{err=""} - end) - ]=] elseif method == "POST" then endpoints.claim_post(req) - --[=[ - --Actually claim a name - http_request_populate_post(req) - local name = assert(http_argument_get_string(req,"user")) - --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 - --assert(text) - --http_response(req,200,text) end +--Create a new paste on the site function paste(req) - --local host = http_request_get_host(req) - --local path = http_request_get_path(req) local method = http_method_text(req) - --local err - --local ret if method == "GET" then endpoints.paste_get(req) - --[=[ - --Get the paste page - if host == 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 - ]=] elseif method == "POST" then endpoints.paste_post(req) - --[=[ - --We're creatinga new paste - 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 = parse_tags(tag_str) - end - local pasteas - local raw = zlib.compress(text) - text = string.gsub(text,"%%(%x%x)",decodeentities) - text = parsers[markup](text) - text = zlib.compress(text) - local esctitle = string.gsub(title,"%%(%x%x)",decodeentities) - --Always sanatize the title with the plain parser. no markup - --in the title. - esctitle = parsers.plain(title) - if host == domain then - --Public paste - --[[ - This doesn't actually do much for IPv4 addresses, - since there are only 32 bits of address. Someone who - got a copy of the database could - just generate all 2^32 hashes and look up who posted - what. Use IPv6, Tor or I2P where possible. (but then I - guess it's harder to ban spammers... hmm..) - ]] - --local ip = http_request_get_ip(req) - --local iphash = sha3(ip) - --Don't store this information for now, until I come up - --with a more elegent solution. - - sqlbind(stmnt_paste,"bind_blob",1,text) - --assert(stmnt_paste:bind_blob(1,text) == sql.OK) - sqlbind(stmnt_paste,"bind",2,esctitle) - --assert(stmnt_paste:bind(2,esctitle) == sql.OK) - sqlbind(stmnt_paste,"bind",3,-1) - --assert(stmnt_paste:bind(3,-1) == sql.OK) - sqlbind(stmnt_paste,"bind",4,true) - --assert(stmnt_paste:bind(4,true) == sql.OK) - sqlbind(stmnt_paste,"bind_blob",5,"") - --assert(stmnt_paste:bind_blob(5,"") == sql.OK) - err = do_sql(stmnt_paste) - stmnt_paste:reset() - if err == sql.DONE then - local rowid = stmnt_paste:last_insert_rowid() - assert(stmnt_raw:bind(1,rowid) == sql.OK) - assert(stmnt_raw:bind_blob(2,raw) == sql.OK) - assert(stmnt_raw:bind(3,markup) == sql.OK) - err = do_sql(stmnt_raw) - stmnt_raw:reset() - if err ~= sql.DONE then - print("Failed to save raw text, but paste still went though") - end - for _,tag in pairs(tags) do - print("tag 1:",stmnt_ins_tag:bind(1,rowid)) - print("Looking at tag",tag) - print("tag 2:",stmnt_ins_tag:bind(2,tag)) - err = do_sql(stmnt_ins_tag) - stmnt_ins_tag:reset() - end - local url = encode_id(rowid) - local loc = string.format("https://%s/%s",domain,url) - http_response_header(req,"Location",loc) - http_response(req,303,"") - cache.dirty(string.format("%s/%s",domain,url)) - cache.dirty(string.format("%s",domain)) - return - elseif err == sql.ERROR or err == sql.MISUSE then - ret = "Failed to paste: " .. tostring(err) - else - error("Error pasting:" .. tostring(err)) - end - stmnt_paste:reset() - - else - --Author paste - local author, authorid = get_session(req) - if author == nil then - ret = pages.author_paste{ - domain = domain, - author = subdomain, - err = "You are not logged in, you must be logged in to post as " .. subdomain .. ".", - text = text - } - end - local asanon = assert(http_argument_get_string(req,"pasteas")) - --No need to check if the author is posting to the - --"right" sudomain, just post it to the one they have - --the session key for. - assert(stmnt_paste:bind_blob(1,text) == sql.OK) - assert(stmnt_paste:bind(2,esctitle) == sql.OK) - assert(stmnt_paste:bind(3,authorid) == sql.OK) - if asanon == "anonymous" then - assert(stmnt_paste:bind(4,true) == sql.OK) - else - assert(stmnt_paste:bind(4,false) == sql.OK) - end - assert(stmnt_paste:bind_blob(5,"") == sql.OK) - err = do_sql(stmnt_paste) - stmnt_paste:reset() - if err == sql.DONE then - local rowid = stmnt_paste:last_insert_rowid() - assert(stmnt_raw:bind(1,rowid) == sql.OK) - assert(stmnt_raw:bind_blob(2,raw) == sql.OK) - assert(stmnt_raw:bind(3,markup) == sql.OK) - err = do_sql(stmnt_raw) - stmnt_raw:reset() - for _,tag in pairs(tags) do - print("tag 1:",stmnt_ins_tag:bind(1,rowid)) - print("Looking at tag",tag) - print("tag 2:",stmnt_ins_tag:bind(2,tag)) - err = do_sql(stmnt_ins_tag) - stmnt_ins_tag:reset() - end - if err ~= sql.DONE then - print("Failed to save raw text, but paste still went through") - end - local url = encode_id(rowid) - local loc - if asanon == "anonymous" then - loc = string.format("https://%s/%s",domain,url) - else - loc = string.format("https://%s.%s/%s",author,domain,url) - end - http_response_header(req,"Location",loc) - http_response(req,303,"") - cache.dirty(string.format("%s.%s",author,domain)) - cache.dirty(string.format("%s/%s",domain,url)) - cache.dirty(string.format("%s",domain)) - return - elseif err == sql.ERROR or err == sql.MISUSE then - ret = "Failed to paste: " .. tostring(err) - else - error("Error pasting:",err) - end - stmnt_paste:reset() - end - ]=] - end - - --assert(ret) - --http_response(req,200,ret) -end - ---A helper function for below -local function read_story(host,path,idp,show_comments,iam) - local cachestr - if show_comments then - cachestr = string.format("%s%s?comments=1",host,path) - else - cachestr = string.format("%s%s",host,path) - end - local id = decode_id(idp) - stmnt_update_views:bind_names{ - id = id - } - print("update:",do_sql(stmnt_update_views)) - stmnt_update_views:reset() - cache.dirty(cachestr) - print("cachestr was:",cachestr) - local readstoryf = function() - stmnt_read:bind_names{ - id = id - } - local err = do_sql(stmnt_read) - if err == sql.DONE then - stmnt_read:reset() - return pages.nostory{ - path = path - } - end - local tags = get_tags(id) - assert(err == sql.ROW,"Could not get row:" .. tostring(id) .. " Error:" .. tostring(err)) - local title, text, authorid, isanon, authorname, views = unpack(stmnt_read:get_values()) - stmnt_comments:bind_names{ - id = id - } - err = do_sql(stmnt_comments) - local comments = {} - while err ~= sql.DONE do - local com_author, com_isanon, com_text = unpack(stmnt_comments:get_values()) - table.insert(comments,{ - author = com_author, - isanon = com_isanon == 1, --int to boolean - text = com_text - }) - err = stmnt_comments:step() - end - stmnt_comments:reset() - text = zlib.decompress(text) - stmnt_read:reset() - return pages.read{ - domain = domain, - title = title, - text = text, - idp = idp, - isanon = isanon == 1, - author = authorname, - comments = comments, - show_comments = show_comments, - iam = iam, - tags = tags, - views = views, - } - end - --Don't cache if we're logged in, someone might see dirty cache information on the page. - --(I.e. When the user has loaded comments, the form to past a comment may contain a username, - --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. - if not iam then - return cache.render(cachestr,readstoryf) - else - return readstoryf() end end function read(req) - --local host = http_request_get_host(req) - --local path = http_request_get_path(req) local method = http_method_text(req) if method == "GET" then endpoints.read_get(req) - --[=[ - local idp = string.sub(path,2)--remove leading "/" - assert(string.len(path) > 0,"Tried to read 0-length story id") - local author, authorid = get_session(req) - http_request_populate_qs(req) - local show_comments = http_argument_get_string(req,"comments") - --parameters needed for the read page - local text - if author then - --We're logged in as someone - local id = decode_id(idp) - stmnt_read:bind_names{ - id = id - } - local err = do_sql(stmnt_read) - local tags = get_tags(id) - if err == sql.DONE then - --We got no story - stmnt_read:reset() - text = pages.nostory{ - path = path - } - else - --If we can edit this story, we don't want to cache - --the page, since it'll have an edit button on it. - assert(err == sql.ROW) - local title, storytext, tauthor, isanon, authorname, views = unpack(stmnt_read:get_values()) - stmnt_update_views:bind_names{ - id = id - } - print("update:",do_sql(stmnt_update_views)) - stmnt_update_views:reset() - storytext = zlib.decompress(storytext) - stmnt_read:reset() - if tauthor == authorid then - --The story exists and we're logged in as the - --owner, display the edit button - text = pages.read{ - domain = domain, - title = title, - text = storytext, - idp = idp, - isanon = isanon == 1, - author = authorname, - iam = authorname, - owner = true, - tags = tags, - views = views, - } - - else - text = read_story(host,path,idp,show_comments,author) - end - end - else - --We're not logged in as anyone - http_request_populate_qs(req) - text = read_story(host,path,idp,show_comments,author) - end - assert(text) - http_response(req,200,text) - return - ]=] elseif method == "POST" then endpoints.read_post(req) - --[=[ - --We're posting a comment - http_request_populate_post(req) - http_populate_cookies(req) - local author, authorid = get_session(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 = 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 = 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 end function login(req) - --local host = http_request_get_host(req) - --local path = http_request_get_path(req) local method = http_method_text(req) - --[[ - if host ~= domain then - --Don't allow logging into subdomains, I guess - http_response_header(req,"Location",string.format("https://%s/_login",domain)) - http_response(req,303,"") - return - end - local text - ]] if method == "GET" then endpoints.login_get(req) - --[=[ - --Just give them the login page - text = cache.render(string.format("%s/_login",domain),function() - return pages.login{ - err = "", - } - end) - ]=] elseif method == "POST" then endpoints.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 err = 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 - ]=] end - --assert(text) - --http_response(req,200,text) end --Edit a story function edit(req) - --local host = http_request_get_host(req) - --local path = http_request_get_path(req) local method = http_method_text(req) - --local author, author_id = get_session(req) - --local ret if method == "GET" then endpoints.edit_get(req) - --[=[ - http_request_populate_qs(req) - local story = assert(http_argument_get_string(req,"story")) - local story_id = decode_id(story) - 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 = 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 = get_tags(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 - } - ]=] elseif method == "POST" then endpoints.edit_post(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 = 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(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() - 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 = encode_id(storyid) - local loc = string.format("https://%s/%s",domain,id_enc) - cache.dirty(string.format("%s/%s",domain,id_enc)) -- This place to read this post - 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). - cache.dirty(string.format("%s.%s",author,domain)) -- The author's index, same reasoning as above. - http_response_header(req,"Location",loc) - http_response(req,303,"") - return - ]=] end - --assert(ret) - --http_response(req,200,ret) end --TODO function edit_bio() - print("we want to edit bio") + error("Not yet implemented") end function teardown() @@ -1231,107 +125,14 @@ end function download(req) endpoints.download_get(req) - --[[ - local host = http_request_get_host(req) - local path = http_request_get_path(req) - print("host:",host,"path:",path) - http_request_populate_qs(req) - local story = assert(http_argument_get_string(req,"story")) - local story_id = decode_id(story) - print("Downloading", story_id) - stmnt_download:bind_names{ - postid = story_id - } - local err = do_sql(stmnt_download) - if err == sql.DONE then - --No rows, story not found - http_responose(req,404,pages.nostory{path=story}) - stmnt_download:reset() - return - 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 text = zlib.decompress(txt_compressed) - stmnt_download:reset() - http_response_header(req,"Content-Type","application/octet-stream") - local nicetitle = title:gsub("%W","_") - http_response_header(req,"Content-Disposition","attachment; filename=\"" .. nicetitle .. ".txt\"") - http_response(req,200,text) - ]] end function preview(req) endpoints.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 = 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 function search(req) endpoints.search_get(req) - --[=[ - local host = http_request_get_host(req) - local path = http_request_get_path(req) - http_request_populate_qs(req) - local tag = http_argument_get_string(req,"tag") - if tag then - stmnt_search:bind_names{ - tag = tag - } - local results = {} - local err - repeat - err = stmnt_search:step() - if err == sql.BUSY then - coroutine.yield() - elseif err == sql.ROW then - local id, title, anon, time, author = unpack(stmnt_search:get_values()) - local idp = encode_id(id) - local tags = get_tags(id) - table.insert(results,{ - id = idp, - title = title, - anon = anon, - time = os.date("%B %d %Y",tonumber(time)), - author = author, - tags = tags - }) - elseif err == sql.DONE then - stmnt_search:reset() - else - error("Failed to search, sql error:" .. tostring(err)) - end - until err == sql.DONE - local ret = pages.search{ - domain = domain, - results = results, - tag = tag, - } - http_response(req,200,ret) - end - ]=] end print("Done with init.lua") diff --git a/src/lua/login_get.lua b/src/lua/login_get.lua deleted file mode 100644 index e9d279d..0000000 --- a/src/lua/login_get.lua +++ /dev/null @@ -1,17 +0,0 @@ -local config = require("config") -local cache = require("cache") -local config = require("config") -local pages = require("pages") - - -local function login_get(req) - --Just give them the login page - local ret = cache.render(string.format("%s/_login",config.domain),function() - return pages.login{ - err = "", - } - end) - http_response(req,200,ret) -end - -return login_get diff --git a/src/lua/login_post.lua b/src/lua/login_post.lua deleted file mode 100644 index 46d7504..0000000 --- a/src/lua/login_post.lua +++ /dev/null @@ -1,61 +0,0 @@ -local sql = require("lsqlite3") - -local db = require("db") -local util = require("util") -local session = require("session") -local config = require("config") -local pages = require("pages") - -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 oldconfigure(...) -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 mysession = session.start(id) - http_response_cookie(req,"session",mysession,"/",0,0) - local loc = string.format("https://%s.%s",name,config.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 diff --git a/src/lua/parser_search.lua b/src/lua/parser_search.lua new file mode 100644 index 0000000..0644f70 --- /dev/null +++ b/src/lua/parser_search.lua @@ -0,0 +1,8 @@ + +--Grammar +local function parser(str) + str:gsub(" + +end + +return parser diff --git a/src/lua/paste_get.lua b/src/lua/paste_get.lua deleted file mode 100644 index b371a0b..0000000 --- a/src/lua/paste_get.lua +++ /dev/null @@ -1,96 +0,0 @@ -local config = require("config") -local session = require("session") -local pages = require("pages") -local cache = require("cache") - -local function paste_get(req) - --Get the paste page - local host = http_request_get_host(req) - local text - local author,_ = session.get(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 = config.domain, - err = "", - } - end) - http_response(req,200,text) - elseif host ~= config.domain and author then - text = pages.author_paste{ - domain = config.domain, - user = author, - err = "", - text="", - } - elseif host ~= config.domain and author == nil then - http_response_header(req,"Location",string.format("https://%s/_paste",config.domain)) - http_response(req,303,"") - else - error(string.format( - "Unable to find a good case for paste:%s,%s,%s", - host, - config.domain, - author - )) - end - assert(text) - http_response(req,200,text) - --[=[ - 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,_ = session.get(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 - -return paste_get diff --git a/src/lua/paste_post.lua b/src/lua/paste_post.lua deleted file mode 100644 index 243cf6e..0000000 --- a/src/lua/paste_post.lua +++ /dev/null @@ -1,194 +0,0 @@ -local sql = require("lsqlite3") -local zlib = require("zlib") - -local util = require("util") -local parsers = require("parsers") -local config = require("config") -local queries = require("queries") -local db = require("db") -local cache = require("cache") -local tags = require("tags") -local session = require("session") - -local stmnt_raw,stmnt_paste - -local oldconfigure = configure -function configure(...) - stmnt_paste = assert(db.conn:prepare(queries.insert_post)) - stmnt_raw = assert(db.conn:prepare(queries.insert_raw)) - return oldconfigure(...) -end - -local function anon_paste(req,ps) - --Public paste - --[[ - This doesn't actually do much for IPv4 addresses, - since there are only 32 bits of address. Someone who - got a copy of the database could - just generate all 2^32 hashes and look up who posted - what. Use IPv6, Tor or I2P where possible. (but then I - guess it's harder to ban spammers... hmm..) - ]] - --local ip = http_request_get_ip(req) - --local iphash = sha3(ip) - --Don't store this information for now, until I come up - --with a more elegent solution. - - util.sqlbind(stmnt_paste,"bind_blob",1,ps.text) - --assert(stmnt_paste:bind_blob(1,text) == sql.OK) - util.sqlbind(stmnt_paste,"bind",2,ps.title) - --assert(stmnt_paste:bind(2,esctitle) == sql.OK) - util.sqlbind(stmnt_paste,"bind",3,-1) - --assert(stmnt_paste:bind(3,-1) == sql.OK) - util.sqlbind(stmnt_paste,"bind",4,true) - --assert(stmnt_paste:bind(4,true) == sql.OK) - util.sqlbind(stmnt_paste,"bind_blob",5,"") - --assert(stmnt_paste:bind_blob(5,"") == sql.OK) - err = util.do_sql(stmnt_paste) - stmnt_paste:reset() - if err == sql.DONE then - local rowid = stmnt_paste:last_insert_rowid() - assert(stmnt_raw:bind(1,rowid) == sql.OK) - assert(stmnt_raw:bind_blob(2,ps.raw) == sql.OK) - assert(stmnt_raw:bind(3,ps.markup) == sql.OK) - err = util.do_sql(stmnt_raw) - stmnt_raw:reset() - if err ~= sql.DONE then - print("Failed to save raw text, but paste still went though") - end - tags.set(rowid,ps.tags) - --[[ - for _,tag in pairs(ps.tags) do - print("tag 1:",stmnt_ins_tag:bind(1,rowid)) - print("Looking at tag",tag) - print("tag 2:",stmnt_ins_tag:bind(2,tag)) - err = util.do_sql(stmnt_ins_tag) - stmnt_ins_tag:reset() - end - ]] - local url = util.encode_id(rowid) - local loc = string.format("https://%s/%s",config.domain,url) - http_response_header(req,"Location",loc) - http_response(req,303,"") - cache.dirty(string.format("%s/%s",config.domain,url)) - cache.dirty(string.format("%s",config.domain)) - return - elseif err == sql.ERROR or err == sql.MISUSE then - ret = "Failed to paste: " .. tostring(err) - else - error("Error pasting:" .. tostring(err)) - end - stmnt_paste:reset() -end -local function author_paste(req,ps) - --Author paste - local author, authorid = session.get(req) - if author == nil then - ret = pages.author_paste{ - domain = domain, - author = subdomain, - err = "You are not logged in, you must be logged in to post as " .. subdomain .. ".", - text = text - } - end - local asanon = assert(http_argument_get_string(req,"pasteas")) - --No need to check if the author is posting to the - --"right" sudomain, just post it to the one they have - --the session key for. - assert(stmnt_paste:bind_blob(1,ps.text) == sql.OK) - assert(stmnt_paste:bind(2,ps.title) == sql.OK) - assert(stmnt_paste:bind(3,authorid) == sql.OK) - if asanon == "anonymous" then - assert(stmnt_paste:bind(4,true) == sql.OK) - else - assert(stmnt_paste:bind(4,false) == sql.OK) - end - assert(stmnt_paste:bind_blob(5,"") == sql.OK) - err = util.do_sql(stmnt_paste) - stmnt_paste:reset() - if err == sql.DONE then - local rowid = stmnt_paste:last_insert_rowid() - assert(stmnt_raw:bind(1,rowid) == sql.OK) - assert(stmnt_raw:bind_blob(2,ps.raw) == sql.OK) - assert(stmnt_raw:bind(3,ps.markup) == sql.OK) - err = util.do_sql(stmnt_raw) - stmnt_raw:reset() - if err ~= sql.DONE then - print("Failed to save raw text, but paste still went through") - end - tags.set(rowid,ps.tags) - --[[ - for _,tag in pairs(ps.tags) do - print("tag 1:",stmnt_ins_tag:bind(1,rowid)) - print("Looking at tag",tag) - print("tag 2:",stmnt_ins_tag:bind(2,tag)) - err = do_sql(stmnt_ins_tag) - stmnt_ins_tag:reset() - end - ]] - local url = util.encode_id(rowid) - local loc - if asanon == "anonymous" then - loc = string.format("https://%s/%s",config.domain,url) - else - loc = string.format("https://%s.%s/%s",author,config.domain,url) - end - http_response_header(req,"Location",loc) - http_response(req,303,"") - cache.dirty(string.format("%s.%s",author,config.domain)) - cache.dirty(string.format("%s/%s",config.domain,url)) - cache.dirty(string.format("%s",config.domain)) - return - elseif err == sql.ERROR or err == sql.MISUSE then - ret = "Failed to paste: " .. tostring(err) - else - error("Error pasting:",err) - end - stmnt_paste:reset() - -end -local function decodeentities(capture) - local n = tonumber(capture,16) - local c = string.char(n) - if escapes[c] then - return escapes[c] - else - return c - end -end -local function paste_post(req) - local host = http_request_get_host(req) - local path = http_request_get_path(req) - - local ps = {} - --We're creatinga new paste - http_request_populate_post(req) - local title = assert(http_argument_get_string(req,"title")) - local text = assert(http_argument_get_string(req,"text")) - ps.markup = assert(http_argument_get_string(req,"markup")) - local tag_str = http_argument_get_string(req,"tags") - ps.tags = {} - if tag_str then - ps.tags = util.parse_tags(tag_str) - end - local pasteas - ps.raw = zlib.compress(text) - text = string.gsub(text,"%%(%x%x)",decodeentities) - text = parsers[ps.markup](text) - assert(text,"Failed to parse text") - text = zlib.compress(text) - assert(text,"Failed to compress text") - ps.text = text - local esctitle = string.gsub(title,"%%(%x%x)",decodeentities) - --Always sanatize the title with the plain parser. no markup - --in the title. - ps.title = parsers.plain(title) - if host == config.domain then - anon_paste(req,ps) - else - author_paste(req,ps) - end -end ---assert(ret) ---http_response(req,200,ret) -return paste_post diff --git a/src/lua/preview_post.lua b/src/lua/preview_post.lua deleted file mode 100644 index e3cb8da..0000000 --- a/src/lua/preview_post.lua +++ /dev/null @@ -1,33 +0,0 @@ -local parsers = require("parsers") -local tags = require("tags") -local util = require("util") -local pages = require("pages") -local config = require("config") - -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 = config.domain, - title = title, - author = "preview", - idp = "preview", - text = parsed, - tags = tags, - } - http_response(req,200,ret) -end - -return preview_post diff --git a/src/lua/read_get.lua b/src/lua/read_get.lua deleted file mode 100644 index 3874b9a..0000000 --- a/src/lua/read_get.lua +++ /dev/null @@ -1,169 +0,0 @@ -local sql = require("sqlite3") - -local session = require("session") -local tags = require("tags") -local db = require("db") -local queries = require("queries") -local util = require("util") -local cache = require("cache") -local pages = require("pages") -local config = require("config") - -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 oldconfigure(...) -end - - ---[[ -Increases a story's hit counter by 1 -]] -local function add_view(storyid) - stmnt_update_views:bind_names{ - id = storyid - } - local err = util.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 = util.do_sql(stmnt_read) - if err == sql.DONE then - --We got no story - stmnt_read:reset() - error("No story by this name",ps.storyid) - return - end - --If we've made it here, we have a story. Populate our settings - --with title, text, ect. - assert(err == sql.ROW) - local title, storytext, tauthor, isanon, authorname, views = unpack( - stmnt_read:get_values() - ) - ps.title = title - ps.text = zlib.decompress(storytext) - ps.tauthor = tauthor - ps.isanon = isanon == 1 - ps.author = authorname - ps.views = views - stmnt_read:reset() - --Tags - ps.tags = tags.get(ps.storyid) - return true -end - ---[[ -Get the comments for a story -]] -local function get_comments(req,ps) - stmnt_comments:bind_names{ - id = ps.storyid - } - err = util.do_sql(stmnt_comments) - local comments = {} - while err ~= sql.DONE do - local com_author, com_isanon, com_text = unpack(stmnt_comments:get_values()) - table.insert(comments,{ - author = com_author, - isanon = com_isanon == 1, --int to boolean - text = com_text - }) - err = stmnt_comments:step() - end - stmnt_comments:reset() - return comments -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 = { - domain = config.domain, - host = http_request_get_host(req), - path = http_request_get_path(req), - method = http_method_text(req), - } - print("reading", ps.path) - - --Get our story id - assert(string.len(ps.path) > 0,"Tried to read 0-length story id") - ps.idp = string.sub(ps.path,2)--remove leading "/" - ps.storyid = util.decode_id(ps.idp) - add_view(ps.storyid) - - --If we're logged in, set author and authorid - local author, authorid = session.get(req) - if author and authorid then - ps.loggedauthor = author - ps.iam = author - ps.loggedauthorid = 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 - - local text - --normal story display - if (not ps.loggedauthor) then - print("not author") - local cachestr = string.format("%s%s%s", - ps.host, - ps.path, - ps.show_comments and "?comments=1" or "" - ) - text = cache.render(cachestr,function() - populate_ps_story(req,ps) - local output = pages.read(ps) - assert(output,"failed to read page:" .. cachestr) - return output - end) - else --we are logged in, don't cache - print("is author") - populate_ps_story(req,ps) - print("tauthor was", ps.tauthor, "while author was:",ps.author) - ps.owner = (ps.loggedauthorid == ps.tauthor) - text = pages.read(ps) - end - assert(text) - http_response(req,200,text) - return -end - -return read_get diff --git a/src/lua/read_post.lua b/src/lua/read_post.lua deleted file mode 100644 index 2fde5ef..0000000 --- a/src/lua/read_post.lua +++ /dev/null @@ -1,53 +0,0 @@ -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", config.domain, path) - http_response_header(req,"Location",redir) - http_response(req,303,"") - end - -end -return read_post diff --git a/src/lua/render.lua b/src/lua/render.lua deleted file mode 100644 index 139597f..0000000 --- a/src/lua/render.lua +++ /dev/null @@ -1,2 +0,0 @@ - - diff --git a/src/lua/search_get.lua b/src/lua/search_get.lua deleted file mode 100644 index 4c340a3..0000000 --- a/src/lua/search_get.lua +++ /dev/null @@ -1,59 +0,0 @@ -local sql = require("lsqlite3") - -local db = require("db") -local queries = require("queries") -local util = require("util") -local libtags = require("tags") -local pages = require("pages") -local config = require("config") - -local stmnt_search -local oldconfigure = configure -function configure(...) - stmnt_search = assert(db.conn:prepare(queries.select_post_tags)) - return oldconfigure(...) -end - -local function search_get(req) - local host = http_request_get_host(req) - local path = http_request_get_path(req) - http_request_populate_qs(req) - local tag = http_argument_get_string(req,"tag") - if tag then - stmnt_search:bind_names{ - tag = tag - } - local results = {} - local err - repeat - err = stmnt_search:step() - if err == sql.BUSY then - coroutine.yield() - elseif err == sql.ROW then - local id, title, anon, time, author = unpack(stmnt_search:get_values()) - local idp = util.encode_id(id) - local tags = libtags.get(id) - table.insert(results,{ - id = idp, - title = title, - anon = anon, - time = os.date("%B %d %Y",tonumber(time)), - author = author, - tags = tags - }) - elseif err == sql.DONE then - stmnt_search:reset() - else - error("Failed to search, sql error:" .. tostring(err)) - end - until err == sql.DONE - local ret = pages.search{ - domain = config.domain, - results = results, - tag = tag, - } - http_response(req,200,ret) - end -end - -return search_get diff --git a/src/pages/index.etlua b/src/pages/index.etlua index f51ab08..7a6bf33 100644 --- a/src/pages/index.etlua +++ b/src/pages/index.etlua @@ -45,6 +45,9 @@ <% for i = 1,math.min(#v.tags, 5) do %>
  • <%= v.tags[i] %>
  • <% end %> + <% if #v.tags > 5 then %> +
  • +<%= #v.tags - 5 %>
  • + <% end %> <%= v.posted %> diff --git a/src/pages/read.etlua b/src/pages/read.etlua index 5224b19..4acb10a 100644 --- a/src/pages/read.etlua +++ b/src/pages/read.etlua @@ -32,7 +32,7 @@

    <%= views %> Hits

    -
      +
        <% for _,tag in pairs(tags) do -%>
      • <%= tag %>
      • <% end -%>