• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

tarantool / http / 4980299572

pending completion
4980299572

push

github

GitHub
Merge 18a0c21e1 into e914200d7

6 of 6 new or added lines in 1 file covered. (100.0%)

607 of 776 relevant lines covered (78.22%)

155.37 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

78.08
/http/server.lua
1
-- http.server
2

3
local lib = require('http.lib')
2✔
4

5
local fio = require('fio')
2✔
6
local require = require
2✔
7
local package = package
2✔
8
local mime_types = require('http.mime_types')
2✔
9
local codes = require('http.codes')
2✔
10

11
local log = require('log')
2✔
12
local socket = require('socket')
2✔
13
local json = require('json')
2✔
14
local errno = require 'errno'
2✔
15

16
local DETACHED = 101
2✔
17

18
local function errorf(fmt, ...)
19
    error(string.format(fmt, ...))
4✔
20
end
21

22
local function sprintf(fmt, ...)
23
    return string.format(fmt, ...)
1,924✔
24
end
25

26
-- Converts a table to a map, values becomes keys with value 'true'.
27
-- { 'a', 'b', 'c' } -> { 'a' = true, 'b' == 'true', 'c' = true }
28
local function tomap(tbl)
29
    local map = {}
80✔
30
    for _, v in pairs(tbl) do
84✔
31
        map[v] = true
4✔
32
    end
33
    return map
80✔
34
end
35

36
local function valid_cookie_value_byte(byte)
37
    -- https://tools.ietf.org/html/rfc6265#section-4.1.1
38
    -- US-ASCII characters excluding CTLs, whitespace DQUOTE, comma, semicolon,
39
    -- and backslash.
40
    return 32 < byte and byte < 127 and byte ~= string.byte('"') and
1,604✔
41
            byte ~= string.byte(",") and byte ~= string.byte(";") and byte ~= string.byte("\\")
1,604✔
42
end
43

44
local function valid_cookie_path_byte(byte)
45
    -- https://tools.ietf.org/html/rfc6265#section-4.1.1
46
    -- <any CHAR except CTLs or ";">
47
    return 32 <= byte and byte < 127 and byte ~= string.byte(";")
604✔
48
end
49

50
local function escape_char(char)
51
    return string.format('%%%02X', string.byte(char))
28✔
52
end
53

54
local function unescape_char(char)
55
    return string.char(tonumber(char, 16))
4✔
56
end
57

58
local function escape_string(str, byte_filter)
59
    local result = {}
598✔
60
    for i = 1, str:len() do
3,404✔
61
        local char = str:sub(i,i)
2,208✔
62
        if byte_filter(string.byte(char)) then
4,416✔
63
            result[i] = char
2,180✔
64
        else
65
            result[i] = escape_char(char)
56✔
66
        end
67
    end
68
    return table.concat(result)
598✔
69
end
70

71
local function escape_value(cookie_value)
72
    return escape_string(cookie_value, valid_cookie_value_byte)
398✔
73
end
74

75
local function escape_path(cookie_path)
76
    return escape_string(cookie_path, valid_cookie_path_byte)
200✔
77
end
78

79
local function uri_escape(str)
80
    local res = {}
8✔
81
    if type(str) == 'table' then
8✔
82
        for _, v in pairs(str) do
×
83
            table.insert(res, uri_escape(v))
×
84
        end
85
    else
86
        res = string.gsub(str, '[^a-zA-Z0-9_]', escape_char)
8✔
87
    end
88
    return res
8✔
89
end
90

91
local function uri_unescape(str, unescape_plus_sign)
92
    local res = {}
96✔
93
    if type(str) == 'table' then
96✔
94
        for _, v in pairs(str) do
×
95
            table.insert(res, uri_unescape(v))
×
96
        end
97
    else
98
        if unescape_plus_sign ~= nil then
96✔
99
            str = string.gsub(str, '+', ' ')
12✔
100
        end
101

102
        res = string.gsub(str, '%%([0-9a-fA-F][0-9a-fA-F])', unescape_char)
96✔
103
    end
104
    return res
96✔
105
end
106

107
local function extend(tbl, tblu, raise)
108
    local res = {}
1,186✔
109
    for k, v in pairs(tbl) do
3,386✔
110
        res[ k ] = v
2,200✔
111
    end
112
    for k, v in pairs(tblu) do
2,880✔
113
        if raise then
1,694✔
114
            if res[ k ] == nil then
276✔
115
                errorf("Unknown option '%s'", k)
×
116
            end
117
        end
118
        res[ k ] = v
1,694✔
119
    end
120
    return res
1,186✔
121
end
122

123
local function type_by_format(fmt)
124
    if fmt == nil then
20✔
125
        return 'application/octet-stream'
×
126
    end
127

128
    local t = mime_types[ fmt ]
20✔
129

130
    if t ~= nil then
20✔
131
        return t
20✔
132
    end
133

134
    return 'application/octet-stream'
×
135
end
136

137
local function reason_by_code(code)
138
    code = tonumber(code)
82✔
139
    if codes[code] ~= nil then
82✔
140
        return codes[code]
82✔
141
    end
142
    return sprintf('Unknown code %d', code)
×
143
end
144

145
local function ucfirst(str)
146
    return str:gsub("^%l", string.upper, 1)
386✔
147
end
148

149
local function cached_query_param(self, name)
150
    if name == nil then
8✔
151
        return self.query_params
8✔
152
    end
153
    return self.query_params[ name ]
×
154
end
155

156
local function cached_post_param(self, name)
157
    if name == nil then
10✔
158
        return self.post_params
8✔
159
    end
160
    return self.post_params[ name ]
2✔
161
end
162

163
local function request_tostring(self)
164
        local res = self:request_line() .. "\r\n"
36✔
165

166
        for hn, hv in pairs(self.headers) do
72✔
167
            res = sprintf("%s%s: %s\r\n", res, ucfirst(hn), hv)
162✔
168
        end
169

170
        return sprintf("%s\r\n%s", res, self.body)
18✔
171
end
172

173
local function request_line(self)
174
        local rstr = self.path
22✔
175
        if string.len(self.query) then
44✔
176
            rstr = rstr .. '?' .. self.query
22✔
177
        end
178
        return sprintf("%s %s HTTP/%d.%d",
22✔
179
            self.method, rstr, self.proto[1], self.proto[2])
22✔
180
end
181

182
local function query_param(self, name)
183
        if self.query == nil and string.len(self.query) == 0 then
8✔
184
            rawset(self, 'query_params', {})
×
185
        else
186
            local params = lib.params(self.query)
8✔
187
            local pres = {}
8✔
188
            for k, v in pairs(params) do
14✔
189
                pres[ uri_unescape(k, true) ] = uri_unescape(v, true)
18✔
190
            end
191
            rawset(self, 'query_params', pres)
8✔
192
        end
193

194
        rawset(self, 'query_param', cached_query_param)
8✔
195
        return self:query_param(name)
8✔
196
end
197

198
local function request_content_type(self)
199
    -- returns content type without encoding string
200
    if self.headers['content-type'] == nil then
4✔
201
        return nil
×
202
    end
203

204
    return string.match(self.headers['content-type'],
8✔
205
                        '^([^;]*)$') or
8✔
206
        string.match(self.headers['content-type'],
×
207
                     '^(.*);.*')
4✔
208
end
209

210
local function post_param(self, name)
211
    local body = self:read_cached()
10✔
212

213
    if body == '' then
10✔
214
        rawset(self, 'post_params', {})
8✔
215
    elseif self:content_type() == 'multipart/form-data' then
4✔
216
        -- TODO: do that!
217
        rawset(self, 'post_params', {})
×
218
    elseif self:content_type() == 'application/json' then
4✔
219
        local params = self:json()
2✔
220
        rawset(self, 'post_params', params)
2✔
221
    elseif self:content_type() == 'application/x-www-form-urlencoded' then
×
222
        local params = lib.params(body)
×
223
        local pres = {}
×
224
        for k, v in pairs(params) do
×
225
            pres[ uri_unescape(k) ] = uri_unescape(v, true)
×
226
        end
227
        rawset(self, 'post_params', pres)
×
228
    else
229
        local params = lib.params(body)
×
230
        local pres = {}
×
231
        for k, v in pairs(params) do
×
232
            pres[ uri_unescape(k) ] = uri_unescape(v)
×
233
        end
234
        rawset(self, 'post_params', pres)
×
235
    end
236

237
    rawset(self, 'post_param', cached_post_param)
10✔
238
    return self:post_param(name)
10✔
239
end
240

241
local function param(self, name)
242
        if name ~= nil then
8✔
243
            local v = self:post_param(name)
×
244
            if v ~= nil then
×
245
                return v
×
246
            end
247
            return self:query_param(name)
×
248
        end
249

250
        local post = self:post_param()
8✔
251
        local query = self:query_param()
8✔
252
        return extend(post, query, false)
8✔
253
end
254

255
local function catfile(...)
256
    local sp = { ... }
38✔
257

258
    local path
259

260
    if #sp == 0 then
38✔
261
        return
×
262
    end
263

264
    for _, pe in pairs(sp) do
150✔
265
        if path == nil then
112✔
266
            path = pe
38✔
267
        elseif string.match(path, '.$') ~= '/' then
74✔
268
            if string.match(pe, '^.') ~= '/' then
74✔
269
                path = path .. '/' .. pe
68✔
270
            else
271
                path = path .. pe
6✔
272
            end
273
        else
274
            if string.match(pe, '^.') == '/' then
×
275
                path = path .. string.gsub(pe, '^/', '', 1)
×
276
            else
277
                path = path .. pe
×
278
            end
279
        end
280
    end
281

282
    return path
38✔
283
end
284

285
local response_mt
286
local request_mt
287

288
local function expires_str(str)
289

290
    local now = os.time()
2✔
291
    local gmtnow = now - os.difftime(now, os.time(os.date("!*t", now)))
2✔
292
    local fmt = '%a, %d-%b-%Y %H:%M:%S GMT'
2✔
293

294
    if str == 'now' or str == 0 or str == '0' then
2✔
295
        return os.date(fmt, gmtnow)
×
296
    end
297

298
    local diff, period = string.match(str, '^[+]?(%d+)([hdmy])$')
2✔
299
    if period == nil then
2✔
300
        return str
2✔
301
    end
302

303
    diff = tonumber(diff)
×
304
    if period == 'h' then
×
305
        diff = diff * 3600
×
306
    elseif period == 'd' then
×
307
        diff = diff * 86400
×
308
    elseif period == 'm' then
×
309
        diff = diff * 86400 * 30
×
310
    else
311
        diff = diff * 86400 * 365
×
312
    end
313

314
    return os.date(fmt, gmtnow + diff)
×
315
end
316

317
local function setcookie(resp, cookie, options)
318
    options = options or {}
916✔
319
    local name = cookie.name
916✔
320
    local value = cookie.value
916✔
321

322
    if name == nil then
916✔
323
        error('cookie.name is undefined')
×
324
    end
325
    if value == nil then
916✔
326
        error('cookie.value is undefined')
×
327
    end
328

329
    if not options.raw then
916✔
330
        value = escape_value(value)
796✔
331
    end
332
    local str = sprintf('%s=%s', name, value)
916✔
333
    if cookie.path ~= nil then
916✔
334
        local cookie_path = cookie.path
458✔
335
        if not options.raw then
458✔
336
            cookie_path = escape_path(cookie.path)
400✔
337
        end
338
        str = sprintf('%s;path=%s', str, cookie_path)
916✔
339
    end
340
    if cookie.domain ~= nil then
916✔
341
        str = sprintf('%s;domain=%s', str, cookie.domain)
8✔
342
    end
343

344
    if cookie.expires ~= nil then
916✔
345
        str = sprintf('%s;expires=%s', str, expires_str(cookie.expires))
6✔
346
    end
347

348
    if not resp.headers then
916✔
349
        resp.headers = {}
912✔
350
    end
351
    if resp.headers['set-cookie'] == nil then
916✔
352
        resp.headers['set-cookie'] = { str }
916✔
353
    elseif type(resp.headers['set-cookie']) == 'string' then
×
354
        resp.headers['set-cookie'] = {
×
355
            resp.headers['set-cookie'],
×
356
            str
357
        }
358
    else
359
        table.insert(resp.headers['set-cookie'], str)
×
360
    end
361
    return resp
916✔
362
end
363

364
local function cookie(tx, cookie, options)
365
    options = options or {}
4✔
366
    if tx.headers.cookie == nil then
4✔
367
        return nil
×
368
    end
369
    for k, v in string.gmatch(
12✔
370
                tx.headers.cookie, "([^=,; \t]+)=([^,; \t]+)") do
8✔
371
        if k == cookie then
4✔
372
            if not options.raw then
4✔
373
                v = uri_unescape(v)
4✔
374
            end
375
            return v
4✔
376
        end
377
    end
378
    return nil
×
379
end
380

381
local function url_for_helper(tx, name, args, query)
382
    return tx:url_for(name, args, query)
×
383
end
384

385
local function load_template(self, r, format)
386
    if r.template ~= nil then
18✔
387
        return
×
388
    end
389

390
    if format == nil then
18✔
391
        format = 'html'
×
392
    end
393

394
    local file
395
    if r.file ~= nil then
18✔
396
        file = r.file
16✔
397
    elseif r.controller ~= nil and r.action ~= nil then
2✔
398
        file = catfile(
4✔
399
            string.gsub(r.controller, '[.]', '/'),
2✔
400
            r.action .. '.' .. format .. '.el')
4✔
401
    else
402
        errorf("Can not find template for '%s'", r.path)
×
403
    end
404

405
    if self.options.cache_templates then
18✔
406
        if self.cache.tpl[ file ] ~= nil then
18✔
407
            return self.cache.tpl[ file ]
×
408
        end
409
    end
410

411

412
    local tpl = catfile(self.options.app_dir, 'templates', file)
18✔
413
    local fh, err = fio.open(tpl)
18✔
414
    if err ~= nil then
18✔
415
        errorf("Can not load template for '%s': '%s'", r.path, err)
×
416
    end
417

418
    local template
419
    template, err = fh:read()
36✔
420
    if err ~= nil then
18✔
421
        errorf("Can not load template for '%s': '%s'", r.path, err)
×
422
    end
423

424
    fh:close()
18✔
425

426
    if self.options.cache_templates then
18✔
427
        self.cache.tpl[ file ] = template
18✔
428
    end
429
    return template
18✔
430
end
431

432
local function render(tx, opts)
433
    if tx == nil then
48✔
434
        error("Usage: self:render({ ... })")
×
435
    end
436

437
    local resp = setmetatable({ headers = {} }, response_mt)
48✔
438
    local vars = {}
48✔
439
    if opts ~= nil then
48✔
440
        if opts.text ~= nil then
40✔
441
            if tx.httpd.options.charset ~= nil then
26✔
442
                resp.headers['content-type'] =
26✔
443
                    sprintf("text/plain; charset=%s",
52✔
444
                        tx.httpd.options.charset
26✔
445
                    )
52✔
446
            else
447
                resp.headers['content-type'] = 'text/plain'
×
448
            end
449
            resp.body = tostring(opts.text)
26✔
450
            return resp
26✔
451
        end
452

453
        if opts.json ~= nil then
14✔
454
            if tx.httpd.options.charset ~= nil then
4✔
455
                resp.headers['content-type'] =
4✔
456
                    sprintf('application/json; charset=%s',
8✔
457
                        tx.httpd.options.charset
4✔
458
                    )
8✔
459
            else
460
                resp.headers['content-type'] = 'application/json'
×
461
            end
462
            resp.body = json.encode(opts.json)
4✔
463
            return resp
4✔
464
        end
465

466
        if opts.data ~= nil then
10✔
467
            resp.body = tostring(opts.data)
×
468
            return resp
×
469
        end
470

471
        vars = extend(tx.tstash, opts, false)
20✔
472
    end
473

474
    local tpl
475

476
    local format = tx.tstash.format
18✔
477
    if format == nil then
18✔
478
        format = 'html'
×
479
    end
480

481
    if tx.endpoint.template ~= nil then
18✔
482
        tpl = tx.endpoint.template
×
483
    else
484
        tpl = load_template(tx.httpd, tx.endpoint, format)
36✔
485
        if tpl == nil then
18✔
486
            errorf('template is not defined for the route')
×
487
        end
488
    end
489

490
    if type(tpl) == 'function' then
18✔
491
        tpl = tpl()
×
492
    end
493

494
    for hname, sub in pairs(tx.httpd.helpers) do
54✔
495
        vars[hname] = function(...) return sub(tx, ...) end
42✔
496
    end
497
    vars.action = tx.endpoint.action
18✔
498
    vars.controller = tx.endpoint.controller
18✔
499
    vars.format = format
18✔
500

501
    resp.body = lib.template(tpl, vars)
18✔
502
    resp.headers['content-type'] = type_by_format(format)
36✔
503

504
    if tx.httpd.options.charset ~= nil then
18✔
505
        if format == 'html' or format == 'js' or format == 'json' then
18✔
506
            resp.headers['content-type'] = resp.headers['content-type']
18✔
507
                .. '; charset=' .. tx.httpd.options.charset
18✔
508
        end
509
    end
510
    return resp
18✔
511
end
512

513
local function iterate(_, gen, param, state)
514
    return setmetatable({ body = { gen = gen, param = param, state = state } },
2✔
515
        response_mt)
2✔
516
end
517

518
local function redirect_to(tx, name, args, query)
519
    local location = tx:url_for(name, args, query)
×
520
    return setmetatable({ status = 302, headers = { location = location } },
×
521
        response_mt)
×
522
end
523

524
local function access_stash(tx, name, ...)
525
    if type(tx) ~= 'table' then
×
526
        error("usage: ctx:stash('name'[, 'value'])")
×
527
    end
528
    if select('#', ...) > 0 then
×
529
        tx.tstash[ name ] = select(1, ...)
×
530
    end
531

532
    return tx.tstash[ name ]
×
533
end
534

535
local function url_for_tx(tx, name, args, query)
536
    if name == 'current' then
×
537
        return tx.endpoint:url_for(args, query)
×
538
    end
539
    return tx.httpd:url_for(name, args, query)
×
540
end
541

542
local function request_json(req)
543
    local data = req:read_cached()
4✔
544
    local s, json = pcall(json.decode, data)
4✔
545
    if s then
4✔
546
       return json
4✔
547
    else
548
       error(sprintf("Can't decode json in request '%s': %s",
×
549
           data, tostring(json)))
×
550
       return nil
×
551
    end
552
end
553

554
local function request_read(req, opts, timeout)
555
    local remaining = req._remaining
112✔
556
    if not remaining then
112✔
557
        remaining = tonumber(req.headers['content-length'])
100✔
558
        if not remaining then
100✔
559
            return ''
88✔
560
        end
561
    end
562

563
    if opts == nil then
24✔
564
        opts = remaining
24✔
565
    elseif type(opts) == 'number' then
×
566
        if opts > remaining then
×
567
            opts = remaining
×
568
        end
569
    elseif type(opts) == 'string' then
×
570
        opts = { size = remaining, delimiter = opts }
×
571
    elseif type(opts) == 'table' then
×
572
        local size = opts.size or opts.chunk
×
573
        if size and size > remaining then
×
574
            opts.size = remaining
×
575
            opts.chunk = nil
×
576
        end
577
    end
578

579
    local buf = req.s:read(opts, timeout)
24✔
580
    if buf == nil then
24✔
581
        req._remaining = 0
×
582
        return ''
×
583
    end
584
    remaining = remaining - #buf
24✔
585
    assert(remaining >= 0)
24✔
586
    req._remaining = remaining
24✔
587
    return buf
24✔
588
end
589

590
local function request_read_cached(self)
591
    if self.cached_data == nil then
18✔
592
        local data = self:read()
12✔
593
        rawset(self, 'cached_data', data)
12✔
594
        return data
12✔
595
    else
596
        return self.cached_data
6✔
597
    end
598
end
599

600
local function static_file(self, request, format)
601
        local file = catfile(self.options.app_dir, 'public', request.path)
6✔
602

603
        if self.options.cache_static and self.cache.static[ file ] ~= nil then
6✔
604
            return {
×
605
                code = 200,
606
                headers = {
×
607
                    [ 'content-type'] = type_by_format(format),
608
                },
609
                body = self.cache.static[ file ]
×
610
            }
611
        end
612

613
        local fh, err = fio.open(file, {'O_RDONLY'})
6✔
614
        if err ~= nil then
6✔
615
            return { status = 404 }
4✔
616
        end
617

618
        local body
619
        body, err = fh:read()
4✔
620
        if err ~= nil then
2✔
621
            errorf("Can not return static file for '%s': '%s'", request:path(), err)
×
622
        end
623

624
        fh:close()
2✔
625

626
        if self.options.cache_static then
2✔
627
            self.cache.static[ file ] = body
2✔
628
        end
629

630
        return {
2✔
631
            status = 200,
632
            headers = {
2✔
633
                [ 'content-type'] = type_by_format(format),
4✔
634
            },
2✔
635
            body = body
2✔
636
        }
2✔
637
end
638

639
request_mt = {
2✔
640
    __index = {
2✔
641
        render      = render,
2✔
642
        cookie      = cookie,
2✔
643
        redirect_to = redirect_to,
2✔
644
        iterate     = iterate,
2✔
645
        stash       = access_stash,
2✔
646
        url_for     = url_for_tx,
2✔
647
        content_type= request_content_type,
2✔
648
        request_line= request_line,
2✔
649
        read_cached = request_read_cached,
2✔
650
        query_param = query_param,
2✔
651
        post_param  = post_param,
2✔
652
        param       = param,
2✔
653
        read        = request_read,
2✔
654
        json        = request_json
2✔
655
    },
2✔
656
    __tostring = request_tostring;
2✔
657
}
2✔
658

659
response_mt = {
2✔
660
    __index = {
2✔
661
        setcookie = setcookie;
2✔
662
    }
2✔
663
}
2✔
664

665
local function is_function(obj)
666
    return type(obj) == 'function'
18✔
667
end
668

669
local function get_request_logger(server_opts, route_opts)
670
    if route_opts and route_opts.endpoint.log_requests ~= nil then
82✔
671
        if is_function(route_opts.endpoint.log_requests) then
12✔
672
            return route_opts.endpoint.log_requests
6✔
673
        elseif route_opts.endpoint.log_requests == false then
×
674
            return log.debug
×
675
        end
676
    end
677

678
    if server_opts.log_requests then
76✔
679
        if is_function(server_opts.log_requests) then
12✔
680
            return server_opts.log_requests
4✔
681
        end
682

683
        return log.info
2✔
684
    end
685

686
    return log.debug
70✔
687
end
688

689
local function get_error_logger(server_opts, route_opts)
690
    if route_opts and route_opts.endpoint.log_errors ~= nil then
12✔
691
        if is_function(route_opts.endpoint.log_errors) then
8✔
692
            return route_opts.endpoint.log_errors
4✔
693
        elseif route_opts.endpoint.log_errors == false then
×
694
            return log.debug
×
695
        end
696
    end
697

698
    if server_opts.log_errors then
8✔
699
        if is_function(server_opts.log_errors) then
4✔
700
            return server_opts.log_errors
2✔
701
        end
702

703
        return log.error
×
704
    end
705

706
    return log.debug
6✔
707
end
708

709
local function handler(self, request)
710
    if self.hooks.before_dispatch ~= nil then
82✔
711
        self.hooks.before_dispatch(self, request)
×
712
    end
713

714
    local format = 'html'
82✔
715

716
    local pformat = string.match(request.path, '[.]([^.]+)$')
82✔
717
    if pformat ~= nil then
82✔
718
        format = pformat
4✔
719
    end
720

721
    local r = self:match(request.method, request.path)
82✔
722
    if r == nil then
82✔
723
        return static_file(self, request, format)
6✔
724
    end
725

726
    local stash = extend(r.stash, { format = format })
76✔
727

728
    request.endpoint = r.endpoint
76✔
729
    request.tstash   = stash
76✔
730

731
    local resp = r.endpoint.sub(request)
76✔
732
    if self.hooks.after_dispatch ~= nil then
64✔
733
        self.hooks.after_dispatch(request, resp)
×
734
    end
735
    return resp
64✔
736
end
737

738
local function normalize_headers(hdrs)
739
    local res = {}
54✔
740
    for h, v in pairs(hdrs) do
108✔
741
        res[ string.lower(h) ] = v
108✔
742
    end
743
    return res
54✔
744
end
745

746
local function parse_request(req)
747
    local p = lib._parse_request(req)
82✔
748
    if p.error then
82✔
749
        return p
×
750
    end
751
    p.path_raw = p.path
82✔
752
    p.path = uri_unescape(p.path)
164✔
753
    if p.path:sub(1, 1) ~= "/" then
164✔
754
        p.error = "invalid uri"
×
755
        return p
×
756
    end
757
    for _, path_segment in ipairs(p.path:split('/')) do
328✔
758
        if path_segment == "." or path_segment == ".." then
164✔
759
            p.error = "invalid uri"
×
760
            return p
×
761
        end
762
    end
763

764
    return p
82✔
765
end
766

767
local function process_client(self, s, peer)
768
    while true do
769
        local hdrs = ''
88✔
770

771
        local is_eof = false
88✔
772
        while true do
773
            local chunk = s:read({
176✔
774
                delimiter = { "\n\n", "\r\n\r\n" },
88✔
775
            }, self.idle_timeout)
88✔
776

777
            if chunk == '' then
86✔
778
                is_eof = true
2✔
779
                break -- eof
2✔
780
            elseif chunk == nil then
84✔
781
                log.error('failed to read request: %s', errno.strerror())
4✔
782
                return
2✔
783
            end
784

785
            hdrs = hdrs .. chunk
82✔
786

787
            if string.endswith(hdrs, "\n\n") or string.endswith(hdrs, "\r\n\r\n") then
246✔
788
                break
82✔
789
            end
790
        end
791

792
        if is_eof then
84✔
793
            break
2✔
794
        end
795

796
        log.debug("request:\n%s", hdrs)
82✔
797
        local p = parse_request(hdrs)
82✔
798
        if p.error ~= nil then
82✔
799
            log.error('failed to parse request: %s', p.error)
×
800
            s:write(sprintf("HTTP/1.0 400 Bad request\r\n\r\n%s", p.error))
×
801
            break
802
        end
803
        p.httpd = self
82✔
804
        p.s = s
82✔
805
        p.peer = peer
82✔
806
        setmetatable(p, request_mt)
82✔
807

808
        if p.headers['expect'] == '100-continue' then
82✔
809
            s:write('HTTP/1.0 100 Continue\r\n\r\n')
×
810
        end
811

812
        local route = self:match(p.method, p.path)
82✔
813
        local logreq = get_request_logger(self.options, route)
82✔
814
        logreq("%s %s%s", p.method, p.path,
164✔
815
               p.query ~= "" and "?"..p.query or "")
82✔
816

817
        local res, reason = pcall(self.options.handler, self, p)
82✔
818
        p:read() -- skip remaining bytes of request body
82✔
819
        local status, hdrs, body
820

821
        if not res then
82✔
822
            status = 500
12✔
823
            hdrs = {}
12✔
824
            local trace = debug.traceback()
12✔
825
            local logerror = get_error_logger(self.options, route)
12✔
826
            logerror('unhandled error: %s\n%s\nrequest:\n%s',
24✔
827
                     tostring(reason), trace, tostring(p))
12✔
828
            if self.options.display_errors then
12✔
829
            body =
830
                  "Unhandled error: " .. tostring(reason) .. "\n"
6✔
831
                .. trace .. "\n\n"
6✔
832
                .. "\n\nRequest:\n"
6✔
833
                .. tostring(p)
12✔
834
            else
835
                body = "Internal Error"
6✔
836
            end
837
       elseif type(reason) == 'table' then
70✔
838
            if reason.status == nil then
60✔
839
                status = 200
48✔
840
            elseif type(reason.status) == 'number' then
12✔
841
                status = reason.status
12✔
842
            else
843
                error('response.status must be a number')
×
844
            end
845
            if reason.headers == nil then
60✔
846
                hdrs = {}
6✔
847
            elseif type(reason.headers) == 'table' then
54✔
848
                hdrs = normalize_headers(reason.headers)
108✔
849
            else
850
                error('response.headers must be a table')
×
851
            end
852
            body = reason.body
60✔
853
        elseif reason == nil then
10✔
854
            status = 200
10✔
855
            hdrs = {}
10✔
856
        elseif type(reason) == 'number' then
×
857
            if reason == DETACHED then
×
858
                break
859
            end
860
        else
861
            error('invalid response')
×
862
        end
863

864
        local gen, param, state
865
        if type(body) == 'string' then
82✔
866
            -- Plain string
867
            hdrs['content-length'] = #body
66✔
868
        elseif type(body) == 'function' then
16✔
869
            -- Generating function
870
            gen = body
×
871
            hdrs['transfer-encoding'] = 'chunked'
×
872
        elseif type(body) == 'table' and body.gen then
16✔
873
            -- Iterator
874
            gen, param, state = body.gen, body.param, body.state
2✔
875
            hdrs['transfer-encoding'] = 'chunked'
2✔
876
        elseif body == nil then
14✔
877
            -- Empty body
878
            hdrs['content-length'] = 0
14✔
879
        else
880
            body = tostring(body)
×
881
            hdrs['content-length'] = #body
×
882
        end
883

884
        if hdrs['content-type'] == nil then
82✔
885
            hdrs['content-type'] = 'text/plain; charset=utf-8'
32✔
886
        end
887

888
        if hdrs.server == nil then
82✔
889
            hdrs.server = sprintf('Tarantool http (tarantool v%s)', _TARANTOOL)
164✔
890
        end
891

892
        if p.proto[1] ~= 1 then
82✔
893
            hdrs.connection = 'close'
×
894
        elseif p.broken then
82✔
895
            hdrs.connection = 'close'
×
896
        elseif rawget(p, 'body') == nil then
82✔
897
            hdrs.connection = 'close'
×
898
        elseif p.proto[2] == 1 then
82✔
899
            if p.headers.connection == nil then
82✔
900
                hdrs.connection = 'keep-alive'
×
901
            elseif string.lower(p.headers.connection) ~= 'keep-alive' then
164✔
902
                hdrs.connection = 'close'
74✔
903
            else
904
                hdrs.connection = 'keep-alive'
8✔
905
            end
906
        elseif p.proto[2] == 0 then
×
907
            if p.headers.connection == nil then
×
908
                hdrs.connection = 'close'
×
909
            elseif string.lower(p.headers.connection) == 'keep-alive' then
×
910
                hdrs.connection = 'keep-alive'
×
911
            else
912
                hdrs.connection = 'close'
×
913
            end
914
        end
915

916
        local useragent = p.headers['user-agent']
82✔
917
        if self.disable_keepalive[useragent] == true then
82✔
918
            hdrs.connection = 'close'
2✔
919
        end
920

921
        local response = {
82✔
922
            "HTTP/1.1 ";
923
            status;
82✔
924
            " ";
925
            reason_by_code(status);
164✔
926
            "\r\n";
927
        };
928
        for k, v in pairs(hdrs) do
414✔
929
            if type(v) == 'table' then
332✔
930
                for _, sv in pairs(v) do
8✔
931
                    table.insert(response, sprintf("%s: %s\r\n", ucfirst(k), sv))
12✔
932
                end
933
            else
934
                table.insert(response, sprintf("%s: %s\r\n", ucfirst(k), v))
984✔
935
            end
936
        end
937
        table.insert(response, "\r\n")
82✔
938

939
        if type(body) == 'string' then
82✔
940
            table.insert(response, body)
66✔
941
            response = table.concat(response)
66✔
942
            if not s:write(response) then
132✔
943
                break
944
            end
945
        elseif gen then
16✔
946
            response = table.concat(response)
2✔
947
            if not s:write(response) then
4✔
948
                break
949
            end
950
            response = nil -- luacheck: no unused
2✔
951
            -- Transfer-Encoding: chunked
952
            for _, part in gen, param, state do
8✔
953
                part = tostring(part)
6✔
954
                if not s:write(sprintf("%x\r\n%s\r\n", #part, part)) then
18✔
955
                    break
956
                end
957
            end
958
            if not s:write("0\r\n\r\n") then
4✔
959
                break
960
            end
961
        else
962
            response = table.concat(response)
14✔
963
            if not s:write(response) then
28✔
964
                break
965
            end
966
        end
967

968
        if p.proto[1] ~= 1 then
82✔
969
            break
970
        end
971

972
        if hdrs.connection ~= 'keep-alive' then
82✔
973
            break
76✔
974
        end
975
    end
976
end
977

978
local function httpd_stop(self)
979
   if type(self) ~= 'table' then
78✔
980
       error("httpd: usage: httpd:stop()")
×
981
    end
982
    if self.is_run then
78✔
983
        self.is_run = false
78✔
984
    else
985
        error("server is already stopped")
×
986
    end
987

988
    if self.tcp_server ~= nil then
78✔
989
        self.tcp_server:close()
78✔
990
        self.tcp_server = nil
78✔
991
    end
992
    return self
78✔
993
end
994

995
local function match_route(self, method, route)
996
    -- route must have '/' at the begin and end
997
    if string.match(route, '.$') ~= '/' then
198✔
998
        route = route .. '/'
176✔
999
    end
1000
    if string.match(route, '^.') ~= '/' then
198✔
1001
        route = '/' .. route
×
1002
    end
1003

1004
    method = string.upper(method)
396✔
1005

1006
    local fit
1007
    local stash = {}
198✔
1008

1009
    for _, r in pairs(self.routes) do
2,604✔
1010
        if r.method == method or r.method == 'ANY' then
2,406✔
1011
            local m = { string.match(route, r.match)  }
2,390✔
1012
            local nfit
1013
            if #m > 0 then
2,390✔
1014
                if #r.stash > 0 then
184✔
1015
                    if #r.stash == #m then
32✔
1016
                        nfit = r
32✔
1017
                    end
1018
                else
1019
                    nfit = r
152✔
1020
                end
1021

1022
                if nfit ~= nil then
184✔
1023
                    if fit == nil then
184✔
1024
                        fit = nfit
184✔
1025
                        stash = m
184✔
1026
                    else
1027
                        if #fit.stash > #nfit.stash then
×
1028
                            fit = nfit
×
1029
                            stash = m
×
1030
                        elseif r.method ~= fit.method then
×
1031
                            if fit.method == 'ANY' then
×
1032
                                fit = nfit
×
1033
                                stash = m
×
1034
                            end
1035
                        end
1036
                    end
1037
                end
1038
            end
1039
        end
1040
    end
1041

1042
    if fit == nil then
198✔
1043
        return fit
14✔
1044
    end
1045
    local resstash = {}
184✔
1046
    for i = 1, #fit.stash do
222✔
1047
        resstash[ fit.stash[ i ] ] = stash[ i ]
38✔
1048
    end
1049
    return  { endpoint = fit, stash = resstash }
184✔
1050
end
1051

1052
local function set_helper(self, name, sub)
1053
    if sub == nil or type(sub) == 'function' then
68✔
1054
        self.helpers[ name ] = sub
68✔
1055
        return self
68✔
1056
    end
1057
    errorf("Wrong type for helper function: %s", type(sub))
×
1058
end
1059

1060
local function set_hook(self, name, sub)
1061
    if sub == nil or type(sub) == 'function' then
×
1062
        self.hooks[ name ] = sub
×
1063
        return self
×
1064
    end
1065
    errorf("Wrong type for hook function: %s", type(sub))
×
1066
end
1067

1068
local function url_for_route(r, args, query)
1069
    if args == nil then
8✔
1070
        args = {}
2✔
1071
    end
1072
    local name = r.path
8✔
1073
    for _, sn in pairs(r.stash) do
20✔
1074
        local sv = args[sn]
12✔
1075
        if sv == nil then
12✔
1076
            sv = ''
4✔
1077
        end
1078
        name = string.gsub(name, '[*:]' .. sn, sv, 1)
12✔
1079
    end
1080

1081
    if query ~= nil then
8✔
1082
        if type(query) == 'table' then
2✔
1083
            local sep = '?'
2✔
1084
            for k, v in pairs(query) do
6✔
1085
                name = name .. sep .. uri_escape(k) .. '=' .. uri_escape(v)
12✔
1086
                sep = '&'
4✔
1087
            end
1088
        else
1089
            name = name .. '?' .. query
×
1090
        end
1091
    end
1092

1093
    if string.match(name, '^/') == nil then
8✔
1094
        return '/' .. name
×
1095
    else
1096
        return name
8✔
1097
    end
1098
end
1099

1100
local function ctx_action(tx)
1101
    local ctx = tx.endpoint.controller
6✔
1102
    local action = tx.endpoint.action
6✔
1103
    if tx.httpd.options.cache_controllers then
6✔
1104
        if tx.httpd.cache[ ctx ] ~= nil then
6✔
1105
            if type(tx.httpd.cache[ ctx ][ action ]) ~= 'function' then
×
1106
                errorf("Controller '%s' doesn't contain function '%s'",
×
1107
                    ctx, action)
×
1108
            end
1109
            return tx.httpd.cache[ ctx ][ action ](tx)
×
1110
        end
1111
    end
1112

1113
    local ppath = package.path
6✔
1114
    package.path = catfile(tx.httpd.options.app_dir, 'controllers', '?.lua')
6✔
1115
                .. ';'
1116
                .. catfile(tx.httpd.options.app_dir,
12✔
1117
                    'controllers', '?/init.lua')
12✔
1118
    if ppath ~= nil then
6✔
1119
        package.path = package.path .. ';' .. ppath
6✔
1120
    end
1121

1122
    local st, mod = pcall(require, ctx)
6✔
1123
    package.path = ppath
6✔
1124
    package.loaded[ ctx ] = nil
6✔
1125

1126
    if not st then
6✔
1127
        errorf("Can't load module '%s': %s'", ctx, tostring(mod))
2✔
1128
    end
1129

1130
    if type(mod) ~= 'table' then
4✔
1131
        errorf("require '%s' didn't return table", ctx)
×
1132
    end
1133

1134
    if type(mod[ action ]) ~= 'function' then
4✔
1135
        errorf("Controller '%s' doesn't contain function '%s'", ctx, action)
2✔
1136
    end
1137

1138
    if tx.httpd.options.cache_controllers then
2✔
1139
        tx.httpd.cache[ ctx ] = mod
2✔
1140
    end
1141

1142
    return mod[action](tx)
2✔
1143
end
1144

1145
local possible_methods = {
2✔
1146
    GET    = 'GET',
1147
    HEAD   = 'HEAD',
1148
    POST   = 'POST',
1149
    PUT    = 'PUT',
1150
    DELETE = 'DELETE',
1151
    PATCH  = 'PATCH',
1152
}
1153

1154
local function add_route(self, opts, sub)
1155
    if type(opts) ~= 'table' or type(self) ~= 'table' then
944✔
1156
        error("Usage: httpd:route({ ... }, function(cx) ... end)")
×
1157
    end
1158

1159
    opts = extend({method = 'ANY'}, opts, false)
1,888✔
1160

1161
    local ctx
1162
    local action
1163

1164
    if sub == nil then
944✔
1165
        sub = render
144✔
1166
    elseif type(sub) == 'string' then
800✔
1167

1168
        ctx, action = string.match(sub, '(.+)#(.*)')
204✔
1169

1170
        if ctx == nil or action == nil then
204✔
1171
            errorf("Wrong controller format '%s', must be 'module#action'", sub)
×
1172
        end
1173

1174
        sub = ctx_action
204✔
1175

1176
    elseif type(sub) ~= 'function' then
596✔
1177
        errorf("wrong argument: expected function, but received %s",
×
1178
            type(sub))
×
1179
    end
1180

1181
    opts.method = possible_methods[string.upper(opts.method)] or 'ANY'
1,888✔
1182

1183
    if opts.path == nil then
944✔
1184
        error("path is not defined")
×
1185
    end
1186

1187
    opts.controller = ctx
944✔
1188
    opts.action = action
944✔
1189
    opts.match = opts.path
944✔
1190
    opts.match = string.gsub(opts.match, '[-]', "[-]")
944✔
1191

1192
    local estash = {  }
944✔
1193
    local stash = {  }
944✔
1194
    while true do
1195
        local name = string.match(opts.match, ':([%a_][%w_]*)')
1,352✔
1196
        if name == nil then
1,352✔
1197
            break
944✔
1198
        end
1199
        if estash[name] then
408✔
1200
            errorf("duplicate stash: %s", name)
×
1201
        end
1202
        estash[name] = true
408✔
1203
        opts.match = string.gsub(opts.match, ':[%a_][%w_]*', '([^/]-)', 1)
408✔
1204

1205
        table.insert(stash, name)
408✔
1206
    end
1207
    while true do
1208
        local name = string.match(opts.match, '[*]([%a_][%w_]*)')
1,082✔
1209
        if name == nil then
1,082✔
1210
            break
944✔
1211
        end
1212
        if estash[name] then
138✔
1213
            errorf("duplicate stash: %s", name)
×
1214
        end
1215
        estash[name] = true
138✔
1216
        opts.match = string.gsub(opts.match, '[*][%a_][%w_]*', '(.-)', 1)
138✔
1217

1218
        table.insert(stash, name)
138✔
1219
    end
1220

1221
    if string.match(opts.match, '.$') ~= '/' then
944✔
1222
        opts.match = opts.match .. '/'
926✔
1223
    end
1224
    if string.match(opts.match, '^.') ~= '/' then
944✔
1225
        opts.match = '/' .. opts.match
×
1226
    end
1227

1228
    opts.match = '^' .. opts.match .. '$'
944✔
1229

1230
    estash = nil -- luacheck: no unused
944✔
1231

1232
    opts.stash = stash
944✔
1233
    opts.sub = sub
944✔
1234
    opts.url_for = url_for_route
944✔
1235

1236
    if opts.log_requests ~= nil then
944✔
1237
        if type(opts.log_requests) ~= 'function' and type(opts.log_requests) ~= 'boolean' then
10✔
1238
            error("'log_requests' option should be a function or a boolean")
4✔
1239
        end
1240
    end
1241

1242
    if opts.log_errors ~= nil then
940✔
1243
        if type(opts.log_errors) ~= 'function' and type(opts.log_errors) ~= 'boolean' then
10✔
1244
            error("'log_errors' option should be a function or a boolean")
4✔
1245
        end
1246
    end
1247

1248
    if opts.name ~= nil then
936✔
1249
        if opts.name == 'current' then
136✔
1250
            error("Route can not have name 'current'")
×
1251
        end
1252
        if self.iroutes[ opts.name ] ~= nil then
136✔
1253
            errorf("Route with name '%s' is already exists", opts.name)
×
1254
        end
1255
        table.insert(self.routes, opts)
136✔
1256
        self.iroutes[ opts.name ] = #self.routes
136✔
1257
    else
1258
        table.insert(self.routes, opts)
800✔
1259
    end
1260
    return self
936✔
1261
end
1262

1263
local function url_for_httpd(httpd, name, args, query)
1264

1265
    local idx = httpd.iroutes[ name ]
10✔
1266
    if idx ~= nil then
10✔
1267
        return httpd.routes[ idx ]:url_for(args, query)
8✔
1268
    end
1269

1270
    if string.match(name, '^/') == nil then
2✔
1271
        if string.match(name, '^https?://') ~= nil then
2✔
1272
            return name
×
1273
        else
1274
            return '/' .. name
2✔
1275
        end
1276
    else
1277
        return name
×
1278
    end
1279
end
1280

1281
local function httpd_start(self)
1282
    if type(self) ~= 'table' then
78✔
1283
        error("httpd: usage: httpd:start()")
×
1284
    end
1285

1286
    local server = self.tcp_server_f(self.host, self.port, {
156✔
1287
        name = 'http',
1288
        handler = function(...)
1289
            self.internal.preprocess_client_handler()
82✔
1290
            process_client(self, ...)
82✔
1291
            self.internal.postprocess_client_handler()
80✔
1292
        end,
1293
        http_server = self,
78✔
1294
    })
1295

1296
    if server == nil then
78✔
1297
        error(sprintf("Can't create tcp_server: %s", errno.strerror()))
×
1298
    end
1299

1300
    rawset(self, 'is_run', true)
78✔
1301
    rawset(self, 'tcp_server', server)
78✔
1302
    rawset(self, 'stop', httpd_stop)
78✔
1303

1304
    return self
78✔
1305
end
1306

1307
local exports = {
2✔
1308
    _VERSION = require('http.version'),
2✔
1309
    DETACHED = DETACHED,
2✔
1310

1311
    new = function(host, port, options)
1312
        if options == nil then
80✔
1313
            options = {}
2✔
1314
        end
1315
        if type(options) ~= 'table' then
80✔
1316
            errorf("options must be table not '%s'", type(options))
×
1317
        end
1318
        local disable_keepalive = options.disable_keepalive or {}
80✔
1319
        if type(disable_keepalive) ~= 'table' then
80✔
1320
            error('Option disable_keepalive must be a table.')
×
1321
        end
1322
        if options.idle_timeout ~= nil and
80✔
1323
           type(options.idle_timeout) ~= 'number' then
2✔
1324
            error('Option idle_timeout must be a number.')
×
1325
        end
1326

1327
        local default = {
80✔
1328
            max_header_size     = 4096,
1329
            header_timeout      = 100,
1330
            handler             = handler,
80✔
1331
            app_dir             = '.',
1332
            charset             = 'utf-8',
1333
            cache_templates     = true,
1334
            cache_controllers   = true,
1335
            cache_static        = true,
1336
            log_requests        = true,
1337
            log_errors          = true,
1338
            display_errors      = false,
1339
            disable_keepalive   = {},
80✔
1340
            idle_timeout        = 0, -- no timeout, option is disabled
1341
        }
1342

1343
        local self = {
80✔
1344
            host    = host,
80✔
1345
            port    = port,
80✔
1346
            is_run  = false,
1347
            stop    = httpd_stop,
80✔
1348
            start   = httpd_start,
80✔
1349
            options = extend(default, options, true),
160✔
1350

1351
            routes  = {  },
80✔
1352
            iroutes = {  },
80✔
1353
            helpers = {
80✔
1354
                url_for = url_for_helper,
80✔
1355
            },
80✔
1356
            hooks   = {  },
80✔
1357

1358
            -- methods
1359
            route   = add_route,
80✔
1360
            match   = match_route,
80✔
1361
            helper  = set_helper,
80✔
1362
            hook    = set_hook,
80✔
1363
            url_for = url_for_httpd,
80✔
1364

1365
            -- Exposed to make it replaceable by a user.
1366
            tcp_server_f = socket.tcp_server,
80✔
1367

1368
            -- caches
1369
            cache   = {
80✔
1370
                tpl         = {},
80✔
1371
                ctx         = {},
80✔
1372
                static      = {},
80✔
1373
            },
80✔
1374

1375
            disable_keepalive   = tomap(disable_keepalive),
160✔
1376
            idle_timeout        = options.idle_timeout,
80✔
1377

1378
            internal = {
80✔
1379
                preprocess_client_handler = function() end,
154✔
1380
                postprocess_client_handler = function() end,
154✔
1381
            }
80✔
1382
        }
1383

1384
        return self
80✔
1385
    end,
1386

1387
    internal = {
2✔
1388
        response_mt = response_mt,
2✔
1389
        request_mt = request_mt,
2✔
1390
        extend = extend,
2✔
1391
    }
2✔
1392
}
1393

1394
return exports
2✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc