/* * This file is part of uIRCd. (https://git.redxen.eu/caskd/uIRCd) * Copyright (c) 2019, 2020 Alex-David Denes * * uIRCd is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * any later version. * * uIRCd is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with uIRCd. If not, see . */ #include "main.h" #include "buffer.h" #include "configuration.h" #include "connection.h" #include "filesystem.h" #include "logging.h" #include "memory.h" #include "misc.h" #include "signal.h" #include // errno #include // O_NONBLOCK #include // printf() #include // EXIT_SUCCESS EXIT_FAILURE #include // strerror() #include // time_t size_t #include // openlog() syslog() #include // time() #include // IRC_Message #include // getopt() chdir() close() int parse_args(int argc, char** argv, Connection* conn) { int c; unsigned long chanindex = 0; while ((c = getopt(argc, argv, "V:a:p:d:t:N:U:R:P:q:c:k:" #ifdef UIRCD_FEATURE_LIBCONFIG "C:" #endif /* UIRCD_FEATURE_LIBCONFIG */ "vh")) != -1) { switch (c) { case 'V': { int levelmask = atoi(optarg); setlogmask(LOG_UPTO(levelmask)); LOG(LOG_DEBUG, "Log level was set to %i", levelmask); break; } case 'a': allocate_copy(&conn->data.address, optarg); break; case 'p': allocate_copy(&conn->data.service, optarg); break; case 'd': allocate_copy(&conn->data.path, optarg); break; case 't': { conn->data.timeout = (unsigned int) atoi(optarg); LOG(LOG_DEBUG, "Timeout was set to %li", conn->data.timeout); break; } case 'N': allocate_copy(&conn->user.nickname, optarg); break; case 'U': allocate_copy(&conn->user.username, optarg); break; case 'R': allocate_copy(&conn->user.realname, optarg); break; case 'P': allocate_copy(&conn->user.password, optarg); break; case 'q': allocate_copy(&conn->data.quitmsg, optarg); break; case 'c': { resize_chanarray(&conn->info.channels); chanindex = get_channelindex(optarg, conn->info.channels); if (!set_channel(&conn->info.channels[chanindex], optarg, NULL, true)) LOG(LOG_WARNING, "Couldn't set channel %s as chan #%lu", optarg, chanindex); break; } case 'k': { if (!set_channel(&conn->info.channels[chanindex], NULL, optarg, true)) LOG(LOG_WARNING, "Couldn't set key for channel #%lu", chanindex); break; } case 'v': printf("uIRCd version " UIRCD_VERSION); return 0; case 'h': print_help(); return 0; #ifdef UIRCD_FEATURE_LIBCONFIG case 'C': parse_configfile(optarg, conn); break; #endif /* UIRCD_FEATURE_LIBCONFIG */ } } return 1; } void print_help(void) { struct help { char arg; char* type; char* desc; char* def; } arg_list[] = { { 'a', "host", "Host to connect to (IP/Hostname)", UIRCD_DEFAULT_ADDR }, { 'p', "serv", "Service (port for TCP/UDP)", UIRCD_DEFAULT_PORT }, { 'N', "str", "Nickname for registration", UIRCD_DEFAULT_NICK }, { 'U', "str", "Username for registration", UIRCD_DEFAULT_USER }, { 'R', "str", "Realname for registration", UIRCD_DEFAULT_REAL }, { 'P', "str", "Password for registration", NULL }, { 'd', "str", "Directory for logs", "current dir" }, { 'q', "str", "Quit message", UIRCD_DEFAULT_QUITMSG }, { 't', "uint", "Timeout duration", NULL }, { 'V', "int", "Log level", "7 [LOG_INFO]" }, { 'c', "str", "Channel", NULL }, { 'k', "str", "Channel key", NULL }, #ifdef UIRCD_FEATURE_LIBCONFIG { 'C', "path", "Configuration path", NULL }, #endif /* UIRCD_FEATURE_LIBCONFIG */ { 'v', NULL, "Print version information", NULL }, { 'h', NULL, "Print this help message", NULL } }; printf("usage: uircd -a [ ... ]\n"); for (size_t i = 0; i < sizeof(arg_list) / sizeof(*arg_list); i++) { printf(" -%c", arg_list[i].arg); if (arg_list[i].type != NULL) printf(" \t%s", arg_list[i].type); if (arg_list[i].desc != NULL) printf(" \t%s", arg_list[i].desc); if (arg_list[i].def != NULL) printf(" (default: %s)", arg_list[i].def); putchar('\n'); } } int main(int argc, char* argv[]) { IRC_Message buffer; Connection connection = { .info.state = CONN_PENDING }; // Start off with a pending connection time_t ctime; // Current time struct timespec sleep = { 1, 0 }; // Interval between loops when idle (network buffer is empty) struct { char buf[UIRCD_LIMITS_LINE + 1]; size_t pos; } Buf_FIFO = { .pos = 0 }, Buf_RECV = { .pos = 0 }; // async buffers, these are managed without blocking int net_fd = -1, fifo_fd = -1; // Network I/O and FIFO I/O (logs are opened and closed on write) char Buf_INTERNAL[UIRCD_LIMITS_LINE + 1]; // sync internal buffer, must be fully flushed each use /* * Initialisation */ openlog("uIRCd", LOG_CONS | LOG_PID | LOG_PERROR, LOG_DAEMON); setlogmask(LOG_UPTO(LOG_INFO)); init_chanarray(&connection.info.channels); setup_signals(); if (!set_config_defaults(&connection)) { LOG(LOG_ERR, "Couldn't allocate memory for configuration variables: " ERRNOFMT, strerror(errno), errno); return EXIT_FAILURE; } switch (parse_args(argc, argv, &connection)) { case 0: return EXIT_SUCCESS; case -1: return EXIT_FAILURE; } if (connection.data.path != NULL) { int tmpres; // TODO: Add chroot()/jail support by default where supported if ((tmpres = chdir(connection.data.path)) != 0) { LOG(LOG_ERR, "Couldn't change log directory to %s: " ERRNOFMT, connection.data.path, strerror(errno), errno); return EXIT_FAILURE; } LOG(LOG_INFO, "Changed log root directory to %s", connection.data.path); } /* * Main loop */ for (;;) { ctime = time(NULL); if (connection.info.state == CONN_CLOSED) { LOG(LOG_INFO, "Exiting gracefully"); return EXIT_SUCCESS; } else if (!run || connection.info.state == CONN_CLOSING) { ssize_t len; IRC_Message build_buf; if ((len = Assm_mesg(Buf_INTERNAL, Assm_cmd_QUIT(&build_buf, connection.data.quitmsg), sizeof(Buf_INTERNAL))) > 0) { ssize_t tmp; if ((tmp = write_buffer(Buf_INTERNAL, net_fd, (size_t) len, true)) > 0) LOG(LOG_INFO, "Sent a QUIT message to " ADDRFMT " containing \"%s\"", connection.data.address, connection.data.service, connection.data.quitmsg); else if (tmp == -1) LOG(LOG_WARNING, "Failed to send a QUIT message to " ADDRFMT " containing \"%s\"", connection.data.address, connection.data.service, connection.data.quitmsg); } close(net_fd); close(fifo_fd); connection.info.state = CONN_CLOSED; LOG(LOG_INFO, "Connection to " ADDRFMT " was closed", connection.data.address, connection.data.service); continue; } else if (connection.info.state == CONN_RECONNECTING) { close(net_fd); close(fifo_fd); net_fd = -1; fifo_fd = -1; connection.info.state = CONN_PENDING; if (connection.info.reconinter <= 300) connection.info.reconinter += 5; continue; } else if (connection.info.state == CONN_PENDING) { // Reconnection throttling if (ctime - connection.info.l_connect < connection.info.reconinter) continue; Buf_RECV.buf[(Buf_RECV.pos = 0)] = '\0'; Buf_FIFO.buf[(Buf_FIFO.pos = 0)] = '\0'; connection.info.l_connect = ctime; connection.info.l_message = 0; // Connection initialisation and registration fifo_fd = makeinput("in"); signed int tmp; if ((tmp = init_connection(&connection)) > 0) { add_socket_flags(tmp, O_NONBLOCK); net_fd = tmp; if (register_user(Buf_INTERNAL, sizeof(Buf_INTERNAL), net_fd, &connection.user)) { LOG(LOG_INFO, "Registered to server " ADDRFMT, connection.data.address, connection.data.service); connection.info.state = CONN_REGISTERED; } else { LOG(LOG_WARNING, "Failed to register to server " ADDRFMT ": " ERRNOFMT, connection.data.address, connection.data.service, strerror(errno), errno); connection.info.state = CONN_RECONNECTING; } } else if (tmp == INIT_SOFTFAIL) { connection.info.state = CONN_RECONNECTING; } else if (tmp == INIT_HARDFAIL) { connection.info.state = CONN_CLOSED; } continue; } else if (connection.info.state == CONN_IDLE) { if (connection.info.state == CONN_IDLE) nanosleep(&sleep, NULL); if (connection.data.timeout > 0 && connection.info.l_message < ctime - connection.data.timeout) { LOG(LOG_WARNING, "Timed out because no message was received since %lu", connection.info.l_message); connection.info.state = CONN_RECONNECTING; continue; } if (fifo_fd >= 0) { /* FIFO input is passthrough, no validation is made, allows raw input */ ssize_t r_bytes, w_bytes; if ((r_bytes = read_buffer(Buf_FIFO.buf + Buf_FIFO.pos, fifo_fd, sizeof(Buf_FIFO.buf) - Buf_FIFO.pos - 1)) == -1) { connection.info.state = CONN_RECONNECTING; continue; } else if (r_bytes > 0) Buf_FIFO.pos += (size_t) r_bytes; if ((w_bytes = write_buffer(Buf_FIFO.buf, net_fd, Buf_FIFO.pos, false)) == -1) { connection.info.state = CONN_RECONNECTING; continue; } else if (w_bytes > 0) Buf_FIFO.pos -= dequeue_bytes(Buf_FIFO.buf, (size_t) w_bytes, Buf_FIFO.pos); } } else if (connection.info.state == CONN_ACTIVE) { if (Buf_RECV.pos == 0) connection.info.state = CONN_IDLE; } /* Receive buffer reader */ ssize_t r_bytes, len; if ((r_bytes = read_buffer(Buf_RECV.buf + Buf_RECV.pos, net_fd, sizeof(Buf_RECV.buf) - Buf_RECV.pos - 1)) == -1) { connection.info.state = CONN_RECONNECTING; continue; } else Buf_RECV.pos += (size_t) r_bytes; memset((void*) &buffer, '\0', sizeof(IRC_Message)); if ((len = tok_irc_line(Buf_RECV.buf)) > 0) { LOG(LOG_DEBUG, "Got IRC message on recvbuffer: %s", Buf_RECV.buf); if (Tok_mesg(Buf_RECV.buf, &buffer) == 1) { connection.info.l_message = ctime; char datebuf[25]; if (buffer.tags.time.value == NULL) { Assm_tag_timestamp(datebuf, sizeof(datebuf), ctime); buffer.tags.time.value = datebuf; } if ((Assm_mesg(Buf_INTERNAL, &buffer, sizeof(Buf_INTERNAL))) > 0) { printf("%s", Buf_INTERNAL); write_log("out", Buf_INTERNAL); } switch (buffer.cmd) { case (PING): { LOG(LOG_DEBUG, "Auto-replying to ping \"%s\"", buffer.args[0]); ssize_t tmp; IRC_Message build_buf; if ((tmp = Assm_mesg(Buf_INTERNAL, Assm_cmd_PONG(&build_buf, buffer.args[0], NULL), sizeof(Buf_INTERNAL))) > 0) if (write_buffer(Buf_INTERNAL, net_fd, (size_t) tmp, true) == -1) { LOG(LOG_WARNING, "Couldn't pong " ADDRFMT ". " ERRNOFMT, connection.data.address, connection.data.service, strerror(errno), errno); connection.info.state = CONN_RECONNECTING; return 0; } break; } /* Autojoin channels from current conn on first response from the server */ case (RPL_WELCOME): { LOG(LOG_NOTICE, "Connection established to " ADDRFMT, connection.data.address, connection.data.service); connection.info.state = CONN_ACTIVE; connection.info.reconinter = 0; if (!commit_channelist(Buf_INTERNAL, sizeof(Buf_INTERNAL), net_fd, connection.info.channels)) connection.info.state = CONN_RECONNECTING; break; } case (ERROR): { LOG(LOG_ERR, "Received error: %s", buffer.args[0]); break; } case (JOIN): case (PART): { resize_chanarray(&connection.info.channels); set_channel(&connection.info.channels[get_channelindex(buffer.args[0], connection.info.channels)], buffer.args[0], buffer.args[1], buffer.cmd == JOIN); break; } case (ERR_NICKNAMEINUSE): case (ERR_NICKCOLLISION): { LOG(LOG_WARNING, "Nickname \"%s\" is already taken", connection.user.nickname); connection.info.state = CONN_RECONNECTING; break; } } connection.info.state = CONN_ACTIVE; } else LOG(LOG_WARNING, "Received invalid IRC message (see RFC2812)."); Buf_RECV.pos -= dequeue_bytes(Buf_RECV.buf, (size_t) len, Buf_RECV.pos); } else { if (len == 0) { if (Buf_RECV.pos != sizeof(Buf_RECV.buf) - 1) continue; LOG(LOG_WARNING, "Receive buffer is full and no message could be parsed"); } connection.info.state = CONN_RECONNECTING; } } }