Skip to content

Instantly share code, notes, and snippets.

@supechicken
Created June 6, 2025 17:26
Show Gist options
  • Save supechicken/f33638feba8d426543edc9e3796f294d to your computer and use it in GitHub Desktop.
Save supechicken/f33638feba8d426543edc9e3796f294d to your computer and use it in GitHub Desktop.
Simple WebSocket library written in C
#include "websocket.h"
#define HTTP_WRONG_PROTOCOL_HEADERS \
"HTTP/1.1 405 Method Not Allowed\r\n" \
"Connection: closed\r\n" \
"\r\n"
#define HTTP_UPGRADE_HEADERS \
"HTTP/1.1 101 Switching Protocols\r\n" \
"Connection: upgrade\r\n" \
"Upgrade: websocket\r\n" \
"Sec-WebSocket-Accept: %s\r\n" \
"\r\n"
#define WS_MAGIC "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"
bool read_ws_frame(FILE *client, uint8_t **payload, uint64_t *payload_len) {
uint8_t read_buf[16], masking_key[4], fin, rsv, opcode, mask;
uint64_t bytes_read = 0;
if (!fread(read_buf, 2, 1, client)) return false;
fin = (read_buf[0] >> 7) & 0b00000001;
rsv = (read_buf[0] >> 4) & 0b00000111;
opcode = (read_buf[0] >> 0) & 0b00001111;
mask = (read_buf[1] >> 7) & 0b00000001;
*payload_len = (read_buf[1] >> 0) & 0b01111111;
switch (opcode) {
case 0x1:
case 0x2:
break;
case 0x8:
fprintf(stderr, "Terminating WebSocket connection as requested...\n");
fclose(client);
return false;
case 0x9:
case 0xA:
fprintf(stderr, "Ping/Pong frame received, ignoring...\n");
return true;
}
if (!fin) fprintf(stderr, "Warning: Message fragmentation isn't supported yet, payload might be incomplete\n");
/*
https://datatracker.ietf.org/doc/html/rfc6455#section-5.2:
RSV1, RSV2, RSV3: 1 bit each
MUST be 0 unless an extension is negotiated that defines meanings
for non-zero values. If a nonzero value is received and none of
the negotiated extensions defines the meaning of such a nonzero
value, the receiving endpoint MUST **Fail the WebSocket Connection**.
*/
if (rsv) {
fprintf(stderr, "Error: One or more RSV bit is set\n");
fclose(client);
return true;
}
/*
https://datatracker.ietf.org/doc/html/rfc6455#section-5.1:
To avoid confusing network intermediaries (such as
intercepting proxies) and for security reasons that are further
discussed in Section 10.3, a client MUST mask all frames that it
sends to the server (see Section 5.3 for further details).
*/
if (!mask) {
fprintf(stderr, "Error: MASK bit must be 1\n");
fclose(client);
return true;
}
switch (*payload_len) {
case 126:
fread(read_buf, sizeof(uint16_t), 1, client);
*payload_len = be16toh(*(uint16_t *) read_buf);
break;
case 127:
fread(read_buf, sizeof(uint64_t), 1, client);
*payload_len = be64toh(*(uint64_t *) read_buf);
break;
}
*payload = malloc(*payload_len);
fread(masking_key, sizeof(masking_key), 1, client);
printf("Payload length: %lu\n", *payload_len);
// read all bytes in payload
while (bytes_read != *payload_len) bytes_read += fread(*payload, 1, *payload_len - bytes_read, client);
// decode payload with masking key
for (uint64_t i = 0; i < *payload_len; i++) (*payload)[i] ^= masking_key[i % 4];
return true;
}
void send_ws_frame(FILE *client, uint8_t *payload, uint64_t payload_len) {
uint8_t ws_header[10], ws_header_len = 2;
ws_header[0] = 0b10000010;
if (payload_len > 65535) {
ws_header[1] = 127;
ws_header_len += sizeof(uint64_t);
*(uint64_t *) &ws_header[2] = htobe64(payload_len);
} else if (payload_len > 125) {
ws_header[1] = 126;
ws_header_len += sizeof(uint16_t);
*(uint16_t *) &ws_header[2] = htobe16(payload_len);
} else {
ws_header[1] = payload_len;
}
fwrite(ws_header, ws_header_len, 1, client);
fwrite(payload, payload_len, 1, client);
}
int start_ws_server(uint16_t port, void callback(FILE *, uint8_t *, uint64_t)) {
int client_fd = -1,
socket_fd = socket(AF_INET, SOCK_STREAM | SOCK_CLOEXEC, 0),
reuseaddr = 1;
socklen_t sockaddr_len = sizeof(struct sockaddr_in);
uint8_t read_buf[128];
FILE *client;
char *strtok_ptr;
struct sockaddr_in client_info,
socket_info;
socket_fd = socket(AF_INET, SOCK_STREAM | SOCK_CLOEXEC, 0);
socket_info.sin_family = AF_INET;
socket_info.sin_addr.s_addr = inet_addr("127.0.0.1");
socket_info.sin_port = htons(port);
setsockopt(socket_fd, SOL_SOCKET, SO_REUSEADDR, &reuseaddr, sizeof(reuseaddr));
if (bind(socket_fd, (struct sockaddr *) &socket_info, sockaddr_len) == -1) {
fprintf(stderr, "bind() failed: %s\n", strerror(errno));
exit(errno);
}
if (listen(socket_fd, 1) == -1) {
fprintf(stderr, "listen() failed: %s\n", strerror(errno));
exit(errno);
}
while (client_fd = accept(socket_fd, (struct sockaddr *) &client_info, &sockaddr_len)) {
char *method, *path, *http_ver, ws_key[64], ws_key_sha1[20], ws_key_base64[32];
client = fdopen(client_fd, "r+");
setvbuf(client, NULL, _IONBF, 0);
// read and parse HTTP request line
fgets(read_buf, sizeof(read_buf), client);
method = strdup(strtok_r(read_buf, " ", &strtok_ptr));
path = strdup(strtok_r(NULL, " ", &strtok_ptr));
http_ver = strdup(strtok_r(NULL, "\r", &strtok_ptr));
printf("%s %s %s\n", method, path, http_ver);
// block non-GET requrests
if (strcmp(method, "GET") != 0) {
fprintf(client, HTTP_WRONG_PROTOCOL_HEADERS);
fclose(client);
continue;
}
// read and parse HTTP headers
while (fgets(read_buf, sizeof(read_buf), client)) {
char *strtok_ptr2, *key, *value;
if (read_buf[0] == '\r') break;
key = strtok_r(read_buf, ":", &strtok_ptr);
value = strtok_r(NULL, "\r", &strtok_ptr) + 1;
printf("%s: %s\n", key, value);
if (strcmp(key, "Sec-WebSocket-Key") == 0) {
/*
https://datatracker.ietf.org/doc/html/rfc6455#section-1.3
To prove that the handshake was received, the server has to take two
pieces of information and combine them to form a response. The first
piece of information comes from the |Sec-WebSocket-Key| header field
in the client handshake.
For this header field, the server has to take the value (as present
in the header field) and concatenate this with the Globally Unique
Identifier (GUID) "258EAFA5-E914-47DA-95CA-C5AB0DC85B11" in string form,
which is unlikely to be used by network endpoints that do not understand
the WebSocket Protocol. A SHA-1 hash (160 bits), base64-encoded, of
this concatenation is then returned in the server's handshake.
*/
snprintf(ws_key, sizeof(ws_key), "%s" WS_MAGIC, value);
SHA1(ws_key, strlen(ws_key), ws_key_sha1);
EVP_EncodeBlock(ws_key_base64, ws_key_sha1, sizeof(ws_key_sha1));
}
}
// send an HTTP 101 response (with calculated Sec-WebSocket-Accept header) to finish handshaking
fprintf(client, HTTP_UPGRADE_HEADERS, ws_key_base64);
uint8_t *payload;
uint64_t payload_len;
while (read_ws_frame(client, &payload, &payload_len)) callback(client, payload, payload_len);
}
return socket_fd;
}
#include <stdio.h>
#include <stdbool.h>
#include <stdlib.h>
#include <stdint.h>
#include <string.h>
#include <unistd.h>
#include <errno.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <openssl/sha.h>
#include <openssl/evp.h>
extern bool read_ws_frame(FILE *client, uint8_t **payload, uint64_t *payload_len);
extern void send_ws_frame(FILE *client, uint8_t *payload, uint64_t payload_len);
extern int start_ws_server(uint16_t port, void callback(FILE *, uint8_t *, uint64_t));
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment