Initial Commit
[ancientarts.git] / api / .rocks / share / tarantool / http / server.lua
1 -- http.server
2
3 local lib = require('http.lib')
4
5 local io = io
6 local require = require
7 local package = package
8 local mime_types = require('http.mime_types')
9 local codes = require('http.codes')
10
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'
16
17 local function errorf(fmt, ...)
18 error(string.format(fmt, ...))
19 end
20
21 local function sprintf(fmt, ...)
22 return string.format(fmt, ...)
23 end
24
25 local function uri_escape(str)
26 local res = {}
27 if type(str) == 'table' then
28 for _, v in pairs(str) do
29 table.insert(res, uri_escape(v))
30 end
31 else
32 res = string.gsub(str, '[^a-zA-Z0-9_]',
33 function(c)
34 return string.format('%%%02X', string.byte(c))
35 end
36 )
37 end
38 return res
39 end
40
41 local function uri_unescape(str, unescape_plus_sign)
42 local res = {}
43 if type(str) == 'table' then
44 for _, v in pairs(str) do
45 table.insert(res, uri_unescape(v))
46 end
47 else
48 if unescape_plus_sign ~= nil then
49 str = string.gsub(str, '+', ' ')
50 end
51
52 res = string.gsub(str, '%%([0-9a-fA-F][0-9a-fA-F])',
53 function(c)
54 return string.char(tonumber(c, 16))
55 end
56 )
57 end
58 return res
59 end
60
61 local function extend(tbl, tblu, raise)
62 local res = {}
63 for k, v in pairs(tbl) do
64 res[ k ] = v
65 end
66 for k, v in pairs(tblu) do
67 if raise then
68 if res[ k ] == nil then
69 errorf("Unknown option '%s'", k)
70 end
71 end
72 res[ k ] = v
73 end
74 return res
75 end
76
77 local function type_by_format(fmt)
78 if fmt == nil then
79 return 'application/octet-stream'
80 end
81
82 local t = mime_types[ fmt ]
83
84 if t ~= nil then
85 return t
86 end
87
88 return 'application/octet-stream'
89 end
90
91 local function reason_by_code(code)
92 code = tonumber(code)
93 if codes[code] ~= nil then
94 return codes[code]
95 end
96 return sprintf('Unknown code %d', code)
97 end
98
99 local function ucfirst(str)
100 return str:gsub("^%l", string.upper, 1)
101 end
102
103 local function cached_query_param(self, name)
104 if name == nil then
105 return self.query_params
106 end
107 return self.query_params[ name ]
108 end
109
110 local function cached_post_param(self, name)
111 if name == nil then
112 return self.post_params
113 end
114 return self.post_params[ name ]
115 end
116
117 local function request_tostring(self)
118 local res = self:request_line() .. "\r\n"
119
120 for hn, hv in pairs(self.headers) do
121 res = sprintf("%s%s: %s\r\n", res, ucfirst(hn), hv)
122 end
123
124 return sprintf("%s\r\n%s", res, self.body)
125 end
126
127 local function request_line(self)
128 local rstr = self.path
129 if string.len(self.query) then
130 rstr = rstr .. '?' .. self.query
131 end
132 return sprintf("%s %s HTTP/%d.%d",
133 self.method, rstr, self.proto[1], self.proto[2])
134 end
135
136 local function query_param(self, name)
137 if self.query == nil and string.len(self.query) == 0 then
138 rawset(self, 'query_params', {})
139 else
140 local params = lib.params(self.query)
141 local pres = {}
142 for k, v in pairs(params) do
143 pres[ uri_unescape(k) ] = uri_unescape(v)
144 end
145 rawset(self, 'query_params', pres)
146 end
147
148 rawset(self, 'query_param', cached_query_param)
149 return self:query_param(name)
150 end
151
152 local function request_content_type(self)
153 -- returns content type without encoding string
154 if self.headers['content-type'] == nil then
155 return nil
156 end
157
158 return string.match(self.headers['content-type'],
159 '^([^;]*)$') or
160 string.match(self.headers['content-type'],
161 '^(.*);.*')
162 end
163
164 local function post_param(self, name)
165 local content_type = self:content_type()
166 if self:content_type() == 'multipart/form-data' then
167 -- TODO: do that!
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())
174 local pres = {}
175 for k, v in pairs(params) do
176 pres[ uri_unescape(k) ] = uri_unescape(v, true)
177 end
178 rawset(self, 'post_params', pres)
179 else
180 local params = lib.params(self:read_cached())
181 local pres = {}
182 for k, v in pairs(params) do
183 pres[ uri_unescape(k) ] = uri_unescape(v)
184 end
185 rawset(self, 'post_params', pres)
186 end
187
188 rawset(self, 'post_param', cached_post_param)
189 return self:post_param(name)
190 end
191
192 local function param(self, name)
193 if name ~= nil then
194 local v = self:post_param(name)
195 if v ~= nil then
196 return v
197 end
198 return self:query_param(name)
199 end
200
201 local post = self:post_param()
202 local query = self:query_param()
203 return extend(post, query, false)
204 end
205
206 local function catfile(...)
207 local sp = { ... }
208
209 local path
210
211 if #sp == 0 then
212 return
213 end
214
215 for i, pe in pairs(sp) do
216 if path == nil then
217 path = pe
218 elseif string.match(path, '.$') ~= '/' then
219 if string.match(pe, '^.') ~= '/' then
220 path = path .. '/' .. pe
221 else
222 path = path .. pe
223 end
224 else
225 if string.match(pe, '^.') == '/' then
226 path = path .. string.gsub(pe, '^/', '', 1)
227 else
228 path = path .. pe
229 end
230 end
231 end
232
233 return path
234 end
235
236 local response_mt
237 local request_mt
238
239 local function expires_str(str)
240
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'
244
245 if str == 'now' or str == 0 or str == '0' then
246 return os.date(fmt, gmtnow)
247 end
248
249 local diff, period = string.match(str, '^[+]?(%d+)([hdmy])$')
250 if period == nil then
251 return str
252 end
253
254 diff = tonumber(diff)
255 if period == 'h' then
256 diff = diff * 3600
257 elseif period == 'd' then
258 diff = diff * 86400
259 elseif period == 'm' then
260 diff = diff * 86400 * 30
261 else
262 diff = diff * 86400 * 365
263 end
264
265 return os.date(fmt, gmtnow + diff)
266 end
267
268 local function setcookie(resp, cookie)
269 local name = cookie.name
270 local value = cookie.value
271
272 if name == nil then
273 error('cookie.name is undefined')
274 end
275 if value == nil then
276 error('cookie.value is undefined')
277 end
278
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))
282 end
283 if cookie.domain ~= nil then
284 str = sprintf('%s;domain=%s', str, cookie.domain)
285 end
286
287 if cookie.expires ~= nil then
288 str = sprintf('%s;expires="%s"', str, expires_str(cookie.expires))
289 end
290
291 if not resp.headers then
292 resp.headers = {}
293 end
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'],
299 str
300 }
301 else
302 table.insert(resp.headers['set-cookie'], str)
303 end
304 return resp
305 end
306
307 local function cookie(tx, cookie)
308 if tx.headers.cookie == nil then
309 return nil
310 end
311 for k, v in string.gmatch(
312 tx.headers.cookie, "([^=,; \t]+)=([^,; \t]+)") do
313 if k == cookie then
314 return uri_unescape(v)
315 end
316 end
317 return nil
318 end
319
320 local function url_for_helper(tx, name, args, query)
321 return tx:url_for(name, args, query)
322 end
323
324 local function load_template(self, r, format)
325 if r.template ~= nil then
326 return
327 end
328
329 if format == nil then
330 format = 'html'
331 end
332
333 local file
334 if r.file ~= nil then
335 file = r.file
336 elseif r.controller ~= nil and r.action ~= nil then
337 file = catfile(
338 string.gsub(r.controller, '[.]', '/'),
339 r.action .. '.' .. format .. '.el')
340 else
341 errorf("Can not find template for '%s'", r.path)
342 end
343
344 if self.options.cache_templates then
345 if self.cache.tpl[ file ] ~= nil then
346 return self.cache.tpl[ file ]
347 end
348 end
349
350
351 local tpl = catfile(self.options.app_dir, 'templates', file)
352 local fh = io.input(tpl)
353 local template = fh:read('*a')
354 fh:close()
355
356 if self.options.cache_templates then
357 self.cache.tpl[ file ] = template
358 end
359 return template
360 end
361
362 local function render(tx, opts)
363 if tx == nil then
364 error("Usage: self:render({ ... })")
365 end
366
367 local resp = setmetatable({ headers = {} }, response_mt)
368 local vars = {}
369 if opts ~= nil then
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
375 )
376 else
377 resp.headers['content-type'] = 'text/plain'
378 end
379 resp.body = tostring(opts.text)
380 return resp
381 end
382
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
388 )
389 else
390 resp.headers['content-type'] = 'application/json'
391 end
392 resp.body = json.encode(opts.json)
393 return resp
394 end
395
396 if opts.data ~= nil then
397 resp.body = tostring(opts.data)
398 return resp
399 end
400
401 vars = extend(tx.tstash, opts, false)
402 end
403
404 local tpl
405
406 local format = tx.tstash.format
407 if format == nil then
408 format = 'html'
409 end
410
411 if tx.endpoint.template ~= nil then
412 tpl = tx.endpoint.template
413 else
414 tpl = load_template(tx.httpd, tx.endpoint, format)
415 if tpl == nil then
416 errorf('template is not defined for the route')
417 end
418 end
419
420 if type(tpl) == 'function' then
421 tpl = tpl()
422 end
423
424 for hname, sub in pairs(tx.httpd.helpers) do
425 vars[hname] = function(...) return sub(tx, ...) end
426 end
427 vars.action = tx.endpoint.action
428 vars.controller = tx.endpoint.controller
429 vars.format = format
430
431 resp.body = lib.template(tpl, vars)
432 resp.headers['content-type'] = type_by_format(format)
433
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
438 end
439 end
440 return resp
441 end
442
443 local function iterate(tx, gen, param, state)
444 return setmetatable({ body = { gen = gen, param = param, state = state } },
445 response_mt)
446 end
447
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 } },
451 response_mt)
452 end
453
454 local function access_stash(tx, name, ...)
455 if type(tx) ~= 'table' then
456 error("usage: ctx:stash('name'[, 'value'])")
457 end
458 if select('#', ...) > 0 then
459 tx.tstash[ name ] = select(1, ...)
460 end
461
462 return tx.tstash[ name ]
463 end
464
465 local function url_for_tx(tx, name, args, query)
466 if name == 'current' then
467 return tx.endpoint:url_for(args, query)
468 end
469 return tx.httpd:url_for(name, args, query)
470 end
471
472 local function request_json(req)
473 local data = req:read_cached()
474 local s, json = pcall(json.decode, data)
475 if s then
476 return json
477 else
478 error(sprintf("Can't decode json in request '%s': %s",
479 data, tostring(json)))
480 return nil
481 end
482 end
483
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
489 return ''
490 end
491 end
492
493 if opts == nil then
494 opts = remaining
495 elseif type(opts) == 'number' then
496 if opts > remaining then
497 opts = remaining
498 end
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
505 opts.chunk = nil
506 end
507 end
508
509 local buf = req.s:read(opts, timeout)
510 remaining = remaining - #buf
511 assert(remaining >= 0)
512 req._remaining = remaining
513 return buf
514 end
515
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)
520 return data
521 else
522 return self.cached_data
523 end
524 end
525
526 local function static_file(self, request, format)
527 local file = catfile(self.options.app_dir, 'public', request.path)
528
529 if self.options.cache_static and self.cache.static[ file ] ~= nil then
530 return {
531 code = 200,
532 headers = {
533 [ 'content-type'] = type_by_format(format),
534 },
535 body = self.cache.static[ file ]
536 }
537 end
538
539 local s, fh = pcall(io.input, file)
540
541 if not s then
542 return { status = 404 }
543 end
544
545 local body = fh:read('*a')
546 io.close(fh)
547
548 if self.options.cache_static then
549 self.cache.static[ file ] = body
550 end
551
552 return {
553 status = 200,
554 headers = {
555 [ 'content-type'] = type_by_format(format),
556 },
557 body = body
558 }
559 end
560
561 request_mt = {
562 __index = {
563 render = render,
564 cookie = cookie,
565 redirect_to = redirect_to,
566 iterate = iterate,
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,
574 param = param,
575 read = request_read,
576 json = request_json
577 },
578 __tostring = request_tostring;
579 }
580
581 response_mt = {
582 __index = {
583 setcookie = setcookie;
584 }
585 }
586
587 local function handler(self, request)
588 if self.hooks.before_dispatch ~= nil then
589 self.hooks.before_dispatch(self, request)
590 end
591
592 local format = 'html'
593
594 local pformat = string.match(request.path, '[.]([^.]+)$')
595 if pformat ~= nil then
596 format = pformat
597 end
598
599
600 local r = self:match(request.method, request.path)
601 if r == nil then
602 return static_file(self, request, format)
603 end
604
605 local stash = extend(r.stash, { format = format })
606
607 request.endpoint = r.endpoint
608 request.tstash = stash
609
610 local resp = r.endpoint.sub(request)
611 if self.hooks.after_dispatch ~= nil then
612 self.hooks.after_dispatch(request, resp)
613 end
614 return resp
615 end
616
617 local function normalize_headers(hdrs)
618 local res = {}
619 for h, v in pairs(hdrs) do
620 res[ string.lower(h) ] = v
621 end
622 return res
623 end
624
625 local function parse_request(req)
626 local p = lib._parse_request(req)
627 if p.error then
628 return p
629 end
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"
633 return p
634 end
635 return p
636 end
637
638 local function process_client(self, s, peer)
639 while true do
640 local hdrs = ''
641
642 local is_eof = false
643 while true do
644 local chunk = s:read{
645 delimiter = { "\n\n", "\r\n\r\n" }
646 }
647
648 if chunk == '' then
649 is_eof = true
650 break -- eof
651 elseif chunk == nil then
652 log.error('failed to read request: %s', errno.strerror())
653 return
654 end
655
656 hdrs = hdrs .. chunk
657
658 if string.endswith(hdrs, "\n\n") or string.endswith(hdrs, "\r\n\r\n") then
659 break
660 end
661 end
662
663 if is_eof then
664 break
665 end
666
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))
672 break
673 end
674 p.httpd = self
675 p.s = s
676 p.peer = peer
677 setmetatable(p, request_mt)
678
679 if p.headers['expect'] == '100-continue' then
680 s:write('HTTP/1.0 100 Continue\r\n\r\n')
681 end
682
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 "")
686
687 local res, reason = pcall(self.options.handler, self, p)
688 p:read() -- skip remaining bytes of request body
689 local status, hdrs, body
690
691 if not res then
692 status = 500
693 hdrs = {}
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
699 body =
700 "Unhandled error: " .. tostring(reason) .. "\n"
701 .. trace .. "\n\n"
702 .. "\n\nRequest:\n"
703 .. tostring(p)
704 else
705 body = "Internal Error"
706 end
707 elseif type(reason) == 'table' then
708 if reason.status == nil then
709 status = 200
710 elseif type(reason.status) == 'number' then
711 status = reason.status
712 else
713 error('response.status must be a number')
714 end
715 if reason.headers == nil then
716 hdrs = {}
717 elseif type(reason.headers) == 'table' then
718 hdrs = normalize_headers(reason.headers)
719 else
720 error('response.headers must be a table')
721 end
722 body = reason.body
723 elseif reason == nil then
724 status = 200
725 hdrs = {}
726 else
727 error('invalid response')
728 end
729
730 local gen, param, state
731 if type(body) == 'string' then
732 -- Plain string
733 hdrs['content-length'] = #body
734 elseif type(body) == 'function' then
735 -- Generating function
736 gen = body
737 hdrs['transfer-encoding'] = 'chunked'
738 elseif type(body) == 'table' and body.gen then
739 -- Iterator
740 gen, param, state = body.gen, body.param, body.state
741 hdrs['transfer-encoding'] = 'chunked'
742 elseif body == nil then
743 -- Empty body
744 hdrs['content-length'] = 0
745 else
746 body = tostring(body)
747 hdrs['content-length'] = #body
748 end
749
750 if hdrs.server == nil then
751 hdrs.server = sprintf('Tarantool http (tarantool v%s)', _TARANTOOL)
752 end
753
754 if p.proto[1] ~= 1 then
755 hdrs.connection = 'close'
756 elseif p.broken then
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'
765 else
766 hdrs.connection = 'keep-alive'
767 end
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'
773 else
774 hdrs.connection = 'close'
775 end
776 end
777
778 local response = {
779 "HTTP/1.1 ";
780 status;
781 " ";
782 reason_by_code(status);
783 "\r\n";
784 };
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))
789 end
790 else
791 table.insert(response, sprintf("%s: %s\r\n", ucfirst(k), v))
792 end
793 end
794 table.insert(response, "\r\n")
795
796 if type(body) == 'string' then
797 table.insert(response, body)
798 response = table.concat(response)
799 if not s:write(response) then
800 break
801 end
802 elseif gen then
803 response = table.concat(response)
804 if not s:write(response) then
805 break
806 end
807 response = nil
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
812 break
813 end
814 end
815 if not s:write("0\r\n\r\n") then
816 break
817 end
818 else
819 response = table.concat(response)
820 if not s:write(response) then
821 break
822 end
823 end
824
825 if p.proto[1] ~= 1 then
826 break
827 end
828
829 if hdrs.connection ~= 'keep-alive' then
830 break
831 end
832 end
833 end
834
835 local function httpd_stop(self)
836 if type(self) ~= 'table' then
837 error("httpd: usage: httpd:stop()")
838 end
839 if self.is_run then
840 self.is_run = false
841 else
842 error("server is already stopped")
843 end
844
845 if self.tcp_server ~= nil then
846 self.tcp_server:close()
847 self.tcp_server = nil
848 end
849 return self
850 end
851
852 local function match_route(self, method, route)
853 -- route must have '/' at the begin and end
854 if string.match(route, '.$') ~= '/' then
855 route = route .. '/'
856 end
857 if string.match(route, '^.') ~= '/' then
858 route = '/' .. route
859 end
860
861 method = string.upper(method)
862
863 local fit
864 local stash = {}
865
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) }
869 local nfit
870 if #m > 0 then
871 if #r.stash > 0 then
872 if #r.stash == #m then
873 nfit = r
874 end
875 else
876 nfit = r
877 end
878
879 if nfit ~= nil then
880 if fit == nil then
881 fit = nfit
882 stash = m
883 else
884 if #fit.stash > #nfit.stash then
885 fit = nfit
886 stash = m
887 elseif r.method ~= fit.method then
888 if fit.method == 'ANY' then
889 fit = nfit
890 stash = m
891 end
892 end
893 end
894 end
895 end
896 end
897 end
898
899 if fit == nil then
900 return fit
901 end
902 local resstash = {}
903 for i = 1, #fit.stash do
904 resstash[ fit.stash[ i ] ] = stash[ i ]
905 end
906 return { endpoint = fit, stash = resstash }
907 end
908
909 local function set_helper(self, name, sub)
910 if sub == nil or type(sub) == 'function' then
911 self.helpers[ name ] = sub
912 return self
913 end
914 errorf("Wrong type for helper function: %s", type(sub))
915 end
916
917 local function set_hook(self, name, sub)
918 if sub == nil or type(sub) == 'function' then
919 self.hooks[ name ] = sub
920 return self
921 end
922 errorf("Wrong type for hook function: %s", type(sub))
923 end
924
925 local function url_for_route(r, args, query)
926 if args == nil then
927 args = {}
928 end
929 local name = r.path
930 for i, sn in pairs(r.stash) do
931 local sv = args[sn]
932 if sv == nil then
933 sv = ''
934 end
935 name = string.gsub(name, '[*:]' .. sn, sv, 1)
936 end
937
938 if query ~= nil then
939 if type(query) == 'table' then
940 local sep = '?'
941 for k, v in pairs(query) do
942 name = name .. sep .. uri_escape(k) .. '=' .. uri_escape(v)
943 sep = '&'
944 end
945 else
946 name = name .. '?' .. query
947 end
948 end
949
950 if string.match(name, '^/') == nil then
951 return '/' .. name
952 else
953 return name
954 end
955 end
956
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'",
964 ctx, action)
965 end
966 return tx.httpd.cache[ ctx ][ action ](tx)
967 end
968 end
969
970 local ppath = package.path
971 package.path = catfile(tx.httpd.options.app_dir, 'controllers', '?.lua')
972 .. ';'
973 .. catfile(tx.httpd.options.app_dir,
974 'controllers', '?/init.lua')
975 if ppath ~= nil then
976 package.path = package.path .. ';' .. ppath
977 end
978
979 local st, mod = pcall(require, ctx)
980 package.path = ppath
981 package.loaded[ ctx ] = nil
982
983 if not st then
984 errorf("Can't load module '%s': %s'", ctx, tostring(mod))
985 end
986
987 if type(mod) ~= 'table' then
988 errorf("require '%s' didn't return table", ctx)
989 end
990
991 if type(mod[ action ]) ~= 'function' then
992 errorf("Controller '%s' doesn't contain function '%s'", ctx, action)
993 end
994
995 if tx.httpd.options.cache_controllers then
996 tx.httpd.cache[ ctx ] = mod
997 end
998
999 return mod[action](tx)
1000 end
1001
1002 local possible_methods = {
1003 GET = 'GET',
1004 HEAD = 'HEAD',
1005 POST = 'POST',
1006 PUT = 'PUT',
1007 DELETE = 'DELETE',
1008 PATCH = 'PATCH',
1009 }
1010
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)")
1014 end
1015
1016 opts = extend({method = 'ANY'}, opts, false)
1017
1018 local ctx
1019 local action
1020
1021 if sub == nil then
1022 sub = render
1023 elseif type(sub) == 'string' then
1024
1025 ctx, action = string.match(sub, '(.+)#(.*)')
1026
1027 if ctx == nil or action == nil then
1028 errorf("Wrong controller format '%s', must be 'module#action'", sub)
1029 end
1030
1031 sub = ctx_action
1032
1033 elseif type(sub) ~= 'function' then
1034 errorf("wrong argument: expected function, but received %s",
1035 type(sub))
1036 end
1037
1038 opts.method = possible_methods[string.upper(opts.method)] or 'ANY'
1039
1040 if opts.path == nil then
1041 error("path is not defined")
1042 end
1043
1044 opts.controller = ctx
1045 opts.action = action
1046 opts.match = opts.path
1047 opts.match = string.gsub(opts.match, '[-]', "[-]")
1048
1049 local estash = { }
1050 local stash = { }
1051 while true do
1052 local name = string.match(opts.match, ':([%a_][%w_]*)')
1053 if name == nil then
1054 break
1055 end
1056 if estash[name] then
1057 errorf("duplicate stash: %s", name)
1058 end
1059 estash[name] = true
1060 opts.match = string.gsub(opts.match, ':[%a_][%w_]*', '([^/]-)', 1)
1061
1062 table.insert(stash, name)
1063 end
1064 while true do
1065 local name = string.match(opts.match, '[*]([%a_][%w_]*)')
1066 if name == nil then
1067 break
1068 end
1069 if estash[name] then
1070 errorf("duplicate stash: %s", name)
1071 end
1072 estash[name] = true
1073 opts.match = string.gsub(opts.match, '[*][%a_][%w_]*', '(.-)', 1)
1074
1075 table.insert(stash, name)
1076 end
1077
1078 if string.match(opts.match, '.$') ~= '/' then
1079 opts.match = opts.match .. '/'
1080 end
1081 if string.match(opts.match, '^.') ~= '/' then
1082 opts.match = '/' .. opts.match
1083 end
1084
1085 opts.match = '^' .. opts.match .. '$'
1086
1087 estash = nil
1088
1089 opts.stash = stash
1090 opts.sub = sub
1091 opts.url_for = url_for_route
1092
1093 if opts.name ~= nil then
1094 if opts.name == 'current' then
1095 error("Route can not have name 'current'")
1096 end
1097 if self.iroutes[ opts.name ] ~= nil then
1098 errorf("Route with name '%s' is already exists", opts.name)
1099 end
1100 table.insert(self.routes, opts)
1101 self.iroutes[ opts.name ] = #self.routes
1102 else
1103 table.insert(self.routes, opts)
1104 end
1105 return self
1106 end
1107
1108 local function url_for_httpd(httpd, name, args, query)
1109
1110 local idx = httpd.iroutes[ name ]
1111 if idx ~= nil then
1112 return httpd.routes[ idx ]:url_for(args, query)
1113 end
1114
1115 if string.match(name, '^/') == nil then
1116 if string.match(name, '^https?://') ~= nil then
1117 return name
1118 else
1119 return '/' .. name
1120 end
1121 else
1122 return name
1123 end
1124 end
1125
1126 local function httpd_start(self)
1127 if type(self) ~= 'table' then
1128 error("httpd: usage: httpd:start()")
1129 end
1130
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()))
1135 end
1136
1137 rawset(self, 'is_run', true)
1138 rawset(self, 'tcp_server', server)
1139 rawset(self, 'stop', httpd_stop)
1140
1141 return self
1142 end
1143
1144 local exports = {
1145 new = function(host, port, options)
1146 if options == nil then
1147 options = {}
1148 end
1149 if type(options) ~= 'table' then
1150 errorf("options must be table not '%s'", type(options))
1151 end
1152 local default = {
1153 max_header_size = 4096,
1154 header_timeout = 100,
1155 handler = handler,
1156 app_dir = '.',
1157 charset = 'utf-8',
1158 cache_templates = true,
1159 cache_controllers = true,
1160 cache_static = true,
1161 log_requests = true,
1162 log_errors = true,
1163 display_errors = true,
1164 }
1165
1166 local self = {
1167 host = host,
1168 port = port,
1169 is_run = false,
1170 stop = httpd_stop,
1171 start = httpd_start,
1172 options = extend(default, options, true),
1173
1174 routes = { },
1175 iroutes = { },
1176 helpers = {
1177 url_for = url_for_helper,
1178 },
1179 hooks = { },
1180
1181 -- methods
1182 route = add_route,
1183 match = match_route,
1184 helper = set_helper,
1185 hook = set_hook,
1186 url_for = url_for_httpd,
1187
1188 -- caches
1189 cache = {
1190 tpl = {},
1191 ctx = {},
1192 static = {},
1193 },
1194 }
1195
1196 return self
1197 end
1198 }
1199
1200 return exports