Skip to content

Commit 9e8b4d1

Browse files
KlausVcbKlaus VancamelbekeEllerbach
authored
Multipart form support (#278)
Co-authored-by: Klaus Vancamelbeke <[email protected]> Co-authored-by: Laurent Ellerbach <[email protected]>
1 parent 2f831ad commit 9e8b4d1

20 files changed

+1187
-58
lines changed

README.md

Lines changed: 22 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -373,12 +373,24 @@ if (url.ToLower().IndexOf("/api/") == 0)
373373
{
374374

375375
ret += $"Size of content: {e.Context.Request.ContentLength64}\r\n";
376-
byte[] buff = new byte[e.Context.Request.ContentLength64];
377-
e.Context.Request.InputStream.Read(buff, 0, buff.Length);
378-
ret += $"Hex string representation:\r\n";
379-
for (int i = 0; i < buff.Length; i++)
376+
377+
var contentTypes = e.Context.Request.Headers?.GetValues("Content-Type");
378+
var isMultipartForm = contentTypes != null && contentTypes.Length > 0 && contentTypes[0].StartsWith("multipart/form-data;");
379+
380+
if(isMultipartForm)
380381
{
381-
ret += buff[i].ToString("X") + " ";
382+
var form = e.Context.Request.ReadForm();
383+
ret += $"Received a form with {form.Parameters.Length} parameters and {form.Files.Length} files.";
384+
}
385+
else
386+
{
387+
var body = e.Context.Request.ReadBody();
388+
389+
ret += $"Request body hex string representation:\r\n";
390+
for (int i = 0; i < body.Length; i++)
391+
{
392+
ret += body[i].ToString("X") + " ";
393+
}
382394
}
383395

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

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

406+
Notice the extension methods to read the body of the request:
407+
408+
- 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
409+
- ReadForm allows to read a multipart/form-data form and returns the text key/value pairs as well as any files in the request
410+
394411
Example of a result with call:
395412

396413
![result](./doc/POSTcapture.jpg)

nanoFramework.WebServer.FileSystem/nanoFramework.WebServer.FileSystem.nfproj

Lines changed: 26 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -32,49 +32,30 @@
3232
</PropertyGroup>
3333
<Import Project="$(NanoFrameworkProjectSystemPath)NFProjectSystem.props" Condition="Exists('$(NanoFrameworkProjectSystemPath)NFProjectSystem.props')" />
3434
<ItemGroup>
35+
<Compile Include="..\nanoFramework.WebServer\Authentication.cs" Link="Authentication.cs" />
36+
<Compile Include="..\nanoFramework.WebServer\AuthenticationAttribute.cs" Link="AuthenticationAttribute.cs" />
37+
<Compile Include="..\nanoFramework.WebServer\AuthenticationType.cs" Link="AuthenticationType.cs" />
38+
<Compile Include="..\nanoFramework.WebServer\CallbackRoutes.cs" Link="CallbackRoutes.cs" />
39+
<Compile Include="..\nanoFramework.WebServer\CaseSensitiveAttribute.cs" Link="CaseSensitiveAttribute.cs" />
40+
<Compile Include="..\nanoFramework.WebServer\Header.cs" Link="Header.cs" />
41+
<Compile Include="..\nanoFramework.WebServer\HttpListenerRequestExtensions.cs" Link="HttpListenerRequestExtensions.cs" />
42+
<Compile Include="..\nanoFramework.WebServer\HttpMultipartParser\FilePart.cs" Link="HttpMultipartParser\FilePart.cs" />
43+
<Compile Include="..\nanoFramework.WebServer\HttpMultipartParser\HashtableUtility.cs" Link="HttpMultipartParser\HashtableUtility.cs" />
44+
<Compile Include="..\nanoFramework.WebServer\HttpMultipartParser\HeaderUtility.cs" Link="HttpMultipartParser\HeaderUtility.cs" />
45+
<Compile Include="..\nanoFramework.WebServer\HttpMultipartParser\LineBuffer.cs" Link="HttpMultipartParser\LineBuffer.cs" />
46+
<Compile Include="..\nanoFramework.WebServer\HttpMultipartParser\LineReader.cs" Link="HttpMultipartParser\LineReader.cs" />
47+
<Compile Include="..\nanoFramework.WebServer\HttpMultipartParser\MultipartFormDataParser.cs" Link="HttpMultipartParser\MultipartFormDataParser.cs" />
48+
<Compile Include="..\nanoFramework.WebServer\HttpMultipartParser\MultipartFormDataParserException.cs" Link="HttpMultipartParser\MultipartFormDataParserException.cs" />
49+
<Compile Include="..\nanoFramework.WebServer\HttpMultipartParser\ParameterPart.cs" Link="HttpMultipartParser\ParameterPart.cs" />
50+
<Compile Include="..\nanoFramework.WebServer\HttpProtocol.cs" Link="HttpProtocol.cs" />
51+
<Compile Include="..\nanoFramework.WebServer\MethodAttribute.cs" Link="MethodAttribute.cs" />
52+
<Compile Include="..\nanoFramework.WebServer\RouteAttribute.cs" Link="RouteAttribute.cs" />
53+
<Compile Include="..\nanoFramework.WebServer\UrlParameter.cs" Link="UrlParameter.cs" />
54+
<Compile Include="..\nanoFramework.WebServer\WebServer.cs" Link="WebServer.cs" />
55+
<Compile Include="..\nanoFramework.WebServer\WebServerEventArgs.cs" Link="WebServerEventArgs.cs" />
56+
<Compile Include="..\nanoFramework.WebServer\WebServerStatus.cs" Link="WebServerStatus.cs" />
57+
<Compile Include="..\nanoFramework.WebServer\WebServerStatusEventArgs.cs" Link="WebServerStatusEventArgs.cs" />
3558
<Compile Include="Properties\AssemblyInfo.cs" />
36-
<Compile Include="..\nanoFramework.WebServer\Authentication.cs">
37-
<Link>Authentication.cs</Link>
38-
</Compile>
39-
<Compile Include="..\nanoFramework.WebServer\AuthenticationAttirbute.cs">
40-
<Link>AuthenticationAttirbute.cs</Link>
41-
</Compile>
42-
<Compile Include="..\nanoFramework.WebServer\AuthenticationType.cs">
43-
<Link>AuthenticationType.cs</Link>
44-
</Compile>
45-
<Compile Include="..\nanoFramework.WebServer\CallbackRoutes.cs">
46-
<Link>CallbackRoutes.cs</Link>
47-
</Compile>
48-
<Compile Include="..\nanoFramework.WebServer\CaseSensitiveAttribute.cs">
49-
<Link>CaseSensitiveAttribute.cs</Link>
50-
</Compile>
51-
<Compile Include="..\nanoFramework.WebServer\HttpProtocol.cs">
52-
<Link>HttpProtocol.cs</Link>
53-
</Compile>
54-
<Compile Include="..\nanoFramework.WebServer\WebServerEventArgs.cs">
55-
<Link>WebServerEventArgs.cs</Link>
56-
</Compile>
57-
<Compile Include="..\nanoFramework.WebServer\Header.cs">
58-
<Link>Header.cs</Link>
59-
</Compile>
60-
<Compile Include="..\nanoFramework.WebServer\MethodAttribute.cs">
61-
<Link>MethodAttribute.cs</Link>
62-
</Compile>
63-
<Compile Include="..\nanoFramework.WebServer\RouteAttribute.cs">
64-
<Link>RouteAttribute.cs</Link>
65-
</Compile>
66-
<Compile Include="..\nanoFramework.WebServer\UrlParameter.cs">
67-
<Link>UrlParameter.cs</Link>
68-
</Compile>
69-
<Compile Include="..\nanoFramework.WebServer\WebServer.cs">
70-
<Link>WebServer.cs</Link>
71-
</Compile>
72-
<Compile Include="..\nanoFramework.WebServer\WebServerStatus.cs">
73-
<Link>WebServerStatus.cs</Link>
74-
</Compile>
75-
<Compile Include="..\nanoFramework.WebServer\WebServerStatusEventArgs.cs">
76-
<Link>WebServerStatusEventArgs.cs</Link>
77-
</Compile>
7859
<None Include="..\key.snk" />
7960
</ItemGroup>
8061
<ItemGroup>
@@ -118,6 +99,9 @@
11899
<ItemGroup>
119100
<Content Include="packages.lock.json" />
120101
</ItemGroup>
102+
<ItemGroup>
103+
<Folder Include="HttpMultipartParser\" />
104+
</ItemGroup>
121105
<Import Project="$(NanoFrameworkProjectSystemPath)NFProjectSystem.CSharp.targets" Condition="Exists('$(NanoFrameworkProjectSystemPath)NFProjectSystem.CSharp.targets')" />
122106
<ProjectExtensions>
123107
<ProjectCapabilities>

nanoFramework.WebServer/HttpConnectionType.cs

Lines changed: 0 additions & 10 deletions
This file was deleted.
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
using System;
5+
using System.IO;
6+
using System.Net;
7+
using System.Threading;
8+
using nanoFramework.WebServer.HttpMultipartParser;
9+
10+
namespace nanoFramework.WebServer
11+
{
12+
/// <summary>Contains extension methods for HttpListenerRequest</summary>
13+
public static class HttpListenerRequestExtensions
14+
{
15+
/// <summary>
16+
/// Reads a Multipart form from the request
17+
/// </summary>
18+
/// <param name="httpListenerRequest">The request to read the form from</param>
19+
/// <returns>A <see cref="MultipartFormDataParser">MultipartFormDataParser</see> containing a collection of the parameters and files in the form.</returns>
20+
public static MultipartFormDataParser ReadForm(this HttpListenerRequest httpListenerRequest) =>
21+
MultipartFormDataParser.Parse(httpListenerRequest.InputStream);
22+
23+
/// <summary>
24+
/// Reads a body from the HttpListenerRequest inputstream
25+
/// </summary>
26+
/// <param name="httpListenerRequest">The request to read the body from</param>
27+
/// <returns>A byte[] containing the body of the request</returns>
28+
public static byte[] ReadBody(this HttpListenerRequest httpListenerRequest)
29+
{
30+
byte[] body = new byte[httpListenerRequest.ContentLength64];
31+
byte[] buffer = new byte[4096];
32+
Stream stream = httpListenerRequest.InputStream;
33+
34+
int position = 0;
35+
36+
while (true)
37+
{
38+
// The stream is (should be) a NetworkStream which might still be receiving data while
39+
// we're already processing. Give the stream a chance to receive more data or we might
40+
// end up with "zero bytes read" too soon...
41+
Thread.Sleep(1);
42+
43+
long length = stream.Length;
44+
45+
if (length > buffer.Length)
46+
{
47+
length = buffer.Length;
48+
}
49+
50+
int bytesRead = stream.Read(buffer, 0, (int)length);
51+
52+
if (bytesRead == 0)
53+
{
54+
break;
55+
}
56+
57+
Array.Copy(buffer, 0, body, position, bytesRead);
58+
59+
position += bytesRead;
60+
}
61+
62+
return body;
63+
}
64+
}
65+
}
Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
using System.Collections;
5+
using System.IO;
6+
7+
namespace nanoFramework.WebServer.HttpMultipartParser
8+
{
9+
/// <summary>Represents a single file extracted from a multipart/form-data stream.</summary>
10+
public class FilePart
11+
{
12+
/// <summary>Initializes a new instance of the <see cref="FilePart" /> class.</summary>
13+
/// <param name="name">The name of the input field used for the upload.</param>
14+
/// <param name="fileName">The name of the file.</param>
15+
/// <param name="data">The file data.</param>
16+
/// <param name="additionalProperties">Additional properties associated with this file.</param>
17+
/// <param name="contentType">The content type.</param>
18+
/// <param name="contentDisposition">The content disposition.</param>
19+
public FilePart(string name, string fileName, Stream data, Hashtable additionalProperties, string contentType, string contentDisposition)
20+
{
21+
string[] parts = fileName?.Split(GetInvalidFileNameChars());
22+
23+
Name = name;
24+
FileName = parts != null && parts.Length > 0 ? parts[parts.Length - 1] : string.Empty;
25+
Data = data;
26+
ContentType = contentType;
27+
ContentDisposition = contentDisposition;
28+
AdditionalProperties = additionalProperties;
29+
}
30+
31+
/// <summary>Gets the data.</summary>
32+
public Stream Data
33+
{
34+
get;
35+
}
36+
37+
/// <summary>Gets the file name.</summary>
38+
public string FileName
39+
{
40+
get;
41+
}
42+
43+
/// <summary>Gets the name.</summary>
44+
public string Name
45+
{
46+
get;
47+
}
48+
49+
/// <summary>Gets the content-type. Defaults to text/plain if unspecified.</summary>
50+
public string ContentType
51+
{
52+
get;
53+
}
54+
55+
/// <summary>Gets the content-disposition. Defaults to form-data if unspecified.</summary>
56+
public string ContentDisposition
57+
{
58+
get;
59+
}
60+
61+
/// <summary>
62+
/// Gets the additional properties associated with this file.
63+
/// An additional property is any property other than the "well known" ones such as name, filename, content-type, etc.
64+
/// </summary>
65+
public Hashtable AdditionalProperties
66+
{
67+
get;
68+
private set;
69+
}
70+
71+
private static char[] GetInvalidFileNameChars() => new char[]
72+
{
73+
'\"', '<', '>', '|', '\0',
74+
(char)1, (char)2, (char)3, (char)4, (char)5, (char)6, (char)7, (char)8, (char)9, (char)10,
75+
(char)11, (char)12, (char)13, (char)14, (char)15, (char)16, (char)17, (char)18, (char)19, (char)20,
76+
(char)21, (char)22, (char)23, (char)24, (char)25, (char)26, (char)27, (char)28, (char)29, (char)30,
77+
(char)31, ':', '*', '?', '\\', '/'
78+
};
79+
}
80+
}
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
using System.Collections;
5+
6+
namespace nanoFramework.WebServer.HttpMultipartParser
7+
{
8+
internal static class HashtableUtility
9+
{
10+
public static bool TryGetValue(this Hashtable hashtable, string key, out string value)
11+
{
12+
if (hashtable != null && hashtable.Contains(key))
13+
{
14+
var obj = hashtable[key];
15+
value = obj == null ? string.Empty : obj.ToString();
16+
return true;
17+
}
18+
19+
value = null;
20+
return false;
21+
}
22+
}
23+
}
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
using System.Collections;
5+
using System.Text;
6+
7+
namespace nanoFramework.WebServer.HttpMultipartParser
8+
{
9+
/// <summary>
10+
/// Provides parsing headers from a Http Multipart Form
11+
/// </summary>
12+
public static class HeaderUtility
13+
{
14+
/// <summary>
15+
/// Reads headers from a line of text.
16+
/// Headers are delimited by a semi-colon ';'
17+
/// Key-value pairs are separated by colon ':' or equals '='
18+
/// Values can be delimited by quotes '"' or not
19+
/// </summary>
20+
/// <param name="text">The line of text containing one or more headers</param>
21+
/// <param name="headers">
22+
/// The hashtable that will receive the key values.
23+
/// Passed in since a Multipart Part can contain multiple lines of headers
24+
/// </param>
25+
public static void ParseHeaders(string text, Hashtable headers)
26+
{
27+
bool inQuotes = false;
28+
bool inKey = true;
29+
StringBuilder key = new();
30+
StringBuilder value = new();
31+
32+
foreach (char c in text)
33+
{
34+
if (c == '"')
35+
{
36+
inQuotes = !inQuotes;
37+
}
38+
else if (inQuotes)
39+
{
40+
value.Append(c);
41+
}
42+
else if (c == ';')
43+
{
44+
headers[key.ToString().ToLower()] = value.ToString();
45+
key.Clear();
46+
inKey = true;
47+
}
48+
else if (c == '=' || c == ':')
49+
{
50+
value = value.Clear();
51+
inKey = false;
52+
}
53+
else if (c != ' ')
54+
{
55+
if (inKey)
56+
{
57+
key.Append(c);
58+
}
59+
else
60+
{
61+
value.Append(c);
62+
}
63+
}
64+
}
65+
66+
if (key.Length > 0)
67+
{
68+
headers.Add(key.ToString().ToLower(), value.ToString());
69+
}
70+
}
71+
}
72+
}

0 commit comments

Comments
 (0)