1. 多語言
1)檢查:
opkg list | grep luci-i18n-
2)安裝語言包:
opkg install luci-i18n-hungarian
2.uhttpd
這個是LuCI所在的Web Server。docroot在/www下邊,index-html指向了/cgi-bin/luci,注意這是相對於docroot而言的路徑。
openwrt中利用它作為web服務器,實現客戶端web頁面配置功能。對於request處理方式,采用的是cgi,而所用的cgi程序就是luci。
1)工作框架如下圖所示:
Client端和serv端采用cgi方式交互,uhttpd服務器的cgi方式中,fork出一個子進程,子進程利用execl替換為luci進 程空間,並通過setenv環境變量的方式,傳遞一些固定格式的數據(如PATH_INFO)給luci。另外一些非固定格式的數據(post- data)則由父進程通過一個w_pipe寫給luci的stdin,而luci的返回數據則寫在stdout上,由父進程通過一個r_pipe讀取。
2)luci 文件(權限一般是 755 ) , luci 的代碼如下:
#!/usr/bin/lua -- 執行命令的路徑
require"luci.cacheloader" -- 導入 cacheloader 包
require"luci.sgi.cgi" -- 導入 sgi.cgi 包
luci.dispatcher.indexcache = "/tmp/luci-indexcache" --cache 緩存路徑地址
luci.sgi.cgi.run() -- 執 行 run 方法,此方法位於 /usr/lib/lua/luci/sgi/cgi.lua中
3)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端聯合認證。
初始階段http報文,可以看到從第2次交互開始,所有request都是cgi方式(除一些css、js等resource文件外),且執行的cgi程序都是luci,只是帶的參數不同,且即使所帶參數相同(如都是'/'),由於需要認證,執行的過程也是不同的。
正是由於多種情況的存在,使得luci中需要多個判斷分支,代碼多少看起來有點亂,但openwrt還是把這些分支都糅合在了一個流程線中。下面首 先給出整體流程,首先介紹一下lua語言中一個執行方式coroutine,它可以創造出另一個執行體,但卻沒有並行性,如下圖所示,每一時刻只有一個執 行體在執行,通過resume、yield來傳遞數據,且數據可以是任意類型,任意多個的。
Luci正是利用了這種方式,它首先執行的是running()函數,其中create出另一個執行體httpdispatch,每次 httpdispatch執行yield返回一些數據時,running()函數就讀取這些數據,做相應處理,然后再次執行 resume(httpdispath),……如此直到httpdispatch執行完畢,如下圖所示:
如上圖所示,其實luci真正的主體部分正是dispatch,該函數中有多個判斷分支,全部糅合在一起。
4)節點樹node-tree
在controller目錄下,每個.lua文件中,都有一個index()函數,其中主要調用entry()函數,形如 entry(path,target,title,order),path形如{admin,network,wireless},entry()函數根 據這些創建一個node,並把它放在全局node-tree的相應位置,后面的參數都是該node的屬性,還可以有其他的參數。其中最重要的就是 target。
Createtree()函數就是要找到controller目錄下所有的.lua文件,並找到其中的index()函數執行,從而生成一個 node-tree。這樣做的io操作太多,為了效率,第一次執行后,把生成的node-tree放在/tmp/treecache文件中,以后只要沒有 更新(一般情況下,服務器里的.lua文件是不會變的),直接讀該文件即可。生成的node-tree如下:
這里要注意的是,每次dispatch()會根據path_info逐層索引,且每一層都把找到的節點信息放在一個變量track中,這樣做使得上 層node的信息會影響下層node,而下層node的信息又會覆蓋上層node。比如{/admin/system},最后的 auto=false,target=aa,而由於admin有sysauth值,它會遺傳給它的子節點,也即所有admin下的節點都需要認證。
5)target簡介
對每個節點,最重要的屬性當然是target,這也是dispatch()流程最后要執行的方法。target主要有:alise、 firstchild、call、cbi、form、template。這幾個總體上可以分成兩類,前兩種主要用於鏈接其它node,后一個則是主要的操 作、以及頁面生成。下面分別描述。
鏈接方法:在介紹初始登錄流程時,已經講到了這種方法。比如初始登錄時,url中的path_info僅為 '/',這應該會索引到rootnode節點。而該節點本身是沒有內容顯示的,所以它用alias('admin')方法,自動鏈接到admin節點。再 比如,admin節點本身也沒有內容顯示,它用firstchild()方法,自動鏈接到它的第一個子節點/admin/status。
操作方法:這種方法一般用於一個路徑的葉節點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端。
6)sysauth用戶認證
由於節點是由上而下逐層索引的,所以只要一個節點有sysauth值,那么它所有的子節點都需要認證。不難想象,/admin節點有sysauth 值,它以下的所有子節點都是需要認證才能查看、操作的;/mini節點沒有sysauth值,那么它以下的所有子節點都不需要認證。
luci中關於登陸密碼,用到的幾個函數為:
可以看出它的密碼是用的linux的密碼,而openwrt的精簡內核沒有實現多用戶機制,只有一個root用戶,且開機時自動以root用戶登錄。要實現多用戶,必須在web層面上,實現另外一套(user、passwd)系統。
另外,認證后,serv端會發給client一個session值,且它要一直以cookie的形式存在於request報文中,供serv端來識別用戶。這是web服務器的一般做法,這里就不多講了。
7)MVC界面生成
這其實是luci的精華所在,/usr/lib/lua/luci/下有三個目錄model、view、controller,它們對應M、V、C。下面簡單介紹生成界面的方法。
Call()方法會調用controller里的函數,主要通過openwrt系統的uci、network、inconfig等工具對系統進行設 置,如果需要還會生成新界面。動態生成界面的方法有兩種,一是通過cbi()/form()方法,它們利用model中定義的模板map,生成html文 件;另一種是通過template()方法,利用view中定義的htm(一種類似html的文件),直接生成界面。
上面的標題是由node-tree生成的,下面的內容由每個node通過上面的方法來動態生成。這套系統是很復雜的,但只要定義好了,使用起來就非常方便,增加頁面,修改頁面某個內容等操作都非常簡單。
8)啟動:
/etc/init.d/uhttpd start
9)開機自啟動:
/etc/init.d/uhttpd enable
代碼分析
1,啟動:
在瀏覽器中輸入:http://192.168.1.1/ 會自動跳到http://192.168.1.1/cgi-bin/luci
luci\modules\base\htdocs\cgi-bin\luci
1 #!/usr/bin/lua -- 執行命令的路徑 2 require"luci.cacheloader" -- 導入 cacheloader 包 3 require"luci.sgi.cgi" -- 導入 sgi.cgi 包 4 luci.dispatcher.indexcache = "/tmp/luci-indexcache" --cache 緩存路徑地址 5 luci.sgi.cgi.run() -- 執 行 run 方法,此方法位於 luci\modules\base\luasrc\sgi\cgi.lua 中
run方法
1 luci\modules\base\luasrc\sgi\cgi.lua
1 function run() 2 local r = luci.http.Request( 3 luci.sys.getenv(), 4 limitsource(io.stdin, tonumber(luci.sys.getenv("CONTENT_LENGTH"))), 5 ltn12.sink.file(io.stderr) 6 ) 7 8 local x = coroutine.create(luci.dispatcher.httpdispatch) //開啟協助線程---->調用luci\modules\base\luasrc\dispatcher.lua里的httpdispatch函數 9 local hcache = "" 10 local active = true 11 12 while coroutine.status(x) ~= "dead" do 13 local res, id, data1, data2 = coroutine.resume(x, r) 14 15 if not res then 16 print("Status: 500 Internal Server Error") 17 print("Content-Type: text/plain\n") 18 print(id) 19 break; 20 end 21 22 if active then 23 if id == 1 then 24 io.write("Status: " .. tostring(data1) .. " " .. data2 .. "\r\n") 25 elseif id == 2 then 26 hcache = hcache .. data1 .. ": " .. data2 .. "\r\n" 27 elseif id == 3 then 28 io.write(hcache) 29 io.write("\r\n") 30 elseif id == 4 then 31 io.write(tostring(data1 or "")) 32 elseif id == 5 then 33 io.flush() 34 io.close() 35 active = false 36 elseif id == 6 then 37 data1:copyz(nixio.stdout, data2) 38 data1:close() 39 end 40 end 41 end 42 end
2,進入網頁
在瀏覽器中輸入:http://192.168.1.1/ 會自動跳到http://192.168.1.1/cgi-bin/luci 登陸默認root 密碼是空,默認的幾個一級菜單都是在modules\admin-full\luasrc\controller\admin\ 這個目錄下,index.lua為執行文件
1 module("luci.controller.admin.index", package.seeall) //聲明一下這個模塊, 模塊入口為函數 2 3 function index() 4 local root = node() //定義了最外面的節點,也就是最上層的菜單顯示 5 if not root.target then 6 root.target = alias("admin") 7 root.index = true 8 end 9 10 local page = node("admin") 11 page.target = firstchild() //----->luci\modules\base\luasrc\dispatcher.lua--->firstchild() 12 page.title = _("Administration") 13 page.order = 10 14 page.sysauth = "root" 15 page.sysauth_authenticator = "htmlauth" //---->luci\modules\base\luasrc\dispatcher.lua---->htmlauth() 找到哪個用戶 16 page.ucidata = true 17 page.index = true 18 19 -- Empty services menu to be populated by addons 20 entry({"admin", "services"}, firstchild(), _("Services"), 40).index = true 21 22 entry({"admin", "logout"}, call("action_logout"), _("Logout"), 90) 23 end
entry 定義了菜單下的一個子菜單。
1 entry(路徑, 調用目標, _("顯示名稱"), 顯示順序) 2 entry(path, target, title=nil, order=nil)
第1項為菜單入口;
第2項為菜單對應的頁面,可以是lua的源代碼文件,也可以是html頁面,甚至可以是以上兩種頁面的組合。(alias是指向別的entry的別名,from調用的某一個view,cbi調用某一個model,call直接調用函數)
第3項是菜單的文本,直接添加string不會國際化,_("string"),就國際化了
第4項是同級菜單下,此菜單項的位置,從大到小。
3,登陸
目錄:luci\modules\base\luasrc\dispatcher.lua
1 function authenticator.htmlauth(validator, accs, default) 2 local user = luci.http.formvalue("username") 3 local pass = luci.http.formvalue("password") 4 5 if user and validator(user, pass) then 6 return user 7 end 8 9 require("luci.i18n") 10 require("luci.template") 11 context.path = {} 12 luci.template.render("sysauth", {duser=default, fuser=user}) 13 return false 14 15 end
4.子菜單項
1 function _firstchild() 2 local path = { unpack(context.path) } 3 local name = table.concat(path, ".") //當前目錄下,既:luci\modules\admin-full\luasrc\controller\admin\ 4 local node = context.treecache[name] 5 6 local lowest 7 if node and node.nodes and next(node.nodes) then 8 local k, v 9 for k, v in pairs(node.nodes) do 10 if not lowest or 11 (v.order or 100) < (node.nodes[lowest].order or 100) 12 then 13 lowest = k 14 end 15 end 16 end 17 18 assert(lowest ~= nil, 19 "The requested node contains no childs, unable to redispatch") 20 21 path[#path+1] = lowest 22 dispatch(path) 23 end 24 25 --- Alias the first (lowest order) page automatically 26 function firstchild() 27 return { type = "firstchild", target = _firstchild } 28 end
即
1 entry({"admin", "services"}, {"firstchild","network.lua system.lua ......"}, _("Services"), 40).index = true
如上可以看出,登陸后第一級目錄如下:luci\modules\admin-full\luasrc\controller\admin\下的xxx.lua文件
增加一個菜單:
1 http://www.right.com.cn/forum/thread-131035-1-1.html