Skip to content

Commit d206af5

Browse files
committed
Merge TASK-008: Internal detail::body hierarchy
Brings in the polymorphic detail::body hierarchy plus iter1 review-pass fixes (file_body TOCTOU, deferred_body null-callable guard, header lifetime/ownership docs, and accompanying tests).
2 parents 1228e20 + 828006c commit d206af5

7 files changed

Lines changed: 892 additions & 4 deletions

File tree

src/Makefile.am

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,12 +19,12 @@
1919
AM_CPPFLAGS = -I../ -I$(srcdir)/httpserver/ -DHTTPSERVER_COMPILATION
2020
METASOURCES = AUTO
2121
lib_LTLIBRARIES = libhttpserver.la
22-
libhttpserver_la_SOURCES = string_utilities.cpp webserver.cpp http_utils.cpp file_info.cpp http_request.cpp http_response.cpp string_response.cpp digest_auth_fail_response.cpp deferred_response.cpp file_response.cpp pipe_response.cpp empty_response.cpp iovec_response.cpp http_resource.cpp create_webserver.cpp details/http_endpoint.cpp
22+
libhttpserver_la_SOURCES = string_utilities.cpp webserver.cpp http_utils.cpp file_info.cpp http_request.cpp http_response.cpp string_response.cpp digest_auth_fail_response.cpp deferred_response.cpp file_response.cpp pipe_response.cpp empty_response.cpp iovec_response.cpp http_resource.cpp create_webserver.cpp details/http_endpoint.cpp details/body.cpp
2323
# noinst_HEADERS: shipped in the tarball but NEVER installed under $prefix/include.
2424
# Detail headers (httpserver/details/*.hpp) live here so they cannot leak to
2525
# downstream consumers — the public surface comes in through <httpserver.hpp>.
26-
noinst_HEADERS = httpserver/string_utilities.hpp httpserver/details/modded_request.hpp httpserver/details/http_endpoint.hpp gettext.h
27-
nobase_include_HEADERS = httpserver.hpp httpserver/constants.hpp httpserver/create_webserver.hpp httpserver/webserver.hpp httpserver/http_utils.hpp httpserver/file_info.hpp httpserver/http_request.hpp httpserver/http_response.hpp httpserver/http_resource.hpp httpserver/string_response.hpp httpserver/digest_auth_fail_response.hpp httpserver/deferred_response.hpp httpserver/file_response.hpp httpserver/pipe_response.hpp httpserver/empty_response.hpp httpserver/feature_unavailable.hpp httpserver/iovec_entry.hpp httpserver/iovec_response.hpp httpserver/http_arg_value.hpp httpserver/http_method.hpp
26+
noinst_HEADERS = httpserver/string_utilities.hpp httpserver/details/modded_request.hpp httpserver/details/http_endpoint.hpp httpserver/details/body.hpp gettext.h
27+
nobase_include_HEADERS = httpserver.hpp httpserver/body_kind.hpp httpserver/constants.hpp httpserver/create_webserver.hpp httpserver/webserver.hpp httpserver/http_utils.hpp httpserver/file_info.hpp httpserver/http_request.hpp httpserver/http_response.hpp httpserver/http_resource.hpp httpserver/string_response.hpp httpserver/digest_auth_fail_response.hpp httpserver/deferred_response.hpp httpserver/file_response.hpp httpserver/pipe_response.hpp httpserver/empty_response.hpp httpserver/feature_unavailable.hpp httpserver/iovec_entry.hpp httpserver/iovec_response.hpp httpserver/http_arg_value.hpp httpserver/http_method.hpp
2828

2929
if HAVE_BAUTH
3030
libhttpserver_la_SOURCES += basic_auth_fail_response.cpp

src/details/body.cpp

Lines changed: 220 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,220 @@
1+
/*
2+
This file is part of libhttpserver
3+
Copyright (C) 2011-2019 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+
#include "httpserver/details/body.hpp"
22+
23+
#include <fcntl.h>
24+
#include <sys/stat.h>
25+
#include <sys/types.h>
26+
#include <sys/uio.h>
27+
#include <unistd.h>
28+
29+
#include <cstddef>
30+
#include <cstdint>
31+
#include <limits>
32+
#include <type_traits>
33+
#include <utility>
34+
35+
#include <microhttpd.h>
36+
37+
namespace httpserver {
38+
39+
namespace detail {
40+
41+
// ---------------------------------------------------------------------------
42+
// Layout-pinning static_asserts for iovec_entry → MHD_IoVec / struct iovec.
43+
// Duplicated from src/iovec_response.cpp during the M2 transition: the
44+
// asserts must live next to every cast site, and TASK-013 will delete
45+
// iovec_response.cpp once http_response::iovec() lands. Duplicate
46+
// static_asserts on identical layouts are harmless.
47+
//
48+
// LIBHTTPSERVER_TODO_TASK013: drop the originals from iovec_response.cpp
49+
// when iovec_response is removed.
50+
// ---------------------------------------------------------------------------
51+
static_assert(sizeof(::httpserver::iovec_entry) == sizeof(struct iovec),
52+
"iovec_entry size must match POSIX struct iovec — divergent platform; "
53+
"implement memcpy fallback (see TASK-004)");
54+
static_assert(offsetof(::httpserver::iovec_entry, base) ==
55+
offsetof(struct iovec, iov_base),
56+
"iovec_entry::base offset must match struct iovec::iov_base");
57+
static_assert(offsetof(::httpserver::iovec_entry, len) ==
58+
offsetof(struct iovec, iov_len),
59+
"iovec_entry::len offset must match struct iovec::iov_len");
60+
61+
static_assert(sizeof(::httpserver::iovec_entry) == sizeof(MHD_IoVec),
62+
"iovec_entry size must match libmicrohttpd MHD_IoVec — MHD layout drift");
63+
static_assert(offsetof(::httpserver::iovec_entry, base) ==
64+
offsetof(MHD_IoVec, iov_base),
65+
"iovec_entry::base offset must match MHD_IoVec::iov_base");
66+
static_assert(offsetof(::httpserver::iovec_entry, len) ==
67+
offsetof(MHD_IoVec, iov_len),
68+
"iovec_entry::len offset must match MHD_IoVec::iov_len");
69+
70+
static_assert(alignof(::httpserver::iovec_entry) == alignof(struct iovec),
71+
"iovec_entry alignment must match POSIX struct iovec — divergent platform; "
72+
"implement memcpy fallback (see TASK-004)");
73+
static_assert(alignof(::httpserver::iovec_entry) == alignof(MHD_IoVec),
74+
"iovec_entry alignment must match MHD_IoVec — MHD layout drift");
75+
76+
static_assert(std::is_standard_layout_v<::httpserver::iovec_entry>,
77+
"iovec_entry must be standard layout for reinterpret_cast to MHD_IoVec");
78+
79+
// ---------------------------------------------------------------------------
80+
// body — virtual destructor anchor (forces vtable emission in this TU).
81+
// ---------------------------------------------------------------------------
82+
body::~body() = default;
83+
84+
// ---------------------------------------------------------------------------
85+
// empty_body
86+
// ---------------------------------------------------------------------------
87+
MHD_Response* empty_body::materialize() {
88+
return MHD_create_response_empty(static_cast<MHD_ResponseFlags>(flags_));
89+
}
90+
91+
// ---------------------------------------------------------------------------
92+
// string_body
93+
// ---------------------------------------------------------------------------
94+
MHD_Response* string_body::materialize() {
95+
// PERSISTENT, not MUST_COPY: content_ is owned by *this and outlives the
96+
// returned MHD_Response (TASK-009 anchors the lifetime). This matches v1
97+
// string_response::get_raw_response.
98+
return MHD_create_response_from_buffer(
99+
content_.size(),
100+
const_cast<void*>(static_cast<const void*>(content_.data())),
101+
MHD_RESPMEM_PERSISTENT);
102+
}
103+
104+
// ---------------------------------------------------------------------------
105+
// file_body — opens the file and fstat's it at construction so size() is
106+
// accurate immediately. materialize() uses fstat's st_size; it never calls
107+
// lseek(), so the fd's read position remains at 0 when handed to
108+
// MHD_create_response_from_fd (security-reviewer-iter1-1 / CWE-367).
109+
// ---------------------------------------------------------------------------
110+
file_body::file_body(std::string path) noexcept
111+
: path_(std::move(path)) {
112+
#ifndef _WIN32
113+
fd_ = ::open(path_.c_str(), O_RDONLY | O_NOFOLLOW);
114+
#else
115+
fd_ = ::open(path_.c_str(), O_RDONLY);
116+
#endif
117+
if (fd_ == -1) return;
118+
119+
struct stat sb;
120+
if (::fstat(fd_, &sb) != 0 || !S_ISREG(sb.st_mode)) {
121+
::close(fd_);
122+
fd_ = -1;
123+
return;
124+
}
125+
126+
// Use fstat's st_size directly — no lseek, no TOCTOU, no fd-position
127+
// side-effect (security-reviewer-iter1-1 / performance-reviewer-iter1-4).
128+
size_ = static_cast<std::size_t>(sb.st_size);
129+
}
130+
131+
file_body::~file_body() {
132+
// Close only if MHD never took ownership (materialized_ stays false until
133+
// MHD_create_response_from_fd returns non-null).
134+
if (!materialized_ && fd_ != -1) {
135+
::close(fd_);
136+
}
137+
}
138+
139+
MHD_Response* file_body::materialize() {
140+
if (fd_ == -1) return nullptr;
141+
142+
if (size_) {
143+
MHD_Response* r = MHD_create_response_from_fd(size_, fd_);
144+
if (r != nullptr) {
145+
materialized_ = true; // MHD now owns fd_
146+
}
147+
return r;
148+
}
149+
// Zero-byte file: serve empty response without giving the fd to MHD.
150+
::close(fd_);
151+
fd_ = -1;
152+
materialized_ = true; // suppress ~file_body's close (already closed)
153+
return MHD_create_response_from_buffer(
154+
0, nullptr, MHD_RESPMEM_PERSISTENT);
155+
}
156+
157+
// ---------------------------------------------------------------------------
158+
// iovec_body
159+
// ---------------------------------------------------------------------------
160+
MHD_Response* iovec_body::materialize() {
161+
// CWE-190 guard preserved from v1 iovec_response::get_raw_response.
162+
if (entries_.size() >
163+
static_cast<std::size_t>(
164+
std::numeric_limits<unsigned int>::max())) {
165+
return nullptr;
166+
}
167+
return MHD_create_response_from_iovec(
168+
reinterpret_cast<const MHD_IoVec*>(entries_.data()),
169+
static_cast<unsigned int>(entries_.size()),
170+
nullptr,
171+
nullptr);
172+
}
173+
174+
// ---------------------------------------------------------------------------
175+
// pipe_body
176+
// ---------------------------------------------------------------------------
177+
pipe_body::~pipe_body() {
178+
// Only close if MHD never took ownership. After a successful
179+
// materialize(), libmicrohttpd closes fd_ when the MHD_Response is
180+
// destroyed.
181+
if (!materialized_ && fd_ != -1) {
182+
::close(fd_);
183+
}
184+
}
185+
186+
MHD_Response* pipe_body::materialize() {
187+
MHD_Response* r = MHD_create_response_from_pipe(fd_);
188+
if (r != nullptr) {
189+
materialized_ = true; // MHD now owns fd_
190+
}
191+
return r;
192+
}
193+
194+
// ---------------------------------------------------------------------------
195+
// deferred_body — trampoline + materialize.
196+
// ---------------------------------------------------------------------------
197+
ssize_t deferred_body::trampoline(void* cls, std::uint64_t pos,
198+
char* buf, std::size_t max) {
199+
// Guard against null cls or empty producer_ (security-reviewer-iter1-3 /
200+
// CWE-476). MHD's callback mechanism does not catch C++ exceptions, so
201+
// throwing std::bad_function_call here would call std::terminate().
202+
// Return MHD_CONTENT_READER_END_WITH_ERROR instead.
203+
auto* self = static_cast<deferred_body*>(cls);
204+
if (!self || !self->producer_) {
205+
return MHD_CONTENT_READER_END_WITH_ERROR;
206+
}
207+
return self->producer_(pos, buf, max);
208+
}
209+
210+
MHD_Response* deferred_body::materialize() {
211+
// Block size 1024 mirrors v1 deferred_response::get_raw_response_helper.
212+
// Free-callback is nullptr because *this owns producer_ and outlives the
213+
// MHD_Response (TASK-009 enforces this via http_response's lifetime).
214+
return MHD_create_response_from_callback(
215+
MHD_SIZE_UNKNOWN, 1024, &deferred_body::trampoline, this, nullptr);
216+
}
217+
218+
} // namespace detail
219+
220+
} // namespace httpserver

src/httpserver.hpp

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@
3030
#ifdef HAVE_BAUTH
3131
#include "httpserver/basic_auth_fail_response.hpp"
3232
#endif // HAVE_BAUTH
33+
#include "httpserver/body_kind.hpp"
3334
#include "httpserver/constants.hpp"
3435
#include "httpserver/deferred_response.hpp"
3536
#ifdef HAVE_DAUTH

src/httpserver/body_kind.hpp

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
/*
2+
This file is part of libhttpserver
3+
Copyright (C) 2011-2019 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+
#if !defined (_HTTPSERVER_HPP_INSIDE_) && !defined (HTTPSERVER_COMPILATION)
22+
#error "Only <httpserver.hpp> or <httpserverpp> can be included directly."
23+
#endif
24+
25+
#ifndef SRC_HTTPSERVER_BODY_KIND_HPP_
26+
#define SRC_HTTPSERVER_BODY_KIND_HPP_
27+
28+
#include <cstdint>
29+
30+
namespace httpserver {
31+
32+
// Tag identifying which subclass of detail::body a given http_response is
33+
// currently holding. Consumers reach this through http_response::kind()
34+
// (TASK-011) and should never have to name detail::body directly — the
35+
// enum is the only consumer-visible part of the body hierarchy.
36+
//
37+
// `empty` is enumerator 0 so a value-initialised body_kind{} matches the
38+
// "no body" state, which is what TASK-009's default-constructed
39+
// http_response will report.
40+
//
41+
// Underlying type is pinned to std::uint8_t so that future additions
42+
// stay within a single byte and do not silently grow http_response. The
43+
// fixed underlying type also makes the enum forward-declarable, although
44+
// http_response.hpp will still pull in this full header (consumers will
45+
// name the enumerators).
46+
enum class body_kind : std::uint8_t {
47+
empty,
48+
string,
49+
file,
50+
iovec,
51+
pipe,
52+
deferred,
53+
};
54+
55+
} // namespace httpserver
56+
#endif // SRC_HTTPSERVER_BODY_KIND_HPP_

0 commit comments

Comments
 (0)