def find_endpoint(endpoints, security_mode, policy_uri): """ Find endpoint with required security mode and policy URI """ _logger.info("find_endpoint %r %r %r", endpoints, security_mode, policy_uri) for ep in endpoints: if (ep.EndpointUrl.startswith(ua.OPC_TCP_SCHEME) and ep.SecurityMode == security_mode and ep.SecurityPolicyUri == policy_uri): return ep raise ua.UaError( f"No matching endpoints: {security_mode}, {policy_uri}")
def _process_received_message(self, msg: Union[ua.Message, ua.Acknowledge, ua.ErrorMessage]): if msg is None: pass elif isinstance(msg, ua.Message): self._call_callback(msg.request_id(), msg.body()) elif isinstance(msg, ua.Acknowledge): self._call_callback(0, msg) elif isinstance(msg, ua.ErrorMessage): self.logger.fatal("Received an error: %r", msg) self._call_callback(0, ua.UaStatusCodeError(msg.Error.value)) else: raise ua.UaError(f"Unsupported message type: {msg}")
async def enable_history_event(self, source, period=timedelta(days=7), count=0): """ Set attribute History Read of object events to True and start storing data for history """ event_notifier = await source.read_event_notifier() if ua.EventNotifier.SubscribeToEvents not in event_notifier: raise ua.UaError('Node does not generate events', event_notifier) if ua.EventNotifier.HistoryRead not in event_notifier: event_notifier.add(ua.EventNotifier.HistoryRead) await source.set_event_notifier(event_notifier) await self.history_manager.historize_event(source, period, count)
def __init__(self, server, nodeid): self.server = server self.nodeid = None if isinstance(nodeid, Node): self.nodeid = nodeid.nodeid elif isinstance(nodeid, ua.NodeId): self.nodeid = nodeid elif type(nodeid) in (str, bytes): self.nodeid = ua.NodeId.from_string(nodeid) elif isinstance(nodeid, int): self.nodeid = ua.NodeId(nodeid, 0) else: raise ua.UaError("argument to node must be a NodeId object or a string defining a nodeid found {0} of type {1}".format(nodeid, type(nodeid))) self.basenodeid = None
def _check_sym_header(self, security_hdr): """ Validates the symmetric header of the message chunk and revolves the security token if needed. """ assert isinstance( security_hdr, ua.SymmetricAlgorithmHeader ), "Expected SymAlgHeader, got: {0}".format(security_hdr) if security_hdr.TokenId == self.security_token.TokenId: return if security_hdr.TokenId == self.next_security_token.TokenId: self.revolve_tokens() return if self._allow_prev_token and security_hdr.TokenId == self.prev_security_token.TokenId: # From spec, part 4, section 5.5.2.1: Clients should accept Messages secured by an # expired SecurityToken for up to 25 % of the token lifetime. This should ensure that # Messages sent by the Server before the token expired are not rejected because of # network delays. timeout = self.prev_security_token.CreatedAt + timedelta( milliseconds=self.prev_security_token.RevisedLifetime * 1.25) if timeout < datetime.utcnow(): raise ua.UaError( "Security token id {} has timed out ({} < {})".format( security_hdr.TokenId, timeout, datetime.utcnow())) return expected_tokens = [ self.security_token.TokenId, self.next_security_token.TokenId ] if self._allow_prev_token: expected_tokens.insert(0, self.prev_security_token.TokenId) raise ua.UaError( "Invalid security token id {}, expected one of: {}".format( security_hdr.TokenId, expected_tokens))
async def historize_data_change(self, node, period=timedelta(days=7), count=0): """ Subscribe to the nodes' data changes and store the data in the active storage. """ if not self._sub: self._sub = await self._create_subscription( SubHandler(self.storage, self.iserver.loop)) if node in self._handlers: raise ua.UaError("Node {0} is already historized".format(node)) await self.storage.new_historized_node(node.nodeid, period, count) handler = await self._sub.subscribe_data_change(node) self._handlers[node] = handler
async def create_session(self): """ send a CreateSessionRequest to server with reasonable parameters. If you want o modify settings look at code of this methods and make your own """ desc = ua.ApplicationDescription() desc.ApplicationUri = self.application_uri desc.ProductUri = self.product_uri desc.ApplicationName = ua.LocalizedText(self.name) desc.ApplicationType = ua.ApplicationType.Client params = ua.CreateSessionParameters() # at least 32 random bytes for server to prove possession of private key (specs part 4, 5.6.2.2) nonce = create_nonce(32) params.ClientNonce = nonce params.ClientCertificate = self.security_policy.host_certificate params.ClientDescription = desc params.EndpointUrl = self.server_url.geturl() params.SessionName = f"{self.description} Session{self._session_counter}" # Requested maximum number of milliseconds that a Session should remain open without activity params.RequestedSessionTimeout = self.session_timeout params.MaxResponseMessageSize = 0 # means no max size response = await self.uaclient.create_session(params) if self.security_policy.host_certificate is None: data = nonce else: data = self.security_policy.host_certificate + nonce self.security_policy.asymmetric_cryptography.verify( data, response.ServerSignature.Signature) self._server_nonce = response.ServerNonce if not self.security_policy.peer_certificate: self.security_policy.peer_certificate = response.ServerCertificate elif self.security_policy.peer_certificate != response.ServerCertificate: raise ua.UaError("Server certificate mismatch") # remember PolicyId's: we will use them in activate_session() ep = Client.find_endpoint(response.ServerEndpoints, self.security_policy.Mode, self.security_policy.URI) self._policy_ids = ep.UserIdentityTokens # Actual maximum number of milliseconds that a Session shall remain open without activity if self.session_timeout != response.RevisedSessionTimeout: _logger.warning( "Requested session timeout to be %dms, got %dms instead", self.secure_channel_timeout, response.RevisedSessionTimeout) self.session_timeout = response.RevisedSessionTimeout self._renew_channel_task = self.loop.create_task( self._renew_channel_loop()) return response
def _call_callback(self, request_id, body): try: self._callbackmap[request_id].set_result(body) except KeyError: raise ua.UaError( f"No request found for request id: {request_id}, pending are {self._callbackmap.keys()}, body was {body}" ) except asyncio.InvalidStateError: if not self.closed: self.logger.warning("Future for request id %s is already done", request_id) return self.logger.debug( "Future for request id %s not handled due to disconnect", request_id) del self._callbackmap[request_id]
def __init__(self, security_policy, body=b'', msg_type=ua.MessageType.SecureMessage, chunk_type=ua.ChunkType.Single): self.MessageHeader = ua.Header(msg_type, chunk_type) if msg_type in (ua.MessageType.SecureMessage, ua.MessageType.SecureClose): self.SecurityHeader = ua.SymmetricAlgorithmHeader() elif msg_type == ua.MessageType.SecureOpen: self.SecurityHeader = ua.AsymmetricAlgorithmHeader() else: raise ua.UaError(f"Unsupported message type: {msg_type}") self.SequenceHeader = ua.SequenceHeader() self.Body = body self.security_policy = security_policy
async def get_base_data_type(datatype): """ Looks up the base datatype of the provided datatype Node The base datatype is either: A primitive type (ns=0, i<=21) or a complex one (ns=0 i>21 and i<=30) like Enum and Struct. Args: datatype: NodeId of a datype of a variable Returns: NodeId of datatype base or None in case base datype can not be determined """ base = datatype while base: if base.nodeid.NamespaceIndex == 0 and isinstance(base.nodeid.Identifier, int) and base.nodeid.Identifier <= 30: return base base = await get_node_supertype(base) raise ua.UaError("Datatype must be a subtype of builtin types {0!s}".format(datatype))
def _receive(self, msg): self._check_incoming_chunk(msg) self._incoming_parts.append(msg) if msg.MessageHeader.ChunkType == ua.ChunkType.Intermediate: return None if msg.MessageHeader.ChunkType == ua.ChunkType.Abort: err = struct_from_binary(ua.ErrorMessage, ua.utils.Buffer(msg.Body)) logger.warning("Message %s aborted: %s", msg, err) # specs Part 6, 6.7.3 say that aborted message shall be ignored # and SecureChannel should not be closed self._incoming_parts = [] return None if msg.MessageHeader.ChunkType == ua.ChunkType.Single: message = ua.Message(self._incoming_parts) self._incoming_parts = [] return message raise ua.UaError("Unsupported chunk type: {0}".format(msg))
async def set_security_string(self, string: str): """ Set SecureConnection mode. String format: Policy,Mode,certificate,private_key[,server_private_key] where Policy is Basic128Rsa15, Basic256 or Basic256Sha256, Mode is Sign or SignAndEncrypt certificate, private_key and server_private_key are paths to .pem or .der files Call this before connect() """ if not string: return parts = string.split(",") if len(parts) < 4: raise ua.UaError("Wrong format: `{}`, expected at least 4 comma-separated values".format(string)) policy_class = getattr(security_policies, "SecurityPolicy{}".format(parts[0])) mode = getattr(ua.MessageSecurityMode, parts[1]) return await self.set_security(policy_class, parts[2], parts[3], parts[4] if len(parts) >= 5 else None, mode)
def receive_from_header_and_body(self, header, body): """ Convert MessageHeader and binary body to OPC UA TCP message (see OPC UA specs Part 6, 7.1: Hello, Acknowledge or ErrorMessage), or a Message object, or None (if intermediate chunk is received) """ if header.MessageType == ua.MessageType.SecureOpen: data = body.copy(header.body_size) security_header = struct_from_binary(ua.AsymmetricAlgorithmHeader, data) if not self.is_open(): # Only call select_policy if the channel isn't open. Otherwise # it will break the Secure channel renewal. self.select_policy(security_header.SecurityPolicyURI, security_header.SenderCertificate) elif header.MessageType in (ua.MessageType.SecureMessage, ua.MessageType.SecureClose): data = body.copy(header.body_size) security_header = struct_from_binary(ua.SymmetricAlgorithmHeader, data) self._check_sym_header(security_header) if header.MessageType in (ua.MessageType.SecureMessage, ua.MessageType.SecureOpen, ua.MessageType.SecureClose): chunk = MessageChunk.from_header_and_body(self.security_policy, header, body) return self._receive(chunk) if header.MessageType == ua.MessageType.Hello: msg = struct_from_binary(ua.Hello, body) self._max_chunk_size = msg.ReceiveBufferSize return msg if header.MessageType == ua.MessageType.Acknowledge: msg = struct_from_binary(ua.Acknowledge, body) self._max_chunk_size = msg.SendBufferSize return msg if header.MessageType == ua.MessageType.Error: msg = struct_from_binary(ua.ErrorMessage, body) logger.warning(f"Received an error: {msg}") return msg raise ua.UaError(f"Unsupported message type {header.MessageType}")
def receive_from_header_and_body(self, header, body): """ Convert MessageHeader and binary body to OPC UA TCP message (see OPC UA specs Part 6, 7.1: Hello, Acknowledge or ErrorMessage), or a Message object, or None (if intermediate chunk is received) """ if header.MessageType == ua.MessageType.SecureOpen: data = body.copy(header.body_size) security_header = struct_from_binary(ua.AsymmetricAlgorithmHeader, data) self.select_policy(security_header.SecurityPolicyURI, security_header.SenderCertificate) elif header.MessageType in (ua.MessageType.SecureMessage, ua.MessageType.SecureClose): data = body.copy(header.body_size) security_header = struct_from_binary(ua.SymmetricAlgorithmHeader, data) self._check_sym_header(security_header) if header.MessageType in (ua.MessageType.SecureMessage, ua.MessageType.SecureOpen, ua.MessageType.SecureClose): chunk = MessageChunk.from_header_and_body(self.security_policy, header, body) return self._receive(chunk) elif header.MessageType == ua.MessageType.Hello: msg = struct_from_binary(ua.Hello, body) self._max_chunk_size = msg.ReceiveBufferSize return msg elif header.MessageType == ua.MessageType.Acknowledge: msg = struct_from_binary(ua.Acknowledge, body) self._max_chunk_size = msg.SendBufferSize return msg elif header.MessageType == ua.MessageType.Error: msg = struct_from_binary(ua.ErrorMessage, body) logger.warning("Received an error: %s", msg) return msg else: raise ua.UaError("Unsupported message type {0}".format( header.MessageType))
async def set_security_string(self, string: str): """ Set SecureConnection mode. :param string: Mode format ``Policy,Mode,certificate,private_key[,server_private_key]`` where: - ``Policy`` is ``Basic128Rsa15``, ``Basic256`` or ``Basic256Sha256`` - ``Mode`` is ``Sign`` or ``SignAndEncrypt`` - ``certificate`` and ``server_private_key`` are paths to ``.pem`` or ``.der`` files - ``private_key`` may be a path to a ``.pem`` or ``.der`` file or a conjunction of ``path``::``password`` where ``password`` is the private key password. Call this before connect() """ if not string: return parts = string.split(",") if len(parts) < 4: raise ua.UaError( "Wrong format: `{}`, expected at least 4 comma-separated values" .format(string)) if '::' in parts[ 3]: # if the filename contains a colon, assume it's a conjunction and parse it parts[3], client_key_password = parts[3].split('::') else: client_key_password = None policy_class = getattr(security_policies, "SecurityPolicy{}".format(parts[0])) mode = getattr(ua.MessageSecurityMode, parts[1]) return await self.set_security(policy_class, parts[2], parts[3], client_key_password, parts[4] if len(parts) >= 5 else None, mode)
def _call_callback(self, request_id, body): future = self._callbackmap.pop(request_id, None) if future is None: raise ua.UaError("No request found for requestid: {0}, callbacks in list are {1}".format( request_id, self._callbackmap.keys())) future.set_result(body)
def encrypted_size(self, plain_size): size = plain_size + self.security_policy.signature_size() pbs = self.security_policy.plain_block_size() if size % pbs != 0: raise ua.UaError("Encryption error") return size // pbs * self.security_policy.encrypted_block_size()