From 4eb5b4a7bdbe81ccadf77761aa2d9131140aa714 Mon Sep 17 00:00:00 2001 From: Robin Malley Date: Tue, 9 Feb 2021 02:41:41 +0000 Subject: [PATCH] Add some tools for interfaceing with smr Add a script to reset passwords and another generate archives. --- tools/accounts/main.lua | 127 +++++++++++++++++++++++++++ tools/archive/crc32.lua | 170 ++++++++++++++++++++++++++++++++++++ tools/archive/main.etlua | 26 ++++++ tools/archive/main.etlua.in | 24 +++++ tools/archive/main.lua | 109 +++++++++++++++++++++++ tools/archive/story.etlua | 23 +++++ tools/migrate/main.lua | 22 +++++ 7 files changed, 501 insertions(+) create mode 100644 tools/accounts/main.lua create mode 100644 tools/archive/crc32.lua create mode 100644 tools/archive/main.etlua create mode 100644 tools/archive/main.etlua.in create mode 100644 tools/archive/main.lua create mode 100644 tools/archive/story.etlua create mode 100644 tools/migrate/main.lua diff --git a/tools/accounts/main.lua b/tools/accounts/main.lua new file mode 100644 index 0000000..3aedfa8 --- /dev/null +++ b/tools/accounts/main.lua @@ -0,0 +1,127 @@ +--[[ +Implements some utilities for accounts in an smr-style database. + +# Lists all author_id, author_name in the database +lua tools/accounts/main.lua ls + +# Resets the author 13 with a newly generated passfile, and writes the newly +# generated file to "new.passfile" +lua tools/accounts/main.lua -o new.passfile reset_password 13 + +# Deletes the author with id 40, all posts and comments by this author will +# also be deleted. +lua tools/accounts/main.lua delete 40 +]] +local function sha3(data) + local tmpfn = os.tmpname() + local tmpf = assert(io.open(tmpfn,"wb")) + assert(tmpf:write(data)) + assert(tmpf:close()) + local pd = assert(io.popen("openssl dgst -sha3-512 " .. tmpfn, "r")) + local hex = pd:read("*a") + --[[ + hex looks like + SHA3-512(/tmp/lua_qQtQu4)= 9ece086e9bac491fac5c1d1046ca11d737b92a2b2ebd93f005d7b710110c0a678288166e7fbe796883a4f2e9b3ca9f484f521d0ce464345cc1aec96779149c14 + ]] + hex = hex:match("= (%x+)%s*$") + assert(pd:close()) + local ret_data = {} + for hex_byte in hex:gmatch("%x%x") do + table.insert(ret_data,string.char(tonumber(hex_byte,16))) + end + return table.concat(ret_data) +end +local sql = require("lsqlite3") +local argparse = require("argparse") + +local parser = argparse(){ + name = "tools/accounts/main.lua", + description = "View, reset, and delete accounts", + epilog = "Other tools included in the smr source distribution include:archive", +} +parser:help_max_width(80) +parser:option("-d --database","The database file","kore_chroot/data/posts.db") +parser:option("-o --output","Output to file (instead of stdout)") +local ls = parser:command("ls") { + description = "lists all user_id's and user_names in the database" +} +local reset_password = parser:command("reset_password") { + description = "Resets a password for a user, pass either an account id or an account name. If the inputed string can be converted to an account_id, and the account_id exists, it is considered an account_id, even if there is a user by the same name if it were considered an account_name." +} +reset_password:mutex( + reset_password:argument("account_id_or_name","The accout id or account name to reset") +) +local delete = parser:command("delete") { + description = "Deletes an account from the system. All posts and comments by this user are deleted as well. Once deleted, another user may claim the same username." +} +delete:mutex( + delete:argument("account_id","The accout id to reset"), + delete:argument("account_name","The name of the account to reset") +) + + +args = parser:parse() +local db,err,errmsg = sql.open(args.database, sql.OPEN_READWRITE) +if not db then + error(string.format("Failed to open %s : %s",args.database,errmsg)) +end +if args.output then + io.stdout = assert(io.open(args.output,"w")) +end +if args.ls then + for row in db:rows("SELECT id, name FROM authors;") do + local id,name = unpack(row) + io.stdout:write(string.format("%d\t%s\n",id,name)) + end +end +if args.reset_password then + 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) + local authorid, authorname + authorid = tonumber(args.account_id_or_name) + if authorid == nil then --could not be converted to a number + authorname = args.account_id_or_name + end + rngf:close() + + assert(io.stdout:write(password)) + local hash = sha3(salt .. password) + local stmt_update_pass + if authorid then + stmt_update_pass = db:prepare([[ + UPDATE authors + SET + salt = :salt, + passhash = :hash + WHERE + id=:authorid;]] + ) + stmt_update_pass:bind(3,authorid) + else + stmt_update_pass = db:prepare([[ + UPDATE authors + SET + salt = :salt, + passhash = :hash + WHERE + name=:authorname;]] + ) + stmt_update_pass:bind(3,authorname) + end + + stmt_update_pass:bind_blob(1,salt) + stmt_update_pass:bind_blob(2,hash) + local err = stmt_update_pass:step() + if err ~= sql.DONE then + io.stderr:write(string.format("Failed %d:%s",err,db:errmsg())) + end +end + +db:close() +--[[ +for k,v in pairs(args) do + print(k,":",v) +end +]] diff --git a/tools/archive/crc32.lua b/tools/archive/crc32.lua new file mode 100644 index 0000000..0c280b2 --- /dev/null +++ b/tools/archive/crc32.lua @@ -0,0 +1,170 @@ +--Copyright (c) 2007-2008 Neil Richardson (nrich@iinet.net.au) +-- +--Permission is hereby granted, free of charge, to any person obtaining a copy +--of this software and associated documentation files (the "Software"), to deal +--in the Software without restriction, including without limitation the rights +--to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +--copies of the Software, and to permit persons to whom the Software is +--furnished to do so, subject to the following conditions: +-- +--The above copyright notice and this permission notice shall be included in all +--copies or substantial portions of the Software. +-- +--THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +--IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +--FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +--AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +--LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +--OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS +--IN THE SOFTWARE. +local max = 2^32 -1 + +local CRC32 = { + 0,79764919,159529838,222504665,319059676, + 398814059,445009330,507990021,638119352, + 583659535,797628118,726387553,890018660, + 835552979,1015980042,944750013,1276238704, + 1221641927,1167319070,1095957929,1595256236, + 1540665371,1452775106,1381403509,1780037320, + 1859660671,1671105958,1733955601,2031960084, + 2111593891,1889500026,1952343757,2552477408, + 2632100695,2443283854,2506133561,2334638140, + 2414271883,2191915858,2254759653,3190512472, + 3135915759,3081330742,3009969537,2905550212, + 2850959411,2762807018,2691435357,3560074640, + 3505614887,3719321342,3648080713,3342211916, + 3287746299,3467911202,3396681109,4063920168, + 4143685023,4223187782,4286162673,3779000052, + 3858754371,3904687514,3967668269,881225847, + 809987520,1023691545,969234094,662832811, + 591600412,771767749,717299826,311336399, + 374308984,453813921,533576470,25881363, + 88864420,134795389,214552010,2023205639, + 2086057648,1897238633,1976864222,1804852699, + 1867694188,1645340341,1724971778,1587496639, + 1516133128,1461550545,1406951526,1302016099, + 1230646740,1142491917,1087903418,2896545431, + 2825181984,2770861561,2716262478,3215044683, + 3143675388,3055782693,3001194130,2326604591, + 2389456536,2200899649,2280525302,2578013683, + 2640855108,2418763421,2498394922,3769900519, + 3832873040,3912640137,3992402750,4088425275, + 4151408268,4197601365,4277358050,3334271071, + 3263032808,3476998961,3422541446,3585640067, + 3514407732,3694837229,3640369242,1762451694, + 1842216281,1619975040,1682949687,2047383090, + 2127137669,1938468188,2001449195,1325665622, + 1271206113,1183200824,1111960463,1543535498, + 1489069629,1434599652,1363369299,622672798, + 568075817,748617968,677256519,907627842, + 853037301,1067152940,995781531,51762726, + 131386257,177728840,240578815,269590778, + 349224269,429104020,491947555,4046411278, + 4126034873,4172115296,4234965207,3794477266, + 3874110821,3953728444,4016571915,3609705398, + 3555108353,3735388376,3664026991,3290680682, + 3236090077,3449943556,3378572211,3174993278, + 3120533705,3032266256,2961025959,2923101090, + 2868635157,2813903052,2742672763,2604032198, + 2683796849,2461293480,2524268063,2284983834, + 2364738477,2175806836,2238787779,1569362073, + 1498123566,1409854455,1355396672,1317987909, + 1246755826,1192025387,1137557660,2072149281, + 2135122070,1912620623,1992383480,1753615357, + 1816598090,1627664531,1707420964,295390185, + 358241886,404320391,483945776,43990325, + 106832002,186451547,266083308,932423249, + 861060070,1041341759,986742920,613929101, + 542559546,756411363,701822548,3316196985, + 3244833742,3425377559,3370778784,3601682597, + 3530312978,3744426955,3689838204,3819031489, + 3881883254,3928223919,4007849240,4037393693, + 4100235434,4180117107,4259748804,2310601993, + 233574846,2151335527,2231098320,2596047829, + 2659030626,2470359227,2550115596,2947551409, + 2876312838,2788305887,2733848168,3165939309, + 3094707162,3040238851,2985771188, +} + +local function xor(a, b) + local calc = 0 + + for i = 32, 0, -1 do + local val = 2 ^ i + local aa = false + local bb = false + + if a == 0 then + calc = calc + b + break + end + + if b == 0 then + calc = calc + a + break + end + + if a >= val then + aa = true + a = a - val + end + + if b >= val then + bb = true + b = b - val + end + + if not (aa and bb) and (aa or bb) then + calc = calc + val + end + end + + return calc +end + +local function lshift(num, left) + local res = num * (2 ^ left) + return res % (2 ^ 32) +end + +local function rshift(num, right) + local res = num / (2 ^ right) + return math.floor(res) +end + +local function crc32(str) + local count = string.len(tostring(str)) + local crc = max + + local i = 1 + while count > 0 do + local byte = string.byte(str, i) + + crc = xor(lshift(crc, 8), CRC32[xor(rshift(crc, 24), byte) + 1]) + + i = i + 1 + count = count - 1 + end + + return crc +end + +return crc32 + +-- +-- CRC32.lua +-- +-- A pure Lua implementation of a CRC32 hashing algorithm. Slower than using a C implemtation, +-- but useful having no other dependencies. +-- +-- +-- Synopsis +-- +-- require('CRC32') +-- +-- crchash = CRC32.Hash('a string') +-- +-- Methods: +-- +-- hashval = CRC32.Hash(val) +-- Calculates and returns (as an integer) the CRC32 hash of the parameter 'val'. diff --git a/tools/archive/main.etlua b/tools/archive/main.etlua new file mode 100644 index 0000000..a3ecef5 --- /dev/null +++ b/tools/archive/main.etlua @@ -0,0 +1,26 @@ + + + + + + + + + + + +<% for k,story in pairs(stories) do %> + + + + + + +<% end %> +
TitleAuthorTagsPosted
<%- story.title %>By <%= story.author %>
    + <% for i = 1,#story.tags do %> +
  • <%= story.tags[i] %>
  • + <% end %> +
<%= story.posted %>
+ + diff --git a/tools/archive/main.etlua.in b/tools/archive/main.etlua.in new file mode 100644 index 0000000..f7a54e6 --- /dev/null +++ b/tools/archive/main.etlua.in @@ -0,0 +1,24 @@ + + + + + + + + + +<% for k,story in pairs(stories) do %> + + + + + + +<% end %> +
TitleAuthorTagsPosted
<%- story.title %>By <%= story.author %>
    + <% for i = 1,#story.tags do %> +
  • <%= story.tags[i] %>
  • + <% end %> +
<%= story.posted %>
+ + diff --git a/tools/archive/main.lua b/tools/archive/main.lua new file mode 100644 index 0000000..438c081 --- /dev/null +++ b/tools/archive/main.lua @@ -0,0 +1,109 @@ +--[[ +Implements extracting the database to an archive file, you probably want a cron +job for this. + +# Generates the archive +lua tools/archive/main.lua +]] +local sql = require("lsqlite3") +local argparse = require("argparse") +local zlib = require("zlib") +local etlua = require("etlua") +local crc32 = require("tools.archive.crc32") + +local parser = argparse(){ + name = "tools/archive/main.lua", + description = "Generate a downloadable zip archive.", + epilog = "Other tools included in the smr source distribution include:accounts", +} +parser:help_max_width(80) +parser:option("-d --database","The database file","kore_chroot/data/posts.db") +parser:option("-o --output","Output to directory","kore_chroot/data/archive") + +args = parser:parse() +local db,err,errmsg = sql.open(args.database, sql.OPEN_READWRITE) +local outfile = io.open(args.output,"w") +if not db then + error(string.format("Failed to open %s : %s",args.database,errmsg)) +end + +local index_f = assert(io.open("tools/archive/main.etlua")) +local index_tmpl = assert(index_f:read("*a")) +index_f:close() +local template_main = etlua.compile(index_tmpl) + +local story_f = assert(io.open("tools/archive/story.etlua")) +story_tmpl = assert(story_f:read("*a")) +story_f:close() +local template_story = etlua.compile(story_tmpl) +local zipped_files = {} +for row in db:rows([[ + SELECT + posts.id, + posts.post_text, + posts.post_title, + authors.name, + posts.isanon, + posts.post_time, + GROUP_CONCAT(tags.tag,";") + FROM posts, authors + LEFT JOIN tags ON tags.postid = posts.id + WHERE + posts.authorid = authors.id AND + posts.unlisted = 0 + GROUP BY posts.id + ORDER BY posts.post_time +]]) do + local id, text, title, author, isanon, post_time, tags_txt = unpack(row) + local tags_txt = (tags_txt or "") .. ";" + local tags = {} + for tag in tags_txt:gmatch("([^;]+)") do + table.insert(tags,tag) + end + local story_txt = zlib.decompress(text) + local file_data = template_story{ + text = story_txt, + author = isanon == 0 and author or "Anonymous", + title = title, + tags = tags, + } + local filename = string.format("s%d.html",id) + table.insert(zipped_files,{ + filename = filename, + author = isanon == 0 and author or "Anonymous", + tags = tags, + title = title, + posted = os.date("%B %d %Y",tonumber(post_time)), + }) + local fd = assert(io.open(args.output .. "/" .. filename,"w")) + assert(fd:write(file_data)) + assert(fd:close()) +end + +--index.html +local index_txt = template_main{ + stories = zipped_files, +} +local fd = assert(io.open(string.format("%s/index.html",args.output),"w")) +assert(fd:write(index_txt)) +assert(fd:close()) + +--css +local cssfd = assert(io.open("assets/style.css","r")) +local csstxt = assert(cssfd:read("*a")) +assert(cssfd:close()) +local fd = assert(io.open(string.format("%s/style.css",args.output),"w")) +assert(fd:write(csstxt)) +assert(fd:close()) + +--remove the previous zip archive if it exists +local oldzipfd, err = io.open(args.output,"r") +if oldzipfd then + local rmfd = assert(io.popen(string.format("rm %s.zip",args.output),"r")) + assert(rmfd:read("*a")) + assert(rmfd:close()) +end +local zipfd = assert(io.popen(string.format("zip -r -j %s %s",args.output, args.output),"r")) +assert(zipfd:read("*a")) +assert(zipfd:close()) +db:close() diff --git a/tools/archive/story.etlua b/tools/archive/story.etlua new file mode 100644 index 0000000..664cf8e --- /dev/null +++ b/tools/archive/story.etlua @@ -0,0 +1,23 @@ + + + + "/> + + + + + + +
+

<%- title %>

+

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

+ + <%- text %> +
+ diff --git a/tools/migrate/main.lua b/tools/migrate/main.lua new file mode 100644 index 0000000..f28ffee --- /dev/null +++ b/tools/migrate/main.lua @@ -0,0 +1,22 @@ +local argparse = require("argparse") +local sql = require("lsqlite3") + +local parser = argparse(){ + name = "tools/migrate/main.lua", + description = "Perform database migrations", + epilog = "Other tools included in the smr source distribution include:accounts,archive", +} +parser:help_max_width(80) +parser:option("-d --database","The database file","kore_chroot/data/posts.db") + +args = parser:parse() + +local db,err,errmsg = sql.open(args.database, sql.OPEN_READWRITE) +if not db then + error(string.format("Failed to open %s : %s",args.database,errmsg)) +end + +--Unlisted pastes update +local db:exec([[ +UPDATE posts SET hash=randomblob(64) WHERE hash = -1; +]])