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 _)); + } +}