|
| 1 | +/* |
| 2 | + This file is part of libhttpserver |
| 3 | + Copyright (C) 2011-2026 Sebastiano Merlino |
| 4 | +
|
| 5 | + This library is free software; you can redistribute it and/or |
| 6 | + modify it under the terms of the GNU Lesser General Public |
| 7 | + License as published by the Free Software Foundation; either |
| 8 | + version 2.1 of the License, or (at your option) any later version. |
| 9 | +
|
| 10 | + This library is distributed in the hope that it will be useful, |
| 11 | + but WITHOUT ANY WARRANTY; without even the implied warranty of |
| 12 | + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU |
| 13 | + Lesser General Public License for more details. |
| 14 | +
|
| 15 | + You should have received a copy of the GNU Lesser General Public |
| 16 | + License along with this library; if not, write to the Free Software |
| 17 | + Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 |
| 18 | + USA |
| 19 | +*/ |
| 20 | + |
| 21 | +// response_materializer behavior service (DR-014 §4.11). Logic moved |
| 22 | +// verbatim out of the former detail/webserver_response_queue.cpp; that TU |
| 23 | +// now holds only the thin webserver_impl::materialize_and_queue_response |
| 24 | +// forwarder. Rewiring vs the original: the error paths call errors_ |
| 25 | +// (error_pages) instead of the in-class helpers; response_sent fires via |
| 26 | +// hook_dispatch_; log_dispatch_error is the free function over config_. |
| 27 | + |
| 28 | +#include "httpserver/detail/response_materializer.hpp" |
| 29 | + |
| 30 | +#include <microhttpd.h> |
| 31 | + |
| 32 | +#include <optional> |
| 33 | +#include <stdexcept> |
| 34 | +#include <string> |
| 35 | +#include <utility> |
| 36 | + |
| 37 | +#include "httpserver/create_webserver.hpp" |
| 38 | +#include "httpserver/http_response.hpp" |
| 39 | +#include "httpserver/http_utils.hpp" |
| 40 | +#include "httpserver/detail/body.hpp" |
| 41 | +#include "httpserver/detail/dispatch_util.hpp" |
| 42 | +#include "httpserver/detail/error_pages.hpp" |
| 43 | +#include "httpserver/detail/hook_dispatcher.hpp" |
| 44 | +#include "httpserver/detail/modded_request.hpp" |
| 45 | + |
| 46 | +namespace httpserver { |
| 47 | +namespace detail { |
| 48 | + |
| 49 | +// materialize_response: ask the body to produce a fresh MHD_Response with |
| 50 | +// no headers/footers/cookies attached. webserver_impl / response_materializer |
| 51 | +// are friends of http_response so body_ is reachable directly. |
| 52 | +MHD_Response* response_materializer::materialize_response(http_response* resp) { |
| 53 | + if (resp == nullptr || resp->body_ == nullptr) { |
| 54 | + return nullptr; |
| 55 | + } |
| 56 | + return resp->body_->materialize(); |
| 57 | +} |
| 58 | + |
| 59 | +// decorate_mhd_response: walk the response's header/footer/cookie maps and |
| 60 | +// attach each to the materialized MHD_Response. |
| 61 | +void response_materializer::decorate_mhd_response(MHD_Response* response, |
| 62 | + const http_response& resp) { |
| 63 | + for (const auto& [k, v] : resp.get_headers()) { |
| 64 | + MHD_add_response_header(response, k.c_str(), v.c_str()); |
| 65 | + } |
| 66 | + for (const auto& [k, v] : resp.get_footers()) { |
| 67 | + MHD_add_response_footer(response, k.c_str(), v.c_str()); |
| 68 | + } |
| 69 | + // Render from the structured cookie list (not the legacy cookies_ map) |
| 70 | + // via cookie::to_set_cookie_header() so attributes propagate to the wire |
| 71 | + // per RFC 6265 §4.1. |
| 72 | + for (const auto& c : resp.get_cookies_parsed()) { |
| 73 | + const std::string cookie_hdr = c.to_set_cookie_header(); |
| 74 | + MHD_add_response_header(response, "Set-Cookie", cookie_hdr.c_str()); |
| 75 | + } |
| 76 | +} |
| 77 | + |
| 78 | +struct MHD_Response* response_materializer::get_raw_response_with_fallback( |
| 79 | + detail::modded_request* mr) { |
| 80 | + // Every assignment into mr->response uses emplace(std::move(...)); the |
| 81 | + // optional owns the value and the deferred-body trampoline keeps a |
| 82 | + // pointer into it for the lifetime of the modded_request. |
| 83 | + auto try_materialize = [&]() -> struct MHD_Response* { |
| 84 | + return materialize_response(mr->response ? &*mr->response : nullptr); |
| 85 | + }; |
| 86 | + auto emplace_and_materialize = [&](http_response r) -> struct MHD_Response* { |
| 87 | + mr->response.emplace(std::move(r)); |
| 88 | + return try_materialize(); |
| 89 | + }; |
| 90 | + try { |
| 91 | + struct MHD_Response* raw = try_materialize(); |
| 92 | + if (raw == nullptr) { |
| 93 | + // No exception, but the body materializer returned null. Route |
| 94 | + // through the safe internal-error path. |
| 95 | + return emplace_and_materialize( |
| 96 | + errors_.run_internal_error_handler_safely( |
| 97 | + mr, "materialize_response returned null")); |
| 98 | + } |
| 99 | + return raw; |
| 100 | + } catch(const std::invalid_argument&) { |
| 101 | + try { |
| 102 | + return emplace_and_materialize(errors_.not_found_page(mr)); |
| 103 | + } catch(...) { |
| 104 | + return nullptr; |
| 105 | + } |
| 106 | + } catch(const std::exception& e) { |
| 107 | + log_dispatch_error(config_, std::string("materialize threw: ") + e.what()); |
| 108 | + try { |
| 109 | + return emplace_and_materialize( |
| 110 | + errors_.run_internal_error_handler_safely(mr, e.what())); |
| 111 | + } catch(...) { |
| 112 | + return nullptr; |
| 113 | + } |
| 114 | + } catch(...) { |
| 115 | + log_dispatch_error(config_, "materialize threw unknown exception"); |
| 116 | + try { |
| 117 | + return emplace_and_materialize( |
| 118 | + errors_.run_internal_error_handler_safely(mr, |
| 119 | + "unknown exception")); |
| 120 | + } catch(...) { |
| 121 | + return nullptr; |
| 122 | + } |
| 123 | + } |
| 124 | +} |
| 125 | + |
| 126 | +// Kind-dispatched queueing. For body_kind::digest_challenge, delegate to |
| 127 | +// MHD_queue_auth_required_response3 so libmicrohttpd writes the RFC-7616 |
| 128 | +// WWW-Authenticate header with its HMAC-keyed nonce, our opaque, and the |
| 129 | +// requested algorithm/qop/charset/userhash bits. Every other body kind goes |
| 130 | +// through the standard MHD_queue_response path. The digest mapping is |
| 131 | +// factored into the map_to_mhd_digest_args_ anonymous-namespace helper so |
| 132 | +// the dispatcher stays under the CCN ceiling. |
| 133 | +#ifdef HAVE_DAUTH |
| 134 | +namespace { |
| 135 | +struct mhd_digest_args { |
| 136 | + MHD_DigestAuthMultiAlgo3 algo; |
| 137 | + MHD_DigestAuthMultiQOP qop; |
| 138 | + const char* opaque_cstr; |
| 139 | + const char* domain_cstr; |
| 140 | +}; |
| 141 | + |
| 142 | +mhd_digest_args map_to_mhd_digest_args_( |
| 143 | + const detail::digest_challenge_body::params& p, |
| 144 | + const std::string& server_opaque) { |
| 145 | + MHD_DigestAuthMultiAlgo3 algo; |
| 146 | + switch (p.algorithm) { |
| 147 | + case http::http_utils::digest_algorithm::SHA256: |
| 148 | + algo = MHD_DIGEST_AUTH_MULT_ALGO3_SHA256; |
| 149 | + break; |
| 150 | + case http::http_utils::digest_algorithm::SHA512_256: |
| 151 | + algo = MHD_DIGEST_AUTH_MULT_ALGO3_SHA512_256; |
| 152 | + break; |
| 153 | + case http::http_utils::digest_algorithm::MD5: |
| 154 | + default: |
| 155 | + algo = MHD_DIGEST_AUTH_MULT_ALGO3_MD5; |
| 156 | + break; |
| 157 | + } |
| 158 | + // qop="auth" is the only v2.0-supported variant; auth-int is parked. |
| 159 | + // qop_auth == false -> RFC-2069 no-qop. |
| 160 | + MHD_DigestAuthMultiQOP qop = p.qop_auth |
| 161 | + ? MHD_DIGEST_AUTH_MULT_QOP_AUTH |
| 162 | + : MHD_DIGEST_AUTH_MULT_QOP_NONE; |
| 163 | + // Empty user opaque -> substitute the per-webserver opaque. |
| 164 | + const char* opaque_cstr = |
| 165 | + p.opaque.empty() ? server_opaque.c_str() : p.opaque.c_str(); |
| 166 | + const char* domain_cstr = |
| 167 | + p.domain.empty() ? nullptr : p.domain.c_str(); |
| 168 | + return {algo, qop, opaque_cstr, domain_cstr}; |
| 169 | +} |
| 170 | +} // namespace |
| 171 | +#endif // HAVE_DAUTH |
| 172 | + |
| 173 | +int response_materializer::queue_response_dispatching_kind( |
| 174 | + MHD_Connection* connection, |
| 175 | + detail::modded_request* mr, |
| 176 | + MHD_Response* raw_response) { |
| 177 | +#ifdef HAVE_DAUTH |
| 178 | + if (mr->response->kind() == body_kind::digest_challenge) { |
| 179 | + auto* dch = static_cast<detail::digest_challenge_body*>( |
| 180 | + mr->response->body_); |
| 181 | + if (dch == nullptr) { |
| 182 | + // Defensive guard (CWE-476): kind() reported digest_challenge but |
| 183 | + // body_ is null. Fall back to the plain queue path. |
| 184 | + return static_cast<int>(MHD_queue_response( |
| 185 | + connection, mr->response->get_status(), raw_response)); |
| 186 | + } |
| 187 | + const auto& p = dch->get_params(); |
| 188 | + auto args = map_to_mhd_digest_args_(p, digest_opaque_); |
| 189 | + return static_cast<int>(MHD_queue_auth_required_response3( |
| 190 | + connection, |
| 191 | + p.realm.c_str(), |
| 192 | + args.opaque_cstr, |
| 193 | + args.domain_cstr, |
| 194 | + raw_response, |
| 195 | + p.signal_stale ? MHD_YES : MHD_NO, |
| 196 | + args.qop, |
| 197 | + args.algo, |
| 198 | + p.userhash_support ? MHD_YES : MHD_NO, |
| 199 | + p.prefer_utf8 ? MHD_YES : MHD_NO)); |
| 200 | + } |
| 201 | +#endif // HAVE_DAUTH |
| 202 | + return static_cast<int>(MHD_queue_response( |
| 203 | + connection, mr->response->get_status(), raw_response)); |
| 204 | +} |
| 205 | + |
| 206 | +MHD_Result response_materializer::materialize_and_queue_response( |
| 207 | + MHD_Connection* connection, |
| 208 | + detail::modded_request* mr, |
| 209 | + http_resource* resource) { |
| 210 | + struct MHD_Response* raw_response = get_raw_response_with_fallback(mr); |
| 211 | + if (raw_response == nullptr) { |
| 212 | + // Belt-and-suspenders: even get_raw_response_with_fallback's own |
| 213 | + // try/catch couldn't produce a response. Force the empty-body 500 so |
| 214 | + // MHD always has something to queue. Contract: log before the 500. |
| 215 | + log_dispatch_error(config_, |
| 216 | + "materialize_and_queue_response: " |
| 217 | + "get_raw_response_with_fallback returned null; " |
| 218 | + "forcing hardcoded empty-body 500"); |
| 219 | + mr->response.emplace( |
| 220 | + errors_.internal_error_page(mr, "", /*force_our=*/true)); |
| 221 | + raw_response = materialize_response(&*mr->response); |
| 222 | + if (raw_response == nullptr) { |
| 223 | + // Last-resort guard: internal_error_page's materialization also |
| 224 | + // returned null (e.g. under extreme memory pressure). Cannot call |
| 225 | + // decorate_mhd_response(nullptr, ...) -- that would pass a null |
| 226 | + // MHD_Response to MHD_add_response_header (CWE-476). Return MHD_NO |
| 227 | + // so the connection terminates gracefully rather than crashing. |
| 228 | + return (MHD_Result) MHD_NO; |
| 229 | + } |
| 230 | + } |
| 231 | + decorate_mhd_response(raw_response, *mr->response); |
| 232 | + int to_ret = queue_response_dispatching_kind(connection, mr, raw_response); |
| 233 | + // Fire response_sent AFTER MHD_queue_response (status/bytes reflect what |
| 234 | + // was queued) and BEFORE MHD_destroy_response (ctx.response backed by live |
| 235 | + // storage). MHD copies the response data during queue, so destroying the |
| 236 | + // MHD_Response below does not affect the queued bytes. |
| 237 | + hook_dispatch_.fire_response_sent_gated(mr, resource); |
| 238 | + // MHD reference-counting: for callback (deferred/streaming) responses MHD |
| 239 | + // increments its own refcount during queue, so this destroy only releases |
| 240 | + // the caller's reference. MHD keeps the streaming callback (and the cls |
| 241 | + // pointer into mr->response) alive until request_completed fires. The |
| 242 | + // modded_request (and mr->response) are destroyed only in the |
| 243 | + // request_completed callback, after MHD is done streaming. |
| 244 | + MHD_destroy_response(raw_response); |
| 245 | + return (MHD_Result) to_ret; |
| 246 | +} |
| 247 | + |
| 248 | +} // namespace detail |
| 249 | +} // namespace httpserver |
0 commit comments