Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
52 changes: 50 additions & 2 deletions src/Plugins/SignClient/Settings.cs
Original file line number Diff line number Diff line change
Expand Up @@ -18,22 +18,39 @@ public class Settings : PluginSettings
public const string SectionName = "PluginConfiguration";
private const string DefaultEndpoint = "http://127.0.0.1:9991";

internal const string EndpointTcp = "tcp";
internal const string EndpointVsock = "vsock";

/// <summary>
/// The name of the sign client(i.e. Signer).
/// </summary>
public readonly string Name;

/// <summary>
/// The type of the endpoint. Default is "tcp", and "tcp" and "vsock" are supported now.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
/// The type of the endpoint. Default is "tcp", and "tcp" and "vsock" are supported now.
/// The type of the endpoint. Default is "tcp" and "vsock" are supported.

/// If the type is "vsock", the "Endpoint" should be "http://contextId:port" or "https://contextId:port".
/// </summary>
public readonly string EndpointType;

/// <summary>
/// The host of the sign client(i.e. Signer).
/// </summary>
public readonly string Endpoint;

/// <summary>
/// Create a new settings instance from the configuration section.
/// </summary>
/// <param name="section">The configuration section.</param>
/// <exception cref="FormatException">If the endpoint type or endpoint is invalid.</exception>
public Settings(IConfigurationSection section) : base(section)
{
Name = section.GetValue("Name", "SignClient");
EndpointType = section.GetValue("EndpointType", EndpointTcp);
if (EndpointType != EndpointTcp && EndpointType != EndpointVsock)
throw new FormatException($"Invalid endpoint type: {EndpointType}");

// Only support local host at present, so host always is "127.0.0.1" or "::1" now.
Endpoint = section.GetValue("Endpoint", DefaultEndpoint);
Endpoint = section.GetValue("Endpoint", DefaultEndpoint); // Only support local host at present
_ = GetVsockAddress(); // for check the endpoint is valid
}

public static Settings Default
Expand All @@ -51,5 +68,36 @@ public static Settings Default
return new Settings(section);
}
}

/// <summary>
/// Get the vsock address from the endpoint.
/// </summary>
/// <returns>The vsock address. If the endpoint type is not vsock, return null.</returns>
/// <exception cref="FormatException">If the endpoint is invalid.</exception>
internal VsockAddress? GetVsockAddress()
{
if (EndpointType != EndpointVsock) return null;

var endpoint = Endpoint;
if (Endpoint.StartsWith("http://"))
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can you make http:// a prefix variable

OR

string url = "http://www.google.co.uk/path1/path2";
Regex rgx = new Regex(@"(http(s?)://)?(www.)?((?<content>.*?)\.){1}([\w]+\.?)+");
Match MatchResult = rgx.Match(url);
string result = MatchResult.Groups["content"].Value; //google

Copy link
Contributor Author

@Wi1l-B0t Wi1l-B0t Jun 15, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's too complicated.

Simpler code, fewer bugs.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can you make at least http:// a const or static variable

{
endpoint = endpoint.Substring("http://".Length);
}
else if (Endpoint.StartsWith("https://"))
{
endpoint = endpoint.Substring("https://".Length);
}

var parts = endpoint.Split(':', 2);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ipv6 could fail

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

does anyone really using ipv6 now, just asking..... barely see that

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ipv6 could fail

Only vsock address here

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why dont you use URI class to get all the data

Uri uri = new Uri("https://www.dotnetperls.com/");

// Print properties of Uri instance.
Console.WriteLine("AbsolutePath = {0}", uri.AbsolutePath);
Console.WriteLine("AbsoluteUri = {0}", uri.AbsoluteUri);
Console.WriteLine("Authority = {0}", uri.Authority);
Console.WriteLine("DnsSafeHost = {0}", uri.DnsSafeHost);
Console.WriteLine("Fragment = {0}", uri.Fragment);
Console.WriteLine("Host = {0}", uri.Host);
Console.WriteLine("HostNameType = {0}", uri.HostNameType);
Console.WriteLine("IsAbsoluteUri = {0}", uri.IsAbsoluteUri);
Console.WriteLine("IsDefaultPort = {0}", uri.IsDefaultPort);
Console.WriteLine("IsFile = {0}", uri.IsFile);
Console.WriteLine("IsLoopback = {0}", uri.IsLoopback);
Console.WriteLine("IsUnc = {0}", uri.IsUnc);
Console.WriteLine("LocalPath = {0}", uri.LocalPath);
Console.WriteLine("OriginalString = {0}", uri.OriginalString);
Console.WriteLine("PathAndQuery = {0}", uri.PathAndQuery);
Console.WriteLine("Port = {0}", uri.Port);
Console.WriteLine("Query = {0}", uri.Query);
Console.WriteLine("Scheme = {0}", uri.Scheme);
Console.WriteLine("Segments = {0}", string.Join(",", uri.Segments));
Console.WriteLine("UserEscaped = {0}", uri.UserEscaped);
Console.WriteLine("UserInfo = {0}", uri.UserInfo);
AbsolutePath = /
AbsoluteUri = https://www.dotnetperls.com/
Authority = www.dotnetperls.com
DnsSafeHost = www.dotnetperls.com
Fragment =
Host = www.dotnetperls.com
HostNameType = Dns
IsAbsoluteUri = True
IsDefaultPort = True
IsFile = False
IsLoopback = False
IsUnc = False
LocalPath = /
OriginalString = https://www.dotnetperls.com/
PathAndQuery = /
Port = 443
Query =
Scheme = https
Segments = /
UserEscaped = False
UserInfo =

if (parts.Length != 2) throw new FormatException($"Invalid vsock endpoint: {Endpoint}");
try
{
return new VsockAddress(int.Parse(parts[0]), int.Parse(parts[1]));
}
catch
{
throw new FormatException($"Invalid vsock endpoint: {Endpoint}");
}
}
}
}
23 changes: 18 additions & 5 deletions src/Plugins/SignClient/SignClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -66,9 +66,8 @@ private void Reset(string name, SecureSign.SecureSignClient? client)
if (!string.IsNullOrEmpty(_name)) SignerManager.RegisterSigner(_name, this);
}

private void Reset(Settings settings)
private ServiceConfig GetServiceConfig(Settings settings)
{
// _settings = settings;
var methodConfig = new MethodConfig
{
Names = { MethodName.Default },
Expand All @@ -91,10 +90,24 @@ private void Reset(Settings settings)
}
};

var channel = GrpcChannel.ForAddress(settings.Endpoint, new GrpcChannelOptions
return new ServiceConfig { MethodConfigs = { methodConfig } };
}

private void Reset(Settings settings)
{
// _settings = settings;
var serviceConfig = GetServiceConfig(settings);
var vsockAddress = settings.GetVsockAddress();

GrpcChannel channel;
if (vsockAddress is not null)
{
ServiceConfig = new ServiceConfig { MethodConfigs = { methodConfig } }
});
channel = Vsock.CreateChannel(vsockAddress, serviceConfig);
}
else
{
channel = GrpcChannel.ForAddress(settings.Endpoint, new() { ServiceConfig = serviceConfig });
}

_channel?.Dispose();
_channel = channel;
Expand Down
2 changes: 2 additions & 0 deletions src/Plugins/SignClient/SignClient.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@
<PackageReference Include="Grpc.Net.Client" Version="2.70.0" />
<!-- NOTE: Need install rosetta2 on macOS ARM64 -->
<PackageReference Include="Grpc.Tools" Version="2.70.0" PrivateAssets="all" />

<PackageReference Include="Ookii.VmSockets" Version="1.0.0" />
</ItemGroup>

<ItemGroup>
Expand Down
3 changes: 2 additions & 1 deletion src/Plugins/SignClient/SignClient.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
{
"PluginConfiguration": {
"Name": "SignClient",
"Endpoint": "http://127.0.0.1:9991"
"EndpointType": "tcp", // "tcp" or "vsock"
"Endpoint": "http://127.0.0.1:9991" // tcp: "http://127.0.0.1:port", vsock: "http://contextId:port"
}
}
83 changes: 83 additions & 0 deletions src/Plugins/SignClient/Vsock.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
// Copyright (C) 2015-2025 The Neo Project.
//
// Vsock.cs file belongs to the neo project and is free
// software distributed under the MIT software license, see the
// accompanying file LICENSE in the main directory of the
// repository or http://www.opensource.org/licenses/mit-license.php
// for more details.
//
// Redistribution and use in source and binary forms with or without
// modifications are permitted.

using Grpc.Net.Client;
using Grpc.Net.Client.Configuration;
using Ookii.VmSockets;
using System.Net.Sockets;

namespace Neo.Plugins.SignClient
{
/// <summary>
/// The address of the vsock address.
/// </summary>
public record VsockAddress(int ContextId, int Port);

/// <summary>
/// Grpc adapter for VSock. Only supported on Linux.
/// This is for the SignClient plugin to connect to the AWS Nitro Enclave.
/// </summary>
public class Vsock
{
private readonly VSockEndPoint _endpoint;

/// <summary>
/// Initializes a new instance of the <see cref="Vsock"/> class.
/// </summary>
/// <param name="address">The vsock address.</param>
public Vsock(VsockAddress address)
{
if (!OperatingSystem.IsLinux()) throw new PlatformNotSupportedException("Vsock is only supported on Linux.");

_endpoint = new VSockEndPoint(address.ContextId, address.Port);
}

internal async ValueTask<Stream> ConnectAsync(SocketsHttpConnectionContext context, CancellationToken cancellationToken)
{
if (!OperatingSystem.IsLinux()) throw new PlatformNotSupportedException("Vsock is only supported on Linux.");

var socket = VSock.Create(SocketType.Stream);
try
{
// Have to use `Task.Run` with `Connect` to avoid some compatibility issues(if use ConnectAsync).
await Task.Run(() => socket.Connect(_endpoint), cancellationToken);
return new NetworkStream(socket, true);
}
catch
{
socket.Dispose();
throw;
}
}

/// <summary>
/// Creates a Grpc channel for the vsock endpoint.
/// </summary>
/// <param name="address">The vsock address.</param>
/// <param name="serviceConfig">The Grpc service config.</param>
/// <returns>The Grpc channel.</returns>
public static GrpcChannel CreateChannel(VsockAddress address, ServiceConfig serviceConfig)
{
var vsock = new Vsock(address);
var socketsHttpHandler = new SocketsHttpHandler
{
ConnectCallback = vsock.ConnectAsync,
};

var addressPlaceholder = $"http://127.0.0.1:{address.Port}"; // just a placeholder
return GrpcChannel.ForAddress(addressPlaceholder, new GrpcChannelOptions
{
HttpHandler = socketsHttpHandler,
ServiceConfig = serviceConfig,
});
}
}
}
12 changes: 10 additions & 2 deletions tests/Neo.Plugins.SignClient.Tests/UT_SignClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -44,15 +44,23 @@ public class UT_SignClient

private static SignClient NewClient(Block? block, ExtensiblePayload? payload)
{
// for test sign service, set SIGN_SERVICE_ENDPOINT env
// When test sepcific endpoint, set SIGN_SERVICE_ENDPOINT_TYPE and set SIGN_SERVICE_ENDPOINT
// For example:
// export SIGN_SERVICE_ENDPOINT=http://127.0.0.1:9991
// export SIGN_SERVICE_ENDPOINT_TYPE=tcp
// or
// export SIGN_SERVICE_ENDPOINT=http://2345:9991
// export SIGN_SERVICE_ENDPOINT_TYPE=vsock
var endpoint = Environment.GetEnvironmentVariable("SIGN_SERVICE_ENDPOINT");
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we make this environment variable have a prefix of NEO_?

Copy link
Contributor Author

@Wi1l-B0t Wi1l-B0t Jun 23, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's trivial, I think.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Isnt this plugin related to neo functionality?

var endpointType = Environment.GetEnvironmentVariable("SIGN_SERVICE_ENDPOINT_TYPE");
if (endpoint is not null)
{
var section = new ConfigurationBuilder()
.AddInMemoryCollection(new Dictionary<string, string?>
{
[Settings.SectionName + ":Name"] = "SignClient",
[Settings.SectionName + ":Endpoint"] = endpoint
[Settings.SectionName + ":Endpoint"] = endpoint,
[Settings.SectionName + ":EndpointType"] = endpointType ?? Settings.EndpointTcp
})
.Build()
.GetSection(Settings.SectionName);
Expand Down
71 changes: 71 additions & 0 deletions tests/Neo.Plugins.SignClient.Tests/UT_Vsock.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
// Copyright (C) 2015-2025 The Neo Project.
//
// UT_Vsock.cs file belongs to the neo project and is free
// software distributed under the MIT software license, see the
// accompanying file LICENSE in the main directory of the
// repository or http://www.opensource.org/licenses/mit-license.php
// for more details.
//
// Redistribution and use in source and binary forms with or without
// modifications are permitted.

using Microsoft.Extensions.Configuration;
using Microsoft.VisualStudio.TestTools.UnitTesting;

namespace Neo.Plugins.SignClient.Tests
{
[TestClass]
public class UT_Vsock
{
[TestMethod]
public void TestGetVsockAddress()
{
var address = new VsockAddress(1, 9991);
var section = new ConfigurationBuilder()
.AddInMemoryCollection(new Dictionary<string, string?>
{
["PluginConfiguration:EndpointType"] = Settings.EndpointVsock,
["PluginConfiguration:Endpoint"] = $"http://{address.ContextId}:{address.Port}"
})
.Build()
.GetSection("PluginConfiguration");

var settings = new Settings(section);
Assert.AreEqual(address, settings.GetVsockAddress());

section = new ConfigurationBuilder()
.AddInMemoryCollection(new Dictionary<string, string?>
{
["PluginConfiguration:Endpoint"] = "http://127.0.0.1:9991",
["PluginConfiguration:EndpointType"] = Settings.EndpointTcp
})
.Build()
.GetSection("PluginConfiguration");
Assert.IsNull(new Settings(section).GetVsockAddress());
}

[TestMethod]
public void TestInvalidEndpoint()
{
var section = new ConfigurationBuilder()
.AddInMemoryCollection(new Dictionary<string, string?>
{
["PluginConfiguration:EndpointType"] = Settings.EndpointVsock,
["PluginConfiguration:Endpoint"] = "http://127.0.0.1:9991"
})
.Build()
.GetSection("PluginConfiguration");
Assert.ThrowsExactly<FormatException>(() => _ = new Settings(section).GetVsockAddress());

section = new ConfigurationBuilder()
.AddInMemoryCollection(new Dictionary<string, string?>
{
["PluginConfiguration:EndpointType"] = "xyz",
["PluginConfiguration:Endpoint"] = "http://127.0.0.1:9991"
})
.Build()
.GetSection("PluginConfiguration");
Assert.ThrowsExactly<FormatException>(() => _ = new Settings(section).GetVsockAddress());
}
}
}