changeset 6520:7eb114975cec

mod_admin_web2: Fork of admin_web for prosody 13.0. It is a fork because it changes access rights and might be insecure. Mark as alpha.
author Menel <menel@snikket.de>
date Fri, 24 Apr 2026 12:40:13 +0200
parents 01d8bfbfc435
children 28e39dbf9d7b
files mod_admin_web2/README.md mod_admin_web2/admin_web2/get_deps.sh mod_admin_web2/admin_web2/mod_admin_web2.lua mod_admin_web2/admin_web2/www_files/css/style.css mod_admin_web2/admin_web2/www_files/images/blue_orange.png mod_admin_web2/admin_web2/www_files/images/compressed.png mod_admin_web2/admin_web2/www_files/images/encrypted.png mod_admin_web2/admin_web2/www_files/images/prosody.png mod_admin_web2/admin_web2/www_files/images/secure.png mod_admin_web2/admin_web2/www_files/index.html mod_admin_web2/admin_web2/www_files/js/main.js
diffstat 11 files changed, 881 insertions(+), 0 deletions(-) [+]
line wrap: on
line diff
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/mod_admin_web2/README.md	Fri Apr 24 12:40:13 2026 +0200
@@ -0,0 +1,67 @@
+---
+labels:
+- 'Stage-Alpha'
+summary: Web administration interface
+rockspec:
+  build:
+    platforms:
+      unix:
+        type: command
+        build_command: cd admin_web2; sh get_deps.sh
+        install_command: |
+          cd admin_web2;
+          cp mod_admin_web2.lua $(LUADIR);
+          cp -r www_files $(PREFIX);
+...
+
+Introduction
+============
+
+This module is a fork of [mod_admin_web], because that on is incompatible with prosody 13.0.
+It was made with only small changes but also without much understanding of lua and the internal workigs of prosody. Use with care. Restrict access from the web.
+Patches welcome.
+
+This module provides a basic web-administration interface. It currently
+gives you access to Ad-Hoc commands on any virtual host or component. It
+also provides a live list of S2S and C2S connections. (buggy, not showing all of them)
+
+**New:** You need to be the global instance operator to access this module. Smiplifying access rights.
+**Known Issue:** If you define it on more then one host or globally, it might be nessesary to reload the module to have access to all hosts from within one interface.
+
+Installation
+============
+
+1.  Copy the admin\_web directory into a directory Prosody will check
+    for plugins. (cf. [Installing
+    modules](http://prosody.im/doc/installing_modules))
+2.  **Execute the contained `get_deps.sh` script from within the admin\_web
+    directory.** (Requires wget, tar, and a basic shell)
+
+Configuration Details
+=====================
+
+"admin\_web" needs to be added to the modules\_enabled table of the host
+you want to load this module on.
+
+By default the interface will then be reachable under
+`http://example.com:5280/admin`, or `https://example.com:5281/admin`.
+
+The module will automatically enable two other modules if they aren't
+already: mod\_bosh (used to connect to the server from the web), and
+mod\_admin\_adhoc (which provides admin commands over XMPP).
+
+    VirtualHost "example.com"
+       modules_enabled = {
+           .....
+           "admin_web";
+           .....
+       }
+
+Compatibility
+=============
+
+  --------- ----------------------------------
+  13.0      Works
+  0.12      not supported, use [mod_admin_web]
+  \<= 0.11  not supported
+  --------- ----------------------------------
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/mod_admin_web2/admin_web2/get_deps.sh	Fri Apr 24 12:40:13 2026 +0200
@@ -0,0 +1,23 @@
+#!/bin/sh
+JQUERY_VERSION="1.10.2"
+STROPHE_VERSION="1.1.2"
+BOOTSTRAP_VERSION="1.4.0"
+ADHOC_COMMITISH="87bfedccdb91e2ff7cfb165e989e5259c155b513"
+
+cd www_files/js
+
+rm -f jquery-$JQUERY_VERSION.min.js
+wget http://code.jquery.com/jquery-$JQUERY_VERSION.min.js || exit 1
+
+rm -f adhoc.js
+wget -O adhoc.js "http://git.babelmonkeys.de/?p=adhocweb.git;a=blob_plain;f=js/adhoc.js;hb=$ADHOC_COMMITISH" || exit 1
+
+rm -f strophe.min.js
+wget https://raw.github.com/strophe/strophe.im/gh-pages/strophejs/downloads/strophejs-$STROPHE_VERSION.tar.gz &&
+	tar xzf strophejs-$STROPHE_VERSION.tar.gz strophejs-$STROPHE_VERSION/strophe.min.js &&
+	mv strophejs-$STROPHE_VERSION/strophe.min.js . &&
+	rm -r strophejs-$STROPHE_VERSION strophejs-$STROPHE_VERSION.tar.gz || exit 1
+
+cd ../css
+rm -f bootstrap-$BOOTSTRAP_VERSION.min.css
+wget https://raw.github.com/twbs/bootstrap/v$BOOTSTRAP_VERSION/bootstrap.min.css -O bootstrap-$BOOTSTRAP_VERSION.min.css || exit 1
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/mod_admin_web2/admin_web2/mod_admin_web2.lua	Fri Apr 24 12:40:13 2026 +0200
@@ -0,0 +1,332 @@
+-- Copyright (C) 2010 Florian Zeitz
+--
+-- This file is MIT/X11 licensed. Please see the
+-- COPYING file in the source package for more information.
+--
+
+-- <session xmlns="http://prosody.im/streams/c2s" jid="alice@example.com/brussels">
+--   <encrypted/>
+--   <compressed/>
+-- </session>
+
+-- <session xmlns="http://prosody.im/streams/s2s" jid="example.com">
+--   <encrypted>
+--     <valid/> / <invalid/>
+--   </encrypted>
+--   <compressed/>
+--   <in/> / <out/>
+-- </session>
+
+module:default_permission("prosody:operator", ":web-operator")
+
+local st = require "util.stanza";
+local uuid_generate = require "util.uuid".generate;
+local pubsub = require "util.pubsub";
+local jid_bare = require "util.jid".bare;
+
+local hosts = prosody.hosts;
+local incoming_s2s = prosody.incoming_s2s;
+
+module:set_global();
+
+local service = {};
+
+local xmlns_adminsub = "http://prosody.im/adminsub";
+local xmlns_c2s_session = "http://prosody.im/streams/c2s";
+local xmlns_s2s_session = "http://prosody.im/streams/s2s";
+
+local idmap = {};
+
+local function add_client(session, host)
+	local name = session.full_jid;
+	local id = idmap[name];
+	if not id then
+		id = uuid_generate();
+		idmap[name] = id;
+	end
+	local item = st.stanza("item", { id = id }):tag("session", {xmlns = xmlns_c2s_session, jid = name}):up();
+	if session.secure then
+		local encrypted = item:tag("encrypted");
+		local sock = session.conn and session.conn.socket and session.conn:socket()
+		local info = sock and sock.info and sock:info();
+		for k, v in pairs(info or {}) do
+			encrypted:tag("info", { name = k }):text(tostring(v)):up();
+		end
+	end
+	if session.compressed then
+		item:tag("compressed"):up();
+	end
+	service[host]:publish(xmlns_c2s_session, host, id, item);
+	module:log("debug", "Added client %s", name);
+end
+
+local function del_client(session, host)
+	local name = session.full_jid;
+	local id = idmap[name];
+	if id then
+		local notifier = st.stanza("retract", { id = id });
+		service[host]:retract(xmlns_c2s_session, host, id, notifier);
+	end
+end
+
+local function add_host(session, type, host)
+	local name = (type == "out" and session.to_host) or (type == "in" and session.from_host);
+	local id = idmap[name.."_"..type];
+	if not id then
+		id = uuid_generate();
+		idmap[name.."_"..type] = id;
+	end
+	local item = st.stanza("item", { id = id }):tag("session", {xmlns = xmlns_s2s_session, jid = name})
+		:tag(type):up();
+	if session.secure then
+		local encrypted = item:tag("encrypted");
+
+		local sock = session.conn and session.conn.socket and session.conn:socket()
+		local info = sock and sock.info and sock:info();
+		for k, v in pairs(info or {}) do
+			encrypted:tag("info", { name = k }):text(tostring(v)):up();
+		end
+
+		if session.cert_identity_status == "valid" then
+			encrypted:tag("valid");
+		else
+			encrypted:tag("invalid");
+		end
+	end
+	if session.compressed then
+		item:tag("compressed"):up();
+	end
+	service[host]:publish(xmlns_s2s_session, host, id, item);
+	module:log("debug", "Added host %s s2s%s", name, type);
+end
+
+local function del_host(session, type, host)
+	local name = (type == "out" and session.to_host) or (type == "in" and session.from_host);
+	local id = idmap[name.."_"..type];
+	if id then
+		local notifier = st.stanza("retract", { id = id });
+		service[host]:retract(xmlns_s2s_session, host, id, notifier);
+	end
+end
+
+function module.add_host(module)
+	-- Dependencies
+	module:depends("bosh");
+	module:depends("admin_adhoc");
+	module:depends("http");
+
+	local http_files = require "net.http.files";
+	local serve_file = http_files.serve {
+		path = module:get_directory() .. "/www_files";
+	};
+
+	-- Setup HTTP server
+	module:provides("http", {
+		title = "Admin Interface";
+		name = "admin";
+		route = {
+			["GET"] = function(event)
+				event.response.headers.location = event.request.path .. "/";
+				return 301;
+			end;
+			["GET /*"] = serve_file;
+		}
+	});
+
+	-- Setup adminsub service
+	local function simple_broadcast(kind, node, jids, item)
+		if item then
+			item = st.clone(item);
+			item.attr.xmlns = nil; -- Clear the pubsub namespace
+		end
+		local message = st.message({ from = module.host, type = "headline" })
+			:tag("event", { xmlns = xmlns_adminsub .. "#event" })
+				:tag(kind, { node = node })
+					:add_child(item);
+		for jid in pairs(jids) do
+			module:log("debug", "Sending notification to %s", jid);
+			message.attr.to = jid;
+			module:send(message);
+		end
+	end
+
+	service[module.host] = pubsub.new({
+		broadcaster = simple_broadcast;
+		normalize_jid = jid_bare;
+-- what was this for? prosody:operator has full rights everywhere, this shoud be fine?
+--		get_affiliation = function(jid) return get_affiliation(jid, module.host) end;
+		get_affiliation = function() return "owner" end;
+		capabilities = {
+			member = {
+				create = false;
+				publish = false;
+				retract = false;
+				get_nodes = true;
+
+				subscribe = true;
+				unsubscribe = true;
+				get_subscription = true;
+				get_subscriptions = true;
+				get_items = true;
+
+				subscribe_other = false;
+				unsubscribe_other = false;
+				get_subscription_other = false;
+				get_subscriptions_other = false;
+
+				be_subscribed = true;
+				be_unsubscribed = true;
+
+				set_affiliation = false;
+			};
+
+			owner = {
+				create = true;
+				publish = true;
+				retract = true;
+				get_nodes = true;
+
+				subscribe = true;
+				unsubscribe = true;
+				get_subscription = true;
+				get_subscriptions = true;
+				get_items = true;
+
+				subscribe_other = true;
+				unsubscribe_other = true;
+				get_subscription_other = true;
+				get_subscriptions_other = true;
+
+				be_subscribed = true;
+				be_unsubscribed = true;
+
+				set_affiliation = true;
+			};
+		};
+	});
+
+	-- Create node for s2s sessions
+	local ok, err = service[module.host]:create(xmlns_s2s_session, true);
+	if not ok then
+		module:log("warn", "Could not create node %s: %s", xmlns_s2s_session, err);
+	else
+		service[module.host]:set_affiliation(xmlns_s2s_session, true, module.host, "owner")
+	end
+
+	-- Add outgoing s2s sessions
+	for _, session in pairs(hosts[module.host].s2sout) do
+		if session.type ~= "s2sout_unauthed" then
+			add_host(session, "out", module.host);
+		end
+	end
+
+	-- Add incoming s2s sessions
+	for session in pairs(incoming_s2s) do
+		if session.to_host == module.host then
+			add_host(session, "in", module.host);
+		end
+	end
+
+	-- Create node for c2s sessions
+	ok, err = service[module.host]:create(xmlns_c2s_session, true);
+	if not ok then
+		module:log("warn", "Could not create node %s: %s", xmlns_c2s_session, tostring(err));
+	else
+		service[module.host]:set_affiliation(xmlns_c2s_session, true, module.host, "owner")
+	end
+
+	-- Add c2s sessions
+	for _, user in pairs(hosts[module.host].sessions or {}) do
+		for _, session in pairs(user.sessions or {}) do
+			add_client(session, module.host);
+		end
+	end
+
+	-- Register adminsub handler
+	module:hook("iq/host/http://prosody.im/adminsub:adminsub", function(event)
+		-- luacheck: ignore 431/ok
+		local origin, stanza = event.origin, event.stanza;
+		local adminsub = stanza.tags[1];
+		local action = adminsub.tags[1];
+		local reply;
+		if action.name == "subscribe" then
+			local ok, ret = service[module.host]:add_subscription(action.attr.node, stanza.attr.from, stanza.attr.from);
+			if ok then
+				reply = st.reply(stanza)
+					:tag("adminsub", { xmlns = xmlns_adminsub });
+			else
+				reply = st.error_reply(stanza, "cancel", ret);
+			end
+		elseif action.name == "unsubscribe" then
+			local ok, ret = service[module.host]:remove_subscription(action.attr.node, stanza.attr.from, stanza.attr.from);
+			if ok then
+				reply = st.reply(stanza)
+					:tag("adminsub", { xmlns = xmlns_adminsub });
+			else
+				reply = st.error_reply(stanza, "cancel", ret);
+			end
+		elseif action.name == "items" then
+			local node = action.attr.node;
+			local ok, ret = service[module.host]:get_items(node, stanza.attr.from);
+			if not ok then
+				origin.send(st.error_reply(stanza, "cancel", ret));
+				return true;
+			end
+
+			local data = st.stanza("items", { node = node });
+			for _, entry in pairs(ret) do
+				data:add_child(entry);
+			end
+			if data then
+				reply = st.reply(stanza)
+					:tag("adminsub", { xmlns = xmlns_adminsub })
+						:add_child(data);
+			else
+				reply = st.error_reply(stanza, "cancel", "item-not-found");
+			end
+		elseif action.name == "adminfor" then
+			local data = st.stanza("adminfor");
+			for host_name in pairs(hosts) do
+				-- new Permission API:
+				if module:context(host_name):may(":web-operator", stanza.attr.from) then
+					data:tag("item"):text(host_name):up();
+				end
+			end
+			reply = st.reply(stanza)
+				:tag("adminsub", { xmlns = xmlns_adminsub })
+					:add_child(data);
+		else
+			reply = st.error_reply(stanza, "feature-not-implemented");
+		end
+		origin.send(reply);
+		return true;
+	end);
+
+	-- Add/remove c2s sessions
+	module:hook("resource-bind", function(event)
+		add_client(event.session, module.host);
+	end);
+
+	module:hook("resource-unbind", function(event)
+		del_client(event.session, module.host);
+		service[module.host]:remove_subscription(xmlns_c2s_session, module.host, event.session.full_jid);
+		service[module.host]:remove_subscription(xmlns_s2s_session, module.host, event.session.full_jid);
+	end);
+
+	-- Add/remove s2s sessions
+	module:hook("s2sout-established", function(event)
+		add_host(event.session, "out", module.host);
+	end);
+
+	module:hook("s2sin-established", function(event)
+		add_host(event.session, "in", module.host);
+	end);
+
+	module:hook("s2sout-destroyed", function(event)
+		del_host(event.session, "out", module.host);
+	end);
+
+	module:hook("s2sin-destroyed", function(event)
+		del_host(event.session, "in", module.host);
+	end);
+end
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/mod_admin_web2/admin_web2/www_files/css/style.css	Fri Apr 24 12:40:13 2026 +0200
@@ -0,0 +1,114 @@
+body {
+  margin: 0
+}
+
+a {
+  color: #0000FF
+}
+
+#adhocCommands > ul {
+  margin: 0
+}
+
+.btn {
+  margin-right: 0.3em
+}
+
+.btn:last-child {
+  margin-right: 0
+}
+
+#log_container {
+  clear: both;
+  display: none
+}
+
+#adhocCommands {
+  border-right: solid 1px
+}
+
+#adhocCommands li {
+  list-style: inside
+}
+
+#login {
+  float: left;
+  margin: 1em 2em 0 1em;
+  padding-right: 1em;
+  border: solid 1px;
+  background: #eef0f2;
+  color: #000000
+}
+
+#main {
+  display: none;
+  margin: 1em
+}
+
+#main p {
+  margin: 0
+}
+
+#top {
+  clear: both;
+  width: 100%;
+  padding: 0;
+}
+
+@media screen and (min-width: 757px) {
+  #header {
+    background: url(../images/blue_orange.png) repeat-x
+  }
+}
+
+#header img {
+  max-width: 100%;
+  height: auto
+}
+
+#menu {
+  display: none;
+  color: #454748;
+  font-size: 1.1em;
+  background: #eef0f2;
+  width: 100%;
+}
+
+#menu ul {
+  display: inline;
+  list-style-type: none;
+  margin: 0;
+  padding: 0.5em 0
+}
+
+#menu li {
+  display: inline;
+  padding: 0 0.5em
+}
+
+#menu a {
+  color: #454748;
+  text-decoration: none
+}
+
+#menu li a:hover {
+  color: #6197DF;
+  text-decoration: underline
+}
+
+#selector {
+  display: inline-block
+}
+
+#s2sList h2, #c2sList h2 {
+  color: #4b8ade;
+  margin: 0
+}
+
+#s2sList li, #c2sList li {
+  cursor: pointer
+}
+
+#host {
+  margin: 0.25em;
+}
Binary file mod_admin_web2/admin_web2/www_files/images/blue_orange.png has changed
Binary file mod_admin_web2/admin_web2/www_files/images/compressed.png has changed
Binary file mod_admin_web2/admin_web2/www_files/images/encrypted.png has changed
Binary file mod_admin_web2/admin_web2/www_files/images/prosody.png has changed
Binary file mod_admin_web2/admin_web2/www_files/images/secure.png has changed
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/mod_admin_web2/admin_web2/www_files/index.html	Fri Apr 24 12:40:13 2026 +0200
@@ -0,0 +1,90 @@
+<?xml version="1.0" encoding="utf-8" ?>
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN"
+	"http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">
+<html xmlns="http://www.w3.org/1999/xhtml">
+<head>
+<title>Prosody Webadmin</title>
+<link rel="stylesheet" type="text/css" href="css/bootstrap-1.4.0.min.css" />
+<link rel="stylesheet" type="text/css" href="css/style.css" />
+<meta http-equiv="Content-Type" content="application/xhtml+xml; charset=utf-8" />
+<script type="text/javascript" src="js/jquery-1.10.2.min.js"></script>
+<script type="text/javascript" src="js/strophe.min.js"></script>
+<script type="text/javascript" src="js/adhoc.js"></script>
+<script type="text/javascript" src="js/main.js"></script>
+</head>
+
+<body>
+<div id='top'>
+  <div id='header'>
+    <img src="images/prosody.png" alt="Prosody"/>
+  </div>
+  <div id='menu'>
+    <ul>
+      <li><a id='adhocMenu' href="#adhoc">General</a></li>
+      <li><a id='serverMenu' href="#s2sList">Servers</a></li>
+      <li><a id='clientMenu' href="#c2sList">Clients</a></li>
+      <li><span id="selector">Host:&nbsp;<select id="host"></select></span></li>
+      <li><a href="#" id='logout'>Logout</a></li>
+    </ul>
+  </div>
+</div>
+
+<div id='login'>
+  <form id='cred' name='cred' class='form-stacked'>
+    <fieldset>
+      <div class='clearfix'>
+	<label for='jid'>JID:</label>
+	<div class='input'>
+	  <input type='text' id='jid' />
+	</div>
+      </div>
+      <div class='clearfix'>
+	<label for='pass'>Password:</label>
+	<div class='input'>
+	  <input type='password' id='pass' />
+	</div>
+      </div>
+      <div class='clearfix'>
+	<div class='input'>
+	  <input type='submit' id='connect' value='Login' class='btn' />
+	</div>
+      </div>
+    </fieldset>
+  </form>
+</div>
+
+<div id='main'>
+  <div id="adhoc">
+    <div class="row">
+      <div id="adhocCommands" class="span4"></div>
+      <div id="adhocDisplay" class="span12"></div>
+    </div>
+  </div>
+  <div id="s2sList" class="container">
+   <div class="row">
+    <div class="span8">
+      <h2>Incoming S2S connections:</h2>
+      <ul id="s2sin"></ul>
+    </div>
+    <div class="span8">
+      <h2>Outgoing S2S connections:</h2>
+      <ul id="s2sout"></ul>
+    </div>
+   </div>
+  </div>
+  <div id="c2sList" class="container">
+   <div class="row">
+    <div class="span16">
+      <h2>Client connections:</h2>
+      <ul id="c2s"></ul>
+    </div>
+   </div>
+  </div>
+</div>
+
+<div id='log_container'>
+  <a id='log_toggle' href='#'>Status Log :</a>
+  <div id='log'></div>
+</div>
+</body>
+</html>
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/mod_admin_web2/admin_web2/www_files/js/main.js	Fri Apr 24 12:40:13 2026 +0200
@@ -0,0 +1,255 @@
+var BOSH_SERVICE = '/http-bind/';
+var show_log = false;
+
+Strophe.addNamespace('C2SSTREAM', 'http://prosody.im/streams/c2s');
+Strophe.addNamespace('S2SSTREAM', 'http://prosody.im/streams/s2s');
+Strophe.addNamespace('ADMINSUB', 'http://prosody.im/adminsub');
+Strophe.addNamespace('CAPS', 'http://jabber.org/protocol/caps');
+
+var localJID = null;
+var connection   = null;
+
+var adminsubHost = null;
+var adhocControl = new Adhoc('#adhocDisplay', function() {});
+
+function log(msg) {
+    var entry = $('<div></div>').append(document.createTextNode(msg));
+    $('#log').append(entry);
+}
+
+function rawInput(data) {
+    log('RECV: ' + data);
+}
+
+function rawOutput(data) {
+    log('SENT: ' + data);
+}
+
+function _cbNewS2S(e) {
+    var items, item, entry, tmp, retract, id, jid, infos, info, metadata;
+    items = e.getElementsByTagName('item');
+    for (i = 0; i < items.length; i++) {
+        item = items[i];
+        id = item.attributes.getNamedItem('id').value;
+        jid = item.getElementsByTagName('session')[0].attributes.getNamedItem('jid').value;
+        infos = item.getElementsByTagName('info');
+
+        entry = $('<li id="' + id + '">' + jid + '</li>');
+        if (tmp = item.getElementsByTagName('encrypted')[0]) {
+            if (tmp.getElementsByTagName('valid')[0]) {
+                entry.append('<img src="images/secure.png" title="encrypted (certificate valid)" alt=" (secure) (encrypted)" />');
+            } else {
+                entry.append('<img src="images/encrypted.png" title="encrypted (certificate invalid)" alt=" (encrypted)" />');
+            }
+        }
+        if (item.getElementsByTagName('compressed')[0]) {
+            entry.append('<img src="images/compressed.png" title="compressed" alt=" (compressed)" />');
+        }
+        metadata = $('<ul/>').css('display', 'none');
+        entry.on('click', function() {
+            $(this).find("ul").slideToggle();
+        });
+        metadata.appendTo(entry);
+        for (j = 0; j < infos.length; j++) {
+            info = infos[j];
+            metadata.append('<li><b>' + info.attributes.getNamedItem('name').value + ':</b> ' + info.textContent + '</li>');
+        }
+        if (infos.length == 0)
+            metadata.append('<li>No information available</li>');
+
+        if (items[i].getElementsByTagName('out')[0]) {
+            entry.appendTo('#s2sout');
+        } else {
+            entry.appendTo('#s2sin');
+        }
+    }
+    retract = e.getElementsByTagName('retract')[0];
+    if (retract) {
+        id = retract.attributes.getNamedItem('id').value;
+        $('#' + id).remove();
+    }
+    return true;
+}
+
+function _cbNewC2S(e) {
+    var items, item, entry, retract, id, jid, infos, info, metadata;
+    items = e.getElementsByTagName('item');
+    for (i = 0; i < items.length; i++) {
+        item = items[i];
+        id = item.attributes.getNamedItem('id').value;
+        jid = item.getElementsByTagName('session')[0].attributes.getNamedItem('jid').value;
+        infos = item.getElementsByTagName('info');
+        entry = $('<li id="' + id + '">' + jid + '</li>');
+        if (item.getElementsByTagName('encrypted')[0]) {
+            entry.append('<img src="images/encrypted.png" title="encrypted" alt=" (encrypted)" />');
+        }
+        if (item.getElementsByTagName('compressed')[0]) {
+            entry.append('<img src="images/compressed.png" title="compressed" alt=" (compressed)" />');
+        }
+        metadata = $('<ul/>').css('display', 'none');
+        entry.on('click', function() {
+            $(this).find("ul").slideToggle();
+        });
+        metadata.appendTo(entry);
+        for (j = 0; j < infos.length; j++) {
+            info = infos[j];
+            metadata.append('<li><b>' + info.attributes.getNamedItem('name').value + ':</b> ' + info.textContent + '</li>');
+        }
+        if (infos.length == 0)
+            metadata.append('<li>No information available</li>');
+        entry.appendTo('#c2s');
+    }
+    retract = e.getElementsByTagName('retract')[0];
+    if (retract) {
+        id = retract.attributes.getNamedItem('id').value;
+        $('#' + id).remove();
+    }
+    return true;
+}
+
+function _cbAdminSub(e) {
+    var node = e.getElementsByTagName('items')[0].attributes.getNamedItem('node').value;
+    if (node == Strophe.NS.C2SSTREAM) {
+        _cbNewC2S(e);
+    } else if (node == Strophe.NS.S2SSTREAM) {
+        _cbNewS2S(e);
+    }
+
+    return true;
+}
+
+function onConnect(status) {
+    if (status == Strophe.Status.CONNECTING) {
+        log('Strophe is connecting.');
+    } else if (status == Strophe.Status.CONNFAIL) {
+        alert('Connection failed (Wrong host?)');
+        log('Strophe failed to connect.');
+        showConnect();
+    } else if (status == Strophe.Status.DISCONNECTING) {
+        log('Strophe is disconnecting.');
+    } else if (status == Strophe.Status.DISCONNECTED) {
+        log('Strophe is disconnected.');
+        showConnect();
+    } else if (status == Strophe.Status.AUTHFAIL) {
+        alert('Wrong username and/or password');
+        log('Authentication failed');
+        if (connection) {
+            connection.disconnect();
+        }
+    } else if (status == Strophe.Status.CONNECTED) {
+        log('Strophe is connected.');
+        connection.sendIQ($iq({to: connection.domain, type: 'get', id: connection.getUniqueId()}).c('adminsub', {xmlns: Strophe.NS.ADMINSUB})
+            .c('adminfor'), function(e) {
+                var items;
+                var domainpart = Strophe.getDomainFromJid(connection.jid);
+                items = e.getElementsByTagName('item');
+                if (items.length == 0) {
+                    alert("You are not an administrator");
+                    connection.disconnect();
+                    return false;
+                }
+                for (i = 0; i < items.length; i++) {
+                    var host = $(items[i]).text();
+                    $('<option/>').text(host).prop('selected', host == domainpart).appendTo('#host');
+                }
+                showDisconnect();
+                adminsubHost = $('#host').val();
+                adhocControl.checkFeatures(adminsubHost,
+                    function () { adhocControl.getCommandNodes(function (result) { $('#adhocDisplay').empty(); $('#adhocCommands').html(result); }) },
+                    function () { $('#adhocCommands').empty(); $('#adhocDisplay').html('<p>This host does not support commands</p>'); });
+                connection.addHandler(_cbAdminSub, Strophe.NS.ADMINSUB + '#event', 'message');
+                connection.send($iq({to: adminsubHost, type: 'set', id: connection.getUniqueId()}).c('adminsub', {xmlns: Strophe.NS.ADMINSUB})
+                    .c('subscribe', {node: Strophe.NS.C2SSTREAM}));
+                connection.send($iq({to: adminsubHost, type: 'set', id: connection.getUniqueId()}).c('adminsub', {xmlns: Strophe.NS.ADMINSUB})
+                    .c('subscribe', {node: Strophe.NS.S2SSTREAM}));
+                connection.sendIQ($iq({to: adminsubHost, type: 'get', id: connection.getUniqueId()}).c('adminsub', {xmlns: Strophe.NS.ADMINSUB})
+                    .c('items', {node: Strophe.NS.S2SSTREAM}), _cbNewS2S);
+                connection.sendIQ($iq({to: adminsubHost, type: 'get', id: connection.getUniqueId()}).c('adminsub', {xmlns: Strophe.NS.ADMINSUB})
+                    .c('items', {node: Strophe.NS.C2SSTREAM}), _cbNewC2S);
+        });
+    }
+}
+
+function showConnect() {
+    $('#login').show();
+    $('#menu').hide();
+    $('#main').hide();
+    $('#s2sin').empty();
+    $('#s2sout').empty();
+    $('#c2s').empty();
+    $('#host').empty();
+}
+
+function showDisconnect() {
+    $('#s2sList').hide();
+    $('#c2sList').hide();
+    $('#login').hide();
+
+    $('#menu').show();
+    $('#main').show();
+    $('#adhoc').show();
+}
+
+$(document).ready(function () {
+    connection = new Strophe.Connection(BOSH_SERVICE);
+    if (show_log) {
+        $('#log_container').show();
+        connection.rawInput = rawInput;
+        connection.rawOutput = rawOutput;
+    }
+
+    $("#log_toggle").click(function () {
+        $("#log").toggle();
+    });
+
+    $('#cred').on('submit', function (event) {
+        var button = $('#connect').get(0);
+        var jid = $('#jid');
+        var pass = $('#pass');
+        localJID = jid.get(0).value;
+
+        $('#log').empty();
+        connection.connect(localJID, pass.get(0).value, onConnect);
+        event.preventDefault();
+    });
+
+    $('#logout').click(function (event) {
+        connection.disconnect();
+        event.preventDefault();
+    });
+
+    $('#adhocMenu, #serverMenu, #clientMenu').click(function (event) {
+        event.preventDefault();
+        var tab = $(this).attr('href');
+        $('#main > div').hide();
+        $(tab).fadeIn('fast');
+    });
+
+    $('#host').on('change', function (event) {
+        connection.send($iq({to: adminsubHost, type: 'set', id: connection.getUniqueId()}).c('adminsub', {xmlns: Strophe.NS.ADMINSUB})
+            .c('unsubscribe', {node: Strophe.NS.C2SSTREAM}));
+        connection.send($iq({to: adminsubHost, type: 'set', id: connection.getUniqueId()}).c('adminsub', {xmlns: Strophe.NS.ADMINSUB})
+            .c('unsubscribe', {node: Strophe.NS.S2SSTREAM}));
+        adminsubHost = $(this).val();
+        adhocControl.checkFeatures(adminsubHost,
+            function () { adhocControl.getCommandNodes(function (result) { $('#adhocDisplay').empty(); $('#adhocCommands').html(result); }) },
+            function () { $('#adhocCommands').empty(); $('#adhocDisplay').html('<p>This host does not support commands</p>'); });
+        $('#s2sin').empty();
+        $('#s2sout').empty();
+        $('#c2s').empty();
+        connection.send($iq({to: adminsubHost, type: 'set', id: connection.getUniqueId()}).c('adminsub', {xmlns: Strophe.NS.ADMINSUB})
+            .c('subscribe', {node: Strophe.NS.C2SSTREAM}));
+        connection.send($iq({to: adminsubHost, type: 'set', id: connection.getUniqueId()}).c('adminsub', {xmlns: Strophe.NS.ADMINSUB})
+            .c('subscribe', {node: Strophe.NS.S2SSTREAM}));
+        connection.sendIQ($iq({to: adminsubHost, type: 'get', id: connection.getUniqueId()}).c('adminsub', {xmlns: Strophe.NS.ADMINSUB})
+            .c('items', {node: Strophe.NS.S2SSTREAM}), _cbNewS2S);
+        connection.sendIQ($iq({to: adminsubHost, type: 'get', id: connection.getUniqueId()}).c('adminsub', {xmlns: Strophe.NS.ADMINSUB})
+            .c('items', {node: Strophe.NS.C2SSTREAM}), _cbNewC2S);
+    });
+});
+
+window.onunload = window.onbeforeunload = function() {
+    if (connection) {
+        connection.disconnect();
+    }
+}