2022-12-20 17:04:06 +03:00
# -*- coding: utf-8 -*-
#
# Copyright (c) 2022 Virtual Cable S.L.U.
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without modification,
# are permitted provided that the following conditions are met:
#
# * Redistributions of source code must retain the above copyright notice,
# this list of conditions and the following disclaimer.
# * Redistributions in binary form must reproduce the above copyright notice,
# this list of conditions and the following disclaimer in the documentation
# and/or other materials provided with the distribution.
# * Neither the name of Virtual Cable S.L. nor the names of its contributors
# may be used to endorse or promote products derived from this software
# without specific prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
'''
Author : Adolfo Gómez , dkmaster at dkmon dot com
'''
import typing
import asyncio
2022-12-20 20:31:04 +03:00
import random
2022-12-20 17:04:06 +03:00
import logging
from unittest import IsolatedAsyncioTestCase , mock
2023-01-06 01:47:32 +03:00
from uds_tunnel import consts , stats
2022-12-20 17:04:06 +03:00
2022-12-21 16:29:25 +03:00
from . utils import tuntools , tools , conf
2022-12-20 17:04:06 +03:00
2022-12-20 20:31:04 +03:00
if typing . TYPE_CHECKING :
from uds_tunnel import config
2022-12-20 17:04:06 +03:00
logger = logging . getLogger ( __name__ )
2023-05-21 06:35:52 +03:00
2022-12-20 17:04:06 +03:00
class TestUDSTunnelApp ( IsolatedAsyncioTestCase ) :
2023-05-21 17:19:58 +03:00
async def client_task ( self , host : str , tunnel_port : int , remote_port : int , use_tunnel_handshake : bool = False ) - > None :
2022-12-20 20:31:04 +03:00
received : bytes = b ' '
callback_invoked : asyncio . Event = asyncio . Event ( )
2022-12-21 23:06:42 +03:00
# Data sent will be received by server
# One single write will ensure all data is on same packet
test_str = (
2023-05-21 06:35:52 +03:00
b ' Some Random Data ' + bytes ( random . randint ( 0 , 255 ) for _ in range ( 1024 ) ) * 4 + b ' STREAM_END ' # nosec: just testing data
2023-01-06 01:47:32 +03:00
) # length = 16 + 1024 * 4 + 10 = 4122
2023-05-21 06:35:52 +03:00
test_response = bytes ( random . randint ( 48 , 127 ) for _ in range ( 12 ) ) # nosec: length = 12, random printable chars
2022-12-20 20:31:04 +03:00
2023-01-06 01:47:32 +03:00
def callback ( data : bytes ) - > typing . Optional [ bytes ] :
2022-12-20 20:31:04 +03:00
nonlocal received
received + = data
# if data contains EOS marcker ('STREAM_END'), we are done
if b ' STREAM_END ' in data :
callback_invoked . set ( )
2023-01-06 01:47:32 +03:00
return test_response
return None
2022-12-20 20:31:04 +03:00
async with tools . AsyncTCPServer (
2022-12-21 23:06:42 +03:00
host = host , port = remote_port , callback = callback , name = ' client_task '
2023-05-21 06:45:51 +03:00
) as server : # pylint: disable=unused-variable
2022-12-20 20:31:04 +03:00
# Create a random ticket with valid format
2022-12-21 23:06:42 +03:00
ticket = tuntools . get_correct_ticket ( prefix = f ' bX0bwmb { remote_port } bX0bwmb ' )
2022-12-20 20:31:04 +03:00
# Open and send handshake
2022-12-21 16:29:25 +03:00
# Fake config, only needed data for open_tunnel_client
cfg = mock . MagicMock ( )
cfg . ipv6 = ' : ' in host
cfg . listen_address = host
2022-12-21 23:06:42 +03:00
cfg . listen_port = tunnel_port
2022-12-21 16:29:25 +03:00
2022-12-21 23:06:42 +03:00
async with tuntools . open_tunnel_client (
2023-05-21 17:19:58 +03:00
cfg , local_port = remote_port + 10000 , use_tunnel_handshake = use_tunnel_handshake
2022-12-21 23:06:42 +03:00
) as (
2022-12-20 20:31:04 +03:00
creader ,
cwriter ,
) :
# Now open command with ticket
cwriter . write ( consts . COMMAND_OPEN )
# fake ticket, consts.TICKET_LENGTH bytes long, letters and numbers. Use a random ticket,
cwriter . write ( ticket )
await cwriter . drain ( )
# Read response, should be ok
data = await creader . read ( 1024 )
2022-12-21 23:06:42 +03:00
logger . debug ( ' Received response: %r ' , data )
2022-12-20 20:31:04 +03:00
self . assertEqual (
data ,
consts . RESPONSE_OK ,
2022-12-21 23:06:42 +03:00
f ' Server host: { host } : { tunnel_port } - Ticket: { ticket !r} - Response: { data !r} ' ,
2022-12-20 20:31:04 +03:00
)
2022-12-21 23:06:42 +03:00
# Clean received data
received = b ' '
# And reset event
callback_invoked . clear ( )
cwriter . write ( test_str )
await cwriter . drain ( )
2023-01-06 01:47:32 +03:00
# Read response, should be just FAKE_OK_RESPONSE
data = await creader . read ( 1024 )
logger . debug ( ' Received response: %r ' , data )
self . assertEqual (
data ,
test_response ,
f ' Server host: { host } : { tunnel_port } - Ticket: { ticket !r} - Response: { data !r} ' ,
)
2022-12-21 23:06:42 +03:00
# Close connection
cwriter . close ( )
# Wait for callback to be invoked
await callback_invoked . wait ( )
self . assertEqual ( received , test_str )
2022-12-20 17:04:06 +03:00
2022-12-22 17:25:45 +03:00
async def test_app_concurrency ( self ) - > None :
2023-05-21 17:19:58 +03:00
concurrent_tasks = 1024
2022-12-21 23:06:42 +03:00
fake_broker_port = 20000
tunnel_server_port = fake_broker_port + 1
remote_port = fake_broker_port + 2
2023-05-21 06:35:52 +03:00
2022-12-21 23:06:42 +03:00
# Extracts the port from an string that has bX0bwmbPORTbX0bwmb in it
def extract_port ( data : bytes ) - > int :
if b ' bX0bwmb ' not in data :
return 12345 # No port, wil not be used because is an "stop" request
return int ( data . split ( b ' bX0bwmb ' ) [ 1 ] )
2022-12-20 17:04:06 +03:00
for host in ( ' 127.0.0.1 ' , ' ::1 ' ) :
2022-12-21 16:29:25 +03:00
if ' : ' in host :
2022-12-21 23:06:42 +03:00
url = f ' http://[ { host } ]: { fake_broker_port } /uds/rest '
2022-12-21 16:29:25 +03:00
else :
2022-12-21 23:06:42 +03:00
url = f ' http:// { host } : { fake_broker_port } /uds/rest '
2022-12-21 16:29:25 +03:00
# Create fake uds broker
async with tuntools . create_fake_broker_server (
2022-12-21 23:06:42 +03:00
host ,
fake_broker_port ,
2023-05-21 06:45:51 +03:00
response = lambda data : conf . UDS_GET_TICKET_RESPONSE ( host , extract_port ( data ) ) , # pylint: disable=cell-var-from-loop
2022-12-21 23:06:42 +03:00
) as req_queue :
if req_queue is None :
raise AssertionError ( ' req_queue is None ' )
2022-12-21 16:29:25 +03:00
async with tuntools . tunnel_app_runner (
2022-12-21 23:06:42 +03:00
host ,
tunnel_server_port ,
wait_for_port = True ,
# Tunnel config
uds_server = url ,
2023-05-21 06:45:51 +03:00
logfile = ' /tmp/tunnel_test.log ' , # nosec: Testing file, fine to be in /tmp
2022-12-21 16:29:25 +03:00
loglevel = ' DEBUG ' ,
2022-12-21 23:06:42 +03:00
workers = 4 ,
2023-01-06 01:47:32 +03:00
command_timeout = 16 , # Increase command timeout because heavy load we will create
2023-05-21 06:45:51 +03:00
) as process : # pylint: disable=unused-variable
2022-12-21 16:29:25 +03:00
# Create a "bunch" of clients
tasks = [
2023-05-21 17:19:58 +03:00
asyncio . create_task ( self . client_task ( host , tunnel_server_port , remote_port + i , use_tunnel_handshake = True ) )
2022-12-22 17:25:45 +03:00
async for i in tools . waitable_range ( concurrent_tasks )
2022-12-21 16:29:25 +03:00
]
# Wait for all tasks to finish
await asyncio . wait ( tasks , return_when = asyncio . ALL_COMPLETED )
2022-12-21 23:06:42 +03:00
2022-12-21 16:29:25 +03:00
# If any exception was raised, raise it
for task in tasks :
task . result ( )
2022-12-21 23:06:42 +03:00
# Queue should have all requests (concurrent_tasks*2, one for open and one for close)
self . assertEqual ( req_queue . qsize ( ) , concurrent_tasks * 2 )
2022-12-22 17:25:45 +03:00
async def test_tunnel_proc_concurrency ( self ) - > None :
concurrent_tasks = 512
fake_broker_port = 20000
tunnel_server_port = fake_broker_port + 1
remote_port = fake_broker_port + 2
# Extracts the port from an string that has bX0bwmbPORTbX0bwmb in it
2023-05-21 06:35:52 +03:00
req_queue : ' asyncio.Queue[bytes] ' = asyncio . Queue ( )
2022-12-22 17:25:45 +03:00
def extract_port ( data : bytes ) - > int :
2023-01-06 01:47:32 +03:00
logger . debug ( ' Data: %r ' , data )
2022-12-22 17:25:45 +03:00
req_queue . put_nowait ( data )
if b ' bX0bwmb ' not in data :
return 12345 # No port, wil not be used because is an "stop" request
return int ( data . split ( b ' bX0bwmb ' ) [ 1 ] )
for host in ( ' 127.0.0.1 ' , ' ::1 ' ) :
2023-01-06 01:47:32 +03:00
req_queue = asyncio . Queue ( ) # clear queue
2022-12-22 17:25:45 +03:00
# Use tunnel proc for testing
2023-01-06 01:47:32 +03:00
stats_collector = stats . GlobalStats ( )
2022-12-22 17:25:45 +03:00
async with tuntools . create_tunnel_proc (
host ,
tunnel_server_port ,
2023-05-21 06:45:51 +03:00
response = lambda data : conf . UDS_GET_TICKET_RESPONSE ( host , extract_port ( data ) ) , # pylint: disable=cell-var-from-loop
2023-01-06 01:47:32 +03:00
command_timeout = 16 , # Increase command timeout because heavy load we will create,
global_stats = stats_collector ,
2023-05-21 06:45:51 +03:00
) as _ : # (_ is a tuple, but not used here, just the context)
2022-12-22 17:25:45 +03:00
# Create a "bunch" of clients
tasks = [
2023-05-21 06:35:52 +03:00
asyncio . create_task ( self . client_task ( host , tunnel_server_port , remote_port + i ) )
2022-12-22 17:25:45 +03:00
async for i in tools . waitable_range ( concurrent_tasks )
]
# Wait for tasks to finish and check for exceptions
await asyncio . wait ( tasks , return_when = asyncio . ALL_COMPLETED )
# If any exception was raised, raise it
2023-01-06 01:47:32 +03:00
await asyncio . gather ( * tasks , return_exceptions = True )
2022-12-22 17:25:45 +03:00
# Queue should have all requests (concurrent_tasks*2, one for open and one for close)
self . assertEqual ( req_queue . qsize ( ) , concurrent_tasks * 2 )
2023-05-21 06:35:52 +03:00
2023-01-06 01:47:32 +03:00
# Check stats
2023-05-21 06:35:52 +03:00
self . assertEqual ( stats_collector . ns . recv , concurrent_tasks * 12 )
self . assertEqual ( stats_collector . ns . sent , concurrent_tasks * 4122 )
2023-01-06 01:47:32 +03:00
self . assertEqual ( stats_collector . ns . total , concurrent_tasks )