Websocket Server in C

5 minute read

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 is BE/bv0JO2wBVnABhxQO5kQ== and concatenate it with Websocket GID 258EAFA5-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>