/* 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 */ #if !defined (_HTTPSERVER_HPP_INSIDE_) && !defined (HTTPSERVER_COMPILATION) #error "Only or can be included directly." #endif #ifndef SRC_HTTPSERVER_WEBSERVER_HPP_ #define SRC_HTTPSERVER_WEBSERVER_HPP_ #include #include #include #include #include #include #include #include #include #include #include "httpserver/constants.hpp" #include "httpserver/hook_action.hpp" #include "httpserver/hook_context.hpp" #include "httpserver/hook_handle.hpp" #include "httpserver/hook_phase.hpp" #include "httpserver/http_method.hpp" #include "httpserver/http_utils.hpp" #include "httpserver/create_webserver.hpp" // Socket-layer types kept minimal in this public header. // `struct sockaddr` is a POSIX-tagged struct and can be forward-declared // per-use, but `fd_set` is a typedef of an unnamed struct in glibc, so // it must be a complete type when get_fdset's signature is parsed. // Pull in only the system header that defines fd_set; the rest of the // BSD-socket / select API does not leak into consumer TUs. #if defined(_WIN32) && !defined(__CYGWIN__) #include #else #include #endif // Forward declarations: backend (MHD) types are intentionally NOT pulled in. // The libmicrohttpd and pthread headers live behind the PIMPL // boundary in detail/webserver_impl.hpp. namespace httpserver { class http_resource; class http_response; // Forward-declared unconditionally so the public surface of // webserver is identical in HAVE_WEBSOCKET-on and HAVE_WEBSOCKET-off // builds. When HAVE_WEBSOCKET is undefined the // class definition in websocket_handler.hpp is still included via the // umbrella header; member-function bodies live in src/websocket_handler.cpp. class websocket_handler; namespace detail { struct connection_context; class webserver_impl; class daemon_lifecycle; class http_endpoint; } // namespace detail } // namespace httpserver namespace httpserver { /** * Class representing the webserver. Main class of the apis. * * ### Threading contract * * The webserver dispatches each request on one of libmicrohttpd's worker * threads. The thread-safety contract: * * 1. Public registration / un-registration methods (@ref register_path, * @ref register_prefix, the @ref on_get * family, @ref route, @ref unregister_path, @ref unregister_prefix, * @ref unregister_resource, @ref register_ws_resource, * @ref unregister_ws_resource, @ref deny_ip, @ref remove_denied_ip, * @ref allow_ip, @ref remove_allowed_ip) are * thread-safe and re-entrant from inside a request handler. * The route lookup LRU cache (`detail::route_lru_cache`) is consulted * outside `route_table_mutex_`, so an in-flight request that hit the * cache may still be served by a resource whose @ref unregister_path / * @ref unregister_resource call already completed concurrently. No * use-after-free results -- the cache holds a `shared_ptr` that keeps * the resource alive -- but the response for that one request may be * logically stale (served by the just-unregistered handler). * 2. The exceptions are @ref stop, @ref stop_and_wait, and the * destructor: each joins libmicrohttpd's worker threads and * therefore deadlocks (or aborts with "Failed to join a thread." * on some libmicrohttpd versions) when called from within a * handler thread. Call them from the thread that owns the * webserver instance. * 3. `http_request` is single-threaded per request: it is owned by * the worker thread servicing that request and MUST NOT be * retained beyond the handler's return. * 4. `http_response` is a value type with exclusive ownership; no * cross-thread sharing. * 5. User-supplied callbacks invoked from MHD worker threads -- * @ref create_webserver::log_access, @ref create_webserver::log_error, * @ref create_webserver::not_found_handler, * @ref create_webserver::method_not_allowed_handler, * @ref create_webserver::internal_error_handler, * @ref create_webserver::file_cleanup_callback, the PSK / SNI / ALPN * callbacks, any registered @ref http_resource render method, and * any registered lifecycle hook (@ref add_hook / @ref http_resource::add_hook ) * -- may run concurrently on multiple threads. Implementations MUST be thread-safe. * * ### Handler error-propagation contract * * Every registered request handler is invoked from the dispatch path under * a two-branch try/catch. The contract: * * 1. The handler call is wrapped in * `try { ... } catch (const std::exception& e) { ... } catch (...) { ... }`. * 2. On `std::exception`: the message is logged via the configured * `log_error` callback, then `internal_error_handler` is invoked with * `e.what()`. The response it returns is sent on the wire. When no * handler is configured, the default 500 carries the fixed body * `"Internal Server Error"` (CWE-209 fix); * the originating message is still surfaced via the * `log_error` callback. The verbose v1 body (message in the body) * is opt-in via @ref create_webserver::expose_exception_messages. * 3. On non-`std::exception` (e.g. `throw 42`): same path with the * message replaced by the literal string `"unknown exception"`. * 4. If `internal_error_handler` itself throws while servicing 2 or 3, * the failure is logged generically and a hardcoded 500 with an * EMPTY body is sent. No exception ever escapes into libmicrohttpd. * 5. `feature_unavailable` (a `std::runtime_error` subclass) is NOT * mapped to a special status: it lands as a generic 500 like any * other `std::exception`. * 6. The `log_error` callback may be invoked concurrently from multiple * MHD worker threads; user implementations MUST be thread-safe. * 7. Hook layering: * @ref hook_phase::handler_exception hooks fire BEFORE this alias; * throwing hooks are caught and the chain continues; (4) fires * without re-invoking the alias on full chain failure. * * Resources are encouraged to throw rather than synthesise 500s. **/ class webserver { public: // Explicit to forbid implicit conversion from // create_webserver. Callers must direct-init: webserver ws{cw}; explicit webserver(const create_webserver& params); /** * Destructor. * * Calls stop() unconditionally, which joins libmicrohttpd's * worker threads. For the same reason as stop(), destroying a * webserver from inside a handler thread deadlocks (or, on some * libmicrohttpd versions, aborts with "Failed to join a thread."). * Destroy the webserver from the thread that constructed it. * * @see stop() for the threading constraints that apply equally here. **/ ~webserver(); // PIMPL-owned: copy/move would slice the backing impl object. webserver(const webserver&) = delete; webserver& operator=(const webserver&) = delete; webserver(webserver&&) = delete; webserver& operator=(webserver&&) = delete; /** * Method used to start the webserver. * This method can be blocking or not. * @param blocking param indicating if the method is blocking or not * @return a boolean indicating if the webserver is running or not. **/ bool start(bool blocking = false); /** * Stop the webserver. * * Joins libmicrohttpd's worker threads before returning. Safe to * call from any thread *except* a handler thread: stop() blocks * until every worker (including the calling one) drains, so a * call from inside a handler self-joins and deadlocks (or, on * some libmicrohttpd versions, aborts with "Failed to join a * thread."). * * For the same reason, ~webserver() (which calls stop()) * deadlocks if it runs on a handler thread; destroy the * webserver from the thread that constructed it. * * @return true if the daemon was running and is now stopped; * false if it was already stopped. **/ bool stop(); /** * Method used to evaluate if the server is running or not. * @return true if the webserver is running **/ bool is_running(); // Registration, on_* shortcuts, route(), and unregister_* live in // a sibling header to keep this class under the project per-file // LOC ceiling. The inner gate forces the header to be included // only from within this class body. #define SRC_HTTPSERVER_WEBSERVER_HPP_INSIDE_CLASS_ #include "httpserver/webserver_routes.hpp" #undef SRC_HTTPSERVER_WEBSERVER_HPP_INSIDE_CLASS_ /** * Add @p ip (or a range, e.g. "127.0.0.*") to the IP deny list. * Connections from a matching address are refused at the policy * callback. This is the exception list under the default ACCEPT * policy (deny only these; permit everyone else). * * Precedence: a matching @ref allow_ip entry overrides a matching * deny entry, so an allow-listed address is admitted even under * ACCEPT. No-op semantics are preserved when the same IP is added * twice; a more specific entry replaces a previously-recorded * wildcard. * * @param ip an IP literal or wildcard pattern. * @see remove_denied_ip, allow_ip **/ void deny_ip(std::string_view ip); /** * Remove @p ip from the IP deny list. Idempotent: removing an IP * that is not currently denied is a no-op. * * @param ip an IP literal or wildcard pattern previously passed to @ref deny_ip. * @see deny_ip **/ void remove_denied_ip(std::string_view ip); /** * Add @p ip (or a range, e.g. "127.0.0.*") to the IP allow list. * This is the exception list under the REJECT policy * (@ref create_webserver::default_policy): permit only these, * refuse everyone else. Under the default ACCEPT policy an allow * entry overrides a matching @ref deny_ip entry (allow wins). * * No-op semantics are preserved when the same IP is added twice; * a more specific entry replaces a previously-recorded wildcard. * * @param ip an IP literal or wildcard pattern. * @see remove_allowed_ip, deny_ip **/ void allow_ip(std::string_view ip); /** * Remove @p ip from the IP allow list. Idempotent: removing an IP * that is not currently allowed is a no-op. * * @param ip an IP literal or wildcard pattern previously passed to @ref allow_ip. * @see allow_ip **/ void remove_allowed_ip(std::string_view ip); /// Returns the configured access-log callback; null if none was set. /// The callback may be invoked concurrently from MHD worker threads. log_access_ptr get_access_logger() const { return config.log_access; } /// Returns the configured error-log callback; null if none was set. /// The callback may be invoked concurrently from MHD worker threads. log_error_ptr get_error_logger() const { return config.log_error; } /// Returns the configured request-validator callback; null if none was set. /// The callback may be invoked concurrently from MHD worker threads. [[deprecated("validator callback is not invoked by v2 dispatch; use webserver::add_hook(hook_phase::request_received, ...) instead")]] validator_ptr get_request_validator() const { return config.validator; } /// Returns the configured URL-unescaper callback; null if none was set. /// The callback may be invoked concurrently from MHD worker threads. unescaper_ptr get_unescaper() const { return config.unescaper; } // Event-loop, connection-management, and feature-reporting methods // live in a sibling header to keep this class under the project // per-file LOC ceiling. The inner gate forces the header to be // included only from within this class body. #define SRC_HTTPSERVER_WEBSERVER_HPP_INSIDE_CLASS_ #include "httpserver/webserver_runtime.hpp" #undef SRC_HTTPSERVER_WEBSERVER_HPP_INSIDE_CLASS_ // Websocket registration surface and lifecycle hook bus // live in sibling headers to keep this class under the project // per-file LOC ceiling. #define SRC_HTTPSERVER_WEBSERVER_HPP_INSIDE_CLASS_ #include "httpserver/webserver_websocket.hpp" #include "httpserver/webserver_hooks.hpp" #undef SRC_HTTPSERVER_WEBSERVER_HPP_INSIDE_CLASS_ private: // All builder inputs, copied wholesale from the create_webserver // builder at construction (see webserver_config in // create_webserver.hpp). Immutable for the server's lifetime. // webserver_impl and the dispatch path read options as // parent->config.. const webserver_config config; // Pre-normalized form of @ref config.auth_skip_paths, populated once // at construction (a derived value, not a builder input, so it is not // part of webserver_config). webserver_impl::should_skip_auth compares // request paths against this list (not config.auth_skip_paths) so // non-canonical entries like "/public/" or "/a/../b" match the // canonical request path the dispatch surface produces. Built by // detail::normalize_auth_skip_paths in webserver_request.cpp. const std::vector auth_skip_paths_normalized; // Shared registration helper. Both register_path and register_prefix // funnel through here so the validation/insertion logic lives in one // place. `family=true` is prefix-matching; `family=false` is // exact-matching. void register_impl_(const std::string& path, std::shared_ptr res, bool family); // register_impl_ helpers carved out so the parent stays under the // project-wide CCN gate. void validate_register_inputs_(const std::string& path, const std::shared_ptr& res, bool family) const; // Shared unregistration helper. Erases a single registration of the // requested kind. void unregister_impl_(const std::string& path, bool family); // Shared lambda-registration helper. Builds-or- // merges a hidden detail::lambda_resource shim at @p path, sets every // bit in @p methods on it, and stores @p handler into each of those // method slots. All seven public on_* overloads and both public // route() overloads forward to this single entry point so the // merge-and-conflict logic lives in one place. Validation is // atomic: if any requested method already has a slot on the path, // no slot is mutated and the call throws -- callers therefore see // either a fully-installed registration or no change at all. // Throws std::invalid_argument if @p methods is empty, if @p // handler is empty, if the path conflicts with single_resource // mode, if a class-based resource is already registered at the // path, or if a lambda is already registered for any requested // (method, path). void on_methods_(method_set methods, const std::string& path, std::function handler); // on_methods_ helpers carved out so the parent stays under the // project-wide CCN gate. void validate_on_methods_inputs_(method_set methods, const std::string& path, const std::function& handler) const; // PIMPL: backend-coupled state (MHD daemon, pthread mutexes, route // table, ban set, route cache, websocket registry, GnuTLS SNI cache, // and the dispatch helpers / MHD trampolines that operate on those) // lives behind this pointer in detail/webserver_impl.hpp. The public // header carries no // baggage. std::unique_ptr impl_; // detail::webserver_impl reads the const config bag above (tcp_nodelay, // unescaper, regex_checking, auth_handler, etc.) when servicing // requests, and houses the MHD trampolines / dispatch helpers so // stays out of this public header. Granting friendship // is preferable to introducing a long list of trivial public getters // that cross the PIMPL boundary in both directions. friend class detail::webserver_impl; // daemon_lifecycle (the extracted MHD daemon-construction collaborator) // reads the same const config bag when building the option array and // start flags, so it needs the same friendship as webserver_impl. friend class detail::daemon_lifecycle; friend class http_response; #if defined(HTTPSERVER_COMPILATION) // Test-only hook so unit tests in test/unit/ can poke // at the v2 route-table impl (lookup_v2, the three tier maps) // without widening the public API. The pattern matches the SBO // test access friend used by http_response. Gated on // HTTPSERVER_COMPILATION so it never appears in installed headers. friend struct webserver_test_access; #endif }; #if defined(HTTPSERVER_COMPILATION) // Forward-declared friend giving test code (which compiles with // HTTPSERVER_COMPILATION via test/Makefile.am AM_CPPFLAGS) a thin // pointer to the otherwise-private impl_. Defined inline so any TU // including this header in COMPILATION mode can use it. struct webserver_test_access { static detail::webserver_impl* impl(webserver& w) noexcept { return w.impl_.get(); } }; #endif } // namespace httpserver #endif // SRC_HTTPSERVER_WEBSERVER_HPP_