🔧 master: work in progress [might break]
This commit is contained in:
parent
779e101c92
commit
d92cbaf201
11 changed files with 1508 additions and 0 deletions
12
soakbean/middleware/blacklisturl.lua
Normal file
12
soakbean/middleware/blacklisturl.lua
Normal 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
|
||||||
50
soakbean/middleware/cgi.lua
Normal file
50
soakbean/middleware/cgi.lua
Normal 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
|
||||||
408
soakbean/middleware/json.lua
Normal file
408
soakbean/middleware/json.lua
Normal 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
|
||||||
78
soakbean/middleware/rproxy.lua
Normal file
78
soakbean/middleware/rproxy.lua
Normal 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
52
soakbean/src/.init.lua
Normal 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
408
soakbean/src/.lua/json.lua
Normal 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
|
||||||
145
soakbean/src/.lua/opendirectory.lua
Normal file
145
soakbean/src/.lua/opendirectory.lua
Normal 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
|
||||||
227
soakbean/src/.lua/soakbean.lua
Normal file
227
soakbean/src/.lua/soakbean.lua
Normal 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
11
soakbean/src/data.lua
Normal 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
40
soakbean/src/index.html
Normal 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
77
soakbean/src/tests.html
Normal 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>
|
||||||
Loading…
Add table
Reference in a new issue