ref: b37209a58eb37c8964c35755df501e4a29789935
author: Nima <nimaa@tuta.io>
date: Thu Oct 13 14:20:36 EDT 2022
Initial Commit
--- /dev/null
+++ b/Makefile
@@ -1,0 +1,2 @@
+all:
+ cc -I. net-posix.c irc.c utils.c examples/echo_bot.c
--- /dev/null
+++ b/README.txt
@@ -1,0 +1,10 @@
+This "library" tries to make the process of writing irc client programs
+easier. it is not designed to be comperhensive, but rather designed to be
+easily expandable. it was originally written for an irc game bot and that
+required very little and basic usage of the irc protocol. but you can easily
+add more funcitonality to it, if you need to.
+
+see examples/echo_bot.c for details of how to use the library
+see irc.c and irc.h for details of how to expand the library
+see net.h if you need to port libirc to other platforms/networking APIs
+
--- /dev/null
+++ b/examples/echo_bot.c
@@ -1,0 +1,53 @@
+#include <stdio.h>
+#include <string.h>
+#include "irc.h"
+#include "utils.h"
+
+int invite_handler(irc_session_t* s, irc_msg_t* msg) {
+ udebug("got invited to channel %s, joining the channel.", IRC_INVITE_CHANNEL(msg));
+
+ irc_send_join(s, IRC_INVITE_CHANNEL(msg));
+ return 0;
+}
+
+int privmsg_handler(irc_session_t* s, irc_msg_t* msg) {
+ udebug("privmsg_handler(): message from %s : %s", IRC_PRIVMSG_ORIGIN(msg), IRC_PRIVMSG_MSG(msg));
+
+ irc_send_privmsg(s, IRC_PRIVMSG_ORIGIN(msg), IRC_PRIVMSG_MSG(msg));
+ return 0;
+}
+
+int ping_handler(irc_session_t* s, irc_msg_t* msg) {
+ // libirc handles PINGs itself, but we can use this handler for debuugin purposes maybe
+ udebug("PING %s", IRC_PING_PARAMETER(msg));
+ return 0;
+}
+
+int main() {
+ irc_session_t sess;
+ irc_event_handler_set_t es;
+
+ memset(&es, 0, sizeof(es)); /* it is very important that all the unused event handlers are set to NULL */
+ es.invite_handler = invite_handler;
+ es.privmsg_handler = privmsg_handler;
+ es.ping_handler = ping_handler;
+
+ irc_init_session(&sess, /* session */
+ "bsdforall.org", /* server */
+ "6667", /* port */
+ "mister_echo", /* nick name */
+ NULL, /* password */
+ &es); /* event handlers */
+ if (irc_connect(&sess)) {
+ uerror("irc_connect() failed.");
+ return -1;
+ }
+
+ irc_main_loop(&sess); /* this blocks, libirc doesnt suppport multiple
+ sessions, however, nothing's stopping you
+ from applying your own threading
+ mechanism to it. libirc has no global state and thus, you
+ can have DIFFERENT IRC SESSIONS in different threads without
+ any syncrhonization.*/
+}
+
--- /dev/null
+++ b/irc.c
@@ -1,0 +1,329 @@
+#include <stdio.h>
+#include <stdlib.h>
+#include <errno.h>
+#include <assert.h>
+#include <string.h>
+#include <stdint.h>
+#include <memory.h>
+#include <stdarg.h>
+#include "irc.h"
+#include "utils.h"
+#include "net.h"
+
+void die(const char* msg) {
+ perror(msg);
+ exit(EXIT_FAILURE);
+}
+
+
+int irc_init_session(irc_session_t* s, const char* serv, const char* portno, const char* nick, const char* pass, irc_event_handler_set_t* es) {
+
+ memset(s, 0, sizeof(irc_session_t));
+
+ if (serv == NULL || portno == NULL || nick == NULL)
+ return -1;
+ s->server = strdup(serv);
+ s->portno = strdup(portno);
+ s->nick = strdup(nick);
+ s->password = pass? strdup(pass) : NULL;
+ s->buffer = (char *)malloc(IRC_MAX_BUFFER);
+ s->pos = 0;
+ s->event_handlers = *es;
+ assert(s->buffer);
+
+ s->connected = 0;
+ return 0;
+}
+ssize_t irc_send_raw(irc_session_t* s, const char* format, ...) {
+ char buffer[IRC_MAX_BUFFER];
+ va_list ap;
+ va_start(ap, format);
+
+ int n = vsnprintf(buffer, IRC_MAX_BUFFER, format, ap);
+ /* TODO: */
+ va_end(ap);
+ int nwrote = irc_socket_send(s->sockfd, buffer, n);
+#ifdef IRC_DEBUG
+ udbug("irc_send_raw(): wrote %d bytes into s->sockfd.", nwrote);
+#endif
+ return nwrote;
+}
+
+
+int irc_connect(irc_session_t* s) {
+#ifdef IRC_DEBUG
+ udebug("irc_connect(): connecting to %s:%s", s->server, s->portno);
+#endif
+ /* create/connect the socket, send the messages, see if it was a success */
+ // we have to use getaddrinfo and stuff, i think i wrote it somewhere right?
+ /* RFC 1459:
+ * The recommended order for a client to register is as follows:
+ * 1. Pass message
+ * 2. Nick message
+ * 3. User message */
+ s->sockfd = irc_socket_create(s->server, s->portno);
+ if (s->sockfd == -1) {
+ return -1; // FIXME: proper error messages
+ }
+#ifdef IRC_DEBUG
+ udebug("irc_connect() registering as NICK : %s USER: %s PASS: %s", s->nick, s->nick, s->password);
+#endif
+ if (s->password)
+ irc_send_raw(s, "PASS %s \r\n", s->password);
+ irc_send_raw(s, "NICK %s \r\n", s->nick);
+ irc_send_raw(s, "USER %s * * : AAAA\r\n", s->nick);
+#ifdef IRC_DEBUG
+ udebug("irc_connect(), sucessfully connected.");
+#endif
+ /* some irc servers require that we ping them first */
+ irc_send_raw(s, "PING : random\r\n");
+ return 0;
+}
+
+
+char* irc_str_skip_to(char *str, char c) {
+ while (*str) {
+ if (*str == c)
+ return str;
+ str++;
+ }
+ return NULL;
+}
+
+
+// returns the first cr(lf) in the string
+int irc_find_crlf(const char* str, int size) {
+ for (int i = 0; i < size; i++) {
+ if (str[i] == '\r' && str[i + 1] == '\n')
+ return i + 1;
+ }
+ return -1;
+}
+
+
+
+// buffer must be null-terminated, complete, raw irc message(excluding lf)
+int irc_parse_msg(char* buffer, irc_msg_t* msg, int lfpos) {
+ /*
+ * From RFC 1459:
+ * <message> ::= [':' <prefix> <SPACE> ] <command> <params> <crlf>
+ * <prefix> ::= <servername> | <nick> [ '!' <user> ] [ '@' <host> ]
+ * <command> ::= <letter> { <letter> } | <number> <number> <number>
+ * <SPACE> ::= ' ' { ' ' }
+ * <params> ::= <SPACE> [ ':' <trailing> | <middle> <params> ]
+ * <middle> ::= <Any *non-empty* sequence of octets not including SPACE
+ * or NUL or CR or LF, the first of which may not be ':'>
+ * <trailing> ::= <Any, possibly *empty*, sequence of octets not including
+ * NUL or CR or LF>
+ */
+ memset(msg, 0, sizeof(irc_msg_t));
+
+ char *p = buffer;
+ char *prefix;
+ if (*p == ':') {
+ char *prefix_end = irc_str_skip_to(p, ' ');
+ if (!prefix_end) {
+#ifdef IRC_DEBUG_MODE
+ uerror("invalid message, parsing the prefix");
+#endif
+ goto invalid_msg;
+ }
+ *prefix_end = '\0';
+ prefix = p + 1;
+
+ msg->prefix_name = prefix;
+ char *n = irc_str_skip_to(prefix, '!');
+ if (n < prefix_end && n != NULL) {
+ // there's ['!' <user>] in the prefix.
+ *n = '\0';
+ msg->prefix_user = n + 1;
+ }
+
+ n = irc_str_skip_to(prefix, '@');
+ if (n < prefix_end && n != NULL) {
+ *n = '\0';
+ msg->prefix_host = n + 1;
+ }
+
+ *prefix_end = '\0';
+ p = prefix_end + 1;
+#ifdef IRC_DEBUG
+ udebug("msg->prefix_name = %s, msg->prefix_user = %s, msg->prefix_host = %s", msg->prefix_name, msg->prefix_user, msg->prefix_host);
+#endif
+ }
+
+ char *params_start = irc_str_skip_to(p, ' ');
+
+ if (!params_start) {
+#ifdef IRC_DEBUG_MOD
+ uerror("we don't have params_start.");
+#endif
+ goto invalid_msg;
+ }
+
+ *params_start = '\0';
+ msg->cmd = p;
+
+
+ p = params_start + 1;
+ char *params_end = irc_str_skip_to(p, ':');
+ if (!params_end) {
+#ifdef IRC_DEBUG_MODE
+ uwarn("we don't have trailing_start");
+#endif
+ params_end = buffer + lfpos; // apparently some servers don't have <trailing> with every msg
+ }
+ char *s = p;
+ int parameter_index = 0;
+ while (p != params_end && parameter_index < IRC_MAX_PARAMS - 1) {
+ if (*p == ' ') {
+ *p = '\0';
+ msg->parameters[parameter_index++] = s;
+ s = p + 1;
+ }
+ p++;
+ }
+ if(params_end != buffer + lfpos) { // if we had ':'
+ msg->parameters[parameter_index++] = params_end + 1; // it's already '\0' at the end
+ } else { // if we didn't have ':'
+ msg->parameters[parameter_index++] = s;
+ }
+ msg->parameters_size = parameter_index;
+
+#ifdef IRC_DEBUG_MODE
+ udebug("irc_parse_msg(); msg->cmd='%s'", msg->cmd);
+
+ if (msg->parameters_size) {
+ udebug("irc_parse_msg(); msg->params are:");
+ for (int i = 0; i < msg->parameters_size; i++) {
+ printf(" [%d]: '%s'\n", i, msg->parameters[i]);
+ }
+ }
+#endif
+
+ return 0;
+invalid_msg:
+
+
+#ifdef IRC_DEBUG
+ uerror("invalid message(%s)", buffer);
+#endif
+
+
+ return -1;
+}
+
+int irc_process_msg(irc_session_t *s, irc_msg_t *m) { //call the right even handler
+ int32_t cmd = WORDL(m->cmd[0], m->cmd[1], m->cmd[2], m->cmd[3]);
+ int ret = 0;
+
+ /* FIXME: check m->parameters_size for validity of messages, if it doesnt have a sufficent number of
+ * arguemtns, don't pass it to the handlers */
+ switch(cmd) {
+ case WORDL('P', 'I', 'N', 'G'):
+
+ /* apparently some IRC servers require PONG :<param> and some require PONG <param>,
+ * and some require both! */
+ if (s->event_handlers.ping_handler)
+ ret = s->event_handlers.ping_handler(s, m);
+ if (m->parameters_size > 1)
+ irc_send_raw(s, "PONG %s :%s\r\n", m->parameters[0], m->parameters[1]);
+ else
+ irc_send_raw(s, "PONG :%s\r\n", m->parameters[0]);
+ break;
+
+
+ case WORDL('P', 'R', 'I', 'V'):
+ if (s->event_handlers.privmsg_handler)
+ ret = s->event_handlers.privmsg_handler(s, m);
+ break;
+
+ case WORDL('N', 'I', 'C', 'K'):
+ if (s->event_handlers.nick_handler)
+ ret = s->event_handlers.nick_handler(s, m);
+ break;
+
+ case WORDL('I', 'N', 'V', 'I'):
+ if (s->event_handlers.invite_handler)
+ ret = s->event_handlers.invite_handler(s, m);
+ break;
+ case WORDL('J', 'O', 'I', 'N'):
+ if (s->event_handlers.join_handler)
+ ret = s->event_handlers.join_handler(s, m);
+ }
+ return ret;
+}
+int irc_handle_incoming_data(irc_session_t* s, int lfpos) {
+
+ irc_msg_t msg;
+
+ s->buffer[lfpos - 1] = '\0';
+ if (irc_parse_msg(s->buffer, &msg, lfpos) == -1) {
+#ifdef IRC_DEBUG_MODE
+ uerror("irc_handle_incoming_adta(): invalid server command. \n");
+#endif
+ return -1;
+ }
+
+ int r = irc_process_msg(s, &msg); // call the right callbacks.
+ if (r != 0) {
+#ifdef IRC_DEBUG_MODE
+ uerror("irc_process_msg() wasn't sucessfull, ignoring the server message.");
+#endif
+ return -1;
+ }
+
+ return 0;
+}
+
+
+void irc_main_loop(irc_session_t* s) {
+ while (1) {
+#ifdef IRC_DEBUG_MODE
+ udebug("irc_main_loop();");
+#endif
+ int nread = irc_socket_recv(s->sockfd, s->buffer + s->pos, IRC_MAX_BUFFER - s->pos);
+ if (nread <= 0) {
+#ifdef IRC_DEBUG_MODE
+ uerror("irc_socket_recv() problem in irc_main_loop(), exiting for now.\n");
+ // FIXME proper error messages
+#endif
+ exit(EXIT_FAILURE);
+ }
+
+ s->pos += nread;
+
+ int lfpos;
+ while ((lfpos = irc_find_crlf(s->buffer,s->pos)) != -1) {
+ irc_handle_incoming_data(s, lfpos);
+ memmove(s->buffer, s->buffer + lfpos + 1, s->pos - lfpos);
+ s->pos = s->pos - (lfpos + 1);
+ }
+
+ }
+}
+
+
+void irc_send_join(irc_session_t* s, const char *channel) {
+ irc_send_raw(s, "JOIN %s\r\n", channel);
+}
+
+void irc_send_privmsg(irc_session_t* s, const char *guy, const char *msg) {
+ irc_send_raw(s, "PRIVMSG %s :%s\r\n", guy, msg);
+}
+
+void irc_send_nick(irc_session_t* s, const char *nick) {
+ irc_send_raw(s, "NICK %s : \r\n", s, nick);
+}
+
+void irc_sendv_privmsg(irc_session_t* s, const char *guy, const char *format, ...) {
+ char buffer[IRC_MAX_MSG]; // FIXME
+ va_list ap;
+
+ va_start(ap, format);
+
+ vsnprintf(buffer, sizeof(buffer), format, ap);
+ irc_send_privmsg(s, guy, buffer);
+
+ va_end(ap);
+}
--- /dev/null
+++ b/irc.h
@@ -1,0 +1,86 @@
+#pragma once
+#include <stdio.h>
+#include <stdlib.h>
+#include <unistd.h>
+#include <sys/socket.h>
+#include <sys/types.h>
+#include <netdb.h>
+#include <errno.h>
+#include <assert.h>
+#include <string.h>
+#include <stdint.h>
+#include <memory.h>
+#include <stdarg.h>
+#include <utils.h>
+#include <stdint.h>
+
+
+#define IRC_MAX_PARAMS 15 + 1 // one for the trailing
+#define IRC_MAX_BUFFER 1024
+#define WORDL(c0, c1, c2, c3) (uint32_t)(c0 | (c1 << 8) | (c2 << 16) | (c3 << 24)) // shoutout to Hexchat
+#define IRC_MAX_MSG 512
+
+typedef struct {
+ char *prefix_name; // <servername> | <nick>
+ char *prefix_user; // ['!' <user>]
+ char *prefix_host; // ['@' <host>]
+ char *cmd; // <cmd>
+ char *parameters[IRC_MAX_PARAMS]; //<params>
+ int parameters_size; // size of this guy ^
+} irc_msg_t;
+
+typedef struct irc_session irc_session_t;
+typedef int (*event_handler_t)(struct irc_session* , irc_msg_t*); /* event handlers must return 0 on success, and non-zero values on failure */
+
+typedef struct {
+ event_handler_t privmsg_handler;
+ event_handler_t join_handler;
+ event_handler_t nick_handler;
+ event_handler_t ping_handler;
+ event_handler_t invite_handler;
+} irc_event_handler_set_t;
+
+typedef struct irc_session{
+ char *server;
+ char *portno;
+ char *nick;
+ char *password;
+
+ char *buffer;
+ int pos;
+
+ int sockfd;
+ int connected;
+ irc_event_handler_set_t event_handlers;
+} irc_session_t;
+
+
+/* instances of irc_msg_t should ideally only be used through these macros. */
+/* PRIVMSG handler helper macros */
+#define IRC_PRIVMSG_IS_ORIGIN_CHANNEL(m) m->parameters[0][0] == '#'
+#define IRC_PRIVMSG_ORIGIN(m) IRC_PRIVMSG_IS_ORIGIN_CHANNEL(m) ? m->parameters[0] : m->prefix_name
+#define IRC_PRIVMSG_MSG(m) m->parameters[1]
+
+/* INVITE handler helper macros */
+#define IRC_INVITE_CHANNEL(m) m->parameters[1]
+
+/* PING handler helper macros */
+#define IRC_PING_PARAMETER(m) m->parameters[0]
+
+/* more can(and should) be added as the need for handling other events arises. */
+
+/* functions */
+int irc_init_session(irc_session_t* s, const char* serv, const char* portno, const char* nick, const char* pass, irc_event_handler_set_t* es);
+int irc_create_socket(char *host, char* portno);
+ssize_t irc_send_raw(irc_session_t* s, const char* format, ...);
+int irc_connect(irc_session_t* s);
+int irc_find_crlf(const char* str, int size);
+char* irc_str_skip_to(char *str, char c);
+int irc_parse_msg(char* buffer, irc_msg_t* msg, int lfpos);
+int irc_process_msg(irc_session_t *s, irc_msg_t *m); //call the right even handler
+int irc_handle_incoming_data(irc_session_t* s, int lfpos);
+void irc_main_loop(irc_session_t* s);
+void irc_send_join(irc_session_t* s, const char *channel);
+void irc_send_privmsg(irc_session_t* s, const char *guy, const char *msg);
+void irc_send_nick(irc_session_t* s, const char *nick);
+void irc_sendf_privmsg(irc_session_t* s, const char *guy, const char *format, ...);
--- /dev/null
+++ b/net-posix.c
@@ -1,0 +1,48 @@
+#include "net.h"
+
+#include <unistd.h>
+#include <sys/socket.h>
+#include <sys/types.h>
+#include <netdb.h>
+#include <string.h>
+
+irc_socket_t irc_socket_create(char* server, char* port) {
+ struct addrinfo hints;
+ struct addrinfo* result, *rp;
+ int sockfd, s;
+
+ memset(&hints, 0, sizeof(hints));
+ hints.ai_family = AF_UNSPEC;
+ hints.ai_socktype = SOCK_STREAM;
+ hints.ai_flags = 0;
+ hints.ai_canonname = NULL;
+ hints.ai_addr = NULL;
+ hints.ai_next = NULL;
+
+ s = getaddrinfo(server, port, &hints, &result);
+ if (s != 0)
+ return -1;
+ for (rp = result; rp != NULL; rp = rp->ai_next) {
+ sockfd = socket(rp->ai_family, rp->ai_socktype, rp->ai_protocol);
+ if (sockfd == -1)
+ continue;
+ s = connect(sockfd, rp->ai_addr, rp->ai_addrlen);
+ if (s != 0) {
+ close(sockfd);
+ continue;
+ }
+ break;
+ }
+ freeaddrinfo(result);
+ if (rp == NULL)
+ return -1;
+ return sockfd;
+}
+
+ssize_t irc_socket_recv(irc_socket_t sock, char* buffer, size_t max) {
+ return recv(sock, buffer, max, 0);
+}
+
+ssize_t irc_socket_send(irc_socket_t sock, char* buffer, size_t size) {
+ return write(sock, buffer, size);
+}
--- /dev/null
+++ b/net.h
@@ -1,0 +1,19 @@
+/* if you need to use any other networking api, all you need to do is to implement
+ * this interface */
+#pragma once
+
+#include <sys/types.h> // size_t
+typedef int irc_socket_t; /* network handle type, change this to what is
+ appropriate for your api, we're using the berekeley sockets.*/
+
+/* connect to server:port, and return a "handle" that will passed to irc_sock_recv() and irc_irc_sock_send() functioons*/
+irc_socket_t irc_socket_create(char *server, char *port);
+
+/* reads at most max bytes from the sock and puts into the buffer,
+ * returns the number of bytes read on success, and a negative value on failure */
+ssize_t irc_socket_recv(irc_socket_t sock, char* buffer, size_t max);
+
+/* writes size bytes from the buffer into irc_socket_t,
+ * returns the number of bytes that were written into sock on sucess, and a negative
+ * value on failure.*/
+ssize_t irc_socket_send(irc_socket_t sock, char* buffer, size_t size);
--- /dev/null
+++ b/utils.c
@@ -1,0 +1,37 @@
+#include "utils.h"
+
+
+void udebug(const char *fmt, ...) {
+ va_list ap;
+ va_start(ap, fmt);
+ //SAGI_PRINT(fmt, "[DEBUG]", SAGI_COLOR_GREEN_FG , ap);
+ printf(SAGI_COLOR_BLUE_FG "[DEBUG]" SAGI_COLOR_RESET);
+ vprintf(fmt, ap);
+ printf("\n");
+ va_end(ap);
+}
+
+
+void uwarn(const char *fmt, ...) {
+ va_list ap;
+ va_start(ap, fmt);
+
+ printf(SAGI_COLOR_RED_FG "[WARN]" SAGI_COLOR_RESET);
+ vprintf(fmt, ap);
+ printf("\n");
+
+ va_end(ap);
+}
+
+
+
+void uerror(const char *fmt, ...) {
+ va_list ap;
+ va_start(ap, fmt);
+ printf(SAGI_COLOR_RED_FG "[ERROR]" SAGI_COLOR_RESET);
+ vprintf(fmt, ap);
+ printf("\n");
+
+ va_end(ap);
+}
+
--- /dev/null
+++ b/utils.h
@@ -1,0 +1,36 @@
+#pragma once
+
+#include <stdio.h>
+#include <stdarg.h>
+
+
+#define SAGI_COLOR_BLACK_FG "\033[1;30m"
+#define SAGI_COLOR_RED_FG "\033[1;31m"
+#define SAGI_COLOR_GREEN_FG "\033[1;32m"
+#define SAGI_COLOR_YELLOW_FG "\033[1;33m"
+#define SAGI_COLOR_BLUE_FG "\033[1;34m"
+#define SAGI_COLOR_MAGENTA_FG "\033[1;35m"
+#define SAGI_COLOR_CYAN_FG "\033[1;36m"
+#define SAGI_COLOR_WHITE_FG "\033[1;37m"
+#define SAGI_COLOR_BLACK_BG "\033[1;40m"
+#define SAGI_COLOR_RED_BG "\033[1;41m"
+#define SAGI_COLOR_GREEN_BG "\033[1;42m"
+#define SAGI_COLOR_YELLOW_BG "\033[1;43m"
+#define SAGI_COLOR_BLUE_BG "\033[1;44m"
+#define SAGI_COLOR_MAGENTA_BG "\033[1;45m"
+#define SAGI_COLOR_CYAN_BG "\033[1;46m"
+#define SAGI_COLOR_WHITE_BG "\033[1;47m"
+#define SAGI_COLOR_RESET "\033[1;0m"
+#define SAGI_COLOR_BOLD_B "\033[1;1m"
+#define SAGI_COLOR_UNDERLINE "\033[1;4m"
+#define SAGI_COLOR_INVERSE "\033[1;7m"
+#define SAGI_COLOR_BOLD_O "\033[1;21m"
+#define SAGI_COLOR_UNDERLINE_OFF "\033[1;24m"
+#define SAGI_COLOR_INVERSE_OFF "\033[1;27m"
+
+
+void uprint(const char* fmt, const char *txt, const char *color, va_list ap);
+void udebug(const char *fmt, ...);
+void uwarn(const char *fmt, ...);
+void uerror(const char *fmt, ...);
+void udie(const char *msg);