diff options
Diffstat (limited to 'src/microhttpd_ws/test_websocket_browser.c')
-rw-r--r-- | src/microhttpd_ws/test_websocket_browser.c | 563 |
1 files changed, 563 insertions, 0 deletions
diff --git a/src/microhttpd_ws/test_websocket_browser.c b/src/microhttpd_ws/test_websocket_browser.c new file mode 100644 index 00000000..dfbcd116 --- /dev/null +++ b/src/microhttpd_ws/test_websocket_browser.c @@ -0,0 +1,563 @@ +/* + This file is part of libmicrohttpd + Copyright (C) 2021 David Gausmann + + libmicrohttpd is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published + by the Free Software Foundation; either version 3, or (at your + option) any later version. + + libmicrohttpd 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 + General Public License for more details. + + You should have received a copy of the GNU General Public License + along with libmicrohttpd; see the file COPYING. If not, write to the + Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, + Boston, MA 02110-1301, USA. +*/ +/** + * @file test_websocket_browser.c + * @brief Testcase for WebSocket decoding/encoding with external browser + * @author David Gausmann + */ +#include <sys/types.h> +#ifndef _WIN32 +#include <sys/select.h> +#include <sys/socket.h> +#include <fcntl.h> +#else +#include <winsock2.h> +#endif +#include "microhttpd.h" +#include "microhttpd_ws.h" +#include <stdlib.h> +#include <string.h> +#include <stdio.h> +#include <stdint.h> +#include <time.h> +#include <errno.h> + +#define PORT 80 + +#define PAGE \ + "<!DOCTYPE html>\n" \ + "<html>\n" \ + "<head>\n" \ + "<meta charset=\"UTF-8\">\n" \ + "<title>Websocket External Test with Webbrowser</title>\n" \ + "<script>\n" \ + "\n" \ + "let current_mode = 0;\n" \ + "let current_step = 0;\n" \ + "let sent_payload = null;\n" \ + "let charset = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_!@%&/\\\\';\n" \ + "let step_to_bytes = [ 0, 1, 2, 3, 122, 123, 124, 125, 126, 127, 128, 32766, 32767, 32768, 65534, 65535, 65536, 65537, 1048576, 10485760 ];\n" \ + "let url = 'ws' + (window.location.protocol === 'https:' ? 's' : '')" \ + " + '://' +\n" \ + " window.location.host + '/websocket';\n" \ + "let socket = null;\n" \ + "\n" \ + "window.onload = function (event) {\n" \ + " if (!window.WebSocket) {\n" \ + " document.write ('ERROR: The WebSocket class is not supported by your browser.<br>');\n" \ + " }\n" \ + " if (!window.fetch) {\n" \ + " document.write ('ERROR: The fetch-API is not supported by your browser.<br>');\n" \ + " }\n" \ + " document.write ('Starting tests.<br>');\n" \ + " runTest ();\n" \ + "}\n" \ + "\n" \ + "function runTest () {\n" \ + " switch (current_mode) {\n" \ + " case 0:\n" \ + " document.write ('TEXT');\n" \ + " break;\n" \ + " case 1:\n" \ + " document.write ('BINARY');\n" \ + " break;\n" \ + " }\n" \ + " document.write (', ' + step_to_bytes[current_step] + ' Bytes: ');\n" \ + " socket = new WebSocket(url);\n" \ + " socket.binaryType = 'arraybuffer';\n" \ + " socket.onopen = function (event) {\n" \ + " switch (current_mode) {\n" \ + " case 0:\n" \ + " sent_payload = randomText (step_to_bytes[current_step]);\n" \ + " socket.send (sent_payload);\n" \ + " break;\n" \ + " case 1:\n" \ + " sent_payload = randomBinary (step_to_bytes[current_step]);\n" \ + " socket.send (sent_payload);\n" \ + " break;\n" \ + " }\n" \ + " }\n" \ + "\n" \ + " socket.onclose = function (event) {\n" \ + " socket.onmessage = null;\n" \ + " socket.onclose = null;\n" \ + " socket.onerror = null;\n" \ + " document.write ('CLOSED unexpectedly.<br>');\n" \ + " notifyError ();\n" \ + " }\n" \ + "\n" \ + " socket.onerror = function (event) {\n" \ + " socket.onmessage = null;\n" \ + " socket.onclose = null;\n" \ + " socket.onerror = null;\n" \ + " document.write ('ERROR.<br>');\n" \ + " notifyError ();\n" \ + " }\n" \ + "\n" \ + " socket.onmessage = async function (event) {\n" \ + " if (compareData (event.data, sent_payload)) {\n" \ + " document.write ('SUCCESS.<br>');\n" \ + " socket.onmessage = null;\n" \ + " socket.onclose = null;\n" \ + " socket.onerror = null;\n" \ + " socket.close();\n" \ + " socket = null;\n" \ + " if (step_to_bytes.length <= ++current_step) {\n" \ + " current_step = 0;\n" \ + " if (1 < ++current_mode) {\n" \ + " document.write ('FINISHED ALL TESTS.<br>');\n" \ + " return;\n" \ + " }\n" \ + " }\n" \ + " runTest ();\n" \ + " }" \ + " }\n" \ + "}\n" \ + "\n" \ + "function compareData (data, data2) {\n" \ + " if (typeof (data) === 'string' && typeof (data2) === 'string') {\n" \ + " return (data === data2); \n" \ + " } \n" \ + " else if ((data instanceof ArrayBuffer) && (data2 instanceof ArrayBuffer)) {\n" \ + " let view1 = new Uint8Array (data);\n" \ + " let view2 = new Uint8Array (data2);\n" \ + " if (view1.length != view2.length)\n" \ + " return false;\n" \ + " for (let i = 0; i < view1.length; ++i) {\n" \ + " if (view1[i] !== view2[i])\n" \ + " return false;\n" \ + " }\n" \ + " return true;\n" \ + " }\n" \ + " else\n" \ + " {\n" \ + " return false;\n" \ + " }\n" \ + "}\n" \ + "\n" \ + "function randomText (length) {\n" \ + " let result = new Array (length);\n" \ + " for (let i = 0; i < length; ++i)\n" \ + " result [i] = charset [~~(Math.random () * charset.length)];\n" \ + " return result.join ('');\n" \ + "}\n" \ + "\n" \ + "function randomBinary (length) {\n" \ + " let buffer = new ArrayBuffer (length);\n" \ + " let view = new Uint8Array (buffer);\n" \ + " for (let i = 0; i < length; ++i)\n" \ + " view [i] = ~~(Math.random () * 256);\n" \ + " return buffer;\n" \ + "}\n" \ + "\n" \ + "function notifyError () {\n" \ + " fetch('error/' + (0 == current_mode ? 'text' : 'binary') + '/' + step_to_bytes[current_step]);\n" \ + "}\n" \ + "\n" \ + "</script>\n" \ + "</head>\n" \ + "<body>\n" \ + "</body>\n" \ + "</html>" + +#define PAGE_NOT_FOUND \ + "404 Not Found" + +#define PAGE_INVALID_WEBSOCKET_REQUEST \ + "Invalid WebSocket request!" + +static void +send_all (MHD_socket fd, + const char *buf, + size_t len); +static void +make_blocking (MHD_socket fd); + +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) +{ + /* make the socket blocking (operating-system-dependent code) */ + make_blocking (fd); + + /* create a websocket stream for this connection */ + struct MHD_WebSocketStream* ws; + int result = MHD_websocket_stream_init (&ws, + 0, + 0); + if (0 != result) + { + /* Couldn't create the websocket stream. + * So we close the socket and leave + */ + MHD_upgrade_action (urh, + MHD_UPGRADE_ACTION_CLOSE); + return; + } + + /* Let's wait for incoming data */ + const size_t buf_len = 256; + char buf[buf_len]; + ssize_t got; + while (MHD_WEBSOCKET_VALIDITY_VALID == MHD_websocket_stream_is_valid (ws)) + { + got = recv (fd, + buf, + sizeof (buf), + 0); + if (0 >= got) + { + /* the TCP/IP socket has been closed */ + fprintf (stderr, + "Error (The socket has been closed unexpectedly)\n"); + break; + } + + /* parse the entire received data */ + size_t buf_offset = 0; + while (buf_offset < (size_t) got) + { + size_t new_offset = 0; + char *payload_data = NULL; + size_t payload_len = 0; + char *frame_data = NULL; + size_t frame_len = 0; + int status = MHD_websocket_decode (ws, + buf + buf_offset, + ((size_t) got) - buf_offset, + &new_offset, + &payload_data, + &payload_len); + if (0 > status) + { + /* an error occurred and the connection must be closed */ + printf ("Decoding failed: status=%d, passed=%u\n", status, ((size_t) got) - buf_offset); + if (NULL != payload_data) + { + MHD_websocket_free (ws, payload_data); + } + break; + } + else + { + buf_offset += new_offset; + if (0 < status) + { + /* the frame is complete */ + printf ("Decoding succeeded: type=%d, passed=%u, parsed=%u, payload_len=%d\n", status, ((size_t) got) - buf_offset, new_offset, payload_len); + switch (status) + { + case MHD_WEBSOCKET_STATUS_TEXT_FRAME: + case MHD_WEBSOCKET_STATUS_BINARY_FRAME: + /* The client has sent some data. */ + if (NULL != payload_data || 0 == payload_len) + { + /* Send the received data back to the client */ + if (MHD_WEBSOCKET_STATUS_TEXT_FRAME == status) + { + result = MHD_websocket_encode_text (ws, + payload_data, + payload_len, + 0, + &frame_data, + &frame_len, + NULL); + } + else + { + result = MHD_websocket_encode_binary (ws, + payload_data, + payload_len, + 0, + &frame_data, + &frame_len); + } + if (0 == result) + { + send_all (fd, + frame_data, + frame_len); + } + } + else + { + /* should never happen */ + fprintf (stderr, + "Error (Empty buffer with payload_len != 0)\n"); + } + break; + + default: + /* Other frame types are ignored + * in this test script. + */ + break; + } + } + if (NULL != payload_data) + { + MHD_websocket_free (ws, payload_data); + } + if (NULL != frame_data) + { + MHD_websocket_free (ws, frame_data); + } + } + } + } + + /* free the websocket stream */ + MHD_websocket_stream_free (ws); + + /* close the socket when it is not needed anymore */ + MHD_upgrade_action (urh, + MHD_UPGRADE_ACTION_CLOSE); +} + +/* This helper function is used for the case that + * we need to resend some data + */ +static void +send_all (MHD_socket fd, + 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; + } +} + +/* This helper function contains operating-system-dependent code and + * is used to make a socket blocking. + */ +static void +make_blocking (MHD_socket fd) +{ +#ifndef _WIN32 + 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 (); +#else + unsigned long flags = 0; + + ioctlsocket (fd, FIONBIO, &flags); +#endif +} + +static enum MHD_Result +access_handler (void *cls, + struct MHD_Connection *connection, + const char *url, + const char *method, + const char *version, + const char *upload_data, + size_t *upload_data_size, + void **ptr) +{ + static int aptr; + struct MHD_Response *response; + int ret; + + (void) cls; /* Unused. Silent compiler warning. */ + (void) upload_data; /* Unused. Silent compiler warning. */ + (void) upload_data_size; /* Unused. Silent compiler warning. */ + + if (0 != strcmp (method, "GET")) + return MHD_NO; /* unexpected method */ + if (&aptr != *ptr) + { + /* do never respond on first call */ + *ptr = &aptr; + return MHD_YES; + } + *ptr = NULL; /* reset when done */ + + if (0 == strcmp (url, "/")) + { + /* Default page for visiting the server */ + struct MHD_Response *response = MHD_create_response_from_buffer ( + strlen (PAGE), + PAGE, + MHD_RESPMEM_PERSISTENT); + ret = MHD_queue_response (connection, + MHD_HTTP_OK, + response); + MHD_destroy_response (response); + } + else if (0 == strncmp (url, "/error/", 7)) + { + /* Report error */ + fprintf (stderr, "Error in test (%s)\n", url + 7); + + struct MHD_Response *response = MHD_create_response_from_buffer ( + 0, + "", + MHD_RESPMEM_PERSISTENT); + ret = MHD_queue_response (connection, + MHD_HTTP_OK, + response); + MHD_destroy_response (response); + } + else if (0 == strcmp (url, "/websocket")) + { + char is_valid = 1; + const char* value = NULL; + char sec_websocket_accept[29]; + + if (0 != MHD_websocket_check_http_version (version)) + { + is_valid = 0; + } + value = MHD_lookup_connection_value (connection, + MHD_HEADER_KIND, + MHD_HTTP_HEADER_CONNECTION); + if (0 != MHD_websocket_check_connection_header (value)) + { + is_valid = 0; + } + value = MHD_lookup_connection_value (connection, + MHD_HEADER_KIND, + MHD_HTTP_HEADER_UPGRADE); + if (0 != MHD_websocket_check_upgrade_header (value)) + { + is_valid = 0; + } + value = MHD_lookup_connection_value (connection, + MHD_HEADER_KIND, + MHD_HTTP_HEADER_SEC_WEBSOCKET_VERSION); + if (0 != MHD_websocket_check_version_header (value)) + { + is_valid = 0; + } + value = MHD_lookup_connection_value (connection, + MHD_HEADER_KIND, + MHD_HTTP_HEADER_SEC_WEBSOCKET_KEY); + if (0 != MHD_websocket_create_accept_header (value, sec_websocket_accept)) + { + is_valid = 0; + } + + if (1 == is_valid) + { + /* upgrade the connection */ + response = MHD_create_response_for_upgrade (&upgrade_handler, + NULL); + 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); + } + else + { + /* return error page */ + struct MHD_Response*response = MHD_create_response_from_buffer ( + strlen (PAGE_INVALID_WEBSOCKET_REQUEST), + PAGE_INVALID_WEBSOCKET_REQUEST, + MHD_RESPMEM_PERSISTENT); + ret = MHD_queue_response (connection, + MHD_HTTP_BAD_REQUEST, + response); + MHD_destroy_response (response); + } + } + else + { + struct MHD_Response*response = MHD_create_response_from_buffer ( + strlen (PAGE_NOT_FOUND), + PAGE_NOT_FOUND, + MHD_RESPMEM_PERSISTENT); + ret = MHD_queue_response (connection, + MHD_HTTP_NOT_FOUND, + response); + MHD_destroy_response (response); + } + + return ret; +} + +int +main (int argc, + char *const *argv) +{ + (void) argc; /* Unused. Silent compiler warning. */ + (void) argv; /* Unused. Silent compiler warning. */ + struct MHD_Daemon *daemon; + + daemon = MHD_start_daemon (MHD_USE_INTERNAL_POLLING_THREAD | + MHD_USE_THREAD_PER_CONNECTION | + MHD_ALLOW_UPGRADE | + MHD_USE_ERROR_LOG, + PORT, NULL, NULL, + &access_handler, NULL, + MHD_OPTION_END); + + if (NULL == daemon) + { + fprintf (stderr, "Error (Couldn't start daemon for testing)\n"); + return 1; + } + printf("The server is listening now.\n"); + printf("Access the server now with a websocket-capable webbrowser.\n\n"); + printf("Press return to close.\n"); + + (void) getc (stdin); + + MHD_stop_daemon (daemon); + + return 0; +} + |