#ifndef PROTOCOL_H #define PROTOCOL_H #include "arena.h" #include "chatty.h" /// Protocol // - every message has format Header + Message // TODO: security // /// ID // - So clients can be identified uniquely. // - 8 bytes // - number that increments for each new client // /// Strings // - strings are sent with their null terminator // /// Authentication // Each header contains the id of the sender, because ids start at 1 // message with id 0 is considered unauthenticated. // // When the server receives a header with id 0, this can happen // Scenario 1: IDMessage, client already has an ID // 1. client-> Send own ID // 2. server-> knows ID? // y. server-> Success // n. 1. server-> Error 'notfound' // 2. client-> exit // Scenario 2: IntroductionMessage, client requests a new ID // 1. client-> Introduces // 2. server-> Sends & Saves ID // 3. Save ID // // BIFD & UNIFD // Each client has 2 connections that must be authenticated one for 2-way // communication and one for 1-way communication. Respectively BIFD and // UNIFD. BIFD must be authenticated first and is meant for requests such // as getting an IntroductionMessage for a sent IDMessage. // UNIFD is for messages that are like notifications. For example // PresenceMessage that tells us when another user connected. // These two connections separate these message types so we do not have to // worry about receiving a PresenceMessage when waiting for an a response. // /// Naming conventions // Messages end with the Message suffix (eg. TextMessag, HistoryMessage) // // A function that is coupled to a type works like // eg. (printTextMessage, formatTimestamp) #define PROTOCOL_VERSION 0 // Size of author string including null terminator #define AUTHOR_LEN 13 // Size of formatted timestamp string including null terminator #define TIMESTAMP_LEN 9 #define TIMESTAMP_FORMAT "%H:%M:%S" typedef u64 ID; // - 2 bytes for version // - 1 byte for message type // - 16 bytes for checksum typedef struct { u16 version; u8 type; ID id; } HeaderMessage; typedef enum { HEADER_TYPE_TEXT = 0, HEADER_TYPE_HISTORY, HEADER_TYPE_PRESENCE, HEADER_TYPE_ID, HEADER_TYPE_INTRODUCTION, HEADER_TYPE_ERROR } HeaderType; // shorthand for creating a header with a value from the enum #define HEADER_INIT(t) {.version = PROTOCOL_VERSION, .type = t, .id = 0} // from Tsoding video on minicel (https://youtu.be/HCAgvKQDJng?t=4546) // sv(https://github.com/tsoding/sv) #define HEADER_FMT "header: v%d %s(%d) [%d]" #define HEADER_ARG(header) header.version, headerTypeString(header.type), header.type, header.id // For sending texts to other clients // - 13 bytes for the author // - 8 bytes for the timestamp // - 2 bytes for the text length // - x*4 bytes for the text typedef struct { u64 timestamp; // timestamp of when the message was sent u16 len; wchar_t* text; // placeholder for indexing // wchar_t* is used, because this renders the text in the debugger } TextMessage; // Size of TextMessage without text pointer #define TEXTMESSAGE_SIZE (sizeof(TextMessage) - sizeof(u32*)) // Requesting messages sent after a timestamp. // - 8 bytes for the timestamp typedef struct { u64 timestamp; } HistoryMessage; // Introduce the client to the server by sending the client's information. // See "First connection". // - 13 bytes for author typedef struct { u8 author[AUTHOR_LEN]; } IntroductionMessage; #define INTRODUCTION_FMT "introduction: %s" #define INTRODUCTION_ARG(message) message.author // Notifying the sender's state, such as "connected", "disconnected", "AFK", ... // - 1 byte for type typedef struct { u8 type; } PresenceMessage; typedef enum { PRESENCE_TYPE_CONNECTED = 0, PRESENCE_TYPE_DISCONNECTED, PRESENCE_TYPE_AFK } PresenceType; // Send an error message // - 1 byte for type typedef struct { u8 type; } ErrorMessage; typedef enum { ERROR_TYPE_BADMESSAGE = 0, ERROR_TYPE_NOTFOUND, ERROR_TYPE_SUCCESS, ERROR_TYPE_ALREADYCONNECTED, ERROR_TYPE_TOOMANYCONNECTIONS } ErrorType; #define ERROR_INIT(t) {.type = t} typedef struct { ID id; } IDMessage; typedef struct { s32 nrecv; TextMessage* message; } recvTextMessageResult; // Returns string for type byte in HeaderMessage u8* headerTypeString(HeaderType type) { switch (type) { case HEADER_TYPE_TEXT: return (u8*)"TextMessage"; case HEADER_TYPE_HISTORY: return (u8*)"HistoryMessage"; case HEADER_TYPE_PRESENCE: return (u8*)"PresenceMessage"; case HEADER_TYPE_ID: return (u8*)"IDMessage"; case HEADER_TYPE_INTRODUCTION: return (u8*)"IntroductionMessage"; case HEADER_TYPE_ERROR: return (u8*)"ErrorMessage"; default: return (u8*)"Unknown"; } } u8* presenceTypeString(PresenceType type) { switch (type) { case PRESENCE_TYPE_CONNECTED: return (u8*)"connected"; case PRESENCE_TYPE_DISCONNECTED: return (u8*)"disconnected"; case PRESENCE_TYPE_AFK: return (u8*)"afk"; default: return (u8*)"Unknown"; } } u8* errorTypeString(ErrorType type) { switch (type) { case ERROR_TYPE_BADMESSAGE: return (u8*)"bad message"; case ERROR_TYPE_NOTFOUND: return (u8*)"not found"; case ERROR_TYPE_SUCCESS: return (u8*)"success"; case ERROR_TYPE_ALREADYCONNECTED: return (u8*)"already connected"; case ERROR_TYPE_TOOMANYCONNECTIONS: return (u8*)"too many connections"; default: return (u8*)"Unknown"; } } // Formats time t into tmsp string void formatTimestamp(u8 timestamp_str[TIMESTAMP_LEN], u64 timestamp) { struct tm* ltime; ltime = localtime((time_t*)×tamp); strftime((char*)timestamp_str, TIMESTAMP_LEN, TIMESTAMP_FORMAT, ltime); } // Receive a message from fd and store it in the msgsArena, // Returns pointer to the allocated memory TextMessage* recvTextMessage(Arena* msgsArena, u32 fd) { TextMessage* message = ArenaPush(msgsArena, TEXTMESSAGE_SIZE); // Receive everything but the text so we can know the text's size and act accordingly s32 nrecv = recv(fd, message, TEXTMESSAGE_SIZE, 0); Assert(nrecv != -1); Assert(nrecv == TEXTMESSAGE_SIZE); // Allocate memory for text and receive in that memory u32 text_size = message->len * sizeof(*message->text); ArenaPush(msgsArena, text_size); nrecv = recv(fd, (u8*)&message->text, text_size, 0); Assert(nrecv != -1); Assert(nrecv == (s32)(message->len * sizeof(*message->text))); return message; } typedef struct { HeaderMessage* header; void* message; } Message; u32 getMessageSize(HeaderType type) { u32 size = 0; switch (type) { case HEADER_TYPE_ERROR: size = sizeof(ErrorMessage); break; case HEADER_TYPE_HISTORY: size = sizeof(HistoryMessage); break; case HEADER_TYPE_INTRODUCTION: size = sizeof(IntroductionMessage); break; case HEADER_TYPE_PRESENCE: size = sizeof(PresenceMessage); break; case HEADER_TYPE_ID: size = sizeof(IDMessage); break; default: Assert(0); } return size; } s32 recvAnyMessageType(s32 fd, HeaderMessage* header, void *anyMessage, HeaderType type) { s32 nrecv = recv(fd, header, sizeof(*header), 0); if (nrecv == -1 || nrecv == 0) return nrecv; Assert(nrecv == sizeof(*header)); s32 size = 0; switch (type) { case HEADER_TYPE_ERROR: case HEADER_TYPE_HISTORY: case HEADER_TYPE_INTRODUCTION: case HEADER_TYPE_PRESENCE: case HEADER_TYPE_ID: size = getMessageSize(header->type); break; case HEADER_TYPE_TEXT: { TextMessage* message = anyMessage; size = TEXTMESSAGE_SIZE + message->len * sizeof(*message->text); } break; default: Assert(0); break; } Assert(header->type == type); nrecv = recv(fd, anyMessage, size, 0); Assert(nrecv != -1); Assert(nrecv == size); return size; } // Get any message into arena Message recvAnyMessage(Arena* arena, s32 fd) { HeaderMessage* header = ArenaPush(arena, sizeof(*header)); s32 nrecv = recv(fd, header, sizeof(*header), 0); Assert(nrecv != -1); Assert(nrecv == sizeof(*header)); s32 size = 0; switch (header->type) { case HEADER_TYPE_TEXT: { Message result; result.header = header; result.message = recvTextMessage(arena, fd); return result; } break; default: { size = getMessageSize(header->type); } break; } void* message = ArenaPush(arena, size); nrecv = recv(fd, message, size, 0); Assert(nrecv != -1); Assert(nrecv == size); Message result; result.header = header; result.message = message; return result; } Message waitForMessageType(Arena* arena, Arena* queueArena, u32 fd, HeaderType type) { Message message; while (1) { message = recvAnyMessage(arena, fd); if (message.header->type == type) break; ArenaPush(queueArena, getMessageSize(message.header->type)); } return message; } // Generic sending function for sending any type of message to fd // Returns number of bytes sent in message or -1 if there was an error. s32 sendAnyMessage(u32 fd, HeaderMessage header, void* anyMessage) { s32 nsend_total; s32 nsend = send(fd, &header, sizeof(header), 0); if (nsend == -1) return nsend; LoggingF("sendAnyMessage (%d)|sending "HEADER_FMT"\n", fd, HEADER_ARG(header)); Assert(nsend == sizeof(header)); nsend_total = nsend; s32 size = 0; switch (header.type) { case HEADER_TYPE_ERROR: case HEADER_TYPE_HISTORY: case HEADER_TYPE_INTRODUCTION: case HEADER_TYPE_PRESENCE: case HEADER_TYPE_ID: size = getMessageSize(header.type); break; case HEADER_TYPE_TEXT: { nsend = send(fd, anyMessage, TEXTMESSAGE_SIZE, 0); Assert(nsend != -1); Assert(nsend == TEXTMESSAGE_SIZE); nsend_total += nsend; // set size to remaning text size that should be sent TextMessage* message = (TextMessage*)anyMessage; size = message->len * sizeof(*message->text); nsend = 0; anyMessage = &message->text; } break; default: LoggingF("sendAnyMessage (%d)|Cannot send %s\n", fd, headerTypeString(header.type)); return -1; } nsend = send(fd, anyMessage, size, 0); if (nsend == -1) return nsend; Assert(nsend == size); nsend_total += nsend; return nsend_total; } #endif