Websocket Server in C
Published:
I’m starting my first post by describing a program I wrote in C that implements a WebSocket server.
The server accepts a WebSocket connection by completing the opening handshake: it replies with a Sec-WebSocket-Accept value derived from the client’s Sec-WebSocket-Key. Once the handshake succeeds, the server is ready to receive messages from the client. The handshake follows the protocol described in RFC 6455.
The program performs the following tasks:
- Starts a TCP socket server
- Waits for a connection
- Receives a WebSocket handshake request
- Sends a WebSocket handshake response to the client
- Starts receiving messages sent by the client and dumps them to stdout
I took an example of a TCP server from CS 213: Introduction to Computer Systems, Fall 1999.
The handshake message from client is something like this:
GET / HTTP/1.1
Host: localhost:8080
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.11; rv:44.0) Gecko/20100101 Firefox/44.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate
Sec-WebSocket-Version: 13
Origin: null
Sec-WebSocket-Extensions: permessage-deflate
Sec-WebSocket-Key: BE/bv0JO2wBVnABhxQO5kQ==
Connection: keep-alive, Upgrade
Pragma: no-cache
Cache-Control: no-cache
Upgrade: websocket
And the server has to answer back this handshake message in return
HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: qBPI+1rPa4lNhVJ0cztOjbvugAQ=
To construct the Sec-WebSocket-Accept value, we need to perform the following:
- Take value of
Sec-WebSocket-Key, in this case it isBE/bv0JO2wBVnABhxQO5kQ==and concatenate it with Websocket GID258EAFA5-E914-47DA-95CA-C5AB0DC85B11 - Encrypt that concatenated string using
OpenSSL SHA1 - Encode base64 of the encrypted string
char * encrypt_message(char * msg, char * digestname) {
EVP_MD_CTX *mdctx;
const EVP_MD *md;
unsigned char md_value[EVP_MAX_MD_SIZE];
int md_len, i;
unsigned char * encoded;
int nb;
OpenSSL_add_all_digests();
md = EVP_get_digestbyname(digestname);
if (!md) {
printf("Unknown message digest %s\n", digestname);
exit(1);
}
mdctx = EVP_MD_CTX_create();
EVP_DigestInit_ex(mdctx, md, NULL);
EVP_DigestUpdate(mdctx, msg, strlen(msg));
EVP_DigestFinal_ex(mdctx, md_value, &md_len);
EVP_MD_CTX_destroy(mdctx);
/* malloc of md_len */
encoded = malloc(md_len);
memcpy(encoded, md_value, md_len);
/* Call this once before exit. */
EVP_cleanup();
return encoded;
}
This program will take the str specified by its length and encode it to base64.
const char table[64] = {'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H',
'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P',
'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X',
'Y', 'Z', 'a', 'b', 'c', 'd', 'e', 'f',
'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n',
'o', 'p', 'q', 'r', 's', 't', 'u', 'v',
'w', 'x', 'y', 'z', '0', '1', '2', '3',
'4', '5', '6', '7', '8', '9', '+', '/'};
int base64encode(char* encoded, const unsigned char * str, int length) {
int i;
char * p;
p = encoded;
for (i = 0; i < length; i = i + 3) {
if (i == length - 2) { //=
*p++ = table[str[i] >> 2];
*p++ = table[((str[i] << 4) & 0x30) | ((str[i+1] & 0xF0) >> 4)];
*p++ = table[(str[i+1] << 2) & 0x3C];
*p++ = '=';
} else {
if (i == length - 1) { //==
*p++ = table[str[i] >> 2];
*p++ = table[(str[i] << 2) & 0x30];
*p++ = '=';
*p++ = '=';
} else {
*p++ = table[str[i] >> 2];
*p++ = table[((str[i] << 4) & 0x30) | ((str[i+1] & 0xF0) >> 4)];
*p++ = table[((str[i + 1] << 2) & 0x3C) | (str[i+2] >> 6)];
*p++ = table[(str[i + 2] & 0x3F)];
}
}
}
*p++ = '\0';
return p - encoded;
}
This function will
- read
header, - compute the
length of payload data, - get 4 bytes of masks and
- dump message to
stdout
int read_data(int fd) {
uint64_t i, j, length, tmp_length;;
unsigned char c;
unsigned char buf[BUFSIZE];
char masks[4];
int n;
// init
i = 0;
j = 0;
n = 0;
length = 0;
tmp_length = 0;
/* check the payload */
n = read(fd, buf, 2);
/* if read error return errorno */
if (n < 0) return n;
/* read payload length */
length = buf[1] & (0x7F);
printf("Initial payload length: %llu\n", length);
/* check the payload length */
if (length == 126) {
n = read(fd, buf, 2);
/* if read error return errorno */
if (n < 0) return n;
length = buf[0];
length = (length << 8) | buf[1];
} else {
if (length == 127) {
n = read(fd, buf, 8);
/* if read error return errorno */
if (n < 0) return n;
length = buf[0];
for (i = 1; i <= 7; i++) {
length = ((length << 8) | buf[i]);
}
}
}
printf("Total Payload: %"PRIu64"\n", length);
printf("Start reading character and print them out...\n\n");
/* get masks from the next 4 bytes */
n = read(fd, masks, 4);
/* return error if reading is negative */
if (n < 0) return n;
/* save the original length */
tmp_length = length;
/* start dumping content to stdout */
while (TRUE) {
if (tmp_length < BUFSIZE) {
n = read(fd, buf, tmp_length);
if (n < 0) return n;
} else {
n = read(fd, buf, BUFSIZE);
}
for (i = 0; i < n; i++) {
c = (buf[i] ^ masks[j % 4]);
printf("%c", c);
j++;
}
tmp_length = tmp_length - n;
if (j == length) break;
}
printf("\nEnd of transmitted message\n");
return length;
}
Here is the client code in html.
<html>
<body>
<script>
ws = new WebSocket("ws://localhost:8080");
ws.onopen = function() {
// Web Socket is connected. You can send data by send() method.
console.log("connected...");
ws.send("hello world!");
};
</script>
</body>
</html>
