Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
85 changes: 61 additions & 24 deletions nanoFramework.WebServer/HttpListenerRequestExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -21,45 +21,82 @@
MultipartFormDataParser.Parse(httpListenerRequest.InputStream);

/// <summary>
/// Reads a body from the HttpListenerRequest inputstream
/// 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>
/// <returns>
/// A byte[] containing the body of the request, or <see langword="null"/> if the body could not be read.
/// </returns>
public static byte[] ReadBody(this HttpListenerRequest httpListenerRequest)
{
byte[] body = new byte[httpListenerRequest.ContentLength64];
byte[] buffer = new byte[4096];
Stream stream = httpListenerRequest.InputStream;
long contentLength = httpListenerRequest.ContentLength64;

int position = 0;
// check missing or invalid content-length
if (contentLength <= 0)
{
return new byte[0];
}

// Sanity check for huge content-length
// A managed array cannot exceed int.MaxValue elements
// Treat an oversized Content-Length the same as an allocation failure.
if (contentLength > int.MaxValue)
{
return null;

Check warning on line 45 in nanoFramework.WebServer/HttpListenerRequestExtensions.cs

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Return an empty collection instead of null.

See more on https://sonarcloud.io/project/issues?id=nanoframework_lib-nanoframework.WebServer&issues=AZ4rx6_lIf_23_RlbXD3&open=AZ4rx6_lIf_23_RlbXD3&pullRequest=356
}

while (true)
try
{
// 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);
int bodySize = (int)contentLength;
byte[] body = new byte[bodySize];
byte[] buffer = new byte[4096];
Stream stream = httpListenerRequest.InputStream;

long length = stream.Length;
int position = 0;

if (length > buffer.Length)
while (position < bodySize)
{
length = buffer.Length;
}
// 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);

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

if (bytesRead == 0)
{
break;
}
if (length <= 0)
{
break;
}

Array.Copy(buffer, 0, body, position, bytesRead);
if (length > buffer.Length)
{
length = buffer.Length;
}

position += bytesRead;
}
long remaining = bodySize - position;
if (length > remaining)
{
length = remaining;
}

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

if (bytesRead == 0)
{
break;
}

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

position += bytesRead;
}

return body;
}
catch
{
return null;
}
}
}
}
216 changes: 123 additions & 93 deletions nanoFramework.WebServer/WebServer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
using System.Net;
using System.Net.NetworkInformation;
using System.Net.Security;
using System.Net.Sockets;
using System.Security.Cryptography.X509Certificates;
using System.Text;
using System.Threading;
Expand Down Expand Up @@ -664,151 +665,180 @@
_listener.Start();
while (!_cancel)
{
HttpListenerContext context = _listener.GetContext();
HttpListenerContext context;

try
{
context = _listener.GetContext();
}
catch (SocketException)
{
// possibly a transient connection error
// (e.g. OS captive-portal probe reset before request line is received)
// this allows continue and accepting the next connection
continue;
}

if (context == null)
{
return;
}

new Thread(() =>
{
//This is for handling with transitory or bad requests
if (context.Request.RawUrl == null)
try
{
return;
}
//This is for handling with transitory or bad requests
if (context.Request.RawUrl == null)
{
return;
}

CallbackRoutes selectedRoute = null;
bool selectedRouteHasAuth = false;
string multipleCallback = null;
bool hasAuthRoutes = false;
string basicAuthNoCred = null;
bool authFailed = false;
CallbackRoutes selectedRoute = null;
bool selectedRouteHasAuth = false;
string multipleCallback = null;
bool hasAuthRoutes = false;
string basicAuthNoCred = null;
bool authFailed = false;

// Variables used only within the "for". They are here for performance reasons
bool mustAuthenticate;
bool isAuthOk;
// Variables used only within the "for". They are here for performance reasons
bool mustAuthenticate;
bool isAuthOk;

foreach (CallbackRoutes route in _callbackRoutes)
{
if (!IsRouteMatch(route, context.Request.HttpMethod, context.Request.RawUrl))
foreach (CallbackRoutes route in _callbackRoutes)
{
continue;
}
if (!IsRouteMatch(route, context.Request.HttpMethod, context.Request.RawUrl))
{
continue;
}

// Check auth first
mustAuthenticate = route.Authentication != null && route.Authentication.AuthenticationType != AuthenticationType.None;
if (mustAuthenticate)
{
hasAuthRoutes = true;
if (route.Authentication.AuthenticationType == AuthenticationType.Basic)
// Check auth first
mustAuthenticate = route.Authentication != null && route.Authentication.AuthenticationType != AuthenticationType.None;
if (mustAuthenticate)
{
var credReq = context.Request.Credentials;
if (credReq is null)
hasAuthRoutes = true;
if (route.Authentication.AuthenticationType == AuthenticationType.Basic)
{
if (basicAuthNoCred is null)
var credReq = context.Request.Credentials;
if (credReq is null)
{
basicAuthNoCred = route.Route;
if (basicAuthNoCred is null)
{
basicAuthNoCred = route.Route;
}

continue;
}

continue;
var credSite = route.Authentication.Credentials ?? Credential;

isAuthOk = credSite != null
&& (credSite.UserName == credReq.UserName)
&& (credSite.Password == credReq.Password);
}
else if (route.Authentication.AuthenticationType == AuthenticationType.ApiKey)
{
var apikeyReq = GetApiKeyFromHeaders(context.Request.Headers);
if (apikeyReq is null)
{
continue;
}

var credSite = route.Authentication.Credentials ?? Credential;
var apikeySite = route.Authentication.ApiKey ?? ApiKey;

isAuthOk = credSite != null
&& (credSite.UserName == credReq.UserName)
&& (credSite.Password == credReq.Password);
}
else if (route.Authentication.AuthenticationType == AuthenticationType.ApiKey)
{
var apikeyReq = GetApiKeyFromHeaders(context.Request.Headers);
if (apikeyReq is null)
isAuthOk = apikeyReq == apikeySite;
}
else
{
continue;
isAuthOk = false;
}

var apikeySite = route.Authentication.ApiKey ?? ApiKey;
if (isAuthOk)
{
// This route can be used and has precedence over non-authenticated routes
if (!selectedRouteHasAuth)
{
selectedRoute = null;
multipleCallback = null;
}

selectedRouteHasAuth = true;
}
else
{
authFailed = true;
continue;
}
}
else if (selectedRouteHasAuth || authFailed)
{
// The selected route has authentication and/or a route exists with failed authentication.
// Those have precedence over non-authenticated routes
continue;
}

isAuthOk = apikeyReq == apikeySite;
if (selectedRoute is null)
{
selectedRoute = route;
}
else
{
isAuthOk = false;
multipleCallback ??= $"Multiple matching callbacks: {selectedRoute.Callback.DeclaringType.FullName}.{selectedRoute.Callback.Name}";
multipleCallback += $", {route.Callback.DeclaringType.FullName}.{route.Callback.Name}";
}
}


if (isAuthOk)
if (selectedRoute is null)
{
if (hasAuthRoutes)
{
// This route can be used and has precedence over non-authenticated routes
if (!selectedRouteHasAuth)
if (!authFailed && basicAuthNoCred is not null)
{
selectedRoute = null;
multipleCallback = null;
context.Response.Headers.Add("WWW-Authenticate",
$"Basic realm=\"Access to {basicAuthNoCred}\"");
}

selectedRouteHasAuth = true;
context.Response.StatusCode = (int)HttpStatusCode.Unauthorized;
context.Response.ContentLength64 = 0;
}
else if (CommandReceived != null)
{
// Starting a new thread to be able to handle a new request in parallel
CommandReceived.Invoke(this, new WebServerEventArgs(context));
}
else
{
authFailed = true;
continue;
context.Response.StatusCode = (int)HttpStatusCode.NotFound;
context.Response.ContentLength64 = 0;
}
}
else if (selectedRouteHasAuth || authFailed)
{
// The selected route has authentication and/or a route exists with failed authentication.
// Those have precedence over non-authenticated routes
continue;
}

if (selectedRoute is null)
else if (multipleCallback is not null)
{
selectedRoute = route;
multipleCallback += ".";
context.Response.StatusCode = (int)HttpStatusCode.InternalServerError;
OutputAsStream(context.Response, multipleCallback);
}
else
{
multipleCallback ??= $"Multiple matching callbacks: {selectedRoute.Callback.DeclaringType.FullName}.{selectedRoute.Callback.Name}";
multipleCallback += $", {route.Callback.DeclaringType.FullName}.{route.Callback.Name}";
InvokeRoute(selectedRoute, context);
}
}


if (selectedRoute is null)
HandleContextResponse(context);
}
catch (Exception ex)

Check warning on line 829 in nanoFramework.WebServer/WebServer.cs

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

The variable 'ex' is declared but never used

See more on https://sonarcloud.io/project/issues?id=nanoframework_lib-nanoframework.WebServer&issues=AZ4rx67HIf_23_RlbXD2&open=AZ4rx67HIf_23_RlbXD2&pullRequest=356

Check warning on line 829 in nanoFramework.WebServer/WebServer.cs

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Remove the unused local variable 'ex'.

See more on https://sonarcloud.io/project/issues?id=nanoframework_lib-nanoframework.WebServer&issues=AZ4rx67HIf_23_RlbXD1&open=AZ4rx67HIf_23_RlbXD1&pullRequest=356
{
if (hasAuthRoutes)
{
if (!authFailed && basicAuthNoCred is not null)
{
context.Response.Headers.Add("WWW-Authenticate",
$"Basic realm=\"Access to {basicAuthNoCred}\"");
}

context.Response.StatusCode = (int)HttpStatusCode.Unauthorized;
context.Response.ContentLength64 = 0;
}
else if (CommandReceived != null)
try
{
// Starting a new thread to be able to handle a new request in parallel
CommandReceived.Invoke(this, new WebServerEventArgs(context));
context.Response.StatusCode = (int)HttpStatusCode.InternalServerError;
context.Response.ContentLength64 = 0;
}
else
catch
{
context.Response.StatusCode = (int)HttpStatusCode.NotFound;
context.Response.ContentLength64 = 0;
// Response may already be partially sent or the connection closed; nothing to do.
}
}
else if (multipleCallback is not null)
{
multipleCallback += ".";
context.Response.StatusCode = (int)HttpStatusCode.InternalServerError;
OutputAsStream(context.Response, multipleCallback);
}
else
{
InvokeRoute(selectedRoute, context);
}

HandleContextResponse(context);
}).Start();

}
Expand Down