diff --git a/Program.cs b/Program.cs index 58800ce..52f11d7 100644 --- a/Program.cs +++ b/Program.cs @@ -1,5 +1,5 @@ + using System.Reflection; -using System.Text.Json.Serialization; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.SignalR; @@ -10,14 +10,6 @@ using Microsoft.AspNetCore.SignalR; WebApplicationBuilder builder = WebApplication.CreateSlimBuilder(args); builder.Services.AddSignalR(); - -builder.Services.ConfigureHttpJsonOptions(options => - options.SerializerOptions.TypeInfoResolverChain.Insert(0, AppJsonSerializerContext.Default) -); -builder.Services.Configure(options => - options.PayloadSerializerOptions.TypeInfoResolverChain.Insert(0, AppJsonSerializerContext.Default) -); - builder.Services.AddCors(options => { options.AddDefaultPolicy(policy => @@ -39,22 +31,36 @@ app.MapHub("/ws", options => app.MapPost("/send", static async ( - [FromServices] IHubContext hubContext, [FromServices] ILogger logger, - [FromQuery] string? id, [FromBody] string? data + [FromServices] IHubContext hubContext, + [FromServices] ILogger logger, + HttpContext context ) => { - if (string.IsNullOrWhiteSpace(id) || id.Length > 64) - return Results.BadRequest("Connection ID is required and must be at most 64 characters long."); + string? id = context.Request.Query["id"].FirstOrDefault(); + + // Id validation + if (string.IsNullOrEmpty(id) || id.Length > 32) + return Results.StatusCode(StatusCodes.Status400BadRequest); foreach (char c in id) - if (!char.IsLetterOrDigit(c) && c != '-' && c != '_') - return Results.BadRequest("Connection ID contains invalid characters."); + if (!char.IsAsciiLetterOrDigit(c) && c is not ('-' or '_')) + return Results.StatusCode(StatusCodes.Status400BadRequest); - if (string.IsNullOrWhiteSpace(data) || data.Length > 66_560) - return Results.BadRequest("Body is required and must be at most 66,560 characters long."); + // Content validation + if (context.Request.ContentType is not "text/plain") + return Results.StatusCode(StatusCodes.Status415UnsupportedMediaType); - logger.LogDebug("Received payload for connection '{id}' (package length: {len})", id, data.Length); - await hubContext.Clients.Client(id).SendAsync("ReceiveData", data); + if (context.Request.ContentLength is < 1 or > 66_560) + return Results.StatusCode(StatusCodes.Status400BadRequest); + + // Sending data + if (logger.IsEnabled(LogLevel.Debug)) + logger.LogDebug("Received payload for connection '{id}' (package length: {len} B)", id, context.Request.ContentLength); + + using StreamReader reader = new(context.Request.Body); + string? data = await reader.ReadToEndAsync(); + + await hubContext.Clients.Client(id).SendAsync("OnMessage", data); return Results.Ok(); } @@ -63,6 +69,3 @@ app.MapPost("/send", app.Run(); class WsHub : Hub { } - -[JsonSerializable(typeof(string))] -internal partial class AppJsonSerializerContext : JsonSerializerContext { } diff --git a/README.md b/README.md index 0912a50..074a7d1 100644 --- a/README.md +++ b/README.md @@ -34,12 +34,15 @@ sequenceDiagram - **SignalR**: `/ws` - WebSocket endpoint for real-time communication. - **POST**: `/send?id={connectionId}` - HTTP POST endpoint for sending data to the receiver. -Body of the `/send` endpoint must be of type `Content-Type: application/json`. +> [!NOTE] +> For more details on API implementation, restrictions and responses, see [`Program.cs`](/Program.cs) source file. ### Key points -- The arbitrary channel for `connectionId` tranmission should be as secure as possibe (preferably an offline channel), since posession of `connectionId` can pose a security threat. +- The arbitrary channel for `connectionId` tranmission should be as secure as possibe (preferably an offline channel), since posession of the `connectionId` can pose a security threat. - Connection between Backbone and receiver preferably should be re-established after every transmission to avoid replay attacks. +- Data sent via HTTP POST is stored in memory only until it is delivered to the receiver. If the receiver is not connected, the data will be discarded (the call still will be resolved with HTTP 200 OK). +- Data sent via HTTP POST (regardless whether it's an HTTP or HTTPS) *must be* end-to-end encrypted by the sender. ## Related papers diff --git a/api.http b/api.http new file mode 100644 index 0000000..e26885e --- /dev/null +++ b/api.http @@ -0,0 +1,68 @@ +@Host = http://localHost:8080 + +# Valid request + +POST {{Host}}/send?id=12345 +Content-Type: text/plain + +testdata + +### + +# Invalid request: missing id parameter + +POST {{Host}}/send +Content-Type: text/plain + +testdata + +### + +# Invalid request: empty id parameter + +POST {{Host}}/send?id= +Content-Type: text/plain + +testdata + +### + +# Invalid request: invalid id parameter (only ASCII alphanumeric, '-' and '_' characters allowed) + +POST {{Host}}/send?id=hello+world +Content-Type: text/plain + +testdata + +### + +# Invalid request: too long id parameter (more than 32 characters) + +POST {{Host}}/send?id=0123456789ABCDEF1234567890ABCDEF0 +Content-Type: text/plain + +testdata + +### + +# Invalid request: empty body +POST {{Host}}/send?id=12345 +Content-Type: text/plain + +### + +# Invalid request: unsupported method + +GET {{Host}}/send?id=12345 +Content-Type: text/plain + +testdata + +### + +# Invalid request: incorrect Content-Type + +POST {{Host}}/send?id=12345 +Content-Type: application/json + +"testdata"