-
Notifications
You must be signed in to change notification settings - Fork 0
Cookie Storage
LogsParser does not own cookie persistence — the caller does, through the ICookieStorage contract. This page
documents the contract and its exact semantics, the built-in MemoryCookieStorage, how to write a storage that
survives process restarts, and how a storage is selected for a request.
Authentication is reactive: there is no LoginAsync to call first. The transport reacts to the service's 302
redirects, so the very first data request of a process performs the login and the two-factor confirmation when
the session cookies it sends are missing or stale. Everything that identifies that session lives in cookies.
A storage that only lives in memory therefore means a full login on every process start — new credentials POST, a freshly generated TOTP code, and another pass through the site's login pages before any data is returned. A storage that writes its contents somewhere durable and reads them back in its constructor hands the transport a session that is already authenticated, and the first request goes straight to the data.
The library never picks that location for you. It reads cookies before sending a request and writes them back after every response; where they go in between is entirely the implementation's business.
Namespace: LogsParser.Abstractions.
public interface ICookieStorageIReadOnlyCollection<ParserCookie> GetCookies();void SetCookies(IReadOnlyCollection<ParserCookie> cookies);| Member | Returns | Description |
|---|---|---|
GetCookies() |
IReadOnlyCollection<ParserCookie> |
The full current cookie set. Called before every request to build the Cookie header, and at the start of every update. |
SetCookies(IReadOnlyCollection<ParserCookie> cookies) |
void |
Replaces the entire cookie set with cookies. Called after responses that carry Set-Cookie and when the anti-DDoS token is stored. |
Namespace: LogsParser (root).
public sealed record ParserCookie(string Name, string Value);| Property | Type | Description |
|---|---|---|
Name |
string |
Cookie name, compared with StringComparer.Ordinal everywhere in the library. |
Value |
string |
Cookie value, stored and sent verbatim — no encoding or decoding is applied. |
| Rule | Detail |
|---|---|
| Name/value pairs only | Every Set-Cookie attribute — expires, max-age, path, domain, secure, httponly, samesite — is parsed off and discarded before the cookie reaches the storage. Only the first name=value segment of the header survives. A storage is not expected to model expiry or scope, and nothing in the library will ask it to. |
SetCookies is a full replace |
It is not a merge. The collection passed in is the complete new state; anything previously held and not present in the argument is gone. Merging is done by the library before the call, never by the storage. |
| Ordinal name comparison | Cookie names are compared with StringComparer.Ordinal. XSRF-TOKEN and xsrf-token are two different cookies. |
GetCookies returns a snapshot |
Return a copy, not the live backing collection. The caller must not be able to mutate the storage's state behind its back, and the storage must not observe the caller's edits. |
| Thread safety | Both methods can be called concurrently — from several requests in flight, from the authentication flow, from the challenge solver — so an implementation must be safe to call from several threads. |
| Blank names are meaningless | The library never emits a cookie with an empty or whitespace name; a storage may drop such entries, as MemoryCookieStorage does. |
| One entry per name | The Cookie header is built by joining Name=Value pairs with "; " in the order GetCookies returned them. Duplicate names would be sent twice, so de-duplicate on write. |
The default implementation. Namespace: LogsParser.Abstractions (file lives in Infrastructure/Cookies/).
public sealed class MemoryCookieStorage : ICookieStorage-
sealed, with the implicit parameterless constructor. - Both methods take a private lock, so the type is safe to share between concurrent requests.
-
GetCookiesreturns a fresh array each call — a snapshot, never the internal list. -
SetCookiesdrops entries whoseNameis null, empty or whitespace, then de-duplicates byNamewithStringComparer.Ordinal, keeping the first occurrence of each name. - State is per instance and lives only as long as the process. Two instances share nothing; nothing is written anywhere. Restarting the process loses the session.
Throws
| Method | Exception | When |
|---|---|---|
SetCookies |
ArgumentNullException |
cookies is 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=xyzThe site's session cookies and the anti-DDoS token share one flat store — there is no separation by purpose, domain or path, and no second storage anywhere in the library.
| Cookie | Written by | Meaning |
|---|---|---|
arizonarp_session |
the service, via Set-Cookie on any response |
The authenticated session. Losing it means a full login on the next request. |
XSRF-TOKEN |
the service, via Set-Cookie on any response |
The CSRF cookie the site issues alongside the session. |
R3ACTLB |
the library, after solving the React anti-DDoS challenge | The challenge token, stored under this exact name and replayed on subsequent requests. |
Because they live together, persisting the storage persists both the session and a solved challenge token: one file, one restore, no separate bookkeeping. See Transport and Authentication for the flows that produce these values.
Only names and values are kept. Two consequences worth knowing: the library never expires a cookie on its own,
and a server-side deletion (Set-Cookie: name=; expires=…) is recorded as an entry with an empty value rather
than as a removal.
A complete implementation. It loads its contents in the constructor, writes them in SetCookies, and guards
everything with one instance-level lock.
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);
}
}Wiring it in is the only change at the call site:
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);
// First run: logs in, confirms 2FA, writes the session to state/logsparser-session.json.
// Later runs: the session is restored in the constructor and no login happens at all.
var page = await client.GetLogsAsync(new LogsQuery(ServerId: 201));The file holds live session material. Anyone who can read
arizonarp_sessioncan act as the logged-in account without knowing the password or the TOTP secret. Store it outside source control, in a user-private location, with restrictive file permissions — and treat losing it as a session compromise.
The same shape works for any backing store: a database row, a distributed cache, a secrets manager. Only the
Load/Save bodies change.
The data source is created with a default storage. When none is passed, it creates a MemoryCookieStorage of
its own:
public LogsParserHttpDataSource(
LogsParserCredentials? credentials = null,
ICookieStorage? cookieStorage = null,
LogsParserHttpOptions? options = null,
HttpClient? httpClient = null,
ILoggerFactory? loggerFactory = null)Every request carries an optional storage of its own, and it wins for that one call:
public sealed record ParserRequest(string RelativeUri, ICookieStorage? CookieStorage = null);Task<string> GetContentAsync(ParserRequest request, CancellationToken cancellationToken = default);ParserRequest.CookieStorage overrides the data source's default; when it is null, the default is used.
Every LogsParserClient method exposes the same override as an optional parameter and forwards it into the
ParserRequest it builds:
public async Task<LogsPage> GetLogsAsync(
LogsQuery query,
ICookieStorage? cookieStorage = null,
CancellationToken cancellationToken = default)That makes one data source enough for several independent sessions — the credentials, HTTP options and rate limit counters are shared, but the cookies are not:
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);
// Two separate server-side sessions, two separate cookie jars.
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);
// The default storage passed to the constructor is used only when no override is given.
var catalog = await client.GetLogsFilterCatalogAsync();Both calls authenticate with the one credential set the data source was built with, so both sessions belong to the same account — what differs is the session state, which never leaks from one storage into the other. A storage passed per request that starts out empty will trigger its own login on first use.
Under Microsoft.Extensions.DependencyInjection, ICookieStorage is registered as a singleton
(MemoryCookieStorage by default) and can be replaced with LogsParserRegistrationOptions.CookieStorageFactory
— see Dependency Injection.
The library never calls SetCookies directly. Every mutation goes through internal read-modify-write helpers
(applying Set-Cookie headers from a response, and storing the solved R3ACTLB token) that hold a lock keyed
on the storage instance itself, kept in a ConditionalWeakTable. Read and write therefore happen inside one
critical section, and two concurrent requests sharing a storage cannot overwrite each other's cookies.
That lock is internal and not reachable from caller code. So:
- A storage's own
GetCookies/SetCookiesmust still be individually thread-safe — the internal lock protects the library's sequences, not the implementation's fields. - Code that performs its own read-modify-write —
GetCookies, edit the snapshot,SetCookies— is outside that critical section and can lose an update against an in-flight request. Do such edits through a lock of your own, per storage instance, and prefer to make them while no request is running. - Blind full replacements (
SetCookieswith a set built from scratch, e.g. restoring a saved session before the first request) are safe on their own, because they read nothing.
- Transport and Authentication — when cookies are read, written and replayed
-
LogsParserClient — the per-call
cookieStorageparameter on every method -
Dependency Injection —
CookieStorageFactoryand service lifetimes - Getting Started — the first request, manual and DI
- Exceptions — what a missing or stale session ends up throwing
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
- Внедрение зависимостей
- Логирование
- Исключения