Suros

A suros is a single-user responsive object server. It is implemented as a collection of Elisp declarations for operating an ActivityPub-compliant (and IndieWeb-friendly) web server. It is in the earliest stages of development.

Table of Contents

1 Introduction

A suros is a single-user responsive object server. It is implemented as a collection of Elisp declarations for operating an ActivityPub-compliant (and IndieWeb-friendly) web server. It is in the earliest stages of development.

Please be aware, I am a hobbyist crafter of computer instructions. This whole thing is the work of a novice. Documentation may be confusing and the implemented code might not do exactly what it's supposed to. Further, it is a personal project. The "you" in this documentation is me. While I would love if someone found it useful, don't expect it.

There might be a development server up at https://suros-emsenn.pagekite.me.

This document was written by me, emsenn, in the United States. To the extent possible under law, I have waived all copyright and related or neighboring rights to this document. This document was created for the benefit of the public.

2 Suros

A suros is a single-user responsive object server. It is implemented as a collection of Elisp declarations for operating an ActivityPub-compliant (and IndieWeb-friendly) web server. It is in the earliest stages of development.

In plain English, it's an Emacs package for running a dedicated webserver.

In addition to requiring Emacs version 24 or greater, these instructions require the following Emacs packages

  • Emacs Web Server, which provides functions for running the HTTP server and responding to requests.
  • Mustache.el, which provides functions for templating objects.
  • Ht.el, which provides easier ways to declare hash tables.

This document will assume familiarity with the Emacs text-editor as a piece of software, and the following formal languages:

  • Org-mode
  • HTML
  • CSS
  • JSON
  • Elisp
  • Mustache

Additionally, familiarity with the following protocols, specifications, and standards is expected:

  • HTTP
  • ActivityPub
  • IndieWeb
  • Webfinger
  • Webmentions

This document is divided into the following sections:

2.1 Architecture

I can explain how your suros is designed no more than a sparrow can explain how they've designed their nest.

2.2 Operations

2.2.1 Administrative Operations

2.2.1.1 Install

Copy the Suros.el Package appendix and load it how you'd load any local package.

2.2.1.2 DRAFT Configure

Right now your suros' configuration is right there in its source - sorry, unless you're picky about it, trying to configure it from your init won't work right now. At the top of suros.el there's a definition for the suros-config hashtable - edit it carefully, but edit it to configure your suros. See "Suros Configuration" for a detailed explanation of that data structure. Once you're done editing it, either M-x evaluate-buffer or place point (your cursor) after the last paranthesis of the configuration statement and press C-x C-e.

2.2.1.2.1 ActivityPub Actor

Your suros will create an ActivityPub actor at {{base-url}}/actor. In order to do that accurately, you'll need to customize the following configuration variables:

actor-public-key
The public key of an RSA keypair.1
user-name
In @user@base.url, this is user.
base-url
In @user@base.url, this is base.url.
2.2.1.2.2 H-Card

Your suros will create an h-card at its homepage. This requires at least the following configuration varaibles:

proper-name
The name you'd prefer used in official contexts.
2.2.1.3 Run

To run your suros, run the command suros-start.

This puts a webserver up at whatever you set base-url and web-port to, such as https://suros.emsenn.net:9000.

2.2.1.4 Shutdown

To shutdown your suros, evaluate (ws-stop-all). Which actually shuts down all web-servers. Whoops!

2.2.1.5 Repair

2.2.2 User Operations

2.3 Data

Your suros relies on at least having a configuration to look at, currently kept at suros-config. The other kind of data are its templates - collections of mustache templates that are filled in with data to make the rendered objects.

2.3.1 Suros Configuration

This is the configuration for your suros, for configuring the server and all information about you as a user.

(setq
 suros-config
 (ht
  ("actor-public-key" "-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAoo+p0r7lPkESdf9JQ/cw\nBFpQQd8NDddM++Q4mqROliq//8U7UBe9c7AQTfnRz3jc5zs8e+kL0L0tFywv2wwS\nDBp4hq5fQuyNMq/XebEveYjtu/Ckw5alcrtwKI+ArdJJ3rGaGzKLtNDRrxoc40u1\nWnGulzsxNzjoJy+OvPgjjdqaBB55k+oKL6jC/MI5WQN86FYEG/uMXkRSXIJbbB2M\nZMOmLjBi3VlVJ/p9robd7dImb4jhP/9YHQ/wFEzRe3ekD4KKPUkhpM63z1WjVh8n\nHPgulhB6OrAu0phwCozA3TZvg368GJKle6GdxHyLH3dnBvwue3AhyNOYXeVuK0fn\nhwIDAQAB\n-----END PUBLIC KEY-----")
  ("base-url" "suros-emsenn.pagekite.me")
  ("language-code" "en")
  ("make-things" '("words" . "computer instructions"))
  ("make-verb" "craft")
  ("preferred-name" "emsenn")
  ("proper-name" "emsenn")
  ("server-description" "an early-alpha personal webserver")
  ("server-name" "suros")
  ("server-source-link" "https://emsenn.neocities.org/suros.html")
  ("username" "emsenn")
  ("template-directory" "~/src/suros/mustache/")
  ("web-port" 9000)))
2.3.1.1 Configuration Variables
actor-public-key
The public key for your ActivityPub actor. Used by the Actor JSON template.
base-url
The URL that your suros will be deployed at. Used by the Actor JSON and Webfinger JSON templates.
language-code
The ISO 639-1 language code that your content may be assumed to be in, such as en. Used by the Document Declaration HTML template.
make-things
The list of things that you produce. Used by the Maker Brief text template template.
make-verb
The verb used to describe your means of production, such as craft, engineer, synthesise. Used by the Maker Brief text template.
preferred-name
The name you would prefer to be called in general company. Used by the Index Page html template.
proper-name
The name you'd prefer to be used in official contexts.
template-directory
The directory where suros will look for mustache templates.
user-name
The name you'd like to be used in URLs and such. Used by the Webfinger JSON template.
web-port
The port the web server will serve on.

2.3.2 Mustache Templates

Your suros uses Mustache templates to make many of its objects. Right now these templates are organized by rendered language and are named about what they do. As the number of templates increase, this will prove insufficient: this part of the code will get reorganized a lot.

2.3.2.1 CSS Mustache Templates

These Mustache templates are to make cascading style-sheets. Right now the only one is one for adding the Brutstrap template.

2.3.2.1.1 Brutstrap CSS Mustache Template

Brutstrap is one of my CSS themes. This is a copy of it I grabbed from somewhere in another repo. Currently, this template is used.

body {
position: relative;
background-color: #eee;
color: #444;
font-family: serif;
margin: 0 auto;
padding-bottom: 6rem;
min-height: 100%;
font-size: 1.2em;
}
header, h1 {
font-family: sans-serif;
text-align: left;
margin-left: 1em;
width: 80vw;
overflow: hidden;
font-family: sans-serif;
}
.title { font-size: 3.2rem; }
.subtitle { font-size: 2.2rem; }
main, #content {
width: 75vw;
max-width: 40em;
margin: 0 0 0 2em;
line-height: 1.6;
margin-bottom: 8rem;
}
footer, #postamble {
padding: 1em 0;
position: absolute;
right: 0;
bottom: 0;
left: 0;
}
section {
border-bottom: 0.1em solid #444;
margin-bottom: 1em;
}
.outline-3 {
border-bottom: 0.1rem dotted #444;
}
blockquote {
padding: 0.5rem;
border-left: 0.1em solid #444;
}
table {
border-collapse: collapse;
border-spacing: 0;
empty-cells: show;
border: 0.1em solid #444;
}
thead { font-family: sans-serif; }
th, td {
padding: 0 0.3em;
border: 0.01em solid #444;
}
img {
max-width: 80vw;
vertical-align: middle;
}
pre, .src {
padding: 0.5em;
border: 0.1em solid #444;
white-space: pre-wrap;
overflow-x: scroll;
text-overflow: clip;
}
dt {
font-weight: normal;
display: inline-block;
border-bottom: 0.1rem dotted #444;
}
#text-index dt { border-bottom: none; }
h2, h3, h4, h5, h6, h7, h8, h9, h10 {
display: block;
font-family: sans-serif;
margin: 0.5em;
}
h2 {
text-align: center;
}
h2 { font-size: 2em; }
h3 {
font-size: 1.8em;
}
h4 {
font-size: 1.6em;
}
h5 {
font-size: 1.4em;
margin: 0.5em 0 0.2em 10%;
}
h6 {
font-size: 1.2em;
margin: 0.5em 0 0.2em 30%;
}
a {
text-decoration: none;
color: #3ac;
display: inline;
position: relative;
border-bottom: 0.1rem dotted;
line-height: 1.2;
transition: border 0.3s;
}
a:hover {
color: #5ce;
outline-style: none;
border-bottom: 0.1rem solid;
}
a:visited { color: #b8c; }
a:visited:hover { color: #dae; }
a:focus {
outline-style: none;
border-bottom: 0.1rem solid;
}
::selection { background-color: #777; color: #eee; }
a::selection { background-color: #ccc; }
.todo {
background-color: #ddd;
color: #333;
font-size: .8rem;
float: right;
}
.org-src-container + .example {
margin-left: 8em;
}
label.org-src-name {
margin: 0 0 0 3em;
font-family: sans-serif;
font-size: 1rem;
line-height: 0;
}
.skipToContentLink {
opacity: 0;
position: absolute;
}
.skipToContentLink:focus{
opacity:1
}
2.3.2.2 JSON Mustache Templates

These Mustache templates are used to make JSON.

2.3.2.2.1 Actor JSON Mustache Template

The Actor template is used to render an ActivityPub actor. Uses the base-url configuration variable.

{
	"@context": [
		"https://www.w3.org/ns/activitystreams",
		"https://w3id.org/security/v1"
	],

	"id": "https://{{base-url}}/actor",
	"type": "Person",
	"preferredUsername": "{{username}}",
	"inbox": "https://{{base-url}}/inbox",

	"publicKey": {
		"id": "https://{{base-url}}/actor#main-key",
		"owner": "https://{{base-url}}/actor",
		"publicKeyPem": "{{actor-public-key}}"
	}

2.3.2.2.2 Webfinger JSON Mustache Template

The Webfinger JSON template is used to render webfinger information for your suros. Uses the base-url and user-name configuration variables.

{
	"subject": "acct:{{user-name}}@{{base-url}}",

	"links": [
		{
			"rel": "self",
			"type": "application/activity+json",
			"href": "https://{{base-url}}/actor"
		}
	]
}
2.3.2.3 HTML Mustache Templates
2.3.2.3.1 Document Declaration HTML Mustache Template

The Document Declaration HTML template is used to render the DOCTYPE and opening HTML tag. It is used by the Index Page HTML template.

<!DOCTYPE html>
<html
  xmlns="http://www.w3.org/1999/xhtml" {{#language-code}}
  xml:lang="{{language-code}}"
  lang="{{language-code}}"
  {{/language-code}}>
2.3.2.3.2 Index Page HTML Mustache Template

The Index Page HTML template is used to render your suros' homepage. It uses the Document Declaration, Head Meta, and Head Links HTML templates, and the preferred-name configuration variable.

{{> document-declaration.html}}
<head>
  {{> head-meta.html}}
  {{> head-links.html}}
</head>
<body>
  <a class="skipToContentLink" href="#content">Skip to Content</a>
  <header>
    <h1>{{preferred-name}}'s {{server-name}}</h1>
  </header>
  <section id="content">
    <p>
      My name is {{preferred-name}} and this is my <em>{{server-name}}</em>, {{server-description}}. See the source code and documentation at <a href="{{server-source-link}}">{{server-source-link}}</a>.
    </p>
  </section>
</body>
</html>
2.3.2.3.3 Contact Page HTML Mustache Template

The Contact Page

  <html>
    <body>
fdafdsfs
    </body>
  </html>
2.3.2.3.4 Head Meta HTML Mustache Template
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=5">
<meta name="referrer" content="no-referrer">
{{#object-description}}<meta name="description" content={{object-description}}">{{/object-description}}
2.3.2.4 Text Mustache Templates
2.3.2.4.1 Maker Brief Text Mustache Template
I {{#make-verb}}{{make-verb}}{{/make-verb}}{{^make-verb}}make{{/make-verb}}
{{#oxfordize-list}}fasfdasfd{{make-things}}{{/oxfordize-list}}

2.3.3 Template Paths

(setq
 suros-template-alias
 (ht
  ("index.html" '("index.html" . ""))
  ("actor.json" '("actor"))
  ("webfinger.json" '(".well-known/webfinger"))))

2.3.4 Builtin Paths

The hash-table suros-builtin-paths contains a list of objects and the paths at which they are available.

(setq
 suros-builtin-paths
 (ht
  ("actor"  '("actor"))
  ("plans" '("now" "plans"))
  ("support" '("support" "donate" "contribute"))))

2.3.5 Builtin Paths

(setq
 suros-builtin-objects
 (ht
("actor" '('suros-send-html-object "actor.json"))))

2.4 Functions

These functions are the main implementation of your suros - they use the templates and data above to do stuff.

2.4.1 Server Management Functions

2.4.1.1 Stop Server Function
(defun suros-stop ()
  "This function stops your suros."
  (interactive)
  (ws-stop suros-server-object))
2.4.1.2 Start Server Function

The function suros-start starts your suros server.

(setq suros-server-object nil)
  (defun suros-start ()
    "This function starts your suros."
    (interactive)
    (let ((config suros-config))
      (if (suros-validate-config config)
          (setq suros-server-object
                (ws-start
                 '(((:POST . ".*") .
                    (lambda (request) (suros-read-post-request request)))
                   ((:GET . ".*") .
                    (lambda (request) (suros-read-get-request request)))
                   ((:HEAD . ".*") .
                    (lambda (request)
                      (with-slots (process headers) request
                        (ws-response-header process 200 '("Content-type" . "text/plain"))))))
                 (ht-get suros-config "web-port")
                 (get-buffer-create "suros-log"))))))

More precisely, it:

GET requests are when a client is requesting a resource, POST requests are when they're trying to tell your suros something, and HEAD checks are for diagnosis.

2.4.2 Request Management Functions

2.4.2.1 Read GET Request Function

The suros-read-get-request function is the handler for GET requests your suros receives.

(defun suros-read-get-request (request)
  (with-slots (process headers) request
    (let ((path (substring (cdr (assoc :GET headers)) 1))
          (context suros-config))
      (if (equal path "")
          (suros-send-html-object process (suros-render "index.html" headers context))
        (if (ht-contains? (suros-flippen-flatten-ht suros-builtin-objects) path)
            (ht-get (suros-flippen-flatten-ht suros-builtin-objects) path)
          (suros-send-html-object process (concat "No page at " path ". AKA a 404")))))))

In order to determine what to do with the request, your suros looks at the request's path, which is (in practice) the bit of the url between the base URL and the query. Given https://suros.emsenn.net/.well-known/webfinger?resource=acct:emsenn@suros.emsenn.net, the path is .well-known/webfinger.

If the path is empty, your suros sends an HTML object back, rendered from the Index Page HTML template.

Otherwise, it checks the list of [[#builtin-objects][

2.4.2.2 Read POST Request Function
(defun suros-read-post-request (request)
  (with-slots (process headers) request
    (process-send-string process "suros has read your POST request.")))
2.4.2.3 Read HEAD Request Function
(defun suros-read-head-request (request)
  (with-slots (process headers) request
    (ws-response-header process 200 '("Content-type" . "text/plain"))))

2.4.3 Send Object Functions

2.4.3.0.1 Send HTML Object Function

The function suros-send-html sends rendering and appropriate headers to process - the client requesting the object.

(defun suros-send-html-object (process rendering)
  (ws-response-header process 200 '("Content-type" . "text/html"))
  (process-send-string process rendering))
2.4.3.0.2 Send JSON Object Function

The function suros-send-html sends rendering to process - the client requesting the object.

(defun suros-send-json-object (process rendering)
  (ws-response-header process 200 '("Content-type" . "application/json"))
  (process-send-string process rendering))

2.4.4 Processing Functions

2.4.4.1 Render Object Function

The function suros-render returns a rendering of the template Mustache template, given the context. #+suros-render-elisp

(defun suros-render (template headers context)
  "Returns the rendering of TEMPLATE given CONTEXT."
  (let ((mustache-partial-paths (list (gethash "template-directory" suros-config)))
        (template (with-temp-buffer (insert-file-contents
                                     (concat (gethash "template-directory" suros-config) template ".mustache"))
                            (buffer-string))))
        (mustache-render template context)))
2.4.4.2 Send Webfinger Function

The function suros-send-webfinger is incomplete and unused, but will be for returning a given webfinger identity.

(defun suros-send-webfinger (process headers)
  (suros-send-json process (suros-render "webfinger.json")))

2.4.5 Utility Functions

2.4.5.1 Template Predicate Function

The function suros-template-p returns t if template is a Mustache template your suros knows about.

(defun suros-template-p (template)
  "Returns true if there is a file at the given template name. Does no checking of validity."
    (file-exists-p (concat (gethash "template-directory" suros-config) template ".mustache")))
2.4.5.2 Validate Configuration Function

The function suros-validate-config returns t no matter what. It is not good at its job.

(defun suros-validate-config (config)
  "Returns true. Pretty rubbish validator."
  t)
2.4.5.3 Flippen Flatten Function

The function suros-flippen-flatten-ht iterates over a hashtable where each value is assumed to be a list, and returns a new hashtable where each key is an element of those lists, and each value is the corresponding key.

So, given (ht (a '(b)) (c '(d e)) (f '(g h i))), returns (ht (b a) (d c) (e c) (g f) (h f) (i f)).

(defun suros-flippen-flatten-ht (htable)
  (ht<-alist (-flatten-n 1 (append
                            (ht-map (lambda (key vals)
                                      (mapcar (lambda (val) (cons val key)) vals))
                                    htable)))))

(Credit to @comrade_parsley@mastodon.social and B. Slade for writing this function.)

3 Appendices

3.1 Suros.el Package

;;; suros.el --- a personal web server

;; made in the United States by emsenn

;; Author: emsenn <emsenn@emsenn.net>
;; Keywords: server, ActivityPub
;; Version: 0.1.0

;; This program was made by emsenn in the United States.
;; To the extent possible under law, they have waived all
;; copyright and related or neighboring rights to this program.
;; This program was made for the good of the commons.

;;; Commentary:

;; Suros.el provides a web server that provides objects rendered
;; from Elisp data and Org-mode files.

;;; Code:

;; Suros Data
;; Customize this, carefully, by hand.
(setq
 suros-config
 (ht
  ("actor-public-key" "-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAoo+p0r7lPkESdf9JQ/cw\nBFpQQd8NDddM++Q4mqROliq//8U7UBe9c7AQTfnRz3jc5zs8e+kL0L0tFywv2wwS\nDBp4hq5fQuyNMq/XebEveYjtu/Ckw5alcrtwKI+ArdJJ3rGaGzKLtNDRrxoc40u1\nWnGulzsxNzjoJy+OvPgjjdqaBB55k+oKL6jC/MI5WQN86FYEG/uMXkRSXIJbbB2M\nZMOmLjBi3VlVJ/p9robd7dImb4jhP/9YHQ/wFEzRe3ekD4KKPUkhpM63z1WjVh8n\nHPgulhB6OrAu0phwCozA3TZvg368GJKle6GdxHyLH3dnBvwue3AhyNOYXeVuK0fn\nhwIDAQAB\n-----END PUBLIC KEY-----")
  ("base-url" "suros-emsenn.pagekite.me")
  ("language-code" "en")
  ("make-things" '("words" . "computer instructions"))
  ("make-verb" "craft")
  ("preferred-name" "emsenn")
  ("proper-name" "emsenn")
  ("server-description" "an early-alpha personal webserver")
  ("server-name" "suros")
  ("server-source-link" "https://emsenn.neocities.org/suros.html")
  ("username" "emsenn")
  ("template-directory" "~/src/suros/mustache/")
  ("web-port" 9000)))

;; Suros Functions
;; Probably don't touch these.
(setq suros-server-object nil)
  (defun suros-start ()
    "This function starts your suros."
    (interactive)
    (let ((config suros-config))
      (if (suros-validate-config config)
          (setq suros-server-object
                (ws-start
                 '(((:POST . ".*") .
                    (lambda (request) (suros-read-post-request request)))
                   ((:GET . ".*") .
                    (lambda (request) (suros-read-get-request request)))
                   ((:HEAD . ".*") .
                    (lambda (request)
                      (with-slots (process headers) request
                        (ws-response-header process 200 '("Content-type" . "text/plain"))))))
                 (ht-get suros-config "web-port")
                 (get-buffer-create "suros-log"))))))
(defun suros-stop ()
  "This function stops your suros."
  (interactive)
  (ws-stop suros-server-object))
(defun suros-read-get-request (request)
  (with-slots (process headers) request
    (let ((path (substring (cdr (assoc :GET headers)) 1))
          (context suros-config))
      (if (equal path "")
          (suros-send-html-object process (suros-render "index.html" headers context))
        (if (ht-contains? (suros-flippen-flatten-ht suros-builtin-objects) path)
            (ht-get (suros-flippen-flatten-ht suros-builtin-objects) path)
          (suros-send-html-object process (concat "No page at " path ". AKA a 404")))))))
(defun suros-read-post-request (request)
  (with-slots (process headers) request
    (process-send-string process "suros has read your POST request.")))
(defun suros-read-head-request (request)
  (with-slots (process headers) request
    (ws-response-header process 200 '("Content-type" . "text/plain"))))
(defun suros-send-html-object (process rendering)
  (ws-response-header process 200 '("Content-type" . "text/html"))
  (process-send-string process rendering))
(defun suros-send-json-object (process rendering)
  (ws-response-header process 200 '("Content-type" . "application/json"))
  (process-send-string process rendering))

(defun suros-template-p (template)
  "Returns true if there is a file at the given template name. Does no checking of validity."
    (file-exists-p (concat (gethash "template-directory" suros-config) template ".mustache")))
(defun suros-validate-config (config)
  "Returns true. Pretty rubbish validator."
  t)
(defun suros-flippen-flatten-ht (htable)
  (ht<-alist (-flatten-n 1 (append
                            (ht-map (lambda (key vals)
                                      (mapcar (lambda (val) (cons val key)) vals))
                                    htable)))))
;; End of Suros Code
(provide 'suros)
;;; suros.el ends here

3.2 Changelog

3.2.1 [0%] Upcoming

  • [ ] Take my elisp serialized data and present it as html and json objects
    • [ ] Map out the "take a request and get an object" process, hacking at it isn't working.
  • [ ] take registered org-mode files and serve those up as objects
  • [ ] re-serialize my elisp data as org-mode files
  • [ ] save logs and incoming data as org-mode files

3.2.2 Recent

Footnotes:

1

The free tool OpenSSL can be used to generate an RSA keypair. From a command line: openssl genrsa -out private.pem 2048 && openssl rsa -in private.pem -outform PEM -pubout -out public.pem=. This will create a new keypair in the current directory. The contents of public.pem is what should be used for actor-public-key, however you must replace the extant line-breaks with \n.

Author: emsenn

Created: 2019-07-20 Sat 22:02

Validate