From 5d42fa7f8336a523f814bfaab7212fe7673a2f6d Mon Sep 17 00:00:00 2001 From: andrewlalis Date: Thu, 9 Jan 2025 22:26:09 -0500 Subject: [PATCH] Cleaned up main http1.1 module. --- source/handy_http_transport/helpers.d | 95 ++++++++++ source/handy_http_transport/http1/transport.d | 167 +----------------- .../response_output_stream.d | 77 ++++++++ 3 files changed, 175 insertions(+), 164 deletions(-) create mode 100644 source/handy_http_transport/helpers.d create mode 100644 source/handy_http_transport/response_output_stream.d diff --git a/source/handy_http_transport/helpers.d b/source/handy_http_transport/helpers.d new file mode 100644 index 0000000..a671489 --- /dev/null +++ b/source/handy_http_transport/helpers.d @@ -0,0 +1,95 @@ +module handy_http_transport.helpers; + +import streams; + +/** + * Helper function to consume string content from an input stream until a + * certain target pattern of characters is encountered. + * Params: + * inputStream = The stream to read from. + * target = The target at which to stop reading. + * Returns: The string that was read, or a stream error. + */ +Either!(string, "value", StreamError, "error") consumeUntil(S)( + S inputStream, + string target +) if (isByteInputStream!S) { + ubyte[1024] buffer; + size_t idx; + while (true) { + auto result = inputStream.readFromStream(buffer[idx .. idx + 1]); + if (result.hasError) return Either!(string, "value", StreamError, "error")(result.error); + if (result.count != 1) return Either!(string, "value", StreamError, "error")( + StreamError("Failed to read a single element", 1) + ); + idx++; + if (idx >= target.length && buffer[idx - target.length .. idx] == target) { + return Either!(string, "value", StreamError, "error")( + cast(string) buffer[0 .. idx - target.length].idup + ); + } + if (idx >= buffer.length) { + return Either!(string, "value", StreamError, "error")( + StreamError("Couldn't find target \"" ~ target ~ "\" after reading 1024 bytes.", 1) + ); + } + } +} + +/** + * Internal helper function to get the first index of a character in a string. + * Params: + * s = The string to look in. + * c = The character to look for. + * offset = An optional offset to look from. + * Returns: The index of the character, or -1. + */ +ptrdiff_t indexOf(string s, char c, size_t offset = 0) { + for (size_t i = offset; i < s.length; i++) { + if (s[i] == c) return i; + } + return -1; +} + +/** + * Internal helper function that returns the slice of a string excluding any + * preceding or trailing spaces. + * Params: + * s = The string to strip. + * Returns: The slice of the string that has been stripped. + */ +string stripSpaces(string s) { + if (s.length == 0) return s; + ptrdiff_t startIdx = 0; + while (s[startIdx] == ' ' && startIdx < s.length) startIdx++; + s = s[startIdx .. $]; + ptrdiff_t endIdx = s.length - 1; + while (s[endIdx] == ' ' && endIdx >= 0) endIdx--; + return s[0 .. endIdx + 1]; +} + +/** + * Helper function to append an unsigned integer value to a char buffer. It is + * assumed that there's enough space to write the value. + * Params: + * value = The value to append. + * buffer = The buffer to append to. + * idx = A reference to a variable tracking the next writable index in the buffer. + */ +void writeUIntToBuffer(uint value, char[] buffer, ref size_t idx) { + const size_t startIdx = idx; + while (true) { + ubyte remainder = value % 10; + value /= 10; + buffer[idx++] = cast(char) ('0' + remainder); + if (value == 0) break; + } + // Swap the characters to proper order. + for (size_t i = 0; i < (idx - startIdx) / 2; i++) { + size_t p1 = i + startIdx; + size_t p2 = idx - i - 1; + char tmp = buffer[p1]; + buffer[p1] = buffer[p2]; + buffer[p2] = tmp; + } +} diff --git a/source/handy_http_transport/http1/transport.d b/source/handy_http_transport/http1/transport.d index 7e2e11f..b9f577d 100644 --- a/source/handy_http_transport/http1/transport.d +++ b/source/handy_http_transport/http1/transport.d @@ -3,6 +3,9 @@ module handy_http_transport.http1.transport; import std.socket; import handy_http_transport.interfaces; +import handy_http_transport.helpers; +import handy_http_transport.response_output_stream; + import handy_http_primitives; import streams; @@ -158,170 +161,6 @@ Either!(string[][string], "headers", StreamError, "error") parseHeaders(S)(S inp return Either!(string[][string], "headers", StreamError, "error")(headers); } -/** - * Helper function to consume string content from an input stream until a - * certain target pattern of characters is encountered. - * Params: - * inputStream = The stream to read from. - * target = The target at which to stop reading. - * Returns: The string that was read, or a stream error. - */ -private Either!(string, "value", StreamError, "error") consumeUntil(S)( - S inputStream, - string target -) if (isByteInputStream!S) { - ubyte[1024] buffer; - size_t idx; - while (true) { - auto result = inputStream.readFromStream(buffer[idx .. idx + 1]); - if (result.hasError) return Either!(string, "value", StreamError, "error")(result.error); - if (result.count != 1) return Either!(string, "value", StreamError, "error")( - StreamError("Failed to read a single element", 1) - ); - idx++; - if (idx >= target.length && buffer[idx - target.length .. idx] == target) { - return Either!(string, "value", StreamError, "error")( - cast(string) buffer[0 .. idx - target.length].idup - ); - } - if (idx >= buffer.length) { - return Either!(string, "value", StreamError, "error")( - StreamError("Couldn't find target \"" ~ target ~ "\" after reading 1024 bytes.", 1) - ); - } - } -} - -/** - * Internal helper function to get the first index of a character in a string. - * Params: - * s = The string to look in. - * c = The character to look for. - * offset = An optional offset to look from. - * Returns: The index of the character, or -1. - */ -private ptrdiff_t indexOf(string s, char c, size_t offset = 0) { - for (size_t i = offset; i < s.length; i++) { - if (s[i] == c) return i; - } - return -1; -} - -/** - * Internal helper function that returns the slice of a string excluding any - * preceding or trailing spaces. - * Params: - * s = The string to strip. - * Returns: The slice of the string that has been stripped. - */ -private string stripSpaces(string s) { - if (s.length == 0) return s; - ptrdiff_t startIdx = 0; - while (s[startIdx] == ' ' && startIdx < s.length) startIdx++; - s = s[startIdx .. $]; - ptrdiff_t endIdx = s.length - 1; - while (s[endIdx] == ' ' && endIdx >= 0) endIdx--; - return s[0 .. endIdx + 1]; -} - -/** - * Helper function to append an unsigned integer value to a char buffer. It is - * assumed that there's enough space to write the value. - * Params: - * value = The value to append. - * buffer = The buffer to append to. - * idx = A reference to a variable tracking the next writable index in the buffer. - */ -private void writeUIntToBuffer(uint value, char[] buffer, ref size_t idx) { - const size_t startIdx = idx; - while (true) { - ubyte remainder = value % 10; - value /= 10; - buffer[idx++] = cast(char) ('0' + remainder); - if (value == 0) break; - } - // Swap the characters to proper order. - for (size_t i = 0; i < (idx - startIdx) / 2; i++) { - size_t p1 = i + startIdx; - size_t p2 = idx - i - 1; - char tmp = buffer[p1]; - buffer[p1] = buffer[p2]; - buffer[p2] = tmp; - } -} - -/** - * A wrapper around a byte output stream that's used for writing HTTP response - * content. It keeps a reference to the `ServerHttpResponse` so that when a - * handler writes data to the stream, it'll flush the HTTP response status and - * headers beforehand. - */ -struct HttpResponseOutputStream(S) if (isByteOutputStream!S) { - /// The underlying output stream to write to. - private S outputStream; - /// A pointer to the HTTP response that this stream is for. - private ServerHttpResponse* response; - /// Flag that keeps track of if the HTTP status and headers were written. - private bool headersFlushed = false; - - this(S outputStream, ServerHttpResponse* response) { - this.outputStream = outputStream; - this.response = response; - } - - /** - * Writes the given data to the stream. If the referenced HTTP response's - * status and headers haven't yet been written, they will be written first. - * Params: - * buffer = The buffer containing data to write. - * Returns: The result of writing. If status and headers are written, the - * number of bytes written will include that in addition to the buffer size. - */ - StreamResult writeToStream(ubyte[] buffer) { - uint bytesWritten = 0; - if (!headersFlushed) { - auto result = writeHeaders(); - if (result.hasError) return result; - bytesWritten += result.count; - headersFlushed = true; - } - auto result = outputStream.writeToStream(buffer); - if (result.hasError) return result; - return StreamResult(result.count + bytesWritten); - } - - /** - * Writes HTTP/1.1 status line and headers to the underlying output stream, - * which is done before any body content can be written. - * Returns: The stream result of writing. - */ - StreamResult writeHeaders() { - // TODO: Come up with a better way of writing headers than string concatenation. - size_t idx = 0; - char[6] statusCodeBuffer; // Normal HTTP codes are 3 digits, but this leaves room for extensions. - writeUIntToBuffer(response.status.code, statusCodeBuffer, idx); - - string statusAndHeaders = "HTTP/1.1 " - ~ cast(string) statusCodeBuffer[0..idx] - ~ " " ~ response.status.text - ~ "\r\n"; - foreach (headerName; response.headers.keys) { - string headerLine = headerName ~ ": "; - string[] headerValues = response.headers.getAll(headerName); - for (size_t i = 0; i < headerValues.length; i++) { - headerLine ~= headerValues[i]; - if (i + 1 < headerValues.length) { - headerLine ~= ", "; - } - } - headerLine ~= "\r\n"; - statusAndHeaders ~= headerLine; - } - statusAndHeaders ~= "\r\n"; // Trailing CLRF before the body. - return outputStream.writeToStream(cast(ubyte[]) statusAndHeaders); - } -} - unittest { class TestHandler : HttpRequestHandler { void handle(ref ServerHttpRequest request, ref ServerHttpResponse response) { diff --git a/source/handy_http_transport/response_output_stream.d b/source/handy_http_transport/response_output_stream.d new file mode 100644 index 0000000..303d4b1 --- /dev/null +++ b/source/handy_http_transport/response_output_stream.d @@ -0,0 +1,77 @@ +module handy_http_transport.response_output_stream; + +import handy_http_transport.helpers : writeUIntToBuffer; +import handy_http_primitives : ServerHttpResponse; +import streams; + +/** + * A wrapper around a byte output stream that's used for writing HTTP response + * content. It keeps a reference to the `ServerHttpResponse` so that when a + * handler writes data to the stream, it'll flush the HTTP response status and + * headers beforehand. + */ +struct HttpResponseOutputStream(S) if (isByteOutputStream!S) { + /// The underlying output stream to write to. + private S outputStream; + /// A pointer to the HTTP response that this stream is for. + private ServerHttpResponse* response; + /// Flag that keeps track of if the HTTP status and headers were written. + private bool headersFlushed = false; + + this(S outputStream, ServerHttpResponse* response) { + this.outputStream = outputStream; + this.response = response; + } + + /** + * Writes the given data to the stream. If the referenced HTTP response's + * status and headers haven't yet been written, they will be written first. + * Params: + * buffer = The buffer containing data to write. + * Returns: The result of writing. If status and headers are written, the + * number of bytes written will include that in addition to the buffer size. + */ + StreamResult writeToStream(ubyte[] buffer) { + uint bytesWritten = 0; + if (!headersFlushed) { + auto result = writeHeaders(); + if (result.hasError) return result; + bytesWritten += result.count; + headersFlushed = true; + } + auto result = outputStream.writeToStream(buffer); + if (result.hasError) return result; + return StreamResult(result.count + bytesWritten); + } + + /** + * Writes HTTP/1.1 status line and headers to the underlying output stream, + * which is done before any body content can be written. + * Returns: The stream result of writing. + */ + StreamResult writeHeaders() { + // TODO: Come up with a better way of writing headers than string concatenation. + size_t idx = 0; + char[6] statusCodeBuffer; // Normal HTTP codes are 3 digits, but this leaves room for extensions. + writeUIntToBuffer(response.status.code, statusCodeBuffer, idx); + + string statusAndHeaders = "HTTP/1.1 " + ~ cast(string) statusCodeBuffer[0..idx] + ~ " " ~ response.status.text + ~ "\r\n"; + foreach (headerName; response.headers.keys) { + string headerLine = headerName ~ ": "; + string[] headerValues = response.headers.getAll(headerName); + for (size_t i = 0; i < headerValues.length; i++) { + headerLine ~= headerValues[i]; + if (i + 1 < headerValues.length) { + headerLine ~= ", "; + } + } + headerLine ~= "\r\n"; + statusAndHeaders ~= headerLine; + } + statusAndHeaders ~= "\r\n"; // Trailing CLRF before the body. + return outputStream.writeToStream(cast(ubyte[]) statusAndHeaders); + } +}