diff options
author | Konstantin Pavlov <thresh@nginx.com> | 2022-09-13 13:17:16 +0400 |
---|---|---|
committer | Konstantin Pavlov <thresh@nginx.com> | 2022-09-13 13:17:16 +0400 |
commit | ce964aa30b163e2b3263c5af57c1a6dae7d0cebb (patch) | |
tree | 26bf70c1a5991f6471fc4caed8628e068fdc0b7b /test | |
parent | eba4c3c98fa1bf275d94df8c727f70692ae7eae1 (diff) | |
parent | 38bd7e76a134084ab95a4ee3125af1ccd7b35864 (diff) | |
download | unit-1.28.0-1.tar.gz unit-1.28.0-1.tar.bz2 |
Merged with the default branch.1.28.0-1
Diffstat (limited to 'test')
28 files changed, 1121 insertions, 287 deletions
diff --git a/test/conftest.py b/test/conftest.py index 904abc32..18851baa 100644 --- a/test/conftest.py +++ b/test/conftest.py @@ -20,9 +20,11 @@ from unit.check.isolation import check_isolation from unit.check.node import check_node from unit.check.regex import check_regex from unit.check.tls import check_openssl +from unit.check.unix_abstract import check_unix_abstract from unit.http import TestHTTP from unit.log import Log from unit.option import option +from unit.status import Status from unit.utils import public_dir from unit.utils import waitforfiles @@ -213,6 +215,7 @@ def pytest_sessionstart(session): check_chroot() check_isolation() + check_unix_abstract() _clear_conf(unit['temp_dir'] + '/control.unit.sock') @@ -427,6 +430,8 @@ def unit_run(state_dir=None): controller['pid'] = pid_by_name(controller['name']) controller['fds'] = _count_fds(controller['pid']) + Status._check_zeros() + return unit_instance diff --git a/test/python/ctx_iter_atexit/wsgi.py b/test/python/ctx_iter_atexit/wsgi.py index b2f12c35..75d40895 100644 --- a/test/python/ctx_iter_atexit/wsgi.py +++ b/test/python/ctx_iter_atexit/wsgi.py @@ -15,7 +15,6 @@ class application: self.start( '200', [ - ('Content-Type', self.environ.get('CONTENT_TYPE')), ('Content-Length', str(len(body))), ], ) diff --git a/test/python/forwarded_header/wsgi.py b/test/python/forwarded_header/wsgi.py new file mode 100644 index 00000000..44d370ab --- /dev/null +++ b/test/python/forwarded_header/wsgi.py @@ -0,0 +1,10 @@ +def application(env, start_response): + start_response( + '200', + [ + ('Content-Length', '0'), + ('Remote-Addr', env.get('REMOTE_ADDR')), + ('Url-Scheme', env.get('wsgi.url_scheme')), + ], + ) + return [] diff --git a/test/test_access_log.py b/test/test_access_log.py index 5d242a1a..b1d89343 100644 --- a/test/test_access_log.py +++ b/test/test_access_log.py @@ -15,6 +15,15 @@ class TestAccessLog(TestApplicationPython): '"' + option.temp_dir + '/access.log"', 'access_log' ), 'access_log configure' + def set_format(self, format): + assert 'success' in self.conf( + { + 'path': option.temp_dir + '/access.log', + 'format': format, + }, + 'access_log', + ), 'access_log format' + def wait_for_record(self, pattern, name='access.log'): return super().wait_for_record(pattern, name) @@ -27,7 +36,6 @@ class TestAccessLog(TestApplicationPython): headers={ 'Host': 'localhost', 'Connection': 'keep-alive', - 'Content-Type': 'text/html', }, start=True, body='01234', @@ -38,15 +46,7 @@ class TestAccessLog(TestApplicationPython): self.wait_for_record(r'"POST / HTTP/1.1" 200 5') is not None ), 'keepalive 1' - resp = self.post( - headers={ - 'Host': 'localhost', - 'Connection': 'close', - 'Content-Type': 'text/html', - }, - sock=sock, - body='0123456789', - ) + resp = self.post(sock=sock, body='0123456789') assert ( self.wait_for_record(r'"POST / HTTP/1.1" 200 10') is not None @@ -263,3 +263,65 @@ Connection: close self.wait_for_record(r'"GET / HTTP/1.1" 200 0 "-" "-"', 'new.log') is not None ), 'change' + + def test_access_log_format(self): + self.load('empty') + + def check_format(format, expect, url='/'): + self.set_format(format) + + assert self.get(url=url)['status'] == 200 + assert self.wait_for_record(expect) is not None, 'found' + + format = 'BLAH\t0123456789' + check_format(format, format) + check_format('$uri $status $uri $status', '/ 200 / 200') + + def test_access_log_variables(self): + self.load('mirror') + + # $time_local + + self.set_format('$uri $time_local $uri') + assert self.get(url='/time_local')['status'] == 200 + assert self.wait_for_record('/time_local') is not None, 'time log' + date = self.search_in_log( + r'^\/time_local (.*) \/time_local$', 'access.log' + )[1] + assert ( + abs( + self.date_to_sec_epoch(date, '%d/%b/%Y:%X %z') + - time.mktime(time.localtime()) + ) + < 5 + ), '$time_local' + + # $request_line + + self.set_format('$request_line') + assert self.get(url='/r_line')['status'] == 200 + assert self.wait_for_record(r'^GET \/r_line HTTP\/1\.1$') is not None + + # $body_bytes_sent + + self.set_format('$uri $body_bytes_sent') + body = '0123456789' * 50 + self.post(url='/bbs', body=body, read_timeout=1) + assert ( + self.wait_for_record(r'^\/bbs ' + str(len(body)) + r'$') is not None + ), '$body_bytes_sent' + + def test_access_log_incorrect(self, skip_alert): + skip_alert(r'failed to apply new conf') + + assert 'error' in self.conf( + option.temp_dir + '/blah/access.log' 'access_log/path', + ), 'access_log path incorrect' + + assert 'error' in self.conf( + { + 'path': option.temp_dir + '/access.log', + 'format': '$remote_add', + }, + 'access_log', + ), 'access_log format incorrect' diff --git a/test/test_asgi_application.py b/test/test_asgi_application.py index 60fcffc1..34dfe18e 100644 --- a/test/test_asgi_application.py +++ b/test/test_asgi_application.py @@ -60,6 +60,16 @@ custom-header: BLAH }, 'headers' assert resp['body'] == body, 'body' + def test_asgi_application_unix(self, temp_dir): + self.load('empty') + + addr = temp_dir + '/sock' + assert 'success' in self.conf( + {"unix:" + addr: {"pass": "applications/empty"}}, 'listeners' + ) + + assert self.get(sock_type='unix', addr=addr)['status'] == 200 + def test_asgi_application_query_string(self): self.load('query_string') @@ -150,15 +160,7 @@ custom-header: BLAH assert self.get()['status'] == 200, 'init' body = '0123456789AB' * 1024 * 1024 # 12 Mb - resp = self.post( - headers={ - 'Host': 'localhost', - 'Connection': 'close', - 'Content-Type': 'text/html', - }, - body=body, - read_buffer_size=1024 * 1024, - ) + resp = self.post(body=body, read_buffer_size=1024 * 1024) assert resp['body'] == body, 'keep-alive 1' @@ -172,7 +174,6 @@ custom-header: BLAH headers={ 'Host': 'localhost', 'Connection': 'keep-alive', - 'Content-Type': 'text/html', }, start=True, body=body, @@ -182,15 +183,7 @@ custom-header: BLAH assert resp['body'] == body, 'keep-alive 1' body = '0123456789' - resp = self.post( - headers={ - 'Host': 'localhost', - 'Connection': 'close', - 'Content-Type': 'text/html', - }, - sock=sock, - body=body, - ) + resp = self.post(sock=sock, body=body) assert resp['body'] == body, 'keep-alive 2' @@ -208,7 +201,6 @@ custom-header: BLAH headers={ 'Host': 'localhost', 'Connection': 'keep-alive', - 'Content-Type': 'text/html', }, start=True, body=body, @@ -226,7 +218,6 @@ custom-header: BLAH headers={ 'Host': 'localhost', 'Connection': 'keep-alive', - 'Content-Type': 'text/html', }, start=True, sock=socks[i], @@ -239,15 +230,7 @@ custom-header: BLAH self.load('mirror', processes=i + 1) for i in range(conns): - resp = self.post( - headers={ - 'Host': 'localhost', - 'Connection': 'close', - 'Content-Type': 'text/html', - }, - sock=socks[i], - body=body, - ) + resp = self.post(sock=socks[i], body=body) assert resp['body'] == body, 'keep-alive close' @@ -264,7 +247,6 @@ custom-header: BLAH headers={ 'Host': 'localhost', 'Connection': 'keep-alive', - 'Content-Type': 'text/html', }, start=True, body=body, @@ -277,16 +259,7 @@ custom-header: BLAH assert self.get()['status'] == 200, 'init' - (resp, sock) = self.post( - headers={ - 'Host': 'localhost', - 'Connection': 'close', - 'Content-Type': 'text/html', - }, - start=True, - sock=sock, - body=body, - ) + (resp, sock) = self.post(start=True, sock=sock, body=body) assert resp['status'] == 200, 'reconfigure 2 keep-alive 2' assert resp['body'] == '', 'reconfigure 2 keep-alive 2 body' diff --git a/test/test_asgi_application_unix_abstract.py b/test/test_asgi_application_unix_abstract.py new file mode 100644 index 00000000..c4ec812f --- /dev/null +++ b/test/test_asgi_application_unix_abstract.py @@ -0,0 +1,23 @@ +from packaging import version +from unit.applications.lang.python import TestApplicationPython + + +class TestASGIApplicationUnixAbstract(TestApplicationPython): + prerequisites = { + 'modules': { + 'python': lambda v: version.parse(v) >= version.parse('3.5') + }, + 'features': ['unix_abstract'], + } + load_module = 'asgi' + + def test_asgi_application_unix_abstract(self): + self.load('empty') + + addr = '\0sock' + assert 'success' in self.conf( + {"unix:@" + addr[1:]: {"pass": "applications/empty"}}, + 'listeners', + ) + + assert self.get(sock_type='unix', addr=addr)['status'] == 200 diff --git a/test/test_client_ip.py b/test/test_client_ip.py index 53e52201..50aa6afc 100644 --- a/test/test_client_ip.py +++ b/test/test_client_ip.py @@ -1,4 +1,5 @@ from unit.applications.lang.python import TestApplicationPython +from unit.option import option class TestClientIP(TestApplicationPython): @@ -15,15 +16,27 @@ class TestClientIP(TestApplicationPython): "client_ip": options, "pass": "applications/client_ip", }, + "unix:" + + option.temp_dir + + "/sock": { + "client_ip": options, + "pass": "applications/client_ip", + }, }, 'listeners', ), 'listeners configure' def get_xff(self, xff, sock_type='ipv4'): - port = 7081 if sock_type == 'ipv4' else 7082 + address = { + 'ipv4': ('127.0.0.1', 7081), + 'ipv6': ('::1', 7082), + 'unix': (option.temp_dir + '/sock', None), + } + (addr, port) = address[sock_type] return self.get( sock_type=sock_type, + addr=addr, port=port, headers={'Connection': 'close', 'X-Forwarded-For': xff}, )['body'] @@ -31,7 +44,7 @@ class TestClientIP(TestApplicationPython): def setup_method(self): self.load('client_ip') - def test_settings_client_ip_single_ip(self): + def test_client_ip_single_ip(self): self.client_ip( {'header': 'X-Forwarded-For', 'source': '123.123.123.123'} ) @@ -59,7 +72,7 @@ class TestClientIP(TestApplicationPython): assert self.get_xff('1.1.1.1') == '127.0.0.1', 'bad source 3' assert self.get_xff('1.1.1.1', 'ipv6') == '1.1.1.1', 'replace 2' - def test_settings_client_ip_ipv4(self): + def test_client_ip_ipv4(self): self.client_ip({'header': 'X-Forwarded-For', 'source': '127.0.0.1'}) assert ( @@ -72,7 +85,7 @@ class TestClientIP(TestApplicationPython): self.get_xff(['8.8.8.8', '127.0.0.1, 10.0.1.1']) == '10.0.1.1' ), 'xff replace multi' - def test_settings_client_ip_ipv6(self): + def test_client_ip_ipv6(self): self.client_ip({'header': 'X-Forwarded-For', 'source': '::1'}) assert self.get_xff('1.1.1.1') == '127.0.0.1', 'bad source ipv4' @@ -85,7 +98,19 @@ class TestClientIP(TestApplicationPython): ]: assert self.get_xff(ip, 'ipv6') == ip, 'replace' - def test_settings_client_ip_recursive(self): + def test_client_ip_unix(self, temp_dir): + self.client_ip({'header': 'X-Forwarded-For', 'source': 'unix'}) + + assert self.get_xff('1.1.1.1') == '127.0.0.1', 'bad source ipv4' + assert self.get_xff('1.1.1.1', 'ipv6') == '::1', 'bad source ipv6' + + for ip in [ + '1.1.1.1', + '::11.22.33.44', + ]: + assert self.get_xff(ip, 'unix') == ip, 'replace' + + def test_client_ip_recursive(self): self.client_ip( { 'header': 'X-Forwarded-For', @@ -118,20 +143,41 @@ class TestClientIP(TestApplicationPython): == '2001:db8:3c4d:15::1a2f:1a2b' ), 'xff chain ipv6' - def test_settings_client_ip_invalid(self): - assert 'error' in self.conf( - { - "http": { - "client_ip": {'header': 'X-Forwarded-For', 'source': []} - } - }, - 'settings', - ), 'empty array source' + def test_client_ip_case_insensitive(self): + self.client_ip({'header': 'x-forwarded-for', 'source': '127.0.0.1'}) + + assert self.get_xff('1.1.1.1') == '1.1.1.1', 'case insensitive' + + def test_client_ip_empty_source(self): + self.client_ip({'header': 'X-Forwarded-For', 'source': []}) + + assert self.get_xff('1.1.1.1') == '127.0.0.1', 'empty source' + + def test_client_ip_invalid(self): assert 'error' in self.conf( { - "http": { - "client_ip": {'header': 'X-Forwarded-For', 'source': 'a'} + "127.0.0.1:7081": { + "client_ip": {"source": '127.0.0.1'}, + "pass": "applications/client_ip", } }, - 'settings', - ), 'empty source invalid' + 'listeners', + ), 'invalid header' + + def check_invalid_source(source): + assert 'error' in self.conf( + { + "127.0.0.1:7081": { + "client_ip": { + "header": "X-Forwarded-For", + "source": source, + }, + "pass": "applications/client_ip", + } + }, + 'listeners', + ), 'invalid source' + + check_invalid_source(None) + check_invalid_source('a') + check_invalid_source(['a']) diff --git a/test/test_configuration.py b/test/test_configuration.py index 4a9d9840..7c612db0 100644 --- a/test/test_configuration.py +++ b/test/test_configuration.py @@ -2,6 +2,7 @@ import socket import pytest from unit.control import TestControl +from unit.option import option class TestConfiguration(TestControl): @@ -226,6 +227,15 @@ class TestConfiguration(TestControl): {"*:7080": {"pass": "applications/app"}}, 'listeners' ), 'listeners no app' + def test_listeners_unix_abstract(self): + if option.system != 'Linux': + assert 'error' in self.try_addr("unix:@sock"), 'abstract at' + + pytest.skip('not yet') + + assert 'error' in self.try_addr("unix:\0soc"), 'abstract \0' + assert 'error' in self.try_addr("unix:\u0000soc"), 'abstract \0 unicode' + def test_listeners_addr(self): assert 'success' in self.try_addr("*:7080"), 'wildcard' assert 'success' in self.try_addr("127.0.0.1:7081"), 'explicit' diff --git a/test/test_forwarded_header.py b/test/test_forwarded_header.py new file mode 100644 index 00000000..eb2f25f8 --- /dev/null +++ b/test/test_forwarded_header.py @@ -0,0 +1,266 @@ +from unit.applications.lang.python import TestApplicationPython + + +class TestForwardedHeader(TestApplicationPython): + prerequisites = {'modules': {'python': 'any'}} + + def forwarded_header(self, forwarded): + assert 'success' in self.conf( + { + "127.0.0.1:7081": { + "forwarded": forwarded, + "pass": "applications/forwarded_header", + }, + "[::1]:7082": { + "forwarded": forwarded, + "pass": "applications/forwarded_header", + }, + }, + 'listeners', + ), 'listeners configure' + + def get_fwd(self, sock_type='ipv4', xff=None, xfp=None): + port = 7081 if sock_type == 'ipv4' else 7082 + + headers = {'Connection': 'close'} + + if xff is not None: + headers['X-Forwarded-For'] = xff + + if xfp is not None: + headers['X-Forwarded-Proto'] = xfp + + return self.get(sock_type=sock_type, port=port, headers=headers)[ + 'headers' + ] + + def get_addr(self, *args, **kwargs): + return self.get_fwd(*args, **kwargs)['Remote-Addr'] + + def get_scheme(self, *args, **kwargs): + return self.get_fwd(*args, **kwargs)['Url-Scheme'] + + def setup_method(self): + self.load('forwarded_header') + + def test_forwarded_header_single_ip(self): + self.forwarded_header( + { + 'client_ip': 'X-Forwarded-For', + 'protocol': 'X-Forwarded-Proto', + 'source': '123.123.123.123', + } + ) + + resp = self.get_fwd(xff='1.1.1.1', xfp='https') + assert resp['Remote-Addr'] == '127.0.0.1', 'both headers addr' + assert resp['Url-Scheme'] == 'http', 'both headers proto' + + assert self.get_addr() == '127.0.0.1', 'ipv4 default addr' + assert self.get_addr('ipv6') == '::1', 'ipv6 default addr' + assert self.get_addr(xff='1.1.1.1') == '127.0.0.1', 'bad source' + assert self.get_addr(xff='blah') == '127.0.0.1', 'bad xff' + assert self.get_addr('ipv6', '1.1.1.1') == '::1', 'bad source ipv6' + + assert self.get_scheme() == 'http', 'ipv4 default proto' + assert self.get_scheme('ipv6') == 'http', 'ipv6 default proto' + assert self.get_scheme(xfp='https') == 'http', 'bad proto' + assert self.get_scheme(xfp='blah') == 'http', 'bad xfp' + assert self.get_scheme('ipv6', xfp='https') == 'http', 'bad proto ipv6' + + self.forwarded_header( + { + 'client_ip': 'X-Forwarded-For', + 'protocol': 'X-Forwarded-Proto', + 'source': '127.0.0.1', + } + ) + + resp = self.get_fwd(xff='1.1.1.1', xfp='https') + assert resp['Remote-Addr'] == '1.1.1.1', 'both headers addr 2' + assert resp['Url-Scheme'] == 'https', 'both headers proto 2' + + assert self.get_addr() == '127.0.0.1', 'ipv4 default addr 2' + assert self.get_addr('ipv6') == '::1', 'ipv6 default addr 2' + assert self.get_addr(xff='1.1.1.1') == '1.1.1.1', 'xff replace' + assert self.get_addr('ipv6', '1.1.1.1') == '::1', 'bad source ipv6 2' + + assert self.get_scheme() == 'http', 'ipv4 default proto 2' + assert self.get_scheme('ipv6') == 'http', 'ipv6 default proto 2' + assert self.get_scheme(xfp='https') == 'https', 'xfp replace' + assert self.get_scheme(xfp='on') == 'https', 'xfp replace 2' + assert ( + self.get_scheme('ipv6', xfp='https') == 'http' + ), 'bad proto ipv6 2' + + self.forwarded_header( + { + 'client_ip': 'X-Forwarded-For', + 'protocol': 'X-Forwarded-Proto', + 'source': '!127.0.0.1', + } + ) + + assert self.get_addr(xff='1.1.1.1') == '127.0.0.1', 'bad source 3' + assert self.get_addr('ipv6', '1.1.1.1') == '1.1.1.1', 'xff replace 2' + assert self.get_scheme(xfp='https') == 'http', 'bad proto 2' + assert self.get_scheme('ipv6', xfp='https') == 'https', 'xfp replace 3' + + def test_forwarded_header_ipv4(self): + self.forwarded_header( + { + 'client_ip': 'X-Forwarded-For', + 'protocol': 'X-Forwarded-Proto', + 'source': '127.0.0.1', + } + ) + + assert ( + self.get_addr(xff='8.8.8.8, 84.23.23.11') == '84.23.23.11' + ), 'xff replace' + assert ( + self.get_addr(xff='8.8.8.8, 84.23.23.11, 127.0.0.1') == '127.0.0.1' + ), 'xff replace 2' + assert ( + self.get_addr(xff=['8.8.8.8', '127.0.0.1, 10.0.1.1']) == '10.0.1.1' + ), 'xff replace multi' + + assert self.get_scheme(xfp='http, https') == 'http', 'xfp replace' + assert ( + self.get_scheme(xfp='http, https, http') == 'http' + ), 'xfp replace 2' + assert ( + self.get_scheme(xfp=['http, https', 'http', 'https']) == 'http' + ), 'xfp replace multi' + + def test_forwarded_header_ipv6(self): + self.forwarded_header( + { + 'client_ip': 'X-Forwarded-For', + 'protocol': 'X-Forwarded-Proto', + 'source': '::1', + } + ) + + assert self.get_addr(xff='1.1.1.1') == '127.0.0.1', 'bad source ipv4' + + for ip in [ + 'f607:7403:1e4b:6c66:33b2:843f:2517:da27', + '2001:db8:3c4d:15::1a2f:1a2b', + '2001::3c4d:15:1a2f:1a2b', + '::11.22.33.44', + ]: + assert self.get_addr('ipv6', ip) == ip, 'replace' + + assert self.get_scheme(xfp='https') == 'http', 'bad source ipv4' + + for proto in ['http', 'https']: + assert self.get_scheme('ipv6', xfp=proto) == proto, 'replace' + + def test_forwarded_header_recursive(self): + self.forwarded_header( + { + 'client_ip': 'X-Forwarded-For', + 'recursive': True, + 'source': ['127.0.0.1', '10.50.0.17', '10.5.2.1'], + } + ) + + assert self.get_addr(xff='1.1.1.1') == '1.1.1.1', 'xff chain' + assert ( + self.get_addr(xff='1.1.1.1, 10.5.2.1') == '1.1.1.1' + ), 'xff chain 2' + assert ( + self.get_addr(xff='8.8.8.8, 1.1.1.1, 10.5.2.1') == '1.1.1.1' + ), 'xff chain 3' + assert ( + self.get_addr(xff='10.50.0.17, 10.5.2.1, 10.5.2.1') == '10.50.0.17' + ), 'xff chain 4' + assert ( + self.get_addr(xff=['8.8.8.8', '1.1.1.1, 127.0.0.1']) == '1.1.1.1' + ), 'xff replace multi' + assert ( + self.get_addr(xff=['8.8.8.8', '1.1.1.1, 127.0.0.1', '10.5.2.1']) + == '1.1.1.1' + ), 'xff replace multi 2' + assert ( + self.get_addr(xff=['10.5.2.1', '10.50.0.17, 1.1.1.1', '10.5.2.1']) + == '1.1.1.1' + ), 'xff replace multi 3' + assert ( + self.get_addr( + xff='8.8.8.8, 2001:db8:3c4d:15::1a2f:1a2b, 127.0.0.1' + ) + == '2001:db8:3c4d:15::1a2f:1a2b' + ), 'xff chain ipv6' + + def test_forwarded_header_case_insensitive(self): + self.forwarded_header( + { + 'client_ip': 'x-forwarded-for', + 'protocol': 'x-forwarded-proto', + 'source': '127.0.0.1', + } + ) + + assert self.get_addr() == '127.0.0.1', 'ipv4 default addr' + assert self.get_addr('ipv6') == '::1', 'ipv6 default addr' + assert self.get_addr(xff='1.1.1.1') == '1.1.1.1', 'replace' + + assert self.get_scheme() == 'http', 'ipv4 default proto' + assert self.get_scheme('ipv6') == 'http', 'ipv6 default proto' + assert self.get_scheme(xfp='https') == 'https', 'replace 1' + assert self.get_scheme(xfp='oN') == 'https', 'replace 2' + + def test_forwarded_header_source_empty(self): + self.forwarded_header( + { + 'client_ip': 'X-Forwarded-For', + 'protocol': 'X-Forwarded-Proto', + 'source': [], + } + ) + + assert self.get_addr(xff='1.1.1.1') == '127.0.0.1', 'empty source xff' + assert self.get_scheme(xfp='https') == 'http', 'empty source xfp' + + def test_forwarded_header_source_range(self): + self.forwarded_header( + { + 'client_ip': 'X-Forwarded-For', + 'protocol': 'X-Forwarded-Proto', + 'source': '127.0.0.0-127.0.0.1', + } + ) + + assert self.get_addr(xff='1.1.1.1') == '1.1.1.1', 'source range' + assert self.get_addr('ipv6', '1.1.1.1') == '::1', 'source range 2' + + def test_forwarded_header_invalid(self): + assert 'error' in self.conf( + { + "127.0.0.1:7081": { + "forwarded": {"source": '127.0.0.1'}, + "pass": "applications/forwarded_header", + } + }, + 'listeners', + ), 'invalid forward' + + def check_invalid_source(source): + assert 'error' in self.conf( + { + "127.0.0.1:7081": { + "forwarded": { + "client_ip": "X-Forwarded-For", + "source": source, + }, + "pass": "applications/forwarded_header", + } + }, + 'listeners', + ), 'invalid source' + + check_invalid_source(None) + check_invalid_source('a') + check_invalid_source(['a']) diff --git a/test/test_go_application.py b/test/test_go_application.py index c8cf3e53..a746c6f4 100644 --- a/test/test_go_application.py +++ b/test/test_go_application.py @@ -95,7 +95,6 @@ class TestGoApplication(TestApplicationGo): headers={ 'Host': 'localhost', 'Connection': 'keep-alive', - 'Content-Type': 'text/html', }, start=True, body=body, @@ -105,16 +104,7 @@ class TestGoApplication(TestApplicationGo): assert resp['body'] == body, 'keep-alive 1' body = '0123456789' - resp = self.post( - headers={ - 'Host': 'localhost', - 'Content-Type': 'text/html', - 'Connection': 'close', - }, - sock=sock, - body=body, - ) - + resp = self.post(sock=sock, body=body) assert resp['body'] == body, 'keep-alive 2' def test_go_application_cookies(self): diff --git a/test/test_node_application.py b/test/test_node_application.py index fc722582..c26c72d0 100644 --- a/test/test_node_application.py +++ b/test/test_node_application.py @@ -124,7 +124,6 @@ class TestNodeApplication(TestApplicationNode): headers={ 'Host': 'localhost', 'Connection': 'keep-alive', - 'Content-Type': 'text/html', }, start=True, body=body, @@ -134,15 +133,7 @@ class TestNodeApplication(TestApplicationNode): assert resp['body'] == '0123456789' * 500, 'keep-alive 1' body = '0123456789' - resp = self.post( - headers={ - 'Host': 'localhost', - 'Connection': 'close', - 'Content-Type': 'text/html', - }, - sock=sock, - body=body, - ) + resp = self.post(sock=sock, body=body) assert resp['body'] == body, 'keep-alive 2' diff --git a/test/test_php_application.py b/test/test_php_application.py index 606ac723..f1dcc995 100644 --- a/test/test_php_application.py +++ b/test/test_php_application.py @@ -226,7 +226,6 @@ opcache.preload_user = %(user)s headers={ 'Host': 'localhost', 'Connection': 'keep-alive', - 'Content-Type': 'text/html', }, start=True, body=body, @@ -236,15 +235,7 @@ opcache.preload_user = %(user)s assert resp['body'] == body, 'keep-alive 1' body = '0123456789' - resp = self.post( - headers={ - 'Host': 'localhost', - 'Connection': 'close', - 'Content-Type': 'text/html', - }, - sock=sock, - body=body, - ) + resp = self.post(sock=sock, body=body) assert resp['body'] == body, 'keep-alive 2' diff --git a/test/test_proxy.py b/test/test_proxy.py index 68ae2394..b0d471e4 100644 --- a/test/test_proxy.py +++ b/test/test_proxy.py @@ -370,7 +370,6 @@ Content-Length: 10 resp = self.post_http10( headers={ 'Host': 'localhost', - 'Content-Type': 'text/html', 'Content-Length': str(len(body)), 'X-Parts': '2', 'X-Delay': '1', @@ -384,7 +383,6 @@ Content-Length: 10 resp = self.post_http10( headers={ 'Host': 'localhost', - 'Content-Type': 'text/html', 'Content-Length': str(len(body)), 'X-Parts': '2', 'X-Delay': '1', @@ -403,7 +401,6 @@ Content-Length: 10 _, sock = self.post_http10( headers={ 'Host': 'localhost', - 'Content-Type': 'text/html', 'Content-Length': '10000', 'X-Parts': '3', 'X-Delay': '1', @@ -419,7 +416,6 @@ Content-Length: 10 _, sock = self.post_http10( headers={ 'Host': 'localhost', - 'Content-Type': 'text/html', 'Content-Length': '10000', 'X-Parts': '3', 'X-Delay': '1', diff --git a/test/test_python_application.py b/test/test_python_application.py index befbd4d8..2ea9a22e 100644 --- a/test/test_python_application.py +++ b/test/test_python_application.py @@ -138,14 +138,7 @@ custom-header: BLAH def test_python_application_ctx_iter_atexit(self): self.load('ctx_iter_atexit') - resp = self.post( - headers={ - 'Host': 'localhost', - 'Connection': 'close', - 'Content-Type': 'text/html', - }, - body='0123456789', - ) + resp = self.post(body='0123456789') assert resp['status'] == 200, 'ctx iter status' assert resp['body'] == '0123456789', 'ctx iter body' @@ -166,7 +159,6 @@ custom-header: BLAH headers={ 'Host': 'localhost', 'Connection': 'keep-alive', - 'Content-Type': 'text/html', }, start=True, body=body, @@ -176,15 +168,7 @@ custom-header: BLAH assert resp['body'] == body, 'keep-alive 1' body = '0123456789' - resp = self.post( - headers={ - 'Host': 'localhost', - 'Connection': 'close', - 'Content-Type': 'text/html', - }, - sock=sock, - body=body, - ) + resp = self.post(sock=sock, body=body) assert resp['body'] == body, 'keep-alive 2' @@ -202,7 +186,6 @@ custom-header: BLAH headers={ 'Host': 'localhost', 'Connection': 'keep-alive', - 'Content-Type': 'text/html', }, start=True, body=body, @@ -220,7 +203,6 @@ custom-header: BLAH headers={ 'Host': 'localhost', 'Connection': 'keep-alive', - 'Content-Type': 'text/html', }, start=True, sock=socks[i], @@ -233,15 +215,7 @@ custom-header: BLAH self.load('mirror', processes=i + 1) for i in range(conns): - resp = self.post( - headers={ - 'Host': 'localhost', - 'Connection': 'close', - 'Content-Type': 'text/html', - }, - sock=socks[i], - body=body, - ) + resp = self.post(sock=socks[i], body=body) assert resp['body'] == body, 'keep-alive close' @@ -258,7 +232,6 @@ custom-header: BLAH headers={ 'Host': 'localhost', 'Connection': 'keep-alive', - 'Content-Type': 'text/html', }, start=True, body=body, @@ -271,16 +244,7 @@ custom-header: BLAH assert self.get()['status'] == 200, 'init' - (resp, sock) = self.post( - headers={ - 'Host': 'localhost', - 'Connection': 'close', - 'Content-Type': 'text/html', - }, - start=True, - sock=sock, - body=body, - ) + (resp, sock) = self.post(start=True, sock=sock, body=body) assert resp['status'] == 200, 'reconfigure 2 keep-alive 2' assert resp['body'] == '', 'reconfigure 2 keep-alive 2 body' diff --git a/test/test_routing.py b/test/test_routing.py index fda429a4..3649b37c 100644 --- a/test/test_routing.py +++ b/test/test_routing.py @@ -1723,18 +1723,26 @@ class TestRouting(TestApplicationPython): addr = temp_dir + '/sock' assert 'success' in self.conf( - {"unix:" + addr: {"pass": "routes"}}, 'listeners' + { + "127.0.0.1:7081": {"pass": "routes"}, + "unix:" + addr: {"pass": "routes"}, + }, + 'listeners', ), 'source listeners configure' self.route_match({"source": "!0.0.0.0/0"}) assert ( self.get(sock_type='unix', addr=addr)['status'] == 200 - ), 'unix ipv4' + ), 'unix ipv4 neg' self.route_match({"source": "!::/0"}) assert ( self.get(sock_type='unix', addr=addr)['status'] == 200 - ), 'unix ipv6' + ), 'unix ipv6 neg' + + self.route_match({"source": "unix"}) + assert self.get(port=7081)['status'] == 404, 'unix ipv4' + assert self.get(sock_type='unix', addr=addr)['status'] == 200, 'unix' def test_routes_match_source(self): self.route_match({"source": "::"}) diff --git a/test/test_ruby_application.py b/test/test_ruby_application.py index 95c75d47..83af39be 100644 --- a/test/test_ruby_application.py +++ b/test/test_ruby_application.py @@ -44,7 +44,7 @@ class TestRubyApplication(TestApplicationRuby): 'Request-Method': 'POST', 'Request-Uri': '/', 'Http-Host': 'localhost', - 'Script-Name': 'config.ru', + 'Script-Name': '', 'Server-Protocol': 'HTTP/1.1', 'Custom-Header': 'blah', 'Rack-Version': '13', @@ -347,7 +347,6 @@ class TestRubyApplication(TestApplicationRuby): headers={ 'Host': 'localhost', 'Connection': 'keep-alive', - 'Content-Type': 'text/html', }, start=True, body=body, @@ -357,15 +356,7 @@ class TestRubyApplication(TestApplicationRuby): assert resp['body'] == body, 'keep-alive 1' body = '0123456789' - resp = self.post( - headers={ - 'Host': 'localhost', - 'Connection': 'close', - 'Content-Type': 'text/html', - }, - sock=sock, - body=body, - ) + resp = self.post(sock=sock, body=body) assert resp['body'] == body, 'keep-alive 2' diff --git a/test/test_static_chroot.py b/test/test_static_chroot.py index b896a9b9..e33a181c 100644 --- a/test/test_static_chroot.py +++ b/test/test_static_chroot.py @@ -14,8 +14,7 @@ class TestStaticChroot(TestApplicationProto): Path(temp_dir + '/assets/index.html').write_text('0123456789') Path(temp_dir + '/assets/dir/file').write_text('blah') - test = Path(__file__) - self.test_path = '/' + test.parent.name + '/' + test.name + self.test_path = '/' + os.path.relpath(Path(__file__)) self._load_conf( { @@ -39,26 +38,18 @@ class TestStaticChroot(TestApplicationProto): assert self.get(url='/dir/file')['status'] == 200, 'default chroot' assert self.get(url='/index.html')['status'] == 200, 'default chroot 2' - assert 'success' in self.conf( - { - "share": temp_dir + "/assets$uri", - "chroot": temp_dir + "/assets/dir", - }, - 'routes/0/action', - ), 'configure chroot' + assert 'success' in self.update_action( + temp_dir + "/assets$uri", temp_dir + "/assets/dir" + ) assert self.get(url='/dir/file')['status'] == 200, 'chroot' assert self.get(url='/index.html')['status'] == 403, 'chroot 403 2' assert self.get(url='/file')['status'] == 403, 'chroot 403' def test_share_chroot_array(self, temp_dir): - assert 'success' in self.conf( - { - "share": ["/blah", temp_dir + "/assets$uri"], - "chroot": temp_dir + "/assets/dir", - }, - 'routes/0/action', - ), 'configure share array' + assert 'success' in self.update_action( + ["/blah", temp_dir + "/assets$uri"], temp_dir + "/assets/dir" + ) assert self.get(url='/dir/file')['status'] == 200, 'share array' assert 'success' in self.update_action( @@ -66,13 +57,9 @@ class TestStaticChroot(TestApplicationProto): ) assert self.get_custom('/dir/file', 'dir') == 200, 'array variable' - assert 'success' in self.conf( - { - "share": ["/blah", "/blah2"], - "chroot": temp_dir + "/assets/dir", - }, - 'routes/0/action', - ), 'configure share array bad' + assert 'success' in self.update_action( + ["/blah", "/blah2"], temp_dir + "/assets/dir" + ) assert self.get()['status'] != 200, 'share array bad' def test_static_chroot_permission(self, is_su, temp_dir): @@ -81,58 +68,30 @@ class TestStaticChroot(TestApplicationProto): os.chmod(temp_dir + '/assets/dir', 0o100) - assert 'success' in self.conf( - { - "share": temp_dir + "/assets$uri", - "chroot": temp_dir + "/assets/dir", - }, - 'routes/0/action', + assert 'success' in self.update_action( + temp_dir + "/assets$uri", temp_dir + "/assets/dir" ), 'configure chroot' assert self.get(url='/dir/file')['status'] == 200, 'chroot' def test_static_chroot_empty(self, temp_dir): - assert 'success' in self.conf( - {"share": temp_dir + "/assets$uri", "chroot": ""}, - 'routes/0/action', - ), 'configure chroot empty absolute' - - assert ( - self.get(url='/dir/file')['status'] == 200 - ), 'chroot empty absolute' - - assert 'success' in self.conf( - {"share": ".$uri", "chroot": ""}, - 'routes/0/action', - ), 'configure chroot empty relative' + assert 'success' in self.update_action(temp_dir + "/assets$uri", "") + assert self.get(url='/dir/file')['status'] == 200, 'empty absolute' - assert ( - self.get(url=self.test_path)['status'] == 200 - ), 'chroot empty relative' + assert 'success' in self.update_action(".$uri", "") + assert self.get(url=self.test_path)['status'] == 200, 'empty relative' def test_static_chroot_relative(self, is_su, temp_dir): if is_su: pytest.skip('does\'t work under root') - assert 'success' in self.conf( - {"share": temp_dir + "/assets$uri", "chroot": "."}, - 'routes/0/action', - ), 'configure relative chroot' - + assert 'success' in self.update_action(temp_dir + "/assets$uri", ".") assert self.get(url='/dir/file')['status'] == 403, 'relative chroot' - assert 'success' in self.conf( - {"share": ".$uri"}, - 'routes/0/action', - ), 'configure relative share' - + assert 'success' in self.conf({"share": ".$uri"}, 'routes/0/action') assert self.get(url=self.test_path)['status'] == 200, 'relative share' - assert 'success' in self.conf( - {"share": ".$uri", "chroot": "."}, - 'routes/0/action', - ), 'configure relative' - + assert 'success' in self.update_action(".$uri", ".") assert self.get(url=self.test_path)['status'] == 200, 'relative' def test_static_chroot_variables(self, temp_dir): @@ -150,64 +109,41 @@ class TestStaticChroot(TestApplicationProto): assert 'success' in self.update_action( temp_dir + '/assets/dir/$host', '$uri/assets/dir' ) - assert self.get_custom(temp_dir, 'file') == 200 def test_static_chroot_variables_buildin_mid(self, temp_dir): assert 'success' in self.update_action( temp_dir + '/assets$uri', temp_dir + '/$host/dir' ) - assert self.get_custom('/dir/file', 'assets') == 200 def test_static_chroot_variables_buildin_end(self, temp_dir): assert 'success' in self.update_action( temp_dir + '/assets$uri', temp_dir + '/assets/$host' ) - assert self.get_custom('/dir/file', 'dir') == 200 def test_static_chroot_slash(self, temp_dir): - assert 'success' in self.conf( - { - "share": temp_dir + "/assets$uri", - "chroot": temp_dir + "/assets/dir/", - }, - 'routes/0/action', - ), 'configure chroot slash end' - + assert 'success' in self.update_action( + temp_dir + "/assets$uri", temp_dir + "/assets/dir/" + ) assert self.get(url='/dir/file')['status'] == 200, 'slash end' assert self.get(url='/dirxfile')['status'] == 403, 'slash end bad' - assert 'success' in self.conf( - { - "share": temp_dir + "/assets$uri", - "chroot": temp_dir + "/assets/dir", - }, - 'routes/0/action', - ), 'configure chroot no slash end' - + assert 'success' in self.update_action( + temp_dir + "/assets$uri", temp_dir + "/assets/dir" + ) assert self.get(url='/dir/file')['status'] == 200, 'no slash end' - assert 'success' in self.conf( - { - "share": temp_dir + "/assets$uri", - "chroot": temp_dir + "/assets/dir/", - }, - 'routes/0/action', - ), 'configure chroot slash end 2' - + assert 'success' in self.update_action( + temp_dir + "/assets$uri", temp_dir + "/assets/dir/" + ) assert self.get(url='/dir/file')['status'] == 200, 'slash end 2' assert self.get(url='/dirxfile')['status'] == 403, 'slash end 2 bad' - assert 'success' in self.conf( - { - "share": temp_dir + "///assets/////$uri", - "chroot": temp_dir + "//assets////dir///", - }, - 'routes/0/action', - ), 'configure chroot multiple slashes' - + assert 'success' in self.update_action( + temp_dir + "///assets/////$uri", temp_dir + "//assets////dir///" + ) assert self.get(url='/dir/file')['status'] == 200, 'multiple slashes' def test_static_chroot_invalid(self, temp_dir): diff --git a/test/test_status.py b/test/test_status.py new file mode 100644 index 00000000..214072d4 --- /dev/null +++ b/test/test_status.py @@ -0,0 +1,223 @@ +import time + +import pytest +from unit.applications.lang.python import TestApplicationPython +from unit.option import option +from unit.status import Status + + +class TestStatus(TestApplicationPython): + prerequisites = {'modules': {'python': 'any'}} + + def test_status(self): + assert 'error' in self.conf_delete('/status'), 'DELETE method' + + def test_status_requests(self, skip_alert): + skip_alert(r'Python failed to import module "blah"') + + assert 'success' in self.conf( + { + "listeners": { + "*:7080": {"pass": "routes"}, + "*:7081": {"pass": "applications/empty"}, + "*:7082": {"pass": "applications/blah"}, + }, + "routes": [{"action": {"return": 200}}], + "applications": { + "empty": { + "type": self.get_application_type(), + "processes": {"spare": 0}, + "path": option.test_dir + '/python/empty', + "working_directory": option.test_dir + '/python/empty', + "module": "wsgi", + }, + "blah": { + "type": self.get_application_type(), + "processes": {"spare": 0}, + "module": "blah", + }, + }, + }, + ) + + Status.init() + + assert self.get()['status'] == 200 + assert Status.get('/requests/total') == 1, '2xx' + + assert self.get(port=7081)['status'] == 200 + assert Status.get('/requests/total') == 2, '2xx app' + + assert ( + self.get(headers={'Host': '/', 'Connection': 'close'})['status'] + == 400 + ) + assert Status.get('/requests/total') == 3, '4xx' + + assert self.get(port=7082)['status'] == 503 + assert Status.get('/requests/total') == 4, '5xx' + + self.http( + b"""GET / HTTP/1.1 +Host: localhost + +GET / HTTP/1.1 +Host: localhost +Connection: close + +""", + raw=True, + ) + assert Status.get('/requests/total') == 6, 'pipeline' + + (_, sock) = self.get(port=7081, no_recv=True, start=True) + + time.sleep(1) + + assert Status.get('/requests/total') == 7, 'no receive' + + sock.close() + + def test_status_connections(self): + def check_connections(accepted, active, idle, closed): + Status.get('/connections') == { + 'accepted': accepted, + 'active': active, + 'idle': idle, + 'closed': closed, + } + + assert 'success' in self.conf( + { + "listeners": { + "*:7080": {"pass": "routes"}, + "*:7081": {"pass": "applications/delayed"}, + }, + "routes": [{"action": {"return": 200}}], + "applications": { + "delayed": { + "type": self.get_application_type(), + "processes": {"spare": 0}, + "path": option.test_dir + "/python/delayed", + "working_directory": option.test_dir + + "/python/delayed", + "module": "wsgi", + }, + }, + }, + ) + + Status.init() + + # accepted, closed + + assert self.get()['status'] == 200 + check_connections(1, 0, 0, 1) + + # idle + + _, sock = self.http(b'', start=True, raw=True, no_recv=True) + check_connections(2, 0, 1, 1) + + self.get(sock=sock) + check_connections(2, 0, 0, 2) + + # active + + (_, sock) = self.get( + headers={ + 'Host': 'localhost', + 'X-Delay': '2', + 'Connection': 'close', + }, + port=7081, + start=True, + read_timeout=1, + ) + check_connections(3, 1, 0, 2) + + self.get(sock=sock) + check_connections(3, 0, 0, 3) + + def test_status_applications(self): + def check_applications(expert): + apps = list(self.conf_get('/status/applications').keys()).sort() + assert apps == expert.sort() + + def check_application(name, running, starting, idle, active): + Status.get('/applications/' + name) == { + 'processes': { + 'running': running, + 'starting': starting, + 'idle': idle, + }, + 'requests': {'active': active}, + } + + self.load('delayed') + Status.init() + + check_applications(['delayed']) + check_application('delayed', 0, 0, 0, 0) + + # idle + + assert self.get()['status'] == 200 + check_application('delayed', 1, 0, 1, 0) + + assert 'success' in self.conf('4', 'applications/delayed/processes') + check_application('delayed', 4, 0, 4, 0) + + # active + + (_, sock) = self.get( + headers={ + 'Host': 'localhost', + 'X-Delay': '2', + 'Connection': 'close', + }, + start=True, + read_timeout=1, + ) + check_application('delayed', 4, 0, 3, 1) + sock.close() + + # starting + + assert 'success' in self.conf( + { + "listeners": { + "*:7080": {"pass": "applications/restart"}, + "*:7081": {"pass": "applications/delayed"}, + }, + "routes": [], + "applications": { + "restart": { + "type": self.get_application_type(), + "processes": {"spare": 0}, + "path": option.test_dir + "/python/restart", + "working_directory": option.test_dir + + "/python/restart", + "module": "longstart", + }, + "delayed": { + "type": self.get_application_type(), + "processes": {"spare": 0}, + "path": option.test_dir + "/python/delayed", + "working_directory": option.test_dir + + "/python/delayed", + "module": "wsgi", + }, + }, + }, + ) + Status.init() + + check_applications(['delayed', 'restart']) + check_application('restart', 0, 0, 0, 0) + check_application('delayed', 0, 0, 0, 0) + + self.get(read_timeout=1) + + check_application('restart', 0, 1, 0, 1) + check_application('delayed', 0, 0, 0, 0) diff --git a/test/test_status_tls.py b/test/test_status_tls.py new file mode 100644 index 00000000..dc3d68da --- /dev/null +++ b/test/test_status_tls.py @@ -0,0 +1,30 @@ +from unit.applications.tls import TestApplicationTLS +from unit.status import Status + + +class TestStatusTLS(TestApplicationTLS): + prerequisites = {'modules': {'openssl': 'any'}} + + def test_status_tls_requests(self): + self.certificate() + + assert 'success' in self.conf( + { + "listeners": { + "*:7080": {"pass": "routes"}, + "*:7081": { + "pass": "routes", + "tls": {"certificate": "default"}, + }, + }, + "routes": [{"action": {"return": 200}}], + "applications": {}, + } + ) + + Status.init() + + assert self.get()['status'] == 200 + assert self.get_ssl(port=7081)['status'] == 200 + + assert Status.get('/requests/total') == 2 diff --git a/test/test_tls.py b/test/test_tls.py index 56ee8298..d4edcbd3 100644 --- a/test/test_tls.py +++ b/test/test_tls.py @@ -1,5 +1,4 @@ import io -import re import ssl import subprocess import time @@ -13,7 +12,7 @@ class TestTLS(TestApplicationTLS): prerequisites = {'modules': {'python': 'any', 'openssl': 'any'}} def openssl_date_to_sec_epoch(self, date): - return self.date_to_sec_epoch(date, '%b %d %H:%M:%S %Y %Z') + return self.date_to_sec_epoch(date, '%b %d %X %Y %Z') def add_tls(self, application='empty', cert='default', port=7080): assert 'success' in self.conf( @@ -506,7 +505,6 @@ basicConstraints = critical,CA:TRUE""" headers={ 'Host': 'localhost', 'Connection': 'keep-alive', - 'Content-Type': 'text/html', }, start=True, body='0123456789', @@ -519,7 +517,6 @@ basicConstraints = critical,CA:TRUE""" headers={ 'Host': 'localhost', 'Connection': 'close', - 'Content-Type': 'text/html', }, sock=sock, body='0123456789', @@ -571,9 +568,7 @@ basicConstraints = critical,CA:TRUE""" assert 'success' in self.conf_delete('/certificates/default') try: - resp = self.get_ssl( - headers={'Host': 'localhost', 'Connection': 'close'}, sock=sock - ) + resp = self.get_ssl(sock=sock) except KeyboardInterrupt: raise @@ -606,7 +601,6 @@ basicConstraints = critical,CA:TRUE""" headers={ 'Host': 'localhost', 'Connection': 'keep-alive', - 'Content-Type': 'text/html', }, start=True, body='0123456789', @@ -617,23 +611,13 @@ basicConstraints = critical,CA:TRUE""" subprocess.check_output(['kill', '-9', app_id]) - skip_alert(r'process .* %s.* exited on signal 9' % app_id) + skip_alert(r'process %s exited on signal 9' % app_id) self.wait_for_record( - re.compile( - r' (?!' + app_id + r'#)(\d+)#\d+ "mirror" application started' - ) + r' (?!' + app_id + r'#)(\d+)#\d+ "mirror" application started' ) - resp = self.post_ssl( - headers={ - 'Host': 'localhost', - 'Connection': 'close', - 'Content-Type': 'text/html', - }, - sock=sock, - body='0123456789', - ) + resp = self.post_ssl(sock=sock, body='0123456789') assert resp['status'] == 200, 'application respawn status' assert resp['body'] == '0123456789', 'application respawn body' diff --git a/test/test_tls_sni.py b/test/test_tls_sni.py index dbd5d900..44cc21e1 100644 --- a/test/test_tls_sni.py +++ b/test/test_tls_sni.py @@ -18,7 +18,7 @@ class TestTLSSNI(TestApplicationTLS): ) def openssl_date_to_sec_epoch(self, date): - return self.date_to_sec_epoch(date, '%b %d %H:%M:%S %Y %Z') + return self.date_to_sec_epoch(date, '%b %d %X %Y %Z') def add_tls(self, cert='default'): assert 'success' in self.conf( diff --git a/test/test_unix_abstract.py b/test/test_unix_abstract.py new file mode 100644 index 00000000..195b0aa7 --- /dev/null +++ b/test/test_unix_abstract.py @@ -0,0 +1,109 @@ +from unit.applications.lang.python import TestApplicationPython +from unit.option import option + + +class TestUnixAbstract(TestApplicationPython): + prerequisites = { + 'modules': {'python': 'any'}, + 'features': ['unix_abstract'], + } + + def test_unix_abstract_source(self): + addr = '\0sock' + + def source(source): + assert 'success' in self.conf( + '"' + source + '"', 'routes/0/match/source' + ) + + assert 'success' in self.conf( + { + "listeners": { + "127.0.0.1:7080": {"pass": "routes"}, + "unix:@" + addr[1:]: {"pass": "routes"}, + }, + "routes": [ + { + "match": {"source": "!0.0.0.0/0"}, + "action": {"return": 200}, + } + ], + "applications": {}, + } + ) + + assert ( + self.get(sock_type='unix', addr=addr)['status'] == 200 + ), 'neg ipv4' + + source("!::/0") + assert ( + self.get(sock_type='unix', addr=addr)['status'] == 200 + ), 'neg ipv6' + + source("unix") + assert self.get()['status'] == 404, 'ipv4' + assert self.get(sock_type='unix', addr=addr)['status'] == 200, 'unix' + + def test_unix_abstract_client_ip(self): + def get_xff(xff, sock_type='ipv4'): + address = { + 'ipv4': ('127.0.0.1', 7080), + 'ipv6': ('::1', 7081), + 'unix': ('\0sock', None), + } + (addr, port) = address[sock_type] + + return self.get( + sock_type=sock_type, + addr=addr, + port=port, + headers={'Connection': 'close', 'X-Forwarded-For': xff}, + )['body'] + + assert 'success' in self.conf( + { + "listeners": { + "127.0.0.1:7080": { + "client_ip": { + "header": "X-Forwarded-For", + "source": "unix", + }, + "pass": "applications/client_ip", + }, + "[::1]:7081": { + "client_ip": { + "header": "X-Forwarded-For", + "source": "unix", + }, + "pass": "applications/client_ip", + }, + "unix:@sock": { + "client_ip": { + "header": "X-Forwarded-For", + "source": "unix", + }, + "pass": "applications/client_ip", + }, + }, + "applications": { + "client_ip": { + "type": self.get_application_type(), + "processes": {"spare": 0}, + "path": option.test_dir + "/python/client_ip", + "working_directory": option.test_dir + + "/python/client_ip", + "module": "wsgi", + } + }, + } + ) + + assert get_xff('1.1.1.1') == '127.0.0.1', 'bad source ipv4' + assert get_xff('1.1.1.1', 'ipv6') == '::1', 'bad source ipv6' + + for ip in [ + '1.1.1.1', + '::11.22.33.44', + ]: + assert get_xff(ip, 'unix') == ip, 'replace' diff --git a/test/test_variables.py b/test/test_variables.py index 71553685..2ddfdc0a 100644 --- a/test/test_variables.py +++ b/test/test_variables.py @@ -18,6 +18,11 @@ class TestVariables(TestApplicationProto): "GETGET": [{"action": {"return": 207}}], "localhost": [{"action": {"return": 208}}], "9?q#a": [{"action": {"return": 209}}], + "blah": [{"action": {"return": 210}}], + "127.0.0.1": [{"action": {"return": 211}}], + "::1": [{"action": {"return": 212}}], + "referer-value": [{"action": {"return": 213}}], + "MSIE": [{"action": {"return": 214}}], }, }, ), 'configure routes' @@ -62,6 +67,74 @@ class TestVariables(TestApplicationProto): check_host('www.localhost', 404) check_host('localhost1', 404) + def test_variables_remote_addr(self): + self.conf_routes("\"routes/$remote_addr\"") + assert self.get()['status'] == 211 + + assert 'success' in self.conf( + {"[::1]:7080": {"pass": "routes/$remote_addr"}}, 'listeners' + ) + assert self.get(sock_type='ipv6')['status'] == 212 + + def test_variables_header_referer(self): + self.conf_routes("\"routes/$header_referer\"") + + def check_referer(referer, status=213): + assert ( + self.get( + headers={ + 'Host': 'localhost', + 'Connection': 'close', + 'Referer': referer, + } + )['status'] + == status + ) + + check_referer('referer-value') + check_referer('', 404) + check_referer('no', 404) + + def test_variables_header_user_agent(self): + self.conf_routes("\"routes/$header_user_agent\"") + + def check_user_agent(user_agent, status=214): + assert ( + self.get( + headers={ + 'Host': 'localhost', + 'Connection': 'close', + 'User-Agent': user_agent, + } + )['status'] + == status + ) + + check_user_agent('MSIE') + check_user_agent('', 404) + check_user_agent('no', 404) + + def test_variables_dollar(self): + assert 'success' in self.conf( + { + "listeners": {"*:7080": {"pass": "routes"}}, + "routes": [{"action": {"return": 301}}], + } + ) + + def check_dollar(location, expect): + assert 'success' in self.conf( + '"' + location + '"', + 'routes/0/action/location', + ) + assert self.get()['headers']['Location'] == expect + + check_dollar( + 'https://${host}${uri}path${dollar}dollar', + 'https://localhost/path$dollar', + ) + check_dollar('path$dollar${dollar}', 'path$$') + def test_variables_many(self): self.conf_routes("\"routes$uri$method\"") assert self.get(url='/5')['status'] == 206, 'many' @@ -124,6 +197,80 @@ class TestVariables(TestApplicationProto): update_pass("applications") assert self.get(url='/3')['status'] == 404 + def test_variables_dynamic(self): + self.conf_routes("\"routes/$header_foo$arg_foo$cookie_foo\"") + + self.get( + url='/?foo=h', + headers={'Foo': 'b', 'Cookie': 'foo=la', 'Connection': 'close'}, + )['status'] = 210 + + def test_variables_dynamic_headers(self): + def check_header(header, status=210): + assert ( + self.get(headers={header: "blah", 'Connection': 'close'})[ + 'status' + ] + == status + ) + + self.conf_routes("\"routes/$header_foo_bar\"") + check_header('foo-bar') + check_header('Foo-Bar') + check_header('foo_bar', 404) + check_header('Foo', 404) + check_header('Bar', 404) + check_header('foobar', 404) + + self.conf_routes("\"routes/$header_Foo_Bar\"") + check_header('Foo-Bar') + check_header('foo-bar') + check_header('foo_bar', 404) + check_header('foobar', 404) + + self.conf_routes("\"routes/$header_foo-bar\"") + check_header('foo_bar', 404) + + def test_variables_dynamic_arguments(self): + self.conf_routes("\"routes/$arg_foo_bar\"") + assert self.get(url='/?foo_bar=blah')['status'] == 210 + assert self.get(url='/?foo_b%61r=blah')['status'] == 210 + assert self.get(url='/?bar&foo_bar=blah&foo')['status'] == 210 + assert self.get(url='/?Foo_bar=blah')['status'] == 404 + assert self.get(url='/?foo-bar=blah')['status'] == 404 + assert self.get()['status'] == 404 + assert self.get(url='/?foo_bar=')['status'] == 404 + assert self.get(url='/?foo_bar=l&foo_bar=blah')['status'] == 210 + assert self.get(url='/?foo_bar=blah&foo_bar=l')['status'] == 404 + + self.conf_routes("\"routes/$arg_foo_b%61r\"") + assert self.get(url='/?foo_b=blah')['status'] == 404 + assert self.get(url='/?foo_bar=blah')['status'] == 404 + + self.conf_routes("\"routes/$arg_f!~\"") + assert self.get(url='/?f=blah')['status'] == 404 + assert self.get(url='/?f!~=blah')['status'] == 404 + + def test_variables_dynamic_cookies(self): + def check_cookie(cookie, status=210): + assert ( + self.get( + headers={ + 'Host': 'localhost', + 'Cookie': cookie, + 'Connection': 'close', + }, + )['status'] + == status + ), 'match cookie' + + self.conf_routes("\"routes/$cookie_foo_bar\"") + check_cookie('foo_bar=blah', 210) + check_cookie('fOo_bar=blah', 404) + assert self.get()['status'] == 404 + check_cookie('foo_bar', 404) + check_cookie('foo_bar=', 404) + def test_variables_invalid(self): def check_variables(routes): assert 'error' in self.conf( @@ -137,3 +284,10 @@ class TestVariables(TestApplicationProto): check_variables("\"routes$uriblah\"") check_variables("\"routes${uri\"") check_variables("\"routes${{uri}\"") + check_variables("\"routes$ar\"") + check_variables("\"routes$arg\"") + check_variables("\"routes$arg_\"") + check_variables("\"routes$cookie\"") + check_variables("\"routes$cookie_\"") + check_variables("\"routes$header\"") + check_variables("\"routes$header_\"") diff --git a/test/unit/applications/lang/go.py b/test/unit/applications/lang/go.py index 04af26e1..3db955f3 100644 --- a/test/unit/applications/lang/go.py +++ b/test/unit/applications/lang/go.py @@ -9,6 +9,11 @@ from unit.option import option class TestApplicationGo(TestApplicationProto): @staticmethod def prepare_env(script, name='app', static=False): + try: + subprocess.check_output(['which', 'go']) + except subprocess.CalledProcessError: + return None + temp_dir = option.temp_dir + '/go/' if not os.path.exists(temp_dir): diff --git a/test/unit/applications/proto.py b/test/unit/applications/proto.py index cd8672ba..f04ee408 100644 --- a/test/unit/applications/proto.py +++ b/test/unit/applications/proto.py @@ -13,21 +13,21 @@ class TestApplicationProto(TestControl): def sec_epoch(self): return time.mktime(time.gmtime()) - def date_to_sec_epoch(self, date, template='%a, %d %b %Y %H:%M:%S %Z'): + def date_to_sec_epoch(self, date, template='%a, %d %b %Y %X %Z'): return time.mktime(time.strptime(date, template)) - def findall(self, pattern, name='unit.log'): + def findall(self, pattern, name='unit.log', flags=re.M): with Log.open(name) as f: - return re.findall(pattern, f.read()) + return re.findall(pattern, f.read(), flags) - def search_in_log(self, pattern, name='unit.log'): + def search_in_log(self, pattern, name='unit.log', flags=re.M): with Log.open(name) as f: - return re.search(pattern, f.read()) + return re.search(pattern, f.read(), flags) - def wait_for_record(self, pattern, name='unit.log', wait=150): + def wait_for_record(self, pattern, name='unit.log', wait=150, flags=re.M): with Log.open(name) as f: for i in range(wait): - found = re.search(pattern, f.read()) + found = re.search(pattern, f.read(), flags) if found is not None: break diff --git a/test/unit/check/unix_abstract.py b/test/unit/check/unix_abstract.py new file mode 100644 index 00000000..5d1f629e --- /dev/null +++ b/test/unit/check/unix_abstract.py @@ -0,0 +1,25 @@ +import json + +from unit.http import TestHTTP +from unit.option import option + +http = TestHTTP() + + +def check_unix_abstract(): + available = option.available + + resp = http.put( + url='/config', + sock_type='unix', + addr=option.temp_dir + '/control.unit.sock', + body=json.dumps( + { + "listeners": {"unix:@sock": {"pass": "routes"}}, + "routes": [], + } + ), + ) + + if 'success' in resp['body']: + available['features']['unix_abstract'] = True diff --git a/test/unit/http.py b/test/unit/http.py index b4a1a17b..b29667c9 100644 --- a/test/unit/http.py +++ b/test/unit/http.py @@ -51,7 +51,7 @@ class TestHTTP: connect_args = addr if sock_type == 'unix' else (addr, port) try: sock.connect(connect_args) - except ConnectionRefusedError: + except (ConnectionRefusedError, FileNotFoundError): sock.close() pytest.fail('Client can\'t connect to the server.') @@ -209,9 +209,7 @@ class TestHTTP: return {} headers_text, body = m.group(1), m.group(2) - - p = re.compile('(.*?)\x0d\x0a?', re.M | re.S) - headers_lines = p.findall(headers_text) + headers_lines = re.findall('(.*?)\x0d\x0a?', headers_text, re.M | re.S) status = re.search( r'^HTTP\/\d\.\d\s(\d+)|$', headers_lines.pop(0) diff --git a/test/unit/status.py b/test/unit/status.py new file mode 100644 index 00000000..17416f17 --- /dev/null +++ b/test/unit/status.py @@ -0,0 +1,45 @@ +from unit.control import TestControl + + +class Status: + _status = None + control = TestControl() + + def _check_zeros(): + assert Status.control.conf_get('/status') == { + 'connections': { + 'accepted': 0, + 'active': 0, + 'idle': 0, + 'closed': 0, + }, + 'requests': {'total': 0}, + 'applications': {}, + } + + def init(status=None): + Status._status = ( + status if status is not None else Status.control.conf_get('/status') + ) + + def diff(): + def find_diffs(d1, d2): + if isinstance(d1, dict) and isinstance(d2, dict): + return { + k: find_diffs(d1.get(k, 0), d2.get(k, 0)) + for k in d1 + if k in d2 + } + else: + return d1 - d2 + + return find_diffs(Status.control.conf_get('/status'), Status._status) + + def get(path='/'): + path = path.split('/')[1:] + diff = Status.diff() + + for p in path: + diff = diff[p] + + return diff |