commit 8f4162305f38ccd49baa923c4b33caa4d2ca2707 Author: thorium1256 Date: Mon May 11 15:14:22 2026 +0300 first commit, unfinished wip diff --git a/.clangd b/.clangd new file mode 100644 index 0000000..a98c592 --- /dev/null +++ b/.clangd @@ -0,0 +1,2 @@ +Documentation: + CommentFormat: Doxygen \ No newline at end of file diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..db0cebe --- /dev/null +++ b/Makefile @@ -0,0 +1,44 @@ +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 + +APPLICATION := bbirc +APPLICATION_DBG := bbirc-debug + +$(shell mkdir -p $(BUILD_DIR)) +SRCS := $(wildcard $(SRC_DIR)/*.c) +DBG_OBJS := $(patsubst $(SRC_DIR)/%.c,$(BUILD_DIR)/dbg_%.o,$(SRCS)) +REL_OBJS := $(patsubst $(SRC_DIR)/%.c,$(BUILD_DIR)/rel_%.o,$(SRCS)) + +.PHONY: all debug release clean + +all: release + +debug: $(APPLICATION_DBG) +release: $(APPLICATION) +both: debug release + +$(APPLICATION): $(REL_OBJS) + $(CC) -o $@ $^ $(LDFLAGS_REL) + +$(APPLICATION_DBG): $(DBG_OBJS) + $(CC) -o $@ $^ $(LDFLAGS) + +$(BUILD_DIR)/dbg_%.o: $(SRC_DIR)/%.c + $(CC) -c -o $@ $^ $(CFLAGS_DBG) + +$(BUILD_DIR)/rel_%.o: $(SRC_DIR)/%.c + $(CC) -c -o $@ $^ $(CFLAGS_REL) + +clean: + rm -rf $(BUILD_DIR) + rm -rf $(APPLICATION) + rm -rf $(APPLICATION_DBG) \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..4830d3e --- /dev/null +++ b/README.md @@ -0,0 +1,4 @@ +# BareBones IRC +An attempt at trying to make an IRC client in pure C with sockets. + +**UNFINISHED!!!** \ No newline at end of file diff --git a/TODO.md b/TODO.md new file mode 100644 index 0000000..8e50165 --- /dev/null +++ b/TODO.md @@ -0,0 +1,5 @@ +# To-Do + +1. Add actual IRC client functionality +2. Finish the netcode +3. Don't care about the looks of it yet, just have an ANSI command line or something \ No newline at end of file diff --git a/bbirc b/bbirc new file mode 100755 index 0000000..d1d132e Binary files /dev/null and b/bbirc differ diff --git a/build/rel_IRC.o b/build/rel_IRC.o new file mode 100644 index 0000000..2ae7e2d Binary files /dev/null and b/build/rel_IRC.o differ diff --git a/build/rel_main.o b/build/rel_main.o new file mode 100644 index 0000000..9a86563 Binary files /dev/null and b/build/rel_main.o differ diff --git a/build/rel_netcode.o b/build/rel_netcode.o new file mode 100644 index 0000000..6d2636f Binary files /dev/null and b/build/rel_netcode.o differ diff --git a/compile_commands.json b/compile_commands.json new file mode 100644 index 0000000..4ab23de --- /dev/null +++ b/compile_commands.json @@ -0,0 +1,19 @@ +[ + { + "file": "src/main.c", + "arguments": [ + "/usr/bin/clang", + "-c", + "-o", + "build/rel_main.o", + "src/main.c", + "-I", + "include", + "-Wall", + "-Wextra", + "-O2" + ], + "directory": "/home/neon1246/Documents/stuff/code/c/ircclient", + "output": "build/rel_main.o" + } +] \ No newline at end of file diff --git a/include/IRC.h b/include/IRC.h new file mode 100644 index 0000000..f63cec4 --- /dev/null +++ b/include/IRC.h @@ -0,0 +1,39 @@ +#ifndef IRC_H +#define IRC_H + +typedef struct +{ + char *nickname; + int nicklen; + char *ident; + int identlen; + char *hostname; + int hostlen; + +} irc_hostmask_t; + +typedef struct { + char line[513]; + char *source; + char *command; + char *argv[16]; + int argc; +} irc_message_t; + +typedef struct +{ + int sockfd; + irc_hostmask_t ownHostmask; +} irc_client_t; + +typedef struct { + const char *name; + void (*handler)(irc_message_t *msg, irc_client_t *irc); +} command_t; + +int IRC_ParseMessage(char* line, irc_message_t *msg); +void IRC_ProcessMessage(irc_message_t *msg, irc_client_t *irc); + +void IRC_PRIVMSG(char* msg, irc_client_t irc); + +#endif \ No newline at end of file diff --git a/include/IRC_numerics.h b/include/IRC_numerics.h new file mode 100644 index 0000000..86e7557 --- /dev/null +++ b/include/IRC_numerics.h @@ -0,0 +1,203 @@ +#ifndef IRC_NUMERICS_H +#define IRC_NUMERICS_H + +// * 0xx (initial login information) +#define RPL_WELCOME "001" +#define RPL_YOURHOST "002" +#define RPL_CREATED "003" +#define RPL_MYINFO "004" +#define RPL_ISUPPORT "005" +#define RPL_BOUNCE "010" /* deprecated */ + +// * 2xx (request information) + +/* replies to STATS */ +#define RPL_STATSCOMMANDS "212" +#define RPL_ENDOFSTATS "219" +#define RPL_STATSUPTIME "242" + +/* server user modes on-connect */ +#define RPL_UMODEIS "221" + +/* replies to LUSERS */ +#define RPL_LUSERCLIENT "251" +#define RPL_LUSEROP "252" +#define RPL_LUSERUNKNOWN "253" +#define RPL_LUSERCHANNELS "254" +#define RPL_LUSERME "255" +#define RPL_LOCALUSERS "265" +#define RPL_GLOBALUSERS "266" + +/* replies to ADMIN */ +#define RPL_ADMINME "256" +#define RPL_ADMINLOC1 "257" +#define RPL_ADMINLOC2 "258" +#define RPL_ADMINEMAIL "259" + +/* generic replies (i.e. ratelimiting) */ +#define RPL_TRYAGAIN "263" + +/* WHOIS info */ +#define RPL_WHOISCERTFP "276" + +// * 3xx (information for clients) +#define RPL_NONE "300" + +/* replies to WHOIS/WHOWAS */ +#define RPL_AWAY "301" +#define RPL_WHOISREGNICK "307" +#define RPL_WHOISUSER "311" +#define RPL_WHOISSERVER "312" +#define RPL_WHOISOPERATOR "313" +#define RPL_WHOWASUSER "314" +#define RPL_WHOISIDLE "317" +#define RPL_WHOISCHANNELS "319" +#define RPL_WHOISSPECIAL "320" +#define RPL_WHOISACCOUNT "330" +#define RPL_WHOISACTUALLY "338" +#define RPL_WHOISHOST "378" +#define RPL_WHOISMODES "379" +#define RPL_ENDOFWHOIS "318" +#define RPL_ENDOFWHOWAS "369" +#define ERR_WASNOSUCHNICK "406" /* WHOWAS error */ +#define RPL_WHOISSECURE "671" + +/* replies to WHO */ +#define RPL_WHOREPLY "352" +#define RPL_ENDOFWHO "315" + +/* replies to LIST */ +#define RPL_LISTSTART "321" +#define RPL_LIST "322" +#define RPL_LISTEND "323" + +/* upon joining a channel */ +#define RPL_CHANNELMODEIS "324" +#define RPL_CREATIONTIME "329" +#define RPL_TOPIC "332" +#define RPL_TOPICWHOTIME "333" + +/* replies to TOPIC */ +#define RPL_NOTOPIC "331" + +/* replies to INVITE */ +#define RPL_INVITELIST "336" +#define RPL_ENDOFINVITELIST "337" +#define RPL_INVITING "341" + +/* replies to MODE */ +#define RPL_INVEXLIST "346" +#define RPL_ENDOFINVEXLIST "347" +#define RPL_EXCEPTLIST "348" +#define RPL_ENDOFEXCEPTLIST "349" +#define RPL_BANLIST "367" +#define RPL_ENDOFBANLIST "368" +#define ERR_INVALIDMODEPARAM "696" + +/* replies to INFO */ +#define RPL_INFO "371" +#define RPL_ENDOFINFO "374" + +/* when sending a Message of the Day */ +#define RPL_MOTDSTART "375" +#define RPL_MOTD "372" +#define RPL_ENDOFMOTD "376" + +/* replies to VERSION */ +#define RPL_VERSION "351" + +/* replies to NAMES */ +#define RPL_NAMREPLY "353" +#define RPL_ENDOFNAMES "366" + +/* replies to LINKS */ +#define RPL_LINKS "364" +#define RPL_ENDOFLINKS "365" + +/* replies to OPER */ +#define RPL_YOUREOPER "381" + +/* replies to REHASH */ +#define RPL_REHASHING "382" + +/* replies to TIME */ +#define RPL_TIME "391" + +/* replies to USERHOST */ +#define RPL_USERHOST "302" + +/* sent to clients to inform of away status */ +#define RPL_UNAWAY "305" +#define RPL_NOWAWAY "306" + +// * 4xx and 5xx (errors) + +#define ERR_UNKNOWNERROR "400" +#define ERR_NOSUCHNICK "401" +#define ERR_NOSUCHSERVER "402" +#define ERR_NOSUCHCHANNEL "403" +#define ERR_CANNOTSENDTOCHAN "404" +#define ERR_TOOMANYCHANNELS "405" +#define ERR_NOORIGIN "409" +#define ERR_NORECIPIENT "411" +#define ERR_NOTEXTTOSEND "412" +#define ERR_INPUTTOOLONG "417" +#define ERR_UNKNOWNCOMMAND "421" +#define ERR_NOMOTD "422" +#define ERR_NONICKNAMEGIVEN "431" +#define ERR_ERRONEUSNICKNAME "432" +#define ERR_NICKNAMEINUSE "433" +#define ERR_NICKCOLLISION "436" +#define ERR_USERNOTINCHANNEL "441" +#define ERR_NOTONCHANNEL "442" +#define ERR_USERONCHANNEL "443" +#define ERR_NOTREGISTERED "451" +#define ERR_NEEDMOREPARAMS "461" +#define ERR_ALREADYREGISTERED "462" +#define ERR_PASSWDMISMATCH "464" +#define ERR_YOUREBANNEDCREEP "465" +#define ERR_CHANNELISFULL "471" +#define ERR_UNKNOWNMODE "472" +#define ERR_INVITEONLYCHAN "473" +#define ERR_BANNEDFROMCHAN "474" +#define ERR_BADCHANNELKEY "475" +#define ERR_BADCHANMASK "476" +#define ERR_NOPRIVILEGES "481" +#define ERR_CHANOPRIVSNEEDED "482" +#define ERR_CANTKILLSERVER "483" +#define ERR_NOOPERHOST "491" +#define ERR_UMODEUNKNOWNFLAG "501" +#define ERR_USERSDONTMATCH "502" +#define ERR_INVALIDKEY "525" + +/* replies to HELP */ +#define ERR_HELPNOTFOUND "524" +#define RPL_HELPSTART "704" +#define RPL_HELPTXT "705" +#define RPL_ENDOFHELP "706" + +// * 6xx (various TLS and IRCv3 stuff) + +/* STARTTLS stuff */ +#define RPL_STARTTLS "670" +#define ERR_STARTTLS "691" + +// * 7xx (various) + +#define ERR_NOPRIVS "723" + +// * 9xx (authentication) + +/* SASL */ +#define RPL_LOGGEDIN "900" +#define RPL_LOGGEDOUT "901" +#define RPL_SASLSUCCESS "903" +#define ERR_NICKLOCKED "902" +#define ERR_SASLFAIL "904" +#define ERR_SASLTOOLONG "905" +#define ERR_SASLABORTED "906" +#define ERR_SASLALREADY "907" +#define RPL_SASLMECHS "908" + + +#endif \ No newline at end of file diff --git a/include/defines.h b/include/defines.h new file mode 100644 index 0000000..de8f5ed --- /dev/null +++ b/include/defines.h @@ -0,0 +1,6 @@ +#ifndef DEFINES_H +#define DEFINES_H + +#define NUL '\0' + +#endif \ No newline at end of file diff --git a/include/netcode.h b/include/netcode.h new file mode 100644 index 0000000..c4bafb2 --- /dev/null +++ b/include/netcode.h @@ -0,0 +1,9 @@ +#ifndef NETCODE_H +#define NETCODE_H + +// Returns a socket file descriptor. +int NET_Connect(const char* host, int port); +void NET_Send(int sockfd, const char* toSend); +void NET_Close(int sockfd); + +#endif \ No newline at end of file diff --git a/src/IRC.c b/src/IRC.c new file mode 100644 index 0000000..e25fe59 --- /dev/null +++ b/src/IRC.c @@ -0,0 +1,92 @@ +#include "IRC.h" +#include "defines.h" +#include "netcode.h" +#include +#include +#include + +int IRC_ParseMessage(char *line, irc_message_t *msg) +{ + strncpy(msg->line, line, sizeof(msg->line) - 1); + char *ptr = msg->line; + ptr[sizeof(msg->line)] = NUL; + + char *source = NULL; + char *command = NULL; + + // get the source, if it exists (the : at the beginning of an IRC message) + if(*ptr == ':') + { + ptr++; + source = ptr; + + while (*ptr && *ptr != ' ') ptr++; + if (*ptr) *ptr++ = NUL; + + while (*ptr == ' ') ptr++; // skip any extra spaces + } + + msg->source = source; + + // read the command + command = ptr; + + while(*ptr && *ptr != ' ') ptr++; + if (*ptr) *ptr++ = NUL; + + msg->command = command; + + while (*ptr == ' ') ptr++; + + // read its arguments (until we encounter a colon/nullbyte) + msg->argc = 0; + + while(*ptr && msg->argc <= 15) + { + if(*ptr == ':') // if it's a trailing arg + { + ptr++; + msg->argv[msg->argc] = ptr; + msg->argc++; + break; + } + + msg->argv[msg->argc] = ptr; + + while(*ptr && *ptr != ' ') ptr++; + + if (*ptr) + { + *ptr++ = NUL; + while (*ptr == ' ') ptr++; // skip spaces for next arg + } + + msg->argc++; + } + + return 0; +} + +static void handlePing(irc_message_t *msg, irc_client_t *irc) +{ + char buf[128]; + snprintf(buf, sizeof buf, "PONG :%s", msg->argv[0]); + NET_Send(irc->sockfd, buf); +} + +void IRC_ProcessMessage(irc_message_t *msg, irc_client_t *irc) +{ + command_t commands[] = + { + {"PING", handlePing}, + {NULL, NULL} + }; + + for(int i = 0; commands[i].name; i++) + { + if(strcmp(msg->command, commands[i].name) == 0) + { + commands[i].handler(msg, irc); + } + } +} \ No newline at end of file diff --git a/src/main.c b/src/main.c new file mode 100644 index 0000000..e596468 --- /dev/null +++ b/src/main.c @@ -0,0 +1,105 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "IRC.h" +#include "defines.h" +#include "netcode.h" + +int main(int argc, char **argv) +{ + int sockfd = NET_Connect("irc.libera.chat", 6667); + + fd_set readfds; + static char linebuf[1026]; + static size_t lineLen = 0; + char recvline[513]; + char sendline[513]; + + irc_client_t irc; + irc.sockfd = sockfd; + + if(sockfd >= 0) + { + NET_Send(sockfd, "NICK BareBonesDude\r\n"); + NET_Send(sockfd, "USER BareBonesDude 0 * :The Postal Dudesksleton\r\n"); + } + else + { + return 1; + } + + while(1) + { + FD_ZERO(&readfds); + FD_SET(STDIN_FILENO, &readfds); + FD_SET(sockfd, &readfds); + + int maxfd = (sockfd > 0) ? sockfd + 1 : 1; + + select(maxfd, &readfds, NULL, NULL, NULL); + + if(FD_ISSET(STDIN_FILENO, &readfds)) + { + fgets(sendline, sizeof(sendline), stdin); + NET_Send(sockfd, sendline); + } + + if(FD_ISSET(sockfd, &readfds)) + { + ssize_t n = recv(sockfd, recvline, sizeof(recvline), 0); + + if(n <= 0) break; + recvline[n] = NUL; + + for(ssize_t i = 0; i < n; i++) + { + linebuf[lineLen++] = recvline[i]; + + if(lineLen >= 2 && linebuf[lineLen - 2] == '\r' && linebuf[lineLen - 1] == '\n') + { + // we found a complete line + linebuf[lineLen] = NUL; + + // parse the message + irc_message_t msg; + IRC_ParseMessage(linebuf, &msg); + + // try to print it + + if(msg.source != NULL) + { + printf("%s: %s ", msg.source, msg.command); + } + else + { + printf("Server: %s ", msg.command); + } + + for(int i = 0; i < msg.argc; i++) + { + printf("%s ", msg.argv[i]); + } + + lineLen = 0; + } + + if(lineLen >= sizeof(linebuf) - 1) + { + lineLen = 0; + } + } + } + } + + printf("Connection closed by foreign host.\n"); + + return 0; +} \ No newline at end of file diff --git a/src/netcode.c b/src/netcode.c new file mode 100644 index 0000000..a5eebaf --- /dev/null +++ b/src/netcode.c @@ -0,0 +1,123 @@ +#include "netcode.h" + +#include +#include +#include +#include +#include +#include +#include +#include + +int NET_Connect(const char *host, int port) +{ + int status; + struct addrinfo hints, *res; + + memset(&hints, 0, sizeof hints); + hints.ai_family = AF_UNSPEC; + hints.ai_socktype = SOCK_STREAM; + + // save us from user or programmer stupidity + if(!host) + { + return -1; + } + + char portNumber[6]; + snprintf(portNumber, 6, "%d", port); + + printf("Looking up %s...\n", host); + + if ((status = getaddrinfo(host, portNumber, &hints, &res)) == -1) + { + fprintf(stderr, "Unable to lookup %s: %s\n", host, gai_strerror(status)); + return -1; + } + + char ipstr[INET6_ADDRSTRLEN]; + int sockfd = -1; + + for(struct addrinfo *p = res; p != NULL; p = p->ai_next) + { + void *addr; + struct sockaddr_in *ipv4; + struct sockaddr_in6 *ipv6; + + if(p->ai_family == AF_INET) + { + ipv4 = (struct sockaddr_in *)p->ai_addr; + addr = &(ipv4->sin_addr); + } + else + { + ipv6 = (struct sockaddr_in6 *)p->ai_addr; + addr = &(ipv6->sin6_addr); + } + + inet_ntop(p->ai_family, addr, ipstr, sizeof ipstr); + printf("Connecting to %s (%s)...\n", host, ipstr); + + if((sockfd = socket(p->ai_family, p->ai_socktype, p->ai_protocol)) == -1) + { + char perrorMsg[sizeof("Failed to connect to ") + sizeof ipstr]; + snprintf(perrorMsg, sizeof perrorMsg, "Failed to connect to %s", ipstr); + perror(perrorMsg); + continue; + } + + if((connect(sockfd, p->ai_addr, p->ai_addrlen)) == -1) + { + close(sockfd); + char perrorMsg[sizeof("Failed to connect to ") + sizeof ipstr]; + snprintf(perrorMsg, sizeof perrorMsg, "Failed to connect to %s", ipstr); + perror(perrorMsg); + continue; + } + + printf("Connected!\n"); + break; + } + + if(sockfd >= 0) + { + return sockfd; + } + else + { + return -1; + } +} + +void NET_Send(int sockfd, const char *toSend) +{ + size_t bytesSent = 0; + size_t sendLen = strlen(toSend); + + while(bytesSent < sendLen) + { + ssize_t sent = send(sockfd, toSend, sendLen - bytesSent, 0); + if(sent == -1) + { + perror("NET_Send"); + return; + } + + if(sent == 0) + { + fprintf(stderr, "NET_Send: connection closed"); + return; + } + + bytesSent += sent; + } +} + +void NET_Close(int sockfd) +{ + if(sockfd >= 0) + { + shutdown(sockfd, SHUT_RDWR); + close(sockfd); + } +} \ No newline at end of file