MEDIUM: ssl: add mapping from SNI to cert file using "crt-list"

It designates a list of PEM file with an optional list of SNI filter
per certificate, with the following format for each line :

    <crtfile>[ <snifilter>]*

Wildcards are supported in the SNI filter. The certificates will be
presented to clients who provide a valid TLS Server Name Indication
field matching one of SNI filter. If no SNI filter is specified the
CN and alt subjects are used.
diff --git a/doc/configuration.txt b/doc/configuration.txt
index 57ef9c4..69ee1f0 100644
--- a/doc/configuration.txt
+++ b/doc/configuration.txt
@@ -7214,6 +7214,20 @@
   set to 'all', all errors are ignored. SSL handshake is not abored if an error
   is ignored.
 
+crt-list <file>
+  This setting is only available when support for OpenSSL was built in. It
+  designates a list of PEM file with an optional list of SNI filter per
+  certificate, with the following format for each line :
+
+        <crtfile>[ <snifilter>]*
+
+  Wildcards are supported in the SNI filter. The certificates will be presented
+  to clients who provide a valid TLS Server Name Indication field matching one
+  of SNI filter. If no SNI filter is specified the CN and alt subjects are
+  used. This directive may be specified multiple times. See the "crt" option
+  for more information. The default certificate is still needed to meet OpenSSL
+  expectations. If it is not used, the strict-sni option may be used.
+
 defer-accept
   Is an optional keyword which is supported only on certain Linux kernels. It
   states that a connection will only be accepted once some data arrive on it,
diff --git a/src/ssl_sock.c b/src/ssl_sock.c
index f26813b..90c7232 100644
--- a/src/ssl_sock.c
+++ b/src/ssl_sock.c
@@ -258,10 +258,36 @@
 }
 #endif
 
+int ssl_sock_add_cert_sni(SSL_CTX *ctx, struct bind_conf *s, char *name, int len, int order)
+{
+	struct sni_ctx *sc;
+	int wild = 0;
+	int j;
+
+	if (len) {
+		if (*name == '*') {
+			wild = 1;
+			name++;
+			len--;
+		}
+		sc = malloc(sizeof(struct sni_ctx) + len + 1);
+		for (j = 0; j < len; j++)
+			sc->name.key[j] = tolower(name[j]);
+		sc->name.key[len] = 0;
+		sc->order = order++;
+		sc->ctx = ctx;
+		if (wild)
+			ebst_insert(&s->sni_w_ctx, &sc->name);
+		else
+			ebst_insert(&s->sni_ctx, &sc->name);
+	}
+	return order;
+}
+
 /* Loads a certificate key and CA chain from a file. Returns 0 on error, -1 if
  * an early error happens and the caller must call SSL_CTX_free() by itelf.
  */
-int ssl_sock_load_cert_chain_file(SSL_CTX *ctx, const char *file, struct bind_conf *s)
+int ssl_sock_load_cert_chain_file(SSL_CTX *ctx, const char *file, struct bind_conf *s, char *sni_filter)
 {
 	BIO *in;
 	X509 *x = NULL, *ca;
@@ -270,7 +296,6 @@
 	int order = 0;
 	X509_NAME *xname;
 	char *str;
-	struct sni_ctx *sc;
 #ifdef SSL_CTRL_SET_TLSEXT_HOSTNAME
 	STACK_OF(GENERAL_NAME) *names;
 #endif
@@ -286,71 +311,43 @@
 	if (x == NULL)
 		goto end;
 
+	if (sni_filter) {
+		while (*sni_filter) {
+			while (isspace(*sni_filter))
+				sni_filter++;
+			str = sni_filter;
+			while (!isspace(*sni_filter) && *sni_filter)
+				sni_filter++;
+			len = sni_filter - str;
+			order = ssl_sock_add_cert_sni(ctx, s, str, len, order);
+		}
+	}
+	else {
 #ifdef SSL_CTRL_SET_TLSEXT_HOSTNAME
-	names = X509_get_ext_d2i(x, NID_subject_alt_name, NULL, NULL);
-	if (names) {
-		for (i = 0; i < sk_GENERAL_NAME_num(names); i++) {
-			GENERAL_NAME *name = sk_GENERAL_NAME_value(names, i);
-			if (name->type == GEN_DNS) {
-				if (ASN1_STRING_to_UTF8((unsigned char **)&str, name->d.dNSName) >= 0) {
-					if ((len = strlen(str))) {
-						int j;
-
-						if (*str != '*') {
-							sc = malloc(sizeof(struct sni_ctx) + len + 1);
-							for (j = 0; j < len; j++)
-								sc->name.key[j] = tolower(str[j]);
-							sc->name.key[len] = 0;
-							sc->order = order++;
-							sc->ctx = ctx;
-							ebst_insert(&s->sni_ctx, &sc->name);
-						}
-						else {
-							sc = malloc(sizeof(struct sni_ctx) + len);
-							for (j = 1; j < len; j++)
-								sc->name.key[j-1] = tolower(str[j]);
-							sc->name.key[len-1] = 0;
-							sc->order = order++;
-							sc->ctx = ctx;
-							ebst_insert(&s->sni_w_ctx, &sc->name);
-						}
+		names = X509_get_ext_d2i(x, NID_subject_alt_name, NULL, NULL);
+		if (names) {
+			for (i = 0; i < sk_GENERAL_NAME_num(names); i++) {
+				GENERAL_NAME *name = sk_GENERAL_NAME_value(names, i);
+				if (name->type == GEN_DNS) {
+					if (ASN1_STRING_to_UTF8((unsigned char **)&str, name->d.dNSName) >= 0) {
+						len = strlen(str);
+						order = ssl_sock_add_cert_sni(ctx, s, str, len, order);
+						OPENSSL_free(str);
 					}
-					OPENSSL_free(str);
 				}
 			}
+			sk_GENERAL_NAME_pop_free(names, GENERAL_NAME_free);
 		}
-		sk_GENERAL_NAME_pop_free(names, GENERAL_NAME_free);
-	}
 #endif /* SSL_CTRL_SET_TLSEXT_HOSTNAME */
-
-	xname = X509_get_subject_name(x);
-	i = -1;
-	while ((i = X509_NAME_get_index_by_NID(xname, NID_commonName, i)) != -1) {
-		X509_NAME_ENTRY *entry = X509_NAME_get_entry(xname, i);
-		if (ASN1_STRING_to_UTF8((unsigned char **)&str, entry->value) >= 0) {
-			if ((len = strlen(str))) {
-				int j;
-
-				if (*str != '*') {
-					sc = malloc(sizeof(struct sni_ctx) + len + 1);
-					for (j = 0; j < len; j++)
-						sc->name.key[j] = tolower(str[j]);
-					sc->name.key[len] = 0;
-					sc->order = order++;
-					sc->ctx = ctx;
-					ebst_insert(&s->sni_ctx, &sc->name);
-				}
-				else {
-					sc = malloc(sizeof(struct sni_ctx) + len);
-					for (j = 1; j < len; j++)
-						sc->name.key[j-1] = tolower(str[j]);
-					sc->name.key[len-1] = 0;
-					sc->order = order++;
-					sc->ctx = ctx;
-					ebst_insert(&s->sni_w_ctx, &sc->name);
-				}
+		xname = X509_get_subject_name(x);
+		i = -1;
+		while ((i = X509_NAME_get_index_by_NID(xname, NID_commonName, i)) != -1) {
+			X509_NAME_ENTRY *entry = X509_NAME_get_entry(xname, i);
+			if (ASN1_STRING_to_UTF8((unsigned char **)&str, entry->value) >= 0) {
+				len = strlen(str);
+				order = ssl_sock_add_cert_sni(ctx, s, str, len, order);
+				OPENSSL_free(str);
 			}
-			OPENSSL_free(str);
 		}
 	}
 
@@ -387,7 +384,7 @@
 	return ret;
 }
 
-static int ssl_sock_load_cert_file(const char *path, struct bind_conf *bind_conf, struct proxy *curproxy, char **err)
+static int ssl_sock_load_cert_file(const char *path, struct bind_conf *bind_conf, struct proxy *curproxy, char *sni_filter, char **err)
 {
 	int ret;
 	SSL_CTX *ctx;
@@ -406,7 +403,7 @@
 		return 1;
 	}
 
-	ret = ssl_sock_load_cert_chain_file(ctx, path, bind_conf);
+	ret = ssl_sock_load_cert_chain_file(ctx, path, bind_conf, sni_filter);
 	if (ret <= 0) {
 		memprintf(err, "%sunable to load SSL certificate from PEM file '%s'.\n",
 		          err && *err ? *err : "", path);
@@ -457,7 +454,7 @@
 	int cfgerr = 0;
 
 	if (!(dir = opendir(path)))
-		return ssl_sock_load_cert_file(path, bind_conf, curproxy, err);
+		return ssl_sock_load_cert_file(path, bind_conf, curproxy, NULL, err);
 
 	/* strip trailing slashes, including first one */
 	for (end = path + strlen(path) - 1; end >= path && *end == '/'; end--)
@@ -473,7 +470,7 @@
 		}
 		if (!S_ISREG(buf.st_mode))
 			continue;
-		cfgerr += ssl_sock_load_cert_file(fp, bind_conf, curproxy, err);
+		cfgerr += ssl_sock_load_cert_file(fp, bind_conf, curproxy, NULL, err);
 	}
 	closedir(dir);
 	return cfgerr;
@@ -495,6 +492,120 @@
 	return random_initialized;
 }
 
+int ssl_sock_load_cert_list_file(char *file, struct bind_conf *bind_conf, struct proxy *curproxy, char **err)
+{
+	char thisline[65536];
+	FILE *f;
+	int linenum = 0;
+	int cfgerr = 0;
+	char *sni_filter = NULL;
+	char *crt_file = NULL;
+
+	if ((f = fopen(file, "r")) == NULL)
+		return 1;
+
+	while (fgets(thisline, sizeof(thisline), f) != NULL) {
+		int arg;
+		char *end;
+		char *args[MAX_LINE_ARGS + 1];
+		char *line = thisline;
+
+		linenum++;
+		end = line + strlen(line);
+		if (end-line == sizeof(thisline)-1 && *(end-1) != '\n') {
+			/* Check if we reached the limit and the last char is not \n.
+			 * Watch out for the last line without the terminating '\n'!
+			 */
+			Alert("parsing [%s:%d]: line too long, limit: %d.\n",
+			      file, linenum, (int)sizeof(thisline)-1);
+			cfgerr = 1;
+		}
+
+		/* skip leading spaces */
+		while (isspace(*line))
+			line++;
+
+		arg = 0;
+		args[arg] = line;
+
+		while (*line && arg < MAX_LINE_ARGS) {
+			if (*line == '#' || *line == '\n' || *line == '\r') {
+				/* end of string, end of loop */
+				*line = 0;
+				break;
+			}
+			else if (isspace(*line)) {
+				/* a non-escaped space is an argument separator */
+				*line++ = '\0';
+				while (isspace(*line))
+					line++;
+				args[++arg] = line;
+				break;
+			}
+			else {
+				line++;
+			}
+		}
+		while (*line) {
+			if (*line == '#' || *line == '\n' || *line == '\r') {
+				/* end of string, end of loop */
+				*line = 0;
+				break;
+			}
+			else {
+				line++;
+			}
+		}
+		/* empty line */
+		if (!**args)
+			continue;
+		if (*line) {
+			/* we had to stop due to too many args.
+			 * Let's terminate the string, print the offending part then cut the
+			 * last arg.
+			 */
+			while (*line && *line != '#' && *line != '\n' && *line != '\r')
+				line++;
+			*line = '\0';
+
+			Alert("parsing [%s:%d]: line too long, truncating at word %d, position %ld: <%s>.\n",
+			      file, linenum, arg + 1, (long)(args[arg] - thisline + 1), args[arg]);
+			cfgerr = 1;
+			args[arg] = line;
+		}
+		/* zero out remaining args and ensure that at least one entry
+		 * is zeroed out.
+		 */
+		while (++arg <= MAX_LINE_ARGS) {
+			args[arg] = line;
+		}
+
+		crt_file = strdup(args[0]);
+		if (*args[1])
+			sni_filter = strdup(args[1]);
+		cfgerr = ssl_sock_load_cert_file(crt_file, bind_conf, curproxy, sni_filter, err);
+
+		if (sni_filter) {
+			free(sni_filter);
+			sni_filter = NULL;
+		}
+		if (crt_file) {
+			free(crt_file);
+			crt_file = NULL;
+		}
+
+		if (cfgerr)
+			break;
+	}
+	if (sni_filter)
+		free(sni_filter);
+	if (crt_file)
+		free(crt_file);
+
+	fclose(f);
+	return cfgerr;
+}
+
 #ifndef SSL_OP_CIPHER_SERVER_PREFERENCE                 /* needs OpenSSL >= 0.9.7 */
 #define SSL_OP_CIPHER_SERVER_PREFERENCE 0
 #endif
@@ -2388,6 +2499,20 @@
 	return 0;
 }
 
+/* parse the "crt-list" bind keyword */
+static int bind_parse_crt_list(char **args, int cur_arg, struct proxy *px, struct bind_conf *conf, char **err)
+{
+	if (!*args[cur_arg + 1]) {
+		memprintf(err, "'%s' : missing certificate location", args[cur_arg]);
+		return ERR_ALERT | ERR_FATAL;
+	}
+
+	if (ssl_sock_load_cert_list_file(args[cur_arg + 1], conf, px, err) > 0)
+		return ERR_ALERT | ERR_FATAL;
+
+	return 0;
+}
+
 /* parse the "crl-file" bind keyword */
 static int bind_parse_crl_file(char **args, int cur_arg, struct proxy *px, struct bind_conf *conf, char **err)
 {
@@ -2929,6 +3054,7 @@
 	{ "crl-file",              bind_parse_crl_file,       1 }, /* set certificat revocation list file use on client cert verify */
 	{ "crt",                   bind_parse_crt,            1 }, /* load SSL certificates from this location */
 	{ "crt-ignore-err",        bind_parse_ignore_err,     1 }, /* set error IDs to ingore on verify depth == 0 */
+	{ "crt-list",              bind_parse_crt_list,       1 }, /* load a list of crt from this location */
 	{ "ecdhe",                 bind_parse_ecdhe,          1 }, /* defines named curve for elliptic curve Diffie-Hellman */
 	{ "force-sslv3",           bind_parse_force_sslv3,    0 }, /* force SSLv3 */
 	{ "force-tlsv10",          bind_parse_force_tlsv10,   0 }, /* force TLSv10 */