|
|
|
@@ -169,108 +169,131 @@ impl SoapEnvelope {
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Encode a SOAP envelope to NBFX bytes. Returns the byte buffer + the
|
|
|
|
|
/// dynamic dictionary state at end-of-encode (the F25 client threads
|
|
|
|
|
/// that through subsequent envelopes for compression).
|
|
|
|
|
/// dynamic dictionary state at end-of-encode.
|
|
|
|
|
///
|
|
|
|
|
/// Wire shape:
|
|
|
|
|
/// ```xml
|
|
|
|
|
/// <s:Envelope> (dict 4 / 2)
|
|
|
|
|
/// <s:Header> (dict 8)
|
|
|
|
|
/// <a:Action s:mustUnderstand="1">…</a:Action> (dict 10)
|
|
|
|
|
/// [<h:ConnectionValidator …/>] (asb headers ns)
|
|
|
|
|
/// </s:Header>
|
|
|
|
|
/// <s:Body> (dict 14)
|
|
|
|
|
/// {body_tokens}
|
|
|
|
|
/// </s:Body>
|
|
|
|
|
/// </s:Envelope>
|
|
|
|
|
/// ```
|
|
|
|
|
/// **Wire shape (WCF binary message format)**:
|
|
|
|
|
///
|
|
|
|
|
/// 1. **Binary header block** prepended to the NBFX envelope. WCF
|
|
|
|
|
/// uses this to pre-populate the per-session dynamic dictionary
|
|
|
|
|
/// with strings that appear inside the envelope. Each string gets
|
|
|
|
|
/// an odd dictionary id (`1, 3, 5, ...` — even ids are reserved
|
|
|
|
|
/// for the static [MC-NBFS] dictionary).
|
|
|
|
|
///
|
|
|
|
|
/// ```text
|
|
|
|
|
/// [outer length, multibyte-int31]
|
|
|
|
|
/// [action length, multibyte-int31] [action UTF-8 bytes] ← dict id 1
|
|
|
|
|
/// [to length, multibyte-int31] [to UTF-8 bytes] ← dict id 3
|
|
|
|
|
/// ```
|
|
|
|
|
///
|
|
|
|
|
/// 2. **NBFX envelope** that references the pre-populated strings:
|
|
|
|
|
///
|
|
|
|
|
/// ```xml
|
|
|
|
|
/// <s:Envelope xmlns:s="…" xmlns:a="…">
|
|
|
|
|
/// <s:Header>
|
|
|
|
|
/// <a:Action s:mustUnderstand="1">{dict 1}</a:Action>
|
|
|
|
|
/// [<h:ConnectionValidator …/>]
|
|
|
|
|
/// <a:MessageID>urn:uuid:…</a:MessageID>
|
|
|
|
|
/// <a:ReplyTo><a:Address>{anonymous}</a:Address></a:ReplyTo>
|
|
|
|
|
/// <a:To s:mustUnderstand="1">{dict 3}</a:To>
|
|
|
|
|
/// </s:Header>
|
|
|
|
|
/// <s:Body>{body_tokens}</s:Body>
|
|
|
|
|
/// </s:Envelope>
|
|
|
|
|
/// ```
|
|
|
|
|
///
|
|
|
|
|
/// The header form was reverse-engineered from the .NET reference's
|
|
|
|
|
/// wire bytes (captured via `examples/asb-relay`); see `[M5]
|
|
|
|
|
/// live-probe iteration` commits in the followups for the full
|
|
|
|
|
/// derivation.
|
|
|
|
|
pub fn encode_envelope(
|
|
|
|
|
envelope: &SoapEnvelope,
|
|
|
|
|
dynamic: &mut DynamicDictionary,
|
|
|
|
|
) -> Result<Vec<u8>, NbfxError> {
|
|
|
|
|
// ---- Binary header block ----
|
|
|
|
|
//
|
|
|
|
|
// Pre-populate the per-session dynamic dictionary with strings the
|
|
|
|
|
// NBFX envelope below references. Each string gets an odd dict id
|
|
|
|
|
// (`1, 3, 5, ...` — even ids are reserved for [MC-NBFS] static).
|
|
|
|
|
// We always include action + to even when `to_uri` is None: in
|
|
|
|
|
// that case we use an empty string for the To slot. WCF's default
|
|
|
|
|
// service dispatcher requires both populated for net.tcp.
|
|
|
|
|
let to_uri = envelope.to_uri.clone().unwrap_or_default();
|
|
|
|
|
let action_dict_id: u32 = 1;
|
|
|
|
|
let to_dict_id: u32 = 3;
|
|
|
|
|
let header_strings = [envelope.action.as_str(), to_uri.as_str()];
|
|
|
|
|
|
|
|
|
|
// ---- NBFX envelope tokens ----
|
|
|
|
|
let mut tokens = vec![
|
|
|
|
|
// <s:Envelope xmlns:s="…soap-envelope" xmlns:a="…addressing">
|
|
|
|
|
NbfxToken::Element {
|
|
|
|
|
prefix: Some("s".to_string()),
|
|
|
|
|
name: NbfxName::Static(ns::ENVELOPE),
|
|
|
|
|
},
|
|
|
|
|
NbfxToken::NamespaceDeclaration {
|
|
|
|
|
prefix: "s".to_string(),
|
|
|
|
|
value: NbfxText::DictionaryStatic(ns::SOAP_ENVELOPE),
|
|
|
|
|
},
|
|
|
|
|
NbfxToken::NamespaceDeclaration {
|
|
|
|
|
prefix: "a".to_string(),
|
|
|
|
|
value: NbfxText::DictionaryStatic(ns::WS_ADDRESSING),
|
|
|
|
|
},
|
|
|
|
|
// <s:Header>
|
|
|
|
|
NbfxToken::Element {
|
|
|
|
|
prefix: Some("s".to_string()),
|
|
|
|
|
name: NbfxName::Static(ns::HEADER),
|
|
|
|
|
},
|
|
|
|
|
// <a:Action s:mustUnderstand="1">{dict id 1}</a:Action>
|
|
|
|
|
NbfxToken::Element {
|
|
|
|
|
prefix: Some("a".to_string()),
|
|
|
|
|
name: NbfxName::Static(ns::ACTION),
|
|
|
|
|
},
|
|
|
|
|
NbfxToken::Attribute {
|
|
|
|
|
prefix: Some("s".to_string()),
|
|
|
|
|
name: NbfxName::Static(ns::MUST_UNDERSTAND_ATTR),
|
|
|
|
|
value: NbfxText::One,
|
|
|
|
|
},
|
|
|
|
|
NbfxToken::Text(NbfxText::DictionaryStatic(action_dict_id)),
|
|
|
|
|
NbfxToken::EndElement, // </a:Action>
|
|
|
|
|
];
|
|
|
|
|
tokens.push(NbfxToken::NamespaceDeclaration {
|
|
|
|
|
prefix: "s".to_string(),
|
|
|
|
|
value: NbfxText::DictionaryStatic(ns::SOAP_ENVELOPE),
|
|
|
|
|
});
|
|
|
|
|
tokens.push(NbfxToken::NamespaceDeclaration {
|
|
|
|
|
prefix: "a".to_string(),
|
|
|
|
|
value: NbfxText::DictionaryStatic(ns::WS_ADDRESSING),
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// <s:Header>
|
|
|
|
|
tokens.push(NbfxToken::Element {
|
|
|
|
|
prefix: Some("s".to_string()),
|
|
|
|
|
name: NbfxName::Static(ns::HEADER),
|
|
|
|
|
});
|
|
|
|
|
// <h:ConnectionValidator …/> (when present, comes before
|
|
|
|
|
// MessageID/ReplyTo per the .NET dump's element order)
|
|
|
|
|
if let Some(v) = &envelope.validator {
|
|
|
|
|
encode_validator(&mut tokens, v, dynamic);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// <a:Action s:mustUnderstand="1">{action}</a:Action>
|
|
|
|
|
// <a:MessageID>urn:uuid:{uuid}</a:MessageID>
|
|
|
|
|
let message_id = format!("urn:uuid:{}", make_random_uuid_v4());
|
|
|
|
|
tokens.push(NbfxToken::Element {
|
|
|
|
|
prefix: Some("a".to_string()),
|
|
|
|
|
name: NbfxName::Static(ns::ACTION),
|
|
|
|
|
name: NbfxName::Static(26),
|
|
|
|
|
});
|
|
|
|
|
tokens.push(NbfxToken::Text(NbfxText::Chars(message_id)));
|
|
|
|
|
tokens.push(NbfxToken::EndElement); // </a:MessageID>
|
|
|
|
|
|
|
|
|
|
// <a:ReplyTo><a:Address>{anonymous}</a:Address></a:ReplyTo>
|
|
|
|
|
tokens.push(NbfxToken::Element {
|
|
|
|
|
prefix: Some("a".to_string()),
|
|
|
|
|
name: NbfxName::Static(44),
|
|
|
|
|
});
|
|
|
|
|
tokens.push(NbfxToken::Element {
|
|
|
|
|
prefix: Some("a".to_string()),
|
|
|
|
|
name: NbfxName::Static(42),
|
|
|
|
|
});
|
|
|
|
|
tokens.push(NbfxToken::Text(NbfxText::DictionaryStatic(20)));
|
|
|
|
|
tokens.push(NbfxToken::EndElement); // </a:Address>
|
|
|
|
|
tokens.push(NbfxToken::EndElement); // </a:ReplyTo>
|
|
|
|
|
|
|
|
|
|
// <a:To s:mustUnderstand="1">{dict id 3}</a:To>
|
|
|
|
|
tokens.push(NbfxToken::Element {
|
|
|
|
|
prefix: Some("a".to_string()),
|
|
|
|
|
name: NbfxName::Static(12),
|
|
|
|
|
});
|
|
|
|
|
tokens.push(NbfxToken::Attribute {
|
|
|
|
|
prefix: Some("s".to_string()),
|
|
|
|
|
name: NbfxName::Static(ns::MUST_UNDERSTAND_ATTR),
|
|
|
|
|
value: NbfxText::One,
|
|
|
|
|
});
|
|
|
|
|
tokens.push(NbfxToken::Text(NbfxText::Chars(envelope.action.clone())));
|
|
|
|
|
tokens.push(NbfxToken::EndElement); // </a:Action>
|
|
|
|
|
|
|
|
|
|
// <h:ConnectionValidator …/> (WCF dump shows this comes BEFORE
|
|
|
|
|
// MessageID/ReplyTo when present)
|
|
|
|
|
if let Some(v) = &envelope.validator {
|
|
|
|
|
encode_validator(&mut tokens, v, dynamic);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// <a:MessageID>urn:uuid:{uuid}</a:MessageID>
|
|
|
|
|
// WCF's default binding requires MessageID for two-way operations.
|
|
|
|
|
// We auto-generate one per envelope; the value is opaque to the
|
|
|
|
|
// service but must be a valid URI.
|
|
|
|
|
let message_id = format!("urn:uuid:{}", make_random_uuid_v4());
|
|
|
|
|
tokens.push(NbfxToken::Element {
|
|
|
|
|
prefix: Some("a".to_string()),
|
|
|
|
|
name: NbfxName::Static(26), // "MessageID"
|
|
|
|
|
});
|
|
|
|
|
tokens.push(NbfxToken::Text(NbfxText::Chars(message_id)));
|
|
|
|
|
tokens.push(NbfxToken::EndElement); // </a:MessageID>
|
|
|
|
|
|
|
|
|
|
// <a:ReplyTo>
|
|
|
|
|
// <a:Address>http://www.w3.org/2005/08/addressing/anonymous</a:Address>
|
|
|
|
|
// </a:ReplyTo>
|
|
|
|
|
tokens.push(NbfxToken::Element {
|
|
|
|
|
prefix: Some("a".to_string()),
|
|
|
|
|
name: NbfxName::Static(44), // "ReplyTo"
|
|
|
|
|
});
|
|
|
|
|
tokens.push(NbfxToken::Element {
|
|
|
|
|
prefix: Some("a".to_string()),
|
|
|
|
|
name: NbfxName::Static(42), // "Address"
|
|
|
|
|
});
|
|
|
|
|
tokens.push(NbfxToken::Text(NbfxText::DictionaryStatic(20))); // anonymous
|
|
|
|
|
tokens.push(NbfxToken::EndElement); // </a:Address>
|
|
|
|
|
tokens.push(NbfxToken::EndElement); // </a:ReplyTo>
|
|
|
|
|
|
|
|
|
|
// <a:To s:mustUnderstand="1">{to_uri}</a:To> (optional — WCF
|
|
|
|
|
// omits To for net.tcp request/response by default)
|
|
|
|
|
if let Some(to) = &envelope.to_uri {
|
|
|
|
|
tokens.push(NbfxToken::Element {
|
|
|
|
|
prefix: Some("a".to_string()),
|
|
|
|
|
name: NbfxName::Static(12), // "To"
|
|
|
|
|
});
|
|
|
|
|
tokens.push(NbfxToken::Attribute {
|
|
|
|
|
prefix: Some("s".to_string()),
|
|
|
|
|
name: NbfxName::Static(ns::MUST_UNDERSTAND_ATTR),
|
|
|
|
|
value: NbfxText::One,
|
|
|
|
|
});
|
|
|
|
|
tokens.push(NbfxToken::Text(NbfxText::Chars(to.clone())));
|
|
|
|
|
tokens.push(NbfxToken::EndElement);
|
|
|
|
|
}
|
|
|
|
|
tokens.push(NbfxToken::Text(NbfxText::DictionaryStatic(to_dict_id)));
|
|
|
|
|
tokens.push(NbfxToken::EndElement); // </a:To>
|
|
|
|
|
|
|
|
|
|
tokens.push(NbfxToken::EndElement); // </s:Header>
|
|
|
|
|
|
|
|
|
@@ -284,8 +307,121 @@ pub fn encode_envelope(
|
|
|
|
|
|
|
|
|
|
tokens.push(NbfxToken::EndElement); // </s:Envelope>
|
|
|
|
|
|
|
|
|
|
let mut out = Vec::with_capacity(estimate_envelope_size(envelope));
|
|
|
|
|
encode_tokens(&tokens, dynamic, &mut out)?;
|
|
|
|
|
// ---- Assemble output: binary header + NBFX envelope ----
|
|
|
|
|
let mut nbfx_bytes = Vec::with_capacity(estimate_envelope_size(envelope));
|
|
|
|
|
encode_tokens(&tokens, dynamic, &mut nbfx_bytes)?;
|
|
|
|
|
|
|
|
|
|
let header_bytes = encode_binary_header(&header_strings)?;
|
|
|
|
|
|
|
|
|
|
let mut out = Vec::with_capacity(header_bytes.len() + nbfx_bytes.len());
|
|
|
|
|
out.extend_from_slice(&header_bytes);
|
|
|
|
|
out.extend_from_slice(&nbfx_bytes);
|
|
|
|
|
Ok(out)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Encode the WCF binary message header that prepends the NBFX envelope.
|
|
|
|
|
/// The header pre-populates the per-session dynamic dictionary with
|
|
|
|
|
/// `strings`, in order — the first gets dict id 1, the second id 3,
|
|
|
|
|
/// etc. (odd ids only; evens are reserved for static `[MC-NBFS]`).
|
|
|
|
|
///
|
|
|
|
|
/// Wire format:
|
|
|
|
|
/// ```text
|
|
|
|
|
/// [outer length as multibyte-int31]
|
|
|
|
|
/// [string 1 length as multibyte-int31] [UTF-8 bytes]
|
|
|
|
|
/// [string 2 length as multibyte-int31] [UTF-8 bytes]
|
|
|
|
|
/// ...
|
|
|
|
|
/// ```
|
|
|
|
|
/// Parsed WCF binary header: the strings pre-populated into the
|
|
|
|
|
/// session dynamic dictionary + the byte offset where the NBFX
|
|
|
|
|
/// envelope begins.
|
|
|
|
|
struct ParsedBinaryHeader {
|
|
|
|
|
/// Pre-pop strings in declaration order. Wire ids: index 0 → 1,
|
|
|
|
|
/// index 1 → 3, index 2 → 5, etc. (odd numbers; even reserved
|
|
|
|
|
/// for `[MC-NBFS]` static dict).
|
|
|
|
|
strings: Vec<String>,
|
|
|
|
|
nbfx_start: usize,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Detect + decode the WCF binary header block at the start of a SOAP
|
|
|
|
|
/// envelope payload. Returns `None` if no header is present (e.g. the
|
|
|
|
|
/// peer didn't emit one).
|
|
|
|
|
///
|
|
|
|
|
/// Heuristic: read a multibyte-int31 length L from the start. If the
|
|
|
|
|
/// byte at offset 1+L is a plausible NBFX element record byte
|
|
|
|
|
/// (`0x40`-`0x77`), treat the first 1+L bytes as the header. Walk the
|
|
|
|
|
/// inner block as a sequence of length-prefixed UTF-8 strings.
|
|
|
|
|
fn parse_binary_header_prefix(input: &[u8]) -> Option<ParsedBinaryHeader> {
|
|
|
|
|
use mxaccess_asb_nettcp::nmf::decode_multibyte_int31;
|
|
|
|
|
let mut cursor = 0usize;
|
|
|
|
|
let outer_len = decode_multibyte_int31(input, &mut cursor).ok()?;
|
|
|
|
|
let outer_len = usize::try_from(outer_len).ok()?;
|
|
|
|
|
let header_start = cursor;
|
|
|
|
|
let nbfx_start = header_start + outer_len;
|
|
|
|
|
if nbfx_start >= input.len() {
|
|
|
|
|
return None;
|
|
|
|
|
}
|
|
|
|
|
let first_nbfx = *input.get(nbfx_start)?;
|
|
|
|
|
if !(0x40..=0x77).contains(&first_nbfx) {
|
|
|
|
|
return None;
|
|
|
|
|
}
|
|
|
|
|
// Walk the inner block as (multibyte-int31 length, UTF-8 bytes).
|
|
|
|
|
let mut strings = Vec::new();
|
|
|
|
|
let mut p = header_start;
|
|
|
|
|
let header_end = header_start + outer_len;
|
|
|
|
|
while p < header_end {
|
|
|
|
|
let str_len = decode_multibyte_int31(input, &mut p).ok()?;
|
|
|
|
|
let str_len = usize::try_from(str_len).ok()?;
|
|
|
|
|
let bytes = input.get(p..p + str_len)?;
|
|
|
|
|
let s = std::str::from_utf8(bytes).ok()?;
|
|
|
|
|
strings.push(s.to_string());
|
|
|
|
|
p += str_len;
|
|
|
|
|
}
|
|
|
|
|
Some(ParsedBinaryHeader {
|
|
|
|
|
strings,
|
|
|
|
|
nbfx_start,
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Resolve an NBFX text token using static dict + dynamic dict + the
|
|
|
|
|
/// binary-header pre-pop strings (odd dict ids).
|
|
|
|
|
fn resolve_with_header(
|
|
|
|
|
text: &NbfxText,
|
|
|
|
|
dynamic: &DynamicDictionary,
|
|
|
|
|
header: Option<&ParsedBinaryHeader>,
|
|
|
|
|
) -> Option<String> {
|
|
|
|
|
if let NbfxText::DictionaryStatic(id) = text {
|
|
|
|
|
// Even ids hit the static dict; odd ids hit the dynamic
|
|
|
|
|
// pre-pop. Wire id 2N+1 → header.strings[N].
|
|
|
|
|
if id % 2 == 1 {
|
|
|
|
|
if let Some(h) = header {
|
|
|
|
|
let idx = (*id as usize - 1) / 2;
|
|
|
|
|
if let Some(s) = h.strings.get(idx) {
|
|
|
|
|
return Some(s.clone());
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
text.resolve(dynamic)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn encode_binary_header(strings: &[&str]) -> Result<Vec<u8>, NbfxError> {
|
|
|
|
|
use mxaccess_asb_nettcp::nmf::encode_multibyte_int31;
|
|
|
|
|
let mut inner = Vec::new();
|
|
|
|
|
for s in strings {
|
|
|
|
|
let len = i32::try_from(s.len()).map_err(|_| NbfxError::PayloadTooLarge {
|
|
|
|
|
len: s.len(),
|
|
|
|
|
max: i32::MAX as u64,
|
|
|
|
|
})?;
|
|
|
|
|
encode_multibyte_int31(&mut inner, len).map_err(|_| NbfxError::IntOverflow)?;
|
|
|
|
|
inner.extend_from_slice(s.as_bytes());
|
|
|
|
|
}
|
|
|
|
|
let inner_len = i32::try_from(inner.len()).map_err(|_| NbfxError::PayloadTooLarge {
|
|
|
|
|
len: inner.len(),
|
|
|
|
|
max: i32::MAX as u64,
|
|
|
|
|
})?;
|
|
|
|
|
let mut out = Vec::new();
|
|
|
|
|
encode_multibyte_int31(&mut out, inner_len).map_err(|_| NbfxError::IntOverflow)?;
|
|
|
|
|
out.extend_from_slice(&inner);
|
|
|
|
|
Ok(out)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
@@ -306,7 +442,31 @@ pub fn decode_envelope(
|
|
|
|
|
input: &[u8],
|
|
|
|
|
dynamic: &mut DynamicDictionary,
|
|
|
|
|
) -> Result<DecodedEnvelope, EnvelopeError> {
|
|
|
|
|
let (tokens, _consumed) = decode_tokens(input, dynamic)?;
|
|
|
|
|
// Strip + decode the WCF binary header block (action+to pre-pop)
|
|
|
|
|
// if present. The header strings get assigned odd dict ids
|
|
|
|
|
// (1, 3, 5, ...); inside the NBFX envelope they're referenced via
|
|
|
|
|
// `DictionaryText (0xAA) {odd-id}`. We feed each into the F21
|
|
|
|
|
// dynamic dictionary at the matching offset so `text.resolve()`
|
|
|
|
|
// returns the right string.
|
|
|
|
|
let header = parse_binary_header_prefix(input);
|
|
|
|
|
let nbfx_input = match &header {
|
|
|
|
|
Some(h) => input.get(h.nbfx_start..).unwrap_or(input),
|
|
|
|
|
None => input,
|
|
|
|
|
};
|
|
|
|
|
if let Some(h) = &header {
|
|
|
|
|
// F21's DynamicDictionary uses sequential ids starting at 0.
|
|
|
|
|
// Wire ids for dynamic strings are odd (1, 3, 5, ...).
|
|
|
|
|
// Prefill our internal dict with sentinel placeholders at
|
|
|
|
|
// even indices so the strings land at odd ones via `intern`.
|
|
|
|
|
for (i, s) in h.strings.iter().enumerate() {
|
|
|
|
|
// Slot 0 → wire id 1, slot 1 → wire id 3, etc. Since
|
|
|
|
|
// intern just appends, this works as long as we intern
|
|
|
|
|
// header strings before any other dynamic-dict use.
|
|
|
|
|
let _ = i; // silence unused-var if we change scheme later
|
|
|
|
|
dynamic.intern(s);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
let (tokens, _consumed) = decode_tokens(nbfx_input, dynamic)?;
|
|
|
|
|
let mut action = None;
|
|
|
|
|
let mut validator: Option<ConnectionValidator> = None;
|
|
|
|
|
let mut body_tokens = Vec::new();
|
|
|
|
@@ -320,7 +480,7 @@ pub fn decode_envelope(
|
|
|
|
|
} if *id == ns::ACTION => {
|
|
|
|
|
idx = consume_attributes(&tokens, idx + 1);
|
|
|
|
|
if let Some(NbfxToken::Text(text)) = tokens.get(idx) {
|
|
|
|
|
action = text.resolve(dynamic);
|
|
|
|
|
action = resolve_with_header(text, dynamic, header.as_ref());
|
|
|
|
|
idx += 1;
|
|
|
|
|
}
|
|
|
|
|
idx = skip_until_end(&tokens, idx);
|
|
|
|
|