PHP Classes

File: tests/Unit/Protocol/AdditionalCoverageTest.php

Recommend this page to a friend!
  Packages of Gianfrancesco Aurecchia   OPC UA Client   tests/Unit/Protocol/AdditionalCoverageTest.php   Download  
File: tests/Unit/Protocol/AdditionalCoverageTest.php
Role: Example script
Content type: text/plain
Description: Example script
Class: OPC UA Client
Control devices that support the OPC UA protocol
Author: By
Last change:
Date: 5 days ago
Size: 21,796 bytes
 

Contents

Class file image Download
<?php declare(strict_types=1); use PhpOpcua\Client\Encoding\BinaryDecoder; use PhpOpcua\Client\Encoding\BinaryEncoder; use PhpOpcua\Client\Module\History\HistoryReadService; use PhpOpcua\Client\Module\Subscription\MonitoredItemService; use PhpOpcua\Client\Module\Subscription\PublishService; use PhpOpcua\Client\Protocol\MessageHeader; use PhpOpcua\Client\Protocol\SessionService; use PhpOpcua\Client\Security\CertificateManager; use PhpOpcua\Client\Security\SecureChannel; use PhpOpcua\Client\Security\SecurityMode; use PhpOpcua\Client\Security\SecurityPolicy; use PhpOpcua\Client\Types\BuiltinType; use PhpOpcua\Client\Types\NodeId; function writeAdditionalResponseHeader(BinaryEncoder $encoder, int $statusCode = 0): void { $encoder->writeInt64(0); $encoder->writeUInt32(1); $encoder->writeUInt32($statusCode); $encoder->writeByte(0); $encoder->writeInt32(0); $encoder->writeNodeId(NodeId::numeric(0, 0)); $encoder->writeByte(0); } function writeAdditionalMessagePrefix(BinaryEncoder $encoder): void { $encoder->writeUInt32(1); $encoder->writeUInt32(1); $encoder->writeUInt32(1); } function writeFullDiagnosticInfo(BinaryEncoder $encoder): void { $encoder->writeByte(0x1F); $encoder->writeInt32(1); $encoder->writeInt32(2); $encoder->writeInt32(3); $encoder->writeString('details'); $encoder->writeUInt32(0x80010000); } // ======================================================================== // PublishService: diagnostics + results in response // ======================================================================== describe('PublishService decode with diagnostics and results', function () { it('decodes a PublishResponse with Results and DiagnosticInfos', function () { $session = new SessionService(1, 1); $service = new PublishService($session); $encoder = new BinaryEncoder(); writeAdditionalMessagePrefix($encoder); $encoder->writeNodeId(NodeId::numeric(0, 829)); writeAdditionalResponseHeader($encoder); // SubscriptionId $encoder->writeUInt32(1); // AvailableSequenceNumbers: 0 $encoder->writeInt32(0); // MoreNotifications $encoder->writeBoolean(false); // NotificationMessage $encoder->writeUInt32(1); // SequenceNumber $encoder->writeDateTime(null); // PublishTime // NotificationData: 1 DataChangeNotification with diagnostics inside $encoder->writeInt32(1); $encoder->writeNodeId(NodeId::numeric(0, 811)); $encoder->writeByte(0x01); $bodyEncoder = new BinaryEncoder(); // MonitoredItems: 1 $bodyEncoder->writeInt32(1); $bodyEncoder->writeUInt32(1); $bodyEncoder->writeByte(0x01); $bodyEncoder->writeByte(BuiltinType::Int32->value); $bodyEncoder->writeInt32(99); // DiagnosticInfos inside DataChangeNotification: 1 $bodyEncoder->writeInt32(1); writeFullDiagnosticInfo($bodyEncoder); $body = $bodyEncoder->getBuffer(); $encoder->writeInt32(strlen($body)); $encoder->writeRawBytes($body); // Results: 2 status codes $encoder->writeInt32(2); $encoder->writeUInt32(0); $encoder->writeUInt32(0x80010000); // DiagnosticInfos: 1 $encoder->writeInt32(1); writeFullDiagnosticInfo($encoder); $decoder = new BinaryDecoder($encoder->getBuffer()); $result = $service->decodePublishResponse($decoder); expect($result->subscriptionId)->toBe(1); expect($result->notifications)->toHaveCount(1); expect($result->notifications[0]['dataValue']->getValue())->toBe(99); }); it('decodes a PublishResponse where body consumed less than bodyLength', function () { $session = new SessionService(1, 1); $service = new PublishService($session); $encoder = new BinaryEncoder(); writeAdditionalMessagePrefix($encoder); $encoder->writeNodeId(NodeId::numeric(0, 829)); writeAdditionalResponseHeader($encoder); $encoder->writeUInt32(1); $encoder->writeInt32(0); $encoder->writeBoolean(false); $encoder->writeUInt32(1); $encoder->writeDateTime(null); // NotificationData: 1 DataChangeNotification with extra padding in body $encoder->writeInt32(1); $encoder->writeNodeId(NodeId::numeric(0, 811)); $encoder->writeByte(0x01); $bodyEncoder = new BinaryEncoder(); $bodyEncoder->writeInt32(1); $bodyEncoder->writeUInt32(1); $bodyEncoder->writeByte(0x01); $bodyEncoder->writeByte(BuiltinType::Int32->value); $bodyEncoder->writeInt32(55); $bodyEncoder->writeInt32(0); // DiagnosticInfos $body = $bodyEncoder->getBuffer(); // Add 8 extra bytes to simulate body that's larger than what we consume $paddedBody = $body . str_repeat("\x00", 8); $encoder->writeInt32(strlen($paddedBody)); $encoder->writeRawBytes($paddedBody); $encoder->writeInt32(0); // Results $encoder->writeInt32(0); // DiagnosticInfos $decoder = new BinaryDecoder($encoder->getBuffer()); $result = $service->decodePublishResponse($decoder); expect($result->notifications)->toHaveCount(1); expect($result->notifications[0]['dataValue']->getValue())->toBe(55); }); }); // ======================================================================== // MonitoredItemService: diagnostics // ======================================================================== describe('MonitoredItemService decode with diagnostics', function () { it('decodes CreateMonitoredItemsResponse with diagnostics', function () { $session = new SessionService(1, 1); $service = new MonitoredItemService($session); $encoder = new BinaryEncoder(); writeAdditionalMessagePrefix($encoder); $encoder->writeNodeId(NodeId::numeric(0, 754)); writeAdditionalResponseHeader($encoder); // Results: 1 $encoder->writeInt32(1); $encoder->writeUInt32(0); $encoder->writeUInt32(50); $encoder->writeDouble(250.0); $encoder->writeUInt32(2); $encoder->writeNodeId(NodeId::numeric(0, 0)); $encoder->writeByte(0x00); // DiagnosticInfos: 1 $encoder->writeInt32(1); writeFullDiagnosticInfo($encoder); $decoder = new BinaryDecoder($encoder->getBuffer()); $result = $service->decodeCreateMonitoredItemsResponse($decoder); expect($result)->toHaveCount(1); expect($result[0]->monitoredItemId)->toBe(50); }); it('decodes DeleteMonitoredItemsResponse with diagnostics', function () { $session = new SessionService(1, 1); $service = new MonitoredItemService($session); $encoder = new BinaryEncoder(); writeAdditionalMessagePrefix($encoder); $encoder->writeNodeId(NodeId::numeric(0, 784)); writeAdditionalResponseHeader($encoder); // Results: 1 $encoder->writeInt32(1); $encoder->writeUInt32(0); // DiagnosticInfos: 1 $encoder->writeInt32(1); writeFullDiagnosticInfo($encoder); $decoder = new BinaryDecoder($encoder->getBuffer()); $result = $service->decodeDeleteMonitoredItemsResponse($decoder); expect($result)->toBe([0]); }); }); // ======================================================================== // HistoryReadService: diagnostics // ======================================================================== describe('HistoryReadService decode with diagnostics', function () { it('decodes HistoryReadResponse with diagnostics', function () { $session = new SessionService(1, 1); $service = new HistoryReadService($session); $encoder = new BinaryEncoder(); writeAdditionalMessagePrefix($encoder); $encoder->writeNodeId(NodeId::numeric(0, 667)); writeAdditionalResponseHeader($encoder); // Results: 0 $encoder->writeInt32(0); // DiagnosticInfos: 1 $encoder->writeInt32(1); writeFullDiagnosticInfo($encoder); $decoder = new BinaryDecoder($encoder->getBuffer()); $result = $service->decodeHistoryReadResponse($decoder); expect($result)->toBe([]); }); it('decodes HistoryReadResponse where body has extra unconsumed bytes', function () { $session = new SessionService(1, 1); $service = new HistoryReadService($session); $encoder = new BinaryEncoder(); writeAdditionalMessagePrefix($encoder); $encoder->writeNodeId(NodeId::numeric(0, 667)); writeAdditionalResponseHeader($encoder); // Results: 1 $encoder->writeInt32(1); $encoder->writeUInt32(0); // StatusCode $encoder->writeByteString(null); // ContinuationPoint // HistoryData with body larger than consumed $encoder->writeNodeId(NodeId::numeric(0, 658)); $encoder->writeByte(0x01); $bodyEncoder = new BinaryEncoder(); $bodyEncoder->writeInt32(1); $bodyEncoder->writeByte(0x01); $bodyEncoder->writeByte(BuiltinType::Double->value); $bodyEncoder->writeDouble(42.5); $body = $bodyEncoder->getBuffer(); // Add extra bytes to body length $paddedBody = $body . str_repeat("\x00", 12); $encoder->writeInt32(strlen($paddedBody)); $encoder->writeRawBytes($paddedBody); // DiagnosticInfos: 0 $encoder->writeInt32(0); $decoder = new BinaryDecoder($encoder->getBuffer()); $result = $service->decodeHistoryReadResponse($decoder); expect($result)->toHaveCount(1); expect($result[0]->getValue())->toBe(42.5); }); }); // ======================================================================== // SessionService: identity token types and decode paths // ======================================================================== describe('SessionService activate with identity tokens', function () { it('encodes ActivateSession with username/password (no security)', function () { $session = new SessionService(1, 1); $bytes = $session->encodeActivateSessionRequest( 1, NodeId::numeric(0, 0), 'admin', 'secret', ); $decoder = new BinaryDecoder($bytes); $header = MessageHeader::decode($decoder); expect($header->getMessageType())->toBe('MSG'); expect(strlen($bytes))->toBeGreaterThan(60); }); it('encodes ActivateSession with X509 cert (no security)', function () { $privKey = openssl_pkey_new(['private_key_bits' => 2048, 'private_key_type' => OPENSSL_KEYTYPE_RSA]); $csr = openssl_csr_new(['CN' => 'test'], $privKey); $cert = openssl_csr_sign($csr, null, $privKey, 365); openssl_x509_export($cert, $certPem); $cm = new CertificateManager(); $tmpFile = tempnam(sys_get_temp_dir(), 'opcua_test_'); file_put_contents($tmpFile, $certPem); $userCertDer = $cm->loadCertificatePem($tmpFile); unlink($tmpFile); $session = new SessionService(1, 1); $bytes = $session->encodeActivateSessionRequest( 1, NodeId::numeric(0, 0), null, null, $userCertDer, $privKey, 'server-nonce', ); expect(strlen($bytes))->toBeGreaterThan(60); }); it('encodes ActivateSession with secure channel (anonymous)', function () { $session = createSecureSession(); $bytes = $session->encodeActivateSessionRequest( 1, NodeId::numeric(0, 0), ); expect(substr($bytes, 0, 3))->toBe('MSG'); }); it('encodes ActivateSession with secure channel and username', function () { $session = createSecureSession(); $bytes = $session->encodeActivateSessionRequest( 1, NodeId::numeric(0, 0), 'admin', 'password123', null, null, 'server-nonce-bytes', ); expect(substr($bytes, 0, 3))->toBe('MSG'); expect(strlen($bytes))->toBeGreaterThan(100); }); it('encodes ActivateSession with secure channel and X509 cert', function () { $session = createSecureSession(); $privKey = openssl_pkey_new(['private_key_bits' => 2048, 'private_key_type' => OPENSSL_KEYTYPE_RSA]); $csr = openssl_csr_new(['CN' => 'user'], $privKey); $cert = openssl_csr_sign($csr, null, $privKey, 365); openssl_x509_export($cert, $certPem); $cm = new CertificateManager(); $tmpFile = tempnam(sys_get_temp_dir(), 'opcua_user_'); file_put_contents($tmpFile, $certPem); $userCertDer = $cm->loadCertificatePem($tmpFile); unlink($tmpFile); $bytes = $session->encodeActivateSessionRequest( 1, NodeId::numeric(0, 0), null, null, $userCertDer, $privKey, 'server-nonce', ); expect(substr($bytes, 0, 3))->toBe('MSG'); }); }); describe('SessionService decode additional paths', function () { it('decodes CreateSessionResponse with server endpoints and software certs', function () { $session = new SessionService(1, 1); $encoder = new BinaryEncoder(); // Security + sequence header $encoder->writeUInt32(1); $encoder->writeUInt32(1); $encoder->writeUInt32(1); // TypeId: CreateSessionResponse (464) $encoder->writeNodeId(NodeId::numeric(0, 464)); // ResponseHeader writeAdditionalResponseHeader($encoder); // CreateSession response fields $encoder->writeNodeId(NodeId::numeric(0, 999)); // SessionId $encoder->writeNodeId(NodeId::numeric(0, 888)); // AuthenticationToken $encoder->writeDouble(120000.0); // RevisedSessionTimeout $encoder->writeByteString('server-nonce-123'); // ServerNonce $encoder->writeByteString('fake-server-cert'); // ServerCertificate // ServerEndpoints: 1 endpoint $encoder->writeInt32(1); // Full EndpointDescription $encoder->writeString('opc.tcp://localhost:4840'); // EndpointUrl // ApplicationDescription $encoder->writeString('urn:server'); // ApplicationUri $encoder->writeString(null); // ProductUri $encoder->writeByte(0x02); $encoder->writeString('Server'); // ApplicationName (LocalizedText) $encoder->writeUInt32(0); // ApplicationType $encoder->writeString(null); // GatewayServerUri $encoder->writeString(null); // DiscoveryProfileUri $encoder->writeInt32(0); // DiscoveryUrls $encoder->writeByteString(null); // ServerCertificate $encoder->writeUInt32(1); // MessageSecurityMode: None $encoder->writeString('http://opcfoundation.org/UA/SecurityPolicy#None'); // SecurityPolicyUri // UserIdentityTokens: 1 $encoder->writeInt32(1); $encoder->writeString('anonymous'); // PolicyId $encoder->writeUInt32(0); // TokenType: Anonymous $encoder->writeString(null); // IssuedTokenType $encoder->writeString(null); // IssuerEndpointUrl $encoder->writeString(null); // SecurityPolicyUri $encoder->writeString(null); // TransportProfileUri $encoder->writeByte(0); // SecurityLevel // ServerSoftwareCertificates: 1 $encoder->writeInt32(1); $encoder->writeByteString('cert-data'); // CertificateData $encoder->writeByteString('sig-data'); // Signature // ServerSignature $encoder->writeString(null); // Algorithm $encoder->writeByteString(null); // Signature $encoder->writeUInt32(0); // MaxRequestMessageSize $decoder = new BinaryDecoder($encoder->getBuffer()); $result = $session->decodeCreateSessionResponse($decoder); expect($result['authenticationToken']->getIdentifier())->toBe(888); expect($result['serverNonce'])->toBe('server-nonce-123'); expect($result['serverCertificate'])->toBe('fake-server-cert'); }); it('decodes ActivateSessionResponse with results and diagnostics', function () { $session = new SessionService(1, 1); $encoder = new BinaryEncoder(); $encoder->writeUInt32(1); $encoder->writeUInt32(1); $encoder->writeUInt32(1); $encoder->writeNodeId(NodeId::numeric(0, 470)); // ActivateSessionResponse writeAdditionalResponseHeader($encoder); // ServerNonce $encoder->writeByteString('new-nonce'); // Results: 2 StatusCodes $encoder->writeInt32(2); $encoder->writeUInt32(0); $encoder->writeUInt32(0); // DiagnosticInfos: 1 with nested inner diagnostic $encoder->writeInt32(1); $encoder->writeByte(0x21); // symbolicId + innerDiagnosticInfo $encoder->writeInt32(42); $encoder->writeByte(0x08); // inner: additionalInfo $encoder->writeString('inner info'); $decoder = new BinaryDecoder($encoder->getBuffer()); // Should not throw $session->decodeActivateSessionResponse($decoder); expect(true)->toBeTrue(); }); }); // ======================================================================== // CertificateManager: loadCertificateDer // ======================================================================== describe('CertificateManager loadCertificateDer', function () { it('loads a DER certificate from file', function () { // Generate a cert and save it as DER $privKey = openssl_pkey_new(['private_key_bits' => 2048, 'private_key_type' => OPENSSL_KEYTYPE_RSA]); $csr = openssl_csr_new(['CN' => 'test-der'], $privKey); $cert = openssl_csr_sign($csr, null, $privKey, 365); openssl_x509_export($cert, $certPem); $cm = new CertificateManager(); // First load as PEM to get DER $tmpPem = tempnam(sys_get_temp_dir(), 'opcua_pem_'); file_put_contents($tmpPem, $certPem); $expectedDer = $cm->loadCertificatePem($tmpPem); unlink($tmpPem); // Now save as DER and load via loadCertificateDer $tmpDer = tempnam(sys_get_temp_dir(), 'opcua_der_'); file_put_contents($tmpDer, $expectedDer); try { $loadedDer = $cm->loadCertificateDer($tmpDer); expect($loadedDer)->toBe($expectedDer); } finally { unlink($tmpDer); } }); }); // ======================================================================== // SecureChannel: processMessage SignAndEncrypt round-trip // ======================================================================== describe('SecureChannel processMessage SignAndEncrypt', function () { it('round-trips buildMessage and processMessage in SignAndEncrypt mode', function () { // Generate certs $privKey = openssl_pkey_new(['private_key_bits' => 2048, 'private_key_type' => OPENSSL_KEYTYPE_RSA]); $csr = openssl_csr_new(['CN' => 'test'], $privKey); $cert = openssl_csr_sign($csr, null, $privKey, 365); openssl_x509_export($cert, $certPem); $cm = new CertificateManager(); $tmpFile = tempnam(sys_get_temp_dir(), 'opcua_rt_cert_'); file_put_contents($tmpFile, $certPem); $certDer = $cm->loadCertificatePem($tmpFile); unlink($tmpFile); $policy = SecurityPolicy::Basic256Sha256; $mode = SecurityMode::SignAndEncrypt; // Create "client" channel $clientChannel = new SecureChannel($policy, $mode, $certDer, $privKey, $certDer); $clientChannel->createOpenSecureChannelMessage(); $clientNonce = $clientChannel->getClientNonce(); $serverNonce = random_bytes(32); // Process OPN response to derive keys $response = buildEncryptedOPNResponse( $certDer, $privKey, $certDer, $privKey, $clientNonce, $serverNonce, 100, 200, $policy, ); $clientChannel->processOpenSecureChannelResponse($response); // Create a "server" channel with the SAME keys but swapped roles // Client keys are used for sending, server keys for receiving // The server would use the same nonces but swapped: deriveKeys(clientNonce, serverNonce) for its sending // But for simplicity, we just verify the client's buildMessage produces valid output // by checking the structure $inner = new BinaryEncoder(); $inner->writeNodeId(NodeId::numeric(0, 631)); $inner->writeNodeId(NodeId::numeric(0, 0)); $inner->writeInt64(0); $inner->writeUInt32(42); $inner->writeUInt32(0); $inner->writeString(null); $inner->writeUInt32(10000); $inner->writeNodeId(NodeId::numeric(0, 0)); $inner->writeByte(0); $message = $clientChannel->buildMessage($inner->getBuffer()); // Verify the message structure expect(substr($message, 0, 3))->toBe('MSG'); $decoder = new BinaryDecoder($message); $header = MessageHeader::decode($decoder); expect($header->getMessageSize())->toBe(strlen($message)); // The body should be encrypted (different from plaintext) // Total size should be > the plaintext since it includes padding + signature + encryption overhead expect(strlen($message))->toBeGreaterThan(strlen($inner->getBuffer()) + 50); }); });