313 lines
11 KiB
C#
313 lines
11 KiB
C#
using MessengerApi.Configuration.Model;
|
|
using MessengerApi.Configuration.Model.Persistence;
|
|
using MessengerApi.Configuration.Sources.Environment;
|
|
using MessengerApi.Contracts.Factories;
|
|
using MessengerApi.Contracts.Models.Scoped;
|
|
using MessengerApi.Db;
|
|
using MessengerApi.Factories;
|
|
using MessengerApi.Handlers;
|
|
using MessengerApi.Handlers.Endpoint;
|
|
using MessengerApi.Models.Http;
|
|
using MessengerApi.Models.Scoped;
|
|
using Microsoft.AspNetCore.Authentication;
|
|
using Microsoft.AspNetCore.HttpOverrides;
|
|
using Microsoft.AspNetCore.Mvc;
|
|
using Microsoft.EntityFrameworkCore;
|
|
using System.Net;
|
|
using System.Threading.RateLimiting;
|
|
|
|
namespace MessengerApi.Api
|
|
{
|
|
public class Program
|
|
{
|
|
public static void Main(string[] args)
|
|
{
|
|
MessengerConfiguration configuration = null;
|
|
|
|
try
|
|
{
|
|
configuration = new MessengerConfiguration(new EnvironmentConfigurationSource());
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
Console.WriteLine("Can't load settings.", ex);
|
|
throw;
|
|
}
|
|
|
|
var builder = WebApplication.CreateBuilder(args);
|
|
|
|
builder.Configuration.AddEnvironmentVariables();
|
|
|
|
builder.Services.AddMemoryCache();
|
|
builder.Services.AddSingleton<MessengerConfiguration>(configuration);
|
|
builder.Services.AddSingleton<ILogger>(new Factories.LoggerFactory(configuration).CreateLogger());
|
|
builder.Services.AddSingleton<SendEndpointHandler>();
|
|
builder.Services.AddSingleton<HousekeepingHandler>();
|
|
builder.Services.AddSingleton<UserSetupHandler>();
|
|
builder.Services.AddSingleton<IDbContextFactory, DbContextFactory>();
|
|
|
|
builder.Services.AddScoped<Timing>();
|
|
builder.Services.AddScoped<Identity>();
|
|
builder.Services.AddScoped<IUnitOfWork, UnitOfWork>();
|
|
builder.Services.AddScoped<SendEndpointHandler>();
|
|
builder.Services.AddScoped<ReceiveEndpointHandler>();
|
|
builder.Services.AddScoped<AckEndpointHandler>();
|
|
builder.Services.AddScoped<PeekEndpointHandler>();
|
|
|
|
// Authentication.
|
|
builder.Services
|
|
.AddAuthentication("Bearer")
|
|
.AddScheme<AuthenticationSchemeOptions, CustomBearerAuthenticationHandler>("Bearer", null);
|
|
|
|
// CORS.
|
|
builder.Services
|
|
.AddCors(opt => opt.AddPolicy("originpolicy", builder =>
|
|
{
|
|
builder
|
|
.WithOrigins(configuration.Origins.ToArray())
|
|
.AllowAnyHeader()
|
|
.AllowAnyMethod()
|
|
.AllowCredentials();
|
|
}));
|
|
|
|
// Ratelimiting
|
|
builder.Services.AddRateLimiter(options =>
|
|
{
|
|
options.GlobalLimiter = PartitionedRateLimiter.Create<HttpContext, string>(httpContext =>
|
|
{
|
|
var key = httpContext.Request.Headers["Authorization"].FirstOrDefault()
|
|
?? "anonymous";
|
|
|
|
return RateLimitPartition.GetFixedWindowLimiter(key, _ => new FixedWindowRateLimiterOptions
|
|
{
|
|
PermitLimit = configuration.RateLimitPerMinute,
|
|
Window = TimeSpan.FromMinutes(1),
|
|
QueueProcessingOrder = QueueProcessingOrder.OldestFirst,
|
|
QueueLimit = 0
|
|
});
|
|
});
|
|
|
|
options.RejectionStatusCode = 429;
|
|
});
|
|
|
|
// Proxy registration to forward real client IPs.
|
|
builder.Services.Configure<ForwardedHeadersOptions>(options =>
|
|
{
|
|
options.ForwardedHeaders = ForwardedHeaders.XForwardedFor | ForwardedHeaders.XForwardedProto;
|
|
|
|
foreach (var proxy in configuration.Proxies)
|
|
{
|
|
options.KnownProxies.Add(IPAddress.Parse(proxy));
|
|
}
|
|
});
|
|
|
|
var app = builder.Build();
|
|
app.UseDeveloperExceptionPage();
|
|
|
|
// DB Migrations
|
|
using (var ctx = app.Services.GetRequiredService<IDbContextFactory>().CreateDbContext())
|
|
{
|
|
var migrationLogger = app.Services.GetRequiredService<ILogger>();
|
|
|
|
try
|
|
{
|
|
if (ctx.Database.GetPendingMigrations().Any())
|
|
{
|
|
migrationLogger.Info("Applying migrations.");
|
|
ctx.Database.Migrate();
|
|
}
|
|
else
|
|
{
|
|
migrationLogger.Info("No migrations pending.");
|
|
}
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
migrationLogger.Error("Can't run migrations successfully.", ex);
|
|
throw;
|
|
}
|
|
}
|
|
|
|
// Housekeeping.
|
|
if (configuration.HousekeepingEnabled)
|
|
{
|
|
_ = Task.Run(async () =>
|
|
{
|
|
while (true)
|
|
{
|
|
await app.Services.GetService<HousekeepingHandler>().RemoveOldMessages();
|
|
await Task.Delay(TimeSpan.FromMinutes(1));
|
|
}
|
|
});
|
|
}
|
|
|
|
// User synchronization
|
|
var userSetupHandler = app.Services.GetRequiredService<UserSetupHandler>();
|
|
userSetupHandler.UpdateFromFile(new FileInfo(Constants.USERFILE_FILENAME)).GetAwaiter().GetResult();
|
|
|
|
app.UseStaticFiles();
|
|
app.UseRouting();
|
|
app.UseAuthentication();
|
|
app.UseCors("originpolicy");
|
|
app.UseForwardedHeaders();
|
|
|
|
// Ray id logging.
|
|
app.Use(async (context, next) =>
|
|
{
|
|
var stamp = DateTime.UtcNow;
|
|
var logger = context.RequestServices.GetRequiredService<ILogger>();
|
|
var ipa = context?.Connection?.RemoteIpAddress?.ToString() ?? "unknown";
|
|
var uid = context?.User?.Identity?.Name ?? "unknown";
|
|
var una = context?.User?.Claims?.SingleOrDefault(x => x.Type == "UserName")?.Value ?? "unknown";
|
|
var rid = context?.TraceIdentifier ?? "unknown";
|
|
var endpoint = context?.GetEndpoint()?.DisplayName ?? "unknown";
|
|
|
|
logger.Info($"{endpoint} call {rid}; ip {ipa}; u {una}/{uid}");
|
|
|
|
await next();
|
|
});
|
|
|
|
app.UseRateLimiter();
|
|
|
|
// Endpoint registration.
|
|
app.MapPost("/send", async (
|
|
ILogger logger,
|
|
IUnitOfWork unitOfWork,
|
|
SendEndpointHandler handler,
|
|
[FromBody] SendRequest request) =>
|
|
{
|
|
try
|
|
{
|
|
var response = await handler.SendMessage(request.ToUserId, request.Payload, request.PayloadType, request.PayloadLifetimeInSeconds);
|
|
await unitOfWork.SaveChanges();
|
|
return Results.Json(response.Id);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
logger.Error("Can't send.", ex);
|
|
return Results.InternalServerError();
|
|
}
|
|
});
|
|
|
|
app.MapGet("/receive", async (
|
|
ILogger logger,
|
|
IUnitOfWork unitOfWork,
|
|
ReceiveEndpointHandler handler) =>
|
|
{
|
|
try
|
|
{
|
|
var messages = await handler.ReceiveMessages();
|
|
|
|
if (messages?.Any() != true)
|
|
{
|
|
return Results.NoContent();
|
|
}
|
|
else
|
|
{
|
|
await unitOfWork.SaveChanges();
|
|
|
|
return Results.Json(new
|
|
{
|
|
Messages = messages.Select(x => new
|
|
{
|
|
Id = x.Id,
|
|
TimestampUtc = x.CreatedUtc,
|
|
Payload = x.Payload,
|
|
PayloadType = x.PayloadType,
|
|
Sender = x.FromId
|
|
})
|
|
});
|
|
}
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
logger.Error("Can't send.", ex);
|
|
return Results.InternalServerError();
|
|
}
|
|
});
|
|
|
|
app.MapPost("/ack", async (
|
|
ILogger logger,
|
|
IUnitOfWork unitOfWork,
|
|
AckEndpointHandler handler,
|
|
AckRequest request) =>
|
|
{
|
|
try
|
|
{
|
|
await handler.AckMessage(request.MessageId);
|
|
await unitOfWork.SaveChanges();
|
|
return Results.Ok();
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
logger.Error("Can't send.", ex);
|
|
return Results.InternalServerError();
|
|
}
|
|
});
|
|
|
|
app.MapGet("/yellowpages", (
|
|
ILogger logger,
|
|
IUnitOfWork unitOfWork,
|
|
Identity identity) =>
|
|
{
|
|
try
|
|
{
|
|
var routes = unitOfWork.UserRoutes.GetByFrom(identity.User).ToList();
|
|
return Results.Json(new
|
|
{
|
|
Users = routes.Select(x => new
|
|
{
|
|
Id = x.To.Id,
|
|
Name = x.To.Name
|
|
})
|
|
});
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
logger.Error("Can't yellowpages.", ex);
|
|
return Results.InternalServerError();
|
|
}
|
|
});
|
|
|
|
app.MapGet("/peek", async (
|
|
ILogger logger,
|
|
PeekEndpointHandler handler) =>
|
|
{
|
|
try
|
|
{
|
|
var pending = await handler.Peek();
|
|
return Results.Json(pending);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
logger.Error("Can't peek.", ex);
|
|
return Results.InternalServerError();
|
|
}
|
|
});
|
|
|
|
app.MapGet("/verify", (
|
|
ILogger logger,
|
|
IUnitOfWork unitOfWork,
|
|
Guid messageId) =>
|
|
{
|
|
try
|
|
{
|
|
var message = unitOfWork.Messages.GetById(messageId);
|
|
|
|
return Results.Json(new
|
|
{
|
|
message.IsDelivered,
|
|
message.IsAcknowledged
|
|
});
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
logger.Error("Can't verify.", ex);
|
|
return Results.InternalServerError();
|
|
}
|
|
});
|
|
|
|
app.Run();
|
|
}
|
|
}
|
|
} |