-
Notifications
You must be signed in to change notification settings - Fork 23
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add a middleware for promoting a connection to WebSocket (fix #205)
Call 'next' if the client does not want to establish a handshake. Add some documentation to cover the WebSocket middleware.
- Loading branch information
Showing
11 changed files
with
1,125 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,33 @@ | ||
WebSocket | ||
========= | ||
|
||
.. versionadded:: 0.4 | ||
|
||
Valum support WebSocket via :valadoc:`libsoup-2.4/Soup.WebsocketConnection` | ||
implementation if libsoup-2.4 (>=2.50) is installed. | ||
|
||
.. note:: | ||
|
||
Not all server protocols support WebSocket. It is at least guaranteed to | ||
work with the :doc:`../vsgi/server/http` server and for other, it should only a matter of | ||
implementation. | ||
|
||
The ``websocket`` middleware can be used in the context of a ``GET`` method. It | ||
will perform the handshake and promote the underlying :doc:`../vsgi/connection` | ||
to perform WebSocket message exchanges. | ||
|
||
The first argument is a list of supported protocols, which can be left empty. | ||
The second argument is a forward callback that will receive the WebSocket | ||
connection. | ||
|
||
:: | ||
|
||
app.get ("/", websocket ({}, (req, res, next, ctx, ws) => { | ||
ws.send_text (); | ||
return true; | ||
})); | ||
|
||
Since the middleware actually *steal* the connection, body streams are rendered | ||
useless and futher communications should be done exclusively via the WebSocket | ||
connection. | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,122 @@ | ||
/* | ||
* This file is part of Valum. | ||
* | ||
* Valum is free software: you can redistribute it and/or modify it under the | ||
* terms of the GNU Lesser General Public License as published by the Free | ||
* Software Foundation, either version 3 of the License, or (at your option) any | ||
* later version. | ||
* | ||
* Valum is distributed in the hope that it will be useful, but WITHOUT ANY | ||
* WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR | ||
* A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more | ||
* details. | ||
* | ||
* You should have received a copy of the GNU Lesser General Public License | ||
* along with Valum. If not, see <http://www.gnu.org/licenses/>. | ||
*/ | ||
|
||
using GLib; | ||
|
||
namespace Valum { | ||
|
||
#if SOUP_2_50 | ||
/** | ||
* Perform a handshake and promote the {@link Request.connection} to | ||
* communicate using the WebSocket protocol. | ||
* | ||
* If the client does not provide 'Connection: Upgrade' nor 'Upgrade: websocket', | ||
* the control is passed to the next handler. | ||
* | ||
* @param protocols a list of supported protocols, or an empty array | ||
* not to use any in particular | ||
*/ | ||
[Version (since = "0.4")] | ||
public HandlerCallback websocket (string[] protocols, ForwardCallback<Soup.WebsocketConnection> forward) { | ||
return (req, res, next, ctx) => { | ||
if (req.method != "GET") { | ||
throw new ClientError.METHOD_NOT_ALLOWED ("The WebSocket protocol require a 'GET' method to be initiated."); | ||
} | ||
|
||
var connection = req.headers.get_one ("Connection"); | ||
if (connection == null || !Soup.str_case_equal (connection, "Upgrade")) { | ||
return next (); | ||
} | ||
|
||
var upgrade = req.headers.get_one ("Upgrade"); | ||
if (upgrade == null || !Soup.str_case_equal (upgrade, "websocket")) { | ||
return next (); | ||
} | ||
|
||
var ws_key = req.headers.get_one ("Sec-WebSocket-Key"); | ||
var ws_protocol = req.headers.get_list ("Sec-WebSocket-Protocol"); | ||
var ws_version = req.headers.get_one ("Sec-WebSocket-Version"); | ||
|
||
if (ws_key == null) { | ||
throw new ClientError.BAD_REQUEST ("The 'Sec-WebSocket-Key' header is missing."); | ||
} | ||
|
||
if (ws_version == null) { | ||
throw new ClientError.BAD_REQUEST ("The 'Sec-WebSocket-Version' header is missing."); | ||
} | ||
|
||
var ws_accept_sha1 = new Checksum (ChecksumType.SHA1); | ||
|
||
ws_accept_sha1.update (ws_key.data, ws_key.length); | ||
ws_accept_sha1.update ("258EAFA5-E914-47DA-95CA-C5AB0DC85B11".data, 36); | ||
|
||
uint8 ws_accept_digest[20]; | ||
size_t ws_accept_digest_len = 20; | ||
ws_accept_sha1.get_digest (ws_accept_digest, ref ws_accept_digest_len); | ||
|
||
var ws_accept = Base64.encode (ws_accept_digest); | ||
|
||
string? chosen_protocol = null; | ||
if (protocols.length == 0) { | ||
chosen_protocol = null; | ||
} else if (ws_protocol == null) { | ||
chosen_protocol = protocols[0]; | ||
} else { | ||
// negotiate the protocol | ||
foreach (var protocol in protocols) { | ||
foreach (var client_protocol in Soup.header_parse_list (ws_protocol)) { | ||
if (Soup.str_case_equal (protocol, client_protocol)) { | ||
chosen_protocol = protocol; | ||
break; | ||
} | ||
} | ||
if (chosen_protocol != null) { | ||
break; | ||
} | ||
} | ||
} | ||
|
||
if (res.head_written) { | ||
throw new ServerError.INTERNAL_SERVER_ERROR ("The connection cannot be promoted to WebSocket: headers have already been written."); | ||
} | ||
|
||
res.status = Soup.Status.SWITCHING_PROTOCOLS; | ||
res.headers.replace ("Connection", "Upgrade"); | ||
res.headers.replace ("Upgrade", "websocket"); | ||
res.headers.replace ("Sec-WebSocket-Accept", ws_accept); | ||
|
||
// ensure that the full head has been sent and that no pending write | ||
// will interfer with the WebSocket flux | ||
res.write_head (null, null); | ||
req.connection.output_stream.flush (); | ||
|
||
IOStream? con = req.steal_connection (); | ||
if (con == null) { | ||
throw new ServerError.NOT_IMPLEMENTED ("..."); | ||
} | ||
|
||
var ws_conn = new Soup.WebsocketConnection (con, | ||
req.uri, | ||
Soup.WebsocketConnectionType.SERVER, | ||
req.headers.get_one ("Origin"), | ||
chosen_protocol); | ||
|
||
return forward (req, res, next, ctx, ws_conn); | ||
}; | ||
} | ||
#endif | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,122 @@ | ||
/* | ||
* This file is part of Valum. | ||
* | ||
* Valum is free software: you can redistribute it and/or modify it under the | ||
* terms of the GNU Lesser General Public License as published by the Free | ||
* Software Foundation, either version 3 of the License, or (at your option) any | ||
* later version. | ||
* | ||
* Valum is distributed in the hope that it will be useful, but WITHOUT ANY | ||
* WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR | ||
* A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more | ||
* details. | ||
* | ||
* You should have received a copy of the GNU Lesser General Public License | ||
* along with Valum. If not, see <http://www.gnu.org/licenses/>. | ||
*/ | ||
|
||
using GLib; | ||
|
||
namespace Valum { | ||
|
||
#if SOUP_2_50 | ||
/** | ||
* Perform a handshake and promote the {@link Request.connection} to | ||
* communicate using the WebSocket protocol. | ||
* | ||
* If the client does not provide 'Connection: Upgrade' nor 'Upgrade: websocket', | ||
* the control is passed to the next handler. | ||
* | ||
* @param protocols a list of supported protocols, or an empty array | ||
* not to use any in particular | ||
*/ | ||
[Version (since = "0.4")] | ||
public HandlerCallback websocket (string[] protocols, ForwardCallback<Soup.WebsocketConnection> forward) { | ||
return (req, res, next, ctx) => { | ||
if (req.method != "GET") { | ||
throw new ClientError.METHOD_NOT_ALLOWED ("The WebSocket protocol require a 'GET' method to be initiated."); | ||
} | ||
|
||
var connection = req.headers.get_one ("Connection"); | ||
if (connection == null || !Soup.str_case_equal (connection, "Upgrade")) { | ||
return next (); | ||
} | ||
|
||
var upgrade = req.headers.get_one ("Upgrade"); | ||
if (upgrade == null || !Soup.str_case_equal (upgrade, "websocket")) { | ||
return next (); | ||
} | ||
|
||
var ws_key = req.headers.get_one ("Sec-WebSocket-Key"); | ||
var ws_protocol = req.headers.get_list ("Sec-WebSocket-Protocol"); | ||
var ws_version = req.headers.get_one ("Sec-WebSocket-Version"); | ||
|
||
if (ws_key == null) { | ||
throw new ClientError.BAD_REQUEST ("The 'Sec-WebSocket-Key' header is missing."); | ||
} | ||
|
||
if (ws_version == null) { | ||
throw new ClientError.BAD_REQUEST ("The 'Sec-WebSocket-Version' header is missing."); | ||
} | ||
|
||
var ws_accept_sha1 = new Checksum (ChecksumType.SHA1); | ||
|
||
ws_accept_sha1.update (ws_key.data, ws_key.length); | ||
ws_accept_sha1.update ("258EAFA5-E914-47DA-95CA-C5AB0DC85B11".data, 36); | ||
|
||
uint8 ws_accept_digest[20]; | ||
size_t ws_accept_digest_len = 20; | ||
ws_accept_sha1.get_digest (ws_accept_digest, ref ws_accept_digest_len); | ||
|
||
var ws_accept = Base64.encode (ws_accept_digest); | ||
|
||
string? chosen_protocol = null; | ||
if (protocols.length == 0) { | ||
chosen_protocol = null; | ||
} else if (ws_protocol == null) { | ||
chosen_protocol = protocols[0]; | ||
} else { | ||
// negotiate the protocol | ||
foreach (var protocol in protocols) { | ||
foreach (var client_protocol in Soup.header_parse_list (ws_protocol)) { | ||
if (Soup.str_case_equal (protocol, client_protocol)) { | ||
chosen_protocol = protocol; | ||
break; | ||
} | ||
} | ||
if (chosen_protocol != null) { | ||
break; | ||
} | ||
} | ||
} | ||
|
||
if (res.head_written) { | ||
throw new ServerError.INTERNAL_SERVER_ERROR ("The connection cannot be promoted to WebSocket: headers have already been written."); | ||
} | ||
|
||
res.status = Soup.Status.SWITCHING_PROTOCOLS; | ||
res.headers.replace ("Connection", "Upgrade"); | ||
res.headers.replace ("Upgrade", "websocket"); | ||
res.headers.replace ("Sec-WebSocket-Accept", ws_accept); | ||
|
||
// ensure that the full head has been sent and that no pending write | ||
// will interfer with the WebSocket flux | ||
res.write_head (null, null); | ||
req.connection.output_stream.flush (); | ||
|
||
IOStream? con = req.steal_connection (); | ||
if (con == null) { | ||
throw new ServerError.NOT_IMPLEMENTED ("..."); | ||
} | ||
|
||
var ws_conn = new Soup.WebsocketConnection (con, | ||
req.uri, | ||
Soup.WebsocketConnectionType.SERVER, | ||
req.headers.get_one ("Origin"), | ||
chosen_protocol); | ||
|
||
return forward (req, res, next, ctx, ws_conn); | ||
}; | ||
} | ||
#endif | ||
} |
Oops, something went wrong.