0

Using PS 5.1.14409.1005 on W2K12 R2 x64.

So far as I can see there are two ways to do this -

  1. Create the multipart/form body manually following the RFC format
  2. Use MultipartFormDataContent .NET types

I tried MultipartFormDataContent but it isn't working when I add the constructed object to Invoke-WebRequest's body parameter (see below for example), and I had success constructing manually with text form parts but I'm having trouble with binary. The screenshot shows a fiddler request capture from powershell's Invoke-WebRequest which doesn't work and one from chrome which does. The binary content is different and I'm not sure how to get it the same as chrome.

Here are the first few bytes as hex comparing the actual file to what chrome and powershell sent in fiddler. There are small differences throughout the rest...

file:   30 82 1D 32 02 01 03 30 82 1C F8
chrome: 30 82 1D 32 02 01 03 30 82 1C F8
ps:     30 2C 1D 32 02 01 03 30 2C 1C F8

Here is my current code. The binary section is being populated by Get-Content -Path $FilePath -Raw.

function Publish-RestApiFile {
    param ([string] $Url, [hashtable] $Params, [string] $FilePath, [string] $FileParamName)
    Add-Type -AssemblyName System.Web
    $bnd = [System.Guid]::NewGuid().ToString().ToLower().Replace('-','').PadLeft(40,'-')
    $content_type = "multipart/form-data; boundary=$bnd"
    $LF = "`r`n"
    $lines = @("--${bnd}${LF}")
    $Params.GetEnumerator() | ForEach-Object {
        $lines += "$('Content-Disposition: form-data; name="{0}"' -f $_.Name)${LF}${LF}$($_.Value)${LF}"
        $lines += "--${bnd}${LF}"
    }
    $file_name = Split-Path -Path $FilePath -Leaf
    $mime_type = [System.Web.MimeMapping]::GetMimeMapping($FilePath)
    $ct = Get-Content -Path $FilePath -Raw
    $lines += ('Content-Disposition: form-data; name="{0}"; filename="{1}"' -f $FileParamName,$file_name)+${LF}
    $lines += ("Content-Type: ${mime_type}${LF}${LF}"),$ct,${LF}
    $lines += ("--${bnd}--"+${LF})
    $lines = $lines -join ''
    Invoke-RestMethod -Uri $url -Method Post -Body $lines -ContentType $content_type -MaximumRedirection 0
}

This shows the body string generated by the current code - Generated multpart/form format

This shows the differences in binary from the request captures in fiddler between PS Invoke-WebRequest (LEFT) and chrome (RIGHT). PowerShell (LEFT) Chrome (RIGHT)

Here is the example using MultipartFormDataContent. Fiddler shows the object is just being cast to string in the body e.g. System.Net.Http.StringContent System.Net.Http.StringContent System.Net.Http.StreamContent.

function Publish-RestApiFile2 {
    param ([string] $Url, [hashtable] $Params, [string] $FilePath, [string] $FileParamName)
    Add-Type -AssemblyName System.Net.Http, System.Web

    $mime_type = [System.Web.MimeMapping]::GetMimeMapping($FilePath)
    $form = [System.Net.Http.MultipartFormDataContent]::New()
    foreach ($param in $Params.GetEnumerator()) {
        $header = [System.Net.Http.Headers.ContentDispositionHeaderValue]::new("form-data")
        $header.Name = $param.Name
        $content = [System.Net.Http.StringContent]::new($param.Value)
        $content.Headers.ContentDisposition = $header
        $form.Add($content)
    }
    $stream = [System.IO.FileStream]::New($FilePath, 'Open')
    $header = [System.Net.Http.Headers.ContentDispositionHeaderValue]::new("form-data")
    $header.Name = $FileParamName
    $header.FileName = (Split-Path -Path $FilePath -Leaf)
    $content = [System.Net.Http.StreamContent]::New($stream)
    $content.Headers.ContentDisposition = $header
    $content.Headers.ContentType = [System.Net.Http.Headers.MediaTypeHeaderValue]::Parse($mime_type)
    $form.Add($content)

    Invoke-WebRequest -Uri $Url -Method Post -Body $form -ContentType 'multipart/form-data'
    #Invoke-WebRequest -Uri $Url -Method Post -Body $form
    $stream.Close(); $stream.Dispose()
}
1
  • Have you tried replacing Get-Content with [Io.File]::ReadAllBytes()? I've found many performance-related problems with Get-Content that I try to avoid it at all costs. Commented Mar 31, 2020 at 18:34

1 Answer 1

3

I got it working using all .NET types rather than Invoke-WebRequest -

In case it helps someone -

Add-Type -AssemblyName System.Net.Http

$url = "https://server.local/restendpoint"
$file = "C:\cert.pfx"

$fs = [System.IO.FileStream]::New($file, [System.IO.FileMode]::Open)

$s1 = New-Object System.Net.Http.StringContent 'alias_value'
$s2 = New-Object System.Net.Http.StringContent 'password_value'
$f1 = New-Object System.Net.Http.StreamContent $fs

$handler = New-Object System.Net.Http.HttpClientHandler
$handler.AllowAutoRedirect = $false # Don't follow after post redirect code 303
$client = New-Object System.Net.Http.HttpClient -ArgumentList $handler
$client.DefaultRequestHeaders.ConnectionClose = $true # Disable keep alive, get a 200 response rather than 303
$form = New-Object System.Net.Http.MultipartFormDataContent

$form.Add($s1, 'alias')
$form.Add($s2, 'password')
$form.Add($f1, 'file', 'cert.pfx')

$rsp = $client.PostAsync($u, $form).Result
$rsp.IsSuccessStatusCode # false if 303
$rsp.StatusCode -eq 303 # true if 303

$fs.Close(); $fs.Dispose()
Sign up to request clarification or add additional context in comments.

Comments

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.