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 之后,就会出现登录界面:
- -bash-4.0# pwd
- /var/www/cgi-bin
- -bash-4.0#./luci
- Status:200 OK
- Content-Type: ext/html;
- charset=utf-8
- Cache-Control: no-cache
- Expires: 0
- <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN"
- "http://www.w3.org/TR/html4/strict.dtd">
- <html class="ext-strict">
- </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提供一个集中控制的接口。
- UCI的配置文件全部存储在 /etc/config 目录下
- 常见UCI配置文件
dhcp | 面向LAN口提供的IP地址分配服务配置 |
dropbear | SSH服务配置 |
firewall | 路由转发,端口转发,防火墙规则 |
network | 自身网络接口配置 |
system | 时间服务器时区配置 |
wireless | 无线网络配置 |
uhttpd | Web服务器选项配置 |
luci | 基本的LuCI配置 |
每个文件都涉及到系统配置的一部分。可以用VI编辑器或用UCI命令行修改配置文件。也可以通过各种编程API(如shell、lua、C)来修改,这也是Web接口例如LuCI修改UCI文件的方式。
- 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的名字如果相同,则相同键的值将会被当作数组传递给相应软件
- UCI常见的命令:
add | 增加指定配置文件的类型为section-type 的匿名区段。 |
add_list | 对已存在的list选项增加字符串。 |
commit | 对给定的配置文件写入修改 |
export | 导出一个机器可读格式的配置。它是作为操作配置文件的shell脚本而在内部使用,导出配置内容时会在前面加“package”和文件名。 |
import | 以UCI语法导入配置文件。 |
changes | 列出配置文件分阶段修改的内容,即未使用“uci commit”提交的修改。如果没有指定配置文件,则指所有的配置文件的修改部分。 |
show | 显示指定的选项、配置节或配置文件。以精简的方式输出,即key=value的方式输出。 |
get | 获取指令区域选项的值。 |
set | 设置指定配置节选项的值,或者是增加一个配置节,类型设置为指定的值。 |
delete | 删除指定的配置或选项。 |
rename | 对指定的选项或配置节重命名为指定的名字。 |
revert | 恢复指定的选项,配置节点或配置文件。 |
- 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()` 方法获取具体的配置信息。