1import re 2import time 3from pathlib import Path 4 5import pytest 6 7from unit.applications.lang.python import ApplicationPython 8from unit.applications.proto import ApplicationProto 9from unit.option import option 10 11client = ApplicationProto() 12client_python = ApplicationPython() 13 14 15@pytest.fixture(autouse=True) 16def setup_method_fixture(): 17 assert 'success' in client.conf( 18 { 19 "listeners": {"*:8080": {"pass": "routes"}}, 20 "routes": [{"action": {"return": 200}}], 21 }, 22 ), 'configure routes' 23 24 25def set_format(log_format): 26 assert 'success' in client.conf( 27 { 28 'path': f'{option.temp_dir}/access.log', 29 'format': log_format, 30 }, 31 'access_log', 32 ), 'access_log format' 33 34 35def test_variables_dollar(): 36 assert 'success' in client.conf("301", 'routes/0/action/return') 37 38 def check_dollar(location, expect): 39 assert 'success' in client.conf( 40 f'"{location}"', 41 'routes/0/action/location', 42 ) 43 assert client.get()['headers']['Location'] == expect 44 45 check_dollar( 46 'https://${host}${uri}path${dollar}dollar', 47 'https://localhost/path$dollar', 48 ) 49 check_dollar('path$dollar${dollar}', 'path$$') 50 51 52def test_variables_request_time(wait_for_record): 53 set_format('$uri $request_time') 54 55 sock = client.http(b'', raw=True, no_recv=True) 56 57 time.sleep(1) 58 59 assert client.get(url='/r_time_1', sock=sock)['status'] == 200 60 assert wait_for_record(r'\/r_time_1 0\.\d{3}', 'access.log') is not None 61 62 sock = client.http( 63 b"""G""", 64 no_recv=True, 65 raw=True, 66 ) 67 68 time.sleep(2) 69 70 client.http( 71 b"""ET /r_time_2 HTTP/1.1 72Host: localhost 73Connection: close 74 75""", 76 sock=sock, 77 raw=True, 78 ) 79 assert wait_for_record(r'\/r_time_2 [1-9]\.\d{3}', 'access.log') is not None 80 81 82def test_variables_method(search_in_file, wait_for_record): 83 set_format('$method') 84 85 reg = r'^GET$' 86 assert search_in_file(reg, 'access.log') is None 87 assert client.get()['status'] == 200 88 assert wait_for_record(reg, 'access.log') is not None, 'method GET' 89 90 reg = r'^POST$' 91 assert search_in_file(reg, 'access.log') is None 92 assert client.post()['status'] == 200 93 assert wait_for_record(reg, 'access.log') is not None, 'method POST' 94 95 96def test_variables_request_uri(search_in_file, wait_for_record): 97 set_format('$request_uri') 98 99 def check_request_uri(req_uri): 100 reg = fr'^{re.escape(req_uri)}$' 101 102 assert search_in_file(reg, 'access.log') is None 103 assert client.get(url=req_uri)['status'] == 200 104 assert wait_for_record(reg, 'access.log') is not None 105 106 check_request_uri('/3') 107 check_request_uri('/4*') 108 check_request_uri('/4%2A') 109 check_request_uri('/9?q#a') 110 111 112def test_variables_uri(search_in_file, wait_for_record): 113 set_format('$uri') 114 115 def check_uri(uri, expect=None): 116 expect = uri if expect is None else expect 117 reg = fr'^{re.escape(expect)}$' 118 119 assert search_in_file(reg, 'access.log') is None 120 assert client.get(url=uri)['status'] == 200 121 assert wait_for_record(reg, 'access.log') is not None 122 123 check_uri('/3') 124 check_uri('/4*') 125 check_uri('/5%2A', '/5*') 126 check_uri('/9?q#a', '/9') 127 128 129def test_variables_uri_no_cache(temp_dir): 130 Path(f'{temp_dir}/foo/bar').mkdir(parents=True) 131 Path(f'{temp_dir}/foo/bar/index.html').write_text('index', encoding='utf-8') 132 133 assert 'success' in client.conf( 134 { 135 "listeners": {"*:8080": {"pass": "routes"}}, 136 "routes": [ 137 { 138 "action": { 139 "rewrite": "/foo${uri}/", 140 "share": f'{temp_dir}$uri', 141 } 142 } 143 ], 144 } 145 ) 146 147 assert client.get(url='/bar')['status'] == 200 148 149 150def test_variables_host(search_in_file, wait_for_record): 151 set_format('$host') 152 153 def check_host(host, expect=None): 154 expect = host if expect is None else expect 155 reg = fr'^{re.escape(expect)}$' 156 157 assert search_in_file(reg, 'access.log') is None 158 assert ( 159 client.get(headers={'Host': host, 'Connection': 'close'})['status'] 160 == 200 161 ) 162 assert wait_for_record(reg, 'access.log') is not None 163 164 check_host('localhost') 165 check_host('localhost1.', 'localhost1') 166 check_host('localhost2:8080', 'localhost2') 167 check_host('.localhost') 168 check_host('www.localhost') 169 170 171def test_variables_remote_addr(search_in_file, wait_for_record): 172 set_format('$remote_addr') 173 174 assert client.get()['status'] == 200 175 assert wait_for_record(r'^127\.0\.0\.1$', 'access.log') is not None 176 177 assert 'success' in client.conf( 178 {"[::1]:8080": {"pass": "routes"}}, 'listeners' 179 ) 180 181 reg = r'^::1$' 182 assert search_in_file(reg, 'access.log') is None 183 assert client.get(sock_type='ipv6')['status'] == 200 184 assert wait_for_record(reg, 'access.log') is not None 185 186 187def test_variables_time_local( 188 date_to_sec_epoch, search_in_file, wait_for_record 189): 190 set_format('$uri $time_local $uri') 191 192 assert search_in_file(r'/time_local', 'access.log') is None 193 assert client.get(url='/time_local')['status'] == 200 194 assert wait_for_record(r'/time_local', 'access.log') is not None, 'time log' 195 date = search_in_file(r'^\/time_local (.*) \/time_local$', 'access.log')[1] 196 assert ( 197 abs( 198 date_to_sec_epoch(date, '%d/%b/%Y:%X %z') 199 - time.mktime(time.localtime()) 200 ) 201 < 5 202 ), '$time_local' 203 204 205def test_variables_request_line(search_in_file, wait_for_record): 206 set_format('$request_line') 207 208 reg = r'^GET \/r_line HTTP\/1\.1$' 209 assert search_in_file(reg, 'access.log') is None 210 assert client.get(url='/r_line')['status'] == 200 211 assert wait_for_record(reg, 'access.log') is not None 212 213 214def test_variables_request_id(search_in_file, wait_for_record, findall): 215 set_format('$uri $request_id $request_id') 216 217 assert search_in_file(r'/request_id', 'access.log') is None 218 assert client.get(url='/request_id_1')['status'] == 200 219 assert client.get(url='/request_id_2')['status'] == 200 220 assert wait_for_record(r'/request_id_2', 'access.log') is not None 221 222 id1 = findall( 223 r'^\/request_id_1 ([0-9a-f]{32}) ([0-9a-f]{32})$', 'access.log' 224 )[0] 225 id2 = findall( 226 r'^\/request_id_2 ([0-9a-f]{32}) ([0-9a-f]{32})$', 'access.log' 227 )[0] 228 229 assert id1[0] == id1[1], 'same ids first' 230 assert id2[0] == id2[1], 'same ids second' 231 assert id1[0] != id2[0], 'first id != second id' 232 233 234def test_variables_status(search_in_file, wait_for_record): 235 set_format('$status') 236 237 assert 'success' in client.conf("418", 'routes/0/action/return') 238 239 reg = r'^418$' 240 assert search_in_file(reg, 'access.log') is None 241 assert client.get()['status'] == 418 242 assert wait_for_record(reg, 'access.log') is not None 243 244 245def test_variables_header_referer(search_in_file, wait_for_record): 246 set_format('$method $header_referer') 247 248 def check_referer(referer): 249 reg = fr'^GET {re.escape(referer)}$' 250 251 assert search_in_file(reg, 'access.log') is None 252 assert ( 253 client.get( 254 headers={ 255 'Host': 'localhost', 256 'Connection': 'close', 257 'Referer': referer, 258 } 259 )['status'] 260 == 200 261 ) 262 assert wait_for_record(reg, 'access.log') is not None 263 264 check_referer('referer-value') 265 check_referer('') 266 check_referer('no') 267 268 269def test_variables_header_user_agent(search_in_file, wait_for_record): 270 set_format('$method $header_user_agent') 271 272 def check_user_agent(user_agent): 273 reg = fr'^GET {re.escape(user_agent)}$' 274 275 assert search_in_file(reg, 'access.log') is None 276 assert ( 277 client.get( 278 headers={ 279 'Host': 'localhost', 280 'Connection': 'close', 281 'User-Agent': user_agent, 282 } 283 )['status'] 284 == 200 285 ) 286 assert wait_for_record(reg, 'access.log') is not None 287 288 check_user_agent('MSIE') 289 check_user_agent('') 290 check_user_agent('no') 291 292 293def test_variables_many(search_in_file, wait_for_record): 294 def check_vars(uri, expect): 295 reg = fr'^{re.escape(expect)}$' 296 297 assert search_in_file(reg, 'access.log') is None 298 assert client.get(url=uri)['status'] == 200 299 assert wait_for_record(reg, 'access.log') is not None 300 301 set_format('$uri$method') 302 check_vars('/1', '/1GET') 303 304 set_format('${uri}${method}') 305 check_vars('/2', '/2GET') 306 307 set_format('${uri}$method') 308 check_vars('/3', '/3GET') 309 310 set_format('$method$method') 311 check_vars('/', 'GETGET') 312 313 314def test_variables_dynamic(wait_for_record): 315 set_format('$header_foo$cookie_foo$arg_foo') 316 317 assert ( 318 client.get( 319 url='/?foo=h', 320 headers={'Foo': 'b', 'Cookie': 'foo=la', 'Connection': 'close'}, 321 )['status'] 322 == 200 323 ) 324 assert wait_for_record(r'^blah$', 'access.log') is not None 325 326 327def test_variables_dynamic_arguments(search_in_file, wait_for_record): 328 def check_arg(url, expect=None): 329 expect = url if expect is None else expect 330 reg = fr'^{re.escape(expect)}$' 331 332 assert search_in_file(reg, 'access.log') is None 333 assert client.get(url=url)['status'] == 200 334 assert wait_for_record(reg, 'access.log') is not None 335 336 def check_no_arg(url): 337 assert client.get(url=url)['status'] == 200 338 assert search_in_file(r'^0$', 'access.log') is None 339 340 set_format('$arg_foo_bar') 341 check_arg('/?foo_bar=1', '1') 342 check_arg('/?foo_b%61r=2', '2') 343 check_arg('/?bar&foo_bar=3&foo', '3') 344 check_arg('/?foo_bar=l&foo_bar=4', '4') 345 check_no_arg('/') 346 check_no_arg('/?foo_bar=') 347 check_no_arg('/?Foo_bar=0') 348 check_no_arg('/?foo-bar=0') 349 check_no_arg('/?foo_bar=0&foo_bar=l') 350 351 set_format('$arg_foo_b%61r') 352 check_no_arg('/?foo_b=0') 353 check_no_arg('/?foo_bar=0') 354 355 set_format('$arg_f!~') 356 check_no_arg('/?f=0') 357 check_no_arg('/?f!~=0') 358 359 360def test_variables_dynamic_headers(search_in_file, wait_for_record): 361 def check_header(header, value): 362 reg = fr'^{value}$' 363 364 assert search_in_file(reg, 'access.log') is None 365 assert ( 366 client.get(headers={header: value, 'Connection': 'close'})['status'] 367 == 200 368 ) 369 assert wait_for_record(reg, 'access.log') is not None 370 371 def check_no_header(header): 372 assert ( 373 client.get(headers={header: '0', 'Connection': 'close'})['status'] 374 == 200 375 ) 376 assert search_in_file(r'^0$', 'access.log') is None 377 378 set_format('$header_foo_bar') 379 check_header('foo-bar', '1') 380 check_header('Foo-Bar', '2') 381 check_no_header('foo_bar') 382 check_no_header('foobar') 383 384 set_format('$header_Foo_Bar') 385 check_header('Foo-Bar', '4') 386 check_header('foo-bar', '5') 387 check_no_header('foo_bar') 388 check_no_header('foobar') 389 390 391def test_variables_dynamic_cookies(search_in_file, wait_for_record): 392 def check_no_cookie(cookie): 393 assert ( 394 client.get( 395 headers={ 396 'Host': 'localhost', 397 'Cookie': cookie, 398 'Connection': 'close', 399 }, 400 )['status'] 401 == 200 402 ) 403 assert search_in_file(r'^0$', 'access.log') is None 404 405 set_format('$cookie_foo_bar') 406 407 reg = r'^1$' 408 assert search_in_file(reg, 'access.log') is None 409 assert ( 410 client.get( 411 headers={ 412 'Host': 'localhost', 413 'Cookie': 'foo_bar=1', 414 'Connection': 'close', 415 }, 416 )['status'] 417 == 200 418 ) 419 assert wait_for_record(reg, 'access.log') is not None 420 421 check_no_cookie('fOo_bar=0') 422 check_no_cookie('foo_bar=') 423 424 425def test_variables_response_header(temp_dir, wait_for_record): 426 # If response has two headers with the same name then first value 427 # will be stored in variable. 428 # $response_header_transfer_encoding value can be 'chunked' or null only. 429 430 # return 431 432 set_format( 433 'return@$response_header_server@$response_header_date@' 434 '$response_header_content_length@$response_header_connection' 435 ) 436 437 assert client.get()['status'] == 200 438 assert ( 439 wait_for_record(r'return@Unit/.*@.*GMT@0@close', 'access.log') 440 is not None 441 ) 442 443 # share 444 445 Path(f'{temp_dir}/foo').mkdir() 446 Path(f'{temp_dir}/foo/index.html').write_text('index', encoding='utf-8') 447 448 assert 'success' in client.conf( 449 { 450 "listeners": {"*:8080": {"pass": "routes"}}, 451 "routes": [ 452 { 453 "action": { 454 "share": f'{temp_dir}$uri', 455 } 456 } 457 ], 458 } 459 ) 460 461 set_format( 462 'share@$response_header_last_modified@$response_header_etag@' 463 '$response_header_content_type@$response_header_server@' 464 '$response_header_date@$response_header_content_length@' 465 '$response_header_connection' 466 ) 467 468 assert client.get(url='/foo/index.html')['status'] == 200 469 assert ( 470 wait_for_record( 471 r'share@.*GMT@".*"@text/html@Unit/.*@.*GMT@5@close', 'access.log' 472 ) 473 is not None 474 ) 475 476 # redirect 477 478 set_format( 479 'redirect@$response_header_location@$response_header_server@' 480 '$response_header_date@$response_header_content_length@' 481 '$response_header_connection' 482 ) 483 484 assert client.get(url='/foo')['status'] == 301 485 assert ( 486 wait_for_record(r'redirect@/foo/@Unit/.*@.*GMT@0@close', 'access.log') 487 is not None 488 ) 489 490 # error 491 492 set_format( 493 'error@$response_header_content_type@$response_header_server@' 494 '$response_header_date@$response_header_content_length@' 495 '$response_header_connection' 496 ) 497 498 assert client.get(url='/blah')['status'] == 404 499 assert ( 500 wait_for_record(r'error@text/html@Unit/.*@.*GMT@54@close', 'access.log') 501 is not None 502 ) 503 504 505def test_variables_response_header_application(require, wait_for_record): 506 require({'modules': {'python': 'any'}}) 507 508 client_python.load('chunked') 509 510 set_format('$uri@$response_header_transfer_encoding') 511 512 assert client_python.get(url='/1')['status'] == 200 513 assert wait_for_record(r'/1@chunked', 'access.log') is not None 514 515 516def test_variables_invalid(temp_dir): 517 def check_variables(log_format): 518 assert 'error' in client.conf( 519 { 520 'path': f'{temp_dir}/access.log', 521 'format': log_format, 522 }, 523 'access_log', 524 ), 'access_log format' 525 526 check_variables("$") 527 check_variables("${") 528 check_variables("${}") 529 check_variables("$ur") 530 check_variables("$uri$$host") 531 check_variables("$uriblah") 532 check_variables("${uri") 533 check_variables("${{uri}") 534 check_variables("$ar") 535 check_variables("$arg") 536 check_variables("$arg_") 537 check_variables("$cookie") 538 check_variables("$cookie_") 539 check_variables("$header") 540 check_variables("$header_") 541