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

tarantool / http / 15923841339

27 Jun 2025 10:12AM UTC coverage: 78.617% (-0.1%) from 78.747%
15923841339

push

github

themilchenko
roles: change server address after reload

If the `listen` value was changed the server won't update it after
config reload.

After the patch this bug was fixed.

Closes #209

864 of 1099 relevant lines covered (78.62%)

73.74 hits per line

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

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

3
local lib = require('http.lib')
1✔
4
local sslsocket_supported, sslsocket = pcall(require, 'http.sslsocket')
1✔
5

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

12
local log = require('log')
1✔
13
local socket = require('socket')
1✔
14
local json = require('json')
1✔
15
local errno = require 'errno'
1✔
16

17
local DETACHED = 101
1✔
18

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

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

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

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

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

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

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

59
local function escape_string(str, byte_filter)
60
    local result = {}
299✔
61
    for i = 1, str:len() do
1,702✔
62
        local char = str:sub(i,i)
1,104✔
63
        if byte_filter(string.byte(char)) then
2,208✔
64
            result[i] = char
1,090✔
65
        else
66
            result[i] = escape_char(char)
28✔
67
        end
68
    end
69
    return table.concat(result)
299✔
70
end
71

72
local function escape_value(cookie_value)
73
    return escape_string(cookie_value, valid_cookie_value_byte)
199✔
74
end
75

76
local function escape_path(cookie_path)
77
    return escape_string(cookie_path, valid_cookie_path_byte)
100✔
78
end
79

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

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

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

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

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

129
    local t = mime_types[ fmt ]
16✔
130

131
    if t ~= nil then
16✔
132
        return t
16✔
133
    end
134

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

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

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

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

157
local function cached_post_param(self, name)
158
    if name == nil then
5✔
159
        return self.post_params
4✔
160
    end
161
    return self.post_params[ name ]
1✔
162
end
163

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

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

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

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

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

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

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

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

211
local function post_param(self, name)
212
    local body = self:read_cached()
5✔
213

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

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

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

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

256
local function catfile(...)
257
    local sp = { ... }
26✔
258

259
    local path
260

261
    if #sp == 0 then
26✔
262
        return
×
263
    end
264

265
    for _, pe in pairs(sp) do
129✔
266
        if path == nil then
77✔
267
            path = pe
26✔
268
        elseif string.match(path, '.$') ~= '/' then
51✔
269
            if string.match(pe, '^.') ~= '/' then
51✔
270
                path = path .. '/' .. pe
47✔
271
            else
272
                path = path .. pe
4✔
273
            end
274
        else
275
            if string.match(pe, '^.') == '/' then
×
276
                path = path .. string.gsub(pe, '^/', '', 1)
×
277
            else
278
                path = path .. pe
×
279
            end
280
        end
281
    end
282

283
    return path
26✔
284
end
285

286
local response_mt
287
local request_mt
288

289
local function expires_str(str)
290

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

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

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

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

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

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

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

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

345
    if cookie.expires ~= nil then
458✔
346
        str = sprintf('%s;expires=%s', str, expires_str(cookie.expires))
3✔
347
    end
348

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

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

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

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

391
    if format == nil then
15✔
392
        format = 'html'
×
393
    end
394

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

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

412

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

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

425
    fh:close()
15✔
426

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

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

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

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

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

472
        vars = extend(tx.tstash, opts, false)
22✔
473
    end
474

475
    local tpl
476

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

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

491
    if type(tpl) == 'function' then
15✔
492
        tpl = tpl()
×
493
    end
494

495
    for hname, sub in pairs(tx.httpd.helpers) do
60✔
496
        vars[hname] = function(...) return sub(tx, ...) end
33✔
497
    end
498
    vars.action = tx.endpoint.action
15✔
499
    vars.controller = tx.endpoint.controller
15✔
500
    vars.format = format
15✔
501

502
    resp.body = lib.template(tpl, vars)
15✔
503
    resp.headers['content-type'] = type_by_format(format)
30✔
504

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

625
        fh:close()
1✔
626

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

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

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

660
response_mt = {
1✔
661
    __index = {
1✔
662
        setcookie = setcookie;
1✔
663
    }
1✔
664
}
1✔
665

666
local function is_function(obj)
667
    return type(obj) == 'function'
9✔
668
end
669

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

679
    if server_opts.log_requests then
46✔
680
        if is_function(server_opts.log_requests) then
6✔
681
            return server_opts.log_requests
2✔
682
        end
683

684
        return log.info
1✔
685
    end
686

687
    return log.debug
43✔
688
end
689

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

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

704
        return log.error
×
705
    end
706

707
    return log.debug
3✔
708
end
709

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

715
    local format = 'html'
49✔
716

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

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

727
    local stash = extend(r.stash, { format = format })
45✔
728

729
    request.endpoint = r.endpoint
45✔
730
    request.tstash   = stash
45✔
731

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

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

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

765
    return p
49✔
766
end
767

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

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

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

786
            hdrs = hdrs .. chunk
49✔
787

788
            if string.endswith(hdrs, "\n\n") or string.endswith(hdrs, "\r\n\r\n") then
147✔
789
                break
49✔
790
            end
791
        end
792

793
        if is_eof then
49✔
794
            break
795
        end
796

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

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

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

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

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

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

885
        if hdrs['content-type'] == nil then
49✔
886
            hdrs['content-type'] = 'text/plain; charset=utf-8'
18✔
887
        end
888

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

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

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

922
        local response = {
49✔
923
            "HTTP/1.1 ";
924
            status;
49✔
925
            " ";
926
            reason_by_code(status);
98✔
927
            "\r\n";
928
        };
929
        for k, v in pairs(hdrs) do
296✔
930
            if type(v) == 'table' then
198✔
931
                for _, sv in pairs(v) do
6✔
932
                    table.insert(response, sprintf("%s: %s\r\n", ucfirst(k), sv))
6✔
933
                end
934
            else
935
                table.insert(response, sprintf("%s: %s\r\n", ucfirst(k), v))
588✔
936
            end
937
        end
938
        table.insert(response, "\r\n")
49✔
939

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

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

973
        if hdrs.connection ~= 'keep-alive' then
49✔
974
            break
46✔
975
        end
976
    end
977
end
978

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

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

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

1005
    method = string.upper(method)
230✔
1006

1007
    local fit
1008
    local stash = {}
115✔
1009

1010
    for _, r in pairs(self.routes) do
1,643✔
1011
        if r.method == method or r.method == 'ANY' then
1,413✔
1012
            local m = { string.match(route, r.match)  }
1,405✔
1013
            local nfit
1014
            if #m > 0 then
1,405✔
1015
                if #r.stash > 0 then
106✔
1016
                    if #r.stash == #m then
16✔
1017
                        nfit = r
16✔
1018
                    end
1019
                else
1020
                    nfit = r
90✔
1021
                end
1022

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

1043
    if fit == nil then
115✔
1044
        return fit
9✔
1045
    end
1046
    local resstash = {}
106✔
1047
    for i = 1, #fit.stash do
125✔
1048
        resstash[ fit.stash[ i ] ] = stash[ i ]
19✔
1049
    end
1050
    return  { endpoint = fit, stash = resstash }
106✔
1051
end
1052

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

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

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

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

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

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

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

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

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

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

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

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

1143
    return mod[action](tx)
1✔
1144
end
1145

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

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

1160
    opts = extend({method = 'ANY'}, opts, false)
1,310✔
1161

1162
    local ctx
1163
    local action
1164

1165
    if sub == nil then
655✔
1166
        sub = render
100✔
1167
    elseif type(sub) == 'string' then
555✔
1168

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

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

1175
        sub = ctx_action
144✔
1176

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

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

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

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

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

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

1219
        table.insert(stash, name)
97✔
1220
    end
1221

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

1229
    opts.match = '^' .. opts.match .. '$'
655✔
1230

1231
    estash = nil -- luacheck: no unused
655✔
1232

1233
    opts.stash = stash
655✔
1234
    opts.sub = sub
655✔
1235
    opts.url_for = url_for_route
655✔
1236

1237
    if opts.log_requests ~= nil then
655✔
1238
        if type(opts.log_requests) ~= 'function' and type(opts.log_requests) ~= 'boolean' then
5✔
1239
            error("'log_requests' option should be a function or a boolean")
2✔
1240
        end
1241
    end
1242

1243
    if opts.log_errors ~= nil then
653✔
1244
        if type(opts.log_errors) ~= 'function' and type(opts.log_errors) ~= 'boolean' then
5✔
1245
            error("'log_errors' option should be a function or a boolean")
2✔
1246
        end
1247
    end
1248

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

1264
local function delete_route(self, name)
1265
    local route = self.iroutes[name]
1✔
1266
    if route == nil then
1✔
1267
        return
×
1268
    end
1269

1270
    self.iroutes[name] = nil
1✔
1271
    table.remove(self.routes, route)
1✔
1272

1273
    -- Update iroutes numeration.
1274
    for n, r in ipairs(self.routes) do
14✔
1275
        if r.name then
13✔
1276
            self.iroutes[r.name] = n
2✔
1277
        end
1278
    end
1279
end
1280

1281
local function url_for_httpd(httpd, name, args, query)
1282

1283
    local idx = httpd.iroutes[ name ]
5✔
1284
    if idx ~= nil then
5✔
1285
        return httpd.routes[ idx ]:url_for(args, query)
4✔
1286
    end
1287

1288
    if string.match(name, '^/') == nil then
1✔
1289
        if string.match(name, '^https?://') ~= nil then
1✔
1290
            return name
×
1291
        else
1292
            return '/' .. name
1✔
1293
        end
1294
    else
1295
        return name
×
1296
    end
1297
end
1298

1299
local function create_ssl_ctx(host, port, opts)
1300
    local ok, ctx = pcall(sslsocket.ctx, sslsocket.tls_server_method())
26✔
1301
    if ok ~= true then
13✔
1302
        error(ctx)
×
1303
    end
1304

1305
    local rc = sslsocket.ctx_use_private_key_file(ctx, opts.ssl_key_file,
26✔
1306
        opts.ssl_password, opts.ssl_password_file)
13✔
1307
    if rc == false then
13✔
1308
        errorf(
4✔
1309
            "Can't start server on %s:%s: %s %s",
2✔
1310
            host, port, 'Private key is invalid or password mismatch', opts.ssl_key_file
2✔
1311
        )
2✔
1312
    end
1313

1314
    rc = sslsocket.ctx_use_certificate_file(ctx, opts.ssl_cert_file)
22✔
1315
    if rc == false then
11✔
1316
        errorf(
×
1317
            "Can't start server on %s:%s: %s %s",
1318
            host, port, 'Certificate is invalid', opts.ssl_cert_file
×
1319
        )
1320
    end
1321

1322
    if opts.ssl_ca_file ~= nil then
11✔
1323
        rc = sslsocket.ctx_load_verify_locations(ctx, opts.ssl_ca_file)
14✔
1324
        if rc == false then
7✔
1325
            errorf(
2✔
1326
                "Can't start server on %s:%s: %s",
1✔
1327
                host, port, 'CA file is invalid'
1✔
1328
            )
1✔
1329
        end
1330

1331
        sslsocket.ctx_set_verify(ctx, 0x01 + 0x02)
6✔
1332
    end
1333

1334
    if opts.ssl_ciphers ~= nil then
10✔
1335
        rc = sslsocket.ctx_set_cipher_list(ctx, opts.ssl_ciphers)
4✔
1336
        if rc == false then
2✔
1337
            errorf(
2✔
1338
                "Can't start server on %s:%s: %s",
1✔
1339
                host, port, 'Ciphers are invalid'
1✔
1340
            )
1✔
1341
        end
1342
    end
1343

1344
    return ctx
9✔
1345
end
1346

1347
local function httpd_start(self)
1348
    if type(self) ~= 'table' then
60✔
1349
        error("httpd: usage: httpd:start()")
×
1350
    end
1351

1352
    local server = self.tcp_server_f(self.host, self.port, {
120✔
1353
        name = 'http',
1354
        handler = function(...)
1355
            self.internal.preprocess_client_handler()
52✔
1356
            process_client(self, ...)
52✔
1357
            self.internal.postprocess_client_handler()
50✔
1358
        end,
1359
        http_server = self,
60✔
1360
    })
1361

1362
    if server == nil then
56✔
1363
        error(sprintf("Can't create tcp_server: %s", errno.strerror()))
×
1364
    end
1365

1366
    rawset(self, 'is_run', true)
56✔
1367
    rawset(self, 'tcp_server', server)
56✔
1368
    rawset(self, 'stop', httpd_stop)
56✔
1369

1370
    return self
56✔
1371
end
1372

1373
-- validate_ssl_opts validates ssl_opts and returns true if at least ssl_cert_file
1374
-- and ssl_key_file parameters are not nil.
1375
local function validate_ssl_opts(opts)
1376
    local is_tls_enabled = false
91✔
1377

1378
    for key, value in pairs(opts) do
252✔
1379
        if value ~= nil then
86✔
1380
            is_tls_enabled = true
86✔
1381

1382
            if type(value) ~= 'string' then
86✔
1383
                errorf("%s option must be a string", key)
12✔
1384
            end
1385

1386
            if string.find(key, 'file') ~= nil and fio.path.exists(value) ~= true then
137✔
1387
                errorf("file %q not exists", value)
4✔
1388
            end
1389
        end
1390
    end
1391

1392
    if is_tls_enabled and (opts.ssl_key_file == nil or opts.ssl_cert_file == nil) then
75✔
1393
        error("ssl_key_file and ssl_cert_file must be set to enable TLS")
3✔
1394
    end
1395

1396
    if is_tls_enabled and not sslsocket_supported then
72✔
1397
        error("ssl socket is not supported")
×
1398
    end
1399

1400
    return is_tls_enabled
72✔
1401
end
1402

1403
local exports = {
1✔
1404
    _VERSION = require('http.version'),
2✔
1405
    DETACHED = DETACHED,
1✔
1406

1407
    -- Since TLS support this function uses in roles's validate section to check
1408
    -- TLS options.
1409
    new = function(host, port, options)
1410
        if options == nil then
91✔
1411
            options = {}
1✔
1412
        end
1413
        if type(options) ~= 'table' then
91✔
1414
            errorf("options must be table not '%s'", type(options))
×
1415
        end
1416
        local disable_keepalive = options.disable_keepalive or {}
91✔
1417
        if type(disable_keepalive) ~= 'table' then
91✔
1418
            error('Option disable_keepalive must be a table.')
×
1419
        end
1420
        if options.idle_timeout ~= nil and
91✔
1421
           type(options.idle_timeout) ~= 'number' then
1✔
1422
            error('Option idle_timeout must be a number.')
×
1423
        end
1424

1425
        local is_tls_enabled = validate_ssl_opts({
182✔
1426
            ssl_cert_file = options.ssl_cert_file,
91✔
1427
            ssl_key_file = options.ssl_key_file,
91✔
1428
            ssl_password = options.ssl_password,
91✔
1429
            ssl_password_file = options.ssl_password_file,
91✔
1430
            ssl_ca_file = options.ssl_ca_file,
91✔
1431
            ssl_ciphers = options.ssl_ciphers,
91✔
1432
        })
1433

1434
        local default = {
72✔
1435
            max_header_size     = 4096,
1436
            header_timeout      = 100,
1437
            handler             = handler,
72✔
1438
            app_dir             = '.',
1439
            charset             = 'utf-8',
1440
            cache_templates     = true,
1441
            cache_controllers   = true,
1442
            cache_static        = true,
1443
            log_requests        = true,
1444
            log_errors          = true,
1445
            display_errors      = false,
1446
            disable_keepalive   = {},
72✔
1447
            idle_timeout        = 0, -- no timeout, option is disabled
1448
        }
1449

1450
        local self = {
72✔
1451
            host    = host,
72✔
1452
            port    = port,
72✔
1453
            is_run  = false,
1454
            stop    = httpd_stop,
72✔
1455
            start   = httpd_start,
72✔
1456
            use_tls = is_tls_enabled,
72✔
1457
            options = extend(default, options, false),
144✔
1458

1459
            routes  = {  },
72✔
1460
            iroutes = {  },
72✔
1461
            helpers = {
72✔
1462
                url_for = url_for_helper,
72✔
1463
            },
72✔
1464
            hooks   = {  },
72✔
1465

1466
            -- methods
1467
            route   = add_route,
72✔
1468
            delete  = delete_route,
72✔
1469
            match   = match_route,
72✔
1470
            helper  = set_helper,
72✔
1471
            hook    = set_hook,
72✔
1472
            url_for = url_for_httpd,
72✔
1473

1474
            -- Exposed to make it replaceable by a user.
1475
            tcp_server_f = socket.tcp_server,
72✔
1476

1477
            -- caches
1478
            cache   = {
72✔
1479
                tpl         = {},
72✔
1480
                ctx         = {},
72✔
1481
                static      = {},
72✔
1482
            },
72✔
1483

1484
            disable_keepalive   = tomap(disable_keepalive),
144✔
1485
            idle_timeout        = options.idle_timeout,
72✔
1486

1487
            internal = {
72✔
1488
                preprocess_client_handler = function() end,
120✔
1489
                postprocess_client_handler = function() end,
120✔
1490
            }
72✔
1491
        }
1492

1493
        if self.use_tls then
72✔
1494
            self.tcp_server_f = function(host, port, handler, timeout)
1495
                local ssl_ctx = create_ssl_ctx(host, port, {
26✔
1496
                    ssl_cert_file = self.options.ssl_cert_file,
13✔
1497
                    ssl_key_file = self.options.ssl_key_file,
13✔
1498
                    ssl_password = self.options.ssl_password,
13✔
1499
                    ssl_password_file = self.options.ssl_password_file,
13✔
1500
                    ssl_ca_file = self.options.ssl_ca_file,
13✔
1501
                    ssl_ciphers = self.options.ssl_ciphers,
13✔
1502
                })
1503
                return sslsocket.tcp_server(host, port, handler, timeout, ssl_ctx)
9✔
1504
            end
1505
        end
1506

1507
        return self
72✔
1508
    end,
1509

1510
    internal = {
1✔
1511
        response_mt = response_mt,
1✔
1512
        request_mt = request_mt,
1✔
1513
        extend = extend,
1✔
1514
    }
1✔
1515
}
1516

1517
return exports
1✔
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