Added luci app for ocserv.
authorNikos Mavrogiannopoulos <nmav@gnutls.org>
Wed, 18 Jun 2014 19:09:17 +0000 (21:09 +0200)
committerNikos Mavrogiannopoulos <nmav@gnutls.org>
Wed, 18 Jun 2014 19:32:05 +0000 (21:32 +0200)
Signed-off-by: Nikos Mavrogiannopoulos <nmav@gnutls.org>
net/luci-app-ocserv/Makefile [new file with mode: 0644]
net/luci-app-ocserv/files/usr/lib/lua/luci/controller/ocserv.lua [new file with mode: 0644]
net/luci-app-ocserv/files/usr/lib/lua/luci/model/cbi/ocserv/main.lua [new file with mode: 0644]
net/luci-app-ocserv/files/usr/lib/lua/luci/model/cbi/ocserv/user-config.lua [new file with mode: 0644]
net/luci-app-ocserv/files/usr/lib/lua/luci/model/cbi/ocserv/users.lua [new file with mode: 0644]
net/luci-app-ocserv/files/usr/lib/lua/luci/view/admin_status/index/ocserv.htm [new file with mode: 0644]
net/luci-app-ocserv/files/usr/lib/lua/luci/view/ocserv_status.htm [new file with mode: 0644]

diff --git a/net/luci-app-ocserv/Makefile b/net/luci-app-ocserv/Makefile
new file mode 100644 (file)
index 0000000..b77372d
--- /dev/null
@@ -0,0 +1,57 @@
+#    Copyright (C) 2014 Nikos Mavrogiannopoulos
+#
+#    This program is free software; you can redistribute it and/or modify
+#    it under the terms of the GNU General Public License as published by
+#    the Free Software Foundation; either version 2 of the License, or
+#    (at your option) any later version.
+#
+#    This program is distributed in the hope that it will be useful,
+#    but WITHOUT ANY WARRANTY; without even the implied warranty of
+#    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+#    GNU General Public License for more details.
+#
+#    You should have received a copy of the GNU General Public License along
+#    with this program; if not, write to the Free Software Foundation, Inc.,
+#    51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+#
+#    The full GNU General Public License is included in this distribution in
+#    the file called "COPYING".
+
+include $(TOPDIR)/rules.mk
+
+PKG_NAME:=luci-app-ocserv
+PKG_RELEASE:=1
+
+PKG_BUILD_DIR := $(BUILD_DIR)/$(PKG_NAME)
+
+include $(INCLUDE_DIR)/package.mk
+
+define Package/luci-app-ocserv
+  SECTION:=luci
+  CATEGORY:=LuCI
+  SUBMENU:=3. Applications
+  TITLE:= OpenConnect VPN server configuration and status module
+  DEPENDS:=+luci-lib-json +luci-mod-admin-core +ocserv
+  MAINTAINER:= Nikos Mavrogiannopoulos <n.mavrogiannopoulos@gmail.com>
+endef
+
+define Package/luci-app-ocserv/description
+       ocserv web module for LuCi web interface
+endef
+
+define Build/Prepare
+endef
+
+define Build/Configure
+endef
+
+define Build/Compile
+endef
+
+# Fixme: How can we add <%+ocserv_status%> in view/admin_status/index.htm?
+define Package/luci-app-ocserv/install
+       $(CP) ./files/* $(1)/
+endef
+
+$(eval $(call BuildPackage,luci-app-ocserv))
+
diff --git a/net/luci-app-ocserv/files/usr/lib/lua/luci/controller/ocserv.lua b/net/luci-app-ocserv/files/usr/lib/lua/luci/controller/ocserv.lua
new file mode 100644 (file)
index 0000000..5cb0fb9
--- /dev/null
@@ -0,0 +1,90 @@
+--[[
+LuCI - Lua Configuration Interface
+
+Copyright 2014 Nikos Mavrogiannopoulos <n.mavrogiannopoulos@gmail.com>
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+       http://www.apache.org/licenses/LICENSE-2.0
+
+$Id$
+]]--
+
+module("luci.controller.ocserv", package.seeall)
+
+function index()
+       if not nixio.fs.access("/etc/config/ocserv") then
+               return
+       end
+
+       local page
+
+       page = entry({"admin", "services", "ocserv"}, alias("admin", "services", "ocserv", "main"),
+               _("OpenConnect VPN"))
+       page.dependent = true
+       
+       page = entry({"admin", "services", "ocserv", "main"},
+               cbi("ocserv/main"),
+               _("Server Settings"), 200)
+       page.dependent = true
+
+       page = entry({"admin", "services", "ocserv", "users"},
+               cbi("ocserv/users"),
+               _("User Settings"), 300)
+       page.dependent = true
+
+       entry({"admin", "services", "ocserv", "status"},
+               call("ocserv_status")).leaf = true
+
+       entry({"admin", "services", "ocserv", "disconnect"},
+               call("ocserv_disconnect")).leaf = true
+
+end
+
+function ocserv_status()
+       local ipt = io.popen("/usr/bin/occtl show users");
+
+       if ipt then
+
+               local fwd = { }
+               while true do
+
+                       local ln = ipt:read("*l")
+                       if not ln then break end
+               
+                       local id, user, group, vpn_ip, ip, device, time, cipher, status = 
+                               ln:match("^%s*(%d+)%s+([-_%w]+)%s+([%.%*-_%w]+)%s+([%:%.-_%w]+)%s+([%:%.-_%w]+)%s+([%:%.-_%w]+)%s+([%:%.-_%w]+)%s+([%:%.-_%w]+)%s+([%:%.-_%w]+).*")
+                       if id then
+                               fwd[#fwd+1] = {
+                                       id = id,
+                                       user = user,
+                                       group = group,
+                                       vpn_ip = vpn_ip,
+                                       ip = ip,
+                                       device = device,
+                                       time = time,
+                                       cipher = cipher,
+                                       status = status
+                               }
+                       end
+               end
+               ipt:close()
+               luci.http.prepare_content("application/json")
+               luci.http.write_json(fwd)
+       end
+end
+
+function ocserv_disconnect(num)
+       local idx = tonumber(num)
+       local uci = luci.model.uci.cursor()
+
+       if idx and idx > 0 then
+               luci.sys.call("/usr/bin/occtl disconnect id %d" % idx)
+               luci.http.status(200, "OK")
+
+               return
+       end
+       luci.http.status(400, "Bad request")
+end
diff --git a/net/luci-app-ocserv/files/usr/lib/lua/luci/model/cbi/ocserv/main.lua b/net/luci-app-ocserv/files/usr/lib/lua/luci/model/cbi/ocserv/main.lua
new file mode 100644 (file)
index 0000000..65f8878
--- /dev/null
@@ -0,0 +1,146 @@
+--[[
+LuCI - Lua Configuration Interface
+
+Copyright 2014 Nikos Mavrogiannopoulos <n.mavrogiannopoulos@gmail.com>
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+       http://www.apache.org/licenses/LICENSE-2.0
+
+$Id$
+local niulib = require "luci.niulib"
+]]--
+
+local fs = require "nixio.fs"
+local has_ipv6 = fs.access("/proc/net/ipv6_route")
+
+m = Map("ocserv", translate("OpenConnect VPN"))
+
+s = m:section(TypedSection, "ocserv", "OpenConnect")
+s.anonymous = true
+
+s:tab("general",  translate("General Settings"))
+s:tab("ca", translate("CA certificate"))
+s:tab("template", translate("Edit Template"))
+
+local e = s:taboption("general", Flag, "enable", translate("Enable server"))
+e.rmempty = false
+e.default = "1"
+
+function m.on_commit(map)
+       luci.sys.call("/usr/bin/occtl reload  >/dev/null 2>&1")
+end
+
+function e.write(self, section, value)
+       if value == "0" then
+               luci.sys.call("/etc/init.d/ocserv stop >/dev/null 2>&1")
+               luci.sys.call("/etc/init.d/ocserv disable  >/dev/null 2>&1")
+       else
+               luci.sys.call("/etc/init.d/ocserv enable  >/dev/null 2>&1")
+               luci.sys.call("/etc/init.d/ocserv restart  >/dev/null 2>&1")
+       end
+       Flag.write(self, section, value)
+end
+
+local o
+
+o = s:taboption("general", ListValue, "auth", translate("User Authentication"),
+       translate("The authentication method for the users. The simplest is plain with a single username-password pair. Use PAM modules to authenticate using another server (e.g., LDAP, Radius)."))
+o.rmempty = false
+o.default = "plain"
+o:value("plain")
+o:value("PAM")
+
+o = s:taboption("general", Value, "zone", translate("Firewall Zone"),
+       translate("The firewall zone that the VPN clients will be set to"))
+o.nocreate = true
+o.default = "lan"
+o.template = "cbi/firewall_zonelist"
+
+s:taboption("general", Value, "port", translate("Port"),
+       translate("The same UDP and TCP ports will be used"))
+s:taboption("general", Value, "max_clients", translate("Max clients"))
+s:taboption("general", Value, "max_same", translate("Max same clients"))
+s:taboption("general", Value, "dpd", translate("Dead peer detection time (secs)"))
+
+local pip = s:taboption("general", Flag, "predictable_ips", translate("Predictable IPs"),
+       translate("The assigned IPs will be selected deterministically"))
+pip.default = "1"
+
+local udp = s:taboption("general", Flag, "udp", translate("Enable UDP"),
+       translate("Enable UDP channel support; this must be enabled unless you know what you are doing"))
+udp.default = "1"
+
+local cisco = s:taboption("general", Flag, "cisco_compat", translate("AnyConnect client compatibility"),
+       translate("Enable support for CISCO AnyConnect clients"))
+cisco.default = "1"
+
+ipaddr = s:taboption("general", Value, "ipaddr", translate("VPN <abbr title=\"Internet Protocol Version 4\">IPv4</abbr>-Network-Address"))
+ipaddr.default = "192.168.100.1"
+
+nm = s:taboption("general", Value, "netmask", translate("VPN <abbr title=\"Internet Protocol Version 4\">IPv4</abbr>-Netmask"))
+nm.default = "255.255.255.0"
+nm:value("255.255.255.0")
+nm:value("255.255.0.0")
+nm:value("255.0.0.0")
+
+if has_ipv6 then
+       ip6addr = s:taboption("general", Value, "ip6addr", translate("VPN <abbr title=\"Internet Protocol Version 6\">IPv6</abbr>-Network-Address"), translate("<abbr title=\"Classless Inter-Domain Routing\">CIDR</abbr>-Notation: address/prefix"))
+end
+
+
+tmpl = s:taboption("template", Value, "_tmpl",
+       translate("Edit the template that is used for generating the ocserv configuration."))
+
+tmpl.template = "cbi/tvalue"
+tmpl.rows = 20
+
+function tmpl.cfgvalue(self, section)
+       return nixio.fs.readfile("/etc/ocserv/ocserv.conf.template")
+end
+
+function tmpl.write(self, section, value)
+       value = value:gsub("\r\n?", "\n")
+       nixio.fs.writefile("/etc/ocserv/ocserv.conf.template", value)
+end
+
+ca = s:taboption("ca", Value, "_ca",
+       translate("View the CA certificate used by this server. You will need to save it as 'ca.pem' and import it into the clients."))
+
+ca.template = "cbi/tvalue"
+ca.rows = 20
+
+function ca.cfgvalue(self, section)
+       return nixio.fs.readfile("/etc/ocserv/ca.pem")
+end
+
+--[[DNS]]--
+
+s = m:section(TypedSection, "dns", translate("DNS servers"),
+       translate("The DNS servers to be provided to clients; can be either IPv6 or IPv4"))
+s.anonymous = true
+s.addremove = true
+s.template = "cbi/tblsection"
+
+s:option(Value, "ip", translate("IP Address")).rmempty = true
+
+--[[Routes]]--
+
+s = m:section(TypedSection, "routes", translate("Routing table"),
+       translate("The routing table to be provided to clients; you can mix IPv4 and IPv6 routes, the server will send only the appropriate. Leave empty to set a default route"))
+s.anonymous = true
+s.addremove = true
+s.template = "cbi/tblsection"
+
+s:option(Value, "ip", translate("IP Address")).rmempty = true
+
+o = s:option(Value, "netmask", translate("Netmask (or IPv6-prefix)"))
+o.default = "255.255.255.0"
+o:value("255.255.255.0")
+o:value("255.255.0.0")
+o:value("255.0.0.0")
+
+
+return m
diff --git a/net/luci-app-ocserv/files/usr/lib/lua/luci/model/cbi/ocserv/user-config.lua b/net/luci-app-ocserv/files/usr/lib/lua/luci/model/cbi/ocserv/user-config.lua
new file mode 100644 (file)
index 0000000..65f8878
--- /dev/null
@@ -0,0 +1,146 @@
+--[[
+LuCI - Lua Configuration Interface
+
+Copyright 2014 Nikos Mavrogiannopoulos <n.mavrogiannopoulos@gmail.com>
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+       http://www.apache.org/licenses/LICENSE-2.0
+
+$Id$
+local niulib = require "luci.niulib"
+]]--
+
+local fs = require "nixio.fs"
+local has_ipv6 = fs.access("/proc/net/ipv6_route")
+
+m = Map("ocserv", translate("OpenConnect VPN"))
+
+s = m:section(TypedSection, "ocserv", "OpenConnect")
+s.anonymous = true
+
+s:tab("general",  translate("General Settings"))
+s:tab("ca", translate("CA certificate"))
+s:tab("template", translate("Edit Template"))
+
+local e = s:taboption("general", Flag, "enable", translate("Enable server"))
+e.rmempty = false
+e.default = "1"
+
+function m.on_commit(map)
+       luci.sys.call("/usr/bin/occtl reload  >/dev/null 2>&1")
+end
+
+function e.write(self, section, value)
+       if value == "0" then
+               luci.sys.call("/etc/init.d/ocserv stop >/dev/null 2>&1")
+               luci.sys.call("/etc/init.d/ocserv disable  >/dev/null 2>&1")
+       else
+               luci.sys.call("/etc/init.d/ocserv enable  >/dev/null 2>&1")
+               luci.sys.call("/etc/init.d/ocserv restart  >/dev/null 2>&1")
+       end
+       Flag.write(self, section, value)
+end
+
+local o
+
+o = s:taboption("general", ListValue, "auth", translate("User Authentication"),
+       translate("The authentication method for the users. The simplest is plain with a single username-password pair. Use PAM modules to authenticate using another server (e.g., LDAP, Radius)."))
+o.rmempty = false
+o.default = "plain"
+o:value("plain")
+o:value("PAM")
+
+o = s:taboption("general", Value, "zone", translate("Firewall Zone"),
+       translate("The firewall zone that the VPN clients will be set to"))
+o.nocreate = true
+o.default = "lan"
+o.template = "cbi/firewall_zonelist"
+
+s:taboption("general", Value, "port", translate("Port"),
+       translate("The same UDP and TCP ports will be used"))
+s:taboption("general", Value, "max_clients", translate("Max clients"))
+s:taboption("general", Value, "max_same", translate("Max same clients"))
+s:taboption("general", Value, "dpd", translate("Dead peer detection time (secs)"))
+
+local pip = s:taboption("general", Flag, "predictable_ips", translate("Predictable IPs"),
+       translate("The assigned IPs will be selected deterministically"))
+pip.default = "1"
+
+local udp = s:taboption("general", Flag, "udp", translate("Enable UDP"),
+       translate("Enable UDP channel support; this must be enabled unless you know what you are doing"))
+udp.default = "1"
+
+local cisco = s:taboption("general", Flag, "cisco_compat", translate("AnyConnect client compatibility"),
+       translate("Enable support for CISCO AnyConnect clients"))
+cisco.default = "1"
+
+ipaddr = s:taboption("general", Value, "ipaddr", translate("VPN <abbr title=\"Internet Protocol Version 4\">IPv4</abbr>-Network-Address"))
+ipaddr.default = "192.168.100.1"
+
+nm = s:taboption("general", Value, "netmask", translate("VPN <abbr title=\"Internet Protocol Version 4\">IPv4</abbr>-Netmask"))
+nm.default = "255.255.255.0"
+nm:value("255.255.255.0")
+nm:value("255.255.0.0")
+nm:value("255.0.0.0")
+
+if has_ipv6 then
+       ip6addr = s:taboption("general", Value, "ip6addr", translate("VPN <abbr title=\"Internet Protocol Version 6\">IPv6</abbr>-Network-Address"), translate("<abbr title=\"Classless Inter-Domain Routing\">CIDR</abbr>-Notation: address/prefix"))
+end
+
+
+tmpl = s:taboption("template", Value, "_tmpl",
+       translate("Edit the template that is used for generating the ocserv configuration."))
+
+tmpl.template = "cbi/tvalue"
+tmpl.rows = 20
+
+function tmpl.cfgvalue(self, section)
+       return nixio.fs.readfile("/etc/ocserv/ocserv.conf.template")
+end
+
+function tmpl.write(self, section, value)
+       value = value:gsub("\r\n?", "\n")
+       nixio.fs.writefile("/etc/ocserv/ocserv.conf.template", value)
+end
+
+ca = s:taboption("ca", Value, "_ca",
+       translate("View the CA certificate used by this server. You will need to save it as 'ca.pem' and import it into the clients."))
+
+ca.template = "cbi/tvalue"
+ca.rows = 20
+
+function ca.cfgvalue(self, section)
+       return nixio.fs.readfile("/etc/ocserv/ca.pem")
+end
+
+--[[DNS]]--
+
+s = m:section(TypedSection, "dns", translate("DNS servers"),
+       translate("The DNS servers to be provided to clients; can be either IPv6 or IPv4"))
+s.anonymous = true
+s.addremove = true
+s.template = "cbi/tblsection"
+
+s:option(Value, "ip", translate("IP Address")).rmempty = true
+
+--[[Routes]]--
+
+s = m:section(TypedSection, "routes", translate("Routing table"),
+       translate("The routing table to be provided to clients; you can mix IPv4 and IPv6 routes, the server will send only the appropriate. Leave empty to set a default route"))
+s.anonymous = true
+s.addremove = true
+s.template = "cbi/tblsection"
+
+s:option(Value, "ip", translate("IP Address")).rmempty = true
+
+o = s:option(Value, "netmask", translate("Netmask (or IPv6-prefix)"))
+o.default = "255.255.255.0"
+o:value("255.255.255.0")
+o:value("255.255.0.0")
+o:value("255.0.0.0")
+
+
+return m
diff --git a/net/luci-app-ocserv/files/usr/lib/lua/luci/model/cbi/ocserv/users.lua b/net/luci-app-ocserv/files/usr/lib/lua/luci/model/cbi/ocserv/users.lua
new file mode 100644 (file)
index 0000000..0a4a678
--- /dev/null
@@ -0,0 +1,87 @@
+--[[
+LuCI - Lua Configuration Interface
+
+Copyright 2014 Nikos Mavrogiannopoulos <n.mavrogiannopoulos@gmail.com>
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+       http://www.apache.org/licenses/LICENSE-2.0
+
+$Id$
+]]--
+
+local dsp = require "luci.dispatcher"
+local nixio  = require "nixio"
+
+m = Map("ocserv", translate("OpenConnect VPN"))
+
+if m.uci:get("ocserv", "config", "auth") == "plain" then
+
+--[[Users]]--
+
+function m.on_commit(map)
+       luci.sys.call("/usr/bin/occtl reload  >/dev/null 2>&1")
+end
+
+s = m:section(TypedSection, "ocservusers", translate("Available users"))
+s.anonymous = true
+s.addremove = true
+s.template = "cbi/tblsection"
+
+s:option(Value, "name", translate("Name")).rmempty = true
+s:option(DummyValue, "group", translate("Group")).rmempty = true
+pwd = s:option(Value, "password", translate("Password"))
+pwd.password = false
+
+function pwd.write(self, section, value)
+       local pass
+       if string.match(value, "^\$%d\$.*") then
+               pass = value
+       else
+               local t = tonumber(nixio.getpid()*os.time())
+               local salt = "$5$" .. t .. "$"
+               pass = nixio.crypt(value, salt)
+       end
+       Value.write(self, section, pass)
+end    
+
+--[[if plain]]--
+end
+
+local lusers = { }
+local fd = io.popen("/usr/bin/occtl show users", "r")
+if fd then local ln
+       repeat
+               ln = fd:read("*l")
+               if not ln then break end
+
+               local id, user, group, vpn_ip, ip, device, time, cipher, status = 
+                       ln:match("^%s*(%d+)%s+([-_%w]+)%s+([%.%*-_%w]+)%s+([%:%.-_%w]+)%s+([%:%.-_%w]+)%s+([%:%.-_%w]+)%s+([%:%.-_%w]+)%s+([%:%.-_%w]+)%s+([%:%.-_%w]+).*")
+               if id then
+                       table.insert(lusers, {id, user, group, vpn_ip, ip, device, time, cipher, status})
+               end
+       until not ln
+       fd:close()
+end
+
+
+--[[Active Users]]--
+
+local s = m:section(Table, lusers, translate("Active users"))
+s.anonymous = true
+s.rmempty = true
+s.template = "cbi/tblsection"
+
+s:option(DummyValue, 1, translate("ID"))
+s:option(DummyValue, 2, translate("Username"))
+s:option(DummyValue, 3, translate("Group"))
+s:option(DummyValue, 4, translate("IP"))
+s:option(DummyValue, 5, translate("VPN IP"))
+s:option(DummyValue, 6, translate("Device"))
+s:option(DummyValue, 7, translate("Time"))
+s:option(DummyValue, 8, translate("Cipher"))
+s:option(DummyValue, 9, translate("Status"))
+
+return m
diff --git a/net/luci-app-ocserv/files/usr/lib/lua/luci/view/admin_status/index/ocserv.htm b/net/luci-app-ocserv/files/usr/lib/lua/luci/view/admin_status/index/ocserv.htm
new file mode 100644 (file)
index 0000000..4575806
--- /dev/null
@@ -0,0 +1 @@
+<%+ocserv_status%>
diff --git a/net/luci-app-ocserv/files/usr/lib/lua/luci/view/ocserv_status.htm b/net/luci-app-ocserv/files/usr/lib/lua/luci/view/ocserv_status.htm
new file mode 100644 (file)
index 0000000..fabc1bc
--- /dev/null
@@ -0,0 +1,76 @@
+<script type="text/javascript">//<![CDATA[
+
+       function ocserv_disconnect(idx) {
+               XHR.get('<%=luci.dispatcher.build_url("admin", "services", "ocserv", "disconnect")%>/' + idx, null,
+                       function(x)
+                       {
+                               var tb = document.getElementById('ocserv_status_table');
+                               if (tb && (idx < tb.rows.length))
+                                       tb.rows[0].parentNode.removeChild(tb.rows[idx]);
+                       }
+               );
+       }
+
+       XHR.poll(5, '<%=luci.dispatcher.build_url("admin", "services", "ocserv", "status")%>', null,
+               function(x, st)
+               {
+                       var tb = document.getElementById('ocserv_status_table');
+                       if (st && tb)
+                       {
+                               /* clear all rows */
+                               while( tb.rows.length > 1 )
+                                       tb.deleteRow(1);
+
+                               for( var i = 0; i < st.length; i++ )
+                               {
+                                       var tr = tb.insertRow(-1);
+                                               tr.className = 'cbi-section-table-row cbi-rowstyle-' + ((i % 2) + 1);
+
+                                       tr.insertCell(-1).innerHTML = st[i].user;
+                                       tr.insertCell(-1).innerHTML = st[i].group;
+                                       tr.insertCell(-1).innerHTML = st[i].vpn_ip;
+                                       tr.insertCell(-1).innerHTML = st[i].ip;
+                                       tr.insertCell(-1).innerHTML = st[i].device;
+                                       tr.insertCell(-1).innerHTML = st[i].time;
+                                       tr.insertCell(-1).innerHTML = st[i].cipher;
+                                       tr.insertCell(-1).innerHTML = st[i].status;
+
+                                       tr.insertCell(-1).innerHTML = String.format(
+                                               '<input class="cbi-button cbi-input-remove" type="button" value="<%:Disconnect%>" onclick="ocserv_disconnect(%d)" />',
+                                                       st[i].id
+                                       );
+                               }
+
+                               if( tb.rows.length == 1 )
+                               {
+                                       var tr = tb.insertRow(-1);
+                                               tr.className = 'cbi-section-table-row';
+
+                                       var td = tr.insertCell(-1);
+                                               td.colSpan = 5;
+                                               td.innerHTML = '<em><br /><%:There are no active users.%></em>';
+                               }
+                       }
+               }
+       );
+//]]></script>
+
+<fieldset class="cbi-section">
+       <legend><%:Active OpenConnect Users%></legend>
+       <table class="cbi-section-table" id="ocserv_status_table">
+               <tr class="cbi-section-table-titles">
+                       <th class="cbi-section-table-cell"><%:User%></th>
+                       <th class="cbi-section-table-cell"><%:Group%></th>
+                       <th class="cbi-section-table-cell"><%:IP Address%></th>
+                       <th class="cbi-section-table-cell"><%:VPN IP Address%></th>
+                       <th class="cbi-section-table-cell"><%:Device%></th>
+                       <th class="cbi-section-table-cell"><%:Time%></th>
+                       <th class="cbi-section-table-cell"><%:Cipher%></th>
+                       <th class="cbi-section-table-cell"><%:Status%></th>
+                       <th class="cbi-section-table-cell">&#160;</th>
+               </tr>
+               <tr class="cbi-section-table-row">
+                       <td colspan="5"><em><br /><%:Collecting data...%></em></td>
+               </tr>
+       </table>
+</fieldset>