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