forked from mirror/oddmu
Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ce6af76f69 |
1
.gitignore
vendored
1
.gitignore
vendored
@@ -1 +0,0 @@
|
||||
/oddmu
|
||||
28
Makefile
28
Makefile
@@ -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
230
README.md
@@ -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).
|
||||
|
||||
25
edit.html
25
edit.html
@@ -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
13
go.mod
@@ -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
14
go.sum
@@ -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=
|
||||
|
||||
@@ -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
|
||||
18
view.html
18
view.html
@@ -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
49
wiki.go
@@ -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)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user