Publish-20240919-105900/PowerPAN/Private/New-MultipartFormData.ps1

<#
   .SYNOPSIS
   Creates multipart/form-data Content-Type Header and Body with unquoted boundary value in Content-Type header.
   .DESCRIPTION
   Built to workaround limitations in PAN-OS XML-API with quoted boundary in OUTER Content-Type header.
   Returns a PSCustomObject with Header and Body properties which can be used as input to Invoke-WebRequest/Invoke-RestMethod
   .NOTES
   PAN-OS XML-API fails when with multipart/form-data POSTs when the boundary value is quoted in the Content-Type header.

   Issue below captures the challenge nicely
   https://github.com/PowerShell/PowerShell/issues/9241

   .NET System.Net.Http.MultipartFormDataContent also DOES quote the boundary. Not an option.
   In PowerShell 7+, Invoke-WebRequest -Form, Invoke-RestMethod -Form DO quote the boundary. Not an option.
   Needed to build something to keep the Content-Type boundary value unquoted.

   MIME mapping is limited to a few defined file extensions. Can be extended.

   Some additional content
   https://www.reddit.com/r/paloaltonetworks/comments/l47a4h/upload_certificate_via_api/
   https://stackoverflow.com/questions/25075010/upload-multiple-files-from-powershell-script
   https://stackoverflow.com/questions/22491129/how-to-send-multipart-form-data-with-powershell-invoke-restmethod
   .INPUTS
   None
   .OUTPUTS
   PSCustomObject
   .PARAMETER Boundary
   Specify a boundary value. If not provided, a GUID will be generated and used.
   .PARAMETER UnquotedBoundary
   Switch parameter that when specified, boundary value in the Content-Type header will be unquoted (not quoted).
   .EXAMPLE
   PS> $Data = New-MultipartFormData -File "C:\path\to\file.p12" -UnquotedBoundary
   PS> Invoke-WebRequest -Method Post -Uri 'https://...' -ContentType $Data.Header.ContentType -Body $Data.Body ...
#>

function New-MultipartFormData {
   [CmdletBinding()]
   param(
      [parameter(Mandatory=$true,Position=0,HelpMessage='File(s) to be rendered')]
      [System.IO.FileInfo[]] $File,
      [parameter(HelpMessage='Specified boundary. Optional')]
      [String] $Boundary,
      [parameter(HelpMessage='Special processing for unquoted boundary definition')]
      [Switch] $UnquotedBoundary
   )

   # Propagate -Debug and -Verbose to this module function, https://tinyurl.com/y5dcbb34
   if($PSBoundParameters.Debug) { $DebugPreference = 'Continue' }
   if($PSBoundParameters.Verbose) { $VerbosePreference = 'Continue' }
   # Announce
   Write-Debug ($MyInvocation.MyCommand.Name + ':')

   # Structure of the returned object
   $MPFData = [PSCustomObject]@{
      'Header' = [PSCustomObject]@{
         'ContentType' = [String]$null
      };
      'Body' = [String]$null
   }

   if($PSBoundParameters.ContainsKey('Boundary')) {
      $Boundary = $PSBoundParameters.Boundary
   }
   else {
      $Boundary = [System.Guid]::NewGuid().ToString()
   }
   Write-Debug ($MyInvocation.MyCommand.Name + ": Using boundary $Boundary")

   # Newline to be used
   $LF = "`r`n"

   # OUTER HTTP Content-Type header (Invoke-WebRequest ContentType parameter, no hyphen)
   # Implementation Note: PAN-OS XML-API does NOT support a quoted boundary on the OUTER Content-Type where the boundary is first defined
   # Works: Content-Type: multipart/form-data; boundary=asdf1234
   # Fails: Content-Type: multipart/form-data; boundary="asdf1234"
   if($PSBoundParameters.UnquotedBoundary.IsPresent) {
      $MPFData.Header.ContentType = "multipart/form-data; boundary=$Boundary"
      Write-Debug ($MyInvocation.MyCommand.Name + ": Unquoted boundary: $($MPFData.Header.ContentType)")
   }
   else {
      $MPFData.Header.ContentType = "multipart/form-data; boundary=`"$Boundary`""
      Write-Debug ($MyInvocation.MyCommand.Name + ": Quoted boundary: $($MPFData.Header.ContentType)")
   }

   # Loop through one or more files
   foreach($FileCur in $PSBoundParameters.File) {
      if(Test-Path -Path $FileCur.FullName -Type Leaf) {
         # Body content. In the body, the boundary is always NOT quoted. No special changes for PAN-OS here
         $MPFData.Body += "--$Boundary$LF"
         $MPFData.Body += "Content-Disposition: form-data; name=`"file`"; filename=`"$($FileCur.Name)`"$LF"
         # Determine MIME type for INNER Content-Type
         $MimeMap = @{
            'cer' = 'application/pkix-cert';
            'pem' = 'application/x-pem-file';
            'p12' = 'application/x-pkcs12';
            'pfx' = 'application/x-pkcs12'
         }
         if($MimeMap.ContainsKey($FileCur.Extension.TrimStart('.'))) {
            $MPFData.Body += 'Content-Type: ' + $MimeMap.$($FileCur.Extension.TrimStart('.')) + "$LF"
         }
         else {
            $MPFData.Body += "Content-Type: application/octet-stream$LF"
         }

         # Required blank line between INNER content header and inner content
         $MPFData.Body += "$LF"

         # File Content
         # Read file as byte array
         $FileCurBin = [System.IO.File]::ReadAllBytes($FileCur.FullName)

         # Convert to string without changing and add to body
         $MPFData.Body += $([System.Text.Encoding]::GetEncoding('iso-8859-1')).GetString($FileCurBin) + "$LF"
      } # end if(Test-Path)
      else {
         Write-Error -Message "$($FileCur.FullName) not found"
      }
   }

   # End INNER content boundary with final two dashes "--" after boundary value, unquoted per standard. No special changes for PAN-OS
   $MPFData.Body += "--$Boundary--$LF"

   # Return $MPFData, including Header(s) and Body
   $MPFData
}