1
0
mirror of https://github.com/XFox111/backbone.git synced 2026-04-22 07:17:59 +03:00

feat!: use 'text/plain' content type for /send endpoint + reworked validation + renamed signalr method

This commit is contained in:
2025-12-11 01:21:59 +00:00
parent a27400928a
commit 23f2d7ccb1
3 changed files with 98 additions and 24 deletions
+25 -22
View File
@@ -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<JsonHubProtocolOptions>(options =>
options.PayloadSerializerOptions.TypeInfoResolverChain.Insert(0, AppJsonSerializerContext.Default)
);
builder.Services.AddCors(options =>
{
options.AddDefaultPolicy(policy =>
@@ -39,22 +31,36 @@ app.MapHub<WsHub>("/ws", options =>
app.MapPost("/send",
static async (
[FromServices] IHubContext<WsHub> hubContext, [FromServices] ILogger<Program> logger,
[FromQuery] string? id, [FromBody] string? data
[FromServices] IHubContext<WsHub> hubContext,
[FromServices] ILogger<Program> 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 { }
+5 -2
View File
@@ -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
+68
View File
@@ -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"