[RFC/PATCH v2] ssh: config directive to modify the local environment

Bert Wesarg bert.wesarg at googlemail.com
Fri Jan 7 23:00:55 EST 2011


This provides a mechanism to attach arbitrary configure options into the
ssh_config file and use them from the LocalCommand and ProxyCommand.

Examples:

    # set FOO to foo
    LocalEnvMod FOO = foo

    # append bar to FOO with default separator ","
    LocalEnvMod FOO += bar

    # unset FOO
    LocalEnvMod FOO =

    # append foo to BAR with separator ":", if BAR is empty no separator will
    # be used
    LocalEnvMod BAR +:= foo

    # prepend baz to BAR with separator ":"
    LocalEnvMod BAR %:= baz

Currently any chararacter can be used as separator.

My intended use case for this is to automount arbitrary remote directories
via sshfs based on the host by the mux master via LocalCommand. I have a
default of 'mount the remote home at ~/Remotes/<host>' but I can add more
mount directives based on individual hosts.

The LocalEnvMod directive is cumulative and are exectuted in one file in order
(ie. from top to buttom), but in reverse order of file parsing. That is
directives in ~/.ssh/config are executed after /etc/ssh/ssh_config but
command line directives (-oLocalEnvMod=) will be executed last.

If there are security concerns what variables are allowed to change, we could
use a blacklist (HOME, SHELL, ... come to mind).

---

v2: This time with my own fix for the %n expension.

 readconf.c             |  172 +++++++++++++++++++++++++++++++++++++++-
 readconf.h             |   11 +++
 regress/Makefile       |    4 +-
 regress/localenvmod.sh |  210 ++++++++++++++++++++++++++++++++++++++++++++++++
 ssh.c                  |   57 +++++++++++++
 5 files changed, 452 insertions(+), 2 deletions(-)

diff --git a/readconf.c b/readconf.c
index eb4a8b9..9f862a9 100644
--- a/readconf.c
+++ b/readconf.c
@@ -135,7 +135,7 @@ typedef enum {
 	oTunnel, oTunnelDevice, oLocalCommand, oPermitLocalCommand,
 	oVisualHostKey, oUseRoaming, oZeroKnowledgePasswordAuthentication,
 	oKexAlgorithms, oIPQoS,
-	oDeprecated, oUnsupported
+	oDeprecated, oUnsupported, oLocalEnvMod
 } OpCodes;
 
 /* Textual representations of the tokens. */
@@ -245,6 +245,7 @@ static struct {
 #endif
 	{ "kexalgorithms", oKexAlgorithms },
 	{ "ipqos", oIPQoS },
+	{ "localenvmod", oLocalEnvMod },
 
 	{ NULL, oBadOption }
 };
@@ -325,6 +326,45 @@ clear_forwardings(Options *options)
 }
 
 /*
+ * Adds a command to modify the local environment. Never returns if there is an
+ * error.
+ */
+
+void
+add_local_env_mod(Options *options, const EnvMod *newmod)
+{
+	EnvMod *mod;
+
+	options->local_env_mods = xrealloc(options->local_env_mods,
+	    options->num_local_env_mods + 1,
+	    sizeof(*options->local_env_mods));
+	mod = &options->local_env_mods[options->num_local_env_mods++];
+
+	mod->name = newmod->name;
+	mod->operation = newmod->operation;
+	mod->value = newmod->value;
+}
+
+static void
+clear_local_env_mods(Options *options)
+{
+	int i;
+
+	for (i = 0; i < options->num_local_env_mods; i++) {
+		xfree(options->local_env_mods[i].name);
+		xfree(options->local_env_mods[i].value);
+	}
+	if (options->num_local_env_mods > 0) {
+		xfree(options->local_env_mods);
+		options->num_local_env_mods = 0;
+		options->local_env_mods = NULL;
+	}
+}
+
+static int
+parse_env_mod(EnvMod *mod, const char *modspec);
+
+/*
  * Returns the number of the token pointed to by cp or oBadOption.
  */
 
@@ -359,6 +399,7 @@ process_config_line(Options *options, const char *host,
 	long long orig, val64;
 	size_t len;
 	Forward fwd;
+	EnvMod mod;
 
 	/* Strip trailing whitespace */
 	for (len = strlen(line) - 1; len > 0; len--) {
@@ -997,6 +1038,20 @@ parse_int:
 		intptr = &options->use_roaming;
 		goto parse_flag;
 
+	case oLocalEnvMod:
+		/* We try to consume the complete line */
+		arg = s;
+		s = s + strlen(s);
+
+		if (parse_env_mod(&mod, arg) == 0)
+			fatal("%.200s line %d: Bad env mod specification.",
+			    filename, linenum);
+
+		if (*activep) {
+			add_local_env_mod(options, &mod);
+		}
+		break;
+
 	case oDeprecated:
 		debug("%s line %d: Deprecated option \"%s\"",
 		    filename, linenum, keyword);
@@ -1034,6 +1089,7 @@ read_config_file(const char *filename, const char *host, Options *options,
 	char line[1024];
 	int active, linenum;
 	int bad_options = 0;
+	int prev_num_local_env_mods = options->num_local_env_mods;
 
 	if ((f = fopen(filename, "r")) == NULL)
 		return 0;
@@ -1066,6 +1122,36 @@ read_config_file(const char *filename, const char *host, Options *options,
 	if (bad_options > 0)
 		fatal("%s: terminating, %d bad configuration options",
 		    filename, bad_options);
+
+	/* swap LocalEnvMod directives from this file in-front of previous ones */
+	if (prev_num_local_env_mods != options->num_local_env_mods) {
+		EnvMod *start = options->local_env_mods;
+		EnvMod *split = options->local_env_mods + prev_num_local_env_mods;
+		EnvMod *end   = options->local_env_mods + options->num_local_env_mods;
+		EnvMod tmp, *low, *high;
+
+		low = start; high = split - 1;
+		while (low < high) {
+			tmp = *high;
+			*high-- = *low;
+			*low++ = tmp;
+		}
+
+		low = split; high = end - 1;
+		while (low < high) {
+			tmp = *high;
+			*high-- = *low;
+			*low++ = tmp;
+		}
+
+		low = start; high = end - 1;
+		while (low < high) {
+			tmp = *high;
+			*high-- = *low;
+			*low++ = tmp;
+		}
+	}
+
 	return 1;
 }
 
@@ -1157,6 +1243,8 @@ initialize_options(Options * options)
 	options->zero_knowledge_password_authentication = -1;
 	options->ip_qos_interactive = -1;
 	options->ip_qos_bulk = -1;
+	options->local_env_mods = NULL;
+	options->num_local_env_mods = 0;
 }
 
 /*
@@ -1420,3 +1508,85 @@ parse_forward(Forward *fwd, const char *fwdspec, int dynamicfwd, int remotefwd)
 	}
 	return (0);
 }
+
+/*
+ * variablename[whitespace][{+,%}[separator]]=[whitespace]value
+ * value may be optional for set command (ie. w/o +)
+ * SSHFS_MOUNT += mars-fastfs:/fastfs
+ * SOME_PATH +:= /some/bin
+ * SOME_PATH %:= /some/other/bin
+ * '+' and '%' shouldn't be used as separator
+ * TODO: don't overwrite?
+ * VAR ?= value
+ */
+int
+parse_env_mod(EnvMod *mod, const char *modspec)
+{
+	char *p, *cp, *ne, *eq;
+	size_t len;
+
+	memset(mod, '\0', sizeof(*mod));
+
+	cp = p = xstrdup(modspec);
+
+	/* skip leading spaces */
+	while (isspace(*cp))
+		cp++;
+
+	eq = strchr(cp, '=');
+
+	if (!eq)
+		return 0;
+
+	len = eq - modspec;
+	if (len == 0)
+		return 0;
+
+	ne = eq;
+	if ((len > 2 && eq[-1] == '+') || (len > 3 && eq[-2] == '+') ||
+	    (len > 2 && eq[-1] == '%') || (len > 3 && eq[-2] == '%')) {
+		ne--;
+		/* append/prepend, comma is default separator */
+		mod->operation = ',';
+		if ((len > 3 && eq[-2] == '+') || (len > 3 && eq[-2] == '%')) {
+			ne--;
+			mod->operation = eq[-1];
+		}
+		/* prepend? */
+		if ((len > 2 && eq[-1] == '%') || (len > 3 && eq[-2] == '%')) {
+			mod->operation = -mod->operation;
+		}
+	}
+	/* Remove traling whitespace from variable name */
+	while ((ne - 1) > cp && isspace(ne[-1]))
+		ne--;
+	/* Terminate variable name */
+	*ne = '\0';
+
+	if (strlen(cp) == 0)
+		return 0;
+
+	/* Skip leading spaces for variable value */
+	eq++;
+	while (isspace(*eq))
+		eq++;
+
+	/* Remove possible double quotes around value */
+	len = strlen(eq);
+	if (len > 1 && eq[0] == '"' && eq[len - 1] == '"') {
+		eq[len - 1] = '\0';
+		eq++;
+		len -= 2;
+	}
+
+	/* Allow to unset when  */
+        if (mod->operation && len == 0)
+		return 0;
+
+	mod->name = xstrdup(cp);
+	mod->value = xstrdup(eq);
+
+	xfree(p);
+
+	return 1;
+}
diff --git a/readconf.h b/readconf.h
index ee160df..166631f 100644
--- a/readconf.h
+++ b/readconf.h
@@ -25,6 +25,13 @@ typedef struct {
 	int	  connect_port;		/* Port to connect on connect_host. */
 	int	  allocated_port;	/* Dynamically allocated listen port */
 }       Forward;
+/* Data structure for representing a local env modification. */
+
+typedef struct {
+	char	 *name;		/* The name of the variable. */
+	int	  operation;	/* The operation (\0 for set, other chars for append with this separator). */
+	char	 *value;	/* The operand for the operation. */
+}       EnvMod;
 /* Data structure for representing option data. */
 
 #define MAX_SEND_ENV	256
@@ -132,6 +139,10 @@ typedef struct {
 
 	int	use_roaming;
 
+	/* Changes to the local envirionment */
+	int	num_local_env_mods;
+	EnvMod	*local_env_mods;
+
 }       Options;
 
 #define SSHCTL_MASTER_NO	0
diff --git a/regress/Makefile b/regress/Makefile
index 85fd3a5..b5724f8 100644
--- a/regress/Makefile
+++ b/regress/Makefile
@@ -51,6 +51,7 @@ LTESTS= 	connect \
 		cfgmatch \
 		addrmatch \
 		localcommand \
+		localenvmod \
 		forcecommand \
 		portnum \
 		keytype \
@@ -76,7 +77,8 @@ CLEANFILES=	t2.out t6.out1 t6.out2 t7.out t7.out.pub copy.1 copy.2 \
 		sshd_proxy_bak rsa_ssh2_cr.prv rsa_ssh2_crnl.prv \
 		known_hosts-cert host_ca_key* cert_host_key* \
 		putty.rsa2 sshd_proxy_orig \
-		authorized_principals_${USER} expect actual
+		authorized_principals_${USER} expect actual \
+		localenvmods.in localenvmods.expect localenvmods.out ssh_proxy.tmpl
 
 # Enable all malloc(3) randomisations and checks
 TEST_ENV=      "MALLOC_OPTIONS=AFGJPRX"
diff --git a/regress/localenvmod.sh b/regress/localenvmod.sh
new file mode 100644
index 0000000..c986816
--- /dev/null
+++ b/regress/localenvmod.sh
@@ -0,0 +1,210 @@
+#	Placed in the Public Domain.
+
+tid="localenvmod"
+
+cp $OBJ/ssh_proxy $OBJ/ssh_proxy.tmpl
+echo 'PermitLocalCommand yes' >>$OBJ/ssh_proxy.tmpl
+
+cat <<EOI | sed -e 's/<SP>/ /g' >localenvmods.in
+FOO=foo
+FOO=<SP>foo
+FOO<SP>=foo
+FOO<SP>=<SP>foo
+FOO=foo<SP>
+FOO=<SP>foo<SP>
+FOO<SP>=foo<SP>
+FOO<SP>=<SP>foo<SP>
+FOO="<SP>foo<SP>"
+FOO=<SP>"<SP>foo<SP>"
+FOO<SP>="<SP>foo<SP>"
+FOO<SP>=<SP>"<SP>foo<SP>"
+FOO="<SP>foo<SP>"<SP>
+FOO=<SP>"<SP>foo<SP>"<SP>
+FOO<SP>="<SP>foo<SP>"<SP>
+FOO<SP>=<SP>"<SP>foo<SP>"
+EOI
+
+tid="localenvmod quoting"
+
+cat <<EOE | sed -e 's/<SP>/ /g' >localenvmods.expect
+foo
+foo
+foo
+foo
+foo
+foo
+foo
+foo
+<SP>foo<SP>
+<SP>foo<SP>
+<SP>foo<SP>
+<SP>foo<SP>
+<SP>foo<SP>
+<SP>foo<SP>
+<SP>foo<SP>
+<SP>foo<SP>
+EOE
+
+verbose "test $tid"
+exec 4>localenvmods.out
+while IFS= read mod; do
+	trace "test $tid: <$mod>"
+	(
+		cat $OBJ/ssh_proxy.tmpl
+		printf 'LocalCommand printf "%%%%s\\n" "$FOO"\n'
+		printf "LocalEnvMod %s\n" "$mod"
+	) >$OBJ/ssh_proxy
+	${SSH} -n -F $OBJ/ssh_proxy somehost true >&4 || fail "$tid: <$mod>"
+done <localenvmods.in
+exec 4>&-
+
+diff localenvmods.expect localenvmods.out || fail "$tid"
+
+cat >localenvmods.in <<EOI
+FOO += foo
+FOO %= foo
+FOO +:= foo
+FOO %:= foo
+FOO + = foo
+FOO % = foo
+EOI
+
+tid="localenvmod set (preset: unset)"
+
+cat >localenvmods.expect <<EOE
+foo
+foo
+foo
+foo
+foo
+foo
+EOE
+
+verbose "test $tid"
+exec 4>localenvmods.out
+while IFS= read mod; do
+	trace "test $tid: <$mod>"
+	(
+		cat $OBJ/ssh_proxy.tmpl
+		printf 'LocalCommand printf "%%%%s\\n" "$FOO"\n'
+		printf "LocalEnvMod %s\n" "$mod"
+	) >$OBJ/ssh_proxy
+	${SSH} -n -F $OBJ/ssh_proxy somehost true >&4 || fail "$tid: <$mod>"
+done <localenvmods.in
+exec 4>&-
+
+diff localenvmods.expect localenvmods.out || fail "$tid"
+
+tid="localenvmod set (preset: '')"
+
+cat >localenvmods.expect <<EOE
+foo
+foo
+foo
+foo
+foo
+foo
+EOE
+
+verbose "test $tid"
+exec 4>localenvmods.out
+while IFS= read mod; do
+	trace "test $tid: <$mod>"
+	(
+		cat $OBJ/ssh_proxy.tmpl
+		printf 'LocalCommand printf "%%%%s\\n" "$FOO"\n'
+		printf "LocalEnvMod %s\n" "$mod"
+	) >$OBJ/ssh_proxy
+	FOO="" ${SSH} -n -F $OBJ/ssh_proxy somehost true >&4 || fail "$tid: <$mod>"
+done <localenvmods.in
+exec 4>&-
+
+diff localenvmods.expect localenvmods.out || fail "$tid"
+
+tid="localenvmod set (preset: 'bar')"
+
+cat >localenvmods.expect <<EOE
+bar,foo
+foo,bar
+bar:foo
+foo:bar
+bar foo
+foo bar
+EOE
+
+verbose "test $tid"
+exec 4>localenvmods.out
+while IFS= read mod; do
+	trace "test $tid: <$mod>"
+	(
+		cat $OBJ/ssh_proxy.tmpl
+		printf 'LocalCommand printf "%%%%s\\n" "$FOO"\n'
+		printf "LocalEnvMod %s\n" "$mod"
+	) >$OBJ/ssh_proxy
+	FOO=bar ${SSH} -n -F $OBJ/ssh_proxy somehost true >&4 || fail "$tid: <$mod>"
+done <localenvmods.in
+exec 4>&-
+
+diff localenvmods.expect localenvmods.out || fail "$tid"
+
+tid="localenvmod unset"
+
+cat >localenvmods.in <<EOI
+FOO=
+FOO=""
+EOI
+
+cat >localenvmods.expect <<EOE
+true
+true
+EOE
+
+verbose "test $tid"
+exec 4>localenvmods.out
+while IFS= read mod; do
+	trace "test $tid: <$mod>"
+	(
+		cat $OBJ/ssh_proxy.tmpl
+		printf 'LocalCommand test "${FOO:+set}" = set || echo true\n'
+		printf "LocalEnvMod %s\n" "$mod"
+	) >$OBJ/ssh_proxy
+	FOO=bar ${SSH} -n -F $OBJ/ssh_proxy somehost true >&4 || fail "$tid: <$mod>"
+done <localenvmods.in
+exec 4>&-
+
+diff localenvmods.expect localenvmods.out || fail "$tid"
+
+tid="localenvmod commandline overwrites config file (change)"
+
+cat >localenvmods.expect <<EOE
+foo
+EOE
+
+verbose "test $tid"
+(
+	cat $OBJ/ssh_proxy.tmpl
+	printf 'LocalCommand printf "%%%%s\\n" "$FOO"\n'
+	printf "LocalEnvMod FOO=bar\n" "$mod"
+) >$OBJ/ssh_proxy
+${SSH} -n -F $OBJ/ssh_proxy -o"LocalEnvMod=FOO=foo" somehost true >localenvmods.out || fail "$tid"
+
+diff localenvmods.expect localenvmods.out || fail "$tid"
+
+tid="localenvmod commandline overwrites config file (unset)"
+
+cat >localenvmods.expect <<EOE
+true
+EOE
+
+verbose "test $tid"
+(
+	cat $OBJ/ssh_proxy.tmpl
+	printf 'LocalCommand test "${FOO:+set}" = set || echo true\n'
+	printf "LocalEnvMod FOO=bar\n" "$mod"
+) >$OBJ/ssh_proxy
+${SSH} -n -F $OBJ/ssh_proxy -o"LocalEnvMod=FOO=" somehost true >localenvmods.out || fail "$tid"
+
+diff localenvmods.expect localenvmods.out || fail "$tid"
+
+# reset tid
+tid="localenvmod"
diff --git a/ssh.c b/ssh.c
index 9409fa7..32b9464 100644
--- a/ssh.c
+++ b/ssh.c
@@ -751,6 +751,63 @@ main(int ac, char **av)
 	if (options.control_path != NULL)
 		muxclient(options.control_path);
 
+	if (options.num_local_env_mods > 0) {
+		char thishost[NI_MAXHOST];
+
+		if (gethostname(thishost, sizeof(thishost)) == -1)
+			fatal("gethostname: %s", strerror(errno));
+		snprintf(buf, sizeof(buf), "%d", options.port);
+
+		for (i = 0; i < options.num_local_env_mods; i++) {
+			char sepbuf[2], *oldval;
+			int prepend = 0;
+			int op = options.local_env_mods[i].operation;
+			if (0 > op) {
+				prepend = 1;
+				op = -op;
+			}
+			sepbuf[0] = op;
+			sepbuf[1] = '\0';
+			debug3("expanding LocalEnvMod: %s %s%s= %s",
+			    options.local_env_mods[i].name,
+			    op ? (prepend ? "%" : "+") : "",
+			    sepbuf,
+			    options.local_env_mods[i].value);
+			cp = options.local_env_mods[i].value;
+			options.local_env_mods[i].value = percent_expand(cp,
+			    "d", pw->pw_dir,
+			    "h", host,
+			    "l", thishost,
+			    "n", host_arg,
+			    "r", options.user,
+			    "p", buf,
+			    "u", pw->pw_name,
+			    (char *)NULL);
+			debug3("expanded LocalEnvMod:  %s %s%s= %s",
+			    options.local_env_mods[i].name,
+			    op ? (prepend ? "%" : "+") : "",
+			    sepbuf,
+			    options.local_env_mods[i].value);
+			xfree(cp);
+			if (op &&
+			    (oldval = getenv(options.local_env_mods[i].name)) &&
+			    strlen(oldval) > 0) {
+				char *newval;
+				newval = xmalloc(strlen(oldval) + 1 + strlen(options.local_env_mods[i].value) + 1);
+				strcpy(newval, prepend ? options.local_env_mods[i].value : oldval);
+				strcat(newval, sepbuf);
+				strcat(newval, prepend ? oldval : options.local_env_mods[i].value);
+				setenv(options.local_env_mods[i].name, newval, 1);
+			} else if (options.local_env_mods[i].value[0]) {
+				/* set or mod without current value for variable */
+				setenv(options.local_env_mods[i].name,
+				    options.local_env_mods[i].value, 1);
+			} else {
+				unsetenv(options.local_env_mods[i].name);
+			}
+		}
+	}
+
 	timeout_ms = options.connection_timeout * 1000;
 
 	/* Open a connection to the remote host. */
-- 
tg: (bc28466..) bw/localenvmod (depends on: master)


More information about the openssh-unix-dev mailing list