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("";
+ }
+
+ if (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