Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
01bab77900 | ||
|
|
cfd9196083 |
No files matched your search
@@ -47,6 +47,10 @@
|
|||||||
- `POST /api/proxy/events/search` — получить расписание;
|
- `POST /api/proxy/events/search` — получить расписание;
|
||||||
- `POST /api/proxy/rooms/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:`.
|
Если `Modeus` отвечает ошибкой, сервис возвращает соответствующий HTTP-статус и текст с префиксом `Proxied Modeus:`.
|
||||||
|
|
||||||
### 2. Работа с расписанием
|
### 2. Работа с расписанием
|
||||||
|
|||||||
@@ -38,6 +38,7 @@ public class ProxyController(ModeusService modeusService, ILogger<ProxyControlle
|
|||||||
"Proxied Modeus: " + e.Message);
|
"Proxied Modeus: " + e.Message);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
schedule = await ScheduleLocationFilter.FilterAsync(schedule, HttpContext);
|
||||||
return Ok(schedule);
|
return Ok(schedule);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -46,11 +46,20 @@ public static class PluginLoader
|
|||||||
public sealed class PluginLoadContext(string pluginMainAssemblyPath) : AssemblyLoadContext(true)
|
public sealed class PluginLoadContext(string pluginMainAssemblyPath) : AssemblyLoadContext(true)
|
||||||
{
|
{
|
||||||
private readonly AssemblyDependencyResolver _resolver = new(pluginMainAssemblyPath);
|
private readonly AssemblyDependencyResolver _resolver = new(pluginMainAssemblyPath);
|
||||||
|
private static readonly HashSet<string> SharedAssemblyNames =
|
||||||
|
[
|
||||||
|
typeof(IPlugin).Assembly.GetName().Name!,
|
||||||
|
typeof(PluginLoader).Assembly.GetName().Name!
|
||||||
|
];
|
||||||
|
|
||||||
// Разрешаем управляемые зависимости плагина из его папки.
|
// Контракт плагина и API хоста должны иметь одинаковую идентичность типов в обоих контекстах загрузки.
|
||||||
// Возвращаем null, чтобы отдать решение в Default ALC для общих сборок (например, ModeusSchedule.Abstractions).
|
// Поэтому их всегда берём из Default ALC, даже если копии DLL лежат рядом с плагином.
|
||||||
protected override Assembly? Load(AssemblyName assemblyName)
|
protected override Assembly? Load(AssemblyName assemblyName)
|
||||||
{
|
{
|
||||||
|
if (assemblyName.Name != null && SharedAssemblyNames.Contains(assemblyName.Name))
|
||||||
|
return null;
|
||||||
|
|
||||||
|
// Остальные управляемые зависимости разрешаем из папки плагина.
|
||||||
var path = _resolver.ResolveAssemblyToPath(assemblyName);
|
var path = _resolver.ResolveAssemblyToPath(assemblyName);
|
||||||
return path is null ? null : LoadFromAssemblyPath(path);
|
return path is null ? null : LoadFromAssemblyPath(path);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,62 @@
|
|||||||
|
using System.Text.Json;
|
||||||
|
using System.Text.Json.Nodes;
|
||||||
|
using System.Text.RegularExpressions;
|
||||||
|
using Microsoft.AspNetCore.Authentication;
|
||||||
|
using Microsoft.AspNetCore.Authentication.Cookies;
|
||||||
|
|
||||||
|
namespace SfeduSchedule.Services;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Скрывает ссылки на подключение к занятиям от пользователей без Microsoft-сессии.
|
||||||
|
/// </summary>
|
||||||
|
public static class ScheduleLocationFilter
|
||||||
|
{
|
||||||
|
private const string AuthorizationRequiredMessage = "авторизуйтесь для просмотра";
|
||||||
|
private static readonly Regex AbsoluteUriPattern = new(
|
||||||
|
@"(?<![A-Za-z0-9+.-])[A-Za-z][A-Za-z0-9+.-]*:[^\s<>\[\]()]+",
|
||||||
|
RegexOptions.CultureInvariant);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Возвращает исходное расписание авторизованному пользователю или заменяет значение
|
||||||
|
/// <c>customLocation</c> безопасной заглушкой, если оно содержит абсолютный URI.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="schedule">JSON расписания Modeus.</param>
|
||||||
|
/// <param name="httpContext">Контекст запроса, содержащий Microsoft authentication cookie.</param>
|
||||||
|
/// <returns>Исходный либо отфильтрованный JSON; <see langword="null"/>, если расписание отсутствует.</returns>
|
||||||
|
/// <exception cref="JsonException">JSON расписания пуст или имеет некорректный формат.</exception>
|
||||||
|
public static async Task<string?> 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<JsonObject>())
|
||||||
|
{
|
||||||
|
if (eventLocation["customLocation"] is not JsonValue value ||
|
||||||
|
!value.TryGetValue<string>(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 _));
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in new issue
Block a user