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

tarantool / http / 11781220690

11 Nov 2024 03:13PM UTC coverage: 78.435% (+0.09%) from 78.344%
11781220690

push

github

themilchenko
ci: fix tests for ssl

There was errors with SSL options for several versions of Tarantool.

After the patch building of Tarantool was changed on dynamic one in CI.

862 of 1099 relevant lines covered (78.43%)

72.13 hits per line

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

79.19
/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, ...))
16✔
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 = {}
56✔
31
    for _, v in pairs(tbl) do
114✔
32
        map[v] = true
2✔
33
    end
34
    return map
56✔
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 = {}
828✔
110
    for k, v in pairs(tbl) do
3,213✔
111
        res[ k ] = v
1,557✔
112
    end
113
    for k, v in pairs(tblu) do
2,888✔
114
        if raise then
1,232✔
115
            if res[ k ] == nil then
×
116
                errorf("Unknown option '%s'", k)
×
117
            end
118
        end
119
        res[ k ] = v
1,232✔
120
    end
121
    return res
828✔
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
55✔
779
                is_eof = true
2✔
780
                break -- eof
2✔
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
51✔
794
            break
2✔
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
51✔
981
       error("httpd: usage: httpd:stop()")
×
982
    end
983
    if self.is_run then
51✔
984
        self.is_run = false
51✔
985
    else
986
        error("server is already stopped")
×
987
    end
988

989
    if self.tcp_server ~= nil then
51✔
990
        self.tcp_server:close()
51✔
991
        self.tcp_server = nil
51✔
992
    end
993
    return self
51✔
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
55✔
1349
        error("httpd: usage: httpd:start()")
×
1350
    end
1351

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

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

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

1370
    return self
51✔
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
68✔
1377

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

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

1386
            if string.find(key, 'file') ~= nil and fio.path.exists(value) ~= true then
125✔
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
58✔
1393
        error("ssl_key_file and ssl_cert_file must be set to enable TLS")
2✔
1394
    end
1395

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

1400
    return is_tls_enabled
56✔
1401
end
1402

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

1407
    new = function(host, port, options)
1408
        if options == nil then
68✔
1409
            options = {}
3✔
1410
        end
1411
        if type(options) ~= 'table' then
68✔
1412
            errorf("options must be table not '%s'", type(options))
×
1413
        end
1414
        local disable_keepalive = options.disable_keepalive or {}
68✔
1415
        if type(disable_keepalive) ~= 'table' then
68✔
1416
            error('Option disable_keepalive must be a table.')
×
1417
        end
1418
        if options.idle_timeout ~= nil and
68✔
1419
           type(options.idle_timeout) ~= 'number' then
1✔
1420
            error('Option idle_timeout must be a number.')
×
1421
        end
1422

1423
        local is_tls_enabled = validate_ssl_opts({
136✔
1424
            ssl_cert_file = options.ssl_cert_file,
68✔
1425
            ssl_key_file = options.ssl_key_file,
68✔
1426
            ssl_password = options.ssl_password,
68✔
1427
            ssl_password_file = options.ssl_password_file,
68✔
1428
            ssl_ca_file = options.ssl_ca_file,
68✔
1429
            ssl_ciphers = options.ssl_ciphers,
68✔
1430
        })
1431

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

1448
        local self = {
56✔
1449
            host    = host,
56✔
1450
            port    = port,
56✔
1451
            is_run  = false,
1452
            stop    = httpd_stop,
56✔
1453
            start   = httpd_start,
56✔
1454
            use_tls = is_tls_enabled,
56✔
1455
            options = extend(default, options, false),
112✔
1456

1457
            routes  = {  },
56✔
1458
            iroutes = {  },
56✔
1459
            helpers = {
56✔
1460
                url_for = url_for_helper,
56✔
1461
            },
56✔
1462
            hooks   = {  },
56✔
1463

1464
            -- methods
1465
            route   = add_route,
56✔
1466
            delete  = delete_route,
56✔
1467
            match   = match_route,
56✔
1468
            helper  = set_helper,
56✔
1469
            hook    = set_hook,
56✔
1470
            url_for = url_for_httpd,
56✔
1471

1472
            -- Exposed to make it replaceable by a user.
1473
            tcp_server_f = socket.tcp_server,
56✔
1474

1475
            -- caches
1476
            cache   = {
56✔
1477
                tpl         = {},
56✔
1478
                ctx         = {},
56✔
1479
                static      = {},
56✔
1480
            },
56✔
1481

1482
            disable_keepalive   = tomap(disable_keepalive),
112✔
1483
            idle_timeout        = options.idle_timeout,
56✔
1484

1485
            internal = {
56✔
1486
                preprocess_client_handler = function() end,
104✔
1487
                postprocess_client_handler = function() end,
104✔
1488
            }
56✔
1489
        }
1490

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

1505
        return self
56✔
1506
    end,
1507

1508
    internal = {
1✔
1509
        response_mt = response_mt,
1✔
1510
        request_mt = request_mt,
1✔
1511
        extend = extend,
1✔
1512
    }
1✔
1513
}
1514

1515
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