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

tarantool / http / 11743392950

08 Nov 2024 01:52PM UTC coverage: 79.107% (+0.8%) from 78.344%
11743392950

push

github

web-flow
Merge 47576bf4e into 0e7af5a7d

273 of 336 new or added lines in 2 files covered. (81.25%)

1 existing line in 1 file now uncovered.

886 of 1120 relevant lines covered (79.11%)

71.86 hits per line

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

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

3
local lib = require('http.lib')
1✔
4
local _, 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 ffi = require('ffi')
1✔
14
local socket = require('socket')
1✔
15
local json = require('json')
1✔
16
local errno = require 'errno'
1✔
17

18
local DETACHED = 101
1✔
19

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

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

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

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

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

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

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

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

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

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

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

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

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

109
local function extend(tbl, tblu, raise)
110
    local res = {}
858✔
111
    for k, v in pairs(tbl) do
3,447✔
112
        res[ k ] = v
1,731✔
113
    end
114
    for k, v in pairs(tblu) do
3,022✔
115
        if raise then
1,306✔
UNCOV
116
            if res[ k ] == nil then
×
117
                errorf("Unknown option '%s'", k)
×
118
            end
119
        end
120
        res[ k ] = v
1,306✔
121
    end
122
    return res
858✔
123
end
124

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

130
    local t = mime_types[ fmt ]
16✔
131

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

260
    local path
261

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

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

284
    return path
26✔
285
end
286

287
local response_mt
288
local request_mt
289

290
local function expires_str(str)
291

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

413

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

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

426
    fh:close()
15✔
427

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

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

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

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

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

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

476
    local tpl
477

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

626
        fh:close()
1✔
627

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

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

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

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

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

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

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

685
        return log.info
1✔
686
    end
687

688
    return log.debug
43✔
689
end
690

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

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

705
        return log.error
×
706
    end
707

708
    return log.debug
3✔
709
end
710

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

716
    local format = 'html'
49✔
717

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

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

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

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

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

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

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

766
    return p
49✔
767
end
768

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

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

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

787
            hdrs = hdrs .. chunk
49✔
788

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

794
        if is_eof then
50✔
795
            break
1✔
796
        end
797

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

1161
    opts = extend({method = 'ANY'}, opts, false)
1,362✔
1162

1163
    local ctx
1164
    local action
1165

1166
    if sub == nil then
681✔
1167
        sub = render
104✔
1168
    elseif type(sub) == 'string' then
577✔
1169

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

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

1176
        sub = ctx_action
150✔
1177

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

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

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

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

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

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

1220
        table.insert(stash, name)
101✔
1221
    end
1222

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

1230
    opts.match = '^' .. opts.match .. '$'
681✔
1231

1232
    estash = nil -- luacheck: no unused
681✔
1233

1234
    opts.stash = stash
681✔
1235
    opts.sub = sub
681✔
1236
    opts.url_for = url_for_route
681✔
1237

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

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

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

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

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

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

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

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

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

1300
local function enable_tls(host, port, ssl_opts)
1301
    local ok, ctx = pcall(sslsocket.ctx, ffi.C.TLS_server_method())
15✔
1302
    if ok ~= true then
15✔
NEW
1303
        return nil, error(ctx)
×
1304
    end
1305

1306
    local rc = sslsocket.ctx_use_private_key_file(ctx, ssl_opts.ssl_key_file,
30✔
1307
        ssl_opts.ssl_password, ssl_opts.ssl_password_file)
15✔
1308
    if rc == false then
15✔
1309
        errorf(
6✔
1310
            "Can't start server on %s:%s: %s %s",
3✔
1311
            host, port, 'Private key is invalid or password mismatch', ssl_opts.ssl_key_file
3✔
1312
        )
3✔
1313
    end
1314

1315
    rc = sslsocket.ctx_use_certificate_file(ctx, ssl_opts.ssl_cert_file)
24✔
1316
    if rc == false then
12✔
1317
        errorf(
2✔
1318
            "Can't start server on %s:%s: %s %s",
1✔
1319
            host, port, 'Certificate is invalid', ssl_opts.ssl_cert_file
1✔
1320
        )
1✔
1321
    end
1322

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

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

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

1345
    return ctx
9✔
1346
end
1347

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

1353
    local ssl_ctx
1354
    if self.options.use_tls then
57✔
1355
        ssl_ctx = enable_tls(self.host, self.port, {
30✔
1356
            ssl_cert_file = self.options.ssl_cert_file,
15✔
1357
            ssl_key_file = self.options.ssl_key_file,
15✔
1358
            ssl_password = self.options.ssl_password,
15✔
1359
            ssl_password_file = self.options.ssl_password_file,
15✔
1360
            ssl_ca_file = self.options.ssl_ca_file,
15✔
1361
            ssl_ciphers = self.options.ssl_ciphers,
15✔
1362
        })
9✔
1363
        self.tcp_server_f = sslsocket.tcp_server
9✔
1364
    end
1365

1366
    local server = self.tcp_server_f(self.host, self.port, {
102✔
1367
        name = 'http',
1368
        handler = function(...)
1369
            self.internal.preprocess_client_handler()
52✔
1370
            process_client(self, ...)
52✔
1371
            self.internal.postprocess_client_handler()
51✔
1372
        end,
1373
        http_server = self,
51✔
1374
    }, self.options.socket_timeout, ssl_ctx)
51✔
1375

1376
    if server == nil then
51✔
1377
        error(sprintf("Can't create tcp_server: %s", errno.strerror()))
×
1378
    end
1379

1380
    rawset(self, 'is_run', true)
51✔
1381
    rawset(self, 'tcp_server', server)
51✔
1382
    rawset(self, 'stop', httpd_stop)
51✔
1383

1384
    return self
51✔
1385
end
1386

1387
local function validate_ssl_opts(ssl_opts)
1388
    if ssl_opts.ssl_cert_file ~= nil then
25✔
1389
        if type(ssl_opts.ssl_cert_file) ~= 'string' then
24✔
1390
            error("ssl_cert_file option must be a string")
1✔
1391
        end
1392
        if fio.path.exists(ssl_opts.ssl_cert_file) ~= true then
46✔
1393
            errorf("file %q not exists", ssl_opts.ssl_cert_file)
1✔
1394
        end
1395
    end
1396

1397
    if ssl_opts.ssl_key_file ~= nil then
23✔
1398
        if type(ssl_opts.ssl_key_file) ~= 'string' then
22✔
1399
            error("ssl_key_file option must be a string")
1✔
1400
        end
1401
        if fio.path.exists(ssl_opts.ssl_key_file) ~= true then
42✔
1402
            errorf("file %q not exists", ssl_opts.ssl_key_file)
1✔
1403
        end
1404
    end
1405

1406
    if ssl_opts.ssl_password ~= nil then
21✔
1407
        if type(ssl_opts.ssl_password) ~= 'string' then
8✔
1408
            error("ssl_password option must be a string")
1✔
1409
        end
1410
    end
1411

1412
    if ssl_opts.ssl_password_file then
20✔
1413
        if type(ssl_opts.ssl_password_file) ~= 'string' then
7✔
1414
            error("ssl_password_file option must be a string")
1✔
1415
        end
1416
        if fio.path.exists(ssl_opts.ssl_password_file) ~= true then
12✔
1417
            errorf("file %q not exists", ssl_opts.ssl_password_file)
1✔
1418
        end
1419
    end
1420

1421
    if ssl_opts.ssl_ca_file ~= nil then
18✔
1422
        if type(ssl_opts.ssl_ca_file) ~= 'string' then
10✔
1423
            error("ssl_ca_file option must be a string")
1✔
1424
        end
1425
        if fio.path.exists(ssl_opts.ssl_ca_file) ~= true then
18✔
1426
            errorf("file %q not exists", ssl_opts.ssl_ca_file)
1✔
1427
        end
1428
    end
1429

1430
    if ssl_opts.ssl_ciphers ~= nil then
16✔
1431
        if type(ssl_opts.ssl_ciphers) ~= 'string' then
3✔
1432
            error("ssl_ciphers option must be a string")
1✔
1433
        end
1434
    end
1435
end
1436

1437
local exports = {
1✔
1438
    _VERSION = require('http.version'),
2✔
1439
    DETACHED = DETACHED,
1✔
1440

1441
    new = function(host, port, options)
1442
        if options == nil then
69✔
1443
            options = {}
3✔
1444
        end
1445
        if type(options) ~= 'table' then
69✔
1446
            errorf("options must be table not '%s'", type(options))
×
1447
        end
1448
        local disable_keepalive = options.disable_keepalive or {}
69✔
1449
        if type(disable_keepalive) ~= 'table' then
69✔
1450
            error('Option disable_keepalive must be a table.')
×
1451
        end
1452
        if options.idle_timeout ~= nil and
69✔
1453
           type(options.idle_timeout) ~= 'number' then
1✔
1454
            error('Option idle_timeout must be a number.')
×
1455
        end
1456
        if options.socket_timeout ~= nil and type(options.socket_timeout) ~= 'number' then
69✔
1457
            error('Option socket_timeout must be a number')
1✔
1458
        end
1459

1460
        if options.use_tls == true then
68✔
1461
            validate_ssl_opts({
50✔
1462
                ssl_cert_file = options.ssl_cert_file,
25✔
1463
                ssl_key_file = options.ssl_key_file,
25✔
1464
                ssl_password = options.ssl_password,
25✔
1465
                ssl_password_file = options.ssl_password_file,
25✔
1466
                ssl_ca_file = options.ssl_ca_file,
25✔
1467
                ssl_ciphers = options.ssl_ciphers,
25✔
1468
            })
1469
        end
1470

1471
        local default = {
58✔
1472
            max_header_size     = 4096,
1473
            header_timeout      = 100,
1474
            handler             = handler,
58✔
1475
            app_dir             = '.',
1476
            charset             = 'utf-8',
1477
            cache_templates     = true,
1478
            cache_controllers   = true,
1479
            cache_static        = true,
1480
            log_requests        = true,
1481
            log_errors          = true,
1482
            display_errors      = false,
1483
            disable_keepalive   = {},
58✔
1484
            idle_timeout        = 0, -- no timeout, option is disabled
1485
            socket_timeout      = 60,
1486
            use_tls             = false,
1487
        }
1488

1489
        local self = {
58✔
1490
            host    = host,
58✔
1491
            port    = port,
58✔
1492
            is_run  = false,
1493
            stop    = httpd_stop,
58✔
1494
            start   = httpd_start,
58✔
1495
            options = extend(default, options, false),
116✔
1496

1497
            routes  = {  },
58✔
1498
            iroutes = {  },
58✔
1499
            helpers = {
58✔
1500
                url_for = url_for_helper,
58✔
1501
            },
58✔
1502
            hooks   = {  },
58✔
1503

1504
            -- methods
1505
            route   = add_route,
58✔
1506
            delete  = delete_route,
58✔
1507
            match   = match_route,
58✔
1508
            helper  = set_helper,
58✔
1509
            hook    = set_hook,
58✔
1510
            url_for = url_for_httpd,
58✔
1511

1512
            -- Exposed to make it replaceable by a user.
1513
            tcp_server_f = socket.tcp_server,
58✔
1514

1515
            -- caches
1516
            cache   = {
58✔
1517
                tpl         = {},
58✔
1518
                ctx         = {},
58✔
1519
                static      = {},
58✔
1520
            },
58✔
1521

1522
            disable_keepalive   = tomap(disable_keepalive),
116✔
1523
            idle_timeout        = options.idle_timeout,
58✔
1524

1525
            internal = {
58✔
1526
                preprocess_client_handler = function() end,
106✔
1527
                postprocess_client_handler = function() end,
106✔
1528
            }
58✔
1529
        }
1530

1531
        return self
58✔
1532
    end,
1533

1534
    internal = {
1✔
1535
        response_mt = response_mt,
1✔
1536
        request_mt = request_mt,
1✔
1537
        extend = extend,
1✔
1538
    }
1✔
1539
}
1540

1541
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