From 3b5e664c3b750febdd3e4a8f5ad84c77ee735970 Mon Sep 17 00:00:00 2001 From: Max Dymond Date: Mon, 26 Jun 2017 16:46:53 +0100 Subject: [PATCH 1/3] test1450: add simple testing for DICT Add a new server which provides a DICT interface. This is intended to begin coverage testing for lib/dict.c Closes #1615 Upstream-commit: 9b387034ab5d0d98d1ef4b6c2d0d8cd2185081fb Signed-off-by: Kamil Dudka --- tests/dictserver.py | 156 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 156 insertions(+) create mode 100755 tests/dictserver.py diff --git a/tests/dictserver.py b/tests/dictserver.py new file mode 100755 index 0000000..45cc155 --- /dev/null +++ b/tests/dictserver.py @@ -0,0 +1,156 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +# +""" DICT server """ + +from __future__ import (absolute_import, division, print_function, + unicode_literals) +import argparse +import os +import sys +import logging +try: # Python 2 + import SocketServer as socketserver +except ImportError: # Python 3 + import socketserver + + +log = logging.getLogger(__name__) +HOST = "localhost" + +# The strings that indicate the test framework is checking our aliveness +VERIFIED_REQ = b"verifiedserver" +VERIFIED_RSP = "WE ROOLZ: {pid}" + + +def dictserver(options): + """ + Starts up a TCP server with a DICT handler and serves DICT requests + forever. + """ + if options.pidfile: + pid = os.getpid() + with open(options.pidfile, "w") as f: + f.write("{0}".format(pid)) + + local_bind = (HOST, options.port) + log.info("[DICT] Listening on %s", local_bind) + + # Need to set the allow_reuse on the class, not on the instance. + socketserver.TCPServer.allow_reuse_address = True + server = socketserver.TCPServer(local_bind, DictHandler) + server.serve_forever() + + return ScriptRC.SUCCESS + + +class DictHandler(socketserver.BaseRequestHandler): + """Handler class for DICT connections. + + """ + def handle(self): + """ + Simple function which responds to all queries with a 552. + """ + + # First, send a response to allow the server to continue. + rsp = "220 dictserver \n" + self.request.sendall(rsp.encode("utf-8")) + + # Receive the request. + data = self.request.recv(1024).strip() + log.debug("[DICT] Incoming data: %r", data) + + if VERIFIED_REQ in data: + log.debug("[DICT] Received verification request from test " + "framework") + response_data = VERIFIED_RSP.format(pid=os.getpid()) + else: + log.debug("[DICT] Received normal request") + response_data = "No matches" + + # Send back a failure to find. + response = "552 {0}\n".format(response_data) + log.debug("[DICT] Responding with %r", response) + self.request.sendall(response.encode("utf-8")) + + +def get_options(): + parser = argparse.ArgumentParser() + + parser.add_argument("--port", action="store", default=9016, + type=int, help="port to listen on") + parser.add_argument("--verbose", action="store", type=int, default=0, + help="verbose output") + parser.add_argument("--pidfile", action="store", + help="file name for the PID") + parser.add_argument("--logfile", action="store", + help="file name for the log") + parser.add_argument("--srcdir", action="store", help="test directory") + parser.add_argument("--id", action="store", help="server ID") + parser.add_argument("--ipv4", action="store_true", default=0, + help="IPv4 flag") + + return parser.parse_args() + + +def setup_logging(options): + """ + Set up logging from the command line options + """ + root_logger = logging.getLogger() + add_stdout = False + + formatter = logging.Formatter("%(asctime)s %(levelname)-5.5s %(message)s") + + # Write out to a logfile + if options.logfile: + handler = logging.FileHandler(options.logfile, mode="w") + handler.setFormatter(formatter) + handler.setLevel(logging.DEBUG) + root_logger.addHandler(handler) + else: + # The logfile wasn't specified. Add a stdout logger. + add_stdout = True + + if options.verbose: + # Add a stdout logger as well in verbose mode + root_logger.setLevel(logging.DEBUG) + add_stdout = True + else: + root_logger.setLevel(logging.INFO) + + if add_stdout: + stdout_handler = logging.StreamHandler(sys.stdout) + stdout_handler.setFormatter(formatter) + stdout_handler.setLevel(logging.DEBUG) + root_logger.addHandler(stdout_handler) + + +class ScriptRC(object): + """Enum for script return codes""" + SUCCESS = 0 + FAILURE = 1 + EXCEPTION = 2 + + +class ScriptException(Exception): + pass + + +if __name__ == '__main__': + # Get the options from the user. + options = get_options() + + # Setup logging using the user options + setup_logging(options) + + # Run main script. + try: + rc = dictserver(options) + except Exception as e: + log.exception(e) + rc = ScriptRC.EXCEPTION + + log.info("[DICT] Returning %d", rc) + sys.exit(rc) -- 2.9.4 From ff33b777b2556cdf285c87723bc2f2d78fd1c454 Mon Sep 17 00:00:00 2001 From: Max Dymond Date: Fri, 30 Jun 2017 12:45:17 +0100 Subject: [PATCH 2/3] test1450: fix up DICT server in torture mode As per https://github.com/curl/curl/pull/1615, the DICT server is a little spammy in torture mode due to the sockets being torn down unexpectedly. Fix this by adding some error handling to the handling function. Closes #1629 Upstream-commit: 0362c778418a7c0594dae5537f91247ca1dae289 Signed-off-by: Kamil Dudka --- tests/dictserver.py | 39 +++++++++++++++++++++------------------ 1 file changed, 21 insertions(+), 18 deletions(-) diff --git a/tests/dictserver.py b/tests/dictserver.py index 45cc155..a41a8a0 100755 --- a/tests/dictserver.py +++ b/tests/dictserver.py @@ -52,34 +52,37 @@ class DictHandler(socketserver.BaseRequestHandler): """ Simple function which responds to all queries with a 552. """ + try: + # First, send a response to allow the server to continue. + rsp = "220 dictserver \n" + self.request.sendall(rsp.encode("utf-8")) - # First, send a response to allow the server to continue. - rsp = "220 dictserver \n" - self.request.sendall(rsp.encode("utf-8")) + # Receive the request. + data = self.request.recv(1024).strip() + log.debug("[DICT] Incoming data: %r", data) - # Receive the request. - data = self.request.recv(1024).strip() - log.debug("[DICT] Incoming data: %r", data) + if VERIFIED_REQ in data: + log.debug("[DICT] Received verification request from test " + "framework") + response_data = VERIFIED_RSP.format(pid=os.getpid()) + else: + log.debug("[DICT] Received normal request") + response_data = "No matches" - if VERIFIED_REQ in data: - log.debug("[DICT] Received verification request from test " - "framework") - response_data = VERIFIED_RSP.format(pid=os.getpid()) - else: - log.debug("[DICT] Received normal request") - response_data = "No matches" + # Send back a failure to find. + response = "552 {0}\n".format(response_data) + log.debug("[DICT] Responding with %r", response) + self.request.sendall(response.encode("utf-8")) - # Send back a failure to find. - response = "552 {0}\n".format(response_data) - log.debug("[DICT] Responding with %r", response) - self.request.sendall(response.encode("utf-8")) + except IOError: + log.exception("[DICT] IOError hit during request") def get_options(): parser = argparse.ArgumentParser() parser.add_argument("--port", action="store", default=9016, - type=int, help="port to listen on") + type=int, help="port to listen on") parser.add_argument("--verbose", action="store", type=int, default=0, help="verbose output") parser.add_argument("--pidfile", action="store", -- 2.9.4 From e364320be1dc3185ea3a2f1e979b6acfe324ed0f Mon Sep 17 00:00:00 2001 From: Max Dymond Date: Wed, 5 Jul 2017 10:12:10 +0100 Subject: [PATCH 3/3] test1452: add telnet negotiation Add a basic telnet server for negotiating some telnet options before echoing back any data that's sent to it. Closes #1645 Upstream-commit: 06a6feba9de67733ef943ed9f64076923dc5f747 Signed-off-by: Kamil Dudka --- tests/negtelnetserver.py | 349 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 349 insertions(+) create mode 100755 tests/negtelnetserver.py diff --git a/tests/negtelnetserver.py b/tests/negtelnetserver.py new file mode 100755 index 0000000..8cfd409 --- /dev/null +++ b/tests/negtelnetserver.py @@ -0,0 +1,349 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +# +""" A telnet server which negotiates""" + +from __future__ import (absolute_import, division, print_function, + unicode_literals) +import argparse +import os +import sys +import logging +import struct +try: # Python 2 + import SocketServer as socketserver +except ImportError: # Python 3 + import socketserver + + +log = logging.getLogger(__name__) +HOST = "localhost" +IDENT = "NTEL" + + +# The strings that indicate the test framework is checking our aliveness +VERIFIED_REQ = b"verifiedserver" +VERIFIED_RSP = b"WE ROOLZ: {pid}" + + +def telnetserver(options): + """ + Starts up a TCP server with a telnet handler and serves DICT requests + forever. + """ + if options.pidfile: + pid = os.getpid() + with open(options.pidfile, "w") as f: + f.write(b"{0}".format(pid)) + + local_bind = (HOST, options.port) + log.info("Listening on %s", local_bind) + + # Need to set the allow_reuse on the class, not on the instance. + socketserver.TCPServer.allow_reuse_address = True + server = socketserver.TCPServer(local_bind, NegotiatingTelnetHandler) + server.serve_forever() + + return ScriptRC.SUCCESS + + +class NegotiatingTelnetHandler(socketserver.BaseRequestHandler): + """Handler class for Telnet connections. + + """ + def handle(self): + """ + Negotiates options before reading data. + """ + neg = Negotiator(self.request) + + try: + # Send some initial negotiations. + neg.send_do("NEW_ENVIRON") + neg.send_will("NEW_ENVIRON") + neg.send_dont("NAWS") + neg.send_wont("NAWS") + + # Get the data passed through the negotiator + data = neg.recv(1024) + log.debug("Incoming data: %r", data) + + if VERIFIED_REQ in data: + log.debug("Received verification request from test framework") + response_data = VERIFIED_RSP.format(pid=os.getpid()) + else: + log.debug("Received normal request - echoing back") + response_data = data.strip() + + if response_data: + log.debug("Sending %r", response_data) + self.request.sendall(response_data) + + except IOError: + log.exception("IOError hit during request") + + +class Negotiator(object): + NO_NEG = 0 + START_NEG = 1 + WILL = 2 + WONT = 3 + DO = 4 + DONT = 5 + + def __init__(self, tcp): + self.tcp = tcp + self.state = self.NO_NEG + + def recv(self, bytes): + """ + Read bytes from TCP, handling negotiation sequences + + :param bytes: Number of bytes to read + :return: a buffer of bytes + """ + buffer = bytearray() + + # If we keep receiving negotiation sequences, we won't fill the buffer. + # Keep looping while we can, and until we have something to give back + # to the caller. + while len(buffer) == 0: + data = self.tcp.recv(bytes) + if not data: + # TCP failed to give us any data. Break out. + break + + for byte in data: + byte_int = self.byte_to_int(byte) + + if self.state == self.NO_NEG: + self.no_neg(byte, byte_int, buffer) + elif self.state == self.START_NEG: + self.start_neg(byte_int) + elif self.state in [self.WILL, self.WONT, self.DO, self.DONT]: + self.handle_option(byte_int) + else: + # Received an unexpected byte. Stop negotiations + log.error("Unexpected byte %s in state %s", + byte_int, + self.state) + self.state = self.NO_NEG + + return buffer + + def byte_to_int(self, byte): + return struct.unpack(b'B', byte)[0] + + def no_neg(self, byte, byte_int, buffer): + # Not negotiating anything thus far. Check to see if we + # should. + if byte_int == NegTokens.IAC: + # Start negotiation + log.debug("Starting negotiation (IAC)") + self.state = self.START_NEG + else: + # Just append the incoming byte to the buffer + buffer.append(byte) + + def start_neg(self, byte_int): + # In a negotiation. + log.debug("In negotiation (%s)", + NegTokens.from_val(byte_int)) + + if byte_int == NegTokens.WILL: + # Client is confirming they are willing to do an option + log.debug("Client is willing") + self.state = self.WILL + elif byte_int == NegTokens.WONT: + # Client is confirming they are unwilling to do an + # option + log.debug("Client is unwilling") + self.state = self.WONT + elif byte_int == NegTokens.DO: + # Client is indicating they can do an option + log.debug("Client can do") + self.state = self.DO + elif byte_int == NegTokens.DONT: + # Client is indicating they can't do an option + log.debug("Client can't do") + self.state = self.DONT + else: + # Received an unexpected byte. Stop negotiations + log.error("Unexpected byte %s in state %s", + byte_int, + self.state) + self.state = self.NO_NEG + + def handle_option(self, byte_int): + if byte_int in [NegOptions.BINARY, + NegOptions.CHARSET, + NegOptions.SUPPRESS_GO_AHEAD, + NegOptions.NAWS, + NegOptions.NEW_ENVIRON]: + log.debug("Option: %s", NegOptions.from_val(byte_int)) + + # No further negotiation of this option needed. Reset the state. + self.state = self.NO_NEG + + else: + # Received an unexpected byte. Stop negotiations + log.error("Unexpected byte %s in state %s", + byte_int, + self.state) + self.state = self.NO_NEG + + def send_message(self, message): + packed_message = self.pack(message) + self.tcp.sendall(packed_message) + + def pack(self, arr): + return struct.pack(b'{0}B'.format(len(arr)), *arr) + + def send_iac(self, arr): + message = [NegTokens.IAC] + message.extend(arr) + self.send_message(message) + + def send_do(self, option_str): + log.debug("Sending DO %s", option_str) + self.send_iac([NegTokens.DO, NegOptions.to_val(option_str)]) + + def send_dont(self, option_str): + log.debug("Sending DONT %s", option_str) + self.send_iac([NegTokens.DONT, NegOptions.to_val(option_str)]) + + def send_will(self, option_str): + log.debug("Sending WILL %s", option_str) + self.send_iac([NegTokens.WILL, NegOptions.to_val(option_str)]) + + def send_wont(self, option_str): + log.debug("Sending WONT %s", option_str) + self.send_iac([NegTokens.WONT, NegOptions.to_val(option_str)]) + + +class NegBase(object): + @classmethod + def to_val(cls, name): + return getattr(cls, name) + + @classmethod + def from_val(cls, val): + for k in cls.__dict__.keys(): + if getattr(cls, k) == val: + return k + + return "" + + +class NegTokens(NegBase): + # The start of a negotiation sequence + IAC = 255 + # Confirm willingness to negotiate + WILL = 251 + # Confirm unwillingness to negotiate + WONT = 252 + # Indicate willingness to negotiate + DO = 253 + # Indicate unwillingness to negotiate + DONT = 254 + + # The start of sub-negotiation options. + SB = 250 + # The end of sub-negotiation options. + SE = 240 + + +class NegOptions(NegBase): + # Binary Transmission + BINARY = 0 + # Suppress Go Ahead + SUPPRESS_GO_AHEAD = 3 + # NAWS - width and height of client + NAWS = 31 + # NEW-ENVIRON - environment variables on client + NEW_ENVIRON = 39 + # Charset option + CHARSET = 42 + + +def get_options(): + parser = argparse.ArgumentParser() + + parser.add_argument("--port", action="store", default=9019, + type=int, help="port to listen on") + parser.add_argument("--verbose", action="store", type=int, default=0, + help="verbose output") + parser.add_argument("--pidfile", action="store", + help="file name for the PID") + parser.add_argument("--logfile", action="store", + help="file name for the log") + parser.add_argument("--srcdir", action="store", help="test directory") + parser.add_argument("--id", action="store", help="server ID") + parser.add_argument("--ipv4", action="store_true", default=0, + help="IPv4 flag") + + return parser.parse_args() + + +def setup_logging(options): + """ + Set up logging from the command line options + """ + root_logger = logging.getLogger() + add_stdout = False + + formatter = logging.Formatter("%(asctime)s %(levelname)-5.5s " + "[{ident}] %(message)s" + .format(ident=IDENT)) + + # Write out to a logfile + if options.logfile: + handler = logging.FileHandler(options.logfile, mode="w") + handler.setFormatter(formatter) + handler.setLevel(logging.DEBUG) + root_logger.addHandler(handler) + else: + # The logfile wasn't specified. Add a stdout logger. + add_stdout = True + + if options.verbose: + # Add a stdout logger as well in verbose mode + root_logger.setLevel(logging.DEBUG) + add_stdout = True + else: + root_logger.setLevel(logging.INFO) + + if add_stdout: + stdout_handler = logging.StreamHandler(sys.stdout) + stdout_handler.setFormatter(formatter) + stdout_handler.setLevel(logging.DEBUG) + root_logger.addHandler(stdout_handler) + + +class ScriptRC(object): + """Enum for script return codes""" + SUCCESS = 0 + FAILURE = 1 + EXCEPTION = 2 + + +class ScriptException(Exception): + pass + + +if __name__ == '__main__': + # Get the options from the user. + options = get_options() + + # Setup logging using the user options + setup_logging(options) + + # Run main script. + try: + rc = telnetserver(options) + except Exception as e: + log.exception(e) + rc = ScriptRC.EXCEPTION + + log.info("Returning %d", rc) + sys.exit(rc) -- 2.9.4