diff --git a/ChangeLog b/ChangeLog index 7bb0aefb..45bc62db 100644 --- a/ChangeLog +++ b/ChangeLog @@ -1,5 +1,27 @@ Version 0.20.0 + Raised minimum libmicrohttpd requirement to 1.0.0. + Migrated Basic Auth to v3 API (MHD_basic_auth_get_username_password3, + MHD_queue_basic_auth_required_response3) with UTF-8 support. + Migrated Digest Auth to v3 API (MHD_digest_auth_check3, + MHD_digest_auth_check_digest3, MHD_queue_auth_required_response3) + with SHA-512/256 support, userhash, nonce binding, and structured + digest_auth_result enum. Default algorithm changed to SHA-256. + Added new response types: empty_response, pipe_response, iovec_response. + Added external event loop integration: webserver::run(), run_wait(), + get_fdset(), get_timeout(), add_connection(). + Added daemon management: quiesce(), get_listen_fd(), + get_active_connections(), get_bound_port(). + Added daemon options: listen_backlog, address_reuse, + connection_memory_increment, tcp_fastopen_queue_size, + sigpipe_handled_by_app, https_mem_dhparams, https_key_password, + https_priorities_append, no_alpn, client_discipline_level. + Added startup flags: no_listen_socket, no_thread_safety, turbo, + suppress_date_header. + Added WebSocket support (conditional on HAVE_WEBSOCKET): + websocket_handler, websocket_session, register_ws_resource(). + Added utility functions: reason_phrase(), is_feature_supported(), + get_mhd_version(). Added conditional compilation for basic auth (HAVE_BAUTH), mirroring existing HAVE_DAUTH pattern for digest auth. Basic auth support is auto-detected via AC_CHECK_LIB and can be disabled at build time. diff --git a/README.md b/README.md index a70ff849..e0f2267e 100644 --- a/README.md +++ b/README.md @@ -33,6 +33,13 @@ libhttpserver is built upon [libmicrohttpd](https://www.gnu.org/software/libmic - Support for basic and digest authentication (optional) - Support for centralized authentication with path-based skip rules - Support for TLS (requires libgnutls, optional) +- WebSocket support (requires libmicrohttpd built with WebSocket support, optional) +- New response types: empty, iovec (scatter-gather), and pipe-based responses +- External event loop integration (run/run_wait, fd sets, add_connection) +- Daemon introspection (bound port, active connections, listen FD) +- Turbo mode for high-performance scenarios +- TCP Fast Open support +- Enhanced digest authentication with SHA-256 and SHA-512/256 algorithms ## Table of Contents * [Introduction](#introduction) @@ -47,6 +54,8 @@ libhttpserver is built upon [libmicrohttpd](https://www.gnu.org/software/libmic * [Building responses to requests](#building-responses-to-requests) * [IP Blacklisting and Whitelisting](#ip-blacklisting-and-whitelisting) * [Authentication](#authentication) +* [WebSocket Support](#websocket-support) +* [Daemon Introspection and External Event Loops](#daemon-introspection-and-external-event-loops) * [HTTP Utils](#http-utils) * [Other Examples](#other-examples) @@ -80,7 +89,7 @@ libhttpserver can be used without any dependencies aside from libmicrohttpd. The minimum versions required are: * g++ >= 5.5.0 or clang-3.6 * C++17 or newer -* libmicrohttpd >= 0.9.64 +* libmicrohttpd >= 1.0.0 * [Optionally]: for TLS (HTTPS) support, you'll need [libgnutls](http://www.gnutls.org/). * [Optionally]: to compile the code-reference, you'll need [doxygen](http://www.doxygen.nl/). @@ -141,7 +150,7 @@ MSYS2 provides multiple shell environments with different purposes. Understandin pacman -S --needed mingw-w64-x86_64-{gcc,libtool,make,pkg-config,doxygen,gnutls,curl} autotools ``` -4. Build and install [libmicrohttpd](https://www.gnu.org/software/libmicrohttpd/) (>= 0.9.64) +4. Build and install [libmicrohttpd](https://www.gnu.org/software/libmicrohttpd/) (>= 1.0.0) 5. Build libhttpserver: ```bash @@ -234,6 +243,11 @@ You can also check this example on [github](https://github.com/etr/libhttpserver * _basic_auth_fail_response:_ A failure in basic authentication. * _digest_auth_fail_response:_ A failure in digest authentication. * _deferred_response:_ A response getting content from a callback. + * _empty_response:_ A response with no body (e.g., for 204 No Content). + * _iovec_response:_ A scatter-gather response from multiple buffers. + * _pipe_response:_ A response that streams content from a pipe file descriptor. +* _websocket_handler:_ Base class for handling WebSocket connections. Derive and implement `on_message()`. + * _websocket_session:_ Represents an active WebSocket connection with methods to send text, binary, ping/pong, and close frames. [Back to TOC](#table-of-contents) @@ -275,6 +289,16 @@ For example, if your connection limit is “1”, a browser may open a first con * _.file_cleanup_callback(**file_cleanup_callback_ptr** callback):_ Sets a callback function to control what happens to uploaded files when the request completes. By default (when no callback is set), all uploaded files are automatically deleted. The callback signature is `bool(const std::string& key, const std::string& filename, const http::file_info& info)` where `key` is the form field name, `filename` is the original uploaded filename, and `info` contains file metadata including the filesystem path. Return `true` to delete the file (default behavior) or `false` to keep it (e.g., after moving it to permanent storage). If the callback throws an exception, the file will be deleted as a safety measure. * _.deferred()_ and _.no_deferred():_ Enables/Disables the ability for the server to suspend and resume connections. Simply put, it enables/disables the ability to use `deferred_response`. Read more [here](#building-responses-to-requests). `on` by default. * _.single_resource() and .no_single_resource:_ Sets or unsets the server in single resource mode. This limits all endpoints to be served from a single resource. The resultant is that the webserver will process the request matching to the endpoint skipping any complex semantic. Because of this, the option is incompatible with `regex_checking` and requires the resource to be registered against an empty endpoint or the root endpoint (`"/"`). The resource will also have to be registered as family. (For more information on resource registration, read more [here](#registering-resources)). `off` by default. +* _.no_listen_socket():_ Run the daemon without a listening socket. The server will not bind to any port on its own; instead, you must provide connections externally via `add_connection()`. Useful for integrating with an external accept loop or passing sockets from systemd or another process. `off` by default. +* _.no_thread_safety():_ Disable internal thread-safety mechanisms. This can improve performance when you guarantee that only a single thread will access the daemon at a time. **Only use this if you are sure you do not need concurrent access.** `off` by default. +* _.turbo():_ Enable turbo mode. This is a performance optimization that allows the daemon to skip certain internal operations. Requires the application to meet specific threading and response constraints — consult the libmicrohttpd documentation for details. `off` by default. +* _.suppress_date_header():_ Suppress the automatic addition of a `Date:` header in responses. Useful for reproducible tests or when the application manages its own date headers. `off` by default. +* _.listen_backlog(**int** backlog):_ Set the TCP listen backlog size. Higher values allow more pending connections in the kernel queue. Default is `0` (system default). +* _.address_reuse(**int** reuse):_ Control address reuse (`SO_REUSEADDR`/`SO_REUSEPORT`). Pass `1` to enable, `-1` to disable. Default is `0` (system default). +* _.connection_memory_increment(**size_t** increment):_ Increment size for per-connection memory allocation when the initial pool is exhausted. Default is `0` (system default, typically 1024 bytes). +* _.tcp_fastopen_queue_size(**int** queue_size):_ Set the size of the TCP Fast Open queue. When set, enables TCP Fast Open with the specified queue depth. Default is `0` (disabled). +* _.sigpipe_handled_by_app():_ Inform the daemon that the application is handling `SIGPIPE` on its own, so libmicrohttpd should not install a handler. `off` by default. +* _.client_discipline_level(**int** level):_ Controls how strictly the server enforces HTTP protocol compliance. Higher values make the server stricter with misbehaving clients. Default is `-1` (use libmicrohttpd default). ### Threading Models * _.start_method(**const http::http_utils::start_method_T&** start_method):_ libhttpserver can operate with two different threading models that can be selected through this method. Default value is `INTERNAL_SELECT`. @@ -389,7 +413,11 @@ You can also check this example on [github](https://github.com/etr/libhttpserver * _.https_mem_trust(**const std::string&** filename):_ String representing the path to a file containing the CA certificate to be used by the HTTPS daemon to authenticate and trust clients certificates. The presence of this option activates the request of certificate to the client. The request to the client is marked optional, and it is the responsibility of the server to check the presence of the certificate if needed. Note that most browsers will only present a client certificate only if they have one matching the specified CA, not sending any certificate otherwise. * _.https_priorities(**const std::string&** priority_string):_ SSL/TLS protocol version and ciphers. Must be followed by a string specifying the SSL/TLS protocol versions and ciphers that are acceptable for the application. The string is passed unchanged to gnutls_priority_init. If this option is not specified, `"NORMAL"` is used. * _.psk_cred_handler(**psk_cred_handler_callback** handler):_ Sets a callback function for TLS-PSK (Pre-Shared Key) authentication. The callback receives a username and should return the corresponding hex-encoded PSK, or an empty string if the user is unknown. This option requires `use_ssl()`, `cred_type(http::http_utils::PSK)`, and an appropriate `https_priorities()` string that enables PSK cipher suites. PSK authentication allows TLS without certificates by using a shared secret key. -* _.sni_callback(**sni_callback_t** callback):_ Sets a callback function for SNI (Server Name Indication) support. The callback receives the server name requested by the client and should return a `std::pair` containing the PEM-encoded certificate and key for that server name. Return empty strings to use the default certificate. Requires libmicrohttpd 0.9.71+ with GnuTLS. +* _.sni_callback(**sni_callback_t** callback):_ Sets a callback function for SNI (Server Name Indication) support. The callback receives the server name requested by the client and should return a `std::pair` containing the PEM-encoded certificate and key for that server name. Return empty strings to use the default certificate. Requires libmicrohttpd 1.0.0+ with GnuTLS. +* _.https_mem_dhparams(**const std::string&** dhparams):_ String containing the Diffie-Hellman (DH) parameters in PEM format. This is used for DHE key exchange in TLS. If not specified, default DH parameters may be used. +* _.https_key_password(**const std::string&** password):_ Password for the private key specified by `https_mem_key`, if the key file is encrypted. +* _.https_priorities_append(**const std::string&** priorities):_ Additional GnuTLS priorities to append to the base priority string. Unlike `https_priorities()` which replaces the entire string, this appends to the default, making it easier to adjust specific cipher suites or algorithms. +* _.no_alpn():_ Disable Application-Layer Protocol Negotiation (ALPN) for TLS connections. `off` by default. #### Minimal example using HTTPS ```cpp @@ -511,10 +539,19 @@ You should calculate the value of NC_SIZE based on the number of connections per ``` ### Starting and stopping a webserver Once a webserver is created, you can manage its execution through the following methods on the `webserver` class: -* _**void** webserver::start(**bool** blocking):_ Allows to start a server. If the `blocking` flag is passed as `true`, it will block the execution of the current thread until a call to stop on the same webserver object is performed. +* _**void** webserver::start(**bool** blocking):_ Allows to start a server. If the `blocking` flag is passed as `true`, it will block the execution of the current thread until a call to stop on the same webserver object is performed. * _**void** webserver::stop():_ Allows to stop a server. It immediately stops it. * _**bool** webserver::is_running():_ Checks if a server is running * _**void** webserver::sweet_kill():_ Allows to stop a server. It doesn't guarantee an immediate halt to allow for thread termination and connection closure. +* _**int** webserver::quiesce():_ Quiesce the daemon: stop accepting new connections while letting in-flight requests complete. Returns the listen socket file descriptor (the caller can close it), or `-1` on error. +* _**bool** webserver::run():_ Run the webserver's event loop once (non-blocking). For use with external event loops when the server is started without internal threading. Returns `true` on success. +* _**bool** webserver::run_wait(**int32_t** millisec):_ Run the webserver's event loop, blocking until there is activity or the timeout expires. Pass `-1` for indefinite wait. Returns `true` on success. +* _**bool** webserver::get_fdset(**fd_set*** read_fd_set, **fd_set*** write_fd_set, **fd_set*** except_fd_set, **int*** max_fd):_ Get the file descriptor sets for `select()`-based external event loop integration. Returns `true` on success. +* _**bool** webserver::get_timeout(**uint64_t*** timeout):_ Get the timeout (in milliseconds) until the next daemon action is needed. Returns `true` if a timeout was set, `false` if no timeout is needed. +* _**bool** webserver::add_connection(**int** client_socket, **const struct sockaddr*** addr, **socklen_t** addrlen):_ Add an externally-accepted socket connection to the daemon. Useful with `no_listen_socket()`. Returns `true` on success. +* _**int** webserver::get_listen_fd():_ Get the listen socket file descriptor, or `-1` if not available. +* _**unsigned int** webserver::get_active_connections():_ Get the number of currently active connections. +* _**uint16_t** webserver::get_bound_port():_ Get the actual port the daemon is bound to. Particularly useful when port `0` was specified to let the OS choose an ephemeral port. [Back to TOC](#table-of-contents) @@ -806,15 +843,23 @@ You can also check this example on [github](https://github.com/etr/libhttpserver ## Building responses to requests As seen in the documentation of [http_resource](#the-resource-object), every extensible method returns in output a `http_response` object. The webserver takes the responsibility to convert the `http_response` object you create into a response on the network. -There are 5 types of response that you can create - we will describe them here through their constructors: +There are 8 types of response that you can create - we will describe them here through their constructors: * _string_response(**const std::string&** content, **int** response_code = `200`, **const std::string&** content_type = `"text/plain"`):_ The most basic type of response. It uses the `content` string passed in construction as body of the HTTP response. The other two optional parameters are the `response_code` and the `content_type`. You can find constant definition for the various response codes within the [http_utils](https://github.com/etr/libhttpserver/blob/master/src/httpserver/http_utils.hpp) library file. * _file_response(**const std::string&** filename, **int** response_code = `200`, **const std::string&** content_type = `"text/plain"`):_ Uses the `filename` passed in construction as pointer to a file on disk. The body of the HTTP response will be set using the content of the file. The file must be a regular file and exist on disk. Otherwise libhttpserver will return an error 500 (Internal Server Error). The other two optional parameters are the `response_code` and the `content_type`. You can find constant definition for the various response codes within the [http_utils](https://github.com/etr/libhttpserver/blob/master/src/httpserver/http_utils.hpp) library file. -* _basic_auth_fail_response(**const std::string&** content, **const std::string&** realm = `""`, **int** response_code = `200`, **const std::string&** content_type = `"text/plain"`):_ A response in return to a failure during basic authentication. It allows to specify a `content` string as a message to send back to the client. The `realm` parameter should contain your realm of authentication (if any). The other two optional parameters are the `response_code` and the `content_type`. You can find constant definition for the various response codes within the [http_utils](https://github.com/etr/libhttpserver/blob/master/src/httpserver/http_utils.hpp) library file. -* _digest_auth_fail_response(**const std::string&** content, **const std::string&** realm = `""`, **const std::string&** opaque = `""`, **bool** reload_nonce = `false`, **int** response_code = `200`, **const std::string&** content_type = `"text/plain"`):_ A response in return to a failure during digest authentication. It allows to specify a `content` string as a message to send back to the client. The `realm` parameter should contain your realm of authentication (if any). The `opaque` represents a value that gets passed to the client and expected to be passed again to the server as-is. This value can be a hexadecimal or base64 string. The `reload_nonce` parameter tells the server to reload the nonce (you should use the value returned by the `check_digest_auth` method on the `http_request`. The other two optional parameters are the `response_code` and the `content_type`. You can find constant definition for the various response codes within the [http_utils](https://github.com/etr/libhttpserver/blob/master/src/httpserver/http_utils.hpp) library file. +* _basic_auth_fail_response(**const std::string&** content, **const std::string&** realm = `""`, **bool** prefer_utf8 = `true`, **int** response_code = `200`, **const std::string&** content_type = `"text/plain"`):_ A response in return to a failure during basic authentication. It allows to specify a `content` string as a message to send back to the client. The `realm` parameter should contain your realm of authentication (if any). The `prefer_utf8` parameter controls whether UTF-8 encoding is preferred in the `WWW-Authenticate` header. The other two optional parameters are the `response_code` and the `content_type`. You can find constant definition for the various response codes within the [http_utils](https://github.com/etr/libhttpserver/blob/master/src/httpserver/http_utils.hpp) library file. +* _digest_auth_fail_response(**const std::string&** content, **const std::string&** realm = `""`, **const std::string&** opaque = `""`, **bool** signal_stale = `false`, **int** response_code = `200`, **const std::string&** content_type = `"text/plain"`, **http::http_utils::digest_algorithm** algorithm = `SHA256`, **const std::string&** domain = `""`, **bool** userhash_support = `false`, **bool** prefer_utf8 = `true`):_ A response in return to a failure during digest authentication. It allows to specify a `content` string as a message to send back to the client. The `realm` parameter should contain your realm of authentication (if any). The `opaque` represents a value that gets passed to the client and expected to be passed again to the server as-is. The `signal_stale` parameter indicates whether to signal the client that its nonce is stale and should be refreshed (set to `true` when `check_digest_auth` returns `NONCE_STALE`). The `algorithm` selects the digest algorithm (`MD5`, `SHA256`, or `SHA512_256` — default is `SHA256`). The `domain` specifies the protection domain for digest authentication. The `userhash_support` enables RFC 7616 userhash support. The `prefer_utf8` controls whether UTF-8 encoding is preferred. * _deferred_response(**ssize_t(*cycle_callback_ptr)(shared_ptr<T>, char*, size_t)** cycle_callback, **const std::string&** content = `""`, **int** response_code = `200`, **const std::string&** content_type = `"text/plain"`):_ A response that obtains additional content from a callback executed in a deferred way. It leaves the client in pending state (returning a `100 CONTINUE` message) and suspends the connection. Besides the callback, optionally, you can provide a `content` parameter that sets the initial message sent immediately to the client. The other two optional parameters are the `response_code` and the `content_type`. You can find constant definition for the various response codes within the [http_utils](https://github.com/etr/libhttpserver/blob/master/src/httpserver/http_utils.hpp) library file. To use `deferred_response` you need to have the `deferred` option active on your webserver (enabled by default). * The `cycle_callback_ptr` has this shape: _**ssize_t** cycle_callback(**shared_ptr<T> closure_data, char*** buf, **size_t** max_size)_. You are supposed to implement a function in this shape and provide it to the `deferred_repsonse` method. The webserver will provide a `char*` to the function. It is responsibility of the function to allocate it and fill its content. The method is supposed to respect the `max_size` parameter passed in input. The function must return a `ssize_t` value representing the actual size you filled the `buf` with. Any value different from `-1` will keep the resume the connection, deliver the content and suspend it again (with a `100 CONTINUE`). If the method returns `-1`, the webserver will complete the communication with the client and close the connection. You can also pass a `shared_ptr` pointing to a data object of your choice (this will be templetized with a class of your choice). The server will guarantee that this object is passed at each invocation of the method allowing the client code to use it as a memory buffer during computation. +* _empty_response(**int** response_code = `204`, **int** flags = `NONE`):_ A response with no body. Ideal for `204 No Content` or `304 Not Modified` responses. The `flags` parameter supports the following values: + * `NONE`: No special flags (default). + * `HTTP_1_0_COMPATIBLE_STRICT`: Ensure strict HTTP 1.0 compatibility. + * `HTTP_1_0_SERVER`: Behave like an HTTP 1.0 server. + * `SEND_KEEP_ALIVE_HEADER`: Include a `Keep-Alive` header. + * `HEAD_ONLY`: Produce a response suitable for a HEAD request (headers only). +* _iovec_response(**std::vector** buffers, **int** response_code = `200`, **const std::string&** content_type = `"text/plain"`):_ A scatter-gather response that assembles its body from multiple string buffers. This allows you to efficiently compose a response from separate data segments without concatenating them first. The `buffers` are sent in order. +* _pipe_response(**int** pipe_fd, **int** response_code = `200`, **const std::string&** content_type = `"application/octet-stream"`):_ A response that streams content from a pipe file descriptor. The daemon reads data from the pipe until EOF and sends it to the client. The pipe should be the read end of a `pipe()` call. This is useful for streaming output from subprocesses or other producers. ### Setting additional properties of the response The `http_response` class offers an additional set of methods to "decorate" your responses. This set of methods is: @@ -920,7 +965,7 @@ libhttpserver support three types of client authentication. Basic authentication uses a simple authentication method based on BASE64 algorithm. Username and password are exchanged in clear between the client and the server, so this method must only be used for non-sensitive content or when the session is protected with https. When using basic authentication libhttpserver will have access to the clear password, possibly allowing to create a chained authentication toward an external authentication server. You can enable/disable support for Basic authentication through the `basic_auth` and `no_basic_auth` methods of the `create_webserver` class. -Digest authentication uses a one-way authentication method based on MD5 hash algorithm. Only the hash will transit over the network, hence protecting the user password. The nonce will prevent replay attacks. This method is appropriate for general use, especially when https is not used to encrypt the session. You can enable/disable support for Digest authentication through the `digest_auth` and `no_digest_auth` methods of the `create_webserver` class. +Digest authentication uses a one-way authentication method based on hash algorithms (MD5, SHA-256, or SHA-512/256). Only the hash will transit over the network, hence protecting the user password. The nonce will prevent replay attacks. This method is appropriate for general use, especially when https is not used to encrypt the session. SHA-256 is the default algorithm; SHA-512/256 is also available for stronger security. You can enable/disable support for Digest authentication through the `digest_auth` and `no_digest_auth` methods of the `create_webserver` class. Client certificate authentication uses a X.509 certificate from the client. This is the strongest authentication mechanism but it requires the use of HTTPS. Client certificate authentication can be used simultaneously with Basic or Digest Authentication in order to provide a two levels authentication (like for instance separate machine and user authentication). You can enable/disable support for Certificate authentication through the `use_ssl` and `no_ssl` methods of the `create_webserver` class. @@ -959,26 +1004,40 @@ You will receive back the user and password you passed in input. Try to pass the You can also check this example on [github](https://github.com/etr/libhttpserver/blob/master/examples/basic_authentication.cpp). ### Using Digest Authentication + +The `check_digest_auth` method returns a `digest_auth_result` enum with fine-grained status codes: +* `OK` — authentication succeeded. +* `NONCE_STALE` — the nonce is stale; signal the client to retry with a fresh nonce by setting `signal_stale` to `true` in the response. +* `WRONG_USERNAME`, `WRONG_REALM`, `WRONG_URI`, `WRONG_QOP`, `WRONG_ALGO`, `RESPONSE_WRONG` — specific reasons for authentication failure. +* `WRONG_HEADER`, `TOO_LARGE`, `NONCE_WRONG`, `NONCE_OTHER_COND`, `ERROR` — other failure conditions. + +You can also use `check_digest_auth_digest` to verify against a pre-computed HA1 digest instead of a plaintext password. + ```cpp #include #define MY_OPAQUE "11733b200778ce33060f31c9af70a870ba96ddd4" using namespace httpserver; + using http::http_utils; class digest_resource : public httpserver::http_resource { public: std::shared_ptr render_GET(const http_request& req) { if (req.get_digested_user() == "") { - return std::shared_ptr(new digest_auth_fail_response("FAIL", "test@example.com", MY_OPAQUE, true)); - } - else { - bool reload_nonce = false; - if(!req.check_digest_auth("test@example.com", "mypass", 300, reload_nonce)) { - return std::shared_ptr(new digest_auth_fail_response("FAIL", "test@example.com", MY_OPAQUE, reload_nonce)); + return std::make_shared("FAIL", "test@example.com", MY_OPAQUE, true, + http_utils::http_ok, http_utils::text_plain, http_utils::digest_algorithm::MD5); + } else { + auto result = req.check_digest_auth("test@example.com", "mypass", 300, 0, http_utils::digest_algorithm::MD5); + if (result == http_utils::digest_auth_result::NONCE_STALE) { + return std::make_shared("FAIL", "test@example.com", MY_OPAQUE, true, + http_utils::http_ok, http_utils::text_plain, http_utils::digest_algorithm::MD5); + } else if (result != http_utils::digest_auth_result::OK) { + return std::make_shared("FAIL", "test@example.com", MY_OPAQUE, false, + http_utils::http_ok, http_utils::text_plain, http_utils::digest_algorithm::MD5); } } - return std::shared_ptr(new string_response("SUCCESS", 200, "text/plain")); + return std::make_shared("SUCCESS", 200, "text/plain"); } }; @@ -1190,13 +1249,117 @@ To use SNI with libhttpserver, configure an SNI callback that returns the certif } ``` -Note: SNI support requires libmicrohttpd 0.9.71 or later compiled with GnuTLS. +Note: SNI support requires libmicrohttpd 1.0.0 or later compiled with GnuTLS. + +[Back to TOC](#table-of-contents) + +## WebSocket Support + +libhttpserver provides WebSocket support when libmicrohttpd is built with WebSocket functionality. To use WebSockets, derive from the `websocket_handler` class and implement the `on_message()` method. + +### The websocket_handler class +The `websocket_handler` class provides the following virtual methods: +* _**void** on_open(**websocket_session&** session):_ Called when a new WebSocket connection is established. Default implementation does nothing. +* _**void** on_message(**websocket_session&** session, **std::string_view** msg):_ Called when a text message is received. **This is the only pure virtual method and must be implemented.** +* _**void** on_binary(**websocket_session&** session, **const void*** data, **size_t** len):_ Called when a binary message is received. Default implementation does nothing. +* _**void** on_ping(**websocket_session&** session, **std::string_view** payload):_ Called when a ping frame is received. Default implementation sends a pong. +* _**void** on_close(**websocket_session&** session, **uint16_t** code, **const std::string&** reason):_ Called when the WebSocket connection is closed. Default implementation does nothing. + +### The websocket_session class +The `websocket_session` class provides methods to interact with the client: +* _**void** send_text(**const std::string&** msg):_ Send a text message. +* _**void** send_binary(**const void*** data, **size_t** len):_ Send a binary message. +* _**void** send_ping(**const std::string&** payload = `""`):_ Send a ping frame. +* _**void** send_pong(**const std::string&** payload = `""`):_ Send a pong frame. +* _**void** close(**uint16_t** code = `1000`, **const std::string&** reason = `""`):_ Close the WebSocket connection. +* _**bool** is_valid():_ Check if the session is still valid. + +### Registering WebSocket resources +Register a WebSocket handler using `register_ws_resource`: +```cpp + #include + + using namespace httpserver; + + class echo_handler : public websocket_handler { + public: + void on_message(websocket_session& session, std::string_view msg) override { + session.send_text("Echo: " + std::string(msg)); + } + }; + + int main() { + webserver ws = create_webserver(8080); + + echo_handler handler; + ws.register_ws_resource("/ws", &handler); + ws.start(true); + + return 0; + } +``` + +Note: WebSocket support requires libmicrohttpd 1.0.0 or later built with WebSocket support enabled. + +[Back to TOC](#table-of-contents) + +## Daemon Introspection and External Event Loops + +libhttpserver exposes several methods for integrating with external event loops and for querying daemon state at runtime. + +### Daemon introspection +* _**uint16_t** webserver::get_bound_port():_ Returns the actual port the daemon is bound to. This is especially useful when you pass port `0` to let the operating system choose an ephemeral port. +* _**int** webserver::get_listen_fd():_ Returns the listen socket file descriptor, or `-1` if not available. +* _**unsigned int** webserver::get_active_connections():_ Returns the number of currently active connections. + +### External event loop integration +When using the server without internal threading (e.g., with `no_listen_socket()` or a single-threaded design), you can drive the event loop yourself: +* _**bool** webserver::run():_ Process pending events once and return immediately. +* _**bool** webserver::run_wait(**int32_t** millisec):_ Block until events are available or the timeout expires. +* _**bool** webserver::get_fdset(...):_ Retrieve file descriptor sets for use with `select()`. +* _**bool** webserver::get_timeout(**uint64_t*** timeout):_ Get the maximum time to wait before calling `run()` again. +* _**bool** webserver::add_connection(**int** socket, **const sockaddr*** addr, **socklen_t** len):_ Hand off an externally-accepted connection to the daemon. +* _**int** webserver::quiesce():_ Stop accepting new connections while allowing in-flight requests to complete. Returns the listen socket FD. + +### Example: querying the bound port +```cpp + #include + #include + + using namespace httpserver; + + class hello_resource : public http_resource { + public: + std::shared_ptr render(const http_request&) { + return std::make_shared("Hello!"); + } + }; + + int main() { + webserver ws = create_webserver(0); // Let the OS choose a port + + hello_resource hr; + ws.register_resource("/hello", &hr); + ws.start(false); + + std::cout << "Listening on port: " << ws.get_bound_port() << std::endl; + std::cout << "Active connections: " << ws.get_active_connections() << std::endl; + + ws.stop(); + return 0; + } +``` [Back to TOC](#table-of-contents) ## HTTP Utils libhttpserver provides a set of constants to help you develop your HTTP server. It would be redundant to list them here; so, please, consult the list directly [here](https://github.com/etr/libhttpserver/blob/master/src/httpserver/http_utils.hpp). +Additionally, the following utility methods are available: +* _**static const char*** http_utils::reason_phrase(**unsigned int** status_code):_ Returns the standard HTTP reason phrase for a given status code (e.g., `"OK"` for 200, `"Not Found"` for 404). +* _**static bool** http_utils::is_feature_supported(**int** feature):_ Checks whether a specific libmicrohttpd feature is supported on the current system. Feature constants are defined by the MHD_FEATURE enum. +* _**static const char*** http_utils::get_mhd_version():_ Returns the version string of the underlying libmicrohttpd library. + [Back to TOC](#table-of-contents) ## Other Examples @@ -1331,6 +1494,297 @@ To test the above example, you can run the following command from a terminal: You can also check this example on [github](https://github.com/etr/libhttpserver/blob/master/examples/deferred_with_accumulator.cpp). +#### Example of an empty response (204 No Content) +```cpp + #include + + using namespace httpserver; + + class no_content_resource : public http_resource { + public: + std::shared_ptr render_DELETE(const http_request&) { + // Return a 204 No Content response with no body + return std::make_shared( + http::http_utils::http_no_content); + } + + std::shared_ptr render_HEAD(const http_request&) { + // Return a HEAD-only response with headers but no body + auto response = std::make_shared( + http::http_utils::http_ok, + empty_response::HEAD_ONLY); + response->with_header("X-Total-Count", "42"); + return response; + } + }; + + int main() { + webserver ws = create_webserver(8080); + + no_content_resource ncr; + ws.register_resource("/items", &ncr); + ws.start(true); + + return 0; + } +``` +To test the above example, you can run the following commands from a terminal: + + curl -XDELETE -v localhost:8080/items + curl -I -v localhost:8080/items + +You can also check this example on [github](https://github.com/etr/libhttpserver/blob/master/examples/empty_response_example.cpp). + +#### Example of a scatter-gather (iovec) response +```cpp + #include + + using namespace httpserver; + + class iovec_resource : public http_resource { + public: + std::shared_ptr render_GET(const http_request&) { + // Build a response from multiple separate buffers without copying + std::vector parts; + parts.push_back("{\"header\": \"value\", "); + parts.push_back("\"items\": [1, 2, 3], "); + parts.push_back("\"footer\": \"end\"}"); + + return std::make_shared( + std::move(parts), 200, "application/json"); + } + }; + + int main() { + webserver ws = create_webserver(8080); + + iovec_resource ir; + ws.register_resource("/data", &ir); + ws.start(true); + + return 0; + } +``` +To test the above example, you can run the following command from a terminal: + + curl -XGET -v localhost:8080/data + +You can also check this example on [github](https://github.com/etr/libhttpserver/blob/master/examples/iovec_response_example.cpp). + +#### Example of a pipe-based streaming response +```cpp + #include + #include + #include + #include + + using namespace httpserver; + + class pipe_resource : public http_resource { + public: + std::shared_ptr render_GET(const http_request&) { + int pipefd[2]; + if (pipe(pipefd) == -1) { + return std::make_shared("pipe failed", 500); + } + + // Spawn a thread to write data into the pipe + std::thread writer([fd = pipefd[1]]() { + const char* messages[] = {"Hello ", "from ", "a pipe!\n"}; + for (const char* msg : messages) { + ssize_t ret = write(fd, msg, strlen(msg)); + (void)ret; + } + close(fd); + }); + writer.detach(); + + // Return the read end of the pipe as the response + return std::make_shared(pipefd[0], 200, "text/plain"); + } + }; + + int main() { + webserver ws = create_webserver(8080); + + pipe_resource pr; + ws.register_resource("/stream", &pr); + ws.start(true); + + return 0; + } +``` +To test the above example, you can run the following command from a terminal: + + curl -XGET -v localhost:8080/stream + +You can also check this example on [github](https://github.com/etr/libhttpserver/blob/master/examples/pipe_response_example.cpp). + +#### Example of a WebSocket echo server +```cpp + #include + #include + + using namespace httpserver; + + class echo_handler : public websocket_handler { + public: + void on_open(websocket_session& session) override { + std::cout << "WebSocket connection opened" << std::endl; + session.send_text("Welcome to the echo server!"); + } + + void on_message(websocket_session& session, std::string_view msg) override { + std::cout << "Received: " << msg << std::endl; + session.send_text("Echo: " + std::string(msg)); + } + + void on_close(websocket_session& session, uint16_t code, const std::string& reason) override { + std::cout << "WebSocket closed (code=" << code << ", reason=" << reason << ")" << std::endl; + } + }; + + int main() { + webserver ws = create_webserver(8080); + + echo_handler handler; + ws.register_ws_resource("/ws", &handler); + ws.start(true); + + return 0; + } +``` +Note: WebSocket support requires libmicrohttpd 1.0.0 built with WebSocket support. You can test this with any WebSocket client library or browser JavaScript: `new WebSocket("ws://localhost:8080/ws")`. + +You can also check this example on [github](https://github.com/etr/libhttpserver/blob/master/examples/websocket_echo.cpp). + +#### Example of daemon introspection +```cpp + #include + #include + + using namespace httpserver; + + class hello_resource : public http_resource { + public: + std::shared_ptr render_GET(const http_request&) { + return std::make_shared("Hello, World!"); + } + }; + + int main() { + // Use port 0 to let the OS assign an ephemeral port + webserver ws = create_webserver(0); + + hello_resource hr; + ws.register_resource("/hello", &hr); + ws.start(false); + + // Query daemon information + std::cout << "libmicrohttpd version: " + << http::http_utils::get_mhd_version() << std::endl; + std::cout << "Bound port: " << ws.get_bound_port() << std::endl; + std::cout << "Listen FD: " << ws.get_listen_fd() << std::endl; + std::cout << "Active connections: " << ws.get_active_connections() << std::endl; + std::cout << "HTTP 200 reason: " + << http::http_utils::reason_phrase(200) << std::endl; + std::cout << "HTTP 404 reason: " + << http::http_utils::reason_phrase(404) << std::endl; + + ws.sweet_kill(); + return 0; + } +``` +You can also check this example on [github](https://github.com/etr/libhttpserver/blob/master/examples/daemon_info.cpp). + +#### Example of an external event loop +```cpp + #include + #include + #include + + using namespace httpserver; + + static volatile bool running = true; + + void signal_handler(int) { running = false; } + + class hello_resource : public http_resource { + public: + std::shared_ptr render_GET(const http_request&) { + return std::make_shared("Hello from external event loop!"); + } + }; + + int main() { + signal(SIGINT, signal_handler); + + webserver ws = create_webserver(8080); + + hello_resource hr; + ws.register_resource("/hello", &hr); + ws.start(false); + + std::cout << "Server running on port " << ws.get_bound_port() << std::endl; + + // Drive the event loop externally using run_wait + while (running) { + // Block for up to 1000ms waiting for HTTP activity + ws.run_wait(1000); + + // You can do other work here between iterations + } + + // Graceful shutdown: stop accepting new connections first + ws.quiesce(); + ws.stop(); + + return 0; + } +``` +To test the above example, you can run the following command from a terminal: + + curl -XGET -v localhost:8080/hello + +You can also check this example on [github](https://github.com/etr/libhttpserver/blob/master/examples/external_event_loop.cpp). + +#### Example of turbo mode with performance options +```cpp + #include + + using namespace httpserver; + + class hello_resource : public http_resource { + public: + std::shared_ptr render_GET(const http_request&) { + return std::make_shared("Hello, turbo world!"); + } + }; + + int main() { + // Create a high-performance server with turbo mode, + // suppressed date headers, and a thread pool. + webserver ws = create_webserver(8080) + .start_method(http::http_utils::INTERNAL_SELECT) + .max_threads(4) + .turbo() + .suppress_date_header() + .tcp_fastopen_queue_size(16) + .listen_backlog(128); + + hello_resource hr; + ws.register_resource("/hello", &hr); + ws.start(true); + + return 0; + } +``` +To test the above example, you can run the following command from a terminal: + + curl -XGET -v localhost:8080/hello + +You can also check this example on [github](https://github.com/etr/libhttpserver/blob/master/examples/turbo_mode.cpp). + [Back to TOC](#table-of-contents) ## Copying diff --git a/configure.ac b/configure.ac index 003170c6..4069589d 100644 --- a/configure.ac +++ b/configure.ac @@ -108,18 +108,18 @@ AC_CHECK_HEADER([gnutls/gnutls.h],[have_gnutls="yes"],[AC_MSG_WARN("gnutls/gnutl # Checks for libmicrohttpd if test x"$host" = x"$build"; then AC_CHECK_HEADER([microhttpd.h], - AC_CHECK_LIB([microhttpd], [MHD_get_fdset2], - [AC_MSG_CHECKING([for libmicrohttpd >= 0.9.64]) + AC_CHECK_LIB([microhttpd], [MHD_start_daemon], + [AC_MSG_CHECKING([for libmicrohttpd >= 1.0.0]) AC_COMPILE_IFELSE( [AC_LANG_SOURCE([ #include - #if (MHD_VERSION < 0x00096400) - #error needs at least version 0.9.64 + #if (MHD_VERSION < 0x01000000) + #error needs at least version 1.0.0 #endif int main () { return 0; } ])], [], - [AC_MSG_ERROR("libmicrohttpd is too old - install libmicrohttpd >= 0.9.64")] + [AC_MSG_ERROR("libmicrohttpd is too old - install libmicrohttpd >= 1.0.0")] ) ], [AC_MSG_ERROR(["libmicrohttpd not found"])] @@ -133,7 +133,7 @@ if test x"$host" = x"$build"; then cond_cross_compile="no" else AC_CHECK_HEADER([microhttpd.h], - AC_CHECK_LIB([microhttpd], [MHD_get_fdset2], + AC_CHECK_LIB([microhttpd], [MHD_start_daemon], [], [AC_MSG_ERROR(["libmicrohttpd not found"])] ), @@ -149,15 +149,22 @@ fi AM_CONDITIONAL([COND_CROSS_COMPILE],[test x"$cond_cross_compile" = x"yes"]) AC_SUBST(COND_CROSS_COMPILE) -# Check for basic auth support in libmicrohttpd -AC_CHECK_LIB([microhttpd], [MHD_queue_basic_auth_fail_response], +# Check for basic auth v3 support in libmicrohttpd +AC_CHECK_LIB([microhttpd], [MHD_basic_auth_get_username_password3], [have_bauth="yes"], - [have_bauth="no"; AC_MSG_WARN("libmicrohttpd basic auth support not found. Basic auth will be disabled")]) + [have_bauth="no"; AC_MSG_WARN("libmicrohttpd basic auth v3 support not found. Basic auth will be disabled")]) -# Check for digest auth support in libmicrohttpd -AC_CHECK_LIB([microhttpd], [MHD_queue_auth_fail_response], +# Check for digest auth v3 support in libmicrohttpd +AC_CHECK_LIB([microhttpd], [MHD_digest_auth_check3], [have_dauth="yes"], - [have_dauth="no"; AC_MSG_WARN("libmicrohttpd digest auth support not found. Digest auth will be disabled")]) + [have_dauth="no"; AC_MSG_WARN("libmicrohttpd digest auth v3 support not found. Digest auth will be disabled")]) + +# Check for WebSocket support in libmicrohttpd_ws +AC_CHECK_HEADER([microhttpd_ws.h], + [AC_CHECK_LIB([microhttpd_ws], [MHD_websocket_stream_init], + [have_websocket="yes"], + [have_websocket="no"; AC_MSG_WARN("libmicrohttpd_ws not found. WebSocket support will be disabled")])], + [have_websocket="no"; AC_MSG_WARN("microhttpd_ws.h not found. WebSocket support will be disabled")]) AC_MSG_CHECKING([whether to build with TCP_FASTOPEN support]) AC_ARG_ENABLE([fastopen], @@ -283,6 +290,14 @@ fi AM_CONDITIONAL([HAVE_DAUTH],[test x"$have_dauth" = x"yes"]) +if test x"$have_websocket" = x"yes"; then + AM_CXXFLAGS="$AM_CXXFLAGS -DHAVE_WEBSOCKET" + AM_CFLAGS="$AM_CXXFLAGS -DHAVE_WEBSOCKET" + LHT_LIBDEPS="$LHT_LIBDEPS -lmicrohttpd_ws" +fi + +AM_CONDITIONAL([HAVE_WEBSOCKET],[test x"$have_websocket" = x"yes"]) + DX_HTML_FEATURE(ON) DX_CHM_FEATURE(OFF) DX_CHI_FEATURE(OFF) @@ -341,6 +356,7 @@ AC_MSG_NOTICE([Configuration Summary: TLS Enabled : ${have_gnutls} Basic Auth : ${have_bauth} Digest Auth : ${have_dauth} + WebSocket : ${have_websocket} TCP_FASTOPEN : ${is_fastopen_supported} Static : ${static} Windows build : ${is_windows} diff --git a/examples/Makefile.am b/examples/Makefile.am index c04e0acf..cc7d7e45 100644 --- a/examples/Makefile.am +++ b/examples/Makefile.am @@ -19,7 +19,7 @@ LDADD = $(top_builddir)/src/libhttpserver.la AM_CPPFLAGS = -I$(top_srcdir)/src -I$(top_srcdir)/src/httpserver/ METASOURCES = AUTO -noinst_PROGRAMS = hello_world service minimal_hello_world custom_error allowing_disallowing_methods handlers hello_with_get_arg args_processing setting_headers custom_access_log minimal_https minimal_file_response minimal_deferred url_registration minimal_ip_ban benchmark_select benchmark_threads benchmark_nodelay deferred_with_accumulator file_upload file_upload_with_callback +noinst_PROGRAMS = hello_world service minimal_hello_world custom_error allowing_disallowing_methods handlers hello_with_get_arg args_processing setting_headers custom_access_log minimal_https minimal_file_response minimal_deferred url_registration minimal_ip_ban benchmark_select benchmark_threads benchmark_nodelay deferred_with_accumulator file_upload file_upload_with_callback empty_response_example iovec_response_example pipe_response_example daemon_info external_event_loop turbo_mode hello_world_SOURCES = hello_world.cpp service_SOURCES = service.cpp @@ -42,6 +42,12 @@ benchmark_threads_SOURCES = benchmark_threads.cpp benchmark_nodelay_SOURCES = benchmark_nodelay.cpp file_upload_SOURCES = file_upload.cpp file_upload_with_callback_SOURCES = file_upload_with_callback.cpp +empty_response_example_SOURCES = empty_response_example.cpp +iovec_response_example_SOURCES = iovec_response_example.cpp +pipe_response_example_SOURCES = pipe_response_example.cpp +daemon_info_SOURCES = daemon_info.cpp +external_event_loop_SOURCES = external_event_loop.cpp +turbo_mode_SOURCES = turbo_mode.cpp if HAVE_BAUTH noinst_PROGRAMS += basic_authentication centralized_authentication @@ -59,3 +65,9 @@ if HAVE_DAUTH noinst_PROGRAMS += digest_authentication digest_authentication_SOURCES = digest_authentication.cpp endif + +if HAVE_WEBSOCKET +noinst_PROGRAMS += websocket_echo +websocket_echo_SOURCES = websocket_echo.cpp +websocket_echo_LDADD = $(LDADD) -lmicrohttpd_ws +endif diff --git a/examples/daemon_info.cpp b/examples/daemon_info.cpp new file mode 100644 index 00000000..c854bbac --- /dev/null +++ b/examples/daemon_info.cpp @@ -0,0 +1,59 @@ +/* + 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 +#include + +#include + +class hello_resource : public httpserver::http_resource { + public: + std::shared_ptr render_GET(const httpserver::http_request&) { + return std::make_shared("Hello, World!"); + } +}; + +int main() { + // Use port 0 to let the OS assign an ephemeral port + httpserver::webserver ws = httpserver::create_webserver(0); + + hello_resource hr; + ws.register_resource("/hello", &hr); + ws.start(false); + + // Query daemon information + std::cout << "libmicrohttpd version: " + << httpserver::http::http_utils::get_mhd_version() << std::endl; + std::cout << "Bound port: " << ws.get_bound_port() << std::endl; + std::cout << "Listen FD: " << ws.get_listen_fd() << std::endl; + std::cout << "Active connections: " << ws.get_active_connections() << std::endl; + std::cout << "HTTP 200 reason: " + << httpserver::http::http_utils::reason_phrase(200) << std::endl; + std::cout << "HTTP 404 reason: " + << httpserver::http::http_utils::reason_phrase(404) << std::endl; + + std::cout << "\nServer running on port " << ws.get_bound_port() + << ". Press Ctrl+C to stop." << std::endl; + + // Block until interrupted + ws.sweet_kill(); + + return 0; +} diff --git a/examples/digest_authentication.cpp b/examples/digest_authentication.cpp index fb87cd4b..ddf0be77 100644 --- a/examples/digest_authentication.cpp +++ b/examples/digest_authentication.cpp @@ -27,15 +27,21 @@ class digest_resource : public httpserver::http_resource { public: std::shared_ptr render_GET(const httpserver::http_request& req) { + using httpserver::http::http_utils; if (req.get_digested_user() == "") { - return std::shared_ptr(new httpserver::digest_auth_fail_response("FAIL", "test@example.com", MY_OPAQUE, true)); + return std::make_shared("FAIL", "test@example.com", MY_OPAQUE, true, + http_utils::http_ok, http_utils::text_plain, http_utils::digest_algorithm::MD5); } else { - bool reload_nonce = false; - if (!req.check_digest_auth("test@example.com", "mypass", 300, &reload_nonce)) { - return std::shared_ptr(new httpserver::digest_auth_fail_response("FAIL", "test@example.com", MY_OPAQUE, reload_nonce)); + auto result = req.check_digest_auth("test@example.com", "mypass", 300, 0, http_utils::digest_algorithm::MD5); + if (result == http_utils::digest_auth_result::NONCE_STALE) { + return std::make_shared("FAIL", "test@example.com", MY_OPAQUE, true, + http_utils::http_ok, http_utils::text_plain, http_utils::digest_algorithm::MD5); + } else if (result != http_utils::digest_auth_result::OK) { + return std::make_shared("FAIL", "test@example.com", MY_OPAQUE, false, + http_utils::http_ok, http_utils::text_plain, http_utils::digest_algorithm::MD5); } } - return std::shared_ptr(new httpserver::string_response("SUCCESS", 200, "text/plain")); + return std::make_shared("SUCCESS", 200, "text/plain"); } }; diff --git a/examples/empty_response_example.cpp b/examples/empty_response_example.cpp new file mode 100644 index 00000000..17a4a443 --- /dev/null +++ b/examples/empty_response_example.cpp @@ -0,0 +1,51 @@ +/* + 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 + +#include + +class no_content_resource : public httpserver::http_resource { + public: + std::shared_ptr render_DELETE(const httpserver::http_request&) { + // Return a 204 No Content response with no body + return std::make_shared( + httpserver::http::http_utils::http_no_content); + } + + std::shared_ptr render_HEAD(const httpserver::http_request&) { + // Return a HEAD-only response with headers but no body + auto response = std::make_shared( + httpserver::http::http_utils::http_ok, + httpserver::empty_response::HEAD_ONLY); + response->with_header("X-Total-Count", "42"); + return response; + } +}; + +int main() { + httpserver::webserver ws = httpserver::create_webserver(8080); + + no_content_resource ncr; + ws.register_resource("/items", &ncr); + ws.start(true); + + return 0; +} diff --git a/examples/external_event_loop.cpp b/examples/external_event_loop.cpp new file mode 100644 index 00000000..031b8f24 --- /dev/null +++ b/examples/external_event_loop.cpp @@ -0,0 +1,67 @@ +/* + 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 +#include +#include +#include + +#include + +static volatile bool running = true; + +void signal_handler(int) { + running = false; +} + +class hello_resource : public httpserver::http_resource { + public: + std::shared_ptr render_GET(const httpserver::http_request&) { + return std::make_shared("Hello from external event loop!"); + } +}; + +int main() { + signal(SIGINT, signal_handler); + + httpserver::webserver ws = httpserver::create_webserver(8080); + + hello_resource hr; + ws.register_resource("/hello", &hr); + ws.start(false); + + std::cout << "Server running on port " << ws.get_bound_port() << std::endl; + + // Drive the event loop externally using run_wait + while (running) { + // Block for up to 1000ms waiting for HTTP activity + ws.run_wait(1000); + + // You can do other work here between iterations + } + + std::cout << "\nShutting down..." << std::endl; + + // Graceful shutdown: stop accepting new connections first + ws.quiesce(); + ws.stop(); + + return 0; +} diff --git a/examples/iovec_response_example.cpp b/examples/iovec_response_example.cpp new file mode 100644 index 00000000..781d9b33 --- /dev/null +++ b/examples/iovec_response_example.cpp @@ -0,0 +1,49 @@ +/* + 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 +#include +#include + +#include + +class iovec_resource : public httpserver::http_resource { + public: + std::shared_ptr render_GET(const httpserver::http_request&) { + // Build a response from multiple separate buffers without copying + std::vector parts; + parts.push_back("{\"header\": \"value\", "); + parts.push_back("\"items\": [1, 2, 3], "); + parts.push_back("\"footer\": \"end\"}"); + + return std::make_shared( + std::move(parts), 200, "application/json"); + } +}; + +int main() { + httpserver::webserver ws = httpserver::create_webserver(8080); + + iovec_resource ir; + ws.register_resource("/data", &ir); + ws.start(true); + + return 0; +} diff --git a/examples/pipe_response_example.cpp b/examples/pipe_response_example.cpp new file mode 100644 index 00000000..e99f3cb7 --- /dev/null +++ b/examples/pipe_response_example.cpp @@ -0,0 +1,60 @@ +/* + 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 +#include +#include +#include + +#include + +class pipe_resource : public httpserver::http_resource { + public: + std::shared_ptr render_GET(const httpserver::http_request&) { + int pipefd[2]; + if (pipe(pipefd) == -1) { + return std::make_shared("pipe failed", 500); + } + + // Spawn a thread to write data into the pipe + std::thread writer([fd = pipefd[1]]() { + const char* messages[] = {"Hello ", "from ", "a pipe!\n"}; + for (const char* msg : messages) { + ssize_t ret = write(fd, msg, strlen(msg)); + (void)ret; + } + close(fd); + }); + writer.detach(); + + // Return the read end of the pipe as the response + return std::make_shared(pipefd[0], 200, "text/plain"); + } +}; + +int main() { + httpserver::webserver ws = httpserver::create_webserver(8080); + + pipe_resource pr; + ws.register_resource("/stream", &pr); + ws.start(true); + + return 0; +} diff --git a/examples/turbo_mode.cpp b/examples/turbo_mode.cpp new file mode 100644 index 00000000..378eca97 --- /dev/null +++ b/examples/turbo_mode.cpp @@ -0,0 +1,48 @@ +/* + 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 + +#include + +class hello_resource : public httpserver::http_resource { + public: + std::shared_ptr render_GET(const httpserver::http_request&) { + return std::make_shared("Hello, turbo world!"); + } +}; + +int main() { + // Create a high-performance server with turbo mode, + // suppressed date headers, and a thread pool. + httpserver::webserver ws = httpserver::create_webserver(8080) + .start_method(httpserver::http::http_utils::INTERNAL_SELECT) + .max_threads(4) + .turbo() + .suppress_date_header() + .tcp_fastopen_queue_size(16) + .listen_backlog(128); + + hello_resource hr; + ws.register_resource("/hello", &hr); + ws.start(true); + + return 0; +} diff --git a/examples/websocket_echo.cpp b/examples/websocket_echo.cpp new file mode 100644 index 00000000..0e7f20fc --- /dev/null +++ b/examples/websocket_echo.cpp @@ -0,0 +1,52 @@ +/* + 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 +#include +#include + +#include + +class echo_handler : public httpserver::websocket_handler { + public: + void on_open(httpserver::websocket_session& session) override { + std::cout << "WebSocket connection opened" << std::endl; + session.send_text("Welcome to the echo server!"); + } + + void on_message(httpserver::websocket_session& session, std::string_view msg) override { + std::cout << "Received: " << msg << std::endl; + session.send_text("Echo: " + std::string(msg)); + } + + void on_close(httpserver::websocket_session& session, uint16_t code, const std::string& reason) override { + std::cout << "WebSocket closed (code=" << code << ", reason=" << reason << ")" << std::endl; + } +}; + +int main() { + httpserver::webserver ws = httpserver::create_webserver(8080); + + echo_handler handler; + ws.register_ws_resource("/ws", &handler); + ws.start(true); + + return 0; +} diff --git a/libhttpserver.pc.in b/libhttpserver.pc.in index aaf116af..305cc71e 100644 --- a/libhttpserver.pc.in +++ b/libhttpserver.pc.in @@ -6,7 +6,7 @@ includedir=@includedir@ Name: libhttpserver Description: A C++ library for creating an embedded Rest HTTP server Version: @VERSION@ -Requires: libmicrohttpd >= 0.9.52 +Requires: libmicrohttpd >= 1.0.0 Conflicts: Libs: -L${libdir} -lhttpserver Libs.private: @LHT_LIBDEPS@ diff --git a/src/Makefile.am b/src/Makefile.am index ed8dc8f4..a06fc171 100644 --- a/src/Makefile.am +++ b/src/Makefile.am @@ -19,15 +19,20 @@ AM_CPPFLAGS = -I../ -I$(srcdir)/httpserver/ METASOURCES = AUTO lib_LTLIBRARIES = libhttpserver.la -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 http_resource.cpp create_webserver.cpp details/http_endpoint.cpp +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 noinst_HEADERS = httpserver/string_utilities.hpp httpserver/details/modded_request.hpp gettext.h -nobase_include_HEADERS = httpserver.hpp httpserver/create_webserver.hpp httpserver/webserver.hpp httpserver/http_utils.hpp httpserver/file_info.hpp httpserver/details/http_endpoint.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/http_arg_value.hpp +nobase_include_HEADERS = httpserver.hpp httpserver/create_webserver.hpp httpserver/webserver.hpp httpserver/http_utils.hpp httpserver/file_info.hpp httpserver/details/http_endpoint.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/iovec_response.hpp httpserver/http_arg_value.hpp if HAVE_BAUTH libhttpserver_la_SOURCES += basic_auth_fail_response.cpp nobase_include_HEADERS += httpserver/basic_auth_fail_response.hpp endif +if HAVE_WEBSOCKET +libhttpserver_la_SOURCES += websocket_handler.cpp +nobase_include_HEADERS += httpserver/websocket_handler.hpp +endif + AM_CXXFLAGS += -fPIC -Wall if COND_GCOV @@ -38,6 +43,9 @@ endif if !COND_CROSS_COMPILE libhttpserver_la_LIBADD = -lmicrohttpd +if HAVE_WEBSOCKET +libhttpserver_la_LIBADD += -lmicrohttpd_ws +endif endif libhttpserver_la_CFLAGS = $(AM_CFLAGS) diff --git a/src/basic_auth_fail_response.cpp b/src/basic_auth_fail_response.cpp index 1e6aa0e5..ebf0c5d3 100644 --- a/src/basic_auth_fail_response.cpp +++ b/src/basic_auth_fail_response.cpp @@ -30,7 +30,7 @@ struct MHD_Response; namespace httpserver { int basic_auth_fail_response::enqueue_response(MHD_Connection* connection, MHD_Response* response) { - return MHD_queue_basic_auth_fail_response(connection, realm.c_str(), response); + return MHD_queue_basic_auth_required_response3(connection, realm.c_str(), prefer_utf8 ? MHD_YES : MHD_NO, response); } } // namespace httpserver diff --git a/src/digest_auth_fail_response.cpp b/src/digest_auth_fail_response.cpp index 1fb8307c..934708fc 100644 --- a/src/digest_auth_fail_response.cpp +++ b/src/digest_auth_fail_response.cpp @@ -30,13 +30,17 @@ struct MHD_Response; namespace httpserver { int digest_auth_fail_response::enqueue_response(MHD_Connection* connection, MHD_Response* response) { - return MHD_queue_auth_fail_response2( + return MHD_queue_auth_required_response3( connection, realm.c_str(), opaque.c_str(), + domain.empty() ? nullptr : domain.c_str(), response, - reload_nonce ? MHD_YES : MHD_NO, - static_cast(algorithm)); + signal_stale ? MHD_YES : MHD_NO, + MHD_DIGEST_AUTH_MULT_QOP_ANY_NON_INT, + static_cast(algorithm), + userhash_support ? MHD_YES : MHD_NO, + prefer_utf8 ? MHD_YES : MHD_NO); } } // namespace httpserver diff --git a/src/empty_response.cpp b/src/empty_response.cpp new file mode 100644 index 00000000..52d6bc03 --- /dev/null +++ b/src/empty_response.cpp @@ -0,0 +1,32 @@ +/* + 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/empty_response.hpp" +#include + +struct MHD_Response; + +namespace httpserver { + +MHD_Response* empty_response::get_raw_response() { + return MHD_create_response_empty(static_cast(flags)); +} + +} // namespace httpserver diff --git a/src/http_request.cpp b/src/http_request.cpp index 4d67bf39..41c92061 100644 --- a/src/http_request.cpp +++ b/src/http_request.cpp @@ -71,6 +71,23 @@ class scoped_x509_cert { bool is_valid() const { return valid_; } gnutls_x509_crt_t get() const { return cert_; } + // Movable + scoped_x509_cert(scoped_x509_cert&& other) noexcept + : cert_(other.cert_), valid_(other.valid_) { + other.cert_ = nullptr; + other.valid_ = false; + } + scoped_x509_cert& operator=(scoped_x509_cert&& other) noexcept { + if (this != &other) { + if (cert_ != nullptr) gnutls_x509_crt_deinit(cert_); + cert_ = other.cert_; + valid_ = other.valid_; + other.cert_ = nullptr; + other.valid_ = false; + } + return *this; + } + // Non-copyable scoped_x509_cert(const scoped_x509_cert&) = delete; scoped_x509_cert& operator=(const scoped_x509_cert&) = delete; @@ -95,49 +112,48 @@ void http_request::set_method(const std::string& method) { } #ifdef HAVE_DAUTH -bool http_request::check_digest_auth(const std::string& realm, const std::string& password, int nonce_timeout, bool* reload_nonce) const { +http::http_utils::digest_auth_result http_request::check_digest_auth( + const std::string& realm, + const std::string& password, + unsigned int nonce_timeout, + uint32_t max_nc, + http::http_utils::digest_algorithm algo) const { std::string_view digested_user = get_digested_user(); - int val = MHD_digest_auth_check(underlying_connection, realm.c_str(), digested_user.data(), password.c_str(), nonce_timeout); + enum MHD_DigestAuthResult result = MHD_digest_auth_check3( + underlying_connection, + realm.c_str(), + digested_user.data(), + password.c_str(), + nonce_timeout, + max_nc, + MHD_DIGEST_AUTH_MULT_QOP_ANY_NON_INT, + static_cast(algo)); - if (val == MHD_INVALID_NONCE) { - *reload_nonce = true; - return false; - } else if (val == MHD_NO) { - *reload_nonce = false; - return false; - } - *reload_nonce = false; - return true; + return static_cast(result); } -bool http_request::check_digest_auth_ha1( +http::http_utils::digest_auth_result http_request::check_digest_auth_digest( const std::string& realm, - const unsigned char* digest, - size_t digest_size, - int nonce_timeout, - bool* reload_nonce, + const void* userdigest, + size_t userdigest_size, + unsigned int nonce_timeout, + uint32_t max_nc, http::http_utils::digest_algorithm algo) const { std::string_view digested_user = get_digested_user(); - int val = MHD_digest_auth_check_digest2( + enum MHD_DigestAuthResult result = MHD_digest_auth_check_digest3( underlying_connection, realm.c_str(), digested_user.data(), - digest, - digest_size, + userdigest, + userdigest_size, nonce_timeout, - static_cast(algo)); + max_nc, + MHD_DIGEST_AUTH_MULT_QOP_ANY_NON_INT, + static_cast(algo)); - if (val == MHD_INVALID_NONCE) { - *reload_nonce = true; - return false; - } else if (val == MHD_NO) { - *reload_nonce = false; - return false; - } - *reload_nonce = false; - return true; + return static_cast(result); } #endif // HAVE_DAUTH @@ -312,16 +328,14 @@ MHD_Result http_request::build_request_querystring(void *cls, enum MHD_ValueKind #ifdef HAVE_BAUTH void http_request::fetch_user_pass() const { - char* password = nullptr; - auto* username = MHD_basic_auth_get_username_password(underlying_connection, &password); + struct MHD_BasicAuthInfo* info = MHD_basic_auth_get_username_password3(underlying_connection); - if (username != nullptr) { - cache->username = username; - MHD_free(username); - } - if (password != nullptr) { - cache->password = password; - MHD_free(password); + if (info != nullptr) { + cache->username.assign(info->username, info->username_len); + if (info->password != nullptr) { + cache->password.assign(info->password, info->password_len); + } + MHD_free(info); } } @@ -348,12 +362,14 @@ std::string_view http_request::get_digested_user() const { return cache->digested_user; } - char* digested_user_c = MHD_digest_auth_get_username(underlying_connection); + struct MHD_DigestAuthUsernameInfo* info = MHD_digest_auth_get_username3(underlying_connection); cache->digested_user = EMPTY; - if (digested_user_c != nullptr) { - cache->digested_user = digested_user_c; - MHD_free(digested_user_c); + if (info != nullptr) { + if (info->username != nullptr) { + cache->digested_user.assign(info->username, info->username_len); + } + MHD_free(info); } return cache->digested_user; @@ -388,156 +404,126 @@ bool http_request::has_client_certificate() const { return (cert_list != nullptr && list_size > 0); } -std::string http_request::get_client_cert_dn() const { - if (!has_tls_session()) { - return ""; - } - - scoped_x509_cert cert; - if (!cert.init_from_session(get_tls_session())) { - return ""; - } - - size_t dn_size = 0; - gnutls_x509_crt_get_dn(cert.get(), nullptr, &dn_size); - - std::string dn(dn_size, '\0'); - if (gnutls_x509_crt_get_dn(cert.get(), &dn[0], &dn_size) != GNUTLS_E_SUCCESS) { - return ""; - } - - // Remove trailing null if present - if (!dn.empty() && dn.back() == '\0') { - dn.pop_back(); +void http_request::populate_all_cert_fields() const { + if (cache->client_cert_fields_cached) { + return; } - return dn; -} + cache->client_cert_fields_cached = true; -std::string http_request::get_client_cert_issuer_dn() const { - if (!has_tls_session()) { - return ""; + gnutls_session_t session = nullptr; + if (has_tls_session()) { + session = get_tls_session(); } scoped_x509_cert cert; - if (!cert.init_from_session(get_tls_session())) { - return ""; + if (session != nullptr) { + cert.init_from_session(session); } - size_t dn_size = 0; - gnutls_x509_crt_get_issuer_dn(cert.get(), nullptr, &dn_size); - - std::string dn(dn_size, '\0'); - if (gnutls_x509_crt_get_issuer_dn(cert.get(), &dn[0], &dn_size) != GNUTLS_E_SUCCESS) { - return ""; - } - - // Remove trailing null if present - if (!dn.empty() && dn.back() == '\0') { - dn.pop_back(); + if (!cert.is_valid()) { + // Default values (empty strings and -1) are already set by the + // cache struct initializers; client_cert_verified defaults to false. + return; } - return dn; -} - -std::string http_request::get_client_cert_cn() const { - if (!has_tls_session()) { - return ""; + // Client certificate verification + { + unsigned int status = 0; + if (gnutls_certificate_verify_peers2(session, &status) == GNUTLS_E_SUCCESS) { + cache->client_cert_verified = (status == 0); + } } - scoped_x509_cert cert; - if (!cert.init_from_session(get_tls_session())) { - return ""; + // Subject DN + { + size_t dn_size = 0; + gnutls_x509_crt_get_dn(cert.get(), nullptr, &dn_size); + std::string dn(dn_size, '\0'); + if (gnutls_x509_crt_get_dn(cert.get(), &dn[0], &dn_size) == GNUTLS_E_SUCCESS) { + if (!dn.empty() && dn.back() == '\0') dn.pop_back(); + cache->client_cert_dn = dn; + } } - size_t cn_size = 0; - gnutls_x509_crt_get_dn_by_oid(cert.get(), GNUTLS_OID_X520_COMMON_NAME, 0, 0, nullptr, &cn_size); - - if (cn_size == 0) { - return ""; + // Issuer DN + { + size_t dn_size = 0; + gnutls_x509_crt_get_issuer_dn(cert.get(), nullptr, &dn_size); + std::string dn(dn_size, '\0'); + if (gnutls_x509_crt_get_issuer_dn(cert.get(), &dn[0], &dn_size) == GNUTLS_E_SUCCESS) { + if (!dn.empty() && dn.back() == '\0') dn.pop_back(); + cache->client_cert_issuer_dn = dn; + } } - std::string cn(cn_size, '\0'); - if (gnutls_x509_crt_get_dn_by_oid(cert.get(), GNUTLS_OID_X520_COMMON_NAME, 0, 0, &cn[0], &cn_size) != GNUTLS_E_SUCCESS) { - return ""; + // Common Name + { + size_t cn_size = 0; + gnutls_x509_crt_get_dn_by_oid(cert.get(), GNUTLS_OID_X520_COMMON_NAME, 0, 0, nullptr, &cn_size); + if (cn_size > 0) { + std::string cn(cn_size, '\0'); + if (gnutls_x509_crt_get_dn_by_oid(cert.get(), GNUTLS_OID_X520_COMMON_NAME, 0, 0, &cn[0], &cn_size) == GNUTLS_E_SUCCESS) { + if (!cn.empty() && cn.back() == '\0') cn.pop_back(); + cache->client_cert_cn = cn; + } + } } - // Remove trailing null if present - if (!cn.empty() && cn.back() == '\0') { - cn.pop_back(); + // SHA-256 fingerprint + { + unsigned char fingerprint[32]; + size_t fingerprint_size = sizeof(fingerprint); + if (gnutls_x509_crt_get_fingerprint(cert.get(), GNUTLS_DIG_SHA256, fingerprint, &fingerprint_size) == GNUTLS_E_SUCCESS) { + std::string hex_fingerprint; + hex_fingerprint.reserve(fingerprint_size * 2); + for (size_t i = 0; i < fingerprint_size; ++i) { + char hex[3]; + snprintf(hex, sizeof(hex), "%02x", fingerprint[i]); + hex_fingerprint += hex; + } + cache->client_cert_fingerprint_sha256 = hex_fingerprint; + } } - return cn; + // Validity times + cache->client_cert_not_before = gnutls_x509_crt_get_activation_time(cert.get()); + cache->client_cert_not_after = gnutls_x509_crt_get_expiration_time(cert.get()); } -bool http_request::is_client_cert_verified() const { - if (!has_tls_session()) { - return false; - } +std::string http_request::get_client_cert_dn() const { + populate_all_cert_fields(); + return cache->client_cert_dn; +} - gnutls_session_t session = get_tls_session(); - unsigned int status = 0; +std::string http_request::get_client_cert_issuer_dn() const { + populate_all_cert_fields(); + return cache->client_cert_issuer_dn; +} - if (gnutls_certificate_verify_peers2(session, &status) != GNUTLS_E_SUCCESS) { - return false; - } +std::string http_request::get_client_cert_cn() const { + populate_all_cert_fields(); + return cache->client_cert_cn; +} - return (status == 0); +bool http_request::is_client_cert_verified() const { + populate_all_cert_fields(); + return cache->client_cert_verified; } std::string http_request::get_client_cert_fingerprint_sha256() const { - if (!has_tls_session()) { - return ""; - } - - scoped_x509_cert cert; - if (!cert.init_from_session(get_tls_session())) { - return ""; - } - - unsigned char fingerprint[32]; // SHA-256 is 32 bytes - size_t fingerprint_size = sizeof(fingerprint); - - if (gnutls_x509_crt_get_fingerprint(cert.get(), GNUTLS_DIG_SHA256, fingerprint, &fingerprint_size) != GNUTLS_E_SUCCESS) { - return ""; - } - - // Convert to hex string - std::string hex_fingerprint; - hex_fingerprint.reserve(fingerprint_size * 2); - for (size_t i = 0; i < fingerprint_size; ++i) { - char hex[3]; - snprintf(hex, sizeof(hex), "%02x", fingerprint[i]); - hex_fingerprint += hex; - } - - return hex_fingerprint; + populate_all_cert_fields(); + return cache->client_cert_fingerprint_sha256; } time_t http_request::get_client_cert_not_before() const { - if (!has_tls_session()) { - return -1; - } - - scoped_x509_cert cert; - if (!cert.init_from_session(get_tls_session())) { - return -1; - } - - return gnutls_x509_crt_get_activation_time(cert.get()); + populate_all_cert_fields(); + return cache->client_cert_not_before; } time_t http_request::get_client_cert_not_after() const { - if (!has_tls_session()) { - return -1; - } - - scoped_x509_cert cert; - if (!cert.init_from_session(get_tls_session())) { - return -1; - } - - return gnutls_x509_crt_get_expiration_time(cert.get()); + populate_all_cert_fields(); + return cache->client_cert_not_after; } #endif // HAVE_GNUTLS diff --git a/src/http_utils.cpp b/src/http_utils.cpp index 695292a3..11bab910 100644 --- a/src/http_utils.cpp +++ b/src/http_utils.cpp @@ -576,5 +576,17 @@ size_t base_unescaper(std::string* s, unescaper_ptr unescaper) { return http_unescape(s); } +const char* http_utils::reason_phrase(unsigned int status_code) { + return MHD_get_reason_phrase_for(status_code); +} + +bool http_utils::is_feature_supported(enum MHD_FEATURE feature) { + return MHD_is_feature_supported(feature) == MHD_YES; +} + +const char* http_utils::get_mhd_version() { + return MHD_get_version(); +} + } // namespace http } // namespace httpserver diff --git a/src/httpserver.hpp b/src/httpserver.hpp index b2bba186..6fe33181 100644 --- a/src/httpserver.hpp +++ b/src/httpserver.hpp @@ -34,14 +34,20 @@ #ifdef HAVE_DAUTH #include "httpserver/digest_auth_fail_response.hpp" #endif // HAVE_DAUTH +#include "httpserver/empty_response.hpp" #include "httpserver/file_response.hpp" #include "httpserver/http_arg_value.hpp" #include "httpserver/http_request.hpp" #include "httpserver/http_resource.hpp" #include "httpserver/http_response.hpp" #include "httpserver/http_utils.hpp" +#include "httpserver/iovec_response.hpp" #include "httpserver/file_info.hpp" +#include "httpserver/pipe_response.hpp" #include "httpserver/string_response.hpp" #include "httpserver/webserver.hpp" +#ifdef HAVE_WEBSOCKET +#include "httpserver/websocket_handler.hpp" +#endif // HAVE_WEBSOCKET #endif // SRC_HTTPSERVER_HPP_ diff --git a/src/httpserver/basic_auth_fail_response.hpp b/src/httpserver/basic_auth_fail_response.hpp index d88bbbff..07b15c6e 100644 --- a/src/httpserver/basic_auth_fail_response.hpp +++ b/src/httpserver/basic_auth_fail_response.hpp @@ -43,10 +43,12 @@ class basic_auth_fail_response : public string_response { explicit basic_auth_fail_response( const std::string& content, const std::string& realm = "", + bool prefer_utf8 = true, int response_code = http::http_utils::http_ok, const std::string& content_type = http::http_utils::text_plain): string_response(content, response_code, content_type), - realm(realm) { } + realm(realm), + prefer_utf8(prefer_utf8) { } basic_auth_fail_response(const basic_auth_fail_response& other) = default; basic_auth_fail_response(basic_auth_fail_response&& other) noexcept = default; @@ -59,6 +61,7 @@ class basic_auth_fail_response : public string_response { private: std::string realm = ""; + bool prefer_utf8 = true; }; } // namespace httpserver diff --git a/src/httpserver/create_webserver.hpp b/src/httpserver/create_webserver.hpp index 991b8501..226738dc 100644 --- a/src/httpserver/create_webserver.hpp +++ b/src/httpserver/create_webserver.hpp @@ -409,6 +409,76 @@ class create_webserver { return *this; } + create_webserver& no_listen_socket() { + _no_listen_socket = true; + return *this; + } + + create_webserver& no_thread_safety() { + _no_thread_safety = true; + return *this; + } + + create_webserver& turbo() { + _turbo = true; + return *this; + } + + create_webserver& suppress_date_header() { + _suppress_date_header = true; + return *this; + } + + create_webserver& listen_backlog(int backlog) { + _listen_backlog = backlog; + return *this; + } + + create_webserver& address_reuse(int reuse) { + _address_reuse = reuse; + return *this; + } + + create_webserver& connection_memory_increment(size_t increment) { + _connection_memory_increment = increment; + return *this; + } + + create_webserver& tcp_fastopen_queue_size(int queue_size) { + _tcp_fastopen_queue_size = queue_size; + return *this; + } + + create_webserver& sigpipe_handled_by_app() { + _sigpipe_handled_by_app = true; + return *this; + } + + create_webserver& https_mem_dhparams(const std::string& dhparams) { + _https_mem_dhparams = dhparams; + return *this; + } + + create_webserver& https_key_password(const std::string& password) { + _https_key_password = password; + return *this; + } + + create_webserver& https_priorities_append(const std::string& priorities) { + _https_priorities_append = priorities; + return *this; + } + + create_webserver& no_alpn() { + _no_alpn = true; + return *this; + } + + create_webserver& client_discipline_level(int level) { + _client_discipline_level = level; + return *this; + } + private: uint16_t _port = DEFAULT_WS_PORT; http::http_utils::start_method_T _start_method = http::http_utils::INTERNAL_SELECT; @@ -461,6 +531,20 @@ class create_webserver { auth_handler_ptr _auth_handler = nullptr; std::vector _auth_skip_paths; sni_callback_t _sni_callback = nullptr; + bool _no_listen_socket = false; + bool _no_thread_safety = false; + bool _turbo = false; + bool _suppress_date_header = false; + int _listen_backlog = 0; + int _address_reuse = 0; + size_t _connection_memory_increment = 0; + int _tcp_fastopen_queue_size = 0; + bool _sigpipe_handled_by_app = false; + std::string _https_mem_dhparams = ""; + std::string _https_key_password = ""; + std::string _https_priorities_append = ""; + bool _no_alpn = false; + int _client_discipline_level = -1; friend class webserver; }; diff --git a/src/httpserver/digest_auth_fail_response.hpp b/src/httpserver/digest_auth_fail_response.hpp index 2eb044dc..0aac862d 100644 --- a/src/httpserver/digest_auth_fail_response.hpp +++ b/src/httpserver/digest_auth_fail_response.hpp @@ -43,16 +43,22 @@ class digest_auth_fail_response : public string_response { digest_auth_fail_response(const std::string& content, const std::string& realm = "", const std::string& opaque = "", - bool reload_nonce = false, + bool signal_stale = false, int response_code = http::http_utils::http_ok, const std::string& content_type = http::http_utils::text_plain, http::http_utils::digest_algorithm algorithm = - http::http_utils::digest_algorithm::MD5): + http::http_utils::digest_algorithm::SHA256, + const std::string& domain = "", + bool userhash_support = false, + bool prefer_utf8 = true): string_response(content, response_code, content_type), realm(realm), opaque(opaque), - reload_nonce(reload_nonce), - algorithm(algorithm) { } + domain(domain), + signal_stale(signal_stale), + algorithm(algorithm), + userhash_support(userhash_support), + prefer_utf8(prefer_utf8) { } digest_auth_fail_response(const digest_auth_fail_response& other) = default; digest_auth_fail_response(digest_auth_fail_response&& other) noexcept = default; @@ -66,9 +72,12 @@ class digest_auth_fail_response : public string_response { private: std::string realm = ""; std::string opaque = ""; - bool reload_nonce = false; + std::string domain = ""; + bool signal_stale = false; http::http_utils::digest_algorithm algorithm = - http::http_utils::digest_algorithm::MD5; + http::http_utils::digest_algorithm::SHA256; + bool userhash_support = false; + bool prefer_utf8 = true; }; } // namespace httpserver diff --git a/src/httpserver/empty_response.hpp b/src/httpserver/empty_response.hpp new file mode 100644 index 00000000..2b794644 --- /dev/null +++ b/src/httpserver/empty_response.hpp @@ -0,0 +1,69 @@ +/* + 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_EMPTY_RESPONSE_HPP_ +#define SRC_HTTPSERVER_EMPTY_RESPONSE_HPP_ + +#include +#include "httpserver/http_utils.hpp" +#include "httpserver/http_response.hpp" + +struct MHD_Response; + +namespace httpserver { + +class empty_response : public http_response { + public: + enum response_flags { + NONE = MHD_RF_NONE, + HTTP_1_0_COMPATIBLE_STRICT = MHD_RF_HTTP_1_0_COMPATIBLE_STRICT, + HTTP_1_0_SERVER = MHD_RF_HTTP_1_0_SERVER, + SEND_KEEP_ALIVE_HEADER = MHD_RF_SEND_KEEP_ALIVE_HEADER, + HEAD_ONLY = MHD_RF_HEAD_ONLY_RESPONSE + }; + + empty_response() = default; + + explicit empty_response( + int response_code = http::http_utils::http_no_content, + int flags = NONE): + http_response(response_code, ""), + flags(flags) { } + + empty_response(const empty_response& other) = default; + empty_response(empty_response&& other) noexcept = default; + + empty_response& operator=(const empty_response& b) = default; + empty_response& operator=(empty_response&& b) = default; + + ~empty_response() = default; + + MHD_Response* get_raw_response(); + + private: + int flags = NONE; +}; + +} // namespace httpserver +#endif // SRC_HTTPSERVER_EMPTY_RESPONSE_HPP_ diff --git a/src/httpserver/http_request.hpp b/src/httpserver/http_request.hpp index 2b621b11..862a8a53 100644 --- a/src/httpserver/http_request.hpp +++ b/src/httpserver/http_request.hpp @@ -38,6 +38,7 @@ #include #include #include + #include #include #include @@ -307,26 +308,20 @@ class http_request { uint16_t get_requestor_port() const; #ifdef HAVE_DAUTH - bool check_digest_auth(const std::string& realm, const std::string& password, int nonce_timeout, bool* reload_nonce) const; - - /** - * Check digest authentication using a pre-computed HA1 hash. - * The HA1 hash is computed as: hash(username:realm:password) using the specified algorithm. - * @param realm The authentication realm. - * @param digest Pointer to the pre-computed HA1 hash bytes. - * @param digest_size Size of the digest (16 for MD5, 32 for SHA-256). - * @param nonce_timeout Nonce validity timeout in seconds. - * @param reload_nonce Output: set to true if nonce should be regenerated. - * @param algo The digest algorithm (defaults to MD5). - * @return true if authenticated, false otherwise. - */ - bool check_digest_auth_ha1( + http::http_utils::digest_auth_result check_digest_auth( + const std::string& realm, + const std::string& password, + unsigned int nonce_timeout = 0, + uint32_t max_nc = 0, + http::http_utils::digest_algorithm algo = http::http_utils::digest_algorithm::SHA256) const; + + http::http_utils::digest_auth_result check_digest_auth_digest( const std::string& realm, - const unsigned char* digest, - size_t digest_size, - int nonce_timeout, - bool* reload_nonce, - http::http_utils::digest_algorithm algo = http::http_utils::digest_algorithm::MD5) const; + const void* userdigest, + size_t userdigest_size, + unsigned int nonce_timeout = 0, + uint32_t max_nc = 0, + http::http_utils::digest_algorithm algo = http::http_utils::digest_algorithm::SHA256) const; #endif // HAVE_DAUTH friend std::ostream &operator<< (std::ostream &os, http_request &r); @@ -388,6 +383,10 @@ class http_request { void fetch_user_pass() const; #endif // HAVE_BAUTH +#ifdef HAVE_GNUTLS + void populate_all_cert_fields() const; +#endif // HAVE_GNUTLS + /** * Method used to set an argument value by key. * @param key The name identifying the argument @@ -505,6 +504,17 @@ class http_request { bool args_populated = false; bool path_pieces_cached = false; + +#ifdef HAVE_GNUTLS + bool client_cert_fields_cached = false; + std::string client_cert_dn; + std::string client_cert_issuer_dn; + std::string client_cert_cn; + std::string client_cert_fingerprint_sha256; + time_t client_cert_not_before = static_cast(-1); + time_t client_cert_not_after = static_cast(-1); + bool client_cert_verified = false; +#endif // HAVE_GNUTLS }; std::unique_ptr cache = std::make_unique(); void ensure_path_pieces_cached() const { diff --git a/src/httpserver/http_utils.hpp b/src/httpserver/http_utils.hpp index 8e44c15b..0ff707dd 100644 --- a/src/httpserver/http_utils.hpp +++ b/src/httpserver/http_utils.hpp @@ -60,9 +60,6 @@ #define DEFAULT_MASK_VALUE 0xFFFF -#if MHD_VERSION < 0x00097002 -typedef int MHD_Result; -#endif namespace httpserver { @@ -119,12 +116,30 @@ class http_utils { #ifdef HAVE_DAUTH enum class digest_algorithm { - MD5 = MHD_DIGEST_ALG_MD5, - SHA256 = MHD_DIGEST_ALG_SHA256 + MD5 = MHD_DIGEST_AUTH_ALGO3_MD5, + SHA256 = MHD_DIGEST_AUTH_ALGO3_SHA256, + SHA512_256 = MHD_DIGEST_AUTH_ALGO3_SHA512_256 + }; + + enum class digest_auth_result { + OK = MHD_DAUTH_OK, + ERROR = MHD_DAUTH_ERROR, + WRONG_HEADER = MHD_DAUTH_WRONG_HEADER, + WRONG_USERNAME = MHD_DAUTH_WRONG_USERNAME, + WRONG_REALM = MHD_DAUTH_WRONG_REALM, + WRONG_URI = MHD_DAUTH_WRONG_URI, + WRONG_QOP = MHD_DAUTH_WRONG_QOP, + WRONG_ALGO = MHD_DAUTH_WRONG_ALGO, + TOO_LARGE = MHD_DAUTH_TOO_LARGE, + NONCE_STALE = MHD_DAUTH_NONCE_STALE, + NONCE_OTHER_COND = MHD_DAUTH_NONCE_OTHER_COND, + NONCE_WRONG = MHD_DAUTH_NONCE_WRONG, + RESPONSE_WRONG = MHD_DAUTH_RESPONSE_WRONG }; static constexpr size_t md5_digest_size = 16; static constexpr size_t sha256_digest_size = 32; + static constexpr size_t sha512_256_digest_size = 32; #endif // HAVE_DAUTH static const uint16_t http_method_connect_code; @@ -274,6 +289,10 @@ class http_utils { static const std::string generate_random_upload_filename(const std::string& directory); static std::string sanitize_upload_filename(const std::string& filename); + + static const char* reason_phrase(unsigned int status_code); + static bool is_feature_supported(enum MHD_FEATURE feature); + static const char* get_mhd_version(); }; #define COMPARATOR(x, y, op) { \ diff --git a/src/httpserver/iovec_response.hpp b/src/httpserver/iovec_response.hpp new file mode 100644 index 00000000..98ae81eb --- /dev/null +++ b/src/httpserver/iovec_response.hpp @@ -0,0 +1,63 @@ +/* + 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_IOVEC_RESPONSE_HPP_ +#define SRC_HTTPSERVER_IOVEC_RESPONSE_HPP_ + +#include +#include +#include "httpserver/http_utils.hpp" +#include "httpserver/http_response.hpp" + +struct MHD_Response; + +namespace httpserver { + +class iovec_response : public http_response { + public: + iovec_response() = default; + + explicit iovec_response( + std::vector buffers, + int response_code = http::http_utils::http_ok, + const std::string& content_type = http::http_utils::text_plain): + http_response(response_code, content_type), + buffers(std::move(buffers)) { } + + iovec_response(const iovec_response& other) = default; + iovec_response(iovec_response&& other) noexcept = default; + + iovec_response& operator=(const iovec_response& b) = default; + iovec_response& operator=(iovec_response&& b) = default; + + ~iovec_response() = default; + + MHD_Response* get_raw_response(); + + private: + std::vector buffers; +}; + +} // namespace httpserver +#endif // SRC_HTTPSERVER_IOVEC_RESPONSE_HPP_ diff --git a/src/httpserver/pipe_response.hpp b/src/httpserver/pipe_response.hpp new file mode 100644 index 00000000..c382d851 --- /dev/null +++ b/src/httpserver/pipe_response.hpp @@ -0,0 +1,61 @@ +/* + 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_PIPE_RESPONSE_HPP_ +#define SRC_HTTPSERVER_PIPE_RESPONSE_HPP_ + +#include "httpserver/http_utils.hpp" +#include "httpserver/http_response.hpp" + +struct MHD_Response; + +namespace httpserver { + +class pipe_response : public http_response { + public: + pipe_response() = default; + + explicit pipe_response( + int pipe_fd, + int response_code = http::http_utils::http_ok, + const std::string& content_type = http::http_utils::application_octet_stream): + http_response(response_code, content_type), + pipe_fd(pipe_fd) { } + + pipe_response(const pipe_response& other) = delete; + pipe_response(pipe_response&& other) noexcept = default; + + pipe_response& operator=(const pipe_response& b) = delete; + pipe_response& operator=(pipe_response&& b) = default; + + ~pipe_response() = default; + + MHD_Response* get_raw_response(); + + private: + int pipe_fd = -1; +}; + +} // namespace httpserver +#endif // SRC_HTTPSERVER_PIPE_RESPONSE_HPP_ diff --git a/src/httpserver/webserver.hpp b/src/httpserver/webserver.hpp index 66d81ddd..2a4041cd 100644 --- a/src/httpserver/webserver.hpp +++ b/src/httpserver/webserver.hpp @@ -60,6 +60,9 @@ namespace httpserver { class http_resource; } namespace httpserver { class http_response; } +#ifdef HAVE_WEBSOCKET +namespace httpserver { class websocket_handler; } +#endif // HAVE_WEBSOCKET namespace httpserver { namespace details { struct modded_request; } } struct MHD_Connection; @@ -131,6 +134,78 @@ class webserver { **/ void sweet_kill(); + /** + * Run the webserver's event loop once (non-blocking). + * For use with external event loops when the server is started + * without internal threading. + * @return true on success, false on error + **/ + bool run(); + + /** + * Run the webserver's event loop, blocking until there is activity + * or the timeout expires. + * @param millisec timeout in milliseconds (-1 for indefinite) + * @return true on success, false on error + **/ + bool run_wait(int32_t millisec); + + /** + * Get the file descriptor sets for select()-based event loop integration. + * @param read_fd_set set of FDs to watch for reading + * @param write_fd_set set of FDs to watch for writing + * @param except_fd_set set of FDs to watch for exceptions + * @param max_fd highest FD number set in any of the sets + * @return true on success, false on error + **/ + bool get_fdset(fd_set* read_fd_set, fd_set* write_fd_set, fd_set* except_fd_set, int* max_fd); + + /** + * Get the timeout until the next MHD action is needed. + * @param timeout output: timeout in milliseconds + * @return true if a timeout was set, false if no timeout is needed + **/ + bool get_timeout(uint64_t* timeout); + + /** + * Add an externally-accepted socket connection. + * @param client_socket the accepted client socket + * @param addr the client address + * @param addrlen length of the address + * @return true on success, false on error + **/ + bool add_connection(int client_socket, const struct sockaddr* addr, socklen_t addrlen); + + /** + * Quiesce the daemon: stop accepting new connections while letting + * in-flight requests complete. + * @return the listen socket FD (caller can close it), or -1 on error + **/ + int quiesce(); + + /** + * Get the listen socket file descriptor. + * @return the listen FD, or -1 if not available + **/ + int get_listen_fd() const; + + /** + * Get the number of currently active connections. + * @return active connection count + **/ + unsigned int get_active_connections() const; + + /** + * Get the actual port the daemon is bound to. + * Useful when port 0 was specified to let the OS choose. + * @return the bound port, or 0 if not available + **/ + uint16_t get_bound_port() const; + +#ifdef HAVE_WEBSOCKET + bool register_ws_resource(const std::string& resource, websocket_handler* handler); +#endif // HAVE_WEBSOCKET + protected: webserver& operator=(const webserver& other); @@ -191,6 +266,20 @@ class webserver { const auth_handler_ptr auth_handler; const std::vector auth_skip_paths; const sni_callback_t sni_callback; + const bool no_listen_socket; + const bool no_thread_safety; + const bool turbo; + const bool suppress_date_header; + const int listen_backlog; + const int address_reuse; + const size_t connection_memory_increment; + const int tcp_fastopen_queue_size; + const bool sigpipe_handled_by_app; + const std::string https_mem_dhparams; + const std::string https_key_password; + const std::string https_priorities_append; + const bool no_alpn; + const int client_discipline_level; std::shared_mutex registered_resources_mutex; std::map registered_resources; std::map registered_resources_str; @@ -213,6 +302,10 @@ class webserver { struct MHD_Daemon* daemon; +#ifdef HAVE_WEBSOCKET + std::map registered_ws_handlers; +#endif // HAVE_WEBSOCKET + std::shared_ptr method_not_allowed_page(details::modded_request* mr) const; std::shared_ptr internal_error_page(details::modded_request* mr, bool force_our = false) const; std::shared_ptr not_found_page(details::modded_request* mr) const; @@ -230,7 +323,17 @@ class webserver { const char *filename, const char *content_type, const char *transfer_encoding, const char *data, uint64_t off, size_t size); - static void upgrade_handler(void *cls, struct MHD_Connection* connection, void **con_cls, int upgrade_socket); +#ifdef HAVE_WEBSOCKET + struct ws_upgrade_data { + webserver* ws; + websocket_handler* handler; + }; + + static void upgrade_handler(void *cls, struct MHD_Connection* connection, + void *req_cls, const char *extra_in, + size_t extra_in_size, MHD_socket sock, + struct MHD_UpgradeResponseHandle *urh); +#endif // HAVE_WEBSOCKET MHD_Result requests_answer_first_step(MHD_Connection* connection, struct details::modded_request* mr); @@ -240,6 +343,8 @@ class webserver { MHD_Result finalize_answer(MHD_Connection* connection, struct details::modded_request* mr, const char* method); + struct MHD_Response* get_raw_response_with_fallback(details::modded_request* mr); + MHD_Result complete_request(MHD_Connection* connection, struct details::modded_request* mr, const char* version, const char* method); void invalidate_route_cache(); diff --git a/src/httpserver/websocket_handler.hpp b/src/httpserver/websocket_handler.hpp new file mode 100644 index 00000000..29d0f168 --- /dev/null +++ b/src/httpserver/websocket_handler.hpp @@ -0,0 +1,81 @@ +/* + 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_WEBSOCKET_HANDLER_HPP_ +#define SRC_HTTPSERVER_WEBSOCKET_HANDLER_HPP_ + +#ifdef HAVE_WEBSOCKET + +#include +#include +#include +#include +#include +#include + +namespace httpserver { + +class http_request; + +class websocket_session { + public: + void send_text(const std::string& msg); + void send_binary(const void* data, size_t len); + void send_ping(const std::string& payload = ""); + void send_pong(const std::string& payload = ""); + void close(uint16_t code = 1000, const std::string& reason = ""); + bool is_valid() const; + + private: + websocket_session(MHD_socket sock, struct MHD_UpgradeResponseHandle* urh, + struct MHD_WebSocketStream* ws_stream); + ~websocket_session(); + + websocket_session(const websocket_session&) = delete; + websocket_session& operator=(const websocket_session&) = delete; + + MHD_socket sock; + struct MHD_UpgradeResponseHandle* urh; + struct MHD_WebSocketStream* ws_stream; + bool valid; + + friend class webserver; +}; + +class websocket_handler { + public: + virtual ~websocket_handler() = default; + + virtual void on_open(websocket_session& session); + virtual void on_message(websocket_session& session, std::string_view msg) = 0; + virtual void on_binary(websocket_session& session, const void* data, size_t len); + virtual void on_ping(websocket_session& session, std::string_view payload); + virtual void on_close(websocket_session& session, uint16_t code, const std::string& reason); +}; + +} // namespace httpserver + +#endif // HAVE_WEBSOCKET + +#endif // SRC_HTTPSERVER_WEBSOCKET_HANDLER_HPP_ diff --git a/src/iovec_response.cpp b/src/iovec_response.cpp new file mode 100644 index 00000000..16707d87 --- /dev/null +++ b/src/iovec_response.cpp @@ -0,0 +1,46 @@ +/* + 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/iovec_response.hpp" +#include +#include + +struct MHD_Response; + +namespace httpserver { + +MHD_Response* iovec_response::get_raw_response() { + // MHD_create_response_from_iovec makes an internal copy of the iov array, + // so the local vector is safe. The buffer data pointed to by iov_base must + // remain valid until the response is destroyed — this is guaranteed because + // the buffers are owned by this iovec_response object. + std::vector iov(buffers.size()); + for (size_t i = 0; i < buffers.size(); ++i) { + iov[i].iov_base = buffers[i].data(); + iov[i].iov_len = buffers[i].size(); + } + return MHD_create_response_from_iovec( + iov.data(), + static_cast(iov.size()), + nullptr, + nullptr); +} + +} // namespace httpserver diff --git a/src/pipe_response.cpp b/src/pipe_response.cpp new file mode 100644 index 00000000..218742a6 --- /dev/null +++ b/src/pipe_response.cpp @@ -0,0 +1,32 @@ +/* + 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/pipe_response.hpp" +#include + +struct MHD_Response; + +namespace httpserver { + +MHD_Response* pipe_response::get_raw_response() { + return MHD_create_response_from_pipe(pipe_fd); +} + +} // namespace httpserver diff --git a/src/webserver.cpp b/src/webserver.cpp index 971d3d5a..5926d316 100644 --- a/src/webserver.cpp +++ b/src/webserver.cpp @@ -19,6 +19,10 @@ */ #include "httpserver/webserver.hpp" +#ifdef HAVE_WEBSOCKET +#include "httpserver/websocket_handler.hpp" +#include +#endif // HAVE_WEBSOCKET #if defined(_WIN32) && !defined(__CYGWIN__) #include @@ -48,6 +52,7 @@ #include #include #include +#include #include #include @@ -178,7 +183,21 @@ webserver::webserver(const create_webserver& params): file_cleanup_callback(params._file_cleanup_callback), auth_handler(params._auth_handler), auth_skip_paths(params._auth_skip_paths), - sni_callback(params._sni_callback) { + sni_callback(params._sni_callback), + no_listen_socket(params._no_listen_socket), + no_thread_safety(params._no_thread_safety), + turbo(params._turbo), + suppress_date_header(params._suppress_date_header), + listen_backlog(params._listen_backlog), + address_reuse(params._address_reuse), + connection_memory_increment(params._connection_memory_increment), + tcp_fastopen_queue_size(params._tcp_fastopen_queue_size), + sigpipe_handled_by_app(params._sigpipe_handled_by_app), + https_mem_dhparams(params._https_mem_dhparams), + https_key_password(params._https_key_password), + https_priorities_append(params._https_priorities_append), + no_alpn(params._no_alpn), + client_discipline_level(params._client_discipline_level) { ignore_sigpipe(); pthread_mutex_init(&mutexwait, nullptr); pthread_cond_init(&mutexcond, nullptr); @@ -241,6 +260,17 @@ bool webserver::register_resource(const std::string& resource, http_resource* hr return false; } +#ifdef HAVE_WEBSOCKET +bool webserver::register_ws_resource(const std::string& resource, websocket_handler* handler) { + if (handler == nullptr) { + throw std::invalid_argument("The websocket_handler pointer cannot be null"); + } + std::unique_lock lock(registered_resources_mutex); + registered_ws_handlers[http_utils::standardize_url(resource)] = handler; + return true; +} +#endif // HAVE_WEBSOCKET + bool webserver::start(bool blocking) { struct { MHD_OptionItem operator ()(enum MHD_OPTION opt, intptr_t val, void *ptr = nullptr) { @@ -292,21 +322,15 @@ bool webserver::start(bool blocking) { if (use_ssl) { // Need for const_cast to respect MHD interface that needs a void* iov.push_back(gen(MHD_OPTION_HTTPS_MEM_KEY, 0, reinterpret_cast(const_cast(https_mem_key.c_str())))); - } - - if (use_ssl) { - // Need for const_cast to respect MHD interface that needs a void* iov.push_back(gen(MHD_OPTION_HTTPS_MEM_CERT, 0, reinterpret_cast(const_cast(https_mem_cert.c_str())))); - } - if (https_mem_trust != "" && use_ssl) { - // Need for const_cast to respect MHD interface that needs a void* - iov.push_back(gen(MHD_OPTION_HTTPS_MEM_TRUST, 0, reinterpret_cast(const_cast(https_mem_trust.c_str())))); - } + if (!https_mem_trust.empty()) { + iov.push_back(gen(MHD_OPTION_HTTPS_MEM_TRUST, 0, reinterpret_cast(const_cast(https_mem_trust.c_str())))); + } - if (https_priorities != "" && use_ssl) { - // Need for const_cast to respect MHD interface that needs a void* - iov.push_back(gen(MHD_OPTION_HTTPS_PRIORITIES, 0, reinterpret_cast(const_cast(https_priorities.c_str())))); + if (!https_priorities.empty()) { + iov.push_back(gen(MHD_OPTION_HTTPS_PRIORITIES, 0, reinterpret_cast(const_cast(https_priorities.c_str())))); + } } #ifdef HAVE_DAUTH @@ -334,6 +358,46 @@ bool webserver::start(bool blocking) { #endif // MHD_OPTION_HTTPS_CERT_CALLBACK #endif // HAVE_GNUTLS + if (listen_backlog > 0) { + iov.push_back(gen(MHD_OPTION_LISTEN_BACKLOG_SIZE, listen_backlog)); + } + + if (address_reuse != 0) { + iov.push_back(gen(MHD_OPTION_LISTENING_ADDRESS_REUSE, address_reuse)); + } + + if (connection_memory_increment > 0) { + iov.push_back(gen(MHD_OPTION_CONNECTION_MEMORY_INCREMENT, connection_memory_increment)); + } + + if (tcp_fastopen_queue_size > 0) { + iov.push_back(gen(MHD_OPTION_TCP_FASTOPEN_QUEUE_SIZE, tcp_fastopen_queue_size)); + } + + if (sigpipe_handled_by_app) { + iov.push_back(gen(MHD_OPTION_SIGPIPE_HANDLED_BY_APP, 1)); + } + + if (!https_mem_dhparams.empty()) { + iov.push_back(gen(MHD_OPTION_HTTPS_MEM_DHPARAMS, 0, const_cast(https_mem_dhparams.c_str()))); + } + + if (!https_key_password.empty()) { + iov.push_back(gen(MHD_OPTION_HTTPS_KEY_PASSWORD, 0, const_cast(https_key_password.c_str()))); + } + + if (!https_priorities_append.empty()) { + iov.push_back(gen(MHD_OPTION_HTTPS_PRIORITIES_APPEND, 0, const_cast(https_priorities_append.c_str()))); + } + + if (no_alpn) { + iov.push_back(gen(MHD_OPTION_TLS_NO_ALPN, 1)); + } + + if (client_discipline_level >= 0) { + iov.push_back(gen(MHD_OPTION_CLIENT_DISCIPLINE_LVL, client_discipline_level)); + } + iov.push_back(gen(MHD_OPTION_END, 0, nullptr)); int start_conf = start_method; @@ -365,6 +429,28 @@ bool webserver::start(bool blocking) { start_conf |= MHD_USE_TCP_FASTOPEN; #endif + if (no_listen_socket) { + start_conf |= MHD_USE_NO_LISTEN_SOCKET; + } + + if (no_thread_safety) { + start_conf |= MHD_USE_NO_THREAD_SAFETY; + } + + if (turbo) { + start_conf |= MHD_USE_TURBO; + } + + if (suppress_date_header) { + start_conf |= MHD_USE_SUPPRESS_DATE_NO_CLOCK; + } + +#ifdef HAVE_WEBSOCKET + if (!registered_ws_handlers.empty()) { + start_conf |= MHD_ALLOW_UPGRADE; + } +#endif // HAVE_WEBSOCKET + daemon = nullptr; if (bind_address == nullptr) { daemon = MHD_start_daemon(start_conf, port, &policy_callback, this, @@ -414,6 +500,68 @@ bool webserver::stop() { return true; } +int webserver::quiesce() { + if (daemon == nullptr) return -1; + MHD_socket fd = MHD_quiesce_daemon(daemon); + return static_cast(fd); +} + +int webserver::get_listen_fd() const { + if (daemon == nullptr) return -1; + const union MHD_DaemonInfo* info = MHD_get_daemon_info(daemon, MHD_DAEMON_INFO_LISTEN_FD); + if (info == nullptr) return -1; + return static_cast(info->listen_fd); +} + +unsigned int webserver::get_active_connections() const { + if (daemon == nullptr) return 0; + const union MHD_DaemonInfo* info = MHD_get_daemon_info(daemon, MHD_DAEMON_INFO_CURRENT_CONNECTIONS); + if (info == nullptr) return 0; + return info->num_connections; +} + +uint16_t webserver::get_bound_port() const { + if (daemon == nullptr) return 0; + const union MHD_DaemonInfo* info = MHD_get_daemon_info(daemon, MHD_DAEMON_INFO_BIND_PORT); + if (info == nullptr) return 0; + return info->port; +} + +bool webserver::run() { + if (daemon == nullptr) return false; + return MHD_run(daemon) == MHD_YES; +} + +bool webserver::run_wait(int32_t millisec) { + if (daemon == nullptr) return false; + return MHD_run_wait(daemon, millisec) == MHD_YES; +} + +bool webserver::get_fdset(fd_set* read_fd_set, fd_set* write_fd_set, fd_set* except_fd_set, int* max_fd) { + if (daemon == nullptr) return false; + MHD_socket mhd_max_fd = 0; + if (MHD_get_fdset(daemon, read_fd_set, write_fd_set, except_fd_set, &mhd_max_fd) != MHD_YES) { + return false; + } + *max_fd = static_cast(mhd_max_fd); + return true; +} + +bool webserver::get_timeout(uint64_t* timeout) { + if (daemon == nullptr) return false; + MHD_UNSIGNED_LONG_LONG mhd_timeout = 0; + if (MHD_get_timeout(daemon, &mhd_timeout) != MHD_YES) { + return false; + } + *timeout = static_cast(mhd_timeout); + return true; +} + +bool webserver::add_connection(int client_socket, const struct sockaddr* addr, socklen_t addrlen) { + if (daemon == nullptr) return false; + return MHD_add_connection(daemon, client_socket, addr, addrlen) == MHD_YES; +} + void webserver::invalidate_route_cache() { std::lock_guard lock(route_cache_mutex); route_cache_list.clear(); @@ -665,7 +813,7 @@ size_t unescaper_func(void * cls, struct MHD_Connection *c, char *s) { // IT IS DUE TO A BOGUS ON libmicrohttpd (V0.99) THAT PRODUCING A // STRING CONTAINING '\0' AFTER AN UNESCAPING, IS UNABLE TO PARSE // ARGS WITH get_connection_values FUNC OR lookup FUNC. - return std::string(s).size(); + return strlen(s); } MHD_Result webserver::post_iterator(void *cls, enum MHD_ValueKind kind, @@ -751,12 +899,114 @@ MHD_Result webserver::post_iterator(void *cls, enum MHD_ValueKind kind, } } -void webserver::upgrade_handler(void *cls, struct MHD_Connection* connection, void **con_cls, int upgrade_socket) { - std::ignore = cls; +#ifdef HAVE_WEBSOCKET +static void decode_websocket_buffer(struct MHD_WebSocketStream* ws_stream, + websocket_handler* handler, + websocket_session& session, + const char* buf, size_t buf_len) { + size_t offset = 0; + while (offset < buf_len && session.is_valid()) { + char* frame_data = nullptr; + size_t frame_len = 0; + size_t step = 0; + int status = MHD_websocket_decode(ws_stream, + buf + offset, + buf_len - offset, + &step, + &frame_data, + &frame_len); + offset += step; + switch (status) { + case MHD_WEBSOCKET_STATUS_TEXT_FRAME: + handler->on_message(session, std::string_view(frame_data, frame_len)); + MHD_websocket_free(ws_stream, frame_data); + break; + case MHD_WEBSOCKET_STATUS_BINARY_FRAME: + handler->on_binary(session, frame_data, frame_len); + MHD_websocket_free(ws_stream, frame_data); + break; + case MHD_WEBSOCKET_STATUS_PING_FRAME: + handler->on_ping(session, std::string_view(frame_data, frame_len)); + MHD_websocket_free(ws_stream, frame_data); + break; + case MHD_WEBSOCKET_STATUS_CLOSE_FRAME: { + uint16_t close_code = 1000; + std::string close_reason; + if (frame_len >= 2) { + close_code = static_cast( + (static_cast(frame_data[0]) << 8) | + static_cast(frame_data[1])); + if (frame_len > 2) { + close_reason.assign(frame_data + 2, frame_len - 2); + } + } + handler->on_close(session, close_code, close_reason); + MHD_websocket_free(ws_stream, frame_data); + // Send close response and end the loop + session.close(close_code, close_reason); + break; + } + case MHD_WEBSOCKET_STATUS_OK: + // Need more data - go back to recv + if (frame_data != nullptr) { + MHD_websocket_free(ws_stream, frame_data); + } + break; + default: + // Protocol error or unknown frame + if (frame_data != nullptr) { + MHD_websocket_free(ws_stream, frame_data); + } + session.close(1002, "Protocol error"); + break; + } + // If decode consumed no bytes, we need more data + if (step == 0) break; + } +} + +void webserver::upgrade_handler(void *cls, struct MHD_Connection* connection, + void *req_cls, const char *extra_in, + size_t extra_in_size, MHD_socket sock, + struct MHD_UpgradeResponseHandle *urh) { std::ignore = connection; - std::ignore = con_cls; - std::ignore = upgrade_socket; + std::ignore = req_cls; + + ws_upgrade_data* data = static_cast(cls); + websocket_handler* handler = data->handler; + delete data; + + // Create a WebSocket stream for this connection + struct MHD_WebSocketStream* ws_stream = nullptr; + int ws_result = MHD_websocket_stream_init(&ws_stream, + MHD_WEBSOCKET_FLAG_SERVER | MHD_WEBSOCKET_FLAG_NO_FRAGMENTS, + 0); + if (ws_result != MHD_WEBSOCKET_STATUS_OK || ws_stream == nullptr) { + MHD_upgrade_action(urh, MHD_UPGRADE_ACTION_CLOSE); + return; + } + + websocket_session session(sock, urh, ws_stream); + handler->on_open(session); + + // Process any initial data that MHD may have buffered + if (extra_in != nullptr && extra_in_size > 0) { + decode_websocket_buffer(ws_stream, handler, session, extra_in, extra_in_size); + } + + // Receive loop + char buf[4096]; + while (session.is_valid()) { + ssize_t got = recv(sock, buf, sizeof(buf), 0); + if (got <= 0) break; + + decode_websocket_buffer(ws_stream, handler, session, + buf, static_cast(got)); + } + + // Session destructor will free ws_stream and close urh } +#endif // HAVE_WEBSOCKET std::shared_ptr webserver::not_found_page(details::modded_request* mr) const { if (not_found_resource != nullptr) { @@ -782,33 +1032,34 @@ std::shared_ptr webserver::internal_error_page(details::modded_re } } -bool webserver::should_skip_auth(const std::string& path) const { - // Normalize path: resolve ".." and "." segments to prevent bypass - std::string normalized; - { - std::vector segments; - std::string::size_type start = 0; - // Skip leading slash - if (!path.empty() && path[0] == '/') { - start = 1; - } - while (start < path.size()) { - auto end = path.find('/', start); - if (end == std::string::npos) end = path.size(); - std::string seg = path.substr(start, end - start); - if (seg == "..") { - if (!segments.empty()) segments.pop_back(); - } else if (!seg.empty() && seg != ".") { - segments.push_back(seg); - } - start = end + 1; - } - normalized = "/"; - for (size_t i = 0; i < segments.size(); i++) { - if (i > 0) normalized += "/"; - normalized += segments[i]; +static std::string normalize_path(const std::string& path) { + std::vector segments; + std::string::size_type start = 0; + // Skip leading slash + if (!path.empty() && path[0] == '/') { + start = 1; + } + while (start < path.size()) { + auto end = path.find('/', start); + if (end == std::string::npos) end = path.size(); + std::string seg = path.substr(start, end - start); + if (seg == "..") { + if (!segments.empty()) segments.pop_back(); + } else if (!seg.empty() && seg != ".") { + segments.push_back(seg); } + start = end + 1; + } + std::string normalized = "/"; + for (size_t i = 0; i < segments.size(); i++) { + if (i > 0) normalized += "/"; + normalized += segments[i]; } + return normalized; +} + +bool webserver::should_skip_auth(const std::string& path) const { + std::string normalized = normalize_path(path); for (const auto& skip_path : auth_skip_paths) { if (skip_path == normalized) return true; @@ -875,9 +1126,90 @@ MHD_Result webserver::requests_answer_second_step(MHD_Connection* connection, co return MHD_YES; } +struct MHD_Response* webserver::get_raw_response_with_fallback(details::modded_request* mr) { + try { + struct MHD_Response* raw = mr->dhrs->get_raw_response(); + if (raw == nullptr) { + mr->dhrs = internal_error_page(mr); + raw = mr->dhrs->get_raw_response(); + } + return raw; + } catch(const std::invalid_argument&) { + try { + mr->dhrs = not_found_page(mr); + return mr->dhrs->get_raw_response(); + } catch(...) { + return nullptr; + } + } catch(...) { + try { + mr->dhrs = internal_error_page(mr); + return mr->dhrs->get_raw_response(); + } catch(...) { + return nullptr; + } + } +} + MHD_Result webserver::finalize_answer(MHD_Connection* connection, struct details::modded_request* mr, const char* method) { int to_ret = MHD_NO; +#ifdef HAVE_WEBSOCKET + // Check for WebSocket upgrade request before normal resource dispatch + { + const char* upgrade_header = MHD_lookup_connection_value(connection, MHD_HEADER_KIND, MHD_HTTP_HEADER_UPGRADE); + if (upgrade_header != nullptr && 0 == strcasecmp(upgrade_header, "websocket")) { + // RFC 6455 handshake validation + const char* connection_header = MHD_lookup_connection_value(connection, MHD_HEADER_KIND, MHD_HTTP_HEADER_CONNECTION); + const char* ws_version = MHD_lookup_connection_value(connection, MHD_HEADER_KIND, "Sec-WebSocket-Version"); + const char* ws_key = MHD_lookup_connection_value(connection, MHD_HEADER_KIND, "Sec-WebSocket-Key"); + + // Validate required headers per RFC 6455 Section 4.2.1 + auto send_bad_request = [&]() -> MHD_Result { + struct MHD_Response* bad_response = MHD_create_response_from_buffer(0, nullptr, MHD_RESPMEM_PERSISTENT); + MHD_Result ret = (MHD_Result) MHD_queue_response(connection, MHD_HTTP_BAD_REQUEST, bad_response); + MHD_destroy_response(bad_response); + return ret; + }; + + if (connection_header == nullptr || strcasestr(connection_header, "Upgrade") == nullptr) { + return send_bad_request(); + } + if (ws_version == nullptr || strcmp(ws_version, "13") != 0) { + return send_bad_request(); + } + if (ws_key == nullptr || strlen(ws_key) == 0) { + return send_bad_request(); + } + + std::shared_lock lock(registered_resources_mutex); + auto ws_it = registered_ws_handlers.find(mr->standardized_url); + if (ws_it != registered_ws_handlers.end()) { + websocket_handler* handler = ws_it->second; + lock.unlock(); + + ws_upgrade_data* data = new ws_upgrade_data{this, handler}; + struct MHD_Response* response = MHD_create_response_for_upgrade(&upgrade_handler, data); + if (response != nullptr) { + // Add required WebSocket response headers + MHD_add_response_header(response, MHD_HTTP_HEADER_UPGRADE, "websocket"); + + // Compute Sec-WebSocket-Accept from client's key (RFC 6455 Section 4.2.2) + char accept_header[29]; // Base64 of SHA-1 = 28 chars + null + if (MHD_websocket_create_accept_header(ws_key, accept_header) == MHD_WEBSOCKET_STATUS_OK) { + MHD_add_response_header(response, "Sec-WebSocket-Accept", accept_header); + } + + to_ret = MHD_queue_response(connection, MHD_HTTP_SWITCHING_PROTOCOLS, response); + MHD_destroy_response(response); + return (MHD_Result) to_ret; + } + delete data; + } + } + } +#endif // HAVE_WEBSOCKET + map::iterator fe; http_resource* hrm; @@ -1017,24 +1349,8 @@ MHD_Result webserver::finalize_answer(MHD_Connection* connection, struct details mr->dhrs = not_found_page(mr); } - try { - try { - raw_response = mr->dhrs->get_raw_response(); - if (raw_response == nullptr) { - mr->dhrs = internal_error_page(mr); - raw_response = mr->dhrs->get_raw_response(); - } - } catch(const std::invalid_argument& iae) { - mr->dhrs = not_found_page(mr); - raw_response = mr->dhrs->get_raw_response(); - } catch(const std::exception& e) { - mr->dhrs = internal_error_page(mr); - raw_response = mr->dhrs->get_raw_response(); - } catch(...) { - mr->dhrs = internal_error_page(mr); - raw_response = mr->dhrs->get_raw_response(); - } - } catch(...) { // catches errors in internal error page + raw_response = get_raw_response_with_fallback(mr); + if (raw_response == nullptr) { mr->dhrs = internal_error_page(mr, true); raw_response = mr->dhrs->get_raw_response(); } diff --git a/src/websocket_handler.cpp b/src/websocket_handler.cpp new file mode 100644 index 00000000..17891abe --- /dev/null +++ b/src/websocket_handler.cpp @@ -0,0 +1,135 @@ +/* + 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 +*/ + +#ifdef HAVE_WEBSOCKET + +#include "httpserver/websocket_handler.hpp" + +#include +#include + +#if !defined(__MINGW32__) +#include +#endif + +#include +#include + +namespace httpserver { + +// websocket_session implementation + +websocket_session::websocket_session(MHD_socket sock, struct MHD_UpgradeResponseHandle* urh, + struct MHD_WebSocketStream* ws_stream): + sock(sock), urh(urh), ws_stream(ws_stream), valid(true) { +} + +websocket_session::~websocket_session() { + if (ws_stream != nullptr) { + MHD_websocket_stream_free(ws_stream); + } + if (urh != nullptr) { + MHD_upgrade_action(urh, MHD_UPGRADE_ACTION_CLOSE); + } +} + +static bool send_all(MHD_socket sock, const char* data, size_t len) { + size_t sent = 0; + while (sent < len) { + ssize_t ret = send(sock, data + sent, len - sent, 0); + if (ret <= 0) return false; + sent += static_cast(ret); + } + return true; +} + +void websocket_session::send_text(const std::string& msg) { + if (!valid) return; + char* frame = nullptr; + size_t frame_len = 0; + if (MHD_websocket_encode_text(ws_stream, msg.c_str(), msg.size(), 0, &frame, &frame_len, nullptr) == MHD_WEBSOCKET_STATUS_OK) { + if (!send_all(sock, frame, frame_len)) valid = false; + MHD_websocket_free(ws_stream, frame); + } +} + +void websocket_session::send_binary(const void* data, size_t len) { + if (!valid) return; + char* frame = nullptr; + size_t frame_len = 0; + if (MHD_websocket_encode_binary(ws_stream, static_cast(data), len, 0, &frame, &frame_len) == MHD_WEBSOCKET_STATUS_OK) { + if (!send_all(sock, frame, frame_len)) valid = false; + MHD_websocket_free(ws_stream, frame); + } +} + +void websocket_session::send_ping(const std::string& payload) { + if (!valid) return; + char* frame = nullptr; + size_t frame_len = 0; + if (MHD_websocket_encode_ping(ws_stream, payload.c_str(), payload.size(), &frame, &frame_len) == MHD_WEBSOCKET_STATUS_OK) { + if (!send_all(sock, frame, frame_len)) valid = false; + MHD_websocket_free(ws_stream, frame); + } +} + +void websocket_session::send_pong(const std::string& payload) { + if (!valid) return; + char* frame = nullptr; + size_t frame_len = 0; + if (MHD_websocket_encode_pong(ws_stream, payload.c_str(), payload.size(), &frame, &frame_len) == MHD_WEBSOCKET_STATUS_OK) { + if (!send_all(sock, frame, frame_len)) valid = false; + MHD_websocket_free(ws_stream, frame); + } +} + +void websocket_session::close(uint16_t code, const std::string& reason) { + if (!valid) return; + valid = false; + char* frame = nullptr; + size_t frame_len = 0; + if (MHD_websocket_encode_close(ws_stream, code, reason.c_str(), reason.size(), &frame, &frame_len) == MHD_WEBSOCKET_STATUS_OK) { + send_all(sock, frame, frame_len); + MHD_websocket_free(ws_stream, frame); + } +} + +bool websocket_session::is_valid() const { + return valid; +} + +// websocket_handler default implementations + +void websocket_handler::on_open(websocket_session&) { +} + +void websocket_handler::on_binary(websocket_session&, const void*, size_t) { +} + +void websocket_handler::on_ping(websocket_session& session, std::string_view payload) { + session.send_pong(std::string(payload)); +} + +void websocket_handler::on_close(websocket_session&, uint16_t, const std::string&) { +} + +} // namespace httpserver + +#endif // HAVE_WEBSOCKET diff --git a/test/Makefile.am b/test/Makefile.am index cdbacf26..4fa65b70 100644 --- a/test/Makefile.am +++ b/test/Makefile.am @@ -26,7 +26,7 @@ LDADD += -lcurl AM_CPPFLAGS = -I$(top_srcdir)/src -I$(top_srcdir)/src/httpserver/ METASOURCES = AUTO -check_PROGRAMS = basic file_upload http_utils threaded nodelay string_utilities http_endpoint ban_system ws_start_stop authentication deferred http_resource http_response create_webserver +check_PROGRAMS = basic file_upload http_utils threaded nodelay string_utilities http_endpoint ban_system ws_start_stop authentication deferred http_resource http_response create_webserver new_response_types daemon_info MOSTLYCLEANFILES = *.gcda *.gcno *.gcov @@ -44,6 +44,8 @@ nodelay_SOURCES = integ/nodelay.cpp http_resource_SOURCES = unit/http_resource_test.cpp http_response_SOURCES = unit/http_response_test.cpp create_webserver_SOURCES = unit/create_webserver_test.cpp +new_response_types_SOURCES = integ/new_response_types.cpp +daemon_info_SOURCES = integ/daemon_info.cpp noinst_HEADERS = littletest.hpp AM_CXXFLAGS += -Wall -fPIC -Wno-overloaded-virtual diff --git a/test/integ/authentication.cpp b/test/integ/authentication.cpp index b043f566..b14ce961 100644 --- a/test/integ/authentication.cpp +++ b/test/integ/authentication.cpp @@ -84,12 +84,18 @@ class user_pass_resource : public http_resource { class digest_resource : public http_resource { public: shared_ptr render_GET(const http_request& req) { + using httpserver::http::http_utils; if (req.get_digested_user() == "") { - return std::make_shared("FAIL", "examplerealm", MY_OPAQUE, true); + return std::make_shared("FAIL", "examplerealm", MY_OPAQUE, true, + http_utils::http_ok, http_utils::text_plain, http_utils::digest_algorithm::MD5); } else { - bool reload_nonce = false; - if (!req.check_digest_auth("examplerealm", "mypass", 300, &reload_nonce)) { - return std::make_shared("FAIL", "examplerealm", MY_OPAQUE, reload_nonce); + auto result = req.check_digest_auth("examplerealm", "mypass", 300, 0, http_utils::digest_algorithm::MD5); + if (result == http_utils::digest_auth_result::NONCE_STALE) { + return std::make_shared("FAIL", "examplerealm", MY_OPAQUE, true, + http_utils::http_ok, http_utils::text_plain, http_utils::digest_algorithm::MD5); + } else if (result != http_utils::digest_auth_result::OK) { + return std::make_shared("FAIL", "examplerealm", MY_OPAQUE, false, + http_utils::http_ok, http_utils::text_plain, http_utils::digest_algorithm::MD5); } } return std::make_shared("SUCCESS", 200, "text/plain"); @@ -184,22 +190,26 @@ static const unsigned char PRECOMPUTED_HA1_SHA256[32] = { class digest_ha1_md5_resource : public http_resource { public: shared_ptr render_GET(const http_request& req) { + using httpserver::http::http_utils; if (req.get_digested_user() == "") { return std::make_shared( "FAIL", "examplerealm", MY_OPAQUE, true, - httpserver::http::http_utils::http_ok, - httpserver::http::http_utils::text_plain, - httpserver::http::http_utils::digest_algorithm::MD5); + http_utils::http_ok, http_utils::text_plain, + http_utils::digest_algorithm::MD5); } - bool reload_nonce = false; - if (!req.check_digest_auth_ha1("examplerealm", PRECOMPUTED_HA1_MD5, - httpserver::http::http_utils::md5_digest_size, 300, &reload_nonce, - httpserver::http::http_utils::digest_algorithm::MD5)) { + auto result = req.check_digest_auth_digest("examplerealm", PRECOMPUTED_HA1_MD5, + http_utils::md5_digest_size, 300, 0, + http_utils::digest_algorithm::MD5); + if (result == http_utils::digest_auth_result::NONCE_STALE) { return std::make_shared( - "FAIL", "examplerealm", MY_OPAQUE, reload_nonce, - httpserver::http::http_utils::http_ok, - httpserver::http::http_utils::text_plain, - httpserver::http::http_utils::digest_algorithm::MD5); + "FAIL", "examplerealm", MY_OPAQUE, true, + http_utils::http_ok, http_utils::text_plain, + http_utils::digest_algorithm::MD5); + } else if (result != http_utils::digest_auth_result::OK) { + return std::make_shared( + "FAIL", "examplerealm", MY_OPAQUE, false, + http_utils::http_ok, http_utils::text_plain, + http_utils::digest_algorithm::MD5); } return std::make_shared("SUCCESS", 200, "text/plain"); } @@ -208,22 +218,26 @@ class digest_ha1_md5_resource : public http_resource { class digest_ha1_sha256_resource : public http_resource { public: shared_ptr render_GET(const http_request& req) { + using httpserver::http::http_utils; if (req.get_digested_user() == "") { return std::make_shared( "FAIL", "examplerealm", MY_OPAQUE, true, - httpserver::http::http_utils::http_ok, - httpserver::http::http_utils::text_plain, - httpserver::http::http_utils::digest_algorithm::SHA256); + http_utils::http_ok, http_utils::text_plain, + http_utils::digest_algorithm::SHA256); } - bool reload_nonce = false; - if (!req.check_digest_auth_ha1("examplerealm", PRECOMPUTED_HA1_SHA256, - httpserver::http::http_utils::sha256_digest_size, 300, &reload_nonce, - httpserver::http::http_utils::digest_algorithm::SHA256)) { + auto result = req.check_digest_auth_digest("examplerealm", PRECOMPUTED_HA1_SHA256, + http_utils::sha256_digest_size, 300, 0, + http_utils::digest_algorithm::SHA256); + if (result == http_utils::digest_auth_result::NONCE_STALE) { + return std::make_shared( + "FAIL", "examplerealm", MY_OPAQUE, true, + http_utils::http_ok, http_utils::text_plain, + http_utils::digest_algorithm::SHA256); + } else if (result != http_utils::digest_auth_result::OK) { return std::make_shared( - "FAIL", "examplerealm", MY_OPAQUE, reload_nonce, - httpserver::http::http_utils::http_ok, - httpserver::http::http_utils::text_plain, - httpserver::http::http_utils::digest_algorithm::SHA256); + "FAIL", "examplerealm", MY_OPAQUE, false, + http_utils::http_ok, http_utils::text_plain, + http_utils::digest_algorithm::SHA256); } return std::make_shared("SUCCESS", 200, "text/plain"); } @@ -474,9 +488,16 @@ LT_END_AUTO_TEST(digest_auth_with_ha1_sha256_wrong_pass) class digest_user_cache_resource : public http_resource { public: shared_ptr render_GET(const http_request& req) { + using httpserver::http::http_utils; // First call - will populate cache (line 300 nullptr or non-null branch) std::string user1 = std::string(req.get_digested_user()); + if (user1.empty()) { + // No digest auth provided - send a 401 challenge so curl can retry + return std::make_shared("FAIL", "testrealm", MY_OPAQUE, true, + http_utils::http_ok, http_utils::text_plain, http_utils::digest_algorithm::SHA256); + } + // Second call - should hit cache (lines 293-295) std::string user2 = std::string(req.get_digested_user()); @@ -485,11 +506,6 @@ class digest_user_cache_resource : public http_resource { return std::make_shared("CACHE_MISMATCH", 500, "text/plain"); } - if (user1.empty()) { - // No digest auth provided - tests the nullptr branch (line 299-300) - return std::make_shared("NO_DIGEST_USER", 200, "text/plain"); - } - // Return the digested user (tests cache hit with valid user) return std::make_shared("USER:" + user1, 200, "text/plain"); } @@ -497,7 +513,9 @@ class digest_user_cache_resource : public http_resource { // Test digested user caching when no digest auth is provided (nullptr branch) LT_BEGIN_AUTO_TEST(authentication_suite, digest_user_cache_no_auth) - webserver ws = create_webserver(PORT); + webserver ws = create_webserver(PORT) + .digest_auth_random("myrandom") + .nonce_nc_size(300); digest_user_cache_resource resource; LT_ASSERT_EQ(true, ws.register_resource("cache_test", &resource)); @@ -507,14 +525,16 @@ LT_BEGIN_AUTO_TEST(authentication_suite, digest_user_cache_no_auth) std::string s; CURL *curl = curl_easy_init(); CURLcode res; - // No authentication - should trigger nullptr branch in get_digested_user + long http_code = 0; + // No authentication - should trigger 401 challenge curl_easy_setopt(curl, CURLOPT_URL, "localhost:" PORT_STRING "/cache_test"); curl_easy_setopt(curl, CURLOPT_HTTPGET, 1L); curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, writefunc); curl_easy_setopt(curl, CURLOPT_WRITEDATA, &s); res = curl_easy_perform(curl); LT_ASSERT_EQ(res, 0); - LT_CHECK_EQ(s, "NO_DIGEST_USER"); + curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &http_code); + LT_CHECK_EQ(http_code, 401); curl_easy_cleanup(curl); ws.stop(); @@ -550,10 +570,9 @@ LT_BEGIN_AUTO_TEST(authentication_suite, digest_user_cache_with_auth) // or NO_DIGEST_USER if no auth was provided. With CURLAUTH_DIGEST, // curl will respond to the 401 challenge and include auth headers. // The resource calls get_digested_user twice to test caching. - // Check that response is not empty and not a cache mismatch - LT_CHECK_EQ(s.find("CACHE_MISMATCH") == std::string::npos, true); - // Should contain either "USER:" (auth worked) or "NO_DIGEST_USER" (fallback) - LT_CHECK_EQ(s.find("USER:") != std::string::npos || s == "NO_DIGEST_USER", true); + // With CURLAUTH_DIGEST, curl responds to the 401 challenge. + // The server should return "USER:testuser". + LT_CHECK_EQ(s, "USER:testuser"); curl_easy_cleanup(curl); ws.stop(); @@ -939,23 +958,47 @@ LT_BEGIN_AUTO_TEST(authentication_suite, auth_multiple_skip_paths) long http_code = 0; // NOLINT(runtime/int) std::string s; - // All skip paths should work without auth - const char* skip_urls[] = {"/health", "/metrics", "/status"}; - for (const char* url : skip_urls) { - curl = curl_easy_init(); - s = ""; - http_code = 0; - std::string full_url = std::string("localhost:" PORT_STRING) + url; - curl_easy_setopt(curl, CURLOPT_URL, full_url.c_str()); - curl_easy_setopt(curl, CURLOPT_HTTPGET, 1L); - curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, writefunc); - curl_easy_setopt(curl, CURLOPT_WRITEDATA, &s); - res = curl_easy_perform(curl); - curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &http_code); - LT_ASSERT_EQ(res, 0); - LT_CHECK_EQ(http_code, 200); - curl_easy_cleanup(curl); - } + // /health should work without auth + curl = curl_easy_init(); + s = ""; + http_code = 0; + curl_easy_setopt(curl, CURLOPT_URL, "localhost:" PORT_STRING "/health"); + curl_easy_setopt(curl, CURLOPT_HTTPGET, 1L); + curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, writefunc); + curl_easy_setopt(curl, CURLOPT_WRITEDATA, &s); + res = curl_easy_perform(curl); + curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &http_code); + LT_ASSERT_EQ(res, 0); + LT_CHECK_EQ(http_code, 200); + curl_easy_cleanup(curl); + + // /metrics should work without auth + curl = curl_easy_init(); + s = ""; + http_code = 0; + curl_easy_setopt(curl, CURLOPT_URL, "localhost:" PORT_STRING "/metrics"); + curl_easy_setopt(curl, CURLOPT_HTTPGET, 1L); + curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, writefunc); + curl_easy_setopt(curl, CURLOPT_WRITEDATA, &s); + res = curl_easy_perform(curl); + curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &http_code); + LT_ASSERT_EQ(res, 0); + LT_CHECK_EQ(http_code, 200); + curl_easy_cleanup(curl); + + // /status should work without auth + curl = curl_easy_init(); + s = ""; + http_code = 0; + curl_easy_setopt(curl, CURLOPT_URL, "localhost:" PORT_STRING "/status"); + curl_easy_setopt(curl, CURLOPT_HTTPGET, 1L); + curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, writefunc); + curl_easy_setopt(curl, CURLOPT_WRITEDATA, &s); + res = curl_easy_perform(curl); + curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &http_code); + LT_ASSERT_EQ(res, 0); + LT_CHECK_EQ(http_code, 200); + curl_easy_cleanup(curl); // Protected should still require auth curl = curl_easy_init(); diff --git a/test/integ/daemon_info.cpp b/test/integ/daemon_info.cpp new file mode 100644 index 00000000..e3d15e35 --- /dev/null +++ b/test/integ/daemon_info.cpp @@ -0,0 +1,224 @@ +/* + 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 +#include +#include +#include +#include + +#include "./httpserver.hpp" +#include "./littletest.hpp" + +using std::shared_ptr; +using std::string; +using httpserver::http_resource; +using httpserver::http_request; +using httpserver::http_response; +using httpserver::string_response; +using httpserver::webserver; +using httpserver::create_webserver; + +#ifdef HTTPSERVER_PORT +#define PORT HTTPSERVER_PORT +#else +#define PORT 8080 +#endif + +#define STR2(p) #p +#define STR(p) STR2(p) +#define PORT_STRING STR(PORT) + +size_t writefunc(void *ptr, size_t size, size_t nmemb, string *s) { + s->append(reinterpret_cast(ptr), size*nmemb); + return size*nmemb; +} + +class simple_resource : public http_resource { + public: + shared_ptr render_GET(const http_request&) { + return std::make_shared("OK", 200, "text/plain"); + } +}; + +LT_BEGIN_SUITE(daemon_info_suite) + void set_up() { + } + void tear_down() { + } +LT_END_SUITE(daemon_info_suite) + +LT_BEGIN_AUTO_TEST(daemon_info_suite, get_bound_port_explicit) + webserver ws = create_webserver(PORT); + + simple_resource sr; + ws.register_resource("test", &sr); + ws.start(false); + + LT_CHECK_EQ(ws.get_bound_port(), PORT); + LT_CHECK_GT(ws.get_listen_fd(), 0); + LT_CHECK_EQ(ws.get_active_connections(), 0u); + + ws.stop(); +LT_END_AUTO_TEST(get_bound_port_explicit) + +LT_BEGIN_AUTO_TEST(daemon_info_suite, basic_request_succeeds) + webserver ws = create_webserver(PORT); + + simple_resource sr; + ws.register_resource("test", &sr); + ws.start(false); + + curl_global_init(CURL_GLOBAL_ALL); + string s; + CURL *curl = curl_easy_init(); + curl_easy_setopt(curl, CURLOPT_URL, "localhost:" PORT_STRING "/test"); + curl_easy_setopt(curl, CURLOPT_HTTPGET, 1L); + curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, writefunc); + curl_easy_setopt(curl, CURLOPT_WRITEDATA, &s); + curl_easy_perform(curl); + long http_code = 0; + curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &http_code); + LT_CHECK_EQ(http_code, 200); + LT_CHECK_EQ(s, "OK"); + curl_easy_cleanup(curl); + curl_global_cleanup(); + + ws.stop(); +LT_END_AUTO_TEST(basic_request_succeeds) + +LT_BEGIN_AUTO_TEST(daemon_info_suite, quiesce_does_not_crash) + webserver ws = create_webserver(PORT); + + simple_resource sr; + ws.register_resource("test", &sr); + ws.start(false); + + // Verify it works before quiesce + curl_global_init(CURL_GLOBAL_ALL); + string s; + CURL *curl = curl_easy_init(); + curl_easy_setopt(curl, CURLOPT_URL, "localhost:" PORT_STRING "/test"); + curl_easy_setopt(curl, CURLOPT_HTTPGET, 1L); + curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, writefunc); + curl_easy_setopt(curl, CURLOPT_WRITEDATA, &s); + CURLcode res = curl_easy_perform(curl); + LT_ASSERT_EQ(res, 0); + + // Quiesce: stop accepting new connections. + // Note: quiesce may return -1 if not supported with current daemon flags + // (e.g., thread-per-connection mode). We just verify it doesn't crash. + int listen_fd = ws.quiesce(); + // If quiesce succeeded, the FD should be positive + if (listen_fd > 0) { + close(listen_fd); + } + + curl_easy_cleanup(curl); + curl_global_cleanup(); + + ws.stop(); +LT_END_AUTO_TEST(quiesce_does_not_crash) + +LT_BEGIN_AUTO_TEST(daemon_info_suite, utility_functions) + const char* version = httpserver::http::http_utils::get_mhd_version(); + LT_CHECK_NEQ(version, nullptr); + + const char* phrase = httpserver::http::http_utils::reason_phrase(200); + LT_CHECK_EQ(string(phrase), "OK"); + + const char* not_found = httpserver::http::http_utils::reason_phrase(404); + LT_CHECK_EQ(string(not_found), "Not Found"); +LT_END_AUTO_TEST(utility_functions) + +LT_BEGIN_AUTO_TEST(daemon_info_suite, is_feature_supported_check) + // MHD_FEATURE_MESSAGES is universally supported (basic error logging) + LT_CHECK_EQ(httpserver::http::http_utils::is_feature_supported(MHD_FEATURE_MESSAGES), true); + + // MHD_FEATURE_LARGE_FILE support depends on platform/build configuration. + // Just verify the call does not crash. + bool large_file = httpserver::http::http_utils::is_feature_supported(MHD_FEATURE_LARGE_FILE); + (void)large_file; + + // MHD_FEATURE_AUTODETECT_BIND_PORT support depends on the platform. + // Just verify the call does not crash. + bool autodetect = httpserver::http::http_utils::is_feature_supported(MHD_FEATURE_AUTODETECT_BIND_PORT); + (void)autodetect; +LT_END_AUTO_TEST(is_feature_supported_check) + +// Drive the MHD external event loop alongside a curl multi handle. +// Returns true if curl completed within max_iters iterations. +static bool drive_event_loop(webserver& ws, CURLM* multi, int max_iters) { + int still_running = 1; + while (still_running && max_iters-- > 0) { + // Let MHD process with a short timeout + ws.run_wait(50); + + // Let curl process + curl_multi_perform(multi, &still_running); + } + return (still_running == 0); +} + +LT_BEGIN_AUTO_TEST(daemon_info_suite, external_event_loop) + // Start server with no internal threading (external event loop mode) + webserver ws = create_webserver(PORT) + .start_method(httpserver::http::http_utils::INTERNAL_SELECT) + .max_threads(0); + + simple_resource sr; + ws.register_resource("test", &sr); + ws.start(false); + + // Drive one request through the event loop manually + curl_global_init(CURL_GLOBAL_ALL); + CURL *curl = curl_easy_init(); + string s; + curl_easy_setopt(curl, CURLOPT_URL, "localhost:" PORT_STRING "/test"); + curl_easy_setopt(curl, CURLOPT_HTTPGET, 1L); + curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, writefunc); + curl_easy_setopt(curl, CURLOPT_WRITEDATA, &s); + curl_easy_setopt(curl, CURLOPT_TIMEOUT, 5L); + + // Use run_wait to drive the event loop - it blocks until activity + // or timeout. We use a non-blocking curl multi handle to send + // a request while driving the MHD event loop. + CURLM *multi = curl_multi_init(); + curl_multi_add_handle(multi, curl); + + bool completed = drive_event_loop(ws, multi, 200); + LT_CHECK_EQ(completed, true); + + long http_code = 0; + curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &http_code); + LT_CHECK_EQ(http_code, 200); + LT_CHECK_EQ(s, "OK"); + + curl_multi_remove_handle(multi, curl); + curl_easy_cleanup(curl); + curl_multi_cleanup(multi); + curl_global_cleanup(); + + ws.stop(); +LT_END_AUTO_TEST(external_event_loop) + +LT_BEGIN_AUTO_TEST_ENV() + AUTORUN_TESTS() +LT_END_AUTO_TEST_ENV() diff --git a/test/integ/new_response_types.cpp b/test/integ/new_response_types.cpp new file mode 100644 index 00000000..9b14cedc --- /dev/null +++ b/test/integ/new_response_types.cpp @@ -0,0 +1,166 @@ +/* + 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 +#include +#include +#include +#include +#include + +#include "./httpserver.hpp" +#include "./littletest.hpp" + +using std::shared_ptr; +using std::string; +using std::vector; +using httpserver::http_resource; +using httpserver::http_request; +using httpserver::http_response; +using httpserver::empty_response; +using httpserver::pipe_response; +using httpserver::iovec_response; +using httpserver::webserver; +using httpserver::create_webserver; + +#ifdef HTTPSERVER_PORT +#define PORT HTTPSERVER_PORT +#else +#define PORT 8080 +#endif + +#define STR2(p) #p +#define STR(p) STR2(p) +#define PORT_STRING STR(PORT) + +size_t writefunc(void *ptr, size_t size, size_t nmemb, string *s) { + s->append(reinterpret_cast(ptr), size*nmemb); + return size*nmemb; +} + +class empty_resource : public http_resource { + public: + shared_ptr render_GET(const http_request&) { + return std::make_shared(204); + } +}; + +class pipe_resource : public http_resource { + public: + shared_ptr render_GET(const http_request&) { + int pipefd[2]; + if (pipe(pipefd) != 0) { + return std::make_shared(500); + } + const char* msg = "hello from pipe"; + write(pipefd[1], msg, strlen(msg)); + close(pipefd[1]); + return std::make_shared(pipefd[0], 200); + } +}; + +class iovec_resource : public http_resource { + public: + shared_ptr render_GET(const http_request&) { + vector parts = {"Hello", " ", "World"}; + return std::make_shared(parts, 200, "text/plain"); + } +}; + +static webserver* ws_ptr = nullptr; +static empty_resource er; +static pipe_resource pr; +static iovec_resource ir; + +LT_BEGIN_SUITE(response_types_suite) + void set_up() { + ws_ptr = new webserver(create_webserver(PORT)); + ws_ptr->register_resource("empty", &er); + ws_ptr->register_resource("pipe", &pr); + ws_ptr->register_resource("iovec", &ir); + ws_ptr->start(false); + } + void tear_down() { + ws_ptr->stop(); + delete ws_ptr; + ws_ptr = nullptr; + } +LT_END_SUITE(response_types_suite) + +LT_BEGIN_AUTO_TEST(response_types_suite, empty_response_test) + curl_global_init(CURL_GLOBAL_ALL); + string s; + CURL *curl = curl_easy_init(); + CURLcode res; + long http_code = 0; + curl_easy_setopt(curl, CURLOPT_URL, "localhost:" PORT_STRING "/empty"); + curl_easy_setopt(curl, CURLOPT_HTTPGET, 1L); + curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, writefunc); + curl_easy_setopt(curl, CURLOPT_WRITEDATA, &s); + res = curl_easy_perform(curl); + curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &http_code); + LT_ASSERT_EQ(res, 0); + LT_CHECK_EQ(http_code, 204); + LT_CHECK_EQ(s, ""); + curl_easy_cleanup(curl); + curl_global_cleanup(); +LT_END_AUTO_TEST(empty_response_test) + +LT_BEGIN_AUTO_TEST(response_types_suite, pipe_response_test) + curl_global_init(CURL_GLOBAL_ALL); + string s; + CURL *curl = curl_easy_init(); + CURLcode res; + long http_code = 0; + curl_easy_setopt(curl, CURLOPT_URL, "localhost:" PORT_STRING "/pipe"); + curl_easy_setopt(curl, CURLOPT_HTTPGET, 1L); + curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, writefunc); + curl_easy_setopt(curl, CURLOPT_WRITEDATA, &s); + res = curl_easy_perform(curl); + curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &http_code); + LT_ASSERT_EQ(res, 0); + LT_CHECK_EQ(http_code, 200); + LT_CHECK_EQ(s, "hello from pipe"); + curl_easy_cleanup(curl); + curl_global_cleanup(); +LT_END_AUTO_TEST(pipe_response_test) + +LT_BEGIN_AUTO_TEST(response_types_suite, iovec_response_test) + curl_global_init(CURL_GLOBAL_ALL); + string s; + CURL *curl = curl_easy_init(); + CURLcode res; + long http_code = 0; + curl_easy_setopt(curl, CURLOPT_URL, "localhost:" PORT_STRING "/iovec"); + curl_easy_setopt(curl, CURLOPT_HTTPGET, 1L); + curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, writefunc); + curl_easy_setopt(curl, CURLOPT_WRITEDATA, &s); + res = curl_easy_perform(curl); + curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &http_code); + LT_ASSERT_EQ(res, 0); + LT_CHECK_EQ(http_code, 200); + LT_CHECK_EQ(s, "Hello World"); + curl_easy_cleanup(curl); + curl_global_cleanup(); +LT_END_AUTO_TEST(iovec_response_test) + +LT_BEGIN_AUTO_TEST_ENV() + AUTORUN_TESTS() +LT_END_AUTO_TEST_ENV()