diff --git a/README.md b/README.md index 43d662e..6e44c12 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,7 @@ Internet-detector is an application for checking the availability of the Interne **OpenWrt** >= 19.07. -**Dependences:** lua, luci-lib-nixio, libuci-lua. +**Dependences:** lua, luaposix, libuci-lua. **Features:** - It can run continuously as a system service or only in an open web interface. @@ -12,22 +12,22 @@ Internet-detector is an application for checking the availability of the Interne ![](https://github.com/gSpotx2f/luci-app-internet-detector/blob/master/screenshots/internet-led.jpg) - Performing actions when connecting and disconnecting the Internet (Restarting network, modem or device. Executing custom shell scripts). - Sending email notification when Internet access is restored. - - The daemon is written entirely in Lua using the nixio library. + - The daemon is written entirely in Lua using the luaposix library. ## Installation notes **OpenWrt >= 21.02:** opkg update - wget --no-check-certificate -O /tmp/internet-detector_0.6-0_all.ipk https://github.com/gSpotx2f/packages-openwrt/raw/master/current/internet-detector_0.6-0_all.ipk - opkg install /tmp/internet-detector_0.6-0_all.ipk - rm /tmp/internet-detector_0.6-0_all.ipk + wget --no-check-certificate -O /tmp/internet-detector_1.0-0_all.ipk https://github.com/gSpotx2f/packages-openwrt/raw/master/current/internet-detector_1.0-0_all.ipk + opkg install /tmp/internet-detector_1.0-0_all.ipk + rm /tmp/internet-detector_1.0-0_all.ipk /etc/init.d/internet-detector start /etc/init.d/internet-detector enable - wget --no-check-certificate -O /tmp/luci-app-internet-detector_0.6-1_all.ipk https://github.com/gSpotx2f/packages-openwrt/raw/master/current/luci-app-internet-detector_0.6-1_all.ipk - opkg install /tmp/luci-app-internet-detector_0.6-1_all.ipk - rm /tmp/luci-app-internet-detector_0.6-1_all.ipk + wget --no-check-certificate -O /tmp/luci-app-internet-detector_1.0-0_all.ipk https://github.com/gSpotx2f/packages-openwrt/raw/master/current/luci-app-internet-detector_1.0-0_all.ipk + opkg install /tmp/luci-app-internet-detector_1.0-0_all.ipk + rm /tmp/luci-app-internet-detector_1.0-0_all.ipk /etc/init.d/rpcd restart Email notification: @@ -36,9 +36,9 @@ Email notification: i18n-ru: - wget --no-check-certificate -O /tmp/luci-i18n-internet-detector-ru_0.6-1_all.ipk https://github.com/gSpotx2f/packages-openwrt/raw/master/current/luci-i18n-internet-detector-ru_0.6-1_all.ipk - opkg install /tmp/luci-i18n-internet-detector-ru_0.6-1_all.ipk - rm /tmp/luci-i18n-internet-detector-ru_0.6-1_all.ipk + wget --no-check-certificate -O /tmp/luci-i18n-internet-detector-ru_1.0-0_all.ipk https://github.com/gSpotx2f/packages-openwrt/raw/master/current/luci-i18n-internet-detector-ru_1.0-0_all.ipk + opkg install /tmp/luci-i18n-internet-detector-ru_1.0-0_all.ipk + rm /tmp/luci-i18n-internet-detector-ru_1.0-0_all.ipk **[OpenWrt 19.07](https://github.com/gSpotx2f/luci-app-internet-detector/tree/19.07)** @@ -46,3 +46,5 @@ i18n-ru: ![](https://github.com/gSpotx2f/luci-app-internet-detector/blob/master/screenshots/01.jpg) ![](https://github.com/gSpotx2f/luci-app-internet-detector/blob/master/screenshots/02.jpg) +![](https://github.com/gSpotx2f/luci-app-internet-detector/blob/master/screenshots/03.jpg) +![](https://github.com/gSpotx2f/luci-app-internet-detector/blob/master/screenshots/04.jpg) diff --git a/internet-detector/Makefile b/internet-detector/Makefile index a36fded..c893897 100644 --- a/internet-detector/Makefile +++ b/internet-detector/Makefile @@ -1,11 +1,11 @@ # -# (с) 2021 gSpot (https://github.com/gSpotx2f/luci-app-internet-detector) +# (с) 2023 gSpot (https://github.com/gSpotx2f/luci-app-internet-detector) # include $(TOPDIR)/rules.mk PKG_NAME:=internet-detector -PKG_VERSION:=0.6 +PKG_VERSION:=1.0 PKG_RELEASE:=0 PKG_MAINTAINER:=gSpot @@ -17,17 +17,19 @@ define Package/$(PKG_NAME) TITLE:=Internet detector URL:=https://github.com/gSpotx2f/luci-app-internet-detector PKGARCH:=all - DEPENDS:=+lua +luci-lib-nixio +libuci-lua + DEPENDS:=+lua +luaposix +libuci-lua endef define Package/$(PKG_NAME)/description Internet-detector is a small daemon for checking Internet availability. - Written in Lua using the nixio library. + Written in Lua using the luaposix library. endef define Package/$(PKG_NAME)/conffiles /etc/config/internet-detector +/etc/internet-detector/down-script.internet +/etc/internet-detector/up-script.internet endef define Build/Configure @@ -40,8 +42,8 @@ define Package/$(PKG_NAME)/install $(INSTALL_DIR) $(1)/etc/config $(INSTALL_CONF) ./files/etc/config/internet-detector $(1)/etc/config/internet-detector $(INSTALL_DIR) $(1)/etc/internet-detector - $(INSTALL_BIN) ./files/etc/internet-detector/down-script $(1)/etc/internet-detector/down-script - $(INSTALL_BIN) ./files/etc/internet-detector/up-script $(1)/etc/internet-detector/up-script + $(INSTALL_DATA) ./files/etc/internet-detector/down-script.internet $(1)/etc/internet-detector/down-script.internet + $(INSTALL_DATA) ./files/etc/internet-detector/up-script.internet $(1)/etc/internet-detector/up-script.internet $(INSTALL_DIR) $(1)/etc/init.d $(INSTALL_BIN) ./files/etc/init.d/internet-detector $(1)/etc/init.d/internet-detector $(INSTALL_DIR) $(1)/usr/bin diff --git a/internet-detector/files/etc/config/internet-detector b/internet-detector/files/etc/config/internet-detector index 5596073..37414c2 100644 --- a/internet-detector/files/etc/config/internet-detector +++ b/internet-detector/files/etc/config/internet-detector @@ -1,52 +1,48 @@ config main 'config' - option mode '2' + option mode '1' + option enable_logger '1' + +config ui 'ui' list hosts '8.8.8.8' list hosts '1.1.1.1' option check_type '0' option tcp_port '53' - option ui_interval_up '6' - option ui_interval_down '1' - option ui_connection_attempts '1' - option ui_connection_timeout '1' - option service_interval_up '30' - option service_interval_down '5' - option service_connection_attempts '2' - option service_connection_timeout '2' - option service_enable_logger '1' + option interval_up '6' + option interval_down '1' + option connection_attempts '1' + option connection_timeout '1' -config module 'mod_led_control' - option enabled '0' - -config module 'mod_reboot' - option enabled '0' - option dead_period '3600' - option force_reboot_delay '300' - -config module 'mod_network_restart' - option enabled '0' - option dead_period '900' - option attempts '1' - option restart_timeout '0' - -config module 'mod_modem_restart' - option enabled '0' - option dead_period '600' - option any_band '0' - -config module 'mod_public_ip' - option enabled '0' - option provider 'opendns1' - option interval '600' - option timeout '3' - -config module 'mod_email' - option enabled '0' - option alive_period '0' - option mail_smtp 'smtp.gmail.com' - option mail_smtp_port '587' - option mail_security 'tls' - -config module 'mod_user_scripts' - option enabled '0' - option alive_period '0' - option dead_period '0' +config instance 'internet' + option enabled '1' + list hosts '8.8.8.8' + list hosts '1.1.1.1' + option check_type '0' + option tcp_port '53' + option interval_up '30' + option interval_down '5' + option connection_attempts '2' + option connection_timeout '2' + option mod_led_control_enabled '0' + option mod_reboot_enabled '0' + option mod_reboot_dead_period '3600' + option mod_reboot_force_reboot_delay '300' + option mod_network_restart_enabled '0' + option mod_network_restart_dead_period '900' + option mod_network_restart_attempts '1' + option mod_network_restart_restart_timeout '0' + option mod_modem_restart_enabled '0' + option mod_modem_restart_dead_period '600' + option mod_modem_restart_any_band '0' + option mod_public_ip_enabled '0' + option mod_public_ip_provider 'opendns1' + option mod_public_ip_qtype '0' + option mod_public_ip_interval '600' + option mod_public_ip_timeout '3' + option mod_email_enabled '0' + option mod_email_alive_period '0' + option mod_email_mail_smtp 'smtp.gmail.com' + option mod_email_mail_smtp_port '587' + option mod_email_mail_security 'tls' + option mod_user_scripts_enabled '0' + option mod_user_scripts_alive_period '0' + option mod_user_scripts_dead_period '0' diff --git a/internet-detector/files/etc/init.d/internet-detector b/internet-detector/files/etc/init.d/internet-detector index 35ebf41..86a2a99 100755 --- a/internet-detector/files/etc/init.d/internet-detector +++ b/internet-detector/files/etc/init.d/internet-detector @@ -3,17 +3,23 @@ START=97 STOP=01 -ID="/usr/bin/internet-detector" +PROG=/usr/bin/internet-detector + +config_app() { + config_get enabled "$1" enabled "0" + if [ $enabled = "1" ]; then + $PROG service "$1" + fi +} start() { - $ID + config_load internet-detector + config_get mode "config" mode "0" + if [ $mode = "1" ]; then + config_foreach config_app "instance" + fi } stop() { - $ID stop -} - -restart() { - stop - start + $PROG stop } diff --git a/internet-detector/files/etc/internet-detector/down-script b/internet-detector/files/etc/internet-detector/down-script.internet old mode 100755 new mode 100644 similarity index 100% rename from internet-detector/files/etc/internet-detector/down-script rename to internet-detector/files/etc/internet-detector/down-script.internet diff --git a/internet-detector/files/etc/internet-detector/up-script b/internet-detector/files/etc/internet-detector/up-script.internet old mode 100755 new mode 100644 similarity index 100% rename from internet-detector/files/etc/internet-detector/up-script rename to internet-detector/files/etc/internet-detector/up-script.internet diff --git a/internet-detector/files/usr/bin/internet-detector b/internet-detector/files/usr/bin/internet-detector index 87c2cc3..a63319c 100755 --- a/internet-detector/files/usr/bin/internet-detector +++ b/internet-detector/files/usr/bin/internet-detector @@ -5,112 +5,103 @@ Dependences: lua - luci-lib-nixio + luaposix libuci-lua - (с) 2021 gSpot (https://github.com/gSpotx2f/luci-app-internet-detector) + (с) 2023 gSpot (https://github.com/gSpotx2f/luci-app-internet-detector) --]] --- Default settings - -local Config = { - mode = 2, - enableLogger = true, - intervalUp = 30, - intervalDown = 5, - connectionAttempts = 2, - connectionTimeout = 2, - UIConnectionAttempts = 1, - UIConnectionTimeout = 1, - hosts = { - [1] = "8.8.8.8", - [2] = "1.1.1.1", - }, - tcpPort = 53, - pingPacketSize = 56, - iface = nil, - checkType = 0, -- 0: TCP, 1: ping - hostname = "OpenWrt", - appName = "internet-detector", - commonDir = "/tmp/run", - debugLog = "/tmp/internet-detector.debug", - pingCmd = "/bin/ping", - pingParams = "-c 1", - debug = false, - modules = {}, - parsedHosts = {}, -} -Config.configDir = string.format("/etc/%s", Config.appName) -Config.modulesDir = string.format("/usr/lib/%s", Config.appName) -Config.pidFile = string.format("%s/%s.pid", Config.commonDir, Config.appName) -Config.statusFile = string.format("%s/%s.status", Config.commonDir, Config.appName) - -- Importing packages -local function prequire(package) +local dirent = require("posix.dirent") +local fcntl = require("posix.fcntl") +local signal = require("posix.signal") +local socket = require("posix.sys.socket") +local stat = require("posix.sys.stat") +local syslog = require("posix.syslog") +local time = require("posix.time") +local unistd = require("posix.unistd") +local uci = require("uci") + +-- Default settings + +local InternetDetector = { + mode = 0, + enableLogger = true, + hostname = "OpenWrt", + appName = "internet-detector", + commonDir = "/tmp/run", + pingCmd = "/bin/ping", + pingParams = "-c 1", + debug = false, + serviceConfig = { + hosts = { + [1] = "8.8.8.8", + [2] = "1.1.1.1", + }, + check_type = 0, -- 0: TCP, 1: ICMP + tcp_port = 53, + icmp_packet_size = 56, + interval_up = 30, + interval_down = 5, + connection_attempts = 2, + connection_timeout = 2, + iface = nil, + instance = nil, + }, + modules = {}, + parsedHosts = {}, +} +InternetDetector.configDir = string.format("/etc/%s", InternetDetector.appName) +InternetDetector.modulesDir = string.format("/usr/lib/%s", InternetDetector.appName) + +-- Loading settings from UCI +local uciCursor = uci.cursor() +InternetDetector.mode = tonumber( + uciCursor:get(InternetDetector.appName, "config", "mode")) +InternetDetector.enableLogger = (tonumber( + uciCursor:get(InternetDetector.appName, "config", "enable_logger")) ~= 0) +local hostname = uciCursor:get("system", "@[0]", "hostname") +if hostname ~= nil then + InternetDetector.hostname = hostname +end + +local RUNNING + +function InternetDetector:prequire(package) local retVal, pkg = pcall(require, package) return retVal and pkg end -local nixio = prequire("nixio") -if not nixio then - error("You need to install nixio...") +function InternetDetector:loadUCIConfig(sType, instance) + local success + local num = 0 + uciCursor:foreach( + self.appName, + sType, + function(s) + if s[".name"] == instance then + for k, v in pairs(s) do + if type(v) == "string" and v:match("^[%d]+$") then + v = tonumber(v) + end + self.serviceConfig[k] = v + end + success = true + self.serviceConfig.instanceNum = num + end + num = num + 1 + end + ) + self.serviceConfig.instance = instance + self.pidFile = string.format( + "%s/%s.%s.pid", self.commonDir, self.appName, instance) + self.statusFile = string.format( + "%s/%s.%s.status", self.commonDir, self.appName, instance) + return success end -local uci = prequire("uci") -if not uci then - error("You need to install libuci-lua...") -end - --- Loading settings from UCI - -local uciCursor = uci.cursor() -Config.mode = tonumber(uciCursor:get( - Config.appName, "config", "mode")) -Config.enableLogger = (tonumber(uciCursor:get( - Config.appName, "config", "service_enable_logger")) ~= 0) -Config.intervalUp = tonumber(uciCursor:get( - Config.appName, "config", "service_interval_up")) -Config.intervalDown = tonumber(uciCursor:get( - Config.appName, "config", "service_interval_down")) -Config.connectionAttempts = tonumber(uciCursor:get( - Config.appName, "config", "service_connection_attempts")) -Config.connectionTimeout = tonumber(uciCursor:get( - Config.appName, "config", "service_connection_timeout")) -Config.UIConnectionAttempts = tonumber(uciCursor:get( - Config.appName, "config", "ui_connection_attempts")) -Config.UIConnectionTimeout = tonumber(uciCursor:get( - Config.appName, "config", "ui_connection_timeout")) -Config.hosts = uciCursor:get(Config.appName, "config", "hosts") - -local tcpPort = uciCursor:get( - Config.appName, "config", "tcp_port") -if tcpPort ~= nil then - Config.tcpPort = tonumber(tcpPort) -end - -local pingPacketSize = uciCursor:get( - Config.appName, "config", "ping_packet_size") -if pingPacketSize ~= nil then - Config.pingPacketSize = tonumber(pingPacketSize) -end - -local iface = uciCursor:get( - Config.appName, "config", "iface") -if iface ~= nil then - Config.iface = iface -end - -Config.checkType = tonumber(uciCursor:get( - Config.appName, "config", "check_type")) - -local hostname = uciCursor:get("system", "@[0]", "hostname") -if hostname ~= nil then - Config.hostname = hostname -end - - -local function writeValueToFile(filePath, str) +function InternetDetector:writeValueToFile(filePath, str) local retValue = false local fh = io.open(filePath, "w") if fh then @@ -122,7 +113,7 @@ local function writeValueToFile(filePath, str) return retValue end -local function readValueFromFile(filePath) +function InternetDetector:readValueFromFile(filePath) local retValue local fh = io.open(filePath, "r") if fh then @@ -132,72 +123,97 @@ local function readValueFromFile(filePath) return retValue end -local function statusJson(inet, t) - local lines = { [1] = string.format('"inet":%d', inet) } +function InternetDetector:statusJson(inet, instance, t) + local lines = { [1] = string.format( + '{"instance":"%s","num":"%d","inet":%d', + instance, + self.serviceConfig.instanceNum, + inet)} if t then for k, v in pairs(t) do lines[#lines + 1] = string.format('"%s":"%s"', k, v) end end - return "{" .. table.concat(lines, ",") .. "}" + return table.concat(lines, ",") .. "}" end -local function writeLogMessage(level, msg) - if Config.enableLogger then - nixio.syslog(level, msg) +function InternetDetector:writeLogMessage(level, msg) + if self.enableLogger then + local levels = { + emerg = syslog.LOG_EMERG, + alert = syslog.LOG_ALERT, + crit = syslog.LOG_CRIT, + err = syslog.LOG_ERR, + warning = syslog.LOG_WARNING, + notice = syslog.LOG_NOTICE, + info = syslog.LOG_INFO, + debug = syslog.LOG_DEBUG, + } + syslog.syslog(levels[level] or syslog.LOG_INFO, string.format( + "%s: %s", self.serviceConfig.instance or "", msg)) end end -local function loadModules() - package.path = string.format("%s;%s/?.lua", package.path, Config.modulesDir) - Config.modules = {} - uciCursor:foreach( - Config.appName, - "module", - function(s) - local mod_name = s[".name"] - if mod_name and s.enabled == "1" then - local m = prequire(mod_name) - if m then - m.config = Config - m.syslog = writeLogMessage - m.writeValue = writeValueToFile - m.readValue = readValueFromFile - m:init(s) - Config.modules[#Config.modules + 1] = m +function InternetDetector:loadModules() + package.path = string.format("%s;%s/?.lua", package.path, self.modulesDir) + self.modules = {} + local ok, modulesDir = pcall(dirent.files, self.modulesDir) + if ok then + for item in modulesDir do + if item:match("^mod_") then + local modName = item:gsub("%.lua$", "") + local modConfig = {} + for k, v in pairs(self.serviceConfig) do + if k:match("^" .. modName) then + modConfig[k:gsub("^" .. modName .. "_", "")] = v + end + end + if modConfig.enabled == 1 then + local m = self:prequire(modName) + if m then + m.config = self + m.syslog = function(level, msg) self:writeLogMessage(level, msg) end + m.writeValue = function(filePath, str) return self:writeValueToFile(filePath, str) end + m.readValue = function(filePath) return self:readValueFromFile(filePath) end + m:init(modConfig) + self.modules[#self.modules + 1] = m + end end end end - ) -end - -local function parseHost(host) - local addr, port = host:match("^([^:]+):?(%d*)") - return addr, tonumber(port) or false -end - -local function parseHosts() - Config.parsedHosts = {} - for k, v in ipairs(Config.hosts) do - local addr, port = parseHost(v) - Config.parsedHosts[k] = { addr = addr, port = port } end end -local function pingHost(host) +function InternetDetector:parseHost(host) + local addr, port = host:match("^([^%[%]:]+):?(%d?%d?%d?%d?%d?)$") + if not addr then + addr, port = host:match("^%[?([^%[%]]+)%]?:?(%d?%d?%d?%d?%d?)$") + end + return addr, tonumber(port) +end + +function InternetDetector:parseHosts() + self.parsedHosts = {} + for k, v in ipairs(self.serviceConfig.hosts) do + local addr, port = self:parseHost(v) + self.parsedHosts[k] = { addr = addr, port = port } + end +end + +function InternetDetector:pingHost(host) local ping = string.format( "%s %s -W %d -s %d%s %s > /dev/null 2>&1", - Config.pingCmd, - Config.pingParams, - Config.connectionTimeout, - Config.pingPacketSize, - Config.iface and (" -I " .. Config.iface) or "", + self.pingCmd, + self.pingParams, + self.serviceConfig.connection_timeout, + self.serviceConfig.icmp_packet_size, + self.serviceConfig.iface and (" -I " .. self.serviceConfig.iface) or "", host ) local retCode = os.execute(ping) -- Debug - if Config.debug then + if self.debug then io.stdout:write(string.format( "--- Ping ---\ntime = %s\n%s\nretCode = %s\n", os.time(), ping, retCode) ) @@ -207,53 +223,94 @@ local function pingHost(host) return retCode end -local function TCPConnectionToHost(host, port) - local retCode = 1 - local addrInfo = nixio.getaddrinfo(host, "any") - if addrInfo then - local family = addrInfo[1].family - if family then - local socket = nixio.socket(family, "stream") - socket:setopt("socket", "sndtimeo", Config.connectionTimeout) - socket:setopt("socket", "rcvtimeo", Config.connectionTimeout) - if Config.iface then - socket:setopt("socket", "bindtodevice", Config.iface) - end - local success = socket:connect(host, port or Config.tcpPort) +function InternetDetector:TCPConnectionToHost(host, port) + local retCode = 1 + local saTable, errMsg, errNum = socket.getaddrinfo(host, port or self.serviceConfig.tcp_port) - -- Debug - if Config.debug then - local sockAddr, sockPort = socket:getsockname() - local peerAddr, peerPort = socket:getpeername() + if not saTable then + if self.debug then + io.stdout:write(string.format( + "GETADDRINFO ERROR: %s, %s\n", errMsg, errNum)) + end + else + local family = saTable[1].family + + if family then + local sock, errMsg, errNum = socket.socket(family, socket.SOCK_STREAM, 0) + + if not sock then + if self.debug then + io.stdout:write(string.format( + "SOCKET ERROR: %s, %s\n", errMsg, errNum)) + end + return retCode + end + + socket.setsockopt(sock, socket.SOL_SOCKET, + socket.SO_SNDTIMEO, self.serviceConfig.connection_timeout, 0) + socket.setsockopt(sock, socket.SOL_SOCKET, + socket.SO_RCVTIMEO, self.serviceConfig.connection_timeout, 0) + + if self.serviceConfig.iface then + local ok, errMsg, errNum = socket.setsockopt(sock, socket.SOL_SOCKET, + socket.SO_BINDTODEVICE, self.serviceConfig.iface) + if not ok then + if self.debug then + io.stdout:write(string.format( + "SOCKET ERROR: %s, %s\n", errMsg, errNum)) + end + return retCode + end + end + + local success = socket.connect(sock, saTable[1]) + + if self.debug then + if not success then + io.stdout:write(string.format( + "SOCKET CONNECT ERROR: %s\n", tostring(success))) + end + local sockTable, err_s, e_s = socket.getsockname(sock) + local peerTable, err_p, e_p = socket.getpeername(sock) + if not sockTable then + sockTable = {} + io.stdout:write( + string.format("SOCKET ERROR: %s, %s\n", err_s, e_s)) + end + if not peerTable then + peerTable = {} + io.stdout:write( + string.format("SOCKET ERROR: %s, %s\n", err_p, e_p)) + end io.stdout:write(string.format( - "--- TCP ---\ntime = %s\nconnectionTimeout = %s\niface = %s\nhost:port = %s:%s\nsockname = %s:%s\npeername = %s:%s\nsuccess = %s\n", + "--- TCP ---\ntime = %s\nconnection_timeout = %s\niface = %s\nhost:port = [%s]:%s\nsockname = [%s]:%s\npeername = [%s]:%s\nsuccess = %s\n", os.time(), - Config.connectionTimeout, - tostring(Config.iface), + self.serviceConfig.connection_timeout, + tostring(self.serviceConfig.iface), host, - port or Config.tcpPort, - tostring(sockAddr), - tostring(sockPort), - tostring(peerAddr), - tostring(peerPort), + port or self.serviceConfig.tcp_port, + tostring(sockTable.addr), + tostring(sockTable.port), + tostring(peerTable.addr), + tostring(peerTable.port), tostring(success)) ) io.stdout:flush() end - socket:close() + unistd.close(sock) retCode = success and 0 or 1 end end return retCode end -local function checkHosts() - local checkFunc = (Config.checkType == 1) and pingHost or TCPConnectionToHost +function InternetDetector:checkHosts() + local checkFunc = (self.serviceConfig.check_type == 1) and self.pingHost or self.TCPConnectionToHost local retCode = 1 - for k, v in ipairs(Config.parsedHosts) do - for i = 1, Config.connectionAttempts do - if checkFunc(v.addr, v.port) == 0 then + for k, v in ipairs(self.parsedHosts) do + for i = 1, self.serviceConfig.connection_attempts do + if checkFunc(self, v.addr, v.port) == 0 then retCode = 0 break end @@ -265,37 +322,51 @@ local function checkHosts() return retCode end -local function main() - local lastStatus, currentStatus, timeNow, timeDiff, lastTime - local interval = Config.intervalUp - local counter = 0 +function InternetDetector:breakMain(signo) + RUNNING = false +end - while true do +function InternetDetector:main() + signal.signal(signal.SIGTERM, function(signo) self:breakMain(signo) end) + signal.signal(signal.SIGINT, function(signo) self:breakMain(signo) end) + signal.signal(signal.SIGQUIT, function(signo) self:breakMain(signo) end) + + local lastStatus, currentStatus, timeNow, timeDiff, lastTime + local interval = self.serviceConfig.interval_up + local counter = 0 + local onStart = true + RUNNING = true + while RUNNING do if counter == 0 or counter >= interval then - currentStatus = checkHosts() - if not nixio.fs.access(Config.statusFile, "r") then - writeValueToFile(Config.statusFile, statusJson(currentStatus)) + currentStatus = self:checkHosts() + if onStart or not stat.stat(self.statusFile) then + self:writeValueToFile(self.statusFile, self:statusJson( + currentStatus, self.serviceConfig.instance)) + onStart = false end if currentStatus == 0 then - interval = Config.intervalUp + interval = self.serviceConfig.interval_up if lastStatus ~= nil and currentStatus ~= lastStatus then - writeValueToFile(Config.statusFile, statusJson(currentStatus)) - writeLogMessage("notice", "Internet connected") + self:writeValueToFile(self.statusFile, self:statusJson( + currentStatus, self.serviceConfig.instance)) + self:writeLogMessage("notice", "Connected") end else - interval = Config.intervalDown + interval = self.serviceConfig.interval_down if lastStatus ~= nil and currentStatus ~= lastStatus then - writeValueToFile(Config.statusFile, statusJson(currentStatus)) - writeLogMessage("notice", "Internet disconnected") + self:writeValueToFile(self.statusFile, self:statusJson( + currentStatus, self.serviceConfig.instance)) + self:writeLogMessage("notice", "Disconnected") end end + counter = 0 end timeDiff = 0 - for _, e in ipairs(Config.modules) do - timeNow = nixio.sysinfo().uptime + for _, e in ipairs(self.modules) do + timeNow = time.clock_gettime(time.CLOCK_MONOTONIC).tv_sec if lastTime then timeDiff = timeDiff + timeNow - lastTime else @@ -306,142 +377,156 @@ local function main() end local modulesStatus = {} - for k, v in ipairs(Config.modules) do + for k, v in ipairs(self.modules) do if v.status ~= nil then modulesStatus[v.name] = v.status end end if next(modulesStatus) then - writeValueToFile(Config.statusFile, statusJson(currentStatus, modulesStatus)) + self:writeValueToFile(self.statusFile, self:statusJson( + currentStatus, self.serviceConfig.instance, modulesStatus)) end lastStatus = currentStatus - nixio.nanosleep(1) + unistd.sleep(1) counter = counter + 1 end end -local function removeProcessFiles() - os.remove(Config.pidFile) - os.remove(Config.statusFile) +function InternetDetector:removeProcessFiles() + os.remove(string.format( + "%s/%s.%s.pid", self.commonDir, self.appName, self.serviceConfig.instance)) + os.remove(string.format( + "%s/%s.%s.status", self.commonDir, self.appName, self.serviceConfig.instance)) end -local function status() - if nixio.fs.access(Config.pidFile, "r") then - return "running" - else - return "stoped" - end -end - -local function poll(attempts, timeout) - if Config.mode == 1 then - Config.connectionAttempts = Config.UIConnectionAttempts - Config.connectionTimeout = Config.UIConnectionTimeout - end - if attempts then - Config.connectionAttempts = attempts - end - if timeout then - Config.connectionTimeout = timeout - end - if checkHosts() == 0 then - return statusJson(0) - else - return statusJson(1) - end -end - -local function inetStatus(json) - local inetStat = 1 - if nixio.fs.access(Config.statusFile, "r") then - local inetStatVal = readValueFromFile(Config.statusFile) - inetStat = inetStatVal - elseif Config.mode == 1 then - inetStat = poll() - else - os.exit(126) - end - if not json then - local sVal = inetStat:match('"inet":[0-9]') - if sVal then - sVal = sVal:match("[0-9]") - inetStat = (tonumber(sVal) == 0) and "up" or "down" +function InternetDetector:status() + local ok, commonDir = pcall(dirent.files, self.commonDir) + if ok then + local appName = self.appName:gsub("-", "%%-") + for item in commonDir do + if item:match("^" .. appName .. ".-%.pid$") then + return "running" + end end end + return "stoped" +end + +function InternetDetector:poll() + local retCode = self:checkHosts() + return string.format('{"instances":[%s]}', self:statusJson( + retCode, self.serviceConfig.instance)) +end + +function InternetDetector:inetStatus() + local inetStat = '{"instances":[]}' + local ok, commonDir = pcall(dirent.files, self.commonDir) + if ok then + local appName = self.appName:gsub("-", "%%-") + local lines = {} + for item in commonDir do + if item:match("^" .. appName .. ".-%.status$") then + lines[#lines + 1] = self:readValueFromFile( + string.format("%s/%s", self.commonDir, item)) + end + end + inetStat = '{"instances":[' .. table.concat(lines, ",") .. "]}" + end return inetStat end -local function stop() - local pidValue - if Config.enableLogger then - nixio.openlog(Config.appName) - end - if nixio.fs.access(Config.pidFile, "r") then - pidValue = readValueFromFile(Config.pidFile) +function InternetDetector:stopInstance(pidFile) + local retVal + if stat.stat(pidFile) then + pidValue = self:readValueFromFile(pidFile) if pidValue then - local success + local ok, errMsg, errNum for i = 0, 10 do - success = nixio.kill(tonumber(pidValue), 15) - if success then + ok, errMsg, errNum = signal.kill(tonumber(pidValue), signal.SIGTERM) + if ok then break end end - if not success then - io.stderr:write(string.format('No such process: "%s"\n', pidValue)) + if not ok then + io.stderr:write(string.format( + 'Process stop error: %s (%s). PID: "%s"\n', errMsg, errNum, pidValue)) end - writeLogMessage("info", string.format("[%s] stoped", pidValue)) - removeProcessFiles() + if errNum == 3 then + os.remove(pidFile) + end + retVal = true end end if not pidValue then io.stderr:write( string.format('PID file "%s" does not exist. %s not running?\n', - Config.pidFile, Config.appName)) - end - if Config.enableLogger then - nixio.closelog() + pidFile, self.appName)) end + return retVal end -local function preRun() - -- Exit if internet-detector mode != 2(Service) - if Config.mode ~= 2 then - io.stderr:write(string.format('Start failed, mode != 2\n', Config.appName)) +function InternetDetector:stop() + local appName = self.appName:gsub("-", "%%-") + local success + for i = 0, 10 do + success = true + local ok, commonDir = pcall(dirent.files, self.commonDir) + if ok then + for item in commonDir do + if item:match("^" .. appName .. ".-%.pid$") then + self:stopInstance(string.format("%s/%s", self.commonDir, item)) + success = false + end + end + if success then + break + end + unistd.sleep(1) + else + break + end + end + +end + +function InternetDetector:preRun() + -- Exit if internet-detector mode != 1(Service) + if self.mode ~= 1 then + io.stderr:write(string.format('Start failed, mode != 1\n', self.appName)) os.exit(0) end - if nixio.fs.access(Config.pidFile, "r") then + if stat.stat(self.pidFile) then io.stderr:write( string.format('PID file "%s" already exist. %s already running?\n', - Config.pidFile, Config.appName)) + self.pidFile, self.appName)) return false end return true end -local function run() - local pidValue = nixio.getpid() - writeValueToFile(Config.pidFile, pidValue) - if Config.enableLogger then - nixio.openlog(Config.appName, "pid") +function InternetDetector:run() + local pidValue = unistd.getpid() + self:writeValueToFile(self.pidFile, pidValue) + if self.enableLogger then + syslog.openlog(self.appName, syslog.LOG_PID, syslog.LOG_DAEMON) end - writeLogMessage("info", "started") - loadModules() + self:writeLogMessage("info", "started") + self:loadModules() -- Loaded modules local modules = {} - for _, v in ipairs(Config.modules) do + for _, v in ipairs(self.modules) do modules[#modules + 1] = string.format("%s", v.name) end if #modules > 0 then - writeLogMessage( + self:writeLogMessage( "info", string.format("Loaded modules: %s", table.concat(modules, ", ")) ) end -- Debug - if Config.debug then - + if self.debug then local function inspectTable() local tables = {}, f f = function(t, prefix) @@ -459,92 +544,125 @@ local function run() end io.stdout:write("--- Config ---\n") - inspectTable()(Config, "Config.") + inspectTable()(self, "self.") io.stdout:flush() end - main() - if Config.enableLogger then - nixio.closelog() + self:writeValueToFile( + self.statusFile, self:statusJson(-1, self.serviceConfig.instance)) + + self:main() + + self:removeProcessFiles() + if self.enableLogger then + self:writeLogMessage("info", "stoped") + syslog.closelog() end end -local function noDaemon() - if not preRun() then +function InternetDetector:noDaemon() + if not self:preRun() then return end - run() + self:run() end -local function daemon(debug) - if not preRun() then +function InternetDetector:daemon() + if not self:preRun() then return end -- UNIX double fork - if nixio.fork() == 0 then - nixio.setsid() - if nixio.fork() == 0 then - nixio.chdir("/") - nixio.umask(0) - local output = "/dev/null" - if debug then - output = Config.debugLog - Config.debug = true - end + if unistd.fork() == 0 then + unistd.setpid("s") + if unistd.fork() == 0 then + unistd.chdir("/") + stat.umask(0) + local devnull = fcntl.open("/dev/null", fcntl.O_RDWR) io.stdout:flush() io.stderr:flush() - nixio.dup(io.open("/dev/null", "r"), io.stdin) - nixio.dup(io.open(output, "a+"), io.stdout) - nixio.dup(io.open(output, "a+"), io.stderr) - run() + unistd.dup2(devnull, 0) -- io.stdin + unistd.dup2(devnull, 1) -- io.stdout + unistd.dup2(devnull, 2) -- io.stderr + self:run() + unistd.close(devnull) end os.exit(0) end os.exit(0) end -local function restart() - stop() - daemon() +function InternetDetector:setServiceConfig(instance) + if self:loadUCIConfig("instance", instance) then + self:parseHosts() + return true + end end -- Main section -parseHosts() - local function help() return string.format( - "Usage: %s [start|stop|restart|no-daemon|debug|status|inet-status|inet-status-json|poll [] []|--help]", + "Usage: %s service | nodaemon | debug | stop | status | inet-status | poll [] [] | --help", arg[0] ) end local helpArgs = { ["-h"] = true, ["--help"] = true, ["help"] = true } -if arg[1] == "start" or #arg == 0 then - daemon() -elseif arg[1] == "no-daemon" then - noDaemon() -elseif arg[1] == "debug" then - daemon(true) -elseif arg[1] == "stop" then - stop() -elseif arg[1] == "restart" then - restart() -elseif arg[1] == "status" then - print(status()) -elseif arg[1] == "inet-status" then - print(inetStatus()) -elseif arg[1] == "inet-status-json" then - print(inetStatus(true)) -elseif arg[1] == "poll" then - local attempts, timeout - if arg[2] and arg[2]:match("[0-9]+") then - attempts = tonumber(arg[2]) - if arg[3] and arg[3]:match("[0-9]+") then - timeout = tonumber(arg[3]) +if arg[1] == "service" then + if arg[2] then + if InternetDetector:setServiceConfig(arg[2]) then + InternetDetector:daemon() + else + os.exit(126) end + else + print(help()) + os.exit(1) + end +elseif arg[1] == "nodaemon" then + if arg[2] then + if InternetDetector:setServiceConfig(arg[2]) then + InternetDetector:noDaemon() + else + os.exit(126) + end + else + print(help()) + os.exit(1) + end +elseif arg[1] == "debug" then + if arg[2] then + if InternetDetector:setServiceConfig(arg[2]) then + InternetDetector.debug = true + InternetDetector:noDaemon() + else + os.exit(126) + end + else + print(help()) + os.exit(1) + end +elseif arg[1] == "stop" then + InternetDetector:stop() +elseif arg[1] == "status" then + print(InternetDetector:status()) +elseif arg[1] == "inet-status" then + print(InternetDetector:inetStatus()) +elseif arg[1] == "poll" then + if InternetDetector:loadUCIConfig("ui", "ui") then + InternetDetector:parseHosts() + + if arg[2] and arg[2]:match("[0-9]+") then + InternetDetector.serviceConfig.connection_attempts = tonumber(arg[2]) + if arg[3] and arg[3]:match("[0-9]+") then + InternetDetector.serviceConfig.connection_timeout = tonumber(arg[3]) + end + end + + print(InternetDetector:poll()) + else + os.exit(126) end - print(poll(attempts, timeout)) elseif helpArgs[arg[1]] then print(help()) else diff --git a/internet-detector/files/usr/lib/internet-detector/mod_email.lua b/internet-detector/files/usr/lib/internet-detector/mod_email.lua index aa9665a..e1c15a0 100644 --- a/internet-detector/files/usr/lib/internet-detector/mod_email.lua +++ b/internet-detector/files/usr/lib/internet-detector/mod_email.lua @@ -2,11 +2,13 @@ Dependences: mailsend --]] -local nixio = require("nixio") +local unistd = require("posix.unistd") local Module = { name = "mod_email", - config = {}, + config = { + debug = false, + }, syslog = function(level, msg) return true end, writeValue = function(filePath, str) return false end, readValue = function(filePath) return nil end, @@ -45,7 +47,7 @@ function Module:init(t) self.mailSmtpPort = t.mail_smtp_port self.mailSecurity = t.mail_security - if nixio.fs.access(self.mta, "x") then + if unistd.access(self.mta, "x") then self._enabled = true else self._enabled = false @@ -66,16 +68,19 @@ end function Module:sendMessage(msg) local verboseArg = "" + -- Debug if self.config.debug then verboseArg = " -v" - io.stdout:write("--- mod_email ---\n") + io.stdout:write(string.format("--- %s ---\n", self.name)) io.stdout:flush() end + local securityArgs = "-starttls -auth-login" if self.mailSecurity == "ssl" then securityArgs = "-ssl -auth" end + local mtaCmd = string.format( '%s%s %s -smtp "%s" -port %s -cs utf-8 -user "%s" -pass "%s" -f "%s" -t "%s" -sub "%s" -M "%s"', self.mta, verboseArg, securityArgs, self.mailSmtp, self.mailSmtpPort, @@ -102,12 +107,14 @@ function Module:run(currentStatus, lastStatus, timeDiff) self._msgSent = false self._lastConnection = nil if not self._disconnected then - self._disconnected = true + self._disconnected = true if not self._lastDisconnection then self._lastDisconnection = os.date("%Y.%m.%d %H:%M:%S", os.time()) end end + else + if not self._msgSent then if not self._lastConnection then diff --git a/internet-detector/files/usr/lib/internet-detector/mod_led_control.lua b/internet-detector/files/usr/lib/internet-detector/mod_led_control.lua index 935cddc..a888206 100644 --- a/internet-detector/files/usr/lib/internet-detector/mod_led_control.lua +++ b/internet-detector/files/usr/lib/internet-detector/mod_led_control.lua @@ -1,5 +1,6 @@ -local nixio = require("nixio") +local unistd = require("posix.unistd") +local dirent = require("posix.dirent") local Module = { name = "mod_led_control", @@ -20,13 +21,13 @@ local Module = { } function Module:resetLeds() - local dir = nixio.fs.dir(self.sysLedsDir) - if not dir then + local ok, dir = pcall(dirent.files, self.sysLedsDir) + if not ok then return end for led in dir do local brightness = string.format("%s/%s/brightness", self.sysLedsDir, led) - if nixio.fs.access(brightness, "w") then + if unistd.access(brightness, "w") then self.writeValue(brightness, 0) end end @@ -41,10 +42,11 @@ function Module:init(t) self._ledMaxBrightnessFile = string.format("%s/max_brightness", self._ledDir) self._ledBrightnessFile = string.format("%s/brightness", self._ledDir) self._ledMaxBrightness = self.readValue(self._ledMaxBrightnessFile) or 1 - if (not nixio.fs.access(self._ledDir, "r") or - not nixio.fs.access(self._ledBrightnessFile, "r", "w")) then + if (not unistd.access(self._ledDir, "r") or + not unistd.access(self._ledBrightnessFile, "rw")) then self._enabled = false - self.syslog("warning", string.format("%s: LED '%s' is not available", self.name, self.ledName)) + self.syslog("warning", string.format( + "%s: LED '%s' is not available", self.name, self.ledName)) else self._enabled = true -- Reset all LEDs @@ -85,6 +87,7 @@ function Module:run(currentStatus, lastStatus, timeDiff) self._counter = 0 end + self._counter = self._counter + timeDiff end diff --git a/internet-detector/files/usr/lib/internet-detector/mod_modem_restart.lua b/internet-detector/files/usr/lib/internet-detector/mod_modem_restart.lua index 3001c0d..1dcb879 100644 --- a/internet-detector/files/usr/lib/internet-detector/mod_modem_restart.lua +++ b/internet-detector/files/usr/lib/internet-detector/mod_modem_restart.lua @@ -2,7 +2,7 @@ Dependences: modemmanager --]] -local nixio = require("nixio") +local unistd = require("posix.unistd") local Module = { name = "mod_modem_restart", @@ -50,11 +50,11 @@ function Module:init(t) self.iface = t.iface self.anyBand = (tonumber(t.any_band) ~= 0) - if not nixio.fs.access(self.mmcli, "x") then + if not unistd.access(self.mmcli, "x") then self.anyBand = false end - if nixio.fs.access(self.mmInit, "x") then + if unistd.access(self.mmInit, "x") then self._enabled = true else self._enabled = false diff --git a/internet-detector/files/usr/lib/internet-detector/mod_network_restart.lua b/internet-detector/files/usr/lib/internet-detector/mod_network_restart.lua index 9014318..e1f0487 100644 --- a/internet-detector/files/usr/lib/internet-detector/mod_network_restart.lua +++ b/internet-detector/files/usr/lib/internet-detector/mod_network_restart.lua @@ -1,5 +1,5 @@ -local nixio = require("nixio") +local unistd = require("posix.unistd") local Module = { name = "mod_network_restart", @@ -22,9 +22,9 @@ end function Module:toggleDevice(flag) local ip = "/sbin/ip" - if nixio.fs.access(ip, "x") then - return os.execute( - string.format("%s link set dev %s %s", ip, self.iface, (flag and "up" or "down")) + if unistd.access(ip, "x") then + return os.execute(string.format( + "%s link set dev %s %s", ip, self.iface, (flag and "up" or "down")) ) end end @@ -71,10 +71,11 @@ function Module:run(currentStatus, lastStatus, timeDiff) self.syslog("info", string.format( "%s: restarting network interface '%s'", self.name, self.iface)) self:ifaceDown() - nixio.nanosleep(self.restartTimeout) + unistd.sleep(self.restartTimeout) self:ifaceUp() else - self.syslog("info", string.format("%s: restarting network", self.name)) + self.syslog("info", string.format( + "%s: restarting network", self.name)) self:networkRestart() end self._deadCounter = 0 diff --git a/internet-detector/files/usr/lib/internet-detector/mod_public_ip.lua b/internet-detector/files/usr/lib/internet-detector/mod_public_ip.lua index c6d7f82..2800f21 100644 --- a/internet-detector/files/usr/lib/internet-detector/mod_public_ip.lua +++ b/internet-detector/files/usr/lib/internet-detector/mod_public_ip.lua @@ -1,79 +1,322 @@ -local nixio = require("nixio") +local socket = require("posix.sys.socket") +local unistd = require("posix.unistd") local Module = { - name = "mod_public_ip", - config = {}, - syslog = function(level, msg) return true end, - writeValue = function(filePath, str) return false end, - readValue = function(filePath) return nil end, - runInterval = 600, - nslookup = "/usr/bin/nslookup", - timeout = 3, - providers = { - opendns1 = { - name = "opendns1", server = "208.67.222.222", - host = "myip.opendns.com", queryType = "a" - }, - opendns2 = { - name = "opendns2", server = "208.67.220.220", - host = "myip.opendns.com", queryType = "a" - }, - opendns3 = { - name = "opendns3", server = "208.67.222.220", - host = "myip.opendns.com", queryType = "a" - }, - opendns4 = { - name = "opendns4", server = "208.67.220.222", - host = "myip.opendns.com", queryType = "a" - }, - akamai = { - name = "akamai", server = "ns1-1.akamaitech.net", - host = "whoami.akamai.net", queryType = "a" - }, - google = { - name = "google", server = "ns1.google.com", - host = "o-o.myaddr.l.google.com", queryType = "txt" + name = "mod_public_ip", + config = { + debug = false, + serviceConfig = { + iface = nil, }, }, - status = nil, - _provider = nil, - _nslookupCmd = nil, - _currentIp = nil, - _enabled = false, - _counter = 0, + syslog = function(level, msg) return true end, + writeValue = function(filePath, str) return false end, + readValue = function(filePath) return nil end, + port = 53, + runInterval = 600, + runIntervalFailed = 60, + timeout = 3, + reqAttempts = 3, + providers = { + opendns1 = { + name = "opendns1", host = "myip.opendns.com", + server = "208.67.222.222", server6 = "2620:119:35::35", + port = 53, queryType = "A", queryType6 = "AAAA", + }, + opendns2 = { + name = "opendns2", host = "myip.opendns.com", + server = "208.67.220.220", server6 = "2620:119:35::35", + port = 53, queryType = "A", queryType6 = "AAAA", + }, + opendns3 = { + name = "opendns3", host = "myip.opendns.com", + server = "208.67.222.220", server6 = "2620:119:35::35", + port = 53, queryType = "A", queryType6 = "AAAA", + }, + opendns4 = { + name = "opendns4", host = "myip.opendns.com", + server = "208.67.220.222", server6 = "2620:119:35::35", + port = 53, queryType = "A", queryType6 = "AAAA", + }, + akamai = { + name = "akamai", host = "whoami.akamai.net", + server = "ns1-1.akamaitech.net", server6 = "ns1-1.akamaitech.net", + port = 53, queryType = "A", queryType6 = "AAAA", + }, + google = { + name = "google", host = "o-o.myaddr.l.google.com", + server = "ns1.google.com", server6 = "ns1.google.com", + port = 53, queryType = "TXT", queryType6 = "TXT", + }, + }, + status = nil, + _provider = nil, + _qtype = false, + _currentIp = nil, + _enabled = false, + _counter = 0, + _interval = 600, + _DNSPacket = nil, } -function Module:parseA(str) - res = str:match("Name:%s+" .. self._provider.host .. "\nAddress:%s+[%w.:]+") - if res then - return res:match("[%w.:]+$") +function Module:getQueryType(type) + local types = { + A = 1, + NS = 2, + MD = 3, + MF = 4, + CNAME = 5, + SOA = 6, + MB = 7, + MG = 8, + MR = 9, + NULL = 10, + WKS = 11, + PTS = 12, + HINFO = 13, + MINFO = 14, + MX = 15, + TXT = 16, + AAAA = 28, + } + return types[type] +end + +function Module:buildMessage(address, queryType) + if not queryType then + queryType = "A" + end + queryType = self:getQueryType(queryType) + + local addressString = "" + for part in address:gmatch("[^.]+") do + local t = {} + for i in part:gmatch(".") do + t[#t + 1] = i + end + addrLen = #part + addrPart = table.concat(t) + addressString = addressString .. string.char(addrLen) .. addrPart + end + + local data = ( + string.char( + 0xaa, 0xaa, + 0x01, 0x00, + 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 + ) .. + addressString .. + string.char( + 0x00, + 0x00, queryType, + 0x00, 0x01 + ) + ) + return data +end + +function Module:sendUDPMessage(message, server, port) + local success + local retCode = 1 + local data + + if self.config.debug then + io.stdout:write(string.format("--- %s ---\n", self.name)) + io.stdout:flush() + end + + local saTable, errMsg, errNum = socket.getaddrinfo(server, port) + + if not saTable then + if self.config.debug then + io.stdout:write(string.format( + "GETADDRINFO ERROR: %s, %s\n", errMsg, errNum)) + end + else + local family = saTable[1].family + + if family then + local sock, errMsg, errNum = socket.socket(family, socket.SOCK_DGRAM, 0) + + if not sock then + if self.config.debug then + io.stdout:write(string.format( + "SOCKET ERROR: %s, %s\n", errMsg, errNum)) + end + return retCode + end + + socket.setsockopt(sock, socket.SOL_SOCKET, + socket.SO_SNDTIMEO, self.timeout, 0) + socket.setsockopt(sock, socket.SOL_SOCKET, + socket.SO_RCVTIMEO, self.timeout, 0) + + if self.config.serviceConfig.iface then + local ok, errMsg, errNum = socket.setsockopt(sock, socket.SOL_SOCKET, + socket.SO_BINDTODEVICE, self.config.serviceConfig.iface) + if not ok then + if self.config.debug then + io.stdout:write(string.format( + "SOCKET ERROR: %s, %s\n", errMsg, errNum)) + end + return retCode + end + end + + local ok, errMsg, errNum = socket.sendto(sock, message, saTable[1]) + local response = {} + if ok then + local ret, resp, errNum = socket.recvfrom(sock, 1024) + data = ret + if data then + success = true + response = resp + elseif self.config.debug then + io.stdout:write(string.format( + "SOCKET RECV ERROR: %s, %s\n", tostring(resp), tostring(errNum))) + end + elseif self.config.debug then + io.stdout:write(string.format( + "SOCKET SEND ERROR: %s, %s\n", tostring(errMsg), tostring(errNum))) + end + + if self.config.debug then + io.stdout:write(string.format( + "--- UDP ---\ntime = %s\nconnection_timeout = %s\niface = %s\nserver = %s:%s\nsockname = %s:%s\nsuccess = %s\n", + os.time(), + self.timeout, + tostring(self.config.serviceConfig.iface), + server, + tostring(port), + tostring(response.addr), + tostring(response.port), + tostring(success)) + ) + io.stdout:flush() + end + + unistd.close(sock) + retCode = success and 0 or 1 + end + end + return retCode, tostring(data) +end + +function Module:parseParts(message, start, parts) + local partStart = start + 2 + local partLen = message:sub(start, start + 1) + + if #partLen == 0 then + return parts + end + + local partEnd = partStart + (tonumber(partLen, 16) * 2) + parts[#parts + 1] = message:sub(partStart, partEnd - 1) + if message:sub(partEnd, partEnd + 1) == "00" or partEnd > #message then + return parts + else + return self:parseParts(message, partEnd, parts) end end -function Module:parseGoogle(str) - res = str:match(self._provider.host .. '%s+text%s+=%s+"[%w.:]+"') - if res then - return res:gsub('"', ''):match("[%w.:]+$") +function Module:decodeMessage(message) + local retTable = {} + + local t = {} + for i = 1, #message do + t[#t + 1] = string.format("%.2x", string.byte(message, i)) end + message = table.concat(t) + + local ANCOUNT = message:sub(13, 16) + local NSCOUNT = message:sub(17, 20) + local ARCOUNT = message:sub(21, 24) + + local questionSectionStarts = 25 + local questionParts = self:parseParts(message, questionSectionStarts, {}) + local qtypeStarts = questionSectionStarts + (#table.concat(questionParts)) + (#questionParts * 2) + 1 + local qclassStarts = qtypeStarts + 4 + + local answerSectionStarts = qclassStarts + 4 + local numAnswers = math.max( + tonumber(ANCOUNT, 16), tonumber(NSCOUNT, 16), tonumber(ARCOUNT, 16)) + + if numAnswers > 0 then + for answerCount = 1, numAnswers do + + if answerSectionStarts < #message then + local ATYPE = tonumber( + message:sub(answerSectionStarts + 5, answerSectionStarts + 8), 16) + local RDLENGTH = tonumber( + message:sub(answerSectionStarts + 21, answerSectionStarts + 24), 16) + local RDDATA = message:sub( + answerSectionStarts + 25, answerSectionStarts + 24 + (RDLENGTH * 2)) + local RDDATA_decoded = "" + + if #RDDATA > 0 then + if ATYPE == self:getQueryType("A") or ATYPE == self:getQueryType("AAAA") then + local octets = {} + local sep = "." + if #RDDATA > 8 then + sep = ":" + for i = 1, #RDDATA, 4 do + local string = RDDATA:sub(i, i + 3) + string = string:gsub("^00?0?", "") + octets[#octets + 1] = string + end + else + for i = 1, #RDDATA, 2 do + octets[#octets + 1] = tonumber(RDDATA:sub(i, i + 1), 16) + end + end + RDDATA_decoded = table.concat(octets, sep):gsub("0:[0:]+", "::", 1):gsub("::+", "::") + else + local rdata_t = {} + for _, v in ipairs(self:parseParts(RDDATA, 1, {})) do + local t = {} + for i = 1, #v, 2 do + t[#t + 1] = string.char(tonumber(v:sub(i, i + 1), 16)) + end + rdata_t[#rdata_t + 1] = table.concat(t) + end + RDDATA_decoded = table.concat(rdata_t) + end + end + answerSectionStarts = answerSectionStarts + 24 + (RDLENGTH * 2) + + if RDDATA_decoded:match("^[a-f0-9.:]+$") then + retTable[#retTable + 1] = RDDATA_decoded + end + end + end + end + + return retTable end function Module:resolveIP() local res - local fh = io.popen(self._nslookupCmd, "r") - if fh then - output = fh:read("*a") - fh:close() - if self._provider.name == "google" then - res = self:parseGoogle(output) - else - res = self:parseA(output) + local qtype = self._qtype and self._provider.queryType6 or self._provider.queryType + local server = self._qtype and self._provider.server6 or self._provider.server + local port = self._provider.port or self.port + + if not self._DNSPacket then + self._DNSPacket = self:buildMessage(self._provider.host, qtype) + end + + local retCode, response = self:sendUDPMessage(self._DNSPacket, server, port) + if retCode == 0 then + local retTable = self:decodeMessage(response) + if #retTable > 0 then + res = table.concat(retTable, ", ") end else self.syslog("err", string.format( - "%s: Nslookup call failed (%s)", self.name, self.nslookup)) + "%s: DNS error when requesting an IP address", self.name)) end - return res or "Undefined" + + return res end function Module:init(t) @@ -88,23 +331,11 @@ function Module:init(t) else self._provider = self.providers.opendns1 end - if not nixio.fs.access(self.nslookup, "x") then - self._enabled = false - self.syslog( - "warning", - string.format("%s: '%s' does not exists", self.name, self.nslookup) - ) - else - self._enabled = true - self._nslookupCmd = string.format( - "%s -type=%s -timeout=%d %s %s", - self.nslookup, - self._provider.queryType, - self.timeout, - self._provider.host, - self._provider.server - ) - end + self._qtype = (tonumber(t.qtype) ~= 0) + self._currentIp = nil + self._DNSPacket = nil + self._interval = self.runInterval + self._enabled = true end function Module:run(currentStatus, lastStatus, timeDiff) @@ -112,8 +343,17 @@ function Module:run(currentStatus, lastStatus, timeDiff) return end if currentStatus == 0 then - if self._counter == 0 or self._counter >= self.runInterval or currentStatus ~= lastStatus then + if self._counter == 0 or self._counter >= self._interval or currentStatus ~= lastStatus then + local ip = self:resolveIP() + + if not ip then + ip = "Undefined" + self._interval = self.runIntervalFailed + else + self._interval = self.runInterval + end + if ip ~= self._currentIp then self.status = ip self.syslog( @@ -123,6 +363,7 @@ function Module:run(currentStatus, lastStatus, timeDiff) else self.status = nil end + self._currentIp = ip self._counter = 0 else @@ -132,7 +373,9 @@ function Module:run(currentStatus, lastStatus, timeDiff) self.status = nil self._currentIp = nil self._counter = 0 + self._interval = self.runInterval end + self._counter = self._counter + timeDiff end diff --git a/internet-detector/files/usr/lib/internet-detector/mod_reboot.lua b/internet-detector/files/usr/lib/internet-detector/mod_reboot.lua index 1811d63..5f92466 100644 --- a/internet-detector/files/usr/lib/internet-detector/mod_reboot.lua +++ b/internet-detector/files/usr/lib/internet-detector/mod_reboot.lua @@ -1,5 +1,5 @@ -local nixio = require("nixio") +local unistd = require("posix.unistd") local Module = { name = "mod_reboot", @@ -16,9 +16,8 @@ local Module = { function Module:rebootDevice() self.syslog("warning", string.format("%s: reboot", self.name)) os.execute("/sbin/reboot &") - if self.forceRebootDelay > 0 then - nixio.nanosleep(self.forceRebootDelay) + unistd.sleep(self.forceRebootDelay) self.syslog("warning", string.format("%s: force reboot", self.name)) self.writeValue("/proc/sys/kernel/sysrq", "1") self.writeValue("/proc/sysrq-trigger", "b") diff --git a/internet-detector/files/usr/lib/internet-detector/mod_user_scripts.lua b/internet-detector/files/usr/lib/internet-detector/mod_user_scripts.lua index 80ce9a9..c5eec95 100644 --- a/internet-detector/files/usr/lib/internet-detector/mod_user_scripts.lua +++ b/internet-detector/files/usr/lib/internet-detector/mod_user_scripts.lua @@ -1,5 +1,5 @@ -local nixio = require("nixio") +local unistd = require("posix.unistd") local Module = { name = "mod_user_scripts", @@ -19,8 +19,8 @@ local Module = { } function Module:runExternalScript(scriptPath) - if nixio.fs.access(scriptPath, "x") then - os.execute(string.format('/bin/sh -c "%s" &', scriptPath)) + if unistd.access(scriptPath, "r") then + os.execute(string.format('/bin/sh "%s" &', scriptPath)) end end @@ -28,8 +28,10 @@ function Module:init(t) self.deadPeriod = tonumber(t.dead_period) self.alivePeriod = tonumber(t.alive_period) if self.config.configDir then - self.upScript = string.format("%s/up-script", self.config.configDir) - self.downScript = string.format("%s/down-script", self.config.configDir) + self.upScript = string.format( + "%s/up-script.%s", self.config.configDir, self.config.serviceConfig.instance) + self.downScript = string.format( + "%s/down-script.%s", self.config.configDir, self.config.serviceConfig.instance) end end @@ -37,7 +39,6 @@ function Module:run(currentStatus, lastStatus, timeDiff) if currentStatus == 1 then self._aliveCounter = 0 self._downScriptExecuted = false - if not self._upScriptExecuted then if self._deadCounter >= self.deadPeriod then self:runExternalScript(self.downScript) @@ -49,7 +50,6 @@ function Module:run(currentStatus, lastStatus, timeDiff) else self._deadCounter = 0 self._upScriptExecuted = false - if not self._downScriptExecuted then if self._aliveCounter >= self.alivePeriod then self:runExternalScript(self.upScript) diff --git a/luci-app-internet-detector/Makefile b/luci-app-internet-detector/Makefile index 3f5c3ef..0b3ca63 100644 --- a/luci-app-internet-detector/Makefile +++ b/luci-app-internet-detector/Makefile @@ -1,10 +1,10 @@ # -# (с) 2021 gSpot (https://github.com/gSpotx2f/luci-app-internet-detector) +# (с) 2023 gSpot (https://github.com/gSpotx2f/luci-app-internet-detector) # include $(TOPDIR)/rules.mk -PKG_VERSION:=0.6-1 +PKG_VERSION:=1.0-0 LUCI_TITLE:=LuCI support for internet-detector LUCI_DEPENDS:=+internet-detector LUCI_PKGARCH:=all diff --git a/luci-app-internet-detector/htdocs/luci-static/resources/view/internet-detector.js b/luci-app-internet-detector/htdocs/luci-static/resources/view/internet-detector.js index 3d66ad5..fb0c233 100644 --- a/luci-app-internet-detector/htdocs/luci-static/resources/view/internet-detector.js +++ b/luci-app-internet-detector/htdocs/luci-static/resources/view/internet-detector.js @@ -12,27 +12,50 @@ document.head.append(E('style', {'type': 'text/css'}, ` :root { - --app-id-font-color: #fff; - --app-id-connected-color: #2ea256; - --app-id-disconnected-color: #ff4e54; - --app-id-undefined-color: #8a8a8a; + --app-id-font-color: #454545; + --app-id-font-shadow: #fff; + --app-id-connected-color: #6bdebb; + --app-id-disconnected-color: #f8aeba; + --app-id-undefined-color: #dfdfdf; } :root[data-darkmode="true"] { + --app-id-font-color: #f6f6f6; + --app-id-font-shadow: #4d4d4d; --app-id-connected-color: #005F20; --app-id-disconnected-color: #a93734; --app-id-undefined-color: #4d4d4d; } .id-connected { + --on-color: var(--app-id-font-color); background-color: var(--app-id-connected-color) !important; + border-color: var(--app-id-connected-color) !important; color: var(--app-id-font-color) !important; + text-shadow: 0 1px 1px var(--app-id-font-shadow); } .id-disconnected { + --on-color: var(--app-id-font-color); background-color: var(--app-id-disconnected-color) !important; + border-color: var(--app-id-disconnected-color) !important; color: var(--app-id-font-color) !important; + text-shadow: 0 1px 1px var(--app-id-font-shadow); } .id-undefined { + --on-color: var(--app-id-font-color); background-color: var(--app-id-undefined-color) !important; + border-color: var(--app-id-undefined-color) !important; color: var(--app-id-font-color) !important; + text-shadow: 0 1px 1px var(--app-id-font-shadow); +} +.id-label-status { + display: inline-block; + word-wrap: break-word; + margin: 2px !important; + padding: 4px 8px; + border: 1px solid; + -webkit-border-radius: 4px; + -moz-border-radius: 4px; + border-radius: 4px; + font-weight: bold; } `)); @@ -107,17 +130,14 @@ var Timefield = ui.Textfield.extend({ return view.extend({ appName : 'internet-detector', execPath : '/usr/bin/internet-detector', - upScriptPath : '/etc/internet-detector/up-script', - downScriptPath : '/etc/internet-detector/down-script', + configDir : '/etc/internet-detector', ledsPath : '/sys/class/leds', mtaPath : '/usr/bin/mailsend', pollInterval : L.env.pollinterval, appStatus : 'stoped', initStatus : null, inetStatus : null, - publicIp : null, - inetStatusLabel : E('span', { 'class': 'label', 'id': 'inetStatusLabel' }), - inetStatusSpinner : E('span', { 'style': 'margin-top:1em' }, ' '), + inetStatusArea : E('div', { 'class': 'cbi-value-field', 'id': 'inetStatusArea' }), serviceStatusLabel : E('em', { 'id': 'serviceStatusLabel' }), initButton : null, uiPollCounter : 0, @@ -125,6 +145,7 @@ return view.extend({ uiCheckIntervalUp : null, uiCheckIntervalDown : null, currentAppMode : '0', + defaultHosts : [ '8.8.8.8', '1.1.1.1' ], leds : [], mm : false, mta : false, @@ -168,31 +189,41 @@ return view.extend({ }); }, - setInetStatusSpinner: function() { - this.inetStatusSpinner.className = 'spinning'; - }, - - unsetInetStatusSpinner: function() { - this.inetStatusSpinner.className = ''; - }, - setInternetStatus: function() { - if(this.inetStatus === 0) { - this.inetStatusLabel.textContent = _('Connected') + (this.publicIp ? ' | %s: %s'.format(_('Public IP'), _(this.publicIp)) : ''); - this.inetStatusLabel.className = "label id-connected"; - this.unsetInetStatusSpinner(); - } - else if(this.inetStatus === 1) { - this.inetStatusLabel.textContent = _('Disconnected'); - this.inetStatusLabel.className = "label id-disconnected"; - this.unsetInetStatusSpinner(); - } - else { - this.inetStatusLabel.textContent = _('Undefined'); - this.inetStatusLabel.className = "label id-undefined"; + this.inetStatusArea.innerHTML = ''; + + if(!this.inetStatus || !this.inetStatus.instances || this.inetStatus.instances.length === 0) { + let label = E('span', { 'class': 'id-label-status id-undefined' }, _('Undefined')) if(this.currentAppMode !== '0' && this.appStatus !== 'stoped') { - this.setInetStatusSpinner(); + label.classList.add('spinning'); + }; + this.inetStatusArea.append(label); + } else { + this.inetStatus.instances.sort((a, b) => a.num > b.num); + + for(let i of this.inetStatus.instances) { + let status = _('Disconnected'); + let className = 'id-label-status id-disconnected'; + if(i.inet == 0) { + status = _('Connected'); + className = 'id-label-status id-connected'; + } + else if(i.inet == -1) { + status = _('Undefined'); + className = 'id-label-status id-undefined spinning'; + }; + + let publicIp = (i.mod_public_ip) ? ' | %s: %s'.format( + _('Public IP'), _(i.mod_public_ip) + ) : ''; + + this.inetStatusArea.append( + E('span', { 'class': className }, '%s%s%s'.format( + (this.currentAppMode === '1') ? i.instance + ': ' : '', + status, publicIp) + ) + ); }; }; @@ -204,41 +235,33 @@ return view.extend({ }, inetStatusFromJson: function(res) { - let curInetStatus = null; - let curPubIp = null; + let inetStatData = null; if(res.code === 0) { try { - let json = JSON.parse(res.stdout.trim()); - curInetStatus = json.inet; - curPubIp = json.mod_public_ip; + inetStatData = JSON.parse(res.stdout.trim()); } catch(e) {}; }; - return [ curInetStatus, curPubIp ]; + return inetStatData; }, servicePoll: function() { return Promise.all([ fs.exec(this.execPath, [ 'status' ]), - fs.exec(this.execPath, [ 'inet-status-json' ]), + fs.exec(this.execPath, [ 'inet-status' ]), ]).then(stat => { - let curAppStatus = (stat[0].code === 0) ? stat[0].stdout.trim() : null; - let [ curInetStatus, curPubIp ] = this.inetStatusFromJson(stat[1]); - if(this.inetStatus === curInetStatus && this.appStatus === curAppStatus && this.publicIp === curPubIp) { - return; - }; + let curAppStatus = (stat[0].code === 0) ? stat[0].stdout.trim() : null; + let inetStatData = this.inetStatusFromJson(stat[1]); + this.appStatus = curAppStatus; - this.inetStatus = curInetStatus; - this.publicIp = curPubIp; + this.inetStatus = inetStatData; this.setInternetStatus(); }).catch(e => { this.appStatus = 'stoped'; - this.inetStatus = null; - this.publicIp = null + this.inetStatus = {}; }); }, uiPoll: function() { - let curInetStatus = null; this.uiPollCounter = ++this.uiPollCounter; if((this.uiPollState === 0 && this.uiPollCounter % this.uiCheckIntervalUp) || @@ -248,20 +271,23 @@ return view.extend({ this.uiPollCounter = 0; - return fs.exec(this.execPath, [ 'inet-status-json' ]).then(res => { - let curPubIp; - [ this.uiPollState, curPubIp ] = this.inetStatusFromJson(res); - if(this.inetStatus !== this.uiPollState || this.publicIp !== curPubIp) { - this.inetStatus = (this.currentAppMode === '0') ? null : this.uiPollState; - this.publicIp = (this.currentAppMode === '0') ? null : curPubIp; - this.setInternetStatus(); + return fs.exec(this.execPath, [ 'poll' ]).then(res => { + let inetStatData = this.inetStatusFromJson(res); + if(inetStatData.instances[0]) { + this.uiPollState = inetStatData.instances[0].inet; }; + this.inetStatus = inetStatData; + this.setInternetStatus(); }); }, - serviceRestart: function(ev) { + serviceRestart: function() { + return this.handleServiceAction('restart'); + }, + + serviceRestartHandler: function() { poll.stop(); - return this.handleServiceAction('restart').then(() => { + return this.serviceRestart().then(() => { window.setTimeout(() => this.servicePoll(), 1000); poll.start(); }); @@ -304,15 +330,10 @@ return view.extend({ renderWidget: function(section_id, option_index, cfgvalue) { this.ctx.setInternetStatus(); - return E([ - E('label', { 'class': 'cbi-value-title', 'for': 'inetStatusLabel' }, + E('label', { 'class': 'cbi-value-title', 'for': 'inetStatusArea' }, _('Internet status') - ), - E('div', { 'class': 'cbi-value-field' }, [ - this.ctx.inetStatusLabel, - this.ctx.inetStatusSpinner - ]), + ), this.ctx.inetStatusArea ]) }, }), @@ -390,58 +411,38 @@ return view.extend({ }, }), - fileEditDialog: baseclass.extend({ - __init__: function(file, title, description, callback, fileExists=false) { + CBIBlockFileEdit: form.Value.extend({ + __name__ : 'CBI.BlockFileEdit', + + __init__ : function(map, section, ctx, id, file, title, description, callback) { + this.map = map; + this.section = section; + this.ctx = ctx; + this.id = id, + this.optional = true; + this.rmempty = true; this.file = file; this.title = title; this.description = description; this.callback = callback; - this.fileExists = fileExists; + this.content = ''; }, - load: function() { - return L.resolveDefault(fs.read(this.file), ''); + cfgvalue: function(section_id, option) { + return this.content; }, - render: function(content) { - ui.showModal(this.title, [ - E('div', { 'class': 'cbi-section' }, [ - E('div', { 'class': 'cbi-section-descr' }, this.description), - E('div', { 'class': 'cbi-section' }, - E('p', {}, - E('textarea', { - 'id' : 'widget.modal_content', - 'class': 'cbi-input-textarea', - 'style': 'width:100% !important', - 'rows' : 10, - 'wrap' : 'off', - 'spellcheck': 'false', - }, - content) - ) - ), - ]), - E('div', { 'class': 'right' }, [ - E('button', { - 'class': 'btn', - 'click': ui.hideModal, - }, _('Dismiss')), - ' ', - E('button', { - 'id': 'btn_save', - 'class': 'btn cbi-button-positive important', - 'click': ui.createHandlerFn(this, this.handleSave), - }, _('Save')), - ]), - ]); + formvalue: function(section_id) { + let value = this.content; + let textarea = document.getElementById('widget.file_edit.content.' + this.id); + if(textarea) { + value = textarea.value.trim().replace(/\r\n/g, '\n') + '\n'; + }; + return value; }, - handleSave: function(ev) { - let textarea = document.getElementById('widget.modal_content'); - let value = textarea.value.trim().replace(/\r\n/g, '\n') + '\n'; - - return fs.write(this.file, value).then(rc => { - textarea.value = value; + write: function(section_id, formvalue) { + return fs.write(this.file, formvalue).then(rc => { ui.addNotification(null, E('p', _('Contents have been saved.')), 'info'); if(this.callback) { @@ -450,41 +451,24 @@ return view.extend({ }).catch(e => { ui.addNotification(null, E('p', _('Unable to save the contents') + ': %s'.format(e.message))); - }).finally(() => { - ui.hideModal(); }); }, - error: function(e) { - if(!this.fileExists && e instanceof Error && e.name === 'NotFoundError') { - return this.render(); - } else { - ui.showModal(this.title, [ - E('div', { 'class': 'cbi-section' }, - E('p', {}, _('Unable to read the contents') - + ': %s'.format(e.message)) - ), - E('div', { 'class': 'right' }, - E('button', { - 'class': 'btn', - 'click': ui.hideModal, - }, _('Dismiss')) - ), - ]); - }; + load: function() { + return L.resolveDefault(fs.read(this.file), '').then(c => { + this.content = c; + }); }, - show: function() { - ui.showModal(null, - E('p', { 'class': 'spinning' }, _('Loading')) - ); - this.load().then(content => { - ui.hideModal(); - return this.render(content); - }).catch(e => { - ui.hideModal(); - return this.error(e); - }) + renderWidget: function(section_id, option_index, cfgvalue) { + return E('textarea', { + 'id' : 'widget.file_edit.content.' + this.id, + 'class' : 'cbi-input-textarea', + 'style' : 'width:100% !important', + 'rows' : 10, + 'wrap' : 'off', + 'spellcheck': 'false', + }, cfgvalue); }, }), @@ -515,8 +499,8 @@ return view.extend({ this.mta = true; }; this.currentAppMode = uci.get(this.appName, 'config', 'mode'); - this.uiCheckIntervalUp = Number(uci.get(this.appName, 'config', 'ui_interval_up')); - this.uiCheckIntervalDown = Number(uci.get(this.appName, 'config', 'ui_interval_down')); + this.uiCheckIntervalUp = Number(uci.get(this.appName, 'ui', 'interval_up')); + this.uiCheckIntervalDown = Number(uci.get(this.appName, 'ui', 'interval_down')); let s, o, ss; let m = new form.Map(this.appName, @@ -524,19 +508,25 @@ return view.extend({ _('Checking Internet availability.')); - /* Service widget */ + /* Status widget */ s = m.section(form.NamedSection, 'config', 'main'); o = s.option(this.CBIBlockInetStatus, this); - if(this.currentAppMode === '2') { + + s = m.section(form.NamedSection, 'config', 'main'); + + + /* Service widget */ + + if(this.currentAppMode === '1') { o = s.option(this.CBIBlockServiceStatus, this); // restart button o = s.option(form.Button, '_restart_btn', _('Restart service') ); - o.onclick = () => this.serviceRestart(); + o.onclick = () => this.serviceRestartHandler(); o.inputtitle = _('Restart'); o.inputstyle = btnStyleApply; @@ -547,115 +537,90 @@ return view.extend({ /* Main settings */ - s = m.section(form.NamedSection, 'config', 'main'); - - s.tab('main_configuration', _('Main settings')); - // mode - let mode = s.taboption('main_configuration', form.ListValue, - 'mode', _('Internet detector mode')); + let mode = s.option(form.ListValue, 'mode', + _('Internet detector mode')); mode.value('0', _('Disabled')); - mode.value('1', _('Web UI only')); - mode.value('2', _('Service')); + mode.value('1', _('Service')); + mode.value('2', _('Web UI only (UI detector)')); mode.description = '%s
%s
%s'.format( _('Disabled: detector is completely off.'), - _('Web UI only: detector works only when the Web UI is open (UI detector).'), - _('Service: detector always runs as a system service.') + _('Service: detector always runs as a system service.'), + _('Web UI only: detector works only when the Web UI is open (UI detector).') ); - // hosts - o = s.taboption('main_configuration', form.DynamicList, - 'hosts', _('Hosts'), - _('Hosts to check Internet availability. Hosts are polled (in list order) until at least one of them responds.') + + s = m.section(form.NamedSection, 'config', 'main'); + + + /* Service instances configuration */ + + s.tab('service', _('Service configuration')); + + // enable_logger + o = s.taboption('service', form.Flag, 'enable_logger', + _('Enable logging'), + _('Write messages to the system log.') ); - o.datatype = 'or(host,hostport)'; + o.rmempty = false; - // check_type - o = s.taboption('main_configuration', form.ListValue, - 'check_type', _('Check type'), - _('Host availability check type.') + o = s.taboption('service', form.SectionValue, 'instance', form.GridSection, + 'instance' ); - o.value(0, _('TCP port connection')); - o.value(1, _('Ping host')); + ss = o.subsection; - // tcp_port - o = s.taboption('main_configuration', form.Value, - 'tcp_port', _('TCP port'), - _('Default port value for TCP connections.') - ); - o.datatype = 'port'; - o.default = '53'; - o.depends({ check_type: '0' }); + ss.title = _('Service instances'); + ss.addremove = true; + ss.sortable = true; + ss.nodescriptions = true; + ss.addbtntitle = _('Add instance'); - // ping_packet_size - o = s.taboption('main_configuration', form.ListValue, - 'ping_packet_size', _('Ping packet size')); - o.value(1, _('Small: 1 byte')); - o.value(32, _('Windows: 32 bytes')); - o.value(56, _('Standard: 56 bytes')); - o.value(248, _('Big: 248 bytes')); - o.value(1492, _('Huge: 1492 bytes')); - o.value(9000, _('Jumbo: 9000 bytes')); - o.default = '56'; - o.depends({ check_type: '1' }); + ss.addModalOptions = (s, section_id, ev) => { - // iface - o = s.taboption('main_configuration', widgets.DeviceSelect, - 'iface', _('Interface'), - _('Network interface for Internet access. If not specified, the default interface is used.') - ); - o.noaliases = true; + // User scripts + // enabled + o = s.taboption('user_scripts', form.Flag, 'mod_user_scripts_enabled', + _('Enabled')); + o.rmempty = false; + o.modalonly = true; - /* UI detector configuration */ + // up_script edit dialog + o = s.taboption('user_scripts', this.CBIBlockFileEdit, this, + 'up_script', + this.configDir + '/up-script.' + s.section, + _('Edit up-script'), + _('Shell commands that run when connected to the Internet.') + ); + o.modalonly = true; - s.tab('ui_settings', _('UI detector configuration')); + // alive_period + o = s.taboption('user_scripts', this.CBITimeInput, + 'mod_user_scripts_alive_period', _('Alive period'), + _('Longest period of time after connecting to Internet before "up-script" runs.') + ); + o.default = '0'; + o.rmempty = false; + o.modalonly = true; - let makeUIIntervalOptions = L.bind(function(list) { - list.value(1, '%d %s'.format(this.pollInterval, _('sec'))); - list.value(2, '%d %s'.format(this.pollInterval * 2, _('sec'))); - list.value(3, '%d %s'.format(this.pollInterval * 3, _('sec'))); - list.value(4, '%d %s'.format(this.pollInterval * 4, _('sec'))); - list.value(5, '%d %s'.format(this.pollInterval * 5, _('sec'))); - list.value(6, '%d %s'.format(this.pollInterval * 6, _('sec'))); - }, this); + // down_script edit dialog + o = s.taboption('user_scripts', this.CBIBlockFileEdit, this, + 'down_script', + this.configDir + '/down-script.' + s.section, + _('Edit down-script'), + _('Shell commands that run when connected to the Internet.') + ); + o.modalonly = true; - // interval_up - o = s.taboption('ui_settings', form.ListValue, - 'ui_interval_up', _('Alive interval'), - _('Hosts polling interval when the Internet is up.') - ); - makeUIIntervalOptions(o); - - // interval_down - o = s.taboption('ui_settings', form.ListValue, - 'ui_interval_down', _('Dead interval'), - _('Hosts polling interval when the Internet is down.') - ); - makeUIIntervalOptions(o); - - // connection_attempts - o = s.taboption('ui_settings', form.ListValue, - 'ui_connection_attempts', _('Connection attempts'), - _('Maximum number of attempts to connect to each host.') - ); - o.value(1); - o.value(2); - o.value(3); - - // connection_timeout - o = s.taboption('ui_settings', form.ListValue, - 'ui_connection_timeout', _('Connection timeout'), - _('Maximum timeout for waiting for a response from the host.') - ); - o.value(1, '1 ' + _('sec')); - o.value(2, '2 ' + _('sec')); - o.value(3, '3 ' + _('sec')); - - - /* Service configuration */ - - s.tab('service_settings', _('Service configuration')); + // dead_period + o = s.taboption('user_scripts', this.CBITimeInput, + 'mod_user_scripts_dead_period', _('Dead period'), + _('Longest period of time after disconnecting from Internet before "down-script" runs.') + ); + o.default = '0'; + o.rmempty = false; + o.modalonly = true; + }; function makeIntervalOptions(list) { list.value(2, '2 ' + _('sec')); @@ -671,36 +636,103 @@ return view.extend({ list.value(600, '10 ' + _('min')); } + ss.tab('main', _('Main settings')); + + // enabled + o = ss.taboption('main', form.Flag, 'enabled', + _('Enabled'), + ); + o.rmempty = false; + o.default = '1'; + o.editable = true; + o.modalonly = false; + + // hosts + o = ss.taboption('main', form.DynamicList, + 'hosts', _('Hosts'), + _('Hosts to check Internet availability. Hosts are polled (in list order) until at least one of them responds.') + ); + o.datatype = 'or(or(host,hostport),ipaddrport(1))'; + o.default = this.defaultHosts; + o.rmempty = false; + + // check_type + o = ss.taboption('main', form.ListValue, + 'check_type', _('Check type'), + _('Host availability check type.') + ); + o.value(0, _('TCP port connection')); + o.value(1, _('ICMP-echo request (ping)')); + o.default = '0'; + o.modalonly = true; + + // tcp_port + o = ss.taboption('main', form.Value, + 'tcp_port', _('TCP port'), + _('Default port value for TCP connections.') + ); + o.datatype = 'port'; + o.default = '53'; + o.depends({ check_type: '0' }); + o.modalonly = true; + + // icmp_packet_size + o = ss.taboption('main', form.ListValue, + 'icmp_packet_size', _('ICMP packet data size')); + o.value(1, _('Small: 1 byte')); + o.value(32, _('Windows: 32 bytes')); + o.value(56, _('Standard: 56 bytes')); + o.value(248, _('Big: 248 bytes')); + o.value(1492, _('Huge: 1492 bytes')); + o.value(9000, _('Jumbo: 9000 bytes')); + o.default = '56'; + o.depends({ check_type: '1' }); + o.modalonly = true; + + // iface + o = ss.taboption('main', widgets.DeviceSelect, + 'iface', _('Interface'), + _('Network interface for Internet access. If not specified, the default interface is used.') + ); + o.noaliases = true; + // interval_up - o = s.taboption('service_settings', form.ListValue, - 'service_interval_up', _('Alive interval'), + o = ss.taboption('main', form.ListValue, + 'interval_up', _('Alive interval'), _('Hosts polling interval when the Internet is up.') ); + o.default = '30'; + o.modalonly = true; makeIntervalOptions(o); // interval_down - o = s.taboption('service_settings', form.ListValue, - 'service_interval_down', _('Dead interval'), + o = ss.taboption('main', form.ListValue, + 'interval_down', _('Dead interval'), _('Hosts polling interval when the Internet is down.') ); + o.default = '5'; + o.modalonly = true; makeIntervalOptions(o); // connection_attempts - o = s.taboption('service_settings', form.ListValue, - 'service_connection_attempts', _('Connection attempts'), + o = ss.taboption('main', form.ListValue, + 'connection_attempts', _('Connection attempts'), _('Maximum number of attempts to connect to each host.') ); + o.modalonly = true; o.value(1); o.value(2); o.value(3); o.value(4); o.value(5); + o.default = '2'; // connection_timeout - o = s.taboption('service_settings', form.ListValue, - 'service_connection_timeout', _('Connection timeout'), + o = ss.taboption('main', form.ListValue, + 'connection_timeout', _('Connection timeout'), _('Maximum timeout for waiting for a response from the host.') ); + o.modalonly = true; o.value(1, '1 ' + _('sec')); o.value(2, '2 ' + _('sec')); o.value(3, '3 ' + _('sec')); @@ -711,84 +743,78 @@ return view.extend({ o.value(8, '8 ' + _('sec')); o.value(9, '9 ' + _('sec')); o.value(10, '10 ' + _('sec')); - - // enable_logger - o = s.taboption('service_settings', form.Flag, - 'service_enable_logger', _('Enable logging'), - _('Write messages to the system log.') - ); - o.rmempty = false; + o.default = '2'; /* Modules */ - s = m.section(form.NamedSection, 'mod_led_control', 'module', - _('Service modules'), - _('Performing actions when connecting and disconnecting the Internet (available in the "Service" mode).')); - // LED control - s.tab('led_control', _('LED control')); + ss.tab('led_control', _('LED control')); - o = s.taboption('led_control', form.DummyValue, '_dummy'); + o = ss.taboption('led_control', form.DummyValue, '_dummy'); o.rawhtml = true; o.default = '
' + _('LED is on when Internet is available.') + '
'; + o.modalonly = true; if(this.leds.length > 0) { // enabled - o = s.taboption('led_control', form.Flag, 'enabled', - _('Enable')); + o = ss.taboption('led_control', form.Flag, 'mod_led_control_enabled', + _('Enabled')); o.rmempty = false; + o.modalonly = true; // led_name - o = s.taboption('led_control', form.ListValue, 'led_name', + o = ss.taboption('led_control', form.ListValue, 'mod_led_control_led_name', _('LED Name')); - o.depends({ enabled: '1' }); + o.depends({ mod_led_control_enabled: '1' }); + o.modalonly = true; this.leds.sort((a, b) => a.name > b.name); this.leds.forEach(e => o.value(e.name)); } else { - o = s.taboption('led_control', form.DummyValue, '_dummy'); + o = ss.taboption('led_control', form.DummyValue, '_dummy'); o.rawhtml = true; o.default = '
' + _('No LEDs available...') + '
'; + o.modalonly = true; }; // Reboot device - s.tab('reboot_device', _('Reboot device')); + ss.tab('reboot_device', _('Reboot device')); - o = s.taboption('reboot_device', form.DummyValue, '_dummy'); + o = ss.taboption('reboot_device', form.DummyValue, '_dummy'); o.rawhtml = true; o.default = '
' + _('Device will be rebooted when the Internet is disconnected.') + '
'; - - o = s.taboption('reboot_device', form.SectionValue, 'mod_reboot', form.NamedSection, - 'mod_reboot', 'mod_reboot' - ); - ss = o.subsection; + o.modalonly = true; // enabled - o = ss.option(form.Flag, 'enabled', - _('Enable')); + o = ss.taboption('reboot_device', form.Flag, 'mod_reboot_enabled', + _('Enabled')); o.rmempty = false; + o.modalonly = true; // dead_period - o = ss.option(this.CBITimeInput, - 'dead_period', _('Dead period'), + o = ss.taboption('reboot_device', this.CBITimeInput, + 'mod_reboot_dead_period', _('Dead period'), _('Longest period of time without Internet access until the device is rebooted.') ); - o.rmempty = false; + o.default = '3600'; + o.rmempty = false; + o.modalonly = true; // force_reboot_delay - o = ss.option(form.ListValue, - 'force_reboot_delay', _('Forced reboot delay'), + o = ss.taboption('reboot_device', form.ListValue, + 'mod_reboot_force_reboot_delay', _('Forced reboot delay'), _('Waiting for a reboot to complete before performing a forced reboot.') ); + o.modalonly = true; o.value(0, _('Disable forced reboot')); o.value(60, '1 ' + _('min')); o.value(120, '2 ' + _('min')); @@ -796,55 +822,60 @@ return view.extend({ o.value(600, '10 ' + _('min')); o.value(1800, '30 ' + _('min')); o.value(3600, '1 ' + _('hour')); + o.default = '300'; // Restart network - s.tab('restart_network', _('Restart network')); + ss.tab('restart_network', _('Restart network')); - o = s.taboption('restart_network', form.DummyValue, '_dummy'); + o = ss.taboption('restart_network', form.DummyValue, '_dummy'); o.rawhtml = true; o.default = '
' + _('Network will be restarted when the Internet is disconnected.') + '
'; - - o = s.taboption('restart_network', form.SectionValue, 'mod_network_restart', form.NamedSection, - 'mod_network_restart', 'mod_network_restart' - ); - ss = o.subsection; + o.modalonly = true; // enabled - o = ss.option(form.Flag, 'enabled', - _('Enable')); + o = ss.taboption('restart_network', form.Flag, 'mod_network_restart_enabled', + _('Enabled')); o.rmempty = false; + o.modalonly = true; // dead_period - o = ss.option(this.CBITimeInput, - 'dead_period', _('Dead period'), + o = ss.taboption('restart_network', this.CBITimeInput, + 'mod_network_restart_dead_period', _('Dead period'), _('Longest period of time without Internet access before network restart.') ); - o.rmempty = false; + o.default = '900'; + o.rmempty = false; + o.modalonly = true; // attempts - o = ss.option(form.ListValue, - 'attempts', _('Restart attempts'), + o = ss.taboption('restart_network', form.ListValue, + 'mod_network_restart_attempts', _('Restart attempts'), _('Maximum number of network restart attempts before Internet access is available.') ); + o.modalonly = true; o.value(1); o.value(2); o.value(3); o.value(4); o.value(5); + o.default = '1'; // iface - o = ss.option(widgets.DeviceSelect, 'iface', _('Interface'), + o = ss.taboption('restart_network', widgets.DeviceSelect, 'mod_network_restart_iface', + _('Interface'), _('Network interface to restart. If not specified, then the network service is restarted.') ); + o.modalonly = true; // restart_timeout - o = ss.option(form.ListValue, - 'restart_timeout', _('Restart timeout'), + o = ss.taboption('restart_network', form.ListValue, + 'mod_network_restart_restart_timeout', _('Restart timeout'), _('Timeout between stopping and starting the interface.') ); + o.modalonly = true; o.value(0, '0 ' + _('sec')); o.value(1, '1 ' + _('sec')); o.value(2, '2 ' + _('sec')); @@ -856,97 +887,111 @@ return view.extend({ o.value(8, '8 ' + _('sec')); o.value(9, '9 ' + _('sec')); o.value(10, '10 ' + _('sec')); + o.default = '0'; // Restart modem - s.tab('restart_modem', _('Restart modem')); + ss.tab('restart_modem', _('Restart modem')); - o = s.taboption('restart_modem', form.DummyValue, '_dummy'); + o = ss.taboption('restart_modem', form.DummyValue, '_dummy'); o.rawhtml = true; o.default = '
' + _('Modem will be restarted when the Internet is disconnected.') + '
'; - - o = s.taboption('restart_modem', form.SectionValue, 'mod_modem_restart', form.NamedSection, - 'mod_modem_restart', 'mod_modem_restart' - ); - ss = o.subsection; + o.modalonly = true; if(this.mm) { // enabled - o = ss.option(form.Flag, 'enabled', - _('Enable'), + o = ss.taboption('restart_modem', form.Flag, 'mod_modem_restart_enabled', + _('Enabled'), ); - o.rmempty = false; + o.rmempty = false; + o.modalonly = true; // dead_period - o = ss.option(this.CBITimeInput, - 'dead_period', _('Dead period'), + o = ss.taboption('restart_modem', this.CBITimeInput, + 'mod_modem_restart_dead_period', _('Dead period'), _('Longest period of time without Internet access before modem restart.') ); - o.rmempty = false; + o.default = '600'; + o.rmempty = false; + o.modalonly = true; // any_band - o = ss.option(form.Flag, - 'any_band', _('Unlock modem bands'), + o = ss.taboption('restart_modem', form.Flag, + 'mod_modem_restart_any_band', _('Unlock modem bands'), _('Set the modem to be allowed to use any band.') ); - o.rmempty = false; + o.rmempty = false; + o.modalonly = true; // iface - o = ss.option(widgets.NetworkSelect, 'iface', _('Interface'), + o = ss.taboption('restart_modem', widgets.NetworkSelect, 'mod_modem_restart_iface', + _('Interface'), _('ModemManger interface. If specified, it will be restarted after restarting ModemManager.') ); o.multiple = false; o.nocreate = true; - o.rmempty = true; + o.modalonly = true; } else { - o = ss.option(form.DummyValue, '_dummy'); + o = ss.taboption('restart_modem', form.DummyValue, '_dummy'); o.rawhtml = true; o.default = '
' + _('ModemManager is not available...') + '
'; + o.modalonly = true; }; // Public IP address - s.tab('public_ip', _('Public IP address')); + ss.tab('public_ip', _('Public IP address')); - o = s.taboption('public_ip', form.DummyValue, '_dummy'); + o = ss.taboption('public_ip', form.DummyValue, '_dummy'); o.rawhtml = true; o.default = '
' + _('Checking the real public IP address.') + '
'; - - o = s.taboption('public_ip', form.SectionValue, 'mod_public_ip', form.NamedSection, - 'mod_public_ip', 'mod_public_ip' - ); - ss = o.subsection; + o.modalonly = true; // enabled - o = ss.option(form.Flag, 'enabled', - _('Enable')); - o.rmempty = false; + o = ss.taboption('public_ip', form.Flag, 'mod_public_ip_enabled', + _('Enabled')); + o.rmempty = false; + o.modalonly = true; // provider - o = ss.option(form.ListValue, - 'provider', _('DNS provider'), + o = ss.taboption('public_ip', form.ListValue, + 'mod_public_ip_provider', _('DNS provider'), _('Service for determining the public IP address through DNS.') ); + o.modalonly = true; o.value('opendns1'); o.value('opendns2'); o.value('opendns3'); o.value('opendns4'); o.value('akamai'); o.value('google'); + o.default = 'opendns1'; + + // ipv6 + o = ss.taboption('public_ip', form.ListValue, + 'mod_public_ip_qtype', _('DNS query type'), + _('The type of record requested in the DNS query (if the service supports it).') + ); + o.modalonly = true; + o.value('0', 'A (IPv4)'); + o.value('1', 'AAAA (IPv6)'); + o.default = '0'; // interval - o = ss.option(form.ListValue, - 'interval', _('Polling interval'), + o = ss.taboption('public_ip', form.ListValue, + 'mod_public_ip_interval', _('Polling interval'), _('Interval between IP address requests.') ); + o.default = '600'; + o.modalonly = true; o.value(60, '1' + ' ' + _('min')); o.value(300, '5' + ' ' + _('min')); o.value(600, '10' + ' ' + _('min')); @@ -955,167 +1000,224 @@ return view.extend({ o.value(10800, '3' + ' ' + _('hour')); // timeout - o = ss.option(form.ListValue, - 'timeout', _('Server response timeout') + o = ss.taboption('public_ip', form.ListValue, + 'mod_public_ip_timeout', _('Server response timeout') ); + o.default = '3' + o.modalonly = true; for(let i=1; i<=5; i++) { o.value(i, i + ' ' + _('sec')); }; // Email notification - s.tab('email', _('Email notification')); + ss.tab('email', _('Email notification')); - o = s.taboption('email', form.DummyValue, '_dummy'); + o = ss.taboption('email', form.DummyValue, '_dummy'); o.rawhtml = true; o.default = '
' + _('An email will be sent when the internet connection is restored after being disconnected.') + '
'; - - o = s.taboption('email', form.SectionValue, 'mod_email', form.NamedSection, - 'mod_email', 'mod_email' - ); - ss = o.subsection; + o.modalonly = true; if(this.mta) { // enabled - o = ss.option(form.Flag, 'enabled', - _('Enable')); + o = ss.taboption('email', form.Flag, 'mod_email_enabled', + _('Enabled')); o.rmempty = false; + o.modalonly = true; // alive_period - o = ss.option(this.CBITimeInput, - 'alive_period', _('Alive period'), + o = ss.taboption('email', this.CBITimeInput, + 'mod_email_alive_period', _('Alive period'), _('Longest period of time after connecting to the Internet before sending a message.') ); o.rmempty = false; + o.modalonly = true; // host_alias - o = ss.option(form.Value, 'host_alias', + o = ss.taboption('email', form.Value, 'mod_email_host_alias', _('Host alias'), _('Host identifier in messages. If not specified, hostname will be used.')); + o.modalonly = true; // mail_recipient - o = ss.option(form.Value, - 'mail_recipient', _('Recipient')); + o = ss.taboption('email', form.Value, + 'mod_email_mail_recipient', _('Recipient')); o.description = _('Email address of the recipient.'); + o.modalonly = true; // mail_sender - o = ss.option(form.Value, - 'mail_sender', _('Sender')); + o = ss.taboption('email', form.Value, + 'mod_email_mail_sender', _('Sender')); o.description = _('Email address of the sender.'); + o.modalonly = true; // mail_user - o = ss.option(form.Value, - 'mail_user', _('User')); + o = ss.taboption('email', form.Value, + 'mod_email_mail_user', _('User')); o.description = _('Username for SMTP authentication.'); + o.modalonly = true; // mail_password - o = ss.option(form.Value, - 'mail_password', _('Password')); + o = ss.taboption('email', form.Value, + 'mod_email_mail_password', _('Password')); o.description = _('Password for SMTP authentication.'); o.password = true; + o.modalonly = true; // mail_smtp - o = ss.option(form.Value, - 'mail_smtp', _('SMTP server')); + o = ss.taboption('email', form.Value, + 'mod_email_mail_smtp', _('SMTP server')); o.description = _('Hostname/IP address of the SMTP server.'); o.datatype = 'host'; - o.default = 'smtp.gmail.com'; + o.default = 'smtp.gmail.com'; + o.modalonly = true; // mail_smtp_port - o = ss.option(form.Value, - 'mail_smtp_port', _('SMTP server port')); + o = ss.taboption('email', form.Value, + 'mod_email_mail_smtp_port', _('SMTP server port')); o.datatype = 'port'; - o.default = '587'; + o.default = '587'; + o.modalonly = true; // mail_security - o = ss.option(form.ListValue, - 'mail_security', _('Security')); + o = ss.taboption('email', form.ListValue, + 'mod_email_mail_security', _('Security')); o.description = '%s
%s'.format( _('TLS: use STARTTLS if the server supports it.'), _('SSL: SMTP over SSL.'), ); o.value('tls', 'TLS'); o.value('ssl', 'SSL'); - o.default = 'tls'; + o.default = 'tls'; + o.modalonly = true; } else { - o = ss.option(form.DummyValue, '_dummy'); + o = ss.taboption('email', form.DummyValue, '_dummy'); o.rawhtml = true; o.default = '
' + _('Mailsend is not available...') + '
'; + o.modalonly = true; }; // User scripts + ss.tab('user_scripts', _('User scripts')); - let upScriptEditDialog = new this.fileEditDialog( - this.upScriptPath, - _('up-script'), - _('Shell commands that run when connected to the Internet.'), - ); - let downScriptEditDialog = new this.fileEditDialog( - this.downScriptPath, - _('down-script'), - _('Shell commands to run when disconnected from the Internet.'), - ); - - s.tab('user_scripts', _('User scripts')); - - o = s.taboption('user_scripts', form.DummyValue, '_dummy'); + o = ss.taboption('user_scripts', form.DummyValue, '_dummy'); o.rawhtml = true; o.default = '
' + _('Shell commands to run when connected or disconnected from the Internet.') + '
'; + o.modalonly = true; - o = s.taboption('user_scripts', form.SectionValue, 'mod_user_scripts', form.NamedSection, - 'mod_user_scripts', 'mod_user_scripts' + + /* UI detector configuration */ + + s.tab('ui_detector', _('UI detector configuration')); + + o = s.taboption('ui_detector', form.SectionValue, + 'ui', form.NamedSection, 'ui' ); ss = o.subsection; - // enabled - o = ss.option(form.Flag, 'enabled', - _('Enable')); - o.rmempty = false; + let makeUIIntervalOptions = L.bind(function(list) { + list.value(1, '%d %s'.format(this.pollInterval, _('sec'))); + list.value(2, '%d %s'.format(this.pollInterval * 2, _('sec'))); + list.value(3, '%d %s'.format(this.pollInterval * 3, _('sec'))); + list.value(4, '%d %s'.format(this.pollInterval * 4, _('sec'))); + list.value(5, '%d %s'.format(this.pollInterval * 5, _('sec'))); + list.value(6, '%d %s'.format(this.pollInterval * 6, _('sec'))); + }, this); - // up_script edit dialog - o = ss.option(form.Button, - '_up_script_btn', _('Edit up-script'), - _('Shell commands that run when connected to the Internet.') + // hosts + o = ss.option(form.DynamicList, + 'hosts', _('Hosts'), + _('Hosts to check Internet availability. Hosts are polled (in list order) until at least one of them responds.') ); - o.onclick = () => upScriptEditDialog.show(); - o.inputtitle = _('Edit'); - o.inputstyle = 'edit btn'; + o.datatype = 'or(or(host,hostport),ipaddrport(1))'; + o.default = this.defaultHosts; + o.rmempty = false; - // alive_period - o = ss.option(this.CBITimeInput, - 'alive_period', _('Alive period'), - _('Longest period of time after connecting to Internet before "up-script" runs.') + // check_type + o = ss.option(form.ListValue, + 'check_type', _('Check type'), + _('Host availability check type.') ); - o.rmempty = false; + o.value(0, _('TCP port connection')); + o.value(1, _('ICMP-echo request (ping)')); + o.default = '0'; - // down_script edit dialog - o = ss.option(form.Button, - '_down_script_btn', _('Edit down-script'), - _('Shell commands to run when disconnected from the Internet.') + // tcp_port + o = ss.option(form.Value, + 'tcp_port', _('TCP port'), + _('Default port value for TCP connections.') ); - o.onclick = () => downScriptEditDialog.show(); - o.inputtitle = _('Edit'); - o.inputstyle = 'edit btn'; + o.datatype = 'port'; + o.default = '53'; + o.depends({ check_type: '0' }); - // dead_period - o = ss.option(this.CBITimeInput, - 'dead_period', _('Dead period'), - _('Longest period of time after disconnecting from Internet before "down-script" runs.') + // icmp_packet_size + o = ss.option(form.ListValue, + 'icmp_packet_size', _('ICMP packet data size')); + o.value(1, _('Small: 1 byte')); + o.value(32, _('Windows: 32 bytes')); + o.value(56, _('Standard: 56 bytes')); + o.value(248, _('Big: 248 bytes')); + o.value(1492, _('Huge: 1492 bytes')); + o.value(9000, _('Jumbo: 9000 bytes')); + o.default = '56'; + o.depends({ check_type: '1' }); + + // iface + o = ss.option(widgets.DeviceSelect, + 'iface', _('Interface'), + _('Network interface for Internet access. If not specified, the default interface is used.') ); - o.rmempty = false; + o.noaliases = true; + + // interval_up + o = ss.option(form.ListValue, + 'interval_up', _('Alive interval'), + _('Hosts polling interval when the Internet is up.') + ); + makeUIIntervalOptions(o); + o.default = '6'; + + // interval_down + o = ss.option(form.ListValue, + 'interval_down', _('Dead interval'), + _('Hosts polling interval when the Internet is down.') + ); + makeUIIntervalOptions(o); + o.default = '1'; + + // connection_attempts + o = ss.option(form.ListValue, + 'connection_attempts', _('Connection attempts'), + _('Maximum number of attempts to connect to each host.') + ); + o.value(1); + o.value(2); + o.value(3); + o.default = '1'; + + // connection_timeout + o = ss.option(form.ListValue, + 'connection_timeout', _('Connection timeout'), + _('Maximum timeout for waiting for a response from the host.') + ); + o.value(1, '1 ' + _('sec')); + o.value(2, '2 ' + _('sec')); + o.value(3, '3 ' + _('sec')); + o.default = '1'; if(this.currentAppMode !== '0') { poll.add( - L.bind((this.currentAppMode === '2') ? this.servicePoll : this.uiPoll, this), + L.bind((this.currentAppMode === '1') ? this.servicePoll : this.uiPoll, this), this.pollInterval ); }; @@ -1126,6 +1228,7 @@ return view.extend({ }, handleSaveApply: function(ev, mode) { + poll.stop(); return this.handleSave(ev).then(() => { ui.changes.apply(mode == '0'); window.setTimeout(() => this.serviceRestart(), 3000); diff --git a/luci-app-internet-detector/htdocs/luci-static/resources/view/status/include/00_internet.js b/luci-app-internet-detector/htdocs/luci-static/resources/view/status/include/00_internet.js index 0c8ad72..5b19af6 100644 --- a/luci-app-internet-detector/htdocs/luci-static/resources/view/status/include/00_internet.js +++ b/luci-app-internet-detector/htdocs/luci-static/resources/view/status/include/00_internet.js @@ -6,113 +6,154 @@ document.head.append(E('style', {'type': 'text/css'}, ` :root { - --app-id-font-color: #fff; - --app-id-connected-color: #2ea256; - --app-id-disconnected-color: #ff4e54; - --app-id-undefined-color: #8a8a8a; + --app-id-font-color: #454545; + --app-id-font-shadow: #fff; + --app-id-connected-color: #6bdebb; + --app-id-disconnected-color: #f8aeba; + --app-id-undefined-color: #dfdfdf; } :root[data-darkmode="true"] { + --app-id-font-color: #f6f6f6; + --app-id-font-shadow: #4d4d4d; --app-id-connected-color: #005F20; --app-id-disconnected-color: #a93734; --app-id-undefined-color: #4d4d4d; } .id-connected { + --on-color: var(--app-id-font-color); background-color: var(--app-id-connected-color) !important; + border-color: var(--app-id-connected-color) !important; color: var(--app-id-font-color) !important; + text-shadow: 0 1px 1px var(--app-id-font-shadow); } .id-disconnected { + --on-color: var(--app-id-font-color); background-color: var(--app-id-disconnected-color) !important; + border-color: var(--app-id-disconnected-color) !important; color: var(--app-id-font-color) !important; + text-shadow: 0 1px 1px var(--app-id-font-shadow); } .id-undefined { + --on-color: var(--app-id-font-color); background-color: var(--app-id-undefined-color) !important; + border-color: var(--app-id-undefined-color) !important; color: var(--app-id-font-color) !important; + text-shadow: 0 1px 1px var(--app-id-font-shadow); +} +.id-label-status { + display: inline-block; + word-wrap: break-word; + margin: 2px !important; + padding: 4px 8px; + border: 1px solid; + -webkit-border-radius: 4px; + -moz-border-radius: 4px; + border-radius: 4px; + font-weight: bold; } `)); return baseclass.extend({ - title : _('Internet'), - appName : 'internet-detector', - execPath : '/usr/bin/internet-detector', - inetStatus : null, - publicIp : null, + title : _('Internet'), + appName : 'internet-detector', + execPath : '/usr/bin/internet-detector', + uiCheckIntervalUp : null, + uiCheckIntervalDown : null, + currentAppMode : null, + inetStatus : null, - inetStatusFromJson: function(res) { - let curInetStatus = null; - let curPubIp = null; + inetStatusFromJson : function(res) { + let inetStatData = null; if(res.code === 0) { try { - let json = JSON.parse(res.stdout.trim()); - curInetStatus = json.inet; - curPubIp = json.mod_public_ip; + inetStatData = JSON.parse(res.stdout.trim()); } catch(e) {}; }; - return [ curInetStatus, curPubIp ]; + return inetStatData; }, load: async function() { if(!( - 'uiCheckIntervalUp' in window && - 'uiCheckIntervalDown' in window && - 'currentAppMode' in window + this.uiCheckIntervalUp && + this.uiCheckIntervalDown && + this.currentAppMode )) { await uci.load(this.appName).then(data => { - window.uiCheckIntervalUp = Number(uci.get(this.appName, 'config', 'ui_interval_up')); - window.uiCheckIntervalDown = Number(uci.get(this.appName, 'config', 'ui_interval_down')); - window.currentAppMode = uci.get(this.appName, 'config', 'mode'); + this.uiCheckIntervalUp = Number(uci.get(this.appName, 'ui', 'interval_up')); + this.uiCheckIntervalDown = Number(uci.get(this.appName, 'ui', 'interval_down')); + this.currentAppMode = uci.get(this.appName, 'config', 'mode'); }).catch(e => {}); }; - if(window.currentAppMode === '1' || window.currentAppMode === '2') { - window.internetDetectorCounter = ('internetDetectorCounter' in window) ? - ++window.internetDetectorCounter : 0; + if(this.currentAppMode === '2') { + this.internetDetectorCounter = ('internetDetectorCounter' in this) ? + ++this.internetDetectorCounter : 0; - if(!('internetDetectorState' in window)) { - window.internetDetectorState = 2; - }; - - if(window.currentAppMode === '1' && ( - (window.internetDetectorState === 0 && window.internetDetectorCounter % window.uiCheckIntervalUp) || - (window.internetDetectorState === 1 && window.internetDetectorCounter % window.uiCheckIntervalDown) - )) { + if((this.internetDetectorStateUi === 0 && + this.internetDetectorCounter % this.uiCheckIntervalUp) || + (this.internetDetectorStateUi === 1 && + this.internetDetectorCounter % this.uiCheckIntervalDown) + ) { return; }; - window.internetDetectorCounter = 0; - return L.resolveDefault(fs.exec(this.execPath, [ 'inet-status-json' ]), null); + this.internetDetectorCounter = 0; + return L.resolveDefault(fs.exec(this.execPath, [ 'poll' ]), null); } - else { - window.internetDetectorState = 2; + else if(this.currentAppMode === '1') { + return L.resolveDefault(fs.exec(this.execPath, [ 'inet-status' ]), null); }; }, render: function(data) { - if(window.currentAppMode === '0') { - return + if(this.currentAppMode === '0') { + return; }; if(data) { - [ window.internetDetectorState, this.publicIp ] = this.inetStatusFromJson(data); + this.inetStatus = this.inetStatusFromJson(data); + if(this.currentAppMode === '2') { + this.internetDetectorStateUi = this.inetStatus.instances[0].inet; + }; }; - let internetStatus = E('span', { 'class': 'label' }); + let inetStatusArea = E('div', {}); - if(window.internetDetectorState === 0) { - internetStatus.textContent = _('Connected') + (this.publicIp ? ' | %s: %s'.format(_('Public IP'), _(this.publicIp)) : ''); - internetStatus.className = "label id-connected"; - } - else if(window.internetDetectorState === 1) { - internetStatus.textContent = _('Disconnected'); - internetStatus.className = "label id-disconnected"; - } - else { - internetStatus.textContent = _('Undefined'); - internetStatus.className = "label id-undefined"; + if(!this.inetStatus || !this.inetStatus.instances || this.inetStatus.instances.length === 0) { + inetStatusArea.append( + E('span', { 'class': 'id-label-status id-undefined' }, _('Undefined')) + ); + } else { + this.inetStatus.instances.sort((a, b) => a.num > b.num); + + for(let i of this.inetStatus.instances) { + let status = _('Disconnected'); + let className = 'id-label-status id-disconnected'; + if(i.inet == 0) { + status = _('Connected'); + className = 'id-label-status id-connected'; + } + else if(i.inet == -1) { + status = _('Undefined'); + className = 'id-label-status id-undefined spinning'; + }; + + let publicIp = (i.mod_public_ip) ? ' | %s: %s'.format( + _('Public IP'), _(i.mod_public_ip) + ) : ''; + + inetStatusArea.append( + E('span', { 'class': className }, '%s%s%s'.format( + (this.currentAppMode === '1') ? i.instance + ': ' : '', + status, publicIp) + ) + ); + }; }; return E('div', { 'class': 'cbi-section', 'style': 'margin-bottom:1em', - }, internetStatus); + }, inetStatusArea); }, }); diff --git a/luci-app-internet-detector/po/ru/internet-detector.po b/luci-app-internet-detector/po/ru/internet-detector.po index dd14d45..54350c0 100644 --- a/luci-app-internet-detector/po/ru/internet-detector.po +++ b/luci-app-internet-detector/po/ru/internet-detector.po @@ -24,6 +24,9 @@ msgid "" "available." msgstr "LED включен если Интернет доступен." +msgid "Add instance" +msgstr "Добавить экземпляр" + msgid "Alive interval" msgstr "Интервал при подключении" @@ -92,6 +95,9 @@ msgstr "Отключен" msgid "Dismiss" msgstr "Закрыть" +msgid "DNS query type" +msgstr "Тип DNS-запроса" + msgid "DNS provider" msgstr "DNS провайдер" @@ -117,7 +123,7 @@ msgid "Enable" msgstr "Включить" msgid "Enable logging" -msgstr "Включить запись событий в лог" +msgstr "Запись событий в лог" msgid "Enabled" msgstr "Включен" @@ -168,6 +174,12 @@ msgstr "" msgid "Huge: 1492 bytes" msgstr "Огромный: 1492 байта" +msgid "ICMP-echo request (ping)" +msgstr "Запрос ICMP-echo (ping)" + +msgid "ICMP packet data size" +msgstr "Размер данных ICMP-пакета" + msgid "Interface" msgstr "Интерфейс" @@ -288,19 +300,6 @@ msgstr "Пароль" msgid "Password for SMTP authentication." msgstr "Пароль для SMTP-аутентификации." -msgid "" -"Performing actions when connecting and disconnecting the Internet (available " -"in the \"Service\" mode)." -msgstr "" -"Выполнение действий при подключении и отключении Интернет (доступно в режиме " -"\"Служба\")." - -msgid "Ping host" -msgstr "Пинг хоста" - -msgid "Ping packet size" -msgstr "Размер пакета Ping" - msgid "Polling interval" msgstr "Интервал опроса" @@ -367,12 +366,12 @@ msgstr "Конфигурация службы" msgid "Service for determining the public IP address through DNS." msgstr "Сервис для определения публичного IP адреса через DNS." -msgid "Service modules" -msgstr "Модули службы" - msgid "Service: detector always runs as a system service." msgstr "Служба: детектор работает постоянно, как системная служба." +msgid "Service instances" +msgstr "Экземпляры службы" + msgid "Set the modem to be allowed to use any band." msgstr "Разрешить модему использование любой частоты." @@ -400,6 +399,9 @@ msgstr "TCP-порт" msgid "TCP port connection" msgstr "Подключение к TCP-порту" +msgid "The type of record requested in the DNS query (if the service supports it)." +msgstr "Тип записи запрашиваемой в DNS-запросе (если сервис поддерживает)." + msgid "TLS: use STARTTLS if the server supports it." msgstr "TLS: использовать STARTTLS если сервер поддерживает." @@ -438,8 +440,8 @@ msgstr "" "Ожидание завершения перезагрузки перед выполнением принудительной " "перезагрузки." -msgid "Web UI only" -msgstr "Только web-интерфейс" +msgid "Web UI only (UI detector)" +msgstr "Только web-интерфейс (UI детектор)" msgid "Web UI only: detector works only when the Web UI is open (UI detector)." msgstr "" diff --git a/luci-app-internet-detector/po/templates/internet-detector.pot b/luci-app-internet-detector/po/templates/internet-detector.pot index 57f6ba0..4be8c77 100644 --- a/luci-app-internet-detector/po/templates/internet-detector.pot +++ b/luci-app-internet-detector/po/templates/internet-detector.pot @@ -12,6 +12,9 @@ msgid "" "available." msgstr "" +msgid "Add instance" +msgstr "" + msgid "Alive interval" msgstr "" @@ -80,6 +83,9 @@ msgstr "" msgid "Dismiss" msgstr "" +msgid "DNS query type" +msgstr "" + msgid "DNS provider" msgstr "" @@ -154,6 +160,12 @@ msgstr "" msgid "Huge: 1492 bytes" msgstr "" +msgid "ICMP-echo request (ping)" +msgstr "" + +msgid "ICMP packet data size" +msgstr "" + msgid "Interface" msgstr "" @@ -258,17 +270,6 @@ msgstr "" msgid "Password for SMTP authentication." msgstr "" -msgid "" -"Performing actions when connecting and disconnecting the Internet (available " -"in the \"Service\" mode)." -msgstr "" - -msgid "Ping host" -msgstr "" - -msgid "Ping packet size" -msgstr "" - msgid "Polling interval" msgstr "" @@ -335,10 +336,10 @@ msgstr "" msgid "Service for determining the public IP address through DNS." msgstr "" -msgid "Service modules" +msgid "Service: detector always runs as a system service." msgstr "" -msgid "Service: detector always runs as a system service." +msgid "Service instances" msgstr "" msgid "Set the modem to be allowed to use any band." @@ -368,6 +369,9 @@ msgstr "" msgid "TCP port connection" msgstr "" +msgid "The type of record requested in the DNS query (if the service supports it)." +msgstr "" + msgid "TLS: use STARTTLS if the server supports it." msgstr "" @@ -404,7 +408,7 @@ msgstr "" msgid "Waiting for a reboot to complete before performing a forced reboot." msgstr "" -msgid "Web UI only" +msgid "Web UI only (UI detector)" msgstr "" msgid "Web UI only: detector works only when the Web UI is open (UI detector)." diff --git a/luci-app-internet-detector/root/usr/share/rpcd/acl.d/luci-app-internet-detector.json b/luci-app-internet-detector/root/usr/share/rpcd/acl.d/luci-app-internet-detector.json index 7a49cf0..7736789 100644 --- a/luci-app-internet-detector/root/usr/share/rpcd/acl.d/luci-app-internet-detector.json +++ b/luci-app-internet-detector/root/usr/share/rpcd/acl.d/luci-app-internet-detector.json @@ -4,8 +4,8 @@ "read": { "file": { "/sys/class/leds": [ "list" ], - "/etc/internet-detector/up-script": [ "read" ], - "/etc/internet-detector/down-script": [ "read" ], + "/etc/internet-detector/up-script*": [ "read" ], + "/etc/internet-detector/down-script*": [ "read" ], "/usr/bin/internet-detector*": [ "exec" ], "/usr/bin/mailsend": [ "exec" ] }, @@ -16,8 +16,8 @@ }, "write": { "file": { - "/etc/internet-detector/up-script": [ "write" ], - "/etc/internet-detector/down-script": [ "write" ] + "/etc/internet-detector/up-script*": [ "write" ], + "/etc/internet-detector/down-script*": [ "write" ] }, "uci": [ "internet-detector" ] } diff --git a/screenshots/01.jpg b/screenshots/01.jpg index 5a0bb75..2373ff1 100644 Binary files a/screenshots/01.jpg and b/screenshots/01.jpg differ diff --git a/screenshots/02.jpg b/screenshots/02.jpg index 89ee32f..84fbb7a 100644 Binary files a/screenshots/02.jpg and b/screenshots/02.jpg differ diff --git a/screenshots/03.jpg b/screenshots/03.jpg new file mode 100644 index 0000000..9807930 Binary files /dev/null and b/screenshots/03.jpg differ diff --git a/screenshots/04.jpg b/screenshots/04.jpg new file mode 100644 index 0000000..5fd9b15 Binary files /dev/null and b/screenshots/04.jpg differ