summary refs log tree commit diff
diff options
context:
space:
mode:
authorJune McEnroe <june@causal.agency>2019-10-29 18:35:44 -0400
committerJune McEnroe <june@causal.agency>2019-10-29 18:35:44 -0400
commit9f55d9bd3f98915b1a98a4f38d467832e0148b91 (patch)
treead86dc186782e5cb51874a2d48928267604152fc
parentRelay optional 5th RPL_MYINFO parameter (diff)
downloadpounce-9f55d9bd3f98915b1a98a4f38d467832e0148b91.tar.gz
pounce-9f55d9bd3f98915b1a98a4f38d467832e0148b91.zip
Implement getopt_long-integrated configuration parsing
-rw-r--r--.gitignore1
-rw-r--r--Makefile1
-rw-r--r--README.75
-rw-r--r--bounce.c70
-rw-r--r--bounce.h6
-rw-r--r--config.c133
-rw-r--r--pounce.1118
7 files changed, 242 insertions, 92 deletions
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 <assert.h>
 #include <err.h>
 #include <errno.h>
+#include <getopt.h>
 #include <limits.h>
 #include <poll.h>
 #include <signal.h>
@@ -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 <june@causal.agency>
+ *
+ * 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 <http://www.gnu.org/licenses/>.
+ */
+
+#include <err.h>
+#include <getopt.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+
+#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