diff --git a/Docker/Dockerfile b/Docker/Dockerfile new file mode 100644 index 0000000..ea5cee9 --- /dev/null +++ b/Docker/Dockerfile @@ -0,0 +1,44 @@ +FROM ubuntu:14.04 + +ENV DEBIAN_FRONTEND noninteractive +ENV HOME /root + +ENV XMPP_NAME Otalk +ENV XMPP_DOMAIN example.com +ENV XMPP_WSS wss://example.com:5281/xmpp-websocket/ +ENV XMPP_MUC chat.example.com +ENV XMPP_STARTUP groupchat/room%40chat.example.com +ENV XMPP_ADMIN admin + +ENV LDAP_BASE dc=example.com +ENV LDAP_DN cn=admin,dc=example.com +ENV LDAP_PWD password +ENV LDAP_GROUP mygroup + +RUN sed -i 's/^#\s*\(deb.*universe\)$/\1/g' /etc/apt/sources.list && \ + sed -i 's/^#\s*\(deb.*multiverse\)$/\1/g' /etc/apt/sources.list && \ + apt-get -y update && \ + dpkg-divert --local --rename --add /sbin/initctl && \ + ln -sf /bin/true /sbin/initctl && \ + dpkg-divert --local --rename --add /usr/bin/ischroot && \ + ln -sf /bin/true /usr/bin/ischroot && \ + apt-get -y upgrade && \ + apt-get install -y vim wget sudo net-tools pwgen unzip openssh-server \ + logrotate supervisor language-pack-en software-properties-common \ + python-software-properties apt-transport-https ca-certificates curl && \ + apt-get clean + +RUN locale-gen en_US && locale-gen en_US.UTF-8 && echo 'LANG="en_US.UTF-8"' > /etc/default/locale + +RUN apt-get update && apt-get install -y --force-yes nodejs git-core libldap2-dev uuid-dev + +RUN apt-get remove -y --force-yes nodejs && apt-get install -y --force-yes nodejs-legacy npm + +RUN git clone git://github.com/digicoop/otalk.git + +RUN cd otalk && npm install + +ADD app /app + +RUN chmod +x /app/start.sh +CMD "/app/start.sh" diff --git a/Docker/app/config/dev_config.json b/Docker/app/config/dev_config.json new file mode 100644 index 0000000..ba831a8 --- /dev/null +++ b/Docker/app/config/dev_config.json @@ -0,0 +1,28 @@ +{ + "isDev": true, + "http": { + "baseUrl": "http://localhost:8000", + "port": 8000, + "key": "./fakekeys/privatekey.pem", + "cert": "./fakekeys/certificate.pem" + }, + "session": { + "secret": "shhhhhh don't tell anyone ok?" + }, + "server": { + "name": "{{XMPP_NAME}}", + "domain": "{{XMPP_DOMAIN}}", + "wss": "{{XMPP_WSS}}", + "muc": "{{XMPP_MUC}}", + "startup": "{{XMPP_STARTUP}}", + "admin": "{{XMPP_ADMIN}}" + }, + "ldap": { + "address": "{{LDAP_HOST}}", + "user": "{{LDAP_DN}}", + "password": "{{LDAP_PWD}}", + "base": "ou=users,{{LDAP_BASE}}", + "filter": "objectClass=person", + "group": "cn={{LDAP_GROUP}},ou=groups,{{LDAP_BASE}}" + } +} diff --git a/Docker/app/stanza.io/index-browser.js b/Docker/app/stanza.io/index-browser.js new file mode 100644 index 0000000..4f6401a --- /dev/null +++ b/Docker/app/stanza.io/index-browser.js @@ -0,0 +1,49 @@ +'use strict'; + +module.exports = function (client) { + // We always need this one first + client.use(require('./disco')); + + client.use(require('./attention')); + client.use(require('./avatar')); + client.use(require('./blocking')); + client.use(require('./bob')); + client.use(require('./bookmarks')); + client.use(require('./carbons')); + client.use(require('./chatstates')); + client.use(require('./command')); + client.use(require('./correction')); + client.use(require('./csi')); + client.use(require('./dataforms')); + client.use(require('./delayed')); + client.use(require('./escaping')); + client.use(require('./extdisco')); + client.use(require('./forwarding')); + client.use(require('./geoloc')); + client.use(require('./hashes')); + client.use(require('./idle')); + client.use(require('./invisible')); + client.use(require('./jidprep')); + //client.use(require('./jingle')); + client.use(require('./json')); + client.use(require('./keepalive')); + client.use(require('./logging')); + client.use(require('./mam')); + client.use(require('./muc')); + client.use(require('./mood')); + client.use(require('./nick')); + client.use(require('./oob')); + client.use(require('./ping')); + client.use(require('./private')); + client.use(require('./psa')); + client.use(require('./pubsub')); + client.use(require('./reach')); + client.use(require('./receipts')); + client.use(require('./register')); + client.use(require('./roster')); + client.use(require('./rtt')); + client.use(require('./shim')); + client.use(require('./time')); + client.use(require('./vcard')); + client.use(require('./version')); +}; diff --git a/Docker/app/stanza.io/muc.js b/Docker/app/stanza.io/muc.js new file mode 100644 index 0000000..7f62ff7 --- /dev/null +++ b/Docker/app/stanza.io/muc.js @@ -0,0 +1,265 @@ +'use strict'; + +var stanza = require('jxt'); +var Message = require('./message'); +var Presence = require('./presence'); +var Iq = require('./iq'); +var DataForm = require('./dataforms').DataForm; +var jxtutil = require('jxt-xmpp-types'); + +var NS = 'http://jabber.org/protocol/muc'; +var USER_NS = NS + '#user'; +var ADMIN_NS = NS + '#admin'; +var OWNER_NS = NS + '#owner'; +var UNIQ_NS = NS + '#unique'; + + +var proxy = function (child, field) { + return { + get: function () { + if (this._extensions[child]) { + return this[child][field]; + } + }, + set: function (value) { + this[child][field] = value; + } + }; +}; + +var UserItem = stanza.define({ + name: '_mucUserItem', + namespace: USER_NS, + element: 'item', + fields: { + affiliation: stanza.attribute('affiliation'), + nick: stanza.attribute('nick'), + jid: jxtutil.jidAttribute('jid'), + role: stanza.attribute('role'), + reason: stanza.subText(USER_NS, 'reason') + } +}); + +var UserActor = stanza.define({ + name: '_mucUserActor', + namespace: USER_NS, + element: 'actor', + fields: { + nick: stanza.attribute('nick'), + jid: jxtutil.jidAttribute('jid') + } +}); + +var Destroyed = stanza.define({ + name: 'destroyed', + namespace: USER_NS, + element: 'destroy', + fields: { + jid: jxtutil.jidAttribute('jid'), + reason: stanza.subText(USER_NS, 'reason') + } +}); + +var Invite = stanza.define({ + name: 'invite', + namespace: USER_NS, + element: 'invite', + fields: { + to: jxtutil.jidAttribute('to'), + from: jxtutil.jidAttribute('from'), + reason: stanza.subText(USER_NS, 'reason'), + thread: stanza.subAttribute(USER_NS, 'continue', 'thread'), + 'continue': stanza.boolSub(USER_NS, 'continue') + } +}); + +var Decline = stanza.define({ + name: 'decline', + namespace: USER_NS, + element: 'decline', + fields: { + to: jxtutil.jidAttribute('to'), + from: jxtutil.jidAttribute('from'), + reason: stanza.subText(USER_NS, 'reason') + } +}); + +var AdminItem = stanza.define({ + name: '_mucAdminItem', + namespace: ADMIN_NS, + element: 'item', + fields: { + affiliation: stanza.attribute('affiliation'), + nick: stanza.attribute('nick'), + jid: jxtutil.jidAttribute('jid'), + role: stanza.attribute('role'), + reason: stanza.subText(ADMIN_NS, 'reason') + } +}); + +var AdminActor = stanza.define({ + name: 'actor', + namespace: USER_NS, + element: 'actor', + fields: { + nick: stanza.attribute('nick'), + jid: jxtutil.jidAttribute('jid') + } +}); + +var Destroy = stanza.define({ + name: 'destroy', + namespace: OWNER_NS, + element: 'destroy', + fields: { + jid: jxtutil.jidAttribute('jid'), + password: stanza.subText(OWNER_NS, 'password'), + reason: stanza.subText(OWNER_NS, 'reason') + } +}); + +exports.MUC = stanza.define({ + name: 'muc', + namespace: USER_NS, + element: 'x', + fields: { + affiliation: proxy('_mucUserItem', 'affiliation'), + nick: proxy('_mucUserItem', 'nick'), + jid: proxy('_mucUserItem', 'jid'), + role: proxy('_mucUserItem', 'role'), + actor: proxy('_mucUserItem', '_mucUserActor'), + reason: proxy('_mucUserItem', 'reason'), + password: stanza.subText(USER_NS, 'password'), + codes: { + get: function () { + return stanza.getMultiSubText(this.xml, USER_NS, 'status', function (sub) { + return stanza.getAttribute(sub, 'code'); + }); + }, + set: function (value) { + var self = this; + stanza.setMultiSubText(this.xml, USER_NS, 'status', value, function (val) { + var child = stanza.createElement(USER_NS, 'status', USER_NS); + stanza.setAttribute(child, 'code', val); + self.xml.appendChild(child); + }); + } + } + } +}); + +exports.MUCAdmin = stanza.define({ + name: 'mucAdmin', + namespace: ADMIN_NS, + element: 'query', + fields: { + affiliation: proxy('_mucAdminItem', 'affiliation'), + nick: proxy('_mucAdminItem', 'nick'), + jid: proxy('_mucAdminItem', 'jid'), + role: proxy('_mucAdminItem', 'role'), + actor: proxy('_mucAdminItem', '_mucAdminActor'), + reason: proxy('_mucAdminItem', 'reason') + } +}); + +exports.MUCOwner = stanza.define({ + name: 'mucOwner', + namespace: OWNER_NS, + element: 'query' +}); + +exports.MUCJoin = stanza.define({ + name: 'joinMuc', + namespace: NS, + element: 'x', + fields: { + password: stanza.subText(NS, 'password'), + history: { + get: function () { + var result = {}; + var hist = stanza.find(this.xml, this._NS, 'history'); + + if (!hist.length) { + return {}; + } + hist = hist[0]; + + var maxchars = hist.getAttribute('maxchars') || ''; + var maxstanzas = hist.getAttribute('maxstanas') || ''; + var seconds = hist.getAttribute('seconds') || ''; + var since = hist.getAttribute('since') || ''; + + + if (maxchars) { + result.maxchars = parseInt(maxchars, 10); + } + if (maxstanzas) { + result.maxstanzas = parseInt(maxstanzas, 10); + } + if (seconds) { + result.seconds = parseInt(seconds, 10); + } + if (since) { + result.since = new Date(since); + } + }, + set: function (opts) { + var existing = stanza.find(this.xml, this._NS, 'history'); + if (existing.length) { + for (var i = 0; i < existing.length; i++) { + this.xml.removeChild(existing[i]); + } + } + + var hist = stanza.createElement(this._NS, 'history', this._NS); + this.xml.appendChild(hist); + + if (opts.maxchars) { + hist.setAttribute('maxchars' + opts.maxchars); + } + if (opts.maxstanzas) { + hist.setAttribute('maxstanzas', opts.maxstanzas); + } + if (opts.seconds) { + hist.setAttribute('seconds' + opts.seconds); + } + if (opts.since) { + hist.setAttribute('since', opts.since.toISOString()); + } + } + } + } +}); + +exports.DirectInvite = stanza.define({ + name: 'mucInvite', + namespace: 'jabber:x:conference', + element: 'x', + fields: { + jid: jxtutil.jidAttribute('jid'), + password: stanza.attribute('password'), + reason: stanza.attribute('reason'), + thread: stanza.attribute('thread'), + 'continue': stanza.boolAttribute('continue') + } +}); + + +stanza.add(Iq, 'mucUnique', stanza.subText(UNIQ_NS, 'unique')); + + +stanza.extend(UserItem, UserActor); +stanza.extend(exports.MUC, UserItem); +stanza.extend(exports.MUC, Invite, 'invites'); +stanza.extend(exports.MUC, Decline); +stanza.extend(exports.MUC, Destroyed); +stanza.extend(AdminItem, AdminActor); +stanza.extend(exports.MUCAdmin, AdminItem, 'items'); +stanza.extend(exports.MUCOwner, Destroy); +stanza.extend(exports.MUCOwner, DataForm); +stanza.extend(Presence, exports.MUC); +stanza.extend(Message, exports.MUC); +stanza.extend(Presence, exports.MUCJoin); +stanza.extend(Message, exports.DirectInvite); +stanza.extend(Iq, exports.MUCAdmin); +stanza.extend(Iq, exports.MUCOwner); diff --git a/Docker/app/stanza.io/websocket.js b/Docker/app/stanza.io/websocket.js new file mode 100644 index 0000000..9a22c87 --- /dev/null +++ b/Docker/app/stanza.io/websocket.js @@ -0,0 +1,158 @@ +'use strict'; + +var util = require('util'); +var stanza = require('jxt'); +var WildEmitter = require('wildemitter'); +var async = require('async'); +var framing = require('../stanza/framing'); +var StreamError = require('../stanza/streamError'); + +var WS = (require('faye-websocket') && require('faye-websocket').Client) ? + require('faye-websocket').Client : + window.WebSocket; + +var WS_OPEN = 1; + + + +function WSConnection(sm) { + var self = this; + + WildEmitter.call(this); + + self.sm = sm; + self.closing = false; + + self.sendQueue = async.queue(function (data, cb) { + if (self.conn) { + if (typeof data !== 'string') { + data = data.toString(); + } + + data = new Buffer(data, 'utf8').toString(); + + self.emit('raw:outgoing', data); + if (self.conn.readyState === WS_OPEN) { + self.conn.send(data); + } + } + cb(); + }, 1); + + self.on('connected', function () { + self.send(self.startHeader()); + }); + + self.on('raw:incoming', function (data) { + var stanzaObj, err; + + data = data.trim(); + if (data === '') { + return; + } + + if (data.indexOf(" 0 && data.indexOf("") > 0) { + data = data.replace("", "true"); + } + + try { + stanzaObj = stanza.parse(data); + } catch (e) { + err = new StreamError({ + condition: 'invalid-xml' + }); + self.emit('stream:error', err, e); + self.send(err); + return self.disconnect(); + } + + if (stanzaObj._name === 'openStream') { + self.hasStream = true; + self.stream = stanzaObj; + return self.emit('stream:start', stanzaObj.toJSON()); + } + if (stanzaObj._name === 'closeStream') { + self.emit('stream:end'); + return self.disconnect(); + } + + if (!stanzaObj.lang) { + stanzaObj.lang = self.stream ? self.stream.lang : "en"; + } + + self.emit('stream:data', stanzaObj); + }); +} + +util.inherits(WSConnection, WildEmitter); + +WSConnection.prototype.connect = function (opts) { + var self = this; + + self.config = opts; + + self.hasStream = false; + self.closing = false; + + self.conn = new WS(opts.wsURL, 'xmpp'); + self.conn.onerror = function (e) { + e.preventDefault(); + self.emit('disconnected', self); + }; + + self.conn.onclose = function () { + self.emit('disconnected', self); + }; + + self.conn.onopen = function () { + self.sm.started = false; + self.emit('connected', self); + }; + + self.conn.onmessage = function (wsMsg) { + self.emit('raw:incoming', new Buffer(wsMsg.data, 'utf8').toString()); + }; +}; + +WSConnection.prototype.startHeader = function () { + return new framing.Open({ + version: this.config.version || '1.0', + lang: this.config.lang || 'en', + to: this.config.server + }); +}; + +WSConnection.prototype.closeHeader = function () { + return new framing.Close(); +}; + +WSConnection.prototype.disconnect = function () { + if (this.conn && !this.closing) { + this.closing = true; + this.send(this.closeHeader()); + } else { + this.hasStream = false; + this.stream = undefined; + if (this.conn.readyState === WS_OPEN) { + this.conn.close(); + } + this.conn = undefined; + } +}; + +WSConnection.prototype.restart = function () { + var self = this; + self.hasStream = false; + self.send(this.startHeader()); +}; + +WSConnection.prototype.send = function (data) { + this.sendQueue.push(data); +}; + + +module.exports = WSConnection; diff --git a/Docker/app/start.sh b/Docker/app/start.sh new file mode 100644 index 0000000..25e3abb --- /dev/null +++ b/Docker/app/start.sh @@ -0,0 +1,35 @@ +#!/bin/bash + +echo "Configuring dev_config.json..." + +export XMPP_NAME="$(echo ${XMPP_NAME} | sed 's/\//\\\//g')" +export XMPP_DOMAIN="$(echo ${XMPP_DOMAIN} | sed 's/\//\\\//g')" +export XMPP_WSS="$(echo ${XMPP_WSS} | sed 's/\//\\\//g')" +export XMPP_MUC="$(echo ${XMPP_MUC} | sed 's/\//\\\//g')" +export XMPP_STARTUP="$(echo ${XMPP_STARTUP} | sed 's/\//\\\//g')" +export XMPP_ADMIN="$(echo ${XMPP_ADMIN} | sed 's/\//\\\//g')" + +sed 's/{{XMPP_NAME}}/'"${XMPP_NAME}"'/' -i /app/config/dev_config.json +sed 's/{{XMPP_DOMAIN}}/'"${XMPP_DOMAIN}"'/' -i /app/config/dev_config.json +sed 's/{{XMPP_WSS}}/'"${XMPP_WSS}"'/' -i /app/config/dev_config.json +sed 's/{{XMPP_MUC}}/'"${XMPP_MUC}"'/' -i /app/config/dev_config.json +sed 's/{{XMPP_STARTUP}}/'"${XMPP_STARTUP}"'/' -i /app/config/dev_config.json +sed 's/{{XMPP_ADMIN}}/'"${XMPP_ADMIN}"'/' -i /app/config/dev_config.json + +sed 's/{{LDAP_HOST}}/'"${LDAP_PORT_389_TCP_ADDR}"'/' -i /app/config/dev_config.json +sed 's/{{LDAP_BASE}}/'"${LDAP_BASE}"'/' -i /app/config/dev_config.json +sed 's/{{LDAP_DN}}/'"${LDAP_DN}"'/' -i /app/config/dev_config.json +sed 's/{{LDAP_PWD}}/'"${LDAP_PWD}"'/' -i /app/config/dev_config.json +sed 's/{{LDAP_GROUP}}/'"${LDAP_GROUP}"'/' -i /app/config/dev_config.json + +cp /app/config/dev_config.json /otalk + +echo "Configuring otalk..." + +cd otalk + +cp /app/stanza.io/websocket.js node_modules/stanza.io/lib/transports +cp /app/stanza.io/index-browser.js node_modules/stanza.io/lib/plugins +cp /app/stanza.io/muc.js node_modules/stanza.io/lib/stanza + +node server