Merge re/grpc-2023r2-handshake: M0 gRPC parity (probe/system-param/metadata/browse) + handshake fix
This commit is contained in:
@@ -9,15 +9,54 @@ HCAL replacement, built on the **2023 R2 gRPC transport**. Derived from
|
||||
> protocol serializer/parser + golden-byte unit test + an env-gated live integration
|
||||
> test against the local Historian.
|
||||
|
||||
## Progress (updated 2026-06-19)
|
||||
## Progress (updated 2026-06-21)
|
||||
|
||||
- ✅ **R0.6 version gate** — `HistorianServerVersionGate` + `HistorianClientOptions.VerifyServerInterfaceVersion`;
|
||||
fail-closed on connect, wired into both WCF and gRPC paths. Supported versions are
|
||||
evidence-based (Hist=11, Retr=4, Trx=2; Status reachability-only), captured from the
|
||||
live server. 10 unit tests.
|
||||
evidence-based (Hist=11/12, Retr=4, Trx=2; Status reachability-only), captured from the
|
||||
live server. History 12 (2023 R2 gRPC) accepted alongside 11 (buffer-compatible).
|
||||
- ✅ **CW-1 capture pipeline** — `ProtocolCaptureSanitizer` + `ProtocolFixtureWriter` +
|
||||
`capture-tag-info` CLI command; produces sanitized `fixtures/protocol/<op>/` golden files.
|
||||
11 unit tests. First fixture: `get-tag-info/analog-*.json`.
|
||||
- ✅ **gRPC auth handshake (read chain)** — LIVE-VERIFIED 2026-06-21 against a real 2023 R2
|
||||
server: `ReadRawAsync` over `RemoteGrpc` returns rows. Token loop routes to
|
||||
`StorageService.ValidateClientCredential`. Shared handshake extracted to
|
||||
`Grpc/HistorianGrpcHandshake` for reuse by the status/browse/metadata paths.
|
||||
- ✅ **R0.4 Probe over gRPC** — `Grpc/HistorianGrpcProbe` (History/Retrieval/Status
|
||||
`GetInterfaceVersion`); `ProbeAsync` routes over gRPC when `Transport==RemoteGrpc`.
|
||||
**LIVE-VERIFIED 2026-06-21** (no credentials required — runs before the auth loop).
|
||||
- ✅ **R0.3 System parameter over gRPC** — `Grpc/HistorianGrpcStatusClient.GetSystemParameterAsync`
|
||||
(`StatusService.GetSystemParameter`); routed in the dialect. Built + unit-tested + **LIVE-VERIFIED
|
||||
2026-06-21** against a real 2023 R2 server (returned `HistorianVersion`). Code path is the proven
|
||||
handshake + a single string-in/string-out RPC.
|
||||
- ✅ **R0.2 Tag metadata over gRPC** — `Grpc/HistorianGrpcTagClient.GetTagMetadataAsync`
|
||||
(`RetrievalService.GetTagInfosFromName`, the plural **string-handle** op). `GetTagMetadataAsync`
|
||||
routes over gRPC when `Transport==RemoteGrpc`. Request `btTagNames` = `uint count + per-name(uint
|
||||
charCount + UTF-16LE)` (golden-byte unit-tested); response `btTagInfos` = `uint count + CTagMetadata`
|
||||
records (reuses `ParseGetTagInfoResponse`); string handle = uppercase Open2 storage GUID. The 2020
|
||||
WCF string-handle wall does **not** apply on the gRPC front door (as predicted). **LIVE-VERIFIED
|
||||
2026-06-21** — `GetTagMetadataAsync` returned the requested tag + a valid data type.
|
||||
- ✅ **R0.1 Browse over gRPC** — DONE, **LIVE-VERIFIED 2026-06-21**.
|
||||
`HistorianClient.BrowseTagNamesAsync` routes over gRPC via
|
||||
`Grpc/HistorianGrpcTagClient.BrowseTagNamesAsync`: StartTagQuery(**OData** filter) → paged
|
||||
**QueryTag** (`btRequest` = `u16 0x6752 + u16 1 + u16 queryType + u32 startIndex + u32 count`) →
|
||||
EndTagQuery; response = `u32 count + per-name(u32 charCount + UTF-16LE) + trailer`. The SDK glob
|
||||
filter is translated by `GlobToODataFilter` (`Pre*`→`startswith`, `*suf`→`endswith`, `*mid*`→
|
||||
`contains`, exact→`eq`). The QueryTag packet-id `0x6752` was recovered from a `.rdata`
|
||||
packet-descriptor table (`{0x6751,1}`=StartTagQuery, `{0x6752,1}`=QueryTag) — no Ghidra needed.
|
||||
Golden-byte + glob unit tests + gated live test. Full finding:
|
||||
`docs/reverse-engineering/grpc-tag-query-odata.md`.
|
||||
|
||||
> ✅ **Milestone 0 (gRPC parity) is COMPLETE** — probe, system-param, metadata, and browse all run
|
||||
> over `RemoteGrpc` and are live-verified against a real 2023 R2 server, alongside the read chain.
|
||||
|
||||
> ℹ️ **Auth note (2026-06-21, resolved):** an apparent NTLM round-1 `SEC_E_LOGON_DENIED` blocker
|
||||
> turned out to be a **test-harness credential-parsing bug**, not a server/account/SDK issue — the
|
||||
> gitignored creds file stores **quoted** values (`"nam\user"`, `"pass"`), and the env-setup must
|
||||
> **strip surrounding quotes** before exporting `HISTORIAN_USER`/`HISTORIAN_PASSWORD`. With quotes
|
||||
> stripped, the domain account authenticates and the full read + system-param + probe chain passes
|
||||
> live. The round-failure diagnostic added during the hunt is kept
|
||||
> (`HistorianNativeHandshake.DescribeError` decodes the native error + hex/ASCII preview).
|
||||
|
||||
> ⚠️ **Live-verification constraint:** the local Historian is **2020** (WCF, port 32568) — the
|
||||
> 2023 R2 gRPC endpoint (32565) is absent. M0's gRPC routing (R0.1–R0.4) can be built and
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
# R0.1 browse over gRPC — StartTagQuery takes an OData filter (2026-06-21)
|
||||
|
||||
Live-probed `RetrievalService.StartTagQuery` / `QueryTag` against a real **2023 R2** server over the
|
||||
gRPC front door (string-handle = uppercase Open2 storage GUID). Key result: **browse is feasible on
|
||||
2023 R2** — the 2020 WCF "metadata-server pipe" wall does **not** block here.
|
||||
|
||||
## StartTagQuery — CRACKED
|
||||
|
||||
`StartTagQuery(strHandle, btRequest)` where `btRequest` = the native
|
||||
`marker(26449) + version(1) + WriteHistorianString(filter)` buffer
|
||||
(`HistorianTagQueryProtocol.CreateStartTagQueryAttempt`). The server runs
|
||||
`CMdServer::StartTagQuery::StartActiveTagnamesQuery` over `\\.\pipe\aahMetadataServer\console` and
|
||||
**parses the filter string as OData** (not SQL-LIKE). Swept filters:
|
||||
|
||||
| filter | result |
|
||||
|---|---|
|
||||
| `startswith(TagName,'Sys')` | ✅ success, 8-byte response |
|
||||
| `contains(TagName,'Sys')` | ✅ success |
|
||||
| `TagName eq 'SysTimeSec'` | ✅ success |
|
||||
| `` (empty) | ✅ success (all tags) |
|
||||
| `Sys*` / `*` | ❌ `ODataFilter ... bad token` |
|
||||
| `TagName like 'Sys%'` / `Name like 'Sys%'` | ❌ rejected |
|
||||
|
||||
Success response `btResponse` is the 8-byte `(queryHandle:uint, tagCount:uint)` pair
|
||||
(`ParseStartTagQueryResponse`). Live: `startswith(TagName,'Sys')` → tagCount = 220.
|
||||
|
||||
**Implication for the public API:** browse must translate the SDK's glob filter to OData —
|
||||
`*` → empty, `Pre*` → `startswith(TagName,'Pre')`, `*sub*` → `contains(TagName,'sub')`,
|
||||
exact → `TagName eq '...'`. (Escaping single-quotes in names still TBD.)
|
||||
|
||||
## QueryTag — CRACKED (2026-06-21), browse SHIPPED
|
||||
|
||||
`QueryTag(strHandle, uiQueryHandle, btRequest)` is the paging call that returns the tag-name rows.
|
||||
The blocker was the packet id: every guessed `btRequest` returned native error **type 4 / code 72 =
|
||||
`InvalidPacketId`** (`ArchestrA.CloudHistorian.Contract.ErrorCode.InvalidPacketId`). The generic
|
||||
`0x6751` header that StartTagQuery accepts is the **wrong** id for QueryTag.
|
||||
|
||||
**How it was found (no Ghidra needed):** a `.rdata` **packet-descriptor table** in
|
||||
`aahClientManaged.dll` lists consecutive `{uint marker, uint version}` entries —
|
||||
`{0x6751, 1}` (StartTagQuery) immediately followed by **`{0x6752, 1}`** (the paired op). Found by
|
||||
`pefile` byte-scan of `.rdata` for `51 67 00 00` and dumping the surrounding dwords. Testing `0x6752`
|
||||
live confirmed it.
|
||||
|
||||
**QueryTag wire format (live-verified):**
|
||||
- request `btRequest` = `u16 marker(0x6752) + u16 version(1) + u16 queryType + u32 startIndex + u32 count`
|
||||
— `queryType = 1` returns tag-name rows (`queryType = 0` returns an empty/count-only page).
|
||||
- response `btResonse` = `u32 count + per-name(u32 charCount + UTF-16LE) + trailer`
|
||||
(the trailer is the CloudHistorian `NextIndex`/`TagMetadataBuffer` region — ignored by
|
||||
`HistorianTagQueryProtocol.ParseTagNameQueryPage`).
|
||||
- Semantic fields match `ArchestrA.CloudHistorian.Contract.QueryTagRequest`
|
||||
(`QueryType/StartIndex/TagCount`; the QueryHandle travels in the protobuf `uiQueryHandle`).
|
||||
|
||||
**Browse is shipped:** `HistorianClient.BrowseTagNamesAsync` routes over gRPC when
|
||||
`Transport==RemoteGrpc` via `Grpc/HistorianGrpcTagClient.BrowseTagNamesAsync`
|
||||
(StartTagQuery(OData) → paged QueryTag(0x6752) → EndTagQuery), with the SDK glob filter translated by
|
||||
`GlobToODataFilter`. Golden-byte + glob unit tests and a gated live test
|
||||
(`BrowseTagNamesAsync_OverGrpc_ReturnsSystemTags`) cover it. **M0 gRPC parity is complete.**
|
||||
Reference in New Issue
Block a user