From 6cf4dd13fed9318310efef03737b03b2748039a3 Mon Sep 17 00:00:00 2001 From: Joseph Doherty Date: Tue, 23 Jun 2026 13:20:39 -0400 Subject: [PATCH] =?UTF-8?q?gRPC=20events:=20decompile=20the=20stock=20mana?= =?UTF-8?q?ged=20client=20=E2=80=94=20confirms=20no=20hidden=20client-side?= =?UTF-8?q?=20difference?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Closed the blind spot the zero-rows conclusion had leaned on: prior cycles used wire capture (instrument-grpc-nonstream hooks only byte[] params), blind to gRPC metadata, interceptors, channel options. Read the stock managed source directly (histsdk-2023r2-analysis/decompiled/Archestra.Historian.GrpcClient + HistorianAccess; the pure-managed assemblies decompile cleanly though mixed-mode aahClientManaged crashes ILSpy). Findings: - GrpcClientBase.InitializeBase uses GrpcWebHandler (GrpcWebMode, HttpVersion 1.1) — the stock client speaks gRPC-Web over HTTP/1.1, the SAME transport as our SDK. This corrects the premise of hypothesis #1: there was never a native Grpc.Core HTTP/2 path to differ from; the stock client returning 50 rows is itself gRPC-Web. The HTTP/2 disproof's conclusion stands and is reinforced (identical transport on both sides). - m_metadata on every RPC (incl. StartEventQuery/GetNextEventQueryResultBuffer) is only grpc-internal-encoding-request: gzip — exactly our header set. The ClientInterceptor is a no-op (empty LogCall). So the "invisible per-connection metadata/header" blind spot is confirmed empty — no hidden client-side identity the byte[] capture missed. - CreateEventQuery/StartQuery/MoveNext are not in managed code; the managed GrpcRetrievalClient.StartEventQuery is a thin one-RPC stub. The query logic lives in the native C++/CLI HistorianClient core — consistent with the working-set being native/server-side. Every client-controllable layer is now confirmed identical by reading the stock source, not just by wire match: request bytes, transport, channel options, gRPC metadata, interceptor. The remaining difference is below the managed surface / server-side. Conclusion unchanged: gRPC event-row retrieval is auth-solved / retrieval-server-gated. Co-Authored-By: Claude Opus 4.8 (1M context) Claude-Session: https://claude.ai/code/session_01B6mcaT2PjRFKcogzp9UkfC --- .../grpc-event-query-capture.md | 45 ++++++++++++++++--- 1 file changed, 38 insertions(+), 7 deletions(-) diff --git a/docs/reverse-engineering/grpc-event-query-capture.md b/docs/reverse-engineering/grpc-event-query-capture.md index b39b510..9806dcc 100644 --- a/docs/reverse-engineering/grpc-event-query-capture.md +++ b/docs/reverse-engineering/grpc-event-query-capture.md @@ -420,13 +420,44 @@ live-server access reference. `Microsoft.Data.SqlClient`; authenticate with the server's SQL login, not the domain Historian acct — creds in the gitignored creds file). -**Conclusion (after #1 disproven + #4 answered).** Three independent angles are now exhausted: client -payload (byte-identical), transport (native HTTP/2 == gRPC-Web, both 0 rows), and data store (global, -unscoped, 71,332 events the engine serves via INSQL but withholds from our gRPC connection). The gate is -a **server-internal per-connection retrieval working-set** that a pure-managed client cannot reconstruct -by matching wire bytes, transport, or data. The remaining angles (#2 client-cert, #3 HTTP/2-frame -capture) are low-probability — #1 showed auth+registration succeed with no client cert over plain HTTP/2 -and the gate still holds. **gRPC event-row retrieval stands documented as auth-solved / +### Stock managed client decompiled (2026-06-23) — confirms no hidden client-side difference + +Closing the gap that prior cycles left: the zero-rows conclusion had leaned on **wire capture** +(`instrument-grpc-nonstream`, which only hooks `byte[]` params on `Grpc*Client` methods) — blind to gRPC +metadata/headers, interceptors, channel options, and any non-`byte[]` call. Read the **stock managed +client source directly** (`histsdk-2023r2-analysis/decompiled/Archestra.Historian.GrpcClient` + +`HistorianAccess`; the pure-managed assemblies decompile cleanly even though the mixed-mode +`aahClientManaged.dll` crashes ILSpy). Findings: + +- **`GrpcClientBase.InitializeBase` builds the same channel we do.** `GrpcWebHandler((GrpcWebMode)0, + HttpClientHandler)` with `HttpVersion = 1.1` — i.e. **the stock client speaks gRPC-Web over HTTP/1.1, + the same transport as our SDK.** This *corrects the premise of hypothesis #1*: there was never a native + `Grpc.Core` HTTP/2 path to differ from — the stock client that returns 50 rows is itself gRPC-Web. The + HTTP/2 disproof's *conclusion* stands (and is reinforced: identical transport on both sides). +- **`m_metadata` passed to every RPC (incl. `StartEventQuery`/`GetNextEventQueryResultBuffer`) is only + `grpc-internal-encoding-request: gzip`** — exactly our header set. No connection-id, session token, or + auth header rides in gRPC metadata. The **`ClientInterceptor` is a no-op** (`LogCall` is empty; both + unary overloads just invoke the continuation). So the "invisible per-connection metadata/header" blind + spot is **confirmed empty** — there is no hidden client-side identity the `byte[]` capture missed. +- **The event-read query orchestration is genuinely not in managed code.** `CreateEventQuery` / + `EventQuery.StartQuery` / `MoveNext` are not in the managed `HistorianAccess`; the managed + `GrpcRetrievalClient.StartEventQuery` is a thin one-RPC stub. The query logic lives in the native + C++/CLI `HistorianClient` core (the mixed-mode part ILSpy can't decompile) — consistent with the + working-set being native/server-side, not a managed step we could read and replicate. + +So **every client-controllable layer is now confirmed identical by reading the stock source**, not just +by wire match: request bytes, transport, channel options, gRPC metadata, interceptor. The remaining +difference is below the managed surface (native core) / server-side. + +**Conclusion (after #1 disproven + #4 answered + stock client decompiled).** Four independent angles are +now exhausted: client payload (byte-identical), transport (stock client is *also* gRPC-Web/HTTP-1.1 — +HTTP/2 makes no difference, both 0 rows), client-side metadata/interceptor/channel (decompiled — identical, +no hidden header), and data store (global, unscoped, 71,332 events the engine serves via INSQL but +withholds from our gRPC connection). The gate is a **server-internal per-connection retrieval working-set** +that a pure-managed client cannot reconstruct by matching wire bytes, transport, metadata, or data — and +the establishing logic is in the native `HistorianClient` C++ core, not in any decompilable managed step. +The remaining angle (#3 HTTP/2-frame capture) is low-probability given the stock client uses the same +gRPC-Web/HTTP-1.1 channel. **gRPC event-row retrieval stands documented as auth-solved / retrieval-server-gated**; `ReadEventsAsync` over gRPC keeps the honest no-row throw, and event reads use the WCF transport.