primitives/source/handy_http_primitives/request.d

210 lines
7.0 KiB
D
Raw Normal View History

2024-08-15 23:21:48 +00:00
module handy_http_primitives.request;
2024-09-17 21:08:15 +00:00
import streams : InputStream;
import std.traits : isSomeString, EnumMembers;
2024-08-15 23:21:48 +00:00
import handy_http_primitives.multivalue_map;
2024-09-17 21:08:15 +00:00
import handy_http_primitives.optional;
2024-08-15 23:21:48 +00:00
2024-09-17 21:08:15 +00:00
/**
* The HTTP request struct which represents the content of an HTTP request as
* received by a server.
*/
struct ServerHttpRequest {
/// The HTTP version of the request.
const HttpVersion httpVersion = HttpVersion.V1;
/// The remote address of the client that sent this request.
const InternetAddress clientAddress;
2024-09-17 21:08:15 +00:00
/// The HTTP verb used in the request.
const string method = HttpMethod.GET;
2024-09-17 21:08:15 +00:00
/// The URL that was requested.
const string url = "";
2024-09-17 21:08:15 +00:00
/// A case-insensitive map of all request headers.
2024-08-15 23:21:48 +00:00
const(CaseInsensitiveStringMultiValueMap) headers;
2024-09-17 21:08:15 +00:00
/// The underlying stream used to read the body from the request.
2024-08-15 23:21:48 +00:00
InputStream!ubyte inputStream;
}
2024-09-17 21:08:15 +00:00
/**
* Enumeration of all possible HTTP request versions.
2024-09-17 21:08:15 +00:00
*/
public enum HttpVersion : ubyte {
/// HTTP Version 1, including versions 0.9, 1.0, and 1.1.
V1 = 1 << 1,
/// HTTP Version 2.
2024-09-17 21:08:15 +00:00
V2 = 1 << 2,
/// HTTP Version 3.
2024-09-17 21:08:15 +00:00
V3 = 1 << 3
}
2024-08-15 23:21:48 +00:00
/**
* Enumeration of all possible HTTP methods, excluding extensions like WebDAV.
2024-08-15 23:21:48 +00:00
*
* https://developer.mozilla.org/en-US/docs/Web/HTTP/Methods
*/
public enum HttpMethod : string {
GET = "GET",
HEAD = "HEAD",
POST = "POST",
PUT = "PUT",
DELETE = "DELETE",
CONNECT = "CONNECT",
OPTIONS = "OPTIONS",
TRACE = "TRACE",
PATCH = "PATCH"
2024-08-15 23:21:48 +00:00
}
2024-09-17 21:08:15 +00:00
/**
* Attempts to parse an HttpMethod from a string.
* Params:
* s = The string to parse.
* Returns: An optional which may contain an HttpMethod, if one was parsed.
*/
Optional!HttpMethod parseHttpMethod(S)(S s) if (isSomeString!S) {
import std.uni : toUpper;
import std.string : strip;
static foreach (m; EnumMembers!HttpMethod) {
if (s == m) return Optional!HttpMethod.of(m);
2024-09-17 21:08:15 +00:00
}
const cleanStr = strip(toUpper(s));
static foreach (m; EnumMembers!HttpMethod) {
if (cleanStr == m) return Optional!HttpMethod.of(m);
2024-09-17 21:08:15 +00:00
}
return Optional!HttpMethod.empty;
}
unittest {
assert(parseHttpMethod("GET") == Optional!HttpMethod.of(HttpMethod.GET));
assert(parseHttpMethod("get") == Optional!HttpMethod.of(HttpMethod.GET));
assert(parseHttpMethod(" geT ") == Optional!HttpMethod.of(HttpMethod.GET));
assert(parseHttpMethod("PATCH") == Optional!HttpMethod.of(HttpMethod.PATCH));
assert(parseHttpMethod(" not a method!") == Optional!HttpMethod.empty);
assert(parseHttpMethod("") == Optional!HttpMethod.empty);
2024-09-17 21:08:15 +00:00
}
/// The data representing a remote IPv4 internet address, available as an int or bytes.
union IPv4InternetAddress {
const uint intValue;
const ubyte[4] bytes;
}
/// The data representing a remote IPv6 internet address.
struct IPv6InternetAddress {
const ubyte[16] bytes;
}
/// A remote internet address, which is either IPv4 or IPv6. Check `isIPv6`.
struct InternetAddress {
/// True if this address is IPv6. False if this is an IPv4 address.
const bool isIPv6;
/// The port number assigned to the connecting client on this machine.
const ushort port;
union {
IPv4InternetAddress ipv4Address;
IPv6InternetAddress ipv6Address;
}
}
/// Stores a single query parameter's key and values.
struct QueryParameter {
string key;
string[] values;
}
/**
* Parses a list of query parameters from a URL.
* Params:
* url = The URL to parse query parameters from.
* Returns: The list of query parameters.
*/
QueryParameter[] parseQueryParameters(string url) {
if (url is null || url.length == 0) {
return [];
}
ptrdiff_t paramsStartIdx = url.indexOf('?');
if (paramsStartIdx == -1 || paramsStartIdx + 1 >= url.length) return [];
string paramsStr = url[paramsStartIdx + 1 .. $];
QueryParameter[] params;
size_t idx = 0;
while (idx < paramsStr.length) {
// First, isolate the text up to the next '&' separator.
ptrdiff_t nextParamIdx = paramsStr.indexOf('&', idx);
size_t currentParamEndIdx = nextParamIdx == -1 ? paramsStr.length : nextParamIdx;
string currentParamStr = paramsStr[idx .. currentParamEndIdx];
// Then, look for an '=' to separate the parameter's key and value.
ptrdiff_t currentParamEqualsIdx = currentParamStr.indexOf('=');
string key;
string val;
if (currentParamEqualsIdx == -1) {
// No '=' is present, so we have a key with an empty value.
key = currentParamStr;
val = "";
} else if (currentParamEqualsIdx == 0) {
// The '=' is the first character, so the key is empty.
key = "";
val = currentParamStr[1 .. $];
} else {
// There is a legitimate key and value.
key = currentParamStr[0 .. currentParamEqualsIdx];
val = currentParamStr[currentParamEqualsIdx + 1 .. $];
}
// Clean up URI-encoded characters. TODO: do this without using std lib GC methods?
import std.uri : decodeComponent;
import std.string : replace;
key = key.replace("+", " ").decodeComponent();
val = val.replace("+", " ").decodeComponent();
// If the key already exists, insert the value into that array.
bool keyExists = false;
foreach (ref param; params) {
if (param.key == key) {
param.values ~= val;
keyExists = true;
break;
}
}
// Otherwise, add a new query parameter.
if (!keyExists) {
params ~= QueryParameter(key, [val]);
}
// Advance our current index pointer to the start of the next query parameter.
// (past the '&' character separating query parameters)
idx = currentParamEndIdx + 1;
}
return params;
}
unittest {
QueryParameter[] r;
// Test a basic common example.
r = parseQueryParameters("https://www.example.com?a=1&b=2&c=3");
assert(r == [QueryParameter("a", ["1"]), QueryParameter("b", ["2"]), QueryParameter("c", ["3"])]);
// Test parsing multiple values for a single key.
r = parseQueryParameters("test?key=a&key=b&key=abc");
assert(r == [QueryParameter("key", ["a", "b", "abc"])]);
// Test URLs without any parameters.
assert(parseQueryParameters("test").length == 0);
assert(parseQueryParameters("test?").length == 0);
// Test parameter with any values.
assert(parseQueryParameters("test?test") == [QueryParameter("test", [""])]);
// Test parameter without a name.
assert(parseQueryParameters("test?=value") == [QueryParameter("", ["value"])]);
}
/**
* 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;
}