-
Notifications
You must be signed in to change notification settings - Fork 0
Cookie Storage RU
LogsParser не владеет хранением cookie — этим занимается вызывающий код через контракт ICookieStorage. Эта
страница описывает контракт и его точную семантику, встроенную реализацию MemoryCookieStorage, способ написать
хранилище, переживающее перезапуск процесса, и то, как хранилище выбирается для запроса.
Аутентификация реактивна: вызывать LoginAsync перед работой не нужно, такого метода нет. Транспорт реагирует
на редиректы 302 от сервиса, поэтому самый первый запрос данных в процессе сам выполняет вход и подтверждение
второго фактора, если отправленные им сессионные cookie отсутствуют или устарели. Всё, что идентифицирует эту
сессию, живёт в cookie.
Хранилище, существующее только в памяти, означает полный вход при каждом старте процесса: новый POST с учётными данными, свежесгенерированный код TOTP и ещё один проход по страницам входа сайта до того, как вернутся данные. Хранилище, которое записывает своё содержимое в долговременное место и читает его обратно в конструкторе, отдаёт транспорту уже аутентифицированную сессию, и первый запрос сразу идёт за данными.
Библиотека никогда не выбирает это место за вас. Она читает cookie перед отправкой запроса и записывает их обратно после каждого ответа; где они находятся между этими моментами — целиком дело реализации.
Пространство имён: LogsParser.Abstractions.
public interface ICookieStorageIReadOnlyCollection<ParserCookie> GetCookies();void SetCookies(IReadOnlyCollection<ParserCookie> cookies);| Член | Возвращает | Описание |
|---|---|---|
GetCookies() |
IReadOnlyCollection<ParserCookie> |
Полный текущий набор cookie. Вызывается перед каждым запросом для построения заголовка Cookie и в начале каждого обновления. |
SetCookies(IReadOnlyCollection<ParserCookie> cookies) |
void |
Полностью заменяет набор cookie значением cookies. Вызывается после ответов с Set-Cookie и при сохранении токена анти-DDoS-защиты. |
Пространство имён: LogsParser (корневое).
public sealed record ParserCookie(string Name, string Value);| Свойство | Тип | Описание |
|---|---|---|
Name |
string |
Имя cookie; во всей библиотеке сравнивается через StringComparer.Ordinal. |
Value |
string |
Значение cookie; хранится и отправляется дословно — никакого кодирования или декодирования не выполняется. |
| Правило | Подробности |
|---|---|
| Только пары имя/значение | Все атрибуты Set-Cookie — expires, max-age, path, domain, secure, httponly, samesite — отбрасываются до того, как cookie попадёт в хранилище. Сохраняется только первый сегмент name=value заголовка. От хранилища не ожидается моделирование срока жизни или области видимости, и библиотека никогда об этом не попросит. |
SetCookies — полная замена |
Это не слияние. Переданная коллекция является полным новым состоянием; всё, что хранилось раньше и отсутствует в аргументе, исчезает. Слияние выполняет библиотека до вызова, а не хранилище. |
| Порядковое сравнение имён | Имена cookie сравниваются через StringComparer.Ordinal. XSRF-TOKEN и xsrf-token — это две разные cookie. |
GetCookies возвращает снимок |
Возвращайте копию, а не «живую» внутреннюю коллекцию. Вызывающий код не должен иметь возможности изменить состояние хранилища в обход него, а хранилище не должно видеть правки вызывающего кода. |
| Потокобезопасность | Оба метода могут вызываться параллельно — из нескольких выполняющихся запросов, из процедуры аутентификации, из решателя challenge, — поэтому реализация обязана быть безопасной при вызове из нескольких потоков. |
| Пустые имена бессмысленны | Библиотека никогда не создаёт cookie с пустым или пробельным именем; хранилище может отбрасывать такие записи, как это делает MemoryCookieStorage. |
| Одна запись на имя | Заголовок Cookie собирается склейкой пар Name=Value через "; " в том порядке, в котором их вернул GetCookies. Дублирующиеся имена были бы отправлены дважды, поэтому убирайте дубликаты при записи. |
Реализация по умолчанию. Пространство имён: LogsParser.Abstractions (файл лежит в Infrastructure/Cookies/).
public sealed class MemoryCookieStorage : ICookieStorage-
sealed, с неявным конструктором без параметров. - Оба метода берут приватную блокировку, поэтому тип безопасно разделять между параллельными запросами.
-
GetCookiesвозвращает новый массив при каждом вызове — снимок, а не внутренний список. -
SetCookiesотбрасывает записи, у которыхNameравенnull, пуст или состоит из пробелов, затем убирает дубликаты поNameчерезStringComparer.Ordinal, сохраняя первое вхождение каждого имени. - Состояние привязано к экземпляру и живёт только пока живёт процесс. Два экземпляра ничем не связаны; ничего никуда не записывается. Перезапуск процесса теряет сессию.
Исключения
| Метод | Исключение | Когда |
|---|---|---|
SetCookies |
ArgumentNullException |
cookies равен null. |
using LogsParser;
using LogsParser.Abstractions;
var storage = new MemoryCookieStorage();
storage.SetCookies(
[
new ParserCookie("arizonarp_session", "abc"),
new ParserCookie("arizonarp_session", "ignored-duplicate"),
new ParserCookie("", "dropped"),
new ParserCookie("XSRF-TOKEN", "xyz")
]);
foreach (var cookie in storage.GetCookies())
{
Console.WriteLine($"{cookie.Name}={cookie.Value}");
}
// arizonarp_session=abc
// XSRF-TOKEN=xyzСессионные cookie сайта и токен анти-DDoS-защиты лежат в одном плоском хранилище — никакого разделения по назначению, домену или пути нет, второго хранилища в библиотеке тоже нет.
| Cookie | Кем записывается | Значение |
|---|---|---|
arizonarp_session |
сервисом, через Set-Cookie в любом ответе |
Аутентифицированная сессия. Её потеря означает полный вход при следующем запросе. |
XSRF-TOKEN |
сервисом, через Set-Cookie в любом ответе |
CSRF-cookie, которую сайт выдаёт вместе с сессией. |
R3ACTLB |
библиотекой, после решения React-challenge анти-DDoS-защиты | Токен challenge, сохраняемый ровно под этим именем и переотправляемый в последующих запросах. |
Поскольку они лежат вместе, сохранение хранилища сохраняет и сессию, и решённый токен challenge: один файл, одно восстановление, никакого отдельного учёта. Процессы, порождающие эти значения, описаны на странице Транспорт и аутентификация.
Хранятся только имена и значения. Отсюда два следствия, о которых стоит знать: библиотека сама никогда не
удаляет cookie по сроку жизни, а удаление на стороне сервера (Set-Cookie: name=; expires=…) записывается как
запись с пустым значением, а не как удаление.
Полная реализация. Она загружает содержимое в конструкторе, записывает его в SetCookies и защищает всё одной
блокировкой уровня экземпляра.
using System.Text.Json;
using LogsParser;
using LogsParser.Abstractions;
public sealed class FileCookieStorage : ICookieStorage
{
private readonly object _sync = new();
private readonly string _path;
private Dictionary<string, string> _cookies;
public FileCookieStorage(string path)
{
_path = path ?? throw new ArgumentNullException(nameof(path));
_cookies = Load(path);
}
public IReadOnlyCollection<ParserCookie> GetCookies()
{
lock (_sync)
{
return _cookies.Select(pair => new ParserCookie(pair.Key, pair.Value)).ToArray();
}
}
public void SetCookies(IReadOnlyCollection<ParserCookie> cookies)
{
ArgumentNullException.ThrowIfNull(cookies);
lock (_sync)
{
_cookies = cookies
.Where(cookie => !string.IsNullOrWhiteSpace(cookie.Name))
.GroupBy(cookie => cookie.Name, StringComparer.Ordinal)
.ToDictionary(group => group.Key, group => group.First().Value, StringComparer.Ordinal);
Save();
}
}
private void Save()
{
var directory = Path.GetDirectoryName(_path);
if (!string.IsNullOrEmpty(directory))
{
Directory.CreateDirectory(directory);
}
var temporary = _path + ".tmp";
File.WriteAllText(temporary, JsonSerializer.Serialize(_cookies));
File.Move(temporary, _path, overwrite: true);
}
private static Dictionary<string, string> Load(string path)
{
if (!File.Exists(path))
{
return new Dictionary<string, string>(StringComparer.Ordinal);
}
var restored = JsonSerializer.Deserialize<Dictionary<string, string>>(File.ReadAllText(path));
return restored is null
? new Dictionary<string, string>(StringComparer.Ordinal)
: new Dictionary<string, string>(restored, StringComparer.Ordinal);
}
}Подключение — единственное изменение на стороне вызова:
using LogsParser;
using LogsParser.Models;
using LogsParser.Net;
using var dataSource = new LogsParserHttpDataSource(
credentials: new LogsParserCredentials("my_login", "my_password", "BASE32SECRET"),
cookieStorage: new FileCookieStorage("state/logsparser-session.json"));
var client = new LogsParserClient(dataSource);
// Первый запуск: вход, подтверждение 2FA, запись сессии в state/logsparser-session.json.
// Последующие запуски: сессия восстанавливается в конструкторе, вход не выполняется вовсе.
var page = await client.GetLogsAsync(new LogsQuery(ServerId: 201));Файл содержит действующий материал сессии. Любой, кто может прочитать
arizonarp_session, способен действовать от имени вошедшей учётной записи, не зная ни пароля, ни секрета TOTP. Храните файл вне системы контроля версий, в приватном для пользователя месте, с ограничивающими правами доступа — и считайте его утечку компрометацией сессии.
Та же схема подходит для любого носителя: строки в базе данных, распределённого кэша, менеджера секретов.
Меняются только тела Load/Save.
Источник данных создаётся с хранилищем по умолчанию. Если оно не передано, он создаёт собственный
MemoryCookieStorage:
public LogsParserHttpDataSource(
LogsParserCredentials? credentials = null,
ICookieStorage? cookieStorage = null,
LogsParserHttpOptions? options = null,
HttpClient? httpClient = null,
ILoggerFactory? loggerFactory = null)Каждый запрос несёт собственное необязательное хранилище, и оно побеждает для этого одного вызова:
public sealed record ParserRequest(string RelativeUri, ICookieStorage? CookieStorage = null);Task<string> GetContentAsync(ParserRequest request, CancellationToken cancellationToken = default);ParserRequest.CookieStorage переопределяет хранилище по умолчанию у источника данных; когда оно равно null,
используется хранилище по умолчанию. Каждый метод LogsParserClient предоставляет то же переопределение в виде
необязательного параметра и передаёт его в создаваемый ParserRequest:
public async Task<LogsPage> GetLogsAsync(
LogsQuery query,
ICookieStorage? cookieStorage = null,
CancellationToken cancellationToken = default)Благодаря этому одного источника данных достаточно для нескольких независимых сессий: учётные данные, HTTP-параметры и счётчики лимита запросов общие, а cookie — нет:
using LogsParser;
using LogsParser.Abstractions;
using LogsParser.Models;
using LogsParser.Net;
using var dataSource = new LogsParserHttpDataSource(
credentials: new LogsParserCredentials("my_login", "my_password", "BASE32SECRET"),
cookieStorage: new MemoryCookieStorage());
var client = new LogsParserClient(dataSource);
// Две отдельные серверные сессии, две отдельные «банки» cookie.
ICookieStorage persistent = new FileCookieStorage("state/logsparser-session.json");
ICookieStorage scratch = new MemoryCookieStorage();
var reused = await client.GetLogsAsync(new LogsQuery(ServerId: 201), persistent);
var isolated = await client.GetLogsAsync(new LogsQuery(ServerId: 201), scratch);
// Хранилище по умолчанию, переданное в конструктор, используется только при отсутствии переопределения.
var catalog = await client.GetLogsFilterCatalogAsync();Оба вызова аутентифицируются единственным набором учётных данных, с которым создан источник данных, поэтому обе сессии принадлежат одной учётной записи — различается состояние сессии, которое никогда не перетекает из одного хранилища в другое. Переданное на запрос пустое хранилище вызовет собственный вход при первом использовании.
При использовании Microsoft.Extensions.DependencyInjection ICookieStorage регистрируется как singleton (по
умолчанию MemoryCookieStorage) и может быть заменён через LogsParserRegistrationOptions.CookieStorageFactory
— см. Внедрение зависимостей.
Библиотека никогда не вызывает SetCookies напрямую. Каждое изменение проходит через внутренние
read-modify-write-помощники (применение заголовков Set-Cookie из ответа и сохранение решённого токена
R3ACTLB), которые удерживают блокировку, привязанную к самому экземпляру хранилища и хранящуюся в
ConditionalWeakTable. Поэтому чтение и запись происходят в одной критической секции, и два параллельных
запроса, использующих одно хранилище, не могут затереть cookie друг друга.
Эта блокировка внутренняя и недоступна из кода вызывающей стороны. Отсюда:
- Собственные
GetCookies/SetCookiesхранилища всё равно обязаны быть потокобезопасными по отдельности — внутренняя блокировка защищает последовательности библиотеки, а не поля реализации. - Код, выполняющий собственный read-modify-write —
GetCookies, правка снимка,SetCookies, — находится вне этой критической секции и может потерять обновление в гонке с выполняющимся запросом. Делайте такие правки под собственной блокировкой, отдельной для каждого экземпляра хранилища, и предпочтительно тогда, когда ни один запрос не выполняется. - «Слепые» полные замены (
SetCookiesс набором, собранным с нуля, например восстановление сохранённой сессии до первого запроса) безопасны сами по себе, поскольку ничего не читают.
- Транспорт и аутентификация — когда cookie читаются, записываются и переотправляются
-
LogsParserClient — параметр
cookieStorageу каждого метода -
Внедрение зависимостей —
CookieStorageFactoryи времена жизни сервисов - Начало работы — первый запрос, вручную и через DI
- Исключения — что выбрасывается при отсутствующей или устаревшей сессии
LogsParser · CC BY-NC 4.0 — non-commercial use only / только некоммерческое использование
English
- Home
- Getting Started
- LogsParserClient
- Requests and URI Builder
- Models
- Parsers
- Transport and Authentication
- Cookie Storage
- Dependency Injection
- Logging
- Exceptions
Русский
- Главная
- Начало работы
- LogsParserClient
- Запросы и построитель URI
- Модели
- Парсеры
- Транспорт и авторизация
- Хранение cookies
- Внедрение зависимостей
- Логирование
- Исключения