-
Notifications
You must be signed in to change notification settings - Fork 0
Transport and Authentication
This page covers the layer below LogsParserClient: the ILogsParserDataSource contract, the
LogsParserHttpDataSource implementation, the reactive authentication state machine, retries, rate limiting and the
React anti-DDoS challenge. It is the page to read when a request hangs, loops or returns something that does not
look like log HTML.
Namespace LogsParser.Abstractions.
public interface ILogsParserDataSource
{
Task<string> GetContentAsync(ParserRequest request, CancellationToken cancellationToken = default);
}The interface has exactly one member. LogsParserClient builds a relative URI, hands it to this method, receives a
raw HTML string and passes it to a static parser — it never touches HttpClient, cookies or credentials. As a
consequence, the entire transport is replaceable: implement the interface and the whole client keeps working
against files, a cache, a recorded fixture set or a proxy service.
| Member | Type | Description |
|---|---|---|
GetContentAsync |
Task<string> |
Returns the raw HTML for request.RelativeUri. Nothing about the return value is validated by the caller — parsers accept whatever string comes back. |
Namespace LogsParser (root).
public sealed record ParserRequest(string RelativeUri, ICookieStorage? CookieStorage = null);| Parameter | Type | Default | Description |
|---|---|---|---|
RelativeUri |
string |
— | Relative URI with its query string, as produced by LogsRequestUriBuilder. Resolved against the HttpClient's BaseAddress. |
CookieStorage |
ICookieStorage? |
null |
Storage that overrides the data source's default storage for this one request. null means "use the default". |
The per-request storage is how one process can serve several independent sessions through a single data source: see Cookie Storage.
A minimal implementation that serves saved HTML from disk instead of hitting the network:
using LogsParser;
using LogsParser.Abstractions;
using LogsParser.Models;
public sealed class FileHtmlDataSource : ILogsParserDataSource
{
private readonly string _root;
public FileHtmlDataSource(string root) => _root = root;
public Task<string> GetContentAsync(ParserRequest request, CancellationToken cancellationToken = default)
{
var name = string.Concat(request.RelativeUri.Split(Path.GetInvalidFileNameChars()));
return File.ReadAllTextAsync(Path.Combine(_root, name + ".html"), cancellationToken);
}
}
var client = new LogsParserClient(new FileHtmlDataSource(@"C:\fixtures"));
var page = await client.GetLogsAsync(new LogsQuery(ServerId: 201));Nothing else in the library has to change. A custom source is also the simplest way to add caching, request
throttling or logging in front of the real HTTP source by wrapping LogsParserHttpDataSource and delegating to it.
Namespace LogsParser.Net. This is the only implementation shipped with the package.
public sealed class LogsParserHttpDataSource : ILogsParserDataSource, IDisposablepublic LogsParserHttpDataSource(
LogsParserCredentials? credentials = null,
ICookieStorage? cookieStorage = null,
LogsParserHttpOptions? options = null,
HttpClient? httpClient = null,
ILoggerFactory? loggerFactory = null)| Parameter | Type | Default | Description |
|---|---|---|---|
credentials |
LogsParserCredentials? |
null |
Login, password and Base32 TOTP secret. Without them the data source works only for pages that need no session, and throws AuthenticationRequiredException as soon as the service asks for one. |
cookieStorage |
ICookieStorage? |
null |
Default cookie storage for every request. When omitted, a new MemoryCookieStorage is created. |
options |
LogsParserHttpOptions? |
null |
Transport options. When omitted, a new LogsParserHttpOptions() with all defaults is used. |
httpClient |
HttpClient? |
null |
Externally owned client. When omitted, the data source creates and owns one. |
loggerFactory |
ILoggerFactory? |
null |
When not null, it is installed into LogsParserLogging — see Logging. |
-
No
HttpClientsupplied. It buildsnew HttpClientHandler { AllowAutoRedirect = false, AutomaticDecompression = DecompressionMethods.All }, wraps it in anHttpClientcreated withdisposeHandler: true, and remembers that this client is its own.Dispose()disposes it. -
An
HttpClientsupplied. The instance is used as-is and is not disposed byDispose(); its lifetime stays with whoever created it. -
BaseAddressis assigned with??=: theLogsParserHttpOptions.BaseUrivalue is applied only when the client has noBaseAddressyet. -
User-Agentis added only whenDefaultRequestHeaders.UserAgentis empty,Acceptonly whenDefaultRequestHeaders.Acceptis empty. An injected client that already sets these keeps its own values and the corresponding options are silently ignored. -
loggerFactory, when supplied, callsLogsParserLogging.UseLoggerFactory(loggerFactory), which is a process-wide switch, not a per-instance setting. - It logs one
Debugline describing the base URI, whether credentials are present,MaxRetryAttempts, the cookie storage type and whether theHttpClientisinternalorexternal.
Namespace LogsParser.Models. Full reference in Models; repeated here because every knob on this page
lives on this record.
| Property | Type | Default | Description |
|---|---|---|---|
BaseUri |
Uri |
https://arizonarp.logsparser.info/ |
Applied only if the HttpClient has no BaseAddress. |
UserAgent |
string |
a Chrome 128 desktop UA string | Applied only if the client sends no User-Agent. |
Accept |
string |
the browser text/html,… accept string |
Applied only if the client sends no Accept. |
MaxRetryAttempts |
int |
5 |
Upper bound on transient-failure attempts. |
WaitForRateLimitReset |
bool |
true |
On 429, wait for the reset instant instead of throwing. |
public async Task<string> GetContentAsync(ParserRequest request, CancellationToken cancellationToken = default)One call may perform several HTTP round trips: challenge solving, authentication and rate-limit waits all retry the same request internally. Cookies are read from the storage into the request headers before every send and written back from the response headers after it, so the session survives across calls.
Throws:
| Exception | When |
|---|---|
ArgumentNullException |
request is null. |
AuthenticationRequiredException |
The service redirected to /login or /authenticator and credentials was null. |
AuthenticationFailedException |
Login page unavailable, credentials rejected, or the login branch ran 3 times without stabilising. |
TwoFactorAuthenticationException |
TOTP rejected, secret empty or not Base32, required time zone missing, or the 2FA branch ran 3 times without stabilising. |
AccountConfigurationException |
The service redirected to /profile. |
CsrfTokenNotFoundException |
A page fetched during authentication contained no <meta name="csrf-token" content="…">. |
RateLimitExceededException |
429 with WaitForRateLimitReset = false, or retries exhausted while backing off from a 429 when no reset instant is known. |
ReactShieldBypassException |
The challenge payload could not be decrypted, or the service kept serving the challenge after 3 solved tokens. |
LogsParserHttpException |
Any other non-success status code, or a transient failure that survived MaxRetryAttempts (the original exception is the InnerException). |
OperationCanceledException |
The token was cancelled, or the retry loop ended without producing content. |
All of these except ArgumentNullException and OperationCanceledException derive from LogsParserException — see
Exceptions.
public int RateLimitMax { get; private set; }public int RateLimitRemaining { get; private set; }public DateTimeOffset? RateLimitReset { get; private set; }They are declared on the class, not on ILogsParserDataSource, so reading them requires a reference to the concrete
type rather than the interface.
public void Dispose()Disposes the internally created HttpClient (and, through disposeHandler: true, its handler). If an HttpClient
was injected, Dispose() does nothing to it.
Warning — always set
AllowAutoRedirect = false. The entire authentication protocol is read off302responses: the library inspectsLocationand decides whether to log in, re-confirm the second factor or fail. A handler that follows redirects automatically resolves those302s insideHttpClient, so the library never sees them. The call then succeeds and returns the HTML of the login page as if it were content — no exception, no warning, just parsers that find nothing. The default, library-created handler setsAllowAutoRedirect = false; every injected client must do the same.
The usual reason to inject a client is a proxy:
using System.Net;
using LogsParser;
using LogsParser.Models;
using LogsParser.Net;
var handler = new HttpClientHandler
{
AllowAutoRedirect = false, // mandatory
AutomaticDecompression = DecompressionMethods.All,
Proxy = new WebProxy("socks5://127.0.0.1:1488"),
UseProxy = true
};
using var httpClient = new HttpClient(handler)
{
Timeout = TimeSpan.FromSeconds(60)
};
using var dataSource = new LogsParserHttpDataSource(
credentials: new LogsParserCredentials("my_login", "my_password", "BASE32SECRET"),
options: new LogsParserHttpOptions { MaxRetryAttempts = 3 },
httpClient: httpClient);
var client = new LogsParserClient(dataSource);Here BaseAddress, User-Agent and Accept are still taken from the options, because this client sets none of
them. Setting httpClient.BaseAddress yourself would make LogsParserHttpOptions.BaseUri inert. httpClient is
disposed by the using in this snippet, not by the data source.
There is no LoginAsync, no SignInAsync and no "connect" step. Authentication happens because a data request came
back as a redirect: the data source recognises the target, runs the matching flow, and then retries the original
request. The first call in a process therefore transparently performs the login, and later calls do nothing extra
until the service says otherwise.
Applies to a 302 Found on the requested URI, keyed on Location.AbsolutePath:
Location |
What the library does |
|---|---|
/login |
Full login. GET /login, read the token out of <meta name="csrf-token" content="…">, POST /login with _token, name and password, then fall straight through into the TOTP confirmation. A response that redirects back to /login means the credentials were rejected → AuthenticationFailedException. |
/authenticator |
TOTP re-confirmation only. GET /authenticator, take a fresh CSRF token, POST /authenticator with _token and code. The password is not re-posted — this is the mid-session path where the first factor is still valid but the 2FA confirmation window lapsed. A response that redirects back to /authenticator means the code was rejected → TwoFactorAuthenticationException. |
/profile |
AccountConfigurationException. The account exists but is not set up on the site; no retry can fix it. |
If credentials were not supplied, the /login and /authenticator branches both throw
AuthenticationRequiredException before doing any work.
One detail of the 2FA branch: if GET /authenticator itself answers with a redirect, the form is not being served —
either the first factor is gone (/login) or the session is already confirmed — so nothing is submitted and the
original request is simply retried, which re-enters the state machine with the new signal.
The /login and /authenticator branches share one counter with a hard-coded bound of 3. Exceeding it throws
AuthenticationFailedException from the login branch and TwoFactorAuthenticationException from the 2FA branch.
The bound protects against a service that keeps redirecting to a login flow which reports success.
The library implements RFC 6238 itself: Base32 decode → HMAC-SHA1 over the 30-second counter → dynamic truncation →
a 6-digit code. It consumes a Base32 secret string and nothing else — QR images, otpauth:// URIs and Google
Authenticator migration payloads are not parsed; extract the secret yourself and pass it as
LogsParserCredentials.TotpSecret.
var credentials = new LogsParserCredentials(
Login: "my_login",
Password: "my_password",
TotpSecret: "GEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQ");The code is generated against the site's own time zone, resolved as America/Juneau with the Windows identifier
Alaskan Standard Time as a fallback. When neither identifier exists on the host, generation throws
TwoFactorAuthenticationException rather than quietly falling back to UTC, so a container without time-zone data
fails loudly instead of producing codes the site rejects. An empty secret and a secret containing non-Base32
characters throw the same exception.
Three independent bounds are at work inside one GetContentAsync call:
| Bound | Value | Applies to |
|---|---|---|
LogsParserHttpOptions.MaxRetryAttempts |
5 by default |
Transient failures — arbitrary exceptions from the send, and 429 backoff when no reset instant is known. |
| Authentication attempts |
3, hard-coded |
Shared by the /login and /authenticator branches. |
| Challenge attempts |
3, hard-coded |
React challenge solving. |
Transient failures back off exponentially: after the n-th failure the loop waits 2^n seconds (2 s, 4 s, 8 s, …)
before sending again. When the attempts are used up, the last exception is wrapped in a LogsParserHttpException
whose message names the URI and the attempt count.
Two things deliberately do not consume a retry attempt: solving a React challenge, and waiting out a rate limit
when the reset instant is known. Both are expected, self-correcting states rather than failures, and charging them
against MaxRetryAttempts would let a busy period exhaust the budget before a real error ever occurred.
Any exception deriving from LogsParserException is rethrown immediately and is never retried. That includes
every exception in the table above. A new domain exception that does not derive from LogsParserException would be
treated as a transient failure and silently retried MaxRetryAttempts times.
Cancellation is also never retried: OperationCanceledException propagates as-is.
Every response to the requested URI updates three properties from response headers:
| Header | Property | Type | Notes |
|---|---|---|---|
X-Ratelimit-Limit |
RateLimitMax |
int |
Requests allowed per window. |
X-Ratelimit-Remaining |
RateLimitRemaining |
int |
Requests left in the current window. |
X-Ratelimit-Reset |
RateLimitReset |
DateTimeOffset? |
Unix seconds; applied only when the value parses and is greater than zero. |
A header that is absent or unparseable leaves the corresponding property at its previous value, so the properties always report the last observation rather than resetting to zero.
On 429 Too Many Requests:
-
WaitForRateLimitReset = true(default). IfRateLimitResetis known, the loop sleeps until that instant plus one second and retries without consuming a retry attempt. If it is not known, it falls back to theRetry-Afterheader, or to2^(attempt+1)seconds when that header is missing too — and this fallback path does consume a retry attempt, throwingRateLimitExceededExceptiononce they are exhausted. -
WaitForRateLimitReset = false. ThrowsRateLimitExceededExceptionimmediately.
RateLimitExceededException (namespace LogsParser.Exceptions) carries the wait it would have used:
| Property | Type | Description |
|---|---|---|
RetryAfterSeconds |
int |
Seconds resolved from Retry-After, or the exponential fallback. |
ResetAt |
DateTimeOffset? |
The RateLimitReset value at the moment of the failure, when known. |
A polite client checks the remaining budget between calls instead of waiting for a 429:
using var dataSource = new LogsParserHttpDataSource(
credentials: credentials,
options: new LogsParserHttpOptions { WaitForRateLimitReset = false });
var client = new LogsParserClient(dataSource);
foreach (var serverId in serverIds)
{
if (dataSource.RateLimitRemaining <= 1 && dataSource.RateLimitReset is { } resetAt)
{
var pause = resetAt - DateTimeOffset.UtcNow + TimeSpan.FromSeconds(1);
if (pause > TimeSpan.Zero)
{
await Task.Delay(pause);
}
}
var page = await client.GetLogsAsync(new LogsQuery(ServerId: serverId));
Console.WriteLine($"{serverId}: {page.Entries.Count} entries, {dataSource.RateLimitRemaining}/{dataSource.RateLimitMax} left");
}The site sits behind a React/vDDoS shield that occasionally answers a normal request with 200 OK and a page of
obfuscated JavaScript instead of content. The page contains an AES-128-CBC puzzle; a real browser runs the script,
derives a token, stores it as a cookie and reloads.
The library does the same thing, and it does it automatically — there is no API to call and no option to enable:
-
Detection. A
200response is treated as a challenge only when itsserverheader equalsnginxand itscache-controlheader equalsno-cache, and the body contains/vddosw3data.jsorPlease turn JavaScript on and reload the page.These literals mirror the live service. - Solving. The AES key, IV and payload are recovered from the script and the block is decrypted. The preferred path reads them from the obfuscated indexed array; if that shape does not match, structurally plausible triples of hex values are tried in turn.
-
Storing. The resulting token is written into the same cookie storage as the session cookies, under the exact
name
R3ACTLB. Persisting the storage therefore persists the shield token together with the session — see Cookie Storage. - Retrying. The original request is sent again with the new cookie, without consuming a retry attempt.
Nothing in the decrypted plaintext identifies it as the correct token, so a solved token cannot be validated before
it is used: a wrong one simply produces another challenge. That is exactly why the challenge branch carries its own
bound of 3 — without it, a service that kept re-issuing the challenge would spin forever. On the fourth challenge
the call throws ReactShieldBypassException instead. The same exception is thrown when the payload cannot be
decrypted at all, or when the page yields fewer than three distinct string literals to try.
ReactShieldBypass itself is an internal implementation detail and is not part of the public API.
| Symptom | Cause | Fix |
|---|---|---|
| The call succeeds but the content is the login page; parsers return empty results | An injected HttpClient whose handler follows redirects — the library never sees the 302s |
Set AllowAutoRedirect = false on the handler |
AuthenticationRequiredException |
The service asked for a session and credentials was null
|
Pass LogsParserCredentials to the constructor, or via Dependency Injection
|
AuthenticationFailedException on the first request |
Login or password rejected (redirect back to /login), or the login page did not load |
Verify the credentials against the site in a browser |
TwoFactorAuthenticationException mentioning time zones |
Neither America/Juneau nor Alaskan Standard Time is installed on the host — common in slim Linux containers |
Install the tz database (for example the tzdata package) |
TwoFactorAuthenticationException on a rejected code |
Wrong Base32 secret, or host clock drift | Re-copy the secret; synchronise the system clock |
AccountConfigurationException |
The service redirected to /profile: the account is not configured on the site |
Finish the account setup in a browser; retries cannot help |
ReactShieldBypassException |
The shield kept re-issuing the challenge, or its payload changed shape | Retry later; if it persists, the challenge format has moved and the solver needs updating |
Calls stall for long stretches with Warning logs about waiting |
429 responses with WaitForRateLimitReset = true — this is the intended behaviour |
Lower the request rate, or set WaitForRateLimitReset = false and handle RateLimitExceededException
|
LogsParserHttpException wrapping a TaskCanceledException
|
Network or HttpClient.Timeout failures retried to exhaustion |
Raise the timeout, check the proxy, inspect the InnerException
|
| Nothing is logged at all | No logger factory was installed | Pass loggerFactory to the constructor — see Logging
|
Turning the log level up to Trace shows every send with its status code, the cookie names in play and the
rate-limit counters, which is usually enough to tell these cases apart.
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
- Внедрение зависимостей
- Логирование
- Исключения