From 9f55d9bd3f98915b1a98a4f38d467832e0148b91 Mon Sep 17 00:00:00 2001 From: Curtis McEnroe Date: Tue, 29 Oct 2019 18:35:44 -0400 Subject: Implement getopt_long-integrated configuration parsing --- .gitignore | 1 + Makefile | 1 + README.7 | 5 ++- bounce.c | 70 ++++++++++++++++---------------- bounce.h | 6 +++ config.c | 133 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ pounce.1 | 118 ++++++++++++++++++++++++++++-------------------------- 7 files changed, 242 insertions(+), 92 deletions(-) create mode 100644 config.c diff --git a/.gitignore b/.gitignore index 1833d1e..c8c6546 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ +*.conf *.o *.pem config.mk diff --git a/Makefile b/Makefile index 650c255..6af3477 100644 --- a/Makefile +++ b/Makefile @@ -12,6 +12,7 @@ LDLIBS = -ltls OBJS += bounce.o OBJS += client.o +OBJS += config.o OBJS += listen.o OBJS += ring.o OBJS += server.o diff --git a/README.7 b/README.7 index 59220cc..8cee7f1 100644 --- a/README.7 +++ b/README.7 @@ -1,4 +1,4 @@ -.Dd October 26, 2019 +.Dd October 29, 2019 .Dt README 7 .Os "Causal Agency" . @@ -60,6 +60,9 @@ remote client functions state shared between clients .It Pa ring.c buffer between server and clients +.It Pa config.c +.Xr getopt_long 3 Ns -integrated +configuration parsing .It Pa rc.pounce .Fx .Xr rc 8 diff --git a/bounce.c b/bounce.c index ff54c98..79a75f2 100644 --- a/bounce.c +++ b/bounce.c @@ -17,6 +17,7 @@ #include #include #include +#include #include #include #include @@ -95,31 +96,9 @@ static void saveLoad(const char *path) { atexit(saveExit); } -static char *sensitive(char *arg) { - char *value = NULL; - if (arg[0] == '@') { - FILE *file = fopen(&arg[1], "r"); - if (!file) err(EX_NOINPUT, "%s", &arg[1]); - - size_t cap = 0; - ssize_t len = getline(&value, &cap, file); - if (len < 0) err(EX_IOERR, "%s", &arg[1]); - - if (len && value[len - 1] == '\n') value[len - 1] = '\0'; - fclose(file); - - } else { - value = strdup(arg); - if (!value) err(EX_OSERR, "strdup"); - } - memset(arg, '\0', strlen(arg)); - arg[0] = '*'; - return value; -} - int main(int argc, char *argv[]) { - const char *localHost = "localhost"; - const char *localPort = "6697"; + const char *bindHost = "localhost"; + const char *bindPort = "6697"; char certPath[PATH_MAX] = ""; char privPath[PATH_MAX] = ""; const char *save = NULL; @@ -136,20 +115,43 @@ int main(int argc, char *argv[]) { const char *away = "pounced :3"; const char *quit = "connection reset by purr"; + const char *Opts = "!A:C:H:K:NP:Q:W:a:f:h:j:n:p:r:u:vw:"; + const struct option LongOpts[] = { + { "insecure", no_argument, NULL, '!' }, + { "away", required_argument, NULL, 'A' }, + { "cert", required_argument, NULL, 'C' }, + { "bind-host", required_argument, NULL, 'H' }, + { "key", required_argument, NULL, 'K' }, + { "names", no_argument, NULL, 'N' }, + { "bind-port", required_argument, NULL, 'P' }, + { "quit", required_argument, NULL, 'Q' }, + { "client-pass", required_argument, NULL, 'W' }, + { "sasl", required_argument, NULL, 'a' }, + { "save", required_argument, NULL, 'f' }, + { "host", required_argument, NULL, 'h' }, + { "join", required_argument, NULL, 'j' }, + { "nick", required_argument, NULL, 'n' }, + { "port", required_argument, NULL, 'p' }, + { "real", required_argument, NULL, 'r' }, + { "user", required_argument, NULL, 'u' }, + { "verbose", no_argument, NULL, 'v' }, + { "pass", required_argument, NULL, 'w' }, + {0}, + }; + int opt; - const char *opts = "!A:C:H:K:NP:Q:W:a:f:h:j:n:p:r:u:vw:"; - while (0 < (opt = getopt(argc, argv, opts))) { + while (0 < (opt = getopt_config(argc, argv, Opts, LongOpts, NULL))) { switch (opt) { break; case '!': insecure = true; break; case 'A': away = optarg; break; case 'C': strlcpy(certPath, optarg, sizeof(certPath)); - break; case 'H': localHost = optarg; + break; case 'H': bindHost = optarg; break; case 'K': strlcpy(privPath, optarg, sizeof(privPath)); break; case 'N': stateJoinNames = true; - break; case 'P': localPort = optarg; + break; case 'P': bindPort = optarg; break; case 'Q': quit = optarg; - break; case 'W': clientPass = sensitive(optarg); - break; case 'a': auth = sensitive(optarg); + break; case 'W': clientPass = optarg; + break; case 'a': auth = optarg; break; case 'f': save = optarg; break; case 'h': host = optarg; break; case 'j': join = optarg; @@ -158,16 +160,16 @@ int main(int argc, char *argv[]) { break; case 'r': real = optarg; break; case 'u': user = optarg; break; case 'v': verbose = true; - break; case 'w': pass = sensitive(optarg); + break; case 'w': pass = optarg; break; default: return EX_USAGE; } } if (!certPath[0]) { - snprintf(certPath, sizeof(certPath), DEFAULT_CERT_PATH, localHost); + snprintf(certPath, sizeof(certPath), DEFAULT_CERT_PATH, bindHost); } if (!privPath[0]) { - snprintf(privPath, sizeof(privPath), DEFAULT_PRIV_PATH, localHost); + snprintf(privPath, sizeof(privPath), DEFAULT_PRIV_PATH, bindHost); } if (!host) errx(EX_USAGE, "no host"); @@ -183,7 +185,7 @@ int main(int argc, char *argv[]) { listenConfig(certPath, privPath); int bind[8]; - size_t binds = listenBind(bind, 8, localHost, localPort); + size_t binds = listenBind(bind, 8, bindHost, bindPort); int server = serverConnect(insecure, host, port); stateLogin(pass, auth, nick, user, real); diff --git a/bounce.h b/bounce.h index 6e9ddbd..c37ff61 100644 --- a/bounce.h +++ b/bounce.h @@ -98,3 +98,9 @@ bool stateReady(void); void stateParse(char *line); void stateSync(struct Client *client); const char *stateEcho(void); + +struct option; +int getopt_config( + int argc, char *const *argv, + const char *optstring, const struct option *longopts, int *longindex +); diff --git a/config.c b/config.c new file mode 100644 index 0000000..adf2b34 --- /dev/null +++ b/config.c @@ -0,0 +1,133 @@ +/* Copyright (C) 2019 C. McEnroe + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero 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 Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +#include +#include +#include +#include +#include + +#define WS "\t " + +static const char *path; +static FILE *file; +static size_t num; +static char *line; +static size_t cap; + +static int clean(int opt) { + if (file) fclose(file); + free(line); + line = NULL; + cap = 0; + return opt; +} + +int getopt_config( + int argc, char *const *argv, + const char *optstring, const struct option *longopts, int *longindex +) { + int opt = getopt_long(argc, argv, optstring, longopts, longindex); + if (opt >= 0) return opt; + + for (;;) { + if (!file) { + if (optind < argc) { + num = 0; + path = argv[optind++]; + file = fopen(path, "r"); + if (!file) { + warn("%s", path); + return clean('?'); + } + } else { + return clean(-1); + } + } + + for (;;) { + ssize_t llen = getline(&line, &cap, file); + if (ferror(file)) { + warn("%s", path); + return clean('?'); + } + if (llen <= 0) break; + if (line[llen - 1] == '\n') line[llen - 1] = '\0'; + num++; + + char *name = line + strspn(line, WS); + size_t len = strcspn(name, WS "="); + if (!name[0] || name[0] == '#') continue; + + const struct option *option; + for (option = longopts; option->name; ++option) { + if (strlen(option->name) != len) continue; + if (!strncmp(option->name, name, len)) break; + } + if (!option->name) { + warnx( + "%s:%zu: unrecognized option `%.*s'", + path, num, (int)len, name + ); + return clean('?'); + } + + char *equal = &name[len] + strspn(&name[len], WS); + if (*equal && *equal != '=') { + warnx( + "%s:%zu: option `%s' missing equals sign", + path, num, option->name + ); + return clean('?'); + } + if (option->has_arg == no_argument && *equal) { + warnx( + "%s:%zu: option `%s' doesn't allow an argument", + path, num, option->name + ); + return clean('?'); + } + if (option->has_arg == required_argument && !*equal) { + warnx( + "%s:%zu: option `%s' requires an argument", + path, num, option->name + ); + return clean(':'); + } + + optarg = NULL; + if (*equal) { + char *arg = &equal[1] + strspn(&equal[1], WS); + optarg = strdup(arg); + if (!optarg) { + warn("getopt_config"); + return clean('?'); + } + } + + if (longindex) *longindex = option - longopts; + if (option->flag) { + *option->flag = option->val; + return 0; + } else { + return option->val; + } + } + + fclose(file); + file = NULL; + } +} diff --git a/pounce.1 b/pounce.1 index 82cc6de..acb8433 100644 --- a/pounce.1 +++ b/pounce.1 @@ -9,15 +9,15 @@ .Sh SYNOPSIS .Nm .Op Fl Nv -.Op Fl A Ar away -.Op Fl C Ar cert +.Op Fl A Ar mesg +.Op Fl C Ar path .Op Fl H Ar host -.Op Fl K Ar priv +.Op Fl K Ar path .Op Fl P Ar port -.Op Fl Q Ar quit +.Op Fl Q Ar mesg .Op Fl W Ar pass .Op Fl a Ar auth -.Op Fl f Ar file +.Op Fl f Ar path .Op Fl h Ar host .Op Fl j Ar chan .Op Fl n Ar nick @@ -25,6 +25,7 @@ .Op Fl r Ar real .Op Fl u Ar user .Op Fl w Ar pass +.Op Ar config ... . .Sh DESCRIPTION The @@ -39,17 +40,27 @@ to know when missed messages were received and uniquely identify themselves by username. . .Pp +Options can be loaded from +files listed on the command line. +Each option is placed on a line, +and lines beginning with +.Ql # +are ignored. +The options are listed below +following their corresponding flags. +. +.Pp The arguments are as follows: . -.Bl -tag -width "-W @file" -.It Fl A Ar away +.Bl -tag -width Ds +.It Fl A Ar mesg , Cm away = Ar mesg Set away status to -.Ar away +.Ar mesg when no clients are connected. . -.It Fl C Ar cert +.It Fl C Ar path , Cm cert = Ar path Load TLS certificate from -.Ar cert . +.Ar path . The default path is .Pa /usr/local/etc/letsencrypt/live/ Ns Ar host Ns Pa /fullchain.pem where @@ -57,14 +68,14 @@ where is set by .Fl H . . -.It Fl H Ar host -Bind to local +.It Fl H Ar host , Cm bind-host = Ar host +Bind to .Ar host . The default host is localhost. . -.It Fl K Ar priv +.It Fl K Ar path , Cm key = Ar path Load TLS private key from -.Ar priv . +.Ar path . The default path is .Pa /usr/local/etc/letsencrypt/live/ Ns Ar host Ns Pa /privkey.pem where @@ -72,96 +83,80 @@ where is set by .Fl H . . -.It Fl N +.It Fl N , Cm names Request .Ql NAMES for each channel when a client connects. . -.It Fl P Ar port -Bind to local +.It Fl P Ar port , Cm bind-port = Ar port +Bind to .Ar port . The default port is 6697. . -.It Fl Q Ar quit +.It Fl Q Ar mesg , Cm quit = Ar mesg Quit with message -.Ar quit +.Ar mesg when shutting down. . -.It Fl W Ar pass +.It Fl W Ar pass , Cm client-pass = Ar pass Require the password .Ar pass to connect. . -.It Fl W Cm @ Ns Ar file -Set -.Fl W Ar pass -to the first line read from -.Ar file . -. -.It Fl a Ar auth -Authenticate with SASL PLAIN. -.Ar auth -is a colon-separated username and password. -. -.It Fl a Cm @ Ns Ar file -Set -.Fl a Ar auth -to the first line read from -.Ar file . +.It Fl a Ar user : Ns Ar pass , Cm sasl = Ar user : Ns Ar pass +Authenticate as +.Ar user +with +.Ar pass +using SASL PLAIN. . -.It Fl f Ar file +.It Fl f Ar path , Cm save = Ar path Load the contents of the ring buffer from -.Ar file -if it exists. -The file is then truncated. +.Ar path , +if it exists, +and truncate it. On shutdown, save the contents of the ring buffer to -.Ar file . +.Ar path . . -.It Fl h Ar host +.It Fl h Ar host , Cm host = Ar host Connect to .Ar host . . -.It Fl j Ar chan +.It Fl j Ar chan , Cm join = Ar chan Join the comma-separated list of .Ar chan . . -.It Fl n Ar nick +.It Fl n Ar nick , Cm nick = Ar nick Set nickname to .Ar nick . The default nickname is the user's name. . -.It Fl p Ar port +.It Fl p Ar port , Cm port = Ar port Connect to .Ar port . The default port is 6697. . -.It Fl r Ar real +.It Fl r Ar real , Cm real = Ar real Set realname to .Ar real . The default realname is the same as the nickname. . -.It Fl u Ar user +.It Fl u Ar user , Cm user = Ar user Set the username to .Ar user . The default username is the same as the nickname. . -.It Fl v +.It Fl v , Cm verbose Write IRC messages to standard error in red to the server, green from the server, yellow from clients and blue to clients. . -.It Fl w Ar pass -Log in with the password +.It Fl w Ar pass , Cm pass = Ar pass +Log in with the server password .Ar pass . -. -.It Fl w Cm @ Ns Ar file -Set -.Fl w Ar pass -to the first line read from -.Ar file . .El . .Pp @@ -189,10 +184,19 @@ The default nickname. .El . .Sh EXAMPLES -.Bd -literal +Command line: +.Bd -literal -offset indent .Nm Fl H Li pounce.example.org Fl h Li chat.freenode.net Fl j Li '#ascii.town' .Ed . +.Pp +Configuration file: +.Bd -literal -offset indent +bind-host = pounce.example.org +host = chat.freenode.net +join = #ascii.town +.Ed +. .Sh STANDARDS The .Nm -- cgit 1.4.1