This commit is contained in:
2026-08-04 06:17:32 +03:00
parent 53f9819d40
commit 2028db671f
19 changed files with 159 additions and 45 deletions
+5 -2
View File
@@ -14,12 +14,15 @@ internal class Program
private static async Task Main(string[] args) private static async Task Main(string[] args)
{ {
var epPort = ushort.Parse(
Environment.GetEnvironmentVariable("ESIUR_PORT")
?? throw new InvalidOperationException("Set ESIUR_PORT before starting the example."));
var wh = new Warehouse(); var wh = new Warehouse();
// Create a store to keep objects. // Create a store to keep objects.
var system = await wh.Put("sys", new MemoryStore()); var system = await wh.Put("sys", new MemoryStore());
// Create a distibuted server // Create a distibuted server
var esiurServer = await wh.Put("sys/server", new EpServer()); var esiurServer = await wh.Put("sys/server", new EpServer() { Port = epPort });
// Add your object to the store // Add your object to the store
var service = await wh.Put("sys/demo", new Demo()); var service = await wh.Put("sys/demo", new Demo());
@@ -86,7 +89,7 @@ internal class Program
// Start your server // Start your server
await wh.Open(); await wh.Open();
Console.WriteLine("Running on http://localhost:8888"); Console.WriteLine($"Running on http://localhost:8888/?epPort={epPort}");
} }
} }
+4 -1
View File
@@ -1,7 +1,10 @@
async function init() { async function init() {
try { try {
const epPort = new URLSearchParams(window.location.search).get("epPort");
if (!epPort)
throw new Error("Open this example with the application-defined epPort query parameter.");
connection = await wh.get(`ep://${window.location.hostname}`, { connection = await wh.get(`ep://${window.location.hostname}:${epPort}`, {
autoReconnect: true autoReconnect: true
}); });
@@ -217,7 +217,7 @@ internal sealed class EsiurWebSocketEndpoint
{ {
endPoint = null!; endPoint = null!;
if (address is null if (address is null
|| port is <= IPEndPoint.MinPort or > IPEndPoint.MaxPort || port is < IPEndPoint.MinPort or > IPEndPoint.MaxPort
|| address.Equals(IPAddress.Any) || address.Equals(IPAddress.Any)
|| address.Equals(IPAddress.IPv6Any) || address.Equals(IPAddress.IPv6Any)
|| address.Equals(IPAddress.None) || address.Equals(IPAddress.None)
+2 -2
View File
@@ -260,10 +260,10 @@ using Esiur.Resource;
var clientWarehouse = new Warehouse(); var clientWarehouse = new Warehouse();
var service = await clientWarehouse.Get<IResource>( var service = await clientWarehouse.Get<IResource>(
"ep://api.example.com/sys/service", "ep://api.example.com:443/sys/service",
new EpConnectionContext new EpConnectionContext
{ {
WebSocketUri = new Uri("wss://api.example.com/esiur"), WebSocketUri = new Uri("wss://api.example.com:443/esiur"),
// Add the authentication mode, identity, and protocol required by the server. // Add the authentication mode, identity, and protocol required by the server.
}); });
``` ```
+1 -1
View File
@@ -5,7 +5,7 @@
<Copyright>Ahmed Kh. Zamil</Copyright> <Copyright>Ahmed Kh. Zamil</Copyright>
<PackageProjectUrl>https://www.esiur.com</PackageProjectUrl> <PackageProjectUrl>https://www.esiur.com</PackageProjectUrl>
<GeneratePackageOnBuild>true</GeneratePackageOnBuild> <GeneratePackageOnBuild>true</GeneratePackageOnBuild>
<Version>3.0.0</Version> <Version>3.0.1</Version>
<RepositoryUrl>https://github.com/esiur/esiur-dotnet</RepositoryUrl> <RepositoryUrl>https://github.com/esiur/esiur-dotnet</RepositoryUrl>
<Authors>Ahmed Kh. Zamil</Authors> <Authors>Ahmed Kh. Zamil</Authors>
<AssemblyVersion></AssemblyVersion> <AssemblyVersion></AssemblyVersion>
+9 -3
View File
@@ -3143,10 +3143,16 @@ public partial class EpConnection : NetworkConnection, IStore
return new AsyncReply<bool>(true); return new AsyncReply<bool>(true);
var host = Instance.Name.Split(':'); if (!Uri.TryCreate($"ep://{Instance.Name}", UriKind.Absolute, out var endpoint)
|| string.IsNullOrWhiteSpace(endpoint.Host)
|| endpoint.Port <= 0
|| endpoint.Port > ushort.MaxValue)
throw new FormatException(
"EP endpoints must include an explicit port (for example, ep://host:port)."
);
var address = host[0]; var address = endpoint.Host;
var port = host.Length > 1 ? ushort.Parse(host[1]) : (ushort)10518; var port = checked((ushort)endpoint.Port);
// assign domain from hostname if not provided // assign domain from hostname if not provided
if (context is EpConnectionContext epContext) if (context is EpConnectionContext epContext)
+4 -2
View File
@@ -131,12 +131,14 @@ public class EpServer : NetworkServer<EpConnection>, IResource
set; set;
} }
//[Attribute] /// <summary>
/// Application-supplied native TCP port. Zero requests an ephemeral port from the OS.
/// </summary>
public ushort Port public ushort Port
{ {
get; get;
set; set;
} = 10518; }
/// <summary> /// <summary>
/// Controls whether warehouse initialization opens Esiur's native TCP listener. /// Controls whether warehouse initialization opens Esiur's native TCP listener.
+16 -4
View File
@@ -93,8 +93,13 @@ Add an `EpServer` to expose the resource through the Esiur EP protocol. Anonymou
```C# ```C#
var epPort = ushort.Parse(
Environment.GetEnvironmentVariable("ESIUR_PORT")
?? throw new InvalidOperationException("Set ESIUR_PORT."));
await warehouse.Put("sys/server", new EpServer await warehouse.Put("sys/server", new EpServer
{ {
Port = epPort,
AllowUnauthorizedAccess = true, // Development only. AllowUnauthorizedAccess = true, // Development only.
}); });
``` ```
@@ -113,11 +118,15 @@ To sum up
>using Esiur.Resource; >using Esiur.Resource;
>using Esiur.Stores; >using Esiur.Stores;
> >
>var epPort = ushort.Parse(
> Environment.GetEnvironmentVariable("ESIUR_PORT")
> ?? throw new InvalidOperationException("Set ESIUR_PORT."));
>var warehouse = new Warehouse(); >var warehouse = new Warehouse();
>await warehouse.Put("sys", new MemoryStore()); >await warehouse.Put("sys", new MemoryStore());
>await warehouse.Put("sys/hello", new HelloResource()); >await warehouse.Put("sys/hello", new HelloResource());
>await warehouse.Put("sys/server", new EpServer >await warehouse.Put("sys/server", new EpServer
>{ >{
> Port = epPort,
> AllowUnauthorizedAccess = true, // Development only. > AllowUnauthorizedAccess = true, // Development only.
>}); >});
>await warehouse.Open(); >await warehouse.Open();
@@ -129,7 +138,8 @@ To access our resource remotely, we need to use it's full path including the pro
```C# ```C#
var warehouse = new Warehouse(); var warehouse = new Warehouse();
dynamic res = await warehouse.Get<IResource>("ep://localhost/sys/hello"); var epPort = ushort.Parse(Environment.GetEnvironmentVariable("ESIUR_PORT")!);
dynamic res = await warehouse.Get<IResource>($"ep://localhost:{epPort}/sys/hello");
``` ```
Now we can invoke the exported functions and read/write properties; Now we can invoke the exported functions and read/write properties;
@@ -147,7 +157,8 @@ Summing up
>using Esiur.Resource; >using Esiur.Resource;
> >
>var warehouse = new Warehouse(); >var warehouse = new Warehouse();
>dynamic res = await warehouse.Get<IResource>("ep://localhost/sys/hello"); >var epPort = ushort.Parse(Environment.GetEnvironmentVariable("ESIUR_PORT")!);
>dynamic res = await warehouse.Get<IResource>($"ep://localhost:{epPort}/sys/hello");
> >
>var reply = await res.SayHi("Hi, I'm calling you from dotnet"); >var reply = await res.SayHi("Hi, I'm calling you from dotnet");
> >
@@ -165,7 +176,7 @@ Esiur has a self describing feature which comes with every language it supports,
After installing the Esiur NuGet package, a new command named ***Get-Types*** is added to the Visual Studio Package Manager Console. It generates client-side classes for robust static typing. After installing the Esiur NuGet package, a new command named ***Get-Types*** is added to the Visual Studio Package Manager Console. It generates client-side classes for robust static typing.
```ps ```ps
Get-Types ep://localhost/sys/hello Get-Types "ep://localhost:$env:ESIUR_PORT/sys/hello"
``` ```
This will generate and add wrappers for all types needed by our resource. This will generate and add wrappers for all types needed by our resource.
@@ -173,7 +184,8 @@ This will generate and add wrappers for all types needed by our resource.
Allowing us to use Allowing us to use
```C# ```C#
var warehouse = new Warehouse(); var warehouse = new Warehouse();
var res = await warehouse.Get<MyResource>("ep://localhost/sys/hello"); var epPort = ushort.Parse(Environment.GetEnvironmentVariable("ESIUR_PORT")!);
var res = await warehouse.Get<MyResource>($"ep://localhost:{epPort}/sys/hello");
var reply = await res.SayHi("Static typing is better"); var reply = await res.SayHi("Static typing is better");
Console.WriteLine(reply); Console.WriteLine(reply);
``` ```
+12 -5
View File
@@ -56,7 +56,7 @@ dotnet add package Esiur.AspNetCore --version 3.0.0
For the standalone runtime or a client: For the standalone runtime or a client:
```shell ```shell
dotnet add package Esiur --version 3.0.0 dotnet add package Esiur --version 3.0.1
``` ```
## Define a resource ## Define a resource
@@ -157,7 +157,7 @@ using Esiur.Resource;
var client = new Warehouse(); var client = new Warehouse();
dynamic counter = await client.Get<IResource>( dynamic counter = await client.Get<IResource>(
"ep://localhost/sys/counter", "ep://localhost:8080/sys/counter",
new EpConnectionContext new EpConnectionContext
{ {
WebSocketUri = new Uri("ws://localhost:8080/esiur"), WebSocketUri = new Uri("ws://localhost:8080/esiur"),
@@ -176,8 +176,12 @@ the case-sensitive `EP` WebSocket subprotocol.
For native TCP, omit `WebSocketUri` and include the EP port in the logical URL: For native TCP, omit `WebSocketUri` and include the EP port in the logical URL:
```csharp ```csharp
var epPort = ushort.Parse(
Environment.GetEnvironmentVariable("ESIUR_PORT")
?? throw new InvalidOperationException("Set ESIUR_PORT."));
dynamic counter = await client.Get<IResource>( dynamic counter = await client.Get<IResource>(
"ep://localhost:10518/sys/counter"); $"ep://localhost:{epPort}/sys/counter");
``` ```
## Standalone hosting ## Standalone hosting
@@ -191,13 +195,16 @@ using Esiur.Protocol;
using Esiur.Resource; using Esiur.Resource;
using Esiur.Stores; using Esiur.Stores;
var epPort = ushort.Parse(
Environment.GetEnvironmentVariable("ESIUR_PORT")
?? throw new InvalidOperationException("Set ESIUR_PORT."));
var warehouse = new Warehouse(); var warehouse = new Warehouse();
await warehouse.Put("sys", new MemoryStore()); await warehouse.Put("sys", new MemoryStore());
await warehouse.Put("sys/counter", new CounterResource()); await warehouse.Put("sys/counter", new CounterResource());
await warehouse.Put("sys/server", new EpServer await warehouse.Put("sys/server", new EpServer
{ {
Port = 10518, Port = epPort,
AllowUnauthorizedAccess = true, // Development only. AllowUnauthorizedAccess = true, // Development only.
}); });
@@ -344,7 +351,7 @@ typed models. Install the v3 CLI as a .NET tool:
```shell ```shell
dotnet tool install --global Esiur.CLI --version 3.0.0 dotnet tool install --global Esiur.CLI --version 3.0.0
esiur get-template ep://localhost:10518/sys/counter --dir Generated esiur get-template "ep://localhost:${ESIUR_PORT}/sys/counter" --dir Generated
``` ```
Use `--async-setters` to generate asynchronous property setters. The CLI also Use `--async-setters` to generate asynchronous property setters. The CLI also
+1 -1
View File
@@ -20,7 +20,7 @@ public sealed class CommandTests
{ {
["production"] = new ConnectionProfile ["production"] = new ConnectionProfile
{ {
Name = "production", Endpoint = "ep://host", Name = "production", Endpoint = "ep://host:65535",
}, },
}, },
}, default); }, default);
+7 -6
View File
@@ -8,13 +8,14 @@ namespace Esiur.CLI.Tests;
public sealed class ConfigurationTests public sealed class ConfigurationTests
{ {
[Theory] [Theory]
[InlineData("ep://localhost", "ep://localhost")] [InlineData("ep://localhost:65535", "ep://localhost:65535")]
[InlineData("ep://example.test:9000/sys/service", "ep://example.test:9000")] [InlineData("ep://example.test:9000/sys/service", "ep://example.test:9000")]
public void EndpointParserExtractsConnectionEndpoint(string value, string expected) => public void EndpointParserExtractsConnectionEndpoint(string value, string expected) =>
Assert.Equal(expected, EndpointParser.ConnectionEndpoint(value)); Assert.Equal(expected, EndpointParser.ConnectionEndpoint(value));
[Theory] [Theory]
[InlineData("http://localhost")] [InlineData("http://localhost")]
[InlineData("ep://localhost")]
[InlineData("ep:///missing-host")] [InlineData("ep:///missing-host")]
[InlineData("not-an-endpoint")] [InlineData("not-an-endpoint")]
public void EndpointParserRejectsInvalidEndpoints(string value) => public void EndpointParserRejectsInvalidEndpoints(string value) =>
@@ -43,7 +44,7 @@ public sealed class ConfigurationTests
["production"] = new ConnectionProfile ["production"] = new ConnectionProfile
{ {
Name = "production", Name = "production",
Endpoint = "ep://host", Endpoint = "ep://host:65535",
Provider = "password", Provider = "password",
Identity = "ahmed", Identity = "ahmed",
}, },
@@ -55,7 +56,7 @@ public sealed class ConfigurationTests
Assert.DoesNotContain("secret", text, StringComparison.OrdinalIgnoreCase); Assert.DoesNotContain("secret", text, StringComparison.OrdinalIgnoreCase);
var loaded = await store.LoadAsync(default); var loaded = await store.LoadAsync(default);
Assert.Equal("production", loaded.DefaultProfile); Assert.Equal("production", loaded.DefaultProfile);
Assert.Equal("ep://host", loaded.Profiles["PRODUCTION"].Endpoint); Assert.Equal("ep://host:65535", loaded.Profiles["PRODUCTION"].Endpoint);
} }
finally { directory.Delete(true); } finally { directory.Delete(true); }
} }
@@ -71,14 +72,14 @@ public sealed class ConfigurationTests
{ {
["saved"] = new ConnectionProfile ["saved"] = new ConnectionProfile
{ {
Name = "saved", Endpoint = "ep://saved", OutputFormat = "raw", Name = "saved", Endpoint = "ep://saved:65535", OutputFormat = "raw",
Identity = "stored", Identity = "stored",
}, },
}, },
}; };
var result = ConfigurationResolver.Resolve(configuration, var result = ConfigurationResolver.Resolve(configuration,
new GlobalOptions("saved", "ep://explicit", null, "explicit", "json", TimeSpan.FromSeconds(4), false, false)); new GlobalOptions("saved", "ep://explicit:65535", null, "explicit", "json", TimeSpan.FromSeconds(4), false, false));
Assert.Equal("ep://explicit", result.Endpoint); Assert.Equal("ep://explicit:65535", result.Endpoint);
Assert.Equal("explicit", result.Identity); Assert.Equal("explicit", result.Identity);
Assert.Equal("json", result.OutputFormat); Assert.Equal("json", result.OutputFormat);
Assert.Equal(TimeSpan.FromSeconds(4), result.Timeout); Assert.Equal(TimeSpan.FromSeconds(4), result.Timeout);
+32 -3
View File
@@ -361,6 +361,31 @@ public sealed class AspNetCoreIntegrationTests
cleanupCancellation.Token); cleanupCancellation.Token);
} }
[Fact]
public async Task FrameworkWebSocket_AcceptsProxyEndpointWithUnspecifiedRemotePort()
{
await using var host = await StartApplicationAsync(
configureApplication: application => application.Use(
async (context, next) =>
{
context.Connection.RemotePort = IPEndPoint.MinPort;
await next(context);
}));
using var cancellation = new CancellationTokenSource(TestTimeout);
using var socket = new ClientWebSocket();
socket.Options.AddSubProtocol(FrameworkWebSocket.SubProtocol);
await socket.ConnectAsync(host.WebSocketAddress, cancellation.Token);
Assert.Equal(WebSocketState.Open, socket.State);
Assert.Equal(FrameworkWebSocket.SubProtocol, socket.SubProtocol);
await WaitUntilAsync(
() => host.Server.Connections.Count == 1,
cancellation.Token);
socket.Abort();
}
[Fact] [Fact]
public async Task HostShutdown_CancelsWebSocketAndCleansUpAdmission() public async Task HostShutdown_CancelsWebSocketAndCleansUpAdmission()
{ {
@@ -640,7 +665,8 @@ public sealed class AspNetCoreIntegrationTests
private static WebApplication BuildApplication( private static WebApplication BuildApplication(
Action<EsiurBuilder>? configureEsiur = null, Action<EsiurBuilder>? configureEsiur = null,
Action<EpServer>? configureServer = null, Action<EpServer>? configureServer = null,
Action<WarehouseConfiguration>? configureWarehouse = null) Action<WarehouseConfiguration>? configureWarehouse = null,
Action<WebApplication>? configureApplication = null)
{ {
var builder = WebApplication.CreateBuilder(new WebApplicationOptions var builder = WebApplication.CreateBuilder(new WebApplicationOptions
{ {
@@ -665,6 +691,7 @@ public sealed class AspNetCoreIntegrationTests
var application = builder.Build(); var application = builder.Build();
application.UseWebSockets(); application.UseWebSockets();
configureApplication?.Invoke(application);
application.MapGet("/health", () => Results.Text("healthy")); application.MapGet("/health", () => Results.Text("healthy"));
application.MapEsiur("/esiur"); application.MapEsiur("/esiur");
return application; return application;
@@ -673,7 +700,8 @@ public sealed class AspNetCoreIntegrationTests
private static async Task<TestApplication> StartApplicationAsync( private static async Task<TestApplication> StartApplicationAsync(
Action<EpServer>? configureServer = null, Action<EpServer>? configureServer = null,
Action<WarehouseConfiguration>? configureWarehouse = null, Action<WarehouseConfiguration>? configureWarehouse = null,
Action<EsiurBuilder>? configureEsiur = null) Action<EsiurBuilder>? configureEsiur = null,
Action<WebApplication>? configureApplication = null)
{ {
var application = BuildApplication( var application = BuildApplication(
esiur => esiur =>
@@ -682,7 +710,8 @@ public sealed class AspNetCoreIntegrationTests
configureEsiur?.Invoke(esiur); configureEsiur?.Invoke(esiur);
}, },
configureServer, configureServer,
configureWarehouse); configureWarehouse,
configureApplication);
using var cancellation = new CancellationTokenSource(TestTimeout); using var cancellation = new CancellationTokenSource(TestTimeout);
await application.StartAsync(cancellation.Token); await application.StartAsync(cancellation.Token);
+5 -5
View File
@@ -27,7 +27,7 @@ public sealed class EpConnectionReconnectTests
var open = connection.Connect( var open = connection.Connect(
hostname: "localhost", hostname: "localhost",
port: 10518, port: IPEndPoint.MaxPort,
domain: "test"); domain: "test");
var completed = await Task.WhenAny( var completed = await Task.WhenAny(
@@ -62,7 +62,7 @@ public sealed class EpConnectionReconnectTests
var open = connection.Connect( var open = connection.Connect(
hostname: "localhost", hostname: "localhost",
port: 10518, port: IPEndPoint.MaxPort,
domain: "test"); domain: "test");
await delayedSocket.ConnectInvoked.WaitAsync(TimeSpan.FromSeconds(2)); await delayedSocket.ConnectInvoked.WaitAsync(TimeSpan.FromSeconds(2));
@@ -95,7 +95,7 @@ public sealed class EpConnectionReconnectTests
var open = connection.Connect( var open = connection.Connect(
hostname: "localhost", hostname: "localhost",
port: 10518, port: IPEndPoint.MaxPort,
domain: "test"); domain: "test");
connection.AutoReconnect = false; connection.AutoReconnect = false;
@@ -133,7 +133,7 @@ public sealed class EpConnectionReconnectTests
_ = connection.Connect( _ = connection.Connect(
initialSocket, initialSocket,
hostname: "localhost", hostname: "localhost",
port: 10518, port: IPEndPoint.MaxPort,
domain: "test"); domain: "test");
initialSocket.Disconnect(); initialSocket.Disconnect();
connection.AutoReconnect = false; connection.AutoReconnect = false;
@@ -160,7 +160,7 @@ public sealed class EpConnectionReconnectTests
public SocketState State => state; public SocketState State => state;
public INetworkReceiver<ISocket> Receiver { get; set; } = null!; public INetworkReceiver<ISocket> Receiver { get; set; } = null!;
public IPEndPoint RemoteEndPoint { get; } = public IPEndPoint RemoteEndPoint { get; } =
new(IPAddress.Loopback, 10518); new(IPAddress.Loopback, IPEndPoint.MaxPort);
public IPEndPoint LocalEndPoint { get; } = public IPEndPoint LocalEndPoint { get; } =
new(IPAddress.Loopback, 50000); new(IPAddress.Loopback, 50000);
public int ConnectCount { get; private set; } public int ConnectCount { get; private set; }
+22
View File
@@ -0,0 +1,22 @@
using Esiur.Protocol;
using Esiur.Resource;
namespace Esiur.Tests.Unit;
public sealed class EpPortTests
{
[Fact]
public void ServerDoesNotAssignAProtocolPort()
=> Assert.Equal(0, new EpServer().Port);
[Fact]
public async Task ClientEndpointWithoutPortIsRejected()
{
var warehouse = new Warehouse();
var exception = await Assert.ThrowsAsync<FormatException>(async () =>
await warehouse.Get<EpConnection>("ep://localhost/sys/resource"));
Assert.Contains("explicit port", exception.Message, StringComparison.OrdinalIgnoreCase);
}
}
+2 -2
View File
@@ -251,7 +251,7 @@ public class PeerConnectionLimitTests
connection, connection,
null); null);
_ = connection.Connect(socket, "example.test", 10518, "example.test"); _ = connection.Connect(socket, "example.test", IPEndPoint.MaxPort, "example.test");
var deadline = DateTime.UtcNow + TimeSpan.FromSeconds(3); var deadline = DateTime.UtcNow + TimeSpan.FromSeconds(3);
while (socket.State != SocketState.Closed && DateTime.UtcNow < deadline) while (socket.State != SocketState.Closed && DateTime.UtcNow < deadline)
@@ -269,7 +269,7 @@ public class PeerConnectionLimitTests
public SocketState State { get; private set; } = SocketState.Established; public SocketState State { get; private set; } = SocketState.Established;
public INetworkReceiver<ISocket> Receiver { get; set; } = null!; public INetworkReceiver<ISocket> Receiver { get; set; } = null!;
public IPEndPoint RemoteEndPoint { get; } public IPEndPoint RemoteEndPoint { get; }
public IPEndPoint LocalEndPoint { get; } = new(IPAddress.Loopback, 10518); public IPEndPoint LocalEndPoint { get; } = new(IPAddress.Loopback, IPEndPoint.MaxPort);
public TestSocket(IPAddress address, int port) public TestSocket(IPAddress address, int port)
=> RemoteEndPoint = new IPEndPoint(address, port); => RemoteEndPoint = new IPEndPoint(address, port);
+1 -1
View File
@@ -382,7 +382,7 @@ public class SocketProtocolSecurityTests
public SocketState State { get; private set; } = SocketState.Listening; public SocketState State { get; private set; } = SocketState.Listening;
public INetworkReceiver<ISocket> Receiver { get; set; } = null!; public INetworkReceiver<ISocket> Receiver { get; set; } = null!;
public IPEndPoint RemoteEndPoint => null!; public IPEndPoint RemoteEndPoint => null!;
public IPEndPoint LocalEndPoint { get; } = new(IPAddress.Loopback, 10518); public IPEndPoint LocalEndPoint { get; } = new(IPAddress.Loopback, IPEndPoint.MaxPort);
public ISocket Accept() public ISocket Accept()
{ {
@@ -74,8 +74,37 @@ public static class ConfigurationResolver
if (!Uri.TryCreate(endpoint, UriKind.Absolute, out var uri) if (!Uri.TryCreate(endpoint, UriKind.Absolute, out var uri)
|| !ValidSchemes.Contains(uri.Scheme, StringComparer.OrdinalIgnoreCase) || !ValidSchemes.Contains(uri.Scheme, StringComparer.OrdinalIgnoreCase)
|| string.IsNullOrWhiteSpace(uri.Host) || string.IsNullOrWhiteSpace(uri.Host)
|| !string.IsNullOrEmpty(uri.UserInfo)) || !string.IsNullOrEmpty(uri.UserInfo)
throw new CliException($"Endpoint \"{endpoint}\" is not a valid ep:// or ws:// endpoint.", ExitCodes.InvalidArguments); || !HasExplicitPort(endpoint))
throw new CliException(
$"Endpoint \"{endpoint}\" is not a valid ep:// or ws:// endpoint with an explicit port.",
ExitCodes.InvalidArguments);
}
static bool HasExplicitPort(string endpoint)
{
var schemeEnd = endpoint.IndexOf("://", StringComparison.Ordinal);
if (schemeEnd < 0)
return false;
var authorityStart = schemeEnd + 3;
var authorityEnd = endpoint.IndexOfAny(['/', '?', '#'], authorityStart);
if (authorityEnd < 0)
authorityEnd = endpoint.Length;
var authority = endpoint[authorityStart..authorityEnd];
var at = authority.LastIndexOf('@');
if (at >= 0)
authority = authority[(at + 1)..];
var colon = authority.StartsWith('[')
? authority.IndexOf(']') + 1
: authority.LastIndexOf(':');
return colon > 0
&& colon < authority.Length - 1
&& authority[colon] == ':'
&& ushort.TryParse(authority[(colon + 1)..], out var port)
&& port > 0;
} }
} }
@@ -2,7 +2,7 @@
"profiles": { "profiles": {
"Esiur.CLI": { "Esiur.CLI": {
"commandName": "Project", "commandName": "Project",
"commandLineArgs": "get-template ep://phase.delta.iq/sys/phase --dir c:\\temp\\an" "commandLineArgs": "get-template ep://phase.delta.iq:65535/sys/phase --dir c:\\temp\\an"
} }
} }
} }
+3 -3
View File
@@ -19,13 +19,13 @@ Runtime-specific publishes are configured as self-contained, single-file, and no
Create and verify a password-authenticated profile: Create and verify a password-authenticated profile:
```console ```console
esiur login production ep://host --provider password --identity ahmed esiur login production "ep://host:${ESIUR_PORT}" --provider password --identity ahmed
``` ```
The password prompt does not echo input. For automation, supply it on standard input: The password prompt does not echo input. For automation, supply it on standard input:
```console ```console
printf '%s' "$ESIUR_PASSWORD" | esiur login production ep://host \ printf '%s' "$ESIUR_PASSWORD" | esiur login production "ep://host:${ESIUR_PORT}" \
--provider password --identity ahmed --password-stdin --provider password --identity ahmed --password-stdin
``` ```
@@ -86,7 +86,7 @@ Every operational command accepts a saved profile or a temporary endpoint:
```console ```console
esiur --profile production describe sys/service esiur --profile production describe sys/service
esiur --endpoint ep://host query sys --output json esiur --endpoint "ep://host:${ESIUR_PORT}" query sys --output json
esiur get sys/service Name --timeout 30s esiur get sys/service Name --timeout 30s
``` ```