xref: /unit/test/test_proxy.py (revision 2616:ab2896c980ab)
1import re
2import socket
3import time
4
5import pytest
6
7from conftest import run_process
8from unit.applications.lang.python import ApplicationPython
9from unit.option import option
10from unit.utils import waitforsocket
11
12prerequisites = {'modules': {'python': 'any'}}
13
14client = ApplicationPython()
15SERVER_PORT = 7999
16
17
18@pytest.fixture(autouse=True)
19def setup_method_fixture():
20    run_process(run_server, SERVER_PORT)
21    waitforsocket(SERVER_PORT)
22
23    python_dir = f'{option.test_dir}/python'
24    assert 'success' in client.conf(
25        {
26            "listeners": {
27                "*:8080": {"pass": "routes"},
28                "*:8081": {"pass": "applications/mirror"},
29            },
30            "routes": [{"action": {"proxy": "http://127.0.0.1:8081"}}],
31            "applications": {
32                "mirror": {
33                    "type": client.get_application_type(),
34                    "processes": {"spare": 0},
35                    "path": f'{python_dir}/mirror',
36                    "working_directory": f'{python_dir}/mirror',
37                    "module": "wsgi",
38                },
39                "custom_header": {
40                    "type": client.get_application_type(),
41                    "processes": {"spare": 0},
42                    "path": f'{python_dir}/custom_header',
43                    "working_directory": f'{python_dir}/custom_header',
44                    "module": "wsgi",
45                },
46                "delayed": {
47                    "type": client.get_application_type(),
48                    "processes": {"spare": 0},
49                    "path": f'{python_dir}/delayed',
50                    "working_directory": f'{python_dir}/delayed',
51                    "module": "wsgi",
52                },
53            },
54        }
55    ), 'proxy initial configuration'
56
57
58def run_server(server_port):
59    sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
60    sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
61
62    server_address = ('', server_port)
63    sock.bind(server_address)
64    sock.listen(5)
65
66    def recvall(sock):
67        buff_size = 4096
68        data = b''
69        while True:
70            part = sock.recv(buff_size)
71            data += part
72            if len(part) < buff_size:
73                break
74        return data
75
76    req = b"""HTTP/1.1 200 OK
77Content-Length: 10
78
79"""
80
81    while True:
82        connection, _ = sock.accept()
83
84        data = recvall(connection).decode()
85
86        to_send = req
87
88        m = re.search(r'X-Len: (\d+)', data)
89        if m:
90            to_send += b'X' * int(m.group(1))
91
92        connection.sendall(to_send)
93
94        connection.close()
95
96
97def get_http10(*args, **kwargs):
98    return client.get(*args, http_10=True, **kwargs)
99
100
101def post_http10(*args, **kwargs):
102    return client.post(*args, http_10=True, **kwargs)
103
104
105def test_proxy_http10():
106    for _ in range(10):
107        assert get_http10()['status'] == 200, 'status'
108
109
110def test_proxy_chain():
111    assert 'success' in client.conf(
112        {
113            "listeners": {
114                "*:8080": {"pass": "routes/first"},
115                "*:8081": {"pass": "routes/second"},
116                "*:8082": {"pass": "routes/third"},
117                "*:8083": {"pass": "routes/fourth"},
118                "*:8084": {"pass": "routes/fifth"},
119                "*:8085": {"pass": "applications/mirror"},
120            },
121            "routes": {
122                "first": [{"action": {"proxy": "http://127.0.0.1:8081"}}],
123                "second": [{"action": {"proxy": "http://127.0.0.1:8082"}}],
124                "third": [{"action": {"proxy": "http://127.0.0.1:8083"}}],
125                "fourth": [{"action": {"proxy": "http://127.0.0.1:8084"}}],
126                "fifth": [{"action": {"proxy": "http://127.0.0.1:8085"}}],
127            },
128            "applications": {
129                "mirror": {
130                    "type": client.get_application_type(),
131                    "processes": {"spare": 0},
132                    "path": f'{option.test_dir}/python/mirror',
133                    "working_directory": f'{option.test_dir}/python/mirror',
134                    "module": "wsgi",
135                }
136            },
137        }
138    ), 'proxy chain configuration'
139
140    assert get_http10()['status'] == 200, 'status'
141
142
143def test_proxy_body():
144    payload = '0123456789'
145    for _ in range(10):
146        resp = post_http10(body=payload)
147
148        assert resp['status'] == 200, 'status'
149        assert resp['body'] == payload, 'body'
150
151    payload = 'X' * 4096
152    for _ in range(10):
153        resp = post_http10(body=payload)
154
155        assert resp['status'] == 200, 'status'
156        assert resp['body'] == payload, 'body'
157
158    payload = 'X' * 4097
159    for _ in range(10):
160        resp = post_http10(body=payload)
161
162        assert resp['status'] == 200, 'status'
163        assert resp['body'] == payload, 'body'
164
165    payload = 'X' * 4096 * 256
166    for _ in range(10):
167        resp = post_http10(body=payload, read_buffer_size=4096 * 128)
168
169        assert resp['status'] == 200, 'status'
170        assert resp['body'] == payload, 'body'
171
172    payload = 'X' * 4096 * 257
173    for _ in range(10):
174        resp = post_http10(body=payload, read_buffer_size=4096 * 128)
175
176        assert resp['status'] == 200, 'status'
177        assert resp['body'] == payload, 'body'
178
179    assert 'success' in client.conf(
180        {'http': {'max_body_size': 32 * 1024 * 1024}}, 'settings'
181    )
182
183    payload = '0123456789abcdef' * 32 * 64 * 1024
184    resp = post_http10(body=payload, read_buffer_size=1024 * 1024)
185    assert resp['status'] == 200, 'status'
186    assert resp['body'] == payload, 'body'
187
188
189def test_proxy_parallel():
190    payload = 'X' * 4096 * 257
191    buff_size = 4096 * 258
192
193    socks = []
194    for i in range(10):
195        sock = post_http10(
196            body=f'{payload}{i}',
197            no_recv=True,
198            read_buffer_size=buff_size,
199        )
200        socks.append(sock)
201
202    for i in range(10):
203        resp = client.recvall(socks[i], buff_size=buff_size).decode()
204        socks[i].close()
205
206        resp = client._resp_to_dict(resp)
207
208        assert resp['status'] == 200, 'status'
209        assert resp['body'] == f'{payload}{i}', 'body'
210
211
212def test_proxy_header():
213    assert 'success' in client.conf(
214        {"pass": "applications/custom_header"}, 'listeners/*:8081'
215    ), 'custom_header configure'
216
217    header_value = 'blah'
218    assert (
219        get_http10(
220            headers={'Host': 'localhost', 'Custom-Header': header_value}
221        )['headers']['Custom-Header']
222        == header_value
223    ), 'custom header'
224
225    header_value = r"(),/:;<=>?@[\]{}\t !#$%&'*+-.^_`|~"
226    assert (
227        get_http10(
228            headers={'Host': 'localhost', 'Custom-Header': header_value}
229        )['headers']['Custom-Header']
230        == header_value
231    ), 'custom header 2'
232
233    header_value = 'X' * 4096
234    assert (
235        get_http10(
236            headers={'Host': 'localhost', 'Custom-Header': header_value}
237        )['headers']['Custom-Header']
238        == header_value
239    ), 'custom header 3'
240
241    header_value = 'X' * 8191
242    assert (
243        get_http10(
244            headers={'Host': 'localhost', 'Custom-Header': header_value}
245        )['headers']['Custom-Header']
246        == header_value
247    ), 'custom header 4'
248
249    header_value = 'X' * 8192
250    assert (
251        get_http10(
252            headers={'Host': 'localhost', 'Custom-Header': header_value}
253        )['status']
254        == 431
255    ), 'custom header 5'
256
257
258def test_proxy_fragmented():
259    sock = client.http(b"""GET / HTT""", raw=True, no_recv=True)
260
261    time.sleep(1)
262
263    sock.sendall("P/1.0\r\nHost: localhos".encode())
264
265    time.sleep(1)
266
267    sock.sendall("t\r\n\r\n".encode())
268
269    assert re.search('200 OK', client.recvall(sock).decode()), 'fragmented send'
270    sock.close()
271
272
273def test_proxy_fragmented_close():
274    sock = client.http(b"""GET / HTT""", raw=True, no_recv=True)
275
276    time.sleep(1)
277
278    sock.sendall("P/1.0\r\nHo".encode())
279
280    sock.close()
281
282
283def test_proxy_fragmented_body():
284    sock = client.http(b"""GET / HTT""", raw=True, no_recv=True)
285
286    time.sleep(1)
287
288    sock.sendall("P/1.0\r\nHost: localhost\r\n".encode())
289    sock.sendall("Content-Length: 30000\r\n".encode())
290
291    time.sleep(1)
292
293    sock.sendall("\r\n".encode())
294    sock.sendall(("X" * 10000).encode())
295
296    time.sleep(1)
297
298    sock.sendall(("X" * 10000).encode())
299
300    time.sleep(1)
301
302    sock.sendall(("X" * 10000).encode())
303
304    resp = client._resp_to_dict(client.recvall(sock).decode())
305    sock.close()
306
307    assert resp['status'] == 200, 'status'
308    assert resp['body'] == "X" * 30000, 'body'
309
310
311def test_proxy_fragmented_body_close():
312    sock = client.http(b"""GET / HTT""", raw=True, no_recv=True)
313
314    time.sleep(1)
315
316    sock.sendall("P/1.0\r\nHost: localhost\r\n".encode())
317    sock.sendall("Content-Length: 30000\r\n".encode())
318
319    time.sleep(1)
320
321    sock.sendall("\r\n".encode())
322    sock.sendall(("X" * 10000).encode())
323
324    sock.close()
325
326
327def test_proxy_nowhere():
328    assert 'success' in client.conf(
329        [{"action": {"proxy": "http://127.0.0.1:8082"}}], 'routes'
330    ), 'proxy path changed'
331
332    assert get_http10()['status'] == 502, 'status'
333
334
335def test_proxy_ipv6():
336    assert 'success' in client.conf(
337        {
338            "*:8080": {"pass": "routes"},
339            "[::1]:8081": {'application': 'mirror'},
340        },
341        'listeners',
342    ), 'add ipv6 listener configure'
343
344    assert 'success' in client.conf(
345        [{"action": {"proxy": "http://[::1]:8081"}}], 'routes'
346    ), 'proxy ipv6 configure'
347
348    assert get_http10()['status'] == 200, 'status'
349
350
351def test_proxy_unix(temp_dir):
352    addr = f'{temp_dir}/sock'
353
354    assert 'success' in client.conf(
355        {
356            "*:8080": {"pass": "routes"},
357            f'unix:{addr}': {'application': 'mirror'},
358        },
359        'listeners',
360    ), 'add unix listener configure'
361
362    assert 'success' in client.conf(
363        [{"action": {"proxy": f'http://unix:{addr}'}}], 'routes'
364    ), 'proxy unix configure'
365
366    assert get_http10()['status'] == 200, 'status'
367
368
369def test_proxy_delayed():
370    assert 'success' in client.conf(
371        {"pass": "applications/delayed"}, 'listeners/*:8081'
372    ), 'delayed configure'
373
374    body = '0123456789' * 1000
375    resp = post_http10(
376        headers={
377            'Host': 'localhost',
378            'Content-Length': str(len(body)),
379            'X-Parts': '2',
380            'X-Delay': '1',
381        },
382        body=body,
383    )
384
385    assert resp['status'] == 200, 'status'
386    assert resp['body'] == body, 'body'
387
388    resp = post_http10(
389        headers={
390            'Host': 'localhost',
391            'Content-Length': str(len(body)),
392            'X-Parts': '2',
393            'X-Delay': '1',
394        },
395        body=body,
396    )
397
398    assert resp['status'] == 200, 'status'
399    assert resp['body'] == body, 'body'
400
401
402def test_proxy_delayed_close():
403    assert 'success' in client.conf(
404        {"pass": "applications/delayed"}, 'listeners/*:8081'
405    ), 'delayed configure'
406
407    sock = post_http10(
408        headers={
409            'Host': 'localhost',
410            'Content-Length': '10000',
411            'X-Parts': '3',
412            'X-Delay': '1',
413        },
414        body='0123456789' * 1000,
415        no_recv=True,
416    )
417
418    assert re.search('200 OK', sock.recv(100).decode()), 'first'
419    sock.close()
420
421    sock = post_http10(
422        headers={
423            'Host': 'localhost',
424            'Content-Length': '10000',
425            'X-Parts': '3',
426            'X-Delay': '1',
427        },
428        body='0123456789' * 1000,
429        no_recv=True,
430    )
431
432    assert re.search('200 OK', sock.recv(100).decode()), 'second'
433    sock.close()
434
435
436@pytest.mark.skip('not yet')
437def test_proxy_content_length():
438    assert 'success' in client.conf(
439        [{"action": {"proxy": f'http://127.0.0.1:{SERVER_PORT}'}}],
440        'routes',
441    ), 'proxy backend configure'
442
443    resp = get_http10()
444    assert len(resp['body']) == 0, 'body lt Content-Length 0'
445
446    resp = get_http10(headers={'Host': 'localhost', 'X-Len': '5'})
447    assert len(resp['body']) == 5, 'body lt Content-Length 5'
448
449    resp = get_http10(headers={'Host': 'localhost', 'X-Len': '9'})
450    assert len(resp['body']) == 9, 'body lt Content-Length 9'
451
452    resp = get_http10(headers={'Host': 'localhost', 'X-Len': '11'})
453    assert len(resp['body']) == 10, 'body gt Content-Length 11'
454
455    resp = get_http10(headers={'Host': 'localhost', 'X-Len': '15'})
456    assert len(resp['body']) == 10, 'body gt Content-Length 15'
457
458
459def test_proxy_invalid():
460    def check_proxy(proxy):
461        assert 'error' in client.conf(
462            [{"action": {"proxy": proxy}}], 'routes'
463        ), 'proxy invalid'
464
465    check_proxy('blah')
466    check_proxy('/blah')
467    check_proxy('unix:/blah')
468    check_proxy('http://blah')
469    check_proxy('http://127.0.0.1')
470    check_proxy('http://127.0.0.1:')
471    check_proxy('http://127.0.0.1:blah')
472    check_proxy('http://127.0.0.1:-1')
473    check_proxy('http://127.0.0.1:8080b')
474    check_proxy('http://[]')
475    check_proxy('http://[]:8080')
476    check_proxy('http://[:]:8080')
477    check_proxy('http://[::8080')
478
479
480@pytest.mark.skip('not yet')
481def test_proxy_loop(skip_alert):
482    skip_alert(
483        r'socket.*failed',
484        r'accept.*failed',
485        r'new connections are not accepted',
486    )
487    assert 'success' in client.conf(
488        {
489            "listeners": {
490                "*:8080": {"pass": "routes"},
491                "*:8081": {"pass": "applications/mirror"},
492                "*:8082": {"pass": "routes"},
493            },
494            "routes": [{"action": {"proxy": "http://127.0.0.1:8082"}}],
495            "applications": {
496                "mirror": {
497                    "type": client.get_application_type(),
498                    "processes": {"spare": 0},
499                    "path": f'{option.test_dir}/python/mirror',
500                    "working_directory": f'{option.test_dir}/python/mirror',
501                    "module": "wsgi",
502                },
503            },
504        }
505    )
506
507    get_http10(no_recv=True)
508    get_http10(read_timeout=1)
509