Ejemplo n.º 1
0
def test_iterators():
    i				= cpppo.chaining()
    j				= cpppo.chainable( i )
    k				= cpppo.chainable( 'abc' )
    assert i is j
    try:
        next( i )
        assert False, "stream with no iterable should raise StopIteration"
    except StopIteration:
        pass
    assert k is not j
    assert isinstance( k, cpppo.chaining )

    assert cpppo.peekable( i ) is i
    p				= cpppo.peekable( '123' )
    assert cpppo.peekable( p ) is p
    assert cpppo.chainable( p ) is not p
    assert list( p ) == ['1', '2', '3']
    assert p.sent == 3
    p.push('x')
    assert p.sent == 2
    assert list( p ) == ['x']
    assert list( p ) == []


    i.chain('abc')
    i.chain('')
    i.chain( '123' )
    assert list( i ) == ['a','b','c','1','2','3']
    assert i.sent == 6

    i.chain( 'y' )
    i.push( 'x' )
    assert list( i ) == ['x','y']

    i.chain( None )
    try:
        next( i )
        assert False, "Expected TypeError to be raised"
    except TypeError:
        pass
    except Exception as e:
        assert False, "Expected TypeError, not %r" % ( e )


    r				= cpppo.rememberable( '123' )
    assert next( r ) == '1'
    assert r.memory == [ '1' ]
    try:
        r.push( 'x' )
        assert False, "Should have rejected push of inconsistent symbol"
    except AssertionError:
        pass
    assert r.sent == 1
    r.push( '1' )
    assert r.sent == 0
    assert r.memory == []
    assert list( r ) == r.memory == [ '1', '2','3' ]
Ejemplo n.º 2
0
def test_iterators():
    i = cpppo.chaining()
    j = cpppo.chainable(i)
    k = cpppo.chainable("abc")
    assert i is j
    try:
        next(i)
        assert False, "stream with no iterable should raise StopIteration"
    except StopIteration:
        pass
    assert k is not j
    assert isinstance(k, cpppo.chaining)

    assert cpppo.peekable(i) is i
    p = cpppo.peekable("123")
    assert cpppo.peekable(p) is p
    assert cpppo.chainable(p) is not p
    assert list(p) == ["1", "2", "3"]
    assert p.sent == 3
    p.push("x")
    assert p.sent == 2
    assert list(p) == ["x"]
    assert list(p) == []

    i.chain("abc")
    i.chain("")
    i.chain("123")
    assert list(i) == ["a", "b", "c", "1", "2", "3"]
    assert i.sent == 6

    i.chain("y")
    i.push("x")
    assert list(i) == ["x", "y"]

    i.chain(None)
    try:
        next(i)
        assert False, "Expected TypeError to be raised"
    except TypeError:
        pass
    except Exception as e:
        assert False, "Expected TypeError, not %r" % (e)

    r = cpppo.rememberable("123")
    assert next(r) == "1"
    assert r.memory == ["1"]
    try:
        r.push("x")
        assert False, "Should have rejected push of inconsistent symbol"
    except AssertionError:
        pass
    assert r.sent == 1
    r.push("1")
    assert r.sent == 0
    assert r.memory == []
    assert list(r) == r.memory == ["1", "2", "3"]
Ejemplo n.º 3
0
    def request(self, data):
        """
        Handles an unparsed request.input, parses it and processes the request with the Message Router.
        

        """
        # We don't check for Unconnected Send 0x52, because replies (and some requests) don't
        # include the full wrapper, just the raw command.  This is quite confusing; especially since
        # some of the commands have the same code (eg. Read Tag Fragmented, 0x52).  Of course, their
        # replies don't (0x52|0x80 == 0xd2).  The CIP.produce recognizes the absence of the
        # .command, and simply copies the encapsulated request.input as the response payload.  We
        # don't encode the response here; it is done by the UCMM.
        assert 'request' in data and 'input' in data.request, \
            "Unconnected Send message with absent or empty request"
        if log.isEnabledFor(logging.INFO):
            log.info("%s Request: %s", self, enip_format(data))

        #log.info( "%s Parsing: %s", self, enip_format( data.request ))
        # Get the Message Router to parse and process the request into a response, producing a
        # data.request.input encoded response, which we will pass back as our own encoded response.
        MR = lookup(class_id=0x02, instance_id=1)
        source = cpppo.rememberable(data.request.input)
        try:
            with MR.parser as machine:
                for i, (m, s) in enumerate(
                        machine.run(path='request', source=source, data=data)):
                    pass
                    #log.detail( "%s #%3d -> %10.10s; next byte %3d: %-10.10r: %s",
                    #            machine.name_centered(), i, s, source.sent, source.peek(),
                    #            repr( data ) if log.getEffectiveLevel() < logging.DETAIL else reprlib.repr( data ))

            #log.info( "%s Executing: %s", self, enip_format( data.request ))
            MR.request(data.request)
        except:
            # Parsing failure.  We're done.  Suck out some remaining input to give us some context.
            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)" % (
                processed, repr(memory + future), '-' *
                (len(repr(memory)) - 1) + '^', pos)
            log.error("EtherNet/IP CIP error %s\n", where)
            raise

        if log.isEnabledFor(logging.INFO):
            log.info("%s Response: %s", self, enip_format(data))
        return True
Ejemplo n.º 4
0
    def request( self, data ):
        """
        Handles an unparsed request.input, parses it and processes the request with the Message Router.
        

        """
        # We don't check for Unconnected Send 0x52, because replies (and some requests) don't
        # include the full wrapper, just the raw command.  This is quite confusing; especially since
        # some of the commands have the same code (eg. Read Tag Fragmented, 0x52).  Of course, their
        # replies don't (0x52|0x80 == 0xd2).  The CIP.produce recognizes the absence of the
        # .command, and simply copies the encapsulated request.input as the response payload.  We
        # don't encode the response here; it is done by the UCMM.
        assert 'request' in data and 'input' in data.request, \
            "Unconnected Send message with absent or empty request"
        if log.isEnabledFor( logging.INFO ):
            log.info( "%s Request: %s", self, enip_format( data ))

        #log.info( "%s Parsing: %s", self, enip_format( data.request ))
        # Get the Message Router to parse and process the request into a response, producing a
        # data.request.input encoded response, which we will pass back as our own encoded response.
        MR			= lookup( class_id=0x02, instance_id=1 )
        source			= cpppo.rememberable( data.request.input )
        try: 
            with MR.parser as machine:
                for i,(m,s) in enumerate( machine.run( path='request', source=source, data=data )):
                    pass
                    #log.detail( "%s #%3d -> %10.10s; next byte %3d: %-10.10r: %s",
                    #            machine.name_centered(), i, s, source.sent, source.peek(),
                    #            repr( data ) if log.getEffectiveLevel() < logging.DETAIL else reprlib.repr( data ))

            #log.info( "%s Executing: %s", self, enip_format( data.request ))
            MR.request( data.request )
        except:
            # Parsing failure.  We're done.  Suck out some remaining input to give us some context.
            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)" % (
                processed, repr(memory+future), '-' * (len(repr(memory))-1) + '^', pos )
            log.error( "EtherNet/IP CIP error %s\n", where )
            raise

        if log.isEnabledFor( logging.INFO ):
            log.info( "%s Response: %s", self, enip_format( data ))
        return True
Ejemplo n.º 5
0
def test_logix_multiple():
    """Test the Multiple Request Service.  Ensure multiple requests can be successfully handled, and
    invalid tags are correctly rejected.

    The Logix is a Message_Router instance, and is expected to be at Class 2, Instance 1.  Eject any
    non-Logix Message_Router that presently exist.

    """
    Obj				= enip.device.lookup( enip.device.Message_Router.class_id, instance_id=1 )
    if not isinstance( Obj, logix.Logix ):
        if Obj is not None:
            del enip.device.directory['2']['1']
        Obj			= logix.Logix( instance_id=1 )

    # Create some Attributes to test, but mask the big ones from Get Attributes All.
    size			= 1000
    Obj_a1 = Obj.attribute['1']	= enip.device.Attribute( 'parts',       enip.parser.DINT, default=[n for n in range( size )],
                                                         mask=enip.device.Attribute.MASK_GA_ALL )
    Obj_a2 = Obj.attribute['2']	= enip.device.Attribute( 'ControlWord', enip.parser.DINT, default=[0,0])
    Obj_a3 = Obj.attribute['3']	= enip.device.Attribute( 'SCADA_40001', enip.parser.INT,  default=[n for n in range( size )],
                                                         mask=enip.device.Attribute.MASK_GA_ALL )
    Obj_a4 = Obj.attribute['4']	= enip.device.Attribute( 'number',      enip.parser.REAL, default=0.0)

    # Set up a symbolic tag referencing the Logix Object's Attribute
    enip.device.symbol['parts']	= {'class': Obj.class_id, 'instance': Obj.instance_id, 'attribute':1 }
    enip.device.symbol['ControlWord'] \
				= {'class': Obj.class_id, 'instance': Obj.instance_id, 'attribute':2 }
    enip.device.symbol['SCADA_40001'] \
				= {'class': Obj.class_id, 'instance': Obj.instance_id, 'attribute':3 }
    enip.device.symbol['number'] \
				= {'class': Obj.class_id, 'instance': Obj.instance_id, 'attribute':4 }


    assert len( Obj_a1 ) == size
    assert len( Obj_a3 ) == size
    assert len( Obj_a4 ) == 1
    Obj_a1[0]			= 42
    Obj_a2[0]			= 476
    Obj_a4[0]			= 1.0
    # Ensure that the basic CIP Object requests work on a derived Class.
    for description,original,produced,parsed,result,response in GA_tests:
        request			= cpppo.dotdict( original )

        log.warning( "%s; request: %s", description, enip.enip_format( request ))
        encoded			= Obj.produce( request )
        assert encoded == produced, "%s: Didn't produce correct encoded request: %r != %r" % (
            description, encoded, produced )

        # Now, use the Message_Router's parser to decode the encoded bytes
        source			= cpppo.rememberable( encoded )
        decoded			= cpppo.dotdict()
        with Obj.parser as machine:
            for m,s in enumerate( machine.run( source=source, data=decoded )):
                pass
        for k,v in cpppo.dotdict( parsed ).items():
            assert decoded[k] == v, "%s: Didn't parse expected value: %s != %r in %s" % (
                description, k, v, enip.enip_format( decoded ))

        # Process the request into a reply, and ensure we get the expected result (some Attributes
        # are filtered from Get Attributes All; only a 2-element DINT array and a single REAL should
        # be produced)
        Obj.request( request )
        logging.warning("%s: reply:   %s", description, enip.enip_format( request ))
        for k,v in cpppo.dotdict( result ).items():
            assert k in request and request[k] == v, \
                "%s: Didn't result in expected response: %s != %r; got %r" % (
                    description, k, v, request[k] if k in request else "(not found)" )

        # Finally, produce the encoded response
        encoded			= Obj.produce( request )
        assert encoded == response, "%s: Didn't produce correct encoded response: %r != %r" % (
            description, encoded, response )


    # Test that we correctly compute beg,end,endactual for various Read Tag Fragmented scenarios,
    # with 2-byte and 4-byte types.  For the purposes of this test, we only look at path...elements.
    data			= cpppo.dotdict()
    data.service		= Obj.RD_FRG_RPY
    data.path			= { 'segment': [ cpppo.dotdict( d )
                                                 for d in [
                                                         {'element': 0 },
                                                       ]] }
    data.read_frag		= {}
    data.read_frag.elements	= 1000
    data.read_frag.offset	= 0
    
    # Reply maximum size limited
    beg,end,endactual		= Obj.reply_elements( Obj_a1, data, 'read_frag' )
    assert beg == 0 and end == 125 and endactual == 1000 # DINT == 4 bytes
    beg,end,endactual		= Obj.reply_elements( Obj_a3, data, 'read_frag' )
    assert beg == 0 and end == 250 and endactual == 1000 # INT == 2 bytes

    data.read_frag.offset	= 125*4 # OK, second request; begin after byte offset of first
    beg,end,endactual		= Obj.reply_elements( Obj_a1, data, 'read_frag' )
    assert beg == 125 and end == 250 and endactual == 1000 # DINT == 4 bytes

    # Request elements limited; 0 offset
    data.read_frag.elements	= 30
    data.read_frag.offset	= 0
    beg,end,endactual		= Obj.reply_elements( Obj_a3, data, 'read_frag' )
    assert beg == 0 and end == 30 and endactual == 30 # INT == 2 bytes

    # Request elements limited; +'ve offset
    data.read_frag.elements	= 70
    data.read_frag.offset	= 80
    beg,end,endactual		= Obj.reply_elements( Obj_a3, data, 'read_frag' )
    assert beg == 40 and end == 70 and endactual == 70 # INT == 2 bytes

    # Request limited by size of data provided (Write Tag [Fragmented])
    data			= cpppo.dotdict()
    data.service		= Obj.WR_FRG_RPY
    data.path			= { 'segment': [ cpppo.dotdict( d )
                                                 for d in [
                                                         {'element': 0 },
                                                       ]] }
    data.write_frag		= {}
    data.write_frag.data	= [0] * 100 # 100 elements provided in this request
    data.write_frag.elements	= 200       # Total request is to write 200 elements
    data.write_frag.offset	= 16        # request starts 16 bytes in (8 INTs)
    beg,end,endactual		= Obj.reply_elements( Obj_a3, data, 'write_frag' )
    assert beg == 8 and end == 108 and endactual == 200 # INT == 2 bytes

    # ... same, but lets say request started somewhere in the middle of the array
    data.path			= { 'segment': [ cpppo.dotdict( d )
                                                 for d in [
                                                         {'element': 222 },
                                                       ]] }
    beg,end,endactual		= Obj.reply_elements( Obj_a3, data, 'write_frag' )
    assert beg == 8+222 and end == 108+222 and endactual == 200+222 # INT == 2 bytes

    # Ensure correct computation of (beg,end] that are byte-offset and data/size limited
    data			= cpppo.dotdict()
    data.service		= Obj.WR_FRG_RPY
    data.path			= { 'segment': [] }

    data.write_frag		= {}
    data.write_frag.data	= [3,4,5,6]
    data.write_frag.offset	= 6
    beg,end,endactual		= Obj.reply_elements( Obj_a3, data, 'write_frag' )
    assert beg == 3 and end == 7 and endactual == 1000 # INT == 2 bytes

    # Trigger the error cases only accessible via write

    # Too many elements provided for attribute capacity
    data.write_frag.offset	= ( 1000 - 3 ) * 2
    try:
        beg,end,endactual	= Obj.reply_elements( Obj_a3, data, 'write_frag' )
        assert False, "Should have raised Exception due to capacity"
    except Exception as exc:
        assert "capacity exceeded" in str( exc )

    data			= cpppo.dotdict()
    data.service		= Obj.RD_FRG_RPY
    data.path			= { 'segment': [] }

    data.read_frag		= {}
    data.read_frag.offset	= 6
    beg,end,endactual		= Obj.reply_elements( Obj_a3, data, 'read_frag' )
    assert beg == 3 and end == 253 and endactual == 1000 # INT == 2 bytes

    # And we should be able to read with an offset right up to the last element
    data.read_frag.offset	= 1998
    beg,end,endactual		= Obj.reply_elements( Obj_a3, data, 'read_frag' )
    assert beg == 999 and end == 1000 and endactual == 1000 # INT == 2 bytes


    # Trigger all the remaining error cases

    # Unknown service
    data.service		= Obj.RD_FRG_REQ
    try:
        beg,end,endactual	= Obj.reply_elements( Obj_a3, data, 'read_frag' )
        assert False, "Should have raised Exception due to service"
    except Exception as exc:
        assert "unknown service" in str( exc )

    # Offset indivisible by element size
    data.service		= Obj.RD_FRG_RPY
    data.read_frag.offset	= 7
    try:
        beg,end,endactual	= Obj.reply_elements( Obj_a3, data, 'read_frag' )
        assert False, "Should have raised Exception due to odd byte offset"
    except Exception as exc:
        assert "element boundary" in str( exc )

    # Initial element outside bounds
    data.read_frag.offset	= 2000
    try:
        beg,end,endactual	= Obj.reply_elements( Obj_a3, data, 'read_frag' )
        assert False, "Should have raised Exception due to initial element"
    except Exception as exc:
        assert "initial element invalid" in str( exc )

    # Ending element outside bounds
    data.read_frag.offset	= 0
    data.read_frag.elements	= 1001
    try:
        beg,end,endactual	= Obj.reply_elements( Obj_a3, data, 'read_frag' )
        assert False, "Should have raised Exception due to ending element"
    except Exception as exc:
        assert "ending element invalid" in str( exc )

    # Beginning element after ending (should be no way to trigger).  This request doesn't specify an
    # element in the path, hence defaults to element 0, and asks for a number of elements == 2.
    # Thus, there is no 6-byte offset possible (a 2-byte offset is, though).
    data.read_frag.offset	= 6
    data.read_frag.elements	= 2
    try:
        beg,end,endactual	= Obj.reply_elements( Obj_a3, data, 'read_frag' )
        assert False, "Should have raised Exception due to ending element order"
    except Exception as exc:
        assert "ending element before beginning" in str( exc )
    data.read_frag.offset	= 2
    data.read_frag.elements	= 2
    beg,end,endactual		= Obj.reply_elements( Obj_a3, data, 'read_frag' )
    assert beg == 1 and end == 2 and endactual == 2 # INT == 2 bytes


    # Test an example valid multiple request
    data			= cpppo.dotdict()
    data.multiple		= {}
    data.multiple.request	= [
        cpppo.dotdict(), cpppo.dotdict(), cpppo.dotdict(), cpppo.dotdict(), cpppo.dotdict() ] 
    req				= data.multiple.request

    req[0].path			= { 'segment': [ cpppo.dotdict( d )
                                                 for d in [{'symbolic': 'parts'}]] }
    req[0].read_tag		= {}
    req[0].read_tag.elements	= 1
    
    req[1].path			= { 'segment': [ cpppo.dotdict( d )
                                                 for d in [{'symbolic': 'ControlWord'}]] }
    req[1].read_tag		= {}
    req[1].read_tag.elements	= 1

    req[2].path			= { 'segment': [ cpppo.dotdict( d )
                                                 for d in [{'symbolic': 'number'}]] }
    req[2].read_tag		= {}
    req[2].read_tag.elements	= 1

    req[3].path			= { 'segment': [ cpppo.dotdict( d )
                                                 for d in [{'symbolic': 'number'}]] }
    req[3].write_tag		= {}
    req[3].write_tag.elements	= 1
    req[3].write_tag.type	= 0x00ca
    req[3].write_tag.data	= [1.25]

    req[4].path			= { 'segment': [ cpppo.dotdict( d )
                                                 for d in [{'symbolic': 'number'}]] }
    req[4].read_tag		= {}
    req[4].read_tag.elements	= 1


    request			= Obj.produce( data )

    req_1			= bytes(bytearray([
        0x0A,
        0x02,
        0x20, 0x02, 0x24, 0x01,
        
        0x05, 0x00,
        
        0x0c, 0x00,
        0x18, 0x00,
        0x2a, 0x00,
        0x36, 0x00,
        0x48, 0x00,
        
        0x4C,
        0x04, 0x91, 0x05, 0x70, 0x61,
        0x72, 0x74, 0x73, 0x00,
        0x01, 0x00,
        
        0x4C,
        0x07, 0x91, 0x0B, 0x43, 0x6F,
        0x6E, 0x74, 0x72, 0x6F, 0x6C,
        0x57, 0x6F, 0x72, 0x64, 0x00,
        0x01, 0x00,

        b'L'[0],
        0x04, 0x91, 0x06, b'n'[0], b'u'[0], b'm'[0], b'b'[0], b'e'[0], b'r'[0],
        0x01, 0x00,

        b'M'[0],
        0x04, 0x91, 0x06, b'n'[0], b'u'[0], b'm'[0], b'b'[0], b'e'[0], b'r'[0],
        0xca, 0x00, 0x01, 0x00, 0x00, 0x00, 0xa0, 0x3f,

        b'L'[0],
        0x04, 0x91, 0x06, b'n'[0], b'u'[0], b'm'[0], b'b'[0], b'e'[0], b'r'[0],
        0x01, 0x00,
    ]))
                       
    assert request == req_1, \
        "Unexpected result from Multiple Request Service; got: \n%r\nvs.\n%r " % ( request, req_1 )

    # Now, use the Message_Router's parser
    source			= cpppo.rememberable( request )
    data			= cpppo.dotdict()
    with Obj.parser as machine:
        for i,(m,s) in enumerate( machine.run( source=source, data=data )):
            pass
    log.normal( "Multiple Request: %s", enip.enip_format( data ))
    assert 'multiple' in data, \
        "No parsed multiple found in data: %s" % enip.enip_format( data )
    assert data.service == enip.device.Message_Router.MULTIPLE_REQ, \
        "Expected a Multiple Request Service request: %s" % enip.enip_format( data )
    assert data.multiple.number == 5, \
        "Expected 5 requests in request.multiple: %s" % enip.enip_format( data )

    # And ensure if we re-encode the parsed result, we get the original encoded request back
    assert Obj.produce( data ) == req_1

    # Process the request into a reply.
    Obj.request( data )
    log.normal( "Multiple Response: %s", enip.enip_format( data ))
    assert data.service == enip.device.Message_Router.MULTIPLE_RPY, \
        "Expected a Multiple Request Service reply: %s" % enip.enip_format( data )

    rpy_1			= bytearray([
        0x8A,
        0x00,
        0x00,
        0x00,

        0x05, 0x00,

        0x0c, 0x00,
        0x16, 0x00,
        0x20, 0x00,
        0x2a, 0x00,
        0x2e, 0x00,

        0xCC, 0x00, 0x00, 0x00,
        0xC4, 0x00,
        0x2A, 0x00, 0x00, 0x00,

        0xCC, 0x00, 0x00, 0x00,
        0xC4, 0x00,
        0xDC, 0x01, 0x00, 0x00,

        0xCC, 0x00, 0x00, 0x00,
        0xCA, 0x00, 0x00, 0x00, 0x80, 0x3F,

        0xcd, 0x00, 0x00, 0x00,

        0xcc, 0x00, 0x00, 0x00,
        0xca, 0x00, 0x00, 0x00, 0xa0, 0x3f,
    ])

    assert data.input == rpy_1, \
        "Unexpected reply from Multiple Request Service request; got: \n%r\nvs.\n%r " % ( data.input, rpy_1 )

    # Now lets try some valid and invalid requests
    data			= cpppo.dotdict()
    data.multiple		= {}
    data.multiple.request = req	= [ cpppo.dotdict() ]
    req[0].path			= { 'segment': [ cpppo.dotdict( d )
                                                 for d in [{'symbolic': 'SCADA_40001'}]] }
    req[0].read_tag		= {}
    req[0].read_tag.elements	= 1
    data.multiple.number	= len( data.multiple.request )

    request			= Obj.produce( data )

    req_good			= bytearray([
        0x0A,
        0x02,
        0x20, 0x02, ord('$'), 0x01,
        
        0x01, 0x00,
        
        0x04, 0x00,
        
        0x4C,
        0x07, 0x91, 0x0b, ord('S'), ord('C'),
        ord('A'), ord('D'), ord('A'), ord('_'), ord('4'),
        ord('0'), ord('0'), ord('0'), ord('1'), 0x00,
        0x01, 0x00,
    ])
    assert request == req_good, \
        "Unexpected result from Multiple Request Service request for SCADA_40001; got: \n%r\nvs.\n%r " % ( request, req_good )

    Obj.request( data )
    rpy_good			= bytearray([
        0x8A,
        0x00,
        0x00,
        0x00,

        0x01, 0x00,

        0x04, 0x00,

        0xCC, 0x00, 0x00, 0x00,
        0xC3, 0x00,
        0x00, 0x00,
    ])

    assert data.input == rpy_good, \
        "Unexpected reply from Multiple Request Service request for SCADA_40001; got: \n%r\nvs.\n%r " % ( data.input, rpy_good )

    # Add an invalid request
    data			= cpppo.dotdict()
    data.multiple		= {}
    data.multiple.request = req	= [ cpppo.dotdict(), cpppo.dotdict() ]
    req[0].path			= { 'segment': [ cpppo.dotdict( d )
                                                 for d in [{'symbolic': 'SCADA_40001'}]] }
    req[0].read_tag		= {}
    req[0].read_tag.elements	= 1
    req[1].path			= { 'segment': [ cpppo.dotdict( d )
                                                 for d in [{'symbolic': 'SCADA_40002'}]] }
    req[1].read_tag		= {}
    req[1].read_tag.elements	= 1
    data.multiple.number	= len( data.multiple.request )

    request			= Obj.produce( data )

    req_bad			= bytearray([
        0x0A,
        0x02,
        0x20, 0x02, ord('$'), 0x01,
        
        0x02, 0x00,
        
        0x06, 0x00,
        0x18, 0x00,
        
        0x4C,
        0x07, 0x91, 0x0b, ord('S'), ord('C'),
        ord('A'), ord('D'), ord('A'), ord('_'), ord('4'),
        ord('0'), ord('0'), ord('0'), ord('1'), 0x00,
        0x01, 0x00,

        0x4C,
        0x07, 0x91, 0x0b, ord('S'), ord('C'),
        ord('A'), ord('D'), ord('A'), ord('_'), ord('4'),
        ord('0'), ord('0'), ord('0'), ord('2'), 0x00,
        0x01, 0x00,
    ])
    assert request == req_bad, \
        "Unexpected result from Multiple Request Service request for SCADA_40001/2; got: \n%r\nvs.\n%r " % ( request, req_bad )

    Obj.request( data )
    rpy_bad			= bytearray([
        0x8A,
        0x00,
        0x00,
        0x00,

        0x02, 0x00,

        0x06, 0x00,
        0x0e, 0x00,

        0xCC, 0x00, 0x00, 0x00,
        0xC3, 0x00,
        0x00, 0x00,

        0xCC, 0x00, 0x05, 0x01, # Status code 0x05 (invalid path)
        0x00, 0x00,
    ])
    assert data.input == rpy_bad, \
        "Unexpected reply from Multiple Request Service request for SCADA_40001/2; got: \n%r\nvs.\n%r " % ( data.input, rpy_bad )
Ejemplo n.º 6
0
Archivo: main.py Proyecto: 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()
Ejemplo n.º 7
0
def test_logix_multiple():
    """Test the Multiple Request Service.  Ensure multiple requests can be successfully handled, and
    invalid tags are correctly rejected.

    The Logix is a Message_Router instance, and is expected to be at Class 2, Instance 1.  Eject any
    non-Logix Message_Router that presently exist.

    """
    enip.lookup_reset()  # Flush out any existing CIP Objects for a fresh start
    Obj = logix.Logix(instance_id=1)

    # Create some Attributes to test, but mask the big ones from Get Attributes All.
    size = 1000
    Obj_a1 = Obj.attribute['1'] = enip.device.Attribute(
        'parts',
        enip.parser.DINT,
        default=[n for n in range(size)],
        mask=enip.device.Attribute.MASK_GA_ALL)
    Obj_a2 = Obj.attribute['2'] = enip.device.Attribute('ControlWord',
                                                        enip.parser.DINT,
                                                        default=[0, 0])
    Obj_a3 = Obj.attribute['3'] = enip.device.Attribute(
        'SCADA_40001',
        enip.parser.INT,
        default=[n for n in range(size)],
        mask=enip.device.Attribute.MASK_GA_ALL)
    Obj_a4 = Obj.attribute['4'] = enip.device.Attribute('number',
                                                        enip.parser.REAL,
                                                        default=0.0)

    # Set up a symbolic tag referencing the Logix Object's Attribute
    enip.device.symbol['parts'] = {
        'class': Obj.class_id,
        'instance': Obj.instance_id,
        'attribute': 1
    }
    enip.device.symbol['ControlWord'] \
    = {'class': Obj.class_id, 'instance': Obj.instance_id, 'attribute':2 }
    enip.device.symbol['SCADA_40001'] \
    = {'class': Obj.class_id, 'instance': Obj.instance_id, 'attribute':3 }
    enip.device.symbol['number'] \
    = {'class': Obj.class_id, 'instance': Obj.instance_id, 'attribute':4 }

    assert len(Obj_a1) == size
    assert len(Obj_a3) == size
    assert len(Obj_a4) == 1
    Obj_a1[0] = 42
    Obj_a2[0] = 476
    Obj_a4[0] = 1.0
    # Ensure that the basic CIP Object requests work on a derived Class.
    for description, original, produced, parsed, result, response in GA_tests:
        request = cpppo.dotdict(original)

        log.warning("%s; request: %s", description, enip.enip_format(request))
        encoded = Obj.produce(request)
        assert encoded == produced, "%s: Didn't produce correct encoded request: %r != %r" % (
            description, encoded, produced)

        # Now, use the Message_Router's parser to decode the encoded bytes
        source = cpppo.rememberable(encoded)
        decoded = cpppo.dotdict()
        with Obj.parser as machine:
            for m, s in enumerate(machine.run(source=source, data=decoded)):
                pass
        for k, v in cpppo.dotdict(parsed).items():
            assert decoded[
                k] == v, "%s: Didn't parse expected value: %s != %r in %s" % (
                    description, k, v, enip.enip_format(decoded))

        # Process the request into a reply, and ensure we get the expected result (some Attributes
        # are filtered from Get Attributes All; only a 2-element DINT array and a single REAL should
        # be produced)
        Obj.request(request)
        logging.warning("%s: reply:   %s", description,
                        enip.enip_format(request))
        for k, v in cpppo.dotdict(result).items():
            assert k in request and request[k] == v, \
                "%s: Didn't result in expected response: %s != %r; got %r" % (
                    description, k, v, request[k] if k in request else "(not found)" )

        # Finally, produce the encoded response
        encoded = Obj.produce(request)
        assert encoded == response, "%s: Didn't produce correct encoded response: %r != %r" % (
            description, encoded, response)

    # Test that we correctly compute beg,end,endactual for various Read Tag Fragmented scenarios,
    # with 2-byte and 4-byte types.  For the purposes of this test, we only look at path...elements.
    data = cpppo.dotdict()
    data.service = Obj.RD_FRG_RPY
    data.path = {
        'segment': [cpppo.dotdict(d) for d in [
            {
                'element': 0
            },
        ]]
    }
    data.read_frag = {}
    data.read_frag.elements = 1000
    data.read_frag.offset = 0

    # Reply maximum size limited
    beg, end, endactual = Obj.reply_elements(Obj_a1, data, 'read_frag')
    assert beg == 0 and end == 125 and endactual == 1000  # DINT == 4 bytes
    beg, end, endactual = Obj.reply_elements(Obj_a3, data, 'read_frag')
    assert beg == 0 and end == 250 and endactual == 1000  # INT == 2 bytes

    data.read_frag.offset = 125 * 4  # OK, second request; begin after byte offset of first
    beg, end, endactual = Obj.reply_elements(Obj_a1, data, 'read_frag')
    assert beg == 125 and end == 250 and endactual == 1000  # DINT == 4 bytes

    # Request elements limited; 0 offset
    data.read_frag.elements = 30
    data.read_frag.offset = 0
    beg, end, endactual = Obj.reply_elements(Obj_a3, data, 'read_frag')
    assert beg == 0 and end == 30 and endactual == 30  # INT == 2 bytes

    # Request elements limited; +'ve offset
    data.read_frag.elements = 70
    data.read_frag.offset = 80
    beg, end, endactual = Obj.reply_elements(Obj_a3, data, 'read_frag')
    assert beg == 40 and end == 70 and endactual == 70  # INT == 2 bytes

    # Request limited by size of data provided (Write Tag [Fragmented])
    data = cpppo.dotdict()
    data.service = Obj.WR_FRG_RPY
    data.path = {
        'segment': [cpppo.dotdict(d) for d in [
            {
                'element': 0
            },
        ]]
    }
    data.write_frag = {}
    data.write_frag.data = [0] * 100  # 100 elements provided in this request
    data.write_frag.elements = 200  # Total request is to write 200 elements
    data.write_frag.offset = 16  # request starts 16 bytes in (8 INTs)
    beg, end, endactual = Obj.reply_elements(Obj_a3, data, 'write_frag')
    assert beg == 8 and end == 108 and endactual == 200  # INT == 2 bytes

    # ... same, but lets say request started somewhere in the middle of the array
    data.path = {
        'segment': [cpppo.dotdict(d) for d in [
            {
                'element': 222
            },
        ]]
    }
    beg, end, endactual = Obj.reply_elements(Obj_a3, data, 'write_frag')
    assert beg == 8 + 222 and end == 108 + 222 and endactual == 200 + 222  # INT == 2 bytes

    # Ensure correct computation of (beg,end] that are byte-offset and data/size limited
    data = cpppo.dotdict()
    data.service = Obj.WR_FRG_RPY
    data.path = {'segment': []}

    data.write_frag = {}
    data.write_frag.data = [3, 4, 5, 6]
    data.write_frag.offset = 6
    beg, end, endactual = Obj.reply_elements(Obj_a3, data, 'write_frag')
    assert beg == 3 and end == 7 and endactual == 1000  # INT == 2 bytes

    # Trigger the error cases only accessible via write

    # Too many elements provided for attribute capacity
    data.write_frag.offset = (1000 - 3) * 2
    try:
        beg, end, endactual = Obj.reply_elements(Obj_a3, data, 'write_frag')
        assert False, "Should have raised Exception due to capacity"
    except Exception as exc:
        assert "capacity exceeded" in str(exc)

    data = cpppo.dotdict()
    data.service = Obj.RD_FRG_RPY
    data.path = {'segment': []}

    data.read_frag = {}
    data.read_frag.offset = 6
    beg, end, endactual = Obj.reply_elements(Obj_a3, data, 'read_frag')
    assert beg == 3 and end == 253 and endactual == 1000  # INT == 2 bytes

    # And we should be able to read with an offset right up to the last element
    data.read_frag.offset = 1998
    beg, end, endactual = Obj.reply_elements(Obj_a3, data, 'read_frag')
    assert beg == 999 and end == 1000 and endactual == 1000  # INT == 2 bytes

    # Trigger all the remaining error cases

    # Unknown service
    data.service = Obj.RD_FRG_REQ
    try:
        beg, end, endactual = Obj.reply_elements(Obj_a3, data, 'read_frag')
        assert False, "Should have raised Exception due to service"
    except Exception as exc:
        assert "unknown service" in str(exc)

    # Offset indivisible by element size
    data.service = Obj.RD_FRG_RPY
    data.read_frag.offset = 7
    try:
        beg, end, endactual = Obj.reply_elements(Obj_a3, data, 'read_frag')
        assert False, "Should have raised Exception due to odd byte offset"
    except Exception as exc:
        assert "element boundary" in str(exc)

    # Initial element outside bounds
    data.read_frag.offset = 2000
    try:
        beg, end, endactual = Obj.reply_elements(Obj_a3, data, 'read_frag')
        assert False, "Should have raised Exception due to initial element"
    except Exception as exc:
        assert "initial element invalid" in str(exc)

    # Ending element outside bounds
    data.read_frag.offset = 0
    data.read_frag.elements = 1001
    try:
        beg, end, endactual = Obj.reply_elements(Obj_a3, data, 'read_frag')
        assert False, "Should have raised Exception due to ending element"
    except Exception as exc:
        assert "ending element invalid" in str(exc)

    # Beginning element after ending (should be no way to trigger).  This request doesn't specify an
    # element in the path, hence defaults to element 0, and asks for a number of elements == 2.
    # Thus, there is no 6-byte offset possible (a 2-byte offset is, though).
    data.read_frag.offset = 6
    data.read_frag.elements = 2
    try:
        beg, end, endactual = Obj.reply_elements(Obj_a3, data, 'read_frag')
        assert False, "Should have raised Exception due to ending element order"
    except Exception as exc:
        assert "ending element before beginning" in str(exc)
    data.read_frag.offset = 2
    data.read_frag.elements = 2
    beg, end, endactual = Obj.reply_elements(Obj_a3, data, 'read_frag')
    assert beg == 1 and end == 2 and endactual == 2  # INT == 2 bytes

    # Test an example valid multiple request
    data = cpppo.dotdict()
    data.multiple = {}
    data.multiple.request = [
        cpppo.dotdict(),
        cpppo.dotdict(),
        cpppo.dotdict(),
        cpppo.dotdict(),
        cpppo.dotdict()
    ]
    req = data.multiple.request

    req[0].path = {
        'segment': [cpppo.dotdict(d) for d in [{
            'symbolic': 'parts'
        }]]
    }
    req[0].read_tag = {}
    req[0].read_tag.elements = 1

    req[1].path = {
        'segment': [cpppo.dotdict(d) for d in [{
            'symbolic': 'ControlWord'
        }]]
    }
    req[1].read_tag = {}
    req[1].read_tag.elements = 1

    req[2].path = {
        'segment': [cpppo.dotdict(d) for d in [{
            'symbolic': 'number'
        }]]
    }
    req[2].read_tag = {}
    req[2].read_tag.elements = 1

    req[3].path = {
        'segment': [cpppo.dotdict(d) for d in [{
            'symbolic': 'number'
        }]]
    }
    req[3].write_tag = {}
    req[3].write_tag.elements = 1
    req[3].write_tag.type = 0x00ca
    req[3].write_tag.data = [1.25]

    req[4].path = {
        'segment': [cpppo.dotdict(d) for d in [{
            'symbolic': 'number'
        }]]
    }
    req[4].read_tag = {}
    req[4].read_tag.elements = 1

    request = Obj.produce(data)

    req_1 = bytes(
        bytearray([
            0x0A,
            0x02,
            0x20,
            0x02,
            0x24,
            0x01,
            0x05,
            0x00,
            0x0c,
            0x00,
            0x18,
            0x00,
            0x2a,
            0x00,
            0x36,
            0x00,
            0x48,
            0x00,
            0x4C,
            0x04,
            0x91,
            0x05,
            0x70,
            0x61,
            0x72,
            0x74,
            0x73,
            0x00,
            0x01,
            0x00,
            0x4C,
            0x07,
            0x91,
            0x0B,
            0x43,
            0x6F,
            0x6E,
            0x74,
            0x72,
            0x6F,
            0x6C,
            0x57,
            0x6F,
            0x72,
            0x64,
            0x00,
            0x01,
            0x00,
            b'L'[0],
            0x04,
            0x91,
            0x06,
            b'n'[0],
            b'u'[0],
            b'm'[0],
            b'b'[0],
            b'e'[0],
            b'r'[0],
            0x01,
            0x00,
            b'M'[0],
            0x04,
            0x91,
            0x06,
            b'n'[0],
            b'u'[0],
            b'm'[0],
            b'b'[0],
            b'e'[0],
            b'r'[0],
            0xca,
            0x00,
            0x01,
            0x00,
            0x00,
            0x00,
            0xa0,
            0x3f,
            b'L'[0],
            0x04,
            0x91,
            0x06,
            b'n'[0],
            b'u'[0],
            b'm'[0],
            b'b'[0],
            b'e'[0],
            b'r'[0],
            0x01,
            0x00,
        ]))

    assert request == req_1, \
        "Unexpected result from Multiple Request Service; got: \n%r\nvs.\n%r " % ( request, req_1 )

    # Now, use the Message_Router's parser
    source = cpppo.rememberable(request)
    data = cpppo.dotdict()
    with Obj.parser as machine:
        for i, (m, s) in enumerate(machine.run(source=source, data=data)):
            pass
    log.normal("Multiple Request: %s", enip.enip_format(data))
    assert 'multiple' in data, \
        "No parsed multiple found in data: %s" % enip.enip_format( data )
    assert data.service == enip.device.Message_Router.MULTIPLE_REQ, \
        "Expected a Multiple Request Service request: %s" % enip.enip_format( data )
    assert data.multiple.number == 5, \
        "Expected 5 requests in request.multiple: %s" % enip.enip_format( data )

    # And ensure if we re-encode the parsed result, we get the original encoded request back
    assert Obj.produce(data) == req_1

    # Process the request into a reply.
    Obj.request(data)
    log.normal("Multiple Response: %s", enip.enip_format(data))
    assert data.service == enip.device.Message_Router.MULTIPLE_RPY, \
        "Expected a Multiple Request Service reply: %s" % enip.enip_format( data )

    rpy_1 = bytearray([
        0x8A,
        0x00,
        0x00,
        0x00,
        0x05,
        0x00,
        0x0c,
        0x00,
        0x16,
        0x00,
        0x20,
        0x00,
        0x2a,
        0x00,
        0x2e,
        0x00,
        0xCC,
        0x00,
        0x00,
        0x00,
        0xC4,
        0x00,
        0x2A,
        0x00,
        0x00,
        0x00,
        0xCC,
        0x00,
        0x00,
        0x00,
        0xC4,
        0x00,
        0xDC,
        0x01,
        0x00,
        0x00,
        0xCC,
        0x00,
        0x00,
        0x00,
        0xCA,
        0x00,
        0x00,
        0x00,
        0x80,
        0x3F,
        0xcd,
        0x00,
        0x00,
        0x00,
        0xcc,
        0x00,
        0x00,
        0x00,
        0xca,
        0x00,
        0x00,
        0x00,
        0xa0,
        0x3f,
    ])

    assert data.input == rpy_1, \
        "Unexpected reply from Multiple Request Service request; got: \n%r\nvs.\n%r " % ( data.input, rpy_1 )

    # Now lets try some valid and invalid requests
    data = cpppo.dotdict()
    data.multiple = {}
    data.multiple.request = req = [cpppo.dotdict()]
    req[0].path = {
        'segment': [cpppo.dotdict(d) for d in [{
            'symbolic': 'SCADA_40001'
        }]]
    }
    req[0].read_tag = {}
    req[0].read_tag.elements = 1
    data.multiple.number = len(data.multiple.request)

    request = Obj.produce(data)

    req_good = bytearray([
        0x0A,
        0x02,
        0x20,
        0x02,
        ord('$'),
        0x01,
        0x01,
        0x00,
        0x04,
        0x00,
        0x4C,
        0x07,
        0x91,
        0x0b,
        ord('S'),
        ord('C'),
        ord('A'),
        ord('D'),
        ord('A'),
        ord('_'),
        ord('4'),
        ord('0'),
        ord('0'),
        ord('0'),
        ord('1'),
        0x00,
        0x01,
        0x00,
    ])
    assert request == req_good, \
        "Unexpected result from Multiple Request Service request for SCADA_40001; got: \n%r\nvs.\n%r " % ( request, req_good )

    Obj.request(data)
    rpy_good = bytearray([
        0x8A,
        0x00,
        0x00,
        0x00,
        0x01,
        0x00,
        0x04,
        0x00,
        0xCC,
        0x00,
        0x00,
        0x00,
        0xC3,
        0x00,
        0x00,
        0x00,
    ])

    assert data.input == rpy_good, \
        "Unexpected reply from Multiple Request Service request for SCADA_40001; got: \n%r\nvs.\n%r " % ( data.input, rpy_good )

    # Add an invalid request
    data = cpppo.dotdict()
    data.multiple = {}
    data.multiple.request = req = [cpppo.dotdict(), cpppo.dotdict()]
    req[0].path = {
        'segment': [cpppo.dotdict(d) for d in [{
            'symbolic': 'SCADA_40001'
        }]]
    }
    req[0].read_tag = {}
    req[0].read_tag.elements = 1
    req[1].path = {
        'segment': [cpppo.dotdict(d) for d in [{
            'symbolic': 'SCADA_40002'
        }]]
    }
    req[1].read_tag = {}
    req[1].read_tag.elements = 1
    data.multiple.number = len(data.multiple.request)

    request = Obj.produce(data)

    req_bad = bytearray([
        0x0A,
        0x02,
        0x20,
        0x02,
        ord('$'),
        0x01,
        0x02,
        0x00,
        0x06,
        0x00,
        0x18,
        0x00,
        0x4C,
        0x07,
        0x91,
        0x0b,
        ord('S'),
        ord('C'),
        ord('A'),
        ord('D'),
        ord('A'),
        ord('_'),
        ord('4'),
        ord('0'),
        ord('0'),
        ord('0'),
        ord('1'),
        0x00,
        0x01,
        0x00,
        0x4C,
        0x07,
        0x91,
        0x0b,
        ord('S'),
        ord('C'),
        ord('A'),
        ord('D'),
        ord('A'),
        ord('_'),
        ord('4'),
        ord('0'),
        ord('0'),
        ord('0'),
        ord('2'),
        0x00,
        0x01,
        0x00,
    ])
    assert request == req_bad, \
        "Unexpected result from Multiple Request Service request for SCADA_40001/2; got: \n%r\nvs.\n%r " % ( request, req_bad )

    Obj.request(data)
    rpy_bad = bytearray([
        0x8A,
        0x00,
        0x00,
        0x00,
        0x02,
        0x00,
        0x06,
        0x00,
        0x0e,
        0x00,
        0xCC,
        0x00,
        0x00,
        0x00,
        0xC3,
        0x00,
        0x00,
        0x00,
        0xCC,
        0x00,
        0x05,
        0x01,  # Status code 0x05 (invalid path)
        0x00,
        0x00,
    ])
    assert data.input == rpy_bad, \
        "Unexpected reply from Multiple Request Service request for SCADA_40001/2; got: \n%r\nvs.\n%r " % ( data.input, rpy_bad )
Ejemplo n.º 8
0
    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())))
Ejemplo n.º 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()
Ejemplo n.º 10
0
def test_logix_multiple():
    """Test the Multiple Request Service.  Ensure multiple requests can be successfully handled, and
    invalid tags are correctly rejected.

    """
    size			= 1000
    Obj				= logix.Logix()
    Obj_a1 = Obj.attribute['1']	= enip.device.Attribute( 'parts',       enip.parser.DINT, default=[n for n in range( size )])
    Obj_a2 = Obj.attribute['2']	= enip.device.Attribute( 'ControlWord', enip.parser.DINT, default=[n for n in range( size )])
    Obj_a3 = Obj.attribute['3']	= enip.device.Attribute( 'SCADA_40001', enip.parser.INT,  default=[n for n in range( size )])

    assert len( Obj_a1 ) == size
    assert len( Obj_a2 ) == size
    Obj_a1[0]			= 42
    Obj_a2[0]			= 476

    # Set up a symbolic tag referencing the Logix Object's Attribute
    enip.device.symbol['parts']	= {'class': Obj.class_id, 'instance': Obj.instance_id, 'attribute':1 }
    enip.device.symbol['ControlWord'] \
				= {'class': Obj.class_id, 'instance': Obj.instance_id, 'attribute':2 }
    enip.device.symbol['SCADA_40001'] \
				= {'class': Obj.class_id, 'instance': Obj.instance_id, 'attribute':3 }

    # Test that we correctly compute beg,end,endactual for various Read Tag Fragmented scenarios,
    # with 2-byte and 4-byte types.  For the purposes of this test, we only look at path...elements.
    data			= cpppo.dotdict()
    data.service		= Obj.RD_FRG_RPY
    data.path			= { 'segment': [ cpppo.dotdict( d )
                                                 for d in [
                                                         {'element': 0 },
                                                       ]] }
    data.read_frag		= {}
    data.read_frag.elements	= 1000
    data.read_frag.offset	= 0
    
    # Reply maximum size limited
    beg,end,endactual		= Obj.reply_elements( Obj_a1, data, 'read_frag' )
    assert beg == 0 and end == 125 and endactual == 1000 # DINT == 4 bytes
    beg,end,endactual		= Obj.reply_elements( Obj_a3, data, 'read_frag' )
    assert beg == 0 and end == 250 and endactual == 1000 # INT == 2 bytes

    data.read_frag.offset	= 125*4 # OK, second request; begin after byte offset of first
    beg,end,endactual		= Obj.reply_elements( Obj_a1, data, 'read_frag' )
    assert beg == 125 and end == 250 and endactual == 1000 # DINT == 4 bytes

    # Request elements limited; 0 offset
    data.read_frag.elements	= 30
    data.read_frag.offset	= 0
    beg,end,endactual		= Obj.reply_elements( Obj_a3, data, 'read_frag' )
    assert beg == 0 and end == 30 and endactual == 30 # INT == 2 bytes

    # Request elements limited; +'ve offset
    data.read_frag.elements	= 70
    data.read_frag.offset	= 80
    beg,end,endactual		= Obj.reply_elements( Obj_a3, data, 'read_frag' )
    assert beg == 40 and end == 70 and endactual == 70 # INT == 2 bytes

    # Request limited by size of data provided (Write Tag [Fragmented])
    data			= cpppo.dotdict()
    data.service		= Obj.WR_FRG_RPY
    data.path			= { 'segment': [ cpppo.dotdict( d )
                                                 for d in [
                                                         {'element': 0 },
                                                       ]] }
    data.write_frag		= {}
    data.write_frag.data	= [0] * 100 # 100 elements provided in this request
    data.write_frag.elements	= 200       # Total request is to write 200 elements
    data.write_frag.offset	= 16        # request starts 16 bytes in (8 INTs)
    beg,end,endactual		= Obj.reply_elements( Obj_a3, data, 'write_frag' )
    assert beg == 8 and end == 108 and endactual == 200 # INT == 2 bytes

    # ... same, but lets say request started somewhere in the middle of the array
    data.path			= { 'segment': [ cpppo.dotdict( d )
                                                 for d in [
                                                         {'element': 222 },
                                                       ]] }
    beg,end,endactual		= Obj.reply_elements( Obj_a3, data, 'write_frag' )
    assert beg == 8+222 and end == 108+222 and endactual == 200+222 # INT == 2 bytes

    # Ensure correct computation of (beg,end] that are byte-offset and data/size limited
    data			= cpppo.dotdict()
    data.service		= Obj.WR_FRG_RPY
    data.path			= { 'segment': [] }

    data.write_frag		= {}
    data.write_frag.data	= [3,4,5,6]
    data.write_frag.offset	= 6
    beg,end,endactual		= Obj.reply_elements( Obj_a3, data, 'write_frag' )
    assert beg == 3 and end == 7 and endactual == 1000 # INT == 2 bytes

    # Trigger the error cases only accessible via write

    # Too many elements provided for attribute capacity
    data.write_frag.offset	= ( 1000 - 3 ) * 2
    try:
        beg,end,endactual	= Obj.reply_elements( Obj_a3, data, 'write_frag' )
        assert False, "Should have raised Exception due to capacity"
    except Exception as exc:
        assert "capacity exceeded" in str( exc )

    data			= cpppo.dotdict()
    data.service		= Obj.RD_FRG_RPY
    data.path			= { 'segment': [] }

    data.read_frag		= {}
    data.read_frag.offset	= 6
    beg,end,endactual		= Obj.reply_elements( Obj_a3, data, 'read_frag' )
    assert beg == 3 and end == 253 and endactual == 1000 # INT == 2 bytes

    # And we should be able to read with an offset right up to the last element
    data.read_frag.offset	= 1998
    beg,end,endactual		= Obj.reply_elements( Obj_a3, data, 'read_frag' )
    assert beg == 999 and end == 1000 and endactual == 1000 # INT == 2 bytes


    # Trigger all the remaining error cases

    # Unknown service
    data.service		= Obj.RD_FRG_REQ
    try:
        beg,end,endactual	= Obj.reply_elements( Obj_a3, data, 'read_frag' )
        assert False, "Should have raised Exception due to service"
    except Exception as exc:
        assert "unknown service" in str( exc )

    # Offset indivisible by element size
    data.service		= Obj.RD_FRG_RPY
    data.read_frag.offset	= 7
    try:
        beg,end,endactual	= Obj.reply_elements( Obj_a3, data, 'read_frag' )
        assert False, "Should have raised Exception due to odd byte offset"
    except Exception as exc:
        assert "element boundary" in str( exc )

    # Initial element outside bounds
    data.read_frag.offset	= 2000
    try:
        beg,end,endactual	= Obj.reply_elements( Obj_a3, data, 'read_frag' )
        assert False, "Should have raised Exception due to initial element"
    except Exception as exc:
        assert "initial element invalid" in str( exc )

    # Ending element outside bounds
    data.read_frag.offset	= 0
    data.read_frag.elements	= 1001
    try:
        beg,end,endactual	= Obj.reply_elements( Obj_a3, data, 'read_frag' )
        assert False, "Should have raised Exception due to ending element"
    except Exception as exc:
        assert "ending element invalid" in str( exc )

    # Beginning element after ending (should be no way to trigger).  This request doesn't specify an
    # element in the path, hence defaults to element 0, and asks for a number of elements == 2.
    # Thus, there is no 6-byte offset possible (a 2-byte offset is, though).
    data.read_frag.offset	= 6
    data.read_frag.elements	= 2
    try:
        beg,end,endactual	= Obj.reply_elements( Obj_a3, data, 'read_frag' )
        assert False, "Should have raised Exception due to ending element order"
    except Exception as exc:
        assert "ending element before beginning" in str( exc )
    data.read_frag.offset	= 2
    data.read_frag.elements	= 2
    beg,end,endactual		= Obj.reply_elements( Obj_a3, data, 'read_frag' )
    assert beg == 1 and end == 2 and endactual == 2 # INT == 2 bytes


    # Test an example valid multiple request
    data			= cpppo.dotdict()
    data.multiple		= {}
    data.multiple.request	= [ cpppo.dotdict(), cpppo.dotdict() ]
    req				= data.multiple.request

    req[0].path			= { 'segment': [ cpppo.dotdict( d )
                                                 for d in [{'symbolic': 'parts'}]] }
    req[0].read_tag		= {}
    req[0].read_tag.elements	= 1
    
    req[1].path			= { 'segment': [ cpppo.dotdict( d )
                                                 for d in [{'symbolic': 'ControlWord'}]] }
    req[1].read_tag		= {}
    req[1].read_tag.elements	= 1

    request			= Obj.produce( data )

    req_1			= bytes(bytearray([
        0x0A,
        0x02,
        0x20, 0x02, 0x24, 0x01,
        
        0x02, 0x00,
        
        0x06, 0x00,
        0x12, 0x00,
        
        0x4C,
        0x04, 0x91, 0x05, 0x70, 0x61,
        0x72, 0x74, 0x73, 0x00,
        0x01, 0x00,
        
        0x4C,
        0x07, 0x91, 0x0B, 0x43, 0x6F,
        0x6E, 0x74, 0x72, 0x6F, 0x6C,
        0x57, 0x6F, 0x72, 0x64, 0x00,
        0x01, 0x00,
    ]))
                       
    assert request == req_1, \
        "Unexpected result from Multiple Request Service; got: \n%r\nvs.\n%r " % ( request, req_1 )

    # Now, use the Message_Router's parser
    source			= cpppo.rememberable( request )
    data			= cpppo.dotdict()
    with Obj.parser as machine:
        for i,(m,s) in enumerate( machine.run( source=source, data=data )):
            pass
    log.normal( "Multiple Request: %s", enip.enip_format( data ))
    assert 'multiple' in data, \
        "No parsed multiple found in data: %s" % enip.enip_format( data )
    assert data.service == device.Message_Router.MULTIPLE_REQ, \
        "Expected a Multiple Request Service request: %s" % enip.enip_format( data )
    assert data.multiple.number == 2, \
        "Expected 2 requests in request.multiple: %s" % enip.enip_format( data )

    # And ensure if we re-encode the parsed result, we get the original encoded request back
    assert Obj.produce( data ) == req_1

    # Process the request into a reply.
    Obj.request( data )
    log.normal( "Multiple Response: %s", enip.enip_format( data ))
    assert data.service == device.Message_Router.MULTIPLE_RPY, \
        "Expected a Multiple Request Service reply: %s" % enip.enip_format( data )

    rpy_1			= bytearray([
        0x8A,
        0x00,
        0x00,
        0x00,

        0x02, 0x00,

        0x06, 0x00,
        0x10, 0x00,

        0xCC, 0x00, 0x00, 0x00,
        0xC4, 0x00,
        0x2A, 0x00, 0x00, 0x00,

        0xCC, 0x00, 0x00, 0x00,
        0xC4, 0x00,
        0xDC, 0x01, 0x00, 0x00,
    ])

    assert data.input == rpy_1, \
        "Unexpected reply from Multiple Request Service request; got: \n%r\nvs.\n%r " % ( data.input, rpy_1 )

    # Now lets try some valid and invalid requests
    data			= cpppo.dotdict()
    data.multiple		= {}
    data.multiple.request = req	= [ cpppo.dotdict() ]
    req[0].path			= { 'segment': [ cpppo.dotdict( d )
                                                 for d in [{'symbolic': 'SCADA_40001'}]] }
    req[0].read_tag		= {}
    req[0].read_tag.elements	= 1
    data.multiple.number	= len( data.multiple.request )

    request			= Obj.produce( data )

    req_good			= bytearray([
        0x0A,
        0x02,
        0x20, 0x02, ord('$'), 0x01,
        
        0x01, 0x00,
        
        0x04, 0x00,
        
        0x4C,
        0x07, 0x91, 0x0b, ord('S'), ord('C'),
        ord('A'), ord('D'), ord('A'), ord('_'), ord('4'),
        ord('0'), ord('0'), ord('0'), ord('1'), 0x00,
        0x01, 0x00,
    ])
    assert request == req_good, \
        "Unexpected result from Multiple Request Service request for SCADA_40001; got: \n%r\nvs.\n%r " % ( request, req_good )

    Obj.request( data )
    rpy_good			= bytearray([
        0x8A,
        0x00,
        0x00,
        0x00,

        0x01, 0x00,

        0x04, 0x00,

        0xCC, 0x00, 0x00, 0x00,
        0xC3, 0x00,
        0x00, 0x00,
    ])

    assert data.input == rpy_good, \
        "Unexpected reply from Multiple Request Service request for SCADA_40001; got: \n%r\nvs.\n%r " % ( data.input, rpy_good )

    # Add an invalid request
    data			= cpppo.dotdict()
    data.multiple		= {}
    data.multiple.request = req	= [ cpppo.dotdict(), cpppo.dotdict() ]
    req[0].path			= { 'segment': [ cpppo.dotdict( d )
                                                 for d in [{'symbolic': 'SCADA_40001'}]] }
    req[0].read_tag		= {}
    req[0].read_tag.elements	= 1
    req[1].path			= { 'segment': [ cpppo.dotdict( d )
                                                 for d in [{'symbolic': 'SCADA_40002'}]] }
    req[1].read_tag		= {}
    req[1].read_tag.elements	= 1
    data.multiple.number	= len( data.multiple.request )

    request			= Obj.produce( data )

    req_bad			= bytearray([
        0x0A,
        0x02,
        0x20, 0x02, ord('$'), 0x01,
        
        0x02, 0x00,
        
        0x06, 0x00,
        0x18, 0x00,
        
        0x4C,
        0x07, 0x91, 0x0b, ord('S'), ord('C'),
        ord('A'), ord('D'), ord('A'), ord('_'), ord('4'),
        ord('0'), ord('0'), ord('0'), ord('1'), 0x00,
        0x01, 0x00,

        0x4C,
        0x07, 0x91, 0x0b, ord('S'), ord('C'),
        ord('A'), ord('D'), ord('A'), ord('_'), ord('4'),
        ord('0'), ord('0'), ord('0'), ord('2'), 0x00,
        0x01, 0x00,
    ])
    assert request == req_bad, \
        "Unexpected result from Multiple Request Service request for SCADA_40001/2; got: \n%r\nvs.\n%r " % ( request, req_bad )

    Obj.request( data )
    rpy_bad			= bytearray([
        0x8A,
        0x00,
        0x00,
        0x00,

        0x02, 0x00,

        0x06, 0x00,
        0x0e, 0x00,

        0xCC, 0x00, 0x00, 0x00,
        0xC3, 0x00,
        0x00, 0x00,

        0xCC, 0x00, 0x05, 0x01, # Status code 0x05 (invalid path)
        0x00, 0x00,
    ])
    assert data.input == rpy_bad, \
        "Unexpected reply from Multiple Request Service request for SCADA_40001/2; got: \n%r\nvs.\n%r " % ( data.input, rpy_bad )
Ejemplo n.º 11
0
    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())))
Ejemplo n.º 12
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()
Ejemplo n.º 13
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()
Ejemplo n.º 14
0
def process( addr, data, **kwds ):
    """Processes an incoming parsed EtherNet/IP encapsulated request in data.request.enip.input, and
    produces a response with a prepared encapsulated reply, in data.response.enip.input, ready for
    re-encapsulation and transmission as a response.

    Returns True while session lives, False when the session is cleanly terminated.  Raises an
    exception when a fatal protocol processing error occurs, and the session should be terminated
    forcefully.

    When a connection is closed, a final invocation with 

    This roughly corresponds to the CIP Connection "client" object functionality.  We parse the raw
    EtherNet/IP encapsulation to get something like this Register request, in data.request:

        "enip.command": 101, 
        "enip.input": "array('c', '\\x01\\x00\\x00\\x00')",
        "enip.length": 4, 
        "enip.options": 0, 
        "enip.session_handle": 0, 
        "enip.status": 0
        "enip.length": 4


    This is parsed by the Connection Manager:

        "enip.CIP.register.options": 0, 
        "enip.CIP.register.protocol_version": 1, 

    Other requests such as:

        "enip.command": 111, 
        "enip.input": "array('c', '\\x00\\x00\\x00\\x00\\x05\\x00\\x02\\x00\\x00\\x00\\x00\\x00\\xb2\\x00\\x06\\x00\\x01\\x02 f$\\x01')", 
        "enip.length": 22, 
        "enip.options": 0, 
        "enip.sender_context.input": "array('c', '\\x01\\x00\\x00\\x00\\x00\\x00\\x00\\x00')", 
        "enip.session_handle": 285351425, 
        "enip.status": 0

    are parsed by the Connection Manager, and contain CPF entries requiring further processing by the Unconnected Message
    Manager (UCMM):

        "enip.CIP.send_data.CPF.count": 2, 
        "enip.CIP.send_data.CPF.item[0].length": 0, 
        "enip.CIP.send_data.CPF.item[0].type_id": 0, 
        "enip.CIP.send_data.CPF.item[1].length": 6, 
        "enip.CIP.send_data.CPF.item[1].type_id": 178, 
        "enip.CIP.send_data.CPF.item[1].unconnected_send.request_path.segment[0].class": 102, 
        "enip.CIP.send_data.CPF.item[1].unconnected_send.request_path.segment[1].instance": 1, 
        "enip.CIP.send_data.CPF.item[1].unconnected_send.request_path.size": 2, 
        "enip.CIP.send_data.CPF.item[1].unconnected_send.service": 1, 
        "enip.CIP.send_data.interface": 0, 
        "enip.CIP.send_data.timeout": 5,



    """
    ucmm			= setup()

    # If tags are specified, check that we've got them all set up right.  If the tag doesn't exist,
    # add it.  If it's error code doesn't match, change it.
    if 'tags' in kwds:
        for key,val in dict( kwds['tags'] ).items():
            log.detail( "EtherNet/IP CIP Request  (Client %16s): setup tag: %r", addr, (key, val) )
            if not resolve_tag( key ):
                # A new tag!  Allocate a new attribute ID in the Logix (Message Router).
                cls, ins	= 0x02, 1 # The Logix Message Router
                Lx		= lookup( cls, ins )
                att		= len( Lx.attribute )
                while ( str( att ) in Lx.attribute ):
                    att	       += 1
                log.normal( "%24s.%s Attribute %3d added", Lx, val.attribute, att )
                Lx.attribute[str(att)]= val.attribute
                redirect_tag( key, {'class': cls, 'instance': ins, 'attribute': att })
            # Attribute (now) exists; find it, and make sure its error code is right.
            attribute		= lookup( *resolve_tag( key ))
            assert attribute is not None, "Failed to find existing tag: %r" % key
            if 'error' in val and attribute.error != val.error:
                log.warning( "Attribute %s error code changed: 0x%02x", attribute, val.error )
                attribute.error	= val.error


    source			= cpppo.rememberable()
    try:
        # Find the Connection Manager, and use it to parse the encapsulated EtherNet/IP request.  We
        # pass an additional request.addr, to allow the Connection Manager to identify the
        # connection, in the case where the connection is closed spontaneously (no request, no
        # request.enip.session_handle).
        data['request.addr']	= addr	  		# data.request may not exist, or be empty

        if 'enip' in data.request:
            source.chain( data.request.enip.input )
            with ucmm.parser as machine:
                for i,(m,s) in enumerate( machine.run( path='request.enip', source=source, data=data )):
                    #log.detail( "%s #%3d -> %10.10s; next byte %3d: %-10.10r: %s",
                    #            machine.name_centered(), i, s, source.sent, source.peek(),
                    #            repr( data ) if log.getEffectiveLevel() < logging.DETAIL else reprlib.repr( data ))
                    pass
        if log.isEnabledFor( logging.DETAIL ):
            log.detail( "EtherNet/IP CIP Request  (Client %16s): %s", addr, enip_format( data.request ))

        # Create a data.response with a structural copy of the request.enip.header.  This means that
        # the dictionary structure is new (we won't alter the request.enip... when we add entries in
        # the resonse...), but the actual mutable values (eg. bytearray ) are copied.  If we need
        # to change any values, replace them with new values instead of altering them!
        data.response		= cpppo.dotdict( data.request )

        # Let the Connection Manager process the (copied) request in response.enip, producing the
        # appropriate data.response.enip.input encapsulated EtherNet/IP message to return, along
        # with other response.enip... values (eg. .session_handle for a new Register Session).  The
        # enip.status should normally be 0x00; the encapsulated response will contain appropriate
        # error indications if the encapsulated request failed.
        
        proceed			= ucmm.request( data.response )
        if log.isEnabledFor( logging.DETAIL ):
            log.detail( "EtherNet/IP CIP Response (Client %16s): %s", addr, enip_format( data.response ))

        return proceed
    except:
        # Parsing failure.  We're done.  Suck out some remaining input to give us some context.
        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)" % (
            processed, repr(memory+future), '-' * (len(repr(memory))-1) + '^', pos )
        log.error( "EtherNet/IP CIP error %s\n", where )
        raise