Tests: get rid of classes in test files.

Class usage came from the unittest framework and it was always redundant
after migration to the pytest.  This commit removes classes from files
containing tests to make them more readable and understandable.
This commit is contained in:
Andrei Zeliankou
2023-06-14 18:20:09 +01:00
parent c6d05191a0
commit c183bd8749
84 changed files with 17455 additions and 16814 deletions

View File

@@ -15,7 +15,7 @@ from multiprocessing import Process
import pytest import pytest
from unit.check.discover_available import discover_available from unit.check.discover_available import discover_available
from unit.check.check_prerequisites import check_prerequisites from unit.check.check_prerequisites import check_prerequisites
from unit.http import TestHTTP from unit.http import HTTP1
from unit.log import Log from unit.log import Log
from unit.log import print_log_on_assert from unit.log import print_log_on_assert
from unit.option import option from unit.option import option
@@ -82,7 +82,7 @@ _fds_info = {
'skip': False, 'skip': False,
}, },
} }
http = TestHTTP() http = HTTP1()
is_findmnt = check_findmnt() is_findmnt = check_findmnt()
@@ -113,15 +113,16 @@ def pytest_configure(config):
def pytest_generate_tests(metafunc): def pytest_generate_tests(metafunc):
cls = metafunc.cls module = metafunc.module
if ( if (
not hasattr(cls, 'application_type') not hasattr(module, 'client')
or cls.application_type == None or not hasattr(module.client, 'application_type')
or cls.application_type == 'external' or module.client.application_type is None
or module.client.application_type == 'external'
): ):
return return
type = cls.application_type app_type = module.client.application_type
def generate_tests(versions): def generate_tests(versions):
if not versions: if not versions:
@@ -133,7 +134,7 @@ def pytest_generate_tests(metafunc):
for version in versions: for version in versions:
option.generated_tests[ option.generated_tests[
f'{metafunc.function.__name__} [{version}]' f'{metafunc.function.__name__} [{version}]'
] = f'{type} {version}' ] = f'{app_type} {version}'
# take available module from option and generate tests for each version # take available module from option and generate tests for each version
@@ -149,7 +150,7 @@ def pytest_generate_tests(metafunc):
elif version == 'any': elif version == 'any':
option.generated_tests[ option.generated_tests[
metafunc.function.__name__ metafunc.function.__name__
] = f'{type} {available_versions[0]}' ] = f'{app_type} {available_versions[0]}'
elif callable(version): elif callable(version):
generate_tests(list(filter(version, available_versions))) generate_tests(list(filter(version, available_versions)))

View File

@@ -1,61 +1,63 @@
import time import time
import pytest import pytest
from unit.applications.lang.python import TestApplicationPython from unit.applications.lang.python import ApplicationPython
from unit.option import option from unit.option import option
prerequisites = {'modules': {'python': 'any'}} prerequisites = {'modules': {'python': 'any'}}
client = ApplicationPython()
class TestAccessLog(TestApplicationPython):
def load(self, script):
super().load(script)
assert 'success' in self.conf( def load(script):
f'"{option.temp_dir}/access.log"', 'access_log' client.load(script)
), 'access_log configure'
def set_format(self, format): assert 'success' in client.conf(
assert 'success' in self.conf( f'"{option.temp_dir}/access.log"', 'access_log'
{ ), 'access_log configure'
'path': f'{option.temp_dir}/access.log',
'format': format,
},
'access_log',
), 'access_log format'
def test_access_log_keepalive(self, wait_for_record):
self.load('mirror')
assert self.get()['status'] == 200, 'init' def set_format(format):
assert 'success' in client.conf(
{
'path': f'{option.temp_dir}/access.log',
'format': format,
},
'access_log',
), 'access_log format'
(_, sock) = self.post(
headers={
'Host': 'localhost',
'Connection': 'keep-alive',
},
start=True,
body='01234',
read_timeout=1,
)
assert ( def test_access_log_keepalive(wait_for_record):
wait_for_record(r'"POST / HTTP/1.1" 200 5', 'access.log') load('mirror')
is not None
), 'keepalive 1'
_ = self.post(sock=sock, body='0123456789') assert client.get()['status'] == 200, 'init'
assert ( (_, sock) = client.post(
wait_for_record(r'"POST / HTTP/1.1" 200 10', 'access.log') headers={
is not None 'Host': 'localhost',
), 'keepalive 2' 'Connection': 'keep-alive',
},
start=True,
body='01234',
read_timeout=1,
)
def test_access_log_pipeline(self, wait_for_record): assert (
self.load('empty') wait_for_record(r'"POST / HTTP/1.1" 200 5', 'access.log') is not None
), 'keepalive 1'
self.http( _ = client.post(sock=sock, body='0123456789')
b"""GET / HTTP/1.1
assert (
wait_for_record(r'"POST / HTTP/1.1" 200 10', 'access.log') is not None
), 'keepalive 2'
def test_access_log_pipeline(wait_for_record):
load('empty')
client.http(
b"""GET / HTTP/1.1
Host: localhost Host: localhost
Referer: Referer-1 Referer: Referer-1
@@ -69,244 +71,254 @@ Referer: Referer-3
Connection: close Connection: close
""", """,
raw_resp=True, raw_resp=True,
raw=True, raw=True,
)
assert (
wait_for_record(r'"GET / HTTP/1.1" 200 0 "Referer-1" "-"', 'access.log')
is not None
), 'pipeline 1'
assert (
wait_for_record(r'"GET / HTTP/1.1" 200 0 "Referer-2" "-"', 'access.log')
is not None
), 'pipeline 2'
assert (
wait_for_record(r'"GET / HTTP/1.1" 200 0 "Referer-3" "-"', 'access.log')
is not None
), 'pipeline 3'
def test_access_log_ipv6(wait_for_record):
load('empty')
assert 'success' in client.conf(
{"[::1]:7080": {"pass": "applications/empty"}}, 'listeners'
)
client.get(sock_type='ipv6')
assert (
wait_for_record(
r'::1 - - \[.+\] "GET / HTTP/1.1" 200 0 "-" "-"', 'access.log'
) )
is not None
), 'ipv6'
assert (
wait_for_record(
r'"GET / HTTP/1.1" 200 0 "Referer-1" "-"', 'access.log'
)
is not None
), 'pipeline 1'
assert (
wait_for_record(
r'"GET / HTTP/1.1" 200 0 "Referer-2" "-"', 'access.log'
)
is not None
), 'pipeline 2'
assert (
wait_for_record(
r'"GET / HTTP/1.1" 200 0 "Referer-3" "-"', 'access.log'
)
is not None
), 'pipeline 3'
def test_access_log_ipv6(self, wait_for_record): def test_access_log_unix(temp_dir, wait_for_record):
self.load('empty') load('empty')
assert 'success' in self.conf( addr = f'{temp_dir}/sock'
{"[::1]:7080": {"pass": "applications/empty"}}, 'listeners'
assert 'success' in client.conf(
{f'unix:{addr}': {"pass": "applications/empty"}}, 'listeners'
)
client.get(sock_type='unix', addr=addr)
assert (
wait_for_record(
r'unix: - - \[.+\] "GET / HTTP/1.1" 200 0 "-" "-"', 'access.log'
) )
is not None
), 'unix'
self.get(sock_type='ipv6')
assert ( def test_access_log_referer(wait_for_record):
wait_for_record( load('empty')
r'::1 - - \[.+\] "GET / HTTP/1.1" 200 0 "-" "-"', 'access.log'
)
is not None
), 'ipv6'
def test_access_log_unix(self, temp_dir, wait_for_record): client.get(
self.load('empty') headers={
'Host': 'localhost',
'Referer': 'referer-value',
'Connection': 'close',
}
)
addr = f'{temp_dir}/sock' assert (
wait_for_record(
assert 'success' in self.conf( r'"GET / HTTP/1.1" 200 0 "referer-value" "-"', 'access.log'
{f'unix:{addr}': {"pass": "applications/empty"}}, 'listeners'
) )
is not None
), 'referer'
self.get(sock_type='unix', addr=addr)
assert ( def test_access_log_user_agent(wait_for_record):
wait_for_record( load('empty')
r'unix: - - \[.+\] "GET / HTTP/1.1" 200 0 "-" "-"', 'access.log'
)
is not None
), 'unix'
def test_access_log_referer(self, wait_for_record): client.get(
self.load('empty') headers={
'Host': 'localhost',
'User-Agent': 'user-agent-value',
'Connection': 'close',
}
)
self.get( assert (
headers={ wait_for_record(
'Host': 'localhost', r'"GET / HTTP/1.1" 200 0 "-" "user-agent-value"', 'access.log'
'Referer': 'referer-value',
'Connection': 'close',
}
) )
is not None
), 'user agent'
assert (
wait_for_record(
r'"GET / HTTP/1.1" 200 0 "referer-value" "-"', 'access.log'
)
is not None
), 'referer'
def test_access_log_user_agent(self, wait_for_record): def test_access_log_http10(wait_for_record):
self.load('empty') load('empty')
self.get( client.get(http_10=True)
headers={
'Host': 'localhost', assert (
'User-Agent': 'user-agent-value', wait_for_record(r'"GET / HTTP/1.0" 200 0 "-" "-"', 'access.log')
'Connection': 'close', is not None
} ), 'http 1.0'
def test_access_log_partial(wait_for_record):
load('empty')
assert client.post()['status'] == 200, 'init'
_ = client.http(b"""GE""", raw=True, read_timeout=1)
time.sleep(1)
assert (
wait_for_record(r'"-" 400 0 "-" "-"', 'access.log') is not None
), 'partial'
def test_access_log_partial_2(wait_for_record):
load('empty')
assert client.post()['status'] == 200, 'init'
client.http(b"""GET /\n""", raw=True)
assert (
wait_for_record(r'"-" 400 \d+ "-" "-"', 'access.log') is not None
), 'partial 2'
def test_access_log_partial_3(wait_for_record):
load('empty')
assert client.post()['status'] == 200, 'init'
_ = client.http(b"""GET / HTTP/1.1""", raw=True, read_timeout=1)
time.sleep(1)
assert (
wait_for_record(r'"-" 400 0 "-" "-"', 'access.log') is not None
), 'partial 3'
def test_access_log_partial_4(wait_for_record):
load('empty')
assert client.post()['status'] == 200, 'init'
_ = client.http(b"""GET / HTTP/1.1\n""", raw=True, read_timeout=1)
time.sleep(1)
assert (
wait_for_record(r'"-" 400 0 "-" "-"', 'access.log') is not None
), 'partial 4'
@pytest.mark.skip('not yet')
def test_access_log_partial_5(wait_for_record):
load('empty')
assert client.post()['status'] == 200, 'init'
client.get(headers={'Connection': 'close'})
assert (
wait_for_record(r'"GET / HTTP/1.1" 400 \d+ "-" "-"', 'access.log')
is not None
), 'partial 5'
def test_access_log_get_parameters(wait_for_record):
load('empty')
client.get(url='/?blah&var=val')
assert (
wait_for_record(
r'"GET /\?blah&var=val HTTP/1.1" 200 0 "-" "-"', 'access.log'
) )
is not None
), 'get parameters'
assert (
wait_for_record(
r'"GET / HTTP/1.1" 200 0 "-" "user-agent-value"', 'access.log'
)
is not None
), 'user agent'
def test_access_log_http10(self, wait_for_record): def test_access_log_delete(search_in_file):
self.load('empty') load('empty')
self.get(http_10=True) assert 'success' in client.conf_delete('access_log')
assert ( client.get(url='/delete')
wait_for_record(r'"GET / HTTP/1.0" 200 0 "-" "-"', 'access.log')
is not None
), 'http 1.0'
def test_access_log_partial(self, wait_for_record): assert search_in_file(r'/delete', 'access.log') is None, 'delete'
self.load('empty')
assert self.post()['status'] == 200, 'init'
_ = self.http(b"""GE""", raw=True, read_timeout=1) def test_access_log_change(temp_dir, wait_for_record):
load('empty')
time.sleep(1) client.get()
assert ( assert 'success' in client.conf(f'"{temp_dir}/new.log"', 'access_log')
wait_for_record(r'"-" 400 0 "-" "-"', 'access.log') is not None
), 'partial'
def test_access_log_partial_2(self, wait_for_record): client.get()
self.load('empty')
assert self.post()['status'] == 200, 'init' assert (
wait_for_record(r'"GET / HTTP/1.1" 200 0 "-" "-"', 'new.log')
is not None
), 'change'
self.http(b"""GET /\n""", raw=True)
assert ( def test_access_log_format(wait_for_record):
wait_for_record(r'"-" 400 \d+ "-" "-"', 'access.log') is not None load('empty')
), 'partial 2'
def test_access_log_partial_3(self, wait_for_record): def check_format(format, expect, url='/'):
self.load('empty') set_format(format)
assert self.post()['status'] == 200, 'init' assert client.get(url=url)['status'] == 200
assert wait_for_record(expect, 'access.log') is not None, 'found'
_ = self.http(b"""GET / HTTP/1.1""", raw=True, read_timeout=1) format = 'BLAH\t0123456789'
check_format(format, format)
check_format('$uri $status $uri $status', '/ 200 / 200')
time.sleep(1)
assert ( def test_access_log_variables(wait_for_record):
wait_for_record(r'"-" 400 0 "-" "-"', 'access.log') is not None load('mirror')
), 'partial 3'
def test_access_log_partial_4(self, wait_for_record): # $body_bytes_sent
self.load('empty')
assert self.post()['status'] == 200, 'init' set_format('$uri $body_bytes_sent')
body = '0123456789' * 50
client.post(url='/bbs', body=body, read_timeout=1)
assert (
wait_for_record(fr'^\/bbs {len(body)}$', 'access.log') is not None
), '$body_bytes_sent'
_ = self.http(b"""GET / HTTP/1.1\n""", raw=True, read_timeout=1)
time.sleep(1) def test_access_log_incorrect(temp_dir, skip_alert):
skip_alert(r'failed to apply new conf')
assert ( assert 'error' in client.conf(
wait_for_record(r'"-" 400 0 "-" "-"', 'access.log') is not None f'{temp_dir}/blah/access.log',
), 'partial 4' 'access_log/path',
), 'access_log path incorrect'
@pytest.mark.skip('not yet') assert 'error' in client.conf(
def test_access_log_partial_5(self, wait_for_record): {
self.load('empty') 'path': f'{temp_dir}/access.log',
'format': '$remote_add',
assert self.post()['status'] == 200, 'init' },
'access_log',
self.get(headers={'Connection': 'close'}) ), 'access_log format incorrect'
assert (
wait_for_record(r'"GET / HTTP/1.1" 400 \d+ "-" "-"', 'access.log')
is not None
), 'partial 5'
def test_access_log_get_parameters(self, wait_for_record):
self.load('empty')
self.get(url='/?blah&var=val')
assert (
wait_for_record(
r'"GET /\?blah&var=val HTTP/1.1" 200 0 "-" "-"', 'access.log'
)
is not None
), 'get parameters'
def test_access_log_delete(self, search_in_file):
self.load('empty')
assert 'success' in self.conf_delete('access_log')
self.get(url='/delete')
assert search_in_file(r'/delete', 'access.log') is None, 'delete'
def test_access_log_change(self, temp_dir, wait_for_record):
self.load('empty')
self.get()
assert 'success' in self.conf(f'"{temp_dir}/new.log"', 'access_log')
self.get()
assert (
wait_for_record(r'"GET / HTTP/1.1" 200 0 "-" "-"', 'new.log')
is not None
), 'change'
def test_access_log_format(self, wait_for_record):
self.load('empty')
def check_format(format, expect, url='/'):
self.set_format(format)
assert self.get(url=url)['status'] == 200
assert wait_for_record(expect, 'access.log') 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, wait_for_record):
self.load('mirror')
# $body_bytes_sent
self.set_format('$uri $body_bytes_sent')
body = '0123456789' * 50
self.post(url='/bbs', body=body, read_timeout=1)
assert (
wait_for_record(fr'^\/bbs {len(body)}$', 'access.log') is not None
), '$body_bytes_sent'
def test_access_log_incorrect(self, temp_dir, skip_alert):
skip_alert(r'failed to apply new conf')
assert 'error' in self.conf(
f'{temp_dir}/blah/access.log',
'access_log/path',
), 'access_log path incorrect'
assert 'error' in self.conf(
{
'path': f'{temp_dir}/access.log',
'format': '$remote_add',
},
'access_log',
), 'access_log format incorrect'

View File

@@ -3,23 +3,22 @@ import time
import pytest import pytest
from packaging import version from packaging import version
from unit.applications.lang.python import TestApplicationPython from unit.applications.lang.python import ApplicationPython
prerequisites = { prerequisites = {
'modules': {'python': lambda v: version.parse(v) >= version.parse('3.5')} 'modules': {'python': lambda v: version.parse(v) >= version.parse('3.5')}
} }
client = ApplicationPython(load_module='asgi')
class TestASGIApplication(TestApplicationPython):
load_module = 'asgi'
def test_asgi_application_variables(self, date_to_sec_epoch, sec_epoch): def test_asgi_application_variables(date_to_sec_epoch, sec_epoch):
self.load('variables') client.load('variables')
body = 'Test body string.' body = 'Test body string.'
resp = self.http( resp = client.http(
f"""POST / HTTP/1.1 f"""POST / HTTP/1.1
Host: localhost Host: localhost
Content-Length: {len(body)} Content-Length: {len(body)}
Custom-Header: blah Custom-Header: blah
@@ -29,190 +28,230 @@ Connection: close
custom-header: BLAH custom-header: BLAH
{body}""".encode(), {body}""".encode(),
raw=True, raw=True,
) )
assert resp['status'] == 200, 'status' assert resp['status'] == 200, 'status'
headers = resp['headers'] headers = resp['headers']
header_server = headers.pop('Server') header_server = headers.pop('Server')
assert re.search(r'Unit/[\d\.]+', header_server), 'server header' assert re.search(r'Unit/[\d\.]+', header_server), 'server header'
date = headers.pop('Date') date = headers.pop('Date')
assert date[-4:] == ' GMT', 'date header timezone' assert date[-4:] == ' GMT', 'date header timezone'
assert abs(date_to_sec_epoch(date) - sec_epoch) < 5, 'date header' assert abs(date_to_sec_epoch(date) - sec_epoch) < 5, 'date header'
assert headers == { assert headers == {
'Connection': 'close', 'Connection': 'close',
'content-length': str(len(body)), 'content-length': str(len(body)),
'content-type': 'text/html', 'content-type': 'text/html',
'request-method': 'POST', 'request-method': 'POST',
'request-uri': '/', 'request-uri': '/',
'http-host': 'localhost', 'http-host': 'localhost',
'http-version': '1.1', 'http-version': '1.1',
'custom-header': 'blah, Blah, BLAH', 'custom-header': 'blah, Blah, BLAH',
'asgi-version': '3.0', 'asgi-version': '3.0',
'asgi-spec-version': '2.1', 'asgi-spec-version': '2.1',
'scheme': 'http', 'scheme': 'http',
}, 'headers' }, 'headers'
assert resp['body'] == body, 'body' assert resp['body'] == body, 'body'
def test_asgi_application_ipv6(self):
self.load('empty')
assert 'success' in self.conf( def test_asgi_application_ipv6():
{"[::1]:7080": {"pass": "applications/empty"}}, 'listeners' client.load('empty')
)
assert self.get(sock_type='ipv6')['status'] == 200 assert 'success' in client.conf(
{"[::1]:7080": {"pass": "applications/empty"}}, 'listeners'
)
def test_asgi_application_unix(self, temp_dir): assert client.get(sock_type='ipv6')['status'] == 200
self.load('empty')
addr = f'{temp_dir}/sock'
assert 'success' in self.conf(
{f"unix:{addr}": {"pass": "applications/empty"}}, 'listeners'
)
assert self.get(sock_type='unix', addr=addr)['status'] == 200 def test_asgi_application_unix(temp_dir):
client.load('empty')
def test_asgi_application_query_string(self): addr = f'{temp_dir}/sock'
self.load('query_string') assert 'success' in client.conf(
{f"unix:{addr}": {"pass": "applications/empty"}}, 'listeners'
)
resp = self.get(url='/?var1=val1&var2=val2') assert client.get(sock_type='unix', addr=addr)['status'] == 200
assert (
resp['headers']['query-string'] == 'var1=val1&var2=val2'
), 'query-string header'
def test_asgi_application_prefix(self): def test_asgi_application_query_string():
self.load('prefix', prefix='/api/rest') client.load('query_string')
def set_prefix(prefix): resp = client.get(url='/?var1=val1&var2=val2')
self.conf(f'"{prefix}"', 'applications/prefix/prefix')
def check_prefix(url, prefix): assert (
resp = self.get(url=url) resp['headers']['query-string'] == 'var1=val1&var2=val2'
assert resp['status'] == 200 ), 'query-string header'
assert resp['headers']['prefix'] == prefix
check_prefix('/ap', 'NULL')
check_prefix('/api', 'NULL')
check_prefix('/api/', 'NULL')
check_prefix('/api/res', 'NULL')
check_prefix('/api/restful', 'NULL')
check_prefix('/api/rest', '/api/rest')
check_prefix('/api/rest/', '/api/rest')
check_prefix('/api/rest/get', '/api/rest')
check_prefix('/api/rest/get/blah', '/api/rest')
set_prefix('/api/rest/') def test_asgi_application_prefix():
check_prefix('/api/rest', '/api/rest') client.load('prefix', prefix='/api/rest')
check_prefix('/api/restful', 'NULL')
check_prefix('/api/rest/', '/api/rest')
check_prefix('/api/rest/blah', '/api/rest')
set_prefix('/app') def set_prefix(prefix):
check_prefix('/ap', 'NULL') client.conf(f'"{prefix}"', 'applications/prefix/prefix')
check_prefix('/app', '/app')
check_prefix('/app/', '/app')
check_prefix('/application/', 'NULL')
set_prefix('/') def check_prefix(url, prefix):
check_prefix('/', 'NULL') resp = client.get(url=url)
check_prefix('/app', 'NULL') assert resp['status'] == 200
assert resp['headers']['prefix'] == prefix
def test_asgi_application_query_string_space(self): check_prefix('/ap', 'NULL')
self.load('query_string') check_prefix('/api', 'NULL')
check_prefix('/api/', 'NULL')
check_prefix('/api/res', 'NULL')
check_prefix('/api/restful', 'NULL')
check_prefix('/api/rest', '/api/rest')
check_prefix('/api/rest/', '/api/rest')
check_prefix('/api/rest/get', '/api/rest')
check_prefix('/api/rest/get/blah', '/api/rest')
resp = self.get(url='/ ?var1=val1&var2=val2') set_prefix('/api/rest/')
assert ( check_prefix('/api/rest', '/api/rest')
resp['headers']['query-string'] == 'var1=val1&var2=val2' check_prefix('/api/restful', 'NULL')
), 'query-string space' check_prefix('/api/rest/', '/api/rest')
check_prefix('/api/rest/blah', '/api/rest')
resp = self.get(url='/ %20?var1=val1&var2=val2') set_prefix('/app')
assert ( check_prefix('/ap', 'NULL')
resp['headers']['query-string'] == 'var1=val1&var2=val2' check_prefix('/app', '/app')
), 'query-string space 2' check_prefix('/app/', '/app')
check_prefix('/application/', 'NULL')
resp = self.get(url='/ %20 ?var1=val1&var2=val2') set_prefix('/')
assert ( check_prefix('/', 'NULL')
resp['headers']['query-string'] == 'var1=val1&var2=val2' check_prefix('/app', 'NULL')
), 'query-string space 3'
resp = self.get(url='/blah %20 blah? var1= val1 & var2=val2')
assert (
resp['headers']['query-string'] == ' var1= val1 & var2=val2'
), 'query-string space 4'
def test_asgi_application_query_string_empty(self): def test_asgi_application_query_string_space():
self.load('query_string') client.load('query_string')
resp = self.get(url='/?') resp = client.get(url='/ ?var1=val1&var2=val2')
assert (
resp['headers']['query-string'] == 'var1=val1&var2=val2'
), 'query-string space'
assert resp['status'] == 200, 'query string empty status' resp = client.get(url='/ %20?var1=val1&var2=val2')
assert resp['headers']['query-string'] == '', 'query string empty' assert (
resp['headers']['query-string'] == 'var1=val1&var2=val2'
), 'query-string space 2'
def test_asgi_application_query_string_absent(self): resp = client.get(url='/ %20 ?var1=val1&var2=val2')
self.load('query_string') assert (
resp['headers']['query-string'] == 'var1=val1&var2=val2'
), 'query-string space 3'
resp = self.get() resp = client.get(url='/blah %20 blah? var1= val1 & var2=val2')
assert (
resp['headers']['query-string'] == ' var1= val1 & var2=val2'
), 'query-string space 4'
assert resp['status'] == 200, 'query string absent status'
assert resp['headers']['query-string'] == '', 'query string absent'
@pytest.mark.skip('not yet') def test_asgi_application_query_string_empty():
def test_asgi_application_server_port(self): client.load('query_string')
self.load('server_port')
assert ( resp = client.get(url='/?')
self.get()['headers']['Server-Port'] == '7080'
), 'Server-Port header'
@pytest.mark.skip('not yet') assert resp['status'] == 200, 'query string empty status'
def test_asgi_application_working_directory_invalid(self): assert resp['headers']['query-string'] == '', 'query string empty'
self.load('empty')
assert 'success' in self.conf(
'"/blah"', 'applications/empty/working_directory'
), 'configure invalid working_directory'
assert self.get()['status'] == 500, 'status' def test_asgi_application_query_string_absent():
client.load('query_string')
def test_asgi_application_204_transfer_encoding(self): resp = client.get()
self.load('204_no_content')
assert ( assert resp['status'] == 200, 'query string absent status'
'Transfer-Encoding' not in self.get()['headers'] assert resp['headers']['query-string'] == '', 'query string absent'
), '204 header transfer encoding'
def test_asgi_application_shm_ack_handle(self):
# Minimum possible limit
shm_limit = 10 * 1024 * 1024
self.load('mirror', limits={"shm": shm_limit}) @pytest.mark.skip('not yet')
def test_asgi_application_server_port():
client.load('server_port')
# Should exceed shm_limit assert (
max_body_size = 12 * 1024 * 1024 client.get()['headers']['Server-Port'] == '7080'
), 'Server-Port header'
assert 'success' in self.conf(
f'{{"http":{{"max_body_size": {max_body_size} }}}}',
'settings',
)
assert self.get()['status'] == 200, 'init' @pytest.mark.skip('not yet')
def test_asgi_application_working_directory_invalid():
client.load('empty')
body = '0123456789AB' * 1024 * 1024 # 12 Mb assert 'success' in client.conf(
resp = self.post(body=body, read_buffer_size=1024 * 1024) '"/blah"', 'applications/empty/working_directory'
), 'configure invalid working_directory'
assert resp['body'] == body, 'keep-alive 1' assert client.get()['status'] == 500, 'status'
def test_asgi_keepalive_body(self):
self.load('mirror')
assert self.get()['status'] == 200, 'init' def test_asgi_application_204_transfer_encoding():
client.load('204_no_content')
body = '0123456789' * 500 assert (
(resp, sock) = self.post( 'Transfer-Encoding' not in client.get()['headers']
), '204 header transfer encoding'
def test_asgi_application_shm_ack_handle():
# Minimum possible limit
shm_limit = 10 * 1024 * 1024
client.load('mirror', limits={"shm": shm_limit})
# Should exceed shm_limit
max_body_size = 12 * 1024 * 1024
assert 'success' in client.conf(
f'{{"http":{{"max_body_size": {max_body_size} }}}}',
'settings',
)
assert client.get()['status'] == 200, 'init'
body = '0123456789AB' * 1024 * 1024 # 12 Mb
resp = client.post(body=body, read_buffer_size=1024 * 1024)
assert resp['body'] == body, 'keep-alive 1'
def test_asgi_keepalive_body():
client.load('mirror')
assert client.get()['status'] == 200, 'init'
body = '0123456789' * 500
(resp, sock) = client.post(
headers={
'Host': 'localhost',
'Connection': 'keep-alive',
},
start=True,
body=body,
read_timeout=1,
)
assert resp['body'] == body, 'keep-alive 1'
body = '0123456789'
resp = client.post(sock=sock, body=body)
assert resp['body'] == body, 'keep-alive 2'
def test_asgi_keepalive_reconfigure():
client.load('mirror')
assert client.get()['status'] == 200, 'init'
body = '0123456789'
conns = 3
socks = []
for i in range(conns):
(resp, sock) = client.post(
headers={ headers={
'Host': 'localhost', 'Host': 'localhost',
'Connection': 'keep-alive', 'Connection': 'keep-alive',
@@ -222,235 +261,216 @@ custom-header: BLAH
read_timeout=1, read_timeout=1,
) )
assert resp['body'] == body, 'keep-alive 1' assert resp['body'] == body, 'keep-alive open'
body = '0123456789' client.load('mirror', processes=i + 1)
resp = self.post(sock=sock, body=body)
assert resp['body'] == body, 'keep-alive 2' socks.append(sock)
def test_asgi_keepalive_reconfigure(self): for i in range(conns):
self.load('mirror') (resp, sock) = client.post(
assert self.get()['status'] == 200, 'init'
body = '0123456789'
conns = 3
socks = []
for i in range(conns):
(resp, sock) = self.post(
headers={
'Host': 'localhost',
'Connection': 'keep-alive',
},
start=True,
body=body,
read_timeout=1,
)
assert resp['body'] == body, 'keep-alive open'
self.load('mirror', processes=i + 1)
socks.append(sock)
for i in range(conns):
(resp, sock) = self.post(
headers={
'Host': 'localhost',
'Connection': 'keep-alive',
},
start=True,
sock=socks[i],
body=body,
read_timeout=1,
)
assert resp['body'] == body, 'keep-alive request'
self.load('mirror', processes=i + 1)
for i in range(conns):
resp = self.post(sock=socks[i], body=body)
assert resp['body'] == body, 'keep-alive close'
self.load('mirror', processes=i + 1)
def test_asgi_keepalive_reconfigure_2(self):
self.load('mirror')
assert self.get()['status'] == 200, 'init'
body = '0123456789'
(resp, sock) = self.post(
headers={ headers={
'Host': 'localhost', 'Host': 'localhost',
'Connection': 'keep-alive', 'Connection': 'keep-alive',
}, },
start=True, start=True,
sock=socks[i],
body=body, body=body,
read_timeout=1, read_timeout=1,
) )
assert resp['body'] == body, 'reconfigure 2 keep-alive 1' assert resp['body'] == body, 'keep-alive request'
self.load('empty') client.load('mirror', processes=i + 1)
assert self.get()['status'] == 200, 'init' for i in range(conns):
resp = client.post(sock=socks[i], body=body)
(resp, sock) = self.post(start=True, sock=sock, body=body) assert resp['body'] == body, 'keep-alive close'
assert resp['status'] == 200, 'reconfigure 2 keep-alive 2' client.load('mirror', processes=i + 1)
assert resp['body'] == '', 'reconfigure 2 keep-alive 2 body'
assert 'success' in self.conf(
{"listeners": {}, "applications": {}}
), 'reconfigure 2 clear configuration'
resp = self.get(sock=sock) def test_asgi_keepalive_reconfigure_2():
client.load('mirror')
assert resp == {}, 'reconfigure 2 keep-alive 3' assert client.get()['status'] == 200, 'init'
def test_asgi_keepalive_reconfigure_3(self): body = '0123456789'
self.load('empty')
assert self.get()['status'] == 200, 'init' (resp, sock) = client.post(
headers={
'Host': 'localhost',
'Connection': 'keep-alive',
},
start=True,
body=body,
read_timeout=1,
)
sock = self.http( assert resp['body'] == body, 'reconfigure 2 keep-alive 1'
b"""GET / HTTP/1.1
client.load('empty')
assert client.get()['status'] == 200, 'init'
(resp, sock) = client.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'
assert 'success' in client.conf(
{"listeners": {}, "applications": {}}
), 'reconfigure 2 clear configuration'
resp = client.get(sock=sock)
assert resp == {}, 'reconfigure 2 keep-alive 3'
def test_asgi_keepalive_reconfigure_3():
client.load('empty')
assert client.get()['status'] == 200, 'init'
sock = client.http(
b"""GET / HTTP/1.1
""", """,
raw=True, raw=True,
no_recv=True, no_recv=True,
) )
assert self.get()['status'] == 200 assert client.get()['status'] == 200
assert 'success' in self.conf( assert 'success' in client.conf(
{"listeners": {}, "applications": {}} {"listeners": {}, "applications": {}}
), 'reconfigure 3 clear configuration' ), 'reconfigure 3 clear configuration'
resp = self.http( resp = client.http(
b"""Host: localhost b"""Host: localhost
Connection: close Connection: close
""", """,
sock=sock, sock=sock,
raw=True, raw=True,
) )
assert resp['status'] == 200, 'reconfigure 3' assert resp['status'] == 200, 'reconfigure 3'
def test_asgi_process_switch(self):
self.load('delayed', processes=2)
self.get( def test_asgi_process_switch():
client.load('delayed', processes=2)
client.get(
headers={
'Host': 'localhost',
'Content-Length': '0',
'X-Delay': '5',
'Connection': 'close',
},
no_recv=True,
)
headers_delay_1 = {
'Connection': 'close',
'Host': 'localhost',
'Content-Length': '0',
'X-Delay': '1',
}
client.get(headers=headers_delay_1, no_recv=True)
time.sleep(0.5)
for _ in range(10):
client.get(headers=headers_delay_1, no_recv=True)
client.get(headers=headers_delay_1)
def test_asgi_application_loading_error(skip_alert):
skip_alert(r'Python failed to import module "blah"')
client.load('empty', module="blah")
assert client.get()['status'] == 503, 'loading error'
def test_asgi_application_threading(wait_for_record):
"""wait_for_record() timeouts after 5s while every thread works at
least 3s. So without releasing GIL test should fail.
"""
client.load('threading')
for _ in range(10):
client.get(no_recv=True)
assert (
wait_for_record(r'\(5\) Thread: 100', wait=50) is not None
), 'last thread finished'
def test_asgi_application_threads():
client.load('threads', threads=2)
socks = []
for _ in range(2):
sock = client.get(
headers={ headers={
'Host': 'localhost', 'Host': 'localhost',
'Content-Length': '0', 'X-Delay': '3',
'X-Delay': '5',
'Connection': 'close', 'Connection': 'close',
}, },
no_recv=True, no_recv=True,
) )
headers_delay_1 = { socks.append(sock)
'Connection': 'close',
time.sleep(1.0) # required to avoid greedy request reading
threads = set()
for sock in socks:
resp = client.recvall(sock).decode('utf-8')
client.log_in(resp)
resp = client._resp_to_dict(resp)
assert resp['status'] == 200, 'status'
threads.add(resp['headers']['x-thread'])
sock.close()
assert len(socks) == len(threads), 'threads differs'
def test_asgi_application_legacy():
client.load('legacy')
resp = client.get(
headers={
'Host': 'localhost', 'Host': 'localhost',
'Content-Length': '0', 'Content-Length': '0',
'X-Delay': '1', 'Connection': 'close',
} },
)
self.get(headers=headers_delay_1, no_recv=True) assert resp['status'] == 200, 'status'
time.sleep(0.5)
for _ in range(10): def test_asgi_application_legacy_force():
self.get(headers=headers_delay_1, no_recv=True) client.load('legacy_force', protocol='asgi')
self.get(headers=headers_delay_1) resp = client.get(
headers={
'Host': 'localhost',
'Content-Length': '0',
'Connection': 'close',
},
)
def test_asgi_application_loading_error(self, skip_alert): assert resp['status'] == 200, 'status'
skip_alert(r'Python failed to import module "blah"')
self.load('empty', module="blah")
assert self.get()['status'] == 503, 'loading error'
def test_asgi_application_threading(self, wait_for_record):
"""wait_for_record() timeouts after 5s while every thread works at
least 3s. So without releasing GIL test should fail.
"""
self.load('threading')
for _ in range(10):
self.get(no_recv=True)
assert (
wait_for_record(r'\(5\) Thread: 100', wait=50) is not None
), 'last thread finished'
def test_asgi_application_threads(self):
self.load('threads', threads=2)
socks = []
for _ in range(2):
sock = self.get(
headers={
'Host': 'localhost',
'X-Delay': '3',
'Connection': 'close',
},
no_recv=True,
)
socks.append(sock)
time.sleep(1.0) # required to avoid greedy request reading
threads = set()
for sock in socks:
resp = self.recvall(sock).decode('utf-8')
self.log_in(resp)
resp = self._resp_to_dict(resp)
assert resp['status'] == 200, 'status'
threads.add(resp['headers']['x-thread'])
sock.close()
assert len(socks) == len(threads), 'threads differs'
def test_asgi_application_legacy(self):
self.load('legacy')
resp = self.get(
headers={
'Host': 'localhost',
'Content-Length': '0',
'Connection': 'close',
},
)
assert resp['status'] == 200, 'status'
def test_asgi_application_legacy_force(self):
self.load('legacy_force', protocol='asgi')
resp = self.get(
headers={
'Host': 'localhost',
'Content-Length': '0',
'Connection': 'close',
},
)
assert resp['status'] == 200, 'status'

View File

@@ -1,22 +1,21 @@
from packaging import version from packaging import version
from unit.applications.lang.python import TestApplicationPython from unit.applications.lang.python import ApplicationPython
prerequisites = { prerequisites = {
'modules': {'python': lambda v: version.parse(v) >= version.parse('3.5')}, 'modules': {'python': lambda v: version.parse(v) >= version.parse('3.5')},
'features': {'unix_abstract': True}, 'features': {'unix_abstract': True},
} }
client = ApplicationPython(load_module='asgi')
class TestASGIApplicationUnixAbstract(TestApplicationPython):
load_module = 'asgi'
def test_asgi_application_unix_abstract(self): def test_asgi_application_unix_abstract():
self.load('empty') client.load('empty')
addr = '\0sock' addr = '\0sock'
assert 'success' in self.conf( assert 'success' in client.conf(
{f"unix:@{addr[1:]}": {"pass": "applications/empty"}}, {f"unix:@{addr[1:]}": {"pass": "applications/empty"}},
'listeners', 'listeners',
) )
assert self.get(sock_type='unix', addr=addr)['status'] == 200 assert client.get(sock_type='unix', addr=addr)['status'] == 200

View File

@@ -2,122 +2,126 @@ import os
from conftest import unit_stop from conftest import unit_stop
from packaging import version from packaging import version
from unit.applications.lang.python import TestApplicationPython from unit.applications.lang.python import ApplicationPython
from unit.option import option from unit.option import option
prerequisites = { prerequisites = {
'modules': {'python': lambda v: version.parse(v) >= version.parse('3.5')} 'modules': {'python': lambda v: version.parse(v) >= version.parse('3.5')}
} }
client = ApplicationPython(load_module='asgi')
class TestASGILifespan(TestApplicationPython):
load_module = 'asgi'
def setup_cookies(self, prefix): def assert_cookies(prefix):
base_dir = f'{option.test_dir}/python/lifespan/empty' for name in ['startup', 'shutdown']:
path = f'{option.test_dir}/python/lifespan/empty/{prefix}{name}'
exists = os.path.isfile(path)
if exists:
os.remove(path)
os.chmod(base_dir, 0o777) assert not exists, name
for name in ['startup', 'shutdown', 'version']: path = f'{option.test_dir}/python/lifespan/empty/{prefix}version'
path = f'{option.test_dir}/python/lifespan/empty/{prefix}{name}'
open(path, 'a').close()
os.chmod(path, 0o777)
def assert_cookies(self, prefix): with open(path, 'r') as f:
for name in ['startup', 'shutdown']: version = f.read()
path = f'{option.test_dir}/python/lifespan/empty/{prefix}{name}'
exists = os.path.isfile(path)
if exists:
os.remove(path)
assert not exists, name os.remove(path)
path = f'{option.test_dir}/python/lifespan/empty/{prefix}version' assert version == '3.0 2.0', 'version'
with open(path, 'r') as f:
version = f.read()
os.remove(path) def setup_cookies(prefix):
base_dir = f'{option.test_dir}/python/lifespan/empty'
assert version == '3.0 2.0', 'version' os.chmod(base_dir, 0o777)
def test_asgi_lifespan(self): for name in ['startup', 'shutdown', 'version']:
self.load('lifespan/empty') path = f'{option.test_dir}/python/lifespan/empty/{prefix}{name}'
open(path, 'a').close()
os.chmod(path, 0o777)
self.setup_cookies('')
assert self.get()['status'] == 204 def test_asgi_lifespan():
client.load('lifespan/empty')
unit_stop() setup_cookies('')
self.assert_cookies('') assert client.get()['status'] == 204
def test_asgi_lifespan_targets(self): unit_stop()
path = f'{option.test_dir}/python/lifespan/empty'
assert 'success' in self.conf( assert_cookies('')
{
"listeners": {"*:7080": {"pass": "routes"}},
"routes": [ def test_asgi_lifespan_targets():
{ path = f'{option.test_dir}/python/lifespan/empty'
"match": {"uri": "/1"},
"action": {"pass": "applications/targets/1"}, assert 'success' in client.conf(
}, {
{ "listeners": {"*:7080": {"pass": "routes"}},
"match": {"uri": "/2"}, "routes": [
"action": {"pass": "applications/targets/2"}, {
}, "match": {"uri": "/1"},
], "action": {"pass": "applications/targets/1"},
"applications": {
"targets": {
"type": self.get_application_type(),
"processes": {"spare": 0},
"working_directory": path,
"path": path,
"targets": {
"1": {"module": "asgi", "callable": "application"},
"2": {
"module": "asgi",
"callable": "application2",
},
},
}
}, },
} {
) "match": {"uri": "/2"},
"action": {"pass": "applications/targets/2"},
},
],
"applications": {
"targets": {
"type": client.get_application_type(),
"processes": {"spare": 0},
"working_directory": path,
"path": path,
"targets": {
"1": {"module": "asgi", "callable": "application"},
"2": {
"module": "asgi",
"callable": "application2",
},
},
}
},
}
)
self.setup_cookies('') setup_cookies('')
self.setup_cookies('app2_') setup_cookies('app2_')
assert self.get(url="/1")['status'] == 204 assert client.get(url="/1")['status'] == 204
assert self.get(url="/2")['status'] == 204 assert client.get(url="/2")['status'] == 204
unit_stop() unit_stop()
self.assert_cookies('') assert_cookies('')
self.assert_cookies('app2_') assert_cookies('app2_')
def test_asgi_lifespan_failed(self, wait_for_record):
self.load('lifespan/failed')
assert self.get()['status'] == 503 def test_asgi_lifespan_failed(wait_for_record):
client.load('lifespan/failed')
assert ( assert client.get()['status'] == 503
wait_for_record(r'\[error\].*Application startup failed')
is not None
), 'error message'
assert wait_for_record(r'Exception blah') is not None, 'exception'
def test_asgi_lifespan_error(self, wait_for_record): assert (
self.load('lifespan/error') wait_for_record(r'\[error\].*Application startup failed') is not None
), 'error message'
assert wait_for_record(r'Exception blah') is not None, 'exception'
self.get()
assert wait_for_record(r'Exception blah') is not None, 'exception' def test_asgi_lifespan_error(wait_for_record):
client.load('lifespan/error')
def test_asgi_lifespan_error_auto(self, wait_for_record): client.get()
self.load('lifespan/error_auto')
self.get() assert wait_for_record(r'Exception blah') is not None, 'exception'
assert wait_for_record(r'AssertionError') is not None, 'assertion'
def test_asgi_lifespan_error_auto(wait_for_record):
client.load('lifespan/error_auto')
client.get()
assert wait_for_record(r'AssertionError') is not None, 'assertion'

View File

@@ -1,137 +1,142 @@
import pytest import pytest
from packaging import version from packaging import version
from unit.applications.lang.python import TestApplicationPython from unit.applications.lang.python import ApplicationPython
from unit.option import option from unit.option import option
prerequisites = { prerequisites = {
'modules': {'python': lambda v: version.parse(v) >= version.parse('3.5')} 'modules': {'python': lambda v: version.parse(v) >= version.parse('3.5')}
} }
client = ApplicationPython(load_module='asgi')
class TestASGITargets(TestApplicationPython):
load_module = 'asgi'
@pytest.fixture(autouse=True) @pytest.fixture(autouse=True)
def setup_method_fixture(self): def setup_method_fixture():
path = f'{option.test_dir}/python/targets/' path = f'{option.test_dir}/python/targets/'
assert 'success' in self.conf( assert 'success' in client.conf(
{ {
"listeners": {"*:7080": {"pass": "routes"}}, "listeners": {"*:7080": {"pass": "routes"}},
"routes": [ "routes": [
{
"match": {"uri": "/1"},
"action": {"pass": "applications/targets/1"},
},
{
"match": {"uri": "/2"},
"action": {"pass": "applications/targets/2"},
},
],
"applications": {
"targets": {
"type": self.get_application_type(),
"processes": {"spare": 0},
"working_directory": path,
"path": path,
"protocol": "asgi",
"targets": {
"1": {
"module": "asgi",
"callable": "application_200",
},
"2": {
"module": "asgi",
"callable": "application_201",
},
},
}
},
}
)
def conf_targets(self, targets):
assert 'success' in self.conf(targets, 'applications/targets/targets')
def test_asgi_targets(self):
assert self.get(url='/1')['status'] == 200
assert self.get(url='/2')['status'] == 201
def test_asgi_targets_legacy(self):
self.conf_targets(
{
"1": {"module": "asgi", "callable": "legacy_application_200"},
"2": {"module": "asgi", "callable": "legacy_application_201"},
}
)
assert self.get(url='/1')['status'] == 200
assert self.get(url='/2')['status'] == 201
def test_asgi_targets_mix(self):
self.conf_targets(
{
"1": {"module": "asgi", "callable": "application_200"},
"2": {"module": "asgi", "callable": "legacy_application_201"},
}
)
assert self.get(url='/1')['status'] == 200
assert self.get(url='/2')['status'] == 201
def test_asgi_targets_broken(self, skip_alert):
skip_alert(r'Python failed to get "blah" from module')
self.conf_targets(
{
"1": {"module": "asgi", "callable": "application_200"},
"2": {"module": "asgi", "callable": "blah"},
}
)
assert self.get(url='/1')['status'] != 200
def test_asgi_targets_prefix(self):
self.conf_targets(
{
"1": {
"module": "asgi",
"callable": "application_prefix",
"prefix": "/1/",
},
"2": {
"module": "asgi",
"callable": "application_prefix",
"prefix": "/api",
},
}
)
self.conf(
[
{ {
"match": {"uri": "/1*"}, "match": {"uri": "/1"},
"action": {"pass": "applications/targets/1"}, "action": {"pass": "applications/targets/1"},
}, },
{ {
"match": {"uri": "*"}, "match": {"uri": "/2"},
"action": {"pass": "applications/targets/2"}, "action": {"pass": "applications/targets/2"},
}, },
], ],
"routes", "applications": {
) "targets": {
"type": client.get_application_type(),
"processes": {"spare": 0},
"working_directory": path,
"path": path,
"protocol": "asgi",
"targets": {
"1": {
"module": "asgi",
"callable": "application_200",
},
"2": {
"module": "asgi",
"callable": "application_201",
},
},
}
},
}
)
def check_prefix(url, prefix):
resp = self.get(url=url)
assert resp['status'] == 200
assert resp['headers']['prefix'] == prefix
check_prefix('/1', '/1') def conf_targets(targets):
check_prefix('/11', 'NULL') assert 'success' in client.conf(targets, 'applications/targets/targets')
check_prefix('/1/', '/1')
check_prefix('/', 'NULL')
check_prefix('/ap', 'NULL') def test_asgi_targets():
check_prefix('/api', '/api') assert client.get(url='/1')['status'] == 200
check_prefix('/api/', '/api') assert client.get(url='/2')['status'] == 201
check_prefix('/api/test/', '/api')
check_prefix('/apis', 'NULL')
check_prefix('/apis/', 'NULL') def test_asgi_targets_legacy():
conf_targets(
{
"1": {"module": "asgi", "callable": "legacy_application_200"},
"2": {"module": "asgi", "callable": "legacy_application_201"},
}
)
assert client.get(url='/1')['status'] == 200
assert client.get(url='/2')['status'] == 201
def test_asgi_targets_mix():
conf_targets(
{
"1": {"module": "asgi", "callable": "application_200"},
"2": {"module": "asgi", "callable": "legacy_application_201"},
}
)
assert client.get(url='/1')['status'] == 200
assert client.get(url='/2')['status'] == 201
def test_asgi_targets_broken(skip_alert):
skip_alert(r'Python failed to get "blah" from module')
conf_targets(
{
"1": {"module": "asgi", "callable": "application_200"},
"2": {"module": "asgi", "callable": "blah"},
}
)
assert client.get(url='/1')['status'] != 200
def test_asgi_targets_prefix():
conf_targets(
{
"1": {
"module": "asgi",
"callable": "application_prefix",
"prefix": "/1/",
},
"2": {
"module": "asgi",
"callable": "application_prefix",
"prefix": "/api",
},
}
)
client.conf(
[
{
"match": {"uri": "/1*"},
"action": {"pass": "applications/targets/1"},
},
{
"match": {"uri": "*"},
"action": {"pass": "applications/targets/2"},
},
],
"routes",
)
def check_prefix(url, prefix):
resp = client.get(url=url)
assert resp['status'] == 200
assert resp['headers']['prefix'] == prefix
check_prefix('/1', '/1')
check_prefix('/11', 'NULL')
check_prefix('/1/', '/1')
check_prefix('/', 'NULL')
check_prefix('/ap', 'NULL')
check_prefix('/api', '/api')
check_prefix('/api/', '/api')
check_prefix('/api/test/', '/api')
check_prefix('/apis', 'NULL')
check_prefix('/apis/', 'NULL')

File diff suppressed because it is too large Load Diff

View File

@@ -1,183 +1,186 @@
import pytest import pytest
from unit.applications.lang.python import TestApplicationPython from unit.applications.lang.python import ApplicationPython
from unit.option import option from unit.option import option
prerequisites = {'modules': {'python': 'any'}} prerequisites = {'modules': {'python': 'any'}}
client = ApplicationPython()
class TestClientIP(TestApplicationPython):
@pytest.fixture(autouse=True)
def setup_method_fixture(self):
self.load('client_ip')
def client_ip(self, options): @pytest.fixture(autouse=True)
assert 'success' in self.conf( def setup_method_fixture():
{ client.load('client_ip')
"127.0.0.1:7081": {
"client_ip": options,
"pass": "applications/client_ip", def client_ip(options):
}, assert 'success' in client.conf(
"[::1]:7082": { {
"client_ip": options, "127.0.0.1:7081": {
"pass": "applications/client_ip", "client_ip": options,
}, "pass": "applications/client_ip",
f"unix:{option.temp_dir}/sock": {
"client_ip": options,
"pass": "applications/client_ip",
},
}, },
'listeners', "[::1]:7082": {
), 'listeners configure' "client_ip": options,
"pass": "applications/client_ip",
},
f"unix:{option.temp_dir}/sock": {
"client_ip": options,
"pass": "applications/client_ip",
},
},
'listeners',
), 'listeners configure'
def get_xff(self, xff, sock_type='ipv4'):
address = { def get_xff(xff, sock_type='ipv4'):
'ipv4': ('127.0.0.1', 7081), address = {
'ipv6': ('::1', 7082), 'ipv4': ('127.0.0.1', 7081),
'unix': (f'{option.temp_dir}/sock', None), 'ipv6': ('::1', 7082),
'unix': (f'{option.temp_dir}/sock', None),
}
(addr, port) = address[sock_type]
return client.get(
sock_type=sock_type,
addr=addr,
port=port,
headers={'Connection': 'close', 'X-Forwarded-For': xff},
)['body']
def test_client_ip_single_ip():
client_ip({'header': 'X-Forwarded-For', 'source': '123.123.123.123'})
assert client.get(port=7081)['body'] == '127.0.0.1', 'ipv4 default'
assert (
client.get(sock_type='ipv6', port=7082)['body'] == '::1'
), 'ipv6 default'
assert get_xff('1.1.1.1') == '127.0.0.1', 'bad source'
assert get_xff('blah') == '127.0.0.1', 'bad header'
assert get_xff('1.1.1.1', 'ipv6') == '::1', 'bad source ipv6'
client_ip({'header': 'X-Forwarded-For', 'source': '127.0.0.1'})
assert client.get(port=7081)['body'] == '127.0.0.1', 'ipv4 default 2'
assert (
client.get(sock_type='ipv6', port=7082)['body'] == '::1'
), 'ipv6 default 2'
assert get_xff('1.1.1.1') == '1.1.1.1', 'replace'
assert get_xff('blah') == '127.0.0.1', 'bad header 2'
assert get_xff('1.1.1.1', 'ipv6') == '::1', 'bad source ipv6 2'
client_ip({'header': 'X-Forwarded-For', 'source': '!127.0.0.1'})
assert get_xff('1.1.1.1') == '127.0.0.1', 'bad source 3'
assert get_xff('1.1.1.1', 'ipv6') == '1.1.1.1', 'replace 2'
def test_client_ip_ipv4():
client_ip({'header': 'X-Forwarded-For', 'source': '127.0.0.1'})
assert get_xff('8.8.8.8, 84.23.23.11') == '84.23.23.11', 'xff replace'
assert (
get_xff('8.8.8.8, 84.23.23.11, 127.0.0.1') == '127.0.0.1'
), 'xff replace 2'
assert (
get_xff(['8.8.8.8', '127.0.0.1, 10.0.1.1']) == '10.0.1.1'
), 'xff replace multi'
def test_client_ip_ipv6():
client_ip({'header': 'X-Forwarded-For', 'source': '::1'})
assert get_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 get_xff(ip, 'ipv6') == ip, 'replace'
def test_client_ip_unix():
client_ip({'header': 'X-Forwarded-For', 'source': 'unix'})
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'
def test_client_ip_recursive():
client_ip(
{
'header': 'X-Forwarded-For',
'recursive': True,
'source': ['127.0.0.1', '10.50.0.17', '10.5.2.1'],
} }
(addr, port) = address[sock_type] )
return self.get( assert get_xff('1.1.1.1') == '1.1.1.1', 'xff chain'
sock_type=sock_type, assert get_xff('1.1.1.1, 10.5.2.1') == '1.1.1.1', 'xff chain 2'
addr=addr, assert get_xff('8.8.8.8, 1.1.1.1, 10.5.2.1') == '1.1.1.1', 'xff chain 3'
port=port, assert (
headers={'Connection': 'close', 'X-Forwarded-For': xff}, get_xff('10.50.0.17, 10.5.2.1, 10.5.2.1') == '10.50.0.17'
)['body'] ), 'xff chain 4'
assert (
get_xff(['8.8.8.8', '1.1.1.1, 127.0.0.1']) == '1.1.1.1'
), 'xff replace multi'
assert (
get_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 (
get_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 (
get_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_client_ip_single_ip(self):
self.client_ip(
{'header': 'X-Forwarded-For', 'source': '123.123.123.123'}
)
assert self.get(port=7081)['body'] == '127.0.0.1', 'ipv4 default' def test_client_ip_case_insensitive():
assert ( client_ip({'header': 'x-forwarded-for', 'source': '127.0.0.1'})
self.get(sock_type='ipv6', port=7082)['body'] == '::1'
), 'ipv6 default'
assert self.get_xff('1.1.1.1') == '127.0.0.1', 'bad source'
assert self.get_xff('blah') == '127.0.0.1', 'bad header'
assert self.get_xff('1.1.1.1', 'ipv6') == '::1', 'bad source ipv6'
self.client_ip({'header': 'X-Forwarded-For', 'source': '127.0.0.1'}) assert get_xff('1.1.1.1') == '1.1.1.1', 'case insensitive'
assert self.get(port=7081)['body'] == '127.0.0.1', 'ipv4 default 2'
assert (
self.get(sock_type='ipv6', port=7082)['body'] == '::1'
), 'ipv6 default 2'
assert self.get_xff('1.1.1.1') == '1.1.1.1', 'replace'
assert self.get_xff('blah') == '127.0.0.1', 'bad header 2'
assert self.get_xff('1.1.1.1', 'ipv6') == '::1', 'bad source ipv6 2'
self.client_ip({'header': 'X-Forwarded-For', 'source': '!127.0.0.1'}) def test_client_ip_empty_source():
client_ip({'header': 'X-Forwarded-For', 'source': []})
assert self.get_xff('1.1.1.1') == '127.0.0.1', 'bad source 3' assert get_xff('1.1.1.1') == '127.0.0.1', 'empty source'
assert self.get_xff('1.1.1.1', 'ipv6') == '1.1.1.1', 'replace 2'
def test_client_ip_ipv4(self):
self.client_ip({'header': 'X-Forwarded-For', 'source': '127.0.0.1'})
assert ( def test_client_ip_invalid():
self.get_xff('8.8.8.8, 84.23.23.11') == '84.23.23.11' assert 'error' in client.conf(
), 'xff replace' {
assert ( "127.0.0.1:7081": {
self.get_xff('8.8.8.8, 84.23.23.11, 127.0.0.1') == '127.0.0.1' "client_ip": {"source": '127.0.0.1'},
), 'xff replace 2' "pass": "applications/client_ip",
assert (
self.get_xff(['8.8.8.8', '127.0.0.1, 10.0.1.1']) == '10.0.1.1'
), 'xff replace multi'
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'
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_xff(ip, 'ipv6') == ip, 'replace'
def test_client_ip_unix(self):
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',
'recursive': True,
'source': ['127.0.0.1', '10.50.0.17', '10.5.2.1'],
} }
) },
'listeners',
), 'invalid header'
assert self.get_xff('1.1.1.1') == '1.1.1.1', 'xff chain' def check_invalid_source(source):
assert self.get_xff('1.1.1.1, 10.5.2.1') == '1.1.1.1', 'xff chain 2' assert 'error' in client.conf(
assert (
self.get_xff('8.8.8.8, 1.1.1.1, 10.5.2.1') == '1.1.1.1'
), 'xff chain 3'
assert (
self.get_xff('10.50.0.17, 10.5.2.1, 10.5.2.1') == '10.50.0.17'
), 'xff chain 4'
assert (
self.get_xff(['8.8.8.8', '1.1.1.1, 127.0.0.1']) == '1.1.1.1'
), 'xff replace multi'
assert (
self.get_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_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_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_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(
{ {
"127.0.0.1:7081": { "127.0.0.1:7081": {
"client_ip": {"source": '127.0.0.1'}, "client_ip": {
"header": "X-Forwarded-For",
"source": source,
},
"pass": "applications/client_ip", "pass": "applications/client_ip",
} }
}, },
'listeners', 'listeners',
), 'invalid header' ), 'invalid source'
def check_invalid_source(source): check_invalid_source(None)
assert 'error' in self.conf( check_invalid_source('a')
{ check_invalid_source(['a'])
"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'])

View File

@@ -1,438 +1,465 @@
import socket import socket
import pytest import pytest
from unit.control import TestControl from unit.control import Control
prerequisites = {'modules': {'python': 'any'}} prerequisites = {'modules': {'python': 'any'}}
client = Control()
class TestConfiguration(TestControl):
def try_addr(self, addr): def try_addr(addr):
return self.conf( return client.conf(
{ {
"listeners": {addr: {"pass": "routes"}}, "listeners": {addr: {"pass": "routes"}},
"routes": [{"action": {"return": 200}}], "routes": [{"action": {"return": 200}}],
"applications": {}, "applications": {},
}
)
def test_json_empty():
assert 'error' in client.conf(''), 'empty'
def test_json_leading_zero():
assert 'error' in client.conf('00'), 'leading zero'
def test_json_unicode():
assert 'success' in client.conf(
"""
{
"ap\u0070": {
"type": "\u0070ython",
"processes": { "spare": 0 },
"path": "\u002Fapp",
"module": "wsgi"
} }
) }
""",
'applications',
), 'unicode'
def test_json_empty(self): assert client.conf_get('applications') == {
assert 'error' in self.conf(''), 'empty' "app": {
"type": "python",
"processes": {"spare": 0},
"path": "/app",
"module": "wsgi",
}
}, 'unicode get'
def test_json_leading_zero(self):
assert 'error' in self.conf('00'), 'leading zero'
def test_json_unicode(self): def test_json_unicode_2():
assert 'success' in self.conf( assert 'success' in client.conf(
""" {
{ "приложение": {
"ap\u0070": {
"type": "\u0070ython",
"processes": { "spare": 0 },
"path": "\u002Fapp",
"module": "wsgi"
}
}
""",
'applications',
), 'unicode'
assert self.conf_get('applications') == {
"app": {
"type": "python", "type": "python",
"processes": {"spare": 0}, "processes": {"spare": 0},
"path": "/app", "path": "/app",
"module": "wsgi", "module": "wsgi",
} }
}, 'unicode get' },
'applications',
), 'unicode 2'
def test_json_unicode_2(self): assert 'приложение' in client.conf_get('applications')
assert 'success' in self.conf(
{
"приложение": {
"type": "python",
"processes": {"spare": 0},
"path": "/app",
"module": "wsgi",
}
},
'applications',
), 'unicode 2'
assert 'приложение' in self.conf_get('applications'), 'unicode 2 get'
def test_json_unicode_number(self): def test_json_unicode_number():
assert 'success' in self.conf( assert 'success' in client.conf(
""" """
{ {
"app": { "app": {
"type": "python", "type": "python",
"processes": { "spare": \u0030 }, "processes": { "spare": \u0030 },
"path": "/app", "path": "/app",
"module": "wsgi" "module": "wsgi"
}
} }
""",
'applications',
), 'unicode number'
def test_json_utf8_bom(self):
assert 'success' in self.conf(
b"""\xEF\xBB\xBF
{
"app": {
"type": "python",
"processes": {"spare": 0},
"path": "/app",
"module": "wsgi"
}
}
""",
'applications',
), 'UTF-8 BOM'
def test_json_comment_single_line(self):
assert 'success' in self.conf(
b"""
// this is bridge
{
"//app": {
"type": "python", // end line
"processes": {"spare": 0},
// inside of block
"path": "/app",
"module": "wsgi"
}
// double //
}
// end of json \xEF\t
""",
'applications',
), 'single line comments'
def test_json_comment_multi_line(self):
assert 'success' in self.conf(
b"""
/* this is bridge */
{
"/*app": {
/**
* multiple lines
**/
"type": "python",
"processes": /* inline */ {"spare": 0},
"path": "/app",
"module": "wsgi"
/*
// end of block */
}
/* blah * / blah /* blah */
}
/* end of json \xEF\t\b */
""",
'applications',
), 'multi line comments'
def test_json_comment_invalid(self):
assert 'error' in self.conf(b'/{}', 'applications'), 'slash'
assert 'error' in self.conf(b'//{}', 'applications'), 'comment'
assert 'error' in self.conf(b'{} /', 'applications'), 'slash end'
assert 'error' in self.conf(b'/*{}', 'applications'), 'slash star'
assert 'error' in self.conf(b'{} /*', 'applications'), 'slash star end'
def test_applications_open_brace(self):
assert 'error' in self.conf('{', 'applications'), 'open brace'
def test_applications_string(self):
assert 'error' in self.conf('"{}"', 'applications'), 'string'
@pytest.mark.skip('not yet, unsafe')
def test_applications_type_only(self):
assert 'error' in self.conf(
{"app": {"type": "python"}}, 'applications'
), 'type only'
def test_applications_miss_quote(self):
assert 'error' in self.conf(
"""
{
app": {
"type": "python",
"processes": { "spare": 0 },
"path": "/app",
"module": "wsgi"
}
}
""",
'applications',
), 'miss quote'
def test_applications_miss_colon(self):
assert 'error' in self.conf(
"""
{
"app" {
"type": "python",
"processes": { "spare": 0 },
"path": "/app",
"module": "wsgi"
}
}
""",
'applications',
), 'miss colon'
def test_applications_miss_comma(self):
assert 'error' in self.conf(
"""
{
"app": {
"type": "python"
"processes": { "spare": 0 },
"path": "/app",
"module": "wsgi"
}
}
""",
'applications',
), 'miss comma'
def test_applications_skip_spaces(self):
assert 'success' in self.conf(
b'{ \n\r\t}', 'applications'
), 'skip spaces'
def test_applications_relative_path(self):
assert 'success' in self.conf(
{
"app": {
"type": "python",
"processes": {"spare": 0},
"path": "../app",
"module": "wsgi",
}
},
'applications',
), 'relative path'
@pytest.mark.skip('not yet, unsafe')
def test_listeners_empty(self):
assert 'error' in self.conf(
{"*:7080": {}}, 'listeners'
), 'listener empty'
def test_listeners_no_app(self):
assert 'error' in self.conf(
{"*:7080": {"pass": "applications/app"}}, 'listeners'
), 'listeners no app'
def test_listeners_unix_abstract(self, system):
if 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'
assert 'success' in self.try_addr("[::1]:7082"), 'explicit ipv6'
def test_listeners_addr_error(self):
assert 'error' in self.try_addr("127.0.0.1"), 'no port'
def test_listeners_addr_error_2(self, skip_alert):
skip_alert(r'bind.*failed', r'failed to apply new conf')
assert 'error' in self.try_addr(
"[f607:7403:1e4b:6c66:33b2:843f:2517:da27]:7080"
)
def test_listeners_port_release(self):
for _ in range(10):
fail = False
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
self.conf(
{
"listeners": {"127.0.0.1:7080": {"pass": "routes"}},
"routes": [],
}
)
resp = self.conf({"listeners": {}, "applications": {}})
try:
s.bind(('127.0.0.1', 7080))
s.listen()
except OSError:
fail = True
if fail:
pytest.fail('cannot bind or listen to the address')
assert 'success' in resp, 'port release'
def test_json_application_name_large(self):
name = "X" * 1024 * 1024
assert 'success' in self.conf(
{
"listeners": {"*:7080": {"pass": f"applications/{name}"}},
"applications": {
name: {
"type": "python",
"processes": {"spare": 0},
"path": "/app",
"module": "wsgi",
}
},
}
)
@pytest.mark.skip('not yet')
def test_json_application_many(self):
apps = 999
conf = {
"applications": {
f"app-{a}": {
"type": "python",
"processes": {"spare": 0},
"path": "/app",
"module": "wsgi",
}
for a in range(apps)
},
"listeners": {
f"*:{(7000 + a)}": {"pass": f"applications/app-{a}"}
for a in range(apps)
},
} }
""",
'applications',
), 'unicode number'
assert 'success' in self.conf(conf)
def test_json_application_python_prefix(self): def test_json_utf8_bom():
conf = { assert 'success' in client.conf(
"applications": { b"""\xEF\xBB\xBF
"sub-app": { {
"type": "python", "app": {
"processes": {"spare": 0}, "type": "python",
"path": "/app", "processes": {"spare": 0},
"module": "wsgi", "path": "/app",
"prefix": "/app", "module": "wsgi"
} }
}, }
"listeners": {"*:7080": {"pass": "routes"}}, """,
"routes": [ 'applications',
), 'UTF-8 BOM'
def test_json_comment_single_line():
assert 'success' in client.conf(
b"""
// this is bridge
{
"//app": {
"type": "python", // end line
"processes": {"spare": 0},
// inside of block
"path": "/app",
"module": "wsgi"
}
// double //
}
// end of json \xEF\t
""",
'applications',
), 'single line comments'
def test_json_comment_multi_line():
assert 'success' in client.conf(
b"""
/* this is bridge */
{
"/*app": {
/**
* multiple lines
**/
"type": "python",
"processes": /* inline */ {"spare": 0},
"path": "/app",
"module": "wsgi"
/*
// end of block */
}
/* blah * / blah /* blah */
}
/* end of json \xEF\t\b */
""",
'applications',
), 'multi line comments'
def test_json_comment_invalid():
assert 'error' in client.conf(b'/{}', 'applications'), 'slash'
assert 'error' in client.conf(b'//{}', 'applications'), 'comment'
assert 'error' in client.conf(b'{} /', 'applications'), 'slash end'
assert 'error' in client.conf(b'/*{}', 'applications'), 'slash star'
assert 'error' in client.conf(b'{} /*', 'applications'), 'slash star end'
def test_applications_open_brace():
assert 'error' in client.conf('{', 'applications'), 'open brace'
def test_applications_string():
assert 'error' in client.conf('"{}"', 'applications'), 'string'
@pytest.mark.skip('not yet, unsafe')
def test_applications_type_only():
assert 'error' in client.conf(
{"app": {"type": "python"}}, 'applications'
), 'type only'
def test_applications_miss_quote():
assert 'error' in client.conf(
"""
{
app": {
"type": "python",
"processes": { "spare": 0 },
"path": "/app",
"module": "wsgi"
}
}
""",
'applications',
), 'miss quote'
def test_applications_miss_colon():
assert 'error' in client.conf(
"""
{
"app" {
"type": "python",
"processes": { "spare": 0 },
"path": "/app",
"module": "wsgi"
}
}
""",
'applications',
), 'miss colon'
def test_applications_miss_comma():
assert 'error' in client.conf(
"""
{
"app": {
"type": "python"
"processes": { "spare": 0 },
"path": "/app",
"module": "wsgi"
}
}
""",
'applications',
), 'miss comma'
def test_applications_skip_spaces():
assert 'success' in client.conf(b'{ \n\r\t}', 'applications'), 'skip spaces'
def test_applications_relative_path():
assert 'success' in client.conf(
{
"app": {
"type": "python",
"processes": {"spare": 0},
"path": "../app",
"module": "wsgi",
}
},
'applications',
), 'relative path'
@pytest.mark.skip('not yet, unsafe')
def test_listeners_empty():
assert 'error' in client.conf({"*:7080": {}}, 'listeners'), 'listener empty'
def test_listeners_no_app():
assert 'error' in client.conf(
{"*:7080": {"pass": "applications/app"}}, 'listeners'
), 'listeners no app'
def test_listeners_unix_abstract(system):
if system != 'Linux':
assert 'error' in try_addr("unix:@sock"), 'abstract at'
pytest.skip('not yet')
assert 'error' in try_addr("unix:\0soc"), 'abstract \0'
assert 'error' in try_addr("unix:\u0000soc"), 'abstract \0 unicode'
def test_listeners_addr():
assert 'success' in try_addr("*:7080"), 'wildcard'
assert 'success' in try_addr("127.0.0.1:7081"), 'explicit'
assert 'success' in try_addr("[::1]:7082"), 'explicit ipv6'
def test_listeners_addr_error():
assert 'error' in try_addr("127.0.0.1"), 'no port'
def test_listeners_addr_error_2(skip_alert):
skip_alert(r'bind.*failed', r'failed to apply new conf')
assert 'error' in try_addr("[f607:7403:1e4b:6c66:33b2:843f:2517:da27]:7080")
def test_listeners_port_release():
for _ in range(10):
fail = False
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
client.conf(
{ {
"match": {"uri": "/app/*"}, "listeners": {"127.0.0.1:7080": {"pass": "routes"}},
"action": {"pass": "applications/sub-app"}, "routes": [],
} }
], )
}
assert 'success' in self.conf(conf) resp = client.conf({"listeners": {}, "applications": {}})
def test_json_application_prefix_target(self): try:
conf = { s.bind(('127.0.0.1', 7080))
s.listen()
except OSError:
fail = True
if fail:
pytest.fail('cannot bind or listen to the address')
assert 'success' in resp, 'port release'
def test_json_application_name_large():
name = "X" * 1024 * 1024
assert 'success' in client.conf(
{
"listeners": {"*:7080": {"pass": f"applications/{name}"}},
"applications": { "applications": {
"sub-app": { name: {
"type": "python", "type": "python",
"processes": {"spare": 0}, "processes": {"spare": 0},
"path": "/app", "path": "/app",
"targets": { "module": "wsgi",
"foo": {"module": "foo.wsgi", "prefix": "/app"}, }
"bar": { },
"module": "bar.wsgi", }
"callable": "bar", )
"prefix": "/api",
},
@pytest.mark.skip('not yet')
def test_json_application_many():
apps = 999
conf = {
"applications": {
f"app-{a}": {
"type": "python",
"processes": {"spare": 0},
"path": "/app",
"module": "wsgi",
}
for a in range(apps)
},
"listeners": {
f"*:{(7000 + a)}": {"pass": f"applications/app-{a}"}
for a in range(apps)
},
}
assert 'success' in client.conf(conf)
def test_json_application_python_prefix():
conf = {
"applications": {
"sub-app": {
"type": "python",
"processes": {"spare": 0},
"path": "/app",
"module": "wsgi",
"prefix": "/app",
}
},
"listeners": {"*:7080": {"pass": "routes"}},
"routes": [
{
"match": {"uri": "/app/*"},
"action": {"pass": "applications/sub-app"},
}
],
}
assert 'success' in client.conf(conf)
def test_json_application_prefix_target():
conf = {
"applications": {
"sub-app": {
"type": "python",
"processes": {"spare": 0},
"path": "/app",
"targets": {
"foo": {"module": "foo.wsgi", "prefix": "/app"},
"bar": {
"module": "bar.wsgi",
"callable": "bar",
"prefix": "/api",
}, },
}
},
"listeners": {"*:7080": {"pass": "routes"}},
"routes": [
{
"match": {"uri": "/app/*"},
"action": {"pass": "applications/sub-app/foo"},
}, },
{ }
"match": {"uri": "/api/*"}, },
"action": {"pass": "applications/sub-app/bar"}, "listeners": {"*:7080": {"pass": "routes"}},
}, "routes": [
],
}
assert 'success' in self.conf(conf)
def test_json_application_invalid_python_prefix(self):
conf = {
"applications": {
"sub-app": {
"type": "python",
"processes": {"spare": 0},
"path": "/app",
"module": "wsgi",
"prefix": "app",
}
},
"listeners": {"*:7080": {"pass": "applications/sub-app"}},
}
assert 'error' in self.conf(conf)
def test_json_application_empty_python_prefix(self):
conf = {
"applications": {
"sub-app": {
"type": "python",
"processes": {"spare": 0},
"path": "/app",
"module": "wsgi",
"prefix": "",
}
},
"listeners": {"*:7080": {"pass": "applications/sub-app"}},
}
assert 'error' in self.conf(conf)
def test_json_application_many2(self):
conf = {
"applications": {
f"app-{a}": {
"type": "python",
"processes": {"spare": 0},
"path": "/app",
"module": "wsgi",
}
# Larger number of applications can cause test fail with default
# open files limit due to the lack of file descriptors.
for a in range(100)
},
"listeners": {"*:7080": {"pass": "applications/app-1"}},
}
assert 'success' in self.conf(conf)
def test_unprivileged_user_error(self, require, skip_alert):
require({'privileged_user': False})
skip_alert(r'cannot set user "root"', r'failed to apply new conf')
assert 'error' in self.conf(
{ {
"app": { "match": {"uri": "/app/*"},
"type": "external", "action": {"pass": "applications/sub-app/foo"},
"processes": 1,
"executable": "/app",
"user": "root",
}
}, },
'applications', {
), 'setting user' "match": {"uri": "/api/*"},
"action": {"pass": "applications/sub-app/bar"},
},
],
}
assert 'success' in client.conf(conf)
def test_json_application_invalid_python_prefix():
conf = {
"applications": {
"sub-app": {
"type": "python",
"processes": {"spare": 0},
"path": "/app",
"module": "wsgi",
"prefix": "app",
}
},
"listeners": {"*:7080": {"pass": "applications/sub-app"}},
}
assert 'error' in client.conf(conf)
def test_json_application_empty_python_prefix():
conf = {
"applications": {
"sub-app": {
"type": "python",
"processes": {"spare": 0},
"path": "/app",
"module": "wsgi",
"prefix": "",
}
},
"listeners": {"*:7080": {"pass": "applications/sub-app"}},
}
assert 'error' in client.conf(conf)
def test_json_application_many2():
conf = {
"applications": {
f"app-{a}": {
"type": "python",
"processes": {"spare": 0},
"path": "/app",
"module": "wsgi",
}
# Larger number of applications can cause test fail with default
# open files limit due to the lack of file descriptors.
for a in range(100)
},
"listeners": {"*:7080": {"pass": "applications/app-1"}},
}
assert 'success' in client.conf(conf)
def test_unprivileged_user_error(require, skip_alert):
require({'privileged_user': False})
skip_alert(r'cannot set user "root"', r'failed to apply new conf')
assert 'error' in client.conf(
{
"app": {
"type": "external",
"processes": 1,
"executable": "/app",
"user": "root",
}
},
'applications',
), 'setting user'

View File

@@ -1,266 +1,270 @@
import pytest import pytest
from unit.applications.lang.python import TestApplicationPython from unit.applications.lang.python import ApplicationPython
prerequisites = {'modules': {'python': 'any'}} prerequisites = {'modules': {'python': 'any'}}
client = ApplicationPython()
class TestForwardedHeader(TestApplicationPython):
@pytest.fixture(autouse=True)
def setup_method_fixture(self):
self.load('forwarded_header')
def forwarded_header(self, forwarded): @pytest.fixture(autouse=True)
assert 'success' in self.conf( def setup_method_fixture():
{ client.load('forwarded_header')
"127.0.0.1:7081": {
"forwarded": forwarded,
"pass": "applications/forwarded_header", def forwarded_header(forwarded):
}, assert 'success' in client.conf(
"[::1]:7082": { {
"forwarded": forwarded, "127.0.0.1:7081": {
"pass": "applications/forwarded_header", "forwarded": forwarded,
}, "pass": "applications/forwarded_header",
}, },
'listeners', "[::1]:7082": {
), 'listeners configure' "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'} def get_fwd(sock_type='ipv4', xff=None, xfp=None):
port = 7081 if sock_type == 'ipv4' else 7082
if xff is not None: headers = {'Connection': 'close'}
headers['X-Forwarded-For'] = xff
if xfp is not None: if xff is not None:
headers['X-Forwarded-Proto'] = xfp headers['X-Forwarded-For'] = xff
return self.get(sock_type=sock_type, port=port, headers=headers)[ if xfp is not None:
'headers' headers['X-Forwarded-Proto'] = xfp
]
def get_addr(self, *args, **kwargs): return client.get(sock_type=sock_type, port=port, headers=headers)[
return self.get_fwd(*args, **kwargs)['Remote-Addr'] 'headers'
]
def get_scheme(self, *args, **kwargs):
return self.get_fwd(*args, **kwargs)['Url-Scheme']
def test_forwarded_header_single_ip(self): def get_addr(*args, **kwargs):
self.forwarded_header( return get_fwd(*args, **kwargs)['Remote-Addr']
{
'client_ip': 'X-Forwarded-For',
'protocol': 'X-Forwarded-Proto', def get_scheme(*args, **kwargs):
'source': '123.123.123.123', return get_fwd(*args, **kwargs)['Url-Scheme']
def test_forwarded_header_single_ip():
forwarded_header(
{
'client_ip': 'X-Forwarded-For',
'protocol': 'X-Forwarded-Proto',
'source': '123.123.123.123',
}
)
resp = 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 get_addr() == '127.0.0.1', 'ipv4 default addr'
assert get_addr('ipv6') == '::1', 'ipv6 default addr'
assert get_addr(xff='1.1.1.1') == '127.0.0.1', 'bad source'
assert get_addr(xff='blah') == '127.0.0.1', 'bad xff'
assert get_addr('ipv6', '1.1.1.1') == '::1', 'bad source ipv6'
assert get_scheme() == 'http', 'ipv4 default proto'
assert get_scheme('ipv6') == 'http', 'ipv6 default proto'
assert get_scheme(xfp='https') == 'http', 'bad proto'
assert get_scheme(xfp='blah') == 'http', 'bad xfp'
assert get_scheme('ipv6', xfp='https') == 'http', 'bad proto ipv6'
forwarded_header(
{
'client_ip': 'X-Forwarded-For',
'protocol': 'X-Forwarded-Proto',
'source': '127.0.0.1',
}
)
resp = 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 get_addr() == '127.0.0.1', 'ipv4 default addr 2'
assert get_addr('ipv6') == '::1', 'ipv6 default addr 2'
assert get_addr(xff='1.1.1.1') == '1.1.1.1', 'xff replace'
assert get_addr('ipv6', '1.1.1.1') == '::1', 'bad source ipv6 2'
assert get_scheme() == 'http', 'ipv4 default proto 2'
assert get_scheme('ipv6') == 'http', 'ipv6 default proto 2'
assert get_scheme(xfp='https') == 'https', 'xfp replace'
assert get_scheme(xfp='on') == 'https', 'xfp replace 2'
assert get_scheme('ipv6', xfp='https') == 'http', 'bad proto ipv6 2'
forwarded_header(
{
'client_ip': 'X-Forwarded-For',
'protocol': 'X-Forwarded-Proto',
'source': '!127.0.0.1',
}
)
assert get_addr(xff='1.1.1.1') == '127.0.0.1', 'bad source 3'
assert get_addr('ipv6', '1.1.1.1') == '1.1.1.1', 'xff replace 2'
assert get_scheme(xfp='https') == 'http', 'bad proto 2'
assert get_scheme('ipv6', xfp='https') == 'https', 'xfp replace 3'
def test_forwarded_header_ipv4():
forwarded_header(
{
'client_ip': 'X-Forwarded-For',
'protocol': 'X-Forwarded-Proto',
'source': '127.0.0.1',
}
)
assert get_addr(xff='8.8.8.8, 84.23.23.11') == '84.23.23.11', 'xff replace'
assert (
get_addr(xff='8.8.8.8, 84.23.23.11, 127.0.0.1') == '127.0.0.1'
), 'xff replace 2'
assert (
get_addr(xff=['8.8.8.8', '127.0.0.1, 10.0.1.1']) == '10.0.1.1'
), 'xff replace multi'
assert get_scheme(xfp='http, https') == 'http', 'xfp replace'
assert get_scheme(xfp='http, https, http') == 'http', 'xfp replace 2'
assert (
get_scheme(xfp=['http, https', 'http', 'https']) == 'http'
), 'xfp replace multi'
def test_forwarded_header_ipv6():
forwarded_header(
{
'client_ip': 'X-Forwarded-For',
'protocol': 'X-Forwarded-Proto',
'source': '::1',
}
)
assert 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 get_addr('ipv6', ip) == ip, 'replace'
assert get_scheme(xfp='https') == 'http', 'bad source ipv4'
for proto in ['http', 'https']:
assert get_scheme('ipv6', xfp=proto) == proto, 'replace'
def test_forwarded_header_recursive():
forwarded_header(
{
'client_ip': 'X-Forwarded-For',
'recursive': True,
'source': ['127.0.0.1', '10.50.0.17', '10.5.2.1'],
}
)
assert get_addr(xff='1.1.1.1') == '1.1.1.1', 'xff chain'
assert get_addr(xff='1.1.1.1, 10.5.2.1') == '1.1.1.1', 'xff chain 2'
assert (
get_addr(xff='8.8.8.8, 1.1.1.1, 10.5.2.1') == '1.1.1.1'
), 'xff chain 3'
assert (
get_addr(xff='10.50.0.17, 10.5.2.1, 10.5.2.1') == '10.50.0.17'
), 'xff chain 4'
assert (
get_addr(xff=['8.8.8.8', '1.1.1.1, 127.0.0.1']) == '1.1.1.1'
), 'xff replace multi'
assert (
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 (
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 (
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():
forwarded_header(
{
'client_ip': 'x-forwarded-for',
'protocol': 'x-forwarded-proto',
'source': '127.0.0.1',
}
)
assert get_addr() == '127.0.0.1', 'ipv4 default addr'
assert get_addr('ipv6') == '::1', 'ipv6 default addr'
assert get_addr(xff='1.1.1.1') == '1.1.1.1', 'replace'
assert get_scheme() == 'http', 'ipv4 default proto'
assert get_scheme('ipv6') == 'http', 'ipv6 default proto'
assert get_scheme(xfp='https') == 'https', 'replace 1'
assert get_scheme(xfp='oN') == 'https', 'replace 2'
def test_forwarded_header_source_empty():
forwarded_header(
{
'client_ip': 'X-Forwarded-For',
'protocol': 'X-Forwarded-Proto',
'source': [],
}
)
assert get_addr(xff='1.1.1.1') == '127.0.0.1', 'empty source xff'
assert get_scheme(xfp='https') == 'http', 'empty source xfp'
def test_forwarded_header_source_range():
forwarded_header(
{
'client_ip': 'X-Forwarded-For',
'protocol': 'X-Forwarded-Proto',
'source': '127.0.0.0-127.0.0.1',
}
)
assert get_addr(xff='1.1.1.1') == '1.1.1.1', 'source range'
assert get_addr('ipv6', '1.1.1.1') == '::1', 'source range 2'
def test_forwarded_header_invalid():
assert 'error' in client.conf(
{
"127.0.0.1:7081": {
"forwarded": {"source": '127.0.0.1'},
"pass": "applications/forwarded_header",
} }
) },
'listeners',
), 'invalid forward'
resp = self.get_fwd(xff='1.1.1.1', xfp='https') def check_invalid_source(source):
assert resp['Remote-Addr'] == '127.0.0.1', 'both headers addr' assert 'error' in client.conf(
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": { "127.0.0.1:7081": {
"forwarded": {"source": '127.0.0.1'}, "forwarded": {
"client_ip": "X-Forwarded-For",
"source": source,
},
"pass": "applications/forwarded_header", "pass": "applications/forwarded_header",
} }
}, },
'listeners', 'listeners',
), 'invalid forward' ), 'invalid source'
def check_invalid_source(source): check_invalid_source(None)
assert 'error' in self.conf( check_invalid_source('a')
{ check_invalid_source(['a'])
"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'])

View File

@@ -1,161 +1,168 @@
import re import re
import pytest from unit.applications.lang.go import ApplicationGo
from unit.applications.lang.go import TestApplicationGo
prerequisites = {'modules': {'go': 'all'}} prerequisites = {'modules': {'go': 'all'}}
client = ApplicationGo()
class TestGoApplication(TestApplicationGo):
def test_go_application_variables(self, date_to_sec_epoch, sec_epoch):
self.load('variables')
body = 'Test body string.' def test_go_application_variables(date_to_sec_epoch, sec_epoch):
client.load('variables')
resp = self.post( body = 'Test body string.'
headers={
'Host': 'localhost',
'Content-Type': 'text/html',
'Custom-Header': 'blah',
'Connection': 'close',
},
body=body,
)
assert resp['status'] == 200, 'status' resp = client.post(
headers = resp['headers'] headers={
header_server = headers.pop('Server') 'Host': 'localhost',
assert re.search(r'Unit/[\d\.]+', header_server), 'server header'
date = headers.pop('Date')
assert date[-4:] == ' GMT', 'date header timezone'
assert abs(date_to_sec_epoch(date) - sec_epoch) < 5, 'date header'
assert headers == {
'Content-Length': str(len(body)),
'Content-Type': 'text/html', 'Content-Type': 'text/html',
'Request-Method': 'POST',
'Request-Uri': '/',
'Http-Host': 'localhost',
'Server-Protocol': 'HTTP/1.1',
'Server-Protocol-Major': '1',
'Server-Protocol-Minor': '1',
'Custom-Header': 'blah', 'Custom-Header': 'blah',
'Connection': 'close', 'Connection': 'close',
}, 'headers' },
assert resp['body'] == body, 'body' body=body,
)
def test_go_application_get_variables(self): assert resp['status'] == 200, 'status'
self.load('get_variables') headers = resp['headers']
header_server = headers.pop('Server')
assert re.search(r'Unit/[\d\.]+', header_server), 'server header'
resp = self.get(url='/?var1=val1&var2=&var3') date = headers.pop('Date')
assert resp['headers']['X-Var-1'] == 'val1', 'GET variables' assert date[-4:] == ' GMT', 'date header timezone'
assert resp['headers']['X-Var-2'] == '', 'GET variables 2' assert abs(date_to_sec_epoch(date) - sec_epoch) < 5, 'date header'
assert resp['headers']['X-Var-3'] == '', 'GET variables 3'
def test_go_application_post_variables(self): assert headers == {
self.load('post_variables') 'Content-Length': str(len(body)),
'Content-Type': 'text/html',
'Request-Method': 'POST',
'Request-Uri': '/',
'Http-Host': 'localhost',
'Server-Protocol': 'HTTP/1.1',
'Server-Protocol-Major': '1',
'Server-Protocol-Minor': '1',
'Custom-Header': 'blah',
'Connection': 'close',
}, 'headers'
assert resp['body'] == body, 'body'
resp = self.post(
headers={
'Host': 'localhost',
'Content-Type': 'application/x-www-form-urlencoded',
'Connection': 'close',
},
body='var1=val1&var2=&var3',
)
assert resp['headers']['X-Var-1'] == 'val1', 'POST variables' def test_go_application_get_variables():
assert resp['headers']['X-Var-2'] == '', 'POST variables 2' client.load('get_variables')
assert resp['headers']['X-Var-3'] == '', 'POST variables 3'
def test_go_application_404(self): resp = client.get(url='/?var1=val1&var2=&var3')
self.load('404') assert resp['headers']['X-Var-1'] == 'val1', 'GET variables'
assert resp['headers']['X-Var-2'] == '', 'GET variables 2'
assert resp['headers']['X-Var-3'] == '', 'GET variables 3'
resp = self.get()
assert resp['status'] == 404, '404 status' def test_go_application_post_variables():
assert re.search( client.load('post_variables')
r'<title>404 Not Found</title>', resp['body']
), '404 body'
def test_go_keepalive_body(self): resp = client.post(
self.load('mirror') headers={
'Host': 'localhost',
'Content-Type': 'application/x-www-form-urlencoded',
'Connection': 'close',
},
body='var1=val1&var2=&var3',
)
assert self.get()['status'] == 200, 'init' assert resp['headers']['X-Var-1'] == 'val1', 'POST variables'
assert resp['headers']['X-Var-2'] == '', 'POST variables 2'
assert resp['headers']['X-Var-3'] == '', 'POST variables 3'
body = '0123456789' * 500
(resp, sock) = self.post(
headers={
'Host': 'localhost',
'Connection': 'keep-alive',
},
start=True,
body=body,
read_timeout=1,
)
assert resp['body'] == body, 'keep-alive 1' def test_go_application_404():
client.load('404')
body = '0123456789' resp = client.get()
resp = self.post(sock=sock, body=body)
assert resp['body'] == body, 'keep-alive 2'
def test_go_application_cookies(self): assert resp['status'] == 404, '404 status'
self.load('cookies') assert re.search(r'<title>404 Not Found</title>', resp['body']), '404 body'
resp = self.get(
headers={
'Host': 'localhost',
'Cookie': 'var1=val1; var2=val2',
'Connection': 'close',
}
)
assert resp['headers']['X-Cookie-1'] == 'val1', 'cookie 1' def test_go_keepalive_body():
assert resp['headers']['X-Cookie-2'] == 'val2', 'cookie 2' client.load('mirror')
def test_go_application_command_line_arguments_type(self): assert client.get()['status'] == 200, 'init'
self.load('command_line_arguments')
assert 'error' in self.conf( body = '0123456789' * 500
'' "a b c", 'applications/command_line_arguments/arguments' (resp, sock) = client.post(
), 'arguments type' headers={
'Host': 'localhost',
'Connection': 'keep-alive',
},
start=True,
body=body,
read_timeout=1,
)
def test_go_application_command_line_arguments_0(self): assert resp['body'] == body, 'keep-alive 1'
self.load('command_line_arguments')
assert self.get()['headers']['X-Arg-0'] == self.conf_get( body = '0123456789'
'applications/command_line_arguments/executable' resp = client.post(sock=sock, body=body)
), 'argument 0' assert resp['body'] == body, 'keep-alive 2'
def test_go_application_command_line_arguments(self):
self.load('command_line_arguments')
arg1 = '--cc=gcc-7.2.0' def test_go_application_cookies():
arg2 = "--cc-opt='-O0 -DNXT_DEBUG_MEMORY=1 -fsanitize=address'" client.load('cookies')
arg3 = '--debug'
assert 'success' in self.conf( resp = client.get(
f'["{arg1}", "{arg2}", "{arg3}"]', headers={
'applications/command_line_arguments/arguments', 'Host': 'localhost',
) 'Cookie': 'var1=val1; var2=val2',
'Connection': 'close',
}
)
assert self.get()['body'] == f'{arg1},{arg2},{arg3}', 'arguments' assert resp['headers']['X-Cookie-1'] == 'val1', 'cookie 1'
assert resp['headers']['X-Cookie-2'] == 'val2', 'cookie 2'
def test_go_application_command_line_arguments_change(self):
self.load('command_line_arguments')
args_path = 'applications/command_line_arguments/arguments' def test_go_application_command_line_arguments_type():
client.load('command_line_arguments')
assert 'success' in self.conf('["0", "a", "$", ""]', args_path) assert 'error' in client.conf(
'' "a b c", 'applications/command_line_arguments/arguments'
), 'arguments type'
assert self.get()['body'] == '0,a,$,', 'arguments'
assert 'success' in self.conf('["-1", "b", "%"]', args_path) def test_go_application_command_line_arguments_0():
client.load('command_line_arguments')
assert self.get()['body'] == '-1,b,%', 'arguments change' assert client.get()['headers']['X-Arg-0'] == client.conf_get(
'applications/command_line_arguments/executable'
), 'argument 0'
assert 'success' in self.conf('[]', args_path)
assert self.get()['headers']['Content-Length'] == '0', 'arguments empty' def test_go_application_command_line_arguments():
client.load('command_line_arguments')
arg1 = '--cc=gcc-7.2.0'
arg2 = "--cc-opt='-O0 -DNXT_DEBUG_MEMORY=1 -fsanitize=address'"
arg3 = '--debug'
assert 'success' in client.conf(
f'["{arg1}", "{arg2}", "{arg3}"]',
'applications/command_line_arguments/arguments',
)
assert client.get()['body'] == f'{arg1},{arg2},{arg3}', 'arguments'
def test_go_application_command_line_arguments_change():
client.load('command_line_arguments')
args_path = 'applications/command_line_arguments/arguments'
assert 'success' in client.conf('["0", "a", "$", ""]', args_path)
assert client.get()['body'] == '0,a,$,', 'arguments'
assert 'success' in client.conf('["-1", "b", "%"]', args_path)
assert client.get()['body'] == '-1,b,%', 'arguments change'
assert 'success' in client.conf('[]', args_path)
assert client.get()['headers']['Content-Length'] == '0', 'arguments empty'

View File

@@ -3,357 +3,365 @@ import os
import pwd import pwd
import pytest import pytest
from unit.applications.lang.go import TestApplicationGo from unit.applications.lang.go import ApplicationGo
from unit.option import option from unit.option import option
from unit.utils import getns from unit.utils import getns
prerequisites = {'modules': {'go': 'any'}, 'features': {'isolation': True}} prerequisites = {'modules': {'go': 'any'}, 'features': {'isolation': True}}
client = ApplicationGo()
class TestGoIsolation(TestApplicationGo):
def unpriv_creds(self):
nobody_uid = pwd.getpwnam('nobody').pw_uid
try: def unpriv_creds():
nogroup_gid = grp.getgrnam('nogroup').gr_gid nobody_uid = pwd.getpwnam('nobody').pw_uid
nogroup = 'nogroup'
except KeyError:
nogroup_gid = grp.getgrnam('nobody').gr_gid
nogroup = 'nobody'
return (nobody_uid, nogroup_gid, nogroup) try:
nogroup_gid = grp.getgrnam('nogroup').gr_gid
nogroup = 'nogroup'
except KeyError:
nogroup_gid = grp.getgrnam('nobody').gr_gid
nogroup = 'nobody'
def test_isolation_values(self): return (nobody_uid, nogroup_gid, nogroup)
self.load('ns_inspect')
obj = self.getjson()['body']
for ns, ns_value in option.available['features']['isolation'].items(): def test_isolation_values():
if ns.upper() in obj['NS']: client.load('ns_inspect')
assert obj['NS'][ns.upper()] == ns_value, f'{ns} match'
def test_isolation_unpriv_user(self, require): obj = client.getjson()['body']
for ns, ns_value in option.available['features']['isolation'].items():
if ns.upper() in obj['NS']:
assert obj['NS'][ns.upper()] == ns_value, f'{ns} match'
def test_isolation_unpriv_user(require):
require(
{
'privileged_user': False,
'features': {'isolation': ['unprivileged_userns_clone']},
}
)
client.load('ns_inspect')
obj = client.getjson()['body']
assert obj['UID'] == os.geteuid(), 'uid match'
assert obj['GID'] == os.getegid(), 'gid match'
client.load('ns_inspect', isolation={'namespaces': {'credential': True}})
obj = client.getjson()['body']
nobody_uid, nogroup_gid, nogroup = unpriv_creds()
# unprivileged unit map itself to nobody in the container by default
assert obj['UID'] == nobody_uid, 'uid of nobody'
assert obj['GID'] == nogroup_gid, f'gid of {nogroup}'
client.load(
'ns_inspect',
user='root',
isolation={'namespaces': {'credential': True}},
)
obj = client.getjson()['body']
assert obj['UID'] == 0, 'uid match user=root'
assert obj['GID'] == 0, 'gid match user=root'
client.load(
'ns_inspect',
user='root',
group=nogroup,
isolation={'namespaces': {'credential': True}},
)
obj = client.getjson()['body']
assert obj['UID'] == 0, 'uid match user=root group=nogroup'
assert obj['GID'] == nogroup_gid, 'gid match user=root group=nogroup'
client.load(
'ns_inspect',
user='root',
group='root',
isolation={
'namespaces': {'credential': True},
'uidmap': [{'container': 0, 'host': os.geteuid(), 'size': 1}],
'gidmap': [{'container': 0, 'host': os.getegid(), 'size': 1}],
},
)
obj = client.getjson()['body']
assert obj['UID'] == 0, 'uid match uidmap'
assert obj['GID'] == 0, 'gid match gidmap'
def test_isolation_priv_user(require):
require({'privileged_user': True})
client.load('ns_inspect')
nobody_uid, nogroup_gid, nogroup = unpriv_creds()
obj = client.getjson()['body']
assert obj['UID'] == nobody_uid, 'uid match'
assert obj['GID'] == nogroup_gid, 'gid match'
client.load('ns_inspect', isolation={'namespaces': {'credential': True}})
obj = client.getjson()['body']
# privileged unit map app creds in the container by default
assert obj['UID'] == nobody_uid, 'uid nobody'
assert obj['GID'] == nogroup_gid, 'gid nobody'
client.load(
'ns_inspect',
user='root',
isolation={'namespaces': {'credential': True}},
)
obj = client.getjson()['body']
assert obj['UID'] == 0, 'uid nobody user=root'
assert obj['GID'] == 0, 'gid nobody user=root'
client.load(
'ns_inspect',
user='root',
group=nogroup,
isolation={'namespaces': {'credential': True}},
)
obj = client.getjson()['body']
assert obj['UID'] == 0, 'uid match user=root group=nogroup'
assert obj['GID'] == nogroup_gid, 'gid match user=root group=nogroup'
client.load(
'ns_inspect',
user='root',
group='root',
isolation={
'namespaces': {'credential': True},
'uidmap': [{'container': 0, 'host': 0, 'size': 1}],
'gidmap': [{'container': 0, 'host': 0, 'size': 1}],
},
)
obj = client.getjson()['body']
assert obj['UID'] == 0, 'uid match uidmap user=root'
assert obj['GID'] == 0, 'gid match gidmap user=root'
# map 65535 uids
client.load(
'ns_inspect',
user='nobody',
isolation={
'namespaces': {'credential': True},
'uidmap': [{'container': 0, 'host': 0, 'size': nobody_uid + 1}],
},
)
obj = client.getjson()['body']
assert obj['UID'] == nobody_uid, 'uid match uidmap user=nobody'
assert obj['GID'] == nogroup_gid, 'gid match uidmap user=nobody'
def test_isolation_mnt(require):
require(
{
'features': {'isolation': ['unprivileged_userns_clone', 'mnt']},
}
)
client.load(
'ns_inspect',
isolation={'namespaces': {'mount': True, 'credential': True}},
)
obj = client.getjson()['body']
# all but user and mnt
allns = list(option.available['features']['isolation'].keys())
allns.remove('user')
allns.remove('mnt')
for ns in allns:
if ns.upper() in obj['NS']:
assert (
obj['NS'][ns.upper()]
== option.available['features']['isolation'][ns]
), f'{ns} match'
assert obj['NS']['MNT'] != getns('mnt'), 'mnt set'
assert obj['NS']['USER'] != getns('user'), 'user set'
def test_isolation_pid(is_su, require):
require({'features': {'isolation': ['pid']}})
if not is_su:
require( require(
{ {
'privileged_user': False, 'features': {
'features': {'isolation': ['unprivileged_userns_clone']}, 'isolation': [
'unprivileged_userns_clone',
'user',
'mnt',
]
}
} }
) )
self.load('ns_inspect') isolation = {'namespaces': {'pid': True}}
obj = self.getjson()['body']
assert obj['UID'] == os.geteuid(), 'uid match' if not is_su:
assert obj['GID'] == os.getegid(), 'gid match' isolation['namespaces']['mount'] = True
isolation['namespaces']['credential'] = True
self.load('ns_inspect', isolation={'namespaces': {'credential': True}}) client.load('ns_inspect', isolation=isolation)
obj = self.getjson()['body'] obj = client.getjson()['body']
nobody_uid, nogroup_gid, nogroup = self.unpriv_creds() assert obj['PID'] == 2, 'pid of container is 2'
# unprivileged unit map itself to nobody in the container by default
assert obj['UID'] == nobody_uid, 'uid of nobody'
assert obj['GID'] == nogroup_gid, f'gid of {nogroup}'
self.load( def test_isolation_namespace_false():
'ns_inspect', client.load('ns_inspect')
user='root', allns = list(option.available['features']['isolation'].keys())
isolation={'namespaces': {'credential': True}},
)
obj = self.getjson()['body'] remove_list = ['unprivileged_userns_clone', 'ipc', 'cgroup']
allns = [ns for ns in allns if ns not in remove_list]
assert obj['UID'] == 0, 'uid match user=root' namespaces = {}
assert obj['GID'] == 0, 'gid match user=root' for ns in allns:
if ns == 'user':
namespaces['credential'] = False
elif ns == 'mnt':
namespaces['mount'] = False
elif ns == 'net':
namespaces['network'] = False
elif ns == 'uts':
namespaces['uname'] = False
else:
namespaces[ns] = False
self.load( client.load('ns_inspect', isolation={'namespaces': namespaces})
'ns_inspect',
user='root',
group=nogroup,
isolation={'namespaces': {'credential': True}},
)
obj = self.getjson()['body'] obj = client.getjson()['body']
assert obj['UID'] == 0, 'uid match user=root group=nogroup' for ns in allns:
assert obj['GID'] == nogroup_gid, 'gid match user=root group=nogroup' if ns.upper() in obj['NS']:
assert (
obj['NS'][ns.upper()]
== option.available['features']['isolation'][ns]
), f'{ns} match'
self.load(
'ns_inspect',
user='root',
group='root',
isolation={
'namespaces': {'credential': True},
'uidmap': [{'container': 0, 'host': os.geteuid(), 'size': 1}],
'gidmap': [{'container': 0, 'host': os.getegid(), 'size': 1}],
},
)
obj = self.getjson()['body'] def test_go_isolation_rootfs_container(is_su, require, temp_dir):
if not is_su:
assert obj['UID'] == 0, 'uid match uidmap'
assert obj['GID'] == 0, 'gid match gidmap'
def test_isolation_priv_user(self, require):
require({'privileged_user': True})
self.load('ns_inspect')
nobody_uid, nogroup_gid, nogroup = self.unpriv_creds()
obj = self.getjson()['body']
assert obj['UID'] == nobody_uid, 'uid match'
assert obj['GID'] == nogroup_gid, 'gid match'
self.load('ns_inspect', isolation={'namespaces': {'credential': True}})
obj = self.getjson()['body']
# privileged unit map app creds in the container by default
assert obj['UID'] == nobody_uid, 'uid nobody'
assert obj['GID'] == nogroup_gid, 'gid nobody'
self.load(
'ns_inspect',
user='root',
isolation={'namespaces': {'credential': True}},
)
obj = self.getjson()['body']
assert obj['UID'] == 0, 'uid nobody user=root'
assert obj['GID'] == 0, 'gid nobody user=root'
self.load(
'ns_inspect',
user='root',
group=nogroup,
isolation={'namespaces': {'credential': True}},
)
obj = self.getjson()['body']
assert obj['UID'] == 0, 'uid match user=root group=nogroup'
assert obj['GID'] == nogroup_gid, 'gid match user=root group=nogroup'
self.load(
'ns_inspect',
user='root',
group='root',
isolation={
'namespaces': {'credential': True},
'uidmap': [{'container': 0, 'host': 0, 'size': 1}],
'gidmap': [{'container': 0, 'host': 0, 'size': 1}],
},
)
obj = self.getjson()['body']
assert obj['UID'] == 0, 'uid match uidmap user=root'
assert obj['GID'] == 0, 'gid match gidmap user=root'
# map 65535 uids
self.load(
'ns_inspect',
user='nobody',
isolation={
'namespaces': {'credential': True},
'uidmap': [{'container': 0, 'host': 0, 'size': nobody_uid + 1}],
},
)
obj = self.getjson()['body']
assert obj['UID'] == nobody_uid, 'uid match uidmap user=nobody'
assert obj['GID'] == nogroup_gid, 'gid match uidmap user=nobody'
def test_isolation_mnt(self, require):
require( require(
{ {
'features': {'isolation': ['unprivileged_userns_clone', 'mnt']}, 'features': {
'isolation': [
'unprivileged_userns_clone',
'user',
'mnt',
'pid',
]
}
} }
) )
self.load( isolation = {'rootfs': temp_dir}
'ns_inspect',
isolation={'namespaces': {'mount': True, 'credential': True}},
)
obj = self.getjson()['body'] if not is_su:
isolation['namespaces'] = {
# all but user and mnt 'mount': True,
allns = list(option.available['features']['isolation'].keys()) 'credential': True,
allns.remove('user') 'pid': True,
allns.remove('mnt')
for ns in allns:
if ns.upper() in obj['NS']:
assert (
obj['NS'][ns.upper()]
== option.available['features']['isolation'][ns]
), f'{ns} match'
assert obj['NS']['MNT'] != getns('mnt'), 'mnt set'
assert obj['NS']['USER'] != getns('user'), 'user set'
def test_isolation_pid(self, is_su, require):
require({'features': {'isolation': ['pid']}})
if not is_su:
require(
{
'features': {
'isolation': [
'unprivileged_userns_clone',
'user',
'mnt',
]
}
}
)
isolation = {'namespaces': {'pid': True}}
if not is_su:
isolation['namespaces']['mount'] = True
isolation['namespaces']['credential'] = True
self.load('ns_inspect', isolation=isolation)
obj = self.getjson()['body']
assert obj['PID'] == 2, 'pid of container is 2'
def test_isolation_namespace_false(self):
self.load('ns_inspect')
allns = list(option.available['features']['isolation'].keys())
remove_list = ['unprivileged_userns_clone', 'ipc', 'cgroup']
allns = [ns for ns in allns if ns not in remove_list]
namespaces = {}
for ns in allns:
if ns == 'user':
namespaces['credential'] = False
elif ns == 'mnt':
namespaces['mount'] = False
elif ns == 'net':
namespaces['network'] = False
elif ns == 'uts':
namespaces['uname'] = False
else:
namespaces[ns] = False
self.load('ns_inspect', isolation={'namespaces': namespaces})
obj = self.getjson()['body']
for ns in allns:
if ns.upper() in obj['NS']:
assert (
obj['NS'][ns.upper()]
== option.available['features']['isolation'][ns]
), f'{ns} match'
def test_go_isolation_rootfs_container(self, is_su, require, temp_dir):
if not is_su:
require(
{
'features': {
'isolation': [
'unprivileged_userns_clone',
'user',
'mnt',
'pid',
]
}
}
)
isolation = {'rootfs': temp_dir}
if not is_su:
isolation['namespaces'] = {
'mount': True,
'credential': True,
'pid': True,
}
self.load('ns_inspect', isolation=isolation)
obj = self.getjson(url='/?file=/go/app')['body']
assert obj['FileExists'], 'app relative to rootfs'
obj = self.getjson(url='/?file=/bin/sh')['body']
assert not obj['FileExists'], 'file should not exists'
def test_go_isolation_rootfs_container_priv(self, require, temp_dir):
require({'privileged_user': True, 'features': {'isolation': ['mnt']}})
isolation = {
'namespaces': {'mount': True},
'rootfs': temp_dir,
} }
self.load('ns_inspect', isolation=isolation) client.load('ns_inspect', isolation=isolation)
obj = self.getjson(url='/?file=/go/app')['body'] obj = client.getjson(url='/?file=/go/app')['body']
assert obj['FileExists'], 'app relative to rootfs' assert obj['FileExists'], 'app relative to rootfs'
obj = self.getjson(url='/?file=/bin/sh')['body'] obj = client.getjson(url='/?file=/bin/sh')['body']
assert not obj['FileExists'], 'file should not exists' assert not obj['FileExists'], 'file should not exists'
def test_go_isolation_rootfs_automount_tmpfs(
self, is_su, require, temp_dir
):
try:
open("/proc/self/mountinfo")
except:
pytest.skip('The system lacks /proc/self/mountinfo file')
if not is_su: def test_go_isolation_rootfs_container_priv(require, temp_dir):
require( require({'privileged_user': True, 'features': {'isolation': ['mnt']}})
{
'features': { isolation = {
'isolation': [ 'namespaces': {'mount': True},
'unprivileged_userns_clone', 'rootfs': temp_dir,
'user', }
'mnt',
'pid', client.load('ns_inspect', isolation=isolation)
]
} obj = client.getjson(url='/?file=/go/app')['body']
assert obj['FileExists'], 'app relative to rootfs'
obj = client.getjson(url='/?file=/bin/sh')['body']
assert not obj['FileExists'], 'file should not exists'
def test_go_isolation_rootfs_automount_tmpfs(is_su, require, temp_dir):
try:
open("/proc/self/mountinfo")
except:
pytest.skip('The system lacks /proc/self/mountinfo file')
if not is_su:
require(
{
'features': {
'isolation': [
'unprivileged_userns_clone',
'user',
'mnt',
'pid',
]
} }
)
isolation = {'rootfs': temp_dir}
if not is_su:
isolation['namespaces'] = {
'mount': True,
'credential': True,
'pid': True,
} }
)
isolation['automount'] = {'tmpfs': False} isolation = {'rootfs': temp_dir}
self.load('ns_inspect', isolation=isolation) if not is_su:
isolation['namespaces'] = {
'mount': True,
'credential': True,
'pid': True,
}
obj = self.getjson(url='/?mounts=true')['body'] isolation['automount'] = {'tmpfs': False}
assert ( client.load('ns_inspect', isolation=isolation)
"/ /tmp" not in obj['Mounts'] and "tmpfs" not in obj['Mounts']
), 'app has no /tmp mounted'
isolation['automount'] = {'tmpfs': True} obj = client.getjson(url='/?mounts=true')['body']
self.load('ns_inspect', isolation=isolation) assert (
"/ /tmp" not in obj['Mounts'] and "tmpfs" not in obj['Mounts']
), 'app has no /tmp mounted'
obj = self.getjson(url='/?mounts=true')['body'] isolation['automount'] = {'tmpfs': True}
assert ( client.load('ns_inspect', isolation=isolation)
"/ /tmp" in obj['Mounts'] and "tmpfs" in obj['Mounts']
), 'app has /tmp mounted on /' obj = client.getjson(url='/?mounts=true')['body']
assert (
"/ /tmp" in obj['Mounts'] and "tmpfs" in obj['Mounts']
), 'app has /tmp mounted on /'

View File

@@ -1,5 +1,4 @@
import pytest from unit.applications.lang.go import ApplicationGo
from unit.applications.lang.go import TestApplicationGo
prerequisites = { prerequisites = {
'modules': {'go': 'all'}, 'modules': {'go': 'all'},
@@ -7,16 +6,14 @@ prerequisites = {
'privileged_user': True, 'privileged_user': True,
} }
client = ApplicationGo()
class TestGoIsolationRootfs(TestApplicationGo):
def test_go_isolation_rootfs_chroot(self, temp_dir):
isolation = {'rootfs': temp_dir}
self.load('ns_inspect', isolation=isolation) def test_go_isolation_rootfs_chroot(temp_dir):
client.load('ns_inspect', isolation={'rootfs': temp_dir})
obj = self.getjson(url='/?file=/go/app')['body'] obj = client.getjson(url='/?file=/go/app')['body']
assert obj['FileExists'], 'app relative to rootfs'
assert obj['FileExists'], 'app relative to rootfs' obj = client.getjson(url='/?file=/bin/sh')['body']
assert not obj['FileExists'], 'file should not exists'
obj = self.getjson(url='/?file=/bin/sh')['body']
assert not obj['FileExists'], 'file should not exists'

View File

@@ -1,468 +1,500 @@
import pytest import pytest
from unit.applications.lang.python import TestApplicationPython from unit.applications.lang.python import ApplicationPython
prerequisites = {'modules': {'python': 'any'}} prerequisites = {'modules': {'python': 'any'}}
client = ApplicationPython()
class TestHTTPHeader(TestApplicationPython):
def test_http_header_value_leading_sp(self):
self.load('custom_header')
resp = self.get( def test_http_header_value_leading_sp():
headers={ client.load('custom_header')
'Host': 'localhost',
'Custom-Header': ' ,',
'Connection': 'close',
}
)
assert resp['status'] == 200, 'value leading sp status' resp = client.get(
assert ( headers={
resp['headers']['Custom-Header'] == ',' 'Host': 'localhost',
), 'value leading sp custom header' 'Custom-Header': ' ,',
'Connection': 'close',
}
)
def test_http_header_value_leading_htab(self): assert resp['status'] == 200, 'value leading sp status'
self.load('custom_header') assert (
resp['headers']['Custom-Header'] == ','
), 'value leading sp custom header'
resp = self.get(
headers={
'Host': 'localhost',
'Custom-Header': '\t,',
'Connection': 'close',
}
)
assert resp['status'] == 200, 'value leading htab status' def test_http_header_value_leading_htab():
assert ( client.load('custom_header')
resp['headers']['Custom-Header'] == ','
), 'value leading htab custom header'
def test_http_header_value_trailing_sp(self): resp = client.get(
self.load('custom_header') headers={
'Host': 'localhost',
'Custom-Header': '\t,',
'Connection': 'close',
}
)
resp = self.get( assert resp['status'] == 200, 'value leading htab status'
headers={ assert (
'Host': 'localhost', resp['headers']['Custom-Header'] == ','
'Custom-Header': ', ', ), 'value leading htab custom header'
'Connection': 'close',
}
)
assert resp['status'] == 200, 'value trailing sp status'
assert (
resp['headers']['Custom-Header'] == ','
), 'value trailing sp custom header'
def test_http_header_value_trailing_htab(self): def test_http_header_value_trailing_sp():
self.load('custom_header') client.load('custom_header')
resp = self.get( resp = client.get(
headers={ headers={
'Host': 'localhost', 'Host': 'localhost',
'Custom-Header': ',\t', 'Custom-Header': ', ',
'Connection': 'close', 'Connection': 'close',
} }
) )
assert resp['status'] == 200, 'value trailing htab status' assert resp['status'] == 200, 'value trailing sp status'
assert ( assert (
resp['headers']['Custom-Header'] == ',' resp['headers']['Custom-Header'] == ','
), 'value trailing htab custom header' ), 'value trailing sp custom header'
def test_http_header_value_both_sp(self):
self.load('custom_header')
resp = self.get( def test_http_header_value_trailing_htab():
headers={ client.load('custom_header')
'Host': 'localhost',
'Custom-Header': ' , ',
'Connection': 'close',
}
)
assert resp['status'] == 200, 'value both sp status' resp = client.get(
assert ( headers={
resp['headers']['Custom-Header'] == ',' 'Host': 'localhost',
), 'value both sp custom header' 'Custom-Header': ',\t',
'Connection': 'close',
}
)
def test_http_header_value_both_htab(self): assert resp['status'] == 200, 'value trailing htab status'
self.load('custom_header') assert (
resp['headers']['Custom-Header'] == ','
), 'value trailing htab custom header'
resp = self.get(
headers={
'Host': 'localhost',
'Custom-Header': '\t,\t',
'Connection': 'close',
}
)
assert resp['status'] == 200, 'value both htab status' def test_http_header_value_both_sp():
assert ( client.load('custom_header')
resp['headers']['Custom-Header'] == ','
), 'value both htab custom header'
def test_http_header_value_chars(self): resp = client.get(
self.load('custom_header') headers={
'Host': 'localhost',
'Custom-Header': ' , ',
'Connection': 'close',
}
)
resp = self.get( assert resp['status'] == 200, 'value both sp status'
headers={ assert (
'Host': 'localhost', resp['headers']['Custom-Header'] == ','
'Custom-Header': r"(),/:;<=>?@[\]{}\t !#$%&'*+-.^_`|~", ), 'value both sp custom header'
'Connection': 'close',
}
)
assert resp['status'] == 200, 'value chars status'
assert (
resp['headers']['Custom-Header']
== r"(),/:;<=>?@[\]{}\t !#$%&'*+-.^_`|~"
), 'value chars custom header'
def test_http_header_value_chars_edge(self): def test_http_header_value_both_htab():
self.load('custom_header') client.load('custom_header')
resp = self.http( resp = client.get(
b"""GET / HTTP/1.1 headers={
'Host': 'localhost',
'Custom-Header': '\t,\t',
'Connection': 'close',
}
)
assert resp['status'] == 200, 'value both htab status'
assert (
resp['headers']['Custom-Header'] == ','
), 'value both htab custom header'
def test_http_header_value_chars():
client.load('custom_header')
resp = client.get(
headers={
'Host': 'localhost',
'Custom-Header': r"(),/:;<=>?@[\]{}\t !#$%&'*+-.^_`|~",
'Connection': 'close',
}
)
assert resp['status'] == 200, 'value chars status'
assert (
resp['headers']['Custom-Header']
== r"(),/:;<=>?@[\]{}\t !#$%&'*+-.^_`|~"
), 'value chars custom header'
def test_http_header_value_chars_edge():
client.load('custom_header')
resp = client.http(
b"""GET / HTTP/1.1
Host: localhost Host: localhost
Custom-Header: \x20\xFF Custom-Header: \x20\xFF
Connection: close Connection: close
""", """,
raw=True, raw=True,
encoding='latin1', encoding='latin1',
) )
assert resp['status'] == 200, 'value chars edge status' assert resp['status'] == 200, 'value chars edge status'
assert resp['headers']['Custom-Header'] == '\xFF', 'value chars edge' assert resp['headers']['Custom-Header'] == '\xFF', 'value chars edge'
def test_http_header_value_chars_below(self):
self.load('custom_header')
resp = self.http( def test_http_header_value_chars_below():
b"""GET / HTTP/1.1 client.load('custom_header')
resp = client.http(
b"""GET / HTTP/1.1
Host: localhost Host: localhost
Custom-Header: \x1F Custom-Header: \x1F
Connection: close Connection: close
""", """,
raw=True, raw=True,
)
assert resp['status'] == 400, 'value chars below'
def test_http_header_field_leading_sp():
client.load('empty')
assert (
client.get(
headers={
'Host': 'localhost',
' Custom-Header': 'blah',
'Connection': 'close',
}
)['status']
== 400
), 'field leading sp'
def test_http_header_field_leading_htab():
client.load('empty')
assert (
client.get(
headers={
'Host': 'localhost',
'\tCustom-Header': 'blah',
'Connection': 'close',
}
)['status']
== 400
), 'field leading htab'
def test_http_header_field_trailing_sp():
client.load('empty')
assert (
client.get(
headers={
'Host': 'localhost',
'Custom-Header ': 'blah',
'Connection': 'close',
}
)['status']
== 400
), 'field trailing sp'
def test_http_header_field_trailing_htab():
client.load('empty')
assert (
client.get(
headers={
'Host': 'localhost',
'Custom-Header\t': 'blah',
'Connection': 'close',
}
)['status']
== 400
), 'field trailing htab'
def test_http_header_content_length_big():
client.load('empty')
assert (
client.post(
headers={
'Host': 'localhost',
'Content-Length': str(2**64),
'Connection': 'close',
},
body='X' * 1000,
)['status']
== 400
), 'Content-Length big'
def test_http_header_content_length_negative():
client.load('empty')
assert (
client.post(
headers={
'Host': 'localhost',
'Content-Length': '-100',
'Connection': 'close',
},
body='X' * 1000,
)['status']
== 400
), 'Content-Length negative'
def test_http_header_content_length_text():
client.load('empty')
assert (
client.post(
headers={
'Host': 'localhost',
'Content-Length': 'blah',
'Connection': 'close',
},
body='X' * 1000,
)['status']
== 400
), 'Content-Length text'
def test_http_header_content_length_multiple_values():
client.load('empty')
assert (
client.post(
headers={
'Host': 'localhost',
'Content-Length': '41, 42',
'Connection': 'close',
},
body='X' * 1000,
)['status']
== 400
), 'Content-Length multiple value'
def test_http_header_content_length_multiple_fields():
client.load('empty')
assert (
client.post(
headers={
'Host': 'localhost',
'Content-Length': ['41', '42'],
'Connection': 'close',
},
body='X' * 1000,
)['status']
== 400
), 'Content-Length multiple fields'
@pytest.mark.skip('not yet')
def test_http_header_host_absent():
client.load('host')
resp = client.get(headers={'Connection': 'close'})
assert resp['status'] == 400, 'Host absent status'
def test_http_header_host_empty():
client.load('host')
resp = client.get(headers={'Host': '', 'Connection': 'close'})
assert resp['status'] == 200, 'Host empty status'
assert resp['headers']['X-Server-Name'] != '', 'Host empty SERVER_NAME'
def test_http_header_host_big():
client.load('empty')
assert (
client.get(headers={'Host': 'X' * 10000, 'Connection': 'close'})[
'status'
]
== 431
), 'Host big'
def test_http_header_host_port():
client.load('host')
resp = client.get(
headers={'Host': 'exmaple.com:7080', 'Connection': 'close'}
)
assert resp['status'] == 200, 'Host port status'
assert (
resp['headers']['X-Server-Name'] == 'exmaple.com'
), 'Host port SERVER_NAME'
assert (
resp['headers']['X-Http-Host'] == 'exmaple.com:7080'
), 'Host port HTTP_HOST'
def test_http_header_host_port_empty():
client.load('host')
resp = client.get(headers={'Host': 'exmaple.com:', 'Connection': 'close'})
assert resp['status'] == 200, 'Host port empty status'
assert (
resp['headers']['X-Server-Name'] == 'exmaple.com'
), 'Host port empty SERVER_NAME'
assert (
resp['headers']['X-Http-Host'] == 'exmaple.com:'
), 'Host port empty HTTP_HOST'
def test_http_header_host_literal():
client.load('host')
resp = client.get(headers={'Host': '127.0.0.1', 'Connection': 'close'})
assert resp['status'] == 200, 'Host literal status'
assert (
resp['headers']['X-Server-Name'] == '127.0.0.1'
), 'Host literal SERVER_NAME'
def test_http_header_host_literal_ipv6():
client.load('host')
resp = client.get(headers={'Host': '[::1]:7080', 'Connection': 'close'})
assert resp['status'] == 200, 'Host literal ipv6 status'
assert (
resp['headers']['X-Server-Name'] == '[::1]'
), 'Host literal ipv6 SERVER_NAME'
assert (
resp['headers']['X-Http-Host'] == '[::1]:7080'
), 'Host literal ipv6 HTTP_HOST'
def test_http_header_host_trailing_period():
client.load('host')
resp = client.get(headers={'Host': '127.0.0.1.', 'Connection': 'close'})
assert resp['status'] == 200, 'Host trailing period status'
assert (
resp['headers']['X-Server-Name'] == '127.0.0.1'
), 'Host trailing period SERVER_NAME'
assert (
resp['headers']['X-Http-Host'] == '127.0.0.1.'
), 'Host trailing period HTTP_HOST'
def test_http_header_host_trailing_period_2():
client.load('host')
resp = client.get(headers={'Host': 'EXAMPLE.COM.', 'Connection': 'close'})
assert resp['status'] == 200, 'Host trailing period 2 status'
assert (
resp['headers']['X-Server-Name'] == 'example.com'
), 'Host trailing period 2 SERVER_NAME'
assert (
resp['headers']['X-Http-Host'] == 'EXAMPLE.COM.'
), 'Host trailing period 2 HTTP_HOST'
def test_http_header_host_case_insensitive():
client.load('host')
resp = client.get(headers={'Host': 'EXAMPLE.COM', 'Connection': 'close'})
assert resp['status'] == 200, 'Host case insensitive'
assert (
resp['headers']['X-Server-Name'] == 'example.com'
), 'Host case insensitive SERVER_NAME'
def test_http_header_host_double_dot():
client.load('empty')
assert (
client.get(headers={'Host': '127.0.0..1', 'Connection': 'close'})[
'status'
]
== 400
), 'Host double dot'
def test_http_header_host_slash():
client.load('empty')
assert (
client.get(headers={'Host': '/localhost', 'Connection': 'close'})[
'status'
]
== 400
), 'Host slash'
def test_http_header_host_multiple_fields():
client.load('empty')
assert (
client.get(
headers={
'Host': ['localhost', 'example.com'],
'Connection': 'close',
}
)['status']
== 400
), 'Host multiple fields'
def test_http_discard_unsafe_fields():
client.load('header_fields')
def check_status(header):
resp = client.get(
headers={
'Host': 'localhost',
header: 'blah',
'Connection': 'close',
}
) )
assert resp['status'] == 400, 'value chars below' assert resp['status'] == 200
return resp
def test_http_header_field_leading_sp(self): resp = check_status("!Custom-Header")
self.load('empty') assert 'CUSTOM' not in resp['headers']['All-Headers']
assert ( resp = check_status("Custom_Header")
self.get( assert 'CUSTOM' not in resp['headers']['All-Headers']
headers={
'Host': 'localhost',
' Custom-Header': 'blah',
'Connection': 'close',
}
)['status']
== 400
), 'field leading sp'
def test_http_header_field_leading_htab(self): assert 'success' in client.conf(
self.load('empty') {'http': {'discard_unsafe_fields': False}},
'settings',
)
assert ( resp = check_status("!#$%&'*+.^`|~Custom_Header")
self.get( assert 'CUSTOM' in resp['headers']['All-Headers']
headers={
'Host': 'localhost',
'\tCustom-Header': 'blah',
'Connection': 'close',
}
)['status']
== 400
), 'field leading htab'
def test_http_header_field_trailing_sp(self): assert 'success' in client.conf(
self.load('empty') {'http': {'discard_unsafe_fields': True}},
'settings',
)
assert ( resp = check_status("!Custom-Header")
self.get( assert 'CUSTOM' not in resp['headers']['All-Headers']
headers={
'Host': 'localhost',
'Custom-Header ': 'blah',
'Connection': 'close',
}
)['status']
== 400
), 'field trailing sp'
def test_http_header_field_trailing_htab(self): resp = check_status("Custom_Header")
self.load('empty') assert 'CUSTOM' not in resp['headers']['All-Headers']
assert (
self.get(
headers={
'Host': 'localhost',
'Custom-Header\t': 'blah',
'Connection': 'close',
}
)['status']
== 400
), 'field trailing htab'
def test_http_header_content_length_big(self):
self.load('empty')
assert (
self.post(
headers={
'Host': 'localhost',
'Content-Length': str(2**64),
'Connection': 'close',
},
body='X' * 1000,
)['status']
== 400
), 'Content-Length big'
def test_http_header_content_length_negative(self):
self.load('empty')
assert (
self.post(
headers={
'Host': 'localhost',
'Content-Length': '-100',
'Connection': 'close',
},
body='X' * 1000,
)['status']
== 400
), 'Content-Length negative'
def test_http_header_content_length_text(self):
self.load('empty')
assert (
self.post(
headers={
'Host': 'localhost',
'Content-Length': 'blah',
'Connection': 'close',
},
body='X' * 1000,
)['status']
== 400
), 'Content-Length text'
def test_http_header_content_length_multiple_values(self):
self.load('empty')
assert (
self.post(
headers={
'Host': 'localhost',
'Content-Length': '41, 42',
'Connection': 'close',
},
body='X' * 1000,
)['status']
== 400
), 'Content-Length multiple value'
def test_http_header_content_length_multiple_fields(self):
self.load('empty')
assert (
self.post(
headers={
'Host': 'localhost',
'Content-Length': ['41', '42'],
'Connection': 'close',
},
body='X' * 1000,
)['status']
== 400
), 'Content-Length multiple fields'
@pytest.mark.skip('not yet')
def test_http_header_host_absent(self):
self.load('host')
resp = self.get(headers={'Connection': 'close'})
assert resp['status'] == 400, 'Host absent status'
def test_http_header_host_empty(self):
self.load('host')
resp = self.get(headers={'Host': '', 'Connection': 'close'})
assert resp['status'] == 200, 'Host empty status'
assert resp['headers']['X-Server-Name'] != '', 'Host empty SERVER_NAME'
def test_http_header_host_big(self):
self.load('empty')
assert (
self.get(headers={'Host': 'X' * 10000, 'Connection': 'close'})[
'status'
]
== 431
), 'Host big'
def test_http_header_host_port(self):
self.load('host')
resp = self.get(
headers={'Host': 'exmaple.com:7080', 'Connection': 'close'}
)
assert resp['status'] == 200, 'Host port status'
assert (
resp['headers']['X-Server-Name'] == 'exmaple.com'
), 'Host port SERVER_NAME'
assert (
resp['headers']['X-Http-Host'] == 'exmaple.com:7080'
), 'Host port HTTP_HOST'
def test_http_header_host_port_empty(self):
self.load('host')
resp = self.get(headers={'Host': 'exmaple.com:', 'Connection': 'close'})
assert resp['status'] == 200, 'Host port empty status'
assert (
resp['headers']['X-Server-Name'] == 'exmaple.com'
), 'Host port empty SERVER_NAME'
assert (
resp['headers']['X-Http-Host'] == 'exmaple.com:'
), 'Host port empty HTTP_HOST'
def test_http_header_host_literal(self):
self.load('host')
resp = self.get(headers={'Host': '127.0.0.1', 'Connection': 'close'})
assert resp['status'] == 200, 'Host literal status'
assert (
resp['headers']['X-Server-Name'] == '127.0.0.1'
), 'Host literal SERVER_NAME'
def test_http_header_host_literal_ipv6(self):
self.load('host')
resp = self.get(headers={'Host': '[::1]:7080', 'Connection': 'close'})
assert resp['status'] == 200, 'Host literal ipv6 status'
assert (
resp['headers']['X-Server-Name'] == '[::1]'
), 'Host literal ipv6 SERVER_NAME'
assert (
resp['headers']['X-Http-Host'] == '[::1]:7080'
), 'Host literal ipv6 HTTP_HOST'
def test_http_header_host_trailing_period(self):
self.load('host')
resp = self.get(headers={'Host': '127.0.0.1.', 'Connection': 'close'})
assert resp['status'] == 200, 'Host trailing period status'
assert (
resp['headers']['X-Server-Name'] == '127.0.0.1'
), 'Host trailing period SERVER_NAME'
assert (
resp['headers']['X-Http-Host'] == '127.0.0.1.'
), 'Host trailing period HTTP_HOST'
def test_http_header_host_trailing_period_2(self):
self.load('host')
resp = self.get(headers={'Host': 'EXAMPLE.COM.', 'Connection': 'close'})
assert resp['status'] == 200, 'Host trailing period 2 status'
assert (
resp['headers']['X-Server-Name'] == 'example.com'
), 'Host trailing period 2 SERVER_NAME'
assert (
resp['headers']['X-Http-Host'] == 'EXAMPLE.COM.'
), 'Host trailing period 2 HTTP_HOST'
def test_http_header_host_case_insensitive(self):
self.load('host')
resp = self.get(headers={'Host': 'EXAMPLE.COM', 'Connection': 'close'})
assert resp['status'] == 200, 'Host case insensitive'
assert (
resp['headers']['X-Server-Name'] == 'example.com'
), 'Host case insensitive SERVER_NAME'
def test_http_header_host_double_dot(self):
self.load('empty')
assert (
self.get(headers={'Host': '127.0.0..1', 'Connection': 'close'})[
'status'
]
== 400
), 'Host double dot'
def test_http_header_host_slash(self):
self.load('empty')
assert (
self.get(headers={'Host': '/localhost', 'Connection': 'close'})[
'status'
]
== 400
), 'Host slash'
def test_http_header_host_multiple_fields(self):
self.load('empty')
assert (
self.get(
headers={
'Host': ['localhost', 'example.com'],
'Connection': 'close',
}
)['status']
== 400
), 'Host multiple fields'
def test_http_discard_unsafe_fields(self):
self.load('header_fields')
def check_status(header):
resp = self.get(
headers={
'Host': 'localhost',
header: 'blah',
'Connection': 'close',
}
)
assert resp['status'] == 200
return resp
resp = check_status("!Custom-Header")
assert 'CUSTOM' not in resp['headers']['All-Headers']
resp = check_status("Custom_Header")
assert 'CUSTOM' not in resp['headers']['All-Headers']
assert 'success' in self.conf(
{'http': {'discard_unsafe_fields': False}},
'settings',
)
resp = check_status("!#$%&'*+.^`|~Custom_Header")
assert 'CUSTOM' in resp['headers']['All-Headers']
assert 'success' in self.conf(
{'http': {'discard_unsafe_fields': True}},
'settings',
)
resp = check_status("!Custom-Header")
assert 'CUSTOM' not in resp['headers']['All-Headers']
resp = check_status("Custom_Header")
assert 'CUSTOM' not in resp['headers']['All-Headers']

File diff suppressed because it is too large Load Diff

View File

@@ -2,64 +2,65 @@ import os
import subprocess import subprocess
import pytest import pytest
from unit.applications.lang.java import TestApplicationJava from unit.applications.lang.java import ApplicationJava
from unit.option import option from unit.option import option
prerequisites = {'modules': {'java': 'all'}, 'privileged_user': True} prerequisites = {'modules': {'java': 'all'}, 'privileged_user': True}
client = ApplicationJava()
class TestJavaIsolationRootfs(TestApplicationJava):
@pytest.fixture(autouse=True)
def setup_method_fixture(self, temp_dir):
os.makedirs(f'{temp_dir}/jars')
os.makedirs(f'{temp_dir}/tmp')
os.chmod(f'{temp_dir}/tmp', 0o777)
try: @pytest.fixture(autouse=True)
subprocess.run( def setup_method_fixture(temp_dir):
[ os.makedirs(f'{temp_dir}/jars')
"mount", os.makedirs(f'{temp_dir}/tmp')
"--bind", os.chmod(f'{temp_dir}/tmp', 0o777)
f'{option.current_dir}/build',
f'{temp_dir}/jars',
],
stderr=subprocess.STDOUT,
)
except KeyboardInterrupt: try:
raise subprocess.run(
[
except subprocess.CalledProcessError: "mount",
pytest.fail("Can't run mount process.") "--bind",
f'{option.current_dir}/build',
def teardown_method(self): f'{temp_dir}/jars',
try: ],
subprocess.run( stderr=subprocess.STDOUT,
["umount", "--lazy", f"{option.temp_dir}/jars"],
stderr=subprocess.STDOUT,
)
except KeyboardInterrupt:
raise
except subprocess.CalledProcessError:
pytest.fail("Can't run umount process.")
def test_java_isolation_rootfs_chroot_war(self, temp_dir):
isolation = {'rootfs': temp_dir}
self.load('empty_war', isolation=isolation)
assert 'success' in self.conf(
'"/"',
'/config/applications/empty_war/working_directory',
) )
assert 'success' in self.conf( except KeyboardInterrupt:
'"/jars"', 'applications/empty_war/unit_jars' raise
)
assert 'success' in self.conf( except subprocess.CalledProcessError:
'"/java/empty.war"', 'applications/empty_war/webapp' pytest.fail("Can't run mount process.")
yield
try:
subprocess.run(
["umount", "--lazy", f"{option.temp_dir}/jars"],
stderr=subprocess.STDOUT,
) )
assert self.get()['status'] == 200, 'war' except KeyboardInterrupt:
raise
except subprocess.CalledProcessError:
pytest.fail("Can't run umount process.")
def test_java_isolation_rootfs_chroot_war(temp_dir):
client.load('empty_war', isolation={'rootfs': temp_dir})
assert 'success' in client.conf(
'"/"',
'/config/applications/empty_war/working_directory',
)
assert 'success' in client.conf(
'"/jars"', 'applications/empty_war/unit_jars'
)
assert 'success' in client.conf(
'"/java/empty.war"', 'applications/empty_war/webapp'
)
assert client.get()['status'] == 200, 'war'

File diff suppressed because it is too large Load Diff

View File

@@ -1,92 +1,101 @@
import os import os
import pytest import pytest
from unit.applications.proto import TestApplicationProto from unit.applications.proto import ApplicationProto
from unit.option import option from unit.option import option
from unit.utils import waitforfiles from unit.utils import waitforfiles
prerequisites = {'modules': {'njs': 'any'}} prerequisites = {'modules': {'njs': 'any'}}
client = ApplicationProto()
class TestNJS(TestApplicationProto):
@pytest.fixture(autouse=True)
def setup_method_fixture(self, temp_dir):
assert 'success' in self.conf(
{
"listeners": {"*:7080": {"pass": "routes"}},
"routes": [{"action": {"share": f"{temp_dir}/assets$uri"}}],
}
)
def create_files(self, *files): @pytest.fixture(autouse=True)
assets_dir = f'{option.temp_dir}/assets/' def setup_method_fixture(temp_dir):
os.makedirs(assets_dir) assert 'success' in client.conf(
{
"listeners": {"*:7080": {"pass": "routes"}},
"routes": [{"action": {"share": f"{temp_dir}/assets$uri"}}],
}
)
[open(assets_dir + f, 'a') for f in files]
waitforfiles(*[assets_dir + f for f in files])
def set_share(self, share): def create_files(*files):
assert 'success' in self.conf(share, 'routes/0/action/share') assets_dir = f'{option.temp_dir}/assets/'
os.makedirs(assets_dir)
def check_expression(self, expression, url='/'): [open(assets_dir + f, 'a') for f in files]
self.set_share(f'"`{option.temp_dir}/assets{expression}`"') waitforfiles(*[assets_dir + f for f in files])
assert self.get(url=url)['status'] == 200
def test_njs_template_string(self, temp_dir):
self.create_files('str', '`string`', '`backtick', 'l1\nl2')
self.check_expression('/str') def set_share(share):
self.check_expression('/\\\\`backtick') assert 'success' in client.conf(share, 'routes/0/action/share')
self.check_expression('/l1\\nl2')
self.set_share(f'"{temp_dir}/assets/`string`"')
assert self.get()['status'] == 200
def test_njs_template_expression(self): def check_expression(expression, url='/'):
self.create_files('str', 'localhost') set_share(f'"`{option.temp_dir}/assets{expression}`"')
assert client.get(url=url)['status'] == 200
self.check_expression('${uri}', '/str')
self.check_expression('${uri}${host}')
self.check_expression('${uri + host}')
self.check_expression('${uri + `${host}`}')
def test_njs_iteration(self): def test_njs_template_string(temp_dir):
self.create_files('Connection,Host', 'close,localhost') create_files('str', '`string`', '`backtick', 'l1\nl2')
self.check_expression('/${Object.keys(headers).sort().join()}') check_expression('/str')
self.check_expression('/${Object.values(headers).sort().join()}') check_expression('/\\\\`backtick')
check_expression('/l1\\nl2')
def test_njs_variables(self, temp_dir): set_share(f'"{temp_dir}/assets/`string`"')
self.create_files('str', 'localhost', '127.0.0.1') assert client.get()['status'] == 200
self.check_expression('/${host}')
self.check_expression('/${remoteAddr}')
self.check_expression('/${headers.Host}')
self.set_share(f'"`{temp_dir}/assets/${{cookies.foo}}`"') def test_njs_template_expression():
assert ( create_files('str', 'localhost')
self.get(headers={'Cookie': 'foo=str', 'Connection': 'close'})[
'status'
]
== 200
), 'cookies'
self.set_share(f'"`{temp_dir}/assets/${{args.foo}}`"') check_expression('${uri}', '/str')
assert self.get(url='/?foo=str')['status'] == 200, 'args' check_expression('${uri}${host}')
check_expression('${uri + host}')
check_expression('${uri + `${host}`}')
def test_njs_invalid(self, skip_alert):
skip_alert(r'js exception:')
def check_invalid(template): def test_njs_iteration():
assert 'error' in self.conf(template, 'routes/0/action/share') create_files('Connection,Host', 'close,localhost')
check_invalid('"`a"') check_expression('/${Object.keys(headers).sort().join()}')
check_invalid('"`a``"') check_expression('/${Object.values(headers).sort().join()}')
check_invalid('"`a`/"')
def check_invalid_resolve(template):
assert 'success' in self.conf(template, 'routes/0/action/share')
assert self.get()['status'] == 500
check_invalid_resolve('"`${a}`"') def test_njs_variables(temp_dir):
check_invalid_resolve('"`${uri.a.a}`"') create_files('str', 'localhost', '127.0.0.1')
check_expression('/${host}')
check_expression('/${remoteAddr}')
check_expression('/${headers.Host}')
set_share(f'"`{temp_dir}/assets/${{cookies.foo}}`"')
assert (
client.get(headers={'Cookie': 'foo=str', 'Connection': 'close'})[
'status'
]
== 200
), 'cookies'
set_share(f'"`{temp_dir}/assets/${{args.foo}}`"')
assert client.get(url='/?foo=str')['status'] == 200, 'args'
def test_njs_invalid(skip_alert):
skip_alert(r'js exception:')
def check_invalid(template):
assert 'error' in client.conf(template, 'routes/0/action/share')
check_invalid('"`a"')
check_invalid('"`a``"')
check_invalid('"`a`/"')
def check_invalid_resolve(template):
assert 'success' in client.conf(template, 'routes/0/action/share')
assert client.get()['status'] == 500
check_invalid_resolve('"`${a}`"')
check_invalid_resolve('"`${uri.a.a}`"')

View File

@@ -1,99 +1,104 @@
from unit.applications.proto import TestApplicationProto from unit.applications.proto import ApplicationProto
from unit.option import option from unit.option import option
prerequisites = {'modules': {'njs': 'any'}} prerequisites = {'modules': {'njs': 'any'}}
client = ApplicationProto()
class TestNJSModules(TestApplicationProto):
def njs_script_load(self, module, name=None, expect='success'):
if name is None:
name = module
with open(f'{option.test_dir}/njs/{module}/script.js', 'rb') as s: def njs_script_load(module, name=None, expect='success'):
assert expect in self.conf(s.read(), f'/js_modules/{name}') if name is None:
name = module
def test_njs_modules(self): with open(f'{option.test_dir}/njs/{module}/script.js', 'rb') as script:
self.njs_script_load('next') assert expect in client.conf(script.read(), f'/js_modules/{name}')
assert 'export' in self.conf_get('/js_modules/next')
assert 'error' in self.conf_post('"blah"', '/js_modules/next')
assert 'success' in self.conf( def test_njs_modules():
{ njs_script_load('next')
"settings": {"js_module": "next"},
"listeners": {"*:7080": {"pass": "routes/first"}},
"routes": {
"first": [{"action": {"pass": "`routes/${next.route()}`"}}],
"next": [{"action": {"return": 200}}],
},
}
)
assert self.get()['status'] == 200, 'string'
assert 'success' in self.conf({"js_module": ["next"]}, 'settings') assert 'export' in client.conf_get('/js_modules/next')
assert self.get()['status'] == 200, 'array' assert 'error' in client.conf_post('"blah"', '/js_modules/next')
# add one more value to array assert 'success' in client.conf(
{
"settings": {"js_module": "next"},
"listeners": {"*:7080": {"pass": "routes/first"}},
"routes": {
"first": [{"action": {"pass": "`routes/${next.route()}`"}}],
"next": [{"action": {"return": 200}}],
},
}
)
assert client.get()['status'] == 200, 'string'
assert len(self.conf_get('/js_modules').keys()) == 1 assert 'success' in client.conf({"js_module": ["next"]}, 'settings')
assert client.get()['status'] == 200, 'array'
self.njs_script_load('next', 'next_2') # add one more value to array
assert len(self.conf_get('/js_modules').keys()) == 2 assert len(client.conf_get('/js_modules').keys()) == 1
assert 'success' in self.conf_post('"next_2"', 'settings/js_module') njs_script_load('next', 'next_2')
assert self.get()['status'] == 200, 'array len 2'
assert 'success' in self.conf( assert len(client.conf_get('/js_modules').keys()) == 2
'"`routes/${next_2.route()}`"', 'routes/first/0/action/pass'
)
assert self.get()['status'] == 200, 'array new'
# can't update exsisting script assert 'success' in client.conf_post('"next_2"', 'settings/js_module')
assert client.get()['status'] == 200, 'array len 2'
self.njs_script_load('global_this', 'next', expect='error') assert 'success' in client.conf(
'"`routes/${next_2.route()}`"', 'routes/first/0/action/pass'
)
assert client.get()['status'] == 200, 'array new'
# delete modules # can't update exsisting script
assert 'error' in self.conf_delete('/js_modules/next_2') njs_script_load('global_this', 'next', expect='error')
assert 'success' in self.conf_delete('settings/js_module')
assert 'success' in self.conf_delete('/js_modules/next_2')
def test_njs_modules_import(self): # delete modules
self.njs_script_load('import_from')
assert 'success' in self.conf( assert 'error' in client.conf_delete('/js_modules/next_2')
{ assert 'success' in client.conf_delete('settings/js_module')
"settings": {"js_module": "import_from"}, assert 'success' in client.conf_delete('/js_modules/next_2')
"listeners": {"*:7080": {"pass": "routes/first"}},
"routes": {
"first": [
{"action": {"pass": "`routes/${import_from.num()}`"}}
],
"number": [{"action": {"return": 200}}],
},
}
)
assert self.get()['status'] == 200
def test_njs_modules_this(self):
self.njs_script_load('global_this')
assert 'success' in self.conf( def test_njs_modules_import():
{ njs_script_load('import_from')
"settings": {"js_module": "global_this"},
"listeners": {"*:7080": {"pass": "routes/first"}},
"routes": {
"first": [
{"action": {"pass": "`routes/${global_this.str()}`"}}
],
"string": [{"action": {"return": 200}}],
},
}
)
assert self.get()['status'] == 200
def test_njs_modules_invalid(self, skip_alert): assert 'success' in client.conf(
skip_alert(r'.*JS compile module.*failed.*') {
"settings": {"js_module": "import_from"},
"listeners": {"*:7080": {"pass": "routes/first"}},
"routes": {
"first": [
{"action": {"pass": "`routes/${import_from.num()}`"}}
],
"number": [{"action": {"return": 200}}],
},
}
)
assert client.get()['status'] == 200
self.njs_script_load('invalid', expect='error')
def test_njs_modules_this():
njs_script_load('global_this')
assert 'success' in client.conf(
{
"settings": {"js_module": "global_this"},
"listeners": {"*:7080": {"pass": "routes/first"}},
"routes": {
"first": [
{"action": {"pass": "`routes/${global_this.str()}`"}}
],
"string": [{"action": {"return": 200}}],
},
}
)
assert client.get()['status'] == 200
def test_njs_modules_invalid(skip_alert):
skip_alert(r'.*JS compile module.*failed.*')
njs_script_load('invalid', expect='error')

View File

@@ -1,305 +1,332 @@
import re import re
import pytest import pytest
from unit.applications.lang.node import TestApplicationNode from unit.applications.lang.node import ApplicationNode
from unit.utils import waitforfiles from unit.utils import waitforfiles
prerequisites = {'modules': {'node': 'all'}} prerequisites = {'modules': {'node': 'all'}}
client = ApplicationNode()
class TestNodeApplication(TestApplicationNode):
def assert_basic_application(self):
resp = self.get()
assert resp['headers']['Content-Type'] == 'text/plain', 'basic header'
assert resp['body'] == 'Hello World\n', 'basic body'
def test_node_application_basic(self): def assert_basic_application():
self.load('basic') resp = client.get()
assert resp['headers']['Content-Type'] == 'text/plain', 'basic header'
assert resp['body'] == 'Hello World\n', 'basic body'
self.assert_basic_application()
def test_node_application_loader_unit_http(self): def test_node_application_basic():
self.load('loader/unit_http') client.load('basic')
self.assert_basic_application() assert_basic_application()
def test_node_application_loader_transitive_dependency(self):
self.load('loader/transitive_dependency')
self.assert_basic_application() def test_node_application_loader_unit_http():
client.load('loader/unit_http')
def test_node_application_seq(self): assert_basic_application()
self.load('basic')
assert self.get()['status'] == 200, 'seq'
assert self.get()['status'] == 200, 'seq 2'
def test_node_application_variables(self, date_to_sec_epoch, sec_epoch): def test_node_application_loader_transitive_dependency():
self.load('variables') client.load('loader/transitive_dependency')
body = 'Test body string.' assert_basic_application()
resp = self.post(
def test_node_application_seq():
client.load('basic')
assert client.get()['status'] == 200, 'seq'
assert client.get()['status'] == 200, 'seq 2'
def test_node_application_variables(date_to_sec_epoch, sec_epoch):
client.load('variables')
body = 'Test body string.'
resp = client.post(
headers={
'Host': 'localhost',
'Content-Type': 'text/html',
'Custom-Header': 'blah',
'Connection': 'close',
},
body=body,
)
assert resp['status'] == 200, 'status'
headers = resp['headers']
header_server = headers.pop('Server')
assert re.search(r'Unit/[\d\.]+', header_server), 'server header'
date = headers.pop('Date')
assert date[-4:] == ' GMT', 'date header timezone'
assert abs(date_to_sec_epoch(date) - sec_epoch) < 5, 'date header'
raw_headers = headers.pop('Request-Raw-Headers')
assert re.search(
r'^(?:Host|localhost|Content-Type|'
r'text\/html|Custom-Header|blah|Content-Length|17|Connection|'
r'close|,)+$',
raw_headers,
), 'raw headers'
assert headers == {
'Connection': 'close',
'Content-Length': str(len(body)),
'Content-Type': 'text/html',
'Request-Method': 'POST',
'Request-Uri': '/',
'Http-Host': 'localhost',
'Server-Protocol': 'HTTP/1.1',
'Custom-Header': 'blah',
}, 'headers'
assert resp['body'] == body, 'body'
def test_node_application_get_variables():
client.load('get_variables')
resp = client.get(url='/?var1=val1&var2=&var3')
assert resp['headers']['X-Var-1'] == 'val1', 'GET variables'
assert resp['headers']['X-Var-2'] == '', 'GET variables 2'
assert resp['headers']['X-Var-3'] == '', 'GET variables 3'
def test_node_application_post_variables():
client.load('post_variables')
resp = client.post(
headers={
'Content-Type': 'application/x-www-form-urlencoded',
'Host': 'localhost',
'Connection': 'close',
},
body='var1=val1&var2=&var3',
)
assert resp['headers']['X-Var-1'] == 'val1', 'POST variables'
assert resp['headers']['X-Var-2'] == '', 'POST variables 2'
assert resp['headers']['X-Var-3'] == '', 'POST variables 3'
def test_node_application_404():
client.load('404')
resp = client.get()
assert resp['status'] == 404, '404 status'
assert re.search(r'<title>404 Not Found</title>', resp['body']), '404 body'
def test_node_keepalive_body():
client.load('mirror')
assert client.get()['status'] == 200, 'init'
body = '0123456789' * 500
(resp, sock) = client.post(
headers={
'Host': 'localhost',
'Connection': 'keep-alive',
},
start=True,
body=body,
read_timeout=1,
)
assert resp['body'] == '0123456789' * 500, 'keep-alive 1'
body = '0123456789'
resp = client.post(sock=sock, body=body)
assert resp['body'] == body, 'keep-alive 2'
def test_node_application_write_buffer():
client.load('write_buffer')
assert client.get()['body'] == 'buffer', 'write buffer'
def test_node_application_write_callback(temp_dir):
client.load('write_callback')
assert client.get()['body'] == 'helloworld', 'write callback order'
assert waitforfiles(f'{temp_dir}/node/callback'), 'write callback'
def test_node_application_write_before_write_head():
client.load('write_before_write_head')
assert client.get()['status'] == 200, 'write before writeHead'
def test_node_application_double_end():
client.load('double_end')
assert client.get()['status'] == 200, 'double end'
assert client.get()['status'] == 200, 'double end 2'
def test_node_application_write_return():
client.load('write_return')
assert client.get()['body'] == 'bodytrue', 'write return'
def test_node_application_remove_header():
client.load('remove_header')
resp = client.get(
headers={
'Host': 'localhost',
'X-Remove': 'X-Header',
'Connection': 'close',
}
)
assert resp['headers']['Was-Header'] == 'true', 'was header'
assert resp['headers']['Has-Header'] == 'false', 'has header'
assert not ('X-Header' in resp['headers']), 'remove header'
def test_node_application_remove_header_nonexisting():
client.load('remove_header')
assert (
client.get(
headers={
'Host': 'localhost',
'X-Remove': 'blah',
'Connection': 'close',
}
)['headers']['Has-Header']
== 'true'
), 'remove header nonexisting'
def test_node_application_update_header():
client.load('update_header')
assert client.get()['headers']['X-Header'] == 'new', 'update header'
def test_node_application_set_header_array():
client.load('set_header_array')
assert client.get()['headers']['Set-Cookie'] == [
'tc=one,two,three',
'tc=four,five,six',
], 'set header array'
@pytest.mark.skip('not yet')
def test_node_application_status_message():
client.load('status_message')
assert re.search(r'200 blah', client.get(raw_resp=True)), 'status message'
def test_node_application_get_header_type():
client.load('get_header_type')
assert client.get()['headers']['X-Type'] == 'number', 'get header type'
def test_node_application_header_name_case():
client.load('header_name_case')
headers = client.get()['headers']
assert headers['X-HEADER'] == '3', 'header value'
assert 'X-Header' not in headers, 'insensitive'
assert 'X-header' not in headers, 'insensitive 2'
def test_node_application_promise_handler_write_after_end():
client.load('promise_handler')
assert (
client.post(
headers={ headers={
'Host': 'localhost', 'Host': 'localhost',
'Content-Type': 'text/html', 'Content-Type': 'text/html',
'Custom-Header': 'blah', 'X-Write-Call': '1',
'Connection': 'close', 'Connection': 'close',
}, },
body=body, body='callback',
) )['status']
== 200
), 'promise handler request write after end'
assert resp['status'] == 200, 'status'
headers = resp['headers']
header_server = headers.pop('Server')
assert re.search(r'Unit/[\d\.]+', header_server), 'server header'
date = headers.pop('Date') def test_node_application_promise_end(temp_dir):
assert date[-4:] == ' GMT', 'date header timezone' client.load('promise_end')
assert abs(date_to_sec_epoch(date) - sec_epoch) < 5, 'date header'
raw_headers = headers.pop('Request-Raw-Headers') assert (
assert re.search( client.post(
r'^(?:Host|localhost|Content-Type|'
r'text\/html|Custom-Header|blah|Content-Length|17|Connection|'
r'close|,)+$',
raw_headers,
), 'raw headers'
assert headers == {
'Connection': 'close',
'Content-Length': str(len(body)),
'Content-Type': 'text/html',
'Request-Method': 'POST',
'Request-Uri': '/',
'Http-Host': 'localhost',
'Server-Protocol': 'HTTP/1.1',
'Custom-Header': 'blah',
}, 'headers'
assert resp['body'] == body, 'body'
def test_node_application_get_variables(self):
self.load('get_variables')
resp = self.get(url='/?var1=val1&var2=&var3')
assert resp['headers']['X-Var-1'] == 'val1', 'GET variables'
assert resp['headers']['X-Var-2'] == '', 'GET variables 2'
assert resp['headers']['X-Var-3'] == '', 'GET variables 3'
def test_node_application_post_variables(self):
self.load('post_variables')
resp = self.post(
headers={ headers={
'Content-Type': 'application/x-www-form-urlencoded',
'Host': 'localhost', 'Host': 'localhost',
'Content-Type': 'text/html',
'Connection': 'close', 'Connection': 'close',
}, },
body='var1=val1&var2=&var3', body='end',
) )['status']
== 200
), 'promise end request'
assert waitforfiles(f'{temp_dir}/node/callback'), 'promise end'
assert resp['headers']['X-Var-1'] == 'val1', 'POST variables'
assert resp['headers']['X-Var-2'] == '', 'POST variables 2'
assert resp['headers']['X-Var-3'] == '', 'POST variables 3'
def test_node_application_404(self): @pytest.mark.skip('not yet')
self.load('404') def test_node_application_header_name_valid():
client.load('header_name_valid')
resp = self.get() assert 'status' not in client.get(), 'header name valid'
assert resp['status'] == 404, '404 status'
assert re.search(
r'<title>404 Not Found</title>', resp['body']
), '404 body'
def test_node_keepalive_body(self): def test_node_application_header_value_object():
self.load('mirror') client.load('header_value_object')
assert self.get()['status'] == 200, 'init' assert 'X-Header' in client.get()['headers'], 'header value object'
body = '0123456789' * 500
(resp, sock) = self.post( def test_node_application_get_header_names():
client.load('get_header_names')
assert client.get()['headers']['X-Names'] == [
'date',
'x-header',
], 'get header names'
def test_node_application_has_header():
client.load('has_header')
assert (
client.get(
headers={ headers={
'Host': 'localhost', 'Host': 'localhost',
'Connection': 'keep-alive', 'X-Header': 'length',
},
start=True,
body=body,
read_timeout=1,
)
assert resp['body'] == '0123456789' * 500, 'keep-alive 1'
body = '0123456789'
resp = self.post(sock=sock, body=body)
assert resp['body'] == body, 'keep-alive 2'
def test_node_application_write_buffer(self):
self.load('write_buffer')
assert self.get()['body'] == 'buffer', 'write buffer'
def test_node_application_write_callback(self, temp_dir):
self.load('write_callback')
assert self.get()['body'] == 'helloworld', 'write callback order'
assert waitforfiles(f'{temp_dir}/node/callback'), 'write callback'
def test_node_application_write_before_write_head(self):
self.load('write_before_write_head')
assert self.get()['status'] == 200, 'write before writeHead'
def test_node_application_double_end(self):
self.load('double_end')
assert self.get()['status'] == 200, 'double end'
assert self.get()['status'] == 200, 'double end 2'
def test_node_application_write_return(self):
self.load('write_return')
assert self.get()['body'] == 'bodytrue', 'write return'
def test_node_application_remove_header(self):
self.load('remove_header')
resp = self.get(
headers={
'Host': 'localhost',
'X-Remove': 'X-Header',
'Connection': 'close', 'Connection': 'close',
} }
) )['headers']['X-Has-Header']
assert resp['headers']['Was-Header'] == 'true', 'was header' == 'false'
assert resp['headers']['Has-Header'] == 'false', 'has header' ), 'has header length'
assert not ('X-Header' in resp['headers']), 'remove header'
def test_node_application_remove_header_nonexisting(self): assert (
self.load('remove_header') client.get(
headers={
'Host': 'localhost',
'X-Header': 'Date',
'Connection': 'close',
}
)['headers']['X-Has-Header']
== 'false'
), 'has header date'
assert (
self.get(
headers={
'Host': 'localhost',
'X-Remove': 'blah',
'Connection': 'close',
}
)['headers']['Has-Header']
== 'true'
), 'remove header nonexisting'
def test_node_application_update_header(self): def test_node_application_write_multiple():
self.load('update_header') client.load('write_multiple')
assert self.get()['headers']['X-Header'] == 'new', 'update header' assert client.get()['body'] == 'writewrite2end', 'write multiple'
def test_node_application_set_header_array(self):
self.load('set_header_array')
assert self.get()['headers']['Set-Cookie'] == [
'tc=one,two,three',
'tc=four,five,six',
], 'set header array'
@pytest.mark.skip('not yet')
def test_node_application_status_message(self):
self.load('status_message')
assert re.search(r'200 blah', self.get(raw_resp=True)), 'status message'
def test_node_application_get_header_type(self):
self.load('get_header_type')
assert self.get()['headers']['X-Type'] == 'number', 'get header type'
def test_node_application_header_name_case(self):
self.load('header_name_case')
headers = self.get()['headers']
assert headers['X-HEADER'] == '3', 'header value'
assert 'X-Header' not in headers, 'insensitive'
assert 'X-header' not in headers, 'insensitive 2'
def test_node_application_promise_handler_write_after_end(self):
self.load('promise_handler')
assert (
self.post(
headers={
'Host': 'localhost',
'Content-Type': 'text/html',
'X-Write-Call': '1',
'Connection': 'close',
},
body='callback',
)['status']
== 200
), 'promise handler request write after end'
def test_node_application_promise_end(self, temp_dir):
self.load('promise_end')
assert (
self.post(
headers={
'Host': 'localhost',
'Content-Type': 'text/html',
'Connection': 'close',
},
body='end',
)['status']
== 200
), 'promise end request'
assert waitforfiles(f'{temp_dir}/node/callback'), 'promise end'
@pytest.mark.skip('not yet')
def test_node_application_header_name_valid(self):
self.load('header_name_valid')
assert 'status' not in self.get(), 'header name valid'
def test_node_application_header_value_object(self):
self.load('header_value_object')
assert 'X-Header' in self.get()['headers'], 'header value object'
def test_node_application_get_header_names(self):
self.load('get_header_names')
assert self.get()['headers']['X-Names'] == [
'date',
'x-header',
], 'get header names'
def test_node_application_has_header(self):
self.load('has_header')
assert (
self.get(
headers={
'Host': 'localhost',
'X-Header': 'length',
'Connection': 'close',
}
)['headers']['X-Has-Header']
== 'false'
), 'has header length'
assert (
self.get(
headers={
'Host': 'localhost',
'X-Header': 'Date',
'Connection': 'close',
}
)['headers']['X-Has-Header']
== 'false'
), 'has header date'
def test_node_application_write_multiple(self):
self.load('write_multiple')
assert self.get()['body'] == 'writewrite2end', 'write multiple'

View File

@@ -1,46 +1,48 @@
from packaging import version from packaging import version
from unit.applications.lang.node import TestApplicationNode from unit.applications.lang.node import ApplicationNode
from unit.applications.websockets import TestApplicationWebsocket from unit.applications.websockets import ApplicationWebsocket
prerequisites = { prerequisites = {
'modules': {'node': lambda v: version.parse(v) >= version.parse('14.16.0')} 'modules': {'node': lambda v: version.parse(v) >= version.parse('14.16.0')}
} }
client = ApplicationNode(es_modules=True)
ws = ApplicationWebsocket()
class TestNodeESModules(TestApplicationNode):
es_modules = True
ws = TestApplicationWebsocket()
def assert_basic_application(self): def assert_basic_application():
resp = self.get() resp = client.get()
assert resp['headers']['Content-Type'] == 'text/plain', 'basic header' assert resp['headers']['Content-Type'] == 'text/plain', 'basic header'
assert resp['body'] == 'Hello World\n', 'basic body' assert resp['body'] == 'Hello World\n', 'basic body'
def test_node_es_modules_loader_http(self):
self.load('loader/es_modules_http', name="app.mjs")
self.assert_basic_application() def test_node_es_modules_loader_http():
client.load('loader/es_modules_http', name="app.mjs")
def test_node_es_modules_loader_http_indirect(self): assert_basic_application()
self.load('loader/es_modules_http_indirect', name="app.js")
self.assert_basic_application()
def test_node_es_modules_loader_websockets(self): def test_node_es_modules_loader_http_indirect():
self.load('loader/es_modules_websocket', name="app.mjs") client.load('loader/es_modules_http_indirect', name="app.js")
message = 'blah' assert_basic_application()
_, sock, _ = self.ws.upgrade()
self.ws.frame_write(sock, self.ws.OP_TEXT, message) def test_node_es_modules_loader_websockets():
frame = self.ws.frame_read(sock) client.load('loader/es_modules_websocket', name="app.mjs")
assert message == frame['data'].decode('utf-8'), 'mirror' message = 'blah'
self.ws.frame_write(sock, self.ws.OP_TEXT, message) _, sock, _ = ws.upgrade()
frame = self.ws.frame_read(sock)
assert message == frame['data'].decode('utf-8'), 'mirror 2' ws.frame_write(sock, ws.OP_TEXT, message)
frame = ws.frame_read(sock)
sock.close() assert message == frame['data'].decode('utf-8'), 'mirror'
ws.frame_write(sock, ws.OP_TEXT, message)
frame = ws.frame_read(sock)
assert message == frame['data'].decode('utf-8'), 'mirror 2'
sock.close()

File diff suppressed because it is too large Load Diff

View File

@@ -1,292 +1,318 @@
import re import re
import pytest import pytest
from unit.applications.lang.perl import TestApplicationPerl from unit.applications.lang.perl import ApplicationPerl
prerequisites = {'modules': {'perl': 'all'}} prerequisites = {'modules': {'perl': 'all'}}
client = ApplicationPerl()
class TestPerlApplication(TestApplicationPerl):
def test_perl_application(self, date_to_sec_epoch, sec_epoch):
self.load('variables')
body = 'Test body string.' def test_perl_application(date_to_sec_epoch, sec_epoch):
client.load('variables')
resp = self.post( body = 'Test body string.'
headers={
'Host': 'localhost',
'Content-Type': 'text/html',
'Custom-Header': 'blah',
'Connection': 'close',
},
body=body,
)
assert resp['status'] == 200, 'status' resp = client.post(
headers = resp['headers'] headers={
header_server = headers.pop('Server') 'Host': 'localhost',
assert re.search(r'Unit/[\d\.]+', header_server), 'server header'
assert (
headers.pop('Server-Software') == header_server
), 'server software header'
date = headers.pop('Date')
assert date[-4:] == ' GMT', 'date header timezone'
assert abs(date_to_sec_epoch(date) - sec_epoch) < 5, 'date header'
assert headers == {
'Connection': 'close',
'Content-Length': str(len(body)),
'Content-Type': 'text/html', 'Content-Type': 'text/html',
'Request-Method': 'POST',
'Request-Uri': '/',
'Http-Host': 'localhost',
'Server-Protocol': 'HTTP/1.1',
'Custom-Header': 'blah', 'Custom-Header': 'blah',
'Psgi-Version': '11', 'Connection': 'close',
'Psgi-Url-Scheme': 'http', },
'Psgi-Multithread': '', body=body,
'Psgi-Multiprocess': '1', )
'Psgi-Run-Once': '',
'Psgi-Nonblocking': '',
'Psgi-Streaming': '1',
}, 'headers'
assert resp['body'] == body, 'body'
def test_perl_application_query_string(self): assert resp['status'] == 200, 'status'
self.load('query_string') headers = resp['headers']
header_server = headers.pop('Server')
assert re.search(r'Unit/[\d\.]+', header_server), 'server header'
assert (
headers.pop('Server-Software') == header_server
), 'server software header'
resp = self.get(url='/?var1=val1&var2=val2') date = headers.pop('Date')
assert date[-4:] == ' GMT', 'date header timezone'
assert abs(date_to_sec_epoch(date) - sec_epoch) < 5, 'date header'
assert ( assert headers == {
resp['headers']['Query-String'] == 'var1=val1&var2=val2' 'Connection': 'close',
), 'Query-String header' 'Content-Length': str(len(body)),
'Content-Type': 'text/html',
'Request-Method': 'POST',
'Request-Uri': '/',
'Http-Host': 'localhost',
'Server-Protocol': 'HTTP/1.1',
'Custom-Header': 'blah',
'Psgi-Version': '11',
'Psgi-Url-Scheme': 'http',
'Psgi-Multithread': '',
'Psgi-Multiprocess': '1',
'Psgi-Run-Once': '',
'Psgi-Nonblocking': '',
'Psgi-Streaming': '1',
}, 'headers'
assert resp['body'] == body, 'body'
def test_perl_application_query_string_empty(self):
self.load('query_string')
resp = self.get(url='/?') def test_perl_application_query_string():
client.load('query_string')
assert resp['status'] == 200, 'query string empty status' resp = client.get(url='/?var1=val1&var2=val2')
assert resp['headers']['Query-String'] == '', 'query string empty'
def test_perl_application_query_string_absent(self): assert (
self.load('query_string') resp['headers']['Query-String'] == 'var1=val1&var2=val2'
), 'Query-String header'
resp = self.get()
assert resp['status'] == 200, 'query string absent status' def test_perl_application_query_string_empty():
assert resp['headers']['Query-String'] == '', 'query string absent' client.load('query_string')
@pytest.mark.skip('not yet') resp = client.get(url='/?')
def test_perl_application_server_port(self):
self.load('server_port')
assert ( assert resp['status'] == 200, 'query string empty status'
self.get()['headers']['Server-Port'] == '7080' assert resp['headers']['Query-String'] == '', 'query string empty'
), 'Server-Port header'
def test_perl_application_input_read_empty(self):
self.load('input_read_empty')
assert self.get()['body'] == '', 'read empty' def test_perl_application_query_string_absent():
client.load('query_string')
def test_perl_application_input_read_parts(self): resp = client.get()
self.load('input_read_parts')
assert ( assert resp['status'] == 200, 'query string absent status'
self.post(body='0123456789')['body'] == '0123456789' assert resp['headers']['Query-String'] == '', 'query string absent'
), 'input read parts'
def test_perl_application_input_buffered_read(self):
self.load('input_buffered_read')
assert self.post(body='012345')['body'] == '012345', 'buffered read #1' @pytest.mark.skip('not yet')
assert ( def test_perl_application_server_port():
self.post(body='9876543210')['body'] == '9876543210' client.load('server_port')
), 'buffered read #2'
def test_perl_application_input_close(self): assert (
self.load('input_close') client.get()['headers']['Server-Port'] == '7080'
), 'Server-Port header'
assert self.post(body='012345')['body'] == '012345', 'input close #1'
assert (
self.post(body='9876543210')['body'] == '9876543210'
), 'input close #2'
@pytest.mark.skip('not yet') def test_perl_application_input_read_empty():
def test_perl_application_input_read_offset(self): client.load('input_read_empty')
self.load('input_read_offset')
assert self.post(body='0123456789')['body'] == '4567', 'read offset' assert client.get()['body'] == '', 'read empty'
def test_perl_application_input_copy(self):
self.load('input_copy')
body = '0123456789' def test_perl_application_input_read_parts():
assert self.post(body=body)['body'] == body, 'input copy' client.load('input_read_parts')
def test_perl_application_errors_print(self, wait_for_record): assert (
self.load('errors_print') client.post(body='0123456789')['body'] == '0123456789'
), 'input read parts'
assert self.get()['body'] == '1', 'errors result'
assert ( def test_perl_application_input_buffered_read():
wait_for_record(r'\[error\].+Error in application') is not None client.load('input_buffered_read')
), 'errors print'
def test_perl_application_header_equal_names(self): assert client.post(body='012345')['body'] == '012345', 'buffered read #1'
self.load('header_equal_names') assert (
client.post(body='9876543210')['body'] == '9876543210'
), 'buffered read #2'
assert self.get()['headers']['Set-Cookie'] == [
'tc=one,two,three',
'tc=four,five,six',
], 'header equal names'
def test_perl_application_header_pairs(self): def test_perl_application_input_close():
self.load('header_pairs') client.load('input_close')
assert self.get()['headers']['blah'] == 'blah', 'header pairs' assert client.post(body='012345')['body'] == '012345', 'input close #1'
assert (
client.post(body='9876543210')['body'] == '9876543210'
), 'input close #2'
def test_perl_application_body_empty(self):
self.load('body_empty')
assert self.get()['body'] == '', 'body empty' @pytest.mark.skip('not yet')
def test_perl_application_input_read_offset():
client.load('input_read_offset')
def test_perl_application_body_array(self): assert client.post(body='0123456789')['body'] == '4567', 'read offset'
self.load('body_array')
assert self.get()['body'] == '0123456789', 'body array'
def test_perl_application_body_large(self): def test_perl_application_input_copy():
self.load('variables') client.load('input_copy')
body = '0123456789' * 1000 body = '0123456789'
assert client.post(body=body)['body'] == body, 'input copy'
resp = self.post(body=body)['body']
assert resp == body, 'body large' def test_perl_application_errors_print(wait_for_record):
client.load('errors_print')
def test_perl_application_body_io_empty(self): assert client.get()['body'] == '1', 'errors result'
self.load('body_io_empty')
assert self.get()['status'] == 200, 'body io empty' assert (
wait_for_record(r'\[error\].+Error in application') is not None
), 'errors print'
def test_perl_application_body_io_file(self):
self.load('body_io_file')
assert self.get()['body'] == 'body\n', 'body io file' def test_perl_application_header_equal_names():
client.load('header_equal_names')
def test_perl_streaming_body_multiple_responses(self): assert client.get()['headers']['Set-Cookie'] == [
self.load('streaming_body_multiple_responses') 'tc=one,two,three',
'tc=four,five,six',
], 'header equal names'
assert self.get()['status'] == 200
@pytest.mark.skip('not yet') def test_perl_application_header_pairs():
def test_perl_application_syntax_error(self, skip_alert): client.load('header_pairs')
skip_alert(r'PSGI: Failed to parse script')
self.load('syntax_error')
assert self.get()['status'] == 500, 'syntax error' assert client.get()['headers']['blah'] == 'blah', 'header pairs'
def test_perl_keepalive_body(self):
self.load('variables')
assert self.get()['status'] == 200, 'init' def test_perl_application_body_empty():
client.load('body_empty')
body = '0123456789' * 500 assert client.get()['body'] == '', 'body empty'
(resp, sock) = self.post(
headers={
'Host': 'localhost', def test_perl_application_body_array():
'Connection': 'keep-alive', client.load('body_array')
'Content-Type': 'text/html',
}, assert client.get()['body'] == '0123456789', 'body array'
start=True,
body=body,
read_timeout=1, def test_perl_application_body_large():
) client.load('variables')
assert resp['body'] == body, 'keep-alive 1' body = '0123456789' * 1000
body = '0123456789' resp = client.post(body=body)['body']
resp = self.post(
assert resp == body, 'body large'
def test_perl_application_body_io_empty():
client.load('body_io_empty')
assert client.get()['status'] == 200, 'body io empty'
def test_perl_application_body_io_file():
client.load('body_io_file')
assert client.get()['body'] == 'body\n', 'body io file'
def test_perl_streaming_body_multiple_responses():
client.load('streaming_body_multiple_responses')
assert client.get()['status'] == 200
@pytest.mark.skip('not yet')
def test_perl_application_syntax_error(skip_alert):
skip_alert(r'PSGI: Failed to parse script')
client.load('syntax_error')
assert client.get()['status'] == 500, 'syntax error'
def test_perl_keepalive_body():
client.load('variables')
assert client.get()['status'] == 200, 'init'
body = '0123456789' * 500
(resp, sock) = client.post(
headers={
'Host': 'localhost',
'Connection': 'keep-alive',
'Content-Type': 'text/html',
},
start=True,
body=body,
read_timeout=1,
)
assert resp['body'] == body, 'keep-alive 1'
body = '0123456789'
resp = client.post(
headers={
'Host': 'localhost',
'Connection': 'close',
'Content-Type': 'text/html',
},
sock=sock,
body=body,
)
assert resp['body'] == body, 'keep-alive 2'
def test_perl_body_io_fake(wait_for_record):
client.load('body_io_fake')
assert client.get()['body'] == '21', 'body io fake'
assert (
wait_for_record(r'\[error\].+IOFake getline\(\) \$\/ is \d+')
is not None
), 'body io fake $/ value'
assert (
wait_for_record(r'\[error\].+IOFake close\(\) called') is not None
), 'body io fake close'
def test_perl_delayed_response():
client.load('delayed_response')
resp = client.get()
assert resp['status'] == 200, 'status'
assert resp['body'] == 'Hello World!', 'body'
def test_perl_streaming_body():
client.load('streaming_body')
resp = client.get()
assert resp['status'] == 200, 'status'
assert resp['body'] == 'Hello World!', 'body'
def test_perl_application_threads():
client.load('threads')
assert 'success' in client.conf(
'4', 'applications/threads/threads'
), 'configure 4 threads'
socks = []
for _ in range(4):
sock = client.get(
headers={ headers={
'Host': 'localhost', 'Host': 'localhost',
'X-Delay': '2',
'Connection': 'close', 'Connection': 'close',
'Content-Type': 'text/html',
}, },
sock=sock, no_recv=True,
body=body,
) )
assert resp['body'] == body, 'keep-alive 2' socks.append(sock)
def test_perl_body_io_fake(self, wait_for_record): threads = set()
self.load('body_io_fake')
assert self.get()['body'] == '21', 'body io fake' for sock in socks:
resp = client.recvall(sock).decode('utf-8')
assert ( client.log_in(resp)
wait_for_record(r'\[error\].+IOFake getline\(\) \$\/ is \d+')
is not None
), 'body io fake $/ value'
assert ( resp = client._resp_to_dict(resp)
wait_for_record(r'\[error\].+IOFake close\(\) called') is not None
), 'body io fake close'
def test_perl_delayed_response(self):
self.load('delayed_response')
resp = self.get()
assert resp['status'] == 200, 'status' assert resp['status'] == 200, 'status'
assert resp['body'] == 'Hello World!', 'body'
def test_perl_streaming_body(self): threads.add(resp['headers']['X-Thread'])
self.load('streaming_body')
resp = self.get() assert resp['headers']['Psgi-Multithread'] == '1', 'multithread'
assert resp['status'] == 200, 'status' sock.close()
assert resp['body'] == 'Hello World!', 'body'
def test_perl_application_threads(self): assert len(socks) == len(threads), 'threads differs'
self.load('threads')
assert 'success' in self.conf(
'4', 'applications/threads/threads'
), 'configure 4 threads'
socks = []
for _ in range(4):
sock = self.get(
headers={
'Host': 'localhost',
'X-Delay': '2',
'Connection': 'close',
},
no_recv=True,
)
socks.append(sock)
threads = set()
for sock in socks:
resp = self.recvall(sock).decode('utf-8')
self.log_in(resp)
resp = self._resp_to_dict(resp)
assert resp['status'] == 200, 'status'
threads.add(resp['headers']['X-Thread'])
assert resp['headers']['Psgi-Multithread'] == '1', 'multithread'
sock.close()
assert len(socks) == len(threads), 'threads differs'

File diff suppressed because it is too large Load Diff

View File

@@ -1,123 +1,130 @@
from unit.control import TestControl from unit.control import Control
prerequisites = {'modules': {'php': 'any'}} prerequisites = {'modules': {'php': 'any'}}
client = Control()
class TestPHPBasic(TestControl): conf_app = {
conf_app = { "app": {
"type": "php",
"processes": {"spare": 0},
"root": "/app",
"index": "index.php",
}
}
conf_basic = {
"listeners": {"*:7080": {"pass": "applications/app"}},
"applications": conf_app,
}
def test_php_get_applications():
assert 'success' in client.conf(conf_app, 'applications')
conf = client.conf_get()
assert conf['listeners'] == {}, 'listeners'
assert conf['applications'] == {
"app": { "app": {
"type": "php", "type": "php",
"processes": {"spare": 0}, "processes": {"spare": 0},
"root": "/app", "root": "/app",
"index": "index.php", "index": "index.php",
} }
} }, 'applications'
conf_basic = { assert client.conf_get('applications') == {
"listeners": {"*:7080": {"pass": "applications/app"}}, "app": {
"applications": conf_app,
}
def test_php_get_applications(self):
assert 'success' in self.conf(self.conf_app, 'applications')
conf = self.conf_get()
assert conf['listeners'] == {}, 'listeners'
assert conf['applications'] == {
"app": {
"type": "php",
"processes": {"spare": 0},
"root": "/app",
"index": "index.php",
}
}, 'applications'
assert self.conf_get('applications') == {
"app": {
"type": "php",
"processes": {"spare": 0},
"root": "/app",
"index": "index.php",
}
}, 'applications prefix'
assert self.conf_get('applications/app') == {
"type": "php", "type": "php",
"processes": {"spare": 0}, "processes": {"spare": 0},
"root": "/app", "root": "/app",
"index": "index.php", "index": "index.php",
}, 'applications prefix 2' }
}, 'applications prefix'
assert self.conf_get('applications/app/type') == 'php', 'type' assert client.conf_get('applications/app') == {
assert ( "type": "php",
self.conf_get('applications/app/processes/spare') == 0 "processes": {"spare": 0},
), 'spare processes' "root": "/app",
"index": "index.php",
}, 'applications prefix 2'
def test_php_get_listeners(self): assert client.conf_get('applications/app/type') == 'php', 'type'
assert 'success' in self.conf(self.conf_basic) assert (
client.conf_get('applications/app/processes/spare') == 0
), 'spare processes'
assert self.conf_get()['listeners'] == {
"*:7080": {"pass": "applications/app"}
}, 'listeners'
assert self.conf_get('listeners') == { def test_php_get_listeners():
"*:7080": {"pass": "applications/app"} assert 'success' in client.conf(conf_basic)
}, 'listeners prefix'
assert self.conf_get('listeners/*:7080') == { assert client.conf_get()['listeners'] == {
"pass": "applications/app" "*:7080": {"pass": "applications/app"}
}, 'listeners prefix 2' }, 'listeners'
def test_php_change_listener(self): assert client.conf_get('listeners') == {
assert 'success' in self.conf(self.conf_basic) "*:7080": {"pass": "applications/app"}
assert 'success' in self.conf( }, 'listeners prefix'
{"*:7081": {"pass": "applications/app"}}, 'listeners'
)
assert self.conf_get('listeners') == { assert client.conf_get('listeners/*:7080') == {
"*:7081": {"pass": "applications/app"} "pass": "applications/app"
}, 'change listener' }, 'listeners prefix 2'
def test_php_add_listener(self):
assert 'success' in self.conf(self.conf_basic)
assert 'success' in self.conf(
{"pass": "applications/app"}, 'listeners/*:7082'
)
assert self.conf_get('listeners') == { def test_php_change_listener():
"*:7080": {"pass": "applications/app"}, assert 'success' in client.conf(conf_basic)
"*:7082": {"pass": "applications/app"}, assert 'success' in client.conf(
}, 'add listener' {"*:7081": {"pass": "applications/app"}}, 'listeners'
)
def test_php_change_application(self): assert client.conf_get('listeners') == {
assert 'success' in self.conf(self.conf_basic) "*:7081": {"pass": "applications/app"}
}, 'change listener'
assert 'success' in self.conf('30', 'applications/app/processes/max')
assert (
self.conf_get('applications/app/processes/max') == 30
), 'change application max'
assert 'success' in self.conf('"/www"', 'applications/app/root') def test_php_add_listener():
assert ( assert 'success' in client.conf(conf_basic)
self.conf_get('applications/app/root') == '/www' assert 'success' in client.conf(
), 'change application root' {"pass": "applications/app"}, 'listeners/*:7082'
)
def test_php_delete(self): assert client.conf_get('listeners') == {
assert 'success' in self.conf(self.conf_basic) "*:7080": {"pass": "applications/app"},
"*:7082": {"pass": "applications/app"},
}, 'add listener'
assert 'error' in self.conf_delete('applications/app')
assert 'success' in self.conf_delete('listeners/*:7080')
assert 'success' in self.conf_delete('applications/app')
assert 'error' in self.conf_delete('applications/app')
def test_php_delete_blocks(self): def test_php_change_application():
assert 'success' in self.conf(self.conf_basic) assert 'success' in client.conf(conf_basic)
assert 'success' in self.conf_delete('listeners') assert 'success' in client.conf('30', 'applications/app/processes/max')
assert 'success' in self.conf_delete('applications') assert (
client.conf_get('applications/app/processes/max') == 30
), 'change application max'
assert 'success' in self.conf(self.conf_app, 'applications') assert 'success' in client.conf('"/www"', 'applications/app/root')
assert 'success' in self.conf( assert (
{"*:7081": {"pass": "applications/app"}}, 'listeners' client.conf_get('applications/app/root') == '/www'
), 'applications restore' ), 'change application root'
def test_php_delete():
assert 'success' in client.conf(conf_basic)
assert 'error' in client.conf_delete('applications/app')
assert 'success' in client.conf_delete('listeners/*:7080')
assert 'success' in client.conf_delete('applications/app')
assert 'error' in client.conf_delete('applications/app')
def test_php_delete_blocks():
assert 'success' in client.conf(conf_basic)
assert 'success' in client.conf_delete('listeners')
assert 'success' in client.conf_delete('applications')
assert 'success' in client.conf(conf_app, 'applications')
assert 'success' in client.conf(
{"*:7081": {"pass": "applications/app"}}, 'listeners'
), 'applications restore'

View File

@@ -1,83 +1,85 @@
from unit.applications.lang.php import TestApplicationPHP from unit.applications.lang.php import ApplicationPHP
prerequisites = {'modules': {'php': 'any'}, 'features': {'isolation': True}} prerequisites = {'modules': {'php': 'any'}, 'features': {'isolation': True}}
client = ApplicationPHP()
class TestPHPIsolation(TestApplicationPHP):
def test_php_isolation_rootfs(self, is_su, require, temp_dir):
isolation = {'rootfs': temp_dir}
if not is_su: def test_php_isolation_rootfs(is_su, require, temp_dir):
require( isolation = {'rootfs': temp_dir}
{
'features': { if not is_su:
'isolation': [ require(
'unprivileged_userns_clone', {
'user', 'features': {
'mnt', 'isolation': [
'pid', 'unprivileged_userns_clone',
] 'user',
} 'mnt',
'pid',
]
} }
)
isolation['namespaces'] = {
'mount': True,
'credential': True,
'pid': True,
} }
self.load('phpinfo', isolation=isolation)
assert 'success' in self.conf(
'"/app/php/phpinfo"', 'applications/phpinfo/root'
)
assert 'success' in self.conf(
'"/app/php/phpinfo"', 'applications/phpinfo/working_directory'
) )
assert self.get()['status'] == 200, 'empty rootfs' isolation['namespaces'] = {
'mount': True,
'credential': True,
'pid': True,
}
def test_php_isolation_rootfs_extensions(self, is_su, require, temp_dir): client.load('phpinfo', isolation=isolation)
isolation = {'rootfs': temp_dir}
if not is_su: assert 'success' in client.conf(
require( '"/app/php/phpinfo"', 'applications/phpinfo/root'
{ )
'features': { assert 'success' in client.conf(
'isolation': [ '"/app/php/phpinfo"', 'applications/phpinfo/working_directory'
'unprivileged_userns_clone', )
'user',
'mnt', assert client.get()['status'] == 200, 'empty rootfs'
'pid',
]
} def test_php_isolation_rootfs_extensions(is_su, require, temp_dir):
isolation = {'rootfs': temp_dir}
if not is_su:
require(
{
'features': {
'isolation': [
'unprivileged_userns_clone',
'user',
'mnt',
'pid',
]
} }
)
isolation['namespaces'] = {
'mount': True,
'credential': True,
'pid': True,
} }
self.load('list-extensions', isolation=isolation)
assert 'success' in self.conf(
'"/app/php/list-extensions"', 'applications/list-extensions/root'
) )
assert 'success' in self.conf( isolation['namespaces'] = {
{'file': '/php/list-extensions/php.ini'}, 'mount': True,
'applications/list-extensions/options', 'credential': True,
) 'pid': True,
}
assert 'success' in self.conf( client.load('list-extensions', isolation=isolation)
'"/app/php/list-extensions"',
'applications/list-extensions/working_directory',
)
extensions = self.getjson()['body'] assert 'success' in client.conf(
'"/app/php/list-extensions"', 'applications/list-extensions/root'
)
assert 'json' in extensions, 'json in extensions list' assert 'success' in client.conf(
assert 'unit' in extensions, 'unit in extensions list' {'file': '/php/list-extensions/php.ini'},
'applications/list-extensions/options',
)
assert 'success' in client.conf(
'"/app/php/list-extensions"',
'applications/list-extensions/working_directory',
)
extensions = client.getjson()['body']
assert 'json' in extensions, 'json in extensions list'
assert 'unit' in extensions, 'unit in extensions list'

View File

@@ -1,100 +1,100 @@
from unit.applications.lang.php import TestApplicationPHP from unit.applications.lang.php import ApplicationPHP
from unit.option import option from unit.option import option
prerequisites = {'modules': {'php': 'any'}} prerequisites = {'modules': {'php': 'any'}}
client = ApplicationPHP()
class TestPHPTargets(TestApplicationPHP):
def test_php_application_targets(self): def test_php_application_targets():
targets_dir = f"{option.test_dir}/php/targets" targets_dir = f"{option.test_dir}/php/targets"
assert 'success' in self.conf( assert 'success' in client.conf(
{ {
"listeners": {"*:7080": {"pass": "routes"}}, "listeners": {"*:7080": {"pass": "routes"}},
"routes": [ "routes": [
{ {
"match": {"uri": "/1"}, "match": {"uri": "/1"},
"action": {"pass": "applications/targets/1"}, "action": {"pass": "applications/targets/1"},
}, },
{ {
"match": {"uri": "/2"}, "match": {"uri": "/2"},
"action": {"pass": "applications/targets/2"}, "action": {"pass": "applications/targets/2"},
}, },
{"action": {"pass": "applications/targets/default"}}, {"action": {"pass": "applications/targets/default"}},
], ],
"applications": { "applications": {
"targets": {
"type": client.get_application_type(),
"processes": {"spare": 0},
"targets": { "targets": {
"type": self.get_application_type(), "1": {
"processes": {"spare": 0}, "script": "1.php",
"targets": { "root": targets_dir,
"1": {
"script": "1.php",
"root": targets_dir,
},
"2": {
"script": "2.php",
"root": f'{targets_dir}/2',
},
"default": {
"index": "index.php",
"root": targets_dir,
},
}, },
} "2": {
}, "script": "2.php",
} "root": f'{targets_dir}/2',
) },
"default": {
"index": "index.php",
"root": targets_dir,
},
},
}
},
}
)
assert self.get(url='/1')['body'] == '1' assert client.get(url='/1')['body'] == '1'
assert self.get(url='/2')['body'] == '2' assert client.get(url='/2')['body'] == '2'
assert self.get(url='/blah')['status'] == 404 assert client.get(url='/blah')['status'] == 404
assert self.get(url='/')['body'] == 'index' assert client.get(url='/')['body'] == 'index'
assert self.get(url='/1.php?test=test.php/')['body'] == '1' assert client.get(url='/1.php?test=test.php/')['body'] == '1'
assert 'success' in self.conf( assert 'success' in client.conf(
"\"1.php\"", 'applications/targets/targets/default/index' "\"1.php\"", 'applications/targets/targets/default/index'
), 'change targets index' ), 'change targets index'
assert self.get(url='/')['body'] == '1' assert client.get(url='/')['body'] == '1'
assert 'success' in self.conf_delete( assert 'success' in client.conf_delete(
'applications/targets/targets/default/index' 'applications/targets/targets/default/index'
), 'remove targets index' ), 'remove targets index'
assert self.get(url='/')['body'] == 'index' assert client.get(url='/')['body'] == 'index'
def test_php_application_targets_error(self):
assert 'success' in self.conf( def test_php_application_targets_error():
{ assert 'success' in client.conf(
"listeners": { {
"*:7080": {"pass": "applications/targets/default"} "listeners": {"*:7080": {"pass": "applications/targets/default"}},
}, "applications": {
"applications": { "targets": {
"type": client.get_application_type(),
"processes": {"spare": 0},
"targets": { "targets": {
"type": self.get_application_type(), "default": {
"processes": {"spare": 0}, "index": "index.php",
"targets": { "root": f"{option.test_dir}/php/targets",
"default": {
"index": "index.php",
"root": f"{option.test_dir}/php/targets",
},
}, },
} },
}, }
} },
), 'initial configuration' }
assert self.get()['status'] == 200 ), 'initial configuration'
assert client.get()['status'] == 200
assert 'error' in self.conf( assert 'error' in client.conf(
{"pass": "applications/targets/blah"}, 'listeners/*:7080' {"pass": "applications/targets/blah"}, 'listeners/*:7080'
), 'invalid targets pass' ), 'invalid targets pass'
assert 'error' in self.conf( assert 'error' in client.conf(
f'"{option.test_dir}/php/targets"', f'"{option.test_dir}/php/targets"',
'applications/targets/root', 'applications/targets/root',
), 'invalid root' ), 'invalid root'
assert 'error' in self.conf( assert 'error' in client.conf(
'"index.php"', 'applications/targets/index' '"index.php"', 'applications/targets/index'
), 'invalid index' ), 'invalid index'
assert 'error' in self.conf( assert 'error' in client.conf(
'"index.php"', 'applications/targets/script' '"index.php"', 'applications/targets/script'
), 'invalid script' ), 'invalid script'
assert 'error' in self.conf_delete( assert 'error' in client.conf_delete(
'applications/targets/default/root' 'applications/targets/default/root'
), 'root remove' ), 'root remove'

View File

@@ -4,487 +4,504 @@ import time
import pytest import pytest
from conftest import run_process from conftest import run_process
from unit.applications.lang.python import TestApplicationPython from unit.applications.lang.python import ApplicationPython
from unit.option import option from unit.option import option
from unit.utils import waitforsocket from unit.utils import waitforsocket
prerequisites = {'modules': {'python': 'any'}} prerequisites = {'modules': {'python': 'any'}}
client = ApplicationPython()
SERVER_PORT = 7999
class TestProxy(TestApplicationPython):
SERVER_PORT = 7999
@pytest.fixture(autouse=True) @pytest.fixture(autouse=True)
def setup_method_fixture(self): def setup_method_fixture():
run_process(self.run_server, self.SERVER_PORT) run_process(run_server, SERVER_PORT)
waitforsocket(self.SERVER_PORT) waitforsocket(SERVER_PORT)
python_dir = f'{option.test_dir}/python' python_dir = f'{option.test_dir}/python'
assert 'success' in self.conf( assert 'success' in client.conf(
{ {
"listeners": { "listeners": {
"*:7080": {"pass": "routes"}, "*:7080": {"pass": "routes"},
"*:7081": {"pass": "applications/mirror"}, "*:7081": {"pass": "applications/mirror"},
},
"routes": [{"action": {"proxy": "http://127.0.0.1:7081"}}],
"applications": {
"mirror": {
"type": client.get_application_type(),
"processes": {"spare": 0},
"path": f'{python_dir}/mirror',
"working_directory": f'{python_dir}/mirror',
"module": "wsgi",
}, },
"routes": [{"action": {"proxy": "http://127.0.0.1:7081"}}], "custom_header": {
"applications": { "type": client.get_application_type(),
"mirror": { "processes": {"spare": 0},
"type": self.get_application_type(), "path": f'{python_dir}/custom_header',
"processes": {"spare": 0}, "working_directory": f'{python_dir}/custom_header',
"path": f'{python_dir}/mirror', "module": "wsgi",
"working_directory": f'{python_dir}/mirror',
"module": "wsgi",
},
"custom_header": {
"type": self.get_application_type(),
"processes": {"spare": 0},
"path": f'{python_dir}/custom_header',
"working_directory": f'{python_dir}/custom_header',
"module": "wsgi",
},
"delayed": {
"type": self.get_application_type(),
"processes": {"spare": 0},
"path": f'{python_dir}/delayed',
"working_directory": f'{python_dir}/delayed',
"module": "wsgi",
},
}, },
} "delayed": {
), 'proxy initial configuration' "type": client.get_application_type(),
"processes": {"spare": 0},
"path": f'{python_dir}/delayed',
"working_directory": f'{python_dir}/delayed',
"module": "wsgi",
},
},
}
), 'proxy initial configuration'
@staticmethod
def run_server(server_port):
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
server_address = ('', server_port) def run_server(server_port):
sock.bind(server_address) sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.listen(5) sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
def recvall(sock): server_address = ('', server_port)
buff_size = 4096 sock.bind(server_address)
data = b'' sock.listen(5)
while True:
part = sock.recv(buff_size)
data += part
if len(part) < buff_size:
break
return data
req = b"""HTTP/1.1 200 OK def recvall(sock):
buff_size = 4096
data = b''
while True:
part = sock.recv(buff_size)
data += part
if len(part) < buff_size:
break
return data
req = b"""HTTP/1.1 200 OK
Content-Length: 10 Content-Length: 10
""" """
while True: while True:
connection, _ = sock.accept() connection, _ = sock.accept()
data = recvall(connection).decode() data = recvall(connection).decode()
to_send = req to_send = req
m = re.search(r'X-Len: (\d+)', data) m = re.search(r'X-Len: (\d+)', data)
if m: if m:
to_send += b'X' * int(m.group(1)) to_send += b'X' * int(m.group(1))
connection.sendall(to_send) connection.sendall(to_send)
connection.close() connection.close()
def get_http10(self, *args, **kwargs):
return self.get(*args, http_10=True, **kwargs)
def post_http10(self, *args, **kwargs): def get_http10(*args, **kwargs):
return self.post(*args, http_10=True, **kwargs) return client.get(*args, http_10=True, **kwargs)
def test_proxy_http10(self):
for _ in range(10):
assert self.get_http10()['status'] == 200, 'status'
def test_proxy_chain(self): def post_http10(*args, **kwargs):
assert 'success' in self.conf( return client.post(*args, http_10=True, **kwargs)
{
"listeners": {
"*:7080": {"pass": "routes/first"},
"*:7081": {"pass": "routes/second"},
"*:7082": {"pass": "routes/third"},
"*:7083": {"pass": "routes/fourth"},
"*:7084": {"pass": "routes/fifth"},
"*:7085": {"pass": "applications/mirror"},
},
"routes": {
"first": [{"action": {"proxy": "http://127.0.0.1:7081"}}],
"second": [{"action": {"proxy": "http://127.0.0.1:7082"}}],
"third": [{"action": {"proxy": "http://127.0.0.1:7083"}}],
"fourth": [{"action": {"proxy": "http://127.0.0.1:7084"}}],
"fifth": [{"action": {"proxy": "http://127.0.0.1:7085"}}],
},
"applications": {
"mirror": {
"type": self.get_application_type(),
"processes": {"spare": 0},
"path": f'{option.test_dir}/python/mirror',
"working_directory": f'{option.test_dir}/python/mirror',
"module": "wsgi",
}
},
}
), 'proxy chain configuration'
assert self.get_http10()['status'] == 200, 'status'
def test_proxy_body(self): def test_proxy_http10():
payload = '0123456789' for _ in range(10):
for _ in range(10): assert get_http10()['status'] == 200, 'status'
resp = self.post_http10(body=payload)
assert resp['status'] == 200, 'status'
assert resp['body'] == payload, 'body'
payload = 'X' * 4096 def test_proxy_chain():
for _ in range(10): assert 'success' in client.conf(
resp = self.post_http10(body=payload) {
"listeners": {
"*:7080": {"pass": "routes/first"},
"*:7081": {"pass": "routes/second"},
"*:7082": {"pass": "routes/third"},
"*:7083": {"pass": "routes/fourth"},
"*:7084": {"pass": "routes/fifth"},
"*:7085": {"pass": "applications/mirror"},
},
"routes": {
"first": [{"action": {"proxy": "http://127.0.0.1:7081"}}],
"second": [{"action": {"proxy": "http://127.0.0.1:7082"}}],
"third": [{"action": {"proxy": "http://127.0.0.1:7083"}}],
"fourth": [{"action": {"proxy": "http://127.0.0.1:7084"}}],
"fifth": [{"action": {"proxy": "http://127.0.0.1:7085"}}],
},
"applications": {
"mirror": {
"type": client.get_application_type(),
"processes": {"spare": 0},
"path": f'{option.test_dir}/python/mirror',
"working_directory": f'{option.test_dir}/python/mirror',
"module": "wsgi",
}
},
}
), 'proxy chain configuration'
assert resp['status'] == 200, 'status' assert get_http10()['status'] == 200, 'status'
assert resp['body'] == payload, 'body'
payload = 'X' * 4097
for _ in range(10):
resp = self.post_http10(body=payload)
assert resp['status'] == 200, 'status' def test_proxy_body():
assert resp['body'] == payload, 'body' payload = '0123456789'
for _ in range(10):
resp = post_http10(body=payload)
payload = 'X' * 4096 * 256
for _ in range(10):
resp = self.post_http10(body=payload, read_buffer_size=4096 * 128)
assert resp['status'] == 200, 'status'
assert resp['body'] == payload, 'body'
payload = 'X' * 4096 * 257
for _ in range(10):
resp = self.post_http10(body=payload, read_buffer_size=4096 * 128)
assert resp['status'] == 200, 'status'
assert resp['body'] == payload, 'body'
assert 'success' in self.conf(
{'http': {'max_body_size': 32 * 1024 * 1024}}, 'settings'
)
payload = '0123456789abcdef' * 32 * 64 * 1024
resp = self.post_http10(body=payload, read_buffer_size=1024 * 1024)
assert resp['status'] == 200, 'status' assert resp['status'] == 200, 'status'
assert resp['body'] == payload, 'body' assert resp['body'] == payload, 'body'
def test_proxy_parallel(self): payload = 'X' * 4096
payload = 'X' * 4096 * 257 for _ in range(10):
buff_size = 4096 * 258 resp = post_http10(body=payload)
socks = []
for i in range(10):
sock = self.post_http10(
body=f'{payload}{i}',
no_recv=True,
read_buffer_size=buff_size,
)
socks.append(sock)
for i in range(10):
resp = self.recvall(socks[i], buff_size=buff_size).decode()
socks[i].close()
resp = self._resp_to_dict(resp)
assert resp['status'] == 200, 'status'
assert resp['body'] == f'{payload}{i}', 'body'
def test_proxy_header(self):
assert 'success' in self.conf(
{"pass": "applications/custom_header"}, 'listeners/*:7081'
), 'custom_header configure'
header_value = 'blah'
assert (
self.get_http10(
headers={'Host': 'localhost', 'Custom-Header': header_value}
)['headers']['Custom-Header']
== header_value
), 'custom header'
header_value = r"(),/:;<=>?@[\]{}\t !#$%&'*+-.^_`|~"
assert (
self.get_http10(
headers={'Host': 'localhost', 'Custom-Header': header_value}
)['headers']['Custom-Header']
== header_value
), 'custom header 2'
header_value = 'X' * 4096
assert (
self.get_http10(
headers={'Host': 'localhost', 'Custom-Header': header_value}
)['headers']['Custom-Header']
== header_value
), 'custom header 3'
header_value = 'X' * 8191
assert (
self.get_http10(
headers={'Host': 'localhost', 'Custom-Header': header_value}
)['headers']['Custom-Header']
== header_value
), 'custom header 4'
header_value = 'X' * 8192
assert (
self.get_http10(
headers={'Host': 'localhost', 'Custom-Header': header_value}
)['status']
== 431
), 'custom header 5'
def test_proxy_fragmented(self):
sock = self.http(b"""GET / HTT""", raw=True, no_recv=True)
time.sleep(1)
sock.sendall("P/1.0\r\nHost: localhos".encode())
time.sleep(1)
sock.sendall("t\r\n\r\n".encode())
assert re.search(
'200 OK', self.recvall(sock).decode()
), 'fragmented send'
sock.close()
def test_proxy_fragmented_close(self):
sock = self.http(b"""GET / HTT""", raw=True, no_recv=True)
time.sleep(1)
sock.sendall("P/1.0\r\nHo".encode())
sock.close()
def test_proxy_fragmented_body(self):
sock = self.http(b"""GET / HTT""", raw=True, no_recv=True)
time.sleep(1)
sock.sendall("P/1.0\r\nHost: localhost\r\n".encode())
sock.sendall("Content-Length: 30000\r\n".encode())
time.sleep(1)
sock.sendall("\r\n".encode())
sock.sendall(("X" * 10000).encode())
time.sleep(1)
sock.sendall(("X" * 10000).encode())
time.sleep(1)
sock.sendall(("X" * 10000).encode())
resp = self._resp_to_dict(self.recvall(sock).decode())
sock.close()
assert resp['status'] == 200, 'status' assert resp['status'] == 200, 'status'
assert resp['body'] == "X" * 30000, 'body' assert resp['body'] == payload, 'body'
def test_proxy_fragmented_body_close(self): payload = 'X' * 4097
sock = self.http(b"""GET / HTT""", raw=True, no_recv=True) for _ in range(10):
resp = post_http10(body=payload)
time.sleep(1)
sock.sendall("P/1.0\r\nHost: localhost\r\n".encode())
sock.sendall("Content-Length: 30000\r\n".encode())
time.sleep(1)
sock.sendall("\r\n".encode())
sock.sendall(("X" * 10000).encode())
sock.close()
def test_proxy_nowhere(self):
assert 'success' in self.conf(
[{"action": {"proxy": "http://127.0.0.1:7082"}}], 'routes'
), 'proxy path changed'
assert self.get_http10()['status'] == 502, 'status'
def test_proxy_ipv6(self):
assert 'success' in self.conf(
{
"*:7080": {"pass": "routes"},
"[::1]:7081": {'application': 'mirror'},
},
'listeners',
), 'add ipv6 listener configure'
assert 'success' in self.conf(
[{"action": {"proxy": "http://[::1]:7081"}}], 'routes'
), 'proxy ipv6 configure'
assert self.get_http10()['status'] == 200, 'status'
def test_proxy_unix(self, temp_dir):
addr = f'{temp_dir}/sock'
assert 'success' in self.conf(
{
"*:7080": {"pass": "routes"},
f'unix:{addr}': {'application': 'mirror'},
},
'listeners',
), 'add unix listener configure'
assert 'success' in self.conf(
[{"action": {"proxy": f'http://unix:{addr}'}}], 'routes'
), 'proxy unix configure'
assert self.get_http10()['status'] == 200, 'status'
def test_proxy_delayed(self):
assert 'success' in self.conf(
{"pass": "applications/delayed"}, 'listeners/*:7081'
), 'delayed configure'
body = '0123456789' * 1000
resp = self.post_http10(
headers={
'Host': 'localhost',
'Content-Length': str(len(body)),
'X-Parts': '2',
'X-Delay': '1',
},
body=body,
)
assert resp['status'] == 200, 'status' assert resp['status'] == 200, 'status'
assert resp['body'] == body, 'body' assert resp['body'] == payload, 'body'
resp = self.post_http10( payload = 'X' * 4096 * 256
headers={ for _ in range(10):
'Host': 'localhost', resp = post_http10(body=payload, read_buffer_size=4096 * 128)
'Content-Length': str(len(body)),
'X-Parts': '2',
'X-Delay': '1',
},
body=body,
)
assert resp['status'] == 200, 'status' assert resp['status'] == 200, 'status'
assert resp['body'] == body, 'body' assert resp['body'] == payload, 'body'
def test_proxy_delayed_close(self): payload = 'X' * 4096 * 257
assert 'success' in self.conf( for _ in range(10):
{"pass": "applications/delayed"}, 'listeners/*:7081' resp = post_http10(body=payload, read_buffer_size=4096 * 128)
), 'delayed configure'
sock = self.post_http10( assert resp['status'] == 200, 'status'
headers={ assert resp['body'] == payload, 'body'
'Host': 'localhost',
'Content-Length': '10000', assert 'success' in client.conf(
'X-Parts': '3', {'http': {'max_body_size': 32 * 1024 * 1024}}, 'settings'
'X-Delay': '1', )
},
body='0123456789' * 1000, payload = '0123456789abcdef' * 32 * 64 * 1024
resp = post_http10(body=payload, read_buffer_size=1024 * 1024)
assert resp['status'] == 200, 'status'
assert resp['body'] == payload, 'body'
def test_proxy_parallel():
payload = 'X' * 4096 * 257
buff_size = 4096 * 258
socks = []
for i in range(10):
sock = post_http10(
body=f'{payload}{i}',
no_recv=True, no_recv=True,
read_buffer_size=buff_size,
) )
socks.append(sock)
assert re.search('200 OK', sock.recv(100).decode()), 'first' for i in range(10):
sock.close() resp = client.recvall(socks[i], buff_size=buff_size).decode()
socks[i].close()
sock = self.post_http10( resp = client._resp_to_dict(resp)
headers={
'Host': 'localhost', assert resp['status'] == 200, 'status'
'Content-Length': '10000', assert resp['body'] == f'{payload}{i}', 'body'
'X-Parts': '3',
'X-Delay': '1',
def test_proxy_header():
assert 'success' in client.conf(
{"pass": "applications/custom_header"}, 'listeners/*:7081'
), 'custom_header configure'
header_value = 'blah'
assert (
get_http10(
headers={'Host': 'localhost', 'Custom-Header': header_value}
)['headers']['Custom-Header']
== header_value
), 'custom header'
header_value = r"(),/:;<=>?@[\]{}\t !#$%&'*+-.^_`|~"
assert (
get_http10(
headers={'Host': 'localhost', 'Custom-Header': header_value}
)['headers']['Custom-Header']
== header_value
), 'custom header 2'
header_value = 'X' * 4096
assert (
get_http10(
headers={'Host': 'localhost', 'Custom-Header': header_value}
)['headers']['Custom-Header']
== header_value
), 'custom header 3'
header_value = 'X' * 8191
assert (
get_http10(
headers={'Host': 'localhost', 'Custom-Header': header_value}
)['headers']['Custom-Header']
== header_value
), 'custom header 4'
header_value = 'X' * 8192
assert (
get_http10(
headers={'Host': 'localhost', 'Custom-Header': header_value}
)['status']
== 431
), 'custom header 5'
def test_proxy_fragmented():
sock = client.http(b"""GET / HTT""", raw=True, no_recv=True)
time.sleep(1)
sock.sendall("P/1.0\r\nHost: localhos".encode())
time.sleep(1)
sock.sendall("t\r\n\r\n".encode())
assert re.search('200 OK', client.recvall(sock).decode()), 'fragmented send'
sock.close()
def test_proxy_fragmented_close():
sock = client.http(b"""GET / HTT""", raw=True, no_recv=True)
time.sleep(1)
sock.sendall("P/1.0\r\nHo".encode())
sock.close()
def test_proxy_fragmented_body():
sock = client.http(b"""GET / HTT""", raw=True, no_recv=True)
time.sleep(1)
sock.sendall("P/1.0\r\nHost: localhost\r\n".encode())
sock.sendall("Content-Length: 30000\r\n".encode())
time.sleep(1)
sock.sendall("\r\n".encode())
sock.sendall(("X" * 10000).encode())
time.sleep(1)
sock.sendall(("X" * 10000).encode())
time.sleep(1)
sock.sendall(("X" * 10000).encode())
resp = client._resp_to_dict(client.recvall(sock).decode())
sock.close()
assert resp['status'] == 200, 'status'
assert resp['body'] == "X" * 30000, 'body'
def test_proxy_fragmented_body_close():
sock = client.http(b"""GET / HTT""", raw=True, no_recv=True)
time.sleep(1)
sock.sendall("P/1.0\r\nHost: localhost\r\n".encode())
sock.sendall("Content-Length: 30000\r\n".encode())
time.sleep(1)
sock.sendall("\r\n".encode())
sock.sendall(("X" * 10000).encode())
sock.close()
def test_proxy_nowhere():
assert 'success' in client.conf(
[{"action": {"proxy": "http://127.0.0.1:7082"}}], 'routes'
), 'proxy path changed'
assert get_http10()['status'] == 502, 'status'
def test_proxy_ipv6():
assert 'success' in client.conf(
{
"*:7080": {"pass": "routes"},
"[::1]:7081": {'application': 'mirror'},
},
'listeners',
), 'add ipv6 listener configure'
assert 'success' in client.conf(
[{"action": {"proxy": "http://[::1]:7081"}}], 'routes'
), 'proxy ipv6 configure'
assert get_http10()['status'] == 200, 'status'
def test_proxy_unix(temp_dir):
addr = f'{temp_dir}/sock'
assert 'success' in client.conf(
{
"*:7080": {"pass": "routes"},
f'unix:{addr}': {'application': 'mirror'},
},
'listeners',
), 'add unix listener configure'
assert 'success' in client.conf(
[{"action": {"proxy": f'http://unix:{addr}'}}], 'routes'
), 'proxy unix configure'
assert get_http10()['status'] == 200, 'status'
def test_proxy_delayed():
assert 'success' in client.conf(
{"pass": "applications/delayed"}, 'listeners/*:7081'
), 'delayed configure'
body = '0123456789' * 1000
resp = post_http10(
headers={
'Host': 'localhost',
'Content-Length': str(len(body)),
'X-Parts': '2',
'X-Delay': '1',
},
body=body,
)
assert resp['status'] == 200, 'status'
assert resp['body'] == body, 'body'
resp = post_http10(
headers={
'Host': 'localhost',
'Content-Length': str(len(body)),
'X-Parts': '2',
'X-Delay': '1',
},
body=body,
)
assert resp['status'] == 200, 'status'
assert resp['body'] == body, 'body'
def test_proxy_delayed_close():
assert 'success' in client.conf(
{"pass": "applications/delayed"}, 'listeners/*:7081'
), 'delayed configure'
sock = post_http10(
headers={
'Host': 'localhost',
'Content-Length': '10000',
'X-Parts': '3',
'X-Delay': '1',
},
body='0123456789' * 1000,
no_recv=True,
)
assert re.search('200 OK', sock.recv(100).decode()), 'first'
sock.close()
sock = post_http10(
headers={
'Host': 'localhost',
'Content-Length': '10000',
'X-Parts': '3',
'X-Delay': '1',
},
body='0123456789' * 1000,
no_recv=True,
)
assert re.search('200 OK', sock.recv(100).decode()), 'second'
sock.close()
@pytest.mark.skip('not yet')
def test_proxy_content_length():
assert 'success' in client.conf(
[{"action": {"proxy": f'http://127.0.0.1:{SERVER_PORT}'}}],
'routes',
), 'proxy backend configure'
resp = get_http10()
assert len(resp['body']) == 0, 'body lt Content-Length 0'
resp = get_http10(headers={'Host': 'localhost', 'X-Len': '5'})
assert len(resp['body']) == 5, 'body lt Content-Length 5'
resp = get_http10(headers={'Host': 'localhost', 'X-Len': '9'})
assert len(resp['body']) == 9, 'body lt Content-Length 9'
resp = get_http10(headers={'Host': 'localhost', 'X-Len': '11'})
assert len(resp['body']) == 10, 'body gt Content-Length 11'
resp = get_http10(headers={'Host': 'localhost', 'X-Len': '15'})
assert len(resp['body']) == 10, 'body gt Content-Length 15'
def test_proxy_invalid():
def check_proxy(proxy):
assert 'error' in client.conf(
[{"action": {"proxy": proxy}}], 'routes'
), 'proxy invalid'
check_proxy('blah')
check_proxy('/blah')
check_proxy('unix:/blah')
check_proxy('http://blah')
check_proxy('http://127.0.0.1')
check_proxy('http://127.0.0.1:')
check_proxy('http://127.0.0.1:blah')
check_proxy('http://127.0.0.1:-1')
check_proxy('http://127.0.0.1:7080b')
check_proxy('http://[]')
check_proxy('http://[]:7080')
check_proxy('http://[:]:7080')
check_proxy('http://[::7080')
@pytest.mark.skip('not yet')
def test_proxy_loop(skip_alert):
skip_alert(
r'socket.*failed',
r'accept.*failed',
r'new connections are not accepted',
)
assert 'success' in client.conf(
{
"listeners": {
"*:7080": {"pass": "routes"},
"*:7081": {"pass": "applications/mirror"},
"*:7082": {"pass": "routes"},
}, },
body='0123456789' * 1000, "routes": [{"action": {"proxy": "http://127.0.0.1:7082"}}],
no_recv=True, "applications": {
) "mirror": {
"type": client.get_application_type(),
assert re.search('200 OK', sock.recv(100).decode()), 'second' "processes": {"spare": 0},
sock.close() "path": f'{option.test_dir}/python/mirror',
"working_directory": f'{option.test_dir}/python/mirror',
@pytest.mark.skip('not yet') "module": "wsgi",
def test_proxy_content_length(self):
assert 'success' in self.conf(
[{"action": {"proxy": f'http://127.0.0.1:{self.SERVER_PORT}'}}],
'routes',
), 'proxy backend configure'
resp = self.get_http10()
assert len(resp['body']) == 0, 'body lt Content-Length 0'
resp = self.get_http10(headers={'Host': 'localhost', 'X-Len': '5'})
assert len(resp['body']) == 5, 'body lt Content-Length 5'
resp = self.get_http10(headers={'Host': 'localhost', 'X-Len': '9'})
assert len(resp['body']) == 9, 'body lt Content-Length 9'
resp = self.get_http10(headers={'Host': 'localhost', 'X-Len': '11'})
assert len(resp['body']) == 10, 'body gt Content-Length 11'
resp = self.get_http10(headers={'Host': 'localhost', 'X-Len': '15'})
assert len(resp['body']) == 10, 'body gt Content-Length 15'
def test_proxy_invalid(self):
def check_proxy(proxy):
assert 'error' in self.conf(
[{"action": {"proxy": proxy}}], 'routes'
), 'proxy invalid'
check_proxy('blah')
check_proxy('/blah')
check_proxy('unix:/blah')
check_proxy('http://blah')
check_proxy('http://127.0.0.1')
check_proxy('http://127.0.0.1:')
check_proxy('http://127.0.0.1:blah')
check_proxy('http://127.0.0.1:-1')
check_proxy('http://127.0.0.1:7080b')
check_proxy('http://[]')
check_proxy('http://[]:7080')
check_proxy('http://[:]:7080')
check_proxy('http://[::7080')
@pytest.mark.skip('not yet')
def test_proxy_loop(self, skip_alert):
skip_alert(
r'socket.*failed',
r'accept.*failed',
r'new connections are not accepted',
)
assert 'success' in self.conf(
{
"listeners": {
"*:7080": {"pass": "routes"},
"*:7081": {"pass": "applications/mirror"},
"*:7082": {"pass": "routes"},
}, },
"routes": [{"action": {"proxy": "http://127.0.0.1:7082"}}], },
"applications": { }
"mirror": { )
"type": self.get_application_type(),
"processes": {"spare": 0},
"path": f'{option.test_dir}/python/mirror',
"working_directory": f'{option.test_dir}/python/mirror',
"module": "wsgi",
},
},
}
)
self.get_http10(no_recv=True) get_http10(no_recv=True)
self.get_http10(read_timeout=1) get_http10(read_timeout=1)

View File

@@ -5,234 +5,224 @@ import time
import pytest import pytest
from conftest import run_process from conftest import run_process
from unit.applications.lang.python import TestApplicationPython from unit.applications.lang.python import ApplicationPython
from unit.utils import waitforsocket from unit.utils import waitforsocket
prerequisites = {'modules': {'python': 'any'}} prerequisites = {'modules': {'python': 'any'}}
client = ApplicationPython()
SERVER_PORT = 7999
class TestProxyChunked(TestApplicationPython):
SERVER_PORT = 7999
@pytest.fixture(autouse=True) @pytest.fixture(autouse=True)
def setup_method_fixture(self): def setup_method_fixture():
run_process(self.run_server, self.SERVER_PORT) run_process(run_server, SERVER_PORT)
waitforsocket(self.SERVER_PORT) waitforsocket(SERVER_PORT)
assert 'success' in self.conf( assert 'success' in client.conf(
{ {
"listeners": { "listeners": {
"*:7080": {"pass": "routes"}, "*:7080": {"pass": "routes"},
}, },
"routes": [ "routes": [
{ {"action": {"proxy": f'http://127.0.0.1:{SERVER_PORT}'}}
"action": { ],
"proxy": f'http://127.0.0.1:{self.SERVER_PORT}' }
} ), 'proxy initial configuration'
}
],
}
), 'proxy initial configuration'
@staticmethod
def run_server(server_port):
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
sock.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1)
server_address = ('127.0.0.1', server_port) def run_server(server_port):
sock.bind(server_address) sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.listen(10) sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
sock.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1)
def recvall(sock): server_address = ('127.0.0.1', server_port)
buff_size = 4096 * 4096 sock.bind(server_address)
data = b'' sock.listen(10)
while True:
rlist = select.select([sock], [], [], 0.1)
if not rlist[0]:
break
part = sock.recv(buff_size)
data += part
if not len(part):
break
return data
def recvall(sock):
buff_size = 4096 * 4096
data = b''
while True: while True:
connection, _ = sock.accept() rlist = select.select([sock], [], [], 0.1)
req = """HTTP/1.1 200 OK\r\nTransfer-Encoding: chunked""" if not rlist[0]:
break
data = recvall(connection).decode() part = sock.recv(buff_size)
data += part
m = re.search('\x0d\x0a\x0d\x0a(.*)', data, re.M | re.S) if not len(part):
if m is not None: break
body = m.group(1)
for line in re.split('\r\n', body): return data
add = ''
m1 = re.search(r'(.*)\sX\s(\d+)', line)
if m1 is not None: while True:
add = m1.group(1) * int(m1.group(2)) connection, _ = sock.accept()
else:
add = line
req = f'{req}{add}\r\n' req = """HTTP/1.1 200 OK\r\nTransfer-Encoding: chunked"""
for chunk in re.split(r'([@#])', req): data = recvall(connection).decode()
if chunk == '@' or chunk == '#':
if chunk == '#':
time.sleep(0.1)
continue
connection.sendall(chunk.encode()) m = re.search('\x0d\x0a\x0d\x0a(.*)', data, re.M | re.S)
if m is not None:
body = m.group(1)
connection.close() for line in re.split('\r\n', body):
add = ''
m1 = re.search(r'(.*)\sX\s(\d+)', line)
def chunks(self, chunks): if m1 is not None:
body = '\r\n\r\n' add = m1.group(1) * int(m1.group(2))
else:
add = line
for l, c in chunks: req = f'{req}{add}\r\n'
body = f'{body}{l}\r\n{c}\r\n'
return f'{body}0\r\n\r\n' for chunk in re.split(r'([@#])', req):
if chunk == '@' or chunk == '#':
if chunk == '#':
time.sleep(0.1)
continue
def get_http10(self, *args, **kwargs): connection.sendall(chunk.encode())
return self.get(*args, http_10=True, **kwargs)
def test_proxy_chunked(self): connection.close()
for _ in range(10):
assert self.get_http10(body='\r\n\r\n0\r\n\r\n')['status'] == 200
def test_proxy_chunked_body(self):
part = '0123456789abcdef'
assert ( def chunks(chunks):
self.get_http10(body=self.chunks([('1000', f'{part} X 256')]))[ body = '\r\n\r\n'
'body'
] for l, c in chunks:
== part * 256 body = f'{body}{l}\r\n{c}\r\n'
)
assert ( return f'{body}0\r\n\r\n'
self.get_http10(body=self.chunks([('100000', f'{part} X 65536')]))[
'body'
] def get_http10(*args, **kwargs):
== part * 65536 return client.get(*args, http_10=True, **kwargs)
)
assert (
self.get_http10( def test_proxy_chunked():
body=self.chunks([('1000000', f'{part} X 1048576')]), for _ in range(10):
assert get_http10(body='\r\n\r\n0\r\n\r\n')['status'] == 200
def test_proxy_chunked_body():
part = '0123456789abcdef'
assert (
get_http10(body=chunks([('1000', f'{part} X 256')]))['body']
== part * 256
)
assert (
get_http10(body=chunks([('100000', f'{part} X 65536')]))['body']
== part * 65536
)
assert (
get_http10(
body=chunks([('1000000', f'{part} X 1048576')]),
read_buffer_size=4096 * 4096,
)['body']
== part * 1048576
)
assert (
get_http10(
body=chunks([('1000', f'{part} X 256'), ('1000', f'{part} X 256')])
)['body']
== part * 256 * 2
)
assert (
get_http10(
body=chunks(
[
('100000', f'{part} X 65536'),
('100000', f'{part} X 65536'),
]
)
)['body']
== part * 65536 * 2
)
assert (
get_http10(
body=chunks(
[
('1000000', f'{part} X 1048576'),
('1000000', f'{part} X 1048576'),
]
),
read_buffer_size=4096 * 4096,
)['body']
== part * 1048576 * 2
)
def test_proxy_chunked_fragmented():
part = '0123456789abcdef'
assert (
get_http10(
body=chunks([('1', hex(i % 16)[2:]) for i in range(4096)]),
)['body']
== part * 256
)
def test_proxy_chunked_send():
assert get_http10(body='\r\n\r\n@0@\r\n\r\n')['status'] == 200
assert (
get_http10(body='\r@\n\r\n2\r@\na@b\r\n2\r\ncd@\r\n0\r@\n\r\n')['body']
== 'abcd'
)
assert (
get_http10(body='\r\n\r\n2\r#\na#b\r\n##2\r\n#cd\r\n0\r\n#\r#\n')[
'body'
]
== 'abcd'
)
def test_proxy_chunked_invalid():
def check_invalid(body):
assert get_http10(body=body)['status'] != 200
check_invalid('\r\n\r0')
check_invalid('\r\n\r\n\r0')
check_invalid('\r\n\r\n\r\n0')
check_invalid('\r\nContent-Length: 5\r\n\r\n0\r\n\r\n')
check_invalid('\r\n\r\n1\r\nXX\r\n0\r\n\r\n')
check_invalid('\r\n\r\n2\r\nX\r\n0\r\n\r\n')
check_invalid('\r\n\r\nH\r\nXX\r\n0\r\n\r\n')
check_invalid('\r\n\r\n0\r\nX')
resp = get_http10(body='\r\n\r\n65#\r\nA X 100')
assert resp['status'] == 200, 'incomplete chunk status'
assert resp['body'][-5:] != '0\r\n\r\n', 'incomplete chunk'
resp = get_http10(body='\r\n\r\n64#\r\nA X 100')
assert resp['status'] == 200, 'no zero chunk status'
assert resp['body'][-5:] != '0\r\n\r\n', 'no zero chunk'
assert get_http10(body='\r\n\r\n80000000\r\nA X 100')['status'] == 200
assert (
get_http10(body='\r\n\r\n10000000000000000\r\nA X 100')['status'] == 502
)
assert (
len(
get_http10(
body='\r\n\r\n1000000\r\nA X 1048576\r\n1000000\r\nA X 100',
read_buffer_size=4096 * 4096, read_buffer_size=4096 * 4096,
)['body'] )['body']
== part * 1048576
) )
>= 1048576
assert ( )
self.get_http10( assert (
body=self.chunks( len(
[('1000', f'{part} X 256'), ('1000', f'{part} X 256')] get_http10(
) body='\r\n\r\n1000000\r\nA X 1048576\r\nXXX\r\nA X 100',
)['body']
== part * 256 * 2
)
assert (
self.get_http10(
body=self.chunks(
[
('100000', f'{part} X 65536'),
('100000', f'{part} X 65536'),
]
)
)['body']
== part * 65536 * 2
)
assert (
self.get_http10(
body=self.chunks(
[
('1000000', f'{part} X 1048576'),
('1000000', f'{part} X 1048576'),
]
),
read_buffer_size=4096 * 4096, read_buffer_size=4096 * 4096,
)['body'] )['body']
== part * 1048576 * 2
)
def test_proxy_chunked_fragmented(self):
part = '0123456789abcdef'
assert (
self.get_http10(
body=self.chunks([('1', hex(i % 16)[2:]) for i in range(4096)]),
)['body']
== part * 256
)
def test_proxy_chunked_send(self):
assert self.get_http10(body='\r\n\r\n@0@\r\n\r\n')['status'] == 200
assert (
self.get_http10(
body='\r@\n\r\n2\r@\na@b\r\n2\r\ncd@\r\n0\r@\n\r\n'
)['body']
== 'abcd'
)
assert (
self.get_http10(
body='\r\n\r\n2\r#\na#b\r\n##2\r\n#cd\r\n0\r\n#\r#\n'
)['body']
== 'abcd'
)
def test_proxy_chunked_invalid(self):
def check_invalid(body):
assert self.get_http10(body=body)['status'] != 200
check_invalid('\r\n\r0')
check_invalid('\r\n\r\n\r0')
check_invalid('\r\n\r\n\r\n0')
check_invalid('\r\nContent-Length: 5\r\n\r\n0\r\n\r\n')
check_invalid('\r\n\r\n1\r\nXX\r\n0\r\n\r\n')
check_invalid('\r\n\r\n2\r\nX\r\n0\r\n\r\n')
check_invalid('\r\n\r\nH\r\nXX\r\n0\r\n\r\n')
check_invalid('\r\n\r\n0\r\nX')
resp = self.get_http10(body='\r\n\r\n65#\r\nA X 100')
assert resp['status'] == 200, 'incomplete chunk status'
assert resp['body'][-5:] != '0\r\n\r\n', 'incomplete chunk'
resp = self.get_http10(body='\r\n\r\n64#\r\nA X 100')
assert resp['status'] == 200, 'no zero chunk status'
assert resp['body'][-5:] != '0\r\n\r\n', 'no zero chunk'
assert (
self.get_http10(body='\r\n\r\n80000000\r\nA X 100')['status'] == 200
)
assert (
self.get_http10(body='\r\n\r\n10000000000000000\r\nA X 100')[
'status'
]
== 502
)
assert (
len(
self.get_http10(
body='\r\n\r\n1000000\r\nA X 1048576\r\n1000000\r\nA X 100',
read_buffer_size=4096 * 4096,
)['body']
)
>= 1048576
)
assert (
len(
self.get_http10(
body='\r\n\r\n1000000\r\nA X 1048576\r\nXXX\r\nA X 100',
read_buffer_size=4096 * 4096,
)['body']
)
>= 1048576
) )
>= 1048576
)

File diff suppressed because it is too large Load Diff

View File

@@ -1,127 +1,134 @@
from unit.control import TestControl from unit.control import Control
prerequisites = {'modules': {'python': 'any'}} prerequisites = {'modules': {'python': 'any'}}
client = Control()
class TestPythonBasic(TestControl): conf_app = {
"app": {
"type": "python",
"processes": {"spare": 0},
"path": "/app",
"module": "wsgi",
}
}
conf_app = { conf_basic = {
"listeners": {"*:7080": {"pass": "applications/app"}},
"applications": conf_app,
}
def test_python_get_empty():
assert client.conf_get() == {'listeners': {}, 'applications': {}}
assert client.conf_get('listeners') == {}
assert client.conf_get('applications') == {}
def test_python_get_applications():
client.conf(conf_app, 'applications')
conf = client.conf_get()
assert conf['listeners'] == {}, 'listeners'
assert conf['applications'] == {
"app": { "app": {
"type": "python", "type": "python",
"processes": {"spare": 0}, "processes": {"spare": 0},
"path": "/app", "path": "/app",
"module": "wsgi", "module": "wsgi",
} }
} }, 'applications'
conf_basic = { assert client.conf_get('applications') == {
"listeners": {"*:7080": {"pass": "applications/app"}}, "app": {
"applications": conf_app,
}
def test_python_get_empty(self):
assert self.conf_get() == {'listeners': {}, 'applications': {}}
assert self.conf_get('listeners') == {}
assert self.conf_get('applications') == {}
def test_python_get_applications(self):
self.conf(self.conf_app, 'applications')
conf = self.conf_get()
assert conf['listeners'] == {}, 'listeners'
assert conf['applications'] == {
"app": {
"type": "python",
"processes": {"spare": 0},
"path": "/app",
"module": "wsgi",
}
}, 'applications'
assert self.conf_get('applications') == {
"app": {
"type": "python",
"processes": {"spare": 0},
"path": "/app",
"module": "wsgi",
}
}, 'applications prefix'
assert self.conf_get('applications/app') == {
"type": "python", "type": "python",
"processes": {"spare": 0}, "processes": {"spare": 0},
"path": "/app", "path": "/app",
"module": "wsgi", "module": "wsgi",
}, 'applications prefix 2' }
}, 'applications prefix'
assert self.conf_get('applications/app/type') == 'python', 'type' assert client.conf_get('applications/app') == {
assert self.conf_get('applications/app/processes/spare') == 0, 'spare' "type": "python",
"processes": {"spare": 0},
"path": "/app",
"module": "wsgi",
}, 'applications prefix 2'
def test_python_get_listeners(self): assert client.conf_get('applications/app/type') == 'python', 'type'
assert 'success' in self.conf(self.conf_basic) assert client.conf_get('applications/app/processes/spare') == 0, 'spare'
assert self.conf_get()['listeners'] == {
"*:7080": {"pass": "applications/app"}
}, 'listeners'
assert self.conf_get('listeners') == { def test_python_get_listeners():
"*:7080": {"pass": "applications/app"} assert 'success' in client.conf(conf_basic)
}, 'listeners prefix'
assert self.conf_get('listeners/*:7080') == { assert client.conf_get()['listeners'] == {
"pass": "applications/app" "*:7080": {"pass": "applications/app"}
}, 'listeners prefix 2' }, 'listeners'
def test_python_change_listener(self): assert client.conf_get('listeners') == {
assert 'success' in self.conf(self.conf_basic) "*:7080": {"pass": "applications/app"}
assert 'success' in self.conf( }, 'listeners prefix'
{"*:7081": {"pass": "applications/app"}}, 'listeners'
)
assert self.conf_get('listeners') == { assert client.conf_get('listeners/*:7080') == {
"*:7081": {"pass": "applications/app"} "pass": "applications/app"
}, 'change listener' }, 'listeners prefix 2'
def test_python_add_listener(self):
assert 'success' in self.conf(self.conf_basic)
assert 'success' in self.conf(
{"pass": "applications/app"}, 'listeners/*:7082'
)
assert self.conf_get('listeners') == { def test_python_change_listener():
"*:7080": {"pass": "applications/app"}, assert 'success' in client.conf(conf_basic)
"*:7082": {"pass": "applications/app"}, assert 'success' in client.conf(
}, 'add listener' {"*:7081": {"pass": "applications/app"}}, 'listeners'
)
def test_python_change_application(self): assert client.conf_get('listeners') == {
assert 'success' in self.conf(self.conf_basic) "*:7081": {"pass": "applications/app"}
}, 'change listener'
assert 'success' in self.conf('30', 'applications/app/processes/max')
assert (
self.conf_get('applications/app/processes/max') == 30
), 'change application max'
assert 'success' in self.conf('"/www"', 'applications/app/path') def test_python_add_listener():
assert ( assert 'success' in client.conf(conf_basic)
self.conf_get('applications/app/path') == '/www' assert 'success' in client.conf(
), 'change application path' {"pass": "applications/app"}, 'listeners/*:7082'
)
def test_python_delete(self): assert client.conf_get('listeners') == {
assert 'success' in self.conf(self.conf_basic) "*:7080": {"pass": "applications/app"},
"*:7082": {"pass": "applications/app"},
}, 'add listener'
assert 'error' in self.conf_delete('applications/app')
assert 'success' in self.conf_delete('listeners/*:7080')
assert 'success' in self.conf_delete('applications/app')
assert 'error' in self.conf_delete('applications/app')
def test_python_delete_blocks(self): def test_python_change_application():
assert 'success' in self.conf(self.conf_basic) assert 'success' in client.conf(conf_basic)
assert 'success' in self.conf_delete('listeners') assert 'success' in client.conf('30', 'applications/app/processes/max')
assert 'success' in self.conf_delete('applications') assert (
client.conf_get('applications/app/processes/max') == 30
), 'change application max'
assert 'success' in self.conf(self.conf_app, 'applications') assert 'success' in client.conf('"/www"', 'applications/app/path')
assert 'success' in self.conf( assert (
{"*:7081": {"pass": "applications/app"}}, 'listeners' client.conf_get('applications/app/path') == '/www'
), 'applications restore' ), 'change application path'
def test_python_delete():
assert 'success' in client.conf(conf_basic)
assert 'error' in client.conf_delete('applications/app')
assert 'success' in client.conf_delete('listeners/*:7080')
assert 'success' in client.conf_delete('applications/app')
assert 'error' in client.conf_delete('applications/app')
def test_python_delete_blocks():
assert 'success' in client.conf(conf_basic)
assert 'success' in client.conf_delete('listeners')
assert 'success' in client.conf_delete('applications')
assert 'success' in client.conf(conf_app, 'applications')
assert 'success' in client.conf(
{"*:7081": {"pass": "applications/app"}}, 'listeners'
), 'applications restore'

View File

@@ -1,155 +1,162 @@
from unit.applications.lang.python import TestApplicationPython from unit.applications.lang.python import ApplicationPython
prerequisites = {'modules': {'python': 'any'}} prerequisites = {'modules': {'python': 'any'}}
client = ApplicationPython()
class TestPythonEnvironment(TestApplicationPython):
def test_python_environment_name_null(self):
self.load('environment')
assert 'error' in self.conf( def test_python_environment_name_null():
{"va\0r": "val1"}, 'applications/environment/environment' client.load('environment')
), 'name null'
def test_python_environment_name_equals(self): assert 'error' in client.conf(
self.load('environment') {"va\0r": "val1"}, 'applications/environment/environment'
), 'name null'
assert 'error' in self.conf(
{"var=": "val1"}, 'applications/environment/environment'
), 'name equals'
def test_python_environment_value_null(self): def test_python_environment_name_equals():
self.load('environment') client.load('environment')
assert 'error' in self.conf( assert 'error' in client.conf(
{"var": "\0val"}, 'applications/environment/environment' {"var=": "val1"}, 'applications/environment/environment'
), 'value null' ), 'name equals'
def test_python_environment_update(self):
self.load('environment')
self.conf({"var": "val1"}, 'applications/environment/environment') def test_python_environment_value_null():
client.load('environment')
assert ( assert 'error' in client.conf(
self.get( {"var": "\0val"}, 'applications/environment/environment'
headers={ ), 'value null'
'Host': 'localhost',
'X-Variables': 'var',
'Connection': 'close',
}
)['body']
== 'val1'
), 'set'
self.conf({"var": "val2"}, 'applications/environment/environment')
assert ( def test_python_environment_update():
self.get( client.load('environment')
headers={
'Host': 'localhost',
'X-Variables': 'var',
'Connection': 'close',
}
)['body']
== 'val2'
), 'update'
def test_python_environment_replace(self): client.conf({"var": "val1"}, 'applications/environment/environment')
self.load('environment')
self.conf({"var1": "val1"}, 'applications/environment/environment') assert (
client.get(
headers={
'Host': 'localhost',
'X-Variables': 'var',
'Connection': 'close',
}
)['body']
== 'val1'
), 'set'
assert ( client.conf({"var": "val2"}, 'applications/environment/environment')
self.get(
headers={
'Host': 'localhost',
'X-Variables': 'var1',
'Connection': 'close',
}
)['body']
== 'val1'
), 'set'
self.conf({"var2": "val2"}, 'applications/environment/environment') assert (
client.get(
headers={
'Host': 'localhost',
'X-Variables': 'var',
'Connection': 'close',
}
)['body']
== 'val2'
), 'update'
assert (
self.get(
headers={
'Host': 'localhost',
'X-Variables': 'var1,var2',
'Connection': 'close',
}
)['body']
== 'val2'
), 'replace'
def test_python_environment_clear(self): def test_python_environment_replace():
self.load('environment') client.load('environment')
self.conf( client.conf({"var1": "val1"}, 'applications/environment/environment')
{"var1": "val1", "var2": "val2"},
'applications/environment/environment',
)
assert ( assert (
self.get( client.get(
headers={ headers={
'Host': 'localhost', 'Host': 'localhost',
'X-Variables': 'var1,var2', 'X-Variables': 'var1',
'Connection': 'close', 'Connection': 'close',
} }
)['body'] )['body']
== 'val1,val2' == 'val1'
), 'set' ), 'set'
self.conf({}, 'applications/environment/environment') client.conf({"var2": "val2"}, 'applications/environment/environment')
assert ( assert (
self.get( client.get(
headers={ headers={
'Host': 'localhost', 'Host': 'localhost',
'X-Variables': 'var1,var2', 'X-Variables': 'var1,var2',
'Connection': 'close', 'Connection': 'close',
} }
)['body'] )['body']
== '' == 'val2'
), 'clear' ), 'replace'
def test_python_environment_replace_default(self):
self.load('environment')
home_default = self.get( def test_python_environment_clear():
client.load('environment')
client.conf(
{"var1": "val1", "var2": "val2"},
'applications/environment/environment',
)
assert (
client.get(
headers={
'Host': 'localhost',
'X-Variables': 'var1,var2',
'Connection': 'close',
}
)['body']
== 'val1,val2'
), 'set'
client.conf({}, 'applications/environment/environment')
assert (
client.get(
headers={
'Host': 'localhost',
'X-Variables': 'var1,var2',
'Connection': 'close',
}
)['body']
== ''
), 'clear'
def test_python_environment_replace_default():
client.load('environment')
home_default = client.get(
headers={
'Host': 'localhost',
'X-Variables': 'HOME',
'Connection': 'close',
}
)['body']
assert len(home_default) > 1, 'get default'
client.conf({"HOME": "/"}, 'applications/environment/environment')
assert (
client.get(
headers={ headers={
'Host': 'localhost', 'Host': 'localhost',
'X-Variables': 'HOME', 'X-Variables': 'HOME',
'Connection': 'close', 'Connection': 'close',
} }
)['body'] )['body']
== '/'
), 'replace default'
assert len(home_default) > 1, 'get default' client.conf({}, 'applications/environment/environment')
self.conf({"HOME": "/"}, 'applications/environment/environment') assert (
client.get(
assert ( headers={
self.get( 'Host': 'localhost',
headers={ 'X-Variables': 'HOME',
'Host': 'localhost', 'Connection': 'close',
'X-Variables': 'HOME', }
'Connection': 'close', )['body']
} == home_default
)['body'] ), 'restore default'
== '/'
), 'replace default'
self.conf({}, 'applications/environment/environment')
assert (
self.get(
headers={
'Host': 'localhost',
'X-Variables': 'HOME',
'Connection': 'close',
}
)['body']
== home_default
), 'restore default'

View File

@@ -4,7 +4,7 @@ import subprocess
from pathlib import Path from pathlib import Path
import pytest import pytest
from unit.applications.lang.python import TestApplicationPython from unit.applications.lang.python import ApplicationPython
from unit.option import option from unit.option import option
from unit.utils import findmnt from unit.utils import findmnt
from unit.utils import waitformount from unit.utils import waitformount
@@ -12,204 +12,203 @@ from unit.utils import waitforunmount
prerequisites = {'modules': {'python': 'any'}, 'features': {'isolation': True}} prerequisites = {'modules': {'python': 'any'}, 'features': {'isolation': True}}
client = ApplicationPython()
class TestPythonIsolation(TestApplicationPython):
def get_cgroup(self, app_name):
output = subprocess.check_output(
['ps', 'ax', '-o', 'pid', '-o', 'cmd']
).decode()
pid = re.search( def get_cgroup(app_name):
fr'(\d+)\s*unit: "{app_name}" application', output output = subprocess.check_output(
).group(1) ['ps', 'ax', '-o', 'pid', '-o', 'cmd']
).decode()
cgroup = f'/proc/{pid}/cgroup' pid = re.search(fr'(\d+)\s*unit: "{app_name}" application', output).group(1)
if not os.path.isfile(cgroup): cgroup = f'/proc/{pid}/cgroup'
pytest.skip(f'no cgroup at {cgroup}')
with open(cgroup, 'r') as f: if not os.path.isfile(cgroup):
return f.read().rstrip() pytest.skip(f'no cgroup at {cgroup}')
def test_python_isolation_rootfs(self, is_su, require, temp_dir): with open(cgroup, 'r') as f:
isolation = {'rootfs': temp_dir} return f.read().rstrip()
if not is_su:
require( def test_python_isolation_rootfs(is_su, require, temp_dir):
{ isolation = {'rootfs': temp_dir}
'features': {
'isolation': [ if not is_su:
'unprivileged_userns_clone', require(
'user', {
'mnt', 'features': {
'pid', 'isolation': [
] 'unprivileged_userns_clone',
} 'user',
'mnt',
'pid',
]
} }
)
isolation['namespaces'] = {
'mount': True,
'credential': True,
'pid': True,
} }
self.load('ns_inspect', isolation=isolation)
assert not (
self.getjson(url=f'/?path={temp_dir}')['body']['FileExists']
), 'temp_dir does not exists in rootfs'
assert self.getjson(url='/?path=/proc/self')['body'][
'FileExists'
], 'no /proc/self'
assert not (
self.getjson(url='/?path=/dev/pts')['body']['FileExists']
), 'no /dev/pts'
assert not (
self.getjson(url='/?path=/sys/kernel')['body']['FileExists']
), 'no /sys/kernel'
ret = self.getjson(url='/?path=/app/python/ns_inspect')
assert ret['body']['FileExists'], 'application exists in rootfs'
def test_python_isolation_rootfs_no_language_deps(self, require, temp_dir):
require({'privileged_user': True})
isolation = {'rootfs': temp_dir, 'automount': {'language_deps': False}}
self.load('empty', isolation=isolation)
python_path = f'{temp_dir}/usr'
assert findmnt().find(python_path) == -1
assert self.get()['status'] != 200, 'disabled language_deps'
assert findmnt().find(python_path) == -1
isolation['automount']['language_deps'] = True
self.load('empty', isolation=isolation)
assert findmnt().find(python_path) == -1
assert self.get()['status'] == 200, 'enabled language_deps'
assert waitformount(python_path), 'language_deps mount'
self.conf({"listeners": {}, "applications": {}})
assert waitforunmount(python_path), 'language_deps unmount'
def test_python_isolation_procfs(self, require, temp_dir):
require({'privileged_user': True})
isolation = {'rootfs': temp_dir, 'automount': {'procfs': False}}
self.load('ns_inspect', isolation=isolation)
assert not (
self.getjson(url='/?path=/proc/self')['body']['FileExists']
), 'no /proc/self'
isolation['automount']['procfs'] = True
self.load('ns_inspect', isolation=isolation)
assert self.getjson(url='/?path=/proc/self')['body'][
'FileExists'
], '/proc/self'
def test_python_isolation_cgroup(self, require):
require(
{'privileged_user': True, 'features': {'isolation': ['cgroup']}}
) )
def set_cgroup_path(path): isolation['namespaces'] = {
isolation = {'cgroup': {'path': path}} 'mount': True,
self.load('empty', processes=1, isolation=isolation) 'credential': True,
'pid': True,
}
set_cgroup_path('scope/python') client.load('ns_inspect', isolation=isolation)
cgroup_rel = Path(self.get_cgroup('empty')) assert not (
assert cgroup_rel.parts[-2:] == ('scope', 'python'), 'cgroup rel' client.getjson(url=f'/?path={temp_dir}')['body']['FileExists']
), 'temp_dir does not exists in rootfs'
set_cgroup_path('/scope2/python') assert client.getjson(url='/?path=/proc/self')['body'][
'FileExists'
], 'no /proc/self'
cgroup_abs = Path(self.get_cgroup('empty')) assert not (
assert cgroup_abs.parts[-2:] == ('scope2', 'python'), 'cgroup abs' client.getjson(url='/?path=/dev/pts')['body']['FileExists']
), 'no /dev/pts'
assert len(cgroup_rel.parts) >= len(cgroup_abs.parts) assert not (
client.getjson(url='/?path=/sys/kernel')['body']['FileExists']
), 'no /sys/kernel'
def test_python_isolation_cgroup_two(self, require): ret = client.getjson(url='/?path=/app/python/ns_inspect')
require(
{'privileged_user': True, 'features': {'isolation': ['cgroup']}}
)
def set_two_cgroup_path(path, path2): assert ret['body']['FileExists'], 'application exists in rootfs'
script_path = f'{option.test_dir}/python/empty'
assert 'success' in self.conf(
{ def test_python_isolation_rootfs_no_language_deps(require, temp_dir):
"listeners": { require({'privileged_user': True})
"*:7080": {"pass": "applications/one"},
"*:7081": {"pass": "applications/two"}, isolation = {'rootfs': temp_dir, 'automount': {'language_deps': False}}
}, client.load('empty', isolation=isolation)
"applications": {
"one": { python_path = f'{temp_dir}/usr'
"type": "python",
"processes": 1, assert findmnt().find(python_path) == -1
"path": script_path, assert client.get()['status'] != 200, 'disabled language_deps'
"working_directory": script_path, assert findmnt().find(python_path) == -1
"module": "wsgi",
"isolation": { isolation['automount']['language_deps'] = True
'cgroup': {'path': path},
}, client.load('empty', isolation=isolation)
},
"two": { assert findmnt().find(python_path) == -1
"type": "python", assert client.get()['status'] == 200, 'enabled language_deps'
"processes": 1, assert waitformount(python_path), 'language_deps mount'
"path": script_path,
"working_directory": script_path, client.conf({"listeners": {}, "applications": {}})
"module": "wsgi",
"isolation": { assert waitforunmount(python_path), 'language_deps unmount'
'cgroup': {'path': path2},
},
def test_python_isolation_procfs(require, temp_dir):
require({'privileged_user': True})
isolation = {'rootfs': temp_dir, 'automount': {'procfs': False}}
client.load('ns_inspect', isolation=isolation)
assert not (
client.getjson(url='/?path=/proc/self')['body']['FileExists']
), 'no /proc/self'
isolation['automount']['procfs'] = True
client.load('ns_inspect', isolation=isolation)
assert client.getjson(url='/?path=/proc/self')['body'][
'FileExists'
], '/proc/self'
def test_python_isolation_cgroup(require):
require({'privileged_user': True, 'features': {'isolation': ['cgroup']}})
def set_cgroup_path(path):
isolation = {'cgroup': {'path': path}}
client.load('empty', processes=1, isolation=isolation)
set_cgroup_path('scope/python')
cgroup_rel = Path(get_cgroup('empty'))
assert cgroup_rel.parts[-2:] == ('scope', 'python'), 'cgroup rel'
set_cgroup_path('/scope2/python')
cgroup_abs = Path(get_cgroup('empty'))
assert cgroup_abs.parts[-2:] == ('scope2', 'python'), 'cgroup abs'
assert len(cgroup_rel.parts) >= len(cgroup_abs.parts)
def test_python_isolation_cgroup_two(require):
require({'privileged_user': True, 'features': {'isolation': ['cgroup']}})
def set_two_cgroup_path(path, path2):
script_path = f'{option.test_dir}/python/empty'
assert 'success' in client.conf(
{
"listeners": {
"*:7080": {"pass": "applications/one"},
"*:7081": {"pass": "applications/two"},
},
"applications": {
"one": {
"type": "python",
"processes": 1,
"path": script_path,
"working_directory": script_path,
"module": "wsgi",
"isolation": {
'cgroup': {'path': path},
}, },
}, },
} "two": {
) "type": "python",
"processes": 1,
set_two_cgroup_path('/scope/python', '/scope/python') "path": script_path,
assert self.get_cgroup('one') == self.get_cgroup('two') "working_directory": script_path,
"module": "wsgi",
set_two_cgroup_path('/scope/python', '/scope2/python') "isolation": {
assert self.get_cgroup('one') != self.get_cgroup('two') 'cgroup': {'path': path2},
},
def test_python_isolation_cgroup_invalid(self, require): },
require( },
{'privileged_user': True, 'features': {'isolation': ['cgroup']}} }
) )
def check_invalid(path): set_two_cgroup_path('/scope/python', '/scope/python')
script_path = f'{option.test_dir}/python/empty' assert get_cgroup('one') == get_cgroup('two')
assert 'error' in self.conf(
{
"listeners": {"*:7080": {"pass": "applications/empty"}},
"applications": {
"empty": {
"type": "python",
"processes": {"spare": 0},
"path": script_path,
"working_directory": script_path,
"module": "wsgi",
"isolation": {
'cgroup': {'path': path},
},
}
},
}
)
check_invalid('') set_two_cgroup_path('/scope/python', '/scope2/python')
check_invalid('../scope') assert get_cgroup('one') != get_cgroup('two')
check_invalid('scope/../python')
def test_python_isolation_cgroup_invalid(require):
require({'privileged_user': True, 'features': {'isolation': ['cgroup']}})
def check_invalid(path):
script_path = f'{option.test_dir}/python/empty'
assert 'error' in client.conf(
{
"listeners": {"*:7080": {"pass": "applications/empty"}},
"applications": {
"empty": {
"type": "python",
"processes": {"spare": 0},
"path": script_path,
"working_directory": script_path,
"module": "wsgi",
"isolation": {
'cgroup': {'path': path},
},
}
},
}
)
check_invalid('')
check_invalid('../scope')
check_invalid('scope/../python')

View File

@@ -1,30 +1,29 @@
from unit.applications.lang.python import TestApplicationPython from unit.applications.lang.python import ApplicationPython
prerequisites = {'modules': {'python': 'any'}, 'privileged_user': True} prerequisites = {'modules': {'python': 'any'}, 'privileged_user': True}
client = ApplicationPython()
class TestPythonIsolation(TestApplicationPython):
def test_python_isolation_chroot(self, temp_dir):
isolation = {'rootfs': temp_dir}
self.load('ns_inspect', isolation=isolation) def test_python_isolation_chroot(temp_dir):
client.load('ns_inspect', isolation={'rootfs': temp_dir})
assert not ( assert not (
self.getjson(url=f'/?path={temp_dir}')['body']['FileExists'] client.getjson(url=f'/?path={temp_dir}')['body']['FileExists']
), 'temp_dir does not exists in rootfs' ), 'temp_dir does not exists in rootfs'
assert self.getjson(url='/?path=/proc/self')['body'][ assert client.getjson(url='/?path=/proc/self')['body'][
'FileExists' 'FileExists'
], 'no /proc/self' ], 'no /proc/self'
assert not ( assert not (
self.getjson(url='/?path=/dev/pts')['body']['FileExists'] client.getjson(url='/?path=/dev/pts')['body']['FileExists']
), 'no /dev/pts' ), 'no /dev/pts'
assert not ( assert not (
self.getjson(url='/?path=/sys/kernel')['body']['FileExists'] client.getjson(url='/?path=/sys/kernel')['body']['FileExists']
), 'no /sys/kernel' ), 'no /sys/kernel'
ret = self.getjson(url='/?path=/app/python/ns_inspect') ret = client.getjson(url='/?path=/app/python/ns_inspect')
assert ret['body']['FileExists'], 'application exists in rootfs' assert ret['body']['FileExists'], 'application exists in rootfs'

View File

@@ -4,282 +4,299 @@ import subprocess
import time import time
import pytest import pytest
from unit.applications.lang.python import TestApplicationPython from unit.applications.lang.python import ApplicationPython
from unit.option import option from unit.option import option
prerequisites = {'modules': {'python': 'any'}} prerequisites = {'modules': {'python': 'any'}}
client = ApplicationPython()
class TestPythonProcman(TestApplicationPython):
@pytest.fixture(autouse=True)
def setup_method_fixture(self, temp_dir):
self.app_name = f'app-{temp_dir.split("/")[-1]}'
self.app_proc = f'applications/{self.app_name}/processes'
self.load('empty', self.app_name)
def pids_for_process(self): @pytest.fixture(autouse=True)
time.sleep(0.2) def setup_method_fixture(temp_dir):
client.app_name = f'app-{temp_dir.split("/")[-1]}'
client.app_proc = f'applications/{client.app_name}/processes'
client.load('empty', client.app_name)
output = subprocess.check_output(['ps', 'ax'])
pids = set() def pids_for_process():
for m in re.findall( time.sleep(0.2)
fr'.*unit: "{self.app_name}" application', output.decode()
):
pids.add(re.search(r'^\s*(\d+)', m).group(1))
return pids output = subprocess.check_output(['ps', 'ax'])
def conf_proc(self, conf, path=None): pids = set()
if path is None: for m in re.findall(
path = self.app_proc fr'.*unit: "{client.app_name}" application', output.decode()
):
pids.add(re.search(r'^\s*(\d+)', m).group(1))
assert 'success' in self.conf(conf, path), 'configure processes' return pids
@pytest.mark.skip('not yet')
def test_python_processes_idle_timeout_zero(self):
self.conf_proc({"spare": 0, "max": 2, "idle_timeout": 0})
self.get() def conf_proc(conf, path=None):
assert len(self.pids_for_process()) == 0, 'idle timeout 0' if path is None:
path = client.app_proc
def test_python_prefork(self): assert 'success' in client.conf(conf, path), 'configure processes'
self.conf_proc('2')
pids = self.pids_for_process()
assert len(pids) == 2, 'prefork 2'
self.get() def stop_all():
assert self.pids_for_process() == pids, 'prefork still 2' assert 'success' in client.conf({"listeners": {}, "applications": {}})
self.conf_proc('4') assert len(pids_for_process()) == 0, 'stop all'
pids = self.pids_for_process()
assert len(pids) == 4, 'prefork 4'
self.get() @pytest.mark.skip('not yet')
assert self.pids_for_process() == pids, 'prefork still 4' def test_python_processes_idle_timeout_zero():
conf_proc({"spare": 0, "max": 2, "idle_timeout": 0})
self.stop_all() client.get()
assert len(pids_for_process()) == 0, 'idle timeout 0'
@pytest.mark.skip('not yet')
def test_python_prefork_same_processes(self):
self.conf_proc('2')
pids = self.pids_for_process()
self.conf_proc('4') def test_python_prefork():
pids_new = self.pids_for_process() conf_proc('2')
assert pids.issubset(pids_new), 'prefork same processes' pids = pids_for_process()
assert len(pids) == 2, 'prefork 2'
def test_python_ondemand(self): client.get()
self.conf_proc({"spare": 0, "max": 8, "idle_timeout": 1}) assert pids_for_process() == pids, 'prefork still 2'
assert len(self.pids_for_process()) == 0, 'on-demand 0' conf_proc('4')
self.get() pids = pids_for_process()
pids = self.pids_for_process() assert len(pids) == 4, 'prefork 4'
assert len(pids) == 1, 'on-demand 1'
self.get() client.get()
assert self.pids_for_process() == pids, 'on-demand still 1' assert pids_for_process() == pids, 'prefork still 4'
time.sleep(1) stop_all()
assert len(self.pids_for_process()) == 0, 'on-demand stop idle'
self.stop_all() @pytest.mark.skip('not yet')
def test_python_prefork_same_processes():
conf_proc('2')
pids = pids_for_process()
def test_python_scale_updown(self): conf_proc('4')
self.conf_proc({"spare": 2, "max": 8, "idle_timeout": 1}) pids_new = pids_for_process()
pids = self.pids_for_process() assert pids.issubset(pids_new), 'prefork same processes'
assert len(pids) == 2, 'updown 2'
self.get()
pids_new = self.pids_for_process()
assert len(pids_new) == 3, 'updown 3'
assert pids.issubset(pids_new), 'updown 3 only 1 new'
self.get() def test_python_ondemand():
assert self.pids_for_process() == pids_new, 'updown still 3' conf_proc({"spare": 0, "max": 8, "idle_timeout": 1})
time.sleep(1) assert len(pids_for_process()) == 0, 'on-demand 0'
pids = self.pids_for_process() client.get()
assert len(pids) == 2, 'updown stop idle' pids = pids_for_process()
assert len(pids) == 1, 'on-demand 1'
self.get() client.get()
pids_new = self.pids_for_process() assert pids_for_process() == pids, 'on-demand still 1'
assert len(pids_new) == 3, 'updown again 3'
assert pids.issubset(pids_new), 'updown again 3 only 1 new'
self.stop_all() time.sleep(1)
def test_python_reconfigure(self): assert len(pids_for_process()) == 0, 'on-demand stop idle'
self.conf_proc({"spare": 2, "max": 6, "idle_timeout": 1})
pids = self.pids_for_process() stop_all()
assert len(pids) == 2, 'reconf 2'
self.get()
pids_new = self.pids_for_process()
assert len(pids_new) == 3, 'reconf 3'
assert pids.issubset(pids_new), 'reconf 3 only 1 new'
self.conf_proc('6', f'{self.app_proc}/spare') def test_python_scale_updown():
conf_proc({"spare": 2, "max": 8, "idle_timeout": 1})
pids = self.pids_for_process() pids = pids_for_process()
assert len(pids) == 6, 'reconf 6' assert len(pids) == 2, 'updown 2'
self.get() client.get()
assert self.pids_for_process() == pids, 'reconf still 6' pids_new = pids_for_process()
assert len(pids_new) == 3, 'updown 3'
assert pids.issubset(pids_new), 'updown 3 only 1 new'
self.stop_all() client.get()
assert pids_for_process() == pids_new, 'updown still 3'
def test_python_idle_timeout(self): time.sleep(1)
self.conf_proc({"spare": 0, "max": 6, "idle_timeout": 2})
self.get() pids = pids_for_process()
pids = self.pids_for_process() assert len(pids) == 2, 'updown stop idle'
assert len(pids) == 1, 'idle timeout 1'
time.sleep(1) client.get()
pids_new = pids_for_process()
assert len(pids_new) == 3, 'updown again 3'
assert pids.issubset(pids_new), 'updown again 3 only 1 new'
self.get() stop_all()
time.sleep(1)
pids_new = self.pids_for_process() def test_python_reconfigure():
assert len(pids_new) == 1, 'idle timeout still 1' conf_proc({"spare": 2, "max": 6, "idle_timeout": 1})
assert self.pids_for_process() == pids, 'idle timeout still 1 same pid'
time.sleep(1) pids = pids_for_process()
assert len(pids) == 2, 'reconf 2'
assert len(self.pids_for_process()) == 0, 'idle timed out' client.get()
pids_new = pids_for_process()
assert len(pids_new) == 3, 'reconf 3'
assert pids.issubset(pids_new), 'reconf 3 only 1 new'
def test_python_processes_connection_keepalive(self): conf_proc('6', f'{client.app_proc}/spare')
self.conf_proc({"spare": 0, "max": 6, "idle_timeout": 2})
(_, sock) = self.get( pids = pids_for_process()
headers={'Host': 'localhost', 'Connection': 'keep-alive'}, assert len(pids) == 6, 'reconf 6'
start=True,
read_timeout=1,
)
assert len(self.pids_for_process()) == 1, 'keepalive connection 1'
time.sleep(2) client.get()
assert pids_for_process() == pids, 'reconf still 6'
assert len(self.pids_for_process()) == 0, 'keepalive connection 0' stop_all()
sock.close()
def test_python_processes_access(self): def test_python_idle_timeout():
self.conf_proc('1') conf_proc({"spare": 0, "max": 6, "idle_timeout": 2})
path = f'/{self.app_proc}' client.get()
assert 'error' in self.conf_get(f'{path}/max') pids = pids_for_process()
assert 'error' in self.conf_get(f'{path}/spare') assert len(pids) == 1, 'idle timeout 1'
assert 'error' in self.conf_get(f'{path}/idle_timeout')
def test_python_processes_invalid(self): time.sleep(1)
assert 'error' in self.conf(
{"spare": -1}, self.app_proc
), 'negative spare'
assert 'error' in self.conf({"max": -1}, self.app_proc), 'negative max'
assert 'error' in self.conf(
{"idle_timeout": -1}, self.app_proc
), 'negative idle_timeout'
assert 'error' in self.conf(
{"spare": 2}, self.app_proc
), 'spare gt max default'
assert 'error' in self.conf(
{"spare": 2, "max": 1}, self.app_proc
), 'spare gt max'
assert 'error' in self.conf(
{"spare": 0, "max": 0}, self.app_proc
), 'max zero'
def stop_all(self): client.get()
assert 'success' in self.conf({"listeners": {}, "applications": {}})
assert len(self.pids_for_process()) == 0, 'stop all' time.sleep(1)
def test_python_restart(self, temp_dir): pids_new = pids_for_process()
shutil.copyfile( assert len(pids_new) == 1, 'idle timeout still 1'
f'{option.test_dir}/python/restart/v1.py', f'{temp_dir}/wsgi.py' assert pids_for_process() == pids, 'idle timeout still 1 same pid'
)
self.load( time.sleep(1)
temp_dir,
name=self.app_name,
processes=1,
environment={'PYTHONDONTWRITEBYTECODE': '1'},
)
b = self.get()['body'] assert len(pids_for_process()) == 0, 'idle timed out'
assert b == "v1", 'process started'
shutil.copyfile(
f'{option.test_dir}/python/restart/v2.py', f'{temp_dir}/wsgi.py'
)
b = self.get()['body'] def test_python_processes_connection_keepalive():
assert b == "v1", 'still old process' conf_proc({"spare": 0, "max": 6, "idle_timeout": 2})
assert 'success' in self.conf_get( (_, sock) = client.get(
f'/control/applications/{self.app_name}/restart' headers={'Host': 'localhost', 'Connection': 'keep-alive'},
), 'restart processes' start=True,
read_timeout=1,
)
assert len(pids_for_process()) == 1, 'keepalive connection 1'
b = self.get()['body'] time.sleep(2)
assert b == "v2", 'new process started'
assert 'error' in self.conf_get( assert len(pids_for_process()) == 0, 'keepalive connection 0'
'/control/applications/blah/restart'
), 'application incorrect'
assert 'error' in self.conf_delete( sock.close()
f'/control/applications/{self.app_name}/restart'
), 'method incorrect'
def test_python_restart_multi(self):
self.conf_proc('2')
pids = self.pids_for_process() def test_python_processes_access():
assert len(pids) == 2, 'restart 2 started' conf_proc('1')
assert 'success' in self.conf_get( path = f'/{client.app_proc}'
f'/control/applications/{self.app_name}/restart' assert 'error' in client.conf_get(f'{path}/max')
), 'restart processes' assert 'error' in client.conf_get(f'{path}/spare')
assert 'error' in client.conf_get(f'{path}/idle_timeout')
new_pids = self.pids_for_process()
assert len(new_pids) == 2, 'restart still 2'
assert len(new_pids.intersection(pids)) == 0, 'restart all new' def test_python_processes_invalid():
assert 'error' in client.conf(
{"spare": -1}, client.app_proc
), 'negative spare'
assert 'error' in client.conf({"max": -1}, client.app_proc), 'negative max'
assert 'error' in client.conf(
{"idle_timeout": -1}, client.app_proc
), 'negative idle_timeout'
assert 'error' in client.conf(
{"spare": 2}, client.app_proc
), 'spare gt max default'
assert 'error' in client.conf(
{"spare": 2, "max": 1}, client.app_proc
), 'spare gt max'
assert 'error' in client.conf(
{"spare": 0, "max": 0}, client.app_proc
), 'max zero'
def test_python_restart_longstart(self):
self.load(
'restart',
name=self.app_name,
module="longstart",
processes={"spare": 1, "max": 2, "idle_timeout": 5},
)
assert len(self.pids_for_process()) == 1, 'longstarts == 1' def test_python_restart(temp_dir):
shutil.copyfile(
f'{option.test_dir}/python/restart/v1.py', f'{temp_dir}/wsgi.py'
)
self.get() client.load(
temp_dir,
name=client.app_name,
processes=1,
environment={'PYTHONDONTWRITEBYTECODE': '1'},
)
pids = self.pids_for_process() b = client.get()['body']
assert len(pids) == 2, 'longstarts == 2' assert b == "v1", 'process started'
assert 'success' in self.conf_get( shutil.copyfile(
f'/control/applications/{self.app_name}/restart' f'{option.test_dir}/python/restart/v2.py', f'{temp_dir}/wsgi.py'
), 'restart processes' )
# wait for longstarted app
time.sleep(2)
new_pids = self.pids_for_process()
assert len(new_pids) == 1, 'restart 1'
assert len(new_pids.intersection(pids)) == 0, 'restart all new' b = client.get()['body']
assert b == "v1", 'still old process'
assert 'success' in client.conf_get(
f'/control/applications/{client.app_name}/restart'
), 'restart processes'
b = client.get()['body']
assert b == "v2", 'new process started'
assert 'error' in client.conf_get(
'/control/applications/blah/restart'
), 'application incorrect'
assert 'error' in client.conf_delete(
f'/control/applications/{client.app_name}/restart'
), 'method incorrect'
def test_python_restart_multi():
conf_proc('2')
pids = pids_for_process()
assert len(pids) == 2, 'restart 2 started'
assert 'success' in client.conf_get(
f'/control/applications/{client.app_name}/restart'
), 'restart processes'
new_pids = pids_for_process()
assert len(new_pids) == 2, 'restart still 2'
assert len(new_pids.intersection(pids)) == 0, 'restart all new'
def test_python_restart_longstart():
client.load(
'restart',
name=client.app_name,
module="longstart",
processes={"spare": 1, "max": 2, "idle_timeout": 5},
)
assert len(pids_for_process()) == 1, 'longstarts == 1'
client.get()
pids = pids_for_process()
assert len(pids) == 2, 'longstarts == 2'
assert 'success' in client.conf_get(
f'/control/applications/{client.app_name}/restart'
), 'restart processes'
# wait for longstarted app
time.sleep(2)
new_pids = pids_for_process()
assert len(new_pids) == 1, 'restart 1'
assert len(new_pids.intersection(pids)) == 0, 'restart all new'

View File

@@ -1,103 +1,105 @@
from unit.applications.lang.python import TestApplicationPython from unit.applications.lang.python import ApplicationPython
from unit.option import option from unit.option import option
prerequisites = {'modules': {'python': 'all'}} prerequisites = {'modules': {'python': 'all'}}
client = ApplicationPython()
class TestPythonTargets(TestApplicationPython):
def test_python_targets(self):
python_dir = f'{option.test_dir}/python'
assert 'success' in self.conf( def test_python_targets():
{ python_dir = f'{option.test_dir}/python'
"listeners": {"*:7080": {"pass": "routes"}},
"routes": [ assert 'success' in client.conf(
{ {
"match": {"uri": "/1"}, "listeners": {"*:7080": {"pass": "routes"}},
"action": {"pass": "applications/targets/1"}, "routes": [
}, {
{ "match": {"uri": "/1"},
"match": {"uri": "/2"}, "action": {"pass": "applications/targets/1"},
"action": {"pass": "applications/targets/2"},
},
],
"applications": {
"targets": {
"type": self.get_application_type(),
"working_directory": f'{python_dir}/targets/',
"path": f'{python_dir}/targets/',
"targets": {
"1": {
"module": "wsgi",
"callable": "wsgi_target_a",
},
"2": {
"module": "wsgi",
"callable": "wsgi_target_b",
},
},
}
}, },
} {
) "match": {"uri": "/2"},
"action": {"pass": "applications/targets/2"},
resp = self.get(url='/1')
assert resp['status'] == 200
assert resp['body'] == '1'
resp = self.get(url='/2')
assert resp['status'] == 200
assert resp['body'] == '2'
def test_python_targets_prefix(self):
python_dir = f'{option.test_dir}/python'
assert 'success' in self.conf(
{
"listeners": {"*:7080": {"pass": "routes"}},
"routes": [
{
"match": {"uri": ["/app*"]},
"action": {"pass": "applications/targets/app"},
},
{
"match": {"uri": "*"},
"action": {"pass": "applications/targets/catchall"},
},
],
"applications": {
"targets": {
"type": "python",
"working_directory": f'{python_dir}/targets/',
"path": f'{python_dir}/targets/',
"protocol": "wsgi",
"targets": {
"app": {
"module": "wsgi",
"callable": "wsgi_target_prefix",
"prefix": "/app/",
},
"catchall": {
"module": "wsgi",
"callable": "wsgi_target_prefix",
"prefix": "/api",
},
},
}
}, },
} ],
) "applications": {
"targets": {
"type": client.get_application_type(),
"working_directory": f'{python_dir}/targets/',
"path": f'{python_dir}/targets/',
"targets": {
"1": {
"module": "wsgi",
"callable": "wsgi_target_a",
},
"2": {
"module": "wsgi",
"callable": "wsgi_target_b",
},
},
}
},
}
)
def check_prefix(url, body): resp = client.get(url='/1')
resp = self.get(url=url) assert resp['status'] == 200
assert resp['status'] == 200 assert resp['body'] == '1'
assert resp['body'] == body
check_prefix('/app', '/app ') resp = client.get(url='/2')
check_prefix('/app/', '/app /') assert resp['status'] == 200
check_prefix('/app/rest/user/', '/app /rest/user/') assert resp['body'] == '2'
check_prefix('/catchall', 'No Script Name /catchall')
check_prefix('/api', '/api ')
check_prefix('/api/', '/api /') def test_python_targets_prefix():
check_prefix('/apis', 'No Script Name /apis') python_dir = f'{option.test_dir}/python'
check_prefix('/api/users/', '/api /users/')
assert 'success' in client.conf(
{
"listeners": {"*:7080": {"pass": "routes"}},
"routes": [
{
"match": {"uri": ["/app*"]},
"action": {"pass": "applications/targets/app"},
},
{
"match": {"uri": "*"},
"action": {"pass": "applications/targets/catchall"},
},
],
"applications": {
"targets": {
"type": "python",
"working_directory": f'{python_dir}/targets/',
"path": f'{python_dir}/targets/',
"protocol": "wsgi",
"targets": {
"app": {
"module": "wsgi",
"callable": "wsgi_target_prefix",
"prefix": "/app/",
},
"catchall": {
"module": "wsgi",
"callable": "wsgi_target_prefix",
"prefix": "/api",
},
},
}
},
}
)
def check_prefix(url, body):
resp = client.get(url=url)
assert resp['status'] == 200
assert resp['body'] == body
check_prefix('/app', '/app ')
check_prefix('/app/', '/app /')
check_prefix('/app/rest/user/', '/app /rest/user/')
check_prefix('/catchall', 'No Script Name /catchall')
check_prefix('/api', '/api ')
check_prefix('/api/', '/api /')
check_prefix('/apis', 'No Script Name /apis')
check_prefix('/api/users/', '/api /users/')

View File

@@ -1,50 +1,54 @@
import time import time
import pytest import pytest
from unit.applications.proto import TestApplicationProto from unit.applications.proto import ApplicationProto
client = ApplicationProto()
class TestReconfigure(TestApplicationProto): @pytest.fixture(autouse=True)
@pytest.fixture(autouse=True) def setup_method_fixture():
def setup_method_fixture(self): assert 'success' in client.conf(
assert 'success' in self.conf( {
{ "listeners": {"*:7080": {"pass": "routes"}},
"listeners": {"*:7080": {"pass": "routes"}}, "routes": [{"action": {"return": 200}}],
"routes": [{"action": {"return": 200}}], "applications": {},
"applications": {}, }
} )
)
def clear_conf(self):
assert 'success' in self.conf({"listeners": {}, "applications": {}})
def test_reconfigure(self): def clear_conf():
sock = self.http( assert 'success' in client.conf({"listeners": {}, "applications": {}})
b"""GET / HTTP/1.1
def test_reconfigure():
sock = client.http(
b"""GET / HTTP/1.1
""", """,
raw=True, raw=True,
no_recv=True, no_recv=True,
) )
self.clear_conf() clear_conf()
resp = self.http( resp = client.http(
b"""Host: localhost b"""Host: localhost
Connection: close Connection: close
""", """,
sock=sock, sock=sock,
raw=True, raw=True,
) )
assert resp['status'] == 200, 'finish request' assert resp['status'] == 200, 'finish request'
def test_reconfigure_2(self):
sock = self.http(b'', raw=True, no_recv=True)
# Waiting for connection completion. def test_reconfigure_2():
# Delay should be more than TCP_DEFER_ACCEPT. sock = client.http(b'', raw=True, no_recv=True)
time.sleep(1.5)
self.clear_conf() # Waiting for connection completion.
# Delay should be more than TCP_DEFER_ACCEPT.
time.sleep(1.5)
assert self.get(sock=sock)['status'] == 408, 'request timeout' clear_conf()
assert client.get(sock=sock)['status'] == 408, 'request timeout'

View File

@@ -3,103 +3,110 @@ import ssl
import time import time
import pytest import pytest
from unit.applications.tls import TestApplicationTLS from unit.applications.tls import ApplicationTLS
prerequisites = {'modules': {'openssl': 'any'}} prerequisites = {'modules': {'openssl': 'any'}}
client = ApplicationTLS()
class TestReconfigureTLS(TestApplicationTLS):
@pytest.fixture(autouse=True)
def setup_method_fixture(self):
if 'HAS_TLSv1_2' not in dir(ssl) or not ssl.HAS_TLSv1_2:
pytest.skip('OpenSSL too old')
self.certificate() @pytest.fixture(autouse=True)
def setup_method_fixture():
if 'HAS_TLSv1_2' not in dir(ssl) or not ssl.HAS_TLSv1_2:
pytest.skip('OpenSSL too old')
assert 'success' in self.conf( client.certificate()
{
"listeners": {
"*:7080": {
"pass": "routes",
"tls": {"certificate": "default"},
}
},
"routes": [{"action": {"return": 200}}],
"applications": {},
}
), 'load application configuration'
def create_socket(self): assert 'success' in client.conf(
ctx = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT) {
ctx.check_hostname = False "listeners": {
ctx.verify_mode = ssl.CERT_NONE "*:7080": {
"pass": "routes",
"tls": {"certificate": "default"},
}
},
"routes": [{"action": {"return": 200}}],
"applications": {},
}
), 'load application configuration'
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
ssl_sock = ctx.wrap_socket(
s, server_hostname='localhost', do_handshake_on_connect=False
)
ssl_sock.connect(('127.0.0.1', 7080))
return ssl_sock def create_socket():
ctx = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT)
ctx.check_hostname = False
ctx.verify_mode = ssl.CERT_NONE
def clear_conf(self): s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
assert 'success' in self.conf({"listeners": {}, "applications": {}}) ssl_sock = ctx.wrap_socket(
s, server_hostname='localhost', do_handshake_on_connect=False
)
ssl_sock.connect(('127.0.0.1', 7080))
@pytest.mark.skip('not yet') return ssl_sock
def test_reconfigure_tls_switch(self):
assert 'success' in self.conf_delete('listeners/*:7080/tls')
(_, sock) = self.get(
headers={'Host': 'localhost', 'Connection': 'keep-alive'},
start=True,
read_timeout=1,
)
assert 'success' in self.conf( def clear_conf():
{"pass": "routes", "tls": {"certificate": "default"}}, assert 'success' in client.conf({"listeners": {}, "applications": {}})
'listeners/*:7080',
)
assert self.get(sock=sock)['status'] == 200, 'reconfigure'
assert self.get_ssl()['status'] == 200, 'reconfigure tls'
def test_reconfigure_tls(self): @pytest.mark.skip('not yet')
ssl_sock = self.create_socket() def test_reconfigure_tls_switch():
assert 'success' in client.conf_delete('listeners/*:7080/tls')
ssl_sock.sendall("""GET / HTTP/1.1\r\n""".encode()) (_, sock) = client.get(
headers={'Host': 'localhost', 'Connection': 'keep-alive'},
start=True,
read_timeout=1,
)
self.clear_conf() assert 'success' in client.conf(
{"pass": "routes", "tls": {"certificate": "default"}},
'listeners/*:7080',
)
ssl_sock.sendall( assert client.get(sock=sock)['status'] == 200, 'reconfigure'
"""Host: localhost\r\nConnection: close\r\n\r\n""".encode() assert client.get_ssl()['status'] == 200, 'reconfigure tls'
)
assert (
self.recvall(ssl_sock).decode().startswith('HTTP/1.1 200 OK')
), 'finish request'
def test_reconfigure_tls_2(self): def test_reconfigure_tls():
ssl_sock = self.create_socket() ssl_sock = create_socket()
# Waiting for connection completion. ssl_sock.sendall("""GET / HTTP/1.1\r\n""".encode())
# Delay should be more than TCP_DEFER_ACCEPT.
time.sleep(1.5)
self.clear_conf() clear_conf()
try: ssl_sock.sendall(
ssl_sock.do_handshake() """Host: localhost\r\nConnection: close\r\n\r\n""".encode()
except ssl.SSLError: )
ssl_sock.close()
success = True
if not success: assert (
pytest.fail('Connection is not closed.') client.recvall(ssl_sock).decode().startswith('HTTP/1.1 200 OK')
), 'finish request'
def test_reconfigure_tls_3(self):
ssl_sock = self.create_socket() def test_reconfigure_tls_2():
ssl_sock = create_socket()
# Waiting for connection completion.
# Delay should be more than TCP_DEFER_ACCEPT.
time.sleep(1.5)
clear_conf()
try:
ssl_sock.do_handshake() ssl_sock.do_handshake()
except ssl.SSLError:
ssl_sock.close()
success = True
self.clear_conf() if not success:
pytest.fail('Connection is not closed.')
assert self.get(sock=ssl_sock)['status'] == 408, 'request timeout'
def test_reconfigure_tls_3():
ssl_sock = create_socket()
ssl_sock.do_handshake()
clear_conf()
assert client.get(sock=ssl_sock)['status'] == 408, 'request timeout'

View File

@@ -3,99 +3,106 @@ import subprocess
import time import time
import pytest import pytest
from unit.applications.lang.python import TestApplicationPython from unit.applications.lang.python import ApplicationPython
prerequisites = {'modules': {'python': 'any'}} prerequisites = {'modules': {'python': 'any'}}
client = ApplicationPython()
class TestRespawn(TestApplicationPython): PATTERN_ROUTER = 'unit: router'
PATTERN_ROUTER = 'unit: router' PATTERN_CONTROLLER = 'unit: controller'
PATTERN_CONTROLLER = 'unit: controller'
@pytest.fixture(autouse=True)
def setup_method_fixture(self, temp_dir):
self.app_name = f'app-{temp_dir.split("/")[-1]}'
self.load('empty', self.app_name) @pytest.fixture(autouse=True)
def setup_method_fixture(temp_dir):
client.app_name = f'app-{temp_dir.split("/")[-1]}'
assert 'success' in self.conf( client.load('empty', client.app_name)
'1', f'applications/{self.app_name}/processes'
)
def pid_by_name(self, name, ppid): assert 'success' in client.conf(
output = subprocess.check_output(['ps', 'ax', '-O', 'ppid']).decode() '1', f'applications/{client.app_name}/processes'
m = re.search(fr'\s*(\d+)\s*{ppid}.*{name}', output) )
return None if m is None else m.group(1)
def kill_pids(self, *pids):
subprocess.call(['kill', '-9', *pids])
def wait_for_process(self, process, unit_pid): def pid_by_name(name, ppid):
for _ in range(50): output = subprocess.check_output(['ps', 'ax', '-O', 'ppid']).decode()
found = self.pid_by_name(process, unit_pid) m = re.search(fr'\s*(\d+)\s*{ppid}.*{name}', output)
return None if m is None else m.group(1)
if found is not None:
break
time.sleep(0.1) def kill_pids(*pids):
subprocess.call(['kill', '-9', *pids])
return found
def find_proc(self, name, ppid, ps_output): def wait_for_process(process, unit_pid):
return re.findall(fr'{ppid}.*{name}', ps_output) for _ in range(50):
found = pid_by_name(process, unit_pid)
def smoke_test(self, unit_pid): if found is not None:
for _ in range(10): break
r = self.conf('1', f'applications/{self.app_name}/processes')
if 'success' in r: time.sleep(0.1)
break
time.sleep(0.1) return found
assert 'success' in r
assert self.get()['status'] == 200
# Check if the only one router, controller, def find_proc(name, ppid, ps_output):
# and application processes running. return re.findall(fr'{ppid}.*{name}', ps_output)
out = subprocess.check_output(['ps', 'ax', '-O', 'ppid']).decode()
assert len(self.find_proc(self.PATTERN_ROUTER, unit_pid, out)) == 1
assert len(self.find_proc(self.PATTERN_CONTROLLER, unit_pid, out)) == 1
assert len(self.find_proc(self.app_name, unit_pid, out)) == 1
def test_respawn_router(self, skip_alert, unit_pid, skip_fds_check): def smoke_test(unit_pid):
skip_fds_check(router=True) for _ in range(10):
pid = self.pid_by_name(self.PATTERN_ROUTER, unit_pid) r = client.conf('1', f'applications/{client.app_name}/processes')
self.kill_pids(pid) if 'success' in r:
skip_alert(fr'process {pid} exited on signal 9') break
assert self.wait_for_process(self.PATTERN_ROUTER, unit_pid) is not None time.sleep(0.1)
self.smoke_test(unit_pid) assert 'success' in r
assert client.get()['status'] == 200
def test_respawn_controller(self, skip_alert, unit_pid, skip_fds_check): # Check if the only one router, controller,
skip_fds_check(controller=True) # and application processes running.
pid = self.pid_by_name(self.PATTERN_CONTROLLER, unit_pid)
self.kill_pids(pid) out = subprocess.check_output(['ps', 'ax', '-O', 'ppid']).decode()
skip_alert(fr'process {pid} exited on signal 9') assert len(find_proc(PATTERN_ROUTER, unit_pid, out)) == 1
assert len(find_proc(PATTERN_CONTROLLER, unit_pid, out)) == 1
assert len(find_proc(client.app_name, unit_pid, out)) == 1
assert (
self.wait_for_process(self.PATTERN_CONTROLLER, unit_pid) is not None
)
assert self.get()['status'] == 200 def test_respawn_router(skip_alert, unit_pid, skip_fds_check):
skip_fds_check(router=True)
pid = pid_by_name(PATTERN_ROUTER, unit_pid)
self.smoke_test(unit_pid) kill_pids(pid)
skip_alert(fr'process {pid} exited on signal 9')
def test_respawn_application(self, skip_alert, unit_pid): assert wait_for_process(PATTERN_ROUTER, unit_pid) is not None
pid = self.pid_by_name(self.app_name, unit_pid)
self.kill_pids(pid) smoke_test(unit_pid)
skip_alert(fr'process {pid} exited on signal 9')
assert self.wait_for_process(self.app_name, unit_pid) is not None
self.smoke_test(unit_pid) def test_respawn_controller(skip_alert, unit_pid, skip_fds_check):
skip_fds_check(controller=True)
pid = pid_by_name(PATTERN_CONTROLLER, unit_pid)
kill_pids(pid)
skip_alert(fr'process {pid} exited on signal 9')
assert wait_for_process(PATTERN_CONTROLLER, unit_pid) is not None
assert client.get()['status'] == 200
smoke_test(unit_pid)
def test_respawn_application(skip_alert, unit_pid):
pid = pid_by_name(client.app_name, unit_pid)
kill_pids(pid)
skip_alert(fr'process {pid} exited on signal 9')
assert wait_for_process(client.app_name, unit_pid) is not None
smoke_test(unit_pid)

View File

@@ -1,214 +1,220 @@
import re import re
import pytest import pytest
from unit.applications.proto import TestApplicationProto from unit.applications.proto import ApplicationProto
client = ApplicationProto()
class TestReturn(TestApplicationProto): @pytest.fixture(autouse=True)
@pytest.fixture(autouse=True) def setup_method_fixture():
def setup_method_fixture(self): assert 'success' in client.conf(
self._load_conf( {
{ "listeners": {"*:7080": {"pass": "routes"}},
"listeners": {"*:7080": {"pass": "routes"}}, "routes": [{"action": {"return": 200}}],
"routes": [{"action": {"return": 200}}], "applications": {},
"applications": {}, }
} )
)
def get_resps_sc(self, req=10):
to_send = b"""GET / HTTP/1.1 def get_resps_sc(req=10):
to_send = b"""GET / HTTP/1.1
Host: localhost Host: localhost
""" * ( """ * (
req - 1 req - 1
) )
to_send += b"""GET / HTTP/1.1 to_send += b"""GET / HTTP/1.1
Host: localhost Host: localhost
Connection: close Connection: close
""" """
return self.http(to_send, raw_resp=True, raw=True) return client.http(to_send, raw_resp=True, raw=True)
def test_return(self):
resp = self.get()
assert resp['status'] == 200
assert 'Server' in resp['headers']
assert 'Date' in resp['headers']
assert resp['headers']['Content-Length'] == '0'
assert resp['headers']['Connection'] == 'close'
assert resp['body'] == '', 'body'
resp = self.post(body='blah') def test_return():
assert resp['status'] == 200 resp = client.get()
assert resp['body'] == '', 'body' assert resp['status'] == 200
assert 'Server' in resp['headers']
assert 'Date' in resp['headers']
assert resp['headers']['Content-Length'] == '0'
assert resp['headers']['Connection'] == 'close'
assert resp['body'] == '', 'body'
resp = self.get_resps_sc() resp = client.post(body='blah')
assert len(re.findall('200 OK', resp)) == 10 assert resp['status'] == 200
assert len(re.findall('Connection:', resp)) == 1 assert resp['body'] == '', 'body'
assert len(re.findall('Connection: close', resp)) == 1
resp = self.get(http_10=True) resp = get_resps_sc()
assert resp['status'] == 200 assert len(re.findall('200 OK', resp)) == 10
assert 'Server' in resp['headers'] assert len(re.findall('Connection:', resp)) == 1
assert 'Date' in resp['headers'] assert len(re.findall('Connection: close', resp)) == 1
assert resp['headers']['Content-Length'] == '0'
assert 'Connection' not in resp['headers']
assert resp['body'] == '', 'body'
def test_return_update(self): resp = client.get(http_10=True)
assert 'success' in self.conf('0', 'routes/0/action/return') assert resp['status'] == 200
assert 'Server' in resp['headers']
assert 'Date' in resp['headers']
assert resp['headers']['Content-Length'] == '0'
assert 'Connection' not in resp['headers']
assert resp['body'] == '', 'body'
resp = self.get()
assert resp['status'] == 0
assert resp['body'] == ''
assert 'success' in self.conf('404', 'routes/0/action/return') def test_return_update():
assert 'success' in client.conf('0', 'routes/0/action/return')
resp = self.get() resp = client.get()
assert resp['status'] == 404 assert resp['status'] == 0
assert resp['body'] != '' assert resp['body'] == ''
assert 'success' in self.conf('598', 'routes/0/action/return') assert 'success' in client.conf('404', 'routes/0/action/return')
resp = self.get() resp = client.get()
assert resp['status'] == 598 assert resp['status'] == 404
assert resp['body'] != '' assert resp['body'] != ''
assert 'success' in self.conf('999', 'routes/0/action/return') assert 'success' in client.conf('598', 'routes/0/action/return')
resp = self.get() resp = client.get()
assert resp['status'] == 999 assert resp['status'] == 598
assert resp['body'] == '' assert resp['body'] != ''
def test_return_location(self): assert 'success' in client.conf('999', 'routes/0/action/return')
reserved = ":/?#[]@!&'()*+,;="
unreserved = (
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"
"0123456789-._~"
)
unsafe = " \"%<>\\^`{|}"
unsafe_enc = "%20%22%25%3C%3E%5C%5E%60%7B%7C%7D"
def check_location(location, expect=None): resp = client.get()
if expect is None: assert resp['status'] == 999
expect = location assert resp['body'] == ''
assert 'success' in self.conf(
{"return": 301, "location": location}, 'routes/0/action'
), 'configure location'
assert self.get()['headers']['Location'] == expect def test_return_location():
reserved = ":/?#[]@!&'()*+,;="
unreserved = (
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz" "0123456789-._~"
)
unsafe = " \"%<>\\^`{|}"
unsafe_enc = "%20%22%25%3C%3E%5C%5E%60%7B%7C%7D"
# FAIL: can't specify empty header value. def check_location(location, expect=None):
# check_location("") if expect is None:
expect = location
check_location(reserved) assert 'success' in client.conf(
{"return": 301, "location": location}, 'routes/0/action'
), 'configure location'
# After first "?" all other "?" encoded. assert client.get()['headers']['Location'] == expect
check_location(f'/?{reserved}', "/?:/%3F#[]@!&'()*+,;=")
check_location("???", "?%3F%3F")
# After first "#" all other "?" or "#" encoded. # FAIL: can't specify empty header value.
check_location(f'/#{reserved}', "/#:/%3F%23[]@!&'()*+,;=") # check_location("")
check_location("##?#?", "#%23%3F%23%3F")
# After first "?" next "#" not encoded. check_location(reserved)
check_location(f'/?#{reserved}', "/?#:/%3F%23[]@!&'()*+,;=")
check_location("??##", "?%3F#%23")
check_location("/?##?", "/?#%23%3F")
# Unreserved never encoded. # After first "?" all other "?" encoded.
check_location(unreserved) check_location(f'/?{reserved}', "/?:/%3F#[]@!&'()*+,;=")
check_location(f'/{unreserved}?{unreserved}#{unreserved}') check_location("???", "?%3F%3F")
# Unsafe always encoded. # After first "#" all other "?" or "#" encoded.
check_location(unsafe, unsafe_enc) check_location(f'/#{reserved}', "/#:/%3F%23[]@!&'()*+,;=")
check_location(f'?{unsafe}', f'?{unsafe_enc}') check_location("##?#?", "#%23%3F%23%3F")
check_location(f'#{unsafe}', f'#{unsafe_enc}')
# %00-%20 and %7F-%FF always encoded. # After first "?" next "#" not encoded.
check_location("\u0000\u0018\u001F\u0020\u0021", "%00%18%1F%20!") check_location(f'/?#{reserved}', "/?#:/%3F%23[]@!&'()*+,;=")
check_location("\u007F\u0080н\u20BD", "%7F%C2%80%D0%BD%E2%82%BD") check_location("??##", "?%3F#%23")
check_location("/?##?", "/?#%23%3F")
# Encoded string detection. If at least one char need to be encoded # Unreserved never encoded.
# then whole string will be encoded. check_location(unreserved)
check_location("%20") check_location(f'/{unreserved}?{unreserved}#{unreserved}')
check_location("/%20?%20#%20")
check_location(" %20", "%20%2520")
check_location("%20 ", "%2520%20")
check_location("/%20?%20#%20 ", "/%2520?%2520#%2520%20")
def test_return_location_edit(self): # Unsafe always encoded.
assert 'success' in self.conf( check_location(unsafe, unsafe_enc)
{"return": 302, "location": "blah"}, 'routes/0/action' check_location(f'?{unsafe}', f'?{unsafe_enc}')
), 'configure init location' check_location(f'#{unsafe}', f'#{unsafe_enc}')
assert self.get()['headers']['Location'] == 'blah'
assert 'success' in self.conf_delete( # %00-%20 and %7F-%FF always encoded.
'routes/0/action/location' check_location("\u0000\u0018\u001F\u0020\u0021", "%00%18%1F%20!")
), 'location delete' check_location("\u007F\u0080н\u20BD", "%7F%C2%80%D0%BD%E2%82%BD")
assert 'Location' not in self.get()['headers']
assert 'success' in self.conf( # Encoded string detection. If at least one char need to be encoded
'"blah"', 'routes/0/action/location' # then whole string will be encoded.
), 'location restore' check_location("%20")
assert self.get()['headers']['Location'] == 'blah' check_location("/%20?%20#%20")
check_location(" %20", "%20%2520")
check_location("%20 ", "%2520%20")
check_location("/%20?%20#%20 ", "/%2520?%2520#%2520%20")
assert 'error' in self.conf_post(
'"blah"', 'routes/0/action/location'
), 'location method not allowed'
assert self.get()['headers']['Location'] == 'blah'
assert 'success' in self.conf( def test_return_location_edit():
'"https://${host}${uri}"', 'routes/0/action/location' assert 'success' in client.conf(
), 'location with variables' {"return": 302, "location": "blah"}, 'routes/0/action'
assert self.get()['headers']['Location'] == 'https://localhost/' ), 'configure init location'
assert client.get()['headers']['Location'] == 'blah'
assert 'success' in self.conf( assert 'success' in client.conf_delete(
'"/#$host"', 'routes/0/action/location' 'routes/0/action/location'
), 'location with encoding and a variable' ), 'location delete'
assert self.get()['headers']['Location'] == '/#localhost' assert 'Location' not in client.get()['headers']
assert ( assert 'success' in client.conf(
self.get(headers={"Host": "#foo?bar", "Connection": "close"})[ '"blah"', 'routes/0/action/location'
'headers' ), 'location restore'
]['Location'] assert client.get()['headers']['Location'] == 'blah'
== "/#%23foo%3Fbar"
), 'location with a variable with encoding'
assert 'success' in self.conf( assert 'error' in client.conf_post(
'""', 'routes/0/action/location' '"blah"', 'routes/0/action/location'
), 'location empty' ), 'location method not allowed'
assert self.get()['headers']['Location'] == '' assert client.get()['headers']['Location'] == 'blah'
assert 'success' in self.conf( assert 'success' in client.conf(
'"${host}"', 'routes/0/action/location' '"https://${host}${uri}"', 'routes/0/action/location'
), 'location empty with variable' ), 'location with variables'
assert ( assert client.get()['headers']['Location'] == 'https://localhost/'
self.get(headers={"Host": "", "Connection": "close"})['headers'][
'Location'
]
== ""
), 'location with empty variable'
def test_return_invalid(self): assert 'success' in client.conf(
def check_error(conf): '"/#$host"', 'routes/0/action/location'
assert 'error' in self.conf(conf, 'routes/0/action') ), 'location with encoding and a variable'
assert client.get()['headers']['Location'] == '/#localhost'
check_error({"return": "200"}) assert (
check_error({"return": []}) client.get(headers={"Host": "#foo?bar", "Connection": "close"})[
check_error({"return": 80.1}) 'headers'
check_error({"return": 1000}) ]['Location']
check_error({"return": -1}) == "/#%23foo%3Fbar"
check_error({"return": 200, "share": "/blah"}) ), 'location with a variable with encoding'
check_error({"return": 200, "location": "$hos"})
check_error({"return": 200, "location": "$hostblah"})
assert 'error' in self.conf( assert 'success' in client.conf(
'001', 'routes/0/action/return' '""', 'routes/0/action/location'
), 'leading zero' ), 'location empty'
assert client.get()['headers']['Location'] == ''
check_error({"return": 301, "location": 0}) assert 'success' in client.conf(
check_error({"return": 301, "location": []}) '"${host}"', 'routes/0/action/location'
), 'location empty with variable'
assert (
client.get(headers={"Host": "", "Connection": "close"})['headers'][
'Location'
]
== ""
), 'location with empty variable'
def test_return_invalid():
def check_error(conf):
assert 'error' in client.conf(conf, 'routes/0/action')
check_error({"return": "200"})
check_error({"return": []})
check_error({"return": 80.1})
check_error({"return": 1000})
check_error({"return": -1})
check_error({"return": 200, "share": "/blah"})
check_error({"return": 200, "location": "$hos"})
check_error({"return": 200, "location": "$hostblah"})
assert 'error' in client.conf(
'001', 'routes/0/action/return'
), 'leading zero'
check_error({"return": 301, "location": 0})
check_error({"return": 301, "location": []})

View File

@@ -1,213 +1,223 @@
import os import os
import pytest import pytest
from unit.applications.proto import TestApplicationProto from unit.applications.proto import ApplicationProto
client = ApplicationProto()
class TestRewrite(TestApplicationProto): @pytest.fixture(autouse=True)
@pytest.fixture(autouse=True) def setup_method_fixture():
def setup_method_fixture(self): assert 'success' in client.conf(
assert 'success' in self.conf( {
{ "listeners": {"*:7080": {"pass": "routes"}},
"listeners": {"*:7080": {"pass": "routes"}}, "routes": [
"routes": [
{
"match": {"uri": "/"},
"action": {"rewrite": "/new", "pass": "routes"},
},
{"match": {"uri": "/new"}, "action": {"return": 200}},
],
"applications": {},
"settings": {"http": {"log_route": True}},
},
), 'set initial configuration'
def set_rewrite(self, rewrite, uri):
assert 'success' in self.conf(
[
{ {
"match": {"uri": "/"}, "match": {"uri": "/"},
"action": {"rewrite": rewrite, "pass": "routes"}, "action": {"rewrite": "/new", "pass": "routes"},
}, },
{"match": {"uri": uri}, "action": {"return": 200}}, {"match": {"uri": "/new"}, "action": {"return": 200}},
], ],
'routes', "applications": {},
) "settings": {"http": {"log_route": True}},
},
), 'set initial configuration'
def test_rewrite(self, findall, wait_for_record):
assert self.get()['status'] == 200
assert wait_for_record(rf'\[notice\].*"routes/1" selected') is not None
assert len(findall(rf'\[notice\].*URI rewritten to "/new"')) == 1
assert len(findall(rf'\[notice\].*URI rewritten')) == 1
self.set_rewrite("", "") def set_rewrite(rewrite, uri):
assert self.get()['status'] == 200 assert 'success' in client.conf(
[
{
"match": {"uri": "/"},
"action": {"rewrite": rewrite, "pass": "routes"},
},
{"match": {"uri": uri}, "action": {"return": 200}},
],
'routes',
)
def test_rewrite_variable(self):
self.set_rewrite("/$host", "/localhost")
assert self.get()['status'] == 200
self.set_rewrite("${uri}a", "/a") def test_rewrite(findall, wait_for_record):
assert self.get()['status'] == 200 assert client.get()['status'] == 200
assert wait_for_record(rf'\[notice\].*"routes/1" selected') is not None
assert len(findall(rf'\[notice\].*URI rewritten to "/new"')) == 1
assert len(findall(rf'\[notice\].*URI rewritten')) == 1
def test_rewrite_encoded(self): set_rewrite("", "")
assert 'success' in self.conf( assert client.get()['status'] == 200
[
{
"match": {"uri": "/f"}, def test_rewrite_variable():
"action": {"rewrite": "${request_uri}oo", "pass": "routes"}, set_rewrite("/$host", "/localhost")
assert client.get()['status'] == 200
set_rewrite("${uri}a", "/a")
assert client.get()['status'] == 200
def test_rewrite_encoded():
assert 'success' in client.conf(
[
{
"match": {"uri": "/f"},
"action": {"rewrite": "${request_uri}oo", "pass": "routes"},
},
{"match": {"uri": "/foo"}, "action": {"return": 200}},
],
'routes',
)
assert client.get(url='/%66')['status'] == 200
assert 'success' in client.conf(
[
{
"match": {"uri": "/f"},
"action": {
"rewrite": "${request_uri}o%6F",
"pass": "routes",
}, },
{"match": {"uri": "/foo"}, "action": {"return": 200}}, },
], {"match": {"uri": "/foo"}, "action": {"return": 200}},
'routes', ],
) 'routes',
assert self.get(url='/%66')['status'] == 200 )
assert client.get(url='/%66')['status'] == 200
assert 'success' in self.conf(
[
{
"match": {"uri": "/f"},
"action": {
"rewrite": "${request_uri}o%6F",
"pass": "routes",
},
},
{"match": {"uri": "/foo"}, "action": {"return": 200}},
],
'routes',
)
assert self.get(url='/%66')['status'] == 200
def test_rewrite_arguments(self): def test_rewrite_arguments():
assert 'success' in self.conf( assert 'success' in client.conf(
[ [
{ {
"match": {"uri": "/foo", "arguments": {"arg": "val"}}, "match": {"uri": "/foo", "arguments": {"arg": "val"}},
"action": {"rewrite": "/new?some", "pass": "routes"}, "action": {"rewrite": "/new?some", "pass": "routes"},
}, },
{ {
"match": {"uri": "/new", "arguments": {"arg": "val"}}, "match": {"uri": "/new", "arguments": {"arg": "val"}},
"action": {"return": 200}, "action": {"return": 200},
}, },
], ],
'routes', 'routes',
) )
assert self.get(url='/foo?arg=val')['status'] == 200 assert client.get(url='/foo?arg=val')['status'] == 200
def test_rewrite_njs(self, require):
require({'modules': {'njs': 'any'}})
self.set_rewrite("`/${host}`", "/localhost") def test_rewrite_njs(require):
assert self.get()['status'] == 200 require({'modules': {'njs': 'any'}})
def test_rewrite_location(self): set_rewrite("`/${host}`", "/localhost")
def check_location(rewrite, expect): assert client.get()['status'] == 200
assert 'success' in self.conf(
{
"listeners": {"*:7080": {"pass": "routes"}},
"routes": [
{
"action": {
"return": 301,
"location": "$uri",
"rewrite": rewrite,
}
}
],
}
)
assert self.get()['headers']['Location'] == expect
check_location('/new', '/new')
check_location('${request_uri}new', '/new')
def test_rewrite_share(self, temp_dir): def test_rewrite_location():
os.makedirs(f'{temp_dir}/dir') def check_location(rewrite, expect):
os.makedirs(f'{temp_dir}/foo') assert 'success' in client.conf(
with open(f'{temp_dir}/foo/index.html', 'w') as fooindex:
fooindex.write('fooindex')
# same action block
assert 'success' in self.conf(
{ {
"listeners": {"*:7080": {"pass": "routes"}}, "listeners": {"*:7080": {"pass": "routes"}},
"routes": [ "routes": [
{ {
"action": { "action": {
"rewrite": "${request_uri}dir", "return": 301,
"share": f'{temp_dir}$uri', "location": "$uri",
"rewrite": rewrite,
} }
} }
], ],
} }
) )
assert client.get()['headers']['Location'] == expect
resp = self.get() check_location('/new', '/new')
assert resp['status'] == 301, 'redirect status' check_location('${request_uri}new', '/new')
assert resp['headers']['Location'] == '/dir/', 'redirect Location'
# request_uri
index_path = f'{temp_dir}${{request_uri}}/index.html' def test_rewrite_share(temp_dir):
assert 'success' in self.conf( os.makedirs(f'{temp_dir}/dir')
{ os.makedirs(f'{temp_dir}/foo')
"listeners": {"*:7080": {"pass": "routes"}},
"routes": [ with open(f'{temp_dir}/foo/index.html', 'w') as fooindex:
{ fooindex.write('fooindex')
"match": {"uri": "/foo"},
"action": { # same action block
"rewrite": "${request_uri}dir",
"pass": "routes", assert 'success' in client.conf(
}, {
"listeners": {"*:7080": {"pass": "routes"}},
"routes": [
{
"action": {
"rewrite": "${request_uri}dir",
"share": f'{temp_dir}$uri',
}
}
],
}
)
resp = client.get()
assert resp['status'] == 301, 'redirect status'
assert resp['headers']['Location'] == '/dir/', 'redirect Location'
# request_uri
index_path = f'{temp_dir}${{request_uri}}/index.html'
assert 'success' in client.conf(
{
"listeners": {"*:7080": {"pass": "routes"}},
"routes": [
{
"match": {"uri": "/foo"},
"action": {
"rewrite": "${request_uri}dir",
"pass": "routes",
}, },
{"action": {"share": index_path}}, },
], {"action": {"share": index_path}},
} ],
}
)
assert client.get(url='/foo')['body'] == 'fooindex'
# different action block
assert 'success' in client.conf(
{
"listeners": {"*:7080": {"pass": "routes"}},
"routes": [
{
"match": {"uri": "/foo"},
"action": {
"rewrite": "${request_uri}dir",
"pass": "routes",
},
},
{
"action": {
"share": f'{temp_dir}/dir',
}
},
],
}
)
resp = client.get(url='/foo')
assert resp['status'] == 301, 'redirect status 2'
assert resp['headers']['Location'] == '/foodir/', 'redirect Location 2'
def test_rewrite_invalid(skip_alert):
skip_alert(r'failed to apply new conf')
def check_rewrite(rewrite):
assert 'error' in client.conf(
[
{
"match": {"uri": "/"},
"action": {"rewrite": rewrite, "pass": "routes"},
},
{"action": {"return": 200}},
],
'routes',
) )
assert self.get(url='/foo')['body'] == 'fooindex' check_rewrite("/$blah")
check_rewrite(["/"])
# different action block
assert 'success' in self.conf(
{
"listeners": {"*:7080": {"pass": "routes"}},
"routes": [
{
"match": {"uri": "/foo"},
"action": {
"rewrite": "${request_uri}dir",
"pass": "routes",
},
},
{
"action": {
"share": f'{temp_dir}/dir',
}
},
],
}
)
resp = self.get(url='/foo')
assert resp['status'] == 301, 'redirect status 2'
assert resp['headers']['Location'] == '/foodir/', 'redirect Location 2'
def test_rewrite_invalid(self, skip_alert):
skip_alert(r'failed to apply new conf')
def check_rewrite(rewrite):
assert 'error' in self.conf(
[
{
"match": {"uri": "/"},
"action": {"rewrite": rewrite, "pass": "routes"},
},
{"action": {"return": 200}},
],
'routes',
)
check_rewrite("/$blah")
check_rewrite(["/"])

File diff suppressed because it is too large Load Diff

View File

@@ -1,28 +1,29 @@
from unit.applications.tls import TestApplicationTLS from unit.applications.tls import ApplicationTLS
prerequisites = {'modules': {'openssl': 'any'}} prerequisites = {'modules': {'openssl': 'any'}}
client = ApplicationTLS()
class TestRoutingTLS(TestApplicationTLS):
def test_routes_match_scheme_tls(self):
self.certificate()
assert 'success' in self.conf( def test_routes_match_scheme_tls():
{ client.certificate()
"listeners": {
"*:7080": {"pass": "routes"}, assert 'success' in client.conf(
"*:7081": { {
"pass": "routes", "listeners": {
"tls": {"certificate": 'default'}, "*:7080": {"pass": "routes"},
}, "*:7081": {
"pass": "routes",
"tls": {"certificate": 'default'},
}, },
"routes": [ },
{"match": {"scheme": "http"}, "action": {"return": 200}}, "routes": [
{"match": {"scheme": "https"}, "action": {"return": 201}}, {"match": {"scheme": "http"}, "action": {"return": 200}},
], {"match": {"scheme": "https"}, "action": {"return": 201}},
"applications": {}, ],
} "applications": {},
), 'scheme configure' }
), 'scheme configure'
assert self.get()['status'] == 200, 'http' assert client.get()['status'] == 200, 'http'
assert self.get_ssl(port=7081)['status'] == 201, 'https' assert client.get_ssl(port=7081)['status'] == 201, 'https'

View File

@@ -2,415 +2,444 @@ import re
import subprocess import subprocess
import pytest import pytest
from unit.applications.lang.ruby import TestApplicationRuby from unit.applications.lang.ruby import ApplicationRuby
prerequisites = {'modules': {'ruby': 'all'}} prerequisites = {'modules': {'ruby': 'all'}}
client = ApplicationRuby()
class TestRubyApplication(TestApplicationRuby):
def test_ruby_application(self, date_to_sec_epoch, sec_epoch):
self.load('variables')
body = 'Test body string.' def test_ruby_application(date_to_sec_epoch, sec_epoch):
client.load('variables')
resp = self.post( body = 'Test body string.'
resp = client.post(
headers={
'Host': 'localhost',
'Content-Type': 'text/html',
'Custom-Header': 'blah',
'Connection': 'close',
},
body=body,
)
assert resp['status'] == 200, 'status'
headers = resp['headers']
header_server = headers.pop('Server')
assert re.search(r'Unit/[\d\.]+', header_server), 'server header'
assert (
headers.pop('Server-Software') == header_server
), 'server software header'
date = headers.pop('Date')
assert date[-4:] == ' GMT', 'date header timezone'
assert abs(date_to_sec_epoch(date) - sec_epoch) < 5, 'date header'
assert headers == {
'Connection': 'close',
'Content-Length': str(len(body)),
'Content-Type': 'text/html',
'Request-Method': 'POST',
'Request-Uri': '/',
'Http-Host': 'localhost',
'Script-Name': '',
'Server-Protocol': 'HTTP/1.1',
'Custom-Header': 'blah',
'Rack-Version': '13',
'Rack-Url-Scheme': 'http',
'Rack-Multithread': 'false',
'Rack-Multiprocess': 'true',
'Rack-Run-Once': 'false',
'Rack-Hijack-Q': 'false',
'Rack-Hijack': '',
'Rack-Hijack-IO': '',
}, 'headers'
assert resp['body'] == body, 'body'
def test_ruby_application_query_string():
client.load('query_string')
resp = client.get(url='/?var1=val1&var2=val2')
assert (
resp['headers']['Query-String'] == 'var1=val1&var2=val2'
), 'Query-String header'
def test_ruby_application_query_string_empty():
client.load('query_string')
resp = client.get(url='/?')
assert resp['status'] == 200, 'query string empty status'
assert resp['headers']['Query-String'] == '', 'query string empty'
def test_ruby_application_query_string_absent():
client.load('query_string')
resp = client.get()
assert resp['status'] == 200, 'query string absent status'
assert resp['headers']['Query-String'] == '', 'query string absent'
@pytest.mark.skip('not yet')
def test_ruby_application_server_port():
client.load('server_port')
assert (
client.get()['headers']['Server-Port'] == '7080'
), 'Server-Port header'
def test_ruby_application_status_int():
client.load('status_int')
assert client.get()['status'] == 200, 'status int'
def test_ruby_application_input_read_empty():
client.load('input_read_empty')
assert client.get()['body'] == '', 'read empty'
def test_ruby_application_input_read_parts():
client.load('input_read_parts')
assert (
client.post(body='0123456789')['body'] == '012345678'
), 'input read parts'
def test_ruby_application_input_read_buffer():
client.load('input_read_buffer')
assert (
client.post(body='0123456789')['body'] == '0123456789'
), 'input read buffer'
def test_ruby_application_input_read_buffer_not_empty():
client.load('input_read_buffer_not_empty')
assert (
client.post(body='0123456789')['body'] == '0123456789'
), 'input read buffer not empty'
def test_ruby_application_input_gets():
client.load('input_gets')
body = '0123456789'
assert client.post(body=body)['body'] == body, 'input gets'
def test_ruby_application_input_gets_2():
client.load('input_gets')
assert (
client.post(body='01234\n56789\n')['body'] == '01234\n'
), 'input gets 2'
def test_ruby_application_input_gets_all():
client.load('input_gets_all')
body = '\n01234\n56789\n\n'
assert client.post(body=body)['body'] == body, 'input gets all'
def test_ruby_application_input_each():
client.load('input_each')
body = '\n01234\n56789\n\n'
assert client.post(body=body)['body'] == body, 'input each'
@pytest.mark.skip('not yet')
def test_ruby_application_input_rewind():
client.load('input_rewind')
body = '0123456789'
assert client.post(body=body)['body'] == body, 'input rewind'
@pytest.mark.skip('not yet')
def test_ruby_application_syntax_error(skip_alert):
skip_alert(
r'Failed to parse rack script',
r'syntax error',
r'new_from_string',
r'parse_file',
)
client.load('syntax_error')
assert client.get()['status'] == 500, 'syntax error'
def test_ruby_application_errors_puts(wait_for_record):
client.load('errors_puts')
assert client.get()['status'] == 200
assert (
wait_for_record(r'\[error\].+Error in application') is not None
), 'errors puts'
def test_ruby_application_errors_puts_int(wait_for_record):
client.load('errors_puts_int')
assert client.get()['status'] == 200
assert (
wait_for_record(r'\[error\].+1234567890') is not None
), 'errors puts int'
def test_ruby_application_errors_write(wait_for_record):
client.load('errors_write')
assert client.get()['status'] == 200
assert (
wait_for_record(r'\[error\].+Error in application') is not None
), 'errors write'
def test_ruby_application_errors_write_to_s_custom():
client.load('errors_write_to_s_custom')
assert client.get()['status'] == 200, 'errors write to_s custom'
def test_ruby_application_errors_write_int(wait_for_record):
client.load('errors_write_int')
assert client.get()['status'] == 200
assert (
wait_for_record(r'\[error\].+1234567890') is not None
), 'errors write int'
def test_ruby_application_at_exit(wait_for_record):
client.load('at_exit')
assert client.get()['status'] == 200
assert 'success' in client.conf({"listeners": {}, "applications": {}})
assert (
wait_for_record(r'\[error\].+At exit called\.') is not None
), 'at exit'
def test_ruby_application_encoding():
client.load('encoding')
try:
locales = (
subprocess.check_output(
['locale', '-a'],
stderr=subprocess.STDOUT,
)
.decode()
.split('\n')
)
except (FileNotFoundError, subprocess.CalledProcessError):
pytest.skip('require locale')
def get_locale(pattern):
return next(
(l for l in locales if re.match(pattern, l.upper()) is not None),
None,
)
utf8 = get_locale(r'.*UTF[-_]?8')
iso88591 = get_locale(r'.*ISO[-_]?8859[-_]?1')
def check_locale(enc):
assert 'success' in client.conf(
{"LC_CTYPE": enc, "LC_ALL": ""},
'/config/applications/encoding/environment',
)
resp = client.get()
assert resp['status'] == 200, 'status'
enc_default = re.sub(r'[-_]', '', resp['headers']['X-Enc']).upper()
assert enc_default == re.sub(r'[-_]', '', enc.split('.')[-1]).upper()
if utf8:
check_locale(utf8)
if iso88591:
check_locale(iso88591)
if not utf8 and not iso88591:
pytest.skip('no available locales')
def test_ruby_application_header_custom():
client.load('header_custom')
resp = client.post(body="\ntc=one,two\ntc=three,four,\n\n")
assert resp['headers']['Custom-Header'] == [
'',
'tc=one,two',
'tc=three,four,',
'',
'',
], 'header custom'
@pytest.mark.skip('not yet')
def test_ruby_application_header_custom_non_printable():
client.load('header_custom')
assert (
client.post(body='\b')['status'] == 500
), 'header custom non printable'
def test_ruby_application_header_status():
client.load('header_status')
assert client.get()['status'] == 200, 'header status'
@pytest.mark.skip('not yet')
def test_ruby_application_header_rack():
client.load('header_rack')
assert client.get()['status'] == 500, 'header rack'
def test_ruby_application_body_empty():
client.load('body_empty')
assert client.get()['body'] == '', 'body empty'
def test_ruby_application_body_array():
client.load('body_array')
assert client.get()['body'] == '0123456789', 'body array'
def test_ruby_application_body_large():
client.load('mirror')
body = '0123456789' * 1000
assert client.post(body=body)['body'] == body, 'body large'
@pytest.mark.skip('not yet')
def test_ruby_application_body_each_error(wait_for_record):
client.load('body_each_error')
assert client.get()['status'] == 500, 'body each error status'
assert (
wait_for_record(r'\[error\].+Failed to run ruby script') is not None
), 'body each error'
def test_ruby_application_body_file():
client.load('body_file')
assert client.get()['body'] == 'body\n', 'body file'
def test_ruby_keepalive_body():
client.load('mirror')
assert client.get()['status'] == 200, 'init'
body = '0123456789' * 500
(resp, sock) = client.post(
headers={
'Host': 'localhost',
'Connection': 'keep-alive',
},
start=True,
body=body,
read_timeout=1,
)
assert resp['body'] == body, 'keep-alive 1'
body = '0123456789'
resp = client.post(sock=sock, body=body)
assert resp['body'] == body, 'keep-alive 2'
def test_ruby_application_constants():
client.load('constants')
resp = client.get()
assert resp['status'] == 200, 'status'
headers = resp['headers']
assert len(headers['X-Copyright']) > 0, 'RUBY_COPYRIGHT'
assert len(headers['X-Description']) > 0, 'RUBY_DESCRIPTION'
assert len(headers['X-Engine']) > 0, 'RUBY_ENGINE'
assert len(headers['X-Engine-Version']) > 0, 'RUBY_ENGINE_VERSION'
assert len(headers['X-Patchlevel']) > 0, 'RUBY_PATCHLEVEL'
assert len(headers['X-Platform']) > 0, 'RUBY_PLATFORM'
assert len(headers['X-Release-Date']) > 0, 'RUBY_RELEASE_DATE'
assert len(headers['X-Revision']) > 0, 'RUBY_REVISION'
assert len(headers['X-Version']) > 0, 'RUBY_VERSION'
def test_ruby_application_threads():
client.load('threads')
assert 'success' in client.conf(
'4', 'applications/threads/threads'
), 'configure 4 threads'
socks = []
for _ in range(4):
sock = client.get(
headers={ headers={
'Host': 'localhost', 'Host': 'localhost',
'Content-Type': 'text/html', 'X-Delay': '2',
'Custom-Header': 'blah',
'Connection': 'close', 'Connection': 'close',
}, },
body=body, no_recv=True,
) )
assert resp['status'] == 200, 'status' socks.append(sock)
headers = resp['headers']
header_server = headers.pop('Server')
assert re.search(r'Unit/[\d\.]+', header_server), 'server header'
assert (
headers.pop('Server-Software') == header_server
), 'server software header'
date = headers.pop('Date') threads = set()
assert date[-4:] == ' GMT', 'date header timezone'
assert abs(date_to_sec_epoch(date) - sec_epoch) < 5, 'date header'
assert headers == { for sock in socks:
'Connection': 'close', resp = client.recvall(sock).decode('utf-8')
'Content-Length': str(len(body)),
'Content-Type': 'text/html',
'Request-Method': 'POST',
'Request-Uri': '/',
'Http-Host': 'localhost',
'Script-Name': '',
'Server-Protocol': 'HTTP/1.1',
'Custom-Header': 'blah',
'Rack-Version': '13',
'Rack-Url-Scheme': 'http',
'Rack-Multithread': 'false',
'Rack-Multiprocess': 'true',
'Rack-Run-Once': 'false',
'Rack-Hijack-Q': 'false',
'Rack-Hijack': '',
'Rack-Hijack-IO': '',
}, 'headers'
assert resp['body'] == body, 'body'
def test_ruby_application_query_string(self): client.log_in(resp)
self.load('query_string')
resp = self.get(url='/?var1=val1&var2=val2') resp = client._resp_to_dict(resp)
assert (
resp['headers']['Query-String'] == 'var1=val1&var2=val2'
), 'Query-String header'
def test_ruby_application_query_string_empty(self):
self.load('query_string')
resp = self.get(url='/?')
assert resp['status'] == 200, 'query string empty status'
assert resp['headers']['Query-String'] == '', 'query string empty'
def test_ruby_application_query_string_absent(self):
self.load('query_string')
resp = self.get()
assert resp['status'] == 200, 'query string absent status'
assert resp['headers']['Query-String'] == '', 'query string absent'
@pytest.mark.skip('not yet')
def test_ruby_application_server_port(self):
self.load('server_port')
assert (
self.get()['headers']['Server-Port'] == '7080'
), 'Server-Port header'
def test_ruby_application_status_int(self):
self.load('status_int')
assert self.get()['status'] == 200, 'status int'
def test_ruby_application_input_read_empty(self):
self.load('input_read_empty')
assert self.get()['body'] == '', 'read empty'
def test_ruby_application_input_read_parts(self):
self.load('input_read_parts')
assert (
self.post(body='0123456789')['body'] == '012345678'
), 'input read parts'
def test_ruby_application_input_read_buffer(self):
self.load('input_read_buffer')
assert (
self.post(body='0123456789')['body'] == '0123456789'
), 'input read buffer'
def test_ruby_application_input_read_buffer_not_empty(self):
self.load('input_read_buffer_not_empty')
assert (
self.post(body='0123456789')['body'] == '0123456789'
), 'input read buffer not empty'
def test_ruby_application_input_gets(self):
self.load('input_gets')
body = '0123456789'
assert self.post(body=body)['body'] == body, 'input gets'
def test_ruby_application_input_gets_2(self):
self.load('input_gets')
assert (
self.post(body='01234\n56789\n')['body'] == '01234\n'
), 'input gets 2'
def test_ruby_application_input_gets_all(self):
self.load('input_gets_all')
body = '\n01234\n56789\n\n'
assert self.post(body=body)['body'] == body, 'input gets all'
def test_ruby_application_input_each(self):
self.load('input_each')
body = '\n01234\n56789\n\n'
assert self.post(body=body)['body'] == body, 'input each'
@pytest.mark.skip('not yet')
def test_ruby_application_input_rewind(self):
self.load('input_rewind')
body = '0123456789'
assert self.post(body=body)['body'] == body, 'input rewind'
@pytest.mark.skip('not yet')
def test_ruby_application_syntax_error(self, skip_alert):
skip_alert(
r'Failed to parse rack script',
r'syntax error',
r'new_from_string',
r'parse_file',
)
self.load('syntax_error')
assert self.get()['status'] == 500, 'syntax error'
def test_ruby_application_errors_puts(self, wait_for_record):
self.load('errors_puts')
assert self.get()['status'] == 200
assert (
wait_for_record(r'\[error\].+Error in application') is not None
), 'errors puts'
def test_ruby_application_errors_puts_int(self, wait_for_record):
self.load('errors_puts_int')
assert self.get()['status'] == 200
assert (
wait_for_record(r'\[error\].+1234567890') is not None
), 'errors puts int'
def test_ruby_application_errors_write(self, wait_for_record):
self.load('errors_write')
assert self.get()['status'] == 200
assert (
wait_for_record(r'\[error\].+Error in application') is not None
), 'errors write'
def test_ruby_application_errors_write_to_s_custom(self):
self.load('errors_write_to_s_custom')
assert self.get()['status'] == 200, 'errors write to_s custom'
def test_ruby_application_errors_write_int(self, wait_for_record):
self.load('errors_write_int')
assert self.get()['status'] == 200
assert (
wait_for_record(r'\[error\].+1234567890') is not None
), 'errors write int'
def test_ruby_application_at_exit(self, wait_for_record):
self.load('at_exit')
assert self.get()['status'] == 200
assert 'success' in self.conf({"listeners": {}, "applications": {}})
assert (
wait_for_record(r'\[error\].+At exit called\.') is not None
), 'at exit'
def test_ruby_application_encoding(self):
self.load('encoding')
try:
locales = (
subprocess.check_output(
['locale', '-a'],
stderr=subprocess.STDOUT,
)
.decode()
.split('\n')
)
except (FileNotFoundError, subprocess.CalledProcessError):
pytest.skip('require locale')
def get_locale(pattern):
return next(
(
l
for l in locales
if re.match(pattern, l.upper()) is not None
),
None,
)
utf8 = get_locale(r'.*UTF[-_]?8')
iso88591 = get_locale(r'.*ISO[-_]?8859[-_]?1')
def check_locale(enc):
assert 'success' in self.conf(
{"LC_CTYPE": enc, "LC_ALL": ""},
'/config/applications/encoding/environment',
)
resp = self.get()
assert resp['status'] == 200, 'status'
enc_default = re.sub(r'[-_]', '', resp['headers']['X-Enc']).upper()
assert (
enc_default == re.sub(r'[-_]', '', enc.split('.')[-1]).upper()
)
if utf8:
check_locale(utf8)
if iso88591:
check_locale(iso88591)
if not utf8 and not iso88591:
pytest.skip('no available locales')
def test_ruby_application_header_custom(self):
self.load('header_custom')
resp = self.post(body="\ntc=one,two\ntc=three,four,\n\n")
assert resp['headers']['Custom-Header'] == [
'',
'tc=one,two',
'tc=three,four,',
'',
'',
], 'header custom'
@pytest.mark.skip('not yet')
def test_ruby_application_header_custom_non_printable(self):
self.load('header_custom')
assert (
self.post(body='\b')['status'] == 500
), 'header custom non printable'
def test_ruby_application_header_status(self):
self.load('header_status')
assert self.get()['status'] == 200, 'header status'
@pytest.mark.skip('not yet')
def test_ruby_application_header_rack(self):
self.load('header_rack')
assert self.get()['status'] == 500, 'header rack'
def test_ruby_application_body_empty(self):
self.load('body_empty')
assert self.get()['body'] == '', 'body empty'
def test_ruby_application_body_array(self):
self.load('body_array')
assert self.get()['body'] == '0123456789', 'body array'
def test_ruby_application_body_large(self):
self.load('mirror')
body = '0123456789' * 1000
assert self.post(body=body)['body'] == body, 'body large'
@pytest.mark.skip('not yet')
def test_ruby_application_body_each_error(self, wait_for_record):
self.load('body_each_error')
assert self.get()['status'] == 500, 'body each error status'
assert (
wait_for_record(r'\[error\].+Failed to run ruby script') is not None
), 'body each error'
def test_ruby_application_body_file(self):
self.load('body_file')
assert self.get()['body'] == 'body\n', 'body file'
def test_ruby_keepalive_body(self):
self.load('mirror')
assert self.get()['status'] == 200, 'init'
body = '0123456789' * 500
(resp, sock) = self.post(
headers={
'Host': 'localhost',
'Connection': 'keep-alive',
},
start=True,
body=body,
read_timeout=1,
)
assert resp['body'] == body, 'keep-alive 1'
body = '0123456789'
resp = self.post(sock=sock, body=body)
assert resp['body'] == body, 'keep-alive 2'
def test_ruby_application_constants(self):
self.load('constants')
resp = self.get()
assert resp['status'] == 200, 'status' assert resp['status'] == 200, 'status'
headers = resp['headers'] threads.add(resp['headers']['X-Thread'])
assert len(headers['X-Copyright']) > 0, 'RUBY_COPYRIGHT'
assert len(headers['X-Description']) > 0, 'RUBY_DESCRIPTION'
assert len(headers['X-Engine']) > 0, 'RUBY_ENGINE'
assert len(headers['X-Engine-Version']) > 0, 'RUBY_ENGINE_VERSION'
assert len(headers['X-Patchlevel']) > 0, 'RUBY_PATCHLEVEL'
assert len(headers['X-Platform']) > 0, 'RUBY_PLATFORM'
assert len(headers['X-Release-Date']) > 0, 'RUBY_RELEASE_DATE'
assert len(headers['X-Revision']) > 0, 'RUBY_REVISION'
assert len(headers['X-Version']) > 0, 'RUBY_VERSION'
def test_ruby_application_threads(self): assert resp['headers']['Rack-Multithread'] == 'true', 'multithread'
self.load('threads')
assert 'success' in self.conf( sock.close()
'4', 'applications/threads/threads'
), 'configure 4 threads'
socks = [] assert len(socks) == len(threads), 'threads differs'
for _ in range(4):
sock = self.get(
headers={
'Host': 'localhost',
'X-Delay': '2',
'Connection': 'close',
},
no_recv=True,
)
socks.append(sock)
threads = set()
for sock in socks:
resp = self.recvall(sock).decode('utf-8')
self.log_in(resp)
resp = self._resp_to_dict(resp)
assert resp['status'] == 200, 'status'
threads.add(resp['headers']['X-Thread'])
assert resp['headers']['Rack-Multithread'] == 'true', 'multithread'
sock.close()
assert len(socks) == len(threads), 'threads differs'

View File

@@ -1,94 +1,99 @@
from unit.applications.lang.ruby import TestApplicationRuby from unit.applications.lang.ruby import ApplicationRuby
from unit.option import option from unit.option import option
from unit.utils import waitforglob from unit.utils import waitforglob
prerequisites = {'modules': {'ruby': 'all'}} prerequisites = {'modules': {'ruby': 'all'}}
client = ApplicationRuby()
class TestRubyHooks(TestApplicationRuby):
def _wait_cookie(self, pattern, count):
return waitforglob(
f'{option.temp_dir}/ruby/hooks/cookie_{pattern}', count
)
def test_ruby_hooks_eval(self): def wait_cookie(pattern, count):
processes = 2 return waitforglob(f'{option.temp_dir}/ruby/hooks/cookie_{pattern}', count)
self.load('hooks', processes=processes, hooks='eval.rb')
hooked = self._wait_cookie('eval.*', processes) def test_ruby_hooks_eval():
processes = 2
assert hooked, 'hooks evaluated' client.load('hooks', processes=processes, hooks='eval.rb')
def test_ruby_hooks_on_worker_boot(self): hooked = wait_cookie('eval.*', processes)
processes = 2
self.load('hooks', processes=processes, hooks='on_worker_boot.rb') assert hooked, 'hooks evaluated'
hooked = self._wait_cookie('worker_boot.*', processes)
assert hooked, 'on_worker_boot called' def test_ruby_hooks_on_worker_boot():
processes = 2
def test_ruby_hooks_on_worker_shutdown(self): client.load('hooks', processes=processes, hooks='on_worker_boot.rb')
processes = 2
self.load('hooks', processes=processes, hooks='on_worker_shutdown.rb') hooked = wait_cookie('worker_boot.*', processes)
assert self.get()['status'] == 200, 'app response' assert hooked, 'on_worker_boot called'
self.load('empty')
hooked = self._wait_cookie('worker_shutdown.*', processes) def test_ruby_hooks_on_worker_shutdown():
processes = 2
assert hooked, 'on_worker_shutdown called' client.load('hooks', processes=processes, hooks='on_worker_shutdown.rb')
def test_ruby_hooks_on_thread_boot(self): assert client.get()['status'] == 200, 'app response'
processes = 1
threads = 2
self.load( client.load('empty')
'hooks',
processes=processes,
threads=threads,
hooks='on_thread_boot.rb',
)
hooked = self._wait_cookie('thread_boot.*', processes * threads) hooked = wait_cookie('worker_shutdown.*', processes)
assert hooked, 'on_thread_boot called' assert hooked, 'on_worker_shutdown called'
def test_ruby_hooks_on_thread_shutdown(self):
processes = 1
threads = 2
self.load( def test_ruby_hooks_on_thread_boot():
'hooks', processes = 1
processes=processes, threads = 2
threads=threads,
hooks='on_thread_shutdown.rb',
)
assert self.get()['status'] == 200, 'app response' client.load(
'hooks',
processes=processes,
threads=threads,
hooks='on_thread_boot.rb',
)
self.load('empty') hooked = wait_cookie('thread_boot.*', processes * threads)
hooked = self._wait_cookie('thread_shutdown.*', processes * threads) assert hooked, 'on_thread_boot called'
assert hooked, 'on_thread_shutdown called'
def test_ruby_hooks_multiple(self): def test_ruby_hooks_on_thread_shutdown():
processes = 1 processes = 1
threads = 1 threads = 2
self.load( client.load(
'hooks', 'hooks',
processes=processes, processes=processes,
threads=threads, threads=threads,
hooks='multiple.rb', hooks='on_thread_shutdown.rb',
) )
hooked = self._wait_cookie('worker_boot.*', processes) assert client.get()['status'] == 200, 'app response'
assert hooked, 'on_worker_boot called'
hooked = self._wait_cookie('thread_boot.*', threads) client.load('empty')
assert hooked, 'on_thread_boot called'
hooked = wait_cookie('thread_shutdown.*', processes * threads)
assert hooked, 'on_thread_shutdown called'
def test_ruby_hooks_multiple():
processes = 1
threads = 1
client.load(
'hooks',
processes=processes,
threads=threads,
hooks='multiple.rb',
)
hooked = wait_cookie('worker_boot.*', processes)
assert hooked, 'on_worker_boot called'
hooked = wait_cookie('thread_boot.*', threads)
assert hooked, 'on_thread_boot called'

View File

@@ -1,42 +1,43 @@
from unit.applications.lang.ruby import TestApplicationRuby from unit.applications.lang.ruby import ApplicationRuby
prerequisites = {'modules': {'ruby': 'any'}, 'features': {'isolation': True}} prerequisites = {'modules': {'ruby': 'any'}, 'features': {'isolation': True}}
client = ApplicationRuby()
class TestRubyIsolation(TestApplicationRuby):
def test_ruby_isolation_rootfs(self, is_su, require, temp_dir):
isolation = {'rootfs': temp_dir}
if not is_su: def test_ruby_isolation_rootfs(is_su, require, temp_dir):
require( isolation = {'rootfs': temp_dir}
{
'features': { if not is_su:
'isolation': [ require(
'unprivileged_userns_clone', {
'user', 'features': {
'mnt', 'isolation': [
'pid', 'unprivileged_userns_clone',
] 'user',
} 'mnt',
'pid',
]
} }
)
isolation['namespaces'] = {
'mount': True,
'credential': True,
'pid': True,
} }
self.load('status_int', isolation=isolation)
assert 'success' in self.conf(
'"/ruby/status_int/config.ru"',
'applications/status_int/script',
) )
assert 'success' in self.conf( isolation['namespaces'] = {
'"/ruby/status_int"', 'mount': True,
'applications/status_int/working_directory', 'credential': True,
) 'pid': True,
}
assert self.get()['status'] == 200, 'status int' client.load('status_int', isolation=isolation)
assert 'success' in client.conf(
'"/ruby/status_int/config.ru"',
'applications/status_int/script',
)
assert 'success' in client.conf(
'"/ruby/status_int"',
'applications/status_int/working_directory',
)
assert client.get()['status'] == 200, 'status int'

File diff suppressed because it is too large Load Diff

View File

@@ -2,347 +2,361 @@ import os
import socket import socket
import pytest import pytest
from unit.applications.proto import TestApplicationProto from unit.applications.proto import ApplicationProto
from unit.utils import waitforfiles from unit.utils import waitforfiles
class TestStatic(TestApplicationProto): client = ApplicationProto()
@pytest.fixture(autouse=True)
def setup_method_fixture(self, temp_dir):
os.makedirs(f'{temp_dir}/assets/dir')
assets_dir = f'{temp_dir}/assets'
with open(f'{assets_dir}/index.html', 'w') as index, open(
f'{assets_dir}/README', 'w'
) as readme, open(f'{assets_dir}/log.log', 'w') as log, open(
f'{assets_dir}/dir/file', 'w'
) as file:
index.write('0123456789')
readme.write('readme')
log.write('[debug]')
file.write('blah')
self._load_conf( @pytest.fixture(autouse=True)
{ def setup_method_fixture(temp_dir):
"listeners": {"*:7080": {"pass": "routes"}}, os.makedirs(f'{temp_dir}/assets/dir')
"routes": [{"action": {"share": f'{assets_dir}$uri'}}], assets_dir = f'{temp_dir}/assets'
"settings": {
"http": { with open(f'{assets_dir}/index.html', 'w') as index, open(
"static": { f'{assets_dir}/README', 'w'
"mime_types": {"text/plain": [".log", "README"]} ) as readme, open(f'{assets_dir}/log.log', 'w') as log, open(
} f'{assets_dir}/dir/file', 'w'
} ) as file:
}, index.write('0123456789')
} readme.write('readme')
log.write('[debug]')
file.write('blah')
assert 'success' in client.conf(
{
"listeners": {"*:7080": {"pass": "routes"}},
"routes": [{"action": {"share": f'{assets_dir}$uri'}}],
"settings": {
"http": {
"static": {"mime_types": {"text/plain": [".log", "README"]}}
}
},
}
)
def test_static_index(temp_dir):
def set_index(index):
assert 'success' in client.conf(
{"share": f'{temp_dir}/assets$uri', "index": index},
'routes/0/action',
), 'configure index'
set_index('README')
assert client.get()['body'] == 'readme', 'index'
client.conf_delete('routes/0/action/index')
assert client.get()['body'] == '0123456789', 'delete index'
set_index('')
assert client.get()['status'] == 404, 'index empty'
def test_static_index_default():
assert client.get(url='/index.html')['body'] == '0123456789', 'index'
assert client.get(url='/')['body'] == '0123456789', 'index 2'
assert client.get(url='//')['body'] == '0123456789', 'index 3'
assert client.get(url='/.')['body'] == '0123456789', 'index 4'
assert client.get(url='/./')['body'] == '0123456789', 'index 5'
assert client.get(url='/?blah')['body'] == '0123456789', 'index vars'
assert client.get(url='/#blah')['body'] == '0123456789', 'index anchor'
assert client.get(url='/dir/')['status'] == 404, 'index not found'
resp = client.get(url='/index.html/')
assert resp['status'] == 404, 'index not found 2 status'
assert (
resp['headers']['Content-Type'] == 'text/html'
), 'index not found 2 Content-Type'
def test_static_index_invalid(skip_alert, temp_dir):
skip_alert(r'failed to apply new conf')
def check_index(index):
assert 'error' in client.conf(
{"share": f'{temp_dir}/assets$uri', "index": index},
'routes/0/action',
) )
def test_static_index(self, temp_dir): check_index({})
def set_index(index): check_index(['index.html', '$blah'])
assert 'success' in self.conf(
{"share": f'{temp_dir}/assets$uri', "index": index},
'routes/0/action',
), 'configure index'
set_index('README')
assert self.get()['body'] == 'readme', 'index'
self.conf_delete('routes/0/action/index') def test_static_large_file(temp_dir):
assert self.get()['body'] == '0123456789', 'delete index' file_size = 32 * 1024 * 1024
with open(f'{temp_dir}/assets/large', 'wb') as f:
f.seek(file_size - 1)
f.write(b'\0')
set_index('') assert (
assert self.get()['status'] == 404, 'index empty' len(client.get(url='/large', read_buffer_size=1024 * 1024)['body'])
== file_size
), 'large file'
def test_static_index_default(self):
assert self.get(url='/index.html')['body'] == '0123456789', 'index'
assert self.get(url='/')['body'] == '0123456789', 'index 2'
assert self.get(url='//')['body'] == '0123456789', 'index 3'
assert self.get(url='/.')['body'] == '0123456789', 'index 4'
assert self.get(url='/./')['body'] == '0123456789', 'index 5'
assert self.get(url='/?blah')['body'] == '0123456789', 'index vars'
assert self.get(url='/#blah')['body'] == '0123456789', 'index anchor'
assert self.get(url='/dir/')['status'] == 404, 'index not found'
resp = self.get(url='/index.html/') def test_static_etag(temp_dir):
assert resp['status'] == 404, 'index not found 2 status' etag = client.get(url='/')['headers']['ETag']
assert ( etag_2 = client.get(url='/README')['headers']['ETag']
resp['headers']['Content-Type'] == 'text/html'
), 'index not found 2 Content-Type'
def test_static_index_invalid(self, skip_alert, temp_dir): assert etag != etag_2, 'different ETag'
skip_alert(r'failed to apply new conf') assert etag == client.get(url='/')['headers']['ETag'], 'same ETag'
def check_index(index): with open(f'{temp_dir}/assets/index.html', 'w') as f:
assert 'error' in self.conf( f.write('blah')
{"share": f'{temp_dir}/assets$uri', "index": index},
'routes/0/action',
)
check_index({}) assert etag != client.get(url='/')['headers']['ETag'], 'new ETag'
check_index(['index.html', '$blah'])
def test_static_large_file(self, temp_dir):
file_size = 32 * 1024 * 1024
with open(f'{temp_dir}/assets/large', 'wb') as f:
f.seek(file_size - 1)
f.write(b'\0')
assert ( def test_static_redirect():
len(self.get(url='/large', read_buffer_size=1024 * 1024)['body']) resp = client.get(url='/dir')
== file_size assert resp['status'] == 301, 'redirect status'
), 'large file' assert resp['headers']['Location'] == '/dir/', 'redirect Location'
assert 'Content-Type' not in resp['headers'], 'redirect Content-Type'
def test_static_etag(self, temp_dir):
etag = self.get(url='/')['headers']['ETag']
etag_2 = self.get(url='/README')['headers']['ETag']
assert etag != etag_2, 'different ETag' def test_static_space_in_name(temp_dir):
assert etag == self.get(url='/')['headers']['ETag'], 'same ETag' assets_dir = f'{temp_dir}/assets'
with open(f'{temp_dir}/assets/index.html', 'w') as f: os.rename(
f.write('blah') f'{assets_dir}/dir/file',
f'{assets_dir}/dir/fi le',
)
assert waitforfiles(f'{assets_dir}/dir/fi le')
assert client.get(url='/dir/fi le')['body'] == 'blah', 'file name'
assert etag != self.get(url='/')['headers']['ETag'], 'new ETag' os.rename(f'{assets_dir}/dir', f'{assets_dir}/di r')
assert waitforfiles(f'{assets_dir}/di r/fi le')
assert client.get(url='/di r/fi le')['body'] == 'blah', 'dir name'
def test_static_redirect(self): os.rename(f'{assets_dir}/di r', f'{assets_dir}/ di r ')
resp = self.get(url='/dir') assert waitforfiles(f'{assets_dir}/ di r /fi le')
assert resp['status'] == 301, 'redirect status' assert (
assert resp['headers']['Location'] == '/dir/', 'redirect Location' client.get(url='/ di r /fi le')['body'] == 'blah'
assert 'Content-Type' not in resp['headers'], 'redirect Content-Type' ), 'dir name enclosing'
def test_static_space_in_name(self, temp_dir): assert (
assets_dir = f'{temp_dir}/assets' client.get(url='/%20di%20r%20/fi le')['body'] == 'blah'
), 'dir encoded'
assert client.get(url='/ di r %2Ffi le')['body'] == 'blah', 'slash encoded'
assert client.get(url='/ di r /fi%20le')['body'] == 'blah', 'file encoded'
assert (
client.get(url='/%20di%20r%20%2Ffi%20le')['body'] == 'blah'
), 'encoded'
assert (
client.get(url='/%20%64%69%20%72%20%2F%66%69%20%6C%65')['body']
== 'blah'
), 'encoded 2'
os.rename(
f'{assets_dir}/ di r /fi le',
f'{assets_dir}/ di r / fi le ',
)
assert waitforfiles(f'{assets_dir}/ di r / fi le ')
assert (
client.get(url='/%20di%20r%20/%20fi%20le%20')['body'] == 'blah'
), 'file name enclosing'
try:
open(f'{temp_dir}а', 'a').close()
utf8 = True
except KeyboardInterrupt:
raise
except:
utf8 = False
if utf8:
os.rename( os.rename(
f'{assets_dir}/dir/file',
f'{assets_dir}/dir/fi le',
)
assert waitforfiles(f'{assets_dir}/dir/fi le')
assert self.get(url='/dir/fi le')['body'] == 'blah', 'file name'
os.rename(f'{assets_dir}/dir', f'{assets_dir}/di r')
assert waitforfiles(f'{assets_dir}/di r/fi le')
assert self.get(url='/di r/fi le')['body'] == 'blah', 'dir name'
os.rename(f'{assets_dir}/di r', f'{assets_dir}/ di r ')
assert waitforfiles(f'{assets_dir}/ di r /fi le')
assert (
self.get(url='/ di r /fi le')['body'] == 'blah'
), 'dir name enclosing'
assert (
self.get(url='/%20di%20r%20/fi le')['body'] == 'blah'
), 'dir encoded'
assert (
self.get(url='/ di r %2Ffi le')['body'] == 'blah'
), 'slash encoded'
assert self.get(url='/ di r /fi%20le')['body'] == 'blah', 'file encoded'
assert (
self.get(url='/%20di%20r%20%2Ffi%20le')['body'] == 'blah'
), 'encoded'
assert (
self.get(url='/%20%64%69%20%72%20%2F%66%69%20%6C%65')['body']
== 'blah'
), 'encoded 2'
os.rename(
f'{assets_dir}/ di r /fi le',
f'{assets_dir}/ di r / fi le ', f'{assets_dir}/ di r / fi le ',
f'{assets_dir}/ di r /фа йл',
) )
assert waitforfiles(f'{assets_dir}/ di r / fi le ') assert waitforfiles(f'{assets_dir}/ di r /фа йл')
assert client.get(url='/ di r /фа йл')['body'] == 'blah'
os.rename(
f'{assets_dir}/ di r ',
f'{assets_dir}/ди ректория',
)
assert waitforfiles(f'{assets_dir}/ди ректория/фа йл')
assert ( assert (
self.get(url='/%20di%20r%20/%20fi%20le%20')['body'] == 'blah' client.get(url='/ди ректория/фа йл')['body'] == 'blah'
), 'file name enclosing' ), 'dir name 2'
try:
open(f'{temp_dir}а', 'a').close()
utf8 = True
except KeyboardInterrupt: def test_static_unix_socket(temp_dir):
raise sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
sock.bind(f'{temp_dir}/assets/unix_socket')
except: assert client.get(url='/unix_socket')['status'] == 404, 'socket'
utf8 = False
if utf8: sock.close()
os.rename(
f'{assets_dir}/ di r / fi le ',
f'{assets_dir}/ di r /фа йл',
)
assert waitforfiles(f'{assets_dir}/ di r /фа йл')
assert (
self.get(url='/ di r /фа йл')['body'] == 'blah'
), 'file name 2'
os.rename(
f'{assets_dir}/ di r ',
f'{assets_dir}/ди ректория',
)
assert waitforfiles(f'{assets_dir}/ди ректория/фа йл')
assert (
self.get(url='/ди ректория/фа йл')['body'] == 'blah'
), 'dir name 2'
def test_static_unix_socket(self, temp_dir): def test_static_unix_fifo(temp_dir):
sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) os.mkfifo(f'{temp_dir}/assets/fifo')
sock.bind(f'{temp_dir}/assets/unix_socket')
assert self.get(url='/unix_socket')['status'] == 404, 'socket' assert client.get(url='/fifo')['status'] == 404, 'fifo'
sock.close()
def test_static_unix_fifo(self, temp_dir): def test_static_method():
os.mkfifo(f'{temp_dir}/assets/fifo') resp = client.head()
assert resp['status'] == 200, 'HEAD status'
assert resp['body'] == '', 'HEAD empty body'
assert self.get(url='/fifo')['status'] == 404, 'fifo' assert client.delete()['status'] == 405, 'DELETE'
assert client.post()['status'] == 405, 'POST'
assert client.put()['status'] == 405, 'PUT'
def test_static_method(self):
resp = self.head()
assert resp['status'] == 200, 'HEAD status'
assert resp['body'] == '', 'HEAD empty body'
assert self.delete()['status'] == 405, 'DELETE' def test_static_path():
assert self.post()['status'] == 405, 'POST' assert client.get(url='/dir/../dir/file')['status'] == 200, 'relative'
assert self.put()['status'] == 405, 'PUT'
def test_static_path(self): assert client.get(url='./')['status'] == 400, 'path invalid'
assert self.get(url='/dir/../dir/file')['status'] == 200, 'relative' assert client.get(url='../')['status'] == 400, 'path invalid 2'
assert client.get(url='/..')['status'] == 400, 'path invalid 3'
assert client.get(url='../assets/')['status'] == 400, 'path invalid 4'
assert client.get(url='/../assets/')['status'] == 400, 'path invalid 5'
assert self.get(url='./')['status'] == 400, 'path invalid'
assert self.get(url='../')['status'] == 400, 'path invalid 2'
assert self.get(url='/..')['status'] == 400, 'path invalid 3'
assert self.get(url='../assets/')['status'] == 400, 'path invalid 4'
assert self.get(url='/../assets/')['status'] == 400, 'path invalid 5'
def test_static_two_clients(self): def test_static_two_clients():
sock = self.get(no_recv=True) sock = client.get(no_recv=True)
sock2 = self.get(no_recv=True) sock2 = client.get(no_recv=True)
assert sock.recv(1) == b'H', 'client 1' assert sock.recv(1) == b'H', 'client 1'
assert sock2.recv(1) == b'H', 'client 2' assert sock2.recv(1) == b'H', 'client 2'
assert sock.recv(1) == b'T', 'client 1 again' assert sock.recv(1) == b'T', 'client 1 again'
assert sock2.recv(1) == b'T', 'client 2 again' assert sock2.recv(1) == b'T', 'client 2 again'
sock.close() sock.close()
sock2.close() sock2.close()
def test_static_mime_types(self):
assert 'success' in self.conf(
{
"text/x-code/x-blah/x-blah": "readme",
"text/plain": [".html", ".log", "file"],
},
'settings/http/static/mime_types',
), 'configure mime_types'
assert ( def test_static_mime_types():
self.get(url='/README')['headers']['Content-Type'] assert 'success' in client.conf(
== 'text/x-code/x-blah/x-blah' {
), 'mime_types string case insensitive' "text/x-code/x-blah/x-blah": "readme",
assert ( "text/plain": [".html", ".log", "file"],
self.get(url='/index.html')['headers']['Content-Type'] },
== 'text/plain' 'settings/http/static/mime_types',
), 'mime_types html' ), 'configure mime_types'
assert (
self.get(url='/')['headers']['Content-Type'] == 'text/plain'
), 'mime_types index default'
assert (
self.get(url='/dir/file')['headers']['Content-Type'] == 'text/plain'
), 'mime_types file in dir'
def test_static_mime_types_partial_match(self): assert (
assert 'success' in self.conf( client.get(url='/README')['headers']['Content-Type']
{ == 'text/x-code/x-blah/x-blah'
"text/x-blah": ["ile", "fil", "f", "e", ".file"], ), 'mime_types string case insensitive'
}, assert (
'settings/http/static/mime_types', client.get(url='/index.html')['headers']['Content-Type'] == 'text/plain'
), 'configure mime_types' ), 'mime_types html'
assert 'Content-Type' not in self.get(url='/dir/file'), 'partial match' assert (
client.get(url='/')['headers']['Content-Type'] == 'text/plain'
), 'mime_types index default'
assert (
client.get(url='/dir/file')['headers']['Content-Type'] == 'text/plain'
), 'mime_types file in dir'
def test_static_mime_types_reconfigure(self):
assert 'success' in self.conf(
{
"text/x-code": "readme",
"text/plain": [".html", ".log", "file"],
},
'settings/http/static/mime_types',
), 'configure mime_types'
assert self.conf_get('settings/http/static/mime_types') == { def test_static_mime_types_partial_match():
'text/x-code': 'readme', assert 'success' in client.conf(
'text/plain': ['.html', '.log', 'file'], {
}, 'mime_types get' "text/x-blah": ["ile", "fil", "f", "e", ".file"],
assert ( },
self.conf_get('settings/http/static/mime_types/text%2Fx-code') 'settings/http/static/mime_types',
== 'readme' ), 'configure mime_types'
), 'mime_types get string' assert 'Content-Type' not in client.get(url='/dir/file'), 'partial match'
assert self.conf_get(
'settings/http/static/mime_types/text%2Fplain'
) == ['.html', '.log', 'file'], 'mime_types get array'
assert (
self.conf_get('settings/http/static/mime_types/text%2Fplain/1')
== '.log'
), 'mime_types get array element'
assert 'success' in self.conf_delete(
'settings/http/static/mime_types/text%2Fplain/2'
), 'mime_types remove array element'
assert (
'Content-Type' not in self.get(url='/dir/file')['headers']
), 'mime_types removed'
assert 'success' in self.conf_post( def test_static_mime_types_reconfigure():
'"file"', 'settings/http/static/mime_types/text%2Fplain' assert 'success' in client.conf(
), 'mime_types add array element' {
assert ( "text/x-code": "readme",
self.get(url='/dir/file')['headers']['Content-Type'] == 'text/plain' "text/plain": [".html", ".log", "file"],
), 'mime_types reverted' },
'settings/http/static/mime_types',
), 'configure mime_types'
assert 'success' in self.conf( assert client.conf_get('settings/http/static/mime_types') == {
'"file"', 'settings/http/static/mime_types/text%2Fplain' 'text/x-code': 'readme',
), 'configure mime_types update' 'text/plain': ['.html', '.log', 'file'],
assert ( }, 'mime_types get'
self.get(url='/dir/file')['headers']['Content-Type'] == 'text/plain' assert (
), 'mime_types updated' client.conf_get('settings/http/static/mime_types/text%2Fx-code')
assert ( == 'readme'
'Content-Type' not in self.get(url='/log.log')['headers'] ), 'mime_types get string'
), 'mime_types updated 2' assert client.conf_get('settings/http/static/mime_types/text%2Fplain') == [
'.html',
'.log',
'file',
], 'mime_types get array'
assert (
client.conf_get('settings/http/static/mime_types/text%2Fplain/1')
== '.log'
), 'mime_types get array element'
assert 'success' in self.conf( assert 'success' in client.conf_delete(
'".log"', 'settings/http/static/mime_types/text%2Fblahblahblah' 'settings/http/static/mime_types/text%2Fplain/2'
), 'configure mime_types create' ), 'mime_types remove array element'
assert ( assert (
self.get(url='/log.log')['headers']['Content-Type'] 'Content-Type' not in client.get(url='/dir/file')['headers']
== 'text/blahblahblah' ), 'mime_types removed'
), 'mime_types create'
def test_static_mime_types_correct(self): assert 'success' in client.conf_post(
assert 'error' in self.conf( '"file"', 'settings/http/static/mime_types/text%2Fplain'
{"text/x-code": "readme", "text/plain": "readme"}, ), 'mime_types add array element'
'settings/http/static/mime_types', assert (
), 'mime_types same extensions' client.get(url='/dir/file')['headers']['Content-Type'] == 'text/plain'
assert 'error' in self.conf( ), 'mime_types reverted'
{"text/x-code": [".h", ".c"], "text/plain": ".c"},
'settings/http/static/mime_types',
), 'mime_types same extensions array'
assert 'error' in self.conf(
{
"text/x-code": [".h", ".c", "readme"],
"text/plain": "README",
},
'settings/http/static/mime_types',
), 'mime_types same extensions case insensitive'
@pytest.mark.skip('not yet') assert 'success' in client.conf(
def test_static_mime_types_invalid(self, temp_dir): '"file"', 'settings/http/static/mime_types/text%2Fplain'
assert 'error' in self.http( ), 'configure mime_types update'
b"""PUT /config/settings/http/static/mime_types/%0%00% HTTP/1.1\r assert (
client.get(url='/dir/file')['headers']['Content-Type'] == 'text/plain'
), 'mime_types updated'
assert (
'Content-Type' not in client.get(url='/log.log')['headers']
), 'mime_types updated 2'
assert 'success' in client.conf(
'".log"', 'settings/http/static/mime_types/text%2Fblahblahblah'
), 'configure mime_types create'
assert (
client.get(url='/log.log')['headers']['Content-Type']
== 'text/blahblahblah'
), 'mime_types create'
def test_static_mime_types_correct():
assert 'error' in client.conf(
{"text/x-code": "readme", "text/plain": "readme"},
'settings/http/static/mime_types',
), 'mime_types same extensions'
assert 'error' in client.conf(
{"text/x-code": [".h", ".c"], "text/plain": ".c"},
'settings/http/static/mime_types',
), 'mime_types same extensions array'
assert 'error' in client.conf(
{
"text/x-code": [".h", ".c", "readme"],
"text/plain": "README",
},
'settings/http/static/mime_types',
), 'mime_types same extensions case insensitive'
@pytest.mark.skip('not yet')
def test_static_mime_types_invalid(temp_dir):
assert 'error' in client.http(
b"""PUT /config/settings/http/static/mime_types/%0%00% HTTP/1.1\r
Host: localhost\r Host: localhost\r
Connection: close\r Connection: close\r
Content-Length: 6\r Content-Length: 6\r
\r \r
\"blah\"""", \"blah\"""",
raw_resp=True, raw_resp=True,
raw=True, raw=True,
sock_type='unix', sock_type='unix',
addr=f'{temp_dir}/control.unit.sock', addr=f'{temp_dir}/control.unit.sock',
), 'mime_types invalid' ), 'mime_types invalid'

View File

@@ -2,148 +2,162 @@ import os
from pathlib import Path from pathlib import Path
import pytest import pytest
from unit.applications.proto import TestApplicationProto from unit.applications.proto import ApplicationProto
from unit.option import option from unit.option import option
prerequisites = {'features': {'chroot': True}} prerequisites = {'features': {'chroot': True}}
client = ApplicationProto()
class TestStaticChroot(TestApplicationProto):
@pytest.fixture(autouse=True)
def setup_method_fixture(self, temp_dir):
os.makedirs(f'{temp_dir}/assets/dir')
Path(f'{temp_dir}/assets/index.html').write_text('0123456789')
Path(f'{temp_dir}/assets/dir/file').write_text('blah')
self.test_path = f'/{os.path.relpath(Path(__file__))}' @pytest.fixture(autouse=True)
def setup_method_fixture(temp_dir):
os.makedirs(f'{temp_dir}/assets/dir')
Path(f'{temp_dir}/assets/index.html').write_text('0123456789')
Path(f'{temp_dir}/assets/dir/file').write_text('blah')
self._load_conf( client.test_path = f'/{os.path.relpath(Path(__file__))}'
{
"listeners": {"*:7080": {"pass": "routes"}},
"routes": [{"action": {"share": f'{temp_dir}/assets$uri'}}],
}
)
def update_action(self, chroot, share=f'{option.temp_dir}/assets$uri'): assert 'success' in client.conf(
return self.conf( {
{'chroot': chroot, 'share': share}, "listeners": {"*:7080": {"pass": "routes"}},
'routes/0/action', "routes": [{"action": {"share": f'{temp_dir}/assets$uri'}}],
) }
)
def get_custom(self, uri, host):
return self.get(url=uri, headers={'Host': host, 'Connection': 'close'})[
'status'
]
def test_static_chroot(self, temp_dir): def update_action(chroot, share=f'{option.temp_dir}/assets$uri'):
assert self.get(url='/dir/file')['status'] == 200, 'default chroot' return client.conf(
assert self.get(url='/index.html')['status'] == 200, 'default chroot 2' {'chroot': chroot, 'share': share},
'routes/0/action',
)
assert 'success' in self.update_action(f'{temp_dir}/assets/dir')
assert self.get(url='/dir/file')['status'] == 200, 'chroot' def get_custom(uri, host):
assert self.get(url='/index.html')['status'] == 403, 'chroot 403 2' return client.get(url=uri, headers={'Host': host, 'Connection': 'close'})[
assert self.get(url='/file')['status'] == 403, 'chroot 403' 'status'
]
def test_share_chroot_array(self, temp_dir):
assert 'success' in self.update_action(
f'{temp_dir}/assets/dir', ["/blah", f'{temp_dir}/assets$uri']
)
assert self.get(url='/dir/file')['status'] == 200, 'share array'
assert 'success' in self.update_action( def test_static_chroot(temp_dir):
f'{temp_dir}/assets/$host', assert client.get(url='/dir/file')['status'] == 200, 'default chroot'
['/blah', f'{temp_dir}/assets$uri'], assert client.get(url='/index.html')['status'] == 200, 'default chroot 2'
)
assert self.get_custom('/dir/file', 'dir') == 200, 'array variable'
assert 'success' in self.update_action( assert 'success' in update_action(f'{temp_dir}/assets/dir')
f'{temp_dir}/assets/dir', ['/blah', '/blah2']
)
assert self.get()['status'] != 200, 'share array bad'
def test_static_chroot_permission(self, require, temp_dir): assert client.get(url='/dir/file')['status'] == 200, 'chroot'
require({'privileged_user': False}) assert client.get(url='/index.html')['status'] == 403, 'chroot 403 2'
assert client.get(url='/file')['status'] == 403, 'chroot 403'
os.chmod(f'{temp_dir}/assets/dir', 0o100)
assert 'success' in self.update_action( def test_share_chroot_array(temp_dir):
f'{temp_dir}/assets/dir' assert 'success' in update_action(
), 'configure chroot' f'{temp_dir}/assets/dir', ["/blah", f'{temp_dir}/assets$uri']
)
assert client.get(url='/dir/file')['status'] == 200, 'share array'
assert self.get(url='/dir/file')['status'] == 200, 'chroot' assert 'success' in update_action(
f'{temp_dir}/assets/$host',
['/blah', f'{temp_dir}/assets$uri'],
)
assert get_custom('/dir/file', 'dir') == 200, 'array variable'
def test_static_chroot_empty(self): assert 'success' in update_action(
assert 'success' in self.update_action('') f'{temp_dir}/assets/dir', ['/blah', '/blah2']
assert self.get(url='/dir/file')['status'] == 200, 'empty absolute' )
assert client.get()['status'] != 200, 'share array bad'
assert 'success' in self.update_action("", ".$uri")
assert self.get(url=self.test_path)['status'] == 200, 'empty relative'
def test_static_chroot_relative(self, require): def test_static_chroot_permission(require, temp_dir):
require({'privileged_user': False}) require({'privileged_user': False})
assert 'success' in self.update_action('.') os.chmod(f'{temp_dir}/assets/dir', 0o100)
assert self.get(url='/dir/file')['status'] == 403, 'relative chroot'
assert 'success' in self.conf({"share": ".$uri"}, 'routes/0/action') assert 'success' in update_action(
assert self.get(url=self.test_path)['status'] == 200, 'relative share' f'{temp_dir}/assets/dir'
), 'configure chroot'
assert 'success' in self.update_action(".", ".$uri") assert client.get(url='/dir/file')['status'] == 200, 'chroot'
assert self.get(url=self.test_path)['status'] == 200, 'relative'
def test_static_chroot_variables(self, temp_dir):
assert 'success' in self.update_action(f'{temp_dir}/assets/$host')
assert self.get_custom('/dir/file', 'dir') == 200
assert 'success' in self.update_action(f'{temp_dir}/assets/${{host}}') def test_static_chroot_empty():
assert self.get_custom('/dir/file', 'dir') == 200 assert 'success' in update_action('')
assert client.get(url='/dir/file')['status'] == 200, 'empty absolute'
def test_static_chroot_variables_buildin_start(self, temp_dir): assert 'success' in update_action("", ".$uri")
assert 'success' in self.update_action( assert client.get(url=client.test_path)['status'] == 200, 'empty relative'
'$uri/assets/dir',
f'{temp_dir}/assets/dir/$host',
)
assert self.get_custom(temp_dir, 'file') == 200
def test_static_chroot_variables_buildin_mid(self, temp_dir):
assert 'success' in self.update_action(f'{temp_dir}/$host/dir')
assert self.get_custom('/dir/file', 'assets') == 200
def test_static_chroot_variables_buildin_end(self, temp_dir): def test_static_chroot_relative(require):
assert 'success' in self.update_action(f'{temp_dir}/assets/$host') require({'privileged_user': False})
assert self.get_custom('/dir/file', 'dir') == 200
def test_static_chroot_slash(self, temp_dir): assert 'success' in update_action('.')
assert 'success' in self.update_action(f'{temp_dir}/assets/dir/') assert client.get(url='/dir/file')['status'] == 403, 'relative chroot'
assert self.get(url='/dir/file')['status'] == 200, 'slash end'
assert self.get(url='/dirxfile')['status'] == 403, 'slash end bad'
assert 'success' in self.update_action(f'{temp_dir}/assets/dir') assert 'success' in client.conf({"share": ".$uri"}, 'routes/0/action')
assert self.get(url='/dir/file')['status'] == 200, 'no slash end' assert client.get(url=client.test_path)['status'] == 200, 'relative share'
assert 'success' in self.update_action(f'{temp_dir}/assets/dir/') assert 'success' in update_action(".", ".$uri")
assert self.get(url='/dir/file')['status'] == 200, 'slash end 2' assert client.get(url=client.test_path)['status'] == 200, 'relative'
assert self.get(url='/dirxfile')['status'] == 403, 'slash end 2 bad'
assert 'success' in self.update_action(
f'{temp_dir}//assets////dir///', f'{temp_dir}///assets/////$uri'
)
assert self.get(url='/dir/file')['status'] == 200, 'multiple slashes'
def test_static_chroot_invalid(self, temp_dir): def test_static_chroot_variables(temp_dir):
assert 'error' in self.conf( assert 'success' in update_action(f'{temp_dir}/assets/$host')
{"share": temp_dir, "chroot": True}, assert get_custom('/dir/file', 'dir') == 200
'routes/0/action',
), 'configure chroot error'
assert 'error' in self.conf(
{"share": temp_dir, "symlinks": "True"},
'routes/0/action',
), 'configure symlink error'
assert 'error' in self.conf(
{"share": temp_dir, "mount": "True"},
'routes/0/action',
), 'configure mount error'
assert 'error' in self.update_action(f'{temp_dir}/assets/d$r$uri') assert 'success' in update_action(f'{temp_dir}/assets/${{host}}')
assert 'error' in self.update_action(f'{temp_dir}/assets/$$uri') assert get_custom('/dir/file', 'dir') == 200
def test_static_chroot_variables_buildin_start(temp_dir):
assert 'success' in update_action(
'$uri/assets/dir',
f'{temp_dir}/assets/dir/$host',
)
assert get_custom(temp_dir, 'file') == 200
def test_static_chroot_variables_buildin_mid(temp_dir):
assert 'success' in update_action(f'{temp_dir}/$host/dir')
assert get_custom('/dir/file', 'assets') == 200
def test_static_chroot_variables_buildin_end(temp_dir):
assert 'success' in update_action(f'{temp_dir}/assets/$host')
assert get_custom('/dir/file', 'dir') == 200
def test_static_chroot_slash(temp_dir):
assert 'success' in update_action(f'{temp_dir}/assets/dir/')
assert client.get(url='/dir/file')['status'] == 200, 'slash end'
assert client.get(url='/dirxfile')['status'] == 403, 'slash end bad'
assert 'success' in update_action(f'{temp_dir}/assets/dir')
assert client.get(url='/dir/file')['status'] == 200, 'no slash end'
assert 'success' in update_action(f'{temp_dir}/assets/dir/')
assert client.get(url='/dir/file')['status'] == 200, 'slash end 2'
assert client.get(url='/dirxfile')['status'] == 403, 'slash end 2 bad'
assert 'success' in update_action(
f'{temp_dir}//assets////dir///', f'{temp_dir}///assets/////$uri'
)
assert client.get(url='/dir/file')['status'] == 200, 'multiple slashes'
def test_static_chroot_invalid(temp_dir):
assert 'error' in client.conf(
{"share": temp_dir, "chroot": True},
'routes/0/action',
), 'configure chroot error'
assert 'error' in client.conf(
{"share": temp_dir, "symlinks": "True"},
'routes/0/action',
), 'configure symlink error'
assert 'error' in client.conf(
{"share": temp_dir, "mount": "True"},
'routes/0/action',
), 'configure mount error'
assert 'error' in update_action(f'{temp_dir}/assets/d$r$uri')
assert 'error' in update_action(f'{temp_dir}/assets/$$uri')

View File

@@ -2,149 +2,156 @@ import os
from pathlib import Path from pathlib import Path
import pytest import pytest
from unit.applications.proto import TestApplicationProto from unit.applications.proto import ApplicationProto
client = ApplicationProto()
class TestStaticFallback(TestApplicationProto): @pytest.fixture(autouse=True)
@pytest.fixture(autouse=True) def setup_method_fixture(temp_dir):
def setup_method_fixture(self, temp_dir): assets_dir = f'{temp_dir}/assets'
assets_dir = f'{temp_dir}/assets' os.makedirs(f'{assets_dir}/dir')
os.makedirs(f'{assets_dir}/dir') Path(f'{assets_dir}/index.html').write_text('0123456789')
Path(f'{assets_dir}/index.html').write_text('0123456789')
os.makedirs(f'{assets_dir}/403') os.makedirs(f'{assets_dir}/403')
os.chmod(f'{assets_dir}/403', 0o000) os.chmod(f'{assets_dir}/403', 0o000)
self._load_conf( assert 'success' in client.conf(
{
"listeners": {
"*:7080": {"pass": "routes"},
"*:7081": {"pass": "routes"},
},
"routes": [{"action": {"share": f'{assets_dir}$uri'}}],
"applications": {},
}
)
yield
try:
os.chmod(f'{assets_dir}/403', 0o777)
except FileNotFoundError:
pass
def action_update(conf):
assert 'success' in client.conf(conf, 'routes/0/action')
def test_static_fallback():
action_update({"share": "/blah"})
assert client.get()['status'] == 404, 'bad path no fallback'
action_update({"share": "/blah", "fallback": {"return": 200}})
resp = client.get()
assert resp['status'] == 200, 'bad path fallback status'
assert resp['body'] == '', 'bad path fallback'
def test_static_fallback_valid_path(temp_dir):
action_update(
{"share": f"{temp_dir}/assets$uri", "fallback": {"return": 200}}
)
resp = client.get()
assert resp['status'] == 200, 'fallback status'
assert resp['body'] == '0123456789', 'fallback'
resp = client.get(url='/403/')
assert resp['status'] == 200, 'fallback status 403'
assert resp['body'] == '', 'fallback 403'
resp = client.post()
assert resp['status'] == 200, 'fallback status 405'
assert resp['body'] == '', 'fallback 405'
assert client.get(url='/dir')['status'] == 301, 'fallback status 301'
def test_static_fallback_nested():
action_update(
{
"share": "/blah",
"fallback": {
"share": "/blah/blah",
"fallback": {"return": 200},
},
}
)
resp = client.get()
assert resp['status'] == 200, 'fallback nested status'
assert resp['body'] == '', 'fallback nested'
def test_static_fallback_share(temp_dir):
action_update(
{
"share": "/blah",
"fallback": {"share": f"{temp_dir}/assets$uri"},
}
)
resp = client.get()
assert resp['status'] == 200, 'fallback share status'
assert resp['body'] == '0123456789', 'fallback share'
resp = client.head()
assert resp['status'] == 200, 'fallback share status HEAD'
assert resp['body'] == '', 'fallback share HEAD'
assert client.get(url='/dir')['status'] == 301, 'fallback share status 301'
def test_static_fallback_proxy():
assert 'success' in client.conf(
[
{ {
"listeners": { "match": {"destination": "*:7081"},
"*:7080": {"pass": "routes"}, "action": {"return": 200},
"*:7081": {"pass": "routes"}, },
},
"routes": [{"action": {"share": f'{assets_dir}$uri'}}],
"applications": {},
}
)
yield
try:
os.chmod(f'{assets_dir}/403', 0o777)
except FileNotFoundError:
pass
def action_update(self, conf):
assert 'success' in self.conf(conf, 'routes/0/action')
def test_static_fallback(self):
self.action_update({"share": "/blah"})
assert self.get()['status'] == 404, 'bad path no fallback'
self.action_update({"share": "/blah", "fallback": {"return": 200}})
resp = self.get()
assert resp['status'] == 200, 'bad path fallback status'
assert resp['body'] == '', 'bad path fallback'
def test_static_fallback_valid_path(self, temp_dir):
self.action_update(
{"share": f"{temp_dir}/assets$uri", "fallback": {"return": 200}}
)
resp = self.get()
assert resp['status'] == 200, 'fallback status'
assert resp['body'] == '0123456789', 'fallback'
resp = self.get(url='/403/')
assert resp['status'] == 200, 'fallback status 403'
assert resp['body'] == '', 'fallback 403'
resp = self.post()
assert resp['status'] == 200, 'fallback status 405'
assert resp['body'] == '', 'fallback 405'
assert self.get(url='/dir')['status'] == 301, 'fallback status 301'
def test_static_fallback_nested(self):
self.action_update(
{ {
"share": "/blah", "action": {
"fallback": { "share": "/blah",
"share": "/blah/blah", "fallback": {"proxy": "http://127.0.0.1:7081"},
"fallback": {"return": 200}, }
}, },
} ],
) 'routes',
), 'configure fallback proxy route'
resp = self.get() resp = client.get()
assert resp['status'] == 200, 'fallback nested status' assert resp['status'] == 200, 'fallback proxy status'
assert resp['body'] == '', 'fallback nested' assert resp['body'] == '', 'fallback proxy'
def test_static_fallback_share(self, temp_dir):
self.action_update(
{
"share": "/blah",
"fallback": {"share": f"{temp_dir}/assets$uri"},
}
)
resp = self.get() @pytest.mark.skip('not yet')
assert resp['status'] == 200, 'fallback share status' def test_static_fallback_proxy_loop(skip_alert):
assert resp['body'] == '0123456789', 'fallback share' skip_alert(
r'open.*/blah/index.html.*failed',
r'accept.*failed',
r'socket.*failed',
r'new connections are not accepted',
)
resp = self.head() action_update(
assert resp['status'] == 200, 'fallback share status HEAD' {"share": "/blah", "fallback": {"proxy": "http://127.0.0.1:7080"}}
assert resp['body'] == '', 'fallback share HEAD' )
client.get(no_recv=True)
assert ( assert 'success' in client.conf_delete('listeners/*:7081')
self.get(url='/dir')['status'] == 301 client.get(read_timeout=1)
), 'fallback share status 301'
def test_static_fallback_proxy(self):
assert 'success' in self.conf(
[
{
"match": {"destination": "*:7081"},
"action": {"return": 200},
},
{
"action": {
"share": "/blah",
"fallback": {"proxy": "http://127.0.0.1:7081"},
}
},
],
'routes',
), 'configure fallback proxy route'
resp = self.get() def test_static_fallback_invalid():
assert resp['status'] == 200, 'fallback proxy status' def check_error(conf):
assert resp['body'] == '', 'fallback proxy' assert 'error' in client.conf(conf, 'routes/0/action')
@pytest.mark.skip('not yet') check_error({"share": "/blah", "fallback": {}})
def test_static_fallback_proxy_loop(self, skip_alert): check_error({"share": "/blah", "fallback": ""})
skip_alert( check_error({"return": 200, "fallback": {"share": "/blah"}})
r'open.*/blah/index.html.*failed', check_error(
r'accept.*failed', {"proxy": "http://127.0.0.1:7081", "fallback": {"share": "/blah"}}
r'socket.*failed', )
r'new connections are not accepted', check_error({"fallback": {"share": "/blah"}})
)
self.action_update(
{"share": "/blah", "fallback": {"proxy": "http://127.0.0.1:7080"}}
)
self.get(no_recv=True)
assert 'success' in self.conf_delete('listeners/*:7081')
self.get(read_timeout=1)
def test_static_fallback_invalid(self):
def check_error(conf):
assert 'error' in self.conf(conf, 'routes/0/action')
check_error({"share": "/blah", "fallback": {}})
check_error({"share": "/blah", "fallback": ""})
check_error({"return": 200, "fallback": {"share": "/blah"}})
check_error(
{"proxy": "http://127.0.0.1:7081", "fallback": {"share": "/blah"}}
)
check_error({"fallback": {"share": "/blah"}})

View File

@@ -3,130 +3,134 @@ import subprocess
from pathlib import Path from pathlib import Path
import pytest import pytest
from unit.applications.proto import TestApplicationProto from unit.applications.proto import ApplicationProto
prerequisites = {'features': {'chroot': True}, 'privileged_user': True} prerequisites = {'features': {'chroot': True}, 'privileged_user': True}
client = ApplicationProto()
class TestStaticMount(TestApplicationProto):
@pytest.fixture(autouse=True)
def setup_method_fixture(self, temp_dir):
os.makedirs(f'{temp_dir}/assets/dir/mount')
os.makedirs(f'{temp_dir}/assets/dir/dir')
os.makedirs(f'{temp_dir}/assets/mount')
Path(f'{temp_dir}/assets/index.html').write_text('index')
Path(f'{temp_dir}/assets/dir/dir/file').write_text('file')
Path(f'{temp_dir}/assets/mount/index.html').write_text('mount')
try: @pytest.fixture(autouse=True)
subprocess.check_output( def setup_method_fixture(temp_dir):
[ os.makedirs(f'{temp_dir}/assets/dir/mount')
"mount", os.makedirs(f'{temp_dir}/assets/dir/dir')
"--bind", os.makedirs(f'{temp_dir}/assets/mount')
f'{temp_dir}/assets/mount', Path(f'{temp_dir}/assets/index.html').write_text('index')
f'{temp_dir}/assets/dir/mount', Path(f'{temp_dir}/assets/dir/dir/file').write_text('file')
], Path(f'{temp_dir}/assets/mount/index.html').write_text('mount')
stderr=subprocess.STDOUT,
)
except KeyboardInterrupt: try:
raise subprocess.check_output(
[
except subprocess.CalledProcessError: "mount",
pytest.fail("Can't run mount process.") "--bind",
f'{temp_dir}/assets/mount',
self._load_conf( f'{temp_dir}/assets/dir/mount',
{ ],
"listeners": {"*:7080": {"pass": "routes"}}, stderr=subprocess.STDOUT,
"routes": [{"action": {"share": f'{temp_dir}/assets/dir$uri'}}],
}
) )
yield except KeyboardInterrupt:
raise
try: except subprocess.CalledProcessError:
subprocess.check_output( pytest.fail("Can't run mount process.")
["umount", "--lazy", f'{temp_dir}/assets/dir/mount'],
stderr=subprocess.STDOUT,
)
except KeyboardInterrupt: assert 'success' in client.conf(
raise {
"listeners": {"*:7080": {"pass": "routes"}},
"routes": [{"action": {"share": f'{temp_dir}/assets/dir$uri'}}],
}
)
except subprocess.CalledProcessError: yield
pytest.fail("Can't run umount process.")
def test_static_mount(self, temp_dir, skip_alert): try:
skip_alert(r'opening.*failed') subprocess.check_output(
["umount", "--lazy", f'{temp_dir}/assets/dir/mount'],
stderr=subprocess.STDOUT,
)
resp = self.get(url='/mount/') except KeyboardInterrupt:
assert resp['status'] == 200 raise
assert resp['body'] == 'mount'
assert 'success' in self.conf( except subprocess.CalledProcessError:
{"share": f'{temp_dir}/assets/dir$uri', "traverse_mounts": False}, pytest.fail("Can't run umount process.")
'routes/0/action',
), 'configure mount disable'
assert self.get(url='/mount/')['status'] == 403
assert 'success' in self.conf( def test_static_mount(temp_dir, skip_alert):
{"share": f'{temp_dir}/assets/dir$uri', "traverse_mounts": True}, skip_alert(r'opening.*failed')
'routes/0/action',
), 'configure mount enable'
resp = self.get(url='/mount/') resp = client.get(url='/mount/')
assert resp['status'] == 200 assert resp['status'] == 200
assert resp['body'] == 'mount' assert resp['body'] == 'mount'
def test_static_mount_two_blocks(self, temp_dir, skip_alert): assert 'success' in client.conf(
skip_alert(r'opening.*failed') {"share": f'{temp_dir}/assets/dir$uri', "traverse_mounts": False},
'routes/0/action',
), 'configure mount disable'
os.symlink(f'{temp_dir}/assets/dir', f'{temp_dir}/assets/link') assert client.get(url='/mount/')['status'] == 403
assert 'success' in self.conf( assert 'success' in client.conf(
[ {"share": f'{temp_dir}/assets/dir$uri', "traverse_mounts": True},
{ 'routes/0/action',
"match": {"method": "HEAD"}, ), 'configure mount enable'
"action": {
"share": f'{temp_dir}/assets/dir$uri',
"traverse_mounts": False,
},
},
{
"match": {"method": "GET"},
"action": {
"share": f'{temp_dir}/assets/dir$uri',
"traverse_mounts": True,
},
},
],
'routes',
), 'configure two options'
assert self.get(url='/mount/')['status'] == 200, 'block enabled' resp = client.get(url='/mount/')
assert self.head(url='/mount/')['status'] == 403, 'block disabled' assert resp['status'] == 200
assert resp['body'] == 'mount'
def test_static_mount_chroot(self, temp_dir, skip_alert):
skip_alert(r'opening.*failed')
assert 'success' in self.conf( def test_static_mount_two_blocks(temp_dir, skip_alert):
skip_alert(r'opening.*failed')
os.symlink(f'{temp_dir}/assets/dir', f'{temp_dir}/assets/link')
assert 'success' in client.conf(
[
{ {
"share": f'{temp_dir}/assets/dir$uri', "match": {"method": "HEAD"},
"chroot": f'{temp_dir}/assets', "action": {
"share": f'{temp_dir}/assets/dir$uri',
"traverse_mounts": False,
},
}, },
'routes/0/action',
), 'configure chroot mount default'
assert self.get(url='/mount/')['status'] == 200, 'chroot'
assert 'success' in self.conf(
{ {
"share": f'{temp_dir}/assets/dir$uri', "match": {"method": "GET"},
"chroot": f'{temp_dir}/assets', "action": {
"traverse_mounts": False, "share": f'{temp_dir}/assets/dir$uri',
"traverse_mounts": True,
},
}, },
'routes/0/action', ],
), 'configure chroot mount disable' 'routes',
), 'configure two options'
assert self.get(url='/mount/')['status'] == 403, 'chroot mount' assert client.get(url='/mount/')['status'] == 200, 'block enabled'
assert client.head(url='/mount/')['status'] == 403, 'block disabled'
def test_static_mount_chroot(temp_dir, skip_alert):
skip_alert(r'opening.*failed')
assert 'success' in client.conf(
{
"share": f'{temp_dir}/assets/dir$uri',
"chroot": f'{temp_dir}/assets',
},
'routes/0/action',
), 'configure chroot mount default'
assert client.get(url='/mount/')['status'] == 200, 'chroot'
assert 'success' in client.conf(
{
"share": f'{temp_dir}/assets/dir$uri',
"chroot": f'{temp_dir}/assets',
"traverse_mounts": False,
},
'routes/0/action',
), 'configure chroot mount disable'
assert client.get(url='/mount/')['status'] == 403, 'chroot mount'

View File

@@ -2,69 +2,72 @@ import os
from pathlib import Path from pathlib import Path
import pytest import pytest
from unit.applications.proto import TestApplicationProto from unit.applications.proto import ApplicationProto
client = ApplicationProto()
class TestStaticShare(TestApplicationProto): @pytest.fixture(autouse=True)
@pytest.fixture(autouse=True) def setup_method_fixture(temp_dir):
def setup_method_fixture(self, temp_dir): os.makedirs(f'{temp_dir}/assets/dir')
os.makedirs(f'{temp_dir}/assets/dir') os.makedirs(f'{temp_dir}/assets/dir2')
os.makedirs(f'{temp_dir}/assets/dir2')
Path(f'{temp_dir}/assets/dir/file').write_text('1') Path(f'{temp_dir}/assets/dir/file').write_text('1')
Path(f'{temp_dir}/assets/dir2/file2').write_text('2') Path(f'{temp_dir}/assets/dir2/file2').write_text('2')
assert 'success' in self.conf( assert 'success' in client.conf(
{ {
"listeners": {"*:7080": {"pass": "routes"}}, "listeners": {"*:7080": {"pass": "routes"}},
"routes": [{"action": {"share": f'{temp_dir}/assets$uri'}}], "routes": [{"action": {"share": f'{temp_dir}/assets$uri'}}],
"applications": {}, "applications": {},
} }
) )
def action_update(self, conf):
assert 'success' in self.conf(conf, 'routes/0/action')
def test_share_array(self, temp_dir): def action_update(conf):
assert self.get(url='/dir/file')['body'] == '1' assert 'success' in client.conf(conf, 'routes/0/action')
assert self.get(url='/dir2/file2')['body'] == '2'
self.action_update({"share": [f'{temp_dir}/assets/dir$uri']})
assert self.get(url='/file')['body'] == '1' def test_share_array(temp_dir):
assert self.get(url='/file2')['status'] == 404 assert client.get(url='/dir/file')['body'] == '1'
assert client.get(url='/dir2/file2')['body'] == '2'
self.action_update( action_update({"share": [f'{temp_dir}/assets/dir$uri']})
{
"share": [
f'{temp_dir}/assets/dir$uri',
f'{temp_dir}/assets/dir2$uri',
]
}
)
assert self.get(url='/file')['body'] == '1' assert client.get(url='/file')['body'] == '1'
assert self.get(url='/file2')['body'] == '2' assert client.get(url='/file2')['status'] == 404
self.action_update( action_update(
{ {
"share": [ "share": [
f'{temp_dir}/assets/dir2$uri', f'{temp_dir}/assets/dir$uri',
f'{temp_dir}/assets/dir3$uri', f'{temp_dir}/assets/dir2$uri',
] ]
} }
) )
assert self.get(url='/file')['status'] == 404 assert client.get(url='/file')['body'] == '1'
assert self.get(url='/file2')['body'] == '2' assert client.get(url='/file2')['body'] == '2'
def test_share_array_fallback(self): action_update(
self.action_update( {
{"share": ["/blah", "/blah2"], "fallback": {"return": 201}} "share": [
) f'{temp_dir}/assets/dir2$uri',
f'{temp_dir}/assets/dir3$uri',
]
}
)
assert self.get()['status'] == 201 assert client.get(url='/file')['status'] == 404
assert client.get(url='/file2')['body'] == '2'
def test_share_array_invalid(self):
assert 'error' in self.conf({"share": []}, 'routes/0/action') def test_share_array_fallback():
assert 'error' in self.conf({"share": {}}, 'routes/0/action') action_update({"share": ["/blah", "/blah2"], "fallback": {"return": 201}})
assert client.get()['status'] == 201
def test_share_array_invalid():
assert 'error' in client.conf({"share": []}, 'routes/0/action')
assert 'error' in client.conf({"share": {}}, 'routes/0/action')

View File

@@ -2,92 +2,94 @@ import os
from pathlib import Path from pathlib import Path
import pytest import pytest
from unit.applications.proto import TestApplicationProto from unit.applications.proto import ApplicationProto
prerequisites = {'features': {'chroot': True}} prerequisites = {'features': {'chroot': True}}
client = ApplicationProto()
class TestStaticSymlink(TestApplicationProto):
@pytest.fixture(autouse=True)
def setup_method_fixture(self, temp_dir):
os.makedirs(f'{temp_dir}/assets/dir/dir')
Path(f'{temp_dir}/assets/index.html').write_text('0123456789')
Path(f'{temp_dir}/assets/dir/file').write_text('blah')
self._load_conf( @pytest.fixture(autouse=True)
def setup_method_fixture(temp_dir):
os.makedirs(f'{temp_dir}/assets/dir/dir')
Path(f'{temp_dir}/assets/index.html').write_text('0123456789')
Path(f'{temp_dir}/assets/dir/file').write_text('blah')
assert 'success' in client.conf(
{
"listeners": {"*:7080": {"pass": "routes"}},
"routes": [{"action": {"share": f'{temp_dir}/assets$uri'}}],
}
)
def test_static_symlink(temp_dir, skip_alert):
skip_alert(r'opening.*failed')
os.symlink(f'{temp_dir}/assets/dir', f'{temp_dir}/assets/link')
assert client.get(url='/dir')['status'] == 301, 'dir'
assert client.get(url='/dir/file')['status'] == 200, 'file'
assert client.get(url='/link')['status'] == 301, 'symlink dir'
assert client.get(url='/link/file')['status'] == 200, 'symlink file'
assert 'success' in client.conf(
{"share": f'{temp_dir}/assets$uri', "follow_symlinks": False},
'routes/0/action',
), 'configure symlink disable'
assert client.get(url='/link/file')['status'] == 403, 'symlink disabled'
assert 'success' in client.conf(
{"share": f'{temp_dir}/assets$uri', "follow_symlinks": True},
'routes/0/action',
), 'configure symlink enable'
assert client.get(url='/link/file')['status'] == 200, 'symlink enabled'
def test_static_symlink_two_blocks(temp_dir, skip_alert):
skip_alert(r'opening.*failed')
os.symlink(f'{temp_dir}/assets/dir', f'{temp_dir}/assets/link')
assert 'success' in client.conf(
[
{ {
"listeners": {"*:7080": {"pass": "routes"}}, "match": {"method": "HEAD"},
"routes": [{"action": {"share": f'{temp_dir}/assets$uri'}}], "action": {
} "share": f'{temp_dir}/assets$uri',
) "follow_symlinks": False,
def test_static_symlink(self, temp_dir, skip_alert):
skip_alert(r'opening.*failed')
os.symlink(f'{temp_dir}/assets/dir', f'{temp_dir}/assets/link')
assert self.get(url='/dir')['status'] == 301, 'dir'
assert self.get(url='/dir/file')['status'] == 200, 'file'
assert self.get(url='/link')['status'] == 301, 'symlink dir'
assert self.get(url='/link/file')['status'] == 200, 'symlink file'
assert 'success' in self.conf(
{"share": f'{temp_dir}/assets$uri', "follow_symlinks": False},
'routes/0/action',
), 'configure symlink disable'
assert self.get(url='/link/file')['status'] == 403, 'symlink disabled'
assert 'success' in self.conf(
{"share": f'{temp_dir}/assets$uri', "follow_symlinks": True},
'routes/0/action',
), 'configure symlink enable'
assert self.get(url='/link/file')['status'] == 200, 'symlink enabled'
def test_static_symlink_two_blocks(self, temp_dir, skip_alert):
skip_alert(r'opening.*failed')
os.symlink(f'{temp_dir}/assets/dir', f'{temp_dir}/assets/link')
assert 'success' in self.conf(
[
{
"match": {"method": "HEAD"},
"action": {
"share": f'{temp_dir}/assets$uri',
"follow_symlinks": False,
},
}, },
{
"match": {"method": "GET"},
"action": {
"share": f'{temp_dir}/assets$uri',
"follow_symlinks": True,
},
},
],
'routes',
), 'configure two options'
assert self.get(url='/link/file')['status'] == 200, 'block enabled'
assert self.head(url='/link/file')['status'] == 403, 'block disabled'
def test_static_symlink_chroot(self, temp_dir, skip_alert):
skip_alert(r'opening.*failed')
os.symlink(
f'{temp_dir}/assets/dir/file', f'{temp_dir}/assets/dir/dir/link'
)
assert self.get(url='/dir/dir/link')['status'] == 200, 'default chroot'
assert 'success' in self.conf(
{
"share": f'{temp_dir}/assets$uri',
"chroot": f'{temp_dir}/assets/dir/dir',
}, },
'routes/0/action', {
), 'configure chroot' "match": {"method": "GET"},
"action": {
"share": f'{temp_dir}/assets$uri',
"follow_symlinks": True,
},
},
],
'routes',
), 'configure two options'
assert self.get(url='/dir/dir/link')['status'] == 404, 'chroot' assert client.get(url='/link/file')['status'] == 200, 'block enabled'
assert client.head(url='/link/file')['status'] == 403, 'block disabled'
def test_static_symlink_chroot(temp_dir, skip_alert):
skip_alert(r'opening.*failed')
os.symlink(f'{temp_dir}/assets/dir/file', f'{temp_dir}/assets/dir/dir/link')
assert client.get(url='/dir/dir/link')['status'] == 200, 'default chroot'
assert 'success' in client.conf(
{
"share": f'{temp_dir}/assets$uri',
"chroot": f'{temp_dir}/assets/dir/dir',
},
'routes/0/action',
), 'configure chroot'
assert client.get(url='/dir/dir/link')['status'] == 404, 'chroot'

View File

@@ -1,170 +1,173 @@
from pathlib import Path from pathlib import Path
import pytest import pytest
from unit.applications.proto import TestApplicationProto from unit.applications.proto import ApplicationProto
client = ApplicationProto()
class TestStaticTypes(TestApplicationProto): @pytest.fixture(autouse=True)
@pytest.fixture(autouse=True) def setup_method_fixture(temp_dir):
def setup_method_fixture(self, temp_dir): Path(f'{temp_dir}/assets').mkdir()
Path(f'{temp_dir}/assets').mkdir() for ext in ['.xml', '.mp4', '.php', '', '.txt', '.html', '.png']:
for ext in ['.xml', '.mp4', '.php', '', '.txt', '.html', '.png']: Path(f'{temp_dir}/assets/file{ext}').write_text(ext)
Path(f'{temp_dir}/assets/file{ext}').write_text(ext)
Path(f'{temp_dir}/assets/index.html').write_text('index') Path(f'{temp_dir}/assets/index.html').write_text('index')
self._load_conf( assert 'success' in client.conf(
{
"listeners": {
"*:7080": {"pass": "routes"},
"*:7081": {"pass": "routes"},
},
"routes": [{"action": {"share": f'{temp_dir}/assets$uri'}}],
"applications": {},
}
)
def action_update(conf):
assert 'success' in client.conf(conf, 'routes/0/action')
def check_body(http_url, body):
resp = client.get(url=http_url)
assert resp['status'] == 200, 'status'
assert resp['body'] == body, 'body'
def test_static_types_basic(temp_dir):
action_update({"share": f'{temp_dir}/assets$uri'})
check_body('/index.html', 'index')
check_body('/file.xml', '.xml')
action_update(
{"share": f'{temp_dir}/assets$uri', "types": "application/xml"}
)
check_body('/file.xml', '.xml')
action_update(
{"share": f'{temp_dir}/assets$uri', "types": ["application/xml"]}
)
check_body('/file.xml', '.xml')
action_update({"share": f'{temp_dir}/assets$uri', "types": [""]})
assert client.get(url='/file.xml')['status'] == 403, 'no mtype'
def test_static_types_wildcard(temp_dir):
action_update(
{"share": f'{temp_dir}/assets$uri', "types": ["application/*"]}
)
check_body('/file.xml', '.xml')
assert client.get(url='/file.mp4')['status'] == 403, 'app * mtype mp4'
action_update({"share": f'{temp_dir}/assets$uri', "types": ["video/*"]})
assert client.get(url='/file.xml')['status'] == 403, 'video * mtype xml'
check_body('/file.mp4', '.mp4')
def test_static_types_negation(temp_dir):
action_update(
{"share": f'{temp_dir}/assets$uri', "types": ["!application/xml"]}
)
assert client.get(url='/file.xml')['status'] == 403, 'forbidden negation'
check_body('/file.mp4', '.mp4')
# sorting negation
action_update(
{
"share": f'{temp_dir}/assets$uri',
"types": ["!video/*", "image/png", "!image/jpg"],
}
)
assert client.get(url='/file.mp4')['status'] == 403, 'negation sort mp4'
check_body('/file.png', '.png')
assert client.get(url='/file.jpg')['status'] == 403, 'negation sort jpg'
def test_static_types_regex(temp_dir):
action_update(
{
"share": f'{temp_dir}/assets$uri',
"types": ["~text/(html|plain)"],
}
)
assert client.get(url='/file.php')['status'] == 403, 'regex fail'
check_body('/file.html', '.html')
check_body('/file.txt', '.txt')
def test_static_types_case(temp_dir):
action_update(
{"share": f'{temp_dir}/assets$uri', "types": ["!APpliCaTiOn/xMl"]}
)
check_body('/file.mp4', '.mp4')
assert (
client.get(url='/file.xml')['status'] == 403
), 'mixed case xml negation'
action_update({"share": f'{temp_dir}/assets$uri', "types": ["vIdEo/mp4"]})
assert client.get(url='/file.mp4')['status'] == 200, 'mixed case'
assert (
client.get(url='/file.xml')['status'] == 403
), 'mixed case video negation'
action_update({"share": f'{temp_dir}/assets$uri', "types": ["vIdEo/*"]})
check_body('/file.mp4', '.mp4')
assert (
client.get(url='/file.xml')['status'] == 403
), 'mixed case video * negation'
def test_static_types_fallback(temp_dir):
assert 'success' in client.conf(
[
{ {
"listeners": { "match": {"destination": "*:7081"},
"*:7080": {"pass": "routes"}, "action": {"return": 200},
"*:7081": {"pass": "routes"}, },
},
"routes": [{"action": {"share": f'{temp_dir}/assets$uri'}}],
"applications": {},
}
)
def action_update(self, conf):
assert 'success' in self.conf(conf, 'routes/0/action')
def check_body(self, http_url, body):
resp = self.get(url=http_url)
assert resp['status'] == 200, 'status'
assert resp['body'] == body, 'body'
def test_static_types_basic(self, temp_dir):
self.action_update({"share": f'{temp_dir}/assets$uri'})
self.check_body('/index.html', 'index')
self.check_body('/file.xml', '.xml')
self.action_update(
{"share": f'{temp_dir}/assets$uri', "types": "application/xml"}
)
self.check_body('/file.xml', '.xml')
self.action_update(
{"share": f'{temp_dir}/assets$uri', "types": ["application/xml"]}
)
self.check_body('/file.xml', '.xml')
self.action_update({"share": f'{temp_dir}/assets$uri', "types": [""]})
assert self.get(url='/file.xml')['status'] == 403, 'no mtype'
def test_static_types_wildcard(self, temp_dir):
self.action_update(
{"share": f'{temp_dir}/assets$uri', "types": ["application/*"]}
)
self.check_body('/file.xml', '.xml')
assert self.get(url='/file.mp4')['status'] == 403, 'app * mtype mp4'
self.action_update(
{"share": f'{temp_dir}/assets$uri', "types": ["video/*"]}
)
assert self.get(url='/file.xml')['status'] == 403, 'video * mtype xml'
self.check_body('/file.mp4', '.mp4')
def test_static_types_negation(self, temp_dir):
self.action_update(
{"share": f'{temp_dir}/assets$uri', "types": ["!application/xml"]}
)
assert self.get(url='/file.xml')['status'] == 403, 'forbidden negation'
self.check_body('/file.mp4', '.mp4')
# sorting negation
self.action_update(
{ {
"share": f'{temp_dir}/assets$uri', "action": {
"types": ["!video/*", "image/png", "!image/jpg"], "share": f'{temp_dir}/assets$uri',
} "types": ["!application/x-httpd-php"],
) "fallback": {"proxy": "http://127.0.0.1:7081"},
assert self.get(url='/file.mp4')['status'] == 403, 'negation sort mp4' }
self.check_body('/file.png', '.png') },
assert self.get(url='/file.jpg')['status'] == 403, 'negation sort jpg' ],
'routes',
), 'configure fallback proxy route'
def test_static_types_regex(self, temp_dir): check_body('/file.php', '')
self.action_update( check_body('/file.mp4', '.mp4')
{
"share": f'{temp_dir}/assets$uri',
"types": ["~text/(html|plain)"],
}
)
assert self.get(url='/file.php')['status'] == 403, 'regex fail'
self.check_body('/file.html', '.html')
self.check_body('/file.txt', '.txt')
def test_static_types_case(self, temp_dir):
self.action_update(
{"share": f'{temp_dir}/assets$uri', "types": ["!APpliCaTiOn/xMl"]}
)
self.check_body('/file.mp4', '.mp4')
assert (
self.get(url='/file.xml')['status'] == 403
), 'mixed case xml negation'
self.action_update( def test_static_types_index(temp_dir):
{"share": f'{temp_dir}/assets$uri', "types": ["vIdEo/mp4"]} action_update(
) {"share": f'{temp_dir}/assets$uri', "types": "application/xml"}
assert self.get(url='/file.mp4')['status'] == 200, 'mixed case' )
assert ( check_body('/', 'index')
self.get(url='/file.xml')['status'] == 403 check_body('/file.xml', '.xml')
), 'mixed case video negation' assert client.get(url='/index.html')['status'] == 403, 'forbidden mtype'
assert client.get(url='/file.mp4')['status'] == 403, 'forbidden mtype'
self.action_update(
{"share": f'{temp_dir}/assets$uri', "types": ["vIdEo/*"]}
)
self.check_body('/file.mp4', '.mp4')
assert (
self.get(url='/file.xml')['status'] == 403
), 'mixed case video * negation'
def test_static_types_fallback(self, temp_dir): def test_static_types_custom_mime(temp_dir):
assert 'success' in self.conf( assert 'success' in client.conf(
[ {
{ "listeners": {"*:7080": {"pass": "routes"}},
"match": {"destination": "*:7081"}, "routes": [{"action": {"share": f'{temp_dir}/assets$uri'}}],
"action": {"return": 200}, "applications": {},
}, "settings": {
{ "http": {"static": {"mime_types": {"test/mime-type": ["file"]}}}
"action": { },
"share": f'{temp_dir}/assets$uri', }
"types": ["!application/x-httpd-php"], )
"fallback": {"proxy": "http://127.0.0.1:7081"},
}
},
],
'routes',
), 'configure fallback proxy route'
self.check_body('/file.php', '') action_update({"share": f'{temp_dir}/assets$uri', "types": [""]})
self.check_body('/file.mp4', '.mp4') assert client.get(url='/file')['status'] == 403, 'forbidden custom mime'
def test_static_types_index(self, temp_dir): action_update(
self.action_update( {"share": f'{temp_dir}/assets$uri', "types": ["test/mime-type"]}
{"share": f'{temp_dir}/assets$uri', "types": "application/xml"} )
) check_body('/file', '')
self.check_body('/', 'index')
self.check_body('/file.xml', '.xml')
assert self.get(url='/index.html')['status'] == 403, 'forbidden mtype'
assert self.get(url='/file.mp4')['status'] == 403, 'forbidden mtype'
def test_static_types_custom_mime(self, temp_dir):
self._load_conf(
{
"listeners": {"*:7080": {"pass": "routes"}},
"routes": [{"action": {"share": f'{temp_dir}/assets$uri'}}],
"applications": {},
"settings": {
"http": {
"static": {"mime_types": {"test/mime-type": ["file"]}}
}
},
}
)
self.action_update({"share": f'{temp_dir}/assets$uri', "types": [""]})
assert self.get(url='/file')['status'] == 403, 'forbidden custom mime'
self.action_update(
{"share": f'{temp_dir}/assets$uri', "types": ["test/mime-type"]}
)
self.check_body('/file', '')

View File

@@ -2,76 +2,82 @@ import os
from pathlib import Path from pathlib import Path
import pytest import pytest
from unit.applications.proto import TestApplicationProto from unit.applications.proto import ApplicationProto
client = ApplicationProto()
class TestStaticVariables(TestApplicationProto): @pytest.fixture(autouse=True)
@pytest.fixture(autouse=True) def setup_method_fixture(temp_dir):
def setup_method_fixture(self, temp_dir): os.makedirs(f'{temp_dir}/assets/dir')
os.makedirs(f'{temp_dir}/assets/dir') os.makedirs(f'{temp_dir}/assets/d$r')
os.makedirs(f'{temp_dir}/assets/d$r') Path(f'{temp_dir}/assets/index.html').write_text('0123456789')
Path(f'{temp_dir}/assets/index.html').write_text('0123456789') Path(f'{temp_dir}/assets/dir/file').write_text('file')
Path(f'{temp_dir}/assets/dir/file').write_text('file') Path(f'{temp_dir}/assets/d$r/file').write_text('d$r')
Path(f'{temp_dir}/assets/d$r/file').write_text('d$r')
self._load_conf( assert 'success' in client.conf(
{ {
"listeners": {"*:7080": {"pass": "routes"}}, "listeners": {"*:7080": {"pass": "routes"}},
"routes": [{"action": {"share": f'{temp_dir}/assets$uri'}}], "routes": [{"action": {"share": f'{temp_dir}/assets$uri'}}],
} }
) )
def update_share(self, share):
if isinstance(share, list):
return self.conf(share, 'routes/0/action/share')
return self.conf(f'"{share}"', 'routes/0/action/share') def update_share(share):
if isinstance(share, list):
return client.conf(share, 'routes/0/action/share')
def test_static_variables(self, temp_dir): return client.conf(f'"{share}"', 'routes/0/action/share')
assert self.get(url='/index.html')['status'] == 200
assert self.get(url='/d$r/file')['status'] == 200
assert 'success' in self.update_share('$uri')
assert self.get(url=f'{temp_dir}/assets/index.html')['status'] == 200
assert 'success' in self.update_share(f'{temp_dir}/assets${{uri}}') def test_static_variables(temp_dir):
assert self.get(url='/index.html')['status'] == 200 assert client.get(url='/index.html')['status'] == 200
assert client.get(url='/d$r/file')['status'] == 200
def test_static_variables_array(self, temp_dir): assert 'success' in update_share('$uri')
assert 'success' in self.update_share( assert client.get(url=f'{temp_dir}/assets/index.html')['status'] == 200
[f'{temp_dir}/assets$uri', '$uri']
)
assert self.get(url='/dir/file')['status'] == 200 assert 'success' in update_share(f'{temp_dir}/assets${{uri}}')
assert self.get(url=f'{temp_dir}/assets/index.html')['status'] == 200 assert client.get(url='/index.html')['status'] == 200
assert self.get(url='/blah')['status'] == 404
assert 'success' in self.conf(
{
"share": [f'{temp_dir}/assets$uri', '$uri'],
"fallback": {"return": 201},
},
'routes/0/action',
)
assert self.get(url='/dir/file')['status'] == 200 def test_static_variables_array(temp_dir):
assert self.get(url=f'{temp_dir}/assets/index.html')['status'] == 200 assert 'success' in update_share([f'{temp_dir}/assets$uri', '$uri'])
assert self.get(url='/dir/blah')['status'] == 201
def test_static_variables_buildin_start(self, temp_dir): assert client.get(url='/dir/file')['status'] == 200
assert 'success' in self.update_share('$uri/assets/index.html') assert client.get(url=f'{temp_dir}/assets/index.html')['status'] == 200
assert self.get(url=temp_dir)['status'] == 200 assert client.get(url='/blah')['status'] == 404
def test_static_variables_buildin_mid(self, temp_dir): assert 'success' in client.conf(
assert 'success' in self.update_share(f'{temp_dir}$uri/index.html') {
assert self.get(url='/assets')['status'] == 200 "share": [f'{temp_dir}/assets$uri', '$uri'],
"fallback": {"return": 201},
},
'routes/0/action',
)
def test_static_variables_buildin_end(self): assert client.get(url='/dir/file')['status'] == 200
assert self.get(url='/index.html')['status'] == 200 assert client.get(url=f'{temp_dir}/assets/index.html')['status'] == 200
assert client.get(url='/dir/blah')['status'] == 201
def test_static_variables_invalid(self, temp_dir):
assert 'error' in self.update_share(f'{temp_dir}/assets/d$r$uri') def test_static_variables_buildin_start(temp_dir):
assert 'error' in self.update_share(f'{temp_dir}/assets/$$uri') assert 'success' in update_share('$uri/assets/index.html')
assert 'error' in self.update_share( assert client.get(url=temp_dir)['status'] == 200
[f'{temp_dir}/assets$uri', f'{temp_dir}/assets/dir', '$$uri']
)
def test_static_variables_buildin_mid(temp_dir):
assert 'success' in update_share(f'{temp_dir}$uri/index.html')
assert client.get(url='/assets')['status'] == 200
def test_static_variables_buildin_end():
assert client.get(url='/index.html')['status'] == 200
def test_static_variables_invalid(temp_dir):
assert 'error' in update_share(f'{temp_dir}/assets/d$r$uri')
assert 'error' in update_share(f'{temp_dir}/assets/$$uri')
assert 'error' in update_share(
[f'{temp_dir}/assets$uri', f'{temp_dir}/assets/dir', '$$uri']
)

View File

@@ -1,75 +1,79 @@
import time import time
from unit.applications.lang.python import TestApplicationPython from unit.applications.lang.python import ApplicationPython
from unit.option import option from unit.option import option
from unit.status import Status from unit.status import Status
prerequisites = {'modules': {'python': 'any'}} prerequisites = {'modules': {'python': 'any'}}
client = ApplicationPython()
class TestStatus(TestApplicationPython):
def check_connections(self, accepted, active, idle, closed):
assert Status.get('/connections') == {
'accepted': accepted,
'active': active,
'idle': idle,
'closed': closed,
}
def app_default(self, name="empty", module="wsgi"): def check_connections(accepted, active, idle, closed):
name_dir = f'{option.test_dir}/python/{name}' assert Status.get('/connections') == {
return { 'accepted': accepted,
"type": self.get_application_type(), 'active': active,
"processes": {"spare": 0}, 'idle': idle,
"path": name_dir, 'closed': closed,
"working_directory": name_dir, }
"module": module,
}
def test_status(self):
assert 'error' in self.conf_delete('/status'), 'DELETE method'
def test_status_requests(self, skip_alert): def app_default(name="empty", module="wsgi"):
skip_alert(r'Python failed to import module "blah"') name_dir = f'{option.test_dir}/python/{name}'
return {
"type": client.get_application_type(),
"processes": {"spare": 0},
"path": name_dir,
"working_directory": name_dir,
"module": module,
}
assert 'success' in self.conf(
{ def test_status():
"listeners": { assert 'error' in client.conf_delete('/status'), 'DELETE method'
"*:7080": {"pass": "routes"},
"*:7081": {"pass": "applications/empty"},
"*:7082": {"pass": "applications/blah"}, def test_status_requests(skip_alert):
}, skip_alert(r'Python failed to import module "blah"')
"routes": [{"action": {"return": 200}}],
"applications": { assert 'success' in client.conf(
"empty": self.app_default(), {
"blah": { "listeners": {
"type": self.get_application_type(), "*:7080": {"pass": "routes"},
"processes": {"spare": 0}, "*:7081": {"pass": "applications/empty"},
"module": "blah", "*:7082": {"pass": "applications/blah"},
}, },
"routes": [{"action": {"return": 200}}],
"applications": {
"empty": app_default(),
"blah": {
"type": client.get_application_type(),
"processes": {"spare": 0},
"module": "blah",
}, },
}, },
) },
)
Status.init() Status.init()
assert self.get()['status'] == 200 assert client.get()['status'] == 200
assert Status.get('/requests/total') == 1, '2xx' assert Status.get('/requests/total') == 1, '2xx'
assert self.get(port=7081)['status'] == 200 assert client.get(port=7081)['status'] == 200
assert Status.get('/requests/total') == 2, '2xx app' assert Status.get('/requests/total') == 2, '2xx app'
assert ( assert (
self.get(headers={'Host': '/', 'Connection': 'close'})['status'] client.get(headers={'Host': '/', 'Connection': 'close'})['status']
== 400 == 400
) )
assert Status.get('/requests/total') == 3, '4xx' assert Status.get('/requests/total') == 3, '4xx'
assert self.get(port=7082)['status'] == 503 assert client.get(port=7082)['status'] == 503
assert Status.get('/requests/total') == 4, '5xx' assert Status.get('/requests/total') == 4, '5xx'
self.http( client.http(
b"""GET / HTTP/1.1 b"""GET / HTTP/1.1
Host: localhost Host: localhost
GET / HTTP/1.1 GET / HTTP/1.1
@@ -77,159 +81,162 @@ Host: localhost
Connection: close Connection: close
""", """,
raw=True, raw=True,
) )
assert Status.get('/requests/total') == 6, 'pipeline' assert Status.get('/requests/total') == 6, 'pipeline'
sock = self.get(port=7081, no_recv=True) sock = client.get(port=7081, no_recv=True)
time.sleep(1) time.sleep(1)
assert Status.get('/requests/total') == 7, 'no receive' assert Status.get('/requests/total') == 7, 'no receive'
sock.close() sock.close()
def test_status_connections(self):
assert 'success' in self.conf( def test_status_connections():
{ assert 'success' in client.conf(
"listeners": { {
"*:7080": {"pass": "routes"}, "listeners": {
"*:7081": {"pass": "applications/delayed"}, "*:7080": {"pass": "routes"},
}, "*:7081": {"pass": "applications/delayed"},
"routes": [{"action": {"return": 200}}],
"applications": {
"delayed": self.app_default("delayed"),
},
}, },
) "routes": [{"action": {"return": 200}}],
"applications": {
Status.init() "delayed": app_default("delayed"),
# accepted, closed
assert self.get()['status'] == 200
self.check_connections(1, 0, 0, 1)
# idle
(_, sock) = self.get(
headers={'Host': 'localhost', 'Connection': 'keep-alive'},
start=True,
read_timeout=1,
)
self.check_connections(2, 0, 1, 1)
self.get(sock=sock)
self.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,
)
self.check_connections(3, 1, 0, 2)
self.get(sock=sock) Status.init()
self.check_connections(3, 0, 0, 3)
def test_status_applications(self): # accepted, closed
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): assert client.get()['status'] == 200
assert Status.get(f'/applications/{name}') == { check_connections(1, 0, 0, 1)
'processes': {
'running': running,
'starting': starting,
'idle': idle,
},
'requests': {'active': active},
}
self.load('delayed') # idle
Status.init()
check_applications(['delayed']) (_, sock) = client.get(
check_application('delayed', 0, 0, 0, 0) headers={'Host': 'localhost', 'Connection': 'keep-alive'},
start=True,
read_timeout=1,
)
# idle check_connections(2, 0, 1, 1)
assert self.get()['status'] == 200 client.get(sock=sock)
check_application('delayed', 1, 0, 1, 0) check_connections(2, 0, 0, 2)
assert 'success' in self.conf('4', 'applications/delayed/processes') # active
check_application('delayed', 4, 0, 4, 0)
# active (_, sock) = client.get(
headers={
'Host': 'localhost',
'X-Delay': '2',
'Connection': 'close',
},
port=7081,
start=True,
read_timeout=1,
)
check_connections(3, 1, 0, 2)
(_, sock) = self.get( client.get(sock=sock)
headers={ check_connections(3, 0, 0, 3)
'Host': 'localhost',
'X-Delay': '2',
'Connection': 'close', def test_status_applications():
def check_applications(expert):
apps = list(client.conf_get('/status/applications').keys()).sort()
assert apps == expert.sort()
def check_application(name, running, starting, idle, active):
assert Status.get(f'/applications/{name}') == {
'processes': {
'running': running,
'starting': starting,
'idle': idle,
}, },
start=True, 'requests': {'active': active},
read_timeout=1, }
)
check_application('delayed', 4, 0, 3, 1)
sock.close()
# starting client.load('delayed')
Status.init()
assert 'success' in self.conf( check_applications(['delayed'])
{ check_application('delayed', 0, 0, 0, 0)
"listeners": {
"*:7080": {"pass": "applications/restart"}, # idle
"*:7081": {"pass": "applications/delayed"},
}, assert client.get()['status'] == 200
"routes": [], check_application('delayed', 1, 0, 1, 0)
"applications": {
"restart": self.app_default("restart", "longstart"), assert 'success' in client.conf('4', 'applications/delayed/processes')
"delayed": self.app_default("delayed"), check_application('delayed', 4, 0, 4, 0)
},
# active
(_, sock) = client.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 client.conf(
{
"listeners": {
"*:7080": {"pass": "applications/restart"},
"*:7081": {"pass": "applications/delayed"},
}, },
) "routes": [],
Status.init() "applications": {
"restart": app_default("restart", "longstart"),
check_applications(['delayed', 'restart']) "delayed": app_default("delayed"),
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)
def test_status_proxy(self):
assert 'success' in self.conf(
{
"listeners": {
"*:7080": {"pass": "routes"},
"*:7081": {"pass": "applications/empty"},
},
"routes": [
{
"match": {"uri": "/"},
"action": {"proxy": "http://127.0.0.1:7081"},
}
],
"applications": {
"empty": self.app_default(),
},
}, },
) },
)
Status.init()
Status.init() check_applications(['delayed', 'restart'])
check_application('restart', 0, 0, 0, 0)
check_application('delayed', 0, 0, 0, 0)
assert self.get()['status'] == 200 client.get(read_timeout=1)
self.check_connections(2, 0, 0, 2)
assert Status.get('/requests/total') == 2, 'proxy' check_application('restart', 0, 1, 0, 1)
check_application('delayed', 0, 0, 0, 0)
def test_status_proxy():
assert 'success' in client.conf(
{
"listeners": {
"*:7080": {"pass": "routes"},
"*:7081": {"pass": "applications/empty"},
},
"routes": [
{
"match": {"uri": "/"},
"action": {"proxy": "http://127.0.0.1:7081"},
}
],
"applications": {
"empty": app_default(),
},
},
)
Status.init()
assert client.get()['status'] == 200
check_connections(2, 0, 0, 2)
assert Status.get('/requests/total') == 2, 'proxy'

View File

@@ -1,30 +1,31 @@
from unit.applications.tls import TestApplicationTLS from unit.applications.tls import ApplicationTLS
from unit.status import Status from unit.status import Status
prerequisites = {'modules': {'openssl': 'any'}} prerequisites = {'modules': {'openssl': 'any'}}
client = ApplicationTLS()
class TestStatusTLS(TestApplicationTLS):
def test_status_tls_requests(self):
self.certificate()
assert 'success' in self.conf( def test_status_tls_requests():
{ client.certificate()
"listeners": {
"*:7080": {"pass": "routes"}, assert 'success' in client.conf(
"*:7081": { {
"pass": "routes", "listeners": {
"tls": {"certificate": "default"}, "*:7080": {"pass": "routes"},
}, "*:7081": {
"pass": "routes",
"tls": {"certificate": "default"},
}, },
"routes": [{"action": {"return": 200}}], },
"applications": {}, "routes": [{"action": {"return": 200}}],
} "applications": {},
) }
)
Status.init() Status.init()
assert self.get()['status'] == 200 assert client.get()['status'] == 200
assert self.get_ssl(port=7081)['status'] == 200 assert client.get_ssl(port=7081)['status'] == 200
assert Status.get('/requests/total') == 2 assert Status.get('/requests/total') == 2

File diff suppressed because it is too large Load Diff

View File

@@ -1,111 +1,114 @@
import ssl import ssl
import pytest import pytest
from unit.applications.tls import TestApplicationTLS from unit.applications.tls import ApplicationTLS
prerequisites = {'modules': {'openssl': 'any'}} prerequisites = {'modules': {'openssl': 'any'}}
client = ApplicationTLS()
class TestTLSConfCommand(TestApplicationTLS):
@pytest.fixture(autouse=True)
def setup_method_fixture(self):
self.certificate()
assert 'success' in self.conf( @pytest.fixture(autouse=True)
{ def setup_method_fixture():
"listeners": { client.certificate()
"*:7080": {
"pass": "routes",
"tls": {"certificate": "default"},
}
},
"routes": [{"action": {"return": 200}}],
"applications": {},
}
), 'load application configuration'
def test_tls_conf_command(self): assert 'success' in client.conf(
def check_no_connection(): {
try: "listeners": {
self.get_ssl() "*:7080": {
pytest.fail('Unexpected connection.') "pass": "routes",
"tls": {"certificate": "default"},
}
},
"routes": [{"action": {"return": 200}}],
"applications": {},
}
), 'load application configuration'
except (ssl.SSLError, ConnectionRefusedError):
pass
# Set one conf_commands (disable protocol). def test_tls_conf_command():
def check_no_connection():
try:
client.get_ssl()
pytest.fail('Unexpected connection.')
(_, sock) = self.get_ssl(start=True) except (ssl.SSLError, ConnectionRefusedError):
pass
# Set one conf_commands (disable protocol).
(_, sock) = client.get_ssl(start=True)
shared_ciphers = sock.shared_ciphers()
protocols = list(set(c[1] for c in shared_ciphers))
protocol = sock.cipher()[1]
if '/' in protocol:
pytest.skip('Complex protocol format.')
assert 'success' in client.conf(
{
"certificate": "default",
"conf_commands": {"protocol": f'-{protocol}'},
},
'listeners/*:7080/tls',
), 'protocol disabled'
sock.close()
if len(protocols) > 1:
(_, sock) = client.get_ssl(start=True)
cipher = sock.cipher()
assert cipher[1] != protocol, 'new protocol used'
shared_ciphers = sock.shared_ciphers() shared_ciphers = sock.shared_ciphers()
protocols = list(set(c[1] for c in shared_ciphers)) ciphers = list(set(c for c in shared_ciphers if c[1] == cipher[1]))
protocol = sock.cipher()[1]
if '/' in protocol: sock.close()
pytest.skip('Complex protocol format.') else:
check_no_connection()
pytest.skip('One TLS protocol available only.')
assert 'success' in self.conf( # Set two conf_commands (disable protocol and cipher).
{
"certificate": "default", assert 'success' in client.conf(
"conf_commands": {"protocol": f'-{protocol}'}, {
"certificate": "default",
"conf_commands": {
"protocol": f'-{protocol}',
"cipherstring": f"{cipher[1]}:!{cipher[0]}",
}, },
'listeners/*:7080/tls', },
), 'protocol disabled' 'listeners/*:7080/tls',
), 'cipher disabled'
if len(ciphers) > 1:
(_, sock) = client.get_ssl(start=True)
cipher_new = sock.cipher()
assert cipher_new[1] == cipher[1], 'previous protocol used'
assert cipher_new[0] != cipher[0], 'new cipher used'
sock.close() sock.close()
if len(protocols) > 1: else:
(_, sock) = self.get_ssl(start=True) check_no_connection()
cipher = sock.cipher()
assert cipher[1] != protocol, 'new protocol used'
shared_ciphers = sock.shared_ciphers() def test_tls_conf_command_invalid(skip_alert):
ciphers = list(set(c for c in shared_ciphers if c[1] == cipher[1])) skip_alert(r'SSL_CONF_cmd', r'failed to apply new conf')
sock.close() def check_conf_commands(conf_commands):
else: assert 'error' in client.conf(
check_no_connection() {"certificate": "default", "conf_commands": conf_commands},
pytest.skip('One TLS protocol available only.')
# Set two conf_commands (disable protocol and cipher).
assert 'success' in self.conf(
{
"certificate": "default",
"conf_commands": {
"protocol": f'-{protocol}',
"cipherstring": f"{cipher[1]}:!{cipher[0]}",
},
},
'listeners/*:7080/tls', 'listeners/*:7080/tls',
), 'cipher disabled' ), 'ivalid conf_commands'
if len(ciphers) > 1: check_conf_commands([])
(_, sock) = self.get_ssl(start=True) check_conf_commands("blah")
check_conf_commands({"": ""})
cipher_new = sock.cipher() check_conf_commands({"blah": ""})
assert cipher_new[1] == cipher[1], 'previous protocol used' check_conf_commands({"protocol": {}})
assert cipher_new[0] != cipher[0], 'new cipher used' check_conf_commands({"protocol": "blah"})
check_conf_commands({"protocol": "TLSv1.2", "blah": ""})
sock.close()
else:
check_no_connection()
def test_tls_conf_command_invalid(self, skip_alert):
skip_alert(r'SSL_CONF_cmd', r'failed to apply new conf')
def check_conf_commands(conf_commands):
assert 'error' in self.conf(
{"certificate": "default", "conf_commands": conf_commands},
'listeners/*:7080/tls',
), 'ivalid conf_commands'
check_conf_commands([])
check_conf_commands("blah")
check_conf_commands({"": ""})
check_conf_commands({"blah": ""})
check_conf_commands({"protocol": {}})
check_conf_commands({"protocol": "blah"})
check_conf_commands({"protocol": "TLSv1.2", "blah": ""})

View File

@@ -12,115 +12,121 @@ from OpenSSL.SSL import (
Connection, Connection,
_lib, _lib,
) )
from unit.applications.tls import TestApplicationTLS from unit.applications.tls import ApplicationTLS
prerequisites = {'modules': {'openssl': 'any'}} prerequisites = {'modules': {'openssl': 'any'}}
client = ApplicationTLS()
class TestTLSSession(TestApplicationTLS):
@pytest.fixture(autouse=True)
def setup_method_fixture(self):
self.certificate()
assert 'success' in self.conf( @pytest.fixture(autouse=True)
{ def setup_method_fixture():
"listeners": { client.certificate()
"*:7080": {
"pass": "routes",
"tls": {"certificate": "default", "session": {}},
}
},
"routes": [{"action": {"return": 200}}],
"applications": {},
}
), 'load application configuration'
def add_session(self, cache_size=None, timeout=None): assert 'success' in client.conf(
session = {} {
"listeners": {
"*:7080": {
"pass": "routes",
"tls": {"certificate": "default", "session": {}},
}
},
"routes": [{"action": {"return": 200}}],
"applications": {},
}
), 'load application configuration'
if cache_size is not None:
session['cache_size'] = cache_size
if timeout is not None:
session['timeout'] = timeout
return self.conf(session, 'listeners/*:7080/tls/session') def add_session(cache_size=None, timeout=None):
session = {}
def connect(self, ctx=None, session=None): if cache_size is not None:
sock = socket.create_connection(('127.0.0.1', 7080)) session['cache_size'] = cache_size
if timeout is not None:
session['timeout'] = timeout
if ctx is None: return client.conf(session, 'listeners/*:7080/tls/session')
ctx = Context(TLSv1_2_METHOD)
ctx.set_session_cache_mode(SESS_CACHE_CLIENT)
ctx.set_options(OP_NO_TICKET)
client = Connection(ctx, sock)
client.set_connect_state()
if session is not None: def connect(ctx=None, session=None):
client.set_session(session) sock = socket.create_connection(('127.0.0.1', 7080))
client.do_handshake() if ctx is None:
client.shutdown() ctx = Context(TLSv1_2_METHOD)
ctx.set_session_cache_mode(SESS_CACHE_CLIENT)
ctx.set_options(OP_NO_TICKET)
return ( conn = Connection(ctx, sock)
client, conn.set_connect_state()
client.get_session(),
ctx,
_lib.SSL_session_reused(client._ssl),
)
def test_tls_session(self): if session is not None:
_, sess, ctx, reused = self.connect() conn.set_session(session)
assert not reused, 'new connection'
_, _, _, reused = self.connect(ctx, sess) conn.do_handshake()
assert not reused, 'no cache' conn.shutdown()
assert 'success' in self.add_session(cache_size=2) return (
conn,
conn.get_session(),
ctx,
_lib.SSL_session_reused(conn._ssl),
)
_, sess, ctx, reused = self.connect()
assert not reused, 'new connection cache'
_, _, _, reused = self.connect(ctx, sess) def test_tls_session():
assert reused, 'cache' _, sess, ctx, reused = connect()
assert not reused, 'new connection'
_, _, _, reused = self.connect(ctx, sess) _, _, _, reused = connect(ctx, sess)
assert reused, 'cache 2' assert not reused, 'no cache'
# check that at least one session of four is not reused assert 'success' in add_session(cache_size=2)
clients = [self.connect() for _ in range(4)] _, sess, ctx, reused = connect()
assert True not in [c[-1] for c in clients], 'cache small all new' assert not reused, 'new connection cache'
clients_again = [self.connect(c[2], c[1]) for c in clients] _, _, _, reused = connect(ctx, sess)
assert False in [c[-1] for c in clients_again], 'cache small no reuse' assert reused, 'cache'
# all four sessions are reused _, _, _, reused = connect(ctx, sess)
assert reused, 'cache 2'
assert 'success' in self.add_session(cache_size=8) # check that at least one session of four is not reused
clients = [self.connect() for _ in range(4)] conns = [connect() for _ in range(4)]
assert True not in [c[-1] for c in clients], 'cache big all new' assert True not in [c[-1] for c in conns], 'cache small all new'
clients_again = [self.connect(c[2], c[1]) for c in clients] conns_again = [connect(c[2], c[1]) for c in conns]
assert False not in [c[-1] for c in clients_again], 'cache big reuse' assert False in [c[-1] for c in conns_again], 'cache small no reuse'
def test_tls_session_timeout(self): # all four sessions are reused
assert 'success' in self.add_session(cache_size=5, timeout=1)
_, sess, ctx, reused = self.connect() assert 'success' in add_session(cache_size=8)
assert not reused, 'new connection'
_, _, _, reused = self.connect(ctx, sess) conns = [connect() for _ in range(4)]
assert reused, 'no timeout' assert True not in [c[-1] for c in conns], 'cache big all new'
time.sleep(3) conns_again = [connect(c[2], c[1]) for c in conns]
assert False not in [c[-1] for c in conns_again], 'cache big reuse'
_, _, _, reused = self.connect(ctx, sess)
assert not reused, 'timeout'
def test_tls_session_invalid(self): def test_tls_session_timeout():
assert 'error' in self.add_session(cache_size=-1) assert 'success' in add_session(cache_size=5, timeout=1)
assert 'error' in self.add_session(cache_size={})
assert 'error' in self.add_session(timeout=-1) _, sess, ctx, reused = connect()
assert 'error' in self.add_session(timeout={}) assert not reused, 'new connection'
_, _, _, reused = connect(ctx, sess)
assert reused, 'no timeout'
time.sleep(3)
_, _, _, reused = connect(ctx, sess)
assert not reused, 'timeout'
def test_tls_session_invalid():
assert 'error' in add_session(cache_size=-1)
assert 'error' in add_session(cache_size={})
assert 'error' in add_session(timeout=-1)
assert 'error' in add_session(timeout={})

View File

@@ -2,36 +2,111 @@ import ssl
import subprocess import subprocess
import pytest import pytest
from unit.applications.tls import TestApplicationTLS from unit.applications.tls import ApplicationTLS
from unit.option import option from unit.option import option
prerequisites = {'modules': {'openssl': 'any'}} prerequisites = {'modules': {'openssl': 'any'}}
client = ApplicationTLS()
class TestTLSSNI(TestApplicationTLS):
@pytest.fixture(autouse=True) @pytest.fixture(autouse=True)
def setup_method_fixture(self): def setup_method_fixture():
self._load_conf( assert 'success' in client.conf(
{ {
"listeners": {"*:7080": {"pass": "routes"}}, "listeners": {"*:7080": {"pass": "routes"}},
"routes": [{"action": {"return": 200}}], "routes": [{"action": {"return": 200}}],
"applications": {}, "applications": {},
} }
)
def add_tls(cert='default'):
assert 'success' in client.conf(
{"pass": "routes", "tls": {"certificate": cert}},
'listeners/*:7080',
)
def check_cert(host, expect, ctx):
resp, sock = client.get_ssl(
headers={
'Host': host,
'Content-Length': '0',
'Connection': 'close',
},
start=True,
context=ctx,
)
assert resp['status'] == 200
assert sock.getpeercert()['subject'][0][0][1] == expect
def config_bundles(bundles):
client.certificate('root', False)
for b in bundles:
client.openssl_conf(rewrite=True, alt_names=bundles[b]['alt_names'])
subj = f'/CN={bundles[b]["subj"]}/' if 'subj' in bundles[b] else '/'
subprocess.check_output(
[
'openssl',
'req',
'-new',
'-subj',
subj,
'-config',
f'{option.temp_dir}/openssl.conf',
'-out',
f'{option.temp_dir}/{b}.csr',
'-keyout',
f'{option.temp_dir}/{b}.key',
],
stderr=subprocess.STDOUT,
) )
def add_tls(self, cert='default'): generate_ca_conf()
assert 'success' in self.conf(
{"pass": "routes", "tls": {"certificate": cert}}, for b in bundles:
'listeners/*:7080', subj = f'/CN={bundles[b]["subj"]}/' if 'subj' in bundles[b] else '/'
subprocess.check_output(
[
'openssl',
'ca',
'-batch',
'-subj',
subj,
'-config',
f'{option.temp_dir}/ca.conf',
'-keyfile',
f'{option.temp_dir}/root.key',
'-cert',
f'{option.temp_dir}/root.crt',
'-in',
f'{option.temp_dir}/{b}.csr',
'-out',
f'{option.temp_dir}/{b}.crt',
],
stderr=subprocess.STDOUT,
) )
def remove_tls(self): load_certs(bundles)
assert 'success' in self.conf({"pass": "routes"}, 'listeners/*:7080')
def generate_ca_conf(self): context = ssl.create_default_context()
with open(f'{option.temp_dir}/ca.conf', 'w') as f: context.check_hostname = False
f.write( context.verify_mode = ssl.CERT_REQUIRED
f"""[ ca ] context.load_verify_locations(f'{option.temp_dir}/root.crt')
return context
def generate_ca_conf():
with open(f'{option.temp_dir}/ca.conf', 'w') as f:
f.write(
f"""[ ca ]
default_ca = myca default_ca = myca
[ myca ] [ myca ]
@@ -49,231 +124,177 @@ commonName = optional
[ myca_extensions ] [ myca_extensions ]
basicConstraints = critical,CA:TRUE""" basicConstraints = critical,CA:TRUE"""
)
with open(f'{option.temp_dir}/certserial', 'w') as f:
f.write('1000')
with open(f'{option.temp_dir}/certindex', 'w') as f:
f.write('')
def config_bundles(self, bundles):
self.certificate('root', False)
for b in bundles:
self.openssl_conf(rewrite=True, alt_names=bundles[b]['alt_names'])
subj = f'/CN={bundles[b]["subj"]}/' if 'subj' in bundles[b] else '/'
subprocess.check_output(
[
'openssl',
'req',
'-new',
'-subj',
subj,
'-config',
f'{option.temp_dir}/openssl.conf',
'-out',
f'{option.temp_dir}/{b}.csr',
'-keyout',
f'{option.temp_dir}/{b}.key',
],
stderr=subprocess.STDOUT,
)
self.generate_ca_conf()
for b in bundles:
subj = f'/CN={bundles[b]["subj"]}/' if 'subj' in bundles[b] else '/'
subprocess.check_output(
[
'openssl',
'ca',
'-batch',
'-subj',
subj,
'-config',
f'{option.temp_dir}/ca.conf',
'-keyfile',
f'{option.temp_dir}/root.key',
'-cert',
f'{option.temp_dir}/root.crt',
'-in',
f'{option.temp_dir}/{b}.csr',
'-out',
f'{option.temp_dir}/{b}.crt',
],
stderr=subprocess.STDOUT,
)
self.context = ssl.create_default_context()
self.context.check_hostname = False
self.context.verify_mode = ssl.CERT_REQUIRED
self.context.load_verify_locations(f'{option.temp_dir}/root.crt')
self.load_certs(bundles)
def load_certs(self, bundles):
for bname, bvalue in bundles.items():
assert 'success' in self.certificate_load(
bname, bname
), f'certificate {bvalue["subj"]} upload'
def check_cert(self, host, expect):
resp, sock = self.get_ssl(
headers={
'Host': host,
'Content-Length': '0',
'Connection': 'close',
},
start=True,
) )
assert resp['status'] == 200 with open(f'{option.temp_dir}/certserial', 'w') as f:
assert sock.getpeercert()['subject'][0][0][1] == expect f.write('1000')
def test_tls_sni(self): with open(f'{option.temp_dir}/certindex', 'w') as f:
bundles = { f.write('')
"default": {"subj": "default", "alt_names": ["default"]},
"localhost.com": {
"subj": "localhost.com", def load_certs(bundles):
"alt_names": ["alt1.localhost.com"], for bname, bvalue in bundles.items():
}, assert 'success' in client.certificate_load(
"example.com": { bname, bname
"subj": "example.com", ), f'certificate {bvalue["subj"]} upload'
"alt_names": ["alt1.example.com", "alt2.example.com"],
},
def remove_tls():
assert 'success' in client.conf({"pass": "routes"}, 'listeners/*:7080')
def test_tls_sni():
bundles = {
"default": {"subj": "default", "alt_names": ["default"]},
"localhost.com": {
"subj": "localhost.com",
"alt_names": ["alt1.localhost.com"],
},
"example.com": {
"subj": "example.com",
"alt_names": ["alt1.example.com", "alt2.example.com"],
},
}
ctx = config_bundles(bundles)
add_tls(["default", "localhost.com", "example.com"])
check_cert('alt1.localhost.com', bundles['localhost.com']['subj'], ctx)
check_cert('alt2.example.com', bundles['example.com']['subj'], ctx)
check_cert('blah', bundles['default']['subj'], ctx)
def test_tls_sni_no_hostname():
bundles = {
"localhost.com": {"subj": "localhost.com", "alt_names": []},
"example.com": {
"subj": "example.com",
"alt_names": ["example.com"],
},
}
ctx = config_bundles(bundles)
add_tls(["localhost.com", "example.com"])
resp, sock = client.get_ssl(
headers={'Content-Length': '0', 'Connection': 'close'},
start=True,
context=ctx,
)
assert resp['status'] == 200
assert (
sock.getpeercert()['subject'][0][0][1]
== bundles['localhost.com']['subj']
)
def test_tls_sni_upper_case():
bundles = {
"localhost.com": {"subj": "LOCALHOST.COM", "alt_names": []},
"example.com": {
"subj": "example.com",
"alt_names": ["ALT1.EXAMPLE.COM", "*.ALT2.EXAMPLE.COM"],
},
}
ctx = config_bundles(bundles)
add_tls(["localhost.com", "example.com"])
check_cert('localhost.com', bundles['localhost.com']['subj'], ctx)
check_cert('LOCALHOST.COM', bundles['localhost.com']['subj'], ctx)
check_cert('EXAMPLE.COM', bundles['localhost.com']['subj'], ctx)
check_cert('ALT1.EXAMPLE.COM', bundles['example.com']['subj'], ctx)
check_cert('WWW.ALT2.EXAMPLE.COM', bundles['example.com']['subj'], ctx)
def test_tls_sni_only_bundle():
bundles = {
"localhost.com": {
"subj": "localhost.com",
"alt_names": ["alt1.localhost.com", "alt2.localhost.com"],
} }
self.config_bundles(bundles) }
self.add_tls(["default", "localhost.com", "example.com"]) ctx = config_bundles(bundles)
add_tls(["localhost.com"])
self.check_cert('alt1.localhost.com', bundles['localhost.com']['subj']) check_cert('domain.com', bundles['localhost.com']['subj'], ctx)
self.check_cert('alt2.example.com', bundles['example.com']['subj']) check_cert('alt1.domain.com', bundles['localhost.com']['subj'], ctx)
self.check_cert('blah', bundles['default']['subj'])
def test_tls_sni_no_hostname(self):
bundles = { def test_tls_sni_wildcard():
"localhost.com": {"subj": "localhost.com", "alt_names": []}, bundles = {
"example.com": { "localhost.com": {"subj": "localhost.com", "alt_names": []},
"subj": "example.com", "example.com": {
"alt_names": ["example.com"], "subj": "example.com",
}, "alt_names": ["*.example.com", "*.alt.example.com"],
},
}
ctx = config_bundles(bundles)
add_tls(["localhost.com", "example.com"])
check_cert('example.com', bundles['localhost.com']['subj'], ctx)
check_cert('www.example.com', bundles['example.com']['subj'], ctx)
check_cert('alt.example.com', bundles['example.com']['subj'], ctx)
check_cert('www.alt.example.com', bundles['example.com']['subj'], ctx)
check_cert('www.alt.example.ru', bundles['localhost.com']['subj'], ctx)
def test_tls_sni_duplicated_bundle():
bundles = {
"localhost.com": {
"subj": "localhost.com",
"alt_names": ["localhost.com", "alt2.localhost.com"],
} }
self.config_bundles(bundles) }
self.add_tls(["localhost.com", "example.com"]) ctx = config_bundles(bundles)
add_tls(["localhost.com", "localhost.com"])
resp, sock = self.get_ssl( check_cert('localhost.com', bundles['localhost.com']['subj'], ctx)
headers={'Content-Length': '0', 'Connection': 'close'}, check_cert('alt2.localhost.com', bundles['localhost.com']['subj'], ctx)
start=True,
)
assert resp['status'] == 200 def test_tls_sni_same_alt():
assert ( bundles = {
sock.getpeercert()['subject'][0][0][1] "localhost": {"subj": "subj1", "alt_names": "same.altname.com"},
== bundles['localhost.com']['subj'] "example": {"subj": "subj2", "alt_names": "same.altname.com"},
}
ctx = config_bundles(bundles)
add_tls(["localhost", "example"])
check_cert('localhost', bundles['localhost']['subj'], ctx)
check_cert('example', bundles['localhost']['subj'], ctx)
def test_tls_sni_empty_cn():
bundles = {"localhost": {"alt_names": ["alt.localhost.com"]}}
ctx = config_bundles(bundles)
add_tls(["localhost"])
resp, sock = client.get_ssl(
headers={
'Host': 'domain.com',
'Content-Length': '0',
'Connection': 'close',
},
start=True,
context=ctx,
)
assert resp['status'] == 200
assert sock.getpeercert()['subjectAltName'][0][1] == 'alt.localhost.com'
def test_tls_sni_invalid():
_ = config_bundles({"localhost": {"subj": "subj1", "alt_names": ''}})
add_tls(["localhost"])
def check_certificate(cert):
assert 'error' in client.conf(
{"pass": "routes", "tls": {"certificate": cert}},
'listeners/*:7080',
) )
def test_tls_sni_upper_case(self): check_certificate('')
bundles = { check_certificate('blah')
"localhost.com": {"subj": "LOCALHOST.COM", "alt_names": []}, check_certificate([])
"example.com": { check_certificate(['blah'])
"subj": "example.com", check_certificate(['localhost', 'blah'])
"alt_names": ["ALT1.EXAMPLE.COM", "*.ALT2.EXAMPLE.COM"], check_certificate(['localhost', []])
},
}
self.config_bundles(bundles)
self.add_tls(["localhost.com", "example.com"])
self.check_cert('localhost.com', bundles['localhost.com']['subj'])
self.check_cert('LOCALHOST.COM', bundles['localhost.com']['subj'])
self.check_cert('EXAMPLE.COM', bundles['localhost.com']['subj'])
self.check_cert('ALT1.EXAMPLE.COM', bundles['example.com']['subj'])
self.check_cert('WWW.ALT2.EXAMPLE.COM', bundles['example.com']['subj'])
def test_tls_sni_only_bundle(self):
bundles = {
"localhost.com": {
"subj": "localhost.com",
"alt_names": ["alt1.localhost.com", "alt2.localhost.com"],
}
}
self.config_bundles(bundles)
self.add_tls(["localhost.com"])
self.check_cert('domain.com', bundles['localhost.com']['subj'])
self.check_cert('alt1.domain.com', bundles['localhost.com']['subj'])
def test_tls_sni_wildcard(self):
bundles = {
"localhost.com": {"subj": "localhost.com", "alt_names": []},
"example.com": {
"subj": "example.com",
"alt_names": ["*.example.com", "*.alt.example.com"],
},
}
self.config_bundles(bundles)
self.add_tls(["localhost.com", "example.com"])
self.check_cert('example.com', bundles['localhost.com']['subj'])
self.check_cert('www.example.com', bundles['example.com']['subj'])
self.check_cert('alt.example.com', bundles['example.com']['subj'])
self.check_cert('www.alt.example.com', bundles['example.com']['subj'])
self.check_cert('www.alt.example.ru', bundles['localhost.com']['subj'])
def test_tls_sni_duplicated_bundle(self):
bundles = {
"localhost.com": {
"subj": "localhost.com",
"alt_names": ["localhost.com", "alt2.localhost.com"],
}
}
self.config_bundles(bundles)
self.add_tls(["localhost.com", "localhost.com"])
self.check_cert('localhost.com', bundles['localhost.com']['subj'])
self.check_cert('alt2.localhost.com', bundles['localhost.com']['subj'])
def test_tls_sni_same_alt(self):
bundles = {
"localhost": {"subj": "subj1", "alt_names": "same.altname.com"},
"example": {"subj": "subj2", "alt_names": "same.altname.com"},
}
self.config_bundles(bundles)
self.add_tls(["localhost", "example"])
self.check_cert('localhost', bundles['localhost']['subj'])
self.check_cert('example', bundles['localhost']['subj'])
def test_tls_sni_empty_cn(self):
bundles = {"localhost": {"alt_names": ["alt.localhost.com"]}}
self.config_bundles(bundles)
self.add_tls(["localhost"])
resp, sock = self.get_ssl(
headers={
'Host': 'domain.com',
'Content-Length': '0',
'Connection': 'close',
},
start=True,
)
assert resp['status'] == 200
assert sock.getpeercert()['subjectAltName'][0][1] == 'alt.localhost.com'
def test_tls_sni_invalid(self):
self.config_bundles({"localhost": {"subj": "subj1", "alt_names": ''}})
self.add_tls(["localhost"])
def check_certificate(cert):
assert 'error' in self.conf(
{"pass": "routes", "tls": {"certificate": cert}},
'listeners/*:7080',
)
check_certificate('')
check_certificate('blah')
check_certificate([])
check_certificate(['blah'])
check_certificate(['localhost', 'blah'])
check_certificate(['localhost', []])

View File

@@ -9,186 +9,194 @@ from OpenSSL.SSL import (
Connection, Connection,
_lib, _lib,
) )
from unit.applications.tls import TestApplicationTLS from unit.applications.tls import ApplicationTLS
prerequisites = {'modules': {'openssl': 'any'}} prerequisites = {'modules': {'openssl': 'any'}}
client = ApplicationTLS()
class TestTLSTicket(TestApplicationTLS): TICKET = 'U1oDTh11mMxODuw12gS0EXX1E/PkZG13cJNQ6m5+6BGlfPTjNlIEw7PSVU3X1gTE'
ticket = 'U1oDTh11mMxODuw12gS0EXX1E/PkZG13cJNQ6m5+6BGlfPTjNlIEw7PSVU3X1gTE' TICKET2 = '5AV0DSYIYbZWZQB7fCnTHZmMxtotb/aXjam+n2XS79lTvX3Tq9xGqpC8XKNEF2lt'
ticket2 = '5AV0DSYIYbZWZQB7fCnTHZmMxtotb/aXjam+n2XS79lTvX3Tq9xGqpC8XKNEF2lt' TICKET80 = '6Pfil8lv/k8zf8MndPpfXaO5EAV6dhME6zs6CfUyq2yziynQwSywtKQMqHGnJ2HR\
ticket80 = '6Pfil8lv/k8zf8MndPpfXaO5EAV6dhME6zs6CfUyq2yziynQwSywtKQMqHGnJ2HR\
49TZXi/Y4/8RSIO7QPsU51/HLR1gWIMhVM2m9yh93Bw=' 49TZXi/Y4/8RSIO7QPsU51/HLR1gWIMhVM2m9yh93Bw='
@pytest.fixture(autouse=True)
def setup_method_fixture(self):
self.certificate()
listener_conf = { @pytest.fixture(autouse=True)
"pass": "routes", def setup_method_fixture():
"tls": { client.certificate()
"certificate": "default",
"session": {"cache_size": 0, "tickets": True}, listener_conf = {
"pass": "routes",
"tls": {
"certificate": "default",
"session": {"cache_size": 0, "tickets": True},
},
}
assert 'success' in client.conf(
{
"listeners": {
"*:7080": listener_conf,
"*:7081": listener_conf,
"*:7082": listener_conf,
}, },
"routes": [{"action": {"return": 200}}],
"applications": {},
} }
), 'load application configuration'
assert 'success' in self.conf(
{
"listeners": {
"*:7080": listener_conf,
"*:7081": listener_conf,
"*:7082": listener_conf,
},
"routes": [{"action": {"return": 200}}],
"applications": {},
}
), 'load application configuration'
def set_tickets(self, tickets=True, port=7080): def connect(ctx=None, session=None, port=7080):
assert 'success' in self.conf( sock = socket.create_connection(('127.0.0.1', port))
{"cache_size": 0, "tickets": tickets},
f'listeners/*:{port}/tls/session', if ctx is None:
ctx = Context(TLSv1_2_METHOD)
conn = Connection(ctx, sock)
conn.set_connect_state()
if session is not None:
conn.set_session(session)
conn.do_handshake()
conn.shutdown()
return (
conn.get_session(),
ctx,
_lib.SSL_session_reused(conn._ssl),
)
def has_ticket(sess):
return _lib.SSL_SESSION_has_ticket(sess._session)
def set_tickets(tickets=True, port=7080):
assert 'success' in client.conf(
{"cache_size": 0, "tickets": tickets},
f'listeners/*:{port}/tls/session',
)
@pytest.mark.skipif(
not hasattr(_lib, 'SSL_SESSION_has_ticket'),
reason='ticket check is not supported',
)
def test_tls_ticket():
sess, ctx, reused = connect()
assert has_ticket(sess), 'tickets True'
assert not reused, 'tickets True not reused'
sess, ctx, reused = connect(ctx, sess)
assert has_ticket(sess), 'tickets True reconnect'
assert reused, 'tickets True reused'
set_tickets(tickets=False)
sess, _, _ = connect()
assert not has_ticket(sess), 'tickets False'
assert 'success' in client.conf_delete(
'listeners/*:7080/tls/session/tickets'
), 'tickets default configure'
sess, _, _ = connect()
assert not has_ticket(sess), 'tickets default (false)'
@pytest.mark.skipif(
not hasattr(_lib, 'SSL_SESSION_has_ticket'),
reason='ticket check is not supported',
)
def test_tls_ticket_string():
set_tickets(TICKET)
sess, ctx, _ = connect()
assert has_ticket(sess), 'tickets string'
sess2, _, reused = connect(ctx, sess)
assert has_ticket(sess2), 'tickets string reconnect'
assert reused, 'tickets string reused'
sess2, _, reused = connect(ctx, sess, port=7081)
assert has_ticket(sess2), 'connect True'
assert not reused, 'connect True not reused'
set_tickets(TICKET2, port=7081)
sess2, _, reused = connect(ctx, sess, port=7081)
assert has_ticket(sess2), 'wrong ticket'
assert not reused, 'wrong ticket not reused'
set_tickets(TICKET80)
sess, ctx, _ = connect()
assert has_ticket(sess), 'tickets string 80'
sess2, _, reused = connect(ctx, sess)
assert has_ticket(sess2), 'tickets string 80 reconnect'
assert reused, 'tickets string 80 reused'
sess2, _, reused = connect(ctx, sess, port=7081)
assert has_ticket(sess2), 'wrong ticket 80'
assert not reused, 'wrong ticket 80 not reused'
@pytest.mark.skipif(
not hasattr(_lib, 'SSL_SESSION_has_ticket'),
reason='ticket check is not supported',
)
def test_tls_ticket_array():
set_tickets([])
sess, ctx, _ = connect()
assert not has_ticket(sess), 'tickets array empty'
set_tickets([TICKET, TICKET2])
set_tickets(TICKET, port=7081)
set_tickets(TICKET2, port=7082)
sess, ctx, _ = connect()
_, _, reused = connect(ctx, sess, port=7081)
assert not reused, 'not last ticket'
_, _, reused = connect(ctx, sess, port=7082)
assert reused, 'last ticket'
sess, ctx, _ = connect(port=7081)
_, _, reused = connect(ctx, sess)
assert reused, 'first ticket'
sess, ctx, _ = connect(port=7082)
_, _, reused = connect(ctx, sess)
assert reused, 'second ticket'
assert 'success' in client.conf_delete(
'listeners/*:7080/tls/session/tickets/0'
), 'removed first ticket'
assert 'success' in client.conf_post(
f'"{TICKET}"', 'listeners/*:7080/tls/session/tickets'
), 'add new ticket to the end of array'
sess, ctx, _ = connect()
_, _, reused = connect(ctx, sess, port=7082)
assert not reused, 'not last ticket 2'
_, _, reused = connect(ctx, sess, port=7081)
assert reused, 'last ticket 2'
def test_tls_ticket_invalid():
def check_tickets(tickets):
assert 'error' in client.conf(
{"tickets": tickets},
'listeners/*:7080/tls/session',
) )
def connect(self, ctx=None, session=None, port=7080): check_tickets({})
sock = socket.create_connection(('127.0.0.1', port)) check_tickets('!?&^' * 16)
check_tickets(f'{TICKET[:-2]}!{TICKET[3:]}')
if ctx is None: check_tickets(TICKET[:-1])
ctx = Context(TLSv1_2_METHOD) check_tickets(f'{TICKET}b')
check_tickets(f'{TICKET}blah')
client = Connection(ctx, sock) check_tickets([True, TICKET, TICKET2])
client.set_connect_state() check_tickets([TICKET, 'blah', TICKET2])
check_tickets([TICKET, TICKET2, []])
if session is not None:
client.set_session(session)
client.do_handshake()
client.shutdown()
return (
client.get_session(),
ctx,
_lib.SSL_session_reused(client._ssl),
)
def has_ticket(self, sess):
return _lib.SSL_SESSION_has_ticket(sess._session)
@pytest.mark.skipif(
not hasattr(_lib, 'SSL_SESSION_has_ticket'),
reason='ticket check is not supported',
)
def test_tls_ticket(self):
sess, ctx, reused = self.connect()
assert self.has_ticket(sess), 'tickets True'
assert not reused, 'tickets True not reused'
sess, ctx, reused = self.connect(ctx, sess)
assert self.has_ticket(sess), 'tickets True reconnect'
assert reused, 'tickets True reused'
self.set_tickets(tickets=False)
sess, _, _ = self.connect()
assert not self.has_ticket(sess), 'tickets False'
assert 'success' in self.conf_delete(
'listeners/*:7080/tls/session/tickets'
), 'tickets default configure'
sess, _, _ = self.connect()
assert not self.has_ticket(sess), 'tickets default (false)'
@pytest.mark.skipif(
not hasattr(_lib, 'SSL_SESSION_has_ticket'),
reason='ticket check is not supported',
)
def test_tls_ticket_string(self):
self.set_tickets(self.ticket)
sess, ctx, _ = self.connect()
assert self.has_ticket(sess), 'tickets string'
sess2, _, reused = self.connect(ctx, sess)
assert self.has_ticket(sess2), 'tickets string reconnect'
assert reused, 'tickets string reused'
sess2, _, reused = self.connect(ctx, sess, port=7081)
assert self.has_ticket(sess2), 'connect True'
assert not reused, 'connect True not reused'
self.set_tickets(self.ticket2, port=7081)
sess2, _, reused = self.connect(ctx, sess, port=7081)
assert self.has_ticket(sess2), 'wrong ticket'
assert not reused, 'wrong ticket not reused'
self.set_tickets(self.ticket80)
sess, ctx, _ = self.connect()
assert self.has_ticket(sess), 'tickets string 80'
sess2, _, reused = self.connect(ctx, sess)
assert self.has_ticket(sess2), 'tickets string 80 reconnect'
assert reused, 'tickets string 80 reused'
sess2, _, reused = self.connect(ctx, sess, port=7081)
assert self.has_ticket(sess2), 'wrong ticket 80'
assert not reused, 'wrong ticket 80 not reused'
@pytest.mark.skipif(
not hasattr(_lib, 'SSL_SESSION_has_ticket'),
reason='ticket check is not supported',
)
def test_tls_ticket_array(self):
self.set_tickets([])
sess, ctx, _ = self.connect()
assert not self.has_ticket(sess), 'tickets array empty'
self.set_tickets([self.ticket, self.ticket2])
self.set_tickets(self.ticket, port=7081)
self.set_tickets(self.ticket2, port=7082)
sess, ctx, _ = self.connect()
_, _, reused = self.connect(ctx, sess, port=7081)
assert not reused, 'not last ticket'
_, _, reused = self.connect(ctx, sess, port=7082)
assert reused, 'last ticket'
sess, ctx, _ = self.connect(port=7081)
_, _, reused = self.connect(ctx, sess)
assert reused, 'first ticket'
sess, ctx, _ = self.connect(port=7082)
_, _, reused = self.connect(ctx, sess)
assert reused, 'second ticket'
assert 'success' in self.conf_delete(
'listeners/*:7080/tls/session/tickets/0'
), 'removed first ticket'
assert 'success' in self.conf_post(
f'"{self.ticket}"', 'listeners/*:7080/tls/session/tickets'
), 'add new ticket to the end of array'
sess, ctx, _ = self.connect()
_, _, reused = self.connect(ctx, sess, port=7082)
assert not reused, 'not last ticket 2'
_, _, reused = self.connect(ctx, sess, port=7081)
assert reused, 'last ticket 2'
def test_tls_ticket_invalid(self):
def check_tickets(tickets):
assert 'error' in self.conf(
{"tickets": tickets},
'listeners/*:7080/tls/session',
)
check_tickets({})
check_tickets('!?&^' * 16)
check_tickets(f'{self.ticket[:-2]}!{self.ticket[3:]}')
check_tickets(self.ticket[:-1])
check_tickets(f'{self.ticket}b')
check_tickets(f'{self.ticket}blah')
check_tickets([True, self.ticket, self.ticket2])
check_tickets([self.ticket, 'blah', self.ticket2])
check_tickets([self.ticket, self.ticket2, []])

View File

@@ -1,4 +1,4 @@
from unit.applications.lang.python import TestApplicationPython from unit.applications.lang.python import ApplicationPython
from unit.option import option from unit.option import option
prerequisites = { prerequisites = {
@@ -6,104 +6,100 @@ prerequisites = {
'features': {'unix_abstract': True}, 'features': {'unix_abstract': True},
} }
client = ApplicationPython()
class TestUnixAbstract(TestApplicationPython):
def test_unix_abstract_source(self):
addr = '\0sock'
def source(source): def test_unix_abstract_source():
assert 'success' in self.conf( addr = '\0sock'
f'"{source}"', 'routes/0/match/source'
)
assert 'success' in self.conf( def source(source):
{ assert 'success' in client.conf(f'"{source}"', 'routes/0/match/source')
"listeners": {
"127.0.0.1:7080": {"pass": "routes"},
f"unix:@{addr[1:]}": {"pass": "routes"},
},
"routes": [
{
"match": {"source": "!0.0.0.0/0"},
"action": {"return": 200},
}
],
"applications": {},
}
)
assert ( assert 'success' in client.conf(
self.get(sock_type='unix', addr=addr)['status'] == 200 {
), 'neg ipv4' "listeners": {
"127.0.0.1:7080": {"pass": "routes"},
f"unix:@{addr[1:]}": {"pass": "routes"},
},
"routes": [
{
"match": {"source": "!0.0.0.0/0"},
"action": {"return": 200},
}
],
"applications": {},
}
)
source("!::/0") assert client.get(sock_type='unix', addr=addr)['status'] == 200, 'neg ipv4'
assert (
self.get(sock_type='unix', addr=addr)['status'] == 200
), 'neg ipv6'
source("unix") source("!::/0")
assert self.get()['status'] == 404, 'ipv4' assert client.get(sock_type='unix', addr=addr)['status'] == 200, 'neg ipv6'
assert self.get(sock_type='unix', addr=addr)['status'] == 200, 'unix'
def test_unix_abstract_client_ip(self): source("unix")
def get_xff(xff, sock_type='ipv4'): assert client.get()['status'] == 404, 'ipv4'
address = { assert client.get(sock_type='unix', addr=addr)['status'] == 200, 'unix'
'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']
client_ip_dir = f"{option.test_dir}/python/client_ip" def test_unix_abstract_client_ip():
assert 'success' in self.conf( def get_xff(xff, sock_type='ipv4'):
{ address = {
"listeners": { 'ipv4': ('127.0.0.1', 7080),
"127.0.0.1:7080": { 'ipv6': ('::1', 7081),
"client_ip": { 'unix': ('\0sock', None),
"header": "X-Forwarded-For", }
"source": "unix", (addr, port) = address[sock_type]
},
"pass": "applications/client_ip", return client.get(
}, sock_type=sock_type,
"[::1]:7081": { addr=addr,
"client_ip": { port=port,
"header": "X-Forwarded-For", headers={'Connection': 'close', 'X-Forwarded-For': xff},
"source": "unix", )['body']
},
"pass": "applications/client_ip", client_ip_dir = f"{option.test_dir}/python/client_ip"
}, assert 'success' in client.conf(
"unix:@sock": { {
"client_ip": { "listeners": {
"header": "X-Forwarded-For", "127.0.0.1:7080": {
"source": "unix",
},
"pass": "applications/client_ip",
},
},
"applications": {
"client_ip": { "client_ip": {
"type": self.get_application_type(), "header": "X-Forwarded-For",
"processes": {"spare": 0}, "source": "unix",
"path": client_ip_dir, },
"working_directory": client_ip_dir, "pass": "applications/client_ip",
"module": "wsgi",
}
}, },
} "[::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": client.get_application_type(),
"processes": {"spare": 0},
"path": client_ip_dir,
"working_directory": client_ip_dir,
"module": "wsgi",
}
},
}
)
assert get_xff('1.1.1.1') == '127.0.0.1', 'bad source ipv4' 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' assert get_xff('1.1.1.1', 'ipv6') == '::1', 'bad source ipv6'
for ip in [ for ip in [
'1.1.1.1', '1.1.1.1',
'::11.22.33.44', '::11.22.33.44',
]: ]:
assert get_xff(ip, 'unix') == ip, 'replace' assert get_xff(ip, 'unix') == ip, 'replace'

View File

@@ -2,477 +2,491 @@ import os
import re import re
import pytest import pytest
from unit.applications.lang.python import TestApplicationPython from unit.applications.lang.python import ApplicationPython
from unit.option import option from unit.option import option
prerequisites = {'modules': {'python': 'any'}} prerequisites = {'modules': {'python': 'any'}}
client = ApplicationPython()
class TestUpstreamsRR(TestApplicationPython):
@pytest.fixture(autouse=True) @pytest.fixture(autouse=True)
def setup_method_fixture(self): def setup_method_fixture():
assert 'success' in self.conf( assert 'success' in client.conf(
{ {
"listeners": { "listeners": {
"*:7080": {"pass": "upstreams/one"}, "*:7080": {"pass": "upstreams/one"},
"*:7090": {"pass": "upstreams/two"}, "*:7090": {"pass": "upstreams/two"},
"*:7081": {"pass": "routes/one"}, "*:7081": {"pass": "routes/one"},
"*:7082": {"pass": "routes/two"}, "*:7082": {"pass": "routes/two"},
"*:7083": {"pass": "routes/three"}, "*:7083": {"pass": "routes/three"},
},
"upstreams": {
"one": {
"servers": {
"127.0.0.1:7081": {},
"127.0.0.1:7082": {},
},
},
"two": {
"servers": {
"127.0.0.1:7081": {},
"127.0.0.1:7082": {},
},
},
},
"routes": {
"one": [{"action": {"return": 200}}],
"two": [{"action": {"return": 201}}],
"three": [{"action": {"return": 202}}],
},
"applications": {},
}, },
), 'upstreams initial configuration' "upstreams": {
"one": {
"servers": {
"127.0.0.1:7081": {},
"127.0.0.1:7082": {},
},
},
"two": {
"servers": {
"127.0.0.1:7081": {},
"127.0.0.1:7082": {},
},
},
},
"routes": {
"one": [{"action": {"return": 200}}],
"two": [{"action": {"return": 201}}],
"three": [{"action": {"return": 202}}],
},
"applications": {},
},
), 'upstreams initial configuration'
self.cpu_count = os.cpu_count() client.cpu_count = os.cpu_count()
def get_resps(self, req=100, port=7080):
resps = [0]
for _ in range(req): def get_resps(req=100, port=7080):
status = self.get(port=port)['status'] resps = [0]
if 200 > status or status > 209:
continue
ups = status % 10 for _ in range(req):
if ups > len(resps) - 1: status = client.get(port=port)['status']
resps.extend([0] * (ups - len(resps) + 1)) if 200 > status or status > 209:
continue
resps[ups] += 1 ups = status % 10
if ups > len(resps) - 1:
resps.extend([0] * (ups - len(resps) + 1))
return resps resps[ups] += 1
def get_resps_sc(self, req=100, port=7080): return resps
to_send = b"""GET / HTTP/1.1
def get_resps_sc(req=100, port=7080):
to_send = b"""GET / HTTP/1.1
Host: localhost Host: localhost
""" * ( """ * (
req - 1 req - 1
) )
to_send += b"""GET / HTTP/1.1 to_send += b"""GET / HTTP/1.1
Host: localhost Host: localhost
Connection: close Connection: close
""" """
resp = self.http(to_send, raw_resp=True, raw=True, port=port) resp = client.http(to_send, raw_resp=True, raw=True, port=port)
status = re.findall(r'HTTP\/\d\.\d\s(\d\d\d)', resp) status = re.findall(r'HTTP\/\d\.\d\s(\d\d\d)', resp)
status = list(filter(lambda x: x[:2] == '20', status)) status = list(filter(lambda x: x[:2] == '20', status))
ups = list(map(lambda x: int(x[-1]), status)) ups = list(map(lambda x: int(x[-1]), status))
resps = [0] * (max(ups) + 1) resps = [0] * (max(ups) + 1)
for i in range(len(ups)): for _, up in enumerate(ups):
resps[ups[i]] += 1 resps[up] += 1
return resps return resps
def test_upstreams_rr_no_weight(self):
resps = self.get_resps()
assert sum(resps) == 100, 'no weight sum'
assert abs(resps[0] - resps[1]) <= self.cpu_count, 'no weight'
assert 'success' in self.conf_delete( def test_upstreams_rr_no_weight():
'upstreams/one/servers/127.0.0.1:7081' resps = get_resps()
), 'no weight server remove' assert sum(resps) == 100, 'no weight sum'
assert abs(resps[0] - resps[1]) <= client.cpu_count, 'no weight'
resps = self.get_resps(req=50) assert 'success' in client.conf_delete(
assert resps[1] == 50, 'no weight 2' 'upstreams/one/servers/127.0.0.1:7081'
), 'no weight server remove'
assert 'success' in self.conf( resps = get_resps(req=50)
{}, 'upstreams/one/servers/127.0.0.1:7081' assert resps[1] == 50, 'no weight 2'
), 'no weight server revert'
resps = self.get_resps() assert 'success' in client.conf(
assert sum(resps) == 100, 'no weight 3 sum' {}, 'upstreams/one/servers/127.0.0.1:7081'
assert abs(resps[0] - resps[1]) <= self.cpu_count, 'no weight 3' ), 'no weight server revert'
assert 'success' in self.conf( resps = get_resps()
{}, 'upstreams/one/servers/127.0.0.1:7083' assert sum(resps) == 100, 'no weight 3 sum'
), 'no weight server new' assert abs(resps[0] - resps[1]) <= client.cpu_count, 'no weight 3'
resps = self.get_resps() assert 'success' in client.conf(
assert sum(resps) == 100, 'no weight 4 sum' {}, 'upstreams/one/servers/127.0.0.1:7083'
assert max(resps) - min(resps) <= self.cpu_count, 'no weight 4' ), 'no weight server new'
resps = self.get_resps_sc(req=30) resps = get_resps()
assert resps[0] == 10, 'no weight 4 0' assert sum(resps) == 100, 'no weight 4 sum'
assert resps[1] == 10, 'no weight 4 1' assert max(resps) - min(resps) <= client.cpu_count, 'no weight 4'
assert resps[2] == 10, 'no weight 4 2'
def test_upstreams_rr_weight(self): resps = get_resps_sc(req=30)
assert 'success' in self.conf( assert resps[0] == 10, 'no weight 4 0'
{"weight": 3}, 'upstreams/one/servers/127.0.0.1:7081' assert resps[1] == 10, 'no weight 4 1'
), 'configure weight' assert resps[2] == 10, 'no weight 4 2'
resps = self.get_resps_sc()
assert resps[0] == 75, 'weight 3 0'
assert resps[1] == 25, 'weight 3 1'
assert 'success' in self.conf_delete( def test_upstreams_rr_weight():
'upstreams/one/servers/127.0.0.1:7081/weight' assert 'success' in client.conf(
), 'configure weight remove' {"weight": 3}, 'upstreams/one/servers/127.0.0.1:7081'
resps = self.get_resps_sc(req=10) ), 'configure weight'
assert resps[0] == 5, 'weight 0 0'
assert resps[1] == 5, 'weight 0 1'
assert 'success' in self.conf( resps = get_resps_sc()
'1', 'upstreams/one/servers/127.0.0.1:7081/weight' assert resps[0] == 75, 'weight 3 0'
), 'configure weight 1' assert resps[1] == 25, 'weight 3 1'
resps = self.get_resps_sc() assert 'success' in client.conf_delete(
assert resps[0] == 50, 'weight 1 0' 'upstreams/one/servers/127.0.0.1:7081/weight'
assert resps[1] == 50, 'weight 1 1' ), 'configure weight remove'
resps = get_resps_sc(req=10)
assert resps[0] == 5, 'weight 0 0'
assert resps[1] == 5, 'weight 0 1'
assert 'success' in self.conf( assert 'success' in client.conf(
'1', 'upstreams/one/servers/127.0.0.1:7081/weight'
), 'configure weight 1'
resps = get_resps_sc()
assert resps[0] == 50, 'weight 1 0'
assert resps[1] == 50, 'weight 1 1'
assert 'success' in client.conf(
{
"127.0.0.1:7081": {"weight": 3},
"127.0.0.1:7083": {"weight": 2},
},
'upstreams/one/servers',
), 'configure weight 2'
resps = get_resps_sc()
assert resps[0] == 60, 'weight 2 0'
assert resps[2] == 40, 'weight 2 1'
def test_upstreams_rr_weight_rational():
def set_weights(w1, w2):
assert 'success' in client.conf(
{ {
"127.0.0.1:7081": {"weight": 3}, "127.0.0.1:7081": {"weight": w1},
"127.0.0.1:7083": {"weight": 2}, "127.0.0.1:7082": {"weight": w2},
},
'upstreams/one/servers',
), 'configure weight 2'
resps = self.get_resps_sc()
assert resps[0] == 60, 'weight 2 0'
assert resps[2] == 40, 'weight 2 1'
def test_upstreams_rr_weight_rational(self):
def set_weights(w1, w2):
assert 'success' in self.conf(
{
"127.0.0.1:7081": {"weight": w1},
"127.0.0.1:7082": {"weight": w2},
},
'upstreams/one/servers',
), 'configure weights'
def check_reqs(w1, w2, reqs=10):
resps = self.get_resps_sc(req=reqs)
assert resps[0] == reqs * w1 / (w1 + w2), 'weight 1'
assert resps[1] == reqs * w2 / (w1 + w2), 'weight 2'
def check_weights(w1, w2):
set_weights(w1, w2)
check_reqs(w1, w2)
check_weights(0, 1)
check_weights(0, 999999.0123456)
check_weights(1, 9)
check_weights(100000, 900000)
check_weights(1, 0.25)
check_weights(1, 0.25)
check_weights(0.2, 0.8)
check_weights(1, 1.5)
check_weights(1e-3, 1e-3)
check_weights(1e-20, 1e-20)
check_weights(1e4, 1e4)
check_weights(1000000, 1000000)
set_weights(0.25, 0.25)
assert 'success' in self.conf_delete(
'upstreams/one/servers/127.0.0.1:7081/weight'
), 'delete weight'
check_reqs(1, 0.25)
assert 'success' in self.conf(
{
"127.0.0.1:7081": {"weight": 0.1},
"127.0.0.1:7082": {"weight": 1},
"127.0.0.1:7083": {"weight": 0.9},
}, },
'upstreams/one/servers', 'upstreams/one/servers',
), 'configure weights' ), 'configure weights'
resps = self.get_resps_sc(req=20)
assert resps[0] == 1, 'weight 3 1'
assert resps[1] == 10, 'weight 3 2'
assert resps[2] == 9, 'weight 3 3'
def test_upstreams_rr_independent(self): def check_reqs(w1, w2, reqs=10):
def sum_resps(*args): resps = get_resps_sc(req=reqs)
sum = [0] * len(args[0]) assert resps[0] == reqs * w1 / (w1 + w2), 'weight 1'
for arg in args: assert resps[1] == reqs * w2 / (w1 + w2), 'weight 2'
sum = [x + y for x, y in zip(sum, arg)]
return sum def check_weights(w1, w2):
set_weights(w1, w2)
check_reqs(w1, w2)
resps = self.get_resps_sc(req=30, port=7090) check_weights(0, 1)
assert resps[0] == 15, 'dep two before 0' check_weights(0, 999999.0123456)
assert resps[1] == 15, 'dep two before 1' check_weights(1, 9)
check_weights(100000, 900000)
check_weights(1, 0.25)
check_weights(1, 0.25)
check_weights(0.2, 0.8)
check_weights(1, 1.5)
check_weights(1e-3, 1e-3)
check_weights(1e-20, 1e-20)
check_weights(1e4, 1e4)
check_weights(1000000, 1000000)
resps = self.get_resps_sc(req=30) set_weights(0.25, 0.25)
assert resps[0] == 15, 'dep one before 0' assert 'success' in client.conf_delete(
assert resps[1] == 15, 'dep one before 1' 'upstreams/one/servers/127.0.0.1:7081/weight'
), 'delete weight'
check_reqs(1, 0.25)
assert 'success' in self.conf( assert 'success' in client.conf(
'2', 'upstreams/two/servers/127.0.0.1:7081/weight' {
), 'configure dep weight' "127.0.0.1:7081": {"weight": 0.1},
"127.0.0.1:7082": {"weight": 1},
"127.0.0.1:7083": {"weight": 0.9},
},
'upstreams/one/servers',
), 'configure weights'
resps = get_resps_sc(req=20)
assert resps[0] == 1, 'weight 3 1'
assert resps[1] == 10, 'weight 3 2'
assert resps[2] == 9, 'weight 3 3'
resps = self.get_resps_sc(req=30, port=7090)
assert resps[0] == 20, 'dep two 0'
assert resps[1] == 10, 'dep two 1'
resps = self.get_resps_sc(req=30) def test_upstreams_rr_independent():
assert resps[0] == 15, 'dep one 0' def sum_resps(*args):
assert resps[1] == 15, 'dep one 1' sum_r = [0] * len(args[0])
for arg in args:
sum_r = [x + y for x, y in zip(sum_r, arg)]
assert 'success' in self.conf( return sum_r
'1', 'upstreams/two/servers/127.0.0.1:7081/weight'
), 'configure dep weight 1'
r_one, r_two = [0, 0], [0, 0] resps = get_resps_sc(req=30, port=7090)
for _ in range(10): assert resps[0] == 15, 'dep two before 0'
r_one = sum_resps(r_one, self.get_resps(req=10)) assert resps[1] == 15, 'dep two before 1'
r_two = sum_resps(r_two, self.get_resps(req=10, port=7090))
assert sum(r_one) == 100, 'dep one mix sum' resps = get_resps_sc(req=30)
assert abs(r_one[0] - r_one[1]) <= self.cpu_count, 'dep one mix' assert resps[0] == 15, 'dep one before 0'
assert sum(r_two) == 100, 'dep two mix sum' assert resps[1] == 15, 'dep one before 1'
assert abs(r_two[0] - r_two[1]) <= self.cpu_count, 'dep two mix'
def test_upstreams_rr_delay(self): assert 'success' in client.conf(
delayed_dir = f'{option.test_dir}/python/delayed' '2', 'upstreams/two/servers/127.0.0.1:7081/weight'
assert 'success' in self.conf( ), 'configure dep weight'
{
"listeners": { resps = get_resps_sc(req=30, port=7090)
"*:7080": {"pass": "upstreams/one"}, assert resps[0] == 20, 'dep two 0'
"*:7081": {"pass": "routes"}, assert resps[1] == 10, 'dep two 1'
"*:7082": {"pass": "routes"},
}, resps = get_resps_sc(req=30)
"upstreams": { assert resps[0] == 15, 'dep one 0'
"one": { assert resps[1] == 15, 'dep one 1'
"servers": {
"127.0.0.1:7081": {}, assert 'success' in client.conf(
"127.0.0.1:7082": {}, '1', 'upstreams/two/servers/127.0.0.1:7081/weight'
}, ), 'configure dep weight 1'
r_one, r_two = [0, 0], [0, 0]
for _ in range(10):
r_one = sum_resps(r_one, get_resps(req=10))
r_two = sum_resps(r_two, get_resps(req=10, port=7090))
assert sum(r_one) == 100, 'dep one mix sum'
assert abs(r_one[0] - r_one[1]) <= client.cpu_count, 'dep one mix'
assert sum(r_two) == 100, 'dep two mix sum'
assert abs(r_two[0] - r_two[1]) <= client.cpu_count, 'dep two mix'
def test_upstreams_rr_delay():
delayed_dir = f'{option.test_dir}/python/delayed'
assert 'success' in client.conf(
{
"listeners": {
"*:7080": {"pass": "upstreams/one"},
"*:7081": {"pass": "routes"},
"*:7082": {"pass": "routes"},
},
"upstreams": {
"one": {
"servers": {
"127.0.0.1:7081": {},
"127.0.0.1:7082": {},
}, },
}, },
"routes": [
{
"match": {"destination": "*:7081"},
"action": {"pass": "applications/delayed"},
},
{
"match": {"destination": "*:7082"},
"action": {"return": 201},
},
],
"applications": {
"delayed": {
"type": self.get_application_type(),
"processes": {"spare": 0},
"path": delayed_dir,
"working_directory": delayed_dir,
"module": "wsgi",
}
},
}, },
), 'upstreams initial configuration' "routes": [
{
req = 50 "match": {"destination": "*:7081"},
"action": {"pass": "applications/delayed"},
socks = []
for i in range(req):
delay = 1 if i % 5 == 0 else 0
sock = self.get(
headers={
'Host': 'localhost',
'Content-Length': '0',
'X-Delay': str(delay),
'Connection': 'close',
}, },
no_recv=True, {
) "match": {"destination": "*:7082"},
socks.append(sock) "action": {"return": 201},
},
],
"applications": {
"delayed": {
"type": client.get_application_type(),
"processes": {"spare": 0},
"path": delayed_dir,
"working_directory": delayed_dir,
"module": "wsgi",
}
},
},
), 'upstreams initial configuration'
resps = [0, 0] req = 50
for i in range(req):
resp = self.recvall(socks[i]).decode()
socks[i].close()
m = re.search(r'HTTP/1.1 20(\d)', resp) socks = []
assert m is not None, 'status' for i in range(req):
resps[int(m.group(1))] += 1 delay = 1 if i % 5 == 0 else 0
sock = client.get(
headers={
'Host': 'localhost',
'Content-Length': '0',
'X-Delay': str(delay),
'Connection': 'close',
},
no_recv=True,
)
socks.append(sock)
assert sum(resps) == req, 'delay sum' resps = [0, 0]
assert abs(resps[0] - resps[1]) <= self.cpu_count, 'delay' for i in range(req):
resp = client.recvall(socks[i]).decode()
socks[i].close()
def test_upstreams_rr_active_req(self): m = re.search(r'HTTP/1.1 20(\d)', resp)
conns = 5 assert m is not None, 'status'
socks = [] resps[int(m.group(1))] += 1
socks2 = []
for _ in range(conns): assert sum(resps) == req, 'delay sum'
sock = self.get(no_recv=True) assert abs(resps[0] - resps[1]) <= client.cpu_count, 'delay'
socks.append(sock)
sock2 = self.http(
b"""POST / HTTP/1.1 def test_upstreams_rr_active_req():
conns = 5
socks = []
socks2 = []
for _ in range(conns):
sock = client.get(no_recv=True)
socks.append(sock)
sock2 = client.http(
b"""POST / HTTP/1.1
Host: localhost Host: localhost
Content-Length: 10 Content-Length: 10
Connection: close Connection: close
""", """,
no_recv=True, no_recv=True,
raw=True, raw=True,
) )
socks2.append(sock2) socks2.append(sock2)
# Send one more request and read response to make sure that previous # Send one more request and read response to make sure that previous
# requests had enough time to reach server. # requests had enough time to reach server.
assert self.get()['body'] == '' assert client.get()['body'] == ''
assert 'success' in self.conf( assert 'success' in client.conf(
{"127.0.0.1:7083": {"weight": 2}}, {"127.0.0.1:7083": {"weight": 2}},
'upstreams/one/servers', 'upstreams/one/servers',
), 'active req new server' ), 'active req new server'
assert 'success' in self.conf_delete( assert 'success' in client.conf_delete(
'upstreams/one/servers/127.0.0.1:7083' 'upstreams/one/servers/127.0.0.1:7083'
), 'active req server remove' ), 'active req server remove'
assert 'success' in self.conf_delete( assert 'success' in client.conf_delete(
'listeners/*:7080' 'listeners/*:7080'
), 'delete listener' ), 'delete listener'
assert 'success' in self.conf_delete( assert 'success' in client.conf_delete(
'upstreams/one' 'upstreams/one'
), 'active req upstream remove' ), 'active req upstream remove'
for i in range(conns): for i in range(conns):
assert ( assert (
self.http(b'', sock=socks[i], raw=True)['body'] == '' client.http(b'', sock=socks[i], raw=True)['body'] == ''
), 'active req GET' ), 'active req GET'
assert ( assert (
self.http(b"""0123456789""", sock=socks2[i], raw=True)['body'] client.http(b"""0123456789""", sock=socks2[i], raw=True)['body']
== '' == ''
), 'active req POST' ), 'active req POST'
def test_upstreams_rr_bad_server(self):
assert 'success' in self.conf(
{"weight": 1}, 'upstreams/one/servers/127.0.0.1:7084'
), 'configure bad server'
resps = self.get_resps_sc(req=30) def test_upstreams_rr_bad_server():
assert resps[0] == 10, 'bad server 0' assert 'success' in client.conf(
assert resps[1] == 10, 'bad server 1' {"weight": 1}, 'upstreams/one/servers/127.0.0.1:7084'
assert sum(resps) == 20, 'bad server sum' ), 'configure bad server'
def test_upstreams_rr_pipeline(self): resps = get_resps_sc(req=30)
resps = self.get_resps_sc() assert resps[0] == 10, 'bad server 0'
assert resps[1] == 10, 'bad server 1'
assert sum(resps) == 20, 'bad server sum'
assert resps[0] == 50, 'pipeline 0'
assert resps[1] == 50, 'pipeline 1'
def test_upstreams_rr_post(self): def test_upstreams_rr_pipeline():
resps = [0, 0] resps = get_resps_sc()
for _ in range(50):
resps[self.get()['status'] % 10] += 1
resps[self.post(body='0123456789')['status'] % 10] += 1
assert sum(resps) == 100, 'post sum' assert resps[0] == 50, 'pipeline 0'
assert abs(resps[0] - resps[1]) <= self.cpu_count, 'post' assert resps[1] == 50, 'pipeline 1'
def test_upstreams_rr_unix(self, temp_dir):
addr_0 = f'{temp_dir}/sock_0'
addr_1 = f'{temp_dir}/sock_1'
assert 'success' in self.conf( def test_upstreams_rr_post():
{ resps = [0, 0]
"*:7080": {"pass": "upstreams/one"}, for _ in range(50):
f"unix:{addr_0}": {"pass": "routes/one"}, resps[client.get()['status'] % 10] += 1
f"unix:{addr_1}": {"pass": "routes/two"}, resps[client.post(body='0123456789')['status'] % 10] += 1
},
'listeners',
), 'configure listeners unix'
assert 'success' in self.conf( assert sum(resps) == 100, 'post sum'
{f"unix:{addr_0}": {}, f"unix:{addr_1}": {}}, assert abs(resps[0] - resps[1]) <= client.cpu_count, 'post'
'upstreams/one/servers',
), 'configure servers unix'
resps = self.get_resps_sc()
assert resps[0] == 50, 'unix 0' def test_upstreams_rr_unix(temp_dir):
assert resps[1] == 50, 'unix 1' addr_0 = f'{temp_dir}/sock_0'
addr_1 = f'{temp_dir}/sock_1'
def test_upstreams_rr_ipv6(self): assert 'success' in client.conf(
assert 'success' in self.conf( {
{ "*:7080": {"pass": "upstreams/one"},
"*:7080": {"pass": "upstreams/one"}, f"unix:{addr_0}": {"pass": "routes/one"},
"[::1]:7081": {"pass": "routes/one"}, f"unix:{addr_1}": {"pass": "routes/two"},
"[::1]:7082": {"pass": "routes/two"}, },
}, 'listeners',
'listeners', ), 'configure listeners unix'
), 'configure listeners ipv6'
assert 'success' in self.conf( assert 'success' in client.conf(
{"[::1]:7081": {}, "[::1]:7082": {}}, 'upstreams/one/servers' {f"unix:{addr_0}": {}, f"unix:{addr_1}": {}},
), 'configure servers ipv6' 'upstreams/one/servers',
), 'configure servers unix'
resps = self.get_resps_sc() resps = get_resps_sc()
assert resps[0] == 50, 'ipv6 0' assert resps[0] == 50, 'unix 0'
assert resps[1] == 50, 'ipv6 1' assert resps[1] == 50, 'unix 1'
def test_upstreams_rr_servers_empty(self):
assert 'success' in self.conf(
{}, 'upstreams/one/servers'
), 'configure servers empty'
assert self.get()['status'] == 502, 'servers empty'
assert 'success' in self.conf( def test_upstreams_rr_ipv6():
{"127.0.0.1:7081": {"weight": 0}}, 'upstreams/one/servers' assert 'success' in client.conf(
), 'configure servers empty one' {
assert self.get()['status'] == 502, 'servers empty one' "*:7080": {"pass": "upstreams/one"},
assert 'success' in self.conf( "[::1]:7081": {"pass": "routes/one"},
{ "[::1]:7082": {"pass": "routes/two"},
"127.0.0.1:7081": {"weight": 0}, },
"127.0.0.1:7082": {"weight": 0}, 'listeners',
}, ), 'configure listeners ipv6'
'upstreams/one/servers',
), 'configure servers empty two'
assert self.get()['status'] == 502, 'servers empty two'
def test_upstreams_rr_invalid(self): assert 'success' in client.conf(
assert 'error' in self.conf({}, 'upstreams'), 'upstreams empty' {"[::1]:7081": {}, "[::1]:7082": {}}, 'upstreams/one/servers'
assert 'error' in self.conf( ), 'configure servers ipv6'
{}, 'upstreams/one'
), 'named upstreams empty'
assert 'error' in self.conf(
{}, 'upstreams/one/servers/127.0.0.1'
), 'invalid address'
assert 'error' in self.conf(
{}, 'upstreams/one/servers/127.0.0.1:7081/blah'
), 'invalid server option'
def check_weight(w): resps = get_resps_sc()
assert 'error' in self.conf(
w, 'upstreams/one/servers/127.0.0.1:7081/weight'
), 'invalid weight option'
check_weight({}) assert resps[0] == 50, 'ipv6 0'
check_weight('-1') assert resps[1] == 50, 'ipv6 1'
check_weight('1.')
check_weight('1.1.')
check_weight('.') def test_upstreams_rr_servers_empty():
check_weight('.01234567890123') assert 'success' in client.conf(
check_weight('1000001') {}, 'upstreams/one/servers'
check_weight('2e6') ), 'configure servers empty'
assert client.get()['status'] == 502, 'servers empty'
assert 'success' in client.conf(
{"127.0.0.1:7081": {"weight": 0}}, 'upstreams/one/servers'
), 'configure servers empty one'
assert client.get()['status'] == 502, 'servers empty one'
assert 'success' in client.conf(
{
"127.0.0.1:7081": {"weight": 0},
"127.0.0.1:7082": {"weight": 0},
},
'upstreams/one/servers',
), 'configure servers empty two'
assert client.get()['status'] == 502, 'servers empty two'
def test_upstreams_rr_invalid():
assert 'error' in client.conf({}, 'upstreams'), 'upstreams empty'
assert 'error' in client.conf({}, 'upstreams/one'), 'named upstreams empty'
assert 'error' in client.conf(
{}, 'upstreams/one/servers/127.0.0.1'
), 'invalid address'
assert 'error' in client.conf(
{}, 'upstreams/one/servers/127.0.0.1:7081/blah'
), 'invalid server option'
def check_weight(w):
assert 'error' in client.conf(
w, 'upstreams/one/servers/127.0.0.1:7081/weight'
), 'invalid weight option'
check_weight({})
check_weight('-1')
check_weight('1.')
check_weight('1.1.')
check_weight('.')
check_weight('.01234567890123')
check_weight('1000001')
check_weight('2e6')

View File

@@ -1,91 +1,87 @@
import os import os
import signal import signal
from unit.applications.lang.python import TestApplicationPython from unit.applications.lang.python import ApplicationPython
from unit.log import Log from unit.log import Log
from unit.utils import waitforfiles from unit.utils import waitforfiles
prerequisites = {'modules': {'python': 'any'}} prerequisites = {'modules': {'python': 'any'}}
client = ApplicationPython()
class TestUSR1(TestApplicationPython):
def test_usr1_access_log(
self, search_in_file, temp_dir, unit_pid, wait_for_record
):
self.load('empty')
log = 'access.log' def test_usr1_access_log(search_in_file, temp_dir, unit_pid, wait_for_record):
log_new = 'new.log' client.load('empty')
log_path = f'{temp_dir}/{log}'
assert 'success' in self.conf( log = 'access.log'
f'"{log_path}"', 'access_log' log_new = 'new.log'
), 'access log configure' log_path = f'{temp_dir}/{log}'
assert waitforfiles(log_path), 'open' assert 'success' in client.conf(
f'"{log_path}"', 'access_log'
), 'access log configure'
os.rename(log_path, f'{temp_dir}/{log_new}') assert waitforfiles(log_path), 'open'
assert self.get()['status'] == 200 os.rename(log_path, f'{temp_dir}/{log_new}')
assert ( assert client.get()['status'] == 200
wait_for_record(r'"GET / HTTP/1.1" 200 0 "-" "-"', log_new)
is not None assert (
), 'rename new' wait_for_record(r'"GET / HTTP/1.1" 200 0 "-" "-"', log_new) is not None
), 'rename new'
assert not os.path.isfile(log_path), 'rename old'
os.kill(unit_pid, signal.SIGUSR1)
assert waitforfiles(log_path), 'reopen'
assert client.get(url='/usr1')['status'] == 200
assert (
wait_for_record(r'"GET /usr1 HTTP/1.1" 200 0 "-" "-"', log) is not None
), 'reopen 2'
assert search_in_file(r'/usr1', log_new) is None, 'rename new 2'
def test_usr1_unit_log(search_in_file, temp_dir, unit_pid, wait_for_record):
client.load('log_body')
log_new = 'new.log'
log_path = f'{temp_dir}/unit.log'
log_path_new = f'{temp_dir}/{log_new}'
os.rename(log_path, log_path_new)
Log.swap(log_new)
try:
body = 'body_for_a_log_new\n'
assert client.post(body=body)['status'] == 200
assert wait_for_record(body, log_new) is not None, 'rename new'
assert not os.path.isfile(log_path), 'rename old' assert not os.path.isfile(log_path), 'rename old'
os.kill(unit_pid, signal.SIGUSR1) os.kill(unit_pid, signal.SIGUSR1)
assert waitforfiles(log_path), 'reopen' assert waitforfiles(log_path), 'reopen'
assert self.get(url='/usr1')['status'] == 200 body = 'body_for_a_log_unit\n'
assert client.post(body=body)['status'] == 200
assert ( assert wait_for_record(body) is not None, 'rename new'
wait_for_record(r'"GET /usr1 HTTP/1.1" 200 0 "-" "-"', log) assert search_in_file(body, log_new) is None, 'rename new 2'
is not None
), 'reopen 2'
assert search_in_file(r'/usr1', log_new) is None, 'rename new 2'
def test_usr1_unit_log( finally:
self, search_in_file, temp_dir, unit_pid, wait_for_record # merge two log files into unit.log to check alerts
):
self.load('log_body')
log_new = 'new.log' with open(log_path, 'r', errors='ignore') as unit_log:
log_path = f'{temp_dir}/unit.log' log = unit_log.read()
log_path_new = f'{temp_dir}/{log_new}'
os.rename(log_path, log_path_new) with open(log_path, 'w') as unit_log, open(
log_path_new, 'r', errors='ignore'
) as unit_log_new:
unit_log.write(unit_log_new.read())
unit_log.write(log)
Log.swap(log_new) Log.swap(log_new)
try:
body = 'body_for_a_log_new\n'
assert self.post(body=body)['status'] == 200
assert wait_for_record(body, log_new) is not None, 'rename new'
assert not os.path.isfile(log_path), 'rename old'
os.kill(unit_pid, signal.SIGUSR1)
assert waitforfiles(log_path), 'reopen'
body = 'body_for_a_log_unit\n'
assert self.post(body=body)['status'] == 200
assert wait_for_record(body) is not None, 'rename new'
assert search_in_file(body, log_new) is None, 'rename new 2'
finally:
# merge two log files into unit.log to check alerts
with open(log_path, 'r', errors='ignore') as unit_log:
log = unit_log.read()
with open(log_path, 'w') as unit_log, open(
log_path_new, 'r', errors='ignore'
) as unit_log_new:
unit_log.write(unit_log_new.read())
unit_log.write(log)
Log.swap(log_new)

View File

@@ -2,394 +2,403 @@ import re
import time import time
import pytest import pytest
from unit.applications.proto import TestApplicationProto from unit.applications.proto import ApplicationProto
from unit.option import option from unit.option import option
client = ApplicationProto()
class TestVariables(TestApplicationProto):
@pytest.fixture(autouse=True)
def setup_method_fixture(self):
assert 'success' in self.conf(
{
"listeners": {"*:7080": {"pass": "routes"}},
"routes": [{"action": {"return": 200}}],
},
), 'configure routes'
def set_format(self, format): @pytest.fixture(autouse=True)
assert 'success' in self.conf( def setup_method_fixture():
{ assert 'success' in client.conf(
'path': f'{option.temp_dir}/access.log', {
'format': format, "listeners": {"*:7080": {"pass": "routes"}},
}, "routes": [{"action": {"return": 200}}],
'access_log', },
), 'access_log format' ), 'configure routes'
def test_variables_dollar(self):
assert 'success' in self.conf("301", 'routes/0/action/return')
def check_dollar(location, expect): def set_format(format):
assert 'success' in self.conf( assert 'success' in client.conf(
f'"{location}"', {
'routes/0/action/location', 'path': f'{option.temp_dir}/access.log',
) 'format': format,
assert self.get()['headers']['Location'] == expect },
'access_log',
), 'access_log format'
check_dollar(
'https://${host}${uri}path${dollar}dollar', def test_variables_dollar():
'https://localhost/path$dollar', assert 'success' in client.conf("301", 'routes/0/action/return')
def check_dollar(location, expect):
assert 'success' in client.conf(
f'"{location}"',
'routes/0/action/location',
) )
check_dollar('path$dollar${dollar}', 'path$$') assert client.get()['headers']['Location'] == expect
def test_variables_request_time(self, wait_for_record): check_dollar(
self.set_format('$uri $request_time') 'https://${host}${uri}path${dollar}dollar',
'https://localhost/path$dollar',
)
check_dollar('path$dollar${dollar}', 'path$$')
sock = self.http(b'', raw=True, no_recv=True)
time.sleep(1) def test_variables_request_time(wait_for_record):
set_format('$uri $request_time')
assert self.get(url='/r_time_1', sock=sock)['status'] == 200 sock = client.http(b'', raw=True, no_recv=True)
assert wait_for_record(r'\/r_time_1 0\.\d{3}', 'access.log') is not None
sock = self.http( time.sleep(1)
b"""G""",
no_recv=True,
raw=True,
)
time.sleep(2) assert client.get(url='/r_time_1', sock=sock)['status'] == 200
assert wait_for_record(r'\/r_time_1 0\.\d{3}', 'access.log') is not None
self.http( sock = client.http(
b"""ET /r_time_2 HTTP/1.1 b"""G""",
no_recv=True,
raw=True,
)
time.sleep(2)
client.http(
b"""ET /r_time_2 HTTP/1.1
Host: localhost Host: localhost
Connection: close Connection: close
""", """,
sock=sock, sock=sock,
raw=True, raw=True,
) )
assert ( assert wait_for_record(r'\/r_time_2 [1-9]\.\d{3}', 'access.log') is not None
wait_for_record(r'\/r_time_2 [1-9]\.\d{3}', 'access.log')
is not None
)
def test_variables_method(self, search_in_file, wait_for_record):
self.set_format('$method')
reg = r'^GET$' def test_variables_method(search_in_file, wait_for_record):
set_format('$method')
reg = r'^GET$'
assert search_in_file(reg, 'access.log') is None
assert client.get()['status'] == 200
assert wait_for_record(reg, 'access.log') is not None, 'method GET'
reg = r'^POST$'
assert search_in_file(reg, 'access.log') is None
assert client.post()['status'] == 200
assert wait_for_record(reg, 'access.log') is not None, 'method POST'
def test_variables_request_uri(search_in_file, wait_for_record):
set_format('$request_uri')
def check_request_uri(req_uri):
reg = fr'^{re.escape(req_uri)}$'
assert search_in_file(reg, 'access.log') is None assert search_in_file(reg, 'access.log') is None
assert self.get()['status'] == 200 assert client.get(url=req_uri)['status'] == 200
assert wait_for_record(reg, 'access.log') is not None, 'method GET'
reg = r'^POST$'
assert search_in_file(reg, 'access.log') is None
assert self.post()['status'] == 200
assert wait_for_record(reg, 'access.log') is not None, 'method POST'
def test_variables_request_uri(self, search_in_file, wait_for_record):
self.set_format('$request_uri')
def check_request_uri(req_uri):
reg = fr'^{re.escape(req_uri)}$'
assert search_in_file(reg, 'access.log') is None
assert self.get(url=req_uri)['status'] == 200
assert wait_for_record(reg, 'access.log') is not None
check_request_uri('/3')
check_request_uri('/4*')
check_request_uri('/4%2A')
check_request_uri('/9?q#a')
def test_variables_uri(self, search_in_file, wait_for_record):
self.set_format('$uri')
def check_uri(uri, expect=None):
expect = uri if expect is None else expect
reg = fr'^{re.escape(expect)}$'
assert search_in_file(reg, 'access.log') is None
assert self.get(url=uri)['status'] == 200
assert wait_for_record(reg, 'access.log') is not None
check_uri('/3')
check_uri('/4*')
check_uri('/5%2A', '/5*')
check_uri('/9?q#a', '/9')
def test_variables_host(self, search_in_file, wait_for_record):
self.set_format('$host')
def check_host(host, expect=None):
expect = host if expect is None else expect
reg = fr'^{re.escape(expect)}$'
assert search_in_file(reg, 'access.log') is None
assert (
self.get(headers={'Host': host, 'Connection': 'close'})[
'status'
]
== 200
)
assert wait_for_record(reg, 'access.log') is not None
check_host('localhost')
check_host('localhost1.', 'localhost1')
check_host('localhost2:7080', 'localhost2')
check_host('.localhost')
check_host('www.localhost')
def test_variables_remote_addr(self, search_in_file, wait_for_record):
self.set_format('$remote_addr')
assert self.get()['status'] == 200
assert wait_for_record(r'^127\.0\.0\.1$', 'access.log') is not None
assert 'success' in self.conf(
{"[::1]:7080": {"pass": "routes"}}, 'listeners'
)
reg = r'^::1$'
assert search_in_file(reg, 'access.log') is None
assert self.get(sock_type='ipv6')['status'] == 200
assert wait_for_record(reg, 'access.log') is not None assert wait_for_record(reg, 'access.log') is not None
def test_variables_time_local( check_request_uri('/3')
self, date_to_sec_epoch, search_in_file, wait_for_record check_request_uri('/4*')
): check_request_uri('/4%2A')
self.set_format('$uri $time_local $uri') check_request_uri('/9?q#a')
assert search_in_file(r'/time_local', 'access.log') is None
assert self.get(url='/time_local')['status'] == 200
assert (
wait_for_record(r'/time_local', 'access.log') is not None
), 'time log'
date = search_in_file(
r'^\/time_local (.*) \/time_local$', 'access.log'
)[1]
assert (
abs(
date_to_sec_epoch(date, '%d/%b/%Y:%X %z')
- time.mktime(time.localtime())
)
< 5
), '$time_local'
def test_variables_request_line(self, search_in_file, wait_for_record): def test_variables_uri(search_in_file, wait_for_record):
self.set_format('$request_line') set_format('$uri')
def check_uri(uri, expect=None):
expect = uri if expect is None else expect
reg = fr'^{re.escape(expect)}$'
reg = r'^GET \/r_line HTTP\/1\.1$'
assert search_in_file(reg, 'access.log') is None assert search_in_file(reg, 'access.log') is None
assert self.get(url='/r_line')['status'] == 200 assert client.get(url=uri)['status'] == 200
assert wait_for_record(reg, 'access.log') is not None assert wait_for_record(reg, 'access.log') is not None
def test_variables_status(self, search_in_file, wait_for_record): check_uri('/3')
self.set_format('$status') check_uri('/4*')
check_uri('/5%2A', '/5*')
check_uri('/9?q#a', '/9')
assert 'success' in self.conf("418", 'routes/0/action/return')
reg = r'^418$' def test_variables_host(search_in_file, wait_for_record):
set_format('$host')
def check_host(host, expect=None):
expect = host if expect is None else expect
reg = fr'^{re.escape(expect)}$'
assert search_in_file(reg, 'access.log') is None assert search_in_file(reg, 'access.log') is None
assert self.get()['status'] == 418 assert (
client.get(headers={'Host': host, 'Connection': 'close'})['status']
== 200
)
assert wait_for_record(reg, 'access.log') is not None assert wait_for_record(reg, 'access.log') is not None
def test_variables_header_referer(self, search_in_file, wait_for_record): check_host('localhost')
self.set_format('$method $header_referer') check_host('localhost1.', 'localhost1')
check_host('localhost2:7080', 'localhost2')
check_host('.localhost')
check_host('www.localhost')
def check_referer(referer):
reg = fr'^GET {re.escape(referer)}$'
assert search_in_file(reg, 'access.log') is None def test_variables_remote_addr(search_in_file, wait_for_record):
assert ( set_format('$remote_addr')
self.get(
headers={
'Host': 'localhost',
'Connection': 'close',
'Referer': referer,
}
)['status']
== 200
)
assert wait_for_record(reg, 'access.log') is not None
check_referer('referer-value') assert client.get()['status'] == 200
check_referer('') assert wait_for_record(r'^127\.0\.0\.1$', 'access.log') is not None
check_referer('no')
def test_variables_header_user_agent(self, search_in_file, wait_for_record): assert 'success' in client.conf(
self.set_format('$method $header_user_agent') {"[::1]:7080": {"pass": "routes"}}, 'listeners'
)
def check_user_agent(user_agent): reg = r'^::1$'
reg = fr'^GET {re.escape(user_agent)}$' assert search_in_file(reg, 'access.log') is None
assert client.get(sock_type='ipv6')['status'] == 200
assert wait_for_record(reg, 'access.log') is not None
assert search_in_file(reg, 'access.log') is None
assert (
self.get(
headers={
'Host': 'localhost',
'Connection': 'close',
'User-Agent': user_agent,
}
)['status']
== 200
)
assert wait_for_record(reg, 'access.log') is not None
check_user_agent('MSIE') def test_variables_time_local(
check_user_agent('') date_to_sec_epoch, search_in_file, wait_for_record
check_user_agent('no') ):
set_format('$uri $time_local $uri')
def test_variables_many(self, search_in_file, wait_for_record): assert search_in_file(r'/time_local', 'access.log') is None
def check_vars(uri, expect): assert client.get(url='/time_local')['status'] == 200
reg = fr'^{re.escape(expect)}$' assert wait_for_record(r'/time_local', 'access.log') is not None, 'time log'
date = search_in_file(r'^\/time_local (.*) \/time_local$', 'access.log')[1]
assert (
abs(
date_to_sec_epoch(date, '%d/%b/%Y:%X %z')
- time.mktime(time.localtime())
)
< 5
), '$time_local'
assert search_in_file(reg, 'access.log') is None
assert self.get(url=uri)['status'] == 200
assert wait_for_record(reg, 'access.log') is not None
self.set_format('$uri$method') def test_variables_request_line(search_in_file, wait_for_record):
check_vars('/1', '/1GET') set_format('$request_line')
self.set_format('${uri}${method}') reg = r'^GET \/r_line HTTP\/1\.1$'
check_vars('/2', '/2GET') assert search_in_file(reg, 'access.log') is None
assert client.get(url='/r_line')['status'] == 200
assert wait_for_record(reg, 'access.log') is not None
self.set_format('${uri}$method')
check_vars('/3', '/3GET')
self.set_format('$method$method') def test_variables_status(search_in_file, wait_for_record):
check_vars('/', 'GETGET') set_format('$status')
def test_variables_dynamic(self, wait_for_record): assert 'success' in client.conf("418", 'routes/0/action/return')
self.set_format('$header_foo$cookie_foo$arg_foo')
reg = r'^418$'
assert search_in_file(reg, 'access.log') is None
assert client.get()['status'] == 418
assert wait_for_record(reg, 'access.log') is not None
def test_variables_header_referer(search_in_file, wait_for_record):
set_format('$method $header_referer')
def check_referer(referer):
reg = fr'^GET {re.escape(referer)}$'
assert search_in_file(reg, 'access.log') is None
assert ( assert (
self.get( client.get(
url='/?foo=h', headers={
headers={'Foo': 'b', 'Cookie': 'foo=la', 'Connection': 'close'}, 'Host': 'localhost',
'Connection': 'close',
'Referer': referer,
}
)['status'] )['status']
== 200 == 200
) )
assert wait_for_record(r'^blah$', 'access.log') is not None assert wait_for_record(reg, 'access.log') is not None
def test_variables_dynamic_arguments(self, search_in_file, wait_for_record): check_referer('referer-value')
def check_arg(url, expect=None): check_referer('')
expect = url if expect is None else expect check_referer('no')
reg = fr'^{re.escape(expect)}$'
assert search_in_file(reg, 'access.log') is None
assert self.get(url=url)['status'] == 200
assert wait_for_record(reg, 'access.log') is not None
def check_no_arg(url): def test_variables_header_user_agent(search_in_file, wait_for_record):
assert self.get(url=url)['status'] == 200 set_format('$method $header_user_agent')
assert search_in_file(r'^0$', 'access.log') is None
self.set_format('$arg_foo_bar') def check_user_agent(user_agent):
check_arg('/?foo_bar=1', '1') reg = fr'^GET {re.escape(user_agent)}$'
check_arg('/?foo_b%61r=2', '2')
check_arg('/?bar&foo_bar=3&foo', '3')
check_arg('/?foo_bar=l&foo_bar=4', '4')
check_no_arg('/')
check_no_arg('/?foo_bar=')
check_no_arg('/?Foo_bar=0')
check_no_arg('/?foo-bar=0')
check_no_arg('/?foo_bar=0&foo_bar=l')
self.set_format('$arg_foo_b%61r')
check_no_arg('/?foo_b=0')
check_no_arg('/?foo_bar=0')
self.set_format('$arg_f!~')
check_no_arg('/?f=0')
check_no_arg('/?f!~=0')
def test_variables_dynamic_headers(self, search_in_file, wait_for_record):
def check_header(header, value):
reg = fr'^{value}$'
assert search_in_file(reg, 'access.log') is None
assert (
self.get(headers={header: value, 'Connection': 'close'})[
'status'
]
== 200
)
assert wait_for_record(reg, 'access.log') is not None
def check_no_header(header):
assert (
self.get(headers={header: '0', 'Connection': 'close'})['status']
== 200
)
assert search_in_file(r'^0$', 'access.log') is None
self.set_format('$header_foo_bar')
check_header('foo-bar', '1')
check_header('Foo-Bar', '2')
check_no_header('foo_bar')
check_no_header('foobar')
self.set_format('$header_Foo_Bar')
check_header('Foo-Bar', '4')
check_header('foo-bar', '5')
check_no_header('foo_bar')
check_no_header('foobar')
def test_variables_dynamic_cookies(self, search_in_file, wait_for_record):
def check_no_cookie(cookie):
assert (
self.get(
headers={
'Host': 'localhost',
'Cookie': cookie,
'Connection': 'close',
},
)['status']
== 200
)
assert search_in_file(r'^0$', 'access.log') is None
self.set_format('$cookie_foo_bar')
reg = r'^1$'
assert search_in_file(reg, 'access.log') is None assert search_in_file(reg, 'access.log') is None
assert ( assert (
self.get( client.get(
headers={ headers={
'Host': 'localhost', 'Host': 'localhost',
'Cookie': 'foo_bar=1', 'Connection': 'close',
'User-Agent': user_agent,
}
)['status']
== 200
)
assert wait_for_record(reg, 'access.log') is not None
check_user_agent('MSIE')
check_user_agent('')
check_user_agent('no')
def test_variables_many(search_in_file, wait_for_record):
def check_vars(uri, expect):
reg = fr'^{re.escape(expect)}$'
assert search_in_file(reg, 'access.log') is None
assert client.get(url=uri)['status'] == 200
assert wait_for_record(reg, 'access.log') is not None
set_format('$uri$method')
check_vars('/1', '/1GET')
set_format('${uri}${method}')
check_vars('/2', '/2GET')
set_format('${uri}$method')
check_vars('/3', '/3GET')
set_format('$method$method')
check_vars('/', 'GETGET')
def test_variables_dynamic(wait_for_record):
set_format('$header_foo$cookie_foo$arg_foo')
assert (
client.get(
url='/?foo=h',
headers={'Foo': 'b', 'Cookie': 'foo=la', 'Connection': 'close'},
)['status']
== 200
)
assert wait_for_record(r'^blah$', 'access.log') is not None
def test_variables_dynamic_arguments(search_in_file, wait_for_record):
def check_arg(url, expect=None):
expect = url if expect is None else expect
reg = fr'^{re.escape(expect)}$'
assert search_in_file(reg, 'access.log') is None
assert client.get(url=url)['status'] == 200
assert wait_for_record(reg, 'access.log') is not None
def check_no_arg(url):
assert client.get(url=url)['status'] == 200
assert search_in_file(r'^0$', 'access.log') is None
set_format('$arg_foo_bar')
check_arg('/?foo_bar=1', '1')
check_arg('/?foo_b%61r=2', '2')
check_arg('/?bar&foo_bar=3&foo', '3')
check_arg('/?foo_bar=l&foo_bar=4', '4')
check_no_arg('/')
check_no_arg('/?foo_bar=')
check_no_arg('/?Foo_bar=0')
check_no_arg('/?foo-bar=0')
check_no_arg('/?foo_bar=0&foo_bar=l')
set_format('$arg_foo_b%61r')
check_no_arg('/?foo_b=0')
check_no_arg('/?foo_bar=0')
set_format('$arg_f!~')
check_no_arg('/?f=0')
check_no_arg('/?f!~=0')
def test_variables_dynamic_headers(search_in_file, wait_for_record):
def check_header(header, value):
reg = fr'^{value}$'
assert search_in_file(reg, 'access.log') is None
assert (
client.get(headers={header: value, 'Connection': 'close'})['status']
== 200
)
assert wait_for_record(reg, 'access.log') is not None
def check_no_header(header):
assert (
client.get(headers={header: '0', 'Connection': 'close'})['status']
== 200
)
assert search_in_file(r'^0$', 'access.log') is None
set_format('$header_foo_bar')
check_header('foo-bar', '1')
check_header('Foo-Bar', '2')
check_no_header('foo_bar')
check_no_header('foobar')
set_format('$header_Foo_Bar')
check_header('Foo-Bar', '4')
check_header('foo-bar', '5')
check_no_header('foo_bar')
check_no_header('foobar')
def test_variables_dynamic_cookies(search_in_file, wait_for_record):
def check_no_cookie(cookie):
assert (
client.get(
headers={
'Host': 'localhost',
'Cookie': cookie,
'Connection': 'close', 'Connection': 'close',
}, },
)['status'] )['status']
== 200 == 200
) )
assert wait_for_record(reg, 'access.log') is not None assert search_in_file(r'^0$', 'access.log') is None
check_no_cookie('fOo_bar=0') set_format('$cookie_foo_bar')
check_no_cookie('foo_bar=')
def test_variables_invalid(self, temp_dir): reg = r'^1$'
def check_variables(format): assert search_in_file(reg, 'access.log') is None
assert 'error' in self.conf( assert (
{ client.get(
'path': f'{temp_dir}/access.log', headers={
'format': format, 'Host': 'localhost',
}, 'Cookie': 'foo_bar=1',
'access_log', 'Connection': 'close',
), 'access_log format' },
)['status']
== 200
)
assert wait_for_record(reg, 'access.log') is not None
check_variables("$") check_no_cookie('fOo_bar=0')
check_variables("${") check_no_cookie('foo_bar=')
check_variables("${}")
check_variables("$ur")
check_variables("$uri$$host") def test_variables_invalid(temp_dir):
check_variables("$uriblah") def check_variables(format):
check_variables("${uri") assert 'error' in client.conf(
check_variables("${{uri}") {
check_variables("$ar") 'path': f'{temp_dir}/access.log',
check_variables("$arg") 'format': format,
check_variables("$arg_") },
check_variables("$cookie") 'access_log',
check_variables("$cookie_") ), 'access_log format'
check_variables("$header")
check_variables("$header_") check_variables("$")
check_variables("${")
check_variables("${}")
check_variables("$ur")
check_variables("$uri$$host")
check_variables("$uriblah")
check_variables("${uri")
check_variables("${{uri}")
check_variables("$ar")
check_variables("$arg")
check_variables("$arg_")
check_variables("$cookie")
check_variables("$cookie_")
check_variables("$header")
check_variables("$header_")

View File

@@ -2,11 +2,11 @@ import os
import shutil import shutil
import subprocess import subprocess
from unit.applications.proto import TestApplicationProto from unit.applications.proto import ApplicationProto
from unit.option import option from unit.option import option
class TestApplicationGo(TestApplicationProto): class ApplicationGo(ApplicationProto):
@staticmethod @staticmethod
def prepare_env(script, name='app', static=False): def prepare_env(script, name='app', static=False):
try: try:
@@ -88,7 +88,7 @@ replace unit.nginx.org/go => {replace_path}
executable = f"/go/{name}" executable = f"/go/{name}"
static_build = True static_build = True
TestApplicationGo.prepare_env(script, name, static=static_build) ApplicationGo.prepare_env(script, name, static=static_build)
conf = { conf = {
"listeners": {"*:7080": {"pass": f"applications/{script}"}}, "listeners": {"*:7080": {"pass": f"applications/{script}"}},

View File

@@ -4,12 +4,13 @@ import shutil
import subprocess import subprocess
import pytest import pytest
from unit.applications.proto import TestApplicationProto from unit.applications.proto import ApplicationProto
from unit.option import option from unit.option import option
class TestApplicationJava(TestApplicationProto): class ApplicationJava(ApplicationProto):
application_type = "java" def __init__(self, application_type='java'):
self.application_type = application_type
def prepare_env(self, script): def prepare_env(self, script):
app_path = f'{option.temp_dir}/java' app_path = f'{option.temp_dir}/java'

View File

@@ -1,14 +1,15 @@
import shutil import shutil
from urllib.parse import quote from urllib.parse import quote
from unit.applications.proto import TestApplicationProto from unit.applications.proto import ApplicationProto
from unit.option import option from unit.option import option
from unit.utils import public_dir from unit.utils import public_dir
class TestApplicationNode(TestApplicationProto): class ApplicationNode(ApplicationProto):
application_type = "node" def __init__(self, application_type='node', es_modules=False):
es_modules = False self.application_type = application_type
self.es_modules = es_modules
def prepare_env(self, script): def prepare_env(self, script):
# copy application # copy application

View File

@@ -1,9 +1,10 @@
from unit.applications.proto import TestApplicationProto from unit.applications.proto import ApplicationProto
from unit.option import option from unit.option import option
class TestApplicationPerl(TestApplicationProto): class ApplicationPerl(ApplicationProto):
application_type = "perl" def __init__(self, application_type='perl'):
self.application_type = application_type
def load(self, script, name='psgi.pl', **kwargs): def load(self, script, name='psgi.pl', **kwargs):
script_path = f'{option.test_dir}/perl/{script}' script_path = f'{option.test_dir}/perl/{script}'

View File

@@ -1,12 +1,13 @@
import os import os
import shutil import shutil
from unit.applications.proto import TestApplicationProto from unit.applications.proto import ApplicationProto
from unit.option import option from unit.option import option
class TestApplicationPHP(TestApplicationProto): class ApplicationPHP(ApplicationProto):
application_type = "php" def __init__(self, application_type='php'):
self.application_type = application_type
def load(self, script, index='index.php', **kwargs): def load(self, script, index='index.php', **kwargs):
script_path = f'{option.test_dir}/php/{script}' script_path = f'{option.test_dir}/php/{script}'

View File

@@ -2,13 +2,14 @@ import os
import shutil import shutil
from urllib.parse import quote from urllib.parse import quote
from unit.applications.proto import TestApplicationProto from unit.applications.proto import ApplicationProto
from unit.option import option from unit.option import option
class TestApplicationPython(TestApplicationProto): class ApplicationPython(ApplicationProto):
application_type = "python" def __init__(self, application_type='python', load_module='wsgi'):
load_module = "wsgi" self.application_type = application_type
self.load_module = load_module
def load(self, script, name=None, module=None, **kwargs): def load(self, script, name=None, module=None, **kwargs):
if name is None: if name is None:

View File

@@ -1,12 +1,13 @@
import shutil import shutil
from unit.applications.proto import TestApplicationProto from unit.applications.proto import ApplicationProto
from unit.option import option from unit.option import option
from unit.utils import public_dir from unit.utils import public_dir
class TestApplicationRuby(TestApplicationProto): class ApplicationRuby(ApplicationProto):
application_type = "ruby" def __init__(self, application_type='ruby'):
self.application_type = application_type
def prepare_env(self, script): def prepare_env(self, script):
shutil.copytree( shutil.copytree(

View File

@@ -1,10 +1,10 @@
import os import os
from unit.control import TestControl from unit.control import Control
from unit.option import option from unit.option import option
class TestApplicationProto(TestControl): class ApplicationProto(Control):
application_type = None application_type = None
def get_application_type(self): def get_application_type(self):

View File

@@ -2,15 +2,15 @@ import os
import ssl import ssl
import subprocess import subprocess
from unit.applications.proto import TestApplicationProto from unit.applications.proto import ApplicationProto
from unit.option import option from unit.option import option
class TestApplicationTLS(TestApplicationProto): class ApplicationTLS(ApplicationProto):
def setup_method(self): def __init__(self):
self.context = ssl.create_default_context() self._default_context = ssl.create_default_context()
self.context.check_hostname = False self._default_context.check_hostname = False
self.context.verify_mode = ssl.CERT_NONE self._default_context.verify_mode = ssl.CERT_NONE
def certificate(self, name='default', load=True): def certificate(self, name='default', load=True):
self.openssl_conf() self.openssl_conf()
@@ -47,10 +47,12 @@ class TestApplicationTLS(TestApplicationProto):
return self.conf(k.read() + c.read(), f'/certificates/{crt}') return self.conf(k.read() + c.read(), f'/certificates/{crt}')
def get_ssl(self, **kwargs): def get_ssl(self, **kwargs):
return self.get(wrapper=self.context.wrap_socket, **kwargs) context = kwargs.get('context', self._default_context)
return self.get(wrapper=context.wrap_socket, **kwargs)
def post_ssl(self, **kwargs): def post_ssl(self, **kwargs):
return self.post(wrapper=self.context.wrap_socket, **kwargs) context = kwargs.get('context', self._default_context)
return self.post(wrapper=context.wrap_socket, **kwargs)
def openssl_conf(self, rewrite=False, alt_names=None): def openssl_conf(self, rewrite=False, alt_names=None):
alt_names = alt_names or [] alt_names = alt_names or []

View File

@@ -6,12 +6,12 @@ import select
import struct import struct
import pytest import pytest
from unit.applications.proto import TestApplicationProto from unit.applications.proto import ApplicationProto
GUID = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11" GUID = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"
class TestApplicationWebsocket(TestApplicationProto): class ApplicationWebsocket(ApplicationProto):
OP_CONT = 0x00 OP_CONT = 0x00
OP_TEXT = 0x01 OP_TEXT = 0x01

View File

@@ -1,9 +1,9 @@
import json import json
from unit.http import TestHTTP from unit.http import HTTP1
from unit.option import option from unit.option import option
http = TestHTTP() http = HTTP1()
def check_chroot(): def check_chroot():

View File

@@ -1,5 +1,5 @@
from unit.applications.lang.go import TestApplicationGo from unit.applications.lang.go import ApplicationGo
def check_go(): def check_go():
return TestApplicationGo.prepare_env('empty') is not None return ApplicationGo.prepare_env('empty') is not None

View File

@@ -1,16 +1,16 @@
import json import json
import os import os
from unit.applications.lang.go import TestApplicationGo from unit.applications.lang.go import ApplicationGo
from unit.applications.lang.java import TestApplicationJava from unit.applications.lang.java import ApplicationJava
from unit.applications.lang.node import TestApplicationNode from unit.applications.lang.node import ApplicationNode
from unit.applications.lang.ruby import TestApplicationRuby from unit.applications.lang.ruby import ApplicationRuby
from unit.http import TestHTTP from unit.http import HTTP1
from unit.option import option from unit.option import option
from unit.utils import getns from unit.utils import getns
allns = ['pid', 'mnt', 'ipc', 'uts', 'cgroup', 'net'] allns = ['pid', 'mnt', 'ipc', 'uts', 'cgroup', 'net']
http = TestHTTP() http = HTTP1()
def check_isolation(): def check_isolation():
@@ -18,7 +18,7 @@ def check_isolation():
conf = '' conf = ''
if 'go' in available['modules']: if 'go' in available['modules']:
TestApplicationGo().prepare_env('empty', 'app') ApplicationGo().prepare_env('empty', 'app')
conf = { conf = {
"listeners": {"*:7080": {"pass": "applications/empty"}}, "listeners": {"*:7080": {"pass": "applications/empty"}},
@@ -64,7 +64,7 @@ def check_isolation():
} }
elif 'ruby' in available['modules']: elif 'ruby' in available['modules']:
TestApplicationRuby().prepare_env('empty') ApplicationRuby().prepare_env('empty')
conf = { conf = {
"listeners": {"*:7080": {"pass": "applications/empty"}}, "listeners": {"*:7080": {"pass": "applications/empty"}},
@@ -80,7 +80,7 @@ def check_isolation():
} }
elif 'java' in available['modules']: elif 'java' in available['modules']:
TestApplicationJava().prepare_env('empty') ApplicationJava().prepare_env('empty')
conf = { conf = {
"listeners": {"*:7080": {"pass": "applications/empty"}}, "listeners": {"*:7080": {"pass": "applications/empty"}},
@@ -97,7 +97,7 @@ def check_isolation():
} }
elif 'node' in available['modules']: elif 'node' in available['modules']:
TestApplicationNode().prepare_env('basic') ApplicationNode().prepare_env('basic')
conf = { conf = {
"listeners": {"*:7080": {"pass": "applications/basic"}}, "listeners": {"*:7080": {"pass": "applications/basic"}},

View File

@@ -1,9 +1,9 @@
import json import json
from unit.http import TestHTTP from unit.http import HTTP1
from unit.option import option from unit.option import option
http = TestHTTP() http = HTTP1()
def check_unix_abstract(): def check_unix_abstract():

View File

@@ -1,6 +1,6 @@
import json import json
from unit.http import TestHTTP from unit.http import HTTP1
from unit.option import option from unit.option import option
@@ -29,7 +29,7 @@ def args_handler(conf_func):
return args_wrapper return args_wrapper
class TestControl(TestHTTP): class Control(HTTP1):
@args_handler @args_handler
def conf(self, conf, url): def conf(self, conf, url):
return self.put(**self._get_args(url, conf))['body'] return self.put(**self._get_args(url, conf))['body']

View File

@@ -10,7 +10,7 @@ import pytest
from unit.option import option from unit.option import option
class TestHTTP: class HTTP1:
def http(self, start_str, **kwargs): def http(self, start_str, **kwargs):
sock_type = kwargs.get('sock_type', 'ipv4') sock_type = kwargs.get('sock_type', 'ipv4')
port = kwargs.get('port', 7080) port = kwargs.get('port', 7080)

View File

@@ -1,6 +1,7 @@
import os import os
import platform import platform
class Options: class Options:
_options = { _options = {
'architecture': platform.architecture()[0], 'architecture': platform.architecture()[0],
@@ -8,7 +9,7 @@ class Options:
'is_privileged': os.geteuid() == 0, 'is_privileged': os.geteuid() == 0,
'skip_alerts': [], 'skip_alerts': [],
'skip_sanitizer': False, 'skip_sanitizer': False,
'system': platform.system() 'system': platform.system(),
} }
def __setattr__(self, name, value): def __setattr__(self, name, value):

View File

@@ -1,9 +1,9 @@
from unit.control import TestControl from unit.control import Control
class Status: class Status:
_status = None _status = None
control = TestControl() control = Control()
def _check_zeros(): def _check_zeros():
assert Status.control.conf_get('/status') == { assert Status.control.conf_get('/status') == {