mirror of
https://github.com/mainflux/mainflux.git
synced 2025-05-12 19:29:30 +08:00

* NOISSUE- Add OPC-UA adapter Signed-off-by: Manuel Imperiale <manuel.imperiale@gmail.com> * NOISSUE - Add opc-adapter PoC, docker and vendor Signed-off-by: Manuel Imperiale <manuel.imperiale@gmail.com> * Convert OPC messages to SenML Signed-off-by: Manuel Imperiale <manuel.imperiale@gmail.com> * Add gopcua package Signed-off-by: Manuel Imperiale <manuel.imperiale@gmail.com> * lora-adapter typo Signed-off-by: Manuel Imperiale <manuel.imperiale@gmail.com> * Add OPC Reader Signed-off-by: Manuel Imperiale <manuel.imperiale@gmail.com> * Typo fix Signed-off-by: Manuel Imperiale <manuel.imperiale@gmail.com> * Typo fix Signed-off-by: Manuel Imperiale <manuel.imperiale@gmail.com> * Update copyright headers Signed-off-by: Manuel Imperiale <manuel.imperiale@gmail.com> * Fix reviews Signed-off-by: Manuel Imperiale <manuel.imperiale@gmail.com> * Fix reviews Signed-off-by: Manuel Imperiale <manuel.imperiale@gmail.com> * Add opc config Signed-off-by: Manuel Imperiale <manuel.imperiale@gmail.com> * Add all opc envars in the config Signed-off-by: Manuel Imperiale <manuel.imperiale@gmail.com> * Config typo Signed-off-by: Manuel Imperiale <manuel.imperiale@gmail.com> * Add route map Signed-off-by: Manuel Imperiale <manuel.imperiale@gmail.com> * Use opcua package instead of opc Signed-off-by: Manuel Imperiale <manuel.imperiale@gmail.com> * Fix OPCUA typo Signed-off-by: Manuel Imperiale <manuel.imperiale@gmail.com> * Rm MQTT sub Signed-off-by: Manuel Imperiale <manuel.imperiale@gmail.com> * Move interefaces to root Signed-off-by: Manuel Imperiale <manuel.imperiale@gmail.com> * Fix revieews and typo Signed-off-by: Manuel Imperiale <manuel.imperiale@gmail.com> * Update Gopkg.toml Signed-off-by: Manuel Imperiale <manuel.imperiale@gmail.com> * Add all envars into .env Signed-off-by: Manuel Imperiale <manuel.imperiale@gmail.com>
412 lines
12 KiB
Go
412 lines
12 KiB
Go
// Copyright 2018-2019 opcua authors. All rights reserved.
|
|
// Use of this source code is governed by a MIT-style license that can be
|
|
// found in the LICENSE file.
|
|
|
|
package opcua
|
|
|
|
import (
|
|
"crypto/rsa"
|
|
"crypto/x509"
|
|
"encoding/pem"
|
|
"io/ioutil"
|
|
"log"
|
|
"math/rand"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/gopcua/opcua/errors"
|
|
"github.com/gopcua/opcua/ua"
|
|
"github.com/gopcua/opcua/uapolicy"
|
|
"github.com/gopcua/opcua/uasc"
|
|
)
|
|
|
|
// DefaultClientConfig returns the default configuration for a client
|
|
// to establish a secure channel.
|
|
func DefaultClientConfig() *uasc.Config {
|
|
return &uasc.Config{
|
|
SecurityPolicyURI: ua.SecurityPolicyURINone,
|
|
SecurityMode: ua.MessageSecurityModeNone,
|
|
Lifetime: uint32(time.Hour / time.Millisecond),
|
|
RequestTimeout: 10 * time.Second,
|
|
}
|
|
}
|
|
|
|
// DefaultSessionConfig returns the default configuration for a client
|
|
// to establish a session.
|
|
func DefaultSessionConfig() *uasc.SessionConfig {
|
|
return &uasc.SessionConfig{
|
|
SessionTimeout: 20 * time.Minute,
|
|
ClientDescription: &ua.ApplicationDescription{
|
|
ApplicationURI: "urn:gopcua:client",
|
|
ProductURI: "urn:gopcua",
|
|
ApplicationName: &ua.LocalizedText{Text: "gopcua - OPC UA implementation in Go"},
|
|
ApplicationType: ua.ApplicationTypeClient,
|
|
},
|
|
LocaleIDs: []string{"en-us"},
|
|
UserTokenSignature: &ua.SignatureData{},
|
|
}
|
|
}
|
|
|
|
// ApplyConfig applies the config options to the default configuration.
|
|
// todo(fs): Can we find a better name?
|
|
func ApplyConfig(opts ...Option) (*uasc.Config, *uasc.SessionConfig) {
|
|
c := DefaultClientConfig()
|
|
sc := DefaultSessionConfig()
|
|
for _, opt := range opts {
|
|
opt(c, sc)
|
|
}
|
|
return c, sc
|
|
}
|
|
|
|
// Option is an option function type to modify the configuration.
|
|
type Option func(*uasc.Config, *uasc.SessionConfig)
|
|
|
|
// ApplicationName sets the application name in the session configuration.
|
|
func ApplicationName(s string) Option {
|
|
return func(c *uasc.Config, sc *uasc.SessionConfig) {
|
|
sc.ClientDescription.ApplicationName = &ua.LocalizedText{Text: s}
|
|
}
|
|
}
|
|
|
|
// ApplicationURI sets the application uri in the session configuration.
|
|
func ApplicationURI(s string) Option {
|
|
return func(c *uasc.Config, sc *uasc.SessionConfig) {
|
|
sc.ClientDescription.ApplicationURI = s
|
|
}
|
|
}
|
|
|
|
// Lifetime sets the lifetime of the secure channel in milliseconds.
|
|
func Lifetime(d time.Duration) Option {
|
|
return func(c *uasc.Config, sc *uasc.SessionConfig) {
|
|
c.Lifetime = uint32(d / time.Millisecond)
|
|
}
|
|
}
|
|
|
|
// Locales sets the locales in the session configuration.
|
|
func Locales(locale ...string) Option {
|
|
return func(c *uasc.Config, sc *uasc.SessionConfig) {
|
|
sc.LocaleIDs = locale
|
|
}
|
|
}
|
|
|
|
// ProductURI sets the product uri in the session configuration.
|
|
func ProductURI(s string) Option {
|
|
return func(c *uasc.Config, sc *uasc.SessionConfig) {
|
|
sc.ClientDescription.ProductURI = s
|
|
}
|
|
}
|
|
|
|
// RandomRequestID assigns a random initial request id.
|
|
func RandomRequestID() Option {
|
|
return func(c *uasc.Config, sc *uasc.SessionConfig) {
|
|
c.RequestID = uint32(rand.Int31())
|
|
}
|
|
}
|
|
|
|
// RemoteCertificate sets the server certificate.
|
|
func RemoteCertificate(cert []byte) Option {
|
|
return func(c *uasc.Config, sc *uasc.SessionConfig) {
|
|
c.RemoteCertificate = cert
|
|
}
|
|
}
|
|
|
|
// RemoteCertificateFile sets the server certificate from the file
|
|
// in PEM or DER encoding.
|
|
func RemoteCertificateFile(filename string) Option {
|
|
return func(c *uasc.Config, sc *uasc.SessionConfig) {
|
|
cert, err := loadCertificate(filename)
|
|
if err != nil {
|
|
log.Fatal(err)
|
|
}
|
|
c.RemoteCertificate = cert
|
|
}
|
|
}
|
|
|
|
// SecurityMode sets the security mode for the secure channel.
|
|
func SecurityMode(m ua.MessageSecurityMode) Option {
|
|
return func(c *uasc.Config, sc *uasc.SessionConfig) {
|
|
c.SecurityMode = m
|
|
}
|
|
}
|
|
|
|
// SecurityModeString sets the security mode for the secure channel.
|
|
// Valid values are "None", "Sign", and "SignAndEncrypt".
|
|
func SecurityModeString(s string) Option {
|
|
return func(c *uasc.Config, sc *uasc.SessionConfig) {
|
|
c.SecurityMode = ua.MessageSecurityModeFromString(s)
|
|
}
|
|
}
|
|
|
|
// SecurityPolicy sets the security policy uri for the secure channel.
|
|
func SecurityPolicy(s string) Option {
|
|
return func(c *uasc.Config, sc *uasc.SessionConfig) {
|
|
c.SecurityPolicyURI = ua.FormatSecurityPolicyURI(s)
|
|
}
|
|
}
|
|
|
|
// SessionTimeout sets the timeout in the session configuration.
|
|
func SessionTimeout(d time.Duration) Option {
|
|
return func(c *uasc.Config, sc *uasc.SessionConfig) {
|
|
sc.SessionTimeout = d
|
|
}
|
|
}
|
|
|
|
// PrivateKey sets the RSA private key in the secure channel configuration.
|
|
func PrivateKey(key *rsa.PrivateKey) Option {
|
|
return func(c *uasc.Config, sc *uasc.SessionConfig) {
|
|
c.LocalKey = key
|
|
}
|
|
}
|
|
|
|
// PrivateKeyFile sets the RSA private key in the secure channel configuration
|
|
// from a PEM or DER encoded file.
|
|
func PrivateKeyFile(filename string) Option {
|
|
return func(c *uasc.Config, sc *uasc.SessionConfig) {
|
|
if filename == "" {
|
|
return
|
|
}
|
|
key, err := loadPrivateKey(filename)
|
|
if err != nil {
|
|
log.Fatal(err)
|
|
}
|
|
c.LocalKey = key
|
|
}
|
|
}
|
|
|
|
func loadPrivateKey(filename string) (*rsa.PrivateKey, error) {
|
|
b, err := ioutil.ReadFile(filename)
|
|
if err != nil {
|
|
return nil, errors.Errorf("Failed to load private key: %s", err)
|
|
}
|
|
|
|
derBytes := b
|
|
if strings.HasSuffix(filename, ".pem") {
|
|
block, _ := pem.Decode(b)
|
|
if block == nil || block.Type != "RSA PRIVATE KEY" {
|
|
return nil, errors.Errorf("Failed to decode PEM block with private key")
|
|
}
|
|
derBytes = block.Bytes
|
|
}
|
|
|
|
pk, err := x509.ParsePKCS1PrivateKey(derBytes)
|
|
if err != nil {
|
|
return nil, errors.Errorf("Failed to parse private key: %s", err)
|
|
}
|
|
return pk, nil
|
|
}
|
|
|
|
// Certificate sets the client X509 certificate in the secure channel configuration.
|
|
// It also detects and sets the ApplicationURI from the URI within the certificate.
|
|
func Certificate(cert []byte) Option {
|
|
return func(c *uasc.Config, sc *uasc.SessionConfig) {
|
|
setCertificate(cert, c, sc)
|
|
}
|
|
}
|
|
|
|
// Certificate sets the client X509 certificate in the secure channel configuration
|
|
// from the PEM or DER encoded file. It also detects and sets the ApplicationURI
|
|
// from the URI within the certificate.
|
|
func CertificateFile(filename string) Option {
|
|
return func(c *uasc.Config, sc *uasc.SessionConfig) {
|
|
if filename == "" {
|
|
return
|
|
}
|
|
|
|
cert, err := loadCertificate(filename)
|
|
if err != nil {
|
|
log.Fatal(err)
|
|
}
|
|
setCertificate(cert, c, sc)
|
|
}
|
|
}
|
|
|
|
func loadCertificate(filename string) ([]byte, error) {
|
|
b, err := ioutil.ReadFile(filename)
|
|
if err != nil {
|
|
return nil, errors.Errorf("Failed to load certificate: %s", err)
|
|
}
|
|
|
|
if !strings.HasSuffix(filename, ".pem") {
|
|
return b, nil
|
|
}
|
|
|
|
block, _ := pem.Decode(b)
|
|
if block == nil || block.Type != "CERTIFICATE" {
|
|
return nil, errors.Errorf("Failed to decode PEM block with certificate")
|
|
}
|
|
return block.Bytes, nil
|
|
}
|
|
|
|
func setCertificate(cert []byte, c *uasc.Config, sc *uasc.SessionConfig) {
|
|
c.Certificate = cert
|
|
|
|
// Extract the application URI from the certificate.
|
|
x509cert, err := x509.ParseCertificate(cert)
|
|
if err != nil {
|
|
log.Fatalf("Failed to parse certificate: %s", err)
|
|
return
|
|
}
|
|
if len(x509cert.URIs) == 0 {
|
|
return
|
|
}
|
|
appURI := x509cert.URIs[0].String()
|
|
if appURI == "" {
|
|
return
|
|
}
|
|
sc.ClientDescription.ApplicationURI = appURI
|
|
}
|
|
|
|
// SecurityFromEndpoint sets the server-related security parameters from
|
|
// a chosen endpoint (received from GetEndpoints())
|
|
func SecurityFromEndpoint(ep *ua.EndpointDescription, authType ua.UserTokenType) Option {
|
|
return func(c *uasc.Config, sc *uasc.SessionConfig) {
|
|
c.SecurityPolicyURI = ep.SecurityPolicyURI
|
|
c.SecurityMode = ep.SecurityMode
|
|
c.RemoteCertificate = ep.ServerCertificate
|
|
c.Thumbprint = uapolicy.Thumbprint(ep.ServerCertificate)
|
|
|
|
for _, t := range ep.UserIdentityTokens {
|
|
if t.TokenType != authType {
|
|
continue
|
|
}
|
|
|
|
if sc.UserIdentityToken == nil {
|
|
switch authType {
|
|
case ua.UserTokenTypeAnonymous:
|
|
sc.UserIdentityToken = &ua.AnonymousIdentityToken{}
|
|
case ua.UserTokenTypeUserName:
|
|
sc.UserIdentityToken = &ua.UserNameIdentityToken{}
|
|
case ua.UserTokenTypeCertificate:
|
|
sc.UserIdentityToken = &ua.X509IdentityToken{}
|
|
case ua.UserTokenTypeIssuedToken:
|
|
sc.UserIdentityToken = &ua.IssuedIdentityToken{}
|
|
}
|
|
}
|
|
|
|
setPolicyID(sc.UserIdentityToken, t.PolicyID)
|
|
sc.AuthPolicyURI = t.SecurityPolicyURI
|
|
return
|
|
}
|
|
|
|
if sc.UserIdentityToken == nil {
|
|
sc.UserIdentityToken = &ua.AnonymousIdentityToken{PolicyID: defaultAnonymousPolicyID}
|
|
sc.AuthPolicyURI = ua.SecurityPolicyURINone
|
|
}
|
|
}
|
|
}
|
|
|
|
func setPolicyID(t interface{}, policy string) {
|
|
switch tok := t.(type) {
|
|
case *ua.AnonymousIdentityToken:
|
|
tok.PolicyID = policy
|
|
case *ua.UserNameIdentityToken:
|
|
tok.PolicyID = policy
|
|
case *ua.X509IdentityToken:
|
|
tok.PolicyID = policy
|
|
case *ua.IssuedIdentityToken:
|
|
tok.PolicyID = policy
|
|
}
|
|
}
|
|
|
|
// AuthPolicyID sets the policy ID of the user identity token
|
|
// Note: This should only be called if you know the exact policy ID the server is expecting.
|
|
// Most callers should use SecurityFromEndpoint as it automatically finds the policyID
|
|
// todo(fs): Should we make 'policy' an option to the other
|
|
// todo(fs): AuthXXX methods since this approach requires context
|
|
// todo(fs): and ordering?
|
|
func AuthPolicyID(policy string) Option {
|
|
return func(c *uasc.Config, sc *uasc.SessionConfig) {
|
|
if sc.UserIdentityToken == nil {
|
|
log.Printf("policy ID needs to be set after the policy type is chosen, no changes made. Call SecurityFromEndpoint() or an AuthXXX() option first")
|
|
return
|
|
}
|
|
setPolicyID(sc.UserIdentityToken, policy)
|
|
}
|
|
}
|
|
|
|
// AuthAnonymous sets the client's authentication X509 certificate
|
|
// Note: PolicyID still needs to be set outside of this method, typically through
|
|
// the SecurityFromEndpoint() Option
|
|
func AuthAnonymous() Option {
|
|
return func(c *uasc.Config, sc *uasc.SessionConfig) {
|
|
if sc.UserIdentityToken == nil {
|
|
sc.UserIdentityToken = &ua.AnonymousIdentityToken{}
|
|
}
|
|
|
|
_, ok := sc.UserIdentityToken.(*ua.AnonymousIdentityToken)
|
|
if !ok {
|
|
// todo(fs): should we Fatal here?
|
|
log.Printf("non-anonymous authentication already configured, ignoring")
|
|
return
|
|
}
|
|
}
|
|
}
|
|
|
|
// AuthUsername sets the client's authentication username and password
|
|
// Note: PolicyID still needs to be set outside of this method, typically through
|
|
// the SecurityFromEndpoint() Option
|
|
func AuthUsername(user, pass string) Option {
|
|
return func(c *uasc.Config, sc *uasc.SessionConfig) {
|
|
if sc.UserIdentityToken == nil {
|
|
sc.UserIdentityToken = &ua.UserNameIdentityToken{}
|
|
}
|
|
|
|
t, ok := sc.UserIdentityToken.(*ua.UserNameIdentityToken)
|
|
if !ok {
|
|
// todo(fs): should we Fatal here?
|
|
log.Printf("non-username authentication already configured, ignoring")
|
|
return
|
|
}
|
|
|
|
t.UserName = user
|
|
sc.AuthPassword = pass
|
|
}
|
|
}
|
|
|
|
// AuthCertificate sets the client's authentication X509 certificate
|
|
// Note: PolicyID still needs to be set outside of this method, typically through
|
|
// the SecurityFromEndpoint() Option
|
|
func AuthCertificate(cert []byte) Option {
|
|
return func(c *uasc.Config, sc *uasc.SessionConfig) {
|
|
if sc.UserIdentityToken == nil {
|
|
sc.UserIdentityToken = &ua.X509IdentityToken{}
|
|
}
|
|
|
|
t, ok := sc.UserIdentityToken.(*ua.X509IdentityToken)
|
|
if !ok {
|
|
// todo(fs): should we Fatal here?
|
|
log.Printf("non-certificate authentication already configured, ignoring")
|
|
return
|
|
}
|
|
|
|
t.CertificateData = cert
|
|
}
|
|
}
|
|
|
|
// AuthIssuedToken sets the client's authentication data based on an externally-issued token
|
|
// Note: PolicyID still needs to be set outside of this method, typically through
|
|
// the SecurityFromEndpoint() Option
|
|
func AuthIssuedToken(tokenData []byte) Option {
|
|
return func(c *uasc.Config, sc *uasc.SessionConfig) {
|
|
if sc.UserIdentityToken == nil {
|
|
sc.UserIdentityToken = &ua.IssuedIdentityToken{}
|
|
}
|
|
|
|
t, ok := sc.UserIdentityToken.(*ua.IssuedIdentityToken)
|
|
if !ok {
|
|
log.Printf("non-issued token authentication already configured, ignoring")
|
|
return
|
|
}
|
|
|
|
// todo(dw): not correct; need to read spec
|
|
t.TokenData = tokenData
|
|
}
|
|
}
|
|
|
|
// RequestTimeout sets the timeout for all requests over SecureChannel
|
|
func RequestTimeout(t time.Duration) Option {
|
|
return func(c *uasc.Config, sc *uasc.SessionConfig) {
|
|
c.RequestTimeout = t
|
|
}
|
|
}
|