diff --git a/src/server/portal/portal.py b/src/server/portal/portal.py index 366da88009..8c4a812199 100644 --- a/src/server/portal/portal.py +++ b/src/server/portal/portal.py @@ -42,20 +42,21 @@ TELNET_PORTS = settings.TELNET_PORTS SSL_PORTS = settings.SSL_PORTS SSH_PORTS = settings.SSH_PORTS WEBSERVER_PORTS = settings.WEBSERVER_PORTS -WEBSOCKET_PORTS = settings.WEBSOCKET_PORTS +WEBSOCKET_CLIENT_PORT = settings.WEBSOCKET_CLIENT_PORT TELNET_INTERFACES = settings.TELNET_INTERFACES SSL_INTERFACES = settings.SSL_INTERFACES SSH_INTERFACES = settings.SSH_INTERFACES WEBSERVER_INTERFACES = settings.WEBSERVER_INTERFACES -WEBSOCKET_INTERFACES = settings.WEBSOCKET_INTERFACES +WEBSOCKET_CLIENT_INTERFACE = settings.WEBSOCKET_CLIENT_INTERFACE +WEBSOCKET_CLIENT_URL = settings.WEBSOCKET_CLIENT_URL TELNET_ENABLED = settings.TELNET_ENABLED and TELNET_PORTS and TELNET_INTERFACES SSL_ENABLED = settings.SSL_ENABLED and SSL_PORTS and SSL_INTERFACES SSH_ENABLED = settings.SSH_ENABLED and SSH_PORTS and SSH_INTERFACES WEBSERVER_ENABLED = settings.WEBSERVER_ENABLED and WEBSERVER_PORTS and WEBSERVER_INTERFACES WEBCLIENT_ENABLED = settings.WEBCLIENT_ENABLED -WEBSOCKET_ENABLED = settings.WEBSOCKET_ENABLED and WEBSOCKET_PORTS and WEBSOCKET_INTERFACES +WEBSOCKET_CLIENT_ENABLED = settings.WEBSOCKET_CLIENT_ENABLED and WEBSOCKET_CLIENT_PORT and WEBSOCKET_CLIENT_INTERFACE AMP_HOST = settings.AMP_HOST AMP_PORT = settings.AMP_PORT @@ -257,16 +258,31 @@ if WEBSERVER_ENABLED: if WEBCLIENT_ENABLED: # create ajax client processes at /webclientdata from src.server.portal.webclient import WebClient + webclient = WebClient() webclient.sessionhandler = PORTAL_SESSIONS web_root.putChild("webclientdata", webclient) - webclientstr = "/client" + webclientstr = "\n + client (ajax only)" - #from src.server.portal import websocket - #factory = protocol.ServerFactory() - #websocketclient = websocket.WebSocketProtocol() - #websocketclient.sessionhandler = PORTAL_SESSIONS - #web_root.putChild("websocket", websocketclient) + if WEBSOCKET_CLIENT_ENABLED: + # start websocket client port for the webclient + from src.server.portal import websocket_client + from src.utils.txws import WebSocketFactory + + interface = WEBSOCKET_CLIENT_INTERFACE + port = WEBSOCKET_CLIENT_PORT + ifacestr = "" + if interface not in ('0.0.0.0', '::'): + ifacestr = "-%s" % interface + pstring = "%s:%s" % (ifacestr, port) + factory = protocol.ServerFactory() + factory.protocol = websocket_client.WebSocketClient + factory.sessionhandler = PORTAL_SESSIONS + websocket_service = internet.TCPServer(port, WebSocketFactory(factory), interface=interface) + websocket_service.setName('EvenniaWebSocket%s' % pstring) + PORTAL.services.addService(websocket_service) + + webclientstr = webclientstr[:-11] + "(%s:%s)" % (WEBSOCKET_CLIENT_URL, port) web_root = server.Site(web_root, logPath=settings.HTTP_LOG_FILE) proxy_service = internet.TCPServer(proxyport, @@ -274,30 +290,8 @@ if WEBSERVER_ENABLED: interface=interface) proxy_service.setName('EvenniaWebProxy%s' % pstring) PORTAL.services.addService(proxy_service) - print " webproxy%s%s:%s (<-> %s)" % (webclientstr, ifacestr, proxyport, serverport) + print " webproxy%s:%s (<-> %s)%s" % (ifacestr, proxyport, serverport, webclientstr) -if WEBSOCKET_ENABLED: - # websocket support is experimental! - - # start websocket ports for real-time web communication - - from src.server.portal import websocket - from src.utils.txws import WebSocketFactory - - for interface in WEBSOCKET_INTERFACES: - ifacestr = "" - if interface not in ('0.0.0.0', '::') or len(WEBSOCKET_INTERFACES) > 1: - ifacestr = "-%s" % interface - for port in WEBSOCKET_PORTS: - pstring = "%s:%s" % (ifacestr, port) - factory = protocol.ServerFactory() - factory.protocol = websocket.WebSocketProtocol - factory.sessionhandler = PORTAL_SESSIONS - websocket_service = internet.TCPServer(port, WebSocketFactory(factory), interface=interface) - websocket_service.setName('EvenniaWebSocket%s' % pstring) - PORTAL.services.addService(websocket_service) - - print ' websocket%s: %s' % (ifacestr, port) for plugin_module in PORTAL_SERVICES_PLUGIN_MODULES: # external plugin services to start diff --git a/src/server/portal/websocket.py b/src/server/portal/websocket_client.py similarity index 89% rename from src/server/portal/websocket.py rename to src/server/portal/websocket_client.py index 7060e79249..2fd8c158f9 100644 --- a/src/server/portal/websocket.py +++ b/src/server/portal/websocket_client.py @@ -1,8 +1,9 @@ """ -Websockets Protocol +Websocket-webclient -This implements WebSockets (http://en.wikipedia.org/wiki/WebSocket) -by use of the txws implementation (https://github.com/MostAwesomeDude/txWS). +This implements a webclient with WebSockets (http://en.wikipedia.org/wiki/WebSocket) +by use of the txws implementation (https://github.com/MostAwesomeDude/txWS). It is +used together with src/web/media/javascript/evennia_websocket_webclient.js. Thanks to Ricard Pillosu whose Evennia plugin inspired this module. @@ -10,13 +11,13 @@ Communication over the websocket interface is done with normal text communication. A special case is OOB-style communication; to do this the client must send data on the following form: - OOB{oobfunc:[[args], {kwargs}], ...} + OOB{"func1":[args], "func2":[args], ...} -where the tuple/list is sent json-encoded. The initial OOB-prefix +where the dict is JSON encoded. The initial OOB-prefix is used to identify this type of communication, all other data is considered plain text (command input). -Example of call from javascript client: +Example of call from a javascript client: websocket = new WeSocket("ws://localhost:8021") var msg1 = "WebSocket Test" @@ -33,9 +34,10 @@ from src.utils.logger import log_trace from src.utils.utils import to_str from src.utils.text2html import parse_html -class WebSocketProtocol(Protocol, Session): + +class WebSocketClient(Protocol, Session): """ - This is called when the connection is first established + Implements the server-side of the Websocket connection. """ def connectionMade(self): diff --git a/src/settings_default.py b/src/settings_default.py index f03b3544a0..0c470fd3a7 100644 --- a/src/settings_default.py +++ b/src/settings_default.py @@ -58,18 +58,22 @@ UPSTREAM_IPS = ['127.0.0.1'] # with server load. Set the minimum and maximum number of threads it # may use as (min, max) (must be > 0) WEBSERVER_THREADPOOL_LIMITS = (1, 20) -# Start the evennia ajax client on /webclient -# (the webserver must also be running) +# Start the evennia webclient. This requires the webserver to be running and +# offers the fallback ajax-based webclient backbone for browsers not supporting +# the websocket one. WEBCLIENT_ENABLED = True -# Activate Websocket support. If this is on, the default webclient will use this -# before going for the ajax implementation -WEBSOCKET_ENABLED = True -# Ports to use for Websockets. If this is changed, you must also update -# src/web/media/javascript/evennia_websocket_webclient.js to match. -WEBSOCKET_PORTS = [8001] +# Activate Websocket support for modern browsers. If this is on, the +# default webclient will use this and only use the ajax version of the browser +# is too old to support websockets. Requires WEBCLIENT_ENABLED. +WEBSOCKET_CLIENT_ENABLED = True +# Server-side websocket port to open for the webclient. +WEBSOCKET_CLIENT_PORT = 8001 # Interface addresses to listen to. If 0.0.0.0, listen to all. Use :: for IPv6. -WEBSOCKET_INTERFACES = ['0.0.0.0'] -# Activate SSH protocol (SecureShell) +WEBSOCKET_CLIENT_INTERFACE = '0.0.0.0' +# Actual URL for webclient component to reach the websocket. The first +# port number in the WEBSOCKET_PORTS list will be automatically appended. +WEBSOCKET_CLIENT_URL = "ws://localhost" +# Activate SSH protocol communication (SecureShell) SSH_ENABLED = False # Ports to use for SSH SSH_PORTS = [8022] diff --git a/src/web/media/javascript/evennia_websocket_webclient.js b/src/web/media/javascript/evennia_websocket_webclient.js index 2b5e46f554..a5ba1d156a 100644 --- a/src/web/media/javascript/evennia_websocket_webclient.js +++ b/src/web/media/javascript/evennia_websocket_webclient.js @@ -2,30 +2,39 @@ Evennia websocket webclient (javascript component) -The client is composed of several parts: - templates/webclient.html - the main page - webclient/views.py - the django view serving the template (based on urls.py pattern) - src/server/portal/websockets.py - the server component talking to the client +The client is composed of two parts: + src/server/portal/websocket_client.py - the portal-side component this file - the javascript component handling dynamic content -This implements an mud client for use with Evennia, using jQuery -for simplicity. - -messages sent to the client is one of three modes: - OOB(func,(args), func,(args), ...) - OOB command executions - text - any other text is considered a normal text to echo +messages sent to the client is one of two modes: + OOB("func1",args, "func2",args, ...) - OOB command executions, this will + call unique javascript functions + func1(args), func2(args) etc. + text - any other text is considered a normal text output in the main output window. */ -// jQuery must be imported by the calling html page before this script -// There are plenty of help on using the jQuery library on http://jquery.com/ +// If on, allows client user to send OOB messages to server by +// prepending with ##OOB{}, for example ##OOB{"echo":[1,2,3,4]} +var OOB_debug = true -// Server communications -// Set this to the value matching settings.WEBSOCKET_PORTS -var wsurl = "ws://localhost:8001"; +// Custom OOB functions +// functions defined here can be called by name by the server. For +// example the OOB{"echo":arguments} will trigger a function named +// echo(arguments). + + +function echo(message) { + // example echo function. + doShow("out", "ECHO return: " + message) } + + + + +// Webclient code function webclient_init(){ - // initializing the client once the html page has loaded + // called when client is just initializing websocket = new WebSocket(wsurl); websocket.onopen = function(evt) { onOpen(evt) }; websocket.onclose = function(evt) { onClose(evt) }; @@ -34,91 +43,100 @@ function webclient_init(){ } function onOpen(evt) { - // client is just connecting + // called when client is first connecting $("#connecting").remove(); // remove the "connecting ..." message - msg_display("sys", "Using websockets - connected to " + wsurl + ".") + doShow("sys", "Using websockets - connected to " + wsurl + ".") setTimeout(function () { - $("#playercount").fadeOut('slow', webclient_set_sizes); + $("#playercount").fadeOut('slow', doSetSizes); }, 10000); } function onClose(evt) { - // client is closing + // called when client is closing CLIENT_HASH = 0; alert("Mud client connection was closed cleanly."); } function onMessage(evt) { - // outgoing message from server + // called when the Evennia is sending data to client var inmsg = evt.data if (inmsg.length > 3 && inmsg.substr(0, 3) == "OOB") { // dynamically call oob methods, if available try {var oobarray = JSON.parse(inmsg.slice(3));} // everything after OOB } catch(err) { // not JSON packed - a normal text - msg_display('out', err + " " + inmsg); + doShow('out', err + " " + inmsg); return; } for (var ind in oobarray) { try { window[oobarray[ind][0]](oobarray[ind][1]) } - catch(err) { msg_display("err", "Could not execute OOB function " + oobtuple[0] + "(" + oobtuple[1] + ")!") } + catch(err) { doShow("err", "Could not execute OOB function " + oobtuple[0] + "(" + oobtuple[1] + ")!") } } } else { // normal message - msg_display('out', inmsg); } + doShow('out', inmsg); } } function onError(evt) { - // client error message - msg_display('err', "Error: Server returned an error. Try reloading the page."); + // called on a server error + doShow('err', "Error: Server returned an error. Try reloading the page."); } function doSend(){ - // sending data from client to server + // relays data from client to Evennia. + // If OOB_debug is set, allows OOB test data on the + // form ##OOB{func:args} outmsg = $("#inputfield").val(); history_add(outmsg); HISTORY_POS = 0; $('#inputform')[0].reset(); // clear input field - if (outmsg.length > 4 && outmsg.substr(0, 5) == "##OOB") { + if (OOB_debug && outmsg.length > 4 && outmsg.substr(0, 5) == "##OOB") { // test OOB messaging doOOB(JSON.parse(outmsg.slice(5))); } else { + // normal output websocket.send(outmsg); } } function doOOB(oobdict){ - // Handle OOB communication from client side - // Takes a dict on form {funcname:[args], funcname: [args], ... ] - msg_display("out", "into doOOB: " + oobdict) - msg_display("out", "stringify: " + JSON.stringify(oobdict)) + // Send OOB data from client to Evennia. + // Takes input on form {funcname:[args], funcname: [args], ... } var oobmsg = JSON.stringify(oobdict); websocket.send("OOB" + oobmsg); } - -// -// OOB functions -// - -function echo(message) { - msg_display("out", "ECHO return: " + message) } - -// -// Display messages - -function msg_display(type, msg){ - // Add a div to the message window. +function doShow(type, msg){ + // Add msg to the main output window. // type gives the class of div to use. + // The default types are + // "out" (normal output) or "err" (red error message) $("#messagewindow").append( "
"+ msg +"
"); // scroll message window to bottom $('#messagewindow').animate({scrollTop: $('#messagewindow')[0].scrollHeight}); } -// Input history mechanism + +function doSetSizes() { + // Sets the size of the message window + var win_h = $(document).height(); + //var win_w = $('#wrapper').width(); + var inp_h = $('#inputform').outerHeight(true); + //var inp_w = $('#inputsend').outerWidth(true); + + $("#messagewindow").css({'height': win_h - inp_h - 1}); + //$("#inputfield").css({'width': win_w - inp_w - 20}); +} + + +// +// Input code +// + +// Input history var HISTORY_MAX_LENGTH = 21 var HISTORY = new Array(); @@ -223,6 +241,8 @@ $.fn.appendCaret = function() { }); }; +// Input jQuery callbacks + $(document).keydown( function(event) { // Get the pressed key (normalized by jQuery) var code = event.which, @@ -233,7 +253,7 @@ $(document).keydown( function(event) { // Special keys recognized by client - //msg_display("out", "key code pressed: " + code); // debug + //doShow("out", "key code pressed: " + code); // debug if (code == 13) { // Enter Key doSend(); @@ -252,36 +272,24 @@ $(document).keydown( function(event) { // handler to avoid double-clicks until the ajax request finishes //$("#inputsend").one("click", webclient_input) -function webclient_set_sizes() { - // Sets the size of the message window - var win_h = $(document).height(); - //var win_w = $('#wrapper').width(); - var inp_h = $('#inputform').outerHeight(true); - //var inp_w = $('#inputsend').outerWidth(true); +// Callback function - called when the browser window resizes +$(window).resize(doSetSizes); - $("#messagewindow").css({'height': win_h - inp_h - 1}); - //$("#inputfield").css({'width': win_w - inp_w - 20}); -} - - -// Callback function - called when page has finished loading (gets things going) +// Callback function - called when page is closed or moved away from. +//$(window).bind("beforeunload", webclient_close); +// +// Callback function - called when page has finished loading (kicks the client into gear) $(document).ready(function(){ // remove the "no javascript" warning, since we obviously have javascript $('#noscript').remove(); // set sizes of elements and reposition them - webclient_set_sizes(); + doSetSizes(); // a small timeout to stop 'loading' indicator in Chrome setTimeout(function () { webclient_init(); }, 500); // set an idle timer to avoid proxy servers to time out on us (every 3 minutes) setInterval(function() { - webclient_input("idle", true); + websocket.send("idle"); }, 60000*3); }); - -// Callback function - called when the browser window resizes -$(window).resize(webclient_set_sizes); - -// Callback function - called when page is closed or moved away from. -//$(window).bind("beforeunload", webclient_close); diff --git a/src/web/templates/prosimii/webclient.html b/src/web/templates/prosimii/webclient.html index ba7769af23..f36839e708 100644 --- a/src/web/templates/prosimii/webclient.html +++ b/src/web/templates/prosimii/webclient.html @@ -19,9 +19,10 @@ {% else %} diff --git a/src/web/utils/general_context.py b/src/web/utils/general_context.py index 421bf9587a..b901e856e3 100644 --- a/src/web/utils/general_context.py +++ b/src/web/utils/general_context.py @@ -6,7 +6,6 @@ # tuple. # -from django.db import models from django.conf import settings from src.utils.utils import get_evennia_version @@ -30,6 +29,9 @@ WEBSITE = ['Flatpages', 'News', 'Sites'] # The main context processor function +WEBCLIENT_ENABLED = settings.WEBCLIENT_ENABLED +WEBSOCKET_CLIENT_ENABLED = settings.WEBSOCKET_CLIENT_ENABLED +WSURL = "%s:%s" % (settings.WEBSOCKET_CLIENT_URL, settings.WEBSOCKET_CLIENT_PORT) def general_context(request): """ @@ -44,6 +46,7 @@ def general_context(request): 'evennia_setupapps': GAME_SETUP, 'evennia_connectapps': CONNECTIONS, 'evennia_websiteapps':WEBSITE, - "webclient_enabled" : settings.WEBCLIENT_ENABLED, - "websocket_enabled" : settings.WEBSOCKET_ENABLED + "webclient_enabled" : WEBCLIENT_ENABLED, + "websocket_enabled" : WEBSOCKET_CLIENT_ENABLED, + "websocket_url" : WSURL }