| /* |
| 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; |
| } |