├── .gitignore ├── README.md ├── TODO.md ├── bootstrap.lua ├── config.lua ├── index.lua ├── lib ├── estrela │ ├── cli.lua │ ├── io │ │ ├── ob.lua │ │ └── pprint.lua │ ├── log │ │ ├── common.lua │ │ ├── file.lua │ │ └── ngx.lua │ ├── ngx │ │ ├── app.lua │ │ ├── request.lua │ │ ├── response.lua │ │ ├── response │ │ │ └── cookie.lua │ │ ├── router.lua │ │ ├── session │ │ │ └── engine │ │ │ │ └── common.lua │ │ └── tmpl │ │ │ ├── 404.html │ │ │ └── 500.html │ ├── oop │ │ ├── nano.lua │ │ └── single.lua │ ├── storage │ │ └── engine │ │ │ ├── common.lua │ │ │ └── shmem.lua │ ├── util │ │ ├── env.lua │ │ ├── path.lua │ │ ├── string.lua │ │ └── table.lua │ └── web.lua └── resty │ ├── README.md │ └── upload.lua ├── nginx.conf.example ├── tester.lua └── tests ├── 00_check.lua ├── 01_util_table.lua ├── 02_util_string.lua ├── 03_util_path.lua ├── 04_util_env.lua ├── 05_io_ob.lua ├── 06_io_pprint.lua ├── 07_log_common.lua ├── 08_log_file.lua ├── 09_log_ngx.lua ├── 10_storage_engine_common.lua ├── 11_storage_engine_shmem.lua ├── 12_ngx_response_cookie.lua └── tester ├── cli.lua ├── common.lua └── ngx.lua /.gitignore: -------------------------------------------------------------------------------- 1 | .idea/ 2 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | estrela 2 | ======= 3 | 4 | Lua framework for nginx (in early stage of development) 5 | 6 | ### In a nutshell 7 | 8 | **bootstrap.lua** 9 | ```lua 10 | local PP = require('estrela.io.pprint').print 11 | 12 | return require('estrela.web').App { 13 | ['$'] = function(app, req, resp) 14 | app:defer(function() 15 | ngx.say 'Hello from defer!' 16 | end) 17 | 18 | local S = app.session:start() 19 | 20 | local cnt = tonumber(S.cnt) or 0 21 | ngx.say('Hello world ', cnt, ' times') 22 | S.cnt = cnt + 1 23 | 24 | app.session:stop() 25 | 26 | return app:forward('/boo') 27 | end, 28 | 29 | [{account = '/:name$', profile = '/profile/:name$'}] = function(app) 30 | local url = app.router:urlFor(app.route.name, app.route.params) 31 | ngx.say('Hello from ', app.req.method, ' ', url) 32 | end, 33 | 34 | ['/boo'] = { 35 | GET = function(app, req) 36 | ngx.say 'def GET' 37 | PP(req.GET) 38 | PP(req.COOKIE) 39 | end, 40 | 41 | POST = function(app, req) 42 | ngx.say 'def POST' 43 | PP(req.POST) 44 | PP(req.FILES) 45 | end, 46 | 47 | [{'HEAD', 'OPTIONS'}] = function() 48 | ngx.status = 403 49 | ngx.say 'Go home!' 50 | return ngx.exit(0) 51 | end, 52 | }, 53 | 54 | ['/redirect'] = function() 55 | return app:redirect('http://ater.me/') 56 | end, 57 | 58 | ['/external'] = 'ourcoolsite.routes', -- performs require('ourcoolsite.routes') 59 | 60 | ['/fail$'] = function() 61 | -- this function is not defined => throw a HTTP error #500 62 | -- error will be handled in route 500 below 63 | fooBar() 64 | end, 65 | 66 | [404] = function(app) 67 | ngx.say 'Route is not found' 68 | --return app:defaultErrorPage() 69 | end, 70 | 71 | [500] = function(app, req) 72 | ngx.say('Ooops in ', req.url, '\n', SP(app.error)) 73 | --return true 74 | end, 75 | } 76 | 77 | app.router:mount('/admin', { 78 | ['/:action/do$'] = function(app) 79 | if not user_function_for_check_auth() then 80 | return app:abort(404, 'Use cookie, Luke!') 81 | end 82 | ngx.say('admin do ', app.route.params.action, '
') 83 | end, 84 | }) 85 | 86 | app.trigger.before_req:add(function(app, req, resp) 87 | resp.headers.content_type = 'text/plain' 88 | 89 | app.log.debug('New request ', app.req.url) 90 | 91 | if req.GET._method then 92 | req.method = req.GET._method:upper() 93 | end 94 | end) 95 | 96 | app.trigger.after_req:add(function() 97 | ngx.say 'Goodbye!' 98 | end) 99 | ``` 100 | 101 | **nginx.conf** 102 | ```nginx 103 | http { 104 | lua_package_path '/path/to/lua/?.lua;/path/to/lua/lib/?.lua;;'; 105 | 106 | server { 107 | location / { 108 | content_by_lua_file /path/to/lua/index.lua; 109 | } 110 | } 111 | } 112 | ``` 113 | 114 | **index.lua** 115 | ```lua 116 | return xpcall( 117 | function() 118 | local app = require('bootstrap') 119 | 120 | local configurator = require('config') 121 | if type(configurator) == 'function' then 122 | configurator(app.config) 123 | end 124 | 125 | return app:serve() 126 | end, 127 | function(err) 128 | ngx.log(ngx.ERR, err) 129 | ngx.status = 500 130 | ngx.print 'Ooops! Something went wrong' 131 | return ngx.exit(0) 132 | end 133 | ) 134 | ``` 135 | 136 | **config.lua** 137 | ```lua 138 | return function(cfg) 139 | -- Для вывода подробного описания ошибок (если не объявлен 500 роут) 140 | cfg.debug = true 141 | 142 | -- Разрешаем использовать сессии. Без этого app.session использовать нельзя 143 | cfg.session.active = true 144 | 145 | -- nginx.conf "location /estrela {" 146 | -- Если не указать, то пути маршрутизации должны быть полными: ['/estrela/$'], ['/estrela/do/:action'], etc. 147 | cfg.router.pathPrefix = '/estrela' 148 | 149 | -- Вместо логирования в nginx error_log (стандартное поведение) пишем в отдельный файл 150 | cfg.error_logger = require('estrela.log.file'):new('/tmp/estrela.error.log') 151 | end 152 | ``` 153 | -------------------------------------------------------------------------------- /TODO.md: -------------------------------------------------------------------------------- 1 | TODO (список без приоритетов) 2 | ==== 3 | 4 | * хранение шифрованной сессии прямо в куках 5 | * тесты 6 | * документация 7 | * проверить keep-alive с post запросами 8 | * опциональные именованные параметры роутов 9 | * префиксы имен роутов при монтировании для решения проблемы возможного конфликта имен для urlFor 10 | * regexp ограничения на значения именованных параметров 11 | * flash сообщения 12 | * доработать app.trigger для произвольного расширения прикладным кодом 13 | * table.slice 14 | * обойтись в util.string.trim без вызовов ltrim+rtirm 15 | -------------------------------------------------------------------------------- /bootstrap.lua: -------------------------------------------------------------------------------- 1 | local app = require('estrela.web').App { 2 | ['$'] = function(app) 3 | local S = app.session:start() 4 | 5 | local cnt = tonumber(S.cnt) or 0 6 | ngx.say('Hello world ', cnt, ' times') 7 | S.cnt = cnt + 1 8 | 9 | app.session:stop() 10 | end, 11 | } 12 | 13 | app.trigger.before_req:add(function(app) 14 | app.resp.headers.content_type = 'text/plain' 15 | end) 16 | 17 | app.trigger.after_req:add(function() 18 | ngx.say 'Goodbye!' 19 | end) 20 | 21 | return app 22 | -------------------------------------------------------------------------------- /config.lua: -------------------------------------------------------------------------------- 1 | return function(cfg) 2 | -- Для вывода подробного описания ошибок (если не объявлен 500 роут) 3 | cfg.debug = true 4 | 5 | -- Разрешаем использовать сессии. Без этого app.session использовать нельзя 6 | cfg.session.active = true 7 | 8 | -- nginx.conf "location /estrela {" 9 | -- Если не указать, то пути маршрутизации должны быть полными: ['/estrela/$'], ['/estrela/do/:action'], etc. 10 | cfg.router.pathPrefix = '/estrela' 11 | 12 | -- Вместо логирования в nginx error_log (стандартное поведение) пишем в отдельный файл 13 | cfg.error_logger = require('estrela.log.file'):new('/tmp/estrela.error.log') 14 | end 15 | -------------------------------------------------------------------------------- /index.lua: -------------------------------------------------------------------------------- 1 | return xpcall( 2 | function() 3 | local app = require('bootstrap') 4 | 5 | local configurator = require('config') 6 | if type(configurator) == 'function' then 7 | configurator(app.config) 8 | end 9 | 10 | return app:serve() 11 | end, 12 | function(err) 13 | ngx.log(ngx.ERR, err) 14 | ngx.status = 500 15 | ngx.print 'Ooops! Something went wrong' 16 | return ngx.exit(0) 17 | end 18 | ) 19 | -------------------------------------------------------------------------------- /lib/estrela/cli.lua: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/atercattus/estrela/88ac4d6c9c12998c14568c3b5d10f594802f5b20/lib/estrela/cli.lua -------------------------------------------------------------------------------- /lib/estrela/io/ob.lua: -------------------------------------------------------------------------------- 1 | local ipairs = ipairs 2 | local pairs = pairs 3 | local table_concat = table.concat 4 | local table_insert = table.insert 5 | local tostring = tostring 6 | 7 | local buf = {} 8 | 9 | local _direct_print = ngx and ngx.print or io.write 10 | 11 | local _orig_print, _orig_io_write, _orig_ngx_say, _orig_ngx_print 12 | 13 | local M = {} 14 | 15 | function M.print(...) 16 | if #buf == 0 then 17 | for _, v in pairs({...}) do 18 | _direct_print(tostring(v)) 19 | end 20 | else 21 | local len = 0 22 | local _buf = buf[#buf] 23 | for _, v in pairs({...}) do 24 | local chunk = tostring(v) 25 | if _buf['cb'] then 26 | chunk = _buf['cb'](chunk) 27 | end 28 | len = len + #chunk 29 | table_insert(_buf['buf'], chunk) 30 | end 31 | _buf['len'] = _buf['len'] + len 32 | end 33 | end 34 | 35 | function M.println(...) 36 | M.print(...) 37 | M.print('\n') 38 | end 39 | 40 | function M.print_lua(...) 41 | for i, v in pairs({...}) do 42 | if i > 1 then 43 | M.print('\t') 44 | end 45 | M.print(v) 46 | end 47 | M.print('\n') 48 | end 49 | 50 | local function _set_hooks() 51 | _orig_print, print = print, M.print_lua 52 | _orig_io_write, io.write = io.write, M.print 53 | 54 | if ngx then 55 | _orig_ngx_say, ngx.say = ngx.say, M.println 56 | _orig_ngx_print, ngx.print = ngx.print, M.print 57 | end 58 | end 59 | 60 | local function _restore_hooks() 61 | print = _orig_print 62 | io.write = _orig_io_write 63 | 64 | if ngx then 65 | ngx.say = _orig_ngx_say 66 | ngx.print = _orig_ngx_print 67 | end 68 | end 69 | 70 | function M.start(cb) 71 | if #buf == 0 then 72 | _set_hooks() 73 | end 74 | 75 | buf[#buf + 1] = { 76 | buf = {}, 77 | len = 0, 78 | cb = cb, 79 | } 80 | end 81 | 82 | function M.finish() 83 | buf[#buf] = nil 84 | 85 | if #buf == 0 then 86 | _restore_hooks() 87 | end 88 | end 89 | 90 | function M.flush() 91 | if #buf > 0 then 92 | local b, cb = table_concat(buf[#buf]['buf']), buf[#buf]['cb'] 93 | M.finish() 94 | M.print(b) 95 | M.start(cb) 96 | end 97 | end 98 | 99 | function M.flush_finish() 100 | if #buf > 0 then 101 | local b = table_concat(buf[#buf]['buf']) 102 | M.finish() 103 | M.print(b) 104 | end 105 | end 106 | 107 | function M.get() 108 | return (#buf > 0) and table_concat(buf[#buf]['buf']) or nil 109 | end 110 | 111 | function M.levels() 112 | return #buf 113 | end 114 | 115 | function M.len() 116 | return (#buf > 0) and buf[#buf]['len'] or nil 117 | end 118 | 119 | function M.clean() 120 | if #buf > 0 then 121 | buf[#buf]['buf'], buf[#buf]['len'] = {}, 0 122 | end 123 | end 124 | 125 | function M.status() 126 | if #buf == 0 then 127 | return {} 128 | end 129 | 130 | local res = {} 131 | 132 | for _,lvl in ipairs(buf) do 133 | table_insert(res, { 134 | len = lvl['len'], 135 | cb = lvl['cb'], 136 | }) 137 | end 138 | 139 | return res 140 | end 141 | 142 | return M 143 | -------------------------------------------------------------------------------- /lib/estrela/io/pprint.lua: -------------------------------------------------------------------------------- 1 | local require = require 2 | 3 | local debug = require('debug') 4 | local debug_getinfo = debug.getinfo 5 | local pairs = pairs 6 | local string_format = string.format 7 | local string_gsub = string.gsub 8 | local string_rep = string.rep 9 | local table_concat = table.concat 10 | local table_insert = table.insert 11 | local table_sort = table.sort 12 | local tonumber = tonumber 13 | local tostring = tostring 14 | local type = type 15 | 16 | local M = {} 17 | 18 | local T = require('estrela.util.table') 19 | local T_clone = T.clone 20 | 21 | local PATH = require('estrela.util.path') 22 | local path_rel = PATH.rel 23 | 24 | local function sort_cb(a, b) 25 | if type(a) == 'number' and type(b) == 'number' then 26 | return a < b 27 | else 28 | return tostring(a) < tostring(b) 29 | end 30 | end 31 | 32 | local function prepare_string(s) 33 | -- спец обработка табуляции для сохранения ее в строке в форме \t 34 | s = string_gsub(s, '\t', [[\t]]) 35 | s = string_format('%q', s) 36 | s = string_gsub(s, [[\\t]], [[\t]]) 37 | return s 38 | end 39 | 40 | function M.sprint(v, opts) 41 | opts = opts or {} 42 | 43 | -- максимальная глубина обхода вложенных структур 44 | local max_depth = tonumber(opts.max_depth) or 100 45 | -- компактный вывод (без табуляций, новых строк) 46 | local compact = opts.compact 47 | -- выводить ли для функций место (файл:строка) их объявления 48 | local func_pathes = opts.func_pathes == nil and true or opts.func_pathes 49 | -- выводить ли сокращенные (относительные) пути при func_pathes 50 | local func_patches_short = opts.func_patches_short == nil and true or opts.func_patches_short 51 | -- на сколько уровней вверх по файловой системе можно подниматься, если функция была объявлена выше корня фреймворка 52 | local func_patches_short_rel = tonumber(opts.func_patches_short_rel) or 4 53 | 54 | local env_root 55 | if func_pathes and func_patches_short then 56 | env_root = require('estrela.util.env').get_root() 57 | end 58 | 59 | local key_opts = T_clone(opts) 60 | key_opts.compact = true 61 | 62 | local result = {} 63 | 64 | local visited = {} 65 | 66 | local function _pretty_print(name, v, path) 67 | 68 | local prefix = compact and '' or string_rep('\t', #path) 69 | 70 | table_insert(result, prefix) 71 | if #name > 0 then 72 | table_insert(result, name .. ' ') 73 | end 74 | 75 | local type_v = type(v) 76 | if type_v == 'string' then 77 | table_insert(result, prepare_string(v)) 78 | elseif type_v == 'number' or type_v == 'boolean' then 79 | table_insert(result, tostring(v)) 80 | elseif type_v == 'table' then 81 | if #path <= max_depth then 82 | local table_id = tostring(v) 83 | if visited[table_id] then 84 | table_insert(result, 'nil --[[recursion to ' .. visited[table_id] .. ']]') 85 | else 86 | visited[table_id] = table_concat(path, '.') 87 | 88 | local keys = {} 89 | local only_numbers = true 90 | for key, _ in pairs(v) do 91 | table_insert(keys, key) 92 | only_numbers = only_numbers and (type(key) == 'number') 93 | end 94 | 95 | table_sort(keys, only_numbers and nil or sort_cb) 96 | 97 | table_insert(result, compact and '{' or '{\n') 98 | for _, key in pairs(keys) do 99 | local _key 100 | if type(key) == 'string' then 101 | _key = prepare_string(tostring(key)) 102 | elseif type(key) == 'number' then 103 | _key = tostring(key) 104 | else 105 | local _max_depth = key_opts.max_depth 106 | key_opts.max_depth = max_depth - #path - 1 107 | _key = M.sprint(key, key_opts) 108 | key_opts.max_depth = _max_depth 109 | end 110 | table_insert(path, _key) 111 | _pretty_print('[' .. _key .. '] =', v[key], path) 112 | path[#path] = nil 113 | end 114 | table_insert(result, prefix .. '}') 115 | end 116 | else 117 | table_insert(result, 'nil --[[max_depth cutted]]') 118 | end 119 | elseif v == nil then 120 | table_insert(result, 'nil') 121 | else 122 | local _v 123 | if type_v == 'function' then 124 | if func_pathes then 125 | local func_info = debug_getinfo(v) 126 | local path = func_info.short_src 127 | if func_patches_short then 128 | if path:find(env_root, 1, true) == 1 then 129 | path = '...' .. path:sub(env_root:len()+1) 130 | elseif func_patches_short_rel > 0 then 131 | local _path, _rel_len = path_rel(env_root, path) 132 | if _rel_len <= func_patches_short_rel then 133 | path = '.../' .. _path 134 | end 135 | end 136 | end 137 | local line = func_info.linedefined 138 | line = (line >= 0) and (':' .. line) or '' 139 | 140 | _v = tostring(v) .. ' ' .. path .. line 141 | else 142 | _v = tostring(v) 143 | end 144 | else 145 | _v = type_v 146 | end 147 | table_insert(result, 'nil --[[' .. _v .. ']]') 148 | end 149 | 150 | if #path > 0 then 151 | table_insert(result, ',') 152 | end 153 | 154 | if not compact then 155 | table_insert(result, '\n') 156 | end 157 | end 158 | 159 | _pretty_print('', v, {}) 160 | 161 | return table_concat(result) 162 | end 163 | 164 | function M.print(v, opts) 165 | opts = opts or {} 166 | local writer = opts.writer or (ngx and ngx.print or io.write); 167 | writer(M.sprint(v, opts)) 168 | end 169 | 170 | return M 171 | -------------------------------------------------------------------------------- /lib/estrela/log/common.lua: -------------------------------------------------------------------------------- 1 | local error = error 2 | local debug_traceback = debug.traceback 3 | local table_concat = table.concat 4 | local table_insert = table.insert 5 | 6 | local S = require('estrela.util.string') 7 | local S_split = S.split 8 | local S_trim = S.trim 9 | 10 | local M = {} 11 | 12 | function M:new() 13 | local L = { 14 | DEBUG = 8, 15 | INFO = 7, 16 | NOTICE = 6, 17 | WARN = 5, 18 | ERR = 4, 19 | CRIT = 3, 20 | ALERT = 2, 21 | EMERG = 1, 22 | 23 | DISABLE = 0, -- для выключения логирования 24 | } 25 | 26 | L.level = L.DEBUG -- выводить все ошибки 27 | L.bt_level = L.WARN -- не выводить backtrace для некритичных ошибок 28 | 29 | L.level_names = { 30 | [L.DEBUG] = 'DEBUG', 31 | [L.INFO] = 'INFO', 32 | [L.NOTICE] = 'NOTICE', 33 | [L.WARN] = 'WARN', 34 | [L.ERR] = 'ERR', 35 | [L.CRIT] = 'CRIT', 36 | [L.ALERT] = 'ALERT', 37 | [L.EMERG] = 'EMERG', 38 | 39 | [L.DISABLE] = 'DISABLE', 40 | } 41 | 42 | local function _get_bt(bt_lvl) 43 | if L.bt_level >= bt_lvl then 44 | local _bt = S_split(debug_traceback(), '\n') 45 | local bt = {} 46 | for i = 4, #_bt do 47 | table_insert(bt, S_trim(_bt[i])) 48 | end 49 | return table_concat(bt, '\n') 50 | else 51 | return '' 52 | end 53 | end 54 | 55 | local function _write(lvl, args) 56 | if L.level >= lvl then 57 | return L._write(lvl, table_concat(args), _get_bt(lvl)) 58 | end 59 | end 60 | 61 | L._write = function(lvl, msg, bt) return error('Not implemented') end 62 | 63 | L.debug = function(...) return _write(L.DEBUG, {...}) end 64 | L.info = function(...) return _write(L.INFO, {...}) end 65 | L.notice = function(...) return _write(L.NOTICE, {...}) end 66 | L.warn = function(...) return _write(L.WARN, {...}) end 67 | L.err = function(...) return _write(L.ERR, {...}) end 68 | L.crit = function(...) return _write(L.CRIT, {...}) end 69 | L.alert = function(...) return _write(L.ALERT, {...}) end 70 | L.emerg = function(...) return _write(L.EMERG, {...}) end 71 | 72 | L.warning = L.warn 73 | L.error = L.err 74 | L.critical = L.crit 75 | L.emergency = L.emerg 76 | 77 | return L 78 | end 79 | 80 | return M 81 | -------------------------------------------------------------------------------- /lib/estrela/log/file.lua: -------------------------------------------------------------------------------- 1 | local error = error 2 | local string_gsub = string.gsub 3 | local string_format = string.format 4 | local os_date = os.date 5 | local io_open = io.open 6 | local require = require 7 | 8 | local M = {} 9 | 10 | function M:new(path, fmt) 11 | fmt = fmt or '%Y/%m/%d %H:%M:%S {LVL} [{MSG}] {BT}' 12 | 13 | local f 14 | 15 | local L = require('estrela.log.common'):new() 16 | 17 | L._write = function(lvl, msg, bt) 18 | if not f then 19 | f = io_open(path, 'ab') 20 | if not f then 21 | return error('Cannot open log file ' .. path) 22 | end 23 | end 24 | 25 | local line = os_date(fmt) 26 | local bt = string_gsub(bt, '\n', ' => ') 27 | local lvl_str = string_format('%-7s', L.level_names[lvl]) 28 | line = string_gsub(string_gsub(string_gsub(line, '{LVL}', lvl_str), '{MSG}', msg), '{BT}', bt) 29 | f:write(line, '\n') 30 | f:flush() 31 | end 32 | 33 | L.close = function() 34 | if f then 35 | f:close() 36 | f = nil 37 | end 38 | end 39 | 40 | return L 41 | end 42 | 43 | return M 44 | -------------------------------------------------------------------------------- /lib/estrela/log/ngx.lua: -------------------------------------------------------------------------------- 1 | local require = require 2 | local string_gsub = string.gsub 3 | 4 | local ngx_log = ngx.log 5 | 6 | local M = {} 7 | 8 | function M:new() 9 | local L = require('estrela.log.common'):new() 10 | 11 | L._write = function(lvl, msg, bt) 12 | local bt = string_gsub(bt, '\n', ' => ') 13 | return ngx_log(lvl, msg, ', backtrace: ', bt) 14 | end 15 | 16 | return L 17 | end 18 | 19 | return M 20 | -------------------------------------------------------------------------------- /lib/estrela/ngx/app.lua: -------------------------------------------------------------------------------- 1 | local require = require 2 | 3 | local OB = require('estrela.io.ob') 4 | local OB_finish = OB.finish 5 | local OB_flush = OB.flush 6 | local OB_levels = OB.levels 7 | local OB_print = OB.print 8 | local OB_start = OB.start 9 | 10 | local assert = assert 11 | local debug = require('debug') 12 | local debug_traceback = debug.traceback 13 | local error = error 14 | local io_open = io.open 15 | local ipairs = ipairs 16 | local loadstring = loadstring 17 | local next = next 18 | local pairs = pairs 19 | local setmetatable = setmetatable 20 | local table_concat = table.concat 21 | local table_insert = table.insert 22 | local tonumber = tonumber 23 | local tostring = tostring 24 | local type = type 25 | local unpack = unpack 26 | local xpcall = xpcall 27 | 28 | local ngx_gsub = ngx.re.gsub 29 | local ngx_match = ngx.re.match 30 | 31 | local Router = require('estrela.ngx.router') 32 | local Request = require('estrela.ngx.request') 33 | local Response = require('estrela.ngx.response') 34 | 35 | local env_get_root = require('estrela.util.env').get_root 36 | local path_join = require('estrela.util.path').join 37 | 38 | local S = require('estrela.util.string') 39 | local S_split = S.split 40 | local S_starts = S.starts 41 | local S_trim = S.trim 42 | local S_htmlencode = S.htmlencode 43 | 44 | local _config_private = {} 45 | 46 | function _config_private:get(path, default) 47 | local r = self 48 | local p, l = 1, #path 49 | 50 | while p <= l do 51 | local c = string.find(path, '.', p, true) or (l + 1) 52 | local key = string.sub(path, p, c - 1) 53 | if r[key] == nil then 54 | return default 55 | end 56 | p = c + 1 57 | r = r[key] 58 | end 59 | 60 | return r 61 | end 62 | 63 | local _error = {} 64 | 65 | function _error:clean() 66 | self.code = 0 67 | self.msg = nil 68 | self.file = nil 69 | self.line = nil 70 | self.stack = nil 71 | self.aborted_code = nil -- используется для передачи кода ошибки из app:abort() в обработчик xpcall 72 | self.redirect = nil -- используется для передачи url и статуса редиректа из app:redirect() 73 | end 74 | 75 | function _error:init(code, msg) 76 | local info = msg and self:_splitErrorLineByWhereAndMsg(msg) or {} 77 | self.code = tonumber(code) or 500 78 | self.msg = info.msg 79 | self.file = info.file 80 | self.line = info.line 81 | self.stack = self:_parseBacktrace(debug_traceback()) 82 | return false 83 | end 84 | 85 | function _error:_parseBacktrace(str) 86 | str = S_split(str, '\n') 87 | if not S_starts(str[1], 'stack traceback:') then 88 | return nil 89 | end 90 | 91 | local bt = {} 92 | for i = 3, #str do -- выкидываем "stack traceback:" и вызов самого _parseBacktrace() 93 | table_insert(bt, self:_splitErrorLineByWhereAndMsg(str[i])) 94 | end 95 | 96 | return bt 97 | end 98 | 99 | function _error:_splitErrorLineByWhereAndMsg(line) 100 | line = S_trim(line) 101 | local m = ngx_match(line, [[^(?[^:]+):(?[0-9]+):(?.*)$]], 'jo') or {} 102 | return { 103 | msg = S_trim(m.msg or line), 104 | file = m.path or '', 105 | line = tonumber(m.line) or 0, 106 | } 107 | end 108 | 109 | function _error:place() 110 | local line = self.line and (':' .. self.line) or '' 111 | if self.file and (#self.file > 0) then 112 | return ' in ' .. self.file .. line 113 | else 114 | return '' 115 | end 116 | end 117 | 118 | local App = {} 119 | 120 | function App:serve() 121 | ngx.ctx.estrela = self 122 | 123 | local logger = self.config.error_logger 124 | if logger then 125 | if type(logger) == 'string' then 126 | logger = require(logger):new() 127 | elseif type(logger) == 'function' then 128 | logger = logger() 129 | end 130 | end 131 | if logger then 132 | self.log = logger 133 | end 134 | 135 | self.req = Request:new() 136 | self.resp = Response:new() 137 | self.error:clean() 138 | 139 | self.session = nil 140 | 141 | local pathPrefix = self.config:get('router.pathPrefix') 142 | if pathPrefix then 143 | self.router.path_prefix = pathPrefix 144 | end 145 | 146 | local with_ob = self.config:get('ob.active') 147 | local ob_level = 0 148 | 149 | self.subpath = setmetatable({}, { __mode = 'v', }) 150 | self:_protcall(function() 151 | if with_ob then 152 | OB_start() 153 | ob_level = OB_levels() 154 | end 155 | 156 | if self.config:get('session.active') then 157 | local encdec = self.config:get('session.handler.encdec') 158 | if type(encdec) == 'function' then 159 | encdec = encdec() 160 | end 161 | assert(encdec, 'There are no defined encoder/decoder for session') 162 | 163 | local storage = self.config:get('session.storage.handler') 164 | if type(storage) == 'function' then 165 | storage = storage(self) 166 | end 167 | assert(storage, 'There are no defined storage for session') 168 | 169 | local SESSION = require(self.config:get('session.handler.handler')) 170 | 171 | self.session = SESSION:new(storage, encdec.encode, encdec.decode) 172 | end 173 | 174 | return self:_callTriggers(self.trigger.before_req) and self:_callRoutes() 175 | end) 176 | 177 | -- after_req триггеры вызываются даже в случае ошибки внутри before_req или роутах 178 | if next(self.trigger.after_req) then 179 | self:_protcall(function() 180 | return self:_callTriggers(self.trigger.after_req) 181 | end) 182 | end 183 | 184 | if self.error.code > 0 then 185 | if with_ob then 186 | -- при ошибке отбрасываем все ранее выведенное 187 | -- несбалансированность ob при этом не проверяем, т.к. она допустима при ошибках 188 | while OB_levels() >= ob_level do 189 | OB_finish() 190 | end 191 | end 192 | self:_callErrorCb() 193 | else 194 | if with_ob then 195 | if OB_levels() > ob_level then 196 | self.log.warn('OB is not balanced') 197 | end 198 | 199 | while OB_levels() >= ob_level do 200 | if not self.error.redirect then 201 | OB_flush() 202 | end 203 | OB_finish() 204 | end 205 | end 206 | end 207 | 208 | if self.session then 209 | self.session:stop() 210 | end 211 | 212 | if self.error.redirect then 213 | return ngx.redirect(self.error.redirect.url, self.error.redirect.status) 214 | end 215 | end 216 | 217 | function App:defer(func, ...) 218 | if type(func) ~= 'function' then 219 | return nil, 'missing func' 220 | end 221 | table_insert(self.defers, {cb = func, args = {...}}) 222 | return true 223 | end 224 | 225 | function App:abort(code, msg) 226 | self.error.aborted_code = code or 500 227 | return error(msg or '', 2) 228 | end 229 | 230 | function App:redirect(url, status) 231 | self.error.redirect = { 232 | url = url, 233 | status = status or ngx.HTTP_MOVED_TEMPORARILY, 234 | } 235 | return self:abort(0) 236 | end 237 | 238 | function App:defaultErrorPage() 239 | local err = self.error 240 | 241 | self.resp.headers.content_type = 'text/html' 242 | 243 | if err.code > 0 then 244 | ngx.status = err.code 245 | end 246 | 247 | local html = {} 248 | 249 | if self.config.debug then 250 | local enc = S_htmlencode 251 | 252 | table_insert(html, table_concat{ 253 | 'Error #', err.code, ' ', enc(err.msg or ''), ' ', enc(err:place()), '

' 254 | }) 255 | 256 | if type(err.stack) == 'table' then 257 | table_insert(html, 'Stack:
    ') 258 | for _, bt in ipairs(err.stack) do 259 | table_insert(html, '
  • ' .. enc(bt.file)) 260 | if bt.line > 0 then 261 | table_insert(html, ':' .. bt.line) 262 | end 263 | table_insert(html, ' ' .. enc(bt.msg)) 264 | table_insert(html, '
  • ') 265 | end 266 | table_insert(html, '
') 267 | end 268 | end 269 | 270 | local res = self:_renderInternalTemplate(tostring(err.code), { 271 | code = err.code, 272 | descr = table_concat(html), 273 | }) 274 | 275 | if not res then 276 | -- полный ахтунг. не удалось вывести страницу с описанием ошибки 277 | return nil 278 | end 279 | 280 | return true 281 | end 282 | 283 | function App:forward(url) 284 | return { 285 | forward = url, 286 | } 287 | end 288 | 289 | -- @return bool 290 | function App:_renderInternalTemplate(name, args) 291 | local path = path_join(env_get_root(), 'ngx', 'tmpl', name .. '.html') 292 | local fd = io_open(path, 'rb') 293 | if not fd then 294 | return false 295 | end 296 | 297 | local cont = fd:read('*all') 298 | fd:close() 299 | 300 | ngx.header.content_type = 'text/html' 301 | 302 | cont = ngx_gsub( 303 | cont, 304 | [[{{([.a-z]+)}}]], 305 | function(m) 306 | local val = args[m[1]] or assert(loadstring('local self = ...; return ' .. m[1]))(self) or '' 307 | return tostring(val) 308 | end, 309 | 'jo' 310 | ) 311 | 312 | return OB_print(cont) 313 | end 314 | 315 | function App:_protcall(func) 316 | self.error.aborted_code = nil 317 | return xpcall( 318 | func, 319 | function(err) 320 | self.log.err(err) 321 | local code = self.error.aborted_code and self.error.aborted_code or ngx.HTTP_INTERNAL_SERVER_ERROR 322 | return self.error:init(code, err) 323 | end 324 | ) 325 | end 326 | 327 | function App:_callTriggers(triggers_list) 328 | for _, cb in ipairs(triggers_list) do 329 | local ok, res = self:_callRoute(cb) 330 | if not ok then 331 | return ok, res 332 | elseif res == true then 333 | break 334 | end 335 | end 336 | return true 337 | end 338 | 339 | function App:_callErrorCb() 340 | local errno = self.error.code 341 | 342 | local route = self.router:getByName(errno) 343 | if route then 344 | local ok, handled = self:_callRoute(route.cb) 345 | if not (ok and handled) then 346 | -- ошибка в обработчике ошибки, либо обработчик не стал сам обрабатывать ошибку 347 | return self:defaultErrorPage() 348 | end 349 | return ok 350 | else 351 | return self:defaultErrorPage() 352 | end 353 | end 354 | 355 | function App:_callDefers() 356 | local ok = true 357 | 358 | for _, defer in ipairs(self.defers) do 359 | local _ok, _ = self:_protcall(function() 360 | return defer.cb(unpack(defer.args)) 361 | end) 362 | 363 | ok = ok and _ok 364 | end 365 | 366 | self.defers = {} 367 | return ok 368 | end 369 | 370 | function App:_callRoute(cb) 371 | self.defers = {} 372 | 373 | local ok, res = self:_protcall(function() 374 | return cb(self, self.req, self.resp) 375 | end) 376 | 377 | local def_ok, def_res = self:_callDefers() 378 | 379 | if ok and not def_ok then 380 | ok, res = def_ok, def_res 381 | end 382 | 383 | return ok, res 384 | end 385 | 386 | function App:_callRoutes() 387 | local ok = true 388 | 389 | local found = false 390 | 391 | local path = self.req.path 392 | if #self.subpath > 0 then 393 | path = self.subpath[#self.subpath].url 394 | end 395 | 396 | for route in self.router:route(path) do 397 | -- защита от рекурсии 398 | for _, sp in ipairs(self.subpath) do 399 | if sp.cb == route.cb then 400 | return self.error:init(ngx.HTTP_INTERNAL_SERVER_ERROR, 'Route recursion detected') 401 | end 402 | end 403 | 404 | self.route = route 405 | 406 | local _ok, res = self:_callRoute(route.cb) 407 | ok = ok and _ok 408 | 409 | local extras = _ok and (type(res) == 'table') 410 | local pass = _ok and (res == true) 411 | 412 | if not pass then 413 | found = true 414 | 415 | if extras then 416 | if res.forward then 417 | local url = self.router:getFullUrl(res.forward) 418 | 419 | table_insert(self.subpath, {url=url, cb=route.cb}) 420 | ok = self:_callRoutes() 421 | self.subpath[#self.subpath] = nil 422 | end 423 | end 424 | 425 | break 426 | end 427 | end 428 | 429 | if not found then 430 | return self.error:init(ngx.HTTP_NOT_FOUND, 'Route is not found') 431 | end 432 | 433 | return ok 434 | end 435 | 436 | local M = {} 437 | 438 | function M:new(routes) 439 | local A = { 440 | route = nil, 441 | req = nil, 442 | resp = nil, 443 | session = nil, 444 | 445 | subpath = {}, -- для перенаправлений между роутами в пределах одного запроса 446 | 447 | error = _error, 448 | 449 | log = require('estrela.log.ngx'):new(), 450 | 451 | defers = {}, 452 | trigger = { 453 | before_req = {add = table_insert,}, 454 | after_req = {add = table_insert,}, 455 | }, 456 | 457 | config = setmetatable( 458 | { 459 | -- Отладочный режим. При ошибках выводится полный stacktrace 460 | debug = false, 461 | 462 | -- Буферизация вывода 463 | ob = { 464 | active = true, 465 | }, 466 | 467 | -- Сессии 468 | session = { 469 | -- При неактивный сессиях app.session не инициализируется (app.session.start не доступна) 470 | active = false, 471 | -- Настройки хранилища сессионных данных 472 | storage = { 473 | handler = function() 474 | local SHMEM = require('estrela.storage.engine.shmem') 475 | return SHMEM:new('session_cache') 476 | end, 477 | }, 478 | -- Настройки обработчика сессий 479 | handler = { 480 | -- Стандартный обработчик, хранящий сессионный id в куках 481 | handler = 'estrela.ngx.session.engine.common', 482 | -- Имя ключа (чего либо), хранящего сессионый id 483 | key_name = 'estrela_sid', 484 | -- Префикс для всех ключей хранилища, используемых обработчиком сессий 485 | storage_key_prefix = 'estrela_session:', 486 | -- Время жизни блокировки сессионного ключа в хранилище 487 | storage_lock_ttl = 10, 488 | -- Максимальное время ожидания (сек) отпускания блокировки сессионного ключа в хранилище 489 | storage_lock_timeout = 3, 490 | -- Сервис сериализации для хранения сессионный данных в хранилище 491 | encdec = function() 492 | local json = require('cjson') 493 | json.encode_sparse_array(true) 494 | return json 495 | end, 496 | -- Настройки печеньки, в которой хранится сессионый id 497 | -- Используется, если выбранный обработчик сессий работает через куки 498 | cookie = { 499 | params = { -- смотри app.response.COOKIE.empty 500 | --ttl = 86400, 501 | httponly = true, 502 | path = '/', 503 | }, 504 | }, 505 | }, 506 | }, 507 | 508 | -- Маршрутизация запросов 509 | router = { 510 | -- Игнорируемый маршрутизатором префикс URL. На случай работы не из корня сайта. 511 | pathPrefix = '/', 512 | }, 513 | 514 | -- Альтернативный логер вместо nginx error_log файла. 515 | error_logger = nil, 516 | }, { 517 | __index = _config_private, 518 | } 519 | ), 520 | } 521 | 522 | for k, v in pairs(App) do 523 | A[k] = v 524 | end 525 | 526 | ngx.ctx.estrela = A 527 | 528 | A.error:clean() 529 | 530 | A.router = Router:new(routes) 531 | 532 | return A 533 | end 534 | 535 | setmetatable(M, { 536 | __call = function(self, routes) 537 | return self:new(routes) 538 | end, 539 | }) 540 | 541 | return M 542 | -------------------------------------------------------------------------------- /lib/estrela/ngx/request.lua: -------------------------------------------------------------------------------- 1 | local io_tmpfile = io.tmpfile 2 | local require = require 3 | local setmetatable = setmetatable 4 | local table_concat = table.concat 5 | local table_insert = table.insert 6 | local tonumber = tonumber 7 | 8 | local ngx_get_headers = ngx.req.get_headers 9 | local ngx_get_post_args = ngx.req.get_post_args 10 | local ngx_get_uri_args = ngx.req.get_uri_args 11 | local ngx_read_body = ngx.req.read_body 12 | 13 | local S = require('estrela.util.string') 14 | local S_cmpi = S.cmpi 15 | local S_parse_header_value = S.parse_header_value 16 | 17 | local M = {} 18 | 19 | local function parsePostBody(timeout) 20 | local POST, FILES = {}, {} 21 | 22 | local body_len = ngx.var.http_content_length 23 | if not body_len then 24 | return POST, FILES 25 | end 26 | 27 | local body_len = tonumber(body_len) or 0 28 | if body_len == 0 then 29 | return POST, FILES 30 | end 31 | 32 | local app = ngx.ctx.estrela 33 | 34 | local timeout = tonumber(timeout) or 1000 35 | 36 | local chunk_size = body_len > 102400 and 102400 or 8192 37 | 38 | local UPLOAD = require('resty.upload') 39 | local form, _ = UPLOAD:new(chunk_size) 40 | if form then 41 | form:set_timeout(timeout) 42 | 43 | local field = {} 44 | 45 | while true do 46 | local type_, res, err = form:read() 47 | if not type_ then 48 | app.log.err('failed to read: ' .. err) 49 | break 50 | end 51 | 52 | if type_ == 'header' then 53 | local hdr_key, hdr_body = res[1], S_parse_header_value(res[2]) 54 | if S_cmpi(hdr_key, 'Content-Disposition') then 55 | field.name = hdr_body.name 56 | if hdr_body.filename then 57 | field.filename = hdr_body.filename 58 | field.size = 0 59 | else 60 | field.data = {} 61 | end 62 | elseif S_cmpi(hdr_key, 'Content-Type') then 63 | field.type = hdr_body[''] 64 | -- else -- игнорируем другие заголовки 65 | end 66 | elseif type_ == 'body' then 67 | local res_len = #res 68 | if res_len > 0 then 69 | if field.data then 70 | table_insert(field.data, res) 71 | else 72 | if not field.tmpfile then 73 | field.fd = io_tmpfile() 74 | end 75 | field.fd:write(res) 76 | field.size = field.size + res_len 77 | end 78 | end 79 | end 80 | 81 | if type_ == 'part_end' then 82 | if field.filename then 83 | if field.fd then 84 | field.fd:seek('set', 0) 85 | end 86 | FILES[field.name] = field 87 | else 88 | field.data = table_concat(field.data) 89 | POST[field.name] = field 90 | end 91 | field = {} 92 | elseif type_ == 'eof' then 93 | break 94 | end 95 | end 96 | else 97 | ngx_read_body() 98 | POST = ngx_get_post_args() 99 | end 100 | 101 | return POST, FILES 102 | end 103 | 104 | function M:new() 105 | local POST, FILES = parsePostBody() 106 | 107 | local BODY = setmetatable( 108 | { 109 | POST = POST, 110 | FILES = FILES, 111 | }, { 112 | __index = function(me, key) 113 | return me.POST[key] or me.FILES[key] 114 | end, 115 | } 116 | ) 117 | 118 | local R = { 119 | url = ngx.var.request_uri, 120 | path = ngx.var.uri, 121 | method = ngx.var.request_method, 122 | headers = ngx_get_headers(), 123 | 124 | GET = ngx_get_uri_args(), 125 | COOKIE = S_parse_header_value(ngx.var.http_cookie or ''), 126 | 127 | BODY = BODY, 128 | POST = POST, 129 | FILES = FILES, 130 | } 131 | 132 | local mt = { 133 | __index = function(_, key) 134 | return ngx.var[key] 135 | end, 136 | } 137 | 138 | return setmetatable(R, mt) 139 | end 140 | 141 | return M 142 | -------------------------------------------------------------------------------- /lib/estrela/ngx/response.lua: -------------------------------------------------------------------------------- 1 | local COOKIE = require('estrela.ngx.response.cookie') 2 | 3 | local OB = require('estrela.io.ob') 4 | local OB_print = OB.print 5 | local OB_println = OB.println 6 | 7 | local M = {} 8 | 9 | function M:new() 10 | return { 11 | headers = ngx.header, 12 | COOKIE = COOKIE:new(), 13 | 14 | finish = ngx.eof, 15 | write = OB_print, 16 | writeln = OB_println, 17 | } 18 | end 19 | 20 | return M 21 | -------------------------------------------------------------------------------- /lib/estrela/ngx/response/cookie.lua: -------------------------------------------------------------------------------- 1 | local pairs = pairs 2 | local table_concat = table.concat 3 | local table_insert = table.insert 4 | local tostring = tostring 5 | local type = type 6 | 7 | local ngx_cookie_time = ngx.cookie_time 8 | 9 | local M = {} 10 | 11 | local function setCookie(cookie, append) 12 | if not (cookie.name and cookie.value and #cookie.name > 0) then 13 | return nil, 'missing name or value' 14 | end 15 | 16 | if cookie['max-age'] then 17 | cookie['max_age'], cookie['max-age'] = cookie['max-age'], nil 18 | end 19 | 20 | if type(cookie.expires) == 'number' then 21 | cookie.expires = ngx_cookie_time(cookie.expires) 22 | end 23 | 24 | local cookie_str = {cookie.name .. '=' .. cookie.value} 25 | cookie.name, cookie.value = nil, nil 26 | 27 | local attrs = { 28 | max_age = 'max-age', 29 | } 30 | for k, v in pairs(cookie) do 31 | if v and type(v) ~= 'function' then 32 | if type(v) == 'boolean' then 33 | v = v and '' or nil 34 | else 35 | v = '=' .. tostring(v) 36 | end 37 | 38 | if v then 39 | k = tostring(attrs[k] or k) 40 | table_insert(cookie_str, k .. v) 41 | end 42 | end 43 | end 44 | 45 | cookie_str = table_concat(cookie_str, '; ') 46 | 47 | if not append then 48 | ngx.header['Set-Cookie'] = nil 49 | end 50 | 51 | if not ngx.header['Set-Cookie'] then 52 | ngx.header['Set-Cookie'] = {cookie_str} 53 | else 54 | if type(ngx.header['Set-Cookie']) == 'string' then 55 | ngx.header['Set-Cookie'] = {cookie_str, ngx.header['Set-Cookie']} 56 | else 57 | local cookies = ngx.header['Set-Cookie'] 58 | table_insert(cookies, cookie_str) 59 | ngx.header['Set-Cookie'] = cookies 60 | end 61 | end 62 | end 63 | 64 | -- http://tools.ietf.org/html/rfc6265 65 | 66 | function M:new() 67 | local C = {} 68 | 69 | function C:set(cookie, value, expires, path, domain, secure, httponly, append) 70 | if type(cookie) ~= 'table' then 71 | cookie = { 72 | name = cookie, 73 | value = value, 74 | expires = expires, 75 | domain = domain, 76 | path = path, 77 | secure = secure, 78 | httponly = httponly, 79 | } 80 | else 81 | -- если передается таблица, то append идет третьим параметром 82 | append = value 83 | end 84 | 85 | return setCookie(cookie, append) 86 | end 87 | 88 | function C:empty(map) 89 | local cookie = { 90 | name = '', 91 | value = '', 92 | expires = nil, 93 | max_age = nil, 94 | domain = nil, 95 | path = nil, 96 | secure = nil, 97 | httponly = nil, 98 | 99 | set = function(me, append) 100 | return self:set(me, append) 101 | end, 102 | } 103 | 104 | if map then 105 | for k, v in pairs(map) do 106 | cookie[k] = v 107 | end 108 | end 109 | 110 | return cookie 111 | end 112 | 113 | return C 114 | end 115 | 116 | return M 117 | -------------------------------------------------------------------------------- /lib/estrela/ngx/router.lua: -------------------------------------------------------------------------------- 1 | local coroutine_wrap = coroutine.wrap 2 | local coroutine_yield = coroutine.yield 3 | local pairs = pairs 4 | local require = require 5 | local table_insert = table.insert 6 | local table_sort = table.sort 7 | local type = type 8 | 9 | local ngx_gsub = ngx.re.gsub 10 | local ngx_match = ngx.re.match 11 | 12 | local S = require('estrela.util.string') 13 | local S_rtrim = S.rtrim 14 | 15 | local T = require('estrela.util.table') 16 | local T_contains = T.contains 17 | 18 | local name_regexp = ':([_a-zA-Z0-9]+)' 19 | 20 | local function _preprocessRoutes(routes) 21 | local routes_urls, routes_codes = {}, {} 22 | 23 | local function _prefixSimplify(prefix) 24 | local pref = ngx_gsub(prefix, name_regexp, ' ', 'jo') 25 | return pref 26 | end 27 | 28 | local function _prefix2regexp(prefix) 29 | local re = ngx_gsub(prefix, name_regexp, '(?<$1>[^/]+)', 'jo') 30 | -- если в регулярке указывается ограничитель по концу строки, то добавляю опциональный /? перед концом 31 | -- после этого регулярка '/foo$' будет подходить и для '/foo', и для '/foo/' 32 | re = ngx_gsub(re, [[\$$]], [[/?$$]], 'jo') 33 | return '^'..re 34 | end 35 | 36 | local function _addPrefix(prefix, cb, name) 37 | local prefixType = type(prefix) 38 | if prefixType == 'string' then 39 | table_insert(routes_urls, { 40 | cb = cb, 41 | prefixShort = _prefixSimplify(prefix), 42 | prefix = prefix, 43 | name = name or nil, 44 | re = _prefix2regexp(prefix) 45 | }) 46 | elseif prefixType == 'number' then 47 | table_insert(routes_codes, { 48 | cb = cb, 49 | prefix = prefix, 50 | name = name or nil, 51 | }) 52 | end 53 | end 54 | 55 | for prefix, cb in pairs(routes) do 56 | if type(prefix) == 'table' then 57 | for name, pref in pairs(prefix) do 58 | _addPrefix(pref, cb, name) 59 | end 60 | else 61 | _addPrefix(prefix, cb) 62 | end 63 | end 64 | 65 | table_sort(routes_urls, function(a, b) 66 | return a.prefixShort > b.prefixShort 67 | end) 68 | 69 | return routes_urls, routes_codes 70 | end 71 | 72 | local Router = {} 73 | 74 | function Router:getFullUrl(url) 75 | return self.path_prefix .. url 76 | end 77 | 78 | function Router:mount(prefix, routes) 79 | self.routes_urls, self.routes_codes = nil, nil 80 | 81 | for _prefix, cb in pairs(routes) do 82 | local _prefix_type = type(_prefix) 83 | if _prefix_type == 'string' then 84 | self.routes[prefix .. _prefix] = cb 85 | elseif _prefix_type == 'number' then 86 | self.routes[_prefix] = cb 87 | elseif _prefix_type == 'table' then 88 | local _new_prefix = {} 89 | for name, _sub_prefix in pairs(_prefix) do 90 | _new_prefix[name] = prefix .. _sub_prefix 91 | end 92 | self.routes[_new_prefix] = cb 93 | end 94 | end 95 | end 96 | 97 | function Router:route(pathFull) 98 | local app = ngx.ctx.estrela 99 | 100 | if not self.routes_urls then 101 | self.routes_urls, self.routes_codes = _preprocessRoutes(self.routes) 102 | end 103 | 104 | local path = pathFull 105 | if self.path_prefix then 106 | path = path:sub(self.path_prefix:len() + 1) 107 | end 108 | 109 | local method = app.req.method 110 | 111 | local function check_method(route) 112 | for k, cb in pairs(route) do 113 | if type(k) == 'table' then 114 | if T_contains(k, method) then 115 | return cb 116 | end 117 | elseif method == k:upper() then 118 | return cb 119 | end 120 | end 121 | return nil 122 | end 123 | 124 | return coroutine_wrap(function() 125 | for _, p in ipairs(self.routes_urls) do 126 | local captures = ngx_match(path, p.re, 'jo') 127 | if captures then 128 | local cb = p.cb 129 | 130 | if type(cb) == 'string' then 131 | cb = require(cb) 132 | end 133 | 134 | if type(cb) == 'table' then 135 | cb = check_method(cb) 136 | end 137 | 138 | if cb then 139 | coroutine_yield { 140 | prefix = p.prefix, 141 | cb = cb, 142 | params = captures, 143 | name = p.name, 144 | path = path, 145 | pathFull = pathFull, 146 | } 147 | end 148 | end 149 | end 150 | end) 151 | end 152 | 153 | function Router:getByName(name) 154 | if not self.routes_urls then 155 | self.routes_urls, self.routes_codes = _preprocessRoutes(self.routes) 156 | end 157 | 158 | local name_type = type(name) 159 | if name_type == 'string' then 160 | for _,p in pairs(self.routes_urls) do 161 | if p.name == name then 162 | return p 163 | end 164 | end 165 | elseif name_type == 'number' then 166 | for _,p in pairs(self.routes_codes) do 167 | if (p.prefix == name) or (p.name == name) then 168 | return p 169 | end 170 | end 171 | end 172 | end 173 | 174 | function Router:urlFor(name, params) 175 | params = params or {} 176 | 177 | local route = self:getByName(name) 178 | if not route then 179 | return nil 180 | end 181 | 182 | local url = ngx_gsub(route.prefix, name_regexp, function(m) return params[m[1]] or '' end, 'jo') 183 | return self:getFullUrl(S_rtrim(url, '$')) 184 | end 185 | 186 | local M = {} 187 | 188 | function M:new(routes) 189 | local R = { 190 | routes = routes, 191 | routes_urls = nil, 192 | routes_codes = nil, 193 | path_prefix = '', 194 | } 195 | 196 | for k, v in pairs(Router) do 197 | R[k] = v 198 | end 199 | 200 | return R 201 | end 202 | 203 | return M 204 | -------------------------------------------------------------------------------- /lib/estrela/ngx/session/engine/common.lua: -------------------------------------------------------------------------------- 1 | local error = error 2 | local math_random = math.random 3 | local math_randomseed = math.randomseed 4 | local type = type 5 | 6 | local ngx_md5 = ngx.md5 7 | local ngx_now = ngx.now 8 | local ngx_sleep = ngx.sleep 9 | local ngx_time = ngx.time 10 | 11 | local T = require('estrela.util.table') 12 | local T_clone = T.clone 13 | 14 | local M = {} 15 | 16 | function M:new(storage, encoder, decoder) 17 | local S = { 18 | data = nil, 19 | storage = storage, 20 | } 21 | 22 | function S:new() 23 | return self 24 | end 25 | 26 | function S:start() 27 | if self.data then 28 | return false -- сессия уже запущена 29 | end 30 | 31 | math_randomseed(ngx_time()) 32 | 33 | local app = ngx.ctx.estrela 34 | 35 | self.ttl = app.config:get('session.handler.cookie.params.ttl', 86400) 36 | 37 | self.key_name = app.config:get('session.handler.key_name', 'estrela_sid') 38 | self.storage_key_prefix = app.config:get('session.handler.storage_key_prefix', 'estrela_session:') 39 | self.storage_lock_ttl = app.config:get('session.handler.storage_lock_ttl', 10) 40 | self.storage_lock_timeout = app.config:get('session.handler.storage_lock_timeout', 2) 41 | 42 | self.sessid = app.req.COOKIE[self.key_name] 43 | if type(self.sessid) == 'table' then 44 | self.sessid = self.sessid[1] 45 | end 46 | 47 | if not self.sessid then 48 | self:_create() 49 | end 50 | 51 | if not self.sessid then 52 | return error('Cannot start session', 2) 53 | end 54 | 55 | self:_update_session_cookie() 56 | 57 | self.storage_key_lock = self:_get_storage_key() .. ':lock' 58 | 59 | if not self:_storage_lock() then 60 | return error('Session storage is locked', 2) 61 | end 62 | 63 | return self:_load() 64 | end 65 | 66 | function S:stop() 67 | if not self.sessid then 68 | return 69 | end 70 | 71 | local storage_key = self:_get_storage_key() 72 | 73 | local res = self.storage:set(storage_key, encoder(self.data), self.ttl) 74 | self:_storage_unlock() 75 | self.data, self.sessid = nil, nil 76 | return res 77 | end 78 | 79 | function S:delete() 80 | local storage_key = self:_get_storage_key() 81 | local res = self.storage:delete(storage_key) 82 | self.sessid, self.data = nil, nil 83 | self:_storage_unlock() 84 | return res 85 | end 86 | 87 | function S:gc() 88 | return self.storage:flush_expired(1000) 89 | end 90 | 91 | function S:_create() 92 | self.data = {} 93 | local encoded_data = encoder(self.data) 94 | local tries = 10 95 | while tries > 0 do 96 | self.sessid = self:_gen_sessid() 97 | local storage_key = self:_get_storage_key() 98 | if self.storage:add(storage_key, encoded_data, self.ttl) then 99 | return self.data 100 | end 101 | 102 | tries = tries - 1 103 | end 104 | 105 | self.data, self.sessid = nil, nil 106 | return nil 107 | end 108 | 109 | function S:_load() 110 | local storage_key = self:_get_storage_key() 111 | self.data = self.storage:get(storage_key) 112 | if self.data then 113 | self.data = decoder(self.data) 114 | end 115 | if not self.data then 116 | self.data = {} 117 | end 118 | 119 | return self.data 120 | end 121 | 122 | function S:_gen_sessid() 123 | return ngx_md5( 124 | ngx.var.remote_addr .. ngx.var.pid .. ngx_now() .. ngx.var.connection .. math_random() 125 | ):sub(1, 16) 126 | end 127 | 128 | function S:_get_storage_key() 129 | return self.storage_key_prefix .. self.sessid 130 | end 131 | 132 | function S:_storage_lock() 133 | local lock_ttl = self.storage_lock_ttl 134 | local lock_timeout = self.storage_lock_timeout 135 | local try_until = ngx_now() + lock_timeout 136 | local locked 137 | while true do 138 | locked = self.storage:add(self.storage_key_lock, 1, lock_ttl) 139 | if locked or (try_until < ngx_now()) then 140 | break 141 | end 142 | ngx_sleep(0.01) 143 | end 144 | 145 | return locked 146 | end 147 | 148 | function S:_storage_unlock() 149 | return self.storage:delete(self.storage_key_lock) 150 | end 151 | 152 | function S:_update_session_cookie() 153 | local app = ngx.ctx.estrela 154 | if ngx.headers_sent then 155 | return app.log.err('Error saving the session cookie: headers already sent') 156 | else 157 | local cookie = T_clone(app.config:get('session.handler.cookie.params', {})) 158 | cookie.name = self.key_name 159 | cookie.value = self.sessid 160 | if cookie.ttl then 161 | cookie.expires = ngx_time() + cookie.ttl 162 | cookie.ttl = nil 163 | end 164 | 165 | app.resp.COOKIE:empty(cookie):set() 166 | end 167 | end 168 | 169 | return S:new() 170 | end 171 | 172 | return M 173 | -------------------------------------------------------------------------------- /lib/estrela/ngx/tmpl/404.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Error {{code}} 6 | 7 | 8 |

Ooops! Page {{self.req.path}} is not found

9 | {{descr}} 10 | 11 | 12 | -------------------------------------------------------------------------------- /lib/estrela/ngx/tmpl/500.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Estrelas cadentes {{code}} 6 | 7 | 8 |

Ooops! Something went wrong

9 | {{descr}} 10 | 11 | 12 | -------------------------------------------------------------------------------- /lib/estrela/oop/nano.lua: -------------------------------------------------------------------------------- 1 | local pairs = pairs 2 | local setmetatable = setmetatable 3 | 4 | local M = {} 5 | 6 | function M.class(struct) 7 | struct = struct or {} 8 | 9 | local cls = {} 10 | 11 | setmetatable(cls, { 12 | __call = function(self, ...) 13 | local inst = {} 14 | 15 | for k, v in pairs(struct) do 16 | inst[k] = v 17 | end 18 | 19 | local new = inst.new 20 | if new then 21 | new(inst, ...) 22 | end 23 | 24 | return inst 25 | end, 26 | }) 27 | 28 | return cls 29 | end 30 | 31 | return M 32 | -------------------------------------------------------------------------------- /lib/estrela/oop/single.lua: -------------------------------------------------------------------------------- 1 | local debug = require('debug') 2 | local debug_getinfo = debug.getinfo 3 | local getmetatable = getmetatable 4 | local pairs = pairs 5 | local rawget = rawget 6 | local rawset = rawset 7 | local require = require 8 | local setmetatable = setmetatable 9 | local tostring = tostring 10 | local type = type 11 | 12 | local M = {} 13 | 14 | local function super_func(self, ...) 15 | local frame = debug_getinfo(2) 16 | local mt = getmetatable(self) 17 | assert(mt and mt.__base, 'There are no super method') 18 | 19 | local func = mt.__base[frame.name] 20 | return func and func(self, ...) or nil 21 | end 22 | 23 | function M.class(name, struct, parent) 24 | if type(name) ~= 'string' then 25 | name, struct, parent = nil, name, struct 26 | end 27 | 28 | if type(parent) == 'string' then 29 | parent = require(parent) 30 | end 31 | 32 | local cls = {} 33 | 34 | name = name or ('Cls' .. tostring(cls):sub(10)) 35 | 36 | struct = struct or {} 37 | 38 | -- создание новой инстанции класса без вызова конструкторов 39 | local function _create_inst() 40 | local base = parent and parent:create() or nil 41 | 42 | local inst = {} 43 | 44 | setmetatable(inst, { 45 | __base = base, 46 | __index = setmetatable( 47 | { 48 | super = super_func, 49 | }, { 50 | __index = function(_, key) 51 | local idx_func = rawget(inst, '__index__') 52 | if idx_func then 53 | return idx_func(inst, key) 54 | end 55 | end, 56 | } 57 | ), 58 | }) 59 | 60 | if base then 61 | for k, v in pairs(base) do 62 | inst[k] = v 63 | end 64 | end 65 | 66 | for k, v in pairs(struct) do 67 | inst[k] = v 68 | end 69 | 70 | inst.__class = cls 71 | 72 | return inst 73 | end 74 | 75 | setmetatable(cls, { 76 | __index = setmetatable( 77 | { 78 | __name = name, 79 | name = M.name, 80 | subclass = M.subclass, 81 | create = _create_inst, 82 | }, { 83 | __index = function(_, key) 84 | if parent then 85 | return parent[key] 86 | end 87 | end, 88 | } 89 | ), 90 | __newindex = function(tbl, key, val) 91 | if key ~= 'name' then 92 | rawset(tbl, key, val) 93 | end 94 | end, 95 | __call = function(cls, ...) 96 | local inst = cls:create() 97 | 98 | local new = inst.new 99 | if new then 100 | new(inst, ...) 101 | end 102 | 103 | return inst 104 | end, 105 | }) 106 | 107 | return cls 108 | end 109 | 110 | function M.subclass(parent, name) 111 | return function(struct) 112 | return name and M.class(name, struct, parent) or M.class(struct, parent) 113 | end 114 | end 115 | 116 | function M.name(name) 117 | return { 118 | class = function(struct, parent) 119 | return M.class(name, struct) 120 | end, 121 | 122 | subclass = function(parent) 123 | return function(struct) 124 | return M.class(name, struct, parent) 125 | end 126 | end, 127 | } 128 | end 129 | 130 | return M 131 | -------------------------------------------------------------------------------- /lib/estrela/storage/engine/common.lua: -------------------------------------------------------------------------------- 1 | local error = error 2 | 3 | local M = {} 4 | 5 | function M:new() 6 | local E = {} 7 | 8 | function E:new() 9 | return self 10 | end 11 | 12 | function E:get(key, stale) 13 | return error('Not implemented') 14 | end 15 | 16 | function E:set(key, val, ttl) 17 | return error('Not implemented') 18 | end 19 | 20 | function E:add(key, val, ttl) 21 | return error('Not implemented') 22 | end 23 | 24 | function E:replace(key, val, ttl) 25 | return error('Not implemented') 26 | end 27 | 28 | function E:incr(key, by) 29 | return error('Not implemented') 30 | end 31 | 32 | function E:delete(key) 33 | return error('Not implemented') 34 | end 35 | 36 | function E:exists(key) 37 | return error('Not implemented') 38 | end 39 | 40 | return E:new() 41 | end 42 | 43 | return M 44 | -------------------------------------------------------------------------------- /lib/estrela/storage/engine/shmem.lua: -------------------------------------------------------------------------------- 1 | local error = error 2 | local require = require 3 | local tostring = tostring 4 | 5 | local M = {} 6 | 7 | function M:new(shared_var) 8 | if not shared_var or not ngx.shared[shared_var] then 9 | return error('There are no defined ngx.shared[' .. tostring(shared_var) .. ']') 10 | end 11 | 12 | local E = require('estrela.storage.engine.common'):new() 13 | 14 | function E:new() 15 | self.shmem = ngx.shared[shared_var] 16 | return self 17 | end 18 | 19 | function E:get(key, stale) 20 | local func = stale and self.shmem.get_stale or self.shmem.get 21 | return func(self.shmem, key) 22 | end 23 | 24 | function E:set(key, val, ttl) 25 | return self.shmem:set(key, val, ttl or 0) 26 | end 27 | 28 | function E:add(key, val, ttl) 29 | return self.shmem:add(key, val, ttl or 0) 30 | end 31 | 32 | function E:replace(key, val, ttl) 33 | return self.shmem:replace(key, val, ttl or 0) 34 | end 35 | 36 | function E:incr(key, by) 37 | return self.shmem:incr(key, by or 1) 38 | end 39 | 40 | function E:delete(key) 41 | return self.shmem:delete(key) 42 | end 43 | 44 | function E:exists(key) 45 | return self:get(key) ~= nil 46 | end 47 | 48 | return E:new() 49 | end 50 | 51 | return M 52 | -------------------------------------------------------------------------------- /lib/estrela/util/env.lua: -------------------------------------------------------------------------------- 1 | local debug = require('debug') 2 | local debug_getinfo = debug.getinfo 3 | local string_len = string.len 4 | 5 | local S = require('estrela.util.string') 6 | local S_ltrim = S.ltrim 7 | local S_rtrim = S.rtrim 8 | 9 | local path_split = require('estrela.util.path').split 10 | 11 | local M = {} 12 | 13 | local _lib_root 14 | 15 | function M.get_root() 16 | return _lib_root 17 | end 18 | 19 | local function _setup_env() 20 | local _path = S_ltrim(debug_getinfo(1).source, '@') 21 | _path = S_rtrim(path_split(_path).path, [[/]]) 22 | _lib_root = _path:sub(1, -string_len('/util')-1) 23 | end 24 | 25 | _setup_env() 26 | 27 | return M 28 | -------------------------------------------------------------------------------- /lib/estrela/util/path.lua: -------------------------------------------------------------------------------- 1 | local ipairs = ipairs 2 | local math_min = math.min 3 | local table_concat = table.concat 4 | local table_insert = table.insert 5 | 6 | local S = require('estrela.util.string') 7 | local S_find_last = S.find_last 8 | local S_rtrim = S.rtrim 9 | local S_trim = S.trim 10 | local S_split = S.split 11 | 12 | local M = {} 13 | 14 | -- *nix only 15 | 16 | function M.split(path) 17 | local name, ext 18 | 19 | local slash_pos = S_find_last(path, '/') 20 | if slash_pos then 21 | name = path:sub(slash_pos + 1) 22 | path = path:sub(1, slash_pos) 23 | else 24 | path, name = '', path 25 | end 26 | 27 | local dot_pos = S_find_last(name, '.') 28 | ext = dot_pos and name:sub(dot_pos + 1) or '' 29 | 30 | return { 31 | path = path, 32 | name = name, 33 | ext = ext, 34 | } 35 | end 36 | 37 | function M.rel(base, path) 38 | local base = S_trim(base, '/') 39 | local path = S_trim(path, '/') 40 | local _base = (#base > 0) and S_split(base, '/') or {} 41 | local _path = (#path > 0) and S_split(path, '/') or {} 42 | 43 | local rel = {} 44 | local common_len = 0 45 | for p = 1, math_min(#_path, #_base) do 46 | if _base[p] == _path[p] then 47 | common_len = p 48 | else 49 | break 50 | end 51 | end 52 | 53 | for _ = 1, (#_base - common_len) do 54 | table_insert(rel, '..') 55 | end 56 | 57 | for p = common_len + 1, #_path do 58 | table_insert(rel, _path[p]) 59 | end 60 | 61 | rel = table_concat(rel, '/') 62 | 63 | return rel, (#_base - common_len) 64 | end 65 | 66 | function M.join(path, ...) 67 | local dirs = {...} 68 | if #dirs == 0 then 69 | return path 70 | end 71 | 72 | local path = {S_rtrim(path, '/')} 73 | 74 | for _, dir in ipairs(dirs) do 75 | table_insert(path, S_trim(dir, '/')) 76 | end 77 | 78 | return table_concat(path, '/') 79 | end 80 | 81 | return M 82 | -------------------------------------------------------------------------------- /lib/estrela/util/string.lua: -------------------------------------------------------------------------------- 1 | local getmetatable = getmetatable 2 | local loadstring = loadstring 3 | local pairs = pairs 4 | local pcall = pcall 5 | local string_byte = string.byte 6 | local string_find = string.find 7 | local string_format = string.format 8 | local table_insert = table.insert 9 | local type = type 10 | local unpack = unpack 11 | 12 | local T_list = require('estrela.util.table').list 13 | 14 | local M = {} 15 | 16 | function M.find_first(str, sub, start, stop) 17 | stop = stop or #str 18 | start = start or 1 19 | local p = string_find(str, sub, start, true) 20 | return (p and p <= stop) and p or nil 21 | end 22 | 23 | function M.find_last(str, sub, start, stop) 24 | stop = stop or #str 25 | start = start or 1 26 | 27 | local last_pos 28 | while true do 29 | local f, t = string_find(str, sub, start, true) 30 | if not f or f > stop then 31 | break 32 | end 33 | 34 | last_pos = f 35 | start = t + 1 36 | end 37 | 38 | return last_pos 39 | end 40 | 41 | function M.split(str, sep, maxsplit, sep_regex) 42 | if type(sep) ~= 'string' or #sep < 1 then 43 | return T_list(str:gmatch('.')) 44 | end 45 | 46 | local plain = not sep_regex 47 | 48 | if not string_find(str, sep, 1, plain) then 49 | return {str} 50 | end 51 | 52 | if not maxsplit or maxsplit < 1 then 53 | maxsplit = #str 54 | end 55 | 56 | local res = {} 57 | 58 | local prev_pos = 1 59 | while #res < maxsplit - 1 do 60 | local pos, pos_t = string_find(str, sep, prev_pos, plain) 61 | if not pos then 62 | res[#res + 1] = str:sub(prev_pos) 63 | prev_pos = #str + 1 64 | break 65 | end 66 | 67 | res[#res + 1] = str:sub(prev_pos, pos - 1) 68 | prev_pos = pos_t + 1 69 | end 70 | 71 | if prev_pos <= #str then 72 | res[#res + 1] = str:sub(prev_pos) 73 | end 74 | 75 | return res 76 | end 77 | 78 | function M.trim(str, chars) 79 | if type(chars) == 'string' and #chars == 0 then 80 | return str 81 | end 82 | 83 | chars = chars or '%s' 84 | return M.ltrim(M.rtrim(str, chars), chars) 85 | end 86 | 87 | function M.rtrim(str, chars) 88 | if type(chars) == 'string' and #chars == 0 then 89 | return str 90 | end 91 | 92 | chars = chars or '%s' 93 | local res = str:gsub('['..chars..']+$', '') 94 | return res 95 | end 96 | 97 | function M.ltrim(str, chars) 98 | if type(chars) == 'string' and #chars == 0 then 99 | return str 100 | end 101 | 102 | chars = chars or '%s' 103 | local res = str:gsub('^['..chars..']+', '') 104 | return res 105 | end 106 | 107 | function M.htmlencode(str, withQuotes) 108 | str = str 109 | :gsub([[&]], [[&]]) 110 | :gsub([[<]], [[<]]) 111 | :gsub([[>]], [[>]]) 112 | 113 | if withQuotes then 114 | str = str 115 | :gsub([["]], [["]]) 116 | :gsub([[']], [[']]) 117 | end 118 | 119 | return str 120 | end 121 | 122 | function M.starts(str, prefix) 123 | return prefix:len() == 0 or string_find(str, prefix, 1, true) == 1 124 | end 125 | 126 | function M.ends(str, suffix) 127 | local sl = suffix:len() 128 | return sl == 0 or string_find(str, suffix, -sl, true) == (str:len() - sl + 1) 129 | end 130 | 131 | --[[ Разбор значений HTTP заголовков на компоненты 132 | ]] 133 | function M.parse_header_value(str) 134 | if #str == 0 then 135 | return {} 136 | end 137 | 138 | local trim = M.trim 139 | 140 | local ST_KEY = 1 141 | local ST_VAL = 2 142 | local ST_VAL_WAIT_QUOTE = 3 143 | local ST_VAL_SLASHED = 4 144 | 145 | local SEMI = string_byte(';') 146 | local EQUAL = string_byte('=') 147 | local QUOTE = string_byte('"') 148 | local SLASH = string_byte([[\]]) 149 | 150 | local map = {} 151 | 152 | local function to_map(k, v) 153 | k = trim(k) 154 | 155 | if not v then 156 | k, v = '', k 157 | else 158 | v = trim(v) 159 | end 160 | 161 | if string_byte(v) == QUOTE then 162 | v = loadstring('return '..v) 163 | if v then 164 | local ok, res = pcall(v) 165 | v = ok and res or nil 166 | end 167 | v = v or '' 168 | end 169 | 170 | if map[k] then 171 | if type(map[k]) ~= 'table' then 172 | map[k] = {map[k]} 173 | end 174 | table_insert(map[k], v) 175 | else 176 | map[k] = trim(v) 177 | end 178 | end 179 | 180 | local state = ST_KEY 181 | local sl = #str 182 | local pos = 1 183 | local last_pos = 1 184 | local key 185 | 186 | while pos <= sl do 187 | local ch = string_byte(str, pos) 188 | 189 | if state == ST_KEY then 190 | if ch == SEMI then 191 | if pos > last_pos then 192 | to_map(str:sub(last_pos, pos - 1), nil) 193 | end 194 | last_pos = pos + 1 195 | elseif ch == EQUAL then 196 | key = str:sub(last_pos, pos - 1) 197 | last_pos = pos + 1 198 | state = ST_VAL 199 | end 200 | elseif state == ST_VAL then 201 | if ch == SEMI then 202 | to_map(key, str:sub(last_pos, pos - 1)) 203 | key = nil 204 | last_pos = pos + 1 205 | state = ST_KEY 206 | elseif ch == QUOTE then 207 | last_pos = pos 208 | state = ST_VAL_WAIT_QUOTE 209 | end 210 | elseif state == ST_VAL_WAIT_QUOTE then 211 | if ch == QUOTE then 212 | to_map(key, str:sub(last_pos, pos)) 213 | key = nil 214 | last_pos = pos + 1 215 | state = ST_KEY 216 | elseif ch == SLASH then 217 | state = ST_VAL_SLASHED 218 | end 219 | elseif state == ST_VAL_SLASHED then 220 | state = ST_VAL_WAIT_QUOTE 221 | end 222 | 223 | pos = pos + 1 224 | end 225 | 226 | local val = last_pos <= sl and str:sub(last_pos, sl) or nil 227 | if not key then 228 | key, val = val, nil 229 | end 230 | 231 | if key then 232 | to_map(key, val) 233 | end 234 | 235 | return map 236 | end 237 | 238 | function M.cmpi(str, str2) 239 | return str:lower() == str2:lower() 240 | end 241 | 242 | function M.format(fmt, vars) 243 | if type(vars) == 'table' then 244 | return string_format(fmt, unpack(vars)) 245 | else 246 | return string_format(fmt, vars) 247 | end 248 | end 249 | 250 | function M.apply_patch() 251 | local mt = getmetatable('') 252 | for k, v in pairs(M) do 253 | mt.__index[k] = v 254 | end 255 | mt.__mod = M.format 256 | end 257 | 258 | return M 259 | -------------------------------------------------------------------------------- /lib/estrela/util/table.lua: -------------------------------------------------------------------------------- 1 | local coroutine_wrap = coroutine.wrap 2 | local coroutine_yield = coroutine.yield 3 | local getmetatable = getmetatable 4 | local ipairs = ipairs 5 | local pairs = pairs 6 | local setmetatable = setmetatable 7 | local table_concat = table.concat 8 | local table_insert = table.insert 9 | local type = type 10 | 11 | local M = {} 12 | 13 | local function wrap_gen_func(gen) 14 | local gen = coroutine_wrap(gen) 15 | 16 | return setmetatable({}, { 17 | __index = function(self, key) 18 | return (key == 'list') and M.list or nil 19 | end, 20 | __call = function(...) 21 | return gen(...) 22 | end, 23 | }) 24 | end 25 | 26 | function M.push(tbl, ...) 27 | for _, v in ipairs{...} do 28 | table_insert(tbl, v) 29 | end 30 | 31 | return tbl 32 | end 33 | 34 | function M.clone(tbl) 35 | if type(tbl) ~= 'table' then 36 | return tbl 37 | end 38 | 39 | local copy = {} 40 | for k, v in pairs(tbl) do 41 | copy[M.clone(k)] = M.clone(v) 42 | end 43 | 44 | setmetatable(copy, M.clone(getmetatable(tbl))) 45 | 46 | return copy 47 | end 48 | 49 | function M.rep(tbl, times) 50 | local t = {} 51 | while times > 0 do 52 | table_insert(t, M.clone(tbl)) 53 | times = times - 1 54 | end 55 | return t 56 | end 57 | 58 | function M.range(start, stop, step) 59 | if not stop then 60 | start, stop = 1, start 61 | end 62 | 63 | if not step then 64 | step = start <= stop and 1 or -1 65 | end 66 | 67 | return wrap_gen_func(function() 68 | for i = start, stop, step do 69 | coroutine_yield(i) 70 | end 71 | end) 72 | end 73 | 74 | function M.join(tbl, sep) 75 | return table_concat(tbl, sep) 76 | end 77 | 78 | function M.len(tbl) 79 | local cnt = 0 80 | for _, _ in pairs(tbl) do 81 | cnt = cnt + 1 82 | end 83 | return cnt 84 | end 85 | 86 | function M.list(smth) 87 | local l = {} 88 | for v in smth do 89 | table_insert(l, v) 90 | end 91 | return l 92 | end 93 | 94 | function M.contains(tbl, item) 95 | for _, v in pairs(tbl) do 96 | if v == item then 97 | return true 98 | end 99 | end 100 | return false 101 | end 102 | 103 | local MT = { 104 | __index = M, 105 | __add = M.push, 106 | __mul = M.rep, 107 | } 108 | 109 | setmetatable(M, { 110 | __call = function(cls, ...) 111 | return setmetatable({}, MT):push(...) 112 | end, 113 | }) 114 | 115 | return M 116 | -------------------------------------------------------------------------------- /lib/estrela/web.lua: -------------------------------------------------------------------------------- 1 | if not ngx then 2 | return error('[lua-nginx-module](https://github.com/openresty/lua-nginx-module) is required') 3 | end 4 | 5 | local app = require('estrela.ngx.app') 6 | 7 | return { 8 | App = app, 9 | } 10 | -------------------------------------------------------------------------------- /lib/resty/README.md: -------------------------------------------------------------------------------- 1 | COPYRIGHT 2 | ============================================================ 3 | 4 | The sources files in this directory are from: 5 | * upload.lua from [lua-resty-upload](https://github.com/openresty/lua-resty-upload) 6 | -------------------------------------------------------------------------------- /lib/resty/upload.lua: -------------------------------------------------------------------------------- 1 | -- Copyright (C) Yichun Zhang (agentzh) 2 | 3 | 4 | local sub = string.sub 5 | local req_socket = ngx.req.socket 6 | local null = ngx.null 7 | local match = string.match 8 | local setmetatable = setmetatable 9 | local error = error 10 | local get_headers = ngx.req.get_headers 11 | local type = type 12 | -- local print = print 13 | 14 | 15 | local _M = { _VERSION = '0.09' } 16 | 17 | 18 | local MAX_LINE_SIZE = 512 19 | 20 | local STATE_BEGIN = 1 21 | local STATE_READING_HEADER = 2 22 | local STATE_READING_BODY = 3 23 | local STATE_EOF = 4 24 | 25 | 26 | local mt = { __index = _M } 27 | 28 | local state_handlers 29 | 30 | 31 | local function get_boundary() 32 | local header = get_headers()["content-type"] 33 | if not header then 34 | return nil 35 | end 36 | 37 | if type(header) == "table" then 38 | header = header[1] 39 | end 40 | 41 | local m = match(header, ";%s*boundary=\"([^\"]+)\"") 42 | if m then 43 | return m 44 | end 45 | 46 | return match(header, ";%s*boundary=([^\",;]+)") 47 | end 48 | 49 | 50 | function _M.new(self, chunk_size) 51 | local boundary = get_boundary() 52 | 53 | -- print("boundary: ", boundary) 54 | 55 | if not boundary then 56 | return nil, "no boundary defined in Content-Type" 57 | end 58 | 59 | -- print('boundary: "', boundary, '"') 60 | 61 | local sock, err = req_socket() 62 | if not sock then 63 | return nil, err 64 | end 65 | 66 | local read2boundary, err = sock:receiveuntil("--" .. boundary) 67 | if not read2boundary then 68 | return nil, err 69 | end 70 | 71 | local read_line, err = sock:receiveuntil("\r\n") 72 | if not read_line then 73 | return nil, err 74 | end 75 | 76 | return setmetatable({ 77 | sock = sock, 78 | size = chunk_size or 4096, 79 | read2boundary = read2boundary, 80 | read_line = read_line, 81 | boundary = boundary, 82 | state = STATE_BEGIN 83 | }, mt) 84 | end 85 | 86 | 87 | function _M.set_timeout(self, timeout) 88 | local sock = self.sock 89 | if not sock then 90 | return nil, "not initialized" 91 | end 92 | 93 | return sock:settimeout(timeout) 94 | end 95 | 96 | 97 | local function discard_line(self) 98 | local read_line = self.read_line 99 | 100 | local line, err = self.read_line(MAX_LINE_SIZE) 101 | if not line then 102 | return nil, err 103 | end 104 | 105 | local dummy, err = self.read_line(1) 106 | if dummy then 107 | return nil, "line too long: " .. line .. dummy .. "..." 108 | end 109 | 110 | if err then 111 | return nil, err 112 | end 113 | 114 | return 1 115 | end 116 | 117 | 118 | local function discard_rest(self) 119 | local sock = self.sock 120 | local size = self.size 121 | 122 | while true do 123 | local dummy, err = sock:receive(size) 124 | if err and err ~= 'closed' then 125 | return nil, err 126 | end 127 | 128 | if not dummy then 129 | return 1 130 | end 131 | end 132 | end 133 | 134 | 135 | local function read_body_part(self) 136 | local read2boundary = self.read2boundary 137 | 138 | local chunk, err = read2boundary(self.size) 139 | if err then 140 | return nil, nil, err 141 | end 142 | 143 | if not chunk then 144 | local sock = self.sock 145 | 146 | local data = sock:receive(2) 147 | if data == "--" then 148 | local ok, err = discard_rest(self) 149 | if not ok then 150 | return nil, nil, err 151 | end 152 | 153 | self.state = STATE_EOF 154 | return "part_end" 155 | end 156 | 157 | if data ~= "\r\n" then 158 | local ok, err = discard_line(self) 159 | if not ok then 160 | return nil, nil, err 161 | end 162 | end 163 | 164 | self.state = STATE_READING_HEADER 165 | return "part_end" 166 | end 167 | 168 | return "body", chunk 169 | end 170 | 171 | 172 | local function read_header(self) 173 | local read_line = self.read_line 174 | 175 | local line, err = read_line(MAX_LINE_SIZE) 176 | if err then 177 | return nil, nil, err 178 | end 179 | 180 | local dummy, err = read_line(1) 181 | if dummy then 182 | return nil, nil, "line too long: " .. line .. dummy .. "..." 183 | end 184 | 185 | if err then 186 | return nil, nil, err 187 | end 188 | 189 | -- print("read line: ", line) 190 | 191 | if line == "" then 192 | -- after the last header 193 | self.state = STATE_READING_BODY 194 | return read_body_part(self) 195 | end 196 | 197 | local key, value = match(line, "([^: \t]+)%s*:%s*(.+)") 198 | if not key then 199 | return 'header', line 200 | end 201 | 202 | return 'header', {key, value, line} 203 | end 204 | 205 | 206 | local function eof() 207 | return "eof", nil 208 | end 209 | 210 | 211 | function _M.read(self) 212 | local size = self.size 213 | 214 | local handler = state_handlers[self.state] 215 | if handler then 216 | return handler(self) 217 | end 218 | 219 | return nil, nil, "bad state: " .. self.state 220 | end 221 | 222 | 223 | local function read_preamble(self) 224 | local sock = self.sock 225 | if not sock then 226 | return nil, nil, "not initialized" 227 | end 228 | 229 | local size = self.size 230 | local read2boundary = self.read2boundary 231 | 232 | while true do 233 | local preamble, err = read2boundary(size) 234 | if not preamble then 235 | break 236 | end 237 | 238 | -- discard the preamble data chunk 239 | -- print("read preamble: ", preamble) 240 | end 241 | 242 | local ok, err = discard_line(self) 243 | if not ok then 244 | return nil, nil, err 245 | end 246 | 247 | local read2boundary, err = sock:receiveuntil("\r\n--" .. self.boundary) 248 | if not read2boundary then 249 | return nil, nil, err 250 | end 251 | 252 | self.read2boundary = read2boundary 253 | 254 | self.state = STATE_READING_HEADER 255 | return read_header(self) 256 | end 257 | 258 | 259 | state_handlers = { 260 | read_preamble, 261 | read_header, 262 | read_body_part, 263 | eof 264 | } 265 | 266 | 267 | return _M 268 | -------------------------------------------------------------------------------- /nginx.conf.example: -------------------------------------------------------------------------------- 1 | http { 2 | #lua_code_cache off; # раскомментировать для отладки без постоянных nginx reload 3 | 4 | lua_package_path '/path/to/lua/?.lua;/path/to/lua/lib/?.lua;;'; 5 | #lua_shared_dict session_cache 100m; # для хранения сессий в оперативке 6 | 7 | server { 8 | location /estrela { 9 | content_by_lua_file /path/to/lua/index.lua; 10 | } 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /tester.lua: -------------------------------------------------------------------------------- 1 | --[[ 2 | location /estrela_test { 3 | content_by_lua_file $document_root/../lua/tester.lua; 4 | } 5 | 6 | $ luajit tester.lua 'http://ater.me/estrela_test' ./tests 7 | ]] 8 | 9 | local function load_test(name) 10 | return require('tests.' .. name) 11 | end 12 | 13 | if ngx then 14 | return xpcall( 15 | function() 16 | local test_name = ngx.var.arg_test 17 | if not test_name then 18 | return error('Name of test is not defined') 19 | end 20 | 21 | local tester = require('tests.tester.ngx') 22 | 23 | return load_test(test_name)(tester) 24 | end, 25 | 26 | function(err) 27 | local error_header_name = require('tests.tester.common').error_header_name 28 | 29 | ngx.status = ngx.HTTP_BAD_REQUEST 30 | if #err > 1024 then 31 | err = err:sub(1, 1021) .. '...' 32 | end 33 | ngx.header[error_header_name] = err 34 | return ngx.exit(0) 35 | end 36 | ) 37 | else 38 | package.path = './lib/?.lua;' .. package.path 39 | 40 | if #arg < 2 then 41 | print('Usage: ' .. arg[0] .. ' ') 42 | return os.exit(1) 43 | end 44 | 45 | local url = arg[1] 46 | local dir = arg[2] 47 | 48 | local tester = require('tests.tester.cli') 49 | 50 | local tests, err = tester.ls_tests(dir) 51 | if not tests then 52 | return error(err) 53 | end 54 | 55 | local stats = { 56 | ok = 0, 57 | fail = 0, 58 | } 59 | 60 | local max_test_name_len = 0 61 | for _, test_name in ipairs(tests) do 62 | max_test_name_len = math.max(max_test_name_len, #test_name) 63 | end 64 | 65 | local max_test_idx_digits = math.ceil(math.log10(#tests + 1e-5)) 66 | 67 | local test_header_format = '#### Test #%-' .. max_test_idx_digits .. 'd %-' .. max_test_name_len .. 's - ' 68 | 69 | for idx, test_name in ipairs(tests) do 70 | io.write(string.format(test_header_format, idx, test_name)) 71 | 72 | tester.url = url .. '?test=' .. test_name 73 | 74 | tester.remove_cookies() 75 | 76 | local ok, res, descr = xpcall( 77 | function() 78 | local test = load_test(test_name) 79 | if type(test) ~= 'function' then 80 | -- если нет реализации теста, то используем базовую заготовку 81 | test = function(tester) 82 | local body, status, headers = tester.req(tester.url) 83 | 84 | local ok = status and (status.code == 200) 85 | local err = headers and headers[tester.error_header_name] or nil 86 | 87 | return ok, err 88 | end 89 | end 90 | return test(tester) 91 | end, 92 | function(err) 93 | return err 94 | end 95 | ) 96 | 97 | if ok and res then 98 | stats.ok = stats.ok + 1 99 | print('OK') 100 | else 101 | stats.fail = stats.fail + 1 102 | print('ERROR', descr and descr or res) 103 | end 104 | end 105 | 106 | local format = [[Results: 107 | Total: %d 108 | Success: %d 109 | Failed: %d 110 | ]] 111 | 112 | print(string.format( 113 | format, 114 | stats.ok + stats.fail, 115 | stats.ok, 116 | stats.fail 117 | )) 118 | 119 | tester.remove_cookies() 120 | 121 | return os.exit(stats.fail == 0 and 0 or 1) 122 | end 123 | -------------------------------------------------------------------------------- /tests/00_check.lua: -------------------------------------------------------------------------------- 1 | --[[ Проверка работоспособности nginx и lua_nginx в целом 2 | ]] 3 | if ngx then 4 | return function() 5 | return ngx.print(ngx.var.arg_check) 6 | end 7 | else 8 | return function(tester) 9 | math.randomseed(os.time()) 10 | local check = tostring(os.time()) .. tostring(math.random()) 11 | local body, status, headers = tester.req(tester.url, {check=check}) 12 | return assert(body == check, 'lua_nginx check') 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /tests/01_util_table.lua: -------------------------------------------------------------------------------- 1 | if ngx then 2 | return function(tester) 3 | local T = require('estrela.util.table'); 4 | 5 | -- push 6 | (function() 7 | local tbl = {} 8 | T.push(tbl, 42) 9 | assert( 10 | tester.tbl_cmpi( 11 | tbl, 12 | {42} 13 | ), 14 | 'push single' 15 | ) 16 | 17 | T.push(tbl, 1, 2, 3) 18 | assert( 19 | tester.tbl_cmpi( 20 | tbl, 21 | {42, 1, 2, 3} 22 | ), 23 | 'push multiple' 24 | ) 25 | 26 | T.push(tbl, {4, {5}, 6}) 27 | assert( 28 | tester.tbl_cmpi( 29 | tbl, 30 | {42, 1, 2, 3, {4, {5}, 6}} 31 | ), 32 | 'push table' 33 | ) 34 | end)(); 35 | 36 | -- clone 37 | (function() 38 | assert( 39 | T.clone(42) == 42, 40 | 'clone non table' 41 | ) 42 | 43 | local tbl = {} 44 | assert( 45 | tester.tbl_cmp( 46 | T.clone(tbl), 47 | tbl 48 | ), 49 | 'clone empty table' 50 | ) 51 | 52 | local tbl = {1, 2, setmetatable({3}, {1, 2, 3})} 53 | assert( 54 | tester.tbl_cmpi( 55 | T.clone(tbl), 56 | tbl 57 | ), 58 | 'clone table with metatable' 59 | ) 60 | end)(); 61 | 62 | -- rep 63 | (function() 64 | local tbl = {} 65 | assert( 66 | tester.tbl_cmpi( 67 | T.rep(tbl, 3), 68 | {tbl, tbl, tbl} 69 | ), 70 | 'rep empty table' 71 | ) 72 | 73 | local tbl = {42, {7}} 74 | assert( 75 | tester.tbl_cmpi( 76 | T.rep(tbl, 5), 77 | {tbl, tbl, tbl, tbl, tbl} 78 | ), 79 | 'rep table' 80 | ) 81 | 82 | assert( 83 | tester.tbl_cmpi( 84 | T.rep({}, 0), 85 | {} 86 | ), 87 | 'rep zero times' 88 | ) 89 | end)(); 90 | 91 | -- join 92 | (function() 93 | assert( 94 | T.join({}) == '', 95 | 'join empty table w/o sep' 96 | ) 97 | 98 | assert( 99 | T.join({}, '|') == '', 100 | 'join empty table w/ sep' 101 | ) 102 | 103 | assert( 104 | T.join({1, 2, 3}) == '123', 105 | 'join w/o sep' 106 | ) 107 | 108 | assert( 109 | T.join({1, 2, 3}, '|') == '1|2|3', 110 | 'join w/ sep' 111 | ) 112 | 113 | assert( 114 | T.join({1, 2, '4'}, '|') == '1|2|4', 115 | 'join mix item types' 116 | ) 117 | end)(); 118 | 119 | -- len 120 | (function() 121 | assert( 122 | T.len({}) == 0, 123 | 'len empty table' 124 | ) 125 | 126 | assert( 127 | T.len({1, 2, 42, 100500}) == 4, 128 | 'len table' 129 | ) 130 | 131 | assert( 132 | T.len({1, 2, nil, 3}) == 3, 133 | 'len table w/ nil item' 134 | ) 135 | 136 | assert( 137 | T.len({[0]=1, [7]=2, [42]=100500}) == 3, 138 | 'len separate table' 139 | ) 140 | end)(); 141 | 142 | -- list 143 | (function() 144 | local tbl = {1, 2, 17} 145 | local iter = coroutine.wrap(function() 146 | for _, v in ipairs(tbl) do 147 | coroutine.yield(v) 148 | end 149 | end) 150 | 151 | assert( 152 | tester.tbl_cmpi( 153 | T.list(iter), 154 | tbl 155 | ), 156 | 'list' 157 | ) 158 | end)(); 159 | 160 | -- range 161 | (function() 162 | assert( 163 | tester.tbl_cmpi( 164 | T.list(T.range(1, 5)), 165 | {1, 2, 3, 4, 5} 166 | ), 167 | 'range from..to' 168 | ) 169 | 170 | assert( 171 | tester.tbl_cmpi( 172 | T.range(1, 5):list(), 173 | {1, 2, 3, 4, 5} 174 | ), 175 | 'range from..to to list' 176 | ) 177 | 178 | assert( 179 | tester.tbl_cmpi( 180 | T.range(5):list(), 181 | {1, 2, 3, 4, 5} 182 | ), 183 | 'range to' 184 | ) 185 | 186 | assert( 187 | tester.tbl_cmpi( 188 | T.range(1, 10, 2):list(), 189 | {1, 3, 5, 7, 9} 190 | ), 191 | 'range w/ step' 192 | ) 193 | 194 | assert( 195 | tester.tbl_cmpi( 196 | T.range(5, 1, -2):list(), 197 | {5, 3, 1} 198 | ), 199 | 'range w/ neg step' 200 | ) 201 | 202 | assert( 203 | tester.tbl_cmpi( 204 | T.range(5, 1):list(), 205 | {5, 4, 3, 2, 1} 206 | ), 207 | 'range to..from' 208 | ) 209 | 210 | local idx, res = 0, 0 211 | for v in T.range(2, 10, 3) do 212 | idx = idx + 1 213 | res = res + idx * v 214 | end 215 | assert( 216 | res == (1*2 + 2*5 + 3*8), 217 | 'range for in' 218 | ) 219 | 220 | assert( 221 | T.range(1, math.huge), 222 | 'range huge' 223 | ) 224 | 225 | end)(); 226 | 227 | -- contains 228 | (function() 229 | local tbl = {1, 2, 3, {4, 5}} 230 | assert( 231 | T.contains( 232 | tbl, 233 | 3 234 | ), 235 | 'contains exists' 236 | ) 237 | 238 | assert( 239 | not T.contains( 240 | tbl, 241 | 7 242 | ), 243 | 'contains non exists' 244 | ) 245 | 246 | assert( 247 | not T.contains( 248 | tbl, 249 | nil 250 | ), 251 | 'contains nil' 252 | ) 253 | 254 | assert( 255 | not T.contains( 256 | tbl, 257 | {4, 5} 258 | ), 259 | 'contains table' 260 | ) 261 | 262 | local sub_tbl = {4, 5} 263 | local tbl = {1, 2, 3, sub_tbl} 264 | 265 | assert( 266 | T.contains( 267 | tbl, 268 | sub_tbl 269 | ), 270 | 'contains table by ref' 271 | ) 272 | 273 | end)(); 274 | 275 | -- __call 276 | (function() 277 | assert( 278 | tester.tbl_cmpi( 279 | T(1, 2, 3), 280 | {1, 2, 3}, 281 | true 282 | ), 283 | '__call' 284 | ) 285 | end)(); 286 | 287 | -- __add 288 | (function() 289 | assert( 290 | tester.tbl_cmpi( 291 | T(1, 2, 3) + 4, 292 | {1, 2, 3, 4}, 293 | true 294 | ), 295 | '__add' 296 | ) 297 | end)(); 298 | 299 | -- __mul 300 | (function() 301 | assert( 302 | tester.tbl_cmpi( 303 | T(1, 2) * 3, 304 | {{1, 2}, {1, 2}, {1, 2}}, 305 | true 306 | ), 307 | '__mul' 308 | ) 309 | end)(); 310 | end 311 | end 312 | -------------------------------------------------------------------------------- /tests/02_util_string.lua: -------------------------------------------------------------------------------- 1 | if ngx then 2 | return function(tester) 3 | local S = require('estrela.util.string'); 4 | 5 | local str = 'hello world yep' 6 | 7 | -- find_first 8 | (function() 9 | assert( 10 | S.find_first(str, 'w') == 7, 11 | 'find_first uniq [sub]' 12 | ) 13 | 14 | assert( 15 | S.find_first(str, 'W') == nil, 16 | 'find_first case sensitivity' 17 | ) 18 | 19 | assert( 20 | S.find_first(str, 'l') == 3, 21 | 'find_first ordinary [sub]' 22 | ) 23 | 24 | assert( 25 | S.find_first(str, 'l', 3) == 3, 26 | 'find_first with [start]' 27 | ) 28 | 29 | assert( 30 | S.find_first(str, 'l', 6) == 10, 31 | 'find_first with [start] after first appearance' 32 | ) 33 | 34 | assert( 35 | S.find_first(str, 'l', 1, 3) == 3, 36 | 'find_first with [stop]' 37 | ) 38 | 39 | assert( 40 | S.find_first(str, 'l', 1, 2) == nil, 41 | 'find_first with [stop] before first appearance' 42 | ) 43 | 44 | assert( 45 | S.find_first(str, 'yep') == 13, 46 | 'find_first multichar [sub]' 47 | ) 48 | 49 | assert( 50 | S.find_first(str, 'yep!') == nil, 51 | 'find_first over-tail check' 52 | ) 53 | end)(); 54 | 55 | -- find_last 56 | (function() 57 | assert( 58 | S.find_last(str, 'w') == 7, 59 | 'find_last uniq [sub]' 60 | ) 61 | 62 | assert( 63 | S.find_last(str, 'W') == nil, 64 | 'find_last case sensitivity' 65 | ) 66 | 67 | assert( 68 | S.find_last(str, 'l') == 10, 69 | 'find_last ordinary [sub]' 70 | ) 71 | 72 | assert( 73 | S.find_last(str, 'l', 3) == 10, 74 | 'find_last with [start]' 75 | ) 76 | 77 | assert( 78 | S.find_last(str, 'l', 1, 9) == 4, 79 | 'find_last with [stop] before last appearance' 80 | ) 81 | 82 | assert( 83 | S.find_last(str, 'l', 1, 3) == 3, 84 | 'find_last with [stop]' 85 | ) 86 | 87 | assert( 88 | S.find_last(str, 'l', 12) == nil, 89 | 'find_last with [start] after last appearance' 90 | ) 91 | 92 | assert( 93 | S.find_last(str, 'yep') == 13, 94 | 'find_last multichar [sub]' 95 | ) 96 | 97 | assert( 98 | S.find_last(str, 'yep!') == nil, 99 | 'find_last over-tail check' 100 | ) 101 | end)(); 102 | 103 | -- split 104 | (function() 105 | assert( 106 | tester.tbl_cmpi( 107 | S.split('hello'), 108 | {'h', 'e', 'l', 'l', 'o'} 109 | ), 110 | 'split w/o [sep]' 111 | ) 112 | 113 | assert( 114 | tester.tbl_cmpi( 115 | S.split('hello', ''), 116 | {'h', 'e', 'l', 'l', 'o'} 117 | ), 118 | 'split with empty [sep]' 119 | ) 120 | 121 | assert( 122 | tester.tbl_cmpi( 123 | S.split(str, '|'), 124 | {str} 125 | ), 126 | 'split with nonexistent [sep]' 127 | ) 128 | 129 | assert( 130 | tester.tbl_cmpi( 131 | S.split(str, ' '), 132 | {'hello', 'world', 'yep'} 133 | ), 134 | [[split with [sep] = ' ']] 135 | ) 136 | 137 | assert( 138 | tester.tbl_cmpi( 139 | S.split(str, ' ', 1), 140 | {str} 141 | ), 142 | [[split with [maxsplit] = 1]] 143 | ) 144 | 145 | assert( 146 | tester.tbl_cmpi( 147 | S.split(str, ' ', 2), 148 | {'hello', 'world yep'} 149 | ), 150 | [[split with [maxsplit] = 2]] 151 | ) 152 | 153 | assert( 154 | tester.tbl_cmpi( 155 | S.split(str, ' ', 42), 156 | {'hello', 'world', 'yep'} 157 | ), 158 | [[split with [maxsplit] = 42]] 159 | ) 160 | 161 | assert( 162 | tester.tbl_cmpi( 163 | S.split(str .. ' ', ' '), 164 | {'hello', 'world', 'yep', ''} 165 | ), 166 | [[split with empty section]] 167 | ) 168 | 169 | assert( 170 | tester.tbl_cmpi( 171 | S.split(str, '%s', nil, true), 172 | {'hello', 'world', 'yep'} 173 | ), 174 | [[split with regex [sep] = '%s']] 175 | ) 176 | 177 | assert( 178 | tester.tbl_cmpi( 179 | S.split(str, '%s', nil, true), 180 | {'hello', 'world', 'yep'} 181 | ), 182 | [[split with regex [sep] = '%s']] 183 | ) 184 | 185 | assert( 186 | tester.tbl_cmpi( 187 | S.split('hello world! yep:)', '%W+', nil, true), 188 | {'hello', 'world', 'yep', ''} 189 | ), 190 | [[split with regex [sep] = '%W+']] 191 | ) 192 | end)(); 193 | 194 | -- ltrim 195 | (function() 196 | assert( 197 | S.ltrim(str) == str, 198 | 'ltrim w/o spaces' 199 | ) 200 | 201 | assert( 202 | S.ltrim(' ' .. str) == str, 203 | 'ltrim w/ left spaces' 204 | ) 205 | 206 | assert( 207 | S.ltrim(str .. ' ') == str .. ' ', 208 | 'ltrim w/ right spaces' 209 | ) 210 | 211 | assert( 212 | S.ltrim(' ' .. str .. ' ') == str .. ' ', 213 | 'ltrim w/ left and right spaces' 214 | ) 215 | 216 | assert( 217 | S.ltrim(' \r\n\v' .. str .. ' \r\n\v') == str .. ' \r\n\v', 218 | 'ltrim w/ special chars' 219 | ) 220 | 221 | assert( 222 | S.ltrim(str, 'helo ') == 'world yep', 223 | 'ltrim w/ [chars]' 224 | ) 225 | 226 | assert( 227 | S.ltrim(str, '') == str, 228 | 'ltrim w/ empty [chars]' 229 | ) 230 | end)(); 231 | 232 | -- rtrim 233 | (function() 234 | assert( 235 | S.rtrim(str) == str, 236 | 'rtrim w/o spaces' 237 | ) 238 | 239 | assert( 240 | S.rtrim(' ' .. str) == ' ' .. str, 241 | 'rtrim w/ left spaces' 242 | ) 243 | 244 | assert( 245 | S.rtrim(str .. ' ') == str, 246 | 'rtrim w/ right spaces' 247 | ) 248 | 249 | assert( 250 | S.rtrim(' ' .. str .. ' ') == ' ' .. str, 251 | 'rtrim w/ left and right spaces' 252 | ) 253 | 254 | assert( 255 | S.rtrim(' \r\n\v' .. str .. ' \r\n\v') == ' \r\n\v' .. str, 256 | 'rtrim w/ special chars' 257 | ) 258 | 259 | assert( 260 | S.rtrim(str, 'yepdlrow ') == 'h', 261 | 'rtrim w/ [chars]' 262 | ) 263 | 264 | assert( 265 | S.rtrim(str, '') == str, 266 | 'rtrim w/ empty [chars]' 267 | ) 268 | end)(); 269 | 270 | -- trim 271 | (function() 272 | assert( 273 | S.trim(str) == str, 274 | 'trim w/o spaces' 275 | ) 276 | 277 | assert( 278 | S.trim(' ' .. str) == str, 279 | 'trim w/ left spaces' 280 | ) 281 | 282 | assert( 283 | S.trim(str .. ' ') == str, 284 | 'trim w/ right spaces' 285 | ) 286 | 287 | assert( 288 | S.trim(' ' .. str .. ' ') == str, 289 | 'trim w/ left and right spaces' 290 | ) 291 | 292 | assert( 293 | S.trim(' \r\n\v' .. str .. ' \r\n\v') == str, 294 | 'trim w/ special chars' 295 | ) 296 | 297 | assert( 298 | S.trim(str, 'yepdhelo ') == 'wor', 299 | 'trim w/ [chars]' 300 | ) 301 | 302 | assert( 303 | S.trim(str, '') == str, 304 | 'trim w/ empty [chars]' 305 | ) 306 | end)(); 307 | 308 | -- htmlencode 309 | (function() 310 | assert( 311 | S.htmlencode('') == '', 312 | 'htmlencode empty [str]' 313 | ) 314 | 315 | assert( 316 | S.htmlencode(str) == str, 317 | 'htmlencode w/o special chars' 318 | ) 319 | 320 | assert( 321 | S.htmlencode([[A'C'&"D"&E]]) == [[A<B>'C'&"D"&amp;E</F>]], 322 | 'htmlencode w/o quotes' 323 | ) 324 | 325 | local res = [[A<B>'C'&"D"&amp;E</F>]] 326 | assert( 327 | S.htmlencode([[A'C'&"D"&E]], true) == res, 328 | 'htmlencode w/ quotes' 329 | ) 330 | end)(); 331 | 332 | -- starts 333 | (function() 334 | assert( 335 | S.starts(str, 'hello'), 336 | 'starts' 337 | ) 338 | 339 | assert( 340 | not S.starts(str, 'HELLO'), 341 | 'starts case sensitivity' 342 | ) 343 | 344 | assert( 345 | S.starts(str, ''), 346 | 'starts w/ empty [prefix]' 347 | ) 348 | 349 | assert( 350 | not S.starts(str, 'world'), 351 | 'starts [prefix] inside [str]' 352 | ) 353 | 354 | assert( 355 | not S.starts(str, 'helloworld'), 356 | 'starts over-prefix' 357 | ) 358 | 359 | assert( 360 | S.starts(str .. ' fooBar', str), 361 | 'starts over-str' 362 | ) 363 | 364 | assert( 365 | not S.starts(str, str .. ' over-tail'), 366 | 'starts over-tail' 367 | ) 368 | end)(); 369 | 370 | -- ends 371 | (function() 372 | assert( 373 | S.ends(str, 'yep'), 374 | 'ends' 375 | ) 376 | 377 | assert( 378 | not S.ends(str, 'YEP'), 379 | 'ends case sensitivity' 380 | ) 381 | 382 | assert( 383 | S.ends(str, ''), 384 | 'ends w/ empty [suffix]' 385 | ) 386 | 387 | assert( 388 | not S.ends(str, 'world'), 389 | 'ends [suffix] inside [str]' 390 | ) 391 | 392 | assert( 393 | not S.ends(str, 'worldyep'), 394 | 'ends over-suffix' 395 | ) 396 | 397 | assert( 398 | S.ends('fooBar ' .. str, str), 399 | 'ends over-str' 400 | ) 401 | 402 | assert( 403 | not S.ends(str, 'over-head ' .. str), 404 | 'ends over-head' 405 | ) 406 | end)(); 407 | 408 | -- parse_header_value 409 | (function() 410 | local _headers = { 411 | 'simple-scalar-string', 412 | 'string;key=value1;key2=value2', 413 | 'string;key=value1;key=value2', 414 | 'na/me; key=value; ' 415 | .. '__utmz=1.111.11.11.utmcsr=foo|utmccn=(organic)|utmcmd=organic|utmctr=(not%20provided); ' 416 | .. 'tz=Europe%2FMoscow; user_session=d1K_fuBk11; filename="file.txt"', 417 | } 418 | 419 | local headers = {} 420 | 421 | for idx, line in ipairs(_headers) do 422 | local parsed = S.parse_header_value(line) 423 | assert( 424 | type(parsed) == 'table', 425 | 'parse_header_value' 426 | ) 427 | headers[idx] = parsed 428 | end 429 | 430 | assert( 431 | headers[1][''] == _headers[1], 432 | 'parse_header_value scalar' 433 | ) 434 | 435 | assert( 436 | headers[2][''] == 'string' 437 | and headers[2].key 438 | and headers[2].key == 'value1' 439 | and headers[2].key2 440 | and headers[2].key2 == 'value2', 441 | 'parse_header_value table w/ scalar key values' 442 | ) 443 | 444 | assert( 445 | headers[3][''] == 'string' 446 | and type(headers[3].key) == 'table' 447 | and tester.tbl_cmpi(headers[3].key, {'value1', 'value2'}), 448 | 'parse_header_value table w/ table key values' 449 | ) 450 | 451 | assert( 452 | tester.tbl_cmp(headers[4], { 453 | [''] = 'na/me', 454 | key = 'value', 455 | __utmz = '1.111.11.11.utmcsr=foo|utmccn=(organic)|utmcmd=organic|utmctr=(not%20provided)', 456 | tz = 'Europe%2FMoscow', 457 | user_session = 'd1K_fuBk11', 458 | filename = 'file.txt', 459 | }), 460 | 'parse_header_value mix' 461 | ) 462 | end)(); 463 | 464 | -- cmpi 465 | (function() 466 | assert( 467 | S.cmpi(str, str), 468 | 'cmpi' 469 | ) 470 | 471 | assert( 472 | S.cmpi(str, str:upper()), 473 | 'cmpi w/ upper self' 474 | ) 475 | 476 | assert( 477 | S.cmpi(str, str:lower()), 478 | 'cmpi w/ lower self' 479 | ) 480 | 481 | assert( 482 | not S.cmpi(str, ''), 483 | 'cmpi empty [str2]' 484 | ) 485 | 486 | assert( 487 | not S.cmpi('', str), 488 | 'cmpi empty [str1]' 489 | ) 490 | 491 | assert( 492 | not S.cmpi(str, 'chpachryamba'), 493 | 'cmpi diffs' 494 | ) 495 | end)(); 496 | 497 | -- format 498 | (function() 499 | local _str = 'hello %s' 500 | assert( 501 | S.format(_str) == 'hello nil', 502 | 'format w/o [vars]' 503 | ) 504 | 505 | assert( 506 | S.format(_str, 'world') == 'hello world', 507 | 'format w/ scalar [vars]' 508 | ) 509 | 510 | assert( 511 | S.format(_str, {'world'}) == 'hello world', 512 | 'format w/ table [vars]' 513 | ) 514 | end)(); 515 | 516 | -- apply_patch 517 | (function() 518 | S.apply_patch() 519 | 520 | local _str = 'foo' 521 | 522 | assert( 523 | string.starts and _str.trim, 524 | 'apply_patch mapping' 525 | ) 526 | 527 | assert( 528 | ('hello %s' % 'world') == 'hello world', 529 | 'apply_patch % scalar' 530 | ) 531 | 532 | assert( 533 | ('%s %s' % {'hello', 'world'}) == 'hello world', 534 | 'apply_patch % table' 535 | ) 536 | end)(); 537 | end 538 | end 539 | -------------------------------------------------------------------------------- /tests/03_util_path.lua: -------------------------------------------------------------------------------- 1 | if ngx then 2 | return function(tester) 3 | local P = require('estrela.util.path'); 4 | 5 | -- split 6 | (function() 7 | local info = P.split('/path/to/file.ext') 8 | assert( 9 | info.path == '/path/to/' 10 | and info.name == 'file.ext' 11 | and info.ext == 'ext', 12 | 'split' 13 | ) 14 | 15 | local info = P.split('/path/to/') 16 | assert( 17 | info.path == '/path/to/' 18 | and info.name == '' 19 | and info.ext == '', 20 | 'split only path' 21 | ) 22 | 23 | local info = P.split('/path/to') 24 | assert( 25 | info.path == '/path/' 26 | and info.name == 'to' 27 | and info.ext == '', 28 | 'split w/o ext' 29 | ) 30 | 31 | local info = P.split('/') 32 | assert( 33 | info.path == '/' 34 | and info.name == '' 35 | and info.ext == '', 36 | 'split root' 37 | ) 38 | 39 | local info = P.split('filename') 40 | assert( 41 | info.path == '' 42 | and info.name == 'filename' 43 | and info.ext == '', 44 | 'split only filename' 45 | ) 46 | end)(); 47 | 48 | -- rel 49 | (function() 50 | 51 | assert( 52 | tester.tbl_cmpi( 53 | {P.rel('/path/to/estrela', '/path/to/estrela/bootstrap.lua')}, 54 | {'bootstrap.lua', 0} 55 | ), 56 | 'rel in workdir' 57 | ) 58 | 59 | assert( 60 | tester.tbl_cmpi( 61 | {P.rel('/path/to/estrela', '/path/to/estrela/foo/bar/bootstrap.lua')}, 62 | {'foo/bar/bootstrap.lua', 0} 63 | ), 64 | 'rel downward path' 65 | ) 66 | 67 | assert( 68 | tester.tbl_cmpi( 69 | {P.rel('/path/to/estrela', '/path/foo/bar/bootstrap.lua')}, 70 | {'../../foo/bar/bootstrap.lua', 2} 71 | ), 72 | 'rel upward+downward path' 73 | ) 74 | 75 | assert( 76 | tester.tbl_cmpi( 77 | {P.rel('/path/to/lua/lib/estrela', '/bootstrap.lua')}, 78 | {'../../../../../bootstrap.lua', 5} 79 | ), 80 | 'rel upward path' 81 | ) 82 | 83 | assert( 84 | tester.tbl_cmpi( 85 | {P.rel('/path/to/lua/lib/estrela', '/foo/bar/bootstrap.lua')}, 86 | {'../../../../../foo/bar/bootstrap.lua', 5} 87 | ), 88 | 'rel upward to root+downward path' 89 | ) 90 | 91 | assert( 92 | tester.tbl_cmpi( 93 | {P.rel('/', '/path/to/lua/bootstrap.lua')}, 94 | {'path/to/lua/bootstrap.lua', 0} 95 | ), 96 | 'rel downward from root path' 97 | ) 98 | end)(); 99 | 100 | -- join 101 | (function() 102 | assert( 103 | P.join('/') == '/', 104 | [[join('/')]] 105 | ) 106 | 107 | assert( 108 | P.join('/', 'foo', 'bar') == '/foo/bar', 109 | [[join('/', 'foo', 'bar')]] 110 | ) 111 | 112 | assert( 113 | P.join('/path', 'to', 'file') == '/path/to/file', 114 | [[join('/path', 'to', 'file')]] 115 | ) 116 | 117 | assert( 118 | P.join('/path/', 'to', 'file') == '/path/to/file', 119 | [[join('/path/', 'to', 'file')]] 120 | ) 121 | 122 | assert( 123 | P.join('/path/', 'to/', 'file/') == '/path/to/file', 124 | [[join('/path/', 'to/', 'file/')]] 125 | ) 126 | end)(); 127 | end 128 | end 129 | -------------------------------------------------------------------------------- /tests/04_util_env.lua: -------------------------------------------------------------------------------- 1 | if ngx then 2 | return function(tester) 3 | 4 | -- get_root 5 | (function() 6 | -- monkey patching :) 7 | local debug = require('debug') 8 | debug.getinfo = function() 9 | return { 10 | source = '@/path/to/util/env.lua', 11 | } 12 | end 13 | 14 | local E = require('estrela.util.env') 15 | 16 | assert( 17 | E.get_root() == '/path/to', 18 | 'get_root' 19 | ) 20 | end)(); 21 | end 22 | end 23 | -------------------------------------------------------------------------------- /tests/05_io_ob.lua: -------------------------------------------------------------------------------- 1 | local tests = { 2 | { 3 | name = 'without_ob', 4 | code = function(tester) 5 | ngx.print 'ok' 6 | end, 7 | checker = function(tester, test, body, status, headers) 8 | return assert(body == 'ok', test.name) 9 | end, 10 | }, 11 | 12 | -- print 13 | { 14 | name = 'print_wo_buff', 15 | code = function(tester, test) 16 | local OB = require('estrela.io.ob') 17 | OB.print('hello', 'world') 18 | OB.print(nil) 19 | end, 20 | checker = function(tester, test, body, status, headers) 21 | return assert(body == 'helloworld', test.name) 22 | end, 23 | }, 24 | 25 | -- println 26 | { 27 | name = 'println_wo_buff', 28 | code = function(tester, test) 29 | local OB = require('estrela.io.ob') 30 | OB.println('hello', 'world') 31 | OB.println('traveler!') 32 | OB.println() 33 | end, 34 | checker = function(tester, test, body, status, headers) 35 | return assert(body == 'helloworld\ntraveler!\n\n', test.name) 36 | end, 37 | }, 38 | 39 | -- print_lua 40 | { 41 | name = 'print_lua_wo_buff', 42 | code = function(tester, test) 43 | local OB = require('estrela.io.ob') 44 | OB.print_lua('hello', 'world') 45 | OB.print_lua() 46 | end, 47 | checker = function(tester, test, body, status, headers) 48 | return assert(body == 'hello\tworld\n\n', test.name) 49 | end, 50 | }, 51 | 52 | -- start 53 | { 54 | name = 'start_wo_finish', 55 | code = function(tester, test) 56 | local OB = require('estrela.io.ob') 57 | ngx.print 'hello' 58 | OB.start() 59 | ngx.print 'world' 60 | -- without OB.finish() 61 | end, 62 | checker = function(tester, test, body, status, headers) 63 | return assert(body == 'hello', test.name) 64 | end, 65 | }, 66 | 67 | -- finish 68 | { 69 | name = 'start_with_finish', 70 | code = function(tester, test) 71 | local OB = require('estrela.io.ob') 72 | ngx.print 'hello' 73 | OB.start() 74 | ngx.print 'world' 75 | OB.finish() 76 | end, 77 | checker = function(tester, test, body, status, headers) 78 | return assert(body == 'hello', test.name) 79 | end, 80 | }, 81 | 82 | -- flush_finish 83 | { 84 | name = 'start_with_flush_finish', 85 | code = function(tester, test) 86 | local OB = require('estrela.io.ob') 87 | ngx.print 'hello' 88 | OB.start() 89 | ngx.print 'world' 90 | OB.flush_finish() 91 | end, 92 | checker = function(tester, test, body, status, headers) 93 | return assert(body == 'helloworld', test.name) 94 | end, 95 | }, 96 | 97 | { 98 | name = 'start_with_cb_with_flush_finish', 99 | code = function(tester, test) 100 | local OB = require('estrela.io.ob') 101 | ngx.say 'hello' 102 | OB.start(function(chunk) return string.upper(chunk) end) 103 | ngx.print 'world' 104 | OB.flush_finish() 105 | end, 106 | checker = function(tester, test, body, status, headers) 107 | return assert(body == 'hello\nWORLD', test.name) 108 | end, 109 | }, 110 | 111 | { 112 | name = 'double_start_with_cb_with_flush_finish', 113 | code = function(tester, test) 114 | local OB = require('estrela.io.ob') 115 | ngx.print 'hello' 116 | OB.start(function(chunk) return string.upper(chunk) end) 117 | ngx.print 'world' 118 | OB.start(function(chunk) return string.sub(chunk, 1, 2) end) 119 | ngx.print 'traveler' 120 | OB.flush_finish() 121 | OB.flush_finish() 122 | end, 123 | checker = function(tester, test, body, status, headers) 124 | return assert(body == 'helloWORLDTR', test.name) 125 | end, 126 | }, 127 | 128 | { 129 | name = 'double_start_with_cb_with_only_one_flush_finish', 130 | code = function(tester, test) 131 | local OB = require('estrela.io.ob') 132 | ngx.print 'hello' 133 | OB.start(function(chunk) return string.upper(chunk) end) 134 | ngx.print 'world' 135 | OB.start(function(chunk) return string.sub(chunk, 1, 2) end) 136 | ngx.print 'traveler' 137 | OB.flush_finish() 138 | -- without OB.flush_finish() 139 | end, 140 | checker = function(tester, test, body, status, headers) 141 | return assert(body == 'hello', test.name) 142 | end, 143 | }, 144 | 145 | { 146 | name = 'start_with_double_flush_finish', 147 | code = function(tester, test) 148 | local OB = require('estrela.io.ob') 149 | ngx.print 'hello' 150 | OB.start() 151 | ngx.print 'world' 152 | OB.flush_finish() 153 | 154 | OB.flush_finish() 155 | end, 156 | checker = function(tester, test, body, status, headers) 157 | return assert(body == 'helloworld', test.name) 158 | end, 159 | }, 160 | 161 | -- flush 162 | { 163 | name = 'start_with_flush', 164 | code = function(tester, test) 165 | local OB = require('estrela.io.ob') 166 | ngx.print 'hello' 167 | OB.start() 168 | ngx.print 'world' 169 | OB.flush() 170 | ngx.print 'traveler' 171 | -- without OB.finish() 172 | end, 173 | checker = function(tester, test, body, status, headers) 174 | return assert(body == 'helloworld', test.name) 175 | end, 176 | }, 177 | 178 | -- get 179 | { 180 | name = 'get_after_start', 181 | code = function(tester, test) 182 | local OB = require('estrela.io.ob') 183 | ngx.print 'hello' 184 | OB.start() 185 | print('world', 'traveler') 186 | io.write('!') 187 | local buf = OB.get() 188 | OB.flush_finish() 189 | 190 | ngx.print(buf) 191 | end, 192 | checker = function(tester, test, body, status, headers) 193 | return assert(body == 'helloworld\ttraveler\n!world\ttraveler\n!', test.name) 194 | end, 195 | }, 196 | 197 | { 198 | name = 'get_before_start', 199 | code = function(tester, test) 200 | local OB = require('estrela.io.ob') 201 | ngx.print(type(OB.get())) 202 | end, 203 | checker = function(tester, test, body, status, headers) 204 | return assert(body == 'nil', test.name) 205 | end, 206 | }, 207 | 208 | -- levels 209 | { 210 | name = 'levels', 211 | code = function(tester, test) 212 | local OB = require('estrela.io.ob') 213 | local levels = {} 214 | table.insert(levels, OB.levels()) 215 | OB.start() 216 | table.insert(levels, OB.levels()) 217 | OB.start() 218 | table.insert(levels, OB.levels()) 219 | OB.flush() 220 | table.insert(levels, OB.levels()) 221 | OB.finish() 222 | table.insert(levels, OB.levels()) 223 | OB.flush_finish() 224 | table.insert(levels, OB.levels()) 225 | 226 | ngx.print(table.concat(levels, ',')) 227 | end, 228 | checker = function(tester, test, body, status, headers) 229 | return assert(body == '0,1,2,2,1,0', test.name) 230 | end, 231 | }, 232 | 233 | -- len 234 | { 235 | name = 'len', 236 | code = function(tester, test) 237 | local OB = require('estrela.io.ob') 238 | local len = {} 239 | table.insert(len, OB.len()) 240 | ngx.say(test.__prefix) 241 | table.insert(len, OB.len()) 242 | OB.start() 243 | ngx.print 'a' 244 | table.insert(len, OB.len()) 245 | ngx.print 'b' 246 | OB.start() 247 | ngx.print 'c' 248 | table.insert(len, OB.len()) 249 | ngx.print 'd' 250 | OB.flush() 251 | ngx.print 'e' 252 | table.insert(len, OB.len()) 253 | ngx.print 'f' 254 | OB.finish() 255 | ngx.print 'g' 256 | table.insert(len, OB.len()) 257 | ngx.print 'h' 258 | OB.flush_finish() 259 | ngx.print 'i' 260 | table.insert(len, OB.len()) 261 | 262 | ngx.print(table.concat(len, ',')) 263 | end, 264 | checker = function(tester, test, body, status, headers) 265 | return assert(body == test.__prefix .. '\nabcdghi1,1,1,5', test.name) 266 | end, 267 | __prefix = 'ufheforgofrygforgf7r9fyrurhyeguyrqeg7e', 268 | }, 269 | 270 | -- clean 271 | { 272 | name = 'clean', 273 | code = function(tester, test) 274 | local OB = require('estrela.io.ob') 275 | ngx.print 'hello' 276 | OB.start() 277 | ngx.print 'world' 278 | OB.clean() 279 | OB.flush_finish() 280 | end, 281 | checker = function(tester, test, body, status, headers) 282 | return assert(body == 'hello', test.name) 283 | end, 284 | }, 285 | 286 | -- status 287 | { 288 | name = 'status', 289 | code = function(tester, test) 290 | local OB = require('estrela.io.ob') 291 | 292 | local cb = function(chunk) return chunk end 293 | 294 | OB.start() 295 | ngx.print 'hello' 296 | OB.start(cb) 297 | ngx.print 'world!' 298 | local status = OB.status() 299 | OB.flush_finish() 300 | OB.flush_finish() 301 | 302 | assert( 303 | tester.tbl_cmp( 304 | status, 305 | { 306 | { 307 | len = 5, 308 | cb = nil, 309 | }, 310 | { 311 | len = 6, 312 | cb = cb, 313 | }, 314 | } 315 | ), 316 | test.name 317 | ) 318 | ngx.say 'hello' 319 | end, 320 | checker = function(tester, test, body, status, headers) 321 | return assert(body == 'helloworld!hello\n', test.name) 322 | end, 323 | }, 324 | } 325 | 326 | if ngx then 327 | return function(tester) 328 | return tester:multitest_srv(tests) 329 | end 330 | else 331 | return function(tester) 332 | return tester:multitest_cli(tests) 333 | end 334 | end 335 | -------------------------------------------------------------------------------- /tests/06_io_pprint.lua: -------------------------------------------------------------------------------- 1 | local tests = { 2 | -- sprint 3 | { 4 | name = 'sprint_basic', 5 | code = function(tester, test) 6 | local sprint = require('estrela.io.pprint').sprint 7 | 8 | assert( 9 | sprint() == 'nil\n', 10 | test.name .. ' w/o params' 11 | ) 12 | 13 | assert( 14 | sprint(nil) == 'nil\n', 15 | test.name .. ' nil' 16 | ) 17 | 18 | assert( 19 | sprint('') == '""\n', 20 | test.name .. ' empty string' 21 | ) 22 | 23 | assert( 24 | sprint(0) == '0\n', 25 | test.name .. ' number' 26 | ) 27 | 28 | assert( 29 | sprint('hello') == '"hello"\n', 30 | test.name .. ' string' 31 | ) 32 | 33 | assert( 34 | sprint({}) == '{\n}\n', 35 | test.name .. ' empty table' 36 | ) 37 | 38 | assert( 39 | sprint({'hello', 42, 'world'}) == '{\n\t[1] = "hello",\n\t[2] = 42,\n\t[3] = "world",\n}\n', 40 | test.name .. ' table' 41 | ) 42 | 43 | assert( 44 | sprint(true) == 'true\n', 45 | test.name .. ' boolean' 46 | ) 47 | 48 | assert( 49 | sprint(ngx.null) == 'nil --[[userdata]]\n', 50 | test.name .. ' userdata' 51 | ) 52 | 53 | -- nil --[[function: 0x41209210 .../../../tests/6_io_pprint.lua:10]] 54 | local res = [[^nil --\[\[function: 0x[0-9a-f]+ .*io_pprint\.lua:[0-9]+\]\]\s$]] 55 | 56 | assert( 57 | ngx.re.match(sprint(function() end), res), 58 | test.name .. ' function' 59 | ) 60 | end, 61 | checker = function(tester, test, body, status, headers) 62 | end, 63 | }, 64 | 65 | { 66 | name = 'sprint_compact', 67 | code = function(tester, test) 68 | local sprint = require('estrela.io.pprint').sprint 69 | 70 | assert( 71 | sprint(0, {compact=true}) == '0', 72 | test.name .. ' number' 73 | ) 74 | 75 | assert( 76 | sprint({}, {compact=true}) == '{}', 77 | test.name .. ' empty table' 78 | ) 79 | 80 | assert( 81 | sprint({'hello', 42, 'world'}, {compact=true}) == '{[1] = "hello",[2] = 42,[3] = "world",}', 82 | test.name .. ' table' 83 | ) 84 | end, 85 | checker = function(tester, test, body, status, headers) 86 | end, 87 | }, 88 | 89 | { 90 | name = 'sprint_max_depth', 91 | code = function(tester, test) 92 | local sprint = require('estrela.io.pprint').sprint 93 | 94 | local res = '{[1] = 1,[2] = {[1] = 2,[2] = {[1] = 3,[2] = 4,},[3] = 5,},[3] = 6,}' 95 | assert( 96 | sprint({1, {2, {3, 4}, 5}, 6}, {compact=true, max_depth=5}) == res, 97 | test.name .. ' max_depth > table depth' 98 | ) 99 | 100 | local res = '{[1] = 1,[2] = {[1] = 2,[2] = nil --[[max_depth cutted]],[3] = 5,},[3] = 6,}' 101 | assert( 102 | sprint({1, {2, {3, 4}, 5}, 6}, {compact=true, max_depth=1}) == res, 103 | test.name .. ' max_depth < table depth' 104 | ) 105 | 106 | local res = '{[1] = "a",[2] = {[{[1] = "b",[2] = nil --[[max_depth cutted]],}] = {[nil --[[max_depth cutted]]] = nil --[[max_depth cutted]],},},}' 107 | assert( 108 | sprint({'a', {[{'b', {'c'}}] = {[{'d'}] = {'e', 'f'}}}}, {compact=true, max_depth=2}) == res, 109 | test.name .. ' max_depth < table depth w/ tables as keys' 110 | ) 111 | end, 112 | checker = function(tester, test, body, status, headers) 113 | end, 114 | }, 115 | 116 | { 117 | name = 'sprint_func_pathes', 118 | code = function(tester, test) 119 | local sprint = require('estrela.io.pprint').sprint 120 | 121 | -- nil --[[function: 0x41209210]] 122 | local res = [[^nil --\[\[function: 0x[0-9a-f]+\]\]\s$]] 123 | 124 | assert( 125 | ngx.re.match(sprint(function() end, {func_pathes=false}), res), 126 | test.name 127 | ) 128 | end, 129 | checker = function(tester, test, body, status, headers) 130 | end, 131 | }, 132 | 133 | { 134 | name = 'sprint_func_patches_short', 135 | code = function(tester, test) 136 | local sprint = require('estrela.io.pprint').sprint 137 | 138 | local f = function() end 139 | 140 | assert( 141 | sprint(f, {func_patches_short=false}) ~= sprint(f, {func_patches_short=true}), 142 | test.name 143 | ) 144 | end, 145 | checker = function(tester, test, body, status, headers) 146 | end, 147 | }, 148 | 149 | { 150 | name = 'sprint_func_patches_short_rel', 151 | code = function(tester, test) 152 | local sprint = require('estrela.io.pprint').sprint 153 | 154 | local f = function() end 155 | 156 | assert( 157 | sprint(f, {func_patches_short_rel=100500}) == sprint(f, {func_patches_short=true}), 158 | test.name .. ' rel 100500' 159 | ) 160 | 161 | assert( 162 | sprint(f, {func_patches_short_rel=0}) == sprint(f, {func_patches_short=false}), 163 | test.name .. ' rel 1' 164 | ) 165 | end, 166 | checker = function(tester, test, body, status, headers) 167 | end, 168 | }, 169 | 170 | -- print 171 | { 172 | name = 'print', 173 | code = function(tester, test) 174 | local pprint = require('estrela.io.pprint').print; 175 | 176 | -- с подменой writer'а 177 | (function() 178 | local data = {} 179 | local function writer(chunk) 180 | table.insert(data, chunk) 181 | end 182 | 183 | pprint({'hello', 42, 'world'}, {compact=true, writer=writer}) 184 | 185 | data = table.concat(data) 186 | 187 | assert( 188 | data == '{[1] = "hello",[2] = 42,[3] = "world",}', 189 | test.name .. ' w/ writer' 190 | ) 191 | end)(); 192 | 193 | -- стандартный вывод через ngx.print 194 | (function() 195 | local data = {} 196 | local _ngx_print = ngx.print 197 | ngx.print = function(chunk) 198 | table.insert(data, chunk) 199 | end 200 | 201 | pprint({'hello', 42, 'world'}, {compact=true}) 202 | 203 | ngx.print = _ngx_print 204 | 205 | data = table.concat(data) 206 | 207 | assert( 208 | data == '{[1] = "hello",[2] = 42,[3] = "world",}', 209 | test.name .. ' w/o writer' 210 | ) 211 | end)(); 212 | end, 213 | checker = function(tester, test, body, status, headers) 214 | end, 215 | }, 216 | } 217 | 218 | if ngx then 219 | return function(tester) 220 | return tester:multitest_srv(tests) 221 | end 222 | else 223 | return function(tester) 224 | return tester:multitest_cli(tests) 225 | end 226 | end 227 | -------------------------------------------------------------------------------- /tests/07_log_common.lua: -------------------------------------------------------------------------------- 1 | if ngx then 2 | return function(tester) 3 | local L = require('estrela.log.common'):new(); 4 | 5 | -- проверка наличия методов 6 | local methods = {'debug', 'info', 'notice', 'warn', 'err', 'crit', 'alert', 'emerg',} 7 | 8 | for _, method in ipairs(methods) do 9 | assert(L[method], method) 10 | 11 | local error_triggered = false 12 | xpcall(L[method], function() error_triggered = true end) 13 | assert(error_triggered, method .. ' error() call') 14 | end 15 | 16 | -- проверка наличия алиасов 17 | local methods = {warning='warn', error='err', critical='crit', emergency='emerg',} 18 | for alias, method in pairs(methods) do 19 | assert(L[alias] == L[method], alias) 20 | end 21 | end 22 | end 23 | -------------------------------------------------------------------------------- /tests/08_log_file.lua: -------------------------------------------------------------------------------- 1 | if ngx then 2 | return function(tester) 3 | local levels = require('estrela.log.common'):new().level_names 4 | 5 | math.randomseed(os.time()) 6 | 7 | for level_code, level_name in pairs(levels) do 8 | local tmpname = os.tmpname() 9 | 10 | local check = tostring(os.time()) .. tostring(math.random()) 11 | 12 | local F = require('estrela.log.file'):new(tmpname, 'TESTER %Y/%m/%d %H:%M:%S {LVL} [{MSG}] {BT}') 13 | F._write(level_code, check, '') 14 | F.close() 15 | 16 | local fd = io.open(tmpname, 'rb') 17 | assert(fd, 'logfile exists') 18 | 19 | local cont = fd:read('*a') 20 | fd:close() 21 | 22 | os.remove(tmpname) 23 | 24 | -- TESTER 2014/07/03 18:17:08 EMERG [/tmp/lua_FU3WuM] \n 25 | local re = string.format( 26 | [=[^TESTER \d{4}/\d{2}/\d{2} \d{2}:\d{2}:\d{2} %s\s+\[%s\]\s{2}$]=], 27 | level_name, 28 | check 29 | ) 30 | 31 | assert( 32 | ngx.re.match(cont, re), 33 | 'format ' .. level_name 34 | ) 35 | end 36 | 37 | local tmpname = os.tmpname() 38 | 39 | local F = require('estrela.log.file'):new(tmpname); 40 | F.level = F.EMERG 41 | F.debug('hello') 42 | F.close() 43 | 44 | local fd = io.open(tmpname, 'rb') 45 | assert(fd, 'logfile exists#2') 46 | 47 | local cont = fd:read('*a') 48 | fd:close() 49 | 50 | os.remove(tmpname) 51 | 52 | assert( 53 | #cont == 0, 54 | 'format error levels' 55 | ) 56 | end 57 | end 58 | -------------------------------------------------------------------------------- /tests/09_log_ngx.lua: -------------------------------------------------------------------------------- 1 | if ngx then 2 | return function(tester) 3 | local _ngx_log = ngx.log 4 | 5 | local called = false 6 | ngx.log = function(...) 7 | called = true 8 | end 9 | 10 | local N = require('estrela.log.ngx'):new() 11 | N.emerg('hello world#1') 12 | 13 | local called2 = called 14 | 15 | N.level = N.DISABLE 16 | called = false 17 | N.emerg('hello world#2') 18 | 19 | ngx.log = _ngx_log 20 | 21 | assert( 22 | called2 and (not called), 23 | 'was called' 24 | ) 25 | end 26 | end 27 | -------------------------------------------------------------------------------- /tests/10_storage_engine_common.lua: -------------------------------------------------------------------------------- 1 | if ngx then 2 | return function(tester) 3 | local S = require('estrela.storage.engine.common'):new() 4 | 5 | local error_str 6 | 7 | xpcall(S.get, function(err) error_str = err end) 8 | 9 | assert(error_str == 'Not implemented') 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /tests/11_storage_engine_shmem.lua: -------------------------------------------------------------------------------- 1 | if ngx then 2 | local function get_mock() 3 | local mock = { 4 | _data = {}, 5 | 6 | _mem_enough = true, 7 | } 8 | 9 | function mock:get(key) 10 | local data = self._data[key] 11 | if not data or ((data.expire > 0) and (data.expire < ngx.now())) then 12 | return nil 13 | end 14 | 15 | return data.data 16 | end 17 | 18 | function mock:get_stale(key) 19 | local data = self._data[key] 20 | return data and data.data or nil 21 | end 22 | 23 | function mock:set(key, value, ttl) 24 | if not self._mem_enough then 25 | return false, 'no memory' 26 | end 27 | 28 | ttl = ttl or 0 29 | 30 | self._data[key] = { 31 | data = value, 32 | expire = (ttl > 0) and (ngx.now() + ttl) or 0, 33 | } 34 | 35 | return true 36 | end 37 | 38 | function mock:add(key, value, ttl) 39 | if self:get(key) then 40 | return false, 'exists' 41 | end 42 | 43 | return self:set(key, value, ttl) 44 | end 45 | 46 | function mock:replace(key, value, ttl) 47 | if not self:get(key) then 48 | return false, 'not found' 49 | end 50 | 51 | return self:set(key, value, ttl) 52 | end 53 | 54 | function mock:incr(key, value) 55 | local val = self:get(key) 56 | if not val then 57 | return false, 'not found' 58 | end 59 | 60 | val = tonumber(val) 61 | if not val then 62 | return nil, 'not a number' 63 | end 64 | 65 | val = val + value 66 | 67 | self._data[key].data = val 68 | -- expire не обновляется 69 | 70 | return val 71 | end 72 | 73 | function mock:delete(key) 74 | self._data[key] = nil 75 | end 76 | 77 | return mock 78 | end 79 | 80 | return function(tester) 81 | local shmem_name = tostring(os.time()) .. tostring(math.random()) 82 | 83 | ngx.shared[shmem_name] = get_mock() 84 | 85 | local S = require('estrela.storage.engine.shmem'):new(shmem_name) 86 | 87 | local key = 'foo' 88 | local val = 42 89 | 90 | -- basic 91 | 92 | assert( 93 | S:get(key) == nil, 94 | 'get nonexistent key' 95 | ) 96 | 97 | assert( 98 | S:set(key, val) == true, 99 | 'set w/o ttl' 100 | ) 101 | 102 | assert( 103 | S:incr(key, 7) == 49, 104 | 'incr +' 105 | ) 106 | 107 | assert( 108 | S:incr(key, -7) == val, 109 | 'incr -' 110 | ) 111 | 112 | assert( 113 | S:incr(ngx.now(), 7) == false, 114 | 'incr nonexistent key' 115 | ) 116 | 117 | assert( 118 | S:get(key) == val, 119 | 'get existent key' 120 | ) 121 | 122 | assert( 123 | S:add(key, val .. val) == false, 124 | 'add w/ existent key' 125 | ) 126 | 127 | assert( 128 | S:replace(key, val .. val) == true, 129 | 'replace w/ existent key' 130 | ) 131 | 132 | assert( 133 | S:get(key) == val .. val, 134 | 'get replaced key' 135 | ) 136 | 137 | assert( 138 | S:set(key, val) == true, 139 | 'set existent key' 140 | ) 141 | 142 | assert( 143 | S:exists(key) == true, 144 | 'exists existent key' 145 | ) 146 | 147 | S:delete(key) 148 | 149 | assert( 150 | S:get(key) == nil, 151 | 'get deleted key' 152 | ) 153 | 154 | assert( 155 | S:exists(key) == false, 156 | 'exists nonexistent key' 157 | ) 158 | 159 | -- expires 160 | 161 | assert( 162 | S:set(key, val, 0.5) == true, 163 | 'set w/ ttl' 164 | ) 165 | 166 | assert( 167 | S:exists(key) == true, 168 | 'exists existent key w/ ttl' 169 | ) 170 | 171 | ngx.sleep(0.5) 172 | 173 | assert( 174 | S:get(key) == nil, 175 | 'get existent key w/ ttl after ttl expires' 176 | ) 177 | 178 | assert( 179 | S:get(key, true) == val, 180 | 'get stale existent key w/ ttl after ttl expires' 181 | ) 182 | 183 | -- no enough of memory 184 | ngx.shared[shmem_name]._mem_enough = false 185 | 186 | assert( 187 | S:set(key, val) == false, 188 | 'set w/ no enough of memory' 189 | ) 190 | 191 | ngx.shared[shmem_name] = nil 192 | end 193 | end 194 | -------------------------------------------------------------------------------- /tests/12_ngx_response_cookie.lua: -------------------------------------------------------------------------------- 1 | local demo_cookie = { 2 | name = 'noname', 3 | value = 'fake_value', 4 | path = '/', 5 | httponly = true, 6 | secure = true, 7 | } 8 | 9 | local function checker_demo_cookie(tester, test, body, status, headers) 10 | local parse_header_value = require('estrela.util.string').parse_header_value 11 | 12 | local cookie = headers['set_cookie'] 13 | cookie = cookie and parse_header_value(cookie) or nil 14 | assert(cookie, test.name) 15 | 16 | table.sort(cookie['']) -- чтобы тест tester.tbl_cmpi ниже не зависел от порядка перечисления флагов 17 | 18 | assert( 19 | cookie[demo_cookie.name] == demo_cookie.value 20 | and cookie.path == demo_cookie.path 21 | and tester.tbl_cmpi(cookie[''], {'httponly', 'secure'}), 22 | test.name 23 | ) 24 | end 25 | 26 | local tests = { 27 | -- empty 28 | { 29 | name = 'empty_wo_map', 30 | code = function(tester, test) 31 | local C = require('estrela.ngx.response.cookie'):new() 32 | 33 | local cookie = C:empty() 34 | 35 | assert( 36 | cookie.name and cookie.value and (type(cookie.set) == 'function'), 37 | test.name 38 | ) 39 | end, 40 | checker = function(tester, test, body, status, headers) 41 | end, 42 | }, 43 | 44 | { 45 | name = 'empty_with_map', 46 | code = function(tester, test) 47 | local C = require('estrela.ngx.response.cookie'):new() 48 | 49 | local cookie = C:empty(demo_cookie) 50 | cookie.set = nil -- удаляю метод, чтобы tbl_cmp не ругалась 51 | 52 | assert( 53 | tester.tbl_cmp( 54 | cookie, 55 | demo_cookie 56 | ), 57 | test.name 58 | ) 59 | end, 60 | checker = function(tester, test, body, status, headers) 61 | end, 62 | }, 63 | 64 | -- set 65 | { 66 | name = 'set_via_multi_params', 67 | code = function(tester, test) 68 | local C = require('estrela.ngx.response.cookie'):new() 69 | C:set(demo_cookie.name, demo_cookie.value, demo_cookie.expires, demo_cookie.path, demo_cookie.domain, 70 | demo_cookie.secure, demo_cookie.httponly 71 | ) 72 | end, 73 | checker = checker_demo_cookie, 74 | }, 75 | 76 | { 77 | name = 'set_wo_name', 78 | code = function(tester, test) 79 | local C = require('estrela.ngx.response.cookie'):new() 80 | 81 | local await_err = 'missing name or value' 82 | 83 | local ok, err = C:set() 84 | assert( 85 | (not ok) and (err == await_err), 86 | test.name .. 'w/o name' 87 | ) 88 | 89 | local ok, err = C:set('') 90 | assert( 91 | (not ok) and (err == await_err), 92 | test.name .. 'empty name' 93 | ) 94 | 95 | local ok, err = C:set('name') 96 | assert( 97 | (not ok) and (err == await_err), 98 | test.name .. 'name w/o value' 99 | ) 100 | end, 101 | checker = function(tester, test, body, status, headers) 102 | end, 103 | }, 104 | 105 | { 106 | name = 'set_via_map', 107 | code = function(tester, test) 108 | local C = require('estrela.ngx.response.cookie'):new() 109 | C:set(demo_cookie) 110 | end, 111 | checker = checker_demo_cookie, 112 | }, 113 | 114 | { 115 | name = 'set_via_empty_set', 116 | code = function(tester, test) 117 | local C = require('estrela.ngx.response.cookie'):new() 118 | 119 | local cookie = C:empty() 120 | 121 | for k, v in pairs(demo_cookie) do 122 | cookie[k] = v 123 | end 124 | 125 | cookie:set() 126 | end, 127 | checker = checker_demo_cookie, 128 | }, 129 | } 130 | 131 | if ngx then 132 | return function(tester) 133 | return tester:multitest_srv(tests) 134 | end 135 | else 136 | return function(tester) 137 | return tester:multitest_cli(tests) 138 | end 139 | end 140 | -------------------------------------------------------------------------------- /tests/tester/cli.lua: -------------------------------------------------------------------------------- 1 | local M = require('tests.tester.common') 2 | 3 | local cookie_file_name = os.tmpname() 4 | 5 | function M.exec(cmd, ignore_code) 6 | local stdout_name, stderr_name = os.tmpname(), os.tmpname() 7 | local cmd = string.format('%s >%s 2>%s', cmd, stdout_name, stderr_name) 8 | local code = os.execute(cmd) 9 | 10 | local stdout_fd, stderr_fd = io.open(stdout_name, 'rb'), io.open(stderr_name, 'rb') 11 | local stdout_buf, stderr_buf 12 | 13 | if (ignore_code or (code == 0)) and stdout_fd then 14 | stdout_buf = stdout_fd:read('*a') 15 | end 16 | 17 | if stderr_fd then 18 | stderr_buf = stderr_fd:read('*a') 19 | end 20 | 21 | os.remove(stdout_name) 22 | os.remove(stderr_name) 23 | 24 | return stdout_buf, stderr_buf or '' 25 | end 26 | 27 | function M.exec_by_lines(cmd) 28 | local out, err = M.exec(cmd) 29 | if out then 30 | local lines = M.split(M.trim(out), '\n') 31 | out = {} 32 | for _, line in ipairs(lines) do 33 | table.insert(out, M.trim(line)) 34 | end 35 | end 36 | return out, err 37 | end 38 | 39 | function M.ls_tests(path) 40 | local lst, err = M.exec_by_lines('ls -1 ' .. path) 41 | if not lst then 42 | return lst, err 43 | end 44 | 45 | local tests = {} 46 | 47 | for _, file in ipairs(lst) do 48 | local m = string.match(file, [[^(.+).lua$]]) 49 | if m then 50 | table.insert(tests, m) 51 | end 52 | end 53 | 54 | table.sort(tests) 55 | 56 | return tests 57 | end 58 | 59 | function M.http_req(url, args) 60 | local url = url 61 | 62 | if args then 63 | for k, v in pairs(args) do 64 | url = url .. '&' .. tostring(k) .. '=' .. tostring(v) 65 | end 66 | end 67 | 68 | local req = table.concat( 69 | { 70 | 'wget -S -q -O /dev/stdout -T 5 -w 1 --no-cache --keep-session-cookies', 71 | '--load-cookies', cookie_file_name, 72 | '--save-cookies', cookie_file_name, 73 | '"' .. url .. '"' 74 | }, 75 | ' ' 76 | ) 77 | 78 | local body, err = M.exec(req, true) 79 | if not body then 80 | return nil, err 81 | end 82 | 83 | local status, headers_or_err = M.parse_response_header(err) 84 | if not status then 85 | return nil, headers_or_err 86 | end 87 | 88 | return body, status, headers_or_err 89 | end 90 | 91 | function M.req(...) 92 | local body, status, headers = M.http_req(...) 93 | if headers[M.error_header_name] then 94 | return error(headers[M.error_header_name]) 95 | else 96 | return body, status, headers 97 | end 98 | end 99 | 100 | function M.check(orig, got) 101 | if orig == got then 102 | return true 103 | else 104 | return false, string.format([[Expected: %s, got: %s]], M.prepare_string(orig), M.prepare_string(got)) 105 | end 106 | end 107 | 108 | function M.remove_cookies() 109 | os.remove(cookie_file_name) 110 | end 111 | 112 | function M.multitest_cli(self, tests) 113 | for _, test in ipairs(tests) do 114 | local body, status, headers = self.req(self.url, {name = test.name}) 115 | local ok = test.checker(self, test, body, status, headers) 116 | if ok == nil then 117 | assert(status and (status.code == 200), test.name) 118 | end 119 | end 120 | return true 121 | end 122 | 123 | return M 124 | -------------------------------------------------------------------------------- /tests/tester/common.lua: -------------------------------------------------------------------------------- 1 | local M = {} 2 | 3 | M.error_header_name = 'x_tester_error' 4 | 5 | local json 6 | 7 | function M.rtrim(str, chars) 8 | chars = chars or '%s' 9 | local res = str:gsub('['..chars..']+$', '') 10 | return res 11 | end 12 | 13 | function M.ltrim(str, chars) 14 | chars = chars or '%s' 15 | local res = str:gsub('^['..chars..']+', '') 16 | return res 17 | end 18 | 19 | function M.trim(str, chars) 20 | chars = chars or '%s' 21 | return M.ltrim(M.rtrim(str, chars), chars) 22 | end 23 | 24 | function M.split(str, sep) 25 | if not string.find(str, sep, 1, true) then 26 | return {str} 27 | end 28 | 29 | local res = {} 30 | 31 | local prev_pos = 1 32 | while #res < #str - 1 do 33 | local pos, pos_t = string.find(str, sep, prev_pos, true) 34 | if not pos then 35 | res[#res + 1] = str:sub(prev_pos) 36 | prev_pos = #str + 1 37 | break 38 | end 39 | 40 | res[#res + 1] = str:sub(prev_pos, pos - 1) 41 | prev_pos = pos_t + 1 42 | end 43 | 44 | if prev_pos <= #str then 45 | res[#res + 1] = str:sub(prev_pos) 46 | end 47 | 48 | return res 49 | end 50 | 51 | function M.parse_response_header(header) 52 | local lines = M.split(M.trim(header), '\n') 53 | 54 | local ver, code, descr = string.match(lines[1], [[^HTTP/(%d.%d)%s(%d+)%s(.+)$]]) 55 | if not ver or not code or not descr then 56 | return nil, 'Wrong response status line format' 57 | end 58 | 59 | local status = { 60 | version = ver, 61 | code = tonumber(code), 62 | descr = descr, 63 | } 64 | 65 | local headers = {} 66 | 67 | for i = 2, #lines do 68 | local line = M.trim(lines[i]) 69 | local k, v = string.match(line, [[^([^:]+):%s*(.+)$]]) 70 | if k and v then 71 | k = k:lower():gsub('-', '_') 72 | headers[k] = v 73 | else 74 | return nil, 'Wrong response header line format: ' .. line 75 | end 76 | end 77 | 78 | return status, headers 79 | end 80 | 81 | function M.prepare_string(s) 82 | local s_type = type(s) 83 | if s_type == 'nil' then 84 | return 'nil' 85 | elseif s_type == 'number' then 86 | return tostring(s) 87 | elseif s_type == 'string' then 88 | -- спец обработка \t и \n для сохранения ее в строке в текстовом формате 89 | s = string.gsub(s, '\t', [[\t]]) 90 | s = string.gsub(s, '\n', [[\n]]) 91 | s = string.format('%q', s) 92 | s = string.gsub(s, [[\\t]], [[\t]]) 93 | s = string.gsub(s, [[\\n]], [[\n]]) 94 | return s 95 | else 96 | return error('Unsupported type ' .. s_type) 97 | end 98 | end 99 | 100 | function M.tbl_cmpx(t1, t2, iter, cmp, ignore_mt) 101 | if type(t1) ~= 'table' or type(t2) ~= 'table' then 102 | return false 103 | end 104 | 105 | if not ignore_mt then 106 | local mt1 = getmetatable(t1) 107 | if type(mt1) == 'table' then 108 | local mt2 = getmetatable(t2) 109 | if type(mt2) ~= 'table' then 110 | return false 111 | end 112 | 113 | if not M.tbl_cmp(mt1, mt2) then 114 | return false 115 | end 116 | end 117 | end 118 | 119 | for k, v in iter(t1) do 120 | if type(v) == 'table' then 121 | if not cmp(t2[k], v, ignore_mt) then 122 | return false 123 | end 124 | elseif t2[k] ~= v then 125 | return false 126 | end 127 | end 128 | return true 129 | end 130 | 131 | function M.tbl_cmp(t1, t2, ignore_mt) 132 | return M.tbl_cmpx(t1, t2, pairs, M.tbl_cmp, ignore_mt) 133 | and M.tbl_cmpx(t2, t1, pairs, M.tbl_cmp, ignore_mt) 134 | end 135 | 136 | function M.tbl_cmpi(t1, t2, ignore_mt) 137 | return M.tbl_cmpx(t1, t2, ipairs, M.tbl_cmpi, ignore_mt) 138 | and M.tbl_cmpx(t2, t1, ipairs, M.tbl_cmpi, ignore_mt) 139 | end 140 | 141 | function M.je(...) 142 | if not json then 143 | json = require('cjson') 144 | json.encode_sparse_array(true) 145 | end 146 | return json.encode{...} 147 | end 148 | 149 | return M 150 | -------------------------------------------------------------------------------- /tests/tester/ngx.lua: -------------------------------------------------------------------------------- 1 | local M = require('tests.tester.common') 2 | 3 | function M.multitest_srv(self, tests) 4 | local name = ngx.var.arg_name 5 | assert(name, 'Test name is not specified') 6 | 7 | local test 8 | for _, t in ipairs(tests) do 9 | if t.name == name then 10 | test = t 11 | break 12 | end 13 | end 14 | assert(test, 'Test with name [' .. tostring(name) .. '] is not found') 15 | 16 | return test.code(self, test) 17 | end 18 | 19 | return M 20 | --------------------------------------------------------------------------------