Hi, we recently stumbled over the missing WebSocket support in pound. I attached a patch against vanilla pound-2.8 which adds support. So far it has been tested with Guacamole (https://guacamole.apache.org/) and with a commercial product using WebSockts and it worked without any problems or side effects.
The patch collects the various markers for an upgrade request and for the server`s acceptance of the upgrade in variable "is_ws". When all markers have been collected, the connection switches to "WebSocket mode", i.e. simply forward everything between client and server in both directions until the connection is closed or times out. There is no parsing of WebSocket frames. With the patch, WebSockets should start working out of the box. There's no need to configure anything special (except for the usual listener, service and backend configuration of course). WebSocket connections are usually long-lived (hours or even days). However WebSocket clients usually send keep alive packets to prevent timeouts or silently re-connect when the connection is closed. So I decided to use a rather conservative timeout of just 600 seconds. You can override this default with the new configuration file directive "WSTimeout <seconds>" on the global level and per backend. Regards, Frank
diff -Nur Pound-2.8.orig/config.c Pound-2.8/config.c --- Pound-2.8.orig/config.c 2018-05-11 12:16:05.000000000 +0200 +++ Pound-2.8/config.c 2018-07-30 14:10:01.693667854 +0200 @@ -77,7 +77,7 @@ static regex_t ListenHTTP, ListenHTTPS, End, Address, Port, Cert, xHTTP, Client, CheckURL; static regex_t Err414, Err500, Err501, Err503, MaxRequest, HeadRemove, RewriteLocation, RewriteDestination; static regex_t Service, ServiceName, URL, HeadRequire, HeadDeny, BackEnd, Emergency, Priority, HAport, HAportAddr; -static regex_t Redirect, RedirectN, TimeOut, Session, Type, TTL, ID; +static regex_t Redirect, RedirectN, TimeOut, WSTimeOut, Session, Type, TTL, ID; static regex_t ClientCert, AddHeader, DisableProto, SSLAllowClientRenegotiation, SSLHonorCipherOrder, Ciphers; static regex_t CAlist, VerifyList, CRLlist, NoHTTPS11, Grace, Include, ConnTO, IgnoreCase, HTTPS; static regex_t Disabled, Threads, CNName, Anonymise, ECDHCurve; @@ -96,6 +96,7 @@ static int def_facility = LOG_DAEMON; static int clnt_to = 10; static int be_to = 15; +static int ws_to = 600; static int be_connto = 15; static int ignore_case = 0; #if OPENSSL_VERSION_NUMBER >= 0x0090800fL @@ -242,6 +243,7 @@ res->addr.ai_socktype = SOCK_STREAM; res->to = is_emergency? 120: be_to; res->conn_to = is_emergency? 120: be_connto; + res->ws_to = is_emergency? 120: ws_to; res->alive = 1; memset(&res->addr, 0, sizeof(res->addr)); res->priority = 5; @@ -292,6 +294,8 @@ res->priority = atoi(lin + matches[1].rm_so); } else if(!regexec(&TimeOut, lin, 4, matches, 0)) { res->to = atoi(lin + matches[1].rm_so); + } else if(!regexec(&WSTimeOut, lin, 4, matches, 0)) { + res->ws_to = atoi(lin + matches[1].rm_so); } else if(!regexec(&ConnTO, lin, 4, matches, 0)) { res->conn_to = atoi(lin + matches[1].rm_so); } else if(!regexec(&HAport, lin, 4, matches, 0)) { @@ -1340,6 +1344,8 @@ alive_to = atoi(lin + matches[1].rm_so); } else if(!regexec(&TimeOut, lin, 4, matches, 0)) { be_to = atoi(lin + matches[1].rm_so); + } else if(!regexec(&WSTimeOut, lin, 4, matches, 0)) { + ws_to = atoi(lin + matches[1].rm_so); } else if(!regexec(&ConnTO, lin, 4, matches, 0)) { be_connto = atoi(lin + matches[1].rm_so); } else if(!regexec(&IgnoreCase, lin, 4, matches, 0)) { @@ -1467,6 +1473,7 @@ || regcomp(&Emergency, "^[ \t]*Emergency[ \t]*$", REG_ICASE | REG_NEWLINE | REG_EXTENDED) || regcomp(&Priority, "^[ \t]*Priority[ \t]+([1-9])[ \t]*$", REG_ICASE | REG_NEWLINE | REG_EXTENDED) || regcomp(&TimeOut, "^[ \t]*TimeOut[ \t]+([1-9][0-9]*)[ \t]*$", REG_ICASE | REG_NEWLINE | REG_EXTENDED) + || regcomp(&WSTimeOut, "^[ \t]*WSTimeOut[ \t]+([1-9][0-9]*)[ \t]*$", REG_ICASE | REG_NEWLINE | REG_EXTENDED) || regcomp(&HAport, "^[ \t]*HAport[ \t]+([1-9][0-9]*)[ \t]*$", REG_ICASE | REG_NEWLINE | REG_EXTENDED) || regcomp(&HAportAddr, "^[ \t]*HAport[ \t]+([^ \t]+)[ \t]+([1-9][0-9]*)[ \t]*$", REG_ICASE | REG_NEWLINE | REG_EXTENDED) || regcomp(&Redirect, "^[ \t]*Redirect[ \t]+\"(.+)\"[ \t]*$", REG_ICASE | REG_NEWLINE | REG_EXTENDED) @@ -1632,6 +1639,7 @@ regfree(&Emergency); regfree(&Priority); regfree(&TimeOut); + regfree(&WSTimeOut); regfree(&HAport); regfree(&HAportAddr); regfree(&Redirect); diff -Nur Pound-2.8.orig/http.c Pound-2.8/http.c --- Pound-2.8.orig/http.c 2018-05-11 12:16:05.000000000 +0200 +++ Pound-2.8/http.c 2018-07-30 14:10:01.693667854 +0200 @@ -541,7 +541,7 @@ void do_http(thr_arg *arg) { - int cl_11, be_11, res, chunked, n, sock, no_cont, skip, conn_closed, force_10, sock_proto, is_rpc; + int cl_11, be_11, res, chunked, n, sock, no_cont, skip, conn_closed, force_10, sock_proto, is_rpc, is_ws; LISTENER *lstn; SERVICE *svc; BACKEND *backend, *cur_backend, *old_backend; @@ -662,6 +662,7 @@ for(cl_11 = be_11 = 0;;) { res_bytes = L0; is_rpc = -1; + is_ws = 0; v_host[0] = referer[0] = u_agent[0] = u_name[0] = '\0'; conn_closed = 0; for(n = 0; n < MAXHEADERS; n++) @@ -689,6 +690,8 @@ is_rpc = 1; else if(!strncasecmp(request + matches[1].rm_so, "RPC_OUT_DATA", matches[1].rm_eo - matches[1].rm_so)) is_rpc = 0; + else if(!strncasecmp(request + matches[1].rm_so, "GET", matches[1].rm_eo - matches[1].rm_so)) + is_ws |= 0x1; } else { addr2str(caddr, MAXBUF - 1, &from_host, 1); logmsg(LOG_WARNING, "(%lx) e501 bad request \"%s\" from %s", pthread_self(), request, caddr); @@ -733,6 +736,13 @@ case HEADER_CONNECTION: if(!strcasecmp("close", buf)) conn_closed = 1; + /* Connection: upgrade */ + else if(!regexec(&CONN_UPGRD, buf, 0, NULL, 0)) + is_ws |= 0x2; + break; + case HEADER_UPGRADE: + if(!strcasecmp("websocket", buf)) + is_ws |= 0x4; break; case HEADER_TRANSFER_ENCODING: if(!strcasecmp("chunked", buf)) @@ -1402,12 +1412,21 @@ /* some response codes (1xx, 204, 304) have no content */ if(!no_cont && !regexec(&RESP_IGN, response, 0, NULL, 0)) no_cont = 1; + if(!strncasecmp("101", response + 9, 3)) + is_ws |= 0x10; for(chunked = 0, cont = -1L, n = 1; n < MAXHEADERS && headers[n]; n++) { switch(check_header(headers[n], buf)) { case HEADER_CONNECTION: if(!strcasecmp("close", buf)) conn_closed = 1; + /* Connection: upgrade */ + else if(!regexec(&CONN_UPGRD, buf, 0, NULL, 0)) + is_ws |= 0x20; + break; + case HEADER_UPGRADE: + if(!strcasecmp("websocket", buf)) + is_ws |= 0x40; break; case HEADER_TRANSFER_ENCODING: if(!strcasecmp("chunked", buf)) { @@ -1571,6 +1590,114 @@ clean_all(); return; } + } else if(is_ws == 0x77) { + /* + * special mode for Websockets - content until EOF + */ + char one; + BIO *cl_unbuf; + BIO *be_unbuf; + struct pollfd p[2]; + + cl_11 = be_11 = 0; + + memset(p, 0, sizeof(p)); + BIO_get_fd(cl, &p[0].fd); + p[0].events = POLLIN | POLLPRI; + BIO_get_fd(be, &p[1].fd); + p[1].events = POLLIN | POLLPRI; + + while (BIO_pending(cl) || BIO_pending(be) || poll(p, 2, cur_backend->ws_to * 1000) > 0) { + + /* + * first read whatever is already in the input buffer + */ + while(BIO_pending(cl)) { + if(BIO_read(cl, &one, 1) != 1) { + logmsg(LOG_NOTICE, "(%lx) error read ws request pending: %s", + pthread_self(), strerror(errno)); + clean_all(); + return; + } + if(BIO_write(be, &one, 1) != 1) { + if(errno) + logmsg(LOG_NOTICE, "(%lx) error write ws request pending: %s", + pthread_self(), strerror(errno)); + clean_all(); + return; + } + } + BIO_flush(be); + + while(BIO_pending(be)) { + if(BIO_read(be, &one, 1) != 1) { + logmsg(LOG_NOTICE, "(%lx) error read ws response pending: %s", + pthread_self(), strerror(errno)); + clean_all(); + return; + } + if(BIO_write(cl, &one, 1) != 1) { + if(errno) + logmsg(LOG_NOTICE, "(%lx) error write ws response pending: %s", + pthread_self(), strerror(errno)); + clean_all(); + return; + } + res_bytes++; + } + BIO_flush(cl); + + /* + * find the socket BIO in the chain + */ + if ((cl_unbuf = BIO_find_type(cl, lstn->ctx? BIO_TYPE_SSL : BIO_TYPE_SOCKET)) == NULL) { + logmsg(LOG_WARNING, "(%lx) error get unbuffered: %s", pthread_self(), strerror(errno)); + clean_all(); + return; + } + if((be_unbuf = BIO_find_type(be, cur_backend->ctx? BIO_TYPE_SSL : BIO_TYPE_SOCKET)) == NULL) { + logmsg(LOG_WARNING, "(%lx) error get unbuffered: %s", pthread_self(), strerror(errno)); + clean_all(); + return; + } + + /* + * copy till EOF + */ + if(p[0].revents) { + res = BIO_read(cl_unbuf, buf, MAXBUF); + if(res <= 0) { + break; + } + if(BIO_write(be, buf, res) != res) { + if(errno) + logmsg(LOG_NOTICE, "(%lx) error copy ws request body: %s", + pthread_self(), strerror(errno)); + clean_all(); + return; + } else { + BIO_flush(be); + } + p[0].revents = 0; + } + if(p[1].revents) { + res = BIO_read(be_unbuf, buf, MAXBUF); + if(res <= 0) { + break; + } + if(BIO_write(cl, buf, res) != res) { + if(errno) + logmsg(LOG_NOTICE, "(%lx) error copy ws response body: %s", + pthread_self(), strerror(errno)); + clean_all(); + return; + } else { + res_bytes += res; + BIO_flush(cl); + } + p[1].revents = 0; + } + } } } end_req = cur_time(); diff -Nur Pound-2.8.orig/pound.8 Pound-2.8/pound.8 --- Pound-2.8.orig/pound.8 2018-05-11 12:16:05.000000000 +0200 +++ Pound-2.8/pound.8 2018-07-30 14:10:01.693667854 +0200 @@ -289,6 +289,13 @@ .B TimeOut value. This value can be overridden for specific back-ends. .TP +\fBWSTimeOut\fR value +How long should +.B Pound +wait for data from either back-end or client in a connection upgraded to +a WebSocket (in seconds). Default: 600 seconds. +This value can be overridden for specific back-ends. +.TP \fBGrace\fR value How long should .B Pound @@ -762,6 +769,11 @@ .I ConnTO value. .TP +\fBWSTimeOut\fR val +Override the global +.I WSTimeOut +value. +.TP \fBHAport\fR [ address ] port A port (and optional address) to be used for server function checks. See below the "High Availability" section for a more detailed discussion. By default diff -Nur Pound-2.8.orig/pound.c Pound-2.8/pound.c --- Pound-2.8.orig/pound.c 2018-05-11 12:16:05.000000000 +0200 +++ Pound-2.8/pound.c 2018-07-30 14:10:01.693667854 +0200 @@ -47,6 +47,7 @@ LISTENER *listeners; /* all available listeners */ regex_t HEADER, /* Allowed header */ + CONN_UPGRD, /* upgrade in connection header */ CHUNK_HEAD, /* chunk header line */ RESP_SKIP, /* responses for which we skip response */ RESP_IGN, /* responses for which we ignore content */ @@ -287,6 +288,7 @@ /* prepare regular expressions */ if(regcomp(&HEADER, "^([a-z0-9!#$%&'*+.^_`|~-]+):[ \t]*(.*)[ \t]*$", REG_ICASE | REG_NEWLINE | REG_EXTENDED) + || regcomp(&CONN_UPGRD, "(^|[ \t,])upgrade([ \t,]|$)", REG_ICASE | REG_NEWLINE | REG_EXTENDED) || regcomp(&CHUNK_HEAD, "^([0-9a-f]+).*$", REG_ICASE | REG_NEWLINE | REG_EXTENDED) || regcomp(&RESP_SKIP, "^HTTP/1.1 100.*$", REG_ICASE | REG_NEWLINE | REG_EXTENDED) || regcomp(&RESP_IGN, "^HTTP/1.[01] (10[1-9]|1[1-9][0-9]|204|30[456]).*$", REG_ICASE | REG_NEWLINE | REG_EXTENDED) diff -Nur Pound-2.8.orig/pound.h Pound-2.8/pound.h --- Pound-2.8.orig/pound.h 2018-05-11 12:16:05.000000000 +0200 +++ Pound-2.8/pound.h 2018-07-30 14:10:01.697667855 +0200 @@ -276,6 +276,7 @@ control_sock; /* control socket */ extern regex_t HEADER, /* Allowed header */ + CONN_UPGRD, /* upgrade in connection header */ CHUNK_HEAD, /* chunk header line */ RESP_SKIP, /* responses for which we skip response */ RESP_IGN, /* responses for which we ignore content */ @@ -319,6 +320,7 @@ int priority; /* priority */ int to; /* read/write time-out */ int conn_to; /* connection time-out */ + int ws_to; /* websocket time-out */ struct addrinfo ha_addr; /* HA address/port */ char *url; /* for redirectors */ int redir_req; /* the redirect should include the request path */ @@ -440,6 +442,7 @@ #define HEADER_URI 9 #define HEADER_DESTINATION 10 #define HEADER_EXPECT 11 +#define HEADER_UPGRADE 13 /* control request stuff */ typedef enum { diff -Nur Pound-2.8.orig/svc.c Pound-2.8/svc.c --- Pound-2.8.orig/svc.c 2018-05-11 12:16:05.000000000 +0200 +++ Pound-2.8/svc.c 2018-07-30 14:10:01.697667855 +0200 @@ -395,6 +395,7 @@ { "User-agent", 10, HEADER_USER_AGENT }, { "Destination", 11, HEADER_DESTINATION }, { "Expect", 6, HEADER_EXPECT }, + { "Upgrade", 7, HEADER_UPGRADE }, { "", 0, HEADER_OTHER }, }; int i;