oriolrius.cat

Des del 2000 compartiendo sobre…

Tag: lua

Add a New Dropdown Menu in OpenWRT LUCI

Reading time: 3 – 5 minutes

OpenWRT, the popular open-source Linux operating system designed for embedded devices, offers the LUCI interface for easy configuration and management. LUCI is essentially the web interface for OpenWRT, and while it’s already feature-rich, sometimes you may want to extend its functionalities based on your needs.

Recently, I had a requirement to enhance my OpenWRT LUCI interface. Specifically, I wanted to introduce a new menu named “Apps” in the LUCI dashboard. The objective was to have each entry within this new menu link to different applications installed on my device. And not just that! I wanted these application pages to pop open in new tabs for ease of access.

The Solution

The changes were made in a single file located at:

/usr/lib/lua/luci/controller/apps.lua

Within this file, I implemented the following code:

module("luci.controller.apps", package.seeall)

function index()
    local page

    page = entry({"admin", "apps"}, firstchild(), _("Apps"), 60)
    page.dependent = false

    entry({"admin", "apps", "portainer"}, call("serve_js_redirect", "https", "9443"), _("Portainer"), 10).leaf = true
    entry({"admin", "apps", "nodered"}, call("serve_js_redirect", "http", "1880"), _("NodeRED"), 20).leaf = true
    entry({"admin", "apps", "grafana"}, call("serve_js_redirect", "http", "3000"), _("Grafana"), 30).leaf = true
    entry({"admin", "apps", "prometheus"}, call("serve_js_redirect", "http", "9090"), _("Prometheus"), 40).leaf = true
end

function serve_js_redirect(protocol, port)
    local ip = luci.http.getenv("SERVER_ADDR")
    local url = protocol .. "://" .. ip .. ":" .. port
    luci.http.prepare_content("text/html; charset=utf-8")
    luci.http.write("<!DOCTYPE html><html><head><meta charset='UTF-8'><title>Redirecting...</title></head><body>")
    luci.http.write("<script type='text/javascript'>")
    luci.http.write("window.onload = function() {")
    luci.http.write("  var newWindow = window.open('".. url .."', '_blank');")
    luci.http.write("  if (!newWindow || newWindow.closed || typeof newWindow.closed == 'undefined') {")
    luci.http.write("    document.getElementById('manualLink').style.display = 'block';")
    luci.http.write("    setTimeout(function() { window.location.href = document.referrer; }, 60000);")  -- Redirigir después de 60 segundos
    luci.http.write("  }")
    luci.http.write("}")
    luci.http.write("</script>")
    luci.http.write("<p id='manualLink' style='display: none;'>The window didn't open automatically. <a href='".. url .."' target='_blank'>Click here</a> to open manually.</p>")
    luci.http.write("</body></html>")
end

Key Points

  • The index function is responsible for defining the new menu and its entries.
  • The serve_js_redirect function creates a web page that uses JavaScript to automatically open the desired application in a new browser tab.
  • A failsafe mechanism is added in the form of a link. If, for any reason (like pop-up blockers), the new tab doesn’t open automatically, the user has the option to manually click on a link to open the application.
  • The script also includes a feature that will redirect the user back to the previous page after 60 seconds if the new tab doesn’t open.

This modification provides a seamless way to integrate external applications directly into the LUCI interface, making navigation and management even more convenient!


Secure download URLs with expiration time

Reading time: 4 – 6 minutes

Requirements

Imagine a HTTP server with those restrictions:

  • only specific files can be downloaded
  • with a limited time (expiration date)
  • an ID allows to trace who download files
  • with minimal maintenance and dependencies (no databases, or things like that)

the base of the solution that I designed is the URL format:

http://URL_HOST/<signature>/<customer_id>/<expire_date>/<path_n_file>
  • signature: is calculated with the next formula, given a “seed”
    • seed = “This is just a random text.”
    • str = customer_id + expire_date + path_n_file
    • signature = encode_base64( hmac_sha1( seed, str))
  • customer_id: just an arbitrary identifier when you want to distinguish who use the URL
  • expire_date: when the generated URL stops working
  • path_n_file: relative path in your private repository and the file to share

Understanding the ideas explained before I think it’s enough to understand what is the goal of the solution. I developed the solution using NGINX and LUA. But the NGINX version used is not the default version is a very patched version called Openresty. This version is specially famous because some important Chinese webs works with that, for instance, Taobao.com

Expiration URL solution Architecture schema

In the above schema there is a master who wants to share a file which is in the internal private repository, but the file has a time restriction and the URL is only for that customer. Then using the command line admin creates a unique URL with desired constrains (expiration date, customer to share and file to share). Next step is send the URL to the customer’s user. When the URL is requested NGINX server evaluates the URL and returns desired file only if the user has a valid URL. It means the URL is not expired, the file already exists, the customer identification is valid and the signature is not modified.

NGINX Configuration

server {
 server_name downloads.local;

 location ~ ^/(?<signature>[^/]+)/(?<customer_id>[^/]+)/(?<expire_date>[^/]+)/(?<path_n_file>.*)$ {
 content_by_lua_file "lua/get_file.lua";
 }

 location / {
 return 403;
 }
}

This is the server part of the NGINX configuration file, the rest of the file can as you want. Understanding this file is really simple, because the “server_name” works as always. Then only locations command are relevant. First “location” is just a regular expression which identifies the relevant variables of the URL and passes them to the LUA script. All other URLs that doesn’t match with the URI pattern fall in path “/” and the response is always “Forbiden” (HTTP 403 code). Then magics happen all in LUA code.

LUA scripts

There are some LUA files required:

  • create_secure_link.lua: creates secure URLs
  • get_file.lua: evaluates URLs and serves content of the required file
  • lib.lua: module developed to reuse code between other lua files
  • sha1.lua: SHA-1 secure hash computation, and HMAC-SHA1 signature computation in Lua (get from https://github.com/kikito/sha.lua)

It’s required to configure “lib.lua” file, at the beginning of the file are three variables to set up:

lib.secret = "This is just a long string to set a seed"
lib.base_url = "http://downloads.local/"
lib.base_dir = "/tmp/downloads/"

Create secure URLs is really simple, take look of the command parameters:

$ ./create_secure_link.lua 

 ./create_secure_link.lua <customer_id> <expiration_date> <relative_path/filename>

Create URLs with expiration date.

 customer_id: any string identifying the customer who wants the URL
 expiration_date: when URL has to expire, format: YYYY-MM-DDTHH:MM
 relative_path/filename: relative path to file to transfer, base path is: /tmp/downloads/

Run example:

$ mkdir -p /tmp/downloads/dir1
$ echo hello > /tmp/downloads/dir1/example1.txt
$ ./create_secure_link.lua acme 2015-08-15T20:30 dir1/example1.txt
http://downloads.local/YjZhNDAzZDY0/acme/2015-08-15T20:30/dir1/example1.txt
$ date
Wed Aug 12 20:27:14 CEST 2015
$ curl http://downloads.local:55080/YjZhNDAzZDY0/acme/2015-08-15T20:30/dir1/example1.txt
hello
$ date
Wed Aug 12 20:31:40 CEST 2015
$ curl http://downloads.local:55080/YjZhNDAzZDY0/acme/2015-08-15T20:30/dir1/example1.txt
Link expired

Little video demostration

Resources

Disclaimer and gratefulness