forked from gopcua/opcua
-
Notifications
You must be signed in to change notification settings - Fork 0
/
config.go
574 lines (508 loc) · 15.4 KB
/
config.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
// Copyright 2018-2020 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"
"log"
"math/rand"
"net"
"os"
"strings"
"time"
"github.com/gopcua/opcua/errors"
"github.com/gopcua/opcua/ua"
"github.com/gopcua/opcua/uacp"
"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,
AutoReconnect: true,
ReconnectInterval: 5 * 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.NewLocalizedText("gopcua - OPC UA implementation in Go"),
ApplicationType: ua.ApplicationTypeClient,
},
LocaleIDs: []string{"en-us"},
UserTokenSignature: &ua.SignatureData{},
}
}
// Config contains all config options.
type Config struct {
dialer *uacp.Dialer
sechan *uasc.Config
session *uasc.SessionConfig
}
// NewDialer creates a uacp.Dialer from the config options
func NewDialer(cfg *Config) *uacp.Dialer {
if cfg.dialer == nil {
return &uacp.Dialer{}
}
return cfg.dialer
}
// ApplyConfig applies the config options to the default configuration.
// todo(fs): Can we find a better name?
func ApplyConfig(opts ...Option) (*Config, error) {
cfg := &Config{
sechan: DefaultClientConfig(),
session: DefaultSessionConfig(),
}
var errs []error
for _, opt := range opts {
if err := opt(cfg); err != nil {
errs = append(errs, err)
}
}
return cfg, errors.Join(errs...)
}
// Option is an option function type to modify the configuration.
type Option func(*Config) error
// ApplicationName sets the application name in the session configuration.
func ApplicationName(s string) Option {
return func(cfg *Config) error {
cfg.session.ClientDescription.ApplicationName = ua.NewLocalizedText(s)
return nil
}
}
// ApplicationURI sets the application uri in the session configuration.
func ApplicationURI(s string) Option {
return func(cfg *Config) error {
cfg.session.ClientDescription.ApplicationURI = s
return nil
}
}
// AutoReconnect sets the auto reconnect state of the secure channel.
func AutoReconnect(b bool) Option {
return func(cfg *Config) error {
cfg.sechan.AutoReconnect = b
return nil
}
}
// ReconnectInterval is interval duration between each reconnection attempt.
func ReconnectInterval(d time.Duration) Option {
return func(cfg *Config) error {
cfg.sechan.ReconnectInterval = d
return nil
}
}
// Lifetime sets the lifetime of the secure channel in milliseconds.
func Lifetime(d time.Duration) Option {
return func(cfg *Config) error {
cfg.sechan.Lifetime = uint32(d / time.Millisecond)
return nil
}
}
// Locales sets the locales in the session configuration.
func Locales(locale ...string) Option {
return func(cfg *Config) error {
cfg.session.LocaleIDs = locale
return nil
}
}
// ProductURI sets the product uri in the session configuration.
func ProductURI(s string) Option {
return func(cfg *Config) error {
cfg.session.ClientDescription.ProductURI = s
return nil
}
}
// stubbed out for testing
var randomRequestID func() uint32 = nil
// RandomRequestID assigns a random initial request id.
//
// The request id is generated using the 'rand' package and it
// is the caller's responsibility to initialize the random number
// generator properly.
func RandomRequestID() Option {
return func(cfg *Config) error {
if randomRequestID != nil {
cfg.sechan.RequestIDSeed = randomRequestID()
} else {
cfg.sechan.RequestIDSeed = uint32(rand.Int31())
}
return nil
}
}
// RemoteCertificate sets the server certificate.
func RemoteCertificate(cert []byte) Option {
return func(cfg *Config) error {
cfg.sechan.RemoteCertificate = cert
return nil
}
}
// RemoteCertificateFile sets the server certificate from the file
// in PEM or DER encoding.
func RemoteCertificateFile(filename string) Option {
return func(cfg *Config) error {
if filename == "" {
return nil
}
cert, err := loadCertificate(filename)
if err != nil {
return err
}
cfg.sechan.RemoteCertificate = cert
return nil
}
}
// SecurityMode sets the security mode for the secure channel.
func SecurityMode(m ua.MessageSecurityMode) Option {
return func(cfg *Config) error {
cfg.sechan.SecurityMode = m
return nil
}
}
// SecurityModeString sets the security mode for the secure channel.
// Valid values are "None", "Sign", and "SignAndEncrypt".
func SecurityModeString(s string) Option {
return func(cfg *Config) error {
cfg.sechan.SecurityMode = ua.MessageSecurityModeFromString(s)
return nil
}
}
// SecurityPolicy sets the security policy uri for the secure channel.
func SecurityPolicy(s string) Option {
return func(cfg *Config) error {
cfg.sechan.SecurityPolicyURI = ua.FormatSecurityPolicyURI(s)
return nil
}
}
// SessionName sets the name in the session configuration.
func SessionName(s string) Option {
return func(cfg *Config) error {
cfg.session.SessionName = s
return nil
}
}
// SessionTimeout sets the timeout in the session configuration.
func SessionTimeout(d time.Duration) Option {
return func(cfg *Config) error {
cfg.session.SessionTimeout = d
return nil
}
}
// PrivateKey sets the RSA private key in the secure channel configuration.
func PrivateKey(key *rsa.PrivateKey) Option {
return func(cfg *Config) error {
cfg.sechan.LocalKey = key
return nil
}
}
// 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(cfg *Config) error {
if filename == "" {
return nil
}
key, err := loadPrivateKey(filename)
if err != nil {
return err
}
cfg.sechan.LocalKey = key
return nil
}
}
func loadPrivateKey(filename string) (*rsa.PrivateKey, error) {
b, err := os.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(cfg *Config) error {
setCertificate(cert, cfg)
return nil
}
}
// CertificateFile 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(cfg *Config) error {
if filename == "" {
return nil
}
cert, err := loadCertificate(filename)
if err != nil {
return err
}
setCertificate(cert, cfg)
return nil
}
}
func loadCertificate(filename string) ([]byte, error) {
b, err := os.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, cfg *Config) {
cfg.sechan.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
}
cfg.session.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(cfg *Config) error {
cfg.sechan.SecurityPolicyURI = ep.SecurityPolicyURI
cfg.sechan.SecurityMode = ep.SecurityMode
cfg.sechan.RemoteCertificate = ep.ServerCertificate
cfg.sechan.Thumbprint = uapolicy.Thumbprint(ep.ServerCertificate)
for _, t := range ep.UserIdentityTokens {
if t.TokenType != authType {
continue
}
if cfg.session.UserIdentityToken == nil {
switch authType {
case ua.UserTokenTypeAnonymous:
cfg.session.UserIdentityToken = &ua.AnonymousIdentityToken{}
case ua.UserTokenTypeUserName:
cfg.session.UserIdentityToken = &ua.UserNameIdentityToken{}
case ua.UserTokenTypeCertificate:
cfg.session.UserIdentityToken = &ua.X509IdentityToken{}
case ua.UserTokenTypeIssuedToken:
cfg.session.UserIdentityToken = &ua.IssuedIdentityToken{}
}
}
setPolicyID(cfg.session.UserIdentityToken, t.PolicyID)
cfg.session.AuthPolicyURI = t.SecurityPolicyURI
return nil
}
if cfg.session.UserIdentityToken == nil {
cfg.session.UserIdentityToken = &ua.AnonymousIdentityToken{PolicyID: defaultAnonymousPolicyID}
cfg.session.AuthPolicyURI = ua.SecurityPolicyURINone
}
return nil
}
}
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(cfg *Config) error {
if cfg.session.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 nil
}
setPolicyID(cfg.session.UserIdentityToken, policy)
return nil
}
}
// 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(cfg *Config) error {
if cfg.session.UserIdentityToken == nil {
cfg.session.UserIdentityToken = &ua.AnonymousIdentityToken{}
}
_, ok := cfg.session.UserIdentityToken.(*ua.AnonymousIdentityToken)
if !ok {
// todo(fs): should we Fatal here?
log.Printf("non-anonymous authentication already configured, ignoring")
return nil
}
return nil
}
}
// 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(cfg *Config) error {
if cfg.session.UserIdentityToken == nil {
cfg.session.UserIdentityToken = &ua.UserNameIdentityToken{}
}
t, ok := cfg.session.UserIdentityToken.(*ua.UserNameIdentityToken)
if !ok {
// todo(fs): should we Fatal here?
log.Printf("non-username authentication already configured, ignoring")
return nil
}
t.UserName = user
cfg.session.AuthPassword = pass
return nil
}
}
// 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(cfg *Config) error {
if cfg.session.UserIdentityToken == nil {
cfg.session.UserIdentityToken = &ua.X509IdentityToken{}
}
t, ok := cfg.session.UserIdentityToken.(*ua.X509IdentityToken)
if !ok {
// todo(fs): should we Fatal here?
log.Printf("non-certificate authentication already configured, ignoring")
return nil
}
t.CertificateData = cert
return nil
}
}
// AuthPrivateKey sets the client's authentication RSA private key
// Note: PolicyID still needs to be set outside of this method, typically through
// the SecurityFromEndpoint() Option
func AuthPrivateKey(key *rsa.PrivateKey) Option {
return func(cfg *Config) error {
cfg.sechan.UserKey = key
return nil
}
}
// 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(cfg *Config) error {
if cfg.session.UserIdentityToken == nil {
cfg.session.UserIdentityToken = &ua.IssuedIdentityToken{}
}
t, ok := cfg.session.UserIdentityToken.(*ua.IssuedIdentityToken)
if !ok {
log.Printf("non-issued token authentication already configured, ignoring")
return nil
}
// todo(dw): not correct; need to read spec
t.TokenData = tokenData
return nil
}
}
// RequestTimeout sets the timeout for all requests over SecureChannel
func RequestTimeout(t time.Duration) Option {
return func(cfg *Config) error {
cfg.sechan.RequestTimeout = t
return nil
}
}
// Dialer sets the uacp.Dialer to establish the connection to the server.
func Dialer(d *uacp.Dialer) Option {
return func(cfg *Config) error {
initDialer(cfg)
cfg.dialer = d
return nil
}
}
// DialTimeout sets the timeout for establishing the UACP connection.
// Defaults to DefaultDialTimeout. Set to zero for no timeout.
func DialTimeout(d time.Duration) Option {
return func(cfg *Config) error {
initDialer(cfg)
cfg.dialer.Dialer.Timeout = d
return nil
}
}
// MaxMessageSize sets the maximum message size for the UACP handshake.
func MaxMessageSize(n uint32) Option {
return func(cfg *Config) error {
initDialer(cfg)
cfg.dialer.ClientACK.MaxMessageSize = n
return nil
}
}
// MaxChunkCount sets the maximum chunk count for the UACP handshake.
func MaxChunkCount(n uint32) Option {
return func(cfg *Config) error {
initDialer(cfg)
cfg.dialer.ClientACK.MaxChunkCount = n
return nil
}
}
// ReceiveBufferSize sets the receive buffer size for the UACP handshake.
func ReceiveBufferSize(n uint32) Option {
return func(cfg *Config) error {
initDialer(cfg)
cfg.dialer.ClientACK.ReceiveBufSize = n
return nil
}
}
// SendBufferSize sets the send buffer size for the UACP handshake.
func SendBufferSize(n uint32) Option {
return func(cfg *Config) error {
initDialer(cfg)
cfg.dialer.ClientACK.SendBufSize = n
return nil
}
}
func initDialer(cfg *Config) {
if cfg.dialer == nil {
cfg.dialer = &uacp.Dialer{}
}
if cfg.dialer.Dialer == nil {
cfg.dialer.Dialer = &net.Dialer{}
}
if cfg.dialer.ClientACK == nil {
cfg.dialer.ClientACK = uacp.DefaultClientACK
}
}