Skip to content

M2 Pro 4.0.2

Choose a tag to compare

@jriko9 jriko9 released this 30 Jun 18:33
· 9 commits to main since this release
1fd1c48

OBM2 Pro — CHANGELOG v4.0.2

⚠️ Binary removed for security reasons — download the latest release: https://github.com/ObM2/ObM2Pro/releases/latest
Post-4.0.1 release. Headline work: full migration of the HTTP engine from the
legacy internal Extreme.Net library to RuriLib.Http (OpenBullet 2), done
incrementally behind a flag and then completed by deleting Extreme.Net entirely.
Existing configs keep working unchanged.


Feat — GenerateGUID function: version (V4/V7) + format (D/N/B/P)

Aligned the GenerateGUID block with OpenBullet 2. It used to emit only a v4 GUID
in the fixed D format; it now exposes Version (V4 random / V7 time-ordered) and
Format (D dashes, N compact, B braces, P parentheses). New GuidVersion/GuidFormat
enums and properties in BlockFunction.cs, parsed/serialized in FromLS/ToLS
(backward-compatible: old configs without params stay V4/D). Since RuriLib targets .NET 8
(where Guid.CreateVersion7() doesn't exist — .NET 9+), v7 is built by hand per RFC 9562
(48-bit ms timestamp + version/variant bits + random, with byte reorder for the textual
layout). UI: new tab in PageBlockFunction with the two ComboBoxes + i18n keys
Blk_GuidVersion/Blk_GuidFormat (en/it/es).


Fix — ULP to Combo: HTML sources, dirty tag output + lost combos

The ULP to Combo tool on HTML-marked files produced correct mails but wrong passwords
(truncated digits) and skipped good lines. Cause: the regex required mail:password with the
password right after the colon, but in HTML sources a tag sits between mail and password
(mail@x.com:<span>pass</span>), so the match failed or grabbed a numeric token further along.
Fix in UlpTools.xaml.cs: strip HTML tags before parsing (StripHtml); mail/password separator
is now a [\s:=;|]+ class instead of just :; password is [^\s<>]+; ExtractCombos() with
Regex.Matches pulls every pair on the line. Output is clean username:pass, one per line,
backward-compatible with classic combos. No UI change.


Fix — TLS Request: non-ASCII invisible chars in headers → send error

Custom headers pasted from devtools/formatted sources could contain invisible non-ASCII chars
(NBSP U+00A0, zero-width U+200B/C/D, BOM U+FEFF) in place of spaces — values that look ASCII but
failed sending with HttpRequestException: Request headers must contain only ASCII characters.
(.NET validates ASCII only at send, after TryAddWithoutValidation). Added
BlockTlsRequest.CleanHeaderToken(): NBSP→space, zero-width/BOM removed, other chars >0x7F
dropped; applied to header name+value with a log warning when it cleans something.

The same error could come from the URL: the resolved URL is carried in the x-tls-url
header to the TLS proxy, so a non-ASCII char in the URL made sending fail looking like a header
problem. Added SanitizeUrlForHeader(): strips zero-width/BOM and percent-encodes every char

0x7F in UTF-8 (valid URL, no data loss), applied to localUrl before the x-tls-url header.


Security/Fix — License dialog: no license-server hostname leak + localized messages

On an unreachable backend (network down, 403, DNS), the license activation dialog showed a raw
ex.Message that included the license server hostname (the raw "No such host" error exposed the
backend host:port). Added LicenseGate.ScrubHost(), which
replaces the backend URL/host with [license server]: the user still sees the network error
detail but not the server name. All outcome messages (previously hardcoded in Italian inside
LicenseGate) are now localized in the 3 dictionaries via a thread-safe L() helper (Dispatcher
for the off-UI heartbeat). New keys Dialog_License_NoLicenseFound/Valid/ServerUnreachable/ BadResponse/BadSignature/WrongInstance/ReplayNonce/ReplayExpired.


UI — VPN Proxy tool: modern Tool restyle

IpVanishTool.xaml rewritten from the old 3-column Grid layout to the standard Tool UserControl
pattern (like Convert Tool): header icon+title+subtitle, credentials card with violet primary
Login + secondary actions, Log card (dark monospace green RichTextBox, RtbContextMenu, VS-style
scrollbar) and a generated-proxies card separated by a GridSplitter. Code-behind unchanged (all
x:Name and handlers preserved). New i18n keys
Tools_IPVanish_Title/Subtitle/Credentials/Actions/GeneratedProxies.


Fix — Runner: settings not saved / empty runner on restart

Two distinct causes, both fixed:

1. Runner session wipe (non-atomic persistence). SaveSession() did _repo.RemoveAll()
(DropCollection) then _repo.Add() over two separate Shared LiteDB connections: if the insert
failed, the collection stayed empty and all runners vanished. Added
LiteDBRepository.Replace(IEnumerable) which clears and re-inserts inside a single transaction
(DeleteAll+InsertBulk+Commit/Checkpoint); SaveSession now materializes the list and uses
Replace.

2. Restore abandoning the runner when the wordlist file was missing. With a wordlist on a
disconnected external drive, FileToWordlist (File.ReadLines().First()) threw → the runner lost
config/wordlist and on the next save the path was overwritten with an empty string (progress reset
to zero). Added WordlistManagerViewModel.FileToWordlistSafe, which keeps the path without reading
the file; RestoreSession no longer throws: it keeps config, wordlist/cookie path and
StartingPoint even with files temporarily absent.


UI — Fixed-height Custom Headers/Cookies with grip (OB2 style)

In the Request and TLS Request blocks the Custom Headers and Custom Cookies
RichTextBoxes no longer auto-grow with content. They now have a fixed height (80/100 px) with
vertical scroll and a bottom-right grip to resize height only (width fixed), like OpenBullet 2
textareas. New VResizeGripThumb style in App.xaml (Thumb, SizeNS cursor); ResizeGrip_DragDelta
handlers in PageBlockRequest.xaml.cs and PageBlockTlsRequest.xaml.cs (drag → rtb.Height, capped
at MinHeight→600); each RichTextBox wrapped in a Grid with an overlaid bottom-right Thumb.


New block — Cap Solve (Cap.js PoW captcha)

Added the Cap Solve stacker block (CAPSOLVE), which solves Cap.js proof-of-work captchas
(two-POST challenge) headless, with no browser.

Full flow: the block does the first POST to the Challenge URL, receives
{challenge{c,s,d}, token, instrumentation}, solves the PoW in native C#, reproduces the anti-bot
state with Jint, then does the second POST to the Redeem URL and stores the final token in
<SOLUTION> (plus <CAP_EXPIRES>).

  • Cap.js PoW: salt/target derived from PRNG(FNV1a + xorshift32) over the token; nonce search
    where SHA256hex(salt+nonce).startsWith(target). Native C# port (verified 80/80 against a real
    challenge).
  • Instrumentation (anti-bot): the obfuscated script served by the server (raw deflate) runs ~30
    anti-headless checks (Node.js detector via process/global/require/module, webgl SwiftShader,
    node:internal stack, eval/Function.toString tampering, Object.prototype.toString tag) and
    produces 4 deterministic state numbers. Run with Jint 4.9.2 (already a dependency) in a
    global with DOM/navigator/Symbol.hasInstance/toString-tag stubs — it passes the gate and
    reproduces the exact state. No WebView2: runs on any runner bot thread.
  • Params: Challenge URL, Redeem URL, Challenge Body (opt.), Custom Headers, User Agent, Use Proxy,
    Output Variable (default SOLUTION), Is Capture. LoliScript round-trip verified.

Registration: BlockParser (CAPSOLVE), BlockCatalog (Captchas category), MainWindow mapping
(satellite BlockPage), red ShieldKeyOutline icon. File: RuriLib/Blocks/BlockCapSolve.cs.

Fixes found in testing (validated end-to-end against a live server):

  • MultiLine in LoliScript: Challenge Body and Custom Headers contain real newlines that
    broke the LS line (ERROR: Expected Literal value for 'HEADERS'). Fix: newlines encoded as \n
    on a single line in ToLS, restored in FromLS (EncodeNl/DecodeNl).
  • Compressed response: without AutomaticDecompression, gzip/br responses arrived as raw bytes
    → JObject.Parse failed with "Unexpected character… position 0". Fix:
    HttpClientHandler.AutomaticDecompression = GZip|Deflate|Brotli + default Accept: application/json.
  • Diagnostic log: each POST prints status, N bytes, first 300 chars; JSON parse with a clear
    message (empty / non-JSON / anti-bot block) instead of Json.NET's cryptic error.

Usage notes:

  • Challenge Body = first POST payload (empty if the challenge has no body).
  • Custom Headers = headers shared by both POSTs (Origin/Referer/x-*). Do not put User-Agent
    here: use the dedicated User Agent field (used both in HTTP headers and in navigator.userAgent
    inside Jint). Content-Type: application/json is automatic.
  • Use Proxy = local gate data.UseProxies && UseProxy && data.Proxy != null (same scheme as
    SolveCaptcha).

Runner — fix crash "Index was outside the bounds of the array"

On Runner start the error "The Master Worker has encountered an error: Index was outside the bounds
of the array"
happened often.

Cause: System.Random is not thread-safe. A single shared instance
(RunnerViewModel.random) was used in parallel by all the runner's bots in blocks (RandomNum,
RandomString, etc.). Concurrent access corrupts Random's internal SeedArray and produces
IndexOutOfRangeException. The lock(randomLocker) only covered BotData construction, not the
Random use inside blocks on bot threads → race on every multi-bot run.

Fix: each bot gets its own Random instance, uniquely seeded
(Guid.NewGuid().GetHashCode() ^ id ^ TickCount) at BotData creation. No cross-thread sharing → no
corruption. (this.random remains only as a historical constructor fallback, no longer passed to
bots.) File: RuriLib/Runner/RunnerViewModel.cs.


Home dashboard — Memory card + resource monitor

  • Fix flat memory line: Process.WorkingSet64 is cached → without Process.Refresh() the value
    stuck at the first sample (flat sparkline). Added Refresh() per tick.
  • Memory card redesigned OB2-style: the sparkline is replaced by a progress bar (track + fill)
    showing the M2 process memory (WorkingSet) as a fraction of total system RAM, with usage
    percentage and used MB / total MB detail (e.g. 341 MB / 32,687 MB · 1.0%). The bar is
    measured on the process, not the system.
  • ResourceMonitor/ResourceStats: new MemoryPercent and TotalSystemMemoryMB fields (total RAM
    via GC.GetGCMemoryInfo().TotalAvailableMemoryBytes, computed once).
  • CPU: the measure is correct (cpuDelta / (nCore × wallDelta) × 100 = process % normalized over
    cores); at idle it really is ~0%. No change to the calculation.

File: OpenBullet/Services/ResourceMonitor.cs, OpenBullet/Views/Main/Home.xaml(.cs).


HTTP Engine — Extreme.Net → RuriLib.Http migration

The old internal Extreme.Net fork (a fork of xNet, referenced by RuriLib) has been completely
replaced
by RuriLib.Http 1.0.1 + RuriLib.Proxies 1.0.2 (NuGet; last series compatible
with net8 — 2.0.0 requires net10). Carried out in phases (PoC → proxy adapter → engine wrapper →
selectable branch → residual cleanup → removal) to keep every build green.

Why

  • Header order control on the wire (anti-bot signature) via ProxyClientHandler.
  • Native SOCKS4/4a/5 support — no more transport fallbacks.
  • Cookies captured reliably via CookieContainer.
  • Drops a large, unmaintained internal library in favour of the OB2 stack.

HTTP Request block (REQUEST)

  • New engine wrapper RuriRequest (ProxyClientHandler + System.Net.Http.HttpClient): preserves
    header order, native SOCKS, cookies via CookieContainer, gzip/deflate auto-decompression.
  • The HTTP library selector no longer lists ExtremeNet (the library no longer exists). The
    dropdown now offers only RuriLibHttp and SystemNet. The ExtremeNet enum value is kept
    internally for .loli parse compatibility, but any config using it is auto-migrated to
    RuriLibHttp
    — both at parse time (FromLS) and on block open. Old configs keep working.
  • The System.Net branch's SOCKS fallback now routes to RuriLib.Http (native SOCKS) instead of
    the removed Extreme.Net path.

TLS Request block (TLSREQUEST)

  • Added the HTTP library selector to the TLS Request block too (inherits from BlockRequest).
    Here the library only governs the fallback engine used when local tls-proxy.exe is
    unreachable — the normal path always goes through the TLS proxy. Excludes ExtremeNet and
    auto-migrates it to RuriLibHttp.
  • HttpLibrary is now serialized in the TLS block LoliScript (HTTPLIB token) — previously it
    was inherited but never saved, so the choice was lost on reload.

Engine internals (validated by PoC)

  • Uses ProxyClientHandler + HttpClient, not RLHttpClient: the latter preserves header order
    but drops Set-Cookie (Headers is a Dictionary<string,string>), while ProxyClientHandler
    gives header order and cookies and SOCKS.
  • UseCookies = true + CookieContainer is mandatory (with false, ProxyClientHandler 1.0.1 throws
    NullReferenceException).

Other migrated consumers

  • Download block / Download.RemoteFile → ProxyClientHandler + HttpClient.
  • BASIC OCR / OCR blocks — image-from-URL download → HttpClient + CookieContainer (cookies
    merged back into the bot jar); the second OCR block now uses RuriRequest.
  • Runner — webhook hit POST and remote proxy source download
    (GetProxiesFromRemoteSource sync/async) → HttpClient.
  • Add Proxies dialog (download list from API URL) → HttpClient.
  • VPN Proxy tool login flow → HttpClient + shared CookieContainer (per-request headers via
    HttpRequestMessage). Transport-only refactor; does not fix the VPN provider's server-side login changes.
  • UserAgent — random UA generators ported into RuriLib (UaGenerator), no longer depend on
    Extreme.Net.Http.

Removal (Extreme.Net deleted)

  • New native enums RuriLib.Enums.ProxyType and RuriLib.Enums.HttpMethod, member names identical
    to the old ones (serialized by name in .loli — not renamed).
  • Request.cs reduced to shared types only (SecurityProtocol enum, MultipartContent struct); the
    Extreme-based Request class removed, GenerateMultipartBoundary moved into RuriRequest,
    IsExtremeNetReserved → RuriRequest.IsReservedHeader.
  • CProxy.GetClient() (+ chain helpers) removed; GetRuriClient() is now the only factory.
  • CookieDictionary (Extreme type) → Dictionary<string,string> for GlobalCookies.
  • using Extreme.Net swapped to using RuriLib.Enums across ~30 files.
  • Removed the Extreme.Net ProjectReference from RuriLib, removed the project from the solution,
    deleted the project folder; the diagnostic tools/PoC.RuriHttp removed too.
  • Build: 0 errors; RuriLib.Tests: 34 green (proxy factory mapping + LoliScript round-trip).

Networking / Fixes

  • RuriRequest.Perform timeouts now surface cleanly. Send/read used .Result, wrapping timeouts
    in AggregateException. Switched to .GetAwaiter().GetResult() so the runner sees a clean
    TaskCanceledException (same as old Extreme.Net), classified as a normal fail/retry.
  • System.Net (HttpClient) branch — same unwrap. All Task.Run(...).Result calls in
    BlockRequest → .GetAwaiter().GetResult(), so a dead proxy logs a clean
    HttpRequestException/SocketException instead of AggregateException.
  • ProxyClientHandler TLS callback crash fixed. Setting
    ServerCertificateCustomValidationCallback threw InvalidOperationException: 'RemoteCertificateValidationCallback' option was already set in the SslStream constructor —
    ProxyClientHandler 1.0.1 already sets it internally. Removed our setter in RuriRequest and
    Download.
  • Content-Type with parameters (e.g. ; charset=utf-8) no longer breaks the request. Configs
    with Content-Type: application/json; charset=utf-8 failed with ERROR: The format of value '...' is invalid. RuriRequest set the content type via the StringContent/MediaTypeHeaderValue
    constructor, which accepts only the bare media type and throws FormatException on the
    parameter. Now a helper uses MediaTypeHeaderValue.TryParse with a TryAddWithoutValidation
    fallback. Applies to Standard and Raw bodies. The System.Net branch had the same bug — fixed
    with a shared SetSystemNetContentType helper. Works on both engines.

Proxy Manager / Fixes

  • Country resolution no longer freezes the UI at the end of a check. GeolocateBatchAsync called
    Update() once per proxy, and LiteDBRepository.Update(T) opens/closes the LiteDB file on every
    call → ~1000 open/close cycles on the UI thread. Countries are now assigned in the loop
    (PropertyChanged only) and persisted in a single bulk Update(IEnumerable) off the UI thread.
  • Send to Debugger now includes proxy credentials. The "Send proxy to Debugger" right-click sent
    only host:port, dropping user/pass of authenticated proxies. Now, if the proxy has credentials
    (NeedsAuthentication), it's sent as host:port:username:password.
  • Authenticated SOCKS5 check now works. The checker used HttpClient+WebProxy, which does NOT
    support SOCKS5 user/pass auth → all authenticated SOCKS5 came back "not working". For SOCKS proxies
    the check now uses RuriLib.Http.ProxyClientHandler (same engine as the runner).

Tools — VPN Proxy (reactivated)

  • Old VPN Proxy tool restored and wired into the sidebar (Page→UserControl, obsolete FontAwesome icons
    → Material). Login rewritten on the current flow (Cognito InitiateAuth Bearer + API v4 JSON:
    proxy-credentials, server/list/socks5, subscription), all requests routed via tls-proxy.exe
    (without TLS fingerprint → 403). Generates the 18 SOCKS5 *.socks.ipvanish.com:1080.

Backward Compatibility

  • Existing configs are unaffected. The ExtremeNet value is kept in the enum for .loli parse
    compatibility but is no longer shown in the UI and is auto-migrated to RuriLib.Http; configs that
    default to ExtremeNet (no HTTPLIB token) also keep working.
  • Proxy type names (Http/Socks4/Socks4a/Socks5/Chain) and HTTP method names are unchanged, so
    serialized configs round-trip identically.
  • Removed: proxy chaining (ChainProxyClient) has no equivalent in RuriLib.Proxies — only the
    current proxy node is used (documented gap, was already rarely used).

Notes (TLS fingerprint / curl-impersonate)

  • RuriLib.Http 1.0.1 is a managed client: header order + gzip/deflate only, no TLS fingerprint
    impersonation. Unchanged vs Extreme.Net (which had none either).
  • Real TLS impersonation in M2 continues to come from the external tls-proxy.exe (TLS Request
    block), independent of the HTTP engine.
  • RuriLib.Http 2.0.0 adds native curl-impersonate + brotli/zstd, but targets .NET 10 — out
    of reach until M2 moves off net8. Future option.

Runner Manager / UI

  • New in-line List view, toggle in the toolbar. A second display mode beside the cards: a compact
    table (#, Status, Configuration, Proxies, Bots, CPM, Progress, Hits, Custom,
    To Check, Data Pool, actions ⋮). Two toolbar toggle buttons (grid / list) switch view; the
    choice is persisted in obSettings (General.RunnerManagerViewMode) and restored at boot.
  • Built with ItemsControl + row Border, not a ListView: fixed Grid columns, two *
    columns (Configuration + Data Pool) keep the ⋮ button anchored far right. Rows are never
    selected
    — only a hover highlight; left-click opens the Runner. Colors use the M2 dynamic
    resources.
  • New i18n keys (en/it/es): Runner_Col_Status, Runner_Col_DataPool, Runner_View_Card,
    Runner_View_List; existing Stat_* keys reused.
  • Dead-code cleanup. Removed the never-wired per-item click handlers in RunnerManager.xaml.cs
    (selectConfig_MouseDown, selectWordlist_MouseDown, selectProxies_MouseDown,
    selectBots_MouseDown) and their stale XAML MouseDown bindings.

OpenBullet 2 block/function coverage (config parity)

Closed the real gaps vs OpenBullet 2 2.0.0 (excluding browser-automation:
Puppeteer/Selenium/playwright).

FUNCTION block — new functions

  • JWT (Create/Verify, HMAC HS256/384/512) — helper Crypto.JwtEncode/JwtDecodeVerify.
  • XOR (XOR cipher with repeated key, Base64/HEX output) — Crypto.XorCipher.
  • AWS4Signature (AWS Signature v4) — Crypto.Aws4Signature.
  • MaxInt/MinInt/MaxFloat/MinFloat and MergeByteArrays (second operand).
  • ShellCommand (run a command via cmd.exe, capture stdout).

PARSE block — XPATH

  • Implemented ParseType.XPATH (previously NotImplementedException): Parse.XPATH via
    HtmlAgilityPack (new pkg), XPATH pill+tab in the Parse UI, attribute/innerHTML/outerHTML/
    index/recursive support.

UTILITY block — Dictionary + extras

  • New Dictionary group: Create, Add, RemoveByKey, GetByKey, GetKeyByValue, Keys, Values.
  • New List actions: IndexOf, CreateListOfNumbers.
  • New BIGINT conversion (Bytes⇄big integer base-10).

TCP block — full read

  • New Read (read-only), ReadBytes, SendReadBytes (byte-array output) commands.

New blocks

  • SSH (SSH.NET): Connect (Password/None/PrivateKey), RunCommand, Disconnect.
  • DNS (System.Net.Dns): A / AAAA / Any / Reverse lookup → list.

SMTP block

  • New SendMailAdvanced command: CC, BCC, Reply-To, multiple attachments, HTML/plain body.

Not included

  • NodeJS interop (needs node.exe, analogous to BlockPython) — deferred.
  • Modern captchas (Turnstile/DataDome/GeeTest v4/...) — mapped as TODO only; depend on external
    captcha provider support.

Bugfix — LS Code block disabling (historical OB1/M2 bug)

The LS Code block could not be disabled: the toggle had no runtime effect (the script ran anyway)
and the block didn't grey out live.

Cause: BlockLSCode.ToLS returned the raw script ignoring Disabled → no ! prefix, so at
runtime (config runs as LoliScript text where IsEmptyOrCommentOrDisabled skips !-prefixed lines)
the lines always executed. Also, StackerBlockViewModel.Disable()/DisableColor() had a dedicated
early-return for BlockLSCode (historical exclusion): the toggle set neither Block.Disabled nor
the colour. Grey appeared only after a ToLS→FromLS round-trip.

Fix: ToLS prefixes ! to every non-empty line when Disabled; FromLS round-trips (if all
non-empty lines start with !, mark Disabled = true and strip one for a clean editor view);
StackerBlockViewModel exclusion guards removed, so the LS block disables live like the others.
File: RuriLib/Blocks/BlockLSCode.cs, OpenBullet/ViewModels/StackerBlockViewModel.cs.


Bugfix + UI — CurrentUnixTime: unit as ComboBox

The CurrentUnixTime block failed with ERROR: Object reference not set to an instance of an object if the unit was lowercase (milliseconds...). Process() resolved the method via
case-sensitive reflection GetMethod("ToUnixTime" + Input): only capitalized Seconds/Milliseconds
matched, any other casing returned null → NRE on .Invoke. Root cause was a free text field.

Fix: Input changed from string to enum UnixTimeUnit (Seconds, Milliseconds,
Microseconds, Nanoseconds) with ParamType = BlockParamType.Enum → the UI auto-generates a
ComboBox. Process() now uses a switch (no reflection, no NRE). FromLS parses with
Enum.TryParse(ignoreCase: true) + Seconds fallback for legacy/invalid tokens; ToLS unchanged
(100% backward-compatible). Bonus: Microseconds and Nanoseconds now work too.
File: RuriLib/Blocks/BlockCurrentUnixTime.cs.


Captcha — Cloudflare Turnstile (OB2 2.0.0 parity) + selector crash fix

Added Cloudflare Turnstile support to the SolveCaptcha block, matching OB2 2.0.0. CaptchaSharp
2.1.0 (already referenced) exposes the method on the base CaptchaService, so no dependency
change — only wiring.

New Turnstile type

  • BlockSolveCaptcha: new case CaptchaType.CloudflareTurnstile in FromLS/ToLS/GetResponse. New
    CData and PageData properties. LS:
    SOLVECAPTCHA CloudflareTurnstile "siteKey" "siteUrl" "action" "cData" "pageData".
  • GetResponse: calls SolveCloudflareTurnstileAsync(...). Process: new CloudflareTurnstileResponse
    case → token in <SOLUTION> + solver User-Agent in <CF_USERAGENT> (reuse it in the site
    submit).
  • UI PageBlockSolveCaptcha: new tab with the 5 fields; i18n keys Blk_CData/Blk_PageData.

Captcha selector latent-crash fix (2.1.0 upgrade regression)

The captcha-type combobox listed all 24 values of CaptchaSharp 2.1.0's CaptchaType enum, but the
code-behind mapped only 9 types via a Dictionary → selecting an unhandled type (DataDome, GeeTestV4,
AmazonWaf, etc.) threw KeyNotFoundException and crashed the page. Rewritten with a SupportedTypes
array (order == tab order): the combobox lists only types with a tab, combobox index maps 1:1 to tab
index, no dict, no crash. Bound by enum value, not index.

BlockBypassCF — output as named variable (OB2 parity)

OB2 writes the Challenge Page result into a capturable Output variable; M2 wrote only the
cf_clearance cookie. Added VariableName + IsCapture properties (auto-generated fields).
Process now writes the value both to the cf_clearance cookie (unchanged) and to a
named/capturable CVar if VariableName is set. LS extended with an optional arrow:
BYPASSCF "url" "html" (UseProxy=True) "ua" -> CAP "cfClearance". Backward-compatible (old BYPASSCF
without a variable stays identical). OB2 SafeMode NOT ported (M2 has no per-block runtime exception
handling). File: RuriLib/Blocks/BlockBypassCF.cs.

BlockSolveCaptcha — IsCapture on the solution (OB2 parity)

In OB2 every captcha block has an Is Capture toggle marking the solution variable as capturable
(logged among hits). M2's cumulative SolveCaptcha inserted variables with isCapture hardcoded
false
→ the solution was never capturable. Added IsCapture property + UI checkbox. Process
now passes IsCapture to the solution variables (SOLUTION, GT_CHALLENGE/GT_VALIDATE/
GT_SECCODE
, Turnstile token); CAPTCHAID and CF_USERAGENT stay metadata. Serialized as
Boolean IsCapture in trailing LS tokens (backward-compatible).

Captcha — DataDome + GeeTest v4 (OB2 2.0.0 parity)

Two more modern types added to SolveCaptcha, same scheme as Turnstile (methods already exposed by
CaptchaSharp 2.1.0):

  • DataDome (CaptchaType.DataDome): new CaptchaUrl property; SiteUrl/CaptchaUrl fields.
    SolveDataDomeAsync(...) returns StringResponse → output in <SOLUTION> (valid datadome
    cookie).
  • GeeTest v4 (CaptchaType.GeeTestV4): new CaptchaId property; CaptchaId/SiteUrl fields.
    SolveGeeTestV4Async(...) returns GeeTestV4Response → new case exposing
    <GT4_CAPTCHAID>/<GT4_LOTNUMBER>/<GT4_PASSTOKEN>/<GT4_GENTIME>/<GT4_CAPTCHAOUTPUT>.
    Respects IsCapture.
  • UI: 2 new tabs + extended SupportedTypes; i18n keys Blk_CaptchaUrl/Blk_CaptchaId.

BlockSolveCaptcha — renamable Output Variable (OB2-style)

Added an Output Variable field (like OB2): new VariableName property + SolutionVar helper
(= VariableName if set, else "SOLUTION"). Renames the single-token solution variable (reCaptcha
v2/v3, hCaptcha, FunCaptcha, KeyCaptcha, Capy, Image, Text, Turnstile, DataDome) → recalled as
<chosenName>. Multi-output types (GeeTest v3/v4) and auxiliaries (CAPTCHAID, CF_USERAGENT) keep
fixed names. Serialized at LS end with the arrow as delimiter (... "ua" -> "myToken"); written
only if set, parsed only if present → backward-compatible.

BlockSolveCaptcha — per-solver info footer (OB2 gap)

In OB2 each solver is a separate block with its own info line. M2's cumulative block showed nothing
type-specific. Added a fixed non-scrolling footer (outside the ScrollViewer) showing the selected
type's description + output variables produced, updated in captchaTypeCombobox_SelectionChanged.
TypeInfo dictionary in the code-behind (EN). E.g. GeeTest v4 → "Output is a set (in order):
<GT4_CAPTCHAID>, <GT4_LOTNUMBER>, ...".

Captcha — low-priority types (full OB2 2.0.0 parity)

Added the last 9 CaptchaSharp 2.1.0 captcha types to SolveCaptcha, same scheme (all return
StringResponse → token in <SOLUTION>): AmazonWaf, LeminCropped, AtbCaptcha,
CutCaptcha, ReCaptchaMobile, CyberSiAra, FriendlyCaptcha, MtCaptcha,
TencentCaptcha. New dedicated properties + 9 UI tabs + extended SupportedTypes/TypeInfo.
AudioCaptcha NOT ported (different AudioCaptchaOptions signature, niche — deferred).

SolveCaptcha now exposes 21 types (the 2.1.0 enum has 23: the 2 excluded are
CloudflareChallengePage, covered by the separate BlockBypassCF, and AudioCaptcha, deferred).
Total coverage 22/23 — parity with OB2's separate blocks, only AudioCaptcha missing. The
2026-06-26-captcha-moderni-todo.md TODO is closed.


i18n + UI — License Activation dialog

The license activation dialog (LicenseActivationWindow) had 3 Italian strings hardcoded in the
code-behind — now localized, plus a channel link.

  • "Inserisci una license key.", "Verifica...", "Attiva" moved into the vocabularies (en/it/es)
    as Dialog_License_EmptyKey, Dialog_License_Verifying (reusing Dialog_License_Activate). The
    code-behind uses a Res(key) helper → Application.Current.TryFindResource with key fallback.
  • New Telegram channel link below the buttons for users without a license (Send icon +
    Hyperlink, localized Dialog_License_NoLicense, handler ChannelLink_RequestNavigate opens the
    default browser via Process.Start … UseShellExecute).

File: OpenBullet/Views/Dialogs/LicenseActivationWindow.xaml(.cs), 3× StringResources.


Builds on: CHANGELOG_v4.0.1.md.