Add Multipart Support to Web Cmdlets (#4782)
Partially implements #2112 - Adds `System.Net.Http.MultipartFormDataContent` as a possible type for `-Body` - Adds `/Multipart/` test to WebListener This allows for the user to create their own `Http.MultipartFormDataContent` object and submit it. Since `multipart/form-data` submissions are highly flexible, adding direct support for it to the CmdLets may over-complicate the command parameters and a limited implementation would not address the broad scope of use cases. This at least allows the user to submit multipart forms using the Web Cmdlets and not have to manage their own `HttpClient`. Once this is introduced, limited multipart implementations can be expanded to use the code in this PR.
This commit is contained in:
parent
1e4cbe15ff
commit
fd3a003765
@ -67,6 +67,7 @@ macOS
|
||||
Microsoft.PowerShell.Archive
|
||||
MS-PSRP
|
||||
myget
|
||||
Multipart
|
||||
New-PSSessionOption
|
||||
New-PSTransportOption
|
||||
NuGet
|
||||
|
@ -389,6 +389,11 @@ namespace Microsoft.PowerShell.Commands
|
||||
byte[] bytes = content as byte[];
|
||||
SetRequestContent(request, bytes);
|
||||
}
|
||||
else if (content is MultipartFormDataContent multipartFormDataContent)
|
||||
{
|
||||
WebSession.ContentHeaders.Clear();
|
||||
SetRequestContent(request, multipartFormDataContent);
|
||||
}
|
||||
else
|
||||
{
|
||||
SetRequestContent(request,
|
||||
@ -787,6 +792,32 @@ namespace Microsoft.PowerShell.Commands
|
||||
return streamContent.Headers.ContentLength.Value;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets the ContentLength property of the request and writes the specified content to the request's RequestStream.
|
||||
/// </summary>
|
||||
/// <param name="request">The WebRequest who's content is to be set</param>
|
||||
/// <param name="multipartContent">A MultipartFormDataContent object containing multipart/form-data content.</param>
|
||||
/// <returns>The number of bytes written to the requests RequestStream (and the new value of the request's ContentLength property</returns>
|
||||
/// <remarks>
|
||||
/// Because this function sets the request's ContentLength property and writes content data into the requests's stream,
|
||||
/// it should be called one time maximum on a given request.
|
||||
/// </remarks>
|
||||
internal long SetRequestContent(HttpRequestMessage request, MultipartFormDataContent multipartContent)
|
||||
{
|
||||
if (request == null)
|
||||
{
|
||||
throw new ArgumentNullException("request");
|
||||
}
|
||||
if (multipartContent == null)
|
||||
{
|
||||
throw new ArgumentNullException("multipartContent");
|
||||
}
|
||||
|
||||
request.Content = multipartContent;
|
||||
|
||||
return multipartContent.Headers.ContentLength.Value;
|
||||
}
|
||||
|
||||
internal long SetRequestContent(HttpRequestMessage request, IDictionary content)
|
||||
{
|
||||
if (request == null)
|
||||
|
@ -359,6 +359,39 @@ function ExecuteRestMethod
|
||||
return $result
|
||||
}
|
||||
|
||||
function GetMultipartBody
|
||||
{
|
||||
param
|
||||
(
|
||||
[Switch]$String,
|
||||
[Switch]$File
|
||||
)
|
||||
$multipartContent = [System.Net.Http.MultipartFormDataContent]::new()
|
||||
if ($String.IsPresent)
|
||||
{
|
||||
$stringHeader = [System.Net.Http.Headers.ContentDispositionHeaderValue]::new("form-data")
|
||||
$stringHeader.Name = "TestString"
|
||||
$StringContent = [System.Net.Http.StringContent]::new("TestValue")
|
||||
$StringContent.Headers.ContentDisposition = $stringHeader
|
||||
$multipartContent.Add($stringContent)
|
||||
}
|
||||
if ($File.IsPresent)
|
||||
{
|
||||
$multipartFile = Join-Path $TestDrive 'multipart.txt'
|
||||
"TestContent" | Set-Content $multipartFile
|
||||
$FileStream = [System.IO.FileStream]::new($multipartFile, [System.IO.FileMode]::Open)
|
||||
$fileHeader = [System.Net.Http.Headers.ContentDispositionHeaderValue]::new("form-data")
|
||||
$fileHeader.Name = "TestFile"
|
||||
$fileHeader.FileName = 'multipart.txt'
|
||||
$fileContent = [System.Net.Http.StreamContent]::new($FileStream)
|
||||
$fileContent.Headers.ContentDisposition = $fileHeader
|
||||
$fileContent.Headers.ContentType = [System.Net.Http.Headers.MediaTypeHeaderValue]::Parse("text/plain")
|
||||
$multipartContent.Add($fileContent)
|
||||
}
|
||||
# unary comma required to prevent $multipartContent from being unwrapped/enumerated
|
||||
return ,$multipartContent
|
||||
}
|
||||
|
||||
<#
|
||||
Defines the list of redirect codes to test as well as the
|
||||
expected Method when the redirection is handled.
|
||||
@ -1200,6 +1233,41 @@ Describe "Invoke-WebRequest tests" -Tags "Feature" {
|
||||
}
|
||||
}
|
||||
|
||||
Context "Multipart/form-data Tests" {
|
||||
It "Verifies Invoke-WebRequest Supports Multipart String Values" {
|
||||
$body = GetMultipartBody -String
|
||||
$uri = Get-WebListenerUrl -Test 'Multipart'
|
||||
$response = Invoke-WebRequest -Uri $uri -Body $body -Method 'POST'
|
||||
$result = $response.Content | ConvertFrom-Json
|
||||
|
||||
$result.Headers.'Content-Type' | Should Match 'multipart/form-data'
|
||||
$result.Items.TestString[0] | Should Be 'TestValue'
|
||||
}
|
||||
It "Verifies Invoke-WebRequest Supports Multipart File Values" {
|
||||
$body = GetMultipartBody -File
|
||||
$uri = Get-WebListenerUrl -Test 'Multipart'
|
||||
$response = Invoke-WebRequest -Uri $uri -Body $body -Method 'POST'
|
||||
$result = $response.Content | ConvertFrom-Json
|
||||
|
||||
$result.Headers.'Content-Type' | Should Match 'multipart/form-data'
|
||||
$result.Files[0].FileName | Should Be 'multipart.txt'
|
||||
$result.Files[0].ContentType | Should Be 'text/plain'
|
||||
$result.Files[0].Content | Should Match 'TestContent'
|
||||
}
|
||||
It "Verifies Invoke-WebRequest Supports Mixed Multipart String and File Values" {
|
||||
$body = GetMultipartBody -String -File
|
||||
$uri = Get-WebListenerUrl -Test 'Multipart'
|
||||
$response = Invoke-WebRequest -Uri $uri -Body $body -Method 'POST'
|
||||
$result = $response.Content | ConvertFrom-Json
|
||||
|
||||
$result.Headers.'Content-Type' | Should Match 'multipart/form-data'
|
||||
$result.Items.TestString[0] | Should Be 'TestValue'
|
||||
$result.Files[0].FileName | Should Be 'multipart.txt'
|
||||
$result.Files[0].ContentType | Should Be 'text/plain'
|
||||
$result.Files[0].Content | Should Match 'TestContent'
|
||||
}
|
||||
}
|
||||
|
||||
BeforeEach {
|
||||
if ($env:http_proxy) {
|
||||
$savedHttpProxy = $env:http_proxy
|
||||
@ -1737,6 +1805,38 @@ Describe "Invoke-RestMethod tests" -Tags "Feature" {
|
||||
}
|
||||
}
|
||||
|
||||
Context "Multipart/form-data Tests" {
|
||||
It "Verifies Invoke-RestMethod Supports Multipart String Values" {
|
||||
$body = GetMultipartBody -String
|
||||
$uri = Get-WebListenerUrl -Test 'Multipart'
|
||||
$result = Invoke-RestMethod -Uri $uri -Body $body -Method 'POST'
|
||||
|
||||
$result.Headers.'Content-Type' | Should Match 'multipart/form-data'
|
||||
$result.Items.TestString[0] | Should Be 'TestValue'
|
||||
}
|
||||
It "Verifies Invoke-RestMethod Supports Multipart File Values" {
|
||||
$body = GetMultipartBody -File
|
||||
$uri = Get-WebListenerUrl -Test 'Multipart'
|
||||
$result = Invoke-RestMethod -Uri $uri -Body $body -Method 'POST'
|
||||
|
||||
$result.Headers.'Content-Type' | Should Match 'multipart/form-data'
|
||||
$result.Files[0].FileName | Should Be 'multipart.txt'
|
||||
$result.Files[0].ContentType | Should Be 'text/plain'
|
||||
$result.Files[0].Content | Should Match 'TestContent'
|
||||
}
|
||||
It "Verifies Invoke-RestMethod Supports Mixed Multipart String and File Values" {
|
||||
$body = GetMultipartBody -String -File
|
||||
$uri = Get-WebListenerUrl -Test 'Multipart'
|
||||
$result = Invoke-RestMethod -Uri $uri -Body $body -Method 'POST'
|
||||
|
||||
$result.Headers.'Content-Type' | Should Match 'multipart/form-data'
|
||||
$result.Items.TestString[0] | Should Be 'TestValue'
|
||||
$result.Files[0].FileName | Should Be 'multipart.txt'
|
||||
$result.Files[0].ContentType | Should Be 'text/plain'
|
||||
$result.Files[0].Content | Should Match 'TestContent'
|
||||
}
|
||||
}
|
||||
|
||||
#region charset encoding tests
|
||||
|
||||
Context "Invoke-RestMethod Encoding tests with BasicHtmlWebResponseObject response" {
|
||||
|
@ -116,6 +116,7 @@ function Get-WebListenerUrl {
|
||||
'Cert',
|
||||
'Get',
|
||||
'Home',
|
||||
'Multipart',
|
||||
'/'
|
||||
)]
|
||||
[String]$Test
|
||||
|
89
test/tools/WebListener/Controllers/MultipartController.cs
Normal file
89
test/tools/WebListener/Controllers/MultipartController.cs
Normal file
@ -0,0 +1,89 @@
|
||||
using System;
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Hosting;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.Net.Http.Headers;
|
||||
using mvc.Models;
|
||||
|
||||
|
||||
namespace mvc.Controllers
|
||||
{
|
||||
public class MultipartController : Controller
|
||||
{
|
||||
private IHostingEnvironment _environment;
|
||||
|
||||
public MultipartController(IHostingEnvironment environment)
|
||||
{
|
||||
_environment = environment;
|
||||
}
|
||||
public ActionResult Index()
|
||||
{
|
||||
return View();
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
public JsonResult Index(IFormCollection collection)
|
||||
{
|
||||
if (!Request.HasFormContentType)
|
||||
{
|
||||
Response.StatusCode = 415;
|
||||
Hashtable error = new Hashtable {{"error","Unsupported media type"}};
|
||||
return Json(error);
|
||||
}
|
||||
|
||||
List<Hashtable> fileList = new List<Hashtable>();
|
||||
foreach (var file in collection.Files)
|
||||
{
|
||||
string result = string.Empty;
|
||||
if (file.Length > 0)
|
||||
{
|
||||
using (var reader = new StreamReader(file.OpenReadStream()))
|
||||
{
|
||||
result = reader.ReadToEnd();
|
||||
}
|
||||
}
|
||||
Hashtable fileHash = new Hashtable
|
||||
{
|
||||
{"ContentDisposition" , file.ContentDisposition},
|
||||
{"ContentType" , file.ContentType},
|
||||
{"FileName" , file.FileName},
|
||||
{"Length" , file.Length},
|
||||
{"Name" , file.Name},
|
||||
{"Content" , result},
|
||||
{"Headers" , file.Headers}
|
||||
};
|
||||
fileList.Add(fileHash);
|
||||
}
|
||||
Hashtable itemsHash = new Hashtable();
|
||||
foreach (var key in collection.Keys)
|
||||
{
|
||||
itemsHash.Add(key,collection[key]);
|
||||
}
|
||||
MediaTypeHeaderValue mediaContentType = MediaTypeHeaderValue.Parse(Request.ContentType);
|
||||
Hashtable headers = new Hashtable();
|
||||
foreach (var key in Request.Headers.Keys)
|
||||
{
|
||||
headers.Add(key, String.Join(Constants.HeaderSeparator, Request.Headers[key]));
|
||||
}
|
||||
Hashtable output = new Hashtable
|
||||
{
|
||||
{"Files" , fileList},
|
||||
{"Items" , itemsHash},
|
||||
{"Boundary", HeaderUtilities.RemoveQuotes(mediaContentType.Boundary).Value},
|
||||
{"Headers" , headers}
|
||||
};
|
||||
return Json(output);
|
||||
}
|
||||
|
||||
public IActionResult Error()
|
||||
{
|
||||
return View(new ErrorViewModel { RequestId = Activity.Current?.Id ?? HttpContext.TraceIdentifier });
|
||||
}
|
||||
}
|
||||
}
|
@ -82,3 +82,47 @@ Invoke-WebRequest -Uri 'http://localhost:8083/Get/' -Body @{TestField = 'TestVal
|
||||
"origin": "127.0.0.1"
|
||||
}
|
||||
```
|
||||
|
||||
## /Multipart/
|
||||
|
||||
### GET
|
||||
Provides an HTML form for `multipart/form-data` submission.
|
||||
|
||||
### POST
|
||||
Accepts a `multipart/form-data` submission and returns a JSON object containing information about the submission including the items and files submitted.
|
||||
|
||||
```json
|
||||
{
|
||||
"Files": [
|
||||
{
|
||||
"ContentDisposition": "form-data; name=fileData; filename=test.txt",
|
||||
"Headers": {
|
||||
"Content-Disposition": [
|
||||
"form-data; name=fileData; filename=test.txt"
|
||||
],
|
||||
"Content-Type": [
|
||||
"text/plain"
|
||||
]
|
||||
},
|
||||
"FileName": "test.txt",
|
||||
"Length": 15,
|
||||
"ContentType": "text/plain",
|
||||
"Name": "fileData",
|
||||
"Content": "Test Contents\r\n"
|
||||
}
|
||||
],
|
||||
"Items": {
|
||||
"stringData": [
|
||||
"TestValue"
|
||||
]
|
||||
},
|
||||
"Boundary": "83027bde-fd9b-4ea0-b1ca-a1f661d01ada",
|
||||
"Headers": {
|
||||
"Content-Type": "multipart/form-data; boundary=\"83027bde-fd9b-4ea0-b1ca-a1f661d01ada\"",
|
||||
"Connection": "Keep-Alive",
|
||||
"Content-Length": "336",
|
||||
"Host": "localhost:8083",
|
||||
"User-Agent": "Mozilla/5.0 (Windows NT; Microsoft Windows 10.0.15063 ; en-US) WindowsPowerShell/6.0.0"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
@ -3,4 +3,5 @@
|
||||
<li><a href="/">/</a> - This page</li>
|
||||
<li><a href="/Cert/">/Cert/</a> - Client Certificate Details</li>
|
||||
<li><a href="/Get/">/Get/</a> - Emulates functionality of https://httpbin.org/get by returning GET headers, Arguments, and Request URL</li>
|
||||
<li><a href="/Multipart/">/Multipart/</a> - Multipart/form-data submission testing</li>
|
||||
</ul>
|
||||
|
13
test/tools/WebListener/Views/Multipart/Index.cshtml
Normal file
13
test/tools/WebListener/Views/Multipart/Index.cshtml
Normal file
@ -0,0 +1,13 @@
|
||||
<form name="MultipartForm" method="post" enctype="multipart/form-data">
|
||||
<div>
|
||||
<label for="TextBox">TextBox text field</label>
|
||||
<input name="TextBox" type="text" />
|
||||
</div>
|
||||
<div>
|
||||
<label for="TextFile">TextFile file field</label>
|
||||
<input name="TextFile" type="file" />
|
||||
</div>
|
||||
<div>
|
||||
<input type="submit" value="Submit" />
|
||||
</div>
|
||||
</form>
|
Loading…
Reference in New Issue
Block a user