| /* This Source Code Form is subject to the terms of the Mozilla Public |
| * License, v. 2.0. If a copy of the MPL was not distributed with this |
| * file, You can obtain one at http://mozilla.org/MPL/2.0/. |
| * |
| * Copyright 2014-2018 (c) Fraunhofer IOSB (Author: Julius Pfrommer) |
| * Copyright 2015-2016 (c) Sten GrĂ¼ner |
| * Copyright 2014-2017 (c) Florian Palm |
| * Copyright 2015 (c) Christian Fimmers |
| * Copyright 2015-2016 (c) Chris Iatrou |
| * Copyright 2015-2016 (c) Oleksiy Vasylyev |
| * Copyright 2015 (c) wuyangtang |
| * Copyright 2017 (c) Stefan Profanter, fortiss GmbH |
| * Copyright 2016 (c) Lorenz Haas |
| * Copyright 2017 (c) frax2222 |
| * Copyright 2017 (c) Thomas Bender |
| * Copyright 2017 (c) Julian Grothoff |
| * Copyright 2017 (c) Jonas Green |
| * Copyright 2017 (c) Henrik Norrman |
| */ |
| |
| #include "ua_server_internal.h" |
| #include "ua_types_encoding_binary.h" |
| #include "ua_services.h" |
| |
| #ifdef UA_ENABLE_HISTORIZING |
| #include <open62541/plugin/historydatabase.h> |
| #endif |
| |
| /******************/ |
| /* Access Control */ |
| /******************/ |
| |
| static UA_UInt32 |
| getUserWriteMask(UA_Server *server, const UA_Session *session, |
| const UA_Node *node) { |
| if(session == &server->adminSession) |
| return 0xFFFFFFFF; /* the local admin user has all rights */ |
| UA_UInt32 retval = node->writeMask; |
| UA_UNLOCK(server->serviceMutex); |
| retval &= server->config.accessControl.getUserRightsMask(server, &server->config.accessControl, |
| &session->sessionId, session->sessionHandle, |
| &node->nodeId, node->context); |
| UA_LOCK(server->serviceMutex); |
| return retval; |
| } |
| |
| static UA_Byte |
| getAccessLevel(UA_Server *server, const UA_Session *session, |
| const UA_VariableNode *node) { |
| if(session == &server->adminSession) |
| return 0xFF; /* the local admin user has all rights */ |
| return node->accessLevel; |
| } |
| |
| static UA_Byte |
| getUserAccessLevel(UA_Server *server, const UA_Session *session, |
| const UA_VariableNode *node) { |
| if(session == &server->adminSession) |
| return 0xFF; /* the local admin user has all rights */ |
| UA_Byte retval = node->accessLevel; |
| UA_UNLOCK(server->serviceMutex); |
| retval &= server->config.accessControl.getUserAccessLevel(server, &server->config.accessControl, |
| &session->sessionId, session->sessionHandle, |
| &node->nodeId, node->context); |
| UA_LOCK(server->serviceMutex); |
| return retval; |
| } |
| |
| static UA_Boolean |
| getUserExecutable(UA_Server *server, const UA_Session *session, |
| const UA_MethodNode *node) { |
| if(session == &server->adminSession) |
| return true; /* the local admin user has all rights */ |
| UA_Boolean retval = node->executable; |
| UA_UNLOCK(server->serviceMutex); |
| retval &= server->config.accessControl.getUserExecutable(server, &server->config.accessControl, |
| &session->sessionId, session->sessionHandle, |
| &node->nodeId, node->context); |
| UA_LOCK(server->serviceMutex); |
| return retval; |
| } |
| |
| /****************/ |
| /* Read Service */ |
| /****************/ |
| |
| static UA_StatusCode |
| readIsAbstractAttribute(const UA_Node *node, UA_Variant *v) { |
| const UA_Boolean *isAbstract; |
| switch(node->nodeClass) { |
| case UA_NODECLASS_REFERENCETYPE: |
| isAbstract = &((const UA_ReferenceTypeNode*)node)->isAbstract; |
| break; |
| case UA_NODECLASS_OBJECTTYPE: |
| isAbstract = &((const UA_ObjectTypeNode*)node)->isAbstract; |
| break; |
| case UA_NODECLASS_VARIABLETYPE: |
| isAbstract = &((const UA_VariableTypeNode*)node)->isAbstract; |
| break; |
| case UA_NODECLASS_DATATYPE: |
| isAbstract = &((const UA_DataTypeNode*)node)->isAbstract; |
| break; |
| default: |
| return UA_STATUSCODE_BADATTRIBUTEIDINVALID; |
| } |
| |
| return UA_Variant_setScalarCopy(v, isAbstract, &UA_TYPES[UA_TYPES_BOOLEAN]); |
| } |
| |
| static UA_StatusCode |
| readValueAttributeFromNode(UA_Server *server, UA_Session *session, |
| const UA_VariableNode *vn, UA_DataValue *v, |
| UA_NumericRange *rangeptr) { |
| /* Update the value by the user callback */ |
| if(vn->value.data.callback.onRead) { |
| UA_UNLOCK(server->serviceMutex); |
| vn->value.data.callback.onRead(server, &session->sessionId, |
| session->sessionHandle, &vn->nodeId, |
| vn->context, rangeptr, &vn->value.data.value); |
| UA_LOCK(server->serviceMutex); |
| vn = (const UA_VariableNode*)UA_Nodestore_getNode(server->nsCtx, &vn->nodeId); |
| if(!vn) |
| return UA_STATUSCODE_BADNODEIDUNKNOWN; |
| } |
| |
| /* Set the result */ |
| if(rangeptr) |
| return UA_Variant_copyRange(&vn->value.data.value.value, &v->value, *rangeptr); |
| UA_StatusCode retval = UA_DataValue_copy(&vn->value.data.value, v); |
| |
| /* Clean up */ |
| if(vn->value.data.callback.onRead) |
| UA_Nodestore_releaseNode(server->nsCtx, (const UA_Node *)vn); |
| return retval; |
| } |
| |
| static UA_StatusCode |
| readValueAttributeFromDataSource(UA_Server *server, UA_Session *session, |
| const UA_VariableNode *vn, UA_DataValue *v, |
| UA_TimestampsToReturn timestamps, |
| UA_NumericRange *rangeptr) { |
| if(!vn->value.dataSource.read) |
| return UA_STATUSCODE_BADINTERNALERROR; |
| UA_Boolean sourceTimeStamp = (timestamps == UA_TIMESTAMPSTORETURN_SOURCE || |
| timestamps == UA_TIMESTAMPSTORETURN_BOTH); |
| UA_DataValue v2; |
| UA_DataValue_init(&v2); |
| UA_UNLOCK(server->serviceMutex); |
| UA_StatusCode retval = vn->value.dataSource. |
| read(server, &session->sessionId, session->sessionHandle, |
| &vn->nodeId, vn->context, sourceTimeStamp, rangeptr, &v2); |
| UA_LOCK(server->serviceMutex); |
| if(v2.hasValue && v2.value.storageType == UA_VARIANT_DATA_NODELETE) { |
| retval = UA_DataValue_copy(&v2, v); |
| UA_DataValue_deleteMembers(&v2); |
| } else { |
| *v = v2; |
| } |
| return retval; |
| } |
| |
| static UA_StatusCode |
| readValueAttributeComplete(UA_Server *server, UA_Session *session, |
| const UA_VariableNode *vn, UA_TimestampsToReturn timestamps, |
| const UA_String *indexRange, UA_DataValue *v) { |
| /* Compute the index range */ |
| UA_NumericRange range; |
| UA_NumericRange *rangeptr = NULL; |
| UA_StatusCode retval = UA_STATUSCODE_GOOD; |
| if(indexRange && indexRange->length > 0) { |
| retval = UA_NumericRange_parseFromString(&range, indexRange); |
| if(retval != UA_STATUSCODE_GOOD) |
| return retval; |
| rangeptr = ⦥ |
| } |
| |
| /* Read the value */ |
| if(vn->valueSource == UA_VALUESOURCE_DATA) |
| retval = readValueAttributeFromNode(server, session, vn, v, rangeptr); |
| else |
| retval = readValueAttributeFromDataSource(server, session, vn, v, timestamps, rangeptr); |
| |
| /* Clean up */ |
| if(rangeptr) |
| UA_free(range.dimensions); |
| return retval; |
| } |
| |
| UA_StatusCode |
| readValueAttribute(UA_Server *server, UA_Session *session, |
| const UA_VariableNode *vn, UA_DataValue *v) { |
| return readValueAttributeComplete(server, session, vn, UA_TIMESTAMPSTORETURN_NEITHER, NULL, v); |
| } |
| |
| static const UA_String binEncoding = {sizeof("Default Binary")-1, (UA_Byte*)"Default Binary"}; |
| static const UA_String xmlEncoding = {sizeof("Default XML")-1, (UA_Byte*)"Default XML"}; |
| static const UA_String jsonEncoding = {sizeof("Default JSON")-1, (UA_Byte*)"Default JSON"}; |
| |
| #define CHECK_NODECLASS(CLASS) \ |
| if(!(node->nodeClass & (CLASS))) { \ |
| retval = UA_STATUSCODE_BADATTRIBUTEIDINVALID; \ |
| break; \ |
| } |
| |
| #ifdef UA_ENABLE_TYPEDESCRIPTION |
| static const UA_DataType * |
| findDataType(const UA_Node *node, const UA_DataTypeArray *customTypes) { |
| for(size_t i = 0; i < UA_TYPES_COUNT; ++i) { |
| if(UA_NodeId_equal(&UA_TYPES[i].typeId, &node->nodeId)) { |
| return &UA_TYPES[i]; |
| } |
| } |
| |
| // lookup custom type |
| while(customTypes) { |
| for(size_t i = 0; i < customTypes->typesSize; ++i) { |
| if(UA_NodeId_equal(&customTypes->types[i].typeId, &node->nodeId)) |
| return &customTypes->types[i]; |
| } |
| customTypes = customTypes->next; |
| } |
| return NULL; |
| } |
| |
| static UA_StatusCode |
| getStructureDefinition(const UA_DataType *type, UA_StructureDefinition *def) { |
| def->baseDataType = UA_NODEID_NUMERIC(0, UA_NS0ID_STRUCTURE); |
| def->defaultEncodingId = |
| UA_NODEID_NUMERIC(type->typeId.namespaceIndex, type->binaryEncodingId); |
| def->structureType = UA_STRUCTURETYPE_STRUCTURE; |
| def->fieldsSize = type->membersSize; |
| def->fields = |
| (UA_StructureField *)UA_calloc(def->fieldsSize, sizeof(UA_StructureField)); |
| if(!def->fields) { |
| return UA_STATUSCODE_BADOUTOFMEMORY; |
| } |
| const UA_DataType *typelists[2] = {UA_TYPES, &type[-type->typeIndex]}; |
| for(size_t cnt = 0; cnt < def->fieldsSize; cnt++) { |
| const UA_DataTypeMember *m = &type->members[cnt]; |
| def->fields[cnt].valueRank = UA_TRUE == m->isArray ? 1 : -1; |
| def->fields[cnt].arrayDimensions = NULL; |
| def->fields[cnt].arrayDimensionsSize = 0; |
| def->fields[cnt].name = |
| UA_STRING((char *)(uintptr_t)m->memberName); |
| def->fields[cnt].description.locale = UA_STRING_NULL; |
| def->fields[cnt].description.text = UA_STRING_NULL; |
| def->fields[cnt].dataType = typelists[!m->namespaceZero][m->memberTypeIndex].typeId; |
| def->fields[cnt].maxStringLength = 0; |
| } |
| return UA_STATUSCODE_GOOD; |
| } |
| #endif |
| |
| /* Returns a datavalue that may point into the node via the |
| * UA_VARIANT_DATA_NODELETE tag. Don't access the returned DataValue once the |
| * node has been released! */ |
| void |
| ReadWithNode(const UA_Node *node, UA_Server *server, UA_Session *session, |
| UA_TimestampsToReturn timestampsToReturn, |
| const UA_ReadValueId *id, UA_DataValue *v) { |
| UA_LOG_DEBUG_SESSION(&server->config.logger, session, |
| "Read the attribute %i", id->attributeId); |
| |
| /* Only Binary Encoding is supported */ |
| if(id->dataEncoding.name.length > 0 && |
| !UA_String_equal(&binEncoding, &id->dataEncoding.name)) { |
| if(UA_String_equal(&xmlEncoding, &id->dataEncoding.name) || |
| UA_String_equal(&jsonEncoding, &id->dataEncoding.name)) |
| v->status = UA_STATUSCODE_BADDATAENCODINGUNSUPPORTED; |
| else |
| v->status = UA_STATUSCODE_BADDATAENCODINGINVALID; |
| v->hasStatus = true; |
| return; |
| } |
| |
| /* Index range for an attribute other than value */ |
| if(id->indexRange.length > 0 && id->attributeId != UA_ATTRIBUTEID_VALUE) { |
| v->hasStatus = true; |
| v->status = UA_STATUSCODE_BADINDEXRANGENODATA; |
| return; |
| } |
| |
| /* Read the attribute */ |
| UA_StatusCode retval = UA_STATUSCODE_GOOD; |
| switch(id->attributeId) { |
| case UA_ATTRIBUTEID_NODEID: |
| retval = UA_Variant_setScalarCopy(&v->value, &node->nodeId, &UA_TYPES[UA_TYPES_NODEID]); |
| break; |
| case UA_ATTRIBUTEID_NODECLASS: |
| retval = UA_Variant_setScalarCopy(&v->value, &node->nodeClass, &UA_TYPES[UA_TYPES_NODECLASS]); |
| break; |
| case UA_ATTRIBUTEID_BROWSENAME: |
| retval = UA_Variant_setScalarCopy(&v->value, &node->browseName, &UA_TYPES[UA_TYPES_QUALIFIEDNAME]); |
| break; |
| case UA_ATTRIBUTEID_DISPLAYNAME: |
| retval = UA_Variant_setScalarCopy(&v->value, &node->displayName, &UA_TYPES[UA_TYPES_LOCALIZEDTEXT]); |
| break; |
| case UA_ATTRIBUTEID_DESCRIPTION: |
| retval = UA_Variant_setScalarCopy(&v->value, &node->description, &UA_TYPES[UA_TYPES_LOCALIZEDTEXT]); |
| break; |
| case UA_ATTRIBUTEID_WRITEMASK: |
| retval = UA_Variant_setScalarCopy(&v->value, &node->writeMask, &UA_TYPES[UA_TYPES_UINT32]); |
| break; |
| case UA_ATTRIBUTEID_USERWRITEMASK: { |
| UA_UInt32 userWriteMask = getUserWriteMask(server, session, node); |
| retval = UA_Variant_setScalarCopy(&v->value, &userWriteMask, &UA_TYPES[UA_TYPES_UINT32]); |
| break; } |
| case UA_ATTRIBUTEID_ISABSTRACT: |
| retval = readIsAbstractAttribute(node, &v->value); |
| break; |
| case UA_ATTRIBUTEID_SYMMETRIC: |
| CHECK_NODECLASS(UA_NODECLASS_REFERENCETYPE); |
| retval = UA_Variant_setScalarCopy(&v->value, &((const UA_ReferenceTypeNode*)node)->symmetric, |
| &UA_TYPES[UA_TYPES_BOOLEAN]); |
| break; |
| case UA_ATTRIBUTEID_INVERSENAME: |
| CHECK_NODECLASS(UA_NODECLASS_REFERENCETYPE); |
| retval = UA_Variant_setScalarCopy(&v->value, &((const UA_ReferenceTypeNode*)node)->inverseName, |
| &UA_TYPES[UA_TYPES_LOCALIZEDTEXT]); |
| break; |
| case UA_ATTRIBUTEID_CONTAINSNOLOOPS: |
| CHECK_NODECLASS(UA_NODECLASS_VIEW); |
| retval = UA_Variant_setScalarCopy(&v->value, &((const UA_ViewNode*)node)->containsNoLoops, |
| &UA_TYPES[UA_TYPES_BOOLEAN]); |
| break; |
| case UA_ATTRIBUTEID_EVENTNOTIFIER: |
| CHECK_NODECLASS(UA_NODECLASS_VIEW | UA_NODECLASS_OBJECT); |
| if(node->nodeClass == UA_NODECLASS_VIEW) { |
| retval = UA_Variant_setScalarCopy(&v->value, &((const UA_ViewNode*)node)->eventNotifier, |
| &UA_TYPES[UA_TYPES_BYTE]); |
| } |
| else{ |
| retval = UA_Variant_setScalarCopy(&v->value, &((const UA_ObjectNode*)node)->eventNotifier, |
| &UA_TYPES[UA_TYPES_BYTE]); |
| } |
| break; |
| case UA_ATTRIBUTEID_VALUE: { |
| CHECK_NODECLASS(UA_NODECLASS_VARIABLE | UA_NODECLASS_VARIABLETYPE); |
| /* VariableTypes don't have the AccessLevel concept. Always allow reading the value. */ |
| if(node->nodeClass == UA_NODECLASS_VARIABLE) { |
| /* The access to a value variable is granted via the AccessLevel |
| * and UserAccessLevel attributes */ |
| UA_Byte accessLevel = getAccessLevel(server, session, (const UA_VariableNode*)node); |
| if(!(accessLevel & (UA_ACCESSLEVELMASK_READ))) { |
| retval = UA_STATUSCODE_BADNOTREADABLE; |
| break; |
| } |
| accessLevel = getUserAccessLevel(server, session, |
| (const UA_VariableNode*)node); |
| if(!(accessLevel & (UA_ACCESSLEVELMASK_READ))) { |
| retval = UA_STATUSCODE_BADUSERACCESSDENIED; |
| break; |
| } |
| } |
| retval = readValueAttributeComplete(server, session, (const UA_VariableNode*)node, |
| timestampsToReturn, &id->indexRange, v); |
| break; |
| } |
| case UA_ATTRIBUTEID_DATATYPE: |
| CHECK_NODECLASS(UA_NODECLASS_VARIABLE | UA_NODECLASS_VARIABLETYPE); |
| retval = UA_Variant_setScalarCopy(&v->value, &((const UA_VariableTypeNode*)node)->dataType, |
| &UA_TYPES[UA_TYPES_NODEID]); |
| break; |
| case UA_ATTRIBUTEID_VALUERANK: |
| CHECK_NODECLASS(UA_NODECLASS_VARIABLE | UA_NODECLASS_VARIABLETYPE); |
| retval = UA_Variant_setScalarCopy(&v->value, &((const UA_VariableTypeNode*)node)->valueRank, |
| &UA_TYPES[UA_TYPES_INT32]); |
| break; |
| case UA_ATTRIBUTEID_ARRAYDIMENSIONS: |
| CHECK_NODECLASS(UA_NODECLASS_VARIABLE | UA_NODECLASS_VARIABLETYPE); |
| retval = UA_Variant_setArrayCopy(&v->value, ((const UA_VariableTypeNode*)node)->arrayDimensions, |
| ((const UA_VariableTypeNode*)node)->arrayDimensionsSize, |
| &UA_TYPES[UA_TYPES_UINT32]); |
| break; |
| case UA_ATTRIBUTEID_ACCESSLEVEL: |
| CHECK_NODECLASS(UA_NODECLASS_VARIABLE); |
| retval = UA_Variant_setScalarCopy(&v->value, &((const UA_VariableNode*)node)->accessLevel, |
| &UA_TYPES[UA_TYPES_BYTE]); |
| break; |
| case UA_ATTRIBUTEID_USERACCESSLEVEL: { |
| CHECK_NODECLASS(UA_NODECLASS_VARIABLE); |
| UA_Byte userAccessLevel = getUserAccessLevel(server, session, (const UA_VariableNode*)node); |
| retval = UA_Variant_setScalarCopy(&v->value, &userAccessLevel, &UA_TYPES[UA_TYPES_BYTE]); |
| break; } |
| case UA_ATTRIBUTEID_MINIMUMSAMPLINGINTERVAL: |
| CHECK_NODECLASS(UA_NODECLASS_VARIABLE); |
| retval = UA_Variant_setScalarCopy(&v->value, &((const UA_VariableNode*)node)->minimumSamplingInterval, |
| &UA_TYPES[UA_TYPES_DOUBLE]); |
| break; |
| case UA_ATTRIBUTEID_HISTORIZING: |
| CHECK_NODECLASS(UA_NODECLASS_VARIABLE); |
| retval = UA_Variant_setScalarCopy(&v->value, &((const UA_VariableNode*)node)->historizing, |
| &UA_TYPES[UA_TYPES_BOOLEAN]); |
| break; |
| case UA_ATTRIBUTEID_EXECUTABLE: |
| CHECK_NODECLASS(UA_NODECLASS_METHOD); |
| retval = UA_Variant_setScalarCopy(&v->value, &((const UA_MethodNode*)node)->executable, |
| &UA_TYPES[UA_TYPES_BOOLEAN]); |
| break; |
| case UA_ATTRIBUTEID_USEREXECUTABLE: { |
| CHECK_NODECLASS(UA_NODECLASS_METHOD); |
| UA_Boolean userExecutable = getUserExecutable(server, session, (const UA_MethodNode*)node); |
| retval = UA_Variant_setScalarCopy(&v->value, &userExecutable, &UA_TYPES[UA_TYPES_BOOLEAN]); |
| break; } |
| case UA_ATTRIBUTEID_DATATYPEDEFINITION: { |
| CHECK_NODECLASS(UA_NODECLASS_DATATYPE); |
| |
| #ifdef UA_ENABLE_TYPEDESCRIPTION |
| const UA_DataType *type = findDataType(node, server->config.customDataTypes); |
| if(!type) { |
| retval = UA_STATUSCODE_BADATTRIBUTEIDINVALID; |
| break; |
| } |
| |
| if(UA_DATATYPEKIND_STRUCTURE == type->typeKind || |
| UA_DATATYPEKIND_OPTSTRUCT == type->typeKind) { |
| UA_StructureDefinition def; |
| retval = getStructureDefinition(type, &def); |
| if(UA_STATUSCODE_GOOD!=retval) |
| break; |
| retval = UA_Variant_setScalarCopy(&v->value, &def, |
| &UA_TYPES[UA_TYPES_STRUCTUREDEFINITION]); |
| UA_free(def.fields); |
| break; |
| } |
| #endif |
| retval = UA_STATUSCODE_BADATTRIBUTEIDINVALID; |
| break; } |
| default: |
| retval = UA_STATUSCODE_BADATTRIBUTEIDINVALID; |
| } |
| |
| /* Return error code when reading has failed */ |
| if(retval != UA_STATUSCODE_GOOD) { |
| v->hasStatus = true; |
| v->status = retval; |
| return; |
| } |
| |
| v->hasValue = true; |
| |
| /* Create server timestamp */ |
| if(timestampsToReturn == UA_TIMESTAMPSTORETURN_SERVER || |
| timestampsToReturn == UA_TIMESTAMPSTORETURN_BOTH) { |
| if (!v->hasServerTimestamp) { |
| v->serverTimestamp = UA_DateTime_now(); |
| v->hasServerTimestamp = true; |
| } |
| } else { |
| /* In case the ServerTimestamp has been set manually */ |
| v->hasServerTimestamp = false; |
| } |
| |
| /* Handle source time stamp */ |
| if(id->attributeId == UA_ATTRIBUTEID_VALUE) { |
| if(timestampsToReturn == UA_TIMESTAMPSTORETURN_SERVER || |
| timestampsToReturn == UA_TIMESTAMPSTORETURN_NEITHER) { |
| v->hasSourceTimestamp = false; |
| v->hasSourcePicoseconds = false; |
| } else if(!v->hasSourceTimestamp) { |
| v->sourceTimestamp = UA_DateTime_now(); |
| v->hasSourceTimestamp = true; |
| } |
| } |
| } |
| |
| static void |
| Operation_Read(UA_Server *server, UA_Session *session, UA_ReadRequest *request, |
| UA_ReadValueId *rvi, UA_DataValue *result) { |
| /* Get the node */ |
| const UA_Node *node = UA_Nodestore_getNode(server->nsCtx, &rvi->nodeId); |
| |
| /* Perform the read operation */ |
| if(node) { |
| ReadWithNode(node, server, session, request->timestampsToReturn, rvi, result); |
| UA_Nodestore_releaseNode(server->nsCtx, node); |
| } else { |
| result->hasStatus = true; |
| result->status = UA_STATUSCODE_BADNODEIDUNKNOWN; |
| } |
| } |
| |
| void |
| Service_Read(UA_Server *server, UA_Session *session, |
| const UA_ReadRequest *request, UA_ReadResponse *response) { |
| UA_LOG_DEBUG_SESSION(&server->config.logger, session, "Processing ReadRequest"); |
| UA_LOCK_ASSERT(server->serviceMutex, 1); |
| |
| /* Check if the timestampstoreturn is valid */ |
| if(request->timestampsToReturn > UA_TIMESTAMPSTORETURN_NEITHER) { |
| response->responseHeader.serviceResult = UA_STATUSCODE_BADTIMESTAMPSTORETURNINVALID; |
| return; |
| } |
| |
| /* Check if maxAge is valid */ |
| if(request->maxAge < 0) { |
| response->responseHeader.serviceResult = UA_STATUSCODE_BADMAXAGEINVALID; |
| return; |
| } |
| |
| /* Check if there are too many operations */ |
| if(server->config.maxNodesPerRead != 0 && |
| request->nodesToReadSize > server->config.maxNodesPerRead) { |
| response->responseHeader.serviceResult = UA_STATUSCODE_BADTOOMANYOPERATIONS; |
| return; |
| } |
| |
| UA_LOCK_ASSERT(server->serviceMutex, 1); |
| |
| response->responseHeader.serviceResult = |
| UA_Server_processServiceOperations(server, session, (UA_ServiceOperation)Operation_Read, |
| request, |
| &request->nodesToReadSize, &UA_TYPES[UA_TYPES_READVALUEID], |
| &response->resultsSize, &UA_TYPES[UA_TYPES_DATAVALUE]); |
| } |
| |
| UA_DataValue |
| UA_Server_readWithSession(UA_Server *server, UA_Session *session, |
| const UA_ReadValueId *item, |
| UA_TimestampsToReturn timestampsToReturn) { |
| UA_DataValue dv; |
| UA_DataValue_init(&dv); |
| |
| /* Get the node */ |
| const UA_Node *node = UA_Nodestore_getNode(server->nsCtx, &item->nodeId); |
| if(!node) { |
| dv.hasStatus = true; |
| dv.status = UA_STATUSCODE_BADNODEIDUNKNOWN; |
| return dv; |
| } |
| |
| /* Perform the read operation */ |
| ReadWithNode(node, server, session, timestampsToReturn, item, &dv); |
| |
| /* Release the node and return */ |
| UA_Nodestore_releaseNode(server->nsCtx, node); |
| return dv; |
| } |
| |
| UA_DataValue |
| readAttribute(UA_Server *server, const UA_ReadValueId *item, |
| UA_TimestampsToReturn timestamps) { |
| return UA_Server_readWithSession(server, &server->adminSession, item, timestamps); |
| } |
| |
| UA_StatusCode |
| readWithReadValue(UA_Server *server, const UA_NodeId *nodeId, |
| const UA_AttributeId attributeId, void *v) { |
| /* Call the read service */ |
| UA_ReadValueId item; |
| UA_ReadValueId_init(&item); |
| item.nodeId = *nodeId; |
| item.attributeId = attributeId; |
| UA_DataValue dv = readAttribute(server, &item, UA_TIMESTAMPSTORETURN_NEITHER); |
| |
| /* Check the return value */ |
| UA_StatusCode retval = UA_STATUSCODE_GOOD; |
| if(dv.hasStatus) |
| retval = dv.status; |
| else if(!dv.hasValue) |
| retval = UA_STATUSCODE_BADUNEXPECTEDERROR; |
| if(retval != UA_STATUSCODE_GOOD) { |
| UA_DataValue_deleteMembers(&dv); |
| return retval; |
| } |
| |
| if(attributeId == UA_ATTRIBUTEID_VALUE || |
| attributeId == UA_ATTRIBUTEID_ARRAYDIMENSIONS) { |
| /* Return the entire variant */ |
| memcpy(v, &dv.value, sizeof(UA_Variant)); |
| } else { |
| /* Return the variant content only */ |
| memcpy(v, dv.value.data, dv.value.type->memSize); |
| UA_free(dv.value.data); |
| } |
| return retval; |
| } |
| |
| /* Exposes the Read service to local users */ |
| UA_DataValue |
| UA_Server_read(UA_Server *server, const UA_ReadValueId *item, |
| UA_TimestampsToReturn timestamps) { |
| UA_LOCK(server->serviceMutex); |
| UA_DataValue dv = readAttribute(server, item, timestamps); |
| UA_UNLOCK(server->serviceMutex); |
| return dv; |
| } |
| |
| /* Used in inline functions exposing the Read service with more syntactic sugar |
| * for individual attributes */ |
| UA_StatusCode |
| __UA_Server_read(UA_Server *server, const UA_NodeId *nodeId, |
| const UA_AttributeId attributeId, void *v) { |
| UA_LOCK(server->serviceMutex); |
| UA_StatusCode retval = readWithReadValue(server, nodeId, attributeId, v); |
| UA_UNLOCK(server->serviceMutex); |
| return retval; |
| } |
| |
| UA_StatusCode |
| UA_Server_readObjectProperty(UA_Server *server, const UA_NodeId objectId, |
| const UA_QualifiedName propertyName, |
| UA_Variant *value) { |
| UA_RelativePathElement rpe; |
| UA_RelativePathElement_init(&rpe); |
| rpe.referenceTypeId = UA_NODEID_NUMERIC(0, UA_NS0ID_HASPROPERTY); |
| rpe.isInverse = false; |
| rpe.includeSubtypes = false; |
| rpe.targetName = propertyName; |
| |
| UA_BrowsePath bp; |
| UA_BrowsePath_init(&bp); |
| bp.startingNode = objectId; |
| bp.relativePath.elementsSize = 1; |
| bp.relativePath.elements = &rpe; |
| |
| UA_StatusCode retval; |
| UA_BrowsePathResult bpr = translateBrowsePathToNodeIds(server, &bp); |
| if(bpr.statusCode != UA_STATUSCODE_GOOD || bpr.targetsSize < 1) { |
| retval = bpr.statusCode; |
| UA_BrowsePathResult_deleteMembers(&bpr); |
| return retval; |
| } |
| |
| retval = UA_Server_readValue(server, bpr.targets[0].targetId.nodeId, value); |
| |
| UA_BrowsePathResult_deleteMembers(&bpr); |
| return retval; |
| } |
| |
| /*****************/ |
| /* Type Checking */ |
| /*****************/ |
| |
| static UA_DataTypeKind |
| typeEquivalence(const UA_DataType *t) { |
| UA_DataTypeKind k = (UA_DataTypeKind)t->typeKind; |
| if(k == UA_DATATYPEKIND_ENUM) |
| return UA_DATATYPEKIND_INT32; |
| return k; |
| } |
| |
| static const UA_NodeId enumNodeId = {0, UA_NODEIDTYPE_NUMERIC, {UA_NS0ID_ENUMERATION}}; |
| |
| UA_Boolean |
| compatibleDataType(UA_Server *server, const UA_NodeId *dataType, |
| const UA_NodeId *constraintDataType, UA_Boolean isValue) { |
| /* Do not allow empty datatypes */ |
| if(UA_NodeId_isNull(dataType)) |
| return false; |
| |
| /* No constraint (TODO: use variant instead) */ |
| if(UA_NodeId_isNull(constraintDataType)) |
| return true; |
| |
| /* Same datatypes */ |
| if (UA_NodeId_equal(dataType, constraintDataType)) |
| return true; |
| |
| /* Variant allows any subtype */ |
| if(UA_NodeId_equal(constraintDataType, &UA_TYPES[UA_TYPES_VARIANT].typeId)) |
| return true; |
| |
| /* Is the value-type a subtype of the required type? */ |
| if(isNodeInTree(server->nsCtx, dataType, constraintDataType, &subtypeId, 1)) |
| return true; |
| |
| /* Enum allows Int32 (only) */ |
| if(UA_NodeId_equal(dataType, &UA_TYPES[UA_TYPES_INT32].typeId) && |
| isNodeInTree(server->nsCtx, constraintDataType, &enumNodeId, &subtypeId, 1)) |
| return true; |
| |
| /* More checks for the data type of real values (variants) */ |
| if(isValue) { |
| /* If value is a built-in type: The target data type may be a sub type of |
| * the built-in type. (e.g. UtcTime is sub-type of DateTime and has a |
| * DateTime value). A type is builtin if its NodeId is in Namespace 0 and |
| * has a numeric identifier <= 25 (DiagnosticInfo) */ |
| if(dataType->namespaceIndex == 0 && |
| dataType->identifierType == UA_NODEIDTYPE_NUMERIC && |
| dataType->identifier.numeric <= 25 && |
| isNodeInTree(server->nsCtx, constraintDataType, |
| dataType, &subtypeId, 1)) |
| return true; |
| } |
| |
| return false; |
| } |
| |
| /* Test whether a ValueRank and the given arraydimensions are compatible. |
| * |
| * 5.6.2 Variable NodeClass: If the maximum is unknown the value shall be 0. The |
| * number of elements shall be equal to the value of the ValueRank Attribute. |
| * This Attribute shall be null if ValueRank <= 0. */ |
| UA_Boolean |
| compatibleValueRankArrayDimensions(UA_Server *server, UA_Session *session, |
| UA_Int32 valueRank, size_t arrayDimensionsSize) { |
| /* ValueRank invalid */ |
| if(valueRank < UA_VALUERANK_SCALAR_OR_ONE_DIMENSION) { |
| UA_LOG_INFO_SESSION(&server->config.logger, session, |
| "The ValueRank is invalid (< -3)"); |
| return false; |
| } |
| |
| /* case -3, UA_VALUERANK_SCALAR_OR_ONE_DIMENSION: the value can be a scalar or a one dimensional array */ |
| /* case -2, UA_VALUERANK_ANY: the value can be a scalar or an array with any number of dimensions */ |
| /* case -1, UA_VALUERANK_SCALAR: the value is a scalar */ |
| /* case 0, UA_VALUERANK_ONE_OR_MORE_DIMENSIONS: the value is an array with one or more dimensions */ |
| if(valueRank <= UA_VALUERANK_ONE_OR_MORE_DIMENSIONS) { |
| if(arrayDimensionsSize > 0) { |
| UA_LOG_INFO_SESSION(&server->config.logger, session, |
| "No ArrayDimensions can be defined for a ValueRank <= 0"); |
| return false; |
| } |
| return true; |
| } |
| |
| /* case >= 1, UA_VALUERANK_ONE_DIMENSION: the value is an array with the specified number of dimensions */ |
| if(arrayDimensionsSize != (size_t)valueRank) { |
| UA_LOG_INFO_SESSION(&server->config.logger, session, |
| "The number of ArrayDimensions is not equal to the (positive) ValueRank"); |
| return false; |
| } |
| return true; |
| } |
| |
| UA_Boolean |
| compatibleValueRanks(UA_Int32 valueRank, UA_Int32 constraintValueRank) { |
| /* Check if the valuerank of the variabletype allows the change. */ |
| switch(constraintValueRank) { |
| case UA_VALUERANK_SCALAR_OR_ONE_DIMENSION: /* the value can be a scalar or a one dimensional array */ |
| if(valueRank != UA_VALUERANK_SCALAR && valueRank != UA_VALUERANK_ONE_DIMENSION) |
| return false; |
| break; |
| case UA_VALUERANK_ANY: /* the value can be a scalar or an array with any number of dimensions */ |
| break; |
| case UA_VALUERANK_SCALAR: /* the value is a scalar */ |
| if(valueRank != UA_VALUERANK_SCALAR) |
| return false; |
| break; |
| case UA_VALUERANK_ONE_OR_MORE_DIMENSIONS: /* the value is an array with one or more dimensions */ |
| if(valueRank < (UA_Int32) UA_VALUERANK_ONE_OR_MORE_DIMENSIONS) |
| return false; |
| break; |
| default: /* >= 1: the value is an array with the specified number of dimensions */ |
| if(valueRank != constraintValueRank) |
| return false; |
| break; |
| } |
| return true; |
| } |
| |
| /* Check if the ValueRank allows for the value dimension. This is more |
| * permissive than checking for the ArrayDimensions attribute. Because the value |
| * can have dimensions if the ValueRank < 0 */ |
| static UA_Boolean |
| compatibleValueRankValue(UA_Int32 valueRank, const UA_Variant *value) { |
| /* Invalid ValueRank */ |
| if(valueRank < UA_VALUERANK_SCALAR_OR_ONE_DIMENSION) |
| return false; |
| |
| /* Empty arrays (-1) always match */ |
| if(!value->data) |
| return true; |
| |
| size_t arrayDims = value->arrayDimensionsSize; |
| if(arrayDims == 0 && !UA_Variant_isScalar(value)) |
| arrayDims = 1; /* array but no arraydimensions -> implicit array dimension 1 */ |
| |
| /* We cannot simply use compatibleValueRankArrayDimensions since we can have |
| * defined ArrayDimensions for the value if the ValueRank is -2 */ |
| switch(valueRank) { |
| case UA_VALUERANK_SCALAR_OR_ONE_DIMENSION: /* The value can be a scalar or a one dimensional array */ |
| return (arrayDims <= 1); |
| case UA_VALUERANK_ANY: /* The value can be a scalar or an array with any number of dimensions */ |
| return true; |
| case UA_VALUERANK_SCALAR: /* The value is a scalar */ |
| return (arrayDims == 0); |
| default: |
| break; |
| } |
| |
| UA_assert(valueRank >= UA_VALUERANK_ONE_OR_MORE_DIMENSIONS); |
| |
| /* case 0: the value is an array with one or more dimensions */ |
| return (arrayDims == (UA_UInt32)valueRank); |
| } |
| |
| UA_Boolean |
| compatibleArrayDimensions(size_t constraintArrayDimensionsSize, |
| const UA_UInt32 *constraintArrayDimensions, |
| size_t testArrayDimensionsSize, |
| const UA_UInt32 *testArrayDimensions) { |
| /* No array dimensions defined -> everything is permitted if the value rank fits */ |
| if(constraintArrayDimensionsSize == 0) |
| return true; |
| |
| /* Dimension count must match */ |
| if(testArrayDimensionsSize != constraintArrayDimensionsSize) |
| return false; |
| |
| /* Dimension lengths must not be larger than the constraint. Zero in the |
| * constraint indicates a wildcard. */ |
| for(size_t i = 0; i < constraintArrayDimensionsSize; ++i) { |
| if(constraintArrayDimensions[i] < testArrayDimensions[i] && |
| constraintArrayDimensions[i] != 0) |
| return false; |
| } |
| return true; |
| } |
| |
| UA_Boolean |
| compatibleValueArrayDimensions(const UA_Variant *value, size_t targetArrayDimensionsSize, |
| const UA_UInt32 *targetArrayDimensions) { |
| size_t valueArrayDimensionsSize = value->arrayDimensionsSize; |
| UA_UInt32 *valueArrayDimensions = value->arrayDimensions; |
| UA_UInt32 tempArrayDimensions; |
| if(!valueArrayDimensions && !UA_Variant_isScalar(value)) { |
| valueArrayDimensionsSize = 1; |
| tempArrayDimensions = (UA_UInt32)value->arrayLength; |
| valueArrayDimensions = &tempArrayDimensions; |
| } |
| UA_assert(valueArrayDimensionsSize == 0 || valueArrayDimensions != NULL); |
| return compatibleArrayDimensions(targetArrayDimensionsSize, targetArrayDimensions, |
| valueArrayDimensionsSize, valueArrayDimensions); |
| } |
| |
| UA_Boolean |
| compatibleValue(UA_Server *server, UA_Session *session, const UA_NodeId *targetDataTypeId, |
| UA_Int32 targetValueRank, size_t targetArrayDimensionsSize, |
| const UA_UInt32 *targetArrayDimensions, const UA_Variant *value, |
| const UA_NumericRange *range) { |
| /* Empty value */ |
| if(!value->type) { |
| /* Empty value is allowed for BaseDataType */ |
| if(UA_NodeId_equal(targetDataTypeId, &UA_TYPES[UA_TYPES_VARIANT].typeId) || |
| UA_NodeId_equal(targetDataTypeId, &UA_NODEID_NULL)) |
| return true; |
| |
| /* Allow empty node values since existing information models may have |
| * variables with no value, e.g. OldValues - ns=0;i=3024. See also |
| * #1889, https://github.com/open62541/open62541/pull/1889#issuecomment-403506538 */ |
| if(server->config.relaxEmptyValueConstraint) { |
| UA_LOG_DEBUG_SESSION(&server->config.logger, session, |
| "Only Variables with data type BaseDataType can contain an " |
| "empty value. Allow via explicit constraint relaxation."); |
| return true; |
| } |
| |
| UA_LOG_INFO_SESSION(&server->config.logger, session, |
| "Only Variables with data type BaseDataType can contain an empty value"); |
| return false; |
| } |
| |
| /* Has the value a subtype of the required type? BaseDataType (Variant) can |
| * be anything... */ |
| if(!compatibleDataType(server, &value->type->typeId, targetDataTypeId, true)) |
| return false; |
| |
| /* Array dimensions are checked later when writing the range */ |
| if(range) |
| return true; |
| |
| /* See if the array dimensions match. */ |
| if(!compatibleValueArrayDimensions(value, targetArrayDimensionsSize, targetArrayDimensions)) |
| return false; |
| |
| /* Check if the valuerank allows for the value dimension */ |
| return compatibleValueRankValue(targetValueRank, value); |
| } |
| |
| /*****************/ |
| /* Write Service */ |
| /*****************/ |
| |
| static void |
| adjustValue(UA_Server *server, UA_Variant *value, |
| const UA_NodeId *targetDataTypeId) { |
| const UA_DataType *targetDataType = UA_findDataType(targetDataTypeId); |
| if(!targetDataType) |
| return; |
| |
| /* A string is written to a byte array. the valuerank and array dimensions |
| * are checked later */ |
| if(targetDataType == &UA_TYPES[UA_TYPES_BYTE] && |
| value->type == &UA_TYPES[UA_TYPES_BYTESTRING] && |
| UA_Variant_isScalar(value)) { |
| UA_ByteString *str = (UA_ByteString*)value->data; |
| value->type = &UA_TYPES[UA_TYPES_BYTE]; |
| value->arrayLength = str->length; |
| value->data = str->data; |
| return; |
| } |
| |
| /* An enum was sent as an int32, or an opaque type as a bytestring. This |
| * is detected with the typeIndex indicating the "true" datatype. */ |
| UA_DataTypeKind te1 = typeEquivalence(targetDataType); |
| UA_DataTypeKind te2 = typeEquivalence(value->type); |
| if(te1 == te2 && te1 <= UA_DATATYPEKIND_ENUM) { |
| value->type = targetDataType; |
| return; |
| } |
| |
| /* No more possible equivalencies */ |
| } |
| |
| static UA_StatusCode |
| writeArrayDimensionsAttribute(UA_Server *server, UA_Session *session, |
| UA_VariableNode *node, const UA_VariableTypeNode *type, |
| size_t arrayDimensionsSize, UA_UInt32 *arrayDimensions) { |
| UA_assert(node != NULL); |
| UA_assert(type != NULL); |
| |
| /* If this is a variabletype, there must be no instances or subtypes of it |
| * when we do the change */ |
| if(node->nodeClass == UA_NODECLASS_VARIABLETYPE && |
| UA_Node_hasSubTypeOrInstances((UA_Node*)node)) { |
| UA_LOG_INFO(&server->config.logger, UA_LOGCATEGORY_SERVER, |
| "Cannot change a variable type with existing instances"); |
| return UA_STATUSCODE_BADINTERNALERROR; |
| } |
| |
| /* Check that the array dimensions match with the valuerank */ |
| if(!compatibleValueRankArrayDimensions(server, session, node->valueRank, arrayDimensionsSize)) { |
| UA_LOG_DEBUG(&server->config.logger, UA_LOGCATEGORY_SERVER, |
| "Cannot write the ArrayDimensions. The ValueRank does not match."); |
| return UA_STATUSCODE_BADTYPEMISMATCH; |
| } |
| |
| /* Check if the array dimensions match with the wildcards in the |
| * variabletype (dimension length 0) */ |
| if(type->arrayDimensions && |
| !compatibleArrayDimensions(type->arrayDimensionsSize, type->arrayDimensions, |
| arrayDimensionsSize, arrayDimensions)) { |
| UA_LOG_DEBUG(&server->config.logger, UA_LOGCATEGORY_SERVER, |
| "Array dimensions in the variable type do not match"); |
| return UA_STATUSCODE_BADTYPEMISMATCH; |
| } |
| |
| /* Check if the current value is compatible with the array dimensions */ |
| UA_DataValue value; |
| UA_DataValue_init(&value); |
| UA_StatusCode retval = readValueAttribute(server, session, node, &value); |
| if(retval != UA_STATUSCODE_GOOD) |
| return retval; |
| if(value.hasValue) { |
| if(!compatibleValueArrayDimensions(&value.value, arrayDimensionsSize, arrayDimensions)) |
| retval = UA_STATUSCODE_BADTYPEMISMATCH; |
| UA_DataValue_deleteMembers(&value); |
| if(retval != UA_STATUSCODE_GOOD) { |
| UA_LOG_DEBUG(&server->config.logger, UA_LOGCATEGORY_SERVER, |
| "Array dimensions in the current value do not match"); |
| return retval; |
| } |
| } |
| |
| /* Ok, apply */ |
| UA_UInt32 *oldArrayDimensions = node->arrayDimensions; |
| size_t oldArrayDimensionsSize = node->arrayDimensionsSize; |
| retval = UA_Array_copy(arrayDimensions, arrayDimensionsSize, |
| (void**)&node->arrayDimensions, |
| &UA_TYPES[UA_TYPES_UINT32]); |
| if(retval != UA_STATUSCODE_GOOD) |
| return retval; |
| UA_Array_delete(oldArrayDimensions, oldArrayDimensionsSize, &UA_TYPES[UA_TYPES_UINT32]); |
| node->arrayDimensionsSize = arrayDimensionsSize; |
| return UA_STATUSCODE_GOOD; |
| } |
| |
| /* Stack layout: ... | node | type */ |
| static UA_StatusCode |
| writeValueRankAttribute(UA_Server *server, UA_Session *session, |
| UA_VariableNode *node, const UA_VariableTypeNode *type, |
| UA_Int32 valueRank) { |
| UA_assert(node != NULL); |
| UA_assert(type != NULL); |
| |
| UA_Int32 constraintValueRank = type->valueRank; |
| |
| /* If this is a variabletype, there must be no instances or subtypes of it |
| * when we do the change */ |
| if(node->nodeClass == UA_NODECLASS_VARIABLETYPE && |
| UA_Node_hasSubTypeOrInstances((const UA_Node*)node)) |
| return UA_STATUSCODE_BADINTERNALERROR; |
| |
| /* Check if the valuerank of the variabletype allows the change. */ |
| if(!compatibleValueRanks(valueRank, constraintValueRank)) |
| return UA_STATUSCODE_BADTYPEMISMATCH; |
| |
| /* Check if the new valuerank is compatible with the array dimensions. Use |
| * the read service to handle data sources. */ |
| size_t arrayDims = node->arrayDimensionsSize; |
| if(arrayDims == 0) { |
| /* the value could be an array with no arrayDimensions defined. |
| dimensions zero indicate a scalar for compatibleValueRankArrayDimensions. */ |
| UA_DataValue value; |
| UA_DataValue_init(&value); |
| UA_StatusCode retval = readValueAttribute(server, session, node, &value); |
| if(retval != UA_STATUSCODE_GOOD) |
| return retval; |
| if(!value.hasValue || !value.value.type) { |
| /* no value -> apply */ |
| node->valueRank = valueRank; |
| return UA_STATUSCODE_GOOD; |
| } |
| if(!UA_Variant_isScalar(&value.value)) |
| arrayDims = 1; |
| UA_DataValue_deleteMembers(&value); |
| } |
| if(!compatibleValueRankArrayDimensions(server, session, valueRank, arrayDims)) |
| return UA_STATUSCODE_BADTYPEMISMATCH; |
| |
| /* All good, apply the change */ |
| node->valueRank = valueRank; |
| return UA_STATUSCODE_GOOD; |
| } |
| |
| static UA_StatusCode |
| writeDataTypeAttribute(UA_Server *server, UA_Session *session, |
| UA_VariableNode *node, const UA_VariableTypeNode *type, |
| const UA_NodeId *dataType) { |
| UA_assert(node != NULL); |
| UA_assert(type != NULL); |
| |
| /* If this is a variabletype, there must be no instances or subtypes of it |
| when we do the change */ |
| if(node->nodeClass == UA_NODECLASS_VARIABLETYPE && |
| UA_Node_hasSubTypeOrInstances((const UA_Node*)node)) |
| return UA_STATUSCODE_BADINTERNALERROR; |
| |
| /* Does the new type match the constraints of the variabletype? */ |
| if(!compatibleDataType(server, dataType, &type->dataType, false)) |
| return UA_STATUSCODE_BADTYPEMISMATCH; |
| |
| /* Check if the current value would match the new type */ |
| UA_DataValue value; |
| UA_DataValue_init(&value); |
| UA_StatusCode retval = readValueAttribute(server, session, node, &value); |
| if(retval != UA_STATUSCODE_GOOD) |
| return retval; |
| if(value.hasValue) { |
| if(!compatibleValue(server, session, dataType, node->valueRank, |
| node->arrayDimensionsSize, node->arrayDimensions, |
| &value.value, NULL)) |
| retval = UA_STATUSCODE_BADTYPEMISMATCH; |
| UA_DataValue_deleteMembers(&value); |
| if(retval != UA_STATUSCODE_GOOD) { |
| UA_LOG_DEBUG(&server->config.logger, UA_LOGCATEGORY_SERVER, |
| "The current value does not match the new data type"); |
| return retval; |
| } |
| } |
| |
| /* Replace the datatype nodeid */ |
| UA_NodeId dtCopy = node->dataType; |
| retval = UA_NodeId_copy(dataType, &node->dataType); |
| if(retval != UA_STATUSCODE_GOOD) { |
| node->dataType = dtCopy; |
| return retval; |
| } |
| UA_NodeId_deleteMembers(&dtCopy); |
| return UA_STATUSCODE_GOOD; |
| } |
| |
| static UA_StatusCode |
| writeValueAttributeWithoutRange(UA_VariableNode *node, const UA_DataValue *value) { |
| UA_DataValue new_value; |
| UA_StatusCode retval = UA_DataValue_copy(value, &new_value); |
| if(retval != UA_STATUSCODE_GOOD) |
| return retval; |
| UA_DataValue_deleteMembers(&node->value.data.value); |
| node->value.data.value = new_value; |
| return UA_STATUSCODE_GOOD; |
| } |
| |
| static UA_StatusCode |
| writeValueAttributeWithRange(UA_VariableNode *node, const UA_DataValue *value, |
| const UA_NumericRange *rangeptr) { |
| /* Value on both sides? */ |
| if(value->status != node->value.data.value.status || |
| !value->hasValue || !node->value.data.value.hasValue) |
| return UA_STATUSCODE_BADINDEXRANGEINVALID; |
| |
| /* Make scalar a one-entry array for range matching */ |
| UA_Variant editableValue; |
| const UA_Variant *v = &value->value; |
| if(UA_Variant_isScalar(&value->value)) { |
| editableValue = value->value; |
| editableValue.arrayLength = 1; |
| v = &editableValue; |
| } |
| |
| /* Check that the type is an exact match and not only "compatible" */ |
| if(!node->value.data.value.value.type || !v->type || |
| !UA_NodeId_equal(&node->value.data.value.value.type->typeId, |
| &v->type->typeId)) |
| return UA_STATUSCODE_BADTYPEMISMATCH; |
| |
| /* Write the value */ |
| UA_StatusCode retval = UA_Variant_setRangeCopy(&node->value.data.value.value, |
| v->data, v->arrayLength, *rangeptr); |
| if(retval != UA_STATUSCODE_GOOD) |
| return retval; |
| |
| /* Write the status and timestamps */ |
| node->value.data.value.hasStatus = value->hasStatus; |
| node->value.data.value.status = value->status; |
| node->value.data.value.hasSourceTimestamp = value->hasSourceTimestamp; |
| node->value.data.value.sourceTimestamp = value->sourceTimestamp; |
| node->value.data.value.hasSourcePicoseconds = value->hasSourcePicoseconds; |
| node->value.data.value.sourcePicoseconds = value->sourcePicoseconds; |
| return UA_STATUSCODE_GOOD; |
| } |
| |
| /* Stack layout: ... | node */ |
| static UA_StatusCode |
| writeValueAttribute(UA_Server *server, UA_Session *session, |
| UA_VariableNode *node, const UA_DataValue *value, |
| const UA_String *indexRange) { |
| UA_assert(node != NULL); |
| |
| /* Parse the range */ |
| UA_NumericRange range; |
| UA_NumericRange *rangeptr = NULL; |
| UA_StatusCode retval = UA_STATUSCODE_GOOD; |
| if(indexRange && indexRange->length > 0) { |
| retval = UA_NumericRange_parseFromString(&range, indexRange); |
| if(retval != UA_STATUSCODE_GOOD) |
| return retval; |
| rangeptr = ⦥ |
| } |
| |
| /* Created an editable version. The data is not touched. Only the variant |
| * "container". */ |
| UA_DataValue adjustedValue = *value; |
| |
| /* Type checking. May change the type of editableValue */ |
| if(value->hasValue && value->value.type) { |
| adjustValue(server, &adjustedValue.value, &node->dataType); |
| |
| /* The value may be an extension object, especially the nodeset compiler |
| * uses extension objects to write variable values. If value is an |
| * extension object we check if the current node value is also an |
| * extension object. */ |
| const UA_NodeId nodeDataType = UA_NODEID_NUMERIC(0, UA_NS0ID_STRUCTURE); |
| const UA_NodeId *nodeDataTypePtr = &node->dataType; |
| if(value->value.type->typeId.identifierType == UA_NODEIDTYPE_NUMERIC && |
| value->value.type->typeId.identifier.numeric == UA_NS0ID_STRUCTURE) |
| nodeDataTypePtr = &nodeDataType; |
| |
| if(!compatibleValue(server, session, nodeDataTypePtr, node->valueRank, |
| node->arrayDimensionsSize, node->arrayDimensions, |
| &adjustedValue.value, rangeptr)) { |
| if(rangeptr) |
| UA_free(range.dimensions); |
| return UA_STATUSCODE_BADTYPEMISMATCH; |
| } |
| } |
| |
| /* Set the source timestamp if there is none */ |
| UA_DateTime now = UA_DateTime_now(); |
| if(!adjustedValue.hasSourceTimestamp) { |
| adjustedValue.sourceTimestamp = now; |
| adjustedValue.hasSourceTimestamp = true; |
| } |
| |
| if(!adjustedValue.hasServerTimestamp) { |
| adjustedValue.serverTimestamp = now; |
| adjustedValue.hasServerTimestamp = true; |
| } |
| |
| /* Ok, do it */ |
| if(node->valueSource == UA_VALUESOURCE_DATA) { |
| if(!rangeptr) |
| retval = writeValueAttributeWithoutRange(node, &adjustedValue); |
| else |
| retval = writeValueAttributeWithRange(node, &adjustedValue, rangeptr); |
| |
| #ifdef UA_ENABLE_HISTORIZING |
| /* node is a UA_VariableNode*, but it may also point to a UA_VariableTypeNode */ |
| /* UA_VariableTypeNode doesn't have the historizing attribute */ |
| if(retval == UA_STATUSCODE_GOOD && node->nodeClass == UA_NODECLASS_VARIABLE && |
| server->config.historyDatabase.setValue) { |
| UA_UNLOCK(server->serviceMutex); |
| server->config.historyDatabase.setValue(server, server->config.historyDatabase.context, |
| &session->sessionId, session->sessionHandle, |
| &node->nodeId, node->historizing, &adjustedValue); |
| UA_LOCK(server->serviceMutex); |
| } |
| #endif |
| /* Callback after writing */ |
| if(retval == UA_STATUSCODE_GOOD && node->value.data.callback.onWrite) { |
| UA_UNLOCK(server->serviceMutex) |
| node->value.data.callback.onWrite(server, &session->sessionId, |
| session->sessionHandle, &node->nodeId, |
| node->context, rangeptr, |
| &adjustedValue); |
| UA_LOCK(server->serviceMutex); |
| |
| } |
| } else { |
| if(node->value.dataSource.write) { |
| UA_UNLOCK(server->serviceMutex); |
| retval = node->value.dataSource.write(server, &session->sessionId, |
| session->sessionHandle, &node->nodeId, |
| node->context, rangeptr, &adjustedValue); |
| UA_LOCK(server->serviceMutex); |
| } else { |
| retval = UA_STATUSCODE_BADWRITENOTSUPPORTED; |
| } |
| } |
| |
| /* Clean up */ |
| if(rangeptr) |
| UA_free(range.dimensions); |
| return retval; |
| } |
| |
| static UA_StatusCode |
| writeIsAbstractAttribute(UA_Node *node, UA_Boolean value) { |
| switch(node->nodeClass) { |
| case UA_NODECLASS_OBJECTTYPE: |
| ((UA_ObjectTypeNode*)node)->isAbstract = value; |
| break; |
| case UA_NODECLASS_REFERENCETYPE: |
| ((UA_ReferenceTypeNode*)node)->isAbstract = value; |
| break; |
| case UA_NODECLASS_VARIABLETYPE: |
| ((UA_VariableTypeNode*)node)->isAbstract = value; |
| break; |
| case UA_NODECLASS_DATATYPE: |
| ((UA_DataTypeNode*)node)->isAbstract = value; |
| break; |
| default: |
| return UA_STATUSCODE_BADNODECLASSINVALID; |
| } |
| return UA_STATUSCODE_GOOD; |
| } |
| |
| /*****************/ |
| /* Write Service */ |
| /*****************/ |
| |
| #define CHECK_DATATYPE_SCALAR(EXP_DT) \ |
| if(!wvalue->value.hasValue || \ |
| &UA_TYPES[UA_TYPES_##EXP_DT] != wvalue->value.value.type || \ |
| !UA_Variant_isScalar(&wvalue->value.value)) { \ |
| retval = UA_STATUSCODE_BADTYPEMISMATCH; \ |
| break; \ |
| } |
| |
| #define CHECK_DATATYPE_ARRAY(EXP_DT) \ |
| if(!wvalue->value.hasValue || \ |
| &UA_TYPES[UA_TYPES_##EXP_DT] != wvalue->value.value.type || \ |
| UA_Variant_isScalar(&wvalue->value.value)) { \ |
| retval = UA_STATUSCODE_BADTYPEMISMATCH; \ |
| break; \ |
| } |
| |
| #define CHECK_NODECLASS_WRITE(CLASS) \ |
| if((node->nodeClass & (CLASS)) == 0) { \ |
| retval = UA_STATUSCODE_BADNODECLASSINVALID; \ |
| break; \ |
| } |
| |
| #define CHECK_USERWRITEMASK(mask) \ |
| if(!(userWriteMask & (mask))) { \ |
| retval = UA_STATUSCODE_BADUSERACCESSDENIED; \ |
| break; \ |
| } |
| |
| #define GET_NODETYPE \ |
| type = (const UA_VariableTypeNode*) \ |
| getNodeType(server, node); \ |
| if(!type) { \ |
| retval = UA_STATUSCODE_BADTYPEMISMATCH; \ |
| break; \ |
| } |
| |
| /* This function implements the main part of the write service and operates on a |
| copy of the node (not in single-threaded mode). */ |
| static UA_StatusCode |
| copyAttributeIntoNode(UA_Server *server, UA_Session *session, |
| UA_Node *node, const UA_WriteValue *wvalue) { |
| const void *value = wvalue->value.value.data; |
| UA_UInt32 userWriteMask = getUserWriteMask(server, session, node); |
| UA_StatusCode retval = UA_STATUSCODE_GOOD; |
| |
| const UA_VariableTypeNode *type; |
| |
| switch(wvalue->attributeId) { |
| case UA_ATTRIBUTEID_NODEID: |
| case UA_ATTRIBUTEID_NODECLASS: |
| case UA_ATTRIBUTEID_USERWRITEMASK: |
| case UA_ATTRIBUTEID_USERACCESSLEVEL: |
| case UA_ATTRIBUTEID_USEREXECUTABLE: |
| retval = UA_STATUSCODE_BADWRITENOTSUPPORTED; |
| break; |
| case UA_ATTRIBUTEID_BROWSENAME: |
| CHECK_USERWRITEMASK(UA_WRITEMASK_BROWSENAME); |
| CHECK_DATATYPE_SCALAR(QUALIFIEDNAME); |
| UA_QualifiedName_deleteMembers(&node->browseName); |
| UA_QualifiedName_copy((const UA_QualifiedName *)value, &node->browseName); |
| break; |
| case UA_ATTRIBUTEID_DISPLAYNAME: |
| CHECK_USERWRITEMASK(UA_WRITEMASK_DISPLAYNAME); |
| CHECK_DATATYPE_SCALAR(LOCALIZEDTEXT); |
| UA_LocalizedText_deleteMembers(&node->displayName); |
| UA_LocalizedText_copy((const UA_LocalizedText *)value, &node->displayName); |
| break; |
| case UA_ATTRIBUTEID_DESCRIPTION: |
| CHECK_USERWRITEMASK(UA_WRITEMASK_DESCRIPTION); |
| CHECK_DATATYPE_SCALAR(LOCALIZEDTEXT); |
| UA_LocalizedText_deleteMembers(&node->description); |
| UA_LocalizedText_copy((const UA_LocalizedText *)value, &node->description); |
| break; |
| case UA_ATTRIBUTEID_WRITEMASK: |
| CHECK_USERWRITEMASK(UA_WRITEMASK_WRITEMASK); |
| CHECK_DATATYPE_SCALAR(UINT32); |
| node->writeMask = *(const UA_UInt32*)value; |
| break; |
| case UA_ATTRIBUTEID_ISABSTRACT: |
| CHECK_USERWRITEMASK(UA_WRITEMASK_ISABSTRACT); |
| CHECK_DATATYPE_SCALAR(BOOLEAN); |
| retval = writeIsAbstractAttribute(node, *(const UA_Boolean*)value); |
| break; |
| case UA_ATTRIBUTEID_SYMMETRIC: |
| CHECK_NODECLASS_WRITE(UA_NODECLASS_REFERENCETYPE); |
| CHECK_USERWRITEMASK(UA_WRITEMASK_SYMMETRIC); |
| CHECK_DATATYPE_SCALAR(BOOLEAN); |
| ((UA_ReferenceTypeNode*)node)->symmetric = *(const UA_Boolean*)value; |
| break; |
| case UA_ATTRIBUTEID_INVERSENAME: |
| CHECK_NODECLASS_WRITE(UA_NODECLASS_REFERENCETYPE); |
| CHECK_USERWRITEMASK(UA_WRITEMASK_INVERSENAME); |
| CHECK_DATATYPE_SCALAR(LOCALIZEDTEXT); |
| UA_LocalizedText_deleteMembers(&((UA_ReferenceTypeNode*)node)->inverseName); |
| UA_LocalizedText_copy((const UA_LocalizedText *)value, |
| &((UA_ReferenceTypeNode*)node)->inverseName); |
| break; |
| case UA_ATTRIBUTEID_CONTAINSNOLOOPS: |
| CHECK_NODECLASS_WRITE(UA_NODECLASS_VIEW); |
| CHECK_USERWRITEMASK(UA_WRITEMASK_CONTAINSNOLOOPS); |
| CHECK_DATATYPE_SCALAR(BOOLEAN); |
| ((UA_ViewNode*)node)->containsNoLoops = *(const UA_Boolean*)value; |
| break; |
| case UA_ATTRIBUTEID_EVENTNOTIFIER: |
| CHECK_NODECLASS_WRITE(UA_NODECLASS_VIEW | UA_NODECLASS_OBJECT); |
| CHECK_USERWRITEMASK(UA_WRITEMASK_EVENTNOTIFIER); |
| CHECK_DATATYPE_SCALAR(BYTE); |
| if(node->nodeClass == UA_NODECLASS_VIEW) { |
| ((UA_ViewNode*)node)->eventNotifier = *(const UA_Byte*)value; |
| } else { |
| ((UA_ObjectNode*)node)->eventNotifier = *(const UA_Byte*)value; |
| } |
| break; |
| case UA_ATTRIBUTEID_VALUE: |
| CHECK_NODECLASS_WRITE(UA_NODECLASS_VARIABLE | UA_NODECLASS_VARIABLETYPE); |
| if(node->nodeClass == UA_NODECLASS_VARIABLE) { |
| /* The access to a value variable is granted via the AccessLevel |
| * and UserAccessLevel attributes */ |
| UA_Byte accessLevel = getAccessLevel(server, session, (const UA_VariableNode*)node); |
| if(!(accessLevel & (UA_ACCESSLEVELMASK_WRITE))) { |
| retval = UA_STATUSCODE_BADNOTWRITABLE; |
| break; |
| } |
| accessLevel = getUserAccessLevel(server, session, |
| (const UA_VariableNode*)node); |
| if(!(accessLevel & (UA_ACCESSLEVELMASK_WRITE))) { |
| retval = UA_STATUSCODE_BADUSERACCESSDENIED; |
| break; |
| } |
| } else { /* UA_NODECLASS_VARIABLETYPE */ |
| CHECK_USERWRITEMASK(UA_WRITEMASK_VALUEFORVARIABLETYPE); |
| } |
| retval = writeValueAttribute(server, session, (UA_VariableNode*)node, |
| &wvalue->value, &wvalue->indexRange); |
| break; |
| case UA_ATTRIBUTEID_DATATYPE: |
| CHECK_NODECLASS_WRITE(UA_NODECLASS_VARIABLE | UA_NODECLASS_VARIABLETYPE); |
| CHECK_USERWRITEMASK(UA_WRITEMASK_DATATYPE); |
| CHECK_DATATYPE_SCALAR(NODEID); |
| GET_NODETYPE |
| retval = writeDataTypeAttribute(server, session, (UA_VariableNode*)node, |
| type, (const UA_NodeId*)value); |
| UA_Nodestore_releaseNode(server->nsCtx, (const UA_Node*)type); |
| break; |
| case UA_ATTRIBUTEID_VALUERANK: |
| CHECK_NODECLASS_WRITE(UA_NODECLASS_VARIABLE | UA_NODECLASS_VARIABLETYPE); |
| CHECK_USERWRITEMASK(UA_WRITEMASK_VALUERANK); |
| CHECK_DATATYPE_SCALAR(INT32); |
| GET_NODETYPE |
| retval = writeValueRankAttribute(server, session, (UA_VariableNode*)node, |
| type, *(const UA_Int32*)value); |
| UA_Nodestore_releaseNode(server->nsCtx, (const UA_Node*)type); |
| break; |
| case UA_ATTRIBUTEID_ARRAYDIMENSIONS: |
| CHECK_NODECLASS_WRITE(UA_NODECLASS_VARIABLE | UA_NODECLASS_VARIABLETYPE); |
| CHECK_USERWRITEMASK(UA_WRITEMASK_ARRRAYDIMENSIONS); |
| CHECK_DATATYPE_ARRAY(UINT32); |
| GET_NODETYPE |
| retval = writeArrayDimensionsAttribute(server, session, (UA_VariableNode*)node, |
| type, wvalue->value.value.arrayLength, |
| (UA_UInt32 *)wvalue->value.value.data); |
| UA_Nodestore_releaseNode(server->nsCtx, (const UA_Node*)type); |
| break; |
| case UA_ATTRIBUTEID_ACCESSLEVEL: |
| CHECK_NODECLASS_WRITE(UA_NODECLASS_VARIABLE); |
| CHECK_USERWRITEMASK(UA_WRITEMASK_ACCESSLEVEL); |
| CHECK_DATATYPE_SCALAR(BYTE); |
| ((UA_VariableNode*)node)->accessLevel = *(const UA_Byte*)value; |
| break; |
| case UA_ATTRIBUTEID_MINIMUMSAMPLINGINTERVAL: |
| CHECK_NODECLASS_WRITE(UA_NODECLASS_VARIABLE); |
| CHECK_USERWRITEMASK(UA_WRITEMASK_MINIMUMSAMPLINGINTERVAL); |
| CHECK_DATATYPE_SCALAR(DOUBLE); |
| ((UA_VariableNode*)node)->minimumSamplingInterval = *(const UA_Double*)value; |
| break; |
| case UA_ATTRIBUTEID_HISTORIZING: |
| CHECK_NODECLASS_WRITE(UA_NODECLASS_VARIABLE); |
| CHECK_USERWRITEMASK(UA_WRITEMASK_HISTORIZING); |
| CHECK_DATATYPE_SCALAR(BOOLEAN); |
| ((UA_VariableNode*)node)->historizing = *(const UA_Boolean*)value; |
| break; |
| case UA_ATTRIBUTEID_EXECUTABLE: |
| CHECK_NODECLASS_WRITE(UA_NODECLASS_METHOD); |
| CHECK_USERWRITEMASK(UA_WRITEMASK_EXECUTABLE); |
| CHECK_DATATYPE_SCALAR(BOOLEAN); |
| ((UA_MethodNode*)node)->executable = *(const UA_Boolean*)value; |
| break; |
| default: |
| retval = UA_STATUSCODE_BADATTRIBUTEIDINVALID; |
| break; |
| } |
| if(retval != UA_STATUSCODE_GOOD) |
| UA_LOG_INFO_SESSION(&server->config.logger, session, |
| "WriteRequest returned status code %s", |
| UA_StatusCode_name(retval)); |
| return retval; |
| } |
| |
| static void |
| Operation_Write(UA_Server *server, UA_Session *session, void *context, |
| UA_WriteValue *wv, UA_StatusCode *result) { |
| *result = UA_Server_editNode(server, session, &wv->nodeId, |
| (UA_EditNodeCallback)copyAttributeIntoNode, wv); |
| } |
| |
| void |
| Service_Write(UA_Server *server, UA_Session *session, |
| const UA_WriteRequest *request, |
| UA_WriteResponse *response) { |
| UA_LOG_DEBUG_SESSION(&server->config.logger, session, |
| "Processing WriteRequest"); |
| UA_LOCK_ASSERT(server->serviceMutex, 1); |
| |
| if(server->config.maxNodesPerWrite != 0 && |
| request->nodesToWriteSize > server->config.maxNodesPerWrite) { |
| response->responseHeader.serviceResult = UA_STATUSCODE_BADTOOMANYOPERATIONS; |
| return; |
| } |
| |
| UA_LOCK_ASSERT(server->serviceMutex, 1); |
| |
| response->responseHeader.serviceResult = |
| UA_Server_processServiceOperations(server, session, (UA_ServiceOperation)Operation_Write, NULL, |
| &request->nodesToWriteSize, &UA_TYPES[UA_TYPES_WRITEVALUE], |
| &response->resultsSize, &UA_TYPES[UA_TYPES_STATUSCODE]); |
| } |
| |
| UA_StatusCode |
| writeWithSession(UA_Server *server, UA_Session *session, |
| const UA_WriteValue *value) { |
| return UA_Server_editNode(server, session, &value->nodeId, |
| (UA_EditNodeCallback)copyAttributeIntoNode, |
| /* casting away const qualifier because callback uses const anyway */ |
| (UA_WriteValue *)(uintptr_t)value); |
| } |
| |
| UA_StatusCode |
| writeAttribute(UA_Server *server, const UA_WriteValue *value) { |
| return UA_Server_editNode(server, &server->adminSession, &value->nodeId, |
| (UA_EditNodeCallback)copyAttributeIntoNode, |
| /* casting away const qualifier because callback uses const anyway */ |
| (UA_WriteValue *)(uintptr_t)value); |
| } |
| |
| UA_StatusCode |
| UA_Server_write(UA_Server *server, const UA_WriteValue *value) { |
| UA_LOCK(server->serviceMutex); |
| UA_StatusCode retval = writeAttribute(server, value); |
| UA_UNLOCK(server->serviceMutex); |
| return retval; |
| } |
| |
| UA_StatusCode |
| writeWithWriteValue(UA_Server *server, const UA_NodeId *nodeId, |
| const UA_AttributeId attributeId, |
| const UA_DataType *attr_type, |
| const void *attr) { |
| UA_WriteValue wvalue; |
| UA_WriteValue_init(&wvalue); |
| wvalue.nodeId = *nodeId; |
| wvalue.attributeId = attributeId; |
| wvalue.value.hasValue = true; |
| if(attr_type != &UA_TYPES[UA_TYPES_VARIANT]) { |
| /* hacked cast. the target WriteValue is used as const anyway */ |
| UA_Variant_setScalar(&wvalue.value.value, |
| (void*)(uintptr_t)attr, attr_type); |
| } else { |
| wvalue.value.value = *(const UA_Variant*)attr; |
| } |
| return writeAttribute(server, &wvalue); |
| } |
| |
| /* Convenience function to be wrapped into inline functions */ |
| UA_StatusCode |
| __UA_Server_write(UA_Server *server, const UA_NodeId *nodeId, |
| const UA_AttributeId attributeId, |
| const UA_DataType *attr_type, |
| const void *attr) { |
| UA_LOCK(server->serviceMutex); |
| UA_StatusCode retval = writeWithWriteValue(server, nodeId, attributeId, attr_type, attr); |
| UA_UNLOCK(server->serviceMutex); |
| return retval; |
| } |
| |
| #ifdef UA_ENABLE_HISTORIZING |
| void |
| Service_HistoryRead(UA_Server *server, UA_Session *session, |
| const UA_HistoryReadRequest *request, |
| UA_HistoryReadResponse *response) { |
| if(request->historyReadDetails.encoding != UA_EXTENSIONOBJECT_DECODED) { |
| response->responseHeader.serviceResult = UA_STATUSCODE_BADNOTSUPPORTED; |
| return; |
| } |
| |
| if(request->historyReadDetails.content.decoded.type != &UA_TYPES[UA_TYPES_READRAWMODIFIEDDETAILS]) { |
| /* TODO handle more request->historyReadDetails.content.decoded.type types */ |
| response->responseHeader.serviceResult = UA_STATUSCODE_BADHISTORYOPERATIONUNSUPPORTED; |
| return; |
| } |
| |
| /* History read with ReadRawModifiedDetails */ |
| UA_ReadRawModifiedDetails * details = (UA_ReadRawModifiedDetails*) |
| request->historyReadDetails.content.decoded.data; |
| if(details->isReadModified) { |
| // TODO add server->config.historyReadService.read_modified |
| response->responseHeader.serviceResult = UA_STATUSCODE_BADHISTORYOPERATIONUNSUPPORTED; |
| return; |
| } |
| |
| /* Something to do? */ |
| if(request->nodesToReadSize == 0) { |
| response->responseHeader.serviceResult = UA_STATUSCODE_BADNOTHINGTODO; |
| return; |
| } |
| |
| /* Check if there are too many operations */ |
| if(server->config.maxNodesPerRead != 0 && |
| request->nodesToReadSize > server->config.maxNodesPerRead) { |
| response->responseHeader.serviceResult = UA_STATUSCODE_BADTOOMANYOPERATIONS; |
| return; |
| } |
| |
| /* The history database is not configured */ |
| if(!server->config.historyDatabase.readRaw) { |
| response->responseHeader.serviceResult = UA_STATUSCODE_BADHISTORYOPERATIONUNSUPPORTED; |
| return; |
| } |
| |
| /* Allocate a temporary array to forward the result pointers to the |
| * backend */ |
| UA_HistoryData ** historyData = (UA_HistoryData **) |
| UA_calloc(request->nodesToReadSize, sizeof(UA_HistoryData*)); |
| if(!historyData) { |
| response->responseHeader.serviceResult = UA_STATUSCODE_BADOUTOFMEMORY; |
| return; |
| } |
| |
| /* Allocate the results array */ |
| response->results = (UA_HistoryReadResult*)UA_Array_new(request->nodesToReadSize, |
| &UA_TYPES[UA_TYPES_HISTORYREADRESULT]); |
| if(!response->results) { |
| UA_free(historyData); |
| response->responseHeader.serviceResult = UA_STATUSCODE_BADOUTOFMEMORY; |
| return; |
| } |
| response->resultsSize = request->nodesToReadSize; |
| |
| for(size_t i = 0; i < response->resultsSize; ++i) { |
| UA_HistoryData * data = UA_HistoryData_new(); |
| response->results[i].historyData.encoding = UA_EXTENSIONOBJECT_DECODED; |
| response->results[i].historyData.content.decoded.type = &UA_TYPES[UA_TYPES_HISTORYDATA]; |
| response->results[i].historyData.content.decoded.data = data; |
| historyData[i] = data; |
| } |
| UA_UNLOCK(server->serviceMutex); |
| server->config.historyDatabase.readRaw(server, server->config.historyDatabase.context, |
| &session->sessionId, session->sessionHandle, |
| &request->requestHeader, details, |
| request->timestampsToReturn, |
| request->releaseContinuationPoints, |
| request->nodesToReadSize, request->nodesToRead, |
| response, historyData); |
| UA_LOCK(server->serviceMutex); |
| UA_free(historyData); |
| } |
| |
| void |
| Service_HistoryUpdate(UA_Server *server, UA_Session *session, |
| const UA_HistoryUpdateRequest *request, |
| UA_HistoryUpdateResponse *response) { |
| response->resultsSize = request->historyUpdateDetailsSize; |
| response->results = (UA_HistoryUpdateResult*)UA_Array_new(response->resultsSize, &UA_TYPES[UA_TYPES_HISTORYUPDATERESULT]); |
| if (!response->results) { |
| response->resultsSize = 0; |
| response->responseHeader.serviceResult = UA_STATUSCODE_BADOUTOFMEMORY; |
| return; |
| } |
| for (size_t i = 0; i < request->historyUpdateDetailsSize; ++i) { |
| UA_HistoryUpdateResult_init(&response->results[i]); |
| if(request->historyUpdateDetails[i].encoding != UA_EXTENSIONOBJECT_DECODED) { |
| response->results[i].statusCode = UA_STATUSCODE_BADNOTSUPPORTED; |
| continue; |
| } |
| if (request->historyUpdateDetails[i].content.decoded.type |
| == &UA_TYPES[UA_TYPES_UPDATEDATADETAILS]) { |
| if (server->config.historyDatabase.updateData) { |
| UA_UNLOCK(server->serviceMutex); |
| server->config.historyDatabase.updateData(server, |
| server->config.historyDatabase.context, |
| &session->sessionId, session->sessionHandle, |
| &request->requestHeader, |
| (UA_UpdateDataDetails*)request->historyUpdateDetails[i].content.decoded.data, |
| &response->results[i]); |
| UA_LOCK(server->serviceMutex); |
| } else { |
| response->results[i].statusCode = UA_STATUSCODE_BADNOTSUPPORTED; |
| } |
| continue; |
| } else |
| if (request->historyUpdateDetails[i].content.decoded.type |
| == &UA_TYPES[UA_TYPES_DELETERAWMODIFIEDDETAILS]) { |
| if (server->config.historyDatabase.deleteRawModified) { |
| UA_UNLOCK(server->serviceMutex); |
| server->config.historyDatabase.deleteRawModified(server, |
| server->config.historyDatabase.context, |
| &session->sessionId, session->sessionHandle, |
| &request->requestHeader, |
| (UA_DeleteRawModifiedDetails*)request->historyUpdateDetails[i].content.decoded.data, |
| &response->results[i]); |
| UA_LOCK(server->serviceMutex); |
| } else { |
| response->results[i].statusCode = UA_STATUSCODE_BADNOTSUPPORTED; |
| } |
| continue; |
| } else { |
| response->results[i].statusCode = UA_STATUSCODE_BADNOTSUPPORTED; |
| continue; |
| } |
| } |
| response->responseHeader.serviceResult = UA_STATUSCODE_GOOD; |
| } |
| |
| #endif |
| |
| UA_StatusCode UA_EXPORT |
| UA_Server_writeObjectProperty(UA_Server *server, const UA_NodeId objectId, |
| const UA_QualifiedName propertyName, |
| const UA_Variant value) { |
| UA_RelativePathElement rpe; |
| UA_RelativePathElement_init(&rpe); |
| rpe.referenceTypeId = UA_NODEID_NUMERIC(0, UA_NS0ID_HASPROPERTY); |
| rpe.isInverse = false; |
| rpe.includeSubtypes = false; |
| rpe.targetName = propertyName; |
| |
| UA_BrowsePath bp; |
| UA_BrowsePath_init(&bp); |
| bp.startingNode = objectId; |
| bp.relativePath.elementsSize = 1; |
| bp.relativePath.elements = &rpe; |
| |
| UA_StatusCode retval; |
| UA_BrowsePathResult bpr = translateBrowsePathToNodeIds(server, &bp); |
| if(bpr.statusCode != UA_STATUSCODE_GOOD || bpr.targetsSize < 1) { |
| retval = bpr.statusCode; |
| UA_BrowsePathResult_deleteMembers(&bpr); |
| return retval; |
| } |
| |
| retval = UA_Server_writeValue(server, bpr.targets[0].targetId.nodeId, value); |
| |
| UA_BrowsePathResult_deleteMembers(&bpr); |
| return retval; |
| } |
| |
| UA_StatusCode UA_EXPORT |
| UA_Server_writeObjectProperty_scalar(UA_Server *server, const UA_NodeId objectId, |
| const UA_QualifiedName propertyName, |
| const void *value, const UA_DataType *type) { |
| UA_Variant var; |
| UA_Variant_init(&var); |
| UA_Variant_setScalar(&var, (void*)(uintptr_t)value, type); |
| return UA_Server_writeObjectProperty(server, objectId, propertyName, var); |
| } |