From 07a5f8f24f0d93c3d6300103bc6430223a721b8b Mon Sep 17 00:00:00 2001 From: Martin Regen Date: Thu, 18 Apr 2024 10:15:21 +0200 Subject: [PATCH 01/83] first binarydecoder --- .../Fuzz/BinaryDecoder.Fuzz.csproj | 18 ++ Fuzzing/BinaryDecoder/Fuzz/FuzzTarget.cs | 59 +++++ Fuzzing/BinaryDecoder/Fuzz/Program.cs | 16 ++ .../Fuzz/Testcases/readrequest.bin | Bin 0 -> 139 bytes .../Fuzz/Testcases/readresponse.bin | Bin 0 -> 255 bytes .../Fuzztools/BinaryDecoder.Fuzztools.csproj | 27 +++ Fuzzing/BinaryDecoder/Fuzztools/Playback.cs | 28 +++ Fuzzing/BinaryDecoder/Fuzztools/Program.cs | 65 ++++++ Fuzzing/BinaryDecoder/Fuzztools/Testcases.cs | 161 ++++++++++++++ Fuzzing/BinaryDecoder/fuzz.sh | 1 + Fuzzing/fuzz.ps1 | 63 ++++++ .../Types/Encoders/BinaryDecoder.cs | 7 +- UA Fuzzing.sln | 208 ++++++++++++++++++ 13 files changed, 650 insertions(+), 3 deletions(-) create mode 100644 Fuzzing/BinaryDecoder/Fuzz/BinaryDecoder.Fuzz.csproj create mode 100644 Fuzzing/BinaryDecoder/Fuzz/FuzzTarget.cs create mode 100644 Fuzzing/BinaryDecoder/Fuzz/Program.cs create mode 100644 Fuzzing/BinaryDecoder/Fuzz/Testcases/readrequest.bin create mode 100644 Fuzzing/BinaryDecoder/Fuzz/Testcases/readresponse.bin create mode 100644 Fuzzing/BinaryDecoder/Fuzztools/BinaryDecoder.Fuzztools.csproj create mode 100644 Fuzzing/BinaryDecoder/Fuzztools/Playback.cs create mode 100644 Fuzzing/BinaryDecoder/Fuzztools/Program.cs create mode 100644 Fuzzing/BinaryDecoder/Fuzztools/Testcases.cs create mode 100644 Fuzzing/BinaryDecoder/fuzz.sh create mode 100644 Fuzzing/fuzz.ps1 create mode 100644 UA Fuzzing.sln diff --git a/Fuzzing/BinaryDecoder/Fuzz/BinaryDecoder.Fuzz.csproj b/Fuzzing/BinaryDecoder/Fuzz/BinaryDecoder.Fuzz.csproj new file mode 100644 index 0000000000..f170a32bdc --- /dev/null +++ b/Fuzzing/BinaryDecoder/Fuzz/BinaryDecoder.Fuzz.csproj @@ -0,0 +1,18 @@ + + + + Exe + net8.0 + BinaryDecoder.Fuzz + + + + + + + + + + + + diff --git a/Fuzzing/BinaryDecoder/Fuzz/FuzzTarget.cs b/Fuzzing/BinaryDecoder/Fuzz/FuzzTarget.cs new file mode 100644 index 0000000000..8390e6de0d --- /dev/null +++ b/Fuzzing/BinaryDecoder/Fuzz/FuzzTarget.cs @@ -0,0 +1,59 @@ + + +using System.IO; +using Opc.Ua; + +namespace BinaryDecoder.Fuzz +{ + public static class FuzzableCode + { + private static ServiceMessageContext messageContext = null; + + public static void FuzzTarget(Stream stream) + { + if (messageContext == null) + { + messageContext = new ServiceMessageContext(); + } + + try + { + using (var decoder = new Opc.Ua.BinaryDecoder(stream, messageContext)) + { + _ = decoder.DecodeMessage(null); + } + } + catch (ServiceResultException) + { + + } + } + } +} + +#if mist + public static class FuzzableCode + { + //public static void FuzzTargetMethod(ReadOnlySpan input) + public static void FuzzTargetMethod(byte[] input) + { + try + { + var messageContext = new ServiceMessageContext(); + using (var decoder = new BinaryDecoder(input, messageContext)) + { + decoder.DecodeMessage(null); + } + } + catch (Exception ex) when (ex is ServiceResultException) + { + // This is an example. You should filter out any + // *expected* exception(s) from your code here, + // but it’s an anti-pattern to catch *all* Exceptions, + // as you might suppress legitimate problems, such as + // your code throwing a NullReferenceException. + } + } + } +#endif + diff --git a/Fuzzing/BinaryDecoder/Fuzz/Program.cs b/Fuzzing/BinaryDecoder/Fuzz/Program.cs new file mode 100644 index 0000000000..3b9334fe40 --- /dev/null +++ b/Fuzzing/BinaryDecoder/Fuzz/Program.cs @@ -0,0 +1,16 @@ + + +using SharpFuzz; + +namespace BinaryDecoder.Fuzz +{ + public static class Program + { + public static void Main(string[] args) + { + Fuzzer.Run(stream => { + FuzzableCode.FuzzTarget(stream); + }); + } + } +} diff --git a/Fuzzing/BinaryDecoder/Fuzz/Testcases/readrequest.bin b/Fuzzing/BinaryDecoder/Fuzz/Testcases/readrequest.bin new file mode 100644 index 0000000000000000000000000000000000000000..49d6ddbeff2b28786893e3fc4b3c6bb94c11cebf GIT binary patch literal 139 ucmZQ%C}(280RMpiLy&=i6)48Y@PZjcLzI9>kO(h^2n&XYAclwpiUDXE?*3VHb@3TgSJc_|DGaSa9BKnai3oSb}x z@cg12pqL3(F#{~>pk}iGXF^CpJoRY%8&+eDF2dH>%156es$IH(S bGK`UlnT3@NU6PSmixDV^=~A% + + + Exe + net8.0 + BinaryDecoder.Fuzztools + + + + + + + + + + + + + + + + + + + + + diff --git a/Fuzzing/BinaryDecoder/Fuzztools/Playback.cs b/Fuzzing/BinaryDecoder/Fuzztools/Playback.cs new file mode 100644 index 0000000000..878b06c7b4 --- /dev/null +++ b/Fuzzing/BinaryDecoder/Fuzztools/Playback.cs @@ -0,0 +1,28 @@ + +using System.IO; +using System; +using BinaryDecoder.Fuzz; + +namespace BinaryDecoder.Fuzztools +{ + public static class Playback + { + public static void Run(string directoryPath) + { + foreach (var crashFile in Directory.EnumerateFiles(directoryPath)) + { + using (var stream = new MemoryStream(File.ReadAllBytes(crashFile))) + { + try + { + FuzzableCode.FuzzTarget(stream); + } + catch (Exception ex) + { + Console.WriteLine("{0}:{1}", ex.GetType().Name, ex.Message); + } + } + }; + } + } +} diff --git a/Fuzzing/BinaryDecoder/Fuzztools/Program.cs b/Fuzzing/BinaryDecoder/Fuzztools/Program.cs new file mode 100644 index 0000000000..55ae258b3d --- /dev/null +++ b/Fuzzing/BinaryDecoder/Fuzztools/Program.cs @@ -0,0 +1,65 @@ + +using System.IO; +using System; +using Mono.Options; +using System.Collections.Generic; + +namespace BinaryDecoder.Fuzztools +{ + public static class Program + { + public static readonly string ApplicationName = "BinaryDecoder.Fuzztools"; + public static readonly string DefaultTestcasesFolder = "../../../../Fuzz/Testcases"; + public static readonly string DefaultFindingsCrashFolder = "../../../../findings/crashes"; + + public static void Main(string[] args) + { + var applicationName = "BinaryDecoder.Fuzztools"; + TextWriter output = Console.Out; + + output.WriteLine($"OPC UA {applicationName}"); + var usage = $"Usage: {applicationName}.exe [OPTIONS]"; + + bool showHelp = false; + bool playback = false; + bool testcases = false; + + OptionSet options = new OptionSet { + usage, + { "h|help", "show this message and exit", h => showHelp = h != null }, + { "p|playback", "playback crashes found in findings", p => playback = p != null }, + { "t|testcases", "create test cases for fuzzing", t => testcases = t != null }, + }; + + IList extraArgs = null; + try + { + extraArgs = options.Parse(args); + } + catch (OptionException e) + { + output.WriteLine(e.Message); + showHelp = true; + } + + if (testcases) + { + Testcases.Run(DefaultTestcasesFolder); + } + else if (playback) + { + Playback.Run(DefaultFindingsCrashFolder); + } + else + { + showHelp = true; + } + + if (showHelp) + { + options.WriteOptionDescriptions(output); + } + + } + } +} diff --git a/Fuzzing/BinaryDecoder/Fuzztools/Testcases.cs b/Fuzzing/BinaryDecoder/Fuzztools/Testcases.cs new file mode 100644 index 0000000000..f1b8a0b674 --- /dev/null +++ b/Fuzzing/BinaryDecoder/Fuzztools/Testcases.cs @@ -0,0 +1,161 @@ + +using System; +using System.IO; +using BinaryDecoder.Fuzz; +using Opc.Ua; + +namespace BinaryDecoder.Fuzztools +{ + + public static class Testcases + { + private static ServiceMessageContext s_messageContext = new ServiceMessageContext(); + + private static byte[] CreateReadRequest() + { + using (var encoder = new BinaryEncoder(s_messageContext)) + { + var nodeId = new NodeId(1000); + var readRequest = new ReadRequest { + NodesToRead = new ReadValueIdCollection { + new ReadValueId { + NodeId = nodeId, + AttributeId = Attributes.Description, + }, + new ReadValueId { + NodeId = nodeId, + AttributeId = Attributes.Value, + }, + new ReadValueId { + NodeId = nodeId, + AttributeId = Attributes.DisplayName, + }, + new ReadValueId { + NodeId = nodeId, + AttributeId = Attributes.AccessLevel, + }, + new ReadValueId { + NodeId = nodeId, + AttributeId = Attributes.RolePermissions, + }, + }, + }; + encoder.EncodeMessage(readRequest); + return encoder.CloseAndReturnBuffer(); + } + } + + private static byte[] CreateReadResponse() + { + var now = DateTime.UtcNow; + using (var encoder = new BinaryEncoder(s_messageContext)) + { + var nodeId = new NodeId(1000); + var readRequest = new ReadResponse { + Results = new DataValueCollection { + new DataValue { + Value = new Variant("Hello World"), + ServerTimestamp = now, + SourceTimestamp = now.AddMinutes(1), + ServerPicoseconds = 100, + SourcePicoseconds = 10, + StatusCode = StatusCodes.Good, + }, + new DataValue { + Value = new Variant((uint)12345678), + ServerTimestamp = now, + SourceTimestamp = now.AddMinutes(1), + StatusCode = StatusCodes.BadDataLost, + }, + new DataValue { + Value = new Variant(new byte[] { 0,1,2,3,4,5,6}), + ServerTimestamp = now, + SourceTimestamp = now.AddMinutes(1), + StatusCode = StatusCodes.Good, + }, + new DataValue { + Value = new Variant((byte)42), + }, + }, + DiagnosticInfos = new DiagnosticInfoCollection { + new DiagnosticInfo { + AdditionalInfo = "Hello World", + InnerStatusCode = StatusCodes.BadCertificateHostNameInvalid, + InnerDiagnosticInfo = new DiagnosticInfo { + AdditionalInfo = "Hello World", + InnerStatusCode = StatusCodes.BadNodeIdUnknown, + }, + }, + }, + ResponseHeader = new ResponseHeader { + Timestamp = DateTime.UtcNow, + RequestHandle = 42, + ServiceResult = StatusCodes.Good, + ServiceDiagnostics = new DiagnosticInfo { + AdditionalInfo = "NodeId not found", + InnerStatusCode = StatusCodes.BadNodeIdExists, + InnerDiagnosticInfo = new DiagnosticInfo { + AdditionalInfo = "Hello World", + InnerStatusCode = StatusCodes.BadNodeIdUnknown, + InnerDiagnosticInfo = new DiagnosticInfo { + AdditionalInfo = "Hello World", + InnerStatusCode = StatusCodes.BadNodeIdUnknown, + InnerDiagnosticInfo = new DiagnosticInfo { + AdditionalInfo = "Hello World", + InnerStatusCode = StatusCodes.BadNodeIdUnknown, + }, + }, + }, + }, + }, + }; + encoder.EncodeMessage(readRequest); + return encoder.CloseAndReturnBuffer(); + } + } + + public static void Run(string directoryPath) + { + var readRequest = CreateReadRequest(); + FuzzTestcase(readRequest); + File.WriteAllBytes(Path.Combine(directoryPath, "readrequest.bin"), readRequest); + var readResponse = CreateReadResponse(); + FuzzTestcase(readResponse); + File.WriteAllBytes(Path.Combine(directoryPath, "readresponse.bin"), readResponse); + } + + public static void FuzzTestcase(byte[] message) + { + using (var stream = new MemoryStream(message)) + { + FuzzableCode.FuzzTarget(stream); + } + } + } + +#if mist + public static class FuzzableCode + { + //public static void FuzzTargetMethod(ReadOnlySpan input) + public static void FuzzTargetMethod(byte[] input) + { + try + { + var messageContext = new ServiceMessageContext(); + using (var decoder = new BinaryDecoder(input, messageContext)) + { + decoder.DecodeMessage(null); + } + } + catch (Exception ex) when (ex is ServiceResultException) + { + // This is an example. You should filter out any + // *expected* exception(s) from your code here, + // but it’s an anti-pattern to catch *all* Exceptions, + // as you might suppress legitimate problems, such as + // your code throwing a NullReferenceException. + } + } + } +#endif +} diff --git a/Fuzzing/BinaryDecoder/fuzz.sh b/Fuzzing/BinaryDecoder/fuzz.sh new file mode 100644 index 0000000000..de3c03ffcf --- /dev/null +++ b/Fuzzing/BinaryDecoder/fuzz.sh @@ -0,0 +1 @@ +pwsh ../fuzz.ps1 ./Fuzz/BinaryDecoder.Fuzz.csproj -i ./Fuzz/Testcases \ No newline at end of file diff --git a/Fuzzing/fuzz.ps1 b/Fuzzing/fuzz.ps1 new file mode 100644 index 0000000000..3e5d79be27 --- /dev/null +++ b/Fuzzing/fuzz.ps1 @@ -0,0 +1,63 @@ +param ( + [Parameter(Mandatory = $true)] + [string]$project, + [Parameter(Mandatory = $true)] + [string]$i, + [string]$x = $null, + [int]$t = 10000, + [string]$command = "sharpfuzz" +) + +Set-StrictMode -Version Latest + +$outputDir = "bin" +$findingsDir = "findings" + +if (Test-Path $outputDir) { + Remove-Item -Recurse -Force $outputDir +} + +if (Test-Path $findingsDir) { + Remove-Item -Recurse -Force $findingsDir +} + +dotnet publish $project -c release -o $outputDir + +$projectName = (Get-Item $project).BaseName +$projectDll = "$projectName.dll" +$project = Join-Path $outputDir $projectDll + +$exclusions = @( + "dnlib.dll", + "SharpFuzz.dll", + "SharpFuzz.Common.dll", + $projectDll +) + +$fuzzingTargets = Get-ChildItem $outputDir -Filter *.dll ` +| Where-Object { $_.Name -notin $exclusions } ` +| Where-Object { $_.Name -notlike "System.*.dll" } + +if (($fuzzingTargets | Measure-Object).Count -eq 0) { + Write-Error "No fuzzing targets found" + exit 1 +} + +foreach ($fuzzingTarget in $fuzzingTargets) { + Write-Output "Instrumenting $fuzzingTarget" + & $command $fuzzingTarget + + if ($LastExitCode -ne 0) { + Write-Error "An error occurred while instrumenting $fuzzingTarget" + exit 1 + } +} + +$env:AFL_SKIP_BIN_CHECK = 1 + +if ($x) { + afl-fuzz -i $i -o $findingsDir -t $t -m none -x $x dotnet $project +} +else { + afl-fuzz -i $i -o $findingsDir -t $t -m none dotnet $project +} \ No newline at end of file diff --git a/Stack/Opc.Ua.Core/Types/Encoders/BinaryDecoder.cs b/Stack/Opc.Ua.Core/Types/Encoders/BinaryDecoder.cs index 11a0231eba..91af1e04aa 100644 --- a/Stack/Opc.Ua.Core/Types/Encoders/BinaryDecoder.cs +++ b/Stack/Opc.Ua.Core/Types/Encoders/BinaryDecoder.cs @@ -213,7 +213,7 @@ public static IEncodeable DecodeMessage(byte[] buffer, System.Type expectedType, /// public IEncodeable DecodeMessage(System.Type expectedType) { - long start = m_istrm.Position; + long start = m_istrm.CanSeek ? m_istrm.Position : 0; // read the node id. NodeId typeId = ReadNodeId(null); @@ -233,13 +233,14 @@ public IEncodeable DecodeMessage(System.Type expectedType) IEncodeable message = ReadEncodeable(null, actualType, absoluteId); // check that the max message size was not exceeded. - if (m_context.MaxMessageSize > 0 && m_context.MaxMessageSize < (int)(m_istrm.Position - start)) + int messageLength = m_istrm.CanSeek ? (int)(m_istrm.Position - start) : 0; + if (m_context.MaxMessageSize > 0 && m_context.MaxMessageSize < messageLength) { throw ServiceResultException.Create( StatusCodes.BadEncodingLimitsExceeded, "MaxMessageSize {0} < {1}", m_context.MaxMessageSize, - (int)(m_istrm.Position - start)); + messageLength); } // return the message. diff --git a/UA Fuzzing.sln b/UA Fuzzing.sln new file mode 100644 index 0000000000..b5eac719b5 --- /dev/null +++ b/UA Fuzzing.sln @@ -0,0 +1,208 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.0.31903.59 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Opc.Ua.Server", "Libraries\Opc.Ua.Server\Opc.Ua.Server.csproj", "{6C449A1E-244E-4515-9949-A7E22012537C}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{857A6774-6F49-4220-AC55-D53D60FCCB3B}" + ProjectSection(SolutionItems) = preProject + .editorconfig = .editorconfig + .gitattributes = .gitattributes + .gitignore = .gitignore + azure-pipelines-preview.yml = azure-pipelines-preview.yml + azure-pipelines.yml = azure-pipelines.yml + Tests\benchmarks.cmd = Tests\benchmarks.cmd + Tests\codecoverage.cmd = Tests\codecoverage.cmd + Tests\codecoverage.sh = Tests\codecoverage.sh + common.props = common.props + Tests\coverlet.runsettings.xml = Tests\coverlet.runsettings.xml + Directory.Build.props = Directory.Build.props + README.md = README.md + targets.props = targets.props + version.json = version.json + version.props = version.props + EndProjectSection +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "nuget", "nuget", "{F69ABCC3-ED93-446B-AC70-D6AA5F047781}" + ProjectSection(SolutionItems) = preProject + nuget\Opc.Ua.Client.ComplexTypes.Symbols.nuspec = nuget\Opc.Ua.Client.ComplexTypes.Symbols.nuspec + nuget\Opc.Ua.nuspec = nuget\Opc.Ua.nuspec + nuget\Opc.Ua.Symbols.nuspec = nuget\Opc.Ua.Symbols.nuspec + EndProjectSection +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "azurepipelines", "azurepipelines", "{1C25BE72-C337-42AE-9F7C-D6B45F7B7079}" + ProjectSection(SolutionItems) = preProject + .azurepipelines\ci.yml = .azurepipelines\ci.yml + .azurepipelines\get-matrix.ps1 = .azurepipelines\get-matrix.ps1 + .azurepipelines\get-root.ps1 = .azurepipelines\get-root.ps1 + .azurepipelines\get-version.ps1 = .azurepipelines\get-version.ps1 + .azurepipelines\preview.yml = .azurepipelines\preview.yml + .azurepipelines\set-version.ps1 = .azurepipelines\set-version.ps1 + .azurepipelines\sln.yml = .azurepipelines\sln.yml + .azurepipelines\test.yml = .azurepipelines\test.yml + .azurepipelines\testcc.yml = .azurepipelines\testcc.yml + EndProjectSection +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Opc.Ua.Core", "Stack\Opc.Ua.Core\Opc.Ua.Core.csproj", "{D918C0F6-39BD-4ED0-8323-E5A2EB9A85DA}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Opc.Ua.Client", "Libraries\Opc.Ua.Client\Opc.Ua.Client.csproj", "{15100583-BFEF-431B-A1EA-1E5A843A39B1}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Opc.Ua.Configuration", "Libraries\Opc.Ua.Configuration\Opc.Ua.Configuration.csproj", "{A39614CA-8EBE-4408-A9D2-38C7E5AE2A1D}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Opc.Ua.Client.ComplexTypes", "Libraries\Opc.Ua.Client.ComplexTypes\Opc.Ua.Client.ComplexTypes.csproj", "{FE9EEB39-0698-4A19-B770-E66836CE0002}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Opc.Ua.Bindings.Https", "Stack\Opc.Ua.Bindings.Https\Opc.Ua.Bindings.Https.csproj", "{92D98D3D-2A7D-4F4B-8C72-1A98908D0221}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Opc.Ua.Security.Certificates", "Libraries\Opc.Ua.Security.Certificates\Opc.Ua.Security.Certificates.csproj", "{4B72937F-5A57-4CEA-B9FE-B4C45CF7B284}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Libraries", "Libraries", "{ACBF012E-08A7-4939-88CF-D6B610E35AC5}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Stack", "Stack", "{2DC9F7F3-6698-4875-88A3-50678170A810}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "BinaryDecoder", "BinaryDecoder", "{128EE105-0A7A-4AB3-81E6-A77AE638B33F}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "BinaryDecoder.Fuzz", "Fuzzing\BinaryDecoder\Fuzz\BinaryDecoder.Fuzz.csproj", "{92CC62D7-B7B6-4FBD-B9AC-181BD0D11EC0}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "BinaryDecoder.Fuzztools", "Fuzzing\BinaryDecoder\Fuzztools\BinaryDecoder.Fuzztools.csproj", "{352C998B-D884-4C25-BCE3-8AFBF371C380}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Debug|x64 = Debug|x64 + Debug|x86 = Debug|x86 + Release|Any CPU = Release|Any CPU + Release|x64 = Release|x64 + Release|x86 = Release|x86 + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {6C449A1E-244E-4515-9949-A7E22012537C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {6C449A1E-244E-4515-9949-A7E22012537C}.Debug|Any CPU.Build.0 = Debug|Any CPU + {6C449A1E-244E-4515-9949-A7E22012537C}.Debug|Any CPU.Deploy.0 = Debug|Any CPU + {6C449A1E-244E-4515-9949-A7E22012537C}.Debug|x64.ActiveCfg = Debug|Any CPU + {6C449A1E-244E-4515-9949-A7E22012537C}.Debug|x64.Build.0 = Debug|Any CPU + {6C449A1E-244E-4515-9949-A7E22012537C}.Debug|x86.ActiveCfg = Debug|Any CPU + {6C449A1E-244E-4515-9949-A7E22012537C}.Debug|x86.Build.0 = Debug|Any CPU + {6C449A1E-244E-4515-9949-A7E22012537C}.Release|Any CPU.ActiveCfg = Release|Any CPU + {6C449A1E-244E-4515-9949-A7E22012537C}.Release|Any CPU.Build.0 = Release|Any CPU + {6C449A1E-244E-4515-9949-A7E22012537C}.Release|x64.ActiveCfg = Release|Any CPU + {6C449A1E-244E-4515-9949-A7E22012537C}.Release|x64.Build.0 = Release|Any CPU + {6C449A1E-244E-4515-9949-A7E22012537C}.Release|x86.ActiveCfg = Release|Any CPU + {6C449A1E-244E-4515-9949-A7E22012537C}.Release|x86.Build.0 = Release|Any CPU + {D918C0F6-39BD-4ED0-8323-E5A2EB9A85DA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {D918C0F6-39BD-4ED0-8323-E5A2EB9A85DA}.Debug|Any CPU.Build.0 = Debug|Any CPU + {D918C0F6-39BD-4ED0-8323-E5A2EB9A85DA}.Debug|x64.ActiveCfg = Debug|Any CPU + {D918C0F6-39BD-4ED0-8323-E5A2EB9A85DA}.Debug|x64.Build.0 = Debug|Any CPU + {D918C0F6-39BD-4ED0-8323-E5A2EB9A85DA}.Debug|x86.ActiveCfg = Debug|Any CPU + {D918C0F6-39BD-4ED0-8323-E5A2EB9A85DA}.Debug|x86.Build.0 = Debug|Any CPU + {D918C0F6-39BD-4ED0-8323-E5A2EB9A85DA}.Release|Any CPU.ActiveCfg = Release|Any CPU + {D918C0F6-39BD-4ED0-8323-E5A2EB9A85DA}.Release|Any CPU.Build.0 = Release|Any CPU + {D918C0F6-39BD-4ED0-8323-E5A2EB9A85DA}.Release|x64.ActiveCfg = Release|Any CPU + {D918C0F6-39BD-4ED0-8323-E5A2EB9A85DA}.Release|x64.Build.0 = Release|Any CPU + {D918C0F6-39BD-4ED0-8323-E5A2EB9A85DA}.Release|x86.ActiveCfg = Release|Any CPU + {D918C0F6-39BD-4ED0-8323-E5A2EB9A85DA}.Release|x86.Build.0 = Release|Any CPU + {15100583-BFEF-431B-A1EA-1E5A843A39B1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {15100583-BFEF-431B-A1EA-1E5A843A39B1}.Debug|Any CPU.Build.0 = Debug|Any CPU + {15100583-BFEF-431B-A1EA-1E5A843A39B1}.Debug|x64.ActiveCfg = Debug|Any CPU + {15100583-BFEF-431B-A1EA-1E5A843A39B1}.Debug|x64.Build.0 = Debug|Any CPU + {15100583-BFEF-431B-A1EA-1E5A843A39B1}.Debug|x86.ActiveCfg = Debug|Any CPU + {15100583-BFEF-431B-A1EA-1E5A843A39B1}.Debug|x86.Build.0 = Debug|Any CPU + {15100583-BFEF-431B-A1EA-1E5A843A39B1}.Release|Any CPU.ActiveCfg = Release|Any CPU + {15100583-BFEF-431B-A1EA-1E5A843A39B1}.Release|Any CPU.Build.0 = Release|Any CPU + {15100583-BFEF-431B-A1EA-1E5A843A39B1}.Release|x64.ActiveCfg = Release|Any CPU + {15100583-BFEF-431B-A1EA-1E5A843A39B1}.Release|x64.Build.0 = Release|Any CPU + {15100583-BFEF-431B-A1EA-1E5A843A39B1}.Release|x86.ActiveCfg = Release|Any CPU + {15100583-BFEF-431B-A1EA-1E5A843A39B1}.Release|x86.Build.0 = Release|Any CPU + {A39614CA-8EBE-4408-A9D2-38C7E5AE2A1D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {A39614CA-8EBE-4408-A9D2-38C7E5AE2A1D}.Debug|Any CPU.Build.0 = Debug|Any CPU + {A39614CA-8EBE-4408-A9D2-38C7E5AE2A1D}.Debug|x64.ActiveCfg = Debug|Any CPU + {A39614CA-8EBE-4408-A9D2-38C7E5AE2A1D}.Debug|x64.Build.0 = Debug|Any CPU + {A39614CA-8EBE-4408-A9D2-38C7E5AE2A1D}.Debug|x86.ActiveCfg = Debug|Any CPU + {A39614CA-8EBE-4408-A9D2-38C7E5AE2A1D}.Debug|x86.Build.0 = Debug|Any CPU + {A39614CA-8EBE-4408-A9D2-38C7E5AE2A1D}.Release|Any CPU.ActiveCfg = Release|Any CPU + {A39614CA-8EBE-4408-A9D2-38C7E5AE2A1D}.Release|Any CPU.Build.0 = Release|Any CPU + {A39614CA-8EBE-4408-A9D2-38C7E5AE2A1D}.Release|x64.ActiveCfg = Release|Any CPU + {A39614CA-8EBE-4408-A9D2-38C7E5AE2A1D}.Release|x64.Build.0 = Release|Any CPU + {A39614CA-8EBE-4408-A9D2-38C7E5AE2A1D}.Release|x86.ActiveCfg = Release|Any CPU + {A39614CA-8EBE-4408-A9D2-38C7E5AE2A1D}.Release|x86.Build.0 = Release|Any CPU + {FE9EEB39-0698-4A19-B770-E66836CE0002}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {FE9EEB39-0698-4A19-B770-E66836CE0002}.Debug|Any CPU.Build.0 = Debug|Any CPU + {FE9EEB39-0698-4A19-B770-E66836CE0002}.Debug|x64.ActiveCfg = Debug|Any CPU + {FE9EEB39-0698-4A19-B770-E66836CE0002}.Debug|x64.Build.0 = Debug|Any CPU + {FE9EEB39-0698-4A19-B770-E66836CE0002}.Debug|x86.ActiveCfg = Debug|Any CPU + {FE9EEB39-0698-4A19-B770-E66836CE0002}.Debug|x86.Build.0 = Debug|Any CPU + {FE9EEB39-0698-4A19-B770-E66836CE0002}.Release|Any CPU.ActiveCfg = Release|Any CPU + {FE9EEB39-0698-4A19-B770-E66836CE0002}.Release|Any CPU.Build.0 = Release|Any CPU + {FE9EEB39-0698-4A19-B770-E66836CE0002}.Release|x64.ActiveCfg = Release|Any CPU + {FE9EEB39-0698-4A19-B770-E66836CE0002}.Release|x64.Build.0 = Release|Any CPU + {FE9EEB39-0698-4A19-B770-E66836CE0002}.Release|x86.ActiveCfg = Release|Any CPU + {FE9EEB39-0698-4A19-B770-E66836CE0002}.Release|x86.Build.0 = Release|Any CPU + {92D98D3D-2A7D-4F4B-8C72-1A98908D0221}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {92D98D3D-2A7D-4F4B-8C72-1A98908D0221}.Debug|Any CPU.Build.0 = Debug|Any CPU + {92D98D3D-2A7D-4F4B-8C72-1A98908D0221}.Debug|x64.ActiveCfg = Debug|Any CPU + {92D98D3D-2A7D-4F4B-8C72-1A98908D0221}.Debug|x64.Build.0 = Debug|Any CPU + {92D98D3D-2A7D-4F4B-8C72-1A98908D0221}.Debug|x86.ActiveCfg = Debug|Any CPU + {92D98D3D-2A7D-4F4B-8C72-1A98908D0221}.Debug|x86.Build.0 = Debug|Any CPU + {92D98D3D-2A7D-4F4B-8C72-1A98908D0221}.Release|Any CPU.ActiveCfg = Release|Any CPU + {92D98D3D-2A7D-4F4B-8C72-1A98908D0221}.Release|Any CPU.Build.0 = Release|Any CPU + {92D98D3D-2A7D-4F4B-8C72-1A98908D0221}.Release|x64.ActiveCfg = Release|Any CPU + {92D98D3D-2A7D-4F4B-8C72-1A98908D0221}.Release|x64.Build.0 = Release|Any CPU + {92D98D3D-2A7D-4F4B-8C72-1A98908D0221}.Release|x86.ActiveCfg = Release|Any CPU + {92D98D3D-2A7D-4F4B-8C72-1A98908D0221}.Release|x86.Build.0 = Release|Any CPU + {4B72937F-5A57-4CEA-B9FE-B4C45CF7B284}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {4B72937F-5A57-4CEA-B9FE-B4C45CF7B284}.Debug|Any CPU.Build.0 = Debug|Any CPU + {4B72937F-5A57-4CEA-B9FE-B4C45CF7B284}.Debug|x64.ActiveCfg = Debug|Any CPU + {4B72937F-5A57-4CEA-B9FE-B4C45CF7B284}.Debug|x64.Build.0 = Debug|Any CPU + {4B72937F-5A57-4CEA-B9FE-B4C45CF7B284}.Debug|x86.ActiveCfg = Debug|Any CPU + {4B72937F-5A57-4CEA-B9FE-B4C45CF7B284}.Debug|x86.Build.0 = Debug|Any CPU + {4B72937F-5A57-4CEA-B9FE-B4C45CF7B284}.Release|Any CPU.ActiveCfg = Release|Any CPU + {4B72937F-5A57-4CEA-B9FE-B4C45CF7B284}.Release|Any CPU.Build.0 = Release|Any CPU + {4B72937F-5A57-4CEA-B9FE-B4C45CF7B284}.Release|x64.ActiveCfg = Release|Any CPU + {4B72937F-5A57-4CEA-B9FE-B4C45CF7B284}.Release|x64.Build.0 = Release|Any CPU + {4B72937F-5A57-4CEA-B9FE-B4C45CF7B284}.Release|x86.ActiveCfg = Release|Any CPU + {4B72937F-5A57-4CEA-B9FE-B4C45CF7B284}.Release|x86.Build.0 = Release|Any CPU + {92CC62D7-B7B6-4FBD-B9AC-181BD0D11EC0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {92CC62D7-B7B6-4FBD-B9AC-181BD0D11EC0}.Debug|Any CPU.Build.0 = Debug|Any CPU + {92CC62D7-B7B6-4FBD-B9AC-181BD0D11EC0}.Debug|x64.ActiveCfg = Debug|Any CPU + {92CC62D7-B7B6-4FBD-B9AC-181BD0D11EC0}.Debug|x64.Build.0 = Debug|Any CPU + {92CC62D7-B7B6-4FBD-B9AC-181BD0D11EC0}.Debug|x86.ActiveCfg = Debug|Any CPU + {92CC62D7-B7B6-4FBD-B9AC-181BD0D11EC0}.Debug|x86.Build.0 = Debug|Any CPU + {92CC62D7-B7B6-4FBD-B9AC-181BD0D11EC0}.Release|Any CPU.ActiveCfg = Release|Any CPU + {92CC62D7-B7B6-4FBD-B9AC-181BD0D11EC0}.Release|Any CPU.Build.0 = Release|Any CPU + {92CC62D7-B7B6-4FBD-B9AC-181BD0D11EC0}.Release|x64.ActiveCfg = Release|Any CPU + {92CC62D7-B7B6-4FBD-B9AC-181BD0D11EC0}.Release|x64.Build.0 = Release|Any CPU + {92CC62D7-B7B6-4FBD-B9AC-181BD0D11EC0}.Release|x86.ActiveCfg = Release|Any CPU + {92CC62D7-B7B6-4FBD-B9AC-181BD0D11EC0}.Release|x86.Build.0 = Release|Any CPU + {352C998B-D884-4C25-BCE3-8AFBF371C380}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {352C998B-D884-4C25-BCE3-8AFBF371C380}.Debug|Any CPU.Build.0 = Debug|Any CPU + {352C998B-D884-4C25-BCE3-8AFBF371C380}.Debug|x64.ActiveCfg = Debug|Any CPU + {352C998B-D884-4C25-BCE3-8AFBF371C380}.Debug|x64.Build.0 = Debug|Any CPU + {352C998B-D884-4C25-BCE3-8AFBF371C380}.Debug|x86.ActiveCfg = Debug|Any CPU + {352C998B-D884-4C25-BCE3-8AFBF371C380}.Debug|x86.Build.0 = Debug|Any CPU + {352C998B-D884-4C25-BCE3-8AFBF371C380}.Release|Any CPU.ActiveCfg = Release|Any CPU + {352C998B-D884-4C25-BCE3-8AFBF371C380}.Release|Any CPU.Build.0 = Release|Any CPU + {352C998B-D884-4C25-BCE3-8AFBF371C380}.Release|x64.ActiveCfg = Release|Any CPU + {352C998B-D884-4C25-BCE3-8AFBF371C380}.Release|x64.Build.0 = Release|Any CPU + {352C998B-D884-4C25-BCE3-8AFBF371C380}.Release|x86.ActiveCfg = Release|Any CPU + {352C998B-D884-4C25-BCE3-8AFBF371C380}.Release|x86.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(NestedProjects) = preSolution + {6C449A1E-244E-4515-9949-A7E22012537C} = {ACBF012E-08A7-4939-88CF-D6B610E35AC5} + {F69ABCC3-ED93-446B-AC70-D6AA5F047781} = {857A6774-6F49-4220-AC55-D53D60FCCB3B} + {1C25BE72-C337-42AE-9F7C-D6B45F7B7079} = {857A6774-6F49-4220-AC55-D53D60FCCB3B} + {D918C0F6-39BD-4ED0-8323-E5A2EB9A85DA} = {2DC9F7F3-6698-4875-88A3-50678170A810} + {15100583-BFEF-431B-A1EA-1E5A843A39B1} = {ACBF012E-08A7-4939-88CF-D6B610E35AC5} + {A39614CA-8EBE-4408-A9D2-38C7E5AE2A1D} = {ACBF012E-08A7-4939-88CF-D6B610E35AC5} + {FE9EEB39-0698-4A19-B770-E66836CE0002} = {ACBF012E-08A7-4939-88CF-D6B610E35AC5} + {92D98D3D-2A7D-4F4B-8C72-1A98908D0221} = {2DC9F7F3-6698-4875-88A3-50678170A810} + {4B72937F-5A57-4CEA-B9FE-B4C45CF7B284} = {ACBF012E-08A7-4939-88CF-D6B610E35AC5} + {92CC62D7-B7B6-4FBD-B9AC-181BD0D11EC0} = {128EE105-0A7A-4AB3-81E6-A77AE638B33F} + {352C998B-D884-4C25-BCE3-8AFBF371C380} = {128EE105-0A7A-4AB3-81E6-A77AE638B33F} + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {33FA5BCB-C827-4D44-AECF-F51342DFE64A} + EndGlobalSection +EndGlobal From b01eb1f74583333726b98d52ddb15ea53f380aff Mon Sep 17 00:00:00 2001 From: Martin Regen Date: Thu, 18 Apr 2024 14:05:56 +0200 Subject: [PATCH 02/83] typos --- .../BinaryDecoder/Fuzztools/BinaryDecoder.Fuzztools.csproj | 4 ---- Fuzzing/Fuzzing.md | 4 ++++ 2 files changed, 4 insertions(+), 4 deletions(-) create mode 100644 Fuzzing/Fuzzing.md diff --git a/Fuzzing/BinaryDecoder/Fuzztools/BinaryDecoder.Fuzztools.csproj b/Fuzzing/BinaryDecoder/Fuzztools/BinaryDecoder.Fuzztools.csproj index 178731d038..757095f41b 100644 --- a/Fuzzing/BinaryDecoder/Fuzztools/BinaryDecoder.Fuzztools.csproj +++ b/Fuzzing/BinaryDecoder/Fuzztools/BinaryDecoder.Fuzztools.csproj @@ -20,8 +20,4 @@ - - - - diff --git a/Fuzzing/Fuzzing.md b/Fuzzing/Fuzzing.md new file mode 100644 index 0000000000..0fbd7b7bc2 --- /dev/null +++ b/Fuzzing/Fuzzing.md @@ -0,0 +1,4 @@ +# Fuzz testing for UA.NET Standard + +This project provides integration of Sharpfuzz with the encoders used by the UA .NET Standard library. + From 8f77d093a34241fd7b6d0d4a5f83c51aa8f39927 Mon Sep 17 00:00:00 2001 From: Martin Regen Date: Thu, 18 Apr 2024 16:19:08 +0200 Subject: [PATCH 03/83] add doc page --- Fuzzing/Fuzzing.md | 62 ++++++++++++++++++++++++++++++++++++++++++++++ UA Fuzzing.sln | 3 +++ 2 files changed, 65 insertions(+) diff --git a/Fuzzing/Fuzzing.md b/Fuzzing/Fuzzing.md index 0fbd7b7bc2..aff4f4ed58 100644 --- a/Fuzzing/Fuzzing.md +++ b/Fuzzing/Fuzzing.md @@ -2,3 +2,65 @@ This project provides integration of Sharpfuzz with the encoders used by the UA .NET Standard library. +## Installation + +The fuzzing project executes on a linux subsystem. The following steps are required to set up the environment: + +- Install a Windows Subsystem for Linux (WSL) by following the instructions at https://docs.microsoft.com/en-us/windows/wsl/install or by installing e.g. the Ubuntu app from the Microsoft store. +- The full instructions for setting up sharpfuzz can be found at https://github.com/Metalnem/sharpfuzz/blob/master/README.md. + +- Open a terminal and run the following commands to install the required packages to compile afl-fuzz: + +```bash +cd /Fuzzing + +sudo apt-get update + +# Install clang and llvm +sudo apt-get install -y build-essential cmake git + +# Install .NET 8.0 SDK +sudo apt-get install -y dotnet-sdk-8.0 +``` + +To compile and install afl-fuzz and to install sharpfuzz, run the following commands: + +```bash +#/bin/sh +set -eux + +# Download and extract the latest afl-fuzz source package +wget http://lcamtuf.coredump.cx/afl/releases/afl-latest.tgz +tar -xvf afl-latest.tgz + +rm afl-latest.tgz +cd afl-2.52b/ + +# Install afl-fuzz +sudo make install +cd .. +rm -rf afl-2.52b/ + +# Install SharpFuzz.CommandLine global .NET tool +dotnet tool install --global SharpFuzz.CommandLine +``` + +To validate that all required tools are available and working, run the following commands: + +```bash +afl-fuzz --help +sharpfuzz +``` + +## Usage + +To run the fuzzing project, execute the following commands, e.g. for the BinaryDecoder fuzzer: + +```bash +#/bin/sh + +cd BinaryDecoder +./fuzz.sh +``` + +Now the fuzzer is started and will run until it is stopped manually by hitting Ctrl-C. The fuzzer will create a directory `findings` in the fuzzer directory, which contains the test cases that caused the fuzzer to crash. \ No newline at end of file diff --git a/UA Fuzzing.sln b/UA Fuzzing.sln index b5eac719b5..fb09de359e 100644 --- a/UA Fuzzing.sln +++ b/UA Fuzzing.sln @@ -61,6 +61,9 @@ EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Stack", "Stack", "{2DC9F7F3-6698-4875-88A3-50678170A810}" EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "BinaryDecoder", "BinaryDecoder", "{128EE105-0A7A-4AB3-81E6-A77AE638B33F}" + ProjectSection(SolutionItems) = preProject + Fuzzing\Fuzzing.md = Fuzzing\Fuzzing.md + EndProjectSection EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "BinaryDecoder.Fuzz", "Fuzzing\BinaryDecoder\Fuzz\BinaryDecoder.Fuzz.csproj", "{92CC62D7-B7B6-4FBD-B9AC-181BD0D11EC0}" EndProject From fb1c9440d15009720503d29d9ce99734f1217713 Mon Sep 17 00:00:00 2001 From: Martin Regen Date: Fri, 19 Apr 2024 13:03:29 +0200 Subject: [PATCH 04/83] doc update --- Fuzzing/BinaryDecoder/Fuzztools/Playback.cs | 1 + Fuzzing/BinaryDecoder/fuzz.sh | 2 +- Fuzzing/Fuzzing.md | 7 ++- Fuzzing/scripts/fuzz-libfuzzer.ps1 | 58 +++++++++++++++++++++ Fuzzing/{ => scripts}/fuzz.ps1 | 2 +- Fuzzing/scripts/install.sh | 17 ++++++ Fuzzing/scripts/readme.txt | 1 + Fuzzing/scripts/test-libfuzzer.ps1 | 29 +++++++++++ Fuzzing/scripts/test.ps1 | 22 ++++++++ 9 files changed, 135 insertions(+), 4 deletions(-) create mode 100644 Fuzzing/scripts/fuzz-libfuzzer.ps1 rename Fuzzing/{ => scripts}/fuzz.ps1 (99%) create mode 100644 Fuzzing/scripts/install.sh create mode 100644 Fuzzing/scripts/readme.txt create mode 100644 Fuzzing/scripts/test-libfuzzer.ps1 create mode 100644 Fuzzing/scripts/test.ps1 diff --git a/Fuzzing/BinaryDecoder/Fuzztools/Playback.cs b/Fuzzing/BinaryDecoder/Fuzztools/Playback.cs index 878b06c7b4..2c1546bf09 100644 --- a/Fuzzing/BinaryDecoder/Fuzztools/Playback.cs +++ b/Fuzzing/BinaryDecoder/Fuzztools/Playback.cs @@ -20,6 +20,7 @@ public static void Run(string directoryPath) catch (Exception ex) { Console.WriteLine("{0}:{1}", ex.GetType().Name, ex.Message); + Console.WriteLine(ex.StackTrace); } } }; diff --git a/Fuzzing/BinaryDecoder/fuzz.sh b/Fuzzing/BinaryDecoder/fuzz.sh index de3c03ffcf..0fe97053bd 100644 --- a/Fuzzing/BinaryDecoder/fuzz.sh +++ b/Fuzzing/BinaryDecoder/fuzz.sh @@ -1 +1 @@ -pwsh ../fuzz.ps1 ./Fuzz/BinaryDecoder.Fuzz.csproj -i ./Fuzz/Testcases \ No newline at end of file +pwsh ../scripts/fuzz.ps1 ./Fuzz/BinaryDecoder.Fuzz.csproj -i ./Fuzz/Testcases \ No newline at end of file diff --git a/Fuzzing/Fuzzing.md b/Fuzzing/Fuzzing.md index aff4f4ed58..3fce984c7f 100644 --- a/Fuzzing/Fuzzing.md +++ b/Fuzzing/Fuzzing.md @@ -19,10 +19,13 @@ sudo apt-get update # Install clang and llvm sudo apt-get install -y build-essential cmake git -# Install .NET 8.0 SDK -sudo apt-get install -y dotnet-sdk-8.0 +# Install .NET 8.0 SDK on Ubuntu +sudo apt-get install -y dotnet-sdk-8.0 powershell ``` +The supplied scripts require powershell on Linux to be installed. +See https://learn.microsoft.com/en-us/powershell/scripting/install/install-ubuntu?view=powershell-7.4 + To compile and install afl-fuzz and to install sharpfuzz, run the following commands: ```bash diff --git a/Fuzzing/scripts/fuzz-libfuzzer.ps1 b/Fuzzing/scripts/fuzz-libfuzzer.ps1 new file mode 100644 index 0000000000..f27fc906ca --- /dev/null +++ b/Fuzzing/scripts/fuzz-libfuzzer.ps1 @@ -0,0 +1,58 @@ +param ( + [Parameter(Mandatory = $true)] + [string]$libFuzzer, + [Parameter(Mandatory = $true)] + [string]$project, + [Parameter(Mandatory = $true)] + [string]$corpus, + [string]$dict = $null, + [int]$timeout = 10, + [string]$command = "sharpfuzz" +) + +Set-StrictMode -Version Latest + +$outputDir = "bin" + +if (Test-Path $outputDir) { + Remove-Item -Recurse -Force $outputDir +} + +dotnet publish $project -c release -o $outputDir + +$projectName = (Get-Item $project).BaseName +$projectDll = "$projectName.dll" +$project = Join-Path $outputDir $projectDll + +$exclusions = @( + "dnlib.dll", + "SharpFuzz.dll", + "SharpFuzz.Common.dll", + $projectDll +) + +$fuzzingTargets = Get-ChildItem $outputDir -Filter *.dll ` +| Where-Object { $_.Name -notin $exclusions } ` +| Where-Object { $_.Name -notlike "System.*.dll" } + +if (($fuzzingTargets | Measure-Object).Count -eq 0) { + Write-Error "No fuzzing targets found" + exit 1 +} + +foreach ($fuzzingTarget in $fuzzingTargets) { + Write-Output "Instrumenting $fuzzingTarget" + & $command $fuzzingTarget.FullName + + if ($LastExitCode -ne 0) { + Write-Error "An error occurred while instrumenting $fuzzingTarget" + exit 1 + } +} + +if ($dict) { + & $libFuzzer -timeout="$timeout" -dict="$dict" --target_path=dotnet --target_arg=$project $corpus +} +else { + & $libFuzzer -timeout="$timeout" --target_path=dotnet --target_arg=$project $corpus +} diff --git a/Fuzzing/fuzz.ps1 b/Fuzzing/scripts/fuzz.ps1 similarity index 99% rename from Fuzzing/fuzz.ps1 rename to Fuzzing/scripts/fuzz.ps1 index 3e5d79be27..12909a86ba 100644 --- a/Fuzzing/fuzz.ps1 +++ b/Fuzzing/scripts/fuzz.ps1 @@ -60,4 +60,4 @@ if ($x) { } else { afl-fuzz -i $i -o $findingsDir -t $t -m none dotnet $project -} \ No newline at end of file +} diff --git a/Fuzzing/scripts/install.sh b/Fuzzing/scripts/install.sh new file mode 100644 index 0000000000..dd28e47771 --- /dev/null +++ b/Fuzzing/scripts/install.sh @@ -0,0 +1,17 @@ +#/bin/sh +set -eux + +# Download and extract the latest afl-fuzz source package +wget http://lcamtuf.coredump.cx/afl/releases/afl-latest.tgz +tar -xvf afl-latest.tgz + +rm afl-latest.tgz +cd afl-2.52b/ + +# Install afl-fuzz +sudo make install +cd .. +rm -rf afl-2.52b/ + +# Install SharpFuzz.CommandLine global .NET tool +dotnet tool install --global SharpFuzz.CommandLine diff --git a/Fuzzing/scripts/readme.txt b/Fuzzing/scripts/readme.txt new file mode 100644 index 0000000000..b027025702 --- /dev/null +++ b/Fuzzing/scripts/readme.txt @@ -0,0 +1 @@ +for script updates see https://github.com/Metalnem/sharpfuzz/tree/master/scripts \ No newline at end of file diff --git a/Fuzzing/scripts/test-libfuzzer.ps1 b/Fuzzing/scripts/test-libfuzzer.ps1 new file mode 100644 index 0000000000..db9cb90028 --- /dev/null +++ b/Fuzzing/scripts/test-libfuzzer.ps1 @@ -0,0 +1,29 @@ +$libFuzzer = "libfuzzer-dotnet-windows.exe" +$uri = "https://github.com/metalnem/libfuzzer-dotnet/releases/latest/download/$libFuzzer" +$corpus = "corpus" + +Invoke-WebRequest -Uri $uri -OutFile $libFuzzer +New-Item -Path $corpus -ItemType Directory + +dotnet publish src/SharpFuzz.CommandLine/SharpFuzz.CommandLine.csproj ` + --output out ` + --configuration release ` + --framework net8.0 + +& scripts/fuzz-libfuzzer.ps1 ` + -libFuzzer "./$libFuzzer" ` + -project tests/Library.LibFuzzer/Library.LibFuzzer.csproj ` + -corpus $corpus ` + -command out/SharpFuzz.CommandLine + +$crasher = "Whoopsie" +$output = Get-ChildItem -Path "timeout-*" +$content = Get-Content -Path $output.FullName -Raw + +if (-not $content.Contains($crasher)) { + Write-Error "Crasher is missing from the libFuzzer output" + exit 1 +} + +Write-Host $crasher +exit 0 diff --git a/Fuzzing/scripts/test.ps1 b/Fuzzing/scripts/test.ps1 new file mode 100644 index 0000000000..874b2f1cdc --- /dev/null +++ b/Fuzzing/scripts/test.ps1 @@ -0,0 +1,22 @@ +New-Item -Path "corpus/test" -ItemType File -Force -Value "W" + +dotnet publish src/SharpFuzz.CommandLine/SharpFuzz.CommandLine.csproj ` + --output out ` + --configuration release ` + --framework net8.0 + +& scripts/fuzz.ps1 ` + -project tests/Library.Fuzz/Library.Fuzz.csproj ` + -i corpus ` + -command out/SharpFuzz.CommandLine + +$output = Get-Content -Path "./findings/.cur_input" -Raw +$crasher = "Whoopsie" + +if (-not $output.Contains($crasher)) { + Write-Error "Crasher is missing from the AFL output" + exit 1 +} + +Write-Host $crasher +exit 0 From 51fd276eb1469f663624cd19ad07ab1fd248a290 Mon Sep 17 00:00:00 2001 From: Martin Regen Date: Fri, 19 Apr 2024 19:03:41 +0200 Subject: [PATCH 05/83] fix BinaryDecoder --- .../Stack/State/BaseVariableState.cs | 61 ++- Stack/Opc.Ua.Core/Stack/State/NodeState.cs | 57 +- .../Stack/State/NodeStateCollection.cs | 2 +- .../Stack/Tcp/TcpReverseConnectChannel.cs | 49 +- .../Opc.Ua.Core/Stack/Tcp/TcpServerChannel.cs | 92 ++-- .../Stack/Tcp/UaSCBinaryChannel.Asymmetric.cs | 148 ++--- .../Stack/Tcp/UaSCBinaryChannel.Symmetric.cs | 189 +++---- .../Stack/Tcp/UaSCBinaryChannel.cs | 22 + .../Stack/Tcp/UaSCBinaryClientChannel.cs | 64 ++- .../Types/Encoders/BinaryDecoder.cs | 507 ++++++++++++------ Stack/Opc.Ua.Core/Types/Utils/DataComparer.cs | 8 +- .../Encoding/UadpDataSetMessageTests.cs | 116 ++-- 12 files changed, 767 insertions(+), 548 deletions(-) diff --git a/Stack/Opc.Ua.Core/Stack/State/BaseVariableState.cs b/Stack/Opc.Ua.Core/Stack/State/BaseVariableState.cs index 7290cf834e..40d4d74b00 100644 --- a/Stack/Opc.Ua.Core/Stack/State/BaseVariableState.cs +++ b/Stack/Opc.Ua.Core/Stack/State/BaseVariableState.cs @@ -361,45 +361,54 @@ public static object DecodeExtensionObject(ISystemContext context, Type targetTy return extension.Body; } - if (Activator.CreateInstance(targetType) is IEncodeable instance) { IDecoder decoder = null; - - ServiceMessageContext messageContext = ServiceMessageContext.GlobalContext; - - if (context != null) + try { - messageContext = new ServiceMessageContext(); - messageContext.NamespaceUris = context.NamespaceUris; - messageContext.ServerUris = context.ServerUris; - messageContext.Factory = context.EncodeableFactory; - } + ServiceMessageContext messageContext = ServiceMessageContext.GlobalContext; - if (extension.Encoding == ExtensionObjectEncoding.Binary) - { - decoder = new BinaryDecoder(extension.Body as byte[], messageContext); - } - else if (extension.Encoding == ExtensionObjectEncoding.Xml) - { - decoder = new XmlDecoder(extension.Body as XmlElement, messageContext); - } + if (context != null) + { + messageContext = new ServiceMessageContext(); + messageContext.NamespaceUris = context.NamespaceUris; + messageContext.ServerUris = context.ServerUris; + messageContext.Factory = context.EncodeableFactory; + } - if (decoder != null) - { - try + if (extension.Encoding == ExtensionObjectEncoding.Binary) { - instance.Decode(decoder); - return instance; + decoder = new BinaryDecoder(extension.Body as byte[], messageContext); } - catch (Exception e) + else if (extension.Encoding == ExtensionObjectEncoding.Xml) { - if (throwOnError) + decoder = new XmlDecoder(extension.Body as XmlElement, messageContext); + } + else if (extension.Encoding == ExtensionObjectEncoding.Json) + { + decoder = new JsonDecoder(extension.Body as string, messageContext); + } + + if (decoder != null) + { + try { - throw ServiceResultException.Create(StatusCodes.BadTypeMismatch, "Cannot convert ExtensionObject to {0}. Error = {1}", targetType.Name, e.Message); + instance.Decode(decoder); + return instance; + } + catch (Exception e) + { + if (throwOnError) + { + throw ServiceResultException.Create(StatusCodes.BadTypeMismatch, "Cannot convert ExtensionObject to {0}. Error = {1}", targetType.Name, e.Message); + } } } } + finally + { + Utils.SilentDispose(decoder); + } } if (throwOnError) diff --git a/Stack/Opc.Ua.Core/Stack/State/NodeState.cs b/Stack/Opc.Ua.Core/Stack/State/NodeState.cs index 7979cff430..129afb3f46 100644 --- a/Stack/Opc.Ua.Core/Stack/State/NodeState.cs +++ b/Stack/Opc.Ua.Core/Stack/State/NodeState.cs @@ -120,7 +120,7 @@ public virtual void Initialize(ISystemContext context, string initializationStri { if (initializationString.StartsWith("<", StringComparison.Ordinal)) { - using (System.IO.StringReader reader = new System.IO.StringReader(initializationString)) + using (var reader = new StringReader(initializationString)) { LoadFromXml(context, reader); } @@ -129,7 +129,7 @@ public virtual void Initialize(ISystemContext context, string initializationStri { byte[] bytes = Convert.FromBase64String(initializationString); - using (System.IO.MemoryStream istrm = new MemoryStream(bytes)) + using (var istrm = new MemoryStream(bytes)) { LoadAsBinary(context, istrm); } @@ -650,37 +650,38 @@ public void LoadAsBinary(ISystemContext context, Stream istrm) messageContext.ServerUris = context.ServerUris; messageContext.Factory = context.EncodeableFactory; - BinaryDecoder decoder = new BinaryDecoder(istrm, messageContext); - - // check if a namespace table was provided. - NamespaceTable namespaceUris = new NamespaceTable(); - - if (!decoder.LoadStringTable(namespaceUris)) + using (var decoder = new BinaryDecoder(istrm, messageContext, true)) { - namespaceUris = null; - } + // check if a namespace table was provided. + NamespaceTable namespaceUris = new NamespaceTable(); - // check if a server uri table was provided. - StringTable serverUris = new StringTable(); + if (!decoder.LoadStringTable(namespaceUris)) + { + namespaceUris = null; + } - if (namespaceUris != null && namespaceUris.Count > 1) - { - serverUris.Append(namespaceUris.GetString(1)); - } + // check if a server uri table was provided. + StringTable serverUris = new StringTable(); - if (!decoder.LoadStringTable(serverUris)) - { - serverUris = null; - } + if (namespaceUris != null && namespaceUris.Count > 1) + { + serverUris.Append(namespaceUris.GetString(1)); + } - // setup the mappings to use during decoding. - decoder.SetMappingTables(namespaceUris, serverUris); + if (!decoder.LoadStringTable(serverUris)) + { + serverUris = null; + } - // update the node and children. - AttributesToSave attributesToLoad = (AttributesToSave)decoder.ReadUInt32(null); - Update(context, decoder, attributesToLoad); - UpdateReferences(context, decoder); - UpdateChildren(context, decoder); + // setup the mappings to use during decoding. + decoder.SetMappingTables(namespaceUris, serverUris); + + // update the node and children. + AttributesToSave attributesToLoad = (AttributesToSave)decoder.ReadUInt32(null); + Update(context, decoder, attributesToLoad); + UpdateReferences(context, decoder); + UpdateChildren(context, decoder); + } } #region AttributesToSave Enumeration @@ -1248,7 +1249,7 @@ public void SaveAsXml(ISystemContext context, XmlEncoder encoder) /// The stream to read. public void LoadFromXml(ISystemContext context, TextReader input) { - using (XmlReader reader = XmlReader.Create(input, Utils.DefaultXmlReaderSettings())) + using (var reader = XmlReader.Create(input, Utils.DefaultXmlReaderSettings())) { LoadFromXml(context, reader); } diff --git a/Stack/Opc.Ua.Core/Stack/State/NodeStateCollection.cs b/Stack/Opc.Ua.Core/Stack/State/NodeStateCollection.cs index 12260b9085..efb55290cd 100644 --- a/Stack/Opc.Ua.Core/Stack/State/NodeStateCollection.cs +++ b/Stack/Opc.Ua.Core/Stack/State/NodeStateCollection.cs @@ -265,7 +265,7 @@ public void LoadFromBinary(ISystemContext context, Stream istrm, bool updateTabl messageContext.ServerUris = context.ServerUris; messageContext.Factory = context.EncodeableFactory; - using (BinaryDecoder decoder = new BinaryDecoder(istrm, messageContext)) + using (var decoder = new BinaryDecoder(istrm, messageContext)) { // check if a namespace table was provided. NamespaceTable namespaceUris = new NamespaceTable(); diff --git a/Stack/Opc.Ua.Core/Stack/Tcp/TcpReverseConnectChannel.cs b/Stack/Opc.Ua.Core/Stack/Tcp/TcpReverseConnectChannel.cs index 75e85cdb4b..e624ecc6c5 100644 --- a/Stack/Opc.Ua.Core/Stack/Tcp/TcpReverseConnectChannel.cs +++ b/Stack/Opc.Ua.Core/Stack/Tcp/TcpReverseConnectChannel.cs @@ -95,37 +95,38 @@ private bool ProcessReverseHelloMessage(uint messageType, ArraySegment mes try { - MemoryStream istrm = new MemoryStream(messageChunk.Array, messageChunk.Offset, messageChunk.Count, false); - BinaryDecoder decoder = new BinaryDecoder(istrm, Quotas.MessageContext); - istrm.Seek(TcpMessageLimits.MessageTypeAndSize, SeekOrigin.Current); + using (var decoder = new BinaryDecoder(messageChunk, Quotas.MessageContext)) + { + ReadAndVerifyMessageTypeAndSize(decoder, TcpMessageType.ReverseHello, messageChunk.Count); - // read peer information. - string serverUri = decoder.ReadString(null); - string endpointUrlString = decoder.ReadString(null); - Uri endpointUri = new Uri(endpointUrlString); + // read peer information. + string serverUri = decoder.ReadString(null); + string endpointUrlString = decoder.ReadString(null); + Uri endpointUri = new Uri(endpointUrlString); - State = TcpChannelState.Connecting; + State = TcpChannelState.Connecting; - Task t = Task.Run(async () => { - try - { - if (false == await Listener.TransferListenerChannel(Id, serverUri, endpointUri).ConfigureAwait(false)) + Task t = Task.Run(async () => { + try { - SetResponseRequired(true); - ForceChannelFault(StatusCodes.BadTcpMessageTypeInvalid, "The reverse connection was rejected by the client."); + if (false == await Listener.TransferListenerChannel(Id, serverUri, endpointUri).ConfigureAwait(false)) + { + SetResponseRequired(true); + ForceChannelFault(StatusCodes.BadTcpMessageTypeInvalid, "The reverse connection was rejected by the client."); + } + else + { + // Socket is now owned by client, don't clean up + CleanupTimer(); + } } - else + catch (Exception) { - // Socket is now owned by client, don't clean up - CleanupTimer(); + SetResponseRequired(true); + ForceChannelFault(StatusCodes.BadInternalError, "Internal error approving the reverse connection."); } - } - catch (Exception) - { - SetResponseRequired(true); - ForceChannelFault(StatusCodes.BadInternalError, "Internal error approving the reverse connection."); - } - }); + }); + } } catch (Exception e) { diff --git a/Stack/Opc.Ua.Core/Stack/Tcp/TcpServerChannel.cs b/Stack/Opc.Ua.Core/Stack/Tcp/TcpServerChannel.cs index 3901ddfeb0..e0aee3cd94 100644 --- a/Stack/Opc.Ua.Core/Stack/Tcp/TcpServerChannel.cs +++ b/Stack/Opc.Ua.Core/Stack/Tcp/TcpServerChannel.cs @@ -348,17 +348,23 @@ private bool ProcessHelloMessage(ArraySegment messageChunk) try { - using (MemoryStream istrm = new MemoryStream(messageChunk.Array, messageChunk.Offset, messageChunk.Count, false)) + // read requested buffer sizes. + uint protocolVersion; + uint receiveBufferSize; + uint sendBufferSize; + uint maxMessageSize; + uint maxChunkCount; + + using (var decoder = new BinaryDecoder(messageChunk, Quotas.MessageContext)) { - BinaryDecoder decoder = new BinaryDecoder(istrm, Quotas.MessageContext); - istrm.Seek(TcpMessageLimits.MessageTypeAndSize, SeekOrigin.Current); + ReadAndVerifyMessageTypeAndSize(decoder, TcpMessageType.Hello, messageChunk.Count); // read requested buffer sizes. - uint protocolVersion = decoder.ReadUInt32(null); - uint receiveBufferSize = decoder.ReadUInt32(null); - uint sendBufferSize = decoder.ReadUInt32(null); - uint maxMessageSize = decoder.ReadUInt32(null); - uint maxChunkCount = decoder.ReadUInt32(null); + protocolVersion = decoder.ReadUInt32(null); + receiveBufferSize = decoder.ReadUInt32(null); + sendBufferSize = decoder.ReadUInt32(null); + maxMessageSize = decoder.ReadUInt32(null); + maxChunkCount = decoder.ReadUInt32(null); // read the endpoint url. int length = decoder.ReadInt32(null); @@ -386,51 +392,51 @@ private bool ProcessHelloMessage(ArraySegment messageChunk) } decoder.Close(); + } - // update receive buffer size. - if (receiveBufferSize < ReceiveBufferSize) - { - ReceiveBufferSize = (int)receiveBufferSize; - } - - if (ReceiveBufferSize < TcpMessageLimits.MinBufferSize) - { - ReceiveBufferSize = TcpMessageLimits.MinBufferSize; - } + // update receive buffer size. + if (receiveBufferSize < ReceiveBufferSize) + { + ReceiveBufferSize = (int)receiveBufferSize; + } - // update send buffer size. - if (sendBufferSize < SendBufferSize) - { - SendBufferSize = (int)sendBufferSize; - } + if (ReceiveBufferSize < TcpMessageLimits.MinBufferSize) + { + ReceiveBufferSize = TcpMessageLimits.MinBufferSize; + } - if (SendBufferSize < TcpMessageLimits.MinBufferSize) - { - SendBufferSize = TcpMessageLimits.MinBufferSize; - } + // update send buffer size. + if (sendBufferSize < SendBufferSize) + { + SendBufferSize = (int)sendBufferSize; + } - // update the max message size. - if (maxMessageSize > 0 && maxMessageSize < MaxResponseMessageSize) - { - MaxResponseMessageSize = (int)maxMessageSize; - } + if (SendBufferSize < TcpMessageLimits.MinBufferSize) + { + SendBufferSize = TcpMessageLimits.MinBufferSize; + } - if (MaxResponseMessageSize < SendBufferSize) - { - MaxResponseMessageSize = SendBufferSize; - } + // update the max message size. + if (maxMessageSize > 0 && maxMessageSize < MaxResponseMessageSize) + { + MaxResponseMessageSize = (int)maxMessageSize; + } - // update the max chunk count. - MaxResponseChunkCount = CalculateChunkCount(MaxResponseMessageSize, SendBufferSize); + if (MaxResponseMessageSize < SendBufferSize) + { + MaxResponseMessageSize = SendBufferSize; + } - if (maxChunkCount > 0 && maxChunkCount < MaxResponseChunkCount) - { - MaxResponseChunkCount = (int)maxChunkCount; - } + // update the max chunk count. + MaxResponseChunkCount = CalculateChunkCount(MaxResponseMessageSize, SendBufferSize); - MaxRequestChunkCount = CalculateChunkCount(MaxRequestMessageSize, ReceiveBufferSize); + if (maxChunkCount > 0 && maxChunkCount < MaxResponseChunkCount) + { + MaxResponseChunkCount = (int)maxChunkCount; } + MaxRequestChunkCount = CalculateChunkCount(MaxRequestMessageSize, ReceiveBufferSize); + // send acknowledge. byte[] buffer = BufferManager.TakeBuffer(kResponseBufferSize, nameof(ProcessHelloMessage)); diff --git a/Stack/Opc.Ua.Core/Stack/Tcp/UaSCBinaryChannel.Asymmetric.cs b/Stack/Opc.Ua.Core/Stack/Tcp/UaSCBinaryChannel.Asymmetric.cs index 002774bb51..1e23efba93 100644 --- a/Stack/Opc.Ua.Core/Stack/Tcp/UaSCBinaryChannel.Asymmetric.cs +++ b/Stack/Opc.Ua.Core/Stack/Tcp/UaSCBinaryChannel.Asymmetric.cs @@ -865,91 +865,93 @@ protected ArraySegment ReadAsymmetricMessage( out uint requestId, out uint sequenceNumber) { - BinaryDecoder decoder = new BinaryDecoder(buffer.Array, buffer.Offset, buffer.Count, Quotas.MessageContext); - - string securityPolicyUri = null; - X509Certificate2Collection senderCertificateChain; - - // parse the security header. - ReadAsymmetricMessageHeader( - decoder, - receiverCertificate, - out channelId, - out senderCertificateChain, - out securityPolicyUri); - - if (senderCertificateChain != null && senderCertificateChain.Count > 0) - { - senderCertificate = senderCertificateChain[0]; - } - else + int headerSize; + using (var decoder = new BinaryDecoder(buffer, Quotas.MessageContext)) { - senderCertificate = null; - } + string securityPolicyUri = null; + X509Certificate2Collection senderCertificateChain; - // validate the sender certificate. - if (senderCertificate != null && Quotas.CertificateValidator != null && securityPolicyUri != SecurityPolicies.None) - { - if (Quotas.CertificateValidator is CertificateValidator certificateValidator) + // parse the security header. + ReadAsymmetricMessageHeader( + decoder, + receiverCertificate, + out channelId, + out senderCertificateChain, + out securityPolicyUri); + + if (senderCertificateChain != null && senderCertificateChain.Count > 0) { - certificateValidator.Validate(senderCertificateChain); + senderCertificate = senderCertificateChain[0]; } else { - Quotas.CertificateValidator.Validate(senderCertificate); + senderCertificate = null; } - } - // check if this is the first open secure channel request. - if (!m_uninitialized) - { - if (securityPolicyUri != m_securityPolicyUri) + // validate the sender certificate. + if (senderCertificate != null && Quotas.CertificateValidator != null && securityPolicyUri != SecurityPolicies.None) { - throw ServiceResultException.Create(StatusCodes.BadSecurityPolicyRejected, "Cannot change the security policy after creating the channnel."); + if (Quotas.CertificateValidator is CertificateValidator certificateValidator) + { + certificateValidator.Validate(senderCertificateChain); + } + else + { + Quotas.CertificateValidator.Validate(senderCertificate); + } } - } - else - { - // find a matching endpoint description. - if (m_endpoints != null) + + // check if this is the first open secure channel request. + if (!m_uninitialized) { - foreach (EndpointDescription endpoint in m_endpoints) + if (securityPolicyUri != m_securityPolicyUri) { - // There may be multiple endpoints with the same securityPolicyUri. - // Just choose the first one that matches. This choice will be re-examined - // When the OpenSecureChannel request body is processed. - if (endpoint.SecurityPolicyUri == securityPolicyUri || (securityPolicyUri == SecurityPolicies.None && endpoint.SecurityMode == MessageSecurityMode.None)) - { - m_securityMode = endpoint.SecurityMode; - m_securityPolicyUri = securityPolicyUri; - m_discoveryOnly = false; - m_uninitialized = false; - m_selectedEndpoint = endpoint; - - // recalculate the key sizes. - CalculateSymmetricKeySizes(); - break; - } + throw ServiceResultException.Create(StatusCodes.BadSecurityPolicyRejected, "Cannot change the security policy after creating the channnel."); } } - - // allow a discovery only channel with no security if policy not suppported - if (m_uninitialized) + else { - if (securityPolicyUri != SecurityPolicies.None) + // find a matching endpoint description. + if (m_endpoints != null) { - throw ServiceResultException.Create(StatusCodes.BadSecurityPolicyRejected, "The security policy is not supported."); + foreach (EndpointDescription endpoint in m_endpoints) + { + // There may be multiple endpoints with the same securityPolicyUri. + // Just choose the first one that matches. This choice will be re-examined + // When the OpenSecureChannel request body is processed. + if (endpoint.SecurityPolicyUri == securityPolicyUri || (securityPolicyUri == SecurityPolicies.None && endpoint.SecurityMode == MessageSecurityMode.None)) + { + m_securityMode = endpoint.SecurityMode; + m_securityPolicyUri = securityPolicyUri; + m_discoveryOnly = false; + m_uninitialized = false; + m_selectedEndpoint = endpoint; + + // recalculate the key sizes. + CalculateSymmetricKeySizes(); + break; + } + } } - m_securityMode = MessageSecurityMode.None; - m_securityPolicyUri = SecurityPolicies.None; - m_discoveryOnly = true; - m_uninitialized = false; - m_selectedEndpoint = null; + // allow a discovery only channel with no security if policy not suppported + if (m_uninitialized) + { + if (securityPolicyUri != SecurityPolicies.None) + { + throw ServiceResultException.Create(StatusCodes.BadSecurityPolicyRejected, "The security policy is not supported."); + } + + m_securityMode = MessageSecurityMode.None; + m_securityPolicyUri = SecurityPolicies.None; + m_discoveryOnly = true; + m_uninitialized = false; + m_selectedEndpoint = null; + } } - } - int headerSize = decoder.Position; + headerSize = decoder.Position; + } // decrypt the body. ArraySegment plainText = Decrypt( @@ -1014,17 +1016,17 @@ protected ArraySegment ReadAsymmetricMessage( } // decode message. - decoder = new BinaryDecoder( + using (var decoder = new BinaryDecoder( plainText.Array, plainText.Offset + headerSize, plainText.Count - headerSize, - Quotas.MessageContext); - - sequenceNumber = decoder.ReadUInt32(null); - requestId = decoder.ReadUInt32(null); - - headerSize += decoder.Position; - decoder.Close(); + Quotas.MessageContext)) + { + sequenceNumber = decoder.ReadUInt32(null); + requestId = decoder.ReadUInt32(null); + headerSize += decoder.Position; + decoder.Close(); + } Utils.LogInfo("Security Policy: {0}", SecurityPolicyUri); Utils.LogCertificate("Sender Certificate:", senderCertificate); diff --git a/Stack/Opc.Ua.Core/Stack/Tcp/UaSCBinaryChannel.Symmetric.cs b/Stack/Opc.Ua.Core/Stack/Tcp/UaSCBinaryChannel.Symmetric.cs index 7b5bbf681c..bdc09812e1 100644 --- a/Stack/Opc.Ua.Core/Stack/Tcp/UaSCBinaryChannel.Symmetric.cs +++ b/Stack/Opc.Ua.Core/Stack/Tcp/UaSCBinaryChannel.Symmetric.cs @@ -465,127 +465,128 @@ protected ArraySegment ReadSymmetricMessage( out uint requestId, out uint sequenceNumber) { - BinaryDecoder decoder = new BinaryDecoder(buffer.Array, buffer.Offset, buffer.Count, Quotas.MessageContext); - - uint messageType = decoder.ReadUInt32(null); - uint messageSize = decoder.ReadUInt32(null); - uint channelId = decoder.ReadUInt32(null); - uint tokenId = decoder.ReadUInt32(null); - - // ensure the channel is valid. - if (channelId != ChannelId) + using (var decoder = new BinaryDecoder(buffer, Quotas.MessageContext)) { - throw ServiceResultException.Create( - StatusCodes.BadTcpSecureChannelUnknown, - "SecureChannelId is not known. ChanneId={0}, CurrentChannelId={1}", - channelId, - ChannelId); - } + uint messageType = decoder.ReadUInt32(null); + uint messageSize = decoder.ReadUInt32(null); + uint channelId = decoder.ReadUInt32(null); + uint tokenId = decoder.ReadUInt32(null); - // check for a message secured with the new token. - if (RenewedToken != null && RenewedToken.TokenId == tokenId) - { - ActivateToken(RenewedToken); - } + // ensure the channel is valid. + if (channelId != ChannelId) + { + throw ServiceResultException.Create( + StatusCodes.BadTcpSecureChannelUnknown, + "SecureChannelId is not known. ChanneId={0}, CurrentChannelId={1}", + channelId, + ChannelId); + } - // check if activation of the new token should be forced. - if (RenewedToken != null && CurrentToken.ActivationRequired) - { - ActivateToken(RenewedToken); + // check for a message secured with the new token. + if (RenewedToken != null && RenewedToken.TokenId == tokenId) + { + ActivateToken(RenewedToken); + } - Utils.LogInfo("ChannelId {0}: Token #{1} activated forced.", Id, CurrentToken.TokenId); - } + // check if activation of the new token should be forced. + if (RenewedToken != null && CurrentToken.ActivationRequired) + { + ActivateToken(RenewedToken); - // check for valid token. - ChannelToken currentToken = CurrentToken; + Utils.LogInfo("ChannelId {0}: Token #{1} activated forced.", Id, CurrentToken.TokenId); + } - if (currentToken == null) - { - throw new ServiceResultException(StatusCodes.BadSecureChannelClosed); - } + // check for valid token. + ChannelToken currentToken = CurrentToken; - // find the token. - if (currentToken.TokenId != tokenId && PreviousToken != null && PreviousToken.TokenId != tokenId) - { - throw ServiceResultException.Create( - StatusCodes.BadTcpSecureChannelUnknown, - "Channel{0}: TokenId is not known. ChanneId={1}, TokenId={2}, CurrentTokenId={3}, PreviousTokenId={4}", - Id, channelId, - tokenId, currentToken.TokenId, - (PreviousToken != null) ? (int)PreviousToken.TokenId : -1); - } + if (currentToken == null) + { + throw new ServiceResultException(StatusCodes.BadSecureChannelClosed); + } - token = currentToken; + // find the token. + if (currentToken.TokenId != tokenId && PreviousToken != null && PreviousToken.TokenId != tokenId) + { + throw ServiceResultException.Create( + StatusCodes.BadTcpSecureChannelUnknown, + "Channel{0}: TokenId is not known. ChanneId={1}, TokenId={2}, CurrentTokenId={3}, PreviousTokenId={4}", + Id, channelId, + tokenId, currentToken.TokenId, + (PreviousToken != null) ? (int)PreviousToken.TokenId : -1); + } - // check for a message secured with the token before it expired. - if (PreviousToken != null && PreviousToken.TokenId == tokenId) - { - token = PreviousToken; - } + token = currentToken; - // check if token has expired. - if (token.Expired) - { - throw ServiceResultException.Create(StatusCodes.BadTcpSecureChannelUnknown, - "Channel{0}: Token #{1} has expired. Lifetime={2:HH:mm:ss.fff}", - Id, token.TokenId, token.CreatedAt); - } - - int headerSize = decoder.Position; + // check for a message secured with the token before it expired. + if (PreviousToken != null && PreviousToken.TokenId == tokenId) + { + token = PreviousToken; + } - if (SecurityMode == MessageSecurityMode.SignAndEncrypt) - { - // decrypt the message. - Decrypt(token, new ArraySegment(buffer.Array, buffer.Offset + headerSize, buffer.Count - headerSize), isRequest); - } + // check if token has expired. + if (token.Expired) + { + throw ServiceResultException.Create(StatusCodes.BadTcpSecureChannelUnknown, + "Channel{0}: Token #{1} has expired. Lifetime={2:HH:mm:ss.fff}", + Id, token.TokenId, token.CreatedAt); + } - if (SecurityMode != MessageSecurityMode.None) - { - // extract signature. - byte[] signature = new byte[SymmetricSignatureSize]; + int headerSize = decoder.Position; - for (int ii = 0; ii < SymmetricSignatureSize; ii++) + if (SecurityMode == MessageSecurityMode.SignAndEncrypt) { - signature[ii] = buffer.Array[buffer.Offset + buffer.Count - SymmetricSignatureSize + ii]; + // decrypt the message. + Decrypt(token, new ArraySegment(buffer.Array, buffer.Offset + headerSize, buffer.Count - headerSize), isRequest); } - // verify the signature. - if (!Verify(token, signature, new ArraySegment(buffer.Array, buffer.Offset, buffer.Count - SymmetricSignatureSize), isRequest)) + if (SecurityMode != MessageSecurityMode.None) { - Utils.LogError("ChannelId {0}: Could not verify signature on message.", Id); - throw ServiceResultException.Create(StatusCodes.BadSecurityChecksFailed, "Could not verify the signature on the message."); - } - } + // extract signature. + byte[] signature = new byte[SymmetricSignatureSize]; + + for (int ii = 0; ii < SymmetricSignatureSize; ii++) + { + signature[ii] = buffer.Array[buffer.Offset + buffer.Count - SymmetricSignatureSize + ii]; + } - int paddingCount = 0; + // verify the signature. + if (!Verify(token, signature, new ArraySegment(buffer.Array, buffer.Offset, buffer.Count - SymmetricSignatureSize), isRequest)) + { + Utils.LogError("ChannelId {0}: Could not verify signature on message.", Id); + throw ServiceResultException.Create(StatusCodes.BadSecurityChecksFailed, "Could not verify the signature on the message."); + } + } - if (SecurityMode == MessageSecurityMode.SignAndEncrypt) - { - // verify padding. - int paddingStart = buffer.Offset + buffer.Count - SymmetricSignatureSize - 1; - paddingCount = buffer.Array[paddingStart]; + int paddingCount = 0; - for (int ii = paddingStart - paddingCount; ii < paddingStart; ii++) + if (SecurityMode == MessageSecurityMode.SignAndEncrypt) { - if (buffer.Array[ii] != paddingCount) + // verify padding. + int paddingStart = buffer.Offset + buffer.Count - SymmetricSignatureSize - 1; + paddingCount = buffer.Array[paddingStart]; + + for (int ii = paddingStart - paddingCount; ii < paddingStart; ii++) { - throw ServiceResultException.Create(StatusCodes.BadSecurityChecksFailed, "Could not verify the padding in the message."); + if (buffer.Array[ii] != paddingCount) + { + throw ServiceResultException.Create(StatusCodes.BadSecurityChecksFailed, "Could not verify the padding in the message."); + } } - } - // add byte for size. - paddingCount++; - } + // add byte for size. + paddingCount++; + } - // extract request id and sequence number. - sequenceNumber = decoder.ReadUInt32(null); - requestId = decoder.ReadUInt32(null); + // extract request id and sequence number. + sequenceNumber = decoder.ReadUInt32(null); + requestId = decoder.ReadUInt32(null); - // return an the data contained in the message. - int startOfBody = buffer.Offset + TcpMessageLimits.SymmetricHeaderSize + TcpMessageLimits.SequenceHeaderSize; - int sizeOfBody = buffer.Count - TcpMessageLimits.SymmetricHeaderSize - TcpMessageLimits.SequenceHeaderSize - paddingCount - SymmetricSignatureSize; + // return an the data contained in the message. + int startOfBody = buffer.Offset + TcpMessageLimits.SymmetricHeaderSize + TcpMessageLimits.SequenceHeaderSize; + int sizeOfBody = buffer.Count - TcpMessageLimits.SymmetricHeaderSize - TcpMessageLimits.SequenceHeaderSize - paddingCount - SymmetricSignatureSize; - return new ArraySegment(buffer.Array, startOfBody, sizeOfBody); + return new ArraySegment(buffer.Array, startOfBody, sizeOfBody); + } } /// diff --git a/Stack/Opc.Ua.Core/Stack/Tcp/UaSCBinaryChannel.cs b/Stack/Opc.Ua.Core/Stack/Tcp/UaSCBinaryChannel.cs index d60c803822..2ca2f0a684 100644 --- a/Stack/Opc.Ua.Core/Stack/Tcp/UaSCBinaryChannel.cs +++ b/Stack/Opc.Ua.Core/Stack/Tcp/UaSCBinaryChannel.cs @@ -826,6 +826,28 @@ protected static int CalculateChunkCount(int messageSize, int bufferSize) } return 1; } + + /// + /// Check the MessageType and size against the content and size of the stream. + /// + /// The decoder of the stream. + /// The message type to be checked. + /// The length of the message. + protected static void ReadAndVerifyMessageTypeAndSize(IDecoder decoder, uint expectedMessageType, int count) + { + uint messageType = decoder.ReadUInt32(null); + if (messageType != expectedMessageType) + { + throw ServiceResultException.Create(StatusCodes.BadTcpMessageTypeInvalid, + "Expected message type {0:X8} instead of {0:X8}.", expectedMessageType, messageType); + } + int messageSize = decoder.ReadInt32(null); + if (messageSize > count) + { + throw ServiceResultException.Create(StatusCodes.BadTcpMessageTooLarge, + "Messages size {0} is larger than buffer size {1}.", messageSize, count); + } + } #endregion #region Private Fields diff --git a/Stack/Opc.Ua.Core/Stack/Tcp/UaSCBinaryClientChannel.cs b/Stack/Opc.Ua.Core/Stack/Tcp/UaSCBinaryClientChannel.cs index 48ddbd12bb..ce109e891b 100644 --- a/Stack/Opc.Ua.Core/Stack/Tcp/UaSCBinaryClientChannel.cs +++ b/Stack/Opc.Ua.Core/Stack/Tcp/UaSCBinaryClientChannel.cs @@ -469,13 +469,10 @@ private bool ProcessAcknowledgeMessage(ArraySegment messageChunk) } // read buffer sizes. - MemoryStream istrm = new MemoryStream(messageChunk.Array, messageChunk.Offset, messageChunk.Count); - BinaryDecoder decoder = new BinaryDecoder(istrm, Quotas.MessageContext); - - istrm.Seek(TcpMessageLimits.MessageTypeAndSize, SeekOrigin.Current); - - try + using (var decoder = new BinaryDecoder(messageChunk, Quotas.MessageContext)) { + ReadAndVerifyMessageTypeAndSize(decoder, TcpMessageType.Acknowledge, messageChunk.Count); + uint protocolVersion = decoder.ReadUInt32(null); SendBufferSize = (int)decoder.ReadUInt32(null); ReceiveBufferSize = (int)decoder.ReadUInt32(null); @@ -499,9 +496,7 @@ private bool ProcessAcknowledgeMessage(ArraySegment messageChunk) { MaxRequestChunkCount = (int)maxChunkCount; } - } - finally - { + decoder.Close(); } @@ -1363,33 +1358,31 @@ private WriteOperation InternalClose(int timeout) /// protected bool ProcessErrorMessage(uint messageType, ArraySegment messageChunk) { - // read request buffer sizes. - MemoryStream istrm = new MemoryStream(messageChunk.Array, messageChunk.Offset, messageChunk.Count, false); - BinaryDecoder decoder = new BinaryDecoder(istrm, Quotas.MessageContext); - - istrm.Seek(TcpMessageLimits.MessageTypeAndSize, SeekOrigin.Current); + ServiceResult error; - try + // read request buffer sizes. + using (var decoder = new BinaryDecoder(messageChunk, Quotas.MessageContext)) { - ServiceResult error = ReadErrorMessageBody(decoder); + ReadAndVerifyMessageTypeAndSize(decoder, TcpMessageType.Error, messageChunk.Count); - Utils.LogTrace("ChannelId {0}: ProcessErrorMessage({1})", ChannelId, error); + error = ReadErrorMessageBody(decoder); - // check if a handshake is in progress - if (m_handshakeOperation != null) - { - m_handshakeOperation.Fault(error); - return false; - } - - // handle the fatal error. - ForceReconnect(error); - return false; + decoder.Close(); } - finally + + Utils.LogTrace("ChannelId {0}: ProcessErrorMessage({1})", ChannelId, error); + + // check if a handshake is in progress + if (m_handshakeOperation != null) { - decoder.Close(); + m_handshakeOperation.Fault(error); + return false; } + + // handle the fatal error. + ForceReconnect(error); + return false; + } /// @@ -1488,11 +1481,14 @@ private bool ProcessResponseMessage(uint messageType, ArraySegment message // get the chunks to process. chunksToProcess = GetSavedChunks(requestId, messageBody, false); - // decoder reason. - MemoryStream istrm = new MemoryStream(messageBody.Array, messageBody.Offset, messageBody.Count, false); - BinaryDecoder decoder = new BinaryDecoder(istrm, Quotas.MessageContext); - ServiceResult error = ReadErrorMessageBody(decoder); - decoder.Close(); + ServiceResult error; + + // decode error reason. + using (var decoder = new BinaryDecoder(messageBody, Quotas.MessageContext)) + { + error = ReadErrorMessageBody(decoder); + decoder.Close(); + } // report a fault. operation.Fault(true, error); diff --git a/Stack/Opc.Ua.Core/Types/Encoders/BinaryDecoder.cs b/Stack/Opc.Ua.Core/Types/Encoders/BinaryDecoder.cs index 91af1e04aa..9b0ea8ff1c 100644 --- a/Stack/Opc.Ua.Core/Types/Encoders/BinaryDecoder.cs +++ b/Stack/Opc.Ua.Core/Types/Encoders/BinaryDecoder.cs @@ -13,6 +13,7 @@ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. using System; using System.Globalization; using System.IO; +using System.Runtime.CompilerServices; using System.Text; using System.Xml; @@ -28,8 +29,15 @@ public class BinaryDecoder : IDecoder /// Creates a decoder that reads from a memory buffer. /// public BinaryDecoder(byte[] buffer, IServiceMessageContext context) - : - this(buffer, 0, buffer.Length, context) + : this(buffer, 0, buffer.Length, context) + { + } + + /// + /// Creates a decoder that reads from an ArraySegment. + /// + public BinaryDecoder(ArraySegment buffer, IServiceMessageContext context) + : this(buffer.Array, buffer.Offset, buffer.Count, context) { } @@ -38,8 +46,8 @@ public BinaryDecoder(byte[] buffer, IServiceMessageContext context) /// public BinaryDecoder(byte[] buffer, int start, int count, IServiceMessageContext context) { - m_istrm = new MemoryStream(buffer, start, count, false); - m_reader = new BinaryReader(m_istrm); + var stream = new MemoryStream(buffer, start, count, false); + m_reader = new BinaryReader(stream); m_context = context; m_nestingLevel = 0; } @@ -47,12 +55,11 @@ public BinaryDecoder(byte[] buffer, int start, int count, IServiceMessageContext /// /// Creates a decoder that reads from a stream. /// - public BinaryDecoder(Stream stream, IServiceMessageContext context) + public BinaryDecoder(Stream stream, IServiceMessageContext context, bool leaveOpen = false) { if (stream == null) throw new ArgumentNullException(nameof(stream)); - m_istrm = stream; - m_reader = new BinaryReader(m_istrm); + m_reader = new BinaryReader(stream, Encoding.UTF8, leaveOpen); m_context = context; m_nestingLevel = 0; } @@ -75,15 +82,8 @@ protected virtual void Dispose(bool disposing) { if (disposing) { - if (m_reader != null) - { - m_reader.Dispose(); - } - - if (m_istrm != null) - { - m_istrm.Dispose(); - } + Utils.SilentDispose(m_reader); + m_reader = null; } } #endregion @@ -116,7 +116,7 @@ public void SetMappingTables(NamespaceTable namespaceUris, StringTable serverUri /// public void Close() { - m_reader.Dispose(); + m_reader.Close(); } /// @@ -137,16 +137,10 @@ public static IEncodeable DecodeMessage(Stream stream, System.Type expectedType, if (stream == null) throw new ArgumentNullException(nameof(stream)); if (context == null) throw new ArgumentNullException(nameof(context)); - BinaryDecoder decoder = new BinaryDecoder(stream, context); - - try + using (var decoder = new BinaryDecoder(stream, context)) { return decoder.DecodeMessage(expectedType); } - finally - { - decoder.Close(); - } } /// @@ -157,9 +151,7 @@ public static IEncodeable DecodeSessionLessMessage(byte[] buffer, IServiceMessag if (buffer == null) throw new ArgumentNullException(nameof(buffer)); if (context == null) throw new ArgumentNullException(nameof(context)); - BinaryDecoder decoder = new BinaryDecoder(buffer, context); - - try + using (var decoder = new BinaryDecoder(buffer, context)) { // read the node id. NodeId typeId = decoder.ReadNodeId(null); @@ -172,7 +164,8 @@ public static IEncodeable DecodeSessionLessMessage(byte[] buffer, IServiceMessag if (actualType == null || actualType != typeof(SessionlessInvokeRequestType)) { - throw new ServiceResultException(StatusCodes.BadDecodingError, Utils.Format("Cannot decode session-less service message with type id: {0}.", absoluteId)); + throw ServiceResultException.Create(StatusCodes.BadDecodingError, + "Cannot decode session-less service message with type id: {0}.", absoluteId); } // decode the actual message. @@ -180,32 +173,24 @@ public static IEncodeable DecodeSessionLessMessage(byte[] buffer, IServiceMessag message.Decode(decoder); - return message.Message; - } - finally - { decoder.Close(); + + return message.Message; } } /// /// Decodes a message from a buffer. /// - public static IEncodeable DecodeMessage(byte[] buffer, System.Type expectedType, IServiceMessageContext context) + public static IEncodeable DecodeMessage(byte[] buffer, Type expectedType, IServiceMessageContext context) { if (buffer == null) throw new ArgumentNullException(nameof(buffer)); if (context == null) throw new ArgumentNullException(nameof(context)); - BinaryDecoder decoder = new BinaryDecoder(buffer, context); - - try + using (var decoder = new BinaryDecoder(buffer, context)) { return decoder.DecodeMessage(expectedType); } - finally - { - decoder.Close(); - } } /// @@ -213,7 +198,7 @@ public static IEncodeable DecodeMessage(byte[] buffer, System.Type expectedType, /// public IEncodeable DecodeMessage(System.Type expectedType) { - long start = m_istrm.CanSeek ? m_istrm.Position : 0; + long start = m_reader.BaseStream.CanSeek ? m_reader.BaseStream.Position : 0; // read the node id. NodeId typeId = ReadNodeId(null); @@ -226,21 +211,19 @@ public IEncodeable DecodeMessage(System.Type expectedType) if (actualType == null) { - throw new ServiceResultException(StatusCodes.BadDecodingError, Utils.Format("Cannot decode message with type id: {0}.", absoluteId)); + throw ServiceResultException.Create(StatusCodes.BadDecodingError, + "Cannot decode message with type id: {0}.", absoluteId); } // read the message. IEncodeable message = ReadEncodeable(null, actualType, absoluteId); // check that the max message size was not exceeded. - int messageLength = m_istrm.CanSeek ? (int)(m_istrm.Position - start) : 0; + int messageLength = m_reader.BaseStream.CanSeek ? (int)(m_reader.BaseStream.Position - start) : 0; if (m_context.MaxMessageSize > 0 && m_context.MaxMessageSize < messageLength) { - throw ServiceResultException.Create( - StatusCodes.BadEncodingLimitsExceeded, - "MaxMessageSize {0} < {1}", - m_context.MaxMessageSize, - messageLength); + throw ServiceResultException.Create(StatusCodes.BadEncodingLimitsExceeded, + "MaxMessageSize {0} < {1}", m_context.MaxMessageSize, messageLength); } // return the message. @@ -252,7 +235,7 @@ public IEncodeable DecodeMessage(System.Type expectedType) /// public bool LoadStringTable(StringTable stringTable) { - int count = ReadInt32(null); + int count = SafeReadInt32(); if (count < -0) { @@ -300,7 +283,7 @@ public void PopNamespace() /// public bool ReadBoolean(string fieldName) { - return m_reader.ReadBoolean(); + return SafeReadBoolean(); } /// @@ -308,7 +291,7 @@ public bool ReadBoolean(string fieldName) /// public sbyte ReadSByte(string fieldName) { - return m_reader.ReadSByte(); + return SafeReadSByte(); } /// @@ -316,7 +299,7 @@ public sbyte ReadSByte(string fieldName) /// public byte ReadByte(string fieldName) { - return m_reader.ReadByte(); + return SafeReadByte(); } /// @@ -324,7 +307,7 @@ public byte ReadByte(string fieldName) /// public short ReadInt16(string fieldName) { - return m_reader.ReadInt16(); + return SafeReadInt16(); } /// @@ -332,7 +315,7 @@ public short ReadInt16(string fieldName) /// public ushort ReadUInt16(string fieldName) { - return m_reader.ReadUInt16(); + return SafeReadUInt16(); } /// @@ -340,7 +323,7 @@ public ushort ReadUInt16(string fieldName) /// public int ReadInt32(string fieldName) { - return m_reader.ReadInt32(); + return SafeReadInt32(); } /// @@ -348,7 +331,7 @@ public int ReadInt32(string fieldName) /// public uint ReadUInt32(string fieldName) { - return m_reader.ReadUInt32(); + return SafeReadUInt32(); } /// @@ -356,7 +339,7 @@ public uint ReadUInt32(string fieldName) /// public long ReadInt64(string fieldName) { - return m_reader.ReadInt64(); + return SafeReadInt64(); } /// @@ -364,7 +347,7 @@ public long ReadInt64(string fieldName) /// public ulong ReadUInt64(string fieldName) { - return m_reader.ReadUInt64(); + return SafeReadUInt64(); } /// @@ -372,7 +355,7 @@ public ulong ReadUInt64(string fieldName) /// public float ReadFloat(string fieldName) { - return m_reader.ReadSingle(); + return SafeReadFloat(); } /// @@ -380,7 +363,7 @@ public float ReadFloat(string fieldName) /// public double ReadDouble(string fieldName) { - return m_reader.ReadDouble(); + return SafeReadDouble(); } /// @@ -396,7 +379,7 @@ public string ReadString(string fieldName) /// public string ReadString(string fieldName, int maxStringLength) { - int length = m_reader.ReadInt32(); + int length = SafeReadInt32(); if (length < 0) { @@ -410,14 +393,11 @@ public string ReadString(string fieldName, int maxStringLength) if (maxStringLength > 0 && maxStringLength < length) { - throw ServiceResultException.Create( - StatusCodes.BadEncodingLimitsExceeded, - "MaxStringLength {0} < {1}", - maxStringLength, - length); + throw ServiceResultException.Create(StatusCodes.BadEncodingLimitsExceeded, + "MaxStringLength {0} < {1}", maxStringLength, length); } - byte[] bytes = m_reader.ReadBytes(length); + byte[] bytes = SafeReadBytes(length); // If 0 terminated, decrease length by one before converting to string var utf8StringLength = bytes[bytes.Length - 1] == 0 ? bytes.Length - 1 : bytes.Length; @@ -429,7 +409,7 @@ public string ReadString(string fieldName, int maxStringLength) /// public DateTime ReadDateTime(string fieldName) { - long ticks = m_reader.ReadInt64(); + long ticks = SafeReadInt64(); if (ticks >= (Int64.MaxValue - Utils.TimeBase.Ticks)) { @@ -456,7 +436,8 @@ public DateTime ReadDateTime(string fieldName) /// public Uuid ReadGuid(string fieldName) { - byte[] bytes = m_reader.ReadBytes(16); + const int kGuidLength = 16; + byte[] bytes = SafeReadBytes(kGuidLength); return new Uuid(new Guid(bytes)); } @@ -473,7 +454,7 @@ public byte[] ReadByteString(string fieldName) /// public byte[] ReadByteString(string fieldName, int maxByteStringLength) { - int length = m_reader.ReadInt32(); + int length = SafeReadInt32(); if (length < 0) { @@ -482,14 +463,11 @@ public byte[] ReadByteString(string fieldName, int maxByteStringLength) if (maxByteStringLength > 0 && maxByteStringLength < length) { - throw ServiceResultException.Create( - StatusCodes.BadEncodingLimitsExceeded, - "MaxByteStringLength {0} < {1}", - maxByteStringLength, - length); + throw ServiceResultException.Create(StatusCodes.BadEncodingLimitsExceeded, + "MaxByteStringLength {0} < {1}", maxByteStringLength, length); } - return m_reader.ReadBytes(length); + return SafeReadBytes(length); } /// @@ -530,7 +508,7 @@ public XmlElement ReadXmlElement(string fieldName) /// public NodeId ReadNodeId(string fieldName) { - byte encodingByte = m_reader.ReadByte(); + byte encodingByte = SafeReadByte(); NodeId value = new NodeId(); @@ -549,7 +527,7 @@ public NodeId ReadNodeId(string fieldName) /// public ExpandedNodeId ReadExpandedNodeId(string fieldName) { - byte encodingByte = m_reader.ReadByte(); + byte encodingByte = SafeReadByte(); ExpandedNodeId value = new ExpandedNodeId(); @@ -570,7 +548,7 @@ public ExpandedNodeId ReadExpandedNodeId(string fieldName) // read the server index if present. if ((encodingByte & 0x40) != 0) { - serverIndex = ReadUInt32(null); + serverIndex = SafeReadUInt32(); value.SetServerIndex(serverIndex); } @@ -592,7 +570,7 @@ public ExpandedNodeId ReadExpandedNodeId(string fieldName) /// public StatusCode ReadStatusCode(string fieldName) { - return m_reader.ReadUInt32(); + return SafeReadUInt32(); } /// @@ -625,7 +603,7 @@ public QualifiedName ReadQualifiedName(string fieldName) public LocalizedText ReadLocalizedText(string fieldName) { // read the encoding byte. - byte encodingByte = m_reader.ReadByte(); + byte encodingByte = SafeReadByte(); string text = null; string locale = null; @@ -667,7 +645,7 @@ public Variant ReadVariant(string fieldName) public DataValue ReadDataValue(string fieldName) { // read the encoding byte. - byte encodingByte = m_reader.ReadByte(); + byte encodingByte = SafeReadByte(); DataValue value = new DataValue(); @@ -727,9 +705,8 @@ public IEncodeable ReadEncodeable(string fieldName, System.Type systemType, Expa if (!(Activator.CreateInstance(systemType) is IEncodeable encodeable)) { - throw new ServiceResultException( - StatusCodes.BadDecodingError, - Utils.Format("Cannot decode type '{0}'.", systemType.FullName)); + throw ServiceResultException.Create(StatusCodes.BadDecodingError, + "Cannot decode type '{0}'.", systemType.FullName); } if (encodeableTypeId != null) @@ -761,7 +738,7 @@ public IEncodeable ReadEncodeable(string fieldName, System.Type systemType, Expa /// public Enum ReadEnumerated(string fieldName, System.Type enumType) { - return (Enum)Enum.ToObject(enumType, m_reader.ReadInt32()); + return (Enum)Enum.ToObject(enumType, SafeReadInt32()); } /// @@ -802,7 +779,7 @@ public SByteCollection ReadSByteArray(string fieldName) for (int ii = 0; ii < length; ii++) { - values.Add(ReadSByte(null)); + values.Add(SafeReadSByte()); } return values; @@ -824,7 +801,7 @@ public ByteCollection ReadByteArray(string fieldName) for (int ii = 0; ii < length; ii++) { - values.Add(ReadByte(null)); + values.Add(SafeReadByte()); } return values; @@ -912,7 +889,7 @@ public UInt32Collection ReadUInt32Array(string fieldName) for (int ii = 0; ii < length; ii++) { - values.Add(ReadUInt32(null)); + values.Add(SafeReadUInt32()); } return values; @@ -1446,9 +1423,8 @@ public Array ReadArray( { return ReadEncodeableArray(fieldName, systemType, encodeableTypeId); } - throw new ServiceResultException( - StatusCodes.BadDecodingError, - Utils.Format("Cannot decode unknown type in Array object with BuiltInType: {0}.", builtInType)); + throw ServiceResultException.Create(StatusCodes.BadDecodingError, + "Cannot decode unknown type in Array object with BuiltInType: {0}.", builtInType); } } } @@ -1482,8 +1458,7 @@ public Array ReadArray( if (elements == null) { - throw ServiceResultException.Create( - StatusCodes.BadDecodingError, + throw ServiceResultException.Create(StatusCodes.BadDecodingError, "Unexpected null Array for multidimensional matrix with {0} elements.", length); } @@ -1500,8 +1475,7 @@ public Array ReadArray( return new Matrix(elements, builtInType, dimensions.ToArray()).ToArray(); } - throw ServiceResultException.Create( - StatusCodes.BadDecodingError, + throw ServiceResultException.Create(StatusCodes.BadDecodingError, "Unexpected null or empty Dimensions for multidimensional matrix."); } return null; @@ -1517,8 +1491,7 @@ private DiagnosticInfo ReadDiagnosticInfo(string fieldName, int depth) { if (depth >= DiagnosticInfo.MaxInnerDepth) { - throw ServiceResultException.Create( - StatusCodes.BadEncodingLimitsExceeded, + throw ServiceResultException.Create(StatusCodes.BadEncodingLimitsExceeded, "Maximum nesting level of InnerDiagnosticInfo was exceeded"); } @@ -1527,7 +1500,7 @@ private DiagnosticInfo ReadDiagnosticInfo(string fieldName, int depth) try { // read the encoding byte. - byte encodingByte = m_reader.ReadByte(); + byte encodingByte = SafeReadByte(); // check if the diagnostic info is null. if (encodingByte == 0) @@ -1623,7 +1596,7 @@ private Array ReadArrayElements(int length, BuiltInType builtInType) for (int ii = 0; ii < values.Length; ii++) { - values[ii] = ReadSByte(null); + values[ii] = SafeReadSByte(); } array = values; @@ -1636,7 +1609,7 @@ private Array ReadArrayElements(int length, BuiltInType builtInType) for (int ii = 0; ii < values.Length; ii++) { - values[ii] = ReadByte(null); + values[ii] = SafeReadByte(); } array = values; @@ -1688,7 +1661,7 @@ private Array ReadArrayElements(int length, BuiltInType builtInType) for (int ii = 0; ii < values.Length; ii++) { - values[ii] = ReadUInt32(null); + values[ii] = SafeReadUInt32(); } array = values; @@ -1936,11 +1909,11 @@ private Array ReadArrayElements(int length, BuiltInType builtInType) array = values; break; } + default: { - throw new ServiceResultException( - StatusCodes.BadDecodingError, - Utils.Format("Cannot decode unknown type in Variant object with BuiltInType: {0}.", builtInType)); + throw ServiceResultException.Create(StatusCodes.BadDecodingError, + "Cannot decode unknown type in Variant object with BuiltInType: {0}.", builtInType); } } @@ -1952,7 +1925,7 @@ private Array ReadArrayElements(int length, BuiltInType builtInType) /// private int ReadArrayLength() { - int length = m_reader.ReadInt32(); + int length = SafeReadInt32(); if (length < 0) { @@ -1961,11 +1934,8 @@ private int ReadArrayLength() if (m_context.MaxArrayLength > 0 && m_context.MaxArrayLength < length) { - throw ServiceResultException.Create( - StatusCodes.BadEncodingLimitsExceeded, - "MaxArrayLength {0} < {1}", - m_context.MaxArrayLength, - length); + throw ServiceResultException.Create(StatusCodes.BadEncodingLimitsExceeded, + "MaxArrayLength {0} < {1}", m_context.MaxArrayLength, length); } return length; @@ -1981,50 +1951,49 @@ private void ReadNodeIdBody(byte encodingByte, NodeId value) case NodeIdEncodingBits.TwoByte: { value.SetNamespaceIndex(0); - value.SetIdentifier(IdType.Numeric, (uint)m_reader.ReadByte()); + value.SetIdentifier(IdType.Numeric, (uint)SafeReadByte()); break; } case NodeIdEncodingBits.FourByte: { - value.SetNamespaceIndex(m_reader.ReadByte()); - value.SetIdentifier(IdType.Numeric, (uint)m_reader.ReadUInt16()); + value.SetNamespaceIndex(SafeReadByte()); + value.SetIdentifier(IdType.Numeric, (uint)SafeReadUInt16()); break; } case NodeIdEncodingBits.Numeric: { - value.SetNamespaceIndex(m_reader.ReadUInt16()); - value.SetIdentifier(IdType.Numeric, (uint)m_reader.ReadUInt32()); + value.SetNamespaceIndex(SafeReadUInt16()); + value.SetIdentifier(IdType.Numeric, SafeReadUInt32()); break; } case NodeIdEncodingBits.String: { - value.SetNamespaceIndex(m_reader.ReadUInt16()); + value.SetNamespaceIndex(SafeReadUInt16()); value.SetIdentifier(IdType.String, ReadString(null)); break; } case NodeIdEncodingBits.Guid: { - value.SetNamespaceIndex(m_reader.ReadUInt16()); + value.SetNamespaceIndex(SafeReadUInt16()); value.SetIdentifier(IdType.Guid, (Guid)ReadGuid(null)); break; } case NodeIdEncodingBits.ByteString: { - value.SetNamespaceIndex(m_reader.ReadUInt16()); + value.SetNamespaceIndex(SafeReadUInt16()); value.SetIdentifier(IdType.Opaque, ReadByteString(null)); break; } default: { - throw new ServiceResultException( - StatusCodes.BadDecodingError, - Utils.Format("Invald encoding byte (0x{0:X2}) for NodeId.", encodingByte)); + throw ServiceResultException.Create(StatusCodes.BadDecodingError, + "Invalid encoding byte (0x{0:X2}) for NodeId.", encodingByte); } } } @@ -2050,7 +2019,7 @@ private ExtensionObject ReadExtensionObject() } // read encoding. - ExtensionObjectEncoding encoding = (ExtensionObjectEncoding)Enum.ToObject(typeof(ExtensionObjectEncoding), m_reader.ReadByte()); + ExtensionObjectEncoding encoding = (ExtensionObjectEncoding)Enum.ToObject(typeof(ExtensionObjectEncoding), SafeReadByte()); // nothing more to do for empty bodies. if (encoding == ExtensionObjectEncoding.None) @@ -2070,20 +2039,21 @@ private ExtensionObject ReadExtensionObject() if (systemType != null && extension.Body != null) { XmlElement element = extension.Body as XmlElement; - XmlDecoder xmlDecoder = new XmlDecoder(element, this.Context); - - try + using (XmlDecoder xmlDecoder = new XmlDecoder(element, this.Context)) { - xmlDecoder.PushNamespace(element.NamespaceURI); - IEncodeable body = xmlDecoder.ReadEncodeable(element.LocalName, systemType, extension.TypeId); - xmlDecoder.PopNamespace(); + try + { + xmlDecoder.PushNamespace(element.NamespaceURI); + IEncodeable body = xmlDecoder.ReadEncodeable(element.LocalName, systemType, extension.TypeId); + xmlDecoder.PopNamespace(); - // update body. - extension.Body = body; - } - catch (Exception e) - { - Utils.LogError("Could not decode known type {0}. Error={1}, Value={2}", systemType.FullName, e.Message, element.OuterXml); + // update body. + extension.Body = body; + } + catch (Exception e) + { + Utils.LogError("Could not decode known type {0}. Error={1}, Value={2}", systemType.FullName, e.Message, element.OuterXml); + } } } @@ -2098,7 +2068,6 @@ private ExtensionObject ReadExtensionObject() encodeable = Activator.CreateInstance(systemType) as IEncodeable; // set type identifier for custom complex data types before decode. - if (encodeable is IComplexTypeInstance complexTypeInstance) { complexTypeInstance.TypeId = extension.TypeId; @@ -2127,10 +2096,8 @@ private ExtensionObject ReadExtensionObject() int used = Position - start; if (length != used) { - throw ServiceResultException.Create( - StatusCodes.BadDecodingError, - "The encodeable.Decoder operation did not match the length of the extension object. {0} != {1}", - used, length); + throw ServiceResultException.Create(StatusCodes.BadDecodingError, + "The encodeable.Decoder operation did not match the length of the extension object. {0} != {1}", used, length); } } catch (EndOfStreamException eofStream) @@ -2162,23 +2129,19 @@ private ExtensionObject ReadExtensionObject() // figure out how long the object is. if (length < 0) { - throw new ServiceResultException( - StatusCodes.BadDecodingError, - Utils.Format("Cannot determine length of unknown extension object body with type '{0}'.", extension.TypeId)); + throw ServiceResultException.Create(StatusCodes.BadDecodingError, + "Cannot determine length of unknown extension object body with type '{0}'.", extension.TypeId); } // check the length. if (m_context.MaxByteStringLength > 0 && m_context.MaxByteStringLength < length) { - throw ServiceResultException.Create( - StatusCodes.BadEncodingLimitsExceeded, - "MaxByteStringLength {0} < {1}", - m_context.MaxByteStringLength, - length); + throw ServiceResultException.Create(StatusCodes.BadEncodingLimitsExceeded, + "MaxByteStringLength {0} < {1}", m_context.MaxByteStringLength, length); } // read the bytes of the body. - extension.Body = m_reader.ReadBytes(length); + extension.Body = SafeReadBytes(length); return extension; } @@ -2188,7 +2151,7 @@ private ExtensionObject ReadExtensionObject() if (unused > 0) { - m_reader.ReadBytes(unused); + SafeReadBytes(unused); } if (encodeable != null) @@ -2208,7 +2171,7 @@ private ExtensionObject ReadExtensionObject() private Variant ReadVariantValue(string fieldName) { // read the encoding byte. - byte encodingByte = m_reader.ReadByte(); + byte encodingByte = SafeReadByte(); Variant value = new Variant(); @@ -2240,8 +2203,7 @@ private Variant ReadVariantValue(string fieldName) // check if ArrayDimensions are consistent with the ArrayLength. if (dimensions == null || dimensions.Count == 0) { - throw new ServiceResultException( - StatusCodes.BadDecodingError, + throw ServiceResultException.Create(StatusCodes.BadDecodingError, "ArrayDimensions not specified when ArrayDimensions encoding bit was set in Variant object."); } @@ -2250,7 +2212,8 @@ private Variant ReadVariantValue(string fieldName) if (!valid || (matrixLength != length)) { - throw new ServiceResultException(StatusCodes.BadDecodingError, "ArrayDimensions does not match with the ArrayLength in Variant object."); + throw ServiceResultException.Create(StatusCodes.BadDecodingError, + "ArrayDimensions length does not match with the ArrayLength in Variant object."); } value = new Variant(new Matrix(array, builtInType, dimensions.ToArray())); @@ -2279,13 +2242,13 @@ private Variant ReadVariantValue(string fieldName) case BuiltInType.SByte: { - value.Set(ReadSByte(null)); + value.Set(SafeReadSByte()); break; } case BuiltInType.Byte: { - value.Set(ReadByte(null)); + value.Set(SafeReadByte()); break; } @@ -2310,7 +2273,7 @@ private Variant ReadVariantValue(string fieldName) case BuiltInType.UInt32: { - value.Set(ReadUInt32(null)); + value.Set(SafeReadUInt32()); break; } @@ -2420,9 +2383,8 @@ private Variant ReadVariantValue(string fieldName) default: { - throw new ServiceResultException( - StatusCodes.BadDecodingError, - Utils.Format("Cannot decode unknown type in Variant object (0x{0:X2}).", encodingByte)); + throw ServiceResultException.Create(StatusCodes.BadDecodingError, + "Cannot decode unknown type in Variant object (0x{0:X2}).", encodingByte); } } } @@ -2430,6 +2392,224 @@ private Variant ReadVariantValue(string fieldName) return value; } + /// + /// Read bytes from stream and validate the length of the returned buffer. + /// Throws decoding error if less than the expected number of bytes were read. + /// + /// The number of bytes to read. + /// The name of the calling function. + /// with + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private byte[] SafeReadBytes(int length, [CallerMemberName] string functionName = null) + { + byte[] bytes = m_reader.ReadBytes(length); + if (bytes.Length != length) + { + throw ServiceResultException.Create(StatusCodes.BadDecodingError, + "Reading {0} bytes of {1} reached end of stream after {2} bytes.", length, functionName, bytes.Length); + } + return bytes; + } + + /// + /// Safe version of which returns a ServiceResultException on error. + /// + /// with + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private bool SafeReadBoolean([CallerMemberName] string functionName = null) + { + try + { + return m_reader.ReadBoolean(); + } + catch (EndOfStreamException) + { + throw CreateDecodingError(nameof(ReadBoolean), functionName); + } + } + + /// + /// Safe version of which returns a ServiceResultException on error. + /// + /// with + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private sbyte SafeReadSByte([CallerMemberName] string functionName = null) + { + try + { + return m_reader.ReadSByte(); + } + catch (EndOfStreamException) + { + throw CreateDecodingError(nameof(ReadSByte), functionName); + } + } + + /// + /// Safe version of which returns a ServiceResultException on error. + /// + /// with + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private byte SafeReadByte([CallerMemberName] string functionName = null) + { + try + { + return m_reader.ReadByte(); + } + catch (EndOfStreamException) + { + throw CreateDecodingError(nameof(ReadByte), functionName); + } + } + + /// + /// Safe version of which returns a ServiceResultException on error. + /// + /// with + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private short SafeReadInt16([CallerMemberName] string functionName = null) + { + try + { + return m_reader.ReadInt16(); + } + catch (EndOfStreamException) + { + throw CreateDecodingError(nameof(ReadInt16), functionName); + } + } + + /// + /// Safe version of which returns a ServiceResultException on error. + /// + /// with + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private ushort SafeReadUInt16([CallerMemberName] string functionName = null) + { + try + { + return m_reader.ReadUInt16(); + } + catch (EndOfStreamException) + { + throw CreateDecodingError(nameof(ReadUInt16), functionName); + } + } + + /// + /// Safe version of which returns a ServiceResultException on error. + /// + /// with + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private int SafeReadInt32([CallerMemberName] string functionName = null) + { + try + { + return m_reader.ReadInt32(); + } + catch (EndOfStreamException) + { + throw CreateDecodingError(nameof(ReadInt32), functionName); + } + } + + /// + /// Safe version of which returns a ServiceResultException on error. + /// + /// with + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private uint SafeReadUInt32([CallerMemberName] string functionName = null) + { + try + { + return m_reader.ReadUInt32(); + } + catch (EndOfStreamException) + { + throw CreateDecodingError(nameof(ReadUInt32), functionName); + } + } + + /// + /// Safe version of which returns a ServiceResultException on error. + /// + /// with + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private long SafeReadInt64([CallerMemberName] string functionName = null) + { + try + { + return m_reader.ReadInt64(); + } + catch (EndOfStreamException) + { + throw CreateDecodingError(nameof(ReadInt64), functionName); + } + } + + /// + /// Safe version of which returns a ServiceResultException on error. + /// + /// with + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private ulong SafeReadUInt64([CallerMemberName] string functionName = null) + { + try + { + return m_reader.ReadUInt64(); + } + catch (EndOfStreamException) + { + throw CreateDecodingError(nameof(ReadUInt64), functionName); + } + } + + /// + /// Safe version of which returns a ServiceResultException on error. + /// + /// with + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private float SafeReadFloat([CallerMemberName] string functionName = null) + { + try + { + return m_reader.ReadSingle(); + } + catch (EndOfStreamException) + { + throw CreateDecodingError(nameof(ReadFloat), functionName); + } + } + + /// + /// Safe version of which returns a ServiceResultException on error. + /// + /// with + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private double SafeReadDouble([CallerMemberName] string functionName = null) + { + try + { + return m_reader.ReadDouble(); + } + catch (EndOfStreamException) + { + throw CreateDecodingError(nameof(ReadDouble), functionName); + } + } + + /// + /// Throws a BadDecodingError for the specific dataType and function. + /// + /// The datatype which reached the end of the stream. + /// The property which tried to read the datatype. + /// with + ServiceResultException CreateDecodingError(string dataTypeName, string functionName) + { + return ServiceResultException.Create(StatusCodes.BadDecodingError, + "Reading {0} in {1} reached end of stream.", dataTypeName, functionName); + } + /// /// Test and increment the nesting level. /// @@ -2437,17 +2617,14 @@ private void CheckAndIncrementNestingLevel() { if (m_nestingLevel > m_context.MaxEncodingNestingLevels) { - throw ServiceResultException.Create( - StatusCodes.BadEncodingLimitsExceeded, - "Maximum nesting level of {0} was exceeded", - m_context.MaxEncodingNestingLevels); + throw ServiceResultException.Create(StatusCodes.BadEncodingLimitsExceeded, + "Maximum nesting level of {0} was exceeded", m_context.MaxEncodingNestingLevels); } m_nestingLevel++; } #endregion #region Private Fields - private Stream m_istrm; private BinaryReader m_reader; private IServiceMessageContext m_context; private ushort[] m_namespaceMappings; diff --git a/Stack/Opc.Ua.Core/Types/Utils/DataComparer.cs b/Stack/Opc.Ua.Core/Types/Utils/DataComparer.cs index 5a7c3a3466..0ae049935d 100644 --- a/Stack/Opc.Ua.Core/Types/Utils/DataComparer.cs +++ b/Stack/Opc.Ua.Core/Types/Utils/DataComparer.cs @@ -969,9 +969,11 @@ public static object GetExtensionObjectBody(ExtensionObject value) if (body is byte[] bytes) { - BinaryDecoder decoder = new BinaryDecoder(bytes, context); - body = decoder.ReadEncodeable(null, expectedType); - decoder.Close(); + using (BinaryDecoder decoder = new BinaryDecoder(bytes, context)) + { + body = decoder.ReadEncodeable(null, expectedType); + decoder.Close(); + } return (IEncodeable)body; } diff --git a/Tests/Opc.Ua.PubSub.Tests/Encoding/UadpDataSetMessageTests.cs b/Tests/Opc.Ua.PubSub.Tests/Encoding/UadpDataSetMessageTests.cs index da48270b6e..f98efc87ee 100644 --- a/Tests/Opc.Ua.PubSub.Tests/Encoding/UadpDataSetMessageTests.cs +++ b/Tests/Opc.Ua.PubSub.Tests/Encoding/UadpDataSetMessageTests.cs @@ -257,17 +257,18 @@ public void ValidateMajorVersionEqMinorVersionEq( } UadpDataSetMessage uaDataSetMessageDecoded = new UadpDataSetMessage(); - BinaryDecoder decoder = new BinaryDecoder(bytes, messageContextEncode); + using (BinaryDecoder decoder = new BinaryDecoder(bytes, messageContextEncode)) + { - // Make sure the reader MajorVersion and MinorVersion are the same with the ones on the dataset message - DataSetReaderDataType reader = (DataSetReaderDataType)m_firstDataSetReaderType.MemberwiseClone(); - reader.DataSetMetaData.ConfigurationVersion.MajorVersion = versionValue; - reader.DataSetMetaData.ConfigurationVersion.MinorVersion = versionValue * 10; + // Make sure the reader MajorVersion and MinorVersion are the same with the ones on the dataset message + DataSetReaderDataType reader = (DataSetReaderDataType)m_firstDataSetReaderType.MemberwiseClone(); + reader.DataSetMetaData.ConfigurationVersion.MajorVersion = versionValue; + reader.DataSetMetaData.ConfigurationVersion.MinorVersion = versionValue * 10; - // workaround - uaDataSetMessageDecoded.DataSetWriterId = kTestDataSetWriterId; - uaDataSetMessageDecoded.DecodePossibleDataSetReader(decoder, reader); - decoder.Dispose(); + // workaround + uaDataSetMessageDecoded.DataSetWriterId = kTestDataSetWriterId; + uaDataSetMessageDecoded.DecodePossibleDataSetReader(decoder, reader); + } // Assert Assert.AreEqual(DataSetDecodeErrorReason.NoError, uaDataSetMessageDecoded.DecodeErrorReason); @@ -307,34 +308,34 @@ public void ValidateMajorVersionEqMinorVersionDiffer( IServiceMessageContext messageContextEncode = new ServiceMessageContext(); byte[] bytes; - var memoryStream = new MemoryStream(); + using (var memoryStream = new MemoryStream()) using (BinaryEncoder encoder = new BinaryEncoder(memoryStream, messageContextEncode, true)) { uadpDataSetMessage.Encode(encoder); _ = encoder.Close(); bytes = ReadBytes(memoryStream); - } - UadpDataSetMessage uaDataSetMessageDecoded = new UadpDataSetMessage(); - BinaryDecoder decoder = new BinaryDecoder(bytes, messageContextEncode); - - // Make sure the reader MajorVersion is same with the ones on the dataset message - // and MinorVersion differ - DataSetReaderDataType reader = (DataSetReaderDataType)m_firstDataSetReaderType.MemberwiseClone(); - reader.DataSetMetaData.ConfigurationVersion.MajorVersion = uadpDataSetMessage.MetaDataVersion.MajorVersion; - reader.DataSetMetaData.ConfigurationVersion.MinorVersion = uadpDataSetMessage.MetaDataVersion.MinorVersion + 1; + UadpDataSetMessage uaDataSetMessageDecoded = new UadpDataSetMessage(); + using (BinaryDecoder decoder = new BinaryDecoder(bytes, messageContextEncode)) + { + // Make sure the reader MajorVersion is same with the ones on the dataset message + // and MinorVersion differ + DataSetReaderDataType reader = (DataSetReaderDataType)m_firstDataSetReaderType.MemberwiseClone(); + reader.DataSetMetaData.ConfigurationVersion.MajorVersion = uadpDataSetMessage.MetaDataVersion.MajorVersion; + reader.DataSetMetaData.ConfigurationVersion.MinorVersion = uadpDataSetMessage.MetaDataVersion.MinorVersion + 1; - // workaround - uaDataSetMessageDecoded.DataSetWriterId = kTestDataSetWriterId; - uaDataSetMessageDecoded.DecodePossibleDataSetReader(decoder, reader); - decoder.Dispose(); + // workaround + uaDataSetMessageDecoded.DataSetWriterId = kTestDataSetWriterId; + uaDataSetMessageDecoded.DecodePossibleDataSetReader(decoder, reader); + } - // Assert - Assert.AreEqual(DataSetDecodeErrorReason.NoError, uaDataSetMessageDecoded.DecodeErrorReason); - Assert.AreEqual(false, uaDataSetMessageDecoded.IsMetadataMajorVersionChange); - Assert.AreNotEqual(null, uaDataSetMessageDecoded.DataSet); - // compare uadpDataSetMessage with uaDataSetMessageDecoded - CompareUadpDataSetMessages(uadpDataSetMessage, uaDataSetMessageDecoded); + // Assert + Assert.AreEqual(DataSetDecodeErrorReason.NoError, uaDataSetMessageDecoded.DecodeErrorReason); + Assert.AreEqual(false, uaDataSetMessageDecoded.IsMetadataMajorVersionChange); + Assert.AreNotEqual(null, uaDataSetMessageDecoded.DataSet); + // compare uadpDataSetMessage with uaDataSetMessageDecoded + CompareUadpDataSetMessages(uadpDataSetMessage, uaDataSetMessageDecoded); + } } [Test(Description = "Validate MajorVersion differ and MinorVersion are equal")] @@ -367,7 +368,7 @@ public void ValidateMajorVersionDiffMinorVersionEq( IServiceMessageContext messageContextEncode = new ServiceMessageContext(); byte[] bytes; - var memoryStream = new MemoryStream(); + using (var memoryStream = new MemoryStream()) using (BinaryEncoder encoder = new BinaryEncoder(memoryStream, messageContextEncode, true)) { uadpDataSetMessage.Encode(encoder); @@ -376,17 +377,17 @@ public void ValidateMajorVersionDiffMinorVersionEq( } UadpDataSetMessage uaDataSetMessageDecoded = new UadpDataSetMessage(); - BinaryDecoder decoder = new BinaryDecoder(bytes, messageContextEncode); - - // Make sure the reader MajorVersion differ and MinorVersion are equal - DataSetReaderDataType reader = (DataSetReaderDataType)m_firstDataSetReaderType.MemberwiseClone(); - reader.DataSetMetaData.ConfigurationVersion.MajorVersion = uadpDataSetMessage.MetaDataVersion.MajorVersion + 1; - reader.DataSetMetaData.ConfigurationVersion.MinorVersion = uadpDataSetMessage.MetaDataVersion.MinorVersion; - - // workaround - uaDataSetMessageDecoded.DataSetWriterId = kTestDataSetWriterId; - uaDataSetMessageDecoded.DecodePossibleDataSetReader(decoder, reader); - decoder.Dispose(); + using (BinaryDecoder decoder = new BinaryDecoder(bytes, messageContextEncode)) + { + // Make sure the reader MajorVersion differ and MinorVersion are equal + DataSetReaderDataType reader = (DataSetReaderDataType)m_firstDataSetReaderType.MemberwiseClone(); + reader.DataSetMetaData.ConfigurationVersion.MajorVersion = uadpDataSetMessage.MetaDataVersion.MajorVersion + 1; + reader.DataSetMetaData.ConfigurationVersion.MinorVersion = uadpDataSetMessage.MetaDataVersion.MinorVersion; + + // workaround + uaDataSetMessageDecoded.DataSetWriterId = kTestDataSetWriterId; + uaDataSetMessageDecoded.DecodePossibleDataSetReader(decoder, reader); + } // Assert Assert.AreEqual(DataSetDecodeErrorReason.MetadataMajorVersion, uaDataSetMessageDecoded.DecodeErrorReason); @@ -433,17 +434,18 @@ public void ValidateMajorVersionDiffMinorVersionDiff( } UadpDataSetMessage uaDataSetMessageDecoded = new UadpDataSetMessage(); - BinaryDecoder decoder = new BinaryDecoder(bytes, messageContextEncode); + using (BinaryDecoder decoder = new BinaryDecoder(bytes, messageContextEncode)) + { - // Make sure the reader MajorVersion differ and MinorVersion differ - DataSetReaderDataType reader = (DataSetReaderDataType)m_firstDataSetReaderType.MemberwiseClone(); - reader.DataSetMetaData.ConfigurationVersion.MajorVersion = uadpDataSetMessage.MetaDataVersion.MajorVersion + 1; - reader.DataSetMetaData.ConfigurationVersion.MinorVersion = uadpDataSetMessage.MetaDataVersion.MinorVersion + 1; + // Make sure the reader MajorVersion differ and MinorVersion differ + DataSetReaderDataType reader = (DataSetReaderDataType)m_firstDataSetReaderType.MemberwiseClone(); + reader.DataSetMetaData.ConfigurationVersion.MajorVersion = uadpDataSetMessage.MetaDataVersion.MajorVersion + 1; + reader.DataSetMetaData.ConfigurationVersion.MinorVersion = uadpDataSetMessage.MetaDataVersion.MinorVersion + 1; - // workaround - uaDataSetMessageDecoded.DataSetWriterId = kTestDataSetWriterId; - uaDataSetMessageDecoded.DecodePossibleDataSetReader(decoder, reader); - decoder.Dispose(); + // workaround + uaDataSetMessageDecoded.DataSetWriterId = kTestDataSetWriterId; + uaDataSetMessageDecoded.DecodePossibleDataSetReader(decoder, reader); + } // Assert Assert.AreEqual(DataSetDecodeErrorReason.MetadataMajorVersion, uaDataSetMessageDecoded.DecodeErrorReason); @@ -570,7 +572,7 @@ private void CompareEncodeDecode(UadpDataSetMessage uadpDataSetMessage) { IServiceMessageContext messageContextEncode = new ServiceMessageContext(); byte[] bytes; - var memoryStream = new MemoryStream(); + using (var memoryStream = new MemoryStream()) using (BinaryEncoder encoder = new BinaryEncoder(memoryStream, messageContextEncode, true)) { uadpDataSetMessage.Encode(encoder); @@ -579,12 +581,12 @@ private void CompareEncodeDecode(UadpDataSetMessage uadpDataSetMessage) } UadpDataSetMessage uaDataSetMessageDecoded = new UadpDataSetMessage(); - BinaryDecoder decoder = new BinaryDecoder(bytes, messageContextEncode); - - // workaround - uaDataSetMessageDecoded.DataSetWriterId = kTestDataSetWriterId; - uaDataSetMessageDecoded.DecodePossibleDataSetReader(decoder, m_firstDataSetReaderType); - decoder.Dispose(); + using (BinaryDecoder decoder = new BinaryDecoder(bytes, messageContextEncode)) + { + // workaround + uaDataSetMessageDecoded.DataSetWriterId = kTestDataSetWriterId; + uaDataSetMessageDecoded.DecodePossibleDataSetReader(decoder, m_firstDataSetReaderType); + } // compare uadpDataSetMessage with uaDataSetMessageDecoded CompareUadpDataSetMessages(uadpDataSetMessage, uaDataSetMessageDecoded); From 453d056ca800041df4617983f6cdc0d2c40df9fc Mon Sep 17 00:00:00 2001 From: Martin Regen Date: Fri, 19 Apr 2024 19:33:23 +0200 Subject: [PATCH 06/83] seek bug --- Stack/Opc.Ua.Core/Stack/Bindings/ArraySegmentStream.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Stack/Opc.Ua.Core/Stack/Bindings/ArraySegmentStream.cs b/Stack/Opc.Ua.Core/Stack/Bindings/ArraySegmentStream.cs index 8c4a53147d..3e813684c3 100644 --- a/Stack/Opc.Ua.Core/Stack/Bindings/ArraySegmentStream.cs +++ b/Stack/Opc.Ua.Core/Stack/Bindings/ArraySegmentStream.cs @@ -352,7 +352,7 @@ public override long Seek(long offset, SeekOrigin origin) int position = (int)offset; - if (position >= GetAbsolutePosition()) + if (position > GetAbsolutePosition()) { CheckEndOfStream(); } From 341083e76a2ed82af48696bc08d75c31f5d25f9d Mon Sep 17 00:00:00 2001 From: Martin Regen Date: Fri, 19 Apr 2024 19:37:26 +0200 Subject: [PATCH 07/83] use arraysegmentstream --- Fuzzing/BinaryDecoder/Fuzz/FuzzTarget.cs | 31 +++++++++++++++++++++++- 1 file changed, 30 insertions(+), 1 deletion(-) diff --git a/Fuzzing/BinaryDecoder/Fuzz/FuzzTarget.cs b/Fuzzing/BinaryDecoder/Fuzz/FuzzTarget.cs index 8390e6de0d..21f792c212 100644 --- a/Fuzzing/BinaryDecoder/Fuzz/FuzzTarget.cs +++ b/Fuzzing/BinaryDecoder/Fuzz/FuzzTarget.cs @@ -1,12 +1,15 @@ +using System; using System.IO; using Opc.Ua; +using Opc.Ua.Bindings; namespace BinaryDecoder.Fuzz { public static class FuzzableCode { + const int SegmentSize = 0x40; private static ServiceMessageContext messageContext = null; public static void FuzzTarget(Stream stream) @@ -18,14 +21,40 @@ public static void FuzzTarget(Stream stream) try { + // fuzzer uses a non seekable stream, causing false positives + // use ArraySegmentStream in combination with fuzzed decoder... + { + using (var binaryStream = new BinaryReader(stream)) + { + var bufferCollection = new BufferCollection(); + byte[] buffer; + do + { + buffer = binaryStream.ReadBytes(SegmentSize); + bufferCollection.Add(buffer); + } while (buffer.Length == SegmentSize); + stream = new ArraySegmentStream(bufferCollection); + } + } + using (var decoder = new Opc.Ua.BinaryDecoder(stream, messageContext)) { _ = decoder.DecodeMessage(null); } } - catch (ServiceResultException) + catch (Exception ex) { + if (ex is ServiceResultException sre) + { + switch (sre.StatusCode) + { + case StatusCodes.BadEncodingLimitsExceeded: + case StatusCodes.BadDecodingError: + return; + } + } + throw; } } } From 95248e839010e412d3a9ce077fbe1e44fa78654e Mon Sep 17 00:00:00 2001 From: Martin Regen Date: Sat, 20 Apr 2024 09:37:11 +0200 Subject: [PATCH 08/83] fix project --- .gitignore | 3 +++ Fuzzing/Fuzzing.md | 2 +- UA Fuzzing.sln | 11 +++++++++++ 3 files changed, 15 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index 9f4b76e9dc..b0a88bcdb7 100644 --- a/.gitignore +++ b/.gitignore @@ -262,3 +262,6 @@ OPC\ Foundation/ /SampleApplications/Samples/OPCOutput !**/Assets/*.* *.Nodeset2.xml.zip + +# UA Fuzzing findings folders +/Fuzzing/**/findings/ diff --git a/Fuzzing/Fuzzing.md b/Fuzzing/Fuzzing.md index 3fce984c7f..36bd626fec 100644 --- a/Fuzzing/Fuzzing.md +++ b/Fuzzing/Fuzzing.md @@ -20,7 +20,7 @@ sudo apt-get update sudo apt-get install -y build-essential cmake git # Install .NET 8.0 SDK on Ubuntu -sudo apt-get install -y dotnet-sdk-8.0 powershell +sudo apt-get install -y dotnet-sdk-8.0 ``` The supplied scripts require powershell on Linux to be installed. diff --git a/UA Fuzzing.sln b/UA Fuzzing.sln index fb09de359e..cf348bf52f 100644 --- a/UA Fuzzing.sln +++ b/UA Fuzzing.sln @@ -69,6 +69,16 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "BinaryDecoder.Fuzz", "Fuzzi EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "BinaryDecoder.Fuzztools", "Fuzzing\BinaryDecoder\Fuzztools\BinaryDecoder.Fuzztools.csproj", "{352C998B-D884-4C25-BCE3-8AFBF371C380}" EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "scripts", "scripts", "{612E66DA-731B-4E7C-969D-88079601ED30}" + ProjectSection(SolutionItems) = preProject + Fuzzing\scripts\fuzz-libfuzzer.ps1 = Fuzzing\scripts\fuzz-libfuzzer.ps1 + Fuzzing\scripts\fuzz.ps1 = Fuzzing\scripts\fuzz.ps1 + Fuzzing\scripts\install.sh = Fuzzing\scripts\install.sh + Fuzzing\scripts\readme.txt = Fuzzing\scripts\readme.txt + Fuzzing\scripts\test-libfuzzer.ps1 = Fuzzing\scripts\test-libfuzzer.ps1 + Fuzzing\scripts\test.ps1 = Fuzzing\scripts\test.ps1 + EndProjectSection +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -204,6 +214,7 @@ Global {4B72937F-5A57-4CEA-B9FE-B4C45CF7B284} = {ACBF012E-08A7-4939-88CF-D6B610E35AC5} {92CC62D7-B7B6-4FBD-B9AC-181BD0D11EC0} = {128EE105-0A7A-4AB3-81E6-A77AE638B33F} {352C998B-D884-4C25-BCE3-8AFBF371C380} = {128EE105-0A7A-4AB3-81E6-A77AE638B33F} + {612E66DA-731B-4E7C-969D-88079601ED30} = {128EE105-0A7A-4AB3-81E6-A77AE638B33F} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {33FA5BCB-C827-4D44-AECF-F51342DFE64A} From 4888f4a455bd27fec1bf2ce05a2d2f6a06f72758 Mon Sep 17 00:00:00 2001 From: Martin Regen Date: Sat, 20 Apr 2024 19:28:05 +0200 Subject: [PATCH 09/83] updates --- .gitignore | 2 + .../Fuzz/BinaryDecoder.Fuzz.csproj | 7 +- Fuzzing/BinaryDecoder/Fuzz/FuzzTarget.cs | 120 ++++++++--------- Fuzzing/BinaryDecoder/Fuzz/Program.cs | 19 +-- ...csproj => BinaryDecoder.Fuzz.Tools.csproj} | 5 +- Fuzzing/BinaryDecoder/Fuzztools/Playback.cs | 27 ++-- Fuzzing/BinaryDecoder/Fuzztools/Program.cs | 89 ++++++------- Fuzzing/BinaryDecoder/Fuzztools/Testcases.cs | 122 +++++++----------- Fuzzing/BinaryDecoder/libfuzz.bat | 1 + Fuzzing/BinaryDecoder/libfuzz.sh | 1 + Fuzzing/scripts/fuzz-libfuzzer.ps1 | 3 +- Fuzzing/scripts/fuzz.ps1 | 2 +- Fuzzing/scripts/readme.txt | 2 +- Fuzzing/scripts/test-libfuzzer.ps1 | 29 ----- Fuzzing/scripts/test.ps1 | 22 ---- Stack/Opc.Ua.Core/Types/BuiltIn/Matrix.cs | 9 +- UA Fuzzing.sln | 7 +- targets.props | 2 +- 18 files changed, 198 insertions(+), 271 deletions(-) rename Fuzzing/BinaryDecoder/Fuzztools/{BinaryDecoder.Fuzztools.csproj => BinaryDecoder.Fuzz.Tools.csproj} (77%) create mode 100644 Fuzzing/BinaryDecoder/libfuzz.bat create mode 100644 Fuzzing/BinaryDecoder/libfuzz.sh delete mode 100644 Fuzzing/scripts/test-libfuzzer.ps1 delete mode 100644 Fuzzing/scripts/test.ps1 diff --git a/.gitignore b/.gitignore index b0a88bcdb7..ff104459c0 100644 --- a/.gitignore +++ b/.gitignore @@ -265,3 +265,5 @@ OPC\ Foundation/ # UA Fuzzing findings folders /Fuzzing/**/findings/ +/Fuzzing/**/Testcases/ +!/Fuzzing/**/Testcases/*.bin \ No newline at end of file diff --git a/Fuzzing/BinaryDecoder/Fuzz/BinaryDecoder.Fuzz.csproj b/Fuzzing/BinaryDecoder/Fuzz/BinaryDecoder.Fuzz.csproj index f170a32bdc..a3a6a21f0c 100644 --- a/Fuzzing/BinaryDecoder/Fuzz/BinaryDecoder.Fuzz.csproj +++ b/Fuzzing/BinaryDecoder/Fuzz/BinaryDecoder.Fuzz.csproj @@ -2,8 +2,13 @@ Exe - net8.0 + $(AppTargetFramework) BinaryDecoder.Fuzz + BinaryDecoder.Fuzz + + + + $(DefineConstants);LIBFUZZER diff --git a/Fuzzing/BinaryDecoder/Fuzz/FuzzTarget.cs b/Fuzzing/BinaryDecoder/Fuzz/FuzzTarget.cs index 21f792c212..604dbd8635 100644 --- a/Fuzzing/BinaryDecoder/Fuzz/FuzzTarget.cs +++ b/Fuzzing/BinaryDecoder/Fuzz/FuzzTarget.cs @@ -5,84 +5,76 @@ using Opc.Ua; using Opc.Ua.Bindings; -namespace BinaryDecoder.Fuzz +public static class FuzzableCode { - public static class FuzzableCode - { - const int SegmentSize = 0x40; - private static ServiceMessageContext messageContext = null; + const int SegmentSize = 0x40; + private static ServiceMessageContext messageContext = null; - public static void FuzzTarget(Stream stream) + /// + /// The fuzz target for afl-fuzz. + /// + /// The stdin stream from the afl-fuzz process. + public static void FuzzTarget(Stream stream) + { + // fuzzer uses a non seekable stream, causing false positives + // use ArraySegmentStream in combination with fuzzed decoder... + MemoryStream memoryStream; + using (var binaryStream = new BinaryReader(stream)) { - if (messageContext == null) - { - messageContext = new ServiceMessageContext(); - } - - try + var bufferCollection = new BufferCollection(); + byte[] buffer; + do { - // fuzzer uses a non seekable stream, causing false positives - // use ArraySegmentStream in combination with fuzzed decoder... - { - using (var binaryStream = new BinaryReader(stream)) - { - var bufferCollection = new BufferCollection(); - byte[] buffer; - do - { - buffer = binaryStream.ReadBytes(SegmentSize); - bufferCollection.Add(buffer); - } while (buffer.Length == SegmentSize); - stream = new ArraySegmentStream(bufferCollection); - } - } + buffer = binaryStream.ReadBytes(SegmentSize); + bufferCollection.Add(buffer); + } while (buffer.Length == SegmentSize); + memoryStream = new ArraySegmentStream(bufferCollection); + } - using (var decoder = new Opc.Ua.BinaryDecoder(stream, messageContext)) - { - _ = decoder.DecodeMessage(null); - } - } - catch (Exception ex) - { - if (ex is ServiceResultException sre) - { - switch (sre.StatusCode) - { - case StatusCodes.BadEncodingLimitsExceeded: - case StatusCodes.BadDecodingError: - return; - } - } + FuzzTargetCore(memoryStream); + } - throw; - } - } + /// + /// The fuzz target for libfuzzer. + /// + public static void FuzzTargetLibfuzzer(ReadOnlySpan input) + { + var memoryStream = new MemoryStream(input.ToArray()); + FuzzTargetCore(memoryStream); } -} -#if mist - public static class FuzzableCode + /// + /// The fuzz target for the BinaryDecoder. + /// + /// A memory stream with fuzz content. + private static void FuzzTargetCore(MemoryStream stream) { - //public static void FuzzTargetMethod(ReadOnlySpan input) - public static void FuzzTargetMethod(byte[] input) + if (messageContext == null) { - try + messageContext = new ServiceMessageContext(); + } + + try + { + using (var decoder = new Opc.Ua.BinaryDecoder(stream, messageContext)) { - var messageContext = new ServiceMessageContext(); - using (var decoder = new BinaryDecoder(input, messageContext)) - { - decoder.DecodeMessage(null); - } + _ = decoder.DecodeMessage(null); } - catch (Exception ex) when (ex is ServiceResultException) + } + catch (Exception ex) + { + if (ex is ServiceResultException sre) { - // This is an example. You should filter out any - // *expected* exception(s) from your code here, - // but it’s an anti-pattern to catch *all* Exceptions, - // as you might suppress legitimate problems, such as - // your code throwing a NullReferenceException. + switch (sre.StatusCode) + { + case StatusCodes.BadEncodingLimitsExceeded: + case StatusCodes.BadDecodingError: + return; + } } + + throw; } } -#endif +} diff --git a/Fuzzing/BinaryDecoder/Fuzz/Program.cs b/Fuzzing/BinaryDecoder/Fuzz/Program.cs index 3b9334fe40..bb065b7993 100644 --- a/Fuzzing/BinaryDecoder/Fuzz/Program.cs +++ b/Fuzzing/BinaryDecoder/Fuzz/Program.cs @@ -2,15 +2,18 @@ using SharpFuzz; -namespace BinaryDecoder.Fuzz +public static class Program { - public static class Program + public static void Main(string[] args) { - public static void Main(string[] args) - { - Fuzzer.Run(stream => { - FuzzableCode.FuzzTarget(stream); - }); - } +#if LIBFUZZER + Fuzzer.LibFuzzer.Run(input => { + FuzzableCode.FuzzTargetLibfuzzer(input); + }); +#else + Fuzzer.Run(stream => { + FuzzableCode.FuzzTarget(stream); + }); +#endif } } diff --git a/Fuzzing/BinaryDecoder/Fuzztools/BinaryDecoder.Fuzztools.csproj b/Fuzzing/BinaryDecoder/Fuzztools/BinaryDecoder.Fuzz.Tools.csproj similarity index 77% rename from Fuzzing/BinaryDecoder/Fuzztools/BinaryDecoder.Fuzztools.csproj rename to Fuzzing/BinaryDecoder/Fuzztools/BinaryDecoder.Fuzz.Tools.csproj index 757095f41b..da65f9fdf3 100644 --- a/Fuzzing/BinaryDecoder/Fuzztools/BinaryDecoder.Fuzztools.csproj +++ b/Fuzzing/BinaryDecoder/Fuzztools/BinaryDecoder.Fuzz.Tools.csproj @@ -2,8 +2,9 @@ Exe - net8.0 - BinaryDecoder.Fuzztools + $(AppTargetFramework) + BinaryDecoder.Fuzz.Tools + BinaryDecoder.Fuzz.Tools diff --git a/Fuzzing/BinaryDecoder/Fuzztools/Playback.cs b/Fuzzing/BinaryDecoder/Fuzztools/Playback.cs index 2c1546bf09..9b0cdf7c1a 100644 --- a/Fuzzing/BinaryDecoder/Fuzztools/Playback.cs +++ b/Fuzzing/BinaryDecoder/Fuzztools/Playback.cs @@ -1,29 +1,28 @@ using System.IO; using System; -using BinaryDecoder.Fuzz; -namespace BinaryDecoder.Fuzztools +public static class Playback { - public static class Playback + public static void Run(string directoryPath, bool stackTrace) { - public static void Run(string directoryPath) + foreach (var crashFile in Directory.EnumerateFiles(directoryPath)) { - foreach (var crashFile in Directory.EnumerateFiles(directoryPath)) + using (var stream = new MemoryStream(File.ReadAllBytes(crashFile))) { - using (var stream = new MemoryStream(File.ReadAllBytes(crashFile))) + try { - try - { - FuzzableCode.FuzzTarget(stream); - } - catch (Exception ex) + FuzzableCode.FuzzTarget(stream); + } + catch (Exception ex) + { + Console.WriteLine("{0}:{1}", ex.GetType().Name, ex.Message); + if (stackTrace) { - Console.WriteLine("{0}:{1}", ex.GetType().Name, ex.Message); Console.WriteLine(ex.StackTrace); } } - }; - } + } + }; } } diff --git a/Fuzzing/BinaryDecoder/Fuzztools/Program.cs b/Fuzzing/BinaryDecoder/Fuzztools/Program.cs index 55ae258b3d..da2df734fe 100644 --- a/Fuzzing/BinaryDecoder/Fuzztools/Program.cs +++ b/Fuzzing/BinaryDecoder/Fuzztools/Program.cs @@ -4,62 +4,63 @@ using Mono.Options; using System.Collections.Generic; -namespace BinaryDecoder.Fuzztools +public static class Program { - public static class Program - { - public static readonly string ApplicationName = "BinaryDecoder.Fuzztools"; - public static readonly string DefaultTestcasesFolder = "../../../../Fuzz/Testcases"; - public static readonly string DefaultFindingsCrashFolder = "../../../../findings/crashes"; + public static readonly string ApplicationName = "BinaryDecoder.Fuzztools"; + public static readonly string DefaultTestcasesFolder = "../../../../Fuzz/Testcases"; + public static readonly string DefaultFindingsCrashFolder = "../../../../findings/crashes"; + public static readonly string DefaultFindingsHangFolder = "../../../../findings/crashes"; + public static readonly string DefaultLibFuzzerCrashes = "../../../crash-*"; + public static readonly string DefaultLibFuzzerHangs = "../../../timeout-*"; - public static void Main(string[] args) - { - var applicationName = "BinaryDecoder.Fuzztools"; - TextWriter output = Console.Out; + public static void Main(string[] args) + { + var applicationName = "BinaryDecoder.Fuzztools"; + TextWriter output = Console.Out; - output.WriteLine($"OPC UA {applicationName}"); - var usage = $"Usage: {applicationName}.exe [OPTIONS]"; + output.WriteLine($"OPC UA {applicationName}"); + var usage = $"Usage: {applicationName}.exe [OPTIONS]"; - bool showHelp = false; - bool playback = false; - bool testcases = false; + bool showHelp = false; + bool playback = false; + bool testcases = false; + bool stacktrace = false; - OptionSet options = new OptionSet { + OptionSet options = new OptionSet { usage, { "h|help", "show this message and exit", h => showHelp = h != null }, - { "p|playback", "playback crashes found in findings", p => playback = p != null }, + { "p|playback", "playback crashes found by afl-fuzz or libfuzzer", p => playback = p != null }, { "t|testcases", "create test cases for fuzzing", t => testcases = t != null }, + { "s|stacktrace", "show stacktrace with playback", s => stacktrace = s != null }, }; - IList extraArgs = null; - try - { - extraArgs = options.Parse(args); - } - catch (OptionException e) - { - output.WriteLine(e.Message); - showHelp = true; - } - - if (testcases) - { - Testcases.Run(DefaultTestcasesFolder); - } - else if (playback) - { - Playback.Run(DefaultFindingsCrashFolder); - } - else - { - showHelp = true; - } + IList extraArgs = null; + try + { + extraArgs = options.Parse(args); + } + catch (OptionException e) + { + output.WriteLine(e.Message); + showHelp = true; + } - if (showHelp) - { - options.WriteOptionDescriptions(output); - } + if (testcases) + { + Testcases.Run(DefaultTestcasesFolder); + } + else if (playback) + { + Playback.Run(DefaultFindingsCrashFolder, stacktrace); + } + else + { + showHelp = true; + } + if (showHelp) + { + options.WriteOptionDescriptions(output); } } } diff --git a/Fuzzing/BinaryDecoder/Fuzztools/Testcases.cs b/Fuzzing/BinaryDecoder/Fuzztools/Testcases.cs index f1b8a0b674..d7daa5cd78 100644 --- a/Fuzzing/BinaryDecoder/Fuzztools/Testcases.cs +++ b/Fuzzing/BinaryDecoder/Fuzztools/Testcases.cs @@ -1,23 +1,19 @@ using System; using System.IO; -using BinaryDecoder.Fuzz; using Opc.Ua; -namespace BinaryDecoder.Fuzztools +public static class Testcases { + private static ServiceMessageContext s_messageContext = new ServiceMessageContext(); - public static class Testcases + private static byte[] CreateReadRequest() { - private static ServiceMessageContext s_messageContext = new ServiceMessageContext(); - - private static byte[] CreateReadRequest() + using (var encoder = new BinaryEncoder(s_messageContext)) { - using (var encoder = new BinaryEncoder(s_messageContext)) - { - var nodeId = new NodeId(1000); - var readRequest = new ReadRequest { - NodesToRead = new ReadValueIdCollection { + var nodeId = new NodeId(1000); + var readRequest = new ReadRequest { + NodesToRead = new ReadValueIdCollection { new ReadValueId { NodeId = nodeId, AttributeId = Attributes.Description, @@ -39,20 +35,20 @@ private static byte[] CreateReadRequest() AttributeId = Attributes.RolePermissions, }, }, - }; - encoder.EncodeMessage(readRequest); - return encoder.CloseAndReturnBuffer(); - } + }; + encoder.EncodeMessage(readRequest); + return encoder.CloseAndReturnBuffer(); } + } - private static byte[] CreateReadResponse() + private static byte[] CreateReadResponse() + { + var now = DateTime.UtcNow; + using (var encoder = new BinaryEncoder(s_messageContext)) { - var now = DateTime.UtcNow; - using (var encoder = new BinaryEncoder(s_messageContext)) - { - var nodeId = new NodeId(1000); - var readRequest = new ReadResponse { - Results = new DataValueCollection { + var nodeId = new NodeId(1000); + var readRequest = new ReadResponse { + Results = new DataValueCollection { new DataValue { Value = new Variant("Hello World"), ServerTimestamp = now, @@ -77,7 +73,7 @@ private static byte[] CreateReadResponse() Value = new Variant((byte)42), }, }, - DiagnosticInfos = new DiagnosticInfoCollection { + DiagnosticInfos = new DiagnosticInfoCollection { new DiagnosticInfo { AdditionalInfo = "Hello World", InnerStatusCode = StatusCodes.BadCertificateHostNameInvalid, @@ -87,75 +83,49 @@ private static byte[] CreateReadResponse() }, }, }, - ResponseHeader = new ResponseHeader { - Timestamp = DateTime.UtcNow, - RequestHandle = 42, - ServiceResult = StatusCodes.Good, - ServiceDiagnostics = new DiagnosticInfo { - AdditionalInfo = "NodeId not found", - InnerStatusCode = StatusCodes.BadNodeIdExists, + ResponseHeader = new ResponseHeader { + Timestamp = DateTime.UtcNow, + RequestHandle = 42, + ServiceResult = StatusCodes.Good, + ServiceDiagnostics = new DiagnosticInfo { + AdditionalInfo = "NodeId not found", + InnerStatusCode = StatusCodes.BadNodeIdExists, + InnerDiagnosticInfo = new DiagnosticInfo { + AdditionalInfo = "Hello World", + InnerStatusCode = StatusCodes.BadNodeIdUnknown, InnerDiagnosticInfo = new DiagnosticInfo { AdditionalInfo = "Hello World", InnerStatusCode = StatusCodes.BadNodeIdUnknown, InnerDiagnosticInfo = new DiagnosticInfo { AdditionalInfo = "Hello World", InnerStatusCode = StatusCodes.BadNodeIdUnknown, - InnerDiagnosticInfo = new DiagnosticInfo { - AdditionalInfo = "Hello World", - InnerStatusCode = StatusCodes.BadNodeIdUnknown, - }, }, }, }, }, - }; - encoder.EncodeMessage(readRequest); - return encoder.CloseAndReturnBuffer(); - } - } - - public static void Run(string directoryPath) - { - var readRequest = CreateReadRequest(); - FuzzTestcase(readRequest); - File.WriteAllBytes(Path.Combine(directoryPath, "readrequest.bin"), readRequest); - var readResponse = CreateReadResponse(); - FuzzTestcase(readResponse); - File.WriteAllBytes(Path.Combine(directoryPath, "readresponse.bin"), readResponse); + }, + }; + encoder.EncodeMessage(readRequest); + return encoder.CloseAndReturnBuffer(); } + } - public static void FuzzTestcase(byte[] message) - { - using (var stream = new MemoryStream(message)) - { - FuzzableCode.FuzzTarget(stream); - } - } + public static void Run(string directoryPath) + { + var readRequest = CreateReadRequest(); + FuzzTestcase(readRequest); + File.WriteAllBytes(Path.Combine(directoryPath, "readrequest.bin"), readRequest); + var readResponse = CreateReadResponse(); + FuzzTestcase(readResponse); + File.WriteAllBytes(Path.Combine(directoryPath, "readresponse.bin"), readResponse); } -#if mist - public static class FuzzableCode + public static void FuzzTestcase(byte[] message) { - //public static void FuzzTargetMethod(ReadOnlySpan input) - public static void FuzzTargetMethod(byte[] input) + using (var stream = new MemoryStream(message)) { - try - { - var messageContext = new ServiceMessageContext(); - using (var decoder = new BinaryDecoder(input, messageContext)) - { - decoder.DecodeMessage(null); - } - } - catch (Exception ex) when (ex is ServiceResultException) - { - // This is an example. You should filter out any - // *expected* exception(s) from your code here, - // but it’s an anti-pattern to catch *all* Exceptions, - // as you might suppress legitimate problems, such as - // your code throwing a NullReferenceException. - } + FuzzableCode.FuzzTarget(stream); } } -#endif + } diff --git a/Fuzzing/BinaryDecoder/libfuzz.bat b/Fuzzing/BinaryDecoder/libfuzz.bat new file mode 100644 index 0000000000..3fcd16bd47 --- /dev/null +++ b/Fuzzing/BinaryDecoder/libfuzz.bat @@ -0,0 +1 @@ +Powershell -File ..\scripts\fuzz-libfuzzer.ps1 -libFuzzer ".\libfuzzer-dotnet-windows.exe" -project .\Fuzz\BinaryDecoder.Fuzz.csproj -corpus .\Fuzz\Testcases \ No newline at end of file diff --git a/Fuzzing/BinaryDecoder/libfuzz.sh b/Fuzzing/BinaryDecoder/libfuzz.sh new file mode 100644 index 0000000000..d9b1c64674 --- /dev/null +++ b/Fuzzing/BinaryDecoder/libfuzz.sh @@ -0,0 +1 @@ +pwsh ../scripts/fuzz-libfuzzer.ps1 -libFuzzer "./libfuzzer-dotnet-ubuntu" -project ./Fuzz/BinaryDecoder.Fuzz.csproj -corpus ./Fuzz/Testcases/ \ No newline at end of file diff --git a/Fuzzing/scripts/fuzz-libfuzzer.ps1 b/Fuzzing/scripts/fuzz-libfuzzer.ps1 index f27fc906ca..0c1883a3c6 100644 --- a/Fuzzing/scripts/fuzz-libfuzzer.ps1 +++ b/Fuzzing/scripts/fuzz-libfuzzer.ps1 @@ -18,7 +18,7 @@ if (Test-Path $outputDir) { Remove-Item -Recurse -Force $outputDir } -dotnet publish $project -c release -o $outputDir +dotnet.exe publish $project -p:LibFuzzer=true -c release --force -o $outputDir $projectName = (Get-Item $project).BaseName $projectDll = "$projectName.dll" @@ -50,6 +50,7 @@ foreach ($fuzzingTarget in $fuzzingTargets) { } } +Write-Output "Start $libFuzzer" if ($dict) { & $libFuzzer -timeout="$timeout" -dict="$dict" --target_path=dotnet --target_arg=$project $corpus } diff --git a/Fuzzing/scripts/fuzz.ps1 b/Fuzzing/scripts/fuzz.ps1 index 12909a86ba..938098117b 100644 --- a/Fuzzing/scripts/fuzz.ps1 +++ b/Fuzzing/scripts/fuzz.ps1 @@ -21,7 +21,7 @@ if (Test-Path $findingsDir) { Remove-Item -Recurse -Force $findingsDir } -dotnet publish $project -c release -o $outputDir +dotnet publish $project --force -c release -o $outputDir $projectName = (Get-Item $project).BaseName $projectDll = "$projectName.dll" diff --git a/Fuzzing/scripts/readme.txt b/Fuzzing/scripts/readme.txt index b027025702..a3f6daa9ff 100644 --- a/Fuzzing/scripts/readme.txt +++ b/Fuzzing/scripts/readme.txt @@ -1 +1 @@ -for script updates see https://github.com/Metalnem/sharpfuzz/tree/master/scripts \ No newline at end of file +the scripts were originally from here: see https://github.com/Metalnem/sharpfuzz/tree/master/scripts \ No newline at end of file diff --git a/Fuzzing/scripts/test-libfuzzer.ps1 b/Fuzzing/scripts/test-libfuzzer.ps1 deleted file mode 100644 index db9cb90028..0000000000 --- a/Fuzzing/scripts/test-libfuzzer.ps1 +++ /dev/null @@ -1,29 +0,0 @@ -$libFuzzer = "libfuzzer-dotnet-windows.exe" -$uri = "https://github.com/metalnem/libfuzzer-dotnet/releases/latest/download/$libFuzzer" -$corpus = "corpus" - -Invoke-WebRequest -Uri $uri -OutFile $libFuzzer -New-Item -Path $corpus -ItemType Directory - -dotnet publish src/SharpFuzz.CommandLine/SharpFuzz.CommandLine.csproj ` - --output out ` - --configuration release ` - --framework net8.0 - -& scripts/fuzz-libfuzzer.ps1 ` - -libFuzzer "./$libFuzzer" ` - -project tests/Library.LibFuzzer/Library.LibFuzzer.csproj ` - -corpus $corpus ` - -command out/SharpFuzz.CommandLine - -$crasher = "Whoopsie" -$output = Get-ChildItem -Path "timeout-*" -$content = Get-Content -Path $output.FullName -Raw - -if (-not $content.Contains($crasher)) { - Write-Error "Crasher is missing from the libFuzzer output" - exit 1 -} - -Write-Host $crasher -exit 0 diff --git a/Fuzzing/scripts/test.ps1 b/Fuzzing/scripts/test.ps1 deleted file mode 100644 index 874b2f1cdc..0000000000 --- a/Fuzzing/scripts/test.ps1 +++ /dev/null @@ -1,22 +0,0 @@ -New-Item -Path "corpus/test" -ItemType File -Force -Value "W" - -dotnet publish src/SharpFuzz.CommandLine/SharpFuzz.CommandLine.csproj ` - --output out ` - --configuration release ` - --framework net8.0 - -& scripts/fuzz.ps1 ` - -project tests/Library.Fuzz/Library.Fuzz.csproj ` - -i corpus ` - -command out/SharpFuzz.CommandLine - -$output = Get-Content -Path "./findings/.cur_input" -Raw -$crasher = "Whoopsie" - -if (-not $output.Contains($crasher)) { - Write-Error "Crasher is missing from the AFL output" - exit 1 -} - -Write-Host $crasher -exit 0 diff --git a/Stack/Opc.Ua.Core/Types/BuiltIn/Matrix.cs b/Stack/Opc.Ua.Core/Types/BuiltIn/Matrix.cs index 064d5578fd..71f99dc620 100644 --- a/Stack/Opc.Ua.Core/Types/BuiltIn/Matrix.cs +++ b/Stack/Opc.Ua.Core/Types/BuiltIn/Matrix.cs @@ -392,15 +392,14 @@ private static (bool valid, int flatLength) ValidateDimensions(Int32Collection d } catch (OverflowException) { - throw new ArgumentException("The dimensions of the matrix are invalid and overflow when used to calculate the size."); + throw ServiceResultException.Create(StatusCodes.BadEncodingLimitsExceeded, + "The dimensions of the matrix are invalid and overflow when used to calculate the size."); } if ((maxArrayLength > 0) && (flatLength > maxArrayLength)) { - throw ServiceResultException.Create( - StatusCodes.BadEncodingLimitsExceeded, + throw ServiceResultException.Create(StatusCodes.BadEncodingLimitsExceeded, "Maximum array length of {0} was exceeded while summing up to {1} from the array dimensions", - maxArrayLength, - flatLength + maxArrayLength, flatLength ); } diff --git a/UA Fuzzing.sln b/UA Fuzzing.sln index cf348bf52f..c2c7ae5c06 100644 --- a/UA Fuzzing.sln +++ b/UA Fuzzing.sln @@ -67,7 +67,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "BinaryDecoder", "BinaryDeco EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "BinaryDecoder.Fuzz", "Fuzzing\BinaryDecoder\Fuzz\BinaryDecoder.Fuzz.csproj", "{92CC62D7-B7B6-4FBD-B9AC-181BD0D11EC0}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "BinaryDecoder.Fuzztools", "Fuzzing\BinaryDecoder\Fuzztools\BinaryDecoder.Fuzztools.csproj", "{352C998B-D884-4C25-BCE3-8AFBF371C380}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "BinaryDecoder.Fuzz.Tools", "Fuzzing\BinaryDecoder\Fuzztools\BinaryDecoder.Fuzz.Tools.csproj", "{352C998B-D884-4C25-BCE3-8AFBF371C380}" EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "scripts", "scripts", "{612E66DA-731B-4E7C-969D-88079601ED30}" ProjectSection(SolutionItems) = preProject @@ -79,6 +79,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "scripts", "scripts", "{612E Fuzzing\scripts\test.ps1 = Fuzzing\scripts\test.ps1 EndProjectSection EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Fuzzing", "Fuzzing", "{E9435CCE-1591-4AC0-87C4-341236E27C50}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -212,9 +214,10 @@ Global {FE9EEB39-0698-4A19-B770-E66836CE0002} = {ACBF012E-08A7-4939-88CF-D6B610E35AC5} {92D98D3D-2A7D-4F4B-8C72-1A98908D0221} = {2DC9F7F3-6698-4875-88A3-50678170A810} {4B72937F-5A57-4CEA-B9FE-B4C45CF7B284} = {ACBF012E-08A7-4939-88CF-D6B610E35AC5} + {128EE105-0A7A-4AB3-81E6-A77AE638B33F} = {E9435CCE-1591-4AC0-87C4-341236E27C50} {92CC62D7-B7B6-4FBD-B9AC-181BD0D11EC0} = {128EE105-0A7A-4AB3-81E6-A77AE638B33F} {352C998B-D884-4C25-BCE3-8AFBF371C380} = {128EE105-0A7A-4AB3-81E6-A77AE638B33F} - {612E66DA-731B-4E7C-969D-88079601ED30} = {128EE105-0A7A-4AB3-81E6-A77AE638B33F} + {612E66DA-731B-4E7C-969D-88079601ED30} = {E9435CCE-1591-4AC0-87C4-341236E27C50} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {33FA5BCB-C827-4D44-AECF-F51342DFE64A} diff --git a/targets.props b/targets.props index f13e729809..2739c0be55 100644 --- a/targets.props +++ b/targets.props @@ -116,7 +116,7 @@ preview-all net6.0;net48 - net6.0 + net8.0 net48;net6.0 net48;netstandard2.1;net6.0;net8.0 net48;netstandard2.0;netstandard2.1;net6.0;net8.0 From aac23f29a607a00cf5659585c66131ceb82d1000 Mon Sep 17 00:00:00 2001 From: Martin Regen Date: Sat, 20 Apr 2024 19:43:42 +0200 Subject: [PATCH 10/83] move, rename --- .../BinaryDecoder.Fuzz.Tools.csproj | 0 .../{Fuzztools => Fuzz.Tools}/Playback.cs | 0 .../{Fuzztools => Fuzz.Tools}/Program.cs | 0 .../{Fuzztools => Fuzz.Tools}/Testcases.cs | 1 - UA Fuzzing.sln | 30 +++++++++---------- 5 files changed, 15 insertions(+), 16 deletions(-) rename Fuzzing/BinaryDecoder/{Fuzztools => Fuzz.Tools}/BinaryDecoder.Fuzz.Tools.csproj (100%) rename Fuzzing/BinaryDecoder/{Fuzztools => Fuzz.Tools}/Playback.cs (100%) rename Fuzzing/BinaryDecoder/{Fuzztools => Fuzz.Tools}/Program.cs (100%) rename Fuzzing/BinaryDecoder/{Fuzztools => Fuzz.Tools}/Testcases.cs (99%) diff --git a/Fuzzing/BinaryDecoder/Fuzztools/BinaryDecoder.Fuzz.Tools.csproj b/Fuzzing/BinaryDecoder/Fuzz.Tools/BinaryDecoder.Fuzz.Tools.csproj similarity index 100% rename from Fuzzing/BinaryDecoder/Fuzztools/BinaryDecoder.Fuzz.Tools.csproj rename to Fuzzing/BinaryDecoder/Fuzz.Tools/BinaryDecoder.Fuzz.Tools.csproj diff --git a/Fuzzing/BinaryDecoder/Fuzztools/Playback.cs b/Fuzzing/BinaryDecoder/Fuzz.Tools/Playback.cs similarity index 100% rename from Fuzzing/BinaryDecoder/Fuzztools/Playback.cs rename to Fuzzing/BinaryDecoder/Fuzz.Tools/Playback.cs diff --git a/Fuzzing/BinaryDecoder/Fuzztools/Program.cs b/Fuzzing/BinaryDecoder/Fuzz.Tools/Program.cs similarity index 100% rename from Fuzzing/BinaryDecoder/Fuzztools/Program.cs rename to Fuzzing/BinaryDecoder/Fuzz.Tools/Program.cs diff --git a/Fuzzing/BinaryDecoder/Fuzztools/Testcases.cs b/Fuzzing/BinaryDecoder/Fuzz.Tools/Testcases.cs similarity index 99% rename from Fuzzing/BinaryDecoder/Fuzztools/Testcases.cs rename to Fuzzing/BinaryDecoder/Fuzz.Tools/Testcases.cs index d7daa5cd78..f41bcc8e35 100644 --- a/Fuzzing/BinaryDecoder/Fuzztools/Testcases.cs +++ b/Fuzzing/BinaryDecoder/Fuzz.Tools/Testcases.cs @@ -127,5 +127,4 @@ public static void FuzzTestcase(byte[] message) FuzzableCode.FuzzTarget(stream); } } - } diff --git a/UA Fuzzing.sln b/UA Fuzzing.sln index c2c7ae5c06..365fb7577a 100644 --- a/UA Fuzzing.sln +++ b/UA Fuzzing.sln @@ -67,8 +67,6 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "BinaryDecoder", "BinaryDeco EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "BinaryDecoder.Fuzz", "Fuzzing\BinaryDecoder\Fuzz\BinaryDecoder.Fuzz.csproj", "{92CC62D7-B7B6-4FBD-B9AC-181BD0D11EC0}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "BinaryDecoder.Fuzz.Tools", "Fuzzing\BinaryDecoder\Fuzztools\BinaryDecoder.Fuzz.Tools.csproj", "{352C998B-D884-4C25-BCE3-8AFBF371C380}" -EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "scripts", "scripts", "{612E66DA-731B-4E7C-969D-88079601ED30}" ProjectSection(SolutionItems) = preProject Fuzzing\scripts\fuzz-libfuzzer.ps1 = Fuzzing\scripts\fuzz-libfuzzer.ps1 @@ -81,6 +79,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "scripts", "scripts", "{612E EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Fuzzing", "Fuzzing", "{E9435CCE-1591-4AC0-87C4-341236E27C50}" EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "BinaryDecoder.Fuzz.Tools", "Fuzzing\BinaryDecoder\Fuzz.Tools\BinaryDecoder.Fuzz.Tools.csproj", "{A2F6528F-2DB0-4E37-ACBC-3A9EB9126D05}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -188,18 +188,18 @@ Global {92CC62D7-B7B6-4FBD-B9AC-181BD0D11EC0}.Release|x64.Build.0 = Release|Any CPU {92CC62D7-B7B6-4FBD-B9AC-181BD0D11EC0}.Release|x86.ActiveCfg = Release|Any CPU {92CC62D7-B7B6-4FBD-B9AC-181BD0D11EC0}.Release|x86.Build.0 = Release|Any CPU - {352C998B-D884-4C25-BCE3-8AFBF371C380}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {352C998B-D884-4C25-BCE3-8AFBF371C380}.Debug|Any CPU.Build.0 = Debug|Any CPU - {352C998B-D884-4C25-BCE3-8AFBF371C380}.Debug|x64.ActiveCfg = Debug|Any CPU - {352C998B-D884-4C25-BCE3-8AFBF371C380}.Debug|x64.Build.0 = Debug|Any CPU - {352C998B-D884-4C25-BCE3-8AFBF371C380}.Debug|x86.ActiveCfg = Debug|Any CPU - {352C998B-D884-4C25-BCE3-8AFBF371C380}.Debug|x86.Build.0 = Debug|Any CPU - {352C998B-D884-4C25-BCE3-8AFBF371C380}.Release|Any CPU.ActiveCfg = Release|Any CPU - {352C998B-D884-4C25-BCE3-8AFBF371C380}.Release|Any CPU.Build.0 = Release|Any CPU - {352C998B-D884-4C25-BCE3-8AFBF371C380}.Release|x64.ActiveCfg = Release|Any CPU - {352C998B-D884-4C25-BCE3-8AFBF371C380}.Release|x64.Build.0 = Release|Any CPU - {352C998B-D884-4C25-BCE3-8AFBF371C380}.Release|x86.ActiveCfg = Release|Any CPU - {352C998B-D884-4C25-BCE3-8AFBF371C380}.Release|x86.Build.0 = Release|Any CPU + {A2F6528F-2DB0-4E37-ACBC-3A9EB9126D05}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {A2F6528F-2DB0-4E37-ACBC-3A9EB9126D05}.Debug|Any CPU.Build.0 = Debug|Any CPU + {A2F6528F-2DB0-4E37-ACBC-3A9EB9126D05}.Debug|x64.ActiveCfg = Debug|Any CPU + {A2F6528F-2DB0-4E37-ACBC-3A9EB9126D05}.Debug|x64.Build.0 = Debug|Any CPU + {A2F6528F-2DB0-4E37-ACBC-3A9EB9126D05}.Debug|x86.ActiveCfg = Debug|Any CPU + {A2F6528F-2DB0-4E37-ACBC-3A9EB9126D05}.Debug|x86.Build.0 = Debug|Any CPU + {A2F6528F-2DB0-4E37-ACBC-3A9EB9126D05}.Release|Any CPU.ActiveCfg = Release|Any CPU + {A2F6528F-2DB0-4E37-ACBC-3A9EB9126D05}.Release|Any CPU.Build.0 = Release|Any CPU + {A2F6528F-2DB0-4E37-ACBC-3A9EB9126D05}.Release|x64.ActiveCfg = Release|Any CPU + {A2F6528F-2DB0-4E37-ACBC-3A9EB9126D05}.Release|x64.Build.0 = Release|Any CPU + {A2F6528F-2DB0-4E37-ACBC-3A9EB9126D05}.Release|x86.ActiveCfg = Release|Any CPU + {A2F6528F-2DB0-4E37-ACBC-3A9EB9126D05}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -216,8 +216,8 @@ Global {4B72937F-5A57-4CEA-B9FE-B4C45CF7B284} = {ACBF012E-08A7-4939-88CF-D6B610E35AC5} {128EE105-0A7A-4AB3-81E6-A77AE638B33F} = {E9435CCE-1591-4AC0-87C4-341236E27C50} {92CC62D7-B7B6-4FBD-B9AC-181BD0D11EC0} = {128EE105-0A7A-4AB3-81E6-A77AE638B33F} - {352C998B-D884-4C25-BCE3-8AFBF371C380} = {128EE105-0A7A-4AB3-81E6-A77AE638B33F} {612E66DA-731B-4E7C-969D-88079601ED30} = {E9435CCE-1591-4AC0-87C4-341236E27C50} + {A2F6528F-2DB0-4E37-ACBC-3A9EB9126D05} = {128EE105-0A7A-4AB3-81E6-A77AE638B33F} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {33FA5BCB-C827-4D44-AECF-F51342DFE64A} From 1821cf051317b4c63277f6ebefb29f494358fa7f Mon Sep 17 00:00:00 2001 From: Martin Regen Date: Sat, 20 Apr 2024 19:44:56 +0200 Subject: [PATCH 11/83] doc --- Fuzzing/Fuzzing.md | 41 ++++++++++++++++++++++++++++++++--------- 1 file changed, 32 insertions(+), 9 deletions(-) diff --git a/Fuzzing/Fuzzing.md b/Fuzzing/Fuzzing.md index 36bd626fec..578558943c 100644 --- a/Fuzzing/Fuzzing.md +++ b/Fuzzing/Fuzzing.md @@ -1,13 +1,29 @@ # Fuzz testing for UA.NET Standard -This project provides integration of Sharpfuzz with the encoders used by the UA .NET Standard library. +This project provides integration of Sharpfuzz with the UA.NET Standard library encoders with support for both afl-fuzz and libfuzzer. -## Installation +Fuzzers for the following decoders are located in the `Fuzzing` directory: +- BinaryDecoder +- JsonDecoder (planned) +- XmlDecoder (planned) +- CRL and Certificate decoder functions using the .NET ASN.1 decoder (planned) -The fuzzing project executes on a linux subsystem. The following steps are required to set up the environment: +Most of the supporting code is shared between all projects, only the project names and the fuzzable support functions differ. -- Install a Windows Subsystem for Linux (WSL) by following the instructions at https://docs.microsoft.com/en-us/windows/wsl/install or by installing e.g. the Ubuntu app from the Microsoft store. -- The full instructions for setting up sharpfuzz can be found at https://github.com/Metalnem/sharpfuzz/blob/master/README.md. +A Tools application for each fuzzer supports recreation of the `Testcases` and to replay the test cases that caused the fuzzer to crash or to hang. The application is located in the `*.Fuzz.Tools` folders. + +## Installation for afl-fuzz and libfuzzer on Linux + +Both fuzzers are supported on Linux. afl-fuzz can be compiled on any Linux system, while for libfuzzer prebuilt binaries are available for Debian and Ubuntu. The instructions were tested on a WSL subsystem on Windows with a Ubuntu installation. + +### Extra step to run the fuzzers on Windows on a linux subsystem (WSL) + +- Install the Windows Subsystem for Linux (WSL) by following the instructions at https://docs.microsoft.com/en-us/windows/wsl/install or by installing e.g. the Ubuntu app from the Microsoft store. + +### Installation of required tools + +The full instructions for setting up sharpfuzz can be found at https://github.com/Metalnem/sharpfuzz/blob/master/README.md. +The following steps are required to set up the environment: - Open a terminal and run the following commands to install the required packages to compile afl-fuzz: @@ -55,13 +71,20 @@ afl-fuzz --help sharpfuzz ``` -## Usage +## Installation for libfuzzer on Windows -To run the fuzzing project, execute the following commands, e.g. for the BinaryDecoder fuzzer: +Install the latest dotnet SDK and runtime from https://dotnet.microsoft.com/download/dotnet/ -```bash -#/bin/sh +```commandline +# Install SharpFuzz.CommandLine global .NET tool +dotnet tool install --global SharpFuzz.CommandLine +``` +## Usage of afl-fuzz on Linux + +To run the afl-fuzz fuzzing project, execute the following commands, e.g. for the BinaryDecoder fuzzer: + +```bash cd BinaryDecoder ./fuzz.sh ``` From 0c44b6e949d449fa68b3c6088133b1f26ea866a5 Mon Sep 17 00:00:00 2001 From: Martin Regen Date: Sat, 20 Apr 2024 21:35:34 +0200 Subject: [PATCH 12/83] update Testcases --- .../Fuzz.Tools/BinaryDecoder.Testcases.cs | 18 +++ Fuzzing/BinaryDecoder/Fuzz.Tools/Testcases.cs | 122 +++++++++--------- .../Fuzz/Testcases/readrequest.bin | Bin 139 -> 139 bytes .../Fuzz/Testcases/readresponse.bin | Bin 255 -> 255 bytes UA Fuzzing.sln | 6 +- 5 files changed, 79 insertions(+), 67 deletions(-) create mode 100644 Fuzzing/BinaryDecoder/Fuzz.Tools/BinaryDecoder.Testcases.cs diff --git a/Fuzzing/BinaryDecoder/Fuzz.Tools/BinaryDecoder.Testcases.cs b/Fuzzing/BinaryDecoder/Fuzz.Tools/BinaryDecoder.Testcases.cs new file mode 100644 index 0000000000..e9e811c364 --- /dev/null +++ b/Fuzzing/BinaryDecoder/Fuzz.Tools/BinaryDecoder.Testcases.cs @@ -0,0 +1,18 @@ + +using System.IO; +using Opc.Ua; + +public static partial class Testcases +{ + public static void Run(string directoryPath) + { + foreach (var messageEncoder in Testcases.MessageEncoders) + { + var encoder = new BinaryEncoder(Testcases.MessageContext); + messageEncoder(encoder); + var message = encoder.CloseAndReturnBuffer(); + FuzzTestcase(message); + File.WriteAllBytes(Path.Combine(directoryPath, $"{messageEncoder.Method.Name}.bin".ToLowerInvariant()), message); + } + } +} diff --git a/Fuzzing/BinaryDecoder/Fuzz.Tools/Testcases.cs b/Fuzzing/BinaryDecoder/Fuzz.Tools/Testcases.cs index f41bcc8e35..1074196a6b 100644 --- a/Fuzzing/BinaryDecoder/Fuzz.Tools/Testcases.cs +++ b/Fuzzing/BinaryDecoder/Fuzz.Tools/Testcases.cs @@ -3,52 +3,58 @@ using System.IO; using Opc.Ua; -public static class Testcases +public partial class Testcases { - private static ServiceMessageContext s_messageContext = new ServiceMessageContext(); + public delegate void MessageEncoder(IEncoder encoder); - private static byte[] CreateReadRequest() + public static ServiceMessageContext MessageContext = new ServiceMessageContext(); + + public static MessageEncoder[] MessageEncoders = new MessageEncoder[] { + ReadRequest, + ReadResponse, + }; + + public static void ReadRequest(IEncoder encoder) { - using (var encoder = new BinaryEncoder(s_messageContext)) - { - var nodeId = new NodeId(1000); - var readRequest = new ReadRequest { - NodesToRead = new ReadValueIdCollection { - new ReadValueId { - NodeId = nodeId, - AttributeId = Attributes.Description, - }, - new ReadValueId { - NodeId = nodeId, - AttributeId = Attributes.Value, - }, - new ReadValueId { - NodeId = nodeId, - AttributeId = Attributes.DisplayName, - }, - new ReadValueId { - NodeId = nodeId, - AttributeId = Attributes.AccessLevel, - }, - new ReadValueId { - NodeId = nodeId, - AttributeId = Attributes.RolePermissions, - }, + var nodeId = new NodeId(1000); + var readRequest = new ReadRequest { + RequestHeader = new RequestHeader { + Timestamp = DateTime.UtcNow, + RequestHandle = 42, + AdditionalHeader = new ExtensionObject(), + }, + NodesToRead = new ReadValueIdCollection { + new ReadValueId { + NodeId = nodeId, + AttributeId = Attributes.Description, }, - }; - encoder.EncodeMessage(readRequest); - return encoder.CloseAndReturnBuffer(); - } + new ReadValueId { + NodeId = nodeId, + AttributeId = Attributes.Value, + }, + new ReadValueId { + NodeId = nodeId, + AttributeId = Attributes.DisplayName, + }, + new ReadValueId { + NodeId = nodeId, + AttributeId = Attributes.AccessLevel, + }, + new ReadValueId { + NodeId = nodeId, + AttributeId = Attributes.RolePermissions, + }, + }, + }; + encoder.EncodeMessage(readRequest); } - private static byte[] CreateReadResponse() + public static void ReadResponse(IEncoder encoder) { var now = DateTime.UtcNow; - using (var encoder = new BinaryEncoder(s_messageContext)) - { - var nodeId = new NodeId(1000); - var readRequest = new ReadResponse { - Results = new DataValueCollection { + var nodeId = new NodeId(1000); + var readRequest = new ReadResponse { + Results = new DataValueCollection { new DataValue { Value = new Variant("Hello World"), ServerTimestamp = now, @@ -73,7 +79,7 @@ private static byte[] CreateReadResponse() Value = new Variant((byte)42), }, }, - DiagnosticInfos = new DiagnosticInfoCollection { + DiagnosticInfos = new DiagnosticInfoCollection { new DiagnosticInfo { AdditionalInfo = "Hello World", InnerStatusCode = StatusCodes.BadCertificateHostNameInvalid, @@ -83,41 +89,29 @@ private static byte[] CreateReadResponse() }, }, }, - ResponseHeader = new ResponseHeader { - Timestamp = DateTime.UtcNow, - RequestHandle = 42, - ServiceResult = StatusCodes.Good, - ServiceDiagnostics = new DiagnosticInfo { - AdditionalInfo = "NodeId not found", - InnerStatusCode = StatusCodes.BadNodeIdExists, + ResponseHeader = new ResponseHeader { + Timestamp = DateTime.UtcNow, + RequestHandle = 42, + ServiceResult = StatusCodes.Good, + ServiceDiagnostics = new DiagnosticInfo { + AdditionalInfo = "NodeId not found", + InnerStatusCode = StatusCodes.BadNodeIdExists, + InnerDiagnosticInfo = new DiagnosticInfo { + AdditionalInfo = "Hello World", + InnerStatusCode = StatusCodes.BadNodeIdUnknown, InnerDiagnosticInfo = new DiagnosticInfo { AdditionalInfo = "Hello World", InnerStatusCode = StatusCodes.BadNodeIdUnknown, InnerDiagnosticInfo = new DiagnosticInfo { AdditionalInfo = "Hello World", InnerStatusCode = StatusCodes.BadNodeIdUnknown, - InnerDiagnosticInfo = new DiagnosticInfo { - AdditionalInfo = "Hello World", - InnerStatusCode = StatusCodes.BadNodeIdUnknown, - }, }, }, }, }, - }; - encoder.EncodeMessage(readRequest); - return encoder.CloseAndReturnBuffer(); - } - } - - public static void Run(string directoryPath) - { - var readRequest = CreateReadRequest(); - FuzzTestcase(readRequest); - File.WriteAllBytes(Path.Combine(directoryPath, "readrequest.bin"), readRequest); - var readResponse = CreateReadResponse(); - FuzzTestcase(readResponse); - File.WriteAllBytes(Path.Combine(directoryPath, "readresponse.bin"), readResponse); + }, + }; + encoder.EncodeMessage(readRequest); } public static void FuzzTestcase(byte[] message) diff --git a/Fuzzing/BinaryDecoder/Fuzz/Testcases/readrequest.bin b/Fuzzing/BinaryDecoder/Fuzz/Testcases/readrequest.bin index 49d6ddbeff2b28786893e3fc4b3c6bb94c11cebf..15f160ef318586d5841ab8ad92bc5602de6390b3 100644 GIT binary patch delta 23 ecmeBX>}KR=WGH81U^upfUoUd=0{51eVO$Jr~ delta 23 VcmeBX>}KR=WGH81K!Az-H2@ Date: Sun, 21 Apr 2024 07:38:39 +0200 Subject: [PATCH 13/83] refactor --- .../Fuzz.Tools/BinaryDecoder.Fuzz.Tools.csproj | 3 +++ .../Fuzz/BinaryDecoder.Fuzz.csproj | 4 ++++ Fuzzing/BinaryDecoder/Fuzz/FuzzTarget.cs | 2 +- .../Fuzz.Tools/Playback.cs | 16 +++++++++++++--- .../Fuzz.Tools/Program.cs | 3 +-- .../Fuzz.Tools/Testcases.cs | 6 ++++-- .../{BinaryDecoder => common}/Fuzz/Program.cs | 4 ++-- Stack/Opc.Ua.Core/Types/Encoders/IEncoder.cs | 5 +++++ Stack/Opc.Ua.Core/Types/Encoders/XmlEncoder.cs | 17 +++++++++++++++++ 9 files changed, 50 insertions(+), 10 deletions(-) rename Fuzzing/{BinaryDecoder => common}/Fuzz.Tools/Playback.cs (59%) rename Fuzzing/{BinaryDecoder => common}/Fuzz.Tools/Program.cs (93%) rename Fuzzing/{BinaryDecoder => common}/Fuzz.Tools/Testcases.cs (97%) rename Fuzzing/{BinaryDecoder => common}/Fuzz/Program.cs (76%) diff --git a/Fuzzing/BinaryDecoder/Fuzz.Tools/BinaryDecoder.Fuzz.Tools.csproj b/Fuzzing/BinaryDecoder/Fuzz.Tools/BinaryDecoder.Fuzz.Tools.csproj index da65f9fdf3..4f6a8ab128 100644 --- a/Fuzzing/BinaryDecoder/Fuzz.Tools/BinaryDecoder.Fuzz.Tools.csproj +++ b/Fuzzing/BinaryDecoder/Fuzz.Tools/BinaryDecoder.Fuzz.Tools.csproj @@ -9,6 +9,9 @@ + + + diff --git a/Fuzzing/BinaryDecoder/Fuzz/BinaryDecoder.Fuzz.csproj b/Fuzzing/BinaryDecoder/Fuzz/BinaryDecoder.Fuzz.csproj index a3a6a21f0c..e616f01300 100644 --- a/Fuzzing/BinaryDecoder/Fuzz/BinaryDecoder.Fuzz.csproj +++ b/Fuzzing/BinaryDecoder/Fuzz/BinaryDecoder.Fuzz.csproj @@ -10,6 +10,10 @@ $(DefineConstants);LIBFUZZER + + + + diff --git a/Fuzzing/BinaryDecoder/Fuzz/FuzzTarget.cs b/Fuzzing/BinaryDecoder/Fuzz/FuzzTarget.cs index 604dbd8635..d3c5671253 100644 --- a/Fuzzing/BinaryDecoder/Fuzz/FuzzTarget.cs +++ b/Fuzzing/BinaryDecoder/Fuzz/FuzzTarget.cs @@ -56,7 +56,7 @@ private static void FuzzTargetCore(MemoryStream stream) try { - using (var decoder = new Opc.Ua.BinaryDecoder(stream, messageContext)) + using (var decoder = new BinaryDecoder(stream, messageContext)) { _ = decoder.DecodeMessage(null); } diff --git a/Fuzzing/BinaryDecoder/Fuzz.Tools/Playback.cs b/Fuzzing/common/Fuzz.Tools/Playback.cs similarity index 59% rename from Fuzzing/BinaryDecoder/Fuzz.Tools/Playback.cs rename to Fuzzing/common/Fuzz.Tools/Playback.cs index 9b0cdf7c1a..8eb88e08b8 100644 --- a/Fuzzing/BinaryDecoder/Fuzz.Tools/Playback.cs +++ b/Fuzzing/common/Fuzz.Tools/Playback.cs @@ -1,6 +1,7 @@ using System.IO; using System; +using System.Text; public static class Playback { @@ -8,12 +9,21 @@ public static void Run(string directoryPath, bool stackTrace) { foreach (var crashFile in Directory.EnumerateFiles(directoryPath)) { - using (var stream = new MemoryStream(File.ReadAllBytes(crashFile))) +#if TEXTFUZZER + var crashData = Encoding.UTF8.GetString(File.ReadAllBytes(crashFile)); { try { - FuzzableCode.FuzzTarget(stream); + FuzzableCode.FuzzTarget(crashData); } +#else + using (var crashStream = new FileStream(crashFile, FileMode.Open, FileAccess.Read)) + { + try + { + FuzzableCode.FuzzTarget(crashStream); + } +#endif catch (Exception ex) { Console.WriteLine("{0}:{1}", ex.GetType().Name, ex.Message); @@ -23,6 +33,6 @@ public static void Run(string directoryPath, bool stackTrace) } } } - }; + } } } diff --git a/Fuzzing/BinaryDecoder/Fuzz.Tools/Program.cs b/Fuzzing/common/Fuzz.Tools/Program.cs similarity index 93% rename from Fuzzing/BinaryDecoder/Fuzz.Tools/Program.cs rename to Fuzzing/common/Fuzz.Tools/Program.cs index da2df734fe..a7f3b6b0c6 100644 --- a/Fuzzing/BinaryDecoder/Fuzz.Tools/Program.cs +++ b/Fuzzing/common/Fuzz.Tools/Program.cs @@ -6,7 +6,6 @@ public static class Program { - public static readonly string ApplicationName = "BinaryDecoder.Fuzztools"; public static readonly string DefaultTestcasesFolder = "../../../../Fuzz/Testcases"; public static readonly string DefaultFindingsCrashFolder = "../../../../findings/crashes"; public static readonly string DefaultFindingsHangFolder = "../../../../findings/crashes"; @@ -15,7 +14,7 @@ public static class Program public static void Main(string[] args) { - var applicationName = "BinaryDecoder.Fuzztools"; + var applicationName = typeof(Program).Assembly.GetName().Name; TextWriter output = Console.Out; output.WriteLine($"OPC UA {applicationName}"); diff --git a/Fuzzing/BinaryDecoder/Fuzz.Tools/Testcases.cs b/Fuzzing/common/Fuzz.Tools/Testcases.cs similarity index 97% rename from Fuzzing/BinaryDecoder/Fuzz.Tools/Testcases.cs rename to Fuzzing/common/Fuzz.Tools/Testcases.cs index 1074196a6b..0ba7946b34 100644 --- a/Fuzzing/BinaryDecoder/Fuzz.Tools/Testcases.cs +++ b/Fuzzing/common/Fuzz.Tools/Testcases.cs @@ -114,11 +114,13 @@ public static void ReadResponse(IEncoder encoder) encoder.EncodeMessage(readRequest); } - public static void FuzzTestcase(byte[] message) +#if !TEXTFUZZER + public static void FuzzTestcase(byte[] input) { - using (var stream = new MemoryStream(message)) + using (var stream = new MemoryStream(input)) { FuzzableCode.FuzzTarget(stream); } } +#endif } diff --git a/Fuzzing/BinaryDecoder/Fuzz/Program.cs b/Fuzzing/common/Fuzz/Program.cs similarity index 76% rename from Fuzzing/BinaryDecoder/Fuzz/Program.cs rename to Fuzzing/common/Fuzz/Program.cs index bb065b7993..59b359690c 100644 --- a/Fuzzing/BinaryDecoder/Fuzz/Program.cs +++ b/Fuzzing/common/Fuzz/Program.cs @@ -11,8 +11,8 @@ public static void Main(string[] args) FuzzableCode.FuzzTargetLibfuzzer(input); }); #else - Fuzzer.Run(stream => { - FuzzableCode.FuzzTarget(stream); + Fuzzer.Run(input => { + FuzzableCode.FuzzTarget(input); }); #endif } diff --git a/Stack/Opc.Ua.Core/Types/Encoders/IEncoder.cs b/Stack/Opc.Ua.Core/Types/Encoders/IEncoder.cs index 8e338d87d7..2fd8414e43 100644 --- a/Stack/Opc.Ua.Core/Types/Encoders/IEncoder.cs +++ b/Stack/Opc.Ua.Core/Types/Encoders/IEncoder.cs @@ -63,6 +63,11 @@ public interface IEncoder : IDisposable /// void PopNamespace(); + /// + /// Encodes a message with its header. + /// + void EncodeMessage(IEncodeable message); + /// /// Writes a boolean to the stream. /// diff --git a/Stack/Opc.Ua.Core/Types/Encoders/XmlEncoder.cs b/Stack/Opc.Ua.Core/Types/Encoders/XmlEncoder.cs index e749c72464..f8041f7a88 100644 --- a/Stack/Opc.Ua.Core/Types/Encoders/XmlEncoder.cs +++ b/Stack/Opc.Ua.Core/Types/Encoders/XmlEncoder.cs @@ -296,6 +296,23 @@ public void PopNamespace() m_namespaces.Pop(); } + /// + /// Encodes a message with its header. + /// + public void EncodeMessage(IEncodeable message) + { + if (message == null) throw new ArgumentNullException(nameof(message)); + + // convert the namespace uri to an index. + NodeId typeId = ExpandedNodeId.ToNodeId(message.TypeId, m_context.NamespaceUris); + + // write the type id. + WriteNodeId("TypeId", typeId); + + // write the message. + WriteEncodeable("Body", message, message.GetType()); + } + /// /// Writes a boolean to the stream. /// From f6772baf21d79241a30f659699f50da052d07fa9 Mon Sep 17 00:00:00 2001 From: Martin Regen Date: Sun, 21 Apr 2024 12:10:57 +0200 Subject: [PATCH 14/83] more fixes --- .../BinaryDecoder.Fuzz.Tools.csproj | 8 ++ Fuzzing/common/Fuzz.Tools/Logging.cs | 114 ++++++++++++++++++ Fuzzing/common/Fuzz.Tools/Playback.cs | 4 +- Fuzzing/common/Fuzz.Tools/Program.cs | 12 +- Stack/Opc.Ua.Core/Types/BuiltIn/Variant.cs | 5 + .../Types/Encoders/BinaryDecoder.cs | 55 +++++---- UA Fuzzing.sln | 17 +++ 7 files changed, 189 insertions(+), 26 deletions(-) create mode 100644 Fuzzing/common/Fuzz.Tools/Logging.cs diff --git a/Fuzzing/BinaryDecoder/Fuzz.Tools/BinaryDecoder.Fuzz.Tools.csproj b/Fuzzing/BinaryDecoder/Fuzz.Tools/BinaryDecoder.Fuzz.Tools.csproj index 4f6a8ab128..a35d6d151f 100644 --- a/Fuzzing/BinaryDecoder/Fuzz.Tools/BinaryDecoder.Fuzz.Tools.csproj +++ b/Fuzzing/BinaryDecoder/Fuzz.Tools/BinaryDecoder.Fuzz.Tools.csproj @@ -12,11 +12,19 @@ + + + + + + + + diff --git a/Fuzzing/common/Fuzz.Tools/Logging.cs b/Fuzzing/common/Fuzz.Tools/Logging.cs new file mode 100644 index 0000000000..18a30479ac --- /dev/null +++ b/Fuzzing/common/Fuzz.Tools/Logging.cs @@ -0,0 +1,114 @@ +/* ======================================================================== + * Copyright (c) 2005-2024 The OPC Foundation, Inc. All rights reserved. + * + * OPC Foundation MIT License 1.00 + * + * Permission is hereby granted, free of charge, to any person + * obtaining a copy of this software and associated documentation + * files (the "Software"), to deal in the Software without + * restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following + * conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES + * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT + * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, + * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR + * OTHER DEALINGS IN THE SOFTWARE. + * + * The complete license agreement can be found here: + * http://opcfoundation.org/License/MIT/1.00/ + * ======================================================================*/ + +using System; +using System.Globalization; +using System.Threading.Tasks; +using Microsoft.Extensions.Logging; +using Opc.Ua; +using Serilog; +using Serilog.Events; +using Serilog.Templates; +using static Opc.Ua.Utils; + +public static class Logging +{ + /// + /// Configure the serilog logging provider. + /// + public static void Configure( + string context, + string outputFilePath, + bool logConsole, + LogLevel consoleLogLevel) + { + AppDomain.CurrentDomain.UnhandledException += CurrentDomain_UnhandledException; + TaskScheduler.UnobservedTaskException += Unobserved_TaskException; + + var loggerConfiguration = new LoggerConfiguration() + .Enrich.FromLogContext(); + + if (logConsole) + { + loggerConfiguration.WriteTo.Console( + restrictedToMinimumLevel: (LogEventLevel)consoleLogLevel, + formatProvider: CultureInfo.InvariantCulture + ); + } +#if DEBUG + else + { + loggerConfiguration + .WriteTo.Debug( + restrictedToMinimumLevel: (LogEventLevel)consoleLogLevel, + formatProvider: CultureInfo.InvariantCulture + ); + } +#endif + LogLevel fileLevel = LogLevel.Information; + + // add file logging if configured + if (!string.IsNullOrWhiteSpace(outputFilePath)) + { + loggerConfiguration.WriteTo.File( + new ExpressionTemplate("{UtcDateTime(@t):yyyy-MM-dd HH:mm:ss.fff} [{@l:u3}] {@m}\n{@x}"), + ReplaceSpecialFolderNames(outputFilePath), + restrictedToMinimumLevel: (LogEventLevel)fileLevel, + rollOnFileSizeLimit: true); + } + + // adjust minimum level + if (fileLevel < LogLevel.Information || consoleLogLevel < LogLevel.Information) + { + loggerConfiguration.MinimumLevel.Verbose(); + } + + // create the serilog logger + var serilogger = loggerConfiguration + .CreateLogger(); + + // create the ILogger for Opc.Ua.Core + var logger = LoggerFactory.Create(builder => builder.SetMinimumLevel(LogLevel.Trace)) + .AddSerilog(serilogger) + .CreateLogger(context); + + // set logger interface, disables TraceEvent + SetLogger(logger); + } + + private static void CurrentDomain_UnhandledException(object sender, UnhandledExceptionEventArgs args) + { + Utils.LogCritical("Unhandled Exception: {0} IsTerminating: {1}", args.ExceptionObject, args.IsTerminating); + } + + private static void Unobserved_TaskException(object sender, UnobservedTaskExceptionEventArgs args) + { + Utils.LogCritical("Unobserved Exception: {0} Observed: {1}", args.Exception, args.Observed); + } +} diff --git a/Fuzzing/common/Fuzz.Tools/Playback.cs b/Fuzzing/common/Fuzz.Tools/Playback.cs index 8eb88e08b8..52a9dd7585 100644 --- a/Fuzzing/common/Fuzz.Tools/Playback.cs +++ b/Fuzzing/common/Fuzz.Tools/Playback.cs @@ -7,7 +7,9 @@ public static class Playback { public static void Run(string directoryPath, bool stackTrace) { - foreach (var crashFile in Directory.EnumerateFiles(directoryPath)) + var path = Path.GetDirectoryName(directoryPath); + var searchPattern = Path.GetFileName(directoryPath); + foreach (var crashFile in Directory.EnumerateFiles(path, searchPattern)) { #if TEXTFUZZER var crashData = Encoding.UTF8.GetString(File.ReadAllBytes(crashFile)); diff --git a/Fuzzing/common/Fuzz.Tools/Program.cs b/Fuzzing/common/Fuzz.Tools/Program.cs index a7f3b6b0c6..e8bbdb3b01 100644 --- a/Fuzzing/common/Fuzz.Tools/Program.cs +++ b/Fuzzing/common/Fuzz.Tools/Program.cs @@ -3,12 +3,13 @@ using System; using Mono.Options; using System.Collections.Generic; +using Microsoft.Extensions.Logging; public static class Program { public static readonly string DefaultTestcasesFolder = "../../../../Fuzz/Testcases"; - public static readonly string DefaultFindingsCrashFolder = "../../../../findings/crashes"; - public static readonly string DefaultFindingsHangFolder = "../../../../findings/crashes"; + public static readonly string DefaultFindingsCrashFolder = "../../../../findings/crashes/"; + public static readonly string DefaultFindingsHangsFolder = "../../../../findings/hangs/"; public static readonly string DefaultLibFuzzerCrashes = "../../../crash-*"; public static readonly string DefaultLibFuzzerHangs = "../../../timeout-*"; @@ -28,11 +29,13 @@ public static void Main(string[] args) OptionSet options = new OptionSet { usage, { "h|help", "show this message and exit", h => showHelp = h != null }, - { "p|playback", "playback crashes found by afl-fuzz or libfuzzer", p => playback = p != null }, + { "p|playback", "playback crashes found by afl-fuzz and libfuzzer", p => playback = p != null }, { "t|testcases", "create test cases for fuzzing", t => testcases = t != null }, { "s|stacktrace", "show stacktrace with playback", s => stacktrace = s != null }, }; + Logging.Configure(applicationName, string.Empty, true, LogLevel.Trace); + IList extraArgs = null; try { @@ -51,6 +54,9 @@ public static void Main(string[] args) else if (playback) { Playback.Run(DefaultFindingsCrashFolder, stacktrace); + Playback.Run(DefaultFindingsHangsFolder, stacktrace); + Playback.Run(DefaultLibFuzzerCrashes, stacktrace); + Playback.Run(DefaultLibFuzzerHangs, stacktrace); } else { diff --git a/Stack/Opc.Ua.Core/Types/BuiltIn/Variant.cs b/Stack/Opc.Ua.Core/Types/BuiltIn/Variant.cs index 2576e64dc6..c1c005f699 100644 --- a/Stack/Opc.Ua.Core/Types/BuiltIn/Variant.cs +++ b/Stack/Opc.Ua.Core/Types/BuiltIn/Variant.cs @@ -63,6 +63,11 @@ public Variant(object value, TypeInfo typeInfo) Set(value, typeInfo); #if DEBUG + // no sanity check possible for null values + if (m_value == null) + { + return; + } TypeInfo sanityCheck = TypeInfo.Construct(m_value); diff --git a/Stack/Opc.Ua.Core/Types/Encoders/BinaryDecoder.cs b/Stack/Opc.Ua.Core/Types/Encoders/BinaryDecoder.cs index 9b0ea8ff1c..ca60af9f50 100644 --- a/Stack/Opc.Ua.Core/Types/Encoders/BinaryDecoder.cs +++ b/Stack/Opc.Ua.Core/Types/Encoders/BinaryDecoder.cs @@ -2014,7 +2014,7 @@ private ExtensionObject ReadExtensionObject() if (!NodeId.IsNull(typeId) && NodeId.IsNull(extension.TypeId)) { Utils.LogWarning( - "Cannot de-serialized extension objects if the NamespaceUri is not in the NamespaceTable: Type = {0}", + "Cannot deserialize extension objects if the NamespaceUri is not in the NamespaceTable: Type = {0}", typeId); } @@ -2052,7 +2052,7 @@ private ExtensionObject ReadExtensionObject() } catch (Exception e) { - Utils.LogError("Could not decode known type {0}. Error={1}, Value={2}", systemType.FullName, e.Message, element.OuterXml); + Utils.LogError("Could not decode known type {0} encoded as Xml. Error={1}, Value={2}", systemType.FullName, e.Message, element.OuterXml); } } } @@ -2060,10 +2060,15 @@ private ExtensionObject ReadExtensionObject() return extension; } + // get the length. + int length = ReadInt32(null); + + // save the current position. + int start = Position; + // create instance of type. IEncodeable encodeable = null; - - if (systemType != null) + if (systemType != null && length >= 0) { encodeable = Activator.CreateInstance(systemType) as IEncodeable; @@ -2074,15 +2079,12 @@ private ExtensionObject ReadExtensionObject() } } - // get the length. - int length = ReadInt32(null); - - // save the current position. - int start = Position; - // process known type. if (encodeable != null) { + bool resetStream = true; + string errorMessage = string.Empty; + Exception exception = null; uint nestingLevel = m_nestingLevel; CheckAndIncrementNestingLevel(); @@ -2096,31 +2098,40 @@ private ExtensionObject ReadExtensionObject() int used = Position - start; if (length != used) { - throw ServiceResultException.Create(StatusCodes.BadDecodingError, - "The encodeable.Decoder operation did not match the length of the extension object. {0} != {1}", used, length); + errorMessage = "Length mismatch"; + } + else + { + // success! + resetStream = false; } } catch (EndOfStreamException eofStream) { - // type was known but decoding failed, reset stream! - m_reader.BaseStream.Position = start; - encodeable = null; - Utils.LogWarning(eofStream, "End of stream, failed to decode encodeable type '{0}', NodeId='{1}'. BinaryDecoder recovered.", - systemType.Name, extension.TypeId); + errorMessage = "End of stream"; + exception = eofStream; } catch (ServiceResultException sre) when ((sre.StatusCode == StatusCodes.BadEncodingLimitsExceeded) || (sre.StatusCode == StatusCodes.BadDecodingError)) { - // type was known but decoding failed, reset stream! - m_reader.BaseStream.Position = start; - encodeable = null; - Utils.LogWarning(sre, "{0}, failed to decode encodeable type '{1}', NodeId='{2}'. BinaryDecoder recovered.", - sre.Message, systemType.Name, extension.TypeId); + errorMessage = sre.Message; + exception = sre; } finally { m_nestingLevel = nestingLevel; } + + if (resetStream) + { + // type was known but decoding failed, reset stream! + m_reader.BaseStream.Position = start; + encodeable = null; + + // log the error. + Utils.LogWarning(exception, "{0}, failed to decode encodeable type '{1}', NodeId='{2}'. BinaryDecoder recovered.", + errorMessage, systemType.Name, extension.TypeId); + } } // process unknown type. diff --git a/UA Fuzzing.sln b/UA Fuzzing.sln index 5049abec05..a4356035cc 100644 --- a/UA Fuzzing.sln +++ b/UA Fuzzing.sln @@ -81,6 +81,20 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Fuzzing", "Fuzzing", "{E943 EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "BinaryDecoder.Fuzz.Tools", "Fuzzing\BinaryDecoder\Fuzz.Tools\BinaryDecoder.Fuzz.Tools.csproj", "{A2F6528F-2DB0-4E37-ACBC-3A9EB9126D05}" EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "common", "common", "{6F57459E-6B63-4FB5-8311-7FC1DDC84650}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Fuzz", "Fuzz", "{C9E285F1-D853-4CD9-9ECF-493E09395548}" + ProjectSection(SolutionItems) = preProject + Fuzzing\common\Fuzz\Program.cs = Fuzzing\common\Fuzz\Program.cs + EndProjectSection +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Fuzz.Tools", "Fuzz.Tools", "{6A642C2C-0B5D-4C2D-BAD0-A6F760175465}" + ProjectSection(SolutionItems) = preProject + Fuzzing\common\Fuzz.Tools\Playback.cs = Fuzzing\common\Fuzz.Tools\Playback.cs + Fuzzing\common\Fuzz.Tools\Program.cs = Fuzzing\common\Fuzz.Tools\Program.cs + Fuzzing\common\Fuzz.Tools\Testcases.cs = Fuzzing\common\Fuzz.Tools\Testcases.cs + EndProjectSection +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -218,6 +232,9 @@ Global {92CC62D7-B7B6-4FBD-B9AC-181BD0D11EC0} = {128EE105-0A7A-4AB3-81E6-A77AE638B33F} {612E66DA-731B-4E7C-969D-88079601ED30} = {E9435CCE-1591-4AC0-87C4-341236E27C50} {A2F6528F-2DB0-4E37-ACBC-3A9EB9126D05} = {128EE105-0A7A-4AB3-81E6-A77AE638B33F} + {6F57459E-6B63-4FB5-8311-7FC1DDC84650} = {E9435CCE-1591-4AC0-87C4-341236E27C50} + {C9E285F1-D853-4CD9-9ECF-493E09395548} = {6F57459E-6B63-4FB5-8311-7FC1DDC84650} + {6A642C2C-0B5D-4C2D-BAD0-A6F760175465} = {6F57459E-6B63-4FB5-8311-7FC1DDC84650} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {33FA5BCB-C827-4D44-AECF-F51342DFE64A} From e492934125ba74cbc307942723814acf9b7e7f41 Mon Sep 17 00:00:00 2001 From: Martin Regen Date: Tue, 23 Apr 2024 07:51:32 +0200 Subject: [PATCH 15/83] add stream and Position tests --- .../Types/Encoders/BinaryDecoder.cs | 82 ++++++++++++++++--- 1 file changed, 69 insertions(+), 13 deletions(-) diff --git a/Stack/Opc.Ua.Core/Types/Encoders/BinaryDecoder.cs b/Stack/Opc.Ua.Core/Types/Encoders/BinaryDecoder.cs index ca60af9f50..f239ad509a 100644 --- a/Stack/Opc.Ua.Core/Types/Encoders/BinaryDecoder.cs +++ b/Stack/Opc.Ua.Core/Types/Encoders/BinaryDecoder.cs @@ -24,6 +24,10 @@ namespace Opc.Ua /// public class BinaryDecoder : IDecoder { + // The number of times the encodeable decoder can recover from + // an error before throwing an exception. + const int kMaxDecoderRecoveries = 32; + #region Constructor /// /// Creates a decoder that reads from a memory buffer. @@ -48,8 +52,7 @@ public BinaryDecoder(byte[] buffer, int start, int count, IServiceMessageContext { var stream = new MemoryStream(buffer, start, count, false); m_reader = new BinaryReader(stream); - m_context = context; - m_nestingLevel = 0; + Initialize(context); } /// @@ -57,11 +60,20 @@ public BinaryDecoder(byte[] buffer, int start, int count, IServiceMessageContext /// public BinaryDecoder(Stream stream, IServiceMessageContext context, bool leaveOpen = false) { - if (stream == null) throw new ArgumentNullException(nameof(stream)); - + ValidateStreamRequirements(stream); m_reader = new BinaryReader(stream, Encoding.UTF8, leaveOpen); + Initialize(context); + + } + + /// + /// Initializes the object. + /// + private void Initialize(IServiceMessageContext context) + { m_context = context; m_nestingLevel = 0; + m_encodeablesRecovered = 0; } #endregion @@ -122,12 +134,28 @@ public void Close() /// /// Returns the current position in the stream. /// - public int Position => (int)m_reader.BaseStream.Position; + public int Position + { + get + { + var stream = BaseStream; + if (stream?.CanSeek != true) + { + throw new ServiceResultException(StatusCodes.BadDecodingError, "Stream does not support seeking."); + } + long position = (stream?.Position ?? 0); + if (position > int.MaxValue || position < int.MinValue) + { + throw new ServiceResultException(StatusCodes.BadDecodingError, "Stream Position exceeds int.MaxValue or int.MinValue."); + } + return (int)position; + } + } /// /// Gets the stream that the decoder is reading from. /// - public Stream BaseStream => m_reader.BaseStream; + public Stream BaseStream => m_reader?.BaseStream; /// /// Decodes a message from a stream. @@ -198,7 +226,7 @@ public static IEncodeable DecodeMessage(byte[] buffer, Type expectedType, IServi /// public IEncodeable DecodeMessage(System.Type expectedType) { - long start = m_reader.BaseStream.CanSeek ? m_reader.BaseStream.Position : 0; + int start = Position; // read the node id. NodeId typeId = ReadNodeId(null); @@ -219,7 +247,7 @@ public IEncodeable DecodeMessage(System.Type expectedType) IEncodeable message = ReadEncodeable(null, actualType, absoluteId); // check that the max message size was not exceeded. - int messageLength = m_reader.BaseStream.CanSeek ? (int)(m_reader.BaseStream.Position - start) : 0; + int messageLength = Position - start; if (m_context.MaxMessageSize > 0 && m_context.MaxMessageSize < messageLength) { throw ServiceResultException.Create(StatusCodes.BadEncodingLimitsExceeded, @@ -2099,6 +2127,7 @@ private ExtensionObject ReadExtensionObject() if (length != used) { errorMessage = "Length mismatch"; + exception = ServiceResultException.Create(StatusCodes.BadDecodingError, errorMessage); } else { @@ -2127,10 +2156,18 @@ private ExtensionObject ReadExtensionObject() // type was known but decoding failed, reset stream! m_reader.BaseStream.Position = start; encodeable = null; + m_encodeablesRecovered++; - // log the error. - Utils.LogWarning(exception, "{0}, failed to decode encodeable type '{1}', NodeId='{2}'. BinaryDecoder recovered.", - errorMessage, systemType.Name, extension.TypeId); + if (m_encodeablesRecovered == 1) + { + // log the error only once to avoid flooding the log. + Utils.LogWarning(exception, "{0}, failed to decode encodeable type '{1}', NodeId='{2}'. BinaryDecoder recovered.", + errorMessage, systemType.Name, extension.TypeId); + } + else if (m_encodeablesRecovered >= kMaxDecoderRecoveries) + { + throw exception; + } } } @@ -2158,11 +2195,16 @@ private ExtensionObject ReadExtensionObject() } // skip any unread data. - int unused = length - (Position - start); + long unused = length - (Position - start); if (unused > 0) { - SafeReadBytes(unused); + if (unused > int.MaxValue) + { + throw ServiceResultException.Create(StatusCodes.BadDecodingError, + "Cannot skip {0} bytes of unknown extension object body with type '{1}'.", unused, extension.TypeId); + } + SafeReadBytes((int)unused); } if (encodeable != null) @@ -2633,6 +2675,19 @@ private void CheckAndIncrementNestingLevel() } m_nestingLevel++; } + + /// + /// Validate the stream requirements. + /// + /// The stream used for decoding. + private void ValidateStreamRequirements(Stream stream) + { + if (stream == null) throw new ArgumentNullException(nameof(stream)); + if (stream?.CanSeek != true || stream?.CanRead != true) + { + throw new ArgumentException("Stream must be seekable and readable."); + } + } #endregion #region Private Fields @@ -2641,6 +2696,7 @@ private void CheckAndIncrementNestingLevel() private ushort[] m_namespaceMappings; private ushort[] m_serverMappings; private uint m_nestingLevel; + private uint m_encodeablesRecovered; #endregion } } From 706fdc3049e87aded356c5a095b9cc2475f3fc88 Mon Sep 17 00:00:00 2001 From: Martin Regen Date: Wed, 24 Apr 2024 20:26:45 +0200 Subject: [PATCH 16/83] updates for recovery --- Fuzzing/common/Fuzz.Tools/Playback.cs | 18 +++--- Fuzzing/common/Fuzz.Tools/Program.cs | 12 ++-- .../Stack/State/BaseVariableState.cs | 4 -- .../Types/Encoders/BinaryDecoder.cs | 55 ++++++++++-------- .../Types/Utils/IServiceMessageContext.cs | 6 ++ .../Types/Utils/ServiceMessageContext.cs | 56 ++++++++----------- common.props | 8 +-- 7 files changed, 82 insertions(+), 77 deletions(-) diff --git a/Fuzzing/common/Fuzz.Tools/Playback.cs b/Fuzzing/common/Fuzz.Tools/Playback.cs index 52a9dd7585..34b3ec50bd 100644 --- a/Fuzzing/common/Fuzz.Tools/Playback.cs +++ b/Fuzzing/common/Fuzz.Tools/Playback.cs @@ -2,6 +2,7 @@ using System.IO; using System; using System.Text; +using System.Diagnostics; public static class Playback { @@ -11,23 +12,24 @@ public static void Run(string directoryPath, bool stackTrace) var searchPattern = Path.GetFileName(directoryPath); foreach (var crashFile in Directory.EnumerateFiles(path, searchPattern)) { + var stopWatch = new Stopwatch(); #if TEXTFUZZER var crashData = Encoding.UTF8.GetString(File.ReadAllBytes(crashFile)); - { - try - { - FuzzableCode.FuzzTarget(crashData); - } #else - using (var crashStream = new FileStream(crashFile, FileMode.Open, FileAccess.Read)) + using (var crashData = new FileStream(crashFile, FileMode.Open, FileAccess.Read)) +#endif { try { - FuzzableCode.FuzzTarget(crashStream); + stopWatch.Start(); + FuzzableCode.FuzzTarget(crashData); + stopWatch.Stop(); + Console.WriteLine("File: {0:20} Elapsed: {1}ms", Path.GetFileName(crashFile), stopWatch.ElapsedMilliseconds); } -#endif catch (Exception ex) { + stopWatch.Stop(); + Console.WriteLine("File: {0:20} Elapsed: {1}ms", Path.GetFileName(crashFile), stopWatch.ElapsedMilliseconds); Console.WriteLine("{0}:{1}", ex.GetType().Name, ex.Message); if (stackTrace) { diff --git a/Fuzzing/common/Fuzz.Tools/Program.cs b/Fuzzing/common/Fuzz.Tools/Program.cs index e8bbdb3b01..b450f8f408 100644 --- a/Fuzzing/common/Fuzz.Tools/Program.cs +++ b/Fuzzing/common/Fuzz.Tools/Program.cs @@ -7,11 +7,12 @@ public static class Program { - public static readonly string DefaultTestcasesFolder = "../../../../Fuzz/Testcases"; - public static readonly string DefaultFindingsCrashFolder = "../../../../findings/crashes/"; - public static readonly string DefaultFindingsHangsFolder = "../../../../findings/hangs/"; - public static readonly string DefaultLibFuzzerCrashes = "../../../crash-*"; - public static readonly string DefaultLibFuzzerHangs = "../../../timeout-*"; + public static readonly string RootFolder = "../../../../"; + public static readonly string DefaultTestcasesFolder = RootFolder + "Fuzz/Testcases/"; + public static readonly string DefaultFindingsCrashFolder = RootFolder + "findings/crashes/"; + public static readonly string DefaultFindingsHangsFolder = RootFolder + "findings/hangs/"; + public static readonly string DefaultLibFuzzerCrashes = RootFolder + "crash-*"; + public static readonly string DefaultLibFuzzerHangs = RootFolder + "timeout-*"; public static void Main(string[] args) { @@ -53,6 +54,7 @@ public static void Main(string[] args) } else if (playback) { + Playback.Run(DefaultTestcasesFolder, stacktrace); Playback.Run(DefaultFindingsCrashFolder, stacktrace); Playback.Run(DefaultFindingsHangsFolder, stacktrace); Playback.Run(DefaultLibFuzzerCrashes, stacktrace); diff --git a/Stack/Opc.Ua.Core/Stack/State/BaseVariableState.cs b/Stack/Opc.Ua.Core/Stack/State/BaseVariableState.cs index 40d4d74b00..48eadba088 100644 --- a/Stack/Opc.Ua.Core/Stack/State/BaseVariableState.cs +++ b/Stack/Opc.Ua.Core/Stack/State/BaseVariableState.cs @@ -384,10 +384,6 @@ public static object DecodeExtensionObject(ISystemContext context, Type targetTy { decoder = new XmlDecoder(extension.Body as XmlElement, messageContext); } - else if (extension.Encoding == ExtensionObjectEncoding.Json) - { - decoder = new JsonDecoder(extension.Body as string, messageContext); - } if (decoder != null) { diff --git a/Stack/Opc.Ua.Core/Types/Encoders/BinaryDecoder.cs b/Stack/Opc.Ua.Core/Types/Encoders/BinaryDecoder.cs index f239ad509a..cbe384b1ba 100644 --- a/Stack/Opc.Ua.Core/Types/Encoders/BinaryDecoder.cs +++ b/Stack/Opc.Ua.Core/Types/Encoders/BinaryDecoder.cs @@ -24,10 +24,6 @@ namespace Opc.Ua /// public class BinaryDecoder : IDecoder { - // The number of times the encodeable decoder can recover from - // an error before throwing an exception. - const int kMaxDecoderRecoveries = 32; - #region Constructor /// /// Creates a decoder that reads from a memory buffer. @@ -403,7 +399,8 @@ public string ReadString(string fieldName) } /// - /// Reads a string from the stream (throws an exception if its length exceeds the limit specified). + /// Reads a string from the stream (throws an exception if + /// its length is invalid or exceeds the limit specified). /// public string ReadString(string fieldName, int maxStringLength) { @@ -427,6 +424,8 @@ public string ReadString(string fieldName, int maxStringLength) byte[] bytes = SafeReadBytes(length); + // length is always >= 1 here + // If 0 terminated, decrease length by one before converting to string var utf8StringLength = bytes[bytes.Length - 1] == 0 ? bytes.Length - 1 : bytes.Length; return Encoding.UTF8.GetString(bytes, 0, utf8StringLength); @@ -2127,7 +2126,7 @@ private ExtensionObject ReadExtensionObject() if (length != used) { errorMessage = "Length mismatch"; - exception = ServiceResultException.Create(StatusCodes.BadDecodingError, errorMessage); + exception = null; } else { @@ -2153,21 +2152,29 @@ private ExtensionObject ReadExtensionObject() if (resetStream) { - // type was known but decoding failed, reset stream! - m_reader.BaseStream.Position = start; - encodeable = null; - m_encodeablesRecovered++; - - if (m_encodeablesRecovered == 1) + // type was known but decoding failed, + // reset stream to return ExtensionObject if configured to do so! + // decoding failure of a known type in ns=0 is always a decoding error. + if (typeId.NamespaceIndex == 0 || + m_encodeablesRecovered >= m_context.MaxDecoderRecoveries) + { + throw exception ?? + ServiceResultException.Create(StatusCodes.BadDecodingError, "{0}, failed to decode encodeable type '{1}', NodeId='{2}'.", + errorMessage, systemType.Name, extension.TypeId); + } + else if (m_encodeablesRecovered == 0) { // log the error only once to avoid flooding the log. Utils.LogWarning(exception, "{0}, failed to decode encodeable type '{1}', NodeId='{2}'. BinaryDecoder recovered.", errorMessage, systemType.Name, extension.TypeId); } - else if (m_encodeablesRecovered >= kMaxDecoderRecoveries) - { - throw exception; - } + + // reset the stream to the begin of the ExtensionObject body. + m_reader.BaseStream.Position = start; + encodeable = null; + + // count number of recoveries + m_encodeablesRecovered++; } } @@ -2194,17 +2201,12 @@ private ExtensionObject ReadExtensionObject() return extension; } - // skip any unread data. + // any unread data indicates a decoding error. long unused = length - (Position - start); - if (unused > 0) { - if (unused > int.MaxValue) - { - throw ServiceResultException.Create(StatusCodes.BadDecodingError, - "Cannot skip {0} bytes of unknown extension object body with type '{1}'.", unused, extension.TypeId); - } - SafeReadBytes((int)unused); + throw ServiceResultException.Create(StatusCodes.BadDecodingError, + "Cannot skip {0} bytes of unknown extension object body with type '{1}'.", unused, extension.TypeId); } if (encodeable != null) @@ -2455,6 +2457,11 @@ private Variant ReadVariantValue(string fieldName) [MethodImpl(MethodImplOptions.AggressiveInlining)] private byte[] SafeReadBytes(int length, [CallerMemberName] string functionName = null) { + if (length == 0) + { + return Array.Empty(); + } + byte[] bytes = m_reader.ReadBytes(length); if (bytes.Length != length) { diff --git a/Stack/Opc.Ua.Core/Types/Utils/IServiceMessageContext.cs b/Stack/Opc.Ua.Core/Types/Utils/IServiceMessageContext.cs index 4060af37d6..dec65c2219 100644 --- a/Stack/Opc.Ua.Core/Types/Utils/IServiceMessageContext.cs +++ b/Stack/Opc.Ua.Core/Types/Utils/IServiceMessageContext.cs @@ -45,6 +45,12 @@ public interface IServiceMessageContext /// uint MaxEncodingNestingLevels { get; } + /// + /// The number of times the decoder can recover from an error + /// caused by a custom complex type before throwing an exception. + /// + uint MaxDecoderRecoveries { get; } + /// /// The table of namespaces used by the server. /// diff --git a/Stack/Opc.Ua.Core/Types/Utils/ServiceMessageContext.cs b/Stack/Opc.Ua.Core/Types/Utils/ServiceMessageContext.cs index 49824597fa..e5aa76b632 100644 --- a/Stack/Opc.Ua.Core/Types/Utils/ServiceMessageContext.cs +++ b/Stack/Opc.Ua.Core/Types/Utils/ServiceMessageContext.cs @@ -25,17 +25,15 @@ public class ServiceMessageContext : IServiceMessageContext /// public ServiceMessageContext() { - m_maxStringLength = UInt16.MaxValue; - m_maxByteStringLength = UInt16.MaxValue * 16; - m_maxArrayLength = UInt16.MaxValue; - m_maxMessageSize = UInt16.MaxValue * 32; - m_namespaceUris = new NamespaceTable(); - m_serverUris = new StringTable(); - m_factory = EncodeableFactory.GlobalFactory; - m_maxEncodingNestingLevels = 200; + Initialize(false); } private ServiceMessageContext(bool shared) : this() + { + Initialize(shared); + } + + private void Initialize(bool shared) { m_maxStringLength = UInt16.MaxValue; m_maxByteStringLength = UInt16.MaxValue * 16; @@ -45,6 +43,7 @@ private ServiceMessageContext(bool shared) : this() m_serverUris = new StringTable(shared); m_factory = EncodeableFactory.GlobalFactory; m_maxEncodingNestingLevels = 200; + m_maxDecoderRecoveries = 0; } #endregion @@ -69,53 +68,49 @@ public static ServiceMessageContext ThreadContext #endregion #region Public Properties - /// - /// The maximum length for any string, byte string or xml element. - /// + /// public int MaxStringLength { get => m_maxStringLength; set { m_maxStringLength = value; } } - /// - /// The maximum length for any array. - /// + /// public int MaxArrayLength { get => m_maxArrayLength; set { m_maxArrayLength = value; } } - /// - /// The maximum length for any ByteString. - /// + /// public int MaxByteStringLength { get => m_maxByteStringLength; set { m_maxByteStringLength = value; } } - /// - /// The maximum length for any Message. - /// + /// public int MaxMessageSize { get => m_maxMessageSize; set { m_maxMessageSize = value; } } - /// - /// The maximum nesting level accepted while encoding or decoding objects. - /// + /// public uint MaxEncodingNestingLevels { get => m_maxEncodingNestingLevels; + set { m_maxEncodingNestingLevels = value; } } - /// - /// The table of namespaces used by the server. - /// + /// + public uint MaxDecoderRecoveries + { + get => m_maxDecoderRecoveries; + set { m_maxDecoderRecoveries = value; } + } + + /// public NamespaceTable NamespaceUris { get => m_namespaceUris; @@ -131,9 +126,7 @@ public NamespaceTable NamespaceUris } } - /// - /// The table of servers used by the server. - /// + /// public StringTable ServerUris { get => m_serverUris; @@ -150,9 +143,7 @@ public StringTable ServerUris } } - /// - /// The factory used to create encodeable objects. - /// + /// public IEncodeableFactory Factory { get => m_factory; @@ -176,6 +167,7 @@ public IEncodeableFactory Factory private int m_maxArrayLength; private int m_maxMessageSize; private uint m_maxEncodingNestingLevels; + private uint m_maxDecoderRecoveries; private NamespaceTable m_namespaceUris; private StringTable m_serverUris; private IEncodeableFactory m_factory; diff --git a/common.props b/common.props index 7041993c95..3db40bb6b1 100644 --- a/common.props +++ b/common.props @@ -2,9 +2,9 @@ OPC UA .NET Standard Library https://github.com/OPCFoundation/UA-.NETStandard - 1.04.372 + 1.05.374 preview-$([System.DateTime]::Now.ToString("yyyyMMdd")) - Copyright © 2004-2023 OPC Foundation, Inc + Copyright © 2004-2024 OPC Foundation, Inc OPC Foundation OPC Foundation NU5125;CA2254;CA1307 @@ -12,12 +12,12 @@ true false true - + 7.3 true - + From ab2c8d6b6860c334317e1280f2697da266e0ac41 Mon Sep 17 00:00:00 2001 From: Martin Regen Date: Wed, 24 Apr 2024 20:33:46 +0200 Subject: [PATCH 17/83] fix --- .../Stack/Tcp/TcpReverseConnectChannel.cs | 25 ++++++++++--------- 1 file changed, 13 insertions(+), 12 deletions(-) diff --git a/Stack/Opc.Ua.Core/Stack/Tcp/TcpReverseConnectChannel.cs b/Stack/Opc.Ua.Core/Stack/Tcp/TcpReverseConnectChannel.cs index 5840b73758..abc79ce092 100644 --- a/Stack/Opc.Ua.Core/Stack/Tcp/TcpReverseConnectChannel.cs +++ b/Stack/Opc.Ua.Core/Stack/Tcp/TcpReverseConnectChannel.cs @@ -106,21 +106,22 @@ private bool ProcessReverseHelloMessage(uint messageType, ArraySegment mes State = TcpChannelState.Connecting; - Task t = Task.Run(async () => { - try - { - if (false == await Listener.TransferListenerChannel(Id, serverUri, endpointUri).ConfigureAwait(false)) + Task t = Task.Run(async () => { + try + { + if (false == await Listener.TransferListenerChannel(Id, serverUri, endpointUri).ConfigureAwait(false)) + { + SetResponseRequired(true); + ForceChannelFault(StatusCodes.BadTcpMessageTypeInvalid, "The reverse connection was rejected by the client."); + } + } + catch (Exception) { SetResponseRequired(true); - ForceChannelFault(StatusCodes.BadTcpMessageTypeInvalid, "The reverse connection was rejected by the client."); + ForceChannelFault(StatusCodes.BadInternalError, "Internal error approving the reverse connection."); } - } - catch (Exception) - { - SetResponseRequired(true); - ForceChannelFault(StatusCodes.BadInternalError, "Internal error approving the reverse connection."); - } - }); + }); + } } catch (Exception e) { From 8e0a545897abd93a5dc78799ee46221a7a8abad3 Mon Sep 17 00:00:00 2001 From: Martin Regen Date: Thu, 25 Apr 2024 20:45:53 +0200 Subject: [PATCH 18/83] add more test cases per project --- Fuzzing/BinaryDecoder/Fuzz/FuzzTarget.cs | 123 +++++++++++++++++------ Fuzzing/BinaryDecoder/fuzz.sh | 2 +- Fuzzing/BinaryDecoder/libfuzz.bat | 2 +- Fuzzing/common/Fuzz/Program.cs | 103 +++++++++++++++++-- Fuzzing/dictionaries/json.dict | 73 ++++++++++++++ Fuzzing/dictionaries/xml.dict | 82 +++++++++++++++ Fuzzing/scripts/fuzz-libfuzzer.ps1 | 8 +- Fuzzing/scripts/fuzz.ps1 | 8 +- 8 files changed, 356 insertions(+), 45 deletions(-) create mode 100644 Fuzzing/dictionaries/json.dict create mode 100644 Fuzzing/dictionaries/xml.dict diff --git a/Fuzzing/BinaryDecoder/Fuzz/FuzzTarget.cs b/Fuzzing/BinaryDecoder/Fuzz/FuzzTarget.cs index d3c5671253..46b4335525 100644 --- a/Fuzzing/BinaryDecoder/Fuzz/FuzzTarget.cs +++ b/Fuzzing/BinaryDecoder/Fuzz/FuzzTarget.cs @@ -8,16 +8,96 @@ public static class FuzzableCode { const int SegmentSize = 0x40; - private static ServiceMessageContext messageContext = null; + private static ServiceMessageContext messageContext = ServiceMessageContext.GlobalContext; /// - /// The fuzz target for afl-fuzz. + /// Print information about the fuzzer target. + /// + public static void FuzzInfo() + { + Console.WriteLine("OPC UA Core Encoder Fuzzer for afl-fuzz and libfuzzer."); + Console.WriteLine("Fuzzing targets for various aspects of the Binary, Json and Xml encoders."); + } + + /// + /// The binary decoder fuzz target for afl-fuzz. /// /// The stdin stream from the afl-fuzz process. - public static void FuzzTarget(Stream stream) + public static void AflfuzzBinaryDecoder(Stream stream) { - // fuzzer uses a non seekable stream, causing false positives - // use ArraySegmentStream in combination with fuzzed decoder... + using (var memoryStream = PrepareArraySegmentStream(stream)) + { + FuzzBinaryDecoderCore(memoryStream); + } + } + + /// + /// The binary encoder fuzz target for afl-fuzz. + /// + /// The stdin stream from the afl-fuzz process. + public static void AflfuzzBinaryEncoder(Stream stream) + { + IEncodeable encodeable = null; + using (var memoryStream = PrepareArraySegmentStream(stream)) + { + try + { + encodeable = FuzzBinaryDecoderCore(memoryStream); + } + catch + { + return; + } + } + + // encode the fuzzed object and see if it crashes + if (encodeable != null) + { + _ = BinaryEncoder.EncodeMessage(encodeable, messageContext); + } + } + + /// + /// The fuzz target for libfuzzer. + /// + public static void LibfuzzBinaryDecoder(ReadOnlySpan input) + { + using (var memoryStream = new MemoryStream(input.ToArray())) + { + _ = FuzzBinaryDecoderCore(memoryStream); + } + } + + /// + /// The binary encoder fuzz target for afl-fuzz. + /// + /// The stdin stream from the afl-fuzz process. + public static void LibfuzzBinaryEncoder(ReadOnlySpan input) + { + IEncodeable encodeable = null; + using (var memoryStream = new MemoryStream(input.ToArray())) + { + try + { + encodeable = FuzzBinaryDecoderCore(memoryStream); + } + catch + { + return; + } + } + + // encode the fuzzed object and see if it crashes + if (encodeable != null) + { + _ = BinaryEncoder.EncodeMessage(encodeable, messageContext); + } + } + + private static MemoryStream PrepareArraySegmentStream(Stream stream) + { + // afl-fuzz uses a non seekable stream, causing false positives + // use ArraySegmentStream in combination with fuzz target... MemoryStream memoryStream; using (var binaryStream = new BinaryReader(stream)) { @@ -31,46 +111,29 @@ public static void FuzzTarget(Stream stream) memoryStream = new ArraySegmentStream(bufferCollection); } - FuzzTargetCore(memoryStream); - } - - /// - /// The fuzz target for libfuzzer. - /// - public static void FuzzTargetLibfuzzer(ReadOnlySpan input) - { - var memoryStream = new MemoryStream(input.ToArray()); - FuzzTargetCore(memoryStream); + return memoryStream; } /// /// The fuzz target for the BinaryDecoder. /// /// A memory stream with fuzz content. - private static void FuzzTargetCore(MemoryStream stream) + private static IEncodeable FuzzBinaryDecoderCore(MemoryStream stream) { - if (messageContext == null) - { - messageContext = new ServiceMessageContext(); - } - try { using (var decoder = new BinaryDecoder(stream, messageContext)) { - _ = decoder.DecodeMessage(null); + return decoder.DecodeMessage(null); } } - catch (Exception ex) + catch (ServiceResultException sre) { - if (ex is ServiceResultException sre) + switch (sre.StatusCode) { - switch (sre.StatusCode) - { - case StatusCodes.BadEncodingLimitsExceeded: - case StatusCodes.BadDecodingError: - return; - } + case StatusCodes.BadEncodingLimitsExceeded: + case StatusCodes.BadDecodingError: + return null; } throw; diff --git a/Fuzzing/BinaryDecoder/fuzz.sh b/Fuzzing/BinaryDecoder/fuzz.sh index 0fe97053bd..cf536b1d19 100644 --- a/Fuzzing/BinaryDecoder/fuzz.sh +++ b/Fuzzing/BinaryDecoder/fuzz.sh @@ -1 +1 @@ -pwsh ../scripts/fuzz.ps1 ./Fuzz/BinaryDecoder.Fuzz.csproj -i ./Fuzz/Testcases \ No newline at end of file +pwsh ../scripts/fuzz.ps1 ./Fuzz/BinaryDecoder.Fuzz.csproj -i ./Fuzz/Testcases -fuzztarget AflfuzzBinaryDecoder \ No newline at end of file diff --git a/Fuzzing/BinaryDecoder/libfuzz.bat b/Fuzzing/BinaryDecoder/libfuzz.bat index 3fcd16bd47..d5469ddf81 100644 --- a/Fuzzing/BinaryDecoder/libfuzz.bat +++ b/Fuzzing/BinaryDecoder/libfuzz.bat @@ -1 +1 @@ -Powershell -File ..\scripts\fuzz-libfuzzer.ps1 -libFuzzer ".\libfuzzer-dotnet-windows.exe" -project .\Fuzz\BinaryDecoder.Fuzz.csproj -corpus .\Fuzz\Testcases \ No newline at end of file +Powershell -File ..\scripts\fuzz-libfuzzer.ps1 -libFuzzer ".\libfuzzer-dotnet-windows.exe" -project .\Fuzz\BinaryDecoder.Fuzz.csproj -fuzztarget LibfuzzBinaryDecoder -corpus .\Fuzz\Testcases \ No newline at end of file diff --git a/Fuzzing/common/Fuzz/Program.cs b/Fuzzing/common/Fuzz/Program.cs index 59b359690c..4787572cda 100644 --- a/Fuzzing/common/Fuzz/Program.cs +++ b/Fuzzing/common/Fuzz/Program.cs @@ -1,19 +1,104 @@ +using System; +using System.IO; +using System.Reflection; using SharpFuzz; public static class Program { + // signatures supported by fuzzers + public delegate void AflFuzzStream(Stream stream); + public delegate void AflFuzzString(string text); + public delegate void LibFuzzSpan(ReadOnlySpan bytes); + public static void Main(string[] args) { -#if LIBFUZZER - Fuzzer.LibFuzzer.Run(input => { - FuzzableCode.FuzzTargetLibfuzzer(input); - }); -#else - Fuzzer.Run(input => { - FuzzableCode.FuzzTarget(input); - }); -#endif + string fuzzingFunction = string.Empty; + + FuzzableCode.FuzzInfo(); + + if (args.Length == 1) + { + // find the function to fuzz based on the first argument using reflection + Type type = typeof(FuzzableCode); + fuzzingFunction = args[0]; + MethodInfo method = type.GetMethod(args[0], BindingFlags.Public | BindingFlags.Instance | BindingFlags.Static); + if (method != null) + { + Console.WriteLine($"Found the fuzzing function: {args[0]}"); + + // call the fuzzer target if there is a matching signature + ParameterInfo[] parameters = method.GetParameters(); + if (parameters.Length == 1) + { + // afl-fuzz targets + if (parameters[0].ParameterType == typeof(Stream)) + { + var fuzzMethod = (AflFuzzStream)method.CreateDelegate(typeof(AflFuzzStream)); + Fuzzer.Run(stream => fuzzMethod(stream)); + return; + } + else if (parameters[0].ParameterType == typeof(string)) + { + var fuzzMethod = (AflFuzzString)method.CreateDelegate(typeof(AflFuzzString)); + Fuzzer.Run(text => fuzzMethod(text)); + return; + } + // libfuzzer span target + else if (parameters[0].ParameterType == typeof(ReadOnlySpan)) + { + var fuzzMethod = (LibFuzzSpan)method.CreateDelegate(typeof(LibFuzzSpan)); + Fuzzer.LibFuzzer.Run(bytes => fuzzMethod(bytes)); + return; + } + } + + Console.Error.WriteLine("The fuzzing function {0} does not have the correct signature {1}.", fuzzingFunction, parameters[0].ParameterType); + } + } + + Usage(fuzzingFunction); + } + + private static void Usage(string fuzzingFunction) + { + Type type = typeof(FuzzableCode); + string applicationName = typeof(Program).Assembly.GetName().Name; + Console.Error.WriteLine("The fuzzing function {0} was not found.", fuzzingFunction); + Console.Error.WriteLine("Usage: {0} [fuzzingFunction]", applicationName); + Console.Error.WriteLine(); + Console.Error.WriteLine("Available fuzzing functions:"); + + foreach (var parameterType in new Type[] { typeof(Stream), typeof(string), typeof(ReadOnlySpan) }) + { + bool writeHeader = true; + foreach (var m in type.GetMethods(BindingFlags.Public | BindingFlags.Instance | BindingFlags.Static)) + { + ParameterInfo[] parameters = m.GetParameters(); + if (parameters.Length == 1 && parameters[0].ParameterType == parameterType) + { + if (writeHeader) + { + Console.Error.WriteLine(); + if (parameterType == typeof(Stream)) + { + Console.Error.WriteLine("afl-fuzz Stream signature:"); + } + else if (parameterType == typeof(string)) + { + Console.Error.WriteLine("afl-fuzz string signature:"); + } + else if (parameterType == typeof(ReadOnlySpan)) + { + Console.Error.WriteLine("libfuzzer: ReadOnlySpan signature:"); + } + writeHeader = false; + } + + Console.Error.WriteLine("-- {0}", m.Name); + } + } + } } } diff --git a/Fuzzing/dictionaries/json.dict b/Fuzzing/dictionaries/json.dict new file mode 100644 index 0000000000..b0d0ba46a0 --- /dev/null +++ b/Fuzzing/dictionaries/json.dict @@ -0,0 +1,73 @@ +"0" +"7" +"," +":" +"2.1e24" + +"true" +"false" +"null" + +"\"\"" +"\"\":" + +"{}" +",{}" +":{}" +"{\"\":0}" +"{{}}" + +"[]" +",[]" +":[]" +"[0]" +"[[]]" + +"''" +"\\" +"\\b" +"\\f" +"\\n" +"\\r" +"\\t" +"\\u0000" +"\\x00" +"\\0" +"\\uD800\\uDC00" +"\\uDBFF\\uDFFF" + +"\"\":0" +"//" +"/**/" + + +# Things like geojson, json-ld, ... +"$ref" +"type" +"coordinates" +"@context" +"@id" +"@type" + +# Strings with truncated special values +"{\"foo\":fa" +"{\"foo\":t" +"{\"foo\":nul" + +"{" +"}" +"\"qty\": 1, \"qty\": -1" +"\"qty\": 1, \"qty\\ud800\": -1" +"\"qty\": 1, \"qt\\y\": -1" +"/*" +"*/" +"\"" +"1.7976931348623157e+308" +"5e-324" +"9007199254740991" +"-9007199254740991" + +"}=" + +",," +"{\"\":" diff --git a/Fuzzing/dictionaries/xml.dict b/Fuzzing/dictionaries/xml.dict new file mode 100644 index 0000000000..e101369188 --- /dev/null +++ b/Fuzzing/dictionaries/xml.dict @@ -0,0 +1,82 @@ +# +# AFL dictionary for XML +# ---------------------- +# +# Several basic syntax elements and attributes, modeled on libxml2. +# +# Created by Michal Zalewski +# + +attr_encoding=" encoding=\"1\"" +attr_generic=" a=\"1\"" +attr_href=" href=\"1\"" +attr_standalone=" standalone=\"no\"" +attr_version=" version=\"1\"" +attr_xml_base=" xml:base=\"1\"" +attr_xml_id=" xml:id=\"1\"" +attr_xml_lang=" xml:lang=\"1\"" +attr_xml_space=" xml:space=\"1\"" +attr_xmlns=" xmlns=\"1\"" + +entity_builtin="<" +entity_decimal="" +entity_external="&a;" +entity_hex="" + +string_any="ANY" +string_brackets="[]" +string_cdata="CDATA" +string_col_fallback=":fallback" +string_col_generic=":a" +string_col_include=":include" +string_dashes="--" +string_empty="EMPTY" +string_empty_dblquotes="\"\"" +string_empty_quotes="''" +string_entities="ENTITIES" +string_entity="ENTITY" +string_fixed="#FIXED" +string_id="ID" +string_idref="IDREF" +string_idrefs="IDREFS" +string_implied="#IMPLIED" +string_nmtoken="NMTOKEN" +string_nmtokens="NMTOKENS" +string_notation="NOTATION" +string_parentheses="()" +string_pcdata="#PCDATA" +string_percent="%a" +string_public="PUBLIC" +string_required="#REQUIRED" +string_schema=":schema" +string_system="SYSTEM" +string_ucs4="UCS-4" +string_utf16="UTF-16" +string_utf8="UTF-8" +string_xmlns="xmlns:" + +tag_attlist="" +tag_doctype="" +tag_open_close="" +tag_open_exclamation="" +tag_xml_q="" + +encoding_utf="UTF-" +encoding_iso1="ISO-8859" +encoding_iso3="ISO-10646-UCS" +encoding_iso5="ISO-LATIN-1" +encoding_jis="SHIFT_JIS" +encoding_utf7="UTF-7" +encoding_utf16le="UTF-16BE" +encoding_utf16le="UTF-16LE" +encoding_ascii="US-ASCII" diff --git a/Fuzzing/scripts/fuzz-libfuzzer.ps1 b/Fuzzing/scripts/fuzz-libfuzzer.ps1 index 0c1883a3c6..cf6c12a8a7 100644 --- a/Fuzzing/scripts/fuzz-libfuzzer.ps1 +++ b/Fuzzing/scripts/fuzz-libfuzzer.ps1 @@ -5,6 +5,8 @@ param ( [string]$project, [Parameter(Mandatory = $true)] [string]$corpus, + [Parameter(Mandatory = $true)] + [string]$fuzztarget, [string]$dict = $null, [int]$timeout = 10, [string]$command = "sharpfuzz" @@ -52,8 +54,10 @@ foreach ($fuzzingTarget in $fuzzingTargets) { Write-Output "Start $libFuzzer" if ($dict) { - & $libFuzzer -timeout="$timeout" -dict="$dict" --target_path=dotnet --target_arg=$project $corpus + Write-Output $libFuzzer -timeout="$timeout" -dict="$dict" --target_path=dotnet --target_arg="$project $fuzztarget" $corpus + & $libFuzzer -timeout="$timeout" -dict="$dict" --target_path=dotnet --target_arg="$project $fuzztarget" $corpus } else { - & $libFuzzer -timeout="$timeout" --target_path=dotnet --target_arg=$project $corpus + Write-Output $libFuzzer -timeout="$timeout" --target_path=dotnet --target_arg="$project $fuzztarget" $corpus + & $libFuzzer -timeout="$timeout" --target_path=dotnet --target_arg="$project $fuzztarget" $corpus } diff --git a/Fuzzing/scripts/fuzz.ps1 b/Fuzzing/scripts/fuzz.ps1 index 938098117b..9c83df4315 100644 --- a/Fuzzing/scripts/fuzz.ps1 +++ b/Fuzzing/scripts/fuzz.ps1 @@ -3,6 +3,8 @@ param ( [string]$project, [Parameter(Mandatory = $true)] [string]$i, + [Parameter(Mandatory = $true)] + [string]$fuzztarget, [string]$x = $null, [int]$t = 10000, [string]$command = "sharpfuzz" @@ -56,8 +58,10 @@ foreach ($fuzzingTarget in $fuzzingTargets) { $env:AFL_SKIP_BIN_CHECK = 1 if ($x) { - afl-fuzz -i $i -o $findingsDir -t $t -m none -x $x dotnet $project + Write-Output afl-fuzz -i $i -o $findingsDir -t $t -m none -x $x dotnet $project $fuzztarget + afl-fuzz -i $i -o $findingsDir -t $t -m none -x $x dotnet $project $target } else { - afl-fuzz -i $i -o $findingsDir -t $t -m none dotnet $project + Write-Output afl-fuzz -i $i -o $findingsDir -t $t -m none dotnet $project $fuzztarget + afl-fuzz -i $i -o $findingsDir -t $t -m none dotnet $project $target } From 28a143b9c22ba5cd69b308d3b5ab17a5ab04b04c Mon Sep 17 00:00:00 2001 From: Martin Regen Date: Thu, 25 Apr 2024 22:18:27 +0200 Subject: [PATCH 19/83] multiple fuzzer per project --- .../{fuzz.sh => aflfuzz_binarydecoder.sh} | 0 Fuzzing/BinaryDecoder/aflfuzz_binaryencoder.sh | 1 + .../{libfuzz.bat => libfuzz_binarydecoder.bat} | 0 Fuzzing/BinaryDecoder/libfuzz_binaryencoder.bat | 1 + Fuzzing/common/Fuzz/Program.cs | 6 +++++- Fuzzing/scripts/fuzz-libfuzzer.ps1 | 12 ++++++++---- Fuzzing/scripts/fuzz.ps1 | 4 ++-- 7 files changed, 17 insertions(+), 7 deletions(-) rename Fuzzing/BinaryDecoder/{fuzz.sh => aflfuzz_binarydecoder.sh} (100%) create mode 100644 Fuzzing/BinaryDecoder/aflfuzz_binaryencoder.sh rename Fuzzing/BinaryDecoder/{libfuzz.bat => libfuzz_binarydecoder.bat} (100%) create mode 100644 Fuzzing/BinaryDecoder/libfuzz_binaryencoder.bat diff --git a/Fuzzing/BinaryDecoder/fuzz.sh b/Fuzzing/BinaryDecoder/aflfuzz_binarydecoder.sh similarity index 100% rename from Fuzzing/BinaryDecoder/fuzz.sh rename to Fuzzing/BinaryDecoder/aflfuzz_binarydecoder.sh diff --git a/Fuzzing/BinaryDecoder/aflfuzz_binaryencoder.sh b/Fuzzing/BinaryDecoder/aflfuzz_binaryencoder.sh new file mode 100644 index 0000000000..cf536b1d19 --- /dev/null +++ b/Fuzzing/BinaryDecoder/aflfuzz_binaryencoder.sh @@ -0,0 +1 @@ +pwsh ../scripts/fuzz.ps1 ./Fuzz/BinaryDecoder.Fuzz.csproj -i ./Fuzz/Testcases -fuzztarget AflfuzzBinaryDecoder \ No newline at end of file diff --git a/Fuzzing/BinaryDecoder/libfuzz.bat b/Fuzzing/BinaryDecoder/libfuzz_binarydecoder.bat similarity index 100% rename from Fuzzing/BinaryDecoder/libfuzz.bat rename to Fuzzing/BinaryDecoder/libfuzz_binarydecoder.bat diff --git a/Fuzzing/BinaryDecoder/libfuzz_binaryencoder.bat b/Fuzzing/BinaryDecoder/libfuzz_binaryencoder.bat new file mode 100644 index 0000000000..ca815b6d25 --- /dev/null +++ b/Fuzzing/BinaryDecoder/libfuzz_binaryencoder.bat @@ -0,0 +1 @@ +Powershell -File ..\scripts\fuzz-libfuzzer.ps1 -libFuzzer ".\libfuzzer-dotnet-windows.exe" -project .\Fuzz\BinaryDecoder.Fuzz.csproj -fuzztarget LibfuzzBinaryEncoder -corpus .\Fuzz\Testcases \ No newline at end of file diff --git a/Fuzzing/common/Fuzz/Program.cs b/Fuzzing/common/Fuzz/Program.cs index 4787572cda..3ad0748d34 100644 --- a/Fuzzing/common/Fuzz/Program.cs +++ b/Fuzzing/common/Fuzz/Program.cs @@ -17,6 +17,7 @@ public static void Main(string[] args) string fuzzingFunction = string.Empty; FuzzableCode.FuzzInfo(); + Console.WriteLine(); if (args.Length == 1) { @@ -56,6 +57,10 @@ public static void Main(string[] args) Console.Error.WriteLine("The fuzzing function {0} does not have the correct signature {1}.", fuzzingFunction, parameters[0].ParameterType); } + else + { + Console.Error.WriteLine("The fuzzing function {0} was not found.", fuzzingFunction); + } } Usage(fuzzingFunction); @@ -65,7 +70,6 @@ private static void Usage(string fuzzingFunction) { Type type = typeof(FuzzableCode); string applicationName = typeof(Program).Assembly.GetName().Name; - Console.Error.WriteLine("The fuzzing function {0} was not found.", fuzzingFunction); Console.Error.WriteLine("Usage: {0} [fuzzingFunction]", applicationName); Console.Error.WriteLine(); Console.Error.WriteLine("Available fuzzing functions:"); diff --git a/Fuzzing/scripts/fuzz-libfuzzer.ps1 b/Fuzzing/scripts/fuzz-libfuzzer.ps1 index cf6c12a8a7..35fddabeff 100644 --- a/Fuzzing/scripts/fuzz-libfuzzer.ps1 +++ b/Fuzzing/scripts/fuzz-libfuzzer.ps1 @@ -7,6 +7,7 @@ param ( [string]$corpus, [Parameter(Mandatory = $true)] [string]$fuzztarget, + [string]$temp = ".\libfuzz\Testcases", [string]$dict = $null, [int]$timeout = 10, [string]$command = "sharpfuzz" @@ -52,12 +53,15 @@ foreach ($fuzzingTarget in $fuzzingTargets) { } } +mkdir $temp +copy $corpus\*.* $temp + Write-Output "Start $libFuzzer" if ($dict) { - Write-Output $libFuzzer -timeout="$timeout" -dict="$dict" --target_path=dotnet --target_arg="$project $fuzztarget" $corpus - & $libFuzzer -timeout="$timeout" -dict="$dict" --target_path=dotnet --target_arg="$project $fuzztarget" $corpus + Write-Output "$libFuzzer -timeout="$timeout" -dict="$dict" --target_path=dotnet --target_arg=$project $fuzztarget $temp" + & $libFuzzer -timeout="$timeout" -dict="$dict" --target_path=dotnet --target_arg="$project $fuzztarget" $temp } else { - Write-Output $libFuzzer -timeout="$timeout" --target_path=dotnet --target_arg="$project $fuzztarget" $corpus - & $libFuzzer -timeout="$timeout" --target_path=dotnet --target_arg="$project $fuzztarget" $corpus + Write-Output "$libFuzzer -timeout="$timeout" --target_path=dotnet --target_arg=$project $fuzztarget $temp" + & $libFuzzer -timeout="$timeout" --target_path=dotnet --target_arg="$project $fuzztarget" $temp } diff --git a/Fuzzing/scripts/fuzz.ps1 b/Fuzzing/scripts/fuzz.ps1 index 9c83df4315..006342cd36 100644 --- a/Fuzzing/scripts/fuzz.ps1 +++ b/Fuzzing/scripts/fuzz.ps1 @@ -59,9 +59,9 @@ $env:AFL_SKIP_BIN_CHECK = 1 if ($x) { Write-Output afl-fuzz -i $i -o $findingsDir -t $t -m none -x $x dotnet $project $fuzztarget - afl-fuzz -i $i -o $findingsDir -t $t -m none -x $x dotnet $project $target + afl-fuzz -i $i -o $findingsDir -t $t -m none -x $x dotnet $project $fuzztarget } else { Write-Output afl-fuzz -i $i -o $findingsDir -t $t -m none dotnet $project $fuzztarget - afl-fuzz -i $i -o $findingsDir -t $t -m none dotnet $project $target + afl-fuzz -i $i -o $findingsDir -t $t -m none dotnet $project $fuzztarget } From f43258234fa4dded54d748fa3936330489595293 Mon Sep 17 00:00:00 2001 From: Martin Regen Date: Sat, 27 Apr 2024 21:48:22 +0200 Subject: [PATCH 20/83] a single fuzz assembly --- .../BinaryDecoder.Fuzz.Tools.csproj | 2 +- ...arget.cs => FuzzableCode.BinaryDecoder.cs} | 39 +---- .../Fuzz/FuzzableCode.JsonDecoder.cs | 105 ++++++++++++ Fuzzing/BinaryDecoder/Fuzz/FuzzableCode.cs | 46 +++++ .../readrequest.bin | Bin .../readresponse.bin | Bin .../Fuzz/TestcasesJson/readrequest.json | 1 + .../Fuzz/TestcasesJson/readresponse.json | 1 + Fuzzing/BinaryDecoder/aflfuzz.sh | 58 +++++++ Fuzzing/scripts/{fuzz.ps1 => fuzz-afl.ps1} | 0 .../Opc.Ua.Core/Types/Encoders/JsonDecoder.cs | 157 +++++++++++------- 11 files changed, 313 insertions(+), 96 deletions(-) rename Fuzzing/BinaryDecoder/Fuzz/{FuzzTarget.cs => FuzzableCode.BinaryDecoder.cs} (67%) create mode 100644 Fuzzing/BinaryDecoder/Fuzz/FuzzableCode.JsonDecoder.cs create mode 100644 Fuzzing/BinaryDecoder/Fuzz/FuzzableCode.cs rename Fuzzing/BinaryDecoder/Fuzz/{Testcases => TestcasesBinary}/readrequest.bin (100%) rename Fuzzing/BinaryDecoder/Fuzz/{Testcases => TestcasesBinary}/readresponse.bin (100%) create mode 100644 Fuzzing/BinaryDecoder/Fuzz/TestcasesJson/readrequest.json create mode 100644 Fuzzing/BinaryDecoder/Fuzz/TestcasesJson/readresponse.json create mode 100644 Fuzzing/BinaryDecoder/aflfuzz.sh rename Fuzzing/scripts/{fuzz.ps1 => fuzz-afl.ps1} (100%) diff --git a/Fuzzing/BinaryDecoder/Fuzz.Tools/BinaryDecoder.Fuzz.Tools.csproj b/Fuzzing/BinaryDecoder/Fuzz.Tools/BinaryDecoder.Fuzz.Tools.csproj index a35d6d151f..195d11656c 100644 --- a/Fuzzing/BinaryDecoder/Fuzz.Tools/BinaryDecoder.Fuzz.Tools.csproj +++ b/Fuzzing/BinaryDecoder/Fuzz.Tools/BinaryDecoder.Fuzz.Tools.csproj @@ -8,7 +8,7 @@ - + diff --git a/Fuzzing/BinaryDecoder/Fuzz/FuzzTarget.cs b/Fuzzing/BinaryDecoder/Fuzz/FuzzableCode.BinaryDecoder.cs similarity index 67% rename from Fuzzing/BinaryDecoder/Fuzz/FuzzTarget.cs rename to Fuzzing/BinaryDecoder/Fuzz/FuzzableCode.BinaryDecoder.cs index 46b4335525..7afea0de04 100644 --- a/Fuzzing/BinaryDecoder/Fuzz/FuzzTarget.cs +++ b/Fuzzing/BinaryDecoder/Fuzz/FuzzableCode.BinaryDecoder.cs @@ -1,24 +1,10 @@ - using System; using System.IO; using Opc.Ua; -using Opc.Ua.Bindings; -public static class FuzzableCode +public static partial class FuzzableCode { - const int SegmentSize = 0x40; - private static ServiceMessageContext messageContext = ServiceMessageContext.GlobalContext; - - /// - /// Print information about the fuzzer target. - /// - public static void FuzzInfo() - { - Console.WriteLine("OPC UA Core Encoder Fuzzer for afl-fuzz and libfuzzer."); - Console.WriteLine("Fuzzing targets for various aspects of the Binary, Json and Xml encoders."); - } - /// /// The binary decoder fuzz target for afl-fuzz. /// @@ -58,7 +44,7 @@ public static void AflfuzzBinaryEncoder(Stream stream) } /// - /// The fuzz target for libfuzzer. + /// The binary decoder fuzz target for libfuzzer. /// public static void LibfuzzBinaryDecoder(ReadOnlySpan input) { @@ -71,7 +57,6 @@ public static void LibfuzzBinaryDecoder(ReadOnlySpan input) /// /// The binary encoder fuzz target for afl-fuzz. /// - /// The stdin stream from the afl-fuzz process. public static void LibfuzzBinaryEncoder(ReadOnlySpan input) { IEncodeable encodeable = null; @@ -94,26 +79,6 @@ public static void LibfuzzBinaryEncoder(ReadOnlySpan input) } } - private static MemoryStream PrepareArraySegmentStream(Stream stream) - { - // afl-fuzz uses a non seekable stream, causing false positives - // use ArraySegmentStream in combination with fuzz target... - MemoryStream memoryStream; - using (var binaryStream = new BinaryReader(stream)) - { - var bufferCollection = new BufferCollection(); - byte[] buffer; - do - { - buffer = binaryStream.ReadBytes(SegmentSize); - bufferCollection.Add(buffer); - } while (buffer.Length == SegmentSize); - memoryStream = new ArraySegmentStream(bufferCollection); - } - - return memoryStream; - } - /// /// The fuzz target for the BinaryDecoder. /// diff --git a/Fuzzing/BinaryDecoder/Fuzz/FuzzableCode.JsonDecoder.cs b/Fuzzing/BinaryDecoder/Fuzz/FuzzableCode.JsonDecoder.cs new file mode 100644 index 0000000000..ca983572d1 --- /dev/null +++ b/Fuzzing/BinaryDecoder/Fuzz/FuzzableCode.JsonDecoder.cs @@ -0,0 +1,105 @@ + + +using System; +using System.Text; +using Opc.Ua; + +public static partial class FuzzableCode +{ + /// + /// The Json decoder fuzz target for afl-fuzz. + /// + public static void AflfuzzJsonDecoder(string input) + { + _ = FuzzJsonDecoderCore(input); + } + + /// + /// The Json encoder fuzz target for afl-fuzz. + /// + public static void AflfuzzBinaryEncoder(string input) + { + IEncodeable encodeable = null; + try + { + encodeable = FuzzJsonDecoderCore(input); + } + catch + { + return; + } + + // encode the fuzzed object and see if it crashes + if (encodeable != null) + { + using (var encoder = new JsonEncoder(messageContext, true)) + { + encoder.EncodeMessage(encodeable); + encoder.Close(); + } + } + } + + /// + /// The json decoder fuzz target for libfuzzer. + /// + public static void LibfuzzJsonDecoder(ReadOnlySpan input) + { + string json = Encoding.UTF8.GetString(input); + _ = FuzzJsonDecoderCore(json); + } + + /// + /// The binary encoder fuzz target for afl-fuzz. + /// + public static void LibfuzzJsonEncoder(ReadOnlySpan input) + { + IEncodeable encodeable = null; + string json = Encoding.UTF8.GetString(input); + try + { + encodeable = FuzzJsonDecoderCore(json); + } + catch + { + return; + } + + // encode the fuzzed object and see if it crashes + if (encodeable != null) + { + using (var encoder = new JsonEncoder(messageContext, true)) + { + encoder.EncodeMessage(encodeable); + encoder.Close(); + } + } + } + + /// + /// The fuzz target for the JsonDecoder. + /// + /// A string with fuzz content. + private static IEncodeable FuzzJsonDecoderCore(string json) + { + try + { + using (var decoder = new JsonDecoder(json, messageContext)) + { + return decoder.DecodeMessage(null); + } + } + catch (ServiceResultException sre) + { + switch (sre.StatusCode) + { + case StatusCodes.BadEncodingLimitsExceeded: + case StatusCodes.BadDecodingError: + return null; + } + + throw; + } + } +} + diff --git a/Fuzzing/BinaryDecoder/Fuzz/FuzzableCode.cs b/Fuzzing/BinaryDecoder/Fuzz/FuzzableCode.cs new file mode 100644 index 0000000000..160c8fa313 --- /dev/null +++ b/Fuzzing/BinaryDecoder/Fuzz/FuzzableCode.cs @@ -0,0 +1,46 @@ + + +using System; +using System.IO; +using Opc.Ua; +using Opc.Ua.Bindings; + +public static partial class FuzzableCode +{ + private static ServiceMessageContext messageContext = ServiceMessageContext.GlobalContext; + + /// + /// Print information about the fuzzer target. + /// + public static void FuzzInfo() + { + Console.WriteLine("OPC UA Core Encoder Fuzzer for afl-fuzz and libfuzzer."); + Console.WriteLine("Fuzzing targets for various aspects of the Binary, Json and Xml encoders."); + } + + /// + /// Prepare a seekable memory stream from the input stream. + /// + private static MemoryStream PrepareArraySegmentStream(Stream stream) + { + const int segmentSize = 0x40; + + // afl-fuzz uses a non seekable stream, causing false positives + // use ArraySegmentStream in combination with fuzz target... + MemoryStream memoryStream; + using (var binaryStream = new BinaryReader(stream)) + { + var bufferCollection = new BufferCollection(); + byte[] buffer; + do + { + buffer = binaryStream.ReadBytes(segmentSize); + bufferCollection.Add(buffer); + } while (buffer.Length == segmentSize); + memoryStream = new ArraySegmentStream(bufferCollection); + } + + return memoryStream; + } +} + diff --git a/Fuzzing/BinaryDecoder/Fuzz/Testcases/readrequest.bin b/Fuzzing/BinaryDecoder/Fuzz/TestcasesBinary/readrequest.bin similarity index 100% rename from Fuzzing/BinaryDecoder/Fuzz/Testcases/readrequest.bin rename to Fuzzing/BinaryDecoder/Fuzz/TestcasesBinary/readrequest.bin diff --git a/Fuzzing/BinaryDecoder/Fuzz/Testcases/readresponse.bin b/Fuzzing/BinaryDecoder/Fuzz/TestcasesBinary/readresponse.bin similarity index 100% rename from Fuzzing/BinaryDecoder/Fuzz/Testcases/readresponse.bin rename to Fuzzing/BinaryDecoder/Fuzz/TestcasesBinary/readresponse.bin diff --git a/Fuzzing/BinaryDecoder/Fuzz/TestcasesJson/readrequest.json b/Fuzzing/BinaryDecoder/Fuzz/TestcasesJson/readrequest.json new file mode 100644 index 0000000000..ba55d02acc --- /dev/null +++ b/Fuzzing/BinaryDecoder/Fuzz/TestcasesJson/readrequest.json @@ -0,0 +1 @@ +{"TypeId":{"Id":629},"Body":{"RequestHeader":{"Timestamp":"2024-04-21T04:30:30.6104202Z","RequestHandle":42,"ReturnDiagnostics":0,"TimeoutHint":0},"MaxAge":0,"TimestampsToReturn":0,"NodesToRead":[{"NodeId":{"Id":1000},"AttributeId":5},{"NodeId":{"Id":1000},"AttributeId":13},{"NodeId":{"Id":1000},"AttributeId":4},{"NodeId":{"Id":1000},"AttributeId":17},{"NodeId":{"Id":1000},"AttributeId":24}]}} \ No newline at end of file diff --git a/Fuzzing/BinaryDecoder/Fuzz/TestcasesJson/readresponse.json b/Fuzzing/BinaryDecoder/Fuzz/TestcasesJson/readresponse.json new file mode 100644 index 0000000000..80262fb5ba --- /dev/null +++ b/Fuzzing/BinaryDecoder/Fuzz/TestcasesJson/readresponse.json @@ -0,0 +1 @@ +{"TypeId":{"Id":632},"Body":{"ResponseHeader":{"Timestamp":"2024-04-21T04:30:30.6969268Z","RequestHandle":42,"ServiceDiagnostics":{"AdditionalInfo":"NodeId not found","InnerStatusCode":2153644032,"InnerDiagnosticInfo":{"AdditionalInfo":"Hello World","InnerStatusCode":2150891520,"InnerDiagnosticInfo":{"AdditionalInfo":"Hello World","InnerStatusCode":2150891520,"InnerDiagnosticInfo":{"AdditionalInfo":"Hello World","InnerStatusCode":2150891520}}}}},"Results":[{"Value":{"Type":12,"Body":"Hello World"},"SourceTimestamp":"2024-04-21T04:31:30.6908516Z","SourcePicoseconds":10,"ServerTimestamp":"2024-04-21T04:30:30.6908516Z","ServerPicoseconds":100},{"Value":{"Type":7,"Body":12345678},"StatusCode":2157772800,"SourceTimestamp":"2024-04-21T04:31:30.6908516Z","ServerTimestamp":"2024-04-21T04:30:30.6908516Z"},{"Value":{"Type":15,"Body":"AAECAwQFBg=="},"SourceTimestamp":"2024-04-21T04:31:30.6908516Z","ServerTimestamp":"2024-04-21T04:30:30.6908516Z"},{"Value":{"Type":3,"Body":42}}],"DiagnosticInfos":[{"AdditionalInfo":"Hello World","InnerStatusCode":2148925440,"InnerDiagnosticInfo":{"AdditionalInfo":"Hello World","InnerStatusCode":2150891520}}]}} \ No newline at end of file diff --git a/Fuzzing/BinaryDecoder/aflfuzz.sh b/Fuzzing/BinaryDecoder/aflfuzz.sh new file mode 100644 index 0000000000..c91abdaf74 --- /dev/null +++ b/Fuzzing/BinaryDecoder/aflfuzz.sh @@ -0,0 +1,58 @@ +#!/bin/bash + +# Function to display the menu +display_menu() { + echo "Select a OPA UA encoder fuzzing function:" + echo "1. Opc.Ua.BinaryDecoder" + echo "2. Opc.Ua.BinaryEncoder" + echo "3. Opc.Ua.JsonDecoder" + echo "4. Opc.Ua.JsonEncoder" + echo "5. Exit" +} + +# Function to execute PowerShell script based on user choice +execute_powershell_script() { + case $1 in + 1) + echo "Running afl-fuzz with Opc.Ua.BinaryDecoder" + pwsh ../scripts/fuzz-afl.ps1 ./Fuzz/BinaryDecoder.Fuzz.csproj -i ./Fuzz/TestcasesBinary -fuzztarget AflfuzzBinaryDecoder + ;; + 2) + echo "Running afl-fuzz with Opc.Ua.BinaryEncoder" + pwsh ../scripts/fuzz-afl.ps1 ./Fuzz/BinaryDecoder.Fuzz.csproj -i ./Fuzz/TestcasesBinary -fuzztarget AflfuzzBinaryEncoder + ;; + 3) + echo "Running afl-fuzz with Opc.Ua.JsonDecoder" + pwsh ../scripts/fuzz-afl.ps1 ./Fuzz/BinaryDecoder.Fuzz.csproj -i ./Fuzz/TestcasesJson -x ../dictionaries/json.dict -fuzztarget AflfuzzJsonDecoder + ;; + 4) + echo "Running afl-fuzz with Opc.Ua.JsonEncoder" + pwsh ../scripts/fuzz-afl.ps1 ./Fuzz/BinaryDecoder.Fuzz.csproj -i ./Fuzz/TestcasesJson -x ../dictionaries/json.dict -fuzztarget AflfuzzJsonEncoder + ;; + *) + echo "Invalid option. Exiting." + ;; + esac +} + +# Main script +while true; do + display_menu + + read -p "Enter your choice (1-5): " choice + + case $choice in + 1|2|3|4) + execute_powershell_script $choice + ;; + 5) + echo "Exiting." + break + ;; + *) + echo "Invalid input. Please enter a number between 1 and 5." + ;; + esac + + echo +done diff --git a/Fuzzing/scripts/fuzz.ps1 b/Fuzzing/scripts/fuzz-afl.ps1 similarity index 100% rename from Fuzzing/scripts/fuzz.ps1 rename to Fuzzing/scripts/fuzz-afl.ps1 diff --git a/Stack/Opc.Ua.Core/Types/Encoders/JsonDecoder.cs b/Stack/Opc.Ua.Core/Types/Encoders/JsonDecoder.cs index fa0a6b0bdf..5f2860af2a 100644 --- a/Stack/Opc.Ua.Core/Types/Encoders/JsonDecoder.cs +++ b/Stack/Opc.Ua.Core/Types/Encoders/JsonDecoder.cs @@ -736,8 +736,15 @@ public DateTime ReadDateTime(string fieldName) if (token is string text) { - var result = XmlConvert.ToDateTime(text, XmlDateTimeSerializationMode.Utc); - return result >= m_dateTimeMaxJsonValue ? DateTime.MaxValue : result; + try + { + var result = XmlConvert.ToDateTime(text, XmlDateTimeSerializationMode.Utc); + return result >= m_dateTimeMaxJsonValue ? DateTime.MaxValue : result; + } + catch (FormatException fe) + { + throw ServiceResultException.Create(StatusCodes.BadDecodingError, "Failed to decode DateTime: {0}", fe.Message); + } } return DateTime.MinValue; @@ -755,13 +762,19 @@ public Uuid ReadGuid(string fieldName) return Uuid.Empty; } - if (!(token is string value)) { return Uuid.Empty; } - return new Uuid(value); + try + { + return new Uuid(value); + } + catch (FormatException fe) + { + throw ServiceResultException.Create(StatusCodes.BadDecodingError, "Failed to create Guid: {0}", fe.Message); + } } /// @@ -786,7 +799,7 @@ public byte[] ReadByteString(string fieldName) return Array.Empty(); } - var bytes = Convert.FromBase64String(value); + var bytes = SafeConvertFromBase64String(value); if (m_context.MaxByteStringLength > 0 && m_context.MaxByteStringLength < bytes.Length) { @@ -814,19 +827,26 @@ public XmlElement ReadXmlElement(string fieldName) return null; } - var bytes = Convert.FromBase64String(value); + var bytes = SafeConvertFromBase64String(value); if (bytes != null && bytes.Length > 0) { - XmlDocument document = new XmlDocument(); - string xmlString = Encoding.UTF8.GetString(bytes, 0, bytes.Length); + try + { + XmlDocument document = new XmlDocument(); + string xmlString = Encoding.UTF8.GetString(bytes, 0, bytes.Length); - using (XmlReader reader = XmlReader.Create(new StringReader(xmlString), Utils.DefaultXmlReaderSettings())) + using (XmlReader reader = XmlReader.Create(new StringReader(xmlString), Utils.DefaultXmlReaderSettings())) + { + document.Load(reader); + } + + return document.DocumentElement; + } + catch (XmlException xe) { - document.Load(reader); + throw ServiceResultException.Create(StatusCodes.BadDecodingError, "Unable to decode Xml: {0}", xe.Message); } - - return document.DocumentElement; } return null; @@ -1496,7 +1516,7 @@ public ByteCollection ReadByteArray(string fieldName) string value = ReadString(fieldName); if (value != null) { - return Convert.FromBase64String(value); + return SafeConvertFromBase64String(value); } if (!ReadArrayField(fieldName, out token)) @@ -2819,61 +2839,67 @@ private Dictionary ReadObject() { Dictionary fields = new Dictionary(); - while (m_reader.Read() && m_reader.TokenType != JsonToken.EndObject) + try { - if (m_reader.TokenType == JsonToken.StartArray) - { - fields[RootArrayName] = ReadArray(); - } - else if (m_reader.TokenType == JsonToken.PropertyName) + while (m_reader.Read() && m_reader.TokenType != JsonToken.EndObject) { - string name = (string)m_reader.Value; - - if (m_reader.Read() && m_reader.TokenType != JsonToken.EndObject) + if (m_reader.TokenType == JsonToken.StartArray) { - switch (m_reader.TokenType) - { - case JsonToken.Comment: - { - break; - } - - case JsonToken.Null: - { - fields[name] = JTokenNullObject.Object; - break; - } - - case JsonToken.Date: - case JsonToken.Bytes: - case JsonToken.Boolean: - case JsonToken.Integer: - case JsonToken.Float: - case JsonToken.String: - { - fields[name] = m_reader.Value; - break; - } - - case JsonToken.StartArray: - { - fields[name] = ReadArray(); - break; - } + fields[RootArrayName] = ReadArray(); + } + else if (m_reader.TokenType == JsonToken.PropertyName) + { + string name = (string)m_reader.Value; - case JsonToken.StartObject: + if (m_reader.Read() && m_reader.TokenType != JsonToken.EndObject) + { + switch (m_reader.TokenType) { - fields[name] = ReadObject(); - break; + case JsonToken.Comment: + { + break; + } + + case JsonToken.Null: + { + fields[name] = JTokenNullObject.Object; + break; + } + + case JsonToken.Date: + case JsonToken.Bytes: + case JsonToken.Boolean: + case JsonToken.Integer: + case JsonToken.Float: + case JsonToken.String: + { + fields[name] = m_reader.Value; + break; + } + + case JsonToken.StartArray: + { + fields[name] = ReadArray(); + break; + } + + case JsonToken.StartObject: + { + fields[name] = ReadObject(); + break; + } + + default: + break; } - - default: - break; } } } } - + catch (JsonReaderException jre) + { + throw ServiceResultException.Create(StatusCodes.BadDecodingError, "Error reading JSON object: {0}", jre.Message); + } return fields; } @@ -3034,6 +3060,21 @@ private bool ReadArrayField(string fieldName, out List array) return true; } + /// + /// Safe Convert function which throws a BadDecodingError if unsuccessful. + /// + private byte[] SafeConvertFromBase64String(string s) + { + try + { + return Convert.FromBase64String(s); + } + catch (FormatException fe) + { + throw ServiceResultException.Create(StatusCodes.BadDecodingError, "Error decoding base64 string: {0}", fe.Message); + } + } + /// /// Test and increment the nesting level. /// From 96c760a6fb0496106066ec55ced2284e02e6a86b Mon Sep 17 00:00:00 2001 From: Martin Regen Date: Sun, 28 Apr 2024 06:27:12 +0200 Subject: [PATCH 21/83] fix afl fuzz scripts --- Fuzzing/BinaryDecoder/aflfuzz.sh | 38 +++++++++---------- .../BinaryDecoder/aflfuzz_binarydecoder.sh | 1 - .../BinaryDecoder/aflfuzz_binaryencoder.sh | 1 - Fuzzing/Fuzzing.md | 19 +++++++--- Fuzzing/scripts/fuzz-afl.ps1 | 4 +- 5 files changed, 33 insertions(+), 30 deletions(-) delete mode 100644 Fuzzing/BinaryDecoder/aflfuzz_binarydecoder.sh delete mode 100644 Fuzzing/BinaryDecoder/aflfuzz_binaryencoder.sh diff --git a/Fuzzing/BinaryDecoder/aflfuzz.sh b/Fuzzing/BinaryDecoder/aflfuzz.sh index c91abdaf74..f02e0f9764 100644 --- a/Fuzzing/BinaryDecoder/aflfuzz.sh +++ b/Fuzzing/BinaryDecoder/aflfuzz.sh @@ -2,7 +2,7 @@ # Function to display the menu display_menu() { - echo "Select a OPA UA encoder fuzzing function:" + echo "Select an OPA UA encoder fuzzing function:" echo "1. Opc.Ua.BinaryDecoder" echo "2. Opc.Ua.BinaryEncoder" echo "3. Opc.Ua.JsonDecoder" @@ -10,7 +10,7 @@ display_menu() { echo "5. Exit" } -# Function to execute PowerShell script based on user choice +# Function to execute fuzz-afl PowerShell script based on user choice execute_powershell_script() { case $1 in 1) @@ -35,24 +35,22 @@ execute_powershell_script() { esac } -# Main script -while true; do - display_menu +# Main +display_menu - read -p "Enter your choice (1-5): " choice +read -p "Enter your choice (1-5): " choice - case $choice in - 1|2|3|4) - execute_powershell_script $choice - ;; - 5) - echo "Exiting." - break - ;; - *) - echo "Invalid input. Please enter a number between 1 and 5." - ;; - esac +case $choice in + 1|2|3|4) + execute_powershell_script $choice + ;; + 5) + echo "Exiting." + break + ;; + *) + echo "Invalid input. Please enter a number between 1 and 5." + ;; +esac - echo -done +echo diff --git a/Fuzzing/BinaryDecoder/aflfuzz_binarydecoder.sh b/Fuzzing/BinaryDecoder/aflfuzz_binarydecoder.sh deleted file mode 100644 index cf536b1d19..0000000000 --- a/Fuzzing/BinaryDecoder/aflfuzz_binarydecoder.sh +++ /dev/null @@ -1 +0,0 @@ -pwsh ../scripts/fuzz.ps1 ./Fuzz/BinaryDecoder.Fuzz.csproj -i ./Fuzz/Testcases -fuzztarget AflfuzzBinaryDecoder \ No newline at end of file diff --git a/Fuzzing/BinaryDecoder/aflfuzz_binaryencoder.sh b/Fuzzing/BinaryDecoder/aflfuzz_binaryencoder.sh deleted file mode 100644 index cf536b1d19..0000000000 --- a/Fuzzing/BinaryDecoder/aflfuzz_binaryencoder.sh +++ /dev/null @@ -1 +0,0 @@ -pwsh ../scripts/fuzz.ps1 ./Fuzz/BinaryDecoder.Fuzz.csproj -i ./Fuzz/Testcases -fuzztarget AflfuzzBinaryDecoder \ No newline at end of file diff --git a/Fuzzing/Fuzzing.md b/Fuzzing/Fuzzing.md index 578558943c..246fe3736c 100644 --- a/Fuzzing/Fuzzing.md +++ b/Fuzzing/Fuzzing.md @@ -4,7 +4,7 @@ This project provides integration of Sharpfuzz with the UA.NET Standard library Fuzzers for the following decoders are located in the `Fuzzing` directory: - BinaryDecoder -- JsonDecoder (planned) +- JsonDecoder - XmlDecoder (planned) - CRL and Certificate decoder functions using the .NET ASN.1 decoder (planned) @@ -22,7 +22,7 @@ Both fuzzers are supported on Linux. afl-fuzz can be compiled on any Linux syste ### Installation of required tools -The full instructions for setting up sharpfuzz can be found at https://github.com/Metalnem/sharpfuzz/blob/master/README.md. +The full instructions for setting up sharpfuzz can be found at this [README](https://github.com/Metalnem/sharpfuzz/blob/master/README.md). The following steps are required to set up the environment: - Open a terminal and run the following commands to install the required packages to compile afl-fuzz: @@ -40,7 +40,7 @@ sudo apt-get install -y dotnet-sdk-8.0 ``` The supplied scripts require powershell on Linux to be installed. -See https://learn.microsoft.com/en-us/powershell/scripting/install/install-ubuntu?view=powershell-7.4 +See [Powershell install on Linux](https://learn.microsoft.com/en-us/powershell/scripting/install/install-ubuntu?view=powershell-7.4). To compile and install afl-fuzz and to install sharpfuzz, run the following commands: @@ -71,7 +71,9 @@ afl-fuzz --help sharpfuzz ``` -## Installation for libfuzzer on Windows +## Libfuzzer + +### Installation for libfuzzer on Windows Install the latest dotnet SDK and runtime from https://dotnet.microsoft.com/download/dotnet/ @@ -81,12 +83,17 @@ dotnet tool install --global SharpFuzz.CommandLine ``` ## Usage of afl-fuzz on Linux +## Afl-fuzz + +### Usage of afl-fuzz on Linux -To run the afl-fuzz fuzzing project, execute the following commands, e.g. for the BinaryDecoder fuzzer: +To run the afl-fuzz fuzzing project, execute the following commands: ```bash cd BinaryDecoder -./fuzz.sh +./aflfuzz.sh ``` +A menu will show up and allow the selection of a fuzzer target function to execute. + Now the fuzzer is started and will run until it is stopped manually by hitting Ctrl-C. The fuzzer will create a directory `findings` in the fuzzer directory, which contains the test cases that caused the fuzzer to crash. \ No newline at end of file diff --git a/Fuzzing/scripts/fuzz-afl.ps1 b/Fuzzing/scripts/fuzz-afl.ps1 index 006342cd36..85059c511c 100644 --- a/Fuzzing/scripts/fuzz-afl.ps1 +++ b/Fuzzing/scripts/fuzz-afl.ps1 @@ -58,10 +58,10 @@ foreach ($fuzzingTarget in $fuzzingTargets) { $env:AFL_SKIP_BIN_CHECK = 1 if ($x) { - Write-Output afl-fuzz -i $i -o $findingsDir -t $t -m none -x $x dotnet $project $fuzztarget + Write-Output "afl-fuzz -i $i -o $findingsDir -t $t -m none -x $x dotnet $project $fuzztarget" afl-fuzz -i $i -o $findingsDir -t $t -m none -x $x dotnet $project $fuzztarget } else { - Write-Output afl-fuzz -i $i -o $findingsDir -t $t -m none dotnet $project $fuzztarget + Write-Output "afl-fuzz -i $i -o $findingsDir -t $t -m none dotnet $project $fuzztarget" afl-fuzz -i $i -o $findingsDir -t $t -m none dotnet $project $fuzztarget } From 55a6337c74aed7bbe5763c924c003ebf44302a4f Mon Sep 17 00:00:00 2001 From: Martin Regen Date: Sun, 28 Apr 2024 07:13:32 +0200 Subject: [PATCH 22/83] renames --- .../Fuzz.Tools/BinaryDecoder.Testcases.cs | 18 ------------------ Fuzzing/BinaryDecoder/libfuzz.sh | 1 - .../BinaryDecoder/libfuzz_binarydecoder.bat | 1 - .../BinaryDecoder/libfuzz_binaryencoder.bat | 1 - .../Fuzz.Tools/Encoders.Fuzz.Tools.csproj} | 4 ++-- .../Fuzz/Encoders.Fuzz.csproj} | 4 ++-- .../Fuzz/FuzzableCode.BinaryDecoder.cs | 3 +++ .../Fuzz/FuzzableCode.JsonDecoder.cs | 3 +++ .../Fuzz/FuzzableCode.cs | 0 .../Fuzz/Testcases.Binary}/readrequest.bin | Bin .../Fuzz/Testcases.Binary}/readresponse.bin | Bin .../Fuzz/Testcases.Json}/readrequest.json | 0 .../Fuzz/Testcases.Json}/readresponse.json | 0 .../{BinaryDecoder => Encoders}/aflfuzz.sh | 10 +++++----- Fuzzing/Fuzzing.md | 2 +- 15 files changed, 16 insertions(+), 31 deletions(-) delete mode 100644 Fuzzing/BinaryDecoder/Fuzz.Tools/BinaryDecoder.Testcases.cs delete mode 100644 Fuzzing/BinaryDecoder/libfuzz.sh delete mode 100644 Fuzzing/BinaryDecoder/libfuzz_binarydecoder.bat delete mode 100644 Fuzzing/BinaryDecoder/libfuzz_binaryencoder.bat rename Fuzzing/{BinaryDecoder/Fuzz.Tools/BinaryDecoder.Fuzz.Tools.csproj => Encoders/Fuzz.Tools/Encoders.Fuzz.Tools.csproj} (92%) rename Fuzzing/{BinaryDecoder/Fuzz/BinaryDecoder.Fuzz.csproj => Encoders/Fuzz/Encoders.Fuzz.csproj} (87%) rename Fuzzing/{BinaryDecoder => Encoders}/Fuzz/FuzzableCode.BinaryDecoder.cs (97%) rename Fuzzing/{BinaryDecoder => Encoders}/Fuzz/FuzzableCode.JsonDecoder.cs (96%) rename Fuzzing/{BinaryDecoder => Encoders}/Fuzz/FuzzableCode.cs (100%) rename Fuzzing/{BinaryDecoder/Fuzz/TestcasesBinary => Encoders/Fuzz/Testcases.Binary}/readrequest.bin (100%) rename Fuzzing/{BinaryDecoder/Fuzz/TestcasesBinary => Encoders/Fuzz/Testcases.Binary}/readresponse.bin (100%) rename Fuzzing/{BinaryDecoder/Fuzz/TestcasesJson => Encoders/Fuzz/Testcases.Json}/readrequest.json (100%) rename Fuzzing/{BinaryDecoder/Fuzz/TestcasesJson => Encoders/Fuzz/Testcases.Json}/readresponse.json (100%) rename Fuzzing/{BinaryDecoder => Encoders}/aflfuzz.sh (62%) diff --git a/Fuzzing/BinaryDecoder/Fuzz.Tools/BinaryDecoder.Testcases.cs b/Fuzzing/BinaryDecoder/Fuzz.Tools/BinaryDecoder.Testcases.cs deleted file mode 100644 index e9e811c364..0000000000 --- a/Fuzzing/BinaryDecoder/Fuzz.Tools/BinaryDecoder.Testcases.cs +++ /dev/null @@ -1,18 +0,0 @@ - -using System.IO; -using Opc.Ua; - -public static partial class Testcases -{ - public static void Run(string directoryPath) - { - foreach (var messageEncoder in Testcases.MessageEncoders) - { - var encoder = new BinaryEncoder(Testcases.MessageContext); - messageEncoder(encoder); - var message = encoder.CloseAndReturnBuffer(); - FuzzTestcase(message); - File.WriteAllBytes(Path.Combine(directoryPath, $"{messageEncoder.Method.Name}.bin".ToLowerInvariant()), message); - } - } -} diff --git a/Fuzzing/BinaryDecoder/libfuzz.sh b/Fuzzing/BinaryDecoder/libfuzz.sh deleted file mode 100644 index d9b1c64674..0000000000 --- a/Fuzzing/BinaryDecoder/libfuzz.sh +++ /dev/null @@ -1 +0,0 @@ -pwsh ../scripts/fuzz-libfuzzer.ps1 -libFuzzer "./libfuzzer-dotnet-ubuntu" -project ./Fuzz/BinaryDecoder.Fuzz.csproj -corpus ./Fuzz/Testcases/ \ No newline at end of file diff --git a/Fuzzing/BinaryDecoder/libfuzz_binarydecoder.bat b/Fuzzing/BinaryDecoder/libfuzz_binarydecoder.bat deleted file mode 100644 index d5469ddf81..0000000000 --- a/Fuzzing/BinaryDecoder/libfuzz_binarydecoder.bat +++ /dev/null @@ -1 +0,0 @@ -Powershell -File ..\scripts\fuzz-libfuzzer.ps1 -libFuzzer ".\libfuzzer-dotnet-windows.exe" -project .\Fuzz\BinaryDecoder.Fuzz.csproj -fuzztarget LibfuzzBinaryDecoder -corpus .\Fuzz\Testcases \ No newline at end of file diff --git a/Fuzzing/BinaryDecoder/libfuzz_binaryencoder.bat b/Fuzzing/BinaryDecoder/libfuzz_binaryencoder.bat deleted file mode 100644 index ca815b6d25..0000000000 --- a/Fuzzing/BinaryDecoder/libfuzz_binaryencoder.bat +++ /dev/null @@ -1 +0,0 @@ -Powershell -File ..\scripts\fuzz-libfuzzer.ps1 -libFuzzer ".\libfuzzer-dotnet-windows.exe" -project .\Fuzz\BinaryDecoder.Fuzz.csproj -fuzztarget LibfuzzBinaryEncoder -corpus .\Fuzz\Testcases \ No newline at end of file diff --git a/Fuzzing/BinaryDecoder/Fuzz.Tools/BinaryDecoder.Fuzz.Tools.csproj b/Fuzzing/Encoders/Fuzz.Tools/Encoders.Fuzz.Tools.csproj similarity index 92% rename from Fuzzing/BinaryDecoder/Fuzz.Tools/BinaryDecoder.Fuzz.Tools.csproj rename to Fuzzing/Encoders/Fuzz.Tools/Encoders.Fuzz.Tools.csproj index 195d11656c..95a9cef067 100644 --- a/Fuzzing/BinaryDecoder/Fuzz.Tools/BinaryDecoder.Fuzz.Tools.csproj +++ b/Fuzzing/Encoders/Fuzz.Tools/Encoders.Fuzz.Tools.csproj @@ -3,8 +3,8 @@ Exe $(AppTargetFramework) - BinaryDecoder.Fuzz.Tools - BinaryDecoder.Fuzz.Tools + Encoders.Fuzz.Tools + Encoders.Fuzz.Tools diff --git a/Fuzzing/BinaryDecoder/Fuzz/BinaryDecoder.Fuzz.csproj b/Fuzzing/Encoders/Fuzz/Encoders.Fuzz.csproj similarity index 87% rename from Fuzzing/BinaryDecoder/Fuzz/BinaryDecoder.Fuzz.csproj rename to Fuzzing/Encoders/Fuzz/Encoders.Fuzz.csproj index e616f01300..c549c5878c 100644 --- a/Fuzzing/BinaryDecoder/Fuzz/BinaryDecoder.Fuzz.csproj +++ b/Fuzzing/Encoders/Fuzz/Encoders.Fuzz.csproj @@ -3,8 +3,8 @@ Exe $(AppTargetFramework) - BinaryDecoder.Fuzz - BinaryDecoder.Fuzz + Encoders.Fuzz + Encoders.Fuzz diff --git a/Fuzzing/BinaryDecoder/Fuzz/FuzzableCode.BinaryDecoder.cs b/Fuzzing/Encoders/Fuzz/FuzzableCode.BinaryDecoder.cs similarity index 97% rename from Fuzzing/BinaryDecoder/Fuzz/FuzzableCode.BinaryDecoder.cs rename to Fuzzing/Encoders/Fuzz/FuzzableCode.BinaryDecoder.cs index 7afea0de04..4a9523ab16 100644 --- a/Fuzzing/BinaryDecoder/Fuzz/FuzzableCode.BinaryDecoder.cs +++ b/Fuzzing/Encoders/Fuzz/FuzzableCode.BinaryDecoder.cs @@ -3,6 +3,9 @@ using System.IO; using Opc.Ua; +/// +/// Fuzzing code for the binary decoder and encoder. +/// public static partial class FuzzableCode { /// diff --git a/Fuzzing/BinaryDecoder/Fuzz/FuzzableCode.JsonDecoder.cs b/Fuzzing/Encoders/Fuzz/FuzzableCode.JsonDecoder.cs similarity index 96% rename from Fuzzing/BinaryDecoder/Fuzz/FuzzableCode.JsonDecoder.cs rename to Fuzzing/Encoders/Fuzz/FuzzableCode.JsonDecoder.cs index ca983572d1..7db79ef0da 100644 --- a/Fuzzing/BinaryDecoder/Fuzz/FuzzableCode.JsonDecoder.cs +++ b/Fuzzing/Encoders/Fuzz/FuzzableCode.JsonDecoder.cs @@ -4,6 +4,9 @@ using System.Text; using Opc.Ua; +/// +/// Fuzzing code for the JSON decoder and encoder. +/// public static partial class FuzzableCode { /// diff --git a/Fuzzing/BinaryDecoder/Fuzz/FuzzableCode.cs b/Fuzzing/Encoders/Fuzz/FuzzableCode.cs similarity index 100% rename from Fuzzing/BinaryDecoder/Fuzz/FuzzableCode.cs rename to Fuzzing/Encoders/Fuzz/FuzzableCode.cs diff --git a/Fuzzing/BinaryDecoder/Fuzz/TestcasesBinary/readrequest.bin b/Fuzzing/Encoders/Fuzz/Testcases.Binary/readrequest.bin similarity index 100% rename from Fuzzing/BinaryDecoder/Fuzz/TestcasesBinary/readrequest.bin rename to Fuzzing/Encoders/Fuzz/Testcases.Binary/readrequest.bin diff --git a/Fuzzing/BinaryDecoder/Fuzz/TestcasesBinary/readresponse.bin b/Fuzzing/Encoders/Fuzz/Testcases.Binary/readresponse.bin similarity index 100% rename from Fuzzing/BinaryDecoder/Fuzz/TestcasesBinary/readresponse.bin rename to Fuzzing/Encoders/Fuzz/Testcases.Binary/readresponse.bin diff --git a/Fuzzing/BinaryDecoder/Fuzz/TestcasesJson/readrequest.json b/Fuzzing/Encoders/Fuzz/Testcases.Json/readrequest.json similarity index 100% rename from Fuzzing/BinaryDecoder/Fuzz/TestcasesJson/readrequest.json rename to Fuzzing/Encoders/Fuzz/Testcases.Json/readrequest.json diff --git a/Fuzzing/BinaryDecoder/Fuzz/TestcasesJson/readresponse.json b/Fuzzing/Encoders/Fuzz/Testcases.Json/readresponse.json similarity index 100% rename from Fuzzing/BinaryDecoder/Fuzz/TestcasesJson/readresponse.json rename to Fuzzing/Encoders/Fuzz/Testcases.Json/readresponse.json diff --git a/Fuzzing/BinaryDecoder/aflfuzz.sh b/Fuzzing/Encoders/aflfuzz.sh similarity index 62% rename from Fuzzing/BinaryDecoder/aflfuzz.sh rename to Fuzzing/Encoders/aflfuzz.sh index f02e0f9764..ec2b93a9f3 100644 --- a/Fuzzing/BinaryDecoder/aflfuzz.sh +++ b/Fuzzing/Encoders/aflfuzz.sh @@ -2,7 +2,7 @@ # Function to display the menu display_menu() { - echo "Select an OPA UA encoder fuzzing function:" + echo "Select a OPA UA Encoder fuzzing function:" echo "1. Opc.Ua.BinaryDecoder" echo "2. Opc.Ua.BinaryEncoder" echo "3. Opc.Ua.JsonDecoder" @@ -15,19 +15,19 @@ execute_powershell_script() { case $1 in 1) echo "Running afl-fuzz with Opc.Ua.BinaryDecoder" - pwsh ../scripts/fuzz-afl.ps1 ./Fuzz/BinaryDecoder.Fuzz.csproj -i ./Fuzz/TestcasesBinary -fuzztarget AflfuzzBinaryDecoder + pwsh ../scripts/fuzz-afl.ps1 ./Fuzz/Encoders.Fuzz.csproj -i ./Fuzz/Testcases.Binary -fuzztarget AflfuzzBinaryDecoder ;; 2) echo "Running afl-fuzz with Opc.Ua.BinaryEncoder" - pwsh ../scripts/fuzz-afl.ps1 ./Fuzz/BinaryDecoder.Fuzz.csproj -i ./Fuzz/TestcasesBinary -fuzztarget AflfuzzBinaryEncoder + pwsh ../scripts/fuzz-afl.ps1 ./Fuzz/Encoders.Fuzz.csproj -i ./Fuzz/Testcases.Binary -fuzztarget AflfuzzBinaryEncoder ;; 3) echo "Running afl-fuzz with Opc.Ua.JsonDecoder" - pwsh ../scripts/fuzz-afl.ps1 ./Fuzz/BinaryDecoder.Fuzz.csproj -i ./Fuzz/TestcasesJson -x ../dictionaries/json.dict -fuzztarget AflfuzzJsonDecoder + pwsh ../scripts/fuzz-afl.ps1 ./Fuzz/Encoders.Fuzz.csproj -i ./Fuzz/Testcases.Json -x ../dictionaries/json.dict -fuzztarget AflfuzzJsonDecoder ;; 4) echo "Running afl-fuzz with Opc.Ua.JsonEncoder" - pwsh ../scripts/fuzz-afl.ps1 ./Fuzz/BinaryDecoder.Fuzz.csproj -i ./Fuzz/TestcasesJson -x ../dictionaries/json.dict -fuzztarget AflfuzzJsonEncoder + pwsh ../scripts/fuzz-afl.ps1 ./Fuzz/Encoders.Fuzz.csproj -i ./Fuzz/Testcases.Json -x ../dictionaries/json.dict -fuzztarget AflfuzzJsonEncoder ;; *) echo "Invalid option. Exiting." diff --git a/Fuzzing/Fuzzing.md b/Fuzzing/Fuzzing.md index 246fe3736c..7596c13d9e 100644 --- a/Fuzzing/Fuzzing.md +++ b/Fuzzing/Fuzzing.md @@ -10,7 +10,7 @@ Fuzzers for the following decoders are located in the `Fuzzing` directory: Most of the supporting code is shared between all projects, only the project names and the fuzzable support functions differ. -A Tools application for each fuzzer supports recreation of the `Testcases` and to replay the test cases that caused the fuzzer to crash or to hang. The application is located in the `*.Fuzz.Tools` folders. +A Tools application supports recreation of the `Testcases` and to replay the test cases that caused the fuzzer to crash or to hang. The application is located in the `*.Fuzz.Tools` folders. ## Installation for afl-fuzz and libfuzzer on Linux From ca513ec3e3553375d391ca34466da5e9608bfc69 Mon Sep 17 00:00:00 2001 From: Martin Regen Date: Sun, 28 Apr 2024 08:36:30 +0200 Subject: [PATCH 23/83] libfuzz menu --- Fuzzing/Encoders/libfuzz.bat | 32 ++++++++++++++ Fuzzing/Encoders/libfuzz.sh | 56 ++++++++++++++++++++++++ Fuzzing/Fuzzing.md | 31 ++++++++++++-- Fuzzing/JsonDecoder/libfuzz.sh | 1 + Fuzzing/scripts/fuzz-libfuzzer.ps1 | 14 +++--- UA Fuzzing.sln | 69 ++++++++++++++++-------------- 6 files changed, 163 insertions(+), 40 deletions(-) create mode 100644 Fuzzing/Encoders/libfuzz.bat create mode 100644 Fuzzing/Encoders/libfuzz.sh create mode 100644 Fuzzing/JsonDecoder/libfuzz.sh diff --git a/Fuzzing/Encoders/libfuzz.bat b/Fuzzing/Encoders/libfuzz.bat new file mode 100644 index 0000000000..2b380fbb6d --- /dev/null +++ b/Fuzzing/Encoders/libfuzz.bat @@ -0,0 +1,32 @@ +@echo off +:menu +cls +echo "Select a OPA UA Encoder fuzzing function:" +echo "1. Opc.Ua.BinaryDecoder" +echo "2. Opc.Ua.BinaryEncoder" +echo "3. Opc.Ua.JsonDecoder" +echo "4. Opc.Ua.JsonEncoder" +echo "5. Exit" + +set /p choice="Enter your choice (1-5): " + +if "%choice%"=="1" ( + echo "Running libfuzzer with Opc.Ua.BinaryDecoder" + powershell.exe -File ../scripts/fuzz-libfuzzer.ps1 -libFuzzer "./libfuzzer-dotnet-windows.exe" -project ./Fuzz/Encoders.Fuzz.csproj -fuzztarget LibfuzzBinaryDecoder -corpus ./Fuzz/Testcases.Binary/ +) else if "%choice%"=="2" ( + echo "Running libfuzzer with Opc.Ua.BinaryEncoder" + powershell.exe -File ../scripts/fuzz-libfuzzer.ps1 -libFuzzer "./libfuzzer-dotnet-windows.exe" -project ./Fuzz/Encoders.Fuzz.csproj -fuzztarget LibfuzzBinaryEncoder -corpus ./Fuzz/Testcases.Binary/ +) else if "%choice%"=="3" ( + echo "Running libfuzzer with Opc.Ua.JsonDecoder" + powershell.exe -File ../scripts/fuzz-libfuzzer.ps1 -libFuzzer "./libfuzzer-dotnet-windows.exe" -project ./Fuzz/Encoders.Fuzz.csproj -fuzztarget LibfuzzJsonDecoder -dict ../dictionaries/json.dict -corpus ./Fuzz/Testcases.Json/ +) else if "%choice%"=="4" ( + echo "Running libfuzzer with Opc.Ua.JsonEncoder" + powershell.exe -File ../scripts/fuzz-libfuzzer.ps1 -libFuzzer "./libfuzzer-dotnet-windows.exe" -project ./Fuzz/Encoders.Fuzz.csproj -fuzztarget LibfuzzJsonEncoder -dict ../dictionaries/json.dict -corpus ./Fuzz/Testcases.Json/ +) else if "%choice%"=="5" ( + echo Exiting. + exit /b +) else ( + echo Invalid input. Please enter a number between 1 and 5. +) + +echo Done \ No newline at end of file diff --git a/Fuzzing/Encoders/libfuzz.sh b/Fuzzing/Encoders/libfuzz.sh new file mode 100644 index 0000000000..8205e22642 --- /dev/null +++ b/Fuzzing/Encoders/libfuzz.sh @@ -0,0 +1,56 @@ +#!/bin/bash + +# Function to display the menu +display_menu() { + echo "Select a OPA UA Encoder fuzzing function:" + echo "1. Opc.Ua.BinaryDecoder" + echo "2. Opc.Ua.BinaryEncoder" + echo "3. Opc.Ua.JsonDecoder" + echo "4. Opc.Ua.JsonEncoder" + echo "5. Exit" +} + +# Function to execute fuzz-afl PowerShell script based on user choice +execute_powershell_script() { + case $1 in + 1) + echo "Running libfuzzer with Opc.Ua.BinaryDecoder" + pwsh ../scripts/fuzz-libfuzzer.ps1 -libFuzzer "./libfuzzer-dotnet-ubuntu" -project ./Fuzz/Encoders.Fuzz.csproj -fuzztarget LibfuzzBinaryDecoder -corpus ./Fuzz/Testcases.Binary/ + ;; + 2) + echo "Running libfuzzer with Opc.Ua.BinaryEncoder" + pwsh ../scripts/fuzz-libfuzzer.ps1 -libFuzzer "./libfuzzer-dotnet-ubuntu" -project ./Fuzz/Encoders.Fuzz.csproj -fuzztarget LibfuzzBinaryEncoder -corpus ./Fuzz/Testcases.Binary/ + ;; + 3) + echo "Running libfuzzer with Opc.Ua.JsonDecoder" + pwsh ../scripts/fuzz-libfuzzer.ps1 -libFuzzer "./libfuzzer-dotnet-ubuntu" -project ./Fuzz/Encoders.Fuzz.csproj -fuzztarget LibfuzzJsonDecoder -dict ../dictionaries/json.dict -corpus ./Fuzz/Testcases.Json/ + ;; + 4) + echo "Running libfuzzer with Opc.Ua.JsonEncoder" + pwsh ../scripts/fuzz-libfuzzer.ps1 -libFuzzer "./libfuzzer-dotnet-ubuntu" -project ./Fuzz/Encoders.Fuzz.csproj -fuzztarget LibfuzzJsonEncoder -dict ../dictionaries/json.dict -corpus ./Fuzz/Testcases.Json/ + ;; + *) + echo "Invalid option. Exiting." + ;; + esac +} + +# Main +display_menu + +read -p "Enter your choice (1-5): " choice + +case $choice in + 1|2|3|4) + execute_powershell_script $choice + ;; + 5) + echo "Exiting." + break + ;; + *) + echo "Invalid input. Please enter a number between 1 and 5." + ;; +esac + +echo diff --git a/Fuzzing/Fuzzing.md b/Fuzzing/Fuzzing.md index 7596c13d9e..de262f79d3 100644 --- a/Fuzzing/Fuzzing.md +++ b/Fuzzing/Fuzzing.md @@ -75,14 +75,35 @@ sharpfuzz ### Installation for libfuzzer on Windows -Install the latest dotnet SDK and runtime from https://dotnet.microsoft.com/download/dotnet/ +Install the latest dotnet SDK and runtime from https://dotnet.microsoft.com/download/dotnet/. ```commandline # Install SharpFuzz.CommandLine global .NET tool dotnet tool install --global SharpFuzz.CommandLine ``` -## Usage of afl-fuzz on Linux +### Usage of libfuzzer on Windows and Linux + +To run a fuzz target with libfuzzer on Windows, execute the following commands: + +```cmd +cd BinaryDecoder +./libfuzz.bat +``` + +On Linux, execute the following commands in a bash window: + +```bash +cd BinaryDecoder +./libfuzz.sh +``` + +A menu will show up to allow the selection of a fuzzer target function to execute. + +Now the fuzzer is started and runs until it hits a crash or timeout or until it is stopped manually by hitting Ctrl-C. Libfuzz writes findings in the current directory with the prefix crash- or timeout. + +To replay the test cases that caused the fuzzer to crash, execute the following command: + ## Afl-fuzz ### Usage of afl-fuzz on Linux @@ -94,6 +115,8 @@ cd BinaryDecoder ./aflfuzz.sh ``` -A menu will show up and allow the selection of a fuzzer target function to execute. +A menu will show up to allow the selection of a fuzzer target function to execute. + +Now the fuzzer is started and runs until it is stopped manually by hitting Ctrl-C. The fuzzer will create a directory `findings` in the fuzzer directory, which contains the test cases that caused the fuzzer to crash. -Now the fuzzer is started and will run until it is stopped manually by hitting Ctrl-C. The fuzzer will create a directory `findings` in the fuzzer directory, which contains the test cases that caused the fuzzer to crash. \ No newline at end of file +To replay the test cases that caused the fuzzer to crash, execute the following command: diff --git a/Fuzzing/JsonDecoder/libfuzz.sh b/Fuzzing/JsonDecoder/libfuzz.sh new file mode 100644 index 0000000000..2c1f7f07cf --- /dev/null +++ b/Fuzzing/JsonDecoder/libfuzz.sh @@ -0,0 +1 @@ +pwsh ../scripts/fuzz-libfuzzer.ps1 -libFuzzer "./libfuzzer-dotnet-ubuntu" -project ./Fuzz/JsonDecoder.Fuzz.csproj -fuzztarget LibfuzzJsonDecoder -corpus ./Fuzz/Testcases/ \ No newline at end of file diff --git a/Fuzzing/scripts/fuzz-libfuzzer.ps1 b/Fuzzing/scripts/fuzz-libfuzzer.ps1 index 35fddabeff..0507da5b2d 100644 --- a/Fuzzing/scripts/fuzz-libfuzzer.ps1 +++ b/Fuzzing/scripts/fuzz-libfuzzer.ps1 @@ -7,7 +7,7 @@ param ( [string]$corpus, [Parameter(Mandatory = $true)] [string]$fuzztarget, - [string]$temp = ".\libfuzz\Testcases", + [string]$temp = "./libfuzz/Testcases/", [string]$dict = $null, [int]$timeout = 10, [string]$command = "sharpfuzz" @@ -53,15 +53,19 @@ foreach ($fuzzingTarget in $fuzzingTargets) { } } -mkdir $temp -copy $corpus\*.* $temp +if (Test-Path $temp) { + Remove-Item -Recurse -Force $temp +} + +New-Item -ItemType Directory -Force -Path "$temp" +Copy-Item -Path "$corpus/*.*" -Destination "$temp" Write-Output "Start $libFuzzer" if ($dict) { - Write-Output "$libFuzzer -timeout="$timeout" -dict="$dict" --target_path=dotnet --target_arg=$project $fuzztarget $temp" + Write-Output "$libFuzzer -timeout=$timeout -dict=$dict --target_path=dotnet --target_arg=$project $fuzztarget $temp" & $libFuzzer -timeout="$timeout" -dict="$dict" --target_path=dotnet --target_arg="$project $fuzztarget" $temp } else { - Write-Output "$libFuzzer -timeout="$timeout" --target_path=dotnet --target_arg=$project $fuzztarget $temp" + Write-Output "$libFuzzer -timeout=$timeout --target_path=dotnet --target_arg=$project $fuzztarget $temp" & $libFuzzer -timeout="$timeout" --target_path=dotnet --target_arg="$project $fuzztarget" $temp } diff --git a/UA Fuzzing.sln b/UA Fuzzing.sln index a4356035cc..d62ada08b3 100644 --- a/UA Fuzzing.sln +++ b/UA Fuzzing.sln @@ -60,14 +60,12 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Libraries", "Libraries", "{ EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Stack", "Stack", "{2DC9F7F3-6698-4875-88A3-50678170A810}" EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "BinaryDecoder", "BinaryDecoder", "{128EE105-0A7A-4AB3-81E6-A77AE638B33F}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "BinaryDecoder.Fuzz", "Fuzzing\BinaryDecoder\Fuzz\BinaryDecoder.Fuzz.csproj", "{92CC62D7-B7B6-4FBD-B9AC-181BD0D11EC0}" +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Encoders", "Encoders", "{128EE105-0A7A-4AB3-81E6-A77AE638B33F}" EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "scripts", "scripts", "{612E66DA-731B-4E7C-969D-88079601ED30}" ProjectSection(SolutionItems) = preProject + Fuzzing\scripts\fuzz-afl.ps1 = Fuzzing\scripts\fuzz-afl.ps1 Fuzzing\scripts\fuzz-libfuzzer.ps1 = Fuzzing\scripts\fuzz-libfuzzer.ps1 - Fuzzing\scripts\fuzz.ps1 = Fuzzing\scripts\fuzz.ps1 Fuzzing\scripts\install.sh = Fuzzing\scripts\install.sh Fuzzing\scripts\readme.txt = Fuzzing\scripts\readme.txt Fuzzing\scripts\test-libfuzzer.ps1 = Fuzzing\scripts\test-libfuzzer.ps1 @@ -79,7 +77,11 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Fuzzing", "Fuzzing", "{E943 Fuzzing\Fuzzing.md = Fuzzing\Fuzzing.md EndProjectSection EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "BinaryDecoder.Fuzz.Tools", "Fuzzing\BinaryDecoder\Fuzz.Tools\BinaryDecoder.Fuzz.Tools.csproj", "{A2F6528F-2DB0-4E37-ACBC-3A9EB9126D05}" +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "dictionaries", "dictionaries", "{3D63FE3C-08CD-470A-9048-757C029C5EBF}" + ProjectSection(SolutionItems) = preProject + Fuzzing\dictionaries\json.dict = Fuzzing\dictionaries\json.dict + Fuzzing\dictionaries\xml.dict = Fuzzing\dictionaries\xml.dict + EndProjectSection EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "common", "common", "{6F57459E-6B63-4FB5-8311-7FC1DDC84650}" EndProject @@ -95,6 +97,10 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Fuzz.Tools", "Fuzz.Tools", Fuzzing\common\Fuzz.Tools\Testcases.cs = Fuzzing\common\Fuzz.Tools\Testcases.cs EndProjectSection EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Encoders.Fuzz", "Fuzzing\Encoders\Fuzz\Encoders.Fuzz.csproj", "{2CA57432-9B51-40A5-804B-D927897E7DAF}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Encoders.Fuzz.Tools", "Fuzzing\Encoders\Fuzz.Tools\Encoders.Fuzz.Tools.csproj", "{BAB22A60-473E-4AE3-82E3-6C78CC8F1E6B}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -190,30 +196,30 @@ Global {4B72937F-5A57-4CEA-B9FE-B4C45CF7B284}.Release|x64.Build.0 = Release|Any CPU {4B72937F-5A57-4CEA-B9FE-B4C45CF7B284}.Release|x86.ActiveCfg = Release|Any CPU {4B72937F-5A57-4CEA-B9FE-B4C45CF7B284}.Release|x86.Build.0 = Release|Any CPU - {92CC62D7-B7B6-4FBD-B9AC-181BD0D11EC0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {92CC62D7-B7B6-4FBD-B9AC-181BD0D11EC0}.Debug|Any CPU.Build.0 = Debug|Any CPU - {92CC62D7-B7B6-4FBD-B9AC-181BD0D11EC0}.Debug|x64.ActiveCfg = Debug|Any CPU - {92CC62D7-B7B6-4FBD-B9AC-181BD0D11EC0}.Debug|x64.Build.0 = Debug|Any CPU - {92CC62D7-B7B6-4FBD-B9AC-181BD0D11EC0}.Debug|x86.ActiveCfg = Debug|Any CPU - {92CC62D7-B7B6-4FBD-B9AC-181BD0D11EC0}.Debug|x86.Build.0 = Debug|Any CPU - {92CC62D7-B7B6-4FBD-B9AC-181BD0D11EC0}.Release|Any CPU.ActiveCfg = Release|Any CPU - {92CC62D7-B7B6-4FBD-B9AC-181BD0D11EC0}.Release|Any CPU.Build.0 = Release|Any CPU - {92CC62D7-B7B6-4FBD-B9AC-181BD0D11EC0}.Release|x64.ActiveCfg = Release|Any CPU - {92CC62D7-B7B6-4FBD-B9AC-181BD0D11EC0}.Release|x64.Build.0 = Release|Any CPU - {92CC62D7-B7B6-4FBD-B9AC-181BD0D11EC0}.Release|x86.ActiveCfg = Release|Any CPU - {92CC62D7-B7B6-4FBD-B9AC-181BD0D11EC0}.Release|x86.Build.0 = Release|Any CPU - {A2F6528F-2DB0-4E37-ACBC-3A9EB9126D05}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {A2F6528F-2DB0-4E37-ACBC-3A9EB9126D05}.Debug|Any CPU.Build.0 = Debug|Any CPU - {A2F6528F-2DB0-4E37-ACBC-3A9EB9126D05}.Debug|x64.ActiveCfg = Debug|Any CPU - {A2F6528F-2DB0-4E37-ACBC-3A9EB9126D05}.Debug|x64.Build.0 = Debug|Any CPU - {A2F6528F-2DB0-4E37-ACBC-3A9EB9126D05}.Debug|x86.ActiveCfg = Debug|Any CPU - {A2F6528F-2DB0-4E37-ACBC-3A9EB9126D05}.Debug|x86.Build.0 = Debug|Any CPU - {A2F6528F-2DB0-4E37-ACBC-3A9EB9126D05}.Release|Any CPU.ActiveCfg = Release|Any CPU - {A2F6528F-2DB0-4E37-ACBC-3A9EB9126D05}.Release|Any CPU.Build.0 = Release|Any CPU - {A2F6528F-2DB0-4E37-ACBC-3A9EB9126D05}.Release|x64.ActiveCfg = Release|Any CPU - {A2F6528F-2DB0-4E37-ACBC-3A9EB9126D05}.Release|x64.Build.0 = Release|Any CPU - {A2F6528F-2DB0-4E37-ACBC-3A9EB9126D05}.Release|x86.ActiveCfg = Release|Any CPU - {A2F6528F-2DB0-4E37-ACBC-3A9EB9126D05}.Release|x86.Build.0 = Release|Any CPU + {2CA57432-9B51-40A5-804B-D927897E7DAF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {2CA57432-9B51-40A5-804B-D927897E7DAF}.Debug|Any CPU.Build.0 = Debug|Any CPU + {2CA57432-9B51-40A5-804B-D927897E7DAF}.Debug|x64.ActiveCfg = Debug|Any CPU + {2CA57432-9B51-40A5-804B-D927897E7DAF}.Debug|x64.Build.0 = Debug|Any CPU + {2CA57432-9B51-40A5-804B-D927897E7DAF}.Debug|x86.ActiveCfg = Debug|Any CPU + {2CA57432-9B51-40A5-804B-D927897E7DAF}.Debug|x86.Build.0 = Debug|Any CPU + {2CA57432-9B51-40A5-804B-D927897E7DAF}.Release|Any CPU.ActiveCfg = Release|Any CPU + {2CA57432-9B51-40A5-804B-D927897E7DAF}.Release|Any CPU.Build.0 = Release|Any CPU + {2CA57432-9B51-40A5-804B-D927897E7DAF}.Release|x64.ActiveCfg = Release|Any CPU + {2CA57432-9B51-40A5-804B-D927897E7DAF}.Release|x64.Build.0 = Release|Any CPU + {2CA57432-9B51-40A5-804B-D927897E7DAF}.Release|x86.ActiveCfg = Release|Any CPU + {2CA57432-9B51-40A5-804B-D927897E7DAF}.Release|x86.Build.0 = Release|Any CPU + {BAB22A60-473E-4AE3-82E3-6C78CC8F1E6B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {BAB22A60-473E-4AE3-82E3-6C78CC8F1E6B}.Debug|Any CPU.Build.0 = Debug|Any CPU + {BAB22A60-473E-4AE3-82E3-6C78CC8F1E6B}.Debug|x64.ActiveCfg = Debug|Any CPU + {BAB22A60-473E-4AE3-82E3-6C78CC8F1E6B}.Debug|x64.Build.0 = Debug|Any CPU + {BAB22A60-473E-4AE3-82E3-6C78CC8F1E6B}.Debug|x86.ActiveCfg = Debug|Any CPU + {BAB22A60-473E-4AE3-82E3-6C78CC8F1E6B}.Debug|x86.Build.0 = Debug|Any CPU + {BAB22A60-473E-4AE3-82E3-6C78CC8F1E6B}.Release|Any CPU.ActiveCfg = Release|Any CPU + {BAB22A60-473E-4AE3-82E3-6C78CC8F1E6B}.Release|Any CPU.Build.0 = Release|Any CPU + {BAB22A60-473E-4AE3-82E3-6C78CC8F1E6B}.Release|x64.ActiveCfg = Release|Any CPU + {BAB22A60-473E-4AE3-82E3-6C78CC8F1E6B}.Release|x64.Build.0 = Release|Any CPU + {BAB22A60-473E-4AE3-82E3-6C78CC8F1E6B}.Release|x86.ActiveCfg = Release|Any CPU + {BAB22A60-473E-4AE3-82E3-6C78CC8F1E6B}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -229,12 +235,13 @@ Global {92D98D3D-2A7D-4F4B-8C72-1A98908D0221} = {2DC9F7F3-6698-4875-88A3-50678170A810} {4B72937F-5A57-4CEA-B9FE-B4C45CF7B284} = {ACBF012E-08A7-4939-88CF-D6B610E35AC5} {128EE105-0A7A-4AB3-81E6-A77AE638B33F} = {E9435CCE-1591-4AC0-87C4-341236E27C50} - {92CC62D7-B7B6-4FBD-B9AC-181BD0D11EC0} = {128EE105-0A7A-4AB3-81E6-A77AE638B33F} {612E66DA-731B-4E7C-969D-88079601ED30} = {E9435CCE-1591-4AC0-87C4-341236E27C50} - {A2F6528F-2DB0-4E37-ACBC-3A9EB9126D05} = {128EE105-0A7A-4AB3-81E6-A77AE638B33F} + {3D63FE3C-08CD-470A-9048-757C029C5EBF} = {E9435CCE-1591-4AC0-87C4-341236E27C50} {6F57459E-6B63-4FB5-8311-7FC1DDC84650} = {E9435CCE-1591-4AC0-87C4-341236E27C50} {C9E285F1-D853-4CD9-9ECF-493E09395548} = {6F57459E-6B63-4FB5-8311-7FC1DDC84650} {6A642C2C-0B5D-4C2D-BAD0-A6F760175465} = {6F57459E-6B63-4FB5-8311-7FC1DDC84650} + {2CA57432-9B51-40A5-804B-D927897E7DAF} = {128EE105-0A7A-4AB3-81E6-A77AE638B33F} + {BAB22A60-473E-4AE3-82E3-6C78CC8F1E6B} = {128EE105-0A7A-4AB3-81E6-A77AE638B33F} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {33FA5BCB-C827-4D44-AECF-F51342DFE64A} From d60704c4bcf3bacd4e37947a929b5259793fff7d Mon Sep 17 00:00:00 2001 From: Martin Regen Date: Sun, 28 Apr 2024 11:13:49 +0200 Subject: [PATCH 24/83] use helper to find methods --- Fuzzing/Encoders/Fuzz/Encoders.Fuzz.csproj | 1 + .../Encoders/Fuzz/FuzzableCode.JsonDecoder.cs | 2 +- Fuzzing/common/Fuzz/FuzzMethods.cs | 126 ++++++++++++++++++ Fuzzing/common/Fuzz/Program.cs | 83 ++++-------- 4 files changed, 153 insertions(+), 59 deletions(-) create mode 100644 Fuzzing/common/Fuzz/FuzzMethods.cs diff --git a/Fuzzing/Encoders/Fuzz/Encoders.Fuzz.csproj b/Fuzzing/Encoders/Fuzz/Encoders.Fuzz.csproj index c549c5878c..cfd8744a7b 100644 --- a/Fuzzing/Encoders/Fuzz/Encoders.Fuzz.csproj +++ b/Fuzzing/Encoders/Fuzz/Encoders.Fuzz.csproj @@ -13,6 +13,7 @@ + diff --git a/Fuzzing/Encoders/Fuzz/FuzzableCode.JsonDecoder.cs b/Fuzzing/Encoders/Fuzz/FuzzableCode.JsonDecoder.cs index 7db79ef0da..3e22c1f635 100644 --- a/Fuzzing/Encoders/Fuzz/FuzzableCode.JsonDecoder.cs +++ b/Fuzzing/Encoders/Fuzz/FuzzableCode.JsonDecoder.cs @@ -20,7 +20,7 @@ public static void AflfuzzJsonDecoder(string input) /// /// The Json encoder fuzz target for afl-fuzz. /// - public static void AflfuzzBinaryEncoder(string input) + public static void AflfuzzJsonEncoder(string input) { IEncodeable encodeable = null; try diff --git a/Fuzzing/common/Fuzz/FuzzMethods.cs b/Fuzzing/common/Fuzz/FuzzMethods.cs new file mode 100644 index 0000000000..2f32d25456 --- /dev/null +++ b/Fuzzing/common/Fuzz/FuzzMethods.cs @@ -0,0 +1,126 @@ + + +using System; +using System.Collections.Generic; +using System.IO; +using System.Reflection; +using SharpFuzz; + +public static class FuzzMethods +{ + // signatures supported by fuzzers + public delegate void AflFuzzStream(Stream stream); + public delegate void AflFuzzString(string text); + public delegate void LibFuzzSpan(ReadOnlySpan bytes); + + public static readonly Type[] Delegates = new Type[] { typeof(AflFuzzStream), typeof(AflFuzzString), typeof(LibFuzzSpan) }; + + public static readonly Dictionary FuzzMethodsToParameterType = new Dictionary + { + { typeof(AflFuzzStream), typeof(Stream) }, + { typeof(AflFuzzString), typeof(string) }, + { typeof(LibFuzzSpan), typeof(ReadOnlySpan) } + }; + + /// + /// Finds all fuzzing methods for specified delegate. + /// + public static List FindFuzzMethods(TextWriter errorOutput, Type delegateType) + { + List fuzzMethods = new List(); + Type delegateParameterType; + Type type = typeof(FuzzableCode); + if (FuzzMethodsToParameterType.TryGetValue(delegateType, out delegateParameterType)) + { + MethodInfo[] methods = type.GetMethods(BindingFlags.Public | BindingFlags.Instance | BindingFlags.Static); + foreach (var method in methods) + { + // Determine the target signature + ParameterInfo[] parameters = method.GetParameters(); + if (parameters.Length == 1 && parameters[0].ParameterType == delegateParameterType) + { + fuzzMethods.Add(method.CreateDelegate(delegateType)); + } + } + } + return fuzzMethods; + } + + /// + /// Finds a fuzzing method by name and returns a delegate to call it. + /// + public static Delegate FindFuzzMethod(TextWriter errorOutput, string fuzzingFunction) + { + // find the function to fuzz based on the first argument using reflection + Type type = typeof(FuzzableCode); + MethodInfo method = type.GetMethod(fuzzingFunction, BindingFlags.Public | BindingFlags.Instance | BindingFlags.Static); + if (method != null) + { + // Determine the target signature + ParameterInfo[] parameters = method.GetParameters(); + if (parameters.Length == 1) + { + // afl-fuzz targets + if (parameters[0].ParameterType == typeof(Stream)) + { + return (AflFuzzStream)method.CreateDelegate(typeof(AflFuzzStream)); + } + else if (parameters[0].ParameterType == typeof(string)) + { + return (AflFuzzString)method.CreateDelegate(typeof(AflFuzzString)); + } + // libfuzzer span target + else if (parameters[0].ParameterType == typeof(ReadOnlySpan)) + { + return (LibFuzzSpan)method.CreateDelegate(typeof(LibFuzzSpan)); + } + } + + errorOutput.WriteLine("The fuzzing function {0} does not have the correct signature {1}.", fuzzingFunction, parameters[0].ParameterType); + } + else + { + errorOutput.WriteLine("The fuzzing function {0} was not found.", fuzzingFunction); + } + + return null; + } + + /// + /// Runs the fuzzing method with the given delegate. + /// + public static void RunFuzzMethod(Delegate fuzzingMethod, bool outOfProcess = false) + { + // find the function to fuzz method based on the type + if (fuzzingMethod is AflFuzzStream aflFuzzStreamMethod) + { + if (outOfProcess) + { + Fuzzer.OutOfProcess.Run(stream => aflFuzzStreamMethod(stream)); + } + else + { + Fuzzer.Run(stream => aflFuzzStreamMethod(stream)); + } + return; + } + else if (fuzzingMethod is AflFuzzString aflFuzzStringMethod) + { + if (outOfProcess) + { + Fuzzer.OutOfProcess.Run(text => aflFuzzStringMethod(text)); + } + else + { + Fuzzer.Run(text => aflFuzzStringMethod(text)); + } + return; + } + // libfuzzer span target + else if (fuzzingMethod is LibFuzzSpan libFuzzSpanMethod) + { + Fuzzer.LibFuzzer.Run(bytes => libFuzzSpanMethod(bytes)); + return; + } + } +} diff --git a/Fuzzing/common/Fuzz/Program.cs b/Fuzzing/common/Fuzz/Program.cs index 3ad0748d34..60a4354e21 100644 --- a/Fuzzing/common/Fuzz/Program.cs +++ b/Fuzzing/common/Fuzz/Program.cs @@ -19,47 +19,17 @@ public static void Main(string[] args) FuzzableCode.FuzzInfo(); Console.WriteLine(); - if (args.Length == 1) + if (args.Length >= 1) { - // find the function to fuzz based on the first argument using reflection - Type type = typeof(FuzzableCode); - fuzzingFunction = args[0]; - MethodInfo method = type.GetMethod(args[0], BindingFlags.Public | BindingFlags.Instance | BindingFlags.Static); - if (method != null) + var fuzzingMethod = FuzzMethods.FindFuzzMethod(Console.Error, args[0]); + + if (fuzzingMethod != null) { - Console.WriteLine($"Found the fuzzing function: {args[0]}"); + Console.WriteLine($"Run the fuzzing function: {args[0]}"); - // call the fuzzer target if there is a matching signature - ParameterInfo[] parameters = method.GetParameters(); - if (parameters.Length == 1) - { - // afl-fuzz targets - if (parameters[0].ParameterType == typeof(Stream)) - { - var fuzzMethod = (AflFuzzStream)method.CreateDelegate(typeof(AflFuzzStream)); - Fuzzer.Run(stream => fuzzMethod(stream)); - return; - } - else if (parameters[0].ParameterType == typeof(string)) - { - var fuzzMethod = (AflFuzzString)method.CreateDelegate(typeof(AflFuzzString)); - Fuzzer.Run(text => fuzzMethod(text)); - return; - } - // libfuzzer span target - else if (parameters[0].ParameterType == typeof(ReadOnlySpan)) - { - var fuzzMethod = (LibFuzzSpan)method.CreateDelegate(typeof(LibFuzzSpan)); - Fuzzer.LibFuzzer.Run(bytes => fuzzMethod(bytes)); - return; - } - } + FuzzMethods.RunFuzzMethod(fuzzingMethod); - Console.Error.WriteLine("The fuzzing function {0} does not have the correct signature {1}.", fuzzingFunction, parameters[0].ParameterType); - } - else - { - Console.Error.WriteLine("The fuzzing function {0} was not found.", fuzzingFunction); + return; } } @@ -74,35 +44,32 @@ private static void Usage(string fuzzingFunction) Console.Error.WriteLine(); Console.Error.WriteLine("Available fuzzing functions:"); - foreach (var parameterType in new Type[] { typeof(Stream), typeof(string), typeof(ReadOnlySpan) }) + foreach (Type parameterType in FuzzMethods.Delegates) { bool writeHeader = true; - foreach (var m in type.GetMethods(BindingFlags.Public | BindingFlags.Instance | BindingFlags.Static)) + foreach (var method in FuzzMethods.FindFuzzMethods(Console.Error, parameterType)) { - ParameterInfo[] parameters = m.GetParameters(); - if (parameters.Length == 1 && parameters[0].ParameterType == parameterType) + if (writeHeader) { - if (writeHeader) + Console.Error.WriteLine(); + if (parameterType.Name == nameof(AflFuzzStream)) { - Console.Error.WriteLine(); - if (parameterType == typeof(Stream)) - { - Console.Error.WriteLine("afl-fuzz Stream signature:"); - } - else if (parameterType == typeof(string)) - { - Console.Error.WriteLine("afl-fuzz string signature:"); - } - else if (parameterType == typeof(ReadOnlySpan)) - { - Console.Error.WriteLine("libfuzzer: ReadOnlySpan signature:"); - } - writeHeader = false; + Console.Error.WriteLine("afl-fuzz Stream signature:"); } - - Console.Error.WriteLine("-- {0}", m.Name); + else if (parameterType.Name == nameof(AflFuzzString)) + { + Console.Error.WriteLine("afl-fuzz string signature:"); + } + else if (parameterType.Name == nameof(LibFuzzSpan)) + { + Console.Error.WriteLine("libfuzzer ReadOnlySpan signature:"); + } + writeHeader = false; } + + Console.Error.WriteLine("-- {0}", method.Method.Name); } } } } + From 1128fc3c3af427963cbeab45c454f7f89447f23c Mon Sep 17 00:00:00 2001 From: Martin Regen Date: Sun, 28 Apr 2024 12:53:51 +0200 Subject: [PATCH 25/83] fixes --- .../Fuzz.Tools/Encoders.Fuzz.Tools.csproj | 2 + .../Encoders/Fuzz.Tools/Encoders.Testcases.cs | 71 +++++++++++++++++++ .../Fuzz/FuzzableCode.BinaryDecoder.cs | 8 ++- .../Encoders/Fuzz/FuzzableCode.JsonDecoder.cs | 8 ++- .../Opc.Ua.Core/Types/Encoders/XmlEncoder.cs | 4 ++ 5 files changed, 89 insertions(+), 4 deletions(-) create mode 100644 Fuzzing/Encoders/Fuzz.Tools/Encoders.Testcases.cs diff --git a/Fuzzing/Encoders/Fuzz.Tools/Encoders.Fuzz.Tools.csproj b/Fuzzing/Encoders/Fuzz.Tools/Encoders.Fuzz.Tools.csproj index 95a9cef067..ec900f2e2e 100644 --- a/Fuzzing/Encoders/Fuzz.Tools/Encoders.Fuzz.Tools.csproj +++ b/Fuzzing/Encoders/Fuzz.Tools/Encoders.Fuzz.Tools.csproj @@ -9,6 +9,8 @@ + + diff --git a/Fuzzing/Encoders/Fuzz.Tools/Encoders.Testcases.cs b/Fuzzing/Encoders/Fuzz.Tools/Encoders.Testcases.cs new file mode 100644 index 0000000000..b0639b88a7 --- /dev/null +++ b/Fuzzing/Encoders/Fuzz.Tools/Encoders.Testcases.cs @@ -0,0 +1,71 @@ + +using System.IO; +using System.Text; +using Opc.Ua; + +public static partial class Testcases +{ + public static void Run(string directoryPath) + { + string workPath = Path.TrimEndingDirectorySeparator(directoryPath); + + // Create the Testcases for the binary decoder. + string pathSuffix = ".Binary"; + string pathTarget = workPath + pathSuffix; + foreach (var messageEncoder in MessageEncoders) + { + byte[] message; + using (var encoder = new BinaryEncoder(MessageContext)) + { + messageEncoder(encoder); + message = encoder.CloseAndReturnBuffer(); + } + + // Test the fuzz targets with the message. + FuzzableCode.LibfuzzBinaryDecoder(message); + FuzzableCode.LibfuzzBinaryEncoder(message); + using (var stream = new MemoryStream(message)) + { + FuzzableCode.AflfuzzBinaryDecoder(stream); + } + using (var stream = new MemoryStream(message)) + { + FuzzableCode.AflfuzzBinaryEncoder(stream); + } + using (var stream = new MemoryStream(message)) + { + FuzzableCode.FuzzBinaryDecoderCore(stream, true); + } + + string fileName = Path.Combine(pathTarget, $"{messageEncoder.Method.Name}.bin".ToLowerInvariant()); + File.WriteAllBytes(fileName, message); + } + + // Create the Testcases for the json decoder. + pathSuffix = ".Json"; + pathTarget = workPath + pathSuffix; + foreach (var messageEncoder in MessageEncoders) + { + byte[] message; + using (var memoryStream = new MemoryStream(0x1000)) + using (var encoder = new JsonEncoder(MessageContext, true, false, memoryStream)) + { + messageEncoder(encoder); + encoder.Close(); + message = memoryStream.ToArray(); + } + + + // Test the fuzz targets with the message. + FuzzableCode.LibfuzzJsonDecoder(message); + FuzzableCode.LibfuzzJsonEncoder(message); + string json = Encoding.UTF8.GetString(message); + FuzzableCode.AflfuzzJsonDecoder(json); + FuzzableCode.AflfuzzJsonEncoder(json); + FuzzableCode.FuzzJsonDecoderCore(json, true); + + string fileName = Path.Combine(pathTarget, $"{messageEncoder.Method.Name}.json".ToLowerInvariant()); + File.WriteAllBytes(fileName, message); + } + } +} diff --git a/Fuzzing/Encoders/Fuzz/FuzzableCode.BinaryDecoder.cs b/Fuzzing/Encoders/Fuzz/FuzzableCode.BinaryDecoder.cs index 4a9523ab16..ed0933a57d 100644 --- a/Fuzzing/Encoders/Fuzz/FuzzableCode.BinaryDecoder.cs +++ b/Fuzzing/Encoders/Fuzz/FuzzableCode.BinaryDecoder.cs @@ -86,7 +86,7 @@ public static void LibfuzzBinaryEncoder(ReadOnlySpan input) /// The fuzz target for the BinaryDecoder. /// /// A memory stream with fuzz content. - private static IEncodeable FuzzBinaryDecoderCore(MemoryStream stream) + internal static IEncodeable FuzzBinaryDecoderCore(MemoryStream stream, bool throwAll = false) { try { @@ -101,7 +101,11 @@ private static IEncodeable FuzzBinaryDecoderCore(MemoryStream stream) { case StatusCodes.BadEncodingLimitsExceeded: case StatusCodes.BadDecodingError: - return null; + if (!throwAll) + { + return null; + } + break; } throw; diff --git a/Fuzzing/Encoders/Fuzz/FuzzableCode.JsonDecoder.cs b/Fuzzing/Encoders/Fuzz/FuzzableCode.JsonDecoder.cs index 3e22c1f635..4486ecb1e0 100644 --- a/Fuzzing/Encoders/Fuzz/FuzzableCode.JsonDecoder.cs +++ b/Fuzzing/Encoders/Fuzz/FuzzableCode.JsonDecoder.cs @@ -83,7 +83,7 @@ public static void LibfuzzJsonEncoder(ReadOnlySpan input) /// The fuzz target for the JsonDecoder. /// /// A string with fuzz content. - private static IEncodeable FuzzJsonDecoderCore(string json) + internal static IEncodeable FuzzJsonDecoderCore(string json, bool throwAll = false) { try { @@ -98,7 +98,11 @@ private static IEncodeable FuzzJsonDecoderCore(string json) { case StatusCodes.BadEncodingLimitsExceeded: case StatusCodes.BadDecodingError: - return null; + if (!throwAll) + { + return null; + } + break; } throw; diff --git a/Stack/Opc.Ua.Core/Types/Encoders/XmlEncoder.cs b/Stack/Opc.Ua.Core/Types/Encoders/XmlEncoder.cs index ca68883315..7dbf26ac76 100644 --- a/Stack/Opc.Ua.Core/Types/Encoders/XmlEncoder.cs +++ b/Stack/Opc.Ua.Core/Types/Encoders/XmlEncoder.cs @@ -306,11 +306,15 @@ public void EncodeMessage(IEncodeable message) // convert the namespace uri to an index. NodeId typeId = ExpandedNodeId.ToNodeId(message.TypeId, m_context.NamespaceUris); + PushNamespace(Namespaces.OpcUaXsd); + // write the type id. WriteNodeId("TypeId", typeId); // write the message. WriteEncodeable("Body", message, message.GetType()); + + PopNamespace(); } /// From 4d3077945766d57ac017d7c9315395a5e029136f Mon Sep 17 00:00:00 2001 From: Martin Regen Date: Mon, 29 Apr 2024 08:08:31 +0200 Subject: [PATCH 26/83] fix project --- .../Fuzz.Tools/Encoders.Fuzz.Tools.csproj | 1 + .../Encoders/Fuzz.Tools/Encoders.Testcases.cs | 37 ++++++++-- Fuzzing/JsonDecoder/libfuzz.sh | 1 - Fuzzing/common/Fuzz.Tools/Playback.cs | 69 ++++++++++++------- Fuzzing/common/Fuzz.Tools/Program.cs | 12 +++- Fuzzing/common/Fuzz.Tools/Testcases.cs | 15 +--- Fuzzing/common/Fuzz/FuzzMethods.cs | 2 +- Fuzzing/common/Fuzz/Program.cs | 2 +- .../Opc.Ua.Core/Stack/Tcp/TcpServerChannel.cs | 1 - .../Stack/Tcp/UaSCBinaryChannel.cs | 13 ++-- .../Opc.Ua.Core/Types/Encoders/JsonDecoder.cs | 35 ++++++---- UA Fuzzing.sln | 3 +- 12 files changed, 121 insertions(+), 70 deletions(-) delete mode 100644 Fuzzing/JsonDecoder/libfuzz.sh diff --git a/Fuzzing/Encoders/Fuzz.Tools/Encoders.Fuzz.Tools.csproj b/Fuzzing/Encoders/Fuzz.Tools/Encoders.Fuzz.Tools.csproj index ec900f2e2e..258bf00272 100644 --- a/Fuzzing/Encoders/Fuzz.Tools/Encoders.Fuzz.Tools.csproj +++ b/Fuzzing/Encoders/Fuzz.Tools/Encoders.Fuzz.Tools.csproj @@ -11,6 +11,7 @@ + diff --git a/Fuzzing/Encoders/Fuzz.Tools/Encoders.Testcases.cs b/Fuzzing/Encoders/Fuzz.Tools/Encoders.Testcases.cs index b0639b88a7..e7c5ba21c7 100644 --- a/Fuzzing/Encoders/Fuzz.Tools/Encoders.Testcases.cs +++ b/Fuzzing/Encoders/Fuzz.Tools/Encoders.Testcases.cs @@ -5,13 +5,23 @@ public static partial class Testcases { + + public enum TestCaseEncoders : int + { + Binary=0, + Json=1, + Xml=2 + }; + + public static string[] TestcaseEncoderSuffixes = new string[] { ".Binary", ".Json", ".Xml" }; + public static void Run(string directoryPath) { string workPath = Path.TrimEndingDirectorySeparator(directoryPath); // Create the Testcases for the binary decoder. - string pathSuffix = ".Binary"; - string pathTarget = workPath + pathSuffix; + string pathSuffix = TestcaseEncoderSuffixes[(int)TestCaseEncoders.Binary]; + string pathTarget = workPath + pathSuffix + Path.DirectorySeparatorChar; foreach (var messageEncoder in MessageEncoders) { byte[] message; @@ -42,8 +52,8 @@ public static void Run(string directoryPath) } // Create the Testcases for the json decoder. - pathSuffix = ".Json"; - pathTarget = workPath + pathSuffix; + pathSuffix = TestcaseEncoderSuffixes[(int)TestCaseEncoders.Json]; + pathTarget = workPath + pathSuffix + Path.DirectorySeparatorChar; foreach (var messageEncoder in MessageEncoders) { byte[] message; @@ -67,5 +77,24 @@ public static void Run(string directoryPath) string fileName = Path.Combine(pathTarget, $"{messageEncoder.Method.Name}.json".ToLowerInvariant()); File.WriteAllBytes(fileName, message); } + + // Create the Testcases for the xml decoder. + pathSuffix = TestcaseEncoderSuffixes[(int)TestCaseEncoders.Xml]; + pathTarget = workPath + pathSuffix + Path.DirectorySeparatorChar; + foreach (var messageEncoder in MessageEncoders) + { + string message; + using (var encoder = new XmlEncoder(MessageContext)) + { + encoder.SetMappingTables(MessageContext.NamespaceUris, MessageContext.ServerUris); + messageEncoder(encoder); + message = encoder.CloseAndReturnText(); + } + + // Test the fuzz targets with the message. + + string fileName = Path.Combine(pathTarget, $"{messageEncoder.Method.Name}.xml".ToLowerInvariant()); + File.WriteAllBytes(fileName, Encoding.UTF8.GetBytes(message)); + } } } diff --git a/Fuzzing/JsonDecoder/libfuzz.sh b/Fuzzing/JsonDecoder/libfuzz.sh deleted file mode 100644 index 2c1f7f07cf..0000000000 --- a/Fuzzing/JsonDecoder/libfuzz.sh +++ /dev/null @@ -1 +0,0 @@ -pwsh ../scripts/fuzz-libfuzzer.ps1 -libFuzzer "./libfuzzer-dotnet-ubuntu" -project ./Fuzz/JsonDecoder.Fuzz.csproj -fuzztarget LibfuzzJsonDecoder -corpus ./Fuzz/Testcases/ \ No newline at end of file diff --git a/Fuzzing/common/Fuzz.Tools/Playback.cs b/Fuzzing/common/Fuzz.Tools/Playback.cs index 34b3ec50bd..7326477b39 100644 --- a/Fuzzing/common/Fuzz.Tools/Playback.cs +++ b/Fuzzing/common/Fuzz.Tools/Playback.cs @@ -1,39 +1,60 @@ -using System.IO; using System; -using System.Text; +using System.Collections.Generic; using System.Diagnostics; +using System.IO; +using static FuzzMethods; public static class Playback { + /// + /// Test the libfuzz methods on the files in the directory. + /// + /// The directory where to find the crash data. + /// If the stack trace should be written to output. public static void Run(string directoryPath, bool stackTrace) { - var path = Path.GetDirectoryName(directoryPath); - var searchPattern = Path.GetFileName(directoryPath); - foreach (var crashFile in Directory.EnumerateFiles(path, searchPattern)) + string path = Path.GetDirectoryName(directoryPath); + string searchPattern = Path.GetFileName(directoryPath); + List libFuzzMethods = FindFuzzMethods(typeof(LibFuzzSpan)); + + IEnumerable crashFiles; + try { - var stopWatch = new Stopwatch(); -#if TEXTFUZZER - var crashData = Encoding.UTF8.GetString(File.ReadAllBytes(crashFile)); -#else - using (var crashData = new FileStream(crashFile, FileMode.Open, FileAccess.Read)) -#endif + crashFiles = Directory.EnumerateFiles(path, searchPattern); + } + catch (Exception) + { + Console.WriteLine("Directory not found: {0}", path); + return; + } + + foreach (string crashFile in crashFiles) + { + Console.WriteLine("### Crash data {0:20} ###", Path.GetFileName(crashFile)); + byte[] crashData = File.ReadAllBytes(crashFile); + + foreach (Delegate method in libFuzzMethods) { - try - { - stopWatch.Start(); - FuzzableCode.FuzzTarget(crashData); - stopWatch.Stop(); - Console.WriteLine("File: {0:20} Elapsed: {1}ms", Path.GetFileName(crashFile), stopWatch.ElapsedMilliseconds); - } - catch (Exception ex) + if (method is LibFuzzSpan libFuzzMethod) { - stopWatch.Stop(); - Console.WriteLine("File: {0:20} Elapsed: {1}ms", Path.GetFileName(crashFile), stopWatch.ElapsedMilliseconds); - Console.WriteLine("{0}:{1}", ex.GetType().Name, ex.Message); - if (stackTrace) + var stopWatch = new Stopwatch(); + try + { + stopWatch.Start(); + libFuzzMethod(crashData); + stopWatch.Stop(); + Console.WriteLine("Target: {0:30} Elapsed: {1}ms", libFuzzMethod.Method.Name, stopWatch.ElapsedMilliseconds); + } + catch (Exception ex) { - Console.WriteLine(ex.StackTrace); + stopWatch.Stop(); + Console.WriteLine("Target: {0:30} Elapsed: {1}ms", libFuzzMethod.Method.Name, stopWatch.ElapsedMilliseconds); + Console.WriteLine("{0}:{1}", ex.GetType().Name, ex.Message); + if (stackTrace) + { + Console.WriteLine(ex.StackTrace); + } } } } diff --git a/Fuzzing/common/Fuzz.Tools/Program.cs b/Fuzzing/common/Fuzz.Tools/Program.cs index b450f8f408..57cfff0e31 100644 --- a/Fuzzing/common/Fuzz.Tools/Program.cs +++ b/Fuzzing/common/Fuzz.Tools/Program.cs @@ -8,7 +8,7 @@ public static class Program { public static readonly string RootFolder = "../../../../"; - public static readonly string DefaultTestcasesFolder = RootFolder + "Fuzz/Testcases/"; + public static readonly string DefaultTestcasesFolder = RootFolder + "Fuzz/Testcases"; public static readonly string DefaultFindingsCrashFolder = RootFolder + "findings/crashes/"; public static readonly string DefaultFindingsHangsFolder = RootFolder + "findings/hangs/"; public static readonly string DefaultLibFuzzerCrashes = RootFolder + "crash-*"; @@ -54,10 +54,18 @@ public static void Main(string[] args) } else if (playback) { - Playback.Run(DefaultTestcasesFolder, stacktrace); + foreach (var encoderType in Testcases.TestcaseEncoderSuffixes) + { + Console.WriteLine("--- Fuzzer testcases for {0} ---", encoderType.Substring(1)); + Playback.Run(DefaultTestcasesFolder + encoderType + Path.DirectorySeparatorChar, stacktrace); + } + Console.WriteLine("--- afl-fuzz crash findings ---"); Playback.Run(DefaultFindingsCrashFolder, stacktrace); + Console.WriteLine("--- afl-fuzz timeout findings ---"); Playback.Run(DefaultFindingsHangsFolder, stacktrace); + Console.WriteLine("--- libfuzzer crashes ---"); Playback.Run(DefaultLibFuzzerCrashes, stacktrace); + Console.WriteLine("--- libfuzzer timeouts ---"); Playback.Run(DefaultLibFuzzerHangs, stacktrace); } else diff --git a/Fuzzing/common/Fuzz.Tools/Testcases.cs b/Fuzzing/common/Fuzz.Tools/Testcases.cs index 0ba7946b34..91ed7ff1fd 100644 --- a/Fuzzing/common/Fuzz.Tools/Testcases.cs +++ b/Fuzzing/common/Fuzz.Tools/Testcases.cs @@ -7,7 +7,7 @@ public partial class Testcases { public delegate void MessageEncoder(IEncoder encoder); - public static ServiceMessageContext MessageContext = new ServiceMessageContext(); + public static ServiceMessageContext MessageContext = ServiceMessageContext.GlobalContext; public static MessageEncoder[] MessageEncoders = new MessageEncoder[] { ReadRequest, @@ -70,13 +70,14 @@ public static void ReadResponse(IEncoder encoder) StatusCode = StatusCodes.BadDataLost, }, new DataValue { - Value = new Variant(new byte[] { 0,1,2,3,4,5,6}), + Value = new Variant(new byte[] { 0,1,2,3,4,5,6 }), ServerTimestamp = now, SourceTimestamp = now.AddMinutes(1), StatusCode = StatusCodes.Good, }, new DataValue { Value = new Variant((byte)42), + SourceTimestamp = now, }, }, DiagnosticInfos = new DiagnosticInfoCollection { @@ -113,14 +114,4 @@ public static void ReadResponse(IEncoder encoder) }; encoder.EncodeMessage(readRequest); } - -#if !TEXTFUZZER - public static void FuzzTestcase(byte[] input) - { - using (var stream = new MemoryStream(input)) - { - FuzzableCode.FuzzTarget(stream); - } - } -#endif } diff --git a/Fuzzing/common/Fuzz/FuzzMethods.cs b/Fuzzing/common/Fuzz/FuzzMethods.cs index 2f32d25456..96dfc3e24e 100644 --- a/Fuzzing/common/Fuzz/FuzzMethods.cs +++ b/Fuzzing/common/Fuzz/FuzzMethods.cs @@ -25,7 +25,7 @@ public static class FuzzMethods /// /// Finds all fuzzing methods for specified delegate. /// - public static List FindFuzzMethods(TextWriter errorOutput, Type delegateType) + public static List FindFuzzMethods(Type delegateType) { List fuzzMethods = new List(); Type delegateParameterType; diff --git a/Fuzzing/common/Fuzz/Program.cs b/Fuzzing/common/Fuzz/Program.cs index 60a4354e21..bdd43dfedb 100644 --- a/Fuzzing/common/Fuzz/Program.cs +++ b/Fuzzing/common/Fuzz/Program.cs @@ -47,7 +47,7 @@ private static void Usage(string fuzzingFunction) foreach (Type parameterType in FuzzMethods.Delegates) { bool writeHeader = true; - foreach (var method in FuzzMethods.FindFuzzMethods(Console.Error, parameterType)) + foreach (var method in FuzzMethods.FindFuzzMethods(parameterType)) { if (writeHeader) { diff --git a/Stack/Opc.Ua.Core/Stack/Tcp/TcpServerChannel.cs b/Stack/Opc.Ua.Core/Stack/Tcp/TcpServerChannel.cs index 48a2f4b251..5b2ceb39cd 100644 --- a/Stack/Opc.Ua.Core/Stack/Tcp/TcpServerChannel.cs +++ b/Stack/Opc.Ua.Core/Stack/Tcp/TcpServerChannel.cs @@ -1009,7 +1009,6 @@ private bool ProcessRequestMessage(uint messageType, ArraySegment messageC chunksToProcess = GetSavedChunks(requestId, messageBody, true); // decode the request. - if (!(BinaryDecoder.DecodeMessage(new ArraySegmentStream(chunksToProcess), null, Quotas.MessageContext) is IServiceRequest request)) { SendServiceFault(token, requestId, ServiceResult.Create(StatusCodes.BadStructureMissing, "Could not parse request body.")); diff --git a/Stack/Opc.Ua.Core/Stack/Tcp/UaSCBinaryChannel.cs b/Stack/Opc.Ua.Core/Stack/Tcp/UaSCBinaryChannel.cs index 1916072591..6cb6b1f147 100644 --- a/Stack/Opc.Ua.Core/Stack/Tcp/UaSCBinaryChannel.cs +++ b/Stack/Opc.Ua.Core/Stack/Tcp/UaSCBinaryChannel.cs @@ -207,15 +207,12 @@ public void SetStateChangedCallback(TcpChannelStateEventHandler callback) /// protected void ChannelStateChanged(TcpChannelState state, ServiceResult reason) { - if (m_StateChanged != null) + var stateChanged = m_StateChanged; + if (stateChanged != null) { - var stateChanged = m_StateChanged; - if (stateChanged != null) - { - Task.Run(() => { - stateChanged?.Invoke(this, state, reason); - }); - } + Task.Run(() => { + stateChanged?.Invoke(this, state, reason); + }); } } diff --git a/Stack/Opc.Ua.Core/Types/Encoders/JsonDecoder.cs b/Stack/Opc.Ua.Core/Types/Encoders/JsonDecoder.cs index 5f2860af2a..ab412f537e 100644 --- a/Stack/Opc.Ua.Core/Types/Encoders/JsonDecoder.cs +++ b/Stack/Opc.Ua.Core/Types/Encoders/JsonDecoder.cs @@ -2999,27 +2999,34 @@ private NodeId DefaultNodeId(IdType idType, ushort namespaceIndex) private void EncodeAsJson(JsonTextWriter writer, object value) { - if (value is Dictionary map) + try { - EncodeAsJson(writer, map); - return; - } - + if (value is Dictionary map) + { + EncodeAsJson(writer, map); + return; + } - if (value is List list) - { - writer.WriteStartArray(); - foreach (var element in list) + if (value is List list) { - EncodeAsJson(writer, element); + writer.WriteStartArray(); + + foreach (var element in list) + { + EncodeAsJson(writer, element); + } + + writer.WriteStartArray(); + return; } - writer.WriteStartArray(); - return; + writer.WriteValue(value); + } + catch (JsonWriterException jwe) + { + throw ServiceResultException.Create(StatusCodes.BadDecodingError, "Unable to encode ExtensionObject Body as Json: {0}", jwe.Message); } - - writer.WriteValue(value); } private void EncodeAsJson(JsonTextWriter writer, Dictionary value) diff --git a/UA Fuzzing.sln b/UA Fuzzing.sln index d62ada08b3..73fbc52862 100644 --- a/UA Fuzzing.sln +++ b/UA Fuzzing.sln @@ -68,8 +68,6 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "scripts", "scripts", "{612E Fuzzing\scripts\fuzz-libfuzzer.ps1 = Fuzzing\scripts\fuzz-libfuzzer.ps1 Fuzzing\scripts\install.sh = Fuzzing\scripts\install.sh Fuzzing\scripts\readme.txt = Fuzzing\scripts\readme.txt - Fuzzing\scripts\test-libfuzzer.ps1 = Fuzzing\scripts\test-libfuzzer.ps1 - Fuzzing\scripts\test.ps1 = Fuzzing\scripts\test.ps1 EndProjectSection EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Fuzzing", "Fuzzing", "{E9435CCE-1591-4AC0-87C4-341236E27C50}" @@ -87,6 +85,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "common", "common", "{6F5745 EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Fuzz", "Fuzz", "{C9E285F1-D853-4CD9-9ECF-493E09395548}" ProjectSection(SolutionItems) = preProject + Fuzzing\common\Fuzz\FuzzMethods.cs = Fuzzing\common\Fuzz\FuzzMethods.cs Fuzzing\common\Fuzz\Program.cs = Fuzzing\common\Fuzz\Program.cs EndProjectSection EndProject From 99047e1abb1534ff3023d74649c12899e6434ff1 Mon Sep 17 00:00:00 2001 From: Martin Regen Date: Mon, 29 Apr 2024 08:37:05 +0200 Subject: [PATCH 27/83] copyrights --- .../Encoders/Fuzz.Tools/Encoders.Testcases.cs | 34 ++++++++++++++++-- .../Fuzz/FuzzableCode.BinaryDecoder.cs | 28 +++++++++++++++ .../Encoders/Fuzz/FuzzableCode.JsonDecoder.cs | 29 ++++++++++++++- Fuzzing/Encoders/Fuzz/FuzzableCode.cs | 29 ++++++++++++++- Fuzzing/common/Fuzz.Tools/Playback.cs | 28 +++++++++++++++ Fuzzing/common/Fuzz.Tools/Program.cs | 36 +++++++++++++++++-- Fuzzing/common/Fuzz.Tools/Testcases.cs | 29 ++++++++++++++- Fuzzing/common/Fuzz/FuzzMethods.cs | 29 ++++++++++++++- Fuzzing/common/Fuzz/Program.cs | 31 ++++++++++++++-- 9 files changed, 261 insertions(+), 12 deletions(-) diff --git a/Fuzzing/Encoders/Fuzz.Tools/Encoders.Testcases.cs b/Fuzzing/Encoders/Fuzz.Tools/Encoders.Testcases.cs index e7c5ba21c7..57ecc46db6 100644 --- a/Fuzzing/Encoders/Fuzz.Tools/Encoders.Testcases.cs +++ b/Fuzzing/Encoders/Fuzz.Tools/Encoders.Testcases.cs @@ -1,3 +1,31 @@ +/* ======================================================================== + * Copyright (c) 2005-2024 The OPC Foundation, Inc. All rights reserved. + * + * OPC Foundation MIT License 1.00 + * + * Permission is hereby granted, free of charge, to any person + * obtaining a copy of this software and associated documentation + * files (the "Software"), to deal in the Software without + * restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following + * conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES + * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT + * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, + * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR + * OTHER DEALINGS IN THE SOFTWARE. + * + * The complete license agreement can be found here: + * http://opcfoundation.org/License/MIT/1.00/ + * ======================================================================*/ using System.IO; using System.Text; @@ -8,9 +36,9 @@ public static partial class Testcases public enum TestCaseEncoders : int { - Binary=0, - Json=1, - Xml=2 + Binary = 0, + Json = 1, + Xml = 2 }; public static string[] TestcaseEncoderSuffixes = new string[] { ".Binary", ".Json", ".Xml" }; diff --git a/Fuzzing/Encoders/Fuzz/FuzzableCode.BinaryDecoder.cs b/Fuzzing/Encoders/Fuzz/FuzzableCode.BinaryDecoder.cs index ed0933a57d..8a2dcd774a 100644 --- a/Fuzzing/Encoders/Fuzz/FuzzableCode.BinaryDecoder.cs +++ b/Fuzzing/Encoders/Fuzz/FuzzableCode.BinaryDecoder.cs @@ -1,3 +1,31 @@ +/* ======================================================================== + * Copyright (c) 2005-2024 The OPC Foundation, Inc. All rights reserved. + * + * OPC Foundation MIT License 1.00 + * + * Permission is hereby granted, free of charge, to any person + * obtaining a copy of this software and associated documentation + * files (the "Software"), to deal in the Software without + * restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following + * conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES + * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT + * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, + * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR + * OTHER DEALINGS IN THE SOFTWARE. + * + * The complete license agreement can be found here: + * http://opcfoundation.org/License/MIT/1.00/ + * ======================================================================*/ using System; using System.IO; diff --git a/Fuzzing/Encoders/Fuzz/FuzzableCode.JsonDecoder.cs b/Fuzzing/Encoders/Fuzz/FuzzableCode.JsonDecoder.cs index 4486ecb1e0..1f18b5d84e 100644 --- a/Fuzzing/Encoders/Fuzz/FuzzableCode.JsonDecoder.cs +++ b/Fuzzing/Encoders/Fuzz/FuzzableCode.JsonDecoder.cs @@ -1,4 +1,31 @@ - +/* ======================================================================== + * Copyright (c) 2005-2024 The OPC Foundation, Inc. All rights reserved. + * + * OPC Foundation MIT License 1.00 + * + * Permission is hereby granted, free of charge, to any person + * obtaining a copy of this software and associated documentation + * files (the "Software"), to deal in the Software without + * restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following + * conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES + * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT + * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, + * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR + * OTHER DEALINGS IN THE SOFTWARE. + * + * The complete license agreement can be found here: + * http://opcfoundation.org/License/MIT/1.00/ + * ======================================================================*/ using System; using System.Text; diff --git a/Fuzzing/Encoders/Fuzz/FuzzableCode.cs b/Fuzzing/Encoders/Fuzz/FuzzableCode.cs index 160c8fa313..a99332ddae 100644 --- a/Fuzzing/Encoders/Fuzz/FuzzableCode.cs +++ b/Fuzzing/Encoders/Fuzz/FuzzableCode.cs @@ -1,4 +1,31 @@ - +/* ======================================================================== + * Copyright (c) 2005-2024 The OPC Foundation, Inc. All rights reserved. + * + * OPC Foundation MIT License 1.00 + * + * Permission is hereby granted, free of charge, to any person + * obtaining a copy of this software and associated documentation + * files (the "Software"), to deal in the Software without + * restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following + * conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES + * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT + * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, + * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR + * OTHER DEALINGS IN THE SOFTWARE. + * + * The complete license agreement can be found here: + * http://opcfoundation.org/License/MIT/1.00/ + * ======================================================================*/ using System; using System.IO; diff --git a/Fuzzing/common/Fuzz.Tools/Playback.cs b/Fuzzing/common/Fuzz.Tools/Playback.cs index 7326477b39..5cca480abc 100644 --- a/Fuzzing/common/Fuzz.Tools/Playback.cs +++ b/Fuzzing/common/Fuzz.Tools/Playback.cs @@ -1,3 +1,31 @@ +/* ======================================================================== + * Copyright (c) 2005-2024 The OPC Foundation, Inc. All rights reserved. + * + * OPC Foundation MIT License 1.00 + * + * Permission is hereby granted, free of charge, to any person + * obtaining a copy of this software and associated documentation + * files (the "Software"), to deal in the Software without + * restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following + * conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES + * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT + * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, + * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR + * OTHER DEALINGS IN THE SOFTWARE. + * + * The complete license agreement can be found here: + * http://opcfoundation.org/License/MIT/1.00/ + * ======================================================================*/ using System; using System.Collections.Generic; diff --git a/Fuzzing/common/Fuzz.Tools/Program.cs b/Fuzzing/common/Fuzz.Tools/Program.cs index 57cfff0e31..397282e438 100644 --- a/Fuzzing/common/Fuzz.Tools/Program.cs +++ b/Fuzzing/common/Fuzz.Tools/Program.cs @@ -1,12 +1,44 @@ +/* ======================================================================== + * Copyright (c) 2005-2024 The OPC Foundation, Inc. All rights reserved. + * + * OPC Foundation MIT License 1.00 + * + * Permission is hereby granted, free of charge, to any person + * obtaining a copy of this software and associated documentation + * files (the "Software"), to deal in the Software without + * restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following + * conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES + * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT + * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, + * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR + * OTHER DEALINGS IN THE SOFTWARE. + * + * The complete license agreement can be found here: + * http://opcfoundation.org/License/MIT/1.00/ + * ======================================================================*/ -using System.IO; using System; -using Mono.Options; using System.Collections.Generic; +using System.IO; using Microsoft.Extensions.Logging; +using Mono.Options; public static class Program { + /// + /// Relative folder references for testcases and findings, + /// for when Fuzz.Tools is started from a Visual Studio project. + /// public static readonly string RootFolder = "../../../../"; public static readonly string DefaultTestcasesFolder = RootFolder + "Fuzz/Testcases"; public static readonly string DefaultFindingsCrashFolder = RootFolder + "findings/crashes/"; diff --git a/Fuzzing/common/Fuzz.Tools/Testcases.cs b/Fuzzing/common/Fuzz.Tools/Testcases.cs index 91ed7ff1fd..137b2ec2a9 100644 --- a/Fuzzing/common/Fuzz.Tools/Testcases.cs +++ b/Fuzzing/common/Fuzz.Tools/Testcases.cs @@ -1,6 +1,33 @@ +/* ======================================================================== + * Copyright (c) 2005-2024 The OPC Foundation, Inc. All rights reserved. + * + * OPC Foundation MIT License 1.00 + * + * Permission is hereby granted, free of charge, to any person + * obtaining a copy of this software and associated documentation + * files (the "Software"), to deal in the Software without + * restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following + * conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES + * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT + * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, + * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR + * OTHER DEALINGS IN THE SOFTWARE. + * + * The complete license agreement can be found here: + * http://opcfoundation.org/License/MIT/1.00/ + * ======================================================================*/ using System; -using System.IO; using Opc.Ua; public partial class Testcases diff --git a/Fuzzing/common/Fuzz/FuzzMethods.cs b/Fuzzing/common/Fuzz/FuzzMethods.cs index 96dfc3e24e..8bcd1132fc 100644 --- a/Fuzzing/common/Fuzz/FuzzMethods.cs +++ b/Fuzzing/common/Fuzz/FuzzMethods.cs @@ -1,4 +1,31 @@ - +/* ======================================================================== + * Copyright (c) 2005-2024 The OPC Foundation, Inc. All rights reserved. + * + * OPC Foundation MIT License 1.00 + * + * Permission is hereby granted, free of charge, to any person + * obtaining a copy of this software and associated documentation + * files (the "Software"), to deal in the Software without + * restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following + * conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES + * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT + * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, + * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR + * OTHER DEALINGS IN THE SOFTWARE. + * + * The complete license agreement can be found here: + * http://opcfoundation.org/License/MIT/1.00/ + * ======================================================================*/ using System; using System.Collections.Generic; diff --git a/Fuzzing/common/Fuzz/Program.cs b/Fuzzing/common/Fuzz/Program.cs index bdd43dfedb..65dd11a723 100644 --- a/Fuzzing/common/Fuzz/Program.cs +++ b/Fuzzing/common/Fuzz/Program.cs @@ -1,9 +1,34 @@ - +/* ======================================================================== + * Copyright (c) 2005-2024 The OPC Foundation, Inc. All rights reserved. + * + * OPC Foundation MIT License 1.00 + * + * Permission is hereby granted, free of charge, to any person + * obtaining a copy of this software and associated documentation + * files (the "Software"), to deal in the Software without + * restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following + * conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES + * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT + * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, + * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR + * OTHER DEALINGS IN THE SOFTWARE. + * + * The complete license agreement can be found here: + * http://opcfoundation.org/License/MIT/1.00/ + * ======================================================================*/ using System; using System.IO; -using System.Reflection; -using SharpFuzz; public static class Program { From 659ae6355bdff31574d40287a213c718cc98769e Mon Sep 17 00:00:00 2001 From: Martin Regen Date: Mon, 29 Apr 2024 09:12:41 +0200 Subject: [PATCH 28/83] Update Fuzzing.md fix doc --- Fuzzing/Fuzzing.md | 22 ++++++++++++++-------- 1 file changed, 14 insertions(+), 8 deletions(-) diff --git a/Fuzzing/Fuzzing.md b/Fuzzing/Fuzzing.md index de262f79d3..6440896027 100644 --- a/Fuzzing/Fuzzing.md +++ b/Fuzzing/Fuzzing.md @@ -84,25 +84,23 @@ dotnet tool install --global SharpFuzz.CommandLine ### Usage of libfuzzer on Windows and Linux -To run a fuzz target with libfuzzer on Windows, execute the following commands: +To run a fuzz target with libfuzzer on Windows, from your github cloned project root, execute the following commands: ```cmd -cd BinaryDecoder +cd Fuzzing/Encoders ./libfuzz.bat ``` On Linux, execute the following commands in a bash window: ```bash -cd BinaryDecoder +cd Fuzzing/Encoders ./libfuzz.sh ``` A menu will show up to allow the selection of a fuzzer target function to execute. -Now the fuzzer is started and runs until it hits a crash or timeout or until it is stopped manually by hitting Ctrl-C. Libfuzz writes findings in the current directory with the prefix crash- or timeout. - -To replay the test cases that caused the fuzzer to crash, execute the following command: +Now the fuzzer is started and runs until it hits a crash or timeout or until it is stopped manually by hitting Ctrl-C. Libfuzz writes findings in the current directory with the prefix crash- or timeout-. ## Afl-fuzz @@ -111,7 +109,7 @@ To replay the test cases that caused the fuzzer to crash, execute the following To run the afl-fuzz fuzzing project, execute the following commands: ```bash -cd BinaryDecoder +cd Fuzzing/Encoders ./aflfuzz.sh ``` @@ -119,4 +117,12 @@ A menu will show up to allow the selection of a fuzzer target function to execut Now the fuzzer is started and runs until it is stopped manually by hitting Ctrl-C. The fuzzer will create a directory `findings` in the fuzzer directory, which contains the test cases that caused the fuzzer to crash. -To replay the test cases that caused the fuzzer to crash, execute the following command: +## Replay of crashes and timeouts with Visual Studio + +To replay the test cases that cause the fuzzer to crash or timeout, execute the Encoders.Fuzz.Tools project from Visual Studio with the -p -s option (playback and stacktrace). + +The playback tool tries to find all crashes and timeouts in the default folders and plays back on all libfuzzer fuzz targets. Playing back on the afl-fuzz targets are skipped because they were duplicates. + +## Recreate or improve Testcases with Visual Studio + +To recreate or improve the Testcases, execute the Encoders.Fuzz.Tools project from Visual Studio with the -t option, then all Fuzz/Testcases.* folder content is recreated with latest code. From cb67645b42f0e51272e844eb5065f8feb7211214 Mon Sep 17 00:00:00 2001 From: Martin Regen Date: Mon, 29 Apr 2024 09:21:32 +0200 Subject: [PATCH 29/83] Update Fuzzing.md --- Fuzzing/Fuzzing.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Fuzzing/Fuzzing.md b/Fuzzing/Fuzzing.md index 6440896027..48c87cc22e 100644 --- a/Fuzzing/Fuzzing.md +++ b/Fuzzing/Fuzzing.md @@ -84,6 +84,8 @@ dotnet tool install --global SharpFuzz.CommandLine ### Usage of libfuzzer on Windows and Linux +First download the latest libfuzzer-dotnet release for Ubuntu, Debian or Windows from the [project website](https://github.com/Metalnem/libfuzzer-dotnet/releases) and store it in the same folder where the libfuzz command and shell scripts are started. + To run a fuzz target with libfuzzer on Windows, from your github cloned project root, execute the following commands: ```cmd From 5abf80e9d421ed716cd2b58a17ca3884356244d9 Mon Sep 17 00:00:00 2001 From: Martin Regen Date: Mon, 29 Apr 2024 09:24:23 +0200 Subject: [PATCH 30/83] Update Fuzzing.md --- Fuzzing/Fuzzing.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Fuzzing/Fuzzing.md b/Fuzzing/Fuzzing.md index 48c87cc22e..36a415fcc5 100644 --- a/Fuzzing/Fuzzing.md +++ b/Fuzzing/Fuzzing.md @@ -88,7 +88,8 @@ First download the latest libfuzzer-dotnet release for Ubuntu, Debian or Windows To run a fuzz target with libfuzzer on Windows, from your github cloned project root, execute the following commands: -```cmd +On Windows, execute the following commands in a Powershell window: +```powershell cd Fuzzing/Encoders ./libfuzz.bat ``` From 777b836815da3a438d013f63bb2421d44757c64d Mon Sep 17 00:00:00 2001 From: Martin Regen Date: Thu, 2 May 2024 14:56:26 +0200 Subject: [PATCH 31/83] fix parameter passing, tests --- .../ApplicationConfigurationBuilder.cs | 14 +++ .../IApplicationConfigurationBuilder.cs | 10 ++ .../Stack/Https/HttpsTransportChannel.cs | 2 + .../Stack/Https/HttpsTransportListener.cs | 2 + .../Schema/ApplicationConfiguration.cs | 50 ++++++++-- .../Schema/ApplicationConfiguration.xsd | 2 + .../Opc.Ua.Core/Stack/Bindings/BaseBinding.cs | 2 + .../Configuration/ApplicationConfiguration.cs | 2 + .../Configuration/EndpointConfiguration.cs | 91 ++++++++++++++----- Stack/Opc.Ua.Core/Stack/Tcp/TcpMessageType.cs | 16 +++- .../Stack/Tcp/TcpTransportListener.cs | 2 + .../Stack/Tcp/UaSCBinaryTransportChannel.cs | 35 +++---- .../Types/Utils/DefaultEncodingLimits.cs | 54 +++++++++++ .../Types/Utils/IServiceMessageContext.cs | 6 +- .../Types/Utils/ServiceMessageContext.cs | 24 +++-- .../Types/Encoders/EncoderTests.cs | 61 ++++--------- 16 files changed, 263 insertions(+), 110 deletions(-) create mode 100644 Stack/Opc.Ua.Core/Types/Utils/DefaultEncodingLimits.cs diff --git a/Libraries/Opc.Ua.Configuration/ApplicationConfigurationBuilder.cs b/Libraries/Opc.Ua.Configuration/ApplicationConfigurationBuilder.cs index 90f3d23f19..199fb1a4a1 100644 --- a/Libraries/Opc.Ua.Configuration/ApplicationConfigurationBuilder.cs +++ b/Libraries/Opc.Ua.Configuration/ApplicationConfigurationBuilder.cs @@ -499,6 +499,20 @@ public IApplicationConfigurationBuilderTransportQuotas SetSecurityTokenLifetime( return this; } + /// + public IApplicationConfigurationBuilderTransportQuotas SetMaxEncodingNestingLevels(int maxEncodingNestingLevels) + { + ApplicationConfiguration.TransportQuotas.MaxEncodingNestingLevels = maxEncodingNestingLevels; + return this; + } + + /// + public IApplicationConfigurationBuilderTransportQuotas SetMaxDecoderRecoveries(int maxDecoderRecoveries) + { + ApplicationConfiguration.TransportQuotas.MaxDecoderRecoveries = maxDecoderRecoveries; + return this; + } + /// public IApplicationConfigurationBuilderServerOptions SetMinRequestThreadCount(int minRequestThreadCount) { diff --git a/Libraries/Opc.Ua.Configuration/IApplicationConfigurationBuilder.cs b/Libraries/Opc.Ua.Configuration/IApplicationConfigurationBuilder.cs index b4bbf0440c..37aac555fd 100644 --- a/Libraries/Opc.Ua.Configuration/IApplicationConfigurationBuilder.cs +++ b/Libraries/Opc.Ua.Configuration/IApplicationConfigurationBuilder.cs @@ -103,6 +103,16 @@ public interface IApplicationConfigurationBuilderTransportQuotas : /// The max buffer size. IApplicationConfigurationBuilderTransportQuotas SetMaxBufferSize(int maxBufferSize); + /// + /// applies to + /// + IApplicationConfigurationBuilderTransportQuotas SetMaxEncodingNestingLevels(int maxEncodingNestingLevels); + + /// + /// applies to + /// + IApplicationConfigurationBuilderTransportQuotas SetMaxDecoderRecoveries(int maxDecoderRecoveries); + /// /// applies to /// The lifetime. diff --git a/Stack/Opc.Ua.Bindings.Https/Stack/Https/HttpsTransportChannel.cs b/Stack/Opc.Ua.Bindings.Https/Stack/Https/HttpsTransportChannel.cs index 4ef389e0d8..f7061f1d2d 100644 --- a/Stack/Opc.Ua.Bindings.Https/Stack/Https/HttpsTransportChannel.cs +++ b/Stack/Opc.Ua.Bindings.Https/Stack/Https/HttpsTransportChannel.cs @@ -496,6 +496,8 @@ private void SaveSettings(Uri url, TransportChannelSettings settings) MaxByteStringLength = m_settings.Configuration.MaxByteStringLength, MaxMessageSize = m_settings.Configuration.MaxMessageSize, MaxStringLength = m_settings.Configuration.MaxStringLength, + MaxEncodingNestingLevels = m_settings.Configuration.MaxEncodingNestingLevels, + MaxDecoderRecoveries = m_settings.Configuration.MaxDecoderRecoveries, NamespaceUris = m_settings.NamespaceUris, ServerUris = new StringTable(), Factory = m_settings.Factory diff --git a/Stack/Opc.Ua.Bindings.Https/Stack/Https/HttpsTransportListener.cs b/Stack/Opc.Ua.Bindings.Https/Stack/Https/HttpsTransportListener.cs index 30376c74e1..1f1c79f0ba 100644 --- a/Stack/Opc.Ua.Bindings.Https/Stack/Https/HttpsTransportListener.cs +++ b/Stack/Opc.Ua.Bindings.Https/Stack/Https/HttpsTransportListener.cs @@ -184,6 +184,8 @@ public void Open( MaxByteStringLength = configuration.MaxByteStringLength, MaxMessageSize = configuration.MaxMessageSize, MaxStringLength = configuration.MaxStringLength, + MaxEncodingNestingLevels = configuration.MaxEncodingNestingLevels, + MaxDecoderRecoveries = configuration.MaxDecoderRecoveries, NamespaceUris = settings.NamespaceUris, ServerUris = new StringTable(), Factory = settings.Factory diff --git a/Stack/Opc.Ua.Core/Schema/ApplicationConfiguration.cs b/Stack/Opc.Ua.Core/Schema/ApplicationConfiguration.cs index 831d61c731..621ca78400 100644 --- a/Stack/Opc.Ua.Core/Schema/ApplicationConfiguration.cs +++ b/Stack/Opc.Ua.Core/Schema/ApplicationConfiguration.cs @@ -14,6 +14,7 @@ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. using System.Collections.Generic; using System.Runtime.Serialization; using System.Security.Cryptography.X509Certificates; +using Opc.Ua.Bindings; namespace Opc.Ua { @@ -311,14 +312,19 @@ public TransportQuotas() /// private void Initialize() { - m_operationTimeout = 120000; - m_maxStringLength = 65535; - m_maxByteStringLength = 65535; - m_maxArrayLength = 65535; - m_maxMessageSize = 1048576; - m_maxBufferSize = 65535; - m_channelLifetime = 600000; - m_securityTokenLifetime = 3600000; + // encoding limits + m_maxMessageSize = DefaultEncodingLimits.MaxMessageSize; + m_maxStringLength = DefaultEncodingLimits.MaxStringLength; + m_maxByteStringLength = DefaultEncodingLimits.MaxByteStringLength; + m_maxArrayLength = DefaultEncodingLimits.MaxArrayLength; + m_maxEncodingNestingLevels = DefaultEncodingLimits.MaxEncodingNestingLevels; + m_maxDecoderRecoveries = DefaultEncodingLimits.MaxDecoderRecoveries; + + // message limits + m_maxBufferSize = TcpMessageLimits.DefaultMaxBufferSize; + m_operationTimeout = TcpMessageLimits.DefaultOperationTimeout; + m_channelLifetime = TcpMessageLimits.DefaultChannelLifetime; + m_securityTokenLifetime = TcpMessageLimits.DefaultSecurityTokenLifeTime; } /// @@ -396,11 +402,33 @@ public int MaxBufferSize set { m_maxBufferSize = value; } } + + /// + /// The maximum nesting level accepted while encoding or decoding objects. + /// + [DataMember(IsRequired = false, Order = 6)] + public int MaxEncodingNestingLevels + { + get { return m_maxEncodingNestingLevels; } + set { m_maxEncodingNestingLevels = value; } + } + + /// + /// The number of times the decoder can recover from a decoder error + /// of an IEncodeable before throwing a decoder error. + /// + [DataMember(IsRequired = false, Order = 7)] + public int MaxDecoderRecoveries + { + get { return m_maxDecoderRecoveries; } + set { m_maxDecoderRecoveries = value; } + } + /// /// The lifetime of a secure channel (in milliseconds). /// /// The channel lifetime. - [DataMember(IsRequired = false, Order = 6)] + [DataMember(IsRequired = false, Order = 8)] public int ChannelLifetime { get { return m_channelLifetime; } @@ -411,7 +439,7 @@ public int ChannelLifetime /// The lifetime of a security token (in milliseconds). /// /// The security token lifetime. - [DataMember(IsRequired = false, Order = 7)] + [DataMember(IsRequired = false, Order = 9)] public int SecurityTokenLifetime { get { return m_securityTokenLifetime; } @@ -428,6 +456,8 @@ public int SecurityTokenLifetime private int m_maxBufferSize; private int m_channelLifetime; private int m_securityTokenLifetime; + private int m_maxEncodingNestingLevels; + private int m_maxDecoderRecoveries; #endregion } #endregion diff --git a/Stack/Opc.Ua.Core/Schema/ApplicationConfiguration.xsd b/Stack/Opc.Ua.Core/Schema/ApplicationConfiguration.xsd index 2e2ddf6da2..3557ebb9a6 100644 --- a/Stack/Opc.Ua.Core/Schema/ApplicationConfiguration.xsd +++ b/Stack/Opc.Ua.Core/Schema/ApplicationConfiguration.xsd @@ -83,6 +83,8 @@ + + diff --git a/Stack/Opc.Ua.Core/Stack/Bindings/BaseBinding.cs b/Stack/Opc.Ua.Core/Stack/Bindings/BaseBinding.cs index 12b0d9e971..3cdecdadfa 100644 --- a/Stack/Opc.Ua.Core/Stack/Bindings/BaseBinding.cs +++ b/Stack/Opc.Ua.Core/Stack/Bindings/BaseBinding.cs @@ -31,6 +31,8 @@ protected BaseBinding( MaxByteStringLength = configuration.MaxByteStringLength, MaxArrayLength = configuration.MaxArrayLength, MaxMessageSize = configuration.MaxMessageSize, + MaxEncodingNestingLevels = configuration.MaxEncodingNestingLevels, + MaxDecoderRecoveries = configuration.MaxDecoderRecoveries, Factory = factory, NamespaceUris = namespaceUris }; diff --git a/Stack/Opc.Ua.Core/Stack/Configuration/ApplicationConfiguration.cs b/Stack/Opc.Ua.Core/Stack/Configuration/ApplicationConfiguration.cs index 0d46c7ebba..9ee2696ad3 100644 --- a/Stack/Opc.Ua.Core/Stack/Configuration/ApplicationConfiguration.cs +++ b/Stack/Opc.Ua.Core/Stack/Configuration/ApplicationConfiguration.cs @@ -181,6 +181,8 @@ public ServiceMessageContext CreateMessageContext(bool clonedFactory = false) messageContext.MaxByteStringLength = m_transportQuotas.MaxByteStringLength; messageContext.MaxStringLength = m_transportQuotas.MaxStringLength; messageContext.MaxMessageSize = m_transportQuotas.MaxMessageSize; + messageContext.MaxEncodingNestingLevels = m_transportQuotas.MaxEncodingNestingLevels; + messageContext.MaxDecoderRecoveries = m_transportQuotas.MaxDecoderRecoveries; } messageContext.NamespaceUris = new NamespaceTable(); diff --git a/Stack/Opc.Ua.Core/Stack/Configuration/EndpointConfiguration.cs b/Stack/Opc.Ua.Core/Stack/Configuration/EndpointConfiguration.cs index 425b70189d..76064cfa69 100644 --- a/Stack/Opc.Ua.Core/Stack/Configuration/EndpointConfiguration.cs +++ b/Stack/Opc.Ua.Core/Stack/Configuration/EndpointConfiguration.cs @@ -11,6 +11,7 @@ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. */ using System; +using Opc.Ua.Bindings; namespace Opc.Ua { @@ -25,17 +26,22 @@ public partial class EndpointConfiguration /// public static EndpointConfiguration Create() { - EndpointConfiguration configuration = new EndpointConfiguration(); + EndpointConfiguration configuration = new EndpointConfiguration { + // message defaults + OperationTimeout = TcpMessageLimits.DefaultOperationTimeout, + UseBinaryEncoding = true, + MaxMessageSize = TcpMessageLimits.DefaultMaxMessageSize, + MaxBufferSize = TcpMessageLimits.DefaultMaxBufferSize, + ChannelLifetime = TcpMessageLimits.DefaultChannelLifetime, + SecurityTokenLifetime = TcpMessageLimits.DefaultSecurityTokenLifeTime, - configuration.OperationTimeout = 120000; - configuration.UseBinaryEncoding = true; - configuration.MaxArrayLength = UInt16.MaxValue; - configuration.MaxByteStringLength = UInt16.MaxValue * 16; - configuration.MaxMessageSize = UInt16.MaxValue * 64; - configuration.MaxStringLength = UInt16.MaxValue; - configuration.MaxBufferSize = UInt16.MaxValue; - configuration.ChannelLifetime = 120000; - configuration.SecurityTokenLifetime = 3600000; + // encoding defaults + MaxArrayLength = DefaultEncodingLimits.MaxArrayLength, + MaxByteStringLength = DefaultEncodingLimits.MaxByteStringLength, + MaxStringLength = DefaultEncodingLimits.MaxStringLength, + MaxEncodingNestingLevels = DefaultEncodingLimits.MaxEncodingNestingLevels, + MaxDecoderRecoveries = DefaultEncodingLimits.MaxDecoderRecoveries, + }; return configuration; } @@ -50,20 +56,63 @@ public static EndpointConfiguration Create(ApplicationConfiguration applicationC return Create(); } - EndpointConfiguration configuration = new EndpointConfiguration(); - - configuration.OperationTimeout = applicationConfiguration.TransportQuotas.OperationTimeout; - configuration.UseBinaryEncoding = true; - configuration.MaxArrayLength = applicationConfiguration.TransportQuotas.MaxArrayLength; - configuration.MaxByteStringLength = applicationConfiguration.TransportQuotas.MaxByteStringLength; - configuration.MaxMessageSize = applicationConfiguration.TransportQuotas.MaxMessageSize; - configuration.MaxStringLength = applicationConfiguration.TransportQuotas.MaxStringLength; - configuration.MaxBufferSize = applicationConfiguration.TransportQuotas.MaxBufferSize; - configuration.ChannelLifetime = applicationConfiguration.TransportQuotas.ChannelLifetime; - configuration.SecurityTokenLifetime = applicationConfiguration.TransportQuotas.SecurityTokenLifetime; + EndpointConfiguration configuration = new EndpointConfiguration { + OperationTimeout = applicationConfiguration.TransportQuotas.OperationTimeout, + UseBinaryEncoding = true, + MaxArrayLength = applicationConfiguration.TransportQuotas.MaxArrayLength, + MaxByteStringLength = applicationConfiguration.TransportQuotas.MaxByteStringLength, + MaxMessageSize = applicationConfiguration.TransportQuotas.MaxMessageSize, + MaxStringLength = applicationConfiguration.TransportQuotas.MaxStringLength, + MaxBufferSize = applicationConfiguration.TransportQuotas.MaxBufferSize, + MaxEncodingNestingLevels = applicationConfiguration.TransportQuotas.MaxEncodingNestingLevels, + MaxDecoderRecoveries = applicationConfiguration.TransportQuotas.MaxDecoderRecoveries, + ChannelLifetime = applicationConfiguration.TransportQuotas.ChannelLifetime, + SecurityTokenLifetime = applicationConfiguration.TransportQuotas.SecurityTokenLifetime + }; return configuration; } #endregion + + #region Public Properties + /// + /// The maximum nesting level accepted while encoding or decoding objects. + /// + public int MaxEncodingNestingLevels + { + get + { + return m_maxEncodingNestingLevels; + } + + set + { + m_maxEncodingNestingLevels = value; + } + } + + /// + /// The number of times the decoder can recover from an error + /// caused by an encoded ExtensionObject before throwing a decoder error. + /// + public int MaxDecoderRecoveries + { + get + { + return m_maxDecoderRecoveries; + } + + set + { + m_maxDecoderRecoveries = value; + } + } + #endregion + + #region Private Fields + int m_maxEncodingNestingLevels; + int m_maxDecoderRecoveries; + #endregion + } } diff --git a/Stack/Opc.Ua.Core/Stack/Tcp/TcpMessageType.cs b/Stack/Opc.Ua.Core/Stack/Tcp/TcpMessageType.cs index 602d9aeafe..003de5b5d2 100644 --- a/Stack/Opc.Ua.Core/Stack/Tcp/TcpMessageType.cs +++ b/Stack/Opc.Ua.Core/Stack/Tcp/TcpMessageType.cs @@ -237,16 +237,19 @@ public static class TcpMessageLimits /// /// The default buffer size to use for communication. /// - public const int DefaultMaxBufferSize = 65535; + public const int DefaultMaxBufferSize = UInt16.MaxValue; /// /// The default maximum chunk count for Request and Response messages. /// - public const int DefaultMaxChunkCount = 16; + public const int DefaultMaxChunkCount = 32; /// /// The default maximum message size. /// + /// + /// This default is for the Tcp transport. for the generic default. + /// public const int DefaultMaxMessageSize = DefaultMaxChunkCount * DefaultMaxBufferSize; /// @@ -255,9 +258,14 @@ public static class TcpMessageLimits public const int DefaultDiscoveryMaxMessageSize = DefaultMaxBufferSize; /// - /// How long a connection will remain in the server after it goes into a faulted state. + /// How long processing of a service call can take before it goes into a faulted state. /// - public const int DefaultChannelLifetime = 60000; + public const int DefaultOperationTimeout = 120000; + + /// + /// How long a secure channel will remain in the server after it goes into a faulted state. + /// + public const int DefaultChannelLifetime = 30000; /// /// How long a security token lasts before it needs to be renewed. diff --git a/Stack/Opc.Ua.Core/Stack/Tcp/TcpTransportListener.cs b/Stack/Opc.Ua.Core/Stack/Tcp/TcpTransportListener.cs index 8868d17531..4f76e1e725 100644 --- a/Stack/Opc.Ua.Core/Stack/Tcp/TcpTransportListener.cs +++ b/Stack/Opc.Ua.Core/Stack/Tcp/TcpTransportListener.cs @@ -145,6 +145,8 @@ public void Open( messageContext.MaxByteStringLength = configuration.MaxByteStringLength; messageContext.MaxMessageSize = configuration.MaxMessageSize; messageContext.MaxStringLength = configuration.MaxStringLength; + messageContext.MaxEncodingNestingLevels = configuration.MaxEncodingNestingLevels; + messageContext.MaxDecoderRecoveries = configuration.MaxDecoderRecoveries; } m_quotas.MessageContext = messageContext; diff --git a/Stack/Opc.Ua.Core/Stack/Tcp/UaSCBinaryTransportChannel.cs b/Stack/Opc.Ua.Core/Stack/Tcp/UaSCBinaryTransportChannel.cs index c4357525d6..c002e7dbd7 100644 --- a/Stack/Opc.Ua.Core/Stack/Tcp/UaSCBinaryTransportChannel.cs +++ b/Stack/Opc.Ua.Core/Stack/Tcp/UaSCBinaryTransportChannel.cs @@ -420,24 +420,27 @@ private void SaveSettings(Uri url, TransportChannelSettings settings) m_operationTimeout = settings.Configuration.OperationTimeout; // initialize the quotas. - m_quotas = new ChannelQuotas(); - - m_quotas.MaxBufferSize = m_settings.Configuration.MaxBufferSize; - m_quotas.MaxMessageSize = m_settings.Configuration.MaxMessageSize; - m_quotas.ChannelLifetime = m_settings.Configuration.ChannelLifetime; - m_quotas.SecurityTokenLifetime = m_settings.Configuration.SecurityTokenLifetime; - m_quotas.MessageContext = new ServiceMessageContext() { - MaxArrayLength = m_settings.Configuration.MaxArrayLength, - MaxByteStringLength = m_settings.Configuration.MaxByteStringLength, - MaxMessageSize = m_settings.Configuration.MaxMessageSize, - MaxStringLength = m_settings.Configuration.MaxStringLength, - NamespaceUris = m_settings.NamespaceUris, - ServerUris = new StringTable(), - Factory = m_settings.Factory + EndpointConfiguration configuration = m_settings.Configuration; + m_quotas = new ChannelQuotas { + MaxBufferSize = configuration.MaxBufferSize, + MaxMessageSize = configuration.MaxMessageSize, + ChannelLifetime = configuration.ChannelLifetime, + SecurityTokenLifetime = configuration.SecurityTokenLifetime, + MessageContext = new ServiceMessageContext() { + MaxArrayLength = configuration.MaxArrayLength, + MaxByteStringLength = configuration.MaxByteStringLength, + MaxMessageSize = configuration.MaxMessageSize, + MaxStringLength = configuration.MaxStringLength, + MaxEncodingNestingLevels = configuration.MaxEncodingNestingLevels, + MaxDecoderRecoveries = configuration.MaxDecoderRecoveries, + NamespaceUris = m_settings.NamespaceUris, + ServerUris = new StringTable(), + Factory = m_settings.Factory + }, + + CertificateValidator = settings.CertificateValidator }; - m_quotas.CertificateValidator = settings.CertificateValidator; - // create the buffer manager. m_bufferManager = new BufferManager("Client", settings.Configuration.MaxBufferSize); } diff --git a/Stack/Opc.Ua.Core/Types/Utils/DefaultEncodingLimits.cs b/Stack/Opc.Ua.Core/Types/Utils/DefaultEncodingLimits.cs new file mode 100644 index 0000000000..1fe0904f85 --- /dev/null +++ b/Stack/Opc.Ua.Core/Types/Utils/DefaultEncodingLimits.cs @@ -0,0 +1,54 @@ +/* Copyright (c) 1996-2024 The OPC Foundation. All rights reserved. + The source code in this file is covered under a dual-license scenario: + - RCL: for OPC Foundation Corporate Members in good-standing + - GPL V2: everybody else + RCL license terms accompanied with this source code. See http://opcfoundation.org/License/RCL/1.00/ + GNU General Public License as published by the Free Software Foundation; + version 2 of the License are accompanied with this source code. See http://opcfoundation.org/License/GPLv2 + This source code is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. +*/ + +using System; + +namespace Opc.Ua +{ + /// + /// Defaults for encoders while encoding and decoding messages. + /// Passed to encoders in . + /// + public static class DefaultEncodingLimits + { + /// + /// The maximum length for any string, byte string or xml element. + /// + public static readonly int MaxStringLength = UInt16.MaxValue; + + /// + /// The maximum length for any array. + /// + public static readonly int MaxArrayLength = UInt16.MaxValue; + + /// + /// The maximum length for any ByteString. + /// + public static readonly int MaxByteStringLength = UInt16.MaxValue * 16; + + /// + /// The maximum length for any Message. + /// + public static readonly int MaxMessageSize = UInt16.MaxValue * 32; + + /// + /// The maximum nesting level accepted while encoding or decoding objects. + /// + public static readonly int MaxEncodingNestingLevels = 200; + + /// + /// The number of times the decoder can recover from an error + /// caused by an encoded ExtensionObject before throwing a decoder error. + /// + public static readonly int MaxDecoderRecoveries = 0; + } +} diff --git a/Stack/Opc.Ua.Core/Types/Utils/IServiceMessageContext.cs b/Stack/Opc.Ua.Core/Types/Utils/IServiceMessageContext.cs index dec65c2219..2a0cc0a1d2 100644 --- a/Stack/Opc.Ua.Core/Types/Utils/IServiceMessageContext.cs +++ b/Stack/Opc.Ua.Core/Types/Utils/IServiceMessageContext.cs @@ -43,13 +43,13 @@ public interface IServiceMessageContext /// /// The maximum nesting level accepted while encoding or decoding objects. /// - uint MaxEncodingNestingLevels { get; } + int MaxEncodingNestingLevels { get; } /// /// The number of times the decoder can recover from an error - /// caused by a custom complex type before throwing an exception. + /// caused by an encoded ExtensionObject before throwing a decoder error. /// - uint MaxDecoderRecoveries { get; } + int MaxDecoderRecoveries { get; } /// /// The table of namespaces used by the server. diff --git a/Stack/Opc.Ua.Core/Types/Utils/ServiceMessageContext.cs b/Stack/Opc.Ua.Core/Types/Utils/ServiceMessageContext.cs index e5aa76b632..c1874691a4 100644 --- a/Stack/Opc.Ua.Core/Types/Utils/ServiceMessageContext.cs +++ b/Stack/Opc.Ua.Core/Types/Utils/ServiceMessageContext.cs @@ -10,12 +10,10 @@ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. */ -using System; - namespace Opc.Ua { /// - /// Stores context information associated with a UA server that is used during message processing. + /// Stores context information associated with a session is used during message processing. /// public class ServiceMessageContext : IServiceMessageContext { @@ -35,15 +33,15 @@ private ServiceMessageContext(bool shared) : this() private void Initialize(bool shared) { - m_maxStringLength = UInt16.MaxValue; - m_maxByteStringLength = UInt16.MaxValue * 16; - m_maxArrayLength = UInt16.MaxValue; - m_maxMessageSize = UInt16.MaxValue * 32; + m_maxStringLength = DefaultEncodingLimits.MaxStringLength; + m_maxByteStringLength = DefaultEncodingLimits.MaxByteStringLength; + m_maxArrayLength = DefaultEncodingLimits.MaxArrayLength; + m_maxMessageSize = DefaultEncodingLimits.MaxMessageSize; + m_maxEncodingNestingLevels = DefaultEncodingLimits.MaxEncodingNestingLevels; + m_maxDecoderRecoveries = DefaultEncodingLimits.MaxDecoderRecoveries; m_namespaceUris = new NamespaceTable(shared); m_serverUris = new StringTable(shared); m_factory = EncodeableFactory.GlobalFactory; - m_maxEncodingNestingLevels = 200; - m_maxDecoderRecoveries = 0; } #endregion @@ -97,14 +95,14 @@ public int MaxMessageSize } /// - public uint MaxEncodingNestingLevels + public int MaxEncodingNestingLevels { get => m_maxEncodingNestingLevels; set { m_maxEncodingNestingLevels = value; } } /// - public uint MaxDecoderRecoveries + public int MaxDecoderRecoveries { get => m_maxDecoderRecoveries; set { m_maxDecoderRecoveries = value; } @@ -166,8 +164,8 @@ public IEncodeableFactory Factory private int m_maxByteStringLength; private int m_maxArrayLength; private int m_maxMessageSize; - private uint m_maxEncodingNestingLevels; - private uint m_maxDecoderRecoveries; + private int m_maxEncodingNestingLevels; + private int m_maxDecoderRecoveries; private NamespaceTable m_namespaceUris; private StringTable m_serverUris; private IEncodeableFactory m_factory; diff --git a/Tests/Opc.Ua.Core.Tests/Types/Encoders/EncoderTests.cs b/Tests/Opc.Ua.Core.Tests/Types/Encoders/EncoderTests.cs index defd31f398..32bb28eef6 100644 --- a/Tests/Opc.Ua.Core.Tests/Types/Encoders/EncoderTests.cs +++ b/Tests/Opc.Ua.Core.Tests/Types/Encoders/EncoderTests.cs @@ -526,17 +526,18 @@ BuiltInType builtInType using (var decoderStream = new MemoryStream(buffer)) using (IDecoder decoder = CreateDecoder(encoderType, Context, decoderStream, typeof(DataValue))) { + ServiceResultException sre = null; switch (encoderType) { case EncodingType.Json: { // check such matrix cannot be initialized when decoding from Json format // the exception is thrown while trying to construct the Matrix - Assert.Throws( - typeof(ArgumentException), + sre = Assert.Throws( () => { decoder.ReadDataValue("DataValue"); }); + Assert.AreEqual((StatusCode)StatusCodes.BadEncodingLimitsExceeded, (StatusCode)sre.StatusCode, sre.Message); break; } case EncodingType.Xml: @@ -550,11 +551,11 @@ BuiltInType builtInType { // check such matrix cannot be initialized when decoding from Binary format // the exception is thrown before trying to construct the Matrix - Assert.Throws( - typeof(ServiceResultException), + sre = Assert.Throws( () => { decoder.ReadDataValue("DataValue"); }); + Assert.AreEqual((StatusCode)StatusCodes.BadDecodingError, (StatusCode)sre.StatusCode, sre.Message); break; } } @@ -623,8 +624,7 @@ BuiltInType builtInType { // check such matrix cannot be initialized when decoding from Json format // the exception is thrown while trying to construct the Matrix - Assert.Throws( - typeof(ArgumentException), + Assert.Throws( () => { decoder.ReadDataValue("DataValue"); }); @@ -641,8 +641,7 @@ BuiltInType builtInType { // check such matrix cannot be initialized when decoding from Binary format // the exception is thrown before trying to construct the Matrix - Assert.Throws( - typeof(ServiceResultException), + Assert.Throws( () => { decoder.ReadDataValue("DataValue"); }); @@ -723,42 +722,18 @@ BuiltInType builtInType using (var decoderStream = new MemoryStream(buffer)) using (IDecoder decoder = CreateDecoder(encoderType, Context, decoderStream, type)) { - switch (encoderType) + ServiceResultException sre = Assert.Throws( + () => { + decoder.ReadArray(builtInType.ToString(), matrix.TypeInfo.ValueRank, builtInType); + }); + + if (encoderType == EncodingType.Json) { - case EncodingType.Json: - { - // If this would execute: - // check such matrix cannot be initialized when decoding from Json format - // the exception is thrown while trying to construct the Matrix - Assert.Throws( - typeof(ServiceResultException), - () => { - decoder.ReadArray(builtInType.ToString(), matrix.TypeInfo.ValueRank, builtInType); - }); - break; - } - case EncodingType.Xml: - { - // check such matrix cannot be initialized when decoding from Xml format - // the exception is thrown while trying to construct the Matrix but is caught and handled - Assert.Throws( - typeof(ArgumentException), - () => { - decoder.ReadArray(builtInType.ToString(), matrix.TypeInfo.ValueRank, builtInType); - }); - break; - } - case EncodingType.Binary: - { - // check such matrix cannot be initialized when decoding from Binary format - // the exception is thrown before trying to construct the Matrix - Assert.Throws( - typeof(ServiceResultException), - () => { - decoder.ReadArray(builtInType.ToString(), matrix.TypeInfo.ValueRank, builtInType); - }); - break; - } + Assert.AreEqual((StatusCode)StatusCodes.BadEncodingLimitsExceeded, (StatusCode)sre.StatusCode, sre.Message); + } + else + { + Assert.AreEqual((StatusCode)StatusCodes.BadDecodingError, (StatusCode)sre.StatusCode, sre.Message); } } } From e869f05d30d5a009ad62b8efc71675783ab6b748 Mon Sep 17 00:00:00 2001 From: Martin Regen Date: Thu, 2 May 2024 18:47:56 +0200 Subject: [PATCH 32/83] fix tests --- .../Types/Encoders/BinaryDecoder.cs | 2 +- .../Opc.Ua.Core/Types/Encoders/JsonDecoder.cs | 18 +++- .../Opc.Ua.Core/Types/Encoders/XmlDecoder.cs | 20 +++-- .../Types/Encoders/EncoderTests.cs | 83 ++++--------------- 4 files changed, 45 insertions(+), 78 deletions(-) diff --git a/Stack/Opc.Ua.Core/Types/Encoders/BinaryDecoder.cs b/Stack/Opc.Ua.Core/Types/Encoders/BinaryDecoder.cs index cbe384b1ba..fa8b385fc0 100644 --- a/Stack/Opc.Ua.Core/Types/Encoders/BinaryDecoder.cs +++ b/Stack/Opc.Ua.Core/Types/Encoders/BinaryDecoder.cs @@ -2246,7 +2246,7 @@ private Variant ReadVariantValue(string fieldName) if (array == null) { - value = new Variant(StatusCodes.BadDecodingError); + value = new Variant((StatusCode)StatusCodes.BadDecodingError); } else { diff --git a/Stack/Opc.Ua.Core/Types/Encoders/JsonDecoder.cs b/Stack/Opc.Ua.Core/Types/Encoders/JsonDecoder.cs index ab412f537e..83f6220a85 100644 --- a/Stack/Opc.Ua.Core/Types/Encoders/JsonDecoder.cs +++ b/Stack/Opc.Ua.Core/Types/Encoders/JsonDecoder.cs @@ -1227,11 +1227,23 @@ public Variant ReadVariant(string fieldName) { return ReadVariantBody("Body", type); } - var dimensions = ReadInt32Array("Dimensions"); - if (array.Value is Array && dimensions != null && dimensions.Count > 1) + Int32Collection dimensions = ReadInt32Array("Dimensions"); + + if (array.Value is Array arrayValue && dimensions != null && dimensions.Count > 1) { - array = new Variant(new Matrix((Array)array.Value, type, dimensions.ToArray())); + int length = arrayValue.Length; + var dimensionsArray = dimensions.ToArray(); + (bool valid, int matrixLength) = Matrix.ValidateDimensions(dimensionsArray, length, Context.MaxArrayLength); + + if (!valid || (matrixLength != length)) + { + throw ServiceResultException.Create(StatusCodes.BadDecodingError, + "ArrayDimensions length does not match with the ArrayLength in Variant object."); + } + + array = new Variant(new Matrix(arrayValue, type, dimensionsArray)); } + return array; } finally diff --git a/Stack/Opc.Ua.Core/Types/Encoders/XmlDecoder.cs b/Stack/Opc.Ua.Core/Types/Encoders/XmlDecoder.cs index 9d1576c4b1..22c8f04cd6 100644 --- a/Stack/Opc.Ua.Core/Types/Encoders/XmlDecoder.cs +++ b/Stack/Opc.Ua.Core/Types/Encoders/XmlDecoder.cs @@ -1237,10 +1237,10 @@ public Variant ReadVariant(string fieldName) object contents = ReadVariantContents(out typeInfo); value = new Variant(contents, typeInfo); } - catch (Exception ex) + catch (Exception ex) when (!(ex is ServiceResultException)) { - Utils.LogError(ex, "XmlDecoder: Error reading variant."); - value = new Variant(StatusCodes.BadDecodingError); + Utils.LogError(ex, "XmlDecoder: Error reading variant. {0}", ex.Message); + value = new Variant((StatusCode)StatusCodes.BadDecodingError); } EndField("Value"); } @@ -2619,7 +2619,7 @@ private DiagnosticInfo ReadDiagnosticInfo(int depth) } /// - /// Reads an Matrix from the stream. + /// Reads a Matrix from the stream. /// private Matrix ReadMatrix(string fieldName) { @@ -2656,7 +2656,17 @@ private Matrix ReadMatrix(string fieldName) if (dimensions != null && dimensions.Count > 0) { - return new Matrix(elements, typeInfo.BuiltInType, dimensions.ToArray()); + int length = elements.Length; + var dimensionsArray = dimensions.ToArray(); + (bool valid, int matrixLength) = Matrix.ValidateDimensions(dimensionsArray, length, Context.MaxArrayLength); + + if (!valid || (matrixLength != length)) + { + throw ServiceResultException.Create(StatusCodes.BadDecodingError, + "ArrayDimensions length does not match with the ArrayLength in Variant object."); + } + + return new Matrix(elements, typeInfo.BuiltInType, dimensionsArray); } return new Matrix(elements, typeInfo.BuiltInType); diff --git a/Tests/Opc.Ua.Core.Tests/Types/Encoders/EncoderTests.cs b/Tests/Opc.Ua.Core.Tests/Types/Encoders/EncoderTests.cs index 32bb28eef6..4dd03960cb 100644 --- a/Tests/Opc.Ua.Core.Tests/Types/Encoders/EncoderTests.cs +++ b/Tests/Opc.Ua.Core.Tests/Types/Encoders/EncoderTests.cs @@ -526,39 +526,13 @@ BuiltInType builtInType using (var decoderStream = new MemoryStream(buffer)) using (IDecoder decoder = CreateDecoder(encoderType, Context, decoderStream, typeof(DataValue))) { - ServiceResultException sre = null; - switch (encoderType) - { - case EncodingType.Json: - { - // check such matrix cannot be initialized when decoding from Json format - // the exception is thrown while trying to construct the Matrix - sre = Assert.Throws( - () => { - decoder.ReadDataValue("DataValue"); - }); - Assert.AreEqual((StatusCode)StatusCodes.BadEncodingLimitsExceeded, (StatusCode)sre.StatusCode, sre.Message); - break; - } - case EncodingType.Xml: - { - // check such matrix cannot be initialized when decoding from Xml format - // the exception is thrown while trying to construct the Matrix but is caught and handled + // check such matrix cannot be initialized when decoding from Binary format + // the exception is thrown before trying to construct the Matrix + ServiceResultException sre = Assert.Throws( + () => { decoder.ReadDataValue("DataValue"); - break; - } - case EncodingType.Binary: - { - // check such matrix cannot be initialized when decoding from Binary format - // the exception is thrown before trying to construct the Matrix - sre = Assert.Throws( - () => { - decoder.ReadDataValue("DataValue"); - }); - Assert.AreEqual((StatusCode)StatusCodes.BadDecodingError, (StatusCode)sre.StatusCode, sre.Message); - break; - } - } + }); + Assert.AreEqual((StatusCode)StatusCodes.BadDecodingError, (StatusCode)sre.StatusCode, sre.Message); } } @@ -618,36 +592,14 @@ BuiltInType builtInType using (var decoderStream = new MemoryStream(buffer)) using (IDecoder decoder = CreateDecoder(encoderType, Context, decoderStream, typeof(DataValue))) { - switch (encoderType) - { - case EncodingType.Json: - { - // check such matrix cannot be initialized when decoding from Json format - // the exception is thrown while trying to construct the Matrix - Assert.Throws( - () => { - decoder.ReadDataValue("DataValue"); - }); - break; - } - case EncodingType.Xml: - { - // check such matrix cannot be initialized when decoding from Xml format - // the exception is thrown while trying to construct the Matrix but is caught and handled + // check such matrix cannot be initialized when decoding from Json format + // the exception is thrown while trying to construct the Matrix + var sre = Assert.Throws( + () => { decoder.ReadDataValue("DataValue"); - break; - } - case EncodingType.Binary: - { - // check such matrix cannot be initialized when decoding from Binary format - // the exception is thrown before trying to construct the Matrix - Assert.Throws( - () => { - decoder.ReadDataValue("DataValue"); - }); - break; - } - } + }); + + Assert.AreEqual((StatusCode)StatusCodes.BadDecodingError, (StatusCode)sre.StatusCode, sre.Message); } } @@ -727,14 +679,7 @@ BuiltInType builtInType decoder.ReadArray(builtInType.ToString(), matrix.TypeInfo.ValueRank, builtInType); }); - if (encoderType == EncodingType.Json) - { - Assert.AreEqual((StatusCode)StatusCodes.BadEncodingLimitsExceeded, (StatusCode)sre.StatusCode, sre.Message); - } - else - { - Assert.AreEqual((StatusCode)StatusCodes.BadDecodingError, (StatusCode)sre.StatusCode, sre.Message); - } + Assert.AreEqual((StatusCode)StatusCodes.BadEncodingLimitsExceeded, (StatusCode)sre.StatusCode, sre.Message); } } #endregion From cfdcc88a213164b92d17af0355031c68ca2fd7c2 Mon Sep 17 00:00:00 2001 From: Martin Regen Date: Thu, 2 May 2024 21:02:50 +0200 Subject: [PATCH 33/83] fix reconnect test --- .../Stack/Configuration/EndpointConfiguration.cs | 6 ++++-- Stack/Opc.Ua.Core/Stack/State/NodeState.cs | 10 +++++----- Stack/Opc.Ua.Core/Stack/State/NodeStateCollection.cs | 10 +++++----- Stack/Opc.Ua.Core/Types/Encoders/BinaryDecoder.cs | 1 - 4 files changed, 14 insertions(+), 13 deletions(-) diff --git a/Stack/Opc.Ua.Core/Stack/Configuration/EndpointConfiguration.cs b/Stack/Opc.Ua.Core/Stack/Configuration/EndpointConfiguration.cs index 76064cfa69..9a95f4bcda 100644 --- a/Stack/Opc.Ua.Core/Stack/Configuration/EndpointConfiguration.cs +++ b/Stack/Opc.Ua.Core/Stack/Configuration/EndpointConfiguration.cs @@ -10,7 +10,7 @@ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. */ -using System; +using System.Runtime.Serialization; using Opc.Ua.Bindings; namespace Opc.Ua @@ -78,11 +78,12 @@ public static EndpointConfiguration Create(ApplicationConfiguration applicationC /// /// The maximum nesting level accepted while encoding or decoding objects. /// + [DataMember(Name = "MaxEncodingNestingLevels", IsRequired = false, Order = 20)] public int MaxEncodingNestingLevels { get { - return m_maxEncodingNestingLevels; + return m_maxEncodingNestingLevels <= 0 ? DefaultEncodingLimits.MaxEncodingNestingLevels : m_maxEncodingNestingLevels; } set @@ -95,6 +96,7 @@ public int MaxEncodingNestingLevels /// The number of times the decoder can recover from an error /// caused by an encoded ExtensionObject before throwing a decoder error. /// + [DataMember(Name = "MaxDecoderRecoveries", IsRequired = false, Order = 21)] public int MaxDecoderRecoveries { get diff --git a/Stack/Opc.Ua.Core/Stack/State/NodeState.cs b/Stack/Opc.Ua.Core/Stack/State/NodeState.cs index 129afb3f46..828bde7829 100644 --- a/Stack/Opc.Ua.Core/Stack/State/NodeState.cs +++ b/Stack/Opc.Ua.Core/Stack/State/NodeState.cs @@ -644,11 +644,11 @@ public void SaveAsBinary(ISystemContext context, BinaryEncoder encoder) /// The stream to read. public void LoadAsBinary(ISystemContext context, Stream istrm) { - ServiceMessageContext messageContext = new ServiceMessageContext(); - - messageContext.NamespaceUris = context.NamespaceUris; - messageContext.ServerUris = context.ServerUris; - messageContext.Factory = context.EncodeableFactory; + ServiceMessageContext messageContext = new ServiceMessageContext { + NamespaceUris = context.NamespaceUris, + ServerUris = context.ServerUris, + Factory = context.EncodeableFactory + }; using (var decoder = new BinaryDecoder(istrm, messageContext, true)) { diff --git a/Stack/Opc.Ua.Core/Stack/State/NodeStateCollection.cs b/Stack/Opc.Ua.Core/Stack/State/NodeStateCollection.cs index efb55290cd..678a3f2316 100644 --- a/Stack/Opc.Ua.Core/Stack/State/NodeStateCollection.cs +++ b/Stack/Opc.Ua.Core/Stack/State/NodeStateCollection.cs @@ -259,11 +259,11 @@ public void SaveAsBinary(ISystemContext context, Stream ostrm) /// public void LoadFromBinary(ISystemContext context, Stream istrm, bool updateTables) { - ServiceMessageContext messageContext = new ServiceMessageContext(); - - messageContext.NamespaceUris = context.NamespaceUris; - messageContext.ServerUris = context.ServerUris; - messageContext.Factory = context.EncodeableFactory; + ServiceMessageContext messageContext = new ServiceMessageContext { + NamespaceUris = context.NamespaceUris, + ServerUris = context.ServerUris, + Factory = context.EncodeableFactory + }; using (var decoder = new BinaryDecoder(istrm, messageContext)) { diff --git a/Stack/Opc.Ua.Core/Types/Encoders/BinaryDecoder.cs b/Stack/Opc.Ua.Core/Types/Encoders/BinaryDecoder.cs index fa8b385fc0..6d6bd73b77 100644 --- a/Stack/Opc.Ua.Core/Types/Encoders/BinaryDecoder.cs +++ b/Stack/Opc.Ua.Core/Types/Encoders/BinaryDecoder.cs @@ -59,7 +59,6 @@ public BinaryDecoder(Stream stream, IServiceMessageContext context, bool leaveOp ValidateStreamRequirements(stream); m_reader = new BinaryReader(stream, Encoding.UTF8, leaveOpen); Initialize(context); - } /// From e8350865a8956368fb991deedbf3944d12b33cfd Mon Sep 17 00:00:00 2001 From: Martin Regen Date: Thu, 2 May 2024 21:11:04 +0200 Subject: [PATCH 34/83] no serialize --- Stack/Opc.Ua.Core/Stack/Configuration/EndpointConfiguration.cs | 2 -- 1 file changed, 2 deletions(-) diff --git a/Stack/Opc.Ua.Core/Stack/Configuration/EndpointConfiguration.cs b/Stack/Opc.Ua.Core/Stack/Configuration/EndpointConfiguration.cs index 9a95f4bcda..3920ad986f 100644 --- a/Stack/Opc.Ua.Core/Stack/Configuration/EndpointConfiguration.cs +++ b/Stack/Opc.Ua.Core/Stack/Configuration/EndpointConfiguration.cs @@ -78,7 +78,6 @@ public static EndpointConfiguration Create(ApplicationConfiguration applicationC /// /// The maximum nesting level accepted while encoding or decoding objects. /// - [DataMember(Name = "MaxEncodingNestingLevels", IsRequired = false, Order = 20)] public int MaxEncodingNestingLevels { get @@ -96,7 +95,6 @@ public int MaxEncodingNestingLevels /// The number of times the decoder can recover from an error /// caused by an encoded ExtensionObject before throwing a decoder error. /// - [DataMember(Name = "MaxDecoderRecoveries", IsRequired = false, Order = 21)] public int MaxDecoderRecoveries { get From 09b548cb99aef4ec4d42c4baf121006d339991e2 Mon Sep 17 00:00:00 2001 From: Martin Regen Date: Thu, 2 May 2024 21:36:52 +0200 Subject: [PATCH 35/83] fix solution build --- Fuzzing/Encoders/Fuzz.Tools/Encoders.Testcases.cs | 4 +--- Fuzzing/Encoders/Fuzz/FuzzableCode.JsonDecoder.cs | 8 ++++++++ Fuzzing/Encoders/Fuzz/FuzzableCode.cs | 2 +- 3 files changed, 10 insertions(+), 4 deletions(-) diff --git a/Fuzzing/Encoders/Fuzz.Tools/Encoders.Testcases.cs b/Fuzzing/Encoders/Fuzz.Tools/Encoders.Testcases.cs index 57ecc46db6..4776fa35eb 100644 --- a/Fuzzing/Encoders/Fuzz.Tools/Encoders.Testcases.cs +++ b/Fuzzing/Encoders/Fuzz.Tools/Encoders.Testcases.cs @@ -43,10 +43,8 @@ public enum TestCaseEncoders : int public static string[] TestcaseEncoderSuffixes = new string[] { ".Binary", ".Json", ".Xml" }; - public static void Run(string directoryPath) + public static void Run(string workPath) { - string workPath = Path.TrimEndingDirectorySeparator(directoryPath); - // Create the Testcases for the binary decoder. string pathSuffix = TestcaseEncoderSuffixes[(int)TestCaseEncoders.Binary]; string pathTarget = workPath + pathSuffix + Path.DirectorySeparatorChar; diff --git a/Fuzzing/Encoders/Fuzz/FuzzableCode.JsonDecoder.cs b/Fuzzing/Encoders/Fuzz/FuzzableCode.JsonDecoder.cs index 1f18b5d84e..cb871eefda 100644 --- a/Fuzzing/Encoders/Fuzz/FuzzableCode.JsonDecoder.cs +++ b/Fuzzing/Encoders/Fuzz/FuzzableCode.JsonDecoder.cs @@ -75,7 +75,11 @@ public static void AflfuzzJsonEncoder(string input) /// public static void LibfuzzJsonDecoder(ReadOnlySpan input) { +#if NETFRAMEWORK + string json = Encoding.UTF8.GetString(input.ToArray()); +#else string json = Encoding.UTF8.GetString(input); +#endif _ = FuzzJsonDecoderCore(json); } @@ -85,7 +89,11 @@ public static void LibfuzzJsonDecoder(ReadOnlySpan input) public static void LibfuzzJsonEncoder(ReadOnlySpan input) { IEncodeable encodeable = null; +#if NETFRAMEWORK + string json = Encoding.UTF8.GetString(input.ToArray()); +#else string json = Encoding.UTF8.GetString(input); +#endif try { encodeable = FuzzJsonDecoderCore(json); diff --git a/Fuzzing/Encoders/Fuzz/FuzzableCode.cs b/Fuzzing/Encoders/Fuzz/FuzzableCode.cs index a99332ddae..01d9536519 100644 --- a/Fuzzing/Encoders/Fuzz/FuzzableCode.cs +++ b/Fuzzing/Encoders/Fuzz/FuzzableCode.cs @@ -62,7 +62,7 @@ private static MemoryStream PrepareArraySegmentStream(Stream stream) do { buffer = binaryStream.ReadBytes(segmentSize); - bufferCollection.Add(buffer); + bufferCollection.Add(new ArraySegment(buffer)); } while (buffer.Length == segmentSize); memoryStream = new ArraySegmentStream(bufferCollection); } From 84de2c6656f2d0f6d8fc92ca0e2415251460a781 Mon Sep 17 00:00:00 2001 From: Martin Regen Date: Sat, 4 May 2024 10:39:11 +0200 Subject: [PATCH 36/83] add a xml fuzzer --- .../Fuzz.Tools/Encoders.Fuzz.Tools.csproj | 1 + .../Encoders/Fuzz.Tools/Encoders.Testcases.cs | 21 +- .../Encoders/Fuzz/FuzzableCode.XmlDecoder.cs | 180 ++++++ .../Fuzz/Testcases.Xml/readrequest.xml | 43 ++ .../Fuzz/Testcases.Xml/readresponse.xml | 130 +++++ Fuzzing/Encoders/aflfuzz.sh | 18 +- Fuzzing/Encoders/libfuzz.bat | 14 +- Fuzzing/Encoders/libfuzz.sh | 16 +- Stack/Opc.Ua.Core/Schema/UANodeSetHelpers.cs | 20 +- Stack/Opc.Ua.Core/Stack/State/NodeState.cs | 51 +- .../Stack/State/NodeStateCollection.cs | 73 +-- .../Types/BuiltIn/ExtensionObject.cs | 41 +- Stack/Opc.Ua.Core/Types/BuiltIn/Variant.cs | 37 +- .../Types/Encoders/BinaryDecoder.cs | 2 + Stack/Opc.Ua.Core/Types/Encoders/IDecoder.cs | 5 + .../Opc.Ua.Core/Types/Encoders/JsonDecoder.cs | 7 +- .../Opc.Ua.Core/Types/Encoders/XmlDecoder.cs | 531 ++++++++++++------ .../Opc.Ua.Core/Types/Encoders/XmlEncoder.cs | 8 +- Stack/Opc.Ua.Core/Types/Utils/DataComparer.cs | 13 +- 19 files changed, 886 insertions(+), 325 deletions(-) create mode 100644 Fuzzing/Encoders/Fuzz/FuzzableCode.XmlDecoder.cs create mode 100644 Fuzzing/Encoders/Fuzz/Testcases.Xml/readrequest.xml create mode 100644 Fuzzing/Encoders/Fuzz/Testcases.Xml/readresponse.xml diff --git a/Fuzzing/Encoders/Fuzz.Tools/Encoders.Fuzz.Tools.csproj b/Fuzzing/Encoders/Fuzz.Tools/Encoders.Fuzz.Tools.csproj index 258bf00272..00c5a427c9 100644 --- a/Fuzzing/Encoders/Fuzz.Tools/Encoders.Fuzz.Tools.csproj +++ b/Fuzzing/Encoders/Fuzz.Tools/Encoders.Fuzz.Tools.csproj @@ -11,6 +11,7 @@ + diff --git a/Fuzzing/Encoders/Fuzz.Tools/Encoders.Testcases.cs b/Fuzzing/Encoders/Fuzz.Tools/Encoders.Testcases.cs index 4776fa35eb..f12fcd4f48 100644 --- a/Fuzzing/Encoders/Fuzz.Tools/Encoders.Testcases.cs +++ b/Fuzzing/Encoders/Fuzz.Tools/Encoders.Testcases.cs @@ -109,18 +109,33 @@ public static void Run(string workPath) pathTarget = workPath + pathSuffix + Path.DirectorySeparatorChar; foreach (var messageEncoder in MessageEncoders) { - string message; + string xml; using (var encoder = new XmlEncoder(MessageContext)) { encoder.SetMappingTables(MessageContext.NamespaceUris, MessageContext.ServerUris); messageEncoder(encoder); - message = encoder.CloseAndReturnText(); + xml = encoder.CloseAndReturnText(); } // Test the fuzz targets with the message. + byte[] message = Encoding.UTF8.GetBytes(xml); + using (var stream = new MemoryStream(message)) + { + FuzzableCode.AflfuzzXmlDecoder(stream); + } + using (var stream = new MemoryStream(message)) + { + FuzzableCode.AflfuzzXmlEncoder(stream); + } + using (var stream = new MemoryStream(message)) + { + FuzzableCode.FuzzXmlDecoderCore(stream, true); + } + FuzzableCode.LibfuzzXmlDecoder(message); + FuzzableCode.LibfuzzXmlEncoder(message); string fileName = Path.Combine(pathTarget, $"{messageEncoder.Method.Name}.xml".ToLowerInvariant()); - File.WriteAllBytes(fileName, Encoding.UTF8.GetBytes(message)); + File.WriteAllBytes(fileName, Encoding.UTF8.GetBytes(xml)); } } } diff --git a/Fuzzing/Encoders/Fuzz/FuzzableCode.XmlDecoder.cs b/Fuzzing/Encoders/Fuzz/FuzzableCode.XmlDecoder.cs new file mode 100644 index 0000000000..40e89134df --- /dev/null +++ b/Fuzzing/Encoders/Fuzz/FuzzableCode.XmlDecoder.cs @@ -0,0 +1,180 @@ +/* ======================================================================== + * Copyright (c) 2005-2024 The OPC Foundation, Inc. All rights reserved. + * + * OPC Foundation MIT License 1.00 + * + * Permission is hereby granted, free of charge, to any person + * obtaining a copy of this software and associated documentation + * files (the "Software"), to deal in the Software without + * restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following + * conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES + * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT + * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, + * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR + * OTHER DEALINGS IN THE SOFTWARE. + * + * The complete license agreement can be found here: + * http://opcfoundation.org/License/MIT/1.00/ + * ======================================================================*/ + +using System; +using System.IO; +using System.Linq; +using System.Xml; +using Opc.Ua; + +/// +/// Fuzzing code for the Xml decoder and encoder. +/// +public static partial class FuzzableCode +{ + /// + /// The Xml decoder fuzz target for afl-fuzz. + /// + public static void AflfuzzXmlDecoder(Stream stream) + { + _ = FuzzXmlDecoderCore(stream); + } + + /// + /// The Xml encoder fuzz target for afl-fuzz. + /// + public static void AflfuzzXmlEncoder(Stream stream) + { + IEncodeable encodeable = null; + try + { + encodeable = FuzzXmlDecoderCore(stream); + } + catch + { + return; + } + + // encode the fuzzed object and see if it crashes + if (encodeable != null) + { + using (var encoder = new JsonEncoder(messageContext, true)) + { + encoder.EncodeMessage(encodeable); + encoder.Close(); + } + } + } + + /// + /// The Xml decoder fuzz target for libfuzzer. + /// + public static void LibfuzzXmlDecoder(ReadOnlySpan input) + { + using (var memoryStream = new MemoryStream(input.ToArray())) + { + _ = FuzzXmlDecoderCore(memoryStream); + } + } + + /// + /// The Xml encoder fuzz target for afl-fuzz. + /// + public static void LibfuzzXmlEncoder(ReadOnlySpan input) + { + IEncodeable encodeable; + try + { + using (var memoryStream = new MemoryStream(input.ToArray())) + { + encodeable = FuzzXmlDecoderCore(memoryStream); + } + } + catch + { + return; + } + + // encode the fuzzed object and see if it crashes + if (encodeable != null) + { + using (var encoder = new XmlEncoder(messageContext)) + { + encoder.EncodeMessage(encodeable); + encoder.Close(); + } + } + } + + /// + /// The fuzz target for the XmlDecoder. + /// + /// A stream with fuzz content. + internal static IEncodeable FuzzXmlDecoderCore(Stream stream, bool throwAll = false) + { + try + { + using (var reader = XmlReader.Create(stream, Utils.DefaultXmlReaderSettings())) + { + Type systemType = null; + try + { + reader.MoveToContent(); + string typeName = reader.LocalName; + string namespaceUri = reader.NamespaceURI; + systemType = messageContext.Factory.EncodeableTypes + .Where(entry => entry.Value.Name == typeName/* && entry.Key.NamespaceUri == namespaceUri*/) + .Select(entry => entry.Value) + .FirstOrDefault(); + } + catch (XmlException ex) + { + if (!throwAll) + { + return null; + } + throw ServiceResultException.Create(StatusCodes.BadDecodingError, ex.Message); + } + + if (systemType == null) + { + if (!throwAll) + { + return null; + } + throw ServiceResultException.Create(StatusCodes.BadDecodingError, "Could not find type for decoding."); + } + + // TODO: match ns GetEncodeableFactory(typeName, namespaceUri, out IEncodeable encodeable, out _); + using (var decoder = new XmlDecoder(reader, messageContext)) + { + return decoder.DecodeMessage(systemType); + } + } + } + catch (ServiceResultException sre) + { + switch (sre.StatusCode) + { + case StatusCodes.BadEncodingLimitsExceeded: + case StatusCodes.BadDecodingError: + if (!throwAll) + { + return null; + } + break; + } + + Console.WriteLine("Unexpected ServiceResultException: {0} {1}", (StatusCode)sre.StatusCode, sre.Message); + + throw; + } + } +} + diff --git a/Fuzzing/Encoders/Fuzz/Testcases.Xml/readrequest.xml b/Fuzzing/Encoders/Fuzz/Testcases.Xml/readrequest.xml new file mode 100644 index 0000000000..34b7bc8cca --- /dev/null +++ b/Fuzzing/Encoders/Fuzz/Testcases.Xml/readrequest.xml @@ -0,0 +1,43 @@ + + + 2024-05-03T12:59:58.4456622Z + 42 + 0 + 0 + + + 0 + Source_0 + + + + i=1000 + + 5 + + + + i=1000 + + 13 + + + + i=1000 + + 4 + + + + i=1000 + + 17 + + + + i=1000 + + 24 + + + \ No newline at end of file diff --git a/Fuzzing/Encoders/Fuzz/Testcases.Xml/readresponse.xml b/Fuzzing/Encoders/Fuzz/Testcases.Xml/readresponse.xml new file mode 100644 index 0000000000..5d550a5183 --- /dev/null +++ b/Fuzzing/Encoders/Fuzz/Testcases.Xml/readresponse.xml @@ -0,0 +1,130 @@ + + + 2024-05-03T13:00:39.3472074Z + 42 + + 0 + + + -1 + -1 + -1 + -1 + NodeId not found + + 2153644032 + + + -1 + -1 + -1 + -1 + Hello World + + 2150891520 + + + -1 + -1 + -1 + -1 + Hello World + + 2150891520 + + + -1 + -1 + -1 + -1 + Hello World + + 2150891520 + + + + + + + + + + + + Hello World + + + + 0 + + 2024-05-03T13:01:39.3471774Z + 10 + 2024-05-03T13:00:39.3471774Z + 100 + + + + + 12345678 + + + + 2157772800 + + 2024-05-03T13:01:39.3471774Z + 0 + 2024-05-03T13:00:39.3471774Z + 0 + + + + + AAECAwQFBg== + + + + 0 + + 2024-05-03T13:01:39.3471774Z + 0 + 2024-05-03T13:00:39.3471774Z + 0 + + + + + 42 + + + + 0 + + 2024-05-03T13:00:39.3471774Z + 0 + 0001-01-01T00:00:00 + 0 + + + + + -1 + -1 + -1 + -1 + Hello World + + 2148925440 + + + -1 + -1 + -1 + -1 + Hello World + + 2150891520 + + + + + \ No newline at end of file diff --git a/Fuzzing/Encoders/aflfuzz.sh b/Fuzzing/Encoders/aflfuzz.sh index ec2b93a9f3..f79a5bb2b3 100644 --- a/Fuzzing/Encoders/aflfuzz.sh +++ b/Fuzzing/Encoders/aflfuzz.sh @@ -7,7 +7,9 @@ display_menu() { echo "2. Opc.Ua.BinaryEncoder" echo "3. Opc.Ua.JsonDecoder" echo "4. Opc.Ua.JsonEncoder" - echo "5. Exit" + echo "5. Opc.Ua.XmlDecoder" + echo "6. Opc.Ua.XmlEncoder" + echo "7. Exit" } # Function to execute fuzz-afl PowerShell script based on user choice @@ -29,6 +31,14 @@ execute_powershell_script() { echo "Running afl-fuzz with Opc.Ua.JsonEncoder" pwsh ../scripts/fuzz-afl.ps1 ./Fuzz/Encoders.Fuzz.csproj -i ./Fuzz/Testcases.Json -x ../dictionaries/json.dict -fuzztarget AflfuzzJsonEncoder ;; + 5) + echo "Running afl-fuzz with Opc.Ua.XmlDecoder" + pwsh ../scripts/fuzz-afl.ps1 ./Fuzz/Encoders.Fuzz.csproj -i ./Fuzz/Testcases.Xml -x ../dictionaries/xml.dict -fuzztarget AflfuzzXmlDecoder + ;; + 6) + echo "Running afl-fuzz with Opc.Ua.XmlEncoder" + pwsh ../scripts/fuzz-afl.ps1 ./Fuzz/Encoders.Fuzz.csproj -i ./Fuzz/Testcases.Xml -x ../dictionaries/xml.dict -fuzztarget AflfuzzXmlEncoder + ;; *) echo "Invalid option. Exiting." ;; @@ -41,15 +51,15 @@ display_menu read -p "Enter your choice (1-5): " choice case $choice in - 1|2|3|4) + 1|2|3|4|5|6) execute_powershell_script $choice ;; - 5) + 7) echo "Exiting." break ;; *) - echo "Invalid input. Please enter a number between 1 and 5." + echo "Invalid input. Please enter a number between 1 and 7." ;; esac diff --git a/Fuzzing/Encoders/libfuzz.bat b/Fuzzing/Encoders/libfuzz.bat index 2b380fbb6d..52407f2095 100644 --- a/Fuzzing/Encoders/libfuzz.bat +++ b/Fuzzing/Encoders/libfuzz.bat @@ -6,9 +6,11 @@ echo "1. Opc.Ua.BinaryDecoder" echo "2. Opc.Ua.BinaryEncoder" echo "3. Opc.Ua.JsonDecoder" echo "4. Opc.Ua.JsonEncoder" -echo "5. Exit" +echo "5. Opc.Ua.XmlDecoder" +echo "6. Opc.Ua.XmlEncoder" +echo "7. Exit" -set /p choice="Enter your choice (1-5): " +set /p choice="Enter your choice (1-7): " if "%choice%"=="1" ( echo "Running libfuzzer with Opc.Ua.BinaryDecoder" @@ -23,10 +25,16 @@ if "%choice%"=="1" ( echo "Running libfuzzer with Opc.Ua.JsonEncoder" powershell.exe -File ../scripts/fuzz-libfuzzer.ps1 -libFuzzer "./libfuzzer-dotnet-windows.exe" -project ./Fuzz/Encoders.Fuzz.csproj -fuzztarget LibfuzzJsonEncoder -dict ../dictionaries/json.dict -corpus ./Fuzz/Testcases.Json/ ) else if "%choice%"=="5" ( + echo "Running libfuzzer with Opc.Ua.XmlDecoder" + powershell.exe -File ../scripts/fuzz-libfuzzer.ps1 -libFuzzer "./libfuzzer-dotnet-windows.exe" -project ./Fuzz/Encoders.Fuzz.csproj -fuzztarget LibfuzzXmlDecoder -dict ../dictionaries/xml.dict -corpus ./Fuzz/Testcases.Xml/ +) else if "%choice%"=="6" ( + echo "Running libfuzzer with Opc.Ua.XmlEncoder" + powershell.exe -File ../scripts/fuzz-libfuzzer.ps1 -libFuzzer "./libfuzzer-dotnet-windows.exe" -project ./Fuzz/Encoders.Fuzz.csproj -fuzztarget LibfuzzXmlEncoder -dict ../dictionaries/xml.dict -corpus ./Fuzz/Testcases.Xml/ +) else if "%choice%"=="7" ( echo Exiting. exit /b ) else ( - echo Invalid input. Please enter a number between 1 and 5. + echo Invalid input. Please enter a number between 1 and 7. ) echo Done \ No newline at end of file diff --git a/Fuzzing/Encoders/libfuzz.sh b/Fuzzing/Encoders/libfuzz.sh index 8205e22642..f4e2a9f47e 100644 --- a/Fuzzing/Encoders/libfuzz.sh +++ b/Fuzzing/Encoders/libfuzz.sh @@ -7,7 +7,9 @@ display_menu() { echo "2. Opc.Ua.BinaryEncoder" echo "3. Opc.Ua.JsonDecoder" echo "4. Opc.Ua.JsonEncoder" - echo "5. Exit" + echo "5. Opc.Ua.XmlDecoder" + echo "6. Opc.Ua.XmlEncoder" + echo "7. Exit" } # Function to execute fuzz-afl PowerShell script based on user choice @@ -29,6 +31,14 @@ execute_powershell_script() { echo "Running libfuzzer with Opc.Ua.JsonEncoder" pwsh ../scripts/fuzz-libfuzzer.ps1 -libFuzzer "./libfuzzer-dotnet-ubuntu" -project ./Fuzz/Encoders.Fuzz.csproj -fuzztarget LibfuzzJsonEncoder -dict ../dictionaries/json.dict -corpus ./Fuzz/Testcases.Json/ ;; + 5) + echo "Running libfuzzer with Opc.Ua.XmlDecoder" + pwsh ../scripts/fuzz-libfuzzer.ps1 -libFuzzer "./libfuzzer-dotnet-ubuntu" -project ./Fuzz/Encoders.Fuzz.csproj -fuzztarget LibfuzzXmlDecoder -dict ../dictionaries/xml.dict -corpus ./Fuzz/Testcases.Xml/ + ;; + 6) + echo "Running libfuzzer with Opc.Ua.XmlEncoder" + pwsh ../scripts/fuzz-libfuzzer.ps1 -libFuzzer "./libfuzzer-dotnet-ubuntu" -project ./Fuzz/Encoders.Fuzz.csproj -fuzztarget LibfuzzXmlEncoder -dict ../dictionaries/xml.dict -corpus ./Fuzz/Testcases.Xml/ + ;; *) echo "Invalid option. Exiting." ;; @@ -41,10 +51,10 @@ display_menu read -p "Enter your choice (1-5): " choice case $choice in - 1|2|3|4) + 1|2|3|4|5|6) execute_powershell_script $choice ;; - 5) + 7) echo "Exiting." break ;; diff --git a/Stack/Opc.Ua.Core/Schema/UANodeSetHelpers.cs b/Stack/Opc.Ua.Core/Schema/UANodeSetHelpers.cs index e4f00aee09..6cd561b311 100644 --- a/Stack/Opc.Ua.Core/Schema/UANodeSetHelpers.cs +++ b/Stack/Opc.Ua.Core/Schema/UANodeSetHelpers.cs @@ -540,10 +540,12 @@ private NodeState Import(ISystemContext context, UANode node) if (o.Value != null) { - XmlDecoder decoder = CreateDecoder(context, o.Value); - TypeInfo typeInfo = null; - value.Value = decoder.ReadVariantContents(out typeInfo); - decoder.Close(); + using (XmlDecoder decoder = CreateDecoder(context, o.Value)) + { + TypeInfo typeInfo = null; + value.Value = decoder.ReadVariantContents(out typeInfo); + decoder.Close(); + } } importedNode = value; @@ -590,10 +592,12 @@ private NodeState Import(ISystemContext context, UANode node) if (o.Value != null) { - XmlDecoder decoder = CreateDecoder(context, o.Value); - TypeInfo typeInfo = null; - value.Value = decoder.ReadVariantContents(out typeInfo); - decoder.Close(); + using (XmlDecoder decoder = CreateDecoder(context, o.Value)) + { + TypeInfo typeInfo = null; + value.Value = decoder.ReadVariantContents(out typeInfo); + decoder.Close(); + } } importedNode = value; diff --git a/Stack/Opc.Ua.Core/Stack/State/NodeState.cs b/Stack/Opc.Ua.Core/Stack/State/NodeState.cs index 828bde7829..c3e81b8e1f 100644 --- a/Stack/Opc.Ua.Core/Stack/State/NodeState.cs +++ b/Stack/Opc.Ua.Core/Stack/State/NodeState.cs @@ -1275,11 +1275,11 @@ public void LoadFromXml(ISystemContext context, Stream input) /// The stream to read. public void LoadFromXml(ISystemContext context, XmlReader reader) { - ServiceMessageContext messageContext = new ServiceMessageContext(); - - messageContext.NamespaceUris = context.NamespaceUris; - messageContext.ServerUris = context.ServerUris; - messageContext.Factory = context.EncodeableFactory; + ServiceMessageContext messageContext = new ServiceMessageContext { + NamespaceUris = context.NamespaceUris, + ServerUris = context.ServerUris, + Factory = context.EncodeableFactory + }; reader.MoveToContent(); @@ -1298,31 +1298,32 @@ public void LoadFromXml(ISystemContext context, XmlReader reader) this.SymbolicName = symbolicName.Name; this.BrowseName = new QualifiedName(symbolicName.Name, (ushort)namespaceIndex); - XmlDecoder decoder = new XmlDecoder(null, reader, messageContext); - - // check if a namespace table was provided. - NamespaceTable namespaceUris = new NamespaceTable(); - - if (!decoder.LoadStringTable("NamespaceUris", "NamespaceUri", namespaceUris)) + using (XmlDecoder decoder = new XmlDecoder(null, reader, messageContext)) { - namespaceUris = null; - } + // check if a namespace table was provided. + NamespaceTable namespaceUris = new NamespaceTable(); - // check if a server uri table was provided. - StringTable serverUris = new StringTable(); + if (!decoder.LoadStringTable("NamespaceUris", "NamespaceUri", namespaceUris)) + { + namespaceUris = null; + } - if (!decoder.LoadStringTable("ServerUris", "ServerUri", serverUris)) - { - serverUris = null; - } + // check if a server uri table was provided. + StringTable serverUris = new StringTable(); - // setup the mappings to use during decoding. - decoder.SetMappingTables(namespaceUris, serverUris); + if (!decoder.LoadStringTable("ServerUris", "ServerUri", serverUris)) + { + serverUris = null; + } - // update the node and children. - Update(context, decoder); - UpdateReferences(context, decoder); - UpdateChildren(context, decoder); + // setup the mappings to use during decoding. + decoder.SetMappingTables(namespaceUris, serverUris); + + // update the node and children. + Update(context, decoder); + UpdateReferences(context, decoder); + UpdateChildren(context, decoder); + } } /// diff --git a/Stack/Opc.Ua.Core/Stack/State/NodeStateCollection.cs b/Stack/Opc.Ua.Core/Stack/State/NodeStateCollection.cs index 678a3f2316..027e30f4b1 100644 --- a/Stack/Opc.Ua.Core/Stack/State/NodeStateCollection.cs +++ b/Stack/Opc.Ua.Core/Stack/State/NodeStateCollection.cs @@ -338,61 +338,62 @@ public void LoadFromXml(ISystemContext context, Stream istrm, bool updateTables) using (XmlReader reader = XmlReader.Create(istrm, Utils.DefaultXmlReaderSettings())) { - XmlDecoder decoder = new XmlDecoder(null, reader, messageContext); - - NamespaceTable namespaceUris = new NamespaceTable(); - - if (!decoder.LoadStringTable("NamespaceUris", "NamespaceUri", namespaceUris)) + using (XmlDecoder decoder = new XmlDecoder(null, reader, messageContext)) { - namespaceUris = null; - } + NamespaceTable namespaceUris = new NamespaceTable(); - // update namespace table. - if (updateTables) - { - if (namespaceUris != null && context.NamespaceUris != null) + if (!decoder.LoadStringTable("NamespaceUris", "NamespaceUri", namespaceUris)) { - for (int ii = 0; ii < namespaceUris.Count; ii++) + namespaceUris = null; + } + + // update namespace table. + if (updateTables) + { + if (namespaceUris != null && context.NamespaceUris != null) { - context.NamespaceUris.GetIndexOrAppend(namespaceUris.GetString((uint)ii)); + for (int ii = 0; ii < namespaceUris.Count; ii++) + { + context.NamespaceUris.GetIndexOrAppend(namespaceUris.GetString((uint)ii)); + } } } - } - StringTable serverUris = new StringTable(); + StringTable serverUris = new StringTable(); - if (!decoder.LoadStringTable("ServerUris", "ServerUri", context.ServerUris)) - { - serverUris = null; - } + if (!decoder.LoadStringTable("ServerUris", "ServerUri", context.ServerUris)) + { + serverUris = null; + } - // update server table. - if (updateTables) - { - if (serverUris != null && context.ServerUris != null) + // update server table. + if (updateTables) { - for (int ii = 0; ii < serverUris.Count; ii++) + if (serverUris != null && context.ServerUris != null) { - context.ServerUris.GetIndexOrAppend(serverUris.GetString((uint)ii)); + for (int ii = 0; ii < serverUris.Count; ii++) + { + context.ServerUris.GetIndexOrAppend(serverUris.GetString((uint)ii)); + } } } - } - // set mapping. - decoder.SetMappingTables(namespaceUris, serverUris); + // set mapping. + decoder.SetMappingTables(namespaceUris, serverUris); - decoder.PushNamespace(Namespaces.OpcUaXsd); + decoder.PushNamespace(Namespaces.OpcUaXsd); - NodeState state = NodeState.LoadNode(context, decoder); + NodeState state = NodeState.LoadNode(context, decoder); - while (state != null) - { - this.Add(state); + while (state != null) + { + this.Add(state); - state = NodeState.LoadNode(context, decoder); - } + state = NodeState.LoadNode(context, decoder); + } - decoder.Close(); + decoder.Close(); + } } } diff --git a/Stack/Opc.Ua.Core/Types/BuiltIn/ExtensionObject.cs b/Stack/Opc.Ua.Core/Types/BuiltIn/ExtensionObject.cs index 3e384a0004..33bbcb2058 100644 --- a/Stack/Opc.Ua.Core/Types/BuiltIn/ExtensionObject.cs +++ b/Stack/Opc.Ua.Core/Types/BuiltIn/ExtensionObject.cs @@ -766,29 +766,30 @@ private XmlElement XmlEncodedBody } // create decoder. - XmlDecoder decoder = new XmlDecoder(value, m_context); - - // read body. - Body = decoder.ReadExtensionObjectBody(m_typeId); + using (XmlDecoder decoder = new XmlDecoder(value, m_context)) + { + // read body. + Body = decoder.ReadExtensionObjectBody(m_typeId); - // clear the type id for encodeables. + // clear the type id for encodeables. - if (m_body is IEncodeable encodeable) - { - m_typeId = ExpandedNodeId.Null; - } + if (m_body is IEncodeable encodeable) + { + m_typeId = ExpandedNodeId.Null; + } - // close decoder. - try - { - decoder.Close(true); - } - catch (Exception e) - { - throw new ServiceResultException( - StatusCodes.BadDecodingError, - Utils.Format("Did not read all of a extension object body: '{0}'", m_typeId), - e); + // close decoder. + try + { + decoder.Close(true); + } + catch (Exception e) + { + throw new ServiceResultException( + StatusCodes.BadDecodingError, + Utils.Format("Did not read all of a extension object body: '{0}'", m_typeId), + e); + } } } } diff --git a/Stack/Opc.Ua.Core/Types/BuiltIn/Variant.cs b/Stack/Opc.Ua.Core/Types/BuiltIn/Variant.cs index c1c005f699..79555ef2bd 100644 --- a/Stack/Opc.Ua.Core/Types/BuiltIn/Variant.cs +++ b/Stack/Opc.Ua.Core/Types/BuiltIn/Variant.cs @@ -805,25 +805,26 @@ private XmlElement XmlEncodedValue TypeInfo typeInfo = null; // create decoder. - XmlDecoder decoder = new XmlDecoder(value, MessageContextExtension.CurrentContext); - - try - { - // read value. - object body = decoder.ReadVariantContents(out typeInfo); - Set(body, typeInfo); - } - catch (Exception e) + using (XmlDecoder decoder = new XmlDecoder(value, MessageContextExtension.CurrentContext)) { - throw ServiceResultException.Create( - StatusCodes.BadDecodingError, - e, - "Error decoding Variant value."); - } - finally - { - // close decoder. - decoder.Close(); + try + { + // read value. + object body = decoder.ReadVariantContents(out typeInfo); + Set(body, typeInfo); + } + catch (Exception e) + { + throw ServiceResultException.Create( + StatusCodes.BadDecodingError, + e, + "Error decoding Variant value."); + } + finally + { + // close decoder. + decoder.Close(); + } } } } diff --git a/Stack/Opc.Ua.Core/Types/Encoders/BinaryDecoder.cs b/Stack/Opc.Ua.Core/Types/Encoders/BinaryDecoder.cs index 6d6bd73b77..2c5e527e96 100644 --- a/Stack/Opc.Ua.Core/Types/Encoders/BinaryDecoder.cs +++ b/Stack/Opc.Ua.Core/Types/Encoders/BinaryDecoder.cs @@ -2075,6 +2075,8 @@ private ExtensionObject ReadExtensionObject() // update body. extension.Body = body; + + xmlDecoder.Close(); } catch (Exception e) { diff --git a/Stack/Opc.Ua.Core/Types/Encoders/IDecoder.cs b/Stack/Opc.Ua.Core/Types/Encoders/IDecoder.cs index 53f5f3cecd..d92011b9fb 100644 --- a/Stack/Opc.Ua.Core/Types/Encoders/IDecoder.cs +++ b/Stack/Opc.Ua.Core/Types/Encoders/IDecoder.cs @@ -42,6 +42,11 @@ public interface IDecoder : IDisposable /// The server URIs referenced by the data being decoded. void SetMappingTables(NamespaceTable namespaceUris, StringTable serverUris); + /// + /// Decodes an object from a buffer. + /// + IEncodeable DecodeMessage(Type expectedType); + /// /// Pushes a namespace onto the namespace stack. /// diff --git a/Stack/Opc.Ua.Core/Types/Encoders/JsonDecoder.cs b/Stack/Opc.Ua.Core/Types/Encoders/JsonDecoder.cs index 83f6220a85..1f1e33816d 100644 --- a/Stack/Opc.Ua.Core/Types/Encoders/JsonDecoder.cs +++ b/Stack/Opc.Ua.Core/Types/Encoders/JsonDecoder.cs @@ -261,11 +261,8 @@ protected virtual void Dispose(bool disposing) { if (disposing) { - if (m_reader != null) - { - m_reader.Close(); - m_reader = null; - } + Utils.SilentDispose(m_reader); + m_reader = null; } } #endregion diff --git a/Stack/Opc.Ua.Core/Types/Encoders/XmlDecoder.cs b/Stack/Opc.Ua.Core/Types/Encoders/XmlDecoder.cs index 22c8f04cd6..f5c012944e 100644 --- a/Stack/Opc.Ua.Core/Types/Encoders/XmlDecoder.cs +++ b/Stack/Opc.Ua.Core/Types/Encoders/XmlDecoder.cs @@ -14,6 +14,7 @@ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. using System.Collections.Generic; using System.Globalization; using System.IO; +using System.Runtime.CompilerServices; using System.Text; using System.Xml; @@ -28,12 +29,13 @@ public class XmlDecoder : IDecoder /// /// Initializes the object with default values. /// - public XmlDecoder(IServiceMessageContext context) + public XmlDecoder(XmlReader reader, IServiceMessageContext context) { if (context == null) throw new ArgumentNullException(nameof(context)); Initialize(); m_context = context; m_nestingLevel = 0; + m_reader = reader; } /// @@ -51,7 +53,7 @@ public XmlDecoder(XmlElement element, IServiceMessageContext context) /// /// Initializes the object with a XML reader. /// - public XmlDecoder(System.Type systemType, XmlReader reader, IServiceMessageContext context) + public XmlDecoder(Type systemType, XmlReader reader, IServiceMessageContext context) { Initialize(); @@ -98,28 +100,6 @@ private void Initialize() #endregion #region Public Methods - /// - /// Initializes the tables used to map namespace and server uris during decoding. - /// - /// The namespace URIs referenced by the data being decoded. - /// The server URIs referenced by the data being decoded. - public void SetMappingTables(NamespaceTable namespaceUris, StringTable serverUris) - { - m_namespaceMappings = null; - - if (namespaceUris != null && m_context.NamespaceUris != null) - { - m_namespaceMappings = m_context.NamespaceUris.CreateMapping(namespaceUris, false); - } - - m_serverMappings = null; - - if (serverUris != null && m_context.ServerUris != null) - { - m_serverMappings = m_context.ServerUris.CreateMapping(serverUris, false); - } - } - /// /// Initializes a string table from an XML stream. /// @@ -160,7 +140,7 @@ public bool LoadStringTable(string tableName, string elementName, StringTable st /// public void Close() { - m_reader.Dispose(); + m_reader.Close(); } /// @@ -173,7 +153,7 @@ public void Close(bool checkEof) m_reader.ReadEndElement(); } - m_reader.Dispose(); + m_reader.Close(); } /// @@ -236,29 +216,37 @@ public void ReadStartElement() /// The qualified name of the element to skip. public void Skip(XmlQualifiedName qname) { - m_reader.MoveToContent(); + try + { + m_reader.MoveToContent(); - int depth = 1; + int depth = 1; - while (depth > 0) - { - if (m_reader.NodeType == XmlNodeType.EndElement) + while (depth > 0) { - if (m_reader.LocalName == qname.Name && m_reader.NamespaceURI == qname.Namespace) + if (m_reader.NodeType == XmlNodeType.EndElement) { - depth--; + if (m_reader.LocalName == qname.Name && m_reader.NamespaceURI == qname.Namespace) + { + depth--; + } } - } - else if (m_reader.NodeType == XmlNodeType.Element) - { - if (m_reader.LocalName == qname.Name && m_reader.NamespaceURI == qname.Namespace) + else if (m_reader.NodeType == XmlNodeType.Element) { - depth++; + if (m_reader.LocalName == qname.Name && m_reader.NamespaceURI == qname.Namespace) + { + depth++; + } } - } - m_reader.Skip(); - m_reader.MoveToContent(); + m_reader.Skip(); + m_reader.MoveToContent(); + } + } + catch (XmlException xe) + { + throw ServiceResultException.Create(StatusCodes.BadDecodingError, + "Skip {0} failed: {1}", qname.Name, xe.Message); } } @@ -515,9 +503,8 @@ public object ReadVariantContents(out TypeInfo typeInfo) } } - throw new ServiceResultException( - StatusCodes.BadDecodingError, - Utils.Format("Element '{1}:{0}' is not allowed in an Variant.", m_reader.LocalName, m_reader.NamespaceURI)); + throw ServiceResultException.Create(StatusCodes.BadDecodingError, + "Element '{1}:{0}' is not allowed in a Variant.", m_reader.LocalName, m_reader.NamespaceURI); } finally { @@ -588,10 +575,8 @@ protected virtual void Dispose(bool disposing) { if (disposing) { - if (m_reader != null) - { - m_reader.Dispose(); - } + Utils.SilentDispose(m_reader); + m_reader = null; } } #endregion @@ -623,6 +608,56 @@ public void PopNamespace() m_namespaces.Pop(); } + /// + /// Initializes the tables used to map namespace and server uris during decoding. + /// + /// The namespace URIs referenced by the data being decoded. + /// The server URIs referenced by the data being decoded. + public void SetMappingTables(NamespaceTable namespaceUris, StringTable serverUris) + { + m_namespaceMappings = null; + + if (namespaceUris != null && m_context.NamespaceUris != null) + { + m_namespaceMappings = m_context.NamespaceUris.CreateMapping(namespaceUris, false); + } + + m_serverMappings = null; + + if (serverUris != null && m_context.ServerUris != null) + { + m_serverMappings = m_context.ServerUris.CreateMapping(serverUris, false); + } + } + + /// + /// Decodes an object from a buffer. + /// + public IEncodeable DecodeMessage(Type expectedType) + { + if (expectedType == null) throw new ArgumentNullException(nameof(expectedType)); + + XmlQualifiedName typeName = EncodeableFactory.GetXmlName(expectedType); + string ns = typeName.Namespace; + string name = typeName.Name; + + int index = name.IndexOf(':'); + + if (index != -1) + { + name = name.Substring(index + 1); + } + + PushNamespace(ns); + + // read the message. + var encodeable = ReadEncodeable(name, expectedType); + + PopNamespace(); + + return encodeable; + } + /// /// Reads a boolean from the stream. /// @@ -630,11 +665,11 @@ public bool ReadBoolean(string fieldName) { if (BeginField(fieldName, true)) { - string xml = ReadString(); + string xml = SafeReadString(); - if (!String.IsNullOrEmpty(xml)) + if (!string.IsNullOrEmpty(xml)) { - bool value = XmlConvert.ToBoolean(xml.ToLowerInvariant()); + bool value = SafeXmlConvert(fieldName, XmlConvert.ToBoolean, xml.ToLowerInvariant()); EndField(fieldName); return value; } @@ -650,11 +685,11 @@ public sbyte ReadSByte(string fieldName) { if (BeginField(fieldName, true)) { - string xml = ReadString(); + string xml = SafeReadString(); - if (!String.IsNullOrEmpty(xml)) + if (!string.IsNullOrEmpty(xml)) { - sbyte value = XmlConvert.ToSByte(xml); + sbyte value = SafeXmlConvert(fieldName, XmlConvert.ToSByte, xml); EndField(fieldName); return value; } @@ -670,11 +705,11 @@ public byte ReadByte(string fieldName) { if (BeginField(fieldName, true)) { - string xml = ReadString(); + string xml = SafeReadString(); - if (!String.IsNullOrEmpty(xml)) + if (!string.IsNullOrEmpty(xml)) { - byte value = XmlConvert.ToByte(xml); + byte value = SafeXmlConvert(fieldName, XmlConvert.ToByte, xml); EndField(fieldName); return value; } @@ -690,11 +725,11 @@ public short ReadInt16(string fieldName) { if (BeginField(fieldName, true)) { - string xml = ReadString(); + string xml = SafeReadString(); - if (!String.IsNullOrEmpty(xml)) + if (!string.IsNullOrEmpty(xml)) { - short value = XmlConvert.ToInt16(xml); + short value = SafeXmlConvert(fieldName, XmlConvert.ToInt16, xml); EndField(fieldName); return value; } @@ -710,11 +745,11 @@ public ushort ReadUInt16(string fieldName) { if (BeginField(fieldName, true)) { - string xml = ReadString(); + string xml = SafeReadString(); - if (!String.IsNullOrEmpty(xml)) + if (!string.IsNullOrEmpty(xml)) { - ushort value = XmlConvert.ToUInt16(xml); + ushort value = SafeXmlConvert(fieldName, XmlConvert.ToUInt16, xml); EndField(fieldName); return value; } @@ -730,11 +765,11 @@ public int ReadInt32(string fieldName) { if (BeginField(fieldName, true)) { - string xml = ReadString(); + string xml = SafeReadString(); - if (!String.IsNullOrEmpty(xml)) + if (!string.IsNullOrEmpty(xml)) { - int value = XmlConvert.ToInt32(xml); + int value = SafeXmlConvert(fieldName, XmlConvert.ToInt32, xml); EndField(fieldName); return value; } @@ -750,11 +785,11 @@ public uint ReadUInt32(string fieldName) { if (BeginField(fieldName, true)) { - string xml = ReadString(); + string xml = SafeReadString(); - if (!String.IsNullOrEmpty(xml)) + if (!string.IsNullOrEmpty(xml)) { - uint value = XmlConvert.ToUInt32(xml); + uint value = SafeXmlConvert(fieldName, XmlConvert.ToUInt32, xml); EndField(fieldName); return value; } @@ -770,11 +805,11 @@ public long ReadInt64(string fieldName) { if (BeginField(fieldName, true)) { - string xml = ReadString(); + string xml = SafeReadString(); - if (!String.IsNullOrEmpty(xml)) + if (!string.IsNullOrEmpty(xml)) { - long value = XmlConvert.ToInt64(xml); + long value = SafeXmlConvert(fieldName, XmlConvert.ToInt64, xml); EndField(fieldName); return value; } @@ -790,11 +825,11 @@ public ulong ReadUInt64(string fieldName) { if (BeginField(fieldName, true)) { - string xml = ReadString(); + string xml = SafeReadString(); - if (!String.IsNullOrEmpty(xml)) + if (!string.IsNullOrEmpty(xml)) { - ulong value = XmlConvert.ToUInt64(xml); + ulong value = SafeXmlConvert(fieldName, XmlConvert.ToUInt64, xml); EndField(fieldName); return value; } @@ -810,11 +845,11 @@ public float ReadFloat(string fieldName) { if (BeginField(fieldName, true)) { - string xml = ReadString(); + string xml = SafeReadString(); - if (!String.IsNullOrEmpty(xml)) + if (!string.IsNullOrEmpty(xml)) { - float value = XmlConvert.ToSingle(xml); + float value = SafeXmlConvert(fieldName, XmlConvert.ToSingle, xml); EndField(fieldName); return value; } @@ -830,11 +865,11 @@ public double ReadDouble(string fieldName) { if (BeginField(fieldName, true)) { - string xml = ReadString(); + string xml = SafeReadString(); - if (!String.IsNullOrEmpty(xml)) + if (!string.IsNullOrEmpty(xml)) { - double value = XmlConvert.ToDouble(xml); + double value = SafeXmlConvert(fieldName, XmlConvert.ToDouble, xml); EndField(fieldName); return value; } @@ -852,7 +887,7 @@ public string ReadString(string fieldName) if (BeginField(fieldName, true, out isNil)) { - string value = ReadString(); + string value = SafeReadString(); if (value != null) { @@ -865,7 +900,7 @@ public string ReadString(string fieldName) if (!isNil) { - return String.Empty; + return string.Empty; } return null; @@ -878,7 +913,7 @@ public DateTime ReadDateTime(string fieldName) { if (BeginField(fieldName, true)) { - string xml = ReadString(); + string xml = SafeReadString(); // check the length. if (m_context.MaxStringLength > 0 && m_context.MaxStringLength < xml.Length) @@ -886,11 +921,18 @@ public DateTime ReadDateTime(string fieldName) throw new ServiceResultException(StatusCodes.BadEncodingLimitsExceeded); } - if (!String.IsNullOrEmpty(xml)) + if (!string.IsNullOrEmpty(xml)) { - DateTime value = XmlConvert.ToDateTime(xml, XmlDateTimeSerializationMode.Utc); - EndField(fieldName); - return value; + try + { + DateTime value = XmlConvert.ToDateTime(xml, XmlDateTimeSerializationMode.Utc); + EndField(fieldName); + return value; + } + catch (FormatException fe) + { + throw CreateBadDecodingError(fieldName, fe); + } } } @@ -907,9 +949,18 @@ public Uuid ReadGuid(string fieldName) if (BeginField(fieldName, true)) { PushNamespace(Namespaces.OpcUaXsd); - value.GuidString = ReadString("String"); + var guidString = ReadString("String"); PopNamespace(); + try + { + value.GuidString = guidString; + } + catch (FormatException fe) + { + throw CreateBadDecodingError(fieldName, fe); + } + EndField(fieldName); } @@ -921,21 +972,30 @@ public Uuid ReadGuid(string fieldName) /// public byte[] ReadByteString(string fieldName) { - bool isNil = false; - - if (BeginField(fieldName, true, out isNil)) + if (BeginField(fieldName, true, out bool isNil)) { byte[] value = null; - string xml = m_reader.ReadContentAsString(); + try + { + string xml = m_reader.ReadContentAsString(); - if (!String.IsNullOrEmpty(xml)) + if (!string.IsNullOrEmpty(xml)) + { + value = Convert.FromBase64String(xml); + } + else + { + value = Array.Empty(); + } + } + catch (XmlException xe) { - value = Convert.FromBase64String(xml); + throw CreateBadDecodingError(fieldName, xe); } - else + catch (InvalidOperationException ioe) { - value = Array.Empty(); + throw CreateBadDecodingError(fieldName, ioe); } // check the length. @@ -1044,9 +1104,22 @@ public NodeId ReadNodeId(string fieldName) if (BeginField(fieldName, true)) { PushNamespace(Namespaces.OpcUaXsd); - value.IdentifierText = ReadString("Identifier"); + string identifierText = ReadString("Identifier"); PopNamespace(); + try + { + value.IdentifierText = identifierText; + } + catch (ServiceResultException sre) when (sre.StatusCode == StatusCodes.BadNodeIdInvalid) + { + throw CreateBadDecodingError(fieldName, sre); + } + catch (ArgumentException ae) + { + throw CreateBadDecodingError(fieldName, ae); + } + EndField(fieldName); } @@ -1068,9 +1141,22 @@ public ExpandedNodeId ReadExpandedNodeId(string fieldName) if (BeginField(fieldName, true)) { PushNamespace(Namespaces.OpcUaXsd); - value.IdentifierText = ReadString("Identifier"); + string identifierText = ReadString("Identifier"); PopNamespace(); + try + { + value.IdentifierText = identifierText; + } + catch (ServiceResultException sre) when (sre.StatusCode == StatusCodes.BadNodeIdInvalid) + { + throw CreateBadDecodingError(fieldName, sre); + } + catch (ArgumentException ae) + { + throw CreateBadDecodingError(fieldName, ae); + } + EndField(fieldName); } @@ -1153,7 +1239,7 @@ public QualifiedName ReadQualifiedName(string fieldName) } else if (!isNil) { - name = String.Empty; + name = string.Empty; } PopNamespace(); @@ -1190,7 +1276,7 @@ public LocalizedText ReadLocalizedText(string fieldName) } else if (!isNil) { - locale = String.Empty; + locale = string.Empty; } if (BeginField("Text", true, out isNil)) @@ -1200,7 +1286,7 @@ public LocalizedText ReadLocalizedText(string fieldName) } else if (!isNil) { - text = String.Empty; + text = string.Empty; } LocalizedText value = new LocalizedText(locale, text); @@ -1352,16 +1438,14 @@ public ExtensionObject ReadExtensionObject(string fieldName) /// The system type of the encodeable object to be read /// The TypeId for the instance that will be read. /// An object that was read from the stream. - public IEncodeable ReadEncodeable(string fieldName, System.Type systemType, ExpandedNodeId encodeableTypeId = null) + public IEncodeable ReadEncodeable(string fieldName, Type systemType, ExpandedNodeId encodeableTypeId = null) { if (systemType == null) throw new ArgumentNullException(nameof(systemType)); - if (!(Activator.CreateInstance(systemType) is IEncodeable value)) { - throw new ServiceResultException( - StatusCodes.BadDecodingError, - Utils.Format("Type does not support IEncodeable interface: '{0}'", systemType.FullName)); + throw ServiceResultException.Create(StatusCodes.BadDecodingError, + "Type does not support IEncodeable interface: '{0}'", systemType.FullName); } if (encodeableTypeId != null) @@ -1393,9 +1477,8 @@ public IEncodeable ReadEncodeable(string fieldName, System.Type systemType, Expa { if (m_reader.NodeType == XmlNodeType.None) { - throw new ServiceResultException( - StatusCodes.BadDecodingError, - Utils.Format("Unexpected end of stream decoding field '{0}' for type '{1}'.", fieldName, systemType.FullName)); + throw ServiceResultException.Create(StatusCodes.BadDecodingError, + "Unexpected end of stream decoding field '{0}' for type '{1}'.", fieldName, systemType.FullName); } m_reader.Skip(); @@ -1405,6 +1488,11 @@ public IEncodeable ReadEncodeable(string fieldName, System.Type systemType, Expa EndField(fieldName); } } + catch (XmlException xe) + { + throw ServiceResultException.Create(StatusCodes.BadDecodingError, + "Error decoding field '{0}' for type '{1}': {2}", fieldName, systemType.Name, xe.Message); + } finally { m_nestingLevel--; @@ -1416,26 +1504,37 @@ public IEncodeable ReadEncodeable(string fieldName, System.Type systemType, Expa /// /// Reads an enumerated value from the stream. /// - public Enum ReadEnumerated(string fieldName, System.Type enumType) + public Enum ReadEnumerated(string fieldName, Type enumType) { Enum value = (Enum)Enum.GetValues(enumType).GetValue(0); if (BeginField(fieldName, true)) { - string xml = ReadString(); + string xml = SafeReadString(); - if (!String.IsNullOrEmpty(xml)) + if (!string.IsNullOrEmpty(xml)) { int index = xml.LastIndexOf('_'); - if (index != -1) + try { - int numericValue = Convert.ToInt32(xml.Substring(index + 1), CultureInfo.InvariantCulture); - value = (Enum)Enum.ToObject(enumType, numericValue); + if (index != -1) + { + int numericValue = Convert.ToInt32(xml.Substring(index + 1), CultureInfo.InvariantCulture); + value = (Enum)Enum.ToObject(enumType, numericValue); + } + else + { + value = (Enum)Enum.Parse(enumType, xml, false); + } } - else + catch (ArgumentException ae) { - value = (Enum)Enum.Parse(enumType, xml, false); + throw CreateBadDecodingError(fieldName, ae); + } + catch (FormatException fe) + { + throw CreateBadDecodingError(fieldName, fe); } } @@ -2402,7 +2501,7 @@ public ExtensionObjectCollection ReadExtensionObjectArray(string fieldName) /// The system type of the encodeable objects to be read object /// The TypeId for the instances that will be read. /// An array that was read from the stream. - public Array ReadEncodeableArray(string fieldName, System.Type systemType, ExpandedNodeId encodeableTypeId = null) + public Array ReadEncodeableArray(string fieldName, Type systemType, ExpandedNodeId encodeableTypeId = null) { if (systemType == null) throw new ArgumentNullException(nameof(systemType)); @@ -2452,7 +2551,7 @@ public Array ReadEncodeableArray(string fieldName, System.Type systemType, Expan /// /// Reads an enumerated value array from the stream. /// - public Array ReadEnumeratedArray(string fieldName, System.Type enumType) + public Array ReadEnumeratedArray(string fieldName, Type enumType) { if (enumType == null) throw new ArgumentNullException(nameof(enumType)); @@ -2895,20 +2994,35 @@ private Array ReadArrayElements(string fieldName, BuiltInType builtInType, Type /// /// Reads a string from the stream. /// - private string ReadString() + private string SafeReadString([CallerMemberName] string functionName = null) { - string value = m_reader.ReadContentAsString(); - - // check the length. - if (value != null) + string message; + try { - if (m_context.MaxStringLength > 0 && m_context.MaxStringLength < value.Length) + string value = m_reader.ReadContentAsString(); + + // check the length. + if (value != null) { - throw new ServiceResultException(StatusCodes.BadEncodingLimitsExceeded); + if (m_context.MaxStringLength > 0 && m_context.MaxStringLength < value.Length) + { + throw ServiceResultException.Create(StatusCodes.BadEncodingLimitsExceeded, + "ReadString in {0} exceeds MaxStringLength: {1} > {2}", functionName, value.Length, m_context.MaxStringLength); + } } - } - return value; + return value; + } + catch (XmlException xe) + { + message = xe.Message; + } + catch (InvalidOperationException ioe) + { + message = ioe.Message; + } + throw ServiceResultException.Create(StatusCodes.BadDecodingError, + "Unable to read string of {0}: {1}", functionName, message); } /// @@ -2916,8 +3030,7 @@ private string ReadString() /// private bool BeginField(string fieldName, bool isOptional) { - bool isNil = false; - return BeginField(fieldName, isOptional, out isNil); + return BeginField(fieldName, isOptional, out _); } /// @@ -2925,68 +3038,76 @@ private bool BeginField(string fieldName, bool isOptional) /// private bool BeginField(string fieldName, bool isOptional, out bool isNil) { - isNil = false; - - // move to the next node. - m_reader.MoveToContent(); - - // allow caller to skip reading element tag if field name is not specified. - if (String.IsNullOrEmpty(fieldName)) + try { - return true; - } + isNil = false; - // check if requested element is present. - if (!m_reader.IsStartElement(fieldName, m_namespaces.Peek())) - { - if (!isOptional) + // move to the next node. + m_reader.MoveToContent(); + + // allow caller to skip reading element tag if field name is not specified. + if (string.IsNullOrEmpty(fieldName)) { - throw new ServiceResultException( - StatusCodes.BadDecodingError, - Utils.Format("Encountered element: '{1}:{0}' when expecting element: '{2}:{3}'.", m_reader.LocalName, m_reader.NamespaceURI, fieldName, m_namespaces.Peek())); + return true; } - isNil = true; + // check if requested element is present. + if (!m_reader.IsStartElement(fieldName, m_namespaces.Peek())) + { + if (!isOptional) + { + throw ServiceResultException.Create(StatusCodes.BadDecodingError, + "Encountered element: '{1}:{0}' when expecting element: '{2}:{3}'.", m_reader.LocalName, m_reader.NamespaceURI, fieldName, m_namespaces.Peek()); + } - // nothing more to read. - return false; - } + isNil = true; - // check for empty or nil element. - if (m_reader.HasAttributes) - { - string nilValue = m_reader.GetAttribute("nil", Namespaces.XmlSchemaInstance); + // nothing more to read. + return false; + } - if (!String.IsNullOrEmpty(nilValue)) + // check for empty or nil element. + if (m_reader.HasAttributes) { - if (XmlConvert.ToBoolean(nilValue)) + string nilValue = m_reader.GetAttribute("nil", Namespaces.XmlSchemaInstance); + + if (!string.IsNullOrEmpty(nilValue)) { - isNil = true; + + if (SafeXmlConvert(fieldName, XmlConvert.ToBoolean, nilValue)) + { + isNil = true; + } } } - } - bool isEmpty = m_reader.IsEmptyElement; + bool isEmpty = m_reader.IsEmptyElement; - m_reader.ReadStartElement(); + m_reader.ReadStartElement(); - if (!isEmpty) - { - m_reader.MoveToContent(); - - // check for an element with no children but not empty (due to whitespace). - if (m_reader.NodeType == XmlNodeType.EndElement) + if (!isEmpty) { - if (m_reader.LocalName == fieldName && m_reader.NamespaceURI == m_namespaces.Peek()) + m_reader.MoveToContent(); + + // check for an element with no children but not empty (due to whitespace). + if (m_reader.NodeType == XmlNodeType.EndElement) { - m_reader.ReadEndElement(); - return false; + if (m_reader.LocalName == fieldName && m_reader.NamespaceURI == m_namespaces.Peek()) + { + m_reader.ReadEndElement(); + return false; + } } } - } - // caller must read contents of element. - return !isNil && !isEmpty; + // caller must read contents of element. + return !isNil && !isEmpty; + } + catch (XmlException xe) + { + throw ServiceResultException.Create(StatusCodes.BadDecodingError, + "Unable to read field {0}: {1}", fieldName, xe.Message); + } } /// @@ -2994,18 +3115,25 @@ private bool BeginField(string fieldName, bool isOptional, out bool isNil) /// private void EndField(string fieldName) { - if (!String.IsNullOrEmpty(fieldName)) + if (!string.IsNullOrEmpty(fieldName)) { - m_reader.MoveToContent(); + try + { + m_reader.MoveToContent(); - if (m_reader.NodeType != XmlNodeType.EndElement || m_reader.LocalName != fieldName || m_reader.NamespaceURI != m_namespaces.Peek()) + if (m_reader.NodeType != XmlNodeType.EndElement || m_reader.LocalName != fieldName || m_reader.NamespaceURI != m_namespaces.Peek()) + { + throw ServiceResultException.Create(StatusCodes.BadDecodingError, + "Encountered end element: '{1}:{0}' when expecting element: '{3}:{2}'.", m_reader.LocalName, m_reader.NamespaceURI, fieldName, m_namespaces.Peek()); + } + + m_reader.ReadEndElement(); + } + catch (XmlException xe) { - throw new ServiceResultException( - StatusCodes.BadDecodingError, - Utils.Format("Encountered end element: '{1}:{0}' when expecting element: '{3}:{2}'.", m_reader.LocalName, m_reader.NamespaceURI, fieldName, m_namespaces.Peek())); + throw ServiceResultException.Create(StatusCodes.BadDecodingError, + "Unable to read end field: {0}: {1}", fieldName, xe.Message); } - - m_reader.ReadEndElement(); } } @@ -3024,7 +3152,7 @@ private bool MoveToElement(string elementName) m_reader.Read(); } - if (String.IsNullOrEmpty(elementName)) + if (string.IsNullOrEmpty(elementName)) { return true; } @@ -3050,17 +3178,46 @@ private bool DetermineIEncodeableSystemType(ref Type systemType, ExpandedNodeId /// /// Test and increment the nesting level. /// - private void CheckAndIncrementNestingLevel() + private void CheckAndIncrementNestingLevel([CallerMemberName] string functionName = null) { if (m_nestingLevel > m_context.MaxEncodingNestingLevels) { - throw ServiceResultException.Create( - StatusCodes.BadEncodingLimitsExceeded, - "Maximum nesting level of {0} was exceeded", - m_context.MaxEncodingNestingLevels); + throw ServiceResultException.Create(StatusCodes.BadEncodingLimitsExceeded, + "Maximum nesting level of {0} in function {1} was exceeded", m_context.MaxEncodingNestingLevels, functionName); } m_nestingLevel++; } + + /// + /// Helper to create a BadDecodingError exception. + /// + private ServiceResultException CreateBadDecodingError(string fieldName, Exception ex, [CallerMemberName] string functionName = null) + { + return ServiceResultException.Create(StatusCodes.BadDecodingError, + "Unable to read field {0} in function {1}: {2}", fieldName, functionName, ex.Message); + } + + /// + /// Wrapper for XmlConvert calls which catches the + /// or " + /// and throws instead a with + /// StatusCode . + /// + private T SafeXmlConvert(string fieldName, Func converter, string xml, [CallerMemberName] string functionName = null) + { + try + { + return converter(xml); + } + catch (OverflowException ove) + { + throw CreateBadDecodingError(fieldName, ove, functionName); + } + catch (FormatException fe) + { + throw CreateBadDecodingError(fieldName, fe, functionName); + } + } #endregion #region Private Fields diff --git a/Stack/Opc.Ua.Core/Types/Encoders/XmlEncoder.cs b/Stack/Opc.Ua.Core/Types/Encoders/XmlEncoder.cs index 7dbf26ac76..d236493adc 100644 --- a/Stack/Opc.Ua.Core/Types/Encoders/XmlEncoder.cs +++ b/Stack/Opc.Ua.Core/Types/Encoders/XmlEncoder.cs @@ -303,16 +303,10 @@ public void EncodeMessage(IEncodeable message) { if (message == null) throw new ArgumentNullException(nameof(message)); - // convert the namespace uri to an index. - NodeId typeId = ExpandedNodeId.ToNodeId(message.TypeId, m_context.NamespaceUris); - PushNamespace(Namespaces.OpcUaXsd); - // write the type id. - WriteNodeId("TypeId", typeId); - // write the message. - WriteEncodeable("Body", message, message.GetType()); + WriteEncodeable(message.GetType().Name, message, message.GetType()); PopNamespace(); } diff --git a/Stack/Opc.Ua.Core/Types/Utils/DataComparer.cs b/Stack/Opc.Ua.Core/Types/Utils/DataComparer.cs index 0ae049935d..f36dd38b59 100644 --- a/Stack/Opc.Ua.Core/Types/Utils/DataComparer.cs +++ b/Stack/Opc.Ua.Core/Types/Utils/DataComparer.cs @@ -956,12 +956,13 @@ public static object GetExtensionObjectBody(ExtensionObject value) if (body is XmlElement xml) { XmlQualifiedName xmlName = Opc.Ua.EncodeableFactory.GetXmlName(expectedType); - XmlDecoder decoder = new XmlDecoder(xml, context); - - decoder.PushNamespace(xmlName.Namespace); - body = decoder.ReadEncodeable(xmlName.Name, expectedType); - decoder.PopNamespace(); - decoder.Close(); + using (XmlDecoder decoder = new XmlDecoder(xml, context)) + { + decoder.PushNamespace(xmlName.Namespace); + body = decoder.ReadEncodeable(xmlName.Name, expectedType); + decoder.PopNamespace(); + decoder.Close(); + } return (IEncodeable)body; } From 9110c91af6a2d803103fd78a9fe9bdf488f56550 Mon Sep 17 00:00:00 2001 From: Martin Regen Date: Fri, 17 May 2024 07:03:10 +0200 Subject: [PATCH 37/83] more encoder decoder tests --- .../Fuzz/FuzzableCode.BinaryDecoder.cs | 91 ++++++++++++++++++- .../Encoders/Fuzz/FuzzableCode.XmlDecoder.cs | 8 +- .../Opc.Ua.Core/Types/Encoders/XmlDecoder.cs | 8 +- .../CRLTests.cs | 2 - .../TestUtils.cs | 1 - 5 files changed, 99 insertions(+), 11 deletions(-) diff --git a/Fuzzing/Encoders/Fuzz/FuzzableCode.BinaryDecoder.cs b/Fuzzing/Encoders/Fuzz/FuzzableCode.BinaryDecoder.cs index 8a2dcd774a..6b06ba7030 100644 --- a/Fuzzing/Encoders/Fuzz/FuzzableCode.BinaryDecoder.cs +++ b/Fuzzing/Encoders/Fuzz/FuzzableCode.BinaryDecoder.cs @@ -29,6 +29,7 @@ using System; using System.IO; +using System.Linq; using Opc.Ua; /// @@ -74,6 +75,48 @@ public static void AflfuzzBinaryEncoder(Stream stream) } } + /// + /// The binary encoder indempotent fuzz target for afl-fuzz. + /// + /// The stdin stream from the afl-fuzz process. + public static void AflfuzzBinaryEncoderIndempotent(Stream stream) + { + IEncodeable encodeable = null; + byte[] serialized = null; + using (var memoryStream = PrepareArraySegmentStream(stream)) + { + try + { + encodeable = FuzzBinaryDecoderCore(memoryStream); + serialized = BinaryEncoder.EncodeMessage(encodeable, messageContext); + } + catch + { + return; + } + } + + // reencode the fuzzed object and see if they are indempotent + if (serialized != null) + { + using (var memoryStream = new MemoryStream(serialized)) + { + IEncodeable encodeable2 = FuzzBinaryDecoderCore(memoryStream); + byte[] serialized2 = BinaryEncoder.EncodeMessage(encodeable2, messageContext); + + if (!serialized.SequenceEqual(serialized2)) + { + throw new Exception("Indempotent encoding failed."); + } + + if (!Utils.IsEqual(encodeable, encodeable2)) + { + throw new Exception("Indempotent encoding failed."); + } + } + } + } + /// /// The binary decoder fuzz target for libfuzzer. /// @@ -86,7 +129,7 @@ public static void LibfuzzBinaryDecoder(ReadOnlySpan input) } /// - /// The binary encoder fuzz target for afl-fuzz. + /// The binary encoder fuzz target for libfuzzer. /// public static void LibfuzzBinaryEncoder(ReadOnlySpan input) { @@ -110,6 +153,47 @@ public static void LibfuzzBinaryEncoder(ReadOnlySpan input) } } + /// + /// The binary encoder indempotent fuzz target for libfuzzer. + /// + public static void LibfuzzBinaryEncoderIndempotent(ReadOnlySpan input) + { + IEncodeable encodeable = null; + byte[] serialized = null; + using (var memoryStream = new MemoryStream(input.ToArray())) + { + try + { + encodeable = FuzzBinaryDecoderCore(memoryStream); + serialized = BinaryEncoder.EncodeMessage(encodeable, messageContext); + } + catch + { + return; + } + } + + // reencode the fuzzed object and see if they are indempotent + if (serialized != null) + { + using (var memoryStream = new MemoryStream(serialized)) + { + IEncodeable encodeable2 = FuzzBinaryDecoderCore(memoryStream); + byte[] serialized2 = BinaryEncoder.EncodeMessage(encodeable2, messageContext); + + if (!serialized.SequenceEqual(serialized2)) + { + throw new Exception("Indempotent decoding failed."); + } + + if (!Utils.IsEqual(encodeable, encodeable2)) + { + throw new Exception("Indempotent encoding failed."); + } + } + } + } + /// /// The fuzz target for the BinaryDecoder. /// @@ -134,9 +218,14 @@ internal static IEncodeable FuzzBinaryDecoderCore(MemoryStream stream, bool thro return null; } break; + + default: + break; + } throw; + } } } diff --git a/Fuzzing/Encoders/Fuzz/FuzzableCode.XmlDecoder.cs b/Fuzzing/Encoders/Fuzz/FuzzableCode.XmlDecoder.cs index 40e89134df..4a82261cbb 100644 --- a/Fuzzing/Encoders/Fuzz/FuzzableCode.XmlDecoder.cs +++ b/Fuzzing/Encoders/Fuzz/FuzzableCode.XmlDecoder.cs @@ -120,11 +120,13 @@ internal static IEncodeable FuzzXmlDecoderCore(Stream stream, bool throwAll = fa { try { - using (var reader = XmlReader.Create(stream, Utils.DefaultXmlReaderSettings())) + XmlReader reader = null; + try { Type systemType = null; try { + reader = XmlReader.Create(stream, Utils.DefaultXmlReaderSettings()); reader.MoveToContent(); string typeName = reader.LocalName; string namespaceUri = reader.NamespaceURI; @@ -157,6 +159,10 @@ internal static IEncodeable FuzzXmlDecoderCore(Stream stream, bool throwAll = fa return decoder.DecodeMessage(systemType); } } + finally + { + Utils.SilentDispose(reader); + } } catch (ServiceResultException sre) { diff --git a/Stack/Opc.Ua.Core/Types/Encoders/XmlDecoder.cs b/Stack/Opc.Ua.Core/Types/Encoders/XmlDecoder.cs index f5c012944e..dbabeee12c 100644 --- a/Stack/Opc.Ua.Core/Types/Encoders/XmlDecoder.cs +++ b/Stack/Opc.Ua.Core/Types/Encoders/XmlDecoder.cs @@ -1528,13 +1528,9 @@ public Enum ReadEnumerated(string fieldName, Type enumType) value = (Enum)Enum.Parse(enumType, xml, false); } } - catch (ArgumentException ae) + catch (Exception ex) when (ex is ArgumentException || ex is FormatException || ex is OverflowException) { - throw CreateBadDecodingError(fieldName, ae); - } - catch (FormatException fe) - { - throw CreateBadDecodingError(fieldName, fe); + throw CreateBadDecodingError(fieldName, ex); } } diff --git a/Tests/Opc.Ua.Security.Certificates.Tests/CRLTests.cs b/Tests/Opc.Ua.Security.Certificates.Tests/CRLTests.cs index b06b7be948..536f78fd0b 100644 --- a/Tests/Opc.Ua.Security.Certificates.Tests/CRLTests.cs +++ b/Tests/Opc.Ua.Security.Certificates.Tests/CRLTests.cs @@ -29,9 +29,7 @@ using System; -using System.Collections.Generic; using System.Globalization; -using System.IO; using System.Security.Cryptography; using System.Security.Cryptography.X509Certificates; using System.Text; diff --git a/Tests/Opc.Ua.Security.Certificates.Tests/TestUtils.cs b/Tests/Opc.Ua.Security.Certificates.Tests/TestUtils.cs index 9483d75931..bc09c332f5 100644 --- a/Tests/Opc.Ua.Security.Certificates.Tests/TestUtils.cs +++ b/Tests/Opc.Ua.Security.Certificates.Tests/TestUtils.cs @@ -33,7 +33,6 @@ using System.IO; using System.Linq; using System.Security.Cryptography.X509Certificates; -using NUnit.Framework; using Opc.Ua.Security.Certificates; using Assert = NUnit.Framework.Legacy.ClassicAssert; From 1ff9eff89aefd57b174a80ad1ec8f4447e24098f Mon Sep 17 00:00:00 2001 From: Martin Regen Date: Tue, 2 Jul 2024 06:05:16 +0200 Subject: [PATCH 38/83] encoder fixes --- Stack/Opc.Ua.Core/Types/BuiltIn/NodeId.cs | 5 + .../Types/Encoders/ArrayPoolBufferSegment.cs | 89 ++++++++++ .../Types/Encoders/ArrayPoolBufferWriter.cs | 166 ++++++++++++++++++ .../Types/Encoders/BinaryDecoder.cs | 26 ++- .../Types/Encoders/BinaryEncoder.cs | 109 +++++++++++- Stack/Opc.Ua.Core/Types/Encoders/IEncoder.cs | 12 ++ .../Opc.Ua.Core/Types/Encoders/JsonEncoder.cs | 70 +++++++- .../Opc.Ua.Core/Types/Encoders/XmlEncoder.cs | 40 ++++- 8 files changed, 497 insertions(+), 20 deletions(-) create mode 100644 Stack/Opc.Ua.Core/Types/Encoders/ArrayPoolBufferSegment.cs create mode 100644 Stack/Opc.Ua.Core/Types/Encoders/ArrayPoolBufferWriter.cs diff --git a/Stack/Opc.Ua.Core/Types/BuiltIn/NodeId.cs b/Stack/Opc.Ua.Core/Types/BuiltIn/NodeId.cs index dd3d0ceed9..d186696280 100644 --- a/Stack/Opc.Ua.Core/Types/BuiltIn/NodeId.cs +++ b/Stack/Opc.Ua.Core/Types/BuiltIn/NodeId.cs @@ -1573,6 +1573,11 @@ private int CompareTo(IdType idType, object id) byte[] id1 = (byte[])m_identifier; byte[] id2 = (byte[])id; + if (id1 == id2) + { + return 0; + } + if (id1.Length == id2.Length) { for (int ii = 0; ii < id1.Length; ii++) diff --git a/Stack/Opc.Ua.Core/Types/Encoders/ArrayPoolBufferSegment.cs b/Stack/Opc.Ua.Core/Types/Encoders/ArrayPoolBufferSegment.cs new file mode 100644 index 0000000000..de2ee159d5 --- /dev/null +++ b/Stack/Opc.Ua.Core/Types/Encoders/ArrayPoolBufferSegment.cs @@ -0,0 +1,89 @@ +/* ======================================================================== + * Copyright (c) 2005-2024 The OPC Foundation, Inc. All rights reserved. + * + * OPC Foundation MIT License 1.00 + * + * Permission is hereby granted, free of charge, to any person + * obtaining a copy of this software and associated documentation + * files (the "Software"), to deal in the Software without + * restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following + * conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES + * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT + * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, + * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR + * OTHER DEALINGS IN THE SOFTWARE. + * + * The complete license agreement can be found here: + * http://opcfoundation.org/License/MIT/1.00/ + * ======================================================================*/ + + +using System; +using System.Buffers; + +namespace Opc.Ua +{ + + /// + /// Helper to build a ReadOnlySequence from a set of allocated buffers. + /// + public sealed class ArrayPoolBufferSegment : ReadOnlySequenceSegment + { + private T[] _array; + + /// + /// Initializes a new instance of the class. + /// + public ArrayPoolBufferSegment(T[] array, int offset, int length) + { + Memory = new ReadOnlyMemory(array, offset, length); + _array = array; + } + + /// + /// Rents a buffer from the pool and returns a instance. + /// + /// The length of the segment. + public static ArrayPoolBufferSegment Rent(int length) + { + var array = ArrayPool.Shared.Rent(length); + return new ArrayPoolBufferSegment(array, 0, length); + } + + /// + /// Returns the base array of the buffer. + /// + public T[] Array() => _array; + + /// + /// Rents a new buffer and appends it to the sequence. + /// + public ArrayPoolBufferSegment RentAndAppend(int length) + { + var array = ArrayPool.Shared.Rent(length); + return Append(array, 0, length); + } + + /// + /// Appends a buffer to the sequence. + /// + public ArrayPoolBufferSegment Append(T[] array, int offset, int length) + { + var segment = new ArrayPoolBufferSegment(array, offset, length) { + RunningIndex = RunningIndex + Memory.Length, + }; + Next = segment; + return segment; + } + } +} diff --git a/Stack/Opc.Ua.Core/Types/Encoders/ArrayPoolBufferWriter.cs b/Stack/Opc.Ua.Core/Types/Encoders/ArrayPoolBufferWriter.cs new file mode 100644 index 0000000000..9c8d65a300 --- /dev/null +++ b/Stack/Opc.Ua.Core/Types/Encoders/ArrayPoolBufferWriter.cs @@ -0,0 +1,166 @@ +/* ======================================================================== + * Copyright (c) 2005-2024 The OPC Foundation, Inc. All rights reserved. + * + * OPC Foundation MIT License 1.00 + * + * Permission is hereby granted, free of charge, to any person + * obtaining a copy of this software and associated documentation + * files (the "Software"), to deal in the Software without + * restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following + * conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES + * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT + * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, + * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR + * OTHER DEALINGS IN THE SOFTWARE. + * + * The complete license agreement can be found here: + * http://opcfoundation.org/License/MIT/1.00/ + * ======================================================================*/ + +using System; +using System.Buffers; +using System.Diagnostics; + +namespace Opc.Ua +{ + /// + /// Helper to build a from a set of buffers. + /// Implements interface. + /// + public sealed class ArrayPoolBufferWriter : IBufferWriter, IDisposable + { +#if DEBUG + private const bool _clearArray = true; +#else + private const bool _clearArray = false; +#endif + + private const int DefaultChunkSize = 1024; + private const int MaxChunkSize = 65536; + private int _chunkSize; + private T[] _currentBuffer; + private ArrayPoolBufferSegment _firstSegment; + private ArrayPoolBufferSegment _nextSegment; + private int _offset; + + /// + /// Initializes a new instance of the class. + /// + public ArrayPoolBufferWriter(int chunksize = DefaultChunkSize, int maxChunkSize = MaxChunkSize) + { + _firstSegment = _nextSegment = null; + _offset = 0; + _chunkSize = chunksize; + _currentBuffer = Array.Empty(); + } + + /// + public void Dispose() + { + if (_firstSegment != null) + { + ArrayPoolBufferSegment segment = _firstSegment; + while (segment != null) + { + ArrayPool.Shared.Return(segment.Array(), _clearArray); + segment = (ArrayPoolBufferSegment)segment.Next; + } + + _firstSegment = _nextSegment = null; + GC.SuppressFinalize(this); + } + } + + /// + public void Advance(int count) + { + _offset += count; + Debug.Assert(_offset <= _currentBuffer.Length, "The offset was advanced beyond the length of the current buffer."); + } + + /// + public Memory GetMemory(int sizeHint = 0) + { + int remainingSpace = CheckAndAllocateBuffer(sizeHint); + return _currentBuffer.AsMemory(_offset, remainingSpace); + } + + /// + public Span GetSpan(int sizeHint = 0) + { + int remainingSpace = CheckAndAllocateBuffer(sizeHint); + return _currentBuffer.AsSpan(_offset, remainingSpace); + } + + /// + /// Get a ReadOnlySequence that represents the written data. + /// The sequence is only valid until the next write operation or + /// until the writer is disposed. + /// + public ReadOnlySequence GetReadOnlySequence() + { + AddSegment(); + + if (_firstSegment == null || _nextSegment == null) + { + return ReadOnlySequence.Empty; + } + + return new ReadOnlySequence(_firstSegment, 0, _nextSegment, _offset); + } + + private void AddSegment() + { + if (_offset > 0) + { + if (_firstSegment == null) + { + _firstSegment = _nextSegment = new ArrayPoolBufferSegment(_currentBuffer, 0, _offset); + } + else + { + _nextSegment = _nextSegment.Append(_currentBuffer, 0, _offset); + } + } + else + { + if (_currentBuffer.Length > 0) + { + ArrayPool.Shared.Return(_currentBuffer, _clearArray); + } + + _currentBuffer = Array.Empty(); + } + } + + private int CheckAndAllocateBuffer(int sizeHint) + { + int remainingSpace = _currentBuffer.Length - _offset; + if (remainingSpace < sizeHint || sizeHint == 0) + { + AddSegment(); + + remainingSpace = Math.Max(sizeHint, _chunkSize); + _currentBuffer = ArrayPool.Shared.Rent(remainingSpace); + _offset = 0; + + if (_chunkSize < MaxChunkSize) + { + _chunkSize *= 2; + } + } + + return remainingSpace; + } + } +} diff --git a/Stack/Opc.Ua.Core/Types/Encoders/BinaryDecoder.cs b/Stack/Opc.Ua.Core/Types/Encoders/BinaryDecoder.cs index 2c5e527e96..be69474981 100644 --- a/Stack/Opc.Ua.Core/Types/Encoders/BinaryDecoder.cs +++ b/Stack/Opc.Ua.Core/Types/Encoders/BinaryDecoder.cs @@ -425,8 +425,12 @@ public string ReadString(string fieldName, int maxStringLength) // length is always >= 1 here - // If 0 terminated, decrease length by one before converting to string - var utf8StringLength = bytes[bytes.Length - 1] == 0 ? bytes.Length - 1 : bytes.Length; + // If 0 terminated, decrease length to remove 0 terminators before converting to string + int utf8StringLength = bytes.Length; + while (utf8StringLength > 0 && bytes[utf8StringLength - 1] == 0) + { + utf8StringLength--; + } return Encoding.UTF8.GetString(bytes, 0, utf8StringLength); } @@ -1569,7 +1573,12 @@ private DiagnosticInfo ReadDiagnosticInfo(string fieldName, int depth) if ((encodingByte & (byte)DiagnosticInfoEncodingBits.InnerDiagnosticInfo) != 0) { - value.InnerDiagnosticInfo = ReadDiagnosticInfo(null, depth + 1); + value.InnerDiagnosticInfo = ReadDiagnosticInfo(null, depth + 1) ?? new DiagnosticInfo(); + } + + if (value.IsNullDiagnosticInfo) + { + // return null; } return value; @@ -2272,7 +2281,14 @@ private Variant ReadVariantValue(string fieldName) "ArrayDimensions length does not match with the ArrayLength in Variant object."); } - value = new Variant(new Matrix(array, builtInType, dimensions.ToArray())); + if (dimensions.Count == 1) + { + value = new Variant(array, new TypeInfo(builtInType, 1)); + } + else + { + value = new Variant(new Matrix(array, builtInType, dimensionsArray)); + } } else { @@ -2389,7 +2405,7 @@ private Variant ReadVariantValue(string fieldName) } catch (Exception ex) { - Utils.LogError(ex, "Error reading xml element for variant."); + Utils.LogTrace(ex, "Error reading xml element for variant."); value.Set(StatusCodes.BadDecodingError); } break; diff --git a/Stack/Opc.Ua.Core/Types/Encoders/BinaryEncoder.cs b/Stack/Opc.Ua.Core/Types/Encoders/BinaryEncoder.cs index dfad98c2d1..acee1799e0 100644 --- a/Stack/Opc.Ua.Core/Types/Encoders/BinaryEncoder.cs +++ b/Stack/Opc.Ua.Core/Types/Encoders/BinaryEncoder.cs @@ -11,6 +11,7 @@ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. */ using System; +using System.Buffers; using System.Collections.Generic; using System.Globalization; using System.IO; @@ -442,18 +443,51 @@ public void WriteString(string fieldName, string value) return; } - byte[] bytes = Encoding.UTF8.GetBytes(value); - - if (m_context.MaxStringLength > 0 && m_context.MaxStringLength < bytes.Length) + if (m_context.MaxStringLength > 0 && m_context.MaxStringLength < value.Length) { throw ServiceResultException.Create( StatusCodes.BadEncodingLimitsExceeded, "MaxStringLength {0} < {1}", m_context.MaxStringLength, - bytes.Length); + value.Length); } - WriteByteString(null, Encoding.UTF8.GetBytes(value)); + int maxByteCount = Encoding.UTF8.GetMaxByteCount(value.Length); +#if NETSTANDARD2_1_OR_GREATER || NET5_0_OR_GREATER +#if NET5_0_OR_GREATER + const int maxByteCountPerBuffer = 8192; +#endif + const int maxStackAllocByteCount = 128; + if (maxByteCount <= maxStackAllocByteCount) + { + Span encodedBytes = stackalloc byte[maxByteCount]; + int count = Encoding.UTF8.GetBytes(value, encodedBytes); + WriteByteString(null, encodedBytes.Slice(0, count)); + } +#if NET5_0_OR_GREATER + else if (maxByteCount > maxByteCountPerBuffer) + { + using (var bufferWriter = new ArrayPoolBufferWriter(maxByteCountPerBuffer)) + { + long count = Encoding.UTF8.GetBytes(value.AsSpan(), bufferWriter); + WriteByteString(null, bufferWriter.GetReadOnlySequence()); + } + } + else +#endif +#endif + { + byte[] encodedBytes = ArrayPool.Shared.Rent(maxByteCount); + try + { + int count = Encoding.UTF8.GetBytes(value, 0, value.Length, encodedBytes, 0); + WriteByteString(null, encodedBytes, 0, count); + } + finally + { + ArrayPool.Shared.Return(encodedBytes); + } + } } /// @@ -505,6 +539,39 @@ public void WriteGuid(string fieldName, Guid value) /// Writes a byte string to the stream. /// public void WriteByteString(string fieldName, byte[] value) + { + WriteByteString( fieldName, value, 0, value.Length); + } + + /// + /// Writes a byte string to the stream from a given index and length. + /// + public void WriteByteString(string fieldName, byte[] value, int index, int count) + { + if (value == null) + { + WriteInt32(null, -1); + return; + } + + if (m_context.MaxByteStringLength > 0 && m_context.MaxByteStringLength < count) + { + throw ServiceResultException.Create( + StatusCodes.BadEncodingLimitsExceeded, + "MaxByteStringLength {0} < {1}", + m_context.MaxByteStringLength, + count); + } + + WriteInt32(null, count); + m_writer.Write(value, index, count); + } + +#if NETSTANDARD2_1_OR_GREATER || NET5_0_OR_GREATER + /// + /// Writes a byte string to the stream. + /// + public void WriteByteString(string fieldName, ReadOnlySpan value) { if (value == null) { @@ -524,6 +591,7 @@ public void WriteByteString(string fieldName, byte[] value) WriteInt32(null, value.Length); m_writer.Write(value); } +#endif /// /// Writes an XmlElement to the stream. @@ -536,7 +604,7 @@ public void WriteXmlElement(string fieldName, XmlElement value) return; } - WriteByteString(null, Encoding.UTF8.GetBytes(value.OuterXml)); + WriteString(fieldName, value.OuterXml); } /// @@ -1917,6 +1985,35 @@ public void WriteArray(string fieldName, object array, int valueRank, BuiltInTyp #endregion #region Private Methods +#if NETSTANDARD2_1_OR_GREATER || NET5_0_OR_GREATER + /// + /// Writes a byte string to the stream. + /// + private void WriteByteString(string fieldName, ReadOnlySequence value) + { + if (value.IsEmpty) + { + WriteInt32(null, -1); + return; + } + + if (m_context.MaxByteStringLength > 0 && m_context.MaxByteStringLength < value.Length) + { + throw ServiceResultException.Create( + StatusCodes.BadEncodingLimitsExceeded, + "MaxByteStringLength {0} < {1}", + m_context.MaxByteStringLength, + value.Length); + } + + WriteInt32(null, (int)value.Length); + foreach (var segment in value) + { + m_writer.Write(segment.Span); + } + } +#endif + /// /// Writes a DiagnosticInfo to the stream. /// Ignores InnerDiagnosticInfo field if the nesting level diff --git a/Stack/Opc.Ua.Core/Types/Encoders/IEncoder.cs b/Stack/Opc.Ua.Core/Types/Encoders/IEncoder.cs index 2fd8414e43..4f071ec983 100644 --- a/Stack/Opc.Ua.Core/Types/Encoders/IEncoder.cs +++ b/Stack/Opc.Ua.Core/Types/Encoders/IEncoder.cs @@ -11,6 +11,7 @@ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. */ using System; +using System.Buffers; using System.Collections.Generic; using System.Xml; @@ -148,6 +149,17 @@ public interface IEncoder : IDisposable /// void WriteByteString(string fieldName, byte[] value); + /// + /// Writes a byte string to the stream with a given index and count. + /// + void WriteByteString(string fieldName, byte[] value, int index, int count); + +#if NETSTANDARD2_1_OR_GREATER || NET5_0_OR_GREATER + /// + /// Writes a byte string to the stream. + /// + void WriteByteString(string fieldName, ReadOnlySpan value); +#endif /// /// Writes an XmlElement to the stream. /// diff --git a/Stack/Opc.Ua.Core/Types/Encoders/JsonEncoder.cs b/Stack/Opc.Ua.Core/Types/Encoders/JsonEncoder.cs index fa53fded3d..7b1aa7b882 100644 --- a/Stack/Opc.Ua.Core/Types/Encoders/JsonEncoder.cs +++ b/Stack/Opc.Ua.Core/Types/Encoders/JsonEncoder.cs @@ -11,6 +11,7 @@ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. */ using System; +using System.Buffers; using System.Collections.Generic; using System.Globalization; using System.IO; @@ -977,6 +978,35 @@ public void WriteGuid(string fieldName, Guid value) /// Writes a byte string to the stream. /// public void WriteByteString(string fieldName, byte[] value) + { + WriteByteString(fieldName, value, 0, value.Length); + } + + /// + /// Writes a byte string to the stream with a given index and count. + /// + public void WriteByteString(string fieldName, byte[] value, int index, int count) + { + if (value == null) + { + WriteSimpleFieldNull(fieldName); + return; + } + + // check the length. + if (m_context.MaxByteStringLength > 0 && m_context.MaxByteStringLength < count) + { + throw new ServiceResultException(StatusCodes.BadEncodingLimitsExceeded); + } + + WriteSimpleField(fieldName, Convert.ToBase64String(value, index, count), EscapeOptions.Quotes | EscapeOptions.NoValueEscape); + } + +#if NETSTANDARD2_1_OR_GREATER || NET5_0_OR_GREATER + /// + /// Writes a byte string to the stream. + /// + public void WriteByteString(string fieldName, ReadOnlySpan value) { if (value == null) { @@ -992,6 +1022,7 @@ public void WriteByteString(string fieldName, byte[] value) WriteSimpleField(fieldName, Convert.ToBase64String(value), EscapeOptions.Quotes | EscapeOptions.NoValueEscape); } +#endif /// /// Writes an XmlElement to the stream. @@ -1005,9 +1036,27 @@ public void WriteXmlElement(string fieldName, XmlElement value) } var xml = value.OuterXml; - var bytes = Encoding.UTF8.GetBytes(xml); + int maxByteCount = Encoding.UTF8.GetMaxByteCount(xml.Length); + byte[] encodedBytes = ArrayPool.Shared.Rent(maxByteCount); + try + { + int count = Encoding.UTF8.GetBytes(xml, 0, xml.Length, encodedBytes, 0); - WriteSimpleField(fieldName, Convert.ToBase64String(bytes), EscapeOptions.Quotes | EscapeOptions.NoValueEscape); + if (m_context.MaxByteStringLength > 0 && m_context.MaxByteStringLength < count) + { + throw ServiceResultException.Create( + StatusCodes.BadEncodingLimitsExceeded, + "MaxByteStringLength {0} < {1}", + m_context.MaxByteStringLength, + count); + } + + WriteSimpleField(fieldName, Convert.ToBase64String(encodedBytes, 0, count), EscapeOptions.Quotes | EscapeOptions.NoValueEscape); + } + finally + { + ArrayPool.Shared.Return(encodedBytes); + } } private void WriteNamespaceIndex(string fieldName, ushort namespaceIndex) @@ -1062,7 +1111,20 @@ private void WriteNodeIdContents(NodeId value, string namespaceUri = null) case IdType.Guid: { - WriteGuid("Id", (Guid)value.Identifier); + if (value.Identifier is Guid guidIdentifier) + { + WriteGuid("Id", guidIdentifier); + } + else if (value.Identifier is Uuid uuidIdentifier) + { + WriteGuid("Id", uuidIdentifier); + } + else + { + throw new ServiceResultException( + StatusCodes.BadEncodingError, + "Invalid Identifier type to encode as Guid NodeId."); + } break; } @@ -2347,7 +2409,7 @@ public void WriteEnumeratedArray(string fieldName, Array values, System.Type sys PopArray(); } - #endregion +#endregion #region Public Methods /// diff --git a/Stack/Opc.Ua.Core/Types/Encoders/XmlEncoder.cs b/Stack/Opc.Ua.Core/Types/Encoders/XmlEncoder.cs index d236493adc..5db67dfed3 100644 --- a/Stack/Opc.Ua.Core/Types/Encoders/XmlEncoder.cs +++ b/Stack/Opc.Ua.Core/Types/Encoders/XmlEncoder.cs @@ -12,6 +12,7 @@ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. using System; using System.Collections.Generic; +using System.Diagnostics; using System.Globalization; using System.IO; using System.Text; @@ -518,12 +519,24 @@ public void WriteGuid(string fieldName, Guid value) /// public void WriteByteString(string fieldName, byte[] value) { - WriteByteString(fieldName, value, false); + WriteByteString(fieldName, value, 0, value?.Length ?? 0, false); } - private void WriteByteString(string fieldName, byte[] value, bool isArrayElement = false) + /// + /// Writes a byte string to the stream with a given index and count. + /// + public void WriteByteString(string fieldName, byte[] value, int index, int count) { - if (BeginField(fieldName, value == null, true, isArrayElement)) + WriteByteString(fieldName, value, index, count, false); + } + +#if NETSTANDARD2_1_OR_GREATER || NET5_0_OR_GREATER + /// + /// Writes a byte string to the stream. + /// + public void WriteByteString(string fieldName, ReadOnlySpan value) + { + if (BeginField(fieldName, value == null, true, false)) { // check the length. if (m_context.MaxByteStringLength > 0 && m_context.MaxByteStringLength < value.Length) @@ -535,6 +548,23 @@ private void WriteByteString(string fieldName, byte[] value, bool isArrayElement EndField(fieldName); } } +#endif + + private void WriteByteString(string fieldName, byte[] value, int index, int count, bool isArrayElement = false) + { + Debug.Assert(value == null || value.Length >= count - index); + if (BeginField(fieldName, value == null, true, isArrayElement)) + { + // check the length. + if (m_context.MaxByteStringLength > 0 && m_context.MaxByteStringLength < count) + { + throw new ServiceResultException(StatusCodes.BadEncodingLimitsExceeded); + } + + m_writer.WriteValue(Convert.ToBase64String(value, index, count, Base64FormattingOptions.InsertLineBreaks)); + EndField(fieldName); + } + } /// /// Writes an XmlElement to the stream. @@ -1363,7 +1393,7 @@ public void WriteByteStringArray(string fieldName, IList values) { for (int ii = 0; ii < values.Count; ii++) { - WriteByteString("ByteString", values[ii], true); + WriteByteString("ByteString", values[ii], 0, values[ii]?.Length ?? 0, true); } } @@ -1747,7 +1777,7 @@ public void WriteEnumeratedArray(string fieldName, Array values, System.Type sys EndField(fieldName); } } - #endregion +#endregion #region Public Methods /// From 58be22b04dd8e7fa743a018dab2a475ebf8a1bfd Mon Sep 17 00:00:00 2001 From: Martin Regen Date: Tue, 16 Jul 2024 15:27:03 +0200 Subject: [PATCH 39/83] add fuz tests for artifacts --- .azurepipelines/test.yml | 12 ++ ...h-0483107a22caf892cff4f795c96ab14c581a8f55 | Bin 0 -> 119 bytes ...t-0cd8a7ed364195b9e9aa896391d10ab4f3d693a1 | Bin 0 -> 2238 bytes ...t-8b790a0680444b262058a1d24d01822a91e8af06 | Bin 0 -> 3853 bytes Fuzzing/Encoders/Fuzz.Tests/EncoderTests.cs | 164 ++++++++++++++++++ .../Opc.Ua.Encoders.Fuzz.Tests.csproj | 75 ++++++++ Fuzzing/Encoders/Fuzz.Tests/TestUtils.cs | 107 ++++++++++++ Fuzzing/Encoders/aflfuzz.sh | 29 ++-- .../Types/Encoders/BinaryEncoder.cs | 2 +- .../Opc.Ua.Core/Types/Encoders/JsonEncoder.cs | 2 +- .../Opc.Ua.Core/Types/Encoders/XmlEncoder.cs | 2 +- UA Core Library.sln | 7 + UA Fuzzing.sln | 15 ++ 13 files changed, 400 insertions(+), 15 deletions(-) create mode 100644 Fuzzing/Encoders/Fuzz.Tests/Assets/crash-0483107a22caf892cff4f795c96ab14c581a8f55 create mode 100644 Fuzzing/Encoders/Fuzz.Tests/Assets/slow-unit-0cd8a7ed364195b9e9aa896391d10ab4f3d693a1 create mode 100644 Fuzzing/Encoders/Fuzz.Tests/Assets/timeout-8b790a0680444b262058a1d24d01822a91e8af06 create mode 100644 Fuzzing/Encoders/Fuzz.Tests/EncoderTests.cs create mode 100644 Fuzzing/Encoders/Fuzz.Tests/Opc.Ua.Encoders.Fuzz.Tests.csproj create mode 100644 Fuzzing/Encoders/Fuzz.Tests/TestUtils.cs diff --git a/.azurepipelines/test.yml b/.azurepipelines/test.yml index 9a285c0951..0097d4c88c 100644 --- a/.azurepipelines/test.yml +++ b/.azurepipelines/test.yml @@ -65,6 +65,18 @@ jobs: command: restore projects: $(file) arguments: '${{ variables.DotCliCommandline }} --configuration ${{ parameters.configuration }}' + - task: DownloadSecureFile@1 + name: fuzzingartifacts + displayName: 'Download Fuzzing artifacts' + inputs: + secureFile: 'FuzzingArtifacts.zip' + - task: ExtractFiles@1 + displayName: 'Extract Fuzzing artifacts' + inputs: + archiveFilePatterns: $(fuzzingartifacts.secureFilePath) + destinationFolder: ./Fuzzing/Encoders/Fuzz.Tests/Assets + cleanDestinationFolder: false + overwriteExistingFiles: true - task: DotNetCoreCLI@2 displayName: Test ${{ parameters.configuration }} timeoutInMinutes: 30 diff --git a/Fuzzing/Encoders/Fuzz.Tests/Assets/crash-0483107a22caf892cff4f795c96ab14c581a8f55 b/Fuzzing/Encoders/Fuzz.Tests/Assets/crash-0483107a22caf892cff4f795c96ab14c581a8f55 new file mode 100644 index 0000000000000000000000000000000000000000..a5d07228da3cf00992029738ea40d210c459a182 GIT binary patch literal 119 wcmZQ%u(M%gKm-3Fpb<@&;T%YS6)Z}G61eWX6b6R4h5~M&fJZ8j`2XJq07V-XDF6Tf literal 0 HcmV?d00001 diff --git a/Fuzzing/Encoders/Fuzz.Tests/Assets/slow-unit-0cd8a7ed364195b9e9aa896391d10ab4f3d693a1 b/Fuzzing/Encoders/Fuzz.Tests/Assets/slow-unit-0cd8a7ed364195b9e9aa896391d10ab4f3d693a1 new file mode 100644 index 0000000000000000000000000000000000000000..9098b2add33ce150b599edf9fc8cfec0c431d4cd GIT binary patch literal 2238 zcmZQ%P-b>Gq|VI1z`zK^3=FA2eDgnu)yQBOn*n0LK_i0&L6wL}EMk zmoHzFsyI3y9sjN?B6=7+aJ!qc7)*PZ*9aADc3k z4I$b)SmhAI?S`*TnLrP=s5ey{>QRUiQ@E^c>K^~$p|GMvRw@Z;*LaRF*;rMh92ALW zae`h+S|~f2z<^i5;Eg^G6LTKURx4@;_xVf((E}H~E)H+BxFKS{@7j~4g|d?g4ESYn zo-bQv_P*2cg&>B{T@?wj(eEC>gt)@^H;_R22*}3lDI2z6zAVne*=j}0T@w*RmM!#X zc#6N=T!|y8)yx*`pn1bTEf4!cXERh}DT(~O58ldGq?H*`s=jIlpRe9S0dA?1GXl?g gWTf5%x?xhGh3c-11ipc>&!Ac!N6P5`C$V}O?plPo4gdfE literal 0 HcmV?d00001 diff --git a/Fuzzing/Encoders/Fuzz.Tests/EncoderTests.cs b/Fuzzing/Encoders/Fuzz.Tests/EncoderTests.cs new file mode 100644 index 0000000000..8cec73d7fa --- /dev/null +++ b/Fuzzing/Encoders/Fuzz.Tests/EncoderTests.cs @@ -0,0 +1,164 @@ +/* ======================================================================== + * Copyright (c) 2005-2024 The OPC Foundation, Inc. All rights reserved. + * + * OPC Foundation MIT License 1.00 + * + * Permission is hereby granted, free of charge, to any person + * obtaining a copy of this software and associated documentation + * files (the "Software"), to deal in the Software without + * restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following + * conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES + * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT + * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, + * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR + * OTHER DEALINGS IN THE SOFTWARE. + * + * The complete license agreement can be found here: + * http://opcfoundation.org/License/MIT/1.00/ + * ======================================================================*/ + +using System.IO; +using System.Text; +using NUnit.Framework; +using System.Linq; +using System; +using Opc.Ua.Tests; +using System.Reflection; + +namespace Opc.Ua.Fuzzing +{ + + [TestFixture] + public class EncoderTests + { + #region DataPointSources + public static readonly TestcaseAsset[] GoodTestcases = new AssetCollection(TestUtils.EnumerateTestAssets("Testcases", "*.*")).ToArray(); + public static readonly TestcaseAsset[] CrashAssets = new AssetCollection(TestUtils.EnumerateTestAssets("Assets", "crash*.*")).ToArray(); + public static readonly TestcaseAsset[] TimeoutAssets = new AssetCollection(TestUtils.EnumerateTestAssets("Assets", "timeout*.*")).ToArray(); + public static readonly TestcaseAsset[] SlowAssets = new AssetCollection(TestUtils.EnumerateTestAssets("Assets", "slow*.*")).ToArray(); + + [DatapointSource] + public static string[] TestcaseEncoderSuffixes = new string[] { ".Binary", ".Json", ".Xml" }; + + [DatapointSource] + public static FuzzTargetFunction[] FuzzableFunctions = typeof(FuzzableCode).GetMethods(BindingFlags.Static | BindingFlags.Public) + .Where(f => f.GetParameters().Length == 1) + .Select(f => new FuzzTargetFunction(f)).ToArray(); + #endregion + + [Theory] + public void FuzzGoodTestcases( + FuzzTargetFunction fuzzableCode, + [ValueSource(nameof(GoodTestcases))] TestcaseAsset messageEncoder) + { + FuzzTarget(fuzzableCode, messageEncoder.Testcase); + } + + [Theory] + public void FuzzCrashAssets( + FuzzTargetFunction fuzzableCode, + [ValueSource(nameof(CrashAssets))] TestcaseAsset messageEncoder) + { + FuzzTarget(fuzzableCode, messageEncoder.Testcase); + } + + [Theory] + [CancelAfter(1000)] + public void FuzzTimeoutAssets( + FuzzTargetFunction fuzzableCode, + [ValueSource(nameof(TimeoutAssets))] TestcaseAsset messageEncoder) + { + FuzzTarget(fuzzableCode, messageEncoder.Testcase); + } + + [Theory] + [CancelAfter(1000)] + public void FuzzSlowAssets( + FuzzTargetFunction fuzzableCode, + [ValueSource(nameof(SlowAssets))] TestcaseAsset messageEncoder) + { + FuzzTarget(fuzzableCode, messageEncoder.Testcase); + } + + delegate void LibFuzzTemplate(ReadOnlySpan span); + + private void FuzzTarget(FuzzTargetFunction fuzzableCode, byte[] blob) + { + var parameters = fuzzableCode.MethodInfo.GetParameters(); + if (parameters.Length != 1) + { + throw new InvalidOperationException("Fuzzable function must have exactly one parameter."); + } + if (parameters[0].ParameterType == typeof(string)) + { + string text = Encoding.UTF8.GetString(blob); + _ = fuzzableCode.MethodInfo.Invoke(null, new object[] { text }); + } + else if (typeof(Stream).IsAssignableFrom(parameters[0].ParameterType)) + { + using (var stream = new MemoryStream(blob)) + { + _ = fuzzableCode.MethodInfo.Invoke(null, new object[] { stream }); + } + } + else if (parameters[0].ParameterType == typeof(ReadOnlySpan)) + { + var span = new ReadOnlySpan(blob); + LibFuzzTemplate fuzzFunction = (LibFuzzTemplate)fuzzableCode.MethodInfo.CreateDelegate(typeof(LibFuzzTemplate)); + fuzzFunction(span); + } + } + } + + /// + /// A Testcase as test asset. + /// + public class TestcaseAsset : IAsset, IFormattable + { + public TestcaseAsset() { } + + public string Path { get; private set; } + public byte[] Testcase { get; private set; } + + public void Initialize(byte[] blob, string path) + { + Path = path; + Testcase = blob; + } + + public string ToString(string format, IFormatProvider formatProvider) + { + var file = System.IO.Path.GetFileName(Path); + return $"{file}"; + } + } + + /// + /// A Testcase as test asset. + /// + public class FuzzTargetFunction : IFormattable + { + public FuzzTargetFunction(MethodInfo methodInfo) + { + MethodInfo = methodInfo; + } + + public MethodInfo MethodInfo { get; private set; } + + public string ToString(string format, IFormatProvider formatProvider) + { + var name = MethodInfo.Name; + return $"{name}"; + } + } +} diff --git a/Fuzzing/Encoders/Fuzz.Tests/Opc.Ua.Encoders.Fuzz.Tests.csproj b/Fuzzing/Encoders/Fuzz.Tests/Opc.Ua.Encoders.Fuzz.Tests.csproj new file mode 100644 index 0000000000..53a6278276 --- /dev/null +++ b/Fuzzing/Encoders/Fuzz.Tests/Opc.Ua.Encoders.Fuzz.Tests.csproj @@ -0,0 +1,75 @@ + + + + Exe + $(TestsTargetFrameworks) + Opc.Ua.Encoders.Fuzz.Tests + Encoders.Fuzz.Tests + false + + + + + + + + + + + + + + + + + all + runtime; build; native; contentfiles; analyzers + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + + + + + + + + + + Testcases\%(RecursiveDir)%(FileName)%(Extension) + + + Testcases\%(RecursiveDir)%(FileName)%(Extension) + + + Testcases\%(RecursiveDir)%(FileName)%(Extension) + + + + + + Testcases\%(RecursiveDir)%(FileName)%(Extension) + PreserveNewest + + + Testcases\%(RecursiveDir)%(FileName)%(Extension) + PreserveNewest + + + Testcases\%(RecursiveDir)%(FileName)%(Extension) + PreserveNewest + + + PreserveNewest + + + + diff --git a/Fuzzing/Encoders/Fuzz.Tests/TestUtils.cs b/Fuzzing/Encoders/Fuzz.Tests/TestUtils.cs new file mode 100644 index 0000000000..55e52879ba --- /dev/null +++ b/Fuzzing/Encoders/Fuzz.Tests/TestUtils.cs @@ -0,0 +1,107 @@ +/* ======================================================================== + * Copyright (c) 2005-2021 The OPC Foundation, Inc. All rights reserved. + * + * OPC Foundation MIT License 1.00 + * + * Permission is hereby granted, free of charge, to any person + * obtaining a copy of this software and associated documentation + * files (the "Software"), to deal in the Software without + * restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following + * conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES + * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT + * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, + * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR + * OTHER DEALINGS IN THE SOFTWARE. + * + * The complete license agreement can be found here: + * http://opcfoundation.org/License/MIT/1.00/ + * ======================================================================*/ + + +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Security.Cryptography.X509Certificates; +using Opc.Ua.Security.Certificates; +using Assert = NUnit.Framework.Legacy.ClassicAssert; + +namespace Opc.Ua.Tests +{ + #region Asset Helpers + /// + /// The interface to initialize an asset. + /// + public interface IAsset + { + void Initialize(byte[] blob, string path); + } + + /// + /// Create a collection of test assets. + /// + public class AssetCollection : List where T : IAsset, new() + { + public AssetCollection() { } + public AssetCollection(IEnumerable collection) : base(collection) { } + public AssetCollection(int capacity) : base(capacity) { } + public static AssetCollection ToAssetCollection(T[] values) + { + return values != null ? new AssetCollection(values) : new AssetCollection(); + } + + public AssetCollection(IEnumerable filelist) : base() + { + foreach (var file in filelist) + { + Add(file); + } + } + + public void Add(string path) + { + byte[] blob = File.ReadAllBytes(path); + T asset = new T(); + asset.Initialize(blob, path); + Add(asset); + } + } + #endregion + + #region TestUtils + /// + /// Test helpers. + /// + public static class TestUtils + { + public static string[] EnumerateTestAssets(string folder, string searchPattern) + { + var assetsPath = Utils.GetAbsoluteDirectoryPath(folder, true, false, false); + if (assetsPath != null) + { + return Directory.EnumerateFiles(assetsPath, searchPattern).ToArray(); + } + return Array.Empty(); + } + + public static void ValidateSelSignedBasicConstraints(X509Certificate2 certificate) + { + var basicConstraintsExtension = X509Extensions.FindExtension(certificate.Extensions); + Assert.NotNull(basicConstraintsExtension); + Assert.False(basicConstraintsExtension.CertificateAuthority); + Assert.True(basicConstraintsExtension.Critical); + Assert.False(basicConstraintsExtension.HasPathLengthConstraint); + } + } + #endregion +} diff --git a/Fuzzing/Encoders/aflfuzz.sh b/Fuzzing/Encoders/aflfuzz.sh index f79a5bb2b3..ebf40ee227 100644 --- a/Fuzzing/Encoders/aflfuzz.sh +++ b/Fuzzing/Encoders/aflfuzz.sh @@ -5,11 +5,12 @@ display_menu() { echo "Select a OPA UA Encoder fuzzing function:" echo "1. Opc.Ua.BinaryDecoder" echo "2. Opc.Ua.BinaryEncoder" - echo "3. Opc.Ua.JsonDecoder" - echo "4. Opc.Ua.JsonEncoder" - echo "5. Opc.Ua.XmlDecoder" - echo "6. Opc.Ua.XmlEncoder" - echo "7. Exit" + echo "3. Opc.Ua.BinaryEncoder Indempotent" + echo "4. Opc.Ua.JsonDecoder" + echo "5. Opc.Ua.JsonEncoder" + echo "6. Opc.Ua.XmlDecoder" + echo "7. Opc.Ua.XmlEncoder" + echo "8. Exit" } # Function to execute fuzz-afl PowerShell script based on user choice @@ -24,18 +25,22 @@ execute_powershell_script() { pwsh ../scripts/fuzz-afl.ps1 ./Fuzz/Encoders.Fuzz.csproj -i ./Fuzz/Testcases.Binary -fuzztarget AflfuzzBinaryEncoder ;; 3) + echo "Running afl-fuzz with Opc.Ua.BinaryEncoder Indempotent" + pwsh ../scripts/fuzz-afl.ps1 ./Fuzz/Encoders.Fuzz.csproj -i ./Fuzz/Testcases.Binary -fuzztarget AflfuzzBinaryEncoderIndempotent + ;; + 4) echo "Running afl-fuzz with Opc.Ua.JsonDecoder" pwsh ../scripts/fuzz-afl.ps1 ./Fuzz/Encoders.Fuzz.csproj -i ./Fuzz/Testcases.Json -x ../dictionaries/json.dict -fuzztarget AflfuzzJsonDecoder ;; - 4) + 5) echo "Running afl-fuzz with Opc.Ua.JsonEncoder" pwsh ../scripts/fuzz-afl.ps1 ./Fuzz/Encoders.Fuzz.csproj -i ./Fuzz/Testcases.Json -x ../dictionaries/json.dict -fuzztarget AflfuzzJsonEncoder ;; - 5) + 6) echo "Running afl-fuzz with Opc.Ua.XmlDecoder" pwsh ../scripts/fuzz-afl.ps1 ./Fuzz/Encoders.Fuzz.csproj -i ./Fuzz/Testcases.Xml -x ../dictionaries/xml.dict -fuzztarget AflfuzzXmlDecoder ;; - 6) + 7) echo "Running afl-fuzz with Opc.Ua.XmlEncoder" pwsh ../scripts/fuzz-afl.ps1 ./Fuzz/Encoders.Fuzz.csproj -i ./Fuzz/Testcases.Xml -x ../dictionaries/xml.dict -fuzztarget AflfuzzXmlEncoder ;; @@ -48,18 +53,18 @@ execute_powershell_script() { # Main display_menu -read -p "Enter your choice (1-5): " choice +read -p "Enter your choice (1-8): " choice case $choice in - 1|2|3|4|5|6) + 1|2|3|4|5|6|7) execute_powershell_script $choice ;; - 7) + 8) echo "Exiting." break ;; *) - echo "Invalid input. Please enter a number between 1 and 7." + echo "Invalid input. Please enter a number between 1 and 8." ;; esac diff --git a/Stack/Opc.Ua.Core/Types/Encoders/BinaryEncoder.cs b/Stack/Opc.Ua.Core/Types/Encoders/BinaryEncoder.cs index acee1799e0..16fdbb37b6 100644 --- a/Stack/Opc.Ua.Core/Types/Encoders/BinaryEncoder.cs +++ b/Stack/Opc.Ua.Core/Types/Encoders/BinaryEncoder.cs @@ -540,7 +540,7 @@ public void WriteGuid(string fieldName, Guid value) /// public void WriteByteString(string fieldName, byte[] value) { - WriteByteString( fieldName, value, 0, value.Length); + WriteByteString(fieldName, value, 0, value.Length); } /// diff --git a/Stack/Opc.Ua.Core/Types/Encoders/JsonEncoder.cs b/Stack/Opc.Ua.Core/Types/Encoders/JsonEncoder.cs index 7b1aa7b882..44c2ddc3c0 100644 --- a/Stack/Opc.Ua.Core/Types/Encoders/JsonEncoder.cs +++ b/Stack/Opc.Ua.Core/Types/Encoders/JsonEncoder.cs @@ -2409,7 +2409,7 @@ public void WriteEnumeratedArray(string fieldName, Array values, System.Type sys PopArray(); } -#endregion + #endregion #region Public Methods /// diff --git a/Stack/Opc.Ua.Core/Types/Encoders/XmlEncoder.cs b/Stack/Opc.Ua.Core/Types/Encoders/XmlEncoder.cs index 5db67dfed3..0a70323f69 100644 --- a/Stack/Opc.Ua.Core/Types/Encoders/XmlEncoder.cs +++ b/Stack/Opc.Ua.Core/Types/Encoders/XmlEncoder.cs @@ -1777,7 +1777,7 @@ public void WriteEnumeratedArray(string fieldName, Array values, System.Type sys EndField(fieldName); } } -#endregion + #endregion #region Public Methods /// diff --git a/UA Core Library.sln b/UA Core Library.sln index 85c776fc56..434d4a0b0d 100644 --- a/UA Core Library.sln +++ b/UA Core Library.sln @@ -90,6 +90,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "actions", "actions", "{82AA .github\workflows\docker-image.yml = .github\workflows\docker-image.yml EndProjectSection EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Opc.Ua.Encoders.Fuzz.Tests", "Fuzzing\Encoders\Fuzz.Tests\Opc.Ua.Encoders.Fuzz.Tests.csproj", "{10C287A7-1FA5-4A6A-AF9F-C37065CDF0F1}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -172,6 +174,10 @@ Global {9FC5C47D-5070-4222-B4F4-94EB495AEE4C}.Debug|Any CPU.Build.0 = Debug|Any CPU {9FC5C47D-5070-4222-B4F4-94EB495AEE4C}.Release|Any CPU.ActiveCfg = Release|Any CPU {9FC5C47D-5070-4222-B4F4-94EB495AEE4C}.Release|Any CPU.Build.0 = Release|Any CPU + {10C287A7-1FA5-4A6A-AF9F-C37065CDF0F1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {10C287A7-1FA5-4A6A-AF9F-C37065CDF0F1}.Debug|Any CPU.Build.0 = Debug|Any CPU + {10C287A7-1FA5-4A6A-AF9F-C37065CDF0F1}.Release|Any CPU.ActiveCfg = Release|Any CPU + {10C287A7-1FA5-4A6A-AF9F-C37065CDF0F1}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -199,6 +205,7 @@ Global {FC588507-282A-426E-8BC9-940860C76610} = {DDDC86D8-79F7-488B-B445-AD176FAE1603} {9FC5C47D-5070-4222-B4F4-94EB495AEE4C} = {EE37EB25-F581-4794-9CAC-F15BF13D520C} {82AA8918-FD9F-4477-A131-874673BDC3B9} = {0152A569-8287-4E49-9F02-E27FD700B719} + {10C287A7-1FA5-4A6A-AF9F-C37065CDF0F1} = {DDDC86D8-79F7-488B-B445-AD176FAE1603} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {EA3AA3AE-CE8A-4DF1-BED8-CA0BAFC87303} diff --git a/UA Fuzzing.sln b/UA Fuzzing.sln index 73fbc52862..aee7a01f8c 100644 --- a/UA Fuzzing.sln +++ b/UA Fuzzing.sln @@ -100,6 +100,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Encoders.Fuzz", "Fuzzing\En EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Encoders.Fuzz.Tools", "Fuzzing\Encoders\Fuzz.Tools\Encoders.Fuzz.Tools.csproj", "{BAB22A60-473E-4AE3-82E3-6C78CC8F1E6B}" EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Opc.Ua.Encoders.Fuzz.Tests", "Fuzzing\Encoders\Fuzz.Tests\Opc.Ua.Encoders.Fuzz.Tests.csproj", "{85BA7E81-EC35-4507-A6A0-D74061178A22}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -219,6 +221,18 @@ Global {BAB22A60-473E-4AE3-82E3-6C78CC8F1E6B}.Release|x64.Build.0 = Release|Any CPU {BAB22A60-473E-4AE3-82E3-6C78CC8F1E6B}.Release|x86.ActiveCfg = Release|Any CPU {BAB22A60-473E-4AE3-82E3-6C78CC8F1E6B}.Release|x86.Build.0 = Release|Any CPU + {85BA7E81-EC35-4507-A6A0-D74061178A22}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {85BA7E81-EC35-4507-A6A0-D74061178A22}.Debug|Any CPU.Build.0 = Debug|Any CPU + {85BA7E81-EC35-4507-A6A0-D74061178A22}.Debug|x64.ActiveCfg = Debug|Any CPU + {85BA7E81-EC35-4507-A6A0-D74061178A22}.Debug|x64.Build.0 = Debug|Any CPU + {85BA7E81-EC35-4507-A6A0-D74061178A22}.Debug|x86.ActiveCfg = Debug|Any CPU + {85BA7E81-EC35-4507-A6A0-D74061178A22}.Debug|x86.Build.0 = Debug|Any CPU + {85BA7E81-EC35-4507-A6A0-D74061178A22}.Release|Any CPU.ActiveCfg = Release|Any CPU + {85BA7E81-EC35-4507-A6A0-D74061178A22}.Release|Any CPU.Build.0 = Release|Any CPU + {85BA7E81-EC35-4507-A6A0-D74061178A22}.Release|x64.ActiveCfg = Release|Any CPU + {85BA7E81-EC35-4507-A6A0-D74061178A22}.Release|x64.Build.0 = Release|Any CPU + {85BA7E81-EC35-4507-A6A0-D74061178A22}.Release|x86.ActiveCfg = Release|Any CPU + {85BA7E81-EC35-4507-A6A0-D74061178A22}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -241,6 +255,7 @@ Global {6A642C2C-0B5D-4C2D-BAD0-A6F760175465} = {6F57459E-6B63-4FB5-8311-7FC1DDC84650} {2CA57432-9B51-40A5-804B-D927897E7DAF} = {128EE105-0A7A-4AB3-81E6-A77AE638B33F} {BAB22A60-473E-4AE3-82E3-6C78CC8F1E6B} = {128EE105-0A7A-4AB3-81E6-A77AE638B33F} + {85BA7E81-EC35-4507-A6A0-D74061178A22} = {128EE105-0A7A-4AB3-81E6-A77AE638B33F} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {33FA5BCB-C827-4D44-AECF-F51342DFE64A} From 106ad7750e3c31c709468de3d57440bf7c556e8d Mon Sep 17 00:00:00 2001 From: Martin Regen Date: Tue, 16 Jul 2024 18:50:54 +0200 Subject: [PATCH 40/83] fix pack --- Stack/Opc.Ua.Core/CompatibilitySuppressions.xml | 10 ++++++++++ 1 file changed, 10 insertions(+) create mode 100644 Stack/Opc.Ua.Core/CompatibilitySuppressions.xml diff --git a/Stack/Opc.Ua.Core/CompatibilitySuppressions.xml b/Stack/Opc.Ua.Core/CompatibilitySuppressions.xml new file mode 100644 index 0000000000..8474161685 --- /dev/null +++ b/Stack/Opc.Ua.Core/CompatibilitySuppressions.xml @@ -0,0 +1,10 @@ + + + + + CP0006 + M:Opc.Ua.IEncoder.WriteByteString(System.String,System.ReadOnlySpan{System.Byte}) + lib/netstandard2.0/Opc.Ua.Core.dll + lib/netstandard2.1/Opc.Ua.Core.dll + + \ No newline at end of file From 319a481d8ada582da471d119e109602c93bb6792 Mon Sep 17 00:00:00 2001 From: Martin Regen Date: Tue, 16 Jul 2024 19:29:49 +0200 Subject: [PATCH 41/83] fix extract --- .azurepipelines/test.yml | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/.azurepipelines/test.yml b/.azurepipelines/test.yml index 0097d4c88c..d20575fd54 100644 --- a/.azurepipelines/test.yml +++ b/.azurepipelines/test.yml @@ -40,6 +40,11 @@ jobs: pool: vmImage: $(poolImage) steps: + - task: DownloadSecureFile@1 + name: fuzzingartifacts + displayName: 'Download Fuzzing artifacts' + inputs: + secureFile: 'FuzzingArtifacts.zip' - task: UseDotNet@2 displayName: 'Install .NET 6.0' condition: eq('${{parameters.framework}}', 'net6.0') @@ -65,15 +70,10 @@ jobs: command: restore projects: $(file) arguments: '${{ variables.DotCliCommandline }} --configuration ${{ parameters.configuration }}' - - task: DownloadSecureFile@1 - name: fuzzingartifacts - displayName: 'Download Fuzzing artifacts' - inputs: - secureFile: 'FuzzingArtifacts.zip' - task: ExtractFiles@1 displayName: 'Extract Fuzzing artifacts' inputs: - archiveFilePatterns: $(fuzzingartifacts.secureFilePath) + archiveFilePatterns: '$(fuzzingartifacts.secureFilePath)' destinationFolder: ./Fuzzing/Encoders/Fuzz.Tests/Assets cleanDestinationFolder: false overwriteExistingFiles: true From 70798565ef0c5c0f4338a5d8673f659bb21a8264 Mon Sep 17 00:00:00 2001 From: Martin Regen Date: Tue, 16 Jul 2024 20:57:33 +0200 Subject: [PATCH 42/83] use new upload and access artifacts directly --- .azurepipelines/test.yml | 22 +++++++++++----------- .azurepipelines/testcc.yml | 17 +++++++++++++---- 2 files changed, 24 insertions(+), 15 deletions(-) diff --git a/.azurepipelines/test.yml b/.azurepipelines/test.yml index d20575fd54..e56790140a 100644 --- a/.azurepipelines/test.yml +++ b/.azurepipelines/test.yml @@ -40,11 +40,6 @@ jobs: pool: vmImage: $(poolImage) steps: - - task: DownloadSecureFile@1 - name: fuzzingartifacts - displayName: 'Download Fuzzing artifacts' - inputs: - secureFile: 'FuzzingArtifacts.zip' - task: UseDotNet@2 displayName: 'Install .NET 6.0' condition: eq('${{parameters.framework}}', 'net6.0') @@ -64,19 +59,24 @@ jobs: inputs: targetType: filePath filePath: ./.azurepipelines/set-version.ps1 - - task: DotNetCoreCLI@2 - displayName: Restore ${{ parameters.configuration }} + - task: DownloadSecureFile@1 + name: fuzzingartifacts + displayName: 'Download Fuzzing artifacts' inputs: - command: restore - projects: $(file) - arguments: '${{ variables.DotCliCommandline }} --configuration ${{ parameters.configuration }}' + secureFile: 'FuzzingArtifacts.zip' - task: ExtractFiles@1 displayName: 'Extract Fuzzing artifacts' inputs: - archiveFilePatterns: '$(fuzzingartifacts.secureFilePath)' + archiveFilePatterns: '$(Agent.TempDirectory)/FuzzingArtifacts.zip' destinationFolder: ./Fuzzing/Encoders/Fuzz.Tests/Assets cleanDestinationFolder: false overwriteExistingFiles: true + - task: DotNetCoreCLI@2 + displayName: Restore ${{ parameters.configuration }} + inputs: + command: restore + projects: $(file) + arguments: '${{ variables.DotCliCommandline }} --configuration ${{ parameters.configuration }}' - task: DotNetCoreCLI@2 displayName: Test ${{ parameters.configuration }} timeoutInMinutes: 30 diff --git a/.azurepipelines/testcc.yml b/.azurepipelines/testcc.yml index 3068016e50..f3d65c492e 100644 --- a/.azurepipelines/testcc.yml +++ b/.azurepipelines/testcc.yml @@ -37,11 +37,23 @@ jobs: - task: NuGetToolInstaller@1 inputs: versionSpec: '>=5.8.x' + - task: DownloadSecureFile@1 + name: fuzzingartifacts + displayName: 'Download Fuzzing artifacts' + inputs: + secureFile: 'FuzzingArtifacts.zip' - task: PowerShell@2 displayName: Versioning inputs: targetType: filePath filePath: ./.azurepipelines/set-version.ps1 + - task: ExtractFiles@1 + displayName: 'Extract Fuzzing artifacts' + inputs: + archiveFilePatterns: '$(Agent.TempDirectory)/FuzzingArtifacts.zip' + destinationFolder: ./Fuzzing/Encoders/Fuzz.Tests/Assets + cleanDestinationFolder: false + overwriteExistingFiles: true - task: DotNetCoreCLI@2 displayName: Restore ${{ parameters.framework }} inputs: @@ -77,13 +89,10 @@ jobs: displayName: Create Code Coverage Report continueOnError: true - - task: PublishCodeCoverageResults@1 + - task: PublishCodeCoverageResults@2 displayName: 'Publish code coverage' inputs: - codeCoverageTool: Cobertura summaryFileLocation: '$(Build.SourcesDirectory)/CodeCoverage/Cobertura.xml' - reportDirectory: '$(Build.SourcesDirectory)/CodeCoverage' - continueOnError: true - script: | bash <(curl -Ls https://coverage.codacy.com/get.sh) report -r $REPORT_LOCATION --commit-uuid $COMMIT_UUID From 0d09316f617b63241287564ca61fb9ae725c1843 Mon Sep 17 00:00:00 2001 From: Martin Regen Date: Tue, 16 Jul 2024 21:24:05 +0200 Subject: [PATCH 43/83] make extract conditional --- .azurepipelines/test.yml | 2 ++ .azurepipelines/testcc.yml | 2 ++ 2 files changed, 4 insertions(+) diff --git a/.azurepipelines/test.yml b/.azurepipelines/test.yml index e56790140a..9d01003c6b 100644 --- a/.azurepipelines/test.yml +++ b/.azurepipelines/test.yml @@ -66,11 +66,13 @@ jobs: secureFile: 'FuzzingArtifacts.zip' - task: ExtractFiles@1 displayName: 'Extract Fuzzing artifacts' + condition: ne(variables['fuzzingartifacts.secureFilePath'], '') inputs: archiveFilePatterns: '$(Agent.TempDirectory)/FuzzingArtifacts.zip' destinationFolder: ./Fuzzing/Encoders/Fuzz.Tests/Assets cleanDestinationFolder: false overwriteExistingFiles: true + continueOnError: true - task: DotNetCoreCLI@2 displayName: Restore ${{ parameters.configuration }} inputs: diff --git a/.azurepipelines/testcc.yml b/.azurepipelines/testcc.yml index f3d65c492e..f109eb500c 100644 --- a/.azurepipelines/testcc.yml +++ b/.azurepipelines/testcc.yml @@ -49,11 +49,13 @@ jobs: filePath: ./.azurepipelines/set-version.ps1 - task: ExtractFiles@1 displayName: 'Extract Fuzzing artifacts' + condition: ne(variables['fuzzingartifacts.secureFilePath'], '') inputs: archiveFilePatterns: '$(Agent.TempDirectory)/FuzzingArtifacts.zip' destinationFolder: ./Fuzzing/Encoders/Fuzz.Tests/Assets cleanDestinationFolder: false overwriteExistingFiles: true + continueOnError: true - task: DotNetCoreCLI@2 displayName: Restore ${{ parameters.framework }} inputs: From cabdb8e7e4ac142aa4d08a36fb2e13f9c37e2347 Mon Sep 17 00:00:00 2001 From: Martin Regen Date: Wed, 17 Jul 2024 11:23:16 +0200 Subject: [PATCH 44/83] push asset target --- .azurepipelines/test.yml | 2 +- .azurepipelines/testcc.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.azurepipelines/test.yml b/.azurepipelines/test.yml index 9d01003c6b..12f7449ad2 100644 --- a/.azurepipelines/test.yml +++ b/.azurepipelines/test.yml @@ -69,7 +69,7 @@ jobs: condition: ne(variables['fuzzingartifacts.secureFilePath'], '') inputs: archiveFilePatterns: '$(Agent.TempDirectory)/FuzzingArtifacts.zip' - destinationFolder: ./Fuzzing/Encoders/Fuzz.Tests/Assets + destinationFolder: ./Fuzzing/Encoders/Fuzz.Tests cleanDestinationFolder: false overwriteExistingFiles: true continueOnError: true diff --git a/.azurepipelines/testcc.yml b/.azurepipelines/testcc.yml index f109eb500c..4f2fdb8ada 100644 --- a/.azurepipelines/testcc.yml +++ b/.azurepipelines/testcc.yml @@ -52,7 +52,7 @@ jobs: condition: ne(variables['fuzzingartifacts.secureFilePath'], '') inputs: archiveFilePatterns: '$(Agent.TempDirectory)/FuzzingArtifacts.zip' - destinationFolder: ./Fuzzing/Encoders/Fuzz.Tests/Assets + destinationFolder: ./Fuzzing/Encoders/Fuzz.Tests cleanDestinationFolder: false overwriteExistingFiles: true continueOnError: true From c5bf6660eb33c314ed580d55c1a329960e8e67db Mon Sep 17 00:00:00 2001 From: Martin Regen Date: Wed, 17 Jul 2024 14:37:46 +0200 Subject: [PATCH 45/83] fix null pointers --- Fuzzing/Encoders/Fuzz.Tests/EncoderTests.cs | 11 +- .../Types/Encoders/BinaryEncoder.cs | 6 + .../Opc.Ua.Core/Types/Encoders/JsonEncoder.cs | 6 + .../Opc.Ua.Core/Types/Encoders/XmlEncoder.cs | 2 +- Stack/Opc.Ua.Core/Types/Utils/Utils.cs | 2 +- .../Types/Encoders/EncoderCommon.cs | 2 +- .../Types/Encoders/EncoderTests.cs | 117 ++++++++++++++++++ 7 files changed, 139 insertions(+), 7 deletions(-) diff --git a/Fuzzing/Encoders/Fuzz.Tests/EncoderTests.cs b/Fuzzing/Encoders/Fuzz.Tests/EncoderTests.cs index 8cec73d7fa..6af237f54a 100644 --- a/Fuzzing/Encoders/Fuzz.Tests/EncoderTests.cs +++ b/Fuzzing/Encoders/Fuzz.Tests/EncoderTests.cs @@ -65,11 +65,14 @@ public void FuzzGoodTestcases( } [Theory] - public void FuzzCrashAssets( - FuzzTargetFunction fuzzableCode, - [ValueSource(nameof(CrashAssets))] TestcaseAsset messageEncoder) + public void FuzzCrashAssets(FuzzTargetFunction fuzzableCode) { - FuzzTarget(fuzzableCode, messageEncoder.Testcase); + // note: too many crash files can take forever to create + // all permutations with nunit, so just run all in one batch + foreach (var messageEncoder in CrashAssets) + { + FuzzTarget(fuzzableCode, messageEncoder.Testcase); + } } [Theory] diff --git a/Stack/Opc.Ua.Core/Types/Encoders/BinaryEncoder.cs b/Stack/Opc.Ua.Core/Types/Encoders/BinaryEncoder.cs index 16fdbb37b6..587512fbfc 100644 --- a/Stack/Opc.Ua.Core/Types/Encoders/BinaryEncoder.cs +++ b/Stack/Opc.Ua.Core/Types/Encoders/BinaryEncoder.cs @@ -540,6 +540,12 @@ public void WriteGuid(string fieldName, Guid value) /// public void WriteByteString(string fieldName, byte[] value) { + if (value == null) + { + WriteInt32(null, -1); + return; + } + WriteByteString(fieldName, value, 0, value.Length); } diff --git a/Stack/Opc.Ua.Core/Types/Encoders/JsonEncoder.cs b/Stack/Opc.Ua.Core/Types/Encoders/JsonEncoder.cs index 44c2ddc3c0..43ca801ff9 100644 --- a/Stack/Opc.Ua.Core/Types/Encoders/JsonEncoder.cs +++ b/Stack/Opc.Ua.Core/Types/Encoders/JsonEncoder.cs @@ -979,6 +979,12 @@ public void WriteGuid(string fieldName, Guid value) /// public void WriteByteString(string fieldName, byte[] value) { + if (value == null) + { + WriteSimpleFieldNull(fieldName); + return; + } + WriteByteString(fieldName, value, 0, value.Length); } diff --git a/Stack/Opc.Ua.Core/Types/Encoders/XmlEncoder.cs b/Stack/Opc.Ua.Core/Types/Encoders/XmlEncoder.cs index 0a70323f69..94edc5dfb0 100644 --- a/Stack/Opc.Ua.Core/Types/Encoders/XmlEncoder.cs +++ b/Stack/Opc.Ua.Core/Types/Encoders/XmlEncoder.cs @@ -65,7 +65,7 @@ public XmlEncoder(XmlQualifiedName root, XmlWriter writer, IServiceMessageContex if (writer == null) { m_destination = new StringBuilder(); - m_writer = XmlWriter.Create(m_destination); + m_writer = XmlWriter.Create(m_destination, Utils.DefaultXmlWriterSettings()); } else { diff --git a/Stack/Opc.Ua.Core/Types/Utils/Utils.cs b/Stack/Opc.Ua.Core/Types/Utils/Utils.cs index a92411d250..6816af4e86 100644 --- a/Stack/Opc.Ua.Core/Types/Utils/Utils.cs +++ b/Stack/Opc.Ua.Core/Types/Utils/Utils.cs @@ -2466,7 +2466,7 @@ public static void UpdateExtension(ref XmlElementCollection extensions, XmlQu // serialize value. StringBuilder buffer = new StringBuilder(); - using (XmlWriter writer = XmlWriter.Create(buffer)) + using (XmlWriter writer = XmlWriter.Create(buffer, DefaultXmlWriterSettings())) { if (value != null) { diff --git a/Tests/Opc.Ua.Core.Tests/Types/Encoders/EncoderCommon.cs b/Tests/Opc.Ua.Core.Tests/Types/Encoders/EncoderCommon.cs index a8e990fea4..ffc5d3697b 100644 --- a/Tests/Opc.Ua.Core.Tests/Types/Encoders/EncoderCommon.cs +++ b/Tests/Opc.Ua.Core.Tests/Types/Encoders/EncoderCommon.cs @@ -468,7 +468,7 @@ protected IEncoder CreateEncoder( return new BinaryEncoder(stream, context, true); case EncodingType.Xml: Assume.That(useReversibleEncoding, "Xml encoding only supports reversible option."); - var xmlWriter = XmlWriter.Create(stream); + var xmlWriter = XmlWriter.Create(stream, Utils.DefaultXmlWriterSettings()); return new XmlEncoder(systemType, xmlWriter, context); case EncodingType.Json: return new JsonEncoder(context, useReversibleEncoding, topLevelIsArray, stream, true) { diff --git a/Tests/Opc.Ua.Core.Tests/Types/Encoders/EncoderTests.cs b/Tests/Opc.Ua.Core.Tests/Types/Encoders/EncoderTests.cs index 4dd03960cb..c195dc1c06 100644 --- a/Tests/Opc.Ua.Core.Tests/Types/Encoders/EncoderTests.cs +++ b/Tests/Opc.Ua.Core.Tests/Types/Encoders/EncoderTests.cs @@ -30,6 +30,8 @@ using System; using System.IO; using System.Text; +using System.Xml; +using Newtonsoft.Json; using NUnit.Framework; using Assert = NUnit.Framework.Legacy.ClassicAssert; @@ -289,6 +291,121 @@ EncodingType encoderType EncodeDecodeDataValue(encoderType, BuiltInType.Variant, MemoryStreamType.ArraySegmentStream, variant); } + [Test] + [Category("WriteByteString")] + public void BinaryEncoder_WriteByteString() + { + using (var stream = new MemoryStream()) + { + using (IEncoder encoder = new BinaryEncoder(stream, new ServiceMessageContext(), true)) + { + encoder.WriteByteString("ByteString1", new byte[] { 0, 1, 2, 3, 4, 5 }, 1, 3); +#if NET5_0_OR_GREATER + var span = new ReadOnlySpan(new byte[] { 0, 1, 2, 3, 4, 5 }, 1, 3); + var nullspan = new ReadOnlySpan(null); + encoder.WriteByteString("ByteString2", span); + encoder.WriteByteString("ByteString3", nullspan); +#endif + encoder.WriteByteString("ByteString4", null); + encoder.WriteByteString("ByteString5", null, 1, 2); + } + stream.Position = 0; + using (var decoder = new BinaryDecoder(stream, new ServiceMessageContext())) + { + var result = decoder.ReadByteString("ByteString1"); + Assert.AreEqual(new byte[] { 1, 2, 3 }, result); +#if NET5_0_OR_GREATER + result = decoder.ReadByteString("ByteString2"); + Assert.AreEqual(new byte[] { 1, 2, 3 }, result); + result = decoder.ReadByteString("ByteString3"); + Assert.AreEqual(null, result); +#endif + result = decoder.ReadByteString("ByteString4"); + Assert.AreEqual(null, result); + result = decoder.ReadByteString("ByteString5"); + Assert.AreEqual(null, result); + } + } + } + + [Test] + [Category("WriteByteString")] + public void XmlEncoder_WriteByteString() + { + using (var stream = new MemoryStream()) + { + XmlWriterSettings settings = Utils.DefaultXmlWriterSettings(); + using (XmlWriter writer = XmlWriter.Create(stream, settings)) + using (IEncoder encoder = new XmlEncoder(new XmlQualifiedName("ByteStrings", Namespaces.OpcUaXsd), writer, new ServiceMessageContext())) + { + encoder.WriteByteString("ByteString1", new byte[] { 0, 1, 2, 3, 4, 5 }, 1, 3); +#if NET5_0_OR_GREATER + var span = new ReadOnlySpan(new byte[] { 0, 1, 2, 3, 4, 5 }, 1, 3); + var nullspan = new ReadOnlySpan(null); + encoder.WriteByteString("ByteString2", span); + encoder.WriteByteString("ByteString3", nullspan); +#endif + encoder.WriteByteString("ByteString4", null); + encoder.WriteByteString("ByteString5", null, 1, 2); + } + stream.Position = 0; + using (XmlReader reader = XmlReader.Create(stream, Utils.DefaultXmlReaderSettings())) + using (var decoder = new XmlDecoder(null, reader, new ServiceMessageContext())) + { + var result = decoder.ReadByteString("ByteString1"); + Assert.AreEqual(new byte[] { 1, 2, 3 }, result); +#if NET5_0_OR_GREATER + result = decoder.ReadByteString("ByteString2"); + Assert.AreEqual(new byte[] { 1, 2, 3 }, result); + result = decoder.ReadByteString("ByteString3"); + Assert.AreEqual(null, result); +#endif + result = decoder.ReadByteString("ByteString4"); + Assert.AreEqual(null, result); + result = decoder.ReadByteString("ByteString5"); + Assert.AreEqual(null, result); + } + } + } + + [Test] + [Category("WriteByteString")] + public void JsonEncoder_WriteByteString() + { + using (var stream = new MemoryStream()) + { + using (IEncoder encoder = new JsonEncoder(new ServiceMessageContext(), true, false, stream, true)) + { + encoder.WriteByteString("ByteString1", new byte[] { 0, 1, 2, 3, 4, 5 }, 1, 3); +#if NET5_0_OR_GREATER + var span = new ReadOnlySpan(new byte[] { 0, 1, 2, 3, 4, 5 }, 1, 3); + var nullspan = new ReadOnlySpan(null); + encoder.WriteByteString("ByteString2", span); + encoder.WriteByteString("ByteString3", nullspan); +#endif + encoder.WriteByteString("ByteString4", null); + encoder.WriteByteString("ByteString5", null, 1, 2); + } + stream.Position = 0; + var jsonTextReader = new JsonTextReader(new StreamReader(stream)); + using (var decoder = new JsonDecoder(null, jsonTextReader, new ServiceMessageContext())) + { + var result = decoder.ReadByteString("ByteString1"); + Assert.AreEqual(new byte[] { 1, 2, 3 }, result); +#if NET5_0_OR_GREATER + result = decoder.ReadByteString("ByteString2"); + Assert.AreEqual(new byte[] { 1, 2, 3 }, result); + result = decoder.ReadByteString("ByteString3"); + Assert.AreEqual(null, result); +#endif + result = decoder.ReadByteString("ByteString4"); + Assert.AreEqual(null, result); + result = decoder.ReadByteString("ByteString5"); + Assert.AreEqual(null, result); + } + } + } + /// /// Verify encode and decode of a Matrix in a Variant. /// From 5727b02240f434cf4b54125b4bd4874ccc8db52e Mon Sep 17 00:00:00 2001 From: Martin Regen Date: Fri, 19 Jul 2024 09:07:10 +0200 Subject: [PATCH 46/83] add support for more buffering --- .../Stack/Bindings/ArraySegmentStream.cs | 12 +- .../Buffers}/ArrayPoolBufferSegment.cs | 30 +- .../Buffers}/ArrayPoolBufferWriter.cs | 78 +++-- .../Types/Encoders/BinaryEncoder.cs | 7 +- .../Opc.Ua.Core.Tests.csproj | 2 +- .../Stack/Buffers/ArrayPoolBufferTests.cs | 151 ++++++++++ .../Stack/Buffers/ArraySegmentStreamTests.cs | 267 ++++++++++++++++++ 7 files changed, 495 insertions(+), 52 deletions(-) rename Stack/Opc.Ua.Core/{Types/Encoders => Stack/Buffers}/ArrayPoolBufferSegment.cs (75%) rename Stack/Opc.Ua.Core/{Types/Encoders => Stack/Buffers}/ArrayPoolBufferWriter.cs (69%) create mode 100644 Tests/Opc.Ua.Core.Tests/Stack/Buffers/ArrayPoolBufferTests.cs create mode 100644 Tests/Opc.Ua.Core.Tests/Stack/Buffers/ArraySegmentStreamTests.cs diff --git a/Stack/Opc.Ua.Core/Stack/Bindings/ArraySegmentStream.cs b/Stack/Opc.Ua.Core/Stack/Bindings/ArraySegmentStream.cs index 3e813684c3..aaf3d93742 100644 --- a/Stack/Opc.Ua.Core/Stack/Bindings/ArraySegmentStream.cs +++ b/Stack/Opc.Ua.Core/Stack/Bindings/ArraySegmentStream.cs @@ -172,7 +172,7 @@ public override bool CanSeek /// public override bool CanWrite { - get { return m_buffers != null; } + get { return m_buffers != null && m_bufferManager != null; } } /// @@ -321,21 +321,19 @@ public override long Seek(long offset, SeekOrigin origin) switch (origin) { case SeekOrigin.Begin: - { break; - } case SeekOrigin.Current: - { offset += GetAbsolutePosition(); break; - } case SeekOrigin.End: - { offset += GetAbsoluteLength(); break; - } + + default: + throw new IOException("Invalid seek origin value."); + } if (offset < 0) diff --git a/Stack/Opc.Ua.Core/Types/Encoders/ArrayPoolBufferSegment.cs b/Stack/Opc.Ua.Core/Stack/Buffers/ArrayPoolBufferSegment.cs similarity index 75% rename from Stack/Opc.Ua.Core/Types/Encoders/ArrayPoolBufferSegment.cs rename to Stack/Opc.Ua.Core/Stack/Buffers/ArrayPoolBufferSegment.cs index de2ee159d5..d8f82d61ca 100644 --- a/Stack/Opc.Ua.Core/Types/Encoders/ArrayPoolBufferSegment.cs +++ b/Stack/Opc.Ua.Core/Stack/Buffers/ArrayPoolBufferSegment.cs @@ -30,8 +30,9 @@ using System; using System.Buffers; +using System.Threading; -namespace Opc.Ua +namespace Opc.Ua.Buffers { /// @@ -51,27 +52,16 @@ public ArrayPoolBufferSegment(T[] array, int offset, int length) } /// - /// Rents a buffer from the pool and returns a instance. + /// Returns a rented buffer to the shared pool and invalidates memory. /// - /// The length of the segment. - public static ArrayPoolBufferSegment Rent(int length) + public void Return(bool clearArray = false) { - var array = ArrayPool.Shared.Rent(length); - return new ArrayPoolBufferSegment(array, 0, length); - } - - /// - /// Returns the base array of the buffer. - /// - public T[] Array() => _array; - - /// - /// Rents a new buffer and appends it to the sequence. - /// - public ArrayPoolBufferSegment RentAndAppend(int length) - { - var array = ArrayPool.Shared.Rent(length); - return Append(array, 0, length); + var array = Interlocked.Exchange(ref _array, null); + if (array != null) + { + ArrayPool.Shared.Return(array, clearArray); + Memory = ReadOnlyMemory.Empty; + } } /// diff --git a/Stack/Opc.Ua.Core/Types/Encoders/ArrayPoolBufferWriter.cs b/Stack/Opc.Ua.Core/Stack/Buffers/ArrayPoolBufferWriter.cs similarity index 69% rename from Stack/Opc.Ua.Core/Types/Encoders/ArrayPoolBufferWriter.cs rename to Stack/Opc.Ua.Core/Stack/Buffers/ArrayPoolBufferWriter.cs index 9c8d65a300..b52d24eb1f 100644 --- a/Stack/Opc.Ua.Core/Types/Encoders/ArrayPoolBufferWriter.cs +++ b/Stack/Opc.Ua.Core/Stack/Buffers/ArrayPoolBufferWriter.cs @@ -30,8 +30,9 @@ using System; using System.Buffers; using System.Diagnostics; +using System.Runtime.CompilerServices; -namespace Opc.Ua +namespace Opc.Ua.Buffers { /// /// Helper to build a from a set of buffers. @@ -39,15 +40,11 @@ namespace Opc.Ua /// public sealed class ArrayPoolBufferWriter : IBufferWriter, IDisposable { -#if DEBUG - private const bool _clearArray = true; -#else - private const bool _clearArray = false; -#endif - - private const int DefaultChunkSize = 1024; + private const int DefaultChunkSize = 256; private const int MaxChunkSize = 65536; + private bool _clearArray; private int _chunkSize; + private int _maxChunkSize; private T[] _currentBuffer; private ArrayPoolBufferSegment _firstSegment; private ArrayPoolBufferSegment _nextSegment; @@ -56,11 +53,29 @@ public sealed class ArrayPoolBufferWriter : IBufferWriter, IDisposable /// /// Initializes a new instance of the class. /// - public ArrayPoolBufferWriter(int chunksize = DefaultChunkSize, int maxChunkSize = MaxChunkSize) + public ArrayPoolBufferWriter() + : this(false, DefaultChunkSize, MaxChunkSize) + { + } + + /// + /// Initializes a new instance of the class. + /// + public ArrayPoolBufferWriter(int defaultChunksize, int maxChunkSize) + : this(false, defaultChunksize, maxChunkSize) + { + } + + /// + /// Initializes a new instance of the class. + /// + public ArrayPoolBufferWriter(bool clearArray, int defaultChunksize, int maxChunkSize) { _firstSegment = _nextSegment = null; _offset = 0; - _chunkSize = chunksize; + _clearArray = clearArray; + _chunkSize = defaultChunksize; + _maxChunkSize = maxChunkSize; _currentBuffer = Array.Empty(); } @@ -72,25 +87,38 @@ public void Dispose() ArrayPoolBufferSegment segment = _firstSegment; while (segment != null) { - ArrayPool.Shared.Return(segment.Array(), _clearArray); + segment.Return(_clearArray); segment = (ArrayPoolBufferSegment)segment.Next; } _firstSegment = _nextSegment = null; - GC.SuppressFinalize(this); } } /// public void Advance(int count) { + if (count < 0) + { + throw new ArgumentOutOfRangeException(nameof(count), $"{nameof(count)} must be non-negative."); + } + + if (_offset + count > _currentBuffer.Length) + { + throw new InvalidOperationException($"Cannot advance past the end of the buffer, which has a size of {_currentBuffer.Length}."); + } + _offset += count; - Debug.Assert(_offset <= _currentBuffer.Length, "The offset was advanced beyond the length of the current buffer."); } /// public Memory GetMemory(int sizeHint = 0) { + if (sizeHint < 0) + { + throw new ArgumentOutOfRangeException(nameof(sizeHint), $"{nameof(sizeHint)} must be non-negative."); + } + int remainingSpace = CheckAndAllocateBuffer(sizeHint); return _currentBuffer.AsMemory(_offset, remainingSpace); } @@ -98,6 +126,12 @@ public Memory GetMemory(int sizeHint = 0) /// public Span GetSpan(int sizeHint = 0) { + if (sizeHint < 0) + { + throw new ArgumentOutOfRangeException(nameof(sizeHint), $"{nameof(sizeHint)} must be non-negative."); + } + + int remainingSpace = CheckAndAllocateBuffer(sizeHint); return _currentBuffer.AsSpan(_offset, remainingSpace); } @@ -116,9 +150,10 @@ public ReadOnlySequence GetReadOnlySequence() return ReadOnlySequence.Empty; } - return new ReadOnlySequence(_firstSegment, 0, _nextSegment, _offset); + return new ReadOnlySequence(_firstSegment, 0, _nextSegment, _nextSegment.Memory.Length); } + [MethodImpl(MethodImplOptions.AggressiveInlining)] private void AddSegment() { if (_offset > 0) @@ -132,17 +167,16 @@ private void AddSegment() _nextSegment = _nextSegment.Append(_currentBuffer, 0, _offset); } } - else + else if (_currentBuffer.Length > 0) { - if (_currentBuffer.Length > 0) - { - ArrayPool.Shared.Return(_currentBuffer, _clearArray); - } - - _currentBuffer = Array.Empty(); + ArrayPool.Shared.Return(_currentBuffer, _clearArray); } + + _offset = 0; + _currentBuffer = Array.Empty(); } + [MethodImpl(MethodImplOptions.AggressiveInlining)] private int CheckAndAllocateBuffer(int sizeHint) { int remainingSpace = _currentBuffer.Length - _offset; @@ -154,7 +188,7 @@ private int CheckAndAllocateBuffer(int sizeHint) _currentBuffer = ArrayPool.Shared.Rent(remainingSpace); _offset = 0; - if (_chunkSize < MaxChunkSize) + if (_chunkSize < _maxChunkSize) { _chunkSize *= 2; } diff --git a/Stack/Opc.Ua.Core/Types/Encoders/BinaryEncoder.cs b/Stack/Opc.Ua.Core/Types/Encoders/BinaryEncoder.cs index 587512fbfc..7e20c3a780 100644 --- a/Stack/Opc.Ua.Core/Types/Encoders/BinaryEncoder.cs +++ b/Stack/Opc.Ua.Core/Types/Encoders/BinaryEncoder.cs @@ -17,6 +17,8 @@ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. using System.IO; using System.Text; using System.Xml; +using Opc.Ua.Bindings; +using Opc.Ua.Buffers; namespace Opc.Ua { @@ -455,6 +457,7 @@ public void WriteString(string fieldName, string value) int maxByteCount = Encoding.UTF8.GetMaxByteCount(value.Length); #if NETSTANDARD2_1_OR_GREATER || NET5_0_OR_GREATER #if NET5_0_OR_GREATER + const int minByteCountPerBuffer = 256; const int maxByteCountPerBuffer = 8192; #endif const int maxStackAllocByteCount = 128; @@ -467,7 +470,7 @@ public void WriteString(string fieldName, string value) #if NET5_0_OR_GREATER else if (maxByteCount > maxByteCountPerBuffer) { - using (var bufferWriter = new ArrayPoolBufferWriter(maxByteCountPerBuffer)) + using (var bufferWriter = new ArrayPoolBufferWriter(minByteCountPerBuffer, maxByteCountPerBuffer)) { long count = Encoding.UTF8.GetBytes(value.AsSpan(), bufferWriter); WriteByteString(null, bufferWriter.GetReadOnlySequence()); @@ -1993,7 +1996,7 @@ public void WriteArray(string fieldName, object array, int valueRank, BuiltInTyp #region Private Methods #if NETSTANDARD2_1_OR_GREATER || NET5_0_OR_GREATER /// - /// Writes a byte string to the stream. + /// Writes a byte string encoded in a ReadOnlySequence to the stream. /// private void WriteByteString(string fieldName, ReadOnlySequence value) { diff --git a/Tests/Opc.Ua.Core.Tests/Opc.Ua.Core.Tests.csproj b/Tests/Opc.Ua.Core.Tests/Opc.Ua.Core.Tests.csproj index 3ae412bb0d..a14a85d757 100644 --- a/Tests/Opc.Ua.Core.Tests/Opc.Ua.Core.Tests.csproj +++ b/Tests/Opc.Ua.Core.Tests/Opc.Ua.Core.Tests.csproj @@ -1,4 +1,4 @@ - + Exe diff --git a/Tests/Opc.Ua.Core.Tests/Stack/Buffers/ArrayPoolBufferTests.cs b/Tests/Opc.Ua.Core.Tests/Stack/Buffers/ArrayPoolBufferTests.cs new file mode 100644 index 0000000000..bd0bd01671 --- /dev/null +++ b/Tests/Opc.Ua.Core.Tests/Stack/Buffers/ArrayPoolBufferTests.cs @@ -0,0 +1,151 @@ +/* ======================================================================== + * Copyright (c) 2005-2024 The OPC Foundation, Inc. All rights reserved. + * + * OPC Foundation MIT License 1.00 + * + * Permission is hereby granted, free of charge, to any person + * obtaining a copy of this software and associated documentation + * files (the "Software"), to deal in the Software without + * restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following + * conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES + * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT + * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, + * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR + * OTHER DEALINGS IN THE SOFTWARE. + * + * The complete license agreement can be found here: + * http://opcfoundation.org/License/MIT/1.00/ + * ======================================================================*/ + +using NUnit.Framework; +using NUnit.Framework.Internal; +using System; +using System.Buffers; + +namespace Opc.Ua.Buffers.Tests +{ + /// + /// Tests for where T is . + /// + [Parallelizable(ParallelScope.All)] + public class ArrayPoolBufferTests + { + /// + /// Test the default behavior of . + /// + [Test] + public void ArrayPoolBufferWriterWhenConstructedWithDefaultOptionsShouldNotThrow() + { + // Arrange + var writer = new ArrayPoolBufferWriter(); + + // Act + Action act = () => writer.Dispose(); + var buffer = new byte[1]; + + var memory = writer.GetMemory(1); + memory.Span[0] = 0; + writer.Advance(1); + writer.Write(buffer); + var sequence = writer.GetReadOnlySequence(); + + // Assert + Assert.Throws(() => writer.GetMemory(-1)); + Assert.Throws(() => writer.GetSpan(-1)); + Assert.Throws(() => writer.Advance(-1)); + Assert.Throws(() => writer.Advance(2)); + + act(); + } + + /// + /// Test the default behavior of . + /// + [Theory] + public void ArrayPoolBufferWriterChunking( + [Values(0, 1, 16, 128, 333, 1024, 7777)] int chunkSize, + [Values(16, 333, 1024, 4096)] int defaultChunkSize, + [Values(0, 1024, 4096, 65536)] int maxChunkSize) + { + var random = new Random(42); + int length; + ReadOnlySequence sequence; + byte[] buffer; + + // Arrange + using (var writer = new ArrayPoolBufferWriter(false, defaultChunkSize, maxChunkSize)) + { + + // Act + for (int i = 0; i <= byte.MaxValue; i++) + { + Span span; + int randomGetChunkSize = maxChunkSize > 0 ? chunkSize + random.Next(maxChunkSize) : chunkSize; + + int repeats = random.Next(3); + do + { + // get a new chunk + if (random.Next(2) == 0) + { + var memory = writer.GetMemory(randomGetChunkSize); + Assert.That(memory.Length, Is.GreaterThanOrEqualTo(chunkSize)); + span = memory.Span; + } + else + { + span = writer.GetSpan(randomGetChunkSize); + } + + Assert.That(span.Length, Is.GreaterThanOrEqualTo(chunkSize)); + } + while (repeats-- > 0); + + // fill chunk with a byte + for (int v = 0; v < chunkSize; v++) + { + span[v] = (byte)i; + } + + writer.Advance(chunkSize); + + // Assert interim projections + if (random.Next(10) == 0) + { + length = chunkSize * (i + 1); + sequence = writer.GetReadOnlySequence(); + buffer = sequence.ToArray(); + + // Assert + Assert.That(buffer.Length, Is.EqualTo(length)); + Assert.That(sequence.Length, Is.EqualTo(length)); + + } + } + + length = (byte.MaxValue + 1) * chunkSize; + sequence = writer.GetReadOnlySequence(); + buffer = sequence.ToArray(); + + // Assert + Assert.That(sequence.Length, Is.EqualTo(length)); + Assert.That(buffer.Length, Is.EqualTo(length)); + + for (int i = 0; i < buffer.Length; i++) + { + Assert.That(buffer[i], Is.EqualTo((byte)(i / chunkSize))); + } + } + } + } +} diff --git a/Tests/Opc.Ua.Core.Tests/Stack/Buffers/ArraySegmentStreamTests.cs b/Tests/Opc.Ua.Core.Tests/Stack/Buffers/ArraySegmentStreamTests.cs new file mode 100644 index 0000000000..b6bcaca240 --- /dev/null +++ b/Tests/Opc.Ua.Core.Tests/Stack/Buffers/ArraySegmentStreamTests.cs @@ -0,0 +1,267 @@ +/* ======================================================================== + * Copyright (c) 2005-2024 The OPC Foundation, Inc. All rights reserved. + * + * OPC Foundation MIT License 1.00 + * + * Permission is hereby granted, free of charge, to any person + * obtaining a copy of this software and associated documentation + * files (the "Software"), to deal in the Software without + * restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following + * conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES + * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT + * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, + * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR + * OTHER DEALINGS IN THE SOFTWARE. + * + * The complete license agreement can be found here: + * http://opcfoundation.org/License/MIT/1.00/ + * ======================================================================*/ + +using NUnit.Framework; +using NUnit.Framework.Internal; +using System; +using System.Buffers; +using System.IO; +using Opc.Ua.Bindings; + +namespace Opc.Ua.Buffers.Tests +{ + + /// + /// Tests for where T is . + /// + [Parallelizable(ParallelScope.All)] + public class ArraySegmentStreamTests + { + /// + /// Test the default behavior of . + /// + [Test] + public void ArraySegmentStreamWhenConstructedWithDefaultOptionsShouldNotThrow() + { + // Arrange + var bufferManager = new BufferManager(nameof(ArraySegmentStreamWhenConstructedWithDefaultOptionsShouldNotThrow), 0x10000 - 1); + var stream = new ArraySegmentStream(bufferManager); + + // Act + Action act = () => stream.Dispose(); + var buffer = new byte[1] { 0x55 }; + + // Assert + Assert.That(stream.CanRead, Is.True); + Assert.That(stream.CanWrite, Is.True); + Assert.That(stream.CanSeek, Is.True); + Assert.Throws(() => stream.SetLength(0)); + Assert.Throws(() => stream.Seek(-1, SeekOrigin.Begin)); + Assert.Throws(() => stream.Seek(0, (SeekOrigin)66)); + + Assert.That(stream.Seek(0, SeekOrigin.Begin), Is.EqualTo(0)); + Assert.That(stream.ReadByte(), Is.EqualTo(-1)); + Assert.That(stream.Read(buffer, 0, 1), Is.EqualTo(0)); +#if NET5_0_OR_GREATER + Assert.That(stream.Read(buffer.AsSpan(0, 1)), Is.EqualTo(0)); +#endif + stream.Position = 0; + Assert.That(stream.Position, Is.EqualTo(0)); + + Assert.That(stream.Length, Is.EqualTo(0)); + + Assert.That(stream.Seek(0, SeekOrigin.Begin), Is.EqualTo(0)); + stream.WriteByte(0xaa); + stream.Write(buffer, 0, 1); +#if NET5_0_OR_GREATER + stream.Write(buffer.AsSpan(0, 1)); +#else + stream.Write(buffer, 0, 1); +#endif + stream.Flush(); + Assert.That(stream.Length, Is.EqualTo(3)); + Assert.That(stream.Position, Is.EqualTo(3)); + Assert.That(stream.Length, Is.EqualTo(3)); + + Assert.That(stream.Seek(-3, SeekOrigin.Current), Is.EqualTo(0)); + Assert.That(stream.ReadByte(), Is.EqualTo(0xaa)); + Assert.That(stream.Length, Is.EqualTo(3)); + Assert.That(stream.Position, Is.EqualTo(1)); + Assert.That(stream.Read(buffer, 0, 1), Is.EqualTo(1)); + Assert.That(stream.Position, Is.EqualTo(2)); + Assert.That(stream.Length, Is.EqualTo(3)); + Assert.That(buffer[0], Is.EqualTo(0x55)); +#if NET5_0_OR_GREATER + Assert.That(stream.Read(buffer.AsSpan(0, 1)), Is.EqualTo(1)); +#else + Assert.That(stream.Read(buffer, 0, 1), Is.EqualTo(1)); +#endif + Assert.That(stream.Position, Is.EqualTo(3)); + Assert.That(stream.Length, Is.EqualTo(3)); + Assert.That(buffer[0], Is.EqualTo(0x55)); + Assert.That(stream.ReadByte(), Is.EqualTo(-1)); + Assert.That(stream.Read(buffer, 0, 1), Is.EqualTo(0)); +#if NET5_0_OR_GREATER + Assert.That(stream.Read(buffer.AsSpan(0, 1)), Is.EqualTo(0)); +#endif + + var array = stream.ToArray(); + Assert.That(array.Length, Is.EqualTo(3)); + Assert.That(array[0], Is.EqualTo(0xaa)); + Assert.That(array[1], Is.EqualTo(0x55)); + Assert.That(array[2], Is.EqualTo(0x55)); + + // now buffer sequence owns the buffers + using (var bufferSequence = stream.GetSequence("Test")) + { + var sequence = bufferSequence.Sequence; + Assert.That(sequence.Length, Is.EqualTo(3)); + Assert.That(sequence.Slice(0, 1).First.Span[0], Is.EqualTo(0xaa)); + Assert.That(sequence.Slice(1, 1).First.Span[0], Is.EqualTo(0x55)); + Assert.That(sequence.Slice(2, 1).First.Span[0], Is.EqualTo(0x55)); + } + + act(); + + Assert.Throws(() => stream.ToArray()); + } + + /// + /// Test the default behavior of . + /// + [Theory] + public void ArraySegmentStreamWrite( + [Values(0, 1, 16, 17, 128, 333, 777, 1024, 4096)] int chunkSize, + [Values(16, 128, 333, 1024, 4096, 65536)] int defaultBufferSize) + { + var random = new Random(42); + int length; + byte[] buffer = new byte[chunkSize]; + + // Arrange + var bufferManager = new BufferManager(nameof(ArraySegmentStreamWhenConstructedWithDefaultOptionsShouldNotThrow), defaultBufferSize); + using (var writer = new ArraySegmentStream(bufferManager)) + { + + // Act + for (int i = 0; i <= byte.MaxValue; i++) + { + // fill chunk with a byte + for (int v = 0; v < chunkSize; v++) + { + buffer[v] = (byte)i; + } + + // write next chunk + switch (random.Next(3)) + { + case 0: + for (int v = 0; v < chunkSize; v++) + { + writer.WriteByte((byte)i); + } + + break; + case 1: + writer.Write(buffer, 0, chunkSize); + break; +#if NET5_0_OR_GREATER + default: + writer.Write(buffer.AsSpan(0, chunkSize)); + break; +#else + default: + writer.Write(buffer, 0, chunkSize); + break; +#endif + } + } + + length = (byte.MaxValue + 1) * chunkSize; + long result = writer.Seek(0, SeekOrigin.Begin); + Assert.That(result, Is.EqualTo(0)); + + result = writer.Seek(0, SeekOrigin.End); + Assert.That(result, Is.EqualTo(length)); + + // read back from writer MemoryStream + result = writer.Seek(0, SeekOrigin.Begin); + Assert.That(result, Is.EqualTo(0)); + + Assert.That(writer.Length, Is.EqualTo(length)); + + long position; + for (int i = 0; i <= byte.MaxValue; i++) + { + if (random.Next(2) == 0) + { + position = writer.Seek(chunkSize * i, SeekOrigin.Begin); + Assert.That(position, Is.EqualTo(chunkSize * i)); + } + + switch (random.Next(3)) + { + case 0: + for (int v = 0; v < chunkSize; v++) + { + Assert.That(writer.ReadByte(), Is.EqualTo((byte)i)); + } + break; + default: +#if NET5_0_OR_GREATER + writer.Read(buffer.AsSpan(0, chunkSize)); + for (int v = 0; v < chunkSize; v++) + { + Assert.That(buffer[v], Is.EqualTo((byte)i)); + } + break; +#endif + case 1: + writer.Read(buffer, 0, chunkSize); + for (int v = 0; v < chunkSize; v++) + { + Assert.That(buffer[v], Is.EqualTo((byte)i)); + } + break; + } + } + + position = writer.Seek(0, SeekOrigin.Begin); + Assert.That(position, Is.EqualTo(0)); + + using (var bufferSequence = writer.GetSequence("Test")) + { + var sequence = bufferSequence.Sequence; + buffer = sequence.ToArray(); + + // Assert sequence properties + Assert.That(buffer.Length, Is.EqualTo(length)); + Assert.That(sequence.Length, Is.EqualTo(length)); + + for (int i = 0; i < buffer.Length; i++) + { + Assert.That(buffer[i], Is.EqualTo((byte)(i / chunkSize))); + } + + for (int i = 0; i <= byte.MaxValue; i++) + { + var chunkSequence = sequence.Slice(i * chunkSize, chunkSize); + Assert.That(chunkSequence.Length, Is.EqualTo((long)chunkSize)); + + buffer = chunkSequence.ToArray(); + for (int v = 0; v < chunkSize; v++) + { + Assert.That(buffer[v], Is.EqualTo((byte)i)); + } + } + } + } + } + } +} From daaa38e52a4e1b5b7702cd77489911b6fdb3d8e2 Mon Sep 17 00:00:00 2001 From: Martin Regen Date: Sun, 28 Jul 2024 11:34:32 +0200 Subject: [PATCH 47/83] fix asn1 exception --- Libraries/Opc.Ua.Security.Certificates/Common/Oids.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Libraries/Opc.Ua.Security.Certificates/Common/Oids.cs b/Libraries/Opc.Ua.Security.Certificates/Common/Oids.cs index ea601b9e2f..60f87bc86e 100644 --- a/Libraries/Opc.Ua.Security.Certificates/Common/Oids.cs +++ b/Libraries/Opc.Ua.Security.Certificates/Common/Oids.cs @@ -153,7 +153,7 @@ public static string GetRSAOid(HashAlgorithmName hashAlgorithm) } else { - throw new NotSupportedException($"Signing RSA with hash {hashAlgorithm.Name} is not supported. "); + throw new CryptographicException($"Signing RSA with hash {hashAlgorithm.Name} is not supported. "); } } @@ -181,7 +181,7 @@ public static string GetECDsaOid(HashAlgorithmName hashAlgorithm) } else { - throw new NotSupportedException($"Signing ECDsa with hash {hashAlgorithm.Name} is not supported. "); + throw new CryptographicException($"Signing ECDsa with hash {hashAlgorithm.Name} is not supported. "); } } @@ -206,7 +206,7 @@ public static HashAlgorithmName GetHashAlgorithmName(string oid) case Oids.RsaPkcs1Sha512: return HashAlgorithmName.SHA512; } - throw new NotSupportedException($"Hash algorithm {oid} is not supported. "); + throw new CryptographicException($"Hash algorithm {oid} is not supported. "); } } From 480237e05143b0377cf08ebf72e50cc978e766db Mon Sep 17 00:00:00 2001 From: Martin Regen Date: Mon, 5 Aug 2024 09:16:52 +0200 Subject: [PATCH 48/83] add certificate and crl fuzz targets --- .../Opc.Ua.Encoders.Fuzz.Tests.csproj | 2 + .../Fuzz.Tools/Encoders.Fuzz.Tools.csproj | 2 + .../Fuzz/FuzzableCode.Certificates.cs | 150 ++++++++++++++++++ Fuzzing/Encoders/Fuzz/FuzzableCode.cs | 4 +- .../Properties/AssemblyInfo.cs | 24 +++ .../Configuration/ConfigurationNodeManager.cs | 11 +- 6 files changed, 187 insertions(+), 6 deletions(-) create mode 100644 Fuzzing/Encoders/Fuzz/FuzzableCode.Certificates.cs diff --git a/Fuzzing/Encoders/Fuzz.Tests/Opc.Ua.Encoders.Fuzz.Tests.csproj b/Fuzzing/Encoders/Fuzz.Tests/Opc.Ua.Encoders.Fuzz.Tests.csproj index 53a6278276..6382493611 100644 --- a/Fuzzing/Encoders/Fuzz.Tests/Opc.Ua.Encoders.Fuzz.Tests.csproj +++ b/Fuzzing/Encoders/Fuzz.Tests/Opc.Ua.Encoders.Fuzz.Tests.csproj @@ -13,6 +13,8 @@ + + diff --git a/Fuzzing/Encoders/Fuzz.Tools/Encoders.Fuzz.Tools.csproj b/Fuzzing/Encoders/Fuzz.Tools/Encoders.Fuzz.Tools.csproj index 0908be4e2c..0b9ef99258 100644 --- a/Fuzzing/Encoders/Fuzz.Tools/Encoders.Fuzz.Tools.csproj +++ b/Fuzzing/Encoders/Fuzz.Tools/Encoders.Fuzz.Tools.csproj @@ -12,6 +12,8 @@ + + diff --git a/Fuzzing/Encoders/Fuzz/FuzzableCode.Certificates.cs b/Fuzzing/Encoders/Fuzz/FuzzableCode.Certificates.cs new file mode 100644 index 0000000000..502b516ef2 --- /dev/null +++ b/Fuzzing/Encoders/Fuzz/FuzzableCode.Certificates.cs @@ -0,0 +1,150 @@ +/* ======================================================================== + * Copyright (c) 2005-2024 The OPC Foundation, Inc. All rights reserved. + * + * OPC Foundation MIT License 1.00 + * + * Permission is hereby granted, free of charge, to any person + * obtaining a copy of this software and associated documentation + * files (the "Software"), to deal in the Software without + * restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following + * conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES + * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT + * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, + * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR + * OTHER DEALINGS IN THE SOFTWARE. + * + * The complete license agreement can be found here: + * http://opcfoundation.org/License/MIT/1.00/ + * ======================================================================*/ + +using System; +using System.IO; +using System.Linq; +using System.Security.Cryptography; +using System.Security.Cryptography.X509Certificates; +using Opc.Ua; + +/// +/// Fuzzing code for the certificate decoder. +/// +public static partial class FuzzableCode +{ + /// + /// The certificate decoder fuzz target for afl-fuzz. + /// + /// The stdin stream from the afl-fuzz process. + public static void AflfuzzCertificateDecoder(Stream stream) + { + using (var memoryStream = PrepareArraySegmentStream(stream)) + { + FuzzCertificateDecoderCore(memoryStream.ToArray()); + } + } + + /// + /// The certificate chain decoder fuzz target for afl-fuzz. + /// + /// The stdin stream from the afl-fuzz process. + public static void AflfuzzCertificateChainDecoder(Stream stream) + { + using (var memoryStream = PrepareArraySegmentStream(stream)) + { + FuzzCertificateChainDecoderCore(memoryStream.ToArray()); + } + } + + /// + /// The certificate chain decoder with custom blob fuzz target for afl-fuzz. + /// + /// The stdin stream from the afl-fuzz process. + public static void AflfuzzCertificateChainDecoderCustom(Stream stream) + { + using (var memoryStream = PrepareArraySegmentStream(stream)) + { + FuzzCertificateChainDecoderCore(memoryStream.ToArray(), true); + } + } + + /// + /// The certificate decoder fuzz target for libfuzzer. + /// + public static void LibfuzzCertificateDecoder(ReadOnlySpan input) + { + _ = FuzzCertificateDecoderCore(input); + } + + /// + /// The certificate encoder fuzz target for libfuzzer. + /// + public static void LibfuzzCertificateChainDecoder(ReadOnlySpan input) + { + _ = FuzzCertificateChainDecoderCore(input); + } + + /// + /// The certificate encoder fuzz target for libfuzzer. + /// + public static void LibfuzzCertificateChainDecoderCustom(ReadOnlySpan input) + { + _ = FuzzCertificateChainDecoderCore(input, true); + } + + /// + /// The fuzz target for the Certificate decoder. + /// + /// A byte array with fuzz content. + internal static X509Certificate2 FuzzCertificateDecoderCore(ReadOnlySpan serialized, bool throwAll = false) + { + try + { + return new X509Certificate2(serialized.ToArray()); + } + catch (CryptographicException) + { + if (!throwAll) + { + return null; + } + throw; + } + } + + /// + /// The fuzz target for the Certificate chain decoder. + /// + /// A byte array with fuzz content. + internal static X509Certificate2Collection FuzzCertificateChainDecoderCore(ReadOnlySpan serialized, bool useAsn1Parser = false, bool throwAll = false) + { + try + { + return Utils.ParseCertificateChainBlob(serialized.ToArray(), useAsn1Parser); + } + catch (ServiceResultException sre) + { + switch (sre.StatusCode) + { + case StatusCodes.BadCertificateInvalid: + if (!throwAll) + { + return null; + } + break; + + default: + break; + } + throw; + } + } +} + diff --git a/Fuzzing/Encoders/Fuzz/FuzzableCode.cs b/Fuzzing/Encoders/Fuzz/FuzzableCode.cs index 01d9536519..43528b1788 100644 --- a/Fuzzing/Encoders/Fuzz/FuzzableCode.cs +++ b/Fuzzing/Encoders/Fuzz/FuzzableCode.cs @@ -35,6 +35,8 @@ public static partial class FuzzableCode { private static ServiceMessageContext messageContext = ServiceMessageContext.GlobalContext; + private static BufferManager bufferManager = new BufferManager(nameof(FuzzableCode), 65535); + private static ChannelQuotas channelQuotas = new ChannelQuotas(); /// /// Print information about the fuzzer target. @@ -42,7 +44,7 @@ public static partial class FuzzableCode public static void FuzzInfo() { Console.WriteLine("OPC UA Core Encoder Fuzzer for afl-fuzz and libfuzzer."); - Console.WriteLine("Fuzzing targets for various aspects of the Binary, Json and Xml encoders."); + Console.WriteLine("Fuzzing targets for various aspects of the OPC UA Binary, Json and Xml encoders."); } /// diff --git a/Libraries/Opc.Ua.Security.Certificates/Properties/AssemblyInfo.cs b/Libraries/Opc.Ua.Security.Certificates/Properties/AssemblyInfo.cs index 30b5ad22b5..fc2c918461 100644 --- a/Libraries/Opc.Ua.Security.Certificates/Properties/AssemblyInfo.cs +++ b/Libraries/Opc.Ua.Security.Certificates/Properties/AssemblyInfo.cs @@ -46,7 +46,31 @@ "f78357b8745cb6e1334655afce1a9527ac92fc829ff585ea79f007e52ba0f83ead627e3edda40b" + "ec5ae574128fc9342cb57cb8285aa4e5b589c0ebef3be571b5c8f2ab1067f7c880e8f8882a73c8" + "0a12a1ef")] +[assembly: InternalsVisibleTo("Encoders.Fuzz, PublicKey = " + + // OPC Foundation Strong Name Public Key + "0024000004800000940000000602000000240000525341310004000001000100d987b12f068b35" + + "80429f3dde01397508880fc7e62621397618456ca1549aeacfbdb90c62adfe918f05ce3677b390" + + "f78357b8745cb6e1334655afce1a9527ac92fc829ff585ea79f007e52ba0f83ead627e3edda40b" + + "ec5ae574128fc9342cb57cb8285aa4e5b589c0ebef3be571b5c8f2ab1067f7c880e8f8882a73c8" + + "0a12a1ef")] +[assembly: InternalsVisibleTo("Encoders.Fuzz.Tools, PublicKey = " + + // OPC Foundation Strong Name Public Key + "0024000004800000940000000602000000240000525341310004000001000100d987b12f068b35" + + "80429f3dde01397508880fc7e62621397618456ca1549aeacfbdb90c62adfe918f05ce3677b390" + + "f78357b8745cb6e1334655afce1a9527ac92fc829ff585ea79f007e52ba0f83ead627e3edda40b" + + "ec5ae574128fc9342cb57cb8285aa4e5b589c0ebef3be571b5c8f2ab1067f7c880e8f8882a73c8" + + "0a12a1ef")] +[assembly: InternalsVisibleTo("Opc.Ua.Encoders.Fuzz.Tests, PublicKey = " + + // OPC Foundation Strong Name Public Key + "0024000004800000940000000602000000240000525341310004000001000100d987b12f068b35" + + "80429f3dde01397508880fc7e62621397618456ca1549aeacfbdb90c62adfe918f05ce3677b390" + + "f78357b8745cb6e1334655afce1a9527ac92fc829ff585ea79f007e52ba0f83ead627e3edda40b" + + "ec5ae574128fc9342cb57cb8285aa4e5b589c0ebef3be571b5c8f2ab1067f7c880e8f8882a73c8" + + "0a12a1ef")] #else [assembly: InternalsVisibleTo("Opc.Ua.Security.Certificates.Tests")] [assembly: InternalsVisibleTo("Opc.Ua.Core.Tests")] +[assembly: InternalsVisibleTo("Encoders.Fuzz")] +[assembly: InternalsVisibleTo("Encoders.Fuzz.Tools")] +[assembly: InternalsVisibleTo("Opc.Ua.Encoders.Fuzz.Tests")] #endif diff --git a/Libraries/Opc.Ua.Server/Configuration/ConfigurationNodeManager.cs b/Libraries/Opc.Ua.Server/Configuration/ConfigurationNodeManager.cs index 68cc77dbca..44b14e1134 100644 --- a/Libraries/Opc.Ua.Server/Configuration/ConfigurationNodeManager.cs +++ b/Libraries/Opc.Ua.Server/Configuration/ConfigurationNodeManager.cs @@ -48,7 +48,8 @@ public class SystemConfigurationIdentity : RoleBasedIdentity /// /// The user identity. public SystemConfigurationIdentity(IUserIdentity identity) - :base(identity, new List {Role.SecurityAdmin, Role.ConfigureAdmin }){ + : base(identity, new List { Role.SecurityAdmin, Role.ConfigureAdmin }) + { } } @@ -231,7 +232,7 @@ public void CreateServerConfiguration( } } - + /// /// Gets and returns the node associated with the specified NamespaceUri @@ -638,14 +639,14 @@ private ServiceResult GetCertificates( //TODO support multiple Application Instance Certificates if (certificateTypeId != null) { - certificateTypeIds = new NodeId[1] {certificateTypeId }; + certificateTypeIds = new NodeId[1] { certificateTypeId }; certificates = new byte[1][]; - certificates[0] = certificateGroup.ApplicationCertificate.Certificate.GetRawCertData(); + certificates[0] = certificateGroup.ApplicationCertificate.Certificate.RawData; } else { certificateTypeIds = new NodeId[0]; - certificates = new byte[0][]; + certificates = Array.Empty(); } return ServiceResult.Good; From 5940bd1002061c303f1b11df15808396d11c362b Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 11 Sep 2024 16:53:44 +0200 Subject: [PATCH 49/83] Bump Moq from 4.20.70 to 4.20.72 (#2758) Bumps [Moq](https://github.com/moq/moq) from 4.20.70 to 4.20.72. - [Release notes](https://github.com/moq/moq/releases) - [Changelog](https://github.com/devlooped/moq/blob/main/changelog.md) - [Commits](https://github.com/moq/moq/compare/v4.20.70...v4.20.72) --- updated-dependencies: - dependency-name: Moq dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- Tests/Opc.Ua.Core.Tests/Opc.Ua.Core.Tests.csproj | 2 +- Tests/Opc.Ua.PubSub.Tests/Opc.Ua.PubSub.Tests.csproj | 2 +- Tests/Opc.Ua.Server.Tests/Opc.Ua.Server.Tests.csproj | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Tests/Opc.Ua.Core.Tests/Opc.Ua.Core.Tests.csproj b/Tests/Opc.Ua.Core.Tests/Opc.Ua.Core.Tests.csproj index 25bebede5e..e3750f5c58 100644 --- a/Tests/Opc.Ua.Core.Tests/Opc.Ua.Core.Tests.csproj +++ b/Tests/Opc.Ua.Core.Tests/Opc.Ua.Core.Tests.csproj @@ -11,7 +11,7 @@ - + diff --git a/Tests/Opc.Ua.PubSub.Tests/Opc.Ua.PubSub.Tests.csproj b/Tests/Opc.Ua.PubSub.Tests/Opc.Ua.PubSub.Tests.csproj index 0b94a18375..f632cbca4f 100644 --- a/Tests/Opc.Ua.PubSub.Tests/Opc.Ua.PubSub.Tests.csproj +++ b/Tests/Opc.Ua.PubSub.Tests/Opc.Ua.PubSub.Tests.csproj @@ -16,7 +16,7 @@ all runtime; build; native; contentfiles; analyzers - + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/Tests/Opc.Ua.Server.Tests/Opc.Ua.Server.Tests.csproj b/Tests/Opc.Ua.Server.Tests/Opc.Ua.Server.Tests.csproj index a29e307506..324787671c 100644 --- a/Tests/Opc.Ua.Server.Tests/Opc.Ua.Server.Tests.csproj +++ b/Tests/Opc.Ua.Server.Tests/Opc.Ua.Server.Tests.csproj @@ -9,7 +9,7 @@ - + From 4f57edbcabde5df9d86239e7a065bb1ab31ed824 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 11 Sep 2024 16:54:23 +0200 Subject: [PATCH 50/83] Bump MQTTnet from 4.3.6.1152 to 4.3.7.1207 (#2756) Bumps [MQTTnet](https://github.com/dotnet/MQTTnet) from 4.3.6.1152 to 4.3.7.1207. - [Release notes](https://github.com/dotnet/MQTTnet/releases) - [Commits](https://github.com/dotnet/MQTTnet/compare/v4.3.6.1152...v4.3.7.1207) --- updated-dependencies: - dependency-name: MQTTnet dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- Libraries/Opc.Ua.PubSub/Opc.Ua.PubSub.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Libraries/Opc.Ua.PubSub/Opc.Ua.PubSub.csproj b/Libraries/Opc.Ua.PubSub/Opc.Ua.PubSub.csproj index d287a1fd39..7286c6b3ec 100644 --- a/Libraries/Opc.Ua.PubSub/Opc.Ua.PubSub.csproj +++ b/Libraries/Opc.Ua.PubSub/Opc.Ua.PubSub.csproj @@ -36,7 +36,7 @@ - + From 436ca73b8048bf8fc036c3870fd2286c03982b78 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 12 Sep 2024 09:45:43 +0200 Subject: [PATCH 51/83] Bump Microsoft.NET.Test.Sdk from 17.11.0 to 17.11.1 (#2757) Bumps [Microsoft.NET.Test.Sdk](https://github.com/microsoft/vstest) from 17.11.0 to 17.11.1. - [Release notes](https://github.com/microsoft/vstest/releases) - [Changelog](https://github.com/microsoft/vstest/blob/main/docs/releases.md) - [Commits](https://github.com/microsoft/vstest/compare/v17.11.0...v17.11.1) --- updated-dependencies: - dependency-name: Microsoft.NET.Test.Sdk dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- Fuzzing/Encoders/Fuzz.Tests/Opc.Ua.Encoders.Fuzz.Tests.csproj | 2 +- .../Opc.Ua.Client.ComplexTypes.Tests.csproj | 2 +- Tests/Opc.Ua.Client.Tests/Opc.Ua.Client.Tests.csproj | 2 +- .../Opc.Ua.Configuration.Tests.csproj | 2 +- Tests/Opc.Ua.Core.Tests/Opc.Ua.Core.Tests.csproj | 2 +- Tests/Opc.Ua.Gds.Tests/Opc.Ua.Gds.Tests.csproj | 2 +- Tests/Opc.Ua.PubSub.Tests/Opc.Ua.PubSub.Tests.csproj | 2 +- .../Opc.Ua.Security.Certificates.Tests.csproj | 2 +- Tests/Opc.Ua.Server.Tests/Opc.Ua.Server.Tests.csproj | 2 +- 9 files changed, 9 insertions(+), 9 deletions(-) diff --git a/Fuzzing/Encoders/Fuzz.Tests/Opc.Ua.Encoders.Fuzz.Tests.csproj b/Fuzzing/Encoders/Fuzz.Tests/Opc.Ua.Encoders.Fuzz.Tests.csproj index c72601593e..37aeaa7855 100644 --- a/Fuzzing/Encoders/Fuzz.Tests/Opc.Ua.Encoders.Fuzz.Tests.csproj +++ b/Fuzzing/Encoders/Fuzz.Tests/Opc.Ua.Encoders.Fuzz.Tests.csproj @@ -18,7 +18,7 @@ - + diff --git a/Tests/Opc.Ua.Client.ComplexTypes.Tests/Opc.Ua.Client.ComplexTypes.Tests.csproj b/Tests/Opc.Ua.Client.ComplexTypes.Tests/Opc.Ua.Client.ComplexTypes.Tests.csproj index f9f4e445b9..7cd79df37a 100644 --- a/Tests/Opc.Ua.Client.ComplexTypes.Tests/Opc.Ua.Client.ComplexTypes.Tests.csproj +++ b/Tests/Opc.Ua.Client.ComplexTypes.Tests/Opc.Ua.Client.ComplexTypes.Tests.csproj @@ -7,7 +7,7 @@ - + diff --git a/Tests/Opc.Ua.Client.Tests/Opc.Ua.Client.Tests.csproj b/Tests/Opc.Ua.Client.Tests/Opc.Ua.Client.Tests.csproj index f14393f766..89000169d6 100644 --- a/Tests/Opc.Ua.Client.Tests/Opc.Ua.Client.Tests.csproj +++ b/Tests/Opc.Ua.Client.Tests/Opc.Ua.Client.Tests.csproj @@ -9,7 +9,7 @@ - + diff --git a/Tests/Opc.Ua.Configuration.Tests/Opc.Ua.Configuration.Tests.csproj b/Tests/Opc.Ua.Configuration.Tests/Opc.Ua.Configuration.Tests.csproj index 8e9bd6ab3d..d6d55125c4 100644 --- a/Tests/Opc.Ua.Configuration.Tests/Opc.Ua.Configuration.Tests.csproj +++ b/Tests/Opc.Ua.Configuration.Tests/Opc.Ua.Configuration.Tests.csproj @@ -8,7 +8,7 @@ - + diff --git a/Tests/Opc.Ua.Core.Tests/Opc.Ua.Core.Tests.csproj b/Tests/Opc.Ua.Core.Tests/Opc.Ua.Core.Tests.csproj index e3750f5c58..52a87fcf0a 100644 --- a/Tests/Opc.Ua.Core.Tests/Opc.Ua.Core.Tests.csproj +++ b/Tests/Opc.Ua.Core.Tests/Opc.Ua.Core.Tests.csproj @@ -10,7 +10,7 @@ - + diff --git a/Tests/Opc.Ua.Gds.Tests/Opc.Ua.Gds.Tests.csproj b/Tests/Opc.Ua.Gds.Tests/Opc.Ua.Gds.Tests.csproj index 7f4a04ad3f..4299a5f25c 100644 --- a/Tests/Opc.Ua.Gds.Tests/Opc.Ua.Gds.Tests.csproj +++ b/Tests/Opc.Ua.Gds.Tests/Opc.Ua.Gds.Tests.csproj @@ -12,7 +12,7 @@ - + diff --git a/Tests/Opc.Ua.PubSub.Tests/Opc.Ua.PubSub.Tests.csproj b/Tests/Opc.Ua.PubSub.Tests/Opc.Ua.PubSub.Tests.csproj index f632cbca4f..dee5340e72 100644 --- a/Tests/Opc.Ua.PubSub.Tests/Opc.Ua.PubSub.Tests.csproj +++ b/Tests/Opc.Ua.PubSub.Tests/Opc.Ua.PubSub.Tests.csproj @@ -8,7 +8,7 @@ - + diff --git a/Tests/Opc.Ua.Security.Certificates.Tests/Opc.Ua.Security.Certificates.Tests.csproj b/Tests/Opc.Ua.Security.Certificates.Tests/Opc.Ua.Security.Certificates.Tests.csproj index 2d7244803c..6d1bd02ccd 100644 --- a/Tests/Opc.Ua.Security.Certificates.Tests/Opc.Ua.Security.Certificates.Tests.csproj +++ b/Tests/Opc.Ua.Security.Certificates.Tests/Opc.Ua.Security.Certificates.Tests.csproj @@ -23,7 +23,7 @@ - + diff --git a/Tests/Opc.Ua.Server.Tests/Opc.Ua.Server.Tests.csproj b/Tests/Opc.Ua.Server.Tests/Opc.Ua.Server.Tests.csproj index 324787671c..9d6a520f3f 100644 --- a/Tests/Opc.Ua.Server.Tests/Opc.Ua.Server.Tests.csproj +++ b/Tests/Opc.Ua.Server.Tests/Opc.Ua.Server.Tests.csproj @@ -8,7 +8,7 @@ - + From cf2e7881253eae9e440a46f5d3135c3a9df2a11d Mon Sep 17 00:00:00 2001 From: Martin Regen Date: Thu, 12 Sep 2024 13:36:47 +0200 Subject: [PATCH 52/83] Openstore returns null if CertificateIdentifier is empty (#2761) - OpenStore behavior changed to return null if the storepath or storetype are invalid or empty, which is a change to the previous release. Hence some support functions should check the return value of OpenStore. - Fix: Check for null in LoadPrivateKeyEx - in addition: try fix the new flaky test issue - fix #2637: an exception when the https server starts and a X509Store certificate has a non exportable private key, it is not needed to create a copy for the TLS transport. Ignore the error and start the cert with the original cert. - restructure which platforms are supported by https and complextypes, to avoid dependency collisions. Remove netcoreapp3.1 (eol), support netstandard2.0/2.1 instead. --------- Co-authored-by: Hoelterhoff, Noah --- .azurepipelines/signlistDebug.txt | 4 ++- .azurepipelines/signlistRelease.txt | 4 ++- .../Stack/Https/HttpsTransportListener.cs | 27 ++++++++++++++----- .../Certificates/CertificateIdentifier.cs | 8 +++++- .../Certificates/TemporaryCertValidator.cs | 16 +++++++++-- .../Stack/Client/ClientTests.cs | 13 +++++++++ targets.props | 12 ++++----- 7 files changed, 66 insertions(+), 18 deletions(-) diff --git a/.azurepipelines/signlistDebug.txt b/.azurepipelines/signlistDebug.txt index c65a2d4cca..077e6c4704 100644 --- a/.azurepipelines/signlistDebug.txt +++ b/.azurepipelines/signlistDebug.txt @@ -4,7 +4,8 @@ Stack\Opc.Ua.Core\bin\Debug\net472\Opc.Ua.Core.dll Stack\Opc.Ua.Core\bin\Debug\net48\Opc.Ua.Core.dll Stack\Opc.Ua.Core\bin\Debug\net6.0\Opc.Ua.Core.dll Stack\Opc.Ua.Core\bin\Debug\net8.0\Opc.Ua.Core.dll -Stack\Opc.Ua.Bindings.Https\bin\Debug\netcoreapp3.1\Opc.Ua.Bindings.Https.dll +Stack\Opc.Ua.Bindings.Https\bin\Debug\netstandard2.0\Opc.Ua.Bindings.Https.dll +Stack\Opc.Ua.Bindings.Https\bin\Debug\netstandard2.1\Opc.Ua.Bindings.Https.dll Stack\Opc.Ua.Bindings.Https\bin\Debug\net472\Opc.Ua.Bindings.Https.dll Stack\Opc.Ua.Bindings.Https\bin\Debug\net48\Opc.Ua.Bindings.Https.dll Stack\Opc.Ua.Bindings.Https\bin\Debug\net6.0\Opc.Ua.Bindings.Https.dll @@ -22,6 +23,7 @@ Libraries\Opc.Ua.Client\bin\Debug\net48\Opc.Ua.Client.dll Libraries\Opc.Ua.Client\bin\Debug\net6.0\Opc.Ua.Client.dll Libraries\Opc.Ua.Client\bin\Debug\net8.0\Opc.Ua.Client.dll Libraries\Opc.Ua.Client.ComplexTypes\bin\Debug\netstandard2.1\Opc.Ua.Client.ComplexTypes.dll +Libraries\Opc.Ua.Client.ComplexTypes\bin\Debug\net462\Opc.Ua.Client.ComplexTypes.dll Libraries\Opc.Ua.Client.ComplexTypes\bin\Debug\net472\Opc.Ua.Client.ComplexTypes.dll Libraries\Opc.Ua.Client.ComplexTypes\bin\Debug\net48\Opc.Ua.Client.ComplexTypes.dll Libraries\Opc.Ua.Client.ComplexTypes\bin\Debug\net6.0\Opc.Ua.Client.ComplexTypes.dll diff --git a/.azurepipelines/signlistRelease.txt b/.azurepipelines/signlistRelease.txt index 824314c07a..c42668f930 100644 --- a/.azurepipelines/signlistRelease.txt +++ b/.azurepipelines/signlistRelease.txt @@ -4,7 +4,8 @@ Stack\Opc.Ua.Core\bin\Release\net472\Opc.Ua.Core.dll Stack\Opc.Ua.Core\bin\Release\net48\Opc.Ua.Core.dll Stack\Opc.Ua.Core\bin\Release\net6.0\Opc.Ua.Core.dll Stack\Opc.Ua.Core\bin\Release\net8.0\Opc.Ua.Core.dll -Stack\Opc.Ua.Bindings.Https\bin\Release\netcoreapp3.1\Opc.Ua.Bindings.Https.dll +Stack\Opc.Ua.Bindings.Https\bin\Release\netstandard2.0\Opc.Ua.Bindings.Https.dll +Stack\Opc.Ua.Bindings.Https\bin\Release\netstandard2.1\Opc.Ua.Bindings.Https.dll Stack\Opc.Ua.Bindings.Https\bin\Release\net472\Opc.Ua.Bindings.Https.dll Stack\Opc.Ua.Bindings.Https\bin\Release\net48\Opc.Ua.Bindings.Https.dll Stack\Opc.Ua.Bindings.Https\bin\Release\net6.0\Opc.Ua.Bindings.Https.dll @@ -22,6 +23,7 @@ Libraries\Opc.Ua.Client\bin\Release\net48\Opc.Ua.Client.dll Libraries\Opc.Ua.Client\bin\Release\net6.0\Opc.Ua.Client.dll Libraries\Opc.Ua.Client\bin\Release\net8.0\Opc.Ua.Client.dll Libraries\Opc.Ua.Client.ComplexTypes\bin\Release\netstandard2.1\Opc.Ua.Client.ComplexTypes.dll +Libraries\Opc.Ua.Client.ComplexTypes\bin\Release\net462\Opc.Ua.Client.ComplexTypes.dll Libraries\Opc.Ua.Client.ComplexTypes\bin\Release\net472\Opc.Ua.Client.ComplexTypes.dll Libraries\Opc.Ua.Client.ComplexTypes\bin\Release\net48\Opc.Ua.Client.ComplexTypes.dll Libraries\Opc.Ua.Client.ComplexTypes\bin\Release\net6.0\Opc.Ua.Client.ComplexTypes.dll diff --git a/Stack/Opc.Ua.Bindings.Https/Stack/Https/HttpsTransportListener.cs b/Stack/Opc.Ua.Bindings.Https/Stack/Https/HttpsTransportListener.cs index ea1e5fceb8..c11742160d 100644 --- a/Stack/Opc.Ua.Bindings.Https/Stack/Https/HttpsTransportListener.cs +++ b/Stack/Opc.Ua.Bindings.Https/Stack/Https/HttpsTransportListener.cs @@ -15,6 +15,7 @@ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. using System.IO; using System.Net; using System.Security.Authentication; +using System.Security.Cryptography; using System.Security.Cryptography.X509Certificates; using System.Threading; using System.Threading.Tasks; @@ -262,17 +263,29 @@ public void Start() { Startup.Listener = this; m_hostBuilder = new WebHostBuilder(); + + // prepare the server TLS certificate + var serverCertificate = m_serverCertificate; +#if NETCOREAPP3_1_OR_GREATER || NETSTANDARD2_1 || NET472_OR_GREATER || NET5_0_OR_GREATER + try + { + // Create a copy of the certificate with the private key on platforms + // which default to the ephemeral KeySet. Also a new certificate must be reloaded. + // If the key fails to copy, its probably a non exportable key from the X509Store. + // Then we can use the original certificate, the private key is already in the key store. + serverCertificate = X509Utils.CreateCopyWithPrivateKey(m_serverCertificate, false); + } + catch (CryptographicException ce) + { + Utils.LogTrace("Copy of the private key for https was denied: {0}", ce.Message); + } +#endif + var httpsOptions = new HttpsConnectionAdapterOptions() { CheckCertificateRevocation = false, ClientCertificateMode = ClientCertificateMode.NoCertificate, // note: this is the TLS certificate! -#if NETCOREAPP3_1_OR_GREATER || NETSTANDARD2_1 || NET472_OR_GREATER || NET5_0_OR_GREATER - // Create a copy of the certificate with the private key on platforms - // which default to the ephemeral KeySet. - ServerCertificate = X509Utils.CreateCopyWithPrivateKey(m_serverCertificate, false) -#else - ServerCertificate = m_serverCertificate -#endif + ServerCertificate = serverCertificate, }; #if NET462 diff --git a/Stack/Opc.Ua.Core/Security/Certificates/CertificateIdentifier.cs b/Stack/Opc.Ua.Core/Security/Certificates/CertificateIdentifier.cs index 4d739a75cd..5a7456a810 100644 --- a/Stack/Opc.Ua.Core/Security/Certificates/CertificateIdentifier.cs +++ b/Stack/Opc.Ua.Core/Security/Certificates/CertificateIdentifier.cs @@ -170,13 +170,14 @@ public async Task LoadPrivateKeyEx(ICertificatePasswordProvide var certificateStoreIdentifier = new CertificateStoreIdentifier(this.StorePath, this.StoreType, false); using (ICertificateStore store = certificateStoreIdentifier.OpenStore()) { - if (store.SupportsLoadPrivateKey) + if (store?.SupportsLoadPrivateKey == true) { string password = passwordProvider?.GetPassword(this); m_certificate = await store.LoadPrivateKey(this.Thumbprint, this.SubjectName, password).ConfigureAwait(false); return m_certificate; } } + return null; } return await Find(true).ConfigureAwait(false); } @@ -202,6 +203,11 @@ public async Task Find(bool needPrivateKey) var certificateStoreIdentifier = new CertificateStoreIdentifier(StorePath, false); using (ICertificateStore store = certificateStoreIdentifier.OpenStore()) { + if (store == null) + { + return null; + } + X509Certificate2Collection collection = await store.Enumerate().ConfigureAwait(false); certificate = Find(collection, m_thumbprint, m_subjectName, needPrivateKey); diff --git a/Tests/Opc.Ua.Core.Tests/Security/Certificates/TemporaryCertValidator.cs b/Tests/Opc.Ua.Core.Tests/Security/Certificates/TemporaryCertValidator.cs index 75984661d8..362cce1791 100644 --- a/Tests/Opc.Ua.Core.Tests/Security/Certificates/TemporaryCertValidator.cs +++ b/Tests/Opc.Ua.Core.Tests/Security/Certificates/TemporaryCertValidator.cs @@ -85,9 +85,21 @@ public void Dispose() m_trustedStore = null; m_rejectedStore = null; var path = Utils.ReplaceSpecialFolderNames(m_pkiRoot); - if (Directory.Exists(path)) + int retries = 5; + while (retries-- > 0) { - Directory.Delete(path, true); + try + { + if (Directory.Exists(path)) + { + Directory.Delete(path, true); + } + retries = 0; + } + catch (IOException) + { + Thread.Sleep(1000); + } } } } diff --git a/Tests/Opc.Ua.Core.Tests/Stack/Client/ClientTests.cs b/Tests/Opc.Ua.Core.Tests/Stack/Client/ClientTests.cs index 4c11b8c7f9..52ffe29ede 100644 --- a/Tests/Opc.Ua.Core.Tests/Stack/Client/ClientTests.cs +++ b/Tests/Opc.Ua.Core.Tests/Stack/Client/ClientTests.cs @@ -94,6 +94,19 @@ public void DiscoveryEndPointUrls(string urlString) Assert.AreEqual(uri.OriginalString, uriBuilder.Uri.OriginalString); } + + [Test] + public void ValidateAppConfigWithoutAppCert() + { + var appConfig = new ApplicationConfiguration() { + ApplicationName = "Test", + ClientConfiguration = new ClientConfiguration() {}, + SecurityConfiguration = new SecurityConfiguration() { + ApplicationCertificate = new CertificateIdentifier() + } + }; + Assert.DoesNotThrow(() => appConfig.Validate(ApplicationType.Client).GetAwaiter().GetResult()); + } #endregion } } diff --git a/targets.props b/targets.props index 8895879623..17b539d7c1 100644 --- a/targets.props +++ b/targets.props @@ -69,7 +69,7 @@ netstandard2.1 netstandard2.1 netstandard2.1 - netcoreapp3.1 + netstandard2.1 @@ -108,9 +108,9 @@ net8.0 - - - + + + preview-all @@ -119,8 +119,8 @@ net48;net8.0 net472;net48;netstandard2.0;netstandard2.1;net6.0;net8.0 net472;net48;netstandard2.0;netstandard2.1;net6.0;net8.0 - net472;net48;netstandard2.1;net6.0;net8.0 - net472;net48;netcoreapp3.1;net6.0;net8.0 + net462;net472;net48;netstandard2.1;net6.0;net8.0 + net472;net48;netstandard2.0;netstandard2.1;net6.0;net8.0 From 36dbdd38736b76c1fbed6784e93eebfd6b0b5704 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 24 Sep 2024 07:22:24 +0200 Subject: [PATCH 53/83] Bump NUnit.Console from 3.18.1 to 3.18.2 (#2769) Bumps [NUnit.Console](https://github.com/nunit/nunit-console) from 3.18.1 to 3.18.2. - [Release notes](https://github.com/nunit/nunit-console/releases) - [Changelog](https://github.com/nunit/nunit-console/blob/main/CHANGES.txt) - [Commits](https://github.com/nunit/nunit-console/compare/3.18.1...3.18.2) --- updated-dependencies: - dependency-name: NUnit.Console dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- Fuzzing/Encoders/Fuzz.Tests/Opc.Ua.Encoders.Fuzz.Tests.csproj | 2 +- .../Opc.Ua.Client.ComplexTypes.Tests.csproj | 2 +- Tests/Opc.Ua.Client.Tests/Opc.Ua.Client.Tests.csproj | 2 +- .../Opc.Ua.Configuration.Tests.csproj | 2 +- Tests/Opc.Ua.Core.Tests/Opc.Ua.Core.Tests.csproj | 2 +- Tests/Opc.Ua.Gds.Tests/Opc.Ua.Gds.Tests.csproj | 2 +- Tests/Opc.Ua.PubSub.Tests/Opc.Ua.PubSub.Tests.csproj | 2 +- .../Opc.Ua.Security.Certificates.Tests.csproj | 2 +- Tests/Opc.Ua.Server.Tests/Opc.Ua.Server.Tests.csproj | 2 +- 9 files changed, 9 insertions(+), 9 deletions(-) diff --git a/Fuzzing/Encoders/Fuzz.Tests/Opc.Ua.Encoders.Fuzz.Tests.csproj b/Fuzzing/Encoders/Fuzz.Tests/Opc.Ua.Encoders.Fuzz.Tests.csproj index 37aeaa7855..82ed45f2f8 100644 --- a/Fuzzing/Encoders/Fuzz.Tests/Opc.Ua.Encoders.Fuzz.Tests.csproj +++ b/Fuzzing/Encoders/Fuzz.Tests/Opc.Ua.Encoders.Fuzz.Tests.csproj @@ -21,7 +21,7 @@ - + all runtime; build; native; contentfiles; analyzers diff --git a/Tests/Opc.Ua.Client.ComplexTypes.Tests/Opc.Ua.Client.ComplexTypes.Tests.csproj b/Tests/Opc.Ua.Client.ComplexTypes.Tests/Opc.Ua.Client.ComplexTypes.Tests.csproj index 7cd79df37a..aaf832334a 100644 --- a/Tests/Opc.Ua.Client.ComplexTypes.Tests/Opc.Ua.Client.ComplexTypes.Tests.csproj +++ b/Tests/Opc.Ua.Client.ComplexTypes.Tests/Opc.Ua.Client.ComplexTypes.Tests.csproj @@ -10,7 +10,7 @@ - + all runtime; build; native; contentfiles; analyzers diff --git a/Tests/Opc.Ua.Client.Tests/Opc.Ua.Client.Tests.csproj b/Tests/Opc.Ua.Client.Tests/Opc.Ua.Client.Tests.csproj index 89000169d6..9b3852595e 100644 --- a/Tests/Opc.Ua.Client.Tests/Opc.Ua.Client.Tests.csproj +++ b/Tests/Opc.Ua.Client.Tests/Opc.Ua.Client.Tests.csproj @@ -12,7 +12,7 @@ - + all runtime; build; native; contentfiles; analyzers diff --git a/Tests/Opc.Ua.Configuration.Tests/Opc.Ua.Configuration.Tests.csproj b/Tests/Opc.Ua.Configuration.Tests/Opc.Ua.Configuration.Tests.csproj index d6d55125c4..40c265b9df 100644 --- a/Tests/Opc.Ua.Configuration.Tests/Opc.Ua.Configuration.Tests.csproj +++ b/Tests/Opc.Ua.Configuration.Tests/Opc.Ua.Configuration.Tests.csproj @@ -11,7 +11,7 @@ - + all runtime; build; native; contentfiles; analyzers diff --git a/Tests/Opc.Ua.Core.Tests/Opc.Ua.Core.Tests.csproj b/Tests/Opc.Ua.Core.Tests/Opc.Ua.Core.Tests.csproj index 52a87fcf0a..271495d07b 100644 --- a/Tests/Opc.Ua.Core.Tests/Opc.Ua.Core.Tests.csproj +++ b/Tests/Opc.Ua.Core.Tests/Opc.Ua.Core.Tests.csproj @@ -14,7 +14,7 @@ - + all runtime; build; native; contentfiles; analyzers diff --git a/Tests/Opc.Ua.Gds.Tests/Opc.Ua.Gds.Tests.csproj b/Tests/Opc.Ua.Gds.Tests/Opc.Ua.Gds.Tests.csproj index 4299a5f25c..0bc9e50dd2 100644 --- a/Tests/Opc.Ua.Gds.Tests/Opc.Ua.Gds.Tests.csproj +++ b/Tests/Opc.Ua.Gds.Tests/Opc.Ua.Gds.Tests.csproj @@ -15,7 +15,7 @@ - + all runtime; build; native; contentfiles; analyzers diff --git a/Tests/Opc.Ua.PubSub.Tests/Opc.Ua.PubSub.Tests.csproj b/Tests/Opc.Ua.PubSub.Tests/Opc.Ua.PubSub.Tests.csproj index dee5340e72..1401055ff8 100644 --- a/Tests/Opc.Ua.PubSub.Tests/Opc.Ua.PubSub.Tests.csproj +++ b/Tests/Opc.Ua.PubSub.Tests/Opc.Ua.PubSub.Tests.csproj @@ -11,7 +11,7 @@ - + all runtime; build; native; contentfiles; analyzers diff --git a/Tests/Opc.Ua.Security.Certificates.Tests/Opc.Ua.Security.Certificates.Tests.csproj b/Tests/Opc.Ua.Security.Certificates.Tests/Opc.Ua.Security.Certificates.Tests.csproj index 6d1bd02ccd..dd08e8d0f8 100644 --- a/Tests/Opc.Ua.Security.Certificates.Tests/Opc.Ua.Security.Certificates.Tests.csproj +++ b/Tests/Opc.Ua.Security.Certificates.Tests/Opc.Ua.Security.Certificates.Tests.csproj @@ -26,7 +26,7 @@ - + all runtime; build; native; contentfiles; analyzers diff --git a/Tests/Opc.Ua.Server.Tests/Opc.Ua.Server.Tests.csproj b/Tests/Opc.Ua.Server.Tests/Opc.Ua.Server.Tests.csproj index 9d6a520f3f..ef5019dfa3 100644 --- a/Tests/Opc.Ua.Server.Tests/Opc.Ua.Server.Tests.csproj +++ b/Tests/Opc.Ua.Server.Tests/Opc.Ua.Server.Tests.csproj @@ -12,7 +12,7 @@ - + all runtime; build; native; contentfiles; analyzers From 5349d9a0338f8d41fabccd3afdde97f9eb2f34b0 Mon Sep 17 00:00:00 2001 From: Suciu Mircea Adrian Date: Wed, 25 Sep 2024 13:18:02 +0300 Subject: [PATCH 54/83] Use HiresClock ticks instead of DateTime for IntervalRunner publish time calculation (#2753) * Use HiresClock instead of DateTime for IntervalRunner publish time calculation. --- Libraries/Opc.Ua.PubSub/IntervalRunner.cs | 46 +++++++++++++------ .../Configuration/UaPublisherTests.cs | 43 ++++++++++++----- 2 files changed, 63 insertions(+), 26 deletions(-) diff --git a/Libraries/Opc.Ua.PubSub/IntervalRunner.cs b/Libraries/Opc.Ua.PubSub/IntervalRunner.cs index fe8dbd9bea..a8a0d9b0ba 100644 --- a/Libraries/Opc.Ua.PubSub/IntervalRunner.cs +++ b/Libraries/Opc.Ua.PubSub/IntervalRunner.cs @@ -42,7 +42,8 @@ public class IntervalRunner : IDisposable private readonly object m_lock = new object(); private double m_interval = kMinInterval; - private DateTime m_nextPublishTime = DateTime.MinValue; + private double m_nextPublishTick = 0; + // event used to cancel run private CancellationTokenSource m_cancellationToken = new CancellationTokenSource(); @@ -154,30 +155,47 @@ protected virtual void Dispose(bool disposing) /// private async Task ProcessAsync() { + lock (m_lock) + { + m_nextPublishTick = HiResClock.Ticks; + } do { - int sleepCycle = 0; - DateTime now = DateTime.UtcNow; - DateTime nextPublishTime = DateTime.MinValue; - - lock (m_lock) + if (m_cancellationToken.IsCancellationRequested) { - sleepCycle = Convert.ToInt32(m_interval); + break; + } + + long nowTick = HiResClock.Ticks; + double nextPublishTick = 0; - nextPublishTime = m_nextPublishTime; + lock(m_lock) + { + nextPublishTick = m_nextPublishTick; } - if (nextPublishTime > now) + double sleepCycle = (nextPublishTick - nowTick) / HiResClock.TicksPerMillisecond; + if (sleepCycle > 16) { - sleepCycle = (int)Math.Min((nextPublishTime - now).TotalMilliseconds, sleepCycle); - sleepCycle = (int)Math.Max(kMinInterval, sleepCycle); + // Use Task.Delay if sleep cycle is larger await Task.Delay(TimeSpan.FromMilliseconds(sleepCycle), m_cancellationToken.Token).ConfigureAwait(false); - } + // Still ticks to consume (spurious wakeup too early), improbable + nowTick = HiResClock.Ticks; + if (nowTick < nextPublishTick) + { + SpinWait.SpinUntil(() => HiResClock.Ticks >= nextPublishTick); + } + } + else if (sleepCycle >= 0 && sleepCycle <= 16) + { + SpinWait.SpinUntil(() => HiResClock.Ticks >= nextPublishTick); + } + lock (m_lock) { - var nextCycle = Convert.ToInt32(m_interval); - m_nextPublishTime = DateTime.UtcNow.AddMilliseconds(nextCycle); + var nextCycle = (long)m_interval * HiResClock.TicksPerMillisecond; + m_nextPublishTick += nextCycle; if (IntervalAction != null && CanExecuteFunc != null && CanExecuteFunc()) { diff --git a/Tests/Opc.Ua.PubSub.Tests/Configuration/UaPublisherTests.cs b/Tests/Opc.Ua.PubSub.Tests/Configuration/UaPublisherTests.cs index 8b38c9d292..f30ce8baf7 100644 --- a/Tests/Opc.Ua.PubSub.Tests/Configuration/UaPublisherTests.cs +++ b/Tests/Opc.Ua.PubSub.Tests/Configuration/UaPublisherTests.cs @@ -37,10 +37,11 @@ namespace Opc.Ua.PubSub.Tests.Configuration { - [TestFixture(Description = "Tests for UAPublisher class")] + [TestFixture(Description = "Tests for UAPublisher class"), SingleThreaded] public class UaPublisherTests { - static IList s_publishTimes = new List(); + static List s_publishTicks = new List(); + static object s_lock = new Object(); [Test(Description = "Test that PublishMessage method is called after a UAPublisher is started.")] [Combinatorial] @@ -53,12 +54,17 @@ public void ValidateUaPublisherPublishIntervalDeviation( [Values(10)] int publishTimeInSeconds) { //Arrange - s_publishTimes.Clear(); + s_publishTicks.Clear(); var mockConnection = new Mock(); mockConnection.Setup(x => x.CanPublish(It.IsAny())).Returns(true); mockConnection.Setup(x => x.CreateNetworkMessages(It.IsAny(), It.IsAny())) - .Callback(() => s_publishTimes.Add(DateTime.Now)); + .Callback(() => { + lock (s_lock) + { + s_publishTicks.Add(HiResClock.Ticks); + } + }); WriterGroupDataType writerGroupDataType = new WriterGroupDataType(); writerGroupDataType.PublishingInterval = publishingInterval; @@ -70,25 +76,38 @@ public void ValidateUaPublisherPublishIntervalDeviation( //wait so many seconds Thread.Sleep(publishTimeInSeconds * 1000); publisher.Stop(); + int faultIndex = -1; double faultDeviation = 0; - s_publishTimes = (from t in s_publishTimes + s_publishTicks = (from t in s_publishTicks orderby t select t).ToList(); //Assert - for (int i = 1; i < s_publishTimes.Count; i++) + for (int i = 1; i < s_publishTicks.Count; i++) { - double interval = s_publishTimes[i].Subtract(s_publishTimes[i - 1]).TotalMilliseconds; - double deviation = Math.Abs(publishingInterval - interval); - if (deviation >= maxDeviation && deviation > faultDeviation) + double interval = (s_publishTicks[i] - s_publishTicks[i - 1])/HiResClock.TicksPerMillisecond; + if (interval != 0) { - faultIndex = i; - faultDeviation = deviation; + double deviation = -1; + if (interval != publishingInterval) + { + deviation = Math.Abs(publishingInterval - interval); + } + if (deviation >= maxDeviation && deviation > faultDeviation) + { + faultIndex = i; + faultDeviation = deviation; + } } } - Assert.IsTrue(faultIndex < 0, "publishingInterval={0}, maxDeviation={1}, publishTimeInSecods={2}, deviation[{3}] = {4} has maximum deviation", publishingInterval, maxDeviation, publishTimeInSeconds, faultIndex, faultDeviation); + Assert.IsTrue(faultIndex < 0, "publishingInterval={0}, maxDeviation={1}, publishTimeInSecods={2}, deviation[{3}] = {4} as max deviation", + publishingInterval, + maxDeviation, + publishTimeInSeconds, + faultIndex, + faultDeviation); } } } From cab1863f0c34d17a8401380ff760b11e7a3d4874 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 25 Sep 2024 13:16:40 +0200 Subject: [PATCH 55/83] Bump NUnit from 4.1.0 to 4.2.2 (#2742) Bumps [NUnit](https://github.com/nunit/nunit) from 4.1.0 to 4.2.2. - [Release notes](https://github.com/nunit/nunit/releases) - [Changelog](https://github.com/nunit/nunit/blob/main/CHANGES.md) - [Commits](https://github.com/nunit/nunit/compare/4.1.0...4.2.2) --- updated-dependencies: - dependency-name: NUnit dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- Fuzzing/Encoders/Fuzz.Tests/Opc.Ua.Encoders.Fuzz.Tests.csproj | 2 +- .../Opc.Ua.Client.ComplexTypes.Tests.csproj | 2 +- Tests/Opc.Ua.Client.Tests/Opc.Ua.Client.Tests.csproj | 2 +- .../Opc.Ua.Configuration.Tests.csproj | 2 +- Tests/Opc.Ua.Core.Tests/Opc.Ua.Core.Tests.csproj | 2 +- Tests/Opc.Ua.Gds.Tests/Opc.Ua.Gds.Tests.csproj | 2 +- Tests/Opc.Ua.PubSub.Tests/Opc.Ua.PubSub.Tests.csproj | 2 +- .../Opc.Ua.Security.Certificates.Tests.csproj | 2 +- Tests/Opc.Ua.Server.Tests/Opc.Ua.Server.Tests.csproj | 2 +- 9 files changed, 9 insertions(+), 9 deletions(-) diff --git a/Fuzzing/Encoders/Fuzz.Tests/Opc.Ua.Encoders.Fuzz.Tests.csproj b/Fuzzing/Encoders/Fuzz.Tests/Opc.Ua.Encoders.Fuzz.Tests.csproj index 82ed45f2f8..c740e2d375 100644 --- a/Fuzzing/Encoders/Fuzz.Tests/Opc.Ua.Encoders.Fuzz.Tests.csproj +++ b/Fuzzing/Encoders/Fuzz.Tests/Opc.Ua.Encoders.Fuzz.Tests.csproj @@ -20,7 +20,7 @@ - + all diff --git a/Tests/Opc.Ua.Client.ComplexTypes.Tests/Opc.Ua.Client.ComplexTypes.Tests.csproj b/Tests/Opc.Ua.Client.ComplexTypes.Tests/Opc.Ua.Client.ComplexTypes.Tests.csproj index aaf832334a..9987b1a1d5 100644 --- a/Tests/Opc.Ua.Client.ComplexTypes.Tests/Opc.Ua.Client.ComplexTypes.Tests.csproj +++ b/Tests/Opc.Ua.Client.ComplexTypes.Tests/Opc.Ua.Client.ComplexTypes.Tests.csproj @@ -9,7 +9,7 @@ - + all diff --git a/Tests/Opc.Ua.Client.Tests/Opc.Ua.Client.Tests.csproj b/Tests/Opc.Ua.Client.Tests/Opc.Ua.Client.Tests.csproj index 9b3852595e..76bcf04a3f 100644 --- a/Tests/Opc.Ua.Client.Tests/Opc.Ua.Client.Tests.csproj +++ b/Tests/Opc.Ua.Client.Tests/Opc.Ua.Client.Tests.csproj @@ -11,7 +11,7 @@ - + all diff --git a/Tests/Opc.Ua.Configuration.Tests/Opc.Ua.Configuration.Tests.csproj b/Tests/Opc.Ua.Configuration.Tests/Opc.Ua.Configuration.Tests.csproj index 40c265b9df..a7c617a427 100644 --- a/Tests/Opc.Ua.Configuration.Tests/Opc.Ua.Configuration.Tests.csproj +++ b/Tests/Opc.Ua.Configuration.Tests/Opc.Ua.Configuration.Tests.csproj @@ -10,7 +10,7 @@ - + all diff --git a/Tests/Opc.Ua.Core.Tests/Opc.Ua.Core.Tests.csproj b/Tests/Opc.Ua.Core.Tests/Opc.Ua.Core.Tests.csproj index 271495d07b..5a44bb0964 100644 --- a/Tests/Opc.Ua.Core.Tests/Opc.Ua.Core.Tests.csproj +++ b/Tests/Opc.Ua.Core.Tests/Opc.Ua.Core.Tests.csproj @@ -13,7 +13,7 @@ - + all diff --git a/Tests/Opc.Ua.Gds.Tests/Opc.Ua.Gds.Tests.csproj b/Tests/Opc.Ua.Gds.Tests/Opc.Ua.Gds.Tests.csproj index 0bc9e50dd2..350f9b10a8 100644 --- a/Tests/Opc.Ua.Gds.Tests/Opc.Ua.Gds.Tests.csproj +++ b/Tests/Opc.Ua.Gds.Tests/Opc.Ua.Gds.Tests.csproj @@ -14,7 +14,7 @@ - + all diff --git a/Tests/Opc.Ua.PubSub.Tests/Opc.Ua.PubSub.Tests.csproj b/Tests/Opc.Ua.PubSub.Tests/Opc.Ua.PubSub.Tests.csproj index 1401055ff8..2f81b8cf91 100644 --- a/Tests/Opc.Ua.PubSub.Tests/Opc.Ua.PubSub.Tests.csproj +++ b/Tests/Opc.Ua.PubSub.Tests/Opc.Ua.PubSub.Tests.csproj @@ -10,7 +10,7 @@ - + all diff --git a/Tests/Opc.Ua.Security.Certificates.Tests/Opc.Ua.Security.Certificates.Tests.csproj b/Tests/Opc.Ua.Security.Certificates.Tests/Opc.Ua.Security.Certificates.Tests.csproj index dd08e8d0f8..b407213078 100644 --- a/Tests/Opc.Ua.Security.Certificates.Tests/Opc.Ua.Security.Certificates.Tests.csproj +++ b/Tests/Opc.Ua.Security.Certificates.Tests/Opc.Ua.Security.Certificates.Tests.csproj @@ -25,7 +25,7 @@ - + all diff --git a/Tests/Opc.Ua.Server.Tests/Opc.Ua.Server.Tests.csproj b/Tests/Opc.Ua.Server.Tests/Opc.Ua.Server.Tests.csproj index ef5019dfa3..1741621781 100644 --- a/Tests/Opc.Ua.Server.Tests/Opc.Ua.Server.Tests.csproj +++ b/Tests/Opc.Ua.Server.Tests/Opc.Ua.Server.Tests.csproj @@ -11,7 +11,7 @@ - + all From d7680fadaa2d2e089bc2081cc4b2e96ce6a45538 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 25 Sep 2024 20:27:55 +0200 Subject: [PATCH 56/83] Bump Nerdbank.GitVersioning from 3.6.141 to 3.6.143 (#2743) * Bump Nerdbank.GitVersioning from 3.6.141 to 3.6.143 Bumps [Nerdbank.GitVersioning](https://github.com/dotnet/Nerdbank.GitVersioning) from 3.6.141 to 3.6.143. - [Release notes](https://github.com/dotnet/Nerdbank.GitVersioning/releases) - [Commits](https://github.com/dotnet/Nerdbank.GitVersioning/compare/v3.6.141...v3.6.143) --- updated-dependencies: - dependency-name: Nerdbank.GitVersioning dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] * Update get-version.ps1 --------- Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Martin Regen --- .azurepipelines/get-version.ps1 | 4 ++-- version.props | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.azurepipelines/get-version.ps1 b/.azurepipelines/get-version.ps1 index 244ee6fe38..02ef3e8b9a 100644 --- a/.azurepipelines/get-version.ps1 +++ b/.azurepipelines/get-version.ps1 @@ -9,8 +9,8 @@ try { # Try install tool - # Note: Keep Version 3.6.141, it is known working for 4 digit versioning - & dotnet @("tool", "install", "--tool-path", "./tools", "--version", "3.6.141", "--framework", "net60", "nbgv") 2>&1 + # Note: Keep Version 3.6.143, it is known working for 4 digit versioning + & dotnet @("tool", "install", "--tool-path", "./tools", "--version", "3.6.143", "--framework", "net60", "nbgv") 2>&1 $props = (& ./tools/nbgv @("get-version", "-f", "json")) | ConvertFrom-Json if ($LastExitCode -ne 0) { diff --git a/version.props b/version.props index ea65f3633b..f4038e5902 100644 --- a/version.props +++ b/version.props @@ -1,7 +1,7 @@ - + all runtime; build; native; contentfiles; analyzers; buildtransitive From 8131a0298483c85654ceae9587bd849e3e355206 Mon Sep 17 00:00:00 2001 From: Martin Regen Date: Thu, 26 Sep 2024 09:59:02 +0200 Subject: [PATCH 57/83] Pubsub JSON updates preview (#2768) Add support for Compact and Verbose JSON encodings. Other bug fixes: - Defaults for non reversible forces the namespace Uri in the NodeId encoding and all default values are encoded. This behavior is according to spec. Old behavior can be changed reestablished with properties on creation of the JSON encoders. - Compact and Verbose are preview for the next spec release. Main difference is to always encode NodeIds and ExpandedNodeIds as string with NamespaceUri. Same for QualifiedName. Both are considered reversible encodings. - JSON decoder accepts any of the new and existing JSOn encodings no matter if the NodeId are encoded as string or JSON object. Co-authored-by: Randy Armstrong --- .../ConsoleReferenceClient/ClientSamples.cs | 10 +- .../Encoding/JsonDataSetMessage.cs | 2 + Libraries/Opc.Ua.Server/Opc.Ua.Server.csproj | 2 +- .../Types/BuiltIn/ExpandedNodeId.cs | 192 ++- Stack/Opc.Ua.Core/Types/BuiltIn/NodeId.cs | 246 ++- .../Types/BuiltIn/QualifiedName.cs | 104 ++ Stack/Opc.Ua.Core/Types/Encoders/IEncoder.cs | 6 +- .../Types/Encoders/IJsonEncoder.cs | 40 +- .../Opc.Ua.Core/Types/Encoders/JsonDecoder.cs | 324 +++- .../Opc.Ua.Core/Types/Encoders/JsonEncoder.cs | 388 +++-- .../Opc.Ua.Core/Types/Utils/DataGenerator.cs | 2 +- Stack/Opc.Ua.Core/Types/Utils/Utils.cs | 84 +- .../Types/ComplexTypesCommon.cs | 3 +- .../Types/EncoderTests.cs | 29 +- .../Types/JsonEncoderTests.cs | 366 ++-- .../Types/MockResolverTests.cs | 33 +- .../Types/Encoders/EncodeableTests.cs | 21 +- .../Types/Encoders/EncoderCommon.cs | 175 +- .../Types/Encoders/EncoderTests.cs | 302 +++- .../Types/Encoders/JsonEncoderCompactTests.cs | 1532 +++++++++++++++++ .../Types/Encoders/JsonEncoderTests.cs | 317 ++-- .../Types/Encoders/JsonValidationData.cs | 82 +- .../Encoding/MessagesHelper.cs | 1 + 23 files changed, 3583 insertions(+), 678 deletions(-) create mode 100644 Tests/Opc.Ua.Core.Tests/Types/Encoders/JsonEncoderCompactTests.cs diff --git a/Applications/ConsoleReferenceClient/ClientSamples.cs b/Applications/ConsoleReferenceClient/ClientSamples.cs index e23bd8ada0..1f3a479355 100644 --- a/Applications/ConsoleReferenceClient/ClientSamples.cs +++ b/Applications/ConsoleReferenceClient/ClientSamples.cs @@ -967,7 +967,7 @@ Task FetchReferenceIdTypesAsync(ISession session) if (ServiceResult.IsNotBad(value.StatusCode)) { - var valueString = ClientSamples.FormatValueAsJson(uaClient.Session.MessageContext, variableId.ToString(), value, true); + var valueString = ClientSamples.FormatValueAsJson(uaClient.Session.MessageContext, variableId.ToString(), value, JsonEncodingType.Compact); m_output.WriteLine(valueString); } else @@ -992,7 +992,7 @@ Task FetchReferenceIdTypesAsync(ISession session) { if (ServiceResult.IsNotBad(errors[ii])) { - var valueString = ClientSamples.FormatValueAsJson(uaClient.Session.MessageContext, variableIds[ii].ToString(), value, true); + var valueString = ClientSamples.FormatValueAsJson(uaClient.Session.MessageContext, variableIds[ii].ToString(), value, JsonEncodingType.Compact); m_output.WriteLine(valueString); } else @@ -1102,15 +1102,15 @@ public async Task SubscribeAllValuesAsync( /// /// The key of the Json value. /// The DataValue. - /// Use reversible encoding. + /// Use reversible encoding. public static string FormatValueAsJson( IServiceMessageContext messageContext, string name, DataValue value, - bool jsonReversible) + JsonEncodingType jsonEncodingType) { string textbuffer; - using (var jsonEncoder = new JsonEncoder(messageContext, jsonReversible)) + using (var jsonEncoder = new JsonEncoder(messageContext, jsonEncodingType)) { jsonEncoder.WriteDataValue(name, value); textbuffer = jsonEncoder.CloseAndReturnText(); diff --git a/Libraries/Opc.Ua.PubSub/Encoding/JsonDataSetMessage.cs b/Libraries/Opc.Ua.PubSub/Encoding/JsonDataSetMessage.cs index 3531b2039b..0bbf418e55 100644 --- a/Libraries/Opc.Ua.PubSub/Encoding/JsonDataSetMessage.cs +++ b/Libraries/Opc.Ua.PubSub/Encoding/JsonDataSetMessage.cs @@ -465,6 +465,7 @@ private void EncodeField(IJsonEncoder encoder, Field field) valueToEncode = field.Value.StatusCode; } +#pragma warning disable CS0618 // Type or member is obsolete switch (m_fieldTypeEncoding) { case FieldTypeEncodingMask.Variant: @@ -519,6 +520,7 @@ private void EncodeField(IJsonEncoder encoder, Field field) encoder.UsingReversibleEncoding(encoder.WriteDataValue, fieldName, dataValue, false); break; } +#pragma warning restore CS0618 // Type or member is obsolete } #endregion diff --git a/Libraries/Opc.Ua.Server/Opc.Ua.Server.csproj b/Libraries/Opc.Ua.Server/Opc.Ua.Server.csproj index a2cc79d831..5eff9082e9 100644 --- a/Libraries/Opc.Ua.Server/Opc.Ua.Server.csproj +++ b/Libraries/Opc.Ua.Server/Opc.Ua.Server.csproj @@ -25,7 +25,7 @@ - + diff --git a/Stack/Opc.Ua.Core/Types/BuiltIn/ExpandedNodeId.cs b/Stack/Opc.Ua.Core/Types/BuiltIn/ExpandedNodeId.cs index 676ceb3522..097696701f 100644 --- a/Stack/Opc.Ua.Core/Types/BuiltIn/ExpandedNodeId.cs +++ b/Stack/Opc.Ua.Core/Types/BuiltIn/ExpandedNodeId.cs @@ -108,7 +108,7 @@ public ExpandedNodeId(NodeId nodeId, string namespaceUri) m_nodeId = new NodeId(nodeId); } - if (!String.IsNullOrEmpty(namespaceUri)) + if (!string.IsNullOrEmpty(namespaceUri)) { SetNamespaceUri(namespaceUri); } @@ -133,7 +133,7 @@ public ExpandedNodeId(NodeId nodeId, string namespaceUri, uint serverIndex) m_nodeId = new NodeId(nodeId); } - if (!String.IsNullOrEmpty(namespaceUri)) + if (!string.IsNullOrEmpty(namespaceUri)) { SetNamespaceUri(namespaceUri); } @@ -417,7 +417,7 @@ public bool IsNull { get { - if (!String.IsNullOrEmpty(m_namespaceUri)) + if (!string.IsNullOrEmpty(m_namespaceUri)) { return false; } @@ -441,7 +441,7 @@ public bool IsAbsolute { get { - if (!String.IsNullOrEmpty(m_namespaceUri) || m_serverIndex > 0) + if (!string.IsNullOrEmpty(m_namespaceUri) || m_serverIndex > 0) { return true; } @@ -565,31 +565,10 @@ public static void Format( buffer.AppendFormat(formatProvider, "svr={0};", serverIndex); } - if (!String.IsNullOrEmpty(namespaceUri)) + if (!string.IsNullOrEmpty(namespaceUri)) { buffer.Append("nsu="); - - for (int ii = 0; ii < namespaceUri.Length; ii++) - { - char ch = namespaceUri[ii]; - - switch (ch) - { - case ';': - case '%': - { - buffer.AppendFormat(formatProvider, "%{0:X2}", Convert.ToInt16(ch)); - break; - } - - default: - { - buffer.Append(ch); - break; - } - } - } - + buffer.Append(Utils.EscapeUri(namespaceUri)); buffer.Append(';'); } @@ -617,7 +596,7 @@ public static ExpandedNodeId Parse(string text, NamespaceTable currentNamespaces // translate the namespace uri. ushort namespaceIndex = 0; - if (!String.IsNullOrEmpty(uri)) + if (!string.IsNullOrEmpty(uri)) { int index = targetNamespaces.GetIndex(uri); @@ -662,7 +641,7 @@ public static ExpandedNodeId Parse(string text) try { // check for null. - if (String.IsNullOrEmpty(text)) + if (string.IsNullOrEmpty(text)) { return ExpandedNodeId.Null; } @@ -698,7 +677,7 @@ internal static void UnescapeUri(string text, int start, int index, StringBuilde ushort value = 0; - int digit = kHexDigits.IndexOf(Char.ToUpperInvariant(text[++ii])); + int digit = kHexDigits.IndexOf(char.ToUpperInvariant(text[++ii])); if (digit == -1) { @@ -708,7 +687,7 @@ internal static void UnescapeUri(string text, int start, int index, StringBuilde value += (ushort)digit; value <<= 4; - digit = kHexDigits.IndexOf(Char.ToUpperInvariant(text[++ii])); + digit = kHexDigits.IndexOf(char.ToUpperInvariant(text[++ii])); if (digit == -1) { @@ -791,7 +770,7 @@ public int CompareTo(object obj) { if (this.NamespaceUri != null) { - return String.CompareOrdinal(NamespaceUri, expandedId.NamespaceUri); + return string.CompareOrdinal(NamespaceUri, expandedId.NamespaceUri); } return -1; @@ -1004,7 +983,7 @@ public static NodeId ToNodeId(ExpandedNodeId nodeId, NamespaceTable namespaceTab } // return a reference to the internal node id object. - if (String.IsNullOrEmpty(nodeId.m_namespaceUri) && nodeId.m_serverIndex == 0) + if (string.IsNullOrEmpty(nodeId.m_namespaceUri) && nodeId.m_serverIndex == 0) { return nodeId.m_nodeId; } @@ -1060,6 +1039,153 @@ internal void SetServerIndex(uint serverIndex) #endregion #region Static Members + /// + /// Parses an ExpandedNodeId formatted as a string and converts it a local NodeId. + /// + /// The current context, + /// The text to parse. + /// The options to use when parsing the ExpandedNodeId. + /// The local identifier. + /// Thrown if the namespace URI is not in the namespace table. + public static ExpandedNodeId Parse(IServiceMessageContext context, string text, NodeIdParsingOptions options = null) + { + if (string.IsNullOrEmpty(text)) + { + return Null; + } + + var originalText = text; + int serverIndex = 0; + + if (text.StartsWith("svu=", StringComparison.Ordinal)) + { + int index = text.IndexOf(';', 4); + + if (index < 0) + { + throw new ServiceResultException(StatusCodes.BadNodeIdInvalid, $"Invalid ExpandedNodeId ({originalText})."); + } + + var serverUri = Utils.UnescapeUri(text.Substring(4, index - 4)); + serverIndex = (options?.UpdateTables == true) ? context.ServerUris.GetIndexOrAppend(serverUri) : context.ServerUris.GetIndex(serverUri); + + if (serverIndex < 0) + { + throw new ServiceResultException(StatusCodes.BadNodeIdInvalid, $"No mapping to ServerIndex for ServerUri ({serverUri})."); + } + + text = text.Substring(index + 1); + } + + if (text.StartsWith("svr=", StringComparison.Ordinal)) + { + int index = text.IndexOf(';', 4); + + if (index < 0) + { + throw new ServiceResultException(StatusCodes.BadNodeIdInvalid, $"Invalid ExpandedNodeId ({originalText})."); + } + + if (ushort.TryParse(text.Substring(4, index - 4), out ushort ns)) + { + serverIndex = ns; + + if (options.ServerMappings != null && options?.NamespaceMappings.Length < ns) + { + serverIndex = options.NamespaceMappings[ns]; + } + } + + text = text.Substring(index + 1); + } + + int namespaceIndex = 0; + string namespaceUri = null; + + if (text.StartsWith("nsu=", StringComparison.Ordinal)) + { + int index = text.IndexOf(';', 4); + + if (index < 0) + { + throw new ServiceResultException(StatusCodes.BadNodeIdInvalid, $"Invalid ExpandedNodeId ({originalText})."); + } + + namespaceUri = Utils.UnescapeUri(text.Substring(4, index - 4)); + namespaceIndex = (options?.UpdateTables == true) ? context.NamespaceUris.GetIndexOrAppend(namespaceUri) : context.NamespaceUris.GetIndex(namespaceUri); + + text = text.Substring(index + 1); + } + + var nodeId = NodeId.Parse(context, text, options); + + if (namespaceIndex > 0) + { + return new ExpandedNodeId( + nodeId.Identifier, + (ushort)namespaceIndex, + null, + (uint)serverIndex); + } + + return new ExpandedNodeId(nodeId, namespaceUri, (uint)serverIndex); + } + + /// + /// Formats a NodeId as a string. + /// + /// The current context. + /// The NamespaceUri and/or ServerUri is used instead of the indexes. + /// The formatted identifier. + public string Format(IServiceMessageContext context, bool useUris = false) + { + if (NodeId.IsNull(m_nodeId)) + { + return null; + } + + var buffer = new StringBuilder(); + + if (m_serverIndex > 0) + { + if (useUris) + { + var serverUri = context.ServerUris.GetString(m_serverIndex); + + if (!string.IsNullOrEmpty(serverUri)) + { + buffer.Append("svu="); + buffer.Append(Utils.EscapeUri(serverUri)); + buffer.Append(';'); + } + else + { + buffer.Append("svr="); + buffer.Append(m_serverIndex); + buffer.Append(';'); + } + } + else + { + buffer.Append("svr="); + buffer.Append(m_serverIndex); + buffer.Append(';'); + } + } + + if (!string.IsNullOrEmpty(m_namespaceUri)) + { + buffer.Append("nsu="); + buffer.Append(Utils.EscapeUri(m_namespaceUri)); + buffer.Append(';'); + } + + var id = m_nodeId.Format(context, useUris); + buffer.Append(id); + + return buffer.ToString(); + } + /// /// Parses an absolute NodeId formatted as a string and converts it a local NodeId. /// diff --git a/Stack/Opc.Ua.Core/Types/BuiltIn/NodeId.cs b/Stack/Opc.Ua.Core/Types/BuiltIn/NodeId.cs index 6866b5d71f..59d58b91b4 100644 --- a/Stack/Opc.Ua.Core/Types/BuiltIn/NodeId.cs +++ b/Stack/Opc.Ua.Core/Types/BuiltIn/NodeId.cs @@ -252,9 +252,9 @@ public NodeId(object value, ushort namespaceIndex) return; } - if (value is Uuid) + if (value is Uuid uuid) { - SetIdentifier(IdType.Guid, value); + SetIdentifier(IdType.Guid, (Guid)uuid); return; } @@ -288,6 +288,197 @@ private void Initialize() #endregion #region Static Members + /// + /// Parses an NodeId formatted as a string and converts it a NodeId. + /// + /// The current context. + /// The text to parse. + /// The options to use when parsing a NodeId. + /// The NodeId. + /// Thrown if the namespace URI is not in the namespace table. + public static NodeId Parse(IServiceMessageContext context, string text, NodeIdParsingOptions options = null) + { + if (String.IsNullOrEmpty(text)) + { + return Null; + } + + var originalText = text; + int namespaceIndex = 0; + + if (text.StartsWith("nsu=", StringComparison.Ordinal)) + { + int index = text.IndexOf(';', 4); + + if (index < 0) + { + throw new ServiceResultException(StatusCodes.BadNodeIdInvalid, $"Invalid NodeId ({originalText})."); + } + + var namespaceUri = Utils.UnescapeUri(text.Substring(4, index - 4)); + namespaceIndex = (options?.UpdateTables == true) ? context.NamespaceUris.GetIndexOrAppend(namespaceUri) : context.NamespaceUris.GetIndex(namespaceUri); + + if (namespaceIndex < 0) + { + throw new ServiceResultException(StatusCodes.BadNodeIdInvalid, $"No mapping to NamespaceIndex for NamespaceUri ({namespaceUri})."); + } + + text = text.Substring(index + 1); + } + + if (text.StartsWith("ns=", StringComparison.Ordinal)) + { + int index = text.IndexOf(';', 3); + + if (index < 0) + { + throw new ServiceResultException(StatusCodes.BadNodeIdInvalid, $"Invalid ExpandedNodeId ({originalText})."); + } + + if (UInt16.TryParse(text.Substring(3, index - 3), out ushort ns)) + { + namespaceIndex = ns; + + if (options?.NamespaceMappings != null && options?.NamespaceMappings.Length < ns) + { + namespaceIndex = options.NamespaceMappings[ns]; + } + } + + text = text.Substring(index + 1); + } + + var idType = text.Substring(0, 1); + text = text.Substring(2); + + switch (idType) + { + case "i": + { + if (UInt32.TryParse(text, out uint number)) + { + return new NodeId(number, (ushort)namespaceIndex); + } + + break; + } + + case "s": + { + if (!String.IsNullOrWhiteSpace(text)) + { + return new NodeId(text, (ushort)namespaceIndex); + } + + break; + } + + case "b": + { + try + { + var bytes = Convert.FromBase64String(text); + return new NodeId(bytes, (ushort)namespaceIndex); + } + catch (Exception) + { + // error handled after the switch statement. + } + + break; + } + + case "g": + { + if (Guid.TryParse(text, out var guid)) + { + return new NodeId(guid, (ushort)namespaceIndex); + } + + break; + } + } + + throw new ServiceResultException(StatusCodes.BadNodeIdInvalid, $"Invalid NodeId Identifier ({originalText})."); + } + + /// + /// Formats a NodeId as a string. + /// + /// The current context. + /// The NamespaceUri is used instead of the NamespaceIndex. + /// The formatted identifier. + public string Format(IServiceMessageContext context, bool useNamespaceUri = false) + { + if (m_identifier == null) + { + return null; + } + + var buffer = new StringBuilder(); + + if (m_namespaceIndex > 0) + { + if (useNamespaceUri) + { + var namespaceUri = context.NamespaceUris.GetString(m_namespaceIndex); + + if (!String.IsNullOrEmpty(namespaceUri)) + { + buffer.Append("nsu="); + buffer.Append(Utils.EscapeUri(namespaceUri)); + buffer.Append(';'); + } + else + { + buffer.Append("ns="); + buffer.Append(m_namespaceIndex); + buffer.Append(';'); + } + } + else + { + buffer.Append("ns="); + buffer.Append(m_namespaceIndex); + buffer.Append(';'); + } + } + + switch (m_identifierType) + { + case IdType.Numeric: + { + buffer.Append("i="); + buffer.Append((uint)m_identifier); + break; + } + + default: + case IdType.String: + { + buffer.Append("s="); + buffer.Append(m_identifier.ToString()); + break; + } + + case IdType.Guid: + { + buffer.Append("g="); + buffer.Append((Guid)m_identifier); + break; + } + + case IdType.Opaque: + { + buffer.Append("b="); + buffer.Append(Convert.ToBase64String((byte[])m_identifier)); + break; + } + } + + return buffer.ToString(); + } + /// /// Converts an identifier and a namespaceUri to a local NodeId using the namespaceTable. /// @@ -825,6 +1016,12 @@ internal void SetIdentifier(IdType idType, object value) break; } + case IdType.Guid: + { + m_identifier = (Guid)value; + break; + } + default: { m_identifier = value; @@ -929,7 +1126,7 @@ public int CompareTo(object obj) return -1; } - if (this.IsNullNodeId && (expandedId.InnerNodeId != null) && expandedId.InnerNodeId.IsNullNodeId) + if (this.IsNullNodeId && expandedId.InnerNodeId?.IsNullNodeId != false) { return 0; } @@ -1560,12 +1757,25 @@ private int CompareTo(IdType idType, object id) case IdType.Guid: { - Guid id1 = (Guid)m_identifier; - if (id is Uuid) + if (m_identifier is Guid id2) { - return id1.CompareTo((Uuid)id); + if (id is Uuid) + { + return id2.CompareTo((Uuid)id); + } + return id2.CompareTo((Guid)id); } - return id1.CompareTo((Guid)id); + + if (m_identifier is Uuid id1) + { + if (id is Uuid) + { + return id1.CompareTo(id); + } + return id1.CompareTo((Guid)id); + } + + return -1; } case IdType.Opaque: @@ -1573,7 +1783,7 @@ private int CompareTo(IdType idType, object id) byte[] id1 = (byte[])m_identifier; byte[] id2 = (byte[])id; - if (id1 == id2) + if (Utils.IsEqual(id1, id2)) { return 0; } @@ -1760,6 +1970,26 @@ public virtual object Clone() }//class #endregion + /// + /// Options that affect how a NodeId string is parsed. + /// + public class NodeIdParsingOptions + { + /// + /// If TRUE, the parser adds unknown URIs to the namespace or server table. + /// + public bool UpdateTables { get; set; } + + /// + /// The mapping from serialized namespace indexes to the indexes used in the context. + /// + public ushort[] NamespaceMappings { get; set; } + + /// + /// The mapping from serialized server indexes to the indexes used in the context. + /// + public ushort[] ServerMappings { get; set; } + } #region NodeIdComparer Class /// /// Helper which implements a NodeId IEqualityComparer for Linq queries. diff --git a/Stack/Opc.Ua.Core/Types/BuiltIn/QualifiedName.cs b/Stack/Opc.Ua.Core/Types/BuiltIn/QualifiedName.cs index e0a25e50f1..e5a819b09a 100644 --- a/Stack/Opc.Ua.Core/Types/BuiltIn/QualifiedName.cs +++ b/Stack/Opc.Ua.Core/Types/BuiltIn/QualifiedName.cs @@ -482,6 +482,110 @@ public static QualifiedName Parse(string text) return new QualifiedName(text.Substring(start), namespaceIndex); } + /// + /// Parses a string containing a QualifiedName with the syntax n:qname + /// + /// The QualifiedName value as a string. + /// The QualifiedName value as a string. + /// Whether the NamespaceTable should be updated with the NamespaceUri. + /// Thrown under a variety of circumstances, each time with a specific message. + public static QualifiedName Parse(IServiceMessageContext context, string text, bool updateTables) + { + // check for null. + if (String.IsNullOrEmpty(text)) + { + return QualifiedName.Null; + } + + var originalText = text; + int namespaceIndex = 0; + + if (text.StartsWith("nsu=", StringComparison.Ordinal)) + { + int index = text.IndexOf(';', 4); + + if (index < 0) + { + throw new ServiceResultException(StatusCodes.BadNodeIdInvalid, $"Invalid QualifiedName ({originalText})."); + } + + var namespaceUri = Utils.UnescapeUri(text.Substring(4, index-4)); + namespaceIndex = (updateTables) ? context.NamespaceUris.GetIndexOrAppend(namespaceUri) : context.NamespaceUris.GetIndex(namespaceUri); + + if (namespaceIndex < 0) + { + throw new ServiceResultException(StatusCodes.BadNodeIdInvalid, $"No mapping to NamespaceIndex for NamespaceUri ({namespaceUri})."); + } + + text = text.Substring(index + 1); + } + else + { + int index = text.IndexOf(':'); + + if (index > 0) + { + if (UInt16.TryParse(text.Substring(0, index), out ushort nsIndex)) + { + namespaceIndex = nsIndex; + } + else + { + throw new ServiceResultException(StatusCodes.BadNodeIdInvalid, $"Invalid QualifiedName ({originalText})."); + } + } + + text = text.Substring(index + 1); + } + + return new QualifiedName(text, (ushort)namespaceIndex); + } + + /// + /// Formats a QualifiedName as a string. + /// + /// The current context. + /// The NamespaceUri is used instead of the NamespaceIndex. + /// The formatted identifier. + public string Format(IServiceMessageContext context, bool useNamespaceUri = false) + { + if (String.IsNullOrEmpty(m_name)) + { + return null; + } + + var buffer = new StringBuilder(); + + if (m_namespaceIndex > 0) + { + if (useNamespaceUri) + { + var namespaceUri = context.NamespaceUris.GetString(m_namespaceIndex); + + if (!String.IsNullOrEmpty(namespaceUri)) + { + buffer.Append("nsu="); + buffer.Append(Utils.EscapeUri(namespaceUri)); + buffer.Append(';'); + } + else + { + buffer.Append(m_namespaceIndex); + buffer.Append(':'); + } + } + else + { + buffer.Append(m_namespaceIndex); + buffer.Append(':'); + } + } + + buffer.Append(m_name); + + return buffer.ToString(); + } + /// /// Returns true if the value is null. /// diff --git a/Stack/Opc.Ua.Core/Types/Encoders/IEncoder.cs b/Stack/Opc.Ua.Core/Types/Encoders/IEncoder.cs index 4f071ec983..11f8e860c2 100644 --- a/Stack/Opc.Ua.Core/Types/Encoders/IEncoder.cs +++ b/Stack/Opc.Ua.Core/Types/Encoders/IEncoder.cs @@ -28,8 +28,12 @@ public interface IEncoder : IDisposable EncodingType EncodingType { get; } /// - /// Selects the reversible encoding type. + /// If the encoder is configured to produce a reversible encoding. /// + /// + /// The BinaryEncoder and XmlEncoder in this library are reversible encoders. + /// For a JsonEncoder, reversability depends on the encoding type. + /// bool UseReversibleEncoding { get; } /// diff --git a/Stack/Opc.Ua.Core/Types/Encoders/IJsonEncoder.cs b/Stack/Opc.Ua.Core/Types/Encoders/IJsonEncoder.cs index f31cfb3e02..25104a1b69 100644 --- a/Stack/Opc.Ua.Core/Types/Encoders/IJsonEncoder.cs +++ b/Stack/Opc.Ua.Core/Types/Encoders/IJsonEncoder.cs @@ -1,4 +1,4 @@ -/* Copyright (c) 1996-2022 The OPC Foundation. All rights reserved. +/* Copyright (c) 1996-2024 The OPC Foundation. All rights reserved. The source code in this file is covered under a dual-license scenario: - RCL: for OPC Foundation Corporate Members in good-standing - GPL V2: everybody else @@ -19,6 +19,11 @@ namespace Opc.Ua /// public interface IJsonEncoder : IEncoder { + /// + /// The type of JSON encoding being used. + /// + JsonEncodingType EncodingToUse { get; } + /// /// Force the Json encoder to encode namespace URI instead of /// namespace Index in NodeIds. @@ -51,6 +56,39 @@ public interface IJsonEncoder : IEncoder /// Call an IEncoder action where the reversible encoding is applied /// before the call to the Action and restored before return. /// + [Obsolete("Non/Reversible encoding is deprecated. Use UsingAlternateEncoding instead to support new encoding types.")] void UsingReversibleEncoding(Action action, string fieldName, T value, bool useReversibleEncoding); + + /// + /// Call an IEncoder action where the alternate encoding type is applied + /// before the call to the Action and restored before return. + /// + void UsingAlternateEncoding(Action action, string fieldName, T value, JsonEncodingType useEncodingType); + } + + /// + /// The type of JSON encoding to use. + /// + public enum JsonEncodingType + { + /// + /// The compact encoding that may require a schema to interpret. + /// + Compact, + + /// + /// A verbose encoding that is more useable even without a schema. + /// + Verbose, + + /// + /// The reversible encoding supported for backward compatibitility. + /// + Reversible, + + /// + /// The non reversible encoding supported for backward compatibitility. + /// + NonReversible } } diff --git a/Stack/Opc.Ua.Core/Types/Encoders/JsonDecoder.cs b/Stack/Opc.Ua.Core/Types/Encoders/JsonDecoder.cs index 1f1e33816d..61a86f4397 100644 --- a/Stack/Opc.Ua.Core/Types/Encoders/JsonDecoder.cs +++ b/Stack/Opc.Ua.Core/Types/Encoders/JsonDecoder.cs @@ -31,6 +31,11 @@ public class JsonDecoder : IJsonDecoder /// The name of the Root array if the json is defined as an array /// public const string RootArrayName = "___root_array___"; + + /// + /// If TRUE then the NamespaceUris and ServerUris tables are updated with new URIs read from the JSON stream. + /// + public bool UpdateNamespaceTable { get; set; } #endregion #region Private Fields @@ -63,6 +68,7 @@ public JsonDecoder(string json, IServiceMessageContext context) { throw new ArgumentNullException(nameof(context)); } + Initialize(); m_context = context; @@ -200,14 +206,48 @@ public void SetMappingTables(NamespaceTable namespaceUris, StringTable serverUri if (namespaceUris != null && m_context.NamespaceUris != null) { - m_namespaceMappings = m_context.NamespaceUris.CreateMapping(namespaceUris, false); + ushort[] namespaceMappings = new ushort[namespaceUris.Count]; + + for (uint ii = 0; ii < namespaceUris.Count; ii++) + { + var uri = namespaceUris.GetString(ii); + + if (UpdateNamespaceTable) + { + namespaceMappings[ii] = m_context.NamespaceUris.GetIndexOrAppend(uri); + } + else + { + var index = m_context.NamespaceUris.GetIndex(namespaceUris.GetString(ii)); + namespaceMappings[ii] = (index >= 0) ? (UInt16)index : UInt16.MaxValue; + } + } + + m_namespaceMappings = namespaceMappings; } m_serverMappings = null; if (serverUris != null && m_context.ServerUris != null) { - m_serverMappings = m_context.ServerUris.CreateMapping(serverUris, false); + ushort[] serverMappings = new ushort[serverUris.Count]; + + for (uint ii = 0; ii < serverUris.Count; ii++) + { + var uri = serverUris.GetString(ii); + + if (UpdateNamespaceTable) + { + serverMappings[ii] = m_context.ServerUris.GetIndexOrAppend(uri); + } + else + { + var index = m_context.ServerUris.GetIndex(serverUris.GetString(ii)); + serverMappings[ii] = (index >= 0) ? (UInt16)index : UInt16.MaxValue; + } + } + + m_serverMappings = serverMappings; } } @@ -301,13 +341,12 @@ public bool ReadField(string fieldName, out object token) { token = null; - if (String.IsNullOrEmpty(fieldName)) + if (string.IsNullOrEmpty(fieldName)) { token = m_stack.Peek(); return true; } - if (!(m_stack.Peek() is Dictionary context) || !context.TryGetValue(fieldName, out token)) { return false; @@ -462,7 +501,7 @@ public int ReadInt32(string fieldName) if (value == null) { - return 0; + return ReadEnumeratedString(token, Int32.TryParse); } if (value < Int32.MinValue || value > Int32.MaxValue) @@ -489,14 +528,7 @@ public uint ReadUInt32(string fieldName) if (value == null) { - uint number = 0; - - if (!(token is string text) || !UInt32.TryParse(text, NumberStyles.Integer, CultureInfo.InvariantCulture, out number)) - { - return 0; - } - - return number; + return ReadEnumeratedString(token, UInt32.TryParse); } if (value < UInt32.MinValue || value > UInt32.MaxValue) @@ -559,9 +591,7 @@ public ulong ReadUInt64(string fieldName) { ulong number = 0; - if (!(token is string text) || !UInt64.TryParse(text, - NumberStyles.Integer, - CultureInfo.InvariantCulture, out number)) + if (!(token is string text) || !UInt64.TryParse(text, NumberStyles.Integer, CultureInfo.InvariantCulture, out number)) { return 0; } @@ -599,15 +629,15 @@ public float ReadFloat(string fieldName) { if (text != null) { - if (String.Equals(text, "Infinity", StringComparison.OrdinalIgnoreCase)) + if (string.Equals(text, "Infinity", StringComparison.OrdinalIgnoreCase)) { return Single.PositiveInfinity; } - else if (String.Equals(text, "-Infinity", StringComparison.OrdinalIgnoreCase)) + else if (string.Equals(text, "-Infinity", StringComparison.OrdinalIgnoreCase)) { return Single.NegativeInfinity; } - else if (String.Equals(text, "NaN", StringComparison.OrdinalIgnoreCase)) + else if (string.Equals(text, "NaN", StringComparison.OrdinalIgnoreCase)) { return Single.NaN; } @@ -657,15 +687,15 @@ public double ReadDouble(string fieldName) { if (text != null) { - if (String.Equals(text, "Infinity", StringComparison.OrdinalIgnoreCase)) + if (string.Equals(text, "Infinity", StringComparison.OrdinalIgnoreCase)) { return Double.PositiveInfinity; } - else if (String.Equals(text, "-Infinity", StringComparison.OrdinalIgnoreCase)) + else if (string.Equals(text, "-Infinity", StringComparison.OrdinalIgnoreCase)) { return Double.NegativeInfinity; } - else if (String.Equals(text, "NaN", StringComparison.OrdinalIgnoreCase)) + else if (string.Equals(text, "NaN", StringComparison.OrdinalIgnoreCase)) { return Double.NaN; } @@ -861,6 +891,19 @@ public NodeId ReadNodeId(string fieldName) return NodeId.Null; } + if (token is string text) + { + var nodeId = NodeId.Parse( + m_context, + text, + new NodeIdParsingOptions() { + UpdateTables = UpdateNamespaceTable, + NamespaceMappings = m_namespaceMappings, + ServerMappings = m_serverMappings + }); + + return nodeId; + } if (!(token is Dictionary value)) { @@ -889,14 +932,14 @@ public NodeId ReadNodeId(string fieldName) { if (namespaceToken is string namespaceUri) { - namespaceIndex = m_context.NamespaceUris.GetIndexOrAppend(namespaceUri); + namespaceIndex = ToNamespaceIndex(namespaceUri); } } else { if (index.Value >= 0 || index.Value < UInt16.MaxValue) { - namespaceIndex = (ushort)index.Value; + namespaceIndex = ToNamespaceIndex(index.Value); } } } @@ -947,6 +990,19 @@ public ExpandedNodeId ReadExpandedNodeId(string fieldName) return ExpandedNodeId.Null; } + if (token is string text) + { + var nodeId = ExpandedNodeId.Parse( + m_context, + text, + new NodeIdParsingOptions() { + UpdateTables = UpdateNamespaceTable, + NamespaceMappings = m_namespaceMappings, + ServerMappings = m_serverMappings + }); + + return nodeId; + } if (!(token is Dictionary value)) { @@ -981,14 +1037,38 @@ public ExpandedNodeId ReadExpandedNodeId(string fieldName) { if (index.Value >= 0 || index.Value < UInt16.MaxValue) { - namespaceIndex = (ushort)index.Value; + namespaceIndex = ToNamespaceIndex(index.Value); } } } - if (value.ContainsKey("ServerUri")) + object serverUriToken = null; + + if (ReadField("ServerUri", out serverUriToken)) { - serverIndex = ReadUInt32("ServerUri"); + var index = serverUriToken as long?; + + if (index == null) + { + serverIndex = ToServerIndex(serverUriToken as string); + } + else + { + if (index.Value >= 0 || index.Value < UInt32.MaxValue) + { + serverIndex = ToServerIndex(index.Value); + } + } + } + + if (namespaceUri != null) + { + namespaceIndex = ToNamespaceIndex(namespaceUri); + + if (UInt16.MaxValue != namespaceIndex) + { + namespaceUri = null; + } } if (value.ContainsKey("Id")) @@ -1032,13 +1112,20 @@ public ExpandedNodeId ReadExpandedNodeId(string fieldName) public StatusCode ReadStatusCode(string fieldName) { object token; + if (!ReadField(fieldName, out token)) { // the status code was not found return StatusCodes.Good; } + if (token is long code) + { + return (StatusCode)code; + } + bool wasPush = PushStructure(fieldName); + try { // try to read the non reversible Code @@ -1079,6 +1166,22 @@ public QualifiedName ReadQualifiedName(string fieldName) return QualifiedName.Null; } + if (token is string text) + { + var qn = QualifiedName.Parse(m_context, text, UpdateNamespaceTable); + + if (qn.NamespaceIndex != 0) + { + var ns = ToNamespaceIndex(qn.NamespaceIndex); + + if (ns != qn.NamespaceIndex) + { + qn = new QualifiedName(qn.Name, ns); + } + } + + return qn; + } if (!(token is Dictionary value)) { @@ -1104,17 +1207,16 @@ public QualifiedName ReadQualifiedName(string fieldName) if (index == null) { - // handle non reversible encoding if (namespaceToken is string namespaceUri) { - namespaceIndex = m_context.NamespaceUris.GetIndexOrAppend(namespaceUri); + namespaceIndex = ToNamespaceIndex(namespaceUri); } } else { if (index.Value >= 0 || index.Value < UInt16.MaxValue) { - namespaceIndex = (ushort)index.Value; + namespaceIndex = ToNamespaceIndex(index.Value); } } } @@ -1302,7 +1404,6 @@ public ExtensionObject ReadExtensionObject(string fieldName) return extension; } - if (!(token is Dictionary value)) { return extension; @@ -1348,7 +1449,7 @@ public ExtensionObject ReadExtensionObject(string fieldName) if (encoding == (byte)ExtensionObjectEncoding.Json) { var json = ReadString("Body"); - if (String.IsNullOrEmpty(json)) + if (string.IsNullOrEmpty(json)) { return extension; } @@ -1449,7 +1550,29 @@ public Enum ReadEnumerated(string fieldName, System.Type enumType) throw new ArgumentNullException(nameof(enumType)); } - return (Enum)Enum.ToObject(enumType, ReadInt32(fieldName)); + object token; + + if (!ReadField(fieldName, out token)) + { + return (Enum)Enum.ToObject(enumType, 0); + } + + if (token is long code) + { + return (Enum)Enum.ToObject(enumType, code); + } + + if (token is string text) + { + int index = text.LastIndexOf('_'); + + if (index > 0 && long.TryParse(text.Substring(index + 1), out code)) + { + return (Enum)Enum.ToObject(enumType, code); + } + } + + return (Enum)Enum.ToObject(enumType, 0); } /// @@ -2389,11 +2512,39 @@ public Array ReadArray( } else if (valueRank >= ValueRanks.TwoDimensions) { - List array; - if (!ReadArrayField(fieldName, out array)) + object token = null; + + if (!ReadField(fieldName, out token)) { return null; } + + if (token is Dictionary value) + { + m_stack.Push(value); + Int32Collection dimensions2 = null; + + if (value.ContainsKey("Dimensions")) + { + dimensions2 = ReadInt32Array("Dimensions"); + } + else + { + dimensions2 = new Int32Collection(valueRank); + } + + var array2 = ReadArray("Array", 1, builtInType, systemType, encodeableTypeId); + m_stack.Pop(); + + var matrix2 = new Matrix(array2, builtInType, dimensions2.ToArray()); + return matrix2.ToArray(); + } + + if (!(token is List array)) + { + return null; + } + List elements = new List(); List dimensions = new List(); if (builtInType == BuiltInType.Enumeration || builtInType == BuiltInType.Variant || builtInType == BuiltInType.Null) @@ -2605,6 +2756,109 @@ public void Pop() #endregion #region Private Methods + private ushort ToNamespaceIndex(string uri) + { + var index = m_context.NamespaceUris.GetIndex(uri); + + if (index < 0) + { + if (!UpdateNamespaceTable) + { + return UInt16.MaxValue; + } + else + { + index = m_context.NamespaceUris.GetIndexOrAppend(uri); + } + } + + return (ushort)index; + } + + private ushort ToNamespaceIndex(long index) + { + if (m_namespaceMappings == null || index <= 0) + { + return (ushort)index; + } + + if (index < 0 || index >= m_namespaceMappings.Length) + { + throw new ServiceResultException(StatusCodes.BadDecodingError, $"No mapping for NamespaceIndex={index}."); + } + + return m_namespaceMappings[index]; + } + + private ushort ToServerIndex(string uri) + { + var index = m_context.ServerUris.GetIndex(uri); + + if (index < 0) + { + if (!UpdateNamespaceTable) + { + return UInt16.MaxValue; + } + else + { + index = m_context.ServerUris.GetIndexOrAppend(uri); + } + } + + return (ushort)index; + } + + private ushort ToServerIndex(long index) + { + if (m_serverMappings == null || index <= 0) + { + return (ushort)index; + } + + if (index < 0 || index >= m_serverMappings.Length) + { + throw new ServiceResultException(StatusCodes.BadDecodingError, $"No mapping for ServerIndex(={index}."); + } + + return m_serverMappings[index]; + } + + /// + /// Helper to provide the TryParse method when reading an enumerated string. + /// + private delegate bool TryParseHandler(string s, NumberStyles numberStyles, CultureInfo cultureInfo, out T result); + + /// + /// Helper to read an enumerated string in an extension object. + /// + /// The number type which was encoded. + /// + /// + /// The parsed number or 0. + private T ReadEnumeratedString(object token, TryParseHandler handler) where T : struct + { + T number = default; + if (token is string text) + { + bool retry = false; + do + { + if (handler?.Invoke(text, NumberStyles.Integer, CultureInfo.InvariantCulture, out number) == false) + { + int lastIndex = text.LastIndexOf('_'); + if (lastIndex == -1) + { + text = text.Substring(lastIndex + 1); + retry = true; + } + } + } while (retry); + } + + return number; + } + /// /// Reads a DiagnosticInfo from the stream. /// Limits the InnerDiagnosticInfos to the specified depth. diff --git a/Stack/Opc.Ua.Core/Types/Encoders/JsonEncoder.cs b/Stack/Opc.Ua.Core/Types/Encoders/JsonEncoder.cs index 43ca801ff9..f2ff438bb1 100644 --- a/Stack/Opc.Ua.Core/Types/Encoders/JsonEncoder.cs +++ b/Stack/Opc.Ua.Core/Types/Encoders/JsonEncoder.cs @@ -52,6 +52,12 @@ public class JsonEncoder : IJsonEncoder private bool m_levelOneSkipped; private bool m_dontWriteClosing; private bool m_leaveOpen; + private bool m_forceNamespaceUri; + private bool m_forceNamespaceUriForIndex1; + private bool m_includeDefaultNumberValues; + private bool m_includeDefaultValues; + private bool m_encodeNodeIdAsString; + [Flags] private enum EscapeOptions : int @@ -65,35 +71,60 @@ private enum EscapeOptions : int #region Constructors /// - /// + /// Initializes the object with default values. + /// Selects the reversible or non reversible encoding. /// - /// - /// public JsonEncoder( IServiceMessageContext context, bool useReversibleEncoding) : - this(context, useReversibleEncoding, false, null, false) + this(context, useReversibleEncoding ? JsonEncodingType.Reversible : JsonEncodingType.NonReversible, false, null, false) + { + } + + /// + /// Initializes the object with default values. + /// Selects the reversible or non reversible encoding. + /// + public JsonEncoder( + IServiceMessageContext context, + bool useReversibleEncoding, + bool topLevelIsArray = false, + Stream stream = null, + bool leaveOpen = false, + int streamSize = kStreamWriterBufferSize) : + this(context, useReversibleEncoding ? JsonEncodingType.Reversible : JsonEncodingType.NonReversible, topLevelIsArray, stream, leaveOpen, streamSize) { } /// /// Initializes the object with default values. + /// Selects the reversible or non reversible encoding. /// public JsonEncoder( IServiceMessageContext context, bool useReversibleEncoding, + StreamWriter streamWriter, + bool topLevelIsArray = false) : + this(context, useReversibleEncoding ? JsonEncodingType.Reversible : JsonEncodingType.NonReversible, streamWriter, topLevelIsArray) + { + } + + /// + /// Initializes the object with default values. + /// + public JsonEncoder( + IServiceMessageContext context, + JsonEncodingType encoding, bool topLevelIsArray = false, Stream stream = null, bool leaveOpen = false, - int streamSize = kStreamWriterBufferSize - ) + int streamSize = kStreamWriterBufferSize) { - Initialize(); + Initialize(encoding); m_context = context; m_stream = stream; m_leaveOpen = leaveOpen; - UseReversibleEncoding = useReversibleEncoding; m_topLevelIsArray = topLevelIsArray; if (m_stream == null) @@ -115,15 +146,14 @@ public JsonEncoder( /// public JsonEncoder( IServiceMessageContext context, - bool useReversibleEncoding, + JsonEncodingType encoding, StreamWriter writer, bool topLevelIsArray = false) { - Initialize(); + Initialize(encoding); m_context = context; m_writer = writer; - UseReversibleEncoding = useReversibleEncoding; m_topLevelIsArray = topLevelIsArray; if (m_writer == null) @@ -138,7 +168,7 @@ public JsonEncoder( /// /// Sets private members to default values. /// - private void Initialize() + private void Initialize(JsonEncodingType encoding) { m_stream = null; m_writer = null; @@ -149,13 +179,29 @@ private void Initialize() m_levelOneSkipped = false; // defaults for JSON encoding - // -- encode namespace index for reversible encoding - // -- do not include default values for built in types - // which are not a Number or a bool - // -- include default values for numbers and bool - ForceNamespaceUri = false; - IncludeDefaultValues = false; - IncludeDefaultNumberValues = true; + EncodingToUse = encoding; + if (encoding == JsonEncodingType.Reversible || encoding == JsonEncodingType.NonReversible) + { + // defaults for reversible and non reversible JSON encoding + // -- encode namespace index for reversible encoding / uri for non reversible + // -- do not include default values for reversible encoding + // -- include default values for non reversible encoding + m_forceNamespaceUri = + m_forceNamespaceUriForIndex1 = + m_includeDefaultValues = encoding == JsonEncodingType.NonReversible; + m_includeDefaultNumberValues = true; + m_encodeNodeIdAsString = false; + } + else + { + // defaults for compact and verbose JSON encoding, properties throw exception if modified + m_forceNamespaceUri = true; + m_forceNamespaceUriForIndex1 = true; + m_includeDefaultValues = encoding == JsonEncodingType.Verbose; + m_includeDefaultNumberValues = encoding == JsonEncodingType.Verbose; + m_encodeNodeIdAsString = true; + } + m_inVariantWithEncoding = IncludeDefaultValues; } /// @@ -346,6 +392,9 @@ protected virtual void Dispose(bool disposing) #endregion #region IJsonEncodeable Members + /// + public JsonEncodingType EncodingToUse { get; private set; } + /// public void PushStructure(string fieldName) { @@ -431,58 +480,95 @@ public void PopArray() } /// + [Obsolete("Non/Reversible encoding is deprecated. Use UsingAlternateEncoding instead to support new encoding types.")] public void UsingReversibleEncoding(Action action, string fieldName, T value, bool useReversibleEncoding) { - bool currentValue = UseReversibleEncoding; + JsonEncodingType currentValue = EncodingToUse; + try + { + EncodingToUse = useReversibleEncoding ? JsonEncodingType.Reversible : JsonEncodingType.NonReversible; + action(fieldName, value); + } + finally + { + EncodingToUse = currentValue; + } + } + + /// + public void UsingAlternateEncoding(Action action, string fieldName, T value, JsonEncodingType useEncoding) + { + JsonEncodingType currentValue = EncodingToUse; try { - UseReversibleEncoding = useReversibleEncoding; + EncodingToUse = useEncoding; action(fieldName, value); } finally { - UseReversibleEncoding = currentValue; + EncodingToUse = currentValue; } } #endregion #region IEncoder Members - /// - /// The type of encoding being used. - /// + /// public EncodingType EncodingType => EncodingType.Json; + /// + public bool UseReversibleEncoding => EncodingToUse != JsonEncodingType.NonReversible; + /// /// The message context associated with the encoder. /// public IServiceMessageContext Context => m_context; - /// - /// The Json encoder reversible encoding option - /// - public bool UseReversibleEncoding { get; private set; } - /// /// The Json encoder to encoder namespace URI instead of /// namespace Index in NodeIds. /// - public bool ForceNamespaceUri { get; set; } + public bool ForceNamespaceUri + { + get => m_forceNamespaceUri; + set => m_forceNamespaceUri = ThrowIfCompactOrVerbose(value); + } /// /// The Json encoder to encode namespace URI for all /// namespaces /// - public bool ForceNamespaceUriForIndex1 { get; set; } + public bool ForceNamespaceUriForIndex1 + { + get => m_forceNamespaceUriForIndex1; + set => m_forceNamespaceUriForIndex1 = ThrowIfCompactOrVerbose(value); + } /// /// The Json encoder default value option. /// - public bool IncludeDefaultValues { get; set; } + public bool IncludeDefaultValues + { + get => m_includeDefaultValues; + set => m_includeDefaultValues = ThrowIfCompactOrVerbose(value); + } /// /// The Json encoder default value option. /// - public bool IncludeDefaultNumberValues { get; set; } + public bool IncludeDefaultNumberValues + { + get => m_includeDefaultNumberValues; + set => m_includeDefaultNumberValues = ThrowIfCompactOrVerbose(value); + } + + /// + /// The Json encoder default encoding for NodeId as string or object. + /// + public bool EncodeNodeIdAsString + { + get => m_encodeNodeIdAsString; + set => m_encodeNodeIdAsString = ThrowIfCompactOrVerbose(value); + } /// /// Pushes a namespace onto the namespace stack. @@ -1073,7 +1159,6 @@ private void WriteNamespaceIndex(string fieldName, ushort namespaceIndex) } if ((!UseReversibleEncoding || ForceNamespaceUri) && namespaceIndex > (ForceNamespaceUriForIndex1 ? 0 : 1)) - { var uri = m_context.NamespaceUris.GetString(namespaceIndex); if (!string.IsNullOrEmpty(uri)) @@ -1157,13 +1242,18 @@ private void WriteNodeIdContents(NodeId value, string namespaceUri = null) /// public void WriteNodeId(string fieldName, NodeId value) { - if (value == null || - (NodeId.IsNull(value) && (value.IdType == IdType.Numeric))) + if (value == null || (NodeId.IsNull(value) && (value.IdType == IdType.Numeric))) { WriteSimpleFieldNull(fieldName); return; } + if (m_encodeNodeIdAsString) + { + WriteSimpleField(fieldName, value.Format(m_context, ForceNamespaceUri), EscapeOptions.Quotes); + return; + } + PushStructure(fieldName); ushort namespaceIndex = value.NamespaceIndex; @@ -1184,13 +1274,18 @@ public void WriteNodeId(string fieldName, NodeId value) /// public void WriteExpandedNodeId(string fieldName, ExpandedNodeId value) { - if (value == null || value.InnerNodeId == null || - (!UseReversibleEncoding && NodeId.IsNull(value))) + if (value == null || value.InnerNodeId == null || (!UseReversibleEncoding && NodeId.IsNull(value))) { WriteSimpleFieldNull(fieldName); return; } + if (m_encodeNodeIdAsString) + { + WriteSimpleField(fieldName, value.Format(m_context, ForceNamespaceUri), EscapeOptions.Quotes); + return; + } + PushStructure(fieldName); string namespaceUri = value.NamespaceUri; @@ -1205,12 +1300,16 @@ public void WriteExpandedNodeId(string fieldName, ExpandedNodeId value) if (serverIndex >= 1) { - var uri = m_context.ServerUris.GetString(serverIndex); - - if (!string.IsNullOrEmpty(uri)) + if (EncodingToUse == JsonEncodingType.NonReversible) { - WriteSimpleField("ServerUri", uri, EscapeOptions.Quotes | EscapeOptions.NoFieldNameEscape); - PopStructure(); + var uri = m_context.ServerUris.GetString(serverIndex); + + if (!string.IsNullOrEmpty(uri)) + { + WriteSimpleField("ServerUri", uri, EscapeOptions.Quotes | EscapeOptions.NoFieldNameEscape); + PopStructure(); + } + return; } @@ -1240,7 +1339,7 @@ public void WriteStatusCode(string fieldName, StatusCode value) return; } - if (UseReversibleEncoding) + if (EncodingToUse == JsonEncodingType.Reversible || EncodingToUse == JsonEncodingType.Compact) { WriteUInt32(fieldName, value.Code); return; @@ -1250,7 +1349,11 @@ public void WriteStatusCode(string fieldName, StatusCode value) { PushStructure(fieldName); WriteSimpleField("Code", value.Code.ToString(CultureInfo.InvariantCulture), EscapeOptions.NoFieldNameEscape); - WriteSimpleField("Symbol", StatusCode.LookupSymbolicId(value.CodeBits), EscapeOptions.Quotes | EscapeOptions.NoFieldNameEscape); + string symbolicId = StatusCode.LookupSymbolicId(value.CodeBits); + if (!string.IsNullOrEmpty(symbolicId)) + { + WriteSimpleField("Symbol", symbolicId, EscapeOptions.Quotes | EscapeOptions.NoFieldNameEscape); + } PopStructure(); } } @@ -1274,10 +1377,15 @@ public void WriteQualifiedName(string fieldName, QualifiedName value) return; } + if (m_encodeNodeIdAsString) + { + WriteSimpleField(fieldName, value.Format(m_context, ForceNamespaceUri), EscapeOptions.Quotes); + return; + } + PushStructure(fieldName); WriteString("Name", value.Name); - WriteNamespaceIndex("Uri", value.NamespaceIndex); PopStructure(); @@ -1294,7 +1402,7 @@ public void WriteLocalizedText(string fieldName, LocalizedText value) return; } - if (UseReversibleEncoding) + if (EncodingToUse != JsonEncodingType.NonReversible) { PushStructure(fieldName); @@ -1328,14 +1436,14 @@ public void WriteVariant(string fieldName, Variant value) try { - bool isNull = (value.TypeInfo == null || value.TypeInfo.BuiltInType == BuiltInType.Null || value.Value == null); - if (UseReversibleEncoding && !isNull) + if (!isNull && EncodingToUse != JsonEncodingType.NonReversible) { PushStructure(fieldName); // encode enums as int32. byte encodingByte = (byte)value.TypeInfo.BuiltInType; + if (value.TypeInfo.BuiltInType == BuiltInType.Enumeration) { encodingByte = (byte)BuiltInType.Int32; @@ -1359,7 +1467,7 @@ public void WriteVariant(string fieldName, Variant value) WriteVariantContents(value.Value, value.TypeInfo); - if (UseReversibleEncoding && !isNull) + if (!isNull && EncodingToUse != JsonEncodingType.NonReversible) { if (value.Value is Matrix matrix) { @@ -1437,9 +1545,10 @@ public void WriteExtensionObject(string fieldName, ExtensionObject value) var encodeable = value.Body as IEncodeable; - if (!UseReversibleEncoding && encodeable != null) + if (encodeable != null && EncodingToUse == JsonEncodingType.NonReversible) { // non reversible encoding, only the content of the Body field is encoded + // TODO: value.Body is Union? if (value.Body is IStructureTypeInfo structureType && structureType.StructureType == StructureType.Union) { @@ -1450,6 +1559,7 @@ public void WriteExtensionObject(string fieldName, ExtensionObject value) PushStructure(fieldName); encodeable.Encode(this); PopStructure(); + return; } @@ -1468,15 +1578,7 @@ public void WriteExtensionObject(string fieldName, ExtensionObject value) } var localTypeId = ExpandedNodeId.ToNodeId(typeId, Context.NamespaceUris); - - if (UseReversibleEncoding) - { - WriteNodeId("TypeId", localTypeId); - } - else - { - WriteExpandedNodeId("TypeId", typeId); - } + WriteNodeId("TypeId", localTypeId); if (encodeable != null) { @@ -1565,7 +1667,9 @@ public void WriteEnumerated(string fieldName, Enum value) { int numeric = Convert.ToInt32(value, CultureInfo.InvariantCulture); var numericString = numeric.ToString(CultureInfo.InvariantCulture); - if (UseReversibleEncoding) + + if (EncodingToUse == JsonEncodingType.Reversible || + EncodingToUse == JsonEncodingType.Compact) { WriteSimpleField(fieldName, numericString); } @@ -1578,7 +1682,7 @@ public void WriteEnumerated(string fieldName, Enum value) } else { - WriteSimpleField(fieldName, Utils.Format("{0}_{1}", value.ToString(), numeric), EscapeOptions.Quotes); + WriteSimpleField(fieldName, Utils.Format("{0}_{1}", valueString, numeric), EscapeOptions.Quotes); } } } @@ -1588,8 +1692,9 @@ public void WriteEnumerated(string fieldName, Enum value) /// public void WriteEnumerated(string fieldName, int numeric) { + bool writeNumber = EncodingToUse == JsonEncodingType.Reversible || EncodingToUse == JsonEncodingType.Compact; var numericString = numeric.ToString(CultureInfo.InvariantCulture); - WriteSimpleField(fieldName, numericString, !UseReversibleEncoding ? EscapeOptions.Quotes : EscapeOptions.None); + WriteSimpleField(fieldName, numericString, writeNumber ? EscapeOptions.None : EscapeOptions.Quotes); } /// @@ -1597,9 +1702,8 @@ public void WriteEnumerated(string fieldName, int numeric) /// public void WriteBooleanArray(string fieldName, IList values) { - if (values == null || (values.Count == 0 && !m_inVariantWithEncoding)) + if (CheckForSimpleFieldNull(fieldName, values)) { - WriteSimpleFieldNull(fieldName); return; } @@ -1624,9 +1728,8 @@ public void WriteBooleanArray(string fieldName, IList values) /// public void WriteSByteArray(string fieldName, IList values) { - if (values == null || (values.Count == 0 && !m_inVariantWithEncoding)) + if (CheckForSimpleFieldNull(fieldName, values)) { - WriteSimpleFieldNull(fieldName); return; } @@ -1651,9 +1754,8 @@ public void WriteSByteArray(string fieldName, IList values) /// public void WriteByteArray(string fieldName, IList values) { - if (values == null || (values.Count == 0 && !m_inVariantWithEncoding)) + if (CheckForSimpleFieldNull(fieldName, values)) { - WriteSimpleFieldNull(fieldName); return; } @@ -1678,9 +1780,8 @@ public void WriteByteArray(string fieldName, IList values) /// public void WriteInt16Array(string fieldName, IList values) { - if (values == null || (values.Count == 0 && !m_inVariantWithEncoding)) + if (CheckForSimpleFieldNull(fieldName, values)) { - WriteSimpleFieldNull(fieldName); return; } @@ -1705,9 +1806,8 @@ public void WriteInt16Array(string fieldName, IList values) /// public void WriteUInt16Array(string fieldName, IList values) { - if (values == null || (values.Count == 0 && !m_inVariantWithEncoding)) + if (CheckForSimpleFieldNull(fieldName, values)) { - WriteSimpleFieldNull(fieldName); return; } @@ -1732,9 +1832,8 @@ public void WriteUInt16Array(string fieldName, IList values) /// public void WriteInt32Array(string fieldName, IList values) { - if (values == null || (values.Count == 0 && !m_inVariantWithEncoding)) + if (CheckForSimpleFieldNull(fieldName, values)) { - WriteSimpleFieldNull(fieldName); return; } @@ -1759,9 +1858,8 @@ public void WriteInt32Array(string fieldName, IList values) /// public void WriteUInt32Array(string fieldName, IList values) { - if (values == null || (values.Count == 0 && !m_inVariantWithEncoding)) + if (CheckForSimpleFieldNull(fieldName, values)) { - WriteSimpleFieldNull(fieldName); return; } @@ -1786,9 +1884,8 @@ public void WriteUInt32Array(string fieldName, IList values) /// public void WriteInt64Array(string fieldName, IList values) { - if (values == null || (values.Count == 0 && !m_inVariantWithEncoding)) + if (CheckForSimpleFieldNull(fieldName, values)) { - WriteSimpleFieldNull(fieldName); return; } @@ -1813,9 +1910,8 @@ public void WriteInt64Array(string fieldName, IList values) /// public void WriteUInt64Array(string fieldName, IList values) { - if (values == null || (values.Count == 0 && !m_inVariantWithEncoding)) + if (CheckForSimpleFieldNull(fieldName, values)) { - WriteSimpleFieldNull(fieldName); return; } @@ -1840,9 +1936,8 @@ public void WriteUInt64Array(string fieldName, IList values) /// public void WriteFloatArray(string fieldName, IList values) { - if (values == null || (values.Count == 0 && !m_inVariantWithEncoding)) + if (CheckForSimpleFieldNull(fieldName, values)) { - WriteSimpleFieldNull(fieldName); return; } @@ -1867,9 +1962,8 @@ public void WriteFloatArray(string fieldName, IList values) /// public void WriteDoubleArray(string fieldName, IList values) { - if (values == null || (values.Count == 0 && !m_inVariantWithEncoding)) + if (CheckForSimpleFieldNull(fieldName, values)) { - WriteSimpleFieldNull(fieldName); return; } @@ -1894,9 +1988,8 @@ public void WriteDoubleArray(string fieldName, IList values) /// public void WriteStringArray(string fieldName, IList values) { - if (values == null || (values.Count == 0 && !m_inVariantWithEncoding)) + if (CheckForSimpleFieldNull(fieldName, values)) { - WriteSimpleFieldNull(fieldName); return; } @@ -1921,9 +2014,8 @@ public void WriteStringArray(string fieldName, IList values) /// public void WriteDateTimeArray(string fieldName, IList values) { - if (values == null || (values.Count == 0 && !m_inVariantWithEncoding)) + if (CheckForSimpleFieldNull(fieldName, values)) { - WriteSimpleFieldNull(fieldName); return; } @@ -1955,9 +2047,8 @@ public void WriteDateTimeArray(string fieldName, IList values) /// public void WriteGuidArray(string fieldName, IList values) { - if (values == null || (values.Count == 0 && !m_inVariantWithEncoding)) + if (CheckForSimpleFieldNull(fieldName, values)) { - WriteSimpleFieldNull(fieldName); return; } @@ -1982,9 +2073,8 @@ public void WriteGuidArray(string fieldName, IList values) /// public void WriteGuidArray(string fieldName, IList values) { - if (values == null || (values.Count == 0 && !m_inVariantWithEncoding)) + if (CheckForSimpleFieldNull(fieldName, values)) { - WriteSimpleFieldNull(fieldName); return; } @@ -2009,9 +2099,8 @@ public void WriteGuidArray(string fieldName, IList values) /// public void WriteByteStringArray(string fieldName, IList values) { - if (values == null || (values.Count == 0 && !m_inVariantWithEncoding)) + if (CheckForSimpleFieldNull(fieldName, values)) { - WriteSimpleFieldNull(fieldName); return; } @@ -2036,9 +2125,8 @@ public void WriteByteStringArray(string fieldName, IList values) /// public void WriteXmlElementArray(string fieldName, IList values) { - if (values == null || (values.Count == 0 && !m_inVariantWithEncoding)) + if (CheckForSimpleFieldNull(fieldName, values)) { - WriteSimpleFieldNull(fieldName); return; } @@ -2063,9 +2151,8 @@ public void WriteXmlElementArray(string fieldName, IList values) /// public void WriteNodeIdArray(string fieldName, IList values) { - if (values == null || (values.Count == 0 && !m_inVariantWithEncoding)) + if (CheckForSimpleFieldNull(fieldName, values)) { - WriteSimpleFieldNull(fieldName); return; } @@ -2090,9 +2177,8 @@ public void WriteNodeIdArray(string fieldName, IList values) /// public void WriteExpandedNodeIdArray(string fieldName, IList values) { - if (values == null || (values.Count == 0 && !m_inVariantWithEncoding)) + if (CheckForSimpleFieldNull(fieldName, values)) { - WriteSimpleFieldNull(fieldName); return; } @@ -2117,9 +2203,8 @@ public void WriteExpandedNodeIdArray(string fieldName, IList val /// public void WriteStatusCodeArray(string fieldName, IList values) { - if (values == null || (values.Count == 0 && !m_inVariantWithEncoding)) + if (CheckForSimpleFieldNull(fieldName, values)) { - WriteSimpleFieldNull(fieldName); return; } @@ -2152,9 +2237,8 @@ public void WriteStatusCodeArray(string fieldName, IList values) /// public void WriteDiagnosticInfoArray(string fieldName, IList values) { - if (values == null || (values.Count == 0 && !m_inVariantWithEncoding)) + if (CheckForSimpleFieldNull(fieldName, values)) { - WriteSimpleFieldNull(fieldName); return; } @@ -2179,9 +2263,8 @@ public void WriteDiagnosticInfoArray(string fieldName, IList val /// public void WriteQualifiedNameArray(string fieldName, IList values) { - if (values == null || (values.Count == 0 && !m_inVariantWithEncoding)) + if (CheckForSimpleFieldNull(fieldName, values)) { - WriteSimpleFieldNull(fieldName); return; } @@ -2206,9 +2289,8 @@ public void WriteQualifiedNameArray(string fieldName, IList value /// public void WriteLocalizedTextArray(string fieldName, IList values) { - if (values == null || (values.Count == 0 && !m_inVariantWithEncoding)) + if (CheckForSimpleFieldNull(fieldName, values)) { - WriteSimpleFieldNull(fieldName); return; } @@ -2233,9 +2315,8 @@ public void WriteLocalizedTextArray(string fieldName, IList value /// public void WriteVariantArray(string fieldName, IList values) { - if (values == null || (values.Count == 0 && !m_inVariantWithEncoding)) + if (CheckForSimpleFieldNull(fieldName, values)) { - WriteSimpleFieldNull(fieldName); return; } @@ -2266,9 +2347,8 @@ public void WriteVariantArray(string fieldName, IList values) /// public void WriteDataValueArray(string fieldName, IList values) { - if (values == null || (values.Count == 0 && !m_inVariantWithEncoding)) + if (CheckForSimpleFieldNull(fieldName, values)) { - WriteSimpleFieldNull(fieldName); return; } @@ -2293,9 +2373,8 @@ public void WriteDataValueArray(string fieldName, IList values) /// public void WriteExtensionObjectArray(string fieldName, IList values) { - if (values == null || (values.Count == 0 && !m_inVariantWithEncoding)) + if (CheckForSimpleFieldNull(fieldName, values)) { - WriteSimpleFieldNull(fieldName); return; } @@ -2320,9 +2399,8 @@ public void WriteExtensionObjectArray(string fieldName, IList v /// public void WriteEncodeableArray(string fieldName, IList values, System.Type systemType) { - if (values == null || (values.Count == 0 && !m_inVariantWithEncoding)) + if (CheckForSimpleFieldNull(fieldName, values)) { - WriteSimpleFieldNull(fieldName); return; } @@ -2423,9 +2501,10 @@ public void WriteEnumeratedArray(string fieldName, Array values, System.Type sys /// public void WriteVariantContents(object value, TypeInfo typeInfo) { + bool inVariantWithEncoding = m_inVariantWithEncoding; try { - m_inVariantWithEncoding = UseReversibleEncoding; + m_inVariantWithEncoding = true; // check for null. if (value == null) @@ -2471,7 +2550,7 @@ public void WriteVariantContents(object value, TypeInfo typeInfo) else if (typeInfo.ValueRank >= ValueRanks.OneDimension) { int valueRank = typeInfo.ValueRank; - if (UseReversibleEncoding && value is Matrix matrix) + if (EncodingToUse != JsonEncodingType.NonReversible && value is Matrix matrix) { // linearize the matrix value = matrix.Elements; @@ -2482,7 +2561,7 @@ public void WriteVariantContents(object value, TypeInfo typeInfo) } finally { - m_inVariantWithEncoding = false; + m_inVariantWithEncoding = inVariantWithEncoding; } } @@ -2491,9 +2570,8 @@ public void WriteVariantContents(object value, TypeInfo typeInfo) /// public void WriteObjectArray(string fieldName, IList values) { - if (values == null || (values.Count == 0 && !m_inVariantWithEncoding)) + if (CheckForSimpleFieldNull(fieldName, values)) { - WriteSimpleFieldNull(fieldName); return; } @@ -2607,6 +2685,7 @@ public void WriteArray(string fieldName, object array, int valueRank, BuiltInTyp } } } + // write matrix. else if (valueRank > ValueRanks.OneDimension) { @@ -2627,8 +2706,15 @@ public void WriteArray(string fieldName, object array, int valueRank, BuiltInTyp if (matrix != null) { - int index = 0; - WriteStructureMatrix(fieldName, matrix, 0, ref index, matrix.TypeInfo); + if (EncodingToUse == JsonEncodingType.Compact || EncodingToUse == JsonEncodingType.Verbose) + { + WriteArrayDimensionMatrix(fieldName, builtInType, matrix); + } + else + { + int index = 0; + WriteStructureMatrix(fieldName, matrix, 0, ref index, matrix.TypeInfo); + } return; } @@ -2638,6 +2724,33 @@ public void WriteArray(string fieldName, object array, int valueRank, BuiltInTyp #endregion #region Private Methods + /// + /// Returns true if a simple field can be written. + /// + private bool CheckForSimpleFieldNull(string fieldName, IList values) + { + // always include default values for non reversible/verbose + // include default values when encoding in a Variant + if (values == null || (values.Count == 0 && !m_inVariantWithEncoding && !m_includeDefaultValues)) + { + WriteSimpleFieldNull(fieldName); + return true; + } + return false; + } + + /// + /// Called on properties which can only be modified for the deprecated encoding. + /// + private bool ThrowIfCompactOrVerbose(bool value) + { + if (EncodingToUse == JsonEncodingType.Compact || EncodingToUse == JsonEncodingType.Verbose) + { + throw new NotSupportedException($"This property can not be modified with {EncodingToUse} encoding."); + } + return value; + } + /// /// Completes writing and returns the text length. /// @@ -2735,6 +2848,28 @@ private void WriteDiagnosticInfo(string fieldName, DiagnosticInfo value, int dep } } + /// + /// Encode the Matrix as Dimensions/Array element. + /// Writes the matrix as a flattended array with dimensions. + /// Validates the dimensions and array size. + /// + private void WriteArrayDimensionMatrix(string fieldName, BuiltInType builtInType, Matrix matrix) + { + // check if matrix is well formed + (bool valid, int sizeFromDimensions) = Matrix.ValidateDimensions(true, matrix.Dimensions, Context.MaxArrayLength); + + if (!valid || (sizeFromDimensions != matrix.Elements.Length)) + { + throw ServiceResultException.Create(StatusCodes.BadEncodingError, + "The number of elements in the matrix does not match the dimensions."); + } + + PushStructure(fieldName); + WriteInt32Array("Dimensions", matrix.Dimensions); + WriteArray("Array", matrix.Elements, 1, builtInType); + PopStructure(); + } + /// /// Write multi dimensional array in structure. /// @@ -2750,7 +2885,8 @@ private void WriteStructureMatrix( if (!valid || (sizeFromDimensions != matrix.Elements.Length)) { - throw new ArgumentException("The number of elements in the matrix does not match the dimensions."); + throw ServiceResultException.Create(StatusCodes.BadEncodingError, + "The number of elements in the matrix does not match the dimensions."); } CheckAndIncrementNestingLevel(); diff --git a/Stack/Opc.Ua.Core/Types/Utils/DataGenerator.cs b/Stack/Opc.Ua.Core/Types/Utils/DataGenerator.cs index 55d96be412..a064d00872 100644 --- a/Stack/Opc.Ua.Core/Types/Utils/DataGenerator.cs +++ b/Stack/Opc.Ua.Core/Types/Utils/DataGenerator.cs @@ -903,7 +903,7 @@ public ExpandedNodeId GetRandomExpandedNodeId() { NodeId nodeId = GetRandomNodeId(); ushort serverIndex = m_serverUris.Count == 0 ? (ushort)0 : (ushort)m_random.NextInt32(m_serverUris.Count - 1); - return new ExpandedNodeId(nodeId, m_namespaceUris.GetString(nodeId.NamespaceIndex), serverIndex); + return new ExpandedNodeId(nodeId, nodeId.NamespaceIndex > 0 ? m_namespaceUris.GetString(nodeId.NamespaceIndex) : null, serverIndex); } #endregion diff --git a/Stack/Opc.Ua.Core/Types/Utils/Utils.cs b/Stack/Opc.Ua.Core/Types/Utils/Utils.cs index 4a0c55e731..bd3917adb1 100644 --- a/Stack/Opc.Ua.Core/Types/Utils/Utils.cs +++ b/Stack/Opc.Ua.Core/Types/Utils/Utils.cs @@ -1260,6 +1260,33 @@ public static string ReplaceDCLocalhost(string subjectName, string hostname = nu return buffer.ToString(); } + /// + /// Escapes a URI string using the percent encoding. + /// + public static string EscapeUri(string uri) + { + if (!String.IsNullOrWhiteSpace(uri)) + { + var builder = new UriBuilder(uri.Replace(";", "%3b")); + return builder.Uri.AbsoluteUri; + } + + return String.Empty; + } + + /// + /// Unescapes a URI string using the percent encoding. + /// + public static string UnescapeUri(string uri) + { + if (!string.IsNullOrWhiteSpace(uri)) + { + return Uri.UnescapeDataString(uri); + } + + return String.Empty; + } + /// /// Parses a URI string. Returns null if it is invalid. /// @@ -1504,24 +1531,35 @@ public static string ToHexString(byte[] buffer, bool invertEndian = false) return String.Empty; } - StringBuilder builder = new StringBuilder(buffer.Length * 2); - - if (invertEndian) +#if NET6_0_OR_GREATER + if (!invertEndian) { - for (int ii = buffer.Length - 1; ii >= 0; ii--) - { - builder.AppendFormat(CultureInfo.InvariantCulture, "{0:X2}", buffer[ii]); - } + return Convert.ToHexString(buffer); } else +#endif { - for (int ii = 0; ii < buffer.Length; ii++) + StringBuilder builder = new StringBuilder(buffer.Length * 2); + +#if !NET6_0_OR_GREATER + if (!invertEndian) { - builder.AppendFormat(CultureInfo.InvariantCulture, "{0:X2}", buffer[ii]); + for (int ii = 0; ii < buffer.Length; ii++) + { + builder.AppendFormat(CultureInfo.InvariantCulture, "{0:X2}", buffer[ii]); + } + } + else +#endif + { + for (int ii = buffer.Length - 1; ii >= 0; ii--) + { + builder.AppendFormat(CultureInfo.InvariantCulture, "{0:X2}", buffer[ii]); + } } - } - return builder.ToString(); + return builder.ToString(); + } } /// @@ -1529,6 +1567,9 @@ public static string ToHexString(byte[] buffer, bool invertEndian = false) /// public static byte[] FromHexString(string buffer) { +#if NET6_0_OR_GREATER + return Convert.FromHexString(buffer); +#else if (buffer == null) { return null; @@ -1575,6 +1616,7 @@ public static byte[] FromHexString(string buffer) } return bytes; +#endif } /// @@ -1728,6 +1770,12 @@ public static object Clone(object value) return value; } + // Guid are special a reference type that does not need to be copied. + if (type == typeof(Guid)) + { + return value; + } + // copy arrays, any dimension. if (value is Array array) { @@ -2529,7 +2577,7 @@ public static void UpdateExtension(ref XmlElementCollection extensions, XmlQu extensions.Add(document.DocumentElement); } } - #endregion +#endregion #region Reflection Helper Functions /// @@ -2595,6 +2643,18 @@ public static uint GetIdentifier(string name, Type constants) return 0; } + private static readonly DateTime kBaseDateTime = new DateTime(2000, 1, 1, 0, 0, 0, DateTimeKind.Utc); + + /// + /// Return the current time in milliseconds since 1/1/2000. + /// + /// The current time in milliseconds since 1/1/2000. + public static uint GetVersionTime() + { + var ticks = (DateTime.UtcNow - kBaseDateTime).TotalMilliseconds; + return (uint)ticks; + } + /// /// Returns the linker timestamp for an assembly. /// diff --git a/Tests/Opc.Ua.Client.ComplexTypes.Tests/Types/ComplexTypesCommon.cs b/Tests/Opc.Ua.Client.ComplexTypes.Tests/Types/ComplexTypesCommon.cs index d47d71b6dd..a881522a72 100644 --- a/Tests/Opc.Ua.Client.ComplexTypes.Tests/Types/ComplexTypesCommon.cs +++ b/Tests/Opc.Ua.Client.ComplexTypes.Tests/Types/ComplexTypesCommon.cs @@ -279,6 +279,7 @@ protected void EncodeDecodeComplexType( IServiceMessageContext encoderContext, MemoryStreamType memoryStreamType, EncodingType encoderType, + JsonEncodingType jsonEncodingType, StructureType structureType, ExpandedNodeId nodeId, object data @@ -295,7 +296,7 @@ object data byte[] buffer; using (var encoderStream = CreateEncoderMemoryStream(memoryStreamType)) { - using (IEncoder encoder = CreateEncoder(encoderType, encoderContext, encoderStream, typeof(DataValue))) + using (IEncoder encoder = CreateEncoder(encoderType, encoderContext, encoderStream, typeof(DataValue), jsonEncodingType)) { encoder.WriteExtensionObject("ExtensionObject", expected); } diff --git a/Tests/Opc.Ua.Client.ComplexTypes.Tests/Types/EncoderTests.cs b/Tests/Opc.Ua.Client.ComplexTypes.Tests/Types/EncoderTests.cs index 916e27fd3d..28131c939e 100644 --- a/Tests/Opc.Ua.Client.ComplexTypes.Tests/Types/EncoderTests.cs +++ b/Tests/Opc.Ua.Client.ComplexTypes.Tests/Types/EncoderTests.cs @@ -87,18 +87,21 @@ public class ComplexTypesEncoderTests : ComplexTypesCommon [Theory] [Category("ComplexTypes")] public void ReEncodeComplexType( + [ValueSource(nameof(EncodingTypesReversibleCompact))] + EncodingTypeGroup encoderTypeGroup, MemoryStreamType memoryStreamType, - EncodingType encoderType, StructureType structureType ) { + EncodingType encoderType = encoderTypeGroup.EncoderType; + JsonEncodingType jsonEncodingType = encoderTypeGroup.JsonEncodingType; ExpandedNodeId nodeId; Type complexType; (nodeId, complexType) = TypeDictionary[structureType]; object emittedType = Activator.CreateInstance(complexType); var baseType = emittedType as BaseComplexType; FillStructWithValues(baseType, true); - EncodeDecodeComplexType(EncoderContext, memoryStreamType, encoderType, structureType, nodeId, emittedType); + EncodeDecodeComplexType(EncoderContext, memoryStreamType, encoderType, jsonEncodingType, structureType, nodeId, emittedType); } /// @@ -108,12 +111,15 @@ StructureType structureType [Theory] [Category("ComplexTypes")] public void ReEncodeStructureWithOptionalFieldsComplexType( + [ValueSource(nameof(EncodingTypesReversibleCompact))] + EncodingTypeGroup encoderTypeGroup, MemoryStreamType memoryStreamType, - EncodingType encoderType, StructureFieldParameter structureFieldParameter ) { ExpandedNodeId nodeId; + EncodingType encoderType = encoderTypeGroup.EncoderType; + JsonEncodingType jsonEncodingType = encoderTypeGroup.JsonEncodingType; Type complexType; (nodeId, complexType) = TypeDictionary[StructureType.StructureWithOptionalFields]; object emittedType = Activator.CreateInstance(complexType); @@ -121,17 +127,17 @@ StructureFieldParameter structureFieldParameter var builtInType = structureFieldParameter.BuiltInType; TestContext.Out.WriteLine($"Optional Field: {structureFieldParameter.BuiltInType} is the only value."); baseType[structureFieldParameter.Name] = DataGenerator.GetRandom(builtInType); - EncodeDecodeComplexType(EncoderContext, memoryStreamType, encoderType, StructureType.StructureWithOptionalFields, nodeId, emittedType); + EncodeDecodeComplexType(EncoderContext, memoryStreamType, encoderType, jsonEncodingType, StructureType.StructureWithOptionalFields, nodeId, emittedType); TestContext.Out.WriteLine($"Optional Field: {structureFieldParameter.BuiltInType} is null."); baseType[structureFieldParameter.Name] = null; - EncodeDecodeComplexType(EncoderContext, memoryStreamType, encoderType, StructureType.StructureWithOptionalFields, nodeId, emittedType); + EncodeDecodeComplexType(EncoderContext, memoryStreamType, encoderType, jsonEncodingType, StructureType.StructureWithOptionalFields, nodeId, emittedType); TestContext.Out.WriteLine($"Optional Field: {structureFieldParameter.BuiltInType} is null, all other fields have random values."); FillStructWithValues(baseType, true); baseType[structureFieldParameter.Name] = null; - EncodeDecodeComplexType(EncoderContext, memoryStreamType, encoderType, StructureType.StructureWithOptionalFields, nodeId, emittedType); + EncodeDecodeComplexType(EncoderContext, memoryStreamType, encoderType, jsonEncodingType, StructureType.StructureWithOptionalFields, nodeId, emittedType); TestContext.Out.WriteLine($"Optional Field: {structureFieldParameter.BuiltInType} has random value."); baseType[structureFieldParameter.Name] = DataGenerator.GetRandom(builtInType); - EncodeDecodeComplexType(EncoderContext, memoryStreamType, encoderType, StructureType.StructureWithOptionalFields, nodeId, emittedType); + EncodeDecodeComplexType(EncoderContext, memoryStreamType, encoderType, jsonEncodingType, StructureType.StructureWithOptionalFields, nodeId, emittedType); } /// @@ -141,11 +147,14 @@ StructureFieldParameter structureFieldParameter [Theory] [Category("ComplexTypes")] public void ReEncodeUnionComplexType( + [ValueSource(nameof(EncodingTypesReversibleCompact))] + EncodingTypeGroup encoderTypeGroup, MemoryStreamType memoryStreamType, - EncodingType encoderType, StructureFieldParameter structureFieldParameter ) { + EncodingType encoderType = encoderTypeGroup.EncoderType; + JsonEncodingType jsonEncodingType = encoderTypeGroup.JsonEncodingType; ExpandedNodeId nodeId; Type complexType; (nodeId, complexType) = TypeDictionary[StructureType.Union]; @@ -154,10 +163,10 @@ StructureFieldParameter structureFieldParameter var builtInType = structureFieldParameter.BuiltInType; TestContext.Out.WriteLine($"Union Field: {structureFieldParameter.BuiltInType} is random."); baseType[structureFieldParameter.Name] = DataGenerator.GetRandom(builtInType); - EncodeDecodeComplexType(EncoderContext, memoryStreamType, encoderType, StructureType.Union, nodeId, emittedType); + EncodeDecodeComplexType(EncoderContext, memoryStreamType, encoderType, jsonEncodingType, StructureType.Union, nodeId, emittedType); TestContext.Out.WriteLine($"Union Field: {structureFieldParameter.BuiltInType} is null."); baseType[structureFieldParameter.Name] = null; - EncodeDecodeComplexType(EncoderContext, memoryStreamType, encoderType, StructureType.Union, nodeId, emittedType); + EncodeDecodeComplexType(EncoderContext, memoryStreamType, encoderType, jsonEncodingType, StructureType.Union, nodeId, emittedType); } #endregion Test Methods } diff --git a/Tests/Opc.Ua.Client.ComplexTypes.Tests/Types/JsonEncoderTests.cs b/Tests/Opc.Ua.Client.ComplexTypes.Tests/Types/JsonEncoderTests.cs index 833fb79910..bbe1853b0f 100644 --- a/Tests/Opc.Ua.Client.ComplexTypes.Tests/Types/JsonEncoderTests.cs +++ b/Tests/Opc.Ua.Client.ComplexTypes.Tests/Types/JsonEncoderTests.cs @@ -96,12 +96,12 @@ public class ComplexTypesJsonEncoderTests : ComplexTypesCommon /// Only a small subset of built in types is tested on complex types. /// [DatapointSource] - public JsonValidationData[] Data = new JsonValidationDataCollection() { - { BuiltInType.Boolean, false, "false", null }, + public static readonly JsonValidationData[] Data = new JsonValidationDataCollection() { + { BuiltInType.Boolean, false, "false", null, null, "false"}, { BuiltInType.Boolean, true,"true", null }, - { BuiltInType.Byte, (Byte)0, "0", null}, + { BuiltInType.Byte, (Byte)0, "0", null, null, "0"}, { BuiltInType.Byte, (Byte)88, "88", null }, - { BuiltInType.SByte, (SByte)0, "0", null }, + { BuiltInType.SByte, (SByte)0, "0", null, null, "0"}, { BuiltInType.UInt16, (UInt16)12345, "12345", null }, { BuiltInType.Int16, (Int16)(-12345), "-12345", null }, { BuiltInType.UInt32, (UInt32)1234567, "1234567", null }, @@ -117,35 +117,12 @@ public class ComplexTypesJsonEncoderTests : ComplexTypesCommon #region Test Methods /// - /// Verify reversible Json encoding for Structure as body of ExtensionObject. + /// Verify encoding of a Structure as body of ExtensionObject. /// [Theory] - public void JsonEncodeStructureRev( - JsonValidationData jsonValidationData - ) - { - ExpandedNodeId nodeId; - Type complexType; - (nodeId, complexType) = TypeDictionary[StructureType.Structure]; - object emittedType = Activator.CreateInstance(complexType); - var baseType = emittedType as BaseComplexType; - baseType[jsonValidationData.BuiltInType.ToString()] = jsonValidationData.Instance; - ExtensionObject extensionObject = CreateExtensionObject(StructureType.Structure, nodeId, emittedType); - EncodeJsonComplexTypeVerifyResult( - jsonValidationData.BuiltInType, - MemoryStreamType.MemoryStream, - extensionObject, - true, - jsonValidationData.ExpectedNonReversible ?? jsonValidationData.ExpectedReversible, - false); - } - - /// - /// Verify non reversible Json encoding of a Structure as body of ExtensionObject. - /// - [Theory] - public void JsonEncodeStructureNonRev( - JsonValidationData jsonValidationData + public void JsonEncodeStructure( + JsonValidationData jsonValidationData, + JsonEncodingType jsonEncoding ) { ExpandedNodeId nodeId; @@ -159,8 +136,8 @@ JsonValidationData jsonValidationData jsonValidationData.BuiltInType, MemoryStreamType.ArraySegmentStream, extensionObject, - false, - jsonValidationData.ExpectedNonReversible ?? jsonValidationData.ExpectedReversible, + jsonEncoding, + jsonValidationData.GetExpected(jsonEncoding), false); } @@ -169,8 +146,9 @@ JsonValidationData jsonValidationData /// with optional fields as body of ExtensionObject. /// [Theory] - public void JsonEncodeOptionalFieldsRev( - JsonValidationData jsonValidationData + public void JsonEncodeOptionalFields( + JsonValidationData jsonValidationData, + JsonEncodingType jsonEncoding ) { ExpandedNodeId nodeId; @@ -184,32 +162,8 @@ JsonValidationData jsonValidationData jsonValidationData.BuiltInType, MemoryStreamType.ArraySegmentStream, extensionObject, - true, - jsonValidationData.ExpectedNonReversible ?? jsonValidationData.ExpectedReversible, - false); - } - - /// - /// Verify non reversible Json encoding of a Structure - /// with optional fields as body of ExtensionObject. - /// - [Theory] - public void JsonEncodeOptionalFieldsNonRev( - JsonValidationData jsonValidationData) - { - ExpandedNodeId nodeId; - Type complexType; - (nodeId, complexType) = TypeDictionary[StructureType.StructureWithOptionalFields]; - object emittedType = Activator.CreateInstance(complexType); - var baseType = emittedType as BaseComplexType; - baseType[jsonValidationData.BuiltInType.ToString()] = jsonValidationData.Instance; - ExtensionObject extensionObject = CreateExtensionObject(StructureType.StructureWithOptionalFields, nodeId, emittedType); - EncodeJsonComplexTypeVerifyResult( - jsonValidationData.BuiltInType, - MemoryStreamType.RecyclableMemoryStream, - extensionObject, - false, - jsonValidationData.ExpectedNonReversible ?? jsonValidationData.ExpectedReversible, + jsonEncoding, + jsonValidationData.GetExpected(jsonEncoding), false); } @@ -217,8 +171,9 @@ public void JsonEncodeOptionalFieldsNonRev( /// Verify reversible Json encoding for Unions with ExtensionObject. /// [Theory] - public void JsonEncodeUnionRev( - JsonValidationData jsonValidationData + public void JsonEncodeUnion( + JsonValidationData jsonValidationData, + JsonEncodingType jsonEncoding ) { ExpandedNodeId nodeId; @@ -232,32 +187,8 @@ JsonValidationData jsonValidationData jsonValidationData.BuiltInType, MemoryStreamType.ArraySegmentStream, extensionObject, - true, - jsonValidationData.ExpectedNonReversible ?? jsonValidationData.ExpectedReversible, - false); - } - - /// - /// Verify non reversible Json encoding of a Union in a ExtensionObject. - /// - [Theory] - public void JsonEncodeUnionNonRev( - JsonValidationData jsonValidationData - ) - { - ExpandedNodeId nodeId; - Type complexType; - (nodeId, complexType) = TypeDictionary[StructureType.Union]; - object emittedType = Activator.CreateInstance(complexType); - var baseType = emittedType as BaseComplexType; - baseType[jsonValidationData.BuiltInType.ToString()] = jsonValidationData.Instance; - ExtensionObject extensionObject = CreateExtensionObject(StructureType.Union, nodeId, emittedType); - EncodeJsonComplexTypeVerifyResult( - jsonValidationData.BuiltInType, - MemoryStreamType.ArraySegmentStream, - extensionObject, - false, - jsonValidationData.ExpectedNonReversible ?? jsonValidationData.ExpectedReversible, + jsonEncoding, + jsonValidationData.GetExpected(jsonEncoding), false); } #endregion Test Methods @@ -267,48 +198,66 @@ protected void EncodeJsonComplexTypeVerifyResult( BuiltInType builtInType, MemoryStreamType memoryStreamType, ExtensionObject data, - bool useReversibleEncoding, + JsonEncodingType jsonEncoding, string expected, bool topLevelIsArray ) { - string encodeInfo = $"Encoder: Json Type:{builtInType} Reversible: {useReversibleEncoding}"; - TestContext.Out.WriteLine(encodeInfo); - TestContext.Out.WriteLine("Data:"); - TestContext.Out.WriteLine(data); - TestContext.Out.WriteLine("Expected:"); + string encodeInfo = $"Encoder: Json Type:{builtInType} Encoding: {jsonEncoding}"; - expected = BuildExpectedResponse(data, builtInType, expected, useReversibleEncoding); - _ = PrettifyAndValidateJson(expected); + expected = BuildExpectedResponse(data, builtInType, expected, jsonEncoding); + var formattedExpected = PrettifyAndValidateJson(expected); byte[] buffer; using (var encoderStream = CreateEncoderMemoryStream(memoryStreamType)) { using (IEncoder encoder = CreateEncoder( EncodingType.Json, EncoderContext, encoderStream, - typeof(ExtensionObject), useReversibleEncoding, topLevelIsArray)) + typeof(ExtensionObject), jsonEncoding, topLevelIsArray)) { - Encode(encoder, BuiltInType.ExtensionObject, useReversibleEncoding ? builtInType.ToString() : null, data); + var builtInTypeString = (jsonEncoding != JsonEncodingType.NonReversible) ? builtInType.ToString() : null; + Encode(encoder, BuiltInType.ExtensionObject, builtInTypeString, data); } buffer = encoderStream.ToArray(); } - TestContext.Out.WriteLine("Result:"); - var result = Encoding.UTF8.GetString(buffer); - if (data.Body is UnionComplexType && !useReversibleEncoding) + string formattedResult = null; + string result = null; + try + { + result = Encoding.UTF8.GetString(buffer); + if (data.Body is UnionComplexType && (jsonEncoding == JsonEncodingType.NonReversible)) + { + // helper to create testable JSON output for Unions + result = result.Replace("{", "{\"Union\" :"); + } + formattedResult = PrettifyAndValidateJson(result); + var jsonLoadSettings = new JsonLoadSettings() { + CommentHandling = CommentHandling.Ignore, + LineInfoHandling = LineInfoHandling.Ignore + }; + var resultParsed = JObject.Parse(result, jsonLoadSettings); + var expectedParsed = JObject.Parse(expected, jsonLoadSettings); + var areEqual = JToken.DeepEquals(expectedParsed, resultParsed); + Assert.IsTrue(areEqual, encodeInfo); + } + catch { - // helper to create testable JSON output for Unions - result = result.Replace("{", "{\"Union\" :"); + TestContext.Out.WriteLine(encodeInfo); + TestContext.Out.WriteLine("Data:"); + TestContext.Out.WriteLine(data); + TestContext.Out.WriteLine("Expected:"); + TestContext.Out.WriteLine(formattedExpected); + TestContext.Out.WriteLine("Result:"); + if (!string.IsNullOrEmpty(formattedResult)) + { + TestContext.Out.WriteLine(formattedResult); + } + else + { + TestContext.Out.WriteLine(result); + } } - var formattedResult = PrettifyAndValidateJson(result); - var jsonLoadSettings = new JsonLoadSettings() { - CommentHandling = CommentHandling.Ignore, - LineInfoHandling = LineInfoHandling.Ignore - }; - var resultParsed = JObject.Parse(result, jsonLoadSettings); - var expectedParsed = JObject.Parse(expected, jsonLoadSettings); - var areEqual = JToken.DeepEquals(expectedParsed, resultParsed); - Assert.IsTrue(areEqual, encodeInfo); } /// @@ -320,121 +269,154 @@ private string BuildExpectedResponse( ExtensionObject data, BuiltInType builtInType, string expected, - bool useReversibleEncoding) + JsonEncodingType jsonEncoding) { // build expected result string typeId = String.Empty; if (!data.TypeId.IsNull) { var nodeId = ExpandedNodeId.ToNodeId(data.TypeId, EncoderContext.NamespaceUris); - typeId = $"\"TypeId\":{{\"Id\":{nodeId.Identifier},\"Namespace\":{nodeId.NamespaceIndex}}},"; + if (jsonEncoding == JsonEncodingType.NonReversible || jsonEncoding == JsonEncodingType.Reversible) + { + typeId = $"\"TypeId\":{{\"Id\":{nodeId.Identifier},\"Namespace\":{nodeId.NamespaceIndex}}},"; + } + else + { + typeId = $"\"TypeId\":\"{nodeId.Format(EncoderContext, true)}\","; + } } + + bool expectedIsEmpty = false; if (String.IsNullOrEmpty(expected)) { expected = "{}"; + expectedIsEmpty = true; } - else if (data.Body is UnionComplexType) + + if (!expectedIsEmpty || jsonEncoding == JsonEncodingType.Compact) { - if (useReversibleEncoding) + if (data.Body is UnionComplexType) { - var union = data.Body as UnionComplexType; - var json = $"{{\"{builtInType}\" :{{"; - if (!data.TypeId.IsNull) + if (jsonEncoding != JsonEncodingType.NonReversible) { - json += typeId; + var union = data.Body as UnionComplexType; + var json = $"{{\"{builtInType}\" :{{"; + if (!data.TypeId.IsNull) + { + json += typeId; + } + json += $"\"Body\":{{\"SwitchField\" : {union.SwitchField}"; + if (!expectedIsEmpty) + { + json += ", \"Value\":" + expected; + } + json += "}}}"; + expected = json; } - json += $"\"Body\":{{\"SwitchField\" : {union.SwitchField}, \"Value\":" + expected + "}}}"; - expected = json; - } - else - { - expected = "{\"Union\" :" + expected + "}"; - } - } - else if (data.Body is OptionalFieldsComplexType) - { - if (useReversibleEncoding) - { - var optional = data.Body as OptionalFieldsComplexType; - var json = $"{{\"{builtInType}\" :{{"; - if (!data.TypeId.IsNull) + else { - json += typeId; + expected = "{\"Union\" :" + expected + "}"; } - json += $"\"Body\":{{\"EncodingMask\" : {optional.EncodingMask}, \"{builtInType}\":" + expected + "}}}"; - expected = json; } - else + else if (data.Body is OptionalFieldsComplexType) { - expected = $"{{\"{builtInType}\" :" + expected + "}"; - } - } - else if (data.Body is BaseComplexType) - { - var structure = data.Body as BaseComplexType; - var body = ""; - bool commaNeeded = false; - foreach (var property in structure.GetPropertyEnumerator()) - { - if (builtInType.ToString() == property.Name) + if (jsonEncoding != JsonEncodingType.NonReversible) { - if (commaNeeded) body += ","; - commaNeeded = true; - body += $"\"{builtInType}\":" + expected; + var optional = data.Body as OptionalFieldsComplexType; + var json = $"{{\"{builtInType}\" :{{"; + if (!data.TypeId.IsNull) + { + json += typeId; + } + json += $"\"Body\":{{\"EncodingMask\" : {optional.EncodingMask}"; + if (!expectedIsEmpty) + { + json += $", \"{builtInType}\":" + expected; + } + json += "}}}"; + expected = json; } else { - object o = property.GetValue(structure); - string oText = o?.ToString().ToLowerInvariant(); - if (property.Name == "DateTime") + expected = $"{{\"{builtInType}\" :" + expected + "}"; + } + } + else if (data.Body is BaseComplexType) + { + var structure = data.Body as BaseComplexType; + var body = ""; + bool commaNeeded = false; + foreach (var property in structure.GetPropertyEnumerator()) + { + if (builtInType.ToString() == property.Name) { - oText = "\"0001-01-01T00:00:00Z\""; - continue; + if (!expectedIsEmpty) + { + if (commaNeeded) body += ","; + commaNeeded = true; + body += $"\"{builtInType}\":" + expected; + } } - else if (property.Name == "StatusCode") + else if (jsonEncoding != JsonEncodingType.Compact) { - if (useReversibleEncoding) + object o = property.GetValue(structure); + string oText = o?.ToString().ToLowerInvariant(); + if (property.Name == "DateTime") { - oText = "0"; + oText = "\"0001-01-01T00:00:00Z\""; + if (jsonEncoding == JsonEncodingType.Reversible || jsonEncoding == JsonEncodingType.NonReversible) + { + continue; + } } - else + else if (property.Name == "StatusCode") { - oText = "{\"Code\": 0,\"Symbol\":\"Good\"}"; - // default statuscode is not encoded + if (jsonEncoding == JsonEncodingType.Compact || jsonEncoding == JsonEncodingType.Reversible) + { + oText = "0"; + } + else + { + oText = "{\"Code\": 0,\"Symbol\":\"Good\"}"; + // default statuscode is not encoded + } + continue; + } + else if (property.Name == "Guid") + { + oText = "\"00000000-0000-0000-0000-000000000000\""; + if (jsonEncoding == JsonEncodingType.Reversible || jsonEncoding == JsonEncodingType.NonReversible) + { + continue; + } + } + else if (property.Name == "UInt64" || property.Name == "Int64") + { + oText = "\"" + oText + "\""; } - continue; - } - else if (property.Name == "Guid") - { - oText = "\"00000000-0000-0000-0000-000000000000\""; - continue; - } - else if (property.Name == "UInt64" || property.Name == "Int64") - { - oText = "\"" + oText + "\""; - } - if (oText != null) + if (oText != null) + { + if (commaNeeded) body += ","; + commaNeeded = true; + body += $"\"{property.Name}\":" + oText; + } + } + } + if (jsonEncoding != JsonEncodingType.NonReversible) + { + var json = $"{{\"{builtInType}\" :{{"; + if (!data.TypeId.IsNull) { - if (commaNeeded) body += ","; - commaNeeded = true; - body += $"\"{property.Name}\":" + oText; + json += typeId; } + json += "\"Body\":{" + body + "}}}"; + expected = json; } - } - if (useReversibleEncoding) - { - var json = $"{{\"{builtInType}\" :{{"; - if (!data.TypeId.IsNull) + else { - json += typeId; + expected = "{" + body + "}"; } - json += "\"Body\":{" + body + "}}}"; - expected = json; - } - else - { - expected = "{" + body + "}"; } } return expected; diff --git a/Tests/Opc.Ua.Client.ComplexTypes.Tests/Types/MockResolverTests.cs b/Tests/Opc.Ua.Client.ComplexTypes.Tests/Types/MockResolverTests.cs index 553dece670..49a8e05051 100644 --- a/Tests/Opc.Ua.Client.ComplexTypes.Tests/Types/MockResolverTests.cs +++ b/Tests/Opc.Ua.Client.ComplexTypes.Tests/Types/MockResolverTests.cs @@ -150,9 +150,14 @@ public void Add(string name, NodeId typeId) /// Test the functionality to create a custom complex type. /// [Theory] - public async Task CreateMockTypeAsync(EncodingType encodingType, MemoryStreamType memoryStreamType) + public async Task CreateMockTypeAsync( + [ValueSource(nameof(EncodingTypesReversibleCompact))] + EncodingTypeGroup encoderTypeGroup, + MemoryStreamType memoryStreamType) { var mockResolver = new MockResolver(); + EncodingType encoderType = encoderTypeGroup.EncoderType; + JsonEncodingType jsonEncodingType = encoderTypeGroup.JsonEncodingType; var nameSpaceIndex = mockResolver.NamespaceUris.GetIndexOrAppend(Namespaces.MockResolverUrl); uint nodeId = 100; @@ -269,7 +274,7 @@ public async Task CreateMockTypeAsync(EncodingType encodingType, MemoryStreamTyp _ = PrettifyAndValidateJson(Encoding.UTF8.GetString(buffer)); // test encoder/decoder - EncodeDecodeComplexType(encoderContext, memoryStreamType, encodingType, StructureType.Structure, nodeId, car); + EncodeDecodeComplexType(encoderContext, memoryStreamType, encoderType, jsonEncodingType, StructureType.Structure, nodeId, car); // Test extracting type definition @@ -283,8 +288,13 @@ public async Task CreateMockTypeAsync(EncodingType encodingType, MemoryStreamTyp /// Test the functionality to create a custom complex type. /// [Theory] - public async Task CreateMockArrayTypeAsync(EncodingType encodingType, MemoryStreamType memoryStreamType) + public async Task CreateMockArrayTypeAsync( + [ValueSource(nameof(EncodingTypesReversibleCompact))] + EncodingTypeGroup encoderTypeGroup, + MemoryStreamType memoryStreamType) { + EncodingType encoderType = encoderTypeGroup.EncoderType; + JsonEncodingType jsonEncodingType = encoderTypeGroup.JsonEncodingType; var mockResolver = new MockResolver(); // only enumerable types in the encodeable factory are stored as Enum in a structure. @@ -434,7 +444,7 @@ public async Task CreateMockArrayTypeAsync(EncodingType encodingType, MemoryStre byte[] buffer; using (var encoderStream = CreateEncoderMemoryStream(memoryStreamType)) { - using (IEncoder encoder = CreateEncoder(EncodingType.Json, encoderContext, encoderStream, arraysTypes, false)) + using (IEncoder encoder = CreateEncoder(EncodingType.Json, encoderContext, encoderStream, arraysTypes)) { encoder.WriteEncodeable("Arrays", arrays, arraysTypes); } @@ -444,7 +454,7 @@ public async Task CreateMockArrayTypeAsync(EncodingType encodingType, MemoryStre _ = PrettifyAndValidateJson(Encoding.UTF8.GetString(buffer)); // test encoder/decoder - EncodeDecodeComplexType(encoderContext, memoryStreamType, encodingType, StructureType.Structure, dataTypeNode.NodeId, arrays); + EncodeDecodeComplexType(encoderContext, memoryStreamType, encoderType, jsonEncodingType, StructureType.Structure, dataTypeNode.NodeId, arrays); // Test extracting type definition @@ -455,11 +465,16 @@ public async Task CreateMockArrayTypeAsync(EncodingType encodingType, MemoryStre } /// - /// Create a complex type with a single scalar or array type, with default and random values . + /// Create a complex type with a single scalar or array type, with default and random values. /// [Theory] - public async Task CreateMockSingleTypeAsync(EncodingType encodingType, MemoryStreamType memoryStreamType, TestType typeDescription, bool randomValues, TestRanks testRank) + public async Task CreateMockSingleTypeAsync( + [ValueSource(nameof(EncodingTypesReversibleCompact))] + EncodingTypeGroup encoderTypeGroup, + MemoryStreamType memoryStreamType, TestType typeDescription, bool randomValues, TestRanks testRank) { + EncodingType encoderType = encoderTypeGroup.EncoderType; + JsonEncodingType jsonEncodingType = encoderTypeGroup.JsonEncodingType; SetRepeatedRandomSeed(); var mockResolver = new MockResolver(); @@ -615,7 +630,7 @@ public async Task CreateMockSingleTypeAsync(EncodingType encodingType, MemoryStr byte [] buffer; using (var encoderStream = CreateEncoderMemoryStream(memoryStreamType)) { - using (IEncoder encoder = CreateEncoder(EncodingType.Json, encoderContext, encoderStream, arraysTypes, false)) + using (IEncoder encoder = CreateEncoder(EncodingType.Json, encoderContext, encoderStream, arraysTypes)) { encoder.WriteEncodeable("TestType", testType, arraysTypes); } @@ -625,7 +640,7 @@ public async Task CreateMockSingleTypeAsync(EncodingType encodingType, MemoryStr _ = PrettifyAndValidateJson(Encoding.UTF8.GetString(buffer)); // test encoder/decoder - EncodeDecodeComplexType(encoderContext, memoryStreamType, encodingType, StructureType.Structure, dataTypeNode.NodeId, testType); + EncodeDecodeComplexType(encoderContext, memoryStreamType, encoderType, jsonEncodingType, StructureType.Structure, dataTypeNode.NodeId, testType); // Test extracting type definition diff --git a/Tests/Opc.Ua.Core.Tests/Types/Encoders/EncodeableTests.cs b/Tests/Opc.Ua.Core.Tests/Types/Encoders/EncodeableTests.cs index 1bd14d2b90..9569ab7fae 100644 --- a/Tests/Opc.Ua.Core.Tests/Types/Encoders/EncodeableTests.cs +++ b/Tests/Opc.Ua.Core.Tests/Types/Encoders/EncodeableTests.cs @@ -58,11 +58,14 @@ public class EncodeableTypesTests : EncoderCommon [Theory] [Category("EncodeableTypes")] public void ActivateEncodeableType( - EncodingType encoderType, + [ValueSource(nameof(EncodingTypesReversibleCompact))] + EncodingTypeGroup encoderTypeGroup, MemoryStreamType memoryStreamType, Type systemType ) { + EncodingType encoderType = encoderTypeGroup.EncoderType; + JsonEncodingType jsonEncodingType = encoderTypeGroup.JsonEncodingType; IEncodeable testObject = CreateDefaultEncodeableType(systemType) as IEncodeable; Assert.NotNull(testObject); Assert.False(testObject.BinaryEncodingId.IsNull); @@ -71,17 +74,20 @@ Type systemType Assert.AreNotEqual(testObject.TypeId, testObject.BinaryEncodingId); Assert.AreNotEqual(testObject.TypeId, testObject.XmlEncodingId); Assert.AreNotEqual(testObject.BinaryEncodingId, testObject.XmlEncodingId); - EncodeDecode(encoderType, BuiltInType.ExtensionObject, memoryStreamType, new ExtensionObject(testObject.TypeId, testObject)); + EncodeDecode(encoderType, jsonEncodingType, BuiltInType.ExtensionObject, memoryStreamType, new ExtensionObject(testObject.TypeId, testObject)); } [Theory] [Category("EncodeableTypes")] public void ActivateEncodeableTypeArray( - EncodingType encoderType, + [ValueSource(nameof(EncodingTypesReversibleCompact))] + EncodingTypeGroup encoderTypeGroup, MemoryStreamType memoryStreamType, Type systemType ) { + EncodingType encoderType = encoderTypeGroup.EncoderType; + JsonEncodingType jsonEncodingType = encoderTypeGroup.JsonEncodingType; int arrayLength = DataGenerator.GetRandomByte(); Array array = Array.CreateInstance(systemType, arrayLength); ExpandedNodeId dataTypeId = NodeId.Null; @@ -101,7 +107,7 @@ Type systemType byte[] buffer; using (var encoderStream = CreateEncoderMemoryStream(memoryStreamType)) { - using (IEncoder encoder = CreateEncoder(encoderType, Context, encoderStream, systemType)) + using (IEncoder encoder = CreateEncoder(encoderType, Context, encoderStream, systemType, jsonEncodingType)) { encoder.PushNamespace("urn:This:is:another:namespace"); encoder.WriteArray(objectName, array, ValueRanks.OneDimension, builtInType); @@ -143,12 +149,15 @@ Type systemType [Theory] [Category("EncodeableTypes")] public void ActivateEncodeableTypeMatrix( - EncodingType encoderType, + [ValueSource(nameof(EncodingTypesReversibleCompact))] + EncodingTypeGroup encoderTypeGroup, MemoryStreamType memoryStreamType, bool encodeAsMatrix, Type systemType ) { + EncodingType encoderType = encoderTypeGroup.EncoderType; + JsonEncodingType jsonEncodingType = encoderTypeGroup.JsonEncodingType; int matrixDimension = RandomSource.NextInt32(2) + 2; int[] dimensions = new int[matrixDimension]; SetMatrixDimensions(dimensions); @@ -174,7 +183,7 @@ Type systemType byte[] buffer; using (var encoderStream = CreateEncoderMemoryStream(memoryStreamType)) { - using (IEncoder encoder = CreateEncoder(encoderType, Context, encoderStream, systemType)) + using (IEncoder encoder = CreateEncoder(encoderType, Context, encoderStream, systemType, jsonEncodingType)) { if (encodeAsMatrix) { diff --git a/Tests/Opc.Ua.Core.Tests/Types/Encoders/EncoderCommon.cs b/Tests/Opc.Ua.Core.Tests/Types/Encoders/EncoderCommon.cs index 3f02fd7672..d305313bf2 100644 --- a/Tests/Opc.Ua.Core.Tests/Types/Encoders/EncoderCommon.cs +++ b/Tests/Opc.Ua.Core.Tests/Types/Encoders/EncoderCommon.cs @@ -146,6 +146,49 @@ protected void SetRandomSeed(int randomSeed) [DatapointSource] public static readonly EncodingType[] EncoderTypes = (EncodingType[])Enum.GetValues(typeof(EncodingType)); + + public static readonly EncodingTypeGroup[] EncodingTypesJson = new EncodingTypeGroup[] { + new EncodingTypeGroup(EncodingType.Json, JsonEncodingType.Reversible), + new EncodingTypeGroup(EncodingType.Json, JsonEncodingType.Compact), + new EncodingTypeGroup(EncodingType.Json, JsonEncodingType.NonReversible), + new EncodingTypeGroup(EncodingType.Json, JsonEncodingType.Verbose) + }; + + public static readonly EncodingTypeGroup[] EncodingTypesJsonNonReversibleVerbose = new EncodingTypeGroup[] { + new EncodingTypeGroup(EncodingType.Json, JsonEncodingType.Reversible), + new EncodingTypeGroup(EncodingType.Json, JsonEncodingType.Compact) + }; + + public static readonly EncodingTypeGroup[] EncodingTypesReversibleCompact = new EncodingTypeGroup[] { + new EncodingTypeGroup(EncodingType.Binary), + new EncodingTypeGroup(EncodingType.Xml), + new EncodingTypeGroup(EncodingType.Json, JsonEncodingType.Reversible), + new EncodingTypeGroup(EncodingType.Json, JsonEncodingType.Compact) + }; + + public static readonly EncodingTypeGroup[] EncodingTypesNonReversibleVerbose = new EncodingTypeGroup[] { + new EncodingTypeGroup(EncodingType.Binary), + new EncodingTypeGroup(EncodingType.Xml), + new EncodingTypeGroup(EncodingType.Json, JsonEncodingType.NonReversible), + new EncodingTypeGroup(EncodingType.Json, JsonEncodingType.Verbose) + }; + + public static readonly EncodingTypeGroup[] EncodingTypesAll = new EncodingTypeGroup[] { + new EncodingTypeGroup(EncodingType.Binary), + new EncodingTypeGroup(EncodingType.Xml), + new EncodingTypeGroup(EncodingType.Json, JsonEncodingType.NonReversible), + new EncodingTypeGroup(EncodingType.Json, JsonEncodingType.Reversible), + new EncodingTypeGroup(EncodingType.Json, JsonEncodingType.Compact), + new EncodingTypeGroup(EncodingType.Json, JsonEncodingType.Verbose) + }; + + public static readonly EncodingTypeGroup[] EncodingTypesAllButJsonNonReversible = new EncodingTypeGroup[] { + new EncodingTypeGroup(EncodingType.Binary), + new EncodingTypeGroup(EncodingType.Xml), + new EncodingTypeGroup(EncodingType.Json, JsonEncodingType.Reversible), + new EncodingTypeGroup(EncodingType.Json, JsonEncodingType.Compact), + new EncodingTypeGroup(EncodingType.Json, JsonEncodingType.Verbose) + }; #endregion #region Protected Methods @@ -157,10 +200,10 @@ protected string EncodeDataValue( BuiltInType builtInType, MemoryStreamType memoryStreamType, object data, - bool useReversibleEncoding = true + JsonEncodingType encoding ) { - string encodeInfo = $"Encoder: {encoderType} Type:{builtInType} Reversible:{useReversibleEncoding}"; + string encodeInfo = $"Encoder: {encoderType} Type:{builtInType} Encoding:{encoding}"; TestContext.Out.WriteLine(encodeInfo); TestContext.Out.WriteLine(data); DataValue expected = CreateDataValue(builtInType, data); @@ -169,7 +212,7 @@ protected string EncodeDataValue( Assert.IsNotNull(expected, "Expected DataValue is Null, " + encodeInfo); using (var encoderStream = CreateEncoderMemoryStream(memoryStreamType)) { - using (IEncoder encoder = CreateEncoder(encoderType, Context, encoderStream, typeof(DataValue), useReversibleEncoding)) + using (IEncoder encoder = CreateEncoder(encoderType, Context, encoderStream, typeof(DataValue), encoding)) { encoder.WriteDataValue("DataValue", expected); } @@ -184,6 +227,7 @@ protected string EncodeDataValue( /// protected void EncodeDecodeDataValue( EncodingType encoderType, + JsonEncodingType jsonEncodingType, BuiltInType builtInType, MemoryStreamType memoryStreamType, object data @@ -194,43 +238,58 @@ object data TestContext.Out.WriteLine(data); DataValue expected = CreateDataValue(builtInType, data); Assert.IsNotNull(expected, "Expected DataValue is Null, " + encodeInfo); - TestContext.Out.WriteLine("Expected:"); - TestContext.Out.WriteLine(expected); - byte[] buffer; - using (var encoderStream = CreateEncoderMemoryStream(memoryStreamType)) + string formatted = null; + DataValue result = null; + try { - using (IEncoder encoder = CreateEncoder(encoderType, Context, encoderStream, typeof(DataValue))) + byte[] buffer; + using (var encoderStream = CreateEncoderMemoryStream(memoryStreamType)) { - encoder.WriteDataValue("DataValue", expected); + using (IEncoder encoder = CreateEncoder(encoderType, Context, encoderStream, typeof(DataValue), jsonEncodingType)) + { + encoder.WriteDataValue("DataValue", expected); + } + buffer = encoderStream.ToArray(); } - buffer = encoderStream.ToArray(); - } - string formatted; - switch (encoderType) - { - case EncodingType.Json: - formatted = PrettifyAndValidateJson(buffer); - break; - case EncodingType.Xml: - formatted = PrettifyAndValidateXml(buffer); - break; - } + switch (encoderType) + { + case EncodingType.Json: + formatted = PrettifyAndValidateJson(buffer); + break; + case EncodingType.Xml: + formatted = PrettifyAndValidateXml(buffer); + break; + } - DataValue result; - using (var decoderStream = new MemoryStream(buffer)) - using (IDecoder decoder = CreateDecoder(encoderType, Context, decoderStream, typeof(DataValue))) - { - result = decoder.ReadDataValue("DataValue"); + using (var decoderStream = new MemoryStream(buffer)) + using (IDecoder decoder = CreateDecoder(encoderType, Context, decoderStream, typeof(DataValue))) + { + result = decoder.ReadDataValue("DataValue"); + } + + Assert.IsNotNull(result, "Resulting DataValue is Null, " + encodeInfo); + expected.Value = AdjustExpectedBoundaryValues(encoderType, builtInType, expected.Value); + Assert.AreEqual(expected, result, encodeInfo); + Assert.IsTrue(Utils.IsEqual(expected, result), "Opc.Ua.Utils.IsEqual failed to compare expected and result. " + encodeInfo); } + catch + { + TestContext.Out.WriteLine("Expected:"); + TestContext.Out.WriteLine(expected); + if (formatted != null) + { + TestContext.Out.WriteLine("Encoded:"); + TestContext.Out.WriteLine(formatted); + } - TestContext.Out.WriteLine("Result:"); - TestContext.Out.WriteLine(result); - Assert.IsNotNull(result, "Resulting DataValue is Null, " + encodeInfo); - expected.Value = AdjustExpectedBoundaryValues(encoderType, builtInType, expected.Value); - Assert.AreEqual(expected, result, encodeInfo); - Assert.IsTrue(Utils.IsEqual(expected, result), "Opc.Ua.Utils.IsEqual failed to compare expected and result. " + encodeInfo); + TestContext.Out.WriteLine("Result:"); + if (result != null) + { + TestContext.Out.WriteLine(result); + } + } } /// @@ -238,6 +297,7 @@ object data /// protected void EncodeDecode( EncodingType encoderType, + JsonEncodingType jsonEncodingType, BuiltInType builtInType, MemoryStreamType memoryStreamType, object expected @@ -253,7 +313,7 @@ object expected byte[] buffer; using (var encoderStream = CreateEncoderMemoryStream(memoryStreamType)) { - using (IEncoder encoder = CreateEncoder(encoderType, Context, encoderStream, type)) + using (IEncoder encoder = CreateEncoder(encoderType, Context, encoderStream, type, jsonEncodingType)) { Encode(encoder, builtInType, builtInType.ToString(), expected); } @@ -279,7 +339,7 @@ object expected { result = Decode(decoder, builtInType, builtInType.ToString(), type); } - + expected = AdjustExpectedBoundaryValues(encoderType, builtInType, expected); if (BuiltInType.DateTime == builtInType) { @@ -309,18 +369,19 @@ protected void EncodeJsonVerifyResult( BuiltInType builtInType, MemoryStreamType memoryStreamType, object data, - bool useReversibleEncoding, + JsonEncodingType jsonEncoding, string expected, bool topLevelIsArray, bool includeDefaults ) { + string result = null; string formattedResult = null; try { - string encodeInfo = $"Encoder: Json Type:{builtInType} Reversible: {useReversibleEncoding}"; + string encodeInfo = $"Encoder: Json Type:{builtInType} Encoding: {jsonEncoding}"; TestContext.Out.WriteLine(encodeInfo); - if (!String.IsNullOrEmpty(expected)) + if (!string.IsNullOrEmpty(expected)) { expected = $"{{\"{builtInType}\":" + expected + "}"; } @@ -337,16 +398,19 @@ bool includeDefaults using (var encoderStream = CreateEncoderMemoryStream(memoryStreamType)) { using (IEncoder encoder = CreateEncoder(EncodingType.Json, Context, encoderStream, typeof(DataValue), - useReversibleEncoding, topLevelIsArray, includeDefaultValues, includeDefaultNumbers)) + jsonEncoding, topLevelIsArray, includeDefaultValues, includeDefaultNumbers)) { - //encoder.SetMappingTables(_nameSpaceUris, _serverUris); + if (jsonEncoding == JsonEncodingType.Reversible || jsonEncoding == JsonEncodingType.NonReversible) + { + // encoder.SetMappingTables(nameSpaceUris, serverUris); + } Encode(encoder, builtInType, builtInType.ToString(), data); } buffer = encoderStream.ToArray(); } TestContext.Out.WriteLine("Result:"); - var result = Encoding.UTF8.GetString(buffer); + result = Encoding.UTF8.GetString(buffer); formattedResult = PrettifyAndValidateJson(result); var jsonLoadSettings = new JsonLoadSettings() { CommentHandling = CommentHandling.Ignore, @@ -364,11 +428,15 @@ bool includeDefaults TestContext.Out.WriteLine("Expected:"); var formattedExpected = PrettifyAndValidateJson(expected); TestContext.Out.WriteLine(formattedExpected); - if (string.IsNullOrEmpty(formattedResult)) + TestContext.Out.WriteLine("Result:"); + if (!string.IsNullOrEmpty(formattedResult)) { - TestContext.Out.WriteLine("Result:"); TestContext.Out.WriteLine(formattedResult); } + else + { + TestContext.Out.WriteLine(result); + } throw; } } @@ -416,15 +484,15 @@ protected string PrettifyAndValidateXml(byte[] xml, bool outputFormatted = false /// /// Format and validate a JSON string. /// - protected string PrettifyAndValidateJson(byte[] json) + public static string PrettifyAndValidateJson(byte[] json, bool outputFormatted = false) { - return PrettifyAndValidateJson(Encoding.UTF8.GetString(json)); + return PrettifyAndValidateJson(Encoding.UTF8.GetString(json), outputFormatted); } /// /// Format and validate a JSON string. /// - protected string PrettifyAndValidateJson(string json, bool outputFormatted = false) + public static string PrettifyAndValidateJson(string json, bool outputFormatted = false) { try { @@ -480,13 +548,12 @@ protected MemoryStream CreateEncoderMemoryStream(MemoryStreamType memoryStreamTy /// /// Encoder factory for all encoding types. /// - /// protected IEncoder CreateEncoder( EncodingType encoderType, IServiceMessageContext context, Stream stream, Type systemType, - bool useReversibleEncoding = true, + JsonEncodingType jsonEncoding = JsonEncodingType.Reversible, bool topLevelIsArray = false, bool includeDefaultValues = false, bool includeDefaultNumbers = true @@ -495,17 +562,21 @@ protected IEncoder CreateEncoder( switch (encoderType) { case EncodingType.Binary: - Assume.That(useReversibleEncoding, "Binary encoding only supports reversible option."); + Assume.That(jsonEncoding == JsonEncodingType.Reversible, "Binary encoding doesn't allow to set the JsonEncodingType."); return new BinaryEncoder(stream, context, true); case EncodingType.Xml: - Assume.That(useReversibleEncoding, "Xml encoding only supports reversible option."); + Assume.That(jsonEncoding == JsonEncodingType.Reversible, "Xml encoding only supports reversible option."); var xmlWriter = XmlWriter.Create(stream, Utils.DefaultXmlWriterSettings()); return new XmlEncoder(systemType, xmlWriter, context); case EncodingType.Json: - return new JsonEncoder(context, useReversibleEncoding, topLevelIsArray, stream, true) { - IncludeDefaultValues = includeDefaultValues, - IncludeDefaultNumberValues = includeDefaultNumbers + var encoder = new JsonEncoder(context, jsonEncoding, topLevelIsArray, stream, true); + // only deprecated encodings allow to set the default value + if (jsonEncoding == JsonEncodingType.Reversible || jsonEncoding == JsonEncodingType.NonReversible) + { + encoder.IncludeDefaultValues = includeDefaultValues; + encoder.IncludeDefaultNumberValues = includeDefaultNumbers; }; + return encoder; default: throw new ArgumentOutOfRangeException(nameof(encoderType), encoderType, "Invalid EncoderType specified."); } diff --git a/Tests/Opc.Ua.Core.Tests/Types/Encoders/EncoderTests.cs b/Tests/Opc.Ua.Core.Tests/Types/Encoders/EncoderTests.cs index ece1cc4bfa..9f95713bac 100644 --- a/Tests/Opc.Ua.Core.Tests/Types/Encoders/EncoderTests.cs +++ b/Tests/Opc.Ua.Core.Tests/Types/Encoders/EncoderTests.cs @@ -33,6 +33,7 @@ using System; using System.IO; +using System.Linq; using System.Text; using System.Xml; using Newtonsoft.Json; @@ -42,6 +43,30 @@ namespace Opc.Ua.Core.Tests.Types.Encoders { + /// + /// A group of encoder types. + /// + public class EncodingTypeGroup : IFormattable + { + public EncodingTypeGroup(EncodingType encoderType, JsonEncodingType jsonEncodingType = JsonEncodingType.Reversible) + { + this.EncoderType = encoderType; + this.JsonEncodingType = jsonEncodingType; + } + + public EncodingType EncoderType { get; } + public JsonEncodingType JsonEncodingType { get; } + + public string ToString(string format, IFormatProvider formatProvider) + { + if (EncoderType == EncodingType.Json) + { + return Utils.Format("{0}:{1}", EncoderType, JsonEncodingType); + } + return Utils.Format("{0}", EncoderType); + } + } + /// /// Tests for the IEncoder and IDecoder class. /// @@ -63,12 +88,14 @@ public class EncoderTests : EncoderCommon [Theory] [Category("BuiltInType")] public void ReEncodeBuiltInTypeDefaultVariantInDataValue( - EncodingType encoderType, - BuiltInType builtInType - ) + [ValueSource(nameof(EncodingTypesReversibleCompact))] + EncodingTypeGroup encoderTypeGroup, + BuiltInType builtInType) { + EncodingType encoderType = encoderTypeGroup.EncoderType; + JsonEncodingType jsonEncodingType = encoderTypeGroup.JsonEncodingType; object defaultValue = TypeInfo.GetDefaultValue(builtInType); - EncodeDecodeDataValue(encoderType, builtInType, MemoryStreamType.MemoryStream, defaultValue); + EncodeDecodeDataValue(encoderType, jsonEncodingType, builtInType, MemoryStreamType.MemoryStream, defaultValue); } /// @@ -78,13 +105,15 @@ BuiltInType builtInType [Theory] [Category("BuiltInType")] public void ReEncodeBuiltInTypeAsVariantInDataValue( - EncodingType encoderType, - BuiltInType builtInType - ) + [ValueSource(nameof(EncodingTypesReversibleCompact))] + EncodingTypeGroup encoderTypeGroup, + BuiltInType builtInType) { + EncodingType encoderType = encoderTypeGroup.EncoderType; + JsonEncodingType jsonEncodingType = encoderTypeGroup.JsonEncodingType; Assume.That(builtInType != BuiltInType.DiagnosticInfo); object randomData = DataGenerator.GetRandom(builtInType); - EncodeDecodeDataValue(encoderType, builtInType, MemoryStreamType.ArraySegmentStream, randomData); + EncodeDecodeDataValue(encoderType, jsonEncodingType, builtInType, MemoryStreamType.ArraySegmentStream, randomData); } /// @@ -93,10 +122,12 @@ BuiltInType builtInType [Theory] [Category("BuiltInType"), Repeat(kRandomRepeats)] public void ReEncodeBuiltInType( - EncodingType encoderType, - BuiltInType builtInType - ) + [ValueSource(nameof(EncodingTypesReversibleCompact))] + EncodingTypeGroup encoderTypeGroup, + BuiltInType builtInType) { + EncodingType encoderType = encoderTypeGroup.EncoderType; + JsonEncodingType jsonEncodingType = encoderTypeGroup.JsonEncodingType; SetRepeatedRandomSeed(); object randomData = null; bool getRandom = true; @@ -128,7 +159,7 @@ BuiltInType builtInType break; } }; - EncodeDecode(encoderType, builtInType, MemoryStreamType.ArraySegmentStream, randomData); + EncodeDecode(encoderType, jsonEncodingType, builtInType, MemoryStreamType.ArraySegmentStream, randomData); } /// @@ -137,10 +168,12 @@ BuiltInType builtInType [Theory] [Category("BuiltInType")] public void ReEncodeBuiltInTypeDefaultValue( - EncodingType encoderType, - BuiltInType builtInType - ) + [ValueSource(nameof(EncodingTypesReversibleCompact))] + EncodingTypeGroup encoderTypeGroup, + BuiltInType builtInType) { + EncodingType encoderType = encoderTypeGroup.EncoderType; + JsonEncodingType jsonEncodingType = encoderTypeGroup.JsonEncodingType; object randomData = TypeInfo.GetDefaultValue(builtInType); if (builtInType == BuiltInType.ExtensionObject) { @@ -148,7 +181,7 @@ BuiltInType builtInType // or encoding of extension objects fails. randomData = ExtensionObject.Null; } - EncodeDecode(encoderType, builtInType, MemoryStreamType.RecyclableMemoryStream, randomData); + EncodeDecode(encoderType, jsonEncodingType, builtInType, MemoryStreamType.RecyclableMemoryStream, randomData); } /// @@ -157,14 +190,16 @@ BuiltInType builtInType [Theory] [Category("BuiltInType")] public void ReEncodeBuiltInTypeBoundaryValue( - EncodingType encoderType, - BuiltInType builtInType - ) + [ValueSource(nameof(EncodingTypesReversibleCompact))] + EncodingTypeGroup encoderTypeGroup, + BuiltInType builtInType) { + EncodingType encoderType = encoderTypeGroup.EncoderType; + JsonEncodingType jsonEncodingType = encoderTypeGroup.JsonEncodingType; Array boundaryValues = DataGenerator.GetRandomArray(builtInType, true, 10, true); foreach (var boundaryValue in boundaryValues) { - EncodeDecode(encoderType, builtInType, MemoryStreamType.MemoryStream, boundaryValue); + EncodeDecode(encoderType, jsonEncodingType, builtInType, MemoryStreamType.MemoryStream, boundaryValue); } } @@ -175,16 +210,19 @@ BuiltInType builtInType [Theory] [Category("BuiltInType")] public void ReEncodeBuiltInTypeArrayAsRandomVariantInDataValue( - EncodingType encoderType, + [ValueSource(nameof(EncodingTypesReversibleCompact))] + EncodingTypeGroup encoderTypeGroup, BuiltInType builtInType, bool useBoundaryValues, int arrayLength ) { + EncodingType encoderType = encoderTypeGroup.EncoderType; + JsonEncodingType jsonEncodingType = encoderTypeGroup.JsonEncodingType; // ensure different sized arrays contain different data set SetRandomSeed(arrayLength); object randomData = DataGenerator.GetRandomArray(builtInType, useBoundaryValues, arrayLength, true); - EncodeDecodeDataValue(encoderType, builtInType, MemoryStreamType.ArraySegmentStream, randomData); + EncodeDecodeDataValue(encoderType, jsonEncodingType, builtInType, MemoryStreamType.ArraySegmentStream, randomData); } /// @@ -194,12 +232,15 @@ int arrayLength [Theory] [Category("BuiltInType")] public void ReEncodeBuiltInTypeZeroLengthArrayAsVariantInDataValue( - EncodingType encoderType, + [ValueSource(nameof(EncodingTypesReversibleCompact))] + EncodingTypeGroup encoderTypeGroup, BuiltInType builtInType ) { + EncodingType encoderType = encoderTypeGroup.EncoderType; + JsonEncodingType jsonEncodingType = encoderTypeGroup.JsonEncodingType; object randomData = DataGenerator.GetRandomArray(builtInType, false, 0, true); - EncodeDecodeDataValue(encoderType, builtInType, MemoryStreamType.RecyclableMemoryStream, randomData); + EncodeDecodeDataValue(encoderType, jsonEncodingType, builtInType, MemoryStreamType.RecyclableMemoryStream, randomData); } /// @@ -209,12 +250,15 @@ BuiltInType builtInType [Theory] [Category("BuiltInType"), Repeat(kRandomRepeats)] public void ReEncodeBuiltInTypeRandomVariantInDataValue( - EncodingType encoderType + [ValueSource(nameof(EncodingTypesReversibleCompact))] + EncodingTypeGroup encoderTypeGroup ) { + EncodingType encoderType = encoderTypeGroup.EncoderType; + JsonEncodingType jsonEncodingType = encoderTypeGroup.JsonEncodingType; SetRepeatedRandomSeed(); object randomData = DataGenerator.GetRandom(BuiltInType.Variant); - EncodeDecodeDataValue(encoderType, BuiltInType.Variant, MemoryStreamType.MemoryStream, randomData); + EncodeDecodeDataValue(encoderType, jsonEncodingType, BuiltInType.Variant, MemoryStreamType.MemoryStream, randomData); } /// @@ -232,11 +276,34 @@ BuiltInType builtInType { Assert.Throws( typeof(ServiceResultException), - () => EncodeDataValue(EncodingType.Json, builtInType, MemoryStreamType.ArraySegmentStream, randomData, false) + () => EncodeDataValue(EncodingType.Json, builtInType, MemoryStreamType.ArraySegmentStream, randomData, JsonEncodingType.NonReversible) + ); + return; + } + string json = EncodeDataValue(EncodingType.Json, builtInType, MemoryStreamType.MemoryStream, randomData, JsonEncodingType.NonReversible); + PrettifyAndValidateJson(json); + } + + /// + /// Validate integrity of non reversible Json encoding + /// of a builtin type as Variant in a DataValue. + /// + [Theory] + [Category("BuiltInType")] + public void EncodeBuiltInTypeAsVariantInDataValueToVerboseJson( + BuiltInType builtInType + ) + { + object randomData = DataGenerator.GetRandom(builtInType); + if (builtInType == BuiltInType.DiagnosticInfo) + { + Assert.Throws( + typeof(ServiceResultException), + () => EncodeDataValue(EncodingType.Json, builtInType, MemoryStreamType.ArraySegmentStream, randomData, JsonEncodingType.Verbose) ); return; } - string json = EncodeDataValue(EncodingType.Json, builtInType, MemoryStreamType.MemoryStream, randomData, false); + string json = EncodeDataValue(EncodingType.Json, builtInType, MemoryStreamType.MemoryStream, randomData, JsonEncodingType.Verbose); PrettifyAndValidateJson(json); } @@ -254,7 +321,7 @@ int arrayLength { SetRandomSeed(arrayLength); object randomData = DataGenerator.GetRandomArray(builtInType, useBoundaryValues, arrayLength, true); - string json = EncodeDataValue(EncodingType.Json, builtInType, MemoryStreamType.RecyclableMemoryStream, randomData, false); + string json = EncodeDataValue(EncodingType.Json, builtInType, MemoryStreamType.RecyclableMemoryStream, randomData, JsonEncodingType.NonReversible); PrettifyAndValidateJson(json); } @@ -269,7 +336,41 @@ BuiltInType builtInType ) { object randomData = DataGenerator.GetRandomArray(builtInType, false, 0, true); - string json = EncodeDataValue(EncodingType.Json, builtInType, MemoryStreamType.MemoryStream, randomData, false); + string json = EncodeDataValue(EncodingType.Json, builtInType, MemoryStreamType.MemoryStream, randomData, JsonEncodingType.NonReversible); + PrettifyAndValidateJson(json); + } + + + /// + /// Validate integrity of non reversible Json encoding + /// of a builtin type array as Variant in a DataValue. + /// + [Theory] + [Category("BuiltInType")] + public void EncodeBuiltInTypeArrayAsVariantInDataValueToVerboseJson( + BuiltInType builtInType, + bool useBoundaryValues, + int arrayLength + ) + { + SetRandomSeed(arrayLength); + object randomData = DataGenerator.GetRandomArray(builtInType, useBoundaryValues, arrayLength, true); + string json = EncodeDataValue(EncodingType.Json, builtInType, MemoryStreamType.RecyclableMemoryStream, randomData, JsonEncodingType.Verbose); + PrettifyAndValidateJson(json); + } + + /// + /// Verify non reversible Json encoding + /// of a builtin type array as Variant in a DataValue. + /// + [Theory] + [Category("BuiltInType")] + public void EncodeBuiltInTypeZeroLengthArrayAsVariantInDataValueToVerboseJson( + BuiltInType builtInType + ) + { + object randomData = DataGenerator.GetRandomArray(builtInType, false, 0, true); + string json = EncodeDataValue(EncodingType.Json, builtInType, MemoryStreamType.MemoryStream, randomData, JsonEncodingType.Verbose); PrettifyAndValidateJson(json); } @@ -279,9 +380,11 @@ BuiltInType builtInType [Theory] [Category("BuiltInType")] public void ReEncodeVariantCollectionInDataValue( - EncodingType encoderType - ) + [ValueSource(nameof(EncodingTypesReversibleCompact))] + EncodingTypeGroup encoderTypeGroup) { + EncodingType encoderType = encoderTypeGroup.EncoderType; + JsonEncodingType jsonEncodingType = encoderTypeGroup.JsonEncodingType; var variant = new VariantCollection { new Variant(4L), new Variant("test"), @@ -292,7 +395,7 @@ EncodingType encoderType //new Variant(new TestEnumType[] { TestEnumType.One, TestEnumType.Two, TestEnumType.Hundred }), new Variant(new Int32[] { 2, 3, 10 }, new TypeInfo(BuiltInType.Enumeration, 1)) }; - EncodeDecodeDataValue(encoderType, BuiltInType.Variant, MemoryStreamType.ArraySegmentStream, variant); + EncodeDecodeDataValue(encoderType, jsonEncodingType, BuiltInType.Variant, MemoryStreamType.ArraySegmentStream, variant); } [Test] @@ -416,16 +519,19 @@ public void JsonEncoder_WriteByteString() [Theory] [Category("Array"), Repeat(kArrayRepeats)] public void ReEncodeVariantArrayInDataValue( - EncodingType encoderType, + [ValueSource(nameof(EncodingTypesAllButJsonNonReversible))] + EncodingTypeGroup encoderTypeGroup, BuiltInType builtInType ) { + EncodingType encoderType = encoderTypeGroup.EncoderType; + JsonEncodingType jsonEncodingType = encoderTypeGroup.JsonEncodingType; SetRepeatedRandomSeed(); Assume.That(builtInType != BuiltInType.Null); int arrayDimension = RandomSource.NextInt32(99) + 1; Array randomData = DataGenerator.GetRandomArray(builtInType, false, arrayDimension, true); var variant = new Variant(randomData, new TypeInfo(builtInType, 1)); - EncodeDecodeDataValue(encoderType, BuiltInType.Variant, MemoryStreamType.RecyclableMemoryStream, variant); + EncodeDecodeDataValue(encoderType, jsonEncodingType, BuiltInType.Variant, MemoryStreamType.RecyclableMemoryStream, variant); } /// @@ -434,10 +540,12 @@ BuiltInType builtInType [Theory] [Category("Array"), Repeat(kArrayRepeats)] public void EncodeArray( - EncodingType encoderType, - BuiltInType builtInType - ) + [ValueSource(nameof(EncodingTypesAll))] + EncodingTypeGroup encoderTypeGroup, + BuiltInType builtInType) { + EncodingType encoderType = encoderTypeGroup.EncoderType; + JsonEncodingType jsonEncodingType = encoderTypeGroup.JsonEncodingType; SetRepeatedRandomSeed(); Assume.That(builtInType != BuiltInType.Null); int arrayDimension = RandomSource.NextInt32(99) + 1; @@ -452,7 +560,7 @@ BuiltInType builtInType byte[] buffer; using (var encoderStream = CreateEncoderMemoryStream(MemoryStreamType.MemoryStream)) { - using (IEncoder encoder = CreateEncoder(encoderType, Context, encoderStream, type, true, false)) + using (IEncoder encoder = CreateEncoder(encoderType, Context, encoderStream, type, jsonEncodingType, false)) { encoder.WriteArray(builtInType.ToString(), randomData, ValueRanks.OneDimension, builtInType); } @@ -477,6 +585,20 @@ BuiltInType builtInType TestContext.Out.WriteLine(result); object expected = AdjustExpectedBoundaryValues(encoderType, builtInType, randomData); + // strip the locale information from localized text for non reversible + if (builtInType == BuiltInType.LocalizedText && jsonEncodingType == JsonEncodingType.NonReversible) + { + var localizedTextCollection = new LocalizedTextCollection(randomData.Length); + foreach (var entry in randomData) + { + if (entry is LocalizedText localizedText) + { + localizedTextCollection.Add(new LocalizedText(null, localizedText.Text)); + } + } + expected = localizedTextCollection.ToArray(); + } + Assert.AreEqual(expected, result, encodeInfo); Assert.IsTrue(Utils.IsEqual(expected, result), "Opc.Ua.Utils.IsEqual failed to compare expected and result. " + encodeInfo); } @@ -487,10 +609,12 @@ BuiltInType builtInType [Theory] [Category("Matrix"), Repeat(kArrayRepeats)] public void ReEncodeVariantMatrixInDataValue( - EncodingType encoderType, - BuiltInType builtInType - ) + [ValueSource(nameof(EncodingTypesReversibleCompact))] + EncodingTypeGroup encoderTypeGroup, + BuiltInType builtInType) { + EncodingType encoderType = encoderTypeGroup.EncoderType; + JsonEncodingType jsonEncodingType = encoderTypeGroup.JsonEncodingType; SetRepeatedRandomSeed(); Assume.That(builtInType != BuiltInType.Null); // reduce array dimension for arrays with large values @@ -506,7 +630,7 @@ BuiltInType builtInType int elements = ElementsFromDimension(dimensions); Array randomData = DataGenerator.GetRandomArray(builtInType, false, elements, true); var variant = new Variant(new Matrix(randomData, builtInType, dimensions)); - EncodeDecodeDataValue(encoderType, BuiltInType.Variant, MemoryStreamType.RecyclableMemoryStream, variant); + EncodeDecodeDataValue(encoderType, jsonEncodingType, BuiltInType.Variant, MemoryStreamType.RecyclableMemoryStream, variant); } /// @@ -514,10 +638,13 @@ BuiltInType builtInType /// [Theory] [Category("Matrix"), Repeat(kArrayRepeats)] - public void EncodeBuiltInTypeMatrixAsVariantInDataValueToNonReversibleJson( - BuiltInType builtInType - ) + public void EncodeBuiltInTypeMatrixAsVariantInDataValueToNonReversibleVerboseJson( + [ValueSource(nameof(EncodingTypesJsonNonReversibleVerbose))] + EncodingTypeGroup encoderTypeGroup, + BuiltInType builtInType) { + EncodingType encoderType = encoderTypeGroup.EncoderType; + JsonEncodingType jsonEncodingType = encoderTypeGroup.JsonEncodingType; SetRepeatedRandomSeed(); Assume.That(builtInType != BuiltInType.Null); int matrixDimension = RandomSource.NextInt32(3) + 2; @@ -526,7 +653,7 @@ BuiltInType builtInType int elements = ElementsFromDimension(dimensions); Array randomData = DataGenerator.GetRandomArray(builtInType, false, elements, true); var variant = new Variant(new Matrix(randomData, builtInType, dimensions)); - string json = EncodeDataValue(EncodingType.Json, BuiltInType.Variant, MemoryStreamType.ArraySegmentStream, variant, false); + string json = EncodeDataValue(encoderType, BuiltInType.Variant, MemoryStreamType.ArraySegmentStream, variant, jsonEncodingType); _ = PrettifyAndValidateJson(json); } @@ -536,9 +663,12 @@ BuiltInType builtInType [Theory] [Category("Matrix"), Repeat(kArrayRepeats)] public void EncodeMatrixInArray( - EncodingType encoderType, + [ValueSource(nameof(EncodingTypesAll))] + EncodingTypeGroup encoderTypeGroup, BuiltInType builtInType) { + EncodingType encoderType = encoderTypeGroup.EncoderType; + JsonEncodingType jsonEncodingType = encoderTypeGroup.JsonEncodingType; SetRepeatedRandomSeed(); Assume.That(builtInType != BuiltInType.Null); int matrixDimension = RandomSource.NextInt32(3) + 2; @@ -557,7 +687,7 @@ public void EncodeMatrixInArray( byte[] buffer; using (var encoderStream = CreateEncoderMemoryStream(MemoryStreamType.MemoryStream)) { - using (IEncoder encoder = CreateEncoder(encoderType, Context, encoderStream, type)) + using (IEncoder encoder = CreateEncoder(encoderType, Context, encoderStream, type, jsonEncodingType)) { encoder.WriteArray(builtInType.ToString(), matrix, matrix.TypeInfo.ValueRank, builtInType); } @@ -582,6 +712,26 @@ public void EncodeMatrixInArray( TestContext.Out.WriteLine(result); object expected = AdjustExpectedBoundaryValues(encoderType, builtInType, matrix); + // strip the locale information from localized text for non reversible + if (builtInType == BuiltInType.LocalizedText && jsonEncodingType == JsonEncodingType.NonReversible) + { + var localizedTextCollection = new LocalizedTextCollection(randomData.Length); + foreach (var entry in matrix.Elements) + { + if (entry is LocalizedText localizedText) + { + localizedTextCollection.Add(new LocalizedText(null, localizedText.Text)); + } + } + + // only compare the text portion + if (result is Array resultArray) + { + expected = localizedTextCollection.ToArray(); + result = resultArray.OfType().ToArray(); + } + } + Assert.AreEqual(expected, result, encodeInfo); Assert.IsTrue(Utils.IsEqual(expected, result), "Opc.Ua.Utils.IsEqual failed to compare expected and result. " + encodeInfo); } @@ -592,10 +742,12 @@ public void EncodeMatrixInArray( [Theory] [Category("Matrix")] public void MatrixOverflow( - EncodingType encoderType, - BuiltInType builtInType - ) + [ValueSource(nameof(EncodingTypesAll))] + EncodingTypeGroup encoderTypeGroup, + BuiltInType builtInType) { + EncodingType encoderType = encoderTypeGroup.EncoderType; + JsonEncodingType jsonEncodingType = encoderTypeGroup.JsonEncodingType; Assume.That(builtInType != BuiltInType.Null); int matrixDimension = RandomSource.NextInt32(8) + 2; int[] dimensions = new int[matrixDimension]; @@ -603,6 +755,7 @@ BuiltInType builtInType int elements = ElementsFromDimension(dimensions); Array randomData = DataGenerator.GetRandomArray(builtInType, false, elements, true); + // create an invalid matrix to validate that the dimension overflow is catched var matrix = new Matrix(randomData, builtInType, dimensions); for (int ii = 0; ii < matrixDimension; ii++) { @@ -629,9 +782,18 @@ BuiltInType builtInType byte[] buffer; using (var encoderStream = CreateEncoderMemoryStream(MemoryStreamType.MemoryStream)) { - using (IEncoder encoder = CreateEncoder(encoderType, Context, encoderStream, typeof(DataValue))) + using (IEncoder encoder = CreateEncoder(encoderType, Context, encoderStream, typeof(DataValue), jsonEncodingType)) { - encoder.WriteDataValue("DataValue", expected); + if (encoderType == EncodingType.Json && jsonEncodingType == JsonEncodingType.NonReversible) + { + var sre = Assert.Throws(() => encoder.WriteDataValue("DataValue", expected)); + Assert.AreEqual(StatusCodes.BadEncodingLimitsExceeded, sre.StatusCode); + return; + } + else + { + encoder.WriteDataValue("DataValue", expected); + } } buffer = encoderStream.ToArray(); } @@ -663,10 +825,12 @@ BuiltInType builtInType [Theory] [Category("Matrix")] public void MatrixOverflowStaticDimensions( - EncodingType encoderType, - BuiltInType builtInType - ) + [ValueSource(nameof(EncodingTypesAll))] + EncodingTypeGroup encoderTypeGroup, + BuiltInType builtInType) { + EncodingType encoderType = encoderTypeGroup.EncoderType; + JsonEncodingType jsonEncodingType = encoderTypeGroup.JsonEncodingType; Assume.That(builtInType != BuiltInType.Null); int matrixDimension = 5; int[] dimensions = new int[matrixDimension]; @@ -681,7 +845,6 @@ BuiltInType builtInType matrix.Dimensions[3] = 14087; matrix.Dimensions[4] = 20446; - var variant = new Variant(matrix); string encodeInfo = $"Encoder: {encoderType} Type:{builtInType}"; @@ -695,9 +858,18 @@ BuiltInType builtInType byte[] buffer; using (var encoderStream = CreateEncoderMemoryStream(MemoryStreamType.ArraySegmentStream)) { - using (IEncoder encoder = CreateEncoder(encoderType, Context, encoderStream, typeof(DataValue))) + using (IEncoder encoder = CreateEncoder(encoderType, Context, encoderStream, typeof(DataValue), jsonEncodingType)) { - encoder.WriteDataValue("DataValue", expected); + if (encoderType == EncodingType.Json && jsonEncodingType == JsonEncodingType.NonReversible) + { + var sre = Assert.Throws(() => encoder.WriteDataValue("DataValue", expected)); + Assert.AreEqual(StatusCodes.BadEncodingLimitsExceeded, sre.StatusCode); + return; + } + else + { + encoder.WriteDataValue("DataValue", expected); + } } buffer = encoderStream.ToArray(); } @@ -730,10 +902,12 @@ BuiltInType builtInType [Theory] [Category("Matrix")] public void EncodeMatrixInArrayOverflow( - EncodingType encoderType, - BuiltInType builtInType - ) + [ValueSource(nameof(EncodingTypesAll))] + EncodingTypeGroup encoderTypeGroup, + BuiltInType builtInType) { + EncodingType encoderType = encoderTypeGroup.EncoderType; + JsonEncodingType jsonEncodingType = encoderTypeGroup.JsonEncodingType; Assume.That(builtInType != BuiltInType.Null); int matrixDimension = RandomSource.NextInt32(8) + 2; int[] dimensions = new int[matrixDimension]; @@ -763,7 +937,7 @@ BuiltInType builtInType byte[] buffer; using (var encoderStream = CreateEncoderMemoryStream(MemoryStreamType.RecyclableMemoryStream)) { - using (IEncoder encoder = CreateEncoder(encoderType, Context, encoderStream, type)) + using (IEncoder encoder = CreateEncoder(encoderType, Context, encoderStream, type, jsonEncodingType)) { switch (encoderType) { diff --git a/Tests/Opc.Ua.Core.Tests/Types/Encoders/JsonEncoderCompactTests.cs b/Tests/Opc.Ua.Core.Tests/Types/Encoders/JsonEncoderCompactTests.cs new file mode 100644 index 0000000000..367ce4d8e7 --- /dev/null +++ b/Tests/Opc.Ua.Core.Tests/Types/Encoders/JsonEncoderCompactTests.cs @@ -0,0 +1,1532 @@ +/* ======================================================================== + * Copyright (c) 2005-2024 The OPC Foundation, Inc. All rights reserved. + * + * OPC Foundation MIT License 1.00 + * + * Permission is hereby granted, free of charge, to any person + * obtaining a copy of this software and associated documentation + * files (the "Software"), to deal in the Software without + * restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following + * conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES + * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT + * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, + * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR + * OTHER DEALINGS IN THE SOFTWARE. + * + * The complete license agreement can be found here: + * http://opcfoundation.org/License/MIT/1.00/ + * ======================================================================*/ + +using System; +using System.Collections.Generic; +using System.Globalization; +using System.IO; +using System.Linq; +using System.Text; +using BenchmarkDotNet.Attributes; +using Microsoft.IO; +using NUnit.Framework; +using Assert = NUnit.Framework.Legacy.ClassicAssert; +using Opc.Ua.Bindings; +using Newtonsoft.Json.Linq; +using Newtonsoft.Json; + +namespace Opc.Ua.Core.Tests.Types.Encoders +{ + /// + /// Tests for the Json encoder and decoder class. + /// + [TestFixture, Category("JsonEncoder")] + [SetCulture("en-us"), SetUICulture("en-us")] + [Parallelizable] + [MemoryDiagnoser] + [DisassemblyDiagnoser] + public class JsonEncoderCompactTests + { + const string NamespaceUri1 = "http://test.org/UA/Data1/"; + const string NamespaceUri2 = "tag:test.org,2024-07:schema:data:2"; + const string NamespaceUri3 = "urn:test.org:2024-07:schema:data:3"; + static readonly string[] NamespaceUris = new string[] { NamespaceUri1, NamespaceUri2, NamespaceUri3 }; + + const string ServerUri1 = "http://test.org/product/server1/"; + const string ServerUri2 = "tag:test.org,2024-07:product:server:2"; + const string ServerUri3 = "urn:test.org:2024-07:product:server:3"; + static readonly string[] ServerUris = new string[] { ServerUri1, ServerUri2, ServerUri3 }; + + const uint NumericId1 = 1234; + const uint NumericId2 = 5678; + const uint NumericId3 = 9876; + static readonly uint[] NumericIds = new uint[] { NumericId1, NumericId2, NumericId3 }; + + const string StringId1 = @"{""World"": ""Pandora""}"; + const string StringId2 = @"Pandora"; + const string StringId3 = @"http://world.org/Pandora/?alien=blue"; + static readonly string[] StringIds = new string[] { StringId1, StringId2, StringId3 }; + + static readonly Guid GuidId1 = new Guid("73861B2D-EA9A-4B97-ACE6-9A2943EF641E"); + static readonly Guid GuidId2 = new Guid("BCFE58C8-CDC5-444F-B1F8-A12903008BE0"); + static readonly Guid GuidId3 = new Guid("C141B9D1-F1FD-4D15-9918-E37FD697EA1D"); + static readonly Guid[] GuidIds = new Guid[] { GuidId1, GuidId2, GuidId3 }; + + static readonly byte[] OpaqueId1 = Utils.FromHexString("138DAA907373409AB6A4A36322063745"); + static readonly byte[] OpaqueId2 = Utils.FromHexString("E41047609A9248318EB907991A66B7BEE6B60CB5114828"); + static readonly byte[] OpaqueId3 = Utils.FromHexString("FBD8F0DE652A479B"); + static readonly byte[][] OpaqueIds = new byte[][] { OpaqueId1, OpaqueId2, OpaqueId3 }; + + private string Escape(string input) + { + return input.Replace("\\", "\\\\").Replace("\"", "\\\""); + } + + private string ToString(Array input, int index) + { + var element = input.GetValue(index % input.Length); + + if (element is byte[] oid) + { + return Convert.ToBase64String(oid); + } + + if (element is string sid) + { + return Escape(sid); + } + + return (element != null) ? element.ToString() : String.Empty; + } + + private T Get(IList input, int index) + { + return input[index % input.Count]; + } + + private void CheckDecodedNodeIds(ServiceMessageContext context, JsonDecoder decoder, int index) + { + var n0 = decoder.ReadNodeId("D0"); + Assert.AreEqual((int)n0.NamespaceIndex, 0); + Assert.AreEqual(2263U, (uint)n0.Identifier); + + var n1 = decoder.ReadNodeId("D1"); + Assert.AreEqual((int)n1.NamespaceIndex, context.NamespaceUris.GetIndex(Get(NamespaceUris, index))); + Assert.AreEqual(Get(NumericIds, index), (uint)n1.Identifier); + + var n2 = decoder.ReadNodeId("D2"); + Assert.AreEqual((int)n2.NamespaceIndex, context.NamespaceUris.GetIndex(Get(NamespaceUris, index + 1))); + Assert.AreEqual(Get(StringIds, index), (string)n2.Identifier); + + var n3 = decoder.ReadNodeId("D3"); + Assert.AreEqual((int)n3.NamespaceIndex, context.NamespaceUris.GetIndex(Get(NamespaceUris, index + 2))); + Assert.AreEqual(Get(GuidIds, index), (Guid)n3.Identifier); + + var n4 = decoder.ReadNodeId("D4"); + Assert.AreEqual((int)n4.NamespaceIndex, context.NamespaceUris.GetIndex(Get(NamespaceUris, index + 3))); + Assert.AreEqual(Utils.ToHexString(Get(OpaqueIds, index)), Utils.ToHexString((byte[])n4.Identifier)); + } + + private void CheckDecodedExpandedNodeIds(ServiceMessageContext context, JsonDecoder decoder, int index) + { + var n0 = decoder.ReadExpandedNodeId("D0"); + Assert.AreEqual((int)n0.ServerIndex, 0); + Assert.AreEqual((int)n0.NamespaceIndex, 0); + Assert.AreEqual(2263U, (uint)n0.Identifier); + + var n1 = decoder.ReadExpandedNodeId("D1"); + Assert.AreEqual((int)n1.ServerIndex, 0); + + var uri = Get(NamespaceUris, index); + var ns = context.NamespaceUris.GetIndex(uri); + if (ns < 0) Assert.AreEqual(n1.NamespaceUri, uri); + else Assert.AreEqual(n1.NamespaceIndex, ns); + + Assert.AreEqual(Get(NumericIds, index), (uint)n1.Identifier); + + var n2 = decoder.ReadExpandedNodeId("D2"); + Assert.AreEqual((int)n2.ServerIndex, 0); + + uri = Get(NamespaceUris, index + 1); + ns = context.NamespaceUris.GetIndex(uri); + if (ns < 0) Assert.AreEqual(n2.NamespaceUri, uri); + else Assert.AreEqual(n2.NamespaceIndex, ns); + + Assert.AreEqual(Get(StringIds, index), (string)n2.Identifier); + + var n3 = decoder.ReadExpandedNodeId("D3"); + Assert.AreEqual((int)n3.ServerIndex, 0); + + uri = Get(NamespaceUris, index + 2); + ns = context.NamespaceUris.GetIndex(uri); + if (ns < 0) Assert.AreEqual(n3.NamespaceUri, uri); + else Assert.AreEqual(n3.NamespaceIndex, ns); + + Assert.AreEqual(Get(GuidIds, index), (Guid)n3.Identifier); + + var n4 = decoder.ReadExpandedNodeId("D4"); + Assert.AreEqual((int)n4.ServerIndex, 0); + + uri = Get(NamespaceUris, index + 3); + ns = context.NamespaceUris.GetIndex(uri); + if (ns < 0) Assert.AreEqual(n4.NamespaceUri, uri); + else Assert.AreEqual(n4.NamespaceIndex, ns); + + Assert.AreEqual(Utils.ToHexString(Get(OpaqueIds, index)), Utils.ToHexString((byte[])n4.Identifier)); + + var n5 = decoder.ReadExpandedNodeId("D5"); + Assert.AreEqual((int)n5.ServerIndex, context.ServerUris.GetIndex(Get(ServerUris, index))); + + uri = Get(NamespaceUris, index); + ns = context.NamespaceUris.GetIndex(uri); + if (ns < 0) Assert.AreEqual(n5.NamespaceUri, uri); + else Assert.AreEqual(n5.NamespaceIndex, ns); + + Assert.AreEqual(Get(NumericIds, index), (uint)n5.Identifier); + + var n6 = decoder.ReadExpandedNodeId("D6"); + Assert.AreEqual((int)n6.ServerIndex, context.ServerUris.GetIndex(Get(ServerUris, index + 1))); + + uri = Get(NamespaceUris, index + 1); + ns = context.NamespaceUris.GetIndex(uri); + if (ns < 0) Assert.AreEqual(n6.NamespaceUri, uri); + else Assert.AreEqual(n6.NamespaceIndex, ns); + + Assert.AreEqual(Get(StringIds, index), (string)n6.Identifier); + + var n7 = decoder.ReadExpandedNodeId("D7"); + Assert.AreEqual((int)n7.ServerIndex, context.ServerUris.GetIndex(Get(ServerUris, index + 2))); + + uri = Get(NamespaceUris, index + 2); + ns = context.NamespaceUris.GetIndex(uri); + if (ns < 0) Assert.AreEqual(n7.NamespaceUri, uri); + else Assert.AreEqual(n7.NamespaceIndex, ns); + + Assert.AreEqual(Get(GuidIds, index), (Guid)n7.Identifier); + + var n8 = decoder.ReadExpandedNodeId("D8"); + Assert.AreEqual((int)n8.ServerIndex, context.ServerUris.GetIndex(Get(ServerUris, index + 3))); + + uri = Get(NamespaceUris, index + 3); + ns = context.NamespaceUris.GetIndex(uri); + if (ns < 0) Assert.AreEqual(n8.NamespaceUri, uri); + else Assert.AreEqual(n8.NamespaceIndex, ns); + + Assert.AreEqual(Utils.ToHexString(Get(OpaqueIds, index)), Utils.ToHexString((byte[])n8.Identifier)); + } + + private void CheckDecodedQualfiiedNames(ServiceMessageContext context, JsonDecoder decoder, int index) + { + var n0 = decoder.ReadQualifiedName("D0"); + Assert.AreEqual((int)n0.NamespaceIndex, 0); + Assert.AreEqual("ServerStatus", n0.Name); + + var n1 = decoder.ReadQualifiedName("D1"); + Assert.AreEqual((int)n1.NamespaceIndex, context.NamespaceUris.GetIndex(Get(NamespaceUris, index))); + Assert.AreEqual("N1", n1.Name); + + var n2 = decoder.ReadQualifiedName("D2"); + Assert.AreEqual((int)n2.NamespaceIndex, context.NamespaceUris.GetIndex(Get(NamespaceUris, index + 1))); + Assert.AreEqual("N2", n2.Name); + + var n3 = decoder.ReadQualifiedName("D3"); + Assert.AreEqual((int)n3.NamespaceIndex, context.NamespaceUris.GetIndex(Get(NamespaceUris, index + 2))); + Assert.AreEqual("N3", n3.Name); + + var n4 = decoder.ReadQualifiedName("D4"); + Assert.AreEqual((int)n4.NamespaceIndex, context.NamespaceUris.GetIndex(Get(NamespaceUris, index + 3))); + Assert.AreEqual("N4", n4.Name); + } + + [Test] + [TestCase(0)] + [TestCase(1)] + [TestCase(2)] + public void DecodeCompactAndVerboseNodeId(int index) + { + var data = $@" + {{ + ""D0"": ""i=2263"", + ""D1"": ""nsu={ToString(NamespaceUris, index)};i={ToString(NumericIds, index)}"", + ""D2"": ""nsu={ToString(NamespaceUris, index + 1)};s={ToString(StringIds, index)}"", + ""D3"": ""nsu={ToString(NamespaceUris, index + 2)};g={ToString(GuidIds, index)}"", + ""D4"": ""nsu={ToString(NamespaceUris, index + 3)};b={ToString(OpaqueIds, index)}"" + }} + "; + + var context = new ServiceMessageContext(); + + using (var decoder = new JsonDecoder(data, context)) + { + decoder.UpdateNamespaceTable = true; + CheckDecodedNodeIds(context, decoder, index); + } + } + + [Test] + [TestCase(0, JsonEncodingType.Verbose)] + [TestCase(1, JsonEncodingType.Verbose)] + [TestCase(2, JsonEncodingType.Verbose)] + [TestCase(0, JsonEncodingType.Compact)] + [TestCase(1, JsonEncodingType.Compact)] + [TestCase(2, JsonEncodingType.Compact)] + public void EncodeCompactOrVerboseNodeId(int index, JsonEncodingType jsonEncoding) + { + var data = $@" + {{ + ""D0"": ""i=2263"", + ""D1"": ""nsu={ToString(NamespaceUris, index)};i={ToString(NumericIds, index)}"", + ""D2"": ""nsu={ToString(NamespaceUris, index + 1)};s={ToString(StringIds, index)}"", + ""D3"": ""nsu={ToString(NamespaceUris, index + 2)};g={ToString(GuidIds, index)}"", + ""D4"": ""nsu={ToString(NamespaceUris, index + 3)};b={ToString(OpaqueIds, index)}"", + }} + "; + + JObject jsonObj = JObject.Parse(data); + string expected = JsonConvert.SerializeObject(jsonObj, Formatting.None); + EncoderCommon.PrettifyAndValidateJson(expected, true); + + var context = new ServiceMessageContext(); + Array.ForEach(NamespaceUris, x => context.NamespaceUris.Append(x)); + + using (var encoder = new JsonEncoder(context, jsonEncoding)) + { + encoder.WriteNodeId("D0", new NodeId(2263)); + encoder.WriteNodeId("D1", new NodeId(Get(NumericIds, index), (ushort)context.NamespaceUris.GetIndex(Get(NamespaceUris, index)))); + encoder.WriteNodeId("D2", new NodeId(Get(StringIds, index), (ushort)context.NamespaceUris.GetIndex(Get(NamespaceUris, index + 1)))); + encoder.WriteNodeId("D3", new NodeId(Get(GuidIds, index), (ushort)context.NamespaceUris.GetIndex(Get(NamespaceUris, index + 2)))); + encoder.WriteNodeId("D4", new NodeId(Get(OpaqueIds, index), (ushort)context.NamespaceUris.GetIndex(Get(NamespaceUris, index + 3)))); + + string actual = encoder.CloseAndReturnText(); + EncoderCommon.PrettifyAndValidateJson(actual, true); + Assert.AreEqual(expected, actual); + } + } + + [Test] + [TestCase(0)] + [TestCase(1)] + [TestCase(2)] + public void DecodeCompactAndVerboseExpandedNodeId(int index) + { + var data = $@" + {{ + ""D0"": ""i=2263"", + ""D1"": ""nsu={ToString(NamespaceUris, index)};i={ToString(NumericIds, index)}"", + ""D2"": ""nsu={ToString(NamespaceUris, index + 1)};s={ToString(StringIds, index)}"", + ""D3"": ""nsu={ToString(NamespaceUris, index + 2)};g={ToString(GuidIds, index)}"", + ""D4"": ""nsu={ToString(NamespaceUris, index + 3)};b={ToString(OpaqueIds, index)}"", + ""D5"": ""svu={ToString(ServerUris, index)};nsu={ToString(NamespaceUris, index)};i={ToString(NumericIds, index)}"", + ""D6"": ""svu={ToString(ServerUris, index + 1)};nsu={ToString(NamespaceUris, index + 1)};s={ToString(StringIds, index)}"", + ""D7"": ""svu={ToString(ServerUris, index + 2)};nsu={ToString(NamespaceUris, index + 2)};g={ToString(GuidIds, index)}"", + ""D8"": ""svu={ToString(ServerUris, index + 3)};nsu={ToString(NamespaceUris, index + 3)};b={ToString(OpaqueIds, index)}"" + }} + "; + + var context = new ServiceMessageContext(); + + using (var decoder = new JsonDecoder(data, context)) + { + decoder.UpdateNamespaceTable = true; + CheckDecodedExpandedNodeIds(context, decoder, index); + } + } + + [Test] + [TestCase(0, JsonEncodingType.Verbose)] + [TestCase(1, JsonEncodingType.Verbose)] + [TestCase(2, JsonEncodingType.Verbose)] + [TestCase(0, JsonEncodingType.Compact)] + [TestCase(1, JsonEncodingType.Compact)] + [TestCase(2, JsonEncodingType.Compact)] + + public void EncodeCompactOrVerboseExpandedNodeId(int index, JsonEncodingType jsonEncoding) + { + var data = $@" + {{ + ""D0"": ""i=2263"", + ""D1"": ""nsu={ToString(NamespaceUris, index)};i={ToString(NumericIds, index)}"", + ""D2"": ""nsu={ToString(NamespaceUris, index + 1)};s={ToString(StringIds, index)}"", + ""D3"": ""nsu={ToString(NamespaceUris, index + 2)};g={ToString(GuidIds, index)}"", + ""D4"": ""nsu={ToString(NamespaceUris, index + 3)};b={ToString(OpaqueIds, index)}"", + ""D5"": ""svu={ToString(ServerUris, index)};nsu={ToString(NamespaceUris, index)};i={ToString(NumericIds, index)}"", + ""D6"": ""svu={ToString(ServerUris, index + 1)};nsu={ToString(NamespaceUris, index + 1)};s={ToString(StringIds, index)}"", + ""D7"": ""svu={ToString(ServerUris, index + 2)};nsu={ToString(NamespaceUris, index + 2)};g={ToString(GuidIds, index)}"", + ""D8"": ""svu={ToString(ServerUris, index + 3)};nsu={ToString(NamespaceUris, index + 3)};b={ToString(OpaqueIds, index)}"" + }} + "; + + JObject jsonObj = JObject.Parse(data); + string expected = JsonConvert.SerializeObject(jsonObj, Formatting.None); + EncoderCommon.PrettifyAndValidateJson(expected, true); + + var context = new ServiceMessageContext(); + Array.ForEach(NamespaceUris, x => context.NamespaceUris.Append(x)); + context.ServerUris.Append("http://server-placeholder"); + Array.ForEach(ServerUris, x => context.ServerUris.Append(x)); + + using (var encoder = new JsonEncoder(context, jsonEncoding)) + { + encoder.WriteExpandedNodeId("D0", new ExpandedNodeId(2263)); + encoder.WriteExpandedNodeId("D1", new ExpandedNodeId(Get(NumericIds, index), (ushort)context.NamespaceUris.GetIndex(Get(NamespaceUris, index)))); + encoder.WriteExpandedNodeId("D2", new ExpandedNodeId(Get(StringIds, index), (ushort)context.NamespaceUris.GetIndex(Get(NamespaceUris, index + 1)))); + encoder.WriteExpandedNodeId("D3", new ExpandedNodeId(Get(GuidIds, index), (ushort)context.NamespaceUris.GetIndex(Get(NamespaceUris, index + 2)))); + encoder.WriteExpandedNodeId("D4", new ExpandedNodeId(Get(OpaqueIds, index), (ushort)context.NamespaceUris.GetIndex(Get(NamespaceUris, index + 3)))); + encoder.WriteExpandedNodeId("D5", new ExpandedNodeId(Get(NumericIds, index), (ushort)context.NamespaceUris.GetIndex(Get(NamespaceUris, index)), null, (uint)context.ServerUris.GetIndex(Get(ServerUris, index)))); + encoder.WriteExpandedNodeId("D6", new ExpandedNodeId(Get(StringIds, index), (ushort)context.NamespaceUris.GetIndex(Get(NamespaceUris, index + 1)), null, (uint)context.ServerUris.GetIndex(Get(ServerUris, index + 1)))); + encoder.WriteExpandedNodeId("D7", new ExpandedNodeId(Get(GuidIds, index), (ushort)context.NamespaceUris.GetIndex(Get(NamespaceUris, index + 2)), null, (uint)context.ServerUris.GetIndex(Get(ServerUris, index + 2)))); + encoder.WriteExpandedNodeId("D8", new ExpandedNodeId(Get(OpaqueIds, index), (ushort)context.NamespaceUris.GetIndex(Get(NamespaceUris, index + 3)), null, (uint)context.ServerUris.GetIndex(Get(ServerUris, index + 3)))); + + string actual = encoder.CloseAndReturnText(); + EncoderCommon.PrettifyAndValidateJson(actual, true); + Assert.AreEqual(expected, actual); + } + } + + [Test] + [TestCase(0)] + [TestCase(1)] + [TestCase(2)] + public void DecodeReversibleNodeId(int index) + { + var context1 = new ServiceMessageContext(); + context1.NamespaceUris.Append(NamespaceUris[0]); + context1.NamespaceUris.Append(NamespaceUris[1]); + context1.NamespaceUris.Append(NamespaceUris[2]); + + var data = $@" + {{ + ""D0"": {{ ""Id"": 2263 }}, + ""D1"": {{ ""Id"": {ToString(NumericIds, index)}, ""Namespace"":{context1.NamespaceUris.GetIndex(Get(NamespaceUris, index))} }}, + ""D2"": {{ ""IdType"": 1, ""Id"": ""{ToString(StringIds, index)}"", ""Namespace"":{context1.NamespaceUris.GetIndex(Get(NamespaceUris, index + 1))} }}, + ""D3"": {{ ""IdType"": 2, ""Id"": ""{ToString(GuidIds, index)}"", ""Namespace"":{context1.NamespaceUris.GetIndex(Get(NamespaceUris, index + 2))} }}, + ""D4"": {{ ""IdType"": 3, ""Id"": ""{ToString(OpaqueIds, index)}"", ""Namespace"":{context1.NamespaceUris.GetIndex(Get(NamespaceUris, index + 3))} }} + }} + "; + + var context2 = new ServiceMessageContext(); + context2.NamespaceUris.Append(NamespaceUris[2]); + context2.NamespaceUris.Append(NamespaceUris[0]); + context2.NamespaceUris.Append(NamespaceUris[1]); + + using (var decoder = new JsonDecoder(data, context2)) + { + decoder.UpdateNamespaceTable = false; + decoder.SetMappingTables(context1.NamespaceUris, context1.ServerUris); + CheckDecodedNodeIds(context2, decoder, index); + } + } + + [Test] + [TestCase(0)] + [TestCase(1)] + [TestCase(2)] + public void EncodeReversibleNodeId(int index) + { + var context1 = new ServiceMessageContext(); + context1.NamespaceUris.Append(NamespaceUris[0]); + context1.NamespaceUris.Append(NamespaceUris[1]); + context1.NamespaceUris.Append(NamespaceUris[2]); + + var data = $@" + {{ + ""D0"": {{ ""Id"": 2263 }}, + ""D1"": {{ ""Id"": {ToString(NumericIds, index)}, ""Namespace"":{context1.NamespaceUris.GetIndex(Get(NamespaceUris, index))} }}, + ""D2"": {{ ""IdType"": 1, ""Id"": ""{ToString(StringIds, index)}"", ""Namespace"":{context1.NamespaceUris.GetIndex(Get(NamespaceUris, index + 1))} }}, + ""D3"": {{ ""IdType"": 2, ""Id"": ""{ToString(GuidIds, index)}"", ""Namespace"":{context1.NamespaceUris.GetIndex(Get(NamespaceUris, index + 2))} }}, + ""D4"": {{ ""IdType"": 3, ""Id"": ""{ToString(OpaqueIds, index)}"", ""Namespace"":{context1.NamespaceUris.GetIndex(Get(NamespaceUris, index + 3))} }} + }} + "; + + JObject jsonObj = JObject.Parse(data); + string expected = JsonConvert.SerializeObject(jsonObj, Formatting.None); + EncoderCommon.PrettifyAndValidateJson(expected, true); + + var context2 = new ServiceMessageContext(); + context2.NamespaceUris.Append(NamespaceUris[2]); + context2.NamespaceUris.Append(NamespaceUris[0]); + context2.NamespaceUris.Append(NamespaceUris[1]); + + using (var encoder = new JsonEncoder(context2, JsonEncodingType.Reversible)) + { + encoder.SetMappingTables(context1.NamespaceUris, context1.ServerUris); + + encoder.WriteNodeId("D0", new NodeId(2263)); + encoder.WriteNodeId("D1", new NodeId(Get(NumericIds, index), (ushort)context2.NamespaceUris.GetIndex(Get(NamespaceUris, index)))); + encoder.WriteNodeId("D2", new NodeId(Get(StringIds, index), (ushort)context2.NamespaceUris.GetIndex(Get(NamespaceUris, index + 1)))); + encoder.WriteNodeId("D3", new NodeId(Get(GuidIds, index), (ushort)context2.NamespaceUris.GetIndex(Get(NamespaceUris, index + 2)))); + encoder.WriteNodeId("D4", new NodeId(Get(OpaqueIds, index), (ushort)context2.NamespaceUris.GetIndex(Get(NamespaceUris, index + 3)))); + + string actual = encoder.CloseAndReturnText(); + EncoderCommon.PrettifyAndValidateJson(actual, true); + Assert.AreEqual(expected, actual); + } + } + + [Test] + [TestCase(0)] + [TestCase(1)] + [TestCase(2)] + public void DecodeReversibleExpandedNodeId(int index) + { + var context1 = new ServiceMessageContext(); + context1.NamespaceUris.Append(NamespaceUris[0]); + context1.NamespaceUris.Append(NamespaceUris[1]); + context1.NamespaceUris.Append(NamespaceUris[2]); + context1.ServerUris.Append("http://server-placeholder"); + context1.ServerUris.Append(ServerUris[0]); + context1.ServerUris.Append(ServerUris[1]); + context1.ServerUris.Append(ServerUris[2]); + + var data = $@" + {{ + ""D0"": {{ ""Id"": 2263 }}, + ""D1"": {{ ""Id"": {ToString(NumericIds, index)}, ""Namespace"":{context1.NamespaceUris.GetIndex(Get(NamespaceUris, index))} }}, + ""D2"": {{ ""IdType"": 1, ""Id"": ""{ToString(StringIds, index)}"", ""Namespace"":{context1.NamespaceUris.GetIndex(Get(NamespaceUris, index + 1))} }}, + ""D3"": {{ ""IdType"": 2, ""Id"": ""{ToString(GuidIds, index)}"", ""Namespace"":{context1.NamespaceUris.GetIndex(Get(NamespaceUris, index + 2))} }}, + ""D4"": {{ ""IdType"": 3, ""Id"": ""{ToString(OpaqueIds, index)}"", ""Namespace"":{context1.NamespaceUris.GetIndex(Get(NamespaceUris, index + 3))} }}, + ""D5"": {{ ""Id"": {ToString(NumericIds, index)}, ""ServerUri"": {context1.ServerUris.GetIndex(Get(ServerUris, index))}, ""Namespace"":""{Get(NamespaceUris, index)}"" }}, + ""D6"": {{ ""IdType"": 1, ""Id"": ""{ToString(StringIds, index)}"", ""ServerUri"": {context1.ServerUris.GetIndex(Get(ServerUris, index + 1))}, ""Namespace"":""{Get(NamespaceUris, index + 1)}"" }}, + ""D7"": {{ ""IdType"": 2, ""Id"": ""{ToString(GuidIds, index)}"", ""ServerUri"": {context1.ServerUris.GetIndex(Get(ServerUris, index + 2))}, ""Namespace"":""{Get(NamespaceUris, index + 2)}"" }}, + ""D8"": {{ ""IdType"": 3, ""Id"": ""{ToString(OpaqueIds, index)}"", ""ServerUri"": {context1.ServerUris.GetIndex(Get(ServerUris, index + 3))}, ""Namespace"":""{Get(NamespaceUris, index + 3)}"" }} + }} + "; + + var context2 = new ServiceMessageContext(); + context2.NamespaceUris.Append(NamespaceUris[2]); + context2.NamespaceUris.Append(NamespaceUris[0]); + context2.NamespaceUris.Append(NamespaceUris[1]); + context2.ServerUris.Append("http://server-placeholder"); + context2.ServerUris.Append(ServerUris[2]); + context2.ServerUris.Append(ServerUris[0]); + context2.ServerUris.Append(ServerUris[1]); + + using (var decoder = new JsonDecoder(data, context2)) + { + decoder.UpdateNamespaceTable = false; + decoder.SetMappingTables(context1.NamespaceUris, context1.ServerUris); + CheckDecodedExpandedNodeIds(context2, decoder, index); + } + } + + [Test] + [TestCase(0)] + [TestCase(1)] + [TestCase(2)] + public void EncodeReversibleExpandedNodeId(int index) + { + var context1 = new ServiceMessageContext(); + context1.NamespaceUris.Append(NamespaceUris[0]); + context1.NamespaceUris.Append(NamespaceUris[1]); + context1.NamespaceUris.Append(NamespaceUris[2]); + context1.ServerUris.Append("http://server-placeholder"); + context1.ServerUris.Append(ServerUris[0]); + context1.ServerUris.Append(ServerUris[1]); + context1.ServerUris.Append(ServerUris[2]); + + var data = $@" + {{ + ""D0"": {{ ""Id"": 2263 }}, + ""D1"": {{ ""Id"": {ToString(NumericIds, index)}, ""Namespace"":{context1.NamespaceUris.GetIndex(Get(NamespaceUris, index))} }}, + ""D2"": {{ ""IdType"": 1, ""Id"": ""{ToString(StringIds, index)}"", ""Namespace"":{context1.NamespaceUris.GetIndex(Get(NamespaceUris, index + 1))} }}, + ""D3"": {{ ""IdType"": 2, ""Id"": ""{ToString(GuidIds, index)}"", ""Namespace"":{context1.NamespaceUris.GetIndex(Get(NamespaceUris, index + 2))} }}, + ""D4"": {{ ""IdType"": 3, ""Id"": ""{ToString(OpaqueIds, index)}"", ""Namespace"":{context1.NamespaceUris.GetIndex(Get(NamespaceUris, index + 3))} }}, + ""D5"": {{ ""Id"": {ToString(NumericIds, index)}, ""Namespace"":{context1.NamespaceUris.GetIndex(Get(NamespaceUris, index))}, ""ServerUri"": {context1.ServerUris.GetIndex(Get(ServerUris, index))} }}, + ""D6"": {{ ""IdType"": 1, ""Id"": ""{ToString(StringIds, index)}"", ""Namespace"":{context1.NamespaceUris.GetIndex(Get(NamespaceUris, index + 1))}, ""ServerUri"": {context1.ServerUris.GetIndex(Get(ServerUris, index + 1))} }}, + ""D7"": {{ ""IdType"": 2, ""Id"": ""{ToString(GuidIds, index)}"", ""Namespace"":{context1.NamespaceUris.GetIndex(Get(NamespaceUris, index + 2))}, ""ServerUri"": {context1.ServerUris.GetIndex(Get(ServerUris, index + 2))} }}, + ""D8"": {{ ""IdType"": 3, ""Id"": ""{ToString(OpaqueIds, index)}"", ""Namespace"":{context1.NamespaceUris.GetIndex(Get(NamespaceUris, index + 3))}, ""ServerUri"": {context1.ServerUris.GetIndex(Get(ServerUris, index + 3))} }} + }} + "; + + JObject jsonObj = JObject.Parse(data); + string expected = JsonConvert.SerializeObject(jsonObj, Formatting.None); + EncoderCommon.PrettifyAndValidateJson(expected, true); + + var context2 = new ServiceMessageContext(); + context2.NamespaceUris.Append(NamespaceUris[2]); + context2.NamespaceUris.Append(NamespaceUris[0]); + context2.NamespaceUris.Append(NamespaceUris[1]); + context2.ServerUris.Append("http://server-placeholder"); + context2.ServerUris.Append(ServerUris[2]); + context2.ServerUris.Append(ServerUris[0]); + context2.ServerUris.Append(ServerUris[1]); + + using (var encoder = new JsonEncoder(context2, JsonEncodingType.Reversible)) + { + encoder.SetMappingTables(context1.NamespaceUris, context1.ServerUris); + + encoder.WriteExpandedNodeId("D0", new ExpandedNodeId(2263)); + encoder.WriteExpandedNodeId("D1", new ExpandedNodeId(Get(NumericIds, index), (ushort)context2.NamespaceUris.GetIndex(Get(NamespaceUris, index)))); + encoder.WriteExpandedNodeId("D2", new ExpandedNodeId(Get(StringIds, index), (ushort)context2.NamespaceUris.GetIndex(Get(NamespaceUris, index + 1)))); + encoder.WriteExpandedNodeId("D3", new ExpandedNodeId(Get(GuidIds, index), (ushort)context2.NamespaceUris.GetIndex(Get(NamespaceUris, index + 2)))); + encoder.WriteExpandedNodeId("D4", new ExpandedNodeId(Get(OpaqueIds, index), (ushort)context2.NamespaceUris.GetIndex(Get(NamespaceUris, index + 3)))); + encoder.WriteExpandedNodeId("D5", new ExpandedNodeId(Get(NumericIds, index), (ushort)context2.NamespaceUris.GetIndex(Get(NamespaceUris, index)), null, (uint)context2.ServerUris.GetIndex(Get(ServerUris, index)))); + encoder.WriteExpandedNodeId("D6", new ExpandedNodeId(Get(StringIds, index), (ushort)context2.NamespaceUris.GetIndex(Get(NamespaceUris, index + 1)), null, (uint)context2.ServerUris.GetIndex(Get(ServerUris, index + 1)))); + encoder.WriteExpandedNodeId("D7", new ExpandedNodeId(Get(GuidIds, index), (ushort)context2.NamespaceUris.GetIndex(Get(NamespaceUris, index + 2)), null, (uint)context2.ServerUris.GetIndex(Get(ServerUris, index + 2)))); + encoder.WriteExpandedNodeId("D8", new ExpandedNodeId(Get(OpaqueIds, index), (ushort)context2.NamespaceUris.GetIndex(Get(NamespaceUris, index + 3)), null, (uint)context2.ServerUris.GetIndex(Get(ServerUris, index + 3)))); + + string actual = encoder.CloseAndReturnText(); + EncoderCommon.PrettifyAndValidateJson(actual, true); + Assert.AreEqual(expected, actual); + } + } + + [Test] + [TestCase(0)] + [TestCase(1)] + [TestCase(2)] + public void DecodeNonReversibleNodeId(int index) + { + var data = $@" + {{ + ""D0"": {{ ""Id"": 2263 }}, + ""D1"": {{ ""Id"": {ToString(NumericIds, index)}, ""Namespace"":""{Get(NamespaceUris, index)}"" }}, + ""D2"": {{ ""IdType"": 1, ""Id"": ""{ToString(StringIds, index)}"", ""Namespace"":""{Get(NamespaceUris, index + 1)}"" }}, + ""D3"": {{ ""IdType"": 2, ""Id"": ""{ToString(GuidIds, index)}"", ""Namespace"":""{Get(NamespaceUris, index + 2)}"" }}, + ""D4"": {{ ""IdType"": 3, ""Id"": ""{ToString(OpaqueIds, index)}"", ""Namespace"":""{Get(NamespaceUris, index + 3)}"" }} + }} + "; + + var context = new ServiceMessageContext(); + + using (var decoder = new JsonDecoder(data, context)) + { + decoder.UpdateNamespaceTable = true; + CheckDecodedNodeIds(context, decoder, index); + } + } + + [Test] + [TestCase(0)] + [TestCase(1)] + [TestCase(2)] + public void EncodeNonReversibleNodeId(int index) + { + var context1 = new ServiceMessageContext(); + context1.NamespaceUris.Append(NamespaceUris[0]); + context1.NamespaceUris.Append(NamespaceUris[1]); + context1.NamespaceUris.Append(NamespaceUris[2]); + + var data = $@" + {{ + ""D0"": {{ ""Id"": 2263 }}, + ""D1"": {{ ""Id"": {ToString(NumericIds, index)}, ""Namespace"":""{Get(NamespaceUris, index)}"" }}, + ""D2"": {{ ""IdType"": 1, ""Id"": ""{ToString(StringIds, index)}"", ""Namespace"":""{Get(NamespaceUris, index + 1)}"" }}, + ""D3"": {{ ""IdType"": 2, ""Id"": ""{ToString(GuidIds, index)}"", ""Namespace"":""{Get(NamespaceUris, index + 2)}"" }}, + ""D4"": {{ ""IdType"": 3, ""Id"": ""{ToString(OpaqueIds, index)}"", ""Namespace"":""{Get(NamespaceUris, index + 3)}"" }} + }} + "; + + JObject jsonObj = JObject.Parse(data); + string expected = JsonConvert.SerializeObject(jsonObj, Formatting.None); + EncoderCommon.PrettifyAndValidateJson(expected, true); + + var context2 = new ServiceMessageContext(); + context2.NamespaceUris.Append(NamespaceUris[2]); + context2.NamespaceUris.Append(NamespaceUris[0]); + context2.NamespaceUris.Append(NamespaceUris[1]); + + using (var encoder = new JsonEncoder(context2, JsonEncodingType.NonReversible)) + { + //encoder.ForceNamespaceUri = true; + //encoder.ForceNamespaceUriForIndex1 = true; + encoder.SetMappingTables(context1.NamespaceUris, context1.ServerUris); + + encoder.WriteNodeId("D0", new NodeId(2263)); + encoder.WriteNodeId("D1", new NodeId(Get(NumericIds, index), (ushort)context2.NamespaceUris.GetIndex(Get(NamespaceUris, index)))); + encoder.WriteNodeId("D2", new NodeId(Get(StringIds, index), (ushort)context2.NamespaceUris.GetIndex(Get(NamespaceUris, index + 1)))); + encoder.WriteNodeId("D3", new NodeId(Get(GuidIds, index), (ushort)context2.NamespaceUris.GetIndex(Get(NamespaceUris, index + 2)))); + encoder.WriteNodeId("D4", new NodeId(Get(OpaqueIds, index), (ushort)context2.NamespaceUris.GetIndex(Get(NamespaceUris, index + 3)))); + + string actual = encoder.CloseAndReturnText(); + EncoderCommon.PrettifyAndValidateJson(actual, true); + Assert.AreEqual(expected, actual); + } + } + + [Test] + [TestCase(0)] + [TestCase(1)] + [TestCase(2)] + public void DecodeNonReversibleExpandedNodeId(int index) + { + var context1 = new ServiceMessageContext(); + context1.NamespaceUris.Append(NamespaceUris[0]); + context1.NamespaceUris.Append(NamespaceUris[1]); + context1.NamespaceUris.Append(NamespaceUris[2]); + context1.ServerUris.Append("http://server-placeholder"); + context1.ServerUris.Append(ServerUris[0]); + context1.ServerUris.Append(ServerUris[1]); + context1.ServerUris.Append(ServerUris[2]); + + var data = $@" + {{ + ""D0"": {{ ""Id"": 2263 }}, + ""D1"": {{ ""Id"": {ToString(NumericIds, index)}, ""Namespace"":""{Get(NamespaceUris, index)}"" }}, + ""D2"": {{ ""IdType"": 1, ""Id"": ""{ToString(StringIds, index)}"", ""Namespace"":""{Get(NamespaceUris, index + 1)}"" }}, + ""D3"": {{ ""IdType"": 2, ""Id"": ""{ToString(GuidIds, index)}"", ""Namespace"":""{Get(NamespaceUris, index + 2)}"" }}, + ""D4"": {{ ""IdType"": 3, ""Id"": ""{ToString(OpaqueIds, index)}"", ""Namespace"":""{Get(NamespaceUris, index + 3)}"" }}, + ""D5"": {{ ""Id"": {ToString(NumericIds, index)}, ""ServerUri"": ""{Get(ServerUris, index)}"", ""Namespace"":""{Get(NamespaceUris, index)}"" }}, + ""D6"": {{ ""IdType"": 1, ""Id"": ""{ToString(StringIds, index)}"", ""ServerUri"": ""{Get(ServerUris, index + 1)}"", ""Namespace"":""{Get(NamespaceUris, index + 1)}"" }}, + ""D7"": {{ ""IdType"": 2, ""Id"": ""{ToString(GuidIds, index)}"", ""ServerUri"": ""{Get(ServerUris, index + 2)}"", ""Namespace"":""{Get(NamespaceUris, index + 2)}"" }}, + ""D8"": {{ ""IdType"": 3, ""Id"": ""{ToString(OpaqueIds, index)}"", ""ServerUri"": ""{Get(ServerUris, index + 3)}"", ""Namespace"":""{Get(NamespaceUris, index + 3)}"" }} + }} + "; + + var context2 = new ServiceMessageContext(); + context2.ServerUris.Append("http://server-placeholder"); + + using (var decoder = new JsonDecoder(data, context2)) + { + decoder.UpdateNamespaceTable = true; + CheckDecodedExpandedNodeIds(context2, decoder, index); + } + } + + [Test] + [TestCase(0)] + [TestCase(1)] + [TestCase(2)] + public void EncodeNonReversibleExpandedNodeId(int index) + { + var context1 = new ServiceMessageContext(); + context1.NamespaceUris.Append(NamespaceUris[0]); + context1.NamespaceUris.Append(NamespaceUris[1]); + context1.NamespaceUris.Append(NamespaceUris[2]); + context1.ServerUris.Append("http://server-placeholder"); + context1.ServerUris.Append(ServerUris[0]); + context1.ServerUris.Append(ServerUris[1]); + context1.ServerUris.Append(ServerUris[2]); + + var data = $@" + {{ + ""D0"": {{ ""Id"": 2263 }}, + ""D1"": {{ ""Id"": {ToString(NumericIds, index)}, ""Namespace"":""{Get(NamespaceUris, index)}"" }}, + ""D2"": {{ ""IdType"": 1, ""Id"": ""{ToString(StringIds, index)}"", ""Namespace"":""{Get(NamespaceUris, index + 1)}"" }}, + ""D3"": {{ ""IdType"": 2, ""Id"": ""{ToString(GuidIds, index)}"", ""Namespace"":""{Get(NamespaceUris, index + 2)}"" }}, + ""D4"": {{ ""IdType"": 3, ""Id"": ""{ToString(OpaqueIds, index)}"", ""Namespace"":""{Get(NamespaceUris, index + 3)}"" }}, + ""D5"": {{ ""Id"": {ToString(NumericIds, index)}, ""Namespace"":""{Get(NamespaceUris, index)}"", ""ServerUri"": ""{Get(ServerUris, index)}"" }}, + ""D6"": {{ ""IdType"": 1, ""Id"": ""{ToString(StringIds, index)}"", ""Namespace"":""{Get(NamespaceUris, index + 1)}"", ""ServerUri"": ""{Get(ServerUris, index + 1)}"" }}, + ""D7"": {{ ""IdType"": 2, ""Id"": ""{ToString(GuidIds, index)}"", ""Namespace"":""{Get(NamespaceUris, index + 2)}"", ""ServerUri"": ""{Get(ServerUris, index + 2)}"" }}, + ""D8"": {{ ""IdType"": 3, ""Id"": ""{ToString(OpaqueIds, index)}"", ""Namespace"":""{Get(NamespaceUris, index + 3)}"", ""ServerUri"": ""{Get(ServerUris, index + 3)}"" }} + }} + "; + + JObject jsonObj = JObject.Parse(data); + string expected = JsonConvert.SerializeObject(jsonObj, Formatting.None); + EncoderCommon.PrettifyAndValidateJson(expected, true); + + var context2 = new ServiceMessageContext(); + context2.NamespaceUris.Append(NamespaceUris[2]); + context2.NamespaceUris.Append(NamespaceUris[0]); + context2.NamespaceUris.Append(NamespaceUris[1]); + context2.ServerUris.Append("http://server-placeholder"); + context2.ServerUris.Append(ServerUris[2]); + context2.ServerUris.Append(ServerUris[0]); + context2.ServerUris.Append(ServerUris[1]); + + using (var encoder = new JsonEncoder(context2, JsonEncodingType.NonReversible)) + { + // encoder.ForceNamespaceUri = true; + // encoder.ForceNamespaceUriForIndex1 = true; + + encoder.SetMappingTables(context1.NamespaceUris, context1.ServerUris); + + encoder.WriteExpandedNodeId("D0", new ExpandedNodeId(2263)); + encoder.WriteExpandedNodeId("D1", new ExpandedNodeId(Get(NumericIds, index), (ushort)context2.NamespaceUris.GetIndex(Get(NamespaceUris, index)))); + encoder.WriteExpandedNodeId("D2", new ExpandedNodeId(Get(StringIds, index), (ushort)context2.NamespaceUris.GetIndex(Get(NamespaceUris, index + 1)))); + encoder.WriteExpandedNodeId("D3", new ExpandedNodeId(Get(GuidIds, index), (ushort)context2.NamespaceUris.GetIndex(Get(NamespaceUris, index + 2)))); + encoder.WriteExpandedNodeId("D4", new ExpandedNodeId(Get(OpaqueIds, index), (ushort)context2.NamespaceUris.GetIndex(Get(NamespaceUris, index + 3)))); + encoder.WriteExpandedNodeId("D5", new ExpandedNodeId(Get(NumericIds, index), (ushort)context2.NamespaceUris.GetIndex(Get(NamespaceUris, index)), null, (uint)context2.ServerUris.GetIndex(Get(ServerUris, index)))); + encoder.WriteExpandedNodeId("D6", new ExpandedNodeId(Get(StringIds, index), (ushort)context2.NamespaceUris.GetIndex(Get(NamespaceUris, index + 1)), null, (uint)context2.ServerUris.GetIndex(Get(ServerUris, index + 1)))); + encoder.WriteExpandedNodeId("D7", new ExpandedNodeId(Get(GuidIds, index), (ushort)context2.NamespaceUris.GetIndex(Get(NamespaceUris, index + 2)), null, (uint)context2.ServerUris.GetIndex(Get(ServerUris, index + 2)))); + encoder.WriteExpandedNodeId("D8", new ExpandedNodeId(Get(OpaqueIds, index), (ushort)context2.NamespaceUris.GetIndex(Get(NamespaceUris, index + 3)), null, (uint)context2.ServerUris.GetIndex(Get(ServerUris, index + 3)))); + + string actual = encoder.CloseAndReturnText(); + EncoderCommon.PrettifyAndValidateJson(actual, true); + Assert.AreEqual(expected, actual); + } + } + + [Test] + public void DecodeCompactAndVerboseQualifiedName() + { + var data = $@" + {{ + ""D0"": ""ServerStatus"", + ""D1"": ""nsu={ToString(NamespaceUris, 0)};N1"", + ""D2"": ""nsu={ToString(NamespaceUris, 1)};N2"", + ""D3"": ""nsu={ToString(NamespaceUris, 2)};N3"", + ""D4"": ""nsu={ToString(NamespaceUris, 3)};N4"" + }} + "; + + var context = new ServiceMessageContext(); + + using (var decoder = new JsonDecoder(data, context)) + { + decoder.UpdateNamespaceTable = true; + CheckDecodedQualfiiedNames(context, decoder, 0); + } + } + + [Test] + [TestCase(JsonEncodingType.Compact)] + [TestCase(JsonEncodingType.Verbose)] + public void EncodeCompactAndVerboseQualifiedName(JsonEncodingType jsonEncoding) + { + var context1 = new ServiceMessageContext(); + context1.NamespaceUris.Append(NamespaceUris[0]); + context1.NamespaceUris.Append(NamespaceUris[1]); + context1.NamespaceUris.Append(NamespaceUris[2]); + + var data = $@" + {{ + ""D0"": ""ServerStatus"", + ""D1"": ""nsu={ToString(NamespaceUris, 0)};N1"", + ""D2"": ""nsu={ToString(NamespaceUris, 1)};N2"", + ""D3"": ""nsu={ToString(NamespaceUris, 2)};N3"", + ""D4"": ""nsu={ToString(NamespaceUris, 3)};N4"" + }} + "; + + JObject jsonObj = JObject.Parse(data); + string expected = JsonConvert.SerializeObject(jsonObj, Formatting.None); + EncoderCommon.PrettifyAndValidateJson(expected, true); + + var context2 = new ServiceMessageContext(); + context2.NamespaceUris.Append(NamespaceUris[2]); + context2.NamespaceUris.Append(NamespaceUris[0]); + context2.NamespaceUris.Append(NamespaceUris[1]); + + using (var encoder = new JsonEncoder(context2, jsonEncoding)) + { + encoder.SetMappingTables(context1.NamespaceUris, context1.ServerUris); + + encoder.WriteQualifiedName("D0", new QualifiedName("ServerStatus")); + encoder.WriteQualifiedName("D1", new QualifiedName("N1", (ushort)context2.NamespaceUris.GetIndex(Get(NamespaceUris, 0)))); + encoder.WriteQualifiedName("D2", new QualifiedName("N2", (ushort)context2.NamespaceUris.GetIndex(Get(NamespaceUris, 1)))); + encoder.WriteQualifiedName("D3", new QualifiedName("N3", (ushort)context2.NamespaceUris.GetIndex(Get(NamespaceUris, 2)))); + encoder.WriteQualifiedName("D4", new QualifiedName("N4", (ushort)context2.NamespaceUris.GetIndex(Get(NamespaceUris, 3)))); + + string actual = encoder.CloseAndReturnText(); + EncoderCommon.PrettifyAndValidateJson(actual, true); + Assert.AreEqual(expected, actual); + } + } + + [Test] + public void DecodeReversibleQualifiedName() + { + var context1 = new ServiceMessageContext(); + context1.NamespaceUris.Append(NamespaceUris[0]); + context1.NamespaceUris.Append(NamespaceUris[1]); + context1.NamespaceUris.Append(NamespaceUris[2]); + + var data = $@" + {{ + ""D0"": {{ ""Name"": ""ServerStatus"" }}, + ""D1"": {{ ""Name"": ""N1"", ""Uri"":{context1.NamespaceUris.GetIndex(Get(NamespaceUris, 0))} }}, + ""D2"": {{ ""Name"": ""N2"", ""Uri"":{context1.NamespaceUris.GetIndex(Get(NamespaceUris, 1))} }}, + ""D3"": {{ ""Name"": ""N3"", ""Uri"":{context1.NamespaceUris.GetIndex(Get(NamespaceUris, 2))} }}, + ""D4"": {{ ""Name"": ""N4"", ""Uri"":{context1.NamespaceUris.GetIndex(Get(NamespaceUris, 3))} }} + }} + "; + + var context2 = new ServiceMessageContext(); + context2.NamespaceUris.Append(NamespaceUris[2]); + context2.NamespaceUris.Append(NamespaceUris[0]); + context2.NamespaceUris.Append(NamespaceUris[1]); + + using (var decoder = new JsonDecoder(data, context2)) + { + decoder.UpdateNamespaceTable = false; + decoder.SetMappingTables(context1.NamespaceUris, context1.ServerUris); + CheckDecodedQualfiiedNames(context2, decoder, 0); + } + } + + [Test] + public void EncodeReversibleQualifiedName() + { + var context1 = new ServiceMessageContext(); + context1.NamespaceUris.Append(NamespaceUris[0]); + context1.NamespaceUris.Append(NamespaceUris[1]); + context1.NamespaceUris.Append(NamespaceUris[2]); + + var data = $@" + {{ + ""D0"": {{ ""Name"": ""ServerStatus"" }}, + ""D1"": {{ ""Name"": ""N1"", ""Uri"":{context1.NamespaceUris.GetIndex(Get(NamespaceUris, 0))} }}, + ""D2"": {{ ""Name"": ""N2"", ""Uri"":{context1.NamespaceUris.GetIndex(Get(NamespaceUris, 1))} }}, + ""D3"": {{ ""Name"": ""N3"", ""Uri"":{context1.NamespaceUris.GetIndex(Get(NamespaceUris, 2))} }}, + ""D4"": {{ ""Name"": ""N4"", ""Uri"":{context1.NamespaceUris.GetIndex(Get(NamespaceUris, 3))} }} + }} + "; + + JObject jsonObj = JObject.Parse(data); + string expected = JsonConvert.SerializeObject(jsonObj, Formatting.None); + EncoderCommon.PrettifyAndValidateJson(expected, true); + + var context2 = new ServiceMessageContext(); + context2.NamespaceUris.Append(NamespaceUris[2]); + context2.NamespaceUris.Append(NamespaceUris[0]); + context2.NamespaceUris.Append(NamespaceUris[1]); + + using (var encoder = new JsonEncoder(context2, JsonEncodingType.Reversible)) + { + encoder.SetMappingTables(context1.NamespaceUris, context1.ServerUris); + + encoder.WriteQualifiedName("D0", new QualifiedName("ServerStatus")); + encoder.WriteQualifiedName("D1", new QualifiedName("N1", (ushort)context2.NamespaceUris.GetIndex(Get(NamespaceUris, 0)))); + encoder.WriteQualifiedName("D2", new QualifiedName("N2", (ushort)context2.NamespaceUris.GetIndex(Get(NamespaceUris, 1)))); + encoder.WriteQualifiedName("D3", new QualifiedName("N3", (ushort)context2.NamespaceUris.GetIndex(Get(NamespaceUris, 2)))); + encoder.WriteQualifiedName("D4", new QualifiedName("N4", (ushort)context2.NamespaceUris.GetIndex(Get(NamespaceUris, 3)))); + + string actual = encoder.CloseAndReturnText(); + EncoderCommon.PrettifyAndValidateJson(actual, true); + Assert.AreEqual(expected, actual); + } + } + + [Test] + public void DecodeNonReversibleQualifiedName() + { + var context1 = new ServiceMessageContext(); + context1.NamespaceUris.Append(NamespaceUris[0]); + context1.NamespaceUris.Append(NamespaceUris[1]); + context1.NamespaceUris.Append(NamespaceUris[2]); + + var data = $@" + {{ + ""D0"": {{ ""Name"":""ServerStatus"" }}, + ""D1"": {{ ""Name"": ""N1"", ""Uri"":""{Get(NamespaceUris, 0)}"" }}, + ""D2"": {{ ""Name"": ""N2"", ""Uri"":""{Get(NamespaceUris, 1)}"" }}, + ""D3"": {{ ""Name"": ""N3"", ""Uri"":""{Get(NamespaceUris, 2)}"" }}, + ""D4"": {{ ""Name"": ""N4"", ""Uri"":""{Get(NamespaceUris, 3)}"" }} + }} + "; + + var context2 = new ServiceMessageContext(); + context2.NamespaceUris.Append(NamespaceUris[2]); + context2.NamespaceUris.Append(NamespaceUris[0]); + context2.NamespaceUris.Append(NamespaceUris[1]); + + using (var decoder = new JsonDecoder(data, context2)) + { + decoder.UpdateNamespaceTable = false; + decoder.SetMappingTables(context1.NamespaceUris, context1.ServerUris); + CheckDecodedQualfiiedNames(context2, decoder, 0); + } + } + + [Test] + public void EncodeNonReversibleQualifiedName() + { + var context1 = new ServiceMessageContext(); + context1.NamespaceUris.Append(NamespaceUris[0]); + context1.NamespaceUris.Append(NamespaceUris[1]); + context1.NamespaceUris.Append(NamespaceUris[2]); + + var data = $@" + {{ + ""D0"": {{ ""Name"": ""ServerStatus"" }}, + ""D1"": {{ ""Name"": ""N1"", ""Uri"":""{Get(NamespaceUris, 0)}"" }}, + ""D2"": {{ ""Name"": ""N2"", ""Uri"":""{Get(NamespaceUris, 1)}"" }}, + ""D3"": {{ ""Name"": ""N3"", ""Uri"":""{Get(NamespaceUris, 2)}"" }}, + ""D4"": {{ ""Name"": ""N4"", ""Uri"":""{Get(NamespaceUris, 3)}"" }} + }} + "; + + JObject jsonObj = JObject.Parse(data); + string expected = JsonConvert.SerializeObject(jsonObj, Formatting.None); + EncoderCommon.PrettifyAndValidateJson(expected, true); + + var context2 = new ServiceMessageContext(); + context2.NamespaceUris.Append(NamespaceUris[2]); + context2.NamespaceUris.Append(NamespaceUris[0]); + context2.NamespaceUris.Append(NamespaceUris[1]); + + using (var encoder = new JsonEncoder(context2, JsonEncodingType.NonReversible)) + { + // encoder.ForceNamespaceUri = true; + // encoder.ForceNamespaceUriForIndex1 = true; + + encoder.SetMappingTables(context1.NamespaceUris, context1.ServerUris); + + encoder.WriteQualifiedName("D0", new QualifiedName("ServerStatus")); + encoder.WriteQualifiedName("D1", new QualifiedName("N1", (ushort)context2.NamespaceUris.GetIndex(Get(NamespaceUris, 0)))); + encoder.WriteQualifiedName("D2", new QualifiedName("N2", (ushort)context2.NamespaceUris.GetIndex(Get(NamespaceUris, 1)))); + encoder.WriteQualifiedName("D3", new QualifiedName("N3", (ushort)context2.NamespaceUris.GetIndex(Get(NamespaceUris, 2)))); + encoder.WriteQualifiedName("D4", new QualifiedName("N4", (ushort)context2.NamespaceUris.GetIndex(Get(NamespaceUris, 3)))); + + string actual = encoder.CloseAndReturnText(); + EncoderCommon.PrettifyAndValidateJson(actual, true); + Assert.AreEqual(expected, actual); + } + } + + [Test] + public void DecodeCompactAndVerboseMatrix() + { + var data = $@" + {{ + ""D0"": {{ ""Dimensions"": [ 2, 3 ], ""Array"": [ 1, 2, 3, 4, 5, 6 ] }}, + ""D1"": {{ ""Dimensions"": [ 1, 2, 3 ], ""Array"": [ 1, 2, 3, 4, 5, 6 ] }} + }} + "; + + var context = new ServiceMessageContext(); + + using (var decoder = new JsonDecoder(data, context)) + { + Array a1 = decoder.ReadArray("D0", 2, BuiltInType.Int64); + Assert.AreEqual(2, a1.Rank); + Assert.AreEqual(6, a1.Length); + Assert.AreEqual(2, a1.GetLength(0)); + Assert.AreEqual(3, a1.GetLength(1)); + + Array a2 = decoder.ReadArray("D1", 2, BuiltInType.Int64); + Assert.AreEqual(3, a2.Rank); + Assert.AreEqual(6, a2.Length); + Assert.AreEqual(1, a2.GetLength(0)); + Assert.AreEqual(2, a2.GetLength(1)); + Assert.AreEqual(3, a2.GetLength(2)); + } + } + + [Test] + [TestCase(JsonEncodingType.Compact)] + [TestCase(JsonEncodingType.Verbose)] + public void EncodeCompactAndVerboseMatrix(JsonEncodingType jsonEncoding) + { + var data = $@" + {{ + ""D0"": {{ ""Dimensions"": [ 2, 3 ], ""Array"": [ 1, 2, 3, 4, 5, 6 ] }}, + ""D1"": {{ ""Dimensions"": [ 1, 2, 3 ], ""Array"": [ 1, 2, 3, 4, 5, 6 ] }} + }} + "; + + JObject jsonObj = JObject.Parse(data); + string expected = JsonConvert.SerializeObject(jsonObj, Formatting.None); + EncoderCommon.PrettifyAndValidateJson(expected, true); + + var context = new ServiceMessageContext(); + + using (var encoder = new JsonEncoder(context, jsonEncoding)) + { + encoder.WriteArray("D0", new int[,] { { 1, 2, 3 }, { 4, 5, 6 } }, 2, BuiltInType.Int32); + encoder.WriteArray("D1", new int[,,] { { { 1, 2, 3 }, { 4, 5, 6 } } }, 3, BuiltInType.Int32); + + string actual = encoder.CloseAndReturnText(); + EncoderCommon.PrettifyAndValidateJson(actual, true); + Assert.AreEqual(expected, actual); + } + } + + [Test] + public void DecodeReversibleMatrix() + { + var data = $@" + {{ + ""D0"": [[1, 2, 3], [4, 5, 6]], + ""D1"": [[[1, 2, 3], [4, 5, 6]]] + }} + "; + + var context = new ServiceMessageContext(); + + using (var decoder = new JsonDecoder(data, context)) + { + Array a1 = decoder.ReadArray("D0", 2, BuiltInType.Int64); + Assert.AreEqual(2, a1.Rank); + Assert.AreEqual(6, a1.Length); + Assert.AreEqual(2, a1.GetLength(0)); + Assert.AreEqual(3, a1.GetLength(1)); + + Array a2 = decoder.ReadArray("D1", 2, BuiltInType.Int64); + Assert.AreEqual(3, a2.Rank); + Assert.AreEqual(6, a2.Length); + Assert.AreEqual(1, a2.GetLength(0)); + Assert.AreEqual(2, a2.GetLength(1)); + Assert.AreEqual(3, a2.GetLength(2)); + } + } + + [Test] + [TestCase(JsonEncodingType.Reversible)] + [TestCase(JsonEncodingType.NonReversible)] + public void EncodeReversibleAndNonReversibleMatrix(JsonEncodingType jsonEncoding) + { + var data = $@" + {{ + ""D0"": [[1, 2, 3], [4, 5, 6]], + ""D1"": [[[1, 2, 3], [4, 5, 6]]] + }} + "; + + JObject jsonObj = JObject.Parse(data); + string expected = JsonConvert.SerializeObject(jsonObj, Formatting.None); + EncoderCommon.PrettifyAndValidateJson(expected, true); + + var context = new ServiceMessageContext(); + + using (var encoder = new JsonEncoder(context, jsonEncoding)) + { + encoder.WriteArray("D0", new int[,] { { 1, 2, 3 }, { 4, 5, 6 } }, 2, BuiltInType.Int32); + encoder.WriteArray("D1", new int[,,] { { { 1, 2, 3 }, { 4, 5, 6 } } }, 3, BuiltInType.Int32); + + string actual = encoder.CloseAndReturnText(); + EncoderCommon.PrettifyAndValidateJson(actual, true); + Assert.AreEqual(expected, actual); + } + } + + [Test] + public void DecodeCompactExtensionObject() + { + var data = $@" + {{ + ""D0"": {{ + ""TypeId"": ""i=884"", + ""Body"": {{ ""High"": 9876.5432 }} + }}, + ""D1"": {{ + ""Type"": 22, + ""Body"": {{ + ""TypeId"": ""nsu=http://opcfoundation.org/UA/GDS/;i=1"", + ""Body"": {{ + ""ApplicationId"": ""nsu=urn:localhost:server;s=urn:123456789"", + ""ApplicationUri"": ""urn:localhost:test.org:client"", + ""ApplicationType"": 1, + ""ApplicationNames"": [{{ ""Text"":""Test Client"", ""Locale"":""en"" }}], + ""ProductUri"": ""http://test.org/client"", + ""DiscoveryUrls"": [""opc.tcp://localhost/""] + }} + }} + }} + }} + "; + + var context = new ServiceMessageContext(); + context.NamespaceUris.Append("urn:localhost:server"); + context.NamespaceUris.Append(Opc.Ua.Gds.Namespaces.OpcUaGds); + + using (var decoder = new JsonDecoder(data, context)) + { + var eo = decoder.ReadExtensionObject("D0"); + Assert.AreEqual(Opc.Ua.DataTypeIds.Range.ToString(), eo.TypeId.ToString()); + var range = eo.Body as Opc.Ua.Range; + Assert.IsNotNull(range); + Assert.AreEqual(0, range.Low); + Assert.AreEqual(9876.5432, range.High); + + var v1 = decoder.ReadVariant("D1"); + Assert.AreEqual(v1.TypeInfo.BuiltInType, BuiltInType.ExtensionObject); + + eo = v1.Value as ExtensionObject; + Assert.IsNotNull(eo); + Assert.AreEqual(Opc.Ua.Gds.DataTypeIds.ApplicationRecordDataType.ToString(), eo.TypeId.ToString()); + + var record = eo.Body as Opc.Ua.Gds.ApplicationRecordDataType; + Assert.IsNotNull(record); + Assert.AreEqual(Opc.Ua.ApplicationType.Client, record.ApplicationType); + Assert.AreEqual("Test Client", record.ApplicationNames[0].Text); + } + } + + [Test] + public void EncodeCompactExtensionObject() + { + var data = $@" + {{ + ""D0"": {{ + ""TypeId"": ""i=884"", + ""Body"": {{ ""High"": 9876.5432 }} + }}, + ""D1"": {{ + ""Type"": 22, + ""Body"": {{ + ""TypeId"": ""nsu=http://opcfoundation.org/UA/GDS/;i=1"", + ""Body"": {{ + ""ApplicationId"": ""nsu=urn:localhost:server;s=urn:123456789"", + ""ApplicationUri"": ""urn:localhost:test.org:client"", + ""ApplicationType"": 0, + ""ApplicationNames"": [{{ ""Text"":""Test Client"", ""Locale"":""en"" }}], + ""ProductUri"": ""http://test.org/client"", + ""DiscoveryUrls"": [""opc.tcp://localhost/""], + ""ServerCapabilities"": [] + }} + }} + }} + }} + "; + + JObject jsonObj = JObject.Parse(data); + string expected = JsonConvert.SerializeObject(jsonObj, Formatting.None); + EncoderCommon.PrettifyAndValidateJson(expected, true); + + var context = new ServiceMessageContext(); + context.NamespaceUris.Append("urn:localhost:server"); + context.NamespaceUris.Append(Opc.Ua.Gds.Namespaces.OpcUaGds); + + using (var encoder = new JsonEncoder(context, JsonEncodingType.Compact)) + { + encoder.WriteExtensionObject( + "D0", + new ExtensionObject( + Opc.Ua.DataTypeIds.Range, + new Opc.Ua.Range() { High = 9876.5432 }) + ); + + encoder.WriteVariant( + "D1", + new Variant(new ExtensionObject( + Opc.Ua.Gds.DataTypeIds.ApplicationRecordDataType, + new Opc.Ua.Gds.ApplicationRecordDataType() { + ApplicationId = new NodeId("urn:123456789", 1), + ApplicationUri = "urn:localhost:test.org:client", + ApplicationNames = new LocalizedText[] { new LocalizedText("en", "Test Client") }, + ProductUri = "http://test.org/client", + DiscoveryUrls = new string[] { "opc.tcp://localhost/" }, + })) + ); + + string actual = encoder.CloseAndReturnText(); + EncoderCommon.PrettifyAndValidateJson(actual, true); + Assert.AreEqual(expected, actual); + } + } + + [Test] + public void DecodeVerboseExtensionObject() + { + var data = $@" + {{ + ""D0"": {{ + ""TypeId"": ""i=884"", + ""Body"": {{ ""Low"": 0, ""High"": 9876.5432 }} + }}, + ""D1"": {{ + ""Type"": 22, + ""Body"": {{ + ""TypeId"": ""nsu=http://opcfoundation.org/UA/GDS/;i=1"", + ""Body"": {{ + ""ApplicationId"": ""nsu=urn:localhost:server;s=urn:123456789"", + ""ApplicationUri"": ""urn:localhost:test.org:client"", + ""ApplicationType"": ""Client_1"", + ""ApplicationNames"": [{{ ""Text"":""Test Client"", ""Locale"":""en"" }}], + ""ProductUri"": ""http://test.org/client"", + ""DiscoveryUrls"": [""opc.tcp://localhost/""], + ""ServerCapabilities"": [] + }} + }} + }} + }} + "; + + var context = new ServiceMessageContext(); + context.NamespaceUris.Append("urn:localhost:server"); + context.NamespaceUris.Append(Opc.Ua.Gds.Namespaces.OpcUaGds); + + using (var decoder = new JsonDecoder(data, context)) + { + var eo = decoder.ReadExtensionObject("D0"); + Assert.AreEqual(Opc.Ua.DataTypeIds.Range.ToString(), eo.TypeId.ToString()); + var range = eo.Body as Opc.Ua.Range; + Assert.IsNotNull(range); + Assert.AreEqual(0, range.Low); + Assert.AreEqual(9876.5432, range.High); + + var v1 = decoder.ReadVariant("D1"); + Assert.AreEqual(v1.TypeInfo.BuiltInType, BuiltInType.ExtensionObject); + + eo = v1.Value as ExtensionObject; + Assert.IsNotNull(eo); + Assert.AreEqual(Opc.Ua.Gds.DataTypeIds.ApplicationRecordDataType.ToString(), eo.TypeId.ToString()); + + var record = eo.Body as Opc.Ua.Gds.ApplicationRecordDataType; + Assert.IsNotNull(record); + Assert.AreEqual(Opc.Ua.ApplicationType.Client, record.ApplicationType); + Assert.AreEqual("Test Client", record.ApplicationNames[0].Text); + } + } + + [Test] + public void EncodeVerboseExtensionObject() + { + var data = $@" + {{ + ""D0"": {{ + ""TypeId"": ""i=884"", + ""Body"": {{ ""Low"": 0, ""High"": 9876.5432 }} + }}, + ""D1"": {{ + ""Type"": 22, + ""Body"": {{ + ""TypeId"": ""nsu=http://opcfoundation.org/UA/GDS/;i=1"", + ""Body"": {{ + ""ApplicationId"": ""nsu=urn:localhost:server;s=urn:123456789"", + ""ApplicationUri"": ""urn:localhost:test.org:client"", + ""ApplicationType"": ""Client_1"", + ""ApplicationNames"": [{{ ""Text"":""Test Client"", ""Locale"":""en"" }}], + ""ProductUri"": ""http://test.org/client"", + ""DiscoveryUrls"": [""opc.tcp://localhost/""], + ""ServerCapabilities"": [] + }} + }} + }} + }} + "; + + JObject jsonObj = JObject.Parse(data); + string expected = JsonConvert.SerializeObject(jsonObj, Formatting.None); + EncoderCommon.PrettifyAndValidateJson(expected, true); + + var context = new ServiceMessageContext(); + context.NamespaceUris.Append("urn:localhost:server"); + context.NamespaceUris.Append(Opc.Ua.Gds.Namespaces.OpcUaGds); + + using (var encoder = new JsonEncoder(context, JsonEncodingType.Verbose)) + { + encoder.WriteExtensionObject( + "D0", + new ExtensionObject( + Opc.Ua.DataTypeIds.Range, + new Opc.Ua.Range() { Low = 0, High = 9876.5432 }) + ); + + encoder.WriteVariant( + "D1", + new Variant(new ExtensionObject( + Opc.Ua.Gds.DataTypeIds.ApplicationRecordDataType, + new Opc.Ua.Gds.ApplicationRecordDataType() { + ApplicationId = new NodeId("urn:123456789", 1), + ApplicationUri = "urn:localhost:test.org:client", + ApplicationType = Opc.Ua.ApplicationType.Client, + ApplicationNames = new LocalizedText[] { new LocalizedText("en", "Test Client") }, + ProductUri = "http://test.org/client", + DiscoveryUrls = new string[] { "opc.tcp://localhost/" }, + })) + ); + + string actual = encoder.CloseAndReturnText(); + EncoderCommon.PrettifyAndValidateJson(actual, true); + Assert.AreEqual(expected, actual); + } + } + + [Test] + public void DecodeReversibleExtensionObject() + { + var data = $@" + {{ + ""D0"": {{ + ""TypeId"": {{ ""Id"": 884 }}, + ""Body"": {{ ""High"": 9876.5432 }} + }}, + ""D1"": {{ + ""Type"": 22, + ""Body"": {{ + ""TypeId"": {{ ""Id"": 1, ""Namespace"": 2 }}, + ""Body"": {{ + ""ApplicationId"": {{ ""IdType"":1, ""Id"":""urn:123456789"",""Namespace"":1 }}, + ""ApplicationUri"": ""urn:localhost:test.org:client"", + ""ApplicationType"": 1, + ""ApplicationNames"": [{{ ""Text"":""Test Client"", ""Locale"":""en"" }}], + ""ProductUri"": ""http://test.org/client"", + ""DiscoveryUrls"": [""opc.tcp://localhost/""] + }} + }} + }} + }} + "; + + var context = new ServiceMessageContext(); + context.NamespaceUris.Append("urn:localhost:server"); + context.NamespaceUris.Append(Opc.Ua.Gds.Namespaces.OpcUaGds); + + using (var decoder = new JsonDecoder(data, context)) + { + var eo = decoder.ReadExtensionObject("D0"); + Assert.AreEqual(Opc.Ua.DataTypeIds.Range.ToString(), eo.TypeId.ToString()); + var range = eo.Body as Opc.Ua.Range; + Assert.IsNotNull(range); + Assert.AreEqual(0, range.Low); + Assert.AreEqual(9876.5432, range.High); + + var v1 = decoder.ReadVariant("D1"); + Assert.AreEqual(v1.TypeInfo.BuiltInType, BuiltInType.ExtensionObject); + + eo = v1.Value as ExtensionObject; + Assert.IsNotNull(eo); + Assert.AreEqual(Opc.Ua.Gds.DataTypeIds.ApplicationRecordDataType.ToString(), eo.TypeId.ToString()); + + var record = eo.Body as Opc.Ua.Gds.ApplicationRecordDataType; + Assert.IsNotNull(record); + Assert.AreEqual(Opc.Ua.ApplicationType.Client, record.ApplicationType); + Assert.AreEqual("Test Client", record.ApplicationNames[0].Text); + } + } + + [Test] + public void EncodeReversibleExtensionObject() + { + var data = $@" + {{ + ""D0"": {{ + ""TypeId"": {{ ""Id"": 884 }}, + ""Body"": {{ ""Low"":0, ""High"": 9876.5432 }} + }}, + ""D1"": {{ + ""Type"": 22, + ""Body"": {{ + ""TypeId"": {{ ""Id"": 1, ""Namespace"": 2 }}, + ""Body"": {{ + ""ApplicationId"": {{ ""IdType"":1, ""Id"":""urn:123456789"",""Namespace"":1 }}, + ""ApplicationUri"": ""urn:localhost:test.org:client"", + ""ApplicationType"": 1, + ""ApplicationNames"": [{{ ""Text"":""Test Client"", ""Locale"":""en"" }}], + ""ProductUri"": ""http://test.org/client"", + ""DiscoveryUrls"": [""opc.tcp://localhost/""], + ""ServerCapabilities"": [] + }} + }} + }} + }} + "; + + JObject jsonObj = JObject.Parse(data); + string expected = JsonConvert.SerializeObject(jsonObj, Formatting.None); + EncoderCommon.PrettifyAndValidateJson(expected, true); + + var context = new ServiceMessageContext(); + context.NamespaceUris.Append("urn:localhost:server"); + context.NamespaceUris.Append(Opc.Ua.Gds.Namespaces.OpcUaGds); + + using (var encoder = new JsonEncoder(context, JsonEncodingType.Reversible)) + { + encoder.WriteExtensionObject( + "D0", + new ExtensionObject( + Opc.Ua.DataTypeIds.Range, + new Opc.Ua.Range() { High = 9876.5432 }) + ); + + encoder.WriteVariant( + "D1", + new Variant(new ExtensionObject( + Opc.Ua.Gds.DataTypeIds.ApplicationRecordDataType, + new Opc.Ua.Gds.ApplicationRecordDataType() { + ApplicationId = new NodeId("urn:123456789", 1), + ApplicationUri = "urn:localhost:test.org:client", + ApplicationType = Opc.Ua.ApplicationType.Client, + ApplicationNames = new LocalizedText[] { new LocalizedText("en", "Test Client") }, + ProductUri = "http://test.org/client", + DiscoveryUrls = new string[] { "opc.tcp://localhost/" }, + })) + ); + + string actual = encoder.CloseAndReturnText(); + EncoderCommon.PrettifyAndValidateJson(actual, true); + Assert.AreEqual(expected, actual); + } + } + + [Test] + public void DecodeNonReversibleExtensionObject() + { + var data = $@" + {{ + ""D0"": {{ ""Low"": 0, ""High"": 9876.5432 }}, + ""D1"": {{ + ""ApplicationId"": {{ ""IdType"":1, ""Id"":""urn:123456789"",""Namespace"":""urn:localhost:server"" }}, + ""ApplicationUri"": ""urn:localhost:test.org:client"", + ""ApplicationType"": ""Client_1"", + ""ApplicationNames"": [""Test Client""], + ""ProductUri"": ""http://test.org/client"", + ""DiscoveryUrls"": [""opc.tcp://localhost/""], + ""ServerCapabilities"": [] + }} + }} + "; + + var context = new ServiceMessageContext(); + context.NamespaceUris.Append("urn:localhost:server"); + context.NamespaceUris.Append(Opc.Ua.Gds.Namespaces.OpcUaGds); + + using (var decoder = new JsonDecoder(data, context)) + { + var range = decoder.ReadEncodeable("D0", typeof(Opc.Ua.Range)) as Opc.Ua.Range; + Assert.IsNotNull(range); + Assert.AreEqual(0, range.Low); + Assert.AreEqual(9876.5432, range.High); + + var record = decoder.ReadEncodeable("D1", typeof(Opc.Ua.Gds.ApplicationRecordDataType)) as Opc.Ua.Gds.ApplicationRecordDataType; + Assert.IsNotNull(record); + Assert.AreEqual(Opc.Ua.ApplicationType.Client, record.ApplicationType); + Assert.AreEqual("Test Client", record.ApplicationNames[0].Text); + } + } + + [Test] + public void EncodeNonReversibleExtensionObject() + { + var data = $@" + {{ + ""D0"": {{ ""Low"": 0, ""High"": 9876.5432 }}, + ""D1"": {{ + ""ApplicationId"": {{ ""IdType"":1, ""Id"":""urn:123456789"",""Namespace"":""urn:localhost:server"" }}, + ""ApplicationUri"": ""urn:localhost:test.org:client"", + ""ApplicationType"": ""Client_1"", + ""ApplicationNames"": [""Test Client""], + ""ProductUri"": ""http://test.org/client"", + ""DiscoveryUrls"": [""opc.tcp://localhost/""], + ""ServerCapabilities"": [] + }} + }} + "; + + JObject jsonObj = JObject.Parse(data); + string expected = JsonConvert.SerializeObject(jsonObj, Formatting.None); + EncoderCommon.PrettifyAndValidateJson(expected, true); + + var context = new ServiceMessageContext(); + context.NamespaceUris.Append("urn:localhost:server"); + + using (var encoder = new JsonEncoder(context, JsonEncodingType.NonReversible)) + { + encoder.WriteExtensionObject( + "D0", + new ExtensionObject( + Opc.Ua.DataTypeIds.Range, + new Opc.Ua.Range() { Low = 0, High = 9876.5432 }) + ); + + encoder.WriteVariant( + "D1", + new Variant(new ExtensionObject( + Opc.Ua.Gds.DataTypeIds.ApplicationRecordDataType, + new Opc.Ua.Gds.ApplicationRecordDataType() { + ApplicationId = new NodeId("urn:123456789", 1), + ApplicationUri = "urn:localhost:test.org:client", + ApplicationType = Opc.Ua.ApplicationType.Client, + ApplicationNames = new LocalizedText[] { new LocalizedText("en", "Test Client") }, + ProductUri = "http://test.org/client", + DiscoveryUrls = new string[] { "opc.tcp://localhost/" }, + })) + ); + + string actual = encoder.CloseAndReturnText(); + EncoderCommon.PrettifyAndValidateJson(actual, true); + Assert.AreEqual(expected, actual); + } + } + } +} diff --git a/Tests/Opc.Ua.Core.Tests/Types/Encoders/JsonEncoderTests.cs b/Tests/Opc.Ua.Core.Tests/Types/Encoders/JsonEncoderTests.cs index 78ce518aa1..b703689800 100644 --- a/Tests/Opc.Ua.Core.Tests/Types/Encoders/JsonEncoderTests.cs +++ b/Tests/Opc.Ua.Core.Tests/Types/Encoders/JsonEncoderTests.cs @@ -1,5 +1,5 @@ /* ======================================================================== - * Copyright (c) 2005-2018 The OPC Foundation, Inc. All rights reserved. + * Copyright (c) 2005-2024 The OPC Foundation, Inc. All rights reserved. * * OPC Foundation MIT License 1.00 * @@ -84,67 +84,67 @@ public class JsonEncoderTests : EncoderCommon /// [DatapointSource] public static readonly JsonValidationData[] Data = new JsonValidationDataCollection() { - { BuiltInType.Boolean, true,"true", null }, - { BuiltInType.Boolean, false, null, null }, - { BuiltInType.Boolean, false, "false", null, true }, + { BuiltInType.Boolean, true, "true", null }, + { BuiltInType.Boolean, false, null, null, null, "false" }, + { BuiltInType.Boolean, false, "false", null, null, "false", true }, - { BuiltInType.Byte, (Byte)0, null, null}, - { BuiltInType.Byte, (Byte)0, "0", null, true }, + { BuiltInType.Byte, (Byte)0, null, null, null, "0"}, + { BuiltInType.Byte, (Byte)0, "0", null, null, "0", true }, { BuiltInType.Byte, (Byte)88, "88", null }, { BuiltInType.Byte, (Byte)188, "188", null }, - { BuiltInType.Byte, Byte.MinValue, Byte.MinValue.ToString(CultureInfo.InvariantCulture), null, true}, + { BuiltInType.Byte, Byte.MinValue, Byte.MinValue.ToString(CultureInfo.InvariantCulture), null, null, Byte.MinValue.ToString(CultureInfo.InvariantCulture), true}, { BuiltInType.Byte, Byte.MaxValue, Byte.MaxValue.ToString(CultureInfo.InvariantCulture), null }, - { BuiltInType.SByte, (SByte)0, null, null }, - { BuiltInType.SByte, (SByte)0, "0", null, true }, + { BuiltInType.SByte, (SByte)0, null, null, null, "0" }, + { BuiltInType.SByte, (SByte)0, "0", null, null, "0", true }, { BuiltInType.SByte, (SByte)(-77), "-77", null }, { BuiltInType.SByte, (SByte)(77), "77", null }, { BuiltInType.SByte, SByte.MaxValue, SByte.MaxValue.ToString(CultureInfo.InvariantCulture), null }, { BuiltInType.SByte, SByte.MinValue, SByte.MinValue.ToString(CultureInfo.InvariantCulture), null }, - { BuiltInType.UInt16, (UInt16)0, null, null}, - { BuiltInType.UInt16, (UInt16)0, "0", null, true }, + { BuiltInType.UInt16, (UInt16)0, null, null, null, "0"}, + { BuiltInType.UInt16, (UInt16)0, "0", null, null, "0", true }, { BuiltInType.UInt16, (UInt16)12345, "12345", null }, { BuiltInType.UInt16, (UInt16)44444, "44444", null }, - { BuiltInType.UInt16, UInt16.MinValue, UInt16.MinValue.ToString(CultureInfo.InvariantCulture), null, true }, + { BuiltInType.UInt16, UInt16.MinValue, UInt16.MinValue.ToString(CultureInfo.InvariantCulture), null, null, UInt16.MinValue.ToString(CultureInfo.InvariantCulture), true }, { BuiltInType.UInt16, UInt16.MaxValue, UInt16.MaxValue.ToString(CultureInfo.InvariantCulture), null }, - { BuiltInType.Int16, (Int16)0, null, null }, - { BuiltInType.Int16, (Int16)0, "0", null, true }, + { BuiltInType.Int16, (Int16)0, null, null,null, "0" }, + { BuiltInType.Int16, (Int16)0, "0", null, null, "0", true }, { BuiltInType.Int16, (Int16)(-12345), "-12345", null }, { BuiltInType.Int16, (Int16)12345, "12345", null }, { BuiltInType.Int16, Int16.MaxValue, Int16.MaxValue.ToString(CultureInfo.InvariantCulture), null }, { BuiltInType.Int16, Int16.MinValue, Int16.MinValue.ToString(CultureInfo.InvariantCulture), null }, - { BuiltInType.UInt32, (UInt32)0, null, null }, - { BuiltInType.UInt32, (UInt32)0, "0", null, true }, + { BuiltInType.UInt32, (UInt32)0, null, null,null, "0" }, + { BuiltInType.UInt32, (UInt32)0, "0", null, null, "0", true }, { BuiltInType.UInt32, (UInt32)1234567, "1234567", null }, { BuiltInType.UInt32, (UInt32)4444444, "4444444", null }, - { BuiltInType.UInt32, UInt32.MinValue, UInt32.MinValue.ToString(CultureInfo.InvariantCulture), null, true }, + { BuiltInType.UInt32, UInt32.MinValue, UInt32.MinValue.ToString(CultureInfo.InvariantCulture), null, null, UInt32.MinValue.ToString(CultureInfo.InvariantCulture), true }, { BuiltInType.UInt32, UInt32.MaxValue, UInt32.MaxValue.ToString(CultureInfo.InvariantCulture), null }, - { BuiltInType.Int32, 0, null, null }, - { BuiltInType.Int32, 0, "0", null, true }, + { BuiltInType.Int32, 0, null, null,null, "0" }, + { BuiltInType.Int32, 0, "0", null, null, "0", true }, { BuiltInType.Int32, -12345678, "-12345678", null }, { BuiltInType.Int32, 12345678, "12345678", null }, { BuiltInType.Int32, Int32.MaxValue, Int32.MaxValue.ToString(CultureInfo.InvariantCulture), null }, { BuiltInType.Int32, Int32.MinValue, Int32.MinValue.ToString(CultureInfo.InvariantCulture), null }, - { BuiltInType.Int64, (Int64)0, null, null }, - { BuiltInType.Int64, (Int64)0, Quotes("0"), null, true }, + { BuiltInType.Int64, (Int64)0, null, null,null, Quotes("0") }, + { BuiltInType.Int64, (Int64)0, Quotes("0"), null, null, Quotes("0"), true }, { BuiltInType.Int64, kInt64Value, Quotes(kInt64Value.ToString(CultureInfo.InvariantCulture)), null }, { BuiltInType.Int64, (Int64)kUInt64Value, Quotes(kUInt64Value.ToString(CultureInfo.InvariantCulture)), null }, { BuiltInType.Int64, Int64.MinValue, Quotes(Int64.MinValue.ToString(CultureInfo.InvariantCulture)), null }, { BuiltInType.Int64, Int64.MaxValue, Quotes(Int64.MaxValue.ToString(CultureInfo.InvariantCulture)), null }, - { BuiltInType.UInt64, (UInt64)0, null, null }, - { BuiltInType.UInt64, (UInt64)0, Quotes("0"), null, true }, + { BuiltInType.UInt64, (UInt64)0, null, null,null, Quotes("0") }, + { BuiltInType.UInt64, (UInt64)0, Quotes("0"), null, null, Quotes("0"), true }, { BuiltInType.UInt64, (UInt64)kUInt64Value, Quotes(kUInt64Value.ToString(CultureInfo.InvariantCulture)), null }, - { BuiltInType.UInt64, UInt64.MinValue, Quotes(UInt64.MinValue.ToString(CultureInfo.InvariantCulture)), null, true }, + { BuiltInType.UInt64, UInt64.MinValue, Quotes(UInt64.MinValue.ToString(CultureInfo.InvariantCulture)), null, null, Quotes(UInt64.MinValue.ToString(CultureInfo.InvariantCulture)), true }, { BuiltInType.UInt64, UInt64.MaxValue, Quotes(UInt64.MaxValue.ToString(CultureInfo.InvariantCulture)), null }, - { BuiltInType.Float, (Single)0, null, null}, - { BuiltInType.Float, (Single)0, "0", null, true}, + { BuiltInType.Float, (Single)0, null, null,null, "0"}, + { BuiltInType.Float, (Single)0, "0", null, null, "0", true}, { BuiltInType.Float, (Single)(-12345678.1234), Convert.ToSingle("-12345678.1234", CultureInfo.InvariantCulture).ToString("R",CultureInfo.InvariantCulture), null }, { BuiltInType.Float, (Single)12345678.1234, Convert.ToSingle("12345678.1234", CultureInfo.InvariantCulture).ToString("R",CultureInfo.InvariantCulture), null }, { BuiltInType.Float, Single.MaxValue, Single.MaxValue.ToString("R",CultureInfo.InvariantCulture), null }, @@ -153,8 +153,8 @@ public class JsonEncoderTests : EncoderCommon { BuiltInType.Float, Single.PositiveInfinity, Quotes("Infinity"), null }, { BuiltInType.Float, Single.NaN, Quotes("NaN"), null }, - { BuiltInType.Double, (Double)0, null, null}, - { BuiltInType.Double, (Double)0, "0", null, true}, + { BuiltInType.Double, (Double)0, null, null,null, "0"}, + { BuiltInType.Double, (Double)0, "0", null, null, "0", true}, { BuiltInType.Double, (Double)(-12345678.1234), Convert.ToDouble("-12345678.1234", CultureInfo.InvariantCulture).ToString("R",CultureInfo.InvariantCulture), null }, { BuiltInType.Double, (Double)12345678.1234, Convert.ToDouble("12345678.1234", CultureInfo.InvariantCulture).ToString("R",CultureInfo.InvariantCulture), null }, { BuiltInType.Double, Double.MaxValue, Double.MaxValue.ToString("R",CultureInfo.InvariantCulture), null }, @@ -165,113 +165,190 @@ public class JsonEncoderTests : EncoderCommon { BuiltInType.DateTime, Utils.TimeBase, Quotes("1601-01-01T00:00:00Z"), null , true}, { BuiltInType.DateTime, Utils.TimeBase.ToUniversalTime(), Quotes("1601-01-01T00:00:00Z"), null }, - { BuiltInType.DateTime, DateTime.MinValue, null, null }, - { BuiltInType.DateTime, DateTime.MinValue, Quotes("0001-01-01T00:00:00Z"), null, true }, + { BuiltInType.DateTime, DateTime.MinValue, null, null, null, Quotes("0001-01-01T00:00:00Z") }, + { BuiltInType.DateTime, DateTime.MinValue, Quotes("0001-01-01T00:00:00Z"), null, null, Quotes("0001-01-01T00:00:00Z"), true }, { BuiltInType.DateTime, DateTime.MaxValue, Quotes("9999-12-31T23:59:59Z"), null }, - { BuiltInType.Guid, Uuid.Empty, null, null }, - { BuiltInType.Guid, Uuid.Empty, Quotes("00000000-0000-0000-0000-000000000000"), null, true }, + { BuiltInType.Guid, Uuid.Empty, null, null, null, Quotes("00000000-0000-0000-0000-000000000000") }, + { BuiltInType.Guid, Uuid.Empty, Quotes("00000000-0000-0000-0000-000000000000"), null, null, Quotes("00000000-0000-0000-0000-000000000000"), true }, { BuiltInType.Guid, new Uuid(s_nodeIdGuid), Quotes($"{s_nodeIdGuid}"), null }, { BuiltInType.NodeId, NodeId.Null, null, null }, - { BuiltInType.NodeId, new NodeId(kNodeIdInt), $"{{\"Id\":{kNodeIdInt}}}", null }, - { BuiltInType.NodeId, new NodeId(kNodeIdInt,1), $"{{\"Id\":{kNodeIdInt},\"Namespace\":1}}", null }, + { BuiltInType.NodeId, new NodeId(kNodeIdInt), $"{{\"Id\":{kNodeIdInt}}}", null, $"\"i={kNodeIdInt}\"", null }, + { BuiltInType.NodeId, new NodeId(kNodeIdInt,1), + $"{{\"Id\":{kNodeIdInt},\"Namespace\":1}}", $"{{\"Id\":{kNodeIdInt},\"Namespace\":\"{kApplicationUri}\"}}", + $"\"nsu={kApplicationUri};i={kNodeIdInt}\"", null }, { BuiltInType.NodeId, new NodeId(kNodeIdInt,kDemoServerIndex), - $"{{\"Id\":{kNodeIdInt},\"Namespace\":{kDemoServerIndex}}}", $"{{\"Id\":{kNodeIdInt},\"Namespace\":\"{kDemoServer}\"}}" }, - { BuiltInType.NodeId, new NodeId(kNodeIdInt,88), $"{{\"Id\":{kNodeIdInt},\"Namespace\":88}}", null}, - { BuiltInType.NodeId, new NodeId("ns=0;"+kNodeIdString), $"{{\"IdType\":1,\"Id\":\"{kNodeIdString}\"}}", null }, - { BuiltInType.NodeId, new NodeId("s="+kNodeIdString), $"{{\"IdType\":1,\"Id\":\"{kNodeIdString}\"}}", null }, - { BuiltInType.NodeId, new NodeId(kNodeIdString,0), $"{{\"IdType\":1,\"Id\":\"{kNodeIdString}\"}}", null }, - { BuiltInType.NodeId, new NodeId(kNodeIdString,1), $"{{\"IdType\":1,\"Id\":\"{kNodeIdString}\",\"Namespace\":1}}", null }, + $"{{\"Id\":{kNodeIdInt},\"Namespace\":{kDemoServerIndex}}}", $"{{\"Id\":{kNodeIdInt},\"Namespace\":\"{kDemoServer}\"}}", + $"\"nsu={kDemoServer};i={kNodeIdInt}\"", null}, + { BuiltInType.NodeId, new NodeId(kNodeIdInt,88), $"{{\"Id\":{kNodeIdInt},\"Namespace\":88}}", null, $"\"ns=88;i={kNodeIdInt}\"", null }, + { BuiltInType.NodeId, new NodeId("ns=0;"+kNodeIdString), $"{{\"IdType\":1,\"Id\":\"{kNodeIdString}\"}}", null, $"\"s={kNodeIdString}\"", null }, + { BuiltInType.NodeId, new NodeId("s="+kNodeIdString), $"{{\"IdType\":1,\"Id\":\"{kNodeIdString}\"}}", null, $"\"s={kNodeIdString}\"", null }, + { BuiltInType.NodeId, new NodeId(kNodeIdString,0), $"{{\"IdType\":1,\"Id\":\"{kNodeIdString}\"}}", null,$"\"s={kNodeIdString}\"", null }, + { BuiltInType.NodeId, new NodeId(kNodeIdString,1), + $"{{\"IdType\":1,\"Id\":\"{kNodeIdString}\",\"Namespace\":1}}", $"{{\"IdType\":1,\"Id\":\"{kNodeIdString}\",\"Namespace\":\"{kApplicationUri}\"}}", + $"\"nsu={kApplicationUri};s={kNodeIdString}\"", null }, { BuiltInType.NodeId, new NodeId(kNodeIdString,kDemoServerIndex), - $"{{\"IdType\":1,\"Id\":\"{kNodeIdString}\",\"Namespace\":{kDemoServerIndex}}}", - $"{{\"IdType\":1,\"Id\":\"{kNodeIdString}\",\"Namespace\":\"{kDemoServer}\"}}" }, - { BuiltInType.NodeId, new NodeId(kNodeIdString,88), $"{{\"IdType\":1,\"Id\":\"{kNodeIdString}\",\"Namespace\":88}}", null}, - { BuiltInType.NodeId, new NodeId(s_nodeIdGuid), $"{{\"IdType\":2,\"Id\":\"{s_nodeIdGuid}\"}}", null }, - { BuiltInType.NodeId, new NodeId(s_nodeIdGuid,1), $"{{\"IdType\":2,\"Id\":\"{s_nodeIdGuid}\",\"Namespace\":1}}", null }, + $"{{\"IdType\":1,\"Id\":\"{kNodeIdString}\",\"Namespace\":{kDemoServerIndex}}}", + $"{{\"IdType\":1,\"Id\":\"{kNodeIdString}\",\"Namespace\":\"{kDemoServer}\"}}", + $"\"nsu={kDemoServer};s={kNodeIdString}\"", null}, + { BuiltInType.NodeId, new NodeId(kNodeIdString,88), $"{{\"IdType\":1,\"Id\":\"{kNodeIdString}\",\"Namespace\":88}}", null,$"\"ns=88;s={kNodeIdString}\"", null}, + { BuiltInType.NodeId, new NodeId(s_nodeIdGuid), $"{{\"IdType\":2,\"Id\":\"{s_nodeIdGuid}\"}}", null, $"\"g={s_nodeIdGuid}\"", null }, + { BuiltInType.NodeId, new NodeId(s_nodeIdGuid,1), + $"{{\"IdType\":2,\"Id\":\"{s_nodeIdGuid}\",\"Namespace\":1}}", $"{{\"IdType\":2,\"Id\":\"{s_nodeIdGuid}\",\"Namespace\":\"{kApplicationUri}\"}}", + $"\"nsu={kApplicationUri};g={s_nodeIdGuid}\"", null }, { BuiltInType.NodeId, new NodeId(s_nodeIdGuid,kDemoServerIndex), - $"{{\"IdType\":2,\"Id\":\"{s_nodeIdGuid}\",\"Namespace\":{kDemoServerIndex}}}", - $"{{\"IdType\":2,\"Id\":\"{s_nodeIdGuid}\",\"Namespace\":\"{kDemoServer}\"}}" }, - { BuiltInType.NodeId, new NodeId(s_nodeIdGuid,88), $"{{\"IdType\":2,\"Id\":\"{s_nodeIdGuid}\",\"Namespace\":88}}", null}, - { BuiltInType.NodeId, new NodeId(s_byteString), $"{{\"IdType\":3,\"Id\":\"{s_byteString64}\"}}", null }, - { BuiltInType.NodeId, new NodeId(s_byteString,1), $"{{\"IdType\":3,\"Id\":\"{s_byteString64}\",\"Namespace\":1}}", null }, + $"{{\"IdType\":2,\"Id\":\"{s_nodeIdGuid}\",\"Namespace\":{kDemoServerIndex}}}", + $"{{\"IdType\":2,\"Id\":\"{s_nodeIdGuid}\",\"Namespace\":\"{kDemoServer}\"}}" , + $"\"nsu={kDemoServer};g={s_nodeIdGuid}\"", null}, + { BuiltInType.NodeId, new NodeId(s_nodeIdGuid,88), $"{{\"IdType\":2,\"Id\":\"{s_nodeIdGuid}\",\"Namespace\":88}}", null,$"\"ns=88;g={s_nodeIdGuid}\"", null}, + { BuiltInType.NodeId, new NodeId(s_byteString), $"{{\"IdType\":3,\"Id\":\"{s_byteString64}\"}}", null, $"\"b={s_byteString64}\"", null }, + { BuiltInType.NodeId, new NodeId(s_byteString,1), + $"{{\"IdType\":3,\"Id\":\"{s_byteString64}\",\"Namespace\":1}}", $"{{\"IdType\":3,\"Id\":\"{s_byteString64}\",\"Namespace\":\"{kApplicationUri}\"}}", + $"\"nsu={kApplicationUri};b={s_byteString64}\"", null }, { BuiltInType.NodeId, new NodeId(s_byteString,kDemoServerIndex), - $"{{\"IdType\":3,\"Id\":\"{s_byteString64}\",\"Namespace\":{kDemoServerIndex}}}", - $"{{\"IdType\":3,\"Id\":\"{s_byteString64}\",\"Namespace\":\"{kDemoServer}\"}}" }, - { BuiltInType.NodeId, new NodeId(s_byteString,88), $"{{\"IdType\":3,\"Id\":\"{s_byteString64}\",\"Namespace\":88}}", null}, + $"{{\"IdType\":3,\"Id\":\"{s_byteString64}\",\"Namespace\":{kDemoServerIndex}}}", + $"{{\"IdType\":3,\"Id\":\"{s_byteString64}\",\"Namespace\":\"{kDemoServer}\"}}", + $"\"nsu={kDemoServer};b={s_byteString64}\"", null}, + { BuiltInType.NodeId, new NodeId(s_byteString,88), $"{{\"IdType\":3,\"Id\":\"{s_byteString64}\",\"Namespace\":88}}", null,$"\"ns=88;b={s_byteString64}\"", null }, // TODO: add cases for serverIndex { BuiltInType.ExpandedNodeId, ExpandedNodeId.Null, null, null }, - { BuiltInType.ExpandedNodeId, new ExpandedNodeId(kNodeIdInt), $"{{\"Id\":{kNodeIdInt}}}", null }, - { BuiltInType.ExpandedNodeId, new ExpandedNodeId(kNodeIdInt,1), $"{{\"Id\":{kNodeIdInt},\"Namespace\":1}}", null }, + { BuiltInType.ExpandedNodeId, new ExpandedNodeId(kNodeIdInt), + $"{{\"Id\":{kNodeIdInt}}}", null, + $"\"i={kNodeIdInt}\"", null}, + { BuiltInType.ExpandedNodeId, new ExpandedNodeId(kNodeIdInt,1), + $"{{\"Id\":{kNodeIdInt},\"Namespace\":1}}", $"{{\"Id\":{kNodeIdInt},\"Namespace\":\"{kApplicationUri}\"}}", + $"\"nsu={kApplicationUri};i={kNodeIdInt}\"", null}, { BuiltInType.ExpandedNodeId, new ExpandedNodeId(kNodeIdInt,kDemoServerIndex), - $"{{\"Id\":{kNodeIdInt},\"Namespace\":{kDemoServerIndex}}}", $"{{\"Id\":{kNodeIdInt},\"Namespace\":\"{kDemoServer}\"}}" }, + $"{{\"Id\":{kNodeIdInt},\"Namespace\":{kDemoServerIndex}}}", $"{{\"Id\":{kNodeIdInt},\"Namespace\":\"{kDemoServer}\"}}", + $"\"nsu={kDemoServer};i={kNodeIdInt}\"", null}, { BuiltInType.ExpandedNodeId, new ExpandedNodeId(kNodeIdInt,kDemoServer2), - $"{{\"Id\":{kNodeIdInt},\"Namespace\":\"{kDemoServer2}\"}}", $"{{\"Id\":{kNodeIdInt},\"Namespace\":\"{kDemoServer2}\"}}" }, - { BuiltInType.ExpandedNodeId, new ExpandedNodeId(kNodeIdInt,88), $"{{\"Id\":{kNodeIdInt},\"Namespace\":88}}", null}, - { BuiltInType.ExpandedNodeId, new ExpandedNodeId("ns=0;"+kNodeIdString), $"{{\"IdType\":1,\"Id\":\"{kNodeIdString}\"}}", null }, - { BuiltInType.ExpandedNodeId, new ExpandedNodeId("s="+kNodeIdString), $"{{\"IdType\":1,\"Id\":\"{kNodeIdString}\"}}", null }, - { BuiltInType.ExpandedNodeId, new ExpandedNodeId(kNodeIdString,0), $"{{\"IdType\":1,\"Id\":\"{kNodeIdString}\"}}", null }, - { BuiltInType.ExpandedNodeId, new ExpandedNodeId(kNodeIdString,1), $"{{\"IdType\":1,\"Id\":\"{kNodeIdString}\",\"Namespace\":1}}", null }, + $"{{\"Id\":{kNodeIdInt},\"Namespace\":\"{kDemoServer2}\"}}", $"{{\"Id\":{kNodeIdInt},\"Namespace\":\"{kDemoServer2}\"}}", + $"\"nsu={kDemoServer2};i={kNodeIdInt}\"", null}, + { BuiltInType.ExpandedNodeId, new ExpandedNodeId(kNodeIdInt,88), + $"{{\"Id\":{kNodeIdInt},\"Namespace\":88}}", null, + $"\"ns=88;i={kNodeIdInt}\"", null}, + { BuiltInType.ExpandedNodeId, new ExpandedNodeId("ns=0;"+kNodeIdString), + $"{{\"IdType\":1,\"Id\":\"{kNodeIdString}\"}}", null, + $"\"s={kNodeIdString}\"", null}, + { BuiltInType.ExpandedNodeId, new ExpandedNodeId("s="+kNodeIdString), + $"{{\"IdType\":1,\"Id\":\"{kNodeIdString}\"}}", null, + $"\"s={kNodeIdString}\"", null}, + { BuiltInType.ExpandedNodeId, new ExpandedNodeId(kNodeIdString,0), + $"{{\"IdType\":1,\"Id\":\"{kNodeIdString}\"}}", null, + $"\"s={kNodeIdString}\"", null}, + { BuiltInType.ExpandedNodeId, new ExpandedNodeId(kNodeIdString,1), + $"{{\"IdType\":1,\"Id\":\"{kNodeIdString}\",\"Namespace\":1}}", $"{{\"IdType\":1,\"Id\":\"{kNodeIdString}\",\"Namespace\":\"{kApplicationUri}\"}}", + $"\"nsu={kApplicationUri};s={kNodeIdString}\"", null}, { BuiltInType.ExpandedNodeId, new ExpandedNodeId(kNodeIdString,kDemoServerIndex), - $"{{\"IdType\":1,\"Id\":\"{kNodeIdString}\",\"Namespace\":{kDemoServerIndex}}}", - $"{{\"IdType\":1,\"Id\":\"{kNodeIdString}\",\"Namespace\":\"{kDemoServer}\"}}" }, + $"{{\"IdType\":1,\"Id\":\"{kNodeIdString}\",\"Namespace\":{kDemoServerIndex}}}", + $"{{\"IdType\":1,\"Id\":\"{kNodeIdString}\",\"Namespace\":\"{kDemoServer}\"}}", + $"\"nsu={kDemoServer};s={kNodeIdString}\"", null}, { BuiltInType.ExpandedNodeId, new ExpandedNodeId(kNodeIdString,kDemoServer2), - $"{{\"IdType\":1,\"Id\":\"{kNodeIdString}\",\"Namespace\":\"{kDemoServer2}\"}}", - $"{{\"IdType\":1,\"Id\":\"{kNodeIdString}\",\"Namespace\":\"{kDemoServer2}\"}}" }, - { BuiltInType.ExpandedNodeId, new ExpandedNodeId(kNodeIdString,88), $"{{\"IdType\":1,\"Id\":\"{kNodeIdString}\",\"Namespace\":88}}", null}, - { BuiltInType.ExpandedNodeId, new ExpandedNodeId(s_nodeIdGuid), $"{{\"IdType\":2,\"Id\":\"{s_nodeIdGuid}\"}}", null }, - { BuiltInType.ExpandedNodeId, new ExpandedNodeId(s_nodeIdGuid, 1), $"{{\"IdType\":2,\"Id\":\"{s_nodeIdGuid}\",\"Namespace\":1}}", null }, + $"{{\"IdType\":1,\"Id\":\"{kNodeIdString}\",\"Namespace\":\"{kDemoServer2}\"}}", + $"{{\"IdType\":1,\"Id\":\"{kNodeIdString}\",\"Namespace\":\"{kDemoServer2}\"}}", + $"\"nsu={kDemoServer2};s={kNodeIdString}\"", null}, + { BuiltInType.ExpandedNodeId, new ExpandedNodeId(kNodeIdString,88), + $"{{\"IdType\":1,\"Id\":\"{kNodeIdString}\",\"Namespace\":88}}", null, + $"\"ns=88;s={kNodeIdString}\"", null}, + { BuiltInType.ExpandedNodeId, new ExpandedNodeId(s_nodeIdGuid), + $"{{\"IdType\":2,\"Id\":\"{s_nodeIdGuid}\"}}", null, + $"\"g={s_nodeIdGuid}\"", null}, + { BuiltInType.ExpandedNodeId, new ExpandedNodeId(s_nodeIdGuid, 1), + $"{{\"IdType\":2,\"Id\":\"{s_nodeIdGuid}\",\"Namespace\":1}}", $"{{\"IdType\":2,\"Id\":\"{s_nodeIdGuid}\",\"Namespace\":\"{kApplicationUri}\"}}", + $"\"nsu={kApplicationUri};g={s_nodeIdGuid}\"", null}, { BuiltInType.ExpandedNodeId, new ExpandedNodeId(s_nodeIdGuid, kDemoServerIndex), - $"{{\"IdType\":2,\"Id\":\"{s_nodeIdGuid}\",\"Namespace\":{kDemoServerIndex}}}", - $"{{\"IdType\":2,\"Id\":\"{s_nodeIdGuid}\",\"Namespace\":\"{kDemoServer}\"}}" }, + $"{{\"IdType\":2,\"Id\":\"{s_nodeIdGuid}\",\"Namespace\":{kDemoServerIndex}}}", + $"{{\"IdType\":2,\"Id\":\"{s_nodeIdGuid}\",\"Namespace\":\"{kDemoServer}\"}}", + $"\"nsu={kDemoServer};g={s_nodeIdGuid}\"", null}, { BuiltInType.ExpandedNodeId, new ExpandedNodeId(s_nodeIdGuid, kDemoServer2), - $"{{\"IdType\":2,\"Id\":\"{s_nodeIdGuid}\",\"Namespace\":\"{kDemoServer2}\"}}", - $"{{\"IdType\":2,\"Id\":\"{s_nodeIdGuid}\",\"Namespace\":\"{kDemoServer2}\"}}" }, - { BuiltInType.ExpandedNodeId, new ExpandedNodeId(s_nodeIdGuid,88), $"{{\"IdType\":2,\"Id\":\"{s_nodeIdGuid}\",\"Namespace\":88}}", null}, - { BuiltInType.ExpandedNodeId, new ExpandedNodeId(s_byteString), $"{{\"IdType\":3,\"Id\":\"{s_byteString64}\"}}", null }, - { BuiltInType.ExpandedNodeId, new ExpandedNodeId(s_byteString,1), $"{{\"IdType\":3,\"Id\":\"{s_byteString64}\",\"Namespace\":1}}", null }, + $"{{\"IdType\":2,\"Id\":\"{s_nodeIdGuid}\",\"Namespace\":\"{kDemoServer2}\"}}", + $"{{\"IdType\":2,\"Id\":\"{s_nodeIdGuid}\",\"Namespace\":\"{kDemoServer2}\"}}", + $"\"nsu={kDemoServer2};g={s_nodeIdGuid}\"", null}, + { BuiltInType.ExpandedNodeId, new ExpandedNodeId(s_nodeIdGuid,88), + $"{{\"IdType\":2,\"Id\":\"{s_nodeIdGuid}\",\"Namespace\":88}}", null, + $"\"ns=88;g={s_nodeIdGuid}\"", null}, + { BuiltInType.ExpandedNodeId, new ExpandedNodeId(s_byteString), + $"{{\"IdType\":3,\"Id\":\"{s_byteString64}\"}}", null, + $"\"b={s_byteString64}\"", null}, + { BuiltInType.ExpandedNodeId, new ExpandedNodeId(s_byteString,1), + $"{{\"IdType\":3,\"Id\":\"{s_byteString64}\",\"Namespace\":1}}", $"{{\"IdType\":3,\"Id\":\"{s_byteString64}\",\"Namespace\":\"{kApplicationUri}\"}}", + $"\"nsu={kApplicationUri};b={s_byteString64}\"", null}, { BuiltInType.ExpandedNodeId, new ExpandedNodeId(s_byteString,kDemoServerIndex), - $"{{\"IdType\":3,\"Id\":\"{s_byteString64}\",\"Namespace\":{kDemoServerIndex}}}", - $"{{\"IdType\":3,\"Id\":\"{s_byteString64}\",\"Namespace\":\"{kDemoServer}\"}}" }, + $"{{\"IdType\":3,\"Id\":\"{s_byteString64}\",\"Namespace\":{kDemoServerIndex}}}", + $"{{\"IdType\":3,\"Id\":\"{s_byteString64}\",\"Namespace\":\"{kDemoServer}\"}}", + $"\"nsu={kDemoServer};b={s_byteString64}\"", null}, { BuiltInType.ExpandedNodeId, new ExpandedNodeId(s_byteString,kDemoServer2), - $"{{\"IdType\":3,\"Id\":\"{s_byteString64}\",\"Namespace\":\"{kDemoServer2}\"}}", - $"{{\"IdType\":3,\"Id\":\"{s_byteString64}\",\"Namespace\":\"{kDemoServer2}\"}}" }, - { BuiltInType.ExpandedNodeId, new ExpandedNodeId(s_byteString,88), $"{{\"IdType\":3,\"Id\":\"{s_byteString64}\",\"Namespace\":88}}", null}, - - { BuiltInType.StatusCode, new StatusCode(StatusCodes.Good), null, null}, - { BuiltInType.StatusCode, new StatusCode(StatusCodes.Good), $"{StatusCodes.Good}", "", true}, + $"{{\"IdType\":3,\"Id\":\"{s_byteString64}\",\"Namespace\":\"{kDemoServer2}\"}}", + $"{{\"IdType\":3,\"Id\":\"{s_byteString64}\",\"Namespace\":\"{kDemoServer2}\"}}", + $"\"nsu={kDemoServer2};b={s_byteString64}\"", null}, + { BuiltInType.ExpandedNodeId, new ExpandedNodeId(s_byteString,88), + $"{{\"IdType\":3,\"Id\":\"{s_byteString64}\",\"Namespace\":88}}", null, + $"\"ns=88;b={s_byteString64}\"", null}, + + { BuiltInType.StatusCode, new StatusCode(StatusCodes.Good), null, null, null, null}, + { BuiltInType.StatusCode, new StatusCode(StatusCodes.Good), $"{StatusCodes.Good}", "", null, null, true}, { BuiltInType.StatusCode, new StatusCode(StatusCodes.BadBoundNotFound), $"{StatusCodes.BadBoundNotFound}", - $"{{\"Code\":{StatusCodes.BadBoundNotFound}, \"Symbol\":\"{nameof(StatusCodes.BadBoundNotFound)}\"}}"}, + $"{{\"Code\":{StatusCodes.BadBoundNotFound}, \"Symbol\":\"{nameof(StatusCodes.BadBoundNotFound)}\"}}"}, { BuiltInType.StatusCode, new StatusCode(StatusCodes.BadCertificateInvalid), - $"{StatusCodes.BadCertificateInvalid}", $"{{\"Code\":{StatusCodes.BadCertificateInvalid}, \"Symbol\":\"{nameof(StatusCodes.BadCertificateInvalid)}\"}}"}, + $"{StatusCodes.BadCertificateInvalid}", $"{{\"Code\":{StatusCodes.BadCertificateInvalid}, \"Symbol\":\"{nameof(StatusCodes.BadCertificateInvalid)}\"}}"}, + { BuiltInType.StatusCode, new StatusCode(1234567), "1234567", $"{{\"Code\":1234567}}"}, { BuiltInType.DiagnosticInfo, new DiagnosticInfo(), null, null}, { BuiltInType.DiagnosticInfo, new DiagnosticInfo(-1,-1,-1,-1,null), null, null}, { BuiltInType.DiagnosticInfo, new DiagnosticInfo(1,2,3,4,"AdditionalInfo"), "{\"SymbolicId\":1,\"NamespaceUri\":2,\"Locale\":3,\"LocalizedText\":4,\"AdditionalInfo\":\"AdditionalInfo\"}", null}, { BuiltInType.QualifiedName, QualifiedName.Null, null, null}, - { BuiltInType.QualifiedName, new QualifiedName(kQualifiedName), $"{{\"Name\":\"{kQualifiedName}\"}}", null}, - { BuiltInType.QualifiedName, new QualifiedName(kQualifiedName, 1), $"{{\"Name\":\"{kQualifiedName}\",\"Uri\":1}}", $"{{\"Name\":\"{kQualifiedName}\",\"Uri\":1}}"}, + { BuiltInType.QualifiedName, new QualifiedName(kQualifiedName), + $"{{\"Name\":\"{kQualifiedName}\"}}", null, + $"\"{kQualifiedName}\"", null}, + { BuiltInType.QualifiedName, new QualifiedName(kQualifiedName, 1), + $"{{\"Name\":\"{kQualifiedName}\",\"Uri\":1}}", + $"{{\"Name\":\"{kQualifiedName}\",\"Uri\":\"{kApplicationUri}\"}}", + $"\"nsu={kApplicationUri};{kQualifiedName}\"", null}, { BuiltInType.QualifiedName, new QualifiedName(kQualifiedName, kDemoServerIndex), - $"{{\"Name\":\"{kQualifiedName}\",\"Uri\":{kDemoServerIndex}}}", $"{{\"Name\":\"{kQualifiedName}\",\"Uri\":\"{kDemoServer}\"}}"}, + $"{{\"Name\":\"{kQualifiedName}\",\"Uri\":{kDemoServerIndex}}}", + $"{{\"Name\":\"{kQualifiedName}\",\"Uri\":\"{kDemoServer}\"}}", + $"\"nsu={kDemoServer};{kQualifiedName}\"", null}, { BuiltInType.LocalizedText, LocalizedText.Null, null, null}, - { BuiltInType.LocalizedText, new LocalizedText(kLocalizedText), $"{{\"Text\":\"{kLocalizedText}\"}}", $"\"{kLocalizedText}\"", true}, - { BuiltInType.LocalizedText, new LocalizedText(kLocale, kLocalizedText), $"{{\"Text\":\"{kLocalizedText}\",\"Locale\":\"{kLocale}\"}}", $"\"{kLocalizedText}\""}, + { BuiltInType.LocalizedText, new LocalizedText(kLocalizedText), + $"{{\"Text\":\"{kLocalizedText}\"}}", $"\"{kLocalizedText}\"", + $"{{\"Text\":\"{kLocalizedText}\"}}", null, + true}, + { BuiltInType.LocalizedText, new LocalizedText(kLocale, kLocalizedText), + $"{{\"Text\":\"{kLocalizedText}\",\"Locale\":\"{kLocale}\"}}", $"\"{kLocalizedText}\"", + $"{{\"Text\":\"{kLocalizedText}\",\"Locale\":\"{kLocale}\"}}", null}, { BuiltInType.ExtensionObject, ExtensionObject.Null, null, null}, { BuiltInType.ExtensionObject, new ExtensionObject(kNodeIdInt), null, null}, { BuiltInType.ExtensionObject, new ExtensionObject((IEncodeable) null), null, null}, { BuiltInType.Variant, Variant.Null, "", null}, - { BuiltInType.Variant, new Variant((SByte)123), $"{{\"Type\":{BuiltInType.SByte.ToString("d")}, \"Body\":123}}", "123"}, - { BuiltInType.Variant, new Variant((Int16)12345), $"{{\"Type\":{BuiltInType.Int16.ToString("d")}, \"Body\":12345}}", "12345"}, - { BuiltInType.Variant, new Variant(1234567), $"{{\"Type\":{BuiltInType.Int32.ToString("d")}, \"Body\":1234567}}", "1234567"}, - { BuiltInType.Variant, new Variant((Int64)123456789), $"{{\"Type\":{BuiltInType.Int64.ToString("d")}, \"Body\":\"123456789\"}}", "\"123456789\""}, - { BuiltInType.Variant, new Variant((Byte)123), $"{{\"Type\":{BuiltInType.Byte.ToString("d")}, \"Body\":123}}", "123"}, - { BuiltInType.Variant, new Variant((UInt16)12345), $"{{\"Type\":{BuiltInType.UInt16.ToString("d")}, \"Body\":12345}}", "12345"}, - { BuiltInType.Variant, new Variant((UInt32)1234567), $"{{\"Type\":{BuiltInType.UInt32.ToString("d")}, \"Body\":1234567}}", "1234567"}, - { BuiltInType.Variant, new Variant((UInt64)123456789), $"{{\"Type\":{BuiltInType.UInt64.ToString("d")}, \"Body\":\"123456789\"}}", "\"123456789\""}, + { BuiltInType.Variant, new Variant((SByte)123), + $"{{\"Type\":{BuiltInType.SByte.ToString("d")}, \"Body\":123}}", "123", + $"{{\"Type\":{BuiltInType.SByte.ToString("d")}, \"Body\":123}}", null}, + { BuiltInType.Variant, new Variant((Int16)12345), + $"{{\"Type\":{BuiltInType.Int16.ToString("d")}, \"Body\":12345}}", "12345", + $"{{\"Type\":{BuiltInType.Int16.ToString("d")}, \"Body\":12345}}", null}, + { BuiltInType.Variant, new Variant(1234567), + $"{{\"Type\":{BuiltInType.Int32.ToString("d")}, \"Body\":1234567}}", "1234567", + $"{{\"Type\":{BuiltInType.Int32.ToString("d")}, \"Body\":1234567}}", null}, + { BuiltInType.Variant, new Variant((Int64)123456789), + $"{{\"Type\":{BuiltInType.Int64.ToString("d")}, \"Body\":\"123456789\"}}", "\"123456789\"", + $"{{\"Type\":{BuiltInType.Int64.ToString("d")}, \"Body\":\"123456789\"}}", null}, + { BuiltInType.Variant, new Variant((Byte)123), + $"{{\"Type\":{BuiltInType.Byte.ToString("d")}, \"Body\":123}}", "123", + $"{{\"Type\":{BuiltInType.Byte.ToString("d")}, \"Body\":123}}", null}, + { BuiltInType.Variant, new Variant((UInt16)12345), + $"{{\"Type\":{BuiltInType.UInt16.ToString("d")}, \"Body\":12345}}", "12345", + $"{{\"Type\":{BuiltInType.UInt16.ToString("d")}, \"Body\":12345}}", null}, + { BuiltInType.Variant, new Variant((UInt32)1234567), + $"{{\"Type\":{BuiltInType.UInt32.ToString("d")}, \"Body\":1234567}}", "1234567", + $"{{\"Type\":{BuiltInType.UInt32.ToString("d")}, \"Body\":1234567}}", null}, + { BuiltInType.Variant, new Variant((UInt64)123456789), + $"{{\"Type\":{BuiltInType.UInt64.ToString("d")}, \"Body\":\"123456789\"}}", "\"123456789\"", + $"{{\"Type\":{BuiltInType.UInt64.ToString("d")}, \"Body\":\"123456789\"}}", null}, { BuiltInType.DataValue, new DataValue(), "{}", null}, { BuiltInType.DataValue, new DataValue(StatusCodes.Good), "{}", null}, @@ -291,7 +368,9 @@ public class JsonEncoderTests : EncoderCommon { BuiltInType.Enumeration, s_testInt32Array, "[2,3,10]", "[\"2\",\"3\",\"10\"]"}, // IEncodeable - { BuiltInType.ExtensionObject, s_testEncodeable, "{\"Body\":{\"Foo\":\"bar_999\"}}", "{\"Foo\":\"bar_999\"}"} + { BuiltInType.ExtensionObject, s_testEncodeable, + "{\"Body\":{\"Foo\":\"bar_999\"}}", "{\"Foo\":\"bar_999\"}", + "{\"Body\":{\"Foo\":\"bar_999\"}}", null} }.ToArray(); #endregion @@ -348,6 +427,20 @@ public void GlobalCleanup() #endregion #region Test Methods + [Test] + [TestCase(JsonEncodingType.Compact)] + [TestCase(JsonEncodingType.Verbose)] + public void ForcePropertiesShouldThrow(JsonEncodingType jsonEncodingType) + { + using (var encoder = new JsonEncoder(Context, jsonEncodingType)) + { + Assert.Throws(() => encoder.ForceNamespaceUri = true); + Assert.Throws(() => encoder.ForceNamespaceUriForIndex1 = true); + Assert.Throws(() => encoder.IncludeDefaultNumberValues = true); + Assert.Throws(() => encoder.IncludeDefaultValues = true); + } + } + /// /// Validate constructor signature. /// @@ -581,33 +674,17 @@ public void ConstructorRecyclableMemoryStreamSequence() } /// - /// Verify reversible Json encoding. + /// Verify any Json encoding. /// [Theory] - public void JsonEncodeRev(JsonValidationData jsonValidationData, MemoryStreamType memoryStreamType) + public void JsonEncode(JsonEncodingType jsonEncodingType, JsonValidationData jsonValidationData, MemoryStreamType memoryStreamType) { EncodeJsonVerifyResult( jsonValidationData.BuiltInType, memoryStreamType, jsonValidationData.Instance, - true, - jsonValidationData.ExpectedReversible, - false, - jsonValidationData.IncludeDefaultValue); - } - - /// - /// Verify non reversible Json encoding. - /// - [Theory] - public void JsonEncodeNonRev(JsonValidationData jsonValidationData, MemoryStreamType memoryStreamType) - { - EncodeJsonVerifyResult( - jsonValidationData.BuiltInType, - memoryStreamType, - jsonValidationData.Instance, - false, - jsonValidationData.ExpectedNonReversible ?? jsonValidationData.ExpectedReversible, + jsonEncodingType, + jsonValidationData.GetExpected(jsonEncodingType), false, jsonValidationData.IncludeDefaultValue); } diff --git a/Tests/Opc.Ua.Core.Tests/Types/Encoders/JsonValidationData.cs b/Tests/Opc.Ua.Core.Tests/Types/Encoders/JsonValidationData.cs index dee618097f..a134ab0be6 100644 --- a/Tests/Opc.Ua.Core.Tests/Types/Encoders/JsonValidationData.cs +++ b/Tests/Opc.Ua.Core.Tests/Types/Encoders/JsonValidationData.cs @@ -44,14 +44,40 @@ public JsonValidationData(BuiltInType builtInType) BuiltInType = builtInType; } + public string GetExpected(JsonEncodingType jsonEncodingType) + { + switch (jsonEncodingType) + { + case JsonEncodingType.Verbose: + return ExpectedVerbose ?? ExpectedCompact; + case JsonEncodingType.NonReversible: + return ExpectedNonReversible ?? ExpectedReversible; + case JsonEncodingType.Reversible: + return ExpectedReversible; + default: + case JsonEncodingType.Compact: + return ExpectedCompact; + } + } + public BuiltInType BuiltInType; public object Instance; + public string ExpectedCompact; + public string ExpectedVerbose; public string ExpectedReversible; public string ExpectedNonReversible; public bool IncludeDefaultValue; public string ToString(string format, IFormatProvider formatProvider) { + if (BuiltInType == BuiltInType.Variant && Instance is Variant variant) + { + BuiltInType? builtInType = variant.TypeInfo?.BuiltInType; + if (builtInType != null) + { + return $"Variant:{builtInType}:{Instance}" + (IncludeDefaultValue ? ":Default" : ""); + } + } return $"{BuiltInType}:{Instance}" + (IncludeDefaultValue ? ":Default" : ""); } }; @@ -76,7 +102,28 @@ public void Add( BuiltInType = builtInType, Instance = instance, ExpectedReversible = expectedReversible, - ExpectedNonReversible = expectedNonReversible + ExpectedNonReversible = expectedNonReversible, + ExpectedCompact = expectedReversible, + ExpectedVerbose = expectedNonReversible + }); + } + + public void Add( + BuiltInType builtInType, + object instance, + string expectedReversible, + string expectedNonReversible, + string expectedCompact, + string expectedVerbose + ) + { + Add(new JsonValidationData() { + BuiltInType = builtInType, + Instance = instance, + ExpectedReversible = expectedReversible, + ExpectedNonReversible = expectedNonReversible, + ExpectedCompact = expectedCompact, + ExpectedVerbose = expectedVerbose }); } @@ -92,8 +139,41 @@ public void Add( Instance = instance, ExpectedReversible = expectedReversible, ExpectedNonReversible = expectedNonReversible, + ExpectedCompact = expectedReversible, + ExpectedVerbose = expectedNonReversible, IncludeDefaultValue = includeDefaultValue }); } + + public void Add( + BuiltInType builtInType, + object instance, + string expectedReversible, + string expectedNonReversible, + string expectedCompact, + string expectedVerbose, + bool includeDefaultValue) + { + Add(new JsonValidationData() { + BuiltInType = builtInType, + Instance = instance, + ExpectedReversible = expectedReversible, + ExpectedNonReversible = expectedNonReversible, + ExpectedCompact = expectedCompact, + ExpectedVerbose = expectedVerbose, + IncludeDefaultValue = includeDefaultValue + }); + } + } + + /// + /// Helper as value source for tests. + /// + public class JsonEncodingTypeCollection : List + { + public JsonEncodingTypeCollection(JsonEncodingType[] values) + { + AddRange(values); + } } } diff --git a/Tests/Opc.Ua.PubSub.Tests/Encoding/MessagesHelper.cs b/Tests/Opc.Ua.PubSub.Tests/Encoding/MessagesHelper.cs index db823bfba8..066206140d 100644 --- a/Tests/Opc.Ua.PubSub.Tests/Encoding/MessagesHelper.cs +++ b/Tests/Opc.Ua.PubSub.Tests/Encoding/MessagesHelper.cs @@ -44,6 +44,7 @@ public static class MessagesHelper /// Ua data message type /// private const string UaDataMessageType = "ua-data"; + /// /// Ua metadata message type /// From 3d24c0ed3619e26dbeb5cccbd7e92aee04936636 Mon Sep 17 00:00:00 2001 From: Martin Regen Date: Thu, 26 Sep 2024 12:58:03 +0200 Subject: [PATCH 58/83] Do not create the default folders for DirectoryStore on Open (#2773) For convenience, the DirectoryStore class creates the default folders when the DirectoryStore is opened. (since #2720) However this may lead into issues mapping folders to the store when started in docker. Fix: remove the code which creates default folders and ensure a null directory does not throw a NullReferenceException. --- .../Certificates/DirectoryCertificateStore.cs | 40 ++----------------- 1 file changed, 3 insertions(+), 37 deletions(-) diff --git a/Stack/Opc.Ua.Core/Security/Certificates/DirectoryCertificateStore.cs b/Stack/Opc.Ua.Core/Security/Certificates/DirectoryCertificateStore.cs index c0fcc42882..5d61a0d9a0 100644 --- a/Stack/Opc.Ua.Core/Security/Certificates/DirectoryCertificateStore.cs +++ b/Stack/Opc.Ua.Core/Security/Certificates/DirectoryCertificateStore.cs @@ -109,7 +109,7 @@ public void Open(string location, bool noPrivateKeys = false) NoPrivateKeys = noPrivateKeys; StorePath = location; m_directory = directory; - if (m_noSubDirs) + if (m_noSubDirs || m_directory == null) { m_certificateSubdir = m_directory; m_crlSubdir = m_directory; @@ -125,39 +125,6 @@ public void Open(string location, bool noPrivateKeys = false) // force load ClearCertificates(); m_lastDirectoryCheck = DateTime.MinValue; - - // create folders if they do not exist - try - { - if (!m_directory.Exists) - { - m_directory.Create(); - } - - if (!m_certificateSubdir.Exists) - { - m_certificateSubdir.Create(); - } - - if (noPrivateKeys) - { - if (!m_crlSubdir.Exists) - { - m_crlSubdir.Create(); - } - } - else - { - if (!m_privateKeySubdir.Exists) - { - m_privateKeySubdir.Create(); - } - } - } - catch (IOException ex) - { - Utils.LogError("Failed to create certificate store: {0}", ex.Message); - } } } } @@ -825,7 +792,7 @@ private IDictionary Load(string thumbprint) DateTime now = DateTime.UtcNow; // refresh the directories. - m_certificateSubdir.Refresh(); + m_certificateSubdir?.Refresh(); if (!NoPrivateKeys) { @@ -833,9 +800,8 @@ private IDictionary Load(string thumbprint) } // check if store exists. - if (!m_certificateSubdir.Exists) + if (m_certificateSubdir?.Exists != true) { - m_certificateSubdir.Create(); ClearCertificates(); return m_certificates; } From c16d1f2c5c1ad5cc241ba0584a830544f841d4d8 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 1 Oct 2024 10:26:28 +0200 Subject: [PATCH 59/83] Bump Serilog and System.Diagnostics.DiagnosticSource (#2780) Bumps [Serilog](https://github.com/serilog/serilog) and [System.Diagnostics.DiagnosticSource](https://github.com/dotnet/runtime). These dependencies needed to be updated together. Updates `Serilog` from 4.0.1 to 4.0.2 - [Release notes](https://github.com/serilog/serilog/releases) - [Commits](https://github.com/serilog/serilog/compare/v4.0.1...v4.0.2) Updates `System.Diagnostics.DiagnosticSource` from 6.0.1 to 8.0.1 - [Release notes](https://github.com/dotnet/runtime/releases) - [Commits](https://github.com/dotnet/runtime/compare/v6.0.1...v8.0.1) --- updated-dependencies: - dependency-name: Serilog dependency-type: direct:production update-type: version-update:semver-patch - dependency-name: System.Diagnostics.DiagnosticSource dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .../ConsoleReferenceClient/ConsoleReferenceClient.csproj | 2 +- .../ConsoleReferenceServer/ConsoleReferenceServer.csproj | 2 +- Fuzzing/Encoders/Fuzz.Tools/Encoders.Fuzz.Tools.csproj | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Applications/ConsoleReferenceClient/ConsoleReferenceClient.csproj b/Applications/ConsoleReferenceClient/ConsoleReferenceClient.csproj index f49c23ce5e..bdfd44f97a 100644 --- a/Applications/ConsoleReferenceClient/ConsoleReferenceClient.csproj +++ b/Applications/ConsoleReferenceClient/ConsoleReferenceClient.csproj @@ -24,7 +24,7 @@ - + diff --git a/Applications/ConsoleReferenceServer/ConsoleReferenceServer.csproj b/Applications/ConsoleReferenceServer/ConsoleReferenceServer.csproj index 06b7c27f91..0fb89a57ea 100644 --- a/Applications/ConsoleReferenceServer/ConsoleReferenceServer.csproj +++ b/Applications/ConsoleReferenceServer/ConsoleReferenceServer.csproj @@ -33,7 +33,7 @@ - + diff --git a/Fuzzing/Encoders/Fuzz.Tools/Encoders.Fuzz.Tools.csproj b/Fuzzing/Encoders/Fuzz.Tools/Encoders.Fuzz.Tools.csproj index 0051b36559..3873af9a7c 100644 --- a/Fuzzing/Encoders/Fuzz.Tools/Encoders.Fuzz.Tools.csproj +++ b/Fuzzing/Encoders/Fuzz.Tools/Encoders.Fuzz.Tools.csproj @@ -23,7 +23,7 @@ - + From 00b1c46168b5c47dcda6f6cd9198afd745ffcf6a Mon Sep 17 00:00:00 2001 From: romanett Date: Wed, 9 Oct 2024 06:37:25 +0200 Subject: [PATCH 60/83] [Server] GDS Push: Enable regeneratePrivatekey for CreateSigningRequest method of Server (#2778) * Enable regeneratePrivatekey for CreateSigningRequest method of Server * only put needed info into cert, limit lifetime, use public key to get the key size --- .../Configuration/ConfigurationNodeManager.cs | 48 ++++++++++++++++--- Tests/Opc.Ua.Gds.Tests/PushTest.cs | 15 +++++- 2 files changed, 54 insertions(+), 9 deletions(-) diff --git a/Libraries/Opc.Ua.Server/Configuration/ConfigurationNodeManager.cs b/Libraries/Opc.Ua.Server/Configuration/ConfigurationNodeManager.cs index 55d0f35d77..b63675147d 100644 --- a/Libraries/Opc.Ua.Server/Configuration/ConfigurationNodeManager.cs +++ b/Libraries/Opc.Ua.Server/Configuration/ConfigurationNodeManager.cs @@ -32,7 +32,6 @@ using System.Linq; using System.Security.Cryptography.X509Certificates; using System.Threading.Tasks; -using System.Xml; namespace Opc.Ua.Server { @@ -446,8 +445,18 @@ private ServiceResult UpdateCertificate( case null: case "": { - X509Certificate2 certWithPrivateKey = certificateGroup.ApplicationCertificate.LoadPrivateKeyEx(passwordProvider).Result; - var exportableKey = X509Utils.CreateCopyWithPrivateKey(certWithPrivateKey, false); + X509Certificate2 exportableKey; + //use the new generated private key if one exists and matches the provided public key + if (certificateGroup.TemporaryApplicationCertificate != null && X509Utils.VerifyRSAKeyPair(newCert, certificateGroup.TemporaryApplicationCertificate)) + { + exportableKey = X509Utils.CreateCopyWithPrivateKey(certificateGroup.TemporaryApplicationCertificate, false); + } + else + { + X509Certificate2 certWithPrivateKey = certificateGroup.ApplicationCertificate.LoadPrivateKeyEx(passwordProvider).Result; + exportableKey = X509Utils.CreateCopyWithPrivateKey(certWithPrivateKey, false); + } + updateCertificate.CertificateWithPrivateKey = CertificateFactory.CreateCertificateWithPrivateKey(newCert, exportableKey); break; } @@ -463,6 +472,10 @@ private ServiceResult UpdateCertificate( break; } } + //dispose temporary new private key as it is no longer needed + certificateGroup.TemporaryApplicationCertificate?.Dispose(); + certificateGroup.TemporaryApplicationCertificate = null; + updateCertificate.IssuerCollection = newIssuerCollection; updateCertificate.SessionId = context.SessionId; } @@ -553,11 +566,30 @@ private ServiceResult CreateSigningRequest( throw new ArgumentNullException(nameof(subjectName)); } - // TODO: implement regeneratePrivateKey - // TODO: use nonce for generating the private key - var passwordProvider = m_configuration.SecurityConfiguration.CertificatePasswordProvider; + certificateGroup.TemporaryApplicationCertificate?.Dispose(); + certificateGroup.TemporaryApplicationCertificate = null; + + ICertificatePasswordProvider passwordProvider = m_configuration.SecurityConfiguration.CertificatePasswordProvider; X509Certificate2 certWithPrivateKey = certificateGroup.ApplicationCertificate.LoadPrivateKeyEx(passwordProvider).Result; + + if (regeneratePrivateKey) + { + ushort keySize = (ushort)(certWithPrivateKey.GetRSAPublicKey()?.KeySize ?? 0); + + certWithPrivateKey = CertificateFactory.CreateCertificate( + m_configuration.ApplicationUri, + null, + certificateGroup.ApplicationCertificate.SubjectName, + null) + .SetNotBefore(DateTime.Today.AddDays(-1)) + .SetNotAfter(DateTime.Today.AddDays(14)) + .SetRSAKeySize(keySize) + .CreateForRSA(); + + certificateGroup.TemporaryApplicationCertificate = certWithPrivateKey; + } + Utils.LogCertificate(Utils.TraceMasks.Security, "Create signing request: ", certWithPrivateKey); certificateRequest = CertificateFactory.CreateSigningRequest(certWithPrivateKey, X509Utils.GetDomainsFromCertificate(certWithPrivateKey)); return ServiceResult.Good; @@ -622,7 +654,8 @@ private ServiceResult GetRejectedList( } ICertificateStore store = m_rejectedStore.OpenStore(); - try { + try + { X509Certificate2Collection collection = store.Enumerate().Result; List rawList = new List(); foreach (var cert in collection) @@ -819,6 +852,7 @@ private class ServerCertificateGroup public CertificateStoreIdentifier IssuerStore; public CertificateStoreIdentifier TrustedStore; public UpdateCertificateData UpdateCertificate; + public X509Certificate2 TemporaryApplicationCertificate; } private ServerConfigurationState m_serverConfigurationNode; diff --git a/Tests/Opc.Ua.Gds.Tests/PushTest.cs b/Tests/Opc.Ua.Gds.Tests/PushTest.cs index 5ba6257974..6b9cfb2362 100644 --- a/Tests/Opc.Ua.Gds.Tests/PushTest.cs +++ b/Tests/Opc.Ua.Gds.Tests/PushTest.cs @@ -379,9 +379,20 @@ public void UpdateCertificateSelfSignedNoPrivateKey() } } + [Test, Order(509)] + public void UpdateCertificateCASignedRegeneratePrivateKey() + { + UpdateCertificateCASigned(true); + } + [Test, Order(510)] public void UpdateCertificateCASigned() { + UpdateCertificateCASigned(false); + } + + public void UpdateCertificateCASigned(bool regeneratePrivateKey = false) + { #if NETCOREAPP3_1_OR_GREATER // this test fails on macOS, ignore if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) @@ -396,7 +407,7 @@ public void UpdateCertificateCASigned() null, m_pushClient.PushClient.ApplicationCertificateType, null, - false, + regeneratePrivateKey, null); Assert.IsNotNull(csr); TestContext.Out.WriteLine("Start Signing Request"); @@ -613,7 +624,7 @@ public void GetCertificates() }, Throws.Exception); m_pushClient.PushClient.GetCertificates(m_pushClient.PushClient.DefaultApplicationGroup, out NodeId[] certificateTypeIds, out byte[][] certificates); - + Assert.That(certificateTypeIds.Length == 1); Assert.NotNull(certificates[0]); using (var x509 = new X509Certificate2(certificates[0])) From 7c63d8a18f03ad538eeb9c23378a0d5e05bd837e Mon Sep 17 00:00:00 2001 From: Martin Regen Date: Wed, 9 Oct 2024 10:12:28 +0200 Subject: [PATCH 61/83] Client ReadNodes, throw BadInvalidType if a value type returned by an attribute is null (#2746) Handle a case where a misconfigured server returns a null value for an attribute when a value type is expected. To avoid the NullReferenceException that stops the whole ReadNodes operation, flag the Node with the service result and or return a default value if required. --- Libraries/Opc.Ua.Client/Session/Session.cs | 43 +++++++++--------- Stack/Opc.Ua.Core/Types/BuiltIn/DataValue.cs | 44 +++++++++++++++++++ .../Certificates/CertificateValidatorTest.cs | 2 +- 3 files changed, 65 insertions(+), 24 deletions(-) diff --git a/Libraries/Opc.Ua.Client/Session/Session.cs b/Libraries/Opc.Ua.Client/Session/Session.cs index f9b8149b2d..14f479ea7c 100644 --- a/Libraries/Opc.Ua.Client/Session/Session.cs +++ b/Libraries/Opc.Ua.Client/Session/Session.cs @@ -1692,9 +1692,6 @@ public void FetchOperationLimits() } } - - - /// public void FetchTypeTree(ExpandedNodeId typeId) { @@ -4404,12 +4401,12 @@ private Node ProcessReadResponse( value = attributes[Attributes.EventNotifier]; - if (value == null || value.Value is null) + if (value == null) { throw ServiceResultException.Create(StatusCodes.BadUnexpectedError, "Object does not support the EventNotifier attribute."); } - objectNode.EventNotifier = (byte)value.GetValue(typeof(byte)); + objectNode.EventNotifier = value.GetValueOrDefault(); node = objectNode; break; } @@ -4425,7 +4422,7 @@ private Node ProcessReadResponse( throw ServiceResultException.Create(StatusCodes.BadUnexpectedError, "ObjectType does not support the IsAbstract attribute."); } - objectTypeNode.IsAbstract = (bool)value.GetValue(typeof(bool)); + objectTypeNode.IsAbstract = value.GetValueOrDefault(); node = objectTypeNode; break; } @@ -4452,7 +4449,7 @@ private Node ProcessReadResponse( throw ServiceResultException.Create(StatusCodes.BadUnexpectedError, "Variable does not support the ValueRank attribute."); } - variableNode.ValueRank = (int)value.GetValue(typeof(int)); + variableNode.ValueRank = value.GetValueOrDefault(); // ArrayDimensions Attribute value = attributes[Attributes.ArrayDimensions]; @@ -4477,7 +4474,7 @@ private Node ProcessReadResponse( throw ServiceResultException.Create(StatusCodes.BadUnexpectedError, "Variable does not support the AccessLevel attribute."); } - variableNode.AccessLevel = (byte)value.GetValue(typeof(byte)); + variableNode.AccessLevel = value.GetValueOrDefault(); // UserAccessLevel Attribute value = attributes[Attributes.UserAccessLevel]; @@ -4487,7 +4484,7 @@ private Node ProcessReadResponse( throw ServiceResultException.Create(StatusCodes.BadUnexpectedError, "Variable does not support the UserAccessLevel attribute."); } - variableNode.UserAccessLevel = (byte)value.GetValue(typeof(byte)); + variableNode.UserAccessLevel = value.GetValueOrDefault(); // Historizing Attribute value = attributes[Attributes.Historizing]; @@ -4497,7 +4494,7 @@ private Node ProcessReadResponse( throw ServiceResultException.Create(StatusCodes.BadUnexpectedError, "Variable does not support the Historizing attribute."); } - variableNode.Historizing = (bool)value.GetValue(typeof(bool)); + variableNode.Historizing = value.GetValueOrDefault(); // MinimumSamplingInterval Attribute value = attributes[Attributes.MinimumSamplingInterval]; @@ -4512,7 +4509,7 @@ private Node ProcessReadResponse( if (value != null) { - variableNode.AccessLevelEx = (uint)value.GetValue(typeof(uint)); + variableNode.AccessLevelEx = value.GetValueOrDefault(); } node = variableNode; @@ -4531,7 +4528,7 @@ private Node ProcessReadResponse( throw ServiceResultException.Create(StatusCodes.BadUnexpectedError, "VariableType does not support the IsAbstract attribute."); } - variableTypeNode.IsAbstract = (bool)value.GetValue(typeof(bool)); + variableTypeNode.IsAbstract = value.GetValueOrDefault(); // DataType Attribute value = attributes[Attributes.DataType]; @@ -4551,7 +4548,7 @@ private Node ProcessReadResponse( throw ServiceResultException.Create(StatusCodes.BadUnexpectedError, "VariableType does not support the ValueRank attribute."); } - variableTypeNode.ValueRank = (int)value.GetValue(typeof(int)); + variableTypeNode.ValueRank = value.GetValueOrDefault(); // ArrayDimensions Attribute value = attributes[Attributes.ArrayDimensions]; @@ -4577,7 +4574,7 @@ private Node ProcessReadResponse( throw ServiceResultException.Create(StatusCodes.BadUnexpectedError, "Method does not support the Executable attribute."); } - methodNode.Executable = (bool)value.GetValue(typeof(bool)); + methodNode.Executable = value.GetValueOrDefault(); // UserExecutable Attribute value = attributes[Attributes.UserExecutable]; @@ -4587,7 +4584,7 @@ private Node ProcessReadResponse( throw ServiceResultException.Create(StatusCodes.BadUnexpectedError, "Method does not support the UserExecutable attribute."); } - methodNode.UserExecutable = (bool)value.GetValue(typeof(bool)); + methodNode.UserExecutable = value.GetValueOrDefault(); node = methodNode; break; @@ -4605,7 +4602,7 @@ private Node ProcessReadResponse( throw ServiceResultException.Create(StatusCodes.BadUnexpectedError, "DataType does not support the IsAbstract attribute."); } - dataTypeNode.IsAbstract = (bool)value.GetValue(typeof(bool)); + dataTypeNode.IsAbstract = value.GetValueOrDefault(); // DataTypeDefinition Attribute value = attributes[Attributes.DataTypeDefinition]; @@ -4631,7 +4628,7 @@ private Node ProcessReadResponse( throw ServiceResultException.Create(StatusCodes.BadUnexpectedError, "ReferenceType does not support the IsAbstract attribute."); } - referenceTypeNode.IsAbstract = (bool)value.GetValue(typeof(bool)); + referenceTypeNode.IsAbstract = value.GetValueOrDefault(); // Symmetric Attribute value = attributes[Attributes.Symmetric]; @@ -4641,7 +4638,7 @@ private Node ProcessReadResponse( throw ServiceResultException.Create(StatusCodes.BadUnexpectedError, "ReferenceType does not support the Symmetric attribute."); } - referenceTypeNode.Symmetric = (bool)value.GetValue(typeof(bool)); + referenceTypeNode.Symmetric = value.GetValueOrDefault(); // InverseName Attribute value = attributes[Attributes.InverseName]; @@ -4667,7 +4664,7 @@ private Node ProcessReadResponse( throw ServiceResultException.Create(StatusCodes.BadUnexpectedError, "View does not support the EventNotifier attribute."); } - viewNode.EventNotifier = (byte)value.GetValue(typeof(byte)); + viewNode.EventNotifier = value.GetValueOrDefault(); // ContainsNoLoops Attribute value = attributes[Attributes.ContainsNoLoops]; @@ -4677,7 +4674,7 @@ private Node ProcessReadResponse( throw ServiceResultException.Create(StatusCodes.BadUnexpectedError, "View does not support the ContainsNoLoops attribute."); } - viewNode.ContainsNoLoops = (bool)value.GetValue(typeof(bool)); + viewNode.ContainsNoLoops = value.GetValueOrDefault(); node = viewNode; break; @@ -4728,14 +4725,14 @@ private Node ProcessReadResponse( if (attributes.TryGetValue(Attributes.WriteMask, out value) && value != null) { - node.WriteMask = (uint)value.GetValue(typeof(uint)); + node.WriteMask = value.GetValueOrDefault(); } // UserWriteMask Attribute if (attributes.TryGetValue(Attributes.UserWriteMask, out value) && value != null) { - node.UserWriteMask = (uint)value.GetValue(typeof(uint)); + node.UserWriteMask = value.GetValueOrDefault(); } // RolePermissions Attribute @@ -4776,7 +4773,7 @@ private Node ProcessReadResponse( if (attributes.TryGetValue(Attributes.AccessRestrictions, out value) && value != null) { - node.AccessRestrictions = (ushort)value.GetValue(typeof(ushort)); + node.AccessRestrictions = value.GetValueOrDefault(); } return node; diff --git a/Stack/Opc.Ua.Core/Types/BuiltIn/DataValue.cs b/Stack/Opc.Ua.Core/Types/BuiltIn/DataValue.cs index ceca1c7b6b..a0a0e594ac 100644 --- a/Stack/Opc.Ua.Core/Types/BuiltIn/DataValue.cs +++ b/Stack/Opc.Ua.Core/Types/BuiltIn/DataValue.cs @@ -589,6 +589,50 @@ public object GetValue(Type expectedType) return value; } + /// + /// Gets the value from the data value. + /// Returns default value for bad status. + /// + /// The type of object. + /// The value. + /// + /// Checks the StatusCode and returns default value for bad status. + /// Extracts the body from an ExtensionObject value if it has the correct type. + /// Throws exception only if there is a type mismatch; + /// + public T GetValueOrDefault() + { + // return default for a DataValue with bad status code. + if (StatusCode.IsBad(this.StatusCode)) + { + return default; + } + + object value = this.Value; + if (value != null) + { + if (value is ExtensionObject extension) + { + value = extension.Body; + } + + if (!typeof(T).IsInstanceOfType(value)) + { + throw ServiceResultException.Create(StatusCodes.BadTypeMismatch, "DataValue is not of type {0}.", typeof(T).Name); + } + + return (T)value; + } + + // a null value for a value type should throw + if (typeof(T).IsValueType) + { + throw ServiceResultException.Create(StatusCodes.BadTypeMismatch, "DataValue is null and not of value type {0}.", typeof(T).Name); + } + + return default; + } + /// /// Gets the value from the data value. /// diff --git a/Tests/Opc.Ua.Core.Tests/Security/Certificates/CertificateValidatorTest.cs b/Tests/Opc.Ua.Core.Tests/Security/Certificates/CertificateValidatorTest.cs index c871d6620c..71466fbced 100644 --- a/Tests/Opc.Ua.Core.Tests/Security/Certificates/CertificateValidatorTest.cs +++ b/Tests/Opc.Ua.Core.Tests/Security/Certificates/CertificateValidatorTest.cs @@ -383,7 +383,7 @@ public async Task VerifyRejectedCertsDoNotOverflowStore() certValidator.MaxRejectedCertificates = 3; await Task.Delay(1000).ConfigureAwait(false); certificates = await validator.RejectedStore.Enumerate().ConfigureAwait(false); - Assert.LessOrEqual(3, certificates.Count); + Assert.GreaterOrEqual(3, certificates.Count); // test setter if allcerts are deleted certValidator.MaxRejectedCertificates = -1; From a29d8717113cdfd111c84d4e699b9a57901886c0 Mon Sep 17 00:00:00 2001 From: ThomasNehring Date: Wed, 9 Oct 2024 11:30:49 +0200 Subject: [PATCH 62/83] [Client] Read large dictionaries in chunks (#2782) A new method is implemented which reads byte strings in chunks and therefore allows to read byte strings which exceed the encoding limits of the server. This is used in the method which reads v103 data dictionaries. --- Libraries/Opc.Ua.Client/DataDictionary.cs | 57 +++- Libraries/Opc.Ua.Client/Session/ISession.cs | 7 + Libraries/Opc.Ua.Client/Session/Session.cs | 101 +++++++ .../Opc.Ua.Client/Session/SessionAsync.cs | 8 + .../Opc.Ua.Client/Session/TraceableSession.cs | 13 +- .../ClientTestServerQuotas.cs | 281 ++++++++++++++++++ 6 files changed, 450 insertions(+), 17 deletions(-) create mode 100644 Tests/Opc.Ua.Client.Tests/ClientTestServerQuotas.cs diff --git a/Libraries/Opc.Ua.Client/DataDictionary.cs b/Libraries/Opc.Ua.Client/DataDictionary.cs index 41f73aa884..fb6f29fc78 100644 --- a/Libraries/Opc.Ua.Client/DataDictionary.cs +++ b/Libraries/Opc.Ua.Client/DataDictionary.cs @@ -31,6 +31,7 @@ using System.Collections.Generic; using System.IO; using System.Linq; +using System.Runtime.ExceptionServices; using System.Runtime.Serialization; using System.Threading; using System.Threading.Tasks; @@ -313,27 +314,53 @@ public byte[] ReadDictionary(NodeId dictionaryId) // read value. DataValueCollection values; DiagnosticInfoCollection diagnosticInfos; + try + { + ResponseHeader responseHeader = m_session.Read( + null, + 0, + TimestampsToReturn.Neither, + itemsToRead, + out values, + out diagnosticInfos); - ResponseHeader responseHeader = m_session.Read( - null, - 0, - TimestampsToReturn.Neither, - itemsToRead, - out values, - out diagnosticInfos); + ClientBase.ValidateResponse(values, itemsToRead); + ClientBase.ValidateDiagnosticInfos(diagnosticInfos, itemsToRead); - ClientBase.ValidateResponse(values, itemsToRead); - ClientBase.ValidateDiagnosticInfos(diagnosticInfos, itemsToRead); + // check for error. + if (StatusCode.IsBad(values[0].StatusCode)) + { + ServiceResult result = ClientBase.GetResult(values[0].StatusCode, 0, diagnosticInfos, responseHeader); + throw new ServiceResultException(result); + } - // check for error. - if (StatusCode.IsBad(values[0].StatusCode)) + // return as a byte array. + return values[0].Value as byte[]; + + } + catch (ServiceResultException ex) { - ServiceResult result = ClientBase.GetResult(values[0].StatusCode, 0, diagnosticInfos, responseHeader); - throw new ServiceResultException(result); + if (ex.StatusCode != StatusCodes.BadEncodingLimitsExceeded) + { + throw; + } + else + { + try + { + byte[] dictionary = m_session.ReadByteStringInChunks(dictionaryId); + return dictionary; + } + catch + { + ExceptionDispatchInfo.Capture(ex).Throw(); + throw; + } + + } } - // return as a byte array. - return values[0].Value as byte[]; + } /// diff --git a/Libraries/Opc.Ua.Client/Session/ISession.cs b/Libraries/Opc.Ua.Client/Session/ISession.cs index bfa90e1fc7..7a4a5d5785 100644 --- a/Libraries/Opc.Ua.Client/Session/ISession.cs +++ b/Libraries/Opc.Ua.Client/Session/ISession.cs @@ -543,6 +543,13 @@ Task> LoadDataTypeSystem( /// The errors reported by the server. void ReadValues(IList nodeIds, out DataValueCollection values, out IList errors); + /// + /// Reads a byte string which is too large for the (server side) encoder to handle. + /// + /// The node id of a byte string variable + /// + byte[] ReadByteStringInChunks(NodeId nodeId); + /// /// Fetches all references for the specified node. /// diff --git a/Libraries/Opc.Ua.Client/Session/Session.cs b/Libraries/Opc.Ua.Client/Session/Session.cs index 14f479ea7c..3de3e6573b 100644 --- a/Libraries/Opc.Ua.Client/Session/Session.cs +++ b/Libraries/Opc.Ua.Client/Session/Session.cs @@ -914,6 +914,16 @@ public uint ServerMaxContinuationPointsPerBrowse set => m_serverMaxContinuationPointsPerBrowse = value; } + /// + /// Read from the Server capability MaxByteStringLength when the Operation Limits are fetched + /// + public uint ServerMaxByteStringLength + { + get => m_serverMaxByteStringLength; + set => m_serverMaxByteStringLength = value; + } + + /// public ContinuationPointPolicy ContinuationPointPolicy { @@ -1651,6 +1661,10 @@ public void FetchOperationLimits() nodeIds.Add(VariableIds.Server_ServerCapabilities_MaxBrowseContinuationPoints); int maxBrowseContinuationPointIndex = nodeIds.Count - 1; + // add the server transport quota MaxByteStringLength. + nodeIds.Add(VariableIds.Server_ServerCapabilities_MaxByteStringLength); + int maxByteStringLengthIndex = nodeIds.Count - 1; + ReadValues(nodeIds, Enumerable.Repeat(typeof(uint), nodeIds.Count).ToList(), out var values, out var errors); var configOperationLimits = m_configuration?.ClientConfiguration?.OperationLimits ?? new OperationLimits(); @@ -1679,6 +1693,11 @@ public void FetchOperationLimits() { ServerMaxContinuationPointsPerBrowse = (UInt16)values[maxBrowseContinuationPointIndex]; } + if (values[maxByteStringLengthIndex] != null + && ServiceResult.IsNotBad(errors[maxByteStringLengthIndex])) + { + ServerMaxByteStringLength = (UInt32)values[maxByteStringLengthIndex]; + } } catch (Exception ex) @@ -2889,6 +2908,87 @@ public void ReadValues( } } + /// + public byte[] ReadByteStringInChunks(NodeId nodeId) + { + + int count = (int)ServerMaxByteStringLength; ; + + int my_MaxByteStringLength = m_configuration.TransportQuotas.MaxByteStringLength; + if (my_MaxByteStringLength > 0) + { + count = ServerMaxByteStringLength > my_MaxByteStringLength ? + my_MaxByteStringLength : (int)ServerMaxByteStringLength; + } + + if (count <= 1) + { + throw new ServiceResultException(StatusCodes.BadIndexRangeNoData, "The MaxByteStringLength is not known or too small for reading data in chunks."); + } + + int offset = 0; + List bytes = new List(); + + while (true) + { + ReadValueId valueToRead = new ReadValueId { + NodeId = nodeId, + AttributeId = Attributes.Value, + IndexRange = new NumericRange(offset, offset + count - 1).ToString(), + DataEncoding = null + }; + ReadValueIdCollection readValueIds = new ReadValueIdCollection { valueToRead }; + + ResponseHeader responseHeader = Read( + null, + 0, + TimestampsToReturn.Neither, + readValueIds, + out DataValueCollection results, + out DiagnosticInfoCollection diagnosticInfos); + + ClientBase.ValidateResponse(results, readValueIds); + ClientBase.ValidateDiagnosticInfos(diagnosticInfos, readValueIds); + + if (offset == 0) + { + Variant wrappedValue = results[0].WrappedValue; + if (wrappedValue.TypeInfo.BuiltInType != BuiltInType.ByteString || + wrappedValue.TypeInfo.ValueRank != ValueRanks.Scalar) + { + throw new ServiceResultException(StatusCodes.BadTypeMismatch, "Value is not a ByteString scalar."); + } + } + + if (StatusCode.IsBad(results[0].StatusCode)) + { + if (results[0].StatusCode == StatusCodes.BadIndexRangeNoData) + { + // this happens when the previous read has fetched all remaining data + break; + } + ServiceResult serviceResult = ClientBase.GetResult(results[0].StatusCode, 0, diagnosticInfos, responseHeader); + throw new ServiceResultException(serviceResult); + } + + byte[] chunk = results[0].Value as byte[]; + if (chunk == null || chunk.Length == 0) + { + break; + } + + bytes.Add(chunk); + + if (chunk.Length < count) + { + break; + } + offset += count; + } + + return bytes.SelectMany(a => a).ToArray(); + } + /// public void ReadDisplayName( IList nodeIds, @@ -6452,6 +6552,7 @@ private static void UpdateLatestSequenceNumberToSend(ref uint latestSequenceNumb private readonly EndpointDescriptionCollection m_discoveryServerEndpoints; private readonly StringCollection m_discoveryProfileUris; private uint m_serverMaxContinuationPointsPerBrowse = 0; + private uint m_serverMaxByteStringLength = 0; private ContinuationPointPolicy m_continuationPointPolicy = ContinuationPointPolicy.Default; diff --git a/Libraries/Opc.Ua.Client/Session/SessionAsync.cs b/Libraries/Opc.Ua.Client/Session/SessionAsync.cs index 0b5e74e060..2c38ffcfba 100644 --- a/Libraries/Opc.Ua.Client/Session/SessionAsync.cs +++ b/Libraries/Opc.Ua.Client/Session/SessionAsync.cs @@ -644,6 +644,9 @@ public async Task FetchOperationLimitsAsync(CancellationToken ct) nodeIds.Add(VariableIds.Server_ServerCapabilities_MaxBrowseContinuationPoints); int maxBrowseContinuationPointIndex = nodeIds.Count - 1; + nodeIds.Add(VariableIds.Server_ServerCapabilities_MaxByteStringLength); + int maxByteStringLengthIndex = nodeIds.Count - 1; + (DataValueCollection values, IList errors) = await ReadValuesAsync(nodeIds, ct).ConfigureAwait(false); OperationLimits configOperationLimits = m_configuration?.ClientConfiguration?.OperationLimits ?? new OperationLimits(); @@ -674,6 +677,11 @@ public async Task FetchOperationLimitsAsync(CancellationToken ct) { ServerMaxContinuationPointsPerBrowse = (UInt16)values[maxBrowseContinuationPointIndex].Value; } + if (values[maxByteStringLengthIndex] != null + && ServiceResult.IsNotBad(errors[maxByteStringLengthIndex])) + { + ServerMaxByteStringLength = (UInt32)values[maxByteStringLengthIndex].Value; + } } catch (Exception ex) { diff --git a/Libraries/Opc.Ua.Client/Session/TraceableSession.cs b/Libraries/Opc.Ua.Client/Session/TraceableSession.cs index 7b9a7b35b1..6bd4d2bcef 100644 --- a/Libraries/Opc.Ua.Client/Session/TraceableSession.cs +++ b/Libraries/Opc.Ua.Client/Session/TraceableSession.cs @@ -661,9 +661,18 @@ public void ReadValues(IList variableIds, IList expectedTypes, out m_session.ReadValues(variableIds, expectedTypes, out values, out errors); } } - + /// - public void ReadDisplayName(IList nodeIds, out IList displayNames, out IList errors) + public byte[] ReadByteStringInChunks(NodeId nodeId) + { + using (Activity activity = ActivitySource.StartActivity()) + { + return m_session.ReadByteStringInChunks(nodeId); + } + } + + /// + public void ReadDisplayName(IList nodeIds, out IList displayNames, out IList errors) { using (Activity activity = ActivitySource.StartActivity()) { diff --git a/Tests/Opc.Ua.Client.Tests/ClientTestServerQuotas.cs b/Tests/Opc.Ua.Client.Tests/ClientTestServerQuotas.cs new file mode 100644 index 0000000000..1aada494ee --- /dev/null +++ b/Tests/Opc.Ua.Client.Tests/ClientTestServerQuotas.cs @@ -0,0 +1,281 @@ +/* ======================================================================== + * Copyright (c) 2005-2020 The OPC Foundation, Inc. All rights reserved. + * + * OPC Foundation MIT License 1.00 + * + * Permission is hereby granted, free of charge, to any person + * obtaining a copy of this software and associated documentation + * files (the "Software"), to deal in the Software without + * restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following + * conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES + * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT + * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, + * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR + * OTHER DEALINGS IN THE SOFTWARE. + * + * The complete license agreement can be found here: + * http://opcfoundation.org/License/MIT/1.00/ + * ======================================================================*/ + +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.IO; +using System.Linq; +using System.Runtime.CompilerServices; +using System.Security.Cryptography.X509Certificates; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using BenchmarkDotNet.Attributes; +using Moq; +using NUnit.Framework; +using Opc.Ua.Bindings; +using Opc.Ua.Configuration; +using Opc.Ua.Server.Tests; +using Quickstarts.ReferenceServer; +using Assert = NUnit.Framework.Legacy.ClassicAssert; + +namespace Opc.Ua.Client.Tests +{ + public class ClientTestServerQuotas : ClientTestFramework + { + const int MaxByteStringLengthForTest = 4096; + public ClientTestServerQuotas() : base(Utils.UriSchemeOpcTcp) + { + } + + public ClientTestServerQuotas(string uriScheme = Utils.UriSchemeOpcTcp) : + base(uriScheme) + { + } + + #region Test Setup + /// + /// Set up a Server and a Client instance. + /// + [OneTimeSetUp] + public new Task OneTimeSetUp() + { + SupportsExternalServerUrl = true; + return base.OneTimeSetUpAsync(); + } + + /// + /// Tear down the Server and the Client. + /// + [OneTimeTearDown] + public new Task OneTimeTearDownAsync() + { + return base.OneTimeTearDownAsync(); + } + + /// + /// Test setup. + /// + [SetUp] + public new Task SetUp() + { + return base.SetUp(); + } + + public override async Task CreateReferenceServerFixture(bool enableTracing, bool disableActivityLogging, bool securityNone, TextWriter writer) + { + { + // start Ref server + ServerFixture = new ServerFixture(enableTracing, disableActivityLogging) { + UriScheme = UriScheme, + SecurityNone = securityNone, + AutoAccept = true, + AllNodeManagers = true, + OperationLimits = true + }; + } + + if (writer != null) + { + ServerFixture.TraceMasks = Utils.TraceMasks.Error | Utils.TraceMasks.Security; + } + + await ServerFixture.LoadConfiguration(PkiRoot).ConfigureAwait(false); + ServerFixture.Config.TransportQuotas.MaxMessageSize = TransportQuotaMaxMessageSize; + ServerFixture.Config.TransportQuotas.MaxByteStringLength = MaxByteStringLengthForTest; + ServerFixture.Config.TransportQuotas.MaxStringLength = TransportQuotaMaxStringLength; + ServerFixture.Config.ServerConfiguration.UserTokenPolicies.Add(new UserTokenPolicy(UserTokenType.UserName)); + ServerFixture.Config.ServerConfiguration.UserTokenPolicies.Add(new UserTokenPolicy(UserTokenType.Certificate)); + ServerFixture.Config.ServerConfiguration.UserTokenPolicies.Add( + new UserTokenPolicy(UserTokenType.IssuedToken) { IssuedTokenType = Opc.Ua.Profiles.JwtUserToken }); + + ReferenceServer = await ServerFixture.StartAsync(writer ?? TestContext.Out).ConfigureAwait(false); + ReferenceServer.TokenValidator = this.TokenValidator; + ServerFixturePort = ServerFixture.Port; + } + + /// + /// Test teardown. + /// + [TearDown] + public new Task TearDown() + { + return base.TearDown(); + } + #endregion + + #region Benchmark Setup + /// + /// Global Setup for benchmarks. + /// + [GlobalSetup] + public new void GlobalSetup() + { + base.GlobalSetup(); + } + + /// + /// Global cleanup for benchmarks. + /// + [GlobalCleanup] + public new void GlobalCleanup() + { + base.GlobalCleanup(); + } + #endregion + + #region Test Methods + + [Test, Order(100)] + public void ReadDictionaryByteString() + { + List dictionaryIds = new List(); + dictionaryIds.Add(VariableIds.OpcUa_BinarySchema); + dictionaryIds.Add(GetTestDataDictionaryNodeId()); + + Session theSession = ((Session)(((TraceableSession)Session).Session)); + + foreach (NodeId dataDictionaryId in dictionaryIds) + { + ReferenceDescription referenceDescription = new ReferenceDescription(); + + referenceDescription.NodeId = NodeId.ToExpandedNodeId(dataDictionaryId, theSession.NodeCache.NamespaceUris); + + // make sure the dictionary is too large to fit in a single message + ReadValueId readValueId = new ReadValueId { + NodeId = dataDictionaryId, + AttributeId = Attributes.Value, + IndexRange = null, + DataEncoding = null + }; + + ReadValueIdCollection nodesToRead = new ReadValueIdCollection { + readValueId + }; + + var x = Assert.Throws(() => { + theSession.Read(null, 0, TimestampsToReturn.Neither, nodesToRead, out DataValueCollection results, out DiagnosticInfoCollection diagnosticInfos); + }); + + Assert.AreEqual(StatusCodes.BadEncodingLimitsExceeded, x.StatusCode); + + // now ensure we get the dictionary in chunks + DataDictionary dictionary = theSession.LoadDataDictionary(referenceDescription); + Assert.IsNotNull(dictionary); + + // Sanity checks: verify that some well-known information is present + Assert.AreEqual(dictionary.TypeSystemName, "OPC Binary"); + + if (dataDictionaryId == dictionaryIds[0]) + { + Assert.IsTrue(dictionary.DataTypes.Count > 160); + Assert.IsTrue(dictionary.DataTypes.ContainsKey(VariableIds.OpcUa_BinarySchema_Union)); + Assert.IsTrue(dictionary.DataTypes.ContainsKey(VariableIds.OpcUa_BinarySchema_OptionSet)); + Assert.AreEqual("http://opcfoundation.org/UA/", dictionary.TypeDictionary.TargetNamespace); + } + else if (dataDictionaryId == dictionaryIds[1]) + { + Assert.IsTrue(dictionary.DataTypes.Count >= 10); + Assert.AreEqual("http://test.org/UA/Data/", dictionary.TypeDictionary.TargetNamespace); + } + } + } + + + [Test, Order(200)] + public void TestBoundaryCaseForReadingChunks() + { + + Session theSession = ((Session)(((TraceableSession)Session).Session)); + + int NamespaceIndex = theSession.NamespaceUris.GetIndex("http://opcfoundation.org/Quickstarts/ReferenceServer"); + NodeId NodeId = new NodeId($"ns={NamespaceIndex};s=Scalar_Static_ByteString"); + + Random random = new Random(); + + byte[] chunk = new byte[MaxByteStringLengthForTest]; + random.NextBytes(chunk); + + WriteValue WriteValue = new WriteValue { + NodeId = NodeId, + AttributeId = Attributes.Value, + Value = new DataValue() { WrappedValue = new Variant(chunk) }, + IndexRange = null + }; + WriteValueCollection writeValues = new WriteValueCollection { + WriteValue + }; + theSession.Write(null, writeValues, out StatusCodeCollection results, out DiagnosticInfoCollection diagnosticInfos); + + if (results[0] != StatusCodes.Good) + { + Assert.Fail($"Write failed with status code {results[0]}"); + } + + byte[] readData = theSession.ReadByteStringInChunks(NodeId); + + Assert.IsTrue(Utils.IsEqual(chunk, readData)); + } + #endregion // Test Methods + #region // helper methods + + /// + /// retrieve the node id of the test data dictionary without relying on + /// hard coded identifiers + /// + /// + public NodeId GetTestDataDictionaryNodeId() + { + BrowseDescription browseDescription = new BrowseDescription() { + NodeId = ObjectIds.OPCBinarySchema_TypeSystem, + BrowseDirection = BrowseDirection.Forward, + ReferenceTypeId = ReferenceTypeIds.HasComponent, + IncludeSubtypes = true, + NodeClassMask = (uint)NodeClass.Variable, + ResultMask = (uint) BrowseResultMask.All + }; + BrowseDescriptionCollection browseDescriptions = new BrowseDescriptionCollection() { browseDescription }; + + Session.Browse(null, null, 0, browseDescriptions, out BrowseResultCollection results, out DiagnosticInfoCollection diagnosticInfos); + + if (results[0] == null || results[0].StatusCode != StatusCodes.Good) + { + throw new Exception("cannot read the id of the test dictionary"); + } + ReferenceDescription referenceDescription = results[0].References.FirstOrDefault(a => a.BrowseName.Name == "TestData"); + NodeId result = ExpandedNodeId.ToNodeId(referenceDescription.NodeId,Session.NamespaceUris); + return result; + + + } + #endregion // helper methods + + } +} From 23b85027785f404ae478abbb35255b7df51e6bb3 Mon Sep 17 00:00:00 2001 From: Martin Regen Date: Thu, 10 Oct 2024 07:01:48 +0200 Subject: [PATCH 63/83] Server doesn't start up with mixed https endpoints (#2789) * allow startup of mixed https endpoints --- .../Quickstarts.ReferenceServer.Config.xml | 3 ++- Stack/Opc.Ua.Bindings.Https/Stack/Https/HttpsServiceHost.cs | 5 +++++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/Applications/ConsoleReferenceServer/Quickstarts.ReferenceServer.Config.xml b/Applications/ConsoleReferenceServer/Quickstarts.ReferenceServer.Config.xml index 59d17b5901..6255b65dba 100644 --- a/Applications/ConsoleReferenceServer/Quickstarts.ReferenceServer.Config.xml +++ b/Applications/ConsoleReferenceServer/Quickstarts.ReferenceServer.Config.xml @@ -14,7 +14,7 @@ Directory - %LocalApplicationData%/OPC Foundation/pki/own + %LocalApplicationData%/OPC Foundation/pki/own CN=Quickstart Reference Server, C=US, S=Arizona, O=OPC Foundation, DC=localhost @@ -77,6 +77,7 @@ opc.https://localhost:62540/Quickstarts/ReferenceServer opc.tcp://localhost:62541/Quickstarts/ReferenceServer + - + - + + @@ -60,8 +61,7 @@ - - + @@ -71,7 +71,7 @@ - + diff --git a/Stack/Opc.Ua.Core/Security/Certificates/CertificateFactory.cs b/Stack/Opc.Ua.Core/Security/Certificates/CertificateFactory.cs index 2f3acaf631..9ae5457077 100644 --- a/Stack/Opc.Ua.Core/Security/Certificates/CertificateFactory.cs +++ b/Stack/Opc.Ua.Core/Security/Certificates/CertificateFactory.cs @@ -58,10 +58,11 @@ public static class CertificateFactory public static X509Certificate2 Create(ReadOnlyMemory encodedData, bool useCache) { #if NET6_0_OR_GREATER - var certificate = new X509Certificate2(encodedData.Span); + var certificate = X509CertificateLoader.LoadCertificate(encodedData.Span); #else - var certificate = new X509Certificate2(encodedData.ToArray()); + var certificate = X509CertificateLoader.LoadCertificate(encodedData.ToArray()); #endif + if (useCache) { return Load(certificate, false); @@ -368,7 +369,7 @@ public static X509Certificate2 CreateCertificateWithPEMPrivateKey( string password = null) { RSA rsaPrivateKey = PEMReader.ImportPrivateKeyFromPEM(pemDataBlob, password); - return new X509Certificate2(certificate.RawData).CopyWithPrivateKey(rsaPrivateKey); + return X509CertificateLoader.LoadCertificate(certificate.RawData).CopyWithPrivateKey(rsaPrivateKey); } #else /// @@ -453,18 +454,18 @@ public static X509Certificate2 CreateCertificateWithPEMPrivateKey( /// The certificate with a private key. [Obsolete("Use the new CreateCertificate methods with CertificateBuilder.")] internal static X509Certificate2 CreateCertificate( - string applicationUri, - string applicationName, - string subjectName, - IList domainNames, - ushort keySize, - DateTime startTime, - ushort lifetimeInMonths, - ushort hashSizeInBits, - bool isCA = false, - X509Certificate2 issuerCAKeyCert = null, - byte[] publicKey = null, - int pathLengthConstraint = 0) + string applicationUri, + string applicationName, + string subjectName, + IList domainNames, + ushort keySize, + DateTime startTime, + ushort lifetimeInMonths, + ushort hashSizeInBits, + bool isCA = false, + X509Certificate2 issuerCAKeyCert = null, + byte[] publicKey = null, + int pathLengthConstraint = 0) { ICertificateBuilder builder = null; if (isCA) diff --git a/Stack/Opc.Ua.Core/Security/Certificates/CertificateValidator.cs b/Stack/Opc.Ua.Core/Security/Certificates/CertificateValidator.cs index 4e30f5d1a6..2c13adf880 100644 --- a/Stack/Opc.Ua.Core/Security/Certificates/CertificateValidator.cs +++ b/Stack/Opc.Ua.Core/Security/Certificates/CertificateValidator.cs @@ -510,7 +510,7 @@ public virtual async Task ValidateAsync(X509Certificate2Collection chain, Config await InternalValidateAsync(chain, endpoint, ct).ConfigureAwait(false); // add to list of validated certificates. - m_validatedCertificates[certificate.Thumbprint] = new X509Certificate2(certificate.RawData); + m_validatedCertificates[certificate.Thumbprint] = X509CertificateLoader.LoadCertificate(certificate.RawData); return; } @@ -529,7 +529,7 @@ public virtual async Task ValidateAsync(X509Certificate2Collection chain, Config try { Utils.LogCertificate(LogLevel.Warning, "Validation errors suppressed: ", certificate); - m_validatedCertificates[certificate.Thumbprint] = new X509Certificate2(certificate.RawData); + m_validatedCertificates[certificate.Thumbprint] = X509CertificateLoader.LoadCertificate(certificate.RawData); } finally { @@ -554,7 +554,7 @@ public virtual void Validate(X509Certificate2Collection chain, ConfiguredEndpoin InternalValidateAsync(chain, endpoint).GetAwaiter().GetResult(); // add to list of validated certificates. - m_validatedCertificates[certificate.Thumbprint] = new X509Certificate2(certificate.RawData); + m_validatedCertificates[certificate.Thumbprint] = X509CertificateLoader.LoadCertificate(certificate.RawData); return; } @@ -574,7 +574,7 @@ public virtual void Validate(X509Certificate2Collection chain, ConfiguredEndpoin try { Utils.LogCertificate(LogLevel.Warning, "Validation errors suppressed: ", certificate); - m_validatedCertificates[certificate.Thumbprint] = new X509Certificate2(certificate.RawData); + m_validatedCertificates[certificate.Thumbprint] = X509CertificateLoader.LoadCertificate(certificate.RawData); } finally { diff --git a/Stack/Opc.Ua.Core/Security/Certificates/DirectoryCertificateStore.cs b/Stack/Opc.Ua.Core/Security/Certificates/DirectoryCertificateStore.cs index abea61dde9..3aec185c37 100644 --- a/Stack/Opc.Ua.Core/Security/Certificates/DirectoryCertificateStore.cs +++ b/Stack/Opc.Ua.Core/Security/Certificates/DirectoryCertificateStore.cs @@ -449,7 +449,7 @@ public async Task LoadPrivateKey(string thumbprint, string sub { try { - var certificate = new X509Certificate2(file.FullName); + var certificate = X509CertificateLoader.LoadCertificateFromFile(file.FullName); if (!String.IsNullOrEmpty(thumbprint)) { @@ -512,10 +512,11 @@ public async Task LoadPrivateKey(string thumbprint, string sub { try { - certificate = new X509Certificate2( + certificate = X509CertificateLoader.LoadPkcs12FromFile( privateKeyFilePfx.FullName, password, flag); + if (X509Utils.VerifyRSAKeyPair(certificate, certificate, true)) { Utils.LogInfo(Utils.TraceMasks.Security, "Imported the PFX private key for [{0}].", certificate.Thumbprint); @@ -831,7 +832,7 @@ private IDictionary Load(string thumbprint) try { var entry = new Entry { - Certificate = new X509Certificate2(file.FullName), + Certificate = X509CertificateLoader.LoadCertificateFromFile(file.FullName), CertificateFile = file, PrivateKeyFile = null, CertificateWithPrivateKey = null, diff --git a/Stack/Opc.Ua.Core/Security/Certificates/X509CertificateStore/X509CertificateStore.cs b/Stack/Opc.Ua.Core/Security/Certificates/X509CertificateStore/X509CertificateStore.cs index 9be4daf454..a65fd0c205 100644 --- a/Stack/Opc.Ua.Core/Security/Certificates/X509CertificateStore/X509CertificateStore.cs +++ b/Stack/Opc.Ua.Core/Security/Certificates/X509CertificateStore/X509CertificateStore.cs @@ -153,7 +153,7 @@ public Task Add(X509Certificate2 certificate, string password = null) else if (certificate.HasPrivateKey && m_noPrivateKeys) { // ensure no private key is added to store - using (var publicKey = new X509Certificate2(certificate.RawData)) + using (var publicKey = X509CertificateLoader.LoadCertificate(certificate.RawData)) { store.Add(publicKey); } diff --git a/Stack/Opc.Ua.Core/Security/Certificates/X509Utils.cs b/Stack/Opc.Ua.Core/Security/Certificates/X509Utils.cs index e0698029ff..6188d59d97 100644 --- a/Stack/Opc.Ua.Core/Security/Certificates/X509Utils.cs +++ b/Stack/Opc.Ua.Core/Security/Certificates/X509Utils.cs @@ -511,7 +511,7 @@ public static X509Certificate2 CreateCopyWithPrivateKey(X509Certificate2 certifi // see https://github.com/dotnet/runtime/issues/29144 string passcode = GeneratePasscode(); X509KeyStorageFlags storageFlags = persisted ? X509KeyStorageFlags.PersistKeySet : X509KeyStorageFlags.Exportable; - return new X509Certificate2(certificate.Export(X509ContentType.Pfx, passcode), passcode, storageFlags); + return X509CertificateLoader.LoadPkcs12(certificate.Export(X509ContentType.Pfx, passcode), passcode, storageFlags); } return certificate; } diff --git a/Tests/Opc.Ua.Client.Tests/ClientTest.cs b/Tests/Opc.Ua.Client.Tests/ClientTest.cs index 36463b8816..1e3c060a32 100644 --- a/Tests/Opc.Ua.Client.Tests/ClientTest.cs +++ b/Tests/Opc.Ua.Client.Tests/ClientTest.cs @@ -156,7 +156,7 @@ public async Task GetEndpointsAsync() if (endpoint.ServerCertificate != null) { - using (var cert = new X509Certificate2(endpoint.ServerCertificate)) + using (var cert = X509CertificateLoader.LoadCertificate(endpoint.ServerCertificate)) { TestContext.Out.WriteLine(" [{0}]", cert.Thumbprint); } diff --git a/Tests/Opc.Ua.Configuration.Tests/ApplicationInstanceTests.cs b/Tests/Opc.Ua.Configuration.Tests/ApplicationInstanceTests.cs index 95f0130fe8..b756318158 100644 --- a/Tests/Opc.Ua.Configuration.Tests/ApplicationInstanceTests.cs +++ b/Tests/Opc.Ua.Configuration.Tests/ApplicationInstanceTests.cs @@ -330,7 +330,7 @@ public async Task TestNoFileConfigAsServerX509Store() { // store public key in trusted store var rawData = applicationCertificate.Certificate.RawData; - await store.Add(new X509Certificate2(rawData)).ConfigureAwait(false); + await store.Add(X509CertificateLoader.LoadCertificate(rawData)).ConfigureAwait(false); } if (deleteAfterUse) @@ -427,7 +427,7 @@ public async Task TestInvalidAppCertDoNotRecreate(InvalidCertType certType, bool applicationCertificate.StoreType, applicationCertificate.StorePath ); - publicKey = new X509Certificate2(testCert.RawData); + publicKey = X509CertificateLoader.LoadCertificate(testCert.RawData); } using (publicKey) @@ -514,7 +514,7 @@ public async Task TestInvalidAppCertChainDoNotRecreate(InvalidCertType certType, applicationCertificate.StoreType, applicationCertificate.StorePath ); - publicKey = new X509Certificate2(testCert.RawData); + publicKey = X509CertificateLoader.LoadCertificate(testCert.RawData); } using (publicKey) @@ -715,7 +715,7 @@ private X509Certificate2Collection CreateInvalidCertChain(InvalidCertType certTy var result = new X509Certificate2Collection { appCert, - new X509Certificate2(rootCA.RawData) + X509CertificateLoader.LoadCertificate(rootCA.RawData) }; return result; diff --git a/Tests/Opc.Ua.Core.Tests/Security/Certificates/CertificateFactoryTest.cs b/Tests/Opc.Ua.Core.Tests/Security/Certificates/CertificateFactoryTest.cs index 93637490d7..37f36749b0 100644 --- a/Tests/Opc.Ua.Core.Tests/Security/Certificates/CertificateFactoryTest.cs +++ b/Tests/Opc.Ua.Core.Tests/Security/Certificates/CertificateFactoryTest.cs @@ -111,7 +111,7 @@ KeyHashPair keyHashPair { rsa.ExportParameters(false); } - var plainCert = new X509Certificate2(cert.RawData); + var plainCert = X509CertificateLoader.LoadCertificate(cert.RawData); Assert.NotNull(plainCert); VerifyApplicationCert(app, plainCert); X509Utils.VerifyRSAKeyPair(cert, cert, true); @@ -143,7 +143,7 @@ KeyHashPair keyHashPair Assert.NotNull(cert); Assert.NotNull(cert.RawData); Assert.True(cert.HasPrivateKey); - using (var plainCert = new X509Certificate2(cert.RawData)) + using (var plainCert = X509CertificateLoader.LoadCertificate(cert.RawData)) { Assert.NotNull(plainCert); VerifyApplicationCert(app, plainCert, issuerCertificate); @@ -171,7 +171,7 @@ KeyHashPair keyHashPair Assert.NotNull(cert); Assert.NotNull(cert.RawData); Assert.True(cert.HasPrivateKey); - var plainCert = new X509Certificate2(cert.RawData); + var plainCert = X509CertificateLoader.LoadCertificate(cert.RawData); Assert.NotNull(plainCert); VerifyCACert(plainCert, subject, pathLengthConstraint); X509Utils.VerifyRSAKeyPair(cert, cert, true); @@ -220,7 +220,7 @@ KeyHashPair keyHashPair Assert.NotNull(rsa); } - using (var plainCert = new X509Certificate2(issuerCertificate.RawData)) + using (var plainCert = X509CertificateLoader.LoadCertificate(issuerCertificate.RawData)) { Assert.NotNull(plainCert); VerifyCACert(plainCert, issuerCertificate.Subject, pathLengthConstraint); @@ -285,7 +285,7 @@ public void ParseCertificateBlob() byte[] singleBlob = AsnUtils.ParseX509Blob(certBlob).ToArray(); Assert.NotNull(singleBlob); - var certX = new X509Certificate2(singleBlob); + var certX = X509CertificateLoader.LoadCertificate(singleBlob); Assert.NotNull(certX); Assert.AreEqual(certArray[0].RawData, singleBlob); Assert.AreEqual(singleBlob, certX.RawData); diff --git a/Tests/Opc.Ua.Core.Tests/Security/Certificates/CertificateStoreTest.cs b/Tests/Opc.Ua.Core.Tests/Security/Certificates/CertificateStoreTest.cs index d77170d59a..012ba43a0e 100644 --- a/Tests/Opc.Ua.Core.Tests/Security/Certificates/CertificateStoreTest.cs +++ b/Tests/Opc.Ua.Core.Tests/Security/Certificates/CertificateStoreTest.cs @@ -118,7 +118,7 @@ public async Task VerifyAppCertX509Store(string storePath) CertificateStoreType.X509Store, storePath ); - using (var publicKey = new X509Certificate2(appCertificate.RawData)) + using (var publicKey = X509CertificateLoader.LoadCertificate(appCertificate.RawData)) { Assert.NotNull(publicKey); Assert.False(publicKey.HasPrivateKey); @@ -163,7 +163,7 @@ public async Task VerifyAppCertDirectoryStore() certificateStoreIdentifier, password ); - using (var publicKey = new X509Certificate2(appCertificate.RawData)) + using (var publicKey = X509CertificateLoader.LoadCertificate(appCertificate.RawData)) { Assert.NotNull(publicKey); Assert.False(publicKey.HasPrivateKey); diff --git a/Tests/Opc.Ua.Core.Tests/Security/Certificates/CertificateValidatorTest.cs b/Tests/Opc.Ua.Core.Tests/Security/Certificates/CertificateValidatorTest.cs index 71466fbced..23a3f03f50 100644 --- a/Tests/Opc.Ua.Core.Tests/Security/Certificates/CertificateValidatorTest.cs +++ b/Tests/Opc.Ua.Core.Tests/Security/Certificates/CertificateValidatorTest.cs @@ -479,7 +479,7 @@ public async Task VerifyAppChainsOneTrusted() TestContext.Out.WriteLine($"InitValidator: {stopWatch.ElapsedMilliseconds - start}"); foreach (var app in m_goodApplicationTestSet) { - certValidator.Validate(new X509Certificate2(app.Certificate)); + certValidator.Validate(X509CertificateLoader.LoadCertificate(app.Certificate)); } TestContext.Out.WriteLine($"Validation: {stopWatch.ElapsedMilliseconds - start}"); } @@ -507,7 +507,7 @@ public async Task VerifyAppChainsAllButOneTrusted() var certValidator = validator.Update(); foreach (var app in m_goodApplicationTestSet) { - certValidator.Validate(new X509Certificate2(app.Certificate)); + certValidator.Validate(X509CertificateLoader.LoadCertificate(app.Certificate)); } } } @@ -535,7 +535,7 @@ public async Task VerifyAppChainsIncompleteChain() var certValidator = validator.Update(); foreach (var app in m_goodApplicationTestSet) { - var serviceResultException = Assert.Throws(() => certValidator.Validate(new X509Certificate2(app.Certificate))); + var serviceResultException = Assert.Throws(() => certValidator.Validate(X509CertificateLoader.LoadCertificate(app.Certificate))); Assert.AreEqual((StatusCode)StatusCodes.BadCertificateChainIncomplete, (StatusCode)serviceResultException.StatusCode, serviceResultException.Message); } } @@ -569,7 +569,7 @@ public async Task VerifyAppChainsInvalidChain() var certValidator = validator.Update(); foreach (var app in m_goodApplicationTestSet) { - var serviceResultException = Assert.Throws(() => certValidator.Validate(new X509Certificate2(app.Certificate))); + var serviceResultException = Assert.Throws(() => certValidator.Validate(X509CertificateLoader.LoadCertificate(app.Certificate))); Assert.AreEqual((StatusCode)StatusCodes.BadCertificateChainIncomplete, (StatusCode)serviceResultException.StatusCode, serviceResultException.Message); } } @@ -598,7 +598,7 @@ public async Task VerifyAppChainsWithGoodAndInvalidChain() var certValidator = validator.Update(); foreach (var app in m_goodApplicationTestSet) { - certValidator.Validate(new X509Certificate2(app.Certificate)); + certValidator.Validate(X509CertificateLoader.LoadCertificate(app.Certificate)); } } } @@ -631,7 +631,7 @@ public async Task VerifyRevokedTrustedStoreAppChains() var certValidator = validator.Update(); foreach (var app in m_goodApplicationTestSet) { - ServiceResultException serviceResultException = Assert.Throws(() => certValidator.Validate(new X509Certificate2(app.Certificate))); + ServiceResultException serviceResultException = Assert.Throws(() => certValidator.Validate(X509CertificateLoader.LoadCertificate(app.Certificate))); Assert.AreEqual(v == kCaChainCount - 1 ? StatusCodes.BadCertificateRevoked : StatusCodes.BadCertificateIssuerRevoked, serviceResultException.StatusCode, serviceResultException.Message); } @@ -665,7 +665,7 @@ public async Task VerifyRevokedIssuerStoreAppChains() var certValidator = validator.Update(); foreach (var app in m_goodApplicationTestSet) { - var serviceResultException = Assert.Throws(() => certValidator.Validate(new X509Certificate2(app.Certificate))); + var serviceResultException = Assert.Throws(() => certValidator.Validate(X509CertificateLoader.LoadCertificate(app.Certificate))); Assert.AreEqual(v == kCaChainCount - 1 ? StatusCodes.BadCertificateRevoked : StatusCodes.BadCertificateIssuerRevoked, serviceResultException.StatusCode, serviceResultException.Message); } @@ -698,12 +698,12 @@ public async Task VerifyRevokedIssuerStoreTrustedAppChains() } foreach (var app in m_goodApplicationTestSet) { - await validator.TrustedStore.Add(new X509Certificate2(app.Certificate)).ConfigureAwait(false); + await validator.TrustedStore.Add(X509CertificateLoader.LoadCertificate(app.Certificate)).ConfigureAwait(false); } var certValidator = validator.Update(); foreach (var app in m_goodApplicationTestSet) { - var serviceResultException = Assert.Throws(() => certValidator.Validate(new X509Certificate2(app.Certificate))); + var serviceResultException = Assert.Throws(() => certValidator.Validate(X509CertificateLoader.LoadCertificate(app.Certificate))); Assert.AreEqual(v == kCaChainCount - 1 ? StatusCodes.BadCertificateRevoked : StatusCodes.BadCertificateIssuerRevoked, serviceResultException.StatusCode, serviceResultException.Message); } @@ -736,7 +736,7 @@ public async Task VerifyRevokedTrustedStoreNotTrustedAppChains() var certValidator = validator.Update(); foreach (var app in m_goodApplicationTestSet) { - var serviceResultException = Assert.Throws(() => certValidator.Validate(new X509Certificate2(app.Certificate))); + var serviceResultException = Assert.Throws(() => certValidator.Validate(X509CertificateLoader.LoadCertificate(app.Certificate))); Assert.AreEqual(v == kCaChainCount - 1 ? StatusCodes.BadCertificateRevoked : StatusCodes.BadCertificateIssuerRevoked, serviceResultException.StatusCode, serviceResultException.Message); } @@ -768,12 +768,12 @@ public async Task VerifyRevokedTrustedStoreTrustedAppChains() } foreach (var app in m_goodApplicationTestSet) { - await validator.TrustedStore.Add(new X509Certificate2(app.Certificate)).ConfigureAwait(false); + await validator.TrustedStore.Add(X509CertificateLoader.LoadCertificate(app.Certificate)).ConfigureAwait(false); } var certValidator = validator.Update(); foreach (var app in m_goodApplicationTestSet) { - var serviceResultException = Assert.Throws(() => certValidator.Validate(new X509Certificate2(app.Certificate))); + var serviceResultException = Assert.Throws(() => certValidator.Validate(X509CertificateLoader.LoadCertificate(app.Certificate))); Assert.AreEqual(v == kCaChainCount - 1 ? StatusCodes.BadCertificateRevoked : StatusCodes.BadCertificateIssuerRevoked, serviceResultException.StatusCode, serviceResultException.Message); } @@ -799,13 +799,13 @@ public async Task VerifyIssuerChainIncompleteTrustedAppCerts() // all app certs are trusted foreach (var app in m_goodApplicationTestSet) { - await validator.TrustedStore.Add(new X509Certificate2(app.Certificate)).ConfigureAwait(false); + await validator.TrustedStore.Add(X509CertificateLoader.LoadCertificate(app.Certificate)).ConfigureAwait(false); } var certValidator = validator.Update(); foreach (var app in m_goodApplicationTestSet) { - certValidator.Validate(new X509Certificate2(app.Certificate)); + certValidator.Validate(X509CertificateLoader.LoadCertificate(app.Certificate)); } } } @@ -834,13 +834,13 @@ public async Task VerifyIssuerChainTrustedAppCerts() // all app certs are trusted foreach (var app in m_goodApplicationTestSet) { - await validator.TrustedStore.Add(new X509Certificate2(app.Certificate)).ConfigureAwait(false); + await validator.TrustedStore.Add(X509CertificateLoader.LoadCertificate(app.Certificate)).ConfigureAwait(false); } var certValidator = validator.Update(); foreach (var app in m_goodApplicationTestSet) { - var serviceResultException = Assert.Throws(() => certValidator.Validate(new X509Certificate2(app.Certificate))); + var serviceResultException = Assert.Throws(() => certValidator.Validate(X509CertificateLoader.LoadCertificate(app.Certificate))); Assert.AreEqual((StatusCode)StatusCodes.BadCertificateChainIncomplete, (StatusCode)serviceResultException.StatusCode, serviceResultException.Message); } } @@ -859,9 +859,9 @@ public void VerifyPemWriterPrivateKeys() var pemDataBlob = PEMWriter.ExportPrivateKeyAsPEM(appCert); var pemString = Encoding.UTF8.GetString(pemDataBlob); TestContext.Out.WriteLine(pemString); - CertificateFactory.CreateCertificateWithPEMPrivateKey(new X509Certificate2(appCert.RawData), pemDataBlob); + CertificateFactory.CreateCertificateWithPEMPrivateKey(X509CertificateLoader.LoadCertificate(appCert.RawData), pemDataBlob); // note: password is ignored - var newCert = CertificateFactory.CreateCertificateWithPEMPrivateKey(new X509Certificate2(appCert.RawData), pemDataBlob, "password"); + var newCert = CertificateFactory.CreateCertificateWithPEMPrivateKey(X509CertificateLoader.LoadCertificate(appCert.RawData), pemDataBlob, "password"); X509Utils.VerifyRSAKeyPair(newCert, newCert, true); } } @@ -897,10 +897,10 @@ public void VerifyPemWriterRSAPrivateKeys() var pemDataBlob = PEMWriter.ExportRSAPrivateKeyAsPEM(appCert); var pemString = Encoding.UTF8.GetString(pemDataBlob); TestContext.Out.WriteLine(pemString); - var cert = CertificateFactory.CreateCertificateWithPEMPrivateKey(new X509Certificate2(appCert.RawData), pemDataBlob); + var cert = CertificateFactory.CreateCertificateWithPEMPrivateKey(X509CertificateLoader.LoadCertificate(appCert.RawData), pemDataBlob); Assert.NotNull(cert); // note: password is ignored - var newCert = CertificateFactory.CreateCertificateWithPEMPrivateKey(new X509Certificate2(appCert.RawData), pemDataBlob, "password"); + var newCert = CertificateFactory.CreateCertificateWithPEMPrivateKey(X509CertificateLoader.LoadCertificate(appCert.RawData), pemDataBlob, "password"); X509Utils.VerifyRSAKeyPair(newCert, newCert, true); } } @@ -1378,7 +1378,7 @@ public async Task VerifySomeMissingCRLRevokedTrustedStoreAppChains(bool rejectUn foreach (var app in m_goodApplicationTestSet) { - ServiceResultException serviceResultException = Assert.Throws(() => certValidator.Validate(new X509Certificate2(app.Certificate))); + ServiceResultException serviceResultException = Assert.Throws(() => certValidator.Validate(X509CertificateLoader.LoadCertificate(app.Certificate))); Assert.AreEqual(v == kCaChainCount - 1 ? StatusCodes.BadCertificateRevoked : StatusCodes.BadCertificateIssuerRevoked, serviceResultException.StatusCode, serviceResultException.Message); @@ -1421,7 +1421,7 @@ public async Task VerifyAllMissingCRLRevokedTrustedStoreAppChains() foreach (var app in m_goodApplicationTestSet) { - ServiceResultException serviceResultException = Assert.Throws(() => certValidator.Validate(new X509Certificate2(app.Certificate))); + ServiceResultException serviceResultException = Assert.Throws(() => certValidator.Validate(X509CertificateLoader.LoadCertificate(app.Certificate))); Assert.IsTrue(StatusCodes.BadCertificateRevocationUnknown == serviceResultException.StatusCode, serviceResultException.Message); @@ -1483,7 +1483,7 @@ public async Task VerifySomeMissingCRLTrustedStoreAppChains(bool rejectUnknownRe { if (rejectUnknownRevocationStatus) { - ServiceResultException serviceResultException = Assert.Throws(() => certValidator.Validate(new X509Certificate2(app.Certificate))); + ServiceResultException serviceResultException = Assert.Throws(() => certValidator.Validate(X509CertificateLoader.LoadCertificate(app.Certificate))); Assert.AreEqual(v == kCaChainCount - 1 ? StatusCodes.BadCertificateRevocationUnknown : StatusCodes.BadCertificateIssuerRevocationUnknown, serviceResultException.StatusCode, @@ -1491,7 +1491,7 @@ public async Task VerifySomeMissingCRLTrustedStoreAppChains(bool rejectUnknownRe } else { - certValidator.Validate(new X509Certificate2(app.Certificate)); + certValidator.Validate(X509CertificateLoader.LoadCertificate(app.Certificate)); } } } @@ -1523,7 +1523,7 @@ public async Task VerifyMissingCRLANDAppChainsIncompleteChain(bool rejectUnknown foreach (var app in m_goodApplicationTestSet) { - var serviceResultException = Assert.Throws(() => certValidator.Validate(new X509Certificate2(app.Certificate))); + var serviceResultException = Assert.Throws(() => certValidator.Validate(X509CertificateLoader.LoadCertificate(app.Certificate))); Assert.AreEqual((StatusCode)StatusCodes.BadCertificateChainIncomplete, (StatusCode)serviceResultException.StatusCode, serviceResultException.Message); // no need to check for inner exceptions, since an incomplete chain error cannot be suppressed. } @@ -1547,12 +1547,12 @@ public async Task VerifyExistingCRLAppChainsExpiredCertificates(bool rejectUnkno { if (i != v || kCaChainCount == 1) { - await validator.TrustedStore.Add(new X509Certificate2(m_caChain[i].RawData)).ConfigureAwait(false); + await validator.TrustedStore.Add(X509CertificateLoader.LoadCertificate(m_caChain[i].RawData)).ConfigureAwait(false); await validator.TrustedStore.AddCRL(m_crlChain[i]).ConfigureAwait(false); } else { - await validator.IssuerStore.Add(new X509Certificate2(m_caChain[i].RawData)).ConfigureAwait(false); + await validator.IssuerStore.Add(X509CertificateLoader.LoadCertificate(m_caChain[i].RawData)).ConfigureAwait(false); await validator.IssuerStore.AddCRL(m_crlChain[i]).ConfigureAwait(false); } } @@ -1563,7 +1563,7 @@ public async Task VerifyExistingCRLAppChainsExpiredCertificates(bool rejectUnkno foreach (var app in m_notYetValidCertsApplicationTestSet) { - var serviceResultException = Assert.Throws(() => certValidator.Validate(new X509Certificate2(app.Certificate))); + var serviceResultException = Assert.Throws(() => certValidator.Validate(X509CertificateLoader.LoadCertificate(app.Certificate))); Assert.AreEqual((StatusCode)StatusCodes.BadCertificateTimeInvalid, (StatusCode)serviceResultException.StatusCode, serviceResultException.Message); } } @@ -1602,7 +1602,7 @@ public async Task VerifyMissingCRLAppChainsExpiredCertificates(bool rejectUnknow foreach (var app in m_notYetValidCertsApplicationTestSet) { - var serviceResultException = Assert.Throws(() => certValidator.Validate(new X509Certificate2(app.Certificate))); + var serviceResultException = Assert.Throws(() => certValidator.Validate(X509CertificateLoader.LoadCertificate(app.Certificate))); Assert.AreEqual((StatusCode)StatusCodes.BadCertificateTimeInvalid, (StatusCode)serviceResultException.StatusCode, serviceResultException.Message); // BadCertificateTimeInvalid can be suppressed. Ensure the other issues are caught, as well: @@ -1663,7 +1663,7 @@ public async Task VerifyMissingCRLNoTrust(bool rejectUnknownRevocationStatus) foreach (var app in m_goodApplicationTestSet) { - var serviceResultException = Assert.Throws(() => certValidator.Validate(new X509Certificate2(app.Certificate))); + var serviceResultException = Assert.Throws(() => certValidator.Validate(X509CertificateLoader.LoadCertificate(app.Certificate))); Assert.AreEqual((StatusCode)StatusCodes.BadCertificateUntrusted, (StatusCode)serviceResultException.StatusCode, serviceResultException.Message); } } diff --git a/Tests/Opc.Ua.Gds.Tests/ClientTest.cs b/Tests/Opc.Ua.Gds.Tests/ClientTest.cs index bff7f8ddd8..4624402337 100644 --- a/Tests/Opc.Ua.Gds.Tests/ClientTest.cs +++ b/Tests/Opc.Ua.Gds.Tests/ClientTest.cs @@ -794,7 +794,7 @@ public void StartGoodSigningRequests() } else { - csrCertificate = CertificateFactory.CreateCertificateWithPEMPrivateKey(new X509Certificate2(application.Certificate), application.PrivateKey, application.PrivateKeyPassword); + csrCertificate = CertificateFactory.CreateCertificateWithPEMPrivateKey(X509CertificateLoader.LoadCertificate(application.Certificate), application.PrivateKey, application.PrivateKeyPassword); } byte[] certificateRequest = CertificateFactory.CreateSigningRequest(csrCertificate, application.DomainNames); csrCertificate.Dispose(); @@ -1089,7 +1089,7 @@ public void GoodSigningRequestAsSelfAdmin() } else { - csrCertificate = CertificateFactory.CreateCertificateWithPEMPrivateKey(new X509Certificate2(application.Certificate), application.PrivateKey, application.PrivateKeyPassword); + csrCertificate = CertificateFactory.CreateCertificateWithPEMPrivateKey(X509CertificateLoader.LoadCertificate(application.Certificate), application.PrivateKey, application.PrivateKeyPassword); } byte[] certificateRequest = CertificateFactory.CreateSigningRequest(csrCertificate, application.DomainNames); csrCertificate.Dispose(); diff --git a/Tests/Opc.Ua.Gds.Tests/GlobalDiscoveryTestClient.cs b/Tests/Opc.Ua.Gds.Tests/GlobalDiscoveryTestClient.cs index e7ea4a97e5..316312c47a 100644 --- a/Tests/Opc.Ua.Gds.Tests/GlobalDiscoveryTestClient.cs +++ b/Tests/Opc.Ua.Gds.Tests/GlobalDiscoveryTestClient.cs @@ -206,7 +206,7 @@ public string ReadLogFile() #region Private Methods private async Task ApplyNewApplicationInstanceCertificateAsync(byte[] certificate, byte[] privateKey) { - using (var x509 = new X509Certificate2(certificate)) + using (var x509 = X509CertificateLoader.LoadCertificate(certificate)) { var certWithPrivateKey = CertificateFactory.CreateCertificateWithPEMPrivateKey(x509, privateKey); m_client.Configuration.SecurityConfiguration.ApplicationCertificate = new CertificateIdentifier(certWithPrivateKey); diff --git a/Tests/Opc.Ua.Gds.Tests/PushTest.cs b/Tests/Opc.Ua.Gds.Tests/PushTest.cs index 6b9cfb2362..4c9f272106 100644 --- a/Tests/Opc.Ua.Gds.Tests/PushTest.cs +++ b/Tests/Opc.Ua.Gds.Tests/PushTest.cs @@ -83,7 +83,7 @@ protected async Task OneTimeSetUp() ConnectGDSClient(true); RegisterPushServerApplication(m_pushClient.PushClient.EndpointUrl); - m_selfSignedServerCert = new X509Certificate2(m_pushClient.PushClient.Session.ConfiguredEndpoint.Description.ServerCertificate); + m_selfSignedServerCert = X509CertificateLoader.LoadCertificate(m_pushClient.PushClient.Session.ConfiguredEndpoint.Description.ServerCertificate); m_domainNames = X509Utils.GetDomainsFromCertificate(m_selfSignedServerCert).ToArray(); await CreateCATestCerts(m_pushClient.TempStorePath).ConfigureAwait(false); @@ -328,7 +328,7 @@ public void UpdateCertificateSelfSignedNoPrivateKeyAsserts() { ConnectPushClient(true); using (X509Certificate2 invalidCert = CertificateFactory.CreateCertificate("uri:x:y:z", "TestApp", "CN=Push Server Test", null).CreateForRSA()) - using (X509Certificate2 serverCert = new X509Certificate2(m_pushClient.PushClient.Session.ConfiguredEndpoint.Description.ServerCertificate)) + using (X509Certificate2 serverCert = X509CertificateLoader.LoadCertificate(m_pushClient.PushClient.Session.ConfiguredEndpoint.Description.ServerCertificate)) { if (!X509Utils.CompareDistinguishedName(serverCert.Subject, serverCert.Issuer)) { @@ -358,7 +358,7 @@ public void UpdateCertificateSelfSignedNoPrivateKeyAsserts() public void UpdateCertificateSelfSignedNoPrivateKey() { ConnectPushClient(true); - using (X509Certificate2 serverCert = new X509Certificate2(m_pushClient.PushClient.Session.ConfiguredEndpoint.Description.ServerCertificate)) + using (X509Certificate2 serverCert = X509CertificateLoader.LoadCertificate(m_pushClient.PushClient.Session.ConfiguredEndpoint.Description.ServerCertificate)) { if (!X509Utils.CompareDistinguishedName(serverCert.Subject, serverCert.Issuer)) { @@ -627,7 +627,7 @@ public void GetCertificates() Assert.That(certificateTypeIds.Length == 1); Assert.NotNull(certificates[0]); - using (var x509 = new X509Certificate2(certificates[0])) + using (var x509 = X509CertificateLoader.LoadCertificate(certificates[0])) { Assert.NotNull(x509); } @@ -687,7 +687,7 @@ private X509Certificate2Collection CreateCertCollection(ByteStringCollection cer var result = new X509Certificate2Collection(); foreach (var rawCert in certList) { - result.Add(new X509Certificate2(rawCert)); + result.Add(X509CertificateLoader.LoadCertificate(rawCert)); } return result; } @@ -757,7 +757,7 @@ private async Task AddTrustListToStore(SecurityConfiguration config, Trust issuerCertificates = new X509Certificate2Collection(); foreach (var cert in trustList.IssuerCertificates) { - issuerCertificates.Add(new X509Certificate2(cert)); + issuerCertificates.Add(X509CertificateLoader.LoadCertificate(cert)); } } if ((masks & TrustListMasks.IssuerCrls) != 0) @@ -773,7 +773,7 @@ private async Task AddTrustListToStore(SecurityConfiguration config, Trust trustedCertificates = new X509Certificate2Collection(); foreach (var cert in trustList.TrustedCertificates) { - trustedCertificates.Add(new X509Certificate2(cert)); + trustedCertificates.Add(X509CertificateLoader.LoadCertificate(cert)); } } if ((masks & TrustListMasks.TrustedCrls) != 0) diff --git a/Tests/Opc.Ua.Gds.Tests/X509TestUtils.cs b/Tests/Opc.Ua.Gds.Tests/X509TestUtils.cs index e61ad47a1c..8bfa910deb 100644 --- a/Tests/Opc.Ua.Gds.Tests/X509TestUtils.cs +++ b/Tests/Opc.Ua.Gds.Tests/X509TestUtils.cs @@ -42,7 +42,7 @@ public static class X509TestUtils { public static void VerifyApplicationCertIntegrity(byte[] certificate, byte[] privateKey, string privateKeyPassword, string privateKeyFormat, byte[][] issuerCertificates) { - X509Certificate2 newCert = new X509Certificate2(certificate); + X509Certificate2 newCert = X509CertificateLoader.LoadCertificate(certificate); Assert.IsNotNull(newCert); X509Certificate2 newPrivateKeyCert = null; if (privateKeyFormat == "PFX") @@ -64,7 +64,7 @@ public static void VerifyApplicationCertIntegrity(byte[] certificate, byte[] pri CertificateIdentifierCollection issuerCertIdCollection = new CertificateIdentifierCollection(); foreach (var issuer in issuerCertificates) { - var issuerCert = new X509Certificate2(issuer); + var issuerCert = X509CertificateLoader.LoadCertificate(issuer); Assert.IsNotNull(issuerCert); issuerCertIdCollection.Add(new CertificateIdentifier(issuerCert)); } @@ -85,8 +85,8 @@ public static void VerifyApplicationCertIntegrity(byte[] certificate, byte[] pri public static void VerifySignedApplicationCert(ApplicationTestData testApp, byte[] rawSignedCert, byte[][] rawIssuerCerts) { - X509Certificate2 signedCert = new X509Certificate2(rawSignedCert); - X509Certificate2 issuerCert = new X509Certificate2(rawIssuerCerts[0]); + X509Certificate2 signedCert = X509CertificateLoader.LoadCertificate(rawSignedCert); + X509Certificate2 issuerCert = X509CertificateLoader.LoadCertificate(rawIssuerCerts[0]); TestContext.Out.WriteLine($"Signed cert: {signedCert}"); TestContext.Out.WriteLine($"Issuer cert: {issuerCert}"); diff --git a/Tests/Opc.Ua.Security.Certificates.Tests/CRLTests.cs b/Tests/Opc.Ua.Security.Certificates.Tests/CRLTests.cs index 536f78fd0b..a6302d9055 100644 --- a/Tests/Opc.Ua.Security.Certificates.Tests/CRLTests.cs +++ b/Tests/Opc.Ua.Security.Certificates.Tests/CRLTests.cs @@ -174,7 +174,7 @@ public void CrlBuilderTest(bool empty, bool noExtensions, KeyHashPair keyHashPai Assert.AreEqual(2, x509Crl.CrlExtensions.Count); } - using (var issuerPubKey = new X509Certificate2(m_issuerCert.RawData)) + using (var issuerPubKey = X509CertificateLoader.LoadCertificate(m_issuerCert.RawData)) { Assert.True(x509Crl.VerifySignature(issuerPubKey, true)); } @@ -219,7 +219,7 @@ public void CrlBuilderTestWithSignatureGenerator(KeyHashPair keyHashPair) Assert.AreEqual(serial, x509Crl.RevokedCertificates[0].UserCertificate); Assert.AreEqual(serstring, x509Crl.RevokedCertificates[1].SerialNumber); Assert.AreEqual(2, x509Crl.CrlExtensions.Count); - using (var issuerPubKey = new X509Certificate2(m_issuerCert.RawData)) + using (var issuerPubKey = X509CertificateLoader.LoadCertificate(m_issuerCert.RawData)) { Assert.True(x509Crl.VerifySignature(issuerPubKey, true)); } diff --git a/Tests/Opc.Ua.Security.Certificates.Tests/CertificateTestUtils.cs b/Tests/Opc.Ua.Security.Certificates.Tests/CertificateTestUtils.cs index 2f2cc4a85a..feb037b27b 100644 --- a/Tests/Opc.Ua.Security.Certificates.Tests/CertificateTestUtils.cs +++ b/Tests/Opc.Ua.Security.Certificates.Tests/CertificateTestUtils.cs @@ -193,7 +193,7 @@ public void Initialize(byte[] blob, string path) Cert = blob; try { - X509Certificate = new X509Certificate2(path); + X509Certificate = X509CertificateLoader.LoadCertificateFromFile(path); } catch { } diff --git a/Tests/Opc.Ua.Security.Certificates.Tests/CertificateTestsForECDsa.cs b/Tests/Opc.Ua.Security.Certificates.Tests/CertificateTestsForECDsa.cs index 2c2d24ab7b..767f96a233 100644 --- a/Tests/Opc.Ua.Security.Certificates.Tests/CertificateTestsForECDsa.cs +++ b/Tests/Opc.Ua.Security.Certificates.Tests/CertificateTestsForECDsa.cs @@ -304,7 +304,7 @@ ECCurveHashPair ecCurveHashPair { var generator = X509SignatureGenerator.CreateForECDsa(ecdsaPrivateKey); var cert = CertificateBuilder.Create("CN=App Cert") - .SetIssuer(new X509Certificate2(signingCert.RawData)) + .SetIssuer(X509CertificateLoader.LoadCertificate(signingCert.RawData)) .CreateForRSA(generator); Assert.NotNull(cert); WriteCertificate(cert, "Default signed ECDsa cert"); @@ -316,7 +316,7 @@ ECCurveHashPair ecCurveHashPair var generator = X509SignatureGenerator.CreateForECDsa(ecdsaPrivateKey); var cert = CertificateBuilder.Create("CN=App Cert") .SetHashAlgorithm(ecCurveHashPair.HashAlgorithmName) - .SetIssuer(new X509Certificate2(signingCert.RawData)) + .SetIssuer(X509CertificateLoader.LoadCertificate(signingCert.RawData)) .SetECDsaPublicKey(ecdsaPublicKey) .CreateForECDsa(generator); Assert.NotNull(cert); @@ -328,7 +328,7 @@ ECCurveHashPair ecCurveHashPair var generator = X509SignatureGenerator.CreateForECDsa(ecdsaPrivateKey); var cert = CertificateBuilder.Create("CN=App Cert") .SetHashAlgorithm(ecCurveHashPair.HashAlgorithmName) - .SetIssuer(new X509Certificate2(signingCert.RawData)) + .SetIssuer(X509CertificateLoader.LoadCertificate(signingCert.RawData)) .SetECCurve(ecCurveHashPair.Curve) .CreateForECDsa(generator); Assert.NotNull(cert); diff --git a/Tests/Opc.Ua.Security.Certificates.Tests/CertificateTestsForRSA.cs b/Tests/Opc.Ua.Security.Certificates.Tests/CertificateTestsForRSA.cs index e9563031e6..7abef4c8f9 100644 --- a/Tests/Opc.Ua.Security.Certificates.Tests/CertificateTestsForRSA.cs +++ b/Tests/Opc.Ua.Security.Certificates.Tests/CertificateTestsForRSA.cs @@ -350,7 +350,7 @@ public void CreateIssuerRSAWithSuppliedKeyPair() .CreateForRSA(generator)) { Assert.NotNull(cert); - issuer = new X509Certificate2(cert.RawData); + issuer = X509CertificateLoader.LoadCertificate(cert.RawData); WriteCertificate(cert, "Default root cert with supplied RSA cert"); CheckPEMWriter(cert); } @@ -388,7 +388,7 @@ public void CreateIssuerRSACngWithSuppliedKeyPair() .CreateForRSA(generator)) { Assert.NotNull(cert); - issuer = new X509Certificate2(cert.RawData); + issuer = X509CertificateLoader.LoadCertificate(cert.RawData); WriteCertificate(cert, "Default root cert with supplied RSA cert"); CheckPEMWriter(cert); } @@ -425,7 +425,7 @@ KeyHashPair keyHashPair using (RSA rsaPrivateKey = signingCert.GetRSAPrivateKey()) { var generator = X509SignatureGenerator.CreateForRSA(rsaPrivateKey, RSASignaturePadding.Pkcs1); - using (var issuer = new X509Certificate2(signingCert.RawData)) + using (var issuer = X509CertificateLoader.LoadCertificate(signingCert.RawData)) using (var cert = CertificateBuilder.Create("CN=App Cert") .SetIssuer(issuer) .CreateForRSA(generator)) @@ -442,7 +442,7 @@ KeyHashPair keyHashPair using (RSA rsaPublicKey = signingCert.GetRSAPublicKey()) { var generator = X509SignatureGenerator.CreateForRSA(rsaPrivateKey, RSASignaturePadding.Pkcs1); - using (var issuer = new X509Certificate2(signingCert.RawData)) + using (var issuer = X509CertificateLoader.LoadCertificate(signingCert.RawData)) using (var cert = CertificateBuilder.Create("CN=App Cert") .SetHashAlgorithm(keyHashPair.HashAlgorithmName) .SetIssuer(issuer) @@ -457,7 +457,7 @@ KeyHashPair keyHashPair using (RSA rsaPrivateKey = signingCert.GetRSAPrivateKey()) { var generator = X509SignatureGenerator.CreateForRSA(rsaPrivateKey, RSASignaturePadding.Pkcs1); - using (var issuer = new X509Certificate2(signingCert.RawData)) + using (var issuer = X509CertificateLoader.LoadCertificate(signingCert.RawData)) using (var cert = CertificateBuilder.Create("CN=App Cert") .SetHashAlgorithm(keyHashPair.HashAlgorithmName) .SetIssuer(issuer) diff --git a/Tests/Opc.Ua.Security.Certificates.Tests/ExtensionTests.cs b/Tests/Opc.Ua.Security.Certificates.Tests/ExtensionTests.cs index 5078a02647..a574e1282d 100644 --- a/Tests/Opc.Ua.Security.Certificates.Tests/ExtensionTests.cs +++ b/Tests/Opc.Ua.Security.Certificates.Tests/ExtensionTests.cs @@ -56,7 +56,7 @@ public void DecodeExtensions( CertificateAsset certAsset ) { - using (var x509Cert = new X509Certificate2(certAsset.Cert)) + using (var x509Cert = X509CertificateLoader.LoadCertificate(certAsset.Cert)) { Assert.NotNull(x509Cert); TestContext.Out.WriteLine("CertificateAsset:"); diff --git a/Tests/customtest.bat b/Tests/customtest.bat index 7ae1bbecb3..fef271ed2a 100644 --- a/Tests/customtest.bat +++ b/Tests/customtest.bat @@ -2,19 +2,19 @@ setlocal enabledelayedexpansion echo This script is used to run custom platform tests for the UA Core Library -echo Supported parameters: net462, netstandard2.0, netstandard2.1, net472, net48, net6.0, net8.0 +echo Supported parameters: net462, netstandard2.0, netstandard2.1, net472, net48, net6.0, net8.0, net 9.0 REM Check if the target framework parameter is provided if "%1"=="" ( echo Usage: %0 [TargetFramework] - echo Allowed values for TargetFramework: net462, netstandard2.0, netstandard2.1, net472, net48, net6.0, net8.0, default + echo Allowed values for TargetFramework: net462, netstandard2.0, netstandard2.1, net472, net48, net6.0, net8.0, net9.0, default goto :eof ) REM Check if the provided TargetFramework is valid -set "validFrameworks= default net462 net472 netstandard2.0 netstandard2.1 net48 net6.0 net8.0 " +set "validFrameworks= default net462 net472 netstandard2.0 netstandard2.1 net48 net6.0 net8.0 net9.0 " if "!validFrameworks: %1 =!"=="%validFrameworks%" ( - echo Invalid TargetFramework specified. Allowed values are: default, net462, net472 netstandard2.0, netstandard2.1, net48, net6.0, net8.0 + echo Invalid TargetFramework specified. Allowed values are: default, net462, net472 netstandard2.0, netstandard2.1, net48, net6.0, net8.0, net9.0 goto :eof ) diff --git a/azure-pipelines.yml b/azure-pipelines.yml index 1d2ede8b5c..0be491de2c 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -102,6 +102,16 @@ stages: framework: net8.0 configuration: Release jobnamesuffix: net80 +- stage: testnet90 + dependsOn: [build] + displayName: 'Test .NET 9.0' + condition: and(succeeded(), ne(variables.ScheduledBuild, 'False')) + jobs: + - template: .azurepipelines/test.yml + parameters: + framework: net9.0 + configuration: Release + jobnamesuffix: net90 - stage: testnet462 dependsOn: [build] displayName: 'Test .NET 4.6.2' diff --git a/targets.props b/targets.props index 17b539d7c1..b1cf79a211 100644 --- a/targets.props +++ b/targets.props @@ -12,7 +12,7 @@ A build with all custom targets which are not part of a regular build is scheduled once a week in the DevOps build pipeline. Uncomment the following lines to test a custom test target - supported values: net462, netstandard2.0, netstandard2.1, net472, net48, net6.0, net8.0 + supported values: net462, netstandard2.0, netstandard2.1, net472, net48, net6.0, net8.0, net9.0 --> - + - preview-all - net8.0;net6.0;net48 - net8.0 - net48;net8.0 - net472;net48;netstandard2.0;netstandard2.1;net6.0;net8.0 - net472;net48;netstandard2.0;netstandard2.1;net6.0;net8.0 - net462;net472;net48;netstandard2.1;net6.0;net8.0 - net472;net48;netstandard2.0;netstandard2.1;net6.0;net8.0 + preview + all + default + net9.0;net6.0;net48 + net9.0 + net48;net9.0 + net472;net48;netstandard2.0;netstandard2.1;net6.0;net8.0;net9.0 + net472;net48;netstandard2.0;netstandard2.1;net6.0;net8.0;net9.0 + net462;net472;net48;netstandard2.1;net6.0;net8.0;net9.0 + net472;net48;netstandard2.0;netstandard2.1;net6.0;net8.0;net9.0