Organizational tabs with OpenResty

2023-02-12nginxsetup

Motivation

I have a problem. I hoard browser tabs. In multiple windows. So much so that I need TabCloud to have an overview and do the occasional reorganization. Here's an illustration with some details redacted:

TabC

This doesn't look that bad, because

I'm talking about the black icons at the start of each row. Now, there's the option to name the windows, but that has a fatal weakness: it doesn't persist across browser restarts. There's an option to save windows, but that requires having an account and logging in, which I won't do, again, for privacy reasons.

To get around this, I started making these small favicons and serving them on a subdomain. Looks something like this:

P0

Pin the tab, and get a nice visual indicator in both the window and TabCloud. This is achieved by a bit of nginx config and a small HTML page:

server {
  server_name org.aldum.pw;
  listen 80;
  override_charset on;
  charset utf-8;
  access_log   off;
  error_log    off;

  location / {
      root /srv/http/aldum.pw/org;
      if ($request_uri ~ ^/(.*)\.html$) {
          return 302 /$1;
      }
      try_files $uri $uri.html $uri/ =404;
  }
}
temp.html
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
  "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html>
<head>
    <meta http-equiv="content-type" content="text/html; charset=utf-8">
    <meta http-equiv="X-Clacks-Overhead" content="GNU Terry Pratchett" />
    <link rel="stylesheet" type="text/css" href="style.css">
    <link rel="shortcut icon" href="out/@=NAME=@.png" />
    <title>@=NAME=@</title>
</head>
<body>
    <center>
    <img src="out/@=NAME=@.png" />
    </center>
</body>
</html>

This is the template, of course, I relatively quickly got tired of manually adding new ones, hence the @=NAME=@ and a quick'n'dirty shell script was added.

new.sh
#!/bin/sh

[ -n "$1" ] && {
  name="$1"
  nf="$name".html
  cp -ap temp.html "$nf"
  sed -i -s "s/@=NAME=@/$name/g" "$nf"
} || echo 'error'

This still needs to be invoked manually, which is kinda' dumb. Computers are our friends, especially when it comes to repetitive, unimaginative tasks. Lately, I had to make a couple of new ones, and in addition to assembling and saving the icon, has another step of running the script. I now have about 30. I figured it's time to automate, at least this simple part.

I've always been running OpenResty, which boils down to nginx + lua. Never really took advantage of it's capabilities, until now! Lua is great, because it's small (both in grammar and interpreter size), and thanks to JIT compiling, performant even. This makes it great for embedding in other software for scripting and extendability.

Notable features/oddities include:

But enough about lua, let's get to work.

Setup

According to the documentation, we can provide an HTML response with content_by_lua* directives.

location / {
    root /srv/http/aldum.pw/org;
    content_by_lua_file "org.lua";
}
org.lua
ngx.say("<p>hello, world</p>")

Hmm, I'm getting a 404. What gives?

404

Turning on the error log, apparently relative paths are not looked up starting at the webroot, but relative to nginx's path.

2023/02/12 18:06:32 [error] 9492#0: *939454 failed to load external Lua file "/opt/openresty/nginx/org.lua": cannot open /opt/openresty/nginx/org.lua: No such file or directory, client: [...], server: test.aldum.pw, request: "GET /book HTTP/1.1", host: "test.aldum.pw"

Weird, but easy enough to fix, let's just provide a full path. But there's a new problem: it's downloading, not displaying a site. The content is correct though: <p>hello, world</p>.

Taking a closer look at the tutorial, a default_type should be provided:

location / {
    root /srv/http/aldum.pw/org;
    default_type text/html;
    content_by_lua_file "/etc/openresty/org.lua";
}

hello

Okay, looking good. Concept proven, now we can go deeper:

org.lua
local page = 'book'
local content = [[
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
  "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html>
<head>
    <meta http-equiv="content-type" content="text/html; charset=utf-8">
    <meta http-equiv="X-Clacks-Overhead" content="GNU Terry Pratchett" />
    <link rel="stylesheet" type="text/css" href="style.css">
    <link rel="shortcut icon" href="out/{page}.png" />
    <title>{page}</title>
</head>
<body>
    <center>
    <img src="out/{page}.png" />
    </center>
</body>
</html>
]]
ngx.say(
  string.gsub(content, "{page}", page)
)

nos

The CSS is AWOL. Nothing we can't fix with a bit of nginx magic. As a bonus, I won't need to copy style.css around from now on.

location = /style.css {
    root /srv/http/aldum.pw;
}

Glancing a little further down, what fresh hell is this? Did I leave a stray '3' somewhere?

P3

I did nost. But I am changing 3 occurences with that gsub, maybe it's also returning that?

Yes, yes it is.

The string.gsub function also returns as a second result the number of times it made the substitution. For instance, an easy way to count the number of spaces in a string is

 _, count = string.gsub(str, " ", " ")

(Remember, the _ is just a dummy variable name.)

See, this is why we do static type checking, it should have been caught that a tuple is being returned to where a string is expected... I will be exploring typed lua in the future.

Adding an intermediate variable and ignoring the count fixes the problem:

local render, _ = string.gsub(content, "{page}", page)
ngx.say(render)

That's better, but it needs one more tweak in nginx, because now it's resolving pictures the same way as the pages, and that's obviously not going to work.

P2

So let's add one more location block above /, like so:

location = /style.css [...]

location /out/ {
    root /srv/http/aldum.pw/org;
}

location / [...]

book

Okay, getting there. What's missing? Observant readers may have noticed, that I deferred a step.

wrong

Right, it's a fixed string literal, not observing the request URL at all. Fixing that:

local page = string.gsub(ngx.var.uri, '/', '', 1)

ngx.var.uri contains the nginx $uri value, and I'm just stripping the leading slash, because it looks wrong as the title.

So that's it, we have a working solution for the HTML side of things. For the next trick, I'll think of a way to automatically generate the icons as well.

Full configs

org.aldum.pw.conf
server {
  server_name org.aldum.pw;
  listen 80;
  override_charset on;
  charset utf-8;
  access_log  off;
  error_log   off;

  location = /style.css {
    root /srv/http/aldum.pw;
  }

  location /out/ {
    root /srv/http/aldum.pw/org;
  }

  location / {
    root /srv/http/aldum.pw/org;
    default_type text/html;
    content_by_lua_file "/etc/openresty/org.lua";
  }
}
org.lua
local page = string.gsub(ngx.var.uri, '/', '', 1)
local content = [[
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
    "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html>
<head>
    <meta http-equiv="content-type" content="text/html; charset=utf-8">
    <meta http-equiv="X-Clacks-Overhead" content="GNU Terry Pratchett" />
    <link rel="stylesheet" type="text/css" href="style.css">
    <link rel="shortcut icon" href="out/{page}.png" />
    <title>{page}</title>
</head>
<body>
    <center>
    <img src="out/{page}.png" />
    </center>
</body>
</html>
]]
local render, _ = string.gsub(content, "{page}", page)
ngx.say(render)