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:
+25
-22
@@ -1,5 +1,5 @@
|
|||||||
|
|
||||||
using System.Reflection;
|
using System.Reflection;
|
||||||
using System.Text.Json.Serialization;
|
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
using Microsoft.AspNetCore.SignalR;
|
using Microsoft.AspNetCore.SignalR;
|
||||||
|
|
||||||
@@ -10,14 +10,6 @@ using Microsoft.AspNetCore.SignalR;
|
|||||||
WebApplicationBuilder builder = WebApplication.CreateSlimBuilder(args);
|
WebApplicationBuilder builder = WebApplication.CreateSlimBuilder(args);
|
||||||
|
|
||||||
builder.Services.AddSignalR();
|
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 =>
|
builder.Services.AddCors(options =>
|
||||||
{
|
{
|
||||||
options.AddDefaultPolicy(policy =>
|
options.AddDefaultPolicy(policy =>
|
||||||
@@ -39,22 +31,36 @@ app.MapHub<WsHub>("/ws", options =>
|
|||||||
|
|
||||||
app.MapPost("/send",
|
app.MapPost("/send",
|
||||||
static async (
|
static async (
|
||||||
[FromServices] IHubContext<WsHub> hubContext, [FromServices] ILogger<Program> logger,
|
[FromServices] IHubContext<WsHub> hubContext,
|
||||||
[FromQuery] string? id, [FromBody] string? data
|
[FromServices] ILogger<Program> logger,
|
||||||
|
HttpContext context
|
||||||
) =>
|
) =>
|
||||||
{
|
{
|
||||||
if (string.IsNullOrWhiteSpace(id) || id.Length > 64)
|
string? id = context.Request.Query["id"].FirstOrDefault();
|
||||||
return Results.BadRequest("Connection ID is required and must be at most 64 characters long.");
|
|
||||||
|
// Id validation
|
||||||
|
if (string.IsNullOrEmpty(id) || id.Length > 32)
|
||||||
|
return Results.StatusCode(StatusCodes.Status400BadRequest);
|
||||||
|
|
||||||
foreach (char c in id)
|
foreach (char c in id)
|
||||||
if (!char.IsLetterOrDigit(c) && c != '-' && c != '_')
|
if (!char.IsAsciiLetterOrDigit(c) && c is not ('-' or '_'))
|
||||||
return Results.BadRequest("Connection ID contains invalid characters.");
|
return Results.StatusCode(StatusCodes.Status400BadRequest);
|
||||||
|
|
||||||
if (string.IsNullOrWhiteSpace(data) || data.Length > 66_560)
|
// Content validation
|
||||||
return Results.BadRequest("Body is required and must be at most 66,560 characters long.");
|
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);
|
if (context.Request.ContentLength is < 1 or > 66_560)
|
||||||
await hubContext.Clients.Client(id).SendAsync("ReceiveData", data);
|
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();
|
return Results.Ok();
|
||||||
}
|
}
|
||||||
@@ -63,6 +69,3 @@ app.MapPost("/send",
|
|||||||
app.Run();
|
app.Run();
|
||||||
|
|
||||||
class WsHub : Hub { }
|
class WsHub : Hub { }
|
||||||
|
|
||||||
[JsonSerializable(typeof(string))]
|
|
||||||
internal partial class AppJsonSerializerContext : JsonSerializerContext { }
|
|
||||||
|
|||||||
@@ -34,12 +34,15 @@ sequenceDiagram
|
|||||||
- **SignalR**: `/ws` - WebSocket endpoint for real-time communication.
|
- **SignalR**: `/ws` - WebSocket endpoint for real-time communication.
|
||||||
- **POST**: `/send?id={connectionId}` - HTTP POST endpoint for sending data to the receiver.
|
- **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
|
### 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.
|
- 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
|
## Related papers
|
||||||
|
|
||||||
|
|||||||
@@ -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"
|
||||||
Reference in New Issue
Block a user