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..2870e081 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, **const std::string&** 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, **const std::string&** 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, const std::string& msg) override { + session.send_text("Echo: " + 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, const std::string& msg) override { + std::cout << "Received: " << msg << std::endl; + session.send_text("Echo: " + 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..232d269e --- /dev/null +++ b/examples/websocket_echo.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 + +#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, const std::string& msg) override { + std::cout << "Received: " << msg << std::endl; + session.send_text("Echo: " + 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..e3e913c2 100644 --- a/src/http_request.cpp +++ b/src/http_request.cpp @@ -95,49 +95,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 +311,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 +345,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; diff --git a/src/http_utils.cpp b/src/http_utils.cpp index 695292a3..15aec307 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(int feature) { + return MHD_is_feature_supported(static_cast(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..e672fc56 100644 --- a/src/httpserver/http_request.hpp +++ b/src/httpserver/http_request.hpp @@ -307,25 +307,19 @@ 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 unsigned char* digest, - size_t digest_size, - int nonce_timeout, - bool* reload_nonce, + 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 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::MD5) const; #endif // HAVE_DAUTH diff --git a/src/httpserver/http_utils.hpp b/src/httpserver/http_utils.hpp index 8e44c15b..f9b70a2c 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(int 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..8077d0cc --- /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) = default; + pipe_response(pipe_response&& other) noexcept = default; + + pipe_response& operator=(const pipe_response& b) = default; + 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..66677955 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; diff --git a/src/httpserver/websocket_handler.hpp b/src/httpserver/websocket_handler.hpp new file mode 100644 index 00000000..bee8f18d --- /dev/null +++ b/src/httpserver/websocket_handler.hpp @@ -0,0 +1,80 @@ +/* + 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 + +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, const std::string& msg) = 0; + virtual void on_binary(websocket_session& session, const void* data, size_t len); + virtual void on_ping(websocket_session& session, const std::string& 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..0a5b5ad7 100644 --- a/src/webserver.cpp +++ b/src/webserver.cpp @@ -19,6 +19,9 @@ */ #include "httpserver/webserver.hpp" +#ifdef HAVE_WEBSOCKET +#include "httpserver/websocket_handler.hpp" +#endif // HAVE_WEBSOCKET #if defined(_WIN32) && !defined(__CYGWIN__) #include @@ -178,7 +181,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 +258,16 @@ 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"); + } + registered_ws_handlers[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) { @@ -334,6 +361,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 +432,22 @@ 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; + } + daemon = nullptr; if (bind_address == nullptr) { daemon = MHD_start_daemon(start_conf, port, &policy_callback, this, @@ -414,6 +497,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(); diff --git a/src/websocket_handler.cpp b/src/websocket_handler.cpp new file mode 100644 index 00000000..85e3b88e --- /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, const std::string& payload) { + session.send_pong(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..3adc114a 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"); } diff --git a/test/integ/daemon_info.cpp b/test/integ/daemon_info.cpp new file mode 100644 index 00000000..f401fe94 --- /dev/null +++ b/test/integ/daemon_info.cpp @@ -0,0 +1,143 @@ +/* + 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; + +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(9876); + + simple_resource sr; + ws.register_resource("test", &sr); + ws.start(false); + + LT_CHECK_EQ(ws.get_bound_port(), 9876); + 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, get_active_connections_during_request) + webserver ws = create_webserver(9877); + + 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:9877/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(get_active_connections_during_request) + +LT_BEGIN_AUTO_TEST(daemon_info_suite, quiesce_stops_new_connections) + webserver ws = create_webserver(9878); + + 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:9878/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_stops_new_connections) + +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_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..ed7fadfe --- /dev/null +++ b/test/integ/new_response_types.cpp @@ -0,0 +1,177 @@ +/* + 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"); + } +}; + +LT_BEGIN_SUITE(response_types_suite) + void set_up() { + } + void tear_down() { + } +LT_END_SUITE(response_types_suite) + +LT_BEGIN_AUTO_TEST(response_types_suite, empty_response_test) + webserver ws = create_webserver(PORT); + + empty_resource er; + LT_ASSERT_EQ(true, ws.register_resource("empty", &er)); + ws.start(false); + + 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(); + + ws.stop(); +LT_END_AUTO_TEST(empty_response_test) + +LT_BEGIN_AUTO_TEST(response_types_suite, pipe_response_test) + webserver ws = create_webserver(PORT); + + pipe_resource pr; + LT_ASSERT_EQ(true, ws.register_resource("pipe", &pr)); + ws.start(false); + + 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(); + + ws.stop(); +LT_END_AUTO_TEST(pipe_response_test) + +LT_BEGIN_AUTO_TEST(response_types_suite, iovec_response_test) + webserver ws = create_webserver(PORT); + + iovec_resource ir; + LT_ASSERT_EQ(true, ws.register_resource("iovec", &ir)); + ws.start(false); + + 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(); + + ws.stop(); +LT_END_AUTO_TEST(iovec_response_test) + +LT_BEGIN_AUTO_TEST_ENV() + AUTORUN_TESTS() +LT_END_AUTO_TEST_ENV()