diff --git a/Makefile b/Makefile index 2d0d0cd..c2496bb 100644 --- a/Makefile +++ b/Makefile @@ -3,8 +3,8 @@ chroot_dir=kore_chroot/ mirror=http://dl-cdn.alpinelinux.org/alpine/ arch=aarch64 version=2.10.5-r0 -#certbot_email=--register-unsafely-without-email -certbot_email=-m you@cock.li +certbot_email=--register-unsafely-without-email +#certbot_email=-m you@cock.li domain=test.monster #Probably don't change stuff past here @@ -20,17 +20,18 @@ all: $(chroot_dir) smr.so $(built_files) $(built_pages) $(built_sql) echo $(built_files) kodev run -apk-tools-static-$(version).apk: Makefile - wget -q $(mirror)latest-stable/main/$(arch)/apk-tools-static-$(version).apk +apk-tools-static-$(version).apk: + # wget -q $(mirror)latest-stable/main/$(arch)/apk-tools-static-$(version).apk clean: kodev clean -$(chroot_dir): #apk-tools-static-$(version).apk +$(chroot_dir): apk-tools-static-$(version).apk mkdir -p $(chroot_dir) mkdir -p $(chroot_dir)/pages mkdir -p $(chroot_dir)/sql - #cd $(chroot_dir) && tar -xvzf ../../apk-tools-static-*.apk + mkdir -p $(chroot_dir)/data + #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 #ln -s /dev/urandom $(chroot_dir)/dev/urandom @@ -39,13 +40,16 @@ $(chroot_dir): #apk-tools-static-$(version).apk #mount -t proc none $(chroot_dir)/proc #mount -o bind /sys $(chroot_dir)/sys #cp /etc/resolv.conf $(chroot_dir)/etc/resolv.conf + #echo "$(mirror)/$(branch)/main" > $(chroot)/etc/apk/repositories + #echo "$(mirror)/$(branch)/community" >> $(chroot)/etc/apk/repositories #cp /etc/apk/repositories $(chroot_dir)/etc/apk/repositories #mkdir $(chroot_dir)/var/sm ## Things to build lua libraries #chroot $(chroot_dir) apk add luarocks5.1 sqlite sqlite-dev lua5.1-dev build-base zlib zlib-dev #chroot $(chroot_dir) luarocks-5.1 install etlua #chroot $(chroot_dir) luarocks-5.1 install lsqlite3 - #chroot $(chroot_dir) luarocks-5.1 install lzlib ZLIB_LIBDIR=/lib #for some reason lzlib looks in /usr/lib for libz, when it needs to look at /lib + #chroot $(chroot_dir) luarocks-5.1 install lpeg + #chroot $(chroot_dir) luarocks-5.1 install lua-zlib ZLIB_LIBDIR=/lib #for some reason lzlib looks in /usr/lib for libz, when it needs to look at /lib ## Once we've built + installed everything, delete extra stuff from the chroot #chroot $(chroot_dir) apk del sqlite-dev lua5.1-dev build-base zlib-dev ## SSL certificates, if you don't trust EFF (they have an antifa black block member as their favicon at time of writing) you may want to replace this. @@ -56,6 +60,7 @@ $(chroot_dir): #apk-tools-static-$(version).apk code : $(built_files) $(built_files): $(chroot_dir)%.lua : src/lua/%.lua + echo built files: $(built_files) cp $^ $@ $(built_pages): $(chroot_dir)pages/%.etlua : src/pages/%.etlua diff --git a/conf/build.conf b/conf/build.conf index 9cb1529..90ab6ae 100644 --- a/conf/build.conf +++ b/conf/build.conf @@ -25,8 +25,9 @@ mime_add=css:text/css dev { # These flags are added to the shared ones when # you build the "dev" flavor. - ldflags=-llua + ldflags=-llua5.1 cflags=-g -Wextra + cflags=-I/usr/include/lua5.1 cxxflags=-g -Wextra } diff --git a/conf/smr.conf b/conf/smr.conf index c9e3ff5..7693d17 100644 --- a/conf/smr.conf +++ b/conf/smr.conf @@ -7,9 +7,9 @@ server tls { seccomp_tracing yes load ./smr.so root kore_chroot -runas root -#keymgr_runas demo -#keymgr_root ./ +runas robin +keymgr_runas robin +keymgr_root . workers 1 http_body_max 8388608 @@ -20,12 +20,13 @@ validator v_any regex .* validator v_storyid regex [a-zA-Z0-9]+ validator v_subdomain regex [a-z0-9]{1,30} validator v_markup regex (plain|imageboard) +validator v_bool regex (0|1) domain * { attach tls - certfile server.pem - certkey key.pem + certfile cert/server.pem + certkey cert/key.pem #I run kore behind a lighttpd reverse proxy, so this is a bit useless to me accesslog /dev/null @@ -40,12 +41,16 @@ domain * { route /_bio edit_bio route /_login login route /_claim claim + route /_download download # Leading ^ is needed for dynamic routes, kore says the route is dynamic if it does not start with '/' route ^/[^_].* read_story params get /_edit { validate story v_storyid } + params get /_download { + validate story v_storyid + } params post /_edit { validate title v_any validate story v_storyid @@ -59,9 +64,14 @@ domain * { validate pasteas v_subdomain validate markup v_markup } - #params get /[^_].* { + params get ^/[^_].* { + validate comments v_bool #validate story v_storyid - #} + } + params post ^/[^_].* { + validate text v_any + validate postas v_subdomain + } params post /_login { validate user v_subdomain validate pass v_any @@ -69,4 +79,5 @@ domain * { params post /_claim { validate user v_any } + } diff --git a/src/libcrypto.c b/src/libcrypto.c index 17b5b20..6679138 100644 --- a/src/libcrypto.c +++ b/src/libcrypto.c @@ -1,5 +1,5 @@ /* -borrowed sha3 implementation from keccak.team +borrowed sha3 implementation from https://keccak.team */ #ifdef BUILD_PROD #include diff --git a/src/lua/init.lua b/src/lua/init.lua index 3506508..ae1e1f5 100644 --- a/src/lua/init.lua +++ b/src/lua/init.lua @@ -1,6 +1,9 @@ +print("Really fast print from init.lua") + local et = require("etlua") local sql = require("lsqlite3") local zlib = require("zlib") + if PRODUCTION then local function print() end --squash prints end @@ -50,6 +53,7 @@ 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 --see https://perishablepress.com/stop-using-unsafe-characters-in-urls/ --no underscore because we use that for our operative pages local url_characters = @@ -82,8 +86,10 @@ 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 local msg = "test message" local one = zlib.compress(msg) local two = zlib.decompress(one) @@ -132,6 +138,8 @@ function configure() --Select the data we need to read a story (and maybe display an edit --button stmnt_read = assert(db:prepare(queries.select_post)) + stmnt_comments = assert(db:prepare(queries.select_comments)) + stmnt_comment_insert = assert(db:prepare(queries.insert_comment)) --TODO: actually let authors edit their bio stmnt_author_bio = assert(db:prepare([[ SELECT authors.biography FROM authors WHERE authors.name = :author; @@ -158,6 +166,8 @@ function configure() stmnt_raw = assert(db:prepare(queries.insert_raw)) --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? @@ -172,7 +182,7 @@ function configure() FROM cache WHERE path = :path AND - ((dirty = 0) OR (strftime('%s','now') - updated) < 10) + ((dirty = 0) OR (strftime('%s','now') - updated) < 2) ; ]]) stmnt_insert_cache = cache:prepare([[ @@ -189,6 +199,7 @@ function configure() ]]) --[=[ ]=] + print("finished running configure()") end print("Created configure function") @@ -232,6 +243,7 @@ local function do_sql(stmnt) end local function dirty_cache(url) + print("Dirtying cache:",url) stmnt_dirty_cache:bind_names{ path = url } @@ -475,25 +487,33 @@ function paste(req) if method == "GET" then --Get the paste page if host == domain then - --For an anonymous user - ret = render(string.format("%s/_paste",host),function() - print("Cache missing, rendering post page") - return pages.paste{ - domain = domain, - err = "", - } - end) + 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 = 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 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 - print("sessionid was nil") http_response_header(req,"Location","https://"..domain.."/_paste") http_response(req,303,"") return @@ -557,7 +577,6 @@ function paste(req) if err ~= sql.DONE then print("Failed to save raw text, but paste still went though") end - print("Successful paste, rowid:", rowid) local url = encode_id(rowid) local loc = string.format("https://%s/%s",domain,url) http_response_header(req,"Location",loc) @@ -570,7 +589,7 @@ function paste(req) elseif err == sql.ERROR or err == sql.MISUSE then ret = "Failed to paste: " .. tostring(err) else - error("Error pasting:",err) + error("Error pasting:" .. tostring(err)) end stmnt_paste:reset() @@ -636,11 +655,15 @@ function paste(req) end --A helper function for below -local function read_story(host,path,idp) - return render(string.format("%s%s",host,path),function() - print("Trying to read, id is",idp,":",decode_id(idp)) +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 readstoryf = function() local id = decode_id(idp) - print("id:",id,type(id)) stmnt_read:bind_names{ id = id } @@ -653,6 +676,20 @@ local function read_story(host,path,idp) end assert(err == sql.ROW,"Could not get row:" .. tostring(id) .. " Error:" .. tostring(err)) local title, text, authorid, isanon, authorname = 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 text = zlib.decompress(text) stmnt_read:reset() return pages.read{ @@ -661,61 +698,108 @@ local function read_story(host,path,idp) text = text, idp = idp, isanon = isanon == 1, - author = authorname + author = authorname, + comments = comments, + show_comments = show_comments, + iam = iam, } - end) - + end + --Don't cache if we're logged in, someone might see dirty cache information on the page. + if not iam then + return 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 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) - 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) - if err == sql.DONE then - --We got no story - stmnt_read:reset() - text = pages.nostory{ - path = path + local method = http_method_text(req) + if method == "GET" then + 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 } - else - assert(err == sql.ROW) - local title, storytext, tauthor, isanon, authorname = unpack(stmnt_read:get_values()) - storytext = zlib.decompress(storytext) - stmnt_read:reset() - if tauthor == authorid then - print("we're the author!") - --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, - owner = true + local err = do_sql(stmnt_read) + if err == sql.DONE then + --We got no story + stmnt_read:reset() + text = pages.nostory{ + path = path } - else - print("we're not the author!") - text = read_story(host,path,idp) + --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 = unpack(stmnt_read:get_values()) + 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 + } + + 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 + --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 + 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 + dirty_cache(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 - else - text = read_story(host,path,idp) end - assert(text) - http_response(req,200,text) end function login(req) @@ -883,4 +967,30 @@ function teardown() print("Finished lua teardown") end +function download(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) + 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 + 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 + print("Done with init.lua") diff --git a/src/lua/parser_imageboard.lua b/src/lua/parser_imageboard.lua index e62d606..d1797a7 100644 --- a/src/lua/parser_imageboard.lua +++ b/src/lua/parser_imageboard.lua @@ -61,10 +61,10 @@ local grammar = P{ heading = wrap("==",[[

%s

]]), strike = wrap("~~",[[%s]]), code = tag("code",[[
%s
]]), - greentext = P"> " * Cs((V"marked" + word)^0) / function(a) + greentext = P">" * (B"\n>" + B">") * Cs((V"marked" + word)^0) / function(a) return string.format([[>%s]],a) end, - pinktext = P"< " * Cs((V"marked" + word)^0) / function(a) + pinktext = P"<" * (B"\n<" + B"<") * Cs((V"marked" + word)^0) / function(a) return string.format([[<%s]],a) end, marked = V"spoiler" + V"bold" + V"italic" + V"underline" + V"heading" + V"strike" + V"spoiler2" + V"code", @@ -76,36 +76,39 @@ local grammar = P{ chunk = V"line"^0 * V"plainline" * V"ending" } ---local text = [[ ---this is **a big** test with ''italics''! ---we need to > sanitize < things that could be tags ---like really badly ---words can include any'single item without=penalty ---Can you use '''one tag ==within== another tag'''? ---let's see if [spoiler]spoiler tags work[/spoiler] ---things might even __go over ---multiple lines__ blah ---Let's test out those [code] ---code tag,s and see how well ---they work - --here's ome - --preformated - --text ---[/code] ---> Or have blank lines +--[=[ +local text = [[ + sanitize < things that could be tags +like really badly +words can include any'single item without=penalty +Can you use '''one tag ==within== another tag'''? +let's see if [spoiler]spoiler tags work[/spoiler] +things might even __go over +multiple lines__ blah +Let's test out those [code] +code tag,s and see how well +they work + here's ome + preformated + text +[/code] +>Or have blank lines ---one important thing is that greentext > should not start in the middle of a line ---> this next line is a green text, what if I include **markup** inside it? ---< and after '''it is''' a pinktext ---> because of some of these restrictions **bold text ---cannot go over multiple lines** in a green text ---__and finally__ there might be some text with ''' ---incomplete syntax with injection !!!! ---]] +one important thing is that greentext > should not start in the middle of a line +>this next line is a green text, what if I include **markup** inside it? +because of some of these restrictions **bold text +cannot go over multiple lines** in a green text +>greentext on the last line + <%= domain %>/<%= idp %> - <% if owner then %> + <% if owner then -%>
- <% end %> + <% end -%> +

<%- title %>

- <% if isanon then %> + <% if isanon then -%> By Anonymous - <% else %> + <% else -%> By <%= author %> - <% end %> + <% end -%>

<%- text %>
+
+ + +
+ <% if not show_comments then -%> +
+ + +
+ <% else %> +
+ + <% if iam then %> + + + <% else %> + + + <% end %> +
+ <% if comments and #comments == 0 then %> +

No comments yet

+ <% else %> +
+ <% for _,comment in pairs(comments) do %> +
+ <% if comment.isanon then %> +

Anonymous

+ <% else %> +

<%= comment.author %>

+ <% end %> +

<%= comment.text %>

+
+ <% end %> +
+ <% end %> + <% end %> + + diff --git a/src/smr.c b/src/smr.c index 5935eaf..2342379 100644 --- a/src/smr.c +++ b/src/smr.c @@ -20,6 +20,7 @@ int edit_bio(struct http_request *); int read_story(struct http_request *); int login(struct http_request *); int claim(struct http_request *); +int download(struct http_request *); int style(struct http_request *); int miligram(struct http_request *); int do_lua(struct http_request *req, const char *name); @@ -107,6 +108,12 @@ claim(struct http_request *req){ return do_lua(req,"claim"); } +int +download(struct http_request *req){ + printf("We want to do download!\n"); + return do_lua(req,"download"); +} + int home(struct http_request *req){ return do_lua(req,"home"); diff --git a/src/sql/insert_comment.sql b/src/sql/insert_comment.sql new file mode 100644 index 0000000..0adf012 --- /dev/null +++ b/src/sql/insert_comment.sql @@ -0,0 +1,15 @@ +INSERT INTO comments( + postid, + author, + isanon, + comment_text, + hashedip, + post_time +) VALUES ( + :postid, + :authorid, + :isanon, + :comment_text, + '', + strftime('%s','now') +); diff --git a/src/sql/select_comments.sql b/src/sql/select_comments.sql new file mode 100644 index 0000000..05b5419 --- /dev/null +++ b/src/sql/select_comments.sql @@ -0,0 +1,11 @@ +SELECT + authors.name, + comments.isanon, + comments.comment_text +FROM + comments, + authors +WHERE + comments.author = authors.id AND + comments.postid = :id +ORDER BY post_time DESC; diff --git a/src/sql/select_download.sql b/src/sql/select_download.sql new file mode 100644 index 0000000..d45cb04 --- /dev/null +++ b/src/sql/select_download.sql @@ -0,0 +1,7 @@ +SELECT + raw_text.post_text, posts.post_title +FROM + raw_text, posts +WHERE + raw_text.id = posts.id AND + raw_text.id = :postid