Esempio n. 1
0
 def add_nodes_to_subscription(self, nodeIds):
     """
     Subscribe to a list of string of NodeIds in the OPC-UA format:
     - "i=42" for an integer NodeId,
     - "s=Foobar" for a string NodeId,
     - "g=C496578A-0DFE-4b8f-870A-745238C6AEAE" for a GUID-NodeId,
     - "b=Barbar" for a ByteString.
     The string can be prepend by a "ns={};" which specifies the namespace index.
     This call is always synchroneous, so that the Toolkit waits for the server response to return.
     """
     # TODO: check format?
     if nodeIds:
         n = len(nodeIds)
         lszNodeIds = [ffi.new('char[]', nid.encode()) for nid in nodeIds]
         lAttrIds = ffi.new(
             'SOPC_LibSub_AttributeId[{}]'.format(n),
             [libsub.SOPC_LibSub_AttributeId_Value for _ in nodeIds])
         lDataIds = ffi.new('SOPC_LibSub_DataId[]', n)
         status = libsub.SOPC_LibSub_AddToSubscription(
             self._id, lszNodeIds, lAttrIds, n, lDataIds)
         assert status == ReturnStatus.OK, 'Add to subscription failed with status {}'.format(
             status)
         for i, nid in zip(lDataIds, nodeIds):
             assert i not in self._dSubscription, 'data_id returned by Toolkit is already associated to a NodeId.'
             self._dSubscription[i] = nid
Esempio n. 2
0
    def add_configuration_unsecured(*,
                                    server_url='opc.tcp://localhost:4841',
                                    publish_period=500,
                                    n_max_keepalive=3,
                                    n_max_lifetime=10,
                                    timeout_ms=10000,
                                    sc_lifetime=3600000,
                                    token_target=3):
        """
        Returns a configuration that can be later used in `PyS2OPC_Client.connect` or `PyS2OPC_Client.get_endpoints`.

        Args:
            server_url: The endpoint and server url to connect to.
            publish_period: The period of the subscription, in ms.
            n_max_keepalive: The number of times the subscription has no notification to send before
                             sending an empty `PublishResponse` (the KeepAlive message). It is necessary
                             to keep `n_max_keepalive*timeout_ms*token_target < REQUEST_TIMEOUT (5000ms)`.
            n_max_lifetime: The maximum number of times a subscription has notifications to send
                            but no available token. In this case, the subscription is destroyed.
            timeout_ms: The `PyS2OPC_Client.connect` timeout, in ms.
            sc_lifetime: The target lifetime of the secure channel, before renewal, in ms.
            token_target: The number of subscription tokens (PublishRequest) that should be
                          made available to the server at anytime.
        """
        assert PyS2OPC._initialized_cli and not PyS2OPC._configured,\
            'Toolkit is not initialized or already configured, cannot add new configurations.'

        pCfgId = ffi.new('SOPC_LibSub_ConfigurationId *')
        dConnectionParameters = {
            'server_url': ffi.new('char[]', server_url.encode()),
            'security_policy': SecurityPolicy.PolicyNone,
            'security_mode': SecurityMode.ModeNone,
            'disable_certificate_verification': True,
            'path_cert_auth': NULL,
            'path_cert_srv': NULL,
            'path_cert_cli': NULL,
            'path_key_cli': NULL,
            'path_crl': NULL,
            'policyId': ffi.new('char[]', b"anonymous"),
            'username': NULL,
            'password': NULL,
            'publish_period_ms': publish_period,
            'n_max_keepalive': n_max_keepalive,
            'n_max_lifetime': n_max_lifetime,
            'data_change_callback': libsub._callback_datachanged,
            'timeout_ms': timeout_ms,
            'sc_lifetime': sc_lifetime,
            'token_target': token_target,
            'generic_response_callback': libsub._callback_client_event
        }
        status = libsub.SOPC_LibSub_ConfigureConnection(
            [dConnectionParameters], pCfgId)
        assert status == ReturnStatus.OK, 'Configuration failed with status {}.'.format(
            ReturnStatus.get_both_from_id(status))

        cfgId = pCfgId[0]
        config = ClientConfiguration(cfgId, dConnectionParameters)
        PyS2OPC_Client._dConfigurations[cfgId] = config
        return config
Esempio n. 3
0
    def connect(configuration, ConnectionHandlerClass):
        """
        Connects to the server with the given `configuration`.
        The configuration is returned by a call to add_configuration_unsecured().
        The ConnectionHandlerClass is a class that must be inherited from BaseClientConnectionHandler,
        and that at least overrides the callbacks.
        It will be instantiated and the instance is returned.

        It can be optionally used in a `with` statement, which automatically disconnects the connection.
        """
        assert PyS2OPC._initialized_cli and PyS2OPC._configured, 'Toolkit not configured, cannot connect().'
        assert isinstance(configuration, ClientConfiguration)
        cfgId = configuration._id
        assert cfgId in PyS2OPC_Client._dConfigurations, 'Unknown configuration, see add_configuration_unsecured().'
        assert issubclass(ConnectionHandlerClass, BaseClientConnectionHandler)

        pConId = ffi.new('SOPC_LibSub_DataId *')
        status = libsub.SOPC_LibSub_Connect(cfgId, pConId)
        if status != ReturnStatus.OK:
            raise ConnectionError(
                'Could not connect to the server with the given configuration with status {}.'
                .format(ReturnStatus.get_name_from_id(status)))

        connectionId = pConId[0]
        assert connectionId not in PyS2OPC_Client._dConnections,\
            'Subscription library returned a connection id that is already taken by an active connection.'

        connection = ConnectionHandlerClass(connectionId, configuration)
        PyS2OPC_Client._dConnections[connectionId] = connection
        return connection
Esempio n. 4
0
    def initialize(logLevel=0, logPath='logs/', logFileMaxBytes=1048576, logMaxFileNumber=50):
        """
        Toolkit and LibSub initializations.
        Must be called exactly once per process.

        Args:
            logLevel: log level (0: error, 1: warning, 2: info, 3: debug)
            logPath: the path for logs (the current working directory) to logPath.
                     logPath is created if it does not exist.
            logFileMAxBytes: The maximum size (best effort) of the log files, before changing the log index.
            logMaxFileNumber: The maximum number of log indexes before cycling logs and reusing the first log.

        This function supports the context management:
        >>> with PyS2OPC.initialize():
        ...     # Do things here
        ...     pass

        When reaching out of the `with` statement, the Toolkit is automatically cleared.
        See clear().
        """
        assert not PyS2OPC._initialized, 'Library is alread initialized'

        status = libsub.SOPC_LibSub_Initialize([(libsub._callback_log,
                                                 libsub._callback_disconnected,
                                                 (logLevel,
                                                  ffi.new('char[]', logPath.encode()),
                                                  logFileMaxBytes,
                                                  logMaxFileNumber))])
        assert status == ReturnStatus.OK, 'Library initialization failed with status code {}.'.format(status)
        PyS2OPC._initialized = True

        try:
            yield
        finally:
            PyS2OPC.clear()
Esempio n. 5
0
    def add_nodes_to_subscription(self, nodeIds):
        """
        Subscribe to a list of string of NodeIds in the OPC-UA format (see `pys2opc` module documentation).
        This call is always synchroneous, so that the Toolkit waits for the server response to return.

        The callback `pys2opc.connection.BaseClientConnectionHandler.on_datachanged` will be called once for each new value of the nodes.
        In particular, the callback is at least called once for the initial value.
        """
        # TODO: check format?
        if nodeIds:
            n = len(nodeIds)
            lszNodeIds = [ffi.new('char[]', nid.encode()) for nid in nodeIds]
            lAttrIds = ffi.new(
                'SOPC_LibSub_AttributeId[{}]'.format(n),
                [libsub.SOPC_LibSub_AttributeId_Value for _ in nodeIds])
            lDataIds = ffi.new('SOPC_LibSub_DataId[]', n)
            status = libsub.SOPC_LibSub_AddToSubscription(
                self._id, lszNodeIds, lAttrIds, n, lDataIds)
            assert status == ReturnStatus.OK, 'Add to subscription failed with status {}'.format(
                status)
            for i, nid in zip(lDataIds, nodeIds):
                assert i not in self._dSubscription, 'data_id returned by Toolkit is already associated to a NodeId.'
                self._dSubscription[i] = nid
Esempio n. 6
0
    def initialize(logLevel=LogLevel.Debug,
                   logPath='logs/',
                   logFileMaxBytes=1048576,
                   logMaxFileNumber=50):
        """
        Toolkit initialization for Server.
        When the toolkit is initialized for servers, it cannot be used to make a server before a call to `PyS2OPC_Server.clear`.

        This function supports the context management:
        >>> with PyS2OPC_Server.initialize():
        ...     # Do things here, namely configure then wait
        ...     pass

        When reaching out of the `with` statement, the Toolkit is automatically cleared.
        See `PyS2OPC_Server.clear`.

        Args:
            logLevel: log level (0: error, 1: warning, 2: info, 3: debug)
            logPath: the path for logs (the current working directory) to logPath.
                     logPath is created if it does not exist.
            logFileMAxBytes: The maximum size (best effort) of the log files, before changing the log index.
            logMaxFileNumber: The maximum number of log indexes before cycling logs and reusing the first log.

        """
        PyS2OPC._assert_not_init()

        logConfig = libsub.SOPC_Common_GetDefaultLogConfiguration()
        logConfig.logLevel = logLevel
        # Note: we don't keep a copy of logDirPath as the string content copied internally in SOPC_Log_CreateInstance
        logConfig.logSysConfig.fileSystemLogConfig.logDirPath = ffi.new(
            'char[]', logPath.encode())
        logConfig.logSysConfig.fileSystemLogConfig.logMaxBytes = logFileMaxBytes
        logConfig.logSysConfig.fileSystemLogConfig.logMaxFiles = logMaxFileNumber

        status = libsub.SOPC_Common_Initialize(logConfig)
        assert status == ReturnStatus.OK, 'Common initialization failed with status {}'.format(
            ReturnStatus.get_both_from_id(status))
        status = libsub.SOPC_Toolkit_Initialize(libsub._callback_toolkit_event)
        assert status == ReturnStatus.OK, 'Toolkit initialization failed with status {}.'.format(
            ReturnStatus.get_both_from_id(status))
        PyS2OPC._initialized_srv = True

        try:
            yield
        finally:
            PyS2OPC_Server.clear()
Esempio n. 7
0
    def initialize(logLevel=LogLevel.Debug,
                   logPath='logs/',
                   logFileMaxBytes=1048576,
                   logMaxFileNumber=50):
        """
        Toolkit and LibSub initializations for Clients.
        When the toolkit is initialized for clients, it cannot be used to make a server before a `clear()`.

        This function supports the context management:
        >>> with PyS2OPC_Client.initialize():
        ...     # Do things here
        ...     pass

        When reaching out of the `with` statement, the Toolkit is automatically cleared.
        See `clear()`.

        Args:
            logLevel: log level for the toolkit logs (one of the `pys2opc.types.LogLevel` values).
            logPath: the path for logs (the current working directory) to logPath.
                     logPath is created if it does not exist.
            logFileMAxBytes: The maximum size (best effort) of the log files, before changing the log index.
            logMaxFileNumber: The maximum number of log indexes before cycling logs and reusing the first log.

        """
        PyS2OPC._assert_not_init()

        status = libsub.SOPC_LibSub_Initialize([
            (libsub._callback_log, libsub._callback_disconnected,
             (logLevel, ffi.new('char[]', logPath.encode()), logFileMaxBytes,
              logMaxFileNumber))
        ])
        assert status == ReturnStatus.OK, 'Library initialization failed with status {}.'.format(
            ReturnStatus.get_both_from_id(status))
        PyS2OPC._initialized_cli = True

        try:
            yield
        finally:
            PyS2OPC_Client.clear()
Esempio n. 8
0
    def load_configuration(xml_path,
                           address_space_handler=None,
                           user_handler=None,
                           method_handler=None,
                           pki_handler=None):
        """
        Creates a configuration structure for a server from an XML file.
        This configuration is later used to open an endpoint.
        There should be only one created configuration.

        The XML configuration format is specific to S2OPC and follows the s2opc_config.xsd scheme.

        Optionally configure the callbacks of the server.
        If handlers are left None, the following default behaviors are used:

        - address space: no notification of address space events,
        - user authentications and authorizations: allow all user and all operations,
        - methods: no callable methods,
        - pki: the default secure Public Key Infrastructure,
          which thoroughly checks the validity of certificates based on trusted issuers, untrusted issuers, and issued certificates.

        This function must be called after `PyS2OPC_Server.initialize`, and before `PyS2OPC_Server.mark_configured`.
        It must be called at most once.

        Note: limitation: for now, changes in user authentications and authorizations, methods, and pki, are not supported.

        Args:
            xml_path: Path to the configuration in the s2opc_config.xsd format
            address_space_handler: None (no notification) or an instance of a subclass of
                                   `pys2opc.server_callbacks.BaseAddressSpaceHandler`
            user_handler: None (authenticate all user and authorize all operations)
            method_handler: None (no method available)
            pki_handler: None (certificate authentications based on certificate authorities)
        """
        assert PyS2OPC._initialized_srv and not PyS2OPC._configured and PyS2OPC_Server._config is None,\
            'Toolkit is either not initialized, initialized as a Client, or already configured.'

        assert user_handler is None, 'Custom User Manager not implemented yet'
        assert method_handler is None, 'Custom Method Manager not implemented yet'
        assert pki_handler is None, 'Custom PKI Manager not implemented yet'
        if address_space_handler is not None:
            assert isinstance(address_space_handler, BaseAddressSpaceHandler)

        # Note: if part of the configuration fails, this leaves the toolkit in an half-configured configuration.
        # In this case, a Clear is required before trying to configure it again.

        # Creates the configuration
        config = ffi.new('SOPC_S2OPC_Config *')
        with open(xml_path, 'r') as fd:
            assert libsub.SOPC_Config_Parse(fd, config)

        # Finish the configuration by setting the manual fields: server certificate and key, create the pki,
        #  the user auth* managers, and the method call manager
        # If any of them fails, we must still clear the config!
        try:
            # Cryptography
            serverCfg = config.serverConfig
            if serverCfg.serverCertPath != NULL or serverCfg.serverKeyPath != NULL:
                assert serverCfg.serverCertPath != NULL and serverCfg.serverKeyPath != NULL,\
                    'The server private key and server certificate work by pair. Either configure them both of them or none of them.'
                ppCert = ffi.addressof(serverCfg, 'serverCertificate')
                status = libsub.SOPC_KeyManager_SerializedCertificate_CreateFromFile(
                    serverCfg.serverCertPath, ppCert)
                assert status == ReturnStatus.OK,\
                    'Cannot load server certificate file {} with status {}. Is path correct?'\
                    .format(ffi.string(serverCfg.serverCertPath), ReturnStatus.get_both_from_id(status))

                ppKey = ffi.addressof(serverCfg, 'serverKey')
                status = libsub.SOPC_KeyManager_SerializedAsymmetricKey_CreateFromFile(
                    serverCfg.serverKeyPath, ppKey)
                assert status == ReturnStatus.OK,\
                    'Cannot load secret key file {} with status {}. Is path correct?'\
                    .format(ffi.string(serverCfg.serverKeyPath), ReturnStatus.get_both_from_id(status))

            # PKI is not required if no CA is configured
            if (serverCfg.trustedRootIssuersList != NULL and serverCfg.trustedRootIssuersList[0] != NULL) or\
               (serverCfg.issuedCertificatesList != NULL and serverCfg.issuedCertificatesList[0] != NULL):
                ppPki = ffi.addressof(serverCfg, 'pki')
                status = libsub.SOPC_PKIProviderStack_CreateFromPaths(
                    serverCfg.trustedRootIssuersList,
                    serverCfg.trustedIntermediateIssuersList,
                    serverCfg.untrustedRootIssuersList,
                    serverCfg.untrustedIntermediateIssuersList,
                    serverCfg.issuedCertificatesList,
                    serverCfg.certificateRevocationPathList, ppPki)

            # Methods
            serverCfg.mcm  # Leave NULL

            # Endpoints have the user management
            for i in range(serverCfg.nbEndpoints):
                endpoint = serverCfg.endpoints[i]
                # By default, creates user managers that accept all users and allow all operations
                endpoint.authenticationManager = libsub.SOPC_UserAuthentication_CreateManager_AllowAll(
                )
                endpoint.authorizationManager = libsub.SOPC_UserAuthorization_CreateManager_AllowAll(
                )
                assert endpoint.authenticationManager != NULL and endpoint.authorizationManager != NULL

                # Register endpoint
                epConfigIdx = libsub.SOPC_ToolkitServer_AddEndpointConfig(
                    ffi.addressof(endpoint))
                assert epConfigIdx,\
                    'Cannot add endpoint configuration. There may be no more endpoint left, or the configuration parameters are incorrect.'
                assert epConfigIdx not in PyS2OPC_Server._dEpIdx,\
                    'Internal failure, epConfigIdx already reserved by another configuration.'
                PyS2OPC_Server._dEpIdx[epConfigIdx] = endpoint
        except:
            libsub.SOPC_S2OPC_Config_Clear(config)
            config = None
            raise
        PyS2OPC_Server._config = config

        # Set address space handler
        if address_space_handler is not None:
            PyS2OPC_Server._adds_handler = address_space_handler
            # Note: SetAddressSpaceNotifCb cannot be called twice, or with NULL
            assert libsub.SOPC_ToolkitServer_SetAddressSpaceNotifCb(
                libsub._callback_address_space_event) == ReturnStatus.OK
Esempio n. 9
0
    def add_configuration_secured(
            *,
            server_url='opc.tcp://localhost:4841',
            publish_period=500,
            n_max_keepalive=3,
            n_max_lifetime=10,
            timeout_ms=10000,
            sc_lifetime=3600000,
            token_target=3,
            security_mode=SecurityMode.Sign,
            security_policy=SecurityPolicy.Basic256,
            path_cert_auth='../../../build/bin/trusted/cacert.der',
            path_crl='../../../build/bin/revoked/cacrl.der',
            path_cert_srv='../../../build/bin/server_public/server_2k_cert.der',
            path_cert_cli='../../../build/bin/client_public/client_2k_cert.der',
            path_key_cli='../../../build/bin/client_private/client_2k_key.pem'
    ):
        """
        Returns a configuration that can be later used in `PyS2OPC_Client.connect` or `PyS2OPC_Client.get_endpoints`.

        Args:
            server_url: The endpoint and server url to connect to.
            publish_period: The period of the subscription, in ms.
            n_max_keepalive: The number of times the subscription has no notification to send before
                             sending an empty PublishResponse (the KeepAlive message). It is necessary
                             to keep `n_max_keepalive*timeout_ms*token_target < 5000ms`.
            n_max_lifetime: The maximum number of times a subscription has notifications to send
                            but no available token. In this case, the subscription is destroyed.
            timeout_ms: The `PyS2OPC_Client.connect` timeout, in ms.
            sc_lifetime: The target lifetime of the secure channel, before renewal, in ms.
            token_target: The number of subscription tokens (PublishRequest) that should be
                          made available to the server at anytime.
            security_mode: The configured security mode, one of the `pys2opc.types.SecurityMode` constants.
            security_policy: The configured security policy, one of the `pys2opc.types.SecurityPolicy` constants.
            path_cert_auth: The path to the certificate authority (in DER or PEM format).
            path_crl      : The path to the CertificateRevocationList of the certificate authority (mandatory)
            path_cert_srv: The path to the expected server certificate (in DER or PEM format).
                           It must be signed by the certificate authority.
            path_cert_cli: The path to the certificate of the client.
            path_key_cli: The path to the private key of the client certificate.
        """
        # TODO: factorize code with add_configuration_unsecured
        assert PyS2OPC._initialized_cli and not PyS2OPC._configured,\
            'Toolkit is not initialized or already configured, cannot add new configurations.'

        pCfgId = ffi.new('SOPC_LibSub_ConfigurationId *')
        dConnectionParameters = {
            'server_url': ffi.new('char[]', server_url.encode()),
            'security_policy': security_policy,
            'security_mode': security_mode,
            'disable_certificate_verification': False,
            'path_cert_auth': ffi.new('char[]', path_cert_auth.encode()),
            'path_crl': ffi.new('char[]', path_crl.encode()),
            'path_cert_srv': ffi.new('char[]', path_cert_srv.encode()),
            'path_cert_cli': ffi.new('char[]', path_cert_cli.encode()),
            'path_key_cli': ffi.new('char[]', path_key_cli.encode()),
            'policyId': ffi.new('char[]', b"anonymous"),
            'username': NULL,
            'password': NULL,
            'publish_period_ms': publish_period,
            'n_max_keepalive': n_max_keepalive,
            'n_max_lifetime': n_max_lifetime,
            'data_change_callback': libsub._callback_datachanged,
            'timeout_ms': timeout_ms,
            'sc_lifetime': sc_lifetime,
            'token_target': token_target,
            'generic_response_callback': libsub._callback_client_event
        }
        status = libsub.SOPC_LibSub_ConfigureConnection(
            [dConnectionParameters], pCfgId)
        assert status == ReturnStatus.OK, 'Configuration failed with status {}.'.format(
            ReturnStatus.get_both_from_id(status))

        cfgId = pCfgId[0]
        config = ClientConfiguration(cfgId, dConnectionParameters)
        PyS2OPC_Client._dConfigurations[cfgId] = config
        return config