MINOR: server: add ws keyword

Implement parsing for the server keyword 'ws'. This is used to configure
the mode of selection for websocket protocol. The configuration
documentation has been updated.

A new regtest has been created to test the proper behavior of the
keyword.

(cherry picked from commit f9d5957cd9a94c517a9d1fea8f536ab08922d9a9)
[ad: no support for dynamic servers for error-limit keyword]
Signed-off-by: Amaury Denoyelle <adenoyelle@haproxy.com>
diff --git a/doc/configuration.txt b/doc/configuration.txt
index b78981f..decfd3a 100644
--- a/doc/configuration.txt
+++ b/doc/configuration.txt
@@ -14101,6 +14101,8 @@
 
        server 127.0.0.1:443 ssl crt pub.pem alpn h2,http/1.1
 
+  See also "ws" to use an alternative ALPN for websocket streams.
+
 backup
   When "backup" is present on a server line, the server is only used in load
   balancing when all other non-backup servers are unavailable. Requests coming
@@ -14714,6 +14716,8 @@
   Idea behind this option is to bypass the selection of the best multiplexer's
   protocol for all connections established to this server.
 
+  See also "ws" to use an alternative protocol for websocket streams.
+
 redir <prefix>
   The "redir" parameter enables the redirection mode for all GET and HEAD
   requests addressing this server. This means that instead of having HAProxy
@@ -15046,6 +15050,26 @@
   can both grow and shrink, for instance between 10 and 100 to leave enough
   room above and below for later adjustments.
 
+ws { auto | h1 | h2 }
+  This option allows to configure the protocol used when relaying websocket
+  streams. This is most notably useful when using an HTTP/2 backend without the
+  support for H2 websockets through the RFC8441.
+
+  The default mode is "auto". This will reuse the same protocol as the main
+  one. The only difference is when using ALPN. In this case, it can try to
+  downgrade the ALPN to "http/1.1" only for websocket streams if the configured
+  server ALPN contains it.
+
+  The value "h1" is used to force HTTP/1.1 for websockets streams, through ALPN
+  if SSL ALPN is activated for the server. Similarly, "h2" can be used to
+  force HTTP/2.0 websockets. Use this value with care : the server must support
+  RFC8441 or an error will be reported by haproxy when relaying websockets.
+
+  Note that NPN is not taken into account as its usage has been deprecated in
+  favor of the ALPN extension.
+
+  See also "alpn" and "proto".
+
 
 5.3. Server IP address resolution using DNS
 -------------------------------------------
diff --git a/doc/management.txt b/doc/management.txt
index 38af652..eaa4835 100644
--- a/doc/management.txt
+++ b/doc/management.txt
@@ -1476,6 +1476,7 @@
   - tfo
   - usesrc
   - weight
+  - ws
 
   Their syntax is similar to the server line from the configuration file,
   please refer to their individual documentation for details.
diff --git a/reg-tests/http-messaging/common.pem b/reg-tests/http-messaging/common.pem
new file mode 120000
index 0000000..a4433d5
--- /dev/null
+++ b/reg-tests/http-messaging/common.pem
@@ -0,0 +1 @@
+../ssl/common.pem
\ No newline at end of file
diff --git a/reg-tests/http-messaging/srv_ws.vtc b/reg-tests/http-messaging/srv_ws.vtc
new file mode 100644
index 0000000..811d9db
--- /dev/null
+++ b/reg-tests/http-messaging/srv_ws.vtc
@@ -0,0 +1,181 @@
+# This reg-test checks websocket support in regards with the server keyword
+# 'ws'
+
+varnishtest "h2 backend websocket management via server keyword"
+
+feature ignore_unknown_macro
+
+#REQUIRE_VERSION=2.4
+#REQUIRE_OPTION=OPENSSL
+
+# haproxy server
+haproxy hapsrv -conf {
+	defaults
+		mode http
+		timeout connect 5s
+		timeout client  5s
+		timeout server  5s
+
+	frontend fe
+		bind "fd@${fe}"
+		bind "fd@${fessl}" ssl crt ${testdir}/common.pem alpn h2,http/1.1
+		capture request header sec-websocket-key len 128
+		http-request set-var(txn.ver) req.ver
+		use_backend be
+
+	backend be
+		# define websocket ACL
+		acl ws_handshake hdr(upgrade) -m str websocket
+
+		# handle non-ws streams
+		http-request return status 200 if !ws_handshake
+
+		# handle ws streams
+		#capture request header sec-websocket-key len 128
+		http-request return status 200 hdr connection upgrade hdr upgrade websocket hdr sec-websocket-accept "%[capture.req.hdr(0),concat(258EAFA5-E914-47DA-95CA-C5AB0DC85B11,,),sha1,base64]" if ws_handshake
+		http-after-response set-status 101 if { status eq 200 } { res.hdr(upgrade) -m str websocket }
+		http-after-response set-header x-backend-protocol "%[var(txn.ver)]"
+} -start
+
+# haproxy LB
+haproxy hap -conf {
+	defaults
+		mode http
+		timeout connect 1s
+		timeout client  1s
+		timeout server  1s
+
+	# proto X ws h1 -> websocket on h1
+	listen li
+		bind "fd@${li}"
+		server hap_srv ${hapsrv_fe_addr}:${hapsrv_fe_port} proto h2 ws h1
+
+	# proto X ws h2 -> websocket on h2
+	listen lih2
+		bind "fd@${lih2}"
+		server hap_srv ${hapsrv_fe_addr}:${hapsrv_fe_port} proto h2 ws h2
+
+	# alpn h2,http/1.1 ws h2 -> websocket on h2
+	listen lisslh2
+		bind "fd@${lisslh2}"
+		server hap_srv ${hapsrv_fessl_addr}:${hapsrv_fessl_port} ssl verify none alpn h2,http/1.1 ws h2
+		http-response set-header x-alpn "%[ssl_bc_alpn]"
+
+	# ws auto -> websocket on h1
+	listen liauto
+		bind "fd@${liauto}"
+		server hap_srv ${hapsrv_fe_addr}:${hapsrv_fe_port}
+
+	# alpn h2,http/1.1 ws auto -> websocket on h1
+	listen lissl
+		bind "fd@${lissl}"
+		server hap_srv ${hapsrv_fessl_addr}:${hapsrv_fessl_port} ssl verify none alpn h2,http/1.1 ws auto
+		http-response set-header x-alpn "%[ssl_bc_alpn]"
+	# alpn h2,http/1.1 ws auto -> websocket on h1
+	listen lisslauto
+		bind "fd@${lisslauto}"
+		server hap_srv ${hapsrv_fessl_addr}:${hapsrv_fessl_port} ssl verify none alpn h2,http/1.1
+		http-response set-header x-alpn "%[ssl_bc_alpn]"
+
+	# proto h2 ws auto -> websocket on h2
+	listen liauto2
+		bind "fd@${liauto2}"
+		server hap_srv ${hapsrv_fe_addr}:${hapsrv_fe_port} proto h2
+
+	# alpn h2 ws auto -> websocket on h2
+	listen lisslauto2
+		bind "fd@${lisslauto2}"
+		server hap_srv ${hapsrv_fessl_addr}:${hapsrv_fessl_port} ssl verify none alpn h2 ws auto
+		http-response set-header x-alpn "%[ssl_bc_alpn]"
+} -start
+
+client c1 -connect ${hap_li_sock} {
+	txreq \
+	  -req "GET" \
+	  -url "/" \
+	  -hdr "host: 127.0.0.1" \
+	  -hdr "connection: upgrade" \
+	  -hdr "upgrade: websocket" \
+	  -hdr "sec-websocket-key: dGhlIHNhbXBsZSBub25jZQ=="
+	rxresp
+	expect resp.status == 101
+	expect resp.http.x-backend-protocol == "1.1"
+} -run
+
+client c1.2 -connect ${hap_lih2_sock} {
+	txreq \
+	  -req "GET" \
+	  -url "/" \
+	  -hdr "host: 127.0.0.1" \
+	  -hdr "connection: upgrade" \
+	  -hdr "upgrade: websocket" \
+	  -hdr "sec-websocket-key: dGhlIHNhbXBsZSBub25jZQ=="
+	rxresp
+	expect resp.status == 101
+	expect resp.http.x-backend-protocol == "2.0"
+} -run
+
+client c1.3 -connect ${hap_liauto_sock} {
+	txreq \
+	  -req "GET" \
+	  -url "/" \
+	  -hdr "host: 127.0.0.1" \
+	  -hdr "connection: upgrade" \
+	  -hdr "upgrade: websocket" \
+	  -hdr "sec-websocket-key: dGhlIHNhbXBsZSBub25jZQ=="
+	rxresp
+	expect resp.status == 101
+	expect resp.http.x-backend-protocol == "1.1"
+} -run
+
+client c1.4 -connect ${hap_liauto2_sock} {
+	txreq \
+	  -req "GET" \
+	  -url "/" \
+	  -hdr "host: 127.0.0.1" \
+	  -hdr "connection: upgrade" \
+	  -hdr "upgrade: websocket" \
+	  -hdr "sec-websocket-key: dGhlIHNhbXBsZSBub25jZQ=="
+	rxresp
+	expect resp.status == 101
+	expect resp.http.x-backend-protocol == "2.0"
+} -run
+
+client c2 -connect ${hap_lisslauto_sock} {
+	txreq \
+	  -req "GET" \
+	  -url "/" \
+	  -hdr "host: 127.0.0.1" \
+	  -hdr "connection: upgrade" \
+	  -hdr "upgrade: websocket" \
+	  -hdr "sec-websocket-key: dGhlIHNhbXBsZSBub25jZQ=="
+	rxresp
+	expect resp.status == 101
+	expect resp.http.x-alpn == "http/1.1"
+} -run
+
+client c2.2 -connect ${hap_lisslauto2_sock} {
+	txreq \
+	  -req "GET" \
+	  -url "/" \
+	  -hdr "host: 127.0.0.1" \
+	  -hdr "connection: upgrade" \
+	  -hdr "upgrade: websocket" \
+	  -hdr "sec-websocket-key: dGhlIHNhbXBsZSBub25jZQ=="
+	rxresp
+	expect resp.status == 101
+	expect resp.http.x-alpn == "h2"
+} -run
+
+client c2.3 -connect ${hap_lisslh2_sock} {
+	txreq \
+	  -req "GET" \
+	  -url "/" \
+	  -hdr "host: 127.0.0.1" \
+	  -hdr "connection: upgrade" \
+	  -hdr "upgrade: websocket" \
+	  -hdr "sec-websocket-key: dGhlIHNhbXBsZSBub25jZQ=="
+	rxresp
+	expect resp.status == 101
+	expect resp.http.x-alpn == "h2"
+} -run
diff --git a/src/server.c b/src/server.c
index 6989f85..54637dc 100644
--- a/src/server.c
+++ b/src/server.c
@@ -505,6 +505,33 @@
 	return 0;
 }
 
+/* Parse the "ws" keyword */
+static int srv_parse_ws(char **args, int *cur_arg,
+                        struct proxy *curproxy, struct server *newsrv, char **err)
+{
+	if (!args[*cur_arg + 1]) {
+		memprintf(err, "'%s' expects 'auto', 'h1' or 'h2' value", args[*cur_arg]);
+		return ERR_ALERT | ERR_FATAL;
+	}
+
+	if (strcmp(args[*cur_arg + 1], "h1") == 0) {
+		newsrv->ws = SRV_WS_H1;
+	}
+	else if (strcmp(args[*cur_arg + 1], "h2") == 0) {
+		newsrv->ws = SRV_WS_H2;
+	}
+	else if (strcmp(args[*cur_arg + 1], "auto") == 0) {
+		newsrv->ws = SRV_WS_AUTO;
+	}
+	else {
+		memprintf(err, "'%s' has to be 'auto', 'h1' or 'h2'", args[*cur_arg]);
+		return ERR_ALERT | ERR_FATAL;
+	}
+
+
+	return 0;
+}
+
 /* Parse the "init-addr" server keyword */
 static int srv_parse_init_addr(char **args, int *cur_arg,
                                struct proxy *curproxy, struct server *newsrv, char **err)
@@ -1733,6 +1760,7 @@
 	{ "disabled",            srv_parse_disabled,            0,  1,  1 }, /* Start the server in 'disabled' state */
 	{ "enabled",             srv_parse_enabled,             0,  1,  1 }, /* Start the server in 'enabled' state */
 	{ "error-limit",         srv_parse_error_limit,         1,  1,  0 }, /* Configure the consecutive count of check failures to consider a server on error */
+	{ "ws",                  srv_parse_ws,                  1,  1,  1 }, /* websocket protocol */
 	{ "id",                  srv_parse_id,                  1,  0,  1 }, /* set id# of server */
 	{ "init-addr",           srv_parse_init_addr,           1,  1,  0 }, /* */
 	{ "log-proto",           srv_parse_log_proto,           1,  1,  0 }, /* Set the protocol for event messages, only relevant in a ring section */