forked from mirror/oddmu
Compare commits
8 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
92a52d2c97 | ||
|
|
7f0b371570 | ||
|
|
a44d903775 | ||
|
|
1ca8e6f3aa | ||
|
|
93197f94bf | ||
|
|
a7861edbad | ||
|
|
d4090ab146 | ||
|
|
4da0ba8d94 |
33
README.md
33
README.md
@@ -87,9 +87,10 @@ Text
|
||||
## Templates
|
||||
|
||||
The template files are the HTML files in the working directory:
|
||||
`add.html`, `edit.html`, `search.html` and `view.html`. Feel free to
|
||||
change the templates and restart the server. The first change you
|
||||
should make is to replace the email address in `view.html`. 😄
|
||||
`add.html`, `edit.html`, `search.html`, `upload.html` and `view.html`.
|
||||
Feel free to change the templates and restart the server. The first
|
||||
change you should make is to replace the email address in `view.html`.
|
||||
😄
|
||||
|
||||
See [Structuring the web
|
||||
with HTML](https://developer.mozilla.org/en-US/docs/Learn/HTML) to
|
||||
@@ -128,6 +129,8 @@ summary, as HTML.
|
||||
|
||||
`{{.Score}}` is a numerical score for search results.
|
||||
|
||||
The `upload.html` template cannot refer to anything.
|
||||
|
||||
When calling the `save` action, the page name is take from the URL and
|
||||
the page content is taken from the `body` form parameter. To
|
||||
illustrate, here's how to edit a page using `curl`:
|
||||
@@ -193,7 +196,7 @@ adduser --system --home /home/oddmu oddmu
|
||||
```
|
||||
|
||||
Copy all the files into `/home/oddmu` to your server: `oddmu`,
|
||||
`oddmu.service`, `view.html` and `edit.html`.
|
||||
`oddmu.service`, and all the template files ending in `.html`.
|
||||
|
||||
Edit the `oddmu.service` file. These are the three lines you most
|
||||
likely have to take care of:
|
||||
@@ -250,7 +253,7 @@ MDCertificateAgreement accepted
|
||||
ServerAdmin alex@alexschroeder.ch
|
||||
ServerName transjovian.org
|
||||
SSLEngine on
|
||||
ProxyPassMatch ^/(search|(view|edit|save|add|append)/(.*))?$ http://localhost:8080/$1
|
||||
ProxyPassMatch ^/(search|upload|save|(view|edit|save|add|append)/(.*))?$ http://localhost:8080/$1
|
||||
</VirtualHost>
|
||||
```
|
||||
|
||||
@@ -308,11 +311,11 @@ htpasswd -D .htpasswd berta
|
||||
```
|
||||
|
||||
Modify your site configuration and protect the `/edit/`, `/save/`,
|
||||
`/add/` and `/append/` URLs with a password by adding the following to
|
||||
your `<VirtualHost *:443>` section:
|
||||
`/add/`, `/append/`, `/upload` and `/save` URLs with a password by
|
||||
adding the following to your `<VirtualHost *:443>` section:
|
||||
|
||||
```apache
|
||||
<LocationMatch "^/(edit|save|add|append)/">
|
||||
<LocationMatch "^/(upload|save|(edit|save|add|append)/(.*))$">
|
||||
AuthType Basic
|
||||
AuthName "Password Required"
|
||||
AuthUserFile /home/oddmu/.htpasswd
|
||||
@@ -337,9 +340,9 @@ webserver can read (world readable file, world readable and executable
|
||||
directory). Populate it with files.
|
||||
|
||||
Make sure that none of the static files look like the wiki paths
|
||||
`/view/`, `/edit/`, `/save/`, `/add/`, `/append/` or `/search/`. For
|
||||
example, create a file called `robots.txt` containing the following,
|
||||
tellin all robots that they're not welcome.
|
||||
`/view/`, `/edit/`, `/save/`, `/add/`, `/append/`, `/upload`, `/save`
|
||||
or `/search`. For example, create a file called `robots.txt`
|
||||
containing the following, tellin all robots that they're not welcome.
|
||||
|
||||
```text
|
||||
User-agent: *
|
||||
@@ -363,7 +366,7 @@ above.
|
||||
This requires a valid login by the user "alex" or "berta":
|
||||
|
||||
```apache
|
||||
<LocationMatch "^/(edit|save)/intetebi/">
|
||||
<LocationMatch "^/(edit|save|add|append)/intetebi/">
|
||||
Require user alex berta
|
||||
</LocationMatch>
|
||||
```
|
||||
@@ -439,6 +442,12 @@ memory.
|
||||
|
||||
Files may not end with a tilde (`~`) – these are backup files.
|
||||
|
||||
You cannot edit uploaded files. If you upload a file called
|
||||
`hello.txt` and attempt to edit it by using `/edit/hello.txt` you will
|
||||
create a page with the name `hello.txt.md` instead.
|
||||
|
||||
You cannot delete uploaded files via the web.
|
||||
|
||||
## Bugs
|
||||
|
||||
If you spot any, [contact](https://alexschroeder.ch/wiki/Contact) me.
|
||||
|
||||
3
TODO.md
3
TODO.md
@@ -1,4 +1,5 @@
|
||||
Upload files.
|
||||
Upload files should use path info so that we can use Apache to
|
||||
restrict access to directories.
|
||||
|
||||
Automatically scale or process files.
|
||||
|
||||
|
||||
2
add.html
2
add.html
@@ -13,7 +13,7 @@ form, textarea { width: 100%; }
|
||||
<body>
|
||||
<h1>Adding to {{.Title}}</h1>
|
||||
<form action="/append/{{.Name}}" method="POST">
|
||||
<div><textarea name="body" rows="20" cols="80" placeholder="Text" autofocus></textarea></div>
|
||||
<textarea name="body" rows="20" cols="80" placeholder="Text" autofocus required></textarea>
|
||||
<p><input type="submit" value="Add">
|
||||
<a href="/view/{{.Name}}"><button>Cancel</button></a></p>
|
||||
</form>
|
||||
|
||||
25
commands.go
Normal file
25
commands.go
Normal file
@@ -0,0 +1,25 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
)
|
||||
|
||||
func commands() {
|
||||
if len(os.Args) == 3 && os.Args[1] == "html" {
|
||||
p, err := loadPage(os.Args[2]);
|
||||
if err != nil {
|
||||
fmt.Println(err);
|
||||
} else {
|
||||
p.renderHtml();
|
||||
fmt.Println(p.Html);
|
||||
}
|
||||
} else {
|
||||
fmt.Printf("Unknown command: %v\n", os.Args[1:])
|
||||
fmt.Print("Without any arguments, serves a wiki.\n")
|
||||
fmt.Print(" Environment variable ODDMUSE_PORT controls the port.\n")
|
||||
fmt.Print(" Environment variable ODDMUSE_LANGAUGES controls the languages detected.\n")
|
||||
fmt.Print("html PAGENAME\n")
|
||||
fmt.Print(" Print the HTML of the page.\n")
|
||||
}
|
||||
}
|
||||
@@ -17,7 +17,6 @@ Environment="ODDMU_PORT=8080"
|
||||
ReadWritePaths=/home/oddmu
|
||||
ProtectHostname=yes
|
||||
RestrictSUIDSGID=yes
|
||||
UMask=0077
|
||||
RemoveIPC=yes
|
||||
MemoryDenyWriteExecute=yes
|
||||
|
||||
|
||||
22
upload.html
Normal file
22
upload.html
Normal file
@@ -0,0 +1,22 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="format-detection" content="telephone=no">
|
||||
<meta name="viewport" content="width=device-width">
|
||||
<title>Upload File</title>
|
||||
<style>
|
||||
html { max-width: 70ch; padding: 2ch; margin: auto; color: #111; background: #ffe; }
|
||||
form, textarea { width: 100%; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h1>Upload File</h1>
|
||||
<form action="/save" method="POST" enctype="multipart/form-data">
|
||||
<input type="text" name="name" placeholder="image.jpg" autofocus required>
|
||||
<p><input type="file" name="file" required>
|
||||
<p><input type="submit" value="Save">
|
||||
<a href="/view/index"><button type="button">Cancel</button></a></p>
|
||||
</form>
|
||||
</body>
|
||||
</html>
|
||||
62
wiki.go
62
wiki.go
@@ -3,14 +3,17 @@ package main
|
||||
import (
|
||||
"fmt"
|
||||
"html/template"
|
||||
"io"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
)
|
||||
|
||||
// Templates are parsed at startup.
|
||||
var templates = template.Must(
|
||||
template.ParseFiles("edit.html", "add.html", "view.html", "search.html"))
|
||||
template.ParseFiles("edit.html", "add.html", "view.html",
|
||||
"search.html", "upload.html"))
|
||||
|
||||
// validPath is a regular expression where the second group matches a
|
||||
// page, so when the editHandler is called, a URL path of "/edit/foo"
|
||||
@@ -119,6 +122,51 @@ func appendHandler(w http.ResponseWriter, r *http.Request, name string) {
|
||||
http.Redirect(w, r, "/view/"+name, http.StatusFound)
|
||||
}
|
||||
|
||||
// uploadHandler uses the "upload.html" template to enable uploads.
|
||||
// The file is saved using the saveUploadHandler.
|
||||
func uploadHandler(w http.ResponseWriter, r *http.Request) {
|
||||
renderTemplate(w, "upload", nil)
|
||||
}
|
||||
|
||||
// saveUploadHandler takes the "name" form field and the "file" form
|
||||
// file and saves the file under the given name. The browser is
|
||||
// redirected to the view of that file.
|
||||
func saveUploadHandler(w http.ResponseWriter, r *http.Request) {
|
||||
filename := r.FormValue("name")
|
||||
file, _, err := r.FormFile("file")
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
defer file.Close()
|
||||
// backup an existing file with the same name
|
||||
_, err = os.Stat(filename)
|
||||
if err != nil {
|
||||
os.Rename(filename, filename + "~")
|
||||
}
|
||||
// create the directory, if necessary
|
||||
d := filepath.Dir(filename)
|
||||
if d != "." {
|
||||
err := os.MkdirAll(d, 0755)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
}
|
||||
// create the new file
|
||||
dst, err := os.Create(filename)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
defer dst.Close()
|
||||
if _, err := io.Copy(dst, file); err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
http.Redirect(w, r, "/view/"+filename, http.StatusFound)
|
||||
}
|
||||
|
||||
// makeHandler returns a handler that uses the URL path without the
|
||||
// first path element as its argument, e.g. if the URL path is
|
||||
// /edit/foo/bar, the editHandler is called with "foo/bar" as its
|
||||
@@ -177,13 +225,15 @@ func scheduleLoadLanguages() {
|
||||
fmt.Printf("Loaded %d languages\n", n)
|
||||
}
|
||||
|
||||
func main() {
|
||||
func serve() {
|
||||
http.HandleFunc("/", rootHandler)
|
||||
http.HandleFunc("/view/", makeHandler(viewHandler))
|
||||
http.HandleFunc("/edit/", makeHandler(editHandler))
|
||||
http.HandleFunc("/save/", makeHandler(saveHandler))
|
||||
http.HandleFunc("/add/", makeHandler(addHandler))
|
||||
http.HandleFunc("/append/", makeHandler(appendHandler))
|
||||
http.HandleFunc("/upload", uploadHandler)
|
||||
http.HandleFunc("/save", saveUploadHandler)
|
||||
http.HandleFunc("/search", searchHandler)
|
||||
go scheduleLoadIndex()
|
||||
go scheduleLoadLanguages()
|
||||
@@ -191,3 +241,11 @@ func main() {
|
||||
fmt.Printf("Serving a wiki on port %s\n", port)
|
||||
http.ListenAndServe(":"+port, nil)
|
||||
}
|
||||
|
||||
func main() {
|
||||
if len(os.Args) == 1 {
|
||||
serve()
|
||||
} else {
|
||||
commands()
|
||||
}
|
||||
}
|
||||
|
||||
61
wiki_test.go
61
wiki_test.go
@@ -1,8 +1,9 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"bytes"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"mime/multipart"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"net/url"
|
||||
@@ -27,8 +28,7 @@ func HTTPHeaders(handler http.HandlerFunc, method, url string, values url.Values
|
||||
// HTTPRedirectTo checks that the request results in a redirect and it
|
||||
// checks the destination of the redirect. It returns whether the
|
||||
// request did in fact result in a redirect. Note: This method assumes
|
||||
// that POST requests ignore the query part of the URL which is often
|
||||
// true but not mandated by the standards.
|
||||
// that POST requests ignore the query part of the URL.
|
||||
func HTTPRedirectTo(t *testing.T, handler http.HandlerFunc, method, url string, values url.Values, destination string) bool {
|
||||
w := httptest.NewRecorder()
|
||||
var req *http.Request
|
||||
@@ -40,19 +40,32 @@ func HTTPRedirectTo(t *testing.T, handler http.HandlerFunc, method, url string,
|
||||
} else {
|
||||
req, err = http.NewRequest(method, url+"?"+values.Encode(), nil)
|
||||
}
|
||||
if err != nil {
|
||||
assert.Fail(t, fmt.Sprintf("Failed to build test request, got error: %s", err))
|
||||
}
|
||||
assert.NoError(t, err)
|
||||
handler(w, req)
|
||||
code := w.Code
|
||||
isRedirectCode := code >= http.StatusMultipleChoices && code <= http.StatusTemporaryRedirect
|
||||
if !isRedirectCode {
|
||||
assert.Fail(t, fmt.Sprintf("Expected HTTP redirect status code for %q but received %d", url+"?"+values.Encode(), code))
|
||||
}
|
||||
assert.True(t, isRedirectCode, "Expected HTTP redirect status code for %q but received %d", url+"?"+values.Encode(), code)
|
||||
headers := w.Result().Header["Location"]
|
||||
if len(headers) != 1 || headers[0] != destination {
|
||||
assert.Fail(t, fmt.Sprintf("Expected HTTP redirect location %s for %q but received %v", destination, url+"?"+values.Encode(), headers))
|
||||
}
|
||||
assert.True(t, len(headers) == 1 && headers[0] == destination,
|
||||
"Expected HTTP redirect location %s for %q but received %v", destination, url+"?"+values.Encode(), headers)
|
||||
return isRedirectCode
|
||||
}
|
||||
|
||||
// HTTPUploadAndRedirectTo checks that the request results in a redirect and it
|
||||
// checks the destination of the redirect. It returns whether the
|
||||
// request did in fact result in a redirect.
|
||||
func HTTPUploadAndRedirectTo(t *testing.T, handler http.HandlerFunc, url, contentType string, body *bytes.Buffer, destination string) bool {
|
||||
w := httptest.NewRecorder()
|
||||
req, err := http.NewRequest("POST", url, body)
|
||||
req.Header.Set("Content-Type", contentType)
|
||||
assert.NoError(t, err)
|
||||
handler(w, req)
|
||||
code := w.Code
|
||||
isRedirectCode := code >= http.StatusMultipleChoices && code <= http.StatusTemporaryRedirect
|
||||
assert.True(t, isRedirectCode, "Expected HTTP redirect status code for %q but received %d", url, code)
|
||||
headers := w.Result().Header["Location"]
|
||||
assert.True(t, len(headers) == 1 && headers[0] == destination,
|
||||
"Expected HTTP redirect location %s for %q but received %v", destination, url, headers)
|
||||
return isRedirectCode
|
||||
}
|
||||
|
||||
@@ -110,6 +123,30 @@ It's not `)}
|
||||
})
|
||||
}
|
||||
|
||||
// wipes testdata
|
||||
func TestUpload(t *testing.T) {
|
||||
_ = os.RemoveAll("testdata")
|
||||
assert.HTTPStatusCode(t, uploadHandler, "GET", "/upload", nil, 200)
|
||||
form := new(bytes.Buffer)
|
||||
writer := multipart.NewWriter(form)
|
||||
field, err := writer.CreateFormField("name")
|
||||
assert.NoError(t, err)
|
||||
_, err = field.Write([]byte("testdata/ok.txt"))
|
||||
assert.NoError(t, err)
|
||||
file, err := writer.CreateFormFile("file", "example.txt");
|
||||
assert.NoError(t, err)
|
||||
file.Write([]byte("Hello!"))
|
||||
err = writer.Close()
|
||||
assert.NoError(t, err)
|
||||
t.Log(writer.FormDataContentType())
|
||||
HTTPUploadAndRedirectTo(t, saveUploadHandler, "/upload", writer.FormDataContentType(), form, "/view/testdata/ok.txt")
|
||||
assert.Regexp(t, regexp.MustCompile("Hello!"),
|
||||
assert.HTTPBody(makeHandler(viewHandler), "GET", "/view/testdata/ok.txt", nil))
|
||||
t.Cleanup(func() {
|
||||
_ = os.RemoveAll("testdata")
|
||||
})
|
||||
}
|
||||
|
||||
// wipes testdata
|
||||
func TestPageTitleWithAmp(t *testing.T) {
|
||||
_ = os.RemoveAll("testdata")
|
||||
|
||||
Reference in New Issue
Block a user