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