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
- 2. Suros
- 2.1. Architecture
- 2.2. Operations
- 2.3. Data
- 2.4. Functions
- 3. Appendices
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 isuser
. base-url
- In
@user@base.url
, this isbase.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.3.5 Head Links HTML Mustache Template
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:
- Validates
suros-config
- Binds a web server to the
web-port
configuration variable - Starts logging to a
suros-log
buffer. - Listens for various requests and passes them to the appropriate handler:
GET
requests are sent tosuros-read-get-request
POST
requests are sent tosuros-read-post-request
HEAD
requests are sent tosuros-read-head-request
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:
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
.