diff options
-rw-r--r-- | webchat/index.js | 152 | ||||
-rw-r--r-- | webchat/proto_spec | 62 | ||||
-rw-r--r-- | webchat/public/client.js | 123 | ||||
-rw-r--r-- | webchat/public/functions.js | 54 | ||||
-rw-r--r-- | webchat/public/krebs.png | bin | 956 -> 2583 bytes | |||
-rw-r--r-- | webchat/public/reset.css | 23 | ||||
-rw-r--r-- | webchat/public/rpc.js | 99 | ||||
-rw-r--r-- | webchat/public/sockjs_client_transport.js | 25 | ||||
-rw-r--r-- | webchat/sockjs_server_connection_transport.js | 26 |
9 files changed, 432 insertions, 132 deletions
diff --git a/webchat/index.js b/webchat/index.js index 8ef737fc..9569fc80 100644 --- a/webchat/index.js +++ b/webchat/index.js @@ -1,20 +1,30 @@ +'use strict'; var fs = require('fs'); var http = require('https'); var sockjs = require('sockjs'); var connect = require('connect'); var irc = require('irc'); -var Clients = []; +var make_sockjs_server_connection_transport = require('./sockjs_server_connection_transport.js') +var RPC = require('./public/rpc.js'); +var irc_nicks = [] -Clients.broadcast = function(object) { //broadcast to all clients - Clients.forEach(function(client) { - client.write(JSON.stringify(object)); - }); +function pluck (key) { + return function (object) { + return object[key] + } +} + +var clients = []; +clients.broadcast = function (method, params) { + clients.map(pluck('rpc')).forEach(function (rpc) { + rpc.send(method, params) + }) } var irc_reconnect = function() { //reconnt to irc - console.log("reconnecting due to pingtimeout"); - irc_client.disconnect(); - irc_client.connect(); + console.log("would reconnect now") +// irc_client.disconnect() +// irc_client.connect() } var pingTimeoutDelay = 3*60*1000 @@ -27,7 +37,7 @@ var irc_client = new irc.Client('irc.freenode.net', 'kweb', { //create irc_clien userName: 'kweb', //todo: read from local_config realName: 'kweb', //todo: read from local_config password: fs.readFileSync(__dirname+'/local_config/irc.key').toString(), - debug: false, + debug: true, showErrors: true, floodProtection: true, port: 6697, @@ -36,7 +46,6 @@ var irc_client = new irc.Client('irc.freenode.net', 'kweb', { //create irc_clien stripColors: true }); - irc_client.on('ping', function(server) { //restart timer on server ping console.log("got ping from server, renewing timeout for automatic reconnect"); clearTimeout(lastping); @@ -45,60 +54,97 @@ irc_client.on('ping', function(server) { //restart timer on server ping irc_client.on('message#krebs', function(from, message) { console.log({ from: from, message: message }); - Clients.broadcast({ from: from, message: message }); //broadcast irc messages to all connected clients + clients.broadcast('msg', {nick: from, msg: message}) clearTimeout(lastping); }); -var web_serv_options = { //certificates for https - key: fs.readFileSync(__dirname+'/local_config/server_npw.key'), - cert: fs.readFileSync(__dirname+'/local_config/server.crt'), -}; +irc_client.on('names#krebs', function(nicks) { + Object.keys(nicks).forEach(function (nick) { + irc_nicks.push(nick) + clients.broadcast('join', {type: 'irc', nick: nick}) + }) +}) -var echo = sockjs.createServer(); -echo.on('connection', function(conn) { - var origin = conn.remoteAddress; - Clients.push(conn); - Clients.broadcast({from: 'system', message: origin + ' has joined'}) -// irc_client.say("#krebs", origin + ' has joined'); - conn.write(JSON.stringify({from: 'system', message: 'hello'})) //welcome message - conn.on('data', function(data) { - console.log('data:',data); - try { - var object = JSON.parse(data); - if (object.message.length > 0) { //if message is not empty - if (/^\/nick\s+(.+)$/.test(object.message)) { //if nick is send use nick instead of ip - object.from = origin; - } else if (typeof object.nick === 'string') { - object.from = object.nick; - } else { - object.from = origin; - }; - console.log(object.message); - irc_client.say("#krebs", object.from + ' → ' + object.message); - Clients.broadcast(object); - } - - } catch (error) { - console.log(error); - } - }); - conn.on('close', function() { //propagate if client quits the page - Clients.splice(Clients.indexOf(conn)); - Clients.broadcast({from: 'system', message: origin + ' has quit'}) -// irc_client.say("#krebs", origin + ' has quit'); -}); +irc_client.on('join#krebs', function(nick, msg) { + if (nick !== 'kweb'){ + irc_nicks.push(nick) + clients.broadcast('join', {type: 'irc', nick: nick}) + } +}) + +irc_client.on('part#krebs', function(nick, rs, msg) { + clients.broadcast('part', {type: 'irc', nick: nick}) }); +irc_client.on('error', function (error) { + irc_nicks.forEach( function(nick) { + client.rpc.send('part', {type: 'irc', nick: nick}) + irc_nicks.splice(irc_nicks.indexOf(nick)) + }) + console.log('irc-client error', error) +}) + +var echo = sockjs.createServer(); + +var total_clients_ever_connected = 0 + +echo.on('connection', function (connection) { + var client = {} + client.rpc = new RPC(make_sockjs_server_connection_transport(connection)) + client.nick = 'anon'+(++total_clients_ever_connected) + client.rpc.send('your_nick', {nick: client.nick}) + client.rpc.register('msg', {msg: 'string'}, function (params, callback) { + callback(null) + clients.broadcast('msg', {type: 'web', nick: client.nick, msg: params.msg}) + irc_client.say('#krebs', client.nick + ' → ' + params.msg) + }) + client.rpc.register('nick', {nick: 'string'}, function (params, callback) { + if (!!~clients.map(pluck('nick')).indexOf(params.nick)) { + callback('name already taken') + } else if (/^anon[0-9]+$/.test(params.nick)) { + callback('bad nick') + } else { + var oldnick = client.nick + client.nick = params.nick + callback(null) + clients.broadcast('nick', {type: 'web', newnick: client.nick, oldnick: oldnick}) + irc_client.say('#krebs', oldnick + ' is now known as ' + client.nick) + } + }) + connection.on('close', function() { //propagate if client quits the page + clients.splice(clients.indexOf(client)); + clients.broadcast('part', {type: 'web', nick: client.nick}) + irc_client.say('#krebs', client.nick + ' has parted') + }) + //send the irc nicklist to the new joined client + irc_nicks.forEach( function(nick) { + client.rpc.send('join', {type: 'irc', nick: nick}) + }) + //send nicklist to newly joined client + clients.map(pluck('nick')).forEach(function (nick) { + client.rpc.send('join', {type: 'web', nick: nick}) + }) + //add new client to list + clients.push(client) + //send all including the new client the join + clients.broadcast('join', {type: 'web', nick: client.nick}) + //send join to irc + irc_client.say('#krebs', client.nick + ' has joined') +}) var app = connect() .use(connect.logger('dev')) .use(connect.static(__dirname+'/public')) .use( function (req, res) { res.writeHead(200, {'Content-Type': 'text/html'}); - page_template='<!doctype html>\n'; + var page_template='<!doctype html>\n'; page_template+='<link rel="stylesheet" type="text/css" href="reset.css">\n'; page_template+='<script src="sockjs-0.3.min.js"></script>\n'; page_template+='<script src="jquery-2.0.3.min.js"></script>\n'; + page_template+='<script src="commands.js"></script>\n'; + page_template+='<script src="functions.js"></script>\n'; + page_template+='<script src="sockjs_client_transport.js"></script>\n'; + page_template+='<script src="rpc.js"></script>\n'; page_template+='<script src="client.js"></script>\n'; page_template+='<div id="bg">'; page_template+='<div id="chatter">'; @@ -106,7 +152,8 @@ var app = connect() page_template+='hello, this is the official krebs support:<br>\n'; page_template+='<table id="chatbox"><tr id="foot"><td id="time"></td><td id="nick" class="chat_from"></td><td><input type="text" id="input"></td></tr></table>\n'; page_template+='</div>'; - page_template+='<div id="sideboard"><div id="links">'; + page_template+='<div id="sideboard"><div id=nicklist></div>'; + page_template+='<div id="links">'; page_template+='<a href="http://gold.krebsco.de/">krebsgold browser plugin</a><br>'; page_template+='<a href="http://ire:1027/dashboard/">ire: Retiolum Dashboard</a><br>'; page_template+='<a href="http://pigstarter/">pigstarter: network graphs</a><br>'; @@ -114,6 +161,11 @@ var app = connect() res.end(page_template); }) + +var web_serv_options = { //certificates for https + key: fs.readFileSync(__dirname+'/local_config/server_npw.key'), + cert: fs.readFileSync(__dirname+'/local_config/server.crt'), +}; var server = http.createServer(web_serv_options, app); echo.installHandlers(server, {prefix:'/echo'}); server.listen(1337, '0.0.0.0'); diff --git a/webchat/proto_spec b/webchat/proto_spec new file mode 100644 index 00000000..fffce165 --- /dev/null +++ b/webchat/proto_spec @@ -0,0 +1,62 @@ +server -> client: +#old +type: 'message' | 'join' | 'quit' | 'nicklist' | 'nickchange' | 'usererror' +nick: the clients nickname ('message','nickchange') +newnick: new nick after nickchange ('nickchange') +from: the clients ip ('message','quit','join') +message: the data send ('message', 'nicklist','usererror' + + +#new +type: 'irc_msg' | 'irc_join' | 'irc_quit' | 'irc_nickchange' | 'irc_client_connect' | 'irc_client_disconnect' | 'web_welcome' |'web_msg' | 'web_join' | 'web_quit' | 'web_nickchange' | 'usererror' +params:{ nick:, oldnick:, nicklist:, msg:, errormsg: } + +'irc_msg': nick, msg +'irc_join': nick +'irc_quit': nick +'irc_nickchange': oldnick, nick +'kweb_irc_connect': nicklist +'kweb_irc_disconnect': -- +'web_welcome': msg, nicklist, nick +'web_msg': nick, msg +'web_join': nick +'web_quit': nick +'web_nickchange': oldnick, nick +'usererror': msg + + +client -> server +#old +method: 'say', 'nick' +params:{ msg:, nick: } + +'say': msg +'nick': nick + + +##############JSON RPC################ +server->client: +{method: 'say', params: {msg: msg}, id: id} +-> {result: {ok: ok}, error {error muted?}, id: id} + +{method: 'nick', params: {nick: nick}, id: id} +-> {result: {nick: nick}, error: {error name taken?/reserved/not allowed}, id: id} + + +client->server: +broadcast: + {method: 'irc_msg', params: {nick: nick, msg: msg}, id: 0} #notification + {method: 'irc_join', params: {nick: nick}, id: 0} #notification + {method: 'irc_quit', params: {nick: nick}, id: 0} #notification + {method: 'irc_nickchange', params: {nick: nick, oldnick: oldnick}, id: 0} #notification + {method: 'kweb_irc_connect', params: {nicklist: nicklist}, id: 0} #notification + {method: 'kweb_irc_disconnect', params: {}, id: 0} #notification + {method: 'web_msg', params: {nick: nick, msg: msg}, id: 0} #notification + {method: 'web_join', params: {nick: nick}, id: 0} #notification + {method: 'web_quit', params: {nick: nick}, id: 0} #notification + {method: 'web_nickchange', params: {nick: nick, oldnick: oldnick}, id: 0} #notification + +unicast: + {method: 'coi', params: {}, id: id} + -> {result: {result: {nick: nick, addr: addr}, error: {connection error?}, id: id} + {method: 'usererror', params: {msg: 'error type'}, id: 0} #notification diff --git a/webchat/public/client.js b/webchat/public/client.js index ca71b537..54ccfe34 100644 --- a/webchat/public/client.js +++ b/webchat/public/client.js @@ -1,34 +1,50 @@ -function replaceURLWithHTMLLinks (text) { - var exp = /(\b(https?|ftp|file):\/\/[-A-Z0-9+&@#\/%?=~_|!:,.;]*[-A-Z0-9+&@#\/%=~_|])/ig; - return text.replace(exp,"<a href='$1'>$1</a>"); -} -function setMaybeNick (input) { - var match = /^\/nick\s+(.+)$/.exec(input); - if (match) { - nick = match[1]; - $('#nick').html(nick); - } -} +var settings = {} +settings.sock = new SockJS('/echo'); +settings.waiting_callbacks = {} -function getCurTime () { - date = new Date; - h = date.getHours(); - if(h<10) - { - h = "0"+h; - } - m = date.getMinutes(); - if(m<10) - { - m = "0"+m; - } - s = date.getSeconds(); - if(s<10) - { - s = "0"+s; +var transport = make_sockjs_client_transport(settings.sock) +settings.rpc = new RPC(transport) + +settings.rpc.register('msg', {type: 'string', nick: 'string', msg: 'string'}, function(params, callback) { + var safe_message = $('<div/>').text(params.msg).html(); + safe_message = replaceURLWithHTMLLinks(safe_message); + var safe_from = $('<div/>').text(params.nick).html(); + chatboxAppend(safe_from, safe_message, 'web_msg') + return callback(null) +}) +settings.rpc.register('nick', {type: 'string', newnick: 'string', oldnick: 'string'}, function(params, callback) { + var safe_oldnick = $('<div/>').text(params.oldnick).html(); + var safe_newnick = $('<div/>').text(params.newnick).html(); + var safe_type = $('<div/>').text(params.type).html(); + if (safe_oldnick === settings.nick){ + settings.nick = safe_newnick + $('#nick').html(settings.nick) } - return ''+h+':'+m+':'+s; -}; + $(getNicklistElement(safe_oldnick,safe_type)).remove(); + $('#nicklist').append('<div class="'+safe_type+'_name">' + safe_newnick + '</div>') ; + chatboxAppend(safe_oldnick, 'is now known as ' + safe_newnick, 'nick'); + return callback(null) +}) +settings.rpc.register('your_nick', {nick: 'string'}, function(params, callback) { + var safe_nick = $('<div/>').text(params.nick).html(); + settings.nick = safe_nick + $('#nick').html(settings.nick) + return callback(null) +}) +settings.rpc.register('join', {type: 'string', nick: 'string'}, function(params, callback) { + var safe_nick = $('<div/>').text(params.nick).html(); + var safe_type = $('<div/>').text(params.type).html(); + $('#nicklist').append('<div class="'+safe_type+'_name">' + safe_nick + '</div>') ; + chatboxAppend(safe_nick, 'has joined'); + return callback(null) +}) +settings.rpc.register('part', {type: 'string', nick: 'string'}, function(params, callback) { + var safe_nick = $('<div/>').text(params.nick).html(); + var safe_type = $('<div/>').text(params.type).html(); + $(getNicklistElement(safe_nick,safe_type)).remove(); + chatboxAppend(safe_nick, 'has parted'); + return callback(null) +}) $(function updateTime () { $('#time').html(getCurTime()); @@ -36,59 +52,18 @@ $(function updateTime () { return true; }); -var nick; - -$(function connect() { - sock = new SockJS('/echo'); - - sock.onopen = function() { - console.log('open'); - sock.send('open'); - }; - sock.onmessage = function(e) { - console.log('message', e.data); - try { - var object = JSON.parse(e.data); - console.log(object.message); - var safe_message = $('<div/>').text(object.message).html(); - safe_message = replaceURLWithHTMLLinks(safe_message); - var safe_from = $('<div/>').text(object.from).html(); - $('<tr><td class="chat_date">'+getCurTime()+'</td><td class="chat_from">'+safe_from+'</td><td class="chat_msg">'+safe_message+'</td></tr>').insertBefore('#foot'); - var elem = document.getElementById('chatter'); - elem.scrollTop = elem.scrollHeight; - - } catch (error) { - console.log(error); - } - }; - sock.onclose = function(event) { - console.log('close'); - switch (event.code) { - case 1006: //abnormal closure - return setTimeout(connect, 1000); - }; - }; - -}); $(function() { $('#input').keydown(function(e) { if (e.keyCode === 13) { e.preventDefault(); e.stopPropagation(); e.stopImmediatePropagation(); - setMaybeNick($('#input').val()); - var sendObj = { - message: $('#input').val(), - }; - - if (typeof nick === 'string') { - sendObj.nick = nick; - }; + var input = ($('#input').val()); + $('#input').val('') - sock.send(JSON.stringify(sendObj)); - $('#input').val(''); - return; + var command = inputParser(input) + return (commands[command.method] || commands.badcommand)(settings, command.params) } }); diff --git a/webchat/public/functions.js b/webchat/public/functions.js new file mode 100644 index 00000000..781fafce --- /dev/null +++ b/webchat/public/functions.js @@ -0,0 +1,54 @@ +function inputParser (str) { + var match = /^\/([a-z]+)(?:\s+(.*\S))?\s*$/.exec(str) + if (match) { + return { method: match[1], params: match[2] } + } else { + return { method: 'msg', params: str } + } +} + +function replaceURLWithHTMLLinks (text) { + var exp = /(\b(https?|ftp|file):\/\/[-A-Z0-9+&@#\/%?=~_|!:,.;]*[-A-Z0-9+&@#\/%=~_|])/ig; + return text.replace(exp,"<a class=chat_link href='$1'>$1</a>"); +} + +function getNicklistElement(name, type) { + var el; + $('.'+type+'_name').each(function (i,e) { + if (e.innerHTML === name) { + if (typeof el !== 'undefined') { + throw new Error('duplicate name: ' + name); + }; + el = e; + }; + }); + return el; +} + +function chatboxAppend (chat_from, chat_msg, type) { + type = type||'msg' + $('<tr><td class="date '+type+'_date">'+getCurTime()+'</td><td class="from '+type+'_from">'+chat_from+'</td><td class="msg '+type+'_msg">'+chat_msg+'</td></tr>').insertBefore('#foot'); + + var elem = document.getElementById('chatter'); + elem.scrollTop = elem.scrollHeight; +}; + +function getCurTime () { + date = new Date; + h = date.getHours(); + if(h<10) + { + h = "0"+h; + } + m = date.getMinutes(); + if(m<10) + { + m = "0"+m; + } + s = date.getSeconds(); + if(s<10) + { + s = "0"+s; + } + return ''+h+':'+m+':'+s; +}; diff --git a/webchat/public/krebs.png b/webchat/public/krebs.png Binary files differindex 263d5b1c..5762e7f4 100644 --- a/webchat/public/krebs.png +++ b/webchat/public/krebs.png diff --git a/webchat/public/reset.css b/webchat/public/reset.css index 65f68058..d369bc86 100644 --- a/webchat/public/reset.css +++ b/webchat/public/reset.css @@ -21,7 +21,6 @@ time, mark, audio, video { border: 0; font-size: 100%; font: inherit; - font-family: monospace; vertical-align: baseline; } /* HTML5 display-role reset for older browsers */ @@ -33,6 +32,7 @@ body { line-height: 1; background-color: black; color: white; + font-family: monospace; } ol, ul { list-style: none; @@ -54,17 +54,18 @@ q:before, q:after { } #input{ width: 100%; - background-color: #555555; + background-color: #221111; border: 1px solid black; color: white; } -.chat_from { +.from { color:grey; font-weight: bold; text-align: right; font-size:12px; + white-space: nowrap; } -.chat_from:after { +.from:after { content: ":"; padding-right: 6px; } @@ -87,16 +88,16 @@ q:before, q:after { } .chat_date,.chat_from,.chat_msg{ } -.chat_msg{ +.msg{ width: 100%; } a { color: red; } -.chat_date { +.date { color: green; } -.chat_date:after { +.date:after { content: ""; padding-right: 4px; } @@ -116,4 +117,10 @@ a { font-size: 14px; position: absolute; bottom: 5px; -}
\ No newline at end of file +} +.join_msg { + color: #00FF00; +} +.quit_msg { + color: #FF0000; +} diff --git a/webchat/public/rpc.js b/webchat/public/rpc.js new file mode 100644 index 00000000..8e911e1d --- /dev/null +++ b/webchat/public/rpc.js @@ -0,0 +1,99 @@ +try { + module.exports = RPC +} +catch(e){ +} + +function RPC (transport) { + this._id = 0 + this._waiting_callbacks = {} + this._methods = {} + this._transport = transport + + transport.onmessage = this.onmessage.bind(this) +} + +RPC.prototype.register = function (method, params, callback) { + this._methods[method] = callback +} + +RPC.prototype.send = function (method, params, callback) { + var message = { + method: method, + params: params, + } + if (callback) { + var id = ++this._id + this._waiting_callbacks[id] = callback + message.id = id + } + return this._transport.send(message) +} + +function _is_request (message) { + return typeof message.method === 'string' +} + +function _is_response (message) { + return message.hasOwnProperty('result') + || message.hasOwnProperty('error') +} + +RPC.prototype.onmessage = function (message) { + console.log('RPC message:', message) + if (_is_request(message)) { + return this._on_request(message) + } + if (_is_response(message)) { + return this._on_response(message) + } + return this._on_bad_message(message) +} + +RPC.prototype._on_request = function (request) { + var method = this._methods[request.method] || function(){ + console.log('method not found', request.method) + } + var params = request.params + var id = request.id + + var transport = this._transport + + if (typeof id === 'string' || typeof id === 'number' || id === null) { + return method(params, function (error, result) { + var response = { + id: id, + } + if (error) { + response.error = error + } else { + response.result = result + } + console.log('request:', request, '->', response) + return transport.send(response) + }) + } else { + return method(params, function (error, result) { + var response = { + id: id, + } + if (error) { + response.error = error + } else { + response.result = result + } + console.log('notification:', request, '->', response) + }) + } +} + +RPC.prototype._on_response = function (response) { + var result = response.result + var error = response.error + var id = response.id + + var callback = this._waiting_callbacks[id] + delete this._waiting_callbacks[id] + + return callback(result, error) +} diff --git a/webchat/public/sockjs_client_transport.js b/webchat/public/sockjs_client_transport.js new file mode 100644 index 00000000..a7b76af3 --- /dev/null +++ b/webchat/public/sockjs_client_transport.js @@ -0,0 +1,25 @@ + +function make_sockjs_client_transport (sock) { + var transport = {} + + sock.onmessage = function (data) { + console.log('sockjs parse', data) + try { + var message = JSON.parse(data.data) + } catch (error) { + return console.log('error', error) + } + transport.onmessage(message) + } + + transport.send = function (message) { + try { + var data = JSON.stringify(message) + } catch (error) { + return console.log('sockjs transport send error:', error) + } + sock.send(data) + } + + return transport +} diff --git a/webchat/sockjs_server_connection_transport.js b/webchat/sockjs_server_connection_transport.js new file mode 100644 index 00000000..6f68b955 --- /dev/null +++ b/webchat/sockjs_server_connection_transport.js @@ -0,0 +1,26 @@ + +module.exports = function make_sockjs_server_connection_transport (connection) { + var transport = {} + + connection.on('data', function (data) { + try { + var message = JSON.parse(data) + } catch (error) { + return console.log('error', error) + } + transport.onmessage(message) + }) + connection.on('close', function () { + }) + + transport.send = function (message) { + try { + var data = JSON.stringify(message) + } catch (error) { + return console.log('sockjs transport send error:', error) + } + connection.write(data) + } + + return transport +} |