Переглянути джерело

Added luci app for ocserv.

Signed-off-by: Nikos Mavrogiannopoulos <nmav@gnutls.org>
Nikos Mavrogiannopoulos 10 роки тому
джерело
коміт
c560ad9604

+ 57
- 0
net/luci-app-ocserv/Makefile Переглянути файл

@@ -0,0 +1,57 @@
1
+#    Copyright (C) 2014 Nikos Mavrogiannopoulos
2
+#
3
+#    This program is free software; you can redistribute it and/or modify
4
+#    it under the terms of the GNU General Public License as published by
5
+#    the Free Software Foundation; either version 2 of the License, or
6
+#    (at your option) any later version.
7
+#
8
+#    This program is distributed in the hope that it will be useful,
9
+#    but WITHOUT ANY WARRANTY; without even the implied warranty of
10
+#    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
11
+#    GNU General Public License for more details.
12
+#
13
+#    You should have received a copy of the GNU General Public License along
14
+#    with this program; if not, write to the Free Software Foundation, Inc.,
15
+#    51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
16
+#
17
+#    The full GNU General Public License is included in this distribution in
18
+#    the file called "COPYING".
19
+
20
+include $(TOPDIR)/rules.mk
21
+
22
+PKG_NAME:=luci-app-ocserv
23
+PKG_RELEASE:=1
24
+
25
+PKG_BUILD_DIR := $(BUILD_DIR)/$(PKG_NAME)
26
+
27
+include $(INCLUDE_DIR)/package.mk
28
+
29
+define Package/luci-app-ocserv
30
+  SECTION:=luci
31
+  CATEGORY:=LuCI
32
+  SUBMENU:=3. Applications
33
+  TITLE:= OpenConnect VPN server configuration and status module
34
+  DEPENDS:=+luci-lib-json +luci-mod-admin-core +ocserv
35
+  MAINTAINER:= Nikos Mavrogiannopoulos <n.mavrogiannopoulos@gmail.com>
36
+endef
37
+
38
+define Package/luci-app-ocserv/description
39
+	ocserv web module for LuCi web interface
40
+endef
41
+
42
+define Build/Prepare
43
+endef
44
+
45
+define Build/Configure
46
+endef
47
+
48
+define Build/Compile
49
+endef
50
+
51
+# Fixme: How can we add <%+ocserv_status%> in view/admin_status/index.htm?
52
+define Package/luci-app-ocserv/install
53
+	$(CP) ./files/* $(1)/
54
+endef
55
+
56
+$(eval $(call BuildPackage,luci-app-ocserv))
57
+

+ 90
- 0
net/luci-app-ocserv/files/usr/lib/lua/luci/controller/ocserv.lua Переглянути файл

@@ -0,0 +1,90 @@
1
+--[[
2
+LuCI - Lua Configuration Interface
3
+
4
+Copyright 2014 Nikos Mavrogiannopoulos <n.mavrogiannopoulos@gmail.com>
5
+
6
+Licensed under the Apache License, Version 2.0 (the "License");
7
+you may not use this file except in compliance with the License.
8
+You may obtain a copy of the License at
9
+
10
+	http://www.apache.org/licenses/LICENSE-2.0
11
+
12
+$Id$
13
+]]--
14
+
15
+module("luci.controller.ocserv", package.seeall)
16
+
17
+function index()
18
+	if not nixio.fs.access("/etc/config/ocserv") then
19
+		return
20
+	end
21
+
22
+	local page
23
+
24
+	page = entry({"admin", "services", "ocserv"}, alias("admin", "services", "ocserv", "main"),
25
+		_("OpenConnect VPN"))
26
+	page.dependent = true
27
+	
28
+	page = entry({"admin", "services", "ocserv", "main"},
29
+		cbi("ocserv/main"),
30
+		_("Server Settings"), 200)
31
+	page.dependent = true
32
+
33
+	page = entry({"admin", "services", "ocserv", "users"},
34
+		cbi("ocserv/users"),
35
+		_("User Settings"), 300)
36
+	page.dependent = true
37
+
38
+	entry({"admin", "services", "ocserv", "status"},
39
+		call("ocserv_status")).leaf = true
40
+
41
+	entry({"admin", "services", "ocserv", "disconnect"},
42
+		call("ocserv_disconnect")).leaf = true
43
+
44
+end
45
+
46
+function ocserv_status()
47
+	local ipt = io.popen("/usr/bin/occtl show users");
48
+
49
+	if ipt then
50
+
51
+		local fwd = { }
52
+		while true do
53
+
54
+			local ln = ipt:read("*l")
55
+			if not ln then break end
56
+		
57
+			local id, user, group, vpn_ip, ip, device, time, cipher, status = 
58
+				ln:match("^%s*(%d+)%s+([-_%w]+)%s+([%.%*-_%w]+)%s+([%:%.-_%w]+)%s+([%:%.-_%w]+)%s+([%:%.-_%w]+)%s+([%:%.-_%w]+)%s+([%:%.-_%w]+)%s+([%:%.-_%w]+).*")
59
+			if id then
60
+				fwd[#fwd+1] = {
61
+					id = id,
62
+					user = user,
63
+					group = group,
64
+					vpn_ip = vpn_ip,
65
+					ip = ip,
66
+					device = device,
67
+					time = time,
68
+					cipher = cipher,
69
+					status = status
70
+				}
71
+			end
72
+		end
73
+		ipt:close()
74
+		luci.http.prepare_content("application/json")
75
+		luci.http.write_json(fwd)
76
+	end
77
+end
78
+
79
+function ocserv_disconnect(num)
80
+	local idx = tonumber(num)
81
+	local uci = luci.model.uci.cursor()
82
+
83
+	if idx and idx > 0 then
84
+		luci.sys.call("/usr/bin/occtl disconnect id %d" % idx)
85
+		luci.http.status(200, "OK")
86
+
87
+		return
88
+	end
89
+	luci.http.status(400, "Bad request")
90
+end

+ 146
- 0
net/luci-app-ocserv/files/usr/lib/lua/luci/model/cbi/ocserv/main.lua Переглянути файл

@@ -0,0 +1,146 @@
1
+--[[
2
+LuCI - Lua Configuration Interface
3
+
4
+Copyright 2014 Nikos Mavrogiannopoulos <n.mavrogiannopoulos@gmail.com>
5
+
6
+Licensed under the Apache License, Version 2.0 (the "License");
7
+you may not use this file except in compliance with the License.
8
+You may obtain a copy of the License at
9
+
10
+	http://www.apache.org/licenses/LICENSE-2.0
11
+
12
+$Id$
13
+local niulib = require "luci.niulib"
14
+]]--
15
+
16
+local fs = require "nixio.fs"
17
+local has_ipv6 = fs.access("/proc/net/ipv6_route")
18
+
19
+m = Map("ocserv", translate("OpenConnect VPN"))
20
+
21
+s = m:section(TypedSection, "ocserv", "OpenConnect")
22
+s.anonymous = true
23
+
24
+s:tab("general",  translate("General Settings"))
25
+s:tab("ca", translate("CA certificate"))
26
+s:tab("template", translate("Edit Template"))
27
+
28
+local e = s:taboption("general", Flag, "enable", translate("Enable server"))
29
+e.rmempty = false
30
+e.default = "1"
31
+
32
+function m.on_commit(map)
33
+	luci.sys.call("/usr/bin/occtl reload  >/dev/null 2>&1")
34
+end
35
+
36
+function e.write(self, section, value)
37
+	if value == "0" then
38
+		luci.sys.call("/etc/init.d/ocserv stop >/dev/null 2>&1")
39
+		luci.sys.call("/etc/init.d/ocserv disable  >/dev/null 2>&1")
40
+	else
41
+		luci.sys.call("/etc/init.d/ocserv enable  >/dev/null 2>&1")
42
+		luci.sys.call("/etc/init.d/ocserv restart  >/dev/null 2>&1")
43
+	end
44
+	Flag.write(self, section, value)
45
+end
46
+
47
+local o
48
+
49
+o = s:taboption("general", ListValue, "auth", translate("User Authentication"),
50
+	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)."))
51
+o.rmempty = false
52
+o.default = "plain"
53
+o:value("plain")
54
+o:value("PAM")
55
+
56
+o = s:taboption("general", Value, "zone", translate("Firewall Zone"),
57
+	translate("The firewall zone that the VPN clients will be set to"))
58
+o.nocreate = true
59
+o.default = "lan"
60
+o.template = "cbi/firewall_zonelist"
61
+
62
+s:taboption("general", Value, "port", translate("Port"),
63
+	translate("The same UDP and TCP ports will be used"))
64
+s:taboption("general", Value, "max_clients", translate("Max clients"))
65
+s:taboption("general", Value, "max_same", translate("Max same clients"))
66
+s:taboption("general", Value, "dpd", translate("Dead peer detection time (secs)"))
67
+
68
+local pip = s:taboption("general", Flag, "predictable_ips", translate("Predictable IPs"),
69
+	translate("The assigned IPs will be selected deterministically"))
70
+pip.default = "1"
71
+
72
+local udp = s:taboption("general", Flag, "udp", translate("Enable UDP"),
73
+	translate("Enable UDP channel support; this must be enabled unless you know what you are doing"))
74
+udp.default = "1"
75
+
76
+local cisco = s:taboption("general", Flag, "cisco_compat", translate("AnyConnect client compatibility"),
77
+	translate("Enable support for CISCO AnyConnect clients"))
78
+cisco.default = "1"
79
+
80
+ipaddr = s:taboption("general", Value, "ipaddr", translate("VPN <abbr title=\"Internet Protocol Version 4\">IPv4</abbr>-Network-Address"))
81
+ipaddr.default = "192.168.100.1"
82
+
83
+nm = s:taboption("general", Value, "netmask", translate("VPN <abbr title=\"Internet Protocol Version 4\">IPv4</abbr>-Netmask"))
84
+nm.default = "255.255.255.0"
85
+nm:value("255.255.255.0")
86
+nm:value("255.255.0.0")
87
+nm:value("255.0.0.0")
88
+
89
+if has_ipv6 then
90
+	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"))
91
+end
92
+
93
+
94
+tmpl = s:taboption("template", Value, "_tmpl",
95
+	translate("Edit the template that is used for generating the ocserv configuration."))
96
+
97
+tmpl.template = "cbi/tvalue"
98
+tmpl.rows = 20
99
+
100
+function tmpl.cfgvalue(self, section)
101
+	return nixio.fs.readfile("/etc/ocserv/ocserv.conf.template")
102
+end
103
+
104
+function tmpl.write(self, section, value)
105
+	value = value:gsub("\r\n?", "\n")
106
+	nixio.fs.writefile("/etc/ocserv/ocserv.conf.template", value)
107
+end
108
+
109
+ca = s:taboption("ca", Value, "_ca",
110
+	translate("View the CA certificate used by this server. You will need to save it as 'ca.pem' and import it into the clients."))
111
+
112
+ca.template = "cbi/tvalue"
113
+ca.rows = 20
114
+
115
+function ca.cfgvalue(self, section)
116
+	return nixio.fs.readfile("/etc/ocserv/ca.pem")
117
+end
118
+
119
+--[[DNS]]--
120
+
121
+s = m:section(TypedSection, "dns", translate("DNS servers"),
122
+	translate("The DNS servers to be provided to clients; can be either IPv6 or IPv4"))
123
+s.anonymous = true
124
+s.addremove = true
125
+s.template = "cbi/tblsection"
126
+
127
+s:option(Value, "ip", translate("IP Address")).rmempty = true
128
+
129
+--[[Routes]]--
130
+
131
+s = m:section(TypedSection, "routes", translate("Routing table"),
132
+	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"))
133
+s.anonymous = true
134
+s.addremove = true
135
+s.template = "cbi/tblsection"
136
+
137
+s:option(Value, "ip", translate("IP Address")).rmempty = true
138
+
139
+o = s:option(Value, "netmask", translate("Netmask (or IPv6-prefix)"))
140
+o.default = "255.255.255.0"
141
+o:value("255.255.255.0")
142
+o:value("255.255.0.0")
143
+o:value("255.0.0.0")
144
+
145
+
146
+return m

+ 146
- 0
net/luci-app-ocserv/files/usr/lib/lua/luci/model/cbi/ocserv/user-config.lua Переглянути файл

@@ -0,0 +1,146 @@
1
+--[[
2
+LuCI - Lua Configuration Interface
3
+
4
+Copyright 2014 Nikos Mavrogiannopoulos <n.mavrogiannopoulos@gmail.com>
5
+
6
+Licensed under the Apache License, Version 2.0 (the "License");
7
+you may not use this file except in compliance with the License.
8
+You may obtain a copy of the License at
9
+
10
+	http://www.apache.org/licenses/LICENSE-2.0
11
+
12
+$Id$
13
+local niulib = require "luci.niulib"
14
+]]--
15
+
16
+local fs = require "nixio.fs"
17
+local has_ipv6 = fs.access("/proc/net/ipv6_route")
18
+
19
+m = Map("ocserv", translate("OpenConnect VPN"))
20
+
21
+s = m:section(TypedSection, "ocserv", "OpenConnect")
22
+s.anonymous = true
23
+
24
+s:tab("general",  translate("General Settings"))
25
+s:tab("ca", translate("CA certificate"))
26
+s:tab("template", translate("Edit Template"))
27
+
28
+local e = s:taboption("general", Flag, "enable", translate("Enable server"))
29
+e.rmempty = false
30
+e.default = "1"
31
+
32
+function m.on_commit(map)
33
+	luci.sys.call("/usr/bin/occtl reload  >/dev/null 2>&1")
34
+end
35
+
36
+function e.write(self, section, value)
37
+	if value == "0" then
38
+		luci.sys.call("/etc/init.d/ocserv stop >/dev/null 2>&1")
39
+		luci.sys.call("/etc/init.d/ocserv disable  >/dev/null 2>&1")
40
+	else
41
+		luci.sys.call("/etc/init.d/ocserv enable  >/dev/null 2>&1")
42
+		luci.sys.call("/etc/init.d/ocserv restart  >/dev/null 2>&1")
43
+	end
44
+	Flag.write(self, section, value)
45
+end
46
+
47
+local o
48
+
49
+o = s:taboption("general", ListValue, "auth", translate("User Authentication"),
50
+	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)."))
51
+o.rmempty = false
52
+o.default = "plain"
53
+o:value("plain")
54
+o:value("PAM")
55
+
56
+o = s:taboption("general", Value, "zone", translate("Firewall Zone"),
57
+	translate("The firewall zone that the VPN clients will be set to"))
58
+o.nocreate = true
59
+o.default = "lan"
60
+o.template = "cbi/firewall_zonelist"
61
+
62
+s:taboption("general", Value, "port", translate("Port"),
63
+	translate("The same UDP and TCP ports will be used"))
64
+s:taboption("general", Value, "max_clients", translate("Max clients"))
65
+s:taboption("general", Value, "max_same", translate("Max same clients"))
66
+s:taboption("general", Value, "dpd", translate("Dead peer detection time (secs)"))
67
+
68
+local pip = s:taboption("general", Flag, "predictable_ips", translate("Predictable IPs"),
69
+	translate("The assigned IPs will be selected deterministically"))
70
+pip.default = "1"
71
+
72
+local udp = s:taboption("general", Flag, "udp", translate("Enable UDP"),
73
+	translate("Enable UDP channel support; this must be enabled unless you know what you are doing"))
74
+udp.default = "1"
75
+
76
+local cisco = s:taboption("general", Flag, "cisco_compat", translate("AnyConnect client compatibility"),
77
+	translate("Enable support for CISCO AnyConnect clients"))
78
+cisco.default = "1"
79
+
80
+ipaddr = s:taboption("general", Value, "ipaddr", translate("VPN <abbr title=\"Internet Protocol Version 4\">IPv4</abbr>-Network-Address"))
81
+ipaddr.default = "192.168.100.1"
82
+
83
+nm = s:taboption("general", Value, "netmask", translate("VPN <abbr title=\"Internet Protocol Version 4\">IPv4</abbr>-Netmask"))
84
+nm.default = "255.255.255.0"
85
+nm:value("255.255.255.0")
86
+nm:value("255.255.0.0")
87
+nm:value("255.0.0.0")
88
+
89
+if has_ipv6 then
90
+	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"))
91
+end
92
+
93
+
94
+tmpl = s:taboption("template", Value, "_tmpl",
95
+	translate("Edit the template that is used for generating the ocserv configuration."))
96
+
97
+tmpl.template = "cbi/tvalue"
98
+tmpl.rows = 20
99
+
100
+function tmpl.cfgvalue(self, section)
101
+	return nixio.fs.readfile("/etc/ocserv/ocserv.conf.template")
102
+end
103
+
104
+function tmpl.write(self, section, value)
105
+	value = value:gsub("\r\n?", "\n")
106
+	nixio.fs.writefile("/etc/ocserv/ocserv.conf.template", value)
107
+end
108
+
109
+ca = s:taboption("ca", Value, "_ca",
110
+	translate("View the CA certificate used by this server. You will need to save it as 'ca.pem' and import it into the clients."))
111
+
112
+ca.template = "cbi/tvalue"
113
+ca.rows = 20
114
+
115
+function ca.cfgvalue(self, section)
116
+	return nixio.fs.readfile("/etc/ocserv/ca.pem")
117
+end
118
+
119
+--[[DNS]]--
120
+
121
+s = m:section(TypedSection, "dns", translate("DNS servers"),
122
+	translate("The DNS servers to be provided to clients; can be either IPv6 or IPv4"))
123
+s.anonymous = true
124
+s.addremove = true
125
+s.template = "cbi/tblsection"
126
+
127
+s:option(Value, "ip", translate("IP Address")).rmempty = true
128
+
129
+--[[Routes]]--
130
+
131
+s = m:section(TypedSection, "routes", translate("Routing table"),
132
+	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"))
133
+s.anonymous = true
134
+s.addremove = true
135
+s.template = "cbi/tblsection"
136
+
137
+s:option(Value, "ip", translate("IP Address")).rmempty = true
138
+
139
+o = s:option(Value, "netmask", translate("Netmask (or IPv6-prefix)"))
140
+o.default = "255.255.255.0"
141
+o:value("255.255.255.0")
142
+o:value("255.255.0.0")
143
+o:value("255.0.0.0")
144
+
145
+
146
+return m

+ 87
- 0
net/luci-app-ocserv/files/usr/lib/lua/luci/model/cbi/ocserv/users.lua Переглянути файл

@@ -0,0 +1,87 @@
1
+--[[
2
+LuCI - Lua Configuration Interface
3
+
4
+Copyright 2014 Nikos Mavrogiannopoulos <n.mavrogiannopoulos@gmail.com>
5
+
6
+Licensed under the Apache License, Version 2.0 (the "License");
7
+you may not use this file except in compliance with the License.
8
+You may obtain a copy of the License at
9
+
10
+	http://www.apache.org/licenses/LICENSE-2.0
11
+
12
+$Id$
13
+]]--
14
+
15
+local dsp = require "luci.dispatcher"
16
+local nixio  = require "nixio"
17
+
18
+m = Map("ocserv", translate("OpenConnect VPN"))
19
+
20
+if m.uci:get("ocserv", "config", "auth") == "plain" then
21
+
22
+--[[Users]]--
23
+
24
+function m.on_commit(map)
25
+	luci.sys.call("/usr/bin/occtl reload  >/dev/null 2>&1")
26
+end
27
+
28
+s = m:section(TypedSection, "ocservusers", translate("Available users"))
29
+s.anonymous = true
30
+s.addremove = true
31
+s.template = "cbi/tblsection"
32
+
33
+s:option(Value, "name", translate("Name")).rmempty = true
34
+s:option(DummyValue, "group", translate("Group")).rmempty = true
35
+pwd = s:option(Value, "password", translate("Password"))
36
+pwd.password = false
37
+
38
+function pwd.write(self, section, value)
39
+	local pass
40
+	if string.match(value, "^\$%d\$.*") then
41
+		pass = value
42
+	else
43
+		local t = tonumber(nixio.getpid()*os.time())
44
+		local salt = "$5$" .. t .. "$"
45
+		pass = nixio.crypt(value, salt)
46
+	end
47
+	Value.write(self, section, pass)
48
+end	
49
+
50
+--[[if plain]]--
51
+end
52
+
53
+local lusers = { }
54
+local fd = io.popen("/usr/bin/occtl show users", "r")
55
+if fd then local ln
56
+	repeat
57
+		ln = fd:read("*l")
58
+		if not ln then break end
59
+
60
+		local id, user, group, vpn_ip, ip, device, time, cipher, status = 
61
+			ln:match("^%s*(%d+)%s+([-_%w]+)%s+([%.%*-_%w]+)%s+([%:%.-_%w]+)%s+([%:%.-_%w]+)%s+([%:%.-_%w]+)%s+([%:%.-_%w]+)%s+([%:%.-_%w]+)%s+([%:%.-_%w]+).*")
62
+		if id then
63
+			table.insert(lusers, {id, user, group, vpn_ip, ip, device, time, cipher, status})
64
+		end
65
+	until not ln
66
+	fd:close()
67
+end
68
+
69
+
70
+--[[Active Users]]--
71
+
72
+local s = m:section(Table, lusers, translate("Active users"))
73
+s.anonymous = true
74
+s.rmempty = true
75
+s.template = "cbi/tblsection"
76
+
77
+s:option(DummyValue, 1, translate("ID"))
78
+s:option(DummyValue, 2, translate("Username"))
79
+s:option(DummyValue, 3, translate("Group"))
80
+s:option(DummyValue, 4, translate("IP"))
81
+s:option(DummyValue, 5, translate("VPN IP"))
82
+s:option(DummyValue, 6, translate("Device"))
83
+s:option(DummyValue, 7, translate("Time"))
84
+s:option(DummyValue, 8, translate("Cipher"))
85
+s:option(DummyValue, 9, translate("Status"))
86
+
87
+return m

+ 1
- 0
net/luci-app-ocserv/files/usr/lib/lua/luci/view/admin_status/index/ocserv.htm Переглянути файл

@@ -0,0 +1 @@
1
+<%+ocserv_status%>

+ 76
- 0
net/luci-app-ocserv/files/usr/lib/lua/luci/view/ocserv_status.htm Переглянути файл

@@ -0,0 +1,76 @@
1
+<script type="text/javascript">//<![CDATA[
2
+
3
+	function ocserv_disconnect(idx) {
4
+		XHR.get('<%=luci.dispatcher.build_url("admin", "services", "ocserv", "disconnect")%>/' + idx, null,
5
+			function(x)
6
+			{
7
+				var tb = document.getElementById('ocserv_status_table');
8
+				if (tb && (idx < tb.rows.length))
9
+					tb.rows[0].parentNode.removeChild(tb.rows[idx]);
10
+			}
11
+		);
12
+	}
13
+
14
+	XHR.poll(5, '<%=luci.dispatcher.build_url("admin", "services", "ocserv", "status")%>', null,
15
+		function(x, st)
16
+		{
17
+			var tb = document.getElementById('ocserv_status_table');
18
+			if (st && tb)
19
+			{
20
+				/* clear all rows */
21
+				while( tb.rows.length > 1 )
22
+					tb.deleteRow(1);
23
+
24
+				for( var i = 0; i < st.length; i++ )
25
+				{
26
+					var tr = tb.insertRow(-1);
27
+						tr.className = 'cbi-section-table-row cbi-rowstyle-' + ((i % 2) + 1);
28
+
29
+					tr.insertCell(-1).innerHTML = st[i].user;
30
+					tr.insertCell(-1).innerHTML = st[i].group;
31
+					tr.insertCell(-1).innerHTML = st[i].vpn_ip;
32
+					tr.insertCell(-1).innerHTML = st[i].ip;
33
+					tr.insertCell(-1).innerHTML = st[i].device;
34
+					tr.insertCell(-1).innerHTML = st[i].time;
35
+					tr.insertCell(-1).innerHTML = st[i].cipher;
36
+					tr.insertCell(-1).innerHTML = st[i].status;
37
+
38
+					tr.insertCell(-1).innerHTML = String.format(
39
+						'<input class="cbi-button cbi-input-remove" type="button" value="<%:Disconnect%>" onclick="ocserv_disconnect(%d)" />',
40
+							st[i].id
41
+					);
42
+				}
43
+
44
+				if( tb.rows.length == 1 )
45
+				{
46
+					var tr = tb.insertRow(-1);
47
+						tr.className = 'cbi-section-table-row';
48
+
49
+					var td = tr.insertCell(-1);
50
+						td.colSpan = 5;
51
+						td.innerHTML = '<em><br /><%:There are no active users.%></em>';
52
+				}
53
+			}
54
+		}
55
+	);
56
+//]]></script>
57
+
58
+<fieldset class="cbi-section">
59
+	<legend><%:Active OpenConnect Users%></legend>
60
+	<table class="cbi-section-table" id="ocserv_status_table">
61
+		<tr class="cbi-section-table-titles">
62
+			<th class="cbi-section-table-cell"><%:User%></th>
63
+			<th class="cbi-section-table-cell"><%:Group%></th>
64
+			<th class="cbi-section-table-cell"><%:IP Address%></th>
65
+			<th class="cbi-section-table-cell"><%:VPN IP Address%></th>
66
+			<th class="cbi-section-table-cell"><%:Device%></th>
67
+			<th class="cbi-section-table-cell"><%:Time%></th>
68
+			<th class="cbi-section-table-cell"><%:Cipher%></th>
69
+			<th class="cbi-section-table-cell"><%:Status%></th>
70
+			<th class="cbi-section-table-cell">&#160;</th>
71
+		</tr>
72
+		<tr class="cbi-section-table-row">
73
+			<td colspan="5"><em><br /><%:Collecting data...%></em></td>
74
+		</tr>
75
+	</table>
76
+</fieldset>