Optimized conntrack (thanks to Joe Burpee)
[project/luci.git] / libs / sys / luasrc / sys.lua
1 --[[
2 LuCI - System library
3
4 Description:
5 Utilities for interaction with the Linux system
6
7 FileId:
8 $Id$
9
10 License:
11 Copyright 2008 Steven Barth <steven@midlink.org>
12
13 Licensed under the Apache License, Version 2.0 (the "License");
14 you may not use this file except in compliance with the License.
15 You may obtain a copy of the License at
16
17 http://www.apache.org/licenses/LICENSE-2.0
18
19 Unless required by applicable law or agreed to in writing, software
20 distributed under the License is distributed on an "AS IS" BASIS,
21 WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
22 See the License for the specific language governing permissions and
23 limitations under the License.
24
25 ]]--
26
27
28 local io = require "io"
29 local os = require "os"
30 local posix = require "posix"
31 local table = require "table"
32
33 local luci = {}
34 luci.util = require "luci.util"
35 luci.fs = require "luci.fs"
36 luci.ip = require "luci.ip"
37
38 local tonumber, ipairs, pairs, pcall = tonumber, ipairs, pairs, pcall
39
40
41 --- LuCI Linux and POSIX system utilities.
42 module "luci.sys"
43
44 --- Execute a given shell command and return the error code
45 -- @class function
46 -- @name call
47 -- @param ... Command to call
48 -- @return Error code of the command
49 function call(...)
50 return os.execute(...) / 256
51 end
52
53 --- Execute a given shell command and capture its standard output
54 -- @class function
55 -- @name exec
56 -- @param command Command to call
57 -- @return String containg the return the output of the command
58 exec = luci.util.exec
59
60 --- Invoke the luci-flash executable to write an image to the flash memory.
61 -- @param image Local path or URL to image file
62 -- @param kpattern Pattern of files to keep over flash process
63 -- @return Return value of os.execute()
64 function flash(image, kpattern)
65 local cmd = "luci-flash "
66 if kpattern then
67 cmd = cmd .. "-k '" .. kpattern:gsub("'", "") .. "' "
68 end
69 cmd = cmd .. "'" .. image:gsub("'", "") .. "' >/dev/null 2>&1"
70
71 return os.execute(cmd)
72 end
73
74 --- Retrieve information about currently mounted file systems.
75 -- @return Table containing mount information
76 function mounts()
77 local data = {}
78 local k = {"fs", "blocks", "used", "available", "percent", "mountpoint"}
79 local ps = luci.util.execi("df")
80
81 if not ps then
82 return
83 else
84 ps()
85 end
86
87 for line in ps do
88 local row = {}
89
90 local j = 1
91 for value in line:gmatch("[^%s]+") do
92 row[k[j]] = value
93 j = j + 1
94 end
95
96 if row[k[1]] then
97
98 -- this is a rather ugly workaround to cope with wrapped lines in
99 -- the df output:
100 --
101 -- /dev/scsi/host0/bus0/target0/lun0/part3
102 -- 114382024 93566472 15005244 86% /mnt/usb
103 --
104
105 if not row[k[2]] then
106 j = 2
107 line = ps()
108 for value in line:gmatch("[^%s]+") do
109 row[k[j]] = value
110 j = j + 1
111 end
112 end
113
114 table.insert(data, row)
115 end
116 end
117
118 return data
119 end
120
121 --- Retrieve environment variables. If no variable is given then a table
122 -- containing the whole environment is returned otherwise this function returns
123 -- the corresponding string value for the given name or nil if no such variable
124 -- exists.
125 -- @class function
126 -- @name getenv
127 -- @param var Name of the environment variable to retrieve (optional)
128 -- @return String containg the value of the specified variable
129 -- @return Table containing all variables if no variable name is given
130 getenv = posix.getenv
131
132 --- Determine the current hostname.
133 -- @return String containing the system hostname
134 function hostname()
135 return posix.uname("%n")
136 end
137
138 --- Returns the contents of a documented referred by an URL.
139 -- @param url The URL to retrieve
140 -- @param stream Return a stream instead of a buffer
141 -- @param target Directly write to target file name
142 -- @return String containing the contents of given the URL
143 function httpget(url, stream, target)
144 if not target then
145 local source = stream and io.popen or luci.util.exec
146 return source("wget -qO- '"..url:gsub("'", "").."'")
147 else
148 return os.execute("wget -qO '%s' '%s'" %
149 {target:gsub("'", ""), url:gsub("'", "")})
150 end
151 end
152
153 --- Returns the system load average values.
154 -- @return String containing the average load value 1 minute ago
155 -- @return String containing the average load value 5 minutes ago
156 -- @return String containing the average load value 15 minutes ago
157 -- @return String containing the active and total number of processes
158 -- @return String containing the last used pid
159 function loadavg()
160 local loadavg = io.lines("/proc/loadavg")()
161 return loadavg:match("^(.-) (.-) (.-) (.-) (.-)$")
162 end
163
164 --- Initiate a system reboot.
165 -- @return Return value of os.execute()
166 function reboot()
167 return os.execute("reboot >/dev/null 2>&1")
168 end
169
170 --- Returns the system type, cpu name and installed physical memory.
171 -- @return String containing the system or platform identifier
172 -- @return String containing hardware model information
173 -- @return String containing the total memory amount in kB
174 -- @return String containing the memory used for caching in kB
175 -- @return String containing the memory used for buffering in kB
176 -- @return String containing the free memory amount in kB
177 function sysinfo()
178 local cpuinfo = luci.fs.readfile("/proc/cpuinfo")
179 local meminfo = luci.fs.readfile("/proc/meminfo")
180
181 local system = cpuinfo:match("system typ.-:%s*([^\n]+)")
182 local model = ""
183 local memtotal = tonumber(meminfo:match("MemTotal:%s*(%d+)"))
184 local memcached = tonumber(meminfo:match("\nCached:%s*(%d+)"))
185 local memfree = tonumber(meminfo:match("MemFree:%s*(%d+)"))
186 local membuffers = tonumber(meminfo:match("Buffers:%s*(%d+)"))
187
188 if not system then
189 system = posix.uname("%m")
190 model = cpuinfo:match("model name.-:%s*([^\n]+)")
191 if not model then
192 model = cpuinfo:match("Processor.-:%s*([^\n]+)")
193 end
194 else
195 model = cpuinfo:match("cpu model.-:%s*([^\n]+)")
196 end
197
198 return system, model, memtotal, memcached, membuffers, memfree
199 end
200
201 --- Retrieves the output of the "logread" command.
202 -- @return String containing the current log buffer
203 function syslog()
204 return luci.util.exec("logread")
205 end
206
207 --- Retrieves the output of the "dmesg" command.
208 -- @return String containing the current log buffer
209 function dmesg()
210 return luci.util.exec("dmesg")
211 end
212
213 --- Generates a random id with specified length.
214 -- @param bytes Number of bytes for the unique id
215 -- @return String containing hex encoded id
216 function uniqueid(bytes)
217 local fp = io.open("/dev/urandom")
218 local chunk = { fp:read(bytes):byte(1, bytes) }
219 fp:close()
220
221 local hex = ""
222
223 local pattern = "%02X"
224 for i, byte in ipairs(chunk) do
225 hex = hex .. pattern:format(byte)
226 end
227
228 return hex
229 end
230
231 --- Returns the current system uptime stats.
232 -- @return String containing total uptime in seconds
233 -- @return String containing idle time in seconds
234 function uptime()
235 local loadavg = io.lines("/proc/uptime")()
236 return loadavg:match("^(.-) (.-)$")
237 end
238
239 --- LuCI system utilities / POSIX user group related functions.
240 -- @class module
241 -- @name luci.sys.group
242 group = {}
243
244 --- Returns information about a POSIX user group.
245 -- @class function
246 -- @name getgroup
247 -- @param group Group ID or name of a system user group
248 -- @return Table with information about the requested group
249 group.getgroup = posix.getgroup
250
251
252 --- LuCI system utilities / network related functions.
253 -- @class module
254 -- @name luci.sys.net
255 net = {}
256
257 --- Returns the current arp-table entries as two-dimensional table.
258 -- @return Table of table containing the current arp entries.
259 -- The following fields are defined for arp entry objects:
260 -- { "IP address", "HW address", "HW type", "Flags", "Mask", "Device" }
261 function net.arptable()
262 return _parse_delimited_table(io.lines("/proc/net/arp"), "%s%s+")
263 end
264
265 --- Returns conntrack information
266 -- @return Table with the currently tracked IP connections
267 function net.conntrack()
268 local connt = {}
269 if luci.fs.access("/proc/net/nf_conntrack", "r") then
270 for line in io.lines("/proc/net/nf_conntrack") do
271 line = line:match "^(.-( [^ =]+=).-)%2"
272 local entry, flags = _parse_mixed_record(line, " +")
273 entry.layer3 = flags[1]
274 entry.layer4 = flags[3]
275 for i=1, #entry do
276 entry[i] = nil
277 end
278
279 connt[#connt+1] = entry
280 end
281 elseif luci.fs.access("/proc/net/ip_conntrack", "r") then
282 for line in io.lines("/proc/net/ip_conntrack") do
283 line = line:match "^(.-( [^ =]+=).-)%2"
284 local entry, flags = _parse_mixed_record(line, " +")
285 entry.layer3 = "ipv4"
286 entry.layer4 = flags[1]
287 for i=1, #entry do
288 entry[i] = nil
289 end
290
291 connt[#connt+1] = entry
292 end
293 else
294 return nil
295 end
296 return connt
297 end
298
299 --- Determine the current IPv4 default route. If multiple default routes exist,
300 -- return the one with the lowest metric.
301 -- @return Table with the properties of the current default route.
302 -- The following fields are defined:
303 -- { "dest", "gateway", "metric", "refcount", "usecount", "irtt",
304 -- "flags", "device" }
305 function net.defaultroute()
306 local route = nil
307 for _, r in pairs(net.routes()) do
308 if r.dest:prefix() == 0 and (not route or route.metric > r.metric) then
309 route = r
310 end
311 end
312 return route
313 end
314
315 --- Determine the current IPv6 default route. If multiple default routes exist,
316 -- return the one with the lowest metric.
317 -- @return Table with the properties of the current default route.
318 -- The following fields are defined:
319 -- { "source", "dest", "nexthop", "metric", "refcount", "usecount",
320 -- "flags", "device" }
321 function net.defaultroute6()
322 local route = nil
323 for _, r in pairs(net.routes6()) do
324 if r.dest:prefix() == 0 and (not route or route.metric > r.metric) then
325 route = r
326 end
327 end
328 return route
329 end
330
331 --- Determine the names of available network interfaces.
332 -- @return Table containing all current interface names
333 function net.devices()
334 local devices = {}
335 for line in io.lines("/proc/net/dev") do
336 table.insert(devices, line:match(" *(.-):"))
337 end
338 return devices
339 end
340
341
342 --- Return information about available network interfaces.
343 -- @return Table containing all current interface names and their information
344 function net.deviceinfo()
345 local devices = {}
346 for line in io.lines("/proc/net/dev") do
347 local name, data = line:match("^ *(.-): *(.*)$")
348 if name and data then
349 devices[name] = luci.util.split(data, " +", nil, true)
350 end
351 end
352 return devices
353 end
354
355
356 -- Determine the MAC address belonging to the given IP address.
357 -- @param ip IPv4 address
358 -- @return String containing the MAC address or nil if it cannot be found
359 function net.ip4mac(ip)
360 local mac = nil
361
362 for i, l in ipairs(net.arptable()) do
363 if l["IP address"] == ip then
364 mac = l["HW address"]
365 end
366 end
367
368 return mac
369 end
370
371 --- Returns the current kernel routing table entries.
372 -- @return Table of tables with properties of the corresponding routes.
373 -- The following fields are defined for route entry tables:
374 -- { "dest", "gateway", "metric", "refcount", "usecount", "irtt",
375 -- "flags", "device" }
376 function net.routes()
377 local routes = { }
378
379 for line in io.lines("/proc/net/route") do
380
381 local dev, dst_ip, gateway, flags, refcnt, usecnt, metric,
382 dst_mask, mtu, win, irtt = line:match(
383 "([^%s]+)\t([A-F0-9]+)\t([A-F0-9]+)\t([A-F0-9]+)\t" ..
384 "(%d+)\t(%d+)\t(%d+)\t([A-F0-9]+)\t(%d+)\t(%d+)\t(%d+)"
385 )
386
387 if dev then
388 gateway = luci.ip.Hex( gateway, 32, luci.ip.FAMILY_INET4 )
389 dst_mask = luci.ip.Hex( dst_mask, 32, luci.ip.FAMILY_INET4 )
390 dst_ip = luci.ip.Hex(
391 dst_ip, dst_mask:prefix(dst_mask), luci.ip.FAMILY_INET4
392 )
393
394 routes[#routes+1] = {
395 dest = dst_ip,
396 gateway = gateway,
397 metric = tonumber(metric),
398 refcount = tonumber(refcnt),
399 usecount = tonumber(usecnt),
400 mtu = tonumber(mtu),
401 window = tonumber(window),
402 irtt = tonumber(irtt),
403 flags = tonumber(flags, 16),
404 device = dev
405 }
406 end
407 end
408
409 return routes
410 end
411
412 --- Returns the current ipv6 kernel routing table entries.
413 -- @return Table of tables with properties of the corresponding routes.
414 -- The following fields are defined for route entry tables:
415 -- { "source", "dest", "nexthop", "metric", "refcount", "usecount",
416 -- "flags", "device" }
417 function net.routes6()
418 local routes = { }
419
420 for line in io.lines("/proc/net/ipv6_route") do
421
422 local dst_ip, dst_prefix, src_ip, src_prefix, nexthop,
423 metric, refcnt, usecnt, flags, dev = line:match(
424 "([a-f0-9]+) ([a-f0-9]+) " ..
425 "([a-f0-9]+) ([a-f0-9]+) " ..
426 "([a-f0-9]+) ([a-f0-9]+) " ..
427 "([a-f0-9]+) ([a-f0-9]+) " ..
428 "([a-f0-9]+) +([^%s]+)"
429 )
430
431 src_ip = luci.ip.Hex(
432 src_ip, tonumber(src_prefix, 16), luci.ip.FAMILY_INET6, false
433 )
434
435 dst_ip = luci.ip.Hex(
436 dst_ip, tonumber(dst_prefix, 16), luci.ip.FAMILY_INET6, false
437 )
438
439 nexthop = luci.ip.Hex( nexthop, 128, luci.ip.FAMILY_INET6, false )
440
441 routes[#routes+1] = {
442 source = src_ip,
443 dest = dst_ip,
444 nexthop = nexthop,
445 metric = tonumber(metric, 16),
446 refcount = tonumber(refcnt, 16),
447 usecount = tonumber(usecnt, 16),
448 flags = tonumber(flags, 16),
449 device = dev
450 }
451 end
452
453 return routes
454 end
455
456 --- Tests whether the given host responds to ping probes.
457 -- @param host String containing a hostname or IPv4 address
458 -- @return Number containing 0 on success and >= 1 on error
459 function net.pingtest(host)
460 return os.execute("ping -c1 '"..host:gsub("'", '').."' >/dev/null 2>&1")
461 end
462
463
464 --- LuCI system utilities / process related functions.
465 -- @class module
466 -- @name luci.sys.process
467 process = {}
468
469 --- Get the current process id.
470 -- @class function
471 -- @name process.info
472 -- @return Number containing the current pid
473 process.info = posix.getpid
474
475 --- Retrieve information about currently running processes.
476 -- @return Table containing process information
477 function process.list()
478 local data = {}
479 local k
480 local ps = luci.util.execi("top -bn1")
481
482 if not ps then
483 return
484 end
485
486 while true do
487 local line = ps()
488 if not line then
489 return
490 end
491
492 k = luci.util.split(luci.util.trim(line), "%s+", nil, true)
493 if k[1] == "PID" then
494 break
495 end
496 end
497
498 for line in ps do
499 local row = {}
500
501 line = luci.util.trim(line)
502 for i, value in ipairs(luci.util.split(line, "%s+", #k-1, true)) do
503 row[k[i]] = value
504 end
505
506 local pid = tonumber(row[k[1]])
507 if pid then
508 data[pid] = row
509 end
510 end
511
512 return data
513 end
514
515 --- Set the gid of a process identified by given pid.
516 -- @param pid Number containing the process id
517 -- @param gid Number containing the Unix group id
518 -- @return Boolean indicating successful operation
519 -- @return String containing the error message if failed
520 -- @return Number containing the error code if failed
521 function process.setgroup(pid, gid)
522 return posix.setpid("g", pid, gid)
523 end
524
525 --- Set the uid of a process identified by given pid.
526 -- @param pid Number containing the process id
527 -- @param uid Number containing the Unix user id
528 -- @return Boolean indicating successful operation
529 -- @return String containing the error message if failed
530 -- @return Number containing the error code if failed
531 function process.setuser(pid, uid)
532 return posix.setpid("u", pid, uid)
533 end
534
535 --- Send a signal to a process identified by given pid.
536 -- @class function
537 -- @name process.signal
538 -- @param pid Number containing the process id
539 -- @param sig Signal to send (default: 15 [SIGTERM])
540 -- @return Boolean indicating successful operation
541 -- @return Number containing the error code if failed
542 process.signal = posix.kill
543
544
545 --- LuCI system utilities / user related functions.
546 -- @class module
547 -- @name luci.sys.user
548 user = {}
549
550 --- Retrieve user informations for given uid.
551 -- @class function
552 -- @name getuser
553 -- @param uid Number containing the Unix user id
554 -- @return Table containing the following fields:
555 -- { "uid", "gid", "name", "passwd", "dir", "shell", "gecos" }
556 user.getuser = posix.getpasswd
557
558 --- Test whether given string matches the password of a given system user.
559 -- @param username String containing the Unix user name
560 -- @param password String containing the password to compare
561 -- @return Boolean indicating wheather the passwords are equal
562 function user.checkpasswd(username, password)
563 local account = user.getuser(username)
564
565 if account then
566 local pwd = account.passwd
567 local shadowpw
568 if #pwd == 1 then
569 if luci.fs.stat("/etc/shadow") then
570 if not pcall(function()
571 for l in io.lines("/etc/shadow") do
572 shadowpw = l:match("^%s:([^:]+)" % username)
573 if shadowpw then
574 pwd = shadowpw
575 break
576 end
577 end
578 end) then
579 return nil, "Unable to access shadow-file"
580 end
581 end
582
583 if pwd == "!" then
584 return true
585 end
586 end
587
588 if pwd and #pwd > 0 and password and #password > 0 then
589 return (pwd == posix.crypt(password, pwd))
590 end
591 end
592
593 return false
594 end
595
596 --- Change the password of given user.
597 -- @param username String containing the Unix user name
598 -- @param password String containing the password to compare
599 -- @return Number containing 0 on success and >= 1 on error
600 function user.setpasswd(username, password)
601 if password then
602 password = password:gsub("'", "")
603 end
604
605 if username then
606 username = username:gsub("'", "")
607 end
608
609 local cmd = "(echo '"..password.."';sleep 1;echo '"..password.."')|"
610 cmd = cmd .. "passwd '"..username.."' >/dev/null 2>&1"
611 return os.execute(cmd)
612 end
613
614
615 --- LuCI system utilities / wifi related functions.
616 -- @class module
617 -- @name luci.sys.wifi
618 wifi = {}
619
620 --- Get iwconfig output for all wireless devices.
621 -- @return Table of tables containing the iwconfing output for each wifi device
622 function wifi.getiwconfig()
623 local cnt = luci.util.exec("/usr/sbin/iwconfig 2>/dev/null")
624 local iwc = {}
625
626 for i, l in pairs(luci.util.split(luci.util.trim(cnt), "\n\n")) do
627 local k = l:match("^(.-) ")
628 l = l:gsub("^(.-) +", "", 1)
629 if k then
630 local entry, flags = _parse_mixed_record(l)
631 if entry then
632 entry.flags = flags
633 end
634 iwc[k] = entry
635 end
636 end
637
638 return iwc
639 end
640
641 --- Get iwlist scan output from all wireless devices.
642 -- @return Table of tables contaiing all scan results
643 function wifi.iwscan(iface)
644 local siface = iface or ""
645 local cnt = luci.util.exec("iwlist "..siface.." scan 2>/dev/null")
646 local iws = {}
647
648 for i, l in pairs(luci.util.split(luci.util.trim(cnt), "\n\n")) do
649 local k = l:match("^(.-) ")
650 l = l:gsub("^[^\n]+", "", 1)
651 l = luci.util.trim(l)
652 if k then
653 iws[k] = {}
654 for j, c in pairs(luci.util.split(l, "\n Cell")) do
655 c = c:gsub("^(.-)- ", "", 1)
656 c = luci.util.split(c, "\n", 7)
657 c = table.concat(c, "\n", 1)
658 local entry, flags = _parse_mixed_record(c)
659 if entry then
660 entry.flags = flags
661 end
662 table.insert(iws[k], entry)
663 end
664 end
665 end
666
667 return iface and (iws[iface] or {}) or iws
668 end
669
670
671 --- LuCI system utilities / init related functions.
672 -- @class module
673 -- @name luci.sys.init
674 init = {}
675 init.dir = "/etc/init.d/"
676
677 --- Get the names of all installed init scripts
678 -- @return Table containing the names of all inistalled init scripts
679 function init.names()
680 local names = { }
681 for _, name in ipairs(luci.fs.glob(init.dir.."*")) do
682 names[#names+1] = luci.fs.basename(name)
683 end
684 return names
685 end
686
687 --- Test whether the given init script is enabled
688 -- @param name Name of the init script
689 -- @return Boolean indicating whether init is enabled
690 function init.enabled(name)
691 if luci.fs.access(init.dir..name) then
692 return ( call(init.dir..name.." enabled") == 0 )
693 end
694 return false
695 end
696
697 --- Get the index of he given init script
698 -- @param name Name of the init script
699 -- @return Numeric index value
700 function init.index(name)
701 if luci.fs.access(init.dir..name) then
702 return call("source "..init.dir..name.."; exit $START")
703 end
704 end
705
706 --- Enable the given init script
707 -- @param name Name of the init script
708 -- @return Boolean indicating success
709 function init.enable(name)
710 if luci.fs.access(init.dir..name) then
711 return ( call(init.dir..name.." enable") == 1 )
712 end
713 end
714
715 --- Disable the given init script
716 -- @param name Name of the init script
717 -- @return Boolean indicating success
718 function init.disable(name)
719 if luci.fs.access(init.dir..name) then
720 return ( call(init.dir..name.." disable") == 0 )
721 end
722 end
723
724
725 -- Internal functions
726
727 function _parse_delimited_table(iter, delimiter)
728 delimiter = delimiter or "%s+"
729
730 local data = {}
731 local trim = luci.util.trim
732 local split = luci.util.split
733
734 local keys = split(trim(iter()), delimiter, nil, true)
735 for i, j in pairs(keys) do
736 keys[i] = trim(keys[i])
737 end
738
739 for line in iter do
740 local row = {}
741 line = trim(line)
742 if #line > 0 then
743 for i, j in pairs(split(line, delimiter, nil, true)) do
744 if keys[i] then
745 row[keys[i]] = j
746 end
747 end
748 end
749 table.insert(data, row)
750 end
751
752 return data
753 end
754
755 function _parse_mixed_record(cnt, delimiter)
756 delimiter = delimiter or " "
757 local data = {}
758 local flags = {}
759
760 for i, l in pairs(luci.util.split(luci.util.trim(cnt), "\n")) do
761 for j, f in pairs(luci.util.split(luci.util.trim(l), delimiter, nil, true)) do
762 local k, x, v = f:match('([^%s][^:=]*) *([:=]*) *"*([^\n"]*)"*')
763
764 if k then
765 if x == "" then
766 table.insert(flags, k)
767 else
768 data[k] = v
769 end
770 end
771 end
772 end
773
774 return data, flags
775 end