1

Situation:

We collect automatically many reports from some web services (PowerShell script running every night), and every day in manual mode (drag and drop on web-form) this reports are loaded in our DB.

Now our IT department gave us an API that can handle this job without user interaction.

Problem:

As was written in covering letter (about this API) it waits for reports[n] array with file. It can be done with PHP and curl:

$report = 'report_20160825.{8302F59C-E1E4-410F-BE37-A24CCD7E515E}.zip';
$cfile = new CURLFile(realpath($report),'application/zip',$report);
$PostData = array("reports[0]"=>$cfile);

But how to send array named reports[n] via PowerShell?

What I have tried:

$url = "https://test.example.com/uploadAPI/upload.php"
$Source =  "D:\report_20160825.{8302F59C-E1E4-410F-BE37-A24CCD7E515E}.zip"
$contentType = "multipart/form-data"

$Username = "ApiUploadKey"
$Headers = @{Authorization="Basic {0}" -f [Convert]::ToBase64String([Text.Encoding]::ASCII.GetBytes(("{0}:" -f $Username)))}

$FileContens = get-content $Source
$PostData = @{"reports[0]" = $FileContens;} 
#$reports = @($FileContens,'application/zip',$Source)

(Invoke-WebRequest -uri $url -Method POST -Headers $Headers -Body $PostData -ContentType $contentType).Content

#Invoke-RestMethod -uri $url -Method POST -Headers $Headers -Body $PostData -ContentType $contentType

That gives me a response that I am passing not-a-report.

EDIT 2016-10-11

Further investigation bring me to this answer and this article. I tried to use boundary:

clear
$url = "https://test.example.com/uploadAPI/upload.php"
$filename = "report_20160825.{8302F59C-E1E4-410F-BE37-A24CCD7E515E}.zip"
$Source =  "D:\"+$filename

$Username = "ApiUploadKey"
$Headers = @{Authorization="Basic {0}" -f [Convert]::ToBase64String([Text.Encoding]::ASCII.GetBytes(("{0}:" -f $Username)))}

$FileContens = get-content $Source
$enc = [System.Text.Encoding]::GetEncoding("iso-8859-1")
$fileBin = [IO.File]::ReadAllBytes($Source)
$fileEnc = $enc.GetString($fileBin)


$boundary = [System.Guid]::NewGuid().ToString()

$LF = "`n"

$contentType = "multipart/form-data; boundary=--$boundary"
#$bodyLines = "--"+$boundary+$LF+"Content-Disposition: form-data; name=`"reports[]`"; filename=`""+$filename+"`""+$LF+$LF+"Content-Type: application/zip"+$LF+"--"+$boundary+"--"+$LF+$LF+$FileContens+$LF+"--"+$boundary

$bodyLines = (
    "--$boundary",   #I have tried reports[0] here too
    "Content-Disposition: form-data; name=`"reports[]`"; filename=`"$filename`"",   # filename= is optional
    "Content-Type: application/zip",
    "",
    #$FileContens,
    $fileEnc,
    "--$boundary--"
    ) -join $LF


try {

    #Invoke-WebRequest -Uri "https://asrp.cntd.ru/uploadAPI/" -Headers $Headers -WebSession $ws
    Invoke-RestMethod  -Uri $url -Body $bodyLines -Method POST -Headers $Headers -ContentType $contentType -TimeoutSec 50
}
catch [System.Net.WebException] {
    Write-Error( "FAILED to reach '$url': $_" )
    throw $_
}

But with same results.

Also I tried this:

$wc = new-object System.Net.WebClient
$wc.Credentials = new-object System.Net.NetworkCredential("ApiUploadKey","")

ls "D:\*.zip" | foreach { 
    $wc.UploadFile('https://test.example.com/uploadAPI/upload.php', $_.FullName )
    write-host $_.FullName
}

And one more solution from this answer:

Invoke-RestMethod -Uri $url -InFile $Source -ContentType "multipart/form-data" -Method POST -Headers $Headers

Always same response - not a report

EDIT 2016-10-17

curl

I have downloaded curl for windows. And use it like:

curl.exe https://test.example.com/uploadAPI/upload.php --user ApiUploadKey: --form "reports[0]=@d:\report_746_226255_20161010_1635.zip;type=application/zip"

And that gave me:

[{"code" : 102 , "guid" : "{23CE9F7F-BEC8-4D4C-8AC3-2865CFA94FBD}" , "id" : "5804902bc73a2475177464", "filename" : "report_746_226255_20161010_1635.zip"}]

So with curl it works fine!

fiddler

Don't know exactly what log to post.

When I send file like this:

POST https://test.example.com/uploadAPI/upload.php

Content-Type: multipart/form-data; boundary=-------------------------acebdf13572468
User-Agent: Fiddler
Host: test.example.com
Authorization: Basic ...
Content-Length: 21075175

Request body:

---------------------------acebdf13572468
Content-Disposition: form-data; name="reports[]"; filename="report_746_226254_20161010_1320.{B67A9D89-368B-4665-96AC-77C2CA0F4766}.zip"
Content-Type: application/zip

<@INCLUDE *D:\report_746_226254_20161010_1320.{B67A9D89-368B-4665-96AC-77C2CA0F4766}.zip*@>
---------------------------acebdf13572468--

I got

HTTP/1.1 200 OK
Date: Mon, 17 Oct 2016 10:04:43 GMT
Server: Apache/2.4.20 (Win64) OpenSSL/1.0.2h PHP/7.0.6
X-Powered-By: PHP/7.0.6
Set-Cookie: SESSION_UPLOAD_ID=.....; path=/
Expires: Thu, 19 Nov 1981 08:52:00 GMT
Cache-Control: no-store, no-cache, must-revalidate
Pragma: no-cache
Connection: close
Content-Length: 193
Content-Encoding: none
Accept-Ranges: bytes
Content-Type: text/html; charset=UTF-8

[{"code" : 102 , "guid" : "{B67A9D89-368B-4665-96AC-77C2CA0F4766}" , "id" : "5804a23be4152532018928", "filename" : "report_746_226254_20161010_1320.{B67A9D89-368B-4665-96AC-77C2CA0F4766}.zip"}]
gofr1
  • 15,741
  • 11
  • 42
  • 52
  • try set content-type, -ContentType "multipart/form-data" – Alex Muravyov Sep 03 '16 at 15:45
  • I tried it. As you can see there is a `$contentType` variable. It was used like `-ContentType $contentType` in `Invoke-WebRequest`. Should edit my question. – gofr1 Sep 03 '16 at 15:50
  • TL;DR. Perhaps you can simplify the question. – Burt_Harris Oct 16 '16 at 01:28
  • @Burt_Harris Just read above **edit 2016-10-11**. In edit are listed my attempts. – gofr1 Oct 16 '16 at 05:23
  • @Burt_Harris in few words: I want to know if I can send web-request (with invoke-webrequest or invoke-restmethod) with array named reports[n] that contains file (like PHP curl sample) in PowerShell. Basically I already wrote a PHP solution, but I'm very curious about PS way to make it work. – gofr1 Oct 16 '16 at 07:41
  • Can you show us the command line parameters using the the cUrl.exe tool not php curl class and if it's working. – M.Hassan Oct 16 '16 at 14:36
  • @M.Hassan I will check it on monday and post my results. I thought of curl too, but was hoping to use PS build-in methods. – gofr1 Oct 16 '16 at 14:41
  • The data format desired by the web service is still unclear to me. If the web-service is expecting the payload to be a zip file, its probably pretty simple in PowerShell, but combining text with binary bytes in building a multi-part MIME message seems like it it's overcomplicating matters. I suggest you capture a successful transaction with Fiddler2 to clarify what the expected HTTP request format is, and post that to clarify what's needed. – Burt_Harris Oct 17 '16 at 03:24
  • P.S. Edit out the authentication data before posting... – Burt_Harris Oct 17 '16 at 03:48
  • @Burt_Harris thanks for advice, I will try it. – gofr1 Oct 17 '16 at 03:49
  • @Burt_Harris the report, that API awaits, is zip file with html report and SQLite DB. API, as I know is PHP based, maybe I could post the code that API uses to fetch the files. – gofr1 Oct 17 '16 at 04:16
  • I'm sorry, I don't know PHP, and I'm having trouble understanding you – Burt_Harris Oct 17 '16 at 07:26
  • @M.Hassan I add curl results, it works fine! – gofr1 Oct 17 '16 at 08:50
  • @Burt_Harris I add some fiddler logs. I use it for first time, so maybe I missed some info to post. – gofr1 Oct 17 '16 at 10:10
  • "Does webrequest support arrays as post form parameters?" http://stackoverflow.com/questions/24706386/does-invoke-webrequest-support-arrays-as-post-form-parameters?rq=1 possibly? – Eris Oct 17 '16 at 17:38
  • @Eris thanks for the link. It was in 2014, now is the end of 2016. There is PowerShell 4 and in Windows Server 2016 MS is releasing PowerShell 5... I do hope they make that stuff working! Or I am wrong? :( – gofr1 Oct 17 '16 at 17:52

1 Answers1

1

The PHP code use Curl class and set $PostData as:

 $PostData = array("reports[0]"=>$cfile);

$PostData is an array of key/value pair (in PHP), and the key is named Reports[0]. it's just a string name represent the key not an array element.

in curl commandLine tool we can upload file with the command:

 curl.exe   -F Reports[0]=@"I:\test\report_20160825.{8302F59C-E1E4-410F-BE37-A24CCD7E515E}.zip"  http://MyServer/uploader/api/upload

or, it may be (note the key Reports[0][2][3] ):

   curl.exe   -F Reports[0][2][3]=@"I:\test\report_20160825.{8302F59C-E1E4-410F-BE37-A24CCD7E515E}.zip"  http://asd-pc/uploader/api/upload

-F is for entering Form key/value pair Note @ before filename Try it in your server and view the session in Fiddler to know how curl and PHP send the request to the server.

You can pass other parameters like -H (header) -v (verbose) -F for other files with other key name e.g Reports1.

I tested this code with My web server running Web Api2 service that upload file to the server and it's running fine.

So, the problem is: how to convert that code to work with Powershell.

In curl either PHP or commandLine tool , it auto prepare the Header and the Request Body for file uploading that support Multipart/form-data.

Both Powershell commands: Invoke-WebRequest and Invoke-RestMethod are unaware on how to format the request body in order to comply to the standard of Multipart/form-data as given in the RFC Forms: multipart/form-data

You have to manually set the message body and then invoke your call

The following script Upload file to web server using powershell.

I set Content-Type: application/octet-stream to support any type including zip files.

    function Upload-File ( $InFile,$Uri    )
    {    
        $Username = "ApiUploadKey"
        $Headers = @{Authorization="Basic {0}" -f [Convert]::ToBase64String([Text.Encoding]::ASCII.GetBytes(("{0}:" -f $Username)))}
        $LF = "`n"   
        $fileName = Split-Path $InFile -leaf
        $boundary = [System.Datetime]::Now.Ticks.ToString()
        $binaryData = [System.IO.File]::ReadAllBytes($InFile)   
        $binaryEncoded=[System.Text.Encoding]::GetEncoding("iso-8859-1").GetString($binaryData) 
        $ContentType = "application/octet-stream" 

        $body = @"
    --$boundary
    Content-Disposition: form-data; name=`"Reports[0]`"; filename=`"$fileName`"
    Content-Type: $ContentType
    $LF
    $binaryEncoded
    --$boundary--
    $LF
    "@  

        try
        { 

          return Invoke-RestMethod -Uri $Uri -Method Post -ContentType "multipart/form-data; boundary=$boundary"   -Body $body -Headers $Headers                             

        }
        catch [Exception]
        {
            $PSCmdlet.ThrowTerminatingError($_)
        }
    }


    #--   test the function ---------------------------------
    cls
    $uri = "http://MyServer/uploader/api/upload"        
    $filePath = "I:\test\report_20160825.{8302F59C-E1E4-410F-BE37-A24CCD7E515E}.zip"
    $response = Upload-File -InFile $filePath -Uri $uri  #-Credential $credentials
    $response

The output session from fiddler

    POST http://MyServer/uploader/api/upload HTTP/1.1
    Authorization: Basic QXBpVXBsb2FkS2V5Og==
    User-Agent: Mozilla/5.0 (Windows NT; Windows NT 6.1; en-US) WindowsPowerShell/5.0.10586.117
    Content-Type: multipart/form-data; boundary=636123339963709320
    Host: MyServer
    Content-Length: 379
    Connection: Keep-Alive

    --636123339963709320
    Content-Disposition: form-data; name="Reports[0]"; filename="report_20160825.{8302F59C-E1E4-410F-BE37-A24CCD7E515E}.zip"
    Content-Type: application/octet-stream


    xxxxxxxxxxxxxxxxxxxxxxxxxxxStream of byte sxxxxxxxxxxxxxxx

In the session data above you see Reports[0] is titled name

You can use the same code with the command: Invoke-WebRequest

M.Hassan
  • 10,282
  • 5
  • 65
  • 84