From 94cac2eea25650e26532957271a80b4c7496bad9 Mon Sep 17 00:00:00 2001 From: Davyd McColl Date: Tue, 7 Jan 2025 10:17:00 +0200 Subject: [PATCH] :recycle: instead of throwing on perceived invalid http request lines, log and continue --- .../HttpProcessor.cs | 939 ++++++------ .../HttpServer.cs | 1298 ++++++++--------- .../PeanutButter.SimpleTcpServer/TcpServer.cs | 1 - 3 files changed, 1125 insertions(+), 1113 deletions(-) diff --git a/source/SimpleServers/PeanutButter.SimpleHTTPServer/HttpProcessor.cs b/source/SimpleServers/PeanutButter.SimpleHTTPServer/HttpProcessor.cs index d373f86955..6a5d828d5d 100644 --- a/source/SimpleServers/PeanutButter.SimpleHTTPServer/HttpProcessor.cs +++ b/source/SimpleServers/PeanutButter.SimpleHTTPServer/HttpProcessor.cs @@ -3,7 +3,7 @@ * Original license is CPOL. My preferred licensing is BSD, which differs only from CPOL in that CPOL explicitly grants you freedom * from prosecution for patent infringement (not that this code is patented or that I even believe in the concept). So, CPOL it is. * You can find the CPOL here: - * http://www.codeproject.com/info/cpol10.aspx + * http://www.codeproject.com/info/cpol10.aspx */ using System; @@ -21,573 +21,588 @@ // ReSharper disable InconsistentNaming -namespace PeanutButter.SimpleHTTPServer +namespace PeanutButter.SimpleHTTPServer; + +/// +/// Processor for HTTP requests on top of the generic TCP processor +/// +public class HttpProcessor : TcpServerProcessor, IProcessor { /// - /// Processor for HTTP requests on top of the generic TCP processor + /// Action to use when attempting to log arbitrary data /// - public class HttpProcessor : TcpServerProcessor, IProcessor - { - /// - /// Action to use when attempting to log arbitrary data - /// - public Action LogAction => Server.LogAction; + public Action LogAction => Server.LogAction; - /// - /// Action to use when attempting to log requests - /// - public Action RequestLogAction => Server.RequestLogAction; + /// + /// Action to use when attempting to log requests + /// + public Action RequestLogAction => Server.RequestLogAction; - private const int BUF_SIZE = 4096; + private const int BUF_SIZE = 4096; - /// - /// Provides access to the server associated with this processor - /// - public HttpServerBase Server { get; protected set; } + /// + /// Provides access to the server associated with this processor + /// + public HttpServerBase Server { get; protected set; } - private StreamWriter _outputStream; + private StreamWriter _outputStream; - /// - /// Method of the current request being processed - /// - public string Method { get; private set; } + /// + /// Method of the current request being processed + /// + public string Method { get; private set; } - /// - /// Full url for the request being processed - /// - public string FullUrl { get; private set; } + /// + /// Full url for the request being processed + /// + public string FullUrl { get; private set; } - /// - /// The full path, including query, for the - /// request being processed - /// - public string FullPath { get; private set; } + /// + /// The full path, including query, for the + /// request being processed + /// + public string FullPath { get; private set; } - /// - /// Just the path for the request being processed - /// - public string Path { get; private set; } + /// + /// Just the path for the request being processed + /// + public string Path { get; private set; } - /// - /// Protocol for the request being processed - /// - public string Protocol { get; private set; } + /// + /// Protocol for the request being processed + /// + public string Protocol { get; private set; } - /// - /// Url parameters for the request being processed - /// - public Dictionary UrlParameters { get; set; } + /// + /// Url parameters for the request being processed + /// + public Dictionary UrlParameters { get; set; } - /// - /// Headers on the request being processed - /// - public Dictionary HttpHeaders { get; private set; } + /// + /// Headers on the request being processed + /// + public Dictionary HttpHeaders { get; private set; } - /// - /// Maximum size, in bytes, to accept for a POST - /// - public long MaxPostSize { get; set; } = MAX_POST_SIZE; + /// + /// Maximum size, in bytes, to accept for a POST + /// + public long MaxPostSize { get; set; } = MAX_POST_SIZE; - /// - public HttpProcessor( - TcpClient tcpClient, - HttpServerBase server - ) : base(tcpClient) - { - Server = server; - HttpHeaders = new Dictionary(); - } + /// + public HttpProcessor( + TcpClient tcpClient, + HttpServerBase server + ) : base(tcpClient) + { + Server = server; + HttpHeaders = new Dictionary(); + } - /// - public void ProcessRequest() + /// + public void ProcessRequest() + { + using (var io = new TcpIoWrapper(TcpClient)) { - using (var io = new TcpIoWrapper(TcpClient)) + try { - try - { - _outputStream = io.StreamWriter; - ParseRequest(); - ReadHeaders(); - HandleRequest(io); - } - catch (FileNotFoundException) - { - WriteFailure(HttpStatusCode.NotFound, Statuses.NOTFOUND); - } - catch (Exception ex) - { - WriteFailure(HttpStatusCode.InternalServerError, $"{Statuses.INTERNALERROR}: {ex.Message}"); - LogAction?.Invoke("Unable to process request: " + ex.Message); - } - finally - { - _outputStream = null; - } + _outputStream = io.StreamWriter; + ParseRequest(); + ReadHeaders(); + HandleRequest(io); } - } - - /// - /// Handles the request, given an IO wrapper - /// - /// - protected void HandleRequest(TcpIoWrapper io) - { - if (Method.Equals(Methods.GET)) + catch (FileNotFoundException) { - this.HandleRequestWithoutBody(Method); - return; + WriteFailure(HttpStatusCode.NotFound, Statuses.NOTFOUND); } - - if (Method.Equals(Methods.PUT) - || Method.Equals(Methods.DELETE) - || Method.Equals(Methods.PATCH) - || Method.Equals(Methods.POST)) + catch (Exception ex) + { + WriteFailure( + HttpStatusCode.InternalServerError, + $"{Statuses.INTERNALERROR}: {ex.Message}" + ); + Log($"Unable to process request: {ex.Message}"); + } + finally { - this.HandleRequestWithBody(io.RawStream, Method); + _outputStream = null; } } + } - /// - /// Parses the request from the TcpClient - /// - public void ParseRequest() + /// + /// Handles the request, given an IO wrapper + /// + /// + protected void HandleRequest(TcpIoWrapper io) + { + if (Method.Equals(Methods.GET)) { - var request = TcpClient.ReadLine(); - var tokens = request.Split(' '); - if (tokens.Length != 3) - { - throw new Exception($"invalid http request line:\n{request}"); - } + this.HandleRequestWithoutBody(Method); + return; + } - Method = tokens[0].ToUpper(); - FullPath = tokens[1]; - FullUrl = Server.GetFullUrlFor(FullPath); - var parts = FullPath.Split('?'); - if (parts.Length == 1) - UrlParameters = new Dictionary(); - else - { - var all = string.Join("?", parts.Skip(1)); - UrlParameters = EncodedStringToDictionary(all); - } + if (Method.Equals(Methods.PUT) + || Method.Equals(Methods.DELETE) + || Method.Equals(Methods.PATCH) + || Method.Equals(Methods.POST)) + { + this.HandleRequestWithBody(io.RawStream, Method); + } + } - Path = parts.First(); - Protocol = tokens[2]; + /// + /// Parses the request from the TcpClient + /// + public void ParseRequest() + { + var request = TcpClient.ReadLine(); + var tokens = request.Split(' '); + if (tokens.Length != 3) + { + Log($"invalid http request line:\n{request}"); + return; } - private Dictionary EncodedStringToDictionary(string s) + Method = tokens[0].ToUpper(); + FullPath = tokens[1]; + FullUrl = Server.GetFullUrlFor(FullPath); + var parts = FullPath.Split('?'); + if (parts.Length == 1) + UrlParameters = new Dictionary(); + else { - var parts = s.Split('&'); - return parts.Select(p => + var all = string.Join("?", parts.Skip(1)); + UrlParameters = EncodedStringToDictionary(all); + } + + Path = parts.First(); + Protocol = tokens[2]; + } + + private Dictionary EncodedStringToDictionary(string s) + { + var parts = s.Split('&'); + return parts.Select( + p => { var subParts = p.Split('='); var key = subParts.First(); var value = string.Join("=", subParts.Skip(1)); - return new { key, value }; - }).ToDictionary(x => x.key, x => x.value); - } + return new + { + key, + value + }; + } + ).ToDictionary(x => x.key, x => x.value); + } - /// - /// Reads in the headers from the TcpClient - /// - public void ReadHeaders() + /// + /// Reads in the headers from the TcpClient + /// + public void ReadHeaders() + { + while (TcpClient.ReadLine() is { } line) { - string line; - while ((line = TcpClient.ReadLine()) != null) + if (line.Equals(string.Empty)) { - if (line.Equals(string.Empty)) - { - return; - } - - var separator = line.IndexOf(':'); - if (separator == -1) - { - throw new Exception("invalid http header line: " + line); - } + return; + } - var name = line.Substring(0, separator); - var pos = separator + 1; - while ((pos < line.Length) && (line[pos] == ' ')) - { - pos++; // strip any spaces - } + var separator = line.IndexOf(':'); + if (separator == -1) + { + Log($"invalid http header line: {line}"); + continue; + } - var value = line.Substring(pos, line.Length - pos); - HttpHeaders[name] = value; + var name = line.Substring(0, separator); + var pos = separator + 1; + while (pos < line.Length && line[pos] == ' ') + { + pos++; // strip any spaces } + + var value = line.Substring(pos, line.Length - pos); + HttpHeaders[name] = value; } + } - /// - /// Handles a request that does not contain a body (as of the HTTP spec). - /// - private void HandleRequestWithoutBody(string method) + /// + /// Handles a request that does not contain a body (as of the HTTP spec). + /// + private void HandleRequestWithoutBody(string method) + { + Server.HandleRequestWithoutBody(this, method); + } + + private void HandleRequestWithBody(Stream stream, string method) + { + using var ms = new MemoryStream(); + if (HttpHeaders.ContainsKey(Headers.CONTENT_LENGTH)) { - Server.HandleRequestWithoutBody(this, method); + var contentLength = Convert.ToInt32(HttpHeaders[Headers.CONTENT_LENGTH]); + ReadBodyContent(stream, method, contentLength, ms); } - - private void HandleRequestWithBody(Stream stream, string method) + else if (HttpHeaders.ContainsKey(Headers.TRANSFER_ENCODING)) { - using var ms = new MemoryStream(); - if (HttpHeaders.ContainsKey(Headers.CONTENT_LENGTH)) - { - var contentLength = Convert.ToInt32(HttpHeaders[Headers.CONTENT_LENGTH]); - ReadBodyContent(stream, method, contentLength, ms); - } - else if (HttpHeaders.ContainsKey(Headers.TRANSFER_ENCODING)) + var transferEncoding = HttpHeaders[Headers.TRANSFER_ENCODING]; + if (!transferEncoding.Equals("chunked", StringComparison.OrdinalIgnoreCase)) { - var transferEncoding = HttpHeaders[Headers.TRANSFER_ENCODING]; - if (!transferEncoding.Equals("chunked", StringComparison.OrdinalIgnoreCase)) - { - throw new NotSupportedException( - $"Transfer-Encoding '{transferEncoding}' is not supported" - ); - } - - ReadChunked(stream, method, ms); + throw new NotSupportedException( + $"Transfer-Encoding '{transferEncoding}' is not supported" + ); } - ParseFormElementsIfRequired(ms); - Server.HandleRequestWithBody(this, ms, method); + ReadChunked(stream, method, ms); } - // see: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Transfer-Encoding#chunked_encoding - private void ReadChunked( - Stream stream, - string method, - MemoryStream ms - ) + ParseFormElementsIfRequired(ms); + Server.HandleRequestWithBody(this, ms, method); + } + + // see: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Transfer-Encoding#chunked_encoding + private void ReadChunked( + Stream stream, + string method, + MemoryStream ms + ) + { + var eol = new byte[2]; + while (true) { - var eol = new byte[2]; - while (true) + var chunkSize = stream.ReadLine(); + if (chunkSize is null) { - var chunkSize = stream.ReadLine(); - if (chunkSize is null) - { - return; - } - - if (!chunkSize.TryParseHex(out var toRead)) - { - throw new InvalidOperationException( - $"{method} Expected a chunk size, but found '{chunkSize}'" - ); - } - - if (toRead == 0) - { - // this is the terminating chunk - we're done after reading the - // final trailing newline - ReadNewlineFromStream(); - return; - } - - CopyBytes(stream, toRead, ms); - ReadNewlineFromStream(); + return; } - void ReadNewlineFromStream() + if (!chunkSize.TryParseHex(out var toRead)) { - // skip the trailing \r\n - var eolRead = stream.Read(eol, 0, 2); - if (eolRead != 2 || eol[0] != '\r' || eol[1] != '\n') - { - LogAction?.Invoke( - $"WARNING: possibly invalid chunked transfer: expected [ 13, 10 ], but received [ {eol[0]}, {eol[1]} ]" - ); - } + throw new InvalidOperationException( + $"{method} Expected a chunk size, but found '{chunkSize}'" + ); } - } - private void ReadBodyContent(Stream stream, string method, int contentLength, MemoryStream ms) - { - if (contentLength > MaxPostSize) + if (toRead == 0) { - throw new Exception( - $"{method} Content-Length({contentLength}) too big for this simple server (max: {MaxPostSize})" - ); + // this is the terminating chunk - we're done after reading the + // final trailing newline + ReadNewlineFromStream(); + return; } - CopyBytes(stream, contentLength, ms); - - ms.Seek(0, SeekOrigin.Begin); + CopyBytes(stream, toRead, ms); + ReadNewlineFromStream(); } - private static void CopyBytes( - Stream source, - int howMany, - Stream target - ) + void ReadNewlineFromStream() { - var buf = new byte[BUF_SIZE]; - var toRead = howMany; - while (toRead > 0) + // skip the trailing \r\n + var eolRead = stream.Read(eol, 0, 2); + if (eolRead != 2 || eol[0] != '\r' || eol[1] != '\n') { - var numRead = source.Read(buf, 0, Math.Min(BUF_SIZE, toRead)); - if (numRead == 0) - { - throw new Exception("client disconnected during post"); - } - - toRead -= numRead; - target.Write(buf, 0, numRead); + Log( + $"WARNING: possibly invalid chunked transfer: expected [ 13, 10 ], but received [ {eol[0]}, {eol[1]} ]" + ); } } + } + + private void Log(string str) + { + LogAction?.Invoke(str); + } - /// - /// Parses form data on the request, if available - /// - /// - public void ParseFormElementsIfRequired(MemoryStream ms) + private void ReadBodyContent(Stream stream, string method, int contentLength, MemoryStream ms) + { + if (contentLength > MaxPostSize) { - if (!HttpHeaders.ContainsKey(Headers.CONTENT_TYPE)) return; - if (HttpHeaders[Headers.CONTENT_TYPE] != "application/x-www-form-urlencoded") return; - try - { - var formData = Encoding.UTF8.GetString(ms.ToArray()); - FormData = EncodedStringToDictionary(formData); - } - catch - { - /* intentionally left blank */ - } + throw new Exception( + $"{method} Content-Length({contentLength}) too big for this simple server (max: {MaxPostSize})" + ); } - /// - /// Form data associated with the request - /// - public Dictionary FormData { get; set; } - - /// - /// Perform a successful write (ie, HTTP status 200) with the optionally - /// provided mime type and data data - /// - /// - /// - public void WriteSuccess(string mimeType = HttpConstants.MimeTypes.HTML, byte[] data = null) + CopyBytes(stream, contentLength, ms); + + ms.Seek(0, SeekOrigin.Begin); + } + + private static void CopyBytes( + Stream source, + int howMany, + Stream target + ) + { + var buf = new byte[BUF_SIZE]; + var toRead = howMany; + while (toRead > 0) { - WriteOKStatusHeader(); - WriteMIMETypeHeader(mimeType); - WriteConnectionClosesAfterCommsHeader(); - if (data != null) + var numRead = source.Read(buf, 0, Math.Min(BUF_SIZE, toRead)); + if (numRead == 0) { - WriteContentLengthHeader(data.Length); + throw new Exception("client disconnected during post"); } - WriteEmptyLineToStream(); - WriteDataToStream(data); + toRead -= numRead; + target.Write(buf, 0, numRead); } + } - /// - /// Writes the specified MIME header (Content-Type) - /// - /// - public void WriteMIMETypeHeader(string mimeType) + /// + /// Parses form data on the request, if available + /// + /// + public void ParseFormElementsIfRequired(MemoryStream ms) + { + if (!HttpHeaders.ContainsKey(Headers.CONTENT_TYPE)) return; + if (HttpHeaders[Headers.CONTENT_TYPE] != "application/x-www-form-urlencoded") return; + try { - WriteResponseLine(Headers.CONTENT_TYPE + ": " + mimeType); + var formData = Encoding.UTF8.GetString(ms.ToArray()); + FormData = EncodedStringToDictionary(formData); } - - /// - /// Writes the OK status header - /// - public void WriteOKStatusHeader() + catch { - WriteStatusHeader(HttpStatusCode.OK, "OK"); + /* intentionally left blank */ } + } - /// - /// Writes the Content-Length header with the provided length - /// - /// - public void WriteContentLengthHeader(int length) - { - WriteHeader("Content-Length", length); - } + /// + /// Form data associated with the request + /// + public Dictionary FormData { get; set; } - /// - /// Writes the header informing the client that the connection - /// will not be held open - /// - public void WriteConnectionClosesAfterCommsHeader() + /// + /// Perform a successful write (ie, HTTP status 200) with the optionally + /// provided mime type and data data + /// + /// + /// + public void WriteSuccess(string mimeType = HttpConstants.MimeTypes.HTML, byte[] data = null) + { + WriteOKStatusHeader(); + WriteMIMETypeHeader(mimeType); + WriteConnectionClosesAfterCommsHeader(); + if (data != null) { - WriteHeader("Connection", "close"); + WriteContentLengthHeader(data.Length); } - /// - /// Writes an arbitrary header to the response stream - /// - /// - /// - public void WriteHeader(string header, string value) - { - WriteResponseLine(string.Join(": ", header, value)); - } + WriteEmptyLineToStream(); + WriteDataToStream(data); + } - /// - /// Writes an integer-value header to the response stream - /// - /// - /// - public void WriteHeader(string header, int value) - { - WriteHeader(header, value.ToString()); - } + /// + /// Writes the specified MIME header (Content-Type) + /// + /// + public void WriteMIMETypeHeader(string mimeType) + { + WriteResponseLine($"{Headers.CONTENT_TYPE}: {mimeType}"); + } - /// - /// Writes the specified status header to the response stream, - /// with the optional message - /// - /// - /// - public void WriteStatusHeader(HttpStatusCode code, string message = null) - { - LogRequest(code, message); - WriteResponseLine($"HTTP/1.0 {(int) code} {message ?? code.ToString()}"); - } + /// + /// Writes the OK status header + /// + public void WriteOKStatusHeader() + { + WriteStatusHeader(HttpStatusCode.OK, "OK"); + } - private void LogRequest(HttpStatusCode code, string message) - { - var action = RequestLogAction; - action?.Invoke( - new RequestLogItem(FullPath, code, Method, message, HttpHeaders) - ); - } + /// + /// Writes the Content-Length header with the provided length + /// + /// + public void WriteContentLengthHeader(int length) + { + WriteHeader("Content-Length", length); + } - /// - /// Writes arbitrary byte data to the response stream - /// - /// - public void WriteDataToStream(byte[] data) - { - if (data == null) return; - try - { - _outputStream.Flush(); - _outputStream.BaseStream.Write(data, 0, data.Length); - _outputStream.BaseStream.Flush(); - } - catch (Exception ex) - { - LogAction?.Invoke(ex.Message); - } - } + /// + /// Writes the header informing the client that the connection + /// will not be held open + /// + public void WriteConnectionClosesAfterCommsHeader() + { + WriteHeader("Connection", "close"); + } - /// - /// Writes an arbitrary string to the response stream - /// - /// - public void WriteDataToStream(string data) - { - WriteDataToStream( - Encoding.UTF8.GetBytes(data ?? "") - ); - } + /// + /// Writes an arbitrary header to the response stream + /// + /// + /// + public void WriteHeader(string header, string value) + { + WriteResponseLine(string.Join(": ", header, value)); + } - /// - /// Writes out a simple http failure - /// - /// - public void WriteFailure(HttpStatusCode code) - { - WriteFailure(code, code.ToString()); - } + /// + /// Writes an integer-value header to the response stream + /// + /// + /// + public void WriteHeader(string header, int value) + { + WriteHeader(header, value.ToString()); + } - /// - /// Writes a failure code and message to the response stream and closes - /// the response - /// - /// - /// - public void WriteFailure( - HttpStatusCode code, - string message - ) + /// + /// Writes the specified status header to the response stream, + /// with the optional message + /// + /// + /// + public void WriteStatusHeader(HttpStatusCode code, string message = null) + { + LogRequest(code, message); + WriteResponseLine($"HTTP/1.0 {(int)code} {message ?? code.ToString()}"); + } + + private void LogRequest(HttpStatusCode code, string message) + { + var action = RequestLogAction; + action?.Invoke( + new RequestLogItem(FullPath, code, Method, message, HttpHeaders) + ); + } + + /// + /// Writes arbitrary byte data to the response stream + /// + /// + public void WriteDataToStream(byte[] data) + { + if (data == null) return; + try { - WriteFailure(code, message, null); + _outputStream.Flush(); + _outputStream.BaseStream.Write(data, 0, data.Length); + _outputStream.BaseStream.Flush(); } - - /// - /// Writes out an http failure with body text - /// - /// The http code to return - /// The message to add to the status line - /// The body to write out - public void WriteFailure( - HttpStatusCode code, - string message, - string body) + catch (Exception ex) { - WriteFailure(code, message, body, "text/plain"); + LogAction?.Invoke(ex.Message); } + } - /// - /// Write out a failure with a message, body and custom mime-type - /// - /// - /// - /// - /// - public void WriteFailure( - HttpStatusCode code, - string message, - string body, - string mimeType - ) - { - WriteStatusHeader(code, message); - WriteConnectionClosesAfterCommsHeader(); - if (string.IsNullOrEmpty(body)) - { - WriteEmptyLineToStream(); - return; - } + /// + /// Writes an arbitrary string to the response stream + /// + /// + public void WriteDataToStream(string data) + { + WriteDataToStream( + Encoding.UTF8.GetBytes(data ?? "") + ); + } - WriteMIMETypeHeader(mimeType); - WriteContentLengthHeader(body.Length); - WriteConnectionClosesAfterCommsHeader(); - WriteEmptyLineToStream(); - WriteDataToStream(body); - } + /// + /// Writes out a simple http failure + /// + /// + public void WriteFailure(HttpStatusCode code) + { + WriteFailure(code, code.ToString()); + } - /// - /// Writes an empty line to the stream: HTTP is line-based, - /// so the client will probably interpret this as an end - /// of section / request - /// - public void WriteEmptyLineToStream() - { - WriteResponseLine(string.Empty); - } + /// + /// Writes a failure code and message to the response stream and closes + /// the response + /// + /// + /// + public void WriteFailure( + HttpStatusCode code, + string message + ) + { + WriteFailure(code, message, null); + } - /// - /// Write an arbitrary string response to the response stream - /// - /// - public void WriteResponseLine(string response) - { - _outputStream.WriteLine(response); - } + /// + /// Writes out an http failure with body text + /// + /// The http code to return + /// The message to add to the status line + /// The body to write out + public void WriteFailure( + HttpStatusCode code, + string message, + string body + ) + { + WriteFailure(code, message, body, "text/plain"); + } - /// - /// Write a textural document to the response stream with - /// the assumption that the mime type is text/html - /// - /// - public void WriteDocument(string document) + /// + /// Write out a failure with a message, body and custom mime-type + /// + /// + /// + /// + /// + public void WriteFailure( + HttpStatusCode code, + string message, + string body, + string mimeType + ) + { + WriteStatusHeader(code, message); + WriteConnectionClosesAfterCommsHeader(); + if (string.IsNullOrEmpty(body)) { - WriteDocument(document, HttpConstants.MimeTypes.HTML); + WriteEmptyLineToStream(); + return; } - /// - /// Write a textural document to the response stream with - /// the optionally-provided mime type - /// - /// - /// - public void WriteDocument(string document, string mimeType) - { - WriteSuccess(mimeType, Encoding.UTF8.GetBytes(document)); - } + WriteMIMETypeHeader(mimeType); + WriteContentLengthHeader(body.Length); + WriteConnectionClosesAfterCommsHeader(); + WriteEmptyLineToStream(); + WriteDataToStream(body); + } + + /// + /// Writes an empty line to the stream: HTTP is line-based, + /// so the client will probably interpret this as an end + /// of section / request + /// + public void WriteEmptyLineToStream() + { + WriteResponseLine(string.Empty); + } + + /// + /// Write an arbitrary string response to the response stream + /// + /// + public void WriteResponseLine(string response) + { + _outputStream.WriteLine(response); + } + + /// + /// Write a textural document to the response stream with + /// the assumption that the mime type is text/html + /// + /// + public void WriteDocument(string document) + { + WriteDocument(document, HttpConstants.MimeTypes.HTML); + } + + /// + /// Write a textural document to the response stream with + /// the optionally-provided mime type + /// + /// + /// + public void WriteDocument(string document, string mimeType) + { + WriteSuccess(mimeType, Encoding.UTF8.GetBytes(document)); } } \ No newline at end of file diff --git a/source/SimpleServers/PeanutButter.SimpleHTTPServer/HttpServer.cs b/source/SimpleServers/PeanutButter.SimpleHTTPServer/HttpServer.cs index 1bdfdff0f5..b5877ba505 100644 --- a/source/SimpleServers/PeanutButter.SimpleHTTPServer/HttpServer.cs +++ b/source/SimpleServers/PeanutButter.SimpleHTTPServer/HttpServer.cs @@ -8,758 +8,756 @@ using System.Xml.Linq; using Newtonsoft.Json; using PeanutButter.Utils; -using static PeanutButter.SimpleHTTPServer.HttpConstants; // ReSharper disable UnusedMember.Global // ReSharper disable IntroduceOptionalParameters.Global // ReSharper disable MemberCanBePrivate.Global -namespace PeanutButter.SimpleHTTPServer +namespace PeanutButter.SimpleHTTPServer; + +/// +/// Result to return from one part of the pipeline +/// +public enum HttpServerPipelineResult { /// - /// Result to return from one part of the pipeline + /// The request was handled exclusively and processing should stop /// - public enum HttpServerPipelineResult - { - /// - /// The request was handled exclusively and processing should stop - /// - HandledExclusively, + HandledExclusively, - /// - /// The request was handled, but may continue along the pipeline - /// - Handled, + /// + /// The request was handled, but may continue along the pipeline + /// + Handled, - /// - /// The request was not handled at all: processing should continue - /// - NotHandled - } + /// + /// The request was not handled at all: processing should continue + /// + NotHandled +} +/// +/// Http methods supported by requests +/// +public enum HttpMethods +{ /// - /// Http methods supported by requests + /// This request is for or supports any method /// - public enum HttpMethods - { - /// - /// This request is for or supports any method - /// - Any, + Any, - /// - /// This request is for or supports GET - /// - Get, + /// + /// This request is for or supports GET + /// + Get, - /// - /// This request is for or supports POST - /// - Post, + /// + /// This request is for or supports POST + /// + Post, - /// - /// This request is for or supports PUT - /// - Put, + /// + /// This request is for or supports PUT + /// + Put, - /// - /// This request is for or supports DELETE - /// - Delete, + /// + /// This request is for or supports DELETE + /// + Delete, - /// - /// This request is for or supports PATCH - /// - Patch, + /// + /// This request is for or supports PATCH + /// + Patch, - /// - /// This request is for or supports OPTIONS - /// - Options, + /// + /// This request is for or supports OPTIONS + /// + Options, - /// - /// This request is for or supports HEAD - /// - Head - } + /// + /// This request is for or supports HEAD + /// + Head +} + +// TODO: allow easier way to throw 404 (or other web exception) from simple handlers (file/document handlers) +/// +/// Provides a simple way to run an in-memory http server situations +/// like testing or where a small, very simple http server might be useful +/// +public interface IHttpServer : IHttpServerBase +{ + /// + /// Adds a handler to the pipeline + /// + /// + Guid AddHandler(Func handler); - // TODO: allow easier way to throw 404 (or other web exception) from simple handlers (file/document handlers) /// - /// Provides a simple way to run an in-memory http server situations - /// like testing or where a small, very simple http server might be useful + /// Adds a handler for providing a file download /// - public interface IHttpServer : IHttpServerBase - { - /// - /// Adds a handler to the pipeline - /// - /// - Guid AddHandler(Func handler); + /// + /// + Guid AddFileHandler( + Func handler, + string contentType = HttpConstants.MimeTypes.BYTES + ); - /// - /// Adds a handler for providing a file download - /// - /// - /// - Guid AddFileHandler( - Func handler, - string contentType = HttpConstants.MimeTypes.BYTES - ); + /// + /// Adds a handler to serve a text document, with (limited) automatic + /// content-type detection. + /// + /// + Guid AddDocumentHandler(Func handler); - /// - /// Adds a handler to serve a text document, with (limited) automatic - /// content-type detection. - /// - /// - Guid AddDocumentHandler(Func handler); - - /// - /// Specifically add a handler to serve an HTML document - /// - /// - Guid AddHtmlDocumentHandler(Func handler); - - /// - /// Specifically add a handler to serve a JSON document - /// - /// - Guid AddJsonDocumentHandler(Func handler); - - /// - /// Serves an XDocument from the provided path, for the provided method - /// - /// Absolute path to serve the document for - /// XDocument to serve - /// Which http method to respond to - void ServeDocument( - string queryPath, - XDocument doc, - HttpMethods method = HttpMethods.Any - ); + /// + /// Specifically add a handler to serve an HTML document + /// + /// + Guid AddHtmlDocumentHandler(Func handler); - /// - /// Serves an XDocument from the provided path, for the provided method - /// - /// Absolute path to serve the document for - /// XDocument to serve - /// Which http method to respond to - void ServeDocument( - string queryPath, - string doc, - HttpMethods method = HttpMethods.Any - ); + /// + /// Specifically add a handler to serve a JSON document + /// + /// + Guid AddJsonDocumentHandler(Func handler); - /// - /// Serves an XDocument from the provided path, for the provided method - /// - /// Absolute path to serve the document for - /// XDocument to serve - /// Which http method to respond to - Guid ServeDocument( - string queryPath, - Func doc, - HttpMethods method = HttpMethods.Any - ); + /// + /// Serves an XDocument from the provided path, for the provided method + /// + /// Absolute path to serve the document for + /// XDocument to serve + /// Which http method to respond to + void ServeDocument( + string queryPath, + XDocument doc, + HttpMethods method = HttpMethods.Any + ); - /// - /// Serves an XDocument from the provided path, for the provided method - /// - /// Absolute path to serve the document for - /// Factory function to get the document contents - /// Which http method to respond to - Guid ServeDocument( - string queryPath, - Func docFactory, - HttpMethods method = HttpMethods.Any - ); + /// + /// Serves an XDocument from the provided path, for the provided method + /// + /// Absolute path to serve the document for + /// XDocument to serve + /// Which http method to respond to + void ServeDocument( + string queryPath, + string doc, + HttpMethods method = HttpMethods.Any + ); - /// - /// Serves a JSON document with the provided data at the provided path for the - /// provided method - /// - /// Absolute path matched for this document - /// Any object which will be serialized into JSON for you - /// Which http method to respond to - Guid ServeJsonDocument( - string path, - object data, - HttpMethods method = HttpMethods.Any - ); + /// + /// Serves an XDocument from the provided path, for the provided method + /// + /// Absolute path to serve the document for + /// XDocument to serve + /// Which http method to respond to + Guid ServeDocument( + string queryPath, + Func doc, + HttpMethods method = HttpMethods.Any + ); - /// - /// Serves a JSON document with the provided data at the provided path for the - /// provided method - /// - /// Absolute path matched for this document - /// Factory function returning any object which will be serialized into JSON for you - /// Which http method to respond to - Guid ServeJsonDocument( - string path, - Func dataFactory, - HttpMethods method = HttpMethods.Any - ); + /// + /// Serves an XDocument from the provided path, for the provided method + /// + /// Absolute path to serve the document for + /// Factory function to get the document contents + /// Which http method to respond to + Guid ServeDocument( + string queryPath, + Func docFactory, + HttpMethods method = HttpMethods.Any + ); - /// - /// Serves an arbitrary file from the provided path for the - /// provided content type (defaults to application/octet-stream) - /// - /// Absolute path matched for this file - /// Data to provide - /// Content type of the data - Guid ServeFile(string path, byte[] data, string contentType = HttpConstants.MimeTypes.BYTES); - - /// - /// Serves a file via a factory Func - /// - /// Absolute path matched for this file - /// Factory for the data - /// Content type - Guid ServeFile( - string path, - Func dataFactory, - string contentType = HttpConstants.MimeTypes.BYTES - ); + /// + /// Serves a JSON document with the provided data at the provided path for the + /// provided method + /// + /// Absolute path matched for this document + /// Any object which will be serialized into JSON for you + /// Which http method to respond to + Guid ServeJsonDocument( + string path, + object data, + HttpMethods method = HttpMethods.Any + ); - /// - /// Clears any registered handlers & log actions - /// so the server can be re-used with completely - /// different logic / handlers - /// - void Reset(); - } + /// + /// Serves a JSON document with the provided data at the provided path for the + /// provided method + /// + /// Absolute path matched for this document + /// Factory function returning any object which will be serialized into JSON for you + /// Which http method to respond to + Guid ServeJsonDocument( + string path, + Func dataFactory, + HttpMethods method = HttpMethods.Any + ); /// - /// Provides the simple HTTP server you may use ad-hoc + /// Serves an arbitrary file from the provided path for the + /// provided content type (defaults to application/octet-stream) /// - [DebuggerDisplay("HttpServer {Port} ({HandlerCount} handlers)")] - public class HttpServer : HttpServerBase, IHttpServer - { - /// - /// Used in debug display - /// - public int HandlerCount => _handlers.Count; + /// Absolute path matched for this file + /// Data to provide + /// Content type of the data + Guid ServeFile(string path, byte[] data, string contentType = HttpConstants.MimeTypes.BYTES); - private ConcurrentQueue _handlers = new(); + /// + /// Serves a file via a factory Func + /// + /// Absolute path matched for this file + /// Factory for the data + /// Content type + Guid ServeFile( + string path, + Func dataFactory, + string contentType = HttpConstants.MimeTypes.BYTES + ); - private class Handler - { - public Func Logic { get; } - public Guid Id { get; } = Guid.NewGuid(); + /// + /// Clears any registered handlers & log actions + /// so the server can be re-used with completely + /// different logic / handlers + /// + void Reset(); +} + +/// +/// Provides the simple HTTP server you may use ad-hoc +/// +[DebuggerDisplay("HttpServer {Port} ({HandlerCount} handlers)")] +public class HttpServer : HttpServerBase, IHttpServer +{ + /// + /// Used in debug display + /// + public int HandlerCount => _handlers.Count; - public Handler( - Func logic - ) - { - Logic = logic; - var q = new ConcurrentQueue(); - q.Clear(); - } - } + private ConcurrentQueue _handlers = new(); - private readonly Func _jsonSerializer = o => o.SerializeToJson(); + private class Handler + { + public Func Logic { get; } + public Guid Id { get; } = Guid.NewGuid(); - /// - public HttpServer(int port, bool autoStart, Action logAction) - : base(port) + public Handler( + Func logic + ) { - LogAction = logAction; - AutoStart(autoStart); + Logic = logic; + var q = new ConcurrentQueue(); + q.Clear(); } + } - /// - /// Constructs an HttpServer listening on the configured port with the given logAction - /// - /// - /// - public HttpServer(int port, Action logAction) - : this(port, true, logAction) - { - } + private readonly Func _jsonSerializer = o => o.SerializeToJson(); - /// - /// Constructs an HttpServer with autoStart true and no logAction - /// - public HttpServer() : this(true, null) - { - } + /// + public HttpServer(int port, bool autoStart, Action logAction) + : base(port) + { + LogAction = logAction; + AutoStart(autoStart); + } - /// - /// Constructs an HttpServer with autoStart true and provided logAction - /// - /// Action to log messages with - public HttpServer(Action logAction) : this(true, logAction) - { - } + /// + /// Constructs an HttpServer listening on the configured port with the given logAction + /// + /// + /// + public HttpServer(int port, Action logAction) + : this(port, true, logAction) + { + } - /// - /// Constructs an HttpServer based on passed in autoStart, with no logAction - /// - /// Start immediately? - public HttpServer(bool autoStart) : this(autoStart, null) - { - } + /// + /// Constructs an HttpServer with autoStart true and no logAction + /// + public HttpServer() : this(true, null) + { + } - /// - /// Constructs an HttpServer - /// - /// Start immediately? - /// Action to log messages - public HttpServer(bool autoStart, Action logAction) - { - LogAction = logAction; - AutoStart(autoStart); - } + /// + /// Constructs an HttpServer with autoStart true and provided logAction + /// + /// Action to log messages with + public HttpServer(Action logAction) : this(true, logAction) + { + } - private void AutoStart(bool autoStart) - { - if (autoStart) - { - Start(); - } - } + /// + /// Constructs an HttpServer based on passed in autoStart, with no logAction + /// + /// Start immediately? + public HttpServer(bool autoStart) : this(autoStart, null) + { + } - /// - protected override void Init() - { - _handlers = new(); - } + /// + /// Constructs an HttpServer + /// + /// Start immediately? + /// Action to log messages + public HttpServer(bool autoStart, Action logAction) + { + LogAction = logAction; + AutoStart(autoStart); + } - // ReSharper disable once MemberCanBePrivate.Global - /// - /// Adds a handler to the pipeline - /// - /// - public Guid AddHandler(Func handler) + private void AutoStart(bool autoStart) + { + if (autoStart) { - var container = new Handler(handler); - _handlers.Enqueue(container); - return container.Id; + Start(); } + } - /// - /// Adds a handler for providing a file download - /// - /// - /// - public Guid AddFileHandler( - Func handler, - string contentType = HttpConstants.MimeTypes.BYTES - ) - { - return AddHandler( - (p, s) => - { - Log("Handling file request: {0}", p.FullUrl); - var data = handler(p, s); - if (data == null) - { - Log(" --> no file handler set up for {0}", p.FullUrl); - return HttpServerPipelineResult.NotHandled; - } + /// + protected override void Init() + { + _handlers = new(); + } + + // ReSharper disable once MemberCanBePrivate.Global + /// + /// Adds a handler to the pipeline + /// + /// + public Guid AddHandler(Func handler) + { + var container = new Handler(handler); + _handlers.Enqueue(container); + return container.Id; + } - p.WriteSuccess(contentType, data); - Log(" --> Successful file handling for {0}", p.FullUrl); - return HttpServerPipelineResult.HandledExclusively; + /// + /// Adds a handler for providing a file download + /// + /// + /// + public Guid AddFileHandler( + Func handler, + string contentType = HttpConstants.MimeTypes.BYTES + ) + { + return AddHandler( + (p, s) => + { + Log("Handling file request: {0}", p.FullUrl); + var data = handler(p, s); + if (data == null) + { + Log(" --> no file handler set up for {0}", p.FullUrl); + return HttpServerPipelineResult.NotHandled; } - ); - } - /// - /// Adds a handler to serve a text document, with (limited) automatic - /// content-type detection. - /// - /// - public Guid AddDocumentHandler(Func handler) - { - return HandleDocumentRequestWith( - handler, - "auto", - AutoSerializer, - AutoMime - ); - } + p.WriteSuccess(contentType, data); + Log(" --> Successful file handling for {0}", p.FullUrl); + return HttpServerPipelineResult.HandledExclusively; + } + ); + } - private static readonly Func[] AutoMimeStrategies = - { - AutoMimeHtml, - AutoMimeJson, - AutoMimeXml, - AutoMimeDefault - }; + /// + /// Adds a handler to serve a text document, with (limited) automatic + /// content-type detection. + /// + /// + public Guid AddDocumentHandler(Func handler) + { + return HandleDocumentRequestWith( + handler, + "auto", + AutoSerializer, + AutoMime + ); + } - private static string AutoMimeDefault(string arg) - { - return HttpConstants.MimeTypes.BYTES; - } + private static readonly Func[] AutoMimeStrategies = + { + AutoMimeHtml, + AutoMimeJson, + AutoMimeXml, + AutoMimeDefault + }; - private static string AutoMimeJson(string content) - { - return - content != null && - content.StartsWith("{") && - content.EndsWith("}") - ? HttpConstants.MimeTypes.JSON - : null; - } + private static string AutoMimeDefault(string arg) + { + return HttpConstants.MimeTypes.BYTES; + } - private static string AutoMimeXml(string content) - { - return content != null && - (content.StartsWith("")) - return false; - var openingTag = tag ?? FindOpeningTag(content); - return content.StartsWith($"<{openingTag}", StringComparison.OrdinalIgnoreCase) && - content.EndsWith($"", StringComparison.OrdinalIgnoreCase); - } + private static string AutoMimeXml(string content) + { + return content != null && + (content.StartsWith("", openTag, StringComparison.Ordinal); - var space = content.IndexOf(" ", openTag, StringComparison.Ordinal); - var end = space == -1 - ? closeTag - : Math.Min(space, closeTag); - return content.Substring(openTag + 1, end - openTag - 1); - } + private static bool HasOpenAndCloseTags(string content, string tag = null) + { + if (!content.StartsWith("<") && !content.EndsWith(">")) + return false; + var openingTag = tag ?? FindOpeningTag(content); + return content.StartsWith($"<{openingTag}", StringComparison.OrdinalIgnoreCase) && + content.EndsWith($"", StringComparison.OrdinalIgnoreCase); + } - private static string AutoMimeHtml(string content) - { - return content != null && - (content.ToLowerInvariant().StartsWith("", openTag, StringComparison.Ordinal); + var space = content.IndexOf(" ", openTag, StringComparison.Ordinal); + var end = space == -1 + ? closeTag + : Math.Min(space, closeTag); + return content.Substring(openTag + 1, end - openTag - 1); + } - private string AutoMime(string servedResult) - { - var trimmed = servedResult.Trim(); - return AutoMimeStrategies.Aggregate( - null as string, - (acc, cur) => acc ?? cur(trimmed) - ); - } + private static string AutoMimeHtml(string content) + { + return content != null && + (content.ToLowerInvariant().StartsWith(" acc ?? cur(trimmed) + ); + } - /// - /// Specifically add a handler to serve an HTML document - /// - /// - public Guid AddHtmlDocumentHandler(Func handler) - { - return HandleDocumentRequestWith( - handler, - "html", - null, - _ => HttpConstants.MimeTypes.HTML - ); - } + private string AutoSerializer(object servedResult) + { + if (servedResult is string stringResult) + return stringResult; + return JsonConvert.SerializeObject(servedResult); + } - /// - /// Specifically add a handler to serve a JSON document - /// - /// - public Guid AddJsonDocumentHandler(Func handler) - { - return HandleDocumentRequestWith( - handler, - "json", - o => _jsonSerializer(o), - _ => HttpConstants.MimeTypes.JSON - ); - } + /// + /// Specifically add a handler to serve an HTML document + /// + /// + public Guid AddHtmlDocumentHandler(Func handler) + { + return HandleDocumentRequestWith( + handler, + "html", + null, + _ => HttpConstants.MimeTypes.HTML + ); + } - private Guid HandleDocumentRequestWith( - Func handler, - string documentTypeForLogging, - Func stringProcessor, - Func mimeTypeGenerator - ) - { - return AddHandler( - (p, s) => + /// + /// Specifically add a handler to serve a JSON document + /// + /// + public Guid AddJsonDocumentHandler(Func handler) + { + return HandleDocumentRequestWith( + handler, + "json", + o => _jsonSerializer(o), + _ => HttpConstants.MimeTypes.JSON + ); + } + + private Guid HandleDocumentRequestWith( + Func handler, + string documentTypeForLogging, + Func stringProcessor, + Func mimeTypeGenerator + ) + { + return AddHandler( + (p, s) => + { + Log($"Handling {documentTypeForLogging} document request: {p.FullUrl}"); + var doc = handler(p, s); + if (doc == null) + { + Log($" --> no {documentTypeForLogging} document handler set up for {0}", p.FullUrl); + return HttpServerPipelineResult.NotHandled; + } + + var asString = doc as string; + if (asString == null) { - Log($"Handling {documentTypeForLogging} document request: {p.FullUrl}"); - var doc = handler(p, s); - if (doc == null) + try { - Log($" --> no {documentTypeForLogging} document handler set up for {0}", p.FullUrl); - return HttpServerPipelineResult.NotHandled; + asString = stringProcessor(doc); } - - var asString = doc as string; - if (asString == null) + catch (Exception ex) { - try - { - asString = stringProcessor(doc); - } - catch (Exception ex) - { - Log($"Unable to process request to string result: {ex.Message}"); - } + Log($"Unable to process request to string result: {ex.Message}"); } - - p.WriteDocument(asString, mimeTypeGenerator(asString)); - Log($" --> Successful {documentTypeForLogging} document handling for {p.FullUrl}"); - return HttpServerPipelineResult.HandledExclusively; } - ); - } - private void InvokeHandlersWith(HttpProcessor p, Stream stream) + p.WriteDocument(asString, mimeTypeGenerator(asString)); + Log($" --> Successful {documentTypeForLogging} document handling for {p.FullUrl}"); + return HttpServerPipelineResult.HandledExclusively; + } + ); + } + + private void InvokeHandlersWith(HttpProcessor p, Stream stream) + { + var handled = false; + foreach (var handler in _handlers) { - var handled = false; - foreach (var handler in _handlers) + try { - try - { - var pipelineResult = handler.Logic(p, stream); - if (pipelineResult == HttpServerPipelineResult.HandledExclusively) - { - handled = true; - break; - } - } - catch (Exception ex) + var pipelineResult = handler.Logic(p, stream); + if (pipelineResult == HttpServerPipelineResult.HandledExclusively) { - var body = string.Join( - "\n", - "Internal server error", - ex.Message, - ex.StackTrace - ); - Log(body); - - p.WriteFailure( - HttpStatusCode.InternalServerError, - "Internal server error", - body - ); - throw; + handled = true; + break; } } - - if (!handled) + catch (Exception ex) { - Log("No handlers found for {0}", p.FullUrl); - throw new FileNotFoundException("Request was not handled by any registered handlers"); + var body = string.Join( + "\n", + "Internal server error", + ex.Message, + ex.StackTrace + ); + Log(body); + + p.WriteFailure( + HttpStatusCode.InternalServerError, + "Internal server error", + body + ); + throw; } } - /// - public override void HandleRequestWithoutBody(HttpProcessor p, string method) + if (!handled) { - Log($"Incoming {method} request: {p.FullUrl}"); - InvokeHandlersWith(p, null); + Log("No handlers found for {0}", p.FullUrl); + throw new FileNotFoundException("Request was not handled by any registered handlers"); } + } - /// - public override void HandleRequestWithBody(HttpProcessor p, MemoryStream inputData, string method) - { - Log($"Incoming {method} request: {p.FullUrl}"); - InvokeHandlersWith(p, inputData); - } + /// + public override void HandleRequestWithoutBody(HttpProcessor p, string method) + { + Log($"Incoming {method} request: {p.FullUrl}"); + InvokeHandlersWith(p, null); + } - /// - /// Serves an XDocument from the provided path, for the provided method - /// - /// Absolute path to serve the document for - /// XDocument to serve - /// Which http method to respond to - public void ServeDocument( - string queryPath, - XDocument doc, - HttpMethods method = HttpMethods.Any - ) - { - ServeDocument(queryPath, () => doc.ToString(), method); - } + /// + public override void HandleRequestWithBody(HttpProcessor p, MemoryStream inputData, string method) + { + Log($"Incoming {method} request: {p.FullUrl}"); + InvokeHandlersWith(p, inputData); + } - /// - /// Serves an XDocument from the provided path, for the provided method - /// - /// Absolute path to serve the document for - /// XDocument to serve - /// Which http method to respond to - public void ServeDocument( - string queryPath, - string doc, - HttpMethods method = HttpMethods.Any - ) - { - ServeDocument(queryPath, () => doc, method); - } + /// + /// Serves an XDocument from the provided path, for the provided method + /// + /// Absolute path to serve the document for + /// XDocument to serve + /// Which http method to respond to + public void ServeDocument( + string queryPath, + XDocument doc, + HttpMethods method = HttpMethods.Any + ) + { + ServeDocument(queryPath, () => doc.ToString(), method); + } - /// - /// Serves an XDocument from the provided path, for the provided method - /// - /// Absolute path to serve the document for - /// XDocument to serve - /// Which http method to respond to - public Guid ServeDocument( - string queryPath, - Func doc, - HttpMethods method = HttpMethods.Any - ) - { - return AddHtmlDocumentHandler( - (p, _) => - { - if (p.FullPath != queryPath || !method.Matches(p.Method)) - return null; - Log("Serving html document at {0}", p.FullUrl); - return doc(); - } - ); - } + /// + /// Serves an XDocument from the provided path, for the provided method + /// + /// Absolute path to serve the document for + /// XDocument to serve + /// Which http method to respond to + public void ServeDocument( + string queryPath, + string doc, + HttpMethods method = HttpMethods.Any + ) + { + ServeDocument(queryPath, () => doc, method); + } - /// - /// Serves an XDocument from the provided path, for the provided method - /// - /// Absolute path to serve the document for - /// Factory function to get the document contents - /// Which http method to respond to - public Guid ServeDocument( - string queryPath, - Func docFactory, - HttpMethods method = HttpMethods.Any - ) - { - return ServeDocument( - queryPath, - () => docFactory().ToString(), - method - ); - } + /// + /// Serves an XDocument from the provided path, for the provided method + /// + /// Absolute path to serve the document for + /// XDocument to serve + /// Which http method to respond to + public Guid ServeDocument( + string queryPath, + Func doc, + HttpMethods method = HttpMethods.Any + ) + { + return AddHtmlDocumentHandler( + (p, _) => + { + if (p.FullPath != queryPath || !method.Matches(p.Method)) + return null; + Log("Serving html document at {0}", p.FullUrl); + return doc(); + } + ); + } - /// - /// Serves a JSON document with the provided data at the provided path for the - /// provided method - /// - /// Absolute path matched for this document - /// Any object which will be serialized into JSON for you - /// Which http method to respond to - public Guid ServeJsonDocument( - string path, - object data, - HttpMethods method = HttpMethods.Any - ) - { - return ServeJsonDocument(path, () => data, method); - } + /// + /// Serves an XDocument from the provided path, for the provided method + /// + /// Absolute path to serve the document for + /// Factory function to get the document contents + /// Which http method to respond to + public Guid ServeDocument( + string queryPath, + Func docFactory, + HttpMethods method = HttpMethods.Any + ) + { + return ServeDocument( + queryPath, + () => docFactory().ToString(), + method + ); + } - /// - /// Serves a JSON document with the provided data at the provided path for the - /// provided method - /// - /// Absolute path matched for this document - /// Factory function returning any object which will be serialized into JSON for you - /// Which http method to respond to - public Guid ServeJsonDocument( - string path, - Func dataFactory, - HttpMethods method = HttpMethods.Any - ) - { - return AddJsonDocumentHandler( - (p, _) => - { - if (p.FullPath != path || !method.Matches(p.Method)) - return null; - Log("Serving JSON document at {0}", p.FullUrl); - return dataFactory(); - } - ); - } + /// + /// Serves a JSON document with the provided data at the provided path for the + /// provided method + /// + /// Absolute path matched for this document + /// Any object which will be serialized into JSON for you + /// Which http method to respond to + public Guid ServeJsonDocument( + string path, + object data, + HttpMethods method = HttpMethods.Any + ) + { + return ServeJsonDocument(path, () => data, method); + } - /// - /// Serves an arbitrary file from the provided path for the - /// provided content type (defaults to application/octet-stream) - /// - /// Absolute path matched for this file - /// Data to provide - /// Content type of the data - public Guid ServeFile(string path, byte[] data, string contentType = HttpConstants.MimeTypes.BYTES) - { - return ServeFile(path, () => data, contentType); - } + /// + /// Serves a JSON document with the provided data at the provided path for the + /// provided method + /// + /// Absolute path matched for this document + /// Factory function returning any object which will be serialized into JSON for you + /// Which http method to respond to + public Guid ServeJsonDocument( + string path, + Func dataFactory, + HttpMethods method = HttpMethods.Any + ) + { + return AddJsonDocumentHandler( + (p, _) => + { + if (p.FullPath != path || !method.Matches(p.Method)) + return null; + Log("Serving JSON document at {0}", p.FullUrl); + return dataFactory(); + } + ); + } - /// - /// Serves a file via a factory Func - /// - /// Absolute path matched for this file - /// Factory for the data - /// Content type - public Guid ServeFile( - string path, - Func dataFactory, - string contentType = HttpConstants.MimeTypes.BYTES - ) - { - return AddFileHandler( - (p, _) => + /// + /// Serves an arbitrary file from the provided path for the + /// provided content type (defaults to application/octet-stream) + /// + /// Absolute path matched for this file + /// Data to provide + /// Content type of the data + public Guid ServeFile(string path, byte[] data, string contentType = HttpConstants.MimeTypes.BYTES) + { + return ServeFile(path, () => data, contentType); + } + + /// + /// Serves a file via a factory Func + /// + /// Absolute path matched for this file + /// Factory for the data + /// Content type + public Guid ServeFile( + string path, + Func dataFactory, + string contentType = HttpConstants.MimeTypes.BYTES + ) + { + return AddFileHandler( + (p, _) => + { + if (p.Path != path) { - if (p.Path != path) - { - return null; - } + return null; + } - Log("Serving file at {0}", p.FullUrl); - return dataFactory(); - }, - contentType - ); - } + Log("Serving file at {0}", p.FullUrl); + return dataFactory(); + }, + contentType + ); + } - /// - /// Clears any registered handlers & log actions - /// so the server can be re-used with completely - /// different logic / handlers - /// - public void Reset() - { - LogAction = null; - RequestLogAction = null; - _handlers.Clear(); - } + /// + /// Clears any registered handlers & log actions + /// so the server can be re-used with completely + /// different logic / handlers + /// + public void Reset() + { + LogAction = null; + RequestLogAction = null; + _handlers.Clear(); + } - /// - /// Removes a previously-registered handler by it's id - /// - /// - /// - /// - public bool RemoveHandler(Guid identifier) + /// + /// Removes a previously-registered handler by it's id + /// + /// + /// + /// + public bool RemoveHandler(Guid identifier) + { + var newHandlers = new ConcurrentQueue(); + var removed = false; + foreach (var item in _handlers.ToArray()) { - var newHandlers = new ConcurrentQueue(); - var removed = false; - foreach (var item in _handlers.ToArray()) + if (item.Id == identifier) { - if (item.Id == identifier) - { - removed = true; - continue; - } - - newHandlers.Enqueue(item); + removed = true; + continue; } - Interlocked.Exchange(ref _handlers, newHandlers); - return removed; + + newHandlers.Enqueue(item); } + Interlocked.Exchange(ref _handlers, newHandlers); + return removed; } } \ No newline at end of file diff --git a/source/SimpleServers/PeanutButter.SimpleTcpServer/TcpServer.cs b/source/SimpleServers/PeanutButter.SimpleTcpServer/TcpServer.cs index 6423286bdb..c1f05e0021 100644 --- a/source/SimpleServers/PeanutButter.SimpleTcpServer/TcpServer.cs +++ b/source/SimpleServers/PeanutButter.SimpleTcpServer/TcpServer.cs @@ -65,7 +65,6 @@ public abstract class TcpServer : ITrackingDisposable // ReSharper disable once MemberCanBePrivate.Global - /// /// Port which this server has bound to ///