diff options
author | June McEnroe <june@causal.agency> | 2022-04-03 21:38:55 -0400 |
---|---|---|
committer | June McEnroe <june@causal.agency> | 2022-04-03 21:38:55 -0400 |
commit | 28f4183abcb20b4a3546956da21ca3956b5cdebf (patch) | |
tree | 046a6544777e871ba81e087cd9a11621769df60e /extra/edit | |
parent | edit: Add prospective pounce-edit(1) manual (diff) | |
download | pounce-28f4183abcb20b4a3546956da21ca3956b5cdebf.tar.gz pounce-28f4183abcb20b4a3546956da21ca3956b5cdebf.zip |
edit: Implement completely unchecked
Diffstat (limited to '')
-rw-r--r-- | extra/edit/.gitignore | 3 | ||||
-rw-r--r-- | extra/edit/edit.c | 454 | ||||
-rw-r--r-- | extra/edit/xdg.c | 129 |
3 files changed, 586 insertions, 0 deletions
diff --git a/extra/edit/.gitignore b/extra/edit/.gitignore new file mode 100644 index 0000000..626888d --- /dev/null +++ b/extra/edit/.gitignore @@ -0,0 +1,3 @@ +*.o +config.mk +pounce-edit diff --git a/extra/edit/edit.c b/extra/edit/edit.c new file mode 100644 index 0000000..6be3d5b --- /dev/null +++ b/extra/edit/edit.c @@ -0,0 +1,454 @@ +/* Copyright (C) 2022 June McEnroe <june@causal.agency> + * + * This program 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 + * (at your option) any later version. + * + * This program 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 this program. If not, see <https://www.gnu.org/licenses/>. + * + * Additional permission under GNU GPL version 3 section 7: + * + * If you modify this Program, or any covered work, by linking or + * combining it with OpenSSL (or a modified version of that library), + * containing parts covered by the terms of the OpenSSL License and the + * original SSLeay license, the licensors of this Program grant you + * additional permission to convey the resulting work. Corresponding + * Source for a non-source form of such a combination shall include the + * source code for the parts of OpenSSL used as well as that of the + * covered work. + */ + +#include <assert.h> +#include <err.h> +#include <limits.h> +#include <signal.h> +#include <stdarg.h> +#include <stdbool.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <sysexits.h> +#include <tls.h> +#include <unistd.h> + +char *configPath(char *buf, size_t cap, const char *path, int i); +FILE *configOpen(const char *path, const char *mode); + +#define WS "\t " + +#define ARRAY_LEN(a) (sizeof(a) / sizeof((a)[0])) + +struct Option { + bool set; + char *name; + char *value; +}; + +struct Config { + size_t cap, len; + struct Option *opts; +}; + +static struct Config inherit; +static struct Config own; +static FILE *config; + +static struct Option configGet(const struct Config *config, const char *name) { + for (size_t i = 0; i < config->len; ++i) { + if (!strcmp(config->opts[i].name, name)) return config->opts[i]; + } + return (struct Option) { .set = false }; +} + +static void +configSet(struct Config *config, const char *name, const char *value) { + for (size_t i = 0; i < config->len; ++i) { + struct Option *opt = &config->opts[i]; + if (strcmp(opt->name, name)) continue; + + opt->set = true; + free(opt->value); + opt->value = NULL; + if (value) { + opt->value = strdup(value); + if (!opt->value) err(EX_OSERR, "strdup"); + } + return; + } + + if (config->len == config->cap) { + config->cap = (config->cap ? config->cap * 2 : 32); + config->opts = realloc( + config->opts, config->cap * sizeof(*config->opts) + ); + if (!config->opts) err(EX_OSERR, "realloc"); + } + + struct Option *opt = &config->opts[config->len++]; + opt->set = true; + opt->name = strdup(name); + if (!opt->name) err(EX_OSERR, "strdup"); + + opt->value = NULL; + if (value) { + opt->value = strdup(value); + if (!opt->value) err(EX_OSERR, "strdup"); + } +} + +static void configUnset(struct Config *config, const char *name) { + for (size_t i = 0; i < config->len; ++i) { + if (strcmp(config->opts[i].name, name)) continue; + config->opts[i].set = false; + break; + } +} + +static void configWrite(const struct Config *config, FILE *file) { + int error = ftruncate(fileno(file), 0); + if (error) err(EX_IOERR, "ftruncate"); + + rewind(file); + fprintf(file, "# written by pounce-edit\n"); + for (size_t i = 0; i < config->len; ++i) { + if (!config->opts[i].set) continue; + fprintf(file, "%s", config->opts[i].name); + if (config->opts[i].value) { + fprintf(file, " = %s", config->opts[i].value); + } + fprintf(file, "\n"); + if (ferror(file)) err(EX_IOERR, "writing configuration"); + } + + error = fflush(file); + if (error) err(EX_IOERR, "writing configuration"); +} + +static void configParse(struct Config *config, const char *path) { + FILE *file = configOpen(path, "r"); + if (!file) exit(EX_NOINPUT); + + ssize_t llen; + size_t cap = 0; + char *buf = NULL; + for (size_t line = 1; 0 < (llen = getline(&buf, &cap, file)); ++line) { + if (buf[llen-1] == '\n') buf[--llen] = '\0'; + + char *name = buf + strspn(buf, WS); + size_t len = strcspn(name, WS "="); + if (!name[0] || name[0] == '#') continue; + + char *equal = &name[len] + strspn(&name[len], WS); + if (*equal && *equal != '=') { + name[len] = '\0'; + errx( + EX_USAGE, "%s:%zu: option `%s' missing equals sign", + path, line, name + ); + } + + char *value = NULL; + if (*equal) { + value = &equal[1] + strspn(&equal[1], WS); + } + + name[len] = '\0'; + configSet(config, name, value); + } + fclose(file); +} + +static bool verbose; +static struct tls *client; + +static void clientWrite(const char *ptr, size_t len) { + while (len) { + ssize_t ret = tls_write(client, ptr, len); + if (ret == TLS_WANT_POLLIN || ret == TLS_WANT_POLLOUT) continue; + if (ret < 0) errx(EX_IOERR, "tls_write: %s", tls_error(client)); + ptr += ret; + len -= ret; + } +} + +static void format(const char *format, ...) +__attribute__((format(printf, 1, 2))); +static void format(const char *format, ...) { + char buf[1024]; + va_list ap; + va_start(ap, format); + int len = vsnprintf(buf, sizeof(buf), format, ap); + va_end(ap); + assert((size_t)len < sizeof(buf)); + if (verbose) fprintf(stderr, "%s", buf); + clientWrite(buf, len); +} + +enum { ParamCap = 2 }; +struct Message { + char *nick; + char *cmd; + char *params[ParamCap]; +}; + +static struct Message parse(char *line) { + if (verbose) fprintf(stderr, "%s\n", line); + struct Message msg = {0}; + if (line[0] == ':') { + char *origin = 1 + strsep(&line, " "); + msg.nick = strsep(&origin, "!"); + } + msg.cmd = strsep(&line, " "); + for (size_t i = 0; line && i < ParamCap; ++i) { + if (line[0] == ':') { + msg.params[i] = &line[1]; + break; + } + msg.params[i] = strsep(&line, " "); + } + return msg; +} + +static void require(const struct Message *msg, bool nick, size_t len) { + if (nick && !msg->nick) errx(EX_PROTOCOL, "%s missing origin", msg->cmd); + for (size_t i = 0; i < len; ++i) { + if (msg->params[i]) continue; + errx(EX_PROTOCOL, "%s missing parameter %zu", msg->cmd, 1 + i); + } +} + +typedef void Handler(struct Message *msg); + +static void handlePing(struct Message *msg) { + require(msg, false, 1); + format("PONG :%s\r\n", msg->params[0]); +} + +static void handleError(struct Message *msg) { + require(msg, false, 1); + errx(EX_UNAVAILABLE, "%s", msg->params[0]); +} + +static void handlePrivmsg(struct Message *msg) { + require(msg, true, 2); + if (strcmp(msg->nick, msg->params[0])) return; + + char *cmd = strsep(&msg->params[1], " "); + char *name = strsep(&msg->params[1], " "); + char *value = msg->params[1]; + + if (!strcmp(cmd, "get")) { + if (!name) { + format("NOTICE %s :", msg->nick); + bool some = false; + for (size_t i = 0; i < own.len; ++i) { + if (!own.opts[i].set) continue; + format("%s\2%s\2", (i ? " " : ""), own.opts[i].name); + some = true; + } + format("%s\r\n", (some ? "" : "none set")); + return; + } + + struct Option opt = configGet(&own, name); + if (!opt.set) opt = configGet(&inherit, name); + if (opt.set && opt.value) { + format("NOTICE %s :\2%s\2 = %s\r\n", msg->nick, name, opt.value); + } else if (opt.set) { + format("NOTICE %s :\2%s\2 is set\r\n", msg->nick, name); + } else { + format("NOTICE %s :\2%s\2 unset\r\n", msg->nick, name); + } + + } else if (!strcmp(cmd, "set")) { + if (!name) { + format( + "NOTICE %s :\2set\2 \35option\35 [\35value\35]\r\n", + msg->nick + ); + return; + } + configSet(&own, name, value); + configWrite(&own, config); + format("NOTICE %s :\2%s\2 set\r\n", msg->nick, name); + + } else if (!strcmp(cmd, "unset")) { + if (!name) { + format("NOTICE %s :\2unset\2 \35option\35\r\n", msg->nick); + return; + } + configUnset(&own, name); + configWrite(&own, config); + struct Option opt = configGet(&inherit, name); + format( + "NOTICE %s :\2%s\2 %s\r\n", + msg->nick, name, (opt.set ? "inherited" : "unset") + ); + + } else if (!strcmp(cmd, "restart")) { + format("QUIT :pounce reloading configuration\r\n"); + } +} + +static const struct { + const char *cmd; + Handler *fn; +} Handlers[] = { + { "ERROR", handleError }, + { "PING", handlePing }, + { "PRIVMSG", handlePrivmsg }, +}; + +static void handle(struct Message *msg) { + if (!msg->cmd) return; + for (size_t i = 0; i < ARRAY_LEN(Handlers); ++i) { + if (strcmp(msg->cmd, Handlers[i].cmd)) continue; + Handlers[i].fn(msg); + break; + } +} + +static void quit(int sig) { + (void)sig; + format("QUIT\r\n"); + tls_close(client); + _exit(EX_OK); +} + +int main(int argc, char *argv[]) { + bool insecure = false; + const char *cert = NULL; + const char *priv = NULL; + const char *host = NULL; + const char *port = NULL; + const char *pass = NULL; + const char *trust = NULL; + const char *user = "pounce-edit"; + + for (int opt; 0 < (opt = getopt(argc, argv, "!c:h:k:p:t:u:vw:"));) { + switch (opt) { + break; case '!': insecure = true; + break; case 'c': cert = optarg; + break; case 'h': host = optarg; + break; case 'k': priv = optarg; + break; case 'p': port = optarg; + break; case 't': trust = optarg; + break; case 'u': user = optarg; + break; case 'v': verbose = true; + break; case 'w': pass = optarg; + break; default: return EX_USAGE; + } + } + if (optind == argc) errx(EX_USAGE, "config required"); + + for (int i = optind; i < argc-1; ++i) { + configParse(&inherit, argv[i]); + } + configParse(&own, argv[argc-1]); + config = configOpen(argv[argc-1], "r+"); + if (!config) exit(EX_NOINPUT); + + if (!host) { + struct Option opt = configGet(&own, "local-host"); + if (!opt.set) opt = configGet(&inherit, "local-host"); + if (!opt.set || !opt.value) errx(EX_USAGE, "host required"); + host = opt.value; + } + if (!port) { + struct Option opt = configGet(&own, "local-port"); + if (!opt.set) opt = configGet(&inherit, "local-port"); + if (opt.set && opt.value) { + port = opt.value; + } else { + port = "6697"; + } + } + + client = tls_client(); + if (!client) errx(EX_SOFTWARE, "tls_client"); + + struct tls_config *config = tls_config_new(); + if (!config) errx(EX_SOFTWARE, "tls_config_new"); + + if (insecure) { + tls_config_insecure_noverifycert(config); + tls_config_insecure_noverifyname(config); + } + + int error; + char path[PATH_MAX]; + if (trust) { + tls_config_insecure_noverifyname(config); + for (int i = 0; configPath(path, sizeof(path), trust, i); ++i) { + error = tls_config_set_ca_file(config, path); + if (!error) break; + } + if (error) errx(EX_NOINPUT, "%s: %s", trust, tls_config_error(config)); + } + if (cert) { + for (int i = 0; configPath(path, sizeof(path), cert, i); ++i) { + if (priv) { + error = tls_config_set_cert_file(config, path); + } else { + error = tls_config_set_keypair_file(config, path, path); + } + if (!error) break; + } + if (error) errx(EX_NOINPUT, "%s: %s", cert, tls_config_error(config)); + } + if (priv) { + for (int i = 0; configPath(path, sizeof(path), priv, i); ++i) { + error = tls_config_set_key_file(config, path); + if (!error) break; + } + if (error) errx(EX_NOINPUT, "%s: %s", priv, tls_config_error(config)); + } + + error = tls_configure(client, config); + if (error) errx(EX_SOFTWARE, "tls_configure: %s", tls_error(client)); + tls_config_free(config); + + error = tls_connect(client, host, port); + if (error) errx(EX_UNAVAILABLE, "tls_connect: %s", tls_error(client)); + + if (pass) format("PASS :%s\r\n", pass); + format( + "CAP REQ :causal.agency/passive\r\n" + "CAP END\r\n" + "NICK *\r\n" + "USER %s 0 * :pounce-edit\r\n", + user + ); + + signal(SIGINT, quit); + signal(SIGTERM, quit); + + size_t len = 0; + char buf[8191 + 512]; + for (;;) { + ssize_t ret = tls_read(client, &buf[len], sizeof(buf) - len); + if (ret == TLS_WANT_POLLIN || ret == TLS_WANT_POLLOUT) continue; + if (ret < 0) errx(EX_IOERR, "tls_read: %s", tls_error(client)); + if (!ret) errx(EX_PROTOCOL, "server closed connection"); + len += ret; + + char *line = buf; + for (;;) { + char *crlf = memmem(line, &buf[len] - line, "\r\n", 2); + if (!crlf) break; + *crlf = '\0'; + struct Message msg = parse(line); + handle(&msg); + line = crlf + 2; + } + len -= line - buf; + memmove(buf, line, len); + } +} diff --git a/extra/edit/xdg.c b/extra/edit/xdg.c new file mode 100644 index 0000000..b9015b2 --- /dev/null +++ b/extra/edit/xdg.c @@ -0,0 +1,129 @@ +/* Copyright (C) 2019, 2020 June McEnroe <june@causal.agency> + * + * This program 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 + * (at your option) any later version. + * + * This program 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 this program. If not, see <https://www.gnu.org/licenses/>. + * + * Additional permission under GNU GPL version 3 section 7: + * + * If you modify this Program, or any covered work, by linking or + * combining it with OpenSSL (or a modified version of that library), + * containing parts covered by the terms of the OpenSSL License and the + * original SSLeay license, the licensors of this Program grant you + * additional permission to convey the resulting work. Corresponding + * Source for a non-source form of such a combination shall include the + * source code for the parts of OpenSSL used as well as that of the + * covered work. + */ + +#include <err.h> +#include <errno.h> +#include <limits.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <sys/stat.h> +#include <sysexits.h> + +#define SUBDIR "pounce" + +struct Base { + const char *envHome; + const char *envDirs; + const char *defHome; + const char *defDirs; +}; + +static const struct Base Config = { + .envHome = "XDG_CONFIG_HOME", + .envDirs = "XDG_CONFIG_DIRS", + .defHome = ".config", + .defDirs = "/etc/xdg", +}; + +static const struct Base Data = { + .envHome = "XDG_DATA_HOME", + .envDirs = "XDG_DATA_DIRS", + .defHome = ".local/share", + .defDirs = "/usr/local/share:/usr/share", +}; + +static char *basePath( + struct Base base, char *buf, size_t cap, const char *path, int i +) { + if (path[strspn(path, ".")] == '/') { + if (i > 0) return NULL; + snprintf(buf, cap, "%s", path); + return buf; + } + + if (i > 0) { + const char *dirs = getenv(base.envDirs); + if (!dirs) dirs = base.defDirs; + for (; i > 1; --i) { + dirs += strcspn(dirs, ":"); + dirs += (*dirs == ':'); + } + if (!*dirs) return NULL; + snprintf( + buf, cap, "%.*s/" SUBDIR "/%s", + (int)strcspn(dirs, ":"), dirs, path + ); + return buf; + } + + const char *home = getenv("HOME"); + const char *baseHome = getenv(base.envHome); + if (baseHome) { + snprintf(buf, cap, "%s/" SUBDIR "/%s", baseHome, path); + } else if (home) { + snprintf(buf, cap, "%s/%s/" SUBDIR "/%s", home, base.defHome, path); + } else { + errx(EX_USAGE, "HOME unset"); + } + return buf; +} + +char *configPath(char *buf, size_t cap, const char *path, int i) { + return basePath(Config, buf, cap, path, i); +} + +char *dataPath(char *buf, size_t cap, const char *path, int i) { + return basePath(Data, buf, cap, path, i); +} + +FILE *configOpen(const char *path, const char *mode) { + char buf[PATH_MAX]; + for (int i = 0; configPath(buf, sizeof(buf), path, i); ++i) { + FILE *file = fopen(buf, mode); + if (file) return file; + if (errno != ENOENT) warn("%s", buf); + } + warn("%s", configPath(buf, sizeof(buf), path, 0)); + return NULL; +} + +FILE *dataOpen(const char *path, const char *mode) { + char buf[PATH_MAX]; + for (int i = 0; dataPath(buf, sizeof(buf), path, i); ++i) { + FILE *file = fopen(buf, mode); + if (file) return file; + if (errno != ENOENT) warn("%s", buf); + } + if (mode[0] != 'r') { + int error = mkdir(dataPath(buf, sizeof(buf), "", 0), S_IRWXU); + if (error && errno != EEXIST) warn("%s", buf); + } + FILE *file = fopen(dataPath(buf, sizeof(buf), path, 0), mode); + if (!file) warn("%s", buf); + return file; +} |