3 local lib = require('http.lib')
6 local require = require
7 local package = package
8 local mime_types = require('http.mime_types')
9 local codes = require('http.codes')
11 local log = require('log')
12 local socket = require('socket')
13 local fiber = require('fiber')
14 local json = require('json')
15 local errno = require 'errno'
17 local function errorf(fmt, ...)
18 error(string.format(fmt, ...))
21 local function sprintf(fmt, ...)
22 return string.format(fmt, ...)
25 local function uri_escape(str)
27 if type(str) == 'table' then
28 for _, v in pairs(str) do
29 table.insert(res, uri_escape(v))
32 res = string.gsub(str, '[^a-zA-Z0-9_]',
34 return string.format('%%%02X', string.byte(c))
41 local function uri_unescape(str, unescape_plus_sign)
43 if type(str) == 'table' then
44 for _, v in pairs(str) do
45 table.insert(res, uri_unescape(v))
48 if unescape_plus_sign ~= nil then
49 str = string.gsub(str, '+', ' ')
52 res = string.gsub(str, '%%([0-9a-fA-F][0-9a-fA-F])',
54 return string.char(tonumber(c, 16))
61 local function extend(tbl, tblu, raise)
63 for k, v in pairs(tbl) do
66 for k, v in pairs(tblu) do
68 if res[ k ] == nil then
69 errorf("Unknown option '%s'", k)
77 local function type_by_format(fmt)
79 return 'application/octet-stream'
82 local t = mime_types[ fmt ]
88 return 'application/octet-stream'
91 local function reason_by_code(code)
93 if codes[code] ~= nil then
96 return sprintf('Unknown code %d', code)
99 local function ucfirst(str)
100 return str:gsub("^%l", string.upper, 1)
103 local function cached_query_param(self, name)
105 return self.query_params
107 return self.query_params[ name ]
110 local function cached_post_param(self, name)
112 return self.post_params
114 return self.post_params[ name ]
117 local function request_tostring(self)
118 local res = self:request_line() .. "\r\n"
120 for hn, hv in pairs(self.headers) do
121 res = sprintf("%s%s: %s\r\n", res, ucfirst(hn), hv)
124 return sprintf("%s\r\n%s", res, self.body)
127 local function request_line(self)
128 local rstr = self.path
129 if string.len(self.query) then
130 rstr = rstr .. '?' .. self.query
132 return sprintf("%s %s HTTP/%d.%d",
133 self.method, rstr, self.proto[1], self.proto[2])
136 local function query_param(self, name)
137 if self.query == nil and string.len(self.query) == 0 then
138 rawset(self, 'query_params', {})
140 local params = lib.params(self.query)
142 for k, v in pairs(params) do
143 pres[ uri_unescape(k) ] = uri_unescape(v)
145 rawset(self, 'query_params', pres)
148 rawset(self, 'query_param', cached_query_param)
149 return self:query_param(name)
152 local function request_content_type(self)
153 -- returns content type without encoding string
154 if self.headers['content-type'] == nil then
158 return string.match(self.headers['content-type'],
160 string.match(self.headers['content-type'],
164 local function post_param(self, name)
165 local content_type = self:content_type()
166 if self:content_type() == 'multipart/form-data' then
168 rawset(self, 'post_params', {})
169 elseif self:content_type() == 'application/json' then
170 local params = self:json()
171 rawset(self, 'post_params', params)
172 elseif self:content_type() == 'application/x-www-form-urlencoded' then
173 local params = lib.params(self:read_cached())
175 for k, v in pairs(params) do
176 pres[ uri_unescape(k) ] = uri_unescape(v, true)
178 rawset(self, 'post_params', pres)
180 local params = lib.params(self:read_cached())
182 for k, v in pairs(params) do
183 pres[ uri_unescape(k) ] = uri_unescape(v)
185 rawset(self, 'post_params', pres)
188 rawset(self, 'post_param', cached_post_param)
189 return self:post_param(name)
192 local function param(self, name)
194 local v = self:post_param(name)
198 return self:query_param(name)
201 local post = self:post_param()
202 local query = self:query_param()
203 return extend(post, query, false)
206 local function catfile(...)
215 for i, pe in pairs(sp) do
218 elseif string.match(path, '.$') ~= '/' then
219 if string.match(pe, '^.') ~= '/' then
220 path = path .. '/' .. pe
225 if string.match(pe, '^.') == '/' then
226 path = path .. string.gsub(pe, '^/', '', 1)
239 local function expires_str(str)
241 local now = os.time()
242 local gmtnow = now - os.difftime(now, os.time(os.date("!*t", now)))
243 local fmt = '%a, %d-%b-%Y %H:%M:%S GMT'
245 if str == 'now' or str == 0 or str == '0' then
246 return os.date(fmt, gmtnow)
249 local diff, period = string.match(str, '^[+]?(%d+)([hdmy])$')
250 if period == nil then
254 diff = tonumber(diff)
255 if period == 'h' then
257 elseif period == 'd' then
259 elseif period == 'm' then
260 diff = diff * 86400 * 30
262 diff = diff * 86400 * 365
265 return os.date(fmt, gmtnow + diff)
268 local function setcookie(resp, cookie)
269 local name = cookie.name
270 local value = cookie.value
273 error('cookie.name is undefined')
276 error('cookie.value is undefined')
279 local str = sprintf('%s=%s', name, uri_escape(value))
280 if cookie.path ~= nil then
281 str = sprintf('%s;path=%s', str, uri_escape(cookie.path))
283 if cookie.domain ~= nil then
284 str = sprintf('%s;domain=%s', str, cookie.domain)
287 if cookie.expires ~= nil then
288 str = sprintf('%s;expires="%s"', str, expires_str(cookie.expires))
291 if not resp.headers then
294 if resp.headers['set-cookie'] == nil then
295 resp.headers['set-cookie'] = { str }
296 elseif type(resp.headers['set-cookie']) == 'string' then
297 resp.headers['set-cookie'] = {
298 resp.headers['set-cookie'],
302 table.insert(resp.headers['set-cookie'], str)
307 local function cookie(tx, cookie)
308 if tx.headers.cookie == nil then
311 for k, v in string.gmatch(
312 tx.headers.cookie, "([^=,; \t]+)=([^,; \t]+)") do
314 return uri_unescape(v)
320 local function url_for_helper(tx, name, args, query)
321 return tx:url_for(name, args, query)
324 local function load_template(self, r, format)
325 if r.template ~= nil then
329 if format == nil then
334 if r.file ~= nil then
336 elseif r.controller ~= nil and r.action ~= nil then
338 string.gsub(r.controller, '[.]', '/'),
339 r.action .. '.' .. format .. '.el')
341 errorf("Can not find template for '%s'", r.path)
344 if self.options.cache_templates then
345 if self.cache.tpl[ file ] ~= nil then
346 return self.cache.tpl[ file ]
351 local tpl = catfile(self.options.app_dir, 'templates', file)
352 local fh = io.input(tpl)
353 local template = fh:read('*a')
356 if self.options.cache_templates then
357 self.cache.tpl[ file ] = template
362 local function render(tx, opts)
364 error("Usage: self:render({ ... })")
367 local resp = setmetatable({ headers = {} }, response_mt)
370 if opts.text ~= nil then
371 if tx.httpd.options.charset ~= nil then
372 resp.headers['content-type'] =
373 sprintf("text/plain; charset=%s",
374 tx.httpd.options.charset
377 resp.headers['content-type'] = 'text/plain'
379 resp.body = tostring(opts.text)
383 if opts.json ~= nil then
384 if tx.httpd.options.charset ~= nil then
385 resp.headers['content-type'] =
386 sprintf('application/json; charset=%s',
387 tx.httpd.options.charset
390 resp.headers['content-type'] = 'application/json'
392 resp.body = json.encode(opts.json)
396 if opts.data ~= nil then
397 resp.body = tostring(opts.data)
401 vars = extend(tx.tstash, opts, false)
406 local format = tx.tstash.format
407 if format == nil then
411 if tx.endpoint.template ~= nil then
412 tpl = tx.endpoint.template
414 tpl = load_template(tx.httpd, tx.endpoint, format)
416 errorf('template is not defined for the route')
420 if type(tpl) == 'function' then
424 for hname, sub in pairs(tx.httpd.helpers) do
425 vars[hname] = function(...) return sub(tx, ...) end
427 vars.action = tx.endpoint.action
428 vars.controller = tx.endpoint.controller
431 resp.body = lib.template(tpl, vars)
432 resp.headers['content-type'] = type_by_format(format)
434 if tx.httpd.options.charset ~= nil then
435 if format == 'html' or format == 'js' or format == 'json' then
436 resp.headers['content-type'] = resp.headers['content-type']
437 .. '; charset=' .. tx.httpd.options.charset
443 local function iterate(tx, gen, param, state)
444 return setmetatable({ body = { gen = gen, param = param, state = state } },
448 local function redirect_to(tx, name, args, query)
449 local location = tx:url_for(name, args, query)
450 return setmetatable({ status = 302, headers = { location = location } },
454 local function access_stash(tx, name, ...)
455 if type(tx) ~= 'table' then
456 error("usage: ctx:stash('name'[, 'value'])")
458 if select('#', ...) > 0 then
459 tx.tstash[ name ] = select(1, ...)
462 return tx.tstash[ name ]
465 local function url_for_tx(tx, name, args, query)
466 if name == 'current' then
467 return tx.endpoint:url_for(args, query)
469 return tx.httpd:url_for(name, args, query)
472 local function request_json(req)
473 local data = req:read_cached()
474 local s, json = pcall(json.decode, data)
478 error(sprintf("Can't decode json in request '%s': %s",
479 data, tostring(json)))
484 local function request_read(req, opts, timeout)
485 local remaining = req._remaining
486 if not remaining then
487 remaining = tonumber(req.headers['content-length'])
488 if not remaining then
495 elseif type(opts) == 'number' then
496 if opts > remaining then
499 elseif type(opts) == 'string' then
500 opts = { size = remaining, delimiter = opts }
501 elseif type(opts) == 'table' then
502 local size = opts.size or opts.chunk
503 if size and size > remaining then
504 opts.size = remaining
509 local buf = req.s:read(opts, timeout)
510 remaining = remaining - #buf
511 assert(remaining >= 0)
512 req._remaining = remaining
516 local function request_read_cached(self)
517 if self.cached_data == nil then
518 local data = self:read()
519 rawset(self, 'cached_data', data)
522 return self.cached_data
526 local function static_file(self, request, format)
527 local file = catfile(self.options.app_dir, 'public', request.path)
529 if self.options.cache_static and self.cache.static[ file ] ~= nil then
533 [ 'content-type'] = type_by_format(format),
535 body = self.cache.static[ file ]
539 local s, fh = pcall(io.input, file)
542 return { status = 404 }
545 local body = fh:read('*a')
548 if self.options.cache_static then
549 self.cache.static[ file ] = body
555 [ 'content-type'] = type_by_format(format),
565 redirect_to = redirect_to,
567 stash = access_stash,
568 url_for = url_for_tx,
569 content_type= request_content_type,
570 request_line= request_line,
571 read_cached = request_read_cached,
572 query_param = query_param,
573 post_param = post_param,
578 __tostring = request_tostring;
583 setcookie = setcookie;
587 local function handler(self, request)
588 if self.hooks.before_dispatch ~= nil then
589 self.hooks.before_dispatch(self, request)
592 local format = 'html'
594 local pformat = string.match(request.path, '[.]([^.]+)$')
595 if pformat ~= nil then
600 local r = self:match(request.method, request.path)
602 return static_file(self, request, format)
605 local stash = extend(r.stash, { format = format })
607 request.endpoint = r.endpoint
608 request.tstash = stash
610 local resp = r.endpoint.sub(request)
611 if self.hooks.after_dispatch ~= nil then
612 self.hooks.after_dispatch(request, resp)
617 local function normalize_headers(hdrs)
619 for h, v in pairs(hdrs) do
620 res[ string.lower(h) ] = v
625 local function parse_request(req)
626 local p = lib._parse_request(req)
630 p.path = uri_unescape(p.path)
631 if p.path:sub(1, 1) ~= "/" or p.path:find("./", nil, true) ~= nil then
632 p.error = "invalid uri"
638 local function process_client(self, s, peer)
644 local chunk = s:read{
645 delimiter = { "\n\n", "\r\n\r\n" }
651 elseif chunk == nil then
652 log.error('failed to read request: %s', errno.strerror())
658 if string.endswith(hdrs, "\n\n") or string.endswith(hdrs, "\r\n\r\n") then
667 log.debug("request:\n%s", hdrs)
668 local p = parse_request(hdrs)
669 if p.error ~= nil then
670 log.error('failed to parse request: %s', p.error)
671 s:write(sprintf("HTTP/1.0 400 Bad request\r\n\r\n%s", p.error))
677 setmetatable(p, request_mt)
679 if p.headers['expect'] == '100-continue' then
680 s:write('HTTP/1.0 100 Continue\r\n\r\n')
683 local logreq = self.options.log_requests and log.info or log.debug
684 logreq("%s %s%s", p.method, p.path,
685 p.query ~= "" and "?"..p.query or "")
687 local res, reason = pcall(self.options.handler, self, p)
688 p:read() -- skip remaining bytes of request body
689 local status, hdrs, body
694 local trace = debug.traceback()
695 local logerror = self.options.log_errors and log.error or log.debug
696 logerror('unhandled error: %s\n%s\nrequest:\n%s',
697 tostring(reason), trace, tostring(p))
698 if self.options.display_errors then
700 "Unhandled error: " .. tostring(reason) .. "\n"
705 body = "Internal Error"
707 elseif type(reason) == 'table' then
708 if reason.status == nil then
710 elseif type(reason.status) == 'number' then
711 status = reason.status
713 error('response.status must be a number')
715 if reason.headers == nil then
717 elseif type(reason.headers) == 'table' then
718 hdrs = normalize_headers(reason.headers)
720 error('response.headers must be a table')
723 elseif reason == nil then
727 error('invalid response')
730 local gen, param, state
731 if type(body) == 'string' then
733 hdrs['content-length'] = #body
734 elseif type(body) == 'function' then
735 -- Generating function
737 hdrs['transfer-encoding'] = 'chunked'
738 elseif type(body) == 'table' and body.gen then
740 gen, param, state = body.gen, body.param, body.state
741 hdrs['transfer-encoding'] = 'chunked'
742 elseif body == nil then
744 hdrs['content-length'] = 0
746 body = tostring(body)
747 hdrs['content-length'] = #body
750 if hdrs.server == nil then
751 hdrs.server = sprintf('Tarantool http (tarantool v%s)', _TARANTOOL)
754 if p.proto[1] ~= 1 then
755 hdrs.connection = 'close'
757 hdrs.connection = 'close'
758 elseif rawget(p, 'body') == nil then
759 hdrs.connection = 'close'
760 elseif p.proto[2] == 1 then
761 if p.headers.connection == nil then
762 hdrs.connection = 'keep-alive'
763 elseif string.lower(p.headers.connection) ~= 'keep-alive' then
764 hdrs.connection = 'close'
766 hdrs.connection = 'keep-alive'
768 elseif p.proto[2] == 0 then
769 if p.headers.connection == nil then
770 hdrs.connection = 'close'
771 elseif string.lower(p.headers.connection) == 'keep-alive' then
772 hdrs.connection = 'keep-alive'
774 hdrs.connection = 'close'
782 reason_by_code(status);
785 for k, v in pairs(hdrs) do
786 if type(v) == 'table' then
787 for i, sv in pairs(v) do
788 table.insert(response, sprintf("%s: %s\r\n", ucfirst(k), sv))
791 table.insert(response, sprintf("%s: %s\r\n", ucfirst(k), v))
794 table.insert(response, "\r\n")
796 if type(body) == 'string' then
797 table.insert(response, body)
798 response = table.concat(response)
799 if not s:write(response) then
803 response = table.concat(response)
804 if not s:write(response) then
808 -- Transfer-Encoding: chunked
809 for _, part in gen, param, state do
810 part = tostring(part)
811 if not s:write(sprintf("%x\r\n%s\r\n", #part, part)) then
815 if not s:write("0\r\n\r\n") then
819 response = table.concat(response)
820 if not s:write(response) then
825 if p.proto[1] ~= 1 then
829 if hdrs.connection ~= 'keep-alive' then
835 local function httpd_stop(self)
836 if type(self) ~= 'table' then
837 error("httpd: usage: httpd:stop()")
842 error("server is already stopped")
845 if self.tcp_server ~= nil then
846 self.tcp_server:close()
847 self.tcp_server = nil
852 local function match_route(self, method, route)
853 -- route must have '/' at the begin and end
854 if string.match(route, '.$') ~= '/' then
857 if string.match(route, '^.') ~= '/' then
861 method = string.upper(method)
866 for k, r in pairs(self.routes) do
867 if r.method == method or r.method == 'ANY' then
868 local m = { string.match(route, r.match) }
872 if #r.stash == #m then
884 if #fit.stash > #nfit.stash then
887 elseif r.method ~= fit.method then
888 if fit.method == 'ANY' then
903 for i = 1, #fit.stash do
904 resstash[ fit.stash[ i ] ] = stash[ i ]
906 return { endpoint = fit, stash = resstash }
909 local function set_helper(self, name, sub)
910 if sub == nil or type(sub) == 'function' then
911 self.helpers[ name ] = sub
914 errorf("Wrong type for helper function: %s", type(sub))
917 local function set_hook(self, name, sub)
918 if sub == nil or type(sub) == 'function' then
919 self.hooks[ name ] = sub
922 errorf("Wrong type for hook function: %s", type(sub))
925 local function url_for_route(r, args, query)
930 for i, sn in pairs(r.stash) do
935 name = string.gsub(name, '[*:]' .. sn, sv, 1)
939 if type(query) == 'table' then
941 for k, v in pairs(query) do
942 name = name .. sep .. uri_escape(k) .. '=' .. uri_escape(v)
946 name = name .. '?' .. query
950 if string.match(name, '^/') == nil then
957 local function ctx_action(tx)
958 local ctx = tx.endpoint.controller
959 local action = tx.endpoint.action
960 if tx.httpd.options.cache_controllers then
961 if tx.httpd.cache[ ctx ] ~= nil then
962 if type(tx.httpd.cache[ ctx ][ action ]) ~= 'function' then
963 errorf("Controller '%s' doesn't contain function '%s'",
966 return tx.httpd.cache[ ctx ][ action ](tx)
970 local ppath = package.path
971 package.path = catfile(tx.httpd.options.app_dir, 'controllers', '?.lua')
973 .. catfile(tx.httpd.options.app_dir,
974 'controllers', '?/init.lua')
976 package.path = package.path .. ';' .. ppath
979 local st, mod = pcall(require, ctx)
981 package.loaded[ ctx ] = nil
984 errorf("Can't load module '%s': %s'", ctx, tostring(mod))
987 if type(mod) ~= 'table' then
988 errorf("require '%s' didn't return table", ctx)
991 if type(mod[ action ]) ~= 'function' then
992 errorf("Controller '%s' doesn't contain function '%s'", ctx, action)
995 if tx.httpd.options.cache_controllers then
996 tx.httpd.cache[ ctx ] = mod
999 return mod[action](tx)
1002 local possible_methods = {
1011 local function add_route(self, opts, sub)
1012 if type(opts) ~= 'table' or type(self) ~= 'table' then
1013 error("Usage: httpd:route({ ... }, function(cx) ... end)")
1016 opts = extend({method = 'ANY'}, opts, false)
1023 elseif type(sub) == 'string' then
1025 ctx, action = string.match(sub, '(.+)#(.*)')
1027 if ctx == nil or action == nil then
1028 errorf("Wrong controller format '%s', must be 'module#action'", sub)
1033 elseif type(sub) ~= 'function' then
1034 errorf("wrong argument: expected function, but received %s",
1038 opts.method = possible_methods[string.upper(opts.method)] or 'ANY'
1040 if opts.path == nil then
1041 error("path is not defined")
1044 opts.controller = ctx
1045 opts.action = action
1046 opts.match = opts.path
1047 opts.match = string.gsub(opts.match, '[-]', "[-]")
1052 local name = string.match(opts.match, ':([%a_][%w_]*)')
1056 if estash[name] then
1057 errorf("duplicate stash: %s", name)
1060 opts.match = string.gsub(opts.match, ':[%a_][%w_]*', '([^/]-)', 1)
1062 table.insert(stash, name)
1065 local name = string.match(opts.match, '[*]([%a_][%w_]*)')
1069 if estash[name] then
1070 errorf("duplicate stash: %s", name)
1073 opts.match = string.gsub(opts.match, '[*][%a_][%w_]*', '(.-)', 1)
1075 table.insert(stash, name)
1078 if string.match(opts.match, '.$') ~= '/' then
1079 opts.match = opts.match .. '/'
1081 if string.match(opts.match, '^.') ~= '/' then
1082 opts.match = '/' .. opts.match
1085 opts.match = '^' .. opts.match .. '$'
1091 opts.url_for = url_for_route
1093 if opts.name ~= nil then
1094 if opts.name == 'current' then
1095 error("Route can not have name 'current'")
1097 if self.iroutes[ opts.name ] ~= nil then
1098 errorf("Route with name '%s' is already exists", opts.name)
1100 table.insert(self.routes, opts)
1101 self.iroutes[ opts.name ] = #self.routes
1103 table.insert(self.routes, opts)
1108 local function url_for_httpd(httpd, name, args, query)
1110 local idx = httpd.iroutes[ name ]
1112 return httpd.routes[ idx ]:url_for(args, query)
1115 if string.match(name, '^/') == nil then
1116 if string.match(name, '^https?://') ~= nil then
1126 local function httpd_start(self)
1127 if type(self) ~= 'table' then
1128 error("httpd: usage: httpd:start()")
1131 local server = socket.tcp_server(self.host, self.port, { name = 'http',
1132 handler = function(...) process_client(self, ...) end })
1133 if server == nil then
1134 error(sprintf("Can't create tcp_server: %s", errno.strerror()))
1137 rawset(self, 'is_run', true)
1138 rawset(self, 'tcp_server', server)
1139 rawset(self, 'stop', httpd_stop)
1145 new = function(host, port, options)
1146 if options == nil then
1149 if type(options) ~= 'table' then
1150 errorf("options must be table not '%s'", type(options))
1153 max_header_size = 4096,
1154 header_timeout = 100,
1158 cache_templates = true,
1159 cache_controllers = true,
1160 cache_static = true,
1161 log_requests = true,
1163 display_errors = true,
1171 start = httpd_start,
1172 options = extend(default, options, true),
1177 url_for = url_for_helper,
1183 match = match_route,
1184 helper = set_helper,
1186 url_for = url_for_httpd,