Signed-off-by: Nikos Mavrogiannopoulos <nmav@gnutls.org>lilik-openwrt-22.03
@ -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)) | |||||
@ -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 |
@ -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 |
@ -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 |
@ -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 |
@ -0,0 +1 @@ | |||||
<%+ocserv_status%> |
@ -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"> </th> | |||||
</tr> | |||||
<tr class="cbi-section-table-row"> | |||||
<td colspan="5"><em><br /><%:Collecting data...%></em></td> | |||||
</tr> | |||||
</table> | |||||
</fieldset> |