openwrt-Luci 整体流程分析

Luci

注意:当post请求来了登陆信息后,会在uhttp中建立一个会话,服务器会生成一个token值发送给客户端,客服端会把token放入url中。stok值就是这个会话的id,session值存储的是用户,权限,验证。成功后,服务器端会根据在cookie中设置一个sysauth值,在后面进行验证时会用到。但是我们是单用户登陆,所以并没有真正的进行节点的验证,这里最主要的是时效性的验证,如果过期需要重新登陆。

在luci 的官方网站说明了 luci 是一个 MVC 架构的框架,这个 MVC 做的可扩展性很好,可以完全的统一的写自己的 html 网页,而且他对 shell 的支持相当的到位,       与 shell 是兄弟)。在登录界面用户名的选择很重要,      luci 是一个单用户框架,公用的模块放置在 */luci/controller/ 下面,各个用户的模块放置在 */luci/controller/ 下面对应的文件夹里面,比如              admin 登录,最终的页面只显示 /luci/controller/admin 下面的菜单。这样既有效的管理了不同管理员的权限。

Luci的执行过程

根目录下有一个www 文件夹,其中的index.html为整个网页的起始页面

href 超链接到 cgi-bin/luci

 run方法的主要任务就是在安全的环境中打开开始页面(登录页面),在 run 中,最主要的功能还是在dispatch.lua 中完成。

 运行luci 之后,就会出现登录界面:

  1. -bash-4.0# pwd
  2. /var/www/cgi-bin
  3. -bash-4.0#./luci
  4. Status:200 OK
  5.  Content-Type: ext/html;
  6.  charset=utf-8
  7.  Cache-Control: no-cache
  8. Expires: 0
  9. <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN"
  10. "http://www.w3.org/TR/html4/strict.dtd">
  11.  <html class="ext-strict">
  12.  </html>

文件中说明了run()是luci的入口函数,run() 函数所在的位置是 /usr/lib/lua/luci/sgi中,下面分析一下run()函数。

定义了一个变量r接受http的request请求。随后创建一个指向httpdispatch的函数协程

通过一个while循环判断协程的状态,来获取返回数据。

res 判断是否执行成功。Id,data1,data2是通过yield返回的数据。resume(x, r) x 代表就是执行这个协程,r就是http的request请求,在执行完httpdispatch这个主要函数后,会根据内部协程 yield 返回的id,调用系统函数io.write(),写入uhttp中,uhttp负责将响应信息发送给客户端。

id 1 : 响应头未输出 构建响应状态码和描述信息

id 2: 响应头开始输出 头部字段和值

id 3 : 响应头输出完毕

id 4 : 输出响应正文

id 5:处理完毕

httpdispatch函数分析

httpdispatch位于/usr/lib/lua/luci/dispatcher中

先看一下context的定义

看一下threadlocal函数

context通常指的是请求上下文对象,它包含了当前HTTP请求的相关信息,以及对请求进行处理和响应的方法和属性。具体来说,context对象通常包括以下内容:

请求信息:包括HTTP请求的方法(GET、POST等)、URL、头部信息、请求参数等。

响应方法:包括向客户端发送HTTP响应的方法,例如write用于写入响应内容,redirect用于重定向等。

会话管理:包括会话状态的管理,如设置和获取会话状态、会话超时处理等。

模板渲染:提供了模板渲染的方法和属性,用于将动态数据渲染到HTML模板中。

国际化支持:包括国际化翻译方法和相关配置。

路由信息:提供了当前请求的路由信息,包括控制器、动作等。

其他工具函数和属性:可能包括一些其他常用的工具函数和属性,用于简化处理逻辑、访问配置等。

可以看到这些代码是用来解析路径的

例如:

http://192.xxx.x.x/cgi-bin/luci/;stok=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxx/admin/system/xxx

解析完成后

context.urltoken 存放的是 stok= xxxxxxxxxxxxxxxxxxxxxxxxxxxxxx

context.request 存放的是 admin system xxx

最主要的是执行了如下语句:

该语句调用了util下的coxpcall函数,使用了该函数调用dispatch函数,观察coxpcall函数

coxpcall 函数中的f 就是dispatch函数,err就是我们传入的error500错误处理函数,这个函数本质还是调用了coroutine.create建立一个执行dispatch的协程。Co执行的就是执行这个函数的注册协程,coxpt就是存储协程父子关系的表。随后执行了performResume函数

performresume函数就是执行co协程后将其作为参数,传递给handleReturnValue函数,coroutine.resume(co,…) 函数返回一个状态码,将状态码传递给handleReturnValue函数进行处理。如果存在错误,内部有assert函数抛出错误,用debug.traceback捕获错误,作为参数传递给error500函数。

看一下error500函数

message就是抛出的异常信息,然后调用until.perror函数进行处理。这里可以看一下http.status() http.perpare() http.write()函数

通过yield函数将参数 发送给run()函数中的remuse()函数 进行处理,发送的是状态码。

发送响应头部,通过coroutine.yield(4, content) 发送响应正文,下面可以看一下header函数:

headers中存放的是响应头的信息,在这里把headers中的key转为小写通过yield发送给run()函数

分析一下dispatch函数

这里的代码主要是设置系统语言

这里很简单,就是读取uci配置文件,uci配置文件再/ect/config

不算很重要的点。

重点是路由树的生成

可以看一下路由树的生成,createtree();

没有索引则创建索引,可以看一下creatindex函数

Cachedata就是缓存文件。会遍历controller下所有的文件。

生成的树的结构可以看一下,找到其节点后最主要的就是里面的target。

看一下路由树的结构,根据request的PTAH_INFO(相对路径)进行解析寻找对应的节点的,例如:/admin/status/routes/,找到其对应的节点,进行后续的操作。

看一下target的简介,target主要有call,alise,cbi,template四种,call一般是直接调用函数,alise是重定向,一般是默认界面会用到,cbi主要是调用的是model下的cbi中的 模板文件主要是lua语言进行编写的,template是直接调用模板文件 view下的文件。

然后往下看

这个代码主要是渲染主题的,主要是有两个主题,主要是负责渲染主题的模板文件。主要是负责 背景,导航栏等生成,主要是图中的这一个部分,整个页面的html代码不是一次性渲染出来的。

看一下 _ifattr 函数。

这个函数的作用是在模板文件中会用到,主要是生成格式化的字符串,生成属性标签.

下面则是生成了一个viewns的lua表,主要是给模板设置了元表看一下这个方法。

其中主要的是write 重定向到 http文件下的 write函数,在上面我们已经介绍过了。

Ifatrr函数和attr函数,主要是返回__ifattr() 函数的执行结果,上面我们已经说过了__ifattr函数的具体作用了。来看个例子:

这是一个生成下拉框的模板文件,可以看到 select和option函数的属性标签,例如 id,name等最终都是通过 __ifatt函数进行生成的。

下面主要是一些进行用户验证的,这一点可以不看略过。

从这里开始看,这个代码是获取当前页面的目标地址,进行页面跳转。

这个代码,是处理索引页面的,如果其存在默认页面,target为默认页面的执行函数。

下面的代码主要的作用的执行索引到节点的目标函数,并检测函数是否执行成功,如果target是call就调用_call函数,如果是cbi就调用_cbi,如果是template函数则直接调用模板进行渲染生成 html代码。

这里我们重点看一下 _cbi函数,看一下cbi模块是怎么最终生成 html代码的。

_cbi函数在dispatcher文件下

这里的重点是 load函数,load函数位于cbi文件中

cbidir主要是拼接出一个完整的cbi文件路径通过loadfile函数加载文件,返回一个函数地址给 func。

重点看一下这些代码,通过执行func函数,会返回一个maps表,。cbi文件中,map,section,option等都是相当于是,在这里需要进行初始化,就是调用其构造函数。

这里先调用了Map构造函数。先看一下map的构造函数。

可以看到调用了Node的初始化,观察一些node的初始化函数

可以看到title,description在我们的编写的map中都有定义,就是标题和描述,还有默认的模板是cbi下的node模板文件,但是我们文件夹中没有给我们这个模板文件,其实并不需要渲染这个node。

这里调用instanceof对section进行初始化。

看一下section的初始化,section的主要是有两种,一个是Namedsection和Typedsection两种,无论是哪一种其实其基类都是AbstractSection。从代码中可以看出

可以看到都是要初始化AbstractSection看一下AbstractSection的初始化

里面定义了一些属性和方法,在后面会用到。至此整个map的结果就是 Node->map->section.

继续像section中添加控件

mqttServerIp= s:taboption("general", Value, "mqttServerIp", translate("MQTT Server Ip_addr"))

会调用AbstractSection中的option函数,函数原型如下:

可以看到又调用了instanceof函数将AbstractValue进行实例化,会调用Value.__init__的初始化函数,最终返回一个maps对象表。给_cbi函数。

回到_cbi函数。

此循环是调用parse()函数,调用map.parse()函数,再循环调用child:parse()对map下的section进行parse。获取状态值 负责解析 HTTP 请求中的参数,并将参数值填充到相应的 Node 实例中

重点是渲染模板生成。

循环遍历maps表,调用map.render函数

调用Node.render函数

主要是

先对模板进行初始化

里面的self.viewns = context.viewns就是我们在dispatch函数中 定义的 viewns元表

重点是:

sourcefile 指向一个真正的html模板文件tparser.parse 会把之前的html文件根据 Map结构 解析出内容 self.template 然后调用Template.render会使用 attr("value", self:cfgvalue(section) or self.default) 调用cfgvalue 或者default 获取配置文件中设置好的值填充

看一下template.render()函数

设置函数执行环境   将parser生成的lua文件中的write 定向到 http.write(),把生成的htm写入http.write(),至此整个页面生成结束。

问题总结:

1.stoken值是怎么产生的:一个用户登陆后,uhttp中建立了一个会话,luci创建生成的一个token值存在服务器端,服务器会把token发给客服端,客服端也就是我们的浏览器,客户端会把这个token放入url中。Stoken就是这个会话的ID,session值存储的是用户权限,验证。成功后,服务器会根据cookie中设置sysauth值设置为true,在后面验证时会用到。

2.页面是如何进行实时更新数据的:XHR.poll 发送一个异步请求,通过ubus获取,在更新标签中的数据进行实现,每五秒更新一次。

3.Load中的prepare函数的主要的作用是设置默认值,处理文件上传工作,生成默认值。

4.Load中的parse函数的主要的作用是执行一些钩子函数,解析map对象输入的数据,编辑和保存,会执行 cfvalue和write函数。

5.模板文件中常常会嵌入一些lua代码,就是在模板渲染的时候会执行,主要是tparser.parse(sourcefile),进行执行,返回出来的就只有htm代码了。

6.元表:元表(metatable)是一种特殊的表,用于定义其他表的行为。每个表都可以关联一个元表,通过元表可以改变表的操作行为,例如重载操作符、定义默认值等。一些常见的函数 rawset/rawget,访问表中一个指定键对应的值,不会触发元表中的任何元方法

7.http常见环境变量的键值对

请求

响应

REQUEST_METHOD

请求方法get,post

STATUS

响应状态码

QUERY_SIRNG

URL中? 部分

CONTENT_TYPE

响应内容的类型

CONTENT_LENGTH

请求体长度

CONTENT_LENGTH

响应内容的长度

HTTP_HOST

请求的主机地址部分

SET_COOKIE

设置响应信息中的COOKIE信息

HTTP_USER_AGENT

客户端的用户代理信息

HTTP_ACCEPT

客户端能够接受的MIME类型

HTTP_cookie

请求中的cookie信息

REMOTE_ADDR

客户端的IP地址

SERVER_PROTOCOL

请求所使用的协议版本

UCI配置文件详解:

UCI是Unified Configuration Interface的缩写,翻译成中文就是统一配置接口,用途就是为OpenWrt提供一个集中控制的接口。

  1. UCI的配置文件全部存储在 /etc/config 目录下
  2. 常见UCI配置文件

dhcp

面向LAN口提供的IP地址分配服务配置

dropbear

SSH服务配置

firewall

路由转发,端口转发,防火墙规则

network

自身网络接口配置

system

时间服务器时区配置

wireless

无线网络配置

uhttpd

Web服务器选项配置

luci

基本的LuCI配置

每个文件都涉及到系统配置的一部分。可以用VI编辑器或用UCI命令行修改配置文件。也可以通过各种编程API(如shell、lua、C)来修改,这也是Web接口例如LuCI修改UCI文件的方式。

  1. UCI文件语法

config 'section_type'      'section'

    option    'key'         'value'

    list      'list_key'       'list_value'

config节点:以关键字 config 开始的一行用来代表当前节点

section_type: 节点类型 -------------对应section中的title 参数

section: 节点名称 ---------------在UCI中自定义,可以在cbi中用anonymous=true 隐匿

UCI允许只有节点类型的匿名节点存在

节点名字建议使用单引号包含

节点可以包含多个option或list

节点遇到文件结束或遇到下一个节点代表完成

option选项:表示节点中的一个元素

key:键 //对应option中的name 参数

value:值

选项value建议使用单引号包含

相同的选项key存在于同一个节点,只有一个生效

list 列表项:表示列表形式的一组参数

list_key: 列表键

list_value:列表值

列表key的名字如果相同,则相同键的值将会被当作数组传递给相应软件

  1. UCI常见的命令:

add

增加指定配置文件的类型为section-type 的匿名区段。

add_list

对已存在的list选项增加字符串。

commit

对给定的配置文件写入修改

export

导出一个机器可读格式的配置。它是作为操作配置文件的shell脚本而在内部使用,导出配置内容时会在前面加“package”和文件名。

import

以UCI语法导入配置文件。

changes

列出配置文件分阶段修改的内容,即未使用“uci commit”提交的修改。如果没有指定配置文件,则指所有的配置文件的修改部分。

show

显示指定的选项、配置节或配置文件。以精简的方式输出,即key=value的方式输出。

get

获取指令区域选项的值。

set

设置指定配置节选项的值,或者是增加一个配置节,类型设置为指定的值。

delete

删除指定的配置或选项。

rename

对指定的选项或配置节重命名为指定的名字。

revert

恢复指定的选项,配置节点或配置文件。

  1. CBI文件编写

1. CBI控件

CBI模型是描述UCI配置文件结构的Lua文件,并且CBI解析器将lua文件转为HTML呈现给用户 。

所有 CBI 模型文件必须返回类型为 luci.cbi.Map 的对象。

CBI 模型文件的范围由 luci.cbi 模块的内容和 luci.i18n 的转换函数自动扩展。

2.用法解析

2.1 class Map (config, title, description)

模型的根对象

参数说明:

config:配置文件名,请参阅 UCI 文档和 /etc/config 中的文件

title: 页面显示名称

description: 页面显示详细描述

方法说明:

function :section(sectionclass, …)

sectionclass: a class object of the section。// section的类对象

//传递给section类的构造函数的其他参数

属性如下:

template: html 模板, 默认为"cbi/tsection"

addremove: 是否可以增加和删除, 默认为 false

anonymous: 是否为匿名 section, 默认为 false

网页示例:

2.2 class NamedSection(name, type, title, description)

根据type来解析一个UCI section

示例: Map:section(NamedSection, "name", "type", "title", "description")

参数说明:

name: UCI section名字, config type section

type: UCI section类型, config type section

type可选:Value、 DynamicList、 Flag、 ListValue、TextValue、MultiValue、DummyValue、StaticList、Button …

title: 页面显示名称

description: 页面显示详细描述

对象属性:

property.addremove = false

//允许用户删除并重新创建配置section。

property.anonymous = true

true 页面不显示section名字

false 页面显示section名字

property.dynamic = false

ture 将section 标记为动态

//将此部分标记为动态。动态section可以包含未找到的完全用户定义的选项数。

property.optional = true

解析可选的options

方法说明:

function :option(optionclass, …)

Creates a new option //创建新选项

ptionclass: a class object of the section //section的类对象

additional parameters passed to the constructor of the option class //传递给选项类的构造函数的其他参数

option(type, name, title, description)

option对象有一些属性如下:

rmempty:如果为空值则删除该选项,默认为 true

default: 默认值, 如果为空值,则设置为该默认值

datatype: 限制输入类型。 例如"and(uinteger,min(1))"限制输入无符号整形而且大于 0, "ip4addr"限制

输入 IPv4 地址, "port"限制输入类型为端口,更多参考/usr/lib/lua/luci/cbi/datatypes.lua

placeholder: 占位符( html5 才支持)

2.3 class TypedSection (type, title, description)

通过类型type选择来描述一组 UCI section的对象。

示例用法: Map:section(TypedSection, "type", "title", "description")

参数说明:

type: UCI section类型, config type section

type可选:Value、 DynamicList、 Flag、 ListValue、TextValue、MultiValue、DummyValue、StaticList、Button …

title: 页面显示名称

description: 页面显示详细描述

对象属性:

property.addremove = false 同NameSection

property.anonymous = true 同NameSection

property.template = “cbi/template” 设置此页面的模块

方法说明:

function :depends(key, value)

仅当另一个选项键在同一section中设置为值时,才显示此选项字段。如果多次调用此函数,则依赖项将链接为 [或]

function .filter(self, section) -abstract-

.//您可以重写此函数以筛选某些不会解析的部分。对于应分析的每个section,将调用筛选器函数,对于应筛选的section,将返回 nil。对于所有其他section,它应返回第二个参数中给出的section名称。

网页实例:

TypedSection与NamedSection的运用场景与区别?

1. TypedSection:

   - 运用场景:适用于配置结构比较固定、且各个配置项的含义和作用相对清晰的情况。一般用于表示一类特定类型的配置段,例如表示一个网络接口、一个无线网络等。

   - 区别:TypedSection 在定义时需要指定一个 schema,这个 schema 定义了配置段中可以包含的选项及其类型、默认值等信息。在实际使用时,可以直接通过 `luci.model.uci.cursor()` 对象的 `sections()` 方法来获取所有指定类型的配置段,并且可以使用 `add()`、`delete()`、`set()` 等方法对其进行操作。

2. NamedSection:

   - 运用场景:适用于配置结构比较灵活、或者某个配置项的含义和作用可能会有变化的情况。一般用于表示一组没有固定结构的配置段,通常是根据配置文件中的不同配置项来动态生成的。

   - 区别:NamedSection 在使用时可以直接指定配置段的名称,而不需要提前定义 schema。它更加灵活,适用于那些没有固定结构的配置段。使用 `luci.model.uci.cursor()` 对象的 `sections()` 方法可以获取所有指定类型的配置段,然后可以通过 `section()` 方法获取具体的配置信息。

  • 25
    点赞
  • 23
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

脑袋瓜凉凉的

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值