Skip to content

Commit

Permalink
Multipart form support (#278)
Browse files Browse the repository at this point in the history
Co-authored-by: Klaus Vancamelbeke <[email protected]>
Co-authored-by: Laurent Ellerbach <[email protected]>
  • Loading branch information
3 people authored Dec 11, 2024
1 parent 2f831ad commit 9e8b4d1
Show file tree
Hide file tree
Showing 20 changed files with 1,187 additions and 58 deletions.
27 changes: 22 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -373,12 +373,24 @@ if (url.ToLower().IndexOf("/api/") == 0)
{

ret += $"Size of content: {e.Context.Request.ContentLength64}\r\n";
byte[] buff = new byte[e.Context.Request.ContentLength64];
e.Context.Request.InputStream.Read(buff, 0, buff.Length);
ret += $"Hex string representation:\r\n";
for (int i = 0; i < buff.Length; i++)

var contentTypes = e.Context.Request.Headers?.GetValues("Content-Type");
var isMultipartForm = contentTypes != null && contentTypes.Length > 0 && contentTypes[0].StartsWith("multipart/form-data;");

if(isMultipartForm)
{
ret += buff[i].ToString("X") + " ";
var form = e.Context.Request.ReadForm();
ret += $"Received a form with {form.Parameters.Length} parameters and {form.Files.Length} files.";
}
else
{
var body = e.Context.Request.ReadBody();

ret += $"Request body hex string representation:\r\n";
for (int i = 0; i < body.Length; i++)
{
ret += body[i].ToString("X") + " ";
}
}

}
Expand All @@ -391,6 +403,11 @@ This API example is basic but as you get the method, you can choose what to do.

As you get the url, you can check for a specific controller called. And you have the parameters and the content payload!

Notice the extension methods to read the body of the request:

- ReadBody will read the data from the InputStream while the data is flowing in which might be in multiple passes depending on the size of the body
- ReadForm allows to read a multipart/form-data form and returns the text key/value pairs as well as any files in the request

Example of a result with call:

![result](./doc/POSTcapture.jpg)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,49 +32,30 @@
</PropertyGroup>
<Import Project="$(NanoFrameworkProjectSystemPath)NFProjectSystem.props" Condition="Exists('$(NanoFrameworkProjectSystemPath)NFProjectSystem.props')" />
<ItemGroup>
<Compile Include="..\nanoFramework.WebServer\Authentication.cs" Link="Authentication.cs" />
<Compile Include="..\nanoFramework.WebServer\AuthenticationAttribute.cs" Link="AuthenticationAttribute.cs" />
<Compile Include="..\nanoFramework.WebServer\AuthenticationType.cs" Link="AuthenticationType.cs" />
<Compile Include="..\nanoFramework.WebServer\CallbackRoutes.cs" Link="CallbackRoutes.cs" />
<Compile Include="..\nanoFramework.WebServer\CaseSensitiveAttribute.cs" Link="CaseSensitiveAttribute.cs" />
<Compile Include="..\nanoFramework.WebServer\Header.cs" Link="Header.cs" />
<Compile Include="..\nanoFramework.WebServer\HttpListenerRequestExtensions.cs" Link="HttpListenerRequestExtensions.cs" />
<Compile Include="..\nanoFramework.WebServer\HttpMultipartParser\FilePart.cs" Link="HttpMultipartParser\FilePart.cs" />
<Compile Include="..\nanoFramework.WebServer\HttpMultipartParser\HashtableUtility.cs" Link="HttpMultipartParser\HashtableUtility.cs" />
<Compile Include="..\nanoFramework.WebServer\HttpMultipartParser\HeaderUtility.cs" Link="HttpMultipartParser\HeaderUtility.cs" />
<Compile Include="..\nanoFramework.WebServer\HttpMultipartParser\LineBuffer.cs" Link="HttpMultipartParser\LineBuffer.cs" />
<Compile Include="..\nanoFramework.WebServer\HttpMultipartParser\LineReader.cs" Link="HttpMultipartParser\LineReader.cs" />
<Compile Include="..\nanoFramework.WebServer\HttpMultipartParser\MultipartFormDataParser.cs" Link="HttpMultipartParser\MultipartFormDataParser.cs" />
<Compile Include="..\nanoFramework.WebServer\HttpMultipartParser\MultipartFormDataParserException.cs" Link="HttpMultipartParser\MultipartFormDataParserException.cs" />
<Compile Include="..\nanoFramework.WebServer\HttpMultipartParser\ParameterPart.cs" Link="HttpMultipartParser\ParameterPart.cs" />
<Compile Include="..\nanoFramework.WebServer\HttpProtocol.cs" Link="HttpProtocol.cs" />
<Compile Include="..\nanoFramework.WebServer\MethodAttribute.cs" Link="MethodAttribute.cs" />
<Compile Include="..\nanoFramework.WebServer\RouteAttribute.cs" Link="RouteAttribute.cs" />
<Compile Include="..\nanoFramework.WebServer\UrlParameter.cs" Link="UrlParameter.cs" />
<Compile Include="..\nanoFramework.WebServer\WebServer.cs" Link="WebServer.cs" />
<Compile Include="..\nanoFramework.WebServer\WebServerEventArgs.cs" Link="WebServerEventArgs.cs" />
<Compile Include="..\nanoFramework.WebServer\WebServerStatus.cs" Link="WebServerStatus.cs" />
<Compile Include="..\nanoFramework.WebServer\WebServerStatusEventArgs.cs" Link="WebServerStatusEventArgs.cs" />
<Compile Include="Properties\AssemblyInfo.cs" />
<Compile Include="..\nanoFramework.WebServer\Authentication.cs">
<Link>Authentication.cs</Link>
</Compile>
<Compile Include="..\nanoFramework.WebServer\AuthenticationAttirbute.cs">
<Link>AuthenticationAttirbute.cs</Link>
</Compile>
<Compile Include="..\nanoFramework.WebServer\AuthenticationType.cs">
<Link>AuthenticationType.cs</Link>
</Compile>
<Compile Include="..\nanoFramework.WebServer\CallbackRoutes.cs">
<Link>CallbackRoutes.cs</Link>
</Compile>
<Compile Include="..\nanoFramework.WebServer\CaseSensitiveAttribute.cs">
<Link>CaseSensitiveAttribute.cs</Link>
</Compile>
<Compile Include="..\nanoFramework.WebServer\HttpProtocol.cs">
<Link>HttpProtocol.cs</Link>
</Compile>
<Compile Include="..\nanoFramework.WebServer\WebServerEventArgs.cs">
<Link>WebServerEventArgs.cs</Link>
</Compile>
<Compile Include="..\nanoFramework.WebServer\Header.cs">
<Link>Header.cs</Link>
</Compile>
<Compile Include="..\nanoFramework.WebServer\MethodAttribute.cs">
<Link>MethodAttribute.cs</Link>
</Compile>
<Compile Include="..\nanoFramework.WebServer\RouteAttribute.cs">
<Link>RouteAttribute.cs</Link>
</Compile>
<Compile Include="..\nanoFramework.WebServer\UrlParameter.cs">
<Link>UrlParameter.cs</Link>
</Compile>
<Compile Include="..\nanoFramework.WebServer\WebServer.cs">
<Link>WebServer.cs</Link>
</Compile>
<Compile Include="..\nanoFramework.WebServer\WebServerStatus.cs">
<Link>WebServerStatus.cs</Link>
</Compile>
<Compile Include="..\nanoFramework.WebServer\WebServerStatusEventArgs.cs">
<Link>WebServerStatusEventArgs.cs</Link>
</Compile>
<None Include="..\key.snk" />
</ItemGroup>
<ItemGroup>
Expand Down Expand Up @@ -118,6 +99,9 @@
<ItemGroup>
<Content Include="packages.lock.json" />
</ItemGroup>
<ItemGroup>
<Folder Include="HttpMultipartParser\" />
</ItemGroup>
<Import Project="$(NanoFrameworkProjectSystemPath)NFProjectSystem.CSharp.targets" Condition="Exists('$(NanoFrameworkProjectSystemPath)NFProjectSystem.CSharp.targets')" />
<ProjectExtensions>
<ProjectCapabilities>
Expand Down
10 changes: 0 additions & 10 deletions nanoFramework.WebServer/HttpConnectionType.cs

This file was deleted.

65 changes: 65 additions & 0 deletions nanoFramework.WebServer/HttpListenerRequestExtensions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System;
using System.IO;
using System.Net;
using System.Threading;
using nanoFramework.WebServer.HttpMultipartParser;

namespace nanoFramework.WebServer
{
/// <summary>Contains extension methods for HttpListenerRequest</summary>
public static class HttpListenerRequestExtensions
{
/// <summary>
/// Reads a Multipart form from the request
/// </summary>
/// <param name="httpListenerRequest">The request to read the form from</param>
/// <returns>A <see cref="MultipartFormDataParser">MultipartFormDataParser</see> containing a collection of the parameters and files in the form.</returns>
public static MultipartFormDataParser ReadForm(this HttpListenerRequest httpListenerRequest) =>
MultipartFormDataParser.Parse(httpListenerRequest.InputStream);

/// <summary>
/// Reads a body from the HttpListenerRequest inputstream
/// </summary>
/// <param name="httpListenerRequest">The request to read the body from</param>
/// <returns>A byte[] containing the body of the request</returns>
public static byte[] ReadBody(this HttpListenerRequest httpListenerRequest)
{
byte[] body = new byte[httpListenerRequest.ContentLength64];
byte[] buffer = new byte[4096];
Stream stream = httpListenerRequest.InputStream;

int position = 0;

while (true)
{
// The stream is (should be) a NetworkStream which might still be receiving data while
// we're already processing. Give the stream a chance to receive more data or we might
// end up with "zero bytes read" too soon...
Thread.Sleep(1);

long length = stream.Length;

if (length > buffer.Length)
{
length = buffer.Length;
}

int bytesRead = stream.Read(buffer, 0, (int)length);

if (bytesRead == 0)
{
break;
}

Array.Copy(buffer, 0, body, position, bytesRead);

position += bytesRead;
}

return body;
}
}
}
80 changes: 80 additions & 0 deletions nanoFramework.WebServer/HttpMultipartParser/FilePart.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System.Collections;
using System.IO;

namespace nanoFramework.WebServer.HttpMultipartParser
{
/// <summary>Represents a single file extracted from a multipart/form-data stream.</summary>
public class FilePart
{
/// <summary>Initializes a new instance of the <see cref="FilePart" /> class.</summary>
/// <param name="name">The name of the input field used for the upload.</param>
/// <param name="fileName">The name of the file.</param>
/// <param name="data">The file data.</param>
/// <param name="additionalProperties">Additional properties associated with this file.</param>
/// <param name="contentType">The content type.</param>
/// <param name="contentDisposition">The content disposition.</param>
public FilePart(string name, string fileName, Stream data, Hashtable additionalProperties, string contentType, string contentDisposition)
{
string[] parts = fileName?.Split(GetInvalidFileNameChars());

Name = name;
FileName = parts != null && parts.Length > 0 ? parts[parts.Length - 1] : string.Empty;
Data = data;
ContentType = contentType;
ContentDisposition = contentDisposition;
AdditionalProperties = additionalProperties;
}

/// <summary>Gets the data.</summary>
public Stream Data
{
get;
}

/// <summary>Gets the file name.</summary>
public string FileName
{
get;
}

/// <summary>Gets the name.</summary>
public string Name
{
get;
}

/// <summary>Gets the content-type. Defaults to text/plain if unspecified.</summary>
public string ContentType
{
get;
}

/// <summary>Gets the content-disposition. Defaults to form-data if unspecified.</summary>
public string ContentDisposition
{
get;
}

/// <summary>
/// Gets the additional properties associated with this file.
/// An additional property is any property other than the "well known" ones such as name, filename, content-type, etc.
/// </summary>
public Hashtable AdditionalProperties
{
get;
private set;
}

private static char[] GetInvalidFileNameChars() => new char[]
{
'\"', '<', '>', '|', '\0',
(char)1, (char)2, (char)3, (char)4, (char)5, (char)6, (char)7, (char)8, (char)9, (char)10,
(char)11, (char)12, (char)13, (char)14, (char)15, (char)16, (char)17, (char)18, (char)19, (char)20,
(char)21, (char)22, (char)23, (char)24, (char)25, (char)26, (char)27, (char)28, (char)29, (char)30,
(char)31, ':', '*', '?', '\\', '/'
};
}
}
23 changes: 23 additions & 0 deletions nanoFramework.WebServer/HttpMultipartParser/HashtableUtility.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System.Collections;

namespace nanoFramework.WebServer.HttpMultipartParser
{
internal static class HashtableUtility
{
public static bool TryGetValue(this Hashtable hashtable, string key, out string value)
{
if (hashtable != null && hashtable.Contains(key))
{
var obj = hashtable[key];
value = obj == null ? string.Empty : obj.ToString();
return true;
}

value = null;
return false;
}
}
}
72 changes: 72 additions & 0 deletions nanoFramework.WebServer/HttpMultipartParser/HeaderUtility.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System.Collections;
using System.Text;

namespace nanoFramework.WebServer.HttpMultipartParser
{
/// <summary>
/// Provides parsing headers from a Http Multipart Form
/// </summary>
public static class HeaderUtility
{
/// <summary>
/// Reads headers from a line of text.
/// Headers are delimited by a semi-colon ';'
/// Key-value pairs are separated by colon ':' or equals '='
/// Values can be delimited by quotes '"' or not
/// </summary>
/// <param name="text">The line of text containing one or more headers</param>
/// <param name="headers">
/// The hashtable that will receive the key values.
/// Passed in since a Multipart Part can contain multiple lines of headers
/// </param>
public static void ParseHeaders(string text, Hashtable headers)
{
bool inQuotes = false;
bool inKey = true;
StringBuilder key = new();
StringBuilder value = new();

foreach (char c in text)
{
if (c == '"')
{
inQuotes = !inQuotes;
}
else if (inQuotes)
{
value.Append(c);
}
else if (c == ';')
{
headers[key.ToString().ToLower()] = value.ToString();
key.Clear();
inKey = true;
}
else if (c == '=' || c == ':')
{
value = value.Clear();
inKey = false;
}
else if (c != ' ')
{
if (inKey)
{
key.Append(c);
}
else
{
value.Append(c);
}
}
}

if (key.Length > 0)
{
headers.Add(key.ToString().ToLower(), value.ToString());
}
}
}
}
Loading

0 comments on commit 9e8b4d1

Please sign in to comment.