This repository has been archived on 2021-02-08. You can view files and clone it, but cannot push or open issues or pull requests.
uIRCd/src/main.c

344 lines
13 KiB
C

/*
* 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 <https://www.gnu.org/licenses/>.
*/
#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.h> // errno
#include <fcntl.h> // O_NONBLOCK
#include <stdio.h> // printf()
#include <stdlib.h> // EXIT_SUCCESS EXIT_FAILURE
#include <string.h> // strerror()
#include <sys/types.h> // time_t size_t
#include <syslog.h> // openlog() syslog()
#include <time.h> // time()
#include <uirc/uirc.h> // IRC_Message
#include <unistd.h> // 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 <host> [ <options> ... ]\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;
}
}
}