[MINOR] acl: add req_ssl_ver in TCP, to match an SSL version

This new keyword matches an dotted version mapped into an integer.
It permits to match an SSL message protocol version just as if it
was an integer, so that it is easy to map ranges, like this :

	acl obsolete_ssl  req_ssl_ver   lt 3
	acl correct_ssl   req_ssl_ver   3.0-3.1
	acl invalid_ssl   req_ssl_ver   gt 3.1

Both SSLv2 hello messages and SSLv3 messages are supported. The
test tries to be strict enough to avoid being easily fooled. In
particular, it waits for as many bytes as announced in the message
header if this header looks valid (bound to the buffer size).

The same decoder will be usable with minor changes to check the
response messages.
diff --git a/src/proto_tcp.c b/src/proto_tcp.c
index e2afb94..4066116 100644
--- a/src/proto_tcp.c
+++ b/src/proto_tcp.c
@@ -446,6 +446,116 @@
 	return 1;
 }
 
+/* Return the version of the SSL protocol in the request. It supports both
+ * SSLv3 (TLSv1) header format for any message, and SSLv2 header format for
+ * the hello message. The SSLv3 format is described in RFC 2246 p49, and the
+ * SSLv2 format is described here, and completed p67 of RFC 2246 :
+ *    http://wp.netscape.com/eng/security/SSL_2.html
+ *
+ * Note: this decoder only works with non-wrapping data.
+ */
+static int
+acl_fetch_req_ssl_ver(struct proxy *px, struct session *l4, void *l7, int dir,
+			struct acl_expr *expr, struct acl_test *test)
+{
+	int version, bleft, msg_len;
+	const unsigned char *data;
+
+	if (!l4 || !l4->req)
+		return 0;
+
+	msg_len = 0;
+	bleft = l4->req->l;
+	if (!bleft)
+		goto too_short;
+
+	data = l4->req->w;
+	if ((*data >= 0x14 && *data <= 0x17) || (*data == 0xFF)) {
+		/* SSLv3 header format */
+		if (bleft < 5)
+			goto too_short;
+
+		version = (data[1] << 16) + data[2]; /* version: major, minor */
+		msg_len = (data[3] <<  8) + data[4]; /* record length */
+
+		/* format introduced with SSLv3 */
+		if (version < 0x00030000)
+			goto not_ssl;
+
+		/* message length between 1 and 2^14 + 2048 */
+		if (msg_len < 1 || msg_len > ((1<<14) + 2048))
+			goto not_ssl;
+
+		bleft -= 5; data += 5;
+	} else {
+		/* SSLv2 header format, only supported for hello (msg type 1) */
+		int rlen, plen, cilen, silen, chlen;
+
+		if (*data & 0x80) {
+			if (bleft < 3)
+				goto too_short;
+			/* short header format : 15 bits for length */
+			rlen = ((data[0] & 0x7F) << 8) | data[1];
+			plen = 0;
+			bleft -= 2; data += 2;
+		} else {
+			if (bleft < 4)
+				goto too_short;
+			/* long header format : 14 bits for length + pad length */
+			rlen = ((data[0] & 0x3F) << 8) | data[1];
+			plen = data[2];
+			bleft -= 3; data += 2;
+		}
+
+		if (*data != 0x01)
+			goto not_ssl;
+		bleft--; data++;
+
+		if (bleft < 8)
+			goto too_short;
+		version = (data[0] << 16) + data[1]; /* version: major, minor */
+		cilen   = (data[2] <<  8) + data[3]; /* cipher len, multiple of 3 */
+		silen   = (data[4] <<  8) + data[5]; /* session_id_len: 0 or 16 */
+		chlen   = (data[6] <<  8) + data[7]; /* 16<=challenge length<=32 */
+
+		bleft -= 8; data += 8;
+		if (cilen % 3 != 0)
+			goto not_ssl;
+		if (silen && silen != 16)
+			goto not_ssl;
+		if (chlen < 16 || chlen > 32)
+			goto not_ssl;
+		if (rlen != 9 + cilen + silen + chlen)
+			goto not_ssl;
+
+		/* focus on the remaining data length */
+		msg_len = cilen + silen + chlen + plen;
+	}
+	/* We could recursively check that the buffer ends exactly on an SSL
+	 * fragment boundary and that a possible next segment is still SSL,
+	 * but that's a bit pointless. However, we could still check that
+	 * all the part of the request which fits in a buffer is already
+	 * there.
+	 */
+	if (msg_len > l4->req->rlim - l4->req->w)
+		msg_len = l4->req->rlim - l4->req->w;
+
+	if (bleft < msg_len)
+		goto too_short;
+
+	/* OK that's enough. We have at least the whole message, and we have
+	 * the protocol version.
+	 */
+	test->i = version;
+	test->flags = ACL_TEST_F_VOLATILE;
+	return 1;
+
+ too_short:
+	test->flags = ACL_TEST_F_MAY_CHANGE;
+ not_ssl:
+	return 0;
+}
+
 
 static struct cfg_kw_list cfg_kws = {{ },{
 	{ CFG_LISTEN, "tcp-request", tcp_parse_tcp_req },
@@ -453,7 +563,8 @@
 }};
 
 static struct acl_kw_list acl_kws = {{ },{
-	{ "req_len",    acl_parse_int, acl_fetch_req_len, acl_match_int },
+	{ "req_len",      acl_parse_int,        acl_fetch_req_len,     acl_match_int },
+	{ "req_ssl_ver",  acl_parse_dotted_ver, acl_fetch_req_ssl_ver, acl_match_int },
 	{ NULL, NULL, NULL, NULL },
 }};
 
diff --git a/tests/test-inspect-ssl.cfg b/tests/test-inspect-ssl.cfg
new file mode 100644
index 0000000..582d1a2
--- /dev/null
+++ b/tests/test-inspect-ssl.cfg
@@ -0,0 +1,37 @@
+# This is a test configuration. It listens on port 8443, waits for an incoming
+# connection, and applies the following rules :
+#   - if the address is in the white list, then accept it and forward the
+#     connection to the server (local port 443)
+#   - if the address is in the black list, then immediately drop it
+#   - otherwise, wait up to 3 seconds for valid SSL data to come in. If those
+#     data are identified as SSL, the connection is immediately accepted, and
+#     if they are definitely identified as non-SSL, the connection is rejected,
+#     which will happen upon timeout if they still don't match SSL.
+
+listen block-non-ssl
+	log 127.0.0.1:514 local0
+	option tcplog
+
+	mode tcp
+	bind :8443
+	timeout  client 6s
+	timeout  server 6s
+	timeout connect 6s
+
+	tcp-request inspect-delay 4s
+
+	acl white_list src 127.0.0.2
+	acl black_list src 127.0.0.3
+
+	# note: SSLv2 is not used anymore, SSLv3.1 is TLSv1.
+	acl obsolete_ssl  req_ssl_ver   lt 3
+	acl correct_ssl   req_ssl_ver   3.0-3.1
+	acl invalid_ssl   req_ssl_ver   gt 3.1
+
+	tcp-request content accept if white_list
+	tcp-request content reject if black_list
+	tcp-request content reject if !correct_ssl
+
+	balance roundrobin
+	server srv1 127.0.0.1:443
+