mirror of
https://github.com/Wessel/c-websocket-server.git
synced 2026-07-04 13:41:44 +02:00
1760 lines
44 KiB
C
1760 lines
44 KiB
C
/*
|
|
* 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/>
|
|
*/
|
|
#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>
|
|
#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 Websocket events.
|
|
*/
|
|
static struct ws_events cli_events;
|
|
|
|
/**
|
|
* @brief Client socks.
|
|
*/
|
|
struct ws_connection
|
|
{
|
|
int client_sock; /**< Client socket FD. */
|
|
int state; /**< WebSocket current state. */
|
|
|
|
/* 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. */
|
|
char ip[INET6_ADDRSTRLEN];
|
|
|
|
/* Ping/Pong IDs and locks. */
|
|
int32_t last_pong_id;
|
|
int32_t current_ping_id;
|
|
pthread_mutex_t mtx_ping;
|
|
};
|
|
|
|
/**
|
|
* @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 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.
|
|
*/
|
|
ws_cli_conn_t *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);
|
|
|
|
/**
|
|
* @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
|
|
}
|
|
|
|
/**
|
|
* @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(ws_cli_conn_t *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(ws_cli_conn_t *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 Returns 0 if success (i.e: all message was sent),
|
|
* -1 otherwise.
|
|
*
|
|
* @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(
|
|
ws_cli_conn_t *client, const void *buf, size_t len, int flags)
|
|
{
|
|
const char *p;
|
|
ssize_t ret;
|
|
|
|
/* Sanity check. */
|
|
if (!CLIENT_VALID(client))
|
|
return (-1);
|
|
|
|
p = buf;
|
|
/* clang-format off */
|
|
pthread_mutex_lock(&client->mtx_snd);
|
|
while (len)
|
|
{
|
|
ret = send(client->client_sock, p, len, flags);
|
|
if (ret == -1)
|
|
{
|
|
pthread_mutex_unlock(&client->mtx_snd);
|
|
return (-1);
|
|
}
|
|
p += ret;
|
|
len -= ret;
|
|
}
|
|
pthread_mutex_unlock(&client->mtx_snd);
|
|
/* clang-format on */
|
|
return (0);
|
|
}
|
|
|
|
/**
|
|
* @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(ws_cli_conn_t *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)
|
|
{
|
|
ws_cli_conn_t *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(ws_cli_conn_t *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(ws_cli_conn_t *client)
|
|
{
|
|
struct sockaddr_in addr;
|
|
socklen_t addr_size;
|
|
|
|
if (!CLIENT_VALID(client))
|
|
return;
|
|
|
|
addr_size = sizeof(struct sockaddr_in);
|
|
|
|
if (getpeername(client->client_sock, (struct sockaddr *)&addr, &addr_size) < 0)
|
|
return;
|
|
|
|
memset(client->ip, 0, sizeof(client->ip));
|
|
inet_ntop(AF_INET, &addr.sin_addr, client->ip, INET_ADDRSTRLEN);
|
|
}
|
|
|
|
/**
|
|
* @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)
|
|
{
|
|
if (!CLIENT_VALID(client))
|
|
return (NULL);
|
|
|
|
return (client->ip);
|
|
}
|
|
|
|
/**
|
|
* @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.
|
|
*
|
|
* @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)
|
|
{
|
|
unsigned char *response; /* Response data. */
|
|
unsigned char frame[10]; /* Frame. */
|
|
uint8_t idx_first_rData; /* Index data. */
|
|
uint64_t length; /* Message length. */
|
|
int idx_response; /* Index response. */
|
|
ssize_t output; /* Bytes sent. */
|
|
ssize_t send_ret; /* Ret send function */
|
|
uint64_t i; /* Loop index. */
|
|
ws_cli_conn_t *cli; /* Client. */
|
|
|
|
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)
|
|
output = SEND(client, response, idx_response);
|
|
|
|
/* If no client specified, broadcast to everyone. */
|
|
if (!client)
|
|
{
|
|
pthread_mutex_lock(&mutex);
|
|
|
|
for (i = 0; i < MAX_CLIENTS; i++)
|
|
{
|
|
cli = &client_socks[i];
|
|
if ((cli->client_sock > -1) && get_client_state(cli) == WS_STATE_OPEN)
|
|
{
|
|
if ((send_ret = SEND(cli, response, idx_response)) != -1)
|
|
output += send_ret;
|
|
else
|
|
{
|
|
output = -1;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
pthread_mutex_unlock(&mutex);
|
|
}
|
|
|
|
free(response);
|
|
return ((int)output);
|
|
}
|
|
|
|
/**
|
|
* @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(ws_cli_conn_t *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(cli, (const char*)ping_msg, sizeof(ping_msg), WS_FR_OP_PING);
|
|
|
|
/* Check previous PONG: if greater than threshold, abort. */
|
|
if ((cli->current_ping_id - cli->last_pong_id) > threshold)
|
|
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 *cli, int threshold)
|
|
{
|
|
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. If NULL, broadcast the message.
|
|
* @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 WebSocket binary frame.
|
|
*
|
|
* @param client Target to be send. If NULL, broadcast the 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(ws_cli_conn_t *client, const char *msg, uint64_t size)
|
|
{
|
|
return ws_sendframe(client, 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)
|
|
{
|
|
return (get_client_state(client));
|
|
}
|
|
|
|
/**
|
|
* @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)
|
|
{
|
|
unsigned char clse_code[2];
|
|
int cc;
|
|
|
|
/* Check if client is a valid and connected client. */
|
|
if (!CLIENT_VALID(client) || client->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(client);
|
|
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 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. */
|
|
cli_events.onopen(wfd->client);
|
|
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, (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, (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, (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);
|
|
}
|
|
|
|
/**
|
|
* @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 opcode Frame opcode.
|
|
* @param buf Buffer to be written.
|
|
* @param frame_length Length of the current frame.
|
|
* @param frame_size Total size of the frame (considering CONT frames)
|
|
* read until the moment.
|
|
* @param msg_idx Message index, reflects the current buffer pointer state.
|
|
* @param masks Masks vector.
|
|
* @param is_fin Is FIN frame indicator.
|
|
*
|
|
* @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_frame(struct ws_frame_data *wfd,
|
|
int opcode,
|
|
unsigned char **buf,
|
|
uint64_t *frame_length,
|
|
uint64_t *frame_size,
|
|
uint64_t *msg_idx,
|
|
uint8_t *masks,
|
|
int is_fin)
|
|
{
|
|
unsigned char *tmp; /* Tmp message. */
|
|
unsigned char *msg; /* Current message. */
|
|
int cur_byte; /* Curr byte read. */
|
|
uint64_t i; /* Loop index. */
|
|
|
|
msg = *buf;
|
|
|
|
/* Decode masks and length for 16-bit messages. */
|
|
if (*frame_length == 126)
|
|
*frame_length = (((uint64_t)next_byte(wfd)) << 8) | next_byte(wfd);
|
|
|
|
/* 64-bit messages. */
|
|
else if (*frame_length == 127)
|
|
{
|
|
*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 += *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 + *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 (*frame_length > 0)
|
|
{
|
|
if (!is_control_frame(opcode))
|
|
{
|
|
tmp = realloc(
|
|
msg, sizeof(unsigned char) * (*msg_idx + *frame_length + is_fin));
|
|
if (!tmp)
|
|
{
|
|
DEBUG("Cannot allocate memory, requested: % " PRId64 "\n",
|
|
(*msg_idx + *frame_length + is_fin));
|
|
|
|
wfd->error = 1;
|
|
return (-1);
|
|
}
|
|
msg = tmp;
|
|
*buf = msg;
|
|
}
|
|
|
|
/* Copy to the proper location. */
|
|
for (i = 0; i < *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 (is_fin && *frame_size > 0)
|
|
{
|
|
/* Increase memory if our FIN frame is of length 0. */
|
|
if (!*frame_length && !is_control_frame(opcode))
|
|
{
|
|
tmp = realloc(msg, sizeof(unsigned char) * (*msg_idx + 1));
|
|
if (!tmp)
|
|
{
|
|
DEBUG("Cannot allocate memory, requested: %" PRId64 "\n",
|
|
(*msg_idx + 1));
|
|
|
|
wfd->error = 1;
|
|
return (-1);
|
|
}
|
|
msg = tmp;
|
|
*buf = 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_frame(struct ws_frame_data *wfd)
|
|
{
|
|
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. */
|
|
uint32_t utf8_state; /* Current UTF-8 state. */
|
|
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. */
|
|
|
|
msg_data = NULL;
|
|
msg_ctrl = wfd->msg_ctrl;
|
|
is_fin = 0;
|
|
frame_length = 0;
|
|
frame_size = 0;
|
|
msg_idx_data = 0;
|
|
msg_idx_ctrl = 0;
|
|
wfd->frame_size = 0;
|
|
wfd->frame_type = -1;
|
|
wfd->msg = NULL;
|
|
utf8_state = UTF8_ACCEPT;
|
|
|
|
/* Read until find a FIN or a unsupported frame. */
|
|
do
|
|
{
|
|
/*
|
|
* Obs: next_byte() can return error if not possible to read the
|
|
* next frame byte, in this case, we return an error.
|
|
*
|
|
* However, please note that this check is only made here and in
|
|
* the subsequent next_bytes() calls this also may occur too.
|
|
* wsServer is assuming that the client only create right
|
|
* frames and we will do not have disconnections while reading
|
|
* the frame but just when waiting for a frame.
|
|
*/
|
|
cur_byte = next_byte(wfd);
|
|
if (cur_byte == -1)
|
|
return (-1);
|
|
|
|
is_fin = (cur_byte & 0xFF) >> WS_FIN_SHIFT;
|
|
opcode = (cur_byte & 0xF);
|
|
|
|
/*
|
|
* Check for RSV field.
|
|
*
|
|
* Since wsServer do not negotiate extensions if we receive
|
|
* a RSV field, we must drop the connection.
|
|
*/
|
|
if (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 && opcode == WS_FR_OP_CONT) ||
|
|
(wfd->frame_type != -1 && !is_control_frame(opcode) &&
|
|
opcode != WS_FR_OP_CONT))
|
|
{
|
|
DEBUG("Unexpected frame was received!, opcode: %d, previous: %d\n",
|
|
opcode, wfd->frame_type);
|
|
wfd->error = 1;
|
|
break;
|
|
}
|
|
|
|
/* Check if one of the valid opcodes. */
|
|
if (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)
|
|
{
|
|
/*
|
|
* Check our current state: if CLOSING, we only accept close
|
|
* frames.
|
|
*
|
|
* Since the server may, at any time, asynchronously, asks
|
|
* to close the client connection, we should terminate
|
|
* immediately.
|
|
*/
|
|
if (get_client_state(wfd->client) == WS_STATE_CLOSING &&
|
|
opcode != WS_FR_OP_CLSE)
|
|
{
|
|
DEBUG("Unexpected frame received, expected CLOSE (%d), "
|
|
"received: (%d)",
|
|
WS_FR_OP_CLSE, opcode);
|
|
wfd->error = 1;
|
|
break;
|
|
}
|
|
|
|
/* Only change frame type if not a CONT frame. */
|
|
if (opcode != WS_FR_OP_CONT && !is_control_frame(opcode))
|
|
wfd->frame_type = opcode;
|
|
|
|
mask = next_byte(wfd);
|
|
frame_length = mask & 0x7F;
|
|
frame_size = 0;
|
|
msg_idx_ctrl = 0;
|
|
|
|
/*
|
|
* We should deny non-FIN control frames or that have
|
|
* more than 125 octets.
|
|
*/
|
|
if (is_control_frame(opcode) && (!is_fin || frame_length > 125))
|
|
{
|
|
DEBUG("Control frame bigger than 125 octets or not a FIN "
|
|
"frame!\n");
|
|
wfd->error = 1;
|
|
break;
|
|
}
|
|
|
|
/* Normal data frames. */
|
|
if (opcode == WS_FR_OP_TXT || opcode == WS_FR_OP_BIN ||
|
|
opcode == WS_FR_OP_CONT)
|
|
{
|
|
if (read_frame(wfd, opcode, &msg_data, &frame_length,
|
|
&wfd->frame_size, &msg_idx_data, masks_data, is_fin) < 0)
|
|
break;
|
|
|
|
#ifdef VALIDATE_UTF8
|
|
/* UTF-8 Validate partial (or not) frame. */
|
|
if (wfd->frame_type == WS_FR_OP_TXT)
|
|
{
|
|
if (is_fin)
|
|
{
|
|
if (is_utf8_len_state(
|
|
msg_data + (msg_idx_data - frame_length),
|
|
frame_length, utf8_state) != UTF8_ACCEPT)
|
|
{
|
|
DEBUG("Dropping invalid complete message!\n");
|
|
wfd->error = 1;
|
|
do_close(wfd, WS_CLSE_INVUTF8);
|
|
}
|
|
}
|
|
|
|
/* Check current state for a CONT or initial TXT frame. */
|
|
else
|
|
{
|
|
utf8_state = is_utf8_len_state(
|
|
msg_data + (msg_idx_data - frame_length), frame_length,
|
|
utf8_state);
|
|
|
|
/* We can be in any state, except reject. */
|
|
if (utf8_state == UTF8_REJECT)
|
|
{
|
|
DEBUG("Dropping invalid cont/initial frame!\n");
|
|
wfd->error = 1;
|
|
do_close(wfd, WS_CLSE_INVUTF8);
|
|
}
|
|
}
|
|
}
|
|
#endif
|
|
}
|
|
|
|
/*
|
|
* 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).
|
|
*/
|
|
else if (opcode == WS_FR_OP_PONG)
|
|
{
|
|
if (read_frame(wfd, opcode, &msg_ctrl, &frame_length, &frame_size,
|
|
&msg_idx_ctrl, masks_ctrl, is_fin) < 0)
|
|
break;
|
|
|
|
is_fin = 0;
|
|
|
|
/* If there is no content and/or differs the size, ignore it. */
|
|
if (frame_size != sizeof(wfd->client->last_pong_id))
|
|
continue;
|
|
|
|
/*
|
|
* 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);
|
|
|
|
pong_id = pong_msg_to_int32(msg_ctrl);
|
|
if (pong_id < 0 || pong_id > wfd->client->current_ping_id)
|
|
{
|
|
pthread_mutex_unlock(&wfd->client->mtx_ping);
|
|
continue;
|
|
}
|
|
wfd->client->last_pong_id = pong_id;
|
|
|
|
pthread_mutex_unlock(&wfd->client->mtx_ping);
|
|
/* clang-format on */
|
|
continue;
|
|
}
|
|
|
|
/* We should answer to a PING frame as soon as possible. */
|
|
else if (opcode == WS_FR_OP_PING)
|
|
{
|
|
if (read_frame(wfd, opcode, &msg_ctrl, &frame_length, &frame_size,
|
|
&msg_idx_ctrl, masks_ctrl, is_fin) < 0)
|
|
break;
|
|
|
|
if (do_pong(wfd, frame_size) < 0)
|
|
break;
|
|
|
|
/* Quick hack to keep our loop. */
|
|
is_fin = 0;
|
|
}
|
|
|
|
/* We interrupt the loop as soon as we find a CLOSE frame. */
|
|
else
|
|
{
|
|
if (read_frame(wfd, opcode, &msg_ctrl, &frame_length, &frame_size,
|
|
&msg_idx_ctrl, masks_ctrl, is_fin) < 0)
|
|
break;
|
|
|
|
#ifdef VALIDATE_UTF8
|
|
/* If there is a close reason, check if it is UTF-8 valid. */
|
|
if (frame_size > 2 && !is_utf8_len(msg_ctrl + 2, frame_size - 2))
|
|
{
|
|
DEBUG("Invalid close frame payload reason! (not UTF-8)\n");
|
|
wfd->error = 1;
|
|
break;
|
|
}
|
|
#endif
|
|
|
|
/* Since we're aborting, we can scratch the 'data'-related
|
|
* vars here. */
|
|
wfd->frame_size = frame_size;
|
|
wfd->frame_type = WS_FR_OP_CLSE;
|
|
free(msg_data);
|
|
return (0);
|
|
}
|
|
}
|
|
|
|
/* Anything else (unsupported frames). */
|
|
else
|
|
{
|
|
DEBUG("Unsupported frame opcode: %d\n", opcode);
|
|
/* We should consider as error receive an unknown frame. */
|
|
wfd->frame_type = opcode;
|
|
wfd->error = 1;
|
|
}
|
|
|
|
} while (!is_fin && !wfd->error);
|
|
|
|
/* Check for error. */
|
|
if (wfd->error)
|
|
{
|
|
free(msg_data);
|
|
wfd->msg = NULL;
|
|
return (-1);
|
|
}
|
|
|
|
wfd->msg = 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. */
|
|
ws_cli_conn_t *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_frame(&wfd) >= 0)
|
|
{
|
|
/* Text/binary event. */
|
|
if ((wfd.frame_type == WS_FR_OP_TXT || wfd.frame_type == WS_FR_OP_BIN) &&
|
|
!wfd.error)
|
|
{
|
|
cli_events.onmessage(client, 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.
|
|
*/
|
|
cli_events.onclose(client);
|
|
|
|
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)
|
|
close_client(client, 1);
|
|
|
|
return (vclient);
|
|
}
|
|
|
|
/**
|
|
* @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 sockaddr_in client; /* Client. */
|
|
pthread_t client_thread; /* Client thread. */
|
|
struct timeval time; /* Client socket timeout. */
|
|
int new_sock; /* New opened connection. */
|
|
int sock; /* Server sock. */
|
|
int len; /* Length of sockaddr. */
|
|
int i; /* Loop index. */
|
|
|
|
sock = *(int *)data;
|
|
len = sizeof(struct sockaddr_in);
|
|
|
|
while (1)
|
|
{
|
|
/* Accept. */
|
|
new_sock = accept(sock, (struct sockaddr *)&client, (socklen_t *)&len);
|
|
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, &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)
|
|
{
|
|
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;
|
|
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 Main loop for the server.
|
|
*
|
|
* @param evs Events structure.
|
|
* @param port Server port.
|
|
* @param thread_loop If any value other than zero, runs
|
|
* the accept loop in another thread
|
|
* and immediately returns. If 0, runs
|
|
* in the same thread and blocks execution.
|
|
*
|
|
* @param timeout_ms Max timeout if the client is not responding
|
|
* (in milliseconds).
|
|
*
|
|
* @return If @p thread_loop != 0, returns 0. Otherwise, never
|
|
* returns.
|
|
*/
|
|
int ws_socket(struct ws_events *evs, uint16_t port, int thread_loop,
|
|
uint32_t timeout_ms)
|
|
{
|
|
struct sockaddr_in server; /* Server. */
|
|
pthread_t accept_thread; /* Accept thread. */
|
|
int reuse; /* Socket option. */
|
|
int *sock; /* Client sock. */
|
|
|
|
timeout = timeout_ms;
|
|
|
|
/* Ignore 'unused functions' warnings. */
|
|
((void)skip_frame);
|
|
|
|
/* Checks if the event list is a valid pointer. */
|
|
if (evs == NULL)
|
|
panic("Invalid event list!");
|
|
|
|
/* Allocates our sock data. */
|
|
sock = malloc(sizeof(*sock));
|
|
if (!sock)
|
|
panic("Unable to allocate sock, out of memory!\n");
|
|
|
|
/* Copy events. */
|
|
memcpy(&cli_events, evs, sizeof(struct ws_events));
|
|
|
|
#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. */
|
|
*sock = socket(AF_INET, SOCK_STREAM, 0);
|
|
if (*sock < 0)
|
|
panic("Could not create socket");
|
|
|
|
/* Reuse previous address. */
|
|
reuse = 1;
|
|
if (setsockopt(*sock, SOL_SOCKET, SO_REUSEADDR, (const char *)&reuse,
|
|
sizeof(reuse)) < 0)
|
|
{
|
|
panic("setsockopt(SO_REUSEADDR) failed");
|
|
}
|
|
|
|
/* Prepare the sockaddr_in structure. */
|
|
server.sin_family = AF_INET;
|
|
server.sin_addr.s_addr = INADDR_ANY;
|
|
server.sin_port = htons(port);
|
|
|
|
/* Bind. */
|
|
if (bind(*sock, (struct sockaddr *)&server, sizeof(server)) < 0)
|
|
panic("Bind failed");
|
|
|
|
/* Listen. */
|
|
listen(*sock, MAX_CLIENTS);
|
|
|
|
/* Wait for incoming connections. */
|
|
printf("Waiting for incoming connections...\n");
|
|
memset(client_socks, -1, sizeof(client_socks));
|
|
|
|
/* Accept connections. */
|
|
if (!thread_loop)
|
|
ws_accept((void *)sock);
|
|
else
|
|
{
|
|
if (pthread_create(&accept_thread, NULL, ws_accept, (void *)sock))
|
|
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
|