Skip to the main content
mkfifo.com

A named pipe for the web

Updated planned discovered changed fixed

The first two days of mkfifo.com: the idea of a named pipe for the web, the envelopes that are its rules, the editor of the envelopes, and the loops that a graph of FIFOs can have.

A named pipe for the web

On Unix, mkfifo makes a named pipe. I want the same thing on the web: a name that takes HTTP requests and emails in, and sends webhooks, emails and answers out.

The idea

The FIFO NAME is at https://mkfifo.com/NAME, and its inbox is NAME@mkfifo.com. A Request goes in. It comes out as a webhook, an email, an answer to the client, or a Request to a different FIFO.

Envelopes

Each rule is one message in the format of RFC 5322, the same as an email. I did not want a custom format or JSON: people and programs already know how an email looks.

  • An ingress envelope is a test. Each header line and each body line is a pattern, and (?<name>...) keeps a part of the Request.
  • An egress envelope replies to it with In-Reply-To, and tells what to send and where. {{name}} puts a capture there.

The stack

Phoenix and LiveView in Elixir, on BEAM.com: the Erlang VM in WebAssembly, in one Durable Object of Cloudflare Workers. SQLite keeps the envelopes. It is one of the first real apps on BEAM.com, so I expect rough edges (see Running on BEAM.com).

A word that was not true

The home page said "No code". But you write regular expressions in a FIFO, and for many people that is code. Now it says "Works with curl".

Loops of FIFOs

An egress envelope can send to a different FIFO. So the FIFOs make a graph, and a graph can have a loop. TLC showed me that my first lock was wrong.

The first design

A save of an egress envelope adds an edge to the graph. My plan: lock the FIFO of the save, look for a loop, then save. I wrote the model first (specs/Loops.tla), and TLC found the loop a -> b -> a in four steps. Two saves looked at the graph at the same time. Each one saw no loop, and each one saved its edge.

The fix

One lock for all the saves of edges. A save that makes a loop gets 422. The hop limit stays too (8 Received lines), for a long chain with no loop.

Later: more than one edge

Mirrors (see Mirrors) add saves with more than one edge. The model got them too. TLC finds a loop when the save of a mirror does not look at the graph, and the code does not skip it.

Envelopes you can edit

Each FIFO has an editor at GET /NAME. An ingress envelope looks like brown paper, an egress envelope looks like airmail, and a pipe goes between them.

  • It works with no JavaScript. Each form sends POST /NAME with _method and a CSRF token. With JavaScript, LiveView does the same thing with no reload.
  • I removed daisyUI and wrote a small theme for the envelopes.
  • The name field of a header line suggests the usual header names.

On a phone

  • The stamp of an envelope pushed the To line to two lines. Container queries fixed it: the stamp now scales with the face of its envelope.
  • A long name on the home page made the page wider than the phone. Fixed.

Notes, three designs in one night

A note tells what a FIFO does. Its design changed three times before it stayed.

  1. Markdown with earmark_parser. It kept <script> and javascript: links, and it was slow on some input. No.
  2. Blocks of HTML, as in the editor of Gutenberg: a paragraph or a heading. I wrote my own HTML cleaner, with a TLA+ model of its stack.
  3. lazy_html. A few hours later, lazy_html (lexbor, the HTML parser of the standard) ran in the Worker as a NIF in WebAssembly. I removed my tokenizer and its model. The cleaner now walks the tree of lazy_html.

A note has blocks: a paragraph, or a heading of level 1 to 6 from a drop-down. It keeps only text with <strong>, <em>, <mark>, <code>, <br>, and <a href> to https:, mailto: or a path of mkfifo.com.

The rules of an envelope

Two rules of the envelopes changed after I used them: what a capture can do in the message that goes out, and what a missing header means.

A capture cannot change the message

A capture comes from whoever sent the Request. It must not change the structure of what goes out. Each place gets its own escapes.

  • In a header line: no line breaks.
  • In a URL: percent-encoding.
  • In a JSON body: the escapes of a JSON string.

I missed the HTML body at first. A capture went into an HTML body as it was, so the sender of a Request could add tags, for example an image, to an email. Now a capture in an HTML or XML body gets the escapes of HTML.

Later I added Markdown (see One shape for stream parsers). {{note | markdown}} in an HTML body gives the capture as Markdown, and my converter escapes each character, so it cannot add a tag. In an email of Content-Type: text/markdown, the Markdown is the text of the envelope, and a capture is only text in it: it cannot add a link or a heading.

A missing header is an empty value

Before, a header line needed the field in the Request. Now a missing field is an empty value, so Website: ^$ agrees when the field is empty or missing.

That gave a honeypot for free. A form has the field Website, and hides it from people. A program that fills each field of a form fills it too, and its Request does not agree. The letter of a page hides such a field: it is out of the page, and it does not take the focus.

The first spam

On 11 October, the contact form got its first spam: one line in Catalan that asked for our prices. The forwarded email could not tell me how it came: through the form, as an email, or as a plain HTTP request. So I changed three things, and none of them changes what a person sees.

  • The email tells how the message came. Each forwarded message now has a line Sent with:: the form and its seconds, an email and its DMARC result, or an HTTP request. The ingress envelope captures X-MkFIFO-DMARC and the new X-MkFIFO-Form-Seconds, and a jq command in the egress envelope writes the line from the two captures. No new code, only the builtins.
  • A form tells how long it took. When mkfifo.com draws the form of a page, the form gets the time, with a signature. When the form is sent, the Request has X-MkFIFO-Form-Seconds: the seconds since the page loaded. Only a form has this field, as only an email has X-MkFIFO-DMARC: mkfifo.com removes it from an HTTP request, an email and a Request from a different FIFO. A program sends a form in a moment, and a person needs more time to write a message, so the contact envelope has X-MkFIFO-Form-Seconds: ^(|[5-9]|[1-9][0-9]+)$. A form sent faster than that gets "Your message was not sent because it came too fast. Wait a few seconds, then send it again." Your own FIFO can use the same line.
  • The honeypot looks like any other field. It was the first field, at left: -9999px, with aria-hidden and tabindex="-1": well-known signs that a program can look for. Now it is in its place between the other fields, with the same label and attributes, and it is inert: a person cannot focus it, a browser does not fill it in, and a screen reader skips it. The class that hides it has a plain name.