forked from gopcua/opcua
-
Notifications
You must be signed in to change notification settings - Fork 0
/
config.go
476 lines (418 loc) · 13.1 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
// 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"
"io/ioutil"
"log"
"math/rand"
"net"
"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
dialTimeout *time.Duration
sechan *uasc.Config
session *uasc.SessionConfig
}
// NewDialer creates a uacp.Dialer from the config options
func NewDialer(cfg *Config) *uacp.Dialer {
d := cfg.dialer
if d == nil {
d = &uacp.Dialer{}
}
if d.Dialer == nil {
d.Dialer = &net.Dialer{}
}
if cfg.dialTimeout != nil {
d.Dialer.Timeout = *cfg.dialTimeout
}
return d
}
// ApplyConfig applies the config options to the default configuration.
// todo(fs): Can we find a better name?
func ApplyConfig(opts ...Option) *Config {
cfg := &Config{
sechan: DefaultClientConfig(),
session: DefaultSessionConfig(),
}
for _, opt := range opts {
opt(cfg)
}
return cfg
}
// Option is an option function type to modify the configuration.
type Option func(*Config)
// ApplicationName sets the application name in the session configuration.
func ApplicationName(s string) Option {
return func(cfg *Config) {
cfg.session.ClientDescription.ApplicationName = ua.NewLocalizedText(s)
}
}
// ApplicationURI sets the application uri in the session configuration.
func ApplicationURI(s string) Option {
return func(cfg *Config) {
cfg.session.ClientDescription.ApplicationURI = s
}
}
// AutoReconnect sets the auto reconnect state of the secure channel.
func AutoReconnect(b bool) Option {
return func(cfg *Config) {
cfg.sechan.AutoReconnect = b
}
}
// ReconnectInterval is interval duration between each reconnection attempt.
func ReconnectInterval(d time.Duration) Option {
return func(cfg *Config) {
cfg.sechan.ReconnectInterval = d
}
}
// Lifetime sets the lifetime of the secure channel in milliseconds.
func Lifetime(d time.Duration) Option {
return func(cfg *Config) {
cfg.sechan.Lifetime = uint32(d / time.Millisecond)
}
}
// Locales sets the locales in the session configuration.
func Locales(locale ...string) Option {
return func(cfg *Config) {
cfg.session.LocaleIDs = locale
}
}
// ProductURI sets the product uri in the session configuration.
func ProductURI(s string) Option {
return func(cfg *Config) {
cfg.session.ClientDescription.ProductURI = s
}
}
// RandomRequestID assigns a random initial request id.
func RandomRequestID() Option {
return func(cfg *Config) {
cfg.sechan.RequestIDSeed = uint32(rand.Int31())
}
}
// RemoteCertificate sets the server certificate.
func RemoteCertificate(cert []byte) Option {
return func(cfg *Config) {
cfg.sechan.RemoteCertificate = cert
}
}
// RemoteCertificateFile sets the server certificate from the file
// in PEM or DER encoding.
func RemoteCertificateFile(filename string) Option {
return func(cfg *Config) {
cert, err := loadCertificate(filename)
if err != nil {
log.Fatal(err)
}
cfg.sechan.RemoteCertificate = cert
}
}
// SecurityMode sets the security mode for the secure channel.
func SecurityMode(m ua.MessageSecurityMode) Option {
return func(cfg *Config) {
cfg.sechan.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(cfg *Config) {
cfg.sechan.SecurityMode = ua.MessageSecurityModeFromString(s)
}
}
// SecurityPolicy sets the security policy uri for the secure channel.
func SecurityPolicy(s string) Option {
return func(cfg *Config) {
cfg.sechan.SecurityPolicyURI = ua.FormatSecurityPolicyURI(s)
}
}
// SessionName sets the name in the session configuration.
func SessionName(s string) Option {
return func(cfg *Config) {
cfg.session.SessionName = s
}
}
// SessionTimeout sets the timeout in the session configuration.
func SessionTimeout(d time.Duration) Option {
return func(cfg *Config) {
cfg.session.SessionTimeout = d
}
}
// PrivateKey sets the RSA private key in the secure channel configuration.
func PrivateKey(key *rsa.PrivateKey) Option {
return func(cfg *Config) {
cfg.sechan.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(cfg *Config) {
if filename == "" {
return
}
key, err := loadPrivateKey(filename)
if err != nil {
log.Fatal(err)
}
cfg.sechan.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(cfg *Config) {
setCertificate(cert, cfg)
}
}
// 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(cfg *Config) {
if filename == "" {
return
}
cert, err := loadCertificate(filename)
if err != nil {
log.Fatal(err)
}
setCertificate(cert, cfg)
}
}
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, 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) {
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
}
if cfg.session.UserIdentityToken == nil {
cfg.session.UserIdentityToken = &ua.AnonymousIdentityToken{PolicyID: defaultAnonymousPolicyID}
cfg.session.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(cfg *Config) {
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
}
setPolicyID(cfg.session.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(cfg *Config) {
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
}
}
}
// 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) {
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
}
t.UserName = user
cfg.session.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(cfg *Config) {
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
}
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(cfg *Config) {
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
}
// 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(cfg *Config) {
cfg.sechan.RequestTimeout = t
}
}
// Dialer sets the uacp.Dialer to establish the connection to the server.
func Dialer(d *uacp.Dialer) Option {
return func(cfg *Config) {
cfg.dialer = d
}
}
// 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) {
cfg.dialTimeout = &d
}
}