src/websocket.c
Rama master · réplica de lectura del repositorio original.
/**
* websocket.c -- An rfc6455-complaint Web Socket Server
* _______ _______ __ __
* / ____/ | / / ___/____ _____/ /_____ / /_
* / / __ | | /| / /\__ \/ __ \/ ___/ //_/ _ \/ __/
* / /_/ / | |/ |/ /___/ / /_/ / /__/ ,< / __/ /_
* \____/ |__/|__//____/\____/\___/_/|_|\___/\__/
*
* The MIT License (MIT)
* Copyright (c) 2009-2026 Gerardo Orellana <hello @ goaccess.io>
*
* 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.
*/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <arpa/inet.h>
#include <ctype.h>
#include <errno.h>
#include <fcntl.h>
#include <netdb.h>
#include <netinet/in.h>
#include <stdarg.h>
#include <stddef.h>
#include <sys/socket.h>
#include <sys/un.h>
#include <sys/stat.h>
#include <sys/time.h>
#include <sys/ioctl.h>
#include <time.h>
#include <unistd.h>
#if HAVE_CONFIG_H
#include <config.h>
#endif
#include "websocket.h"
#include "base64.h"
#include "error.h"
#include "gslist.h"
#include "sha1.h"
#include "util.h"
#include "xmalloc.h"
/* *INDENT-OFF* */
/* UTF-8 Decoder */
/* Copyright (c) 2008-2009 Bjoern Hoehrmann <bjoern@hoehrmann.de>
* See http://bjoern.hoehrmann.de/utf-8/decoder/dfa/ for details. */
#define UTF8_VALID 0
#define UTF8_INVAL 1
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 */
};
/* *INDENT-ON* */
static struct pollfd *fdstate = NULL;
static nfds_t nfdstate = 0;
static WSConfig wsconfig = { 0 };
/* Convert JWT expiration seconds into poll timeout milliseconds. */
#define WS_MILLISECONDS_PER_SECOND 1000.0
/* Check whether an authenticated client's JWT has expired.
*
* On expiration or clock failure, non-zero is returned.
* For unauthenticated or currently valid clients, 0 is returned. */
static int
ws_client_auth_expired (WSClient *client) {
time_t now = 0;
if (wsconfig.auth_secret == NULL || client->auth_expiry == 0)
return 0;
now = time (NULL);
if (now == (time_t) - 1)
return 1;
return now >= client->auth_expiry;
}
/* Mark a client for closure when its JWT has expired.
*
* On expiration, non-zero is returned.
* For a currently valid client, 0 is returned. */
static int
ws_reject_expired_client (WSClient *client) {
if (!ws_client_auth_expired (client))
return 0;
if (!(client->status & WS_CLOSE))
LOG (("Authentication expired for client %d [%s]\n", client->listener, client->remote_ip));
client->status = WS_ERR | WS_CLOSE;
return 1;
}
static void handle_read_close (int *conn, WSClient * client, WSServer * server);
static void handle_reads (int *conn, WSServer * server);
static void handle_writes (int *conn, WSServer * server);
#ifdef HAVE_LIBSSL
static int shutdown_ssl (WSClient * client);
#endif
/* Determine if the given string is valid UTF-8.
*
* The state after the by has been processed is returned. */
static uint32_t
verify_utf8 (uint32_t *state, const char *str, int len) {
int i;
uint32_t type;
for (i = 0; i < len; ++i) {
type = utf8d[(uint8_t) str[i]];
*state = utf8d[256 + (*state) * 16 + type];
if (*state == UTF8_INVAL)
break;
}
return *state;
}
/* Decode a character maintaining state and a byte, and returns the
* state achieved after processing the byte.
*
* The state after the by has been processed is returned. */
static uint32_t
utf8_decode (uint32_t *state, uint32_t *p, uint32_t b) {
uint32_t type = utf8d[(uint8_t) b];
*p = (*state != UTF8_VALID) ? (b & 0x3fu) | (*p << 6) : (0xff >> type) & (b);
*state = utf8d[256 + *state * 16 + type];
return *state;
}
/* Replace malformed sequences with a substitute character.
*
* On success, it replaces the whole sequence and return a malloc'd buffer. */
static char *
sanitize_utf8 (const char *str, int len) {
char *buf = NULL;
uint32_t state = UTF8_VALID, prev = UTF8_VALID, cp = 0;
int i = 0, j = 0, k = 0, l = 0;
buf = xcalloc (len + 1, sizeof (char));
for (; i < len; prev = state, ++i) {
switch (utf8_decode (&state, &cp, (unsigned char) str[i])) {
case UTF8_INVAL:
/* replace the whole sequence */
if (k) {
for (l = i - k; l < i; ++l)
buf[j++] = '?';
} else {
buf[j++] = '?';
}
state = UTF8_VALID;
if (prev != UTF8_VALID)
i--;
k = 0;
break;
case UTF8_VALID:
/* fill i - k valid continuation bytes */
if (k)
for (l = i - k; l < i; ++l)
buf[j++] = str[l];
buf[j++] = str[i];
k = 0;
break;
default:
/* UTF8_VALID + continuation bytes */
k++;
break;
}
}
return buf;
}
/* find a pollfd structure based on fd
* this should only be called by set_pollfd and unset_pollfd
* because the position in memory may change */
static struct pollfd *
get_pollfd (int fd) {
struct pollfd *pfd, *efd = fdstate + nfdstate;
for (pfd = fdstate; pfd < efd; pfd++) {
if (pfd->fd == fd)
return pfd;
}
return NULL;
}
/* set flags for an existing pollfd structure based on fd,
* otherwise malloc a new one */
static void
set_pollfd (int fd, short flags) {
struct pollfd *pfd;
if (fd == -1)
FATAL ("Cannot poll an invalid fd");
pfd = get_pollfd (fd);
if (pfd == NULL) {
struct pollfd *newstate = xrealloc (fdstate, sizeof (*pfd) * (nfdstate + 1));
fdstate = newstate;
pfd = fdstate + nfdstate++;
pfd->fd = fd;
}
pfd->events = flags;
pfd->revents = 0;
}
/* free a pollfd structure based on fd */
static void
unset_pollfd (int fd) {
struct pollfd *pfd = get_pollfd (fd), *efd;
struct pollfd *newstate;
if (pfd == NULL)
return;
nfdstate--;
/* avoid undefined behaviour with realloc with a size of zero */
if (nfdstate == 0) {
free (fdstate);
fdstate = NULL;
return;
}
efd = fdstate + nfdstate;
if (pfd != efd)
memmove (pfd, pfd + 1, (char *) efd - (char *) pfd);
/* realloc could fail, but that's ok, we don't mind. */
newstate = realloc (fdstate, sizeof (*pfd) * nfdstate);
if (newstate != NULL)
fdstate = newstate;
}
/* Allocate memory for a websocket server */
static WSServer *
new_wsserver (void) {
WSServer *server = xcalloc (1, sizeof (WSServer));
return server;
}
/* Allocate memory for a websocket client */
static WSClient *
new_wsclient (void) {
WSClient *client = xcalloc (1, sizeof (WSClient));
client->status = WS_OK;
return client;
}
/* Allocate memory for a websocket header */
static WSHeaders *
new_wsheader (void) {
WSHeaders *headers = xcalloc (1, sizeof (WSHeaders));
memset (headers->buf, 0, sizeof (headers->buf));
headers->reading = 1;
return headers;
}
/* Allocate memory for a websocket frame */
static WSFrame *
new_wsframe (void) {
WSFrame *frame = xcalloc (1, sizeof (WSFrame));
memset (frame->buf, 0, sizeof (frame->buf));
frame->reading = 1;
return frame;
}
/* Allocate memory for a websocket message */
static WSMessage *
new_wsmessage (void) {
WSMessage *msg = xcalloc (1, sizeof (WSMessage));
return msg;
}
/* Allocate memory for a websocket pipeout */
static WSPipeOut *
new_wspipeout (void) {
WSPipeOut *pipeout = xcalloc (1, sizeof (WSPipeOut));
pipeout->fd = -1;
return pipeout;
}
/* Allocate memory for a websocket pipein */
static WSPipeIn *
new_wspipein (void) {
WSPipeIn *pipein = xcalloc (1, sizeof (WSPipeIn));
pipein->fd = -1;
return pipein;
}
/* Escapes the special characters, e.g., '\n', '\r', '\t', '\'
* in the string source by inserting a '\' before them.
*
* On error NULL is returned.
* On success the escaped string is returned */
static char *
escape_http_request (const char *src) {
char *dest, *q;
const unsigned char *p;
if (src == NULL || *src == '\0')
return NULL;
p = (const unsigned char *) src;
q = dest = xmalloc (strlen (src) * 4 + 1);
while (*p) {
switch (*p) {
case '\\':
*q++ = '\\';
*q++ = '\\';
break;
case '\n':
*q++ = '\\';
*q++ = 'n';
break;
case '\r':
*q++ = '\\';
*q++ = 'r';
break;
case '\t':
*q++ = '\\';
*q++ = 't';
break;
case '"':
*q++ = '\\';
*q++ = '"';
break;
default:
if ((*p < ' ') || (*p >= 0177)) {
/* not ASCII */
} else {
*q++ = *p;
}
break;
}
p++;
}
*q = 0;
return dest;
}
/* Chop n characters from the beginning of the supplied buffer.
*
* The new length of the string is returned. */
static size_t
chop_nchars (char *str, size_t n, size_t len) {
if (n == 0 || str == 0)
return 0;
if (n > len)
n = len;
memmove (str, str + n, len - n);
return (len - n);
}
/* Match a client given a socket id and an item from the list.
*
* On match, 1 is returned, else 0. */
static int
ws_find_client_sock_in_list (void *data, void *needle) {
WSClient *client = data;
return client->listener == (*(int *) needle);
}
/* Find a client given a socket id.
*
* On success, an instance of a GSLList node is returned, else NULL. */
static GSLList *
ws_get_list_node_from_list (int listener, GSLList **colist) {
GSLList *match = NULL;
/* Find the client data for the socket in use */
if (!(match = list_find (*colist, ws_find_client_sock_in_list, &listener)))
return NULL;
return match;
}
/* Find a client given a socket id.
*
* On success, an instance of a WSClient is returned, else NULL. */
static WSClient *
ws_get_client_from_list (int listener, GSLList **colist) {
GSLList *match = NULL;
/* Find the client data for the socket in use */
if (!(match = list_find (*colist, ws_find_client_sock_in_list, &listener)))
return NULL;
return (WSClient *) match->data;
}
/* Free a frame structure and its data for the given client. */
static void
ws_free_frame (WSClient *client) {
if (client->frame)
free (client->frame);
client->frame = NULL;
}
/* Free a message structure and its data for the given client. */
static void
ws_free_message (WSClient *client) {
if (client->message && client->message->payload)
free (client->message->payload);
if (client->message)
free (client->message);
client->message = NULL;
}
/* Free all HTTP handshake headers data for the given client. */
static void
ws_free_header_fields (WSHeaders *headers) {
if (headers->connection)
free (headers->connection);
if (headers->host)
free (headers->host);
if (headers->agent)
free (headers->agent);
if (headers->method)
free (headers->method);
if (headers->origin)
free (headers->origin);
if (headers->path)
free (headers->path);
if (headers->protocol)
free (headers->protocol);
if (headers->jwt)
free (headers->jwt);
if (headers->upgrade)
free (headers->upgrade);
if (headers->ws_accept)
free (headers->ws_accept);
if (headers->ws_key)
free (headers->ws_key);
if (headers->ws_protocol)
free (headers->ws_protocol);
if (headers->ws_resp)
free (headers->ws_resp);
if (headers->ws_sock_ver)
free (headers->ws_sock_ver);
if (headers->referer)
free (headers->referer);
}
/* A wrapper to close a socket. */
static void
ws_close (int listener) {
unset_pollfd (listener);
close (listener);
}
/* Clear the client's sent queue and its data. */
static void
ws_clear_queue (WSClient *client) {
WSQueue **queue = &client->sockqueue;
if (!(*queue))
return;
if ((*queue)->queued)
free ((*queue)->queued);
(*queue)->queued = NULL;
(*queue)->qlen = 0;
free ((*queue));
(*queue) = NULL;
/* done sending the whole queue, stop throttling */
client->status &= ~WS_THROTTLING;
/* done sending, close connection if set to close */
if ((client->status & WS_CLOSE) && (client->status & WS_SENDING))
client->status = WS_CLOSE;
}
/* Free all HTTP handshake headers and structure. */
static void
ws_clear_handshake_headers (WSHeaders *headers) {
ws_free_header_fields (headers);
free (headers);
}
/* Remove the given client from the list. */
static void
ws_remove_client_from_list (WSClient *client, WSServer *server) {
GSLList *node = NULL;
if (!(node = ws_get_list_node_from_list (client->listener, &server->colist)))
return;
if (client->headers)
ws_clear_handshake_headers (client->headers);
list_remove_node (&server->colist, node);
}
#if HAVE_LIBSSL
/* Attempt to send the TLS/SSL "close notify" shutdown and and removes
* the SSL structure pointed to by ssl and frees up the allocated
* memory. */
static void
ws_shutdown_dangling_clients (WSClient *client) {
shutdown_ssl (client);
SSL_free (client->ssl);
client->ssl = NULL;
}
/* Attempt to remove the SSL_CTX object pointed to by ctx and frees up
* the allocated memory and cleans some more generally used TLS/SSL
* memory. */
static void
ws_ssl_cleanup (WSServer *server) {
if (!wsconfig.use_ssl)
return;
if (server->ctx)
SSL_CTX_free (server->ctx);
CRYPTO_cleanup_all_ex_data ();
CRYPTO_set_id_callback (NULL);
CRYPTO_set_locking_callback (NULL);
ERR_free_strings ();
#if OPENSSL_VERSION_NUMBER < 0x10100000L
ERR_remove_state (0);
#endif
EVP_cleanup ();
}
#endif
/* Remove all clients that are still hanging out. */
static int
ws_remove_dangling_clients (void *value, void *user_data) {
WSClient *client = value;
(void) (user_data);
if (client == NULL)
return 1;
if (client->headers)
ws_clear_handshake_headers (client->headers);
if (client->sockqueue)
ws_clear_queue (client);
#ifdef HAVE_LIBSSL
if (client->ssl)
ws_shutdown_dangling_clients (client);
#endif
return 0;
}
/* Do some housekeeping on the named pipe data packet. */
static void
ws_clear_fifo_packet (WSPacket *packet) {
if (!packet)
return;
if (packet->data)
free (packet->data);
free (packet);
}
/* Do some housekeeping on the named pipe. */
static void
ws_clear_pipein (WSPipeIn *pipein) {
WSPacket **packet = &pipein->packet;
if (!pipein)
return;
if (pipein->fd != -1)
ws_close (pipein->fd);
ws_clear_fifo_packet (*packet);
free (pipein);
if (wsconfig.pipein && access (wsconfig.pipein, F_OK) != -1)
unlink (wsconfig.pipein);
}
/* Do some housekeeping on the named pipe. */
static void
ws_clear_pipeout (WSPipeOut *pipeout) {
if (!pipeout)
return;
if (pipeout->fd != -1)
ws_close (pipeout->fd);
free (pipeout);
if (wsconfig.pipeout && access (wsconfig.pipeout, F_OK) != -1)
unlink (wsconfig.pipeout);
}
/* Stop the server and do some cleaning. */
void
ws_stop (WSServer *server) {
WSPipeIn **pipein = &server->pipein;
WSPipeOut **pipeout = &server->pipeout;
ws_clear_pipein (*pipein);
ws_clear_pipeout (*pipeout);
/* close access log (if any) */
if (wsconfig.accesslog)
access_log_close ();
/* remove dangling clients */
if (list_count (server->colist) > 0)
list_foreach (server->colist, ws_remove_dangling_clients, NULL);
if (server->colist)
list_remove_nodes (server->colist);
#ifdef HAVE_LIBSSL
ws_ssl_cleanup (server);
#endif
free (server);
free (fdstate);
fdstate = NULL;
}
/* Set the connection status for the given client and return the given
* bytes.
*
* The given number of bytes are returned. */
static int
ws_set_status (WSClient *client, WSStatus status, int bytes) {
client->status = status;
return bytes;
}
/* Append the source string to destination and reallocates and
* updating the destination buffer appropriately. */
static void
ws_append_str (char **dest, const char *src) {
size_t curlen = strlen (*dest);
size_t srclen = strlen (src);
size_t newlen = curlen + srclen;
char *str = xrealloc (*dest, newlen + 1);
memcpy (str + curlen, src, srclen + 1);
*dest = str;
}
#if HAVE_LIBSSL
/* Create a new SSL_CTX object as framework to establish TLS/SSL
* enabled connections.
*
* On error 1 is returned.
* On success, SSL_CTX object is malloc'd and 0 is returned.
*/
static int
initialize_ssl_ctx (WSServer *server) {
int ret = 1;
SSL_CTX *ctx = NULL;
#if OPENSSL_VERSION_NUMBER < 0x10100000L
SSL_library_init ();
SSL_load_error_strings ();
#endif
/* Ciphers and message digests */
OpenSSL_add_ssl_algorithms ();
/* ssl context */
#if OPENSSL_VERSION_NUMBER < 0x10100000L
if (!(ctx = SSL_CTX_new (SSLv23_server_method ())))
#else
if (!(ctx = SSL_CTX_new (TLS_server_method ())))
#endif
goto out;
/* set certificate */
if (!SSL_CTX_use_certificate_file (ctx, wsconfig.sslcert, SSL_FILETYPE_PEM))
goto out;
/* ssl private key */
if (!SSL_CTX_use_PrivateKey_file (ctx, wsconfig.sslkey, SSL_FILETYPE_PEM))
goto out;
if (!SSL_CTX_check_private_key (ctx))
goto out;
/* since we queued up the send data, a retry won't be the same buffer,
* thus we need the following flags */
SSL_CTX_set_mode (ctx, SSL_MODE_ACCEPT_MOVING_WRITE_BUFFER | SSL_MODE_ENABLE_PARTIAL_WRITE);
server->ctx = ctx;
ret = 0;
out:
if (ret) {
SSL_CTX_free (ctx);
LOG (("Error: %s\n", ERR_error_string (ERR_get_error (), NULL)));
}
return ret;
}
/* Log result code for TLS/SSL I/O operation */
static void
log_return_message (int ret, int err, const char *fn) {
unsigned long e;
switch (err) {
case SSL_ERROR_NONE:
LOG (("SSL: %s -> SSL_ERROR_NONE\n", fn));
LOG (("SSL: TLS/SSL I/O operation completed\n"));
break;
case SSL_ERROR_WANT_READ:
LOG (("SSL: %s -> SSL_ERROR_WANT_READ\n", fn));
LOG (("SSL: incomplete, data available for reading\n"));
break;
case SSL_ERROR_WANT_WRITE:
LOG (("SSL: %s -> SSL_ERROR_WANT_WRITE\n", fn));
LOG (("SSL: incomplete, data available for writing\n"));
break;
case SSL_ERROR_ZERO_RETURN:
LOG (("SSL: %s -> SSL_ERROR_ZERO_RETURN\n", fn));
LOG (("SSL: TLS/SSL connection has been closed\n"));
break;
case SSL_ERROR_WANT_X509_LOOKUP:
LOG (("SSL: %s -> SSL_ERROR_WANT_X509_LOOKUP\n", fn));
break;
case SSL_ERROR_SYSCALL:
LOG (("SSL: %s -> SSL_ERROR_SYSCALL\n", fn));
e = ERR_get_error ();
if (e > 0)
LOG (("SSL: %s -> %s\n", fn, ERR_error_string (e, NULL)));
/* call was not successful because a fatal error occurred either at the
* protocol level or a connection failure occurred. */
if (ret != 0) {
LOG (("SSL bogus handshake interrupt: %s\n", strerror (errno)));
break;
}
/* call not yet finished. */
LOG (("SSL: handshake interrupted, got EOF\n"));
if (errno == EINTR || errno == EWOULDBLOCK || errno == EAGAIN)
LOG (("SSL: %s -> not yet finished %s\n", fn, strerror (errno)));
break;
default:
LOG (("SSL: %s -> failed fatal error code: %d\n", fn, err));
LOG (("SSL: %s\n", ERR_error_string (ERR_get_error (), NULL)));
break;
}
}
/* Shut down the client's TLS/SSL connection
*
* On fatal error, 1 is returned.
* If data still needs to be read/written, -1 is returned.
* On success, the TLS/SSL connection is closed and 0 is returned */
static int
shutdown_ssl (WSClient *client) {
int ret = -1, err = 0;
/* all good */
if ((ret = SSL_shutdown (client->ssl)) > 0)
return ws_set_status (client, WS_CLOSE, 0);
err = SSL_get_error (client->ssl, ret);
log_return_message (ret, err, "SSL_shutdown");
switch (err) {
case SSL_ERROR_WANT_READ:
case SSL_ERROR_WANT_WRITE:
client->sslstatus = WS_TLS_SHUTTING;
break;
case SSL_ERROR_SYSCALL:
if (ret == 0) {
LOG (("SSL: SSL_shutdown, connection unexpectedly closed by peer.\n"));
/* The shutdown is not yet finished. */
if (errno == EINTR || errno == EWOULDBLOCK || errno == EAGAIN)
client->sslstatus = WS_TLS_SHUTTING;
break;
}
LOG (("SSL: SSL_shutdown, probably unrecoverable, forcing close.\n"));
/* FALLTHRU */
case SSL_ERROR_ZERO_RETURN:
case SSL_ERROR_WANT_X509_LOOKUP:
default:
return ws_set_status (client, WS_ERR | WS_CLOSE, 1);
}
return ret;
}
/* Wait for a TLS/SSL client to initiate a TLS/SSL handshake
*
* On fatal error, the connection is shut down.
* If data still needs to be read/written, -1 is returned.
* On success, the TLS/SSL connection is completed and 0 is returned */
static int
accept_ssl (WSClient *client) {
int ret = -1, err = 0;
/* all good on TLS handshake */
if ((ret = SSL_accept (client->ssl)) > 0) {
client->sslstatus &= ~WS_TLS_ACCEPTING;
return 0;
}
err = SSL_get_error (client->ssl, ret);
log_return_message (ret, err, "SSL_accept");
switch (err) {
case SSL_ERROR_WANT_READ:
case SSL_ERROR_WANT_WRITE:
client->sslstatus = WS_TLS_ACCEPTING;
break;
case SSL_ERROR_SYSCALL:
/* Wait for more activity else bail out, for instance if the socket is closed
* during the handshake. */
if (ret < 0 && (errno == EAGAIN || errno == EWOULDBLOCK || errno == EINTR)) {
client->sslstatus = WS_TLS_ACCEPTING;
break;
}
/* The peer notified that it is shutting down through a SSL "close_notify" so
* we shutdown too */
/* FALLTHRU */
case SSL_ERROR_ZERO_RETURN:
case SSL_ERROR_WANT_X509_LOOKUP:
default:
client->sslstatus &= ~WS_TLS_ACCEPTING;
return ws_set_status (client, WS_ERR | WS_CLOSE, 1);
}
return ret;
}
/* Create a new SSL structure for a connection and perform handshake */
static void
handle_accept_ssl (WSClient *client, WSServer *server) {
/* attempt to create SSL connection if we don't have one yet */
if (!client->ssl) {
if (!(client->ssl = SSL_new (server->ctx))) {
LOG (("SSL: SSL_new, new SSL structure failed.\n"));
return;
}
if (!SSL_set_fd (client->ssl, client->listener)) {
LOG (("SSL: unable to set file descriptor\n"));
return;
}
}
/* attempt to initiate the TLS/SSL handshake */
if (accept_ssl (client) == 0) {
LOG (("SSL Accepted: %d %s\n", client->listener, client->remote_ip));
}
}
/* Given the current status of the SSL buffer, perform that action.
*
* On error or if no SSL pending status, 1 is returned.
* On success, the TLS/SSL pending action is called and 0 is returned */
static int
handle_ssl_pending_rw (int *conn, WSServer *server, WSClient *client) {
if (!wsconfig.use_ssl)
return 1;
/* trying to write but still waiting for a successful SSL_accept */
if (client->sslstatus & WS_TLS_ACCEPTING) {
handle_accept_ssl (client, server);
return 0;
}
/* trying to read but still waiting for a successful SSL_read */
if (client->sslstatus & WS_TLS_READING) {
handle_reads (conn, server);
return 0;
}
/* trying to write but still waiting for a successful SSL_write */
if (client->sslstatus & WS_TLS_WRITING) {
handle_writes (conn, server);
return 0;
}
/* trying to write but still waiting for a successful SSL_shutdown */
if (client->sslstatus & WS_TLS_SHUTTING) {
if (shutdown_ssl (client) == 0)
handle_read_close (conn, client, server);
return 0;
}
return 1;
}
/* Write bytes to a TLS/SSL connection for a given client.
*
* On error or if no write is performed <=0 is returned.
* On success, the number of bytes actually written to the TLS/SSL
* connection are returned */
static int
send_ssl_buffer (WSClient *client, const char *buffer, int len) {
int bytes = 0, err = 0;
#if OPENSSL_VERSION_NUMBER < 0x10100000L
ERR_clear_error ();
#endif
if ((bytes = SSL_write (client->ssl, buffer, len)) > 0)
return bytes;
err = SSL_get_error (client->ssl, bytes);
log_return_message (bytes, err, "SSL_write");
switch (err) {
case SSL_ERROR_WANT_WRITE:
break;
case SSL_ERROR_WANT_READ:
client->sslstatus = WS_TLS_WRITING;
break;
case SSL_ERROR_SYSCALL:
if ((bytes < 0 && (errno == EAGAIN || errno == EWOULDBLOCK || errno == EINTR)))
break;
/* The connection was shut down cleanly */
/* FALLTHRU */
case SSL_ERROR_ZERO_RETURN:
case SSL_ERROR_WANT_X509_LOOKUP:
default:
return ws_set_status (client, WS_ERR | WS_CLOSE, -1);
}
return bytes;
}
/* Read data from the given client's socket and set a connection
* status given the output of recv().
*
* On error, -1 is returned and the connection status is set.
* On success, the number of bytes read is returned. */
static int
read_ssl_socket (WSClient *client, char *buffer, int size) {
int bytes = 0, done = 0, err = 0;
do {
#if OPENSSL_VERSION_NUMBER < 0x10100000L
ERR_clear_error ();
#endif
done = 0;
if ((bytes = SSL_read (client->ssl, buffer, size)) > 0)
break;
err = SSL_get_error (client->ssl, bytes);
log_return_message (bytes, err, "SSL_read");
switch (err) {
case SSL_ERROR_WANT_WRITE:
client->sslstatus = WS_TLS_READING;
done = 1;
break;
case SSL_ERROR_WANT_READ:
done = 1;
break;
case SSL_ERROR_SYSCALL:
if ((bytes < 0 && (errno == EAGAIN || errno == EWOULDBLOCK || errno == EINTR)))
break;
/* FALLTHRU */
case SSL_ERROR_ZERO_RETURN:
case SSL_ERROR_WANT_X509_LOOKUP:
default:
return ws_set_status (client, WS_ERR | WS_CLOSE, -1);
}
} while (SSL_pending (client->ssl) && !done);
return bytes;
}
#endif
/* Get sockaddr, either IPv4 or IPv6 */
static void *
ws_get_raddr (struct sockaddr *sa) {
if (sa->sa_family == AF_INET)
return &(((struct sockaddr_in *) (void *) sa)->sin_addr);
return &(((struct sockaddr_in6 *) (void *) sa)->sin6_addr);
}
/* Set the given file descriptor as NON BLOCKING. */
void
set_nonblocking (int sock) {
if (fcntl (sock, F_SETFL, fcntl (sock, F_GETFL, 0) | O_NONBLOCK) == -1)
FATAL ("Unable to set socket as non-blocking: %s.", strerror (errno));
}
/* Accept a new connection on a socket and add it to the list of
* current connected clients.
*
* The newly assigned socket is returned. */
static int
accept_client (int listener, GSLList **colist) {
WSClient *client;
struct sockaddr_storage raddr;
int newfd;
const void *src = NULL;
socklen_t alen;
alen = sizeof (raddr);
if ((newfd = accept (listener, (struct sockaddr *) &raddr, &alen)) == -1)
FATAL ("Unable to set accept: %s.", strerror (errno));
if (newfd == -1) {
LOG (("Unable to accept: %s.", strerror (errno)));
return newfd;
}
src = ws_get_raddr ((struct sockaddr *) &raddr);
/* malloc a new client */
client = new_wsclient ();
client->listener = newfd;
inet_ntop (raddr.ss_family, src, client->remote_ip, INET6_ADDRSTRLEN);
/* add up our new client to keep track of */
if (*colist == NULL)
*colist = list_create (client);
else
*colist = list_insert_prepend (*colist, client);
/* make the socket non-blocking */
set_nonblocking (client->listener);
/* poll for the socket */
set_pollfd (client->listener, POLLIN);
return newfd;
}
/* Extract the HTTP method.
*
* On error, or if not found, NULL is returned.
* On success, the HTTP method is returned. */
static const char *
ws_get_method (const char *token) {
const char *lookfor = NULL;
if ((lookfor = "GET", !memcmp (token, "GET ", 4)) ||
(lookfor = "get", !memcmp (token, "get ", 4)))
return lookfor;
return NULL;
}
/* Parse a request containing the method and protocol.
*
* On error, or unable to parse, NULL is returned.
* On success, the HTTP request is returned and the method and
* protocol are assigned to the corresponding buffers. */
static char *
ws_parse_request (char *line, char **method, char **protocol) {
const char *meth;
char *req = NULL, *request = NULL, *proto = NULL;
ptrdiff_t rlen;
if ((meth = ws_get_method (line)) == NULL) {
return NULL;
} else {
req = line + strlen (meth);
if ((proto = strstr (line, " HTTP/1.0")) == NULL &&
(proto = strstr (line, " HTTP/1.1")) == NULL)
return NULL;
req++;
if ((rlen = proto - req) <= 0)
return NULL;
request = xmalloc (rlen + 1);
strncpy (request, req, rlen);
request[rlen] = 0;
(*method) = strtoupper (xstrdup (meth));
(*protocol) = strtoupper (xstrdup (++proto));
}
return request;
}
/* Given a pair of key/values, assign it to our HTTP headers
* structure. */
static void
ws_set_header_key_value (WSHeaders *headers, char *key, char *value) {
if (strcasecmp ("Host", key) == 0)
headers->host = xstrdup (value);
else if (strcasecmp ("Origin", key) == 0)
headers->origin = xstrdup (value);
else if (strcasecmp ("Upgrade", key) == 0)
headers->upgrade = xstrdup (value);
else if (strcasecmp ("Connection", key) == 0)
headers->connection = xstrdup (value);
else if (strcasecmp ("Sec-WebSocket-Protocol", key) == 0)
headers->ws_protocol = xstrdup (value);
else if (strcasecmp ("Sec-WebSocket-Key", key) == 0)
headers->ws_key = xstrdup (value);
else if (strcasecmp ("Sec-WebSocket-Version", key) == 0)
headers->ws_sock_ver = xstrdup (value);
else if (strcasecmp ("User-Agent", key) == 0)
headers->agent = xstrdup (value);
else if (strcasecmp ("Referer", key) == 0)
headers->referer = xstrdup (value);
}
/* Verify that the given HTTP headers were passed upon doing the
* websocket handshake.
*
* On error, or header missing, 1 is returned.
* On success, 0 is returned. */
static int
ws_verify_req_headers (WSHeaders *headers) {
if (!headers->host)
return 1;
if (!headers->method)
return 1;
if (!headers->protocol)
return 1;
if (!headers->path)
return 1;
if (wsconfig.origin && !headers->origin)
return 1;
if (wsconfig.origin && strcasecmp (wsconfig.origin, headers->origin) != 0)
return 1;
if (!headers->connection)
return 1;
if (!headers->ws_key)
return 1;
if (!headers->ws_sock_ver)
return 1;
return 0;
}
/* Extract JWT token from query string */
static char *
ws_extract_jwt_token (char *path) {
char decoded_token[8192] = { 0 }; // Adjust size as needed
char *query = NULL, *tokenParam = NULL, *end = NULL;
if (!path)
return NULL;
// Look for a query string in the path
query = strchr (path, '?');
if (!query)
return NULL;
query++; // Skip the '?'
// Look for the token parameter
tokenParam = strstr (query, "token=");
if (!tokenParam)
return NULL;
tokenParam += strlen ("token="); // Move pointer past "token="
// Decode the token
decode_hex (tokenParam, decoded_token, 1);
// Find the end of the token (next '&' or end of string)
end = strpbrk (decoded_token, "& ");
if (end)
*end = '\0';
// Return a dynamically allocated copy of the decoded token
return strdup (decoded_token);
}
/* From RFC2616, each header field consists of a name followed by a
* colon (":") and the field value. Field names are case-insensitive.
* The field value MAY be preceded by any amount of LWS, though a
* single SP is preferred */
static int
ws_set_header_fields (char *line, WSHeaders *headers) {
char *path = NULL, *method = NULL, *proto = NULL;
char *p = NULL, *value = NULL;
if (line[0] == '\n' || line[0] == '\r')
return 1;
if ((strstr (line, "GET ")) || (strstr (line, "get "))) {
if ((path = ws_parse_request (line, &method, &proto)) == NULL)
return 1;
headers->path = path;
headers->method = method;
headers->protocol = proto;
/* Extract JWT token from path */
headers->jwt = ws_extract_jwt_token (path);
return 0;
}
if ((p = strchr (line, ':')) == NULL)
return 1;
value = p + 1;
while (p != line && isspace ((unsigned char) *(p - 1)))
p--;
if (p == line)
return 1;
*p = '\0';
if (strpbrk (line, " \t") != NULL) {
*p = ' ';
return 1;
}
while (isspace ((unsigned char) *value))
value++;
ws_set_header_key_value (headers, line, value);
return 0;
}
/* Parse the given HTTP headers and set the expected websocket
* handshake.
*
* On error, or 1 is returned.
* On success, 0 is returned. */
static int
parse_headers (WSHeaders *headers) {
char *tmp = NULL;
const char *buffer = headers->buf;
const char *line = buffer, *next = NULL;
int len = 0;
while (line) {
if ((next = strstr (line, "\r\n")) != NULL)
len = (next - line);
else
len = strlen (line);
if (len <= 0)
return 1;
tmp = xmalloc (len + 1);
memcpy (tmp, line, len);
tmp[len] = '\0';
if (ws_set_header_fields (tmp, headers) == 1) {
free (tmp);
return 1;
}
free (tmp);
line = next ? (next + 2) : NULL;
if (next && strcmp (next, "\r\n\r\n") == 0)
break;
}
return 0;
}
/* Set into a queue the data that couldn't be sent. */
static void
ws_queue_sockbuf (WSClient *client, const char *buffer, int len, int bytes) {
WSQueue *queue = xcalloc (1, sizeof (WSQueue));
if (bytes < 1)
bytes = 0;
queue->queued = xcalloc (len - bytes, sizeof (char));
memcpy (queue->queued, buffer + bytes, len - bytes);
queue->qlen = len - bytes;
client->sockqueue = queue;
client->status |= WS_SENDING;
set_pollfd (client->listener, POLLIN | POLLOUT);
}
/* Read data from the given client's socket and set a connection
* status given the output of recv().
*
* On error, -1 is returned and the connection status is set.
* On success, the number of bytes read is returned. */
static int
read_plain_socket (WSClient *client, char *buffer, int size) {
int bytes = 0;
bytes = recv (client->listener, buffer, size, 0);
if (bytes == -1 && (errno == EAGAIN || errno == EWOULDBLOCK))
return ws_set_status (client, WS_READING, bytes);
else if (bytes == -1 || bytes == 0)
return ws_set_status (client, WS_ERR | WS_CLOSE, bytes);
return bytes;
}
/* Read data from the given client's socket and set a connection
* status given the output of recv().
*
* On error, -1 is returned and the connection status is set.
* On success, the number of bytes read is returned. */
static int
read_socket (WSClient *client, char *buffer, int size) {
#ifdef HAVE_LIBSSL
if (wsconfig.use_ssl)
return read_ssl_socket (client, buffer, size);
else
return read_plain_socket (client, buffer, size);
#else
return read_plain_socket (client, buffer, size);
#endif
}
static int
send_plain_buffer (WSClient *client, const char *buffer, int len) {
return send (client->listener, buffer, len, 0);
}
static int
send_buffer (WSClient *client, const char *buffer, int len) {
#ifdef HAVE_LIBSSL
if (wsconfig.use_ssl)
return send_ssl_buffer (client, buffer, len);
else
return send_plain_buffer (client, buffer, len);
#else
return send_plain_buffer (client, buffer, len);
#endif
}
/* Attempt to send the given buffer to the given socket.
*
* On error, -1 is returned and the connection status is set.
* On success, the number of bytes sent is returned. */
static int
ws_respond_data (WSClient *client, const char *buffer, int len) {
int bytes = 0;
bytes = send_buffer (client, buffer, len);
if (bytes == -1 && errno == EPIPE)
return ws_set_status (client, WS_ERR | WS_CLOSE, bytes);
/* did not send all of it... buffer it for a later attempt */
if (bytes < len || (bytes == -1 && (errno == EAGAIN || errno == EWOULDBLOCK)))
ws_queue_sockbuf (client, buffer, len, bytes);
return bytes;
}
/* Attempt to send the queued up client's data to the given socket.
*
* On error, -1 is returned and the connection status is set.
* On success, the number of bytes sent is returned. */
static int
ws_respond_cache (WSClient *client) {
WSQueue *queue = client->sockqueue;
int bytes = 0;
bytes = send_buffer (client, queue->queued, queue->qlen);
if (bytes == -1 && errno == EPIPE)
return ws_set_status (client, WS_ERR | WS_CLOSE, bytes);
if (bytes == -1 && (errno == EAGAIN || errno == EWOULDBLOCK))
return bytes;
if (chop_nchars (queue->queued, bytes, queue->qlen) == 0)
ws_clear_queue (client);
else
queue->qlen -= bytes;
return bytes;
}
/* Attempt to realloc the current sent queue.
*
* On error, 1 is returned and the connection status is set.
* On success, 0 is returned. */
static int
ws_realloc_send_buf (WSClient *client, const char *buf, int len) {
WSQueue *queue = client->sockqueue;
char *tmp = NULL;
int newlen = 0;
newlen = queue->qlen + len;
tmp = realloc (queue->queued, newlen);
if (tmp == NULL && newlen > 0) {
ws_clear_queue (client);
return ws_set_status (client, WS_ERR | WS_CLOSE, 1);
}
queue->queued = tmp;
memcpy (queue->queued + queue->qlen, buf, len);
queue->qlen += len;
/* client probably too slow, so stop queueing until everything is
* sent */
if (queue->qlen >= WS_THROTTLE_THLD)
client->status |= WS_THROTTLING;
return 0;
}
/* An entry point to attempt to send the client's data.
*
* On error, 1 is returned and the connection status is set.
* On success, the number of bytes sent is returned. */
static int
ws_respond (WSClient *client, const char *buffer, int len) {
int bytes = 0;
/* attempt to send the whole buffer */
if (client->sockqueue == NULL)
bytes = ws_respond_data (client, buffer, len);
/* buffer not empty, just append new data iff we're not throttling the
* client */
else if (client->sockqueue != NULL && buffer != NULL && !(client->status & WS_THROTTLING)) {
if (ws_realloc_send_buf (client, buffer, len) == 1)
return bytes;
}
/* send from cache buffer */
else {
bytes = ws_respond_cache (client);
}
return bytes;
}
/* Encode a websocket frame (header/message) and attempt to send it
* through the client's socket.
*
* On success, 0 is returned. */
static int
ws_send_frame (WSClient *client, WSOpcode opcode, const char *p, int sz) {
unsigned char buf[32] = { 0 };
char *frm = NULL;
uint64_t payloadlen = 0, u64;
int hsize = 2;
if (sz < 126) {
payloadlen = sz;
} else if (sz < (1 << 16)) {
payloadlen = WS_PAYLOAD_EXT16;
hsize += 2;
} else {
payloadlen = WS_PAYLOAD_EXT64;
hsize += 8;
}
buf[0] = 0x80 | ((uint8_t) opcode);
switch (payloadlen) {
case WS_PAYLOAD_EXT16:
buf[1] = WS_PAYLOAD_EXT16;
buf[2] = (sz & 0xff00) >> 8;
buf[3] = (sz & 0x00ff) >> 0;
break;
case WS_PAYLOAD_EXT64:
buf[1] = WS_PAYLOAD_EXT64;
u64 = htobe64 (sz);
memcpy (buf + 2, &u64, sizeof (uint64_t));
break;
default:
buf[1] = (sz & 0xff);
}
frm = xcalloc (hsize + sz, sizeof (unsigned char));
memcpy (frm, buf, hsize);
if (p != NULL && sz > 0)
memcpy (frm + hsize, p, sz);
ws_respond (client, frm, hsize + sz);
free (frm);
return 0;
}
/* Send an error message to the given client.
*
* On success, the number of sent bytes is returned. */
static int
ws_error (WSClient *client, unsigned short code, const char *err) {
unsigned int len;
unsigned short code_be;
char buf[128] = { 0 };
len = 2;
code_be = htobe16 (code);
memcpy (buf, &code_be, 2);
if (err)
len += snprintf (buf + 2, sizeof buf - 4, "%s", err);
return ws_send_frame (client, WS_OPCODE_CLOSE, buf, len);
}
/* Log hit to the access log.
*
* On success, the hit/entry is logged. */
static void
access_log (WSClient *client, int status_code) {
WSHeaders *hdrs = client->headers;
char buf[64] = { 0 };
uint32_t elapsed = 0;
struct timeval tv;
struct tm time;
char *req = NULL, *ref = NULL, *ua = NULL;
gettimeofday (&tv, NULL);
strftime (buf, sizeof (buf) - 1, "[%d/%b/%Y:%H:%M:%S %z]", localtime_r (&tv.tv_sec, &time));
elapsed = (client->end_proc.tv_sec - client->start_proc.tv_sec) * 1000.0;
elapsed += (client->end_proc.tv_usec - client->start_proc.tv_usec) / 1000.0;
req = escape_http_request (hdrs->path);
ref = escape_http_request (hdrs->referer);
ua = escape_http_request (hdrs->agent);
ACCESS_LOG (("%s ", client->remote_ip));
ACCESS_LOG (("- - "));
ACCESS_LOG (("%s ", buf));
ACCESS_LOG (("\"%s ", hdrs->method));
ACCESS_LOG (("%s ", req ? req : "-"));
ACCESS_LOG (("%s\" ", hdrs->protocol));
ACCESS_LOG (("%d ", status_code));
ACCESS_LOG (("%d ", hdrs->buflen));
ACCESS_LOG (("\"%s\" ", ref ? ref : "-"));
ACCESS_LOG (("\"%s\" ", ua ? ua : "-"));
ACCESS_LOG (("%u\n", elapsed));
if (req)
free (req);
if (ref)
free (ref);
if (ua)
free (ua);
}
/* Send an HTTP error status to the given client.
*
* On success, the number of sent bytes is returned. */
static int
http_error (WSClient *client, const char *buffer) {
/* do access logging */
gettimeofday (&client->end_proc, NULL);
if (wsconfig.accesslog)
access_log (client, 400);
return ws_respond (client, buffer, strlen (buffer));
}
/* Compute the SHA1 for the handshake. */
static void
ws_sha1_digest (char *s, int len, unsigned char *digest) {
SHA1_CTX sha;
SHA1Init (&sha);
SHA1Update (&sha, (uint8_t *) s, len);
SHA1Final (digest, &sha);
}
/* Set the parsed websocket handshake headers. */
static void
ws_set_handshake_headers (WSHeaders *headers) {
size_t klen = strlen (headers->ws_key);
size_t mlen = strlen (WS_MAGIC_STR);
size_t len = klen + mlen;
char *s = xmalloc (klen + mlen + 1);
uint8_t digest[SHA_DIGEST_LENGTH];
memset (digest, 0, sizeof *digest);
memcpy (s, headers->ws_key, klen);
memcpy (s + klen, WS_MAGIC_STR, mlen + 1);
ws_sha1_digest (s, len, digest);
/* set response headers */
headers->ws_accept = base64_encode ((unsigned char *) digest, sizeof (digest));
headers->ws_resp = xstrdup (WS_SWITCH_PROTO_STR);
if (!headers->upgrade)
headers->upgrade = xstrdup ("websocket");
if (!headers->connection)
headers->connection = xstrdup ("Upgrade");
free (s);
}
/* Send the websocket handshake headers to the given client.
*
* On success, the number of sent bytes is returned. */
static int
ws_send_handshake_headers (WSClient *client, WSHeaders *headers) {
int bytes = 0;
char *str = xstrdup ("");
ws_append_str (&str, headers->ws_resp);
ws_append_str (&str, CRLF);
ws_append_str (&str, "Upgrade: ");
ws_append_str (&str, headers->upgrade);
ws_append_str (&str, CRLF);
ws_append_str (&str, "Connection: ");
ws_append_str (&str, headers->connection);
ws_append_str (&str, CRLF);
ws_append_str (&str, "Sec-WebSocket-Accept: ");
ws_append_str (&str, headers->ws_accept);
ws_append_str (&str, CRLF CRLF);
bytes = ws_respond (client, str, strlen (str));
free (str);
return bytes;
}
/* Given the HTTP connection headers, attempt to parse the web socket
* handshake headers.
*
* On success, the number of sent bytes is returned. */
static int
ws_get_handshake (WSClient *client, WSServer *server) {
int bytes = 0, readh = 0;
char *buf = NULL;
if (client->headers == NULL)
client->headers = new_wsheader ();
buf = client->headers->buf;
readh = client->headers->buflen;
/* Probably the connection was closed before finishing handshake */
if ((bytes = read_socket (client, buf + readh, WS_MAX_HEAD_SZ - readh)) < 1) {
if (client->status & WS_CLOSE) {
LOG (("Connection aborted %d [%s]...\n", client->listener, client->remote_ip));
http_error (client, WS_BAD_REQUEST_STR);
}
LOG (("Can't establish handshake %d [%s]...\n", client->listener, client->remote_ip));
return ws_set_status (client, WS_CLOSE, bytes);
}
client->headers->buflen += bytes;
buf[client->headers->buflen] = '\0'; /* null-terminate */
/* Must have a \r\n\r\n */
if (strstr (buf, "\r\n\r\n") == NULL) {
if (strlen (buf) < WS_MAX_HEAD_SZ) {
LOG (("Headers too long %d [%s]...\n", client->listener, client->remote_ip));
return ws_set_status (client, WS_READING, bytes);
}
LOG (("Invalid newlines for handshake %d [%s]...\n", client->listener, client->remote_ip));
http_error (client, WS_BAD_REQUEST_STR);
return ws_set_status (client, WS_CLOSE, bytes);
}
/* Ensure we have valid HTTP headers for the handshake */
if (parse_headers (client->headers) != 0) {
LOG (("Invalid headers for handshake %d [%s]...\n", client->listener, client->remote_ip));
http_error (client, WS_BAD_REQUEST_STR);
return ws_set_status (client, WS_CLOSE, bytes);
}
/* Ensure we have the required headers */
if (ws_verify_req_headers (client->headers) != 0) {
LOG (("Missing headers for handshake %d [%s]...\n", client->listener, client->remote_ip));
http_error (client, WS_BAD_REQUEST_STR);
return ws_set_status (client, WS_CLOSE, bytes);
}
/* Ensure we can authenticate the connection */
if (wsconfig.auth_secret &&
wsconfig.auth (client->headers->jwt, wsconfig.auth_secret, &client->auth_expiry) != 1) {
LOG (("Unable to authenticate connection %d [%s]...\n", client->listener, client->remote_ip));
http_error (client, WS_UNAUTHORIZED_STR);
return ws_set_status (client, WS_CLOSE, bytes);
}
ws_set_handshake_headers (client->headers);
/* handshake response */
ws_send_handshake_headers (client, client->headers);
/* upon success, call onopen() callback */
if (server->onopen && wsconfig.strict && !wsconfig.echomode)
server->onopen (server->pipeout, client);
client->headers->reading = 0;
/* do access logging */
gettimeofday (&client->end_proc, NULL);
if (wsconfig.accesslog)
access_log (client, 101);
LOG (("Active: %d\n", list_count (server->colist)));
return ws_set_status (client, WS_OK, bytes);
}
/* Send a data message to a client whose authentication is current.
*
* On success, 0 is returned.
* On expired authentication, -1 is returned and the client is marked for closure. */
int
ws_send_data (WSClient *client, WSOpcode opcode, const char *p, int sz) {
char *buf = NULL;
if (ws_reject_expired_client (client))
return -1;
buf = sanitize_utf8 (p, sz);
ws_send_frame (client, opcode, buf, sz);
free (buf);
return 0;
}
/* Read a websocket frame's header.
*
* On success, the number of bytes read is returned. */
static int
ws_read_header (WSClient *client, WSFrame *frm, int pos, int need) {
char *buf = frm->buf;
int bytes = 0;
/* read the first 2 bytes for basic frame info */
if ((bytes = read_socket (client, buf + pos, need)) < 1) {
if (client->status & WS_CLOSE)
ws_error (client, WS_CLOSE_UNEXPECTED, "Unable to read header");
return bytes;
}
frm->buflen += bytes;
frm->buf[frm->buflen] = '\0'; /* null-terminate */
return bytes;
}
/* Read a websocket frame's payload.
*
* On success, the number of bytes read is returned. */
static int
ws_read_payload (WSClient *client, WSMessage *msg, int pos, int need) {
char *buf = msg->payload;
int bytes = 0;
/* read the first 2 bytes for basic frame info */
if ((bytes = read_socket (client, buf + pos, need)) < 1) {
if (client->status & WS_CLOSE)
ws_error (client, WS_CLOSE_UNEXPECTED, "Unable to read payload");
return bytes;
}
msg->buflen += bytes;
msg->payloadsz += bytes;
return bytes;
}
/* Set the basic frame headers on a frame structure.
*
* On success, 0 is returned. */
static int
ws_set_front_header_fields (WSClient *client) {
WSFrame **frm = &client->frame;
char *buf = (*frm)->buf;
(*frm)->fin = WS_FRM_FIN (*(buf));
(*frm)->masking = WS_FRM_MASK (*(buf + 1));
(*frm)->opcode = WS_FRM_OPCODE (*(buf));
(*frm)->res = WS_FRM_R1 (*(buf)) || WS_FRM_R2 (*(buf)) || WS_FRM_R3 (*(buf));
/* should be masked and can't be using RESVd bits */
if (!(*frm)->masking || (*frm)->res)
return ws_set_status (client, WS_ERR | WS_CLOSE, 1);
return 0;
}
/* Unmask the payload given the current frame's masking key. */
static void
ws_unmask_payload (char *buf, int len, int offset, unsigned char mask[]) {
int i, j = 0;
/* unmask data */
for (i = offset; i < len; ++i, ++j) {
buf[i] ^= mask[j % 4];
}
}
/* Close a websocket connection. */
static int
ws_handle_close (WSClient *client) {
client->status = WS_ERR | WS_CLOSE;
return ws_send_frame (client, WS_OPCODE_CLOSE, NULL, 0);
}
/* Handle a websocket error.
*
* On success, the number of bytes sent is returned. */
static int
ws_handle_err (WSClient *client, unsigned short code, WSStatus status, const char *m) {
client->status = status;
return ws_error (client, code, m);
}
/* Handle a websocket pong. */
static void
ws_handle_pong (WSClient *client) {
WSFrame **frm = &client->frame;
if (!(*frm)->fin) {
ws_handle_err (client, WS_CLOSE_PROTO_ERR, WS_ERR | WS_CLOSE, NULL);
return;
}
ws_free_message (client);
}
/* Handle a websocket ping from the client and it attempts to send
* back a pong as soon as possible. */
static void
ws_handle_ping (WSClient *client) {
WSFrame **frm = &client->frame;
WSMessage **msg = &client->message;
char *buf = NULL, *tmp = NULL;
int pos = 0, len = (*frm)->payloadlen, newlen = 0;
/* RFC states that Control frames themselves MUST NOT be
* fragmented. */
if (!(*frm)->fin) {
ws_handle_err (client, WS_CLOSE_PROTO_ERR, WS_ERR | WS_CLOSE, NULL);
return;
}
/* Control frames are only allowed to have payload up to and
* including 125 octets */
if ((*frm)->payloadlen > 125) {
ws_handle_err (client, WS_CLOSE_PROTO_ERR, WS_ERR | WS_CLOSE, NULL);
return;
}
/* No payload from ping */
if (len == 0) {
ws_send_frame (client, WS_OPCODE_PONG, NULL, 0);
return;
}
/* Copy the ping payload */
pos = (*msg)->payloadsz - len;
buf = xcalloc (len, sizeof (char));
memcpy (buf, (*msg)->payload + pos, len);
/* Unmask it */
ws_unmask_payload (buf, len, 0, (*frm)->mask);
/* Resize the current payload (keep an eye on this realloc) */
newlen = (*msg)->payloadsz - len;
tmp = realloc ((*msg)->payload, newlen);
if (tmp == NULL && newlen > 0) {
free ((*msg)->payload);
free (buf);
(*msg)->payload = NULL;
client->status = WS_ERR | WS_CLOSE;
return;
}
(*msg)->payload = tmp;
(*msg)->payloadsz -= len;
ws_send_frame (client, WS_OPCODE_PONG, buf, len);
(*msg)->buflen = 0; /* done with the current frame's payload */
/* Control frame injected in the middle of a fragmented message. */
if (!(*msg)->fragmented) {
ws_free_message (client);
}
free (buf);
}
/* Ensure we have valid UTF-8 text payload.
*
* On error, or if the message is invalid, 1 is returned.
* On success, or if the message is valid, 0 is returned. */
int
ws_validate_string (const char *str, int len) {
uint32_t state = UTF8_VALID;
if (verify_utf8 (&state, str, len) == UTF8_INVAL) {
LOG (("Invalid UTF8 data!\n"));
return 1;
}
if (state != UTF8_VALID) {
LOG (("Invalid UTF8 data!\n"));
return 1;
}
return 0;
}
/* It handles a text or binary message frame from the client. */
static void
ws_handle_text_bin (WSClient *client, WSServer *server) {
WSFrame **frm = &client->frame;
WSMessage **msg = &client->message;
int offset = (*msg)->mask_offset;
/* All data frames after the initial data frame must have opcode 0 */
if ((*msg)->fragmented && (*frm)->opcode != WS_OPCODE_CONTINUATION) {
client->status = WS_ERR | WS_CLOSE;
return;
}
/* RFC states that there is a new masking key per frame, therefore,
* time to unmask... */
ws_unmask_payload ((*msg)->payload, (*msg)->payloadsz, offset, (*frm)->mask);
/* Done with the current frame's payload */
(*msg)->buflen = 0;
/* Reading a fragmented frame */
(*msg)->fragmented = 1;
if (!(*frm)->fin)
return;
/* validate text data encoded as UTF-8 */
if ((*msg)->opcode == WS_OPCODE_TEXT) {
if (ws_validate_string ((*msg)->payload, (*msg)->payloadsz) != 0) {
ws_handle_err (client, WS_CLOSE_INVALID_UTF8, WS_ERR | WS_CLOSE, NULL);
return;
}
}
if ((*msg)->opcode != WS_OPCODE_CONTINUATION && server->onmessage) {
/* just echo the message to the client */
if (wsconfig.echomode)
ws_send_data (client, (*msg)->opcode, (*msg)->payload, (*msg)->payloadsz);
/* just pipe out the message */
else if (!wsconfig.strict)
ws_write_fifo (server->pipeout, (*msg)->payload, (*msg)->payloadsz);
else
server->onmessage (server->pipeout, client);
}
ws_free_message (client);
}
/* Depending on the frame opcode, then we take certain decisions. */
static void
ws_manage_payload_opcode (WSClient *client, WSServer *server) {
WSFrame **frm = &client->frame;
WSMessage **msg = &client->message;
switch ((*frm)->opcode) {
case WS_OPCODE_CONTINUATION:
LOG (("CONTINUATION\n"));
/* first frame can't be a continuation frame */
if (!(*msg)->fragmented) {
client->status = WS_ERR | WS_CLOSE;
break;
}
ws_handle_text_bin (client, server);
break;
case WS_OPCODE_TEXT:
case WS_OPCODE_BIN:
LOG (("TEXT\n"));
client->message->opcode = (*frm)->opcode;
ws_handle_text_bin (client, server);
break;
case WS_OPCODE_PONG:
LOG (("PONG\n"));
ws_handle_pong (client);
break;
case WS_OPCODE_PING:
LOG (("PING\n"));
ws_handle_ping (client);
break;
default:
LOG (("CLOSE\n"));
ws_handle_close (client);
}
}
/* Set the extended payload length into the given pointer. */
static void
ws_set_extended_header_size (const char *buf, int *extended) {
uint64_t payloadlen = 0;
/* determine the payload length, else read more data */
payloadlen = WS_FRM_PAYLOAD (*(buf + 1));
switch (payloadlen) {
case WS_PAYLOAD_EXT16:
*extended = 2;
break;
case WS_PAYLOAD_EXT64:
*extended = 8;
break;
}
}
/* Set the extended payload length into our frame structure. */
static void
ws_set_payloadlen (WSFrame *frm, const char *buf) {
uint64_t payloadlen = 0, len64;
uint16_t len16;
/* determine the payload length, else read more data */
payloadlen = WS_FRM_PAYLOAD (*(buf + 1));
switch (payloadlen) {
case WS_PAYLOAD_EXT16:
memcpy (&len16, (buf + 2), sizeof (uint16_t));
frm->payloadlen = ntohs (len16);
break;
case WS_PAYLOAD_EXT64:
memcpy (&len64, (buf + 2), sizeof (uint64_t));
frm->payloadlen = be64toh (len64);
break;
default:
frm->payloadlen = payloadlen;
}
}
/* Set the masking key into our frame structure. */
static void
ws_set_masking_key (WSFrame *frm, const char *buf) {
uint64_t payloadlen = 0;
/* determine the payload length, else read more data */
payloadlen = WS_FRM_PAYLOAD (*(buf + 1));
switch (payloadlen) {
case WS_PAYLOAD_EXT16:
memcpy (&frm->mask, buf + 4, sizeof (frm->mask));
break;
case WS_PAYLOAD_EXT64:
memcpy (&frm->mask, buf + 10, sizeof (frm->mask));
break;
default:
memcpy (&frm->mask, buf + 2, sizeof (frm->mask));
}
}
/* Attempt to read the frame's header and set the relevant data into
* our frame structure.
*
* On error, or if no data available to read, the number of bytes is
* returned and the appropriate connection status is set.
* On success, the number of bytes is returned. */
static int
ws_get_frm_header (WSClient *client) {
WSFrame **frm = NULL;
int bytes = 0, readh = 0, need = 0, offset = 0, extended = 0;
if (client->frame == NULL)
client->frame = new_wsframe ();
frm = &client->frame;
/* Read the first 2 bytes for basic frame info */
readh = (*frm)->buflen; /* read from header so far */
need = 2 - readh; /* need to read */
if (need > 0) {
if ((bytes = ws_read_header (client, (*frm), readh, need)) < 1)
return bytes;
if (bytes != need)
return ws_set_status (client, WS_READING, bytes);
}
offset += 2;
if (ws_set_front_header_fields (client) != 0)
return bytes;
ws_set_extended_header_size ((*frm)->buf, &extended);
/* read the extended header */
readh = (*frm)->buflen; /* read from header so far */
need = (extended + offset) - readh; /* read from header field so far */
if (need > 0) {
if ((bytes = ws_read_header (client, (*frm), readh, need)) < 1)
return bytes;
if (bytes != need)
return ws_set_status (client, WS_READING, bytes);
}
offset += extended;
/* read the masking key */
readh = (*frm)->buflen; /* read from header so far */
need = (4 + offset) - readh;
if (need > 0) {
if ((bytes = ws_read_header (client, (*frm), readh, need)) < 1)
return bytes;
if (bytes != need)
return ws_set_status (client, WS_READING, bytes);
}
offset += 4;
ws_set_payloadlen ((*frm), (*frm)->buf);
ws_set_masking_key ((*frm), (*frm)->buf);
if ((*frm)->payloadlen > (uint64_t) wsconfig.max_frm_size) {
ws_error (client, WS_CLOSE_TOO_LARGE, "Frame is too big");
return ws_set_status (client, WS_ERR | WS_CLOSE, bytes);
}
(*frm)->buflen = 0;
(*frm)->reading = 0;
(*frm)->payload_offset = offset;
return ws_set_status (client, WS_OK, bytes);
}
/* Attempt to realloc the message payload.
*
* On error, 1 is returned.
* On success, 0 is returned. */
static int
ws_realloc_frm_payload (WSFrame *frm, WSMessage *msg) {
char *tmp = NULL;
uint64_t newlen = 0;
newlen = msg->payloadsz + frm->payloadlen;
if (newlen > (uint64_t) wsconfig.max_frm_size)
return 1;
tmp = realloc (msg->payload, newlen);
if (tmp == NULL && newlen > 0) {
free (msg->payload);
msg->payload = NULL;
return 1;
}
msg->payload = tmp;
return 0;
}
/* Attempt to read the frame's payload and set the relevant data into
* our message structure.
*
* On error, or if no data available to read, the number of bytes is
* returned and the appropriate connection status is set.
* On success, the number of bytes is returned. */
static int
ws_get_frm_payload (WSClient *client, WSServer *server) {
WSFrame **frm = NULL;
WSMessage **msg = NULL;
int bytes = 0, readh = 0, need = 0;
if (client->message == NULL)
client->message = new_wsmessage ();
frm = &client->frame;
msg = &client->message;
/* message within the same frame */
if ((*msg)->payload == NULL && (*frm)->payloadlen)
(*msg)->payload = xcalloc ((*frm)->payloadlen, sizeof (char));
/* handle a new frame */
else if ((*msg)->buflen == 0 && (*frm)->payloadlen) {
if (ws_realloc_frm_payload ((*frm), (*msg)) == 1)
return ws_set_status (client, WS_ERR | WS_CLOSE, 0);
}
readh = (*msg)->buflen; /* read from so far */
need = (*frm)->payloadlen - readh; /* need to read */
if (need > 0) {
if ((bytes = ws_read_payload (client, (*msg), (*msg)->payloadsz, need)) < 0)
return bytes;
if (bytes != need)
return ws_set_status (client, WS_READING, bytes);
}
(*msg)->mask_offset = (*msg)->payloadsz - (*msg)->buflen;
ws_manage_payload_opcode (client, server);
ws_free_frame (client);
return bytes;
}
/* Determine if we need to read a frame's header or its payload.
*
* On success, the number of bytes is returned. */
static int
ws_get_message (WSClient *client, WSServer *server) {
int bytes = 0;
if ((client->frame == NULL) || (client->frame->reading))
if ((bytes = ws_get_frm_header (client)) < 1 || client->frame->reading)
return bytes;
return ws_get_frm_payload (client, server);
}
/* Determine if we need to read an HTTP request or a websocket frame.
*
* On success, the number of bytes is returned. */
static int
read_client_data (WSClient *client, WSServer *server) {
int bytes = 0;
/* Handshake */
if ((!(client->headers) || (client->headers->reading)))
bytes = ws_get_handshake (client, server);
/* Message */
else
bytes = ws_get_message (client, server);
return bytes;
}
/* Determine how long poll may wait before the next JWT expires.
*
* When an expiration deadline exists, its timeout in milliseconds is returned.
* When no authenticated client has a deadline, -1 is returned. */
static int
ws_get_auth_poll_timeout (WSServer *server) {
WSClient *client = NULL;
void *data = NULL;
double remaining = 0.0;
int candidate = 0, timeout = -1;
time_t now = 0;
if (wsconfig.auth_secret == NULL)
return -1;
now = time (NULL);
if (now == (time_t) - 1)
return 0;
/* *INDENT-OFF* */
GSLIST_FOREACH (server->colist, data, {
client = data;
if (client->auth_expiry != 0) {
remaining = difftime (client->auth_expiry, now);
if (remaining <= 0.0)
return 0;
if (remaining >= INT_MAX / WS_MILLISECONDS_PER_SECOND)
candidate = INT_MAX;
else
candidate = (int) (remaining * WS_MILLISECONDS_PER_SECOND);
if (timeout == -1 || candidate < timeout)
timeout = candidate;
}
});
/* *INDENT-ON* */
return timeout;
}
/* Handle a tcp close connection. */
static void
handle_tcp_close (int conn, WSClient *client, WSServer *server) {
LOG (("Closing TCP %d [%s]\n", client->listener, client->remote_ip));
#ifdef HAVE_LIBSSL
if (client->ssl)
shutdown_ssl (client);
#endif
shutdown (conn, SHUT_RDWR);
/* upon close, call onclose() callback */
if (server->onclose && wsconfig.strict && !wsconfig.echomode)
(*server->onclose) (server->pipeout, client);
/* do access logging */
gettimeofday (&client->end_proc, NULL);
if (wsconfig.accesslog)
access_log (client, 200);
/* errored out while parsing a frame or a message */
if (client->status & WS_ERR) {
ws_clear_queue (client);
ws_free_frame (client);
ws_free_message (client);
}
server->closing = 0;
ws_close (conn);
#ifdef HAVE_LIBSSL
if (client->ssl)
SSL_free (client->ssl);
client->ssl = NULL;
#endif
/* remove client from our list */
ws_remove_client_from_list (client, server);
LOG (("Connection Closed.\nActive: %d\n", list_count (server->colist)));
}
/* Close every connected client whose JWT has expired. */
static void
ws_close_expired_clients (WSServer *server) {
WSClient *client = NULL;
void *data = NULL;
int *listeners = NULL;
int count = 0, idx = 0, listener = 0;
if (wsconfig.auth_secret == NULL)
return;
if (server->colist == NULL)
return;
/* *INDENT-OFF* */
GSLIST_FOREACH (server->colist, data, {
client = data;
if (ws_reject_expired_client (client)) {
if (listeners == NULL) {
count = list_count (server->colist);
listeners = xcalloc (count, sizeof (*listeners));
}
listeners[idx++] = client->listener;
}
});
/* *INDENT-ON* */
while (idx > 0) {
listener = listeners[--idx];
client = ws_get_client_from_list (listener, &server->colist);
if (client != NULL)
handle_tcp_close (listener, client, server);
}
free (listeners);
}
/* Handle a tcp read close connection. */
static void
handle_read_close (int *conn, WSClient *client, WSServer *server) {
/* We can still try to send a message to the client if not forcing close, (nice
* goodbye), else proceed to close it */
if (!(client->status & WS_CLOSE) && client->status & WS_SENDING) {
server->closing = 1;
set_pollfd (client->listener, POLLOUT);
return;
}
handle_tcp_close (*conn, client, server);
}
/* Handle a new socket connection. */
static void
handle_accept (int listener, WSServer *server) {
WSClient *client = NULL;
int newfd;
newfd = accept_client (listener, &server->colist);
if (newfd == -1)
return;
if (!(client = ws_get_client_from_list (newfd, &server->colist)))
return;
#ifdef HAVE_LIBSSL
/* set flag to do TLS handshake */
if (wsconfig.use_ssl)
client->sslstatus |= WS_TLS_ACCEPTING;
#endif
LOG (("Accepted: %d [%s]\n", newfd, client->remote_ip));
}
/* Handle a tcp read. */
static void
handle_reads (int *conn, WSServer *server) {
WSClient *client = NULL;
if (!(client = ws_get_client_from_list (*conn, &server->colist)))
return;
LOG (("Handling read %d [%s]...\n", client->listener, client->remote_ip));
if (ws_reject_expired_client (client)) {
handle_read_close (conn, client, server);
*conn = -1;
return;
}
#ifdef HAVE_LIBSSL
if (handle_ssl_pending_rw (conn, server, client) == 0)
return;
#endif
/* *INDENT-OFF* */
client->start_proc = client->end_proc = (struct timeval) {0};
/* *INDENT-ON* */
gettimeofday (&client->start_proc, NULL);
read_client_data (client, server);
/* An error occurred while reading data or connection closed */
if ((client->status & WS_CLOSE)) {
handle_read_close (conn, client, server);
*conn = -1;
}
}
/* Handle a tcp write close connection. */
static void
handle_write_close (int conn, WSClient *client, WSServer *server) {
handle_tcp_close (conn, client, server);
}
/* Handle a tcp write. */
static void
handle_writes (int *conn, WSServer *server) {
WSClient *client = NULL;
if (!(client = ws_get_client_from_list (*conn, &server->colist)))
return;
if (ws_reject_expired_client (client)) {
handle_write_close (*conn, client, server);
*conn = -1;
return;
}
#ifdef HAVE_LIBSSL
if (handle_ssl_pending_rw (conn, server, client) == 0)
return;
#endif
ws_respond (client, NULL, 0); /* buffered data */
/* done sending data */
if (client->sockqueue == NULL) {
client->status &= ~WS_SENDING;
set_pollfd (client->listener, server->closing ? 0 : POLLIN);
}
/* An error occurred while sending data or while reading data but still
* waiting from the last send() from the server to the client. e.g.,
* sending status code */
if ((client->status & WS_CLOSE) && !(client->status & WS_SENDING))
handle_write_close (*conn, client, server);
}
/* Create an owner-only named pipe (FIFO) with the given pipe name, reusing an
* existing FIFO only when it belongs to the current user.
*
* On error, the process exits.
* On success, 0 is returned. */
int
ws_setfifo (const char *pipename) {
struct stat fistat;
const char *f = pipename;
if (lstat (f, &fistat) == 0) {
if (!S_ISFIFO (fistat.st_mode))
FATAL ("pipe is not a fifo: %s.", f);
if (fistat.st_uid != geteuid ())
FATAL ("fifo is not owned by the current user: %s.", f);
if (fistat.st_mode & (S_IRWXG | S_IRWXO))
FATAL ("fifo is accessible by other users: %s. Remove it before restarting.", f);
return 0;
}
if (mkfifo (f, S_IRUSR | S_IWUSR) < 0)
FATAL ("Unable to set fifo: %s.", strerror (errno));
if (lstat (f, &fistat) < 0)
FATAL ("Unable to stat fifo: %s.", strerror (errno));
if (!S_ISFIFO (fistat.st_mode))
FATAL ("pipe is not a fifo: %s.", f);
return 0;
}
/* Open a named pipe (FIFO) for input to the server (reader). */
static void
ws_openfifo_in (WSPipeIn *pipein) {
ws_setfifo (wsconfig.pipein);
/* we should be able to open it at as reader */
if ((pipein->fd = open (wsconfig.pipein, O_RDWR | O_NONBLOCK)) < 0)
FATAL ("Unable to open fifo in: %s.", strerror (errno));
}
/* Open a named pipe (FIFO) for output from the server (writer). */
static int
ws_openfifo_out (WSPipeOut *pipeout) {
int status = 0;
ws_setfifo (wsconfig.pipeout);
status = open (wsconfig.pipeout, O_WRONLY | O_NONBLOCK);
/* will attempt on the next write */
if (status == -1 && errno == ENXIO)
LOG (("Unable to open fifo out: %s.\n", strerror (errno)));
else if (status < 0)
FATAL ("Unable to open fifo out: %s.", strerror (errno));
pipeout->fd = status;
return status;
}
/* Set a new named pipe for incoming messages and one for outgoing
* messages from the client. */
static void
ws_fifo (WSServer *server) {
ws_openfifo_in (server->pipein);
set_pollfd (server->pipein->fd, POLLIN);
ws_openfifo_out (server->pipeout);
set_pollfd (server->pipeout->fd, POLLOUT);
}
/* Clear the queue for an outgoing named pipe. */
static void
clear_fifo_queue (WSPipeOut *pipeout) {
WSQueue **queue = &pipeout->fifoqueue;
if (!(*queue))
return;
if ((*queue)->queued)
free ((*queue)->queued);
(*queue)->queued = NULL;
(*queue)->qlen = 0;
free ((*queue));
(*queue) = NULL;
}
/* Attempt to realloc the current sent queue for an outgoing named pip
* (FIFO).
*
* On error, 1 is returned and the connection status is closed and
* reopened.
* On success, 0 is returned. */
static int
ws_realloc_fifobuf (WSPipeOut *pipeout, const char *buf, int len) {
WSQueue *queue = pipeout->fifoqueue;
char *tmp = NULL;
int newlen = 0;
newlen = queue->qlen + len;
tmp = realloc (queue->queued, newlen);
if (tmp == NULL && newlen > 0) {
ws_close (pipeout->fd);
clear_fifo_queue (pipeout);
ws_openfifo_out (pipeout);
return 1;
}
queue->queued = tmp;
memcpy (queue->queued + queue->qlen, buf, len);
queue->qlen += len;
return 0;
}
/* Set into a queue the data that couldn't be sent in the outgoing
* FIFO. */
static void
ws_queue_fifobuf (WSPipeOut *pipeout, const char *buffer, int len, int bytes) {
WSQueue **queue = &pipeout->fifoqueue;
if (bytes < 1)
bytes = 0;
(*queue) = xcalloc (1, sizeof (WSQueue));
(*queue)->queued = xcalloc (len - bytes, sizeof (char));
memcpy ((*queue)->queued, buffer + bytes, len - bytes);
(*queue)->qlen = len - bytes;
pipeout->status |= WS_SENDING;
set_pollfd (pipeout->fd, POLLOUT);
}
/* Attempt to send the given buffer to the given outgoing FIFO.
*
* On error, the data is queued up.
* On success, the number of bytes sent is returned. */
static int
ws_write_fifo_data (WSPipeOut *pipeout, char *buffer, int len) {
int bytes = 0;
bytes = write (pipeout->fd, buffer, len);
/* At this point, the reader probably closed the pipe, so a cheap *hack* for
* this is to close the pipe on our end and attempt to reopen it. If unable to
* do so, then let it be -1 and try on the next attempt to write. */
if (bytes == -1 && errno == EPIPE) {
ws_close (pipeout->fd);
ws_openfifo_out (pipeout);
return bytes;
}
if (bytes < len || (bytes == -1 && (errno == EAGAIN || errno == EWOULDBLOCK)))
ws_queue_fifobuf (pipeout, buffer, len, bytes);
return bytes;
}
/* Attempt to send the queued up client's data through the outgoing
* named pipe (FIFO) .
*
* On error, 1 is returned and the connection status is set.
* On success, the number of bytes sent is returned. */
static int
ws_write_fifo_cache (WSPipeOut *pipeout) {
WSQueue *queue = pipeout->fifoqueue;
int bytes = 0;
bytes = write (pipeout->fd, queue->queued, queue->qlen);
/* At this point, the reader probably closed the pipe, so a cheap *hack* for
* this is to close the pipe on our end and attempt to reopen it. If unable to
* do so, then let it be -1 and try on the next attempt to write. */
if (bytes == -1 && errno == EPIPE) {
ws_close (pipeout->fd);
ws_openfifo_out (pipeout);
return bytes;
}
if (chop_nchars (queue->queued, bytes, queue->qlen) == 0)
clear_fifo_queue (pipeout);
else
queue->qlen -= bytes;
return bytes;
}
/* An entry point to attempt to send the client's data into an
* outgoing named pipe (FIFO).
*
* On success, the number of bytes sent is returned. */
int
ws_write_fifo (WSPipeOut *pipeout, char *buffer, int len) {
int bytes = 0;
if (pipeout->fd == -1 && ws_openfifo_out (pipeout) == -1)
return bytes;
/* attempt to send the whole buffer */
if (pipeout->fifoqueue == NULL)
bytes = ws_write_fifo_data (pipeout, buffer, len);
/* buffer not empty, just append new data */
else if (pipeout->fifoqueue != NULL && buffer != NULL) {
if (ws_realloc_fifobuf (pipeout, buffer, len) == 1)
return bytes;
}
/* send from cache buffer */
else {
bytes = ws_write_fifo_cache (pipeout);
}
if (pipeout->fifoqueue == NULL) {
pipeout->status &= ~WS_SENDING;
set_pollfd (pipeout->fd, 0);
}
return bytes;
}
/* Clear an incoming FIFO packet and header data. */
static void
clear_fifo_packet (WSPipeIn *pipein) {
memset (pipein->hdr, 0, sizeof (pipein->hdr));
pipein->hlen = 0;
if (pipein->packet == NULL)
return;
if (pipein->packet->data)
free (pipein->packet->data);
free (pipein->packet);
pipein->packet = NULL;
}
/* Broadcast to all connected clients the given message. */
static int
ws_broadcast_fifo (WSClient *client, WSServer *server) {
WSPacket *packet = server->pipein->packet;
LOG (("Broadcasting to %d [%s] ", client->listener, client->remote_ip));
if (client == NULL)
return 1;
if (client->headers == NULL || client->headers->ws_accept == NULL) {
/* no handshake for this client */
LOG (("No headers. Closing %d [%s]\n", client->listener, client->remote_ip));
return -1;
}
LOG ((" - Sending...\n"));
ws_send_data (client, packet->type, packet->data, packet->size);
return 0;
}
static void
ws_broadcast_fifo_to_clients (WSServer *server) {
WSClient *client = NULL;
void *data = NULL;
uint32_t *close_list = NULL;
int n = 0, idx = 0, i = 0, listener = 0;
if ((n = list_count (server->colist)) == 0)
return;
close_list = xcalloc (n, sizeof (uint32_t));
/* *INDENT-OFF* */
GSLIST_FOREACH (server->colist, data, {
client = data;
if (ws_broadcast_fifo(client, server) == -1)
close_list[idx++] = client->listener;
});
/* *INDENT-ON* */
client = NULL;
for (i = 0; i < idx; ++i) {
listener = close_list[i];
if ((client = ws_get_client_from_list (listener, &server->colist)))
handle_tcp_close (listener, client, server);
}
free (close_list);
}
/* Send a message from the incoming named pipe to specific client
* given the socket id. */
static void
ws_send_strict_fifo_to_client (WSServer *server, int listener, WSPacket *pa) {
WSClient *client = NULL;
if (!(client = ws_get_client_from_list (listener, &server->colist)))
return;
/* no handshake for this client */
if (client->headers == NULL || client->headers->ws_accept == NULL) {
LOG (("No headers. Closing %d [%s]\n", client->listener, client->remote_ip));
handle_tcp_close (client->listener, client, server);
return;
}
ws_send_data (client, pa->type, pa->data, pa->len);
}
/* Attempt to read message from a named pipe (FIFO).
*
* On error, -1 is returned.
* On success, the number of bytes read is returned. */
int
ws_read_fifo (int fd, char *buf, int *buflen, int pos, int need) {
int bytes = 0;
bytes = read (fd, buf + pos, need);
if (bytes == -1 && (errno == EAGAIN || errno == EWOULDBLOCK))
return bytes;
else if (bytes == -1)
return bytes;
*buflen += bytes;
return bytes;
}
/* Pack the given value into a network byte order.
*
* On success, the number size of uint32_t is returned. */
size_t
pack_uint32 (void *buf, uint32_t val) {
uint32_t v32 = htonl (val);
memcpy (buf, &v32, sizeof (uint32_t));
return sizeof (uint32_t);
}
/* Unpack the given value into a host byte order.
*
* On success, the number size of uint32_t is returned. */
size_t
unpack_uint32 (const void *buf, uint32_t *val) {
uint32_t v32 = 0;
memcpy (&v32, buf, sizeof (uint32_t));
*val = ntohl (v32);
return sizeof (uint32_t);
}
/* Ensure the fields coming from the named pipe are valid.
*
* On error, 1 is returned.
* On success, 0 is returned. */
static int
validate_fifo_packet (uint32_t type, int size) {
if (type != WS_OPCODE_TEXT && type != WS_OPCODE_BIN) {
LOG (("Invalid fifo packet type\n"));
return 1;
}
if (size > wsconfig.max_frm_size) {
LOG (("Invalid fifo packet size\n"));
return 1;
}
return 0;
}
/* Handle reading and sending the incoming data from the named pipe on
* strict mode. */
static void
handle_strict_fifo (WSServer *server) {
WSPipeIn *pi = server->pipein;
WSPacket **pa = &pi->packet;
int bytes = 0, readh = 0, need = 0;
char *ptr = NULL;
uint32_t listener = 0, type = 0, size = 0;
readh = pi->hlen; /* read from header so far */
need = HDR_SIZE - readh; /* need to read */
if (need > 0) {
if ((bytes = ws_read_fifo (pi->fd, pi->hdr, &pi->hlen, readh, need)) < 0)
return;
if (bytes != need)
return;
}
/* unpack size, and type */
ptr = pi->hdr;
ptr += unpack_uint32 (ptr, &listener);
ptr += unpack_uint32 (ptr, &type);
ptr += unpack_uint32 (ptr, &size);
if (validate_fifo_packet (type, size) == 1) {
ws_close (pi->fd);
clear_fifo_packet (pi);
ws_openfifo_in (pi);
return;
}
if ((*pa) == NULL) {
(*pa) = xcalloc (1, sizeof (WSPacket));
(*pa)->type = type;
(*pa)->size = size;
(*pa)->data = xcalloc (size, sizeof (char));
}
readh = (*pa)->len; /* read from payload so far */
need = (*pa)->size - readh; /* need to read */
if (need > 0) {
if ((bytes = ws_read_fifo (pi->fd, (*pa)->data, &(*pa)->len, readh, need)) < 0)
return;
if (bytes != need)
return;
}
/* no clients to send data to */
if (list_count (server->colist) == 0) {
clear_fifo_packet (pi);
return;
}
/* Either send it to a specific client or broadcast message to all
* clients */
if (listener != 0)
ws_send_strict_fifo_to_client (server, listener, *pa);
else
ws_broadcast_fifo_to_clients (server);
clear_fifo_packet (pi);
}
/* Handle reading and sending the incoming data from the named pipe on
* a fixed buffer mode. */
static void
handle_fixed_fifo (WSServer *server) {
WSPipeIn *pi = server->pipein;
WSPacket **pa = &pi->packet;
int bytes = 0;
char buf[PIPE_BUF] = { 0 };
if ((bytes = read (pi->fd, buf, PIPE_BUF - 1)) < 0)
return;
buf[bytes] = '\0'; /* null-terminate */
if (ws_validate_string (buf, bytes) != 0)
return;
(*pa) = xcalloc (1, sizeof (WSPacket));
(*pa)->type = WS_OPCODE_TEXT;
(*pa)->size = bytes;
(*pa)->data = xstrdup (buf);
/* no clients to send data to */
if (list_count (server->colist) == 0) {
clear_fifo_packet (pi);
return;
}
/* broadcast message to all clients */
ws_broadcast_fifo_to_clients (server);
clear_fifo_packet (pi);
}
/* Determine which mode should use the incoming message from the FIFO. */
static void
handle_fifo (WSServer *server) {
if (wsconfig.strict)
handle_strict_fifo (server);
else
handle_fixed_fifo (server);
}
/* Creates an endpoint for communication and start listening for
* connections on a socket */
static void
ws_socket (int *listener) {
if (wsconfig.unix_socket) {
struct sockaddr_un servaddr;
/* Create a TCP socket. */
if ((*listener = socket (AF_UNIX, SOCK_STREAM, 0)) == -1)
FATAL ("Unable to open socket: %s.", strerror (errno));
memset (&servaddr, 0, sizeof (servaddr));
servaddr.sun_family = AF_UNIX;
strncpy (servaddr.sun_path, wsconfig.unix_socket, sizeof (servaddr.sun_path) - 1);
/* Bind the socket to the address. */
if (bind (*listener, (struct sockaddr *) &servaddr, sizeof (servaddr)) != 0)
FATAL ("Unable to set bind: %s.", strerror (errno));
} else {
int ov = 1, bind_result = 0;
struct addrinfo hints, *ai, *p;
memset (&hints, 0, sizeof (hints));
hints.ai_family = AF_UNSPEC;
hints.ai_socktype = SOCK_STREAM;
if (getaddrinfo (wsconfig.host, wsconfig.port, &hints, &ai) != 0)
FATAL ("Unable to set server: %s.", gai_strerror (errno));
for (p = ai; p != NULL; p = p->ai_next) {
*listener = socket (p->ai_family, p->ai_socktype, p->ai_protocol);
if (*listener == -1)
continue;
if (setsockopt (*listener, SOL_SOCKET, SO_REUSEADDR, &ov, sizeof (ov)) == -1) {
close (*listener);
continue;
}
if (bind (*listener, p->ai_addr, p->ai_addrlen) == 0) {
bind_result = 1;
break;
}
close (*listener);
}
freeaddrinfo (ai);
if (bind_result == 0)
FATAL ("Unable to set bind: %s.", strerror (errno));
}
if (listen (*listener, SOMAXCONN) == -1)
FATAL ("Unable to listen: %s.", strerror (errno));
}
/* Start the websocket server and start to monitor multiple file
* descriptors until we have something to read or write. */
void
ws_start (WSServer *server) {
int listener = -1, poll_timeout = -1, ret = 0;
struct pollfd *cfdstate = NULL, *pfd, *efd;
nfds_t ncfdstate = 0;
bool run = true;
if (server->self_pipe[0] != -1)
set_pollfd (server->self_pipe[0], POLLIN);
#ifdef HAVE_LIBSSL
if (wsconfig.sslcert && wsconfig.sslkey) {
LOG (("==Using TLS/SSL==\n"));
wsconfig.use_ssl = 1;
if (initialize_ssl_ctx (server)) {
LOG (("Unable to initialize_ssl_ctx\n"));
return;
}
}
#endif
ws_socket (&listener);
set_pollfd (listener, POLLIN);
while (run) {
/* take a copy of the fdstate and give that to poll to allow
* any dispatch to modify the real fdstate for the next pass */
if (nfdstate > 0) {
if (ncfdstate != nfdstate) {
free (cfdstate);
cfdstate = xmalloc (nfdstate * sizeof (*cfdstate));
memset (cfdstate, 0, sizeof (*cfdstate) * nfdstate);
ncfdstate = nfdstate;
}
memcpy (cfdstate, fdstate, ncfdstate * sizeof (*cfdstate));
}
poll_timeout = ws_get_auth_poll_timeout (server);
if ((ret = poll (cfdstate, nfdstate, poll_timeout)) == -1) {
switch (errno) {
case EINTR:
LOG (("A signal was caught on poll(2)\n"));
break;
default:
FATAL ("Unable to poll: %s.", strerror (errno));
}
}
/* iterate over existing connections */
efd = cfdstate + nfdstate;
for (pfd = cfdstate; pfd < efd; pfd++) {
if (pfd->revents & POLLHUP)
LOG (("Got POLLHUP %d\n", pfd->fd));
if (pfd->revents & POLLNVAL)
LOG (("Got POLLNVAL %d\n", pfd->fd));
if (pfd->revents & POLLERR)
LOG (("Got POLLERR %d\n", pfd->fd));
/* handle self-pipe trick */
if (pfd->fd == server->self_pipe[0]) {
if (pfd->revents & POLLIN) {
LOG (("Handled self-pipe to close event loop.\n"));
run = false;
break;
}
} else if (pfd->fd == server->pipein->fd) {
/* handle pipein */
if (pfd->revents & POLLIN)
handle_fifo (server);
} else if (pfd->fd == server->pipeout->fd) {
/* handle pipeout */
if (pfd->revents & POLLOUT)
ws_write_fifo (server->pipeout, NULL, 0);
} else if (pfd->fd == listener) {
/* handle new connections */
if (pfd->revents & POLLIN)
handle_accept (listener, server);
} else {
/* handle data from a client */
if (pfd->revents & POLLIN) {
if (server->closing) {
struct pollfd *ffd = get_pollfd (pfd->fd);
if (ffd != NULL)
ffd->events &= ~POLLIN;
} else
handle_reads (&pfd->fd, server);
}
/* handle sending data to a client */
if (pfd->revents & POLLOUT)
handle_writes (&pfd->fd, server);
}
}
ws_close_expired_clients (server);
}
free (cfdstate);
ws_close (listener);
if (server->self_pipe[0] != -1)
unset_pollfd (server->self_pipe[0]);
if (wsconfig.unix_socket) {
unlink (wsconfig.unix_socket);
}
}
/* Set the origin so the server can force connections to have the
* given HTTP origin. */
void
ws_set_config_origin (const char *origin) {
wsconfig.origin = origin;
}
/* Set the maximum websocket frame size. */
void
ws_set_config_frame_size (int max_frm_size) {
wsconfig.max_frm_size = max_frm_size;
}
/* Set specific name for the reader named pipe. */
void
ws_set_config_pipein (const char *pipein) {
wsconfig.pipein = pipein;
}
/* Set specific name for the writer named pipe. */
void
ws_set_config_pipeout (const char *pipeout) {
wsconfig.pipeout = pipeout;
}
/* Set a path and a file for the access log. */
void
ws_set_config_accesslog (const char *accesslog) {
wsconfig.accesslog = accesslog;
if (access_log_open (wsconfig.accesslog) == 1)
FATAL ("Unable to open access log: %s.", strerror (errno));
}
/* Set if the server should handle strict named pipe handling. */
void
ws_set_config_strict (int strict) {
wsconfig.strict = strict;
}
/* Set the server into echo mode. */
void
ws_set_config_echomode (int echomode) {
wsconfig.echomode = echomode;
}
/* Set the server host bind address. */
void
ws_set_config_host (const char *host) {
wsconfig.host = host;
}
/* Set the server unix socket bind address. */
void
ws_set_config_unix_socket (const char *unix_socket) {
wsconfig.unix_socket = unix_socket;
}
/* Set the server port bind address. */
void
ws_set_config_port (const char *port) {
wsconfig.port = port;
}
/* Set specific name for the SSL certificate. */
void
ws_set_config_sslcert (const char *sslcert) {
wsconfig.sslcert = sslcert;
}
/* Set specific name for the SSL key. */
void
ws_set_config_sslkey (const char *sslkey) {
wsconfig.sslkey = sslkey;
}
/* Set auth secret for auth. */
void
ws_set_config_auth_secret (const char *auth_secret) {
wsconfig.auth_secret = auth_secret;
}
/* Set the callback used to authenticate JWTs and obtain their expiration. */
void
ws_set_config_auth_cb (WSAuthCallback auth_cb) {
wsconfig.auth = auth_cb;
}
/* Create a new websocket server context. */
WSServer *
ws_init (const char *host, const char *port, void (*initopts) (void)) {
WSServer *server = new_wsserver ();
server->pipein = new_wspipein ();
server->pipeout = new_wspipeout ();
server->self_pipe[0] = server->self_pipe[1] = -1;
wsconfig.accesslog = NULL;
wsconfig.host = host;
wsconfig.unix_socket = NULL;
wsconfig.max_frm_size = WS_MAX_FRM_SZ;
wsconfig.origin = NULL;
wsconfig.pipein = NULL;
wsconfig.pipeout = NULL;
wsconfig.sslcert = NULL;
wsconfig.sslkey = NULL;
wsconfig.port = port;
wsconfig.strict = 0;
wsconfig.use_ssl = 0;
initopts ();
ws_fifo (server);
return server;
}