[M5] mxaccess-asb: WCF binary message header (action+to dict pre-pop)
Adds the binary header block that WCF prepends to SizedEnvelope
payloads. Reverse-engineered from .NET probe wire bytes captured via
asb-relay.
Wire form (per the .NET capture analysis in the previous commit):
```
[outer length, multibyte-int31]
[string-1 length, multibyte-int31] [UTF-8 bytes] ← dict id 1 (action)
[string-2 length, multibyte-int31] [UTF-8 bytes] ← dict id 3 (to)
[NBFX <s:Envelope>...]
```
Inside the NBFX envelope, `<a:Action>` and `<a:To>` reference the
pre-pop strings via `DictionaryText 0xAA {odd-id}` instead of inlining
their values. The header strings get assigned odd dict ids
(1, 3, 5, ...); even ids stay reserved for the [MC-NBFS] static dict.
Encode side:
* `encode_envelope` now emits header [action, to] before NBFX. `to_uri`
defaults to empty string when None — caller-supplied `with_to(uri)`
is the supported path.
* AsbClient's `send_envelope` and `send_envelope_one_way` auto-fill
`to_uri` from `self.via_uri` when not set.
* New private `encode_binary_header(strings)` helper.
Decode side:
* New `parse_binary_header_prefix(input)` heuristically detects + parses
the header (look for plausible NBFX element record byte 0x40-0x77 at
the offset implied by the outer length).
* New `resolve_with_header(text, dynamic, header)` resolves
`DictionaryText` with odd id by indexing into header.strings; even
ids fall through to static-dict lookup as before.
Tests pass (72) — round-trip envelope → bytes → envelope recovers
action through the new dict-id resolution path.
Live status: this commit gets us further but the connect SOAP
envelope still TCP-RSTs at SMSvcHost. The remaining delta vs the .NET
capture is structural NBFX optimisation: .NET uses single-letter
prefix-element/attribute records (0x44-0x77 PrefixDictionaryElement
_<a-z>, 0x0C-0x25 PrefixDictionaryAttribute_<a-z>, 0x0B
DictionaryXmlnsAttribute) while our F21 encoder always uses the long
forms (0x43 prefix-string + name-dict-id, etc.). Logically
equivalent but WCF's parser likely strict on which form it accepts.
Next iteration will add short-form encoding to F21 for single-letter
prefixes (s:, a:, h:, i:) which covers every namespace prefix in our
envelope.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -152,7 +152,16 @@ impl<T: AsyncRead + AsyncWrite + Unpin + Send> AsbClient<T> {
|
||||
return Err(ClientError::AlreadyClosed);
|
||||
}
|
||||
|
||||
let payload = encode_envelope(envelope, &mut self.write_dictionary)?;
|
||||
// Default the WS-Addressing To header to the same URL we put
|
||||
// in the NMF Via record. WCF dispatches by To-URL match
|
||||
// against the registered service URL; an empty / wrong To
|
||||
// produces an AddressFilterMismatch fault.
|
||||
let envelope = if envelope.to_uri.is_some() {
|
||||
envelope.clone()
|
||||
} else {
|
||||
envelope.clone().with_to(self.via_uri.clone())
|
||||
};
|
||||
let payload = encode_envelope(&envelope, &mut self.write_dictionary)?;
|
||||
let mut framed = Vec::new();
|
||||
NmfRecord::SizedEnvelope(payload).encode_into(&mut framed)?;
|
||||
self.stream.write_all(&framed).await?;
|
||||
@@ -276,7 +285,12 @@ impl<T: AsyncRead + AsyncWrite + Unpin + Send> AsbClient<T> {
|
||||
if self.closed {
|
||||
return Err(ClientError::AlreadyClosed);
|
||||
}
|
||||
let payload = encode_envelope(envelope, &mut self.write_dictionary)?;
|
||||
let envelope = if envelope.to_uri.is_some() {
|
||||
envelope.clone()
|
||||
} else {
|
||||
envelope.clone().with_to(self.via_uri.clone())
|
||||
};
|
||||
let payload = encode_envelope(&envelope, &mut self.write_dictionary)?;
|
||||
let mut framed = Vec::new();
|
||||
NmfRecord::SizedEnvelope(payload).encode_into(&mut framed)?;
|
||||
self.stream.write_all(&framed).await?;
|
||||
|
||||
Reference in New Issue
Block a user