Files
MTSC/MTSC/ServerSide/Handlers/HttpHandler.cs
T
Alexandru Macocian 0582a82d6c - Fixed messages being handled non-sequentially during parallel processing of the clients
- Made unit tests use SSL encryption
- Added netcore3.1 to build list
2020-04-06 22:45:34 +02:00

249 lines
10 KiB
C#

using MTSC.Common.Http;
using MTSC.Common.Http.ServerModules;
using MTSC.Exceptions;
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
namespace MTSC.ServerSide.Handlers
{
/// <summary>
/// Handler for handling server http requests.
/// </summary>
public sealed class HttpHandler : IHandler
{
private static readonly string urlEncodedHeader = "application/x-www-form-urlencoded";
private static readonly string multipartHeader = "multipart/form-data";
#region Fields
List<ClientData> removeFragmentsList = new List<ClientData>();
List<IHttpModule> httpModules = new List<IHttpModule>();
ConcurrentQueue<Tuple<ClientData, HttpResponse>> messageOutQueue = new ConcurrentQueue<Tuple<ClientData, HttpResponse>>();
ConcurrentDictionary<ClientData, (byte[], DateTime)> fragmentedMessages = new ConcurrentDictionary<ClientData, (byte[], DateTime)>();
#endregion
#region Public Properties
public TimeSpan FragmentsExpirationTime { get; set; } = TimeSpan.FromSeconds(15);
public double MaximumRequestSize { get; set; } = 15000;
#endregion
#region Constructors
public HttpHandler()
{
}
#endregion
#region Public Methods
public HttpHandler WithMaximumSize(double size)
{
this.MaximumRequestSize = size;
return this;
}
/// <summary>
/// The amount of time fragments are kept in the buffer before being discarded.
/// </summary>
/// <param name="duration">Time until fragments expire.</param>
/// <returns>This handler object.</returns>
public HttpHandler WithFragmentsExpirationTime(TimeSpan duration)
{
this.FragmentsExpirationTime = duration;
return this;
}
/// <summary>
/// Add a http module onto the server.
/// </summary>
/// <param name="module">Module to be added.</param>
/// <returns>This handler object.</returns>
public HttpHandler AddHttpModule(IHttpModule module)
{
this.httpModules.Add(module);
return this;
}
/// <summary>
/// Send a response back to the client.
/// </summary>
/// <param name="response">Message containing the response.</param>
public void QueueResponse(ClientData client, HttpResponse response)
{
messageOutQueue.Enqueue(new Tuple<ClientData, HttpResponse>(client, response));
}
#endregion
#region Interface Implementation
/// <summary>
/// Handler interface implementation.
/// </summary>
/// <param name="client"></param>
void IHandler.ClientRemoved(Server server, ClientData client)
{
}
/// <summary>
/// Handler interface implementation.
/// </summary>
/// <param name="client"></param>
/// <returns></returns>
bool IHandler.HandleClient(Server server, ClientData client)
{
return false;
}
/// <summary>
/// Handler interface implementation.
/// </summary>
/// <param name="client"></param>
/// <param name="message"></param>
/// <returns></returns>
bool IHandler.HandleReceivedMessage(Server server, ClientData client, Message message)
{
// Parse the request. If the message is incomplete, return 100 and queue the message to be parsed later.
HttpRequest request = null;
byte[] messageBytes = null;
try
{
var trimmedMessageBytes = message.MessageBytes.TrimTrailingNullBytes();
if (fragmentedMessages.ContainsKey(client))
{
byte[] previousBytes = fragmentedMessages[client].Item1;
if(previousBytes.Length + trimmedMessageBytes.Length > MaximumRequestSize)
{
// Discard the message if it is too big
server.LogDebug($"Discarded message. Message size [{previousBytes.Length + trimmedMessageBytes.Length}] > [{MaximumRequestSize}]");
fragmentedMessages.TryRemove(client, out _);
return false;
}
byte[] repackagingBuffer = new byte[previousBytes.Length + trimmedMessageBytes.Length];
Array.Copy(previousBytes, 0, repackagingBuffer, 0, previousBytes.Length);
Array.Copy(trimmedMessageBytes, 0, repackagingBuffer, previousBytes.Length, trimmedMessageBytes.Length);
messageBytes = repackagingBuffer;
}
else
{
if(trimmedMessageBytes.Length > MaximumRequestSize)
{
// Discard the message if it is too big
server.LogDebug($"Discarded message. Message size [{message.MessageBytes.Length}] > [{MaximumRequestSize}]");
return false;
}
messageBytes = trimmedMessageBytes;
}
messageBytes = messageBytes.TrimTrailingNullBytes();
var partialRequest = PartialHttpRequest.FromBytes(messageBytes);
if (partialRequest.Complete)
request = partialRequest.ToRequest();
else
{
HandleIncompleteRequest(client, server, messageBytes, partialRequest);
return true;
}
}
catch (Exception ex) when (
ex is IncompleteHeaderKeyException ||
ex is IncompleteHeaderValueException ||
ex is IncompleteHttpVersionException ||
ex is IncompleteMethodException ||
ex is IncompleteRequestBodyException ||
ex is IncompleteRequestQueryException ||
ex is IncompleteRequestURIException ||
ex is IncompleteRequestException ||
ex is InvalidPostFormException)
{
server.LogDebug(ex.Message);
server.LogDebug(ex.StackTrace);
HandleIncompleteRequest(client, server, messageBytes);
return true;
}
catch (Exception e)
{
throw e;
}
// The message has been parsed. If there was a cache for the current message, remove it.
if (fragmentedMessages.ContainsKey(client))
{
fragmentedMessages.TryRemove(client, out _);
}
HttpResponse response = new HttpResponse();
if(request.Headers.ContainsHeader(HttpMessage.GeneralHeaders.Connection) &&
request.Headers[HttpMessage.GeneralHeaders.Connection].ToLower() == "close")
{
response.Headers[HttpMessage.GeneralHeaders.Connection] = "close";
client.ToBeRemoved = true;
}
else
{
response.Headers[HttpMessage.GeneralHeaders.Connection] = "keep-alive";
}
foreach(IHttpModule module in httpModules)
{
if(module.HandleRequest(server, this, client, request, ref response))
{
break;
}
}
QueueResponse(client, response);
return true;
}
/// <summary>
/// Handler interface implementation.
/// </summary>
/// <param name="client"></param>
/// <param name="message"></param>
/// <returns></returns>
bool IHandler.HandleSendMessage(Server server, ClientData client, ref Message message)
{
return false;
}
/// <summary>
/// Handler interface implementation.
/// </summary>
/// <param name="client"></param>
/// <param name="message"></param>
/// <returns></returns>
bool IHandler.PreHandleReceivedMessage(Server server, ClientData client, ref Message message)
{
return false;
}
/// <summary>
/// Handler interface implementation.
/// </summary>
void IHandler.Tick(Server server)
{
while (messageOutQueue.Count > 0)
{
if (messageOutQueue.TryDequeue(out Tuple<ClientData, HttpResponse> tuple))
{
server.QueueMessage(tuple.Item1, tuple.Item2.GetPackedResponse(true));
}
}
foreach (IHttpModule module in httpModules)
{
module.Tick(server, this);
}
removeFragmentsList.Clear();
foreach(var kvp in fragmentedMessages)
{
if((DateTime.Now - kvp.Value.Item2) > FragmentsExpirationTime)
{
removeFragmentsList.Add(kvp.Key);
}
}
foreach(var key in removeFragmentsList)
{
fragmentedMessages.TryRemove(key, out _);
}
}
#endregion
private void HandleIncompleteRequest(ClientData client, Server server, byte[] messageBytes, PartialHttpRequest partialRequest = null)
{
fragmentedMessages[client] = (messageBytes, DateTime.Now);
server.LogDebug("Incomplete request received!");
if (partialRequest != null && partialRequest.Headers.ContainsHeader(HttpMessage.RequestHeaders.Expect) &&
partialRequest.Headers[HttpMessage.RequestHeaders.Expect].Equals("100-continue", StringComparison.OrdinalIgnoreCase))
{
server.LogDebug("Returning 100-Continue");
var contResponse = new HttpResponse { StatusCode = HttpMessage.StatusCodes.Continue };
contResponse.Headers[HttpMessage.GeneralHeaders.Connection] = "keep-alive";
QueueResponse(client, contResponse);
}
}
}
}