├── .gitignore ├── LICENSE ├── README.md ├── client ├── README.md ├── client.go ├── client_secure_channel.go ├── client_service_set.go ├── client_test.go ├── example_client_browse_test.go ├── example_client_call_test.go ├── example_client_createsubscription_test.go ├── example_client_read_test.go ├── option.go ├── testnodeset_test.xml └── testserver_test.go ├── cmd ├── benchmark │ └── benchmark_test.go ├── gen_opcua │ └── main.go ├── testclient │ └── main.go └── testserver │ ├── main.go │ └── nodeset.xml ├── go.mod ├── go.sum ├── robot6.jpg ├── schema └── Opc.Ua.Types.bsd ├── server ├── README.md ├── auth_provider.go ├── data_type_node.go ├── datachange_monitored_item.go ├── event_monitored_item.go ├── history_read_writer.go ├── method_node.go ├── monitored_item.go ├── namespace_manager.go ├── node.go ├── nodeset_1_04.xml ├── object_node.go ├── object_type_node.go ├── option.go ├── reference_type_node.go ├── roles_provider.go ├── scheduler.go ├── server.go ├── server_secure_channel.go ├── server_service_set.go ├── server_test.go ├── session.go ├── session_manager.go ├── subscription.go ├── subscription_manager.go ├── testnodeset_test.xml ├── testserver_test.go ├── variable_node.go ├── variable_type_node.go └── view_node.go └── ua ├── access_levels.go ├── acknowledgeable_condition.go ├── alarm_condition.go ├── attributeids.go ├── base_event.go ├── base_event_test.go ├── binary_decoder.go ├── binary_encoder.go ├── binary_encoder_test.go ├── binary_registry.go ├── bytes_writer.go ├── bytestring.go ├── certificate_helpers.go ├── certificate_list.go ├── condition.go ├── content_filter.go ├── data_value.go ├── diagnostic_info.go ├── doc.go ├── encoding_context.go ├── enums.generated.go ├── event_notifier.go ├── expanded_nodeid.go ├── extension_object.go ├── gen_opcua.go ├── localized_text.go ├── message_types.go ├── nodeid.go ├── nodeids.generated.go ├── qualified_name.go ├── reference.go ├── rsa_uris.go ├── security_policy.go ├── security_token.go ├── server_capabilites.go ├── service_operation.go ├── service_request.go ├── status_code.generated.go ├── status_code.go ├── structs.generated.go ├── transport_profile_uris.go ├── ua_node_set.go ├── user_identity.go ├── value_rank.go ├── variant.go └── xmlelement.go /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | *.env 3 | *.log 4 | *.swp 5 | bin/ 6 | *.csv 7 | debug 8 | **/bindata.go 9 | *~ 10 | vendor/ 11 | __debug_bin 12 | pki/ 13 | pkiusers/ 14 | *.exe 15 | .vscode/ 16 | dir2pem/ 17 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Converter Systems LLC 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![robot][1] 2 | 3 | # opcua - [![Godoc](http://img.shields.io/badge/go-documentation-blue.svg?style=flat-square)](https://pkg.go.dev/mod/github.com/awcullen/opcua) [![License](http://img.shields.io/badge/license-mit-blue.svg?style=flat-square)](https://raw.githubusercontent.com/awcullen/opcua/master/LICENSE) 4 | Browse, read, write and subscribe to the live data published by the OPC UA servers on your network. 5 | 6 | This package supports OPC UA TCP transport protocol with secure channel and binary encoding. For more information, visit https://reference.opcfoundation.org/v104/. 7 | 8 | 9 | ## Includes Client and Server 10 | 11 | To *connect* to an OPC UA server, start here [![Godoc](http://img.shields.io/badge/go-documentation-blue.svg?style=flat-square)](https://pkg.go.dev/mod/github.com/awcullen/opcua/client) 12 | 13 | To *create* your own OPC UA server, start here [![Godoc](http://img.shields.io/badge/go-documentation-blue.svg?style=flat-square)](https://pkg.go.dev/mod/github.com/awcullen/opcua/server) 14 | 15 | ## Recent News 16 | Encodes variables of 2D/3D slices. 17 | 18 | Benchmark shows this package **10X faster** than Gopcua/opcua to encode a typical payload to the network. 19 | ``` 20 | pkg: github.com/awcullen/opcua/cmd/benchmark 21 | cpu: Intel(R) Core(TM) i7-7500U CPU @ 2.70GHz 22 | BenchmarkGopcuaEncode 23 | BenchmarkGopcuaEncode-4 120178 9332 ns/op 2536 B/op 97 allocs/op 24 | BenchmarkAwcullenEncode 25 | BenchmarkAwcullenEncode-4 1728259 859.3 ns/op 154 B/op 4 allocs/op 26 | PASS 27 | ``` 28 | 29 | 30 | [1]: robot6.jpg 31 | -------------------------------------------------------------------------------- /client/README.md: -------------------------------------------------------------------------------- 1 | # client - [![Godoc](http://img.shields.io/badge/go-documentation-blue.svg?style=flat-square)](https://pkg.go.dev/mod/github.com/awcullen/opcua/client) [![License](http://img.shields.io/badge/license-mit-blue.svg?style=flat-square)](https://raw.githubusercontent.com/awcullen/opcua/master/LICENSE) 2 | Browse, read, write and subscribe to data published by the OPC UA servers in your network. 3 | 4 | With this package, you can call any service of the OPC Unified Architecture, see https://reference.opcfoundation.org/v104/Core/docs/Part4/ 5 | 6 | ## Usage 7 | To connect to your OPC UA server, call client.Dial, passing the endpoint URL of the server and various security options. Dial returns a connected client or an error. 8 | 9 | For example, to connect to an OPC UA Demo Server, and read the server's status: 10 | 11 | ```go 12 | package client_test 13 | 14 | import ( 15 | "context" 16 | "fmt" 17 | 18 | "github.com/awcullen/opcua/client" 19 | "github.com/awcullen/opcua/ua" 20 | ) 21 | 22 | func ExampleClient_Read() { 23 | 24 | ctx := context.Background() 25 | 26 | // open a connection to testserver running locally. Testserver is started if not already running. 27 | ch, err := client.Dial( 28 | ctx, 29 | "opc.tcp://localhost:46010", 30 | client.WithInsecureSkipVerify(), // skips verification of server certificate 31 | ) 32 | if err != nil { 33 | fmt.Printf("Error opening client connection. %s\n", err.Error()) 34 | return 35 | } 36 | 37 | // prepare read request 38 | req := &ua.ReadRequest{ 39 | NodesToRead: []ua.ReadValueID{ 40 | { 41 | NodeID: ua.VariableIDServerServerStatus, 42 | AttributeID: ua.AttributeIDValue, 43 | }, 44 | }, 45 | } 46 | 47 | // send request to server. receive response or error 48 | res, err := ch.Read(ctx, req) 49 | if err != nil { 50 | fmt.Printf("Error reading ServerStatus. %s\n", err.Error()) 51 | ch.Abort(ctx) 52 | return 53 | } 54 | 55 | // print results 56 | if serverStatus, ok := res.Results[0].Value.(ua.ServerStatusDataType); ok { 57 | fmt.Printf("Server status:\n") 58 | fmt.Printf(" ProductName: %s\n", serverStatus.BuildInfo.ProductName) 59 | fmt.Printf(" ManufacturerName: %s\n", serverStatus.BuildInfo.ManufacturerName) 60 | fmt.Printf(" State: %s\n", serverStatus.State) 61 | } else { 62 | fmt.Println("Error decoding ServerStatus.") 63 | } 64 | 65 | // close connection 66 | err = ch.Close(ctx) 67 | if err != nil { 68 | ch.Abort(ctx) 69 | return 70 | } 71 | 72 | // Output: 73 | // Server status: 74 | // ProductName: testserver 75 | // ManufacturerName: awcullen 76 | // State: Running 77 | } 78 | 79 | 80 | ``` 81 | -------------------------------------------------------------------------------- /client/example_client_browse_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2021 Converter Systems LLC. All rights reserved. 2 | 3 | package client_test 4 | 5 | import ( 6 | "context" 7 | "fmt" 8 | 9 | "github.com/awcullen/opcua/client" 10 | "github.com/awcullen/opcua/ua" 11 | ) 12 | 13 | // This example demonstrates browsing the top-level 'Objects' folder of the server. 14 | func ExampleClient_Browse() { 15 | 16 | ctx := context.Background() 17 | 18 | // open a connection to testserver running locally. Testserver is started if not already running. 19 | ch, err := client.Dial( 20 | ctx, 21 | "opc.tcp://localhost:46010", 22 | client.WithInsecureSkipVerify(), // skips verification of server certificate 23 | ) 24 | if err != nil { 25 | fmt.Printf("Error opening client connection. %s\n", err.Error()) 26 | return 27 | } 28 | 29 | // prepare browse request 30 | req := &ua.BrowseRequest{ 31 | NodesToBrowse: []ua.BrowseDescription{ 32 | { 33 | NodeID: ua.ParseNodeID("i=85"), // Objects folder 34 | BrowseDirection: ua.BrowseDirectionForward, 35 | ReferenceTypeID: ua.ReferenceTypeIDHierarchicalReferences, 36 | IncludeSubtypes: true, 37 | ResultMask: uint32(ua.BrowseResultMaskTargetInfo), 38 | }, 39 | }, 40 | } 41 | 42 | // send request to server. receive response or error 43 | res, err := ch.Browse(ctx, req) 44 | if err != nil { 45 | fmt.Printf("Error browsing Objects folder. %s\n", err.Error()) 46 | ch.Abort(ctx) 47 | return 48 | } 49 | 50 | // print results 51 | fmt.Printf("Browse results of NodeID '%s':\n", req.NodesToBrowse[0].NodeID) 52 | for _, r := range res.Results[0].References { 53 | fmt.Printf(" + %s, browseName: %s, nodeClass: %s, nodeId: %s\n", r.DisplayName.Text, r.BrowseName, r.NodeClass, r.NodeID) 54 | } 55 | 56 | // close connection 57 | err = ch.Close(ctx) 58 | if err != nil { 59 | ch.Abort(ctx) 60 | return 61 | } 62 | 63 | // Output: 64 | // Browse results of NodeID 'i=85': 65 | // + Server, browseName: 0:Server, nodeClass: Object, nodeId: i=2253 66 | // + Aliases, browseName: 0:Aliases, nodeClass: Object, nodeId: i=23470 67 | // + Demo, browseName: 2:Demo, nodeClass: Object, nodeId: ns=2;s=Demo 68 | } 69 | -------------------------------------------------------------------------------- /client/example_client_call_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2021 Converter Systems LLC. All rights reserved. 2 | 3 | package client_test 4 | 5 | import ( 6 | "context" 7 | "fmt" 8 | 9 | "github.com/awcullen/opcua/client" 10 | "github.com/awcullen/opcua/ua" 11 | ) 12 | 13 | // This example demonstrates calling an method of the server to add two integers and return the sum. 14 | func ExampleClient_Call() { 15 | 16 | ctx := context.Background() 17 | 18 | // open a connection to testserver running locally. Testserver is started if not already running. 19 | ch, err := client.Dial( 20 | ctx, 21 | "opc.tcp://localhost:46010", 22 | client.WithClientCertificatePaths("./pki/client.crt", "./pki/client.key"), // need secure channel to send password 23 | client.WithUserNameIdentity("root", "secret"), // need role of "operator" to call this method 24 | client.WithInsecureSkipVerify(), // skips verification of server certificate 25 | ) 26 | if err != nil { 27 | fmt.Printf("Error opening client connection. %s\n", err.Error()) 28 | return 29 | } 30 | 31 | // prepare call request 32 | req := &ua.CallRequest{ 33 | MethodsToCall: []ua.CallMethodRequest{ 34 | { 35 | ObjectID: ua.ParseNodeID("ns=2;s=Demo.Methods"), // parent of "MethodIO" method 36 | MethodID: ua.ParseNodeID("ns=2;s=Demo.Methods.MethodIO"), // "MethodIO" method 37 | InputArguments: []ua.Variant{ 38 | uint32(6), 39 | uint32(7), 40 | }, 41 | }, 42 | }, 43 | } 44 | 45 | // send request to server. receive response or error 46 | res, err := ch.Call(ctx, req) 47 | if err != nil { 48 | fmt.Printf("Error calling method. %s\n", err.Error()) 49 | ch.Abort(ctx) 50 | return 51 | } 52 | 53 | // print results 54 | fmt.Printf("Call method result:\n") 55 | if res.Results[0].StatusCode.IsGood() { 56 | fmt.Println(res.Results[0].OutputArguments[0].(uint32)) 57 | } 58 | 59 | // close connection 60 | err = ch.Close(ctx) 61 | if err != nil { 62 | ch.Abort(ctx) 63 | return 64 | } 65 | 66 | // Output: 67 | // Call method result: 68 | // 13 69 | } 70 | -------------------------------------------------------------------------------- /client/example_client_createsubscription_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2021 Converter Systems LLC. All rights reserved. 2 | 3 | package client_test 4 | 5 | import ( 6 | "context" 7 | "fmt" 8 | 9 | "github.com/awcullen/opcua/client" 10 | "github.com/awcullen/opcua/ua" 11 | ) 12 | 13 | // This example demonstrates subscribing to the server's 'CurrentTime' variable and receiving data changes. 14 | func ExampleClient_CreateSubscription() { 15 | 16 | ctx := context.Background() 17 | 18 | // open a connection to testserver running locally. Testserver is started if not already running. 19 | ch, err := client.Dial( 20 | ctx, 21 | "opc.tcp://localhost:46010", 22 | client.WithInsecureSkipVerify(), // skips verification of server certificate 23 | ) 24 | if err != nil { 25 | fmt.Printf("Error opening client connection. %s\n", err.Error()) 26 | return 27 | } 28 | 29 | // prepare create subscription request 30 | req := &ua.CreateSubscriptionRequest{ 31 | RequestedPublishingInterval: 1000.0, 32 | RequestedMaxKeepAliveCount: 30, 33 | RequestedLifetimeCount: 30 * 3, 34 | PublishingEnabled: true, 35 | } 36 | 37 | // send request to server. receive response or error 38 | res, err := ch.CreateSubscription(ctx, req) 39 | if err != nil { 40 | fmt.Printf("Error creating subscription. %s\n", err.Error()) 41 | ch.Abort(ctx) 42 | return 43 | } 44 | 45 | // prepare create monitored items request 46 | req2 := &ua.CreateMonitoredItemsRequest{ 47 | SubscriptionID: res.SubscriptionID, 48 | TimestampsToReturn: ua.TimestampsToReturnBoth, 49 | ItemsToCreate: []ua.MonitoredItemCreateRequest{ 50 | { 51 | ItemToMonitor: ua.ReadValueID{ 52 | NodeID: ua.VariableIDServerServerStatusCurrentTime, 53 | AttributeID: ua.AttributeIDValue, 54 | }, 55 | MonitoringMode: ua.MonitoringModeReporting, 56 | // specify a unique ClientHandle. The ClientHandle is returned in the PublishResponse 57 | RequestedParameters: ua.MonitoringParameters{ 58 | ClientHandle: 42, QueueSize: 1, DiscardOldest: true, SamplingInterval: 1000.0}, 59 | }, 60 | }, 61 | } 62 | 63 | // send request to server. receive response or error 64 | _, err = ch.CreateMonitoredItems(ctx, req2) 65 | if err != nil { 66 | fmt.Printf("Error creating item. %s\n", err.Error()) 67 | ch.Abort(ctx) 68 | return 69 | } 70 | 71 | // prepare an initial publish request 72 | req3 := &ua.PublishRequest{ 73 | RequestHeader: ua.RequestHeader{TimeoutHint: 60000}, 74 | SubscriptionAcknowledgements: []ua.SubscriptionAcknowledgement{}, 75 | } 76 | 77 | // loop until 3 data changes received. 78 | numChanges := 0 79 | for numChanges < 3 { 80 | // send publish request to the server. 81 | res3, err := ch.Publish(ctx, req3) 82 | if err != nil { 83 | break 84 | } 85 | // loop thru all the notifications in the response. 86 | for _, data := range res3.NotificationMessage.NotificationData { 87 | switch body := data.(type) { 88 | case ua.DataChangeNotification: 89 | // the data change notification contains a slice of monitored item notifications. 90 | for _, item := range body.MonitoredItems { 91 | // each monitored item notification contains a clientHandle and dataValue. 92 | if item.ClientHandle == 42 { 93 | fmt.Println("" /* item.Value.Value */) 94 | numChanges++ 95 | } 96 | } 97 | } 98 | } 99 | // prepare another publish request 100 | req3 = &ua.PublishRequest{ 101 | RequestHeader: ua.RequestHeader{TimeoutHint: 60000}, 102 | SubscriptionAcknowledgements: []ua.SubscriptionAcknowledgement{ 103 | {SequenceNumber: res3.NotificationMessage.SequenceNumber, SubscriptionID: res3.SubscriptionID}, 104 | }, 105 | } 106 | } 107 | 108 | // success after receiving 3 data changes. 109 | 110 | // close connection 111 | err = ch.Close(ctx) 112 | if err != nil { 113 | ch.Abort(ctx) 114 | return 115 | } 116 | 117 | // Output: 118 | // 119 | // 120 | // 121 | } 122 | -------------------------------------------------------------------------------- /client/example_client_read_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2021 Converter Systems LLC. All rights reserved. 2 | 3 | package client_test 4 | 5 | import ( 6 | "context" 7 | "fmt" 8 | 9 | "github.com/awcullen/opcua/client" 10 | "github.com/awcullen/opcua/ua" 11 | ) 12 | 13 | // This example demonstrates reading the 'ServerStatus' variable. 14 | func ExampleClient_Read() { 15 | 16 | ctx := context.Background() 17 | 18 | // open a connection to testserver running locally. Testserver is started if not already running. 19 | ch, err := client.Dial( 20 | ctx, 21 | "opc.tcp://localhost:46010", 22 | client.WithInsecureSkipVerify(), // skips verification of server certificate 23 | ) 24 | if err != nil { 25 | fmt.Printf("Error opening client connection. %s\n", err.Error()) 26 | return 27 | } 28 | 29 | // prepare read request 30 | req := &ua.ReadRequest{ 31 | NodesToRead: []ua.ReadValueID{ 32 | { 33 | NodeID: ua.VariableIDServerServerStatus, 34 | AttributeID: ua.AttributeIDValue, 35 | }, 36 | }, 37 | } 38 | 39 | // send request to server. receive response or error 40 | res, err := ch.Read(ctx, req) 41 | if err != nil { 42 | fmt.Printf("Error reading ServerStatus. %s\n", err.Error()) 43 | ch.Abort(ctx) 44 | return 45 | } 46 | 47 | // print results 48 | if serverStatus, ok := res.Results[0].Value.(ua.ServerStatusDataType); ok { 49 | fmt.Printf("Server status:\n") 50 | fmt.Printf(" ProductName: %s\n", serverStatus.BuildInfo.ProductName) 51 | fmt.Printf(" ManufacturerName: %s\n", serverStatus.BuildInfo.ManufacturerName) 52 | fmt.Printf(" State: %s\n", serverStatus.State) 53 | } else { 54 | fmt.Println("Error decoding ServerStatus.") 55 | } 56 | 57 | // close connection 58 | err = ch.Close(ctx) 59 | if err != nil { 60 | ch.Abort(ctx) 61 | return 62 | } 63 | 64 | // Output: 65 | // Server status: 66 | // ProductName: testserver 67 | // ManufacturerName: awcullen 68 | // State: Running 69 | } 70 | 71 | // This example demonstrates reading the 'CustomStruct' variable. 72 | func ExampleClient_Read_customstruct() { 73 | 74 | ctx := context.Background() 75 | 76 | // open a connection to testserver running locally. Testserver is started if not already running. 77 | ch, err := client.Dial( 78 | ctx, 79 | "opc.tcp://localhost:46010", 80 | client.WithInsecureSkipVerify(), // skips verification of server certificate 81 | ) 82 | if err != nil { 83 | fmt.Printf("Error opening client connection. %s\n", err.Error()) 84 | return 85 | } 86 | 87 | // prepare read request 88 | req := &ua.ReadRequest{ 89 | NodesToRead: []ua.ReadValueID{ 90 | { 91 | NodeID: ua.ParseNodeID("ns=2;i=14"), 92 | AttributeID: ua.AttributeIDValue, 93 | }, 94 | }, 95 | } 96 | 97 | // send request to server. receive response or error 98 | res, err := ch.Read(ctx, req) 99 | if err != nil { 100 | fmt.Printf("Error reading CustomStruct. %s\n", err.Error()) 101 | ch.Abort(ctx) 102 | return 103 | } 104 | 105 | // print results 106 | if custom, ok := res.Results[0].Value.(CustomStruct); ok { 107 | fmt.Printf("CustomStruct:\n") 108 | fmt.Printf(" W1: %d\n", custom.W1) 109 | fmt.Printf(" W2: %d\n", custom.W2) 110 | } else { 111 | fmt.Println("Error decoding CustomStruct.") 112 | } 113 | // close connection 114 | err = ch.Close(ctx) 115 | if err != nil { 116 | ch.Abort(ctx) 117 | return 118 | } 119 | 120 | // Output: 121 | // CustomStruct: 122 | // W1: 1 123 | // W2: 2 124 | } 125 | 126 | // This example demonstrates reading an multidimensional array variable. 127 | func ExampleClient_Read_array() { 128 | 129 | ctx := context.Background() 130 | 131 | // open a connection to testserver running locally. Testserver is started if not already running. 132 | ch, err := client.Dial( 133 | ctx, 134 | "opc.tcp://localhost:46010", 135 | client.WithInsecureSkipVerify(), // skips verification of server certificate 136 | ) 137 | if err != nil { 138 | fmt.Printf("Error opening client connection. %s\n", err.Error()) 139 | return 140 | } 141 | 142 | // prepare read request 143 | req := &ua.ReadRequest{ 144 | NodesToRead: []ua.ReadValueID{ 145 | { 146 | NodeID: ua.ParseNodeID("ns=2;s=Demo.Static.Arrays.Matrix"), 147 | AttributeID: ua.AttributeIDValue, 148 | }, 149 | }, 150 | } 151 | 152 | // send request to server. receive response or error 153 | res, err := ch.Read(ctx, req) 154 | if err != nil { 155 | fmt.Printf("Error reading ServerStatus. %s\n", err.Error()) 156 | ch.Abort(ctx) 157 | return 158 | } 159 | 160 | // print results 161 | if val, ok := res.Results[0].Value.([][][]int32); ok { 162 | fmt.Println(val) 163 | } else { 164 | fmt.Println("Error decoding [][][]int32.") 165 | } 166 | 167 | // close connection 168 | err = ch.Close(ctx) 169 | if err != nil { 170 | ch.Abort(ctx) 171 | return 172 | } 173 | 174 | // Output: 175 | // [[[0 1 2] [3 4 5] [6 7 8] [9 10 11]] [[12 13 14] [15 16 17] [18 19 20] [21 22 23]]] 176 | } 177 | -------------------------------------------------------------------------------- /client/option.go: -------------------------------------------------------------------------------- 1 | // Copyright 2021 Converter Systems LLC. All rights reserved. 2 | 3 | package client 4 | 5 | import ( 6 | "bytes" 7 | "crypto/rsa" 8 | "crypto/tls" 9 | 10 | "github.com/awcullen/opcua/ua" 11 | ) 12 | 13 | // Option is a functional option to be applied to a client during initialization. 14 | type Option func(*Client) error 15 | 16 | // WithSecurityPolicyURI selects endpoint with given security policy URI and MessageSecurityMode. (default: "" selects most secure endpoint) 17 | func WithSecurityPolicyURI(uri string, securityMode ua.MessageSecurityMode) Option { 18 | return func(c *Client) error { 19 | c.securityPolicyURI = uri 20 | c.securityMode = securityMode 21 | return nil 22 | } 23 | } 24 | 25 | // WithUserNameIdentity sets the user identity to a UserNameIdentity created from a username and password. (default: AnonymousIdentity) 26 | func WithUserNameIdentity(userName, password string) Option { 27 | return func(c *Client) error { 28 | c.userIdentity = ua.UserNameIdentity{UserName: userName, Password: password} 29 | return nil 30 | } 31 | } 32 | 33 | // WithX509Identity sets the user identity to an X509Identity created from a certificate and private key. (default: AnonymousIdentity) 34 | func WithX509Identity(certificate []byte, privateKey *rsa.PrivateKey) Option { 35 | return func(c *Client) error { 36 | c.userIdentity = ua.X509Identity{Certificate: ua.ByteString(certificate), Key: privateKey} 37 | return nil 38 | } 39 | } 40 | 41 | // WithX509IdentityFile sets the user identity to an X509Identity created from the file paths of the certificate and private key. (default: AnonymousIdentity) 42 | // Reads and parses a public/private key pair from a pair of files. The files must contain PEM encoded data. 43 | // DEPRECIATED. Use WithX509IdentityPaths(). 44 | func WithX509IdentityFile(certPath, keyPath string) Option { 45 | return func(c *Client) error { 46 | cert, err := tls.LoadX509KeyPair(certPath, keyPath) 47 | if err != nil { 48 | return err 49 | } 50 | c.userIdentity = ua.X509Identity{Certificate: ua.ByteString(bytes.Join(cert.Certificate, []byte{})), Key: cert.PrivateKey.(*rsa.PrivateKey)} 51 | return nil 52 | } 53 | } 54 | 55 | // WithX509IdentityPaths sets the user identity to an X509Identity created from the file paths of the certificate and private key. (default: AnonymousIdentity) 56 | // Reads and parses a public/private key pair from a pair of files. The files must contain PEM encoded data. 57 | func WithX509IdentityPaths(certPath, keyPath string) Option { 58 | return func(c *Client) error { 59 | cert, err := tls.LoadX509KeyPair(certPath, keyPath) 60 | if err != nil { 61 | return err 62 | } 63 | c.userIdentity = ua.X509Identity{Certificate: ua.ByteString(bytes.Join(cert.Certificate, []byte{})), Key: cert.PrivateKey.(*rsa.PrivateKey)} 64 | return nil 65 | } 66 | } 67 | 68 | // WithIssuedIdentity sets the user identity to an IssuedIdentity created from token data. (default: AnonymousIdentity) 69 | func WithIssuedIdentity(tokenData []byte) Option { 70 | return func(c *Client) error { 71 | c.userIdentity = ua.IssuedIdentity{TokenData: ua.ByteString(tokenData)} 72 | return nil 73 | } 74 | } 75 | 76 | // WithApplicationName sets the name of the client application. (default: package name) 77 | func WithApplicationName(value string) Option { 78 | return func(c *Client) error { 79 | c.applicationName = value 80 | return nil 81 | } 82 | } 83 | 84 | // WithSessionName sets the name of the session. (default: server assigned) 85 | func WithSessionName(value string) Option { 86 | return func(c *Client) error { 87 | c.sessionName = value 88 | return nil 89 | } 90 | } 91 | 92 | // WithSessionTimeout sets the number of milliseconds that a session may be unused before being closed by the server. (default: 2 min) 93 | func WithSessionTimeout(value float64) Option { 94 | return func(c *Client) error { 95 | c.sessionTimeout = value 96 | return nil 97 | } 98 | } 99 | 100 | // WithClientCertificate sets the client certificate and private key. 101 | func WithClientCertificate(cert []byte, privateKey *rsa.PrivateKey) Option { 102 | return func(c *Client) error { 103 | var err error 104 | c.localCertificate, c.localPrivateKey = cert, privateKey 105 | return err 106 | } 107 | } 108 | 109 | // WithClientCertificateFile sets the file paths of the client certificate and private key. 110 | // Reads and parses a public/private key pair from a pair of files. The files must contain PEM encoded data. 111 | // DEPRECIATED. Use WithClientCertificatePaths(). 112 | func WithClientCertificateFile(certPath, keyPath string) Option { 113 | return func(c *Client) error { 114 | cert, err := tls.LoadX509KeyPair(certPath, keyPath) 115 | if err != nil { 116 | return err 117 | } 118 | c.localCertificate = bytes.Join(cert.Certificate, []byte{}) 119 | c.localPrivateKey, _ = cert.PrivateKey.(*rsa.PrivateKey) 120 | return nil 121 | } 122 | } 123 | 124 | // WithClientCertificatePaths sets the paths of the client certificate and private key. 125 | // Reads and parses a public/private key pair from a pair of files. The files must contain PEM encoded data. 126 | func WithClientCertificatePaths(certPath, keyPath string) Option { 127 | return func(c *Client) error { 128 | cert, err := tls.LoadX509KeyPair(certPath, keyPath) 129 | if err != nil { 130 | return err 131 | } 132 | c.localCertificate = bytes.Join(cert.Certificate, []byte{}) 133 | c.localPrivateKey, _ = cert.PrivateKey.(*rsa.PrivateKey) 134 | return nil 135 | } 136 | } 137 | 138 | // WithTrustedCertificatesFile sets the file path of the trusted server certificates or certificate authorities. 139 | // The file must contain PEM encoded data. 140 | // DEPRECIATED. Use WithTrustedCertificatesPath(). 141 | func WithTrustedCertificatesFile(path string) Option { 142 | return func(c *Client) error { 143 | c.trustedCertsPath = path 144 | return nil 145 | } 146 | } 147 | 148 | // WithTrustedCertificatesPaths sets the file path of the trusted certificates and revocation lists. 149 | // Path may be to a file, comma-separated list of files, or directory. 150 | func WithTrustedCertificatesPaths(certPath, crlPath string) Option { 151 | return func(c *Client) error { 152 | c.trustedCertsPath = certPath 153 | c.trustedCRLsPath = crlPath 154 | return nil 155 | } 156 | } 157 | 158 | // WithIssuerCertificatesPath sets the file path of the issuer certificates and revocation lists. 159 | // Issuer certificates are needed for validation, but are not trusted. 160 | // Path may be to a file, comma-separated list of files, or directory. 161 | func WithIssuerCertificatesPaths(certPath, crlPath string) Option { 162 | return func(c *Client) error { 163 | c.issuerCertsPath = certPath 164 | c.issuerCRLsPath = crlPath 165 | return nil 166 | } 167 | } 168 | 169 | // WithRejectedCertificatesPath sets the file path where rejected certificates are stored. 170 | // Path must be to a directory. 171 | func WithRejectedCertificatesPath(path string) Option { 172 | return func(c *Client) error { 173 | c.rejectedCertsPath = path 174 | return nil 175 | } 176 | } 177 | 178 | // WithInsecureSkipVerify skips verification of server certificate. Skips checking HostName, Expiration, and Authority. 179 | func WithInsecureSkipVerify() Option { 180 | return func(c *Client) error { 181 | c.suppressHostNameInvalid = true 182 | c.suppressCertificateExpired = true 183 | c.suppressCertificateChainIncomplete = true 184 | c.suppressCertificateRevocationUnknown = true 185 | return nil 186 | } 187 | } 188 | 189 | // WithTimeoutHint sets the default number of milliseconds to wait before the ServiceRequest is cancelled. (default: 1500) 190 | func WithTimeoutHint(value uint32) Option { 191 | return func(c *Client) error { 192 | c.timeoutHint = value 193 | return nil 194 | } 195 | } 196 | 197 | // WithDiagnosticsHint sets the default diagnostic hint that is sent in a request. (default: None) 198 | func WithDiagnosticsHint(value uint32) Option { 199 | return func(c *Client) error { 200 | c.diagnosticsHint = value 201 | return nil 202 | } 203 | } 204 | 205 | // WithTokenLifetime sets the requested number of milliseconds before a security token is renewed. (default: 60 min) 206 | func WithTokenLifetime(value uint32) Option { 207 | return func(c *Client) error { 208 | c.tokenLifetime = value 209 | return nil 210 | } 211 | } 212 | 213 | // WithConnectTimeout sets the number of milliseconds to wait for a connection response. (default:5000) 214 | func WithConnectTimeout(value int64) Option { 215 | return func(c *Client) error { 216 | c.connectTimeout = value 217 | return nil 218 | } 219 | } 220 | 221 | // WithTrace logs all ServiceRequests and ServiceResponses to StdOut. 222 | func WithTrace() Option { 223 | return func(c *Client) error { 224 | c.trace = true 225 | return nil 226 | } 227 | } 228 | 229 | // WithTransportLimits sets the limits on the size of the buffers and messages. (default: 64Kb, 64Mb, 4096) 230 | func WithTransportLimits(maxBufferSize, maxMessageSize, maxChunkCount uint32) Option { 231 | return func(c *Client) error { 232 | c.maxBufferSize = maxBufferSize 233 | c.maxMessageSize = maxMessageSize 234 | c.maxChunkCount = maxChunkCount 235 | return nil 236 | } 237 | } 238 | -------------------------------------------------------------------------------- /client/testserver_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2021 Converter Systems LLC. All rights reserved. 2 | 3 | package client_test 4 | 5 | import ( 6 | "crypto/rand" 7 | "crypto/x509" 8 | _ "embed" 9 | "fmt" 10 | "os" 11 | "reflect" 12 | "time" 13 | 14 | "github.com/awcullen/opcua/server" 15 | "github.com/awcullen/opcua/ua" 16 | "golang.org/x/crypto/bcrypt" 17 | ) 18 | 19 | var ( 20 | host, _ = os.Hostname() 21 | port = 46010 22 | SoftwareVersion = "1.0.0" 23 | //go:embed testnodeset_test.xml 24 | testnodeset []byte 25 | ) 26 | 27 | type CustomStruct struct { 28 | W1 uint16 29 | W2 uint16 30 | } 31 | 32 | func init() { 33 | ua.RegisterBinaryEncodingID(reflect.TypeOf(CustomStruct{}), ua.ParseExpandedNodeID("nsu=http://github.com/awcullen/opcua/testserver/;i=12")) 34 | } 35 | 36 | func NewTestServer() (*server.Server, error) { 37 | 38 | // userids for testing 39 | userids := []ua.UserNameIdentity{ 40 | {UserName: "root", Password: "secret"}, 41 | {UserName: "user1", Password: "password"}, 42 | {UserName: "user2", Password: "password1"}, 43 | } 44 | for i := range userids { 45 | hash, _ := bcrypt.GenerateFromPassword([]byte(userids[i].Password), 8) 46 | userids[i].Password = string(hash) 47 | } 48 | 49 | // create server 50 | srv, err := server.New( 51 | ua.ApplicationDescription{ 52 | ApplicationURI: fmt.Sprintf("urn:%s:testserver", host), 53 | ProductURI: "http://github.com/awcullen/opcua", 54 | ApplicationName: ua.LocalizedText{ 55 | Text: fmt.Sprintf("testserver@%s", host), 56 | Locale: "en", 57 | }, 58 | ApplicationType: ua.ApplicationTypeServer, 59 | GatewayServerURI: "", 60 | DiscoveryProfileURI: "", 61 | DiscoveryURLs: []string{fmt.Sprintf("opc.tcp://%s:%d", host, port)}, 62 | }, 63 | "./pki/server.crt", 64 | "./pki/server.key", 65 | fmt.Sprintf("opc.tcp://%s:%d", host, port), 66 | server.WithBuildInfo( 67 | ua.BuildInfo{ 68 | ProductURI: "http://github.com/awcullen/opcua", 69 | ManufacturerName: "awcullen", 70 | ProductName: "testserver", 71 | SoftwareVersion: SoftwareVersion, 72 | }), 73 | server.WithAuthenticateAnonymousIdentityFunc(func(userIdentity ua.AnonymousIdentity, applicationURI string, endpointURL string) error { 74 | // log.Printf("Login anonymous identity from %s\n", applicationURI) 75 | return nil 76 | }), 77 | server.WithAuthenticateUserNameIdentityFunc(func(userIdentity ua.UserNameIdentity, applicationURI string, endpointURL string) error { 78 | valid := false 79 | for _, user := range userids { 80 | if user.UserName == userIdentity.UserName { 81 | if err := bcrypt.CompareHashAndPassword([]byte(user.Password), []byte(userIdentity.Password)); err == nil { 82 | valid = true 83 | break 84 | } 85 | } 86 | } 87 | if !valid { 88 | return ua.BadUserAccessDenied 89 | } 90 | // log.Printf("Login %s from %s\n", userIdentity.UserName, applicationURI) 91 | return nil 92 | }), 93 | server.WithAuthenticateX509IdentityFunc(func(userIdentity ua.X509Identity, applicationURI string, endpointURL string) error { 94 | _, err := x509.ParseCertificates([]byte(userIdentity.Certificate)) 95 | if err != nil { 96 | return ua.BadUserAccessDenied 97 | } 98 | // log.Printf("Login %s from %s\n", cert.Subject, applicationURI) 99 | return nil 100 | }), 101 | server.WithSecurityPolicyNone(true), 102 | server.WithInsecureSkipVerify(), 103 | ) 104 | if err != nil { 105 | return nil, err 106 | } 107 | 108 | // load nodeset 109 | nm := srv.NamespaceManager() 110 | if err := nm.LoadNodeSetFromBuffer(testnodeset); err != nil { 111 | return nil, err 112 | } 113 | 114 | // install MethodNoArgs method 115 | if n, ok := nm.FindMethod(ua.ParseNodeID("ns=2;s=Demo.Methods.MethodNoArgs")); ok { 116 | n.SetCallMethodHandler(func(session *server.Session, req ua.CallMethodRequest) ua.CallMethodResult { 117 | return ua.CallMethodResult{} 118 | }) 119 | } 120 | 121 | // install MethodI method 122 | if n, ok := nm.FindMethod(ua.ParseNodeID("ns=2;s=Demo.Methods.MethodI")); ok { 123 | n.SetCallMethodHandler(func(session *server.Session, req ua.CallMethodRequest) ua.CallMethodResult { 124 | if len(req.InputArguments) < 1 { 125 | return ua.CallMethodResult{StatusCode: ua.BadArgumentsMissing} 126 | } 127 | if len(req.InputArguments) > 1 { 128 | return ua.CallMethodResult{StatusCode: ua.BadTooManyArguments} 129 | } 130 | statusCode := ua.Good 131 | inputArgumentResults := make([]ua.StatusCode, 1) 132 | _, ok := req.InputArguments[0].(uint32) 133 | if !ok { 134 | statusCode = ua.BadInvalidArgument 135 | inputArgumentResults[0] = ua.BadTypeMismatch 136 | } 137 | if statusCode == ua.BadInvalidArgument { 138 | return ua.CallMethodResult{StatusCode: statusCode, InputArgumentResults: inputArgumentResults} 139 | } 140 | return ua.CallMethodResult{OutputArguments: []ua.Variant{}} 141 | }) 142 | } 143 | 144 | // install MethodO method 145 | if n, ok := nm.FindMethod(ua.ParseNodeID("ns=2;s=Demo.Methods.MethodO")); ok { 146 | n.SetCallMethodHandler(func(session *server.Session, req ua.CallMethodRequest) ua.CallMethodResult { 147 | if len(req.InputArguments) > 0 { 148 | return ua.CallMethodResult{StatusCode: ua.BadTooManyArguments} 149 | } 150 | result := uint32(42) 151 | return ua.CallMethodResult{OutputArguments: []ua.Variant{uint32(result)}} 152 | }) 153 | } 154 | 155 | // install MethodIO method 156 | if n, ok := nm.FindMethod(ua.ParseNodeID("ns=2;s=Demo.Methods.MethodIO")); ok { 157 | n.SetCallMethodHandler(func(session *server.Session, req ua.CallMethodRequest) ua.CallMethodResult { 158 | if len(req.InputArguments) < 2 { 159 | return ua.CallMethodResult{StatusCode: ua.BadArgumentsMissing} 160 | } 161 | if len(req.InputArguments) > 2 { 162 | return ua.CallMethodResult{StatusCode: ua.BadTooManyArguments} 163 | } 164 | statusCode := ua.Good 165 | inputArgumentResults := make([]ua.StatusCode, 2) 166 | a, ok := req.InputArguments[0].(uint32) 167 | if !ok { 168 | statusCode = ua.BadInvalidArgument 169 | inputArgumentResults[0] = ua.BadTypeMismatch 170 | } 171 | b, ok := req.InputArguments[1].(uint32) 172 | if !ok { 173 | statusCode = ua.BadInvalidArgument 174 | inputArgumentResults[1] = ua.BadTypeMismatch 175 | } 176 | if statusCode == ua.BadInvalidArgument { 177 | return ua.CallMethodResult{StatusCode: statusCode, InputArgumentResults: inputArgumentResults} 178 | } 179 | result := a + b 180 | return ua.CallMethodResult{OutputArguments: []ua.Variant{uint32(result)}} 181 | }) 182 | } 183 | 184 | // add 'CustomStruct' data type 185 | typCustomStruct := server.NewDataTypeNode( 186 | srv, 187 | ua.NodeIDNumeric{NamespaceIndex: 2, ID: 13}, 188 | ua.QualifiedName{NamespaceIndex: 2, Name: "CustomStruct"}, 189 | ua.LocalizedText{Text: "CustomStruct"}, 190 | ua.LocalizedText{Text: "A CustomStruct data type for testing."}, 191 | nil, 192 | []ua.Reference{ // add type as subtype of 'Structure' 193 | { 194 | ReferenceTypeID: ua.ReferenceTypeIDHasSubtype, 195 | IsInverse: true, 196 | TargetID: ua.ExpandedNodeID{NodeID: ua.DataTypeIDStructure}, 197 | }, 198 | }, 199 | false, 200 | // this definition allows browsers such as UAExpert to decode the CustomStruct 201 | ua.StructureDefinition{ 202 | DefaultEncodingID: ua.NodeIDNumeric{NamespaceIndex: 2, ID: 12}, 203 | BaseDataType: ua.DataTypeIDStructure, 204 | StructureType: ua.StructureTypeStructure, 205 | Fields: []ua.StructureField{ 206 | {Name: "W1", DataType: ua.DataTypeIDUInt16, ValueRank: ua.ValueRankScalar}, 207 | {Name: "W2", DataType: ua.DataTypeIDUInt16, ValueRank: ua.ValueRankScalar}, 208 | }, 209 | }, 210 | ) 211 | 212 | // add 'CustomStruct' variable 213 | varCustomStruct := server.NewVariableNode( 214 | srv, 215 | ua.NodeIDNumeric{NamespaceIndex: 2, ID: 14}, 216 | ua.QualifiedName{NamespaceIndex: 2, Name: "CustomStruct"}, 217 | ua.LocalizedText{Text: "CustomStruct"}, 218 | ua.LocalizedText{Text: "A CustomStruct variable for testing."}, 219 | nil, 220 | []ua.Reference{ // add variable to 'Demo.Static.Scalar' folder 221 | { 222 | ReferenceTypeID: ua.ReferenceTypeIDOrganizes, 223 | IsInverse: true, 224 | TargetID: ua.ExpandedNodeID{NodeID: ua.ParseNodeID("ns=2;s=Demo.Static.Scalar")}, 225 | }, 226 | }, 227 | ua.NewDataValue(CustomStruct{W1: 1, W2: 2}, 0, time.Now().UTC(), 0, time.Now().UTC(), 0), 228 | typCustomStruct.NodeID(), 229 | ua.ValueRankScalar, 230 | []uint32{}, 231 | ua.AccessLevelsCurrentRead|ua.AccessLevelsCurrentWrite, 232 | 250.0, 233 | false, 234 | nil, 235 | ) 236 | 237 | // add 'Matrix' variable 238 | varMatrix := server.NewVariableNode( 239 | srv, 240 | ua.NodeIDString{NamespaceIndex: 2, ID: "Demo.Static.Arrays.Matrix"}, 241 | ua.QualifiedName{NamespaceIndex: 2, Name: "Matrix"}, 242 | ua.LocalizedText{Text: "Matrix"}, 243 | ua.LocalizedText{Text: "A matrix variable for testing."}, 244 | nil, 245 | []ua.Reference{ // add variable to 'Demo.Static.Arrays' folder 246 | { 247 | ReferenceTypeID: ua.ReferenceTypeIDOrganizes, 248 | IsInverse: true, 249 | TargetID: ua.ExpandedNodeID{NodeID: ua.ParseNodeID("ns=2;s=Demo.Static.Arrays")}, 250 | }, 251 | }, 252 | ua.NewDataValue([][][]int32{{{0, 1, 2}, {3, 4, 5}, {6, 7, 8}, {9, 10, 11}}, {{12, 13, 14}, {15, 16, 17}, {18, 19, 20}, {21, 22, 23}}}, 0, time.Now().UTC(), 0, time.Now().UTC(), 0), 253 | ua.DataTypeIDInt32, 254 | ua.ValueRankThreeDimensions, 255 | []uint32{0, 0, 0}, // no maximum 256 | ua.AccessLevelsCurrentRead|ua.AccessLevelsCurrentWrite, 257 | 250.0, 258 | false, 259 | nil, 260 | ) 261 | // add new nodes to namespace 262 | nm.AddNodes( 263 | typCustomStruct, 264 | varCustomStruct, 265 | varMatrix, 266 | ) 267 | 268 | go func() { 269 | source, _ := nm.FindObject(ua.ParseNodeID("ns=2;s=Area1")) 270 | ticker := time.NewTicker(5 * time.Second) 271 | defer ticker.Stop() 272 | for { 273 | select { 274 | case <-ticker.C: 275 | evt := &ua.BaseEvent{ 276 | EventID: getNextEventID(), 277 | EventType: ua.ObjectTypeIDBaseEventType, 278 | SourceNode: source.NodeID(), 279 | SourceName: "Area1", 280 | Time: time.Now(), 281 | ReceiveTime: time.Now(), 282 | Message: ua.LocalizedText{Text: "Event in Area1"}, 283 | Severity: 500, 284 | } 285 | nm.OnEvent(source, evt) 286 | case <-srv.Closing(): 287 | return 288 | } 289 | } 290 | }() 291 | 292 | go func() { 293 | active, acked := true, false 294 | source, _ := nm.FindObject(ua.ParseNodeID("ns=2;s=Area2")) 295 | 296 | ticker := time.NewTicker(5 * time.Second) 297 | defer ticker.Stop() 298 | for { 299 | select { 300 | case <-ticker.C: 301 | evt := &ua.AlarmCondition{ 302 | EventID: getNextEventID(), 303 | EventType: ua.ObjectTypeIDAlarmConditionType, 304 | SourceNode: source.NodeID(), 305 | SourceName: "Area2", 306 | Time: time.Now(), 307 | ReceiveTime: time.Now(), 308 | Message: ua.LocalizedText{Text: "Alarm in Area2"}, 309 | ConditionID: ua.ObjectTypeIDOffNormalAlarmType, 310 | ConditionName: "OffNormalAlarm", 311 | Severity: 500, 312 | Retain: true, 313 | AckedState: acked, 314 | ActiveState: active, 315 | } 316 | nm.OnEvent(source, evt) 317 | if !active { 318 | active = true 319 | } else { 320 | if !acked { 321 | acked = true 322 | } else { 323 | active, acked = false, false 324 | } 325 | } 326 | 327 | case <-srv.Closing(): 328 | return 329 | } 330 | } 331 | }() 332 | 333 | return srv, nil 334 | } 335 | 336 | // getNextEventID gets next random eventID. 337 | func getNextEventID() ua.ByteString { 338 | var nonce = make([]byte, 16) 339 | rand.Read(nonce) 340 | return ua.ByteString(nonce) 341 | } 342 | -------------------------------------------------------------------------------- /cmd/benchmark/benchmark_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2024 Converter Systems LLC. All rights reserved. 2 | 3 | package main 4 | 5 | import ( 6 | "testing" 7 | "time" 8 | 9 | awcullen "github.com/awcullen/opcua/ua" 10 | gopcua "github.com/gopcua/opcua/ua" 11 | ) 12 | 13 | /* 14 | run file benchmarks, results similar to: 15 | pkg: github.com/awcullen/opcua/cmd/benchmark 16 | cpu: Intel(R) Core(TM) i7-7500U CPU @ 2.70GHz 17 | BenchmarkGopcuaEncode 18 | BenchmarkGopcuaEncode-4 120178 9332 ns/op 2536 B/op 97 allocs/op 19 | BenchmarkAwcullenEncode 20 | BenchmarkAwcullenEncode-4 1728259 859.3 ns/op 154 B/op 4 allocs/op 21 | PASS 22 | */ 23 | 24 | // BenchmarkGopcuaEncode encodes typical payload to a mock network connection. 25 | func BenchmarkGopcuaEncode(b *testing.B) { 26 | pr := &gopcua.PublishResponse{ 27 | ResponseHeader: &gopcua.ResponseHeader{ 28 | Timestamp: time.Date(1601, time.January, 01, 12, 0, 0, 0, time.UTC), 29 | RequestHandle: 1000085, 30 | ServiceDiagnostics: &gopcua.DiagnosticInfo{}, 31 | StringTable: []string{}, 32 | AdditionalHeader: gopcua.NewExtensionObject(nil), 33 | }, 34 | SubscriptionID: 1296242973, 35 | AvailableSequenceNumbers: []uint32{4}, 36 | MoreNotifications: false, 37 | NotificationMessage: &gopcua.NotificationMessage{ 38 | SequenceNumber: 4, 39 | PublishTime: time.Date(1601, time.January, 01, 12, 0, 0, 0, time.UTC), 40 | NotificationData: []*gopcua.ExtensionObject{ 41 | gopcua.NewExtensionObject(&gopcua.DataChangeNotification{ 42 | MonitoredItems: []*gopcua.MonitoredItemNotification{ 43 | { 44 | ClientHandle: 9, 45 | Value: &gopcua.DataValue{EncodingMask: 0x15, Value: gopcua.MustVariant(3.14159), Status: 0, SourceTimestamp: time.Date(1601, time.January, 01, 12, 0, 0, 0, time.UTC), SourcePicoseconds: 0, ServerTimestamp: time.Date(1601, time.January, 01, 12, 0, 0, 0, time.UTC), ServerPicoseconds: 0}, 46 | }, 47 | }, 48 | DiagnosticInfos: []*gopcua.DiagnosticInfo{}, 49 | }), 50 | }, 51 | }, 52 | Results: []gopcua.StatusCode{0}, 53 | DiagnosticInfos: []*gopcua.DiagnosticInfo{}, 54 | } 55 | conn := &MockWriter{} 56 | b.ResetTimer() 57 | 58 | for i := 0; i < b.N; i++ { 59 | body, err := gopcua.Encode(pr) 60 | if err != nil { 61 | b.Fatal(err) 62 | } 63 | _, err = conn.Write(body) 64 | if err != nil { 65 | b.Fatal(err) 66 | } 67 | } 68 | } 69 | 70 | // BenchmarkAwcullenEncode encodes typical payload to a mock network connection. 71 | func BenchmarkAwcullenEncode(b *testing.B) { 72 | pr := &awcullen.PublishResponse{ 73 | ResponseHeader: awcullen.ResponseHeader{ 74 | Timestamp: time.Date(1601, time.January, 01, 12, 0, 0, 0, time.UTC), 75 | RequestHandle: 1000085, 76 | }, 77 | SubscriptionID: 1296242973, 78 | AvailableSequenceNumbers: []uint32{4}, 79 | MoreNotifications: false, 80 | NotificationMessage: awcullen.NotificationMessage{ 81 | SequenceNumber: 4, 82 | PublishTime: time.Date(1601, time.January, 01, 12, 0, 0, 0, time.UTC), 83 | NotificationData: []awcullen.ExtensionObject{ 84 | awcullen.DataChangeNotification{ 85 | MonitoredItems: []awcullen.MonitoredItemNotification{ 86 | { 87 | ClientHandle: 9, 88 | Value: awcullen.DataValue{Value: 3.14159, StatusCode: 0, SourceTimestamp: time.Date(1601, time.January, 01, 12, 0, 0, 0, time.UTC), SourcePicoseconds: 0, ServerTimestamp: time.Date(1601, time.January, 01, 12, 0, 0, 0, time.UTC), ServerPicoseconds: 0}, 89 | }, 90 | }, 91 | DiagnosticInfos: []awcullen.DiagnosticInfo{}, 92 | }, 93 | }, 94 | }, 95 | Results: []awcullen.StatusCode{0}, 96 | DiagnosticInfos: []awcullen.DiagnosticInfo{}, 97 | } 98 | ec := awcullen.NewEncodingContext() 99 | conn := &MockWriter{} 100 | b.ResetTimer() 101 | 102 | for i := 0; i < b.N; i++ { 103 | enc := awcullen.NewBinaryEncoder(conn, ec) 104 | if err := enc.Encode(pr); err != nil { 105 | b.Fatal(err) 106 | } 107 | } 108 | } 109 | 110 | type MockWriter struct { 111 | } 112 | 113 | func (w *MockWriter) Write(p []byte) (n int, err error) { 114 | return len(p), nil 115 | } 116 | -------------------------------------------------------------------------------- /cmd/testclient/main.go: -------------------------------------------------------------------------------- 1 | // Copyright 2021 Converter Systems LLC. All rights reserved. 2 | 3 | package main 4 | 5 | import ( 6 | "context" 7 | "fmt" 8 | "log" 9 | "os" 10 | "os/signal" 11 | "syscall" 12 | "time" 13 | 14 | "github.com/awcullen/opcua/client" 15 | "github.com/awcullen/opcua/ua" 16 | ) 17 | 18 | func main() { 19 | 20 | ctx, cancel := context.WithCancel(context.Background()) 21 | 22 | // open a connection to testserver running locally. 23 | ch, err := client.Dial( 24 | ctx, 25 | "opc.tcp://localhost:46010", 26 | client.WithInsecureSkipVerify(), // skips verification of server certificate 27 | ) 28 | if err != nil { 29 | fmt.Printf("Error opening client connection. %s\n", err.Error()) 30 | return 31 | } 32 | 33 | // prepare read request 34 | req := &ua.ReadRequest{ 35 | NodesToRead: []ua.ReadValueID{ 36 | { 37 | NodeID: ua.VariableIDServerServerStatus, 38 | AttributeID: ua.AttributeIDValue, 39 | }, 40 | }, 41 | } 42 | 43 | go func() { 44 | // wait for signal (this conflicts with debugger currently) 45 | log.Println("Press Ctrl-C to exit...") 46 | waitForSignal() 47 | 48 | log.Println("Closing client...") 49 | cancel() 50 | }() 51 | 52 | for { 53 | // send request to server. receive response or error 54 | _, err := ch.Read(ctx, req) 55 | if err != nil { 56 | fmt.Printf("Error reading ServerStatus. %s\n", err.Error()) 57 | break 58 | } 59 | time.Sleep(100 * time.Millisecond) 60 | } 61 | 62 | ctx = context.Background() 63 | err = ch.Close(ctx) 64 | if err != nil { 65 | ch.Abort(ctx) 66 | return 67 | } 68 | log.Println("Client closed.") 69 | 70 | } 71 | 72 | func waitForSignal() { 73 | sigs := make(chan os.Signal, 1) 74 | signal.Notify(sigs, syscall.SIGINT, syscall.SIGTERM) 75 | <-sigs 76 | } 77 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/awcullen/opcua 2 | 3 | go 1.22.0 4 | 5 | toolchain go1.22.6 6 | 7 | require ( 8 | github.com/djherbis/buffer v1.2.0 9 | github.com/gammazero/deque v1.0.0 10 | github.com/gammazero/workerpool v1.1.3 11 | github.com/google/uuid v1.6.0 12 | github.com/gopcua/opcua v0.6.1 13 | github.com/pkg/errors v0.9.1 14 | golang.org/x/crypto v0.31.0 15 | gotest.tools v2.2.0+incompatible 16 | ) 17 | 18 | require github.com/google/go-cmp v0.6.0 // indirect 19 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 2 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 3 | github.com/djherbis/buffer v1.2.0 h1:PH5Dd2ss0C7CRRhQCZ2u7MssF+No9ide8Ye71nPHcrQ= 4 | github.com/djherbis/buffer v1.2.0/go.mod h1:fjnebbZjCUpPinBRD+TDwXSOeNQ7fPQWLfGQqiAiUyE= 5 | github.com/gammazero/deque v1.0.0 h1:LTmimT8H7bXkkCy6gZX7zNLtkbz4NdS2z8LZuor3j34= 6 | github.com/gammazero/deque v1.0.0/go.mod h1:iflpYvtGfM3U8S8j+sZEKIak3SAKYpA5/SQewgfXDKo= 7 | github.com/gammazero/workerpool v1.1.3 h1:WixN4xzukFoN0XSeXF6puqEqFTl2mECI9S6W44HWy9Q= 8 | github.com/gammazero/workerpool v1.1.3/go.mod h1:wPjyBLDbyKnUn2XwwyD3EEwo9dHutia9/fwNmSHWACc= 9 | github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= 10 | github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= 11 | github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= 12 | github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 13 | github.com/gopcua/opcua v0.6.1 h1:kTotHu114p6OIZMds4GLxZjFtJ8Wv+GglrkKNGxkPkg= 14 | github.com/gopcua/opcua v0.6.1/go.mod h1:u6K7mFkgoR/UaEaCiIgncjh38Z1AQZ5ueO32WjyyJ6E= 15 | github.com/pascaldekloe/goe v0.1.1 h1:Ah6WQ56rZONR3RW3qWa2NCZ6JAVvSpUcoLBaOmYFt9Q= 16 | github.com/pascaldekloe/goe v0.1.1/go.mod h1:KSyfaxQOh0HZPjDP1FL/kFtbqYqrALJTaMafFUIccqU= 17 | github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= 18 | github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 19 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 20 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 21 | github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= 22 | github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= 23 | go.uber.org/goleak v1.1.12 h1:gZAh5/EyT/HQwlpkCy6wTpqfH9H8Lz8zbm3dZh+OyzA= 24 | go.uber.org/goleak v1.1.12/go.mod h1:cwTWslyiVhfpKIDGSZEM2HlOvcqm+tG4zioyIeLoqMQ= 25 | golang.org/x/crypto v0.31.0 h1:ihbySMvVjLAeSH1IbfcRTkD/iNscyz8rGzjF/E5hV6U= 26 | golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk= 27 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 28 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 29 | gotest.tools v2.2.0+incompatible h1:VsBPFP1AI068pPrMxtb/S8Zkgf9xEmTLJjfM+P5UIEo= 30 | gotest.tools v2.2.0+incompatible/go.mod h1:DsYFclhRJ6vuDpmuTbkuFWG+y2sxOXAzmJt81HFBacw= 31 | -------------------------------------------------------------------------------- /robot6.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/awcullen/opcua/3cdbbee6cc600b55a964f816e4b9ac1481ef7a82/robot6.jpg -------------------------------------------------------------------------------- /server/README.md: -------------------------------------------------------------------------------- 1 | # server - [![Godoc](http://img.shields.io/badge/go-documentation-blue.svg?style=flat-square)](https://pkg.go.dev/mod/github.com/awcullen/opcua/server) [![License](http://img.shields.io/badge/license-mit-blue.svg?style=flat-square)](https://raw.githubusercontent.com/awcullen/opcua/master/LICENSE) 2 | Publish data to the OPC UA clients in your network. 3 | 4 | With this package, you can create a server of the OPC Unified Architecture, see https://reference.opcfoundation.org/v104/Core/docs/Part4/ 5 | 6 | ## Usage 7 | To create your OPC UA server, call server.New(). Specify the server's description, certificate, private key, endpoint URL, and various options. 8 | 9 | Create a namespace, and add nodes of types Object, Variable, Method and DataType. 10 | 11 | Run the server by calling ListenAndServe(). 12 | 13 | To stop the server, call Close(). 14 | 15 | ```go 16 | package main 17 | 18 | import ( 19 | "context" 20 | "fmt" 21 | 22 | "github.com/awcullen/opcua/server" 23 | "github.com/awcullen/opcua/ua" 24 | ) 25 | 26 | func main() { 27 | 28 | // create directory with certificate and key, if not found. 29 | if err := ensurePKI(); err != nil { 30 | log.Println("Error creating PKI.") 31 | return 32 | } 33 | 34 | // create the endpoint url from hostname and port 35 | host, _ := os.Hostname() 36 | port := 46010 37 | endpointURL := fmt.Sprintf("opc.tcp://%s:%d", host, port) 38 | 39 | // create server 40 | srv, err := server.New( 41 | ua.ApplicationDescription{ 42 | ApplicationURI: fmt.Sprintf("urn:%s:testserver", host), 43 | ProductURI: "http://github.com/awcullen/opcua", 44 | ApplicationName: ua.LocalizedText{ 45 | Text: fmt.Sprintf("testserver@%s", host), 46 | Locale: "en", 47 | }, 48 | ApplicationType: ua.ApplicationTypeServer, 49 | GatewayServerURI: "", 50 | DiscoveryProfileURI: "", 51 | DiscoveryURLs: []string{endpointURL}, 52 | }, 53 | "./pki/server.crt", 54 | "./pki/server.key", 55 | endpointURL, 56 | server.WithBuildInfo( 57 | ua.BuildInfo{ 58 | ProductURI: "http://github.com/awcullen/opcua", 59 | ManufacturerName: "awcullen", 60 | ProductName: "testserver", 61 | SoftwareVersion: "0.3.0", 62 | }), 63 | server.WithAnonymousIdentity(true), 64 | server.WithSecurityPolicyNone(true), 65 | server.WithInsecureSkipVerify(), 66 | server.WithServerDiagnostics(true), 67 | ) 68 | if err != nil { 69 | os.Exit(1) 70 | } 71 | 72 | // load nodeset 73 | nm := srv.NamespaceManager() 74 | if err := nm.LoadNodeSetFromBuffer([]byte(nodeset)); err != nil { 75 | os.Exit(2) 76 | } 77 | 78 | go func() { 79 | // wait for signal 80 | log.Println("Press Ctrl-C to exit...") 81 | waitForSignal() 82 | 83 | log.Println("Stopping server...") 84 | srv.Close() 85 | }() 86 | 87 | // start server 88 | log.Printf("Starting server '%s' at '%s'\n", srv.LocalDescription().ApplicationName.Text, srv.EndpointURL()) 89 | if err := srv.ListenAndServe(); err != ua.BadServerHalted { 90 | log.Println(errors.Wrap(err, "Error opening server")) 91 | } 92 | } 93 | 94 | 95 | ``` 96 | -------------------------------------------------------------------------------- /server/auth_provider.go: -------------------------------------------------------------------------------- 1 | package server 2 | 3 | import "github.com/awcullen/opcua/ua" 4 | 5 | // UserNameIdentityAuthenticator authenticates AnonymousIdentity. 6 | type AnonymousIdentityAuthenticator interface { 7 | // AuthenticateUserNameIdentity returns nil when user identity is authenticated, or BadUserAccessDenied otherwise. 8 | AuthenticateAnonymousIdentity(userIdentity ua.AnonymousIdentity, applicationURI string, endpointURL string) error 9 | } 10 | 11 | // AuthenticateUserNameIdentityFunc authenticates AnonymousIdentity. 12 | type AuthenticateAnonymousIdentityFunc func(userIdentity ua.AnonymousIdentity, applicationURI string, endpointURL string) error 13 | 14 | // AuthenticateUserNameIdentity ... 15 | func (f AuthenticateAnonymousIdentityFunc) AuthenticateAnonymousIdentity(userIdentity ua.AnonymousIdentity, applicationURI string, endpointURL string) error { 16 | return f(userIdentity, applicationURI, endpointURL) 17 | } 18 | 19 | // UserNameIdentityAuthenticator authenticates UserNameIdentity. 20 | type UserNameIdentityAuthenticator interface { 21 | // AuthenticateUserNameIdentity returns nil when user identity is authenticated, or BadUserAccessDenied otherwise. 22 | AuthenticateUserNameIdentity(userIdentity ua.UserNameIdentity, applicationURI string, endpointURL string) error 23 | } 24 | 25 | // AuthenticateUserNameIdentityFunc authenticates UserNameIdentity. 26 | type AuthenticateUserNameIdentityFunc func(userIdentity ua.UserNameIdentity, applicationURI string, endpointURL string) error 27 | 28 | // AuthenticateUserNameIdentity ... 29 | func (f AuthenticateUserNameIdentityFunc) AuthenticateUserNameIdentity(userIdentity ua.UserNameIdentity, applicationURI string, endpointURL string) error { 30 | return f(userIdentity, applicationURI, endpointURL) 31 | } 32 | 33 | // X509IdentityAuthenticator authenticates X509Identity. 34 | type X509IdentityAuthenticator interface { 35 | // AuthenticateUser returns nil when user is authenticated, or BadUserAccessDenied otherwise. 36 | AuthenticateX509Identity(userIdentity ua.X509Identity, applicationURI string, endpointURL string) error 37 | } 38 | 39 | // AuthenticateX509IdentityFunc authenticates X509Identity. 40 | type AuthenticateX509IdentityFunc func(userIdentity ua.X509Identity, applicationURI string, endpointURL string) error 41 | 42 | // AuthenticateX509Identity ... 43 | func (f AuthenticateX509IdentityFunc) AuthenticateX509Identity(userIdentity ua.X509Identity, applicationURI string, endpointURL string) error { 44 | return f(userIdentity, applicationURI, endpointURL) 45 | } 46 | 47 | // IssuedIdentityAuthenticator authenticates user identities. 48 | type IssuedIdentityAuthenticator interface { 49 | // AuthenticateIssuedIdentity returns nil when user is authenticated, or BadUserAccessDenied otherwise. 50 | AuthenticateIssuedIdentity(userIdentity ua.IssuedIdentity, applicationURI string, endpointURL string) error 51 | } 52 | 53 | // AuthenticateIssuedIdentityFunc authenticates user identities. 54 | type AuthenticateIssuedIdentityFunc func(userIdentity ua.IssuedIdentity, applicationURI string, endpointURL string) error 55 | 56 | // AuthenticateIssuedIdentity ... 57 | func (f AuthenticateIssuedIdentityFunc) AuthenticateIssuedIdentity(userIdentity ua.IssuedIdentity, applicationURI string, endpointURL string) error { 58 | return f(userIdentity, applicationURI, endpointURL) 59 | } 60 | -------------------------------------------------------------------------------- /server/data_type_node.go: -------------------------------------------------------------------------------- 1 | // Copyright 2021 Converter Systems LLC. All rights reserved. 2 | 3 | package server 4 | 5 | import ( 6 | "sync" 7 | 8 | "github.com/awcullen/opcua/ua" 9 | ) 10 | 11 | // DataTypeNode is a Node class that describes the syntax of a variable's Value. 12 | type DataTypeNode struct { 13 | sync.RWMutex 14 | server *Server 15 | nodeID ua.NodeID 16 | nodeClass ua.NodeClass 17 | browseName ua.QualifiedName 18 | displayName ua.LocalizedText 19 | description ua.LocalizedText 20 | rolePermissions []ua.RolePermissionType 21 | accessRestrictions uint16 22 | references []ua.Reference 23 | isAbstract bool 24 | dataTypeDefinition any 25 | } 26 | 27 | var _ Node = (*DataTypeNode)(nil) 28 | 29 | // NewDataTypeNode creates a new DataTypeNode. 30 | func NewDataTypeNode(server *Server, nodeID ua.NodeID, browseName ua.QualifiedName, displayName ua.LocalizedText, description ua.LocalizedText, rolePermissions []ua.RolePermissionType, references []ua.Reference, isAbstract bool, structureOrEnumDefinition any) *DataTypeNode { 31 | return &DataTypeNode{ 32 | server: server, 33 | nodeID: nodeID, 34 | nodeClass: ua.NodeClassDataType, 35 | browseName: browseName, 36 | displayName: displayName, 37 | description: description, 38 | rolePermissions: rolePermissions, 39 | accessRestrictions: 0, 40 | references: references, 41 | isAbstract: isAbstract, 42 | dataTypeDefinition: structureOrEnumDefinition, 43 | } 44 | } 45 | 46 | // NodeID returns the NodeID attribute of this node. 47 | func (n *DataTypeNode) NodeID() ua.NodeID { 48 | return n.nodeID 49 | } 50 | 51 | // NodeClass returns the NodeClass attribute of this node. 52 | func (n *DataTypeNode) NodeClass() ua.NodeClass { 53 | return n.nodeClass 54 | } 55 | 56 | // BrowseName returns the BrowseName attribute of this node. 57 | func (n *DataTypeNode) BrowseName() ua.QualifiedName { 58 | return n.browseName 59 | } 60 | 61 | // DisplayName returns the DisplayName attribute of this node. 62 | func (n *DataTypeNode) DisplayName() ua.LocalizedText { 63 | return n.displayName 64 | } 65 | 66 | // Description returns the Description attribute of this node. 67 | func (n *DataTypeNode) Description() ua.LocalizedText { 68 | return n.description 69 | } 70 | 71 | // RolePermissions returns the RolePermissions attribute of this node. 72 | func (n *DataTypeNode) RolePermissions() []ua.RolePermissionType { 73 | return n.rolePermissions 74 | } 75 | 76 | // UserRolePermissions returns the RolePermissions attribute of this node for the current user. 77 | func (n *DataTypeNode) UserRolePermissions(userIdentity any) []ua.RolePermissionType { 78 | filteredPermissions := []ua.RolePermissionType{} 79 | roles, err := n.server.GetRoles(userIdentity, "", "") 80 | if err != nil { 81 | return filteredPermissions 82 | } 83 | rolePermissions := n.RolePermissions() 84 | if rolePermissions == nil { 85 | rolePermissions = n.server.RolePermissions() 86 | } 87 | for _, role := range roles { 88 | for _, rp := range rolePermissions { 89 | if rp.RoleID == role { 90 | filteredPermissions = append(filteredPermissions, rp) 91 | } 92 | } 93 | } 94 | return filteredPermissions 95 | } 96 | 97 | // References returns the References of this node. 98 | func (n *DataTypeNode) References() []ua.Reference { 99 | n.RLock() 100 | defer n.RUnlock() 101 | return n.references 102 | } 103 | 104 | // SetReferences sets the References of the Variable. 105 | func (n *DataTypeNode) SetReferences(value []ua.Reference) { 106 | n.Lock() 107 | defer n.Unlock() 108 | n.references = value 109 | } 110 | 111 | // IsAbstract returns the IsAbstract attribute of this node. 112 | func (n *DataTypeNode) IsAbstract() bool { 113 | return n.isAbstract 114 | } 115 | 116 | // DataTypeDefinition returns the DataTypeDefinition attribute of this node. 117 | func (n *DataTypeNode) DataTypeDefinition() any { 118 | return n.dataTypeDefinition 119 | } 120 | 121 | // IsAttributeIDValid returns true if attributeId is supported for the node. 122 | func (n *DataTypeNode) IsAttributeIDValid(attributeID uint32) bool { 123 | switch attributeID { 124 | case ua.AttributeIDNodeID, ua.AttributeIDNodeClass, ua.AttributeIDBrowseName, 125 | ua.AttributeIDDisplayName, ua.AttributeIDDescription, ua.AttributeIDRolePermissions, 126 | ua.AttributeIDUserRolePermissions, ua.AttributeIDIsAbstract, ua.AttributeIDDataTypeDefinition: 127 | return true 128 | default: 129 | return false 130 | } 131 | } 132 | -------------------------------------------------------------------------------- /server/event_monitored_item.go: -------------------------------------------------------------------------------- 1 | // Copyright 2021 Converter Systems LLC. All rights reserved. 2 | 3 | package server 4 | 5 | import ( 6 | "sync/atomic" 7 | "time" 8 | 9 | "sync" 10 | 11 | "github.com/awcullen/opcua/ua" 12 | deque "github.com/gammazero/deque" 13 | ) 14 | 15 | // EventMonitoredItem specifies a node that is monitored for events. 16 | type EventMonitoredItem struct { 17 | sync.RWMutex 18 | id uint32 19 | itemToMonitor ua.ReadValueID 20 | monitoringMode ua.MonitoringMode 21 | clientHandle uint32 22 | samplingInterval float64 23 | queueSize uint32 24 | discardOldest bool 25 | queue deque.Deque[[]ua.Variant] 26 | node Node 27 | eventFilter ua.EventFilter 28 | sub *Subscription 29 | srv *Server 30 | triggeredItems []MonitoredItem 31 | triggered bool 32 | } 33 | 34 | // NewEventMonitoredItem constructs a new EventMonitoredItem. 35 | func NewEventMonitoredItem(sub *Subscription, node Node, itemToMonitor ua.ReadValueID, monitoringMode ua.MonitoringMode, parameters ua.MonitoringParameters) *EventMonitoredItem { 36 | mi := &EventMonitoredItem{ 37 | sub: sub, 38 | srv: sub.manager.server, 39 | node: node, 40 | id: atomic.AddUint32(&monitoredItemID, 1), 41 | itemToMonitor: itemToMonitor, 42 | monitoringMode: monitoringMode, 43 | clientHandle: parameters.ClientHandle, 44 | discardOldest: parameters.DiscardOldest, 45 | queue: deque.Deque[[]ua.Variant]{}, 46 | } 47 | mi.setQueueSize(parameters.QueueSize) 48 | mi.setSamplingInterval(parameters.SamplingInterval) 49 | mi.setFilter(parameters.Filter) 50 | 51 | mi.Lock() 52 | mi.startMonitoring() 53 | mi.Unlock() 54 | return mi 55 | } 56 | 57 | // ID returns the identifier of the MonitoredItem. 58 | func (mi *EventMonitoredItem) ID() uint32 { 59 | return mi.id 60 | } 61 | 62 | // Node returns the Node of the MonitoredItem. 63 | func (mi *EventMonitoredItem) Node() Node { 64 | return mi.node 65 | } 66 | 67 | // ItemToMonitor returns the ReadValueID of the MonitoredItem. 68 | func (mi *EventMonitoredItem) ItemToMonitor() ua.ReadValueID { 69 | return mi.itemToMonitor 70 | } 71 | 72 | // SamplingInterval returns the sampling interval in ms of the MonitoredItem. 73 | func (mi *EventMonitoredItem) SamplingInterval() float64 { 74 | mi.RLock() 75 | defer mi.RUnlock() 76 | return mi.samplingInterval 77 | } 78 | 79 | // QueueSize returns the queue size of the MonitoredItem. 80 | func (mi *EventMonitoredItem) QueueSize() uint32 { 81 | mi.RLock() 82 | defer mi.RUnlock() 83 | return mi.queueSize 84 | } 85 | 86 | // MonitoringMode returns the monitoring mode of the MonitoredItem. 87 | func (mi *EventMonitoredItem) MonitoringMode() ua.MonitoringMode { 88 | mi.RLock() 89 | defer mi.RUnlock() 90 | return mi.monitoringMode 91 | } 92 | 93 | // ClientHandle returns the client handle of the MonitoredItem. 94 | func (mi *EventMonitoredItem) ClientHandle() uint32 { 95 | mi.RLock() 96 | defer mi.RUnlock() 97 | return mi.clientHandle 98 | } 99 | 100 | // Triggered returns true when the MonitoredItem is triggered. 101 | func (mi *EventMonitoredItem) Triggered() bool { 102 | mi.RLock() 103 | defer mi.RUnlock() 104 | return mi.triggered 105 | } 106 | 107 | // SetTriggered sets when the MonitoredItem is triggered. 108 | func (mi *EventMonitoredItem) SetTriggered(val bool) { 109 | mi.Lock() 110 | defer mi.Unlock() 111 | mi.triggered = val 112 | } 113 | 114 | // Modify modifies the MonitoredItem. 115 | func (mi *EventMonitoredItem) Modify(req ua.MonitoredItemModifyRequest) ua.MonitoredItemModifyResult { 116 | mi.Lock() 117 | defer mi.Unlock() 118 | mi.stopMonitoring() 119 | mi.clientHandle = req.RequestedParameters.ClientHandle 120 | mi.discardOldest = req.RequestedParameters.DiscardOldest 121 | mi.setQueueSize(req.RequestedParameters.QueueSize) 122 | mi.setSamplingInterval(req.RequestedParameters.SamplingInterval) 123 | mi.setFilter(req.RequestedParameters.Filter) 124 | mi.startMonitoring() 125 | return ua.MonitoredItemModifyResult{RevisedSamplingInterval: mi.samplingInterval, RevisedQueueSize: mi.queueSize} 126 | } 127 | 128 | // Delete deletes the DataMonitoredItem. 129 | func (mi *EventMonitoredItem) Delete() { 130 | mi.Lock() 131 | defer mi.Unlock() 132 | mi.stopMonitoring() 133 | mi.queue.Clear() 134 | mi.node = nil 135 | mi.sub = nil 136 | mi.triggeredItems = nil 137 | } 138 | 139 | // SetMonitoringMode sets the MonitoringMode of the MonitoredItem. 140 | func (mi *EventMonitoredItem) SetMonitoringMode(mode ua.MonitoringMode) { 141 | mi.Lock() 142 | defer mi.Unlock() 143 | if mi.monitoringMode == mode { 144 | return 145 | } 146 | mi.stopMonitoring() 147 | mi.monitoringMode = mode 148 | if mode == ua.MonitoringModeDisabled { 149 | mi.queue.Clear() 150 | mi.sub.disabledMonitoredItemCount++ 151 | } else { 152 | mi.sub.disabledMonitoredItemCount-- 153 | } 154 | mi.startMonitoring() 155 | } 156 | 157 | func (mi *EventMonitoredItem) setQueueSize(queueSize uint32) { 158 | mi.queueSize = maxQueueSize 159 | 160 | // trim to size 161 | if mi.discardOldest { 162 | for mi.queue.Len() > int(mi.queueSize) { 163 | mi.queue.PopFront() 164 | } 165 | } else { 166 | for mi.queue.Len() > int(mi.queueSize) { 167 | mi.queue.PopBack() 168 | } 169 | } 170 | } 171 | 172 | func (mi *EventMonitoredItem) setSamplingInterval(samplingInterval float64) { 173 | mi.samplingInterval = 0 174 | } 175 | 176 | func (mi *EventMonitoredItem) setFilter(filter any) { 177 | if ef, ok := filter.(ua.EventFilter); ok { 178 | mi.eventFilter = ef 179 | } else { 180 | mi.eventFilter = ua.EventFilter{} 181 | } 182 | } 183 | 184 | func (mi *EventMonitoredItem) enqueue(item []ua.Variant) { 185 | overflow := false 186 | if mi.discardOldest { 187 | for mi.queue.Len() >= int(mi.queueSize) { 188 | mi.queue.PopFront() // discard oldest 189 | overflow = true 190 | } 191 | mi.queue.PushBack(item) 192 | if overflow && mi.queueSize > 1 { 193 | mi.sub.monitoringQueueOverflowCount++ 194 | } 195 | } else { 196 | for mi.queue.Len() >= int(mi.queueSize) { 197 | mi.queue.PopBack() // discard newest 198 | overflow = true 199 | } 200 | mi.queue.PushBack(item) 201 | if overflow && mi.queueSize > 1 { 202 | mi.sub.monitoringQueueOverflowCount++ 203 | } 204 | } 205 | if mi.triggeredItems != nil { 206 | for _, item := range mi.triggeredItems { 207 | item.SetTriggered(true) 208 | // log.Printf("Item %d triggered %d", mi.id, item.id) 209 | } 210 | } 211 | } 212 | 213 | func (mi *EventMonitoredItem) OnEvent(evt ua.Event) { 214 | mi.Lock() 215 | if res, ok := mi.whereClause(evt, 0).(bool); ok && res { 216 | mi.enqueue(mi.selectFields(evt)) 217 | } 218 | mi.Unlock() 219 | } 220 | 221 | var ( 222 | attributeOperandEventType = ua.SimpleAttributeOperand{TypeDefinitionID: ua.ObjectTypeIDBaseEventType, BrowsePath: ua.ParseBrowsePath("EventType"), AttributeID: ua.AttributeIDValue} 223 | ) 224 | 225 | func (mi *EventMonitoredItem) whereClause(evt ua.Event, idx int) any { 226 | if idx >= len(mi.eventFilter.WhereClause.Elements) { 227 | return true 228 | } 229 | element := mi.eventFilter.WhereClause.Elements[idx] 230 | switch element.FilterOperator { 231 | 232 | case ua.FilterOperatorEquals: 233 | var a, b ua.Variant 234 | switch c := element.FilterOperands[0].(type) { 235 | case ua.LiteralOperand: 236 | a = c.Value 237 | case ua.SimpleAttributeOperand: 238 | a = evt.GetAttribute(c) 239 | case ua.ElementOperand: 240 | a = mi.whereClause(evt, int(c.Index)) 241 | default: 242 | return false 243 | } 244 | switch c := element.FilterOperands[1].(type) { 245 | case ua.LiteralOperand: 246 | b = c.Value 247 | case ua.SimpleAttributeOperand: 248 | b = evt.GetAttribute(c) 249 | case ua.ElementOperand: 250 | b = mi.whereClause(evt, int(c.Index)) 251 | default: 252 | return false 253 | } 254 | return a == b 255 | 256 | case ua.FilterOperatorOfType: 257 | if a, ok := element.FilterOperands[0].(ua.LiteralOperand); ok { 258 | if b, ok := a.Value.(ua.NodeID); ok { 259 | if c, ok := evt.GetAttribute(attributeOperandEventType).(ua.NodeID); ok { 260 | if c == b || mi.srv.namespaceManager.IsSubtype(c, b) { 261 | return true 262 | } 263 | } 264 | } 265 | } 266 | return false 267 | 268 | default: 269 | return false 270 | } 271 | } 272 | 273 | func (mi *EventMonitoredItem) selectFields(evt ua.Event) []ua.Variant { 274 | clauses := mi.eventFilter.SelectClauses 275 | ret := make([]ua.Variant, len(clauses)) 276 | for i, clause := range clauses { 277 | ret[i] = evt.GetAttribute(clause) 278 | } 279 | return ret 280 | } 281 | 282 | func (mi *EventMonitoredItem) startMonitoring() { 283 | if mi.monitoringMode == ua.MonitoringModeDisabled { 284 | return 285 | } 286 | if n2, ok := mi.node.(*ObjectNode); ok { 287 | n2.AddEventListener(mi) 288 | } 289 | } 290 | 291 | func (mi *EventMonitoredItem) stopMonitoring() { 292 | if n2, ok := mi.node.(*ObjectNode); ok { 293 | n2.RemoveEventListener(mi) 294 | } 295 | } 296 | 297 | func (mi *EventMonitoredItem) notifications(max int) (notifications []any, more bool) { 298 | mi.Lock() 299 | defer mi.Unlock() 300 | notifications = make([]any, 0, 4) 301 | for i := 0; i < max; i++ { 302 | if mi.queue.Len() > 0 { 303 | notifications = append(notifications, mi.queue.PopFront()) 304 | } else { 305 | break 306 | } 307 | } 308 | more = mi.queue.Len() > 0 309 | if mi.triggered && !more { 310 | mi.triggered = false 311 | // log.Printf("Reset triggered %d", mi.id) 312 | } 313 | return notifications, more 314 | } 315 | 316 | func (mi *EventMonitoredItem) notificationsAvailable(tn time.Time, late bool, resend bool) bool { 317 | _ = late 318 | mi.Lock() 319 | defer mi.Unlock() 320 | // if disabled, then report false. 321 | if mi.monitoringMode == ua.MonitoringModeDisabled { 322 | return false 323 | } 324 | 325 | return mi.queue.Len() > 0 && (mi.monitoringMode == ua.MonitoringModeReporting || mi.triggered) 326 | } 327 | 328 | // AddTriggeredItem adds a item to be triggered by this item. 329 | func (mi *EventMonitoredItem) AddTriggeredItem(item MonitoredItem) bool { 330 | mi.Lock() 331 | mi.triggeredItems = append(mi.triggeredItems, item) 332 | mi.Unlock() 333 | return true 334 | } 335 | 336 | // RemoveTriggeredItem removes an item to be triggered by this item. 337 | func (mi *EventMonitoredItem) RemoveTriggeredItem(item MonitoredItem) bool { 338 | mi.Lock() 339 | ret := false 340 | for i, e := range mi.triggeredItems { 341 | if e.ID() == item.ID() { 342 | mi.triggeredItems[i] = mi.triggeredItems[len(mi.triggeredItems)-1] 343 | mi.triggeredItems[len(mi.triggeredItems)-1] = nil 344 | mi.triggeredItems = mi.triggeredItems[:len(mi.triggeredItems)-1] 345 | ret = true 346 | break 347 | } 348 | } 349 | mi.Unlock() 350 | return ret 351 | } 352 | -------------------------------------------------------------------------------- /server/history_read_writer.go: -------------------------------------------------------------------------------- 1 | // Copyright 2021 Converter Systems LLC. All rights reserved. 2 | 3 | package server 4 | 5 | import ( 6 | "context" 7 | 8 | "github.com/awcullen/opcua/ua" 9 | ) 10 | 11 | // HistoryReadWriter provides methods to read and write historical data. 12 | type HistoryReadWriter interface { 13 | HistoryReader 14 | HistoryWriter 15 | } 16 | 17 | // HistoryWriter provides methods to write historical data. 18 | type HistoryWriter interface { 19 | 20 | // WriteEvent writes the event to storage. Implementation records object nodeId 21 | // and event fields (provided as slice of Variants). Implementation may check 22 | // context for timeout. 23 | WriteEvent(ctx context.Context, nodeID ua.NodeID, eventFields []ua.Variant) error 24 | 25 | // WriteValue writes the value to storage. Implementation records variable nodeId 26 | // and DataValue (a struct of value, quality and source timestamp). Implementation 27 | // may check context for timeout. 28 | WriteValue(ctx context.Context, nodeID ua.NodeID, value ua.DataValue) error 29 | } 30 | 31 | // HistoryReader provides methods to read historical data. 32 | type HistoryReader interface { 33 | 34 | // ReadEvent reads the events from storage. Implementation returns slice of events for every 35 | // NodeID provided in 'nodesToRead', given StartTime, EndTime and other parameters in 'details'. 36 | // Implementation may check context for timeout. Implementation must return desired choice of 37 | // timestamps. Implementation must return ContinuationPoints if more results are available 38 | // than can be returned in current call. Implementation must release ContinuationPoints 39 | // if no further results are desired. See OPC UA Part 11 chapter 6.4.2.2 for Read Event functionality. 40 | ReadEvent(ctx context.Context, nodesToRead []ua.HistoryReadValueID, details ua.ReadEventDetails, 41 | timestampsToReturn ua.TimestampsToReturn, releaseContinuationPoints bool) ([]ua.HistoryReadResult, ua.StatusCode) 42 | 43 | // ReadRawModified reads the raw or modified data values from storage. Implementation returns 44 | // slice of data values for every NodeID provided in 'nodesToRead', given StartTime, EndTime and 45 | // other parameters in 'details'. Implementation may check context for timeout. Implementation must 46 | // return desired choice of timestamps. Implementation must return ContinuationPoints if more results 47 | // are available than can be returned in current call. Implementation must release ContinuationPoints 48 | // if no further results are desired. See OPC UA Part 11 chapter 6.4.3.2 for Read Raw functionality. 49 | ReadRawModified(ctx context.Context, nodesToRead []ua.HistoryReadValueID, details ua.ReadRawModifiedDetails, 50 | timestampsToReturn ua.TimestampsToReturn, releaseContinuationPoints bool) ([]ua.HistoryReadResult, ua.StatusCode) 51 | 52 | // ReadProcessed reads the aggregated values from storage. Implementation returns slice of 53 | // aggregated data values for every NodeID provided in 'nodesToRead', given StartTime, EndTime and 54 | // other parameters in 'details'. Implementation may check context for timeout. Implementation must 55 | // return desired choice of timestamps. Implementation must return ContinuationPoints if more results 56 | // are available than can be returned in current call. Implementation must release ContinuationPoints 57 | // if no further results are desired. See OPC UA Part 11 chapter 6.4.4.2 for Read Processed functionality. 58 | ReadProcessed(ctx context.Context, nodesToRead []ua.HistoryReadValueID, details ua.ReadProcessedDetails, 59 | timestampsToReturn ua.TimestampsToReturn, releaseContinuationPoints bool) ([]ua.HistoryReadResult, ua.StatusCode) 60 | 61 | // ReadAtTime reads the correlated values from storage. Implementation returns slice of 62 | // correlated data values for every NodeID provided in 'nodesToRead', given slice of timestamps and 63 | // other parameters in 'details'. Implementation may check context for timeout. Implementation must 64 | // return desired choice of timestamps. Implementation must return ContinuationPoints if more results 65 | // are available than can be returned in current call. Implementation must release ContinuationPoints 66 | // if no further results are desired. See OPC UA Part 11 chapter 6.4.5.2 for Read At Time functionality. 67 | ReadAtTime(ctx context.Context, nodesToRead []ua.HistoryReadValueID, details ua.ReadAtTimeDetails, 68 | timestampsToReturn ua.TimestampsToReturn, releaseContinuationPoints bool) ([]ua.HistoryReadResult, ua.StatusCode) 69 | } 70 | -------------------------------------------------------------------------------- /server/method_node.go: -------------------------------------------------------------------------------- 1 | // Copyright 2021 Converter Systems LLC. All rights reserved. 2 | 3 | package server 4 | 5 | import ( 6 | "sync" 7 | 8 | "github.com/awcullen/opcua/ua" 9 | ) 10 | 11 | // MethodNode is a Node class that describes the syntax of a object's Method. 12 | type MethodNode struct { 13 | sync.RWMutex 14 | server *Server 15 | nodeID ua.NodeID 16 | nodeClass ua.NodeClass 17 | browseName ua.QualifiedName 18 | displayName ua.LocalizedText 19 | description ua.LocalizedText 20 | rolePermissions []ua.RolePermissionType 21 | accessRestrictions uint16 22 | references []ua.Reference 23 | executable bool 24 | callMethodHandler func(*Session, ua.CallMethodRequest) ua.CallMethodResult 25 | } 26 | 27 | var _ Node = (*MethodNode)(nil) 28 | 29 | // NewMethodNode constructs a new MethodNode. 30 | func NewMethodNode(server *Server, nodeID ua.NodeID, browseName ua.QualifiedName, displayName ua.LocalizedText, description ua.LocalizedText, rolePermissions []ua.RolePermissionType, references []ua.Reference, executable bool) *MethodNode { 31 | return &MethodNode{ 32 | server: server, 33 | nodeID: nodeID, 34 | nodeClass: ua.NodeClassMethod, 35 | browseName: browseName, 36 | displayName: displayName, 37 | description: description, 38 | rolePermissions: rolePermissions, 39 | accessRestrictions: 0, 40 | references: references, 41 | executable: executable, 42 | } 43 | } 44 | 45 | // NodeID returns the NodeID attribute of this node. 46 | func (n *MethodNode) NodeID() ua.NodeID { 47 | return n.nodeID 48 | } 49 | 50 | // NodeClass returns the NodeClass attribute of this node. 51 | func (n *MethodNode) NodeClass() ua.NodeClass { 52 | return n.nodeClass 53 | } 54 | 55 | // BrowseName returns the BrowseName attribute of this node. 56 | func (n *MethodNode) BrowseName() ua.QualifiedName { 57 | return n.browseName 58 | } 59 | 60 | // DisplayName returns the DisplayName attribute of this node. 61 | func (n *MethodNode) DisplayName() ua.LocalizedText { 62 | return n.displayName 63 | } 64 | 65 | // Description returns the Description attribute of this node. 66 | func (n *MethodNode) Description() ua.LocalizedText { 67 | return n.description 68 | } 69 | 70 | // RolePermissions returns the RolePermissions attribute of this node. 71 | func (n *MethodNode) RolePermissions() []ua.RolePermissionType { 72 | return n.rolePermissions 73 | } 74 | 75 | // UserRolePermissions returns the RolePermissions attribute of this node for the current user. 76 | func (n *MethodNode) UserRolePermissions(userIdentity any) []ua.RolePermissionType { 77 | filteredPermissions := []ua.RolePermissionType{} 78 | roles, err := n.server.GetRoles(userIdentity, "", "") 79 | if err != nil { 80 | return filteredPermissions 81 | } 82 | rolePermissions := n.RolePermissions() 83 | if rolePermissions == nil { 84 | rolePermissions = n.server.RolePermissions() 85 | } 86 | for _, rp := range rolePermissions { 87 | for _, r := range roles { 88 | if rp.RoleID == r { 89 | filteredPermissions = append(filteredPermissions, rp) 90 | } 91 | } 92 | } 93 | return filteredPermissions 94 | } 95 | 96 | // References returns the References of this node. 97 | func (n *MethodNode) References() []ua.Reference { 98 | n.RLock() 99 | defer n.RUnlock() 100 | return n.references 101 | } 102 | 103 | // SetReferences sets the References of the Variable. 104 | func (n *MethodNode) SetReferences(value []ua.Reference) { 105 | n.Lock() 106 | defer n.Unlock() 107 | n.references = value 108 | } 109 | 110 | // Executable returns the Executable attribute of this node. 111 | func (n *MethodNode) Executable() bool { 112 | return n.executable 113 | } 114 | 115 | // UserExecutable returns the UserExecutable attribute of this node. 116 | func (n *MethodNode) UserExecutable(userIdentity any) bool { 117 | if !n.executable { 118 | return false 119 | } 120 | roles, err := n.server.GetRoles(userIdentity, "", "") 121 | if err != nil { 122 | return false 123 | } 124 | rolePermissions := n.RolePermissions() 125 | if rolePermissions == nil { 126 | rolePermissions = n.server.RolePermissions() 127 | } 128 | for _, role := range roles { 129 | for _, rp := range rolePermissions { 130 | if rp.RoleID == role && rp.Permissions&ua.PermissionTypeCall != 0 { 131 | return true 132 | } 133 | } 134 | } 135 | return false 136 | } 137 | 138 | // SetCallMethodHandler sets the CallMethod of the Variable. 139 | func (n *MethodNode) SetCallMethodHandler(value func(*Session, ua.CallMethodRequest) ua.CallMethodResult) { 140 | n.Lock() 141 | defer n.Unlock() 142 | n.callMethodHandler = value 143 | } 144 | 145 | // IsAttributeIDValid returns true if attributeId is supported for the node. 146 | func (n *MethodNode) IsAttributeIDValid(attributeID uint32) bool { 147 | switch attributeID { 148 | case ua.AttributeIDNodeID, ua.AttributeIDNodeClass, ua.AttributeIDBrowseName, 149 | ua.AttributeIDDisplayName, ua.AttributeIDDescription, ua.AttributeIDRolePermissions, 150 | ua.AttributeIDUserRolePermissions, ua.AttributeIDExecutable, ua.AttributeIDUserExecutable: 151 | return true 152 | default: 153 | return false 154 | } 155 | } 156 | -------------------------------------------------------------------------------- /server/monitored_item.go: -------------------------------------------------------------------------------- 1 | // Copyright 2021 Converter Systems LLC. All rights reserved. 2 | 3 | package server 4 | 5 | import ( 6 | "time" 7 | 8 | "github.com/awcullen/opcua/ua" 9 | ) 10 | 11 | const ( 12 | maxQueueSize = 1024 13 | maxSamplingInterval = 60 * 1000.0 14 | ) 15 | 16 | var ( 17 | monitoredItemID = uint32(0) 18 | ) 19 | 20 | // MonitoredItem specifies a node that is monitored 21 | type MonitoredItem interface { 22 | ID() uint32 23 | Node() Node 24 | ItemToMonitor() ua.ReadValueID 25 | SamplingInterval() float64 26 | QueueSize() uint32 27 | MonitoringMode() ua.MonitoringMode 28 | ClientHandle() uint32 29 | Triggered() bool 30 | SetTriggered(bool) 31 | Modify(req ua.MonitoredItemModifyRequest) ua.MonitoredItemModifyResult 32 | Delete() 33 | SetMonitoringMode(mode ua.MonitoringMode) 34 | notifications(max int) (notifications []any, more bool) 35 | notificationsAvailable(tn time.Time, late bool, resend bool) bool 36 | AddTriggeredItem(item MonitoredItem) bool 37 | RemoveTriggeredItem(item MonitoredItem) bool 38 | } 39 | -------------------------------------------------------------------------------- /server/node.go: -------------------------------------------------------------------------------- 1 | // Copyright 2021 Converter Systems LLC. All rights reserved. 2 | 3 | package server 4 | 5 | import ( 6 | "github.com/awcullen/opcua/ua" 7 | ) 8 | 9 | // Node ... 10 | type Node interface { 11 | NodeID() ua.NodeID 12 | NodeClass() ua.NodeClass 13 | BrowseName() ua.QualifiedName 14 | DisplayName() ua.LocalizedText 15 | Description() ua.LocalizedText 16 | RolePermissions() []ua.RolePermissionType 17 | UserRolePermissions(userIdentity any) []ua.RolePermissionType 18 | References() []ua.Reference 19 | SetReferences([]ua.Reference) 20 | IsAttributeIDValid(uint32) bool 21 | } 22 | -------------------------------------------------------------------------------- /server/object_node.go: -------------------------------------------------------------------------------- 1 | // Copyright 2021 Converter Systems LLC. All rights reserved. 2 | 3 | package server 4 | 5 | import ( 6 | "sync" 7 | 8 | "github.com/awcullen/opcua/ua" 9 | ) 10 | 11 | // ObjectNode ... 12 | type ObjectNode struct { 13 | sync.RWMutex 14 | server *Server 15 | nodeID ua.NodeID 16 | nodeClass ua.NodeClass 17 | browseName ua.QualifiedName 18 | displayName ua.LocalizedText 19 | description ua.LocalizedText 20 | rolePermissions []ua.RolePermissionType 21 | accessRestrictions uint16 22 | references []ua.Reference 23 | eventNotifier byte 24 | subs map[EventListener]struct{} 25 | } 26 | 27 | var _ Node = (*ObjectNode)(nil) 28 | 29 | // NewObjectNode ... 30 | func NewObjectNode(server *Server, nodeID ua.NodeID, browseName ua.QualifiedName, displayName ua.LocalizedText, description ua.LocalizedText, rolePermissions []ua.RolePermissionType, references []ua.Reference, eventNotifier byte) *ObjectNode { 31 | return &ObjectNode{ 32 | server: server, 33 | nodeID: nodeID, 34 | nodeClass: ua.NodeClassObject, 35 | browseName: browseName, 36 | displayName: displayName, 37 | description: description, 38 | rolePermissions: rolePermissions, 39 | accessRestrictions: 0, 40 | references: references, 41 | eventNotifier: eventNotifier, 42 | subs: map[EventListener]struct{}{}, 43 | } 44 | } 45 | 46 | // NodeID returns the NodeID attribute of this node. 47 | func (n *ObjectNode) NodeID() ua.NodeID { 48 | return n.nodeID 49 | } 50 | 51 | // NodeClass returns the NodeClass attribute of this node. 52 | func (n *ObjectNode) NodeClass() ua.NodeClass { 53 | return n.nodeClass 54 | } 55 | 56 | // BrowseName returns the BrowseName attribute of this node. 57 | func (n *ObjectNode) BrowseName() ua.QualifiedName { 58 | return n.browseName 59 | } 60 | 61 | // DisplayName returns the DisplayName attribute of this node. 62 | func (n *ObjectNode) DisplayName() ua.LocalizedText { 63 | return n.displayName 64 | } 65 | 66 | // Description returns the Description attribute of this node. 67 | func (n *ObjectNode) Description() ua.LocalizedText { 68 | return n.description 69 | } 70 | 71 | // RolePermissions returns the RolePermissions attribute of this node. 72 | func (n *ObjectNode) RolePermissions() []ua.RolePermissionType { 73 | return n.rolePermissions 74 | } 75 | 76 | // UserRolePermissions returns the RolePermissions attribute of this node for the current user. 77 | func (n *ObjectNode) UserRolePermissions(userIdentity any) []ua.RolePermissionType { 78 | filteredPermissions := []ua.RolePermissionType{} 79 | roles, err := n.server.GetRoles(userIdentity, "", "") 80 | if err != nil { 81 | return filteredPermissions 82 | } 83 | rolePermissions := n.RolePermissions() 84 | if rolePermissions == nil { 85 | rolePermissions = n.server.RolePermissions() 86 | } 87 | for _, role := range roles { 88 | for _, rp := range rolePermissions { 89 | if rp.RoleID == role { 90 | filteredPermissions = append(filteredPermissions, rp) 91 | } 92 | } 93 | } 94 | return filteredPermissions 95 | } 96 | 97 | // References returns the References of this node. 98 | func (n *ObjectNode) References() []ua.Reference { 99 | n.RLock() 100 | defer n.RUnlock() 101 | return n.references 102 | } 103 | 104 | // SetReferences sets the References of the Variable. 105 | func (n *ObjectNode) SetReferences(value []ua.Reference) { 106 | n.Lock() 107 | defer n.Unlock() 108 | n.references = value 109 | } 110 | 111 | // EventNotifier returns the EventNotifier attribute of this node. 112 | func (n *ObjectNode) EventNotifier() byte { 113 | return n.eventNotifier 114 | } 115 | 116 | // OnEvent raises an event from this node. 117 | func (n *ObjectNode) OnEvent(evt ua.Event) { 118 | n.RLock() 119 | defer n.RUnlock() 120 | for sub := range n.subs { 121 | sub.OnEvent(evt) 122 | } 123 | } 124 | 125 | type EventListener interface { 126 | OnEvent(ua.Event) 127 | } 128 | 129 | func (n *ObjectNode) AddEventListener(listener EventListener) { 130 | n.Lock() 131 | defer n.Unlock() 132 | n.subs[listener] = struct{}{} 133 | } 134 | 135 | func (n *ObjectNode) RemoveEventListener(listener EventListener) { 136 | n.Lock() 137 | defer n.Unlock() 138 | delete(n.subs, listener) 139 | } 140 | 141 | // IsAttributeIDValid returns true if attributeId is supported for the node. 142 | func (n *ObjectNode) IsAttributeIDValid(attributeID uint32) bool { 143 | switch attributeID { 144 | case ua.AttributeIDNodeID, ua.AttributeIDNodeClass, ua.AttributeIDBrowseName, 145 | ua.AttributeIDDisplayName, ua.AttributeIDDescription, ua.AttributeIDRolePermissions, 146 | ua.AttributeIDUserRolePermissions, ua.AttributeIDEventNotifier: 147 | return true 148 | default: 149 | return false 150 | } 151 | } 152 | -------------------------------------------------------------------------------- /server/object_type_node.go: -------------------------------------------------------------------------------- 1 | // Copyright 2021 Converter Systems LLC. All rights reserved. 2 | 3 | package server 4 | 5 | import ( 6 | "sync" 7 | 8 | "github.com/awcullen/opcua/ua" 9 | ) 10 | 11 | // ObjectTypeNode ... 12 | type ObjectTypeNode struct { 13 | sync.RWMutex 14 | server *Server 15 | nodeID ua.NodeID 16 | nodeClass ua.NodeClass 17 | browseName ua.QualifiedName 18 | displayName ua.LocalizedText 19 | description ua.LocalizedText 20 | rolePermissions []ua.RolePermissionType 21 | accessRestrictions uint16 22 | references []ua.Reference 23 | isAbstract bool 24 | } 25 | 26 | var _ Node = (*ObjectTypeNode)(nil) 27 | 28 | // NewObjectTypeNode ... 29 | func NewObjectTypeNode(server *Server, nodeID ua.NodeID, browseName ua.QualifiedName, displayName ua.LocalizedText, description ua.LocalizedText, rolePermissions []ua.RolePermissionType, references []ua.Reference, isAbstract bool) *ObjectTypeNode { 30 | return &ObjectTypeNode{ 31 | server: server, 32 | nodeID: nodeID, 33 | nodeClass: ua.NodeClassObjectType, 34 | browseName: browseName, 35 | displayName: displayName, 36 | description: description, 37 | rolePermissions: rolePermissions, 38 | accessRestrictions: 0, 39 | references: references, 40 | isAbstract: isAbstract, 41 | } 42 | } 43 | 44 | // NodeID returns the NodeID attribute of this node. 45 | func (n *ObjectTypeNode) NodeID() ua.NodeID { 46 | return n.nodeID 47 | } 48 | 49 | // NodeClass returns the NodeClass attribute of this node. 50 | func (n *ObjectTypeNode) NodeClass() ua.NodeClass { 51 | return n.nodeClass 52 | } 53 | 54 | // BrowseName returns the BrowseName attribute of this node. 55 | func (n *ObjectTypeNode) BrowseName() ua.QualifiedName { 56 | return n.browseName 57 | } 58 | 59 | // DisplayName returns the DisplayName attribute of this node. 60 | func (n *ObjectTypeNode) DisplayName() ua.LocalizedText { 61 | return n.displayName 62 | } 63 | 64 | // Description returns the Description attribute of this node. 65 | func (n *ObjectTypeNode) Description() ua.LocalizedText { 66 | return n.description 67 | } 68 | 69 | // RolePermissions returns the RolePermissions attribute of this node. 70 | func (n *ObjectTypeNode) RolePermissions() []ua.RolePermissionType { 71 | return n.rolePermissions 72 | } 73 | 74 | // UserRolePermissions returns the RolePermissions attribute of this node for the current user. 75 | func (n *ObjectTypeNode) UserRolePermissions(userIdentity any) []ua.RolePermissionType { 76 | filteredPermissions := []ua.RolePermissionType{} 77 | roles, err := n.server.GetRoles(userIdentity, "", "") 78 | if err != nil { 79 | return filteredPermissions 80 | } 81 | rolePermissions := n.RolePermissions() 82 | if rolePermissions == nil { 83 | rolePermissions = n.server.RolePermissions() 84 | } 85 | for _, role := range roles { 86 | for _, rp := range rolePermissions { 87 | if rp.RoleID == role { 88 | filteredPermissions = append(filteredPermissions, rp) 89 | } 90 | } 91 | } 92 | return filteredPermissions 93 | } 94 | 95 | // References returns the References of this node. 96 | func (n *ObjectTypeNode) References() []ua.Reference { 97 | n.RLock() 98 | defer n.RUnlock() 99 | return n.references 100 | } 101 | 102 | // SetReferences sets the References of the Variable. 103 | func (n *ObjectTypeNode) SetReferences(value []ua.Reference) { 104 | n.Lock() 105 | defer n.Unlock() 106 | n.references = value 107 | } 108 | 109 | // IsAbstract returns the IsAbstract attribute of this node. 110 | func (n *ObjectTypeNode) IsAbstract() bool { 111 | return n.isAbstract 112 | } 113 | 114 | // IsAttributeIDValid returns true if attributeId is supported for the node. 115 | func (n *ObjectTypeNode) IsAttributeIDValid(attributeID uint32) bool { 116 | switch attributeID { 117 | case ua.AttributeIDNodeID, ua.AttributeIDNodeClass, ua.AttributeIDBrowseName, 118 | ua.AttributeIDDisplayName, ua.AttributeIDDescription, ua.AttributeIDRolePermissions, 119 | ua.AttributeIDUserRolePermissions, ua.AttributeIDIsAbstract: 120 | return true 121 | default: 122 | return false 123 | } 124 | } 125 | -------------------------------------------------------------------------------- /server/option.go: -------------------------------------------------------------------------------- 1 | // Copyright 2021 Converter Systems LLC. All rights reserved. 2 | 3 | package server 4 | 5 | import "github.com/awcullen/opcua/ua" 6 | 7 | // Option is a functional option to be applied to a server during initialization. 8 | type Option func(*Server) error 9 | 10 | // WithMaxSessionCount sets the number of sessions that may be active. (default: no limit) 11 | func WithMaxSessionCount(value uint32) Option { 12 | return func(srv *Server) error { 13 | srv.maxSessionCount = value 14 | return nil 15 | } 16 | } 17 | 18 | // WithMaxSubscriptionCount sets the number of subscription that may be active. (default: no limit) 19 | func WithMaxSubscriptionCount(value uint32) Option { 20 | return func(srv *Server) error { 21 | srv.maxSubscriptionCount = value 22 | return nil 23 | } 24 | } 25 | 26 | // WithServerCapabilities sets the ServerCapabilities. 27 | func WithServerCapabilities(value *ua.ServerCapabilities) Option { 28 | return func(srv *Server) error { 29 | srv.serverCapabilities = value 30 | return nil 31 | } 32 | } 33 | 34 | // WithBuildInfo sets the BuildInfo returned by ServerStatus. 35 | func WithBuildInfo(value ua.BuildInfo) Option { 36 | return func(srv *Server) error { 37 | srv.buildInfo = value 38 | return nil 39 | } 40 | } 41 | 42 | // WithTrustedCertificatesPaths sets the file path of the trusted certificates and revocation lists. 43 | // Path may be to a file, comma-separated list of files, or directory. 44 | func WithTrustedCertificatesPaths(certPath, crlPath string) Option { 45 | return func(srv *Server) error { 46 | srv.trustedCertsPath = certPath 47 | srv.trustedCRLsPath = crlPath 48 | return nil 49 | } 50 | } 51 | 52 | // WithIssuerCertificatesPaths sets the file path of the issuer certificates and revocation lists. 53 | // Issuer certificates are needed for validation, but are not trusted. 54 | // Path may be to a file, comma-separated list of files, or directory. 55 | func WithIssuerCertificatesPaths(certPath, crlPath string) Option { 56 | return func(srv *Server) error { 57 | srv.issuerCertsPath = certPath 58 | srv.issuerCRLsPath = crlPath 59 | return nil 60 | } 61 | } 62 | 63 | // WithRejectedCertificatesPath sets the file path where rejected certificates are stored. 64 | // Path must be to a directory. 65 | func WithRejectedCertificatesPath(path string) Option { 66 | return func(srv *Server) error { 67 | srv.rejectedCertsPath = path 68 | return nil 69 | } 70 | } 71 | 72 | // WithInsecureSkipVerify skips verification of client certificate. Skips checking HostName, Expiration, and Authority. 73 | func WithInsecureSkipVerify() Option { 74 | return func(srv *Server) error { 75 | srv.suppressCertificateExpired = true 76 | srv.suppressCertificateChainIncomplete = true 77 | srv.suppressCertificateRevocationUnknown = true 78 | return nil 79 | } 80 | } 81 | 82 | // WithTransportLimits sets the limits on the size of the buffers and messages. (default: 64Kb, 64Mb, 4096) 83 | func WithTransportLimits(maxBufferSize, maxMessageSize, maxChunkCount uint32) Option { 84 | return func(srv *Server) error { 85 | srv.maxBufferSize = maxBufferSize 86 | srv.maxMessageSize = maxMessageSize 87 | srv.maxChunkCount = maxChunkCount 88 | return nil 89 | } 90 | } 91 | 92 | // WithMaxWorkerThreads sets the default number of worker threads that may be created. (default: 4) 93 | func WithMaxWorkerThreads(value int) Option { 94 | return func(opts *Server) error { 95 | opts.maxWorkerThreads = value 96 | return nil 97 | } 98 | } 99 | 100 | // WithServerDiagnostics sets whether to enable the collection of data used for ServerDiagnostics node. 101 | func WithServerDiagnostics(value bool) Option { 102 | return func(opts *Server) error { 103 | opts.serverDiagnostics = value 104 | return nil 105 | } 106 | } 107 | 108 | // WithTrace logs all ServiceRequests and ServiceResponses to StdOut. 109 | func WithTrace() Option { 110 | return func(srv *Server) error { 111 | srv.trace = true 112 | return nil 113 | } 114 | } 115 | 116 | // WithAnonymousIdentity sets whether to allow anonymous identity. 117 | func WithAnonymousIdentity(value bool) Option { 118 | return func(srv *Server) error { 119 | if value { 120 | srv.anonymousIdentityAuthenticator = AuthenticateAnonymousIdentityFunc(func(userIdentity ua.AnonymousIdentity, applicationURI string, endpointURL string) error { 121 | return nil 122 | }) 123 | } else { 124 | srv.anonymousIdentityAuthenticator = nil 125 | } 126 | return nil 127 | } 128 | } 129 | 130 | // WithSecurityPolicyNone sets whether to allow security policy with no encryption. 131 | func WithSecurityPolicyNone(value bool) Option { 132 | return func(srv *Server) error { 133 | srv.allowSecurityPolicyNone = value 134 | return nil 135 | } 136 | } 137 | 138 | // WithAnonymousIdentityAuthenticator sets the authenticator for AnonymousIdentity. 139 | // Provided authenticator can check applicationURI of the client certificate, if provided. 140 | func WithAnonymousIdentityAuthenticator(authenticator AnonymousIdentityAuthenticator) Option { 141 | return func(srv *Server) error { 142 | srv.anonymousIdentityAuthenticator = authenticator 143 | return nil 144 | } 145 | } 146 | 147 | // WithAuthenticateAnonymousIdentityFunc sets the authenticate func for AnonymousIdentity. 148 | // Provided function can check applicationURI of the client certificate, if provided. 149 | func WithAuthenticateAnonymousIdentityFunc(f AuthenticateAnonymousIdentityFunc) Option { 150 | return func(srv *Server) error { 151 | srv.anonymousIdentityAuthenticator = f 152 | return nil 153 | } 154 | } 155 | 156 | // WithUserNameIdentityAuthenticator sets the authenticator for UserNameIdentity. 157 | func WithUserNameIdentityAuthenticator(authenticator UserNameIdentityAuthenticator) Option { 158 | return func(srv *Server) error { 159 | srv.userNameIdentityAuthenticator = authenticator 160 | return nil 161 | } 162 | } 163 | 164 | // WithAuthenticateUserNameIdentityFunc sets the authenticate func for UserNameIdentity. 165 | func WithAuthenticateUserNameIdentityFunc(f AuthenticateUserNameIdentityFunc) Option { 166 | return func(srv *Server) error { 167 | srv.userNameIdentityAuthenticator = f 168 | return nil 169 | } 170 | } 171 | 172 | // WithX509IdentityAuthenticator sets the authenticator for X509Identity. 173 | func WithX509IdentityAuthenticator(authenticator X509IdentityAuthenticator) Option { 174 | return func(srv *Server) error { 175 | srv.x509IdentityAuthenticator = authenticator 176 | return nil 177 | } 178 | } 179 | 180 | // WithAuthenticateX509IdentityFunc sets the authenticate func for X509Identity. 181 | func WithAuthenticateX509IdentityFunc(f AuthenticateX509IdentityFunc) Option { 182 | return func(srv *Server) error { 183 | srv.x509IdentityAuthenticator = f 184 | return nil 185 | } 186 | } 187 | 188 | // WithRolesProvider sets the RolesProvider. 189 | func WithRolesProvider(provider RolesProvider) Option { 190 | return func(srv *Server) error { 191 | srv.rolesProvider = provider 192 | return nil 193 | } 194 | } 195 | 196 | // WithGetRolesFunc sets the GetRolesFunc that returns the roles for the given user identity. 197 | func WithGetRolesFunc(f GetRolesFunc) Option { 198 | return func(srv *Server) error { 199 | srv.rolesProvider = f 200 | return nil 201 | } 202 | } 203 | 204 | // WithRolePermissions sets the permissions for each role. 205 | func WithRolePermissions(permissions []ua.RolePermissionType) Option { 206 | return func(srv *Server) error { 207 | srv.rolePermissions = permissions 208 | return nil 209 | } 210 | } 211 | 212 | // WithHistorian sets the HistoryReadWriter. 213 | func WithHistorian(historian HistoryReadWriter) Option { 214 | return func(srv *Server) error { 215 | srv.historian = historian 216 | return nil 217 | } 218 | } 219 | -------------------------------------------------------------------------------- /server/reference_type_node.go: -------------------------------------------------------------------------------- 1 | package server 2 | 3 | import ( 4 | "sync" 5 | 6 | "github.com/awcullen/opcua/ua" 7 | ) 8 | 9 | // ReferenceTypeNode ... 10 | type ReferenceTypeNode struct { 11 | sync.RWMutex 12 | server *Server 13 | nodeID ua.NodeID 14 | nodeClass ua.NodeClass 15 | browseName ua.QualifiedName 16 | displayName ua.LocalizedText 17 | description ua.LocalizedText 18 | rolePermissions []ua.RolePermissionType 19 | accessRestrictions uint16 20 | references []ua.Reference 21 | isAbstract bool 22 | symmetric bool 23 | inverseName ua.LocalizedText 24 | } 25 | 26 | var _ Node = (*ReferenceTypeNode)(nil) 27 | 28 | // NewReferenceTypeNode ... 29 | func NewReferenceTypeNode(server *Server, nodeID ua.NodeID, browseName ua.QualifiedName, displayName ua.LocalizedText, description ua.LocalizedText, rolePermissions []ua.RolePermissionType, references []ua.Reference, isAbstract bool, symmetric bool, inverseName ua.LocalizedText) *ReferenceTypeNode { 30 | return &ReferenceTypeNode{ 31 | server: server, 32 | nodeID: nodeID, 33 | nodeClass: ua.NodeClassReferenceType, 34 | browseName: browseName, 35 | displayName: displayName, 36 | description: description, 37 | rolePermissions: rolePermissions, 38 | accessRestrictions: 0, 39 | references: references, 40 | isAbstract: isAbstract, 41 | symmetric: symmetric, 42 | inverseName: inverseName, 43 | } 44 | } 45 | 46 | // NodeID returns the NodeID attribute of this node. 47 | func (n *ReferenceTypeNode) NodeID() ua.NodeID { 48 | return n.nodeID 49 | } 50 | 51 | // NodeClass returns the NodeClass attribute of this node. 52 | func (n *ReferenceTypeNode) NodeClass() ua.NodeClass { 53 | return n.nodeClass 54 | } 55 | 56 | // BrowseName returns the BrowseName attribute of this node. 57 | func (n *ReferenceTypeNode) BrowseName() ua.QualifiedName { 58 | return n.browseName 59 | } 60 | 61 | // DisplayName returns the DisplayName attribute of this node. 62 | func (n *ReferenceTypeNode) DisplayName() ua.LocalizedText { 63 | return n.displayName 64 | } 65 | 66 | // Description returns the Description attribute of this node. 67 | func (n *ReferenceTypeNode) Description() ua.LocalizedText { 68 | return n.description 69 | } 70 | 71 | // RolePermissions returns the RolePermissions attribute of this node. 72 | func (n *ReferenceTypeNode) RolePermissions() []ua.RolePermissionType { 73 | return n.rolePermissions 74 | } 75 | 76 | // UserRolePermissions returns the RolePermissions attribute of this node for the current user. 77 | func (n *ReferenceTypeNode) UserRolePermissions(userIdentity any) []ua.RolePermissionType { 78 | filteredPermissions := []ua.RolePermissionType{} 79 | roles, err := n.server.GetRoles(userIdentity, "", "") 80 | if err != nil { 81 | return filteredPermissions 82 | } 83 | rolePermissions := n.RolePermissions() 84 | if rolePermissions == nil { 85 | rolePermissions = n.server.RolePermissions() 86 | } 87 | for _, role := range roles { 88 | for _, rp := range rolePermissions { 89 | if rp.RoleID == role { 90 | filteredPermissions = append(filteredPermissions, rp) 91 | } 92 | } 93 | } 94 | return filteredPermissions 95 | } 96 | 97 | // References returns the References of this node. 98 | func (n *ReferenceTypeNode) References() []ua.Reference { 99 | n.RLock() 100 | defer n.RUnlock() 101 | return n.references 102 | } 103 | 104 | // SetReferences sets the References of the Variable. 105 | func (n *ReferenceTypeNode) SetReferences(value []ua.Reference) { 106 | n.Lock() 107 | defer n.Unlock() 108 | n.references = value 109 | } 110 | 111 | // IsAbstract returns the IsAbstract attribute of this node. 112 | func (n *ReferenceTypeNode) IsAbstract() bool { 113 | return n.isAbstract 114 | } 115 | 116 | // Symmetric returns the Symmetric attribute of this node. 117 | func (n *ReferenceTypeNode) Symmetric() bool { 118 | return n.symmetric 119 | } 120 | 121 | // InverseName returns the InverseName attribute of this node. 122 | func (n *ReferenceTypeNode) InverseName() ua.LocalizedText { 123 | return n.inverseName 124 | } 125 | 126 | // IsAttributeIDValid returns true if attributeId is supported for the node. 127 | func (n *ReferenceTypeNode) IsAttributeIDValid(attributeID uint32) bool { 128 | switch attributeID { 129 | case ua.AttributeIDNodeID, ua.AttributeIDNodeClass, ua.AttributeIDBrowseName, 130 | ua.AttributeIDDisplayName, ua.AttributeIDDescription, ua.AttributeIDRolePermissions, 131 | ua.AttributeIDUserRolePermissions, ua.AttributeIDIsAbstract, ua.AttributeIDSymmetric, 132 | ua.AttributeIDInverseName: 133 | return true 134 | default: 135 | return false 136 | } 137 | } 138 | -------------------------------------------------------------------------------- /server/roles_provider.go: -------------------------------------------------------------------------------- 1 | package server 2 | 3 | import ( 4 | "crypto/sha1" 5 | "fmt" 6 | 7 | "github.com/awcullen/opcua/ua" 8 | ) 9 | 10 | // RolesProvider looks up the roles for the given user identity and connection information. 11 | // Roles are identified by a NodeID. There are a number of well-known roles. 12 | // Later, users are granted Permissions to perform actions based on the user's role memberships. 13 | type RolesProvider interface { 14 | // GetRoles returns the roles for the given user identity and connection information. 15 | GetRoles(userIdentity any, applicationURI string, endpointURL string) ([]ua.NodeID, error) 16 | } 17 | 18 | // GetRolesFunc returns Roles for the given user identity and connection information. 19 | type GetRolesFunc func(userIdentity any, applicationURI string, endpointURL string) ([]ua.NodeID, error) 20 | 21 | // GetRoles ... 22 | func (f GetRolesFunc) GetRoles(userIdentity any, applicationURI string, endpointURL string) ([]ua.NodeID, error) { 23 | return f(userIdentity, applicationURI, endpointURL) 24 | } 25 | 26 | // IdentityMappingRule ... 27 | type IdentityMappingRule struct { 28 | NodeID ua.NodeID 29 | Identities []ua.IdentityMappingRuleType 30 | ApplicationsExclude bool 31 | Applications []string 32 | EndpointsExclude bool 33 | Endpoints []struct { 34 | EndpointUrl string 35 | SecurityMode string 36 | SecurityPolicyURI string 37 | TransportProfileUri string 38 | } 39 | } 40 | 41 | var ( 42 | // DefaultRolePermissions returns RolePermissionTypes for the well known roles. 43 | DefaultRolePermissions []ua.RolePermissionType = []ua.RolePermissionType{ 44 | {RoleID: ua.ObjectIDWellKnownRoleAnonymous, Permissions: (ua.PermissionTypeBrowse | ua.PermissionTypeRead | ua.PermissionTypeReadHistory | ua.PermissionTypeReceiveEvents)}, 45 | {RoleID: ua.ObjectIDWellKnownRoleAuthenticatedUser, Permissions: (ua.PermissionTypeBrowse | ua.PermissionTypeRead | ua.PermissionTypeReadHistory | ua.PermissionTypeReceiveEvents)}, 46 | {RoleID: ua.ObjectIDWellKnownRoleObserver, Permissions: (ua.PermissionTypeBrowse | ua.PermissionTypeRead | ua.PermissionTypeReadHistory | ua.PermissionTypeReceiveEvents)}, 47 | {RoleID: ua.ObjectIDWellKnownRoleOperator, Permissions: (ua.PermissionTypeBrowse | ua.PermissionTypeRead | ua.PermissionTypeWrite | ua.PermissionTypeReadHistory | ua.PermissionTypeReceiveEvents | ua.PermissionTypeCall)}, 48 | {RoleID: ua.ObjectIDWellKnownRoleEngineer, Permissions: (ua.PermissionTypeBrowse | ua.PermissionTypeRead | ua.PermissionTypeWrite | ua.PermissionTypeReadHistory | ua.PermissionTypeReceiveEvents | ua.PermissionTypeCall | ua.PermissionTypeWriteHistorizing)}, 49 | {RoleID: ua.ObjectIDWellKnownRoleSupervisor, Permissions: (ua.PermissionTypeBrowse | ua.PermissionTypeRead | ua.PermissionTypeWrite | ua.PermissionTypeReadHistory | ua.PermissionTypeReceiveEvents | ua.PermissionTypeCall)}, 50 | {RoleID: ua.ObjectIDWellKnownRoleConfigureAdmin, Permissions: (ua.PermissionTypeBrowse | ua.PermissionTypeRead | ua.PermissionTypeWriteAttribute)}, 51 | {RoleID: ua.ObjectIDWellKnownRoleSecurityAdmin, Permissions: (ua.PermissionTypeBrowse | ua.PermissionTypeReadRolePermissions | ua.PermissionTypeWriteRolePermissions)}, 52 | } 53 | // DefaultIdentityMappingRules ... 54 | DefaultIdentityMappingRules []IdentityMappingRule = []IdentityMappingRule{ 55 | // WellKnownRoleAnonymous 56 | { 57 | NodeID: ua.ObjectIDWellKnownRoleAnonymous, 58 | Identities: []ua.IdentityMappingRuleType{ 59 | {CriteriaType: ua.IdentityCriteriaTypeAnonymous}, 60 | }, 61 | ApplicationsExclude: true, 62 | EndpointsExclude: true, 63 | }, 64 | // WellKnownRoleAuthenticatedUser 65 | { 66 | NodeID: ua.ObjectIDWellKnownRoleAuthenticatedUser, 67 | Identities: []ua.IdentityMappingRuleType{ 68 | {CriteriaType: ua.IdentityCriteriaTypeAuthenticatedUser}, 69 | }, 70 | ApplicationsExclude: true, 71 | EndpointsExclude: true, 72 | }, 73 | // WellKnownRoleObserver 74 | { 75 | NodeID: ua.ObjectIDWellKnownRoleObserver, 76 | Identities: []ua.IdentityMappingRuleType{ 77 | {CriteriaType: ua.IdentityCriteriaTypeAuthenticatedUser}, 78 | }, 79 | ApplicationsExclude: true, 80 | EndpointsExclude: true, 81 | }, 82 | // WellKnownRoleOperator 83 | { 84 | NodeID: ua.ObjectIDWellKnownRoleOperator, 85 | Identities: []ua.IdentityMappingRuleType{ 86 | {CriteriaType: ua.IdentityCriteriaTypeAuthenticatedUser}, 87 | }, 88 | ApplicationsExclude: true, 89 | EndpointsExclude: true, 90 | }, 91 | // WellKnownRoleEngineer 92 | { 93 | NodeID: ua.ObjectIDWellKnownRoleEngineer, 94 | ApplicationsExclude: true, 95 | EndpointsExclude: true, 96 | }, 97 | // WellKnownRoleSupervisor 98 | { 99 | NodeID: ua.ObjectIDWellKnownRoleSupervisor, 100 | ApplicationsExclude: true, 101 | EndpointsExclude: true, 102 | }, 103 | // WellKnownRoleConfigureAdmin 104 | { 105 | NodeID: ua.ObjectIDWellKnownRoleConfigureAdmin, 106 | ApplicationsExclude: true, 107 | EndpointsExclude: true, 108 | }, 109 | // WellKnownRoleSecurityAdmin 110 | { 111 | NodeID: ua.ObjectIDWellKnownRoleSecurityAdmin, 112 | ApplicationsExclude: true, 113 | EndpointsExclude: true, 114 | }, 115 | } 116 | // DefaultRolesProvider is a RulesBasedRolesProvider using the DefaultIdentityMappingRules 117 | DefaultRolesProvider = NewRulesBasedRolesProvider(DefaultIdentityMappingRules) 118 | ) 119 | 120 | // IsUserPermitted returns true if the user's role permissions contain a given permissionType. 121 | func IsUserPermitted(userRolePermissions []ua.RolePermissionType, permissionType ua.PermissionType) bool { 122 | for _, rp := range userRolePermissions { 123 | if rp.Permissions&permissionType != 0 { 124 | return true 125 | } 126 | } 127 | return false 128 | } 129 | 130 | // RulesBasedRolesProvider returns WellKnownRoles given server identity mapping rules. 131 | type RulesBasedRolesProvider struct { 132 | identityMappingRules []IdentityMappingRule 133 | } 134 | 135 | // NewRulesBasedRolesProvider ... 136 | func NewRulesBasedRolesProvider(rules []IdentityMappingRule) RolesProvider { 137 | return &RulesBasedRolesProvider{ 138 | identityMappingRules: rules, 139 | } 140 | } 141 | 142 | // GetRoles ... 143 | func (p *RulesBasedRolesProvider) GetRoles(userIdentity any, applicationURI string, endpointURL string) ([]ua.NodeID, error) { 144 | roles := []ua.NodeID{} 145 | for _, rule := range p.identityMappingRules { 146 | ok := rule.ApplicationsExclude // true means the following applications should be excluded 147 | for _, uri := range rule.Applications { 148 | if uri == applicationURI { 149 | ok = !rule.ApplicationsExclude 150 | break 151 | } 152 | } 153 | if !ok { 154 | break // continue with next rule 155 | } 156 | ok = rule.EndpointsExclude // true means the following endpoints should be excluded 157 | for _, ep := range rule.Endpoints { 158 | if ep.EndpointUrl == endpointURL { 159 | ok = !rule.EndpointsExclude 160 | break 161 | } 162 | } 163 | if !ok { 164 | break // continue with next role 165 | } 166 | for _, identity := range rule.Identities { 167 | 168 | switch id := userIdentity.(type) { 169 | case ua.AnonymousIdentity: 170 | if identity.CriteriaType == ua.IdentityCriteriaTypeAnonymous { 171 | roles = append(roles, rule.NodeID) 172 | break // continue with next identity 173 | } 174 | 175 | case ua.UserNameIdentity: 176 | if identity.CriteriaType == ua.IdentityCriteriaTypeAuthenticatedUser { 177 | roles = append(roles, rule.NodeID) 178 | break // continue with next identity 179 | } 180 | if identity.CriteriaType == ua.IdentityCriteriaTypeUserName && identity.Criteria == id.UserName { 181 | roles = append(roles, rule.NodeID) 182 | break // continue with next identity 183 | } 184 | 185 | case ua.X509Identity: 186 | if identity.CriteriaType == ua.IdentityCriteriaTypeAuthenticatedUser { 187 | roles = append(roles, rule.NodeID) 188 | break // continue with next identity 189 | } 190 | thumbprint := fmt.Sprintf("%x", sha1.Sum([]byte(id.Certificate))) 191 | if identity.CriteriaType == ua.IdentityCriteriaTypeThumbprint && identity.Criteria == thumbprint { 192 | roles = append(roles, rule.NodeID) 193 | break // continue with next identity 194 | } 195 | 196 | case ua.IssuedIdentity: 197 | if identity.CriteriaType == ua.IdentityCriteriaTypeAuthenticatedUser { 198 | roles = append(roles, rule.NodeID) 199 | break // continue with next identity 200 | } 201 | 202 | default: 203 | return nil, ua.BadUserAccessDenied 204 | 205 | } 206 | } 207 | } 208 | if len(roles) == 0 { 209 | return nil, ua.BadUserAccessDenied 210 | } 211 | return roles, nil 212 | } 213 | -------------------------------------------------------------------------------- /server/scheduler.go: -------------------------------------------------------------------------------- 1 | package server 2 | 3 | import ( 4 | "sync" 5 | "time" 6 | ) 7 | 8 | type Scheduler struct { 9 | sync.Mutex 10 | server *Server 11 | tickers map[time.Duration]*PollGroup 12 | minSamplingInterval time.Duration 13 | } 14 | 15 | func NewScheduler(server *Server) *Scheduler { 16 | s := &Scheduler{ 17 | server: server, 18 | tickers: make(map[time.Duration]*PollGroup), 19 | minSamplingInterval: time.Duration(server.ServerCapabilities().MinSupportedSampleRate) * time.Millisecond, 20 | } 21 | return s 22 | } 23 | 24 | func (s *Scheduler) GetPollGroup(interval time.Duration) *PollGroup { 25 | s.Lock() 26 | defer s.Unlock() 27 | if interval < s.minSamplingInterval { 28 | interval = s.minSamplingInterval 29 | } 30 | if t, ok := s.tickers[interval]; ok { 31 | return t 32 | } 33 | t := NewPollGroup(interval, s.server.closing) 34 | s.tickers[interval] = t 35 | return t 36 | } 37 | 38 | type PollGroup struct { 39 | sync.Mutex 40 | cancellationCh chan struct{} 41 | interval time.Duration 42 | subs map[PollListener]struct{} 43 | } 44 | 45 | func NewPollGroup(interval time.Duration, cancellationCh chan struct{}) *PollGroup { 46 | b := &PollGroup{ 47 | cancellationCh: cancellationCh, 48 | interval: interval, 49 | subs: map[PollListener]struct{}{}, 50 | } 51 | go b.run() 52 | // log.Printf("Opening PollGroup %d ms\n", b.interval.Nanoseconds()/1000000) 53 | return b 54 | } 55 | 56 | func (b *PollGroup) run() { 57 | ticker := time.NewTicker(b.interval) 58 | for { 59 | select { 60 | case <-b.cancellationCh: 61 | ticker.Stop() 62 | b.Lock() 63 | // log.Printf("Closing PollGroup %d ms with %d subs\n", b.interval.Nanoseconds()/1000000, len(b.subs)) 64 | for sub := range b.subs { 65 | delete(b.subs, sub) 66 | } 67 | b.Unlock() 68 | return 69 | case <-ticker.C: 70 | b.Lock() 71 | listeners := make([]PollListener, len(b.subs)) 72 | i := 0 73 | for sub := range b.subs { 74 | listeners[i] = sub 75 | i++ 76 | } 77 | b.Unlock() 78 | for _, listener := range listeners { 79 | listener.Poll() 80 | } 81 | } 82 | } 83 | } 84 | 85 | func (b *PollGroup) Subscribe(listener PollListener) { 86 | b.Lock() 87 | b.subs[listener] = struct{}{} 88 | b.Unlock() 89 | } 90 | 91 | func (b *PollGroup) Unsubscribe(listener PollListener) { 92 | b.Lock() 93 | delete(b.subs, listener) 94 | b.Unlock() 95 | } 96 | 97 | type PollListener interface { 98 | Poll() 99 | } 100 | -------------------------------------------------------------------------------- /server/testserver_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2021 Converter Systems LLC. All rights reserved. 2 | 3 | package server_test 4 | 5 | import ( 6 | "crypto/x509" 7 | _ "embed" 8 | "fmt" 9 | "os" 10 | 11 | "github.com/awcullen/opcua/server" 12 | "github.com/awcullen/opcua/ua" 13 | "golang.org/x/crypto/bcrypt" 14 | ) 15 | 16 | var ( 17 | host, _ = os.Hostname() 18 | port = 46010 19 | SoftwareVersion = "1.0.0" 20 | //go:embed testnodeset_test.xml 21 | testnodeset []byte 22 | ) 23 | 24 | func NewTestServer() (*server.Server, error) { 25 | 26 | // userids for testing 27 | userids := []ua.UserNameIdentity{ 28 | {UserName: "root", Password: "secret"}, 29 | {UserName: "user1", Password: "password"}, 30 | {UserName: "user2", Password: "password1"}, 31 | } 32 | for i := range userids { 33 | hash, _ := bcrypt.GenerateFromPassword([]byte(userids[i].Password), 8) 34 | userids[i].Password = string(hash) 35 | } 36 | 37 | // create server 38 | srv, err := server.New( 39 | ua.ApplicationDescription{ 40 | ApplicationURI: fmt.Sprintf("urn:%s:testserver", host), 41 | ProductURI: "http://github.com/awcullen/opcua", 42 | ApplicationName: ua.LocalizedText{ 43 | Text: fmt.Sprintf("testserver@%s", host), 44 | Locale: "en", 45 | }, 46 | ApplicationType: ua.ApplicationTypeServer, 47 | GatewayServerURI: "", 48 | DiscoveryProfileURI: "", 49 | DiscoveryURLs: []string{fmt.Sprintf("opc.tcp://%s:%d", host, port)}, 50 | }, 51 | "./pki/server.crt", 52 | "./pki/server.key", 53 | fmt.Sprintf("opc.tcp://%s:%d", host, port), 54 | server.WithBuildInfo( 55 | ua.BuildInfo{ 56 | ProductURI: "http://github.com/awcullen/opcua", 57 | ManufacturerName: "awcullen", 58 | ProductName: "testserver", 59 | SoftwareVersion: SoftwareVersion, 60 | }), 61 | server.WithAuthenticateAnonymousIdentityFunc(func(userIdentity ua.AnonymousIdentity, applicationURI string, endpointURL string) error { 62 | // log.Printf("Login anonymous identity from %s\n", applicationURI) 63 | return nil 64 | }), 65 | server.WithAuthenticateUserNameIdentityFunc(func(userIdentity ua.UserNameIdentity, applicationURI string, endpointURL string) error { 66 | valid := false 67 | for _, user := range userids { 68 | if user.UserName == userIdentity.UserName { 69 | if err := bcrypt.CompareHashAndPassword([]byte(user.Password), []byte(userIdentity.Password)); err == nil { 70 | valid = true 71 | break 72 | } 73 | } 74 | } 75 | if !valid { 76 | return ua.BadUserAccessDenied 77 | } 78 | // log.Printf("Login %s from %s\n", userIdentity.UserName, applicationURI) 79 | return nil 80 | }), 81 | server.WithAuthenticateX509IdentityFunc(func(userIdentity ua.X509Identity, applicationURI string, endpointURL string) error { 82 | _, err := x509.ParseCertificates([]byte(userIdentity.Certificate)) 83 | if err != nil { 84 | return ua.BadUserAccessDenied 85 | } 86 | // log.Printf("Login %s from %s\n", cert.Subject, applicationURI) 87 | return nil 88 | }), 89 | server.WithSecurityPolicyNone(true), 90 | server.WithInsecureSkipVerify(), 91 | ) 92 | if err != nil { 93 | return nil, err 94 | } 95 | 96 | // load nodeset 97 | nm := srv.NamespaceManager() 98 | if err := nm.LoadNodeSetFromBuffer([]byte(testnodeset)); err != nil { 99 | return nil, err 100 | } 101 | 102 | // install MethodNoArgs method 103 | if n, ok := nm.FindMethod(ua.ParseNodeID("ns=2;s=Demo.Methods.MethodNoArgs")); ok { 104 | n.SetCallMethodHandler(func(session *server.Session, req ua.CallMethodRequest) ua.CallMethodResult { 105 | return ua.CallMethodResult{} 106 | }) 107 | } 108 | 109 | // install MethodI method 110 | if n, ok := nm.FindMethod(ua.ParseNodeID("ns=2;s=Demo.Methods.MethodI")); ok { 111 | n.SetCallMethodHandler(func(session *server.Session, req ua.CallMethodRequest) ua.CallMethodResult { 112 | if len(req.InputArguments) < 1 { 113 | return ua.CallMethodResult{StatusCode: ua.BadArgumentsMissing} 114 | } 115 | if len(req.InputArguments) > 1 { 116 | return ua.CallMethodResult{StatusCode: ua.BadTooManyArguments} 117 | } 118 | statusCode := ua.Good 119 | inputArgumentResults := make([]ua.StatusCode, 1) 120 | _, ok := req.InputArguments[0].(uint32) 121 | if !ok { 122 | statusCode = ua.BadInvalidArgument 123 | inputArgumentResults[0] = ua.BadTypeMismatch 124 | } 125 | if statusCode == ua.BadInvalidArgument { 126 | return ua.CallMethodResult{StatusCode: statusCode, InputArgumentResults: inputArgumentResults} 127 | } 128 | return ua.CallMethodResult{OutputArguments: []ua.Variant{}} 129 | }) 130 | } 131 | 132 | // install MethodO method 133 | if n, ok := nm.FindMethod(ua.ParseNodeID("ns=2;s=Demo.Methods.MethodO")); ok { 134 | n.SetCallMethodHandler(func(session *server.Session, req ua.CallMethodRequest) ua.CallMethodResult { 135 | if len(req.InputArguments) > 0 { 136 | return ua.CallMethodResult{StatusCode: ua.BadTooManyArguments} 137 | } 138 | result := uint32(42) 139 | return ua.CallMethodResult{OutputArguments: []ua.Variant{uint32(result)}} 140 | }) 141 | } 142 | 143 | // install MethodIO method 144 | if n, ok := nm.FindMethod(ua.ParseNodeID("ns=2;s=Demo.Methods.MethodIO")); ok { 145 | n.SetCallMethodHandler(func(session *server.Session, req ua.CallMethodRequest) ua.CallMethodResult { 146 | if len(req.InputArguments) < 2 { 147 | return ua.CallMethodResult{StatusCode: ua.BadArgumentsMissing} 148 | } 149 | if len(req.InputArguments) > 2 { 150 | return ua.CallMethodResult{StatusCode: ua.BadTooManyArguments} 151 | } 152 | statusCode := ua.Good 153 | inputArgumentResults := make([]ua.StatusCode, 2) 154 | a, ok := req.InputArguments[0].(uint32) 155 | if !ok { 156 | statusCode = ua.BadInvalidArgument 157 | inputArgumentResults[0] = ua.BadTypeMismatch 158 | } 159 | b, ok := req.InputArguments[1].(uint32) 160 | if !ok { 161 | statusCode = ua.BadInvalidArgument 162 | inputArgumentResults[1] = ua.BadTypeMismatch 163 | } 164 | if statusCode == ua.BadInvalidArgument { 165 | return ua.CallMethodResult{StatusCode: statusCode, InputArgumentResults: inputArgumentResults} 166 | } 167 | result := a + b 168 | return ua.CallMethodResult{OutputArguments: []ua.Variant{uint32(result)}} 169 | }) 170 | } 171 | return srv, nil 172 | } 173 | -------------------------------------------------------------------------------- /server/variable_node.go: -------------------------------------------------------------------------------- 1 | package server 2 | 3 | import ( 4 | "context" 5 | "sync" 6 | 7 | "github.com/awcullen/opcua/ua" 8 | ) 9 | 10 | type VariableNode struct { 11 | sync.RWMutex 12 | server *Server 13 | nodeId ua.NodeID 14 | nodeClass ua.NodeClass 15 | browseName ua.QualifiedName 16 | displayName ua.LocalizedText 17 | description ua.LocalizedText 18 | rolePermissions []ua.RolePermissionType 19 | accessRestrictions uint16 20 | references []ua.Reference 21 | value ua.DataValue 22 | dataType ua.NodeID 23 | valueRank int32 24 | arrayDimensions []uint32 25 | accessLevel byte 26 | minimumSamplingInterval float64 27 | historizing bool 28 | historian HistoryReadWriter 29 | readValueHandler func(*Session, ua.ReadValueID) ua.DataValue 30 | writeValueHandler func(*Session, ua.WriteValue) (ua.DataValue, ua.StatusCode) 31 | } 32 | 33 | var _ Node = (*VariableNode)(nil) 34 | 35 | func NewVariableNode(server *Server, nodeID ua.NodeID, browseName ua.QualifiedName, displayName ua.LocalizedText, description ua.LocalizedText, rolePermissions []ua.RolePermissionType, references []ua.Reference, value ua.DataValue, dataType ua.NodeID, valueRank int32, arrayDimensions []uint32, accessLevel byte, minimumSamplingInterval float64, historizing bool, historian HistoryReadWriter) *VariableNode { 36 | return &VariableNode{ 37 | server: server, 38 | nodeId: nodeID, 39 | nodeClass: ua.NodeClassVariable, 40 | browseName: browseName, 41 | displayName: displayName, 42 | description: description, 43 | rolePermissions: rolePermissions, 44 | accessRestrictions: 0, 45 | references: references, 46 | value: value, 47 | dataType: dataType, 48 | valueRank: valueRank, 49 | arrayDimensions: arrayDimensions, 50 | accessLevel: accessLevel, 51 | minimumSamplingInterval: minimumSamplingInterval, 52 | historizing: historizing, 53 | historian: historian, 54 | } 55 | } 56 | 57 | // NodeID returns the NodeID attribute of this node. 58 | func (n *VariableNode) NodeID() ua.NodeID { 59 | return n.nodeId 60 | } 61 | 62 | // NodeClass returns the NodeClass attribute of this node. 63 | func (n *VariableNode) NodeClass() ua.NodeClass { 64 | return n.nodeClass 65 | } 66 | 67 | // BrowseName returns the BrowseName attribute of this node. 68 | func (n *VariableNode) BrowseName() ua.QualifiedName { 69 | return n.browseName 70 | } 71 | 72 | // DisplayName returns the DisplayName attribute of this node. 73 | func (n *VariableNode) DisplayName() ua.LocalizedText { 74 | return n.displayName 75 | } 76 | 77 | // Description returns the Description attribute of this node. 78 | func (n *VariableNode) Description() ua.LocalizedText { 79 | return n.description 80 | } 81 | 82 | // RolePermissions returns the RolePermissions attribute of this node. 83 | func (n *VariableNode) RolePermissions() []ua.RolePermissionType { 84 | return n.rolePermissions 85 | } 86 | 87 | // UserRolePermissions returns the RolePermissions attribute of this node for the current user. 88 | func (n *VariableNode) UserRolePermissions(userIdentity any) []ua.RolePermissionType { 89 | filteredPermissions := []ua.RolePermissionType{} 90 | roles, err := n.server.GetRoles(userIdentity, "", "") 91 | if err != nil { 92 | return filteredPermissions 93 | } 94 | rolePermissions := n.RolePermissions() 95 | if rolePermissions == nil { 96 | rolePermissions = n.server.RolePermissions() 97 | } 98 | for _, role := range roles { 99 | for _, rp := range rolePermissions { 100 | if rp.RoleID == role { 101 | filteredPermissions = append(filteredPermissions, rp) 102 | } 103 | } 104 | } 105 | return filteredPermissions 106 | } 107 | 108 | // References returns the References of this node. 109 | func (n *VariableNode) References() []ua.Reference { 110 | n.RLock() 111 | defer n.RUnlock() 112 | return n.references 113 | } 114 | 115 | // SetReferences sets the References of this node. 116 | func (n *VariableNode) SetReferences(value []ua.Reference) { 117 | n.Lock() 118 | defer n.Unlock() 119 | n.references = value 120 | } 121 | 122 | // Value returns the value of the Variable. 123 | func (n *VariableNode) Value() ua.DataValue { 124 | n.RLock() 125 | defer n.RUnlock() 126 | return n.value 127 | } 128 | 129 | // SetValue sets the value of the Variable. 130 | func (n *VariableNode) SetValue(value ua.DataValue) { 131 | n.Lock() 132 | defer n.Unlock() 133 | n.value = value 134 | if n.historizing { 135 | n.historian.WriteValue(context.Background(), n.nodeId, value) 136 | } 137 | } 138 | 139 | // DataType returns the DataType attribute of this node. 140 | func (n *VariableNode) DataType() ua.NodeID { 141 | return n.dataType 142 | } 143 | 144 | // ValueRank returns the ValueRank attribute of this node. 145 | func (n *VariableNode) ValueRank() int32 { 146 | return n.valueRank 147 | } 148 | 149 | // ArrayDimensions returns the ArrayDimensions attribute of this node. 150 | func (n *VariableNode) ArrayDimensions() []uint32 { 151 | return n.arrayDimensions 152 | } 153 | 154 | // AccessLevel returns the AccessLevel attribute of this node. 155 | func (n *VariableNode) AccessLevel() byte { 156 | return n.accessLevel 157 | } 158 | 159 | // UserAccessLevel returns the AccessLevel attribute of this node for this user. 160 | func (n *VariableNode) UserAccessLevel(userIdentity any) byte { 161 | accessLevel := n.accessLevel 162 | roles, err := n.server.GetRoles(userIdentity, "", "") 163 | if err != nil { 164 | return 0 165 | } 166 | rolePermissions := n.RolePermissions() 167 | if rolePermissions == nil { 168 | rolePermissions = n.server.RolePermissions() 169 | } 170 | var currentRead, currentWrite, historyRead bool 171 | for _, role := range roles { 172 | for _, rp := range rolePermissions { 173 | if rp.RoleID == role { 174 | if rp.Permissions&ua.PermissionTypeRead != 0 { 175 | currentRead = true 176 | } 177 | if rp.Permissions&ua.PermissionTypeWrite != 0 { 178 | currentWrite = true 179 | } 180 | if rp.Permissions&ua.PermissionTypeReadHistory != 0 { 181 | historyRead = true 182 | } 183 | } 184 | } 185 | } 186 | if !currentRead { 187 | accessLevel &^= ua.AccessLevelsCurrentRead 188 | } 189 | if !currentWrite { 190 | accessLevel &^= ua.AccessLevelsCurrentWrite 191 | } 192 | if !historyRead { 193 | accessLevel &^= ua.AccessLevelsHistoryRead 194 | } 195 | return accessLevel 196 | } 197 | 198 | // MinimumSamplingInterval returns the MinimumSamplingInterval attribute of this node. 199 | func (n *VariableNode) MinimumSamplingInterval() float64 { 200 | return n.minimumSamplingInterval 201 | } 202 | 203 | // Historizing returns the Historizing attribute of this node. 204 | func (n *VariableNode) Historizing() bool { 205 | n.RLock() 206 | defer n.RUnlock() 207 | return n.historizing 208 | } 209 | 210 | // SetHistorizing sets the Historizing attribute of this node. 211 | func (n *VariableNode) SetHistorizing(historizing bool) { 212 | n.Lock() 213 | defer n.Unlock() 214 | n.historizing = historizing 215 | } 216 | 217 | // SetReadValueHandler sets the ReadValueHandler of this node. 218 | func (n *VariableNode) SetReadValueHandler(value func(*Session, ua.ReadValueID) ua.DataValue) { 219 | n.Lock() 220 | defer n.Unlock() 221 | n.readValueHandler = value 222 | } 223 | 224 | // SetWriteValueHandler sets the WriteValueHandler of this node. 225 | func (n *VariableNode) SetWriteValueHandler(value func(*Session, ua.WriteValue) (ua.DataValue, ua.StatusCode)) { 226 | n.Lock() 227 | defer n.Unlock() 228 | n.writeValueHandler = value 229 | } 230 | 231 | // IsAttributeIDValid returns true if attributeId is supported for the node. 232 | func (n *VariableNode) IsAttributeIDValid(attributeID uint32) bool { 233 | switch attributeID { 234 | case ua.AttributeIDNodeID, ua.AttributeIDNodeClass, ua.AttributeIDBrowseName, 235 | ua.AttributeIDDisplayName, ua.AttributeIDDescription, ua.AttributeIDRolePermissions, 236 | ua.AttributeIDUserRolePermissions, ua.AttributeIDValue, ua.AttributeIDDataType, 237 | ua.AttributeIDValueRank, ua.AttributeIDArrayDimensions, ua.AttributeIDAccessLevel, 238 | ua.AttributeIDUserAccessLevel, ua.AttributeIDMinimumSamplingInterval, ua.AttributeIDHistorizing: 239 | return true 240 | default: 241 | return false 242 | } 243 | } 244 | -------------------------------------------------------------------------------- /server/variable_type_node.go: -------------------------------------------------------------------------------- 1 | package server 2 | 3 | import ( 4 | "sync" 5 | 6 | "github.com/awcullen/opcua/ua" 7 | ) 8 | 9 | type VariableTypeNode struct { 10 | sync.RWMutex 11 | server *Server 12 | nodeId ua.NodeID 13 | nodeClass ua.NodeClass 14 | browseName ua.QualifiedName 15 | displayName ua.LocalizedText 16 | description ua.LocalizedText 17 | rolePermissions []ua.RolePermissionType 18 | accessRestrictions uint16 19 | references []ua.Reference 20 | value ua.DataValue 21 | dataType ua.NodeID 22 | valueRank int32 23 | arrayDimensions []uint32 24 | isAbstract bool 25 | } 26 | 27 | var _ Node = (*VariableTypeNode)(nil) 28 | 29 | func NewVariableTypeNode(server *Server, nodeId ua.NodeID, browseName ua.QualifiedName, displayName ua.LocalizedText, description ua.LocalizedText, rolePermissions []ua.RolePermissionType, references []ua.Reference, value ua.DataValue, dataType ua.NodeID, valueRank int32, arrayDimensions []uint32, isAbstract bool) *VariableTypeNode { 30 | return &VariableTypeNode{ 31 | server: server, 32 | nodeId: nodeId, 33 | nodeClass: ua.NodeClassVariableType, 34 | browseName: browseName, 35 | displayName: displayName, 36 | description: description, 37 | rolePermissions: rolePermissions, 38 | accessRestrictions: 0, 39 | references: references, 40 | value: value, 41 | dataType: dataType, 42 | valueRank: valueRank, 43 | arrayDimensions: arrayDimensions, 44 | isAbstract: isAbstract, 45 | } 46 | } 47 | 48 | // NodeID returns the NodeID attribute of this node. 49 | func (n *VariableTypeNode) NodeID() ua.NodeID { 50 | return n.nodeId 51 | } 52 | 53 | // NodeClass returns the NodeClass attribute of this node. 54 | func (n *VariableTypeNode) NodeClass() ua.NodeClass { 55 | return n.nodeClass 56 | } 57 | 58 | // BrowseName returns the BrowseName attribute of this node. 59 | func (n *VariableTypeNode) BrowseName() ua.QualifiedName { 60 | return n.browseName 61 | } 62 | 63 | // DisplayName returns the DisplayName attribute of this node. 64 | func (n *VariableTypeNode) DisplayName() ua.LocalizedText { 65 | return n.displayName 66 | } 67 | 68 | // Description returns the Description attribute of this node. 69 | func (n *VariableTypeNode) Description() ua.LocalizedText { 70 | return n.description 71 | } 72 | 73 | // RolePermissions returns the RolePermissions attribute of this node. 74 | func (n *VariableTypeNode) RolePermissions() []ua.RolePermissionType { 75 | return n.rolePermissions 76 | } 77 | 78 | // UserRolePermissions returns the RolePermissions attribute of this node for the current user. 79 | func (n *VariableTypeNode) UserRolePermissions(userIdentity any) []ua.RolePermissionType { 80 | filteredPermissions := []ua.RolePermissionType{} 81 | roles, err := n.server.GetRoles(userIdentity, "", "") 82 | if err != nil { 83 | return filteredPermissions 84 | } 85 | rolePermissions := n.RolePermissions() 86 | if rolePermissions == nil { 87 | rolePermissions = n.server.RolePermissions() 88 | } 89 | for _, role := range roles { 90 | for _, rp := range rolePermissions { 91 | if rp.RoleID == role { 92 | filteredPermissions = append(filteredPermissions, rp) 93 | } 94 | } 95 | } 96 | return filteredPermissions 97 | } 98 | 99 | // References returns the References of this node. 100 | func (n *VariableTypeNode) References() []ua.Reference { 101 | n.RLock() 102 | defer n.RUnlock() 103 | return n.references 104 | } 105 | 106 | // SetReferences sets the References of the Variable. 107 | func (n *VariableTypeNode) SetReferences(value []ua.Reference) { 108 | n.Lock() 109 | defer n.Unlock() 110 | n.references = value 111 | } 112 | 113 | // Value returns the value of the Variable. 114 | func (n *VariableTypeNode) Value() ua.DataValue { 115 | return n.value 116 | } 117 | 118 | // DataType returns the DataType attribute of this node. 119 | func (n *VariableTypeNode) DataType() ua.NodeID { 120 | return n.dataType 121 | } 122 | 123 | // ValueRank returns the ValueRank attribute of this node. 124 | func (n *VariableTypeNode) ValueRank() int32 { 125 | return n.valueRank 126 | } 127 | 128 | // ArrayDimensions returns the ArrayDimensions attribute of this node. 129 | func (n *VariableTypeNode) ArrayDimensions() []uint32 { 130 | return n.arrayDimensions 131 | } 132 | 133 | // IsAbstract returns the IsAbstract attribute of this node. 134 | func (n *VariableTypeNode) IsAbstract() bool { 135 | return n.isAbstract 136 | } 137 | 138 | // IsAttributeIDValid returns true if attributeId is supported for the node. 139 | func (n *VariableTypeNode) IsAttributeIDValid(attributeId uint32) bool { 140 | switch attributeId { 141 | case ua.AttributeIDNodeID, ua.AttributeIDNodeClass, ua.AttributeIDBrowseName, 142 | ua.AttributeIDDisplayName, ua.AttributeIDDescription, ua.AttributeIDRolePermissions, 143 | ua.AttributeIDUserRolePermissions, ua.AttributeIDIsAbstract, ua.AttributeIDDataType, 144 | ua.AttributeIDValueRank, ua.AttributeIDArrayDimensions: 145 | return true 146 | default: 147 | return false 148 | } 149 | } 150 | -------------------------------------------------------------------------------- /server/view_node.go: -------------------------------------------------------------------------------- 1 | package server 2 | 3 | import ( 4 | "sync" 5 | 6 | "github.com/awcullen/opcua/ua" 7 | ) 8 | 9 | type ViewNode struct { 10 | sync.RWMutex 11 | server *Server 12 | nodeId ua.NodeID 13 | nodeClass ua.NodeClass 14 | browseName ua.QualifiedName 15 | displayName ua.LocalizedText 16 | description ua.LocalizedText 17 | rolePermissions []ua.RolePermissionType 18 | accessRestrictions uint16 19 | references []ua.Reference 20 | containsNoLoops bool 21 | eventNotifier byte 22 | } 23 | 24 | var _ Node = (*ViewNode)(nil) 25 | 26 | func NewViewNode(server *Server, nodeId ua.NodeID, browseName ua.QualifiedName, displayName ua.LocalizedText, description ua.LocalizedText, rolePermissions []ua.RolePermissionType, references []ua.Reference, containsNoLoops bool, eventNotifier byte) *ViewNode { 27 | return &ViewNode{ 28 | server: server, 29 | nodeId: nodeId, 30 | nodeClass: ua.NodeClassView, 31 | browseName: browseName, 32 | displayName: displayName, 33 | description: description, 34 | rolePermissions: rolePermissions, 35 | accessRestrictions: 0, 36 | references: references, 37 | containsNoLoops: containsNoLoops, 38 | eventNotifier: eventNotifier, 39 | } 40 | } 41 | 42 | // NodeID returns the NodeID attribute of this node. 43 | func (n *ViewNode) NodeID() ua.NodeID { 44 | return n.nodeId 45 | } 46 | 47 | // NodeClass returns the NodeClass attribute of this node. 48 | func (n *ViewNode) NodeClass() ua.NodeClass { 49 | return n.nodeClass 50 | } 51 | 52 | // BrowseName returns the BrowseName attribute of this node. 53 | func (n *ViewNode) BrowseName() ua.QualifiedName { 54 | return n.browseName 55 | } 56 | 57 | // DisplayName returns the DisplayName attribute of this node. 58 | func (n *ViewNode) DisplayName() ua.LocalizedText { 59 | return n.displayName 60 | } 61 | 62 | // Description returns the Description attribute of this node. 63 | func (n *ViewNode) Description() ua.LocalizedText { 64 | return n.description 65 | } 66 | 67 | // RolePermissions returns the RolePermissions attribute of this node. 68 | func (n *ViewNode) RolePermissions() []ua.RolePermissionType { 69 | return n.rolePermissions 70 | } 71 | 72 | // UserRolePermissions returns the RolePermissions attribute of this node for the current user. 73 | func (n *ViewNode) UserRolePermissions(userIdentity any) []ua.RolePermissionType { 74 | filteredPermissions := []ua.RolePermissionType{} 75 | roles, err := n.server.GetRoles(userIdentity, "", "") 76 | if err != nil { 77 | return filteredPermissions 78 | } 79 | rolePermissions := n.RolePermissions() 80 | if rolePermissions == nil { 81 | rolePermissions = n.server.RolePermissions() 82 | } 83 | for _, role := range roles { 84 | for _, rp := range rolePermissions { 85 | if rp.RoleID == role { 86 | filteredPermissions = append(filteredPermissions, rp) 87 | } 88 | } 89 | } 90 | return filteredPermissions 91 | } 92 | 93 | // References returns the References of this node. 94 | func (n *ViewNode) References() []ua.Reference { 95 | n.RLock() 96 | defer n.RUnlock() 97 | return n.references 98 | } 99 | 100 | // SetReferences sets the References of the Variable. 101 | func (n *ViewNode) SetReferences(value []ua.Reference) { 102 | n.Lock() 103 | defer n.Unlock() 104 | n.references = value 105 | } 106 | 107 | // ContainsNoLoops returns the ContainsNoLoops attribute of this node. 108 | func (n *ViewNode) ContainsNoLoops() bool { 109 | return n.containsNoLoops 110 | } 111 | 112 | // EventNotifier returns the EventNotifier attribute of this node. 113 | func (n *ViewNode) EventNotifier() byte { 114 | return n.eventNotifier 115 | } 116 | 117 | // IsAttributeIDValid returns true if attributeId is supported for the node. 118 | func (n *ViewNode) IsAttributeIDValid(attributeId uint32) bool { 119 | switch attributeId { 120 | case ua.AttributeIDNodeID, ua.AttributeIDNodeClass, ua.AttributeIDBrowseName, 121 | ua.AttributeIDDisplayName, ua.AttributeIDDescription, ua.AttributeIDRolePermissions, 122 | ua.AttributeIDUserRolePermissions, ua.AttributeIDContainsNoLoops, ua.AttributeIDEventNotifier: 123 | return true 124 | default: 125 | return false 126 | } 127 | } 128 | -------------------------------------------------------------------------------- /ua/access_levels.go: -------------------------------------------------------------------------------- 1 | // Copyright 2021 Converter Systems LLC. All rights reserved. 2 | 3 | package ua 4 | 5 | // AccessLevels set for the AccessLevel attribute. 6 | const ( 7 | AccessLevelsNone byte = 0x0 8 | AccessLevelsCurrentRead byte = 0x1 9 | AccessLevelsCurrentWrite byte = 0x2 10 | AccessLevelsHistoryRead byte = 0x4 11 | AccessLevelsHistoryWrite byte = 0x8 12 | AccessLevelsSemanticChange byte = 0x10 13 | AccessLevelsStatusWrite byte = 0x20 14 | AccessLevelsTimestampWrite byte = 0x40 15 | ) 16 | -------------------------------------------------------------------------------- /ua/acknowledgeable_condition.go: -------------------------------------------------------------------------------- 1 | // Copyright 2021 Converter Systems LLC. All rights reserved. 2 | 3 | package ua 4 | 5 | import ( 6 | "time" 7 | ) 8 | 9 | // AcknowledgeableCondition structure. 10 | type AcknowledgeableCondition struct { 11 | EventID ByteString 12 | EventType NodeID 13 | SourceNode NodeID 14 | SourceName string 15 | Time time.Time 16 | ReceiveTime time.Time 17 | Message LocalizedText 18 | Severity uint16 19 | ConditionID NodeID 20 | ConditionName string 21 | BranchID NodeID 22 | Retain bool 23 | AckedState bool 24 | ConfirmedState bool 25 | } 26 | 27 | // UnmarshalFields ... 28 | func (evt *AcknowledgeableCondition) UnmarshalFields(eventFields []Variant) error { 29 | if len(eventFields) != 14 { 30 | return BadUnexpectedError 31 | } 32 | evt.EventID, _ = eventFields[0].(ByteString) 33 | evt.EventType, _ = eventFields[1].(NodeID) 34 | evt.SourceNode, _ = eventFields[2].(NodeID) 35 | evt.SourceName, _ = eventFields[3].(string) 36 | evt.Time, _ = eventFields[4].(time.Time) 37 | evt.ReceiveTime, _ = eventFields[5].(time.Time) 38 | evt.Message, _ = eventFields[6].(LocalizedText) 39 | evt.Severity, _ = eventFields[7].(uint16) 40 | evt.ConditionID, _ = eventFields[8].(NodeID) 41 | evt.ConditionName, _ = eventFields[9].(string) 42 | evt.BranchID, _ = eventFields[10].(NodeID) 43 | evt.Retain, _ = eventFields[11].(bool) 44 | evt.AckedState, _ = eventFields[12].(bool) 45 | evt.ConfirmedState, _ = eventFields[13].(bool) 46 | return nil 47 | } 48 | 49 | // GetAttribute ... 50 | func (e *AcknowledgeableCondition) GetAttribute(clause SimpleAttributeOperand) Variant { 51 | switch { 52 | case EqualSimpleAttributeOperand(clause, AcknowledgeableConditionSelectClauses[0]): 53 | return Variant(e.EventID) 54 | case EqualSimpleAttributeOperand(clause, AcknowledgeableConditionSelectClauses[1]): 55 | return Variant(e.EventType) 56 | case EqualSimpleAttributeOperand(clause, AcknowledgeableConditionSelectClauses[2]): 57 | return Variant(e.SourceNode) 58 | case EqualSimpleAttributeOperand(clause, AcknowledgeableConditionSelectClauses[3]): 59 | return Variant(e.SourceName) 60 | case EqualSimpleAttributeOperand(clause, AcknowledgeableConditionSelectClauses[4]): 61 | return Variant(e.Time) 62 | case EqualSimpleAttributeOperand(clause, AcknowledgeableConditionSelectClauses[5]): 63 | return Variant(e.ReceiveTime) 64 | case EqualSimpleAttributeOperand(clause, AcknowledgeableConditionSelectClauses[6]): 65 | return Variant(e.Message) 66 | case EqualSimpleAttributeOperand(clause, AcknowledgeableConditionSelectClauses[7]): 67 | return Variant(e.Severity) 68 | case EqualSimpleAttributeOperand(clause, AcknowledgeableConditionSelectClauses[8]): 69 | return Variant(e.ConditionID) 70 | case EqualSimpleAttributeOperand(clause, AcknowledgeableConditionSelectClauses[9]): 71 | return Variant(e.ConditionName) 72 | case EqualSimpleAttributeOperand(clause, AcknowledgeableConditionSelectClauses[10]): 73 | return Variant(e.BranchID) 74 | case EqualSimpleAttributeOperand(clause, AcknowledgeableConditionSelectClauses[11]): 75 | return Variant(e.Retain) 76 | case EqualSimpleAttributeOperand(clause, AcknowledgeableConditionSelectClauses[12]): 77 | return Variant(e.AckedState) 78 | case EqualSimpleAttributeOperand(clause, AcknowledgeableConditionSelectClauses[13]): 79 | return Variant(e.ConfirmedState) 80 | default: 81 | return nil 82 | } 83 | } 84 | 85 | // AcknowledgeableConditionSelectClauses ... 86 | var AcknowledgeableConditionSelectClauses []SimpleAttributeOperand = []SimpleAttributeOperand{ 87 | {TypeDefinitionID: ObjectTypeIDBaseEventType, BrowsePath: ParseBrowsePath("EventId"), AttributeID: AttributeIDValue}, 88 | {TypeDefinitionID: ObjectTypeIDBaseEventType, BrowsePath: ParseBrowsePath("EventType"), AttributeID: AttributeIDValue}, 89 | {TypeDefinitionID: ObjectTypeIDBaseEventType, BrowsePath: ParseBrowsePath("SourceNode"), AttributeID: AttributeIDValue}, 90 | {TypeDefinitionID: ObjectTypeIDBaseEventType, BrowsePath: ParseBrowsePath("SourceName"), AttributeID: AttributeIDValue}, 91 | {TypeDefinitionID: ObjectTypeIDBaseEventType, BrowsePath: ParseBrowsePath("Time"), AttributeID: AttributeIDValue}, 92 | {TypeDefinitionID: ObjectTypeIDBaseEventType, BrowsePath: ParseBrowsePath("ReceiveTime"), AttributeID: AttributeIDValue}, 93 | {TypeDefinitionID: ObjectTypeIDBaseEventType, BrowsePath: ParseBrowsePath("Message"), AttributeID: AttributeIDValue}, 94 | {TypeDefinitionID: ObjectTypeIDBaseEventType, BrowsePath: ParseBrowsePath("Severity"), AttributeID: AttributeIDValue}, 95 | {TypeDefinitionID: ObjectTypeIDConditionType, BrowsePath: ParseBrowsePath(""), AttributeID: AttributeIDNodeID}, 96 | {TypeDefinitionID: ObjectTypeIDConditionType, BrowsePath: ParseBrowsePath("ConditionName"), AttributeID: AttributeIDValue}, 97 | {TypeDefinitionID: ObjectTypeIDConditionType, BrowsePath: ParseBrowsePath("BranchId"), AttributeID: AttributeIDValue}, 98 | {TypeDefinitionID: ObjectTypeIDConditionType, BrowsePath: ParseBrowsePath("Retain"), AttributeID: AttributeIDValue}, 99 | {TypeDefinitionID: ObjectTypeIDAcknowledgeableConditionType, BrowsePath: ParseBrowsePath("AckedState/Id"), AttributeID: AttributeIDValue}, 100 | {TypeDefinitionID: ObjectTypeIDAcknowledgeableConditionType, BrowsePath: ParseBrowsePath("ConfirmedState/Id"), AttributeID: AttributeIDValue}, 101 | } 102 | -------------------------------------------------------------------------------- /ua/alarm_condition.go: -------------------------------------------------------------------------------- 1 | // Copyright 2021 Converter Systems LLC. All rights reserved. 2 | 3 | package ua 4 | 5 | import ( 6 | "time" 7 | ) 8 | 9 | // AlarmCondition structure. 10 | type AlarmCondition struct { 11 | EventID ByteString 12 | EventType NodeID 13 | SourceNode NodeID 14 | SourceName string 15 | Time time.Time 16 | ReceiveTime time.Time 17 | Message LocalizedText 18 | Severity uint16 19 | ConditionID NodeID 20 | ConditionName string 21 | BranchID NodeID 22 | Retain bool 23 | AckedState bool 24 | ConfirmedState bool 25 | ActiveState bool 26 | } 27 | 28 | // UnmarshalFields ... 29 | func (evt *AlarmCondition) UnmarshalFields(eventFields []Variant) error { 30 | if len(eventFields) != 15 { 31 | return BadUnexpectedError 32 | } 33 | evt.EventID, _ = eventFields[0].(ByteString) 34 | evt.EventType, _ = eventFields[1].(NodeID) 35 | evt.SourceNode, _ = eventFields[2].(NodeID) 36 | evt.SourceName, _ = eventFields[3].(string) 37 | evt.Time, _ = eventFields[4].(time.Time) 38 | evt.ReceiveTime, _ = eventFields[5].(time.Time) 39 | evt.Message, _ = eventFields[6].(LocalizedText) 40 | evt.Severity, _ = eventFields[7].(uint16) 41 | evt.ConditionID, _ = eventFields[8].(NodeID) 42 | evt.ConditionName, _ = eventFields[9].(string) 43 | evt.BranchID, _ = eventFields[10].(NodeID) 44 | evt.Retain, _ = eventFields[11].(bool) 45 | evt.AckedState, _ = eventFields[12].(bool) 46 | evt.ConfirmedState, _ = eventFields[13].(bool) 47 | evt.ActiveState, _ = eventFields[14].(bool) 48 | return nil 49 | } 50 | 51 | // GetAttribute ... 52 | func (e *AlarmCondition) GetAttribute(clause SimpleAttributeOperand) Variant { 53 | switch { 54 | case EqualSimpleAttributeOperand(clause, AlarmConditionSelectClauses[0]): 55 | return Variant(e.EventID) 56 | case EqualSimpleAttributeOperand(clause, AlarmConditionSelectClauses[1]): 57 | return Variant(e.EventType) 58 | case EqualSimpleAttributeOperand(clause, AlarmConditionSelectClauses[2]): 59 | return Variant(e.SourceNode) 60 | case EqualSimpleAttributeOperand(clause, AlarmConditionSelectClauses[3]): 61 | return Variant(e.SourceName) 62 | case EqualSimpleAttributeOperand(clause, AlarmConditionSelectClauses[4]): 63 | return Variant(e.Time) 64 | case EqualSimpleAttributeOperand(clause, AlarmConditionSelectClauses[5]): 65 | return Variant(e.ReceiveTime) 66 | case EqualSimpleAttributeOperand(clause, AlarmConditionSelectClauses[6]): 67 | return Variant(e.Message) 68 | case EqualSimpleAttributeOperand(clause, AlarmConditionSelectClauses[7]): 69 | return Variant(e.Severity) 70 | case EqualSimpleAttributeOperand(clause, AlarmConditionSelectClauses[8]): 71 | return Variant(e.ConditionID) 72 | case EqualSimpleAttributeOperand(clause, AlarmConditionSelectClauses[9]): 73 | return Variant(e.ConditionName) 74 | case EqualSimpleAttributeOperand(clause, AlarmConditionSelectClauses[10]): 75 | return Variant(e.BranchID) 76 | case EqualSimpleAttributeOperand(clause, AlarmConditionSelectClauses[11]): 77 | return Variant(e.Retain) 78 | case EqualSimpleAttributeOperand(clause, AlarmConditionSelectClauses[12]): 79 | return Variant(e.AckedState) 80 | case EqualSimpleAttributeOperand(clause, AlarmConditionSelectClauses[13]): 81 | return Variant(e.ConfirmedState) 82 | case EqualSimpleAttributeOperand(clause, AlarmConditionSelectClauses[14]): 83 | return Variant(e.ActiveState) 84 | case EqualSimpleAttributeOperand(clause, AlarmConditionSelectClauseActiveStateEffectiveDisplayName): 85 | if e.ActiveState { 86 | return Variant(LocalizedText{Locale: "en", Text: "Active"}) 87 | } else { 88 | return Variant(LocalizedText{Locale: "en", Text: "Inactive"}) 89 | } 90 | default: 91 | return nil 92 | } 93 | } 94 | 95 | // AlarmConditionSelectClauses ... 96 | var AlarmConditionSelectClauses []SimpleAttributeOperand = []SimpleAttributeOperand{ 97 | {TypeDefinitionID: ObjectTypeIDBaseEventType, BrowsePath: ParseBrowsePath("EventId"), AttributeID: AttributeIDValue}, 98 | {TypeDefinitionID: ObjectTypeIDBaseEventType, BrowsePath: ParseBrowsePath("EventType"), AttributeID: AttributeIDValue}, 99 | {TypeDefinitionID: ObjectTypeIDBaseEventType, BrowsePath: ParseBrowsePath("SourceNode"), AttributeID: AttributeIDValue}, 100 | {TypeDefinitionID: ObjectTypeIDBaseEventType, BrowsePath: ParseBrowsePath("SourceName"), AttributeID: AttributeIDValue}, 101 | {TypeDefinitionID: ObjectTypeIDBaseEventType, BrowsePath: ParseBrowsePath("Time"), AttributeID: AttributeIDValue}, 102 | {TypeDefinitionID: ObjectTypeIDBaseEventType, BrowsePath: ParseBrowsePath("ReceiveTime"), AttributeID: AttributeIDValue}, 103 | {TypeDefinitionID: ObjectTypeIDBaseEventType, BrowsePath: ParseBrowsePath("Message"), AttributeID: AttributeIDValue}, 104 | {TypeDefinitionID: ObjectTypeIDBaseEventType, BrowsePath: ParseBrowsePath("Severity"), AttributeID: AttributeIDValue}, 105 | {TypeDefinitionID: ObjectTypeIDConditionType, BrowsePath: ParseBrowsePath(""), AttributeID: AttributeIDNodeID}, 106 | {TypeDefinitionID: ObjectTypeIDConditionType, BrowsePath: ParseBrowsePath("ConditionName"), AttributeID: AttributeIDValue}, 107 | {TypeDefinitionID: ObjectTypeIDConditionType, BrowsePath: ParseBrowsePath("BranchId"), AttributeID: AttributeIDValue}, 108 | {TypeDefinitionID: ObjectTypeIDConditionType, BrowsePath: ParseBrowsePath("Retain"), AttributeID: AttributeIDValue}, 109 | {TypeDefinitionID: ObjectTypeIDAcknowledgeableConditionType, BrowsePath: ParseBrowsePath("AckedState/Id"), AttributeID: AttributeIDValue}, 110 | {TypeDefinitionID: ObjectTypeIDAcknowledgeableConditionType, BrowsePath: ParseBrowsePath("ConfirmedState/Id"), AttributeID: AttributeIDValue}, 111 | {TypeDefinitionID: ObjectTypeIDAlarmConditionType, BrowsePath: ParseBrowsePath("ActiveState/Id"), AttributeID: AttributeIDValue}, 112 | } 113 | 114 | var AlarmConditionSelectClauseActiveStateEffectiveDisplayName SimpleAttributeOperand = SimpleAttributeOperand{TypeDefinitionID: ObjectTypeIDAlarmConditionType, BrowsePath: ParseBrowsePath("ActiveState/EffectiveDisplayName"), AttributeID: AttributeIDValue} 115 | -------------------------------------------------------------------------------- /ua/attributeids.go: -------------------------------------------------------------------------------- 1 | // Copyright 2021 Converter Systems LLC. All rights reserved. 2 | 3 | package ua 4 | 5 | // AttributeID selects which attribute of the node to read or write. 6 | const ( 7 | AttributeIDNodeID uint32 = 1 8 | AttributeIDNodeClass uint32 = 2 9 | AttributeIDBrowseName uint32 = 3 10 | AttributeIDDisplayName uint32 = 4 11 | AttributeIDDescription uint32 = 5 12 | AttributeIDWriteMask uint32 = 6 13 | AttributeIDUserWriteMask uint32 = 7 14 | AttributeIDIsAbstract uint32 = 8 15 | AttributeIDSymmetric uint32 = 9 16 | AttributeIDInverseName uint32 = 10 17 | AttributeIDContainsNoLoops uint32 = 11 18 | AttributeIDEventNotifier uint32 = 12 19 | AttributeIDValue uint32 = 13 20 | AttributeIDDataType uint32 = 14 21 | AttributeIDValueRank uint32 = 15 22 | AttributeIDArrayDimensions uint32 = 16 23 | AttributeIDAccessLevel uint32 = 17 24 | AttributeIDUserAccessLevel uint32 = 18 25 | AttributeIDMinimumSamplingInterval uint32 = 19 26 | AttributeIDHistorizing uint32 = 20 27 | AttributeIDExecutable uint32 = 21 28 | AttributeIDUserExecutable uint32 = 22 29 | AttributeIDDataTypeDefinition uint32 = 23 30 | AttributeIDRolePermissions uint32 = 24 31 | AttributeIDUserRolePermissions uint32 = 25 32 | AttributeIDAccessRestrictions uint32 = 26 33 | AttributeIDAccessLevelEx uint32 = 27 34 | ) 35 | -------------------------------------------------------------------------------- /ua/base_event.go: -------------------------------------------------------------------------------- 1 | // Copyright 2021 Converter Systems LLC. All rights reserved. 2 | 3 | package ua 4 | 5 | import ( 6 | "time" 7 | ) 8 | 9 | type Event interface { 10 | GetAttribute(clause SimpleAttributeOperand) Variant 11 | } 12 | 13 | // BaseEvent structure. 14 | type BaseEvent struct { 15 | EventID ByteString 16 | EventType NodeID 17 | SourceNode NodeID 18 | SourceName string 19 | Time time.Time 20 | ReceiveTime time.Time 21 | Message LocalizedText 22 | Severity uint16 23 | } 24 | 25 | // UnmarshalFields ... 26 | func (evt *BaseEvent) UnmarshalFields(eventFields []Variant) error { 27 | if len(eventFields) != 8 { 28 | return BadUnexpectedError 29 | } 30 | evt.EventID, _ = eventFields[0].(ByteString) 31 | evt.EventType, _ = eventFields[1].(NodeID) 32 | evt.SourceNode, _ = eventFields[2].(NodeID) 33 | evt.SourceName, _ = eventFields[3].(string) 34 | evt.Time, _ = eventFields[4].(time.Time) 35 | evt.ReceiveTime, _ = eventFields[5].(time.Time) 36 | evt.Message, _ = eventFields[6].(LocalizedText) 37 | evt.Severity, _ = eventFields[7].(uint16) 38 | return nil 39 | } 40 | 41 | // GetAttribute ... 42 | func (e *BaseEvent) GetAttribute(clause SimpleAttributeOperand) Variant { 43 | switch { 44 | case EqualSimpleAttributeOperand(clause, BaseEventSelectClauses[0]): 45 | return Variant(e.EventID) 46 | case EqualSimpleAttributeOperand(clause, BaseEventSelectClauses[1]): 47 | return Variant(e.EventType) 48 | case EqualSimpleAttributeOperand(clause, BaseEventSelectClauses[2]): 49 | return Variant(e.SourceNode) 50 | case EqualSimpleAttributeOperand(clause, BaseEventSelectClauses[3]): 51 | return Variant(e.SourceName) 52 | case EqualSimpleAttributeOperand(clause, BaseEventSelectClauses[4]): 53 | return Variant(e.Time) 54 | case EqualSimpleAttributeOperand(clause, BaseEventSelectClauses[5]): 55 | return Variant(e.ReceiveTime) 56 | case EqualSimpleAttributeOperand(clause, BaseEventSelectClauses[6]): 57 | return Variant(e.Message) 58 | case EqualSimpleAttributeOperand(clause, BaseEventSelectClauses[7]): 59 | return Variant(e.Severity) 60 | default: 61 | return nil 62 | } 63 | } 64 | 65 | // BaseEventSelectClauses ... 66 | var BaseEventSelectClauses []SimpleAttributeOperand = []SimpleAttributeOperand{ 67 | {TypeDefinitionID: ObjectTypeIDBaseEventType, BrowsePath: ParseBrowsePath("EventId"), AttributeID: AttributeIDValue}, 68 | {TypeDefinitionID: ObjectTypeIDBaseEventType, BrowsePath: ParseBrowsePath("EventType"), AttributeID: AttributeIDValue}, 69 | {TypeDefinitionID: ObjectTypeIDBaseEventType, BrowsePath: ParseBrowsePath("SourceNode"), AttributeID: AttributeIDValue}, 70 | {TypeDefinitionID: ObjectTypeIDBaseEventType, BrowsePath: ParseBrowsePath("SourceName"), AttributeID: AttributeIDValue}, 71 | {TypeDefinitionID: ObjectTypeIDBaseEventType, BrowsePath: ParseBrowsePath("Time"), AttributeID: AttributeIDValue}, 72 | {TypeDefinitionID: ObjectTypeIDBaseEventType, BrowsePath: ParseBrowsePath("ReceiveTime"), AttributeID: AttributeIDValue}, 73 | {TypeDefinitionID: ObjectTypeIDBaseEventType, BrowsePath: ParseBrowsePath("Message"), AttributeID: AttributeIDValue}, 74 | {TypeDefinitionID: ObjectTypeIDBaseEventType, BrowsePath: ParseBrowsePath("Severity"), AttributeID: AttributeIDValue}, 75 | } 76 | -------------------------------------------------------------------------------- /ua/base_event_test.go: -------------------------------------------------------------------------------- 1 | package ua_test 2 | 3 | import ( 4 | "testing" 5 | "time" 6 | 7 | "github.com/awcullen/opcua/ua" 8 | "github.com/pkg/errors" 9 | ) 10 | 11 | func TestDeserializeBaseEvent(t *testing.T) { 12 | f := []ua.Variant{ 13 | ua.ByteString("foo"), 14 | ua.NewNodeIDString(1, "bar"), 15 | ua.NewNodeIDString(1, "bar"), 16 | "source", 17 | time.Now().UTC(), 18 | time.Now().UTC(), 19 | ua.NewLocalizedText("Temperature is high.", "en"), 20 | uint16(255), 21 | } 22 | e := ua.BaseEvent{} 23 | if err := e.UnmarshalFields(f); err != nil { 24 | t.Error(errors.Wrap(err, "Error unmarshalling fields")) 25 | } 26 | t.Logf("%+v", e) 27 | } 28 | 29 | func TestDeserializeCondition(t *testing.T) { 30 | f := []ua.Variant{ 31 | ua.ByteString("foo"), 32 | ua.NewNodeIDString(1, "bar"), 33 | ua.NewNodeIDString(1, "bar"), 34 | "source", 35 | time.Now().UTC(), 36 | time.Now().UTC(), 37 | ua.NewLocalizedText("Temperature is high.", "en"), 38 | uint16(255), 39 | ua.NewNodeIDNumeric(1, 45), 40 | "ConditionName", 41 | nil, 42 | true, 43 | } 44 | e := ua.Condition{} 45 | if err := e.UnmarshalFields(f); err != nil { 46 | t.Error(errors.Wrap(err, "Error unmarshalling fields")) 47 | } 48 | t.Logf("%+v", e) 49 | } 50 | -------------------------------------------------------------------------------- /ua/binary_registry.go: -------------------------------------------------------------------------------- 1 | // Copyright 2021 Converter Systems LLC. All rights reserved. 2 | 3 | package ua 4 | 5 | import ( 6 | "fmt" 7 | "reflect" 8 | "sync" 9 | ) 10 | 11 | var ( 12 | binaryEncodingTypes sync.Map // map[ExpandedNodeID]reflect.Type 13 | binaryEncodingIDs sync.Map // map[reflect.Type]ExpandedNodeID 14 | ) 15 | 16 | // RegisterBinaryEncodingID registers the type and id with the BinaryEncoder. 17 | func RegisterBinaryEncodingID(typ reflect.Type, id ExpandedNodeID) { 18 | 19 | if t, dup := binaryEncodingTypes.LoadOrStore(id, typ); dup && t != typ { 20 | panic(fmt.Sprintf("RegisterBinaryEncodingID: registering duplicate types for %q: %s != %s", id, t, typ)) 21 | } 22 | 23 | if n, dup := binaryEncodingIDs.LoadOrStore(typ, id); dup && n != id { 24 | binaryEncodingTypes.Delete(id) 25 | panic(fmt.Sprintf("RegisterBinaryEncodingID: registering duplicate ids for %s: %q != %q", typ, n, id)) 26 | } 27 | 28 | } 29 | 30 | // FindBinaryEncodingIDForType finds the BinaryEncodingID given the type. 31 | func FindBinaryEncodingIDForType(typ reflect.Type) (ExpandedNodeID, bool) { 32 | if val, ok := binaryEncodingIDs.Load(typ); ok { 33 | if id, ok := val.(ExpandedNodeID); ok { 34 | return id, ok 35 | } 36 | } 37 | return NilExpandedNodeID, false 38 | } 39 | 40 | // FindTypeForBinaryEncodingID finds the Type given the BinaryEncodingID. 41 | func FindTypeForBinaryEncodingID(id ExpandedNodeID) (reflect.Type, bool) { 42 | if val, ok := binaryEncodingTypes.Load(id); ok { 43 | if typ, ok := val.(reflect.Type); ok { 44 | return typ, ok 45 | } 46 | } 47 | return nil, false 48 | } 49 | -------------------------------------------------------------------------------- /ua/bytes_writer.go: -------------------------------------------------------------------------------- 1 | // Copyright 2021 Converter Systems LLC. All rights reserved. 2 | 3 | package ua 4 | 5 | import "io" 6 | 7 | // A Writer implements the io.Writer, io.WriterAt 8 | // interfaces by writing to a byte slice. 9 | // Unlike a Buffer, a Writer is write-only. 10 | type Writer struct { 11 | s []byte 12 | i int // current writing index 13 | } 14 | 15 | // NewWriter returns a new Writer writing to b. 16 | func NewWriter(b []byte) *Writer { return &Writer{b, 0} } 17 | 18 | // Len returns the number of bytes of the written portion of the slice. 19 | func (w *Writer) Len() int { 20 | return int(w.i) 21 | } 22 | 23 | // Size returns the original length of the underlying byte slice. 24 | // The returned value is always the same and is not affected by calls 25 | // to any other method. 26 | func (w *Writer) Size() int64 { return int64(len(w.s)) } 27 | 28 | // Write copies slice p to buffer, returning the number of bytes written. 29 | func (w *Writer) Write(p []byte) (n int, err error) { 30 | if w.i >= len(w.s) { 31 | return 0, io.ErrShortWrite 32 | } 33 | d := w.s[w.i:] 34 | n = copy(d, p) 35 | w.i += n 36 | if n < len(p) { 37 | return n, io.ErrShortWrite 38 | } 39 | return n, nil 40 | } 41 | 42 | // WriteAt copies slice p to buffer, at a given offset from start, 43 | // returning the number of bytes written. Only useful to overwrite bytes 44 | // in buffer already written to by Write(). Does not affect the index that Write() 45 | // uses, and therefore does not change the length of slice returned by Bytes(). 46 | func (w *Writer) WriteAt(p []byte, offset int64) (n int, err error) { 47 | if int(offset) >= w.i { 48 | return 0, io.ErrShortWrite 49 | } 50 | d := w.s[offset:] 51 | n = copy(d, p) 52 | if n < len(p) { 53 | return n, io.ErrShortWrite 54 | } 55 | return n, nil 56 | } 57 | 58 | // Bytes returns a slice of length b.Len() holding the written portion of the buffer. 59 | // The slice is valid for use only until the next buffer modification (that is, 60 | // only until the next call to a method like Write). 61 | // The slice aliases the buffer content at least until the next buffer modification, 62 | // so immediate changes to the slice will affect the result of future reads. 63 | func (w *Writer) Bytes() []byte { return w.s[:w.i] } 64 | -------------------------------------------------------------------------------- /ua/bytestring.go: -------------------------------------------------------------------------------- 1 | // Copyright 2021 Converter Systems LLC. All rights reserved. 2 | 3 | package ua 4 | 5 | import ( 6 | "encoding/base64" 7 | ) 8 | 9 | // ByteString is stored as a string. 10 | type ByteString string 11 | 12 | // String returns ByteString as a base64-encoded string. 13 | func (b ByteString) String() string { 14 | return base64.StdEncoding.EncodeToString([]byte(b)) 15 | } 16 | 17 | func (b ByteString) MarshalText() ([]byte, error) { 18 | return []byte(b.String()), nil 19 | } 20 | -------------------------------------------------------------------------------- /ua/certificate_helpers.go: -------------------------------------------------------------------------------- 1 | package ua 2 | 3 | import ( 4 | "bytes" 5 | "crypto/sha1" 6 | "crypto/x509" 7 | "encoding/pem" 8 | "fmt" 9 | "io/fs" 10 | "os" 11 | "path/filepath" 12 | "strings" 13 | "time" 14 | 15 | "github.com/pkg/errors" 16 | ) 17 | 18 | // ValidateCertificate validates the leaf certificate. 19 | func ValidateCertificate(certificates []*x509.Certificate, keyUsages []x509.ExtKeyUsage, hostname, trustedPath, trustedCRLPath, issuersPath, issuersCRLPath, rejectedCertsPath string, 20 | suppressCertificateHostNameInvalid, suppressCertificateTimeInvalid, suppressCertificateChainIncomplete, suppressCertificateRevocationUnknown bool) error { 21 | if len(certificates) == 0 { 22 | return BadCertificateInvalid 23 | } 24 | if len(keyUsages) == 0 { 25 | keyUsages = []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth} 26 | } 27 | certificate := certificates[0] 28 | certificates = certificates[1:] 29 | 30 | intermediates := x509.NewCertPool() 31 | roots := x509.NewCertPool() 32 | trusted := []*x509.Certificate{} 33 | crls := []*x509.RevocationList{} 34 | 35 | for _, crt := range certificates { 36 | if isSelfSigned(crt) { 37 | roots.AddCert(crt) 38 | } else { 39 | intermediates.AddCert(crt) 40 | } 41 | } 42 | 43 | if crts, err := readCertificates(issuersPath); err == nil { 44 | for _, crt := range crts { 45 | if isSelfSigned(crt) { 46 | roots.AddCert(crt) 47 | } else { 48 | intermediates.AddCert(crt) 49 | } 50 | } 51 | } 52 | 53 | if lists, err := readRevocationLists(issuersCRLPath); err == nil { 54 | crls = append(crls, lists...) 55 | } 56 | 57 | if crts, err := readCertificates(trustedPath); err == nil { 58 | trusted = crts 59 | for _, crt := range crts { 60 | if isSelfSigned(crt) { 61 | roots.AddCert(crt) 62 | } else { 63 | intermediates.AddCert(crt) 64 | } 65 | } 66 | } 67 | 68 | if lists, err := readRevocationLists(trustedCRLPath); err == nil { 69 | crls = append(crls, lists...) 70 | } 71 | 72 | opts := x509.VerifyOptions{ 73 | Roots: roots, 74 | Intermediates: intermediates, 75 | KeyUsages: keyUsages, 76 | DNSName: hostname, 77 | } 78 | 79 | if suppressCertificateHostNameInvalid { 80 | opts.DNSName = "" 81 | } 82 | 83 | if suppressCertificateTimeInvalid { 84 | opts.CurrentTime = certificate.NotBefore 85 | } 86 | 87 | if suppressCertificateChainIncomplete { 88 | if opts.Roots == nil { 89 | opts.Roots = x509.NewCertPool() 90 | } 91 | opts.Roots.AddCert(certificate) 92 | trusted = append(trusted, certificate) 93 | } 94 | 95 | // build chain 96 | chains, err := certificate.Verify(opts) 97 | switch se := err.(type) { 98 | case x509.CertificateInvalidError: 99 | switch se.Reason { 100 | case x509.Expired: 101 | // this is just to pass test 033 of Security Certificate Validation 102 | // UnknownAuthorityErrors should have priority over CertificateInvalidErrors 103 | opts.CurrentTime = certificate.NotBefore 104 | if _, err1 := certificate.Verify(opts); err1 == nil { 105 | err = BadCertificateTimeInvalid 106 | } else { 107 | err = BadSecurityChecksFailed 108 | } 109 | case x509.IncompatibleUsage: 110 | err = BadCertificateUseNotAllowed 111 | default: 112 | err = BadSecurityChecksFailed 113 | } 114 | case x509.HostnameError: 115 | err = BadCertificateHostNameInvalid 116 | case x509.UnknownAuthorityError: 117 | err = BadSecurityChecksFailed 118 | } 119 | 120 | if err == nil { 121 | for _, chain := range chains { 122 | chainMemberTrusted := false 123 | for j, c := range chain { 124 | 125 | //LogInfo.Printf("[%d][%d] %s", i, j, c.Subject.String()) 126 | 127 | // check signature if self-signed (otherwise the signatures are checked when building chain?) 128 | if isSelfSigned(c) { 129 | err2 := c.CheckSignature(c.SignatureAlgorithm, c.RawTBSCertificate, c.Signature) 130 | if err2 != nil { 131 | err = BadSecurityChecksFailed 132 | break 133 | } 134 | } 135 | 136 | // check security policy 137 | 138 | // check trust list 139 | if !chainMemberTrusted { 140 | outer: 141 | for _, c2 := range chain { 142 | for _, c3 := range trusted { 143 | if c2.Equal(c3) { 144 | chainMemberTrusted = true 145 | break outer 146 | } 147 | } 148 | } 149 | } 150 | if !chainMemberTrusted { 151 | err = BadSecurityChecksFailed 152 | break 153 | } 154 | 155 | // check validity period 156 | 157 | // check hostname 158 | 159 | // check URI 160 | // if endpoint != nil { 161 | // uriValid := false 162 | // for _, uri := range c.URIs { 163 | // if uri.String() == endpoint.Server.ApplicationUri { 164 | // uriValid = true 165 | // } 166 | // } 167 | // if !uriValid { 168 | // err = BadCertificateUriInvalid 169 | // break 170 | // } 171 | // } 172 | 173 | // check certificate usage 174 | useValid := false 175 | if j == 0 { // is leaf 176 | outer2: 177 | for _, eku := range c.ExtKeyUsage { 178 | for _, ku := range keyUsages { 179 | if eku == ku { 180 | useValid = true 181 | break outer2 182 | } 183 | } 184 | } 185 | if !useValid { 186 | err = BadCertificateUseNotAllowed 187 | break 188 | } 189 | } else { 190 | if c.KeyUsage&x509.KeyUsageCertSign == x509.KeyUsageCertSign { 191 | useValid = true 192 | } 193 | if !useValid { 194 | err = BadCertificateIssuerUseNotAllowed 195 | break 196 | } 197 | } 198 | 199 | // find issuer revocation list, check revocation 200 | err = checkRevocation(chain, j, crls, suppressCertificateRevocationUnknown) 201 | if err != nil { 202 | if err == BadCertificateRevoked || err == BadCertificateIssuerRevoked { 203 | err = BadSecurityChecksFailed 204 | } 205 | break 206 | } 207 | } 208 | 209 | // this chain passed all checks 210 | if err == nil { 211 | break // no need to validate next chain 212 | } 213 | } 214 | } 215 | if err != nil { 216 | // log.Printf("Error verifying remote certificate. %s\n", err) 217 | if len(rejectedCertsPath) > 0 { 218 | os.MkdirAll(rejectedCertsPath, os.ModeDir|0755) 219 | block := &pem.Block{Type: "CERTIFICATE", Bytes: certificate.Raw} 220 | thumbprint := sha1.Sum(certificate.Raw) 221 | if f, err := os.Create(filepath.Join(rejectedCertsPath, fmt.Sprintf("%x.crt", thumbprint))); err == nil { 222 | pem.Encode(f, block) 223 | defer f.Close() 224 | } 225 | } 226 | return err 227 | } 228 | return nil 229 | } 230 | 231 | // readCertificates reads certificates from path. 232 | // Path may be to a file, comma-separated list of files, or directory. 233 | func readCertificates(path string) ([]*x509.Certificate, error) { 234 | fi, err := os.Stat(path) 235 | if err != nil && !errors.Is(err, fs.ErrNotExist) { 236 | return nil, err 237 | } 238 | files := make([]string, 0, 16) 239 | if fi != nil && fi.IsDir() { 240 | fis, err := os.ReadDir(path) 241 | if err != nil { 242 | return nil, err 243 | } 244 | for _, fi := range fis { 245 | files = append(files, filepath.Join(path, fi.Name())) 246 | } 247 | } else { 248 | files = strings.Split(path, ",") 249 | for i := range files { 250 | files[i] = strings.TrimSpace(files[i]) 251 | } 252 | } 253 | list := make([]*x509.Certificate, 0, 16) 254 | for _, f := range files { 255 | buf, err := os.ReadFile(f) 256 | if err != nil { 257 | return nil, err 258 | } 259 | for len(buf) > 0 { 260 | var block *pem.Block 261 | block, buf = pem.Decode(buf) 262 | if block == nil { 263 | if crts, err := x509.ParseCertificates(buf); err == nil { 264 | list = append(list, crts...) 265 | } 266 | break 267 | } 268 | if block.Type != "CERTIFICATE" || len(block.Headers) != 0 { 269 | continue 270 | } 271 | if crt, err := x509.ParseCertificate(block.Bytes); err == nil { 272 | list = append(list, crt) 273 | } 274 | } 275 | } 276 | return list, nil 277 | } 278 | 279 | // readRevocationLists reads certificate revocation lists from path. 280 | // Path may be to a file, comma-separated list of files, or directory. 281 | func readRevocationLists(path string) ([]*x509.RevocationList, error) { 282 | fi, err := os.Stat(path) 283 | if err != nil && !errors.Is(err, fs.ErrNotExist) { 284 | return nil, err 285 | } 286 | files := make([]string, 0, 16) 287 | if fi != nil && fi.IsDir() { 288 | fis, err := os.ReadDir(path) 289 | if err != nil { 290 | return nil, err 291 | } 292 | for _, fi := range fis { 293 | files = append(files, filepath.Join(path, fi.Name())) 294 | } 295 | } else { 296 | files = strings.Split(path, ",") 297 | for i := range files { 298 | files[i] = strings.TrimSpace(files[i]) 299 | } 300 | } 301 | list := make([]*x509.RevocationList, 0, 16) 302 | for _, f := range files { 303 | buf, err := os.ReadFile(f) 304 | if err != nil { 305 | return nil, err 306 | } 307 | for len(buf) > 0 { 308 | var block *pem.Block 309 | block, buf = pem.Decode(buf) 310 | if block == nil { 311 | if crl, err := x509.ParseRevocationList(buf); err == nil { 312 | list = append(list, crl) 313 | } 314 | break 315 | } 316 | if block.Type != "X509 CRL" || len(block.Headers) != 0 { 317 | continue 318 | } 319 | if crl, err := x509.ParseRevocationList(block.Bytes); err == nil { 320 | list = append(list, crl) 321 | } 322 | } 323 | } 324 | return list, nil 325 | } 326 | 327 | // isSelfSigned returns true if the certificate is self-signed. 328 | func isSelfSigned(certificate *x509.Certificate) bool { 329 | return bytes.Equal(certificate.RawIssuer, certificate.RawSubject) 330 | } 331 | 332 | // checkRevocation returns error if certificate was revoked, or revokation list was not found. 333 | func checkRevocation(chain []*x509.Certificate, index int, crls []*x509.RevocationList, suppressCertificateRevocationUnknown bool) error { 334 | if index+1 >= len(chain) { 335 | return nil 336 | } 337 | flag := false 338 | cert := chain[index] 339 | issuer := chain[index+1] 340 | isLeaf := index == 0 341 | for _, crl := range crls { 342 | if time.Now().Before(crl.NextUpdate) { 343 | if err := crl.CheckSignatureFrom(issuer); err == nil { 344 | flag = true 345 | for _, c := range crl.RevokedCertificates { 346 | if c.SerialNumber.Cmp(cert.SerialNumber) == 0 { 347 | if isLeaf { 348 | return BadCertificateRevoked 349 | } 350 | return BadCertificateIssuerRevoked 351 | } 352 | } 353 | break 354 | } 355 | } 356 | } 357 | if !flag && !suppressCertificateRevocationUnknown { 358 | if isLeaf { 359 | return BadCertificateRevocationUnknown 360 | } 361 | return BadCertificateIssuerRevocationUnknown 362 | } 363 | return nil 364 | } 365 | -------------------------------------------------------------------------------- /ua/certificate_list.go: -------------------------------------------------------------------------------- 1 | package ua 2 | 3 | import ( 4 | "crypto/x509" 5 | "encoding/pem" 6 | ) 7 | 8 | // CertificateList is a set of certificates. 9 | type CertificateList struct { 10 | bySubjectKeyID map[string][]int 11 | byName map[string][]int 12 | certs []*x509.Certificate 13 | } 14 | 15 | // NewCertificateList returns a new, empty CertificateList. 16 | func NewCertificateList() *CertificateList { 17 | return &CertificateList{ 18 | bySubjectKeyID: make(map[string][]int), 19 | byName: make(map[string][]int), 20 | } 21 | } 22 | 23 | // FindPotentialParents returns the certificates which might have signed cert. 24 | func (s *CertificateList) FindPotentialParents(cert *x509.Certificate) []*x509.Certificate { 25 | if s == nil { 26 | return nil 27 | } 28 | if len(cert.AuthorityKeyId) > 0 { 29 | ids := s.bySubjectKeyID[string(cert.AuthorityKeyId)] 30 | res := make([]*x509.Certificate, len(ids)) 31 | for i, j := range ids { 32 | res[i] = s.certs[j] 33 | } 34 | return res 35 | } 36 | ids := s.byName[string(cert.RawIssuer)] 37 | res := make([]*x509.Certificate, len(ids)) 38 | for i, j := range ids { 39 | res[i] = s.certs[j] 40 | } 41 | return res 42 | } 43 | 44 | // Contains returns true if the list contains the certificate. 45 | func (s *CertificateList) Contains(cert *x509.Certificate) bool { 46 | if s == nil { 47 | return false 48 | } 49 | 50 | candidates := s.byName[string(cert.RawSubject)] 51 | for _, c := range candidates { 52 | if s.certs[c].Equal(cert) { 53 | return true 54 | } 55 | } 56 | 57 | return false 58 | } 59 | 60 | // AddCert adds a certificate to a list. 61 | func (s *CertificateList) AddCert(cert *x509.Certificate) { 62 | if cert == nil { 63 | panic("adding nil Certificate to CertificateList") 64 | } 65 | 66 | // Check that the certificate isn't being added twice. 67 | if s.Contains(cert) { 68 | return 69 | } 70 | 71 | n := len(s.certs) 72 | s.certs = append(s.certs, cert) 73 | 74 | if len(cert.SubjectKeyId) > 0 { 75 | keyID := string(cert.SubjectKeyId) 76 | s.bySubjectKeyID[keyID] = append(s.bySubjectKeyID[keyID], n) 77 | } 78 | name := string(cert.RawSubject) 79 | s.byName[name] = append(s.byName[name], n) 80 | } 81 | 82 | // AppendCertsFromPEM attempts to parse a series of PEM encoded certificates. 83 | // It appends any certificates found to s and reports whether any certificates 84 | // were successfully parsed. 85 | // 86 | // On many Linux systems, /etc/ssl/cert.pem will contain the system wide set 87 | // of root CAs in a format suitable for this function. 88 | func (s *CertificateList) AppendCertsFromPEM(pemCerts []byte) (ok bool) { 89 | for len(pemCerts) > 0 { 90 | var block *pem.Block 91 | block, pemCerts = pem.Decode(pemCerts) 92 | if block == nil { 93 | break 94 | } 95 | if block.Type != "CERTIFICATE" || len(block.Headers) != 0 { 96 | continue 97 | } 98 | 99 | cert, err := x509.ParseCertificate(block.Bytes) 100 | if err != nil { 101 | continue 102 | } 103 | 104 | s.AddCert(cert) 105 | ok = true 106 | } 107 | 108 | return 109 | } 110 | 111 | // Subjects returns a list of the DER-encoded subjects of 112 | // all of the certificates in the pool. 113 | func (s *CertificateList) Subjects() [][]byte { 114 | res := make([][]byte, len(s.certs)) 115 | for i, c := range s.certs { 116 | res[i] = c.RawSubject 117 | } 118 | return res 119 | } 120 | -------------------------------------------------------------------------------- /ua/condition.go: -------------------------------------------------------------------------------- 1 | // Copyright 2021 Converter Systems LLC. All rights reserved. 2 | 3 | package ua 4 | 5 | import ( 6 | "time" 7 | ) 8 | 9 | // Condition structure. 10 | type Condition struct { 11 | EventID ByteString 12 | EventType NodeID 13 | SourceNode NodeID 14 | SourceName string 15 | Time time.Time 16 | ReceiveTime time.Time 17 | Message LocalizedText 18 | Severity uint16 19 | ConditionID NodeID 20 | ConditionName string 21 | BranchID NodeID 22 | Retain bool 23 | } 24 | 25 | // UnmarshalFields ... 26 | func (evt *Condition) UnmarshalFields(eventFields []Variant) error { 27 | if len(eventFields) != 12 { 28 | return BadUnexpectedError 29 | } 30 | evt.EventID, _ = eventFields[0].(ByteString) 31 | evt.EventType, _ = eventFields[1].(NodeID) 32 | evt.SourceNode, _ = eventFields[2].(NodeID) 33 | evt.SourceName, _ = eventFields[3].(string) 34 | evt.Time, _ = eventFields[4].(time.Time) 35 | evt.ReceiveTime, _ = eventFields[5].(time.Time) 36 | evt.Message, _ = eventFields[6].(LocalizedText) 37 | evt.Severity, _ = eventFields[7].(uint16) 38 | evt.ConditionID, _ = eventFields[8].(NodeID) 39 | evt.ConditionName, _ = eventFields[9].(string) 40 | evt.BranchID, _ = eventFields[10].(NodeID) 41 | evt.Retain, _ = eventFields[11].(bool) 42 | return nil 43 | } 44 | 45 | // GetAttribute ... 46 | func (e *Condition) GetAttribute(clause SimpleAttributeOperand) Variant { 47 | switch { 48 | case EqualSimpleAttributeOperand(clause, ConditionSelectClauses[0]): 49 | return Variant(e.EventID) 50 | case EqualSimpleAttributeOperand(clause, ConditionSelectClauses[1]): 51 | return Variant(e.EventType) 52 | case EqualSimpleAttributeOperand(clause, ConditionSelectClauses[2]): 53 | return Variant(e.SourceNode) 54 | case EqualSimpleAttributeOperand(clause, ConditionSelectClauses[3]): 55 | return Variant(e.SourceName) 56 | case EqualSimpleAttributeOperand(clause, ConditionSelectClauses[4]): 57 | return Variant(e.Time) 58 | case EqualSimpleAttributeOperand(clause, ConditionSelectClauses[5]): 59 | return Variant(e.ReceiveTime) 60 | case EqualSimpleAttributeOperand(clause, ConditionSelectClauses[6]): 61 | return Variant(e.Message) 62 | case EqualSimpleAttributeOperand(clause, ConditionSelectClauses[7]): 63 | return Variant(e.Severity) 64 | case EqualSimpleAttributeOperand(clause, ConditionSelectClauses[8]): 65 | return Variant(e.ConditionID) 66 | case EqualSimpleAttributeOperand(clause, ConditionSelectClauses[9]): 67 | return Variant(e.ConditionName) 68 | case EqualSimpleAttributeOperand(clause, ConditionSelectClauses[10]): 69 | return Variant(e.BranchID) 70 | case EqualSimpleAttributeOperand(clause, ConditionSelectClauses[11]): 71 | return Variant(e.Retain) 72 | default: 73 | return nil 74 | } 75 | } 76 | 77 | // ConditionSelectClauses ... 78 | var ConditionSelectClauses []SimpleAttributeOperand = []SimpleAttributeOperand{ 79 | {TypeDefinitionID: ObjectTypeIDBaseEventType, BrowsePath: ParseBrowsePath("EventId"), AttributeID: AttributeIDValue}, 80 | {TypeDefinitionID: ObjectTypeIDBaseEventType, BrowsePath: ParseBrowsePath("EventType"), AttributeID: AttributeIDValue}, 81 | {TypeDefinitionID: ObjectTypeIDBaseEventType, BrowsePath: ParseBrowsePath("SourceNode"), AttributeID: AttributeIDValue}, 82 | {TypeDefinitionID: ObjectTypeIDBaseEventType, BrowsePath: ParseBrowsePath("SourceName"), AttributeID: AttributeIDValue}, 83 | {TypeDefinitionID: ObjectTypeIDBaseEventType, BrowsePath: ParseBrowsePath("Time"), AttributeID: AttributeIDValue}, 84 | {TypeDefinitionID: ObjectTypeIDBaseEventType, BrowsePath: ParseBrowsePath("ReceiveTime"), AttributeID: AttributeIDValue}, 85 | {TypeDefinitionID: ObjectTypeIDBaseEventType, BrowsePath: ParseBrowsePath("Message"), AttributeID: AttributeIDValue}, 86 | {TypeDefinitionID: ObjectTypeIDBaseEventType, BrowsePath: ParseBrowsePath("Severity"), AttributeID: AttributeIDValue}, 87 | {TypeDefinitionID: ObjectTypeIDConditionType, BrowsePath: ParseBrowsePath(""), AttributeID: AttributeIDNodeID}, 88 | {TypeDefinitionID: ObjectTypeIDConditionType, BrowsePath: ParseBrowsePath("ConditionName"), AttributeID: AttributeIDValue}, 89 | {TypeDefinitionID: ObjectTypeIDConditionType, BrowsePath: ParseBrowsePath("BranchId"), AttributeID: AttributeIDValue}, 90 | {TypeDefinitionID: ObjectTypeIDConditionType, BrowsePath: ParseBrowsePath("Retain"), AttributeID: AttributeIDValue}, 91 | } 92 | -------------------------------------------------------------------------------- /ua/content_filter.go: -------------------------------------------------------------------------------- 1 | // Copyright 2021 Converter Systems LLC. All rights reserved. 2 | 3 | package ua 4 | 5 | func EqualSimpleAttributeOperand(a, b SimpleAttributeOperand) bool { 6 | if a.TypeDefinitionID != ObjectTypeIDBaseEventType && a.TypeDefinitionID != b.TypeDefinitionID { 7 | return false 8 | } 9 | if len(a.BrowsePath) != len(b.BrowsePath) { 10 | return false 11 | } 12 | for i := 0; i < len(a.BrowsePath); i++ { 13 | if a.BrowsePath[i] != b.BrowsePath[i] { 14 | return false 15 | } 16 | } 17 | if a.AttributeID != b.AttributeID { 18 | return false 19 | } 20 | if a.IndexRange != b.IndexRange { 21 | return false 22 | } 23 | return true 24 | } 25 | -------------------------------------------------------------------------------- /ua/data_value.go: -------------------------------------------------------------------------------- 1 | // Copyright 2021 Converter Systems LLC. All rights reserved. 2 | 3 | package ua 4 | 5 | import ( 6 | "time" 7 | ) 8 | 9 | // DataValue holds the value, quality and timestamp 10 | type DataValue struct { 11 | Value Variant 12 | StatusCode StatusCode 13 | SourceTimestamp time.Time 14 | SourcePicoseconds uint16 15 | ServerTimestamp time.Time 16 | ServerPicoseconds uint16 17 | } 18 | 19 | func NewDataValue(value Variant, status StatusCode, sourceTimestamp time.Time, sourcePicoseconds uint16, serverTimestamp time.Time, serverPicoseconds uint16) DataValue { 20 | return DataValue{value, status, sourceTimestamp, sourcePicoseconds, serverTimestamp, serverPicoseconds} 21 | } 22 | 23 | // NilDataValue is the nil value. 24 | var NilDataValue = DataValue{} 25 | -------------------------------------------------------------------------------- /ua/diagnostic_info.go: -------------------------------------------------------------------------------- 1 | // Copyright 2021 Converter Systems LLC. All rights reserved. 2 | 3 | package ua 4 | 5 | // DiagnosticInfo holds additional info regarding errors in service calls. 6 | type DiagnosticInfo struct { 7 | // SymbolicID returns the SymbolicID. 8 | SymbolicID *int32 `json:",omitempty"` 9 | // NamespaceURI returns the index of the NamespaceURI. 10 | NamespaceURI *int32 `json:",omitempty"` 11 | // Locale returns the index of the Locale. 12 | Locale *int32 `json:",omitempty"` 13 | // LocalizedText returns the index of the LocalizedText. 14 | LocalizedText *int32 `json:",omitempty"` 15 | // AdditionalInfo returns the AdditionalInfo. 16 | AdditionalInfo *string `json:",omitempty"` 17 | // InnerStatusCode returns the InnerStatusCode. 18 | InnerStatusCode *StatusCode `json:",omitempty"` 19 | // InnerDiagnosticInfo returns the InnerDiagnosticInfo. 20 | InnerDiagnosticInfo *DiagnosticInfo `json:",omitempty"` 21 | } 22 | -------------------------------------------------------------------------------- /ua/doc.go: -------------------------------------------------------------------------------- 1 | // Copyright 2021 Converter Systems LLC. All rights reserved. 2 | 3 | // Package opcua provides support for OPC Unified Architecture in Go. 4 | // For more information, visit https://reference.opcfoundation.org/v104/ 5 | // 6 | package ua 7 | -------------------------------------------------------------------------------- /ua/encoding_context.go: -------------------------------------------------------------------------------- 1 | // Copyright 2021 Converter Systems LLC. All rights reserved. 2 | 3 | package ua 4 | 5 | // EncodingContext provides a table of NamespaceURIs for encoders/decoders. 6 | type EncodingContext interface { 7 | NamespaceURIs() []string 8 | } 9 | 10 | type encodingContext struct { 11 | namespaceURIs []string 12 | } 13 | 14 | // NewEncodingContext constructs a default EncodingContext. 15 | func NewEncodingContext() EncodingContext { 16 | return &encodingContext{[]string{"http://opcfoundation.org/UA/"}} 17 | } 18 | 19 | // NamespaceURIs returns a slice of NamespaceURI 20 | func (ec *encodingContext) NamespaceURIs() []string { 21 | return ec.namespaceURIs 22 | } 23 | -------------------------------------------------------------------------------- /ua/event_notifier.go: -------------------------------------------------------------------------------- 1 | // Copyright 2021 Converter Systems LLC. All rights reserved. 2 | 3 | package ua 4 | 5 | // EventNotifier are flags that can be set for the EventNotifier attribute. 6 | const ( 7 | EventNotifierNone byte = 0x0 8 | EventNotifierSubscribeToEvents byte = 0x1 9 | EventNotifierHistoryRead byte = 0x4 10 | EventNotifierHistoryWrite byte = 0x8 11 | ) 12 | -------------------------------------------------------------------------------- /ua/expanded_nodeid.go: -------------------------------------------------------------------------------- 1 | // Copyright 2020 Converter Systems LLC. All rights reserved. 2 | 3 | package ua 4 | 5 | import ( 6 | "fmt" 7 | "strconv" 8 | "strings" 9 | ) 10 | 11 | // ExpandedNodeID identifies a remote Node. 12 | type ExpandedNodeID struct { 13 | ServerIndex uint32 14 | NamespaceURI string 15 | NodeID NodeID 16 | } 17 | 18 | func NewExpandedNodeID(nodeID NodeID) ExpandedNodeID { 19 | return ExpandedNodeID{0, "", nodeID} 20 | } 21 | 22 | // NilExpandedNodeID is the nil value. 23 | var NilExpandedNodeID = ExpandedNodeID{0, "", nil} 24 | 25 | // ParseExpandedNodeID returns a NodeID from a string representation. 26 | // - ParseExpandedNodeID("i=85") // integer, assumes nsu=http://opcfoundation.org/UA/ 27 | // - ParseExpandedNodeID("nsu=http://www.unifiedautomation.com/DemoServer/;s=Demo.Static.Scalar.Float") // string 28 | // - ParseExpandedNodeID("nsu=http://www.unifiedautomation.com/DemoServer/;g=5ce9dbce-5d79-434c-9ac3-1cfba9a6e92c") // guid 29 | // - ParseExpandedNodeID("nsu=http://www.unifiedautomation.com/DemoServer/;b=YWJjZA==") // opaque byte string 30 | func ParseExpandedNodeID(s string) ExpandedNodeID { 31 | var svr uint64 32 | var err error 33 | if strings.HasPrefix(s, "svr=") { 34 | var pos = strings.Index(s, ";") 35 | if pos == -1 { 36 | return NilExpandedNodeID 37 | } 38 | 39 | svr, err = strconv.ParseUint(s[4:pos], 10, 32) 40 | if err != nil { 41 | return NilExpandedNodeID 42 | } 43 | s = s[pos+1:] 44 | } 45 | 46 | var nsu string 47 | if strings.HasPrefix(s, "nsu=") { 48 | var pos = strings.Index(s, ";") 49 | if pos == -1 { 50 | return NilExpandedNodeID 51 | } 52 | 53 | nsu = s[4:pos] 54 | s = s[pos+1:] 55 | } 56 | 57 | return ExpandedNodeID{uint32(svr), nsu, ParseNodeID(s)} 58 | } 59 | 60 | // String returns a string representation of the ExpandedNodeID, e.g. "nsu=http://www.unifiedautomation.com/DemoServer/;s=Demo" 61 | func (n ExpandedNodeID) String() string { 62 | b := new(strings.Builder) 63 | if n.ServerIndex > 0 { 64 | fmt.Fprintf(b, "svr=%d;", n.ServerIndex) 65 | } 66 | if len(n.NamespaceURI) > 0 { 67 | fmt.Fprintf(b, "nsu=%s;", n.NamespaceURI) 68 | } 69 | switch n2 := n.NodeID.(type) { 70 | case NodeIDNumeric: 71 | b.WriteString(n2.String()) 72 | case NodeIDString: 73 | b.WriteString(n2.String()) 74 | case NodeIDGUID: 75 | b.WriteString(n2.String()) 76 | case NodeIDOpaque: 77 | b.WriteString(n2.String()) 78 | default: 79 | b.WriteString("i=0") 80 | } 81 | return b.String() 82 | } 83 | 84 | // ToNodeID converts ExpandedNodeID to NodeID by looking up the NamespaceURI and replacing it with the index. 85 | func ToNodeID(n ExpandedNodeID, namespaceURIs []string) NodeID { 86 | if n.NamespaceURI == "" { 87 | return n.NodeID 88 | } 89 | ns := uint16(0) 90 | flag := false 91 | for i, uri := range namespaceURIs { 92 | if uri == n.NamespaceURI { 93 | ns = uint16(i) 94 | flag = true 95 | break 96 | } 97 | } 98 | if !flag { 99 | return nil 100 | } 101 | switch n2 := n.NodeID.(type) { 102 | case NodeIDNumeric: 103 | return NodeIDNumeric{ns, n2.ID} 104 | case NodeIDString: 105 | return NodeIDString{ns, n2.ID} 106 | case NodeIDGUID: 107 | return NodeIDGUID{ns, n2.ID} 108 | case NodeIDOpaque: 109 | return NodeIDOpaque{ns, n2.ID} 110 | default: 111 | return nil 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /ua/extension_object.go: -------------------------------------------------------------------------------- 1 | // Copyright 2021 Converter Systems LLC. All rights reserved. 2 | 3 | package ua 4 | 5 | // ExtensionObject stores a struct. 6 | // Register the struct type and id with the BinaryEncoder using 7 | // 8 | // func RegisterBinaryEncodingID(typ reflect.Type, id ExpandedNodeID) 9 | type ExtensionObject any 10 | -------------------------------------------------------------------------------- /ua/gen_opcua.go: -------------------------------------------------------------------------------- 1 | package ua 2 | 3 | //go:generate go install "github.com/awcullen/opcua/cmd/gen_opcua" 4 | //go:generate gen_opcua -in ../schema -out . 5 | -------------------------------------------------------------------------------- /ua/localized_text.go: -------------------------------------------------------------------------------- 1 | // Copyright 2021 Converter Systems LLC. All rights reserved. 2 | 3 | package ua 4 | 5 | import ( 6 | "fmt" 7 | ) 8 | 9 | // LocalizedText pairs text and a Locale string. 10 | type LocalizedText struct { 11 | Text string `xml:",innerxml"` 12 | Locale string `xml:"Locale,attr"` 13 | } 14 | 15 | // NewLocalizedText constructs a LocalizedText from text and Locale string. 16 | func NewLocalizedText(text, locale string) LocalizedText { 17 | return LocalizedText{text, locale} 18 | } 19 | 20 | // String returns the string representation, e.g. "text (locale)" 21 | func (a LocalizedText) String() string { 22 | if a.Locale == "" { 23 | return a.Text 24 | } 25 | return fmt.Sprintf("%s (%s)", a.Text, a.Locale) 26 | } 27 | 28 | func (a LocalizedText) MarshalText() ([]byte, error) { 29 | return []byte(a.String()), nil 30 | } 31 | -------------------------------------------------------------------------------- /ua/message_types.go: -------------------------------------------------------------------------------- 1 | // Copyright 2021 Converter Systems LLC. All rights reserved. 2 | 3 | package ua 4 | 5 | // MessageType indicate the kind of message. 6 | const ( 7 | MessageTypeHello uint32 = 'H' | 'E'<<8 | 'L'<<16 | 'F'<<24 8 | MessageTypeAck uint32 = 'A' | 'C'<<8 | 'K'<<16 | 'F'<<24 9 | MessageTypeError uint32 = 'E' | 'R'<<8 | 'R'<<16 | 'F'<<24 10 | MessageTypeReverseHello uint32 = 'R' | 'H'<<8 | 'E'<<16 | 'F'<<24 11 | MessageTypeOpenFinal uint32 = 'O' | 'P'<<8 | 'N'<<16 | 'F'<<24 12 | MessageTypeCloseFinal uint32 = 'C' | 'L'<<8 | 'O'<<16 | 'F'<<24 13 | MessageTypeFinal uint32 = 'M' | 'S'<<8 | 'G'<<16 | 'F'<<24 14 | MessageTypeChunk uint32 = 'M' | 'S'<<8 | 'G'<<16 | 'C'<<24 15 | MessageTypeAbort uint32 = 'M' | 'S'<<8 | 'G'<<16 | 'A'<<24 16 | ) 17 | -------------------------------------------------------------------------------- /ua/nodeid.go: -------------------------------------------------------------------------------- 1 | // Copyright 2020 Converter Systems LLC. All rights reserved. 2 | 3 | package ua 4 | 5 | import ( 6 | "encoding/base64" 7 | "fmt" 8 | "strconv" 9 | "strings" 10 | 11 | uuid "github.com/google/uuid" 12 | ) 13 | 14 | // NodeID identifies a Node. 15 | type NodeID interface { 16 | nodeID() 17 | } 18 | 19 | // NodeIDNumeric is a NodeID of numeric type. 20 | type NodeIDNumeric struct { 21 | NamespaceIndex uint16 22 | ID uint32 23 | } 24 | 25 | // NewNodeIDNumeric makes a NodeID of numeric type. 26 | func NewNodeIDNumeric(ns uint16, id uint32) NodeIDNumeric { 27 | return NodeIDNumeric{ns, id} 28 | } 29 | 30 | func (n NodeIDNumeric) nodeID() {} 31 | 32 | // String returns a string representation, e.g. "i=85" 33 | func (n NodeIDNumeric) String() string { 34 | if n.NamespaceIndex == 0 { 35 | return fmt.Sprintf("i=%d", n.ID) 36 | } 37 | return fmt.Sprintf("ns=%d;i=%d", n.NamespaceIndex, n.ID) 38 | } 39 | 40 | func (n NodeIDNumeric) MarshalText() ([]byte, error) { 41 | return []byte(n.String()), nil 42 | } 43 | 44 | // NodeIDString is a NodeID of string type. 45 | type NodeIDString struct { 46 | NamespaceIndex uint16 47 | ID string 48 | } 49 | 50 | // NewNodeIDString makes a NodeID of string type. 51 | func NewNodeIDString(ns uint16, id string) NodeIDString { 52 | return NodeIDString{ns, id} 53 | } 54 | 55 | func (n NodeIDString) nodeID() {} 56 | 57 | // String returns a string representation, e.g. "ns=2;s=Demo.Static.Scalar.Float" 58 | func (n NodeIDString) String() string { 59 | if n.NamespaceIndex == 0 { 60 | return fmt.Sprintf("s=%s", n.ID) 61 | } 62 | return fmt.Sprintf("ns=%d;s=%s", n.NamespaceIndex, n.ID) 63 | } 64 | 65 | func (n NodeIDString) MarshalText() ([]byte, error) { 66 | return []byte(n.String()), nil 67 | } 68 | 69 | // NodeIDGUID is a NodeID of GUID type. 70 | type NodeIDGUID struct { 71 | NamespaceIndex uint16 72 | ID uuid.UUID 73 | } 74 | 75 | // NewNodeIDGUID makes a NodeID of GUID type. 76 | func NewNodeIDGUID(ns uint16, id uuid.UUID) NodeIDGUID { 77 | return NodeIDGUID{ns, id} 78 | } 79 | 80 | func (n NodeIDGUID) nodeID() {} 81 | 82 | // String returns a string representation, e.g. "ns=2;g=5ce9dbce-5d79-434c-9ac3-1cfba9a6e92c" 83 | func (n NodeIDGUID) String() string { 84 | if n.NamespaceIndex == 0 { 85 | return fmt.Sprintf("g=%s", n.ID) 86 | } 87 | return fmt.Sprintf("ns=%d;g=%s", n.NamespaceIndex, n.ID) 88 | } 89 | 90 | func (n NodeIDGUID) MarshalText() ([]byte, error) { 91 | return []byte(n.String()), nil 92 | } 93 | 94 | // NodeIDOpaque is a new NodeID of opaque type. 95 | type NodeIDOpaque struct { 96 | NamespaceIndex uint16 97 | ID ByteString 98 | } 99 | 100 | // NewNodeIDOpaque makes a NodeID of opaque type. 101 | func NewNodeIDOpaque(ns uint16, id ByteString) NodeIDOpaque { 102 | return NodeIDOpaque{ns, id} 103 | } 104 | 105 | func (n NodeIDOpaque) nodeID() {} 106 | 107 | // String returns a string representation, e.g. "ns=2;b=YWJjZA==" 108 | func (n NodeIDOpaque) String() string { 109 | if n.NamespaceIndex == 0 { 110 | return fmt.Sprintf("b=%s", base64.StdEncoding.EncodeToString([]byte(n.ID))) 111 | } 112 | return fmt.Sprintf("ns=%d;b=%s", n.NamespaceIndex, base64.StdEncoding.EncodeToString([]byte(n.ID))) 113 | } 114 | 115 | func (n NodeIDOpaque) MarshalText() ([]byte, error) { 116 | return []byte(n.String()), nil 117 | } 118 | 119 | // ParseNodeID returns a NodeID from a string representation. 120 | // - ParseNodeID("i=85") // integer, assumes ns=0 121 | // - ParseNodeID("ns=2;s=Demo.Static.Scalar.Float") // string 122 | // - ParseNodeID("ns=2;g=5ce9dbce-5d79-434c-9ac3-1cfba9a6e92c") // guid 123 | // - ParseNodeID("ns=2;b=YWJjZA==") // opaque byte string 124 | func ParseNodeID(s string) NodeID { 125 | var ns uint64 126 | var err error 127 | if strings.HasPrefix(s, "ns=") { 128 | var pos = strings.Index(s, ";") 129 | if pos == -1 { 130 | return nil 131 | } 132 | ns, err = strconv.ParseUint(s[3:pos], 10, 16) 133 | if err != nil { 134 | return nil 135 | } 136 | s = s[pos+1:] 137 | } 138 | switch { 139 | case strings.HasPrefix(s, "i="): 140 | var id, err = strconv.ParseUint(s[2:], 10, 32) 141 | if err != nil { 142 | return nil 143 | } 144 | if id == 0 && ns == 0 { 145 | return nil 146 | } 147 | return NodeIDNumeric{uint16(ns), uint32(id)} 148 | case strings.HasPrefix(s, "s="): 149 | return NodeIDString{uint16(ns), s[2:]} 150 | case strings.HasPrefix(s, "g="): 151 | var id, err = uuid.Parse(s[2:]) 152 | if err != nil { 153 | return nil 154 | } 155 | return NodeIDGUID{uint16(ns), id} 156 | case strings.HasPrefix(s, "b="): 157 | var id, err = base64.StdEncoding.DecodeString(s[2:]) 158 | if err != nil { 159 | return nil 160 | } 161 | return NodeIDOpaque{uint16(ns), ByteString(id)} 162 | } 163 | return nil 164 | } 165 | 166 | // ToExpandedNodeID converts the NodeID to an ExpandedNodeID. 167 | // Note: When creating a reference, and the target NodeID is a local node, 168 | // use: NewExpandedNodeID(nodeId) 169 | func ToExpandedNodeID(n NodeID, namespaceURIs []string) ExpandedNodeID { 170 | switch n2 := n.(type) { 171 | case NodeIDNumeric: 172 | if n2.NamespaceIndex > 0 && n2.NamespaceIndex < uint16(len(namespaceURIs)) { 173 | return ExpandedNodeID{0, namespaceURIs[n2.NamespaceIndex], NodeIDNumeric{ID: n2.ID}} 174 | } 175 | return ExpandedNodeID{NodeID: n} 176 | case NodeIDString: 177 | if n2.NamespaceIndex > 0 && n2.NamespaceIndex < uint16(len(namespaceURIs)) { 178 | return ExpandedNodeID{0, namespaceURIs[n2.NamespaceIndex], NodeIDString{ID: n2.ID}} 179 | } 180 | return ExpandedNodeID{NodeID: n} 181 | case NodeIDGUID: 182 | if n2.NamespaceIndex > 0 && n2.NamespaceIndex < uint16(len(namespaceURIs)) { 183 | return ExpandedNodeID{0, namespaceURIs[n2.NamespaceIndex], NodeIDGUID{ID: n2.ID}} 184 | } 185 | return ExpandedNodeID{NodeID: n} 186 | case NodeIDOpaque: 187 | if n2.NamespaceIndex > 0 && n2.NamespaceIndex < uint16(len(namespaceURIs)) { 188 | return ExpandedNodeID{0, namespaceURIs[n2.NamespaceIndex], NodeIDOpaque{ID: n2.ID}} 189 | } 190 | return ExpandedNodeID{NodeID: n} 191 | default: 192 | return NilExpandedNodeID 193 | } 194 | } 195 | -------------------------------------------------------------------------------- /ua/qualified_name.go: -------------------------------------------------------------------------------- 1 | // Copyright 2021 Converter Systems LLC. All rights reserved. 2 | 3 | package ua 4 | 5 | import ( 6 | "fmt" 7 | "strconv" 8 | "strings" 9 | ) 10 | 11 | // QualifiedName pairs a name and a namespace index. 12 | type QualifiedName struct { 13 | NamespaceIndex uint16 14 | Name string 15 | } 16 | 17 | // NewQualifiedName constructs a QualifiedName from a namespace index and a name. 18 | func NewQualifiedName(ns uint16, text string) QualifiedName { 19 | return QualifiedName{ns, text} 20 | } 21 | 22 | // ParseQualifiedName returns a QualifiedName from a string, e.g. ParseQualifiedName("2:Demo") 23 | func ParseQualifiedName(s string) QualifiedName { 24 | var ns uint64 25 | var pos = strings.Index(s, ":") 26 | if pos == -1 { 27 | return QualifiedName{uint16(ns), s} 28 | } 29 | ns, err := strconv.ParseUint(s[:pos], 10, 16) 30 | if err != nil { 31 | return QualifiedName{uint16(ns), s} 32 | } 33 | s = s[pos+1:] 34 | return QualifiedName{uint16(ns), s} 35 | } 36 | 37 | // ParseBrowsePath returns a slice of QualifiedNames from a string, e.g. ParseBrowsePath("2:Demo/2:Dynamic") 38 | func ParseBrowsePath(s string) []QualifiedName { 39 | //TODO: see part4 Annex A.2 40 | if len(s) == 0 { 41 | return []QualifiedName{} 42 | } 43 | toks := strings.Split(s, "/") 44 | path := make([]QualifiedName, len(toks)) 45 | for i, tok := range toks { 46 | path[i] = ParseQualifiedName(tok) 47 | } 48 | return path 49 | } 50 | 51 | // String returns a string representation, e.g. "2:Demo" 52 | func (a QualifiedName) String() string { 53 | return fmt.Sprintf("%d:%s", a.NamespaceIndex, a.Name) 54 | } 55 | 56 | func (a QualifiedName) MarshalText() ([]byte, error) { 57 | return []byte(a.String()), nil 58 | } 59 | -------------------------------------------------------------------------------- /ua/reference.go: -------------------------------------------------------------------------------- 1 | package ua 2 | 3 | // Reference ... 4 | type Reference struct { 5 | ReferenceTypeID NodeID 6 | IsInverse bool 7 | TargetID ExpandedNodeID 8 | } 9 | 10 | func NewReference(referenceTypeID NodeID, isInverse bool, targetID ExpandedNodeID) Reference { 11 | return Reference{referenceTypeID, isInverse, targetID} 12 | } 13 | -------------------------------------------------------------------------------- /ua/rsa_uris.go: -------------------------------------------------------------------------------- 1 | // Copyright 2021 Converter Systems LLC. All rights reserved. 2 | 3 | package ua 4 | 5 | // RSA URIs. 6 | const ( 7 | RsaSha1Signature = "http://www.w3.org/2000/09/xmldsig#rsa-sha1" 8 | RsaSha256Signature = "http://www.w3.org/2001/04/xmldsig-more#rsa-sha256" 9 | RsaPssSha256Signature = "http://opcfoundation.org/UA/security/rsa-pss-sha2-256" 10 | RsaV15KeyWrap = "http://www.w3.org/2001/04/xmlenc#rsa-1_5" 11 | RsaOaepKeyWrap = "http://www.w3.org/2001/04/xmlenc#rsa-oaep" 12 | RsaOaepSha256KeyWrap = "http://opcfoundation.org/UA/security/rsa-oaep-sha2-256" 13 | ) 14 | -------------------------------------------------------------------------------- /ua/security_token.go: -------------------------------------------------------------------------------- 1 | // Copyright 2021 Converter Systems LLC. All rights reserved. 2 | 3 | package ua 4 | 5 | import ( 6 | "crypto/cipher" 7 | "hash" 8 | "time" 9 | ) 10 | 11 | // TODO: implement list 12 | type SecurityToken struct { 13 | ChannelID uint32 14 | TokenID uint32 15 | CreatedAt time.Time 16 | Lifetime int 17 | LocalNonce []byte 18 | RemoteNonce []byte 19 | LocalSigningKey []byte 20 | LocalEncryptingKey []byte 21 | LocalInitializationVector []byte 22 | RemoteSigningKey []byte 23 | RemoteEncryptingKey []byte 24 | RemoteInitializationVector []byte 25 | LocalHmac hash.Hash 26 | RemoteHmac hash.Hash 27 | LocalEncryptor cipher.Block 28 | RemoteEncryptor cipher.Block 29 | } 30 | -------------------------------------------------------------------------------- /ua/server_capabilites.go: -------------------------------------------------------------------------------- 1 | package ua 2 | 3 | // ServerCapabilities contains the server capabilities. 4 | type ServerCapabilities struct { 5 | LocaleIDArray []string 6 | MaxStringLength uint32 7 | MaxArrayLength uint32 8 | MaxByteStringLength uint32 9 | MaxBrowseContinuationPoints uint16 10 | MaxHistoryContinuationPoints uint16 11 | MaxQueryContinuationPoints uint16 12 | MinSupportedSampleRate float64 13 | ServerProfileArray []string 14 | OperationLimits *OperationLimits 15 | } 16 | 17 | // NewServerCapabilities returns a ServerCapabilities structure with default values. 18 | func NewServerCapabilities() *ServerCapabilities { 19 | return &ServerCapabilities{ 20 | LocaleIDArray: []string{"en"}, 21 | MaxStringLength: 4096, 22 | MaxArrayLength: 4096, 23 | MaxByteStringLength: 4096, 24 | MaxBrowseContinuationPoints: 10, 25 | MaxHistoryContinuationPoints: 100, 26 | MaxQueryContinuationPoints: 0, 27 | MinSupportedSampleRate: 100, 28 | ServerProfileArray: []string{"http://opcfoundation.org/UA-Profile/Server/StandardUA2017", "http://opcfoundation.org/UAProfile/Server/Methods"}, 29 | OperationLimits: NewOperationLimits(), 30 | } 31 | } 32 | 33 | // OperationLimits contains the server's operation limits. 34 | type OperationLimits struct { 35 | MaxNodesPerRead uint32 36 | MaxNodesPerHistoryReadData uint32 37 | MaxNodesPerHistoryReadEvents uint32 38 | MaxNodesPerWrite uint32 39 | MaxNodesPerHistoryUpdateData uint32 40 | MaxNodesPerHistoryUpdateEvents uint32 41 | MaxNodesPerMethodCall uint32 42 | MaxNodesPerBrowse uint32 43 | MaxNodesPerRegisterNodes uint32 44 | MaxNodesPerTranslateBrowsePathsToNodeIds uint32 45 | MaxNodesPerNodeManagement uint32 46 | MaxMonitoredItemsPerCall uint32 47 | } 48 | 49 | // NewOperationLimits returns a OperationLimits structure with default values. 50 | func NewOperationLimits() *OperationLimits { 51 | return &OperationLimits{ 52 | MaxNodesPerRead: 1000, 53 | MaxNodesPerHistoryReadData: 1000, 54 | MaxNodesPerHistoryReadEvents: 1000, 55 | MaxNodesPerWrite: 1000, 56 | MaxNodesPerHistoryUpdateData: 1000, 57 | MaxNodesPerHistoryUpdateEvents: 1000, 58 | MaxNodesPerMethodCall: 1000, 59 | MaxNodesPerBrowse: 1000, 60 | MaxNodesPerRegisterNodes: 1000, 61 | MaxNodesPerTranslateBrowsePathsToNodeIds: 1000, 62 | MaxNodesPerNodeManagement: 1000, 63 | MaxMonitoredItemsPerCall: 1000, 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /ua/service_operation.go: -------------------------------------------------------------------------------- 1 | // Copyright 2021 Converter Systems LLC. All rights reserved. 2 | 3 | package ua 4 | 5 | // ServiceOperation holds a request and response channel. 6 | type ServiceOperation struct { 7 | request ServiceRequest 8 | responseCh chan ServiceResponse 9 | } 10 | 11 | // NewServiceOperation constructs a new ServiceOperation 12 | func NewServiceOperation(request ServiceRequest, responseCh chan ServiceResponse) *ServiceOperation { 13 | return &ServiceOperation{request, responseCh} 14 | } 15 | 16 | // Request returns the request that started the operation. 17 | func (o *ServiceOperation) Request() ServiceRequest { 18 | return o.request 19 | } 20 | 21 | // ResponseCh returns a channel that produces the response. 22 | func (o *ServiceOperation) ResponseCh() chan ServiceResponse { 23 | return o.responseCh 24 | } 25 | -------------------------------------------------------------------------------- /ua/service_request.go: -------------------------------------------------------------------------------- 1 | // Copyright 2021 Converter Systems LLC. All rights reserved. 2 | 3 | package ua 4 | 5 | // ServiceRequest is a request for a service. 6 | type ServiceRequest interface { 7 | Header() *RequestHeader 8 | } 9 | 10 | // Header returns the request header. 11 | func (h *RequestHeader) Header() *RequestHeader { 12 | return h 13 | } 14 | 15 | // ServiceResponse is a response from a service. 16 | type ServiceResponse interface { 17 | Header() *ResponseHeader 18 | } 19 | 20 | // Header returns the response header. 21 | func (h *ResponseHeader) Header() *ResponseHeader { 22 | return h 23 | } 24 | 25 | // ResponseWriter is used to write a response to the client. 26 | type ResponseWriter interface { 27 | // Write a response to the client. 28 | Write(res ServiceResponse) error 29 | } 30 | -------------------------------------------------------------------------------- /ua/status_code.go: -------------------------------------------------------------------------------- 1 | // Copyright 2021 Converter Systems LLC. All rights reserved. 2 | 3 | package ua 4 | 5 | // StatusCode is the result of the service call. 6 | type StatusCode uint32 7 | 8 | // IsGood returns true if the StatusCode is good. 9 | func (c StatusCode) IsGood() bool { 10 | return (uint32(c) & SeverityMask) == SeverityGood 11 | } 12 | 13 | // IsBad returns true if the StatusCode is bad. 14 | func (c StatusCode) IsBad() bool { 15 | return (uint32(c) & SeverityMask) == SeverityBad 16 | } 17 | 18 | // IsUncertain returns true if the StatusCode is uncertain. 19 | func (c StatusCode) IsUncertain() bool { 20 | return (uint32(c) & SeverityMask) == SeverityUncertain 21 | } 22 | 23 | // IsStructureChanged returns true if the structure is changed. 24 | func (c StatusCode) IsStructureChanged() bool { 25 | return (uint32(c) & StructureChanged) == StructureChanged 26 | } 27 | 28 | // IsSemanticsChanged returns true if the semantics is changed. 29 | func (c StatusCode) IsSemanticsChanged() bool { 30 | return (uint32(c) & SemanticsChanged) == SemanticsChanged 31 | } 32 | 33 | // IsOverflow returns true if the data value has exceeded the limits of the data type. 34 | func (c StatusCode) IsOverflow() bool { 35 | return ((uint32(c) & InfoTypeMask) == InfoTypeDataValue) && ((uint32(c) & Overflow) == Overflow) 36 | } 37 | 38 | const ( 39 | // Good - The operation completed successfully. 40 | Good StatusCode = 0x00000000 41 | // SeverityMask - . 42 | SeverityMask uint32 = 0xC0000000 43 | // SeverityGood - . 44 | SeverityGood uint32 = 0x00000000 45 | // SeverityUncertain - . 46 | SeverityUncertain uint32 = 0x40000000 47 | // SeverityBad - . 48 | SeverityBad uint32 = 0x80000000 49 | // SubCodeMask - . 50 | SubCodeMask uint32 = 0x0FFF0000 51 | // StructureChanged - . 52 | StructureChanged uint32 = 0x00008000 53 | // SemanticsChanged - . 54 | SemanticsChanged uint32 = 0x00004000 55 | // InfoTypeMask - . 56 | InfoTypeMask uint32 = 0x00000C00 57 | // InfoTypeDataValue - . 58 | InfoTypeDataValue uint32 = 0x00000400 59 | // InfoBitsMask - . 60 | InfoBitsMask uint32 = 0x000003FF 61 | // LimitBitsMask - . 62 | LimitBitsMask uint32 = 0x00000300 63 | // LimitBitsNone - . 64 | LimitBitsNone uint32 = 0x00000000 65 | // LimitBitsLow - . 66 | LimitBitsLow uint32 = 0x00000100 67 | // LimitBitsHigh - . 68 | LimitBitsHigh uint32 = 0x00000200 69 | // LimitBitsConstant - . 70 | LimitBitsConstant uint32 = 0x00000300 71 | // Overflow - . 72 | Overflow uint32 = 0x00000080 73 | // HistorianBitsMask - the mask of bits that pertain to the Historian. 74 | HistorianBitsMask uint32 = 0x0000001F 75 | // HistorianBitsCalculated - A data value which was calculated. 76 | HistorianBitsCalculated uint32 = 0x00000001 77 | // HistorianBitsInterpolated - A data value which was interpolated. 78 | HistorianBitsInterpolated uint32 = 0x00000010 79 | // HistorianBitsPartial - A data value which was calculated with an incomplete interval. 80 | HistorianBitsPartial uint32 = 0x00000100 81 | ) 82 | -------------------------------------------------------------------------------- /ua/transport_profile_uris.go: -------------------------------------------------------------------------------- 1 | // Copyright 2021 Converter Systems LLC. All rights reserved. 2 | 3 | package ua 4 | 5 | // TransportProfileURIs 6 | const ( 7 | TransportProfileURIUaTcpTransport = "http://opcfoundation.org/UA-Profile/Transport/uatcp-uasc-uabinary" 8 | TransportProfileURIHttpsXmlOrBinaryTransport = "http://opcfoundation.org/UA-Profile/Transport/https-uasoapxml-uabinary" 9 | TransportProfileURIHttpsXmlTransport = "http://opcfoundation.org/UA-Profile/Transport/https-uasoapxml" 10 | TransportProfileURIHttpsBinaryTransport = "http://opcfoundation.org/UA-Profile/Transport/https-uabinary" 11 | ) 12 | -------------------------------------------------------------------------------- /ua/ua_node_set.go: -------------------------------------------------------------------------------- 1 | package ua 2 | 3 | import ( 4 | "encoding/xml" 5 | "time" 6 | ) 7 | 8 | // UANodeSet supports reading UANodeSet from xml. 9 | type UANodeSet struct { 10 | NamespaceUris []string `xml:"NamespaceUris>Uri,omitempty"` 11 | ServerUris []string `xml:"ServerUris>Uri,omitempty"` 12 | Aliases []Alias `xml:"Aliases>Alias,omitempty"` 13 | Models []any `xml:"Models>Model,omitempty"` 14 | Extensions []any `xml:"Extensions>Extension,omitempty"` 15 | Nodes []UANode `xml:",any,omitempty"` 16 | LastModified time.Time `xml:"LastModified,attr"` 17 | } 18 | 19 | // UANode supports reading UANodeSet from xml. 20 | type UANode struct { 21 | XMLName xml.Name 22 | DisplayName UALocalizedText `xml:"DisplayName"` 23 | Description UALocalizedText `xml:"Description"` 24 | References []*UAReference `xml:"References>Reference,omitempty"` 25 | Extensions []any `xml:"Extensions>Extension,omitempty"` 26 | NodeID string `xml:"NodeId,attr"` 27 | BrowseName string `xml:"BrowseName,attr"` 28 | WriteMask uint32 `xml:"WriteMask,attr"` 29 | UserWriteMask uint32 `xml:"UserWriteMask,attr"` 30 | // UAType 31 | IsAbstract bool `xml:"IsAbstract,attr"` 32 | // UAObjectType 33 | // UAVariableType 34 | DataType string `xml:"DataType,attr"` 35 | ValueRank string `xml:"ValueRank,attr"` 36 | ArrayDimensions string `xml:"ArrayDimensions,attr"` 37 | // UADataType 38 | Definition *UADataTypeDefinition `xml:"Definition"` 39 | // UAReferenceType 40 | InverseName string `xml:"InverseName"` 41 | Symmetric bool `xml:"Symmetric,attr"` 42 | // UAObject 43 | EventNotifier uint8 `xml:"EventNotifier,attr"` 44 | // UAVariable 45 | Value UAVariant `xml:"Value"` 46 | AccessLevel string `xml:"AccessLevel,attr"` 47 | UserAccessLevel string `xml:"UserAccessLevel,attr"` 48 | MinimumSamplingInterval float64 `xml:"MinimumSamplingInterval,attr"` 49 | Historizing bool `xml:"Historizing,attr"` 50 | // UAMethod 51 | Executable string `xml:"Executable,attr"` 52 | UserExecutable string `xml:"UserExecutable,attr"` 53 | // UAView 54 | ContainsNoLoops bool `xml:"ContainsNoLoops,attr"` 55 | } 56 | 57 | // Alias supports reading UANodeSet from xml. 58 | type Alias struct { 59 | Alias string `xml:"Alias,attr"` 60 | NodeID string `xml:",innerxml"` 61 | } 62 | 63 | // UAReference supports reading UANodeSet from xml. 64 | type UAReference struct { 65 | ReferenceType string `xml:"ReferenceType,attr"` 66 | IsForward string `xml:"IsForward,attr"` 67 | TargetNodeID string `xml:",innerxml"` 68 | } 69 | 70 | // UADataTypeDefinition supports reading UANodeSet from xml. 71 | type UADataTypeDefinition struct { 72 | Field []UADataTypeField 73 | Name string `xml:"Name,attr"` 74 | BaseType string `xml:"BaseType,attr"` 75 | IsUnion bool `xml:"IsUnion,attr"` 76 | } 77 | 78 | // UADataTypeField supports reading UANodeSet from xml. 79 | type UADataTypeField struct { 80 | Description string `xml:"Description"` 81 | Definition UADataTypeDefinition `xml:"Definition"` 82 | Name string `xml:"Name,attr"` 83 | DataType string `xml:"DataType,attr"` 84 | ValueRank int `xml:"ValueRank,attr"` 85 | Value int `xml:"Value,attr"` 86 | IsOptional bool `xml:"IsOptional,attr"` 87 | } 88 | 89 | // ListOfBoolean supports reading UANodeSet from xml. 90 | type ListOfBoolean struct { 91 | List []bool `xml:"Boolean,omitempty"` 92 | } 93 | 94 | // ListOfSByte supports reading UANodeSet from xml. 95 | type ListOfSByte struct { 96 | List []int8 `xml:"SByte,omitempty"` 97 | } 98 | 99 | // ListOfByte supports reading UANodeSet from xml. 100 | type ListOfByte struct { 101 | List []uint16 `xml:"Byte,omitempty"` //bugfix: xml.Encoding can't directly decode into []byte. 102 | } 103 | 104 | // ListOfInt16 supports reading UANodeSet from xml. 105 | type ListOfInt16 struct { 106 | List []int16 `xml:"Int16,omitempty"` 107 | } 108 | 109 | // ListOfUInt16 supports reading UANodeSet from xml. 110 | type ListOfUInt16 struct { 111 | List []uint16 `xml:"UInt16,omitempty"` 112 | } 113 | 114 | // ListOfInt32 supports reading UANodeSet from xml. 115 | type ListOfInt32 struct { 116 | List []int32 `xml:"Int32,omitempty"` 117 | } 118 | 119 | // ListOfUInt32 supports reading UANodeSet from xml. 120 | type ListOfUInt32 struct { 121 | List []uint32 `xml:"UInt32,omitempty"` 122 | } 123 | 124 | // ListOfInt64 supports reading UANodeSet from xml. 125 | type ListOfInt64 struct { 126 | List []int64 `xml:"Int64,omitempty"` 127 | } 128 | 129 | // ListOfUInt64 supports reading UANodeSet from xml. 130 | type ListOfUInt64 struct { 131 | List []uint64 `xml:"UInt64,omitempty"` 132 | } 133 | 134 | // ListOfFloat supports reading UANodeSet from xml. 135 | type ListOfFloat struct { 136 | List []float32 `xml:"Float,omitempty"` 137 | } 138 | 139 | // ListOfDouble supports reading UANodeSet from xml. 140 | type ListOfDouble struct { 141 | List []float64 `xml:"Double,omitempty"` 142 | } 143 | 144 | // ListOfString supports reading UANodeSet from xml. 145 | type ListOfString struct { 146 | List []string `xml:"String,omitempty"` 147 | } 148 | 149 | // ListOfDateTime supports reading UANodeSet from xml. 150 | type ListOfDateTime struct { 151 | List []time.Time `xml:"DateTime,omitempty"` 152 | } 153 | 154 | // ListOfByteString supports reading UANodeSet from xml. 155 | type ListOfByteString struct { 156 | List []ByteString `xml:"ByteString,omitempty"` 157 | } 158 | 159 | // UAXMLElement supports reading UANodeSet from xml. 160 | type UAXMLElement struct { 161 | InnerXML string `xml:",innerxml"` 162 | } 163 | 164 | // ListOfXMLElement supports reading UANodeSet from xml. 165 | type ListOfXMLElement struct { 166 | List []*UAXMLElement `xml:"XmlElement"` 167 | } 168 | 169 | // UAGUID supports reading UANodeSet from xml. 170 | type UAGUID struct { 171 | String string `xml:"String"` 172 | } 173 | 174 | // ListOfGUID supports reading UANodeSet from xml. 175 | type ListOfGUID struct { 176 | List []*string `xml:"Guid>String,omitempty"` 177 | } 178 | 179 | // UALocalizedText supports reading UANodeSet from xml. 180 | type UALocalizedText struct { 181 | Text string `xml:"Text"` 182 | Locale string `xml:"Locale"` 183 | Content string `xml:",innerxml"` 184 | } 185 | 186 | // ListOfLocalizedText supports reading UANodeSet from xml. 187 | type ListOfLocalizedText struct { 188 | List []UALocalizedText `xml:"LocalizedText,omitempty"` 189 | } 190 | 191 | // UAQualifiedName supports reading UANodeSet from xml. 192 | type UAQualifiedName struct { 193 | NamespaceIndex uint16 `xml:"NamespaceIndex"` 194 | Name string `xml:"Name"` 195 | } 196 | 197 | // ListOfQualifiedName supports reading UANodeSet from xml. 198 | type ListOfQualifiedName struct { 199 | List []UAQualifiedName `xml:"QualifiedName,omitempty"` 200 | } 201 | 202 | // UAArgument supports reading UANodeSet from xml. 203 | type UAArgument struct { 204 | Name string `xml:"Name"` 205 | DataType string `xml:"DataType>Identifier"` 206 | ValueRank string `xml:"ValueRank"` 207 | ArrayDimensions string `xml:"ArrayDimensions"` 208 | Description UALocalizedText `xml:"Description"` 209 | } 210 | 211 | // UAEUInformation supports reading UANodeSet from xml. 212 | type UAEUInformation struct { 213 | NamespaceURI string `xml:"NamespaceUri"` 214 | UnitID int32 `xml:"UnitId"` 215 | DisplayName UALocalizedText `xml:"DisplayName"` 216 | Description UALocalizedText `xml:"Description"` 217 | } 218 | 219 | // UARange supports reading UANodeSet from xml. 220 | type UARange struct { 221 | Low float64 `xml:"Low"` 222 | High float64 `xml:"High"` 223 | } 224 | 225 | // UAEnumValueType supports reading UANodeSet from xml. 226 | type UAEnumValueType struct { 227 | Value int64 `xml:"Value"` 228 | DisplayName UALocalizedText `xml:"DisplayName"` 229 | Description UALocalizedText `xml:"Description"` 230 | } 231 | 232 | // UAExtensionObject supports reading UANodeSet from xml. 233 | type UAExtensionObject struct { 234 | TypeID string `xml:"TypeId>Identifier"` 235 | Argument *UAArgument `xml:"Body>Argument"` 236 | EUInformation *UAEUInformation `xml:"Body>EUInformation"` 237 | Range *UARange `xml:"Body>Range"` 238 | EnumValueType *UAEnumValueType `xml:"Body>EnumValueType"` 239 | } 240 | 241 | // ListOfExtensionObject supports reading UANodeSet from xml. 242 | type ListOfExtensionObject struct { 243 | List []UAExtensionObject `xml:"ExtensionObject,omitempty"` 244 | } 245 | 246 | // UANodeID supports reading UANodeSet from xml. 247 | type UANodeID struct { 248 | Identifier string `xml:"Identifier"` 249 | } 250 | 251 | // UAExpandedNodeID supports reading UANodeSet from xml. 252 | type UAExpandedNodeID struct { 253 | Identifier string `xml:"Identifier"` 254 | } 255 | 256 | // UAVariant supports reading UANodeSet from xml. 257 | type UAVariant struct { 258 | XMLName xml.Name 259 | Bool *bool `xml:"Boolean"` 260 | Byte *uint8 `xml:"Byte"` 261 | UInt16 *uint16 `xml:"UInt16"` 262 | UInt32 *uint32 `xml:"UInt32"` 263 | UInt64 *uint64 `xml:"UInt64"` 264 | SByte *int8 `xml:"SByte"` 265 | Int16 *int16 `xml:"Int16"` 266 | Int32 *int32 `xml:"Int32"` 267 | Int64 *int64 `xml:"Int64"` 268 | Float *float32 `xml:"Float"` 269 | Double *float64 `xml:"Double"` 270 | String *string `xml:"String"` 271 | ByteString *ByteString `xml:"ByteString"` 272 | XMLElement *UAXMLElement `xml:"XmlElement"` 273 | DateTime *time.Time `xml:"DateTime"` 274 | GUID *UAGUID `xml:"Guid"` 275 | LocalizedText *UALocalizedText `xml:"LocalizedText"` 276 | QualifiedName *UAQualifiedName `xml:"QualifiedName"` 277 | ExtensionObject *UAExtensionObject `xml:"ExtensionObject"` 278 | NodeID *UANodeID `xml:"NodeId"` 279 | ExpandedNodeID *UAExpandedNodeID `xml:"ExpandedNodeId"` 280 | 281 | ListOfBoolean *ListOfBoolean `xml:"ListOfBoolean"` 282 | ListOfByte *ListOfByte `xml:"ListOfByte"` 283 | ListOfUInt16 *ListOfUInt16 `xml:"ListOfUInt16"` 284 | ListOfUInt32 *ListOfUInt32 `xml:"ListOfUInt32"` 285 | ListOfUInt64 *ListOfUInt64 `xml:"ListOfUInt64"` 286 | ListOfSByte *ListOfSByte `xml:"ListOfSByte"` 287 | ListOfInt16 *ListOfInt16 `xml:"ListOfInt16"` 288 | ListOfInt32 *ListOfInt32 `xml:"ListOfInt32"` 289 | ListOfInt64 *ListOfInt64 `xml:"ListOfInt64"` 290 | ListOfFloat *ListOfFloat `xml:"ListOfFloat"` 291 | ListOfDouble *ListOfDouble `xml:"ListOfDouble"` 292 | ListOfString *ListOfString `xml:"ListOfString"` 293 | ListOfByteString *ListOfByteString `xml:"ListOfByteString"` 294 | ListOfXMLElement *ListOfXMLElement `xml:"ListOfXmlElement"` 295 | ListOfDateTime *ListOfDateTime `xml:"ListOfDateTime"` 296 | ListOfGUID *ListOfGUID `xml:"ListOfGuid"` 297 | ListOfLocalizedText *ListOfLocalizedText `xml:"ListOfLocalizedText"` 298 | ListOfQualifiedName *ListOfQualifiedName `xml:"ListOfQualifiedName"` 299 | ListOfExtensionObject *ListOfExtensionObject `xml:"ListOfExtensionObject"` 300 | ListOfVariant *ListOfVariant `xml:"ListOfVariant"` 301 | } 302 | 303 | // UAVariant2 supports reading UANodeSet from xml. 304 | type UAVariant2 struct { 305 | XMLName xml.Name 306 | InnerXML string `xml:",innerxml"` 307 | } 308 | 309 | // ListOfVariant supports reading UANodeSet from xml. 310 | type ListOfVariant struct { 311 | List []UAVariant2 `xml:",any,omitempty"` 312 | } 313 | -------------------------------------------------------------------------------- /ua/user_identity.go: -------------------------------------------------------------------------------- 1 | // Copyright 2021 Converter Systems LLC. All rights reserved. 2 | 3 | package ua 4 | 5 | import ( 6 | "crypto/rsa" 7 | ) 8 | 9 | // AnonymousIdentity provides no identity to server when activating a session. 10 | type AnonymousIdentity struct { 11 | } 12 | 13 | // UserNameIdentity provides userName and password to server when activating a session. 14 | type UserNameIdentity struct { 15 | UserName string 16 | Password string 17 | } 18 | 19 | // X509Identity provides x509 certificate to server when activating a session. 20 | type X509Identity struct { 21 | Certificate ByteString 22 | Key *rsa.PrivateKey 23 | } 24 | 25 | // IssuedIdentity provides issued token data to server when activating a session. 26 | type IssuedIdentity struct { 27 | TokenData ByteString 28 | } 29 | -------------------------------------------------------------------------------- /ua/value_rank.go: -------------------------------------------------------------------------------- 1 | // Copyright 2021 Converter Systems LLC. All rights reserved. 2 | 3 | package ua 4 | 5 | // ValueRank identifies the rank of value that may be stored in the Value attribute. 6 | const ( 7 | ValueRankThreeDimensions = int32(3) 8 | ValueRankTwoDimensions = int32(2) 9 | ValueRankOneDimension = int32(1) 10 | ValueRankOneOrMoreDimensions = int32(0) 11 | ValueRankScalar = int32(-1) 12 | ValueRankAny = int32(-2) 13 | ValueRankScalarOrOneDimension = int32(-3) 14 | ) 15 | -------------------------------------------------------------------------------- /ua/variant.go: -------------------------------------------------------------------------------- 1 | // Copyright 2021 Converter Systems LLC. All rights reserved. 2 | 3 | package ua 4 | 5 | // VariantTypes 6 | const ( 7 | VariantTypeNull byte = iota 8 | VariantTypeBoolean 9 | VariantTypeSByte 10 | VariantTypeByte 11 | VariantTypeInt16 12 | VariantTypeUInt16 13 | VariantTypeInt32 14 | VariantTypeUInt32 15 | VariantTypeInt64 16 | VariantTypeUInt64 17 | VariantTypeFloat 18 | VariantTypeDouble 19 | VariantTypeString 20 | VariantTypeDateTime 21 | VariantTypeGUID 22 | VariantTypeByteString 23 | VariantTypeXMLElement 24 | VariantTypeNodeID 25 | VariantTypeExpandedNodeID 26 | VariantTypeStatusCode 27 | VariantTypeQualifiedName 28 | VariantTypeLocalizedText 29 | VariantTypeExtensionObject 30 | VariantTypeDataValue 31 | VariantTypeVariant 32 | VariantTypeDiagnosticInfo 33 | 34 | // VariantTypeMultiDimensionArray flags whether the array has more than one dimension 35 | VariantTypeMultiDimensionArray = 0x40 36 | 37 | // VariantTypeArray flags whether the value is an array. 38 | VariantTypeArray = 0x80 39 | ) 40 | 41 | /* 42 | Variant stores a single value or slice of the following types: 43 | 44 | bool, int8, uint8, int16, uint16, int32, uint32 45 | int64, uint64, float32, float64, string 46 | time.Time, uuid.UUID, ByteString, XmlElement 47 | NodeId, ExpandedNodeId, StatusCode, QualifiedName 48 | LocalizedText, DataValue, Variant 49 | 50 | In addition, you may store any type that is registered with the BinaryEncoder. 51 | These types will be encoded as an ExtensionObject by the BinaryEncoder. 52 | */ 53 | type Variant any 54 | -------------------------------------------------------------------------------- /ua/xmlelement.go: -------------------------------------------------------------------------------- 1 | // Copyright 2021 Converter Systems LLC. All rights reserved. 2 | 3 | package ua 4 | 5 | import ( 6 | "regexp" 7 | ) 8 | 9 | var ( 10 | validXML = regexp.MustCompile(`[^\x09\x0A\x0D\x20-\xD7FF\xE000-\xFFFD\x10000-x10FFFF]+`) 11 | ) 12 | 13 | // XMLElement is stored as string 14 | type XMLElement string 15 | 16 | // String returns element as a string. 17 | func (e XMLElement) String() string { 18 | return validXML.ReplaceAllString(string(e), "") 19 | } 20 | --------------------------------------------------------------------------------