blob: f01ada4350a648d018d51eca94c274e17579fac0 [file] [log] [blame]
/*
This file is part of libmicrohttpd
Copyright (C) 2021 David Gausmann (and other contributing authors)
This library is free software; you can redistribute it and/or
modify it under the terms of the GNU Lesser General Public
License as published by the Free Software Foundation; either
version 2.1 of the License, or (at your option) any later version.
This library is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
Lesser General Public License for more details.
You should have received a copy of the GNU Lesser General Public
License along with this library; if not, write to the Free Software
Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
*/
/**
* @file websocket_chatserver_example.c
* @brief example for how to use websockets
* @author David Gausmann
*
* Access the HTTP server with your webbrowser.
* The webbrowser must support JavaScript and WebSockets.
* The websocket access will be initiated via the JavaScript on the website.
* You will get an example chat room, which uses websockets.
* For testing with multiple users, just start several instances of your webbrowser.
*
*/
#if defined(WIN32) || defined(_WIN32) || defined(__WIN32__) || defined(__NT__)
#define _CRT_SECURE_NO_WARNINGS
#endif
#include "platform.h"
#include <microhttpd.h>
#include <microhttpd_ws.h>
#if defined(WIN32) || defined(_WIN32) || defined(__WIN32__) || defined(__NT__)
/*
Workaround for Windows systems, because the NuGet version of pthreads is buggy.
This is a simple replacement. It doesn't offer all functions of pthread, but
has everything, what is required for this example.
See: https://github.com/coapp-packages/pthreads/issues/2
*/
#include "pthread_windows.h"
/*
On Windows we will use stricmp instead of strcasecmp (strcasecmp is undefined there).
*/
#define strcasecmp stricmp
#else
/*
On Unix systems we can use pthread.
*/
#include <pthread.h>
#endif
/*
* Specify with this constant whether or not to use HTTPS.
* 0 means HTTP, 1 means HTTPS.
* Please note that you must enter a valid private key/certificate pair
* in the main procedure to running this example with HTTPS.
*/
#define USE_HTTPS 0
/**
* This is the main website.
* The HTML, CSS and JavaScript code is all in there.
*/
#define PAGE \
"<!DOCTYPE html>" \
"<html>" \
"<head>" \
"<meta charset='UTF-8'>" \
"<title>libmicrohttpd websocket chatserver demo</title>" \
"<style>" \
" html" \
" {\n" \
" font: 11pt sans-serif;\n" \
" }\n" \
" html, body" \
" {\n" \
" margin: 0;\n" \
" width: 100vw;\n" \
" height: 100vh;\n" \
" }\n" \
" div#Chat\n" \
" {\n" \
" display: flex;\n" \
" flex-direction: row;\n" \
" }\n" \
" div#Chat > div.MessagesAndInput\n" \
" {\n" \
" flex: 1 1 auto;" \
" display: flex;\n" \
" flex-direction: column;\n" \
" width: calc(100vw - 20em);\n" \
" }\n" \
" div#Chat > div.MessagesAndInput > div#Messages\n" \
" {\n" \
" flex: 1 1 auto;" \
" display: flex;\n" \
" flex-direction: column;\n" \
" justify-content: flex-start;\n" \
" box-sizing: border-box;\n" \
" overflow-y: scroll;\n" \
" border: 2pt solid #888;\n" \
" background-color: #eee;\n" \
" height: calc(100vh - 2em);\n" \
" }\n" \
" div#Chat > div.MessagesAndInput > div#Messages > div.Message > span\n" \
" {\n" \
" white-space: pre\n" \
" }\n" \
" div#Chat > div.MessagesAndInput > div#Messages > div.Message.error > span\n" \
" {\n" \
" color: red;\n" \
" }\n" \
" div#Chat > div.MessagesAndInput > div#Messages > div.Message.system > span\n" \
" {\n" \
" color: green;\n" \
" }\n" \
" div#Chat > div.MessagesAndInput > div#Messages > div.Message.moderator > span\n" \
" {\n" \
" color: #808000;\n" \
" }\n" \
" div#Chat > div.MessagesAndInput > div#Messages > div.Message.private > span\n" \
" {\n" \
" color: blue;\n" \
" }\n" \
" div#Chat > div.MessagesAndInput > div.Input\n" \
" {\n" \
" flex: 0 0 auto;" \
" height: 2em;" \
" display: flex;" \
" flex-direction: row;" \
" background-color: #eee;\n" \
" }\n" \
" div#Chat > div.MessagesAndInput > div.Input > input#InputMessage\n" \
" {\n" \
" flex: 1 1 auto;" \
" }\n" \
" div#Chat > div.MessagesAndInput > div.Input > button\n" \
" {\n" \
" flex: 0 0 auto;" \
" width: 5em;" \
" margin-left: 4pt;" \
" }\n" \
" div#Chat > div#Users\n" \
" {\n" \
" flex: 0 0 auto;" \
" width: 20em;" \
" display: flex;\n" \
" flex-direction: column;\n" \
" justify-content: flex-start;\n" \
" box-sizing: border-box;\n" \
" overflow-y: scroll;\n" \
" border: 2pt solid #888;\n" \
" background-color: #eee;\n" \
" }\n" \
" div#Chat > div#Users > div\n" \
" {\n" \
" cursor: pointer;\n" \
" user-select: none;\n" \
" -webkit-user-select: none;\n" \
" }\n" \
" div#Chat > div#Users > div.selected\n" \
" {\n" \
" background-color: #7bf;\n" \
" }\n" \
"</style>" \
"<script>\n" \
" 'use strict'\n;" \
"\n" \
" let baseUrl;\n" \
" let socket;\n" \
" let connectedUsers = new Map();\n" \
"\n" \
" window.addEventListener('load', window_onload);\n" \
"\n" \
" /**\n" \
" This is the main procedure which initializes the chat and connects the first socket\n" \
" */\n" \
" function window_onload(event)\n" \
" {\n" \
" /* Determine the base url (for http:/" "/ this is ws:/" "/ for https:/" \
"/ this must be wss:/" "/) */\n" \
" baseUrl = 'ws' + (window.location.protocol === 'https:' ? 's' : '') + ':/" "/' + window.location.host + '/ChatServerWebSocket';\n" \
" chat_generate();\n" \
" chat_connect();\n" \
" }\n" \
"\n" \
" /**\n" \
" This function generates the chat using DOM\n" \
" */\n" \
" function chat_generate()\n" \
" {\n" \
" document.body.innerHTML = '';\n" \
" let chat = document.createElement('div');\n" \
" document.body.appendChild(chat);\n" \
" chat.id = 'Chat';\n" \
" let messagesAndInput = document.createElement('div');\n" \
" chat.appendChild(messagesAndInput);\n" \
" messagesAndInput.classList.add('MessagesAndInput');\n" \
" let messages = document.createElement('div');\n" \
" messagesAndInput.appendChild(messages);\n" \
" messages.id = 'Messages';\n" \
" let input = document.createElement('div');\n" \
" messagesAndInput.appendChild(input);\n" \
" input.classList.add('Input');\n" \
" let inputMessage = document.createElement('input');\n" \
" input.appendChild(inputMessage);\n" \
" inputMessage.type = 'text';\n" \
" inputMessage.id = 'InputMessage';\n" \
" inputMessage.disabled = true;\n" \
" inputMessage.addEventListener('keydown', chat_onKeyDown);\n" \
" let inputMessageSend = document.createElement('button');\n" \
" input.appendChild(inputMessageSend);\n" \
" inputMessageSend.id = 'InputMessageButton';\n" \
" inputMessageSend.disabled = true;\n" \
" inputMessageSend.innerText = 'send';\n" \
" inputMessageSend.addEventListener('click', chat_onSendClicked);\n" \
" let inputImage = document.createElement('input');\n" \
" input.appendChild(inputImage);\n" \
" inputImage.id = 'InputImage';\n" \
" inputImage.type = 'file';\n" \
" inputImage.accept = 'image /*';\n" \
" inputImage.style.display = 'none';\n" \
" inputImage.addEventListener('change', chat_onImageSelected);\n" \
" let inputImageButton = document.createElement('button');\n" \
" input.appendChild(inputImageButton);\n" \
" inputImageButton.id = 'InputImageButton';\n" \
" inputImageButton.disabled = true;\n" \
" inputImageButton.innerText = 'image';\n" \
" inputImageButton.addEventListener('click', chat_onImageClicked);\n" \
" let users = document.createElement('div');\n" \
" chat.appendChild(users);\n" \
" users.id = 'Users';\n" \
" users.addEventListener('click', chat_onUserClicked);\n" \
" let allUsers = document.createElement('div');\n" \
" users.appendChild(allUsers);\n" \
" allUsers.classList.add('selected');\n" \
" allUsers.innerText = '<everyone>';\n" \
" allUsers.setAttribute('data-user', '0');\n" \
" }\n" \
"\n" \
" /**\n" \
" This function creates and connects a WebSocket\n" \
" */\n" \
" function chat_connect()\n" \
" {\n" \
" chat_addMessage(`Connecting to libmicrohttpd chat server demo (${baseUrl})...`, { type: 'system' });\n" \
" socket = new WebSocket(baseUrl);\n" \
" socket.binaryType = 'arraybuffer';\n" \
" socket.onopen = socket_onopen;\n" \
" socket.onclose = socket_onclose;\n" \
" socket.onerror = socket_onerror;\n" \
" socket.onmessage = socket_onmessage;\n" \
" }\n" \
"\n" \
" /**\n" \
" This function adds new text to the chat list\n" \
" */\n" \
" function chat_addMessage(text, options)\n" \
" {\n" \
" let type = options && options.type || 'regular';\n" \
" if(!/^(?:regular|system|error|private|moderator)$/.test(type))\n" \
" type = 'regular';\n" \
" let message = document.createElement('div');\n" \
" message.classList.add('Message');\n" \
" message.classList.add(type);\n" \
" if(typeof(text) === 'string')\n" \
" {\n" \
" let content = document.createElement('span');\n" \
" message.appendChild(content);\n" \
" if(options && options.from)\n" \
" content.innerText = `${options.from}: ${text}`;\n" \
" else\n" \
" content.innerText = text;\n" \
" if(options && options.reconnect)\n" \
" {\n" \
" let span = document.createElement('span');\n" \
" span.appendChild(document.createTextNode(' ('));\n" \
" let reconnect = document.createElement('a');\n" \
" reconnect.href = 'javascript:chat_connect()';\n" \
" reconnect.innerText = 'reconnect';\n" \
" span.appendChild(reconnect);\n" \
" span.appendChild(document.createTextNode(')'));\n" \
" message.appendChild(span);\n" \
" }\n" \
" }\n" \
" else\n" \
" {\n" \
" let content = document.createElement('span');\n" \
" message.appendChild(content);\n" \
" if(options && options.from)\n" \
" {\n" \
" content.innerText = `${options.from}:\\n`;\n" \
" }\n" \
" if(options && options.pictureType && text instanceof Uint8Array)\n" \
" {\n" \
" let img = document.createElement('img');\n" \
" content.appendChild(img);\n" \
" img.src = URL.createObjectURL(new Blob([ text.buffer ], { type: options.pictureType }));\n" \
" }\n" \
" }\n" \
" document.getElementById('Messages').appendChild(message);\n" \
" message.scrollIntoView();\n" \
" }\n" \
"\n" \
" /**\n" \
" This is a keydown event handler, which allows that you can just press enter instead of clicking the 'send' button\n" \
" */\n" \
" function chat_onKeyDown(event)\n" \
" {\n" \
" if(event.key == 'Enter')\n" \
" chat_onSendClicked();\n" \
" }\n" \
"\n" \
" /**\n" \
" This is the code to send a message or command, when clicking the 'send' button\n" \
" */\n" \
" function chat_onSendClicked(event)\n" \
" {\n" \
" let message = document.getElementById('InputMessage').value;\n" \
" if(message.length == 0)\n" \
" return;\n" \
" if(message.substr(0, 1) == '/')\n" \
" {\n" \
" /* command */ \n" \
" let match;\n" \
" if(/^\\/disconnect\\s*$/.test(message))\n" \
" {\n" \
" socket.close(1000);\n" \
" }\n" \
" else if((match = /^\\/m\\s+(\\S+)\\s+/.exec(message)))\n" \
" {\n" \
" message = message.substr(match[0].length);\n" \
" let userId = chat_getUserIdByName(match[1]);\n" \
" if(userId !== null)\n" \
" {\n" \
" socket.send(`private|${userId}|${message}`);\n" \
" }\n" \
" else\n" \
" {\n" \
" chat_addMessage(`Unknown user \"${match[1]}\" for private message: ${message}`, { type: 'error' });\n" \
" }\n" \
" }\n" \
" else if((match = /^\\/ping\\s+(\\S+)\\s*$/.exec(message)))\n" \
" {\n" \
" let userId = chat_getUserIdByName(match[1]);\n" \
" if(userId !== null)\n" \
" {\n" \
" socket.send(`ping|${userId}|`);\n" \
" }\n" \
" else\n" \
" {\n" \
" chat_addMessage(`Unknown user \"${match[1]}\" for ping`, { type: 'error' });\n" \
" }\n" \
" }\n" \
" else if((match = /^\\/name\\s+(\\S+)\\s*$/.exec(message)))\n" \
" {\n" \
" socket.send(`name||${match[1]}`);\n" \
" }\n" \
" else\n" \
" {\n" \
" chat_addMessage(`Unsupported command or invalid syntax: ${message}`, { type: 'error' });\n" \
" }\n" \
" }\n" \
" else\n" \
" {\n" \
" /* regular chat message to the selected user */ \n" \
" let selectedUser = document.querySelector('div#Users > div.selected');\n" \
" let selectedUserId = parseInt(selectedUser.getAttribute('data-user') || '0', 10);\n" \
" if(selectedUserId == 0)\n" \
" socket.send(`||${message}`);\n" \
" else\n" \
" socket.send(`private|${selectedUserId}|${message}`);\n" \
" }\n" \
" document.getElementById('InputMessage').value = '';\n" \
" }\n" \
"\n" \
" /**\n" \
" This is the event when the user hits the 'image' button\n" \
" */\n" \
" function chat_onImageClicked(event)\n" \
" {\n" \
" document.getElementById('InputImage').click();\n" \
" }\n" \
"\n" \
" /**\n" \
" This is the event when the user selected an image.\n" \
" The image will be read with the FileReader (allowed in web, because the user selected the file).\n" \
" */\n" \
" function chat_onImageSelected(event)\n" \
" {\n" \
" let file = event.target.files[0];\n" \
" if(!file || !/^image\\/" "/.test(file.type))\n" \
" return;\n" \
" let selectedUser = document.querySelector('div#Users > div.selected');\n" \
" let selectedUserId = parseInt(selectedUser.getAttribute('data-user') || '0', 10);\n" \
" let reader = new FileReader();\n" \
" reader.onload = function(event) {\n" \
" chat_onImageRead(event, file.type, selectedUserId);\n" \
" };\n" \
" reader.readAsArrayBuffer(file);\n" \
" }\n" \
"\n" \
" /**\n" \
" This is the event when the user selected image has been read.\n" \
" This will add our chat protocol prefix and send it via the websocket.\n" \
" */\n" \
" function chat_onImageRead(event, fileType, selectedUserId)\n" \
" {\n" \
" let encoder = new TextEncoder();\n" \
" let prefix = ((selectedUserId == 0 ? '||' : `private|${selectedUserId}|`) + fileType + '|');\n" \
" prefix = encoder.encode(prefix);\n" \
" let byteData = new Uint8Array(event.target.result);\n" \
" let totalLength = prefix.length + byteData.length;\n" \
" let resultByteData = new Uint8Array(totalLength);\n" \
" resultByteData.set(prefix, 0);\n" \
" resultByteData.set(byteData, prefix.length);\n" \
" socket.send(resultByteData);\n" \
" }\n" \
"\n" \
" /**\n" \
" This is the event when the user clicked a name in the user list.\n" \
" This is useful to send private messages or images without needing to add the /m prefix.\n" \
" */\n" \
" function chat_onUserClicked(event, selectedUserId)\n" \
" {\n" \
" let newSelected = event.target.closest('div#Users > div');\n" \
" if(newSelected === null)\n" \
" return;\n" \
" for(let div of this.querySelectorAll(':scope > div.selected'))\n" \
" div.classList.remove('selected');\n" \
" newSelected.classList.add('selected');\n" \
" }\n" \
"\n" \
" /**\n" \
" This functions returns the current id of a user identified by its name.\n" \
" */\n" \
" function chat_getUserIdByName(name)\n" \
" {\n" \
" let nameUpper = name.toUpperCase();\n" \
" for(let pair of connectedUsers)\n" \
" {\n" \
" if(pair[1].toUpperCase() == nameUpper)\n" \
" return pair[0];\n" \
" }\n" \
" return null;\n" \
" }\n" \
"\n" \
" /**\n" \
" This functions clears the entire user list (needed for reconnecting).\n" \
" */\n" \
" function chat_clearUserList()\n" \
" {\n" \
" let users = document.getElementById('Users');\n" \
" for(let div of users.querySelectorAll(':scope > div'))\n" \
" {\n" \
" if(div.getAttribute('data-user') === '0')\n" \
" {\n" \
" div.classList.add('selected');\n" \
" }\n" \
" else\n" \
" {\n" \
" div.parentNode.removeChild(div);\n" \
" }\n" \
" }\n" \
" return null;\n" \
" }\n" \
"\n" \
" /**\n" \
" This is the event when the socket has established a connection.\n" \
" This will initialize an empty chat and enable the controls.\n" \
" */\n" \
" function socket_onopen(event)\n" \
" {\n" \
" connectedUsers.clear();\n" \
" chat_clearUserList();\n" \
" chat_addMessage('Connected!', { type: 'system' });\n" \
" document.getElementById('InputMessage').disabled = false;\n" \
" document.getElementById('InputMessageButton').disabled = false;\n" \
" document.getElementById('InputImageButton').disabled = false;\n" \
" }\n" \
"\n" \
" /**\n" \
" This is the event when the socket has been closed.\n" \
" This will lock the controls.\n" \
" */\n" \
" function socket_onclose(event)\n" \
" {\n" \
" chat_addMessage('Connection closed!', { type: 'system', reconnect: true });\n" \
" document.getElementById('InputMessage').disabled = true;\n" \
" document.getElementById('InputMessageButton').disabled = true;\n" \
" document.getElementById('InputImageButton').disabled = true;\n" \
" }\n" \
"\n" \
" /**\n" \
" This is the event when the socket reported an error.\n" \
" This will just make an output.\n" \
" In the web browser console (F12 on many browsers) will show you more detailed error information.\n" \
" */\n" \
" function socket_onerror(event)\n" \
" {\n" \
" console.error('WebSocket error reported: ', event);\n" \
" chat_addMessage('The socket reported an error!', { type: 'error' });\n" \
" }\n" \
"\n" \
" /**\n" \
" This is the event when the socket has received a message.\n" \
" This will parse the message and execute the corresponding command (or add the message).\n" \
" */\n" \
" function socket_onmessage(event)\n" \
" {\n" \
" if(typeof(event.data) === 'string')\n" \
" {\n" \
" /* text message or command */ \n" \
" let message = event.data.split('|', 3);\n" \
" switch(message[0])\n" \
" {\n" \
" case 'userinit':\n" \
" connectedUsers.set(message[1], message[2]);\n" \
" {\n" \
" let users = document.getElementById('Users');\n" \
" let div = document.createElement('div');\n" \
" users.appendChild(div);\n" \
" div.innerText = message[2];\n" \
" div.setAttribute('data-user', message[1]);\n" \
" }\n" \
" break;\n" \
" case 'useradd':\n" \
" connectedUsers.set(message[1], message[2]);\n" \
" chat_addMessage(`The user '${message[2]}' has joined our lovely chatroom.`, { type: 'moderator' });\n" \
" {\n" \
" let users = document.getElementById('Users');\n" \
" let div = document.createElement('div');\n" \
" users.appendChild(div);\n" \
" div.innerText = message[2];\n" \
" div.setAttribute('data-user', message[1]);\n" \
" }\n" \
" break;\n" \
" case 'userdel':\n" \
" chat_addMessage(`The user '${connectedUsers.get(message[1])}' has left our chatroom. We will miss you.`, { type: 'moderator' });\n" \
" connectedUsers.delete(message[1]);\n" \
" {\n" \
" let users = document.getElementById('Users');\n" \
" let div = users.querySelector(`div[data-user='${message[1]}']`);\n" \
" if(div !== null)\n" \
" {\n" \
" users.removeChild(div);\n" \
" if(div.classList.contains('selected'))\n" \
" users.querySelector('div[data-user=\\'0\\']').classList.add('selected');\n" \
" }\n" \
" }\n" \
" break;\n" \
" case 'username':\n" \
" chat_addMessage(`The user '${connectedUsers.get(message[1])}' has changed his name to '${message[2]}'.`, { type: 'moderator' });\n" \
" connectedUsers.set(message[1], message[2]);\n" \
" {\n" \
" let users = document.getElementById('Users');\n" \
" let div = users.querySelector(`div[data-user='${message[1]}']`);\n" \
" if(div !== null)\n" \
" {\n" \
" div.innerText = message[2];\n" \
" }\n" \
" }\n" \
" break;\n" \
" case 'ping':\n" \
" chat_addMessage(`The user '${connectedUsers.get(message[1])}' has a ping of ${message[2]} ms.`, { type: 'moderator' });\n" \
" break;\n" \
" default:\n" \
" chat_addMessage(message[2], { type: message[0], from: connectedUsers.get(message[1]) });\n" \
" break;\n" \
" }\n" \
" }\n" \
" else\n" \
" {\n" \
" /* We received a binary frame, which means a picture here */ \n" \
" let byteData = new Uint8Array(event.data);\n" \
" let decoder = new TextDecoder();\n" \
" let message = [ ];\n" \
" /* message type */ \n" \
" let j = 0;\n" \
" let i = byteData.indexOf(0x7C, j); /* | = 0x7C;*/ \n"\
" if(i < 0)\n" \
" return;\n" \
" message.push(decoder.decode(byteData.slice(0, i)));\n" \
" /* picture from */ \n" \
" j = i + 1;\n" \
" i = byteData.indexOf(0x7C, j);\n" \
" if(i < 0)\n" \
" return;\n" \
" message.push(decoder.decode(byteData.slice(j, i)));\n" \
" /* picture encoding */ \n" \
" j = i + 1;\n" \
" i = byteData.indexOf(0x7C, j);\n" \
" if(i < 0)\n" \
" return;\n" \
" message.push(decoder.decode(byteData.slice(j, i)));\n" \
" /* picture */ \n" \
" byteData = byteData.slice(i + 1);\n" \
" chat_addMessage(byteData, { type: message[0], from: connectedUsers.get(message[1]), pictureType: message[2] });\n" \
" }\n" \
" }\n" \
"</script>" \
"</head>" \
"<body><noscript>Please enable JavaScript to test the libmicrohttpd Websocket chatserver demo!</noscript></body>" \
"</html>"
#define PAGE_NOT_FOUND \
"404 Not Found"
#define PAGE_INVALID_WEBSOCKET_REQUEST \
"Invalid WebSocket request!"
/**
* This struct is used to keep the data of a connected chat user.
* It is passed to the socket-receive thread (connecteduser_receive_messages) as well as to
* the socket-send thread (connecteduser_send_messages).
* It can also be accessed via the global array users (mutex protected).
*/
struct ConnectedUser
{
/* the TCP/IP socket for reading/writing */
MHD_socket fd;
/* the UpgradeResponseHandle of libmicrohttpd (needed for closing the socket) */
struct MHD_UpgradeResponseHandle *urh;
/* the websocket encode/decode stream */
struct MHD_WebSocketStream *ws;
/* the possibly read data at the start (only used once) */
char *extra_in;
size_t extra_in_size;
/* the unique user id (counting from 1, ids will never be re-used) */
size_t user_id;
/* the current user name */
char *user_name;
size_t user_name_len;
/* the zero-based index of the next message;
may be decremented when old messages are deleted */
size_t next_message_index;
/* specifies whether the websocket shall be closed (1) or not (0) */
int disconnect;
/* condition variable to wake up the sender of this connection */
pthread_cond_t wake_up_sender;
/* mutex to ensure that no send actions are mixed
(sending can be done by send and recv thread;
may not be simultaneously locked with chat_mutex by the same thread) */
pthread_mutex_t send_mutex;
/* specifies whether a ping shall be executed (1), is being executed (2) or
no ping is pending (0) */
int ping_status;
/* the start time of the ping, if a ping is running */
struct timespec ping_start;
/* the message used for the ping (must match the pong response)*/
char ping_message[128];
/* the length of the ping message (may not exceed 125) */
size_t ping_message_len;
/* the numeric ping message suffix to detect ping messages, which are too old */
int ping_counter;
};
/**
* A single message, which has been send via the chat.
* This can be text, an image or a command.
*/
struct Message
{
/* The user id of the sender. This is 0 if it is a system message- */
size_t from_user_id;
/* The user id of the recipient. This is 0 if every connected user shall receive it */
size_t to_user_id;
/* The data of the message. */
char *data;
size_t data_len;
/* Specifies whether the data is UTF-8 encoded text (0) or binary data (1) */
int is_binary;
};
/* the unique user counter for new users (only accessed by main thread) */
size_t unique_user_id = 0;
/* the chat data (users and messages; may be accessed by all threads, but is protected by mutex) */
pthread_mutex_t chat_mutex;
struct ConnectedUser **users = NULL;
size_t user_count = 0;
struct Message **messages = NULL;
size_t message_count = 0;
/* specifies whether all websockets must close (1) or not (0) */
volatile int disconnect_all = 0;
/* a counter for cleaning old messages (each 10 messages we will try to clean the list */
int clean_count = 0;
#define CLEANUP_LIMIT 10
/**
* Change socket to blocking.
*
* @param fd the socket to manipulate
*/
static void
make_blocking (MHD_socket fd)
{
#if defined(MHD_POSIX_SOCKETS)
int flags;
flags = fcntl (fd, F_GETFL);
if (-1 == flags)
abort ();
if ((flags & ~O_NONBLOCK) != flags)
if (-1 == fcntl (fd, F_SETFL, flags & ~O_NONBLOCK))
abort ();
#elif defined(MHD_WINSOCK_SOCKETS)
unsigned long flags = 0;
if (0 != ioctlsocket (fd, (int) FIONBIO, &flags))
abort ();
#endif /* MHD_WINSOCK_SOCKETS */
}
/**
* Sends all data of the given buffer via the TCP/IP socket
*
* @param fd The TCP/IP socket which is used for sending
* @param buf The buffer with the data to send
* @param len The length in bytes of the data in the buffer
*/
static void
send_all (struct ConnectedUser *cu,
const char *buf,
size_t len)
{
ssize_t ret;
size_t off;
if (0 == pthread_mutex_lock (&cu->send_mutex))
{
for (off = 0; off < len; off += ret)
{
ret = send (cu->fd,
&buf[off],
(int) (len - off),
0);
if (0 > ret)
{
if (EAGAIN == errno)
{
ret = 0;
continue;
}
break;
}
if (0 == ret)
break;
}
pthread_mutex_unlock (&cu->send_mutex);
}
}
/**
* Adds a new chat message to the list of messages.
*
* @param from_user_id the user id of the sender (0 means system)
* @param to_user_id the user id of the recipiend (0 means everyone)
* @param data the data to send (UTF-8 text or binary; will be copied)
* @param data_len the length of the data to send
* @param is_binary specifies whether the data is UTF-8 text (0) or binary (1)
* @param needs_lock specifies whether the caller has already locked the global chat mutex (0) or
* if this procedure needs to lock it (1)
*
* @return 0 on success, other values on error
*/
static int
chat_addmessage (size_t from_user_id,
size_t to_user_id,
char *data,
size_t data_len,
int is_binary,
int needs_lock)
{
/* allocate the buffer and fill it with data */
struct Message *message = (struct Message *) malloc (sizeof (struct Message));
if (NULL == message)
return 1;
memset (message, 0, sizeof (struct Message));
message->from_user_id = from_user_id;
message->to_user_id = to_user_id;
message->is_binary = is_binary;
message->data_len = data_len;
message->data = malloc (data_len + 1);
if (NULL == message->data)
{
free (message);
return 1;
}
memcpy (message->data, data, data_len);
message->data[data_len] = 0;
/* lock the global mutex if needed */
if (0 != needs_lock)
{
if (0 != pthread_mutex_lock (&chat_mutex))
return 1;
}
/* add the new message to the global message list */
size_t message_count_ = message_count + 1;
struct Message **messages_ = (struct Message **) realloc (messages,
message_count_
* sizeof (struct
Message *));
if (NULL == messages_)
{
free (message);
if (0 != needs_lock)
pthread_mutex_unlock (&chat_mutex);
return 1;
}
messages_[message_count] = message;
messages = messages_;
message_count = message_count_;
/* inform the sender threads about the new message */
for (size_t i = 0; i < user_count; ++i)
pthread_cond_signal (&users[i]->wake_up_sender);
/* unlock the global mutex if needed */
if (0 != needs_lock)
{
if (0 != needs_lock)
pthread_mutex_unlock (&chat_mutex);
}
return 0;
}
/**
* Cleans up old messages
*
* @param needs_lock specifies whether the caller has already locked the global chat mutex (0) or
* if this procedure needs to lock it (1)
* @return 0 on success, other values on error
*/
static int
chat_clearmessages (int needs_lock)
{
/* lock the global mutex if needed */
if (0 != needs_lock)
{
if (0 != pthread_mutex_lock (&chat_mutex))
return 1;
}
/* update the clean counter and check whether we need cleaning */
++clean_count;
if (CLEANUP_LIMIT > clean_count)
{
/* no cleanup required */
if (0 != needs_lock)
{
pthread_mutex_unlock (&chat_mutex);
}
return 0;
}
clean_count = 0;
/* check whether we got any messages (without them no cleaning is required */
if (0 < message_count)
{
/* then check whether we got any connected users */
if (0 < user_count)
{
/* determine the minimum index for the next message of all connected users */
size_t min_message = users[0]->next_message_index;
for (size_t i = 1; i < user_count; ++i)
{
if (min_message > users[i]->next_message_index)
min_message = users[i]->next_message_index;
}
if (0 < min_message)
{
/* remove all messages with index below min_message and update
the message indices of the users */
for (size_t i = 0; i < min_message; ++i)
{
free (messages[i]->data);
free (messages[i]);
}
for (size_t i = min_message; i < message_count; ++i)
messages[i - min_message] = messages[i];
message_count -= min_message;
for (size_t i = 0; i < user_count; ++i)
users[i]->next_message_index -= min_message;
}
}
else
{
/* without connected users, simply remove all messages */
for (size_t i = 0; i < message_count; ++i)
{
free (messages[i]->data);
free (messages[i]);
}
free (messages);
messages = NULL;
message_count = 0;
}
}
/* unlock the global mutex if needed */
if (0 != needs_lock)
{
pthread_mutex_unlock (&chat_mutex);
}
return 0;
}
/**
* Adds a new chat user to the global user list.
* This will be called at the start of connecteduser_receive_messages.
*
* @param cu The connected user
* @return 0 on success, other values on error
*/
static int
chat_adduser (struct ConnectedUser *cu)
{
/* initialize the notification message of the new user */
char user_index[32];
snprintf (user_index, 32, "%d", (int) cu->user_id);
size_t user_index_len = strlen (user_index);
size_t data_len = user_index_len + cu->user_name_len + 9;
char *data = (char *) malloc (data_len + 1);
if (NULL == data)
return 1;
strcpy (data, "useradd|");
strcat (data, user_index);
strcat (data, "|");
strcat (data, cu->user_name);
/* lock the mutex */
if (0 != pthread_mutex_lock (&chat_mutex))
{
free (data);
return 1;
}
/* inform the other chat users about the new user */
if (0 != chat_addmessage (0,
0,
data,
data_len,
0,
0))
{
free (data);
pthread_mutex_unlock (&chat_mutex);
return 1;
}
free (data);
/* add the new user to the list */
size_t user_count_ = user_count + 1;
struct ConnectedUser **users_ =
(struct ConnectedUser **) realloc (users, user_count_
* sizeof (struct ConnectedUser *));
if (NULL == users_)
{
/* realloc failed */
pthread_mutex_unlock (&chat_mutex);
return 1;
}
users_[user_count] = cu;
users = users_;
user_count = user_count_;
/* Initialize the next message index to the current message count. */
/* This will skip all old messages for this new connected user. */
cu->next_message_index = message_count;
/* unlock the mutex */
pthread_mutex_unlock (&chat_mutex);
return 0;
}
/**
* Removes a chat user from the global user list.
*
* @param cu The connected user
* @return 0 on success, other values on error
*/
static int
chat_removeuser (struct ConnectedUser *cu)
{
char user_index[32];
/* initialize the chat message for the removed user */
snprintf (user_index, 32, "%d", (int) cu->user_id);
size_t user_index_len = strlen (user_index);
size_t data_len = user_index_len + 9;
char *data = (char *) malloc (data_len + 1);
if (NULL == data)
return 1;
strcpy (data, "userdel|");
strcat (data, user_index);
strcat (data, "|");
/* lock the mutex */
if (0 != pthread_mutex_lock (&chat_mutex))
{
free (data);
return 1;
}
/* inform the other chat users that the user is gone */
int got_error = 0;
if (0 != chat_addmessage (0, 0, data, data_len, 0, 0))
{
free (data);
got_error = 1;
}
/* remove the user from the list */
int found = 0;
for (size_t i = 0; i < user_count; ++i)
{
if (cu == users[i])
{
found = 1;
for (size_t j = i + 1; j < user_count; ++j)
{
users[j - 1] = users[j];
}
--user_count;
break;
}
}
if (0 == found)
got_error = 1;
/* unlock the mutex */
pthread_mutex_unlock (&chat_mutex);
return got_error;
}
/**
* Renames a chat user
*
* @param cu The connected user
* @param new_name The new user name. On success this pointer will be taken.
* @param new_name_len The length of the new name
* @return 0 on success, other values on error. 2 means name already in use.
*/
static int
chat_renameuser (struct ConnectedUser *cu,
char *new_name,
size_t new_name_len)
{
/* lock the mutex */
if (0 != pthread_mutex_lock (&chat_mutex))
{
return 1;
}
/* check whether the name is already in use */
for (size_t i = 0; i < user_count; ++i)
{
if (cu != users[i])
{
if ((users[i]->user_name_len == new_name_len) &&
(0 == strcasecmp (users[i]->user_name, new_name)))
{
pthread_mutex_unlock (&chat_mutex);
return 2;
}
}
}
/* generate the notification message */
char user_index[32];
snprintf (user_index, 32, "%d", (int) cu->user_id);
size_t user_index_len = strlen (user_index);
size_t data_len = user_index_len + new_name_len + 10;
char *data = (char *) malloc (data_len + 1);
if (NULL == data)
return 1;
strcpy (data, "username|");
strcat (data, user_index);
strcat (data, "|");
strcat (data, new_name);
/* inform the other chat users about the new name */
if (0 != chat_addmessage (0, 0, data, data_len, 0, 0))
{
free (data);
pthread_mutex_unlock (&chat_mutex);
return 1;
}
free (data);
/* accept the new user name */
free (cu->user_name);
cu->user_name = new_name;
cu->user_name_len = new_name_len;
/* unlock the mutex */
pthread_mutex_unlock (&chat_mutex);
return 0;
}
/**
* Parses received data from the TCP/IP socket with the websocket stream
*
* @param cu The connected user
* @param new_name The new user name
* @param new_name_len The length of the new name
* @return 0 on success, other values on error
*/
static int
connecteduser_parse_received_websocket_stream (struct ConnectedUser *cu,
char *buf,
size_t buf_len)
{
size_t buf_offset = 0;
while (buf_offset < buf_len)
{
size_t new_offset = 0;
char *frame_data = NULL;
size_t frame_len = 0;
int status = MHD_websocket_decode (cu->ws,
buf + buf_offset,
buf_len - buf_offset,
&new_offset,
&frame_data,
&frame_len);
if (0 > status)
{
/* an error occurred and the connection must be closed */
if (NULL != frame_data)
{
/* depending on the WebSocket flag */
/* MHD_WEBSOCKET_FLAG_GENERATE_CLOSE_FRAMES_ON_ERROR */
/* close frames might be generated on errors */
send_all (cu,
frame_data,
frame_len);
MHD_websocket_free (cu->ws, frame_data);
}
return 1;
}
else
{
buf_offset += new_offset;
if (0 < status)
{
/* the frame is complete */
switch (status)
{
case MHD_WEBSOCKET_STATUS_TEXT_FRAME:
case MHD_WEBSOCKET_STATUS_BINARY_FRAME:
/**
* a text or binary frame has been received.
* in this chat server example we use a simple protocol where
* the JavaScript added a prefix like "<command>|<to_user_id>|data".
* Some examples:
* "||test" means a regular chat message to everyone with the message "test".
* "private|1|secret" means a private chat message to user with id 1 with the message "secret".
* "name||MyNewName" means that the user requests a rename to "MyNewName"
* "ping|1|" means that the user with id 1 shall get a ping
*
* Binary data is handled here like text data.
* The difference in the data is only checked by the JavaScript.
*/
{
size_t command = 1000;
size_t from_user_id = cu->user_id;
size_t to_user_id = 0;
size_t i;
/* parse the command */
for (i = 0; i < frame_len; ++i)
{
if ('|' == frame_data[i])
{
frame_data[i] = 0;
++i;
break;
}
}
if (0 < i)
{
if (i == 1)
{
/* no command means regular message */
command = 0;
}
else if (0 == strcasecmp (frame_data, "private"))
{
/* private means private message */
command = 1;
}
else if (0 == strcasecmp (frame_data, "name"))
{
/* name means chat user rename */
command = 2;
}
else if (0 == strcasecmp (frame_data, "ping"))
{
/* ping means a ping request */
command = 3;
}
else
{
/* no other commands supported, so this means invalid */
command = 1000;
}
}
/* parse the to_user_id, if given */
size_t j = i;
for (; j < frame_len; ++j)
{
if ('|' == frame_data[j])
{
frame_data[j] = 0;
++j;
break;
}
}
if (i + 1 < j)
{
to_user_id = (size_t) atoi (frame_data + i);
}
/* decide via the command what action to do */
if (frame_len >= j)
{
int is_binary = (MHD_WEBSOCKET_STATUS_BINARY_FRAME == status ? 1 :
0);
switch (command)
{
case 0:
/* regular chat message */
{
/**
* Generate the message for the message list.
* Regular chat messages get the command "regular".
* After that we add the from_user_id, followed by the content.
* The content must always be copied with memcpy instead of strcat,
* because the data (binary as well as UTF-8 encoded) is allowed
* to contain the NUL character.
* However we will add a terminating NUL character,
* which is not included in the data length
* (and thus will not be send to the recipients).
* This is useful for debugging with an IDE.
*/
char user_index[32];
snprintf (user_index, 32, "%d", (int) cu->user_id);
size_t user_index_len = strlen (user_index);
size_t data_len = user_index_len + frame_len - j + 9;
char *data = (char *) malloc (data_len + 1);
if (NULL != data)
{
strcpy (data, "regular|");
strcat (data, user_index);
strcat (data, "|");
size_t offset = strlen (data);
memcpy (data + offset,
frame_data + j,
frame_len - j);
data[data_len] = 0;
/* add the chat message to the global list */
chat_addmessage (from_user_id,
0,
data,
data_len,
is_binary,
1);
free (data);
}
}
break;
case 1:
/* private chat message */
if (0 != to_user_id)
{
/**
* Generate the message for the message list.
* This is similar to the code for regular messages above.
* The difference is the prefix "private"
*/
char user_index[32];
snprintf (user_index, 32, "%d", (int) cu->user_id);
size_t user_index_len = strlen (user_index);
size_t data_len = user_index_len + frame_len - j + 9;
char *data = (char *) malloc (data_len + 1);
if (NULL != data)
{
strcpy (data, "private|");
strcat (data, user_index);
strcat (data, "|");
size_t offset = strlen (data);
memcpy (data + offset,
frame_data + j,
frame_len - j);
data[data_len] = 0;
/* add the chat message to the global list */
chat_addmessage (from_user_id,
to_user_id,
data,
data_len,
is_binary,
1);
free (data);
}
}
break;
case 2:
/* rename */
{
/* check whether the new name is valid and allocate a new buffer for it */
size_t new_name_len = frame_len - j;
if (0 == new_name_len)
{
chat_addmessage (0,
from_user_id,
"error||Your new name is invalid. You haven't been renamed.",
58,
0,
1);
break;
}
char *new_name = (char *) malloc (new_name_len + 1);
if (NULL == new_name)
{
chat_addmessage (0,
from_user_id,
"error||Error while renaming. You haven't been renamed.",
54,
0,
1);
break;
}
new_name[new_name_len] = 0;
for (size_t k = 0; k < new_name_len; ++k)
{
char c = frame_data[j + k];
if ((32 >= c) || (c >= 127))
{
free (new_name);
new_name = NULL;
chat_addmessage (0,
from_user_id,
"error||Your new name contains invalid characters. You haven't been renamed.",
75,
0,
1);
break;
}
new_name[k] = c;
}
if (NULL == new_name)
break;
/* rename the user */
int rename_result = chat_renameuser (cu,
new_name,
new_name_len);
if (0 != rename_result)
{
/* the buffer will only be freed if no rename was possible */
free (new_name);
if (2 == rename_result)
{
chat_addmessage (0,
from_user_id,
"error||Your new name is already in use by another user. You haven't been renamed.",
81,
0,
1);
}
else
{
chat_addmessage (0,
from_user_id,
"error||Error while renaming. You haven't been renamed.",
54,
0,
1);
}
}
}
break;
case 3:
/* ping */
{
if (0 == pthread_mutex_lock (&chat_mutex))
{
/* check whether the to_user exists */
struct ConnectedUser *ping_user = NULL;
for (size_t k = 0; k < user_count; ++k)
{
if (users[k]->user_id == to_user_id)
{
ping_user = users[k];
break;
}
}
if (NULL == ping_user)
{
chat_addmessage (0,
from_user_id,
"error||Couldn't find the specified user for pinging.",
52,
0,
0);
}
else
{
/* if pinging is requested, */
/* we mark the user and inform the sender about this */
if (0 == ping_user->ping_status)
{
ping_user->ping_status = 1;
pthread_cond_signal (&ping_user->wake_up_sender);
}
}
pthread_mutex_unlock (&chat_mutex);
}
else
{
chat_addmessage (0,
from_user_id,
"error||Error while pinging.",
27,
0,
1);
}
}
break;
default:
/* invalid command */
chat_addmessage (0,
from_user_id,
"error||You sent an invalid command.",
35,
0,
1);
break;
}
}
}
MHD_websocket_free (cu->ws,
frame_data);
return 0;
case MHD_WEBSOCKET_STATUS_CLOSE_FRAME:
/* if we receive a close frame, we will respond with one */
MHD_websocket_free (cu->ws,
frame_data);
{
char *result = NULL;
size_t result_len = 0;
int er = MHD_websocket_encode_close (cu->ws,
MHD_WEBSOCKET_CLOSEREASON_REGULAR,
NULL,
0,
&result,
&result_len);
if (MHD_WEBSOCKET_STATUS_OK == er)
{
send_all (cu,
result,
result_len);
MHD_websocket_free (cu->ws, result);
}
}
return 1;
case MHD_WEBSOCKET_STATUS_PING_FRAME:
/* if we receive a ping frame, we will respond */
/* with the corresponding pong frame */
{
char *pong = NULL;
size_t pong_len = 0;
int er = MHD_websocket_encode_pong (cu->ws,
frame_data,
frame_len,
&pong,
&pong_len);
MHD_websocket_free (cu->ws,
frame_data);
if (MHD_WEBSOCKET_STATUS_OK == er)
{
send_all (cu,
pong,
pong_len);
MHD_websocket_free (cu->ws,
pong);
}
}
return 0;
case MHD_WEBSOCKET_STATUS_PONG_FRAME:
/* if we receive a pong frame, */
/* we will check whether we requested this frame and */
/* whether it is the last requested pong */
if (2 == cu->ping_status)
{
cu->ping_status = 0;
struct timespec now;
timespec_get (&now, TIME_UTC);
if ((cu->ping_message_len == frame_len) &&
(0 == strcmp (frame_data,
cu->ping_message)))
{
int ping = (int) (((int64_t) (now.tv_sec
- cu->ping_start.tv_sec)) * 1000
+ ((int64_t) (now.tv_nsec
- cu->ping_start.tv_nsec))
/ 1000000);
char result_text[240];
strcpy (result_text,
"ping|");
snprintf (result_text + 5, 235, "%d", (int) cu->user_id);
strcat (result_text,
"|");
snprintf (result_text + strlen (result_text), 240
- strlen (result_text), "%d", (int) ping);
chat_addmessage (0,
0,
result_text,
strlen (result_text),
0,
1);
}
}
MHD_websocket_free (cu->ws,
frame_data);
return 0;
default:
/* This case should really never happen, */
/* because there are only five types of (finished) websocket frames. */
/* If it is ever reached, it means that there is memory corruption. */
MHD_websocket_free (cu->ws,
frame_data);
return 1;
}
}
}
}
return 0;
}
/**
* Sends messages from the message list over the TCP/IP socket
* after encoding it with the websocket stream.
* This is also used for server-side actions,
* because the thread for receiving messages waits for
* incoming data and cannot be woken up.
* But the sender thread can be woken up easily.
*
* @param cls The connected user
* @return Always NULL
*/
static void *
connecteduser_send_messages (void *cls)
{
struct ConnectedUser *cu = cls;
/* the main loop of sending messages requires to lock the mutex */
if (0 == pthread_mutex_lock (&chat_mutex))
{
for (;;)
{
/* loop while not all messages processed */
int all_messages_read = 0;
while (0 == all_messages_read)
{
if (1 == disconnect_all)
{
/* the application closes and want that we disconnect all users */
struct MHD_UpgradeResponseHandle *urh = cu->urh;
if (NULL != urh)
{
/* Close the TCP/IP socket. */
/* This will also wake-up the waiting receive-thread for this connected user. */
cu->urh = NULL;
MHD_upgrade_action (urh,
MHD_UPGRADE_ACTION_CLOSE);
}
pthread_mutex_unlock (&chat_mutex);
return NULL;
}
else if (1 == cu->disconnect)
{
/* The sender thread shall close. */
/* This is only requested by the receive thread, so we can just leave. */
pthread_mutex_unlock (&chat_mutex);
return NULL;
}
else if (1 == cu->ping_status)
{
/* A pending ping is requested */
++cu->ping_counter;
strcpy (cu->ping_message,
"libmicrohttpdchatserverpingdata");
snprintf (cu->ping_message + 31, 97, "%d", (int) cu->ping_counter);
cu->ping_message_len = strlen (cu->ping_message);
char *frame_data = NULL;
size_t frame_len = 0;
int er = MHD_websocket_encode_ping (cu->ws,
cu->ping_message,
cu->ping_message_len,
&frame_data,
&frame_len);
if (MHD_WEBSOCKET_STATUS_OK == er)
{
cu->ping_status = 2;
timespec_get (&cu->ping_start, TIME_UTC);
/* send the data via the TCP/IP socket and */
/* unlock the mutex while sending */
pthread_mutex_unlock (&chat_mutex);
send_all (cu,
frame_data,
frame_len);
if (0 != pthread_mutex_lock (&chat_mutex))
{
return NULL;
}
}
MHD_websocket_free (cu->ws, frame_data);
}
else if (cu->next_message_index < message_count)
{
/* a chat message or command is pending */
char *frame_data = NULL;
size_t frame_len = 0;
int er = 0;
{
struct Message *msg = messages[cu->next_message_index];
if ((0 == msg->to_user_id) ||
(cu->user_id == msg->to_user_id) ||
(cu->user_id == msg->from_user_id) )
{
if (0 == msg->is_binary)
{
er = MHD_websocket_encode_text (cu->ws,
msg->data,
msg->data_len,
MHD_WEBSOCKET_FRAGMENTATION_NONE,
&frame_data,
&frame_len,
NULL);
}
else
{
er = MHD_websocket_encode_binary (cu->ws,
msg->data,
msg->data_len,
MHD_WEBSOCKET_FRAGMENTATION_NONE,
&frame_data,
&frame_len);
}
}
}
++cu->next_message_index;
/* send the data via the TCP/IP socket and */
/* unlock the mutex while sending */
pthread_mutex_unlock (&chat_mutex);
if (MHD_WEBSOCKET_STATUS_OK == er)
{
send_all (cu,
frame_data,
frame_len);
}
MHD_websocket_free (cu->ws,
frame_data);
if (0 != pthread_mutex_lock (&chat_mutex))
{
return NULL;
}
/* check whether there are still pending messages */
all_messages_read = (cu->next_message_index < message_count) ? 0 : 1;
}
else
{
all_messages_read = 1;
}
}
/* clear old messages */
chat_clearmessages (0);
/* Wait for wake up. */
/* This will automatically unlock the mutex while waiting and */
/* lock the mutex after waiting */
pthread_cond_wait (&cu->wake_up_sender, &chat_mutex);
}
}
return NULL;
}
/**
* Receives messages from the TCP/IP socket and
* initializes the connected user.
*
* @param cls The connected user
* @return Always NULL
*/
static void *
connecteduser_receive_messages (void *cls)
{
struct ConnectedUser *cu = cls;
char buf[128];
ssize_t got;
int result;
/* make the socket blocking */
make_blocking (cu->fd);
/* generate the user name */
{
char user_name[32];
strcpy (user_name, "User");
snprintf (user_name + 4, 28, "%d", (int) cu->user_id);
cu->user_name_len = strlen (user_name);
cu->user_name = malloc (cu->user_name_len + 1);
if (NULL == cu->user_name)
{
free (cu->extra_in);
free (cu);
MHD_upgrade_action (cu->urh,
MHD_UPGRADE_ACTION_CLOSE);
return NULL;
}
strcpy (cu->user_name, user_name);
}
/* initialize the wake-up-sender condition variable */
if (0 != pthread_cond_init (&cu->wake_up_sender, NULL))
{
MHD_upgrade_action (cu->urh,
MHD_UPGRADE_ACTION_CLOSE);
free (cu->user_name);
free (cu->extra_in);
free (cu);
return NULL;
}
/* initialize the send mutex */
if (0 != pthread_mutex_init (&cu->send_mutex, NULL))
{
MHD_upgrade_action (cu->urh,
MHD_UPGRADE_ACTION_CLOSE);
pthread_cond_destroy (&cu->wake_up_sender);
free (cu->user_name);
free (cu->extra_in);
free (cu);
return NULL;
}
/* add the user to the chat user list */
chat_adduser (cu);
/* initialize the web socket stream for encoding/decoding */
result = MHD_websocket_stream_init (&cu->ws,
MHD_WEBSOCKET_FLAG_SERVER
| MHD_WEBSOCKET_FLAG_NO_FRAGMENTS,
0);
if (MHD_WEBSOCKET_STATUS_OK != result)
{
chat_removeuser (cu);
pthread_cond_destroy (&cu->wake_up_sender);
pthread_mutex_destroy (&cu->send_mutex);
MHD_upgrade_action (cu->urh,
MHD_UPGRADE_ACTION_CLOSE);
free (cu->user_name);
free (cu->extra_in);
free (cu);
return NULL;
}
/* send a list of all currently connected users (bypassing the messaging system) */
{
struct UserInit
{
char *user_init;
size_t user_init_len;
};
struct UserInit *init_users = NULL;
size_t init_users_len = 0;
/* first collect all users without sending (so the mutex isn't locked too long) */
if (0 == pthread_mutex_lock (&chat_mutex))
{
if (0 < user_count)
{
init_users = (struct UserInit *) malloc (user_count * sizeof (struct
UserInit));
if (NULL != init_users)
{
init_users_len = user_count;
for (size_t i = 0; i < user_count; ++i)
{
char user_index[32];
snprintf (user_index, 32, "%d", (int) users[i]->user_id);
size_t user_index_len = strlen (user_index);
struct UserInit iu;
iu.user_init_len = user_index_len + users[i]->user_name_len + 10;
iu.user_init = (char *) malloc (iu.user_init_len + 1);
if (NULL != iu.user_init)
{
strcpy (iu.user_init, "userinit|");
strcat (iu.user_init, user_index);
strcat (iu.user_init, "|");
if (0 < users[i]->user_name_len)
strcat (iu.user_init, users[i]->user_name);
}
init_users[i] = iu;
}
}
}
pthread_mutex_unlock (&chat_mutex);
}
/* then send all users to the connected client */
for (size_t i = 0; i < init_users_len; ++i)
{
char *frame_data = NULL;
size_t frame_len = 0;
if ((0 < init_users[i].user_init_len) && (NULL !=
init_users[i].user_init) )
{
int status = MHD_websocket_encode_text (cu->ws,
init_users[i].user_init,
init_users[i].user_init_len,
MHD_WEBSOCKET_FRAGMENTATION_NONE,
&frame_data,
&frame_len,
NULL);
if (MHD_WEBSOCKET_STATUS_OK == status)
{
send_all (cu,
frame_data,
frame_len);
MHD_websocket_free (cu->ws,
frame_data);
}
free (init_users[i].user_init);
}
}
free (init_users);
}
/* send the welcome message to the user (bypassing the messaging system) */
{
char *frame_data = NULL;
size_t frame_len = 0;
const char *welcome_msg = "moderator||" \
"Welcome to the libmicrohttpd WebSocket chatserver example.\n" \
"Supported commands are:\n" \
" /m <user> <text> - sends a private message to the specified user\n" \
" /ping <user> - sends a ping to the specified user\n" \
" /name <name> - changes your name to the specified name\n" \
" /disconnect - disconnects your websocket\n\n" \
"All messages, which does not start with a slash, " \
"are regular messages and will be sent to the selected user.\n\n" \
"Have fun!";
MHD_websocket_encode_text (cu->ws,
welcome_msg,
strlen (welcome_msg),
MHD_WEBSOCKET_FRAGMENTATION_NONE,
&frame_data,
&frame_len,
NULL);
send_all (cu,
frame_data,
frame_len);
MHD_websocket_free (cu->ws,
frame_data);
}
/* start the message-send thread */
pthread_t pt;
if (0 != pthread_create (&pt,
NULL,
&connecteduser_send_messages,
cu))
abort ();
/* start by parsing extra data MHD may have already read, if any */
if (0 != cu->extra_in_size)
{
if (0 != connecteduser_parse_received_websocket_stream (cu,
cu->extra_in,
cu->extra_in_size))
{
chat_removeuser (cu);
if (0 == pthread_mutex_lock (&chat_mutex))
{
cu->disconnect = 1;
pthread_cond_signal (&cu->wake_up_sender);
pthread_mutex_unlock (&chat_mutex);
pthread_join (pt, NULL);
}
struct MHD_UpgradeResponseHandle *urh = cu->urh;
if (NULL != urh)
{
cu->urh = NULL;
MHD_upgrade_action (urh,
MHD_UPGRADE_ACTION_CLOSE);
}
pthread_cond_destroy (&cu->wake_up_sender);
pthread_mutex_destroy (&cu->send_mutex);
MHD_websocket_stream_free (cu->ws);
free (cu->user_name);
free (cu->extra_in);
free (cu);
return NULL;
}
free (cu->extra_in);
cu->extra_in = NULL;
}
/* the main loop for receiving data */
while (1)
{
got = recv (cu->fd,
buf,
sizeof (buf),
0);
if (0 >= got)
{
/* the TCP/IP socket has been closed */
break;
}
if (0 < got)
{
if (0 != connecteduser_parse_received_websocket_stream (cu, buf,
(size_t) got))
{
/* A websocket protocol error occurred */
chat_removeuser (cu);
if (0 == pthread_mutex_lock (&chat_mutex))
{
cu->disconnect = 1;
pthread_cond_signal (&cu->wake_up_sender);
pthread_mutex_unlock (&chat_mutex);
pthread_join (pt, NULL);
}
struct MHD_UpgradeResponseHandle *urh = cu->urh;
if (NULL != urh)
{
cu->urh = NULL;
MHD_upgrade_action (urh,
MHD_UPGRADE_ACTION_CLOSE);
}
pthread_cond_destroy (&cu->wake_up_sender);
pthread_mutex_destroy (&cu->send_mutex);
MHD_websocket_stream_free (cu->ws);
free (cu->user_name);
free (cu);
return NULL;
}
}
}
/* cleanup */
chat_removeuser (cu);
if (0 == pthread_mutex_lock (&chat_mutex))
{
cu->disconnect = 1;
pthread_cond_signal (&cu->wake_up_sender);
pthread_mutex_unlock (&chat_mutex);
pthread_join (pt, NULL);
}
struct MHD_UpgradeResponseHandle *urh = cu->urh;
if (NULL != urh)
{
cu->urh = NULL;
MHD_upgrade_action (urh,
MHD_UPGRADE_ACTION_CLOSE);
}
pthread_cond_destroy (&cu->wake_up_sender);
pthread_mutex_destroy (&cu->send_mutex);
MHD_websocket_stream_free (cu->ws);
free (cu->user_name);
free (cu);
return NULL;
}
/**
* Function called after a protocol "upgrade" response was sent
* successfully and the socket should now be controlled by some
* protocol other than HTTP.
*
* Any data already received on the socket will be made available in
* @e extra_in. This can happen if the application sent extra data
* before MHD send the upgrade response. The application should
* treat data from @a extra_in as if it had read it from the socket.
*
* Note that the application must not close() @a sock directly,
* but instead use #MHD_upgrade_action() for special operations
* on @a sock.
*
* Data forwarding to "upgraded" @a sock will be started as soon
* as this function return.
*
* Except when in 'thread-per-connection' mode, implementations
* of this function should never block (as it will still be called
* from within the main event loop).
*
* @param cls closure, whatever was given to #MHD_create_response_for_upgrade().
* @param connection original HTTP connection handle,
* giving the function a last chance
* to inspect the original HTTP request
* @param req_cls last value left in `req_cls` of the `MHD_AccessHandlerCallback`
* @param extra_in if we happened to have read bytes after the
* HTTP header already (because the client sent
* more than the HTTP header of the request before
* we sent the upgrade response),
* these are the extra bytes already read from @a sock
* by MHD. The application should treat these as if
* it had read them from @a sock.
* @param extra_in_size number of bytes in @a extra_in
* @param sock socket to use for bi-directional communication
* with the client. For HTTPS, this may not be a socket
* that is directly connected to the client and thus certain
* operations (TCP-specific setsockopt(), getsockopt(), etc.)
* may not work as expected (as the socket could be from a
* socketpair() or a TCP-loopback). The application is expected
* to perform read()/recv() and write()/send() calls on the socket.
* The application may also call shutdown(), but must not call
* close() directly.
* @param urh argument for #MHD_upgrade_action()s on this @a connection.
* Applications must eventually use this callback to (indirectly)
* perform the close() action on the @a sock.
*/
static void
upgrade_handler (void *cls,
struct MHD_Connection *connection,
void *req_cls,
const char *extra_in,
size_t extra_in_size,
MHD_socket fd,
struct MHD_UpgradeResponseHandle *urh)
{
struct ConnectedUser *cu;
pthread_t pt;
(void) cls; /* Unused. Silent compiler warning. */
(void) connection; /* Unused. Silent compiler warning. */
(void) req_cls; /* Unused. Silent compiler warning. */
/* This callback must return as soon as possible. */
/* allocate new connected user */
cu = malloc (sizeof (struct ConnectedUser));
if (NULL == cu)
abort ();
memset (cu, 0, sizeof (struct ConnectedUser));
if (0 != extra_in_size)
{
cu->extra_in = malloc (extra_in_size);
if (NULL == cu->extra_in)
abort ();
memcpy (cu->extra_in,
extra_in,
extra_in_size);
}
cu->extra_in_size = extra_in_size;
cu->fd = fd;
cu->urh = urh;
cu->user_id = ++unique_user_id;
cu->user_name = NULL;
cu->user_name_len = 0;
/* create thread for the new connected user */
if (0 != pthread_create (&pt,
NULL,
&connecteduser_receive_messages,
cu))
abort ();
pthread_detach (pt);
}
/**
* Function called by the MHD_daemon when the client tries to access a page.
*
* This is used to provide the main page
* (in this example HTML + CSS + JavaScript is all in the same file)
* and to initialize a websocket connection.
* The rules for the initialization of a websocket connection
* are listed near the URL check of "/ChatServerWebSocket".
*
* @param cls closure, whatever was given to #MHD_start_daemon().
* @param connection The HTTP connection handle
* @param url The requested URL
* @param method The request method (typically "GET")
* @param version The HTTP version
* @param upload_data Given upload data for POST requests
* @param upload_data_size The size of the upload data
* @param req_cls A pointer for request specific data
* @return MHD_YES on success or MHD_NO on error.
*/
static enum MHD_Result
access_handler (void *cls,
struct MHD_Connection *connection,
const char *url,
const char *method,
const char *version,
const char *upload_data,
size_t *upload_data_size,
void **req_cls)
{
static int aptr;
struct MHD_Response *response;
int ret;
(void) cls; /* Unused. Silent compiler warning. */
(void) version; /* Unused. Silent compiler warning. */
(void) upload_data; /* Unused. Silent compiler warning. */
(void) upload_data_size; /* Unused. Silent compiler warning. */
if (0 != strcmp (method, "GET"))
return MHD_NO; /* unexpected method */
if (&aptr != *req_cls)
{
/* do never respond on first call */
*req_cls = &aptr;
return MHD_YES;
}
*req_cls = NULL; /* reset when done */
if (0 == strcmp (url, "/"))
{
/* Default page for visiting the server */
struct MHD_Response *response;
response = MHD_create_response_from_buffer_static (strlen (PAGE),
PAGE);
ret = MHD_queue_response (connection,
MHD_HTTP_OK,
response);
MHD_destroy_response (response);
}
else if (0 == strcmp (url, "/ChatServerWebSocket"))
{
/**
* The path for the chat has been accessed.
* For a valid WebSocket request, at least five headers are required:
* 1. "Host: <name>"
* 2. "Connection: Upgrade"
* 3. "Upgrade: websocket"
* 4. "Sec-WebSocket-Version: 13"
* 5. "Sec-WebSocket-Key: <base64 encoded value>"
* Values are compared in a case-insensitive manner.
* Furthermore it must be a HTTP/1.1 or higher GET request.
* See: https://tools.ietf.org/html/rfc6455#section-4.2.1
*
* To make this example portable we skip the Host check
*/
char is_valid = 1;
const char *value = NULL;
char sec_websocket_accept[29];
/* check whether an websocket upgrade is requested */
if (0 != MHD_websocket_check_http_version (version))
{
is_valid = 0;
}
value = MHD_lookup_connection_value (connection,
MHD_HEADER_KIND,
MHD_HTTP_HEADER_CONNECTION);
if (0 != MHD_websocket_check_connection_header (value))
{
is_valid = 0;
}
value = MHD_lookup_connection_value (connection,
MHD_HEADER_KIND,
MHD_HTTP_HEADER_UPGRADE);
if (0 != MHD_websocket_check_upgrade_header (value))
{
is_valid = 0;
}
value = MHD_lookup_connection_value (connection,
MHD_HEADER_KIND,
MHD_HTTP_HEADER_SEC_WEBSOCKET_VERSION);
if (0 != MHD_websocket_check_version_header (value))
{
is_valid = 0;
}
value = MHD_lookup_connection_value (connection,
MHD_HEADER_KIND,
MHD_HTTP_HEADER_SEC_WEBSOCKET_KEY);
if (0 != MHD_websocket_create_accept_header (value, sec_websocket_accept))
{
is_valid = 0;
}
if (1 == is_valid)
{
/* create the response for upgrade */
response = MHD_create_response_for_upgrade (&upgrade_handler,
NULL);
/**
* For the response we need at least the following headers:
* 1. "Connection: Upgrade"
* 2. "Upgrade: websocket"
* 3. "Sec-WebSocket-Accept: <base64value>"
* The value for Sec-WebSocket-Accept can be generated with MHD_websocket_create_accept_header.
* It requires the value of the Sec-WebSocket-Key header of the request.
* See also: https://tools.ietf.org/html/rfc6455#section-4.2.2
*/
MHD_add_response_header (response,
MHD_HTTP_HEADER_UPGRADE,
"websocket");
MHD_add_response_header (response,
MHD_HTTP_HEADER_SEC_WEBSOCKET_ACCEPT,
sec_websocket_accept);
ret = MHD_queue_response (connection,
MHD_HTTP_SWITCHING_PROTOCOLS,
response);
MHD_destroy_response (response);
}
else
{
/* return error page */
struct MHD_Response *response;
response =
MHD_create_response_from_buffer_static ( \
strlen (PAGE_INVALID_WEBSOCKET_REQUEST),
PAGE_INVALID_WEBSOCKET_REQUEST);
ret = MHD_queue_response (connection,
MHD_HTTP_BAD_REQUEST,
response);
MHD_destroy_response (response);
}
}
else
{
struct MHD_Response *response;
response = MHD_create_response_from_buffer_static (strlen (PAGE_NOT_FOUND),
PAGE_NOT_FOUND);
ret = MHD_queue_response (connection,
MHD_HTTP_NOT_FOUND,
response);
MHD_destroy_response (response);
}
return ret;
}
/**
* The main routine for this example
*
* This starts the daemon and waits for a key hit.
* After this it will shutdown the daemon.
*/
int
main (int argc,
char *const *argv)
{
(void) argc; /* Unused. Silent compiler warning. */
(void) argv; /* Unused. Silent compiler warning. */
struct MHD_Daemon *d;
if (0 != pthread_mutex_init (&chat_mutex, NULL))
return 1;
#if USE_HTTPS == 1
const char private_key[] = "TODO: Enter your key in PEM format here";
const char certificate[] = "TODO: Enter your certificate in PEM format here";
d = MHD_start_daemon (MHD_ALLOW_UPGRADE | MHD_USE_AUTO
| MHD_USE_INTERNAL_POLLING_THREAD | MHD_USE_ERROR_LOG
| MHD_USE_TLS,
443,
NULL, NULL,
&access_handler, NULL,
MHD_OPTION_CONNECTION_TIMEOUT, (unsigned int) 120,
MHD_OPTION_HTTPS_MEM_KEY, private_key,
MHD_OPTION_HTTPS_MEM_CERT, certificate,
MHD_OPTION_END);
#else
d = MHD_start_daemon (MHD_ALLOW_UPGRADE | MHD_USE_AUTO
| MHD_USE_INTERNAL_POLLING_THREAD | MHD_USE_ERROR_LOG,
80,
NULL, NULL,
&access_handler, NULL,
MHD_OPTION_CONNECTION_TIMEOUT, (unsigned int) 120,
MHD_OPTION_END);
#endif
if (NULL == d)
return 1;
(void) getc (stdin);
if (0 == pthread_mutex_lock (&chat_mutex))
{
disconnect_all = 1;
for (size_t i = 0; i < user_count; ++i)
pthread_cond_signal (&users[i]->wake_up_sender);
pthread_mutex_unlock (&chat_mutex);
}
sleep (2);
if (0 == pthread_mutex_lock (&chat_mutex))
{
for (size_t i = 0; i < user_count; ++i)
{
struct MHD_UpgradeResponseHandle *urh = users[i]->urh;
if (NULL != urh)
{
users[i]->urh = NULL;
MHD_upgrade_action (users[i]->urh,
MHD_UPGRADE_ACTION_CLOSE);
}
}
pthread_mutex_unlock (&chat_mutex);
}
sleep (2);
/* usually we should wait here in a safe way for all threads to disconnect, */
/* but we skip this in the example */
pthread_mutex_destroy (&chat_mutex);
MHD_stop_daemon (d);
return 0;
}