mirror of
https://github.com/matrix-construct/construct
synced 2024-11-25 08:12:37 +01:00
1432 lines
34 KiB
C++
1432 lines
34 KiB
C++
// Matrix Construct
|
|
//
|
|
// Copyright (C) Matrix Construct Developers, Authors & Contributors
|
|
// Copyright (C) 2016-2018 Jason Volk <jason@zemos.net>
|
|
//
|
|
// Permission to use, copy, modify, and/or distribute this software for any
|
|
// purpose with or without fee is hereby granted, provided that the above
|
|
// copyright notice and this permission notice is present in all copies. The
|
|
// full license for this software is available in the LICENSE file.
|
|
|
|
namespace ircd::http
|
|
{
|
|
using reason_codes = std::unordered_map<http::code, string_view>;
|
|
using expectation_failure = spirit::qi::expectation_failure<const char *>;
|
|
|
|
[[clang::internal_linkage, clang::always_destroy]]
|
|
extern const reason_codes reason;
|
|
|
|
[[noreturn]]
|
|
static void throw_error(const expectation_failure &, const bool & = false);
|
|
}
|
|
|
|
BOOST_FUSION_ADAPT_STRUCT
|
|
(
|
|
ircd::http::query,
|
|
( decltype(ircd::http::query::first), first )
|
|
( decltype(ircd::http::query::second), second )
|
|
)
|
|
|
|
BOOST_FUSION_ADAPT_STRUCT
|
|
(
|
|
ircd::http::header,
|
|
( decltype(ircd::http::header::first), first )
|
|
( decltype(ircd::http::header::second), second )
|
|
)
|
|
|
|
BOOST_FUSION_ADAPT_STRUCT
|
|
(
|
|
ircd::http::line::response,
|
|
( decltype(ircd::http::line::response::version), version )
|
|
( decltype(ircd::http::line::response::status), status )
|
|
( decltype(ircd::http::line::response::reason), reason )
|
|
)
|
|
|
|
BOOST_FUSION_ADAPT_STRUCT
|
|
(
|
|
ircd::http::line::request,
|
|
( decltype(ircd::http::line::request::method), method )
|
|
( decltype(ircd::http::line::request::path), path )
|
|
( decltype(ircd::http::line::request::query), query )
|
|
( decltype(ircd::http::line::request::fragment), fragment )
|
|
( decltype(ircd::http::line::request::version), version )
|
|
)
|
|
|
|
decltype(ircd::http::reason)
|
|
ircd::http::reason
|
|
{
|
|
{ code::CONTINUE, "Continue" },
|
|
{ code::SWITCHING_PROTOCOLS, "Switching Protocols" },
|
|
{ code::PROCESSING, "Processing" },
|
|
{ code::EARLY_HINTS, "Early Hints" },
|
|
|
|
{ code::OK, "OK" },
|
|
{ code::CREATED, "Created" },
|
|
{ code::ACCEPTED, "Accepted" },
|
|
{ code::NON_AUTHORITATIVE_INFORMATION, "Non-Authoritative Information" },
|
|
{ code::NO_CONTENT, "No Content" },
|
|
{ code::PARTIAL_CONTENT, "Partial Content" },
|
|
|
|
{ code::MULTIPLE_CHOICES, "Multiple Choices" },
|
|
{ code::MOVED_PERMANENTLY, "Moved Permanently" },
|
|
{ code::FOUND, "Found" },
|
|
{ code::SEE_OTHER, "See Other" },
|
|
{ code::NOT_MODIFIED, "Not Modified" },
|
|
{ code::USE_PROXY, "Use Proxy" },
|
|
{ code::SWITCH_PROXY, "Switch Proxy" },
|
|
{ code::TEMPORARY_REDIRECT, "Temporary Redirect" },
|
|
{ code::PERMANENT_REDIRECT, "Permanent Redirect" },
|
|
|
|
{ code::BAD_REQUEST, "Bad Request" },
|
|
{ code::UNAUTHORIZED, "Unauthorized" },
|
|
{ code::FORBIDDEN, "Forbidden" },
|
|
{ code::NOT_FOUND, "Not Found" },
|
|
{ code::METHOD_NOT_ALLOWED, "Method Not Allowed" },
|
|
{ code::NOT_ACCEPTABLE, "Not Acceptable" },
|
|
{ code::REQUEST_TIMEOUT, "Request Time-out" },
|
|
{ code::CONFLICT, "Conflict" },
|
|
{ code::GONE, "Gone" },
|
|
{ code::LENGTH_REQUIRED, "Length Required" },
|
|
{ code::PAYLOAD_TOO_LARGE, "Payload Too Large" },
|
|
{ code::REQUEST_URI_TOO_LONG, "Request URI Too Long" },
|
|
{ code::UNSUPPORTED_MEDIA_TYPE, "Unsupported Media Type" },
|
|
{ code::RANGE_NOT_SATISFIABLE, "Range Not Satisfiable" },
|
|
{ code::EXPECTATION_FAILED, "Expectation Failed" },
|
|
{ code::IM_A_TEAPOT, "Negative, I Am A Meat Popsicle" },
|
|
{ code::UNPROCESSABLE_ENTITY, "Unprocessable Entity" },
|
|
{ code::PRECONDITION_REQUIRED, "Precondition Required" },
|
|
{ code::TOO_MANY_REQUESTS, "Too Many Requests" },
|
|
{ code::REQUEST_HEADER_FIELDS_TOO_LARGE, "Request Header Fields Too Large" },
|
|
|
|
{ code::INTERNAL_SERVER_ERROR, "Internal Server Error" },
|
|
{ code::NOT_IMPLEMENTED, "Not Implemented" },
|
|
{ code::BAD_GATEWAY, "Bad Gateway" },
|
|
{ code::SERVICE_UNAVAILABLE, "Service Unavailable" },
|
|
{ code::GATEWAY_TIMEOUT, "Gateway Timeout" },
|
|
{ code::HTTP_VERSION_NOT_SUPPORTED, "HTTP Version Not Supported" },
|
|
{ code::INSUFFICIENT_STORAGE, "Insufficient Storage" },
|
|
|
|
{ code::CLOUDFLARE_REFUSED, "Cloudflare Customer Connection Refused" },
|
|
{ code::CLOUDFLARE_TIMEDOUT, "Cloudflare Customer Connection Timed-out" },
|
|
{ code::CLOUDFLARE_UNREACHABLE, "Cloudflare Customer Unreachable" },
|
|
{ code::CLOUDFLARE_REQUEST_TIMEOUT, "Cloudflare Customer Request Time-out" },
|
|
};
|
|
|
|
namespace ircd::http::parser
|
|
{
|
|
using namespace ircd::spirit;
|
|
|
|
template<class R = unused_type,
|
|
class... S>
|
|
struct [[clang::internal_linkage]] rule
|
|
:qi::rule<const char *, R, S...>
|
|
{
|
|
using qi::rule<const char *, R, S...>::rule;
|
|
};
|
|
|
|
const expr NUL { lit('\0') ,"nul" };
|
|
|
|
// insignificant whitespaces
|
|
const expr SP { lit('\x20') ,"space" };
|
|
const expr HT { lit('\x09') ,"horizontal tab" };
|
|
const expr ws { SP | HT ,"whitespace" };
|
|
|
|
const expr CR { lit('\x0D') ,"carriage return" };
|
|
const expr LF { lit('\x0A') ,"line feed" };
|
|
const expr CRLF { CR >> LF ,"carriage return, line feed" };
|
|
|
|
const expr illegal { NUL | CR | LF ,"illegal" };
|
|
const expr colon { lit(':') ,"colon" };
|
|
const expr slash { lit('/') ,"forward solidus" };
|
|
const expr question { lit('?') ,"question mark" };
|
|
const expr pound { lit('#') ,"pound sign" };
|
|
const expr equal { lit('=') ,"equal sign" };
|
|
const expr ampersand { lit('&') ,"ampersand" };
|
|
|
|
const rule<string_view> token { raw[+(char_ - (illegal | ws))] ,"token" };
|
|
const rule<string_view> str { raw[+(char_ - illegal)] ,"string" };
|
|
const rule<string_view> line { *ws >> -str >> CRLF ,"line" };
|
|
|
|
const rule<string_view> status { raw[repeat(3)[char_('0','9')]] ,"status" };
|
|
const rule<string_view> reason { str ,"reason" };
|
|
|
|
const rule<string_view> head_key { raw[+(char_ - (illegal | ws | colon))] ,"head key" };
|
|
const rule<string_view> head_val { str ,"head value" };
|
|
const rule<http::header> header { head_key >> *ws >> colon >> *ws >> head_val ,"header" };
|
|
const rule<> headers { header % (*ws >> CRLF) ,"headers" };
|
|
|
|
const expr query_terminator { equal | question | ampersand | pound ,"query terminator" };
|
|
const expr query_illegal { illegal | ws | query_terminator ,"query illegal" };
|
|
const rule<string_view> query_key { raw[+(char_ - query_illegal)] ,"query key" };
|
|
const rule<string_view> query_val { raw[*(char_ - query_illegal)] ,"query value" };
|
|
|
|
const rule<string_view> method { token ,"method" };
|
|
const rule<string_view> path { raw[-slash >> *(char_ - query_illegal)] ,"path" };
|
|
const rule<string_view> fragment { pound >> -token ,"fragment" };
|
|
const rule<string_view> version { token ,"version" };
|
|
|
|
const rule<size_t> content_size { ulong_ ,"content length" };
|
|
const rule<uint32_t> chunk_size
|
|
{
|
|
qi::uint_parser<uint32_t, 16, 1, 8>{}
|
|
,"chunk size"
|
|
};
|
|
|
|
const rule<string_view> chunk_extensions
|
|
{
|
|
';' >> raw[str] //TODO: extensions
|
|
,"chunk extensions"
|
|
};
|
|
|
|
const rule<http::query> query
|
|
{
|
|
query_key >> -(equal >> query_val)
|
|
,"query"
|
|
};
|
|
|
|
const rule<string_view> query_string
|
|
{
|
|
question >> -raw[(query_key >> -(equal >> query_val)) % ampersand]
|
|
,"query string"
|
|
};
|
|
|
|
const rule<line::request> request_line
|
|
{
|
|
method >> +SP >> path >> -query_string >> -fragment >> +SP >> version
|
|
,"request line"
|
|
};
|
|
|
|
const rule<line::response> response_line
|
|
{
|
|
version >> +SP >> status >> -(+SP >> -reason)
|
|
,"response line"
|
|
};
|
|
|
|
const rule<> request
|
|
{
|
|
request_line >> *ws >> CRLF >> -headers >> CRLF
|
|
,"request"
|
|
};
|
|
|
|
const rule<> response
|
|
{
|
|
response_line >> *ws >> CRLF >> -headers >> CRLF
|
|
,"response"
|
|
};
|
|
|
|
static size_t content_length(const string_view &val);
|
|
}
|
|
|
|
/// Compose a request. This prints an HTTP head into the buffer. No real IO is
|
|
/// done here. After composing into the buffer, the user can then drive the
|
|
/// socket by sending the header and the content as specified.
|
|
///
|
|
/// If termination is false, no extra CRLF is printed to the buffer allowing
|
|
/// additional headers not specified to be appended later.
|
|
ircd::http::request::request(window_buffer &out,
|
|
const string_view &host,
|
|
const string_view &method,
|
|
const string_view &uri,
|
|
const size_t &content_length,
|
|
const string_view &content_type,
|
|
const vector_view<const header> &headers,
|
|
const bool &termination)
|
|
{
|
|
writeline(out, [&method, &uri](const mutable_buffer &out) -> size_t
|
|
{
|
|
assert(!method.empty());
|
|
assert(!uri.empty());
|
|
return fmt::sprintf
|
|
{
|
|
out, "%s %s HTTP/1.1", method, uri
|
|
};
|
|
});
|
|
|
|
if(!has(headers, "host"))
|
|
writeline(out, [&host](const mutable_buffer &out) -> size_t
|
|
{
|
|
assert(!host.empty());
|
|
return fmt::sprintf
|
|
{
|
|
out, "Host: %s", host
|
|
};
|
|
});
|
|
|
|
if(content_length && !has(headers, "content-type"))
|
|
writeline(out, [&content_type](const mutable_buffer &out) -> size_t
|
|
{
|
|
return fmt::sprintf
|
|
{
|
|
out, "Content-Type: %s", content_type?: "text/plain; charset=utf-8"
|
|
};
|
|
});
|
|
|
|
if(!has(headers, "content-length"))
|
|
writeline(out, [&content_length](const mutable_buffer &out) -> size_t
|
|
{
|
|
return fmt::sprintf
|
|
{
|
|
out, "Content-Length: %zu", content_length
|
|
};
|
|
});
|
|
|
|
// Note: pass your own user-agent with an empty value to mute this header.
|
|
if(!has(headers, "user-agent") && info::user_agent)
|
|
writeline(out, [](const mutable_buffer &out) -> size_t
|
|
{
|
|
return fmt::sprintf
|
|
{
|
|
out, "User-Agent: %s", info::user_agent
|
|
};
|
|
});
|
|
|
|
write(out, headers);
|
|
|
|
if(termination)
|
|
writeline(out);
|
|
}
|
|
|
|
namespace ircd::http
|
|
{
|
|
static void assign(request::head &, const header &);
|
|
}
|
|
|
|
ircd::http::request::head::head(parse::capstan &pc,
|
|
const headers::closure &closure)
|
|
:line::request{pc}
|
|
,uri
|
|
{
|
|
fragment? string_view { begin(path), end(fragment) }:
|
|
query? string_view { begin(path), end(query) }:
|
|
string_view { begin(path), end(path) }
|
|
}
|
|
,headers{[this, &pc, &closure]
|
|
{
|
|
const bool version_compatible
|
|
{
|
|
this->version == "HTTP/1.0" ||
|
|
this->version == "HTTP/1.1"
|
|
};
|
|
|
|
if(unlikely(!version_compatible))
|
|
throw error
|
|
{
|
|
HTTP_VERSION_NOT_SUPPORTED, fmt::snstringf
|
|
{
|
|
128, "Sorry, no speak '%s'. Try HTTP/1.1 here.",
|
|
this->version
|
|
}
|
|
};
|
|
|
|
return http::headers
|
|
{
|
|
pc, [this, &closure](const auto &header)
|
|
{
|
|
assign(*this, header);
|
|
if(likely(closure))
|
|
closure(header);
|
|
}
|
|
};
|
|
}()}
|
|
{
|
|
}
|
|
|
|
ircd::http::request::head::operator
|
|
string_view()
|
|
const
|
|
{
|
|
const string_view request_line
|
|
{
|
|
static_cast<const line::request &>(*this)
|
|
};
|
|
|
|
if(request_line.empty() || headers.empty())
|
|
return request_line;
|
|
|
|
return string_view
|
|
{
|
|
request_line.begin(), headers.end()
|
|
};
|
|
}
|
|
|
|
void
|
|
ircd::http::assign(request::head &head,
|
|
const header &header)
|
|
{
|
|
char buf[64];
|
|
const auto &[key_, val] {header};
|
|
assert(size(key_) <= sizeof(buf));
|
|
const auto &key
|
|
{
|
|
tolower(buf, key_)
|
|
};
|
|
|
|
if(key == "content-length"_sv)
|
|
head.content_length = parser::content_length(val);
|
|
|
|
else if(key == "host"_sv)
|
|
head.host = val;
|
|
|
|
else if(key == "expect"_sv)
|
|
head.expect = val;
|
|
|
|
else if(key == "te"_sv)
|
|
head.te = val;
|
|
|
|
else if(key == "authorization"_sv)
|
|
head.authorization = val;
|
|
|
|
else if(key == "connection"_sv)
|
|
head.connection = val;
|
|
|
|
else if(key == "content-type"_sv)
|
|
head.content_type = val;
|
|
|
|
else if(key == "user-agent"_sv)
|
|
head.user_agent = val;
|
|
|
|
else if(key == "upgrade"_sv)
|
|
head.upgrade = val;
|
|
|
|
else if(key == "range"_sv)
|
|
head.range = val;
|
|
|
|
else if(key == "if-range"_sv)
|
|
head.if_range = val;
|
|
|
|
else if(key == "forwarded"_sv)
|
|
head.forwarded[0] = val;
|
|
|
|
else if(key == "x-forwarded-for"_sv)
|
|
head.forwarded_for[0] = val;
|
|
|
|
else if(key == "x-forwarded-host"_sv)
|
|
head.forwarded_host[0] = val;
|
|
}
|
|
|
|
ircd::http::response::response(window_buffer &out,
|
|
const code &code,
|
|
const size_t &content_length,
|
|
const string_view &content_type,
|
|
const http::headers &headers_s,
|
|
const vector_view<const header> &headers_v,
|
|
const bool &termination)
|
|
{
|
|
const auto has_header{[&headers_s, &headers_v]
|
|
(const string_view &key) -> bool
|
|
{
|
|
return has(headers_v, key) || has(headers_s, key);
|
|
}};
|
|
|
|
writeline(out, [&code](const mutable_buffer &out) -> size_t
|
|
{
|
|
return fmt::sprintf
|
|
{
|
|
out, "HTTP/1.1 %u %s", uint(code), status(code)
|
|
};
|
|
});
|
|
|
|
const bool write_server_header
|
|
{
|
|
code >= 200 && code < 300
|
|
&& !has_header("server")
|
|
};
|
|
|
|
if(write_server_header)
|
|
writeline(out, [&code](const mutable_buffer &out) -> size_t
|
|
{
|
|
size_t ret{0};
|
|
ret += copy(out, "Server: "_sv);
|
|
ret += copy(out + ret, ircd::info::server_agent);
|
|
return ret;
|
|
});
|
|
|
|
const bool write_date_header
|
|
{
|
|
code < 400
|
|
&& !has_header("date")
|
|
};
|
|
|
|
if(write_date_header)
|
|
writeline(out, [](const mutable_buffer &out) -> size_t
|
|
{
|
|
char date_buf[96];
|
|
return fmt::sprintf
|
|
{
|
|
out, "Date: %s", timef(date_buf, ircd::localtime)
|
|
};
|
|
});
|
|
|
|
const bool write_content_type_header
|
|
{
|
|
code != NO_CONTENT
|
|
&& content_type && content_length
|
|
&& !has_header("content-type")
|
|
};
|
|
|
|
if(write_content_type_header)
|
|
writeline(out, [&content_type](const mutable_buffer &out) -> size_t
|
|
{
|
|
return fmt::sprintf
|
|
{
|
|
out, "Content-Type: %s", content_type?: "text/plain; charset=utf-8"
|
|
};
|
|
});
|
|
|
|
const bool write_content_length_header
|
|
{
|
|
code != NO_CONTENT
|
|
&& content_length != size_t(-1) // chunked encoding indication
|
|
&& !has_header("content-length")
|
|
};
|
|
|
|
if(write_content_length_header)
|
|
writeline(out, [&content_length](const mutable_buffer &out) -> size_t
|
|
{
|
|
return fmt::sprintf
|
|
{
|
|
out, "Content-Length: %zu", content_length
|
|
};
|
|
});
|
|
|
|
const bool write_transfer_encoding_chunked
|
|
{
|
|
content_length == size_t(-1)
|
|
&& !has_header("transfer-encoding")
|
|
};
|
|
|
|
if(write_transfer_encoding_chunked)
|
|
writeline(out, [&content_length](const mutable_buffer &out) -> size_t
|
|
{
|
|
return copy(out, "Transfer-Encoding: chunked"_sv);
|
|
});
|
|
|
|
if(!headers_s.empty())
|
|
out([&headers_s](const mutable_buffer &out)
|
|
{
|
|
assert(endswith(headers_s, "\r\n"));
|
|
return copy(out, headers_s);
|
|
});
|
|
|
|
if(!headers_v.empty())
|
|
write(out, headers_v);
|
|
|
|
if(termination)
|
|
writeline(out);
|
|
}
|
|
|
|
namespace ircd::http
|
|
{
|
|
static void assign(response::head &, const header &);
|
|
}
|
|
|
|
ircd::http::response::head::head(parse::capstan &pc)
|
|
try
|
|
:line::response{pc}
|
|
,headers
|
|
{
|
|
pc, [this](const auto &header)
|
|
{
|
|
assign(*this, header);
|
|
}
|
|
}
|
|
{
|
|
}
|
|
catch(const expectation_failure &e)
|
|
{
|
|
throw_error(e, true);
|
|
}
|
|
|
|
ircd::http::response::head::head(parse::capstan &pc,
|
|
const headers::closure &closure)
|
|
try
|
|
:line::response{pc}
|
|
,headers
|
|
{
|
|
pc, [this, &closure](const auto &header)
|
|
{
|
|
assign(*this, header);
|
|
closure(header);
|
|
}
|
|
}
|
|
{
|
|
}
|
|
catch(const expectation_failure &e)
|
|
{
|
|
throw_error(e, true);
|
|
}
|
|
|
|
void
|
|
ircd::http::assign(response::head &head,
|
|
const header &header)
|
|
{
|
|
char buf[96];
|
|
const auto &[key_, val] {header};
|
|
assert(size(key_) <= sizeof(buf));
|
|
if(unlikely(size(key_) > sizeof(buf)))
|
|
throw error
|
|
{
|
|
REQUEST_HEADER_FIELDS_TOO_LARGE
|
|
};
|
|
|
|
const auto &key
|
|
{
|
|
tolower(buf, key_)
|
|
};
|
|
|
|
if(key == "content-length"_sv)
|
|
head.content_length = parser::content_length(val);
|
|
|
|
else if(key == "content-type"_sv)
|
|
head.content_type = val;
|
|
|
|
else if(key == "content-range"_sv)
|
|
head.content_range = val;
|
|
|
|
else if(key == "accept-range"_sv)
|
|
head.content_range = val;
|
|
|
|
else if(key == "transfer-encoding"_sv)
|
|
head.transfer_encoding = val;
|
|
|
|
else if(key == "server"_sv)
|
|
head.server = val;
|
|
|
|
else if(key == "location"_sv)
|
|
head.location = val;
|
|
}
|
|
|
|
namespace ircd::http::parser
|
|
{
|
|
extern const rule<size_t> parse_chunk_head;
|
|
}
|
|
|
|
decltype(ircd::http::parser::parse_chunk_head)
|
|
ircd::http::parser::parse_chunk_head
|
|
{
|
|
eoi | (eps > (chunk_size >> -chunk_extensions))
|
|
,"chunk head"
|
|
};
|
|
|
|
ircd::http::response::chunk::chunk(parse::capstan &pc)
|
|
try
|
|
:line{pc}
|
|
{
|
|
const char *start(line::begin()), *const stop(line::end());
|
|
const auto res
|
|
{
|
|
ircd::parse(start, stop, parser::parse_chunk_head, this->size)
|
|
};
|
|
|
|
assert(res == true);
|
|
}
|
|
catch(const expectation_failure &e)
|
|
{
|
|
throw_error(e, true);
|
|
}
|
|
|
|
//
|
|
// headers
|
|
//
|
|
|
|
bool
|
|
ircd::http::has(const vector_view<const header> &headers,
|
|
const string_view &key)
|
|
{
|
|
return end(headers) != std::find_if(begin(headers), end(headers), [&key]
|
|
(const header &header)
|
|
{
|
|
return header == key;
|
|
});
|
|
}
|
|
|
|
bool
|
|
ircd::http::has(const headers &headers,
|
|
const string_view &key)
|
|
{
|
|
return headers.has(key);
|
|
}
|
|
|
|
//
|
|
// headers::headers
|
|
//
|
|
|
|
decltype(ircd::http::headers::terminator)
|
|
ircd::http::headers::terminator
|
|
{
|
|
"\r\n\r\n"
|
|
};
|
|
|
|
ircd::http::headers::headers(parse::capstan &pc,
|
|
closure c)
|
|
:string_view{[&pc, &c]
|
|
() -> string_view
|
|
{
|
|
header h{pc};
|
|
const char *const &started{h.first.data()}, *stopped{started};
|
|
for(; !h.first.empty(); stopped = pc.parsed, h = header{pc})
|
|
if(c && !c(h))
|
|
c = {};
|
|
|
|
return { started, stopped };
|
|
}()}
|
|
{
|
|
}
|
|
|
|
bool
|
|
ircd::http::headers::has(const string_view &key)
|
|
const
|
|
{
|
|
// has header if break early from for_each
|
|
return !for_each([&key]
|
|
(const header &header)
|
|
{
|
|
// true to continue; false to break (for_each protocol)
|
|
return iequals(header.first, key)? false : true;
|
|
});
|
|
}
|
|
|
|
ircd::string_view
|
|
ircd::http::headers::at(const string_view &key)
|
|
const
|
|
{
|
|
const string_view ret
|
|
{
|
|
this->operator[](key)
|
|
};
|
|
|
|
if(unlikely(!ret))
|
|
throw std::out_of_range{key};
|
|
|
|
return ret;
|
|
}
|
|
|
|
ircd::string_view
|
|
ircd::http::headers::operator[](const string_view &key)
|
|
const
|
|
{
|
|
string_view ret;
|
|
for_each([&key, &ret](const auto &header)
|
|
{
|
|
if(iequals(header.first, key))
|
|
{
|
|
ret = header.second;
|
|
return false;
|
|
}
|
|
else return true;
|
|
});
|
|
|
|
return ret;
|
|
}
|
|
|
|
bool
|
|
ircd::http::headers::for_each(const closure &closure)
|
|
const
|
|
{
|
|
if(empty())
|
|
return true;
|
|
|
|
parse::buffer pb{const_buffer{*this}};
|
|
parse::capstan pc{pb};
|
|
header h{pc};
|
|
for(; !h.first.empty(); h = header{pc})
|
|
if(!closure(h))
|
|
return false;
|
|
|
|
return true;
|
|
}
|
|
|
|
//
|
|
// header
|
|
//
|
|
|
|
namespace ircd::http::parser
|
|
{
|
|
extern const rule<http::header> parse_header;
|
|
extern const rule<http::line::request> parse_request;
|
|
extern const rule<http::line::response> parse_response;
|
|
}
|
|
|
|
decltype(ircd::http::parser::parse_header)
|
|
ircd::http::parser::parse_header
|
|
{
|
|
eoi | expect[header]
|
|
,"header"
|
|
};
|
|
|
|
decltype(ircd::http::parser::parse_request)
|
|
ircd::http::parser::parse_request
|
|
{
|
|
expect[request_line]
|
|
,"request head line"
|
|
};
|
|
|
|
decltype(ircd::http::parser::parse_response)
|
|
ircd::http::parser::parse_response
|
|
{
|
|
expect[response_line]
|
|
,"response head line"
|
|
};
|
|
|
|
ircd::http::header::header(const line &line)
|
|
try
|
|
{
|
|
const char
|
|
*start(line.data()),
|
|
*const stop(line.data() + line.size());
|
|
const auto ok
|
|
{
|
|
ircd::parse(start, stop, parser::parse_header, *this)
|
|
};
|
|
|
|
assert(ok);
|
|
assert(start == stop);
|
|
}
|
|
catch(const expectation_failure &e)
|
|
{
|
|
throw_error(e);
|
|
}
|
|
|
|
ircd::http::line::response::response(const line &line)
|
|
try
|
|
{
|
|
const char
|
|
*start(line.data()),
|
|
*const stop(line.data() + line.size());
|
|
const auto ok
|
|
{
|
|
ircd::parse(start, stop, parser::parse_response, *this)
|
|
};
|
|
|
|
assert(ok);
|
|
assert(start == stop);
|
|
}
|
|
catch(const expectation_failure &e)
|
|
{
|
|
throw_error(e, true);
|
|
}
|
|
|
|
ircd::http::line::request::request(const line &line)
|
|
try
|
|
{
|
|
const char
|
|
*start(line.data()),
|
|
*const stop(line.data() + line.size());
|
|
const auto ok
|
|
{
|
|
ircd::parse(start, stop, parser::parse_request, *this)
|
|
};
|
|
|
|
assert(ok);
|
|
assert(start == stop);
|
|
}
|
|
catch(const expectation_failure &e)
|
|
{
|
|
throw_error(e);
|
|
}
|
|
|
|
ircd::http::line::request::operator
|
|
string_view()
|
|
const
|
|
{
|
|
if(method.empty())
|
|
return string_view{};
|
|
|
|
assert(!version.empty());
|
|
assert(method.begin() < version.end());
|
|
return string_view
|
|
{
|
|
method.begin(), version.end()
|
|
};
|
|
}
|
|
|
|
//
|
|
// http::line
|
|
//
|
|
|
|
namespace ircd::http::parser
|
|
{
|
|
extern const rule<string_view> parse_line;
|
|
}
|
|
|
|
decltype(ircd::http::parser::parse_line)
|
|
ircd::http::parser::parse_line
|
|
{
|
|
expect[line]
|
|
,"line"
|
|
};
|
|
|
|
decltype(ircd::http::line::terminator)
|
|
ircd::http::line::terminator
|
|
{
|
|
"\r\n"
|
|
};
|
|
|
|
ircd::http::line::line(parse::capstan &pc)
|
|
{
|
|
auto &ret(static_cast<string_view &>(*this));
|
|
pc([&ret](const char *&start, const char *const &stop)
|
|
{
|
|
if(start == stop)
|
|
return false;
|
|
|
|
const bool ok
|
|
{
|
|
ircd::parse(start, stop, parser::parse_line, ret)
|
|
};
|
|
|
|
assert(ok);
|
|
return ok;
|
|
});
|
|
}
|
|
|
|
//
|
|
// query::string
|
|
//
|
|
|
|
bool
|
|
ircd::http::query::string::has(const string_view &key)
|
|
const
|
|
{
|
|
bool ret{false};
|
|
for_each(key, [&ret]
|
|
(const auto &)
|
|
noexcept
|
|
{
|
|
ret = true;
|
|
return false;
|
|
});
|
|
|
|
return ret;
|
|
}
|
|
|
|
size_t
|
|
ircd::http::query::string::count(const string_view &key)
|
|
const
|
|
{
|
|
size_t ret{0};
|
|
for_each(key, [&ret]
|
|
(const auto &)
|
|
noexcept
|
|
{
|
|
++ret;
|
|
return true;
|
|
});
|
|
|
|
return ret;
|
|
}
|
|
|
|
ircd::vector_view<ircd::string_view>
|
|
ircd::http::query::string::array(const mutable_buffer &buf,
|
|
const string_view &key,
|
|
string_view *const &out,
|
|
const size_t &max)
|
|
const
|
|
{
|
|
if(unlikely(!max))
|
|
return {};
|
|
|
|
size_t ret(0);
|
|
window_buffer window(buf);
|
|
for_each(key, [&out, &max, &ret, &window]
|
|
(const auto &query)
|
|
{
|
|
window([&out, &max, &ret, &query]
|
|
(const mutable_buffer &buf)
|
|
{
|
|
assert(ret < max);
|
|
const auto &[_, val] {query};
|
|
out[ret] = url::decode(buf, val);
|
|
return out[ret];
|
|
});
|
|
|
|
return ++ret < max;
|
|
});
|
|
|
|
return vector_view<string_view>
|
|
{
|
|
out, ret
|
|
};
|
|
}
|
|
|
|
|
|
ircd::string_view
|
|
ircd::http::query::string::at(const string_view &key,
|
|
const size_t &idx)
|
|
const
|
|
{
|
|
const string_view &ret
|
|
{
|
|
_get(key, idx)
|
|
};
|
|
|
|
if(unlikely(!ret))
|
|
{
|
|
char buf[1024];
|
|
const string_view msg
|
|
{
|
|
fmt::sprintf
|
|
{
|
|
buf, "Failed to find value for required query string key '%s' #%zu.",
|
|
key,
|
|
idx
|
|
}
|
|
};
|
|
|
|
throw std::out_of_range
|
|
{
|
|
msg.c_str() // fmt::sprintf() will null terminate msg
|
|
};
|
|
}
|
|
|
|
return ret;
|
|
}
|
|
|
|
ircd::string_view
|
|
ircd::http::query::string::operator[](const string_view &key)
|
|
const
|
|
{
|
|
return _get(key, 0);
|
|
}
|
|
|
|
ircd::string_view
|
|
ircd::http::query::string::_get(const string_view &key,
|
|
size_t idx)
|
|
const
|
|
{
|
|
string_view ret;
|
|
for_each(key, [&idx, &ret]
|
|
(const auto &query)
|
|
noexcept
|
|
{
|
|
if(!idx--)
|
|
{
|
|
ret = query.second;
|
|
return false;
|
|
}
|
|
else return true;
|
|
});
|
|
|
|
return ret;
|
|
}
|
|
|
|
bool
|
|
ircd::http::query::string::for_each(const string_view &key,
|
|
const closure &closure)
|
|
const
|
|
{
|
|
return for_each([&key, &closure]
|
|
(const auto &query)
|
|
{
|
|
if(query.first != key)
|
|
return true;
|
|
|
|
return closure(query);
|
|
});
|
|
}
|
|
|
|
bool
|
|
ircd::http::query::string::for_each(const closure &view)
|
|
const
|
|
{
|
|
bool ret{true};
|
|
const auto action{[&view, &ret]
|
|
(const auto &attribute, const auto &context, auto &continue_)
|
|
{
|
|
ret = view(attribute);
|
|
continue_ = ret;
|
|
}};
|
|
|
|
const parser::rule<> grammar
|
|
{
|
|
-parser::question >> (parser::query[action] % parser::ampersand)
|
|
,"query strings"
|
|
};
|
|
|
|
const string_view &s(*this);
|
|
const char *start(s.begin()), *const stop(s.end());
|
|
ircd::parse(start, stop, grammar);
|
|
return ret;
|
|
}
|
|
|
|
//
|
|
// parser util
|
|
//
|
|
|
|
size_t
|
|
ircd::http::parser::content_length(const string_view &str)
|
|
{
|
|
const char
|
|
*start(str.data()),
|
|
*const stop(start + str.size());
|
|
|
|
size_t ret;
|
|
const bool parsed
|
|
{
|
|
ircd::parse(start, stop, parser::content_size, ret)
|
|
};
|
|
|
|
if(!parsed || ret >= 256_GiB)
|
|
throw error
|
|
{
|
|
BAD_REQUEST, "Invalid content-length value"
|
|
};
|
|
|
|
return ret;
|
|
}
|
|
|
|
//
|
|
// util
|
|
//
|
|
|
|
ircd::const_buffer
|
|
ircd::http::writechunk(const mutable_buffer &buf,
|
|
const uint32_t &chunk_size)
|
|
{
|
|
window_buffer window{buf};
|
|
writechunk(window, chunk_size);
|
|
return window.completed();
|
|
}
|
|
|
|
void
|
|
ircd::http::writechunk(window_buffer &buf,
|
|
const uint32_t &chunk_size)
|
|
{
|
|
writeline(buf, [&chunk_size]
|
|
(const mutable_buffer &out) -> size_t
|
|
{
|
|
assert(size(out) >= (8 + 1));
|
|
return ::snprintf(data(out), size(out), "%08x", chunk_size);
|
|
});
|
|
}
|
|
|
|
std::string
|
|
ircd::http::strung(const vector_view<const header> &headers)
|
|
{
|
|
return ircd::string(serialized(headers), [&]
|
|
(window_buffer out)
|
|
{
|
|
write(out, headers);
|
|
return out.consumed();
|
|
});
|
|
}
|
|
|
|
/// Indicates the buffer size required to write these headers. This size
|
|
/// may include room for a terminating null character which may be written
|
|
/// by write(headers). Only use write(headers) to know the actually written
|
|
/// string size (without null) not this.
|
|
size_t
|
|
ircd::http::serialized(const vector_view<const header> &headers)
|
|
{
|
|
// Because the write(header) functions use fmt::sprintf we have to
|
|
// indicate an extra space for a null string terminator to not overlof
|
|
const size_t initial{!headers.empty()};
|
|
return std::accumulate(std::begin(headers), std::end(headers), initial, []
|
|
(auto &&ret, const auto &pair)
|
|
{
|
|
// key : SP value CRLF
|
|
return ret += pair.first.size() + 1 + 1 + pair.second.size() + 2;
|
|
});
|
|
}
|
|
|
|
void
|
|
ircd::http::write(window_buffer &out,
|
|
const vector_view<const header> &headers)
|
|
{
|
|
for(const auto &header : headers)
|
|
write(out, header);
|
|
}
|
|
|
|
void
|
|
ircd::http::write(window_buffer &out,
|
|
const header &header)
|
|
{
|
|
if(header.second.empty())
|
|
return;
|
|
|
|
assert(!header.first.empty());
|
|
if(unlikely(header.first.empty()))
|
|
return;
|
|
|
|
writeline(out, [&header](const mutable_buffer &out) -> size_t
|
|
{
|
|
return fmt::sprintf
|
|
{
|
|
out, "%s: %s", header.first, header.second
|
|
};
|
|
});
|
|
}
|
|
|
|
/// Close over the user's closure to append a newline.
|
|
void
|
|
ircd::http::writeline(window_buffer &write,
|
|
const window_buffer::closure &closure)
|
|
{
|
|
// A new window_buffer is implicit constructed out of the mutable_buffer
|
|
// otherwise presented to this closure as its write window.
|
|
write([&closure](window_buffer write)
|
|
{
|
|
const auto newline{[](const mutable_buffer &out)
|
|
{
|
|
return copy(out, line::terminator);
|
|
}};
|
|
|
|
write(closure);
|
|
write(newline);
|
|
return write.consumed();
|
|
});
|
|
}
|
|
|
|
void
|
|
ircd::http::writeline(window_buffer &write)
|
|
{
|
|
writeline(write, []
|
|
(const mutable_buffer &out)
|
|
noexcept
|
|
{
|
|
return 0;
|
|
});
|
|
}
|
|
|
|
/// Called to translate a grammar exception into an http::error within our
|
|
/// system. This will then usually propagate back to our client.
|
|
///
|
|
/// If we are a client to another server, set internal=true. Even though this
|
|
/// still generates an HTTP error, the code is 500 so if it propagates back to
|
|
/// a client it does not indicate to *that* client that *they* made a bad
|
|
/// request from a 400 back to them.
|
|
void
|
|
ircd::http::throw_error(const expectation_failure &e,
|
|
const bool &internal)
|
|
{
|
|
const auto &code_
|
|
{
|
|
internal?
|
|
code::INTERNAL_SERVER_ERROR:
|
|
code::BAD_REQUEST
|
|
};
|
|
|
|
const char *const &fmtstr
|
|
{
|
|
internal?
|
|
"I expected a valid HTTP %s. Server sent %zu invalid characters starting with `%s'.":
|
|
"I require a valid HTTP %s. You sent %zu invalid characters starting with `%s'."
|
|
};
|
|
|
|
const auto &rule
|
|
{
|
|
ircd::string(e.what_)
|
|
};
|
|
|
|
throw error
|
|
{
|
|
code_, fmt::snstringf
|
|
{
|
|
512, fmtstr,
|
|
between(rule, "<", ">"),
|
|
size_t(e.last - e.first),
|
|
string_view{e.first, e.last}
|
|
}
|
|
};
|
|
}
|
|
|
|
//
|
|
// error
|
|
//
|
|
|
|
ircd::http::error::error(const http::code &code,
|
|
std::string content,
|
|
const vector_view<const header> &headers)
|
|
:http::error
|
|
{
|
|
code, std::move(content), strung(headers)
|
|
}
|
|
{
|
|
}
|
|
|
|
ircd::http::error::error(const http::code &code,
|
|
std::string content,
|
|
std::string headers)
|
|
:ircd::error
|
|
{
|
|
generate_skip
|
|
}
|
|
,content
|
|
{
|
|
std::move(content)
|
|
}
|
|
,headers
|
|
{
|
|
std::move(headers)
|
|
}
|
|
,code{code}
|
|
{
|
|
auto &buf(ircd::error::buf);
|
|
const auto msg(status(code));
|
|
::snprintf
|
|
(
|
|
buf, sizeof(buf),
|
|
"%u %s",
|
|
uint(code),
|
|
msg? msg.c_str(): ""
|
|
);
|
|
}
|
|
|
|
// Out-of-line placement.
|
|
ircd::http::error::~error()
|
|
noexcept
|
|
{
|
|
}
|
|
|
|
//
|
|
// status
|
|
//
|
|
|
|
namespace ircd::http::parser
|
|
{
|
|
extern const rule<uint8_t> status_codepoint;
|
|
extern const rule<enum http::category> status_category;
|
|
extern const rule<enum http::code> status_code;
|
|
}
|
|
|
|
decltype(ircd::http::parser::status_codepoint)
|
|
ircd::http::parser::status_codepoint
|
|
{
|
|
qi::uint_parser<uint8_t, 10, 1, 1>{}
|
|
,"status codepoint"
|
|
};
|
|
|
|
decltype(ircd::http::parser::status_category)
|
|
ircd::http::parser::status_category
|
|
{
|
|
&char_('1','9')
|
|
>> status_codepoint
|
|
>> omit[repeat(2)[char_('0','9')] >> (eoi | ws)]
|
|
,"status category"
|
|
};
|
|
|
|
decltype(ircd::http::parser::status_code)
|
|
ircd::http::parser::status_code
|
|
{
|
|
qi::uint_parser<uint16_t, 10, 3, 3>{}
|
|
>> omit[eoi | ws]
|
|
,"status code"
|
|
};
|
|
|
|
enum ircd::http::code
|
|
ircd::http::status(const string_view &str)
|
|
{
|
|
short ret;
|
|
const char *start(begin(str)), *const stop(end(str));
|
|
const bool parsed
|
|
{
|
|
ircd::parse(start, stop, parser::status_code, ret)
|
|
};
|
|
|
|
if(!parsed)
|
|
throw ircd::error
|
|
{
|
|
"Invalid HTTP status code"
|
|
};
|
|
|
|
assert(ret >= 0 && ret < 1000);
|
|
return http::code(ret);
|
|
}
|
|
|
|
ircd::string_view
|
|
ircd::http::status(const enum code &code)
|
|
noexcept try
|
|
{
|
|
return reason.at(code);
|
|
}
|
|
catch(const std::out_of_range &e)
|
|
{
|
|
log::dwarning
|
|
{
|
|
"No reason string for HTTP status code %u", uint(code)
|
|
};
|
|
|
|
return ""_sv;
|
|
}
|
|
|
|
enum ircd::log::level
|
|
ircd::http::severity(const enum category &category)
|
|
noexcept
|
|
{
|
|
switch(category)
|
|
{
|
|
case http::category::NONE: return log::level::DERROR;
|
|
case http::category::SUCCESS: return log::level::DEBUG;
|
|
case http::category::REDIRECT: return log::level::DWARNING;
|
|
case http::category::ERROR: return log::level::DERROR;
|
|
case http::category::SERVER: return log::level::DERROR;
|
|
case http::category::INTERNAL: return log::level::ERROR;
|
|
default:
|
|
case http::category::UNKNOWN: return log::level::DWARNING;
|
|
}
|
|
}
|
|
|
|
enum ircd::http::category
|
|
ircd::http::category(const string_view &str)
|
|
noexcept
|
|
{
|
|
enum category ret;
|
|
const char *start(begin(str)), *const stop(end(str));
|
|
const bool parsed
|
|
{
|
|
ircd::parse(start, stop, parser::status_category, ret)
|
|
};
|
|
|
|
if(!parsed || ret > category::UNKNOWN)
|
|
ret = category::UNKNOWN;
|
|
|
|
assert(uint8_t(ret) <= uint8_t(category::UNKNOWN));
|
|
return ret;
|
|
}
|
|
|
|
enum ircd::http::category
|
|
ircd::http::category(const enum code &code)
|
|
noexcept
|
|
{
|
|
if(ushort(code) == 0)
|
|
return category::NONE;
|
|
|
|
if(ushort(code) < 200)
|
|
return category::INFO;
|
|
|
|
if(ushort(code) < 300)
|
|
return category::SUCCESS;
|
|
|
|
if(ushort(code) < 400)
|
|
return category::REDIRECT;
|
|
|
|
if(ushort(code) < 500)
|
|
return category::ERROR;
|
|
|
|
if(ushort(code) == 500)
|
|
return category::INTERNAL;
|
|
|
|
if(ushort(code) < 600)
|
|
return category::SERVER;
|
|
|
|
return category::UNKNOWN;
|
|
}
|
|
|
|
ircd::string_view
|
|
ircd::http::category(const enum category &category)
|
|
noexcept
|
|
{
|
|
switch(category)
|
|
{
|
|
case category::NONE: return "NONE";
|
|
case category::INFO: return "INFO";
|
|
case category::SUCCESS: return "SUCCESS";
|
|
case category::REDIRECT: return "REDIRECT";
|
|
case category::ERROR: return "ERROR";
|
|
case category::SERVER: return "SERVER";
|
|
case category::INTERNAL: return "INTERNAL";
|
|
case category::UNKNOWN: break;
|
|
}
|
|
|
|
return "UNKNOWN";
|
|
}
|