/src/open62541_15/src/server/ua_services_method.c
Line | Count | Source |
1 | | /* This Source Code Form is subject to the terms of the Mozilla Public |
2 | | * License, v. 2.0. If a copy of the MPL was not distributed with this |
3 | | * file, You can obtain one at http://mozilla.org/MPL/2.0/. |
4 | | * |
5 | | * Copyright 2015 (c) Chris Iatrou |
6 | | * Copyright 2015-2017 (c) Florian Palm |
7 | | * Copyright 2015-2018 (c) Fraunhofer IOSB (Author: Julius Pfrommer) |
8 | | * Copyright 2015-2016 (c) Sten Grüner |
9 | | * Copyright 2015 (c) Oleksiy Vasylyev |
10 | | * Copyright 2016 (c) LEvertz |
11 | | * Copyright 2017 (c) Stefan Profanter, fortiss GmbH |
12 | | * Copyright 2017 (c) Julian Grothoff |
13 | | * Copyright 2020 (c) Hilscher Gesellschaft für Systemautomation mbH (Author: Martin Lang) |
14 | | * Copyright (c) 2025 Pilz GmbH & Co. KG, Author: Marcel Patzlaff |
15 | | */ |
16 | | |
17 | | #include "ua_services.h" |
18 | | #include "ua_server_internal.h" |
19 | | |
20 | | #ifdef UA_ENABLE_METHODCALLS /* conditional compilation */ |
21 | | |
22 | 0 | #define UA_MAX_METHOD_ARGUMENTS 64 |
23 | | |
24 | | struct GetArgumentsNodeContext { |
25 | | UA_Server *server; |
26 | | UA_String withBrowseName; |
27 | | }; |
28 | | |
29 | | static void * |
30 | 0 | getArgumentsNodeCallback(void *context, UA_ReferenceTarget *t) { |
31 | 0 | struct GetArgumentsNodeContext *ctx = (struct GetArgumentsNodeContext*)context; |
32 | 0 | const UA_Node *refTarget = |
33 | 0 | UA_NODESTORE_GETFROMREF_SELECTIVE(ctx->server, t->targetId, |
34 | 0 | UA_NODEATTRIBUTESMASK_NODECLASS | |
35 | 0 | UA_NODEATTRIBUTESMASK_VALUE, |
36 | 0 | UA_REFERENCETYPESET_NONE, |
37 | 0 | UA_BROWSEDIRECTION_INVALID); |
38 | 0 | if(!refTarget) |
39 | 0 | return NULL; |
40 | 0 | if(refTarget->head.nodeClass == UA_NODECLASS_VARIABLE && |
41 | 0 | refTarget->head.browseName.namespaceIndex == 0 && |
42 | 0 | UA_String_equal(&ctx->withBrowseName, &refTarget->head.browseName.name)) { |
43 | 0 | return (void*)(uintptr_t)&refTarget->variableNode; |
44 | 0 | } |
45 | 0 | UA_NODESTORE_RELEASE(ctx->server, refTarget); |
46 | 0 | return NULL; |
47 | 0 | } |
48 | | |
49 | | static const UA_VariableNode * |
50 | | getArgumentsVariableNode(UA_Server *server, const UA_NodeHead *head, |
51 | 0 | UA_String withBrowseName) { |
52 | 0 | for(size_t i = 0; i < head->referencesSize; ++i) { |
53 | 0 | UA_NodeReferenceKind *rk = &head->references[i]; |
54 | 0 | if(rk->isInverse) |
55 | 0 | continue; |
56 | 0 | if(rk->referenceTypeIndex != UA_REFERENCETYPEINDEX_HASPROPERTY) |
57 | 0 | continue; |
58 | 0 | struct GetArgumentsNodeContext ctx; |
59 | 0 | ctx.server = server; |
60 | 0 | ctx.withBrowseName = withBrowseName; |
61 | 0 | return (const UA_VariableNode*) |
62 | 0 | UA_NodeReferenceKind_iterate(rk, getArgumentsNodeCallback, &ctx); |
63 | 0 | } |
64 | 0 | return NULL; |
65 | 0 | } |
66 | | |
67 | | /* inputArgumentResults has the length request->inputArgumentsSize */ |
68 | | static UA_StatusCode |
69 | | checkAdjustArguments(UA_Server *server, UA_Session *session, |
70 | | const UA_VariableNode *argRequirements, size_t argsSize, |
71 | 0 | UA_Variant *args, UA_StatusCode *inputArgumentResults) { |
72 | | /* Verify that we have a Variant containing UA_Argument (scalar or array) in |
73 | | * the "InputArguments" node */ |
74 | 0 | if(argRequirements->valueSourceType != UA_VALUESOURCETYPE_INTERNAL) |
75 | 0 | return UA_STATUSCODE_BADINTERNALERROR; |
76 | 0 | if(!argRequirements->valueSource.internal.value.hasValue) |
77 | 0 | return UA_STATUSCODE_BADINTERNALERROR; |
78 | | |
79 | 0 | const UA_Variant *argVal = &argRequirements->valueSource.internal.value.value; |
80 | 0 | if(argVal->type != &UA_TYPES[UA_TYPES_ARGUMENT]) |
81 | 0 | return UA_STATUSCODE_BADINTERNALERROR; |
82 | | |
83 | | /* Verify the number of arguments. A scalar argument value is interpreted as |
84 | | * an array of length 1. */ |
85 | 0 | size_t argReqsSize = argVal->arrayLength; |
86 | 0 | if(UA_Variant_isScalar(argVal)) |
87 | 0 | argReqsSize = 1; |
88 | 0 | if(argReqsSize > argsSize) |
89 | 0 | return UA_STATUSCODE_BADARGUMENTSMISSING; |
90 | 0 | if(argReqsSize < argsSize) |
91 | 0 | return UA_STATUSCODE_BADTOOMANYARGUMENTS; |
92 | | |
93 | | /* Type-check every argument against the definition */ |
94 | 0 | UA_StatusCode retval = UA_STATUSCODE_GOOD; |
95 | 0 | UA_Argument *argReqs = (UA_Argument*)argVal->data; |
96 | 0 | const char *reason; |
97 | 0 | for(size_t i = 0; i < argReqsSize; ++i) { |
98 | | /* Incompatible value. Try to correct the type if possible. */ |
99 | 0 | adjustValueType(server, &args[i], &argReqs[i].dataType); |
100 | | |
101 | | /* Check */ |
102 | 0 | if(!compatibleValue(server, session, &argReqs[i].dataType, argReqs[i].valueRank, |
103 | 0 | argReqs[i].arrayDimensionsSize, argReqs[i].arrayDimensions, |
104 | 0 | &args[i], NULL, &reason)) { |
105 | 0 | inputArgumentResults[i] = UA_STATUSCODE_BADTYPEMISMATCH; |
106 | 0 | retval = UA_STATUSCODE_BADINVALIDARGUMENT; |
107 | 0 | } |
108 | 0 | } |
109 | 0 | return retval; |
110 | 0 | } |
111 | | |
112 | | static UA_StatusCode |
113 | | checkMethodInTypeHierarchy(UA_Server *server, UA_Session *session, |
114 | | const UA_NodeId *contextId, const UA_NodeId *methodId, |
115 | 0 | const UA_QualifiedName *methodBrowseName) { |
116 | | /* Get the type hierarchy "upwards" from the context */ |
117 | 0 | UA_NodeId *typeHierarchy; |
118 | 0 | size_t typeHierarchySize; |
119 | 0 | UA_StatusCode res = |
120 | 0 | getTypeAndInterfaceHierarchy(server, contextId, false, |
121 | 0 | &typeHierarchy, &typeHierarchySize); |
122 | 0 | if(res != UA_STATUSCODE_GOOD) |
123 | 0 | return res; |
124 | | |
125 | | /* Check if the method is in the type hierarchy */ |
126 | 0 | for(size_t i = 0; i < typeHierarchySize; i++) { |
127 | 0 | UA_NodeId contained; |
128 | 0 | res = findChildByBrowsename(server, session, typeHierarchy[i], |
129 | 0 | UA_NODECLASS_METHOD, |
130 | 0 | UA_REFERENCETYPEINDEX_HASCOMPONENT, |
131 | 0 | UA_NS0ID(HASCOMPONENT), |
132 | 0 | methodBrowseName, &contained); |
133 | 0 | if(res == UA_STATUSCODE_BADNOTFOUND) |
134 | 0 | continue; |
135 | 0 | if(res != UA_STATUSCODE_GOOD) |
136 | 0 | break; |
137 | 0 | UA_Boolean found = UA_NodeId_equal(&contained, methodId); |
138 | 0 | UA_NodeId_clear(&contained); |
139 | 0 | res = (found) ? UA_STATUSCODE_GOOD : UA_STATUSCODE_BADNOTFOUND; |
140 | 0 | if(found) |
141 | 0 | break; |
142 | 0 | } |
143 | | |
144 | | /* Clean up and return */ |
145 | 0 | UA_Array_delete(typeHierarchy, typeHierarchySize, &UA_TYPES[UA_TYPES_NODEID]); |
146 | 0 | return res; |
147 | 0 | } |
148 | | |
149 | | static UA_StatusCode |
150 | | callWithResolvedMethodAndObject(UA_Server *server, UA_Session *session, |
151 | | const UA_CallMethodRequest *request, |
152 | | UA_CallMethodResult *result, |
153 | | const UA_MethodNode *resolvedMethod, |
154 | 0 | const UA_Node *callContext) { |
155 | | /* Is there a method to execute? */ |
156 | 0 | if(!resolvedMethod->method) |
157 | 0 | return UA_STATUSCODE_BADINTERNALERROR; |
158 | | |
159 | | /* Verify access rights */ |
160 | 0 | UA_Boolean executable = resolvedMethod->executable; |
161 | 0 | if(!executable) |
162 | 0 | return UA_STATUSCODE_BADNOTEXECUTABLE; |
163 | | |
164 | 0 | if(session != &server->adminSession) { |
165 | 0 | executable = server->config.accessControl. |
166 | 0 | getUserExecutableOnObject(server, &server->config.accessControl, |
167 | 0 | &session->sessionId, session->context, |
168 | 0 | &resolvedMethod->head.nodeId, |
169 | 0 | resolvedMethod->head.context, |
170 | 0 | &request->objectId, |
171 | 0 | callContext->head.context); |
172 | 0 | } |
173 | 0 | if(!executable) |
174 | 0 | return UA_STATUSCODE_BADUSERACCESSDENIED; |
175 | | |
176 | | /* The input arguments are const and not changed. We move the input |
177 | | * arguments to a secondary array that is mutable. This is used for small |
178 | | * adjustments on the type level during the type checking. But it has to be |
179 | | * ensured that the original array can still by _clear'ed after the methods |
180 | | * call. */ |
181 | 0 | if(request->inputArgumentsSize > UA_MAX_METHOD_ARGUMENTS) |
182 | 0 | return UA_STATUSCODE_BADTOOMANYARGUMENTS; |
183 | | |
184 | 0 | UA_Variant mutableInputArgs[UA_MAX_METHOD_ARGUMENTS]; |
185 | 0 | if(request->inputArgumentsSize > 0) |
186 | 0 | memcpy(mutableInputArgs, request->inputArguments, |
187 | 0 | sizeof(UA_Variant) * request->inputArgumentsSize); |
188 | | |
189 | | /* Allocate the inputArgumentResults array */ |
190 | 0 | result->inputArgumentResults = (UA_StatusCode*) |
191 | 0 | UA_Array_new(request->inputArgumentsSize, &UA_TYPES[UA_TYPES_STATUSCODE]); |
192 | 0 | if(!result->inputArgumentResults) |
193 | 0 | return UA_STATUSCODE_BADOUTOFMEMORY; |
194 | 0 | result->inputArgumentResultsSize = request->inputArgumentsSize; |
195 | | |
196 | | /* Type-check the input arguments */ |
197 | 0 | UA_StatusCode res = UA_STATUSCODE_GOOD; |
198 | 0 | const UA_VariableNode *inputArguments = |
199 | 0 | getArgumentsVariableNode(server, &resolvedMethod->head, |
200 | 0 | UA_STRING("InputArguments")); |
201 | 0 | if(inputArguments) { |
202 | 0 | res = checkAdjustArguments(server, session, |
203 | 0 | inputArguments, request->inputArgumentsSize, |
204 | 0 | mutableInputArgs, result->inputArgumentResults); |
205 | 0 | UA_NODESTORE_RELEASE(server, (const UA_Node*)inputArguments); |
206 | 0 | } else if(request->inputArgumentsSize > 0) { |
207 | 0 | res = UA_STATUSCODE_BADTOOMANYARGUMENTS; |
208 | 0 | } |
209 | | |
210 | | /* Return inputArgumentResults only for BADINVALIDARGUMENT */ |
211 | 0 | if(res != UA_STATUSCODE_BADINVALIDARGUMENT) { |
212 | 0 | UA_Array_delete(result->inputArgumentResults, |
213 | 0 | result->inputArgumentResultsSize, |
214 | 0 | &UA_TYPES[UA_TYPES_STATUSCODE]); |
215 | 0 | result->inputArgumentResults = NULL; |
216 | 0 | result->inputArgumentResultsSize = 0; |
217 | 0 | } |
218 | | |
219 | | /* Error during type-checking? */ |
220 | 0 | if(res != UA_STATUSCODE_GOOD) |
221 | 0 | return res; |
222 | | |
223 | | /* Get the output arguments node */ |
224 | 0 | const UA_VariableNode *outputArguments = |
225 | 0 | getArgumentsVariableNode(server, &resolvedMethod->head, |
226 | 0 | UA_STRING("OutputArguments")); |
227 | | |
228 | | /* Allocate the output arguments array. Always allocate memory, hence the |
229 | | * +1, even if the length is zero. Because we need a unique outputArguments |
230 | | * pointer as the key for async operations. The memory gets deleted in |
231 | | * UA_Array_delete even if the outputArgumentsSize is zero. */ |
232 | 0 | size_t outputArgsSize = 0; |
233 | 0 | if(outputArguments) |
234 | 0 | outputArgsSize = outputArguments->valueSource.internal.value.value.arrayLength; |
235 | 0 | result->outputArguments = (UA_Variant*) |
236 | 0 | UA_Array_new(outputArgsSize+1, &UA_TYPES[UA_TYPES_VARIANT]); |
237 | 0 | if(!result->outputArguments) |
238 | 0 | return UA_STATUSCODE_BADOUTOFMEMORY; |
239 | 0 | result->outputArgumentsSize = outputArgsSize; |
240 | | |
241 | | /* Release the output arguments node */ |
242 | 0 | UA_NODESTORE_RELEASE(server, (const UA_Node*)outputArguments); |
243 | | |
244 | | /* Call the method. If this is an async method, unlock the server lock for |
245 | | * the duration of the (long-running) call. */ |
246 | 0 | return resolvedMethod->method(server, &session->sessionId, session->context, |
247 | 0 | &resolvedMethod->head.nodeId, |
248 | 0 | resolvedMethod->head.context, |
249 | 0 | &callContext->head.nodeId, callContext->head.context, |
250 | 0 | request->inputArgumentsSize, mutableInputArgs, |
251 | 0 | result->outputArgumentsSize, result->outputArguments); |
252 | | |
253 | | /* TODO: Verify Output matches the argument definition */ |
254 | 0 | } |
255 | | |
256 | | static UA_StatusCode |
257 | | callWithMethodAndObject(UA_Server *server, UA_Session *session, |
258 | | const UA_CallMethodRequest *request, |
259 | | UA_CallMethodResult *result, |
260 | | const UA_Node *callMethod, |
261 | 0 | const UA_Node *callObject) { |
262 | 0 | UA_LOCK_ASSERT(&server->serviceMutex); |
263 | | |
264 | | /* Verify the context object's NodeClass */ |
265 | 0 | if(callObject->head.nodeClass != UA_NODECLASS_OBJECT && |
266 | 0 | callObject->head.nodeClass != UA_NODECLASS_OBJECTTYPE) |
267 | 0 | return UA_STATUSCODE_BADNODECLASSINVALID; |
268 | | |
269 | | /* Verify the method's NodeClass */ |
270 | 0 | if(callMethod->head.nodeClass != UA_NODECLASS_METHOD) |
271 | 0 | return UA_STATUSCODE_BADNODECLASSINVALID; |
272 | | |
273 | | /* For object types the given method must be directly referenced! |
274 | | * OPC UA 1.05.06 will state in Part 4 - Call Service Parameters |
275 | | * the following for parameter "objectId": |
276 | | * "In case of an ObjectType the ObjectType shall be the source of a |
277 | | * HasComponent Reference (or subtype of HasComponent Reference) to the |
278 | | * Method specified in methodId" */ |
279 | | |
280 | | /* Find the matching Method of the same BrowseName referenced directly by |
281 | | * the calling object */ |
282 | 0 | UA_NodeId resolvedMethodId; |
283 | 0 | UA_StatusCode res = |
284 | 0 | findChildByBrowsename(server, session, callObject->head.nodeId, |
285 | 0 | UA_NODECLASS_METHOD, UA_REFERENCETYPEINDEX_HASCOMPONENT, |
286 | 0 | UA_NS0ID(HASCOMPONENT), &callMethod->head.browseName, |
287 | 0 | &resolvedMethodId); |
288 | 0 | if(res != UA_STATUSCODE_GOOD) |
289 | 0 | return (res == UA_STATUSCODE_BADNOTFOUND) ? |
290 | 0 | UA_STATUSCODE_BADMETHODINVALID : res; |
291 | | |
292 | | /* The resolved methodId is different */ |
293 | 0 | const UA_MethodNode *resolvedMethod = (const UA_MethodNode*)callMethod; |
294 | 0 | if(!UA_NodeId_equal(&resolvedMethodId, &callMethod->head.nodeId)) { |
295 | | /* Check whether the original MethodId comes from an ObjectType that is |
296 | | * upwards in the type hierarchy. Downwards in the type hierarchy (or |
297 | | * even completely unrelated) is not allowed. */ |
298 | 0 | res = checkMethodInTypeHierarchy(server, session, |
299 | 0 | &callObject->head.nodeId, |
300 | 0 | &callMethod->head.nodeId, |
301 | 0 | &callMethod->head.browseName); |
302 | 0 | if(res != UA_STATUSCODE_GOOD) { |
303 | 0 | if(res == UA_STATUSCODE_BADNOTFOUND) |
304 | 0 | res = UA_STATUSCODE_BADMETHODINVALID; |
305 | 0 | goto errout; |
306 | 0 | } |
307 | | |
308 | | /* Get the resolved method node */ |
309 | 0 | resolvedMethod = (const UA_MethodNode*) |
310 | 0 | UA_NODESTORE_GET_SELECTIVE(server, &resolvedMethodId, |
311 | 0 | UA_NODEATTRIBUTESMASK_NODECLASS | |
312 | 0 | UA_NODEATTRIBUTESMASK_EXECUTABLE, |
313 | 0 | UA_REFERENCETYPESET_NONE, |
314 | 0 | UA_BROWSEDIRECTION_INVALID); |
315 | 0 | if(!resolvedMethod) { |
316 | 0 | res = UA_STATUSCODE_BADINTERNALERROR; |
317 | 0 | goto errout; |
318 | 0 | } |
319 | 0 | } |
320 | | |
321 | | /* Run the main call logic */ |
322 | 0 | res = callWithResolvedMethodAndObject(server, session, request, result, |
323 | 0 | resolvedMethod, callObject); |
324 | |
|
325 | 0 | errout: |
326 | | /* Release the resolved method node if it is different */ |
327 | 0 | if(resolvedMethod && (const UA_Node*)resolvedMethod != callMethod) |
328 | 0 | UA_NODESTORE_RELEASE(server, (const UA_Node*)resolvedMethod); |
329 | 0 | UA_NodeId_clear(&resolvedMethodId); |
330 | 0 | return res; |
331 | 0 | } |
332 | | |
333 | | UA_Boolean |
334 | | Operation_CallMethod(UA_Server *server, UA_Session *session, |
335 | | const UA_CallMethodRequest *request, |
336 | 0 | UA_CallMethodResult *result) { |
337 | 0 | UA_LOCK_ASSERT(&server->serviceMutex); |
338 | | |
339 | | /* Get the method node. We only need the nodeClass and browseName |
340 | | * attribute. */ |
341 | 0 | const UA_Node *method = |
342 | 0 | UA_NODESTORE_GET_SELECTIVE(server, &request->methodId, |
343 | 0 | UA_NODEATTRIBUTESMASK_NODECLASS | |
344 | 0 | UA_NODEATTRIBUTESMASK_BROWSENAME, |
345 | 0 | UA_REFERENCETYPESET_NONE, |
346 | 0 | UA_BROWSEDIRECTION_INVALID); |
347 | 0 | if(!method) { |
348 | 0 | result->statusCode = UA_STATUSCODE_BADMETHODINVALID; |
349 | 0 | return true; |
350 | 0 | } |
351 | | |
352 | | /* Get the object node. We only need the NodeClass attribute. But take all |
353 | | * references for now. |
354 | | * |
355 | | * TODO: Which references do we need actually? */ |
356 | 0 | const UA_Node *object = |
357 | 0 | UA_NODESTORE_GET_SELECTIVE(server, &request->objectId, |
358 | 0 | UA_NODEATTRIBUTESMASK_NODECLASS, |
359 | 0 | UA_REFERENCETYPESET_ALL, |
360 | 0 | UA_BROWSEDIRECTION_BOTH); |
361 | 0 | if(!object) { |
362 | 0 | result->statusCode = UA_STATUSCODE_BADNODEIDUNKNOWN; |
363 | 0 | UA_NODESTORE_RELEASE(server, method); |
364 | 0 | return true; |
365 | 0 | } |
366 | | |
367 | | /* Continue with method and object (which can be an ObjectType) as |
368 | | * context */ |
369 | 0 | result->statusCode = |
370 | 0 | callWithMethodAndObject(server, session, request, |
371 | 0 | result, method, object); |
372 | | |
373 | | /* Release the method and object node */ |
374 | 0 | UA_NODESTORE_RELEASE(server, method); |
375 | 0 | UA_NODESTORE_RELEASE(server, object); |
376 | |
|
377 | 0 | return (result->statusCode != UA_STATUSCODE_GOODCOMPLETESASYNCHRONOUSLY); |
378 | 0 | } |
379 | | |
380 | | UA_CallMethodResult |
381 | 0 | UA_Server_call(UA_Server *server, const UA_CallMethodRequest *request) { |
382 | 0 | UA_CallMethodResult result; |
383 | 0 | UA_CallMethodResult_init(&result); |
384 | 0 | lockServer(server); |
385 | 0 | Operation_CallMethod(server, &server->adminSession, request, &result); |
386 | | /* Cancel asynchronous responses right away */ |
387 | 0 | if(result.statusCode == UA_STATUSCODE_GOODCOMPLETESASYNCHRONOUSLY) { |
388 | 0 | if(server->config.asyncOperationCancelCallback) |
389 | 0 | server->config.asyncOperationCancelCallback(server, result.outputArguments); |
390 | 0 | result.statusCode = UA_STATUSCODE_BADWAITINGFORRESPONSE; |
391 | 0 | } |
392 | 0 | unlockServer(server); |
393 | 0 | return result; |
394 | 0 | } |
395 | | |
396 | | #endif /* UA_ENABLE_METHODCALLS */ |