"""Test the server's shell(reader, writer) callback.""" # std imports import asyncio import logging # local from telnetlib3 import accessories, telnet_server_shell from telnetlib3.telopt import DO, IAC, SGA, ECHO, WILL, WONT, TTYPE, BINARY from telnetlib3.tests.accessories import ( create_server, asyncio_server, open_connection, asyncio_connection, ) async def test_telnet_server_shell_as_coroutine(bind_host, unused_tcp_port): """Test callback shell(reader, writer) as coroutine of create_server().""" _waiter = asyncio.Future() send_input = "Alpha" expect_output = "Beta" expect_hello = IAC + DO + TTYPE hello_reply = IAC + WONT + TTYPE async def shell(reader, writer): _waiter.set_result(True) inp = await reader.readexactly(len(send_input)) assert inp == send_input writer.write(expect_output) await writer.drain() writer.close() await writer.wait_closed() async with create_server(host=bind_host, port=unused_tcp_port, shell=shell): async with asyncio_connection(bind_host, unused_tcp_port) as (reader, writer): # verify IAC DO TTYPE hello = await asyncio.wait_for(reader.readexactly(len(expect_hello)), 0.5) assert hello == expect_hello # respond 'WONT TTYPE' to quickly complete negotiation as failed. writer.write(hello_reply) # await for the shell callback to be ready, await asyncio.wait_for(_waiter, 0.5) # client sends input, reads shell output response writer.write(send_input.encode("ascii")) server_output = await asyncio.wait_for(reader.readexactly(len(expect_output)), 0.5) assert server_output.decode("ascii") == expect_output # nothing more to read from server; server writer closed in shell. result = await reader.read() assert result == b"" async def test_telnet_client_shell_as_coroutine(bind_host, unused_tcp_port): """Test callback shell(reader, writer) as coroutine of create_server().""" _waiter = asyncio.Future() async def shell(reader, writer): # just hang up _waiter.set_result(True) # a server that doesn't care async with asyncio_server(asyncio.Protocol, bind_host, unused_tcp_port): async with open_connection(host=bind_host, port=unused_tcp_port, shell=shell) as ( reader, writer, ): await asyncio.wait_for(_waiter, 0.5) async def test_telnet_server_shell_make_coro_by_function(bind_host, unused_tcp_port): """Test callback shell(reader, writer) as function, for create_server().""" _waiter = asyncio.Future() def shell(reader, writer): _waiter.set_result(True) async with create_server(host=bind_host, port=unused_tcp_port, shell=shell): async with asyncio_connection(bind_host, unused_tcp_port) as (reader, writer): # cancel negotiation and await for the shell callback writer.write(IAC + WONT + TTYPE) await asyncio.wait_for(_waiter, 0.5) async def test_telnet_server_no_shell(bind_host, unused_tcp_port): """Test telnetlib3.TelnetServer() instantiation and connection_made().""" client_expected = IAC + DO + TTYPE + b"beta" async with create_server(host=bind_host, port=unused_tcp_port) as server: async with asyncio_connection(bind_host, unused_tcp_port) as (client_reader, client_writer): client_writer.write(IAC + WONT + TTYPE + b"alpha") srv_instance = await asyncio.wait_for(server.wait_for_client(), 0.5) srv_instance.writer.write("beta") srv_instance.writer.close() await srv_instance.writer.wait_closed() client_recv = await client_reader.read() assert client_recv == client_expected async def test_telnet_server_given_shell(bind_host, unused_tcp_port): """Iterate all state-reading commands of default telnet_server_shell.""" async with create_server( host=bind_host, port=unused_tcp_port, shell=telnet_server_shell, connect_maxwait=0.5, timeout=1.25, limit=13377, never_send_ga=True, ) as server: async with asyncio_connection(bind_host, unused_tcp_port) as (reader, writer): expected = IAC + DO + TTYPE result = await asyncio.wait_for(reader.readexactly(len(expected)), 0.5) assert result == expected writer.write(IAC + WONT + TTYPE) expected = b"Ready.\r\ntel:sh> " result = await asyncio.wait_for(reader.readexactly(len(expected)), 0.5) assert result == expected srv_instance = await asyncio.wait_for(server.wait_for_client(), 0.5) server_port = str(srv_instance._transport.get_extra_info("peername")[1]) # Command & Response table cmd_output_table = ( # exercise backspace in input for help command ( (b"\bhel\blp\r"), ( b"\r\nquit, writer, slc, linemode, toggle [option|all], reader, proto, dump" b"\r\ntel:sh> " ), ), ( b"writer\r\x00", ( b"\r\n" b"\r\ntel:sh> " ), ), ( b"reader\r\n", ( b"\r\n\r\ntel:sh> " ), ), ( b"proto\n", ( b"\r\n" + b"\r\ntel:sh> " ), ), ( b"slc\r\n", ( b"\r\nSpecial Line Characters:" b"\r\n SLC_AO: (^O, variable|flushout)" b"\r\n SLC_EC: (^?, variable)" b"\r\n SLC_EL: (^U, variable)" b"\r\n SLC_EW: (^W, variable)" b"\r\n SLC_IP: (^C, variable|flushin|flushout)" b"\r\n SLC_RP: (^R, variable)" b"\r\n SLC_AYT: (^T, variable)" b"\r\n SLC_EOF: (^D, variable)" b"\r\n SLC_XON: (^Q, variable)" b"\r\n SLC_SUSP: (^Z, variable|flushin)" b"\r\n SLC_XOFF: (^S, variable)" b"\r\n SLC_ABORT: (^\\, variable|flushin|flushout)" b"\r\n SLC_LNEXT: (^V, variable)" b"\r\nUnset by client: SLC_BRK, SLC_EOR, SLC_SYNCH" b"\r\nNot supported by server: SLC_EBOL, SLC_ECR, SLC_EEOL, " b"SLC_EWR, SLC_FORW1, SLC_FORW2, SLC_INSRT, SLC_MCBOL, " b"SLC_MCEOL, SLC_MCL, SLC_MCR, SLC_MCWL, SLC_MCWR, SLC_OVER" b"\r\ntel:sh> " ), ), ( b"toggle\n", ( b"\r\nbinary off" b"\r\necho off" b"\r\ngoahead ON" b"\r\ninbinary off" b"\r\nlflow ON" b"\r\nlinemode off" b"\r\nlinemode-edit off" b"\r\nlinemode-trapsig off" b"\r\noutbinary off" b"\r\nxon-any off" b"\r\ntel:sh> " ), ), (b"toggle not-an-option\r", (b"\r\ntoggle: not an option." b"\r\ntel:sh> ")), ( b"toggle all\r\n", ( b"\r\n" + # negotiation options received, # though ignored by our dumb client. IAC + WILL + ECHO + IAC + WILL + SGA + IAC + WILL + BINARY + IAC + DO + BINARY + b"will echo." b"\r\nwill suppress go-ahead." b"\r\nwill outbinary." b"\r\ndo inbinary." b"\r\nxon-any enabled." b"\r\nlineflow disabled." b"\r\ntel:sh> " ), ), ( b"toggle\n", ( # and therefore the same state values remain unchanged -- # with exception of lineflow and xon-any, which are # states toggled by the shell directly (and presumably # knows what to do with it!) b"\r\nbinary off" b"\r\necho off" b"\r\ngoahead ON" b"\r\ninbinary off" b"\r\nlflow off" # flipped b"\r\nlinemode off" b"\r\nlinemode-edit off" b"\r\nlinemode-trapsig off" b"\r\noutbinary off" b"\r\nxon-any ON" # flipped b"\r\ntel:sh> " ), ), (b"\r\n", (b"\r\ntel:sh> ")), (b"not-a-command\n", (b"\r\nno such command." b"\r\ntel:sh> ")), (b"quit\r", b"\r\nGoodbye.\r\n"), ) for cmd, output_expected in cmd_output_table: logging.debug("cmd=%r, output_expected=%r", cmd, output_expected) writer.write(cmd) await writer.drain() timed_out = False try: result = await asyncio.wait_for(reader.readexactly(len(output_expected)), 0.5) except asyncio.IncompleteReadError as err: result = err.partial except TimeoutError: result = await reader.read(1024) else: if result != output_expected: # fetch extra output, if any, for better understanding of error result += await reader.read(1024) assert result == output_expected and timed_out is False # nothing more to read. result = await reader.read() assert result == b"" async def test_telnet_server_shell_eof(bind_host, unused_tcp_port): """Test EOF in telnet_server_shell().""" async with create_server( host=bind_host, port=unused_tcp_port, shell=telnet_server_shell, timeout=0.25 ) as server: async with asyncio_connection(bind_host, unused_tcp_port) as (reader, writer): writer.write(IAC + WONT + TTYPE) srv_instance = await asyncio.wait_for(server.wait_for_client(), 0.5) # Wait for server to process client disconnect await asyncio.sleep(0.05) assert srv_instance._closing async def test_telnet_server_shell_version_command(bind_host, unused_tcp_port): """Test version command in telnet_server_shell.""" async with create_server( host=bind_host, port=unused_tcp_port, shell=telnet_server_shell, connect_maxwait=0.5 ): async with asyncio_connection(bind_host, unused_tcp_port) as (reader, writer): expected = IAC + DO + TTYPE result = await asyncio.wait_for(reader.readexactly(len(expected)), 0.5) assert result == expected writer.write(IAC + WONT + TTYPE) expected = b"Ready.\r\ntel:sh> " result = await asyncio.wait_for(reader.readexactly(len(expected)), 0.5) assert result == expected writer.write(b"version\r") await asyncio.sleep(0.05) result = b"" while True: try: chunk = await asyncio.wait_for(reader.read(100), 0.2) if not chunk: break result += chunk if b"tel:sh>" in result: break except asyncio.TimeoutError: break expected_version = accessories.get_version() assert expected_version.encode("ascii") in result async def test_telnet_server_shell_dump_with_kb_limit(bind_host, unused_tcp_port): """Test dump command with explicit kb_limit.""" async with create_server( host=bind_host, port=unused_tcp_port, shell=telnet_server_shell, connect_maxwait=0.5 ): async with asyncio_connection(bind_host, unused_tcp_port) as (reader, writer): expected = IAC + DO + TTYPE await asyncio.wait_for(reader.readexactly(len(expected)), 0.5) writer.write(IAC + WONT + TTYPE) expected = b"Ready.\r\ntel:sh> " await asyncio.wait_for(reader.readexactly(len(expected)), 0.5) writer.write(b"dump 0\r") await asyncio.sleep(0.05) result = b"" while True: try: chunk = await asyncio.wait_for(reader.read(200), 0.2) if not chunk: break result += chunk if b"wrote 0 bytes" in result: break except asyncio.TimeoutError: break assert b"kb_limit=0" in result assert b"wrote 0 bytes" in result async def test_telnet_server_shell_dump_with_all_options(bind_host, unused_tcp_port): """Test dump command with all options including close.""" async with create_server( host=bind_host, port=unused_tcp_port, shell=telnet_server_shell, connect_maxwait=0.5 ): async with asyncio_connection(bind_host, unused_tcp_port) as (reader, writer): expected = IAC + DO + TTYPE await asyncio.wait_for(reader.readexactly(len(expected)), 0.5) writer.write(IAC + WONT + TTYPE) expected = b"Ready.\r\ntel:sh> " await asyncio.wait_for(reader.readexactly(len(expected)), 0.5) writer.write(b"dump 0 0 nodrain close\r") await asyncio.sleep(0.05) result = b"" while True: try: chunk = await asyncio.wait_for(reader.read(300), 0.2) if not chunk: break result += chunk except asyncio.TimeoutError: break assert b"kb_limit=0" in result assert b"do_close=True" in result assert b"drain=True" in result async def test_telnet_server_shell_dump_nodrain(bind_host, unused_tcp_port): """Test dump command with nodrain option.""" async with create_server( host=bind_host, port=unused_tcp_port, shell=telnet_server_shell, connect_maxwait=0.5 ): async with asyncio_connection(bind_host, unused_tcp_port) as (reader, writer): expected = IAC + DO + TTYPE await asyncio.wait_for(reader.readexactly(len(expected)), 0.5) writer.write(IAC + WONT + TTYPE) expected = b"Ready.\r\ntel:sh> " await asyncio.wait_for(reader.readexactly(len(expected)), 0.5) writer.write(b"dump 0 0 drain\r") await asyncio.sleep(0.05) result = b"" while True: try: chunk = await asyncio.wait_for(reader.read(200), 0.2) if not chunk: break result += chunk if b"drain=False" in result: break except asyncio.TimeoutError: break assert b"kb_limit=0" in result assert b"drain=False" in result async def test_telnet_server_shell_dump_large_output(bind_host, unused_tcp_port): """Test dump command with larger output.""" async with create_server( host=bind_host, port=unused_tcp_port, shell=telnet_server_shell, connect_maxwait=0.5 ): async with asyncio_connection(bind_host, unused_tcp_port) as (reader, writer): expected = IAC + DO + TTYPE await asyncio.wait_for(reader.readexactly(len(expected)), 0.5) writer.write(IAC + WONT + TTYPE) expected = b"Ready.\r\ntel:sh> " await asyncio.wait_for(reader.readexactly(len(expected)), 0.5) writer.write(b"dump 1\r") await asyncio.sleep(0.05) result = b"" while True: try: chunk = await asyncio.wait_for(reader.read(4096), 0.5) if not chunk: break result += chunk if b"wrote" in result and b"bytes" in result: break except asyncio.TimeoutError: break assert b"kb_limit=1" in result assert b"/" in result or b"\\" in result