A lightweight Gemini server with inline Lua scripting

f971137 major refactoring; removing http stuff, switching to libevent for buffering and IO polling

a month ago

6993b0a adding unit for http stuff

2 months ago

builds.sr.ht status



MoonGem is a Gemini protocol server written in C. It supports serving static files as well as Gemtext (.gmi) files with inline Lua scripting.

Inline scripts are indicated by special begin/end line tokens. An example page might look like this:

# Fibonacci Sequence 

function write_fibonacci(n)
 local i, j = 0, 1
  for k = 1, n do
    i, j = j, i + j

    -- write the result in a Gemtext bullet list
    BODY:line(string.format('* %d', i))

Here are the first 20 members of the Fibonacci sequence:



OpenSSL 1.1.1 or later Lua 5.4 (5.3 may work but I haven't tried it) LibMagic (for mimetype identification)


git clone https://git.sr.ht/~panda-roux/MoonGem
cd MoonGem && mkdir build && cd build
cmake ..
make && sudo make install


# download and run the image (see docker-deploy.sh)
docker pull pandarouxdev/moongem && \
  docker run -dp 1965:1965 \
    --name moongem-1 \
    --mount source=moongem-content,target=/gemini \
    --mount source=moongem-certs,target=/certs,readonly \


./moongem <path-to-certificate.pem> <path-to-key.pem> <root-content-dir>

Optionally, set the MOONGEM_PORT environment variable to listen on a non-default network port.


The start and end of script sections are indicated with a special sequence of characters, which must appear on their own lines, without any prefix:

  • Start: -<<
  • End: >>-

I chose these tokens because in my opinion they're visually distinctive and unlikely to be included in any typical content.

A single global variable PATH contains the path of the requested page (i.e. /my/document.gmi).

MoonGem exposes the following Lua functions for generating content:

BODY:include(<path>) Inserts the contents of the file at path. Note that this DOES NOT run embedded scripts in the source document if a Gemtext file is specified. This is an intentional choice for the sake of simplicity.

BODY:write(<text>) Writes text to the body of the document. No new-line character is appended.

BODY:line([text]) Writes text to the body of the document, followed by a new-line sequence. If text is omitted, only the new-line sequence is written.

BODY:link(<url>, [text]) Writes a link pointing to url to the body of the document. Optionally, text can be specified in order to append link alt-text.

BODY:heading(<text>, [level]) Writes text as a heading line to the body of the document. The value of level indicates the heading level (in other words, level == number of #'s).

BODY:block(<text>) Writes text in a preformatted block.

BODY:begin_block([alt-text]) Writes the beginning of a preformatted block with optional alt-text.

BODY:end_block() Writes the end of a preformatted block.

HEAD:set_lang(<language>) Sets the language tag in the response header.

HEAD:get_input([prompt]), HEAD:get_sensitive_input([prompt]) Prompts the client for input and returns the result.

HEAD:has_cert() Returns true if the client provided a certificate on this request; otherwise, false.

HEAD:get_cert([prompt]) If no client certificate was sent along with the current request, sends a code-60 status response to the client along with the optioal prompt, and returns nil. The client should then repeat the request with a valid certificate. Otherwise, if a certificate is present, returns a table with the following fields:

  • fingerprint: a string representation of a SHA256 hash of the client certificate's public key
  • not_after: the certificate's expiration time, in seconds since the UNIX epoch

HEAD:temp_redirect(<url>), HEAD:perm_redirect(<url>) Sends a redirect response to the client (either temporary or permanent). Note that HEAD:redirect(<url>) is an alias for HEAD:temp_redirect(<url>).


The source code can be found here