Created
June 6, 2025 17:26
-
-
Save supechicken/f33638feba8d426543edc9e3796f294d to your computer and use it in GitHub Desktop.
Simple WebSocket library written in C
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#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; | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#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