/*- * SPDX-License-Identifier: BSD-2-Clause * * Copyright (c) 2024 Rick Parrish . * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * 1. Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * 2. Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. */ #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include struct Creation { /* true if the queue exists. */ bool exists; /* true if a mode value was specified. */ bool set_mode; /* access mode with rwx permission bits. */ mode_t mode; /* maximum queue depth. default to an invalid depth. */ long depth; /* maximum message size. default to an invalid size. */ long size; /* true for blocking I/O and false for non-blocking I/O. */ bool block; /* true if a group ID was specified. */ bool set_group; /* group ID. */ gid_t group; /* true if a user ID was specified. */ bool set_user; /* user ID. */ uid_t user; }; struct element { STAILQ_ENTRY(element) links; const char *text; }; static struct element * malloc_element(const char *context) { struct element *item = malloc(sizeof(struct element)); if (item == NULL) /* the only non-EX_* prefixed exit code. */ err(1, "malloc(%s)", context); return (item); } static STAILQ_HEAD(tqh, element) queues = STAILQ_HEAD_INITIALIZER(queues), contents = STAILQ_HEAD_INITIALIZER(contents); /* send defaults to medium priority. */ static long priority = MQ_PRIO_MAX / 2; static struct Creation creation = { .exists = false, .set_mode = false, .mode = 0755, .depth = -1, .size = -1, .block = true, .set_group = false, .group = 0, .set_user = false, .user = 0 }; static const mqd_t fail = (mqd_t)-1; static const mode_t accepted_mode_bits = S_IRWXU | S_IRWXG | S_IRWXO | S_ISUID | S_ISGID | S_ISTXT; /* OPTIONS parsing utilitarian */ static void parse_long(const char *text, long *capture, const char *knob, const char *name) { char *cursor = NULL; long value = strtol(text, &cursor, 10); if (cursor > text && *cursor == 0) { *capture = value; } else { warnx("%s %s invalid format [%s].", knob, name, text); } } static void parse_unsigned(const char *text, bool *set, unsigned *capture, const char *knob, const char *name) { char *cursor = NULL; unsigned value = strtoul(text, &cursor, 8); if (cursor > text && *cursor == 0) { *set = true; *capture = value; } else { warnx("%s %s format [%s] ignored.", knob, name, text); } } static bool sane_queue(const char *queue) { int size = 0; if (queue[size] != '/') { warnx("queue name [%-.*s] must start with '/'.", NAME_MAX, queue); return (false); } for (size++; queue[size] != 0 && size < NAME_MAX; size++) { if (queue[size] == '/') { warnx("queue name [%-.*s] - only one '/' permitted.", NAME_MAX, queue); return (false); } } if (size == NAME_MAX && queue[size] != 0) { warnx("queue name [%-.*s...] may not be longer than %d.", NAME_MAX, queue, NAME_MAX); return (false); } return (true); } /* OPTIONS parsers */ static void parse_block(const char *text) { if (strcmp(text, "true") == 0 || strcmp(text, "yes") == 0) { creation.block = true; } else if (strcmp(text, "false") == 0 || strcmp(text, "no") == 0) { creation.block = false; } else { char *cursor = NULL; long value = strtol(text, &cursor, 10); if (cursor > text) { creation.block = value != 0; } else { warnx("bad -b block format [%s] ignored.", text); } } } static void parse_content(const char *content) { struct element *n1 = malloc_element("content"); n1->text = content; STAILQ_INSERT_TAIL(&contents, n1, links); } static void parse_depth(const char *text) { parse_long(text, &creation.depth, "-d", "depth"); } static void parse_group(const char *text) { struct group *entry = getgrnam(text); if (entry == NULL) { parse_unsigned(text, &creation.set_group, &creation.group, "-g", "group"); } else { creation.set_group = true; creation.group = entry->gr_gid; } } static void parse_mode(const char *text) { char *cursor = NULL; long value = strtol(text, &cursor, 8); // verify only accepted mode bits are set. if (cursor > text && *cursor == 0 && (value & accepted_mode_bits) == value) { creation.set_mode = true; creation.mode = (mode_t)value; } else { warnx("impossible -m mode value [%s] ignored.", text); } } static void parse_priority(const char *text) { char *cursor = NULL; long value = strtol(text, &cursor, 10); if (cursor > text && *cursor == 0) { if (value >= 0 && value < MQ_PRIO_MAX) { priority = value; } else { warnx("bad -p priority range [%s] ignored.", text); } } else { warnx("bad -p priority format [%s] ignored.", text); } } static void parse_queue(const char *queue) { if (sane_queue(queue)) { struct element *n1 = malloc_element("queue name"); n1->text = queue; STAILQ_INSERT_TAIL(&queues, n1, links); } } static void parse_single_queue(const char *queue) { if (sane_queue(queue)) { if (STAILQ_EMPTY(&queues)) { struct element *n1 = malloc_element("queue name"); n1->text = queue; STAILQ_INSERT_TAIL(&queues, n1, links); } else warnx("ignoring extra -q queue [%s].", queue); } } static void parse_size(const char *text) { parse_long(text, &creation.size, "-s", "size"); } static void parse_user(const char *text) { struct passwd *entry = getpwnam(text); if (entry == NULL) { parse_unsigned(text, &creation.set_user, &creation.user, "-u", "user"); } else { creation.set_user = true; creation.user = entry->pw_uid; } } /* OPTIONS validators */ static bool validate_always_true(void) { return (true); } static bool validate_content(void) { bool valid = !STAILQ_EMPTY(&contents); if (!valid) warnx("no content to send."); return (valid); } static bool validate_depth(void) { bool valid = creation.exists || creation.depth > 0; if (!valid) warnx("-d maximum queue depth not provided."); return (valid); } static bool validate_queue(void) { bool valid = !STAILQ_EMPTY(&queues); if (!valid) warnx("missing -q, or no sane queue name given."); return (valid); } static bool validate_single_queue(void) { bool valid = !STAILQ_EMPTY(&queues) && STAILQ_NEXT(STAILQ_FIRST(&queues), links) == NULL; if (!valid) warnx("expected one queue."); return (valid); } static bool validate_size(void) { bool valid = creation.exists || creation.size > 0; if (!valid) warnx("-s maximum message size not provided."); return (valid); } /* OPTIONS table handling. */ struct Option { /* points to array of string pointers terminated by a null pointer. */ const char **pattern; /* parse argument. */ void (*parse)(const char *); /* * displays an error and returns false if this parameter is not valid. * returns true otherwise. */ bool (*validate)(void); }; /* * parse options by table. * index - current index into argv list. * argc, argv - command line parameters. * options - null terminated list of pointers to options. */ static void parse_options(int index, int argc, const char *argv[], const struct Option **options) { while ((index + 1) < argc) { const struct Option **cursor = options; bool match = false; while (*cursor != NULL && !match) { const struct Option *option = cursor[0]; const char **pattern = option->pattern; while (*pattern != NULL && !match) { const char *knob = *pattern; match = strcmp(knob, argv[index]) == 0; if (!match) pattern++; } if (match) { option->parse(argv[index + 1]); index += 2; break; } cursor++; } if (!match && index < argc) { warnx("skipping [%s].", argv[index]); index++; } } if (index < argc) { warnx("skipping [%s].", argv[index]); } } /* options - null terminated list of pointers to options. */ static bool validate_options(const struct Option **options) { bool valid = true; while (*options != NULL) { const struct Option *option = options[0]; if (!option->validate()) valid = false; options++; } return (valid); } /* SUBCOMMANDS */ /* * queue: name of queue to be created. * q_creation: creation parameters (copied by value). */ static int create(const char *queue, struct Creation q_creation) { int flags = O_RDWR; struct mq_attr stuff = { .mq_curmsgs = 0, .mq_maxmsg = q_creation.depth, .mq_msgsize = q_creation.size, .mq_flags = 0 }; if (!q_creation.block) { flags |= O_NONBLOCK; stuff.mq_flags |= O_NONBLOCK; } mqd_t handle = mq_open(queue, flags); q_creation.exists = handle != fail; if (!q_creation.exists) { /* * apply size and depth checks here. * if queue exists, we can default to existing depth and size. * but for a new queue, we require that input. */ if (validate_size() && validate_depth()) { /* no need to re-apply mode. */ q_creation.set_mode = false; flags |= O_CREAT; handle = mq_open(queue, flags, q_creation.mode, &stuff); } } if (handle == fail) { errno_t what = errno; warnc(what, "mq_open(create)"); return (what); } #ifdef __FreeBSD__ /* * undocumented. * See https://bugs.freebsd.org/bugzilla//show_bug.cgi?id=273230 */ int fd = mq_getfd_np(handle); if (fd < 0) { errno_t what = errno; warnc(what, "mq_getfd_np(create)"); mq_close(handle); return (what); } struct stat status = {0}; int result = fstat(fd, &status); if (result != 0) { errno_t what = errno; warnc(what, "fstat(create)"); mq_close(handle); return (what); } /* do this only if group and / or user given. */ if (q_creation.set_group || q_creation.set_user) { q_creation.user = q_creation.set_user ? q_creation.user : status.st_uid; q_creation.group = q_creation.set_group ? q_creation.group : status.st_gid; result = fchown(fd, q_creation.user, q_creation.group); if (result != 0) { errno_t what = errno; warnc(what, "fchown(create)"); mq_close(handle); return (what); } } /* do this only if altering mode of an existing queue. */ if (q_creation.exists && q_creation.set_mode && q_creation.mode != (status.st_mode & accepted_mode_bits)) { result = fchmod(fd, q_creation.mode); if (result != 0) { errno_t what = errno; warnc(what, "fchmod(create)"); mq_close(handle); return (what); } } #endif /* __FreeBSD__ */ return (mq_close(handle)); } /* queue: name of queue to be removed. */ static int rm(const char *queue) { int result = mq_unlink(queue); if (result != 0) { errno_t what = errno; warnc(what, "mq_unlink"); return (what); } return (result); } /* Return the display character for non-zero mode. */ static char dual(mode_t mode, char display) { return (mode != 0 ? display : '-'); } /* Select one of four display characters based on mode and modifier. */ static char quad(mode_t mode, mode_t modifier) { static const char display[] = "-xSs"; unsigned index = 0; if (mode != 0) index += 1; if (modifier) index += 2; return (display[index]); } /* queue: name of queue to be inspected. */ static int info(const char *queue) { mqd_t handle = mq_open(queue, O_RDONLY); if (handle == fail) { errno_t what = errno; warnc(what, "mq_open(info)"); return (what); } struct mq_attr actual; int result = mq_getattr(handle, &actual); if (result != 0) { errno_t what = errno; warnc(what, "mq_getattr(info)"); return (what); } fprintf(stdout, "queue: '%s'\nQSIZE: %lu\nMSGSIZE: %ld\nMAXMSG: %ld\n" "CURMSG: %ld\nflags: %03ld\n", queue, actual.mq_msgsize * actual.mq_curmsgs, actual.mq_msgsize, actual.mq_maxmsg, actual.mq_curmsgs, actual.mq_flags); #ifdef __FreeBSD__ int fd = mq_getfd_np(handle); struct stat status; result = fstat(fd, &status); if (result != 0) { warn("fstat(info)"); } else { mode_t mode = status.st_mode; fprintf(stdout, "UID: %u\nGID: %u\n", status.st_uid, status.st_gid); fprintf(stdout, "MODE: %c%c%c%c%c%c%c%c%c%c\n", dual(mode & S_ISVTX, 's'), dual(mode & S_IRUSR, 'r'), dual(mode & S_IWUSR, 'w'), quad(mode & S_IXUSR, mode & S_ISUID), dual(mode & S_IRGRP, 'r'), dual(mode & S_IWGRP, 'w'), quad(mode & S_IXGRP, mode & S_ISGID), dual(mode & S_IROTH, 'r'), dual(mode & S_IWOTH, 'w'), dual(mode & S_IXOTH, 'x')); } #endif /* __FreeBSD__ */ return (mq_close(handle)); } /* queue: name of queue to drain one message. */ static int recv(const char *queue) { mqd_t handle = mq_open(queue, O_RDONLY); if (handle == fail) { errno_t what = errno; warnc(what, "mq_open(recv)"); return (what); } struct mq_attr actual; int result = mq_getattr(handle, &actual); if (result != 0) { errno_t what = errno; warnc(what, "mq_attr(recv)"); mq_close(handle); return (what); } char *text = malloc(actual.mq_msgsize + 1); unsigned q_priority = 0; memset(text, 0, actual.mq_msgsize + 1); result = mq_receive(handle, text, actual.mq_msgsize, &q_priority); if (result < 0) { errno_t what = errno; warnc(what, "mq_receive"); mq_close(handle); return (what); } fprintf(stdout, "[%u]: %-*.*s\n", q_priority, result, result, text); return (mq_close(handle)); } /* * queue: name of queue to send one message. * text: message text. * q_priority: message priority in range of 0 to 63. */ static int send(const char *queue, const char *text, unsigned q_priority) { mqd_t handle = mq_open(queue, O_WRONLY); if (handle == fail) { errno_t what = errno; warnc(what, "mq_open(send)"); return (what); } struct mq_attr actual; int result = mq_getattr(handle, &actual); if (result != 0) { errno_t what = errno; warnc(what, "mq_attr(send)"); mq_close(handle); return (what); } int size = strlen(text); if (size > actual.mq_msgsize) { warnx("truncating message to %ld characters.\n", actual.mq_msgsize); size = actual.mq_msgsize; } result = mq_send(handle, text, size, q_priority); if (result != 0) { errno_t what = errno; warnc(what, "mq_send"); mq_close(handle); return (what); } return (mq_close(handle)); } static void usage(FILE *file) { fprintf(file, "usage:\n\tposixmqcontrol [rm|info|recv] -q \n" "\tposixmqcontrol create -q -s -d " "[ -m ] [ -b ] [-u ] [ -g ]\n" "\tposixmqcontrol send -q -c " "[-p ]\n"); } /* end of SUBCOMMANDS */ #define _countof(arg) ((sizeof(arg)) / (sizeof((arg)[0]))) /* convert an errno style error code to a sysexits code. */ static int grace(int err_number) { static const int xlat[][2] = { /* generally means the mqueuefs driver is not loaded. */ {ENOSYS, EX_UNAVAILABLE}, /* no such queue name. */ {ENOENT, EX_OSFILE}, {EIO, EX_IOERR}, {ENODEV, EX_IOERR}, {ENOTSUP, EX_TEMPFAIL}, {EAGAIN, EX_IOERR}, {EPERM, EX_NOPERM}, {EACCES, EX_NOPERM}, {0, EX_OK} }; for (unsigned i = 0; i < _countof(xlat); i++) { if (xlat[i][0] == err_number) return (xlat[i][1]); } return (EX_OSERR); } /* OPTIONS tables */ /* careful: these 'names' arrays must be terminated by a null pointer. */ static const char *names_queue[] = {"-q", "--queue", "-t", "--topic", NULL}; static const struct Option option_queue = { .pattern = names_queue, .parse = parse_queue, .validate = validate_queue}; static const struct Option option_single_queue = { .pattern = names_queue, .parse = parse_single_queue, .validate = validate_single_queue}; static const char *names_depth[] = {"-d", "--depth", "--maxmsg", NULL}; static const struct Option option_depth = { .pattern = names_depth, .parse = parse_depth, .validate = validate_always_true}; static const char *names_size[] = {"-s", "--size", "--msgsize", NULL}; static const struct Option option_size = { .pattern = names_size, .parse = parse_size, .validate = validate_always_true}; static const char *names_block[] = {"-b", "--block", NULL}; static const struct Option option_block = { .pattern = names_block, .parse = parse_block, .validate = validate_always_true}; static const char *names_content[] = { "-c", "--content", "--data", "--message", NULL}; static const struct Option option_content = { .pattern = names_content, .parse = parse_content, .validate = validate_content}; static const char *names_priority[] = {"-p", "--priority", NULL}; static const struct Option option_priority = { .pattern = names_priority, .parse = parse_priority, .validate = validate_always_true}; static const char *names_mode[] = {"-m", "--mode", NULL}; static const struct Option option_mode = { .pattern = names_mode, .parse = parse_mode, .validate = validate_always_true}; static const char *names_group[] = {"-g", "--gid", NULL}; static const struct Option option_group = { .pattern = names_group, .parse = parse_group, .validate = validate_always_true}; static const char *names_user[] = {"-u", "--uid", NULL}; static const struct Option option_user = { .pattern = names_user, .parse = parse_user, .validate = validate_always_true}; /* careful: these arrays must be terminated by a null pointer. */ #ifdef __FreeBSD__ static const struct Option *create_options[] = { &option_queue, &option_depth, &option_size, &option_block, &option_mode, &option_group, &option_user, NULL}; #else /* !__FreeBSD__ */ static const struct Option *create_options[] = { &option_queue, &option_depth, &option_size, &option_block, &option_mode, NULL}; #endif /* __FreeBSD__ */ static const struct Option *info_options[] = {&option_queue, NULL}; static const struct Option *unlink_options[] = {&option_queue, NULL}; static const struct Option *recv_options[] = {&option_single_queue, NULL}; static const struct Option *send_options[] = { &option_queue, &option_content, &option_priority, NULL}; int main(int argc, const char *argv[]) { STAILQ_INIT(&queues); STAILQ_INIT(&contents); if (argc > 1) { const char *verb = argv[1]; int index = 2; if (strcmp("create", verb) == 0 || strcmp("attr", verb) == 0) { parse_options(index, argc, argv, create_options); if (validate_options(create_options)) { int worst = 0; struct element *itq; STAILQ_FOREACH(itq, &queues, links) { const char *queue = itq->text; int result = create(queue, creation); if (result != 0) worst = result; } return (grace(worst)); } return (EX_USAGE); } else if (strcmp("info", verb) == 0 || strcmp("cat", verb) == 0) { parse_options(index, argc, argv, info_options); if (validate_options(info_options)) { int worst = 0; struct element *itq; STAILQ_FOREACH(itq, &queues, links) { const char *queue = itq->text; int result = info(queue); if (result != 0) worst = result; } return (grace(worst)); } return (EX_USAGE); } else if (strcmp("send", verb) == 0) { parse_options(index, argc, argv, send_options); if (validate_options(send_options)) { int worst = 0; struct element *itq; STAILQ_FOREACH(itq, &queues, links) { const char *queue = itq->text; struct element *itc; STAILQ_FOREACH(itc, &contents, links) { const char *content = itc->text; int result = send(queue, content, priority); if (result != 0) worst = result; } } return (grace(worst)); } return (EX_USAGE); } else if (strcmp("recv", verb) == 0 || strcmp("receive", verb) == 0) { parse_options(index, argc, argv, recv_options); if (validate_options(recv_options)) { const char *queue = STAILQ_FIRST(&queues)->text; int worst = recv(queue); return (grace(worst)); } return (EX_USAGE); } else if (strcmp("unlink", verb) == 0 || strcmp("rm", verb) == 0) { parse_options(index, argc, argv, unlink_options); if (validate_options(unlink_options)) { int worst = 0; struct element *itq; STAILQ_FOREACH(itq, &queues, links) { const char *queue = itq->text; int result = rm(queue); if (result != 0) worst = result; } return (grace(worst)); } return (EX_USAGE); } else if (strcmp("help", verb) == 0) { usage(stdout); return (EX_OK); } else { warnx("Unknown verb [%s]", verb); return (EX_USAGE); } } usage(stdout); return (EX_OK); }