1 Commits

Author SHA1 Message Date
Alex Schroeder
ce6af76f69 Switch to Markdown, rewrite README 2025-06-07 22:37:10 +02:00
10 changed files with 40 additions and 391 deletions

1
.gitignore vendored
View File

@@ -1 +0,0 @@
/oddmu

View File

@@ -1,28 +0,0 @@
SHELL=/bin/bash
help:
@echo Help for Oddmu
@echo =====================
@echo
@echo make run
@echo " runs program, offline"
@echo
@echo make test
@echo " runs the tests"
@echo
@echo make upload
@echo " this is how I upgrade my server"
@echo
@echo go build
@echo " just build it"
run:
go run .
test:
go test
upload:
go build
rsync --itemize-changes --archive oddmu oddmu.service *.html README.md sibirocobombus.root:/home/oddmu/
ssh sibirocobombus.root "systemctl restart oddmu"

230
README.md
View File

@@ -1,222 +1,22 @@
# Oddµ: A minimal wiki
# Oddµ: A Small Wiki Written in Go
This program runs a wiki. It serves all the Markdown files (ending in
`.md`) into web pages and allows you to edit them.
This is a minimal wiki in the spirit of [Shortest Wiki Contest](https://wiki.c2.com/?ShortestWikiContest)
and [Wiki Principles](https://wiki.c2.com/?WikiPrinciples).
The article [Writing Web Applications](https://golang.org/doc/articles/wiki/)
provided the initial code. It's how I learned Go.
This is a minimal wiki. There is no version history. It probably makes
sense to only use it as one person or in very small groups.
To get started, use `go run .` to compile and run it; then use your browser to visit
`http://localhost:8080/edit/index`.
It's very minimal and only uses Markdown. No wiki extras, so double
square brackets are not a link. If you're used to that, it'll be
strange as you need to repeat the name: `[like this](like this)`.
## Some context
## Building
Initially, this wiki used [Cajun](https://github.com/m4tty/cajun) to render Wiki Creole
but I switched it to [Markdown](https://github.com/gomarkdown/markdown).
```sh
go build
```
At the time, I planned to use [git2go](https://github.com/libgit2/git2go) to implement
history pages, recent changes, reverting to old versions. None of that ever came about,
however.
## Test
Many years later I did use it as the basis for [Oddμ](https://alexschroeder.ch/view/oddmu/index),
however.
```sh
mkdir wiki
cd wiki
go run ..
```
The program serves the local directory as a wiki on port 8080. Point
your browser to http://localhost:8080/ to get started. This is
equivalent to http://localhost:8080/view/index the first page
you'll create, most likely.
If you ran it in the source directory, try
http://localhost:8080/view/README this serves the README file you're
currently reading.
## Deploying it using systemd
As root:
```sh
# on your server
adduser --system --home /home/oddmu oddmu
```
Copy all the files into `/home/oddmu` on your server: `oddmu`, `oddmu.service`, `view.html` and `edit.html`.
Set the ODDMU_PORT environment variable in the `oddmu.service` file (or accept the default, 8080).
Install the service file and enable it:
```sh
ln -s /home/oddmu/oddmu.service /etc/systemd/system/
systemctl enable --now oddmu
```
Check the log:
```sh
journalctl --unit oddmu
```
Follow the log:
```sh
journalctl --follow --unit oddmu
```
Edit the first page using `lynx`:
```sh
lynx http://localhost:8080/view/index
```
## Web Server Setup
HTTPS is not part of the wiki. You probably want to configure this in
your webserver. If you're using Apache, you might have set up a site
like the following. In my case, that'd be
`/etc/apache2/sites-enabled/500-transjovian.conf`:
```apache
MDomain transjovian.org
MDCertificateAgreement accepted
<VirtualHost *:80>
ServerName transjovian.org
RewriteEngine on
RewriteRule ^/(.*) https://%{HTTP_HOST}/$1 [redirect]
</VirtualHost>
<VirtualHost *:443>
ServerAdmin alex@alexschroeder.ch
ServerName transjovian.org
SSLEngine on
RewriteEngine on
RewriteRule ^/$ http://%{HTTP_HOST}:8080/view/index [redirect]
RewriteRule ^/(view|edit|save)/(.*) http://%{HTTP_HOST}:8080/$1/$2 [proxy]
</VirtualHost>
```
First, it manages the domain, getting the necessary certificates. It
redirects regular HTTP traffic from port 80 to port 443. It turns on
the SSL engine for port 443. It redirects `/` to `/view/index` and any
path that starts with `/view/`, `/edit/` or `/save/` is proxied to
port 8080 where the Oddmu program can handle it.
Thus, this is what happens:
* The user tells the browser to visit `http://transjovian.org` (on port 80)
* Apache redirects this to `http://transjovian.org/` by default (still on port 80)
* Our first virtual host redirects this to `https://transjovian.org/` (encrypted, on port 443)
* Our second virtual host redirects this to `https://transjovian.org/wiki/view/index` (still on port 443)
* This is proxied to `http://transjovian.org:8080/view/index` (no on port 8080, without encryption)
* The wiki converts `index.md` to HTML, adds it to the template, and serves it.
## Access
Access control is not part of the wiki. By default, the wiki is
editable by all. This is most likely not what you want unless you're
running it stand-alone, unconnected to the Internet.
You probably want to configure this in your webserver. If you're using
Apache, you might have set up a site like the following.
Create a new password file called `.htpasswd` and add the user "alex":
```sh
cd /home/oddmu
htpasswd -c .htpasswd alex
```
To add more users, don't use the `-c` option or you will overwrite it!
To add another user:
```sh
htpasswd .htpasswd berta
```
To delete remove a user:
```sh
htpasswd -D .htpasswd berta
```
Modify your site configuration and protect the `/edit/` and `/save/`
URLs with a password by adding the following to your `<VirtualHost
*:443>` section:
```apache
<LocationMatch "^/(edit|save)/">
AuthType Basic
AuthName "Password Required"
AuthUserFile /home/oddmu/.htpasswd
Require valid-user
</LocationMatch>
```
## Configuration
Feel free to change the templates `view.html` and `edit.html` and
restart the server. Modifying the styles in the templates would be a
good start.
You can remove the auto-generated titles from the files, for example.
If your Markdown files start with a level 1 title, then edit
`view.html` and remove the line that says `<h1>{{.Title}}</h1>` (this
is what people see when reading the page). Optionally also remove the
line that says `<title>{{.Title}}</title>` (this is what gets used for
tabs and bookmarks).
If you want to serve static files as well, add a document root to your
webserver configuration. Using Apache, for example:
```apache
DocumentRoot /home/oddmu/static
<Directory /home/oddmu/static>
Require all granted
</Directory>
```
Create this directory, making sure to give it a permission that your
webserver can read (world readable file, world readable and executable
directory). Populate it with files. For example, create a file called
`robots.txt` containing the following, tellin all robots that they're
not welcome.
```text
User-agent: *
Disallow: /
```
You site now serves `/robots.txt` without interfering with the wiki,
and without needing a wiki page.
[Wikipedia](https://en.wikipedia.org/wiki/Robot_exclusion_standard)
has more information.
## Customization (with recompilation)
The Markdown parser can be customized and
[extensions](https://pkg.go.dev/github.com/gomarkdown/markdown/parser#Extensions)
can be added. There's an example in the
[usage](https://github.com/gomarkdown/markdown#usage) section. You'll
need to make changes to the `viewHandler` yourself.
## Limitations
Page titles are filenames with `.md` appended. If your filesystem
cannot handle it, it can't be a page title. Specifically, *no slashes*
in filenames.
## References
[Writing Web Applications](https://golang.org/doc/articles/wiki/)
provided the initial code for this wiki.
For the proxy stuff, see
[Apache: mod_proxy](https://httpd.apache.org/docs/current/mod/mod_proxy.html).
For the usernames and password stuff, see
[Apache: Authentication and Authorization](https://httpd.apache.org/docs/current/howto/auth.html).

View File

@@ -1,21 +1,10 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="format-detection" content="telephone=no">
<meta name="viewport" content="width=device-width">
<title>Editing {{.Title}}</title>
<style>
html { max-width: 70ch; padding: 2ch; margin: auto; color: #111; background: #ffe; }
form, textarea { width: 100%; }
</style>
</head>
<body>
<h1>Editing {{.Title}}</h1>
<form action="/save/{{.Title}}" method="POST">
<div><textarea name="body" rows="20" cols="80">{{printf "%s" .Body}}</textarea></div>
<p><input type="submit" value="Save"></p>
</form>
</body>
<body>
<h1>Editing {{.Title}}</h1>
<form action="/save/{{.Title}}" method="POST">
<div><textarea name="body" rows="20" cols="80">{{printf "%s" .Body}}</textarea></div>
<div><input type="submit" value="Save"></div>
</form>
</body>
</html>

13
go.mod
View File

@@ -1,14 +1,5 @@
module alexschroeder.ch/cgit/oddmu
go 1.21.0
go 1.22.3
require (
github.com/gomarkdown/markdown v0.0.0-20230716120725-531d2d74bc12
github.com/microcosm-cc/bluemonday v1.0.25
)
require (
github.com/aymerick/douceur v0.2.0 // indirect
github.com/gorilla/css v1.0.0 // indirect
golang.org/x/net v0.12.0 // indirect
)
require github.com/gomarkdown/markdown v0.0.0-20250311123330-531bef5e742b // indirect

14
go.sum
View File

@@ -1,10 +1,4 @@
github.com/aymerick/douceur v0.2.0 h1:Mv+mAeH1Q+n9Fr+oyamOlAkUNPWPlA8PPGR0QAaYuPk=
github.com/aymerick/douceur v0.2.0/go.mod h1:wlT5vV2O3h55X9m7iVYN0TBM0NH/MmbLnd30/FjWUq4=
github.com/gomarkdown/markdown v0.0.0-20230716120725-531d2d74bc12 h1:uK3X/2mt4tbSGoHvbLBHUny7CKiuwUip3MArtukol4E=
github.com/gomarkdown/markdown v0.0.0-20230716120725-531d2d74bc12/go.mod h1:JDGcbDT52eL4fju3sZ4TeHGsQwhG9nbDV21aMyhwPoA=
github.com/gorilla/css v1.0.0 h1:BQqNyPTi50JCFMTw/b67hByjMVXZRwGha6wxVGkeihY=
github.com/gorilla/css v1.0.0/go.mod h1:Dn721qIggHpt4+EFCcTLTU/vk5ySda2ReITrtgBl60c=
github.com/microcosm-cc/bluemonday v1.0.25 h1:4NEwSfiJ+Wva0VxN5B8OwMicaJvD8r9tlJWm9rtloEg=
github.com/microcosm-cc/bluemonday v1.0.25/go.mod h1:ZIOjCQp1OrzBBPIJmfX4qDYFuhU02nx4bn030ixfHLE=
golang.org/x/net v0.12.0 h1:cfawfvKITfUsFCeJIHJrbSxpeu/E81khclypR0GVT50=
golang.org/x/net v0.12.0/go.mod h1:zEVYFnQC7m/vmpQFELhcD1EWkZlX69l4oqgmer6hfKA=
github.com/gomarkdown/markdown v0.0.0-20250311123330-531bef5e742b h1:EY/KpStFl60qA17CptGXhwfZ+k1sFNJIUNR8DdbcuUk=
github.com/gomarkdown/markdown v0.0.0-20250311123330-531bef5e742b/go.mod h1:JDGcbDT52eL4fju3sZ4TeHGsQwhG9nbDV21aMyhwPoA=
github.com/m4tty/cajun v0.0.0-20150303030909-35de273cc87b h1:aY3LtSBlkQahoWaPTytHcIFsDbeXFYMc4noRQ/N5Q+A=
github.com/m4tty/cajun v0.0.0-20150303030909-35de273cc87b/go.mod h1:zFXkL7I5vIwKg4dxEA9025SLdIHu9qFX/cYTdUcusHc=

View File

@@ -1,3 +0,0 @@
Hello! 🙃
Check out the [README](README).

View File

@@ -1,50 +0,0 @@
[Unit]
Description=Oddmu
After=network.target
[Install]
WantedBy=multi-user.target
[Service]
Type=simple
Restart=always
DynamicUser=true
MemoryMax=100M
MemoryHigh=120M
ExecStart=/home/oddmu/oddmu
WorkingDirectory=/home/oddmu
Environment="ODDMU_PORT=8080"
# (man "systemd.exec")
ReadWritePaths=/home/oddmu
ProtectHostname=yes
RestrictSUIDSGID=yes
UMask=0077
RemoveIPC=yes
MemoryDenyWriteExecute=yes
# Sandboxing options to harden security
# Details for these options: https://www.freedesktop.org/software/systemd/man/systemd.exec.html
NoNewPrivileges=yes
PrivateTmp=yes
PrivateDevices=yes
RestrictAddressFamilies=AF_UNIX AF_INET AF_INET6
RestrictNamespaces=yes
RestrictRealtime=yes
DevicePolicy=closed
ProtectSystem=full
ProtectControlGroups=yes
ProtectKernelModules=yes
ProtectKernelTunables=yes
LockPersonality=yes
SystemCallFilter=~@clock @debug @module @mount @obsolete @reboot @setuid @swap
# Denying access to capabilities that should not be relevant
# Doc: https://man7.org/linux/man-pages/man7/capabilities.7.html
CapabilityBoundingSet=~CAP_RAWIO CAP_MKNOD
CapabilityBoundingSet=~CAP_AUDIT_CONTROL CAP_AUDIT_READ CAP_AUDIT_WRITE
CapabilityBoundingSet=~CAP_SYS_BOOT CAP_SYS_TIME CAP_SYS_MODULE CAP_SYS_PACCT
CapabilityBoundingSet=~CAP_LEASE CAP_LINUX_IMMUTABLE CAP_IPC_LOCK
CapabilityBoundingSet=~CAP_BLOCK_SUSPEND CAP_WAKE_ALARM
CapabilityBoundingSet=~CAP_SYS_TTY_CONFIG
CapabilityBoundingSet=~CAP_MAC_ADMIN CAP_MAC_OVERRIDE
CapabilityBoundingSet=~CAP_NET_ADMIN CAP_NET_BROADCAST CAP_NET_RAW
CapabilityBoundingSet=~CAP_SYS_ADMIN CAP_SYS_PTRACE CAP_SYSLOG

View File

@@ -1,19 +1,7 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="format-detection" content="telephone=no">
<meta name="viewport" content="width=device-width">
<title>{{.Title}}</title>
<style>
html { max-width: 70ch; padding: 2ch; margin: auto; color: #111; background: #ffe; }
</style>
</head>
<body>
<h1>{{.Title}}</h1>
<p><a href="/edit/{{.Title}}">Edit this page</a></p>
<div>
<body>
{{.Html}}
</div>
</body>
<nav><hr><a href="/edit/{{.Title}}">edit</a></nav>
</body>
</html>

49
wiki.go
View File

@@ -1,20 +1,16 @@
package main
import (
"os"
"fmt"
"github.com/microcosm-cc/bluemonday"
"github.com/gomarkdown/markdown/parser"
"github.com/gomarkdown/markdown/html"
"github.com/gomarkdown/markdown"
"html/template"
"net/http"
"os"
"regexp"
"github.com/gomarkdown/markdown"
)
var templates = template.Must(template.ParseFiles("edit.html", "view.html"))
var validPath = regexp.MustCompile("^/(edit|save|view)/([^/]+)$")
var validPath = regexp.MustCompile("^/(edit|save|view)/([a-zA-Z0-9]+)$")
type Page struct {
Title string
@@ -30,19 +26,13 @@ func (p *Page) save() error {
func loadPage(title string) (*Page, error) {
filename := title + ".md"
body, err := os.ReadFile(filename)
if err == nil {
return &Page{Title: title, Body: body}, nil
if err != nil {
return nil, err
}
filename = title + ".gmi"
body, err = os.ReadFile(filename)
if err == nil {
re := regexp.MustCompile(`(?m)^=>\s*(\S+)\s+(.+)`)
body = []byte(re.ReplaceAllString(string(body), `* [$2]($1)`))
return &Page{Title: title, Body: body}, nil
}
return nil, err
return &Page{Title: title, Body: body}, nil
}
func renderTemplate(w http.ResponseWriter, tmpl string, p *Page) {
err := templates.ExecuteTemplate(w, tmpl+".html", p)
if err != nil {
@@ -50,9 +40,6 @@ func renderTemplate(w http.ResponseWriter, tmpl string, p *Page) {
}
}
func rootHandler(w http.ResponseWriter, r *http.Request) {
http.Redirect(w, r, "/view/index", http.StatusFound)
}
func viewHandler(w http.ResponseWriter, r *http.Request, title string) {
p, err := loadPage(title)
@@ -60,15 +47,7 @@ func viewHandler(w http.ResponseWriter, r *http.Request, title string) {
http.Redirect(w, r, "/edit/"+title, http.StatusFound)
return
}
extensions := parser.CommonExtensions | parser.NoEmptyLineBeforeBlock
markdownParser := parser.NewWithExtensions(extensions)
flags := html.CommonFlags
opts := html.RendererOptions{
Flags: flags,
}
htmlRenderer := html.NewRenderer(opts)
maybeUnsafeHTML := markdown.ToHTML(p.Body, markdownParser, htmlRenderer)
html := bluemonday.UGCPolicy().SanitizeBytes(maybeUnsafeHTML)
html := markdown.ToHTML([]byte(p.Body), nil, nil)
p.Html = template.HTML(html);
renderTemplate(w, "view", p)
}
@@ -103,21 +82,11 @@ func makeHandler(fn func (http.ResponseWriter, *http.Request, string)) http.Hand
}
}
func getPort() string {
port := os.Getenv("ODDMU_PORT")
if port == "" {
port = "8080"
}
return port
}
func main() {
http.HandleFunc("/", rootHandler)
http.HandleFunc("/view/", makeHandler(viewHandler))
http.HandleFunc("/edit/", makeHandler(editHandler))
http.HandleFunc("/save/", makeHandler(saveHandler))
port := getPort()
fmt.Println("Serving a wiki on port " + port)
http.ListenAndServe(":" + port, nil)
http.ListenAndServe(":8080", nil)
}