15 Commits

Author SHA1 Message Date
Alex Schroeder
643b33f4a3 Render gemtext files 2023-08-22 08:36:40 +02:00
Alex Schroeder
cf6190f4fc Add style to the edit template 2023-08-21 22:59:09 +02:00
Alex Schroeder
18060c607d Add two meta elements to the templates
Format detection telephone=no. What an aggravating idea. Every single
website has to add this, now? Is that going to be way forward into the
future? For every single future misfeature?

Viewport width=device-with. Same rant.
2023-08-21 22:50:09 +02:00
Alex Schroeder
e9304cd3d4 Shorter Apache config 2023-08-21 22:23:45 +02:00
Alex Schroeder
e68eda4f27 More about Apache config 2023-08-21 22:21:59 +02:00
Alex Schroeder
73eb4837e9 Add minimal stylesheet 2023-08-21 22:06:48 +02:00
Alex Schroeder
b661655d4a Update to the documentation 2023-08-21 22:05:58 +02:00
Alex Schroeder
8cfb622937 Mention how to add extensions to the Markdown parser 2023-08-21 22:05:37 +02:00
Alex Schroeder
f359ba5c76 Redirect the root page 2023-08-21 21:52:34 +02:00
Alex Schroeder
51fc0fbedb Support ODDMU_PORT 2023-08-21 21:30:02 +02:00
Alex Schroeder
3cc39054bb How to provide your own H1 2023-08-21 21:24:03 +02:00
Alex Schroeder
d45c583f6e Upload the README.md 2023-08-21 21:23:52 +02:00
Alex Schroeder
d7257ffa38 Markup 2023-08-21 21:17:43 +02:00
Alex Schroeder
f3743d8e60 Allow all characters except for the slash 2023-08-21 21:15:12 +02:00
Alex Schroeder
e57002b40c Big changes!
Use Markdown instead of Wiki Creole.
Better templates.
Much better documentation!
2023-08-21 16:49:25 +02:00
10 changed files with 407 additions and 38 deletions

1
.gitignore vendored Normal file
View File

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

28
Makefile Normal file
View File

@@ -0,0 +1,28 @@
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"

229
README.md
View File

@@ -1,15 +1,222 @@
# Oddµ: A Small Wiki Written in Go
# Oddµ: A minimal wiki
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. There is no version history. It probably makes
sense to only use it as one person or in very small groups.
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)`.
## Building
```sh
go build
```
## Test
```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
I'm the maintainer of [Oddmuse](http://oddmuse.org/), a wiki written
in Perl 5. As we wait for Perl 6 to come around, I wanted to
experiment with [Go](https://golang.org/). The article
[Writing Web Applications](https://golang.org/doc/articles/wiki/)
provided the initial code. [Cajun](https://github.com/m4tty/cajun)
provides the rendering engine for
[Wiki Creole](http://www.wikicreole.org/), the "standard" markup for
wikis.
provided the initial code for this wiki.
## Plans
For the proxy stuff, see
[Apache: mod_proxy](https://httpd.apache.org/docs/current/mod/mod_proxy.html).
Add [git2go](https://github.com/libgit2/git2go) with all that entails:
history pages, recent changes, reverting to old versions.
For the usernames and password stuff, see
[Apache: Authentication and Authorization](https://httpd.apache.org/docs/current/howto/auth.html).

View File

@@ -1,6 +1,21 @@
<h1>Editing {{.Title}}</h1>
<!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>
<div><input type="submit" value="Save"></div>
</form>
<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>
</html>

14
go.mod Normal file
View File

@@ -0,0 +1,14 @@
module alexschroeder.ch/cgit/oddmu
go 1.21.0
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
)

10
go.sum Normal file
View File

@@ -0,0 +1,10 @@
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=

3
index.md Normal file
View File

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

50
oddmu.service Normal file
View File

@@ -0,0 +1,50 @@
[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,5 +1,19 @@
<h1>{{.Title}}</h1>
<p>[<a href="/edit/{{.Title}}">edit</a>]</p>
<div>{{.Html}}</div>
<!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>
{{.Html}}
</div>
</body>
</html>

61
wiki.go
View File

@@ -1,16 +1,20 @@
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"
"io/ioutil"
"net/http"
"regexp"
"github.com/m4tty/cajun"
)
var templates = template.Must(template.ParseFiles("edit.html", "view.html"))
var validPath = regexp.MustCompile("^/(edit|save|view)/([a-zA-Z0-9]+)$")
var validPath = regexp.MustCompile("^/(edit|save|view)/([^/]+)$")
type Page struct {
Title string
@@ -19,20 +23,26 @@ type Page struct {
}
func (p *Page) save() error {
filename := p.Title + ".txt"
return ioutil.WriteFile(filename, p.Body, 0600)
filename := p.Title + ".md"
return os.WriteFile(filename, p.Body, 0600)
}
func loadPage(title string) (*Page, error) {
filename := title + ".txt"
body, err := ioutil.ReadFile(filename)
if err != nil {
return nil, err
filename := title + ".md"
body, err := os.ReadFile(filename)
if err == nil {
return &Page{Title: title, Body: body}, nil
}
return &Page{Title: title, Body: body}, nil
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
}
func renderTemplate(w http.ResponseWriter, tmpl string, p *Page) {
err := templates.ExecuteTemplate(w, tmpl+".html", p)
if err != nil {
@@ -40,6 +50,9 @@ 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)
@@ -47,12 +60,16 @@ func viewHandler(w http.ResponseWriter, r *http.Request, title string) {
http.Redirect(w, r, "/edit/"+title, http.StatusFound)
return
}
html, err := cajun.Transform(string(p.Body))
if err != nil {
// not sure what to do?
} else {
p.Html = template.HTML(html);
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)
p.Html = template.HTML(html);
renderTemplate(w, "view", p)
}
@@ -86,11 +103,21 @@ 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))
http.ListenAndServe(":8080", nil)
port := getPort()
fmt.Println("Serving a wiki on port " + port)
http.ListenAndServe(":" + port, nil)
}