28
28
import java .security .cert .CertificateEncodingException ;
29
29
import java .security .cert .CertificateFactory ;
30
30
import java .security .cert .X509Certificate ;
31
+ import java .util .Collection ;
32
+ import java .util .Comparator ;
33
+ import java .util .HashSet ;
31
34
import java .util .Map .Entry ;
35
+ import java .util .Set ;
32
36
import java .util .concurrent .ScheduledExecutorService ;
33
37
import java .util .concurrent .ScheduledFuture ;
34
38
import java .util .concurrent .TimeUnit ;
35
39
import java .util .function .Function ;
36
40
import java .util .function .Supplier ;
41
+ import java .util .stream .Collectors ;
37
42
import org .apache .commons .lang3 .RandomStringUtils ;
38
- import org .apache .commons .lang3 .StringUtils ;
39
43
import org .apache .plc4x .java .api .authentication .PlcAuthentication ;
40
44
import org .apache .plc4x .java .api .authentication .PlcUsernamePasswordAuthentication ;
41
45
import org .apache .plc4x .java .api .exceptions .PlcRuntimeException ;
42
46
import org .apache .plc4x .java .opcua .config .OpcuaConfiguration ;
43
47
import org .apache .plc4x .java .opcua .readwrite .*;
48
+ import org .apache .plc4x .java .opcua .security .MessageSecurity ;
44
49
import org .apache .plc4x .java .opcua .security .SecurityPolicy ;
45
50
import org .apache .plc4x .java .opcua .security .SecurityPolicy .SignatureAlgorithm ;
46
51
import org .apache .plc4x .java .spi .generation .*;
56
61
import java .util .ArrayList ;
57
62
import java .util .List ;
58
63
import java .util .concurrent .CompletableFuture ;
59
- import java .util .regex .Matcher ;
60
64
import java .util .regex .Pattern ;
61
65
62
- import static java .util .concurrent .Executors .newSingleThreadExecutor ;
63
-
64
66
public class SecureChannel {
65
67
66
68
private static final Logger LOGGER = LoggerFactory .getLogger (SecureChannel .class );
@@ -91,7 +93,7 @@ public class SecureChannel {
91
93
private final OpcuaDriverContext driverContext ;
92
94
private final Conversation conversation ;
93
95
private ScheduledFuture <?> keepAlive ;
94
- private final List <String > endpoints = new ArrayList <>();
96
+ private final Set <String > endpoints = new HashSet <>();
95
97
private double sessionTimeout ;
96
98
private long revisedLifetime ;
97
99
@@ -117,9 +119,9 @@ public SecureChannel(Conversation conversation, RequestTransactionManager tm, Op
117
119
// Generate a list of endpoints we can use.
118
120
try {
119
121
InetAddress address = InetAddress .getByName (driverContext .getHost ());
120
- this .endpoints .add (address .getHostAddress ());
121
- this .endpoints .add (address .getHostName ());
122
- this .endpoints .add (address .getCanonicalHostName ());
122
+ this .endpoints .add ("opc.tcp://" + address .getHostAddress () + ":" + driverContext . getPort () + driverContext . getTransportEndpoint ());
123
+ this .endpoints .add ("opc.tcp://" + address .getHostName () + ":" + driverContext . getPort () + driverContext . getTransportEndpoint ());
124
+ this .endpoints .add ("opc.tcp://" + address .getCanonicalHostName () + ":" + driverContext . getPort () + driverContext . getTransportEndpoint ());
123
125
} catch (UnknownHostException e ) {
124
126
LOGGER .warn ("Unable to resolve host name. Using original host from connection string which may cause issues connecting to server" );
125
127
this .endpoints .add (driverContext .getHost ());
@@ -313,23 +315,24 @@ private CompletableFuture<ActivateSessionResponse> onConnectActivateSessionReque
313
315
conversation .setRemoteCertificate (getX509Certificate (sessionResponse .getServerCertificate ().getStringValue ()));
314
316
conversation .setRemoteNonce (sessionResponse .getServerNonce ().getStringValue ());
315
317
316
- String [] endpoints = new String [ 3 ] ;
318
+ List < String > contactPoints = new ArrayList <>( 3 ) ;
317
319
try {
318
320
InetAddress address = InetAddress .getByName (driverContext .getHost ());
319
- endpoints [ 0 ] = "opc.tcp://" + address .getHostAddress () + ":" + driverContext .getPort () + driverContext .getTransportEndpoint ();
320
- endpoints [ 1 ] = "opc.tcp://" + address .getHostName () + ":" + driverContext .getPort () + driverContext .getTransportEndpoint ();
321
- endpoints [ 2 ] = "opc.tcp://" + address .getCanonicalHostName () + ":" + driverContext .getPort () + driverContext .getTransportEndpoint ();
321
+ contactPoints . add ( "opc.tcp://" + address .getHostAddress () + ":" + driverContext .getPort () + driverContext .getTransportEndpoint () );
322
+ contactPoints . add ( "opc.tcp://" + address .getHostName () + ":" + driverContext .getPort () + driverContext .getTransportEndpoint () );
323
+ contactPoints . add ( "opc.tcp://" + address .getCanonicalHostName () + ":" + driverContext .getPort () + driverContext .getTransportEndpoint () );
322
324
} catch (UnknownHostException e ) {
323
325
LOGGER .debug ("error getting host" , e );
324
326
}
325
327
326
- Entry <EndpointDescription , UserTokenPolicy > endpointAndAuthPolicy = selectEndpoint (sessionResponse );
327
- if (endpointAndAuthPolicy == null ) {
328
- throw new PlcRuntimeException ("Unable to find endpoint - " + endpoints [1 ]);
328
+ Entry <EndpointDescription , UserTokenPolicy > selectedEndpoint = selectEndpoint (sessionResponse .getServerEndpoints (), contactPoints ,
329
+ configuration .getSecurityPolicy (), configuration .getMessageSecurity ());
330
+ if (selectedEndpoint == null ) {
331
+ throw new PlcRuntimeException ("Unable to find endpoint matching - " + contactPoints .get (1 ));
329
332
}
330
333
331
- PascalString policyId = endpointAndAuthPolicy .getValue ().getPolicyId ();
332
- UserTokenType tokenType = endpointAndAuthPolicy .getValue ().getTokenType ();
334
+ PascalString policyId = selectedEndpoint .getValue ().getPolicyId ();
335
+ UserTokenType tokenType = selectedEndpoint .getValue ().getTokenType ();
333
336
ExtensionObject userIdentityToken = getIdentityToken (tokenType , policyId .getStringValue ());
334
337
RequestHeader requestHeader = conversation .createRequestHeader ();
335
338
SignatureData clientSignature = new SignatureData (NULL_STRING , NULL_BYTE_STRING );
@@ -421,27 +424,19 @@ public CompletableFuture<EndpointDescription> onDiscoverGetEndpointsRequest() {
421
424
422
425
return conversation .submit (endpointsRequest , GetEndpointsResponse .class ).thenApply (response -> {
423
426
List <ExtensionObjectDefinition > endpoints = response .getEndpoints ();
424
- MessageSecurityMode effectiveMode = this .configuration .getSecurityPolicy () == SecurityPolicy .NONE ? MessageSecurityMode .messageSecurityModeNone : this .configuration .getMessageSecurity ().getMode ();
425
- for (ExtensionObjectDefinition endpoint : endpoints ) {
426
- EndpointDescription endpointDescription = (EndpointDescription ) endpoint ;
427
-
428
- boolean urlMatch = endpointDescription .getEndpointUrl ().getStringValue ().equals (this .endpoint .getStringValue ());
429
- boolean policyMatch = endpointDescription .getSecurityPolicyUri ().getStringValue ().equals (this .configuration .getSecurityPolicy ().getSecurityPolicyUri ());
430
- boolean msgSecurityMatch = endpointDescription .getSecurityMode ().equals (effectiveMode );
431
-
432
- LOGGER .debug ("Validate OPC UA endpoint {} during discovery phase."
433
- + "Expected {}. Endpoint policy {} looking for {}. Message security {}, looking for {}" , endpointDescription .getEndpointUrl ().getStringValue (), this .endpoint .getStringValue (),
434
- endpointDescription .getSecurityPolicyUri ().getStringValue (), configuration .getSecurityPolicy ().getSecurityPolicyUri (),
435
- endpointDescription .getSecurityMode (), configuration .getMessageSecurity ().getMode ());
436
-
437
- if (urlMatch && policyMatch && msgSecurityMatch ) {
438
- LOGGER .info ("Found OPC UA endpoint {}" , this .endpoint .getStringValue ());
439
- return endpointDescription ;
440
- }
427
+ Entry <EndpointDescription , UserTokenPolicy > entry = selectEndpoint (response .getEndpoints (), this .endpoints , this .configuration .getSecurityPolicy (), this .configuration .getMessageSecurity ());
428
+
429
+ if (entry == null ) {
430
+ Set <String > endpointUris = endpoints .stream ()
431
+ .filter (EndpointDescription .class ::isInstance )
432
+ .map (EndpointDescription .class ::cast )
433
+ .map (EndpointDescription ::getEndpointUrl )
434
+ .map (PascalString ::getStringValue )
435
+ .collect (Collectors .toSet ());
436
+ throw new IllegalArgumentException ("Could not find endpoint matching client configuration. Tested " + endpointUris + ". "
437
+ + "Was looking for " + this .endpoint .getStringValue () + " " + this .configuration .getSecurityPolicy ().getSecurityPolicyUri () + " " + this .configuration .getMessageSecurity ().getMode ());
441
438
}
442
-
443
- throw new IllegalArgumentException ("Could not find endpoint matching client configuration. Tested " + endpoints .size () + " endpoints. "
444
- + "None matched " + this .endpoint .getStringValue () + " " + this .configuration .getSecurityPolicy ().getSecurityPolicyUri () + " " + this .configuration .getMessageSecurity ().getMode ());
439
+ return entry .getKey ();
445
440
});
446
441
}
447
442
@@ -503,32 +498,49 @@ private static ReadBufferByteBased toBuffer(Supplier<Payload> supplier) {
503
498
/**
504
499
* Selects the endpoint and authentication policy based on client settings.
505
500
*
506
- * @param sessionResponse - The CreateSessionResponse message returned by the server
507
- * @return Entry representing desired server endpoint and user token policy to access it.
501
+ * @param extensionObjects Endpoint descriptions returned by the server.
502
+ * @param contactPoints Contact points expected by client.
503
+ * @param securityPolicy Security policy searched in endpoints.
504
+ * @param messageSecurity Message security needed by client.
505
+ * @return Endpoint matching given.
508
506
*/
509
- private Entry <EndpointDescription , UserTokenPolicy > selectEndpoint (CreateSessionResponse sessionResponse ) {
507
+ private Entry <EndpointDescription , UserTokenPolicy > selectEndpoint (List <ExtensionObjectDefinition > extensionObjects , Collection <String > contactPoints ,
508
+ SecurityPolicy securityPolicy , MessageSecurity messageSecurity ) throws PlcRuntimeException {
510
509
// Get a list of the endpoints which match ours.
511
- EndpointDescription selectedEndpoint = null ;
512
- for (ExtensionObjectDefinition endpoint : sessionResponse .getServerEndpoints ()) {
513
- if (!(endpoint instanceof EndpointDescription )) {
510
+ MessageSecurityMode effectiveMessageSecurity = SecurityPolicy .NONE == securityPolicy ? MessageSecurityMode .messageSecurityModeNone : messageSecurity .getMode ();
511
+ List <Entry <EndpointDescription , UserTokenPolicy >> serverEndpoints = new ArrayList <>();
512
+
513
+ for (ExtensionObjectDefinition extensionObject : extensionObjects ) {
514
+ if (!(extensionObject instanceof EndpointDescription )) {
514
515
continue ;
515
516
}
516
- if (isEndpoint ((EndpointDescription ) endpoint )) {
517
- selectedEndpoint = (EndpointDescription ) endpoint ;
518
- break ;
517
+
518
+ EndpointDescription endpointDescription = (EndpointDescription ) extensionObject ;
519
+ if (isMatchingEndpoint (endpointDescription , contactPoints )) {
520
+ boolean policyMatch = endpointDescription .getSecurityPolicyUri ().getStringValue ().equals (securityPolicy .getSecurityPolicyUri ());
521
+ boolean msgSecurityMatch = endpointDescription .getSecurityMode ().equals (effectiveMessageSecurity );
522
+
523
+ if (!policyMatch && !msgSecurityMatch ) {
524
+ continue ;
525
+ }
526
+
527
+ for (ExtensionObjectDefinition objectDefinition : endpointDescription .getUserIdentityTokens ()) {
528
+ if (objectDefinition instanceof UserTokenPolicy ) {
529
+ UserTokenPolicy userTokenPolicy = (UserTokenPolicy ) objectDefinition ;
530
+ if (isUserTokenPolicyCompatible (userTokenPolicy , this .username )) {
531
+ serverEndpoints .add (entry (endpointDescription , userTokenPolicy ));
532
+ }
533
+ }
534
+ }
519
535
}
520
536
}
521
537
522
- for (ExtensionObjectDefinition tokenPolicy : selectedEndpoint .getUserIdentityTokens ()) {
523
- if (!(tokenPolicy instanceof UserTokenPolicy )) {
524
- continue ;
525
- }
526
- if (hasIdentity ((UserTokenPolicy ) tokenPolicy )) {
527
- return entry (selectedEndpoint , (UserTokenPolicy ) tokenPolicy );
528
- }
538
+ if (serverEndpoints .isEmpty ()) {
539
+ return null ;
529
540
}
530
541
531
- return null ;
542
+ serverEndpoints .sort (Comparator .comparing (e -> e .getKey ().getSecurityLevel ()));
543
+ return serverEndpoints .get (0 );
532
544
}
533
545
534
546
/**
@@ -539,36 +551,14 @@ private Entry<EndpointDescription, UserTokenPolicy> selectEndpoint(CreateSession
539
551
* @return true if this endpoint matches our configuration
540
552
* @throws PlcRuntimeException - If the returned endpoint string doesn't match the format expected
541
553
*/
542
- private boolean isEndpoint (EndpointDescription endpoint ) throws PlcRuntimeException {
554
+ private static boolean isMatchingEndpoint (EndpointDescription endpoint , Collection < String > contactPoints ) throws PlcRuntimeException {
543
555
// Split up the connection string into it's individual segments.
544
- String endpointUri = endpoint .getEndpointUrl ().getStringValue ();
545
- Matcher matcher = URI_PATTERN .matcher (endpointUri );
546
- if (!matcher .matches ()) {
547
- throw new PlcRuntimeException (
548
- "Endpoint " + endpointUri + " returned from the server doesn't match the format '{protocol-code}:({transport-code})?//{transport-host}(:{transport-port})(/{transport-endpoint})'" );
549
- }
550
- LOGGER .trace ("Using Endpoint {} {} {}" , matcher .group ("transportHost" ), matcher .group ("transportPort" ), matcher .group ("transportEndpoint" ));
551
-
552
- //When the parameter discovery=false is configured, prefer using the custom address. If the transportEndpoint is empty,
553
- // directly replace it with the TransportEndpoint returned by the server.
554
- if (!configuration .isDiscovery () && StringUtils .isBlank (driverContext .getTransportEndpoint ())) {
555
- driverContext .setTransportEndpoint (matcher .group ("transportEndpoint" ));
556
- return true ;
557
- }
558
-
559
- if (configuration .isDiscovery () && !this .endpoints .contains (matcher .group ("transportHost" ))) {
560
- return false ;
561
- }
562
-
563
- if (!driverContext .getPort ().equals (matcher .group ("transportPort" ))) {
564
- return false ;
565
- }
566
-
567
- if (!driverContext .getTransportEndpoint ().equals (matcher .group ("transportEndpoint" ))) {
568
- return false ;
556
+ for (String contactPoint : contactPoints ) {
557
+ if (endpoint .getEndpointUrl ().getStringValue ().startsWith (contactPoint )) {
558
+ return true ;
559
+ }
569
560
}
570
-
571
- return true ;
561
+ return false ;
572
562
}
573
563
574
564
/**
@@ -577,11 +567,11 @@ private boolean isEndpoint(EndpointDescription endpoint) throws PlcRuntimeExcept
577
567
* @param policy - UserTokenPolicy configured for server endpoint.
578
568
* @return True if given token policy matches client configuration.
579
569
*/
580
- private boolean hasIdentity (UserTokenPolicy policy ) {
581
- if ((policy .getTokenType () == UserTokenType .userTokenTypeAnonymous ) && this . username == null ) {
570
+ private static boolean isUserTokenPolicyCompatible (UserTokenPolicy policy , String username ) {
571
+ if ((policy .getTokenType () == UserTokenType .userTokenTypeAnonymous ) && username == null ) {
582
572
return true ;
583
573
}
584
- return policy .getTokenType () == UserTokenType .userTokenTypeUserName && this . username != null ;
574
+ return policy .getTokenType () == UserTokenType .userTokenTypeUserName && username != null ;
585
575
}
586
576
587
577
/**
0 commit comments