diff --git a/Neo4j.Driver/Neo4j.Driver.Tests.TestBackend/SupportedFeatures.cs b/Neo4j.Driver/Neo4j.Driver.Tests.TestBackend/SupportedFeatures.cs index e22ff3dd4..22f23dbe6 100644 --- a/Neo4j.Driver/Neo4j.Driver.Tests.TestBackend/SupportedFeatures.cs +++ b/Neo4j.Driver/Neo4j.Driver.Tests.TestBackend/SupportedFeatures.cs @@ -54,6 +54,7 @@ static SupportedFeatures() "Feature:Bolt:4.4", "Feature:Bolt:5.0", "Feature:Bolt:5.2", + "Feature:Bolt:5.3", "Feature:Bolt:Patch:UTC", "Feature:Impersonation", //"Feature:TLS:1.1", diff --git a/Neo4j.Driver/Neo4j.Driver.Tests/Internal/MessageHandling/Messages/BoltAgentBuilderTests.cs b/Neo4j.Driver/Neo4j.Driver.Tests/Internal/MessageHandling/Messages/BoltAgentBuilderTests.cs new file mode 100644 index 000000000..b2538db90 --- /dev/null +++ b/Neo4j.Driver/Neo4j.Driver.Tests/Internal/MessageHandling/Messages/BoltAgentBuilderTests.cs @@ -0,0 +1,39 @@ +// Copyright (c) "Neo4j" +// Neo4j Sweden AB [http://neo4j.com] +// +// This file is part of Neo4j. +// +// Licensed under the Apache License, Version 2.0 (the "License"). +// You may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +using FluentAssertions; +using Neo4j.Driver.Internal.Messaging.Utils; +using Xunit; + +namespace Neo4j.Driver.Internal.MessageHandling.Messages +{ + public class BoltAgentBuilderTests + { + [Fact] + public void ShouldReturnBoltAgent() + { + var agent = BoltAgentBuilder.Agent; + agent.Should() + .HaveCount(3) + .And.ContainKey("platform") + .And.ContainKey("language_details") + .And.ContainKey("product") + .WhichValue.Should() + .MatchRegex(@"^neo4j-dotnet/\d\.\d+\.\d+$"); + } + } +} diff --git a/Neo4j.Driver/Neo4j.Driver.Tests/Internal/MessageHandling/Messages/HelloMessageTests.cs b/Neo4j.Driver/Neo4j.Driver.Tests/Internal/MessageHandling/Messages/HelloMessageTests.cs index 1045f0866..7d0da1785 100644 --- a/Neo4j.Driver/Neo4j.Driver.Tests/Internal/MessageHandling/Messages/HelloMessageTests.cs +++ b/Neo4j.Driver/Neo4j.Driver.Tests/Internal/MessageHandling/Messages/HelloMessageTests.cs @@ -148,5 +148,19 @@ public void ShouldThrowIfPassingAuthToHelloMessageAbove51(int major, int minor) exception.Should().BeOfType(); } + + [Theory] + [InlineData(5, 3)] + public void ShouldContainBoltAgentAbove53(int major, int minor) + { + var message = + new HelloMessage( + new BoltProtocolVersion(major, minor), + "User-Agent", + null, + default(INotificationsConfig)); + + message.Metadata.Should().ContainKey("bolt_agent"); + } } } diff --git a/Neo4j.Driver/Neo4j.Driver.Tests/Internal/Protocol/BoltProtocolFactoryTests.cs b/Neo4j.Driver/Neo4j.Driver.Tests/Internal/Protocol/BoltProtocolFactoryTests.cs index 75c4fe2b4..9e31fa97e 100644 --- a/Neo4j.Driver/Neo4j.Driver.Tests/Internal/Protocol/BoltProtocolFactoryTests.cs +++ b/Neo4j.Driver/Neo4j.Driver.Tests/Internal/Protocol/BoltProtocolFactoryTests.cs @@ -38,6 +38,9 @@ public void ShouldCreateLegacyBoltProtocol() [InlineData(4, 3)] [InlineData(4, 4)] [InlineData(5, 0)] + [InlineData(5, 1)] + [InlineData(5, 2)] + [InlineData(5, 3)] public void ShouldCreateBoltProtocol(int major, int minor) { var boltProtocol = BoltProtocolFactory.Default.ForVersion(new BoltProtocolVersion(major, minor)); diff --git a/Neo4j.Driver/Neo4j.Driver/Internal/Messaging/HelloMessage.cs b/Neo4j.Driver/Neo4j.Driver/Internal/Messaging/HelloMessage.cs index d272b683b..ee3e55bd9 100644 --- a/Neo4j.Driver/Neo4j.Driver/Internal/Messaging/HelloMessage.cs +++ b/Neo4j.Driver/Neo4j.Driver/Internal/Messaging/HelloMessage.cs @@ -27,6 +27,8 @@ internal sealed class HelloMessage : IRequestMessage { private const string UserAgentMetadataKey = "user_agent"; private const string RoutingMetadataKey = "routing"; + private const string BoltAgentMetadataKey = "bolt_agent"; + private const string PatchBoltMetadataKey = "patch_bolt"; public HelloMessage( BoltProtocolVersion version, @@ -41,7 +43,10 @@ public HelloMessage( if (authToken?.Count > 0) { - Metadata = new Dictionary(authToken) { [UserAgentMetadataKey] = userAgent }; + Metadata = new Dictionary(authToken) + { + [UserAgentMetadataKey] = userAgent + }; } else { @@ -55,7 +60,7 @@ public HelloMessage( if (version >= BoltProtocolVersion.V4_3 && version < BoltProtocolVersion.V5_0) { - Metadata.Add("patch_bolt", new[] { "utc" }); + Metadata.Add(PatchBoltMetadataKey, new[] { "utc" }); } } @@ -72,14 +77,19 @@ public HelloMessage( Metadata = new Dictionary { - [UserAgentMetadataKey] = userAgent, - [RoutingMetadataKey] = routingContext + [RoutingMetadataKey] = routingContext, + [UserAgentMetadataKey] = userAgent }; if (version >= BoltProtocolVersion.V5_2) { NotificationsMetadataWriter.AddNotificationsConfigToMetadata(Metadata, notificationsConfig); } + + if (version >= BoltProtocolVersion.V5_3) + { + Metadata.Add(BoltAgentMetadataKey, BoltAgentBuilder.Agent); + } } public IDictionary Metadata { get; } diff --git a/Neo4j.Driver/Neo4j.Driver/Internal/Messaging/Utils/BoltAgentBuilder.cs b/Neo4j.Driver/Neo4j.Driver/Internal/Messaging/Utils/BoltAgentBuilder.cs new file mode 100644 index 000000000..ba892c82d --- /dev/null +++ b/Neo4j.Driver/Neo4j.Driver/Internal/Messaging/Utils/BoltAgentBuilder.cs @@ -0,0 +1,91 @@ +// Copyright (c) "Neo4j" +// Neo4j Sweden AB [http://neo4j.com] +// +// This file is part of Neo4j. +// +// Licensed under the Apache License, Version 2.0 (the "License"). +// You may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +using System; +using System.Collections.Generic; +using System.Runtime.InteropServices; +using System.Threading; + +namespace Neo4j.Driver.Internal.Messaging.Utils; + +internal static class BoltAgentBuilder +{ + private static readonly Lazy> LazyAgent = new( + GetBoltAgent, + LazyThreadSafetyMode.PublicationOnly); + + public static Dictionary Agent => LazyAgent.Value; + + /// + /// This Dictionary follows a common format and other teams across neo4j rely on it. + /// Changes need to be in accordance with company policy. + /// + private static Dictionary GetBoltAgent() + { + var version = typeof(BoltAgentBuilder).Assembly.GetName().Version; + if (version == null) + { + throw new ClientException("Could not collect assembly version of driver required for handshake."); + } + + var os = OsString(); + var env = DotnetString(); + + var boltAgent = new Dictionary(3) + { + ["product"] = $"neo4j-dotnet/{version.Major}.{version.Minor}.{version.Build}" + }; + + if (!string.IsNullOrEmpty(os)) + { + boltAgent["platform"] = os; + } + + if (!string.IsNullOrEmpty(env)) + { + boltAgent["language_details"] = env; + } + + return boltAgent; + } + + private static string DotnetString() + { + try + { + return RuntimeInformation.FrameworkDescription; + } + catch (Exception) + { + return string.Empty; + } + } + + private static string OsString() + { + try + { + var arch = RuntimeInformation.ProcessArchitecture; + var os = RuntimeInformation.OSDescription; + return $"{os};{arch}"; + } + catch (Exception) + { + return string.Empty; + } + } +} diff --git a/Neo4j.Driver/Neo4j.Driver/Internal/Protocol/BoltProtocolFactory.cs b/Neo4j.Driver/Neo4j.Driver/Internal/Protocol/BoltProtocolFactory.cs index d98680929..59e6e1a89 100644 --- a/Neo4j.Driver/Neo4j.Driver/Internal/Protocol/BoltProtocolFactory.cs +++ b/Neo4j.Driver/Neo4j.Driver/Internal/Protocol/BoltProtocolFactory.cs @@ -52,7 +52,7 @@ internal class BoltProtocolFactory : IBoltProtocolFactory //This is a 'magic' handshake identifier to indicate we're using 'BOLT' ('GOGOBOLT') goGoBolt, // 4 versions max. - BoltProtocolVersion.V5_2.PackToIntRange(BoltProtocolVersion.V5_0), + BoltProtocolVersion.V5_3.PackToIntRange(BoltProtocolVersion.V5_0), BoltProtocolVersion.V4_4.PackToIntRange(BoltProtocolVersion.V4_2), BoltProtocolVersion.V4_1.PackToInt(), BoltProtocolVersion.V3_0.PackToInt() @@ -79,7 +79,7 @@ public IBoltProtocol ForVersion(BoltProtocolVersion version) { MajorVersion: 0, MinorVersion: 0 } => throw new NotSupportedException(NoAgreedVersion), { MajorVersion: 3, MinorVersion: 0 } => BoltProtocolV3.Instance, { MajorVersion: 4, MinorVersion: <= 4, MinorVersion: >= 1 } => BoltProtocol.Instance, - { MajorVersion: 5, MinorVersion: <= 2, MinorVersion: >= 0 } => BoltProtocol.Instance, + { MajorVersion: 5, MinorVersion: <= 3, MinorVersion: >= 0 } => BoltProtocol.Instance, _ => throw new NotSupportedException( $"Protocol error, server suggested unexpected protocol version: {version}") }; diff --git a/Neo4j.Driver/Neo4j.Driver/Internal/Protocol/BoltProtocolVersion.cs b/Neo4j.Driver/Neo4j.Driver/Internal/Protocol/BoltProtocolVersion.cs index 4108907e9..e1ff0bdf3 100644 --- a/Neo4j.Driver/Neo4j.Driver/Internal/Protocol/BoltProtocolVersion.cs +++ b/Neo4j.Driver/Neo4j.Driver/Internal/Protocol/BoltProtocolVersion.cs @@ -37,6 +37,7 @@ internal sealed class BoltProtocolVersion : IEquatable public static readonly BoltProtocolVersion V5_0 = new(5, 0); public static readonly BoltProtocolVersion V5_1 = new(5, 1); public static readonly BoltProtocolVersion V5_2 = new(5, 2); + public static readonly BoltProtocolVersion V5_3 = new(5, 3); private readonly int _compValue;