diff --git a/examples/Makefile.am b/examples/Makefile.am index 318a7a8d..bbde236d 100644 --- a/examples/Makefile.am +++ b/examples/Makefile.am @@ -18,12 +18,14 @@ LDADD = $(top_builddir)/src/libhttpserver.la AM_CPPFLAGS = -I$(top_srcdir)/src -I$(top_srcdir)/src/httpserver/ +AM_LDFLAGS = -lpthread METASOURCES = AUTO -noinst_PROGRAMS = hello_world service minimal_hello_world custom_error allowing_disallowing_methods handlers hello_with_get_arg setting_headers custom_access_log basic_authentication digest_authentication minimal_https minimal_file_response minimal_deferred url_registration minimal_ip_ban benchmark_select benchmark_threads benchmark_nodelay deferred_with_accumulator +noinst_PROGRAMS = hello_world service minimal_hello_world hello_world_websocket custom_error allowing_disallowing_methods handlers hello_with_get_arg setting_headers custom_access_log basic_authentication digest_authentication minimal_https minimal_file_response minimal_deferred url_registration minimal_ip_ban benchmark_select benchmark_threads benchmark_nodelay deferred_with_accumulator hello_world_SOURCES = hello_world.cpp service_SOURCES = service.cpp minimal_hello_world_SOURCES = minimal_hello_world.cpp +hello_world_websocket_SOURCES = hello_world_websocket.cpp custom_error_SOURCES = custom_error.cpp allowing_disallowing_methods_SOURCES = allowing_disallowing_methods.cpp handlers_SOURCES = handlers.cpp diff --git a/examples/hello_world_websocket.cpp b/examples/hello_world_websocket.cpp new file mode 100755 index 00000000..fd59c066 --- /dev/null +++ b/examples/hello_world_websocket.cpp @@ -0,0 +1,107 @@ +/* + This file is part of libhttpserver + Copyright (C) 2011, 2012, 2013, 2014, 2015 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 + +#define CHAT_PAGE \ + "\n" \ + "\n" \ + "WebSocket chat\n" \ + "\n" \ + "\n" \ + "\n" \ + "\n" \ + "

\n" \ + "\n" \ + "\n" \ + "" + +class hello_world_resource : public httpserver::http_resource, public httpserver::websocket_handler { + public: + const std::shared_ptr render(const httpserver::http_request&); + virtual std::thread handle_websocket(httpserver::websocket* ws) override; +}; + +// Using the render method you are able to catch each type of request you receive +const std::shared_ptr hello_world_resource::render(const httpserver::http_request& req) { + // It is possible to send a response initializing an http_string_response that reads the content to send in response from a string. + return std::shared_ptr(new httpserver::string_response(CHAT_PAGE, 200, "text/html")); +} + +std::thread hello_world_resource::handle_websocket(httpserver::websocket* ws) { + return std::thread([ws]{ + while (!ws->disconnect()) { + ws->send("hello world"); + usleep(1000 * 1000); + std::string message; + if (ws->receive(message, 100)) { + ws->send("server received: " + message); + } + } + }); +} + +int main() { + // It is possible to create a webserver passing a great number of parameters. In this case we are just passing the port and the number of thread running. + httpserver::webserver ws = httpserver::create_webserver(8080).start_method(httpserver::http::http_utils::INTERNAL_SELECT).max_threads(1); + + hello_world_resource hwr; + // This way we are registering the hello_world_resource to answer for the endpoint + // "/hello". The requested method is called (if the request is a GET we call the render_GET + // method. In case that the specific render method is not implemented, the generic "render" + // method is called. + ws.register_resource("/hello", &hwr, true); + ws.register_resource("/ws", &hwr, true); + + // This way we are putting the created webserver in listen. We pass true in order to have + // a blocking call; if we want the call to be non-blocking we can just pass false to the method. + ws.start(true); + return 0; +} diff --git a/src/Makefile.am b/src/Makefile.am index 5e549bbc..9f9544cf 100644 --- a/src/Makefile.am +++ b/src/Makefile.am @@ -19,7 +19,7 @@ AM_CPPFLAGS = -I../ -I$(srcdir)/httpserver/ METASOURCES = AUTO lib_LTLIBRARIES = libhttpserver.la -libhttpserver_la_SOURCES = string_utilities.cpp webserver.cpp http_utils.cpp http_request.cpp http_response.cpp string_response.cpp basic_auth_fail_response.cpp digest_auth_fail_response.cpp deferred_response.cpp file_response.cpp http_resource.cpp details/http_endpoint.cpp +libhttpserver_la_SOURCES = string_utilities.cpp webserver.cpp http_utils.cpp http_request.cpp http_response.cpp string_response.cpp websocket.cpp basic_auth_fail_response.cpp digest_auth_fail_response.cpp deferred_response.cpp file_response.cpp http_resource.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/details/http_endpoint.hpp httpserver/http_request.hpp httpserver/http_response.hpp httpserver/http_resource.hpp httpserver/string_response.hpp httpserver/basic_auth_fail_response.hpp httpserver/digest_auth_fail_response.hpp httpserver/deferred_response.hpp httpserver/file_response.hpp @@ -32,7 +32,7 @@ AM_LDFLAGS += -O0 --coverage -lgcov --no-inline endif if !COND_CROSS_COMPILE -libhttpserver_la_LIBADD = -lmicrohttpd +libhttpserver_la_LIBADD = -lmicrohttpd -lmicrohttpd_ws endif libhttpserver_la_CFLAGS = $(AM_CFLAGS) diff --git a/src/httpserver.hpp b/src/httpserver.hpp index 04eb251a..0e8c9ab7 100644 --- a/src/httpserver.hpp +++ b/src/httpserver.hpp @@ -29,6 +29,8 @@ #include "httpserver/file_response.hpp" #include "httpserver/http_request.hpp" #include "httpserver/http_resource.hpp" +#include "httpserver/websocket.hpp" +#include "httpserver/websocket_handler.hpp" #include "httpserver/http_response.hpp" #include "httpserver/http_utils.hpp" #include "httpserver/string_response.hpp" diff --git a/src/httpserver/webserver.hpp b/src/httpserver/webserver.hpp index 763ba86d..938fd7ab 100644 --- a/src/httpserver/webserver.hpp +++ b/src/httpserver/webserver.hpp @@ -51,6 +51,8 @@ namespace httpserver { class http_resource; } namespace httpserver { class http_response; } +namespace httpserver { class websocket; } +namespace httpserver { class websocket_handler; } namespace httpserver { namespace details { struct modded_request; } } struct MHD_Connection; @@ -182,6 +184,10 @@ class webserver { const std::shared_ptr internal_error_page(details::modded_request* mr, bool force_our = false) const; const std::shared_ptr not_found_page(details::modded_request* mr) const; + MHD_Result create_websocket_connection( + websocket_handler* ws_handler, + MHD_Connection *connection); + static void request_completed(void *cls, struct MHD_Connection *connection, void **con_cls, enum MHD_RequestTerminationCode toe); @@ -194,7 +200,19 @@ class webserver { const char *filename, const char *content_type, const char *transfer_encoding, const char *data, uint64_t off, size_t size); - static void upgrade_handler(void *cls, struct MHD_Connection* connection, void **con_cls, int upgrade_socket); + static int connecteduser_parse_received_websocket_stream (websocket* cu, + char* buf, + size_t buf_len); + + static void* connecteduser_receive_messages (void* cls); + + static void upgrade_handler(void *cls, + struct MHD_Connection *connection, + void *con_cls, + const char *extra_in, + size_t extra_in_size, + MHD_socket fd, + struct MHD_UpgradeResponseHandle *urh); MHD_Result requests_answer_first_step(MHD_Connection* connection, struct details::modded_request* mr); diff --git a/src/httpserver/websocket.hpp b/src/httpserver/websocket.hpp new file mode 100644 index 00000000..353140df --- /dev/null +++ b/src/httpserver/websocket.hpp @@ -0,0 +1,77 @@ +/* + 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_HPP_ +#define SRC_HTTPSERVER_WEBSOCKET_HPP_ + +#include +#include +#include +#include +#include +#include + +struct MHD_UpgradeResponseHandle; +struct MHD_WebSocketStream; + +namespace httpserver { + +class websocket { +public: + void send(const std::string& message); + std::string receive(); + bool receive(std::string& message, uint64_t timeout_milliseconds); + bool disconnect() const; +private: + /** + * Sends all data of the given buffer via the TCP/IP socket + * + * @param fd The TCP/IP socket which is used for sending + * @param buf The buffer with the data to send + * @param len The length in bytes of the data in the buffer + */ + void send_raw(const char* buf, size_t len); + void insert_into_receive_queue(const std::string& message); + + /* the TCP/IP socket for reading/writing */ + MHD_socket fd = 0; + /* the UpgradeResponseHandle of libmicrohttpd (needed for closing the socket) */ + MHD_UpgradeResponseHandle* urh = nullptr; + /* the websocket encode/decode stream */ + MHD_WebSocketStream* ws = nullptr; + /* the possibly read data at the start (only used once) */ + char *extra_in = nullptr; + size_t extra_in_size = 0; + /* specifies whether the websocket shall be closed (1) or not (0) */ + bool disconnect_ = false; + class websocket_handler* ws_handler = nullptr; + std::mutex receive_mutex_; + std::condition_variable receive_cv_; + std::list received_messages_; + friend class webserver; +}; + +} // namespace httpserver + +#endif // SRC_HTTPSERVER_STRING_UTILITIES_HPP_ diff --git a/src/httpserver/websocket_handler.hpp b/src/httpserver/websocket_handler.hpp new file mode 100644 index 00000000..333b8f38 --- /dev/null +++ b/src/httpserver/websocket_handler.hpp @@ -0,0 +1,53 @@ +/* + 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_ + +#include + +namespace httpserver { class websocket; } + +namespace httpserver { + +/** + * Class representing a callable websocket resource. +**/ +class websocket_handler { + public: + /** + * Class destructor + **/ + virtual ~websocket_handler() = default; + + /** + * Method used to handle a websocket connection + * @param ws Websocket + * @return A thread object handling the websocket + **/ + virtual std::thread handle_websocket(websocket* ws) = 0; +}; + +} // namespace httpserver +#endif // SRC_HTTPSERVER_WEBSOCKET_HANDLER_HPP_ diff --git a/src/webserver.cpp b/src/webserver.cpp index 7d50fe00..81324806 100644 --- a/src/webserver.cpp +++ b/src/webserver.cpp @@ -32,6 +32,10 @@ #include #endif +#include +#include +#include + #include #include #include @@ -49,6 +53,8 @@ #include #include "httpserver/create_webserver.hpp" +#include "httpserver/websocket.hpp" +#include "httpserver/websocket_handler.hpp" #include "httpserver/details/http_endpoint.hpp" #include "httpserver/details/modded_request.hpp" #include "httpserver/http_request.hpp" @@ -69,6 +75,9 @@ struct MHD_Connection; typedef int MHD_Result; #endif +#define PAGE_INVALID_WEBSOCKET_REQUEST \ + "Invalid WebSocket request!" + using std::string; using std::pair; using std::vector; @@ -305,6 +314,8 @@ bool webserver::start(bool blocking) { if (deferred_enabled) { start_conf |= MHD_USE_SUSPEND_RESUME; } + start_conf |= MHD_USE_ERROR_LOG; + start_conf |= MHD_ALLOW_UPGRADE; #ifdef USE_FASTOPEN start_conf |= MHD_USE_TCP_FASTOPEN; @@ -464,13 +475,6 @@ MHD_Result webserver::post_iterator(void *cls, enum MHD_ValueKind kind, return MHD_YES; } -void webserver::upgrade_handler(void *cls, struct MHD_Connection* connection, void **con_cls, int upgrade_socket) { - std::ignore = cls; - std::ignore = connection; - std::ignore = con_cls; - std::ignore = upgrade_socket; -} - const std::shared_ptr webserver::not_found_page(details::modded_request* mr) const { if (not_found_resource != 0x0) { return not_found_resource(*mr->dhr); @@ -536,6 +540,457 @@ MHD_Result webserver::requests_answer_second_step(MHD_Connection* connection, co return MHD_YES; } +/** + * Change socket to blocking. + * + * @param fd the socket to manipulate + */ +static void +make_blocking (MHD_socket fd) +{ +#if defined(MHD_POSIX_SOCKETS) + int flags; + + flags = fcntl (fd, F_GETFL); + if (-1 == flags) + return; + if ((flags & ~O_NONBLOCK) != flags) + if (-1 == fcntl (fd, F_SETFL, flags & ~O_NONBLOCK)) + abort (); +#elif defined(MHD_WINSOCK_SOCKETS) + unsigned long flags = 0; + + ioctlsocket (fd, FIONBIO, &flags); +#endif /* MHD_WINSOCK_SOCKETS */ + +} + +/** +* Parses received data from the TCP/IP socket with the websocket stream +* +* @param cu The connected user +* @param new_name The new user name +* @param new_name_len The length of the new name +* @return 0 on success, other values on error +*/ +int webserver::connecteduser_parse_received_websocket_stream (websocket* cu, + char* buf, + size_t buf_len) +{ + size_t buf_offset = 0; + while (buf_offset < buf_len) + { + size_t new_offset = 0; + char *frame_data = NULL; + size_t frame_len = 0; + int status = MHD_websocket_decode (cu->ws, + buf + buf_offset, + buf_len - buf_offset, + &new_offset, + &frame_data, + &frame_len); + if (0 > status) + { + /* an error occurred and the connection must be closed */ + if (NULL != frame_data) + { + /* depending on the WebSocket flag */ + /* MHD_WEBSOCKET_FLAG_GENERATE_CLOSE_FRAMES_ON_ERROR */ + /* close frames might be generated on errors */ + cu->send_raw(frame_data, frame_len); + MHD_websocket_free (cu->ws, frame_data); + } + return 1; + } + else + { + buf_offset += new_offset; + + if (0 < status) + { + /* the frame is complete */ + switch (status) + { + case MHD_WEBSOCKET_STATUS_TEXT_FRAME: + case MHD_WEBSOCKET_STATUS_BINARY_FRAME: + /** + * a text or binary frame has been received. + * in this chat server example we use a simple protocol where + * the JavaScript added a prefix like "||data". + * Some examples: + * "||test" means a regular chat message to everyone with the message "test". + * "private|1|secret" means a private chat message to user with id 1 with the message "secret". + * "name||MyNewName" means that the user requests a rename to "MyNewName" + * "ping|1|" means that the user with id 1 shall get a ping + * + * Binary data is handled here like text data. + * The difference in the data is only checked by the JavaScript. + */ + cu->insert_into_receive_queue(std::string(frame_data, frame_len)); + MHD_websocket_free (cu->ws, + frame_data); + return 0; + + case MHD_WEBSOCKET_STATUS_CLOSE_FRAME: + /* if we receive a close frame, we will respond with one */ + MHD_websocket_free (cu->ws, + frame_data); + { + char*result = NULL; + size_t result_len = 0; + int er = MHD_websocket_encode_close (cu->ws, + MHD_WEBSOCKET_CLOSEREASON_REGULAR, + NULL, + 0, + &result, + &result_len); + if (MHD_WEBSOCKET_STATUS_OK == er) + { + cu->send_raw(result, result_len); + MHD_websocket_free (cu->ws, result); + } + } + return 1; + + case MHD_WEBSOCKET_STATUS_PING_FRAME: + /* if we receive a ping frame, we will respond */ + /* with the corresponding pong frame */ + std::cerr << "ping not yet implemented" << std::endl; + return 0; + + case MHD_WEBSOCKET_STATUS_PONG_FRAME: + /* if we receive a pong frame, */ + /* we will check whether we requested this frame and */ + /* whether it is the last requested pong */ + std::cerr << "pong not yet implemented" << std::endl; + return 0; + + default: + /* This case should really never happen, */ + /* because there are only five types of (finished) websocket frames. */ + /* If it is ever reached, it means that there is memory corruption. */ + MHD_websocket_free (cu->ws, + frame_data); + return 1; + } + } + } + } + + return 0; +} + +/** + * Receives messages from the TCP/IP socket and + * initializes the connected user. + * + * @param cls The connected user + * @return Always NULL + */ +void* webserver::connecteduser_receive_messages (void* cls) +{ + websocket* cu = (websocket*)cls; + + /* make the socket blocking */ + make_blocking (cu->fd); + + /* initialize the web socket stream for encoding/decoding */ + int result = MHD_websocket_stream_init (&cu->ws, + MHD_WEBSOCKET_FLAG_SERVER + | MHD_WEBSOCKET_FLAG_NO_FRAGMENTS, + 0); + if (MHD_WEBSOCKET_STATUS_OK != result) + { + MHD_upgrade_action (cu->urh, + MHD_UPGRADE_ACTION_CLOSE); + free (cu); + return NULL; + } + + /* start the message-send thread */ + std::thread thread = cu->ws_handler->handle_websocket(cu); + + /* start by parsing extra data MHD may have already read, if any */ + if (0 != cu->extra_in_size) + { + if (0 != connecteduser_parse_received_websocket_stream (cu, + cu->extra_in, + cu->extra_in_size)) + { + cu->disconnect_ = true; + thread.join(); + struct MHD_UpgradeResponseHandle* urh = cu->urh; + if (NULL != urh) + { + cu->urh = NULL; + MHD_upgrade_action (urh, + MHD_UPGRADE_ACTION_CLOSE); + } + MHD_websocket_stream_free (cu->ws); + free (cu->extra_in); + free (cu); + return NULL; + } + free (cu->extra_in); + cu->extra_in = NULL; + } + + /* the main loop for receiving data */ + while (1) + { + char buf[128]; + ssize_t got = recv (cu->fd, + buf, + sizeof (buf), + 0); + if (0 >= got) + { + /* the TCP/IP socket has been closed */ + break; + } + if (0 < got) + { + if (0 != connecteduser_parse_received_websocket_stream (cu, buf, + (size_t) got)) + { + /* A websocket protocol error occurred */ + cu->disconnect_ = true; + thread.join(); + struct MHD_UpgradeResponseHandle*urh = cu->urh; + if (NULL != urh) + { + cu->urh = NULL; + MHD_upgrade_action (urh, + MHD_UPGRADE_ACTION_CLOSE); + } + MHD_websocket_stream_free (cu->ws); + free (cu); + return NULL; + } + } + } + + /* cleanup */ + cu->disconnect_ = true; + thread.join(); + struct MHD_UpgradeResponseHandle* urh = cu->urh; + if (NULL != urh) + { + cu->urh = NULL; + MHD_upgrade_action (urh, + MHD_UPGRADE_ACTION_CLOSE); + } + MHD_websocket_stream_free (cu->ws); + free (cu); + + return NULL; +} + +/** + * Function called after a protocol "upgrade" response was sent + * successfully and the socket should now be controlled by some + * protocol other than HTTP. + * + * Any data already received on the socket will be made available in + * @e extra_in. This can happen if the application sent extra data + * before MHD send the upgrade response. The application should + * treat data from @a extra_in as if it had read it from the socket. + * + * Note that the application must not close() @a sock directly, + * but instead use #MHD_upgrade_action() for special operations + * on @a sock. + * + * Data forwarding to "upgraded" @a sock will be started as soon + * as this function return. + * + * Except when in 'thread-per-connection' mode, implementations + * of this function should never block (as it will still be called + * from within the main event loop). + * + * @param cls closure, whatever was given to #MHD_create_response_for_upgrade(). + * @param connection original HTTP connection handle, + * giving the function a last chance + * to inspect the original HTTP request + * @param con_cls last value left in `con_cls` of the `MHD_AccessHandlerCallback` + * @param extra_in if we happened to have read bytes after the + * HTTP header already (because the client sent + * more than the HTTP header of the request before + * we sent the upgrade response), + * these are the extra bytes already read from @a sock + * by MHD. The application should treat these as if + * it had read them from @a sock. + * @param extra_in_size number of bytes in @a extra_in + * @param sock socket to use for bi-directional communication + * with the client. For HTTPS, this may not be a socket + * that is directly connected to the client and thus certain + * operations (TCP-specific setsockopt(), getsockopt(), etc.) + * may not work as expected (as the socket could be from a + * socketpair() or a TCP-loopback). The application is expected + * to perform read()/recv() and write()/send() calls on the socket. + * The application may also call shutdown(), but must not call + * close() directly. + * @param urh argument for #MHD_upgrade_action()s on this @a connection. + * Applications must eventually use this callback to (indirectly) + * perform the close() action on the @a sock. + */ +void webserver::upgrade_handler (void *cls, + struct MHD_Connection *connection, + void *con_cls, + const char *extra_in, + size_t extra_in_size, + MHD_socket fd, + struct MHD_UpgradeResponseHandle *urh) +{ + pthread_t pt; + (void) connection; /* Unused. Silent compiler warning. */ + (void) con_cls; /* Unused. Silent compiler warning. */ + + /* This callback must return as soon as possible. */ + + /* allocate new connected user */ + websocket* cu = new websocket(); + if (0 != extra_in_size) + { + cu->extra_in = (char*)malloc (extra_in_size); + if (NULL == cu->extra_in) + abort (); + memcpy (cu->extra_in, + extra_in, + extra_in_size); + } + cu->extra_in_size = extra_in_size; + cu->fd = fd; + cu->urh = urh; + cu->ws_handler = (websocket_handler*)cls; + + /* create thread for the new connected user */ + if (0 != pthread_create (&pt, + NULL, + &connecteduser_receive_messages, + cu)) + abort (); + pthread_detach (pt); +} + +MHD_Result webserver::create_websocket_connection( + websocket_handler* ws_handler, + MHD_Connection *connection) +{ + /** + * The path for the chat has been accessed. + * For a valid WebSocket request, at least five headers are required: + * 1. "Host: " + * 2. "Connection: Upgrade" + * 3. "Upgrade: websocket" + * 4. "Sec-WebSocket-Version: 13" + * 5. "Sec-WebSocket-Key: " + * Values are compared in a case-insensitive manner. + * Furthermore it must be a HTTP/1.1 or higher GET request. + * See: https://tools.ietf.org/html/rfc6455#section-4.2.1 + * + * To ease this example we skip the following checks: + * - Whether the HTTP version is 1.1 or newer + * - Whether Connection is Upgrade, because this header may + * contain multiple values. + * - The requested Host (because we don't know) + */ + + MHD_Result ret; + + /* check whether a websocket upgrade is requested */ + const char*value = MHD_lookup_connection_value (connection, + MHD_HEADER_KIND, + MHD_HTTP_HEADER_UPGRADE); + if ((0 == value) || (0 != strcasecmp (value, "websocket"))) + { + struct MHD_Response*response = MHD_create_response_from_buffer (strlen ( + PAGE_INVALID_WEBSOCKET_REQUEST), + (void*)PAGE_INVALID_WEBSOCKET_REQUEST, + MHD_RESPMEM_PERSISTENT); + ret = MHD_queue_response (connection, + MHD_HTTP_BAD_REQUEST, + response); + MHD_destroy_response (response); + return ret; + } + + /* check the protocol version */ + value = MHD_lookup_connection_value (connection, + MHD_HEADER_KIND, + MHD_HTTP_HEADER_SEC_WEBSOCKET_VERSION); + if ((0 == value) || (0 != strcasecmp (value, "13"))) + { + struct MHD_Response*response = MHD_create_response_from_buffer (strlen ( + PAGE_INVALID_WEBSOCKET_REQUEST), + (void*)PAGE_INVALID_WEBSOCKET_REQUEST, + MHD_RESPMEM_PERSISTENT); + ret = MHD_queue_response (connection, + MHD_HTTP_BAD_REQUEST, + response); + MHD_destroy_response (response); + return ret; + } + + /* read the websocket key (required for the response) */ + value = MHD_lookup_connection_value (connection, MHD_HEADER_KIND, + MHD_HTTP_HEADER_SEC_WEBSOCKET_KEY); + if (0 == value) + { + struct MHD_Response*response = MHD_create_response_from_buffer (strlen ( + PAGE_INVALID_WEBSOCKET_REQUEST), + (void*)PAGE_INVALID_WEBSOCKET_REQUEST, + MHD_RESPMEM_PERSISTENT); + ret = MHD_queue_response (connection, + MHD_HTTP_BAD_REQUEST, + response); + MHD_destroy_response (response); + return ret; + } + + /* generate the response accept header */ + char sec_websocket_accept[29]; + if (0 != MHD_websocket_create_accept (value, sec_websocket_accept)) + { + struct MHD_Response*response = MHD_create_response_from_buffer (strlen ( + PAGE_INVALID_WEBSOCKET_REQUEST), + (void*)PAGE_INVALID_WEBSOCKET_REQUEST, + MHD_RESPMEM_PERSISTENT); + ret = MHD_queue_response (connection, + MHD_HTTP_BAD_REQUEST, + response); + MHD_destroy_response (response); + return ret; + } + + /* create the response for upgrade */ + MHD_Response* response = MHD_create_response_for_upgrade( + &upgrade_handler, ws_handler); + + /** + * For the response we need at least the following headers: + * 1. "Connection: Upgrade" + * 2. "Upgrade: websocket" + * 3. "Sec-WebSocket-Accept: " + * The value for Sec-WebSocket-Accept can be generated with MHD_websocket_create_accept. + * It requires the value of the Sec-WebSocket-Key header of the request. + * See also: https://tools.ietf.org/html/rfc6455#section-4.2.2 + */ + MHD_add_response_header (response, + MHD_HTTP_HEADER_CONNECTION, + "Upgrade"); + MHD_add_response_header (response, + MHD_HTTP_HEADER_UPGRADE, + "websocket"); + MHD_add_response_header (response, + MHD_HTTP_HEADER_SEC_WEBSOCKET_ACCEPT, + sec_websocket_accept); + ret = MHD_queue_response (connection, + MHD_HTTP_SWITCHING_PROTOCOLS, + response); + MHD_destroy_response (response); + return ret; +} + MHD_Result webserver::finalize_answer(MHD_Connection* connection, struct details::modded_request* mr, const char* method) { int to_ret = MHD_NO; @@ -595,9 +1050,25 @@ MHD_Result webserver::finalize_answer(MHD_Connection* connection, struct details if (found) { try { if (hrm->is_allowed(method)) { - mr->dhrs = ((hrm)->*(mr->callback))(*mr->dhr); // copy in memory (move in case) - if (mr->dhrs->get_response_code() == -1) { - mr->dhrs = internal_error_page(mr); + bool is_websocket = false; + if (mr->callback == &http_resource::render_GET) { + const char* value = MHD_lookup_connection_value (connection, + MHD_HEADER_KIND, + MHD_HTTP_HEADER_UPGRADE); + is_websocket = ((0 != value) && (0 == strcasecmp (value, "websocket"))); + } + if (is_websocket) { + websocket_handler* ws_handler = dynamic_cast(hrm); + if (ws_handler == nullptr) { + mr->dhrs = internal_error_page(mr); + } else { + return create_websocket_connection(ws_handler, connection); + } + } else { + mr->dhrs = ((hrm)->*(mr->callback))(*mr->dhr); // copy in memory (move in case) + if (mr->dhrs->get_response_code() == -1) { + mr->dhrs = internal_error_page(mr); + } } } else { mr->dhrs = method_not_allowed_page(mr); diff --git a/src/websocket.cpp b/src/websocket.cpp new file mode 100644 index 00000000..e1767ecf --- /dev/null +++ b/src/websocket.cpp @@ -0,0 +1,80 @@ +#include "httpserver/websocket.hpp" + +#include +#include +#include +#include + +using namespace httpserver; + +void websocket::send(const std::string& message) { + /* a chat message or command is pending */ + char* frame_data = NULL; + size_t frame_len = 0; + int er = MHD_websocket_encode_text (ws, + message.data(), + message.size(), + MHD_WEBSOCKET_FRAGMENTATION_NONE, + &frame_data, + &frame_len, + NULL); + /* send the data via the TCP/IP socket */ + if (MHD_WEBSOCKET_STATUS_OK == er) + { + send_raw(frame_data, + frame_len); + } + MHD_websocket_free (ws, + frame_data); +} + +void websocket::send_raw(const char* buf, size_t len) { + ssize_t ret; + size_t off; + + for (off = 0; off < len; off += ret) + { + ret = ::send(fd, + &buf[off], + (int) (len - off), + 0); + if (0 > ret) + { + if (EAGAIN == errno) + { + ret = 0; + continue; + } + break; + } + if (0 == ret) + break; + } +} + +void websocket::insert_into_receive_queue(const std::string& message) { + std::unique_lock lck(receive_mutex_); + received_messages_.push_front(message); +} + +std::string websocket::receive() { + std::unique_lock lck(receive_mutex_); + while (received_messages_.empty()) receive_cv_.wait(lck); + std::string result = std::move(received_messages_.back()); + received_messages_.pop_back(); + return result; +} + +bool websocket::receive(std::string& message, uint64_t timeout_milliseconds) { + std::unique_lock lck(receive_mutex_); + if (!receive_cv_.wait_for(lck, std::chrono::milliseconds{timeout_milliseconds}, [this](){return !received_messages_.empty();})) { + return false; + } + message = std::move(received_messages_.back()); + received_messages_.pop_back(); + return true; +} + +bool websocket::disconnect() const { + return disconnect_; +}