diff options
| author | Raymaekers Luca <luca@spacehb.net> | 2025-10-02 12:42:29 +0200 |
|---|---|---|
| committer | Raymaekers Luca <luca@spacehb.net> | 2025-10-02 12:42:29 +0200 |
| commit | 02841e3c67c92006f9fe08efcda0d15f3d7283c5 (patch) | |
| tree | c3b79a2c7e14473e1e9244df1af318511ab86ca1 /ws/libs/wsServer | |
| parent | aadff4d1cf17cc23f6f38ab0da51baabfeb9f9d1 (diff) | |
checkpoint
Diffstat (limited to 'ws/libs/wsServer')
| -rw-r--r-- | ws/libs/wsServer/include/base64.h | 19 | ||||
| -rw-r--r-- | ws/libs/wsServer/include/sha1.h | 73 | ||||
| -rw-r--r-- | ws/libs/wsServer/include/utf8.h | 37 | ||||
| -rw-r--r-- | ws/libs/wsServer/include/ws.h | 337 | ||||
| -rw-r--r-- | ws/libs/wsServer/src/base64.c | 155 | ||||
| -rw-r--r-- | ws/libs/wsServer/src/handshake.c | 144 | ||||
| -rw-r--r-- | ws/libs/wsServer/src/sha1.c | 389 | ||||
| -rw-r--r-- | ws/libs/wsServer/src/utf8.c | 89 | ||||
| -rw-r--r-- | ws/libs/wsServer/src/ws.c | 2101 |
9 files changed, 3344 insertions, 0 deletions
diff --git a/ws/libs/wsServer/include/base64.h b/ws/libs/wsServer/include/base64.h new file mode 100644 index 0000000..5ea7f13 --- /dev/null +++ b/ws/libs/wsServer/include/base64.h @@ -0,0 +1,19 @@ +/* + * Base64 encoding/decoding (RFC1341) + * Copyright (c) 2005, Jouni Malinen <j@w1.fi> + * + * This software may be distributed under the terms of the BSD license. + * See README for more details. + */ + +#ifndef BASE64_H +#define BASE64_H + +#include <sys/types.h> + +unsigned char * base64_encode(const unsigned char *src, size_t len, + size_t *out_len); +unsigned char * base64_decode(const unsigned char *src, size_t len, + size_t *out_len); + +#endif /* BASE64_H */ diff --git a/ws/libs/wsServer/include/sha1.h b/ws/libs/wsServer/include/sha1.h new file mode 100644 index 0000000..eb51ed5 --- /dev/null +++ b/ws/libs/wsServer/include/sha1.h @@ -0,0 +1,73 @@ +/* + * sha1.h + * + * Description: + * This is the header file for code which implements the Secure + * Hashing Algorithm 1 as defined in FIPS PUB 180-1 published + * April 17, 1995. + * + * Many of the variable names in this code, especially the + * single character names, were used because those were the names + * used in the publication. + * + * Please read the file sha1.c for more information. + * + */ + +#ifndef _SHA1_H_ +#define _SHA1_H_ + +#include <stdint.h> +/* + * If you do not have the ISO standard stdint.h header file, then you + * must typdef the following: + * name meaning + * uint32_t unsigned 32 bit integer + * uint8_t unsigned 8 bit integer (i.e., unsigned char) + * int_least16_t integer of >= 16 bits + * + */ + +#ifndef _SHA_enum_ +#define _SHA_enum_ +enum +{ + shaSuccess = 0, + shaNull, /* Null pointer parameter */ + shaInputTooLong, /* input data too long */ + shaStateError /* called Input after Result */ +}; +#endif +#define SHA1HashSize 20 + +/* + * This structure will hold context information for the SHA-1 + * hashing operation + */ +typedef struct SHA1Context +{ + uint32_t Intermediate_Hash[SHA1HashSize/4]; /* Message Digest */ + + uint32_t Length_Low; /* Message length in bits */ + uint32_t Length_High; /* Message length in bits */ + + /* Index into message block array */ + int_least16_t Message_Block_Index; + uint8_t Message_Block[64]; /* 512-bit message blocks */ + + int Computed; /* Is the digest computed? */ + int Corrupted; /* Is the message digest corrupted? */ +} SHA1Context; + +/* + * Function Prototypes + */ + +int SHA1Reset( SHA1Context *); +int SHA1Input( SHA1Context *, + const uint8_t *, + unsigned int); +int SHA1Result( SHA1Context *, + uint8_t Message_Digest[SHA1HashSize]); + +#endif diff --git a/ws/libs/wsServer/include/utf8.h b/ws/libs/wsServer/include/utf8.h new file mode 100644 index 0000000..b30a62b --- /dev/null +++ b/ws/libs/wsServer/include/utf8.h @@ -0,0 +1,37 @@ +/** + * Copyright (c) 2008-2009 Bjoern Hoehrmann <bjoern@hoehrmann.de> + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +#ifndef UTF8_DECODE_H +#define UTF8_DECODE_H + + #include <inttypes.h> + #include <stddef.h> + + /* UTF8 return state. */ + #define UTF8_ACCEPT 0 + #define UTF8_REJECT 1 + + extern int is_utf8(uint8_t* s); + extern int is_utf8_len(uint8_t *s, size_t len); + extern uint32_t is_utf8_len_state(uint8_t *s, size_t len, uint32_t state); + +#endif diff --git a/ws/libs/wsServer/include/ws.h b/ws/libs/wsServer/include/ws.h new file mode 100644 index 0000000..900d03b --- /dev/null +++ b/ws/libs/wsServer/include/ws.h @@ -0,0 +1,337 @@ +/* + * Copyright (C) 2016-2022 Davidson Francis <davidsondfgl@gmail.com> + * + * This program 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 of the License, or + * (at your option) any later version. + * + * This program 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 this program. If not, see <http://www.gnu.org/licenses/> + */ + +/** + * @dir include/ + * @brief wsServer include directory + * + * @file ws.h + * @brief wsServer constants and functions. + */ +#ifndef WS_H +#define WS_H + +#ifdef __cplusplus +extern "C" { +#endif + + #include <stdbool.h> + #include <stdint.h> + #include <inttypes.h> + + /** + * @name Global configurations + */ + /**@{*/ + /** + * @brief Max clients connected simultaneously. + */ +#ifndef MAX_CLIENTS + #define MAX_CLIENTS 8 +#endif + + /** + * @name Key and message configurations. + */ + /**@{*/ + /** + * @brief Message buffer length. + */ + #define MESSAGE_LENGTH 2048 + /** + * @brief Maximum frame/message length. + */ + #define MAX_FRAME_LENGTH (16*1024*1024) + /** + * @brief WebSocket key length. + */ + #define WS_KEY_LEN 24 + /** + * @brief Magic string length. + */ + #define WS_MS_LEN 36 + /** + * @brief Accept message response length. + */ + #define WS_KEYMS_LEN (WS_KEY_LEN + WS_MS_LEN) + /** + * @brief Magic string. + */ + #define MAGIC_STRING "258EAFA5-E914-47DA-95CA-C5AB0DC85B11" + /**@}*/ + + /** + * @name Handshake constants. + */ + /**@{*/ + /** + * @brief Alias for 'Sec-WebSocket-Key'. + */ + #define WS_HS_REQ "Sec-WebSocket-Key" + + /** + * @brief Handshake accept message length. + */ + #define WS_HS_ACCLEN 130 + + /** + * @brief Handshake accept message. + */ + #define WS_HS_ACCEPT \ + "HTTP/1.1 101 Switching Protocols\r\n" \ + "Upgrade: websocket\r\n" \ + "Connection: Upgrade\r\n" \ + "Sec-WebSocket-Accept: " + /**@}*/ + + /** + * @name Frame types. + */ + /**@{*/ + /** + * @brief Frame FIN. + */ + #define WS_FIN 128 + + /** + * @brief Frame FIN shift + */ + #define WS_FIN_SHIFT 7 + + /** + * @brief Continuation frame. + */ + #define WS_FR_OP_CONT 0 + + /** + * @brief Text frame. + */ + #define WS_FR_OP_TXT 1 + + /** + * @brief Binary frame. + */ + #define WS_FR_OP_BIN 2 + + /** + * @brief Close frame. + */ + #define WS_FR_OP_CLSE 8 + + /** + * @brief Ping frame. + */ + #define WS_FR_OP_PING 0x9 + + /** + * @brief Pong frame. + */ + #define WS_FR_OP_PONG 0xA + + /** + * @brief Unsupported frame. + */ + #define WS_FR_OP_UNSUPPORTED 0xF + /**@}*/ + + /** + * @name Close codes + */ + /**@{*/ + /** + * @brief Normal close + */ + #define WS_CLSE_NORMAL 1000 + /** + * @brief Protocol error + */ + #define WS_CLSE_PROTERR 1002 + /**@}*/ + /** + * @brief Inconsistent message (invalid utf-8) + */ + #define WS_CLSE_INVUTF8 1007 + + /** + * @name Connection states + */ + /**@{*/ + /** + * @brief Connection not established yet. + */ + #define WS_STATE_CONNECTING 0 + /** + * @brief Communicating. + */ + #define WS_STATE_OPEN 1 + /** + * @brief Closing state. + */ + #define WS_STATE_CLOSING 2 + /** + * @brief Closed. + */ + #define WS_STATE_CLOSED 3 + /**@}*/ + + /** + * @name Timeout util + */ + /**@{*/ + /** + * @brief Nanoseconds macro converter + */ + #define MS_TO_NS(x) ((x)*1000000) + /** + * @brief Timeout in milliseconds. + */ + #define TIMEOUT_MS (500) + /**@}*/ + + /** + * @name Handshake constants. + */ + /**@{*/ + /** + * @brief Debug + */ + #ifdef VERBOSE_MODE + #define DEBUG(...) fprintf(stderr, __VA_ARGS__) + #else + #define DEBUG(...) + #endif + /**@}*/ + + #ifndef AFL_FUZZ + #define SEND(client,buf,len) send_all((client), (buf), (len), MSG_NOSIGNAL) + #define RECV(fd,buf,len) recv((fd)->client_sock, (buf), (len), 0) + #else + #define SEND(client,buf,len) write(fileno(stdout), (buf), (len)) + #define RECV(fd,buf,len) read((fd)->client_sock, (buf), (len)) + #endif + + /* Opaque client connection type. */ + typedef uint64_t ws_cli_conn_t; + + /* Opaque server instance type. */ + typedef struct ws_server ws_server_t; + + /** + * @brief Get server context. + * Set when initializing `.context` in `struct ws_server`. + */ + void *ws_get_server_context(ws_cli_conn_t client); + + /** + * @brief Set connection context. + */ + void ws_set_connection_context(ws_cli_conn_t client, void *ptr); + + /** + * @brief Get connection context. + */ + void *ws_get_connection_context(ws_cli_conn_t client); + + /** + * @brief events Web Socket events types. + */ + struct ws_events + { + /** + * @brief On open event, called when a new client connects. + */ + void (*onopen)(ws_cli_conn_t client); + /** + * @brief On close event, called when a client disconnects. + */ + void (*onclose)(ws_cli_conn_t client); + /** + * @brief On message event, called when a client sends a text + * or binary message. + */ + void (*onmessage)(ws_cli_conn_t client, + const unsigned char *msg, uint64_t msg_size, int type); + }; + + /** + * @brief server Web Socket server parameters + */ + struct ws_server + { + /** + * @brief Required hostname that the wsServer will bind to + */ + const char *host; + /** + * @brief Listening port + */ + uint16_t port; + /** + * @brief Whether if the ws_socket() should create a new thread + * and be non-blocking (1) or not (0). + */ + int thread_loop; + /** + * @brief Ping timeout in milliseconds + */ + uint32_t timeout_ms; + /** + * @brief Server events. + */ + struct ws_events evs; + /** + * @brief Server context. + * Provided by the user, can be accessed via `ws_get_server_context` from `onopen`. + */ + void* context; + }; + + /* Forward declarations. */ + + /* Internal usage. */ + extern int get_handshake_accept(char *wsKey, unsigned char **dest); + extern int get_handshake_response(char *hsrequest, char **hsresponse); + + /* External usage. */ + extern char *ws_getaddress(ws_cli_conn_t client); + extern char *ws_getport(ws_cli_conn_t client); + extern int ws_sendframe( + ws_cli_conn_t client, const char *msg, uint64_t size, int type); + extern int ws_sendframe_bcast( + uint16_t port, const char *msg, uint64_t size, int type); + extern int ws_sendframe_txt(ws_cli_conn_t client, const char *msg); + extern int ws_sendframe_txt_bcast(uint16_t port, const char *msg); + extern int ws_sendframe_bin(ws_cli_conn_t client, const char *msg, + uint64_t size); + extern int ws_sendframe_bin_bcast(uint16_t port, const char *msg, + uint64_t size); + extern int ws_get_state(ws_cli_conn_t client); + extern int ws_close_client(ws_cli_conn_t client); + extern int ws_socket(struct ws_server *ws_srv); + + /* Ping routines. */ + extern void ws_ping(ws_cli_conn_t cid, int threshold); + +#ifdef AFL_FUZZ + extern int ws_file(struct ws_events *evs, const char *file); +#endif + +#ifdef __cplusplus +} +#endif + +#endif /* WS_H */ diff --git a/ws/libs/wsServer/src/base64.c b/ws/libs/wsServer/src/base64.c new file mode 100644 index 0000000..4523734 --- /dev/null +++ b/ws/libs/wsServer/src/base64.c @@ -0,0 +1,155 @@ +/* + * Base64 encoding/decoding (RFC1341) + * Copyright (c) 2005-2011, Jouni Malinen <j@w1.fi> + * + * This software may be distributed under the terms of the BSD license. + * See README for more details. + */ + +#include <string.h> +#include <stdlib.h> +#include <stdint.h> +#include <base64.h> + +static const unsigned char base64_table[65] = + "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; + +/** + * base64_encode - Base64 encode + * @src: Data to be encoded + * @len: Length of the data to be encoded + * @out_len: Pointer to output length variable, or %NULL if not used + * Returns: Allocated buffer of out_len bytes of encoded data, + * or %NULL on failure + * + * Caller is responsible for freeing the returned buffer. Returned buffer is + * nul terminated to make it easier to use as a C string. The nul terminator is + * not included in out_len. + */ +unsigned char * base64_encode(const unsigned char *src, size_t len, + size_t *out_len) +{ + unsigned char *out, *pos; + const unsigned char *end, *in; + size_t olen; + int line_len; + + olen = len * 4 / 3 + 4; /* 3-byte blocks to 4-byte */ + olen += olen / 72; /* line feeds */ + olen++; /* nul termination */ + if (olen < len) + return NULL; /* integer overflow */ + out = malloc(olen); + if (out == NULL) + return NULL; + + end = src + len; + in = src; + pos = out; + line_len = 0; + while (end - in >= 3) { + *pos++ = base64_table[in[0] >> 2]; + *pos++ = base64_table[((in[0] & 0x03) << 4) | (in[1] >> 4)]; + *pos++ = base64_table[((in[1] & 0x0f) << 2) | (in[2] >> 6)]; + *pos++ = base64_table[in[2] & 0x3f]; + in += 3; + line_len += 4; + if (line_len >= 72) { + *pos++ = '\n'; + line_len = 0; + } + } + + if (end - in) { + *pos++ = base64_table[in[0] >> 2]; + if (end - in == 1) { + *pos++ = base64_table[(in[0] & 0x03) << 4]; + *pos++ = '='; + } else { + *pos++ = base64_table[((in[0] & 0x03) << 4) | + (in[1] >> 4)]; + *pos++ = base64_table[(in[1] & 0x0f) << 2]; + } + *pos++ = '='; + line_len += 4; + } + + if (line_len) + *pos++ = '\n'; + + *pos = '\0'; + if (out_len) + *out_len = pos - out; + return out; +} + + +/** + * base64_decode - Base64 decode + * @src: Data to be decoded + * @len: Length of the data to be decoded + * @out_len: Pointer to output length variable + * Returns: Allocated buffer of out_len bytes of decoded data, + * or %NULL on failure + * + * Caller is responsible for freeing the returned buffer. + */ +unsigned char * base64_decode(const unsigned char *src, size_t len, + size_t *out_len) +{ + unsigned char dtable[256], *out, *pos, block[4], tmp; + size_t i, count, olen; + int pad = 0; + + memset(dtable, 0x80, 256); + for (i = 0; i < sizeof(base64_table) - 1; i++) + dtable[base64_table[i]] = (unsigned char) i; + dtable['='] = 0; + + count = 0; + for (i = 0; i < len; i++) { + if (dtable[src[i]] != 0x80) + count++; + } + + if (count == 0 || count % 4) + return NULL; + + olen = count / 4 * 3; + pos = out = malloc(olen); + if (out == NULL) + return NULL; + + count = 0; + for (i = 0; i < len; i++) { + tmp = dtable[src[i]]; + if (tmp == 0x80) + continue; + + if (src[i] == '=') + pad++; + block[count] = tmp; + count++; + if (count == 4) { + *pos++ = (block[0] << 2) | (block[1] >> 4); + *pos++ = (block[1] << 4) | (block[2] >> 2); + *pos++ = (block[2] << 6) | block[3]; + count = 0; + if (pad) { + if (pad == 1) + pos--; + else if (pad == 2) + pos -= 2; + else { + /* Invalid padding */ + free(out); + return NULL; + } + break; + } + } + } + + *out_len = pos - out; + return out; +} diff --git a/ws/libs/wsServer/src/handshake.c b/ws/libs/wsServer/src/handshake.c new file mode 100644 index 0000000..4b8ac30 --- /dev/null +++ b/ws/libs/wsServer/src/handshake.c @@ -0,0 +1,144 @@ +/* + * Copyright (C) 2016-2024 Davidson Francis <davidsondfgl@gmail.com> + * + * This program 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 of the License, or + * (at your option) any later version. + * + * This program 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 this program. If not, see <http://www.gnu.org/licenses/> + */ +#define _POSIX_C_SOURCE 200809L +#include <base64.h> +#include <sha1.h> +#include <ws.h> + +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <strings.h> + +/** + * @dir src/ + * @brief Handshake routines directory + * + * @file handshake.c + * @brief Handshake routines. + */ + +/** + * @brief Gets the field Sec-WebSocket-Accept on response, by + * an previously informed key. + * + * @param wsKey Sec-WebSocket-Key + * @param dest source to be stored the value. + * + * @return Returns 0 if success and a negative number + * otherwise. + * + * @attention This is part of the internal API and is documented just + * for completeness. + */ +int get_handshake_accept(char *wsKey, unsigned char **dest) +{ + unsigned char hash[SHA1HashSize]; /* SHA-1 Hash. */ + SHA1Context ctx; /* SHA-1 Context. */ + char *str; /* WebSocket key + magic string. */ + + /* Invalid key. */ + if (!wsKey) + return (-1); + + str = calloc(1, sizeof(char) * (WS_KEY_LEN + WS_MS_LEN + 1)); + if (!str) + return (-1); + + strncpy(str, wsKey, WS_KEY_LEN); + strcat(str, MAGIC_STRING); + + SHA1Reset(&ctx); + SHA1Input(&ctx, (const uint8_t *)str, WS_KEYMS_LEN); + SHA1Result(&ctx, hash); + + *dest = base64_encode(hash, SHA1HashSize, NULL); + *(*dest + strlen((const char *)*dest) - 1) = '\0'; + free(str); + return (0); +} + +/** + * @brief Finds the ocorrence of @p needle in @p haystack, case + * insensitive. + * + * @param haystack Target string to be searched. + * @param needle Substring to search for. + * + * @returns If found, returns a pointer at the beginning of the + * found substring. Otherwise, returns NULL. + */ +static char *strstricase(const char *haystack, const char *needle) +{ + size_t length; + for (length = strlen(needle); *haystack; haystack++) + if (!strncasecmp(haystack, needle, length)) + return (char*)haystack; + return (NULL); +} + +/** + * @brief Gets the complete response to accomplish a succesfully + * handshake. + * + * @param hsrequest Client request. + * @param hsresponse Server response. + * + * @return Returns 0 if success and a negative number + * otherwise. + * + * @attention This is part of the internal API and is documented just + * for completeness. + */ +int get_handshake_response(char *hsrequest, char **hsresponse) +{ + unsigned char *accept; /* Accept message. */ + char *saveptr; /* strtok_r() pointer. */ + char *s; /* Current string. */ + int ret; /* Return value. */ + + saveptr = NULL; + for (s = strtok_r(hsrequest, "\r\n", &saveptr); s != NULL; + s = strtok_r(NULL, "\r\n", &saveptr)) + { + if (strstricase(s, WS_HS_REQ) != NULL) + break; + } + + /* Ensure that we have a valid pointer. */ + if (s == NULL) + return (-1); + + saveptr = NULL; + s = strtok_r(s, " ", &saveptr); + s = strtok_r(NULL, " ", &saveptr); + + ret = get_handshake_accept(s, &accept); + if (ret < 0) + return (ret); + + *hsresponse = malloc(sizeof(char) * WS_HS_ACCLEN); + if (*hsresponse == NULL) + return (-1); + + strcpy(*hsresponse, WS_HS_ACCEPT); + strcat(*hsresponse, (const char *)accept); + strcat(*hsresponse, "\r\n\r\n"); + + free(accept); + return (0); +} diff --git a/ws/libs/wsServer/src/sha1.c b/ws/libs/wsServer/src/sha1.c new file mode 100644 index 0000000..8cdec64 --- /dev/null +++ b/ws/libs/wsServer/src/sha1.c @@ -0,0 +1,389 @@ +/* + * sha1.c + * + * Description: + * This file implements the Secure Hashing Algorithm 1 as + * defined in FIPS PUB 180-1 published April 17, 1995. + * + * The SHA-1, produces a 160-bit message digest for a given + * data stream. It should take about 2**n steps to find a + * message with the same digest as a given message and + * 2**(n/2) to find any two messages with the same digest, + * when n is the digest size in bits. Therefore, this + * algorithm can serve as a means of providing a + * "fingerprint" for a message. + * + * Portability Issues: + * SHA-1 is defined in terms of 32-bit "words". This code + * uses <stdint.h> (included via "sha1.h" to define 32 and 8 + * bit unsigned integer types. If your C compiler does not + * support 32 bit unsigned integers, this code is not + * appropriate. + * + * Caveats: + * SHA-1 is designed to work with messages less than 2^64 bits + * long. Although SHA-1 allows a message digest to be generated + * for messages of any number of bits less than 2^64, this + * implementation only works with messages with a length that is + * a multiple of the size of an 8-bit character. + * + */ + +#include <sha1.h> + +/* + * Define the SHA1 circular left shift macro + */ +#define SHA1CircularShift(bits,word) \ + (((word) << (bits)) | ((word) >> (32-(bits)))) + +/* Local Function Prototyptes */ +void SHA1PadMessage(SHA1Context *); +void SHA1ProcessMessageBlock(SHA1Context *); + +/* + * SHA1Reset + * + * Description: + * This function will initialize the SHA1Context in preparation + * for computing a new SHA1 message digest. + * + * Parameters: + * context: [in/out] + * The context to reset. + * + * Returns: + * sha Error Code. + * + */ +int SHA1Reset(SHA1Context *context) +{ + if (!context) + { + return shaNull; + } + + context->Length_Low = 0; + context->Length_High = 0; + context->Message_Block_Index = 0; + + context->Intermediate_Hash[0] = 0x67452301; + context->Intermediate_Hash[1] = 0xEFCDAB89; + context->Intermediate_Hash[2] = 0x98BADCFE; + context->Intermediate_Hash[3] = 0x10325476; + context->Intermediate_Hash[4] = 0xC3D2E1F0; + + context->Computed = 0; + context->Corrupted = 0; + + return shaSuccess; +} + +/* + * SHA1Result + * + * Description: + * This function will return the 160-bit message digest into the + * Message_Digest array provided by the caller. + * NOTE: The first octet of hash is stored in the 0th element, + * the last octet of hash in the 19th element. + * + * Parameters: + * context: [in/out] + * The context to use to calculate the SHA-1 hash. + * Message_Digest: [out] + * Where the digest is returned. + * + * Returns: + * sha Error Code. + * + */ +int SHA1Result( SHA1Context *context, + uint8_t Message_Digest[SHA1HashSize]) +{ + int i; + + if (!context || !Message_Digest) + { + return shaNull; + } + + if (context->Corrupted) + { + return context->Corrupted; + } + + if (!context->Computed) + { + SHA1PadMessage(context); + for(i=0; i<64; ++i) + { + /* message may be sensitive, clear it out */ + context->Message_Block[i] = 0; + } + context->Length_Low = 0; /* and clear length */ + context->Length_High = 0; + context->Computed = 1; + + } + + for(i = 0; i < SHA1HashSize; ++i) + { + Message_Digest[i] = context->Intermediate_Hash[i>>2] + >> 8 * ( 3 - ( i & 0x03 ) ); + } + + return shaSuccess; +} + +/* + * SHA1Input + * + * Description: + * This function accepts an array of octets as the next portion + * of the message. + * + * Parameters: + * context: [in/out] + * The SHA context to update + * message_array: [in] + * An array of characters representing the next portion of + * the message. + * length: [in] + * The length of the message in message_array + * + * Returns: + * sha Error Code. + * + */ +int SHA1Input( SHA1Context *context, + const uint8_t *message_array, + unsigned length) +{ + if (!length) + { + return shaSuccess; + } + + if (!context || !message_array) + { + return shaNull; + } + + if (context->Computed) + { + context->Corrupted = shaStateError; + + return shaStateError; + } + + if (context->Corrupted) + { + return context->Corrupted; + } + while(length-- && !context->Corrupted) + { + context->Message_Block[context->Message_Block_Index++] = + (*message_array & 0xFF); + + context->Length_Low += 8; + if (context->Length_Low == 0) + { + context->Length_High++; + if (context->Length_High == 0) + { + /* Message is too long */ + context->Corrupted = 1; + } + } + + if (context->Message_Block_Index == 64) + { + SHA1ProcessMessageBlock(context); + } + + message_array++; + } + + return shaSuccess; +} + +/* + * SHA1ProcessMessageBlock + * + * Description: + * This function will process the next 512 bits of the message + * stored in the Message_Block array. + * + * Parameters: + * None. + * + * Returns: + * Nothing. + * + * Comments: + + * Many of the variable names in this code, especially the + * single character names, were used because those were the + * names used in the publication. + * + * + */ +void SHA1ProcessMessageBlock(SHA1Context *context) +{ + const uint32_t K[] = { /* Constants defined in SHA-1 */ + 0x5A827999, + 0x6ED9EBA1, + 0x8F1BBCDC, + 0xCA62C1D6 + }; + int t; /* Loop counter */ + uint32_t temp; /* Temporary word value */ + uint32_t W[80]; /* Word sequence */ + uint32_t A, B, C, D, E; /* Word buffers */ + + /* + * Initialize the first 16 words in the array W + */ + for(t = 0; t < 16; t++) + { + W[t] = (uint32_t)context->Message_Block[t * 4] << 24; + W[t] |= (uint32_t)context->Message_Block[t * 4 + 1] << 16; + W[t] |= (uint32_t)context->Message_Block[t * 4 + 2] << 8; + W[t] |= (uint32_t)context->Message_Block[t * 4 + 3]; + } + + for(t = 16; t < 80; t++) + { + W[t] = SHA1CircularShift(1,W[t-3] ^ W[t-8] ^ W[t-14] ^ W[t-16]); + } + + A = context->Intermediate_Hash[0]; + B = context->Intermediate_Hash[1]; + C = context->Intermediate_Hash[2]; + D = context->Intermediate_Hash[3]; + E = context->Intermediate_Hash[4]; + + for(t = 0; t < 20; t++) + { + temp = SHA1CircularShift(5,A) + + ((B & C) | ((~B) & D)) + E + W[t] + K[0]; + E = D; + D = C; + C = SHA1CircularShift(30,B); + + B = A; + A = temp; + } + + for(t = 20; t < 40; t++) + { + temp = SHA1CircularShift(5,A) + (B ^ C ^ D) + E + W[t] + K[1]; + E = D; + D = C; + C = SHA1CircularShift(30,B); + B = A; + A = temp; + } + + for(t = 40; t < 60; t++) + { + temp = SHA1CircularShift(5,A) + + ((B & C) | (B & D) | (C & D)) + E + W[t] + K[2]; + E = D; + D = C; + C = SHA1CircularShift(30,B); + B = A; + A = temp; + } + + for(t = 60; t < 80; t++) + { + temp = SHA1CircularShift(5,A) + (B ^ C ^ D) + E + W[t] + K[3]; + E = D; + D = C; + C = SHA1CircularShift(30,B); + B = A; + A = temp; + } + + context->Intermediate_Hash[0] += A; + context->Intermediate_Hash[1] += B; + context->Intermediate_Hash[2] += C; + context->Intermediate_Hash[3] += D; + context->Intermediate_Hash[4] += E; + + context->Message_Block_Index = 0; +} + +/* + * SHA1PadMessage + * + + * Description: + * According to the standard, the message must be padded to an even + * 512 bits. The first padding bit must be a '1'. The last 64 + * bits represent the length of the original message. All bits in + * between should be 0. This function will pad the message + * according to those rules by filling the Message_Block array + * accordingly. It will also call the ProcessMessageBlock function + * provided appropriately. When it returns, it can be assumed that + * the message digest has been computed. + * + * Parameters: + * context: [in/out] + * The context to pad + * ProcessMessageBlock: [in] + * The appropriate SHA*ProcessMessageBlock function + * Returns: + * Nothing. + * + */ + +void SHA1PadMessage(SHA1Context *context) +{ + /* + * Check to see if the current message block is too small to hold + * the initial padding bits and length. If so, we will pad the + * block, process it, and then continue padding into a second + * block. + */ + if (context->Message_Block_Index > 55) + { + context->Message_Block[context->Message_Block_Index++] = 0x80; + while(context->Message_Block_Index < 64) + { + context->Message_Block[context->Message_Block_Index++] = 0; + } + + SHA1ProcessMessageBlock(context); + + while(context->Message_Block_Index < 56) + { + context->Message_Block[context->Message_Block_Index++] = 0; + } + } + else + { + context->Message_Block[context->Message_Block_Index++] = 0x80; + while(context->Message_Block_Index < 56) + { + + context->Message_Block[context->Message_Block_Index++] = 0; + } + } + + /* + * Store the message length as the last 8 octets + */ + context->Message_Block[56] = context->Length_High >> 24; + context->Message_Block[57] = context->Length_High >> 16; + context->Message_Block[58] = context->Length_High >> 8; + context->Message_Block[59] = context->Length_High; + context->Message_Block[60] = context->Length_Low >> 24; + context->Message_Block[61] = context->Length_Low >> 16; + context->Message_Block[62] = context->Length_Low >> 8; + context->Message_Block[63] = context->Length_Low; + + SHA1ProcessMessageBlock(context); +} diff --git a/ws/libs/wsServer/src/utf8.c b/ws/libs/wsServer/src/utf8.c new file mode 100644 index 0000000..824060f --- /dev/null +++ b/ws/libs/wsServer/src/utf8.c @@ -0,0 +1,89 @@ +/** + * Copyright (c) 2008-2009 Bjoern Hoehrmann <bjoern@hoehrmann.de> + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +/* + * Amazing utf8 decoder & validator grabbed from: + * http://bjoern.hoehrmann.de/utf-8/decoder/dfa/ + * + * All rights goes to the original author. + */ + +#include "utf8.h" + +static const uint8_t utf8d[] = { + 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, // 00..1f + 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, // 20..3f + 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, // 40..5f + 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, // 60..7f + 1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,9,9,9,9,9,9,9,9,9,9,9,9,9,9,9,9, // 80..9f + 7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7, // a0..bf + 8,8,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2, // c0..df + 0xa,0x3,0x3,0x3,0x3,0x3,0x3,0x3,0x3,0x3,0x3,0x3,0x3,0x4,0x3,0x3, // e0..ef + 0xb,0x6,0x6,0x6,0x5,0x8,0x8,0x8,0x8,0x8,0x8,0x8,0x8,0x8,0x8,0x8, // f0..ff + 0x0,0x1,0x2,0x3,0x5,0x8,0x7,0x1,0x1,0x1,0x4,0x6,0x1,0x1,0x1,0x1, // s0..s0 + 1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,0,1,1,1,1,1,0,1,0,1,1,1,1,1,1, // s1..s2 + 1,2,1,1,1,1,1,2,1,2,1,1,1,1,1,1,1,1,1,1,1,1,1,2,1,1,1,1,1,1,1,1, // s3..s4 + 1,2,1,1,1,1,1,1,1,2,1,1,1,1,1,1,1,1,1,1,1,1,1,3,1,3,1,1,1,1,1,1, // s5..s6 + 1,3,1,1,1,1,1,3,1,3,1,1,1,1,1,1,1,3,1,1,1,1,1,1,1,1,1,1,1,1,1,1, // s7..s8 +}; + +static +uint32_t decode(uint32_t* state, uint32_t* codep, uint32_t byte) { + uint32_t type = utf8d[byte]; + + *codep = (*state != UTF8_ACCEPT) ? + (byte & 0x3fu) | (*codep << 6) : + (0xff >> type) & (byte); + + *state = utf8d[256 + *state*16 + type]; + return *state; +} + +int is_utf8(uint8_t *s) { + uint32_t codepoint, state = 0; + + while (*s) + decode(&state, &codepoint, *s++); + + return state == UTF8_ACCEPT; +} + +int is_utf8_len(uint8_t *s, size_t len) { + uint32_t codepoint, state = 0; + size_t i; + + for (i = 0; i < len; i++) + decode(&state, &codepoint, *s++); + + return state == UTF8_ACCEPT; +} + +uint32_t is_utf8_len_state(uint8_t *s, size_t len, uint32_t state) { + uint32_t codepoint; + size_t i; + + codepoint = 0; + for (i = 0; i < len; i++) + decode(&state, &codepoint, *s++); + + return state; +} diff --git a/ws/libs/wsServer/src/ws.c b/ws/libs/wsServer/src/ws.c new file mode 100644 index 0000000..f0a3de3 --- /dev/null +++ b/ws/libs/wsServer/src/ws.c @@ -0,0 +1,2101 @@ +/* + * Copyright (C) 2016-2023 Davidson Francis <davidsondfgl@gmail.com> + * + * This program 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 of the License, or + * (at your option) any later version. + * + * This program 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 this program. If not, see <http://www.gnu.org/licenses/> + */ +#define _POSIX_C_SOURCE 200809L +#include <errno.h> +#include <fcntl.h> +#include <pthread.h> +#include <stdbool.h> +#include <stddef.h> +#include <stdint.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <time.h> +#include <sys/time.h> + +/* clang-format off */ +#ifndef _WIN32 +#include <arpa/inet.h> +#include <sys/socket.h> +#include <netinet/in.h> +#include <netdb.h> +#else +#include <winsock2.h> +#include <ws2tcpip.h> +#include <windows.h> +typedef int socklen_t; +#endif +/* clang-format on */ + +/* Windows and macOS seems to not have MSG_NOSIGNAL */ +#ifndef MSG_NOSIGNAL +#define MSG_NOSIGNAL 0 +#endif + +#include <unistd.h> + +#include <utf8.h> +#include <ws.h> + +/** + * @dir src/ + * @brief wsServer source code + * + * @file ws.c + * @brief wsServer main routines. + */ + +/** + * @brief Client socks. + */ +struct ws_connection +{ + int client_sock; /**< Client socket FD. */ + int state; /**< WebSocket current state. */ + + /* wsServer structure copy. */ + struct ws_server ws_srv; + + /* Timeout thread and locks. */ + pthread_mutex_t mtx_state; + pthread_cond_t cnd_state_close; + pthread_t thrd_tout; + bool close_thrd; + + /* Send lock. */ + pthread_mutex_t mtx_snd; + + /* IP address and port. */ + char ip[1025]; /* NI_MAXHOST. */ + char port[32]; /* NI_MAXSERV. */ + + /* Ping/Pong IDs and locks. */ + int32_t last_pong_id; + int32_t current_ping_id; + pthread_mutex_t mtx_ping; + + /* Connection context */ + void *connection_context; + + ws_cli_conn_t client_id; +}; + +static struct ws_connection *get_client_by_cid(ws_cli_conn_t cid); + +/** + * @brief Clients list. + */ +static struct ws_connection client_socks[MAX_CLIENTS]; + +/** + * @brief Timeout to a single send(). + */ +static uint32_t timeout; + +/** + * @brief Client validity macro + */ +#define CLIENT_VALID(cli) \ +((cli) != NULL && (cli) >= &client_socks[0] && \ +(cli) <= &client_socks[MAX_CLIENTS - 1] && \ +(cli)->client_sock > -1) + + +/** + * @brief Get server context. + * Assumed to be set once, when initializing `.context` in `struct ws_server`. + */ +void *ws_get_server_context(ws_cli_conn_t cli) +{ + struct ws_connection *client = get_client_by_cid(cli); + if (!CLIENT_VALID(client)) + return NULL; + return client->ws_srv.context; +} + +/** + * @brief Set connection context. + */ +void ws_set_connection_context(ws_cli_conn_t client, void *ptr) +{ + struct ws_connection *cli = get_client_by_cid(client); + if (!CLIENT_VALID(cli)) + return; + cli->connection_context = ptr; +} + +/** + * @brief Get connection context. + */ +void *ws_get_connection_context(ws_cli_conn_t client) +{ + struct ws_connection *cli = get_client_by_cid(client); + if (!CLIENT_VALID(cli)) + return NULL; + return cli->connection_context; +} + +/** + * @brief WebSocket frame data + */ +struct ws_frame_data +{ + /** + * @brief Frame read. + */ + unsigned char frm[MESSAGE_LENGTH]; + /** + * @brief Processed message at the moment. + */ + unsigned char *msg; + /** + * @brief Control frame payload + */ + unsigned char msg_ctrl[125]; + /** + * @brief Current byte position. + */ + size_t cur_pos; + /** + * @brief Amount of read bytes. + */ + size_t amt_read; + /** + * @brief Frame type, like text or binary. + */ + int frame_type; + /** + * @brief Frame size. + */ + uint64_t frame_size; + /** + * @brief Error flag, set when a read was not possible. + */ + int error; + /** + * @brief Client connection structure. + */ + struct ws_connection *client; +}; + +/** + * @brief Global mutex. + */ +static pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER; + +/** + * @brief Issues an error message and aborts the program. + * + * @param s Error message. + */ +#define panic(s) \ +do \ +{ \ +perror(s); \ +exit(-1); \ +} while (0); + +static struct ws_connection *get_client_by_cid(ws_cli_conn_t cid) +{ + pthread_mutex_lock(&mutex); + for (int i = 0; i < MAX_CLIENTS; i++) + { + if (client_socks[i].client_id == cid) + { + pthread_mutex_unlock(&mutex); + return &client_socks[i]; + } + } + pthread_mutex_unlock(&mutex); + return NULL; +} +/** + * @brief Shutdown and close a given socket. + * + * @param fd Socket file descriptor to be closed. + * + * @attention This is part of the internal API and is documented just + * for completeness. + */ +static void close_socket(int fd) +{ +#ifndef _WIN32 + shutdown(fd, SHUT_RDWR); + close(fd); +#else + closesocket(fd); +#endif +} + + +static uint64_t cid_generator = 1; + +static pthread_mutex_t cid_mutex = PTHREAD_MUTEX_INITIALIZER; + +static uint64_t get_next_cid() +{ + uint64_t next_cid; + + pthread_mutex_lock(&cid_mutex); + next_cid = cid_generator++; + pthread_mutex_unlock(&cid_mutex); + + return next_cid; +} + +/** + * @brief Returns the current client state for a given + * client @p client. + * + * @param client Client structure. + * + * @return Returns the client state, -1 otherwise. + * + * @attention This is part of the internal API and is documented just + * for completeness. + */ +static int get_client_state(struct ws_connection *client) +{ + int state; + + if (!CLIENT_VALID(client)) + return (-1); + + pthread_mutex_lock(&client->mtx_state); + state = client->state; + pthread_mutex_unlock(&client->mtx_state); + return (state); +} + +/** + * @brief Set a state @p state to the client index + * @p client. + * + * @param client Client structure. + * @param state State to be set. + * + * @return Returns 0 if success, -1 otherwise. + * + * @attention This is part of the internal API and is documented just + * for completeness. + */ +static int set_client_state(struct ws_connection *client, int state) +{ + if (!CLIENT_VALID(client)) + return (-1); + + if (state < 0 || state > 3) + return (-1); + + pthread_mutex_lock(&client->mtx_state); + client->state = state; + pthread_mutex_unlock(&client->mtx_state); + return (0); +} + +/** + * @brief Send a given message @p buf on a socket @p sockfd. + * + * @param client Target client. + * @param buf Message to be sent. + * @param len Message length. + * @param flags Send flags. + * + * @return If success (i.e: all message was sent), returns + * the amount of bytes sent. Otherwise, -1. + * + * @note Technically this shouldn't be necessary, since send() should + * block until all content is sent, since _we_ don't use 'O_NONBLOCK'. + * However, it was reported (issue #22 on GitHub) that this was + * happening, so just to be cautious, I will keep using this routine. + */ +static ssize_t send_all( + struct ws_connection *client, const void *buf, size_t len, int flags) +{ + const char *p; + ssize_t ret; + ssize_t r; + + ret = 0; + + /* Sanity check. */ + if (!CLIENT_VALID(client)) + return (-1); + + p = buf; + /* clang-format off */ + pthread_mutex_lock(&client->mtx_snd); + while (len) + { + r = send(client->client_sock, p, len, flags); + if (r == -1) + { + pthread_mutex_unlock(&client->mtx_snd); + return (-1); + } + p += r; + len -= r; + ret += r; + } + pthread_mutex_unlock(&client->mtx_snd); + /* clang-format on */ + return (ret); +} + +/** + * @brief Close client connection (no close handshake, this should + * be done earlier), set appropriate state and destroy mutexes. + * + * @param client Client connection. + * @param lock Should lock the global mutex?. + * + * @attention This is part of the internal API and is documented just + * for completeness. + */ +static void close_client(struct ws_connection *client, int lock) +{ + if (!CLIENT_VALID(client)) + return; + + set_client_state(client, WS_STATE_CLOSED); + + close_socket(client->client_sock); + + /* Destroy client mutexes and clear fd 'slot'. */ + /* clang-format off */ + if (lock) + pthread_mutex_lock(&mutex); + client->client_sock = -1; + pthread_cond_destroy(&client->cnd_state_close); + pthread_mutex_destroy(&client->mtx_state); + pthread_mutex_destroy(&client->mtx_snd); + pthread_mutex_destroy(&client->mtx_ping); + if (lock) + pthread_mutex_unlock(&mutex); + /* clang-format on */ +} + +/** + * @brief Close time-out thread. + * + * For a given client, this routine sleeps until + * TIMEOUT_MS and closes the connection or returns + * sooner if already closed connection. + * + * @param p ws_connection/ws_cli_conn_t Structure Pointer. + * + * @return Always NULL. + * + * @attention This is part of the internal API and is documented just + * for completeness. + */ +static void *close_timeout(void *p) +{ + struct ws_connection *conn = p; + struct timespec ts; + int state; + + pthread_mutex_lock(&conn->mtx_state); + + clock_gettime(CLOCK_REALTIME, &ts); + ts.tv_nsec += MS_TO_NS(TIMEOUT_MS); + + /* Normalize the time. */ + while (ts.tv_nsec >= 1000000000) + { + ts.tv_sec++; + ts.tv_nsec -= 1000000000; + } + + while (conn->state != WS_STATE_CLOSED && + pthread_cond_timedwait(&conn->cnd_state_close, &conn->mtx_state, &ts) != + ETIMEDOUT) + ; + + state = conn->state; + pthread_mutex_unlock(&conn->mtx_state); + + /* If already closed. */ + if (state == WS_STATE_CLOSED) + goto quit; + + DEBUG("Timer expired, closing client %d\n", conn->client_sock); + + close_client(conn, 1); + quit: + return (NULL); +} + +/** + * @brief For a valid client index @p client, starts + * the timeout thread and set the current state + * to 'CLOSING'. + * + * @param client Client connection. + * + * @return Returns 0 if success, -1 otherwise. + * + * @attention This is part of the internal API and is documented just + * for completeness. + */ +static int start_close_timeout(struct ws_connection *client) +{ + if (!CLIENT_VALID(client)) + return (-1); + + pthread_mutex_lock(&client->mtx_state); + + if (client->state != WS_STATE_OPEN) + goto out; + + client->state = WS_STATE_CLOSING; + + if (pthread_create(&client->thrd_tout, NULL, close_timeout, client)) + { + pthread_mutex_unlock(&client->mtx_state); + panic("Unable to create timeout thread\n"); + } + client->close_thrd = true; + out: + pthread_mutex_unlock(&client->mtx_state); + return (0); +} + +/** + * @brief Sets the IP address relative to a client connection opened + * by the server and save inside the client structure. + * + * @param client Client connection. + */ +static void set_client_address(struct ws_connection *client) +{ + struct sockaddr_storage addr; + socklen_t hlen = sizeof(addr); + + if (!CLIENT_VALID(client)) + return; + + memset(client->ip, 0, sizeof(client->ip)); + memset(client->port, 0, sizeof(client->port)); + + if (getpeername(client->client_sock, (struct sockaddr *)&addr, &hlen) < 0) + return; + + getnameinfo((struct sockaddr *)&addr, hlen, + client->ip, sizeof(client->ip), + client->port, sizeof(client->port), + NI_NUMERICHOST|NI_NUMERICSERV); +} + +/** + * @brief Gets the IP address relative to a client connection opened + * by the server. + * + * @param client Client connection. + * + * @return Pointer the ip address, or NULL if fails. + * + * @note The returned string is static, no need to free up memory. + */ +char *ws_getaddress(ws_cli_conn_t client) +{ + struct ws_connection *cli = get_client_by_cid(client); + if (!CLIENT_VALID(cli)) + return (NULL); + + return (cli->ip); +} + +/** + * @brief Gets the IP port relative to a client connection opened + * by the server. + * + * @param client Client connection. + * + * @return Pointer the port, or NULL if fails. + * + * @note The returned string is static, no need to free up memory. + */ +char *ws_getport(ws_cli_conn_t client) +{ + struct ws_connection *cli = get_client_by_cid(client); + if (!CLIENT_VALID(cli)) + return (NULL); + + return (cli->port); +} + +/** + * @brief Creates and send an WebSocket frame with some payload data. + * + * This routine is intended to be used to create a websocket frame for + * a given type e sending to the client. For higher level routines, + * please check @ref ws_sendframe_txt and @ref ws_sendframe_bin. + * + * @param client Target to be send. If NULL, broadcast the message. + * @param msg Message to be send. + * @param size Binary message size. + * @param type Frame type. + * @param port Server listen port to broadcast message (if any). + * + * @return Returns the number of bytes written, -1 if error. + * + * @note If @p size is -1, it is assumed that a text frame is being sent, + * otherwise, a binary frame. In the later case, the @p size is used. + * + * @attention This is part of the internal API and is documented just + * for completeness. + */ +static int ws_sendframe_internal(struct ws_connection *client, const char *msg, + uint64_t size, int type, uint16_t port) +{ + unsigned char *response; /* Response data. */ + unsigned char frame[10]; /* Frame. */ + uint8_t idx_first_rData; /* Index data. */ + struct ws_connection *cli; /* Client. */ + int idx_response; /* Index response. */ + ssize_t send_ret; /* Ret send function */ + uint64_t length; /* Message length. */ + ssize_t output; /* Bytes sent. */ + uint64_t i; /* Loop index. */ + + /* + * Check if there is a valid condition before proceeding. + * + * Valid ones: + * client == true && port == 0 + * -> send to single client + * + * client == false && port != 0 + * -> send to all clients within single port + * + * Other options are invalid! + */ + + if (client) + { + if (port != 0) + return (-1); + } + else + { + if (port == 0) + return (-1); + } + + frame[0] = (WS_FIN | type); + length = (uint64_t)size; + + /* Split the size between octets. */ + if (length <= 125) + { + frame[1] = length & 0x7F; + idx_first_rData = 2; + } + + /* Size between 126 and 65535 bytes. */ + else if (length >= 126 && length <= 65535) + { + frame[1] = 126; + frame[2] = (length >> 8) & 255; + frame[3] = length & 255; + idx_first_rData = 4; + } + + /* More than 65535 bytes. */ + else + { + frame[1] = 127; + frame[2] = (unsigned char)((length >> 56) & 255); + frame[3] = (unsigned char)((length >> 48) & 255); + frame[4] = (unsigned char)((length >> 40) & 255); + frame[5] = (unsigned char)((length >> 32) & 255); + frame[6] = (unsigned char)((length >> 24) & 255); + frame[7] = (unsigned char)((length >> 16) & 255); + frame[8] = (unsigned char)((length >> 8) & 255); + frame[9] = (unsigned char)(length & 255); + idx_first_rData = 10; + } + + /* Add frame bytes. */ + idx_response = 0; + response = malloc(sizeof(unsigned char) * (idx_first_rData + length + 1)); + if (!response) + return (-1); + + for (i = 0; i < idx_first_rData; i++) + { + response[i] = frame[i]; + idx_response++; + } + + /* Add data bytes. */ + for (i = 0; i < length; i++) + { + response[idx_response] = msg[i]; + idx_response++; + } + + response[idx_response] = '\0'; + + /* Send to the client if there is one. */ + output = 0; + if (client && port == 0) + { + output = SEND(client, response, idx_response); + goto skip_broadcast; + } + + /* clang-format off */ + pthread_mutex_lock(&mutex); + /* Do broadcast. */ + for (i = 0; i < MAX_CLIENTS; i++) + { + cli = &client_socks[i]; + + if ((cli->client_sock > -1) && + get_client_state(cli) == WS_STATE_OPEN && + (cli->ws_srv.port == port)) + { + if ((send_ret = SEND(cli, response, idx_response)) != -1) + output += send_ret; + else + { + output = -1; + break; + } + } + } + pthread_mutex_unlock(&mutex); + /* clang-format on */ + + skip_broadcast: + free(response); + return ((int)output); +} + +/** + * @brief Send an WebSocket frame with some payload data. + * + * @param client Target to be send. If NULL, broadcast the message. + * @param msg Message to be send. + * @param size Binary message size. + * @param type Frame type. + * + * @return Returns the number of bytes written, -1 if error. + * + * @note If @p size is -1, it is assumed that a text frame is being sent, + * otherwise, a binary frame. In the later case, the @p size is used. + */ +int ws_sendframe(ws_cli_conn_t client, const char *msg, uint64_t size, int type) +{ + struct ws_connection *cli = get_client_by_cid(client); + if (!CLIENT_VALID(cli)) + return (-1); + return ws_sendframe_internal(cli, msg, size, type, 0); +} + +/** + * @brief Send an WebSocket frame with some payload data to all clients + * connected into the same port. + * + * @param port Server listen port to broadcast message. + * @param msg Message to be send. + * @param size Binary message size. + * @param type Frame type. + * + * @return Returns the number of bytes written, -1 if error. + * + * @note If @p size is -1, it is assumed that a text frame is being sent, + * otherwise, a binary frame. In the later case, the @p size is used. + */ +int ws_sendframe_bcast(uint16_t port, const char *msg, uint64_t size, int type) +{ + return ws_sendframe_internal(NULL, msg, size, type, port); +} + +/** + * @brief Given a PONG message, decodes the content + * as a int32_t number that corresponds to our + * PONG id. + * + * @param msg Content to be decoded. + * + * @return Returns the PONG id. + */ +static inline int32_t pong_msg_to_int32(uint8_t *msg) +{ + int32_t pong_id; + /* Decodes as big-endian. */ + pong_id = (msg[3] << 0) | (msg[2] << 8) | (msg[1] << 16) | (msg[0] << 24); + return (pong_id); +} + +/** + * @brief Given a PING id, encodes the content to be sent + * as payload of a PING frame. + * + * @param ping_id PING id to be encoded. + * @param msg Target buffer. + */ +static inline void int32_to_ping_msg(int32_t ping_id, uint8_t *msg) +{ + /* Encodes as big-endian. */ + msg[0] = (ping_id >> 24); + msg[1] = (ping_id >> 16); + msg[2] = (ping_id >> 8); + msg[3] = (ping_id >> 0); +} + +/** + * @brief Send a ping message and close if the client surpasses + * the threshold imposed. + * + * @param cli Client to be sent. + * @param threshold How many pings can miss?. + * @param lock Should lock global mutex or not?. + * + * @attention This is part of the internal API and is documented just + * for completeness. + */ +static void send_ping_close(struct ws_connection *cli, int threshold, int lock) +{ + uint8_t ping_msg[4]; + + if (!CLIENT_VALID(cli) || get_client_state(cli) != WS_STATE_OPEN) + return; + + /* clang-format off */ + pthread_mutex_lock(&cli->mtx_ping); + + cli->current_ping_id++; + int32_to_ping_msg(cli->current_ping_id, ping_msg); + + /* Send PING. */ + ws_sendframe_internal(cli, (const char*)ping_msg, sizeof(ping_msg), + WS_FR_OP_PING, 0); + + /* Check previous PONG: if greater than threshold, abort. */ + if ((cli->current_ping_id - cli->last_pong_id) > threshold) { + DEBUG("Closing, reason: many unanswered PINGs\n"); + close_client(cli, lock); + } + + pthread_mutex_unlock(&cli->mtx_ping); + /* clang-format on */ +} + +/** + * @brief Sends a PING frame to the client @p cli with threshold + * @p threshold. + * + * This routine sends a PING to a single client pointed to by + * @p cli or a broadcast PING if @p cli is NULL. If the specified + * client does not respond up to @p threshold PINGs, the connection + * is aborted. + * + * ws_ping() is not automatic: the user who wants to send keep-alive + * PINGs *must* call this routine in a timely manner, whether on + * a different thread or inside an event. + * + * See examples/ping/ping.c for a minimal example usage. + * + * @param cli Client to be sent, if NULL, broadcast. + * @param threshold How many ignored PINGs should tolerate? (should be + * positive and greater than 0). + * + * @note It should be noted that the time between each call to + * ws_ping() is the timeout itself for receiving the PONG. + * + * It is also important to note that for devices with unstable + * connections (such as a weak WiFi signal or 3/4/5G from a cell phone), + * a threshold greater than 1 is advisable. + */ +void ws_ping(ws_cli_conn_t client, int threshold) +{ + struct ws_connection *cli = get_client_by_cid(client); + int i; + + /* Sanity check. */ + if (threshold <= 0) + return; + + /* PING a single client. */ + if (cli) + send_ping_close(cli, threshold, 1); + + /* PING broadcast. */ + else + { + /* clang-format off */ + pthread_mutex_lock(&mutex); + for (i = 0; i < MAX_CLIENTS; i++) + send_ping_close(&client_socks[i], threshold, 0); + pthread_mutex_unlock(&mutex); + /* clang-format on */ + } +} + +/** + * @brief Sends a WebSocket text frame. + * + * @param client Target to be send. + * @param msg Message to be send, null terminated. + * + * @return Returns the number of bytes written, -1 if error. + */ +int ws_sendframe_txt(ws_cli_conn_t client, const char *msg) +{ + return ws_sendframe(client, msg, (uint64_t)strlen(msg), WS_FR_OP_TXT); +} + +/** + * @brief Sends a broadcast WebSocket text frame. + * + * @param port Server listen port to broadcast message. + * @param msg Message to be send, null terminated. + * + * @return Returns the number of bytes written, -1 if error. + */ +int ws_sendframe_txt_bcast(uint16_t port, const char *msg) +{ + return ws_sendframe_bcast(port, msg, (uint64_t)strlen(msg), WS_FR_OP_TXT); +} + +/** + * @brief Sends a WebSocket binary frame. + * + * @param client Target to be send. + * @param msg Message to be send. + * @param size Binary message size. + * + * @return Returns the number of bytes written, -1 if error. + */ +int ws_sendframe_bin(ws_cli_conn_t client, const char *msg, uint64_t size) +{ + return ws_sendframe(client, msg, size, WS_FR_OP_BIN); +} + +/** + * @brief Sends a broadcast WebSocket binary frame. + * + * @param port Server listen port to broadcast message. + * @param msg Message to be send. + * @param size Binary message size. + * + * @return Returns the number of bytes written, -1 if error. + */ +int ws_sendframe_bin_bcast(uint16_t port, const char *msg, uint64_t size) +{ + return ws_sendframe_bcast(port, msg, size, WS_FR_OP_BIN); +} + +/** + * @brief For a given @p client, gets the current state for + * the connection, or -1 if invalid. + * + * @param client Client connection. + * + * @return Returns the connection state or -1 if + * invalid @p client. + * + * @see WS_STATE_CONNECTING + * @see WS_STATE_OPEN + * @see WS_STATE_CLOSING + * @see WS_STATE_CLOSED + */ +int ws_get_state(ws_cli_conn_t client) +{ + struct ws_connection *cli = get_client_by_cid(client); + if (!CLIENT_VALID(cli)) + return -1; + return (get_client_state(cli)); +} + +/** + * @brief Close the client connection for the given @p + * client with normal close code (1000) and no reason + * string. + * + * @param client Client connection. + * + * @return Returns 0 on success, -1 otherwise. + * + * @note If the client did not send a close frame in + * TIMEOUT_MS milliseconds, the server will close the + * connection with error code (1002). + */ +int ws_close_client(ws_cli_conn_t client) +{ + struct ws_connection *cli = get_client_by_cid(client); + + unsigned char clse_code[2]; + int cc; + + /* Check if client is a valid and connected client. */ + if (!CLIENT_VALID(cli) || cli->client_sock == -1) + return (-1); + + /* + * Instead of using do_close(), we use this to avoid using + * msg_ctrl buffer from wfd and avoid a race condition + * if this is invoked asynchronously. + */ + cc = WS_CLSE_NORMAL; + clse_code[0] = (cc >> 8); + clse_code[1] = (cc & 0xFF); + if (ws_sendframe( + client, (const char *)clse_code, sizeof(char) * 2, WS_FR_OP_CLSE) < 0) + { + DEBUG("An error has occurred while sending closing frame!\n"); + return (-1); + } + + /* + * Starts the timeout thread: if the client did not send + * a close frame in TIMEOUT_MS milliseconds, the server + * will close the connection with error code (1002). + */ + start_close_timeout(cli); + return (0); +} + +/** + * @brief Checks is a given opcode @p frame + * belongs to a control frame or not. + * + * @param frame Frame opcode to be checked. + * + * @return Returns 1 if is a control frame, 0 otherwise. + * + * @attention This is part of the internal API and is documented just + * for completeness. + */ +static inline int is_control_frame(int frame) +{ + return ( + frame == WS_FR_OP_CLSE || frame == WS_FR_OP_PING || frame == WS_FR_OP_PONG); +} + +/** + * @brief Checks is a given opcode @p opcode is valid or not. + * + * @param opcode Frame opcode to be checked. + * + * @return Returns 1 if valid, 0 otherwise. + * + * @attention This is part of the internal API and is documented just + * for completeness. + */ +static inline int is_valid_frame(int opcode) +{ + return ( + opcode == WS_FR_OP_TXT || opcode == WS_FR_OP_BIN || + opcode == WS_FR_OP_CONT || opcode == WS_FR_OP_PING || + opcode == WS_FR_OP_PONG || opcode == WS_FR_OP_CLSE + ); +} + +/** + * @brief Do the handshake process. + * + * @param wfd Websocket Frame Data. + * + * @return Returns 0 if success, a negative number otherwise. + * + * @attention This is part of the internal API and is documented just + * for completeness. + */ +static int do_handshake(struct ws_frame_data *wfd) +{ + char *response; /* Handshake response message. */ + char *p; /* Last request line pointer. */ + ssize_t n; /* Read/Write bytes. */ + + /* Read the very first client message. */ + if ((n = RECV(wfd->client, wfd->frm, sizeof(wfd->frm) - 1)) < 0) + return (-1); + + /* Advance our pointers before the first next_byte(). */ + p = strstr((const char *)wfd->frm, "\r\n\r\n"); + if (p == NULL) + { + DEBUG("An empty line with \\r\\n was expected!\n"); + return (-1); + } + wfd->amt_read = n; + wfd->cur_pos = (size_t)((ptrdiff_t)(p - (char *)wfd->frm)) + 4; + + /* Get response. */ + if (get_handshake_response((char *)wfd->frm, &response) < 0) + { + DEBUG("Cannot get handshake response, request was: %s\n", wfd->frm); + return (-1); + } + + /* Valid request. */ + DEBUG("Handshaked, response: \n" + "------------------------------------\n" + "%s" + "------------------------------------\n", + response); + + /* Send handshake. */ + if (SEND(wfd->client, response, strlen(response)) < 0) + { + free(response); + DEBUG("As error has occurred while handshaking!\n"); + return (-1); + } + + /* Change state. */ + set_client_state(wfd->client, WS_STATE_OPEN); + + /* Trigger events and clean up buffers. */ + wfd->client->ws_srv.evs.onopen(wfd->client->client_id); + free(response); + return (0); +} + +/** + * @brief Sends a close frame, accordingly with the @p close_code + * or the message inside @p wfd. + * + * @param wfd Websocket Frame Data. + * @param close_code Websocket close code. + * + * @return Returns 0 if success, a negative number otherwise. + * + * @attention This is part of the internal API and is documented just + * for completeness. + */ +static int do_close(struct ws_frame_data *wfd, int close_code) +{ + int cc; /* Close code. */ + + /* If custom close-code. */ + if (close_code != -1) + { + cc = close_code; + goto custom_close; + } + + /* If empty or have a close reason, just re-send. */ + if (wfd->frame_size == 0 || wfd->frame_size > 2) + goto send; + + /* Parse close code and check if valid, if not, we issue an protocol error. + */ + if (wfd->frame_size == 1) + cc = wfd->msg_ctrl[0]; + else + cc = ((int)wfd->msg_ctrl[0]) << 8 | wfd->msg_ctrl[1]; + + /* Check if it's not valid, if so, we send a protocol error (1002). */ + if ((cc < 1000 || cc > 1003) && (cc < 1007 || cc > 1011) && + (cc < 3000 || cc > 4999)) + { + cc = WS_CLSE_PROTERR; + + custom_close: + wfd->msg_ctrl[0] = (cc >> 8); + wfd->msg_ctrl[1] = (cc & 0xFF); + + if (ws_sendframe(wfd->client->client_id, (const char *)wfd->msg_ctrl, sizeof(char) * 2, + WS_FR_OP_CLSE) < 0) + { + DEBUG("An error has occurred while sending closing frame!\n"); + return (-1); + } + return (0); + } + + /* Send the data inside wfd->msg_ctrl. */ + send: + if (ws_sendframe(wfd->client->client_id, (const char *)wfd->msg_ctrl, wfd->frame_size, + WS_FR_OP_CLSE) < 0) + { + DEBUG("An error has occurred while sending closing frame!\n"); + return (-1); + } + return (0); +} + +/** + * @brief Send a pong frame in response to a ping frame. + * + * Accordingly to the RFC, a pong frame must have the same + * data payload as the ping frame, so we just send a + * ordinary frame with PONG opcode. + * + * @param wfd Websocket frame data. + * + * @param frame_size Pong frame size. + * + * @return Returns 0 if success and a negative number + * otherwise. + * + * @attention This is part of the internal API and is documented just + * for completeness. + */ +static int do_pong(struct ws_frame_data *wfd, uint64_t frame_size) +{ + if (ws_sendframe( + wfd->client->client_id, (const char *)wfd->msg_ctrl, frame_size, WS_FR_OP_PONG) < 0) + { + wfd->error = 1; + DEBUG("An error has occurred while ponging!\n"); + return (-1); + } + return (0); +} + +/** + * @brief Read a chunk of bytes and return the next byte + * belonging to the frame. + * + * @param wfd Websocket Frame Data. + * + * @return Returns the byte read, or -1 if error. + * + * @attention This is part of the internal API and is documented just + * for completeness. + */ +static inline int next_byte(struct ws_frame_data *wfd) +{ + ssize_t n; + + /* If empty or full. */ + if (wfd->cur_pos == 0 || wfd->cur_pos == wfd->amt_read) + { + if ((n = RECV(wfd->client, wfd->frm, sizeof(wfd->frm))) <= 0) + { + wfd->error = 1; + DEBUG("An error has occurred while trying to read next byte\n"); + return (-1); + } + wfd->amt_read = (size_t)n; + wfd->cur_pos = 0; + } + return (wfd->frm[wfd->cur_pos++]); +} + +/** + * @brief Skips @p frame_size bytes of the current frame. + * + * @param wfd Websocket Frame Data. + * @param frame_size Amount of bytes to be skipped. + * + * @return Returns 0 if success, a negative number + * otherwise. + * + * @attention This is part of the internal API and is documented just + * for completeness. + */ +static int skip_frame(struct ws_frame_data *wfd, uint64_t frame_size) +{ + uint64_t i; + for (i = 0; i < frame_size; i++) + { + if (next_byte(wfd) == -1) + { + wfd->error = 1; + return (-1); + } + } + return (0); +} + +/** + * Frame state data + * + * This structure holds the current data for handling the + * received frames. + */ +struct frame_state_data +{ + unsigned char *msg_data; /* Data frame. */ + unsigned char *msg_ctrl; /* Control frame. */ + uint8_t masks_data[4]; /* Masks data frame array. */ + uint8_t masks_ctrl[4]; /* Masks control frame array. */ + uint64_t msg_idx_data; /* Current msg index. */ + uint64_t msg_idx_ctrl; /* Current msg index. */ + uint64_t frame_length; /* Frame length. */ + uint64_t frame_size; /* Current frame size. */ +#ifdef VALIDATE_UTF8 + uint32_t utf8_state; /* Current UTF-8 state. */ +#endif + int32_t pong_id; /* Current PONG id. */ + uint8_t opcode; /* Frame opcode. */ + uint8_t is_fin; /* Is FIN frame flag. */ + uint8_t mask; /* Mask. */ + int cur_byte; /* Current frame byte. */ +}; + +/** + * @brief Validates TXT frames if UTF8 validation is enabled. + * If the content is not valid, the connection is aborted. + * + * @param wfd WebSocket frame data. + * @param fsd Frame state data. + * + * @return Always 0. + * + * @attention This is part of the internal API and is documented just + * for completeness. + */ +static int validate_utf8_txt(struct ws_frame_data *wfd, + struct frame_state_data *fsd) +{ +#ifdef VALIDATE_UTF8 + /* UTF-8 Validate partial (or not) frame. */ + if (wfd->frame_type != WS_FR_OP_TXT) + return (0); + + if (fsd->is_fin) + { + if (is_utf8_len_state( + fsd->msg_data + (fsd->msg_idx_data - fsd->frame_length), + fsd->frame_length, fsd->utf8_state) != UTF8_ACCEPT) + { + DEBUG("Dropping invalid complete message!\n"); + wfd->error = 1; + do_close(wfd, WS_CLSE_INVUTF8); + } + + return (0); + } + + /* Check current state for a CONT or initial TXT frame. */ + fsd->utf8_state = + is_utf8_len_state(fsd->msg_data + + (fsd->msg_idx_data - fsd->frame_length), + fsd->frame_length, fsd->utf8_state); + + /* We can be in any state, except reject. */ + if (fsd->utf8_state == UTF8_REJECT) + { + DEBUG("Dropping invalid cont/initial frame!\n"); + wfd->error = 1; + do_close(wfd, WS_CLSE_INVUTF8); + } +#endif + return (0); +} + +/** + * @brief Handle PONG frames in response to our PING + * (or not, unsolicited is possible too). + * + * @param wfd WebSocket frame data. + * @param fsd Frame state data. + * + * @return Always 0. + * + * @attention This is part of the internal API and is documented just + * for completeness. + */ +static int handle_pong_frame(struct ws_frame_data *wfd, + struct frame_state_data *fsd) +{ + fsd->is_fin = 0; + + /* If there is no content and/or differs the size, ignore it. */ + if (fsd->frame_size != sizeof(wfd->client->last_pong_id)) + return (0); + + /* + * Our PONG id should be positive and smaller than our + * current PING id. If not, ignore. + */ + /* clang-format off */ + pthread_mutex_lock(&wfd->client->mtx_ping); + fsd->pong_id = pong_msg_to_int32(fsd->msg_ctrl); + if (fsd->pong_id < 0 || fsd->pong_id > wfd->client->current_ping_id) + { + pthread_mutex_unlock(&wfd->client->mtx_ping); + return (0); + } + wfd->client->last_pong_id = fsd->pong_id; + pthread_mutex_unlock(&wfd->client->mtx_ping); + /* clang-format on */ + + return (0); +} + +/** + * @brief Handle PING frames sending a PONG response. + * + * @param wfd WebSocket frame data. + * @param fsd Frame state data. + * + * @return Returns 0 if success, -1 otherwise. + * + * @attention This is part of the internal API and is documented just + * for completeness. + */ +static int handle_ping_frame(struct ws_frame_data *wfd, + struct frame_state_data *fsd) +{ + if (do_pong(wfd, fsd->frame_size) < 0) + return (-1); + + /* Quick hack to keep our loop. */ + fsd->is_fin = 0; + return (0); +} + +/** + * @brief Handle close frames while checking for UTF8 + * in the close reason. + * + * @param wfd WebSocket frame data. + * @param fsd Frame state data. + * + * @return Returns 0 if success, -1 otherwise. + * + * @attention This is part of the internal API and is documented just + * for completeness. + */ +static int handle_close_frame(struct ws_frame_data *wfd, + struct frame_state_data *fsd) +{ +#ifdef VALIDATE_UTF8 + /* If there is a close reason, check if it is UTF-8 valid. */ + if (fsd->frame_size > 2 && + !is_utf8_len(fsd->msg_ctrl + 2, fsd->frame_size - 2)) + { + DEBUG("Invalid close frame payload reason! (not UTF-8)\n"); + wfd->error = 1; + return (-1); + } +#endif + + /* + * Since we're aborting, we can scratch the 'data'-related + * vars here. + */ + wfd->frame_size = fsd->frame_size; + wfd->frame_type = WS_FR_OP_CLSE; + free(fsd->msg_data); + return (0); +} + +/** + * @brief Reads the current frame isolating data from control frames. + * The parameters are changed in order to reflect the current state. + * + * @param wfd Websocket Frame Data. + * @param fsd Frame state data. + * + * @return Returns 0 if success, a negative number otherwise. + * + * @attention This is part of the internal API and is documented just + * for completeness. + */ +static int read_single_frame(struct ws_frame_data *wfd, + struct frame_state_data *fsd) +{ + uint64_t *frame_size; /* Curr frame size. */ + unsigned char *tmp; /* Tmp message. */ + unsigned char *msg; /* Current message. */ + uint64_t *msg_idx; /* Message index. */ + uint8_t *masks; /* Current mask. */ + int cur_byte; /* Curr byte read. */ + uint64_t i; /* Loop index. */ + + /* Decide which mask and msg to use. */ + if (is_control_frame(fsd->opcode)) { + frame_size = &fsd->frame_size; + msg_idx = &fsd->msg_idx_ctrl; + masks = fsd->masks_ctrl; + msg = fsd->msg_ctrl; + } + else { + frame_size = &wfd->frame_size; + msg_idx = &fsd->msg_idx_data; + masks = fsd->masks_data; + msg = fsd->msg_data; + } + + /* Decode masks and length for 16-bit messages. */ + if (fsd->frame_length == 126) + fsd->frame_length = (((uint64_t)next_byte(wfd)) << 8) | next_byte(wfd); + + /* 64-bit messages. */ + else if (fsd->frame_length == 127) + { + fsd->frame_length = + (((uint64_t)next_byte(wfd)) << 56) | /* frame[2]. */ + (((uint64_t)next_byte(wfd)) << 48) | /* frame[3]. */ + (((uint64_t)next_byte(wfd)) << 40) | + (((uint64_t)next_byte(wfd)) << 32) | + (((uint64_t)next_byte(wfd)) << 24) | + (((uint64_t)next_byte(wfd)) << 16) | + (((uint64_t)next_byte(wfd)) << 8) | + (((uint64_t)next_byte(wfd))); /* frame[9]. */ + } + + *frame_size += fsd->frame_length; + + /* + * Check frame size + * + * We need to limit the amount supported here, since if + * we follow strictly to the RFC, we have to allow 2^64 + * bytes. Also keep in mind that this is still true + * for continuation frames. + */ + if (*frame_size > MAX_FRAME_LENGTH) + { + DEBUG("Current frame from client %d, exceeds the maximum\n" + "amount of bytes allowed (%" PRId64 "/%d)!", + wfd->client->client_sock, *frame_size + fsd->frame_length, + MAX_FRAME_LENGTH); + + wfd->error = 1; + return (-1); + } + + /* Read masks. */ + masks[0] = next_byte(wfd); + masks[1] = next_byte(wfd); + masks[2] = next_byte(wfd); + masks[3] = next_byte(wfd); + + /* + * Abort if error. + * + * This is tricky: we may have multiples error codes from the + * previous next_bytes() calls, but, since we're only setting + * variables and flags, there is no major issue in setting + * them wrong _if_ we do not use their values, thing that + * we do here. + */ + if (wfd->error) + return (-1); + + /* + * Allocate memory. + * + * The statement below will allocate a new chunk of memory + * if msg is NULL with size total_length. Otherwise, it will + * resize the total memory accordingly with the message index + * and if the current frame is a FIN frame or not, if so, + * increment the size by 1 to accommodate the line ending \0. + */ + if (fsd->frame_length > 0) + { + if (!is_control_frame(fsd->opcode)) + { + tmp = realloc(msg, *msg_idx + fsd->frame_length + fsd->is_fin); + if (!tmp) + { + DEBUG("Cannot allocate memory, requested: % " PRId64 "\n", + (*msg_idx + fsd->frame_length + fsd->is_fin)); + + wfd->error = 1; + return (-1); + } + msg = tmp; + fsd->msg_data = msg; + } + + /* Copy to the proper location. */ + for (i = 0; i < fsd->frame_length; i++, (*msg_idx)++) + { + /* We were able to read? .*/ + cur_byte = next_byte(wfd); + if (cur_byte == -1) + return (-1); + + msg[*msg_idx] = cur_byte ^ masks[i % 4]; + } + } + + /* If we're inside a FIN frame, lets... */ + if (fsd->is_fin && *frame_size > 0) + { + /* Increase memory if our FIN frame is of length 0. */ + if (!fsd->frame_length && !is_control_frame(fsd->opcode)) + { + tmp = realloc(msg, *msg_idx + 1); + if (!tmp) + { + DEBUG("Cannot allocate memory, requested: %" PRId64 "\n", + (*msg_idx + 1)); + + wfd->error = 1; + return (-1); + } + msg = tmp; + fsd->msg_data = msg; + } + msg[*msg_idx] = '\0'; + } + + return (0); +} + +/** + * @brief Reads the next frame, whether if a TXT/BIN/CLOSE + * of arbitrary size. + * + * @param wfd Websocket Frame Data. + * + * @return Returns 0 if success, a negative number otherwise. + * + * @attention This is part of the internal API and is documented just + * for completeness. + */ +static int next_complete_frame(struct ws_frame_data *wfd) +{ + struct frame_state_data fsd = {0}; + fsd.msg_data = NULL; + fsd.msg_ctrl = wfd->msg_ctrl; + +#ifdef VALIDATE_UTF8 + fsd.utf8_state = UTF8_ACCEPT; +#endif + + wfd->frame_size = 0; + wfd->frame_type = -1; + wfd->msg = NULL; + + /* Read until find a FIN or a unsupported frame. */ + do + { + fsd.cur_byte = next_byte(wfd); + if (fsd.cur_byte == -1) + return (-1); + + fsd.is_fin = (fsd.cur_byte & 0xFF) >> WS_FIN_SHIFT; + fsd.opcode = (fsd.cur_byte & 0xF); + + /* Check for RSV field. */ + if (fsd.cur_byte & 0x70) + { + DEBUG("RSV is set while wsServer do not negotiate extensions!\n"); + wfd->error = 1; + break; + } + + /* + * Check if the current opcode makes sense: + * a) If we're inside a cont frame but no previous data frame + * + * b) If we're handling a data-frame and receive another data + * frame. (it's expected to receive only CONT or control + * frames). + * + * It is worth to note that in a), we do not need to check + * if the previous frame was FIN or not: if was FIN, an + * on_message event was triggered and this function returned; + * so the only possibility here is a previous non-FIN data + * frame, ;-). + */ + if ((wfd->frame_type == -1 && fsd.opcode == WS_FR_OP_CONT) || + (wfd->frame_type != -1 && !is_control_frame(fsd.opcode) && + fsd.opcode != WS_FR_OP_CONT)) + { + DEBUG("Unexpected frame was received!, opcode: %d, previous: %d\n", + fsd.opcode, wfd->frame_type); + wfd->error = 1; + break; + } + + /* Check if one of the valid opcodes. */ + if (!is_valid_frame(fsd.opcode)) + { + DEBUG("Unsupported frame opcode: %d\n", fsd.opcode); + /* We should consider as error receive an unknown frame. */ + wfd->frame_type = fsd.opcode; + wfd->error = 1; + break; + } + + /* Check our current state: if CLOSING, we only accept close frames. */ + if (get_client_state(wfd->client) == WS_STATE_CLOSING && + fsd.opcode != WS_FR_OP_CLSE) + { + DEBUG("Unexpected frame received, expected CLOSE (%d), " + "received: (%d)", + WS_FR_OP_CLSE, fsd.opcode); + wfd->error = 1; + break; + } + + /* Only change frame type if not a CONT frame. */ + if (fsd.opcode != WS_FR_OP_CONT && !is_control_frame(fsd.opcode)) + wfd->frame_type = fsd.opcode; + + fsd.mask = next_byte(wfd); + fsd.frame_length = fsd.mask & 0x7F; + fsd.frame_size = 0; + fsd.msg_idx_ctrl = 0; + + /* + * We should deny non-FIN control frames or that have + * more than 125 octets. + */ + if (is_control_frame(fsd.opcode) && + (!fsd.is_fin || fsd.frame_length > 125)) + { + DEBUG("Control frame bigger than 125 octets or not a FIN " + "frame!\n"); + wfd->error = 1; + break; + } + + /* Read a single frame, and then handle accordingly. */ + if (read_single_frame(wfd, &fsd) < 0) + break; + + /* Handle each frame + * Obs: If BIN, nothing should be done unless we got + * a FIN-frame. + */ + switch (fsd.opcode) { + /* UTF-8 Validate partial (or not) frame. */ + case WS_FR_OP_CONT: + case WS_FR_OP_TXT: { + validate_utf8_txt(wfd, &fsd); + break; + } + /* + * We _may_ send a PING frame if the ws_ping() routine was invoked. + * + * If the content is invalid and/or differs the size, ignore it. + * (maybe unsolicited PONG). + */ + case WS_FR_OP_PONG: { + handle_pong_frame(wfd, &fsd); + goto next_it; + break; + } + /* We should answer to a PING frame as soon as possible. */ + case WS_FR_OP_PING: { + if (handle_ping_frame(wfd, &fsd) < 0) + goto done; + break; + } + /* We interrupt the loop as soon as we find a CLOSE frame. */ + case WS_FR_OP_CLSE: { + if (handle_close_frame(wfd, &fsd) < 0) + goto done; + return (0); + break; + } + } + + next_it:; + + } while (!fsd.is_fin && !wfd->error); + + done: + /* Check for error. */ + if (wfd->error) + { + free(fsd.msg_data); + wfd->msg = NULL; + return (-1); + } + + wfd->msg = fsd.msg_data; + return (0); +} + +/** + * @brief Establishes to connection with the client and trigger + * events when occurs one. + * + * @param vclient Client connection. + * + * @return Returns @p vclient. + * + * @note This will be run on a different thread. + * + * @attention This is part of the internal API and is documented just + * for completeness. + */ +static void *ws_establishconnection(void *vclient) +{ + struct ws_frame_data wfd; /* WebSocket frame data. */ + struct ws_connection *client; /* Client structure. */ + int clse_thrd; /* Time-out close thread. */ + + client = vclient; + + /* Prepare frame data. */ + memset(&wfd, 0, sizeof(wfd)); + wfd.client = client; + + /* Do handshake. */ + if (do_handshake(&wfd) < 0) + goto closed; + + /* Read next frame until client disconnects or an error occur. */ + while (next_complete_frame(&wfd) >= 0) + { + /* Text/binary event. */ + if ((wfd.frame_type == WS_FR_OP_TXT || + wfd.frame_type == WS_FR_OP_BIN) && !wfd.error) + { + client->ws_srv.evs.onmessage(client->client_id, wfd.msg, wfd.frame_size, + wfd.frame_type); + } + + /* Close event. */ + else if (wfd.frame_type == WS_FR_OP_CLSE && !wfd.error) + { + /* + * We only send a CLOSE frame once, if we're already + * in CLOSING state, there is no need to send. + */ + if (get_client_state(client) != WS_STATE_CLOSING) + { + set_client_state(client, WS_STATE_CLOSING); + + /* We only send a close frameSend close frame */ + do_close(&wfd, -1); + } + + free(wfd.msg); + break; + } + + free(wfd.msg); + } + + /* + * on_close events always occur, whether for client closure + * or server closure, as the server is expected to + * always know when the client disconnects. + */ + client->ws_srv.evs.onclose(client->client_id); + + closed: + clse_thrd = client->close_thrd; + + /* Wait for timeout thread if necessary. */ + if (clse_thrd) + { + pthread_cond_signal(&client->cnd_state_close); + pthread_join(client->thrd_tout, NULL); + } + + /* Close connectin properly. */ + if (get_client_state(client) != WS_STATE_CLOSED) { + DEBUG("Closing: normal close\n"); + close_client(client, 1); + } + + return (vclient); +} + +/** + * Accept parameters. + */ +struct ws_accept_params +{ + int sock; + struct ws_server ws_srv; +}; + +/** + * @brief Main loop that keeps accepting new connections. + * + * @param data Server socket. + * + * @return Returns @p data. + * + * @note This may be run on a different thread. + * + * @attention This is part of the internal API and is documented just + * for completeness. + */ +static void *ws_accept(void *data) +{ + struct ws_accept_params *ws_prm; /* wsServer parameters. */ + struct sockaddr_storage sa; /* Client. */ + pthread_t client_thread; /* Client thread. */ + struct timeval time; /* Client socket timeout. */ + socklen_t salen; /* Length of sockaddr. */ + int new_sock; /* New opened connection. */ + int sock; /* Server sock. */ + int i; /* Loop index. */ + + ws_prm = data; + sock = ws_prm->sock; + salen = sizeof(sa); + + while (1) + { + /* Accept. */ + new_sock = accept(sock, (struct sockaddr *)&sa, &salen); + if (new_sock < 0) + panic("Error on accepting connections.."); + + if (timeout) + { + time.tv_sec = timeout / 1000; + time.tv_usec = (timeout % 1000) * 1000; + + /* + * Socket timeout + * This feature seems to be supported on Linux, Windows, + * macOS and FreeBSD. + * + * See: + * https://linux.die.net/man/3/setsockopt + */ + setsockopt(new_sock, SOL_SOCKET, SO_SNDTIMEO, (const char*)&time, + sizeof(struct timeval)); + } + + /* Adds client socket to socks list. */ + pthread_mutex_lock(&mutex); + for (i = 0; i < MAX_CLIENTS; i++) + { + if (client_socks[i].client_sock == -1) + { + memcpy(&client_socks[i].ws_srv, &ws_prm->ws_srv, + sizeof(struct ws_server)); + + client_socks[i].client_sock = new_sock; + client_socks[i].state = WS_STATE_CONNECTING; + client_socks[i].close_thrd = false; + client_socks[i].last_pong_id = -1; + client_socks[i].current_ping_id = -1; + client_socks[i].client_id = get_next_cid(); + set_client_address(&client_socks[i]); + + if (pthread_mutex_init(&client_socks[i].mtx_state, NULL)) + panic("Error on allocating close mutex"); + if (pthread_cond_init(&client_socks[i].cnd_state_close, NULL)) + panic("Error on allocating condition var\n"); + if (pthread_mutex_init(&client_socks[i].mtx_snd, NULL)) + panic("Error on allocating send mutex"); + if (pthread_mutex_init(&client_socks[i].mtx_ping, NULL)) + panic("Error on allocating ping/pong mutex"); + break; + } + } + pthread_mutex_unlock(&mutex); + + /* Client socket added to socks list ? */ + if (i != MAX_CLIENTS) + { + if (pthread_create( + &client_thread, NULL, ws_establishconnection, &client_socks[i])) + panic("Could not create the client thread!"); + + pthread_detach(client_thread); + } + else + close_socket(new_sock); + } + + free(data); + return (data); +} + +/** + * @brief By using the server parameters provided in @p ws_srv, + * create a socket and bind it accordingly with the server + * configurations. + * + * @param ws_srv Web Socket configurations. + * + * @return Returns the socket file descriptor. + */ +static int do_bind_socket(struct ws_server *ws_srv) +{ + struct addrinfo hints, *results, *try; + char port[8] = {0}; + int reuse; + int sock; + + reuse = 1; + + /* Prepare the getaddrinfo structure. */ + memset(&hints, 0, sizeof(struct addrinfo)); + hints.ai_flags = AI_PASSIVE; + hints.ai_family = AF_UNSPEC; + hints.ai_socktype = SOCK_STREAM; + + /* Port. */ + snprintf(port, sizeof port - 1, "%d", ws_srv->port); + + if (getaddrinfo(ws_srv->host, port, &hints, &results) != 0) + panic("getaddrinfo() failed"); + + /* Try to create a socket with one of the returned addresses. */ + for (try = results; try != NULL; try = try->ai_next) + { + /* try to make a socket with this setup */ + if ((sock = socket(try->ai_family, try->ai_socktype, + try->ai_protocol)) < 0) + { + continue; + } + + /* Reuse previous address. */ + if (setsockopt(sock, SOL_SOCKET, SO_REUSEADDR, (const char *)&reuse, + sizeof(reuse)) < 0) + { + panic("setsockopt(SO_REUSEADDR) failed"); + } + + /* Bind. */ + if (bind(sock, try->ai_addr, try->ai_addrlen) < 0) + panic("Bind failed"); + + /* if it worked, we're done. */ + break; + } + + freeaddrinfo(results); + + /* Check if binded with success. */ + if (try == NULL) + panic("couldn't find a port to bind to"); + + return (sock); +} + +/** + * @brief Main loop for the server. + * + * @param ws_srv Web Socket server parameters. + * + * @return If @p thread_loop != 0, returns 0. Otherwise, never + * returns. + */ +int ws_socket(struct ws_server *ws_srv) +{ + struct ws_accept_params *ws_prm; /* Accept parameters. */ + pthread_t accept_thread; /* Accept thread. */ + int sock; /* Client sock. */ + + timeout = ws_srv->timeout_ms; + + /* Ignore 'unused functions' warnings. */ + ((void)skip_frame); + + /* Allocates our parameters data and copy the ws_server structure. */ + ws_prm = malloc(sizeof(*ws_prm)); + if (!ws_prm) + panic("Unable to allocate ws parameters, out of memory!\n"); + + memcpy(&ws_prm->ws_srv, ws_srv, sizeof(*ws_srv)); + +#ifdef _WIN32 + WSADATA wsaData; + if (WSAStartup(MAKEWORD(2, 2), &wsaData) != 0) + panic("WSAStartup failed!"); + + /** + * Sets stdout to be non-buffered. + * + * According to the docs from MSDN (setvbuf page), Windows do not + * really supports line buffering but full-buffering instead. + * + * Quote from the docs: + * "... _IOLBF For some systems, this provides line buffering. + * However, for Win32, the behavior is the same as _IOFBF" + */ + setvbuf(stdout, NULL, _IONBF, 0); +#endif + + /* Create socket and bind. */ + sock = do_bind_socket(ws_srv); + + /* Listen. */ + if (listen(sock, MAX_CLIENTS) < 0) + panic("Unable to listen!\n"); + + /* Wait for incoming connections. */ + printf("Waiting for incoming connections...\n"); + memset(client_socks, -1, sizeof(client_socks)); + + /* Accept connections. */ + ws_prm->sock = sock; + + if (!ws_srv->thread_loop) + ws_accept(ws_prm); + else + { + if (pthread_create(&accept_thread, NULL, ws_accept, (void *)ws_prm)) + panic("Could not create the client thread!"); + pthread_detach(accept_thread); + } + + return (0); +} + +#ifdef AFL_FUZZ +/** + * @brief WebSocket fuzzy test routine + * + * @param evs Events structure. + * + * @param file File to be read. + * + * @return Returns 0, or crash. + * + * @note This is a 'fuzzy version' of the function @ref ws_socket. + * This routine do not listen to any port nor accept multiples + * connections. It is intended to read a stream of frames through a + * file and process it as if they are coming from a socket. + * + * This behavior enables us to test wsServer against fuzzers, like + * AFL, and see if it crashes, hangs or behaves normally, even under + * weird conditions. + */ +int ws_file(struct ws_events *evs, const char *file) +{ + int sock; + sock = open(file, O_RDONLY); + if (sock < 0) + panic("Invalid file\n"); + + /* Copy events. */ + memcpy(&cli_events, evs, sizeof(struct ws_events)); + + /* Clear client socks list. */ + memset(client_socks, -1, sizeof(client_socks)); + + /* Set client settings. */ + client_socks[0].client_sock = sock; + client_socks[0].state = WS_STATE_CONNECTING; + client_socks[0].close_thrd = false; + + /* Initialize mutexes. */ + if (pthread_mutex_init(&client_socks[0].mtx_state, NULL)) + panic("Error on allocating close mutex"); + if (pthread_cond_init(&client_socks[0].cnd_state_close, NULL)) + panic("Error on allocating condition var\n"); + if (pthread_mutex_init(&client_socks[0].mtx_snd, NULL)) + panic("Error on allocating send mutex"); + if (pthread_mutex_init(&client_socks[0].mtx_ping, NULL)) + panic("Error on allocating ping/pong mutex"); + + ws_establishconnection(&client_socks[0]); + return (0); +} +#endif |
