Пример #1
0
def echo_server(conn, addr):
    """Serve one echo client 'til EOF; then close the socket"""
    source = cpppo.chainable()
    with echo_machine("echo_%s" % addr[1]) as echo_line:
        eof = False
        while not eof:
            data = cpppo.dotdict()
            # See if a line has been recognized, stopping at terminal state.  If this machine
            # is ended early due to an EOF, it should still terminate in a terminal state
            for mch, sta in echo_line.run(source=source, data=data):
                if sta is not None:
                    continue
                # Non-transition; check for input, blocking if non-terminal and none left.  On
                # EOF, terminate early; this will raise a GeneratorExit.
                timeout = 0 if echo_line.terminal or source.peek(
                ) is not None else None
                msg = network.recv(conn, timeout=timeout)
                if msg is not None:
                    eof = not len(msg)
                    log.info("%s recv: %5d: %s", echo_line.name_centered(),
                             len(msg),
                             "EOF" if eof else cpppo.reprlib.repr(msg))
                    source.chain(msg)
                    if eof:
                        break
            # Terminal state (or EOF).
            log.detail("%s: byte %5d: data: %r", echo_line.name_centered(),
                       source.sent, data)
            if echo_line.terminal:
                conn.send(data.echo)

        log.info("%s done", echo_line.name_centered())
Пример #2
0
def tnet_server( conn, addr ):
    """Serve one tnet client 'til EOF; then close the socket"""
    source			= cpppo.chainable()
    with tnet_machine( "tnet_%s" % addr[1] ) as tnet_mesg:
        eof			= False
        while not eof:
            data		= cpppo.dotdict()
            # Loop blocking for input, while we've consumed input from source since the last time.
            # If we hit this again without having used any input, we know we've hit a symbol
            # unacceptable to the state machine; stop
            for mch, sta in tnet_mesg.run( source=source, data=data ):
                if sta is not None:
                    continue
                # Non-transition; check for input, blocking if non-terminal and none left.  On
                # EOF, terminate early; this will raise a GeneratorExit.
                timeout		= 0 if tnet_mesg.terminal or source.peek() is not None else None
                msg		= network.recv( conn, timeout=timeout ) # blocking
                if msg is not None:
                    eof		= not len( msg )
                    log.info( "%s: recv: %5d: %s", tnet_mesg.name_centered(), len( msg ),
                              "EOF" if eof else reprlib.repr( msg )) 
                    source.chain( msg )
                    if eof:
                        break

            # Terminal state (or EOF).
            log.detail( "%s: byte %5d: data: %r", tnet_mesg.name_centered(), source.sent, data )
            if tnet_mesg.terminal:
                res			= json.dumps( data.tnet.type.input, indent=4, sort_keys=True )
                conn.send(( res + "\n\n" ).encode( "utf-8" ))
    
        log.info( "%s done", tnet_mesg.name_centered() )
Пример #3
0
def echo_server( conn, addr ):
    """Serve one echo client 'til EOF; then close the socket"""
    source			= cpppo.chainable()
    with echo_machine( "echo_%s" % addr[1] ) as echo_line:
        eof			= False
        while not eof:
            data		= cpppo.dotdict()
            # See if a line has been recognized, stopping at terminal state.  If this machine
            # is ended early due to an EOF, it should still terminate in a terminal state
            for mch, sta in echo_line.run( source=source, data=data ):
                if sta is not None:
                    continue
                # Non-transition; check for input, blocking if non-terminal and none left.  On
                # EOF, terminate early; this will raise a GeneratorExit.
                timeout		= 0 if echo_line.terminal or source.peek() is not None else None
                msg		= network.recv( conn, timeout=timeout )
                if msg is not None:
                    eof		= not len( msg )
                    log.info( "%s recv: %5d: %s", echo_line.name_centered(), len( msg ),
                              "EOF" if eof else cpppo.reprlib.repr( msg ))
                    source.chain( msg )
                    if eof:
                        break
            # Terminal state (or EOF).
            log.detail( "%s: byte %5d: data: %r", echo_line.name_centered(), source.sent, data )
            if echo_line.terminal:
                conn.send( data.echo )
        
        log.info( "%s done", echo_line.name_centered() )
Пример #4
0
 def recv( maxlen ):
     duration	= cpppo.timer() - started
     remains	= None if timeout is None else max( 0, timeout - duration )
     remains	= latency if timeout is None else min(	# If no timeout, wait for latency (or forever, if None)
         timeout if latency is None else latency,	# Or, we know timeout is numeric; get min of any latency
         max( timeout - duration, 0 ))			#  ... and remaining unused timeout
     return network.recv( conn, maxlen=maxlen, timeout=remains )
Пример #5
0
    def handle( self ):
        '''Callback when we receive any data, until self.running becomes not True.  Blocks indefinitely
        awaiting data.  If shutdown is required, then the global socket.settimeout(<seconds>) may be
        used, to allow timely checking of self.running.  However, since this also affects socket
        connects, if there are outgoing socket connections used in the same program, then these will
        be prevented, if the specfied timeout is too short.  Hence, this is unreliable.

        Specify a latency of None for no recv timeout, and a drain of 0 for no waiting for reply
        EOF, for same behavior as stock ModbusConnectedRequestHandler.

        NOTE: This loop is restructured to employ finally: for logging, but is functionally
        equivalent to the original.

        '''
        log.info("Modbus/TCP client socket handling started for %s", self.client_address )
        try:
            while self.running:
                data		= network.recv( self.request, timeout=self.latency )
                if data is None:
                    continue			# No data w'in timeout; just check self.running
                if not data:
                    self.running= False	# EOF (empty data); done
                if log.isEnabledFor(logging.DEBUG):
                    log.debug(" ".join([hex(ord(x)) for x in data]))
                self.framer.processIncomingPacket( data, self.execute )
        except socket.error as exc:
            log.error("Modbus/TCP client socket error occurred %s", exc )
            self.running	= False
        except:
            log.error("Modbus/TCP client socket exception occurred %s", traceback.format_exc() )
            self.running	= False
        finally:
            log.info("Modbus/TCP client socket handling stopped for %s", self.client_address )
Пример #6
0
def tnet_server( conn, addr ):
    """Serve one tnet client 'til EOF; then close the socket"""
    source			= cpppo.chainable()
    with tnet_machine( "tnet_%s" % addr[1] ) as tnet_mesg:
        eof			= False
        while not eof:
            data		= cpppo.dotdict()
            # Loop blocking for input, while we've consumed input from source since the last time.
            # If we hit this again without having used any input, we know we've hit a symbol
            # unacceptable to the state machine; stop
            for mch, sta in tnet_mesg.run( source=source, data=data ):
                if sta is not None:
                    continue
                # Non-transition; check for input, blocking if non-terminal and none left.  On
                # EOF, terminate early; this will raise a GeneratorExit.
                timeout		= 0 if tnet_mesg.terminal or source.peek() is not None else None
                msg		= network.recv( conn, timeout=timeout ) # blocking
                if msg is not None:
                    eof		= not len( msg )
                    log.info( "%s: recv: %5d: %s", tnet_mesg.name_centered(), len( msg ),
                              "EOF" if eof else cpppo.reprlib.repr( msg )) 
                    source.chain( msg )
                    if eof:
                        break

            # Terminal state (or EOF).
            log.detail( "%s: byte %5d: data: %r", tnet_mesg.name_centered(), source.sent, data )
            if tnet_mesg.terminal:
                res			= json.dumps( data.tnet.type.input, indent=4, sort_keys=True )
                conn.send(( res + "\n\n" ).encode( "utf-8" ))
    
        log.info( "%s done", tnet_mesg.name_centered() )
Пример #7
0
def echo_cli( number, reps ):
    log.normal( "Echo Client %3d connecting... PID [%5d]", number, os.getpid() )
    conn			= socket.socket( socket.AF_INET, socket.SOCK_STREAM )
    conn.connect( echo.address )
    log.detail( "Echo Client %3d connected", number )
        
    sent			= b''
    rcvd			= b''
    try:
        # Send messages and collect replies 'til done (or incoming EOF).  Then, shut down 
        # outgoing half of socket to drain server and shut down server.
        eof			= False
        for r in range( reps ):
            msg			= ("Client %3d, rep %d\r\n" % ( number, r )).encode()
            log.detail("Echo Client %3d send: %5d: %s", number, len( msg ), cpppo.reprlib.repr( msg ))
            sent	       += msg

            while len( msg ) and not eof:
                out		= min( len( msg ), random.randrange( *charrange ))
                conn.send( msg[:out] )
                msg		= msg[out:]

                # Await inter-block chardelay if output remains, otherwise await final response
                # before dropping out to shutdown/drain/close.  If we drop out immediately and send
                # a socket.shutdown, it'll sometimes deliver a reset to the server end of the
                # socket, before delivering the last of the data.
                rpy		= network.recv( conn, timeout=chardelay if len( msg ) else draindelay )
                if rpy is not None:
                    eof		= not len( rpy )
                    log.detail( "Echo Client %3d recv: %5d: %s", number, len( rpy ),
                              "EOF" if eof else cpppo.reprlib.repr( rpy ))
                    rcvd       += rpy
            if eof:
                break

        log.normal( "Echo Client %3d done; %s", number, "due to EOF" if eof else "normal termination" )

    except KeyboardInterrupt as exc:
        log.warning( "Echo Client %3d terminated: %r", number, exc )
    except Exception as exc:
        log.warning( "Echo Client %3d failed: %r\n%s", number, exc, traceback.format_exc() )
    finally:
        # One or more packets may be in flight; wait 'til we timeout/EOF.  This shuts down conn.
        rpy			= network.drain( conn, timeout=draindelay )
        log.info( "Echo Client %3d drain %5d: %s", number, len( rpy ) if rpy is not None else 0,
                  cpppo.reprlib.repr( rpy ))
        if rpy is not None:
            rcvd   	       += rpy

    # Count the number of success/failures reported by the Echo client threads
    failed			= not ( rcvd == sent )
    if failed:
        log.warning( "Echo Client %3d failed: %s != %s sent", number, cpppo.reprlib.repr( rcvd ),
                     cpppo.reprlib.repr( sent ))
    
    log.info( "Echo Client %3d exited", number )
    return failed
Пример #8
0
    def handle(self):
        '''Callback when we receive any data, until self.running becomes not True.  Blocks indefinitely
        awaiting data.  If shutdown is required, then the global socket.settimeout(<seconds>) may be
        used, to allow timely checking of self.running.  However, since this also affects socket
        connects, if there are outgoing socket connections used in the same program, then these will
        be prevented, if the specfied timeout is too short.  Hence, this is unreliable.

        Specify a latency of None for no recv timeout, and a drain of 0 for no waiting for reply
        EOF, for same behavior as stock ModbusConnectedRequestHandler.

        NOTE: This loop is restructured to employ finally: for logging, but is functionally
        equivalent to the original.

        '''
        log.info("Modbus/TCP client socket handling started for %s",
                 self.client_address)
        try:
            while self.running:
                data = network.recv(self.request, timeout=self.latency)
                if data is None:
                    continue  # No data w'in timeout; just check self.running
                if not data:
                    self.running = False  # EOF (empty data); done
                if log.isEnabledFor(logging.DEBUG):
                    log.debug(" ".join([hex(ord(x)) for x in data]))
                self.framer.processIncomingPacket(data, self.execute)
        except socket.error as exc:
            log.error("Modbus/TCP client socket error occurred %s", exc)
            self.running = False
        except:
            log.error("Modbus/TCP client socket exception occurred %s",
                      traceback.format_exc())
            self.running = False
        finally:
            log.info("Modbus/TCP client socket handling stopped for %s",
                     self.client_address)
Пример #9
0
    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()
Пример #10
0
    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()
Пример #11
0
def enip_srv(conn, addr, enip_process=None, delay=None, **kwds):
    """Serve one Ethernet/IP client 'til EOF; then close the socket.  Parses headers and encapsulated
    EtherNet/IP request data 'til either the parser fails (the Client has submitted an un-parsable
    request), or the request handler fails.  Otherwise, encodes the data.response in an EtherNet/IP
    packet and sends it back to the client.

    Use the supplied enip_process function to process each parsed EtherNet/IP frame, returning True
    if a data.response is formulated, False if the session has ended cleanly, or raise an Exception
    if there is a processing failure (eg. an unparsable request, indicating that the Client is
    speaking an unknown dialect and the session must close catastrophically.)

    If a partial EtherNet/IP header is parsed and an EOF is received, the enip_header parser will
    raise an AssertionError, and we'll simply drop the connection.  If we receive a valid header and
    request, the supplied enip_process function is expected to formulate an appropriate error
    response, and we'll continue processing requests.

    An option numeric delay value (or any delay object with a .value attribute evaluating to a
    numeric value) may be specified; every response will be delayed by the specified number of
    seconds.  We assume that such a value may be altered over time, so we access it afresh for each
    use.

    All remaining keywords are passed along to the supplied enip_process function.
    """
    global latency
    global timeout

    name = "enip_%s" % addr[1]
    log.normal("EtherNet/IP Server %s begins serving peer %s", name, addr)

    source = cpppo.rememberable()
    with parser.enip_machine(name=name, context='enip') as enip_mesg:

        # We can be provided a dotdict() to contain our stats.  If one has been passed in, then this
        # means that our stats for this connection will be available to the web API; it may set
        # stats.eof to True at any time, terminating the connection!  The web API will try to coerce
        # its input into the same type as the variable, so we'll keep it an int (type bool doesn't
        # handle coercion from strings).  We'll use an apidict, to ensure that attribute values set
        # via the web API thread (eg. stats.eof) are blocking 'til this thread wakes up and reads
        # them.  Thus, the web API will block setting .eof, and won't return to the caller until the
        # thread is actually in the process of shutting down.  Internally, we'll use __setitem__
        # indexing to change stats values, so we don't block ourself!
        stats = cpppo.apidict(timeout=timeout)
        connkey = ("%s_%d" % addr).replace('.', '_')
        connections[connkey] = stats
        try:
            assert enip_process is not None, \
                "Must specify an EtherNet/IP processing function via 'enip_process'"
            stats['requests'] = 0
            stats['received'] = 0
            stats['eof'] = False
            stats['interface'] = addr[0]
            stats['port'] = addr[1]
            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 = misc.timer()
                log.detail("Transaction begins")
                states = 0
                for mch, sta in enip_mesg.run(path='request',
                                              source=source,
                                              data=data):
                    states += 1
                    if sta is None:
                        # 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 = misc.timer()
                            msg = network.recv(conn, timeout=wait)
                            now = misc.timer()
                            log.detail(
                                "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']:
                                log.detail(
                                    "%s done, due to server done/disable",
                                    enip_mesg.name_centered())
                                stats['eof'] = True
                            if msg is not None:
                                stats['received'] += len(msg)
                                stats['eof'] = stats['eof'] or not len(msg)
                                log.detail("%s recv: %5d: %s",
                                           enip_mesg.name_centered(),
                                           len(msg) if msg is not None else 0,
                                           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.

                log.detail("Transaction parsed  after %7.3fs" %
                           (misc.timer() - begun))
                # Terminal state and EtherNet/IP header recognized, or clean EOF (no partial
                # message); process and return response
                log.info("%s req. data (%5d states): %s",
                         enip_mesg.name_centered(), states,
                         parser.enip_format(data))
                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(addr, data=data, **kwds):
                        # Produce an EtherNet/IP response carrying the encapsulated response data.
                        assert 'response' in data, "Expected EtherNet/IP response; none found"
                        assert 'enip.input' in data.response, \
                            "Expected EtherNet/IP response encapsulated message; none found"
                        rpy = parser.enip_encode(data.response.enip)
                        log.detail("%s send: %5d: %s %s",
                                   enip_mesg.name_centered(), len(rpy),
                                   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:
                                log.detail(
                                    "Unable to delay; invalid seconds: %r",
                                    delay)
                        try:
                            conn.send(rpy)
                        except socket.error as exc:
                            log.detail(
                                "%s session ended (client abandoned): %s",
                                enip_mesg.name_centered(), exc)
                            eof = True
                    else:
                        # Session terminated.  No response, just drop connection.
                        log.detail("%s session ended (client initiated): %s",
                                   enip_mesg.name_centered(),
                                   parser.enip_format(data))
                        eof = True
                    log.detail(
                        "Transaction complete after %7.3fs (w/ %7.3fs delay)" %
                        (misc.timer() - begun, delayseconds))
                except:
                    log.error("Failed request: %s", parser.enip_format(data))
                    enip_process(addr, data=cpppo.dotdict())  # Terminate.
                    raise

            stats['processed'] = source.sent
        except:
            # Parsing failure.  We're done.  Suck out some remaining input to give us some context.
            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)
            log.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.
            connections.pop(connkey, None)
            log.normal(
                "%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(connections))
            sys.stdout.flush()
            conn.close()
Пример #12
0
def tnet_from( conn, addr,
               server	= cpppo.dotdict({'done': False}),
               timeout	= None,
               latency	= None,
               ignore	= None,
               source	= None ):	# Provide a cpppo.chainable, if desire, to receive into and parse from
    """Parse and yield TNET messages from a socket w/in timeout, blocking 'til server.done or EOF
    between messages.  If ignore contains symbols, they are ignored between TNET messages (eg. b'\n').

    An absense of a TNET string within 'timeout' will yield None, allowing the user to decide to
    fail or continue trying for another 'timeout' period.  A 0 timeout will "poll", and a None
    timeout will simply wait forever (the default).  If desired, a separate 'latency' can be
    supplied, in order to pop out regularly and check server.done (eg. to allow a server Thread to
    exit cleanly).

    """
    if source is None:
        source			= cpppo.chainable()
    with tnet_machine( "tnet_%s" % addr[1] ) as engine:
        eof			= False
        while not ( eof or server.done ):
            while ignore and source.peek() and source.peek() in ignore:
                next( source )
            data		= cpppo.dotdict()
            started		= cpppo.timer()		# When did we start the current attempt at a TNET string?
            for mch,sta in engine.run( source=source, data=data ):
                if sta is not None or source.peek() is not None:
                    continue
                # Non-transition state, and we need more data: check for more data, enforce timeout.
                # Waits up to latency, or remainder of timeout -- or forever, if both are None.
                duration	= cpppo.timer() - started
                msg		= None
                while msg is None and not server.done: # Get input, forever or 'til server.done
                    remains	= latency if timeout is None else min(	# If no timeout, wait for latency (or forever, if None)
                        timeout if latency is None else latency,	# Or, we know timeout is numeric; get min of any latency
                        max( timeout - duration, 0 ))			#  ... and remaining unused timeout
                    log.info( "%s: After %7.3fs, awaiting symbols (after %d processed) w/ %s recv timeout",
                              engine.name_centered(), duration, source.sent,
                              remains if remains is None else ( "%7.3fs" % remains ))
                    msg		= network.recv( conn, timeout=remains )
                    duration	= cpppo.timer() - started
                    if msg is None and timeout is not None and duration >= timeout:
                        # No data w/in given timeout expiry!  Inform the consumer, and then try again w/ fresh timeout.
                        log.info( "%s: After %7.3fs, no TNET message after %7.3fs recv timeout",
                              engine.name_centered(), duration, remains )
                        yield None
                        started	= cpppo.timer()
                # Only way to get here without EOF/data, is w/ server.done
                if server.done:
                    break
                assert msg is not None
                # Got EOF or data
                eof		= len( msg ) == 0
                log.info( "%s: After %7.3fs, recv: %5d: %s",
                          engine.name_centered(), duration, len( msg ),
                          'EOF' if eof else cpppo.reprlib.repr( msg ))
                if eof:
                    break
                source.chain( msg )

            # Terminal state, or EOF, or server.done.  Only yield another TNET message if terminal. 
            duration		= cpppo.timer() - started
            if engine.terminal:
                log.debug( "%s: After %7.3fs, found a TNET: %r", engine.name_centered(), duration, data.tnet.type.input )
                yield data.tnet.type.input # Could be a 0:~ / null ==> None

        log.detail( "%s: done w/ %s", engine.name_centered(),
                    ', '.join( ['EOF'] if eof else [] + ['done'] if server.done else [] ))
Пример #13
0
def tnet_cli( number, tests=None ):
    log.info( "%3d client connecting... PID [%5d]", number, os.getpid() )
    conn			= socket.socket( socket.AF_INET, socket.SOCK_STREAM )
    conn.connect( tnet.address )
    log.info( "%3d client connected", number )
        
    rcvd			= ''
    try:
        eof			= False
        for t in tests:
            msg			= tnetstrings.dump( t )
            log.normal( "Tnet Client %3d send: %5d: %s (from data: %s)", number, len( msg ),
                      cpppo.reprlib.repr( msg ), cpppo.reprlib.repr( t ))

            while len( msg ) and not eof:
                out		= min( len( msg ), random.randrange( *charrange ))
                conn.send( msg[:out] )
                msg		= msg[out:]

                # Await inter-block chardelay if output remains, otherwise await
                # final response before dropping out to shutdown/drain/close.
                # If we drop out immediately and send a socket.shutdown, it'll
                # sometimes deliver a reset to the server end of the socket,
                # before delivering the last of the data.
                rpy		= network.recv( conn, timeout=chardelay if len( msg ) else draindelay )
                if rpy is not None:
                    eof		= not len( rpy )
                    log.info( "Tnet Client %3d recv: %5d: %s", number, len( rpy ),
                              "EOF" if eof else cpppo.reprlib.repr( rpy ))
                    rcvd       += rpy.decode( "utf-8" )
            if eof:
                break

        log.normal( "Tnet Client %3d done; %s", number, "due to EOF" if eof else "normal termination" )

    except KeyboardInterrupt as exc:
        log.normal( "%3d client terminated: %r", number, exc )
    except Exception as exc:
        log.warning( "%3d client failed: %r\n%s", number, exc, traceback.format_exc() )
    finally:
        # One or more packets may be in flight; wait 'til we timeout/EOF
        rpy			= network.drain( conn, timeout=draindelay )
        log.info( "Tnet Client %3d drain %5d: %s", number, len( rpy ) if rpy is not None else 0,
                  cpppo.reprlib.repr( rpy ))
        if rpy is not None:
            rcvd   	       += rpy.decode( "utf-8" )

    # Count the number of successfully matched JSON decodes
    successes			= 0
    i 				= 0
    for i, (t, r) in enumerate( zip( tests, rcvd.split( '\n\n' ))):
        e			= json.dumps( t )
        log.info( "%3d test #%3d: %32s --> %s", number, i, cpppo.reprlib.repr( t ), cpppo.reprlib.repr( e ))
        if r == e:
            successes	       += 1
        else:
            log.warning( "%3d test #%3d: %32s got %s", number, i, cpppo.reprlib.repr( t ), cpppo.reprlib.repr( e ))
        
    failed			= successes != len( tests )
    if failed:
        log.warning( "%3d client failed: %d/%d tests succeeded", number, successes, len( tests ))
    
    log.info( "%3d client exited", number )
    return failed
Пример #14
0
    def __next__(self):
        """Return the next available response, or None if no complete response is available.  Raises
        StopIteration (cease iterating) on EOF.  Any other Exception indicates a client failure,
        and should result in the client instance being discarded.
        
        If no input is presently available, harvest any input immediately available; terminate on EOF.

        """
        if self.source.peek() is None:
            rcvd = network.recv(self.conn, timeout=0)
            log.detail("EtherNet/IP-->%16s:%-5d rcvd %5d: %s", self.addr[0],
                       self.addr[1],
                       len(rcvd) if rcvd is not None else 0, repr(rcvd))
            if rcvd is not None:
                # Some input (or EOF); source is empty; if no input available, terminate
                if not len(rcvd):
                    raise StopIteration
                self.source.chain(rcvd)
            else:
                # Don't create parsing engine 'til we have some I/O to process.  This avoids the
                # degenerate situation where empty I/O (EOF) always matches the empty command (used
                # to indicate the end of an EtherNet/IP session).
                if self.engine is None:
                    return None

        # Initiate or continue parsing input using the machine's engine; discard the engine at
        # termination or on error (Exception).  Any exception (including cpppo.NonTerminal) will be
        # propagated.
        result = None
        with self.frame as machine:
            try:
                if self.engine is None:
                    self.data = cpppo.dotdict()
                    self.engine = machine.run(source=self.source,
                                              data=self.data)
                    log.detail(
                        "EtherNet/IP   %16s:%-5d run.: %s -> %10.10s; next byte %3d: %-10.10r: %r",
                        self.addr[0], self.addr[1], machine.name_centered(),
                        machine.current, self.source.sent, self.source.peek(),
                        self.data)

                for m, s in self.engine:
                    log.detail(
                        "EtherNet/IP<--%16s:%-5d rpy.: %s -> %10.10s; next byte %3d: %-10.10r: %r",
                        self.addr[0], self.addr[1], machine.name_centered(), s,
                        self.source.sent, self.source.peek(), self.data)
            except Exception as exc:
                log.warning("EtherNet/IP<x>%16s:%-5d err.: %s", self.addr[0],
                            self.addr[1], str(exc))
                self.engine = None
                raise
            if machine.terminal:
                log.detail(
                    "EtherNet/IP   %16s:%-5d done: %s -> %10.10s; next byte %3d: %-10.10r: %r",
                    self.addr[0], self.addr[1],
                    machine.name_centered(), machine.current, self.source.sent,
                    self.source.peek(), self.data)
                # Got an EtherNet/IP frame.  Return it (after parsing its payload.)
                self.engine = None
                result = self.data

        # Parse the EtherNet/IP encapsulated CIP frame
        if result is not None:
            with self.cip as machine:
                for m, s in machine.run(path='enip',
                                        source=cpppo.peekable(
                                            result.enip.input),
                                        data=result):
                    log.detail(
                        "EtherNet/IP<--%16s:%-5d CIP : %s -> %10.10s; next byte %3d: %-10.10r: %r",
                        self.addr[0], self.addr[1], machine.name_centered(), s,
                        self.source.sent, self.source.peek(), self.data)
                    pass
                assert machine.terminal, "No CIP payload in the EtherNet/IP frame: %r" % (
                    result)

        # Parse the Logix request responses in the EtherNet/IP CIP payload's CPF items
        if result is not None and 'enip.CIP.send_data' in result:
            for item in result.enip.CIP.send_data.CPF.item:
                if 'unconnected_send.request' in item:
                    # An Unconnected Send that contained an encapsulated request (ie. not just a
                    # Get Attribute All)
                    with self.lgx as machine:
                        for m, s in machine.run(
                                source=cpppo.peekable(
                                    item.unconnected_send.request.input),
                                data=item.unconnected_send.request):
                            pass
                        assert machine.terminal, "No Logix request in the EtherNet/IP CIP CPF frame: %r" % (
                            result)

        return result
Пример #15
0
def tnet_cli(number, tests=None):
    log.info("%3d client connecting... PID [%5d]", number, os.getpid())
    conn = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    conn.connect(tnet.address)
    log.info("%3d client connected", number)

    rcvd = ''
    try:
        eof = False
        for t in tests:
            msg = tnetstrings.dump(t)

            while len(msg) and not eof:
                out = min(len(msg), random.randrange(*charrange))
                log.info("Tnet Client %3d send: %5d/%5d: %s", number, out,
                         len(msg), cpppo.reprlib.repr(msg[:out]))
                conn.sendall(msg[:out])
                msg = msg[out:]

                # Await inter-block chardelay if output remains, otherwise await
                # final response before dropping out to shutdown/drain/close.
                # If we drop out immediately and send a socket.shutdown, it'll
                # sometimes deliver a reset to the server end of the socket,
                # before delivering the last of the data.
                rpy = network.recv(
                    conn, timeout=chardelay if len(msg) else draindelay)
                if rpy is not None:
                    eof = not len(rpy)
                    log.info("Tnet Client %3d recv: %5d: %s", number, len(rpy),
                             "EOF" if eof else cpppo.reprlib.repr(rpy))
                    rcvd += rpy.decode("utf-8")
            if eof:
                break

        log.normal("Tnet Client %3d done; %s", number,
                   "due to EOF" if eof else "normal termination")

    except KeyboardInterrupt as exc:
        log.normal("%3d client terminated: %r", number, exc)
    except Exception as exc:
        log.warning("%3d client failed: %r\n%s", number, exc,
                    traceback.format_exc())
    finally:
        # One or more packets may be in flight; wait 'til we timeout/EOF
        rpy = network.drain(conn, timeout=draindelay)
        log.info("Tnet Client %3d drain %5d: %s", number,
                 len(rpy) if rpy is not None else 0, cpppo.reprlib.repr(rpy))
        if rpy is not None:
            rcvd += rpy.decode("utf-8")

    # Count the number of successfully matched JSON decodes
    successes = 0
    i = 0
    for i, (t, r) in enumerate(zip(tests, rcvd.split('\n\n'))):
        e = json.dumps(t)
        log.info("%3d test #%3d: %32s --> %s", number, i,
                 cpppo.reprlib.repr(t), cpppo.reprlib.repr(e))
        if r == e:
            successes += 1
        else:
            log.warning("%3d test #%3d: %32s got %s", number, i,
                        cpppo.reprlib.repr(t), cpppo.reprlib.repr(e))

    failed = successes != len(tests)
    if failed:
        log.warning("%3d client failed: %d/%d tests succeeded", number,
                    successes, len(tests))

    log.info("%3d client exited", number)
    return failed
Пример #16
0
Файл: main.py Проект: ekw/cpppo
def enip_srv( conn, addr, enip_process=None, delay=None, **kwds ):
    """Serve one Ethernet/IP client 'til EOF; then close the socket.  Parses headers and encapsulated
    EtherNet/IP request data 'til either the parser fails (the Client has submitted an un-parsable
    request), or the request handler fails.  Otherwise, encodes the data.response in an EtherNet/IP
    packet and sends it back to the client.

    Use the supplied enip_process function to process each parsed EtherNet/IP frame, returning True
    if a data.response is formulated, False if the session has ended cleanly, or raise an Exception
    if there is a processing failure (eg. an unparsable request, indicating that the Client is
    speaking an unknown dialect and the session must close catastrophically.)

    If a partial EtherNet/IP header is parsed and an EOF is received, the enip_header parser will
    raise an AssertionError, and we'll simply drop the connection.  If we receive a valid header and
    request, the supplied enip_process function is expected to formulate an appropriate error
    response, and we'll continue processing requests.

    An option numeric delay value (or any delay object with a .value attribute evaluating to a
    numeric value) may be specified; every response will be delayed by the specified number of
    seconds.  We assume that such a value may be altered over time, so we access it afresh for each
    use.

    All remaining keywords are passed along to the supplied enip_process function.
    """
    global latency
    global timeout

    name			= "enip_%s" % addr[1]
    log.normal( "EtherNet/IP Server %s begins serving peer %s", name, addr )


    source			= cpppo.rememberable()
    with parser.enip_machine( name=name, context='enip' ) as enip_mesg:

        # We can be provided a dotdict() to contain our stats.  If one has been passed in, then this
        # means that our stats for this connection will be available to the web API; it may set
        # stats.eof to True at any time, terminating the connection!  The web API will try to coerce
        # its input into the same type as the variable, so we'll keep it an int (type bool doesn't
        # handle coercion from strings).  We'll use an apidict, to ensure that attribute values set
        # via the web API thread (eg. stats.eof) are blocking 'til this thread wakes up and reads
        # them.  Thus, the web API will block setting .eof, and won't return to the caller until the
        # thread is actually in the process of shutting down.  Internally, we'll use __setitem__
        # indexing to change stats values, so we don't block ourself!
        stats			= cpppo.apidict( timeout=timeout )
        connkey			= ( "%s_%d" % addr ).replace( '.', '_' )
        connections[connkey]	= stats
        try:
            assert enip_process is not None, \
                "Must specify an EtherNet/IP processing function via 'enip_process'"
            stats['requests']	= 0
            stats['received']	= 0
            stats['eof']	= False
            stats['interface']	= addr[0]
            stats['port']	= addr[1]
            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()
                log.detail( "Transaction begins" )
                for mch,sta in enip_mesg.run( path='request', source=source, data=data ):
                    if sta is None:
                        # 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()
                            log.detail( "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']:
                                log.detail( "%s done, due to server done/disable", 
                                            enip_mesg.name_centered() )
                                stats['eof']	= True
                            if msg is not None:
                                stats['received']+= len( msg )
                                stats['eof']	= stats['eof'] or not len( msg )
                                log.detail( "%s recv: %5d: %s", enip_mesg.name_centered(),
                                            len( msg ) if msg is not None else 0, 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.

                log.detail( "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( 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:
                            log.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 )
                        log.detail( "%s send: %5d: %s %s", enip_mesg.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:
                                log.detail( "Unable to delay; invalid seconds: %r", delay )
                        try:
                            conn.send( rpy )
                        except socket.error as exc:
                            log.detail( "Session ended (client abandoned): %s", exc )
                            stats['eof'] = True
                        if data.response.enip.status:
                            log.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.
                        log.detail( "Session ended (client initiated): %s",
                                    parser.enip_format( data ))
                        stats['eof'] = True
                    log.detail( "Transaction complete after %7.3fs (w/ %7.3fs delay)" % (
                        cpppo.timer() - begun, delayseconds ))
                except:
                    log.error( "Failed request: %s", parser.enip_format( data ))
                    enip_process( addr, data=cpppo.dotdict() ) # Terminate.
                    raise

            stats['processed']	= source.sent
        except:
            # Parsing failure.  We're done.  Suck out some remaining input to give us some context.
            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 )
            log.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.
            connections.pop( connkey, None )
            log.normal( "%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( connections ))
            sys.stdout.flush()
            conn.close()
Пример #17
0
    def __next__(self):
        """Return the next available response, or None if no complete response is available.  Raises
        StopIteration (cease iterating) on EOF.  Any other Exception indicates a client failure,
        and should result in the client instance being discarded.
        
        If no input is presently available, harvest any input immediately available; terminate on EOF.

        The response may not actually contain a payload, eg. if the EtherNet/IP header contains a
        non-zero status.
        """
        if self.source.peek() is None:
            rcvd = network.recv(self.conn, timeout=0)
            log.info(
                "EtherNet/IP-->%16s:%-5d rcvd %5d: %s",
                self.addr[0],
                self.addr[1],
                len(rcvd) if rcvd is not None else 0,
                repr(rcvd),
            )
            if rcvd is not None:
                # Some input (or EOF); source is empty; if no input available, terminate
                if not len(rcvd):
                    raise StopIteration
                self.source.chain(rcvd)
            else:
                # Don't create parsing engine 'til we have some I/O to process.  This avoids the
                # degenerate situation where empty I/O (EOF) always matches the empty command (used
                # to indicate the end of an EtherNet/IP session).
                if self.engine is None:
                    return None

        # Initiate or continue parsing input using the machine's engine; discard the engine at
        # termination or on error (Exception).  Any exception (including cpppo.NonTerminal) will be
        # propagated.
        result = None
        with self.frame as machine:
            try:
                if self.engine is None:
                    self.data = cpppo.dotdict()
                    self.engine = machine.run(source=self.source, data=self.data)
                    log.debug(
                        "EtherNet/IP   %16s:%-5d run.: %s -> %10.10s; next byte %3d: %-10.10r: %r",
                        self.addr[0],
                        self.addr[1],
                        machine.name_centered(),
                        machine.current,
                        self.source.sent,
                        self.source.peek(),
                        self.data,
                    )

                for m, s in self.engine:
                    log.debug(
                        "EtherNet/IP<--%16s:%-5d rpy.: %s -> %10.10s; next byte %3d: %-10.10r: %r",
                        self.addr[0],
                        self.addr[1],
                        machine.name_centered(),
                        s,
                        self.source.sent,
                        self.source.peek(),
                        self.data,
                    )
            except Exception as exc:
                log.warning("EtherNet/IP<x>%16s:%-5d err.: %s", self.addr[0], self.addr[1], str(exc))
                self.engine = None
                raise
            if machine.terminal:
                log.info(
                    "EtherNet/IP   %16s:%-5d done: %s -> %10.10s; next byte %3d: %-10.10r: %r",
                    self.addr[0],
                    self.addr[1],
                    machine.name_centered(),
                    machine.current,
                    self.source.sent,
                    self.source.peek(),
                    self.data,
                )
                # Got an EtherNet/IP frame.  Return it (after parsing its payload.)
                self.engine = None
                result = self.data

        # Parse the EtherNet/IP encapsulated CIP frame, if any.  If the EtherNet/IP header .size was
        # zero, it's status probably indicates why.
        if result is not None and "enip.input" in result:
            with self.cip as machine:
                for m, s in machine.run(path="enip", source=cpppo.peekable(result.enip.input), data=result):
                    log.debug(
                        "EtherNet/IP<--%16s:%-5d CIP : %s -> %10.10s; next byte %3d: %-10.10r: %r",
                        self.addr[0],
                        self.addr[1],
                        machine.name_centered(),
                        s,
                        self.source.sent,
                        self.source.peek(),
                        self.data,
                    )
                    pass
                assert machine.terminal, "No CIP payload in the EtherNet/IP frame: %r" % (result)

        # Parse the Logix request responses in the EtherNet/IP CIP payload's CPF items
        if result is not None and "enip.CIP.send_data" in result:
            for item in result.enip.CIP.send_data.CPF.item:
                if "unconnected_send.request" in item:
                    # An Unconnected Send that contained an encapsulated request (ie. not just a
                    # Get Attribute All)
                    with self.lgx as machine:
                        for m, s in machine.run(
                            source=cpppo.peekable(item.unconnected_send.request.input),
                            data=item.unconnected_send.request,
                        ):
                            pass
                        assert machine.terminal, "No Logix request in the EtherNet/IP CIP CPF frame: %r" % (result)

        return result