Address #534 - handling missing mime types gracefully (#536)

* Added tests for content-type to mime-type detection.

Added a custom_content_types test case that verifies that a user can
specify the mime-type through the contentType parameter upon creation
of a response. If their contentType does not appear in the mime_types
map, but looks like a valid mime type already, it should be used as the
mime type.

Validating against the full list of valid mime types
(https://www.iana.org/assignments/media-types/media-types.xhtml)
would be too intensive, so we merely verify that the parent type
(application, audio, font, text, image, etc) is a valid RFC6838
type, and that the subtype is at least one character. Thus we can
verify that custom/type fails, and incomplete strings such as
image/ and /json fail.
This commit is contained in:
Jake Arkinstall 2022-09-12 12:16:16 +01:00 committed by GitHub
parent c0bfaa7709
commit 0b90bb486c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
2 changed files with 82 additions and 11 deletions

View File

@ -65,6 +65,57 @@ namespace crow
return crow::get_header_value(headers, key);
}
// naive validation of a mime-type string
static bool validate_mime_type(const std::string& candidate) noexcept
{
// Here we simply check that the candidate type starts with
// a valid parent type, and has at least one character afterwards.
std::array<std::string, 10> valid_parent_types = {
"application/", "audio/", "font/", "example/",
"image/", "message/", "model/", "multipart/",
"text/", "video/"};
for (const std::string& parent : valid_parent_types)
{
// ensure the candidate is *longer* than the parent,
// to avoid unnecessary string comparison and to
// reject zero-length subtypes.
if (candidate.size() <= parent.size())
{
continue;
}
// strncmp is used rather than substr to avoid allocation,
// but a string_view approach would be better if Crow
// migrates to C++17.
if (strncmp(parent.c_str(), candidate.c_str(), parent.size()) == 0)
{
return true;
}
}
return false;
}
// Find the mime type from the content type either by lookup,
// or by the content type itself, if it is a valid a mime type.
// Defaults to text/plain.
static std::string get_mime_type(const std::string& contentType)
{
const auto mimeTypeIterator = mime_types.find(contentType);
if (mimeTypeIterator != mime_types.end())
{
return mimeTypeIterator->second;
}
else if (validate_mime_type(contentType))
{
return contentType;
}
else
{
CROW_LOG_WARNING << "Unable to interpret mime type for content type '" << contentType << "'. Defaulting to text/plain.";
return "text/plain";
}
}
// clang-format off
response() {}
explicit response(int code) : code(code) {}
@ -101,13 +152,13 @@ namespace crow
response(std::string contentType, std::string body):
body(std::move(body))
{
set_header("Content-Type", mime_types.at(contentType));
set_header("Content-Type", get_mime_type(contentType));
}
response(int code, std::string contentType, std::string body):
code(code), body(std::move(body))
{
set_header("Content-Type", mime_types.at(contentType));
set_header("Content-Type", get_mime_type(contentType));
}
response& operator=(const response& r) = delete;
@ -255,15 +306,7 @@ namespace crow
if (!extension.empty())
{
const auto mimeType = mime_types.find(extension);
if (mimeType != mime_types.end())
{
this->add_header("Content-Type", mimeType->second);
}
else
{
this->add_header("Content-Type", "text/plain");
}
this->add_header("Content-Type", get_mime_type(extension));
}
}
else

View File

@ -290,6 +290,34 @@ TEST_CASE("simple_response_routing_params")
CHECK("hello" == rp.get<string>(0));
} // simple_response_routing_params
TEST_CASE("custom_content_types")
{
// standard behaviour: content type is a key of mime_types
CHECK("text/html" == response("html", "").get_header_value("Content-Type"));
CHECK("image/jpeg" == response("jpg", "").get_header_value("Content-Type"));
CHECK("video/mpeg" == response("mpg", "").get_header_value("Content-Type"));
// content type is already a valid mime type
CHECK("text/csv" == response("text/csv", "").get_header_value("Content-Type"));
CHECK("application/xhtml+xml" == response("application/xhtml+xml", "").get_header_value("Content-Type"));
CHECK("font/custom;parameters=ok" == response("font/custom;parameters=ok", "").get_header_value("Content-Type"));
// content type looks like a mime type, but is invalid
// note: RFC6838 only allows a limited set of parent types:
// https://datatracker.ietf.org/doc/html/rfc6838#section-4.2.7
//
// These types are: application, audio, font, example, image, message,
// model, multipart, text, video
CHECK("text/plain" == response("custom/type", "").get_header_value("Content-Type"));
// content type does not look like a mime type.
CHECK("text/plain" == response("notarealextension", "").get_header_value("Content-Type"));
CHECK("text/plain" == response("image/", "").get_header_value("Content-Type"));
CHECK("text/plain" == response("/json", "").get_header_value("Content-Type"));
} // custom_content_types
TEST_CASE("handler_with_response")
{
SimpleApp app;