luci-app-cjdns: import package from SeattleMeshnet/meshbox 90/head
authorDaniel Golle <daniel@makrotopia.org>
Mon, 20 Apr 2015 10:45:07 +0000 (12:45 +0200)
committerDaniel Golle <daniel@makrotopia.org>
Mon, 20 Apr 2015 10:45:07 +0000 (12:45 +0200)
Signed-off-by: Daniel Golle <daniel@makrotopia.org>
luci-app-cjdns/Makefile [new file with mode: 0644]
luci-app-cjdns/luasrc/controller/cjdns.lua [new file with mode: 0644]
luci-app-cjdns/luasrc/model/cbi/cjdns/cjdrouteconf.lua [new file with mode: 0644]
luci-app-cjdns/luasrc/model/cbi/cjdns/iptunnel.lua [new file with mode: 0644]
luci-app-cjdns/luasrc/model/cbi/cjdns/overview.lua [new file with mode: 0644]
luci-app-cjdns/luasrc/model/cbi/cjdns/peering.lua [new file with mode: 0644]
luci-app-cjdns/luasrc/model/cbi/cjdns/settings.lua [new file with mode: 0644]
luci-app-cjdns/luasrc/view/admin_status/index/cjdns.htm [new file with mode: 0644]
luci-app-cjdns/luasrc/view/cjdns/status.htm [new file with mode: 0644]
luci-app-cjdns/luasrc/view/cjdns/value.htm [new file with mode: 0644]

diff --git a/luci-app-cjdns/Makefile b/luci-app-cjdns/Makefile
new file mode 100644 (file)
index 0000000..34f18c9
--- /dev/null
@@ -0,0 +1,38 @@
+include $(TOPDIR)/rules.mk
+
+PKG_NAME:=luci-app-cjdns
+PKG_VERSION:=1.3
+PKG_RELEASE:=4
+
+PKG_LICENSE:=GPL-3.0
+
+include $(INCLUDE_DIR)/package.mk
+
+define Package/luci-app-cjdns
+       SECTION:=luci
+       CATEGORY:=LuCI
+       SUBMENU:=3. Applications
+       TITLE:=Encrypted near-zero-conf mesh routing protocol
+       URL:=https://github.com/hyperboria/cjdns
+       MAINTAINER:=Lars Gierth <larsg@systemli.org>
+       DEPENDS:=+cjdns +luci-base
+endef
+
+define Package/luci-app-cjdns/description
+       This package allows you to configure and inspect cjdns networking using LuCI.
+
+       Cjdns implements an encrypted IPv6 network using public-key cryptography
+       for address allocation and a distributed hash table for routing.
+       This provides near-zero-configuration networking, and prevents many
+       of the security and scalability issues that plague existing networks.
+endef
+
+define Build/Compile
+endef
+
+define Package/luci-app-cjdns/install
+       $(INSTALL_DIR) $(1)/usr/lib/lua/luci
+       $(CP) ./luasrc/* $(1)/usr/lib/lua/luci
+endef
+
+$(eval $(call BuildPackage,luci-app-cjdns))
diff --git a/luci-app-cjdns/luasrc/controller/cjdns.lua b/luci-app-cjdns/luasrc/controller/cjdns.lua
new file mode 100644 (file)
index 0000000..63644cb
--- /dev/null
@@ -0,0 +1,105 @@
+module("luci.controller.cjdns", package.seeall)
+
+cjdns  = require "cjdns/init"
+dkjson = require "dkjson"
+
+function index()
+       if not nixio.fs.access("/etc/config/cjdns") then
+               return
+       end
+
+       entry({"admin", "services", "cjdns"},
+               cbi("cjdns/overview"), _("cjdns")).dependent = true
+
+       entry({"admin", "services", "cjdns", "overview"},
+               cbi("cjdns/overview"), _("Overview"), 1).leaf = false
+
+       entry({"admin", "services", "cjdns", "peering"},
+               cbi("cjdns/peering"), _("Peers"), 2).leaf = false
+
+       entry({"admin", "services", "cjdns", "iptunnel"},
+               cbi("cjdns/iptunnel"), _("IP Tunnel"), 3).leaf = false
+
+       entry({"admin", "services", "cjdns", "settings"},
+               cbi("cjdns/settings"), _("Settings"), 4).leaf = false
+
+       entry({"admin", "services", "cjdns", "cjdrouteconf"},
+               cbi("cjdns/cjdrouteconf"), _("cjdroute.conf"), 5).leaf = false
+
+       entry({"admin", "services", "cjdns", "peers"}, call("act_peers")).leaf = true
+       entry({"admin", "services", "cjdns", "ping"}, call("act_ping")).leaf = true
+end
+
+function act_peers()
+       require("cjdns/uci")
+       admin = cjdns.uci.makeInterface()
+
+       local page = 0
+       local peers = {}
+
+       while page do
+               local response, err = admin:auth({
+                       q = "InterfaceController_peerStats",
+                       page = page
+               })
+
+               if err or response.error then
+                       luci.http.status(502, "Bad Gateway")
+                       luci.http.prepare_content("application/json")
+                       luci.http.write_json({ err = err, response = response })
+                       return
+               end
+
+               for i,peer in pairs(response.peers) do
+                       peer.ipv6 = publictoip6(peer.publicKey)
+                       if peer.user == nil then
+                               peer.user = ''
+                               uci.cursor():foreach("cjdns", "udp_peer", function(udp_peer)
+                                       if peer.publicKey == udp_peer.public_key then
+                                               peer.user = udp_peer.user
+                                       end
+                               end)
+                       end
+                       peers[#peers + 1] = peer
+               end
+
+               if response.more then
+                       page = page + 1
+               else
+                       page = nil
+               end
+       end
+
+       luci.http.status(200, "OK")
+       luci.http.prepare_content("application/json")
+       luci.http.write_json(peers)
+end
+
+function act_ping()
+       require("cjdns/uci")
+       admin = cjdns.uci.makeInterface()
+
+       local response, err = admin:auth({
+               q = "SwitchPinger_ping",
+               path = luci.http.formvalue("label"),
+               timeout = tonumber(luci.http.formvalue("timeout"))
+       })
+
+       if err or response.error then
+               luci.http.status(502, "Bad Gateway")
+               luci.http.prepare_content("application/json")
+               luci.http.write_json({ err = err, response = response })
+               return
+       end
+
+       luci.http.status(200, "OK")
+       luci.http.prepare_content("application/json")
+       luci.http.write_json(response)
+end
+
+function publictoip6(publicKey)
+       local process = io.popen("/usr/bin/publictoip6 " .. publicKey, "r")
+       local ipv6    = process:read()
+       process:close()
+       return ipv6
+end
diff --git a/luci-app-cjdns/luasrc/model/cbi/cjdns/cjdrouteconf.lua b/luci-app-cjdns/luasrc/model/cbi/cjdns/cjdrouteconf.lua
new file mode 100644 (file)
index 0000000..00e9ae0
--- /dev/null
@@ -0,0 +1,32 @@
+m = Map("cjdns", translate("cjdns"),
+  translate("Implements an encrypted IPv6 network using public-key \
+    cryptography for address allocation and a distributed hash table for \
+    routing. This provides near-zero-configuration networking, and prevents \
+    many of the security and scalability issues that plague existing \
+    networks."))
+
+dkjson = require("dkjson")
+cjdns = require("cjdns")
+require("cjdns/uci")
+
+local f = SimpleForm("cjdrouteconf", translate("Edit cjdroute.conf"),
+       translate("JSON interface to what's /etc/cjdroute.conf on other systems. \
+    Will be parsed and written to UCI by <code>cjdrouteconf set</code>."))
+
+local o = f:field(Value, "_cjdrouteconf")
+o.template = "cbi/tvalue"
+o.rows = 25
+
+function o.cfgvalue(self, section)
+       return dkjson.encode(cjdns.uci.get(), { indent = true })
+end
+
+function o.write(self, section, value)
+  local obj, pos, err = dkjson.decode(value, 1, nil)
+
+  if obj then
+    cjdns.uci.set(obj)
+  end
+end
+
+return f
diff --git a/luci-app-cjdns/luasrc/model/cbi/cjdns/iptunnel.lua b/luci-app-cjdns/luasrc/model/cbi/cjdns/iptunnel.lua
new file mode 100644 (file)
index 0000000..02b37dd
--- /dev/null
@@ -0,0 +1,46 @@
+uci = require "luci.model.uci"
+cursor = uci:cursor_state()
+
+m = Map("cjdns", translate("cjdns"),
+  translate("Implements an encrypted IPv6 network using public-key \
+    cryptography for address allocation and a distributed hash table for \
+    routing. This provides near-zero-configuration networking, and prevents \
+    many of the security and scalability issues that plague existing \
+    networks."))
+
+m.on_after_commit = function(self)
+  os.execute("/etc/init.d/cjdns restart")
+end
+
+-- Outgoing
+outgoing = m:section(TypedSection, "iptunnel_outgoing", translate("Outgoing IP Tunnel Connections"),
+  translate("Enter the public keys of the nodes that will provide Internet access."))
+outgoing.anonymous = true
+outgoing.addremove = true
+outgoing.template  = "cbi/tblsection"
+
+outgoing:option(Value, "public_key", translate("Public Key")).size = 55
+
+-- Allowed
+allowed = m:section(TypedSection, "iptunnel_allowed", translate("Allowed IP Tunnel Connections"),
+  translate("Enter the public key of the node you will provide Internet access to, along with the \
+             IPv4 and/or IPv6 address you will assign them."))
+allowed.anonymous = true
+allowed.addremove = true
+
+public_key = allowed:option(Value, "public_key", translate("Public Key"))
+public_key.template = "cjdns/value"
+public_key.size = 55
+
+ipv4 = allowed:option(Value, "ipv4", translate("IPv4"))
+ipv4.template = "cjdns/value"
+ipv4.datatype = 'ipaddr'
+ipv4.size = 55
+
+ipv6 = allowed:option(Value, "ipv6", translate("IPv6"),
+  translate("IPv6 addresses should be entered <em>without</em> brackets here, e.g. <code>2001:123:ab::10</code>."))
+ipv6.template = "cjdns/value"
+ipv6.datatype = 'ip6addr'
+ipv6.size = 55
+
+return m
diff --git a/luci-app-cjdns/luasrc/model/cbi/cjdns/overview.lua b/luci-app-cjdns/luasrc/model/cbi/cjdns/overview.lua
new file mode 100644 (file)
index 0000000..efa3a03
--- /dev/null
@@ -0,0 +1,10 @@
+m = Map("cjdns", translate("cjdns"),
+  translate("Implements an encrypted IPv6 network using public-key \
+    cryptography for address allocation and a distributed hash table for \
+    routing. This provides near-zero-configuration networking, and prevents \
+    many of the security and scalability issues that plague existing \
+    networks."))
+
+m:section(SimpleSection).template  = "cjdns/status"
+
+return m
diff --git a/luci-app-cjdns/luasrc/model/cbi/cjdns/peering.lua b/luci-app-cjdns/luasrc/model/cbi/cjdns/peering.lua
new file mode 100644 (file)
index 0000000..2b1fc1b
--- /dev/null
@@ -0,0 +1,73 @@
+uci = require "luci.model.uci"
+cursor = uci:cursor_state()
+
+cjdns = require("cjdns")
+require("cjdns/uci")
+
+m = Map("cjdns", translate("cjdns"),
+  translate("Implements an encrypted IPv6 network using public-key \
+    cryptography for address allocation and a distributed hash table for \
+    routing. This provides near-zero-configuration networking, and prevents \
+    many of the security and scalability issues that plague existing \
+    networks."))
+
+m.on_after_commit = function(self)
+  os.execute("/etc/init.d/cjdns restart")
+end
+
+-- Authorized Passwords
+passwords = m:section(TypedSection, "password", translate("Authorized Passwords"),
+  translate("Anyone offering one of the these passwords will be allowed to peer with you on the existing UDP and Ethernet interfaces."))
+passwords.anonymous = true
+passwords.addremove = true
+passwords.template  = "cbi/tblsection"
+
+passwords:option(Value, "user", translate("User/Name"),
+  translate("Must be unique.")
+).default = "user-" .. cjdns.uci.random_string(6)
+passwords:option(Value, "contact", translate("Contact"), translate("Optional, for out-of-band communication."))
+passwords:option(Value, "password", translate("Password"),
+  translate("Hand out to your peer, in accordance with the peering best practices of the network.")
+).default = cjdns.uci.random_string(32)
+
+-- UDP Peers
+udp_peers = m:section(TypedSection, "udp_peer", translate("Outgoing UDP Peers"),
+  translate("For peering via public IP networks, the peer handed you their Public Key and IP address/port along with a password. IPv6 addresses should be entered with square brackets, like so: <code>[2001::1]</code>."))
+udp_peers.anonymous = true
+udp_peers.addremove = true
+udp_peers.template  = "cbi/tblsection"
+udp_peers:option(Value, "user", translate("User/Name")).datatype = "string"
+
+udp_interface = udp_peers:option(Value, "interface", translate("UDP interface"))
+local index = 1
+for i,section in pairs(cursor:get_all("cjdns")) do
+  if section[".type"] == "udp_interface" then
+    udp_interface:value(index, section.address .. ":" .. section.port)
+  end
+end
+udp_interface.default = 1
+udp_peers:option(Value, "address", translate("IP address"))
+udp_peers:option(Value, "port", translate("Port")).datatype = "portrange"
+udp_peers:option(Value, "public_key", translate("Public key"))
+udp_peers:option(Value, "password", translate("Password"))
+
+-- Ethernet Peers
+eth_peers = m:section(TypedSection, "eth_peer", translate("Outgoing Ethernet Peers"),
+  translate("For peering via local Ethernet networks, the peer handed you their Public Key and MAC address along with a password."))
+eth_peers.anonymous = true
+eth_peers.addremove = true
+eth_peers.template  = "cbi/tblsection"
+
+eth_interface = eth_peers:option(Value, "interface", translate("Ethernet interface"))
+local index = 1
+for i,section in pairs(cursor:get_all("cjdns")) do
+  if section[".type"] == "eth_interface" then
+    eth_interface:value(index, section.bind)
+  end
+end
+eth_interface.default = 1
+eth_peers:option(Value, "address", translate("MAC address")).datatype = "macaddr"
+eth_peers:option(Value, "public_key", translate("Public key"))
+eth_peers:option(Value, "password", translate("Password"))
+
+return m
diff --git a/luci-app-cjdns/luasrc/model/cbi/cjdns/settings.lua b/luci-app-cjdns/luasrc/model/cbi/cjdns/settings.lua
new file mode 100644 (file)
index 0000000..bbe89df
--- /dev/null
@@ -0,0 +1,63 @@
+m = Map("cjdns", translate("cjdns"),
+  translate("Implements an encrypted IPv6 network using public-key \
+    cryptography for address allocation and a distributed hash table for \
+    routing. This provides near-zero-configuration networking, and prevents \
+    many of the security and scalability issues that plague existing \
+    networks."))
+
+m.on_after_commit = function(self)
+  os.execute("/etc/init.d/cjdns restart")
+end
+
+s = m:section(NamedSection, "cjdns", nil, translate("Settings"))
+s.addremove = false
+
+-- Identity
+s:tab("identity", translate("Identity"))
+node6 = s:taboption("identity", Value, "ipv6", translate("IPv6 address"),
+      translate("This node's IPv6 address within the cjdns network."))
+node6.datatype = "ip6addr"
+pbkey = s:taboption("identity", Value, "public_key", translate("Public key"),
+      translate("Used for packet encryption and authentication."))
+pbkey.datatype = "string"
+prkey = s:taboption("identity", Value, "private_key", translate("Private key"),
+      translate("Keep this private. When compromised, generate a new keypair and IPv6."))
+prkey.datatype = "string"
+
+-- Admin Interface
+s:tab("admin", translate("Admin API"), translate("The Admin API can be used by other applications or services to configure and inspect cjdns' routing and peering.<br/><br/>Documentation: <a href=\"https://github.com/cjdelisle/cjdns/tree/master/admin#cjdns-admin-api\">admin/README.md</a>"))
+aip = s:taboption("admin", Value, "admin_address", translate("IP Address"),
+      translate("IPv6 addresses should be entered like so: <code>[2001::1]</code>."))
+apt = s:taboption("admin", Value, "admin_port", translate("Port"))
+apt.datatype = "port"
+apw = s:taboption("admin", Value, "admin_password", translate("Password"))
+apw.datatype = "string"
+
+-- UDP Interfaces
+udp_interfaces = m:section(TypedSection, "udp_interface", translate("UDP Interfaces"),
+  translate("These interfaces allow peering via public IP networks, such as the Internet, or many community-operated wireless networks. IPv6 addresses should be entered with square brackets, like so: <code>[2001::1]</code>."))
+udp_interfaces.anonymous = true
+udp_interfaces.addremove = true
+udp_interfaces.template = "cbi/tblsection"
+
+udp_address = udp_interfaces:option(Value, "address", translate("IP Address"))
+udp_address.placeholder = "0.0.0.0"
+udp_interfaces:option(Value, "port", translate("Port")).datatype = "portrange"
+
+-- Ethernet Interfaces
+eth_interfaces = m:section(TypedSection, "eth_interface", translate("Ethernet Interfaces"),
+  translate("These interfaces allow peering via local Ethernet networks, such as home or office networks, or phone tethering. If an interface name is set to \"all\" each available device will be used."))
+eth_interfaces.anonymous = true
+eth_interfaces.addremove = true
+eth_interfaces.template = "cbi/tblsection"
+
+eth_bind = eth_interfaces:option(Value, "bind", translate("Network Interface"))
+eth_bind.placeholder = "br-lan"
+eth_beacon = eth_interfaces:option(Value, "beacon", translate("Beacon Mode"))
+eth_beacon:value(0, translate("0 -- Disabled"))
+eth_beacon:value(1, translate("1 -- Accept beacons"))
+eth_beacon:value(2, translate("2 -- Accept and send beacons"))
+eth_beacon.default = 2
+eth_beacon.datatype = "integer(range(0,2))"
+
+return m
diff --git a/luci-app-cjdns/luasrc/view/admin_status/index/cjdns.htm b/luci-app-cjdns/luasrc/view/admin_status/index/cjdns.htm
new file mode 100644 (file)
index 0000000..58c3843
--- /dev/null
@@ -0,0 +1 @@
+<%+cjdns/status%>
diff --git a/luci-app-cjdns/luasrc/view/cjdns/status.htm b/luci-app-cjdns/luasrc/view/cjdns/status.htm
new file mode 100644 (file)
index 0000000..9d43e85
--- /dev/null
@@ -0,0 +1,116 @@
+<script type="text/javascript">//<![CDATA[
+
+       var peersURI = '<%=luci.dispatcher.build_url("admin", "services", "cjdns", "peers")%>';
+       var updatePeers = function(x, peers) {
+               var table = document.getElementById('cjdns-peerings');
+               while (table.rows.length > 1) {
+                       table.deleteRow(1);
+               }
+
+               if ((peers) && ((peers.err) || (typeof peers.length === 'undefined'))) {
+                       var errpeer = (peers.err)
+                                               ? 'Socket Error: unable to connect to Admin API'
+                                               : 'No active peers';
+                       var row = table.insertRow(-1);
+                       row.className = 'cbi-section-table-row';
+                       var cell = row.insertCell(-1);
+                       cell.colSpan = 7;
+                       cell.textContent = errpeer;
+                       return;
+               };
+
+               peers.forEach(function(peer, i) {
+                       if (peer.user == null) {
+                               var user = '';
+                       } else if (peer.user == 'Local Peers') {
+                               var user = 'beacon';
+                       } else {
+                               var user = peer.user;
+                       }
+
+                       if (peer.isIncoming === 0) {
+                               var interface = 'outgoing';
+                       } else {
+                               var interface = 'incoming';
+                       }
+
+                       var status = interface + ', ' + peer.state.toLowerCase();
+
+                       if (peer.version === 0) {
+                               var version = '-';
+                       } else {
+                               var version = 'v' + peer.version;
+                       }
+
+                       var rxtx = lbbytes(peer.bytesIn) + ' / ' + lbbytes(peer.bytesOut);
+
+                       var row = table.insertRow(-1);
+                       row.className = 'cbi-section-table-row cbi-rowstyle-' + ((i % 2) + 1);
+                       row.insertCell(-1).textContent = user;
+                       row.insertCell(-1).textContent = peer.ipv6;
+                       row.insertCell(-1).textContent = status;
+                       row.insertCell(-1).textContent = version;
+                       row.insertCell(-1).textContent = rxtx;
+                       var latencyCell = row.insertCell(-1);
+                       latencyCell.textContent = 'waiting';
+
+                       var pingURI = '<%=luci.dispatcher.build_url("admin", "services", "cjdns", "ping")%>';
+                       var timeout = 2000;
+                       XHR.get(pingURI, { label: peer.switchLabel, timeout: timeout }, function(x, pong) {
+                               var pongrsp = ((pong.err == "ai:recv > timeout") || (pong == "undefined") || (pong.ms >= timeout))
+                                       ? '> ' + timeout + ' ms'
+                                       : pong.ms + ' ms';
+                               latencyCell.textContent = pongrsp;
+                       })
+               });
+
+       };
+
+       XHR.get(peersURI, null, updatePeers);
+       XHR.poll(5, peersURI, null, updatePeers);
+
+//]]></script>
+
+<script type="text/javascript">
+<%# Author: [GitHub/75lb] -%>
+//<![CDATA[
+function lbbytes (bytes){
+
+       var kilobyte = 1024,
+           megabyte = kilobyte * 1024,
+           gigabyte = megabyte * 1024,
+           terabyte = gigabyte * 1024;
+
+       if ((bytes >= 0) && (bytes < kilobyte)) {
+               return bytes + " B";
+       } else if ((bytes >= kilobyte) && (bytes < megabyte)) {
+               return (bytes / kilobyte).toFixed(2) + " KB";
+       } else if ((bytes >= megabyte) && (bytes < gigabyte)) {
+               return (bytes / megabyte).toFixed(2) + " MB";
+       } else if ((bytes >= gigabyte) && (bytes < terabyte)) {
+               return (bytes / gigabyte).toFixed(2) + " GB";
+       } else if (bytes >= terabyte) {
+               return (bytes / terabyte).toFixed(2) + " TB";
+       } else {
+               return bytes + " B";
+       }
+};
+//]]>
+</script>
+
+<fieldset class="cbi-section">
+       <legend>Active cjdns peers</legend>
+       <table class="cbi-section-table" id="cjdns-peerings">
+               <tr class="cbi-section-table-titles">
+                       <th class="cbi-section-table-cell">User/Name</th>
+                       <th class="cbi-section-table-cell">IPv6</th>
+                       <th class="cbi-section-table-cell">Status</th>
+                       <th class="cbi-section-table-cell">Version</th>
+                       <th class="cbi-section-table-cell">Rx / Tx</th>
+                       <th class="cbi-section-table-cell">Latency</th>
+               </tr>
+               <tr class="cbi-section-table-row">
+                       <td colspan="7">Querying Admin API</td>
+               </tr>
+       </table>
+</fieldset>
diff --git a/luci-app-cjdns/luasrc/view/cjdns/value.htm b/luci-app-cjdns/luasrc/view/cjdns/value.htm
new file mode 100644 (file)
index 0000000..d1e54bb
--- /dev/null
@@ -0,0 +1,35 @@
+<%+cbi/valueheader%>
+       <input type="<%=self.password and 'password" class="cbi-input-password' or 'text" class="cbi-input-text' %>" onchange="cbi_d_update(this.id)"<%=
+               attr("name", cbid) .. attr("id", cbid) .. attr("value", self:cfgvalue(section) or self.default) ..
+               ifattr(self.size, "size") .. ifattr(self.placeholder, "placeholder")
+       %> style="width: auto" />
+       <% if self.password then %><img src="<%=resource%>/cbi/reload.gif" style="vertical-align:middle" title="<%:Reveal/hide password%>" onclick="var e = document.getElementById('<%=cbid%>'); e.type = (e.type=='password') ? 'text' : 'password';" /><% end %>
+       <% if #self.keylist > 0 or self.datatype then -%>
+       <script type="text/javascript">//<![CDATA[
+               <% if #self.keylist > 0 then -%>
+               cbi_combobox_init('<%=cbid%>', {
+               <%-
+                       for i, k in ipairs(self.keylist) do
+               -%>
+                       <%-=string.format("%q", k) .. ":" .. string.format("%q", self.vallist[i])-%>
+                       <%-if i<#self.keylist then-%>,<%-end-%>
+               <%-
+                       end
+               -%>
+               }, '<%- if not self.rmempty and not self.optional then -%>
+                       <%-: -- Please choose -- -%>
+                       <%- elseif self.placeholder then -%>
+                       <%-= pcdata(self.placeholder) -%>
+               <%- end -%>', '
+               <%- if self.combobox_manual then -%>
+                       <%-=self.combobox_manual-%>
+               <%- else -%>
+                       <%-: -- custom -- -%>
+               <%- end -%>');
+               <%- end %>
+               <% if self.datatype then -%>
+               cbi_validate_field('<%=cbid%>', <%=tostring((self.optional or self.rmempty) == true)%>, '<%=self.datatype:gsub("'", "\\'")%>');
+               <%- end %>
+       //]]></script>
+       <% end -%>
+<%+cbi/valuefooter%>