private/functions/Invoke-AxisSoapRequest.ps1

function Invoke-AxisSoapRequest {
    <#
    .SYNOPSIS
    Sends a SOAP request to the VAPIX web services endpoint of an Axis device.

    .DESCRIPTION
    Wraps the supplied XML body in a SOAP 1.2 envelope, posts it to /vapix/services via Invoke-AxisWebApi
    and returns the Body element of the response as [xml]. SOAP faults are thrown as terminating errors.
    Used for the ONVIF / Axis web service operations that have no JSON equivalent (certificate management on
    older firmware and the web server TLS configuration).

    .PARAMETER Device
    The IP address or hostname of the Axis device.

    .PARAMETER Body
    The XML fragment to place inside the SOAP Body element.

    .PARAMETER Namespaces
    Optional hashtable of additional namespace prefixes to declare on the envelope. The common ONVIF/Axis
    prefixes (tds, tt, acert, acertificates, aweb) are always declared.
    #>

    [cmdletbinding()]
    Param(
        [Parameter(Mandatory)]
        [String]$Device,

        [Parameter(Mandatory)]
        [String]$Body,

        [Parameter()]
        [Hashtable]$Namespaces=@{},

        [Parameter(DontShow)]
        [Switch]$Retried
    )

    $DefaultNamespaces = @{
        tds = "http://www.onvif.org/ver10/device/wsdl"
        tt = "http://www.onvif.org/ver10/schema"
        acert = "http://www.axis.com/vapix/ws/cert"
        acertificates = "http://www.axis.com/vapix/ws/certificates"
        aweb = "http://www.axis.com/vapix/ws/webserver"
    }
    ForEach($Key in $Namespaces.Keys) {
        $DefaultNamespaces[$Key] = $Namespaces[$Key]
    }

    $NamespaceString = ($DefaultNamespaces.Keys | Sort-Object | ForEach-Object { "xmlns:$($_)=`"$($DefaultNamespaces[$_])`"" }) -join " "
    $Envelope = @"
<?xml version="1.0" encoding="UTF-8"?>
<SOAP-ENV:Envelope xmlns:SOAP-ENV="http://www.w3.org/2003/05/soap-envelope" $NamespaceString>
    <SOAP-ENV:Body>
        $Body
    </SOAP-ENV:Body>
</SOAP-ENV:Envelope>
"@


    $Param = @{
        Device = $Device
        Path = "/vapix/services"
        Method = "POST"
        Body = $Envelope
        ContentType = "application/soap+xml; charset=utf-8"
        WebRequest = $true
        SkipHttpErrorCheck = $true
    }

    Try {
        $Response = Invoke-AxisWebApi @Param
        $Content = $Response.Content
    }
    Catch {
        #SOAP faults come back as HTTP 500 with an XML body; surface the fault reason if there is one
        $Content = $null
        if($_.ErrorDetails.Message) {
            $Content = $_.ErrorDetails.Message
        }
        elseif($_.Exception.Response) {
            Try {
                $Stream = $_.Exception.Response.GetResponseStream()
                $Reader = New-Object System.IO.StreamReader($Stream)
                $Content = $Reader.ReadToEnd()
            } Catch {}
        }
        if(!$Content) {
            Throw $_
        }
    }

    Try {
        [xml]$Xml = $Content
    }
    Catch {
        #Seen once on AXIS OS 9.80 right after a web server restart: an empty/HTML body. Retry the request once.
        if(!$Retried) {
            Write-Verbose "Non-XML SOAP response from $Device; retrying once in 2 seconds"
            Start-Sleep -Seconds 2
            Try {
                return Invoke-AxisSoapRequest -Device $Device -Body $Body -Namespaces $Namespaces -Retried
            }
            Catch {
                Throw $_
            }
        }
        $Snippet = ""
        if($Content) { $Snippet = " (" + ($Content.Substring(0, [Math]::Min(120, $Content.Length)) -replace '\s+', ' ') + ")" }
        Throw "Device returned a non-XML response to the SOAP request$Snippet"
    }

    #Elements are namespace-prefixed (SOAP-ENV:Fault), so match on local name + namespace
    $SoapNs = "http://www.w3.org/2003/05/soap-envelope"
    $Fault = $Xml.GetElementsByTagName("Fault", $SoapNs) | Select-Object -First 1
    if($Fault) {
        $Reason = ($Fault.GetElementsByTagName("Text", $SoapNs) | Select-Object -First 1).InnerText
        $Detail = ($Fault.GetElementsByTagName("Detail", $SoapNs) | Select-Object -First 1).InnerText
        $Code = ($Fault.GetElementsByTagName("Value", $SoapNs) | Select-Object -Last 1).InnerText
        Throw "SOAP Fault: $Reason - $Detail ($Code)"
    }

    return $Xml.Envelope.Body
}