diff --git a/Dockerfile b/Dockerfile index 38ea523..6bfe556 100644 --- a/Dockerfile +++ b/Dockerfile @@ -10,17 +10,5 @@ RUN dotnet publish "SfeduSchedule.csproj" -c $BUILD_CONFIGURATION -o /app/publis FROM mcr.microsoft.com/dotnet/aspnet:9.0 AS final EXPOSE 8080 WORKDIR /app - -RUN apt-get update && \ - apt-get install -y --no-install-recommends wget && \ - wget -q https://github.com/PowerShell/PowerShell/releases/download/v7.5.2/powershell_7.5.2-1.deb_amd64.deb && \ - apt-get install -y ./powershell_7.5.2-1.deb_amd64.deb && \ - rm -f powershell_7.5.2-1.deb_amd64.deb && \ - rm -rf /var/lib/apt/lists/* - -ENV PLAYWRIGHT_BROWSERS_PATH=/ms-playwright - COPY --from=build /app/publish . -RUN pwsh ./playwright.ps1 install --with-deps chromium - ENTRYPOINT ["dotnet", "SfeduSchedule.dll"] \ No newline at end of file diff --git a/SfeduSchedule/BrowserScripts/MicrosoftLoginHelper.cs b/SfeduSchedule/BrowserScripts/MicrosoftLoginHelper.cs deleted file mode 100644 index a404843..0000000 --- a/SfeduSchedule/BrowserScripts/MicrosoftLoginHelper.cs +++ /dev/null @@ -1,70 +0,0 @@ -using System.Text.RegularExpressions; -using Microsoft.Playwright; - -namespace SfeduSchedule.BrowserScripts; - -public static class MicrosoftLoginHelper -{ - private static readonly string LoginUrl = "https://sfedu.modeus.org/"; - // private static readonly string StorageStatePath = "ms_storage_state.json"; - - public static async Task LoginMicrosoftAsync(IPage page, string username, string password) - { - if (string.IsNullOrEmpty(username) || string.IsNullOrEmpty(password)) - throw new Exception("username и password обязательны для авторизации Microsoft"); - - await page.GotoAsync(LoginUrl, new PageGotoOptions { WaitUntil = WaitUntilState.DOMContentLoaded }); - - await page.WaitForURLAsync(new Regex("login\\.(microsoftonline|live)\\.com", RegexOptions.IgnoreCase), new PageWaitForURLOptions { Timeout = 60_000 }); - - var useAnotherAccount = page.Locator("div#otherTile, #otherTileText, div[data-test-id='useAnotherAccount']").First; - try - { - await Assertions.Expect(useAnotherAccount).ToBeVisibleAsync(new() { Timeout = 2000 }); - await useAnotherAccount.ClickAsync(); - } - catch (PlaywrightException) - { - // Кнопка не появилась — пропускаем - } - - var emailInput = page.Locator("input[name='loginfmt'], input#i0116"); - await emailInput.WaitForAsync(new LocatorWaitForOptions { State = WaitForSelectorState.Visible, Timeout = 30_000 }); - await emailInput.FillAsync(username); - - var nextButton = page.Locator("#idSIButton9, input#idSIButton9"); - await nextButton.ClickAsync(); - - var passwordInput = page.Locator("input[name='passwd'], input#i0118"); - await passwordInput.WaitForAsync(new LocatorWaitForOptions { State = WaitForSelectorState.Visible, Timeout = 30_000 }); - await passwordInput.FillAsync(password); - await nextButton.ClickAsync(); - - await page.WaitForSelectorAsync("button, input[type='submit'], a", new PageWaitForSelectorOptions { Timeout = 8000 }); - - var locator = page.Locator("#idSIButton9, #idBtn_Back").First; - try - { - await Assertions.Expect(locator).ToBeVisibleAsync(new() { Timeout = 3000 }); - var noBtn = page.Locator("#idBtn_Back"); - if (await noBtn.IsVisibleAsync()) - await noBtn.ClickAsync(); - else - await page.Locator("#idSIButton9").ClickAsync(); - } - catch (PlaywrightException) - { - // Кнопки не появились — пропускаем этот шаг - } - - await page.WaitForURLAsync(url => !Regex.IsMatch(new Uri(url).Host, "login\\.(microsoftonline|live)\\.com", RegexOptions.IgnoreCase), new PageWaitForURLOptions { Timeout = 60_000 }); - await page.WaitForLoadStateAsync(LoadState.NetworkIdle); - - // Сохраняем storage state после успешного входа - // await page.Context.StorageStateAsync(new BrowserContextStorageStateOptions { Path = StorageStatePath }); - - var currentHost = new Uri(page.Url).Host; - if (Regex.IsMatch(currentHost, "login\\.(microsoftonline|live)\\.com", RegexOptions.IgnoreCase)) - throw new Exception("Авторизация не завершена: остались на странице Microsoft Login"); - } -} \ No newline at end of file diff --git a/SfeduSchedule/Jobs/UpdateJWTJob.cs b/SfeduSchedule/Jobs/UpdateJWTJob.cs index fae2b4b..c547eaf 100644 --- a/SfeduSchedule/Jobs/UpdateJWTJob.cs +++ b/SfeduSchedule/Jobs/UpdateJWTJob.cs @@ -1,87 +1,98 @@ -using Microsoft.Playwright; using Quartz; -using SfeduSchedule.BrowserScripts; namespace SfeduSchedule.Jobs; -public class UpdateJwtJob(IConfiguration configuration, ILogger logger) : IJob +public class UpdateJwtJob(IConfiguration configuration, ILogger logger, IHttpClientFactory httpClientFactory) : IJob { + private const int MaxAttempts = 5; // Максимальное число попыток + private const int DelaySeconds = 20; // Задержка между попытками в секундах + private const int TimeoutSeconds = 60; // Таймаут для каждого запроса в секундах + public async Task Execute(IJobExecutionContext jobContext) { logger.LogInformation("Начало выполнения UpdateJwtJob"); - string? username = configuration["MS_USERNAME"]; - string? password = configuration["MS_PASSWORD"]; + var authUrl = configuration["AUTH_URL"] ?? "http://msauth/auth/ms"; + var apiKey = configuration["AUTH_API_KEY"] ?? string.Empty; - if (string.IsNullOrEmpty(username) || string.IsNullOrEmpty(password)) + var client = httpClientFactory.CreateClient("authClient"); + client.Timeout = TimeSpan.FromSeconds(TimeoutSeconds + 10); + + if (!string.IsNullOrEmpty(apiKey)) { - logger.LogError("Не указаны учетные данные для входа"); - return; + client.DefaultRequestHeaders.Remove("X-API-Key"); + client.DefaultRequestHeaders.Add("X-API-Key", apiKey); } - using var playwright = await Playwright.CreateAsync(); - await using var browser = await playwright.Chromium.LaunchAsync(new BrowserTypeLaunchOptions + for (var attempt = 1; attempt <= MaxAttempts; attempt++) { - Headless = true - }); - var context = await browser.NewContextAsync(new BrowserNewContextOptions - { - ViewportSize = null - }); - var page = await context.NewPageAsync(); - - try - { - logger.LogInformation("Начало выполнения авторизации Microsoft"); - await MicrosoftLoginHelper.LoginMicrosoftAsync(page, username, password); - - var sessionStorageJson = await page.EvaluateAsync(@" - JSON.stringify(sessionStorage) - "); - - // Извлечение id_token из sessionStorageJson - string? idToken = null; try { - var sessionStorageDict = System.Text.Json.JsonSerializer.Deserialize>(sessionStorageJson); - if (sessionStorageDict != null) + logger.LogInformation("Попытка {Attempt}/{MaxAttempts} получения JWT из {AuthUrl}", attempt, MaxAttempts, authUrl); + + using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(TimeoutSeconds)); + var response = await client.GetAsync(authUrl, cts.Token); + + if (!response.IsSuccessStatusCode) { - var oidcKey = sessionStorageDict.Keys.FirstOrDefault(k => k.StartsWith("oidc.user:")); - if (oidcKey != null) + logger.LogWarning("Неуспешный статус при получении JWT: {StatusCode}", response.StatusCode); + + if (attempt == MaxAttempts) { - var oidcValueJson = sessionStorageDict[oidcKey]?.ToString(); - if (!string.IsNullOrEmpty(oidcValueJson)) - { - using var doc = System.Text.Json.JsonDocument.Parse(oidcValueJson); - if (doc.RootElement.TryGetProperty("id_token", out var idTokenElement)) - { - idToken = idTokenElement.GetString(); - } - } + logger.LogError("Достигнуто максимальное число попыток получения JWT"); + return; } + + await Task.Delay(TimeSpan.FromSeconds(DelaySeconds), jobContext.CancellationToken); + continue; } + + var body = await response.Content.ReadFromJsonAsync(cancellationToken: jobContext.CancellationToken); + + if (body is null || string.IsNullOrWhiteSpace(body.Jwt)) + { + logger.LogWarning("Ответ от MSAuth не содержит jwt"); + + if (attempt == MaxAttempts) + { + logger.LogError("Достигнуто максимальное число попыток получения корректного JWT"); + return; + } + + await Task.Delay(TimeSpan.FromSeconds(DelaySeconds), jobContext.CancellationToken); + continue; + } + + configuration["TOKEN"] = body.Jwt; + logger.LogInformation("JWT успешно обновлён"); + return; + } + catch (OperationCanceledException ex) + { + logger.LogWarning(ex, "Таймаут при получении JWT (попытка {Attempt}/{MaxAttempts})", attempt, MaxAttempts); + + if (attempt == MaxAttempts) + { + logger.LogError("Достигнут лимит по таймаутам при запросе JWT"); + return; + } + + await Task.Delay(TimeSpan.FromSeconds(DelaySeconds), jobContext.CancellationToken); } catch (Exception ex) { - logger.LogError(ex, "Ошибка при извлечении id_token из sessionStorageJson"); - return; + logger.LogError(ex, "Ошибка при получении JWT (попытка {Attempt}/{MaxAttempts})", attempt, MaxAttempts); + + if (attempt == MaxAttempts) + { + logger.LogError("Достигнуто максимальное число попыток из-за ошибок при запросе JWT"); + return; + } + + await Task.Delay(TimeSpan.FromSeconds(DelaySeconds), jobContext.CancellationToken); } - - configuration["TOKEN"] = idToken; - - await File.WriteAllTextAsync(GlobalVariables.JwtFilePath, idToken + "\n" + DateTime.Now.ToString("O")); - - logger.LogInformation("UpdateJwtJob выполнен успешно"); - - } - catch (Exception ex) - { - logger.LogError(ex, "Ошибка при выполнении UpdateJwtJob"); - } - finally - { - await context.CloseAsync(); - await browser.CloseAsync(); } } + + private sealed record JwtResponse(string Jwt); } \ No newline at end of file diff --git a/SfeduSchedule/Program.cs b/SfeduSchedule/Program.cs index 89cae64..075c8ec 100644 --- a/SfeduSchedule/Program.cs +++ b/SfeduSchedule/Program.cs @@ -60,6 +60,7 @@ if (!string.IsNullOrEmpty(tgChatId) && !string.IsNullOrEmpty(tgToken)) // Включаем MVC контроллеры var mvcBuilder = builder.Services.AddControllers(); builder.Services.AddHttpClient(); +builder.Services.AddHttpClient("authClient"); builder.Services.AddAuthentication() .AddScheme( diff --git a/SfeduSchedule/SfeduSchedule.csproj b/SfeduSchedule/SfeduSchedule.csproj index b451cbf..2f27d9c 100644 --- a/SfeduSchedule/SfeduSchedule.csproj +++ b/SfeduSchedule/SfeduSchedule.csproj @@ -12,7 +12,6 @@ - diff --git a/docker-compose-prod.yml b/docker-compose-prod.yml index f092400..741a0bc 100644 --- a/docker-compose-prod.yml +++ b/docker-compose-prod.yml @@ -11,12 +11,12 @@ services: - AzureAd:ClientSecret= - AzureAd:Domain=sfedu.onmicrosoft.com - AzureAd:CallbackPath=/signin-oidc - - MS_USERNAME=${MS_USERNAME} - - MS_PASSWORD=${MS_PASSWORD} - TG_CHAT_ID=${TG_CHAT_ID} - TG_TOKEN=${TG_TOKEN} - API_KEY=${API_KEY} # - TOKEN=${TOKEN} + - AUTH_URL=${AUTH_URL} + - AUTH_API_KEY=${AUTH_API_KEY} volumes: - data:/app/data restart: always diff --git a/docker-compose-test.yml b/docker-compose-test.yml index 3c0fb34..11a4873 100644 --- a/docker-compose-test.yml +++ b/docker-compose-test.yml @@ -11,12 +11,12 @@ services: - AzureAd:ClientSecret= - AzureAd:Domain=sfedu.onmicrosoft.com - AzureAd:CallbackPath=/signin-oidc - - MS_USERNAME=${MS_USERNAME} - - MS_PASSWORD=${MS_PASSWORD} - TG_CHAT_ID=${TG_CHAT_ID} - TG_TOKEN=${TG_TOKEN} - API_KEY=${API_KEY} # - TOKEN=${TOKEN} + - AUTH_URL=${AUTH_URL} + - AUTH_API_KEY=${AUTH_API_KEY} volumes: - ./data:/app/data restart: unless-stopped