def handle_udp(self, conn, name, enip_process, **kwds): """ Process UDP packets from multiple clients """ with parser.enip_machine(name=name, context='enip') as machine: while not kwds['server']['control']['done'] and not kwds['server']['control']['disable']: try: source = cpppo.rememberable() data = cpppo.dotdict() # If no/partial EtherNet/IP header received, parsing will fail with a NonTerminal # Exception (dfa exits in non-terminal state). Build data.request.enip: begun = cpppo.timer() # waiting for next transaction addr, stats = None, None with contextlib.closing(machine.run( path='request', source=source, data=data)) as engine: # PyPy compatibility; avoid deferred destruction of generators for mch, sta in engine: if sta is not None: # No more transitions available. Wait for input. continue assert not addr, "Incomplete UDP request from client %r" % (addr) msg = None while msg is None: # For UDP, we'll allow no input only at the start of a new request parse # (addr is None); anything else will be considered a failed request Back # to the trough for more symbols, after having already received a packet # from a peer? No go! wait = (kwds['server']['control']['latency'] if source.peek() is None else 0) brx = cpppo.timer() msg, frm = network.recvfrom(conn, timeout=wait) now = cpppo.timer() if not msg: if kwds['server']['control']['done'] or kwds['server']['control']['disable']: return (logger.info if msg else logger.debug)( "Transaction receive after %7.3fs (%5s bytes in %7.3f/%7.3fs): %r", now - begun, len(msg) if msg is not None else "None", now - brx, wait, self.stats_for(frm)[0]) # If we're at a None (can't proceed), and we haven't yet received input, # then this is where we implement "Blocking"; we just loop for input. # We have received exactly one packet from an identified peer! begun = now addr = frm stats, _ = self.stats_for(addr) # For UDP, we don't ever receive incoming EOF, or set stats['eof']. # However, we can respond to a manual eof (eg. from web interface) by # ignoring the peer's packets. assert stats and not stats.get('eof'), \ "Ignoring UDP request from client %r: %r" % (addr, msg) stats['received'] += len(msg) logger.debug("%s recv: %5d: %s", machine.name_centered(), len(msg), cpppo.reprlib.repr(msg)) source.chain(msg) # Terminal state and EtherNet/IP header recognized; process and return response assert stats if 'request' in data: stats['requests'] += 1 # enip_process must be able to handle no request (empty data), indicating the # clean termination of the session if closed from this end (not required if # enip_process returned False, indicating the connection was terminated by # request.) if enip_process(addr, data=data, **kwds): # Produce an EtherNet/IP response carrying the encapsulated response data. # If no encapsulated data, ensure we also return a non-zero EtherNet/IP # status. A non-zero status indicates the end of the session. assert 'response.enip' in data, "Expected EtherNet/IP response; none found" if 'input' not in data.response.enip or not data.response.enip.input: logger.warning("Expected EtherNet/IP response encapsulated message; none found") assert data.response.enip.status, "If no/empty response payload, expected non-zero EtherNet/IP status" rpy = parser.enip_encode(data.response.enip) logger.debug("%s send: %5d: %s", machine.name_centered(), len(rpy), cpppo.reprlib.repr(rpy)) conn.sendto(rpy, addr) logger.debug("Transaction complete after %7.3fs", cpppo.timer() - begun) stats['processed'] = source.sent except: # Parsing failure. Suck out some remaining input to give us some context, but don't re-raise if stats: stats['processed'] = source.sent memory = bytes(bytearray(source.memory)) pos = len(source.memory) future = bytes(bytearray(b for b in source)) where = "at %d total bytes:\n%s\n%s (byte %d)" % ( stats.get('processed', 0) if stats else 0, repr(memory + future), '-' * (len(repr(memory)) - 1) + '^', pos) logger.error("Client %r EtherNet/IP error %s\n\nFailed with exception:\n%s\n", addr, where, ''.join( traceback.format_exception(*sys.exc_info())))
def handle_udp(self, conn, name, enip_process, **kwds): """ Process UDP packets from multiple clients """ with parser.enip_machine(name=name, context='enip') as machine: while not kwds['server']['control']['done'] and not kwds['server']['control']['disable']: try: source = cpppo.rememberable() data = cpppo.dotdict() # If no/partial EtherNet/IP header received, parsing will fail with a NonTerminal # Exception (dfa exits in non-terminal state). Build data.request.enip: begun = cpppo.timer() # waiting for next transaction addr, stats = None, None with contextlib.closing(machine.run( path='request', source=source, data=data)) as engine: # PyPy compatibility; avoid deferred destruction of generators for mch, sta in engine: if sta is not None: # No more transitions available. Wait for input. continue assert not addr, "Incomplete UDP request from client %r" % (addr) msg = None while msg is None: # For UDP, we'll allow no input only at the start of a new request parse # (addr is None); anything else will be considered a failed request Back # to the trough for more symbols, after having already received a packet # from a peer? No go! wait = (kwds['server']['control']['latency'] if source.peek() is None else 0) brx = cpppo.timer() msg, frm = network.recvfrom(conn, timeout=wait) now = cpppo.timer() (logger.info if msg else logger.debug)( "Transaction receive after %7.3fs (%5s bytes in %7.3f/%7.3fs): %r", now - begun, len(msg) if msg is not None else "None", now - brx, wait, self.stats_for(frm)[0]) # If we're at a None (can't proceed), and we haven't yet received input, # then this is where we implement "Blocking"; we just loop for input. # We have received exactly one packet from an identified peer! begun = now addr = frm stats, _ = self.stats_for(addr) # For UDP, we don't ever receive incoming EOF, or set stats['eof']. # However, we can respond to a manual eof (eg. from web interface) by # ignoring the peer's packets. assert stats and not stats.get('eof'), \ "Ignoring UDP request from client %r: %r" % (addr, msg) stats['received'] += len(msg) logger.debug("%s recv: %5d: %s", machine.name_centered(), len(msg), cpppo.reprlib.repr(msg)) source.chain(msg) # Terminal state and EtherNet/IP header recognized; process and return response assert stats if 'request' in data: stats['requests'] += 1 # enip_process must be able to handle no request (empty data), indicating the # clean termination of the session if closed from this end (not required if # enip_process returned False, indicating the connection was terminated by # request.) if enip_process(addr, data=data, **kwds): # Produce an EtherNet/IP response carrying the encapsulated response data. # If no encapsulated data, ensure we also return a non-zero EtherNet/IP # status. A non-zero status indicates the end of the session. assert 'response.enip' in data, "Expected EtherNet/IP response; none found" if 'input' not in data.response.enip or not data.response.enip.input: logger.warning("Expected EtherNet/IP response encapsulated message; none found") assert data.response.enip.status, "If no/empty response payload, expected non-zero EtherNet/IP status" rpy = parser.enip_encode(data.response.enip) logger.debug("%s send: %5d: %s", machine.name_centered(), len(rpy), cpppo.reprlib.repr(rpy)) conn.sendto(rpy, addr) logger.debug("Transaction complete after %7.3fs", cpppo.timer() - begun) stats['processed'] = source.sent except: # Parsing failure. Suck out some remaining input to give us some context, but don't re-raise if stats: stats['processed'] = source.sent memory = bytes(bytearray(source.memory)) pos = len(source.memory) future = bytes(bytearray(b for b in source)) where = "at %d total bytes:\n%s\n%s (byte %d)" % ( stats.get('processed', 0) if stats else 0, repr(memory + future), '-' * (len(repr(memory)) - 1) + '^', pos) logger.error("Client %r EtherNet/IP error %s\n\nFailed with exception:\n%s\n", addr, where, ''.join( traceback.format_exception(*sys.exc_info())))
def handle_tcp(self, conn, address, name, enip_process, delay=None, **kwds): """ Handle a TCP client """ source = cpppo.rememberable() with parser.enip_machine(name=name, context='enip') as machine: try: assert address, "EtherNet/IP CIP server for TCP/IP must be provided a peer address" stats, connkey = self.stats_for(address) while not stats.eof: data = cpppo.dotdict() source.forget() # If no/partial EtherNet/IP header received, parsing will fail with a NonTerminal # Exception (dfa exits in non-terminal state). Build data.request.enip: begun = cpppo.timer() with contextlib.closing(machine.run(path='request', source=source, data=data)) as engine: # PyPy compatibility; avoid deferred destruction of generators for mch, sta in engine: if sta is not None: continue # No more transitions available. Wait for input. EOF (b'') will lead to # termination. We will simulate non-blocking by looping on None (so we can # check our options, in case they've been changed). If we still have input # available to process right now in 'source', we'll just check (0 timeout); # otherwise, use the specified server.control.latency. msg = None while msg is None and not stats.eof: wait = (kwds['server']['control']['latency'] if source.peek() is None else 0) brx = cpppo.timer() msg = network.recv(conn, timeout=wait) now = cpppo.timer() (logger.info if msg else logger.debug)( "Transaction receive after %7.3fs (%5s bytes in %7.3f/%7.3fs)", now - begun, len(msg) if msg is not None else "None", now - brx, wait) # After each block of input (or None), check if the server is being # signalled done/disabled; we need to shut down so signal eof. Assumes # that (shared) server.control.{done,disable} dotdict be in kwds. We do # *not* read using attributes here, to avoid reporting completion to # external APIs (eg. web) awaiting reception of these signals. if kwds['server']['control']['done'] or kwds['server']['control']['disable']: logger.info("%s done, due to server done/disable", machine.name_centered()) stats['eof'] = True if msg is not None: stats['received'] += len(msg) stats['eof'] = stats['eof'] or not len(msg) if logger.getEffectiveLevel() <= logging.INFO: logger.info("%s recv: %5d: %s", machine.name_centered(), len(msg), cpppo.reprlib.repr(msg)) source.chain(msg) else: # No input. If we have symbols available, no problem; continue. # This can occur if the state machine cannot make a transition on # the input symbol, indicating an unacceptable sentence for the # grammar. If it cannot make progress, the machine will terminate # in a non-terminal state, rejecting the sentence. if source.peek() is not None: break # We're at a None (can't proceed), and no input is available. This # is where we implement "Blocking"; just loop. logger.info("Transaction parsed after %7.3fs", cpppo.timer() - begun) # Terminal state and EtherNet/IP header recognized, or clean EOF (no partial # message); process and return response if 'request' in data: stats['requests'] += 1 try: # enip_process must be able to handle no request (empty data), indicating the # clean termination of the session if closed from this end (not required if # enip_process returned False, indicating the connection was terminated by # request.) delayseconds = 0 # response delay (if any) if enip_process(address, data=data, **kwds): # Produce an EtherNet/IP response carrying the encapsulated response data. # If no encapsulated data, ensure we also return a non-zero EtherNet/IP # status. A non-zero status indicates the end of the session. assert 'response.enip' in data, "Expected EtherNet/IP response; none found" if 'input' not in data.response.enip or not data.response.enip.input: logger.warning("Expected EtherNet/IP response encapsulated message; none found") assert data.response.enip.status, "If no/empty response payload, expected non-zero EtherNet/IP status" rpy = parser.enip_encode(data.response.enip) if logger.getEffectiveLevel() <= logging.INFO: logger.info("%s send: %5d: %s %s", machine.name_centered(), len(rpy), cpppo.reprlib.repr(rpy), ("delay: %r" % delay) if delay else "") if delay: # A delay (anything with a delay.value attribute) == #[.#] (converible # to float) is ok; may be changed via web interface. try: delayseconds = float( delay.value if hasattr(delay, 'value') else delay) if delayseconds > 0: time.sleep(delayseconds) except Exception as exc: logger.info( "Unable to delay; invalid seconds: %r", delay) try: conn.send(rpy) except socket.error as exc: logger.info("Session ended (client abandoned): %s", exc) stats['eof'] = True if data.response.enip.status: logger.warning( "Session ended (server EtherNet/IP status: 0x%02x == %d)", data.response.enip.status, data.response.enip.status) stats['eof'] = True else: # Session terminated. No response, just drop connection. if logger.getEffectiveLevel() <= logging.INFO: logger.info("Session ended (client initiated): %s", parser.enip_format(data)) stats['eof'] = True logger.info( "Transaction complete after %7.3fs (w/ %7.3fs delay)", cpppo.timer() - begun, delayseconds) except: logger.error("Failed request: %s", parser.enip_format(data)) enip_process(address, data=cpppo.dotdict()) # Terminate. raise stats['processed'] = source.sent except: # Parsing failure. stats['processed'] = source.sent memory = bytes(bytearray(source.memory)) pos = len(source.memory) future = bytes(bytearray(b for b in source)) where = "at %d total bytes:\n%s\n%s (byte %d)" % (stats.processed, repr(memory + future), '-' * (len(repr(memory)) - 1) + '^', pos) logger.error("EtherNet/IP error %s\n\nFailed with exception:\n%s\n", where, ''.join(traceback.format_exception(*sys.exc_info()))) raise finally: # Not strictly necessary to close (network.server_main will discard the socket, # implicitly closing it), but we'll do it explicitly here in case the thread doesn't die # for some other reason. Clean up the connections entry for this connection address. self.connections.pop(connkey, None) logger.info( "%s done; processed %3d request%s over %5d byte%s/%5d received (%d connections remain)", name, stats.requests, " " if stats.requests == 1 else "s", stats.processed, " " if stats.processed == 1 else "s", stats.received, len(self.connections)) sys.stdout.flush() conn.close()