libs/sys: implement luci.sys.wifi.channels()
[project/luci.git] / libs / sys / luasrc / sys.lua
index 230201d..3412b32 100644 (file)
@@ -35,7 +35,8 @@ luci.util   = require "luci.util"
 luci.fs     = require "luci.fs"
 luci.ip     = require "luci.ip"
 
-local tonumber, ipairs, pairs = tonumber, ipairs, pairs
+local tonumber, ipairs, pairs, pcall, type =
+       tonumber, ipairs, pairs, pcall, type
 
 
 --- LuCI Linux and POSIX system utilities.
@@ -129,10 +130,16 @@ end
 -- @return             Table containing all variables if no variable name is given
 getenv = posix.getenv
 
---- Determine the current hostname.
+--- Get or set the current hostname.
+-- @param              String containing a new hostname to set (optional)
 -- @return             String containing the system hostname
-function hostname()
-       return io.lines("/proc/sys/kernel/hostname")()
+function hostname(newname)
+       if type(newname) == "string" and #newname > 0 then
+               luci.fs.writefile( "/proc/sys/kernel/hostname", newname .. "\n" )
+               return newname
+       else
+               return posix.uname("%n")
+       end
 end
 
 --- Returns the contents of a documented referred by an URL.
@@ -142,7 +149,7 @@ end
 -- @return             String containing the contents of given the URL
 function httpget(url, stream, target)
        if not target then
-               local source = stream and io.open or luci.util.exec
+               local source = stream and io.popen or luci.util.exec
                return source("wget -qO- '"..url:gsub("'", "").."'")
        else
                return os.execute("wget -qO '%s' '%s'" %
@@ -175,27 +182,24 @@ end
 -- @return     String containing the memory used for buffering in kB
 -- @return     String containing the free memory amount in kB
 function sysinfo()
-       local c1 = "cat /proc/cpuinfo|grep system\\ typ|cut -d: -f2 2>/dev/null"
-       local c2 = "uname -m 2>/dev/null"
-       local c3 = "cat /proc/cpuinfo|grep model\\ name|cut -d: -f2 2>/dev/null"
-       local c4 = "cat /proc/cpuinfo|grep cpu\\ model|cut -d: -f2 2>/dev/null"
-       local c5 = "cat /proc/meminfo|grep MemTotal|awk {' print $2 '} 2>/dev/null"
-       local c6 = "cat /proc/meminfo|grep ^Cached|awk {' print $2 '} 2>/dev/null"
-       local c7 = "cat /proc/meminfo|grep MemFree|awk {' print $2 '} 2>/dev/null"
-       local c8 = "cat /proc/meminfo|grep Buffers|awk {' print $2 '} 2>/dev/null"
-
-       local system = luci.util.trim(luci.util.exec(c1))
+       local cpuinfo = luci.fs.readfile("/proc/cpuinfo")
+       local meminfo = luci.fs.readfile("/proc/meminfo")
+
+       local system = cpuinfo:match("system typ.-:%s*([^\n]+)")
        local model = ""
-       local memtotal = tonumber(luci.util.trim(luci.util.exec(c5)))
-       local memcached = tonumber(luci.util.trim(luci.util.exec(c6)))
-       local memfree = tonumber(luci.util.trim(luci.util.exec(c7)))
-       local membuffers = tonumber(luci.util.trim(luci.util.exec(c8)))
-
-       if system == "" then
-               system = luci.util.trim(luci.util.exec(c2))
-               model = luci.util.trim(luci.util.exec(c3))
+       local memtotal = tonumber(meminfo:match("MemTotal:%s*(%d+)"))
+       local memcached = tonumber(meminfo:match("\nCached:%s*(%d+)"))
+       local memfree = tonumber(meminfo:match("MemFree:%s*(%d+)"))
+       local membuffers = tonumber(meminfo:match("Buffers:%s*(%d+)"))
+
+       if not system then
+               system = posix.uname("%m")
+               model = cpuinfo:match("model name.-:%s*([^\n]+)")
+               if not model then
+                       model = cpuinfo:match("Processor.-:%s*([^\n]+)")
+               end
        else
-               model = luci.util.trim(luci.util.exec(c4))
+               model = cpuinfo:match("cpu model.-:%s*([^\n]+)")
        end
 
        return system, model, memtotal, memcached, membuffers, memfree
@@ -207,6 +211,12 @@ function syslog()
        return luci.util.exec("logread")
 end
 
+--- Retrieves the output of the "dmesg" command.
+-- @return     String containing the current log buffer
+function dmesg()
+       return luci.util.exec("dmesg")
+end
+
 --- Generates a random id with specified length.
 -- @param bytes        Number of bytes for the unique id
 -- @return             String containing hex encoded id
@@ -259,21 +269,74 @@ function net.arptable()
        return _parse_delimited_table(io.lines("/proc/net/arp"), "%s%s+")
 end
 
---- Determine the current default route.
+--- Returns conntrack information
+-- @return     Table with the currently tracked IP connections
+function net.conntrack()
+       local connt = {}
+       if luci.fs.access("/proc/net/nf_conntrack", "r") then
+               for line in io.lines("/proc/net/nf_conntrack") do
+                       line = line:match "^(.-( [^ =]+=).-)%2"
+                       local entry, flags = _parse_mixed_record(line, " +")
+                       entry.layer3 = flags[1]
+                       entry.layer4 = flags[3]
+                       for i=1, #entry do
+                               entry[i] = nil
+                       end
+
+                       connt[#connt+1] = entry
+               end
+       elseif luci.fs.access("/proc/net/ip_conntrack", "r") then
+               for line in io.lines("/proc/net/ip_conntrack") do
+                       line = line:match "^(.-( [^ =]+=).-)%2"
+                       local entry, flags = _parse_mixed_record(line, " +")
+                       entry.layer3 = "ipv4"
+                       entry.layer4 = flags[1]
+                       for i=1, #entry do
+                               entry[i] = nil
+                       end
+
+                       connt[#connt+1] = entry
+               end
+       else
+               return nil
+       end
+       return connt
+end
+
+--- Determine the current IPv4 default route. If multiple default routes exist,
+-- return the one with the lowest metric.
 -- @return     Table with the properties of the current default route.
 --                     The following fields are defined:
---                     { "Mask", "RefCnt", "Iface", "Flags", "Window", "IRTT",
---                       "MTU", "Gateway", "Destination", "Metric", "Use" }
+--                     { "dest", "gateway", "metric", "refcount", "usecount", "irtt",
+--                       "flags", "device" }
 function net.defaultroute()
-       local routes = net.routes()
        local route = nil
-
-       for i, r in pairs(luci.sys.net.routes()) do
-               if r.Destination == "00000000" and (not route or route.Metric > r.Metric) then
+       for _, r in pairs(net.routes()) do
+               if r.dest:prefix() == 0 and (not route or route.metric > r.metric) then
                        route = r
                end
        end
+       return route
+end
 
+--- Determine the current IPv6 default route. If multiple default routes exist,
+-- return the one with the lowest metric.
+-- @return     Table with the properties of the current default route.
+--                     The following fields are defined:
+--                     { "source", "dest", "nexthop", "metric", "refcount", "usecount",
+--                       "flags", "device" }
+function net.defaultroute6()
+       local route   = nil
+       local routes6 = net.routes6()
+       if routes6 then
+               for _, r in pairs(routes6) do
+                       if r.dest:prefix() == 0 and
+                          (not route or route.metric > r.metric)
+                       then
+                               route = r
+                       end
+               end
+       end
        return route
 end
 
@@ -320,12 +383,89 @@ end
 --- Returns the current kernel routing table entries.
 -- @return     Table of tables with properties of the corresponding routes.
 --                     The following fields are defined for route entry tables:
---                     { "Mask", "RefCnt", "Iface", "Flags", "Window", "IRTT",
---                       "MTU", "Gateway", "Destination", "Metric", "Use" }
+--                     { "dest", "gateway", "metric", "refcount", "usecount", "irtt",
+--                       "flags", "device" }
 function net.routes()
-       return _parse_delimited_table(io.lines("/proc/net/route"))
+       local routes = { }
+
+       for line in io.lines("/proc/net/route") do
+
+               local dev, dst_ip, gateway, flags, refcnt, usecnt, metric,
+                         dst_mask, mtu, win, irtt = line:match(
+                       "([^%s]+)\t([A-F0-9]+)\t([A-F0-9]+)\t([A-F0-9]+)\t" ..
+                       "(%d+)\t(%d+)\t(%d+)\t([A-F0-9]+)\t(%d+)\t(%d+)\t(%d+)"
+               )
+
+               if dev then
+                       gateway  = luci.ip.Hex( gateway,  32, luci.ip.FAMILY_INET4 )
+                       dst_mask = luci.ip.Hex( dst_mask, 32, luci.ip.FAMILY_INET4 )
+                       dst_ip   = luci.ip.Hex(
+                               dst_ip, dst_mask:prefix(dst_mask), luci.ip.FAMILY_INET4
+                       )
+
+                       routes[#routes+1] = {
+                               dest     = dst_ip,
+                               gateway  = gateway,
+                               metric   = tonumber(metric),
+                               refcount = tonumber(refcnt),
+                               usecount = tonumber(usecnt),
+                               mtu      = tonumber(mtu),
+                               window   = tonumber(window),
+                               irtt     = tonumber(irtt),
+                               flags    = tonumber(flags, 16),
+                               device   = dev
+                       }
+               end
+       end
+
+       return routes
 end
 
+--- Returns the current ipv6 kernel routing table entries.
+-- @return     Table of tables with properties of the corresponding routes.
+--                     The following fields are defined for route entry tables:
+--                     { "source", "dest", "nexthop", "metric", "refcount", "usecount",
+--                       "flags", "device" }
+function net.routes6()
+       if luci.fs.access("/proc/net/ipv6_route", "r") then
+               local routes = { }
+
+               for line in io.lines("/proc/net/ipv6_route") do
+
+                       local dst_ip, dst_prefix, src_ip, src_prefix, nexthop,
+                                 metric, refcnt, usecnt, flags, dev = line:match(
+                               "([a-f0-9]+) ([a-f0-9]+) " ..
+                               "([a-f0-9]+) ([a-f0-9]+) " ..
+                               "([a-f0-9]+) ([a-f0-9]+) " ..
+                               "([a-f0-9]+) ([a-f0-9]+) " ..
+                               "([a-f0-9]+) +([^%s]+)"
+                       )
+
+                       src_ip = luci.ip.Hex(
+                               src_ip, tonumber(src_prefix, 16), luci.ip.FAMILY_INET6, false
+                       )
+
+                       dst_ip = luci.ip.Hex(
+                               dst_ip, tonumber(dst_prefix, 16), luci.ip.FAMILY_INET6, false
+                       )
+
+                       nexthop = luci.ip.Hex( nexthop, 128, luci.ip.FAMILY_INET6, false )
+
+                       routes[#routes+1] = {
+                               source   = src_ip,
+                               dest     = dst_ip,
+                               nexthop  = nexthop,
+                               metric   = tonumber(metric, 16),
+                               refcount = tonumber(refcnt, 16),
+                               usecount = tonumber(usecnt, 16),
+                               flags    = tonumber(flags, 16),
+                               device   = dev
+                       }
+               end
+
+               return routes
+       end
+end
 
 --- Tests whether the given host responds to ping probes.
 -- @param host String containing a hostname or IPv4 address
@@ -459,8 +599,12 @@ function user.checkpasswd(username, password)
                        end
                end
 
-               return (pwd == posix.crypt(password, pwd))
+               if pwd and #pwd > 0 and password and #password > 0 then
+                       return (pwd == posix.crypt(password, pwd))
+               end
        end
+
+       return false
 end
 
 --- Change the password of given user.
@@ -490,14 +634,18 @@ wifi = {}
 --- Get iwconfig output for all wireless devices.
 -- @return     Table of tables containing the iwconfing output for each wifi device
 function wifi.getiwconfig()
-       local cnt = luci.util.exec("/usr/sbin/iwconfig 2>/dev/null")
+       local cnt = luci.util.exec("PATH=/sbin:/usr/sbin iwconfig 2>/dev/null")
        local iwc = {}
 
        for i, l in pairs(luci.util.split(luci.util.trim(cnt), "\n\n")) do
                local k = l:match("^(.-) ")
                l = l:gsub("^(.-) +", "", 1)
                if k then
-                       iwc[k] = _parse_mixed_record(l)
+                       local entry, flags = _parse_mixed_record(l)
+                       if entry then
+                               entry.flags = flags
+                       end
+                       iwc[k] = entry
                end
        end
 
@@ -521,7 +669,11 @@ function wifi.iwscan(iface)
                                c = c:gsub("^(.-)- ", "", 1)
                                c = luci.util.split(c, "\n", 7)
                                c = table.concat(c, "\n", 1)
-                               table.insert(iws[k], _parse_mixed_record(c))
+                               local entry, flags = _parse_mixed_record(c)
+                               if entry then
+                                       entry.flags = flags
+                               end
+                               table.insert(iws[k], entry)
                        end
                end
        end
@@ -529,6 +681,36 @@ function wifi.iwscan(iface)
        return iface and (iws[iface] or {}) or iws
 end
 
+--- Get available channels from given wireless iface.
+-- @param iface        Wireless interface (optional)
+-- @return             Table of available channels
+function wifi.channels(iface)
+       local cmd = "iwlist " .. ( iface or "" ) .. " freq 2>/dev/null"
+       local cns = { }
+
+       local fd = io.popen(cmd)
+       if fd then
+               local ln, c, f
+               repeat
+                       ln = fd:read("*l") or ""
+                       c, f = ln:match("Channel (%d+) : (%d+%.%d+) GHz")
+                       if c and f then
+                               cns[tonumber(c)] = tonumber(f)
+                       end
+               until not ( #ln > 0 )
+               fd:close()
+       end
+
+       if #cns == 0 then
+               cms = {
+                       2.412, 2.417, 2.422, 2.427, 2.432, 2.437,
+                       2.442, 2.447, 2.452, 2.457, 2.462
+               }
+       end
+
+       return cns
+end
+
 
 --- LuCI system utilities / init related functions.
 -- @class      module
@@ -547,7 +729,8 @@ function init.names()
 end
 
 --- Test whether the given init script is enabled
--- @return     Boolean indicating whether init is enabled
+-- @param name Name of the init script
+-- @return             Boolean indicating whether init is enabled
 function init.enabled(name)
        if luci.fs.access(init.dir..name) then
                return ( call(init.dir..name.." enabled") == 0 )
@@ -556,23 +739,26 @@ function init.enabled(name)
 end
 
 --- Get the index of he given init script
--- @return     Numeric index value
+-- @param name Name of the init script
+-- @return             Numeric index value
 function init.index(name)
        if luci.fs.access(init.dir..name) then
-               return call("source "..init.dir..name.." && exit $START")
+               return call("source "..init.dir..name.."; exit $START")
        end
 end
 
 --- Enable the given init script
--- @return     Boolean indicating success
+-- @param name Name of the init script
+-- @return             Boolean indicating success
 function init.enable(name)
        if luci.fs.access(init.dir..name) then
-               return ( call(init.dir..name.." enable") == 0 )
+               return ( call(init.dir..name.." enable") == 1 )
        end
 end
 
 --- Disable the given init script
--- @return     Boolean indicating success
+-- @param name Name of the init script
+-- @return             Boolean indicating success
 function init.disable(name)
        if luci.fs.access(init.dir..name) then
                return ( call(init.dir..name.." disable") == 0 )
@@ -610,22 +796,24 @@ function _parse_delimited_table(iter, delimiter)
        return data
 end
 
-function _parse_mixed_record(cnt)
+function _parse_mixed_record(cnt, delimiter)
+       delimiter = delimiter or "  "
        local data = {}
+       local flags = {}
 
        for i, l in pairs(luci.util.split(luci.util.trim(cnt), "\n")) do
-       for j, f in pairs(luci.util.split(luci.util.trim(l), "  ")) do
-               local k, x, v = f:match('([^%s][^:=]+) *([:=]*) *"*([^\n"]*)"*')
+               for j, f in pairs(luci.util.split(luci.util.trim(l), delimiter, nil, true)) do
+                       local k, x, v = f:match('([^%s][^:=]*) *([:=]*) *"*([^\n"]*)"*')
 
-            if k then
+                       if k then
                                if x == "" then
-                                       table.insert(data, k)
+                                       table.insert(flags, k)
                                else
-                       data[k] = v
+                                       data[k] = v
                                end
-            end
-       end
+                       end
+               end
        end
 
-    return data
+       return data, flags
 end