mirror of
https://github.com/kiddin9/op-packages.git
synced 2026-11-01 04:33:56 +08:00
🌴 Sync 2026-03-05 23:51:42
This commit is contained in:
1 parent
97c049ed0a
commit
0acff03804
14253 files changed
+3893239
No files matched your search
@@ -0,0 +1,68 @@
|
||||
include $(TOPDIR)/rules.mk
|
||||
|
||||
PKG_NAME:=luci-app-cqustdotnet
|
||||
PKG_VERSION:=1.1.10
|
||||
PKG_RELEASE:=1
|
||||
PKG_MAINTAINER:=Hurring <https://github.com/ApeaSuperZ/luci-app-cqustdotnet>
|
||||
|
||||
PKG_BUILD_DIR:=$(BUILD_DIR)/$(PKG_NAME)
|
||||
|
||||
include $(INCLUDE_DIR)/package.mk
|
||||
|
||||
define Package/$(PKG_NAME)/config
|
||||
endef
|
||||
|
||||
define Package/$(PKG_NAME)
|
||||
CATEGORY:=LuCI
|
||||
SUBMENU:=3. Applications
|
||||
TITLE:=A LuCI app to simply connect to network of CQUST
|
||||
PKGARCH:=all
|
||||
DEPENDS:=+bash +curl +lua +libuci-lua
|
||||
MAINTAINER:=Hurring
|
||||
endef
|
||||
|
||||
define Package/$(PKG_NAME)/description
|
||||
A LuCI app to simply connect to network of CQUST
|
||||
endef
|
||||
|
||||
define Build/Prepare
|
||||
$(CP) $(CURDIR)/root $(PKG_BUILD_DIR)
|
||||
$(CP) $(CURDIR)/luasrc $(PKG_BUILD_DIR)
|
||||
chmod 0755 $(PKG_BUILD_DIR)/root/etc/init.d/cqustdotnet
|
||||
chmod -R 0755 $(PKG_BUILD_DIR)/root/usr/share/cqustdotnet/
|
||||
mkdir -p $(PKG_BUILD_DIR)/root/etc/openclash/config
|
||||
exit 0
|
||||
endef
|
||||
|
||||
define Build/Configure
|
||||
endef
|
||||
|
||||
define Build/Compile
|
||||
endef
|
||||
|
||||
define Package/$(PKG_NAME)/conffiles
|
||||
/etc/config/cqustdotnet
|
||||
endef
|
||||
|
||||
define Package/$(PKG_NAME)/preinst
|
||||
endef
|
||||
|
||||
define Package/$(PKG_NAME)/postinst
|
||||
endef
|
||||
|
||||
define Package/$(PKG_NAME)/prerm
|
||||
uci -q set cqustdotnet.config.enabled=0
|
||||
uci -q commit cqustdotnet
|
||||
exit 0
|
||||
endef
|
||||
|
||||
define Package/$(PKG_NAME)/postrm
|
||||
endef
|
||||
|
||||
define Package/$(PKG_NAME)/install
|
||||
$(CP) $(PKG_BUILD_DIR)/root/* $(1)/
|
||||
[ -d $(1)/usr/lib/lua/luci ] || mkdir -p $(1)/usr/lib/lua/luci
|
||||
$(CP) $(PKG_BUILD_DIR)/luasrc/* $(1)/usr/lib/lua/luci/
|
||||
endef
|
||||
|
||||
$(eval $(call BuildPackage,$(PKG_NAME)))
|
||||
@@ -0,0 +1,106 @@
|
||||
module('luci.controller.cqustdotnet', package.seeall)
|
||||
|
||||
function index()
|
||||
-- upvalues 在此函数中是 nil,使用的话需要 require
|
||||
local app_name = require('luci.model.cbi.cqustdotnet.api.constants').LUCI_NAME
|
||||
|
||||
entry({ 'admin', 'services', app_name }).dependent = true
|
||||
entry({ 'admin', 'services', app_name, 'reset_config' }, call('reset_config')).leaf = true
|
||||
if not nixio.fs.access('/etc/config/cqustdotnet') then
|
||||
return
|
||||
end
|
||||
e = entry({ 'admin', 'services', app_name }, alias('admin', 'services', app_name, 'index'), _('CQUST.net'), -10)
|
||||
e.dependent = true
|
||||
e.acl_depends = { 'luci-app-cqustdotnet' }
|
||||
|
||||
-- Client
|
||||
entry({ 'admin', 'services', app_name, 'index' }, cbi(app_name .. '/client/global'), _('主页'), 1).dependent = true
|
||||
entry({ 'admin', 'services', app_name, 'accounts' }, cbi(app_name .. '/client/accounts'), _('账号'), 2).dependent = true
|
||||
entry({ 'admin', 'services', app_name, 'account' }, cbi(app_name .. '/client/account')).leaf = true
|
||||
entry({ 'admin', 'services', app_name, 'log' }, form(app_name .. '/client/log'), _('日志'), 999).leaf = true
|
||||
|
||||
-- API
|
||||
entry({ 'admin', 'services', app_name, 'status' }, call('status')).leaf = true
|
||||
entry({ 'admin', 'services', app_name, 'get_log' }, call('get_log')).leaf = true
|
||||
entry({ 'admin', 'services', app_name, 'clear_log' }, call('clear_log')).leaf = true
|
||||
entry({ 'admin', 'services', app_name, 'fetch_log' }, call('fetch_log')).leaf = true
|
||||
end
|
||||
|
||||
local api = require('luci.model.cbi.cqustdotnet.api.api')
|
||||
local accounts = require('luci.model.cbi.cqustdotnet.api.accounts')
|
||||
local const = require('luci.model.cbi.cqustdotnet.api.constants')
|
||||
|
||||
function reset_config()
|
||||
---@language Shell Script
|
||||
luci.sys.call([[
|
||||
/etc/init.d/cqustdotnet stop
|
||||
[ -f /usr/share/cqustdotnet/0_default_config ] && cp -f /usr/share/cqustdotnet/0_default_config /etc/config/cqustdotnet
|
||||
]])
|
||||
luci.http.redirect(api.url())
|
||||
end
|
||||
|
||||
function status()
|
||||
local status = {
|
||||
connector = api.is_process_running(const.LUCI_NAME .. '/connector.lua')
|
||||
}
|
||||
|
||||
---@type Account|nil
|
||||
local current_account = accounts.current()
|
||||
if current_account then
|
||||
status.account = string.format('%s (%s)', current_account.username, current_account.remark)
|
||||
end
|
||||
|
||||
luci.http.prepare_content('application/json')
|
||||
luci.http.write_json(status)
|
||||
end
|
||||
|
||||
function get_log()
|
||||
luci.http.write(luci.sys.exec("[ -f '/var/log/cqustdotnet.log' ] && cat /var/log/cqustdotnet.log"))
|
||||
end
|
||||
|
||||
function fetch_log()
|
||||
local log_file = api.get_log_file() ---@type file|nil
|
||||
|
||||
-- 日志不存在,返回 404 Not Found
|
||||
if not log_file then
|
||||
luci.http.status(404, 'Not Found')
|
||||
luci.http.close()
|
||||
return
|
||||
end
|
||||
|
||||
local after = tonumber(luci.http.formvalue('after'))
|
||||
local before = tonumber(luci.http.formvalue('before'))
|
||||
local limit = 1024 -- 一次最多返回 1KB 的日志
|
||||
|
||||
luci.http.prepare_content('application/json')
|
||||
local file_size = log_file:seek('end')
|
||||
if after == before or (after and before) then
|
||||
-- after 和 before 都为空时,返回最新日志。
|
||||
-- after 和 before 相等时,视为空参数,返回最新的日志。
|
||||
-- after 和 before 都存在时,属于错误参数,按未传入参数处理。
|
||||
if file_size > limit then
|
||||
log_file:seek('end', -limit)
|
||||
else
|
||||
log_file:seek('set')
|
||||
end
|
||||
luci.http.write_json({
|
||||
cursor = file_size,
|
||||
fragment = (log_file:read(limit))
|
||||
})
|
||||
elseif after then
|
||||
-- after 不为空,before 为空,返回 after 之后的日志
|
||||
log_file:seek('set', after)
|
||||
luci.http.write_json({
|
||||
cursor = file_size - after > limit and after + limit or file_size,
|
||||
fragment = log_file:read(limit)
|
||||
})
|
||||
else
|
||||
-- before 不为空,after 为空,返回 before 之前的日志
|
||||
log_file:seek('set', before <= limit and 0 or before - limit)
|
||||
luci.http.write_json({ fragment = log_file:read(before < limit and limit - before or limit) })
|
||||
end
|
||||
end
|
||||
|
||||
function clear_log()
|
||||
luci.sys.call("echo '' > /var/log/cqustdotnet.log")
|
||||
end
|
||||
@@ -0,0 +1,85 @@
|
||||
module('luci.model.cbi.cqustdotnet.api.accounts', package.seeall)
|
||||
local api = require('luci.model.cbi.cqustdotnet.api.api')
|
||||
local const = require('luci.model.cbi.cqustdotnet.api.constants')
|
||||
|
||||
local uci = api.uci
|
||||
|
||||
---
|
||||
---@class Account 校园网账号
|
||||
---@field remark string 备注
|
||||
---@field username string 用户名
|
||||
---@field password string 密码
|
||||
---@field ban string|nil 解封时间戳
|
||||
---@field wrong_password boolean|nil 密码错误标记
|
||||
local _ = {
|
||||
---@type string UCI 内部名称
|
||||
['.name'] = nil,
|
||||
|
||||
---@type string UCI 数据类型
|
||||
['.type'] = nil,
|
||||
|
||||
---@type boolean 是否匿名 UCI 数据(无 .name)
|
||||
['.anonymous'] = false
|
||||
}
|
||||
|
||||
---
|
||||
--- 根据排序获取首个可用账号。
|
||||
---@overload fun(start_account_name:string):Account
|
||||
---@overload fun():Account
|
||||
---@param start_account_name string|nil 起始账号名,不包含在返回结果内
|
||||
---@param end_account_name string 结束账号名,不包含在返回结果内
|
||||
---@return Account|nil 可用账号,无可用账号时返回 nil
|
||||
function get_first_available(start_account_name, end_account_name)
|
||||
local start_check = not start_account_name
|
||||
local index = 0
|
||||
while true do
|
||||
if not start_check then
|
||||
local datatype, account_name = uci:get(const.LUCI_NAME, '@accounts[' .. index .. ']')
|
||||
|
||||
-- 没有找到起始账号
|
||||
if not datatype then
|
||||
return nil
|
||||
end
|
||||
|
||||
-- 找到起始账号,开始做额外检查
|
||||
if account_name == start_account_name then
|
||||
start_check = true
|
||||
end
|
||||
else
|
||||
---@type Account
|
||||
local account = uci:get_all(const.LUCI_NAME, '@accounts[' .. index .. ']')
|
||||
if not account then
|
||||
-- 没有指定起始账号的情况下,遍历到最后一个账号,说明没有可用账号
|
||||
if not start_account_name then
|
||||
return nil
|
||||
end
|
||||
|
||||
-- 指定了起始账号的情况下,在起始账号之前的账号还没有做详细校验
|
||||
return get_first_available(nil, start_account_name)
|
||||
end
|
||||
|
||||
-- 限制了结束账号
|
||||
if account['.name'] == end_account_name then
|
||||
return nil
|
||||
end
|
||||
|
||||
-- 检查账号状态是否正常
|
||||
if not account.wrong_password and (not account.ban or os.difftime(account.ban, os.time()) <= 0) and not account.arrears then
|
||||
return account
|
||||
end
|
||||
end
|
||||
|
||||
index = index + 1
|
||||
end
|
||||
end
|
||||
|
||||
---
|
||||
--- 获取当前在使用的账号。
|
||||
---@return Account|nil 当前账号,无账号在使用时返回 nil
|
||||
function current()
|
||||
local current_account_name = uci:get(const.LUCI_NAME, 'config', 'current_account')
|
||||
if not current_account_name then
|
||||
return nil
|
||||
end
|
||||
return uci:get_all(const.LUCI_NAME, current_account_name)
|
||||
end
|
||||
@@ -0,0 +1,79 @@
|
||||
module('luci.model.cbi.cqustdotnet.api.api', package.seeall)
|
||||
sys = require('luci.sys')
|
||||
uci = require('luci.model.uci').cursor()
|
||||
dispatcher = require('luci.dispatcher')
|
||||
http = require('luci.http')
|
||||
const = require('luci.model.cbi.cqustdotnet.api.constants')
|
||||
|
||||
function url(...)
|
||||
local url = string.format('admin/services/%s', const.LUCI_NAME)
|
||||
local args = { ... }
|
||||
for _, v in pairs(args) do
|
||||
if v ~= '' then
|
||||
url = url .. '/' .. v
|
||||
end
|
||||
end
|
||||
return dispatcher.build_url(url)
|
||||
end
|
||||
|
||||
---@type file|nil
|
||||
local log_file
|
||||
function log(...)
|
||||
if debug == true then
|
||||
print(...)
|
||||
return
|
||||
end
|
||||
|
||||
if not log_file then
|
||||
log_file = io.open(string.format('/var/log/%s.log', const.LUCI_NAME), 'a+')
|
||||
end
|
||||
|
||||
if log_file then
|
||||
log_file:write(os.date('%Y-%m-%d %H:%M:%S'), ': ')
|
||||
log_file:write(...)
|
||||
log_file:write('\n')
|
||||
log_file:flush()
|
||||
end
|
||||
end
|
||||
|
||||
function get_log_file()
|
||||
if not log_file then
|
||||
log_file = io.open(string.format('/var/log/%s.log', const.LUCI_NAME), 'a+')
|
||||
end
|
||||
return log_file
|
||||
end
|
||||
|
||||
function gen_uuid(keep_dash)
|
||||
local uuid = sys.exec('echo -n $(cat /proc/sys/kernel/random/uuid)')
|
||||
if keep_dash then
|
||||
return uuid
|
||||
else
|
||||
return uuid:gsub('-', '')
|
||||
end
|
||||
end
|
||||
|
||||
function is_process_running(process_name)
|
||||
local advanced_ps = sys.exec("ps --version 2>&1 | grep -c procps-ng | tr -d '\n'")
|
||||
if advanced_ps == '1' then
|
||||
return sys.call(string.format("ps -efw | grep '%s' | grep -v grep >/dev/null", process_name)) == 0
|
||||
else
|
||||
return sys.call(string.format("ps -w | grep '%s' | grep -v grep >/dev/null", process_name)) == 0
|
||||
end
|
||||
end
|
||||
|
||||
---
|
||||
--- 删除字符串首尾的空白字符。
|
||||
---
|
||||
--- 该函数只适用于较为短小的字符串,字符串长度太长或者字符串全为空白字符时效率极低。
|
||||
--- 查阅 http://lua-users.org/wiki/StringTrim 获取更多信息(trim5)。
|
||||
---@param str string
|
||||
function trim_string(str)
|
||||
return str:match('^%s*(.*%S)') or ''
|
||||
end
|
||||
|
||||
function gc()
|
||||
if log_file then
|
||||
log_file:close()
|
||||
log_file = nil
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,4 @@
|
||||
module('luci.model.cbi.cqustdotnet.api.constants', package.seeall)
|
||||
|
||||
LUCI_NAME = 'cqustdotnet'
|
||||
APP_NAME = 'CQUST.net'
|
||||
@@ -0,0 +1,83 @@
|
||||
local api = require('luci.model.cbi.cqustdotnet.api.api')
|
||||
local const = require('luci.model.cbi.cqustdotnet.api.constants')
|
||||
|
||||
map = Map(const.LUCI_NAME)
|
||||
map.redirect = api.url('accounts')
|
||||
map.pageaction = false
|
||||
|
||||
local input_section = map:section(NamedSection, arg[1])
|
||||
input_section.addremove = false
|
||||
input_section.dynamic = false
|
||||
|
||||
option = input_section:option(Value, 'remark', '账号备注')
|
||||
option.rmempty = false
|
||||
option.validate = function(self, value, sec)
|
||||
if value then
|
||||
local count = 0
|
||||
self.map.uci:foreach(const.LUCI_NAME, 'accounts', function(s)
|
||||
if s['.name'] ~= sec and s['remark'] == value then
|
||||
count = count + 1
|
||||
end
|
||||
end)
|
||||
if count > 0 then
|
||||
return nil, '备注已存在,请重新设置。'
|
||||
end
|
||||
return value
|
||||
end
|
||||
end
|
||||
|
||||
option = input_section:option(Value, 'username', '用户名')
|
||||
option.rows = 3
|
||||
option.rmempty = false
|
||||
option.validate = function(self, value, sec)
|
||||
if value then
|
||||
local count = 0
|
||||
self.map.uci:foreach(const.LUCI_NAME, 'accounts', function(s)
|
||||
if s['.name'] ~= sec and s['url'] == value then
|
||||
count = count + 1
|
||||
end
|
||||
end)
|
||||
if count > 0 then
|
||||
return nil, '相同用户名已存在,无需重复添加。'
|
||||
end
|
||||
return value
|
||||
end
|
||||
end
|
||||
|
||||
option = input_section:option(Value, 'password', '密码')
|
||||
option.password = true
|
||||
option.rmempty = false
|
||||
|
||||
section = map:section(Table, { {} }--[[ 随便给 table 一个可索引对象,让 section 不会显示 "尚无任何配置" ]])
|
||||
|
||||
option = section:option(Button, 'back')
|
||||
option.inputtitle = '返回列表'
|
||||
option.inputstyle = 'link cbi-button-link'
|
||||
option.write = function()
|
||||
map.uci:revert(const.LUCI_NAME, arg[1])
|
||||
api.http.redirect(map.redirect)
|
||||
end
|
||||
|
||||
option = section:option(Button, 'commit')
|
||||
option.inputtitle = '保存账号信息'
|
||||
option.inputstyle = 'save'
|
||||
option.write = function()
|
||||
if input_section.error then
|
||||
return
|
||||
end
|
||||
|
||||
if map.changed then
|
||||
local changed_configs = map.uci:changes(const.LUCI_NAME)[const.LUCI_NAME][arg[1]]
|
||||
-- 更改了账号,清除禁封状态
|
||||
if changed_configs['username'] then
|
||||
map.uci:delete(const.LUCI_NAME, arg[1], 'ban')
|
||||
end
|
||||
|
||||
map.uci:delete(const.LUCI_NAME, arg[1], 'wrong_password') -- 无论是否更改了密码,都清除密码错误状态,因为用户可以重设校园网密码为本系统内填写的密码
|
||||
map.uci:commit(const.LUCI_NAME)
|
||||
end
|
||||
|
||||
api.http.redirect(map.redirect)
|
||||
end
|
||||
|
||||
return map
|
||||
@@ -0,0 +1,80 @@
|
||||
local api = require('luci.model.cbi.cqustdotnet.api.api')
|
||||
local const = require('luci.model.cbi.cqustdotnet.api.constants')
|
||||
|
||||
map = Map(const.LUCI_NAME)
|
||||
map.pageaction = false -- 不显示页面上的保存/应用按钮
|
||||
|
||||
-- 账号列表
|
||||
section = map:section(TypedSection, 'accounts')
|
||||
section.addremove = true -- 添加/删除按钮
|
||||
section.anonymous = true
|
||||
section.sortable = true -- 允许排序
|
||||
section.template = 'cbi/tblsection'
|
||||
---@language HTML
|
||||
section.description = [[
|
||||
您可以添加多个账号,某个账号出现问题时,会根据排序切换到其它账号。<br/>
|
||||
<br/>
|
||||
<span style="font-weight: normal">
|
||||
注意:账号状态为<span style="color: darkred">密码错误</span> / <span style="color: darkred">欠费</span>时,不会再尝试使用该账号,状态自然也不再刷新,您必须通过点击<code>修改</code>
|
||||
- <code>保存账号信息</code>来移除此状态。
|
||||
</span>
|
||||
]]
|
||||
section.extedit = api.url('account', '%s') -- 编辑按钮
|
||||
section.create = function(self)
|
||||
local existed
|
||||
map.uci:foreach(const.LUCI_NAME, 'accounts', function(account)
|
||||
if not account['remark'] or not account['username'] or not account['password'] then
|
||||
existed = account['.name']
|
||||
end
|
||||
end)
|
||||
|
||||
local id = existed
|
||||
if not existed then
|
||||
id = api.gen_uuid()
|
||||
TypedSection.create(self, id)
|
||||
end
|
||||
|
||||
luci.http.redirect(self.extedit:format(id))
|
||||
end
|
||||
|
||||
-- 账号备注
|
||||
option = section:option(DummyValue, 'remark', '备注')
|
||||
option.width = 'auto'
|
||||
option.rmempty = false
|
||||
|
||||
-- 账号状态
|
||||
option = section:option(DummyValue, 'state', '状态')
|
||||
option.rawhtml = true
|
||||
option.width = 'auto'
|
||||
option.cfgvalue = function(_, section_id)
|
||||
local unbanned_timestamp = map.uci:get(const.LUCI_NAME, section_id, 'ban')
|
||||
if unbanned_timestamp and os.difftime(unbanned_timestamp, os.time()) > 0 then
|
||||
---@language HTML
|
||||
return string.format([[<span style="color: red">禁封至<br/>%s</span>]], os.date('%Y-%m-%d %H:%M:%S', unbanned_timestamp))
|
||||
elseif map.uci:get_bool(const.LUCI_NAME, section_id, 'wrong_password') then
|
||||
---@language HTML
|
||||
return [[<span style="color: darkred">密码错误</span>]]
|
||||
elseif map.uci:get_bool(const.LUCI_NAME, section_id, 'arrears') then
|
||||
---@language HTML
|
||||
return [[<span style="color: darkred">欠费</span>]]
|
||||
else
|
||||
---@language HTML
|
||||
return [[<span style="color: green">正常</span>]]
|
||||
end
|
||||
end
|
||||
|
||||
-- 用户名
|
||||
option = section:option(DummyValue, 'username', '用户名')
|
||||
option.width = 'auto'
|
||||
option.rmempty = false
|
||||
|
||||
section = map:section(Table, { {} }--[[ 随便给 table 一个可索引对象,让 section 不会显示 "尚无任何配置" ]])
|
||||
|
||||
option = section:option(Button, 'commit')
|
||||
option.inputtitle = '保存更改'
|
||||
option.inputstyle = 'save'
|
||||
option.write = function()
|
||||
map.uci:commit(const.LUCI_NAME)
|
||||
end
|
||||
|
||||
return map
|
||||
@@ -0,0 +1,40 @@
|
||||
local api = require('luci.model.cbi.cqustdotnet.api.api')
|
||||
local const = require('luci.model.cbi.cqustdotnet.api.constants')
|
||||
|
||||
map = Map(const.LUCI_NAME, const.APP_NAME)
|
||||
---@language HTML
|
||||
map.description = [[一个用于连接认证重科校园网的工具,可以检测校园网的认证情况,自动进行认证。]]
|
||||
map.pageaction = false -- 不显示页面上的保存/应用按钮,由我们自定义的按钮来控制,这样保存效率更高且不会影响其它 luci app
|
||||
|
||||
map:section(SimpleSection).template = const.LUCI_NAME .. '/global/status'
|
||||
|
||||
section = map:section(NamedSection, 'config')
|
||||
section.anonymous = true
|
||||
|
||||
option = section:option(Flag, 'enabled', '总开关')
|
||||
option.rmempty = false
|
||||
|
||||
option = section:option(Value, 'network_detection_interval', '网络连通性检测间隔', '单位:秒')
|
||||
option.rmempty = false
|
||||
option.datatype = 'and(min(1), uinteger)'
|
||||
option.default = 5
|
||||
|
||||
section = map:section(Table, { {} }--[[ 随便给 table 一个可索引对象,让 section 不会显示 "尚无任何配置" ]])
|
||||
|
||||
option = section:option(Button, 'commit')
|
||||
option.inputtitle = '保存设置'
|
||||
option.inputstyle = 'save'
|
||||
option.write = function()
|
||||
map.uci:commit(const.LUCI_NAME)
|
||||
end
|
||||
|
||||
option = section:option(Button, 'apply')
|
||||
option.inputtitle = '保存&应用设置'
|
||||
option.inputstyle = 'apply'
|
||||
option.write = function()
|
||||
map.uci:commit(const.LUCI_NAME)
|
||||
api.sys.call(string.format('/etc/init.d/%s restart >/dev/null 2>&1 &', const.LUCI_NAME))
|
||||
api.http.redirect(api.url())
|
||||
end
|
||||
|
||||
return map
|
||||
@@ -0,0 +1,8 @@
|
||||
local const = require('luci.model.cbi.cqustdotnet.api.constants')
|
||||
|
||||
local form = SimpleForm(const.LUCI_NAME)
|
||||
form.reset = false
|
||||
form.submit = false
|
||||
form:append(Template(const.LUCI_NAME .. '/log/log'))
|
||||
|
||||
return form
|
||||
@@ -0,0 +1,111 @@
|
||||
<%
|
||||
local api = require('luci.model.cbi.cqustdotnet.api.api')
|
||||
%>
|
||||
|
||||
<style>
|
||||
.status-section {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-evenly;
|
||||
flex-wrap: wrap;
|
||||
padding: 8px;
|
||||
}
|
||||
|
||||
.status-card {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: flex-start;
|
||||
padding: 20px;
|
||||
margin: 8px;
|
||||
background-color: #fff;
|
||||
border-radius: 4px;
|
||||
min-width: 240px;
|
||||
}
|
||||
|
||||
.status-card > .icon {
|
||||
width: 50px;
|
||||
height: 50px;
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
.status-card > .icon path {
|
||||
fill: #666;
|
||||
}
|
||||
|
||||
.status-card > .texts {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.status-card > .texts > .title {
|
||||
font-size: 1em;
|
||||
padding: 0;
|
||||
color: #555;
|
||||
}
|
||||
|
||||
.status-card > .texts > .content {
|
||||
font-size: 1em;
|
||||
padding: 0;
|
||||
color: #777;
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: light) {
|
||||
.status-section {
|
||||
background-color: rgba(0, 0, 0, .05);
|
||||
}
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
.status-card {
|
||||
background-color: #000;
|
||||
}
|
||||
|
||||
.status-card > .icon path {
|
||||
fill: #999;
|
||||
}
|
||||
|
||||
.status-card > .texts > .title {
|
||||
color: #888;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<fieldset class="cbi-section">
|
||||
<legend>状态</legend>
|
||||
<div class="status-section">
|
||||
<div class="status-card">
|
||||
<svg class="icon" viewBox="0 0 1024 1024">
|
||||
<path d="M832 106.666667H192C145.066667 106.666667 106.666667 145.066667 106.666667 192v128h810.666666V192c0-46.933333-38.4-85.333333-85.333333-85.333333zM202.666667 256c-17.066667 0-32-14.933333-32-32S185.6 192 202.666667 192s32 14.933333 32 32-14.933333 32-32 32z m106.666666 0c-17.066667 0-32-14.933333-32-32S292.266667 192 309.333333 192s32 14.933333 32 32-14.933333 32-32 32z m106.666667 0c-17.066667 0-32-14.933333-32-32S398.933333 192 416 192s32 14.933333 32 32-14.933333 32-32 32zM106.666667 832c0 46.933333 38.4 85.333333 85.333333 85.333333h640c46.933333 0 85.333333-38.4 85.333333-85.333333V384H106.666667v448z m266.666666-64h-106.666666c-17.066667 0-32-14.933333-32-32s14.933333-32 32-32h106.666666c17.066667 0 32 14.933333 32 32s-14.933333 32-32 32z m170.666667-277.333333h213.333333c17.066667 0 32 14.933333 32 32s-14.933333 32-32 32h-213.333333c-17.066667 0-32-14.933333-32-32s14.933333-32 32-32z m0 106.666666h213.333333c17.066667 0 32 14.933333 32 32s-14.933333 32-32 32h-213.333333c-17.066667 0-32-14.933333-32-32s14.933333-32 32-32z m0 106.666667h213.333333c17.066667 0 32 14.933333 32 32s-14.933333 32-32 32h-213.333333c-17.066667 0-32-14.933333-32-32s14.933333-32 32-32z m-307.2-160c12.8-12.8 32-12.8 44.8 0l38.4 38.4 83.2-83.2c12.8-12.8 32-12.8 44.8 0 12.8 12.8 12.8 32 0 44.8l-106.666667 104.533333c-12.8 12.8-32 12.8-44.8 0l-59.733333-59.733333c-12.8-12.8-12.8-32 0-44.8z"/>
|
||||
</svg>
|
||||
<div class="texts">
|
||||
<h4 class="title">网络守护进程</h4>
|
||||
<p id="connector_status" class="content">-</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="status-card">
|
||||
<svg class="icon" viewBox="0 0 1024 1024">
|
||||
<path d="M702.1 521.7l132.1 99.2c31.1 23.4 49.6 60.4 49.6 99.3v124.2c0 45.6-37.1 82.6-82.6 82.6H222.8c-45.6 0-82.6-37.1-82.6-82.7V720.1c0-41.8 20.3-77.4 62.3-108.8l122.3-92c-39.9-53.1-60.6-111.5-60.6-174C264.1 196.8 363.8 97 512 97s247.9 99.8 247.9 248.3c0 66-19.8 125.6-58.8 177.2M553.3 636.2c0-22.9-18.5-41.4-41.3-41.4-22.8 0-41.3 18.5-41.3 41.4v165.6h82.6V636.2z m0 0"/>
|
||||
</svg>
|
||||
<div class="texts">
|
||||
<h4 class="title">当前账号</h4>
|
||||
<p id="current_account" class="content">-</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</fieldset>
|
||||
|
||||
<script type="text/javascript">//<![CDATA[
|
||||
const connector_status = document.getElementById('connector_status')
|
||||
const current_account = document.getElementById('current_account')
|
||||
|
||||
XHR.poll(5, '<%=api.url("status")%>', null, function (x, status) {
|
||||
if (!x || x.status !== 200 || !status) return
|
||||
connector_status.innerHTML = status.connector ? '<b style="color: green">运行中</b>' : '<b style="color: red">未运行</b>'
|
||||
if (status.connector)
|
||||
current_account.innerText = status.account ? status.account : '未知'
|
||||
else
|
||||
current_account.innerText = '-'
|
||||
})
|
||||
//]]></script>
|
||||
@@ -0,0 +1,38 @@
|
||||
<%
|
||||
local api = require('luci.model.cbi.cqustdotnet.api.api')
|
||||
-%>
|
||||
|
||||
<fieldset class="cbi-section">
|
||||
<input type="button" class="btn cbi-button cbi-button-remove" onclick="clearLog()" value="<%:Clear logs%>"/>
|
||||
<textarea id="log_textarea" class="cbi-input-textarea" data-update="change" readonly></textarea>
|
||||
</fieldset>
|
||||
|
||||
<script type="text/javascript">//<![CDATA[
|
||||
const logTextarea = document.getElementById('log_textarea')
|
||||
|
||||
function clearLog() {
|
||||
XHR.get('<%=api.url("clear_log")%>', null, function (x) {
|
||||
if (x && x.status === 200) {
|
||||
logTextarea.innerHTML = ''
|
||||
logTextarea.scrollTop = logTextarea.scrollHeight
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
let logCursor = null
|
||||
XHR.poll(5, '<%=api.url("fetch_log")%>', {after: logCursor}, function (http, data) {
|
||||
if (!http) return
|
||||
if (http.status === 400) {
|
||||
// TODO: 日志文件不存在
|
||||
return
|
||||
}
|
||||
if (http.status !== 200) return // 未知错误
|
||||
|
||||
if (logCursor !== data.cursor) {
|
||||
logCursor = data.cursor
|
||||
data.fragment.trim().split('\n').forEach(function (line) {
|
||||
logTextarea.innerHTML = line + '\n' + logTextarea.innerHTML
|
||||
})
|
||||
}
|
||||
})
|
||||
//]]></script>
|
||||
+95
@@ -0,0 +1,95 @@
|
||||
#!/bin/sh /etc/rc.common
|
||||
|
||||
START=99
|
||||
STOP=10
|
||||
|
||||
CONFIG=cqustdotnet
|
||||
APP_FILE=/usr/share/${CONFIG}/app.lua
|
||||
LOCK_FILE_DIR=/var/lock
|
||||
LOCK_FILE=${LOCK_FILE_DIR}/${CONFIG}.lock
|
||||
|
||||
set_lock() {
|
||||
if [ ! -d "$LOCK_FILE_DIR" ]; then
|
||||
mkdir -p "$LOCK_FILE_DIR"
|
||||
fi
|
||||
exec 789>"$LOCK_FILE"
|
||||
flock -xn 789
|
||||
}
|
||||
|
||||
unset_lock() {
|
||||
flock -u 789
|
||||
rm -rf "$LOCK_FILE"
|
||||
}
|
||||
|
||||
unlock() {
|
||||
local fail_count=1
|
||||
while [ "$fail_count" -le 10 ]; do
|
||||
if [ -f "$LOCK_FILE" ]; then
|
||||
let "fail_count++"
|
||||
sleep 1s
|
||||
if [ "$fail_count" -ge 10 ]; then
|
||||
unset_lock
|
||||
fi
|
||||
else
|
||||
break
|
||||
fi
|
||||
done
|
||||
}
|
||||
|
||||
boot() {
|
||||
$APP_FILE boot
|
||||
}
|
||||
|
||||
start() {
|
||||
set_lock
|
||||
if [ $? == 1 ]; then
|
||||
$APP_FILE log '脚本已在运行,不再重复运行,退出'
|
||||
exit 0
|
||||
fi
|
||||
$APP_FILE start
|
||||
unset_lock
|
||||
}
|
||||
|
||||
stop() {
|
||||
unlock
|
||||
set_lock
|
||||
if [ $? == 1 ]; then
|
||||
$APP_FILE log '停止脚本等待超时,不重复运行,退出'
|
||||
exit 0
|
||||
fi
|
||||
$APP_FILE stop
|
||||
unset_lock
|
||||
}
|
||||
|
||||
restart() {
|
||||
set_lock
|
||||
if [ $? == 1 ]; then
|
||||
$APP_FILE log '脚本已在运行,不重复运行,退出'
|
||||
exit 0
|
||||
fi
|
||||
$APP_FILE stop
|
||||
$APP_FILE start
|
||||
unset_lock
|
||||
}
|
||||
|
||||
disable() {
|
||||
rm -f "$IPKG_INSTROOT"/etc/rc.d/S??${CONFIG}
|
||||
rm -f "$IPKG_INSTROOT"/etc/rc.d/K??${CONFIG}
|
||||
}
|
||||
|
||||
enable() {
|
||||
err=1
|
||||
if [ "$START" ]; then
|
||||
ln -sf "../init.d/${CONFIG}" "$IPKG_INSTROOT/etc/rc.d/S${START}${CONFIG}"
|
||||
err=0
|
||||
fi
|
||||
if [ "$STOP" ]; then
|
||||
ln -sf "../init.d/${CONFIG}" "$IPKG_INSTROOT/etc/rc.d/K${STOP}${CONFIG}"
|
||||
err=0
|
||||
fi
|
||||
return $err
|
||||
}
|
||||
|
||||
enabled() {
|
||||
[ -x "$IPKG_INSTROOT/etc/rc.d/S${START}${CONFIG}" ]
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
#!/bin/sh
|
||||
[ ! -f "/usr/share/ucitrack/luci-app-cqustdotnet.json" ] && {
|
||||
cat > /usr/share/ucitrack/luci-app-cqustdotnet.json << EEOF
|
||||
{
|
||||
"config": "cqustdotnet",
|
||||
"init": "cqustdotnet"
|
||||
}
|
||||
EEOF
|
||||
}
|
||||
|
||||
uci -q batch <<-EOF >/dev/null
|
||||
delete ucitrack.@cqustdotnet[-1]
|
||||
add ucitrack cqustdotnet
|
||||
set ucitrack.@cqustdotnet[-1].init=cqustdotnet
|
||||
commit ucitrack
|
||||
EOF
|
||||
|
||||
[ ! -s '/etc/config/cqustdotnet' ] && cp -f /usr/share/cqustdotnet/0_default_config /etc/config/cqustdotnet
|
||||
rm -f /tmp/luci-indexcache
|
||||
rm -rf /tmp/luci-modulecache/
|
||||
exit 0
|
||||
@@ -0,0 +1,5 @@
|
||||
|
||||
config cqustdotnet 'config'
|
||||
option enabled '0'
|
||||
option network_detection_interval '5'
|
||||
|
||||
@@ -0,0 +1,154 @@
|
||||
#!/usr/bin/lua
|
||||
|
||||
local ucic = require('uci')
|
||||
local const = require('luci.model.cbi.cqustdotnet.api.constants')
|
||||
|
||||
local TMP_PATH = '/tmp/etc/' .. const.LUCI_NAME
|
||||
local LOG_FILE = '/var/log/' .. const.LUCI_NAME .. '.log'
|
||||
local APP_PATH = '/usr/share/' .. const.LUCI_NAME
|
||||
|
||||
local ucic_cursor = ucic.cursor
|
||||
local uci = ucic_cursor()
|
||||
|
||||
---@type file|nil
|
||||
local log_file
|
||||
local function log(...)
|
||||
-- 没有打开日志文件,尝试打开
|
||||
if not log_file then
|
||||
log_file = io.open(LOG_FILE, 'a')
|
||||
end
|
||||
|
||||
if log_file then
|
||||
log_file:write(os.date('%Y-%m-%d %H:%M:%S'), ': ')
|
||||
log_file:write(...)
|
||||
log_file:write('\n')
|
||||
log_file:flush()
|
||||
end
|
||||
end
|
||||
|
||||
local function clean_log_if_too_long()
|
||||
if log_file then
|
||||
log_file:close()
|
||||
end
|
||||
|
||||
local writable_log_file = io.open(LOG_FILE, 'rw')
|
||||
if writable_log_file then
|
||||
local lines_count = 0
|
||||
for _ in writable_log_file:lines() do
|
||||
lines_count = lines_count + 1
|
||||
|
||||
-- 日志大于 1000 行,清空日志
|
||||
if lines_count > 1000 then
|
||||
writable_log_file:write('日志文件过长,清空')
|
||||
break
|
||||
end
|
||||
end
|
||||
writable_log_file:close()
|
||||
end
|
||||
end
|
||||
|
||||
---
|
||||
--- 将 table 转换为 vararg(也就是 ...)。
|
||||
---@overload fun(tab:table):any
|
||||
local function unpack(tab, index)
|
||||
index = index or 1
|
||||
if tab[index] then
|
||||
return tab[index], unpack(tab, index + 1)
|
||||
end
|
||||
end
|
||||
|
||||
---@type string|nil
|
||||
local app_enabled
|
||||
|
||||
---
|
||||
--- 获取当前 LuCI App 启用状态,也就是总开关是否打开。
|
||||
---
|
||||
--- 参数 reload 默认为 false,当 reload 为 true 时,会重新获取状态,
|
||||
--- 否则只会获取一次状态,其后的调用都会直接返回已经获取到的状态。
|
||||
---@overload fun(): boolean
|
||||
---@param reload boolean
|
||||
---@return boolean
|
||||
local function is_app_enabled(reload)
|
||||
if reload or app_enabled == nil then
|
||||
app_enabled = uci:get(const.LUCI_NAME, 'config', 'enabled') or '0'
|
||||
end
|
||||
return app_enabled == '1'
|
||||
end
|
||||
|
||||
---
|
||||
--- 清除与当前 LuCI App 相关的定时任务。
|
||||
local function clean_crontab()
|
||||
local crontab_file = io.open('/etc/crontabs/root', 'rw')
|
||||
local crontab = {}
|
||||
for cron in crontab_file:lines('*L') do
|
||||
if not cron:find(const.LUCI_NAME) then
|
||||
table.insert(crontab, cron)
|
||||
end
|
||||
end
|
||||
crontab_file:write(table.concat(crontab))
|
||||
crontab_file:close()
|
||||
end
|
||||
|
||||
local function start_crontab()
|
||||
-- TODO: 当前没有定时任务
|
||||
--clean_crontab()
|
||||
if not is_app_enabled() then
|
||||
-- TODO: 当前没有定时任务
|
||||
--os.execute('/etc/init.d/cron restart')
|
||||
return
|
||||
end
|
||||
|
||||
os.execute(APP_PATH .. '/connector.lua >/dev/null 2>&1 &')
|
||||
|
||||
-- TODO: 当前没有定时任务
|
||||
--os.execute('/etc/init.d/cron restart')
|
||||
end
|
||||
|
||||
local function stop_crontab()
|
||||
clean_crontab()
|
||||
os.execute('/etc/init.d/cron restart')
|
||||
end
|
||||
|
||||
local function start()
|
||||
start_crontab()
|
||||
log('进程启动完毕')
|
||||
end
|
||||
|
||||
local function boot()
|
||||
if is_app_enabled() then
|
||||
start()
|
||||
end
|
||||
end
|
||||
|
||||
local function stop()
|
||||
clean_log_if_too_long()
|
||||
|
||||
---@language Shell Script
|
||||
os.execute(string.format("pgrep -af '%s/' | awk '! /app\\.lua/{print $1}' | xargs kill -9 >/dev/null 2>&1", const.LUCI_NAME))
|
||||
stop_crontab()
|
||||
os.remove('/tmp/lock/' .. const.LUCI_NAME .. '_connector.lock')
|
||||
log('清空并关闭相关程序和缓存完成')
|
||||
|
||||
os.exit(0)
|
||||
end
|
||||
|
||||
-- 没有运行参数,退出
|
||||
if type(arg) ~= "table" then
|
||||
return
|
||||
end
|
||||
|
||||
local action = arg[1]
|
||||
if action == 'start' then
|
||||
start()
|
||||
elseif action == 'stop' then
|
||||
stop()
|
||||
elseif action == 'boot' then
|
||||
boot()
|
||||
elseif action == 'log' then
|
||||
-- 删掉第一个参数,也就是 action
|
||||
local args = {}
|
||||
for i = 2, #arg do
|
||||
table.insert(args, arg[i])
|
||||
end
|
||||
log(unpack(args))
|
||||
end
|
||||
@@ -0,0 +1,275 @@
|
||||
#!/usr/bin/lua
|
||||
|
||||
local nixio = require('nixio')
|
||||
local http = require('luci.http')
|
||||
local json = require('luci.jsonc')
|
||||
local api = require('luci.model.cbi.cqustdotnet.api.api')
|
||||
local const = require('luci.model.cbi.cqustdotnet.api.constants')
|
||||
local accounts = require('luci.model.cbi.cqustdotnet.api.accounts')
|
||||
|
||||
local uci = api.uci
|
||||
|
||||
local LOCK_FILE = '/tmp/lock/' .. const.LUCI_NAME .. '_connector.lock'
|
||||
|
||||
local function is_file_exists(filename)
|
||||
return nixio.fs.stat(filename, 'type') == 'reg'
|
||||
end
|
||||
|
||||
---
|
||||
--- 尝试访问认证重定向主机,返回成功与否。
|
||||
---
|
||||
--- 参数 max_retry 为最大重试次数,不传默认为 0 不重试。
|
||||
---
|
||||
--- 一般情况下,不能访问认证重定向主机说明认证已经成功,少数情况是校园网故障,
|
||||
--- 无需访问互联网判断。
|
||||
---@overload fun():boolean
|
||||
---@param max_retry number
|
||||
---@return boolean
|
||||
local function can_access_auth(max_retry)
|
||||
local max_try = (max_retry or 0) + 1 -- 默认不重试
|
||||
|
||||
local request = nixio.socket('inet', 'stream')
|
||||
request:setopt('socket', 'sndtimeo', 1)
|
||||
|
||||
for _ = 1, max_try do
|
||||
if request:connect('123.123.123.123', 80) then
|
||||
request:close()
|
||||
return true
|
||||
end
|
||||
request:shutdown()
|
||||
end
|
||||
request:close()
|
||||
return false
|
||||
end
|
||||
|
||||
local redirect_request_http_headers
|
||||
local function get_redirect_request_http_headers()
|
||||
if not redirect_request_http_headers then
|
||||
redirect_request_http_headers = table.concat({
|
||||
'GET / HTTP/1.1',
|
||||
'Host: 123.123.123.123',
|
||||
'User-Agent: Mozilla/5.0 (iPhone; CPU iPhone OS 13_2_3 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/13.0.3 Mobile/15E148 Safari/604.1',
|
||||
'Accept: */*',
|
||||
'Accept-Language: zh-CN,zh;q=0.8',
|
||||
'Accept-Encoding: gzip, deflate',
|
||||
'Connection: keep-alive',
|
||||
'Upgrade-Insecure-Requests: 1'
|
||||
}, '\r\n')
|
||||
end
|
||||
return redirect_request_http_headers
|
||||
end
|
||||
|
||||
---
|
||||
--- 获取重定向时给定的参数,验证登录时要用。
|
||||
---
|
||||
--- 成功返回验证登录主机地址和参数,参数是一个 x-www-form-urlencoded 格式的字符串。失败返回 nil。
|
||||
---@overload fun():string
|
||||
---@return string,string | nil
|
||||
local function get_auth_query_params(max_retry)
|
||||
local max_try = (max_retry or 0) + 1 -- 默认不重试
|
||||
for attempt = 1, max_try do
|
||||
local request = nixio.socket('inet', 'stream')
|
||||
request:setopt('socket', 'sndtimeo', 1) -- 发送 1 秒超时
|
||||
if not request:connect('123.123.123.123', 80) then
|
||||
request:close()
|
||||
api.log('认证参数获取:无法连接到认证服务器,第 ', attempt, '/', max_try, ' 次尝试')
|
||||
else
|
||||
request:setopt('socket', 'rcvtimeo', 1) -- 接收 1 秒超时
|
||||
request:send(get_redirect_request_http_headers())
|
||||
---@type string|nil
|
||||
local response = request:recv(1024)
|
||||
request:close()
|
||||
if not response or #response == 0 then
|
||||
api.log('认证参数获取:无法从认证服务器接收响应,第 ', attempt, '/', max_try, ' 次尝试')
|
||||
else
|
||||
local auth_host = response:match('://(.-)/')
|
||||
local query_params = response:match("%?(.+)'<")
|
||||
if not auth_host or not query_params then
|
||||
api.log('认证参数获取:无法从响应中获取认证参数,第 ', attempt, '/', max_try, ' 次尝试')
|
||||
else
|
||||
return auth_host, query_params
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
local function get_auth_request_headers(host, body_length)
|
||||
return table.concat({
|
||||
'POST /eportal/InterFace.do?method=login HTTP/1.1',
|
||||
'Host: ' .. host,
|
||||
'User-Agent: Mozilla/5.0 (iPhone; CPU iPhone OS 13_2_3 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/13.0.3 Mobile/15E148 Safari/604.1',
|
||||
'Accept: */*',
|
||||
'Accept-Language: zh-CN,zh;q=0.8',
|
||||
'Accept-Encoding: gzip, deflate',
|
||||
'Content-Type: application/x-www-form-urlencoded',
|
||||
'Content-Length: ' .. body_length,
|
||||
'Origin: http://' .. host,
|
||||
'Connection: keep-alive'
|
||||
}, '\r\n')
|
||||
end
|
||||
|
||||
local function get_auth_request_body(username, password, query_params)
|
||||
return string.format('userId=%s&password=%s&service=&queryString=%s&operatorPwd=&operatorUserId=&validcode=&passwordEncrypt=false',
|
||||
username, password, http.urlencode(http.urlencode(query_params)))
|
||||
end
|
||||
|
||||
---
|
||||
--- 尝试登录,返回成功与否。
|
||||
---
|
||||
--- 该函数还负责记录失败时的信息到账号内,比如账号被禁封的话,禁封到何时。
|
||||
---@param account Account
|
||||
---@return boolean
|
||||
local function try_auth(account)
|
||||
if not account then
|
||||
return false
|
||||
end
|
||||
|
||||
local auth_host, query_params = get_auth_query_params()
|
||||
if not auth_host or not query_params then
|
||||
api.log('认证:无法获取认证参数')
|
||||
return false
|
||||
end
|
||||
|
||||
local auth_request_body = get_auth_request_body(account['username'], account['password'], query_params)
|
||||
local auth_request_headers = get_auth_request_headers(auth_host, #auth_request_body)
|
||||
local auth_request_content = auth_request_headers .. '\r\n\r\n' .. auth_request_body
|
||||
local request = nixio.socket('inet', 'stream')
|
||||
request:setopt('socket', 'sndtimeo', 1) -- 发送 1 秒超时
|
||||
if not request:connect(auth_host:match('^[^:]+'), auth_host:match(':([%d]+)$') or 80) then
|
||||
request:close()
|
||||
api.log('认证:无法连接到认证服务器')
|
||||
return false
|
||||
end
|
||||
request:setopt('socket', 'rcvtimeo', 1) -- 接收 1 秒超时
|
||||
request:send(auth_request_content)
|
||||
local response = request:recv(1024) ---@type string
|
||||
request:close()
|
||||
if not response or #response == 0 then
|
||||
api.log('认证:无法从认证服务器接收响应')
|
||||
return false
|
||||
end
|
||||
|
||||
local json_str = response:match('%b{}')
|
||||
if not json_str or #json_str <= 2 then
|
||||
local server_msg = response:match('Server:(.+)$')
|
||||
api.log('认证:无法从响应中获取有效信息,服务器响应:', server_msg)
|
||||
return false
|
||||
end
|
||||
|
||||
local res, err = json.parse(json_str)
|
||||
if not res then
|
||||
api.log('认证:无法解析响应中的有效信息(', err, '):', json_str)
|
||||
return false
|
||||
end
|
||||
|
||||
-- 认证失败
|
||||
if res['result'] ~= 'success' then
|
||||
api.log('认证:失败,原因:', res['message'])
|
||||
|
||||
local unparsed_response = true
|
||||
|
||||
-- 检查是否被禁封
|
||||
local year, month, day, hour, minute, second = res['message']:match('([1-2]%d%d%d)%-([0-1]?%d)%-([0-3]?%d)%s(%d+):([0-5]%d):([0-5]%d)')
|
||||
if year then
|
||||
unparsed_response = false
|
||||
local unbanned_timestamp = os.time({ year = year, month = month, day = day, hour = hour, min = minute, sec = second })
|
||||
api.log('账号 ', account['username'], ' (', account['remark'], ') 被禁封至 ', year, '-', month, '-', day, ' ', hour, ':', minute, ':', second, ' (', unbanned_timestamp, ')')
|
||||
uci:set(const.LUCI_NAME, account['.name'], 'ban', unbanned_timestamp)
|
||||
end
|
||||
|
||||
-- 检查密码是否错误
|
||||
if res['message']:find('密码', 1, true) then
|
||||
unparsed_response = false
|
||||
api.log('账号 ', account['username'], ' (', account['remark'], ') 密码错误')
|
||||
uci:set(const.LUCI_NAME, account['.name'], 'wrong_password', 1)
|
||||
end
|
||||
|
||||
-- 检查账号是否欠费
|
||||
if res['message']:find('The subscriber status is incorrect', 1, true) then
|
||||
unparsed_response = false
|
||||
api.log('账号 ', account['username'], ' (', account['remark'], ') 已欠费')
|
||||
uci:set(const.LUCI_NAME, account['.name'], 'arrears', 1)
|
||||
end
|
||||
|
||||
-- TODO: 认证响应适配
|
||||
if unparsed_response then
|
||||
local server_msg = response:match('Server:(.+)$')
|
||||
api.log('意料之外的认证响应:', api.trim_string(server_msg))
|
||||
else
|
||||
uci:commit(const.LUCI_NAME)
|
||||
end
|
||||
return false
|
||||
end
|
||||
|
||||
return true
|
||||
end
|
||||
|
||||
local function test_and_auto_switch()
|
||||
-- 无法访问认证重定向地址,说明已经认证成功,也可能是校园网故障
|
||||
if not can_access_auth() then
|
||||
return
|
||||
end
|
||||
|
||||
-- 尝试一次登录当前账号,如果禁封账号需要登录来触发计时
|
||||
local current_account = accounts.current() ---@type Account
|
||||
if current_account then
|
||||
if try_auth(current_account) then
|
||||
api.log('自动认证:重新使用账号 ', current_account['username'], ' (', current_account['remark'], ') 认证')
|
||||
return
|
||||
end
|
||||
end
|
||||
|
||||
local new_account = accounts.get_first_available(current_account and current_account['.name'] or nil)
|
||||
if try_auth(new_account) then
|
||||
api.log('自动认证:切换到账号 ', new_account['username'], ' (', new_account['remark'], ')')
|
||||
uci:set(const.LUCI_NAME, 'config', 'current_account', new_account['.name'])
|
||||
uci:commit(const.LUCI_NAME)
|
||||
return
|
||||
end
|
||||
|
||||
-- 自动切换账号失败,把当前账号置空,避免反复尝试登录当前账号
|
||||
if current_account then
|
||||
uci:delete(const.LUCI_NAME, 'config', 'current_account')
|
||||
uci:commit(const.LUCI_NAME)
|
||||
end
|
||||
end
|
||||
|
||||
local function start()
|
||||
if is_file_exists(LOCK_FILE) then
|
||||
api.log('守护进程已经在运行,不重复运行')
|
||||
return
|
||||
end
|
||||
os.execute('touch ' .. LOCK_FILE)
|
||||
|
||||
local enabled = uci:get(const.LUCI_NAME, 'config', 'enabled')
|
||||
if enabled ~= '1' then
|
||||
return
|
||||
end
|
||||
|
||||
-- 检查间隔,一次运行只获取一次,该值变更后需要重新运行该脚本
|
||||
local interval = uci:get(const.LUCI_NAME, 'config', 'network_detection_interval') or 5
|
||||
|
||||
api.log('守护进程启动,网络检测间隔 ', interval, ' 秒')
|
||||
|
||||
while true do
|
||||
test_and_auto_switch()
|
||||
nixio.nanosleep(interval)
|
||||
end
|
||||
end
|
||||
|
||||
if not arg or #arg < 1 or not arg[1] then
|
||||
start()
|
||||
elseif arg[1] == 'get_available_account' then
|
||||
---@type Account
|
||||
local account = accounts.get_first_available(arg[2], arg[3])
|
||||
if account then
|
||||
print(account['.name'])
|
||||
else
|
||||
print()
|
||||
end
|
||||
elseif arg[1] == 'test' then
|
||||
print(test_and_auto_switch())
|
||||
else
|
||||
start()
|
||||
end
|
||||
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"luci-app-cqustdotnet": {
|
||||
"description": "Grant UCI access for luci-app-cqustdotnet",
|
||||
"read": {
|
||||
"uci": [ "cqustdotnet" ]
|
||||
},
|
||||
"write": {
|
||||
"uci": [ "cqustdotnet" ]
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user