-
-
Notifications
You must be signed in to change notification settings - Fork 191
Expand file tree
/
Copy pathhttp_response_factories.cpp
More file actions
303 lines (279 loc) · 13.1 KB
/
Copy pathhttp_response_factories.cpp
File metadata and controls
303 lines (279 loc) · 13.1 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
/*
This file is part of libhttpserver
Copyright (C) 2011-2019 Sebastiano Merlino
This library is free software; you can redistribute it and/or
modify it under the terms of the GNU Lesser General Public
License as published by the Free Software Foundation; either
version 2.1 of the License, or (at your option) any later version.
This library is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
Lesser General Public License for more details.
You should have received a copy of the GNU Lesser General Public
License along with this library; if not, write to the Free Software
Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301
USA
*/
#include "httpserver/http_response.hpp"
#include <sys/types.h> // ssize_t (for the deferred() producer)
#include <cassert>
#include <cstddef>
#include <cstdint>
#include <functional>
#include <iostream>
#include <map>
#include <new>
#include <span>
#include <stdexcept>
#include <string>
#include <string_view>
#include <type_traits>
#include <utility>
#include <vector>
#include "httpserver/detail/response_body.hpp" // complete type for body_->~response_body()
#include "httpserver/detail/http_field_validation.hpp"
#include "httpserver/feature_unavailable.hpp"
#include "httpserver/http_utils.hpp"
#include "httpserver/iovec_entry.hpp"
namespace httpserver {
// http_response_factories.cpp -- static named-constructor factories and the
// shared emplace_body placement-new entry point.
//
// Carved out of src/http_response.cpp to keep both translation
// units under the project per-file LOC ceiling (FILE_LOC_MAX in
// scripts/check-file-size.sh).
// -----------------------------------------------------------------------
// emplace_body — single placement-new entry point shared by all
// factories. Centralising the SBO-vs-heap decision here means
// the matched ::operator new(sizeof(T)) / ::operator delete pairing the
// destructor relies on lives in exactly one place; a
// stray plain `new T(...)` in any factory would mismatch the
// destructor's ::operator delete and trip ASan immediately.
//
// Defined out-of-line in this TU because every factory in this file
// instantiates it (so no separate-TU instantiation is needed) and the
// template body needs the complete type detail::response_body. Per-T size+align
// guards duplicate the SBO budget asserts in detail/response_body.hpp so an
// over-sized future body subclass fails to compile at the factory site
// rather than silently triggering the heap fallback.
// -----------------------------------------------------------------------
template <typename T, typename... Args>
void http_response::emplace_body(body_kind k, Args&&... args) {
static_assert(std::is_base_of_v<detail::response_body, T>,
"emplace_body: T must derive from detail::response_body");
assert(body_ == nullptr &&
"emplace_body: body slot already populated");
if constexpr (sizeof(T) <= body_buf_size && alignof(T) <= 16) {
// SBO inline path.
body_ = ::new (body_storage_) T(std::forward<Args>(args)...);
body_inline_ = true;
} else {
// Heap fallback. ::operator new(sizeof(T)) is paired exactly
// with the destructor's ::operator delete(body_); a plain
// `new T(...)` here would mismatch.
void* mem = ::operator new(sizeof(T));
try {
body_ = ::new (mem) T(std::forward<Args>(args)...);
} catch (...) {
::operator delete(mem);
throw;
}
body_inline_ = false;
}
kind_ = k;
}
// -----------------------------------------------------------------------
// Static factories. Each factory:
// 1. constructs a default http_response (status_code_ = -1, no body),
// 2. sets the status code and any per-kind headers,
// 3. emplaces the appropriate detail::response_body subclass via emplace_body.
//
// The status-code defaults match v1: 200 for content-bearing bodies,
// 204 for empty(), 401 for unauthorized().
// -----------------------------------------------------------------------
http_response http_response::empty(int mhd_flags) {
http_response r;
r.status_code_ = http::http_utils::http_no_content; // 204
r.emplace_body<detail::empty_response_body>(body_kind::empty, mhd_flags);
return r;
}
http_response http_response::string(std::string response_body,
std::string content_type) {
http_response r;
r.status_code_ = http::http_utils::http_ok; // 200
r.with_header(http::http_utils::http_header_content_type,
std::move(content_type));
r.emplace_body<detail::string_response_body>(body_kind::string,
std::move(response_body));
return r;
}
http_response http_response::file(std::string path) {
http_response r;
r.status_code_ = http::http_utils::http_ok;
// Match v1 file_response default Content-Type. Callers can override
// with .with_header("Content-Type", "...") in the chain.
r.with_header(http::http_utils::http_header_content_type,
http::http_utils::application_octet_stream);
r.emplace_body<detail::file_response_body>(body_kind::file, std::move(path));
return r;
}
http_response http_response::iovec(std::span<const iovec_entry> entries) {
// Deep-copy into the body's owned vector so the caller's span need
// not outlive the response. The buffers each entry's `base` points
// at remain BORROWED — see detail::iovec_response_body's lifetime contract.
std::vector<iovec_entry> v(entries.begin(), entries.end());
http_response r;
r.status_code_ = http::http_utils::http_ok;
r.emplace_body<detail::iovec_response_body>(body_kind::iovec, std::move(v));
return r;
}
http_response http_response::pipe(int fd) {
http_response r;
r.status_code_ = http::http_utils::http_ok;
r.emplace_body<detail::pipe_response_body>(body_kind::pipe, fd);
return r;
}
http_response http_response::deferred(
std::function<ssize_t(std::uint64_t, char*, std::size_t)> producer) {
http_response r;
r.status_code_ = http::http_utils::http_ok;
r.emplace_body<detail::deferred_response_body>(body_kind::deferred,
std::move(producer));
return r;
}
http_response http_response::unauthorized(std::string_view scheme,
std::string_view realm,
std::string response_body) {
// Security: reject scheme or realm values containing CR, LF, or NUL.
// Any of these characters can be used to inject additional HTTP headers
// into the WWW-Authenticate response header (CWE-113). This is always a
// caller error — callers must never pass untrusted user input as scheme
// or realm without first validating it. Throw std::invalid_argument so
// the error is visible and cannot be silently swallowed.
// detail::kForbiddenFieldChars is the same constant used by
// validate_http_field in http_response.cpp — shared to avoid a
// duplicate definition.
if (scheme.find_first_of(detail::kForbiddenFieldChars) != std::string_view::npos) {
throw std::invalid_argument(
"http_response::unauthorized: scheme contains forbidden control "
"character (CR, LF, or NUL)");
}
if (realm.find_first_of(detail::kForbiddenFieldChars) != std::string_view::npos) {
throw std::invalid_argument(
"http_response::unauthorized: realm contains forbidden control "
"character (CR, LF, or NUL)");
}
// Security: escape backslash and double-quote characters inside realm
// per RFC 7235 §2.1 quoted-string rules. RFC 7235 allows both to be
// escaped via the quoted-pair rule `\X`; an unescaped `"` terminates
// the quoted-string early (CWE-116) and an unescaped `\` is
// misinterpreted as starting an escape sequence by strict parsers.
// Backslash must be escaped first to avoid double-escaping the
// backslashes we insert for double-quote escaping.
//
// Fast path: the common case (e.g. the canonical `Basic realm="myrealm"`)
// has no backslash or double-quote to escape, so realm can be appended
// directly and the char-by-char copy + heap allocation below is skipped.
http_response r;
r.status_code_ = http::http_utils::http_unauthorized; // 401
// Build `<scheme> realm="<escaped_realm>"`. AC #3 requires byte-for-byte
// `Basic realm="myrealm"` for the canonical case (which has no quotes).
std::string challenge;
challenge.reserve(scheme.size() + realm.size() + 10);
challenge.append(scheme.data(), scheme.size());
challenge.append(" realm=\"");
if (realm.find_first_of("\\\"") == std::string_view::npos) {
challenge.append(realm.data(), realm.size());
} else {
std::string escaped_realm;
escaped_realm.reserve(realm.size());
for (char c : realm) {
if (c == '\\') {
escaped_realm.push_back('\\'); // escape backslash first
} else if (c == '"') {
escaped_realm.push_back('\\'); // escape double-quote
}
escaped_realm.push_back(c);
}
challenge.append(escaped_realm);
}
challenge.push_back('"');
r.with_header(http::http_utils::http_header_www_authenticate,
challenge);
// The body slot literally holds a string_response_body (possibly empty), so
// kind() reports body_kind::string. Switching to body_kind::empty
// for the empty-body case would fork the construction path and
// break the invariant that kind() reflects the placed-new body.
r.emplace_body<detail::string_response_body>(body_kind::string,
std::move(response_body));
return r;
}
#ifdef HAVE_DAUTH
// RFC 7616 §3.3-compliant Digest challenge factory.
//
// Validates the user-supplied fields for header-injection control
// characters (CR/LF/NUL) and packs the parameters into a
// detail::digest_challenge_response_body. No `WWW-Authenticate` header is added
// at the response-value layer; the dispatch path (digest-challenge
// branch in materialize_and_queue_response) calls
// MHD_queue_auth_required_response3 to attach the authoritative
// challenge with nonce/opaque/algorithm/qop/charset/userhash bits.
//
// Empty opaque is preserved: the dispatch path substitutes
// webserver_impl::digest_opaque_ at queue time, so the factory remains
// side-effect-free (no webserver reference required).
http_response http_response::unauthorized(digest_challenge challenge) {
// Same forbidden-character set as validate_http_field above.
auto reject_ctrl_chars = [](std::string_view field,
std::string_view value) {
if (value.find_first_of(detail::kForbiddenFieldChars) !=
std::string_view::npos) {
throw std::invalid_argument(
std::string("http_response::unauthorized(digest_challenge): ") +
std::string(field) +
" contains forbidden control character (CR, LF, or NUL)");
}
};
reject_ctrl_chars("realm", challenge.realm);
reject_ctrl_chars("opaque", challenge.opaque);
reject_ctrl_chars("domain", challenge.domain);
reject_ctrl_chars("response_body", challenge.response_body);
// qop="auth-int" is not implemented: the dispatch path
// (map_to_mhd_digest_args_) has no MHD mapping for it and would
// silently ignore the flag. Fail loudly rather than let a caller
// believe integrity protection was negotiated.
if (challenge.qop_auth_int) {
throw std::invalid_argument(
"http_response::unauthorized(digest_challenge): "
"qop_auth_int (qop=\"auth-int\") is not implemented; "
"leave it false");
}
detail::digest_challenge_response_body::params p{
/*realm=*/ std::move(challenge.realm),
/*opaque=*/ std::move(challenge.opaque),
/*domain=*/ std::move(challenge.domain),
/*body_text=*/ std::move(challenge.response_body),
/*algorithm=*/ challenge.algorithm,
/*qop_auth=*/ challenge.qop_auth,
/*qop_auth_int=*/ challenge.qop_auth_int,
/*signal_stale=*/ challenge.signal_stale,
/*userhash_support=*/ challenge.userhash_support,
/*prefer_utf8=*/ challenge.prefer_utf8,
};
http_response r;
r.status_code_ = http::http_utils::http_unauthorized; // 401
r.emplace_body<detail::digest_challenge_response_body>(
body_kind::digest_challenge, std::move(p));
return r;
}
#else // !HAVE_DAUTH
// The declaration in http_response.hpp is
// unconditional, so a HAVE_DAUTH-off build must still define this overload
// rather than leave it as a link error. Throw feature_unavailable instead
// of constructing a response.
http_response http_response::unauthorized(digest_challenge challenge) {
(void)challenge;
throw feature_unavailable("digest_auth", "HAVE_DAUTH");
}
#endif // HAVE_DAUTH
} // namespace httpserver