Difference between revisions of "Extension:WebSocket"

From Organic Design wiki
(example config)
(See also: Crossbar.io and AngularJS)
 
(6 intermediate revisions by 2 users not shown)
Line 8: Line 8:
  
 
There are some Perl dependencies which can be installed via CPAN on Debian-like systems as follows:
 
There are some Perl dependencies which can be installed via CPAN on Debian-like systems as follows:
{{code|<bash>apt-get install cpanminus
+
<source lang="bash">
cpanm IO::Socket::SSL IO::Socket:INET Net::WebSocket::Server AnyEvent::WebSocket::Client</bash>}}
+
apt-get install cpanminus
 +
cpanm IO::Socket::SSL IO::Socket:INET Net::WebSocket::Server AnyEvent::WebSocket::Client
 +
</source>
 +
 
  
 
The configuration options for the extension are set after the include statement in your ''LocalSettings.php'' and are as follows.
 
The configuration options for the extension are set after the include statement in your ''LocalSettings.php'' and are as follows.
Line 15: Line 18:
 
!Name!!Default!!Description
 
!Name!!Default!!Description
 
|-
 
|-
|WebSocket::$port||1729||Port the WebSocket daemon will run on
+
|WebSocket::$port||1729||Port the WebSocket daemon will listen on (and SSL would listen on 1730)
 
|-
 
|-
 
|WebSocket::$rewrite||''false''||Configure URL rewriting so that the WebSocket port doesn't need to be public (explained in detail below)
 
|WebSocket::$rewrite||''false''||Configure URL rewriting so that the WebSocket port doesn't need to be public (explained in detail below)
Line 26: Line 29:
 
|-
 
|-
 
|WebSocket::$ssl_key||''false''||The path to your site's SSL key file is in this setting
 
|WebSocket::$ssl_key||''false''||The path to your site's SSL key file is in this setting
 +
|-
 +
|WebSocket::$clientID||''Unique ID''||You can override the sender name of WebSocket messages here
 
|}
 
|}
 +
  
 
Here's a sample configuration that would go in ''LocalSettings.php'':
 
Here's a sample configuration that would go in ''LocalSettings.php'':
{{code|<php>include( "$IP/extensions/WebSocket/WebSocket.php" );
+
<source lang="php">
 +
include( "$IP/extensions/WebSocket/WebSocket.php" );
 
WebSocket::$log = __DIR__ . '/ws.log';
 
WebSocket::$log = __DIR__ . '/ws.log';
 
WebSocket::$rewrite = true;
 
WebSocket::$rewrite = true;
 
WebSocket::$ssl_cert = '/var/www/ssl/localhost.crt';
 
WebSocket::$ssl_cert = '/var/www/ssl/localhost.crt';
WebSocket::$ssl_key = '/var/www/ssl/localhost.key';</php>}}
+
WebSocket::$ssl_key = '/var/www/ssl/localhost.key';
 +
</source>
  
 
== Usage ==
 
== Usage ==
 
This extension is intended to be used by other extensions that currently implement an Ajax polling system to determine if a particular event has occurred. Such extensions can be adjusted so that their Ajax polling code includes a condition to poll only if a WebSocket connection is not present. The WebSocket and it's state are available in a global object called ''window.webSocket'' which is defined in [http://svn.organicdesign.co.nz/filedetails.php?repname=extensions&path=%2FWebSocket%2Fwebsocket.js websocket.js]. Here's an example snippet where the ''setTimeout'' callback that would re-call the ''updateData'' function again after five seconds has been made conditional upon their being no WebSocket connection.
 
This extension is intended to be used by other extensions that currently implement an Ajax polling system to determine if a particular event has occurred. Such extensions can be adjusted so that their Ajax polling code includes a condition to poll only if a WebSocket connection is not present. The WebSocket and it's state are available in a global object called ''window.webSocket'' which is defined in [http://svn.organicdesign.co.nz/filedetails.php?repname=extensions&path=%2FWebSocket%2Fwebsocket.js websocket.js]. Here's an example snippet where the ''setTimeout'' callback that would re-call the ''updateData'' function again after five seconds has been made conditional upon their being no WebSocket connection.
{{code|<js>function updateFooData() {
+
<source lang="js">
 +
function updateFooData() {
 
$.ajax({
 
$.ajax({
 
type: 'GET',
 
type: 'GET',
Line 47: Line 56:
 
if( !( typeof webSocket === 'object' && webSocket.connected() ) ) setTimeout( updateFooData, 5000 );
 
if( !( typeof webSocket === 'object' && webSocket.connected() ) ) setTimeout( updateFooData, 5000 );
 
});
 
});
}</js>}}
+
}
 +
</source>
  
  
 
The extension which is upgrading to WebSocket must also tell the ''webSocket'' object to connect to the daemon and declare it's data-updating function as a callback for the desired message type as in the following example. We also add the data-updating method to the ''disconnected'' event of the WebSocket so that the regular Ajax updating will resume if the WebSocket becomes unavailable for some reason.
 
The extension which is upgrading to WebSocket must also tell the ''webSocket'' object to connect to the daemon and declare it's data-updating function as a callback for the desired message type as in the following example. We also add the data-updating method to the ''disconnected'' event of the WebSocket so that the regular Ajax updating will resume if the WebSocket becomes unavailable for some reason.
{{code|<js>if( typeof webSocket === 'object' ) {
+
<source lang="js">
 +
if( typeof webSocket === 'object' ) {
 
webSocket.connect();
 
webSocket.connect();
 
webSocket.disconnected( updateData );
 
webSocket.disconnected( updateData );
 
webSocket.subscribe( 'FooDataChanged', updateData );
 
webSocket.subscribe( 'FooDataChanged', updateData );
}</js>}}
+
}
 +
</source>
  
 
=== Event names ===
 
=== Event names ===
 
Using a simple string name like the examples above creates an event that will be specific to your extension, but if there are many wikis running on the server and all sharing the same WebSocket daemon, then all of them that are running the same extension will receive the events. The WebSocket extension provides a configuration option to the client side called ''wsWikiID'' which is a string composed of the wiki database name and table prefix so that it's unique to each wiki. This can be used to make the name event name, for example.
 
Using a simple string name like the examples above creates an event that will be specific to your extension, but if there are many wikis running on the server and all sharing the same WebSocket daemon, then all of them that are running the same extension will receive the events. The WebSocket extension provides a configuration option to the client side called ''wsWikiID'' which is a string composed of the wiki database name and table prefix so that it's unique to each wiki. This can be used to make the name event name, for example.
{{code|<js>var wsFooDataChanged = mw.config.get( 'wsWikiID' ) + 'FooDataChanged';</js>}}
+
<source lang="js">
 +
var wsFooDataChanged = mw.config.get( 'wsWikiID' ) + 'FooDataChanged';
 +
</source>
  
  
 
Another possibility is that the events may only be relevant to the current article, for example the [[AjaxComments]] extension needs to be notified whenever comments on the current page change, so it defines an event name which includes not only the wiki ID but also the article ID as well:
 
Another possibility is that the events may only be relevant to the current article, for example the [[AjaxComments]] extension needs to be notified whenever comments on the current page change, so it defines an event name which includes not only the wiki ID but also the article ID as well:
{{code|<js>var wsAjaxCommentsEvent = mw.config.get( 'wsWikiID' ) + mw.config.get( 'wgArticleId' ) + ':UpdateComments';</js>}}
+
<source lang="js">
 +
var wsAjaxCommentsEvent = mw.config.get( 'wsWikiID' ) + mw.config.get( 'wgArticleId' ) + ':UpdateComments';
 +
</source>
  
  
Line 70: Line 86:
 
=== Messaging ===
 
=== Messaging ===
 
A message of the type we've declared our callback for must be sent by the source of the changing state. Normally this would be within MediaWiki, for example when an article with specific properties has saved. Here's an example PHP snippet of how to broadcast such a message to all clients when a new article revision is created:
 
A message of the type we've declared our callback for must be sent by the source of the changing state. Normally this would be within MediaWiki, for example when an article with specific properties has saved. Here's an example PHP snippet of how to broadcast such a message to all clients when a new article revision is created:
{{code|<php>function onRevisionInsertComplete( &$rev, $data, $flags ) {
+
<source lang="php">
 +
function onRevisionInsertComplete( &$rev, $data, $flags ) {
 
$page = $rev->getTitle()->getText();
 
$page = $rev->getTitle()->getText();
 
WebSocket::send( 'FooDataChanged', "$page has been changed" );
 
WebSocket::send( 'FooDataChanged', "$page has been changed" );
 
return true;
 
return true;
}</php>}}
+
}
 +
</source>
  
  
 
Events may also arise on the client side in which case the JavaScript object's ''send'' method can be used,
 
Events may also arise on the client side in which case the JavaScript object's ''send'' method can be used,
{{code|<js>webSocket.send( 'FooDataChanged', "Client-side Foo data has changed" );</js>}}
+
<source lang="js">
 +
webSocket.send( 'FooDataChanged', "Client-side Foo data has changed" );
 +
</source>
  
  
 
It's also possible that changes to data of interest to the clients originates outside of the MediaWiki context, such as items being added to log files, or incoming emails etc. Here's an example Perl snippet that triggers our call to ''updateFooData'' (these dependencies can be installed via CPAN). This example demonstrates how to create an SSL connection which uses the ''wss://'' protocol, ''port + 1'' and has the ''ssl_no_verify'' option set to avoid having to pass certificate information.
 
It's also possible that changes to data of interest to the clients originates outside of the MediaWiki context, such as items being added to log files, or incoming emails etc. Here's an example Perl snippet that triggers our call to ''updateFooData'' (these dependencies can be installed via CPAN). This example demonstrates how to create an SSL connection which uses the ''wss://'' protocol, ''port + 1'' and has the ''ssl_no_verify'' option set to avoid having to pass certificate information.
{{code|<perl>#!/usr/bin/perl
+
<source lang="perl">
 +
#!/usr/bin/perl
 
use AnyEvent::WebSocket::Client;
 
use AnyEvent::WebSocket::Client;
  
Line 99: Line 120:
 
# Send a message to the WebSocket connection when some condition occurs (note $msg is JSON)
 
# Send a message to the WebSocket connection when some condition occurs (note $msg is JSON)
 
$ws->send( '{ "type": "FooDataChanged", "msg": "Server-side Foo data has changed" }' );
 
$ws->send( '{ "type": "FooDataChanged", "msg": "Server-side Foo data has changed" }' );
</perl>}}
+
</source>
  
 
=== Message recipients ===
 
=== Message recipients ===
Line 112: Line 133:
  
 
A typical [[Nginx]] configuration might contain something like the following example snippet which matches the format of the URLs intended to be passed to the WebSocket daemon, extracting the port from the URL format to be used in the forwarded request. Note that the extracted port in the ''$1'' variable cannot be used with the DNS resolver in Nginx so "127.0.0.1" has to be used rather than "localhost".
 
A typical [[Nginx]] configuration might contain something like the following example snippet which matches the format of the URLs intended to be passed to the WebSocket daemon, extracting the port from the URL format to be used in the forwarded request. Note that the extracted port in the ''$1'' variable cannot be used with the DNS resolver in Nginx so "127.0.0.1" has to be used rather than "localhost".
{{code|<pre>location ~ websocket:([0-9]+) {
+
<source lang="nginx">
 +
location ~ websocket:([0-9]+) {
 
proxy_pass http://127.0.0.1:$1;
 
proxy_pass http://127.0.0.1:$1;
 
proxy_http_version 1.1;
 
proxy_http_version 1.1;
 
proxy_set_header Upgrade websocket;
 
proxy_set_header Upgrade websocket;
 
proxy_set_header Connection upgrade;
 
proxy_set_header Connection upgrade;
}</pre>}}
+
}
 +
</source>
  
 
In [[Apache]] web-server [https://httpd.apache.org/docs/2.4/mod/mod_proxy_wstunnel.html mod_proxy_wstunnel] should first be installed with <tt>a2enmod proxy_wstunnel</tt> which adds ''WebSocket'' support to it's proxy module. Then a simple rewrite rule can be added which uses the "P" option to direct the rewrite result to the WebSocket daemon via the proxy module. See also [http://serverfault.com/questions/616370/configuring-apache-2-4-mod-proxy-wstunnel-for-socket-io-1-0/623027#623027 this] post for more advanced Apache WebSocket configuration.
 
In [[Apache]] web-server [https://httpd.apache.org/docs/2.4/mod/mod_proxy_wstunnel.html mod_proxy_wstunnel] should first be installed with <tt>a2enmod proxy_wstunnel</tt> which adds ''WebSocket'' support to it's proxy module. Then a simple rewrite rule can be added which uses the "P" option to direct the rewrite result to the WebSocket daemon via the proxy module. See also [http://serverfault.com/questions/616370/configuring-apache-2-4-mod-proxy-wstunnel-for-socket-io-1-0/623027#623027 this] post for more advanced Apache WebSocket configuration.
{{code|<pre>RewriteEngine On
+
<source lang="apache">
 +
RewriteEngine On
 
RewriteCond %{REQUEST_URI} ^/websocket:[0-9]+
 
RewriteCond %{REQUEST_URI} ^/websocket:[0-9]+
RewriteRule ^/websocket(:.+) ws://localhost$1 [P]</pre>}}
+
RewriteRule ^/websocket(:.+) ws://localhost$1 [P]
 +
</source>
  
 
== SSL ==
 
== SSL ==
Line 132: Line 157:
 
The [[AjaxComments]] extension has been upgraded to use a WebSocket instead of Ajax polling if this extension is installed with it. The modifications that were made to the extension to enable WebSocket support can be seen [http://svn.organicdesign.co.nz/comp.php?repname=extensions&compare%5B%5D=%2FMediaWiki%2FAjaxComments%2Fajaxcomments.js%402427&compare%5B%5D=%2FAjaxComments%2Fajaxcomments.js%402089 here].
 
The [[AjaxComments]] extension has been upgraded to use a WebSocket instead of Ajax polling if this extension is installed with it. The modifications that were made to the extension to enable WebSocket support can be seen [http://svn.organicdesign.co.nz/comp.php?repname=extensions&compare%5B%5D=%2FMediaWiki%2FAjaxComments%2Fajaxcomments.js%402427&compare%5B%5D=%2FAjaxComments%2Fajaxcomments.js%402089 here].
  
 +
== See also ==
 +
*[https://github.com/AngularClass/angular-websocket WebSockets for AngularJS]
 +
*[https://github.com/Vayel/crossbardocs/wiki/AngularJS-Application-Components Crossbar.io and AngularJS]
 
[[Category:Extensions]]
 
[[Category:Extensions]]

Latest revision as of 15:44, 10 December 2016

Info.svg This code is in our Git repository here.

Note: If there is no information in this page about this code and it's a MediaWiki extension, there may be something at mediawiki.org.


This extension allows the wiki to respond dynamically to change events using WebSockets instead of Ajax polling. It currently doesn't have any fall-back to Ajax because it was written primarily to augment extensions that already use Ajax polling such as AjaxComments, so these extensions will just continue with Ajax polling as usual if the WebSocket extension is not present or is present but not connected. The extension is designed to serve multiple wikis running on the same host, and can support either insecure or SSL connections at the same time.

Installation and configuration

The extension itself is installed the usual way with an include of WebSocket.php in your LocalSettings.php file. Note that the extension must reside within a directory called "WebSocket" directly in the wikis extensions directory.

There is also a background Perl daemon (WebSocket.pl) which is started by the PHP if it's found not to be running. The daemon uses the Net::WebSocket::Server library to implement a minimal WebSocket service that listens insecurely on the specified port, and using SSL on port + 1.

There are some Perl dependencies which can be installed via CPAN on Debian-like systems as follows:

apt-get install cpanminus
cpanm IO::Socket::SSL IO::Socket:INET Net::WebSocket::Server AnyEvent::WebSocket::Client


The configuration options for the extension are set after the include statement in your LocalSettings.php and are as follows.

Name Default Description
WebSocket::$port 1729 Port the WebSocket daemon will listen on (and SSL would listen on 1730)
WebSocket::$rewrite false Configure URL rewriting so that the WebSocket port doesn't need to be public (explained in detail below)
WebSocket::$perl /usr/bin/perl Location of the Perl interpreter
WebSocket::$log false Set to a writable file to log events and errors from the daemon
WebSocket::$ssl_cert false If the site is running SSL then the WebSocket will also need to be SSL and the path to your site certificate is in this setting
WebSocket::$ssl_key false The path to your site's SSL key file is in this setting
WebSocket::$clientID Unique ID You can override the sender name of WebSocket messages here


Here's a sample configuration that would go in LocalSettings.php:

include( "$IP/extensions/WebSocket/WebSocket.php" );
WebSocket::$log = __DIR__ . '/ws.log';
WebSocket::$rewrite = true;
WebSocket::$ssl_cert = '/var/www/ssl/localhost.crt';
WebSocket::$ssl_key = '/var/www/ssl/localhost.key';

Usage

This extension is intended to be used by other extensions that currently implement an Ajax polling system to determine if a particular event has occurred. Such extensions can be adjusted so that their Ajax polling code includes a condition to poll only if a WebSocket connection is not present. The WebSocket and it's state are available in a global object called window.webSocket which is defined in websocket.js. Here's an example snippet where the setTimeout callback that would re-call the updateData function again after five seconds has been made conditional upon their being no WebSocket connection.

function updateFooData() {
	$.ajax({
		type: 'GET',
		url: mw.util.wikiScript(),
		data: { action: 'ajax', rs: 'Foo::getMyData', rsargs: [arg1,arg2,arg3] },
		dataType: 'json',
		success: renderFooData,
	}).then( function() {
		if( !( typeof webSocket === 'object' && webSocket.connected() ) ) setTimeout( updateFooData, 5000 );
	});
}


The extension which is upgrading to WebSocket must also tell the webSocket object to connect to the daemon and declare it's data-updating function as a callback for the desired message type as in the following example. We also add the data-updating method to the disconnected event of the WebSocket so that the regular Ajax updating will resume if the WebSocket becomes unavailable for some reason.

if( typeof webSocket === 'object' ) {
	webSocket.connect();
	webSocket.disconnected( updateData );
	webSocket.subscribe( 'FooDataChanged', updateData );
}

Event names

Using a simple string name like the examples above creates an event that will be specific to your extension, but if there are many wikis running on the server and all sharing the same WebSocket daemon, then all of them that are running the same extension will receive the events. The WebSocket extension provides a configuration option to the client side called wsWikiID which is a string composed of the wiki database name and table prefix so that it's unique to each wiki. This can be used to make the name event name, for example.

var wsFooDataChanged = mw.config.get( 'wsWikiID' ) + 'FooDataChanged';


Another possibility is that the events may only be relevant to the current article, for example the AjaxComments extension needs to be notified whenever comments on the current page change, so it defines an event name which includes not only the wiki ID but also the article ID as well:

var wsAjaxCommentsEvent = mw.config.get( 'wsWikiID' ) + mw.config.get( 'wgArticleId' ) + ':UpdateComments';


Note that this methods of naming only makes sense when the messages are originating from the client side or from MediaWiki in response to a request. If the events are coming from the server environment outside of the MediaWiki context, then it will be sending messages destined for all wikis and pages.

Messaging

A message of the type we've declared our callback for must be sent by the source of the changing state. Normally this would be within MediaWiki, for example when an article with specific properties has saved. Here's an example PHP snippet of how to broadcast such a message to all clients when a new article revision is created:

function onRevisionInsertComplete( &$rev, $data, $flags ) {
	$page = $rev->getTitle()->getText();
	WebSocket::send( 'FooDataChanged', "$page has been changed" );
	return true;
}


Events may also arise on the client side in which case the JavaScript object's send method can be used,

webSocket.send( 'FooDataChanged', "Client-side Foo data has changed" );


It's also possible that changes to data of interest to the clients originates outside of the MediaWiki context, such as items being added to log files, or incoming emails etc. Here's an example Perl snippet that triggers our call to updateFooData (these dependencies can be installed via CPAN). This example demonstrates how to create an SSL connection which uses the wss:// protocol, port + 1 and has the ssl_no_verify option set to avoid having to pass certificate information.

#!/usr/bin/perl
use AnyEvent::WebSocket::Client;

# Connect to the daemon
my $client = AnyEvent::WebSocket::Client->new( ssl_no_verify => 1 );
my $connected = AnyEvent->condvar;
$client->connect( "wss://localhost:1730" )->cb(sub {
	our $ws = eval { shift->recv };
	die $@ if $@;
	$connected->send;
});
$connected->recv();

   . . .

# Send a message to the WebSocket connection when some condition occurs (note $msg is JSON)
$ws->send( '{ "type": "FooDataChanged", "msg": "Server-side Foo data has changed" }' );

Message recipients

Each wiki client has a unique ID which is automatically added to a "from" argument in the send method. When messages are sent an optional third argument (or "to" field in JSON context) can be added to the function which is an array of recipient IDs. If the argument is missing then the message is broadcast to all clients (but only those hooked in to the event corresponding to the "type" argument will actually receive it).

When the PHP WebSocket::send or the JavaScript webSocket.send methods are used, the ID of the currently running client instance is automatically used for the from argument. When the sending is done from the daemon, no from field is used since it merely acts as a forwarder and expects no messages directly to itself. Other code running outside of the PHP context that send messages may however need to receive responses, so in that case they can simply set the from argument to a unique ID (or even a fixed ID such as "1" if there's only ever one instance of that code running).

The Perl daemon keeps a record of sockets mapped by from ID so that it can forward messages to the intended recipients when it sees a to field in messages it receives, so merely including a from argument is all that's required to become a sender that can be replied to.

The "rewrite" option

This option is used if the server environment doesn't allow new ports to be open to the public. If it's set then the WebSocket requests will be directed to the same domain and port as the wiki is running on with the URI of /websocket. The web-server must be configured to match this URL and redirect it to the local WebSocket daemon's port, but since the protocol is not HTTP, some special configuration is required as shown in the following examples.

A typical Nginx configuration might contain something like the following example snippet which matches the format of the URLs intended to be passed to the WebSocket daemon, extracting the port from the URL format to be used in the forwarded request. Note that the extracted port in the $1 variable cannot be used with the DNS resolver in Nginx so "127.0.0.1" has to be used rather than "localhost".

location ~ websocket:([0-9]+) {
	proxy_pass http://127.0.0.1:$1;
	proxy_http_version 1.1;
	proxy_set_header Upgrade websocket;
	proxy_set_header Connection upgrade;
}

In Apache web-server mod_proxy_wstunnel should first be installed with a2enmod proxy_wstunnel which adds WebSocket support to it's proxy module. Then a simple rewrite rule can be added which uses the "P" option to direct the rewrite result to the WebSocket daemon via the proxy module. See also this post for more advanced Apache WebSocket configuration.

RewriteEngine On
RewriteCond %{REQUEST_URI} ^/websocket:[0-9]+
RewriteRule ^/websocket(:.+) ws://localhost$1 [P]

SSL

The extension is designed to serve multiple wikis running on the same host, and can support either insecure or SSL connections at the same time. If a site is running in SSL then the WebSocket requests coming from the JavaScript must also be SSL, and this means that the daemon must be be able to handle both types of connection (even if using rewrite because the proxy can't convert the data from one form to another). Also this means that the WebSocket::$ssl_cert and WebSocket::$ssl_key configuration settings are compulsory if the wiki connection uses SSL, and the extension will raise a fatal error if they're not present during an SSL connection.

The Perl daemon listens insecurely on the specified port, and listens with SSL on port + 1. The PHP and JavaScript send methods determine whether SSL should be used and what port should be used, but scripts running outside the web environment can choose whichever method they like. The are a couple of client sample scripts to send messages both insecurely and then using SSL to the daemon for development and testing purposes, client.pl and client.php.

Examples

The AjaxComments extension has been upgraded to use a WebSocket instead of Ajax polling if this extension is installed with it. The modifications that were made to the extension to enable WebSocket support can be seen here.

See also