13 Commits
v1.3 ... v1.4

Author SHA1 Message Date
Alex Schroeder
b64d56a648 Document a curious bug with the Apache configuration 2024-01-27 21:27:17 +01:00
Alex Schroeder
ce64d04dde Switch to 70ch max-width for all 2024-01-17 22:09:11 +01:00
Alex Schroeder
de5bd2d23e Remove restore and simplify cleanup
The test setups are now simplified, since no files in the source
directory are modified.
2024-01-17 14:15:07 +01:00
Alex Schroeder
29842fe685 Major change in how notifications work 2024-01-17 14:10:07 +01:00
Alex Schroeder
4042be68f3 Reformatting of documentation, man page generation
Fill column is 80, no double spaces after sentence endings.
2024-01-17 14:06:05 +01:00
Christopher Brannon
87846e15b9 Support socket activation.
This also removes the Unix socket support added yesterday.  To use
Unix sockets, use socket activation and let systemd manage the socket.
2024-01-17 13:56:05 +01:00
Alex Schroeder
1390d82e29 Update man page 2024-01-16 14:02:32 +01:00
Alex Schroeder
bb5bd1c629 Small comment fix
Reformatted comment for 120 characters. Note that the presence of a
forward-slash anywhere in ODDMU_ADDRESS makes this a Unix-domain
socket.
2024-01-16 13:46:09 +01:00
Christopher Brannon
777c498700 Allow specifying the listen address.
This patch also adds support for HTTP over a Unix-domain socket.
2024-01-16 13:43:01 +01:00
Alex Schroeder
803025f56a Fix test for new year 2024-01-16 13:43:01 +01:00
Alex Schroeder
dce66ec5a1 Silence output of static command for tests 2024-01-16 13:43:01 +01:00
Alex Schroeder
b6d596cb08 Improve man page for static subcommand
Discuss what it means to have some files hard linked.
Discuss the use of ODDMU_LANGUAGES to speed site generation up.
2024-01-10 07:37:33 +01:00
Alex Schroeder
3be26b9af1 Load languages before generating static site
Without it, all HTML files have lang="".

Also added a progress indication because generating the site takes a
lot longer, now.
2024-01-10 07:20:47 +01:00
23 changed files with 663 additions and 195 deletions

View File

@@ -48,30 +48,26 @@ It's not `)}
}
func TestAddAppendChanges(t *testing.T) {
cleanup(t, "testdata/notification2", "changes.md", "changes.md~")
restore(t, "index.md")
os.Remove("changes.md")
cleanup(t, "testdata/append")
today := time.Now().Format(time.DateOnly)
p := &Page{Name: "testdata/notification2/" + today + "-water", Body: []byte(`# Water
p := &Page{Name: "testdata/append/" + today + "-water", Body: []byte(`# Water
Sunlight dancing fast
Blue and green and pebbles gray
`)}
p.save()
data := url.Values{}
data.Set("body", "Stand in cold water")
data.Add("notify", "on")
HTTPRedirectTo(t, makeHandler(appendHandler, true),
"POST", "/append/testdata/notification2/"+today+"-water",
data, "/view/testdata/notification2/"+today+"-water")
"POST", "/append/testdata/append/"+today+"-water",
data, "/view/testdata/append/"+today+"-water")
// The changes.md file was created
s, err := os.ReadFile("changes.md")
s, err := os.ReadFile("testdata/append/changes.md")
assert.NoError(t, err)
d := time.Now().Format(time.DateOnly)
assert.Equal(t, "# Changes\n\n## "+d+"\n* [Water](testdata/notification2/"+today+"-water)\n", string(s))
assert.Equal(t, "# Changes\n\n## "+today+"\n* [Water]("+today+"-water)\n", string(s))
// Link added to index.md file
s, err = os.ReadFile("index.md")
s, err = os.ReadFile("testdata/append/index.md")
assert.NoError(t, err)
assert.Contains(t, string(s), "\n* [Water](testdata/notification2/"+today+"-water)\n")
// New index contains just the link
assert.Equal(t, string(s), "* [Water]("+today+"-water)\n")
}

View File

@@ -1,42 +1,54 @@
package main
import (
"log"
"os"
"path"
"regexp"
"strings"
"time"
)
// notify adds a link to the "changes" page, as well as to all the existing hashtag pages. If the "changes" page does
// not exist, it is created. If the hashtag page does not exist, it is not. Hashtag pages are considered optional.
// notify adds a link to the "changes" page, the "index" page, as well as to all the existing hashtag pages. The link to
// the "index" page is only added if the page being edited is a blog page for the current year. The link to existing
// hashtag pages is only added for blog pages. If the "changes" page does not exist, it is created. If the hashtag page
// does not exist, it is not. Hashtag pages are considered optional. If the page that's being edited is in a
// subdirectory, then the "changes", "index" and hashtag pages of that particular subdirectory are affected. Every
// subdirectory is treated like a potentially independent wiki.
func (p *Page) notify() error {
p.handleTitle(false)
if p.Title == "" {
p.Title = p.Name
}
esc := nameEscape(p.Name)
esc := nameEscape(path.Base(p.Name))
link := "* [" + p.Title + "](" + esc + ")\n"
re := regexp.MustCompile(`(?m)^\* \[[^\]]+\]\(` + esc + `\)\n`)
dir := path.Dir(p.Name)
// Recent changes for all pages
err := addLinkWithDate("changes", link, re)
if dir != "." {
err := os.MkdirAll(dir, 0755)
if err != nil {
log.Printf("Creating directory %s failed: %s", dir, err)
return err
}
}
err := addLinkWithDate(path.Join(dir, "changes"), link, re)
if err != nil {
return err
}
// For blog pages only…
if p.isBlog() {
// Add to the index only if the blog post is for the current year
if strings.HasPrefix(path.Base(p.Name), time.Now().Format("2006")) {
err := addLink("index", link, re)
err := addLink(path.Join(dir, "index"), true, link, re)
if err != nil {
log.Printf("Updating index in %s failed: %s", dir, err)
return err
}
}
// Update hashtag pages
p.renderHtml() // to set hashtags
for _, hashtag := range p.Hashtags {
err := addLink(path.Join(dir, hashtag), link, re)
err := addLink(path.Join(dir, hashtag), false, link, re)
if err != nil {
log.Printf("Updating hashtag %s in %s failed: %s", hashtag, dir, err)
return err
}
}
@@ -53,7 +65,7 @@ func addLinkWithDate(name, link string, re *regexp.Regexp) error {
c, err := loadPage(name)
if err != nil {
// create a new page
c = &Page{Name: "changes", Body: []byte("# Changes\n\n## " + date + "\n" + link)}
c = &Page{Name: name, Body: []byte("# Changes\n\n## " + date + "\n" + link)}
} else {
org = string(c.Body)
// remove the old match, if one exists
@@ -136,11 +148,16 @@ func addLinkWithDate(name, link string, re *regexp.Regexp) error {
// addLink adds a link to a named page, if the page exists and doesn't contain the link. If the link exists but with a
// different title, the title is fixed.
func addLink(name, link string, re *regexp.Regexp) error {
func addLink(name string, mandatory bool, link string, re *regexp.Regexp) error {
c, err := loadPage(name)
if err != nil {
// Skip non-existing files: no error
return nil
if (mandatory) {
c = &Page{Name: name, Body: []byte(link)}
return c.save()
} else {
// Skip non-existing files: no error
return nil
}
}
org := string(c.Body)
// if a link exists, that's the place to insert the new link (in which case loc[0] and loc[1] differ)

View File

@@ -10,32 +10,29 @@ import (
// Note TestEditSaveChanges and TestAddAppendChanges.
func TestChanges(t *testing.T) {
cleanup(t, "changes.md", "changes.md~")
restore(t, "index.md")
os.Remove("changes.md")
cleanup(t, "testdata/washing")
today := time.Now().Format(time.DateOnly)
p := &Page{Name: "testdata/" + today + "-machine",
p := &Page{Name: "testdata/washing/" + today + "-machine",
Body: []byte(`# Washing machine
Churning growling thing
Water spraying in a box
Out of sight and dark`)}
p.notify()
// Link added to changes.md file
s, err := os.ReadFile("changes.md")
s, err := os.ReadFile("testdata/washing/changes.md")
assert.NoError(t, err)
assert.Contains(t, string(s), "[Washing machine](testdata/"+today+"-machine)")
assert.Contains(t, string(s), "[Washing machine]("+today+"-machine)")
// Link added to index.md file
s, err = os.ReadFile("index.md")
s, err = os.ReadFile("testdata/washing/index.md")
assert.NoError(t, err)
assert.Contains(t, string(s), "\n* [Washing machine](testdata/"+today+"-machine)\n")
// New index contains just the link
assert.Equal(t, string(s), "* [Washing machine]("+today+"-machine)\n")
}
func TestChangesWithHashtag(t *testing.T) {
cleanup(t, "changes.md", "changes.md~")
restore(t, "index.md")
os.Remove("changes.md")
cleanup(t, "testdata/changes")
intro := "# Haiku\n"
line := "* [Hotel room](testdata/changes/2023-10-27-hotel)\n"
line := "* [Hotel room](2023-10-27-hotel)\n"
h := &Page{Name: "testdata/changes/Haiku", Body: []byte(intro)}
h.save()
p := &Page{Name: "testdata/changes/2023-10-27-hotel",
@@ -46,7 +43,7 @@ Home away from home
#Haiku #Poetry`)}
p.notify()
s, err := os.ReadFile("changes.md")
s, err := os.ReadFile("testdata/changes/changes.md")
assert.NoError(t, err)
assert.Contains(t, string(s), line)
s, err = os.ReadFile("testdata/changes/Haiku.md")
@@ -56,144 +53,154 @@ Home away from home
}
func TestChangesWithList(t *testing.T) {
cleanup(t, "changes.md", "changes.md~")
cleanup(t, "testdata/changes")
intro := "# Changes\n\nThis is a paragraph.\n\n"
line := "* [a change](change)\n"
d := "## " + time.Now().Format(time.DateOnly) + "\n"
os.WriteFile("changes.md", []byte(intro+d+line), 0644)
line := "* [a change](change)\n"
assert.NoError(t, os.MkdirAll("testdata/changes", 0755))
assert.NoError(t, os.WriteFile("testdata/changes/changes.md", []byte(intro+d+line), 0644))
p := &Page{Name: "testdata/changes/alex", Body: []byte(`Hallo!`)}
p.notify()
s, err := os.ReadFile("changes.md")
s, err := os.ReadFile("testdata/changes/changes.md")
assert.NoError(t, err)
new_line := "* [testdata/changes/alex](testdata/changes/alex)\n"
new_line := "* [testdata/changes/alex](alex)\n"
// new line was added at the beginning of the list
assert.Equal(t, intro+d+new_line+line, string(s))
}
func TestChangesWithOldList(t *testing.T) {
cleanup(t, "changes.md", "changes.md~")
cleanup(t, "testdata/changes")
intro := "# Changes\n\nThis is a paragraph.\n\n"
line := "* [a change](change)\n"
y := "## " + time.Now().Add(-24*time.Hour).Format(time.DateOnly) + "\n"
d := "## " + time.Now().Format(time.DateOnly) + "\n"
os.WriteFile("changes.md", []byte(intro+y+line), 0644)
assert.NoError(t, os.MkdirAll("testdata/changes", 0755))
assert.NoError(t, os.WriteFile("testdata/changes/changes.md", []byte(intro+y+line), 0644))
p := &Page{Name: "testdata/changes/alex", Body: []byte(`Hallo!`)}
p.notify()
s, err := os.ReadFile("changes.md")
s, err := os.ReadFile("testdata/changes/changes.md")
assert.NoError(t, err)
new_line := "* [testdata/changes/alex](testdata/changes/alex)\n"
new_line := "* [testdata/changes/alex](alex)\n"
// new line was added at the beginning of the list
assert.Equal(t, intro+d+new_line+"\n"+y+line, string(s))
}
func TestChangesWithOldDisappearingListAtTheEnd(t *testing.T) {
cleanup(t, "changes.md", "changes.md~")
cleanup(t, "testdata/changes")
intro := "# Changes\n\nThis is a paragraph.\n\n"
line := "* [a change](testdata/changes/alex)\n"
line := "* [a change](alex)\n"
y := "## " + time.Now().Add(-24*time.Hour).Format(time.DateOnly) + "\n"
d := "## " + time.Now().Format(time.DateOnly) + "\n"
os.WriteFile("changes.md", []byte(intro+y+line), 0644)
assert.NoError(t, os.MkdirAll("testdata/changes", 0755))
assert.NoError(t, os.WriteFile("testdata/changes/changes.md", []byte(intro+y+line), 0644))
p := &Page{Name: "testdata/changes/alex", Body: []byte(`Hallo!`)}
p.notify()
s, err := os.ReadFile("changes.md")
s, err := os.ReadFile("testdata/changes/changes.md")
assert.NoError(t, err)
new_line := "* [testdata/changes/alex](testdata/changes/alex)\n"
new_line := "* [testdata/changes/alex](alex)\n"
// new line was added at the beginning of the list, with the new date, and the old date disappeared
assert.Equal(t, intro+d+new_line, string(s))
}
func TestChangesWithOldDisappearingListInTheMiddle(t *testing.T) {
cleanup(t, "changes.md", "changes.md~")
cleanup(t, "testdata/changes")
intro := "# Changes\n\nThis is a paragraph.\n\n"
line := "* [a change](testdata/changes/alex)\n"
other := "* [other change](testdata/changes/whatever)\n"
line := "* [a change](alex)\n"
other := "* [other change](whatever)\n"
yy := "## " + time.Now().Add(-48*time.Hour).Format(time.DateOnly) + "\n"
y := "## " + time.Now().Add(-24*time.Hour).Format(time.DateOnly) + "\n"
d := "## " + time.Now().Format(time.DateOnly) + "\n"
os.WriteFile("changes.md", []byte(intro+y+line+"\n"+yy+other), 0644)
assert.NoError(t, os.MkdirAll("testdata/changes", 0755))
assert.NoError(t, os.WriteFile("testdata/changes/changes.md", []byte(intro+y+line+"\n"+yy+other), 0644))
p := &Page{Name: "testdata/changes/alex", Body: []byte(`Hallo!`)}
p.notify()
s, err := os.ReadFile("changes.md")
s, err := os.ReadFile("testdata/changes/changes.md")
assert.NoError(t, err)
new_line := "* [testdata/changes/alex](testdata/changes/alex)\n"
new_line := "* [testdata/changes/alex](alex)\n"
// new line was added at the beginning of the list, with the new date, and the old date disappeared
assert.Equal(t, intro+d+new_line+"\n"+yy+other, string(s))
}
func TestChangesWithListAtTheTop(t *testing.T) {
cleanup(t, "testdata/changes", "changes.md", "changes.md~")
cleanup(t, "testdata/changes")
line := "* [a change](change)\n"
d := "## " + time.Now().Format(time.DateOnly) + "\n"
os.WriteFile("changes.md", []byte(line), 0644)
assert.NoError(t, os.MkdirAll("testdata/changes", 0755))
assert.NoError(t, os.WriteFile("testdata/changes/changes.md", []byte(line), 0644))
p := &Page{Name: "testdata/changes/alex", Body: []byte(`Hallo!`)}
p.notify()
s, err := os.ReadFile("changes.md")
s, err := os.ReadFile("testdata/changes/changes.md")
assert.NoError(t, err)
new_line := "* [testdata/changes/alex](testdata/changes/alex)\n"
new_line := "* [testdata/changes/alex](alex)\n"
// new line was added at the top, no error due to missing introduction
assert.Equal(t, d+new_line+line, string(s))
}
func TestChangesWithNoList(t *testing.T) {
cleanup(t, "testdata/changes", "changes.md", "changes.md~")
cleanup(t, "testdata/changes")
intro := "# Changes\n\nThis is a paragraph."
d := "## " + time.Now().Format(time.DateOnly) + "\n"
os.WriteFile("changes.md", []byte(intro), 0644)
assert.NoError(t, os.MkdirAll("testdata/changes", 0755))
assert.NoError(t, os.WriteFile("testdata/changes/changes.md", []byte(intro), 0644))
p := &Page{Name: "testdata/changes/alex", Body: []byte(`Hallo!`)}
p.notify()
s, err := os.ReadFile("changes.md")
s, err := os.ReadFile("testdata/changes/changes.md")
assert.NoError(t, err)
new_line := "* [testdata/changes/alex](testdata/changes/alex)\n"
new_line := "* [testdata/changes/alex](alex)\n"
// into is still there and a new list was started
assert.Equal(t, intro+"\n\n"+d+new_line, string(s))
}
func TestChangesWithUpdate(t *testing.T) {
cleanup(t, "testdata/changes", "changes.md", "changes.md~")
cleanup(t, "testdata/changes")
intro := "# Changes\n\nThis is a paragraph.\n\n"
other := "* [other change](testdata/changes/whatever)\n"
other := "* [other change](whatever)\n"
d := "## " + time.Now().Format(time.DateOnly) + "\n"
line := "* [a change](testdata/changes/alex)\n"
os.WriteFile("changes.md", []byte(intro+d+other+line), 0644)
line := "* [a change](alex)\n"
assert.NoError(t, os.MkdirAll("testdata/changes", 0755))
assert.NoError(t, os.WriteFile("testdata/changes/changes.md", []byte(intro+d+other+line), 0644))
p := &Page{Name: "testdata/changes/alex", Body: []byte(`Hallo!`)}
p.notify()
s, err := os.ReadFile("changes.md")
s, err := os.ReadFile("testdata/changes/changes.md")
assert.NoError(t, err)
new_line := "* [testdata/changes/alex](testdata/changes/alex)\n"
new_line := "* [testdata/changes/alex](alex)\n"
// the change was already listed, but now it moved up and has a new title
assert.Equal(t, intro+d+new_line+other, string(s))
}
func TestChangesWithNoChangeToTheOrder(t *testing.T) {
cleanup(t, "testdata/changes", "changes.md", "changes.md~")
cleanup(t, "testdata/changes")
intro := "# Changes\n\nThis is a paragraph.\n\n"
other := "* [other change](testdata/changes/whatever)\n"
line := "* [a change](testdata/changes/alex)\n"
d := "## " + time.Now().Format(time.DateOnly) + "\n"
os.WriteFile("changes.md", []byte(intro+d+line+other), 0644)
line := "* [a change](alex)\n"
other := "* [other change](whatever)\n"
assert.NoError(t, os.MkdirAll("testdata/changes", 0755))
assert.NoError(t, os.WriteFile("testdata/changes/changes.md", []byte(intro+d+line+other), 0644))
p := &Page{Name: "testdata/changes/alex", Body: []byte(`Hallo!`)}
p.notify()
s, err := os.ReadFile("changes.md")
s, err := os.ReadFile("testdata/changes/changes.md")
assert.NoError(t, err)
new_line := "* [testdata/changes/alex](testdata/changes/alex)\n"
new_line := "* [testdata/changes/alex](alex)\n"
// the change was already listed at the top, so just use the new title
assert.Equal(t, intro+d+new_line+other, string(s))
// since the file has changed, a backup was necessary
assert.FileExists(t, "testdata/changes/changes.md~")
}
func TestChangesWithNoChanges(t *testing.T) {
cleanup(t, "testdata/changes", "changes.md", "changes.md~")
cleanup(t, "testdata/changes")
intro := "# Changes\n\nThis is a paragraph.\n\n"
other := "* [other change](testdata/changes/whatever)\n"
line := "* [a change](testdata/changes/alex)\n"
d := "## " + time.Now().Format(time.DateOnly) + "\n"
os.Remove("changes.md~")
os.WriteFile("changes.md", []byte(intro+d+line+other), 0644)
line := "* [a change](alex)\n"
other := "* [other change](whatever)\n"
assert.NoError(t, os.MkdirAll("testdata/changes", 0755))
assert.NoError(t, os.WriteFile("testdata/changes/changes.md", []byte(intro+d+line+other), 0644))
p := &Page{Name: "testdata/changes/alex", Body: []byte("# a change\nHallo!")}
p.notify()
s, err := os.ReadFile("changes.md")
s, err := os.ReadFile("testdata/changes/changes.md")
assert.NoError(t, err)
// the change was already listed at the top, so no change was necessary
assert.Equal(t, intro+d+line+other, string(s))
// since the file hasn't changed, no backup was necessary
assert.NoFileExists(t, "changes.md~")
assert.NoFileExists(t, "testdata/changes/changes.md~")
}

View File

@@ -6,7 +6,7 @@
<meta name="viewport" content="width=device-width">
<title>{{.Title}}</title>
<style>
html { max-width: 65ch; padding: 1ch; margin: auto; color: #111; background-color: #ffe; }
html { max-width: 70ch; padding: 1ch; margin: auto; color: #111; background-color: #ffe; }
body { hyphens: auto; }
del { background-color: #fab }
ins { background-color: #af8 }

View File

@@ -37,26 +37,25 @@ func TestEditSave(t *testing.T) {
}
func TestEditSaveChanges(t *testing.T) {
cleanup(t, "testdata/notification", "changes.md")
restore(t, "index.md")
os.Remove("changes.md")
cleanup(t, "testdata/notification")
data := url.Values{}
data.Set("body", "Hallo!")
data.Add("notify", "on")
today := time.Now().Format("2006-01-02")
// Posting to the save URL saves a page
HTTPRedirectTo(t, makeHandler(saveHandler, true),
"POST", "/save/testdata/notification/2023-10-28-alex",
data, "/view/testdata/notification/2023-10-28-alex")
"POST", "/save/testdata/notification/" + today,
data, "/view/testdata/notification/" + today)
// The changes.md file was created
s, err := os.ReadFile("changes.md")
s, err := os.ReadFile("testdata/notification/changes.md")
assert.NoError(t, err)
d := time.Now().Format(time.DateOnly)
assert.Equal(t, "# Changes\n\n## "+d+
"\n* [testdata/notification/2023-10-28-alex](testdata/notification/2023-10-28-alex)\n",
"\n* [testdata/notification/"+today+"]("+today+")\n",
string(s))
// Link added to index.md file
s, err = os.ReadFile("index.md")
s, err = os.ReadFile("testdata/notification/index.md")
assert.NoError(t, err)
assert.Contains(t, string(s),
"\n* [testdata/notification/2023-10-28-alex](testdata/notification/2023-10-28-alex)\n")
// New index contains just the link
assert.Equal(t, string(s), "* [testdata/notification/"+today+"]("+today+")\n")
}

View File

@@ -5,7 +5,7 @@
.nh
.ad l
.\" Begin generated content:
.TH "ODDMU-APACHE" "5" "2023-11-05"
.TH "ODDMU-APACHE" "5" "2024-01-27"
.PP
.SH NAME
.PP
@@ -15,6 +15,8 @@ oddmu-apache - how to setup Apache as a reverse proxy for Oddmu
.PP
The oddmu program serves the current working directory as a wiki on port 8080.\&
This is an unpriviledged port so an ordinary user account can do this.\&
Alternatively, you can reverse proxy HTTP over a Unix-domain socket,
as shown later.\&
.PP
The best way to protect the wiki against vandalism and spam is to use a regular
web server as reverse proxy.\& This page explains how to setup Apache on Debian to
@@ -38,13 +40,14 @@ MDCertificateAgreement accepted
<VirtualHost *:80>
ServerName transjovian\&.org
RewriteEngine on
RewriteRule ^/(\&.*) https://%{HTTP_HOST}/$1 [redirect]
RewriteRule "^/(\&.*)" "https://%{HTTP_HOST}/$1" [redirect]
</VirtualHost>
<VirtualHost *:443>
ServerAdmin alex@alexschroeder\&.ch
ServerName transjovian\&.org
SSLEngine on
ProxyPassMatch "^/((view|diff|edit|save|add|append|upload|drop|search)/(\&.*))?$" "http://localhost:8080/$1"
ProxyPassMatch "^/((view|diff|edit|save|add|append|upload|drop|search)/(\&.*))?$"
"http://localhost:8080/$1"
</VirtualHost>
.fi
.RE
@@ -81,6 +84,99 @@ second instead just proxy to the wiki like you did for the second virtual
host: use a copy of the "ProxyPassMatch" directive instead of "RewriteEngine on"
and "RewriteRule".\&
.PP
.SS Allow HTTP for viewing
.PP
When looking at pages, you might want to allow HTTP since no password is
required.\&
.PP
.nf
.RS 4
MDomain transjovian\&.org
MDCertificateAgreement accepted
<VirtualHost *:80>
ServerName transjovian\&.org
RewriteEngine on
ProxyPassMatch "^/((view|diff|search)/(\&.*))?$"
"http://localhost:8080/$1"
RewriteRule "^/((edit|save|add|append|upload|drop)/(\&.*))?$"
"https://%{HTTP_HOST}/$1" [redirect]
</VirtualHost>
<VirtualHost *:443>
ServerAdmin alex@alexschroeder\&.ch
ServerName transjovian\&.org
SSLEngine on
ProxyPassMatch "^/((view|diff|edit|save|add|append|upload|drop|search)/(\&.*))?$"
"http://localhost:8080/$1"
</VirtualHost>
.fi
.RE
.PP
.SS Using a Unix-domain Socket
.PP
Instead of having Oddmu listen on a TCP port, you can have it listen on a
Unix-domain socket.\& This requires socket activation.\& An example of configuring
the service is given in \fIoddmu.\&service(5)\fR.\&
.PP
To test just the unix domain socket, use \fIncat(1)\fR:
.PP
.nf
.RS 4
echo -e "GET /view/index HTTP/1\&.1rnHost: localhostrnrn"
| ncat --unixsock /run/oddmu/oddmu\&.sock
.fi
.RE
.PP
On the Apache side, you can proxy to the socket directly.\& This sends all
requests to the socket:
.PP
.nf
.RS 4
ProxyPass "/" "unix:/run/oddmu/oddmu\&.sock|http://localhost/"
.fi
.RE
.PP
Now, all traffic between the web server and the wiki goes over the socket at
"/run/oddmu/oddmu.\&sock".\&
.PP
To test it on the command-line, use a tool like \fIcurl(1)\fR.\& Make sure to provide
the correct servername!\&
.PP
.nf
.RS 4
curl http://transjovian\&.org/view/index
.fi
.RE
.PP
You probably want to serve some static files as well (see \fBServe static files\fR).\&
In that case, you need to use the ProxyPassMatch directive.\&
.PP
.nf
.RS 4
ProxyPassMatch "^/((view|diff|edit|save|add|append|upload|drop|search)/(\&.*))?$"
"unix:/run/oddmu/oddmu\&.sock|http://localhost/$1"
.fi
.RE
.PP
There'\&s a curious problem with this expression, however.\& If you use \fIcurl(1)\fR to
get the root path, Apache hangs:
.PP
.nf
.RS 4
curl http://transjovian\&.org/
.fi
.RE
.PP
A workaround is to add the redirect manually and drop the question-mark:
.PP
.nf
.RS 4
RedirectMatch "^/$" "/view/index"
ProxyPassMatch "^/((view|diff|edit|save|add|append|upload|drop|search)/(\&.*))$"
"unix:/run/oddmu/oddmu\&.sock|http://localhost/$1"
.fi
.RE
.PP
.SS Access
.PP
Access control is not part of Oddmu.\& By default, the wiki is editable by all.\&

View File

@@ -8,6 +8,8 @@ oddmu-apache - how to setup Apache as a reverse proxy for Oddmu
The oddmu program serves the current working directory as a wiki on port 8080.
This is an unpriviledged port so an ordinary user account can do this.
Alternatively, you can reverse proxy HTTP over a Unix-domain socket,
as shown later.
The best way to protect the wiki against vandalism and spam is to use a regular
web server as reverse proxy. This page explains how to setup Apache on Debian to
@@ -30,13 +32,14 @@ MDCertificateAgreement accepted
<VirtualHost *:80>
ServerName transjovian.org
RewriteEngine on
RewriteRule ^/(.*) https://%{HTTP_HOST}/$1 [redirect]
RewriteRule "^/(.*)" "https://%{HTTP_HOST}/$1" [redirect]
</VirtualHost>
<VirtualHost *:443>
ServerAdmin alex@alexschroeder.ch
ServerName transjovian.org
SSLEngine on
ProxyPassMatch "^/((view|diff|edit|save|add|append|upload|drop|search)/(.*))?$" "http://localhost:8080/$1"
ProxyPassMatch "^/((view|diff|edit|save|add|append|upload|drop|search)/(.*))?$" \
"http://localhost:8080/$1"
</VirtualHost>
```
@@ -64,6 +67,85 @@ second instead just proxy to the wiki like you did for the second virtual
host: use a copy of the "ProxyPassMatch" directive instead of "RewriteEngine on"
and "RewriteRule".
## Allow HTTP for viewing
When looking at pages, you might want to allow HTTP since no password is
required.
```
MDomain transjovian.org
MDCertificateAgreement accepted
<VirtualHost *:80>
ServerName transjovian.org
RewriteEngine on
ProxyPassMatch "^/((view|diff|search)/(.*))?$" \
"http://localhost:8080/$1"
RewriteRule "^/((edit|save|add|append|upload|drop)/(.*))?$" \
"https://%{HTTP_HOST}/$1" [redirect]
</VirtualHost>
<VirtualHost *:443>
ServerAdmin alex@alexschroeder.ch
ServerName transjovian.org
SSLEngine on
ProxyPassMatch "^/((view|diff|edit|save|add|append|upload|drop|search)/(.*))?$" \
"http://localhost:8080/$1"
</VirtualHost>
```
## Using a Unix-domain Socket
Instead of having Oddmu listen on a TCP port, you can have it listen on a
Unix-domain socket. This requires socket activation. An example of configuring
the service is given in _oddmu.service(5)_.
To test just the unix domain socket, use _ncat(1)_:
```
echo -e "GET /view/index HTTP/1.1\r\nHost: localhost\r\n\r\n" \
| ncat --unixsock /run/oddmu/oddmu.sock
```
On the Apache side, you can proxy to the socket directly. This sends all
requests to the socket:
```
ProxyPass "/" "unix:/run/oddmu/oddmu.sock|http://localhost/"
```
Now, all traffic between the web server and the wiki goes over the socket at
"/run/oddmu/oddmu.sock".
To test it on the command-line, use a tool like _curl(1)_. Make sure to provide
the correct servername!
```
curl http://transjovian.org/view/index
```
You probably want to serve some static files as well (see *Serve static files*).
In that case, you need to use the ProxyPassMatch directive.
```
ProxyPassMatch "^/((view|diff|edit|save|add|append|upload|drop|search)/(.*))?$" \
"unix:/run/oddmu/oddmu.sock|http://localhost/$1"
```
There's a curious problem with this expression, however. If you use _curl(1)_ to
get the root path, Apache hangs:
```
curl http://transjovian.org/
```
A workaround is to add the redirect manually and drop the question-mark:
```
RedirectMatch "^/$" "/view/index"
ProxyPassMatch "^/((view|diff|edit|save|add|append|upload|drop|search)/(.*))$" \
"unix:/run/oddmu/oddmu.sock|http://localhost/$1"
```
## Access
Access control is not part of Oddmu. By default, the wiki is editable by all.

View File

@@ -5,7 +5,7 @@
.nh
.ad l
.\" Begin generated content:
.TH "ODDMU-NOTIFY" "1" "2023-11-06"
.TH "ODDMU-NOTIFY" "1" "2024-01-17"
.PP
.SH NAME
.PP
@@ -20,20 +20,22 @@ oddmu-notify - add links to changes.\&md, index.\&md, and hashtag pages
The "notify" subcommand takes all the page names provided (without the ".\&md"
extension) and adds links to it from other pages.\&
.PP
A new link is added to the \fBchanges\fR page if it doesn'\&t exist.\& The current date
of the machine Oddmu is running on is used as the heading.\& If the requested link
already exists on the changes page, it is moved up to the current date.\& If that
leaves an old date without any links, that date heading is removed.\&
A new link is added to the \fBchanges\fR page in the current directory if it doesn'\&t
exist.\& The current date of the machine Oddmu is running on is used as the
heading.\& If the requested link already exists on the changes page, it is moved
up to the current date.\& If that leaves an old date without any links, that date
heading is removed.\&
.PP
A page whose name starts with an ISO date (YYYY-MM-DD, e.\&g.\& "2023-10-28") is
called a \fBblog\fR page.\&
.PP
A link is created from the \fBindex\fR page to blog pages if and only if the blog
pages are from the current year.\& The idea is that the front page contains a lot
of links to blog posts but eventually the blog post links are moved onto archive
pages (one per year, for example), or simply deleted.\& As when editing older
pages, links to those pages should not get added to the index as if those older
pages were new again.\& A link on the changes page is enough.\&
A link is created from the \fBindex\fR page in the current directory to blog pages
if and only if the blog pages are from the current year.\& The idea is that the
front page contains a lot of links to blog posts but eventually the blog post
links are moved onto archive pages (one per year, for example), or simply
deleted.\& As when editing older pages, links to those pages should not get added
to the index as if those older pages were new again.\& A link on the changes page
is enough.\&
.PP
For every \fBhashtag\fR used on the pages named, another link might be created.\& If a
page named like the hashtag exists, a backlink is added to it.\& A hashtag
@@ -59,6 +61,44 @@ oddmu notify 2023-11-05-climate
.fi
.RE
.PP
The changes file might look as follows:
.PP
.nf
.RS 4
# Changes
This page lists all the changes made to the wiki\&.
## 2023-11-05
* [Global warming](2023-11-05-climate)
.fi
.RE
.PP
The index file might look as follows:
.PP
.nf
.RS 4
# Blog
This page links to all the blog posts\&.
* [Global warming](2023-11-05-climate)
.fi
.RE
.PP
The hashtag file might look as follows:
.PP
.nf
.RS 4
# Climate
This page links to all the blog posts tagged #Climate\&.
* [Global warming](2023-11-05-climate)
.fi
.RE
.PP
.SH SEE ALSO
.PP
\fIoddmu\fR(1)

View File

@@ -13,20 +13,22 @@ oddmu-notify - add links to changes.md, index.md, and hashtag pages
The "notify" subcommand takes all the page names provided (without the ".md"
extension) and adds links to it from other pages.
A new link is added to the *changes* page if it doesn't exist. The current date
of the machine Oddmu is running on is used as the heading. If the requested link
already exists on the changes page, it is moved up to the current date. If that
leaves an old date without any links, that date heading is removed.
A new link is added to the *changes* page in the current directory if it doesn't
exist. The current date of the machine Oddmu is running on is used as the
heading. If the requested link already exists on the changes page, it is moved
up to the current date. If that leaves an old date without any links, that date
heading is removed.
A page whose name starts with an ISO date (YYYY-MM-DD, e.g. "2023-10-28") is
called a *blog* page.
A link is created from the *index* page to blog pages if and only if the blog
pages are from the current year. The idea is that the front page contains a lot
of links to blog posts but eventually the blog post links are moved onto archive
pages (one per year, for example), or simply deleted. As when editing older
pages, links to those pages should not get added to the index as if those older
pages were new again. A link on the changes page is enough.
A link is created from the *index* page in the current directory to blog pages
if and only if the blog pages are from the current year. The idea is that the
front page contains a lot of links to blog posts but eventually the blog post
links are moved onto archive pages (one per year, for example), or simply
deleted. As when editing older pages, links to those pages should not get added
to the index as if those older pages were new again. A link on the changes page
is enough.
For every *hashtag* used on the pages named, another link might be created. If a
page named like the hashtag exists, a backlink is added to it. A hashtag
@@ -50,6 +52,38 @@ it exists):
oddmu notify 2023-11-05-climate
```
The changes file might look as follows:
```
# Changes
This page lists all the changes made to the wiki.
## 2023-11-05
* [Global warming](2023-11-05-climate)
```
The index file might look as follows:
```
# Blog
This page links to all the blog posts.
* [Global warming](2023-11-05-climate)
```
The hashtag file might look as follows:
```
# Climate
This page links to all the blog posts tagged #Climate.
* [Global warming](2023-11-05-climate)
```
# SEE ALSO
_oddmu_(1)

View File

@@ -5,7 +5,7 @@
.nh
.ad l
.\" Begin generated content:
.TH "ODDMU-STATIC" "1" "2023-11-05"
.TH "ODDMU-STATIC" "1" "2024-01-17"
.PP
.SH NAME
.PP
@@ -18,8 +18,8 @@ oddmu-static - create a static copy of the site
.SH DESCRIPTION
.PP
The "static" subcommand generates a static copy of the pages in the current
directory and saves them in the given target directory.\& The target directory
must not exist to unser no existing files are clobbered.\&
directory and saves them in the given destination directory.\& The destination
directory must not exist.\&
.PP
All pages (files with the ".\&md" extension) are turned into HTML files (with the
".\&html" extension) using the "static.\&html" template.\& Links pointing to existing
@@ -28,7 +28,18 @@ pages get ".\&html" appended.\&
Hidden files and directories (starting with a ".\&") and backup files (ending with
a "~") are skipped.\&
.PP
All other files are \fIlinked\fR into the same directory.\&
All other files are \fIhard linked\fR.\& This is done to save space: on a typical blog
the images take a lot more space than the text.\& On my blog in 2023 I had 2.\&62
GiB of JPG files and 0.\&02 GiB of Markdown files.\& There is no point in copying
all those images, most of the time.\&
.PP
Note, however: Hard links cannot span filesystems.\& A hard link is just an extra
name for the same file.\& This is why the destination directory for the static
site has to be on same filesystem as the current directory, if it contains any
other place.\&
.PP
Furthermore, in-place editing changes the file for all names.\& Avoid editing the
image files in the destination directory, just to be on the safe side.\&
.PP
.SH EXAMPLE
.PP
@@ -51,7 +62,14 @@ you to migrate static folders and applications.\&
.SH ENVIRONMENT
.PP
The ODDMU_WEBFINGER environment variable has no effect in this situation.\&
Fediverse accounts are not linked to their profile pages.\&
Fediverse accounts are not linked to their profile pages.\& Since the data isn'\&t
cached, every run of this command would trigger a webfinger request for every
fediverse account mentioned.\&
.PP
If the site is large, determining the language of a page slows things down.\& Set
the ODDMU_LANGUAGES environment variable to a comma-separated list of ISO 639-1
codes, e.\&g.\& "en" or "en,de,fr,pt" to limit the languages loaded and thereby
speed language determination up.\&
.PP
.SH SEE ALSO
.PP

View File

@@ -11,8 +11,8 @@ oddmu-static - create a static copy of the site
# DESCRIPTION
The "static" subcommand generates a static copy of the pages in the current
directory and saves them in the given target directory. The target directory
must not exist to unser no existing files are clobbered.
directory and saves them in the given destination directory. The destination
directory must not exist.
All pages (files with the ".md" extension) are turned into HTML files (with the
".html" extension) using the "static.html" template. Links pointing to existing
@@ -21,7 +21,18 @@ pages get ".html" appended.
Hidden files and directories (starting with a ".") and backup files (ending with
a "~") are skipped.
All other files are _linked_ into the same directory.
All other files are _hard linked_. This is done to save space: on a typical blog
the images take a lot more space than the text. On my blog in 2023 I had 2.62
GiB of JPG files and 0.02 GiB of Markdown files. There is no point in copying
all those images, most of the time.
Note, however: Hard links cannot span filesystems. A hard link is just an extra
name for the same file. This is why the destination directory for the static
site has to be on same filesystem as the current directory, if it contains any
other place.
Furthermore, in-place editing changes the file for all names. Avoid editing the
image files in the destination directory, just to be on the safe side.
# EXAMPLE
@@ -42,7 +53,14 @@ you to migrate static folders and applications.
# ENVIRONMENT
The ODDMU_WEBFINGER environment variable has no effect in this situation.
Fediverse accounts are not linked to their profile pages.
Fediverse accounts are not linked to their profile pages. Since the data isn't
cached, every run of this command would trigger a webfinger request for every
fediverse account mentioned.
If the site is large, determining the language of a page slows things down. Set
the ODDMU_LANGUAGES environment variable to a comma-separated list of ISO 639-1
codes, e.g. "en" or "en,de,fr,pt" to limit the languages loaded and thereby
speed language determination up.
# SEE ALSO

View File

@@ -5,7 +5,7 @@
.nh
.ad l
.\" Begin generated content:
.TH "ODDMU" "1" "2023-12-17"
.TH "ODDMU" "1" "2024-01-17"
.PP
.SH NAME
.PP
@@ -60,6 +60,17 @@ See \fIoddmu-templates\fR(5) for more.\&
.PP
You can change the port served by setting the ODDMU_PORT environment variable.\&
.PP
You can change the address served by setting the ODDMU_ADDRESS environment
variable to either an IPv4 address or an IPv6 address.\& If ODDMU_ADDRESS is
unset, then the program listens on all available unicast addresses, both IPv4
and IPv6.\& Here are a few example addresses:
.PP
ODDMU_ADDRESS=127.\&0.\&0.\&1 # The loopback IPv4 address.\&
ODDMU_ADDRESS=2001:db8::3:1 # An IPv6 address.\&
.PP
See the Socket Activation section for an alternative method of listening which
supports Unix-domain sockets.\&
.PP
In order to limit language-detection to the languages you actually use, set the
environment variable ODDMU_LANGUAGES to a comma-separated list of ISO 639-1
codes, e.\&g.\& "en" or "en,de,fr,pt".\&
@@ -67,6 +78,15 @@ codes, e.\&g.\& "en" or "en,de,fr,pt".\&
You can enable webfinger to link fediverse accounts to their correct profile
pages by setting ODDMU_WEBFINGER to "1".\& See \fIoddmu\fR(5).\&
.PP
.SH Socket Activation
.PP
Instead of specifying ODDMU_ADDRESS or ODDMU_PORT, you can start the service
through socket activation.\& The advantage of this method is that you can use a
Unix-domain socket instead of a TCP socket, and the permissions and ownership of
the socket are set before the program starts.\& See \fIoddmu.\&service\fR(5) and
\fIoddmu-apache\fR(5) for an example of how to use socket activation with a
Unix-domain socket under systemd and Apache.\&
.PP
.SH SECURITY
.PP
If the machine you are running Oddmu on is accessible from the Internet, you
@@ -79,6 +99,8 @@ Oddmu assumes that all the users that can edit pages or upload files are trusted
users and therefore their content is trusted.\& Therefore, Oddmu doesn'\&t perform
HTML sanitization!\&
.PP
For an extra dose of security, consider using a Unix-domain socket.\&
.PP
.SH OPTIONS
.PP
The oddmu program can be run on the command-line using various subcommands.\&

View File

@@ -53,6 +53,17 @@ See _oddmu-templates_(5) for more.
You can change the port served by setting the ODDMU_PORT environment variable.
You can change the address served by setting the ODDMU_ADDRESS environment
variable to either an IPv4 address or an IPv6 address. If ODDMU_ADDRESS is
unset, then the program listens on all available unicast addresses, both IPv4
and IPv6. Here are a few example addresses:
ODDMU_ADDRESS=127.0.0.1 # The loopback IPv4 address.
ODDMU_ADDRESS=2001:db8::3:1 # An IPv6 address.
See the Socket Activation section for an alternative method of listening which
supports Unix-domain sockets.
In order to limit language-detection to the languages you actually use, set the
environment variable ODDMU_LANGUAGES to a comma-separated list of ISO 639-1
codes, e.g. "en" or "en,de,fr,pt".
@@ -60,6 +71,15 @@ codes, e.g. "en" or "en,de,fr,pt".
You can enable webfinger to link fediverse accounts to their correct profile
pages by setting ODDMU_WEBFINGER to "1". See _oddmu_(5).
# Socket Activation
Instead of specifying ODDMU_ADDRESS or ODDMU_PORT, you can start the service
through socket activation. The advantage of this method is that you can use a
Unix-domain socket instead of a TCP socket, and the permissions and ownership of
the socket are set before the program starts. See _oddmu.service_(5) and
_oddmu-apache_(5) for an example of how to use socket activation with a
Unix-domain socket under systemd and Apache.
# SECURITY
If the machine you are running Oddmu on is accessible from the Internet, you
@@ -72,6 +92,8 @@ Oddmu assumes that all the users that can edit pages or upload files are trusted
users and therefore their content is trusted. Therefore, Oddmu doesn't perform
HTML sanitization!
For an extra dose of security, consider using a Unix-domain socket.
# OPTIONS
The oddmu program can be run on the command-line using various subcommands.

View File

@@ -5,7 +5,7 @@
.nh
.ad l
.\" Begin generated content:
.TH "ODDMU.SERVICE" "5" "2023-10-28"
.TH "ODDMU.SERVICE" "5" "2024-01-17"
.PP
.SH NAME
.PP
@@ -80,9 +80,30 @@ sudo ln -sf /home/oddmu/oddmu\&.service
.fi
.RE
.PP
.SH Socket Activation
.PP
Alternatively, you can let systemd handle the creation of the listening socket,
passing it to Oddmu.\& See "oddmu-unix-domain.\&service" and
"oddmu-unix-domain.\&socket" for a fully worked example of how to do this with a
Unix domain socket.\& Take note of "Accept=no" in the .\&socket file and
"StandardInput=socket" in the .\&service file.\& The option "StandardInput=socket"
tells systemd to pass the socket to the service as its standard input.\&
"Accept=no" tells systemd to pass a listening socket, rather than to try calling
Oddmu for each connection.\&
.PP
The instructions for starting and enabling the systemd service are almost
exactly the same as those in the previous section, with "oddmu.\&service" replaced
by "oddmu-unix-domain.\&service".\& You'\&ll also need to run the following:
.PP
.nf
.RS 4
ln -s /home/oddmu/oddmu-unix-domain\&.socket /etc/systemd/system
.fi
.RE
.PP
.SH SEE ALSO
.PP
\fIoddmu\fR(1), \fIsystemd.\&exec\fR(5), \fIcapabilities\fR(7)
\fIoddmu\fR(1), \fIsystemd.\&exec\fR(5), \fIsystemd.\&socket(5), \fRcapabilities_(7)
.PP
.SH AUTHORS
.PP

View File

@@ -61,9 +61,28 @@ sudo ln -sf /home/oddmu/oddmu.service \
/etc/systemd/system/multi-user.target.wants/
```
# Socket Activation
Alternatively, you can let systemd handle the creation of the listening socket,
passing it to Oddmu. See "oddmu-unix-domain.service" and
"oddmu-unix-domain.socket" for a fully worked example of how to do this with a
Unix domain socket. Take note of "Accept=no" in the .socket file and
"StandardInput=socket" in the .service file. The option "StandardInput=socket"
tells systemd to pass the socket to the service as its standard input.
"Accept=no" tells systemd to pass a listening socket, rather than to try calling
Oddmu for each connection.
The instructions for starting and enabling the systemd service are almost
exactly the same as those in the previous section, with "oddmu.service" replaced
by "oddmu-unix-domain.service". You'll also need to run the following:
```
ln -s /home/oddmu/oddmu-unix-domain.socket /etc/systemd/system
```
# SEE ALSO
_oddmu_(1), _systemd.exec_(5), _capabilities_(7)
_oddmu_(1), _systemd.exec_(5), _systemd.socket(5), _capabilities_(7)
# AUTHORS

53
oddmu-unix-domain.service Normal file
View File

@@ -0,0 +1,53 @@
[Unit]
Description=Oddmu
After=network.target
Requires=oddmu-unix-domain.socket
[Install]
WantedBy=multi-user.target
[Service]
Type=simple
Restart=always
StandardInput=socket
StandardOutput=journal
StandardError=journal
DynamicUser=true
MemoryMax=256M
MemoryHigh=128M
ExecStart=/home/oddmu/oddmu
WorkingDirectory=/home/oddmu
Environment="ODDMU_PORT=8080"
Environment="ODDMU_WEBFINGER=1"
# (man "systemd.exec")
ReadWritePaths=/home/oddmu
ProtectHostname=yes
RestrictSUIDSGID=yes
RemoveIPC=yes
MemoryDenyWriteExecute=yes
# Sandboxing options to harden security
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
# (man "capabilities")
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

14
oddmu-unix-domain.socket Normal file
View File

@@ -0,0 +1,14 @@
[Unit]
Description=Oddmu server socket
[Socket]
ListenStream=/run/oddmu/oddmu.sock
SocketGroup=www-data
# Systemd manages the socket, so may as well let it be owned by root.
SocketUser=root
# But it needs to be readable and writable by the web server.
SocketMode=0660
Accept=no
[Install]
WantedBy=sockets.target

View File

@@ -6,7 +6,7 @@
<meta name="viewport" content="width=device-width">
<title>Search for {{.Query}}</title>
<style>
html { max-width: 65ch; padding: 2ch; margin: auto; color: #111; background-color: #ffe; }
html { max-width: 70ch; padding: 2ch; margin: auto; color: #111; background-color: #ffe; }
body { hyphens: auto; }
header a { margin-right: 1ch; }
form { display: inline-block; }

View File

@@ -37,20 +37,33 @@ func (cmd *staticCmd) Execute(_ context.Context, f *flag.FlagSet, _ ...interface
fmt.Println("Exactly one target directory is required")
return subcommands.ExitFailure
}
return staticCli(filepath.Clean(args[0]))
return staticCli(filepath.Clean(args[0]), false)
}
func staticCli(dir string) subcommands.ExitStatus {
// staticCli generates a static site in the designated directory. The quiet flag is used to suppress output when running
// tests.
func staticCli(dir string, quiet bool) subcommands.ExitStatus {
err := os.Mkdir(dir, 0755)
if err != nil {
fmt.Println(err)
return subcommands.ExitFailure
}
initAccounts()
templates := loadTemplates();
if (!quiet) {
fmt.Printf("Loaded %d languages\n", loadLanguages())
}
templates := loadTemplates()
n := 0;
err = filepath.Walk(".", func(path string, info fs.FileInfo, err error) error {
n++
if (!quiet && (n < 100 || n < 1000 && n % 10 == 0 || n % 100 == 0)) {
fmt.Fprintf(os.Stdout, "\r%d", n)
}
return staticFile(path, dir, info, templates, err)
})
if (!quiet) {
fmt.Printf("\r%d\n", n)
}
if err != nil {
fmt.Println(err)
return subcommands.ExitFailure

View File

@@ -8,7 +8,7 @@ import (
func TestStatusCmd(t *testing.T) {
cleanup(t, "testdata/static")
s := staticCli("testdata/static")
s := staticCli("testdata/static", true)
assert.Equal(t, subcommands.ExitSuccess, s)
// pages
assert.FileExists(t, "testdata/static/index.html")

View File

@@ -6,7 +6,7 @@
<meta name="viewport" content="width=device-width">
<title>{{.Title}}</title>
<style>
html { max-width: 65ch; padding: 1ch; margin: auto; color: #111; background-color: #ffe; }
html { max-width: 70ch; padding: 1ch; margin: auto; color: #111; background-color: #ffe; }
body { hyphens: auto; }
header a { margin-right: 1ch; }
form { display: inline-block; }

46
wiki.go
View File

@@ -3,12 +3,16 @@ package main
import (
"context"
"flag"
"fmt"
"github.com/google/subcommands"
"html/template"
"io/fs"
"log"
"net"
"net/http"
"os"
"regexp"
"strings"
)
// validPath is a regular expression where the second group matches a page, so when the editHandler is called, a URL
@@ -65,6 +69,37 @@ func getPort() string {
return port
}
// When stdin is a socket, getListener returns a listener that listens
// on the socket passed as stdin. This allows systemd-style socket
// activation.
// Otherwise, getListener returns a net.Listener listening on the address from
// ODDMU_ADDRESS and the port from ODDMU_PORT.
// ODDMU_ADDRESS may be either an IPV4 address or an IPv6 address.
// If ODDMU_ADDRESS is unspecified, then the
// listener listens on all available unicast addresses, both IPv4 and IPv6.
func getListener() (net.Listener, error) {
address := os.Getenv("ODDMU_ADDRESS")
port := getPort()
stat, err := os.Stdin.Stat()
if stat == nil {
return nil, err
}
if stat.Mode().Type() == fs.ModeSocket {
// Listening socket passed on stdin, through systemd socket
// activation or similar:
log.Println("Serving a wiki on a listening socket passed by systemd.")
return net.FileListener(os.Stdin)
}
if strings.ContainsRune(address, ':') {
address = fmt.Sprintf("[%s]:%s", address, port)
} else {
address = fmt.Sprintf("%s:%s", address, port)
}
log.Printf("Serving a wiki at address %s", address)
return net.Listen("tcp", address)
}
// scheduleLoadIndex calls index.load and prints some messages before and after. For testing, call index.load directly
// and skip the messages.
func scheduleLoadIndex() {
@@ -111,11 +146,14 @@ func serve() {
go scheduleLoadIndex()
go scheduleLoadLanguages()
initAccounts()
port := getPort()
log.Printf("Serving a wiki on port %s", port)
err := http.ListenAndServe(":"+port, nil)
if err != nil {
listener, err := getListener()
if listener == nil {
log.Println(err)
} else {
err := http.Serve(listener, nil)
if err != nil {
log.Println(err)
}
}
}

View File

@@ -86,63 +86,22 @@ func HTTPStatusCodeIfModifiedSince(t *testing.T, handler http.HandlerFunc, url s
assert.Equal(t, http.StatusNotModified, w.Code)
}
// restore remembers the file content before the test starts and restores the file at the end. Important for files such
// as "index.md".
func restore(t *testing.T, files ...string) {
data := make(map[string][]byte)
stat := make(map[string]os.FileInfo)
for _, file := range files {
s, err := os.Stat(file)
if err != nil {
t.Log("Could not stat ", file, ": ", err)
continue
}
c, err := os.ReadFile(file)
if err != nil {
t.Log("Could not read ", file, ": ", err)
continue
}
stat[file] = s
data[file] = c
}
// cleanup deletes a directory mentioned and removes all pages in that directory from the index.
func cleanup(t *testing.T, dir string) {
t.Cleanup(func() {
for file, c := range data {
m := stat[file].Mode()
err := os.WriteFile(file, c, m)
if err != nil {
t.Log("Could not restore ", file, ": ", err)
}
t := stat[file].ModTime()
os.Chtimes(file, t, t)
}
})
}
// cleanup deletes any directories mentioned and removes all pages in those directories from the index. Incidentally, if
// a filename such as "changes.md" or "changes.md~" is provided instead of a directory, then that page file is removed
// and any mention of it is removed from the index.
func cleanup(t *testing.T, dirs ...string) {
t.Cleanup(func() {
for _, dir := range dirs {
_ = os.RemoveAll(dir)
}
_ = os.RemoveAll(dir)
index.Lock()
defer index.Unlock()
for name := range index.titles {
for _, dir := range dirs {
if strings.HasPrefix(name, dir) {
delete(index.titles, name)
}
if strings.HasPrefix(name, dir) {
delete(index.titles, name)
}
}
ids := []docid{}
for id, name := range index.documents {
for _, dir := range dirs {
if strings.HasPrefix(name, dir) {
delete(index.documents, id)
ids = append(ids, id)
}
if strings.HasPrefix(name, dir) {
delete(index.documents, id)
ids = append(ids, id)
}
}
for hashtag, docs := range index.token {