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