luci-app-unbound: add zone-details combined tab 2058/head
authorEric Luehrsen <ericluehrsen@gmail.com>
Sun, 12 Aug 2018 15:13:31 +0000 (11:13 -0400)
committerEric Luehrsen <ericluehrsen@gmail.com>
Mon, 13 Aug 2018 00:06:15 +0000 (20:06 -0400)
Signed-off-by: Eric Luehrsen <ericluehrsen@gmail.com>
applications/luci-app-unbound/luasrc/controller/unbound.lua
applications/luci-app-unbound/luasrc/model/cbi/unbound/configure.lua
applications/luci-app-unbound/luasrc/model/cbi/unbound/uciedit.lua
applications/luci-app-unbound/luasrc/model/cbi/unbound/zone-details.lua [new file with mode: 0644]
applications/luci-app-unbound/luasrc/model/cbi/unbound/zones.lua

index ea3d26b9197ed51dc9e3f66452f1fbb49bc02171..771385b7981d32dec3d0320b027d6bc77d754ea7 100644 (file)
@@ -28,7 +28,8 @@ function index()
 
     if (valman == "0") then
         entry({"admin", "services", "unbound", "zones"},
-            cbi("unbound/zones"), _("Zones"), 15)
+            arcombine(cbi("unbound/zones"), cbi("unbound/zone-details")),
+            _("Zones"), 15).leaf = true
     end
 
 
@@ -106,7 +107,7 @@ end
 
 
 function QuerySysLog()
-    local lcldata = luci.util.exec("logread | grep -i unbound")
+    local lcldata = luci.util.exec("logread -e 'unbound'")
     local lcldesc = luci.i18n.translate(
         "This shows syslog filtered for events involving Unbound.")
 
index f665a2c9da16b8252817cf38e1806d3642518eca..8170f3d2be24156ebceb74201f7d2ac6a6199890 100644 (file)
@@ -19,10 +19,12 @@ local ucl = luci.model.uci.cursor()
 local valman = ucl:get_first("unbound", "unbound", "manual_conf")
 
 m1 = Map("unbound")
-s1 = m1:section(TypedSection, "unbound", translate("DNS Resolver"),
+s1 = m1:section(TypedSection, "unbound", translate("Recursive DNS"),
     translatef("Unbound <a href=\"%s\" target=\"_blank\">(NLnet Labs)</a>"
-    .. " is a validating, recursive, and caching DNS resolver.",
-    "https://www.unbound.net/"))
+    .. " is a validating, recursive, and caching DNS resolver"
+    .. " <a href=\"%s\" target=\"_blank\">(help)</a>.",
+    "https://www.unbound.net/",
+    "https://github.com/openwrt/packages/blob/master/net/unbound/files/README.md"))
 
 s1.addremove = false
 s1.anonymous = true
@@ -40,11 +42,11 @@ end
 
 
 --Basic Tab, unconditional pieces
-ena = s1:taboption("basic", Flag, "enabled", translate("Enable Unbound:"),
+ena = s1:taboption("basic", Flag, "enabled", translate("Enable Unbound"),
     translate("Enable the initialization scripts for Unbound"))
 ena.rmempty = false
 
-mcf = s1:taboption("basic", Flag, "manual_conf", translate("Manual Conf:"),
+mcf = s1:taboption("basic", Flag, "manual_conf", translate("Manual Conf"),
     translate("Skip UCI and use /etc/unbound/unbound.conf"))
 mcf.rmempty = false
 
@@ -53,60 +55,60 @@ if (valman == "0") then
     -- Not in manual configuration mode; show UCI
     --Basic Tab
     lsv = s1:taboption("basic", Flag, "localservice",
-        translate("Local Service:"),
+        translate("Local Service"),
         translate("Accept queries only from local subnets"))
     lsv.rmempty = false
 
     vld = s1:taboption("basic", Flag, "validator",
-        translate("Enable DNSSEC:"),
+        translate("Enable DNSSEC"),
         translate("Enable the DNSSEC validator module"))
     vld.rmempty = false
 
     nvd = s1:taboption("basic", Flag, "validator_ntp",
-        translate("DNSSEC NTP Fix:"),
+        translate("DNSSEC NTP Fix"),
         translate("Break the loop where DNSSEC needs NTP and NTP needs DNS"))
-    nvd.rmempty = false
-    nvd:depends({ validator = true })
+    nvd.optional = true
+    nvd:depends("validator", true)
 
     prt = s1:taboption("basic", Value, "listen_port",
-        translate("Listening Port:"),
+        translate("Listening Port"),
         translate("Choose Unbounds listening port"))
     prt.datatype = "port"
-    prt.rmempty = false
+    prt.placeholder = "53"
 
     --Avanced Tab
     rlh = s1:taboption("advanced", Flag, "rebind_localhost",
-        translate("Filter Localhost Rebind:"),
+        translate("Filter Localhost Rebind"),
         translate("Protect against upstream response of 127.0.0.0/8"))
     rlh.rmempty = false
 
     rpv = s1:taboption("advanced", ListValue, "rebind_protection",
-        translate("Filter Private Rebind:"),
+        translate("Filter Private Rebind"),
         translate("Protect against upstream responses within local subnets"))
     rpv:value("0", translate("No Filter"))
-    rpv:value("1", translate("Filter RFC1918/4193"))
+    rpv:value("1", translate("Filter Private Address"))
     rpv:value("2", translate("Filter Entire Subnet"))
     rpv.rmempty = false
 
-    d64 = s1:taboption("advanced", Flag, "dns64", translate("Enable DNS64:"),
+    d64 = s1:taboption("advanced", Flag, "dns64", translate("Enable DNS64"),
         translate("Enable the DNS64 module"))
     d64.rmempty = false
 
     pfx = s1:taboption("advanced", Value, "dns64_prefix",
-        translate("DNS64 Prefix:"),
+        translate("DNS64 Prefix"),
         translate("Prefix for generated DNS64 addresses"))
     pfx.datatype = "ip6addr"
     pfx.placeholder = "64:ff9b::/96"
     pfx.optional = true
-    pfx:depends({ dns64 = true })
+    pfx:depends("dns64", true)
 
     din = s1:taboption("advanced", DynamicList, "domain_insecure",
-        translate("Domain Insecure:"),
+        translate("Domain Insecure"),
         translate("List domains to bypass checks of DNSSEC"))
-    din:depends({ validator = true })
+    din:depends("validator", true)
 
     ag2 = s1:taboption("advanced", Value, "root_age",
-        translate("Root DSKEY Age:"),
+        translate("Root DSKEY Age"),
         translate("Limit days between RFC5011 copies to reduce flash writes"))
     ag2.datatype = "and(uinteger,min(1),max(99))"
     ag2:value("3", "3")
@@ -116,7 +118,7 @@ if (valman == "0") then
     ag2:value("99", "99 ("..translate("never")..")")
 
     tgr = s1:taboption("advanced", Value, "trigger_interface",
-        translate("Trigger Networks:"),
+        translate("Trigger Networks"),
         translate("Networks that may trigger Unbound to reload (avoid wan6)"))
     tgr.template = "cbi/network_netlist"
     tgr.widget = "checkbox"
@@ -126,7 +128,7 @@ if (valman == "0") then
 
     --DHCP Tab
     dlk = s1:taboption("DHCP", ListValue, "dhcp_link",
-        translate("DHCP Link:"),
+        translate("DHCP Link"),
         translate("Link to supported programs to load DHCP into DNS"))
     dlk:value("none", translate("No Link"))
     dlk:value("dnsmasq", "dnsmasq")
@@ -134,65 +136,70 @@ if (valman == "0") then
     dlk.rmempty = false
 
     dp6 = s1:taboption("DHCP", Flag, "dhcp4_slaac6",
-        translate("DHCPv4 to SLAAC:"),
+        translate("DHCPv4 to SLAAC"),
         translate("Use DHCPv4 MAC to discover IP6 hosts SLAAC (EUI64)"))
-    dp6.rmempty = false
-    dp6:depends({ dhcp_link = "odhcpd" })
+    dp6.optional = true
+    dp6:depends("dhcp_link", "odhcpd")
 
     dom = s1:taboption("DHCP", Value, "domain",
-        translate("Local Domain:"),
+        translate("Local Domain"),
         translate("Domain suffix for this router and DHCP clients"))
     dom.placeholder = "lan"
-    dom:depends({ dhcp_link = "none" })
-    dom:depends({ dhcp_link = "odhcpd" })
+    dom.optional = true
+    dom:depends("dhcp_link", "none")
+    dom:depends("dhcp_link", "odhcpd")
 
     dty = s1:taboption("DHCP", ListValue, "domain_type",
-        translate("Local Domain Type:"),
+        translate("Local Domain Type"),
         translate("How to treat queries of this local domain"))
+    dty.optional = true
     dty:value("deny", translate("Denied (nxdomain)"))
     dty:value("refuse", translate("Refused"))
     dty:value("static", translate("Static (local only)"))
     dty:value("transparent", translate("Transparent (local/global)"))
-    dty:depends({ dhcp_link = "none" })
-    dty:depends({ dhcp_link = "odhcpd" })
+    dty:depends("dhcp_link", "none")
+    dty:depends("dhcp_link", "odhcpd")
 
     lfq = s1:taboption("DHCP", ListValue, "add_local_fqdn",
-        translate("LAN DNS:"),
+        translate("LAN DNS"),
         translate("How to enter the LAN or local network router in DNS"))
+    lfq.optional = true
     lfq:value("0", translate("No Entry"))
     lfq:value("1", translate("Hostname, Primary Address"))
     lfq:value("2", translate("Hostname, All Addresses"))
     lfq:value("3", translate("Host FQDN, All Addresses"))
     lfq:value("4", translate("Interface FQDN, All Addresses"))
-    lfq:depends({ dhcp_link = "none" })
-    lfq:depends({ dhcp_link = "odhcpd" })
+    lfq:depends("dhcp_link", "none")
+    lfq:depends("dhcp_link", "odhcpd")
 
     wfq = s1:taboption("DHCP", ListValue, "add_wan_fqdn",
-        translate("WAN DNS:"),
+        translate("WAN DNS"),
         translate("Override the WAN side router entry in DNS"))
+    wfq.optional = true
     wfq:value("0", translate("Use Upstream"))
     wfq:value("1", translate("Hostname, Primary Address"))
     wfq:value("2", translate("Hostname, All Addresses"))
     wfq:value("3", translate("Host FQDN, All Addresses"))
     wfq:value("4", translate("Interface FQDN, All Addresses"))
-    wfq:depends({ dhcp_link = "none" })
-    wfq:depends({ dhcp_link = "odhcpd" })
+    wfq:depends("dhcp_link", "none")
+    wfq:depends("dhcp_link", "odhcpd")
 
     exa = s1:taboption("DHCP", ListValue, "add_extra_dns",
-        translate("Extra DNS:"),
+        translate("Extra DNS"),
         translate("Use extra DNS entries found in /etc/config/dhcp"))
+    exa.optional = true
     exa:value("0", translate("Ignore"))
     exa:value("1", translate("Host Records"))
     exa:value("2", translate("Host/MX/SRV RR"))
     exa:value("3", translate("Host/MX/SRV/CNAME RR"))
-    exa:depends({ dhcp_link = "none" })
-    exa:depends({ dhcp_link = "odhcpd" })
+    exa:depends("dhcp_link", "none")
+    exa:depends("dhcp_link", "odhcpd")
 
     --TODO: dnsmasq needs to not reference resolve-file and get off port 53.
 
     --Resource Tuning Tab
     ctl = s1:taboption("resource", ListValue, "unbound_control",
-        translate("Unbound Control App:"),
+        translate("Unbound Control App"),
         translate("Enable access for unbound-control"))
     ctl.rmempty = false
     ctl:value("0", translate("No Remote Control"))
@@ -202,7 +209,7 @@ if (valman == "0") then
     ctl:value("4", translate("Local Subnet, Static Encryption"))
 
     pro = s1:taboption("resource", ListValue, "protocol",
-        translate("Recursion Protocol:"),
+        translate("Recursion Protocol"),
         translate("Chose the protocol recursion queries leave on"))
     pro:value("default", translate("Default"))
     pro:value("ip4_only", translate("IP4 Only"))
@@ -212,7 +219,7 @@ if (valman == "0") then
     pro.rmempty = false
 
     rsc = s1:taboption("resource", ListValue, "resource",
-        translate("Memory Resource:"),
+        translate("Memory Resource"),
         translate("Use menu System/Processes to observe any memory growth"))
     rsc:value("default", translate("Default"))
     rsc:value("tiny", translate("Tiny"))
@@ -222,7 +229,7 @@ if (valman == "0") then
     rsc.rmempty = false
 
     rsn = s1:taboption("resource", ListValue, "recursion",
-        translate("Recursion Strength:"),
+        translate("Recursion Strength"),
         translate("Recursion activity affects memory growth and CPU load"))
     rsn:value("default", translate("Default"))
     rsn:value("passive", translate("Passive"))
@@ -230,38 +237,38 @@ if (valman == "0") then
     rsn.rmempty = false
 
     qry = s1:taboption("resource", Flag, "query_minimize",
-        translate("Query Minimize:"),
+        translate("Query Minimize"),
         translate("Break down query components for limited added privacy"))
-    qry.rmempty = false
-    qry:depends({ recursion = "passive" })
-    qry:depends({ recursion = "aggressive" })
+    qry.optional = true
+    qry:depends("recursion", "passive")
+    qry:depends("recursion", "aggressive")
 
     qrs = s1:taboption("resource", Flag, "query_min_strict",
-        translate("Strict Minimize:"),
+        translate("Strict Minimize"),
         translate("Strict version of 'query minimize' but it can break DNS"))
-    qrs.rmempty = false
-    qrs:depends({ query_minimize = true })
+    qrs.optional = true
+    qrs:depends("query_minimize", true)
 
     eds = s1:taboption("resource", Value, "edns_size",
         translate("EDNS Size:"),
         translate("Limit extended DNS packet size"))
     eds.datatype = "and(uinteger,min(512),max(4096))"
-    eds.rmempty = false
+    eds.placeholder = "1280"
 
     tlm = s1:taboption("resource", Value, "ttl_min",
-        translate("TTL Minimum:"),
+        translate("TTL Minimum"),
         translate("Prevent excessively short cache periods"))
-    tlm.datatype = "and(uinteger,min(0),max(600))"
-    tlm.rmempty = false
+    tlm.datatype = "and(uinteger,min(0),max(1200))"
+    tlm.placeholder = "120"
 
     stt = s1:taboption("resource", Flag, "extended_stats",
-        translate("Extended Statistics:"),
+        translate("Extended Statistics"),
         translate("Extended statistics are printed from unbound-control"))
     stt.rmempty = false
 
 else
     ag2 = s1:taboption("basic", Value, "root_age",
-        translate("Root DSKEY Age:"),
+        translate("Root DSKEY Age"),
         translate("Limit days between RFC5011 copies to reduce flash writes"))
     ag2.datatype = "and(uinteger,min(1),max(99))"
     ag2:value("3", "3")
@@ -271,7 +278,7 @@ else
     ag2:value("99", "99 ("..translate("never")..")")
 
     tgr = s1:taboption("basic", Value, "trigger_interface",
-        translate("Trigger Networks:"),
+        translate("Trigger Networks"),
         translate("Networks that may trigger Unbound to reload (avoid wan6)"))
     tgr.template = "cbi/network_netlist"
     tgr.widget = "checkbox"
index 3aef189652053eaea1f5afc665e10317523648d3..f1e1d842a1a8dd17742a2d960ef199654c5f61f3 100644 (file)
@@ -12,7 +12,7 @@ m6.submit = translate("Save")
 m6.reset = false
 s6 = m6:section(SimpleSection, "",
     translatef("Edit '" .. filename .. "' "
-    .. "and help can be found in OpenWrt "
+    .. "and recipes can be found in OpenWrt "
     .. "<a href=\"%s\" target=\"_blank\">Guides</a> "
     .. "and <a href=\"%s\" target=\"_blank\">Github</a>.",
     "https://openwrt.org/docs/guide-user/services/dns/unbound",
diff --git a/applications/luci-app-unbound/luasrc/model/cbi/unbound/zone-details.lua b/applications/luci-app-unbound/luasrc/model/cbi/unbound/zone-details.lua
new file mode 100644 (file)
index 0000000..dcaa877
--- /dev/null
@@ -0,0 +1,95 @@
+-- Copyright 2018 Eric Luehrsen <ericluehrsen@gmail.com>
+-- Licensed to the public under the Apache License 2.0.
+
+local sy = require "luci.sys"
+local ds = require "luci.dispatcher"
+local hp = require "luci.http"
+local m7, s7
+local ena, flb, zty, znm, srv, rlv, tlu
+local prt, tlp, tli, url
+
+arg[1] = arg[1] or ""
+m7 = Map("unbound")
+m7.redirect = ds.build_url("admin/services/unbound/zones")
+
+
+if (arg[1] == "") then
+    hp.redirect(m7.redirect)
+    return
+
+else
+    s7 = m7:section(NamedSection, arg[1], "zone",
+        translatef("Directed Zone"),
+        translatef("Edit a forward, stub, or zone-file-cache zone "
+        .. "for Unbound to use instead of recursion."))
+
+    s7.anonymous = true
+    s7.addremove = false
+
+    ena = s7:option(Flag, "enabled", translate("Enabled"),
+        translate("Enable this directed zone"))
+    ena.rmempty = false
+
+    flb = s7:option(Flag, "fallback", translate("Fall Back"),
+        translate("Allow open recursion when record not in zone"))
+    flb.rmempty = false
+
+    zty = s7:option(ListValue, "zone_type", translate("Zone Type"))
+    zty:value("auth_zone", translate("Authoritative (zone file)"))
+    zty:value("stub_zone", translate("Stub (forced recursion)"))
+    zty:value("forward_zone", translate("Forward (simple handoff)"))
+    zty.rmempty = false
+
+    znm = s7:option(DynamicList, "zone_name", translate("Zone Names"),
+        translate("Zone (Domain) names included in this zone combination"))
+    znm.placeholder="new.example.net."
+
+    srv = s7:option(DynamicList, "server", translate("Servers"),
+        translate("Servers for this zone; see README.md for optional form"))
+    srv.placeholder="192.0.2.53"
+
+    rlv = s7:option(Flag, "resolv_conf", translate("Use 'resolv.conf.auto'"),
+        translate("Forward to upstream nameservers (ISP)"))
+    rlv:depends("zone_type", "forward_zone")
+
+    tlu = s7:option(Flag, "tls_upstream", translate("DNS over TLS"),
+        translate("Connect to servers using TLS"))
+    tlu:depends("zone_type", "forward_zone")
+
+    prt = s7:option(Value, "port", translate("Server Port"),
+        translate("Port servers will receive queries on"))
+    prt:depends("tls_upstream", false)
+    prt.datatype = "port"
+    prt.placeholder="53"
+
+    tlp = s7:option(Value, "tls_port", translate("Server TLS Port"),
+        translate("Port servers will receive queries on"))
+    tlp:depends("tls_upstream", true)
+    tlp.datatype = "port"
+    tlp.placeholder="853"
+
+    tli = s7:option(Value, "tls_index", translate("TLS Name Index"),
+        translate("Domain name to verify TLS certificate"))
+    tli:depends("tls_upstream", true)
+    tli.placeholder="dns.example.net"
+
+    url = s7:option(Value, "url_dir", translate("Zone Download URL"),
+        translate("Directory only part of URL"))
+    url:depends("zone_type", "auth_zone")
+    url.placeholder="https://www.example.net/dl/zones/"
+end
+
+
+function m7.on_commit(self)
+    if sy.init.enabled("unbound") then
+        -- Restart Unbound with configuration
+        sy.call("/etc/init.d/unbound restart >/dev/null 2>&1")
+
+    else
+        sy.call("/etc/init.d/unbound stop >/dev/null 2>&1")
+    end
+end
+
+
+return m7
+
index bbc0e2335f761f5ffcf8eb914b1c978c73128829..798ca6a45cae9f1f5d9803cff343a7d9286ae935 100644 (file)
@@ -1,4 +1,4 @@
--- Copyright 2017 Eric Luehrsen <ericluehrsen@gmail.com>
+-- Copyright 2018 Eric Luehrsen <ericluehrsen@gmail.com>
 -- Licensed to the public under the Apache License 2.0.
 
 local m5, s5
@@ -7,18 +7,22 @@ local ztype, zones, servers, fallback, enabled
 local fs = require "nixio.fs"
 local ut = require "luci.util"
 local sy = require "luci.sys"
+local ds = require "luci.dispatcher"
 local resolvfile = "/tmp/resolv.conf.auto"
+local logerr = ut.exec("logread -e 'unbound.*error.*ssl library'")
 
 m5 = Map("unbound")
 s5 = m5:section(TypedSection, "zone", "Zones",
-    translatef("This shows extended zones and more details can be "
-    .. "changed in Files tab and <a href=\"%s\">Edit:UCI</a> subtab.",
-    "/cgi-bin/luci/admin/services/unbound/files" ))
+    translatef("Organize directed forward, stub, and authoritative zones"
+    .. " <a href=\"%s\" target=\"_blank\">(help)</a>.",
+    "https://www.unbound.net/",
+    "https://github.com/openwrt/packages/blob/master/net/unbound/files/README.md"))
 
-s5.addremove = false
+s5.addremove = true
 s5.anonymous = true
 s5.sortable = true
 s5.template = "cbi/tblsection"
+s5.extedit = ds.build_url("admin/services/unbound/zones/%s")
 
 ztype = s5:option(DummyValue, "DummyType", translate("Type"))
 ztype.rawhtml = true
@@ -36,6 +40,23 @@ enabled = s5:option(Flag, "enabled", translate("Enable"))
 enabled.rmempty = false
 
 
+if logerr and (#logerr > 0) then
+    logerr = logerr:sub((1 + #logerr - math.min(#logerr, 250)), #logerr)
+    m5.message = translatef( "Note: SSL/TLS library is missing an API. "
+        .. "Please review syslog. >> logread ... " .. logerr )
+end
+
+
+function s5.create(self, section)
+    created = TypedSection.create(self, section)
+end
+
+
+function s5.parse(self, ...)
+    TypedSection.parse(self, ...)
+end
+
+
 function ztype.cfgvalue(self, s)
     -- Format a meaninful tile for the Zone Type column
     local itxt = self.map:get(s, "zone_type")
@@ -57,7 +78,7 @@ function ztype.cfgvalue(self, s)
         return translate("AXFR")
 
     else
-        return translate("Error")
+        return translate("Undefined")
     end
 end
 
@@ -85,22 +106,22 @@ function zones.cfgvalue(self, s)
     end
 
 
-    if itype and itype:match("forward") then
-        -- from zone_type create a readable hint for the action
-        otxt = translate("accept upstream results for ") .. otxt
+    if otxt and (#otxt > 0) then
+        if itype and itype:match("forward") then
+            -- from zone_type create a readable hint for the action
+            otxt = translate("accept upstream results for ") .. otxt
 
-    elseif itype and itype:match("stub") then
-        otxt = translate("select recursion for ") .. otxt
+        elseif itype and itype:match("stub") then
+            otxt = translate("select recursion for ") .. otxt
 
-    elseif itype and itype:match("auth") then
-        otxt = translate("prefetch zone files for ") .. otxt
+        elseif itype and itype:match("auth") then
+            otxt = translate("prefetch zone files for ") .. otxt
 
-    else
-        otxt = translate("unknown action for ") .. otxt
-    end
+        else
+            otxt = translate("unknown action for ") .. otxt
+        end
 
 
-    if otxt and (#otxt > 0) then
         return otxt
 
     else
@@ -131,14 +152,17 @@ function servers.cfgvalue(self, s)
     end
 
 
+    if otxt and (#otxt > 0) then
+        otxt = translate("use nameservers ") .. otxt
+    end
+
+
     if otxt and (#otxt > 0)
     and itls and (itls == "1")
     and iidx and (#iidx > 0) then
         -- show TLS certificate name index if provided
-        otxt = translatef("use nameservers by <var>%s</var> at ", iidx) .. otxt
-
-    elseif otxt and (#otxt > 0) then
-        otxt = translate("use nameservers ") .. otxt
+        otxt = otxt .. translatef(
+                    " with default certificate for <var>%s</var>", iidx)
     end
 
 
@@ -174,11 +198,12 @@ function servers.cfgvalue(self, s)
 
 
         if otxt and (#otxt > 0) and rtxt and (#rtxt > 0) then
-            otxt = otxt
-                .. translatef(", and <var>%s</var> entries ", resolvfile) .. rtxt
+            otxt = otxt .. translatef(
+                    ", and <var>%s</var> entries ", resolvfile) .. rtxt
 
         elseif rtxt and (#rtxt > 0) then
-            otxt = translatef("use <var>%s</var> nameservers ", resolvfile) .. rtxt
+            otxt = translatef(
+                    "use <var>%s</var> nameservers ", resolvfile) .. rtxt
         end
     end