Line data Source code
1 : #ifndef THINGER_HTTP_SERVER_BODY_DECODERS_HPP
2 : #define THINGER_HTTP_SERVER_BODY_DECODERS_HPP
3 :
4 : #include "../common/http_response.hpp"
5 : #include "response_compression.hpp" // Accept-Encoding negotiation, declared here before
6 : #include <nlohmann/json.hpp>
7 : #include <cstddef>
8 : #include <functional>
9 : #include <optional>
10 : #include <stdexcept>
11 : #include <string>
12 : #include <string_view>
13 : #include <utility>
14 : #include <vector>
15 :
16 : namespace thinger::http {
17 :
18 : // Media type of a Content-Type (or Accept) value: "type/subtype" and its parameters
19 : // (RFC 9110, 8.3.1), e.g. "Application/JSON; charset=\"UTF-8\"" -> "application/json",
20 : // {"charset", "UTF-8"}
21 : struct media_type {
22 : // "type/subtype", lowercase
23 : std::string type;
24 : // Parameters in order: names lowercase, values unquoted (case kept)
25 : std::vector<std::pair<std::string, std::string>> parameters;
26 :
27 : // Parse a media type; nullopt if it is malformed. Media ranges ("*/*", "text/*") are
28 : // accepted too, for Accept headers.
29 : static std::optional<media_type> parse(std::string_view value);
30 :
31 : // Value of a parameter (the first one with that name), empty if missing
32 : std::string parameter(std::string_view name) const;
33 : };
34 :
35 : // Limits applied while decoding a body, against inputs that are small but expensive to
36 : // decode or to use. The body size itself is limited by the server (set_max_body_size).
37 : // Besides these, a body never decodes to more values than it has bytes (every value takes
38 : // at least one byte in JSON, MessagePack and CBOR; UBJSON can declare arrays of values
39 : // without payload, such as a billion nulls in a few bytes, which are rejected).
40 : struct body_limits {
41 : // Maximum nesting of arrays and objects (a scalar is at depth 0, [] or {} at depth 1).
42 : // Decoding is iterative, but the decoded value is often used recursively (schema
43 : // validation, dump(), copies, comparisons), so an unlimited depth could overflow the stack.
44 : size_t max_depth = 128;
45 : };
46 :
47 : // A body that cannot be decoded: 400 Bad Request if it is invalid, or 415 Unsupported Media
48 : // Type if its type (or charset) is not supported. The message is sent to the client.
49 : class body_decode_error : public std::runtime_error {
50 : public:
51 326 : explicit body_decode_error(const std::string& message,
52 : http_response::status status = http_response::status::bad_request)
53 326 : : std::runtime_error(message), status_(status) {}
54 :
55 320 : http_response::status status() const { return status_; }
56 :
57 : private:
58 : http_response::status status_;
59 : };
60 :
61 : // Decodes a body of a media type into a JSON value, within the limits. Throws
62 : // body_decode_error if it cannot; any other exception is answered as an invalid body (400).
63 : using body_decoder = std::function<nlohmann::json(std::string_view body, const media_type& type,
64 : const body_limits& limits)>;
65 :
66 : // A body format: a decoder and the media types it decodes
67 : struct body_format {
68 : // Name the routes accept it by, e.g. "msgpack"
69 : std::string name;
70 : // Media types it decodes (lowercase), e.g. "application/msgpack", "application/x-msgpack"
71 : std::vector<std::string> media_types;
72 : // Structured syntax suffix it also decodes, e.g. "+json" for "application/problem+json"
73 : // (empty if none)
74 : std::string suffix;
75 : body_decoder decode;
76 : };
77 :
78 : // Decoders of request bodies by media type, used for the routes taking a decoded body
79 : // (see route::accepts). Built in, each one removable:
80 : //
81 : // name media types decoded as
82 : // json application/json (and any +json) the JSON value
83 : // msgpack application/msgpack, application/x-msgpack the value (bin/ext as binary)
84 : // cbor application/cbor (and any +cbor) the value (byte strings as binary,
85 : // tagged items rejected)
86 : // ubjson application/ubjson the value
87 : // form application/x-www-form-urlencoded an object of strings (repeated
88 : // names: an array of strings)
89 : // multipart multipart/form-data the same, with files as objects
90 : // {filename, content_type, content}
91 : // text text/plain a string (valid UTF-8)
92 : // binary application/octet-stream a binary value (json::binary)
93 : //
94 : // JSON, form and text take no charset other than UTF-8 (415); strings in every format must
95 : // be valid UTF-8 (400 otherwise), so a decoded value can always be dumped. Configure it
96 : // before serving: it is read concurrently by the worker threads.
97 : class body_decoder_registry {
98 : public:
99 : // With the built-in decoders
100 : body_decoder_registry();
101 :
102 : // Add a decoder, replacing the one with the same name (if any); its media types (and
103 : // suffix) are removed from any other decoder
104 : body_decoder_registry& add(const std::string& name, std::vector<std::string> media_types,
105 : body_decoder decoder, std::string suffix = {});
106 :
107 : // Remove a decoder (e.g. a built-in one): bodies of its media types are no longer
108 : // decoded, so routes accepting them answer 415. Returns false if there was none.
109 : bool remove(const std::string& name);
110 :
111 : // Decoder by name, null if none
112 : const body_format* find(std::string_view name) const;
113 :
114 : // Decoder for a media type ("type/subtype", lowercase): by its media types, or else by
115 : // its suffix; null if none
116 : const body_format* find_media_type(std::string_view type) const;
117 :
118 : // A media type exactly as a decoder lists it, without parameters (as most clients send
119 : // their Content-Type, e.g. "application/json"): that media type, parsed when the decoder
120 : // was added, and its decoder; {null, null} if it is not one. A Content-Type found here
121 : // needs no parsing on each request.
122 : std::pair<const media_type*, const body_format*> find_plain_media_type(std::string_view type) const;
123 :
124 : const std::vector<body_format>& formats() const { return formats_; }
125 :
126 : // The built-in decoders, to reuse in custom ones
127 : static nlohmann::json decode_json(std::string_view body, const media_type& type, const body_limits& limits);
128 : static nlohmann::json decode_msgpack(std::string_view body, const media_type& type, const body_limits& limits);
129 : static nlohmann::json decode_cbor(std::string_view body, const media_type& type, const body_limits& limits);
130 : static nlohmann::json decode_ubjson(std::string_view body, const media_type& type, const body_limits& limits);
131 : static nlohmann::json decode_form(std::string_view body, const media_type& type, const body_limits& limits);
132 : static nlohmann::json decode_multipart(std::string_view body, const media_type& type, const body_limits& limits);
133 : static nlohmann::json decode_text(std::string_view body, const media_type& type, const body_limits& limits);
134 : static nlohmann::json decode_binary(std::string_view body, const media_type& type, const body_limits& limits);
135 :
136 : private:
137 : // Index the plain media types of the decoders (after adding or removing one)
138 : void index_plain_media_types();
139 :
140 : std::vector<body_format> formats_;
141 : // The media types of the decoders that are plain (lowercase "type/subtype"), parsed,
142 : // with the index of their decoder in formats_
143 : std::vector<std::pair<media_type, size_t>> plain_media_types_;
144 : };
145 :
146 : // How the bodies of the routes are decoded: the application settings (see
147 : // http_application::body_decoders), shared by its virtual hosts
148 : struct body_settings {
149 : body_decoder_registry decoders;
150 : // Types accepted by routes taking a decoded body that do not set their own (route::accepts)
151 : std::vector<std::string> accepted{"json"};
152 : body_limits limits;
153 :
154 : // Defaults, for routes handled without an application
155 : static const body_settings& defaults();
156 : };
157 :
158 : // Media type assumed for a body without Content-Type: JSON, as before body decoders existed
159 : inline constexpr std::string_view default_body_media_type = "application/json";
160 :
161 : // Result of checking the Content-Type of a body against the types a route accepts
162 : struct body_type_match {
163 : // Whether the body is accepted; if not, `error` tells why (for a 415 answer)
164 : bool accepted = false;
165 : std::string error;
166 : // Media type of the body (default_body_media_type without Content-Type)
167 : media_type type;
168 : // Decoder for it, null if none is registered (a type accepted only by a route reading
169 : // the body itself)
170 : const body_format* format = nullptr;
171 : };
172 :
173 : // Check a Content-Type header value (empty if missing) against the accepted types: names of
174 : // decoders in the registry, or media types. A type is accepted if its media type is listed,
175 : // or the name of its decoder.
176 : body_type_match match_body_type(const body_decoder_registry& decoders,
177 : const std::vector<std::string>& accepted,
178 : std::string_view content_type);
179 :
180 : // Media types of a list of accepted types (the media types of each decoder named, and the
181 : // media types listed), in order and without repetitions, e.g. for API documentation
182 : std::vector<std::string> accepted_media_types(const body_decoder_registry& decoders,
183 : const std::vector<std::string>& accepted);
184 :
185 : // Response encoding chosen by the Accept header of a request, among JSON, MessagePack, CBOR
186 : // and UBJSON (see response::encode)
187 : struct response_encoding {
188 : std::string media_type; // Content-Type to send, e.g. "application/msgpack"
189 : std::string format; // "json", "msgpack", "cbor" or "ubjson"
190 : };
191 :
192 : // The encoding the Accept header (empty if missing) prefers, by quality and then in the
193 : // order JSON, MessagePack, CBOR, UBJSON; JSON if it accepts none of them
194 : response_encoding negotiate_response_encoding(std::string_view accept);
195 :
196 : // Encode a value in a format of negotiate_response_encoding()
197 : std::string encode_body(const nlohmann::json& value, const std::string& format);
198 :
199 : } // namespace thinger::http
200 :
201 : #endif // THINGER_HTTP_SERVER_BODY_DECODERS_HPP
|