Files
Florent BEAUCHAMP 3bf50c38ee fix(vmware-explorer): fixes omnibus (#10363)
This branch is a reliability overhaul of the ESXi/vSphere source side of V2V, plus a scope reduction of the package. The SOAP plumbing is rewritten:

    VimClient wraps @vates/node-vsphere-soap in a promise API with one emitter per call (no more cross-talk between concurrent calls, no listener leak) and a pTimeout;
    vim25 faults are now parsed (_parseFault.mjs) so callers get a real fault type instead of task execution failed;
    the library no longer throws from async callbacks (which killed xo-server) and its beforeExit logout hook is per-client instead of removeAllListeners.
    Request specs are built in explicit schema order (soap/specs.mjs) with xsi:type, and property values are normalized (soap/normalize.mjs) so search() and the new #retrieveProperty share one shape.
    search() now paginates, cancels the retrieval and destroys the container view.
    Datastore HTTP downloads get session-cookie reuse, retry with jittered backoff, a headers-only timeout, and range-honouring validation.
    Task waiting polls with backoff and per-operation timeouts (6 h for snapshot consolidation).
    nbdkit servers are memoized by a key that includes the export options, the readiness wait races the process death instead of sleeping 2 s
    the password file is 0600 and unlinked
    the thumbprint is read over TLS in-process instead of two openssl spawns.
    The old VHD-over-datastore readers (VhdEsxi*, Datastore*, openDeltaVmdkAsVhd) are deleted, hence the majo, rleaving the vddk/NBD path with a COWD grain-directory parser as data-map fallback.
    ~1.7k lines of unit tests are added.
2026-09-11 17:36:38 +02:00

103 lines
3.7 KiB
JavaScript

const XML_ENTITIES = {
'&': '&',
''': "'",
'>': '>',
'&lt;': '<',
'&quot;': '"',
}
const unescapeXml = text => text.replace(/&(?:amp|apos|gt|lt|quot);/g, entity => XML_ENTITIES[entity])
// node-soap wraps text nodes either as a plain string or as `{ $value }` depending on
// whether the element carries attributes
const valueOf = node => {
if (typeof node === 'string') {
return node
}
if (node !== null && typeof node === 'object' && typeof node.$value === 'string') {
return node.$value
}
}
const faultTypeOfElementName = name => name.replace(/^.*:/, '').replace(/Fault$/, '')
// SOAP 1.1 requires the children of `Fault` to be unqualified, and that is what ESXi and vCenter
// send, but the raw body is only read when node-soap failed to parse the response: it can then be
// anything, e.g. an error emitted by a reverse proxy in front of vCenter. Hence the tolerance to a
// namespace prefix and to attributes on the element
const textOfElement = name =>
new RegExp(`<(?:[\\w.-]+:)?${name}(?:\\s[^>]*)?>([\\s\\S]*?)</(?:[\\w.-]+:)?${name}>`, 'i')
const FAULTCODE_RE = textOfElement('faultcode')
const FAULTSTRING_RE = textOfElement('faultstring')
const LOCALIZED_MESSAGE_RE = textOfElement('localizedMessage')
const DETAIL_CHILD_RE = /<(?:[\w.-]+:)?detail(?:\s[^>]*)?>\s*<([^\s/>]+)/i
/**
* Extracts the vim25 fault of a failed SOAP call.
*
* The fault type (`FileLocked`, `InvalidDeviceSpec`, `ManagedObjectNotFound`, ...) is the only part
* a caller can branch on: `faultstring` and `localizedMessage` are meant for humans and change with
* the locale of the server.
*
* Two sources are used, in order of reliability:
* 1. `rawError.root`, the envelope parsed by node-soap ( see `wsdl.xmlToObject`, which throws on a
* fault and whose error is stored there by node-soap)
* 2. the raw response body, when the parsing itself failed
*
* @param {unknown} rawError - the error passed by node-soap to its callback
* @returns {{ code?: string, faultcode?: string, faultstring?: string, localizedMessage?: string, body?: string }}
*/
export function parseFault(rawError) {
const fault = rawError?.root?.Envelope?.Body?.Fault
const rawBody = rawError?.response?.data ?? rawError?.body
const body = typeof rawBody === 'string' && rawBody !== '<stream>' ? rawBody : undefined
let code, localizedMessage
let faultcode = valueOf(fault?.faultcode)
let faultstring = valueOf(fault?.faultstring)
const detail = fault?.detail
if (detail !== null && typeof detail === 'object') {
// the single child of `detail` names the fault, e.g.
// `<InvalidDeviceSpecFault xsi:type="InvalidDeviceSpec">`
for (const [name, value] of Object.entries(detail)) {
if (name === 'attributes') {
continue
}
code = value?.attributes?.['xsi:type'] ?? faultTypeOfElementName(name)
localizedMessage = valueOf(value?.localizedMessage)
break
}
}
if (body !== undefined) {
if (faultstring === undefined) {
const matches = body.match(FAULTSTRING_RE)
if (matches !== null) {
faultstring = unescapeXml(matches[1])
}
}
if (faultcode === undefined) {
const matches = body.match(FAULTCODE_RE)
if (matches !== null) {
faultcode = unescapeXml(matches[1])
}
}
if (code === undefined) {
const matches = body.match(DETAIL_CHILD_RE)
if (matches !== null) {
code = faultTypeOfElementName(matches[1])
}
}
if (localizedMessage === undefined) {
const matches = body.match(LOCALIZED_MESSAGE_RE)
if (matches !== null) {
localizedMessage = unescapeXml(matches[1])
}
}
}
return { code, faultcode, faultstring, localizedMessage, body }
}