2017年1月21日 星期六

LinkIt 7688 Web CGI 入門(1)

LinkIt 7688(後簡稱為 7688)預設 Web UI 使用 React。如果你沒有 React 基礎,除非天生神力,想要拿 7688 Web UI source code 改成你想要的畫面門檻高的嚇人。

還有人就算插 SD card 也要把 PHP 移植到 7688,畢竟 PHP 比 React 要容易上手多了。正確的說,React 是前端 JavaScript library,但要看懂 React 如何與後端銜接的學習曲線實在太長了,對於筆者來說,又不需要搞到那麼 fancy。

最誇張的作法是把 7688 原本的 web server(uhttpd) listening port 從 80 改成別的(例如 5566),然後放自己寫的 web server,或者是拿 Boa  等嵌入式 Web Server 替代。

不過我們知道 7688 有內建時下最流行的 Python,那用他來做 CGI 可行嗎?

筆者實驗的結果很不理想,因為 Python 載入需要太多時間,就算"hello, world" 也一樣:


但是很多人都忘了7688 還支援 Lua,同樣的 "hello, world" 在 Lua 上快如閃電:


其實你去觀察 7688 上的 OpenWrt Web console,他就是用 Lua 寫成的:



OpenWrt 官方文件也提到: 

「...uHTTPd supports running Lua in-process, which can speed up Lua CGI scripts...」

所以說用 Lua 實做 7688 Web CGI 才是最佳選擇,很少看到這方面文章的原因可能是因為這語言有點冷門,或者是參考資料太少。今天就來公開實做 7688 Lua Web CGI 的幾個關鍵步驟。

環境設置

編輯 /etc/config/uhttpd,加入下圖紅色方框的部份(假如您堅持要用 Python 也請參照下圖)


HTTP Protocol in ten minutes

再繼續往下之前,必須先對 HTTP protocol 有些粗淺的理解與回顧。Web Browser 與 Web Server 之間基本通訊流程如下圖:


通常 Browser 會對 Server 發送兩種 Reqest:
  • GET
  • POST
上圖範例是 GET,POST 如下圖所示:


CGI 要如何取得 GET or POST 夾帶的資訊?讓筆者引用之前文章的圖:


接著就來示範 Lua CGI script 如何取得環境變數(environment variables),與標準輸入(stdin),與輸出 html。

GET

由上圖得知,GET 把 query string、cookie 等資訊藏在環境變數裡。7688 Lua 取得環境變數需要引用 nixio 模組,範例如下:
require 'nixio'

env = nixio.getenv()
print("env type = " .. type(env))
for k, v in pairs(env) do
    print("key=" .. k .. " value=" .. v)
end
輸出結果如下:


上圖 nixio.getenv() 取出的環境變數沒有與 set 指令列出的環境變數一一對應,原因是 set 指令除了取出全域環境變數外也會取出 shell 自帶變數。

接著我們馬上打造一個簡單的 Lua CGI script 放到目錄 /www/ 實驗看看:
-- get_demo.lua
require 'nixio'

env = nixio.getenv()

html1 = [[
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd">
<html>
    <head>
        <meta http-equiv="Content-Type" content="text/html; charset=utf-8">
        <title>GET Demo</title>
    </head>
    <body>
]]

html2 = [[
    </body>
</html>    
]]

io.write("Content-Type: text/html\r\n\r\n")
io.write(html1)
for k, v in pairs(env) do
    print("key=" .. k .. " value=" .. v .. '<br⁄>')
end
io.write(html2)
得到很多有用的資訊:


如果您用過 PHP,就會知道 PHP 會預先幫你把 query string 轉成 associative array,然後用 $_GET 取出即可。Lua 沒那麼方便,不過這只要直接從 OpenWrt luci 挖一段程式過來就解決了:
-- get_demo2.lua
require 'nixio'

function urldecode( str, no_plus )

 local function __chrdec( hex )
  return string.char( tonumber( hex, 16 ) )
 end

 if type(str) == "string" then
  if not no_plus then
   str = str:gsub( "+", " " )
  end

  str = str:gsub( "%%([a-fA-F0-9][a-fA-F0-9])", __chrdec )
 end

 return str
end

function urldecode_params(url, tbl)

 local params = tbl or { }

 if url:find("?") then
  url = url:gsub( "^.+%?([^?]+)", "%1" )
 end

 for pair in url:gmatch( "[^&;]+" ) do

  -- find key and value
  local key = urldecode( pair:match("^([^=]+)")     )
  local val = urldecode( pair:match("^[^=]+=(.+)$") )

  -- store
  if type(key) == "string" and key:len() > 0 then
   if type(val) ~= "string" then val = "" end
            
   if not params[key] then
                
    params[key] = val
   elseif type(params[key]) ~= "table" then
    params[key] = { params[key], val }
   else                
    table.insert( params[key], val )
   end
  end
 end
    
 return params
end

html1 = [[
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd">
<html>
    <head>
        <meta http-equiv="Content-Type" content="text/html; charset=utf-8">
        <title>GET Demo</title>
    </head>
    <body>
]]

html2 = [[
    </body>
</html>    
]]

io.write("Content-Type: text/html\r\n\r\n")
io.write(html1)

env = nixio.getenv()
_get = urldecode_params(env.QUERY_STRING)
for k, v in pairs(_get) do
    print("key=" .. k .. " value=" .. v .. '<br⁄>')
end
io.write(html2)
輸出結果:


POST

需要 POST 的場合最常見的就是 web form。比方說 login,在 <form>...</form> 內輸入的 id, passoword 通常會以 application/x-www-form-urlencoded 編碼後放到 HTTP Reqeust - message body 內:

uHTTPD 會把這段訊息放到標準輸入(stdin)讓 CGI script 可以讀取,訊息長度則放在環境變數 CONTENT_LENGTH。下面是一個簡單的 login form 示範(與 GET 相同的部份為了節省版面予以略過):
-- post_deom.lua
require 'nixio'

statusmsg = {
 [200] = "OK",
 [206] = "Partial Content",
 [301] = "Moved Permanently",
 [302] = "Found",
 [304] = "Not Modified",
 [400] = "Bad Request",
 [401] = "Authorization Required",
 [403] = "Forbidden",
 [404] = "Not Found",
 [405] = "Method Not Allowed",
 [408] = "Request Time-out",
 [411] = "Length Required",
 [412] = "Precondition Failed",
 [416] = "Requested range not satisfiable",
 [500] = "Internal Server Error",
 [503] = "Server Unavailable",
}

function urldecode( str, no_plus )
-- ....
end

function urldecode_params(url, tbl)
-- ....
end

function get_post_data(env)
    if env == nil or env.CONTENT_LENGTH == nil or type(env.CONTENT_LENGTH) ~= "string" then       
       return {}
    end
    
    local len = tonumber(env.CONTENT_LENGTH)
    if len == nil or len <= 0 then
        return {}
    end
    return urldecode_params(io.read(len))
end

login_form = [[
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd">
<html>
    <head>
        <meta http-equiv="Content-Type" content="text/html; charset=utf-8">
        <title>login test</title>
    </head>
    <body>
    <form action="login.lua" method="post">
    <label><b>Username</b></label><br/>
    <input id="username" type="text" name="username"/><br/>
    <label><b>Password</b></label><br/>
    <input id="password" type="password" name="password"/><br/>
    <input type="submit" value="Login">
    </form>
    </body>
</html>    
]]

local env = nixio.getenv()
local post_data = get_post_data(env)
if post_data.username and post_data.password then
    if post_data.username == 'root' and post_data.password == '12345678' then
        io.write("Status: " .. "302" .. " " .. statusmsg[302] .. "\r\n")
        io.write("Location: rpc_demo.html\r\n")
    else    
        io.write("Content-Type: text/html\r\n\r\n")
        io.write(login_form)
    end
else
    io.write("Content-Type: text/html\r\n\r\n")
    io.write(login_form)
end
運行結果:


上圖 rpc_demo.html 是聯發科提供的範例。

眼尖的讀者應該會發現這個 post_demo.lua 還沒有能力連結到真正的 id, password,請大家多多直持本 blog,讓筆者有動力繼續發表下一篇,感恩~

14 則留言:

  1. 這方面的文章不多 謝謝分享

    回覆刪除
  2. 讚哦~
    我也還在學。
    我們要用LUA做出一個匣道器的控制介面。 看到您的介紹, 受益斐淺!
    Many thanks.

    回覆刪除
  3. 非常感謝,正在用7688開發一個網頁設定畫面,獲益良多!

    回覆刪除
  4. 編譯前要改動哪個地方讓它直接進入Luci而不要去LinkIt Smart的登入畫面
    謝謝

    回覆刪除
    回覆
    1. 編譯?不需要吧,直接把LinkIt Smart預設登入畫面替換掉即可

      刪除
    2. 有點不大知道怎麼做
      是換掉路徑www下的index.html?

      刪除
    3. 因為手邊沒有7688,僅就記憶來回答,您想想,既然首頁是用React做的,然後也可以連到 Luci,那就僅是路徑的差別而已。有一個方法也許可行(沒實驗過),就是不要搬移 Luci,用個 symbolic link 連結過去。

      刪除
  5. 很受用,我在op15的系统下面,照着写了,lua demo.lua,有效,但是在游览器访问的时候,出错了。放在/www目录下,直接文本显示;放在/www/cgi-bin/目录下面,报错,说cgi的格式错误,不过很受益,谢谢楼主

    回覆刪除
    回覆
    1. 感謝支持,抱歉沒用過OP15,不過給您兩點方向參考看看:

      1. 修改已經存在的 .lua 看能不能跑出點東西
      2. 檢查您自己的 .lua 看有沒有執行權限,沒有就用 chmod 補一下

      刪除