Openwrt Web gui LUCI 流程浅析

Openwrt Web gui LUCI 流程浅析网上讲Luci的资料不是很多,搜集整理了一下其基本运行流程。1.总述1.request(get/post):浏览器发起方向请求。2.fork一个子进程:uhttpdfork出一个子进程。3.子进程利用execl替换为luci进程空间3.1服务器利用setenv(传递一些固定格式的数据(如PATH_INFO)给luci)。3.2服务器通过w_pipe(post-data)向luci的stdin写数据。4.服务器通过r_pipe读取luci发向其的stdout

网上讲Luci的资料不是很多,搜集整理了一下其基本运行流程。

1.总述

Openwrt Web gui LUCI 流程浅析

Openwrt Web gui LUCI 流程浅析

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配置时的数据交互:

Openwrt Web gui LUCI 流程浅析

  1. 首次运行时,是以普通的file方式获得docroot/index.html,该文件中以meta的方式自动跳转到cgi的url,这是web服务器的一般做法。

  2. 然后第一次执行luci,path_info=’/’,会alise到’/admin’(’/’会索引到 tree.rootnode,并执行其target方法,即alise(‘/admin’),即重新去索引adminnode,这在后面会详细描述),该节点需要认证,所以返回一个登录界面。

  3. 第3次交互,过程同上一次的,只是这时已post来了登录信息,所以serv端会生成一个session值,然后执行’/admin’的target(它的target为firstchild,即索引第一个子节点),最终返回/admin/status.html,同时会把session值以cookie的形式发给client。这就是从原始状态到得到显示页面的过程,之后主要就是点击页面上的连接,产生新的request。

  4. 每个链接的url中都会带有一个stok值(它是serv生成的,并放在html中的url里),并且每个新request都要带有session值,它和stok值一起供serv端联合认证

2.luci程序流程

2.1 lua语言中的主协进程执行方式

Openwrt Web gui LUCI 流程浅析

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如下:

Openwrt Web gui LUCI 流程浅析

这里要注意的是,每次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()流程。

Openwrt Web gui LUCI 流程浅析

  如上图所示,再联系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的文件),直接生成界面

Openwrt Web gui LUCI 流程浅析

上面的标题是由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

 

 

今天的文章Openwrt Web gui LUCI 流程浅析分享到此就结束了,感谢您的阅读。

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。
如需转载请保留出处:https://bianchenghao.cn/9619.html

(0)
编程小号编程小号

相关推荐

发表回复

您的电子邮箱地址不会被公开。 必填项已用*标注