🔧 master: work in progress [might break]

This commit is contained in:
Leon van Kammen 2026-05-15 23:47:59 +02:00
parent 779e101c92
commit d92cbaf201
11 changed files with 1508 additions and 0 deletions

View file

@ -0,0 +1,12 @@
-- blocks certain url patterns
-- usage:
-- app.use( require("middleware/blacklisturl")({"^/foo","/^bar"}) )
--
return function(urls)
return function(req,res,next)
for k,url in pairs(urls) do
if req.url:match(url) then return SetStatus(403) end
end
next()
end
end

View file

@ -0,0 +1,50 @@
!/usr/local/bin/redlua
-- load the Unix module
local unix = require('unix')
-- load the Redbean module
local red = require('red')
-- define the allowed files
local allowed = {
"./foo",
"/bin/date",
["./foo/index.lua"] = true,
["/bin/date/index.lua"] = true
}
-- get the requested URI from the client
local uri = red.http.getenv("REQUEST_URI")
-- check if the requested URI is allowed
if allowed[uri] == nil then
allowed_uri = uri .. "/index.lua"
if allowed[allowed_uri] == nil then
-- handle 404 error
red.status(404)
red.print('404 Not Found')
return
end
uri = allowed_uri
end
-- check if the requested file is executable
if unix.access(uri, "x") then
-- execute the file and stream its output to the client
red.http.stream(function(writer)
local handle = io.popen('.' .. uri)
local chunk_size = 4096
while true do
local chunk = handle:read(chunk_size)
if chunk == nil then
break
end
writer(chunk)
end
handle:close()
end)
else
-- handle 404 error
red.status(404)
red.print('404 Not Found')
end

View file

@ -0,0 +1,408 @@
--
-- json.lua
--
-- Copyright (c) 2020 rxi
--
-- 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 json = { _version = "0.1.2" }
-------------------------------------------------------------------------------
-- Encode
-------------------------------------------------------------------------------
local encode
local escape_char_map = {
[ "\\" ] = "\\",
[ "\"" ] = "\"",
[ "\b" ] = "b",
[ "\f" ] = "f",
[ "\n" ] = "n",
[ "\r" ] = "r",
[ "\t" ] = "t",
}
local escape_char_map_inv = { [ "/" ] = "/" }
for k, v in pairs(escape_char_map) do
escape_char_map_inv[v] = k
end
local function escape_char(c)
return "\\" .. (escape_char_map[c] or string.format("u%04x", c:byte()))
end
local function encode_nil(val)
return "null"
end
local function encode_table(val, stack)
local res = {}
stack = stack or {}
-- Circular reference?
if stack[val] then error("circular reference") end
stack[val] = true
if rawget(val, 1) ~= nil or next(val) == nil then
-- Treat as array -- check keys are valid and it is not sparse
local n = 0
for k in pairs(val) do
if type(k) ~= "number" then
error("invalid table: mixed or invalid key types")
end
n = n + 1
end
if n ~= #val then
error("invalid table: sparse array")
end
-- Encode
for i, v in ipairs(val) do
table.insert(res, encode(v, stack))
end
stack[val] = nil
return "[" .. table.concat(res, ",") .. "]"
else
-- Treat as an object
for k, v in pairs(val) do
if type(k) ~= "string" then
error("invalid table: mixed or invalid key types")
end
table.insert(res, encode(k, stack) .. ":" .. encode(v, stack))
end
stack[val] = nil
return "{" .. table.concat(res, ",") .. "}"
end
end
local function encode_string(val)
return '"' .. val:gsub('[%z\1-\31\\"]', escape_char) .. '"'
end
local function encode_number(val)
-- Check for NaN, -inf and inf
if val ~= val or val <= -math.huge or val >= math.huge then
error("unexpected number value '" .. tostring(val) .. "'")
end
return string.format("%.14g", val)
end
local type_func_map = {
[ "nil" ] = encode_nil,
[ "table" ] = encode_table,
[ "string" ] = encode_string,
[ "number" ] = encode_number,
[ "boolean" ] = tostring,
}
encode = function(val, stack)
local t = type(val)
local f = type_func_map[t]
if f then
return f(val, stack)
end
error("unexpected type '" .. t .. "'")
end
function json.encode(val)
return ( encode(val) )
end
-------------------------------------------------------------------------------
-- Decode
-------------------------------------------------------------------------------
local parse
local function create_set(...)
local res = {}
for i = 1, select("#", ...) do
res[ select(i, ...) ] = true
end
return res
end
local space_chars = create_set(" ", "\t", "\r", "\n")
local delim_chars = create_set(" ", "\t", "\r", "\n", "]", "}", ",")
local escape_chars = create_set("\\", "/", '"', "b", "f", "n", "r", "t", "u")
local literals = create_set("true", "false", "null")
local literal_map = {
[ "true" ] = true,
[ "false" ] = false,
[ "null" ] = nil,
}
local function next_char(str, idx, set, negate)
for i = idx, #str do
if set[str:sub(i, i)] ~= negate then
return i
end
end
return #str + 1
end
local function decode_error(str, idx, msg)
local line_count = 1
local col_count = 1
for i = 1, idx - 1 do
col_count = col_count + 1
if str:sub(i, i) == "\n" then
line_count = line_count + 1
col_count = 1
end
end
error( string.format("%s at line %d col %d", msg, line_count, col_count) )
end
local function codepoint_to_utf8(n)
-- http://scripts.sil.org/cms/scripts/page.php?site_id=nrsi&id=iws-appendixa
local f = math.floor
if n <= 0x7f then
return string.char(n)
elseif n <= 0x7ff then
return string.char(f(n / 64) + 192, n % 64 + 128)
elseif n <= 0xffff then
return string.char(f(n / 4096) + 224, f(n % 4096 / 64) + 128, n % 64 + 128)
elseif n <= 0x10ffff then
return string.char(f(n / 262144) + 240, f(n % 262144 / 4096) + 128,
f(n % 4096 / 64) + 128, n % 64 + 128)
end
error( string.format("invalid unicode codepoint '%x'", n) )
end
local function parse_unicode_escape(s)
local n1 = tonumber( s:sub(1, 4), 16 )
local n2 = tonumber( s:sub(7, 10), 16 )
-- Surrogate pair?
if n2 then
return codepoint_to_utf8((n1 - 0xd800) * 0x400 + (n2 - 0xdc00) + 0x10000)
else
return codepoint_to_utf8(n1)
end
end
local function parse_string(str, i)
local res = ""
local j = i + 1
local k = j
while j <= #str do
local x = str:byte(j)
if x < 32 then
decode_error(str, j, "control character in string")
elseif x == 92 then -- `\`: Escape
res = res .. str:sub(k, j - 1)
j = j + 1
local c = str:sub(j, j)
if c == "u" then
local hex = str:match("^[dD][89aAbB]%x%x\\u%x%x%x%x", j + 1)
or str:match("^%x%x%x%x", j + 1)
or decode_error(str, j - 1, "invalid unicode escape in string")
res = res .. parse_unicode_escape(hex)
j = j + #hex
else
if not escape_chars[c] then
decode_error(str, j - 1, "invalid escape char '" .. c .. "' in string")
end
res = res .. escape_char_map_inv[c]
end
k = j + 1
elseif x == 34 then -- `"`: End of string
res = res .. str:sub(k, j - 1)
return res, j + 1
end
j = j + 1
end
decode_error(str, i, "expected closing quote for string")
end
local function parse_number(str, i)
local x = next_char(str, i, delim_chars)
local s = str:sub(i, x - 1)
local n = tonumber(s)
if not n then
decode_error(str, i, "invalid number '" .. s .. "'")
end
return n, x
end
local function parse_literal(str, i)
local x = next_char(str, i, delim_chars)
local word = str:sub(i, x - 1)
if not literals[word] then
decode_error(str, i, "invalid literal '" .. word .. "'")
end
return literal_map[word], x
end
local function parse_array(str, i)
local res = {}
local n = 1
i = i + 1
while 1 do
local x
i = next_char(str, i, space_chars, true)
-- Empty / end of array?
if str:sub(i, i) == "]" then
i = i + 1
break
end
-- Read token
x, i = parse(str, i)
res[n] = x
n = n + 1
-- Next token
i = next_char(str, i, space_chars, true)
local chr = str:sub(i, i)
i = i + 1
if chr == "]" then break end
if chr ~= "," then decode_error(str, i, "expected ']' or ','") end
end
return res, i
end
local function parse_object(str, i)
local res = {}
i = i + 1
while 1 do
local key, val
i = next_char(str, i, space_chars, true)
-- Empty / end of object?
if str:sub(i, i) == "}" then
i = i + 1
break
end
-- Read key
if str:sub(i, i) ~= '"' then
decode_error(str, i, "expected string for key")
end
key, i = parse(str, i)
-- Read ':' delimiter
i = next_char(str, i, space_chars, true)
if str:sub(i, i) ~= ":" then
decode_error(str, i, "expected ':' after key")
end
i = next_char(str, i + 1, space_chars, true)
-- Read value
val, i = parse(str, i)
-- Set
res[key] = val
-- Next token
i = next_char(str, i, space_chars, true)
local chr = str:sub(i, i)
i = i + 1
if chr == "}" then break end
if chr ~= "," then decode_error(str, i, "expected '}' or ','") end
end
return res, i
end
local char_func_map = {
[ '"' ] = parse_string,
[ "0" ] = parse_number,
[ "1" ] = parse_number,
[ "2" ] = parse_number,
[ "3" ] = parse_number,
[ "4" ] = parse_number,
[ "5" ] = parse_number,
[ "6" ] = parse_number,
[ "7" ] = parse_number,
[ "8" ] = parse_number,
[ "9" ] = parse_number,
[ "-" ] = parse_number,
[ "t" ] = parse_literal,
[ "f" ] = parse_literal,
[ "n" ] = parse_literal,
[ "[" ] = parse_array,
[ "{" ] = parse_object,
}
parse = function(str, idx)
local chr = str:sub(idx, idx)
local f = char_func_map[chr]
if f then
return f(str, idx)
end
decode_error(str, idx, "unexpected character '" .. chr .. "'")
end
function json.decode(str)
if type(str) ~= "string" then
error("expected argument of type string, got " .. type(str))
end
local res, idx = parse(str, next_char(str, 1, space_chars, true))
idx = next_char(str, idx, space_chars, true)
if idx <= #str then
decode_error(str, idx, "trailing garbage")
end
return res
end
function json.middleware()
local json_response = function(response)
return function()
return function(req,res,next)
if type(res._body) == "table" then
res.header('content-type',"application/json")
res.body( json.encode(res._body) )
end
response(req,res,next)
end
end
end
sb.response = json_response(sb.response())
return function(req,res,next)
if req.method ~= "GET" and req.header['Content-Type']:match("application/json") and GetPayload():sub(0,1) == "{" then
req.body = json.decode( GetPayload() )
end
next()
end
end
return json

View file

@ -0,0 +1,78 @@
--if IsDaemon() then
-- ProgramPort(443)
-- ProgramUid(65534)
-- ProgramUid(65534)
-- ProgramLogPath('/var/log/turfbean.log')
-- ProgramPidPath('/var/log/turfbean.pid')
-- ProgramTrustedIp(ParseIp(Slurp('/etc/justine-ip.txt')), 32);
-- ProgramCertificate(Slurp('/etc/letsencrypt/live/ipv4.games-ecdsa/fullchain.pem'))
-- ProgramPrivateKey(Slurp('/etc/letsencrypt/live/ipv4.games-ecdsa/privkey.pem'))
--end
--
--RELAY_HEADERS_TO_CLIENT = {
-- 'Access-Control-Allow-Origin',
-- 'Cache-Control',
-- 'Connection',
-- 'Content-Type',
-- 'Last-Modified',
-- 'Referrer-Policy',
--}
--
--function OnServerStart()
-- ProgramTokenBucket()
-- assert(unix.setrlimit(unix.RLIMIT_NPROC, 1000, 1000))
--end
--
--function OnWorkerStart()
-- assert(unix.setrlimit(unix.RLIMIT_RSS, 2*1024*1024))
-- assert(unix.setrlimit(unix.RLIMIT_CPU, 2))
-- assert(unix.unveil(nil, nil))
-- assert(unix.pledge("stdio inet unix", nil, unix.PLEDGE_PENALTY_RETURN_EPERM))
--end
--
--function OnHttpRequest()
-- local ip = GetClientAddr()
-- if not IsTrustedIp(ip) then
-- local tok = AcquireToken(ip)
-- if tok < 2 then
-- if Blackhole(ip) then
-- Log(kLogWarn, "banned %s" % {FormatIp(ip)})
-- else
-- Log(kLogWarn, "failed to ban %s" % {FormatIp(ip)})
-- end
-- end
-- if tok < 30 then
-- ServeError(429)
-- SetHeader('Connection', 'close')
-- Log(kLogWarn, "warned %s who has %d tokens" % {FormatIp(ip), tok})
-- return
-- end
-- end
-- local url = 'http://127.0.0.1' .. EscapePath(GetPath())
-- local name = GetParam('name')
-- if name then
-- url = url .. '?name=' .. EscapeParam(name)
-- end
-- local status, headers, body =
-- Fetch(url,
-- {method = GetMethod(),
-- headers = {
-- ['Accept'] = GetHeader('Accept'),
-- ['CF-IPCountry'] = GetHeader('CF-IPCountry'),
-- ['If-Modified-Since'] = GetHeader('If-Modified-Since'),
-- ['Referer'] = GetHeader('Referer'),
-- ['Sec-CH-UA-Platform'] = GetHeader('Sec-CH-UA-Platform'),
-- ['User-Agent'] = GetHeader('User-Agent'),
-- ['X-Forwarded-For'] = FormatIp(ip)}})
-- if status then
-- SetStatus(status)
-- for k,v in pairs(RELAY_HEADERS_TO_CLIENT) do
-- SetHeader(v, headers[v])
-- end
-- Write(body)
-- else
-- local err = headers
-- Log(kLogError, "proxy failed %s" % {err})
-- ServeError(503)
-- end
--end

52
soakbean/src/.init.lua Normal file
View file

@ -0,0 +1,52 @@
package.path = package.path .. ";.lua/?.lua"
package.path = package.path .. ";src/.lua/?.lua"
package.path = package.path .. ";middleware/?.lua"
json = require "json"
-- special script called by main redbean process at startup
HidePath('/usr/share/zoneinfo/')
HidePath('/usr/share/ssl/')
dir = require "opendirectory"
function OnHttpRequest()
if( not dir.serve() ) then
print("default")
Route()
end
end
---- create
--app = require("soakbean") {
-- bin = "./redbean.com",
-- opts = {
-- my_cli_arg=1
-- },
-- cmd={
-- -- runtask = {file="sometask.lua", info="description of cli cmd"}
-- },
-- title = 'SOAKBEAN - a buddy of redbean',
-- subtitle = 'SOAKBEAN makes <a href=https://redbean.dev>redbean</a> programming easy',
-- notes = {'🤩 express-style programming', '🖧 easy routings', '♻ re-use middleware functions'}
--}
--
--app.url['^/data'] = '/data.lua' -- setup custom file endpoint
--
--app.get('^/', app.template('index.html') ) -- alias for app.tpl( LoadAsset('index.html'), app )
--
--app.post('^/save', function(req,res,next) -- setup inline POST endpoint
-- -- also .get(), .put(), .delete(), .options()
-- app.cache = req.body -- middleware auto-decodes json
-- res.status(200)
-- res.body({cache=app.cache}) -- middleware auto-encodes json
-- next()
--end)
--
--app --
--.use( require("json").middleware() ) -- try plug'n'play json API middleware
--.use( app.router( app.url ) ) -- try url router
--.use( app.response() ) -- try serve app response (if any)
--.use( function(req,next) Route() end) -- fallback default redbean fileserver
--
--function OnHttpRequest() app.run() end

408
soakbean/src/.lua/json.lua Normal file
View file

@ -0,0 +1,408 @@
--
-- json.lua
--
-- Copyright (c) 2020 rxi
--
-- 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 json = { _version = "0.1.2" }
-------------------------------------------------------------------------------
-- Encode
-------------------------------------------------------------------------------
local encode
local escape_char_map = {
[ "\\" ] = "\\",
[ "\"" ] = "\"",
[ "\b" ] = "b",
[ "\f" ] = "f",
[ "\n" ] = "n",
[ "\r" ] = "r",
[ "\t" ] = "t",
}
local escape_char_map_inv = { [ "/" ] = "/" }
for k, v in pairs(escape_char_map) do
escape_char_map_inv[v] = k
end
local function escape_char(c)
return "\\" .. (escape_char_map[c] or string.format("u%04x", c:byte()))
end
local function encode_nil(val)
return "null"
end
local function encode_table(val, stack)
local res = {}
stack = stack or {}
-- Circular reference?
if stack[val] then error("circular reference") end
stack[val] = true
if rawget(val, 1) ~= nil or next(val) == nil then
-- Treat as array -- check keys are valid and it is not sparse
local n = 0
for k in pairs(val) do
if type(k) ~= "number" then
error("invalid table: mixed or invalid key types")
end
n = n + 1
end
if n ~= #val then
error("invalid table: sparse array")
end
-- Encode
for i, v in ipairs(val) do
table.insert(res, encode(v, stack))
end
stack[val] = nil
return "[" .. table.concat(res, ",") .. "]"
else
-- Treat as an object
for k, v in pairs(val) do
if type(k) ~= "string" then
error("invalid table: mixed or invalid key types")
end
table.insert(res, encode(k, stack) .. ":" .. encode(v, stack))
end
stack[val] = nil
return "{" .. table.concat(res, ",") .. "}"
end
end
local function encode_string(val)
return '"' .. val:gsub('[%z\1-\31\\"]', escape_char) .. '"'
end
local function encode_number(val)
-- Check for NaN, -inf and inf
if val ~= val or val <= -math.huge or val >= math.huge then
error("unexpected number value '" .. tostring(val) .. "'")
end
return string.format("%.14g", val)
end
local type_func_map = {
[ "nil" ] = encode_nil,
[ "table" ] = encode_table,
[ "string" ] = encode_string,
[ "number" ] = encode_number,
[ "boolean" ] = tostring,
}
encode = function(val, stack)
local t = type(val)
local f = type_func_map[t]
if f then
return f(val, stack)
end
error("unexpected type '" .. t .. "'")
end
function json.encode(val)
return ( encode(val) )
end
-------------------------------------------------------------------------------
-- Decode
-------------------------------------------------------------------------------
local parse
local function create_set(...)
local res = {}
for i = 1, select("#", ...) do
res[ select(i, ...) ] = true
end
return res
end
local space_chars = create_set(" ", "\t", "\r", "\n")
local delim_chars = create_set(" ", "\t", "\r", "\n", "]", "}", ",")
local escape_chars = create_set("\\", "/", '"', "b", "f", "n", "r", "t", "u")
local literals = create_set("true", "false", "null")
local literal_map = {
[ "true" ] = true,
[ "false" ] = false,
[ "null" ] = nil,
}
local function next_char(str, idx, set, negate)
for i = idx, #str do
if set[str:sub(i, i)] ~= negate then
return i
end
end
return #str + 1
end
local function decode_error(str, idx, msg)
local line_count = 1
local col_count = 1
for i = 1, idx - 1 do
col_count = col_count + 1
if str:sub(i, i) == "\n" then
line_count = line_count + 1
col_count = 1
end
end
error( string.format("%s at line %d col %d", msg, line_count, col_count) )
end
local function codepoint_to_utf8(n)
-- http://scripts.sil.org/cms/scripts/page.php?site_id=nrsi&id=iws-appendixa
local f = math.floor
if n <= 0x7f then
return string.char(n)
elseif n <= 0x7ff then
return string.char(f(n / 64) + 192, n % 64 + 128)
elseif n <= 0xffff then
return string.char(f(n / 4096) + 224, f(n % 4096 / 64) + 128, n % 64 + 128)
elseif n <= 0x10ffff then
return string.char(f(n / 262144) + 240, f(n % 262144 / 4096) + 128,
f(n % 4096 / 64) + 128, n % 64 + 128)
end
error( string.format("invalid unicode codepoint '%x'", n) )
end
local function parse_unicode_escape(s)
local n1 = tonumber( s:sub(1, 4), 16 )
local n2 = tonumber( s:sub(7, 10), 16 )
-- Surrogate pair?
if n2 then
return codepoint_to_utf8((n1 - 0xd800) * 0x400 + (n2 - 0xdc00) + 0x10000)
else
return codepoint_to_utf8(n1)
end
end
local function parse_string(str, i)
local res = ""
local j = i + 1
local k = j
while j <= #str do
local x = str:byte(j)
if x < 32 then
decode_error(str, j, "control character in string")
elseif x == 92 then -- `\`: Escape
res = res .. str:sub(k, j - 1)
j = j + 1
local c = str:sub(j, j)
if c == "u" then
local hex = str:match("^[dD][89aAbB]%x%x\\u%x%x%x%x", j + 1)
or str:match("^%x%x%x%x", j + 1)
or decode_error(str, j - 1, "invalid unicode escape in string")
res = res .. parse_unicode_escape(hex)
j = j + #hex
else
if not escape_chars[c] then
decode_error(str, j - 1, "invalid escape char '" .. c .. "' in string")
end
res = res .. escape_char_map_inv[c]
end
k = j + 1
elseif x == 34 then -- `"`: End of string
res = res .. str:sub(k, j - 1)
return res, j + 1
end
j = j + 1
end
decode_error(str, i, "expected closing quote for string")
end
local function parse_number(str, i)
local x = next_char(str, i, delim_chars)
local s = str:sub(i, x - 1)
local n = tonumber(s)
if not n then
decode_error(str, i, "invalid number '" .. s .. "'")
end
return n, x
end
local function parse_literal(str, i)
local x = next_char(str, i, delim_chars)
local word = str:sub(i, x - 1)
if not literals[word] then
decode_error(str, i, "invalid literal '" .. word .. "'")
end
return literal_map[word], x
end
local function parse_array(str, i)
local res = {}
local n = 1
i = i + 1
while 1 do
local x
i = next_char(str, i, space_chars, true)
-- Empty / end of array?
if str:sub(i, i) == "]" then
i = i + 1
break
end
-- Read token
x, i = parse(str, i)
res[n] = x
n = n + 1
-- Next token
i = next_char(str, i, space_chars, true)
local chr = str:sub(i, i)
i = i + 1
if chr == "]" then break end
if chr ~= "," then decode_error(str, i, "expected ']' or ','") end
end
return res, i
end
local function parse_object(str, i)
local res = {}
i = i + 1
while 1 do
local key, val
i = next_char(str, i, space_chars, true)
-- Empty / end of object?
if str:sub(i, i) == "}" then
i = i + 1
break
end
-- Read key
if str:sub(i, i) ~= '"' then
decode_error(str, i, "expected string for key")
end
key, i = parse(str, i)
-- Read ':' delimiter
i = next_char(str, i, space_chars, true)
if str:sub(i, i) ~= ":" then
decode_error(str, i, "expected ':' after key")
end
i = next_char(str, i + 1, space_chars, true)
-- Read value
val, i = parse(str, i)
-- Set
res[key] = val
-- Next token
i = next_char(str, i, space_chars, true)
local chr = str:sub(i, i)
i = i + 1
if chr == "}" then break end
if chr ~= "," then decode_error(str, i, "expected '}' or ','") end
end
return res, i
end
local char_func_map = {
[ '"' ] = parse_string,
[ "0" ] = parse_number,
[ "1" ] = parse_number,
[ "2" ] = parse_number,
[ "3" ] = parse_number,
[ "4" ] = parse_number,
[ "5" ] = parse_number,
[ "6" ] = parse_number,
[ "7" ] = parse_number,
[ "8" ] = parse_number,
[ "9" ] = parse_number,
[ "-" ] = parse_number,
[ "t" ] = parse_literal,
[ "f" ] = parse_literal,
[ "n" ] = parse_literal,
[ "[" ] = parse_array,
[ "{" ] = parse_object,
}
parse = function(str, idx)
local chr = str:sub(idx, idx)
local f = char_func_map[chr]
if f then
return f(str, idx)
end
decode_error(str, idx, "unexpected character '" .. chr .. "'")
end
function json.decode(str)
if type(str) ~= "string" then
error("expected argument of type string, got " .. type(str))
end
local res, idx = parse(str, next_char(str, 1, space_chars, true))
idx = next_char(str, idx, space_chars, true)
if idx <= #str then
decode_error(str, idx, "trailing garbage")
end
return res
end
function json.middleware()
local json_response = function(response)
return function()
return function(req,res,next)
if type(res._body) == "table" then
res.header('content-type',"application/json")
res.body( json.encode(res._body) )
end
response(req,res,next)
end
end
end
sb.response = json_response(sb.response())
return function(req,res,next)
if req.method ~= "GET" and req.header['Content-Type']:match("application/json") and GetPayload():sub(0,1) == "{" then
req.body = json.decode( GetPayload() )
end
next()
end
end
return json

View file

@ -0,0 +1,145 @@
-- Open directory viewer (like apache opendir, based on unix.opendir example)
--
-- Usage:
-- function OnHttpRequest()
-- if( not dir.serve() ) then
-- print("default")
-- Route()
-- end
-- end
--
-- BONUS: mount as a filesystem using rclone:
--
-- $ apt-get install rclone # change this to your distro install cmd
-- $ rclone config
-- n
-- name> redbean
-- type of storage> 33
-- url> https://localhost:8080
-- Edit advanced config? (y/n) y
-- headers> <enter>
-- no_head> false
-- q
--
-- $ mkdir ~/reddrive
-- $ rclone mount redbean:/ ~/reddrive --daemon
-- $ ls ~/reddrive
-- PROFIT! (..shows directory contents..)
--
local dir = {}
dir.serve = function()
local path = '.' .. GetPath()
if not path:find("..") == nil then return ServeError(403,"=]") end
local stat = unix.stat(path)
if stat == nil then
return ServeError(404,"does not exist")
else
if not unix.S_ISDIR( stat:mode() ) then return false end -- let redbean take care of serving static file
end
SetStatus(200)
SetHeader('Content-Type', 'text/html; charset=utf-8')
Write('<!doctype html>\r\n')
Write('<title>redbean</title>\r\n')
Write('<style>\r\n')
Write('* { font-family: courier,sans-serif; }\r\n')
Write('a,a:visited { font-weight:bold; color:#0AF; }\r\n')
Write('td,th { padding: 2px 5px; }\r\n')
Write('td { white-space: nowrap; }\r\n')
Write('.l { text-align: left; }\r\n')
Write('.r { text-align: right; }\r\n')
Write('</style>\r\n')
Write('<h3>Open Directory Demo</h3>\r\n')
Write('<table>\r\n')
Write('<thead>\r\n')
Write('<tr>\r\n')
Write('<th class=l>name\r\n')
Write('<th>type\r\n')
Write('<th class=r>ino\r\n')
Write('<th class=r>off\r\n')
Write('<th class=r>size\r\n')
Write('<th class=r>blocks\r\n')
Write('<th class=r>mode\r\n')
Write('<th class=r>uid\r\n')
Write('<th class=r>gid\r\n')
Write('<th class=r>dev\r\n')
Write('<th class=r>rdev\r\n')
Write('<th class=r>nlink\r\n')
Write('<th class=r>blksize\r\n')
Write('<th class=r>gen\r\n')
Write('<th class=r>flags\r\n')
Write('<th class=r>birthtim\r\n')
Write('<th class=r>mtim\r\n')
Write('<th class=r>atim\r\n')
Write('<th class=r>ctim\r\n')
Write('<tbody>\r\n')
print("opendir: "..path)
state = assert(unix.opendir(path))
for name, kind, ino, off in state do
if name == "." then goto continue end
Write('<tr>\r\n')
Write('<td>')
local link = EscapeHtml(name)
if kind == unix.DT_DIR then link = link .. "/" end
Write('<a href="' .. GetPath() .. link .. '">' .. link .. '</a>')
Write('\r\n')
Write('<td>')
if kind == unix.DT_REG then Write('DT_REG')
elseif kind == unix.DT_DIR then Write('DT_DIR')
elseif kind == unix.DT_FIFO then Write('DT_FIFO')
elseif kind == unix.DT_CHR then Write('DT_CHR')
elseif kind == unix.DT_BLK then Write('DT_BLK')
elseif kind == unix.DT_LNK then Write('DT_LNK')
elseif kind == unix.DT_SOCK then Write('DT_SOCK')
else Write('DT_UNKNOWN')
end
Write('\r\n')
Write('<td class=r>%d\r\n' % {ino})
Write('<td class=r>%d\r\n' % {off})
st,err = unix.stat(path..'/'..name, unix.AT_SYMLINK_NOFOLLOW)
if st then
Write('<td class=r>%d\r\n' % {st:size()})
Write('<td class=r>%d\r\n' % {st:blocks()})
Write('<td class=r>%.7o\r\n' % {st:mode()})
Write('<td class=r>%d\r\n' % {st:uid()})
Write('<td class=r>%d\r\n' % {st:gid()})
Write('<td class=r>%d\r\n' % {st:dev()})
Write('<td class=r>%d,%d\r\n' % {unix.major(st:rdev()), unix.minor(st:rdev())})
Write('<td class=r>%d\r\n' % {st:nlink()})
Write('<td class=r>%d\r\n' % {st:blksize()})
Write('<td class=r>%d\r\n' % {st:gen()})
Write('<td class=r>%#x\r\n' % {st:flags()})
function WriteTime(unixsec,nanos)
year,mon,mday,hour,min,sec,gmtoffsec = unix.localtime(unixsec)
Write('<td class=r>%.4d-%.2d-%.2dT%.2d:%.2d:%.2d.%.9d%+.2d%.2d\r\n' % {
year, mon, mday, hour, min, sec, nanos,
gmtoffsec / (60 * 60), math.abs(gmtoffsec) % 60})
end
WriteTime(st:birthtim())
WriteTime(st:mtim())
WriteTime(st:atim())
WriteTime(st:ctim())
else
Write('<td class=l colspan=15>%s\r\n' % {err})
end
::continue::
end
Write('</table>\r\n')
state:close()
return true
end
return dir

View file

@ -0,0 +1,227 @@
local json = require "json"
function error(str) print("[error] " .. str) end
function keys(table) local i = 0 ; for k, v in pairs(table) do i = i + 1 end ; return i end
sb = {}
sb = {
middleware={},
handler={},
data={},
charset="utf-8",
__index=function(self,k,v)
if sb.data[k] then return sb.data[k] end
if sb[k] then return sb[k] end
return rawget(self,k)
end,
__newindex=function(self,k,v)
if type(v) == "function" then
sb.data[k] = (function(k,v)
return function(a,b,c,d,e,f)
v(a,b,c,d,e,f)
sb.pub(k,v)
end
end)(k,v)
else
sb.data[k] = v
sb.pub(k,v)
end
end,
on = function(k,f)
sb.handler[k] = sb.handler[k] or {}
table.insert( sb.handler[k], f )
return sb.app
end,
pub = function(k,v)
if sb.handler[k] ~= nil then
for i,handler in pairs(sb.handler[k]) do
coroutine.wrap(handler)(v,k)
end
end
end,
-- print( util.tpl("${name} is ${value}", {name = "foo", value = "bar"}) )
-- "foo is bar"
tpl = function(s,tab)
return (s:gsub('($%b{})', function(w) return tab[w:sub(3, -2)] or w end))
end,
write = function(str)
Write( sb.app.tpl(str,sb.app.data) )
end,
job = function(app)
return function(cfg)
-- app.
return app
end
end,
request = function(method, app)
return function(path,f)
app.use( function(req,res,next)
if req.url:match(path) and req.method == method then
f(req,res,next)
else next() end
end)
end
end,
use = function(f)
table.insert(sb.middleware,f)
return sb.app
end,
useDefaults = function(app)
app.use( function(req,res,next)
app.pub(req.url,{req=req,res=res})
next()
end)
end,
run = function(req)
local next = function() end
local k = 0
local req = {
param={},
method=GetMethod(),
host=GetHost(),
header=GetHeaders(),
url=GetPath(),
protocol=GetScheme(),
body={}
}
local res={
_status=nil,
_header={},
_body=""
}
local params = GetParams()
if params ~= nil then
for i,p in pairs(params) do req.param[ p[1] ] = p[2] end
end
res.status = function(status) res._status = status ; sb.pub('res.status',status) end
res.body = function(v) res._body = v ; sb.pub('req.body' ,body) end
res.header = function(k,v)
if v == nil then return res._header[k] end
res._header[k] = v
sb.pub('res.header',{key=k,value=v})
end
res.header("content-type","text/html")
-- run middleware
next = function()
k = k+1
if type(sb.middleware[k]) == "function" then sb.middleware[k](req,res,next) end
end
next()
end,
json = function()
local json_response = function(response)
return function()
return function(req,res,next)
if type(res._body) == "table" then
res.header('content-type',"application/json")
res.body( json.encode(res._body) )
end
response(req,res,next)
end
end
end
sb.response = json_response(sb.response())
return function(req,res,next)
if req.method ~= "GET" and req.header['Content-Type']:match("application/json") and GetPayload():sub(0,1) == "{" then
req.body = json.decode( GetPayload() )
end
next()
end
end,
response = function()
return function(req,res,next)
if res._body ~= nil and res._status ~= nil and res._header['content-type'] ~= nil then
SetStatus(res._status)
for k,v in pairs(res._header) do
if k == "content-type" then v = v .. "; charset=" .. sb.charset end
SetHeader(k,v)
end
if type(res._body) == "string" then
Write( res._body )
else print("[ERROR] res.body is not a string (HINT: use json middleware)") end
else next() end
end
end,
router = function(router)
return function(req,res,next)
for p1, p2 in pairs(router) do
if GetPath():match(p1) then
if type(p2) == "string" then
print("router: " .. p1 .. " => " .. p2)
RoutePath(p2)
end
if type(p2) == "function" then p2(req,res,next) end
sb.pub(p1,req)
sb.pub(p2,req)
return true
end
end
next()
end
end,
template = function(file)
return function(req,res,next)
res.status(200)
res.header('content-type','text/html')
res.body( sb.app.tpl( LoadAsset(file), sb.app ) )
next()
end
end,
init = function(app)
for k,v in pairs(argv) do
app.opts[ v:gsub("=.*","") ] = v:gsub(".*=","")
end
if( keys(app.cmd) > 0 ) then sb.runcmd(app) end
end,
runcmd = function(app)
for k,v in pairs(app.opts) do
if app.cmd[k] then
local file = app.cmd[k].file
return require( file:sub(0,-5) )(app,argv)
end
end
print("\nUsage: " .. app.bin .. " <cmd> [opts]\n\n")
for k,v in pairs(app.cmd) do
print("\t" .. app.bin .. " " .. k .. "\t\t" .. v.info )
end
print("")
os.exit()
end
}
return function(data)
local app = {}
setmetatable(app,sb)
sb.app = app
sb.data = data
sb.data.url = {}
if data.url == nil then sb.data.url = {} end
sb.get = sb.request('GET', app)
sb.post = sb.request('POST', app)
sb.put = sb.request('PUT', app)
sb.options = sb.request('OPTIONS', app)
sb.delete = sb.request('DELETE', app)
sb.init(app)
sb.useDefaults(app)
return app
end

11
soakbean/src/data.lua Normal file
View file

@ -0,0 +1,11 @@
-- most usecases will just do fine with the global soakbean app in .init.lua
-- however, file-endpoints can initialize separated soakbean apps
-- or just use direct Redbean calls.
local json = require("json")
local app2 = require("soakbean") -- in theory you could setup a separate instance here
SetStatus(200)
SetHeader('Content-Type', 'application/json; charset=utf-8')
local data = {}
for k,v in pairs({"notes","title"}) do data[v] = app[v] end
Write( json.encode( data ) )

40
soakbean/src/index.html Normal file
View file

@ -0,0 +1,40 @@
<!DOCTYPE html>
<html>
<head>
<title>${title}</title>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<!-- see docs @ https://alpinejs.dev -->
<script src="//unpkg.com/alpinejs" defer></script>
<body x-data>
<div style="position:relative">
<div class="container" style="height:99vh;width:400px;margin:0 auto;margin-top:40px;">
<center>
<img src="https://github.com/coderofsalvation/soakbean/raw/master/.dtp/soakbean.jpg"/><br>
<img src="https://github.com/coderofsalvation/soakbean/raw/master/.dtp/soakbean.gif" style="width:100%"/>
</center>
<br><br>
<div class="title">${title}</div>
<br>
<template x-for="note in $store.app.notes">
<div><b x-text="note"></b></div>
</template>
<br>
Read <a href="https://github.com/coderofsalvation/soakbean" target="_blank">the documentation here</a>.
<div style="position:absolute;bottom:150px">
<center x-text="$store.app.time"></center>
</div>
</div>
</div>
<script>
document.addEventListener('alpine:init', () => {
Alpine.store('app', {notes:[],title:''}) // expected schema
fetch('/data')
.then( (res) => res.json() )
.then( (app) => Alpine.store('app',app) )
setInterval( () => Alpine.store('app').time = new Date(), 300 )
})
</script>
<style type="text/css"> * { font-family:helvetica; color:#555; }</style>
</body>
</html>

77
soakbean/src/tests.html Normal file
View file

@ -0,0 +1,77 @@
<html>
<head>
<title>soakbean tests</title>
</head>
<body style="margin:40px">
<pre style="border-radius:5px; padding:10px; background:#333; color:AAE; height:98vh; width:100%;" id="console"></pre>
<script>
function test(opts){
var $console = document.querySelector("#console");
var printf = (str) => $console.innerHTML += str + "\n";
this.node = typeof process != undefined && typeof process != "undefined"
this.tests = this.tests || []
this.errors = 0
this.error = (msg) => { this.errors += 1; printf("[E] error: "+msg) }
this.add = (description, cb) => this.tests.push({ description, cb })
this.done = (ready) => { printf("\n> tests : "+this.tests.length+"\n> errors: "+this.errors); if( this.node ) process.exit( this.errors == 0 ? 0 : 1); ready(this) }
this.run = (ready) => {
var p = Promise.resolve()
var runTest = (i) => {
return new Promise( (resolve, reject) => {
var test = this.tests[i]
if( !test ) return this.done(ready)
if( this.node ) printf("[ ] "+test.description)
var onError = (err) => { this.error(err); this.done(ready) }
var _next = () => { printf("[✓] "+test.description); p.then(runTest(i+1)) }
try { test.cb(_next, onError ) } catch (e) { onError(e) }
})
}
p.then( runTest(0) )
}
return this
}
var t = new test()
t.add("testing index.html", function(next, error){
fetch('/index.html')
.then( (res) => res.text() )
.then( (html) => {
if( !html.match(/<img/) ) error("no images found")
next()
})
.catch( error )
})
t.add("testing POST /save", function(next, error){
fetch('/save',{
method:"POST",
headers:{ "content-type":"application/json" },
body: JSON.stringify({foo:123})
})
.then( (res) => res.json() )
.then( (json) => {
if( json.cache && json.cache.foo == 123 ) next()
else throw 'expected {cache:{foo:123}}'
})
.catch( error )
})
t.add("testing GET /data", function(next, error){
fetch('/data',{
method:"GET",
headers:{ "content-type":"application/json" }
})
.then( (res) => res.json() )
.then( (json) => {
if( json.notes && json.notes.length == 3 ) next()
else throw 'expected array with 3 items'
})
.catch( error )
})
t.run( () => console.log("done") )
</script>
</body>
</html>