From cfd9196083f69c31b00434613b1bcb08f33a64af Mon Sep 17 00:00:00 2001 From: Sergey Karmanov Date: Tue, 15 Sep 2026 19:52:19 +0300 Subject: [PATCH] =?UTF-8?q?feat:=20=D0=B4=D0=BE=D0=B1=D0=B0=D0=B2=D0=BB?= =?UTF-8?q?=D0=B5=D0=BD=20=D1=84=D0=B8=D0=BB=D1=8C=D1=82=D1=80=20=D0=B4?= =?UTF-8?q?=D0=BB=D1=8F=20=D1=81=D0=BA=D1=80=D1=8B=D1=82=D0=B8=D1=8F=20?= =?UTF-8?q?=D1=81=D1=81=D1=8B=D0=BB=D0=BE=D0=BA=20=D0=BD=D0=B0=20=D0=B7?= =?UTF-8?q?=D0=B0=D0=BD=D1=8F=D1=82=D0=B8=D1=8F=20=D0=BE=D1=82=20=D0=BD?= =?UTF-8?q?=D0=B5=D0=B0=D0=B2=D1=82=D0=BE=D1=80=D0=B8=D0=B7=D0=BE=D0=B2?= =?UTF-8?q?=D0=B0=D0=BD=D0=BD=D1=8B=D1=85=20=D0=BF=D0=BE=D0=BB=D1=8C=D0=B7?= =?UTF-8?q?=D0=BE=D0=B2=D0=B0=D1=82=D0=B5=D0=BB=D0=B5=D0=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 4 ++ SfeduSchedule/Controllers/ProxyController.cs | 3 +- .../Services/ScheduleLocationFilter.cs | 62 +++++++++++++++++++ 3 files changed, 68 insertions(+), 1 deletion(-) create mode 100644 SfeduSchedule/Services/ScheduleLocationFilter.cs diff --git a/README.md b/README.md index 373bf8d..4bca825 100644 --- a/README.md +++ b/README.md @@ -47,6 +47,10 @@ - `POST /api/proxy/events/search` — получить расписание; - `POST /api/proxy/rooms/search` — искать аудитории. +В ответе `POST /api/proxy/events/search` ссылки на подключение к занятиям защищены. Если `customLocation` содержит абсолютный URI отдельно, внутри текста или Markdown-разметки, пользователь без активной Microsoft-сессии получит значение `авторизуйтесь для просмотра` вместо всего содержимого поля. После входа через Microsoft API возвращает исходное значение. `X-Api-Key` не открывает доступ к ссылкам. + +Фильтрация реализована в `ScheduleLocationFilter`. Плагины могут вызывать этот общий helper для своих JSON-ответов, не дублируя проверку авторизации и обработку `customLocation`. + Если `Modeus` отвечает ошибкой, сервис возвращает соответствующий HTTP-статус и текст с префиксом `Proxied Modeus:`. ### 2. Работа с расписанием diff --git a/SfeduSchedule/Controllers/ProxyController.cs b/SfeduSchedule/Controllers/ProxyController.cs index 0a04f2c..5b8dcb7 100644 --- a/SfeduSchedule/Controllers/ProxyController.cs +++ b/SfeduSchedule/Controllers/ProxyController.cs @@ -38,6 +38,7 @@ public class ProxyController(ModeusService modeusService, ILogger +/// Скрывает ссылки на подключение к занятиям от пользователей без Microsoft-сессии. +/// +public static class ScheduleLocationFilter +{ + private const string AuthorizationRequiredMessage = "авторизуйтесь для просмотра"; + private static readonly Regex AbsoluteUriPattern = new( + @"(?\[\]()]+", + RegexOptions.CultureInvariant); + + /// + /// Возвращает исходное расписание авторизованному пользователю или заменяет значение + /// customLocation безопасной заглушкой, если оно содержит абсолютный URI. + /// + /// JSON расписания Modeus. + /// Контекст запроса, содержащий Microsoft authentication cookie. + /// Исходный либо отфильтрованный JSON; , если расписание отсутствует. + /// JSON расписания пуст или имеет некорректный формат. + public static async Task FilterAsync(string? schedule, HttpContext httpContext) + { + if (schedule == null) + return null; + + // API key не даёт доступ к ссылкам: проверяем только cookie, созданную Microsoft OIDC-входом. + var authenticationResult = + await httpContext.AuthenticateAsync(CookieAuthenticationDefaults.AuthenticationScheme); + if (authenticationResult.Succeeded) + return schedule; + + var root = JsonNode.Parse(schedule) ?? throw new JsonException("Schedule response is empty."); + if (root["_embedded"]?["event-locations"] is not JsonArray eventLocations) + return root.ToJsonString(); + + // Если ссылка окружена текстом или Markdown-разметкой, скрываем всё значение customLocation. + foreach (var eventLocation in eventLocations.OfType()) + { + if (eventLocation["customLocation"] is not JsonValue value || + !value.TryGetValue(out var customLocation) || + string.IsNullOrWhiteSpace(customLocation) || + !ContainsAbsoluteUri(customLocation)) + continue; + + eventLocation["customLocation"] = AuthorizationRequiredMessage; + } + + return root.ToJsonString(); + } + + private static bool ContainsAbsoluteUri(string value) + { + return AbsoluteUriPattern.Matches(value) + .Any(match => Uri.TryCreate(match.Value, UriKind.Absolute, out _)); + } +}