All checks were successful
Create and publish a Docker image / Publish image (push) Successful in 3m54s
216 lines
8.1 KiB
C#
216 lines
8.1 KiB
C#
using System.Threading.RateLimiting;
|
|
using Microsoft.AspNetCore.Authentication;
|
|
using Microsoft.AspNetCore.Authentication.JwtBearer;
|
|
using Microsoft.Identity.Web;
|
|
using Quartz;
|
|
using SfeduSchedule;
|
|
using SfeduSchedule.Jobs;
|
|
using SfeduSchedule.Services;
|
|
using X.Extensions.Logging.Telegram.Extensions;
|
|
using Microsoft.AspNetCore.HttpOverrides;
|
|
using Microsoft.AspNetCore.Mvc.ApplicationParts;
|
|
using SfeduSchedule.Auth;
|
|
|
|
var builder = WebApplication.CreateBuilder(args);
|
|
|
|
var configuration = builder.Configuration;
|
|
string? preinstalledJwtToken = configuration["TOKEN"];
|
|
string? tgChatId = configuration["TG_CHAT_ID"];
|
|
string? tgToken = configuration["TG_TOKEN"];
|
|
string updateJwtCron = configuration["UPDATE_JWT_CRON"] ?? "0 0 4 ? * *";
|
|
|
|
int permitLimit = int.TryParse(configuration["PERMIT_LIMIT"], out var parsedPermitLimit) ? parsedPermitLimit : 40;
|
|
int timeLimit = int.TryParse(configuration["TIME_LIMIT"], out var parsedTimeLimit) ? parsedTimeLimit : 10;
|
|
|
|
// создать папку data если не существует
|
|
var dataDirectory = Path.Combine(AppContext.BaseDirectory, "data");
|
|
if (!Directory.Exists(dataDirectory))
|
|
{
|
|
Directory.CreateDirectory(dataDirectory);
|
|
}
|
|
|
|
GlobalVariables.JwtFilePath = Path.Combine(dataDirectory, "jwt.txt");
|
|
var pluginsPath = Path.Combine(dataDirectory, "Plugins");
|
|
|
|
builder.Logging.ClearProviders();
|
|
builder.Logging.AddConsole();
|
|
if (!string.IsNullOrEmpty(tgChatId) && !string.IsNullOrEmpty(tgToken))
|
|
builder.Logging.AddTelegram(options =>
|
|
{
|
|
options.ChatId = tgChatId;
|
|
options.AccessToken = tgToken;
|
|
options.FormatterConfiguration.UseEmoji = true;
|
|
options.FormatterConfiguration.ReadableApplicationName = "Sfedu Schedule";
|
|
options.LogLevel = new Dictionary<string, LogLevel>
|
|
{
|
|
{ "Default", LogLevel.Error },
|
|
{ "SfeduSchedule.Jobs.UpdateJwtJob", LogLevel.Information },
|
|
{ "Program", LogLevel.Information }
|
|
};
|
|
});
|
|
|
|
// Включаем MVC контроллеры
|
|
var mvcBuilder = builder.Services.AddControllers();
|
|
builder.Services.AddHttpClient<ModeusService>();
|
|
|
|
builder.Services.AddAuthentication()
|
|
.AddScheme<AuthenticationSchemeOptions, ApiKeyAuthenticationHandler>(
|
|
ApiKeyAuthenticationDefaults.Scheme, _ => { });
|
|
|
|
builder.Services.AddAuthorization();
|
|
|
|
builder.Services.AddMicrosoftIdentityWebAppAuthentication(builder.Configuration);
|
|
|
|
// Загружаем плагины (изолированно), даём им зарегистрировать DI и подключаем контроллеры
|
|
var loaded = PluginLoader.LoadPlugins(pluginsPath);
|
|
foreach (var p in loaded)
|
|
{
|
|
// DI из плагина
|
|
p.Instance.ConfigureServices(builder.Services);
|
|
|
|
// Подключаем контроллеры плагина
|
|
mvcBuilder.PartManager.ApplicationParts.Add(new AssemblyPart(p.Assembly));
|
|
}
|
|
|
|
var jobKey = new JobKey("UpdateJWTJob");
|
|
|
|
if (string.IsNullOrEmpty(preinstalledJwtToken))
|
|
{
|
|
builder.Services.AddQuartz(q =>
|
|
{
|
|
q.AddJob<UpdateJwtJob>(opts => opts.WithIdentity(jobKey));
|
|
|
|
q.AddTrigger(opts => opts
|
|
.ForJob(jobKey)
|
|
.WithIdentity("UpdateJWTJob-trigger")
|
|
.WithCronSchedule(updateJwtCron)
|
|
);
|
|
});
|
|
|
|
builder.Services.AddQuartzHostedService(q => q.WaitForJobsToComplete = true);
|
|
}
|
|
|
|
builder.Services.AddEndpointsApiExplorer();
|
|
builder.Services.AddSwaggerGen(options =>
|
|
{
|
|
var xmlFile = $"{System.Reflection.Assembly.GetExecutingAssembly().GetName().Name}.xml";
|
|
var xmlPath = Path.Combine(AppContext.BaseDirectory, xmlFile);
|
|
options.IncludeXmlComments(xmlPath);
|
|
|
|
// Добавляем только схему авторизации по ApiKey
|
|
options.AddSecurityDefinition(ApiKeyAuthenticationDefaults.Scheme, new Microsoft.OpenApi.Models.OpenApiSecurityScheme
|
|
{
|
|
Description = $"Api Key needed to access the endpoints. {ApiKeyAuthenticationDefaults.HeaderName}: Your_API_Key",
|
|
Name = ApiKeyAuthenticationDefaults.HeaderName,
|
|
In = Microsoft.OpenApi.Models.ParameterLocation.Header,
|
|
Type = Microsoft.OpenApi.Models.SecuritySchemeType.ApiKey,
|
|
Scheme = ApiKeyAuthenticationDefaults.Scheme
|
|
});
|
|
options.OperationFilter<SwaggerAuthorizeOperationFilter>();
|
|
});
|
|
|
|
builder.Services.AddRateLimiter(options =>
|
|
{
|
|
options.AddPolicy("throttle", httpContext =>
|
|
RateLimitPartition.GetFixedWindowLimiter(
|
|
partitionKey: (httpContext.Request.Headers.TryGetValue("X-Forwarded-For", out var xff) && !string.IsNullOrWhiteSpace(xff.ToString()))
|
|
? xff.ToString().Split(',')[0].Trim()
|
|
: (httpContext.Connection.RemoteIpAddress?.ToString() ?? "unknown"),
|
|
factory: _ => new FixedWindowRateLimiterOptions
|
|
{
|
|
PermitLimit = permitLimit,
|
|
Window = TimeSpan.FromSeconds(timeLimit)
|
|
}));
|
|
|
|
options.OnRejected = async (context, cancellationToken) =>
|
|
{
|
|
context.HttpContext.Response.StatusCode = StatusCodes.Status429TooManyRequests;
|
|
context.HttpContext.Response.Headers["Retry-After"] = timeLimit.ToString();
|
|
|
|
await context.HttpContext.Response.WriteAsync("Rate limit exceeded. Please try again later.",
|
|
cancellationToken);
|
|
|
|
var reqLogger = context.HttpContext.RequestServices.GetRequiredService<ILogger<Program>>();
|
|
var clientIp = (context.HttpContext.Request.Headers.TryGetValue("X-Forwarded-For", out var xff) && !string.IsNullOrWhiteSpace(xff.ToString()))
|
|
? xff.ToString().Split(',')[0].Trim()
|
|
: context.HttpContext.Connection.RemoteIpAddress?.ToString();
|
|
reqLogger.LogWarning("Rate limit exceeded for IP: {IpAddress}", clientIp);
|
|
};
|
|
});
|
|
|
|
builder.Services.Configure<ForwardedHeadersOptions>(options =>
|
|
{
|
|
options.ForwardedHeaders = ForwardedHeaders.XForwardedFor |
|
|
ForwardedHeaders.XForwardedProto;
|
|
options.KnownNetworks.Clear();
|
|
options.KnownProxies.Clear();
|
|
});
|
|
|
|
var app = builder.Build();
|
|
|
|
var logger = app.Services.GetRequiredService<ILogger<Program>>();
|
|
|
|
app.UseForwardedHeaders();
|
|
|
|
if (string.IsNullOrEmpty(preinstalledJwtToken))
|
|
{
|
|
var schedulerFactory = app.Services.GetRequiredService<ISchedulerFactory>();
|
|
var scheduler = await schedulerFactory.GetScheduler();
|
|
|
|
// Проверить существование файла jwt.txt
|
|
if (File.Exists(GlobalVariables.JwtFilePath))
|
|
{
|
|
logger.LogInformation("Обнаружена прошлая сессия");
|
|
var lines = await File.ReadAllLinesAsync(GlobalVariables.JwtFilePath);
|
|
if (lines.Length > 1 && DateTime.TryParse(lines[1], out var expirationDate))
|
|
{
|
|
logger.LogInformation("Дата истечения токена: {ExpirationDate}", expirationDate);
|
|
if (expirationDate.AddHours(23) > DateTime.Now)
|
|
{
|
|
var token = lines[0];
|
|
logger.LogInformation("Используем существующий токен");
|
|
configuration["TOKEN"] = token;
|
|
}
|
|
else
|
|
{
|
|
logger.LogInformation("Токен истек или скоро истечет, выполняем обновление токена");
|
|
await scheduler.TriggerJob(jobKey);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
logger.LogInformation(
|
|
"Файл jwt.txt не содержит дату истечения или она некорректна, выполняем обновление токена");
|
|
await scheduler.TriggerJob(jobKey);
|
|
}
|
|
}
|
|
else
|
|
await scheduler.TriggerJob(jobKey);
|
|
}
|
|
|
|
app.UseSwagger();
|
|
app.UseSwaggerUI();
|
|
|
|
app.UseAuthentication();
|
|
app.UseAuthorization();
|
|
|
|
app.UseStaticFiles();
|
|
|
|
app.MapGet("/", async context =>
|
|
{
|
|
context.Response.ContentType = "text/html; charset=utf-8";
|
|
await context.Response.SendFileAsync(Path.Combine(app.Environment.WebRootPath, "index.html"));
|
|
});
|
|
|
|
app.MapControllers();
|
|
|
|
// Маршруты Minimal API из плагинов
|
|
foreach (var p in loaded)
|
|
{
|
|
logger.LogInformation("Mapping endpoints for plugin: {PluginName}", p.Instance.Name);
|
|
p.Instance.MapEndpoints(app);
|
|
}
|
|
|
|
app.UseRateLimiter();
|
|
|
|
app.Run(); |