|
| 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 | +// request_pipeline behavior service (DR-014 §4.11). The two body-pipeline |
| 22 | +// stages + complete_request moved verbatim out of |
| 23 | +// detail/webserver_body_pipeline.cpp / webserver_request.cpp (which keep the |
| 24 | +// debug-dump free functions and the answer_to_connection trampoline / |
| 25 | +// resolve_method_callback / should_skip_auth respectively). Rewiring: |
| 26 | +// parent->config.* becomes config_.*, hook gates go through hooks_ |
| 27 | +// (hook_dispatcher), and complete_request hands off to dispatcher_ |
| 28 | +// (request_dispatcher). The redundant belt-and-suspenders `mr->ws = parent` |
| 29 | +// refresh in the post-processor helper is dropped (answer_to_connection |
| 30 | +// already set it and the comment noted it never changes the value). |
| 31 | + |
| 32 | +#include "httpserver/detail/request_pipeline.hpp" |
| 33 | + |
| 34 | +#include <microhttpd.h> |
| 35 | +#include <strings.h> |
| 36 | + |
| 37 | +#include <chrono> |
| 38 | +#include <cstddef> |
| 39 | +#include <cstring> |
| 40 | +#include <iostream> |
| 41 | +#include <optional> |
| 42 | +#include <span> |
| 43 | +#include <string_view> |
| 44 | +#include <utility> |
| 45 | + |
| 46 | +#include "httpserver/create_webserver.hpp" |
| 47 | +#include "httpserver/hook_action.hpp" |
| 48 | +#include "httpserver/hook_context.hpp" |
| 49 | +#include "httpserver/hook_phase.hpp" |
| 50 | +#include "httpserver/http_request.hpp" |
| 51 | +#include "httpserver/http_response.hpp" |
| 52 | +#include "httpserver/http_utils.hpp" |
| 53 | +#include "httpserver/detail/hook_dispatcher.hpp" |
| 54 | +#include "httpserver/detail/modded_request.hpp" |
| 55 | +#include "httpserver/detail/request_dispatcher.hpp" |
| 56 | +#include "httpserver/detail/webserver_impl.hpp" |
| 57 | + |
| 58 | +namespace httpserver { |
| 59 | + |
| 60 | +using httpserver::http::http_utils; |
| 61 | + |
| 62 | +namespace detail { |
| 63 | + |
| 64 | +namespace { |
| 65 | + |
| 66 | +// Wrap the body_chunk firing site so requests_answer_second_step stays a |
| 67 | +// flat sequence of small steps. Returns true iff a hook short-circuited and |
| 68 | +// the caller should signal MHD the chunk was consumed. Side effects on |
| 69 | +// short-circuit: mr->response populated, mr->skip_handler set, any in-flight |
| 70 | +// post-processor destroyed. |
| 71 | +bool fire_and_maybe_short_circuit_body_chunk(hook_dispatcher& hooks, |
| 72 | + modded_request* mr, |
| 73 | + const char* upload_data, |
| 74 | + size_t upload_data_size) { |
| 75 | + // ctx.offset is sourced from body_bytes_seen (not get_content().size()) |
| 76 | + // so it accumulates correctly even when put_processed_data_to_content is |
| 77 | + // false and a post-processor is active. ctx.is_final is hard-coded false: |
| 78 | + // end-of-body is signalled by MHD's zero-size upload callback, which |
| 79 | + // routes to complete_request and never reaches this fire site. |
| 80 | + ::httpserver::body_chunk_ctx ctx{ |
| 81 | + mr->request.get(), |
| 82 | + std::as_bytes(std::span<const char>(upload_data, upload_data_size)), |
| 83 | + mr->body_bytes_seen, |
| 84 | + /*is_final=*/false}; |
| 85 | + mr->body_bytes_seen += upload_data_size; |
| 86 | + auto sc = hooks.fire_body_chunk(ctx); |
| 87 | + if (!sc) return false; |
| 88 | + mr->response.emplace(std::move(*sc)); |
| 89 | + mr->skip_handler = true; |
| 90 | + if (mr->pp != nullptr) { |
| 91 | + MHD_destroy_post_processor(mr->pp); |
| 92 | + mr->pp = nullptr; |
| 93 | + } |
| 94 | + return true; |
| 95 | +} |
| 96 | + |
| 97 | +// Feed @p upload_data through MHD's post processor (when one is attached) |
| 98 | +// and close any open upload-target stream. |
| 99 | +void run_post_processor_if_attached(modded_request* mr, const char* upload_data, |
| 100 | + size_t upload_data_size) { |
| 101 | + if (mr->pp == nullptr) return; |
| 102 | + MHD_post_process(mr->pp, upload_data, upload_data_size); |
| 103 | + if (mr->upload_ostrm != nullptr && mr->upload_ostrm->is_open()) { |
| 104 | + mr->upload_ostrm->close(); |
| 105 | + } |
| 106 | +} |
| 107 | + |
| 108 | +} // namespace |
| 109 | + |
| 110 | +MHD_Result request_pipeline::requests_answer_first_step( |
| 111 | + MHD_Connection* connection, struct detail::modded_request* mr) { |
| 112 | + // The http_request constructor calls pick_resource(connection) internally |
| 113 | + // to locate the per-connection arena installed by connection_notify, then |
| 114 | + // allocates the http_request_impl from that arena. |
| 115 | + mr->request.reset(new http_request(connection, config_.unescaper)); |
| 116 | + mr->request->set_file_cleanup_callback(config_.file_cleanup_callback); |
| 117 | + // Propagate the redaction-bypass bit so operator<< honours the builder |
| 118 | + // opt-in for every request the webserver dispatches. |
| 119 | + mr->request->set_expose_credentials_in_logs( |
| 120 | + config_.expose_credentials_in_logs); |
| 121 | + |
| 122 | + // request_received hook. Fires after the http_request is populated but |
| 123 | + // before any body bytes are read (and before any post-processor is |
| 124 | + // created). Short-circuit: stash the response, mark skip-to-finalize, and |
| 125 | + // return MHD_YES; MHD calls back into second_step with |
| 126 | + // *upload_data_size == 0, which routes through complete_request -> |
| 127 | + // finalize_answer's skip_handler branch. |
| 128 | + if (hooks_.has_hooks_for(::httpserver::hook_phase::request_received)) { |
| 129 | + ::httpserver::request_received_ctx ctx{ |
| 130 | + mr->request.get(), |
| 131 | + std::chrono::steady_clock::now()}; |
| 132 | + if (auto sc = hooks_.fire_request_received(ctx)) { |
| 133 | + mr->response.emplace(std::move(*sc)); |
| 134 | + mr->skip_handler = true; |
| 135 | + return MHD_YES; |
| 136 | + } |
| 137 | + } |
| 138 | + |
| 139 | + if (!mr->has_body) { |
| 140 | + return MHD_YES; |
| 141 | + } |
| 142 | + |
| 143 | + mr->request->set_content_size_limit(config_.content_size_limit); |
| 144 | + const char *encoding = MHD_lookup_connection_value(connection, |
| 145 | + MHD_HEADER_KIND, http_utils::http_header_content_type); |
| 146 | + |
| 147 | + if (config_.post_process_enabled && |
| 148 | + (nullptr != encoding && |
| 149 | + ((0 == strncasecmp(http_utils::http_post_encoding_form_urlencoded, encoding, strlen(http_utils::http_post_encoding_form_urlencoded))) || |
| 150 | + (0 == strncasecmp(http_utils::http_post_encoding_multipart_formdata, encoding, strlen(http_utils::http_post_encoding_multipart_formdata)))))) { |
| 151 | + const size_t post_memory_limit(32 * 1024); // Same as #MHD_POOL_SIZE_DEFAULT |
| 152 | + mr->pp = MHD_create_post_processor(connection, post_memory_limit, &webserver_impl::post_iterator, mr); |
| 153 | + } else { |
| 154 | + mr->pp = nullptr; |
| 155 | + } |
| 156 | + return MHD_YES; |
| 157 | +} |
| 158 | + |
| 159 | +MHD_Result request_pipeline::requests_answer_second_step( |
| 160 | + MHD_Connection* connection, const char* method, const char* version, |
| 161 | + const char* upload_data, size_t* upload_data_size, |
| 162 | + struct detail::modded_request* mr) { |
| 163 | + if (0 == *upload_data_size) return complete_request(connection, mr, version, method); |
| 164 | + |
| 165 | + if (!mr->has_body) { |
| 166 | + *upload_data_size = 0; |
| 167 | + return MHD_YES; |
| 168 | + } |
| 169 | + |
| 170 | + // A prior pre-handler short-circuit (request_received in first_step, or |
| 171 | + // body_chunk on an earlier chunk) already populated mr->response. Consume |
| 172 | + // the chunk so MHD advances; the next *upload_data_size == 0 callback |
| 173 | + // routes to finalize_answer's skip_handler branch. |
| 174 | + if (mr->skip_handler) { |
| 175 | + *upload_data_size = 0; |
| 176 | + return MHD_YES; |
| 177 | + } |
| 178 | + |
| 179 | + // body_chunk hook fires per chunk BEFORE the bytes are appended to |
| 180 | + // mr->request / fed to MHD_post_process. |
| 181 | + if (hooks_.has_hooks_for(::httpserver::hook_phase::body_chunk)) { |
| 182 | + if (fire_and_maybe_short_circuit_body_chunk( |
| 183 | + hooks_, mr, upload_data, *upload_data_size)) { |
| 184 | + *upload_data_size = 0; |
| 185 | + return MHD_YES; |
| 186 | + } |
| 187 | + } |
| 188 | + |
| 189 | + // Raw request-body dump, opt-in via LIBHTTPSERVER_DEBUG_DUMP_REQUEST_BODY. |
| 190 | + // Default behaviour is silent on RELEASE *and* DEBUG builds. The |
| 191 | + // operator<< redaction policy does NOT cover this path: raw bytes are |
| 192 | + // written verbatim. See docs/debug-env-vars.md. |
| 193 | + if (debug_dump_request_body_opted_in()) { |
| 194 | + std::cout << "Writing content: "; |
| 195 | + std::cout.write(upload_data, |
| 196 | + static_cast<std::streamsize>(*upload_data_size)); |
| 197 | + std::cout << std::endl; |
| 198 | + } |
| 199 | + // The post iterator is only created for multipart/form-data and |
| 200 | + // application/x-www-form-urlencoded; all other content (mr->pp == nullptr) |
| 201 | + // must be put to the content even if put_processed_data_to_content is false. |
| 202 | + if (mr->pp == nullptr || config_.put_processed_data_to_content) { |
| 203 | + mr->request->grow_content(upload_data, *upload_data_size); |
| 204 | + } |
| 205 | + run_post_processor_if_attached(mr, upload_data, *upload_data_size); |
| 206 | + |
| 207 | + *upload_data_size = 0; |
| 208 | + return MHD_YES; |
| 209 | +} |
| 210 | + |
| 211 | +MHD_Result request_pipeline::complete_request(MHD_Connection* connection, |
| 212 | + struct detail::modded_request* mr, const char* version, |
| 213 | + const char* method) { |
| 214 | + // mr->ws is pre-populated in answer_to_connection (hoisted there for |
| 215 | + // early-path request_completed coverage); no need to set it again here. |
| 216 | + mr->request->set_path(mr->standardized_url); |
| 217 | + mr->request->set_method(method); |
| 218 | + mr->request->set_version(version); |
| 219 | + |
| 220 | + return dispatcher_.finalize_answer(connection, mr); |
| 221 | +} |
| 222 | + |
| 223 | +} // namespace detail |
| 224 | +} // namespace httpserver |
0 commit comments