From 2ce4676d6a645dd6e45a6470397fbc3919d59528 Mon Sep 17 00:00:00 2001 From: Reuh Date: Sat, 10 Dec 2016 20:31:42 +0100 Subject: [PATCH] Wow such commit --- .gitignore | 2 ++ changelog.txt | 4 ++++ config.default.lua | 24 ++++++++++++++++++++++++ vrel.lua | 45 +++++++++++++++++++++++---------------------- 4 files changed, 53 insertions(+), 22 deletions(-) create mode 100644 config.default.lua diff --git a/.gitignore b/.gitignore index 5e2b370..9c300e3 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,3 @@ pygmentize.tmp +database.sqlite3 +config.lua diff --git a/changelog.txt b/changelog.txt index 4980d25..94c026f 100644 --- a/changelog.txt +++ b/changelog.txt @@ -1,3 +1,7 @@ +vrel 0.1.5 (wip): + - Doubled the default maximum lifetime (3 months -> 6 months). + - Added an optional configuration file. + - Various luacheck cleaning (a few potential bugs are fixed). It's still complaining about legit things tho. vrel 0.1.4: - Now stores the sender's IP - Pastes are detroyed with "burn on read" only if the request comes from a different IP diff --git a/config.default.lua b/config.default.lua new file mode 100644 index 0000000..dff759a --- /dev/null +++ b/config.default.lua @@ -0,0 +1,24 @@ +--- Configuration files for vrel. +-- No line count limitation ; vrel will work fine without this, so I guess it's not cheating. +-- In fact, this file will be read by vrel only if named config.lua (and not config.default.lua). +-- If you want to live the full experience, you can easliy find where theses variables are used in vrel.lua and change the default value here +-- instead of using a config.lua file. +-- Also, the comments describing each variable in this file are here solely for practicality purposes ; the comments and code in vrel.lua should +-- be enough to descibe or imply their utility. + +return { + -- Server address to bind + address = "*", + -- TCP port to bind + port = 8155, + -- Maximal lifetime of a paste + maxLifetime = 15552000, -- 6 months + -- Default lifetime of a paste in the web interface + defaultLifetime = 86400, -- 1 day + -- Maximal size of a request/paste + requestMaxDataSize = 15728640, -- 15MB + -- Request timeout + timeout = 1, -- 1 second + -- Debug mode + debug = false +} diff --git a/vrel.lua b/vrel.lua index c4103ce..a25e33b 100644 --- a/vrel.lua +++ b/vrel.lua @@ -1,9 +1,10 @@ #!/bin/lua ---- vrel v0.1.4: online paste service, in 256 lines of Lua (max line lenght = 256 but we shouldn't go this far if not needed). +--- vrel v0.1.5: online paste service, in 256 lines of Lua (max line lenght = 256 but we shouldn't go this far if not needed). -- This module requires LuaSocket 2.0.2, and debug mode requires LuaFileSystem 1.6.3. Install pygmentize for the optional syntax highlighting. -- If you want persistance for paste storage, install lsqlite3. vrel should work with Lua 5.1 to 5.3. +local config = pcall(dofile, "config.lua") or {} -- Basic HTTP server -- -local httpd, requestMaxDataSize = nil, 15728640 -- max post data size (bytes) (15MB) +local httpd, requestMaxDataSize = nil, config.requestMaxDataSize or 15728640 -- max post/paste data size (bytes) (15MB) httpd = { log = function(str, ...) print("["..os.date().."] "..str:format(...)) end, -- log a message (str:format(...)) peername = function(client) return ("%s:%s"):format(client:getpeername()) end, -- returns a nice display name for the client (address:port) @@ -74,8 +75,8 @@ httpd = { if options.debug then httpd.log("Debug mode enabled") server:settimeout(1) -- Enable timeout (don't block forever so we can run debug code) - -- Warp the server object so we can rewrite its functions - local realServer, server = server, setmetatable({}, {__index = function(t, k) return function(_, ...) return realServer[k](realServer, ...) end end}) + local realServer = server + server = setmetatable({}, {__index = function(_, k) return function(_, ...) return realServer[k](realServer, ...) end end}) -- Warp the server object so we can rewrite its functions -- Reload file on change local lfs = require("lfs") local lastModification = lfs.attributes(arg[0]).modification -- current last modification time @@ -111,7 +112,7 @@ httpd = { end if not responded then local page = errorPages["404"] or {"404", {}, "Page not found"} -- simple default 404 page - httpd.sendResponse(client, unpack(type(page) == "table" and page or page(request))) + httpd.sendResponse(client, unpack(type(page) == "table" and page or page(req))) end else httpd.log("%s - Invalid request: %s", httpd.peername(client), err) end end, function(error) return error..debug.traceback("", 2) end) -- add traceback to the error message @@ -119,7 +120,7 @@ httpd = { httpd.log("Internal server error: %s", err) pcall(function() local page = errorPages["500"] or {"500", {}, "Internal server error"} -- simple default 500 page - httpd.sendResponse(client, unpack(type(page) == "table" and page or page(request))) + httpd.sendResponse(client, unpack(type(page) == "table" and page or page())) end) end client:close() @@ -131,20 +132,20 @@ httpd = { } -- Vrel -- -- Load data -local data = {} -- { ["name"] = { expire = os.time()+lifetime, burnOnRead = false, senderIp = "0.0.0.0", data = "Hello\nWorld" } } +local data = {} -- { ["name"] = { expire = os.time()+lifetime, burnOnRead = false, senderId = "someuniqueidentifier", data = "Hello\nWorld" } } local sqliteAvailable, sqlite3 = pcall(require, "lsqlite3") if sqliteAvailable then httpd.log("Using SQlite3 storage backend") -- SQlite backend local db = sqlite3.open("database.sqlite3") - db:exec("CREATE TABLE IF NOT EXISTS data (name STRING PRIMARY KEY NOT NULL, expire INTEGER NOT NULL, burnOnRead INTEGER NOT NULL, senderIp STRING NOT NULL, data STRING NOT NULL)") + db:exec("CREATE TABLE IF NOT EXISTS data (name STRING PRIMARY KEY NOT NULL, expire INTEGER NOT NULL, burnOnRead INTEGER NOT NULL, senderId STRING NOT NULL, data STRING NOT NULL)") setmetatable(data, { __index = function(self, key) -- data[name]: get paste { expire = integer, burnOnRead = boolean, data = string } - local stmt = db:prepare("SELECT expire, burnOnRead, senderIp, data FROM data WHERE name = ?") stmt:bind_values(key) + local stmt = db:prepare("SELECT expire, burnOnRead, senderId, data FROM data WHERE name = ?") stmt:bind_values(key) local r for row in stmt:nrows() do r = row r.burnOnRead = r.burnOnRead == 1 break end stmt:finalize() return r end, __newindex = function(self, key, value) if value ~= nil then -- data[name] = { expire = integer, burnOnRead = boolean, data = string }: add paste - local stmt = db:prepare("INSERT INTO data VALUES (?, ?, ?, ?, ?)") stmt:bind_values(key, value.expire, value.burnOnRead, value.senderIp, value.data) + local stmt = db:prepare("INSERT INTO data VALUES (?, ?, ?, ?, ?)") stmt:bind_values(key, value.expire, value.burnOnRead, value.senderId, value.data) stmt:step() stmt:finalize() else -- data[name] = nil: delete paste local stmt = db:prepare("DELETE FROM data WHERE name = ?") stmt:bind_values(key) @@ -162,7 +163,7 @@ else httpd.log("Using in-memory storage backend") -- In-memory (table) backend end -- Helpers functions local forbiddenName = { ["g"] = true, ["p"] = true } -local function generateName() -- generate a paste name +local function generateName(size) -- generate a paste name. If size ~= nil, will generate a random ID of this lenght. local name = "" repeat local charType, char = math.random() @@ -170,11 +171,11 @@ local function generateName() -- generate a paste name elseif charType < 36/62 then char = math.random(65, 90) -- upper letters (26 possibilities out of 62) else char = math.random(97, 122) end -- lower letters (26 possibilities out of 62) name = name..string.char(char) - until not (data[name] or forbiddenName[name]) + until (not size and not (data[name] or forbiddenName[name])) or (#name >= (size or math.huge)) return name end -local lastClean, cleanInterval = os.time(), 1800 -- last clean time (all time are stored in seconds) and clean interval (30min) -local maxLifetime, defaultLifetime = 7776000, 86400 -- maximum lifetime of a data (3 month) and default (1 day) +local lastClean, cleanInterval = os.time(), config.cleanInterval or 1800 -- last clean time (all time are stored in seconds) and clean interval (30min) +local maxLifetime, defaultLifetime = config.maxLifetime or 15552000, config.defaultLifetime or 86400 -- maximum lifetime of a paste (6 month) and default (1 day) local function clean() -- clean the database each cleanInterval local time = os.time() if lastClean + cleanInterval < time then @@ -186,16 +187,16 @@ local function get(name, request) clean() -- get a paste (returns nil if non-exi if data[name] then local d = data[name] if d.expire < os.time() then data[name] = nil return end - if request.client:getpeername() ~= d.senderIp and d.burnOnRead then data[name] = nil end + if request.client:getpeername() ~= d.senderId and d.burnOnRead then data[name] = nil end -- burn on read (except if retrieved by original poster) return d end end -local function post(paste, request) clean() -- add a paste, will check data and auto-fill defaults; returns name, data table +local function post(paste, request) clean() -- add a paste, will check data and auto-fill defaults; returns name, paste data table local name = generateName() if paste.lifetime then paste.expire = os.time() + (tonumber(paste.lifetime) or defaultLifetime) end paste.expire = math.min(tonumber(paste.expire) or os.time()+defaultLifetime, os.time()+maxLifetime) paste.burnOnRead = paste.burnOnRead == true - paste.senderIp = paste.senderIp or request.client:getpeername() or "0.0.0.0" + paste.senderId = paste.senderId or request.client:getpeername() or "0.0.0.0" paste.data = tostring(paste.data) data[name] = paste return name, data[name] @@ -214,11 +215,11 @@ local function highlight(code, lexer) -- Syntax highlighting; should returns the else return "
"..code:gsub("([\"&<>])",{["\""]=""",["&"]="&",["<"]="<",[">"]=">"}).."
" end end -- Start! -httpd.start("*", 8155, { -- Pages +httpd.start(config.address or "*", config.port or 8155, { -- Pages ["/([^/]*)"] = function(request, name) if forbiddenName[name] then return end if request.method == "POST" and request.post.data then - local name = post({ lifetime = (tonumber(request.post.lifetime) or defaultLifetime/3600)*3600, burnOnRead = request.post.burnOnRead == "on", data = request.post.data }, request) + name = post({ lifetime = (tonumber(request.post.lifetime) or defaultLifetime/3600)*3600, burnOnRead = request.post.burnOnRead == "on", data = request.post.data }, request) return { "303 See Other", {["Location"] = "/"..name}, "" } end return { "200 OK", {["Content-Type"] = "text/html"}, [[ @@ -246,10 +247,10 @@ httpd.start("*", 8155, { -- Pages ["/g/(.+)"] = function(request, name) local d = get(name, request) return d and { "200 OK", {["Content-Type"] = "text"}, d.data } or nil end, ["/p"] = function(request) if request.method == "POST" and request.post.data then - local name, data = post({ lifetime = tonumber(request.post.lifetime) or defaultLifetime, burnOnRead = request.post.burnOnRead == "on", data = request.post.data }, request) - return { "200 OK", {["Content-Type"] = "text/json"}, "{\"name\":\""..name.."\",\"lifetime\":"..data.expire-os.time()..",\"burnOnRead\":"..tostring(data.burnOnRead).."}\n" } + local name, paste = post({ lifetime = tonumber(request.post.lifetime) or defaultLifetime, burnOnRead = request.post.burnOnRead == "on", data = request.post.data }, request) + return { "200 OK", {["Content-Type"] = "text/json"}, "{\"name\":\""..name.."\",\"lifetime\":"..paste.expire-os.time()..",\"burnOnRead\":"..tostring(paste.burnOnRead).."}\n" } end end }, { -- Error pages ["404"] = { "404", {["Content-Type"] = "text/json"}, "{\"error\":\"page not found\"}\n" }, ["500"] = { "500", {["Content-Type"] = "text/json"}, "{\"error\":\"internal server error\"}\n" } -}, { timeout = 1, debug = true }) +}, { timeout = config.timeout or 1, debug = config.debug or false })