Example of web interface using uHTTPd and Lua

I would like to create a web interface for controlling my home automation, using uHTTPd and Lua, without interfering with LuCI operation.

After reading a lot of documentation and after several trial and error, I was able to write a skeleton, solving the main difficulties.
As I don't encounter any complete example, I would like to share this one, with a double goal:

  • help beginners like me (so with a very basic level :-),
  • have the opinion and possible corrections or suggestions of the experts.

Files are divided into three folders:

Folder /www/
This is where index.html is, the fist file called when invoking LuCI with http:/192.168.1.1
So we have to choose an other name, say test.html and we will invoke our interface by http:/192.168.1.1/test.html
This file is almost identical to index.html

File /www/test.html

<?xml version="1.0" encoding="utf-8"?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN" "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<meta http-equiv="Cache-Control" content="no-cache, no-store, must-revalidate" />
<meta http-equiv="refresh" content="0; URL=cgi-bin/test/test/" />
</head>
<body style="background-color: white">
<a href="cgi-bin/test/test/">Test</a>
</body>
</html>

Folder /www/cgi-bin/test/
Path to cgi, as defined in uHTTPd configuration, is /www/cgi-bin/ .
As I want to separate my files from those of LuCI, I create this new subfolder.
File test, called by test.html, is in this folder.
It is a simple menu offering links to the other two pages. One visualizes the current environment of uHTTPd. The other show how to get the variables passed by a GET

File /www/cgi-bin/test/test

#!/usr/bin/lua

local util = require "test/testutil"

print ("Content-type: Text/html\n\r")
util.printFile( 'test-top' )
print( '<h2>Test Menu</h2>' )
print( '<div>' )
print( '<p><a href="/cgi-bin/test/testget/">' )
print( 'Get current environment table</a></p>' )
print( '<p><a href="/cgi-bin/test/testgetqs/?var1=one&var2=two">' )
print( 'Get query string</a></p>' )
print( '</div>' )
util.printFile( 'test-bottom' )

File /www/cgi-bin/test/testget

#!/usr/bin/lua

local util = require "test/testutil"
nixio = require "nixio"

print ("Content-type: Text/html\n\r")
util.printFile( 'test-top' )
print( '<h2>Get the current environment table</h2>' )
print( '<a href="/cgi-bin/test/test/">Back to menu</a>' )
local envtable = nixio.getenv()
for k,v in pairs( envtable ) do
  print( '<p>'..k..' : '..v..'</p>' )
end
print( '<a href="/cgi-bin/test/test/">Back to menu</a>' )
util.printFile( 'test-bottom' )

File /www/cgi-bin/test/testgetqs

#!/usr/bin/lua

local util = require "test/testutil"
nixio = require "nixio"

print ("Content-type: Text/html\n\r")
util.printFile( 'test-top' )
print( '<h2>Get the current query string</h2>' )
local query = nixio.getenv( 'QUERY_STRING' )
print( '<p>QUERY_STRING : "' .. query .. '"</p>' )
print( '<p>Value of var1 : "' .. util.getValQuery( 'var1' ) .. '"</p>' )
print( '<p>Value of var2 : "' .. util.getValQuery( 'var2' ) .. '"</p>' )
print( '<p>Value of var3 : "' .. util.getValQuery( 'var3' ) .. '"</p>' )
print( '<a href="/cgi-bin/test/test/">Back to menu</a>' )
util.printFile( 'test-bottom' )

Folder /usr/lib/lua/test/
Folder /usr/lib/lua/ is one of the folders where are stored lua's modules.
For the same reason as before, I create a subfolder where will be stored my modules and other files.
Module testutil.lua include two functions:

  • testutil.printFile used to print common parts of web pages. They are stored in files test-top and test-bottom and include html and ccs code.
  • testutil.getValQuery used to extract the value of a variable (used in testgetqs)

File /usr/lib/lua/test/testutil.lua

local testutil = {}

local io = require "io"
local nixio = require "nixio"

-- print file 'filename' located in the same folder /usr/lib/lua/test/

testutil.printFile = function( filename )
  local f = io.open( '/usr/lib/lua/test/' .. filename, "r" )
  if f == nil then
    print( "<p>Can't open " .. filename .. "</p>" )
  else
    local line = f:read()
    while line ~= nil do
      print( line )
      line = f:read()
   end
    f:close()
  end
end

-- Get the value of a query variable 'varname'

testutil.getValQuery = function( varname )
  local value
  local query = nixio.getenv( 'QUERY_STRING' )
  if query ~= nil then
    query = '&' .. query
    varname = '&' .. varname .. '='
    local p, q = string.find( query, varname )
    if q ~= nil then
      p = string.find( query, '&', q )
      if p == nil then p = -1 else p = p - 1 end
      value = string.sub( query, q + 1, p )
    end
  end
  value = value or 'No such variable'
  return value
end

return testutil

File /usr/lib/lua/test/test-top

<html>
  <head>
    <style rel='stylesheet' type='text/css'>
      body {
        background-color: Beige; color: DarkGreen;
        font-family:'DejaVu Sans', Helvetica, sans-serif;
      }
      h1 { font-size:6vh; text-align:center; color: Navy; }
      h2 { font-size:4vh; text-align:center; color: Navy; }
    </style>
    <title>An example</title>
    <meta charset="utf-8">
  </head>
  <body>
    <h1>Example of using Lua under uhttpd</h1>

File /usr/lib/lua/test/test-bottom

    <p style='font-size:4vh; text-align:center; color: Navy;'>
      by J-M Gallego
    </p>
  </body>
</html>

After copying this files in they respective folders and if you have LuCI installed, you must be able to test them, without loading any additional software.
As usual, comments, critics and corrections are welcome!

In particular, I wonder if:

  • would it be possible to have only one folder for cgi and lua files?
  • If there is a need to store data, which folder should I use? /root ? , /tmp ?
4 Likes

Brilliant! I was toying with the idea of writing a page to control my AC and this post comes along, much appreciated, thanks!

A few more things I learn when starting with lua:

  • when things don't work as expected, take a look at the system log (with Status/SystemLog under LuCi, or logread in a terminal)

  • remember to set 'execute' permission to lua files in folder /www/cgi-bin/test/

  • instead of adding a file /www/test.html, an other possibility is to slightly modify /www/index.html.
    This file is a 'menu with only on choice' where the line with http-equiv="refresh" content="0" is used to simulate an immediate HTTP response.
    So we can add an item to the menu and set a delay to a positive value:

<?xml version="1.0" encoding="utf-8"?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN" "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
  <head>
    <meta http-equiv="Cache-Control" content="no-cache, no-store, must-revalidate" />
    <meta http-equiv="refresh" content="5; URL=cgi-bin/luci/" />
  </head>
  <body style="background-color: white">
    <div style="color: black; font-family: arial, helvetica, sans-serif;">
      <p><a href="cgi-bin/luci/">LuCI - Lua Configuration Interface</a></p>
      <p><a href="cgi-bin/test/test/">Test Lua Script</a></p>
    </div>
  </body>
</html>

1 Like

Hello! how is your project? I've just opened a new topic about this. Maybe you can help me :woozy_face:

Every home automation here on openwrt is about other devices (rasp pi, esc's and usb relayboards) what I'm trying to do is give some extra-life to this old device.

1 Like


:man_facepalming:
This is all I get

Hi rodrigom1

Did you try this two tips?

yeah, I did. Nothing about uhttpd in the logs and all files were set to 755 and folders to 644

May I recommend you start a different uhttpd instance on a different port (or relocate standard luci WebUI to a port different from 80 to vacate it for your scripts) for those?

That way you can control the location of code/data, scripting language and have better separation between luci and your scripts.

3 Likes

Hi . thanks for sharing . i have question . i want to include some resources like *.css , *.png , *.js for test-top file . where ( which folder ) should i put them ?

Hi,

I know this is an old’ish post now, but i was just wondering if/how things had progressed ?

Thanks for this great post, it helped me a lot.
But I need few more thing like, I need to redirect to another page based on condition using Lua. Like header("Location: cgi-bin/newPage") in php.

As Lua is likely to python I got something like this in python but same concept not working in Lua.
https://www.codegrepper.com/code-examples/python/redirect+user+based+on+input+with+python+cgi+code

Can you please help ?

Thanks in advance.

io.write("Status: 302 Moved\r\n")
io.write("Location: /cgi-bin/newpage.cgi\r\n")
io.write("\r\n")
os.exit(0)

Thanks a lot for your response its working alone but if i use this under "print ("Content-type: Text/html\n\r") " its not working

print ("Content-type: Text/html\n\r") 

io.write("Status: 302 Moved\r\n")
io.write("Location: /cgi-bin/done\r\n")
io.write("\r\n")
os.exit(0)

But if i use it before(like below) then its working great.

io.write("Status: 302 Moved\r\n")
io.write("Location: /cgi-bin/done\r\n")
io.write("\r\n")
os.exit(0)
print ("Content-type: Text/html\n\r")

The \n\r in print ("Content-type: Text/html\n\r") will terminate the HTTP header block. Since Lua print() will implicitly append a newline to anything it prints, print ("Content-type: Text/html\n\r") will output Content-type: Text/html\n\r\n which is interpreted as end of header due to the consecutive newlines.

Due to the implicit newline handling of print() it is more explicit and less confusing to use io.write() instead. You also should consistently use \r\n (carriage return, line feed) as end of line indicator to achieve maximum compatibility with all CGI enabled web servers.

Corrected example:

-- arbitrary amount of header lines
io.write("Status: 302 Moved\r\n")
io.write("Location: /cgi-bin/done\r\n")
io.write("Content-Type: text/html\r\n")

-- additional empty line to indicate end of headers
io.write("\r\n")

-- arbitrary amount of content data (optional)
io.write([[
<html>
  <head>
    <title>Content has been moved</title>
  </head>
  <body>
    <h1>Content has been moved</h1>
    <p>
      The content has been moved to <a href="/cgi-bin/done">/cgi-bin/done</a>.
    </p>
  </body>
</html>
]])

Thanks a lot dear for your kind response and to make my concept clear.

One more thing I need to know, how to handle session in Lua.

Thanks in advance

Session handling is not Lua specific, you need to look into how to handle sessions within CGI applications in general.

Usually you want to generate some form of ID, send that to the user using a Cookie header, and check the received cookie ID value against a local session database on each request.

You can reuse the rpcd ubus session mechanism for that so that you only need to deal with cookie setting and retrieval.

The relevant calls are:

  • Login: ubus call session login '{ "username": "user", "password": "pass", "timeout": 3600 }'
  • Verify: ubus call session get '{ "ubus_rpc_session": "sessionid" }'
  • Logout: `ubus call session destroy '{ "ubus_rpc_session": "session" }'

You can use the Lua ubus module to invoke these procedures.

Client sent cookies are available in the HTTP_COOKIE environment variable which you can obtain using e.g. Lua os.getenv("HTTP_COOKIE") .

Minimal untested example:

local os = require 'os'
local ubus = require 'ubus'
local string = require 'string'

local ubusconn = ubus.connect()
assert(ubusconn, "Failed to connect to ubus")

local postdata = nil

function urldecode(val)
    return (val or ""):gsub("%%([0-9a-fA-F][0-9a-fA-F])",
        function(x)
            return string.char(tonumber(x, 16))
        end)
end

function get_cookie(name)
    for cookie in (os.getenv("HTTP_COOKIE") or ""):gmatch("[^; ]+") do
       local n, v = cookie:match("^([^=])+=(.*)$")
       if n == name then
           return urldecode(v)
       end
    end
end

function get_postdata(key)
    if not postdata then
        postdata = {}

        local rawlen = tonumber(os.getenv("CONTENT_LENGTH") or "0")
        if rawlen > 0 then
            local rawdata = io.stdin:read(rawlen)
            for tuple in (rawdata or ""):gmatch("([^&]+)") do
                local k, v = tuple:match("^([^=]+)=(.*)$")
                if k and v then
                    postdata[urldecode(k)] = urldecode(v)
                end
            end
        end
    end

    return postdata[key]
end

function get_session(sid)
    local sessiondata = ubusconn:call("session", "get", { ubus_rpc_session = sid })
    if type(sessiondata) == "table" and type(sessiondata.values) == "table" then
        return sessiondata.values
    end
end

function create_session(username, password)
    local sessiondata = ubusconn:call("session", "login", {
        username = username,
        password = password,
        timeout = 3600 
    })

    return (type(sessiondata) == "table") and sessiondata.ubus_rpc_session or nil
end

function set_session(sid, key, value)
    return (ubusconn:call("session", "set", { 
        ubus_rpc_session = sid, values = { [key] = value }
    }) == 0)
end


local current_session_id = get_cookie("session")
local current_session_data = current_session_id and get_session(current_session_id)

-- no session active ...
if not current_session_data then
   -- not logged in...
   local username = get_postdata("username")
   local password = get_postdata("password")

   -- username and password received (e.g. from login form), try to login
   if username and password then
       current_session_id = create_session(username, password)

       -- login failed
       if not current_session_id then
           -- send error message here ...

       -- login okay
       else
           -- set some session parameters
           set_session(current_session_id, "user_name", username)
           set_session(current_session_id, "login_time", os.time())

           -- send welcome message here ...
       end

    -- no login data received, send login form
    else
        -- send login form here ...
    end

-- session active ...
else
  -- logged in, values in current_session_data
  -- e.g. current_session_data.user_name or current_session_data.login_time
end
1 Like

Hello Dear , Please help me to create a cookie by your help now I can get cookie now I need to create a cookie.
Sorry for my bad english.

Send a Set-Cookie header before the Content-Type header.

Thanks a to dude , finally I its complete. I am really thankful to you dear.

Hello Dear, I am really thankful to you regarding your previous help. Now I am trying to upload a file (ex: image. pdf etc) and download a report in pdf format using LUA . Can you please help me ?

Thanks in advance