From 03f72c6173f27198e2e68227cb41e00f8ec4ddc9 Mon Sep 17 00:00:00 2001
From: Guus Sliepen <guus@tinc-vpn.org>
Date: Sun, 15 Jul 2012 18:16:35 +0200
Subject: [PATCH] Allow configuration variables to be added/removed using
 tincctl.

---
 doc/tinc.texi    |  21 ++++
 doc/tincctl.8.in |  24 ++++
 src/tincctl.c    | 282 +++++++++++++++++++++++++++++++++++++++++++++--
 3 files changed, 317 insertions(+), 10 deletions(-)

diff --git a/doc/tinc.texi b/doc/tinc.texi
index 3b312a31..2dbfdfb5 100644
--- a/doc/tinc.texi
+++ b/doc/tinc.texi
@@ -2044,6 +2044,18 @@ the value of this environment variable is used.
 Create initial configuration files and RSA and ECDSA keypairs with default length.
 If no @var{name} for this node is given, it will be asked for.
 
+@item config [set] @var{variable} @var{value}
+Set configuration variable @var{variable} to the given @var{value}.
+All previously existing configuration variables with the same name are removed.
+To set a variable for a specific host, use the notation @var{host}.@var{variable}.
+
+@item config add @var{variable} @var{value}
+As above, but without removing any previously existing configuration variables.
+
+@item config del @var{variable} [@var{value}]
+Remove configuration variables with the same name and @var{value}.
+If no @var{value} is given, all configuration variables with the same name will be removed.
+
 @item start
 Start @samp{tincd}.
 
@@ -2126,6 +2138,15 @@ tincctl -n vpn pcap | tcpdump -r -
 tincctl -n vpn top
 @end example
 
+Example of configuring tinc using tincctl:
+
+@example
+tincctl -n vpn init foo
+tincctl -n vpn config Subnet 192.168.1.0/24
+tincctl -n vpn config bar.Address bar.example.com
+tincctl -n vpn config ConnectTo bar
+@end example
+
 @c ==================================================================
 @node    tincctl top
 @section tincctl top
diff --git a/doc/tincctl.8.in b/doc/tincctl.8.in
index 751e16fb..9f11e118 100644
--- a/doc/tincctl.8.in
+++ b/doc/tincctl.8.in
@@ -52,6 +52,22 @@ Create initial configuration files and RSA and ECDSA keypairs with default lengt
 If no
 .Ar name
 for this node is given, it will be asked for.
+.It config Oo set Oc Ar variable Ar value
+Set configuration variable
+.Ar variable
+to the given
+.Ar value .
+All previously existing configuration variables with the same name are removed.
+To set a variable for a specific host, use the notation
+.Ar host Ns Li . Ns Ar variable .
+.It config add Ar variable Ar value
+As above, but without removing any previously existing configuration variables.
+.It config del Ar variable Op Ar value
+Remove configuration variables with the same name and
+.Ar value .
+If no
+.Ar value
+is given, all configuration variables with the same name will be removed.
 .It start
 Start
 .Xr tincd 8 .
@@ -139,7 +155,15 @@ Examples of some commands:
 tincctl -n vpn dump graph | circo -Txlib
 tincctl -n vpn pcap | tcpdump -r -
 tincctl -n vpn top
+.Pp
 .Ed
+Example of configuring tinc using
+.Nm :
+.Bd -literal -offset indent
+tincctl -n vpn init foo
+tincctl -n vpn config Subnet 192.168.1.0/24
+tincctl -n vpn config bar.Address bar.example.com
+tincctl -n vpn config ConnectTo bar
 .Sh TOP
 The top command connects to a running tinc daemon and repeatedly queries its per-node traffic counters.
 It displays a list of all the known nodes in the left-most column,
diff --git a/src/tincctl.c b/src/tincctl.c
index 1fe4fffe..46688f35 100644
--- a/src/tincctl.c
+++ b/src/tincctl.c
@@ -45,6 +45,8 @@ static char *pidfilename = NULL;			/* pid file location */
 static char controlcookie[1024];
 char *netname = NULL;
 char *confbase = NULL;
+static char *tinc_conf = NULL;
+static char *hosts_dir = NULL;
 
 // Horrible global variables...
 static int pid = 0;
@@ -92,6 +94,10 @@ static void usage(bool status) {
 				"\n"
 				"Valid commands are:\n"
 				"  init [name]                Create initial configuration files.\n"
+				"  config                     Change configuration:\n"
+				"    [set] VARIABLE VALUE     - set VARIABLE to VALUE\n"
+				"    add VARIABLE VALUE       - add VARIABLE with the given VALUE\n"
+				"    del VARIABLE [VALUE]     - remove VARIABLE [only ones with watching VALUE]\n"
 				"  start                      Start tincd.\n"
 				"  stop                       Stop tincd.\n"
 				"  restart                    Restart tincd.\n"
@@ -363,9 +369,9 @@ static void make_names(void) {
 		if(!pidfilename)
 			xasprintf(&pidfilename, "%s/pid", confbase);
 		RegCloseKey(key);
-		if(*installdir)
-			return;
 	}
+
+	if(!*installdir) {
 #endif
 
 	if(!pidfilename)
@@ -380,6 +386,13 @@ static void make_names(void) {
 		if(!confbase)
 			xasprintf(&confbase, CONFDIR "/tinc");
 	}
+
+#ifdef HAVE_MINGW
+	}
+#endif
+
+	xasprintf(&tinc_conf, "%s/tinc.conf", confbase);
+	xasprintf(&hosts_dir, "%s/hosts", confbase);
 }
 
 static char buffer[4096];
@@ -886,15 +899,265 @@ static int cmd_pid(int argc, char *argv[]) {
 	return 0;
 }
 
+static int rstrip(char *value) {
+	int len = strlen(value);
+	while(len && strchr("\t\r\n ", value[len - 1]))
+		value[--len] = 0;
+	return len;
+}
+
+static char *get_my_name() {
+	FILE *f = fopen(tinc_conf, "r");
+	if(!f) {
+		fprintf(stderr, "Could not open %s: %s\n", tinc_conf, strerror(errno));
+		return NULL;
+	}
+
+	char buf[4096];
+	char *value;
+	while(fgets(buf, sizeof buf, f)) {
+		int len = strcspn(buf, "\t =");
+		value = buf + len;
+		value += strspn(value, "\t ");
+		if(*value == '=') {
+			value++;
+			value += strspn(value, "\t ");
+		}
+		if(!rstrip(value))
+			continue;
+		buf[len] = 0;
+		if(strcasecmp(buf, "Name"))
+			continue;
+		if(*value) {
+			fclose(f);
+			return strdup(value);
+		}
+	}
+
+	fclose(f);
+	fprintf(stderr, "Could not find Name in %s.\n", tinc_conf);
+	return NULL;
+}
+
+static char *hostvariables[] = {
+	"Address",
+	"Port",
+	"PublicKey",
+	"Subnet",
+	NULL,
+};
+
+static int cmd_config(int argc, char *argv[]) {
+	if(argc < 2) {
+		fprintf(stderr, "Invalid number of arguments.\n");
+		return 1;
+	}
+
+	int action = 0;
+	if(!strcasecmp(argv[1], "add")) {
+		argv++, argc--, action = 1;
+	} else if(!strcasecmp(argv[1], "del")) {
+		argv++, argc--, action = -1;
+	} else if(!strcasecmp(argv[1], "replace") || !strcasecmp(argv[1], "set") || !strcasecmp(argv[1], "change")) {
+		argv++, argc--, action = 0;
+	}
+
+	if(argc < 2) {
+		fprintf(stderr, "Invalid number of arguments.\n");
+		return 1;
+	}
+
+	// Concatenate the rest of the command line
+	strncpy(line, argv[1], sizeof line - 1);
+	for(int i = 2; i < argc; i++) {
+		strncat(line, " ", sizeof line - 1 - strlen(line));
+		strncat(line, argv[i], sizeof line - 1 - strlen(line));
+	}
+
+	// Liberal parsing into node name, variable name and value.
+	char *node = NULL;
+	char *variable;
+	char *value;
+	int len;
+
+        len = strcspn(line, "\t =");
+        value = line + len;
+        value += strspn(value, "\t ");
+        if(*value == '=') {
+                value++;
+                value += strspn(value, "\t ");
+        }
+        line[len] = '\0';
+	variable = strchr(line, '.');
+	if(variable) {
+		node = line;
+		*variable++ = 0;
+	} else {
+		variable = line;
+	}
+
+	if(!*variable) {
+		fprintf(stderr, "No variable given.\n");
+		return 1;
+	}
+
+	if(action >= 0 && !*value) {
+		fprintf(stderr, "No value for variable given.\n");
+		return 1;
+	}
+
+	// Should this go into our own host config file?
+	if(!node) {
+		for(int i = 0; hostvariables[i]; i++) {
+			if(!strcasecmp(hostvariables[i], variable)) {
+				node = get_my_name();
+				if(!node)
+					return 1;
+				break;
+			}
+		}
+	}
+
+	// Open the right configuration file.
+	char *filename;
+	if(node)
+		xasprintf(&filename, "%s/%s", hosts_dir, node);
+	else
+		filename = tinc_conf;
+
+	FILE *f = fopen(filename, "r");
+	if(!f) {
+		if(action < 0 || errno != ENOENT) {
+			fprintf(stderr, "Could not open configuration file %s: %s\n", filename, strerror(errno));
+			return 1;
+		}
+
+		// If it doesn't exist, create it.
+		f = fopen(filename, "a+");
+		if(!f) {
+			fprintf(stderr, "Could not create configuration file %s: %s\n", filename, strerror(errno));
+			return 1;
+		} else {
+			fprintf(stderr, "Created configuration file %s.\n", filename);
+		}
+	}
+
+	char *tmpfile;
+	xasprintf(&tmpfile, "%s.config.tmp", filename);
+	FILE *tf = fopen(tmpfile, "w");
+	if(!tf) {
+		fprintf(stderr, "Could not open temporary file %s: %s\n", tmpfile, strerror(errno));
+		return 1;
+	}
+
+	// Copy the file, making modifications on the fly.
+	char buf1[4096];
+	char buf2[4096];
+	bool set = false;
+	bool removed = false;
+
+	while(fgets(buf1, sizeof buf1, f)) {
+		buf1[sizeof buf1 - 1] = 0;
+		strcpy(buf2, buf1);
+
+		// Parse line in a simple way
+		char *bvalue;
+		int len;
+
+		len = strcspn(buf2, "\t =");
+		bvalue = buf2 + len;
+		bvalue += strspn(bvalue, "\t ");
+		if(*bvalue == '=') {
+			bvalue++;
+			bvalue += strspn(bvalue, "\t ");
+		}
+		rstrip(bvalue);
+		buf2[len] = '\0';
+
+		// Did it match?
+		if(!strcasecmp(buf2, variable)) {
+			// Del
+			if(action < 0) {
+				if(!*value || !strcasecmp(bvalue, value)) {
+					removed = true;
+					continue;
+				}
+			// Set
+			} else if(action == 0) {
+				// Already set? Delete the rest...
+				if(set)
+					continue;
+				// Otherwise, replace.
+				if(fprintf(tf, "%s = %s\n", variable, value) < 0) {
+					fprintf(stderr, "Error writing to temporary file %s: %s\n", tmpfile, strerror(errno));
+					return 1;
+				}
+				set = true;
+				continue;
+			}
+		}
+
+		// Copy original line...
+		if(fputs(buf1, tf) < 0) {
+			fprintf(stderr, "Error writing to temporary file %s: %s\n", tmpfile, strerror(errno));
+			return 1;
+		}
+	}
+
+	// Make sure we read everything...
+	if(ferror(f) || !feof(f)) {
+		fprintf(stderr, "Error while reading from configuration file %s: %s\n", filename, strerror(errno));
+		return 1;
+	}
+
+	if(fclose(f)) {
+		fprintf(stderr, "Error closing configuration file %s: %s\n", filename, strerror(errno));
+		return 1;
+	}
+
+	// Add new variable if necessary.
+	if(action > 0 || (action == 0 && !set)) {
+		if(fprintf(tf, "%s = %s\n", variable, value) < 0) {
+			fprintf(stderr, "Error writing to temporary file %s: %s\n", tmpfile, strerror(errno));
+			return 1;
+		}
+	}
+
+	// Make sure we wrote everything...
+	if(fclose(tf)) {
+		fprintf(stderr, "Error closing temporary file %s: %s\n", tmpfile, strerror(errno));
+		return 1;
+	}
+
+	// Could we find what we had to remove?
+	if(action < 0 && !removed) {
+		remove(tmpfile);
+		fprintf(stderr, "No configuration variables deleted.\n");
+		return *value;
+	}
+
+	// Replace the configuration file with the new one
+#ifdef HAVE_MINGW
+	if(remove(filename)) {
+		fprintf(stderr, "Error replacing file %s: %s\n", filename, strerror(errno));
+		return 1;
+	}
+#endif
+	if(rename(tmpfile, filename)) {
+		fprintf(stderr, "Error renaming temporary file %s to configuration file %s: %s\n", tmpfile, filename, strerror(errno));
+		return 1;
+	}
+
+	return 0;
+}
+
 static int cmd_init(int argc, char *argv[]) {
-	char *tinc_conf = NULL;
-	xasprintf(&tinc_conf, "%s/tinc.conf", confbase);
-	if(!access(confbase, F_OK)) {
+	if(!access(tinc_conf, F_OK)) {
 		fprintf(stderr, "Configuration file %s already exists!\n", tinc_conf);
 		return 1;
 	}
 
-	if(optind >= argc - 1) {
+	if(argc < 2) {
 		if(isatty(0) && isatty(1)) {
 			char buf[1024];
 			fprintf(stdout, "Enter the Name you want your tinc node to have: ");
@@ -903,9 +1166,7 @@ static int cmd_init(int argc, char *argv[]) {
 				fprintf(stderr, "Error while reading stdin: %s\n", strerror(errno));
 				return 1;
 			}
-			int len = strlen(buf);
-			if(len)
-				buf[--len] = 0;
+			int len = rstrip(buf);
 			if(!len) {
 				fprintf(stderr, "No name given!\n");
 				return 1;
@@ -916,7 +1177,7 @@ static int cmd_init(int argc, char *argv[]) {
 			return 1;
 		}
 	} else {
-		name = strdup(argv[optind + 1]);
+		name = strdup(argv[1]);
 		if(!*name) {
 			fprintf(stderr, "No Name given!\n");
 			return 1;
@@ -1004,6 +1265,7 @@ static const struct {
 	{"pcap", cmd_pcap},
 	{"log", cmd_log},
 	{"pid", cmd_pid},
+	{"config", cmd_config},
 	{"init", cmd_init},
 	{"generate-keys", cmd_generate_keys},
 	{"generate-rsa-keys", cmd_generate_rsa_keys},