【题外话】
学校里最近改造了校园网,要求必须用iNode验证,万幸的是路由器能刷OpenWrt,并且OpenWrt上有好多iNode认证的开源项目,比如njit8021xclient(以下简称njit-client)就非常好用。虽然程序写的好用,但是配置起来还是稍微麻烦一些的,大家通常的方法是在/etc/init.d下写启动脚本,把用户名、密码什么的都直接填进去,但毕竟配置起来不方便,同时日后修改起来也不便。好在用Lua为LuCI写配置模块很简单,索性就自己做了一个,现在把开发的流程写一下,方便初学的同学去做。为njit-client做好的Web配置界面也已经开源,地址:https://github.com/mayswind/luci-app-njitclient,或者直接下载编译好(不限平台)的文件:http://pan.baidu.com/s/1CbPal
【文章索引】
LuCI是OpenWrt上的Web管理界面,LuCI采用了MVC三层架构,同时其使用Lua脚本开发,所以开发LuCI的配置界面不需要编辑任何的Html代码,除非想自己单独去创建网页(View层),否则我们基本上只需要修改Model层就可以了。官方也有一个如何去创建模块的说明文档,虽然写的比较晦涩:http://luci.subsignal.org/trac/wiki/Documentation/ModulesHowTo
要为LuCI增加一个新模块,首先需要创建两个文件,一个位于Controller(/usr/lib/lua/luci/controller/)下,定义模块的入口;另一个位于Model(/usr/lib/lua/luci/model/cbi/)下,为配置模块实际的代码。
首先我们定义模块的入口,在/usr/lib/lua/luci/controller/下创建一个lua文件,类似如下:
module("luci.controller.控制器名", package.seeall) function index() entry(路径, 调用目标, _("显示名称"), 显示顺序) end
第一行说明了程序和模块的名称,比如在controller/目录创建一个mymodule.lua,那么就可以写成“luci.controller.mymodule”,如果你的程序比较多,可能分为好几个模块,那么可以在controller下再常见一个子目录,比如controller/myapp/,那么就可以写成“luci.controller.myapp.mymodule”。
接下来的entry表示添加一个新的模块入口,官方给出了entry的定义,其中后两项都是可以为空的:
entry(path, target, title=nil, order=nil)
第一项是访问的路径,不过路径是按字符串数组给定的,比如路径按如下方式写“{"click", "here", "now"}”,那么就可以在浏览器里访问“http://192.168.1.1/cgi-bin/luci/click/here/now”来访问这个脚本。而通常我们希望为管理员菜单添加脚本,那么我们需要按如下方式编写“{"admin", "一级菜单名", "菜单项名"}”,系统会自动在对应的菜单中生成菜单项。比如想在“网络”菜单下创建一个菜单项,那么一级菜单名可以写为“network”。
第二项为调用目标,调用目标分为三种,分别是执行指定方法(Action)、访问指定页面(Views)以及调用CBI Module。
- 第一种可以直接调用指定的函数,比如点击菜单项就直接重启路由器等等,比如写为“call("function_name")”,然后在lua文件下编写名为function_name的函数就可以调用了。
- 第二种可以访问指定的页面,比如写为“template("myapp/mymodule")”就可以调用/usr/lib/lua/luci/view/myapp/mymodule.htm文件了。
- 而如果要编写配置页面,那么使用第三种方法无非是最方便的,比如写为“cbi("myapp/mymodule")”就可以调用/usr/lib/lua/luci/model/cbi/myapp/mymodule.lua文件了。
而title和order无非是针对管理员菜单来的,可以参考其他的lua文件来决定编写的内容。
这里我们创建/usr/lib/lua/luci/controller/njitclient.lua文件,定义我们的入口,代码如下:
module("luci.controller.njitclient", package.seeall) function index() entry({"admin", "network", "njitclient"}, cbi("njitclient"), _("NJIT Client"), 100) end
我们要做的实际上就是希望能将用户名、密码等信息存储在路由器文件中,同时路由器开机时能根据设定的配置自动运行njit-client,同时我们还希望能动态的禁用和启用njit-client等等。所以最方便的方式就是使用CBI Module,上一节我们也添加了这个调用,那么接下来我们就要根据上边写的路径来创建/usr/lib/lua/luci/model/cbi/njitclient.lua文件。
开发LuCI的配置模块有很多种方式,比较基本的可以用SimpleForm,就跟开发普通的Web应用类似,当然最方便的还是使用UCI(Unified Configuration Interface,统一配置接口)的方式,因为使用UCI接口可以使得在LuCI中可以无需考虑配置文件如何存储和读取(这种方式也会自动创建“保存&应用”、“保存”以及“复位”三个按钮),同时在Bash文件中也可以非常方便的存储和读取。
对于使用UCI的方式,我们首先需要创建对应的配置文件(如果配置文件不存在的话,访问配置页面将会报错),格式即为linux配置文件的格式,文件需要存储在/etc/config,比如文件路径为“/etc/config/njitclient”,内容如下:
config login option username '' option password '' option ifname 'eth0' option domain ''
然后我们要在CBI Module的lua文件中首先需要映射与存储文件的关系,比如:
m = Map("配置文件文件名", "配置页面标题", "配置页面说明")
第一个参数即为配置文件存储的文件名,不包含路径,比如按上述创建的话,应该写为“njitclient”,而第二与第三个参数则是用在来页面上显示的,比如如下所示的图:
接下来需要创建与配置文件中对应的Section,Section分为两种,NamedSection和TypedSection,前者根据配置文件中的Section名,而后者根据配置文件中的Section类型,这里我们使用后者,代码如下。同时我们设定不允许增加或删除Section(“.addremove = false”),以及不显示Section的名称(“.anonymous = true”)。
s = m:section(TypedSection, "login", "") s.addremove = false s.anonymous = true
接下来我们需要创建Section中不同内容的交互(创建Option),常见的比如有Value(文本框)、ListValue(下拉框)、Flag(选择框)等等,详细的可以参考官方的文档:http://luci.subsignal.org/trac/wiki/Documentation/CBI
创建Option的过程非常简单,而且创建后系统会无需考虑读取以及写入配置文件的问题,系统都会自动处理。但是根据上述的要求,我们在应用配置以后可能希望启用、禁用或重新启动njit-client,所以我们还需要在页面最后判断用户是否点击了“应用”按钮,这里与编写asp网页等都是相同的,我们可以通过如下的代码判断是否点击了“应用”按钮:
local apply = luci.http.formvalue("cbi.apply") if apply then --[[ 需要处理的代码 ]]-- end
由于剩余的代码都非常简单,所以所以这部分的全部代码见下:
1 require("luci.sys") 2 3 m = Map("njitclient", translate("NJIT Client"), translate("Configure NJIT 802.11x client.")) 4 5 s = m:section(TypedSection, "login", "") 6 s.addremove = false 7 s.anonymous = true 8 9 enable = s:option(Flag, "enable", translate("Enable")) 10 name = s:option(Value, "username", translate("Username")) 11 pass = s:option(Value, "password", translate("Password")) 12 pass.password = true 13 domain = s:option(Value, "domain", translate("Domain")) 14 15 ifname = s:option(ListValue, "ifname", translate("Interfaces")) 16 for k, v in ipairs(luci.sys.net.devices()) do 17 if v ~= "lo" then 18 ifname:value(v) 19 end 20 end 21 22 local apply = luci.http.formvalue("cbi.apply") 23 if apply then 24 io.popen("/etc/init.d/njitclient restart") 25 end 26 27 return m
其中Luci全部类库的函数定义和使用说明可以参考如下地址:http://luci.subsignal.org/api/luci/index.html
上边我们已经完成了LuCI配置界面的开发,在配置界面中我们已经能读取并保存配置文件了。接下来我们要编写/etc/init.d/njitclient脚本,使程序最终能运行起来。关于UCI接口在脚本文件中的官方说明可以参考:http://wiki.openwrt.org/doc/devel/config-scripting
要使用UCI调用脚本,首先第一步需要读取配置文件,命令为“config_load 配置文件名”,比如我们可以这样读入刚才的配置文件:
config_load njitclient
接下来要遍历配置文件中的Section,可以使用“config_foreach 遍历函数名 Section类型”,例如我们可以这样:
config_foreach run_njit login
然后我们去编写名为“run_njit”的函数,在这个函数中,我们可以使用“config_get 变量名 Section名 Section参数名”获取变量的值,或者使用“config_get_bool 变量名 Section名 Section参数名”获取布尔型的值。所以全部的代码见下:
1 #!/bin/sh /etc/rc.common 2 START=50 3 4 run_njit() 5 { 6 local enable 7 config_get_bool enable $1 enable 8 9 if [ $enable ]; then 10 local username 11 local password 12 local domain 13 local ifname 14 15 config_get username $1 username 16 config_get password $1 password 17 config_get domain $1 domain 18 config_get ifname $1 ifname 19 20 if [ "$domain" != "" ]; then 21 njit-client $username@$domain $password $ifname & 22 else 23 njit-client $username $password $ifname & 24 fi 25 26 echo "NJIT Client has started." 27 fi 28 } 29 30 start() 31 { 32 config_load njitclient 33 config_foreach run_njit login 34 } 35 36 stop() 37 { 38 killall njit-client 39 killall udhcpc 40 41 echo "NJIT Client has stoped." 42 }
如果按上述内容创建好上述4个文件,那么配置页面和程序就能在OpenWrt上运行起来了。但是如果要想把自己写的程序打包,还需要创建OpenWrt的Makefile来使用OpenWrt的SDK进行编译。
关于LuCI上配置Makefile的官方说明可以见这个地址:http://luci.subsignal.org/trac/wiki/Documentation/Modules
无非就是定义包的名称(PKG_NAME)、版本和生成次数(PKG_VERSION、PKG_RELEASE)、在menuconfig中的分类说明等(define Package/luci-app-njitclient)以及安装时进行的操作(define Package/luci-app-njitclient/install)等等。其中安装的文件分为三种,分别是配置文件、可执行文件以及其他数据文件,其中配置可执行文件时,会自动加入执行权限的,所以不需要额外进行处理。Makefile全部的代码见下:
1 include $(TOPDIR)/rules.mk 2 3 PKG_NAME:=luci-app-njitclient 4 PKG_VERSION=1.0 5 PKG_RELEASE:=1 6 7 PKG_BUILD_DIR:=$(BUILD_DIR)/$(PKG_NAME) 8 9 include $(INCLUDE_DIR)/package.mk 10 11 define Package/luci-app-njitclient 12 SECTION:=luci 13 CATEGORY:=LuCI 14 SUBMENU:=3. Applications 15 TITLE:=NJIT 802.1X Client for LuCI 16 PKGARCH:=all 17 endef 18 19 define Package/luci-app-njitclient/description 20 This package contains LuCI configuration pages for njit8021xclient. 21 endef 22 23 define Build/Prepare 24 endef 25 26 define Build/Configure 27 endef 28 29 define Build/Compile 30 endef 31 32 define Package/luci-app-njitclient/install 33 $(INSTALL_DIR) $(1)/etc/config 34 $(INSTALL_DIR) $(1)/etc/init.d 35 $(INSTALL_DIR) $(1)/usr/lib/lua/luci/model/cbi 36 $(INSTALL_DIR) $(1)/usr/lib/lua/luci/controller 37 38 $(INSTALL_CONF) ./files/root/etc/config/njitclient $(1)/etc/config/njitclient 39 $(INSTALL_BIN) ./files/root/etc/init.d/njitclient $(1)/etc/init.d/njitclient 40 $(INSTALL_DATA) ./files/root/usr/lib/lua/luci/model/cbi/njitclient.lua $(1)/usr/lib/lua/luci/model/cbi/njitclient.lua 41 $(INSTALL_DATA) ./files/root/usr/lib/lua/luci/controller/njitclient.lua $(1)/usr/lib/lua/luci/controller/njitclient.lua 42 endef 43 44 $(eval $(call BuildPackage,luci-app-njitclient))
接下来在编译目录下的package目录下创建一个文件夹,如njitclient,然后将所有的文件按目录复制到该目录下即可。之后配置好OpenWrt的交叉编译环境后就可以使用OpenWrt SDK进行编译了,由于这类文章较多,故不再赘述,可以参考相关链接3及之后的文章。
【相关链接】
- LuCI:http://luci.subsignal.org/trac/wiki
- LuCI界面修改实现802.1x配置界面:http://chaochaoblog.com/archives/359
- 【详细教程】编译openwrt + njit-client 1.3 通过iNode认证:http://www.7forz.com/1973/
- openwrt SDK, 利用SDK生成自己的ipk安装包:http://blog.chinaunix.net/uid-27194309-id-3432651.html