From da3aef1468f3916d9fc7d99c5f4f71e05971c583 Mon Sep 17 00:00:00 2001 From: thorium1256 Date: Thu, 14 May 2026 16:35:10 +0300 Subject: [PATCH] finalize SASL, move all IRC-related includes to a specific folder --- .gitignore | 3 +- Makefile | 17 ++-- compile_commands.json | 59 +++++++++++++ include/{ => IRC}/IRC.h | 7 +- include/{ => IRC}/IRC_numerics.h | 0 include/{ => IRC}/IRC_structs.h | 6 +- include/base64.h | 9 ++ src/IRC.c | 147 +++++++++++++++++++++++++++---- src/base64.c | 34 +++++++ src/main.c | 31 ++++--- src/netcode.c | 2 + test | Bin 16176 -> 16096 bytes 12 files changed, 272 insertions(+), 43 deletions(-) rename include/{ => IRC}/IRC.h (75%) rename include/{ => IRC}/IRC_numerics.h (100%) rename include/{ => IRC}/IRC_structs.h (94%) create mode 100644 include/base64.h create mode 100644 src/base64.c diff --git a/.gitignore b/.gitignore index 2a5c394..e9e2d45 100644 --- a/.gitignore +++ b/.gitignore @@ -3,4 +3,5 @@ build bbirc-debug bbirc tests -compile_commands.json \ No newline at end of file +compile_commands.json +privinclude \ No newline at end of file diff --git a/Makefile b/Makefile index db0cebe..c6374ad 100644 --- a/Makefile +++ b/Makefile @@ -1,14 +1,15 @@ -CC := clang -CFLAGS := -I include -Wall -Wextra -CFLAGS_DBG := $(CFLAGS) -g -O0 -CFLAGS_REL := $(CFLAGS) -O2 - -LDFLAGS := -LDFLAGS_REL := $(LDFLAGS) -s - BUILD_DIR := build SRC_DIR := src INCLUDE_DIR := include +PRIVATE_INCLUDE_DIR := privinclude + +CC := clang +CFLAGS := -I $(INCLUDE_DIR) -I $(PRIVATE_INCLUDE_DIR) -Wall -Wextra +CFLAGS_DBG := $(CFLAGS) -g -O0 +CFLAGS_REL := $(CFLAGS) -O2 + +LDFLAGS := +LDFLAGS_REL := $(LDFLAGS) -s APPLICATION := bbirc APPLICATION_DBG := bbirc-debug diff --git a/compile_commands.json b/compile_commands.json index 4ab23de..9f9242b 100644 --- a/compile_commands.json +++ b/compile_commands.json @@ -1,4 +1,42 @@ [ + { + "file": "src/IRC.c", + "arguments": [ + "/usr/bin/clang", + "-c", + "-o", + "build/rel_IRC.o", + "src/IRC.c", + "-I", + "include", + "-I", + "privinclude", + "-Wall", + "-Wextra", + "-O2" + ], + "directory": "/home/neon1246/Documents/stuff/code/c/ircclient", + "output": "build/rel_IRC.o" + }, + { + "file": "src/base64.c", + "arguments": [ + "/usr/bin/clang", + "-c", + "-o", + "build/rel_base64.o", + "src/base64.c", + "-I", + "include", + "-I", + "privinclude", + "-Wall", + "-Wextra", + "-O2" + ], + "directory": "/home/neon1246/Documents/stuff/code/c/ircclient", + "output": "build/rel_base64.o" + }, { "file": "src/main.c", "arguments": [ @@ -9,11 +47,32 @@ "src/main.c", "-I", "include", + "-I", + "privinclude", "-Wall", "-Wextra", "-O2" ], "directory": "/home/neon1246/Documents/stuff/code/c/ircclient", "output": "build/rel_main.o" + }, + { + "file": "src/netcode.c", + "arguments": [ + "/usr/bin/clang", + "-c", + "-o", + "build/rel_netcode.o", + "src/netcode.c", + "-I", + "include", + "-I", + "privinclude", + "-Wall", + "-Wextra", + "-O2" + ], + "directory": "/home/neon1246/Documents/stuff/code/c/ircclient", + "output": "build/rel_netcode.o" } ] \ No newline at end of file diff --git a/include/IRC.h b/include/IRC/IRC.h similarity index 75% rename from include/IRC.h rename to include/IRC/IRC.h index d1c35a4..8e6f00e 100644 --- a/include/IRC.h +++ b/include/IRC/IRC.h @@ -2,6 +2,7 @@ #define IRC_H #include "IRC_structs.h" +#include "IRC_numerics.h" void IRC_ParseMessage(char* line, irc_message_t *msg); void IRC_ProcessMessage(irc_message_t *msg, irc_client_t *irc); @@ -13,7 +14,10 @@ void IRC_NICK(char *nick, irc_client_t *irc); void IRC_USER(char *ident, char* realname, irc_client_t *irc); // registration -void IRC_Register(char *nick, char* ident, char *realname, bool saslEnabled, irc_sasl_mechanism_t saslMethod, char *saslUsername, char *saslPassword, irc_client_t *irc); +void IRC_Register(char *nick, char *username, char *realname, irc_client_t *irc); +void IRC_SetSASLParameters(irc_sasl_mechanism_t, char* username, char* password, irc_client_t *irc); + +void IRC_RequestSASLAuthentication(irc_client_t *irc); // Capability negotiation block void IRC_StartCapabilityNegotiation(irc_client_t *irc); @@ -22,6 +26,7 @@ void IRC_EndCapabilityNegotiation(irc_client_t *irc); void IRC_NeedCapabilities(char* caps, irc_client_t *irc); + int IRC_ParseCapabilities(char* caps, irc_capability_t *capabilities, int capsLength); #endif \ No newline at end of file diff --git a/include/IRC_numerics.h b/include/IRC/IRC_numerics.h similarity index 100% rename from include/IRC_numerics.h rename to include/IRC/IRC_numerics.h diff --git a/include/IRC_structs.h b/include/IRC/IRC_structs.h similarity index 94% rename from include/IRC_structs.h rename to include/IRC/IRC_structs.h index 1b5dd2b..05c05f9 100644 --- a/include/IRC_structs.h +++ b/include/IRC/IRC_structs.h @@ -73,13 +73,9 @@ typedef struct char supportedCapabilities[512]; char requestedCapabilities[512]; irc_capability_t acked[24]; - struct - { - bool saslWorks; - } capabilities; /* Authentication parameters */ - irc_auth_t sasl_auth; + irc_auth_t saslAuth; } irc_client_t; typedef struct diff --git a/include/base64.h b/include/base64.h new file mode 100644 index 0000000..f39aa98 --- /dev/null +++ b/include/base64.h @@ -0,0 +1,9 @@ +#ifndef BASE64_H +#define BASE64_H + +#include +#include + +int base64_encode(const uint8_t *in, size_t in_len, char *out, size_t out_size); + +#endif \ No newline at end of file diff --git a/src/IRC.c b/src/IRC.c index 0b85149..046cadc 100644 --- a/src/IRC.c +++ b/src/IRC.c @@ -1,10 +1,14 @@ -#include "IRC.h" +#include "IRC/IRC_numerics.h" +#include "IRC/IRC_structs.h" #include "defines.h" #include "netcode.h" #include #include #include +#include "IRC/IRC.h" +#include "base64.h" + void IRC_ParseMessage(char *line, irc_message_t *msg) { strncpy(msg->line, line, sizeof(msg->line) - 1); @@ -63,11 +67,27 @@ void IRC_ParseMessage(char *line, irc_message_t *msg) msg->argc++; } + + if (msg->argc > 0) { + char *last = msg->argv[msg->argc - 1]; + size_t len = strlen(last); + while (len > 0 && (last[len-1] == '\r' || last[len-1] == '\n')) { + last[len-1] = '\0'; + len--; + } + } } // Returns the number of capabilities found. int IRC_ParseCapabilities(char *caps, irc_capability_t *capabilities, int capsLength) { + while (*caps == ' ') caps++; + + size_t len = strlen(caps); + while (len > 0 && (caps[len-1] == '\n' || caps[len-1] == '\r' || caps[len-1] == ' ')) { + caps[len-1] = '\0'; + len--; + } char capBak[512]; strncpy(capBak, caps, sizeof(capBak) - 1); // copy the capline so we don't destroy our parameter capBak[sizeof(capBak) - 1] = 0; @@ -123,16 +143,16 @@ int IRC_ParseCapabilities(char *caps, irc_capability_t *capabilities, int capsLe static void handlePing(irc_message_t *msg, irc_client_t *irc) { char buf[128]; // most pingpongs are usually short, with the servername being short too - snprintf(buf, sizeof buf, "PONG :%s", msg->argv[0]); + snprintf(buf, sizeof buf, "PONG :%s\r\n", msg->argv[0]); NET_Send(irc->sockfd, buf); } static void handleCapabilities(irc_message_t *msg, irc_client_t *irc) { // we are willing to tolerate some spaghetti... - if(strcmp(msg->argv[2], "LS") == 0) + if(strcmp(msg->argv[1], "LS") == 0) { - snprintf(irc->supportedCapabilities, sizeof irc->supportedCapabilities, "%s", msg->argv[3]); + snprintf(irc->supportedCapabilities, sizeof irc->supportedCapabilities, "%s", msg->argv[2]); // parse the caps irc_capability_t caps[24]; @@ -149,7 +169,7 @@ static void handleCapabilities(irc_message_t *msg, irc_client_t *irc) { for(int j = 0; j < reqCapCount; j++) { - if(strcmp(caps[i].name, reqCaps[j].name) == 0 == 0) + if(strcmp(caps[i].name, reqCaps[j].name) == 0) { bytesWritten += snprintf(buf + bytesWritten, sizeof(buf) - bytesWritten, "%s ", reqCaps[j].name); } @@ -158,15 +178,68 @@ static void handleCapabilities(irc_message_t *msg, irc_client_t *irc) IRC_RequestCapabilities(buf, irc); } - else if (strcmp(msg->argv[2], "ACK")) + else if (strcmp(msg->argv[1], "ACK") == 0) { - int ackedCount = IRC_ParseCapabilities(msg->argv[3], irc->acked, sizeof(irc->acked) / sizeof(irc_capability_t)); + int ackedCount = IRC_ParseCapabilities(msg->argv[2], irc->acked, sizeof(irc->acked) / sizeof(irc_capability_t)); for(int i = 0; i < ackedCount; i++) { - + printf("%s = %s\n", irc->acked[i].name, irc->acked[i].args ? irc->acked[i].args : "(none)"); + if(strcmp(irc->acked[i].name, "sasl") == 0) + { + IRC_RequestSASLAuthentication(irc); + } } } + // NAK is a no-op anyway, if it isn't in our irc->acked we just won't use it then. +} + +static void handleAuthentication(irc_message_t *msg, irc_client_t *irc) +{ + if(strcmp(msg->argv[0], "+") == 0) + { + // send our base64 sasl username and password + switch(irc->saslAuth.mechanism) + { + case SASL_MECHANISM_NONE: + break; + case SASL_MECHANISM_PLAIN: + { + const char *user = irc->saslAuth.plain_credentials.username; + const char *pass = irc->saslAuth.plain_credentials.password; + size_t user_len = strlen(user); + size_t pass_len = strlen(pass); + + uint8_t sasl_data[user_len + pass_len + 3]; + size_t idx = 0; + + sasl_data[idx++] = 0; + memcpy(sasl_data + idx, user, user_len); + idx += user_len; + sasl_data[idx++] = 0; + memcpy(sasl_data + idx, pass, pass_len); + idx += pass_len; + + char base64Out[512]; + base64_encode(sasl_data, idx, base64Out, sizeof(base64Out)); + char buf[512]; + snprintf(buf, sizeof buf, "AUTHENTICATE %s\r\n", base64Out); + NET_Send(irc->sockfd, buf); + break; + } + case SASL_MECHANISM_EXTERNAL: + { + NET_Send(irc->sockfd, "AUTHENTICATE +\r\n"); + break; + } + } + + } +} + +static void handleSaslSuccess(irc_message_t *msg, irc_client_t *irc) +{ + IRC_EndCapabilityNegotiation(irc); } /* @@ -179,6 +252,8 @@ void IRC_ProcessMessage(irc_message_t *msg, irc_client_t *irc) { {"PING", handlePing}, {"CAP", handleCapabilities}, + {"AUTHENTICATE", handleAuthentication}, + {RPL_SASLSUCCESS, handleSaslSuccess}, {NULL, NULL} }; @@ -194,47 +269,67 @@ void IRC_ProcessMessage(irc_message_t *msg, irc_client_t *irc) void IRC_PRIVMSG(char *to, char *msg, irc_client_t *irc) { char buf[512]; - snprintf(buf, sizeof buf, "PRIVMSG %s :%s", to, msg); + snprintf(buf, sizeof buf, "PRIVMSG %s :%s\r\n", to, msg); NET_Send(irc->sockfd, buf); } void IRC_NOTICE(char *to, char *msg, irc_client_t *irc) { char buf[512]; - snprintf(buf, sizeof buf, "NOTICE %s :%s", to, msg); + snprintf(buf, sizeof buf, "NOTICE %s :%s\r\n", to, msg); NET_Send(irc->sockfd, buf); } void IRC_NICK(char *newNick, irc_client_t *irc) { char buf[512]; - snprintf(buf, sizeof buf, "NICK %s", newNick); + snprintf(buf, sizeof buf, "NICK %s\r\n", newNick); NET_Send(irc->sockfd, buf); } void IRC_USER(char *ident, char *realname, irc_client_t *irc) { char buf[512]; - snprintf(buf, sizeof buf, "USER %s 0 * :%s", ident, realname); + snprintf(buf, sizeof buf, "USER %s 0 * :%s\r\n", ident, realname); NET_Send(irc->sockfd, buf); } -void IRC_REGISTER(char *nick, char *ident, char *realname, irc_client_t *irc) +void IRC_Register(char *nick, char *username, char *realname, irc_client_t *irc) { - IRC_StartCapabilityNegotiation(irc); + if(irc->saslAuth.mechanism != SASL_MECHANISM_NONE) + { + IRC_NeedCapabilities("sasl", irc); + IRC_StartCapabilityNegotiation(irc); + } IRC_NICK(nick, irc); - IRC_USER(ident, realname, irc); + IRC_USER(username, realname, irc); +} + +void IRC_SetSASLParameters(irc_sasl_mechanism_t mechanism, char *username, char *password, irc_client_t *irc) +{ + switch(mechanism) + { + case SASL_MECHANISM_NONE: + break; + case SASL_MECHANISM_EXTERNAL: + // unimplemented + break; + case SASL_MECHANISM_PLAIN: + irc->saslAuth.mechanism = mechanism; + strncpy(irc->saslAuth.plain_credentials.username, username, sizeof(irc->saslAuth.plain_credentials.username) - 1); + strncpy(irc->saslAuth.plain_credentials.password, password, sizeof(irc->saslAuth.plain_credentials.password) - 1); + } } void IRC_StartCapabilityNegotiation(irc_client_t *irc) { - NET_Send(irc->sockfd, "CAP LS 302"); + NET_Send(irc->sockfd, "CAP LS 302\r\n"); } void IRC_RequestCapabilities(char *caps, irc_client_t *irc) { char buf[512]; - snprintf(buf, sizeof buf, "CAP REQ :%s", caps); + snprintf(buf, sizeof buf, "CAP REQ :%s\r\n", caps); NET_Send(irc->sockfd, buf); } @@ -245,5 +340,21 @@ void IRC_NeedCapabilities(char *caps, irc_client_t *irc) void IRC_EndCapabilityNegotiation(irc_client_t *irc) { - NET_Send(irc->sockfd, "CAP END"); + NET_Send(irc->sockfd, "CAP END\r\n"); +} + +void IRC_RequestSASLAuthentication(irc_client_t *irc) +{ + switch(irc->saslAuth.mechanism) + { + case SASL_MECHANISM_NONE: + // why? + break; + case SASL_MECHANISM_PLAIN: + NET_Send(irc->sockfd, "AUTHENTICATE PLAIN\r\n"); + break; + case SASL_MECHANISM_EXTERNAL: + NET_Send(irc->sockfd, "AUTHENTICATE EXTERNAL\r\n"); + break; + } } \ No newline at end of file diff --git a/src/base64.c b/src/base64.c new file mode 100644 index 0000000..1cc42ab --- /dev/null +++ b/src/base64.c @@ -0,0 +1,34 @@ +#include +#include +#include +#include "base64.h" +#include + +static const char b64[] = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; +int base64_encode(const uint8_t *in, size_t in_len, char *out, size_t out_size) +{ + size_t needed = ((in_len + 2) / 3) * 4 + 1; + + if(out_size < needed) return -1; + + size_t i = 0, o = 0; + while(i < in_len) + { + uint32_t n = 0; + int remaining = 3; + + n |= (i < in_len ? in[i++] : 0) << 16; + n |= (i < in_len ? in[i++] : 0) << 8; + n |= (i < in_len ? in[i++] : 0); + + if(i > in_len) remaining = in_len % 3; + + out[o++] = b64[(n >> 18) & 63]; + out[o++] = b64[(n >> 12) & 63]; + out[o++] = remaining > 1 ? b64[(n >> 6) & 63] : '='; + out[o++] = remaining > 2 ? b64[n & 63] : '='; + } + + out[o] = 0; + return o; +} \ No newline at end of file diff --git a/src/main.c b/src/main.c index e971429..c78b911 100644 --- a/src/main.c +++ b/src/main.c @@ -9,10 +9,13 @@ #include #include -#include "IRC.h" +#include "IRC/IRC.h" +#include "IRC/IRC_structs.h" #include "defines.h" #include "netcode.h" +#include "credentials.h" + int main(int argc, char **argv) { int sockfd = NET_Connect("irc.libera.chat", 6667); @@ -28,8 +31,8 @@ int main(int argc, char **argv) if(sockfd >= 0) { - NET_Send(sockfd, "NICK BareBonesDude\r\n"); - NET_Send(sockfd, "USER BareBonesDude 0 * :The Postal Dudesksleton\r\n"); + IRC_SetSASLParameters(SASL_MECHANISM_PLAIN, CREDENTIALS_USERNAME, CREDENTIALS_PASSWORD, &irc); + IRC_Register("BareBonesDude", "bbirc", "A Barebones IRC (https://gitea.codersquack.nl/thorium1256/bbirc) test", &irc); } else { @@ -72,24 +75,32 @@ int main(int argc, char **argv) irc_message_t msg; IRC_ParseMessage(linebuf, &msg); - IRC_ProcessMessage(&msg, &irc); - - // try to print it - if(msg.source != NULL) { - printf("%s: %s ", msg.source, msg.command); + printf("< %s: %s ", msg.source, msg.command); } else { - printf("Server: %s ", msg.command); + printf("< %s ", msg.command); } for(int i = 0; i < msg.argc; i++) { - printf("%s ", msg.argv[i]); + if(i == msg.argc-1) + { + printf("%s\n", msg.argv[i]); + } + else + { + printf("%s ", msg.argv[i]); + } } + + IRC_ProcessMessage(&msg, &irc); + + // try to print it + lineLen = 0; } diff --git a/src/netcode.c b/src/netcode.c index a5eebaf..1f07faf 100644 --- a/src/netcode.c +++ b/src/netcode.c @@ -94,6 +94,8 @@ void NET_Send(int sockfd, const char *toSend) size_t bytesSent = 0; size_t sendLen = strlen(toSend); + printf("> %s", toSend); + while(bytesSent < sendLen) { ssize_t sent = send(sockfd, toSend, sendLen - bytesSent, 0); diff --git a/test b/test index ca261703a37e0fc696abbc83393a1c181397e38f..2fe51c51682260b352b3aad67e4c555c3ffb3625 100755 GIT binary patch delta 2512 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 N9GcDdz;M-y{{TBZh=c$D