网上讲Luci的资料不是很多,搜集整理了一下其基本运行流程。
1.总述
1.request(get/post):浏览器发起方向请求。
2. fork 一个子进程: uhttpd fork出一个子进程。
3.子进程利用execl替换为luci进程空间
3.1 服务器利用setenv(传递一些固定格式的数据(如PATH_INFO)给luci)。
3.2 服务器通过 w_pipe(post-data)向luci的stdin写数据。
4.服务器通过 r_pipe读取luci发向其的stdout的数据。
5.服务器将生成的页面数据,返回给client。
Client端和serv端采用cgi方式交互,uhttpd服务器的cgi方式中,fork出一个子进程,子进程利用execl替换为luci进程空间,并通过setenv环境变量的方式,传递一些固定格式的数据(如PATH_INFO)给luci。另外一些非固定格式的数据(post-data)则由父进程通过一个w_pipe写给luci的stdin,而luci的返回数据则写在stdout上,由父进程通过一个r_pipe读取。
下面的图描述了web配置时的数据交互:
-
首次运行时,是以普通的file方式获得docroot/index.html,该文件中以meta的方式自动跳转到cgi的url,这是web服务器的一般做法。
-
然后第一次执行luci,path_info='/',会alise到'/admin'('/'会索引到 tree.rootnode,并执行其target方法,即alise('/admin'),即重新去索引adminnode,这在后面会详细描述),该节点需要认证,所以返回一个登录界面。
-
第3次交互,过程同上一次的,只是这时已post来了登录信息,所以serv端会生成一个session值,然后执行'/admin'的target(它的target为firstchild,即索引第一个子节点),最终返回/admin/status.html,同时会把session值以cookie的形式发给client。这就是从原始状态到得到显示页面的过程,之后主要就是点击页面上的连接,产生新的request。
-
每个链接的url中都会带有一个stok值(它是serv生成的,并放在html中的url里),并且每个新request都要带有session值,它和stok值一起供serv端联合认证
2.luci程序流程
2.1 lua语言中的主协进程执行方式
1. 主进程create出创建出一个协同程序x。
2. 主程序通过coroutine.resume(x, r),运行上面创建的协同程序x。
3. 协同程序x执行。
4.协同程序x通过coroutine.yield()来向主进程返回运行结果。
依此反复。
2.2 具体跳转流程
1.针对client的请求,先跳转到/www/index.html 对应的源文件为\feeds\luci\modules\luci-base\root\www\index.html
<?xml version="1.0" encoding="utf-8"?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN" "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<meta http-equiv="Cache-Control" content="no-cache" />
<meta http-equiv="refresh" content="0; URL=/cgi-bin/luci" />
</head>
<body style="background-color: white">
<a style="color: black; font-family: arial, helvetica, sans-serif;" href="/cgi-bin/luci">loading......</a>
</body>
</html>
2. 从/www/index.html跳转到/www/cgi-bin/luci对应的源码为\feeds\luci\modules\luci-base\htdocs\cgi-bin\luci
#!/usr/bin/lua
require "luci.cacheloader"
require "luci.sgi.cgi"
luci.dispatcher.indexcache = "/tmp/luci-indexcache" --缓存文件位置“/tmp/luci-indexcache”
luci.sgi.cgi.run() --cgi程序接下来执行程序,Luci的默认路径是/usr/lib/lua/luci,所以luci.sgi.cgi.run()是运行/usr/lib/lua/luci/sgi/cgi.lua文件中的run函数。
3. 运行 /usr/lib/lua/luci/sgi/cgi.lua 里的run函数。对应的源码路径为\feeds\luci\modules\luci-base\luasrc\sgi\cgi.lua
function run()
local r = luci.http.Request(
luci.sys.getenv(), --获取环境变量
limitsource(io.stdin, tonumber(luci.sys.getenv("CONTENT_LENGTH"))),--读取Post数据
ltn12.sink.file(io.stderr)
) --把web请求放于r中(包括环境变量,web请求,出错处理接口)
local x = coroutine.create(luci.dispatcher.httpdispatch) --创建协进程x
local hcache = ""
local active = true
while coroutine.status(x) ~= "dead" do
local res, id, data1, data2 = coroutine.resume(x, r) ----运行上面创建的协同程序,即运行httpdispatch,参数为上面local r里的变量。
if not res then
print("Status: 500 Internal Server Error")
print("Content-Type: text/plain\n")
print(id)
break;
end
if active then --根据返回结果,向客户端写数据。
if id == 1 then
io.write("Status: " .. tostring(data1) .. " " .. data2 .. "\r\n")
elseif id == 2 then --准备header
hcache = hcache .. data1 .. ": " .. data2 .. "\r\n"
elseif id == 3 then --写header、blank
io.write(hcache) --默认到stdout
io.write("\r\n")
elseif id == 4 then --写body
io.write(tostring(data1 or ""))
elseif id == 5 then --EOF
io.flush()
io.close()
active = false
elseif id == 6 then
data1:copyz(nixio.stdout, data2)
data1:close()
end
end
end
end
4.1.跳转到./usr/lib/lua/luci/dispatcher.lua 调用 httpdispatch 对应源码文件\feeds\luci\modules\luci-base\luasrc\dispatcher.lua
--httpdispatch第一个参数就是coroutine.resume(x,r)传过来的请求r,
--prefix为空。httpdispatch的主要功能是从环境变量PATH_INFO获取请求路径,像字串
--http://192.168.1.1/cgi-bin/luci/;stok=e10fa5c70fbb55d478eb8b8a2eaabc6f/admin/network/firewall/
--并把这个字符串解析成单个字符存放在table r{}中,
--最后再调用dispatch()这个函数,解析完后,关闭http连接。
function httpdispatch(request, prefix)
http.context.request = request
local r = {}
context.request = r
context.urltoken = {}
local pathinfo = http.urldecode(request:getenv("PATH_INFO") or "", true)
if prefix then
for _, node in ipairs(prefix) do
r[#r+1] = node
end
end
local tokensok = true
for node in pathinfo:gmatch("[^/]+") do
local tkey, tval
if tokensok then
tkey, tval = node:match(";(%w+)=([a-fA-F0-9]*)")
end
if tkey then
context.urltoken[tkey] = tval
else
tokensok = false
r[#r+1] = node
end
end
local stat, err = util.coxpcall(function()
dispatch(context.request)
end, error500)
http.close()
--context._disable_memtrace()
end
4.2 调用 dispatch,此函数是luci主处理函数,是整个LuCI中的核心。对应源码文件\feeds\luci\modules\luci-base\luasrc\dispatcher.lua
function dispatch(request)
--context._disable_memtrace = require "luci.debug".trap_memtrace("l")
local ctx = context
ctx.path = request
local conf = require "luci.config"
assert(conf.main,
"/etc/config/luci seems to be corrupt, unable to find section 'main'")
local lang = conf.main.lang or "auto"
if lang == "auto" then
local aclang = http.getenv("HTTP_ACCEPT_LANGUAGE") or ""
for lpat in aclang:gmatch("[%w-]+") do
lpat = lpat and lpat:gsub("-", "_")
if conf.languages[lpat] then
lang = lpat
break
end
end
end
require "luci.i18n".setlanguage(lang)
local c = ctx.tree
local stat
if not c then
c = createtree() --1. 节点树node-tree创立
end
local track = {}
local args = {}
ctx.args = args
ctx.requestargs = ctx.requestargs or args
local n
local token = ctx.urltoken
local preq = {}
local freq = {}
for i, s in ipairs(request) do
preq[#preq+1] = s
freq[#freq+1] = s
c = c.nodes[s]
n = i
if not c then
break
end
util.update(track, c)
if c.leaf then
break
end
end
if c and c.leaf then
for j=n+1, #request do
args[#args+1] = request[j]
freq[#freq+1] = request[j]
end
end
ctx.requestpath = ctx.requestpath or freq
ctx.path = preq
if track.i18n then
i18n.loadc(track.i18n)
end
-- Init template engine
if (c and c.index) or not track.notemplate then --需要显示的部分,通过MVC模板自动生成显示页面
local tpl = require("luci.template")
local media = track.mediaurlbase or luci.config.main.mediaurlbase
if not pcall(tpl.Template, "themes/%s/header" % fs.basename(media)) then
media = nil
for name, theme in pairs(luci.config.themes) do
if name:sub(1,1) ~= "." and pcall(tpl.Template,
"themes/%s/header" % fs.basename(theme)) then
media = theme
end
end
assert(media, "No valid theme found")
end
local function _ifattr(cond, key, val)
if cond then
local env = getfenv(3)
local scope = (type(env.self) == "table") and env.self
return string.format(
' %s="%s"', tostring(key),
util.pcdata(tostring( val
or (type(env[key]) ~= "function" and env[key])
or (scope and type(scope[key]) ~= "function" and scope[key])
or "" ))
)
else
return ''
end
end
tpl.context.viewns = setmetatable({
write = http.write;
include = function(name) tpl.Template(name):render(getfenv(2)) end;
translate = i18n.translate;
translatef = i18n.translatef;
export = function(k, v) if tpl.context.viewns[k] == nil then tpl.context.viewns[k] = v end end;
striptags = util.striptags;
pcdata = util.pcdata;
media = media;
theme = fs.basename(media);
resource = luci.config.main.resourcebase;
ifattr = function(...) return _ifattr(...) end;
attr = function(...) return _ifattr(true, ...) end;
}, {__index=function(table, key)
if key == "controller" then
return build_url()
elseif key == "REQUEST_URI" then
return build_url(unpack(ctx.requestpath))
else
return rawget(table, key) or _G[key]
end
end})
end
track.dependent = (track.dependent ~= false)
assert(not track.dependent or not track.auto,
"Access Violation\nThe page at '" .. table.concat(request, "/") .. "/' " ..
"has no parent node so the access to this location has been denied.\n" ..
"This is a software bug, please report this message at " ..
"https://github.com/openwrt/luci/issues"
)
if track.sysauth then --认证部分
local authen = type(track.sysauth_authenticator) == "function"
and track.sysauth_authenticator
or authenticator[track.sysauth_authenticator]
local def = (type(track.sysauth) == "string") and track.sysauth
local accs = def and {track.sysauth} or track.sysauth
local sess = ctx.authsession
local verifytoken = false
if not sess then
sess = http.getcookie("sysauth")
sess = sess and sess:match("^[a-f0-9]*$")
verifytoken = true
end
local sdat = (util.ubus("session", "get", { ubus_rpc_session = sess }) or { }).values
local user
if sdat then
if not verifytoken or ctx.urltoken.stok == sdat.token then
user = sdat.user
end
else
local eu = http.getenv("HTTP_AUTH_USER")
local ep = http.getenv("HTTP_AUTH_PASS")
if eu and ep and sys.user.checkpasswd(eu, ep) then
authen = function() return eu end
end
end
if not util.contains(accs, user) then
if authen then
local user, sess = authen(sys.user.checkpasswd, accs, def)
local token
if not user or not util.contains(accs, user) then
return
else
if not sess then
local sdat = util.ubus("session", "create", { timeout = tonumber(luci.config.sauth.sessiontime) })
if sdat then
token = sys.uniqueid(16)
util.ubus("session", "set", {
ubus_rpc_session = sdat.ubus_rpc_session,
values = {
user = user,
token = token,
section = sys.uniqueid(16)
}
})
sess = sdat.ubus_rpc_session
end
end
if sess and token then
http.header("Set-Cookie", 'sysauth=%s; path=%s/' %{
sess, build_url()
})
ctx.urltoken.stok = token
ctx.authsession = sess
ctx.authuser = user
http.redirect(build_url(unpack(ctx.requestpath)))
end
end
else
http.status(403, "Forbidden")
return
end
else
ctx.authsession = sess
ctx.authuser = user
end
end
if track.setgroup then
sys.process.setgroup(track.setgroup)
end
if track.setuser then
-- trigger ubus connection before dropping root privs
util.ubus()
sys.process.setuser(track.setuser)
end
local target = nil
if c then
if type(c.target) == "function" then
target = c.target
elseif type(c.target) == "table" then
target = c.target.target
end
end
if c and (c.index or type(target) == "function") then
ctx.dispatched = c
ctx.requested = ctx.requested or ctx.dispatched
end
if c and c.index then
local tpl = require "luci.template"
if util.copcall(tpl.render, "indexer", {}) then
return true
end
end
if type(target) == "function" then --显示/处理
util.copcall(function()
local oldenv = getfenv(target)
local module = require(c.module)
local env = setmetatable({}, {__index=
function(tbl, key)
return rawget(tbl, key) or module[key] or oldenv[key]
end})
setfenv(target, env)
end)
local ok, err
if type(c.target) == "table" then
ok, err = util.copcall(target, c.target, unpack(args))
else
ok, err = util.copcall(target, unpack(args))
end
assert(ok,
"Failed to execute " .. (type(c.target) == "function" and "function" or c.target.type or "unknown") ..
" dispatcher target for entry '/" .. table.concat(request, "/") .. "'.\n" ..
"The called action terminated with an exception:\n" .. tostring(err or "(unknown)"))
else
local root = node()
if not root or not root.target then
error404("No root node was registered, this usually happens if no module was installed.\n" ..
"Install luci-mod-admin-full and retry. " ..
"If the module is already installed, try removing the /tmp/luci-indexcache file.")
else
error404("No page is registered at '/" .. table.concat(request, "/") .. "'.\n" ..
"If this url belongs to an extension, make sure it is properly installed.\n" ..
"If the extension was recently installed, try removing the /tmp/luci-indexcache file.")
end
end
end
2.3 以具体请求页面为例:
http://192.168.1.1/cgi-bin/luci/;stok=4b77c83a89c7b9cd8f4dcc0fcbc28024/admin/network/
调用顺序:
1、ok, err = util.copcall(target, unpack(args)) -- dispatcher.lua中的 dispatch函数
2、page.target = firstchild() -- ./usr/lib/lua/luci/controller/admin/network.lua index()
3、firstchild() -- dispatcher.lua中的 firstchild函数
4、_firstchild -- dispatcher.lua中的 _firstchild函数 -- 自动链接到它的第一个子节点
5、在network.lua中定义order,Interfaces是10,为第一个子节点:
page = entry({"admin", "network", "network"}, arcombine(cbi("admin_network/network"), cbi("admin_network/ifaces")), _("Interfaces"), 10)
--通过cbi方法处理admin_network/ifaces.lua和admin_network/network.lua,生成html文件
3.点树node-tree
在controller目录下,每个.lua文件中,都有一个index()函数,其中主要调用entry()函数,形如entry(path,target,title,order),path形如{admin,network,wireless},entry()函数根据这些创建一个node,并把它放在全局node-tree的相应位置,后面的参数都是该node的属性,还可以有其他的参数。其中最重要的就是target。
dispatch中的 createtree()函数就是要找到./usr/lib/lua/luci/controlle目录下所有的.lua文件,并找到其中的index()函数执行,从而生成一个node-tree。这样做的io操作太多,为了效率,第一次执行后,把生成的node-tree放在/tmp/luci-indexcache文件中,以后只要没有更新(一般情况下,服务器里的.lua文件是不会变的),直接读该文件即可。所以调试里,要记得清空tmp下的缓存,所修改的lua文件才会生效。rm -rf /tmp/luci-*
生成的node-tree如下:
这里要注意的是,每次dispatch()会根据path_info逐层索引,且每一层都把找到的节点信息放在一个变量track中,这样做使得上层node的信息会影响下层node,而下层node的信息又会覆盖上层node。比如{/admin/system},最后的auto=false,target=aa,而由于admin有sysauth值,它会遗传给它的子节点,也即所有admin下的节点都需要认证。
4. target简介
对每个节点,最重要的属性当然是target,这也是dispatch()流程最后要执行的方法。target主要有:alise、firstchild、call、cbi、form、template。这几个总体上可以分成两类,前两种主要用于链接其它node,后一个则是主要的操作、以及页面生成。下面分别描述。
链接方法(alise、firstchild):在介绍初始登录流程时,已经讲到了这种方法。比如初始登录时,url中的path_info仅为'/',这应该会索引到根node节点。而该节点本身是没有内容显示的,所以它用alias('admin')方法,自动链接到admin节点。再比如,admin节点本身也没有内容显示,它用firstchild()方法,自动链接到它的第一个子节点/admin/status。
操作方法(call、cbi、form、template):这种方法一般用于一个路径的叶节点leaf,它们会去执行相应的操作,如修改interface参数等,并且动态生成页面html文件,传递给client。这里实际上是利用了所谓的MVC架构,这在后面再描述,这里主要描述luci怎么把生成的html发送给client端。
call、cbi、form、template这几种方法,执行的原理各不相同,但最终都会生成完整的http-response报文(包括html文件),并调用luci.template.render(),luci.http.redirect()等函数,它们会调用几个特殊的函数,把报文内容返回给luci.running()流程。
如上图所示,再联系luci.running()流程,就很容易看出,生成的完整的http-response报文会通过io.write()写在stdout上,而uhttpd架构已决定了,这些数据将传递给父进程,并通过tcp连接返回给client端。
5.MVC界面生成
/usr/lib/lua/luci/下有三个目录model、view、controller
1、call()方法会调用controller里的函数,主要通过openwrt系统的uci、network、inconfig等工具对系统进行设置,如果需要还会生成新界面。
2、动态生成界面的方法有两种,
1)、通过cbi()/form()方法,它们利用model中定义的模板map,生成html文件;
2)、通过template()方法,利用view中定义的htm(一种类似html的文件),直接生成界面
上面的标题是由node-tree生成的,下面的内容由每个node通过上面的方法来动态生成。
6.处理
服务器处理过程和页面生成基本类似,也调用到/usr/lib/lua/luci/dispatcher.lua 并走到显示/处理部分,后继处理如下:
ok, err = util.copcall(target, c.target, unpack(args)) (target在luci/controller/firewall中被赋值为arcombine(cbi("firewall/zones"), cbi("firewall/zone-details")),即两个cbi函数的集合)
function cbi(model, config)
local function _cbi(self, ...)
local cstate = res:parse()
function Map.parse(self, readinput, ...)
Node.parse(self, ...)
Node.parse会调用Map中的每一个子元素自身的处理
EX:
如调用Flag的处理:function Flag.parse(self, section),他会通过遍历处理from传下来的每一个Flag,并通过本身的write/remove来启用和禁用这个选项。
当form保存下来cbid.firewall.cfg02e63d.syn_flood这个Network/Firewall/General Setting下的Flag标签的值时,处理函数就会调用Flag.parse处理:调用self:formvalue来匹配标签值,然后调用model/cbi/firewall/zones.lua的write或者remove来禁用或者启用这个选项所控制的开关。
由于Flag = class(AbstractValue),继承于AbstractValue类,所以其write/remove是调用的AbstractValue类的write/remove方法。
AbstractValue.write调用self.map:set即function Map.set(self, section, option, value),Map.set 再调用self.uci:set(self.config, section, option, value)来设置对应config文件,然后Map.parse 会调用self.uci:commit(config)对已修改的config逐一提交。
生效的两种方式
1、在model>cbi>lua文件中加入
local apply = luci.http.formvalue("cbi.apply")
if apply then
luci.sys.exec("logger -t saved&apply pressed") --在这个加入自己的处理代码
end
2、在对应的.lua文件中写m.on_apply来启动或者处理方式。
至此,luci的运行流程基本搞懂,进阶应用还需研究
主要引用内容来自:
https://www.cnblogs.com/zmkeil/archive/2013/05/14/3078774.html
https://www.cnblogs.com/x_wukong/p/4515357.html