diff --git a/soakbean/middleware/blacklisturl.lua b/soakbean/middleware/blacklisturl.lua new file mode 100644 index 0000000..5b4e7fb --- /dev/null +++ b/soakbean/middleware/blacklisturl.lua @@ -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 diff --git a/soakbean/middleware/cgi.lua b/soakbean/middleware/cgi.lua new file mode 100644 index 0000000..5b6782e --- /dev/null +++ b/soakbean/middleware/cgi.lua @@ -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 diff --git a/soakbean/middleware/json.lua b/soakbean/middleware/json.lua new file mode 100644 index 0000000..413f309 --- /dev/null +++ b/soakbean/middleware/json.lua @@ -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 diff --git a/soakbean/middleware/rproxy.lua b/soakbean/middleware/rproxy.lua new file mode 100644 index 0000000..3fd69fe --- /dev/null +++ b/soakbean/middleware/rproxy.lua @@ -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 diff --git a/soakbean/src/.init.lua b/soakbean/src/.init.lua new file mode 100644 index 0000000..f85bbaf --- /dev/null +++ b/soakbean/src/.init.lua @@ -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 redbean 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 diff --git a/soakbean/src/.lua/json.lua b/soakbean/src/.lua/json.lua new file mode 100644 index 0000000..413f309 --- /dev/null +++ b/soakbean/src/.lua/json.lua @@ -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 diff --git a/soakbean/src/.lua/opendirectory.lua b/soakbean/src/.lua/opendirectory.lua new file mode 100644 index 0000000..dc19263 --- /dev/null +++ b/soakbean/src/.lua/opendirectory.lua @@ -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> +-- 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('\r\n') + Write('redbean\r\n') + Write('\r\n') + + Write('

Open Directory Demo

\r\n') + Write('\r\n') + Write('\r\n') + Write('\r\n') + Write('\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('\r\n') + Write('
name\r\n') + Write('type\r\n') + Write('ino\r\n') + Write('off\r\n') + Write('size\r\n') + Write('blocks\r\n') + Write('mode\r\n') + Write('uid\r\n') + Write('gid\r\n') + Write('dev\r\n') + Write('rdev\r\n') + Write('nlink\r\n') + Write('blksize\r\n') + Write('gen\r\n') + Write('flags\r\n') + Write('birthtim\r\n') + Write('mtim\r\n') + Write('atim\r\n') + Write('ctim\r\n') + Write('
') + local link = EscapeHtml(name) + if kind == unix.DT_DIR then link = link .. "/" end + Write('' .. link .. '') + Write('\r\n') + + Write('') + 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('%d\r\n' % {ino}) + Write('%d\r\n' % {off}) + + st,err = unix.stat(path..'/'..name, unix.AT_SYMLINK_NOFOLLOW) + if st then + + Write('%d\r\n' % {st:size()}) + Write('%d\r\n' % {st:blocks()}) + Write('%.7o\r\n' % {st:mode()}) + Write('%d\r\n' % {st:uid()}) + Write('%d\r\n' % {st:gid()}) + Write('%d\r\n' % {st:dev()}) + Write('%d,%d\r\n' % {unix.major(st:rdev()), unix.minor(st:rdev())}) + Write('%d\r\n' % {st:nlink()}) + Write('%d\r\n' % {st:blksize()}) + Write('%d\r\n' % {st:gen()}) + Write('%#x\r\n' % {st:flags()}) + + function WriteTime(unixsec,nanos) + year,mon,mday,hour,min,sec,gmtoffsec = unix.localtime(unixsec) + Write('%.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('%s\r\n' % {err}) + end + ::continue:: + end + Write('
\r\n') + state:close() + return true +end + +return dir diff --git a/soakbean/src/.lua/soakbean.lua b/soakbean/src/.lua/soakbean.lua new file mode 100644 index 0000000..0e53e6d --- /dev/null +++ b/soakbean/src/.lua/soakbean.lua @@ -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 .. " [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 diff --git a/soakbean/src/data.lua b/soakbean/src/data.lua new file mode 100644 index 0000000..99d5d5c --- /dev/null +++ b/soakbean/src/data.lua @@ -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 ) ) diff --git a/soakbean/src/index.html b/soakbean/src/index.html new file mode 100644 index 0000000..3e26292 --- /dev/null +++ b/soakbean/src/index.html @@ -0,0 +1,40 @@ + + + + ${title} + + + + + +
+
+
+
+ +
+

+
${title}
+
+ +
+ Read the documentation here. +
+
+
+
+
+ + + + diff --git a/soakbean/src/tests.html b/soakbean/src/tests.html new file mode 100644 index 0000000..327a070 --- /dev/null +++ b/soakbean/src/tests.html @@ -0,0 +1,77 @@ + + + soakbean tests + + +

+    
+  
+