MEDIUM: config: centralize handling of SSL config per bind line

SSL config holds many parameters which are per bind line and not per
listener. Let's use a per-bind line config instead of having it
replicated for each listener.

At the moment we only do this for the SSL part but this should probably
evolved to handle more of the configuration and maybe even the state per
bind line.
diff --git a/include/proto/protocols.h b/include/proto/protocols.h
index 91334da..ec98e59 100644
--- a/include/proto/protocols.h
+++ b/include/proto/protocols.h
@@ -134,6 +134,23 @@
 /* returns the protocol associated to family <family> or NULL if not found */
 struct protocol *protocol_by_family(int family);
 
+/* allocate an ssl_conf struct for a bind line, and chain it to list head <lh>.
+ * If <arg> is not NULL, it is duplicated into ->arg to store useful config
+ * information for error reporting.
+ */
+static inline struct ssl_conf *ssl_conf_alloc(struct list *lh, const char *file, int line, const char *arg)
+{
+	struct ssl_conf *ssl_conf = (void *)calloc(1, sizeof(struct ssl_conf));
+
+	ssl_conf->file = strdup(file);
+	ssl_conf->line = line;
+	if (lh)
+		LIST_ADDQ(lh, &ssl_conf->by_fe);
+	if (arg)
+		ssl_conf->arg = strdup(arg);
+	return ssl_conf;
+}
+
 #endif /* _PROTO_PROTOCOLS_H */
 
 /*
diff --git a/include/types/protocols.h b/include/types/protocols.h
index 1d962ea..1ff448e 100644
--- a/include/types/protocols.h
+++ b/include/types/protocols.h
@@ -94,6 +94,23 @@
  * maxconn setting to the global.maxsock value so that its resources are reserved.
  */
 
+/* "bind" line SSL settings */
+struct ssl_conf {
+#ifdef USE_OPENSSL
+	char *ciphers;             /* cipher suite to use if non-null */
+	char *cert;                /* ssl main certificate */
+	int nosslv3;               /* disable SSLv3 */
+	int notlsv1;               /* disable TLSv1 */
+	int prefer_server_ciphers; /* Prefer server ciphers */
+	SSL_CTX *ctx;              /* SSL configuration */
+#endif
+	int ref_cnt;               /* number of users of this config, maybe 0 on error */
+	struct list by_fe;         /* next binding for the same frontend, or NULL */
+	char *arg;                 /* argument passed to "bind" for better error reporting */
+	char *file;                /* file where the section appears */
+	int line;                  /* line where the section appears */
+};
+
 /* The listener will be directly referenced by the fdtab[] which holds its
  * socket. The listener provides the protocol-specific accept() function to
  * the fdtab.
@@ -130,16 +147,8 @@
 	char *interface;		/* interface name or NULL */
 	int maxseg;			/* for TCP, advertised MSS */
 
-	char *ssl_cert;			/* ssl certificate */
-#ifdef USE_OPENSSL
-	struct {
-		SSL_CTX *ctx;
-		char *ciphers;		/* cipher suite to use if non-null */
-		int nosslv3;		/* disable SSLv3 */
-		int notlsv1;		/* disable TLSv1 */
-		int prefer_server_ciphers; /* Prefer server ciphers */
-	} ssl_ctx;
-#endif
+	struct ssl_conf *ssl_conf;	/* SSL settings, otherwise NULL */
+
 	/* warning: this struct is huge, keep it at the bottom */
 	struct sockaddr_storage addr;	/* the address we listen to */
 	struct {
diff --git a/include/types/proxy.h b/include/types/proxy.h
index 13a08a5..1080568 100644
--- a/include/types/proxy.h
+++ b/include/types/proxy.h
@@ -360,6 +360,7 @@
 		struct eb32_node id;		/* place in the tree of used IDs */
 		struct eb_root used_listener_id;/* list of listener IDs in use */
 		struct eb_root used_server_id;	/* list of server IDs in use */
+		struct list ssl_bind;		/* list of SSL bind settings */
 	} conf;					/* config information */
 	void *parent;				/* parent of the proxy when applicable */
 };
diff --git a/src/cfgparse.c b/src/cfgparse.c
index 00531e5..27bed6c 100644
--- a/src/cfgparse.c
+++ b/src/cfgparse.c
@@ -1378,6 +1378,7 @@
 	struct acl_cond *cond = NULL;
 	struct logsrv *tmplogsrv;
 	char *errmsg = NULL;
+	struct ssl_conf *ssl_conf;
 
 	if (!strcmp(args[0], "listen"))
 		rc = PR_CAP_LISTEN;
@@ -1686,6 +1687,7 @@
 		}
 
 		last_listen = curproxy->listen;
+		ssl_conf = NULL;
 
 		/* NOTE: the following line might create several listeners if there
 		 * are comma-separated IPs or port ranges. So all further processing
@@ -1901,19 +1903,20 @@
 				continue;
 			}
 
-			if (!strcmp(args[cur_arg], "ssl")) { /* use ssl certificate */
+			if (!strcmp(args[cur_arg], "ssl")) { /* use ssl */
 #ifdef USE_OPENSSL
 				struct listener *l;
 
-				if (!*args[cur_arg + 1]) {
-					Alert("parsing [%s:%d] : '%s' : missing certificate.\n",
-					      file, linenum, args[0]);
-					err_code |= ERR_ALERT | ERR_FATAL;
-					goto out;
-				}
+				if (!ssl_conf)
+					ssl_conf = ssl_conf_alloc(&curproxy->conf.ssl_bind, file, linenum, args[1]);
+				ssl_conf->cert = strdup(args[cur_arg + 1]);
 
-				for (l = curproxy->listen; l != last_listen; l = l->next)
-					l->ssl_cert = strdup(args[cur_arg + 1]);
+				for (l = curproxy->listen; l != last_listen; l = l->next) {
+					if (!l->ssl_conf) {
+						l->ssl_conf = ssl_conf;
+						ssl_conf->ref_cnt++;
+					}
+				}
 
 				cur_arg += 2;
 				continue;
@@ -1927,8 +1930,6 @@
 
 			if (!strcmp(args[cur_arg], "ciphers")) { /* set cipher suite */
 #ifdef USE_OPENSSL
-				struct listener *l;
-
 				if (!*args[cur_arg + 1]) {
 					Alert("parsing [%s:%d] : '%s' : missing cipher suite.\n",
 					      file, linenum, args[0]);
@@ -1936,8 +1937,9 @@
 					goto out;
 				}
 
-				for (l = curproxy->listen; l != last_listen; l = l->next)
-					l->ssl_ctx.ciphers = strdup(args[cur_arg + 1]);
+				if (!ssl_conf)
+					ssl_conf = ssl_conf_alloc(&curproxy->conf.ssl_bind, file, linenum, args[1]);
+				ssl_conf->ciphers = strdup(args[cur_arg + 1]);
 
 				cur_arg += 2;
 				continue;
@@ -1951,10 +1953,9 @@
 
 			if (!strcmp(args[cur_arg], "nosslv3")) { /* disable SSLv3 */
 #ifdef USE_OPENSSL
-				struct listener *l;
-
-				for (l = curproxy->listen; l != last_listen; l = l->next)
-					l->ssl_ctx.nosslv3 = 1;
+				if (!ssl_conf)
+					ssl_conf = ssl_conf_alloc(&curproxy->conf.ssl_bind, file, linenum, args[1]);
+				ssl_conf->nosslv3 = 1;
 
 				cur_arg += 1;
 				continue;
@@ -1968,10 +1969,9 @@
 
 			if (!strcmp(args[cur_arg], "notlsv1")) { /* disable TLSv1 */
 #ifdef USE_OPENSSL
-				struct listener *l;
-
-				for (l = curproxy->listen; l != last_listen; l = l->next)
-					l->ssl_ctx.notlsv1 = 1;
+				if (!ssl_conf)
+					ssl_conf = ssl_conf_alloc(&curproxy->conf.ssl_bind, file, linenum, args[1]);
+				ssl_conf->notlsv1 = 1;
 
 				cur_arg += 1;
 				continue;
@@ -1985,10 +1985,9 @@
 
 			if (!strcmp(args[cur_arg], "prefer-server-ciphers")) { /* Prefert server ciphers */
 #if defined (USE_OPENSSL) && defined(SSL_OP_CIPHER_SERVER_PREFERENCE)
-				struct listener *l;
-
-				for (l = curproxy->listen; l != last_listen; l = l->next)
-					l->ssl_ctx.prefer_server_ciphers = 1;
+				if (!ssl_conf)
+					ssl_conf = ssl_conf_alloc(&curproxy->conf.ssl_bind, file, linenum, args[1]);
+				ssl_conf->prefer_server_ciphers = 1;
 
 				cur_arg += 1;
 				continue;
@@ -5985,7 +5984,9 @@
 	struct userlist *curuserlist = NULL;
 	int err_code = 0;
 	unsigned int next_pxid = 1;
+	struct ssl_conf *ssl_conf, *ssl_back;
 
+	ssl_back = ssl_conf = NULL;
 	/*
 	 * Now, check for the integrity of all that we have collected.
 	 */
@@ -6881,6 +6882,98 @@
 			curproxy->listen = next;
 		}
 
+#ifdef USE_OPENSSL
+#ifndef SSL_OP_CIPHER_SERVER_PREFERENCE                 /* needs OpenSSL >= 0.9.7 */
+#define SSL_OP_CIPHER_SERVER_PREFERENCE 0
+#endif
+
+#ifndef SSL_OP_NO_SESSION_RESUMPTION_ON_RENEGOTIATION	/* needs OpenSSL >= 0.9.7 */
+#define SSL_OP_NO_SESSION_RESUMPTION_ON_RENEGOTIATION 0
+#endif
+#ifndef SSL_OP_NO_COMPRESSION                           /* needs OpenSSL >= 0.9.9 */
+#define SSL_OP_NO_COMPRESSION 0
+#endif
+#ifndef SSL_MODE_RELEASE_BUFFERS                        /* needs OpenSSL >= 1.0.0 */
+#define SSL_MODE_RELEASE_BUFFERS 0
+#endif
+		/* Configure SSL for each bind line.
+		 * Note: if configuration fails at some point, the ->ctx member
+		 * remains NULL so that listeners can later detach.
+		 */
+		list_for_each_entry(ssl_conf, &curproxy->conf.ssl_bind, by_fe) {
+			int ssloptions =
+				SSL_OP_ALL | /* all known workarounds for bugs */
+				SSL_OP_NO_SSLv2 |
+				SSL_OP_NO_COMPRESSION |
+				SSL_OP_NO_SESSION_RESUMPTION_ON_RENEGOTIATION;
+			int sslmode =
+				SSL_MODE_ENABLE_PARTIAL_WRITE |
+				SSL_MODE_ACCEPT_MOVING_WRITE_BUFFER |
+				SSL_MODE_RELEASE_BUFFERS;
+			SSL_CTX *ctx;
+
+			if (!ssl_conf->cert) {
+				Alert("Proxy '%s': no SSL certificate specified for bind '%s' at [%s:%d] (use 'ssl').\n",
+				      curproxy->id, ssl_conf->arg, ssl_conf->file, ssl_conf->line);
+				cfgerr++;
+				continue;
+			}
+
+			ctx = SSL_CTX_new(SSLv23_server_method());
+			if (!ctx) {
+				Alert("Proxy '%s': unable to allocate SSL context for bind '%s' at [%s:%d] using cert '%s'.\n",
+				      curproxy->id, ssl_conf->arg, ssl_conf->file, ssl_conf->line, ssl_conf->cert);
+				cfgerr++;
+				continue;
+			}
+			if (ssl_conf->nosslv3)
+				ssloptions |= SSL_OP_NO_SSLv3;
+			if (ssl_conf->notlsv1)
+				ssloptions |= SSL_OP_NO_TLSv1;
+			if (ssl_conf->prefer_server_ciphers)
+				ssloptions |= SSL_OP_CIPHER_SERVER_PREFERENCE;
+			SSL_CTX_set_options(ctx, ssloptions);
+			SSL_CTX_set_mode(ctx, sslmode);
+			SSL_CTX_set_verify(ctx, SSL_VERIFY_NONE, NULL);
+			if (shared_context_init(global.tune.sslcachesize) < 0) {
+				Alert("Unable to allocate SSL session cache.\n");
+				cfgerr++;
+				SSL_CTX_free(ctx);
+				continue;
+			}
+			shared_context_set_cache(ctx);
+			if (ssl_conf->ciphers &&
+			    !SSL_CTX_set_cipher_list(ctx, ssl_conf->ciphers)) {
+				Alert("Proxy '%s': unable to set SSL cipher list to '%s' for bind '%s' at [%s:%d] using cert '%s'.\n",
+				      curproxy->id, ssl_conf->ciphers, ssl_conf->arg,
+				      ssl_conf->file, ssl_conf->line, ssl_conf->cert);
+				cfgerr++;
+				SSL_CTX_free(ctx);
+				continue;
+			}
+
+			SSL_CTX_set_info_callback(ctx, ssl_sock_infocbk);
+
+			if (SSL_CTX_use_PrivateKey_file(ctx, ssl_conf->cert, SSL_FILETYPE_PEM) <= 0) {
+				Alert("Proxy '%s': unable to load SSL private key from file '%s' in bind '%s' at [%s:%d].\n",
+				      curproxy->id, ssl_conf->cert, ssl_conf->arg, ssl_conf->file, ssl_conf->line);
+				cfgerr++;
+				SSL_CTX_free(ctx);
+				continue;
+			}
+
+			if (SSL_CTX_use_certificate_chain_file(ctx, ssl_conf->cert) <= 0) {
+				Alert("Proxy '%s': unable to load SSL certificate from file '%s' in bind '%s' at [%s:%d].\n",
+				      curproxy->id, ssl_conf->cert, ssl_conf->arg, ssl_conf->file, ssl_conf->line);
+				cfgerr++;
+				SSL_CTX_free(ctx);
+				continue;
+			}
+
+			ssl_conf->ctx = ctx;
+		}
+#endif /* USE_OPENSSL */
+
 		/* adjust this proxy's listeners */
 		next_id = 1;
 		listener = curproxy->listen;
@@ -6903,83 +6996,17 @@
 					listener->name = strdup(trash);
 				}
 			}
-
 #ifdef USE_OPENSSL
-#ifndef SSL_OP_CIPHER_SERVER_PREFERENCE                 /* needs OpenSSL >= 0.9.7 */
-#define SSL_OP_CIPHER_SERVER_PREFERENCE 0
-#endif
-
-#ifndef SSL_OP_NO_SESSION_RESUMPTION_ON_RENEGOTIATION	/* needs OpenSSL >= 0.9.7 */
-#define SSL_OP_NO_SESSION_RESUMPTION_ON_RENEGOTIATION 0
-#endif
-#ifndef SSL_OP_NO_COMPRESSION                           /* needs OpenSSL >= 0.9.9 */
-#define SSL_OP_NO_COMPRESSION 0
-#endif
-#ifndef SSL_MODE_RELEASE_BUFFERS                        /* needs OpenSSL >= 1.0.0 */
-#define SSL_MODE_RELEASE_BUFFERS 0
-#endif
-			/* Initialize SSL */
-			if (listener->ssl_cert) {
-				int ssloptions =
-					SSL_OP_ALL | /* all known workarounds for bugs */
-					SSL_OP_NO_SSLv2 |
-					SSL_OP_NO_COMPRESSION |
-					SSL_OP_NO_SESSION_RESUMPTION_ON_RENEGOTIATION;
-				int sslmode =
-					SSL_MODE_ENABLE_PARTIAL_WRITE |
-					SSL_MODE_ACCEPT_MOVING_WRITE_BUFFER |
-					SSL_MODE_RELEASE_BUFFERS;
-
-				listener->data = &ssl_sock; /* data layer */
-				listener->ssl_ctx.ctx = SSL_CTX_new(SSLv23_server_method());
-				if (!listener->ssl_ctx.ctx) {
-					Alert("Proxy '%s': unable to allocate SSL context to bind listener %d (%s:%d) using cert '%s'.\n",
-					      curproxy->id, listener->luid, listener->conf.file, listener->conf.line, listener->ssl_cert);
-					cfgerr++;
-					goto skip_ssl;
-				}
-				if (listener->ssl_ctx.nosslv3)
-					ssloptions |= SSL_OP_NO_SSLv3;
-				if (listener->ssl_ctx.notlsv1)
-					ssloptions |= SSL_OP_NO_TLSv1;
-				if (listener->ssl_ctx.prefer_server_ciphers)
-					ssloptions |= SSL_OP_CIPHER_SERVER_PREFERENCE;
-				SSL_CTX_set_options(listener->ssl_ctx.ctx, ssloptions);
-				SSL_CTX_set_mode(listener->ssl_ctx.ctx, sslmode);
-				SSL_CTX_set_verify(listener->ssl_ctx.ctx, SSL_VERIFY_NONE, NULL);
-				if (shared_context_init(global.tune.sslcachesize) < 0) {
-					Alert("Unable to allocate SSL session cache.\n");
-					cfgerr++;
-					goto skip_ssl;
-				}
-				shared_context_set_cache(listener->ssl_ctx.ctx);
-				if (listener->ssl_ctx.ciphers &&
-				    !SSL_CTX_set_cipher_list(listener->ssl_ctx.ctx, listener->ssl_ctx.ciphers)) {
-					Alert("Proxy '%s': unable to set SSL cipher list to '%s' for listener %d (%s:%d) using cert '%s'.\n",
-					      curproxy->id, listener->ssl_ctx.ciphers, listener->luid,
-					      listener->conf.file, listener->conf.line, listener->ssl_cert);
-					cfgerr++;
-					goto skip_ssl;
-				}
-
-				SSL_CTX_set_info_callback(listener->ssl_ctx.ctx, ssl_sock_infocbk);
-
-				if (SSL_CTX_use_PrivateKey_file(listener->ssl_ctx.ctx, listener->ssl_cert, SSL_FILETYPE_PEM) <= 0) {
-					Alert("Proxy '%s': unable to load SSL private key from file '%s' in listener %d (%s:%d).\n",
-					      curproxy->id, listener->ssl_cert, listener->luid, listener->conf.file, listener->conf.line);
-					cfgerr++;
-					goto skip_ssl;
+			if (listener->ssl_conf) {
+				if (listener->ssl_conf->ctx) {
+					listener->data = &ssl_sock; /* SSL data layer */
 				}
-
-				if (SSL_CTX_use_certificate_chain_file(listener->ssl_ctx.ctx, listener->ssl_cert) <= 0) {
-					Alert("Proxy '%s': unable to load SSL certificate from file '%s' in listener %d (%s:%d).\n",
-					      curproxy->id, listener->ssl_cert, listener->luid, listener->conf.file, listener->conf.line);
-					cfgerr++;
-					goto skip_ssl;
+				else {
+					listener->ssl_conf->ref_cnt--;
+					listener->ssl_conf = NULL;
 				}
 			}
-		skip_ssl:
-#endif /* USE_OPENSSL */
+#endif
 			if (curproxy->options & PR_O_TCP_NOLING)
 				listener->options |= LI_O_NOLINGER;
 			if (!listener->maxconn)
@@ -7000,7 +7027,7 @@
 
 			/* smart accept mode is automatic in HTTP mode */
 			if ((curproxy->options2 & PR_O2_SMARTACC) ||
-			    ((curproxy->mode == PR_MODE_HTTP || listener->ssl_cert) &&
+			    ((curproxy->mode == PR_MODE_HTTP || listener->ssl_conf) &&
 			     !(curproxy->no_options2 & PR_O2_SMARTACC)))
 				listener->options |= LI_O_NOQUICKACK;
 
@@ -7008,6 +7035,24 @@
 			listener = listener->next;
 		}
 
+#ifdef USE_OPENSSL
+		/* Release unused SSL configs.
+		 */
+		list_for_each_entry_safe(ssl_conf, ssl_back, &curproxy->conf.ssl_bind, by_fe) {
+			if (ssl_conf->ref_cnt)
+				continue;
+
+			if (ssl_conf->ctx)
+				SSL_CTX_free(ssl_conf->ctx);
+			free(ssl_conf->ciphers);
+			free(ssl_conf->cert);
+			free(ssl_conf->file);
+			free(ssl_conf->arg);
+			LIST_DEL(&ssl_conf->by_fe);
+			free(ssl_conf);
+		}
+#endif /* USE_OPENSSL */
+
 		/* Check multi-process mode compatibility for the current proxy */
 		if (global.nbproc > 1) {
 			int nbproc = 0;
diff --git a/src/haproxy.c b/src/haproxy.c
index f5840d2..e166460 100644
--- a/src/haproxy.c
+++ b/src/haproxy.c
@@ -819,6 +819,7 @@
 	struct uri_auth *uap, *ua = NULL;
 	struct logsrv *log, *logb;
 	struct logformat_node *lf, *lfb;
+	struct ssl_conf *ssl_conf, *ssl_back;
 	int i;
 
 	deinit_signals();
@@ -999,13 +1000,32 @@
 			l_next = l->next;
 			unbind_listener(l);
 			delete_listener(l);
-			free(l->ssl_cert);
+			if (l->ssl_conf) {
+				l->ssl_conf->ref_cnt--;
+				l->ssl_conf = NULL;
+			}
 			free(l->name);
 			free(l->counters);
 			free(l);
 			l = l_next;
 		}/* end while(l) */
 
+		ssl_back = ssl_conf = NULL;
+#ifdef USE_OPENSSL
+		/* Release unused SSL configs.
+		 */
+		list_for_each_entry_safe(ssl_conf, ssl_back, &p->conf.ssl_bind, by_fe) {
+			if (ssl_conf->ctx)
+				SSL_CTX_free(ssl_conf->ctx);
+			free(ssl_conf->ciphers);
+			free(ssl_conf->cert);
+			free(ssl_conf->file);
+			free(ssl_conf->arg);
+			LIST_DEL(&ssl_conf->by_fe);
+			free(ssl_conf);
+		}
+#endif /* USE_OPENSSL */
+
 		free(p->desc);
 		free(p->fwdfor_hdr_name);
 
diff --git a/src/proxy.c b/src/proxy.c
index 6e1f6e5..07e863c 100644
--- a/src/proxy.c
+++ b/src/proxy.c
@@ -443,6 +443,7 @@
 	LIST_INIT(&p->logsrvs);
 	LIST_INIT(&p->logformat);
 	LIST_INIT(&p->format_unique_id);
+	LIST_INIT(&p->conf.ssl_bind);
 
 	/* Timeouts are defined as -1 */
 	proxy_reset_timeouts(p);
diff --git a/src/ssl_sock.c b/src/ssl_sock.c
index 69c4099..4047c71 100644
--- a/src/ssl_sock.c
+++ b/src/ssl_sock.c
@@ -96,7 +96,7 @@
 	}
 	else if (target_client(&conn->target)) {
 		/* Alloc a new SSL session ctx */
-		conn->data_ctx = SSL_new(target_client(&conn->target)->ssl_ctx.ctx);
+		conn->data_ctx = SSL_new(target_client(&conn->target)->ssl_conf->ctx);
 		if (!conn->data_ctx)
 			return -1;