/** * websocket.c -- An rfc6455-complaint Web Socket Server * _______ _______ __ __ * / ____/ | / / ___/____ _____/ /_____ / /_ * / / __ | | /| / /\__ \/ __ \/ ___/ //_/ _ \/ __/ * / /_/ / | |/ |/ /___/ / /_/ / /__/ ,< / __/ /_ * \____/ |__/|__//____/\____/\___/_/|_|\___/\__/ * * The MIT License (MIT) * Copyright (c) 2009-2026 Gerardo Orellana * * 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 #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #if HAVE_CONFIG_H #include #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 * 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; }