xref: /unit/test/test_settings.py (revision 2616:ab2896c980ab)
1import re
2import socket
3import subprocess
4import time
5
6import pytest
7
8from unit.applications.lang.python import ApplicationPython
9
10prerequisites = {'modules': {'python': 'any'}}
11
12client = ApplicationPython()
13
14
15def sysctl():
16    try:
17        out = subprocess.check_output(
18            ['sysctl', '-a'], stderr=subprocess.STDOUT
19        ).decode()
20    except FileNotFoundError:
21        pytest.skip('requires sysctl')
22
23    return out
24
25
26def test_settings_large_header_buffer_size():
27    client.load('empty')
28
29    def set_buffer_size(size):
30        assert 'success' in client.conf(
31            {'http': {'large_header_buffer_size': size}},
32            'settings',
33        )
34
35    def header_value(size, expect=200):
36        headers = {'Host': 'a' * (size - 1), 'Connection': 'close'}
37        assert client.get(headers=headers)['status'] == expect
38
39    set_buffer_size(4096)
40    header_value(4096)
41    header_value(4097, 431)
42
43    set_buffer_size(16384)
44    header_value(16384)
45    header_value(16385, 431)
46
47
48def test_settings_large_header_buffers():
49    client.load('empty')
50
51    def set_buffers(buffers):
52        assert 'success' in client.conf(
53            {'http': {'large_header_buffers': buffers}},
54            'settings',
55        )
56
57    def big_headers(headers_num, expect=200):
58        headers = {'Host': 'localhost', 'Connection': 'close'}
59
60        for i in range(headers_num):
61            headers[f'Custom-header-{i}'] = 'a' * 8000
62
63        assert client.get(headers=headers)['status'] == expect
64
65    set_buffers(1)
66    big_headers(1)
67    big_headers(2, 431)
68
69    set_buffers(2)
70    big_headers(2)
71    big_headers(3, 431)
72
73    set_buffers(8)
74    big_headers(8)
75    big_headers(9, 431)
76
77
78@pytest.mark.skip('not yet')
79def test_settings_large_header_buffer_invalid():
80    def check_error(conf):
81        assert 'error' in client.conf({'http': conf}, 'settings')
82
83    check_error({'large_header_buffer_size': -1})
84    check_error({'large_header_buffer_size': 0})
85    check_error({'large_header_buffers': -1})
86    check_error({'large_header_buffers': 0})
87
88
89def test_settings_server_version():
90    client.load('empty')
91
92    assert client.get()['headers']['Server'].startswith('Unit/')
93
94    assert 'success' in client.conf(
95        {"http": {"server_version": False}}, 'settings'
96    ), 'remove version'
97    assert client.get()['headers']['Server'] == 'Unit'
98
99    assert 'success' in client.conf(
100        {"http": {"server_version": True}}, 'settings'
101    ), 'add version'
102    assert client.get()['headers']['Server'].startswith('Unit/')
103
104
105def test_settings_header_read_timeout():
106    client.load('empty')
107
108    def req():
109        (_, sock) = client.http(
110            b"""GET / HTTP/1.1
111""",
112            start=True,
113            read_timeout=1,
114            raw=True,
115        )
116
117        time.sleep(3)
118
119        return client.http(
120            b"""Host: localhost
121Connection: close
122
123""",
124            sock=sock,
125            raw=True,
126        )
127
128    assert 'success' in client.conf(
129        {'http': {'header_read_timeout': 2}}, 'settings'
130    )
131    assert req()['status'] == 408, 'status header read timeout'
132
133    assert 'success' in client.conf(
134        {'http': {'header_read_timeout': 7}}, 'settings'
135    )
136    assert req()['status'] == 200, 'status header read timeout 2'
137
138
139def test_settings_header_read_timeout_update():
140    client.load('empty')
141
142    assert 'success' in client.conf(
143        {'http': {'header_read_timeout': 4}}, 'settings'
144    )
145
146    sock = client.http(
147        b"""GET / HTTP/1.1
148""",
149        raw=True,
150        no_recv=True,
151    )
152
153    time.sleep(2)
154
155    sock = client.http(
156        b"""Host: localhost
157""",
158        sock=sock,
159        raw=True,
160        no_recv=True,
161    )
162
163    time.sleep(2)
164
165    (resp, sock) = client.http(
166        b"""X-Blah: blah
167""",
168        start=True,
169        sock=sock,
170        read_timeout=1,
171        raw=True,
172    )
173
174    if len(resp) != 0:
175        sock.close()
176
177    else:
178        time.sleep(2)
179
180        resp = client.http(
181            b"""Connection: close
182
183""",
184            sock=sock,
185            raw=True,
186        )
187
188    assert resp['status'] == 408, 'status header read timeout update'
189
190
191def test_settings_body_read_timeout():
192    client.load('empty')
193
194    def req():
195        (_, sock) = client.http(
196            b"""POST / HTTP/1.1
197Host: localhost
198Content-Length: 10
199Connection: close
200
201""",
202            start=True,
203            raw_resp=True,
204            read_timeout=1,
205            raw=True,
206        )
207
208        time.sleep(3)
209
210        return client.http(b"""0123456789""", sock=sock, raw=True)
211
212    assert 'success' in client.conf(
213        {'http': {'body_read_timeout': 2}}, 'settings'
214    )
215    assert req()['status'] == 408, 'status body read timeout'
216
217    assert 'success' in client.conf(
218        {'http': {'body_read_timeout': 7}}, 'settings'
219    )
220    assert req()['status'] == 200, 'status body read timeout 2'
221
222
223def test_settings_body_read_timeout_update():
224    client.load('empty')
225
226    assert 'success' in client.conf(
227        {'http': {'body_read_timeout': 4}}, 'settings'
228    )
229
230    (resp, sock) = client.http(
231        b"""POST / HTTP/1.1
232Host: localhost
233Content-Length: 10
234Connection: close
235
236""",
237        start=True,
238        read_timeout=1,
239        raw=True,
240    )
241
242    time.sleep(2)
243
244    (resp, sock) = client.http(
245        b"""012""", start=True, sock=sock, read_timeout=1, raw=True
246    )
247
248    time.sleep(2)
249
250    (resp, sock) = client.http(
251        b"""345""", start=True, sock=sock, read_timeout=1, raw=True
252    )
253
254    time.sleep(2)
255
256    resp = client.http(b"""6789""", sock=sock, raw=True)
257
258    assert resp['status'] == 200, 'status body read timeout update'
259
260
261def test_settings_send_timeout(temp_dir):
262    client.load('body_generate')
263
264    def req(addr, data_len):
265        sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
266        sock.connect(addr)
267
268        req = f"""GET / HTTP/1.1
269Host: localhost
270X-Length: {data_len}
271Connection: close
272
273"""
274
275        sock.sendall(req.encode())
276
277        data = sock.recv(16).decode()
278
279        time.sleep(3)
280
281        data += client.recvall(sock).decode()
282
283        sock.close()
284
285        return data
286
287    sysctl_out = sysctl()
288    values = re.findall(r'net.core.[rw]mem_(?:max|default).*?(\d+)', sysctl_out)
289    values = [int(v) for v in values]
290
291    data_len = 1048576 if len(values) == 0 else 10 * max(values)
292
293    addr = f'{temp_dir}/sock'
294
295    assert 'success' in client.conf(
296        {f'unix:{addr}': {'application': 'body_generate'}}, 'listeners'
297    )
298
299    assert 'success' in client.conf({'http': {'send_timeout': 1}}, 'settings')
300
301    data = req(addr, data_len)
302    assert re.search(r'200 OK', data), 'send timeout status'
303    assert len(data) < data_len, 'send timeout data '
304
305    client.conf({'http': {'send_timeout': 7}}, 'settings')
306
307    data = req(addr, data_len)
308    assert re.search(r'200 OK', data), 'send timeout status  2'
309    assert len(data) > data_len, 'send timeout data 2'
310
311
312def test_settings_idle_timeout():
313    client.load('empty')
314
315    def req():
316        (_, sock) = client.get(
317            headers={'Host': 'localhost', 'Connection': 'keep-alive'},
318            start=True,
319            read_timeout=1,
320        )
321
322        time.sleep(3)
323
324        return client.get(sock=sock)
325
326    assert client.get()['status'] == 200, 'init'
327
328    assert 'success' in client.conf({'http': {'idle_timeout': 2}}, 'settings')
329    assert req()['status'] == 408, 'status idle timeout'
330
331    assert 'success' in client.conf({'http': {'idle_timeout': 7}}, 'settings')
332    assert req()['status'] == 200, 'status idle timeout 2'
333
334
335def test_settings_idle_timeout_2():
336    client.load('empty')
337
338    def req():
339        sock = client.http(b'', raw=True, no_recv=True)
340
341        time.sleep(3)
342
343        return client.get(sock=sock)
344
345    assert client.get()['status'] == 200, 'init'
346
347    assert 'success' in client.conf({'http': {'idle_timeout': 1}}, 'settings')
348    assert req()['status'] == 408, 'status idle timeout'
349
350    assert 'success' in client.conf({'http': {'idle_timeout': 7}}, 'settings')
351    assert req()['status'] == 200, 'status idle timeout 2'
352
353
354def test_settings_max_body_size():
355    client.load('empty')
356
357    assert 'success' in client.conf({'http': {'max_body_size': 5}}, 'settings')
358
359    assert client.post(body='01234')['status'] == 200, 'status size'
360    assert client.post(body='012345')['status'] == 413, 'status size max'
361
362
363def test_settings_max_body_size_large():
364    client.load('mirror')
365
366    assert 'success' in client.conf(
367        {'http': {'max_body_size': 32 * 1024 * 1024}}, 'settings'
368    )
369
370    body = '0123456789abcdef' * 4 * 64 * 1024
371    resp = client.post(body=body, read_buffer_size=1024 * 1024)
372    assert resp['status'] == 200, 'status size 4'
373    assert resp['body'] == body, 'status body 4'
374
375    body = '0123456789abcdef' * 8 * 64 * 1024
376    resp = client.post(body=body, read_buffer_size=1024 * 1024)
377    assert resp['status'] == 200, 'status size 8'
378    assert resp['body'] == body, 'status body 8'
379
380    body = '0123456789abcdef' * 16 * 64 * 1024
381    resp = client.post(body=body, read_buffer_size=1024 * 1024)
382    assert resp['status'] == 200, 'status size 16'
383    assert resp['body'] == body, 'status body 16'
384
385    body = '0123456789abcdef' * 32 * 64 * 1024
386    resp = client.post(body=body, read_buffer_size=1024 * 1024)
387    assert resp['status'] == 200, 'status size 32'
388    assert resp['body'] == body, 'status body 32'
389
390
391@pytest.mark.skip('not yet')
392def test_settings_negative_value():
393    assert 'error' in client.conf(
394        {'http': {'max_body_size': -1}}, 'settings'
395    ), 'settings negative value'
396
397
398def test_settings_body_buffer_size():
399    client.load('mirror')
400
401    assert 'success' in client.conf(
402        {
403            'http': {
404                'max_body_size': 64 * 1024 * 1024,
405                'body_buffer_size': 32 * 1024 * 1024,
406            }
407        },
408        'settings',
409    )
410
411    body = '0123456789abcdef'
412    resp = client.post(body=body)
413    assert bool(resp), 'response from application'
414    assert resp['status'] == 200, 'status'
415    assert resp['body'] == body, 'body'
416
417    body = '0123456789abcdef' * 1024 * 1024
418    resp = client.post(body=body, read_buffer_size=1024 * 1024)
419    assert bool(resp), 'response from application 2'
420    assert resp['status'] == 200, 'status 2'
421    assert resp['body'] == body, 'body 2'
422
423    body = '0123456789abcdef' * 2 * 1024 * 1024
424    resp = client.post(body=body, read_buffer_size=1024 * 1024)
425    assert bool(resp), 'response from application 3'
426    assert resp['status'] == 200, 'status 3'
427    assert resp['body'] == body, 'body 3'
428
429    body = '0123456789abcdef' * 3 * 1024 * 1024
430    resp = client.post(body=body, read_buffer_size=1024 * 1024)
431    assert bool(resp), 'response from application 4'
432    assert resp['status'] == 200, 'status 4'
433    assert resp['body'] == body, 'body 4'
434
435
436def test_settings_log_route(findall, search_in_file, wait_for_record):
437    def count_fallbacks():
438        return len(findall(r'"fallback" taken'))
439
440    def check_record(template):
441        assert search_in_file(template) is not None
442
443    def check_no_record(template):
444        assert search_in_file(template) is None
445
446    def template_req_line(url):
447        return rf'\[notice\].*http request line "GET {url} HTTP/1\.1"'
448
449    def template_selected(route):
450        return rf'\[notice\].*"{route}" selected'
451
452    def template_discarded(route):
453        return rf'\[info\].*"{route}" discarded'
454
455    def wait_for_request_log(status, uri, route):
456        assert client.get(url=uri)['status'] == status
457        assert wait_for_record(template_req_line(uri)) is not None
458        assert wait_for_record(template_selected(route)) is not None
459
460    # routes array
461
462    assert 'success' in client.conf(
463        {
464            "listeners": {"*:8080": {"pass": "routes"}},
465            "routes": [
466                {
467                    "match": {
468                        "uri": "/zero",
469                    },
470                    "action": {"return": 200},
471                },
472                {
473                    "action": {"return": 201},
474                },
475            ],
476            "applications": {},
477            "settings": {"http": {"log_route": True}},
478        }
479    )
480
481    wait_for_request_log(200, '/zero', 'routes/0')
482    check_no_record(r'discarded')
483
484    wait_for_request_log(201, '/one', 'routes/1')
485    check_record(template_discarded('routes/0'))
486
487    # routes object
488
489    assert 'success' in client.conf(
490        {
491            "listeners": {"*:8080": {"pass": "routes/main"}},
492            "routes": {
493                "main": [
494                    {
495                        "match": {
496                            "uri": "/named_route",
497                        },
498                        "action": {"return": 200},
499                    },
500                    {
501                        "action": {"return": 201},
502                    },
503                ]
504            },
505            "applications": {},
506            "settings": {"http": {"log_route": True}},
507        }
508    )
509
510    wait_for_request_log(200, '/named_route', 'routes/main/0')
511    check_no_record(template_discarded('routes/main'))
512
513    wait_for_request_log(201, '/unnamed_route', 'routes/main/1')
514    check_record(template_discarded('routes/main/0'))
515
516    # routes sequence
517
518    assert 'success' in client.conf(
519        {
520            "listeners": {"*:8080": {"pass": "routes/first"}},
521            "routes": {
522                "first": [
523                    {
524                        "action": {"pass": "routes/second"},
525                    },
526                ],
527                "second": [
528                    {
529                        "action": {"return": 200},
530                    },
531                ],
532            },
533            "applications": {},
534            "settings": {"http": {"log_route": True}},
535        }
536    )
537
538    wait_for_request_log(200, '/sequence', 'routes/second/0')
539    check_record(template_selected('routes/first/0'))
540
541    # fallback
542
543    assert 'success' in client.conf(
544        {
545            "listeners": {"*:8080": {"pass": "routes/fall"}},
546            "routes": {
547                "fall": [
548                    {
549                        "action": {
550                            "share": "/blah",
551                            "fallback": {"pass": "routes/fall2"},
552                        },
553                    },
554                ],
555                "fall2": [
556                    {
557                        "action": {"return": 200},
558                    },
559                ],
560            },
561            "applications": {},
562            "settings": {"http": {"log_route": True}},
563        }
564    )
565
566    wait_for_request_log(200, '/', 'routes/fall2/0')
567    assert count_fallbacks() == 1
568    check_record(template_selected('routes/fall/0'))
569
570    assert client.head()['status'] == 200
571    assert count_fallbacks() == 2
572
573    # disable log
574
575    assert 'success' in client.conf({"log_route": False}, 'settings/http')
576
577    url = '/disable_logging'
578    assert client.get(url=url)['status'] == 200
579
580    time.sleep(1)
581
582    check_no_record(template_req_line(url))
583
584    # total
585
586    assert len(findall(r'\[notice\].*http request line')) == 7
587    assert len(findall(r'\[notice\].*selected')) == 10
588    assert len(findall(r'\[info\].*discarded')) == 2
589