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,讓筆者有動力繼續發表下一篇,感恩~

8 則留言:

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

    回覆刪除
    回覆
    1. 不客氣,請繼續支持小弟,感恩喔

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

    回覆刪除
    回覆
    1. 不客氣,請繼續支持小弟,感恩喔

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

    回覆刪除