code.pablomurad.com

skull-browser

Keyboard-driven browser for the small web, with native Gopher and Gemini

git clone https://code.pablomurad.com/skull-browser.git

Recent commits

  1. 2d7abf9
    chore: drop a stray .deb that got committed, and ignore them
    
  2. 0f036e4
    feat: own visual identity, and gemini over a shared small web layer
    
    F7, the visual identity. The shared chrome stylesheet is now a set of
    CSS custom properties on :root, with a light variant behind
    prefers-color-scheme, and the nine internal pages draw their colours
    from those tokens instead of hardcoding them. The GTK theme in
    theme.lua is rebuilt around the same phosphor palette, private tabs
    read as their own mode rather than a darker shade, and the new tab page
    is an authored plate with the mark, the name and the keys worth
    knowing, its colours taken from the theme.
    
    F8, the small web. gopher and gemini now share lib/smallweb: one page
    shell, one stylesheet, and a render mode setting that applies to both.
    Terminal mode is full width and monospaced with a caret on every link;
    page mode reads like a document. gopher was refactored onto it, so the
    two schemes cannot drift.
    
    gemini:// is new. It speaks the protocol directly over TLS through
    LuaSec, renders gemtext to HTML, follows input and redirect status
    codes, and escapes every field so a hostile page cannot become markup.
    Trust is trust on first use, in lib/smallweb/tofu: the first
    certificate a host presents is remembered and a change is reported, not
    accepted, with the choice to trust the new one. Without LuaSec the
    scheme still loads and every page says what to install.
    
    skull://smallweb (gS) switches the render mode and lists the remembered
    gemini hosts, forgetting any of them. Verified end to end against
    geminiprotocol.net: the handshake completes and the fingerprint is
    stored.
    
    Tests cover gemtext rendering including markup escaping, url parsing
    and link resolution, and the full tofu lifecycle. 97 passing.
    
  3. 15fab09
    docs: everything in English, and a security model in the README
    
    The comments written during this fork were in Portuguese, in ten files
    across Lua, C and shell. They are English now, and so is every message
    setup-deps.sh prints. docs/decisoes is docs/decisions, and the decision
    record about staying on webkit2gtk-4.1 is translated with it.
    
    The README gains three sections.
    
    Security model states what is on and off by default, and then says the
    part that matters: there is no sandbox around Lua. A module under
    ~/.config/skull runs with everything the user account has, and so does
    a userscript installed with :usinstall. Saved form data is plain Lua
    the browser executes on every page load, 0600 and host-anchored but not
    encrypted, and it says so rather than implying otherwise. Certificate
    exceptions, what they cover and how to see and revoke them.
    
    Extending covers the tree layout, writing a module, adding a
    skull:// page in ten lines, and adding binds and commands.
    
    Contributing covers the build, the suite, the style checks that fail
    the build, where tests live and what a commit message looks like.
    
    Two claims in the README were wrong and are corrected: there is no `?`
    binding for help, only :help, and run-tests takes no arguments.
    

Full log →

Readme

Skull Browser

A keyboard-driven web browser that treats the small web as a first-class citizen. Regular sites over https://, plus native gopher:// and gemini:// support. Everything above the C core is Lua, and you can change all of it.

C Lua WebKitGTK

What it is

Modal navigation in the vim tradition: hjkl to scroll, f to follow links by keyboard hint, : for commands. The rendering engine is WebKitGTK, the widget layer is GTK 3, and roughly sixty Lua modules sit on top providing tabs, modes, bindings, ad blocking, bookmarks, history, downloads and the internal skull:// pages.

Version 0.01. Linux only. It has never been built on macOS, BSD or Windows, so it is not claimed to work there. On Windows it runs under WSL with WSLg, because that is Linux.

Requirements

GTK 3
WebKitGTK 4.1
Lua 5.1 or LuaJIT
lfs        (lua filesystem)
socket     (lua socket, for gopher and gemini)
ssl        (luasec, for gemini TLS)
sqlite3
gstreamer  (video playback)

Install

Clone, pull in the dependencies, build:

git clone https://github.com/runawaydevil/skull-browser.git
cd skull-browser
sh build-utils/setup-deps.sh
make

setup-deps.sh handles apt, dnf and pacman. It needs sudo to install and it verifies everything afterwards, so run it once and read what it prints. To check an existing system without installing anything:

sh build-utils/setup-deps.sh --check

Then install system-wide:

sudo make install

Or into your home directory, no root needed:

make PREFIX=$HOME/.local XDGPREFIX=$HOME/.local/etc/xdg install

Make sure $HOME/.local/bin is on your PATH if you go that route.

Run

skull
skull gopher://gopher.floodgap.com
skull -k        # check config and exit

Under WSL, calling it from Windows needs a login shell, otherwise ~/.local/bin is not on PATH and the name does not resolve:

wsl -d Ubuntu -- bash -lc 'skull gopher://gopher.floodgap.com'

Press gA or type :about for version information.

Configuration

User config lives in ~/.config/skull/. Copy the shipped rc.lua there and edit it:

mkdir -p ~/.config/skull
cp config/rc.lua config/theme.lua ~/.config/skull/

A system install also drops a copy in /usr/local/etc/xdg/skull/, which is picked up automatically when there is nothing in ~/.config/skull/. A home install (PREFIX=$HOME/.local) puts it somewhere the browser does not search, so copy it by hand as shown above.

Themes are a flat Lua table in theme.lua with a cascading naming scheme: tab_selected_fg falls back to selected_fg, then to fg. Change a handful of root keys and the whole interface follows. The internal skull:// pages read their colours from the same file through CSS custom properties, so one edit moves the whole browser.

Small web

gopher:// and gemini:// are rendered by the browser itself, not proxied. Both share one presentation layer, so a setting applies to both.

skull gopher://gopher.floodgap.com
skull gemini://geminiprotocol.net

Two render modes, switched at skull://smallweb/ or with gS:

terminal   full width, monospaced, a caret on every link (default)
page       a comfortable measure, read like a document

Gemini has no certificate authorities. The first time a host is seen its certificate is remembered, and a different one later is reported rather than accepted. This is trust on first use. skull://smallweb/ lists the remembered hosts and forgets any of them, which trusts the next certificate afresh.

Gemini needs LuaSec (the ssl module). Without it the scheme still loads but every page explains what to install. setup-deps.sh installs it.

Tests

make run-tests

Requires luassert. The suite covers the Lua modules and the C bindings.

Security model

Worth reading before you trust this with anything.

What it does by default

third-party cookies      blocked
DNS prefetching          off
WebGL, WebAudio          off
media capture            off
clipboard access by JS   denied
popups by JS             denied
developer tools          off
page permissions         denied
typed addresses          https, never http
Referer, cross-domain    dropped
Referer, same domain     cut back to the origin, no path or query
Referer on a downgrade   dropped
files written on disk    0600, the process runs under umask 0077
ad and tracker lists     EasyList, EasyPrivacy, EasyList Portuguese

What it does not do

There is no sandbox around Lua. A module under ~/.config/skull/ runs in the browser process with every power your user account has: it can read your files, open sockets and start programs. That is the same deal as a window manager config, and it is what makes the browser worth using, but it means a Lua module you did not read is a program you did not read.

The same applies to userscripts installed with :usinstall. They reach the bridge into Lua, so treat one from a web page as you would treat a shell script from a web page.

JavaScript is on by default. Turn it off globally or per domain in rc.lua:

settings.webview.enable_javascript = false
settings.on["example.com"].webview.enable_javascript = true

Saved form data, including passwords, is stored in ~/.local/share/skull/ as plain Lua that the browser executes on every page load. The file is 0600 and the pattern is anchored to the page host, but it is not encrypted. If that matters to you, use a password manager and do not use :formfiller.

Certificates

When a certificate fails verification you can trust it anyway. That choice covers one certificate on one host, not the host in general, so a different certificate on the same host warns again. The exception lapses after 90 days.

gC          list the stored exceptions
:certs      same thing

While an exception is in force the status bar shows (exception) rather than (trust), because the connection is not verified, it is excused.

Reporting something

Open an issue at https://github.com/runawaydevil/skull-browser/issues. If it is a vulnerability rather than a bug, say so in the title and leave out the working payload until it is fixed.

Extending

Everything above the C core is Lua, and the Lua is on your disk.

lib/             the modules that make up the browser
lib/*_wm.lua     modules that run inside WebKit's render process
lib/lousy/       widgets and utilities shared by the rest
config/rc.lua    what gets loaded, and in what order
widgets/         the C widgets exposed to Lua
clib/            the C libraries exposed to Lua

A module is an ordinary Lua file returning a table. Drop it in ~/.config/skull/ and require it from your rc.lua.

An internal page takes about ten lines:

local chrome = require "chrome"

chrome.add("hello", function ()
    return "<html><body><h1>hello</h1></body></html>"
end)

That gives you skull://hello/. Pass a table of functions as the fourth argument to chrome.add and they become callable from the page’s JavaScript, returning promises. Only pages served from skull:// can reach them.

Key bindings and commands go through modes:

local modes = require "modes"

modes.add_binds("normal", {
    { "^gh$", "Go home.", function (w) w:navigate("skull://newtab/") end },
})

modes.add_cmds({
    { ":hello", "Say hello.", function (w) w:notify("hello") end },
})

Type :help in the browser for the generated documentation, which covers every module, binding and setting present in your build.

docs/decisions/ holds the decisions that shaped the fork and the reasoning behind them. 001-webkitgtk-api.md explains why nothing new should be written under extension/.

Contributing

make                     build
make run-tests           the whole suite
make apidoc              regenerate doc/apidocs

The suite includes style checks. luacheck runs over every Lua file, trailing whitespace fails the build, and every exported module function needs a doc comment. Run the tests before sending anything; CI runs the same commands on push and on pull requests.

Tests live in tests/async/, one file per module, each returning a table of test_* functions. A fix belongs with a test that fails without it.

Commit messages say what changed and why, in English, present tense, with a short subject line and a body when the subject is not enough.

License

GNU GPLv3. See COPYING.GPLv3.

Based on luakit. All prior authorship is preserved in AUTHORS.