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

tarantool / http / 4541070931

pending completion
4541070931

push

github

GitHub
Merge 6a4b136b6 into e7f263dab

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

610 of 773 relevant lines covered (78.91%)

153.32 hits per line

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

78.78
/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,912✔
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 = {}
78✔
30
    for _, v in pairs(tbl) do
82✔
31
        map[v] = true
4✔
32
    end
33
    return map
78✔
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 = {}
94✔
93
    if type(str) == 'table' then
94✔
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
94✔
99
            str = string.gsub(str, '+', ' ')
12✔
100
        end
101

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

107
local function extend(tbl, tblu, raise)
108
    local res = {}
1,148✔
109
    for k, v in pairs(tbl) do
3,286✔
110
        res[ k ] = v
2,138✔
111
    end
112
    for k, v in pairs(tblu) do
2,792✔
113
        if raise then
1,644✔
114
            if res[ k ] == nil then
268✔
115
                errorf("Unknown option '%s'", k)
×
116
            end
117
        end
118
        res[ k ] = v
1,644✔
119
    end
120
    return res
1,148✔
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)
80✔
139
    if codes[code] ~= nil then
80✔
140
        return codes[code]
80✔
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)
378✔
147
end
148

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

156
local function cached_post_param(self, name)
157
    if name == nil then
8✔
158
        return self.post_params
6✔
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
6✔
184
            rawset(self, 'query_params', {})
×
185
        else
186
            local params = lib.params(self.query)
6✔
187
            local pres = {}
6✔
188
            for k, v in pairs(params) do
12✔
189
                pres[ uri_unescape(k, true) ] = uri_unescape(v, true)
18✔
190
            end
191
            rawset(self, 'query_params', pres)
6✔
192
        end
193

194
        rawset(self, 'query_param', cached_query_param)
6✔
195
        return self:query_param(name)
6✔
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
22✔
201
        return nil
18✔
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
    if self:content_type() == 'multipart/form-data' then
16✔
212
        -- TODO: do that!
213
        rawset(self, 'post_params', {})
×
214
    elseif self:content_type() == 'application/json' then
16✔
215
        local params = self:json()
2✔
216
        rawset(self, 'post_params', params)
2✔
217
    elseif self:content_type() == 'application/x-www-form-urlencoded' then
12✔
218
        local params = lib.params(self:read_cached())
×
219
        local pres = {}
×
220
        for k, v in pairs(params) do
×
221
            pres[ uri_unescape(k) ] = uri_unescape(v, true)
×
222
        end
223
        rawset(self, 'post_params', pres)
×
224
    else
225
        local params = lib.params(self:read_cached())
12✔
226
        local pres = {}
6✔
227
        for k, v in pairs(params) do
6✔
228
            pres[ uri_unescape(k) ] = uri_unescape(v)
×
229
        end
230
        rawset(self, 'post_params', pres)
6✔
231
    end
232

233
    rawset(self, 'post_param', cached_post_param)
8✔
234
    return self:post_param(name)
8✔
235
end
236

237
local function param(self, name)
238
        if name ~= nil then
6✔
239
            local v = self:post_param(name)
×
240
            if v ~= nil then
×
241
                return v
×
242
            end
243
            return self:query_param(name)
×
244
        end
245

246
        local post = self:post_param()
6✔
247
        local query = self:query_param()
6✔
248
        return extend(post, query, false)
6✔
249
end
250

251
local function catfile(...)
252
    local sp = { ... }
38✔
253

254
    local path
255

256
    if #sp == 0 then
38✔
257
        return
×
258
    end
259

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

278
    return path
38✔
279
end
280

281
local response_mt
282
local request_mt
283

284
local function expires_str(str)
285

286
    local now = os.time()
2✔
287
    local gmtnow = now - os.difftime(now, os.time(os.date("!*t", now)))
2✔
288
    local fmt = '%a, %d-%b-%Y %H:%M:%S GMT'
2✔
289

290
    if str == 'now' or str == 0 or str == '0' then
2✔
291
        return os.date(fmt, gmtnow)
×
292
    end
293

294
    local diff, period = string.match(str, '^[+]?(%d+)([hdmy])$')
2✔
295
    if period == nil then
2✔
296
        return str
2✔
297
    end
298

299
    diff = tonumber(diff)
×
300
    if period == 'h' then
×
301
        diff = diff * 3600
×
302
    elseif period == 'd' then
×
303
        diff = diff * 86400
×
304
    elseif period == 'm' then
×
305
        diff = diff * 86400 * 30
×
306
    else
307
        diff = diff * 86400 * 365
×
308
    end
309

310
    return os.date(fmt, gmtnow + diff)
×
311
end
312

313
local function setcookie(resp, cookie, options)
314
    options = options or {}
916✔
315
    local name = cookie.name
916✔
316
    local value = cookie.value
916✔
317

318
    if name == nil then
916✔
319
        error('cookie.name is undefined')
×
320
    end
321
    if value == nil then
916✔
322
        error('cookie.value is undefined')
×
323
    end
324

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

340
    if cookie.expires ~= nil then
916✔
341
        str = sprintf('%s;expires=%s', str, expires_str(cookie.expires))
6✔
342
    end
343

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

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

377
local function url_for_helper(tx, name, args, query)
378
    return tx:url_for(name, args, query)
×
379
end
380

381
local function load_template(self, r, format)
382
    if r.template ~= nil then
18✔
383
        return
×
384
    end
385

386
    if format == nil then
18✔
387
        format = 'html'
×
388
    end
389

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

401
    if self.options.cache_templates then
18✔
402
        if self.cache.tpl[ file ] ~= nil then
18✔
403
            return self.cache.tpl[ file ]
×
404
        end
405
    end
406

407

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

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

420
    fh:close()
18✔
421

422
    if self.options.cache_templates then
18✔
423
        self.cache.tpl[ file ] = template
18✔
424
    end
425
    return template
18✔
426
end
427

428
local function render(tx, opts)
429
    if tx == nil then
46✔
430
        error("Usage: self:render({ ... })")
×
431
    end
432

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

449
        if opts.json ~= nil then
12✔
450
            if tx.httpd.options.charset ~= nil then
2✔
451
                resp.headers['content-type'] =
2✔
452
                    sprintf('application/json; charset=%s',
4✔
453
                        tx.httpd.options.charset
2✔
454
                    )
4✔
455
            else
456
                resp.headers['content-type'] = 'application/json'
×
457
            end
458
            resp.body = json.encode(opts.json)
2✔
459
            return resp
2✔
460
        end
461

462
        if opts.data ~= nil then
10✔
463
            resp.body = tostring(opts.data)
×
464
            return resp
×
465
        end
466

467
        vars = extend(tx.tstash, opts, false)
20✔
468
    end
469

470
    local tpl
471

472
    local format = tx.tstash.format
18✔
473
    if format == nil then
18✔
474
        format = 'html'
×
475
    end
476

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

486
    if type(tpl) == 'function' then
18✔
487
        tpl = tpl()
×
488
    end
489

490
    for hname, sub in pairs(tx.httpd.helpers) do
54✔
491
        vars[hname] = function(...) return sub(tx, ...) end
42✔
492
    end
493
    vars.action = tx.endpoint.action
18✔
494
    vars.controller = tx.endpoint.controller
18✔
495
    vars.format = format
18✔
496

497
    resp.body = lib.template(tpl, vars)
18✔
498
    resp.headers['content-type'] = type_by_format(format)
36✔
499

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

509
local function iterate(_, gen, param, state)
510
    return setmetatable({ body = { gen = gen, param = param, state = state } },
2✔
511
        response_mt)
2✔
512
end
513

514
local function redirect_to(tx, name, args, query)
515
    local location = tx:url_for(name, args, query)
×
516
    return setmetatable({ status = 302, headers = { location = location } },
×
517
        response_mt)
×
518
end
519

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

528
    return tx.tstash[ name ]
×
529
end
530

531
local function url_for_tx(tx, name, args, query)
532
    if name == 'current' then
×
533
        return tx.endpoint:url_for(args, query)
×
534
    end
535
    return tx.httpd:url_for(name, args, query)
×
536
end
537

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

550
local function request_read(req, opts, timeout)
551
    local remaining = req._remaining
108✔
552
    if not remaining then
108✔
553
        remaining = tonumber(req.headers['content-length'])
96✔
554
        if not remaining then
96✔
555
            return ''
84✔
556
        end
557
    end
558

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

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

586
local function request_read_cached(self)
587
    if self.cached_data == nil then
14✔
588
        local data = self:read()
10✔
589
        rawset(self, 'cached_data', data)
10✔
590
        return data
10✔
591
    else
592
        return self.cached_data
4✔
593
    end
594
end
595

596
local function static_file(self, request, format)
597
        local file = catfile(self.options.app_dir, 'public', request.path)
6✔
598

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

609
        local fh, err = fio.open(file, {'O_RDONLY'})
6✔
610
        if err ~= nil then
6✔
611
            return { status = 404 }
4✔
612
        end
613

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

620
        fh:close()
2✔
621

622
        if self.options.cache_static then
2✔
623
            self.cache.static[ file ] = body
2✔
624
        end
625

626
        return {
2✔
627
            status = 200,
628
            headers = {
2✔
629
                [ 'content-type'] = type_by_format(format),
4✔
630
            },
2✔
631
            body = body
2✔
632
        }
2✔
633
end
634

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

655
response_mt = {
2✔
656
    __index = {
2✔
657
        setcookie = setcookie;
2✔
658
    }
2✔
659
}
2✔
660

661
local function is_function(obj)
662
    return type(obj) == 'function'
18✔
663
end
664

665
local function get_request_logger(server_opts, route_opts)
666
    if route_opts and route_opts.endpoint.log_requests ~= nil then
80✔
667
        if is_function(route_opts.endpoint.log_requests) then
12✔
668
            return route_opts.endpoint.log_requests
6✔
669
        elseif route_opts.endpoint.log_requests == false then
×
670
            return log.debug
×
671
        end
672
    end
673

674
    if server_opts.log_requests then
74✔
675
        if is_function(server_opts.log_requests) then
12✔
676
            return server_opts.log_requests
4✔
677
        end
678

679
        return log.info
2✔
680
    end
681

682
    return log.debug
68✔
683
end
684

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

694
    if server_opts.log_errors then
8✔
695
        if is_function(server_opts.log_errors) then
4✔
696
            return server_opts.log_errors
2✔
697
        end
698

699
        return log.error
×
700
    end
701

702
    return log.debug
6✔
703
end
704

705
local function handler(self, request)
706
    if self.hooks.before_dispatch ~= nil then
80✔
707
        self.hooks.before_dispatch(self, request)
×
708
    end
709

710
    local format = 'html'
80✔
711

712
    local pformat = string.match(request.path, '[.]([^.]+)$')
80✔
713
    if pformat ~= nil then
80✔
714
        format = pformat
4✔
715
    end
716

717
    local r = self:match(request.method, request.path)
80✔
718
    if r == nil then
80✔
719
        return static_file(self, request, format)
6✔
720
    end
721

722
    local stash = extend(r.stash, { format = format })
74✔
723

724
    request.endpoint = r.endpoint
74✔
725
    request.tstash   = stash
74✔
726

727
    local resp = r.endpoint.sub(request)
74✔
728
    if self.hooks.after_dispatch ~= nil then
62✔
729
        self.hooks.after_dispatch(request, resp)
×
730
    end
731
    return resp
62✔
732
end
733

734
local function normalize_headers(hdrs)
735
    local res = {}
52✔
736
    for h, v in pairs(hdrs) do
104✔
737
        res[ string.lower(h) ] = v
104✔
738
    end
739
    return res
52✔
740
end
741

742
local function parse_request(req)
743
    local p = lib._parse_request(req)
80✔
744
    if p.error then
80✔
745
        return p
×
746
    end
747
    p.path_raw = p.path
80✔
748
    p.path = uri_unescape(p.path)
160✔
749
    if p.path:sub(1, 1) ~= "/" then
160✔
750
        p.error = "invalid uri"
×
751
        return p
×
752
    end
753
    for _, path_segment in ipairs(p.path:split('/')) do
322✔
754
        if path_segment == "." or path_segment == ".." then
162✔
755
            p.error = "invalid uri"
×
756
            return p
×
757
        end
758
    end
759

760
    return p
80✔
761
end
762

763
local function process_client(self, s, peer)
764
    while true do
765
        local hdrs = ''
86✔
766

767
        local is_eof = false
86✔
768
        while true do
769
            local chunk = s:read({
172✔
770
                delimiter = { "\n\n", "\r\n\r\n" },
86✔
771
            }, self.idle_timeout)
86✔
772

773
            if chunk == '' then
84✔
774
                is_eof = true
2✔
775
                break -- eof
2✔
776
            elseif chunk == nil then
82✔
777
                log.error('failed to read request: %s', errno.strerror())
4✔
778
                return
2✔
779
            end
780

781
            hdrs = hdrs .. chunk
80✔
782

783
            if string.endswith(hdrs, "\n\n") or string.endswith(hdrs, "\r\n\r\n") then
240✔
784
                break
80✔
785
            end
786
        end
787

788
        if is_eof then
82✔
789
            break
2✔
790
        end
791

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

804
        if p.headers['expect'] == '100-continue' then
80✔
805
            s:write('HTTP/1.0 100 Continue\r\n\r\n')
×
806
        end
807

808
        local route = self:match(p.method, p.path)
80✔
809
        local logreq = get_request_logger(self.options, route)
80✔
810
        logreq("%s %s%s", p.method, p.path,
160✔
811
               p.query ~= "" and "?"..p.query or "")
80✔
812

813
        local res, reason = pcall(self.options.handler, self, p)
80✔
814
        p:read() -- skip remaining bytes of request body
80✔
815
        local status, hdrs, body
816

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

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

880
        if hdrs['content-type'] == nil then
80✔
881
            hdrs['content-type'] = 'text/plain; charset=utf-8'
32✔
882
        end
883

884
        if hdrs.server == nil then
80✔
885
            hdrs.server = sprintf('Tarantool http (tarantool v%s)', _TARANTOOL)
160✔
886
        end
887

888
        if p.proto[1] ~= 1 then
80✔
889
            hdrs.connection = 'close'
×
890
        elseif p.broken then
80✔
891
            hdrs.connection = 'close'
×
892
        elseif rawget(p, 'body') == nil then
80✔
893
            hdrs.connection = 'close'
×
894
        elseif p.proto[2] == 1 then
80✔
895
            if p.headers.connection == nil then
80✔
896
                hdrs.connection = 'keep-alive'
×
897
            elseif string.lower(p.headers.connection) ~= 'keep-alive' then
160✔
898
                hdrs.connection = 'close'
72✔
899
            else
900
                hdrs.connection = 'keep-alive'
8✔
901
            end
902
        elseif p.proto[2] == 0 then
×
903
            if p.headers.connection == nil then
×
904
                hdrs.connection = 'close'
×
905
            elseif string.lower(p.headers.connection) == 'keep-alive' then
×
906
                hdrs.connection = 'keep-alive'
×
907
            else
908
                hdrs.connection = 'close'
×
909
            end
910
        end
911

912
        local useragent = p.headers['user-agent']
80✔
913
        if self.disable_keepalive[useragent] == true then
80✔
914
            hdrs.connection = 'close'
2✔
915
        end
916

917
        local response = {
80✔
918
            "HTTP/1.1 ";
919
            status;
80✔
920
            " ";
921
            reason_by_code(status);
160✔
922
            "\r\n";
923
        };
924
        for k, v in pairs(hdrs) do
404✔
925
            if type(v) == 'table' then
324✔
926
                for _, sv in pairs(v) do
8✔
927
                    table.insert(response, sprintf("%s: %s\r\n", ucfirst(k), sv))
12✔
928
                end
929
            else
930
                table.insert(response, sprintf("%s: %s\r\n", ucfirst(k), v))
960✔
931
            end
932
        end
933
        table.insert(response, "\r\n")
80✔
934

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

964
        if p.proto[1] ~= 1 then
80✔
965
            break
966
        end
967

968
        if hdrs.connection ~= 'keep-alive' then
80✔
969
            break
74✔
970
        end
971
    end
972
end
973

974
local function httpd_stop(self)
975
   if type(self) ~= 'table' then
76✔
976
       error("httpd: usage: httpd:stop()")
×
977
    end
978
    if self.is_run then
76✔
979
        self.is_run = false
76✔
980
    else
981
        error("server is already stopped")
×
982
    end
983

984
    if self.tcp_server ~= nil then
76✔
985
        self.tcp_server:close()
76✔
986
        self.tcp_server = nil
76✔
987
    end
988
    return self
76✔
989
end
990

991
local function match_route(self, method, route)
992
    -- route must have '/' at the begin and end
993
    if string.match(route, '.$') ~= '/' then
198✔
994
        route = route .. '/'
168✔
995
    end
996
    if string.match(route, '^.') ~= '/' then
198✔
997
        route = '/' .. route
×
998
    end
999

1000
    method = string.upper(method)
396✔
1001

1002
    local fit
1003
    local stash = {}
198✔
1004

1005
    for _, r in pairs(self.routes) do
2,596✔
1006
        if r.method == method or r.method == 'ANY' then
2,398✔
1007
            local m = { string.match(route, r.match)  }
2,382✔
1008
            local nfit
1009
            if #m > 0 then
2,382✔
1010
                if #r.stash > 0 then
184✔
1011
                    if #r.stash == #m then
36✔
1012
                        nfit = r
36✔
1013
                    end
1014
                else
1015
                    nfit = r
148✔
1016
                end
1017

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

1038
    if fit == nil then
198✔
1039
        return fit
14✔
1040
    end
1041
    local resstash = {}
184✔
1042
    for i = 1, #fit.stash do
226✔
1043
        resstash[ fit.stash[ i ] ] = stash[ i ]
42✔
1044
    end
1045
    return  { endpoint = fit, stash = resstash }
184✔
1046
end
1047

1048
local function set_helper(self, name, sub)
1049
    if sub == nil or type(sub) == 'function' then
66✔
1050
        self.helpers[ name ] = sub
66✔
1051
        return self
66✔
1052
    end
1053
    errorf("Wrong type for helper function: %s", type(sub))
×
1054
end
1055

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

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

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

1089
    if string.match(name, '^/') == nil then
8✔
1090
        return '/' .. name
×
1091
    else
1092
        return name
8✔
1093
    end
1094
end
1095

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

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

1118
    local st, mod = pcall(require, ctx)
6✔
1119
    package.path = ppath
6✔
1120
    package.loaded[ ctx ] = nil
6✔
1121

1122
    if not st then
6✔
1123
        errorf("Can't load module '%s': %s'", ctx, tostring(mod))
2✔
1124
    end
1125

1126
    if type(mod) ~= 'table' then
4✔
1127
        errorf("require '%s' didn't return table", ctx)
×
1128
    end
1129

1130
    if type(mod[ action ]) ~= 'function' then
4✔
1131
        errorf("Controller '%s' doesn't contain function '%s'", ctx, action)
2✔
1132
    end
1133

1134
    if tx.httpd.options.cache_controllers then
2✔
1135
        tx.httpd.cache[ ctx ] = mod
2✔
1136
    end
1137

1138
    return mod[action](tx)
2✔
1139
end
1140

1141
local possible_methods = {
2✔
1142
    GET    = 'GET',
1143
    HEAD   = 'HEAD',
1144
    POST   = 'POST',
1145
    PUT    = 'PUT',
1146
    DELETE = 'DELETE',
1147
    PATCH  = 'PATCH',
1148
}
1149

1150
local function add_route(self, opts, sub)
1151
    if type(opts) ~= 'table' or type(self) ~= 'table' then
914✔
1152
        error("Usage: httpd:route({ ... }, function(cx) ... end)")
×
1153
    end
1154

1155
    opts = extend({method = 'ANY'}, opts, false)
1,828✔
1156

1157
    local ctx
1158
    local action
1159

1160
    if sub == nil then
914✔
1161
        sub = render
140✔
1162
    elseif type(sub) == 'string' then
774✔
1163

1164
        ctx, action = string.match(sub, '(.+)#(.*)')
198✔
1165

1166
        if ctx == nil or action == nil then
198✔
1167
            errorf("Wrong controller format '%s', must be 'module#action'", sub)
×
1168
        end
1169

1170
        sub = ctx_action
198✔
1171

1172
    elseif type(sub) ~= 'function' then
576✔
1173
        errorf("wrong argument: expected function, but received %s",
×
1174
            type(sub))
×
1175
    end
1176

1177
    opts.method = possible_methods[string.upper(opts.method)] or 'ANY'
1,828✔
1178

1179
    if opts.path == nil then
914✔
1180
        error("path is not defined")
×
1181
    end
1182

1183
    opts.controller = ctx
914✔
1184
    opts.action = action
914✔
1185
    opts.match = opts.path
914✔
1186
    opts.match = string.gsub(opts.match, '[-]', "[-]")
914✔
1187

1188
    local estash = {  }
914✔
1189
    local stash = {  }
914✔
1190
    while true do
1191
        local name = string.match(opts.match, ':([%a_][%w_]*)')
1,310✔
1192
        if name == nil then
1,310✔
1193
            break
914✔
1194
        end
1195
        if estash[name] then
396✔
1196
            errorf("duplicate stash: %s", name)
×
1197
        end
1198
        estash[name] = true
396✔
1199
        opts.match = string.gsub(opts.match, ':[%a_][%w_]*', '([^/]-)', 1)
396✔
1200

1201
        table.insert(stash, name)
396✔
1202
    end
1203
    while true do
1204
        local name = string.match(opts.match, '[*]([%a_][%w_]*)')
1,046✔
1205
        if name == nil then
1,046✔
1206
            break
914✔
1207
        end
1208
        if estash[name] then
132✔
1209
            errorf("duplicate stash: %s", name)
×
1210
        end
1211
        estash[name] = true
132✔
1212
        opts.match = string.gsub(opts.match, '[*][%a_][%w_]*', '(.-)', 1)
132✔
1213

1214
        table.insert(stash, name)
132✔
1215
    end
1216

1217
    if string.match(opts.match, '.$') ~= '/' then
914✔
1218
        opts.match = opts.match .. '/'
896✔
1219
    end
1220
    if string.match(opts.match, '^.') ~= '/' then
914✔
1221
        opts.match = '/' .. opts.match
×
1222
    end
1223

1224
    opts.match = '^' .. opts.match .. '$'
914✔
1225

1226
    estash = nil -- luacheck: no unused
914✔
1227

1228
    opts.stash = stash
914✔
1229
    opts.sub = sub
914✔
1230
    opts.url_for = url_for_route
914✔
1231

1232
    if opts.log_requests ~= nil then
914✔
1233
        if type(opts.log_requests) ~= 'function' and type(opts.log_requests) ~= 'boolean' then
10✔
1234
            error("'log_requests' option should be a function or a boolean")
4✔
1235
        end
1236
    end
1237

1238
    if opts.log_errors ~= nil then
910✔
1239
        if type(opts.log_errors) ~= 'function' and type(opts.log_errors) ~= 'boolean' then
10✔
1240
            error("'log_errors' option should be a function or a boolean")
4✔
1241
        end
1242
    end
1243

1244
    if opts.name ~= nil then
906✔
1245
        if opts.name == 'current' then
132✔
1246
            error("Route can not have name 'current'")
×
1247
        end
1248
        if self.iroutes[ opts.name ] ~= nil then
132✔
1249
            errorf("Route with name '%s' is already exists", opts.name)
×
1250
        end
1251
        table.insert(self.routes, opts)
132✔
1252
        self.iroutes[ opts.name ] = #self.routes
132✔
1253
    else
1254
        table.insert(self.routes, opts)
774✔
1255
    end
1256
    return self
906✔
1257
end
1258

1259
local function url_for_httpd(httpd, name, args, query)
1260

1261
    local idx = httpd.iroutes[ name ]
10✔
1262
    if idx ~= nil then
10✔
1263
        return httpd.routes[ idx ]:url_for(args, query)
8✔
1264
    end
1265

1266
    if string.match(name, '^/') == nil then
2✔
1267
        if string.match(name, '^https?://') ~= nil then
2✔
1268
            return name
×
1269
        else
1270
            return '/' .. name
2✔
1271
        end
1272
    else
1273
        return name
×
1274
    end
1275
end
1276

1277
local function httpd_start(self)
1278
    if type(self) ~= 'table' then
76✔
1279
        error("httpd: usage: httpd:start()")
×
1280
    end
1281

1282
    local server = self.tcp_server_f(self.host, self.port, {
152✔
1283
        name = 'http',
1284
        handler = function(...)
1285
            self.internal.preprocess_client_handler()
80✔
1286
            process_client(self, ...)
80✔
1287
            self.internal.postprocess_client_handler()
78✔
1288
        end,
1289
        http_server = self,
76✔
1290
    })
1291

1292
    if server == nil then
76✔
1293
        error(sprintf("Can't create tcp_server: %s", errno.strerror()))
×
1294
    end
1295

1296
    rawset(self, 'is_run', true)
76✔
1297
    rawset(self, 'tcp_server', server)
76✔
1298
    rawset(self, 'stop', httpd_stop)
76✔
1299

1300
    return self
76✔
1301
end
1302

1303
local exports = {
2✔
1304
    _VERSION = require('http.version'),
2✔
1305
    DETACHED = DETACHED,
2✔
1306

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

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

1339
        local self = {
78✔
1340
            host    = host,
78✔
1341
            port    = port,
78✔
1342
            is_run  = false,
1343
            stop    = httpd_stop,
78✔
1344
            start   = httpd_start,
78✔
1345
            options = extend(default, options, true),
156✔
1346

1347
            routes  = {  },
78✔
1348
            iroutes = {  },
78✔
1349
            helpers = {
78✔
1350
                url_for = url_for_helper,
78✔
1351
            },
78✔
1352
            hooks   = {  },
78✔
1353

1354
            -- methods
1355
            route   = add_route,
78✔
1356
            match   = match_route,
78✔
1357
            helper  = set_helper,
78✔
1358
            hook    = set_hook,
78✔
1359
            url_for = url_for_httpd,
78✔
1360

1361
            -- Exposed to make it replaceable by a user.
1362
            tcp_server_f = socket.tcp_server,
78✔
1363

1364
            -- caches
1365
            cache   = {
78✔
1366
                tpl         = {},
78✔
1367
                ctx         = {},
78✔
1368
                static      = {},
78✔
1369
            },
78✔
1370

1371
            disable_keepalive   = tomap(disable_keepalive),
156✔
1372
            idle_timeout        = options.idle_timeout,
78✔
1373

1374
            internal = {
78✔
1375
                preprocess_client_handler = function() end,
150✔
1376
                postprocess_client_handler = function() end,
150✔
1377
            }
78✔
1378
        }
1379

1380
        return self
78✔
1381
    end,
1382

1383
    internal = {
2✔
1384
        response_mt = response_mt,
2✔
1385
        request_mt = request_mt,
2✔
1386
        extend = extend,
2✔
1387
    }
2✔
1388
}
1389

1390
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