900字范文,内容丰富有趣,生活中的好帮手!
900字范文 > Openwrt Web gui LUCI 流程浅析

Openwrt Web gui LUCI 流程浅析

时间:2020-04-03 15:56:06

相关推荐

Openwrt Web gui LUCI 流程浅析

网上讲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" "/TR/xhtml11/DTD/xhtml11.dtd"><html xmlns="/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/luarequire "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.luafunction 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) --创建协进程xlocal hcache = ""local active = truewhile coroutine.status(x) ~= "dead" dolocal res, id, data1, data2 = coroutine.resume(x, r) ----运行上面创建的协同程序,即运行httpdispatch,参数为上面local r里的变量。if not res thenprint("Status: 500 Internal Server Error")print("Content-Type: text/plain\n")print(id)break;endif active then--根据返回结果,向客户端写数据。if id == 1 thenio.write("Status: " .. tostring(data1) .. " " .. data2 .. "\r\n")elseif id == 2 then --准备headerhcache = hcache .. data1 .. ": " .. data2 .. "\r\n"elseif id == 3 then --写header、blankio.write(hcache) --默认到stdoutio.write("\r\n")elseif id == 4 then --写bodyio.write(tostring(data1 or ""))elseif id == 5 then --EOFio.flush()io.close()active = falseelseif id == 6 thendata1:copyz(nixio.stdout, data2)data1:close()endendendend

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 = requestlocal r = {}context.request = rcontext.urltoken = {}local pathinfo = http.urldecode(request:getenv("PATH_INFO") or "", true)if prefix thenfor _, node in ipairs(prefix) dor[#r+1] = nodeendendlocal tokensok = truefor node in pathinfo:gmatch("[^/]+") dolocal tkey, tvalif tokensok thentkey, tval = node:match(";(%w+)=([a-fA-F0-9]*)")endif tkey thencontext.urltoken[tkey] = tvalelsetokensok = falser[#r+1] = nodeendendlocal 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 = contextctx.path = requestlocal 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" thenlocal aclang = http.getenv("HTTP_ACCEPT_LANGUAGE") or ""for lpat in aclang:gmatch("[%w-]+") dolpat = lpat and lpat:gsub("-", "_")if conf.languages[lpat] thenlang = lpatbreakendendendrequire "luci.i18n".setlanguage(lang)local c = ctx.treelocal statif not c thenc = createtree() --1. 节点树node-tree创立endlocal track = {}local args = {}ctx.args = argsctx.requestargs = ctx.requestargs or argslocal nlocal token = ctx.urltokenlocal preq = {}local freq = {}for i, s in ipairs(request) dopreq[#preq+1] = sfreq[#freq+1] = sc = c.nodes[s]n = iif not c thenbreakendutil.update(track, c)if c.leaf thenbreakendendif c and c.leaf thenfor j=n+1, #request doargs[#args+1] = request[j]freq[#freq+1] = request[j]endendctx.requestpath = ctx.requestpath or freqctx.path = preqif track.i18n theni18n.loadc(track.i18n)end-- Init template engineif (c and c.index) or not track.notemplate then --需要显示的部分,通过MVC模板自动生成显示页面local tpl = require("luci.template")local media = track.mediaurlbase or luci.config.main.mediaurlbaseif not pcall(tpl.Template, "themes/%s/header" % fs.basename(media)) thenmedia = nilfor name, theme in pairs(luci.config.themes) doif name:sub(1,1) ~= "." and pcall(tpl.Template,"themes/%s/header" % fs.basename(theme)) thenmedia = themeendendassert(media, "No valid theme found")endlocal function _ifattr(cond, key, val)if cond thenlocal env = getfenv(3)local scope = (type(env.self) == "table") and env.selfreturn string.format(' %s="%s"', tostring(key),util.pcdata(tostring( valor (type(env[key]) ~= "function" and env[key])or (scope and type(scope[key]) ~= "function" and scope[key])or "" )))elsereturn ''endendtpl.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" thenreturn build_url()elseif key == "REQUEST_URI" thenreturn build_url(unpack(ctx.requestpath))elsereturn rawget(table, key) or _G[key]endend})endtrack.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 " .."/openwrt/luci/issues")if track.sysauth then --认证部分local authen = type(track.sysauth_authenticator) == "function"and track.sysauth_authenticatoror authenticator[track.sysauth_authenticator]local def = (type(track.sysauth) == "string") and track.sysauthlocal accs = def and {track.sysauth} or track.sysauthlocal sess = ctx.authsessionlocal verifytoken = falseif not sess thensess = http.getcookie("sysauth")sess = sess and sess:match("^[a-f0-9]*$")verifytoken = trueendlocal sdat = (util.ubus("session", "get", { ubus_rpc_session = sess }) or { }).valueslocal userif sdat thenif not verifytoken or ctx.urltoken.stok == sdat.token thenuser = sdat.userendelselocal eu = http.getenv("HTTP_AUTH_USER")local ep = http.getenv("HTTP_AUTH_PASS")if eu and ep and sys.user.checkpasswd(eu, ep) thenauthen = function() return eu endendendif not util.contains(accs, user) thenif authen thenlocal user, sess = authen(sys.user.checkpasswd, accs, def)local tokenif not user or not util.contains(accs, user) thenreturnelseif not sess thenlocal sdat = util.ubus("session", "create", { timeout = tonumber(luci.config.sauth.sessiontime) })if sdat thentoken = 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_sessionendendif sess and token thenhttp.header("Set-Cookie", 'sysauth=%s; path=%s/' %{sess, build_url()})ctx.urltoken.stok = tokenctx.authsession = sessctx.authuser = userhttp.redirect(build_url(unpack(ctx.requestpath)))endendelsehttp.status(403, "Forbidden")returnendelsectx.authsession = sessctx.authuser = userendendif track.setgroup thensys.process.setgroup(track.setgroup)endif track.setuser then-- trigger ubus connection before dropping root privsutil.ubus()sys.process.setuser(track.setuser)endlocal target = nilif c thenif type(c.target) == "function" thentarget = c.targetelseif type(c.target) == "table" thentarget = c.target.targetendendif c and (c.index or type(target) == "function") thenctx.dispatched = cctx.requested = ctx.requested or ctx.dispatchedendif c and c.index thenlocal tpl = require "luci.template"if util.copcall(tpl.render, "indexer", {}) thenreturn trueendendif 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, errif type(c.target) == "table" thenok, err = util.copcall(target, c.target, unpack(args))elseok, err = util.copcall(target, unpack(args))endassert(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)"))elselocal root = node()if not root or not root.target thenerror404("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.")elseerror404("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.")endendend

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文件中加入

localapply=luci.http.formvalue("cbi.apply")

ifapplythen

luci.sys.exec("logger-tsaved&apply pressed") --在这个加入自己的处理代码

end

2、在对应的.lua文件中写m.on_apply来启动或者处理方式。

至此,luci的运行流程基本搞懂,进阶应用还需研究

主要引用内容来自:

/zmkeil/archive//05/14/3078774.html

/x_wukong/p/4515357.html

本内容不代表本网观点和政治立场,如有侵犯你的权益请联系我们处理。
网友评论
网友评论仅供其表达个人看法,并不表明网站立场。