diff --git a/src/Thinktecture.Relay.Connector/DependencyInjection/RelayConnectorBuilderExtensions.cs b/src/Thinktecture.Relay.Connector/DependencyInjection/RelayConnectorBuilderExtensions.cs new file mode 100644 index 000000000..bc53bc0e2 --- /dev/null +++ b/src/Thinktecture.Relay.Connector/DependencyInjection/RelayConnectorBuilderExtensions.cs @@ -0,0 +1,55 @@ +using Thinktecture.Relay.Acknowledgement; +using Thinktecture.Relay.Connector.DependencyInjection; +using Thinktecture.Relay.Connector.Targets; +using Thinktecture.Relay.Transport; + +// ReSharper disable once CheckNamespace; (extension methods on IServiceCollection namespace) +namespace Microsoft.Extensions.DependencyInjection; + +/// +/// Extension methods for the . +/// +public static class RelayConnectorBuilderExtensions +{ + /// + /// Adds the . + /// + /// The default target key is "$ping". + /// The . + /// The target key for the . + /// The type of request. + /// The type of response. + /// The type of acknowledge. + /// The . + public static IRelayConnectorBuilder AddPingTarget(this IRelayConnectorBuilder builder, string targetKey = "$ping") + where TRequest : IClientRequest + where TResponse : ITargetResponse, new() + where TAcknowledge : IAcknowledgeRequest + { + builder.AddTarget>(targetKey); + + return builder; + } + + /// + /// Adds the . + /// + /// The default target key is "$echo" and should only be added for debugging purposes. + /// The . + /// The target key for the . + /// The type of request. + /// The type of response. + /// The type of acknowledge. + /// The . + public static IRelayConnectorBuilder AddEchoTarget(this IRelayConnectorBuilder builder, string targetKey = "$echo") + where TRequest : IClientRequest + where TResponse : ITargetResponse, new() + where TAcknowledge : IAcknowledgeRequest + { + builder.AddTarget>(targetKey); + + return builder; + } +} diff --git a/src/Thinktecture.Relay.Connector/Targets/EchoTarget.cs b/src/Thinktecture.Relay.Connector/Targets/EchoTarget.cs new file mode 100644 index 000000000..230ed0128 --- /dev/null +++ b/src/Thinktecture.Relay.Connector/Targets/EchoTarget.cs @@ -0,0 +1,83 @@ +using System; +using System.IO; +using System.Net; +using System.Threading; +using System.Threading.Tasks; +using Thinktecture.Relay.Transport; + +namespace Thinktecture.Relay.Connector.Targets; + +/// +public class EchoTarget : IRelayTargetFunc + where TRequest : IClientRequest + where TResponse : ITargetResponse, new() +{ + private class CopyStream(Stream source, long length) : Stream + { + private long _length = Math.Max(0, length); + + public override void Flush() + => throw new NotImplementedException(); + + public override int Read(byte[] buffer, int offset, int count) + { + if (Position == _length) return 0; + + var read = 0; + + while (read < count && Position < _length) + { + var remaining = Math.Min(_length - Position, count); + var available = source.Read(buffer, offset, (int)remaining); + if (available == 0) + { + source.Position = 0; + } + + offset += available; + read += available; + count -= available; + + Position += available; + } + + return read; + } + + public override long Seek(long offset, SeekOrigin origin) + => throw new NotSupportedException(); + + public override void SetLength(long value) + => _length = value; + + public override void Write(byte[] buffer, int offset, int count) + => throw new NotSupportedException(); + + public override bool CanRead => true; + + public override bool CanSeek => false; + + public override bool CanWrite => false; + + public override long Length => _length; + + public override long Position { get; set; } + } + + /// + public Task HandleAsync(TRequest request, CancellationToken cancellationToken = default) + { + if (!request.HttpMethod.Equals("POST", StringComparison.OrdinalIgnoreCase) || request.BodyContent is null) + return Task.FromResult(request.CreateResponse(HttpStatusCode.NoContent)); + + var result = request.CreateResponse(HttpStatusCode.OK); + + result.BodyContent = int.TryParse(request.Url, out var size) + ? new CopyStream(request.BodyContent, size) + : request.BodyContent; + result.BodySize = result.BodyContent.Length; + result.BodyContent.Position = 0; + + return Task.FromResult(result); + } +} diff --git a/src/Thinktecture.Relay.Connector/Targets/PingTarget.cs b/src/Thinktecture.Relay.Connector/Targets/PingTarget.cs new file mode 100644 index 000000000..f22ddd602 --- /dev/null +++ b/src/Thinktecture.Relay.Connector/Targets/PingTarget.cs @@ -0,0 +1,33 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Net; +using System.Threading; +using System.Threading.Tasks; +using Thinktecture.Relay.Transport; + +namespace Thinktecture.Relay.Connector.Targets; + +/// +public class PingTarget : IRelayTargetFunc + where TRequest : IClientRequest + where TResponse : ITargetResponse, new() +{ + /// + public Task HandleAsync(TRequest request, CancellationToken cancellationToken = default) + { + if (!request.HttpMethod.Equals("GET", StringComparison.OrdinalIgnoreCase)) + return Task.FromResult(request.CreateResponse(HttpStatusCode.NotFound)); + + var result = request.CreateResponse(HttpStatusCode.OK); + + result.HttpHeaders = new Dictionary() + { + { "Content-Type", ["text/plain"] }, + }; + result.BodyContent = new MemoryStream("PONG"u8.ToArray()); + result.BodySize = result.BodyContent.Length; + + return Task.FromResult(result); + } +} diff --git a/src/docker/Thinktecture.Relay.Connector.Docker/Startup.cs b/src/docker/Thinktecture.Relay.Connector.Docker/Startup.cs index bf467410f..dc1ac0d51 100644 --- a/src/docker/Thinktecture.Relay.Connector.Docker/Startup.cs +++ b/src/docker/Thinktecture.Relay.Connector.Docker/Startup.cs @@ -25,6 +25,8 @@ public static void ConfigureServices(HostBuilderContext hostBuilderContext, ISer services .AddRelayConnector(options => configuration.GetSection("RelayConnector").Bind(options)) .AddSignalRConnectorTransport() + .AddPingTarget() + .AddEchoTarget() .AddTarget("inprocfunc") .AddTarget("inprocaction");