From a6b75126ad2c77e03613195a8b5a3493f62d8950 Mon Sep 17 00:00:00 2001 From: InterLinked1 <24227567+InterLinked1@users.noreply.github.com> Date: Mon, 27 Nov 2023 08:14:40 -0500 Subject: [PATCH] net_msp: Add Message Send Protocol. This adds support for the Message Send Protocol, which is a simple protocol that may be used to receive short messages targeted towards system users. Both TCP and UDP support is present. The main intended use case of this is to allow remote servers to deliver messages to the system via the IRC and notification subsystems, without requiring that they maintain persistent TCP connections to the BBS. --- README.rst | 10 +- bbs/socket.c | 169 +++++++++------- configs/net_msp.conf | 21 ++ include/socket.h | 16 +- nets/net_finger.c | 1 - nets/net_irc.c | 2 +- nets/net_msp.c | 446 +++++++++++++++++++++++++++++++++++++++++++ tests/net_msp.conf | 5 + tests/test_msp.c | 144 ++++++++++++++ 9 files changed, 740 insertions(+), 74 deletions(-) create mode 100644 configs/net_msp.conf create mode 100644 nets/net_msp.c create mode 100644 tests/net_msp.conf create mode 100644 tests/test_msp.c diff --git a/README.rst b/README.rst index 3b5b7d2..b48eb9b 100644 --- a/README.rst +++ b/README.rst @@ -169,6 +169,8 @@ Config files go in :code:`/etc/lbbs` and are as follows: * :code:`net_irc.conf` - Internet Relay Chat server config +* :code:`net_msp.conf` - Message Send Protocol config + * :code:`net_nntp.conf` - Network News Transfer Protocol (NNTP) server config * :code:`net_pop3.conf` - POP3 server config @@ -237,6 +239,8 @@ The BBS also comes with some network services that aren't intended for terminal * :code:`net_irc` - Internet Relay Chat server +* :code:`net_msp` - Message Send Protocol server + * :code:`net_nntp` - Network News Transfer Protocol (NNTP) server * :code:`net_pop3` - POP3 server @@ -282,10 +286,10 @@ The first is as simple as explicitly granting the BBS binary the right to do so, This is the recommended approach if it works for you. If not, you can also explicitly allow all users to bind to any ports that are at least the specified port number:: - sudo sysctl net.ipv4.ip_unprivileged_port_start=21 + sudo sysctl net.ipv4.ip_unprivileged_port_start=18 -This example would allow any user to bind to ports 21 and above. -The lowest standard port number currently used by the BBS is 21 (FTP). +This example would allow any user to bind to ports 18 and above. +The lowest standard port number currently used by the BBS is 18 (FTP). Note that this method is not as secure as the first method, but is likely to work even if other methods fail. diff --git a/bbs/socket.c b/bbs/socket.c index 52b0ed5..f3d38fa 100644 --- a/bbs/socket.c +++ b/bbs/socket.c @@ -26,6 +26,7 @@ #include #include #include /* use sockaddr_in */ +#include /* use ifreq */ #include /* use struct sockaddr_un */ #include /* use inet_ntop */ #include /* use getnameinfo */ @@ -120,26 +121,59 @@ int __bbs_make_unix_socket(int *sock, const char *sockfile, const char *perm, ui return 0; } -int __bbs_make_tcp_socket(int *sock, int port, const char *file, int line, const char *func) +/*! + * \brief Create and bind a TCP or UDP socket to a specific port + * \param[out] sock Socket file descriptor + * \param rebind Whether to try to force reuse of a particular port if already in use + * \param type SOCK_DGRAM for UDP or SOCK_STREAM for TCP + * \param ip Specific IP/CIDR to which to bind, or NULL for all + * \param interface Specific interface to which to bind, or NULL for all + * \param file + * \param line + * \param func + * \retval 0 on success, -1 on failure + */ +static int __bbs_socket_bind(int *sock, int rebind, int type, int port, const char *ip, const char *interface, const char *file, int line, const char *func) { - struct sockaddr_in sinaddr; /* Internet socket */ - const int enable = 1; int res; + struct sockaddr_in sinaddr; /* Internet socket */ #if defined(DEBUG_FD_LEAKS) && DEBUG_FD_LEAKS == 1 - *sock = __bbs_socket(AF_INET, SOCK_STREAM, 0, file, line, func); + *sock = __bbs_socket(AF_INET, type, 0, file, line, func); #else UNUSED(file); UNUSED(line); UNUSED(func); - *sock = socket(AF_INET, SOCK_STREAM, 0); + *sock = socket(AF_INET, type, 0); #endif if (*sock < 0) { - bbs_error("Unable to create TCP socket: %s\n", strerror(errno)); + bbs_error("Unable to create %s socket: %s\n", type == SOCK_STREAM ? "TCP" : "UDP", strerror(errno)); return -1; } - if (option_rebind) { + memset(&sinaddr, 0, sizeof(sinaddr)); + sinaddr.sin_family = AF_INET; + sinaddr.sin_port = htons((uint16_t) port); /* Public port on which to listen */ + + if (!strlen_zero(ip)) { + sinaddr.sin_addr.s_addr = inet_addr(ip); + } else { + sinaddr.sin_addr.s_addr = INADDR_ANY; + } + + if (!strlen_zero(interface)) { + struct ifreq ifr; + memset(&ifr, 0, sizeof(ifr)); + safe_strncpy(ifr.ifr_name, interface, sizeof(ifr.ifr_name)); + if (setsockopt(*sock, SOL_SOCKET, SO_BINDTODEVICE, &ifr, sizeof(ifr)) < 0) { + bbs_error("Failed to set SO_BINDTODEVICE(%s): %s\n", interface, strerror(errno)); + close(*sock); + return -1; + } + } + + if (rebind && type == SOCK_STREAM) { + const int enable = 1; /* This is necessary since trying a bind without reuse and then trying with reuse * can actually still fail (for some reason...). * If you reuse the first time, though, it should always work. @@ -148,87 +182,88 @@ int __bbs_make_tcp_socket(int *sock, int port, const char *file, int line, const * aren't running, then this may be worth it (e.g. the test framework) */ if (setsockopt(*sock, SOL_SOCKET, SO_REUSEADDR, &enable, sizeof(int)) < 0) { - bbs_error("Unable to create setsockopt: %s\n", strerror(errno)); + bbs_error("Failed to set SO_REUSEADDR: %s\n", strerror(errno)); + close(*sock); return -1; } if (setsockopt(*sock, SOL_SOCKET, SO_REUSEPORT, &enable, sizeof(int)) < 0) { - bbs_error("Unable to create setsockopt: %s\n", strerror(errno)); + bbs_error("Failed to set SO_REUSEPORT: %s\n", strerror(errno)); + close(*sock); return -1; } } - memset(&sinaddr, 0, sizeof(sinaddr)); - sinaddr.sin_family = AF_INET; - sinaddr.sin_addr.s_addr = INADDR_ANY; - sinaddr.sin_port = htons((uint16_t) port); /* Public TCP port on which to listen */ - res = bind(*sock, (struct sockaddr*) &sinaddr, sizeof(sinaddr)); if (res) { - while (errno == EADDRINUSE) { - /* Don't do this by default. - * If somehow multiple instances of the BBS are running, - * then weird things can happen as a result of multiple BBS processes - * running on the same port. Sometimes things will work, usually they won't. - * - * (We do try really hard in bbs.c to prevent multiple instances of the BBS - * from being run at the same time, mostly accidentally, and this usually - * works, but it's not foolproof.) - * - * Therefore, try to bind without reusing first, and only if that fails, - * reuse the port, but make some noise about this just in case. */ - if (option_rebind) { - bbs_error("Port %d was already in use, retrying with reuse\n", port); - } else { - bbs_warning("Port %d was already in use, retrying with reuse\n", port); - } + res = errno; + close(*sock); + errno = res; /* Close but preserve the errno from bind failing */ + } + return res; +} - /* We can't reuse the original socket after bind fails, make a new one. */ - close(*sock); - if (bbs_safe_sleep(500)) { - bbs_verb(4, "Aborting socket bind due to exceptional BBS activity\n"); - break; - } - *sock = socket(AF_INET, SOCK_STREAM, 0); - if (*sock < 0) { - bbs_error("Unable to recreate TCP socket: %s\n", strerror(errno)); - return -1; - } - if (setsockopt(*sock, SOL_SOCKET, SO_REUSEADDR, &enable, sizeof(int)) < 0) { - bbs_error("Unable to create setsockopt: %s\n", strerror(errno)); - return -1; - } - if (setsockopt(*sock, SOL_SOCKET, SO_REUSEPORT, &enable, sizeof(int)) < 0) { - bbs_error("Unable to create setsockopt: %s\n", strerror(errno)); - return -1; - } +static int __bbs_make_ip_socket(int *sock, int port, int type, const char *ip, const char *interface, const char *file, int line, const char *func) +{ + int res; - memset(&sinaddr, 0, sizeof(sinaddr)); - sinaddr.sin_family = AF_INET; - sinaddr.sin_addr.s_addr = INADDR_ANY; - sinaddr.sin_port = htons((uint16_t) port); /* Public TCP port on which to listen */ + res = __bbs_socket_bind(sock, option_rebind, type, port, ip, interface, file, line, func); + while (res && errno == EADDRINUSE) { + /* Don't do this by default. + * If somehow multiple instances of the BBS are running, + * then weird things can happen as a result of multiple BBS processes + * running on the same port. Sometimes things will work, usually they won't. + * + * (We do try really hard in bbs.c to prevent multiple instances of the BBS + * from being run at the same time, mostly accidentally, and this usually + * works, but it's not foolproof.) + * + * Therefore, try to bind without reusing first, and only if that fails, + * reuse the port, but make some noise about this just in case. */ + if (option_rebind) { + bbs_error("Port %d was already in use, retrying with reuse\n", port); + } else { + bbs_warning("Port %d was already in use, retrying with reuse\n", port); + } - res = bind(*sock, (struct sockaddr*) &sinaddr, sizeof(sinaddr)); - if (!option_rebind) { - break; - } + if (bbs_safe_sleep(500)) { + bbs_verb(4, "Aborting socket bind due to exceptional BBS activity\n"); + break; } - if (res) { - bbs_error("Unable to bind TCP socket to port %d: %s\n", port, strerror(errno)); + + res = __bbs_socket_bind(sock, option_rebind, type, port, ip, interface, file, line, func); + if (!option_rebind) { + /* If we don't require reuse, try once and then give up */ + break; + } + } + if (res) { + bbs_error("Unable to bind %s socket to port %d: %s\n", type == SOCK_STREAM ? "TCP" : "UDP", port, strerror(errno)); + *sock = -1; + return -1; + } + + if (type == SOCK_STREAM) { + if (listen(*sock, 10) < 0) { + bbs_error("Unable to listen on %s socket on port %d: %s\n", type == SOCK_STREAM ? "TCP" : "UDP", port, strerror(errno)); close(*sock); *sock = -1; return -1; } - } - if (listen(*sock, 10) < 0) { - bbs_error("Unable to listen on TCP socket on port %d: %s\n", port, strerror(errno)); - close(*sock); - *sock = -1; - return -1; } - bbs_debug(1, "Started %s listener on port %d\n", "TCP", port); + bbs_debug(1, "Started %s listener on port %d\n", type == SOCK_STREAM ? "TCP" : "UDP", port); return 0; } +int __bbs_make_tcp_socket(int *sock, int port, const char *file, int line, const char *func) +{ + return __bbs_make_ip_socket(sock, port, SOCK_STREAM, NULL, NULL, file, line, func); +} + +int __bbs_make_udp_socket(int *sock, int port, const char *ip, const char *interface, const char *file, int line, const char *func) +{ + return __bbs_make_ip_socket(sock, port, SOCK_DGRAM, ip, interface, file, line, func); +} + int bbs_unblock_fd(int fd) { int flags = fcntl(fd, F_GETFL, 0); diff --git a/configs/net_msp.conf b/configs/net_msp.conf new file mode 100644 index 0000000..834eb6b --- /dev/null +++ b/configs/net_msp.conf @@ -0,0 +1,21 @@ +; net_msp.conf - Message Send Protocol +; +; This protocol can be used to allow various clients (e.g. other servers) to submit +; messages to deliver to local BBS users. This avoids the need to, for example, +; create an IRC user for other clients and set up persistent IRC connections, +; for applications that may only need to send messages to users or channels. +; +; WARNING: This protocol may allow unwanted anonymous and spoofed messages to reach users. +; If you load this module, it is HIGHLY recommend that you configure restrictions on the UDP +; listener to only listen to a private interface, through which trusted messages can be +; sent by other endpoints. +; You are urged to NOT EXPOSE this protocol to the Internet or other public networks. + +[ports] +tcp=18 +udp=18 + +; Additional configuration for the UDP listener +[udp] +;ip=127.0.0.1 ; Restrict listener to this IP address +;interface=eth1 ; Specific interface on which to listen diff --git a/include/socket.h b/include/socket.h index 93c8509..0352b3b 100644 --- a/include/socket.h +++ b/include/socket.h @@ -28,8 +28,8 @@ int __bbs_make_unix_socket(int *sock, const char *sockfile, const char *perm, uid_t uid, gid_t gid, const char *file, int line, const char *func); /*! - * \brief Create a TCP socket - * \param sock Pointer to socket + * \brief Create and bind a TCP socket to a particular port + * \param[out] sock Pointer to socket * \param port Port number on which to create the socket * \retval 0 on success, -1 on failure */ @@ -37,6 +37,18 @@ int __bbs_make_unix_socket(int *sock, const char *sockfile, const char *perm, ui int __bbs_make_tcp_socket(int *sock, int port, const char *file, int line, const char *func); +/*! + * \brief Create and bind a UDP socket to a particular port + * \param[out] sock Pointer to socket + * \param port Port number on which to create the socket + * \param ip Specific IP/CIDR to which to bind, or NULL for all + * \param interface Specific interface to which to bind, or NULL for all + * \retval 0 on success, -1 on failure + */ +#define bbs_make_udp_socket(sock, port, ip, interface) __bbs_make_udp_socket(sock, port, ip, interface, __FILE__, __LINE__, __func__) + +int __bbs_make_udp_socket(int *sock, int port, const char *ip, const char *interface, const char *file, int line, const char *func); + /*! \brief Put a socket in nonblocking mode */ int bbs_unblock_fd(int fd); diff --git a/nets/net_finger.c b/nets/net_finger.c index a8985ed..e172614 100644 --- a/nets/net_finger.c +++ b/nets/net_finger.c @@ -23,7 +23,6 @@ #include "include/bbs.h" #include -#include #include "include/module.h" #include "include/config.h" diff --git a/nets/net_irc.c b/nets/net_irc.c index 9b7f9bf..bf131d6 100644 --- a/nets/net_irc.c +++ b/nets/net_irc.c @@ -2811,7 +2811,7 @@ static void handle_client(struct irc_user *user) if (res <= 0) { /* Don't set graceful_close to 0 here, since after a QUIT, the client may close the connection first. * The QUIT message should be whatever the client sent, since it was graceful, not connection closed by remote host. */ - bbs_debug(3, "poll/read returned %lu\n", res); + bbs_debug(3, "bbs_readline returned %ld\n", res); break; } bbs_strterm(s, '\r'); diff --git a/nets/net_msp.c b/nets/net_msp.c new file mode 100644 index 0000000..f45845d --- /dev/null +++ b/nets/net_msp.c @@ -0,0 +1,446 @@ +/* + * LBBS -- The Lightweight Bulletin Board System + * + * Copyright (C) 2023, Naveen Albert + * + * Naveen Albert + * + * This program is free software, distributed under the terms of + * the GNU General Public License Version 2. See the LICENSE file + * at the top of the source tree. + */ + +/*! \file + * + * \brief RFC 1312 Message Send Protocol + * + * \note Supersedes RFC 1159 + * + * \author Naveen Albert + */ + +#include "include/bbs.h" + +#include +#include +#include +#include +#include /* use sockaddr_in */ + +#include "include/module.h" +#include "include/config.h" +#include "include/utils.h" +#include "include/node.h" +#include "include/user.h" +#include "include/notify.h" +#include "include/net_irc.h" + +#define DEFAULT_MSP_PORT 18 +#define MAX_MSP_MSG_LEN 512 + +static int msp_tcp_port = DEFAULT_MSP_PORT; +static int msp_udp_port = DEFAULT_MSP_PORT; + +static pthread_t udp_thread; +static int udp_socket = -1; +static int unloading = 0; + +struct msp { + struct bbs_node *node; /* Node (TCP only) */ + struct sockaddr_in *in; /* sockaddr (UDP only) */ + socklen_t slen; /* sockaddr len (UDP only) */ + unsigned int version:1; /* 0 = 'A', 1 = 'B' */ + const char *recip; + const char *recipterm; + const char *message; + /* Only in version 2 of the protocol: */ + const char *sender; + const char *senderterm; + const char *cookie; + const char *signature; +}; + +/*! + * \brief Variant of strsep that delimits on NULL, with bounds check + * \param[out] var Variable corresponding to next token to parse + * \param buf Current buffer, which will be updated to the remaining portion after the current token + * \param len Amount remaining in buf, which will be updated after buf is parsed + * \retval 0 on success, -1 on failure + */ +static int strnsep(const char **restrict var, char **restrict buf, size_t *restrict len) +{ + char *s; + + if (*len <= 0) { + bbs_error("Buffer exhausted\n"); + return -1; + } else if (!*buf) { + bbs_error("Nothing left in buffer to parse\n"); + return -1; + } + + *var = s = *buf; + + for (;;) { + *len -= 1; + if (*s == '\0') { + /* Since the NUL is already in the buffer, *var is already NUL terminated */ + *buf = s + 1; + if (!*len) { + *buf = NULL; /* There is nothing more to read, that's the end of the buffer */ + } + bbs_debug(4, "Parsed token '%s'\n", *var); + return 0; + } + if (!*len) { + bbs_error("Exhausted buffer before successfully parsing another token (got '%.*s')\n", (int) (s - *buf), *buf); + return -1; + } + s++; + } +} + +static int parse_msp(struct msp *restrict msp, char *restrict buf, size_t len) +{ + char version; + char *tmp = buf; + + /* Since the payload contains NULs, we need to know how long it is. */ + + version = *tmp++; + len--; + if (version == 'B') { + msp->version = 1; + } else if (version != 'A') { + bbs_warning("Invalid MSP protocol version\n"); + return -1; + } + + if (strnsep(&msp->recip, &tmp, &len)) { + bbs_warning("Failed to parse recipient\n"); + return -1; + } + if (strnsep(&msp->recipterm, &tmp, &len)) { + bbs_warning("Failed to parse recipient terminal\n"); + return -1; + } + if (strnsep(&msp->message, &tmp, &len)) { + bbs_warning("Failed to parse message\n"); + return -1; + } + + if (!msp->version) { + /* Finished parsing a version 1 payload */ + return 0; + } + + if (strnsep(&msp->sender, &tmp, &len)) { + bbs_warning("Failed to parse sender\n"); + return -1; + } + if (strnsep(&msp->senderterm, &tmp, &len)) { + bbs_warning("Failed to parse sender terminal\n"); + return -1; + } + if (strnsep(&msp->cookie, &tmp, &len)) { + bbs_warning("Failed to parse cookie\n"); + return -1; + } + if (strnsep(&msp->signature, &tmp, &len)) { + bbs_warning("Failed to parse signature\n"); + return -1; + } + + /* Finished parsing version 2 payload */ + return 0; +} + +/*! \brief Read message into buffer for TCP handler */ +static ssize_t read_msg(struct msp *restrict msp, char *restrict buf, size_t len, int rfd) +{ + int got_version = 0; + int nulls_wanted; + ssize_t bytes = 0; + char *bufptr = buf; + size_t left = len; + + for (;;) { + char *next = bufptr; + ssize_t res = bbs_poll(rfd, MIN_MS(1)); + if (res <= 0) { + return -1; + } + res = read(rfd, bufptr, left); + if (res < 0) { + bbs_error("read failed: %s\n", strerror(errno)); + return -1; + } else if (!res) { + if (got_version) { + bbs_debug(3, "Client disconnected mid-message, with %d part%s remaining\n", nulls_wanted, ESS(nulls_wanted)); + } + return -1; + } + bytes += res; + bufptr += res; + left -= (size_t) res; + if (left <= 0) { + bbs_warning("MSP message too long, buffer exhausted\n"); + return -1; + } + if (!got_version) { + if (buf[0] != 'A' && buf[0] != 'B') { + /* If we don't get a valid protocol version, + * don't even bother reading the rest. */ + bbs_warning("Invalid MSP protocol version: '%c'\n", buf[0]); + return -1; + } + msp->version = buf[0] == 'B'; + got_version = 1; + nulls_wanted = msp->version ? 7 : 3; + } + /* Keep track of how many NULs we've gotten, so we know when we've gotten an entire message */ + while (res--) { + if (*next++ == '\0') { + if (!--nulls_wanted) { + /* Got the entire message */ + return bytes; + } + } + } + } +} + +static int msp_response(struct msp *restrict msp, const char *s, size_t len) +{ + bbs_debug(3, "MSP response <= %.*s\n", (int) len, s); + if (msp->node) { + bbs_node_fd_write(msp->node, msp->node->fd, s, len); + } else { + ssize_t res = sendto(udp_socket, s, len, 0, msp->in, msp->slen); + if (res <= 0) { + bbs_error("sendto failed: %s\n", strerror(errno)); + } + } + return 0; +} + +/* Include NUL terminator, since that's part of the message */ +#define MSP_ERROR(msp, s) msp_response(msp, "-" s, STRLEN("-" s) + 1) + +static int handle_msp(struct msp *restrict msp, const char *ip) +{ + char msgbuf[512]; + int res; + + bbs_verb(4, "Handling Message Send Protocol version %d message\n", msp->version ? 2 : 1); + +#ifdef EXTRA_DEBUG + bbs_debug(4, "MSP message version %d:\n" + "\tRecip: %s\n" + "\tRecipTerm: %s\n" + "\tMessage: %s\n" + "\tSender: %s\n" + "\tSenderTerm: %s\n" + "\tCookie: %s\n" + "\tSignature: %s\n", + msp->version ? 2 : 1, S_IF(msp->recip), S_IF(msp->recipterm), S_IF(msp->message), + S_IF(msp->sender), S_IF(msp->senderterm), S_IF(msp->cookie), S_IF(msp->signature)); +#endif + + if (strlen_zero(msp->message)) { + MSP_ERROR(msp, "Empty message"); + return -1; + } + + /* Only printable characters allowed. */ + if (!bbs_str_isprint(msp->message)) { + MSP_ERROR(msp, "Invalid characters"); + return -1; + } + + /* While we successfully parse version 1 messages, where sender is empty, + * we are now obligated to process these, and we do not. */ + if (strlen_zero(msp->sender)) { + MSP_ERROR(msp, "Empty sender"); + return -1; + } + + /* The following are presently ignored: + * recipterm + * senderterm + * cookie + * signature + */ + + if (strlen_zero(msp->recip)) { + /* Not directed to any particular user. + * We're allowed to deliver "to any user", but just reject it... */ + MSP_ERROR(msp, "This system does not deliver messages without a recipient"); + return -1; + } else { + /* Directed to a particular user (or channel). */ + if (!isalpha(*msp->recip)) { + /* Begins with a non-numeric character. + * Assume it's the name of an IRC channel. */ + res = irc_relay_send(msp->recip, CHANNEL_USER_MODE_NONE, "MSP", msp->sender, NULL, msp->message, NULL); + if (res) { + MSP_ERROR(msp, "Channel does not exist"); + return -1; + } + } else { + unsigned int userid = bbs_userid_from_username(msp->recip); + if (!userid) { + MSP_ERROR(msp, "No such user"); + return -1; + } + snprintf(msgbuf, sizeof(msgbuf), "%s@%s: %s", msp->sender, ip, msp->message); + res = bbs_alert_user(userid, DELIVERY_EPHEMERAL, "%s", msgbuf); + if (res) { + MSP_ERROR(msp, "User not online"); + return -1; + } + } + } + + /* Responses include + or -, an optional description, and a final NUL */ + msp_response(msp, "+", 2); + return 0; +} + +static void *msp_tcp_handler(void *varg) +{ + char buf[MAX_MSP_MSG_LEN + 1]; + struct bbs_node *node = varg; + + bbs_node_net_begin(node); + for (;;) { + struct msp msp; + ssize_t res; + memset(&msp, 0, sizeof(msp)); + + /* Read the message first without parsing it, + * so we can have a common parser for both TCP and UDP. */ + res = read_msg(&msp, buf, sizeof(buf), node->fd); + if (res < 0) { + break; + } + if (parse_msp(&msp, buf, (size_t) res)) { + bbs_debug(4, "Failed to parse MSP payload\n"); + break; + } + /* We got a valid message */ + msp.node = node; + if (handle_msp(&msp, node->ip)) { + bbs_debug(4, "Failed to handle MSP message\n"); + break; + } + /* If all is well, allow the client to continue. */ + } + bbs_node_exit(node); + + return NULL; +} + +static void *msp_udp_listener(void *varg) +{ + struct pollfd pfd; + + UNUSED(varg); + memset(&pfd, 0, sizeof(pfd)); + + pfd.fd = udp_socket; + pfd.events = POLLIN; + for (;;) { + char ipaddr[55]; + struct msp msp; + struct sockaddr_in srcaddr; + socklen_t slen = sizeof(struct sockaddr_in); + char buf[MAX_MSP_MSG_LEN + 1]; + ssize_t res; + pfd.revents = 0; + res = poll(&pfd, 1, -1); + if (res <= 0) { + bbs_debug(3, "poll returned %ld: %s\n", res, strerror(errno)); + break; + } + if (unloading) { + break; + } + res = recvfrom(udp_socket, buf, sizeof(buf), 0, (struct sockaddr*) &srcaddr, &slen); + if (res <= 0) { + bbs_error("recvfrom returned %ld: %s\n", res, strerror(errno)); + break; + } + bbs_get_remote_ip(&srcaddr, ipaddr, sizeof(ipaddr)); + bbs_auth("Received new Message Send Protocol message from %s\n", ipaddr); + memset(&msp, 0, sizeof(msp)); + msp.in = &srcaddr; + msp.slen = slen; + /* Single thread for all incoming UDP connections, + * since it won't take very long to service requests. */ + if (parse_msp(&msp, buf, (size_t) res)) { + bbs_debug(4, "Failed to parse MSP payload\n"); + } else if (handle_msp(&msp, ipaddr)) { + bbs_debug(4, "Failed to handle MSP message\n"); + } + } + return NULL; +} + +static const char *ip = NULL, *interface = NULL; + +static int load_config(void) +{ + struct bbs_config *cfg = bbs_config_load("net_msp.conf", 0); + + if (!cfg) { + return 0; + } + + /* We don't destroy the config after we return, so it's okay that we have a constant reference to it directly */ + ip = bbs_config_val(cfg, "udp", "ip"); + interface = bbs_config_val(cfg, "udp", "interface"); + + return bbs_config_val_set_port(cfg, "ports", "tcp", &msp_tcp_port) && bbs_config_val_set_port(cfg, "ports", "udp", &msp_udp_port); +} + +static int load_module(void) +{ + int res = 0; + + if (load_config()) { + return -1; + } + + if (msp_tcp_port) { + res = bbs_start_tcp_listener(msp_tcp_port, "MSP", msp_tcp_handler); + } + if (!res && msp_udp_port) { + /* Be extra careful about the interfaces to which we bind, + * since the source IP of UDP messages can be spoofed, + * and we won't be able to tell. */ + res = bbs_make_udp_socket(&udp_socket, msp_udp_port, ip, interface); + if (!res) { + res = bbs_pthread_create(&udp_thread, NULL, msp_udp_listener, NULL); + } + if (res) { + bbs_stop_tcp_listener(msp_tcp_port); + } + } + return res; +} + +static int unload_module(void) +{ + if (msp_tcp_port) { + bbs_stop_tcp_listener(msp_tcp_port); + } + if (msp_udp_port) { + unloading = 1; + bbs_socket_close(&udp_socket); + bbs_pthread_join(udp_thread, NULL); + } + return 0; +} + +BBS_MODULE_INFO_DEPENDENT("RFC1312 Message Send Protocol", "net_irc.so"); diff --git a/tests/net_msp.conf b/tests/net_msp.conf new file mode 100644 index 0000000..e58b8cb --- /dev/null +++ b/tests/net_msp.conf @@ -0,0 +1,5 @@ +; net_msp.conf - Message Send Protocol + +[ports] +tcp=18 +udp=18 diff --git a/tests/test_msp.c b/tests/test_msp.c new file mode 100644 index 0000000..06b4b30 --- /dev/null +++ b/tests/test_msp.c @@ -0,0 +1,144 @@ +/* + * LBBS -- The Lightweight Bulletin Board System + * + * Copyright (C) 2023, Naveen Albert + * + * Naveen Albert + * + * This program is free software, distributed under the terms of + * the GNU General Public License Version 2. See the LICENSE file + * at the top of the source tree. + */ + +/*! \file + * + * \brief Message Send Protocol Tests + * + * \author Naveen Albert + */ + +#include "test.h" + +#include +#include +#include +#include +#include +#include /* use sockaddr_in */ +#include + +static int pre(void) +{ + test_preload_module("net_irc.so"); + test_load_module("net_msp.so"); + + TEST_ADD_CONFIG("net_irc.conf"); + TEST_ADD_CONFIG("net_msp.conf"); + return 0; +} + +/* Adapted from examples in RFC 1312 */ +#define VERSION_1_REQUEST "A" TEST_USER "\0\0Hi\r\nHow about lunch?\0" +#define VERSION_2_REQUEST "B" TEST_USER "\0\0Hi\r\nHow about lunch?\0sandy\0console\0910806121325\0\0" + +static int udp_client_test(void) +{ + ssize_t res; + int sfd; + char resp[32]; + struct sockaddr_in saddr; + + memset(&saddr, 0, sizeof(saddr)); + saddr.sin_family = AF_INET; + saddr.sin_port = htons(18); + saddr.sin_addr.s_addr = inet_addr("127.0.0.1"); + + sfd = socket(AF_INET, SOCK_DGRAM, 0); + if (sfd < 0) { + bbs_error("socket failed: %s\n", strerror(errno)); + return -1; + } + + /* Now, test sending and receiving with the UDP version: + * Since the response is coming from the same place the packet is being sent, + * we can call connect() and then use send() and recv() + * instead of sendto() and recvfrom() + * See udp(7) */ + if (connect(sfd, (struct sockaddr*) &saddr, sizeof(struct sockaddr_in))) { + bbs_error("connect failed: %s\n", strerror(errno)); + close(sfd); + return -1; + } + + /* It's UDP, so it probably won't fail anyways */ + res = send(sfd, VERSION_2_REQUEST, STRLEN(VERSION_2_REQUEST), 0); + if (res <= 0) { + bbs_error("send failed: %s\n", strerror(errno)); + close(sfd); + return -1; + } + + res = recv(sfd, resp, sizeof(resp), 0); + if (res <= 0) { + bbs_error("recv failed: %s\n", strerror(errno)); + close(sfd); + return -1; + } + + close(sfd); + return strncmp(resp, "+", 1); +} + +static int run(void) +{ + int clientfd = -1; + int ircfd = -1; + int res = -1; + + /* Connect to IRC to receive messages */ + ircfd = test_make_socket(6667); + REQUIRE_FD(ircfd); + + SWRITE(ircfd, "CAP LS 302\r\n"); + SWRITE(ircfd, "NICK " TEST_USER ENDL); + SWRITE(ircfd, "USER " TEST_USER ENDL); + CLIENT_EXPECT_EVENTUALLY(ircfd, "CAP * LS"); + SWRITE(ircfd, "CAP REQ :sasl\r\n"); + CLIENT_EXPECT(ircfd, "CAP * ACK"); + SWRITE(ircfd, "AUTHENTICATE PLAIN\r\n"); + CLIENT_EXPECT(ircfd, "AUTHENTICATE +\r\n"); + SWRITE(ircfd, "AUTHENTICATE " TEST_SASL "\r\n"); + CLIENT_EXPECT(ircfd, "903"); + SWRITE(ircfd, "CAP END\r\n"); + + CLIENT_EXPECT_EVENTUALLY(ircfd, "376"); /* End of MOTD */ + + SWRITE(ircfd, "JOIN #test1,#test2,#test3\r\n"); + CLIENT_EXPECT_EVENTUALLY(ircfd, "MODE #test3 +oq testuser"); /* Nobody else was in these channels */ + + /* Connected to IRC, now do the MSP stuff */ + + clientfd = test_make_socket(18); + REQUIRE_FD(clientfd); + + /* Version 2 request via TCP */ + SWRITE(clientfd, VERSION_2_REQUEST); + CLIENT_EXPECT_EVENTUALLY(clientfd, "+"); + + /* Version 1 request via TCP */ + SWRITE(clientfd, VERSION_1_REQUEST); + CLIENT_EXPECT(clientfd, "-"); /* We expect this to be rejected since we only support Version 2 */ + + close_if(clientfd); + res = udp_client_test(); + + /* Should've gotten these via IRC */ + CLIENT_EXPECT_EVENTUALLY(ircfd, "How about lunch?"); + +cleanup: + close_if(clientfd); + close_if(ircfd); + return res; +} + +TEST_MODULE_INFO_STANDARD("Message Send Protocol Tests");