Compare commits
No commits in common. "v2.0" and "master" have entirely different histories.
1371 changed files with 279547 additions and 519473 deletions
5
.gitignore
vendored
5
.gitignore
vendored
|
|
@ -1,4 +1 @@
|
|||
/gowiki
|
||||
tmp
|
||||
run.sh
|
||||
TODO.txt
|
||||
gowiki
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
Copyright (C) 2021 Marius Schellenberger
|
||||
Copyright (C) 2016 Marius Schellenberger
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
|
|
|
|||
232
Godeps/Godeps.json
generated
Normal file
232
Godeps/Godeps.json
generated
Normal file
|
|
@ -0,0 +1,232 @@
|
|||
{
|
||||
"ImportPath": "git.giftfish.de/ston1th/gowiki",
|
||||
"GoVersion": "go1.7",
|
||||
"GodepVersion": "v74",
|
||||
"Deps": [
|
||||
{
|
||||
"ImportPath": "git.giftfish.de/ston1th/godrop",
|
||||
"Comment": "v1.0",
|
||||
"Rev": "8666e4461c9612e1432f05af6e0abfd716adca36"
|
||||
},
|
||||
{
|
||||
"ImportPath": "github.com/blevesearch/bleve",
|
||||
"Comment": "v0.5.0",
|
||||
"Rev": "97393d027342f43b17da2f02c090a4e728ac929f"
|
||||
},
|
||||
{
|
||||
"ImportPath": "github.com/blevesearch/bleve/analysis",
|
||||
"Comment": "v0.5.0",
|
||||
"Rev": "97393d027342f43b17da2f02c090a4e728ac929f"
|
||||
},
|
||||
{
|
||||
"ImportPath": "github.com/blevesearch/bleve/analysis/analyzers/standard_analyzer",
|
||||
"Comment": "v0.5.0",
|
||||
"Rev": "97393d027342f43b17da2f02c090a4e728ac929f"
|
||||
},
|
||||
{
|
||||
"ImportPath": "github.com/blevesearch/bleve/analysis/char_filters/html_char_filter",
|
||||
"Comment": "v0.5.0",
|
||||
"Rev": "97393d027342f43b17da2f02c090a4e728ac929f"
|
||||
},
|
||||
{
|
||||
"ImportPath": "github.com/blevesearch/bleve/analysis/char_filters/regexp_char_filter",
|
||||
"Comment": "v0.5.0",
|
||||
"Rev": "97393d027342f43b17da2f02c090a4e728ac929f"
|
||||
},
|
||||
{
|
||||
"ImportPath": "github.com/blevesearch/bleve/analysis/datetime_parsers/datetime_optional",
|
||||
"Comment": "v0.5.0",
|
||||
"Rev": "97393d027342f43b17da2f02c090a4e728ac929f"
|
||||
},
|
||||
{
|
||||
"ImportPath": "github.com/blevesearch/bleve/analysis/datetime_parsers/flexible_go",
|
||||
"Comment": "v0.5.0",
|
||||
"Rev": "97393d027342f43b17da2f02c090a4e728ac929f"
|
||||
},
|
||||
{
|
||||
"ImportPath": "github.com/blevesearch/bleve/analysis/language/en",
|
||||
"Comment": "v0.5.0",
|
||||
"Rev": "97393d027342f43b17da2f02c090a4e728ac929f"
|
||||
},
|
||||
{
|
||||
"ImportPath": "github.com/blevesearch/bleve/analysis/token_filters/lower_case_filter",
|
||||
"Comment": "v0.5.0",
|
||||
"Rev": "97393d027342f43b17da2f02c090a4e728ac929f"
|
||||
},
|
||||
{
|
||||
"ImportPath": "github.com/blevesearch/bleve/analysis/token_filters/porter",
|
||||
"Comment": "v0.5.0",
|
||||
"Rev": "97393d027342f43b17da2f02c090a4e728ac929f"
|
||||
},
|
||||
{
|
||||
"ImportPath": "github.com/blevesearch/bleve/analysis/token_filters/stop_tokens_filter",
|
||||
"Comment": "v0.5.0",
|
||||
"Rev": "97393d027342f43b17da2f02c090a4e728ac929f"
|
||||
},
|
||||
{
|
||||
"ImportPath": "github.com/blevesearch/bleve/analysis/tokenizers/unicode",
|
||||
"Comment": "v0.5.0",
|
||||
"Rev": "97393d027342f43b17da2f02c090a4e728ac929f"
|
||||
},
|
||||
{
|
||||
"ImportPath": "github.com/blevesearch/bleve/document",
|
||||
"Comment": "v0.5.0",
|
||||
"Rev": "97393d027342f43b17da2f02c090a4e728ac929f"
|
||||
},
|
||||
{
|
||||
"ImportPath": "github.com/blevesearch/bleve/index",
|
||||
"Comment": "v0.5.0",
|
||||
"Rev": "97393d027342f43b17da2f02c090a4e728ac929f"
|
||||
},
|
||||
{
|
||||
"ImportPath": "github.com/blevesearch/bleve/index/store",
|
||||
"Comment": "v0.5.0",
|
||||
"Rev": "97393d027342f43b17da2f02c090a4e728ac929f"
|
||||
},
|
||||
{
|
||||
"ImportPath": "github.com/blevesearch/bleve/index/store/boltdb",
|
||||
"Comment": "v0.5.0",
|
||||
"Rev": "97393d027342f43b17da2f02c090a4e728ac929f"
|
||||
},
|
||||
{
|
||||
"ImportPath": "github.com/blevesearch/bleve/index/store/gtreap",
|
||||
"Comment": "v0.5.0",
|
||||
"Rev": "97393d027342f43b17da2f02c090a4e728ac929f"
|
||||
},
|
||||
{
|
||||
"ImportPath": "github.com/blevesearch/bleve/index/upside_down",
|
||||
"Comment": "v0.5.0",
|
||||
"Rev": "97393d027342f43b17da2f02c090a4e728ac929f"
|
||||
},
|
||||
{
|
||||
"ImportPath": "github.com/blevesearch/bleve/numeric_util",
|
||||
"Comment": "v0.5.0",
|
||||
"Rev": "97393d027342f43b17da2f02c090a4e728ac929f"
|
||||
},
|
||||
{
|
||||
"ImportPath": "github.com/blevesearch/bleve/registry",
|
||||
"Comment": "v0.5.0",
|
||||
"Rev": "97393d027342f43b17da2f02c090a4e728ac929f"
|
||||
},
|
||||
{
|
||||
"ImportPath": "github.com/blevesearch/bleve/search",
|
||||
"Comment": "v0.5.0",
|
||||
"Rev": "97393d027342f43b17da2f02c090a4e728ac929f"
|
||||
},
|
||||
{
|
||||
"ImportPath": "github.com/blevesearch/bleve/search/collectors",
|
||||
"Comment": "v0.5.0",
|
||||
"Rev": "97393d027342f43b17da2f02c090a4e728ac929f"
|
||||
},
|
||||
{
|
||||
"ImportPath": "github.com/blevesearch/bleve/search/facets",
|
||||
"Comment": "v0.5.0",
|
||||
"Rev": "97393d027342f43b17da2f02c090a4e728ac929f"
|
||||
},
|
||||
{
|
||||
"ImportPath": "github.com/blevesearch/bleve/search/highlight",
|
||||
"Comment": "v0.5.0",
|
||||
"Rev": "97393d027342f43b17da2f02c090a4e728ac929f"
|
||||
},
|
||||
{
|
||||
"ImportPath": "github.com/blevesearch/bleve/search/highlight/fragment_formatters/html",
|
||||
"Comment": "v0.5.0",
|
||||
"Rev": "97393d027342f43b17da2f02c090a4e728ac929f"
|
||||
},
|
||||
{
|
||||
"ImportPath": "github.com/blevesearch/bleve/search/highlight/fragmenters/simple",
|
||||
"Comment": "v0.5.0",
|
||||
"Rev": "97393d027342f43b17da2f02c090a4e728ac929f"
|
||||
},
|
||||
{
|
||||
"ImportPath": "github.com/blevesearch/bleve/search/highlight/highlighters/html",
|
||||
"Comment": "v0.5.0",
|
||||
"Rev": "97393d027342f43b17da2f02c090a4e728ac929f"
|
||||
},
|
||||
{
|
||||
"ImportPath": "github.com/blevesearch/bleve/search/highlight/highlighters/simple",
|
||||
"Comment": "v0.5.0",
|
||||
"Rev": "97393d027342f43b17da2f02c090a4e728ac929f"
|
||||
},
|
||||
{
|
||||
"ImportPath": "github.com/blevesearch/bleve/search/scorers",
|
||||
"Comment": "v0.5.0",
|
||||
"Rev": "97393d027342f43b17da2f02c090a4e728ac929f"
|
||||
},
|
||||
{
|
||||
"ImportPath": "github.com/blevesearch/bleve/search/searchers",
|
||||
"Comment": "v0.5.0",
|
||||
"Rev": "97393d027342f43b17da2f02c090a4e728ac929f"
|
||||
},
|
||||
{
|
||||
"ImportPath": "github.com/blevesearch/go-porterstemmer",
|
||||
"Comment": "v1.0.1-9-g23a2c8e",
|
||||
"Rev": "23a2c8e5cf1f380f27722c6d2ae8896431dc7d0e"
|
||||
},
|
||||
{
|
||||
"ImportPath": "github.com/blevesearch/segment",
|
||||
"Rev": "762005e7a34fd909a84586299f1dd457371d36ee"
|
||||
},
|
||||
{
|
||||
"ImportPath": "github.com/boltdb/bolt",
|
||||
"Comment": "v1.3.0",
|
||||
"Rev": "583e8937c61f1af6513608ccc75c97b6abdf4ff9"
|
||||
},
|
||||
{
|
||||
"ImportPath": "github.com/golang/protobuf/proto",
|
||||
"Rev": "7cc19b78d562895b13596ddce7aafb59dd789318"
|
||||
},
|
||||
{
|
||||
"ImportPath": "github.com/gorilla/context",
|
||||
"Comment": "v1.1",
|
||||
"Rev": "1ea25387ff6f684839d82767c1733ff4d4d15d0a"
|
||||
},
|
||||
{
|
||||
"ImportPath": "github.com/gorilla/mux",
|
||||
"Comment": "v1.1",
|
||||
"Rev": "0eeaf8392f5b04950925b8a69fe70f110fa7cbfc"
|
||||
},
|
||||
{
|
||||
"ImportPath": "github.com/gorilla/securecookie",
|
||||
"Comment": "v1.1",
|
||||
"Rev": "667fe4e3466a040b780561fe9b51a83a3753eefc"
|
||||
},
|
||||
{
|
||||
"ImportPath": "github.com/gorilla/sessions",
|
||||
"Rev": "f7261893ca3ea922c30eabe742c036d2c1de6e0a"
|
||||
},
|
||||
{
|
||||
"ImportPath": "github.com/russross/blackfriday",
|
||||
"Comment": "v1.4-31-g2004188",
|
||||
"Rev": "2004188462c3946efefbdcd91aa83e49b4175bfb"
|
||||
},
|
||||
{
|
||||
"ImportPath": "github.com/shurcooL/sanitized_anchor_name",
|
||||
"Rev": "10ef21a441db47d8b13ebcc5fd2310f636973c77"
|
||||
},
|
||||
{
|
||||
"ImportPath": "github.com/steveyen/gtreap",
|
||||
"Rev": "0abe01ef9be25c4aedc174758ec2d917314d6d70"
|
||||
},
|
||||
{
|
||||
"ImportPath": "golang.org/x/crypto/bcrypt",
|
||||
"Rev": "9477e0b78b9ac3d0b03822fd95422e2fe07627cd"
|
||||
},
|
||||
{
|
||||
"ImportPath": "golang.org/x/crypto/blowfish",
|
||||
"Rev": "9477e0b78b9ac3d0b03822fd95422e2fe07627cd"
|
||||
},
|
||||
{
|
||||
"ImportPath": "golang.org/x/crypto/sha3",
|
||||
"Rev": "9477e0b78b9ac3d0b03822fd95422e2fe07627cd"
|
||||
},
|
||||
{
|
||||
"ImportPath": "golang.org/x/net/context",
|
||||
"Rev": "55cccaa02af1a99c69ba3213e33468628b61be4b"
|
||||
},
|
||||
{
|
||||
"ImportPath": "golang.org/x/sys/unix",
|
||||
"Rev": "a646d33e2ee3172a661fc09bca23bb4889a41bc8"
|
||||
}
|
||||
]
|
||||
}
|
||||
5
Godeps/Readme
generated
Normal file
5
Godeps/Readme
generated
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
This directory tree is generated automatically by godep.
|
||||
|
||||
Please do not edit.
|
||||
|
||||
See https://github.com/tools/godep for more information.
|
||||
57
Makefile
57
Makefile
|
|
@ -1,59 +1,32 @@
|
|||
CC=go
|
||||
BUILD=build -v -trimpath
|
||||
VERSION=$(shell cat VERSION)
|
||||
GCFLAGS=-gcflags '-e'
|
||||
LDFLAGS=-ldflags '-buildid=$(VERSION) -X main.version=$(VERSION) -s -w'
|
||||
BUILD=build -v
|
||||
LDFLAGS=--ldflags "-s -w"
|
||||
PROGRAM=gowiki
|
||||
CMD=-o $(PROGRAM) cmd/gowiki/main.go
|
||||
ENV=GOGC=0 CGO_ENABLED=0 GO111MODULE=on
|
||||
|
||||
all: $(PROGRAM)
|
||||
|
||||
setup:
|
||||
$(CC) install github.com/client9/misspell/cmd/misspell@latest
|
||||
$(CC) install honnef.co/go/tools/cmd/staticcheck@latest
|
||||
$(CC) get github.com/tools/godep
|
||||
$(CC) get github.com/client9/misspell
|
||||
|
||||
release: clean gofmt codeqa
|
||||
$(ENV) $(CC) $(BUILD) $(GCFLAGS) $(LDFLAGS) $(CMD)
|
||||
|
||||
release-vendor: clean gofmt codeqa
|
||||
$(ENV) $(CC) $(BUILD) -mod=vendor $(GCFLAGS) $(LDFLAGS) $(CMD)
|
||||
|
||||
vendor: clean gofmt
|
||||
$(ENV) $(CC) $(BUILD) -mod=vendor $(GCFLAGS) $(LDFLAGS) $(CMD)
|
||||
|
||||
$(PROGRAM): clean gofmt
|
||||
$(ENV) $(CC) $(BUILD) $(GCFLAGS) $(LDFLAGS) $(CMD)
|
||||
$(PROGRAM): clean codeqa
|
||||
CGO_ENABLED=0 $(CC) $(BUILD) $(LDFLAGS)
|
||||
|
||||
clean:
|
||||
$(CC) clean -x
|
||||
|
||||
codeqa: templates gofmt misspell golint
|
||||
|
||||
gofmt:
|
||||
$(CC) fmt ./...
|
||||
gofmt -w .
|
||||
|
||||
codeqa: govet misspell staticcheck test
|
||||
|
||||
govet:
|
||||
$(CC) vet ./...
|
||||
|
||||
update:
|
||||
$(ENV) $(CC) get -u ./...
|
||||
golint:
|
||||
$(GOPATH)/bin/golint .
|
||||
|
||||
misspell:
|
||||
$(GOPATH)/bin/misspell cmd/* pkg/* Makefile README.md sri.sh
|
||||
$(GOPATH)/bin/misspell **/*
|
||||
|
||||
staticcheck:
|
||||
$(GOPATH)/bin/staticcheck ./...
|
||||
templates:
|
||||
$(CC) generate
|
||||
|
||||
test:
|
||||
ifeq ($(shell go env GOARCH), $(shell go env GOHOSTARCH))
|
||||
ifeq ($(shell go env GOOS), $(shell go env GOHOSTOS))
|
||||
$(CC) test ./...
|
||||
else
|
||||
$(info skipping tests of other platforms)
|
||||
endif
|
||||
else
|
||||
$(info skipping tests of other platforms)
|
||||
endif
|
||||
|
||||
.PHONY: setup release release-vendor vendor build clean codeqa gofmt govet misspell staticcheck test
|
||||
.PHONY: setup build clean templates gofmt misspell golint
|
||||
|
|
|
|||
60
README.md
60
README.md
|
|
@ -1,61 +1 @@
|
|||
# GoWiki - a simple wiki engine written in Go
|
||||
|
||||
## Building
|
||||
|
||||
**Requirements:**
|
||||
|
||||
* Go (golang) >= 1.16
|
||||
|
||||
Linux:
|
||||
|
||||
```
|
||||
make
|
||||
|
||||
or offline
|
||||
|
||||
make vendor
|
||||
```
|
||||
|
||||
OpenBSD:
|
||||
|
||||
```
|
||||
GOOS=openbsd make
|
||||
|
||||
or offline
|
||||
|
||||
GOOS=openbsd make vendor
|
||||
```
|
||||
|
||||
## Install
|
||||
|
||||
See the `scripts/` directory on how to install gowiki on Linux or OpenBSD.
|
||||
|
||||
## Config
|
||||
|
||||
```
|
||||
{
|
||||
"data_dir": "/var/lib/gowiki", // running directory. data will be stored here
|
||||
"user": "wiki", // drop privileges to user
|
||||
"group": "wiki", // drop privileges to group
|
||||
"listen_addr": "127.0.0.1:8080", // listening <address>:<port>
|
||||
"log_file": "gowiki.log", // log file (use - for stdout, this only works in combination with 'foreground: true')
|
||||
"secret": "", // static hmac secret (at least 64 chars, used for JWT signing)
|
||||
"admin": true, // set to false to disable the default admin user
|
||||
"foreground": false, // do not fork into the background
|
||||
"secure_cookie": false, // enable secure cookie flag
|
||||
"debug": false // enable debugging
|
||||
}
|
||||
```
|
||||
|
||||
## Features
|
||||
|
||||
* Fulltext indexing of wiki pages
|
||||
* Password hashing using BCrypt
|
||||
* 2FA using TOTP
|
||||
* Content Security Policy
|
||||
* No Javascript (yet)
|
||||
* Private wiki pages
|
||||
|
||||
### GoWiki on OpenBSD
|
||||
|
||||
GoWiki makes use of the OpenBSD native security features [pledge(2)](https://man.openbsd.org/pledge) and [unveil(2)](https://man.openbsd.org/unveil).
|
||||
|
|
|
|||
1
VERSION
1
VERSION
|
|
@ -1 +0,0 @@
|
|||
v2.0
|
||||
|
|
@ -1,11 +0,0 @@
|
|||
// Copyright (C) 2021 Marius Schellenberger
|
||||
|
||||
package main
|
||||
|
||||
import "git.giftfish.de/ston1th/gowiki/pkg/cmd"
|
||||
|
||||
var version string
|
||||
|
||||
func main() {
|
||||
cmd.Run(version)
|
||||
}
|
||||
250
context.go
Normal file
250
context.go
Normal file
|
|
@ -0,0 +1,250 @@
|
|||
// Copyright (C) 2017 Marius Schellenberger
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"crypto/subtle"
|
||||
"encoding/base64"
|
||||
"errors"
|
||||
"github.com/gorilla/mux"
|
||||
"github.com/gorilla/sessions"
|
||||
"html/template"
|
||||
"io"
|
||||
"net/http"
|
||||
"time"
|
||||
)
|
||||
|
||||
const (
|
||||
keyLen = 32
|
||||
authLen = keyLen * 2
|
||||
cookieName = "gosession"
|
||||
)
|
||||
|
||||
var store *sessions.CookieStore
|
||||
|
||||
func cookieStore() {
|
||||
options := &sessions.Options{
|
||||
Path: "/",
|
||||
MaxAge: 3600 * 12,
|
||||
Secure: secureCookie,
|
||||
HttpOnly: true,
|
||||
}
|
||||
authKey := genKey(authLen)
|
||||
encKey := genKey(keyLen)
|
||||
store = sessions.NewCookieStore(authKey, encKey)
|
||||
store.Options = options
|
||||
|
||||
go func() {
|
||||
for {
|
||||
time.Sleep(time.Hour * 12)
|
||||
newAuthKey := genKey(authLen)
|
||||
newEncKey := genKey(keyLen)
|
||||
newStore := sessions.NewCookieStore(newAuthKey, newEncKey, authKey, encKey)
|
||||
newStore.Options = options
|
||||
|
||||
store = newStore
|
||||
authKey = newAuthKey
|
||||
encKey = newEncKey
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
func genKey(length int) (bytes []byte) {
|
||||
bytes = make([]byte, length)
|
||||
if _, err := io.ReadFull(rand.Reader, bytes); err != nil {
|
||||
log.Println("genKey:", err)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func newContext(w http.ResponseWriter, r *http.Request) *Context {
|
||||
log.Printf("%s %s %s\n", r.RemoteAddr, r.Method, r.URL.Path)
|
||||
h := w.Header()
|
||||
h.Set("X-Frame-Options", "DENY")
|
||||
h.Set("X-Content-Type-Options", "nosniff")
|
||||
h.Set("X-XSS-Protection", "1; mode=block")
|
||||
h.Set("Content-Security-Policy", "default-src 'none';style-src 'self' 'unsafe-inline';frame-ancestors 'none'")
|
||||
return &Context{
|
||||
Request: r,
|
||||
Response: w,
|
||||
}
|
||||
}
|
||||
|
||||
type Context struct {
|
||||
Request *http.Request
|
||||
Response http.ResponseWriter
|
||||
T *template.Template
|
||||
|
||||
Data webData
|
||||
}
|
||||
|
||||
type webData struct {
|
||||
Title string
|
||||
BodyTitle string
|
||||
|
||||
Admin bool
|
||||
Login bool
|
||||
|
||||
User string
|
||||
Token string
|
||||
Msg string
|
||||
Search string
|
||||
|
||||
Data interface{}
|
||||
}
|
||||
|
||||
func (c *Context) Exec() {
|
||||
if c.T == nil {
|
||||
log.Println("Exec:", errors.New("template is nil"))
|
||||
return
|
||||
}
|
||||
c.Data.Admin = c.GetAdmin()
|
||||
c.Data.User = c.GetUser()
|
||||
c.Data.Login = c.LoggedOn()
|
||||
err := c.T.Execute(c.Response, c.Data)
|
||||
if err != nil {
|
||||
log.Println("Exec:", err)
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Context) Template(name string) {
|
||||
c.T = templ[name]
|
||||
}
|
||||
|
||||
func (c *Context) Write(buf []byte) (err error) {
|
||||
_, err = c.Response.Write(buf)
|
||||
return
|
||||
}
|
||||
|
||||
func (c *Context) SetHeader(name, value string) {
|
||||
c.Response.Header().Set(name, value)
|
||||
}
|
||||
|
||||
func (c *Context) Redirect(uri string, code int) {
|
||||
http.Redirect(c.Response, c.Request, uri, code)
|
||||
}
|
||||
|
||||
func (c *Context) Method() string {
|
||||
return c.Request.Method
|
||||
}
|
||||
|
||||
func (c *Context) Path() string {
|
||||
return c.Request.URL.Path
|
||||
}
|
||||
|
||||
func (c *Context) FormSlice(name string) []string {
|
||||
// does nothing if called twice
|
||||
c.Request.ParseForm()
|
||||
return c.Request.PostForm[name]
|
||||
}
|
||||
|
||||
func (c *Context) Form(name string) (ret string) {
|
||||
ret = c.Request.PostFormValue(name)
|
||||
return
|
||||
}
|
||||
|
||||
func (c *Context) Var(name string) (ret string) {
|
||||
ret, _ = mux.Vars(c.Request)[name]
|
||||
return
|
||||
}
|
||||
|
||||
func (c *Context) InitToken() {
|
||||
token := c.getToken()
|
||||
if token == nil || len(token) != keyLen {
|
||||
s, _ := store.Get(c.Request, cookieName)
|
||||
s.Values["token"] = genKey(keyLen)
|
||||
s.Options = store.Options
|
||||
err := s.Save(c.Request, c.Response)
|
||||
if err != nil {
|
||||
log.Println("InitToken: ", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// NewToken sets and returns the xsrf token
|
||||
func (c *Context) NewToken() {
|
||||
rnd := genKey(keyLen)
|
||||
c.Data.Token = base64.StdEncoding.EncodeToString(append(rnd, xor(rnd, c.getToken())...))
|
||||
}
|
||||
|
||||
// CheckToken validates the xsrf token
|
||||
func (c *Context) CheckToken() bool {
|
||||
token, err := base64.StdEncoding.DecodeString(c.Form("token"))
|
||||
if err != nil {
|
||||
log.Println("CheckToken: ", err)
|
||||
return false
|
||||
}
|
||||
if len(token) != authLen {
|
||||
return false
|
||||
}
|
||||
return subtle.ConstantTimeCompare(c.getToken(), xor(token[keyLen:], token[:keyLen])) == 1
|
||||
}
|
||||
|
||||
func (c *Context) LoggedOn() (ok bool) {
|
||||
s, _ := store.Get(c.Request, cookieName)
|
||||
_, ok = s.Values["user"]
|
||||
return
|
||||
}
|
||||
|
||||
func (c *Context) SetUdata(user string, admin bool) {
|
||||
s, _ := store.Get(c.Request, cookieName)
|
||||
s.Values["user"] = user
|
||||
s.Values["admin"] = admin
|
||||
s.Options = store.Options
|
||||
if err := s.Save(c.Request, c.Response); err != nil {
|
||||
log.Println(err)
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Context) UnsetUdata() {
|
||||
s, _ := store.Get(c.Request, cookieName)
|
||||
s.Values = make(map[interface{}]interface{})
|
||||
s.Options = store.Options
|
||||
if err := s.Save(c.Request, c.Response); err != nil {
|
||||
log.Println(err)
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Context) GetUser() string {
|
||||
s, _ := store.Get(c.Request, cookieName)
|
||||
if ret, ok := s.Values["user"].(string); ok {
|
||||
return ret
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (c *Context) GetAdmin() bool {
|
||||
s, _ := store.Get(c.Request, cookieName)
|
||||
if ret, ok := s.Values["admin"].(bool); ok {
|
||||
return ret
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (c *Context) getToken() []byte {
|
||||
s, _ := store.Get(c.Request, cookieName)
|
||||
t, ok := s.Values["token"]
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
b, ok := t.([]byte)
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
return b
|
||||
}
|
||||
|
||||
func xor(a, b []byte) (c []byte) {
|
||||
n := len(a)
|
||||
if len(b) != n {
|
||||
return
|
||||
}
|
||||
c = make([]byte, n)
|
||||
for i := 0; i < n; i++ {
|
||||
c[i] = a[i] ^ b[i]
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
type ctxHandler func(*Context)
|
||||
86
dump.go
Normal file
86
dump.go
Normal file
|
|
@ -0,0 +1,86 @@
|
|||
// Copyright (C) 2017 Marius Schellenberger
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/blevesearch/bleve"
|
||||
)
|
||||
|
||||
type DumpArticle struct {
|
||||
Title string `json:"title"`
|
||||
LinkTitle string `json:"link_title"`
|
||||
MD string `json:"md"`
|
||||
Created string `json:"created"`
|
||||
Updated string `json:"updated"`
|
||||
Public bool `json:"public"`
|
||||
}
|
||||
|
||||
func verboseLog(prefix, title string) {
|
||||
if verbose {
|
||||
fmt.Fprintln(os.Stderr, prefix, title)
|
||||
}
|
||||
}
|
||||
|
||||
func dumpWiki() error {
|
||||
index, err := bleve.OpenUsing(bleveStore, map[string]interface{}{"read_only": true})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
engine = NewEngine(index)
|
||||
art, err := engine.Get(false, indexName)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer engine.Close()
|
||||
var arts []DumpArticle
|
||||
titles, err := engine.Dump()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, title := range titles {
|
||||
art, err = engine.Get(false, title)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
verboseLog("dump:", art.Title)
|
||||
arts = append(arts, DumpArticle{
|
||||
Title: art.Title,
|
||||
LinkTitle: art.LinkTitle,
|
||||
MD: art.MD,
|
||||
Created: art.Created,
|
||||
Updated: art.Updated,
|
||||
Public: art.Public,
|
||||
})
|
||||
}
|
||||
return json.NewEncoder(os.Stdout).Encode(arts)
|
||||
}
|
||||
|
||||
func restoreWiki() (err error) {
|
||||
index, err := bleve.Open(bleveStore)
|
||||
if err != nil {
|
||||
mapping := bleve.NewIndexMapping()
|
||||
index, err = bleve.New(bleveStore, mapping)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
engine = NewEngine(index)
|
||||
defer engine.Close()
|
||||
var arts []DumpArticle
|
||||
err = json.NewDecoder(os.Stdin).Decode(&arts)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
for _, art := range arts {
|
||||
verboseLog("restore:", art.Title)
|
||||
err = engine.Restore(art)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
270
engine.go
Normal file
270
engine.go
Normal file
|
|
@ -0,0 +1,270 @@
|
|||
// Copyright (C) 2017 Marius Schellenberger
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"html/template"
|
||||
"regexp"
|
||||
"time"
|
||||
|
||||
"github.com/blevesearch/bleve"
|
||||
"github.com/blevesearch/bleve/document"
|
||||
)
|
||||
|
||||
const (
|
||||
timeFmt = "2006-01-02 15:04:05"
|
||||
maxAllResult = 1000000
|
||||
maxSearchResult = 101
|
||||
)
|
||||
|
||||
var titleRe = regexp.MustCompile("[^a-zA-Z0-9+ ]+")
|
||||
|
||||
func now() string {
|
||||
return time.Now().Format(timeFmt)
|
||||
}
|
||||
|
||||
type Article struct {
|
||||
Title string `json:"title"`
|
||||
LinkTitle string
|
||||
Search string `json:"search"`
|
||||
Index template.HTML `json:"index"`
|
||||
Text template.HTML `json:"text"`
|
||||
MD string `json:"md"`
|
||||
Created string `json:"created"`
|
||||
Updated string `json:"updated"`
|
||||
Public bool `json:"public"`
|
||||
}
|
||||
|
||||
type Engine struct {
|
||||
index bleve.Index
|
||||
}
|
||||
|
||||
func NewEngine(index bleve.Index) *Engine {
|
||||
return &Engine{index}
|
||||
}
|
||||
|
||||
func (e *Engine) Close() error {
|
||||
return e.index.Close()
|
||||
}
|
||||
|
||||
func (e *Engine) Restore(dumpArt DumpArticle) error {
|
||||
art := render(dumpArt.MD)
|
||||
art.Title = dumpArt.Title
|
||||
art.Created = dumpArt.Created
|
||||
art.Updated = dumpArt.Updated
|
||||
art.Public = dumpArt.Public
|
||||
return e.index.Index(dumpArt.LinkTitle, art)
|
||||
}
|
||||
|
||||
func (e *Engine) Index(title, text, user string, public bool) (string, error) {
|
||||
if user == "" {
|
||||
return "", errors.New("engine: user cannot be empty")
|
||||
}
|
||||
title = titleRe.ReplaceAllString(title, "")
|
||||
newTitle := makeLinkTitle(title)
|
||||
if err := e.Exists(false, newTitle); err == nil {
|
||||
return "", errors.New("engine: article '" + title + "' already exists")
|
||||
}
|
||||
art := render(text)
|
||||
art.Title = title
|
||||
art.Created = user + " " + now()
|
||||
art.Public = public
|
||||
err := e.index.Index(newTitle, art)
|
||||
return newTitle, err
|
||||
}
|
||||
|
||||
func (e *Engine) Exists(public bool, title string) error {
|
||||
_, err := e.Get(public, title)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (e *Engine) Delete(title string) error {
|
||||
if title == indexName {
|
||||
return errors.New("engine: page '" + indexName + "' cannot be deleted")
|
||||
}
|
||||
if err := e.Exists(false, title); err != nil {
|
||||
return err
|
||||
}
|
||||
return e.index.Delete(title)
|
||||
}
|
||||
|
||||
func (e *Engine) Update(title, text, user string, public bool) error {
|
||||
if user == "" {
|
||||
return errors.New("engine: user cannot be empty")
|
||||
}
|
||||
if title == indexName && !public {
|
||||
return errors.New("engine: page '" + indexName + "' must stay public")
|
||||
}
|
||||
if err := e.Exists(false, title); err != nil {
|
||||
return err
|
||||
}
|
||||
art, err := e.Get(false, title)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
rend := render(text)
|
||||
art.Updated = user + " " + now()
|
||||
art.Search = rend.Search
|
||||
art.Index = rend.Index
|
||||
art.Text = rend.Text
|
||||
art.MD = rend.MD
|
||||
art.Public = public
|
||||
return e.index.Index(title, art)
|
||||
}
|
||||
|
||||
func (e *Engine) Get(public bool, title string) (Article, error) {
|
||||
var art Article
|
||||
doc, err := e.index.Document(title)
|
||||
if err != nil {
|
||||
return art, err
|
||||
}
|
||||
if doc == nil || doc.Fields == nil {
|
||||
return art, errors.New("engine: article '" + title + "' does not esist")
|
||||
}
|
||||
for _, field := range doc.Fields {
|
||||
switch f := field.(type) {
|
||||
case *document.TextField:
|
||||
switch field.Name() {
|
||||
case "title":
|
||||
art.Title = string(f.Value())
|
||||
case "index":
|
||||
art.Index = template.HTML(string(f.Value()))
|
||||
case "text":
|
||||
art.Text = template.HTML(string(f.Value()))
|
||||
case "md":
|
||||
art.MD = string(f.Value())
|
||||
case "created":
|
||||
art.Created = string(f.Value())
|
||||
case "updated":
|
||||
art.Updated = string(f.Value())
|
||||
}
|
||||
case *document.BooleanField:
|
||||
if field.Name() == "public" {
|
||||
p, err := f.Boolean()
|
||||
if err != nil {
|
||||
return art, err
|
||||
}
|
||||
art.Public = p
|
||||
}
|
||||
}
|
||||
}
|
||||
art.LinkTitle = title
|
||||
if public && !art.Public {
|
||||
err = errors.New("engine: article '" + title + "' does not esist")
|
||||
}
|
||||
return art, err
|
||||
}
|
||||
|
||||
func (e *Engine) Search(public bool, search string) (string, error) {
|
||||
var (
|
||||
res *bleve.SearchResult
|
||||
req *bleve.SearchRequest
|
||||
err error
|
||||
)
|
||||
if public {
|
||||
req = bleve.NewSearchRequest(bleve.NewConjunctionQuery([]bleve.Query{
|
||||
bleve.NewBoolFieldQuery(true).SetField("public"),
|
||||
bleve.NewQueryStringQuery("title:" + search + " search:" + search),
|
||||
}))
|
||||
} else {
|
||||
req = bleve.NewSearchRequest(bleve.NewQueryStringQuery("title:" + search + " search:" + search))
|
||||
}
|
||||
req.Highlight = bleve.NewHighlightWithStyle("html")
|
||||
req.Size = maxSearchResult
|
||||
res, err = e.index.Search(req)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if res.Total == 0 {
|
||||
return "<b>No search results</b>", nil
|
||||
} else if res.Total == maxSearchResult {
|
||||
return "", errors.New("engine: too many search results")
|
||||
}
|
||||
html := ""
|
||||
for _, hit := range res.Hits {
|
||||
if hit == nil {
|
||||
continue
|
||||
}
|
||||
art, err := e.Get(false, hit.ID)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
html += `<a href="/wiki/` + hit.ID + `"><b>` + art.Title + "</b></a>"
|
||||
for fragField, frags := range hit.Fragments {
|
||||
if fragField == "title" || fragField == "search" {
|
||||
html += `<pre style="white-space: pre-wrap">`
|
||||
for _, f := range frags {
|
||||
html += f
|
||||
}
|
||||
html += "</pre>"
|
||||
break
|
||||
}
|
||||
}
|
||||
html += "<br>"
|
||||
}
|
||||
return html, nil
|
||||
}
|
||||
|
||||
type Title struct {
|
||||
Title string
|
||||
LinkTitle string
|
||||
}
|
||||
|
||||
func (e *Engine) GetAll(public bool) (t []Title, err error) {
|
||||
var (
|
||||
res *bleve.SearchResult
|
||||
req *bleve.SearchRequest
|
||||
)
|
||||
if public {
|
||||
req = bleve.NewSearchRequest(bleve.NewBoolFieldQuery(true).SetField("public"))
|
||||
} else {
|
||||
req = bleve.NewSearchRequest(bleve.NewMatchAllQuery())
|
||||
}
|
||||
req.Size = maxAllResult
|
||||
res, err = e.index.Search(req)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
for _, v := range res.Hits {
|
||||
if v == nil {
|
||||
continue
|
||||
}
|
||||
if v.ID == indexName {
|
||||
continue
|
||||
}
|
||||
doc, err := e.index.Document(v.ID)
|
||||
if err != nil || doc == nil || doc.Fields == nil {
|
||||
continue
|
||||
}
|
||||
for _, field := range doc.Fields {
|
||||
if field.Name() == "title" {
|
||||
f, ok := field.(*document.TextField)
|
||||
if ok {
|
||||
t = append(t, Title{string(f.Value()), v.ID})
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (e *Engine) Dump() (titles []string, err error) {
|
||||
req := bleve.NewSearchRequest(bleve.NewMatchAllQuery())
|
||||
req.Size = maxAllResult
|
||||
res, err := e.index.Search(req)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
for _, v := range res.Hits {
|
||||
if v == nil {
|
||||
continue
|
||||
}
|
||||
titles = append(titles, v.ID)
|
||||
}
|
||||
return
|
||||
}
|
||||
40
go.mod
40
go.mod
|
|
@ -1,40 +0,0 @@
|
|||
module git.giftfish.de/ston1th/gowiki
|
||||
|
||||
go 1.24.0
|
||||
|
||||
require (
|
||||
git.giftfish.de/ston1th/godrop/v2 v2.2.0
|
||||
git.giftfish.de/ston1th/jwt/v3 v3.3.0
|
||||
github.com/blevesearch/bleve v1.0.14
|
||||
github.com/gorilla/mux v1.8.1
|
||||
github.com/pquerna/otp v1.5.0
|
||||
github.com/russross/blackfriday v1.6.0
|
||||
github.com/urfave/cli v1.22.17
|
||||
go.etcd.io/bbolt v1.4.3
|
||||
golang.org/x/crypto v0.43.0
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/RoaringBitmap/roaring v1.9.4 // indirect
|
||||
github.com/bits-and-blooms/bitset v1.24.1 // indirect
|
||||
github.com/blevesearch/go-porterstemmer v1.0.3 // indirect
|
||||
github.com/blevesearch/mmap-go v1.0.4 // indirect
|
||||
github.com/blevesearch/segment v0.9.1 // indirect
|
||||
github.com/blevesearch/snowballstem v0.9.0 // indirect
|
||||
github.com/blevesearch/zap/v11 v11.0.14 // indirect
|
||||
github.com/blevesearch/zap/v12 v12.0.14 // indirect
|
||||
github.com/blevesearch/zap/v13 v13.0.6 // indirect
|
||||
github.com/blevesearch/zap/v14 v14.0.5 // indirect
|
||||
github.com/blevesearch/zap/v15 v15.0.3 // indirect
|
||||
github.com/boombuler/barcode v1.1.0 // indirect
|
||||
github.com/couchbase/vellum v1.0.2 // indirect
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.7 // indirect
|
||||
github.com/golang/protobuf v1.5.4 // indirect
|
||||
github.com/golang/snappy v1.0.0 // indirect
|
||||
github.com/mschoch/smat v0.2.0 // indirect
|
||||
github.com/russross/blackfriday/v2 v2.1.0 // indirect
|
||||
github.com/steveyen/gtreap v0.1.0 // indirect
|
||||
github.com/willf/bitset v1.1.11 // indirect
|
||||
golang.org/x/sys v0.37.0 // indirect
|
||||
google.golang.org/protobuf v1.36.10 // indirect
|
||||
)
|
||||
171
go.sum
171
go.sum
|
|
@ -1,171 +0,0 @@
|
|||
git.giftfish.de/ston1th/godrop/v2 v2.2.0 h1:jdcPJeotiuug3tC4zXvwnauen1Y7kfF9CbFKQbGttYU=
|
||||
git.giftfish.de/ston1th/godrop/v2 v2.2.0/go.mod h1:gDAD0cWURkN0cMAjjSKB6a6jqIXvOrY8Th25HdoJAyg=
|
||||
git.giftfish.de/ston1th/jwt/v3 v3.3.0 h1:lSBVbk1D+KmLHBlxhS7snK33+U975cNbx/Z4Md6S+0A=
|
||||
git.giftfish.de/ston1th/jwt/v3 v3.3.0/go.mod h1:jDALrW7OKeIaqYuMxs3GWyPRnQZmk/R+Lu9p/Em3Ziw=
|
||||
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
|
||||
github.com/BurntSushi/toml v1.5.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho=
|
||||
github.com/RoaringBitmap/roaring v0.4.23/go.mod h1:D0gp8kJQgE1A4LQ5wFLggQEyvDi06Mq5mKs52e1TwOo=
|
||||
github.com/RoaringBitmap/roaring v1.9.4 h1:yhEIoH4YezLYT04s1nHehNO64EKFTop/wBhxv2QzDdQ=
|
||||
github.com/RoaringBitmap/roaring v1.9.4/go.mod h1:6AXUsoIEzDTFFQCe1RbGA6uFONMhvejWj5rqITANK90=
|
||||
github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8=
|
||||
github.com/bits-and-blooms/bitset v1.12.0/go.mod h1:7hO7Gc7Pp1vODcmWvKMRA9BNmbv6a/7QIWpPxHddWR8=
|
||||
github.com/bits-and-blooms/bitset v1.24.1 h1:hqnfFbjjk3pxGa5E9Ho3hjoU7odtUuNmJ9Ao+Bo8s1c=
|
||||
github.com/bits-and-blooms/bitset v1.24.1/go.mod h1:7hO7Gc7Pp1vODcmWvKMRA9BNmbv6a/7QIWpPxHddWR8=
|
||||
github.com/blevesearch/bleve v1.0.14 h1:Q8r+fHTt35jtGXJUM0ULwM3Tzg+MRfyai4ZkWDy2xO4=
|
||||
github.com/blevesearch/bleve v1.0.14/go.mod h1:e/LJTr+E7EaoVdkQZTfoz7dt4KoDNvDbLb8MSKuNTLQ=
|
||||
github.com/blevesearch/blevex v1.0.0 h1:pnilj2Qi3YSEGdWgLj1Pn9Io7ukfXPoQcpAI1Bv8n/o=
|
||||
github.com/blevesearch/blevex v1.0.0/go.mod h1:2rNVqoG2BZI8t1/P1awgTKnGlx5MP9ZbtEciQaNhswc=
|
||||
github.com/blevesearch/cld2 v0.0.0-20200327141045-8b5f551d37f5/go.mod h1:PN0QNTLs9+j1bKy3d/GB/59wsNBFC4sWLWG3k69lWbc=
|
||||
github.com/blevesearch/go-porterstemmer v1.0.3 h1:GtmsqID0aZdCSNiY8SkuPJ12pD4jI+DdXTAn4YRcHCo=
|
||||
github.com/blevesearch/go-porterstemmer v1.0.3/go.mod h1:angGc5Ht+k2xhJdZi511LtmxuEf0OVpvUUNrwmM1P7M=
|
||||
github.com/blevesearch/mmap-go v1.0.2/go.mod h1:ol2qBqYaOUsGdm7aRMRrYGgPvnwLe6Y+7LMvAB5IbSA=
|
||||
github.com/blevesearch/mmap-go v1.0.4 h1:OVhDhT5B/M1HNPpYPBKIEJaD0F3Si+CrEKULGCDPWmc=
|
||||
github.com/blevesearch/mmap-go v1.0.4/go.mod h1:EWmEAOmdAS9z/pi/+Toxu99DnsbhG1TIxUoRmJw/pSs=
|
||||
github.com/blevesearch/segment v0.9.0/go.mod h1:9PfHYUdQCgHktBgvtUOF4x+pc4/l8rdH0u5spnW85UQ=
|
||||
github.com/blevesearch/segment v0.9.1 h1:+dThDy+Lvgj5JMxhmOVlgFfkUtZV2kw49xax4+jTfSU=
|
||||
github.com/blevesearch/segment v0.9.1/go.mod h1:zN21iLm7+GnBHWTao9I+Au/7MBiL8pPFtJBJTsk6kQw=
|
||||
github.com/blevesearch/snowballstem v0.9.0 h1:lMQ189YspGP6sXvZQ4WZ+MLawfV8wOmPoD/iWeNXm8s=
|
||||
github.com/blevesearch/snowballstem v0.9.0/go.mod h1:PivSj3JMc8WuaFkTSRDW2SlrulNWPl4ABg1tC/hlgLs=
|
||||
github.com/blevesearch/zap/v11 v11.0.14 h1:IrDAvtlzDylh6H2QCmS0OGcN9Hpf6mISJlfKjcwJs7k=
|
||||
github.com/blevesearch/zap/v11 v11.0.14/go.mod h1:MUEZh6VHGXv1PKx3WnCbdP404LGG2IZVa/L66pyFwnY=
|
||||
github.com/blevesearch/zap/v12 v12.0.14 h1:2o9iRtl1xaRjsJ1xcqTyLX414qPAwykHNV7wNVmbp3w=
|
||||
github.com/blevesearch/zap/v12 v12.0.14/go.mod h1:rOnuZOiMKPQj18AEKEHJxuI14236tTQ1ZJz4PAnWlUg=
|
||||
github.com/blevesearch/zap/v13 v13.0.6 h1:r+VNSVImi9cBhTNNR+Kfl5uiGy8kIbb0JMz/h8r6+O4=
|
||||
github.com/blevesearch/zap/v13 v13.0.6/go.mod h1:L89gsjdRKGyGrRN6nCpIScCvvkyxvmeDCwZRcjjPCrw=
|
||||
github.com/blevesearch/zap/v14 v14.0.5 h1:NdcT+81Nvmp2zL+NhwSvGSLh7xNgGL8QRVZ67njR0NU=
|
||||
github.com/blevesearch/zap/v14 v14.0.5/go.mod h1:bWe8S7tRrSBTIaZ6cLRbgNH4TUDaC9LZSpRGs85AsGY=
|
||||
github.com/blevesearch/zap/v15 v15.0.3 h1:Ylj8Oe+mo0P25tr9iLPp33lN6d4qcztGjaIsP51UxaY=
|
||||
github.com/blevesearch/zap/v15 v15.0.3/go.mod h1:iuwQrImsh1WjWJ0Ue2kBqY83a0rFtJTqfa9fp1rbVVU=
|
||||
github.com/boombuler/barcode v1.0.1-0.20190219062509-6c824513bacc/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8=
|
||||
github.com/boombuler/barcode v1.1.0 h1:ChaYjBR63fr4LFyGn8E8nt7dBSt3MiU3zMOZqFvVkHo=
|
||||
github.com/boombuler/barcode v1.1.0/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8=
|
||||
github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE=
|
||||
github.com/coreos/go-etcd v2.0.0+incompatible/go.mod h1:Jez6KQU2B/sWsbdaef3ED8NzMklzPG4d5KIOhIy30Tk=
|
||||
github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
|
||||
github.com/couchbase/ghistogram v0.1.0/go.mod h1:s1Jhy76zqfEecpNWJfWUiKZookAFaiGOEoyzgHt9i7k=
|
||||
github.com/couchbase/moss v0.1.0/go.mod h1:9MaHIaRuy9pvLPUJxB8sh8OrLfyDczECVL37grCIubs=
|
||||
github.com/couchbase/vellum v1.0.2 h1:BrbP0NKiyDdndMPec8Jjhy0U47CZ0Lgx3xUC2r9rZqw=
|
||||
github.com/couchbase/vellum v1.0.2/go.mod h1:FcwrEivFpNi24R3jLOs3n+fs5RnuQnQqCLBJ1uAg1W4=
|
||||
github.com/cpuguy83/go-md2man v1.0.10/go.mod h1:SmD6nW6nTyfqj6ABTjUi3V3JVMnlJmwcJI5acqYI6dE=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.7 h1:zbFlGlXEAKlwXpmvle3d8Oe3YnkKIK4xSRTd3sHPnBo=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.7/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
|
||||
github.com/cznic/b v0.0.0-20181122101859-a26611c4d92d h1:SwD98825d6bdB+pEuTxWOXiSjBrHdOl/UVp75eI7JT8=
|
||||
github.com/cznic/b v0.0.0-20181122101859-a26611c4d92d/go.mod h1:URriBxXwVq5ijiJ12C7iIZqlA69nTlI+LgI6/pwftG8=
|
||||
github.com/cznic/mathutil v0.0.0-20181122101859-297441e03548/go.mod h1:e6NPNENfs9mPDVNRekM7lKScauxd5kXTr1Mfyig6TDM=
|
||||
github.com/cznic/strutil v0.0.0-20181122101858-275e90344537/go.mod h1:AHHPPPXTw0h6pVabbcbyGRK1DckRn7r/STdZEeIDzZc=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/facebookgo/ensure v0.0.0-20200202191622-63f1cf65ac4c/go.mod h1:Yg+htXGokKKdzcwhuNDwVvN+uBxDGXJ7G/VN1d8fa64=
|
||||
github.com/facebookgo/stack v0.0.0-20160209184415-751773369052/go.mod h1:UbMTZqLaRiH3MsBH8va0n7s1pQYcu3uTb8G4tygF4Zg=
|
||||
github.com/facebookgo/subset v0.0.0-20200203212716-c811ad88dec4/go.mod h1:5tD+neXqOorC30/tWg0LCSkrqj/AR6gu8yY8/fpw1q0=
|
||||
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
|
||||
github.com/glycerine/go-unsnap-stream v0.0.0-20181221182339-f9677308dec2/go.mod h1:/20jfyN9Y5QPEAprSgKAUr+glWDY39ZiUEAYOEv5dsE=
|
||||
github.com/glycerine/goconvey v0.0.0-20190410193231-58a59202ab31/go.mod h1:Ogl1Tioa0aV7gstGFO7KhffUsb9M4ydbEbbxpcEDc24=
|
||||
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
|
||||
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
|
||||
github.com/golang/snappy v0.0.0-20180518054509-2e65f85255db/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
|
||||
github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
|
||||
github.com/golang/snappy v1.0.0 h1:Oy607GVXHs7RtbggtPBnr2RmDArIsAefDwvrdWvRhGs=
|
||||
github.com/golang/snappy v1.0.0/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
|
||||
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
||||
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
||||
github.com/gopherjs/gopherjs v0.0.0-20190910122728-9d188e94fb99/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
|
||||
github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY=
|
||||
github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ=
|
||||
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
|
||||
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
|
||||
github.com/ikawaha/kagome.ipadic v1.1.2/go.mod h1:DPSBbU0czaJhAb/5uKQZHMc9MTVRpDugJfX+HddPHHg=
|
||||
github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
|
||||
github.com/jmhodges/levigo v1.0.0 h1:q5EC36kV79HWeTBWsod3mG11EgStG3qArTKcvlksN1U=
|
||||
github.com/jmhodges/levigo v1.0.0/go.mod h1:Q6Qx+uH3RAqyK4rFQroq9RL7mdkABMcfhEI+nNuzMJQ=
|
||||
github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
|
||||
github.com/kljensen/snowball v0.6.0/go.mod h1:27N7E8fVU5H68RlUmnWwZCfxgt4POBJfENGMvNRhldw=
|
||||
github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
|
||||
github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
|
||||
github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
|
||||
github.com/mschoch/smat v0.0.0-20160514031455-90eadee771ae/go.mod h1:qAyveg+e4CE+eKJXWVjKXM4ck2QobLqTDytGJbLLhJg=
|
||||
github.com/mschoch/smat v0.2.0 h1:8imxQsjDm8yFEAVBe7azKmKSgzSkZXDuKkSq9374khM=
|
||||
github.com/mschoch/smat v0.2.0/go.mod h1:kc9mz7DoBKqDyiRL7VZN8KvXQMWeTaVnttLRXOlotKw=
|
||||
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
|
||||
github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
|
||||
github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
|
||||
github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic=
|
||||
github.com/philhofer/fwd v1.0.0/go.mod h1:gk3iGcWd9+svBvR0sR+KPcfE+RNWozjowpeBVG3ZVNU=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/pquerna/otp v1.5.0 h1:NMMR+WrmaqXU4EzdGJEE1aUUI0AMRzsp96fFFWNPwxs=
|
||||
github.com/pquerna/otp v1.5.0/go.mod h1:dkJfzwRKNiegxyNb54X/3fLwhCynbMspSyWKnvi1AEg=
|
||||
github.com/rcrowley/go-metrics v0.0.0-20190826022208-cac0b30c2563/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4=
|
||||
github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
|
||||
github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g=
|
||||
github.com/russross/blackfriday v1.6.0 h1:KqfZb0pUVN2lYqZUYRddxF4OR8ZMURnJIG5Y3VRLtww=
|
||||
github.com/russross/blackfriday v1.6.0/go.mod h1:ti0ldHuxg49ri4ksnFxlkCfN+hvslNlmVHqNRXXJNAY=
|
||||
github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=
|
||||
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||
github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ=
|
||||
github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE=
|
||||
github.com/spf13/cobra v0.0.5/go.mod h1:3K3wKZymM7VvHMDS9+Akkh4K60UwM26emMESw8tLCHU=
|
||||
github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo=
|
||||
github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
|
||||
github.com/spf13/viper v1.3.2/go.mod h1:ZiWeW+zYFKm7srdB9IoDzzZXaJaI5eL9QjNiN/DMA2s=
|
||||
github.com/steveyen/gtreap v0.1.0 h1:CjhzTa274PyJLJuMZwIzCO1PfC00oRa8d1Kc78bFXJM=
|
||||
github.com/steveyen/gtreap v0.1.0/go.mod h1:kl/5J7XbrOmlIbYIXdRHDDE5QxHqpk0cmkT7Z4dM9/Y=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
||||
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
||||
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
|
||||
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
||||
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
||||
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
|
||||
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
github.com/syndtr/goleveldb v1.0.0 h1:fBdIW9lB4Iz0n9khmH8w27SJ3QEJ7+IgjPEwGSZiFdE=
|
||||
github.com/syndtr/goleveldb v1.0.0/go.mod h1:ZVVdQEZoIme9iO1Ch2Jdy24qqXrMMOU6lpPAyBWyWuQ=
|
||||
github.com/tebeka/snowball v0.4.2/go.mod h1:4IfL14h1lvwZcp1sfXuuc7/7yCsvVffTWxWxCLfFpYg=
|
||||
github.com/tecbot/gorocksdb v0.0.0-20191217155057-f0fad39f321c h1:g+WoO5jjkqGAzHWCjJB1zZfXPIAaDpzXIEJ0eS6B5Ok=
|
||||
github.com/tecbot/gorocksdb v0.0.0-20191217155057-f0fad39f321c/go.mod h1:ahpPrc7HpcfEWDQRZEmnXMzHY03mLDYMCxeDzy46i+8=
|
||||
github.com/tinylib/msgp v1.1.0/go.mod h1:+d+yLhGm8mzTaHzB+wgMYrodPfmZrzkirds8fDWklFE=
|
||||
github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0=
|
||||
github.com/urfave/cli v1.22.17 h1:SYzXoiPfQjHBbkYxbew5prZHS1TOLT3ierW8SYLqtVQ=
|
||||
github.com/urfave/cli v1.22.17/go.mod h1:b0ht0aqgH/6pBYzzxURyrM4xXNgsoT/n2ZzwQiEhNVo=
|
||||
github.com/willf/bitset v1.1.10/go.mod h1:RjeCKbqT1RxIR/KWY6phxZiaY1IyutSBfGjNPySAYV4=
|
||||
github.com/willf/bitset v1.1.11 h1:N7Z7E9UvjW+sGsEl7k/SJrvY2reP1A07MrGuCjIOjRE=
|
||||
github.com/willf/bitset v1.1.11/go.mod h1:83CECat5yLh5zVOf4P1ErAgKA5UDvKtgyUABdr3+MjI=
|
||||
github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q=
|
||||
go.etcd.io/bbolt v1.3.5/go.mod h1:G5EMThwa9y8QZGBClrRx5EY+Yw9kAhnjy3bSjsnlVTQ=
|
||||
go.etcd.io/bbolt v1.4.3 h1:dEadXpI6G79deX5prL3QRNP6JB8UxVkqo4UPnHaNXJo=
|
||||
go.etcd.io/bbolt v1.4.3/go.mod h1:tKQlpPaYCVFctUIgFKFnAlvbmB3tpy1vkTnDWohtc0E=
|
||||
golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
||||
golang.org/x/crypto v0.43.0 h1:dduJYIi3A3KOfdGOHX8AVZ/jGiyPa3IbBozJ5kNuE04=
|
||||
golang.org/x/crypto v0.43.0/go.mod h1:BFbav4mRNlXJL4wNeejLpWxB7wMbc79PdRGhWKncxR0=
|
||||
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.10.0 h1:3NQrjDixjgGwUOCaF8w2+VYHv0Ve/vGYSbdkTa98gmQ=
|
||||
golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20181205085412-a5c9d58dba9a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20181221143128-b4a75ba826a6/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.37.0 h1:fdNQudmxPjkdUTPnLn5mdQv7Zwvbvpaxqs831goi9kQ=
|
||||
golang.org/x/sys v0.37.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE=
|
||||
google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
|
||||
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
|
||||
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
515
handler.go
Normal file
515
handler.go
Normal file
|
|
@ -0,0 +1,515 @@
|
|||
// Copyright (C) 2017 Marius Schellenberger
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"html/template"
|
||||
"net"
|
||||
"net/http"
|
||||
"regexp"
|
||||
"time"
|
||||
)
|
||||
|
||||
func webserver(l net.Listener) {
|
||||
cookieStore()
|
||||
loadTemplates()
|
||||
srv := &http.Server{
|
||||
Handler: buildRoutes(),
|
||||
}
|
||||
log.Fatal("webserver: ", srv.Serve(l))
|
||||
}
|
||||
|
||||
type notFoundHandler struct{}
|
||||
|
||||
func (notFoundHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
f := func(ctx *Context) {
|
||||
ctx.Template("notFoundHandler")
|
||||
ctx.Data = webData{
|
||||
Title: "404",
|
||||
}
|
||||
ctx.Exec()
|
||||
}
|
||||
w.WriteHeader(http.StatusNotFound)
|
||||
f(newContext(w, r))
|
||||
}
|
||||
|
||||
func contextWrapper(h ctxHandler) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
h(newContext(w, r))
|
||||
}
|
||||
}
|
||||
|
||||
func adminAuthHandler(h ctxHandler) ctxHandler {
|
||||
return func(ctx *Context) {
|
||||
if ctx.GetAdmin() {
|
||||
h(ctx)
|
||||
return
|
||||
}
|
||||
ctx.Redirect("/404", 302)
|
||||
}
|
||||
}
|
||||
|
||||
func userAuthHandler(h ctxHandler) ctxHandler {
|
||||
return func(ctx *Context) {
|
||||
if ctx.GetAdmin() {
|
||||
h(ctx)
|
||||
return
|
||||
}
|
||||
if ctx.GetUser() == ctx.Var("user") {
|
||||
h(ctx)
|
||||
return
|
||||
}
|
||||
ctx.Redirect("/404", 302)
|
||||
}
|
||||
}
|
||||
|
||||
func staticHandler(ctx *Context) {
|
||||
var err error
|
||||
ctx.SetHeader("Content-Type", "text/css; charset=utf-8")
|
||||
ctx.SetHeader("Expires", time.Now().Add(time.Hour*12).Format(http.TimeFormat))
|
||||
switch ctx.Path() {
|
||||
case "/bootstrap.css":
|
||||
err = ctx.Write(res["bootstrap.css"])
|
||||
case "/custom.css":
|
||||
err = ctx.Write(res["custom.css"])
|
||||
}
|
||||
if err != nil {
|
||||
log.Println("static:", err)
|
||||
}
|
||||
}
|
||||
|
||||
func indexHandler(ctx *Context) {
|
||||
ctx.InitToken()
|
||||
if !ctx.LoggedOn() {
|
||||
ctx.UnsetUdata()
|
||||
}
|
||||
ctx.Redirect("/wiki/Index", 302)
|
||||
}
|
||||
|
||||
func loginHandler(ctx *Context) {
|
||||
ctx.InitToken()
|
||||
if ctx.LoggedOn() {
|
||||
ctx.Redirect("/wiki/Index", 302)
|
||||
return
|
||||
}
|
||||
ctx.Template("loginHandler")
|
||||
ctx.Data = webData{
|
||||
Title: "Login",
|
||||
BodyTitle: "Login",
|
||||
}
|
||||
switch ctx.Method() {
|
||||
case "GET":
|
||||
ctx.NewToken()
|
||||
ctx.Exec()
|
||||
case "POST":
|
||||
user := ctx.Form("user")
|
||||
password := ctx.Form("password")
|
||||
submit := ctx.Form("submit")
|
||||
if !ctx.CheckToken() {
|
||||
ctx.NewToken()
|
||||
ctx.Data.Msg = "wrong csrf token"
|
||||
ctx.Exec()
|
||||
return
|
||||
}
|
||||
if user == "" || password == "" || submit != "Login" {
|
||||
ctx.NewToken()
|
||||
ctx.Data.Msg = "wrong inputs"
|
||||
ctx.Exec()
|
||||
return
|
||||
}
|
||||
err := db.Login(user, password)
|
||||
if err != nil {
|
||||
ctx.NewToken()
|
||||
ctx.Data.Msg = err.Error()
|
||||
ctx.Exec()
|
||||
return
|
||||
}
|
||||
ctx.SetUdata(user, db.UserIsAdmin(user))
|
||||
ctx.Redirect("/wiki/Index", 302)
|
||||
}
|
||||
}
|
||||
|
||||
func searchHandler(ctx *Context) {
|
||||
ctx.InitToken()
|
||||
ctx.Template("searchHandler")
|
||||
ctx.Data = webData{
|
||||
Title: "Search",
|
||||
BodyTitle: "Search",
|
||||
}
|
||||
switch ctx.Method() {
|
||||
case "GET":
|
||||
ctx.NewToken()
|
||||
ctx.Exec()
|
||||
case "POST":
|
||||
search := ctx.Form("search")
|
||||
submit := ctx.Form("submit")
|
||||
if !ctx.CheckToken() {
|
||||
ctx.NewToken()
|
||||
ctx.Data.Msg = "wrong csrf token"
|
||||
ctx.Exec()
|
||||
return
|
||||
}
|
||||
if search == "" || submit != "Search" {
|
||||
ctx.NewToken()
|
||||
ctx.Data.Msg = "wrong inputs"
|
||||
ctx.Exec()
|
||||
return
|
||||
}
|
||||
|
||||
res, err := engine.Search(!ctx.LoggedOn(), search)
|
||||
if err != nil {
|
||||
ctx.NewToken()
|
||||
ctx.Data.Msg = err.Error()
|
||||
ctx.Exec()
|
||||
return
|
||||
}
|
||||
ctx.Data.Search = search
|
||||
ctx.Data.Data = template.HTML(res)
|
||||
ctx.NewToken()
|
||||
ctx.Exec()
|
||||
}
|
||||
}
|
||||
|
||||
func allHandler(ctx *Context) {
|
||||
ctx.Template("allHandler")
|
||||
ctx.Data = webData{
|
||||
Title: "All Articles",
|
||||
BodyTitle: "All Articles",
|
||||
Admin: ctx.GetAdmin(),
|
||||
}
|
||||
all, err := engine.GetAll(!ctx.LoggedOn())
|
||||
if err != nil {
|
||||
ctx.Data.Msg = err.Error()
|
||||
}
|
||||
ctx.Data.Data = all
|
||||
ctx.Exec()
|
||||
}
|
||||
|
||||
func wikiNewHandler(ctx *Context) {
|
||||
ctx.Template("wikiNewHandler")
|
||||
ctx.Data = webData{
|
||||
Title: "New Article",
|
||||
BodyTitle: "New Article",
|
||||
Admin: ctx.GetAdmin(),
|
||||
}
|
||||
switch ctx.Method() {
|
||||
case "GET":
|
||||
ctx.NewToken()
|
||||
ctx.Exec()
|
||||
case "POST":
|
||||
title := ctx.Form("title")
|
||||
text := ctx.Form("text")
|
||||
public := ctx.Form("public")
|
||||
pub := false
|
||||
if public == "0" {
|
||||
pub = true
|
||||
}
|
||||
submit := ctx.Form("submit")
|
||||
art := Article{
|
||||
Title: title,
|
||||
MD: text,
|
||||
}
|
||||
ctx.Data.Data = art
|
||||
if !ctx.CheckToken() {
|
||||
ctx.NewToken()
|
||||
ctx.Data.Msg = "wrong csrf token"
|
||||
ctx.Exec()
|
||||
return
|
||||
}
|
||||
ctx.NewToken()
|
||||
m, err := regexp.MatchString("[^a-zA-Z0-9 _-]", title)
|
||||
if err != nil {
|
||||
ctx.Data.Msg = err.Error()
|
||||
ctx.Exec()
|
||||
return
|
||||
}
|
||||
if m {
|
||||
ctx.Data.Msg = "title contains forbidden characters, allowed are [a-zA-Z0-9 _-]"
|
||||
ctx.Exec()
|
||||
return
|
||||
}
|
||||
err = engine.Exists(!ctx.LoggedOn(), title)
|
||||
if err == nil {
|
||||
ctx.Data.Msg = "article alerady exsist"
|
||||
ctx.Exec()
|
||||
return
|
||||
}
|
||||
if submit != "Submit" {
|
||||
ctx.Data.Msg = "wrong inputs"
|
||||
ctx.Exec()
|
||||
return
|
||||
}
|
||||
newTitle, err := engine.Index(title, text, ctx.GetUser(), pub)
|
||||
if err != nil {
|
||||
ctx.Data.Msg = err.Error()
|
||||
ctx.Exec()
|
||||
return
|
||||
}
|
||||
ctx.Redirect("/wiki/"+newTitle, 302)
|
||||
}
|
||||
}
|
||||
|
||||
func wikiHandler(ctx *Context) {
|
||||
if ctx.T == nil {
|
||||
ctx.Template("wikiHandler")
|
||||
}
|
||||
ctx.Data = webData{
|
||||
Admin: ctx.GetAdmin(),
|
||||
}
|
||||
ctx.NewToken()
|
||||
title := ctx.Var("title")
|
||||
art, err := engine.Get(!ctx.LoggedOn(), title)
|
||||
if err != nil {
|
||||
ctx.Redirect("/404", 302)
|
||||
return
|
||||
}
|
||||
art.LinkTitle = title
|
||||
ctx.Data.Data = art
|
||||
ctx.Data.Title = art.Title
|
||||
ctx.Data.BodyTitle = art.Title
|
||||
ctx.Exec()
|
||||
}
|
||||
|
||||
func wikiMDHandler(f ctxHandler) ctxHandler {
|
||||
return func(ctx *Context) {
|
||||
ctx.Template("wikiMDHandler")
|
||||
f(ctx)
|
||||
}
|
||||
}
|
||||
|
||||
func wikiEditHandler(ctx *Context) {
|
||||
ctx.Template("wikiEditHandler")
|
||||
ctx.Data = webData{}
|
||||
title := ctx.Var("title")
|
||||
switch ctx.Method() {
|
||||
case "GET":
|
||||
ctx.NewToken()
|
||||
art, err := engine.Get(!ctx.LoggedOn(), title)
|
||||
if err != nil {
|
||||
ctx.Redirect("/404", 302)
|
||||
return
|
||||
}
|
||||
art.LinkTitle = title
|
||||
ctx.Data.Data = art
|
||||
ctx.Data.Title = art.Title
|
||||
ctx.Data.BodyTitle = art.Title
|
||||
ctx.Exec()
|
||||
case "POST":
|
||||
ftitle := ctx.Form("title")
|
||||
text := ctx.Form("text")
|
||||
public := ctx.Form("public")
|
||||
pub := false
|
||||
if public == "0" {
|
||||
pub = true
|
||||
}
|
||||
submit := ctx.Form("submit")
|
||||
ctx.Data.Data = Article{
|
||||
Title: ftitle,
|
||||
LinkTitle: title,
|
||||
MD: text,
|
||||
Public: pub,
|
||||
}
|
||||
ctx.Data.Title = ftitle
|
||||
ctx.Data.BodyTitle = ftitle
|
||||
if !ctx.CheckToken() {
|
||||
ctx.NewToken()
|
||||
ctx.Data.Msg = "wrong csrf token"
|
||||
ctx.Exec()
|
||||
return
|
||||
}
|
||||
ctx.NewToken()
|
||||
if submit != "Submit" {
|
||||
ctx.Data.Msg = "wrong inputs"
|
||||
ctx.Exec()
|
||||
return
|
||||
}
|
||||
err := engine.Update(title, text, ctx.GetUser(), pub)
|
||||
if err != nil {
|
||||
ctx.Data.Msg = err.Error()
|
||||
ctx.Exec()
|
||||
return
|
||||
}
|
||||
ctx.Redirect("/wiki/"+title, 302)
|
||||
}
|
||||
}
|
||||
|
||||
func wikiDelHandler(ctx *Context) {
|
||||
title := ctx.Var("title")
|
||||
submit := ctx.Form("submit")
|
||||
if !ctx.CheckToken() {
|
||||
ctx.Redirect("/wiki/"+title, 302)
|
||||
return
|
||||
}
|
||||
if submit != "Delete" {
|
||||
ctx.Redirect("/wiki/"+title, 302)
|
||||
return
|
||||
}
|
||||
if err := engine.Delete(title); err != nil {
|
||||
log.Println("delete:", err)
|
||||
ctx.Redirect("/wiki/"+title, 302)
|
||||
}
|
||||
ctx.Redirect("/wiki/Index", 302)
|
||||
}
|
||||
|
||||
func usersHandler(ctx *Context) {
|
||||
if !ctx.LoggedOn() {
|
||||
ctx.Redirect("/404", 302)
|
||||
return
|
||||
}
|
||||
ctx.Template("usersHandler")
|
||||
ctx.Data = webData{
|
||||
Title: "Users",
|
||||
BodyTitle: "Users",
|
||||
User: ctx.GetUser(),
|
||||
}
|
||||
ctx.NewToken()
|
||||
us, err := db.GetUsers()
|
||||
if err != nil {
|
||||
ctx.Data.Msg = err.Error()
|
||||
}
|
||||
ctx.Data.Data = us
|
||||
ctx.Exec()
|
||||
}
|
||||
|
||||
func userNewHandler(ctx *Context) {
|
||||
ctx.Template("userNewHandler")
|
||||
ctx.Data = webData{
|
||||
Title: "New User",
|
||||
BodyTitle: "New User",
|
||||
}
|
||||
switch ctx.Method() {
|
||||
case "GET":
|
||||
ctx.NewToken()
|
||||
ctx.Exec()
|
||||
case "POST":
|
||||
user := ctx.Form("user")
|
||||
password := ctx.Form("password")
|
||||
repeat := ctx.Form("repeat")
|
||||
admin := ctx.Form("admin")
|
||||
adm := false
|
||||
if admin == "0" {
|
||||
adm = true
|
||||
}
|
||||
submit := ctx.Form("submit")
|
||||
if !ctx.CheckToken() {
|
||||
ctx.NewToken()
|
||||
ctx.Data.Msg = "wrong csrf token"
|
||||
ctx.Exec()
|
||||
return
|
||||
}
|
||||
if user == "" || password == "" || password != repeat || submit != "Submit" {
|
||||
ctx.NewToken()
|
||||
ctx.Data.Msg = "wrong inputs"
|
||||
ctx.Exec()
|
||||
return
|
||||
}
|
||||
log.Println(user, password, adm)
|
||||
if err := db.CreateUser(user, password, adm); err != nil {
|
||||
ctx.NewToken()
|
||||
ctx.Data.Msg = err.Error()
|
||||
ctx.Exec()
|
||||
return
|
||||
}
|
||||
ctx.Redirect("/users", 302)
|
||||
}
|
||||
}
|
||||
|
||||
func userEditHandler(ctx *Context) {
|
||||
ctx.Template("userEditHandler")
|
||||
ctx.Data = webData{
|
||||
Title: "Edit User",
|
||||
BodyTitle: "Edit User",
|
||||
}
|
||||
switch ctx.Method() {
|
||||
case "GET":
|
||||
u, err := db.GetUserWithoutPassword(ctx.Var("user"))
|
||||
if err != nil {
|
||||
ctx.Data.Msg = err.Error()
|
||||
ctx.Exec()
|
||||
return
|
||||
}
|
||||
ctx.Data.Data = u
|
||||
ctx.NewToken()
|
||||
ctx.Exec()
|
||||
case "POST":
|
||||
user := ctx.Var("user")
|
||||
password := ctx.Form("password")
|
||||
repeat := ctx.Form("repeat")
|
||||
admin := ctx.Form("admin")
|
||||
adm := false
|
||||
if admin == "0" {
|
||||
adm = true
|
||||
}
|
||||
submit := ctx.Form("submit")
|
||||
if !ctx.CheckToken() {
|
||||
ctx.NewToken()
|
||||
ctx.Data.Msg = "wrong csrf token"
|
||||
ctx.Exec()
|
||||
return
|
||||
}
|
||||
if user == "" || password != repeat || submit != "Submit" {
|
||||
ctx.NewToken()
|
||||
ctx.Data.Msg = "wrong inputs"
|
||||
ctx.Exec()
|
||||
return
|
||||
}
|
||||
if ctx.GetAdmin() {
|
||||
if err := db.AdminUpdateUser(user, password, adm); err != nil {
|
||||
ctx.NewToken()
|
||||
ctx.Data.Msg = err.Error()
|
||||
ctx.Exec()
|
||||
return
|
||||
}
|
||||
} else {
|
||||
if user == ctx.GetUser() {
|
||||
if err := db.UpdateUserPassword(user, password); err != nil {
|
||||
ctx.NewToken()
|
||||
ctx.Data.Msg = err.Error()
|
||||
ctx.Exec()
|
||||
return
|
||||
}
|
||||
ctx.Redirect("/wiki/Index", 302)
|
||||
return
|
||||
}
|
||||
}
|
||||
ctx.Redirect("/users", 302)
|
||||
}
|
||||
}
|
||||
|
||||
func userUnlockHandler(ctx *Context) {
|
||||
user := ctx.Var("user")
|
||||
if err := db.UnlockUser(user); err != nil {
|
||||
log.Println("unlock:", err)
|
||||
}
|
||||
ctx.Redirect("/users", 302)
|
||||
}
|
||||
|
||||
func userDelHandler(ctx *Context) {
|
||||
ctx.Template("userDelHandler")
|
||||
ctx.Data = webData{
|
||||
Title: "Delete User",
|
||||
BodyTitle: "Delete User",
|
||||
}
|
||||
switch ctx.Method() {
|
||||
case "GET":
|
||||
user, err := db.GetUserWithoutPassword(ctx.Var("user"))
|
||||
if err != nil {
|
||||
ctx.Data.Msg = err.Error()
|
||||
ctx.Exec()
|
||||
return
|
||||
}
|
||||
ctx.Data.Data = user
|
||||
ctx.NewToken()
|
||||
ctx.Exec()
|
||||
case "POST":
|
||||
if err := db.DeleteUser(ctx.Var("user")); err != nil {
|
||||
log.Println("delete:", err)
|
||||
}
|
||||
ctx.Redirect("/users", 302)
|
||||
}
|
||||
}
|
||||
|
||||
func logoutHandler(ctx *Context) {
|
||||
ctx.UnsetUdata()
|
||||
ctx.Redirect("/wiki/Index", 302)
|
||||
}
|
||||
23
logger.go
Normal file
23
logger.go
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
// Copyright (C) 2017 Marius Schellenberger
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
logger "log"
|
||||
"os"
|
||||
)
|
||||
|
||||
var log *logger.Logger
|
||||
|
||||
func newLogger(file string) {
|
||||
if file == "" {
|
||||
log = logger.New(os.Stdout, "", logger.LstdFlags)
|
||||
return
|
||||
}
|
||||
f, err := os.OpenFile(file, os.O_RDWR|os.O_CREATE|os.O_APPEND, 0644)
|
||||
if err != nil {
|
||||
logger.Fatal(err)
|
||||
}
|
||||
logger.SetOutput(f)
|
||||
log = logger.New(f, "", logger.LstdFlags)
|
||||
}
|
||||
157
main.go
Normal file
157
main.go
Normal file
|
|
@ -0,0 +1,157 @@
|
|||
// Copyright (C) 2017 Marius Schellenberger
|
||||
|
||||
//go:generate ./templates.sh
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"flag"
|
||||
logger "log"
|
||||
"net"
|
||||
"os"
|
||||
"os/signal"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"git.giftfish.de/ston1th/godrop"
|
||||
"github.com/blevesearch/bleve"
|
||||
)
|
||||
|
||||
const (
|
||||
bleveStore = "."
|
||||
|
||||
defDataDir = "/var/lib/gowiki"
|
||||
defListen = "127.0.0.1:8090"
|
||||
defLog = "gowiki.log"
|
||||
|
||||
defUser = "admin"
|
||||
defPassword = "gowiki"
|
||||
|
||||
defRunUser = "wiki"
|
||||
defRunGroup = "wiki"
|
||||
|
||||
welcome = `# Welcome to GoWiki
|
||||
This is the [Index](/wiki/Index) page.
|
||||
You can customize it how you like.`
|
||||
indexName = "Index"
|
||||
)
|
||||
|
||||
var (
|
||||
datadir string
|
||||
listen string
|
||||
secureCookie bool
|
||||
user string
|
||||
group string
|
||||
logfile string
|
||||
|
||||
engine *Engine
|
||||
db *BoltStore
|
||||
|
||||
dump bool
|
||||
restore bool
|
||||
verbose bool
|
||||
)
|
||||
|
||||
func main() {
|
||||
flag.StringVar(&datadir, "d", defDataDir, "data directory")
|
||||
flag.StringVar(&user, "u", defRunUser, "drop privileges to user")
|
||||
flag.StringVar(&group, "g", defRunGroup, "drop privileges to group")
|
||||
flag.StringVar(&listen, "l", defListen, "listening <address>:<port>")
|
||||
flag.StringVar(&logfile, "log", defLog, "log file")
|
||||
flag.BoolVar(&secureCookie, "s", false, "enable secure cookie")
|
||||
flag.BoolVar(&dump, "dump", false, "create a json dump of all articles in the data directory (dump is written to stdout)")
|
||||
flag.BoolVar(&restore, "restore", false, "restore a json dump to the data directory (dump is read from stdin)")
|
||||
flag.BoolVar(&verbose, "v", false, "enable verbose dump/restore mode")
|
||||
flag.Parse()
|
||||
|
||||
err := os.Chdir(datadir)
|
||||
if err != nil {
|
||||
logger.Fatal("main: ", err)
|
||||
}
|
||||
|
||||
if dump {
|
||||
err = dumpWiki()
|
||||
if err != nil {
|
||||
logger.Fatal("dump: ", err)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if restore {
|
||||
err = restoreWiki()
|
||||
if err != nil {
|
||||
logger.Fatal("restore: ", err)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
cfg := godrop.Config{
|
||||
User: user,
|
||||
Group: group,
|
||||
}
|
||||
err = godrop.Drop(cfg, func() (net.Listener, error) {
|
||||
return net.Listen("tcp", listen)
|
||||
})
|
||||
if err != nil {
|
||||
logger.Fatal("main: ", err)
|
||||
}
|
||||
newLogger(logfile)
|
||||
l, err := godrop.GetListener()
|
||||
if err != nil {
|
||||
log.Fatal("main: ", err)
|
||||
}
|
||||
|
||||
db, err = NewBoltStore("users")
|
||||
if err != nil {
|
||||
log.Fatal("main: ", err)
|
||||
}
|
||||
if !db.UserExists(defUser) {
|
||||
err = db.CreateUser(defUser, defPassword, true)
|
||||
if err != nil {
|
||||
log.Fatal("main: ", err)
|
||||
}
|
||||
}
|
||||
|
||||
if _, err := os.Stat(bleveStore); os.IsNotExist(err) {
|
||||
mapping := bleve.NewIndexMapping()
|
||||
index, err := bleve.New(bleveStore, mapping)
|
||||
if err != nil {
|
||||
log.Fatal("main: ", err)
|
||||
}
|
||||
engine = NewEngine(index)
|
||||
_, err = engine.Index(indexName, welcome, defUser, true)
|
||||
if err != nil {
|
||||
log.Fatal("main: ", err)
|
||||
}
|
||||
} else {
|
||||
index, err := bleve.Open(bleveStore)
|
||||
if err != nil {
|
||||
log.Fatal("main: ", err)
|
||||
}
|
||||
engine = NewEngine(index)
|
||||
}
|
||||
|
||||
go webserver(l)
|
||||
log.Println("gowiki started")
|
||||
sigs := make(chan os.Signal)
|
||||
signal.Notify(sigs, syscall.SIGINT, syscall.SIGTERM)
|
||||
sig := <-sigs
|
||||
log.Println("caught " + sig.String())
|
||||
go func() {
|
||||
time.Sleep(time.Second * 20)
|
||||
log.Fatal(errors.New("stop timed out: killing"))
|
||||
}()
|
||||
err = l.Close()
|
||||
if err != nil {
|
||||
log.Println(err)
|
||||
}
|
||||
err = db.Close()
|
||||
if err != nil {
|
||||
log.Println(err)
|
||||
}
|
||||
err = engine.Close()
|
||||
if err != nil {
|
||||
log.Println(err)
|
||||
}
|
||||
}
|
||||
54
pkg/cache/cache.go
vendored
54
pkg/cache/cache.go
vendored
|
|
@ -1,54 +0,0 @@
|
|||
// Copyright (C) 2021 Marius Schellenberger
|
||||
|
||||
package cache
|
||||
|
||||
import (
|
||||
"git.giftfish.de/ston1th/gowiki/pkg/core"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
type CachedPage struct {
|
||||
Page *core.Page
|
||||
CacheTime time.Time
|
||||
}
|
||||
|
||||
type Cache struct {
|
||||
mu sync.RWMutex
|
||||
Cache map[string]*CachedPage
|
||||
}
|
||||
|
||||
func NewCache() *Cache {
|
||||
return &Cache{Cache: make(map[string]*CachedPage)}
|
||||
}
|
||||
|
||||
func (c *Cache) Add(title string, p *core.Page) {
|
||||
now := time.Now()
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
cp, ok := c.Cache[title]
|
||||
if ok && now.After(cp.CacheTime) {
|
||||
cp.Page = p
|
||||
cp.CacheTime = now
|
||||
c.Cache[title] = cp
|
||||
return
|
||||
}
|
||||
c.Cache[title] = &CachedPage{p, now}
|
||||
}
|
||||
|
||||
func (c *Cache) Get(title string) (p *core.Page) {
|
||||
c.mu.RLock()
|
||||
defer c.mu.RUnlock()
|
||||
if cp, ok := c.Cache[title]; ok {
|
||||
p = new(core.Page)
|
||||
*p = *cp.Page
|
||||
return
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (c *Cache) Delete(title string) {
|
||||
c.mu.Lock()
|
||||
delete(c.Cache, title)
|
||||
c.mu.Unlock()
|
||||
}
|
||||
50
pkg/cache/cache_test.go
vendored
50
pkg/cache/cache_test.go
vendored
|
|
@ -1,50 +0,0 @@
|
|||
// Copyright (C) 2021 Marius Schellenberger
|
||||
|
||||
package cache
|
||||
|
||||
import (
|
||||
"git.giftfish.de/ston1th/gowiki/pkg/core"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestCache(t *testing.T) {
|
||||
title := "test/page"
|
||||
pages := []*core.Page{
|
||||
&core.Page{
|
||||
Title: title,
|
||||
Markdown: "page v1",
|
||||
},
|
||||
&core.Page{
|
||||
Title: title,
|
||||
Markdown: "page v2",
|
||||
},
|
||||
}
|
||||
c := NewCache()
|
||||
t.Run("Add", func(t *testing.T) {
|
||||
c.Add(title, pages[0])
|
||||
})
|
||||
t.Run("Get", func(t *testing.T) {
|
||||
p := c.Get(title)
|
||||
if p.Markdown != pages[0].Markdown {
|
||||
t.Fail()
|
||||
}
|
||||
})
|
||||
t.Run("Add", func(t *testing.T) {
|
||||
c.Add(title, pages[1])
|
||||
})
|
||||
t.Run("Get", func(t *testing.T) {
|
||||
p := c.Get(title)
|
||||
if p.Markdown == pages[0].Markdown {
|
||||
t.Fail()
|
||||
}
|
||||
})
|
||||
t.Run("Delete", func(t *testing.T) {
|
||||
c.Delete(title)
|
||||
})
|
||||
t.Run("Get", func(t *testing.T) {
|
||||
p := c.Get(title)
|
||||
if p != nil {
|
||||
t.Fail()
|
||||
}
|
||||
})
|
||||
}
|
||||
281
pkg/cmd/cmd.go
281
pkg/cmd/cmd.go
|
|
@ -1,281 +0,0 @@
|
|||
// Copyright (C) 2021 Marius Schellenberger
|
||||
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
stdlog "log"
|
||||
"net"
|
||||
"os"
|
||||
"os/signal"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"git.giftfish.de/ston1th/godrop/v2"
|
||||
"git.giftfish.de/ston1th/gowiki/pkg/core"
|
||||
"git.giftfish.de/ston1th/gowiki/pkg/db"
|
||||
"git.giftfish.de/ston1th/gowiki/pkg/log"
|
||||
"git.giftfish.de/ston1th/gowiki/pkg/server"
|
||||
"github.com/urfave/cli"
|
||||
)
|
||||
|
||||
const (
|
||||
defDataDir = "/var/lib/gowiki"
|
||||
defListen = "127.0.0.1:8080"
|
||||
defLogFile = "gowiki.log"
|
||||
|
||||
defRunUser = "_wiki"
|
||||
defRunGroup = "_wiki"
|
||||
)
|
||||
|
||||
var (
|
||||
config core.Config
|
||||
dumpFile string
|
||||
configFile string
|
||||
)
|
||||
|
||||
func initServer(cfg *core.Config) (err error) {
|
||||
if err = godrop.PledgePromises("stdio rpath wpath cpath inet fattr flock unveil"); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
l, err := net.Listen("tcp", cfg.ListenAddr)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
log.InitLogger(cfg)
|
||||
|
||||
srv := server.NewHTTPServer(cfg, l)
|
||||
err = srv.Start()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
log.Println("gowiki " + cfg.Version + " started")
|
||||
sigs := make(chan os.Signal, 1)
|
||||
signal.Notify(sigs, syscall.SIGINT, syscall.SIGTERM)
|
||||
sig := <-sigs
|
||||
log.Println("signal: " + sig.String())
|
||||
go func() {
|
||||
time.Sleep(time.Second * 20)
|
||||
log.Fatal("stop timed out: killing")
|
||||
}()
|
||||
err = srv.Stop()
|
||||
if err != nil {
|
||||
log.Println(err)
|
||||
}
|
||||
log.Println("gowiki stopped")
|
||||
return
|
||||
}
|
||||
|
||||
func loadConfig() error {
|
||||
if configFile == "" {
|
||||
return nil
|
||||
}
|
||||
file, err := os.Open(configFile)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = json.NewDecoder(file).Decode(&config)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return file.Close()
|
||||
}
|
||||
|
||||
func Run(version string) {
|
||||
app := cli.NewApp()
|
||||
app.Name = "gowiki"
|
||||
app.Usage = "a simple wiki engine"
|
||||
app.Version = version
|
||||
app.Commands = []cli.Command{
|
||||
{
|
||||
Name: "server",
|
||||
Usage: "start gowiki server",
|
||||
Flags: defaultFlags(serverFlags()),
|
||||
Action: func(c *cli.Context) error {
|
||||
if err := loadConfig(); err != nil {
|
||||
stdlog.Fatal("server: ", err)
|
||||
}
|
||||
config.Version = app.Version
|
||||
if err := initServer(&config); err != nil {
|
||||
stdlog.Fatal("server: ", err)
|
||||
}
|
||||
return nil
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "dump",
|
||||
Usage: "create a json dump of all articles in the data directory (gowiki needs to be stopped)",
|
||||
Flags: defaultFlags(dumpRestoreFlags()),
|
||||
Action: func(c *cli.Context) error {
|
||||
var (
|
||||
file = os.Stdout
|
||||
err error
|
||||
)
|
||||
if err = loadConfig(); err != nil {
|
||||
stdlog.Fatal("dump: ", err)
|
||||
}
|
||||
log.Stderr()
|
||||
if dumpFile != "-" {
|
||||
file, err = os.OpenFile(dumpFile, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0o600)
|
||||
if err != nil {
|
||||
stdlog.Fatal("dump: ", err)
|
||||
}
|
||||
}
|
||||
DB, err := db.NewPlain(&config)
|
||||
if err != nil {
|
||||
stdlog.Fatal("dump: ", err)
|
||||
}
|
||||
if err = DB.Dump(file); err != nil {
|
||||
stdlog.Fatal("dump: ", err)
|
||||
}
|
||||
file.Close()
|
||||
err = DB.Close()
|
||||
if err != nil {
|
||||
stdlog.Fatal("dump: ", err)
|
||||
}
|
||||
return nil
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "restore",
|
||||
Usage: "restore a json dump to the data directory (gowiki needs to be stopped)",
|
||||
Flags: defaultFlags(dumpRestoreFlags()),
|
||||
Action: func(c *cli.Context) error {
|
||||
var (
|
||||
file = os.Stdin
|
||||
err error
|
||||
)
|
||||
if err = loadConfig(); err != nil {
|
||||
stdlog.Fatal("restore: ", err)
|
||||
}
|
||||
if dumpFile != "-" {
|
||||
file, err = os.Open(dumpFile)
|
||||
if err != nil {
|
||||
stdlog.Fatal("restore: ", err)
|
||||
}
|
||||
}
|
||||
DB, err := db.NewPlain(&config)
|
||||
if err != nil {
|
||||
stdlog.Fatal("restore: ", err)
|
||||
}
|
||||
if err = DB.Restore(file); err != nil {
|
||||
stdlog.Fatal("restore: ", err)
|
||||
}
|
||||
file.Close()
|
||||
err = DB.Close()
|
||||
if err != nil {
|
||||
stdlog.Fatal("restore: ", err)
|
||||
}
|
||||
return nil
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "resetadmin",
|
||||
Usage: "reset the admin users password to the default (gowiki needs to be stopped)",
|
||||
Flags: defaultFlags(nil),
|
||||
Action: func(c *cli.Context) error {
|
||||
var err error
|
||||
if err = loadConfig(); err != nil {
|
||||
stdlog.Fatal("resetadmin: ", err)
|
||||
}
|
||||
DB, err := db.NewPlain(&config)
|
||||
if err != nil {
|
||||
stdlog.Fatal("resetadmin: ", err)
|
||||
}
|
||||
err = DB.ResetAdmin()
|
||||
if err != nil {
|
||||
stdlog.Fatal("resetadmin: ", err)
|
||||
}
|
||||
err = DB.Close()
|
||||
if err != nil {
|
||||
stdlog.Fatal("resetadmin: ", err)
|
||||
}
|
||||
return nil
|
||||
},
|
||||
},
|
||||
}
|
||||
app.Run(os.Args)
|
||||
}
|
||||
|
||||
func serverFlags() []cli.Flag {
|
||||
return []cli.Flag{
|
||||
cli.StringFlag{
|
||||
Name: "user, u",
|
||||
Value: defRunUser,
|
||||
Usage: "drop privileges to user",
|
||||
Destination: &config.User,
|
||||
},
|
||||
cli.StringFlag{
|
||||
Name: "group, g",
|
||||
Value: defRunGroup,
|
||||
Usage: "drop privileges to group",
|
||||
Destination: &config.Group,
|
||||
},
|
||||
cli.StringFlag{
|
||||
Name: "listen, l",
|
||||
Value: defListen,
|
||||
Usage: "listening <address>:<port>",
|
||||
Destination: &config.ListenAddr,
|
||||
},
|
||||
cli.StringFlag{
|
||||
Name: "log",
|
||||
Value: defLogFile,
|
||||
Usage: "log file (use -log=- for stdout, this only works in combination with -f)",
|
||||
Destination: &config.LogFile,
|
||||
},
|
||||
cli.StringFlag{
|
||||
Name: "secret",
|
||||
Usage: "static hmac secret (at least 64 chars, used for JWT signing)",
|
||||
Destination: &config.Secret,
|
||||
},
|
||||
cli.BoolTFlag{
|
||||
Name: "admin, a",
|
||||
Usage: "use -a=false to disable the admin user",
|
||||
Destination: &config.Admin,
|
||||
},
|
||||
cli.BoolFlag{
|
||||
Name: "foreground, f",
|
||||
Usage: "do not fork into the background",
|
||||
Destination: &config.Foreground,
|
||||
},
|
||||
cli.BoolFlag{
|
||||
Name: "secure, s",
|
||||
Usage: "enable secure cookie",
|
||||
Destination: &config.SecureCookie,
|
||||
},
|
||||
cli.BoolFlag{
|
||||
Name: "debug",
|
||||
Usage: "enable debugging",
|
||||
Destination: &config.Debug,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func dumpRestoreFlags() []cli.Flag {
|
||||
return []cli.Flag{
|
||||
cli.StringFlag{
|
||||
Name: "file, f",
|
||||
Value: "-",
|
||||
Usage: "file to read/write dump (default stdin/stdout)",
|
||||
Destination: &dumpFile,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func defaultFlags(f []cli.Flag) []cli.Flag {
|
||||
return append([]cli.Flag{
|
||||
cli.StringFlag{
|
||||
Name: "config, c",
|
||||
Usage: "config file",
|
||||
Destination: &configFile,
|
||||
},
|
||||
cli.StringFlag{
|
||||
Name: "data, d",
|
||||
Value: defDataDir,
|
||||
Usage: "data directory",
|
||||
Destination: &config.DataDir,
|
||||
},
|
||||
}, f...)
|
||||
}
|
||||
|
|
@ -1,26 +0,0 @@
|
|||
// Copyright (C) 2021 Marius Schellenberger
|
||||
|
||||
package core
|
||||
|
||||
const (
|
||||
IndexPage = "Index"
|
||||
WikiSection = "wiki"
|
||||
|
||||
IndexURI = WikiSection + "/" + IndexPage
|
||||
|
||||
RootURI = "/"
|
||||
WikiURI = "/wiki"
|
||||
LoginURI = "/login"
|
||||
LogoutURI = "/logout"
|
||||
TotpURI = "/totp"
|
||||
SectionsURI = "/sections"
|
||||
SectionURI = "/section"
|
||||
SearchURI = "/search"
|
||||
StatsURI = "/stats"
|
||||
NewURI = "/new"
|
||||
BlacklistURI = "/blacklist"
|
||||
UsersURI = "/users"
|
||||
UserURI = "/user"
|
||||
ViewURI = "/view"
|
||||
StaticURI = "/static"
|
||||
)
|
||||
|
|
@ -1,13 +0,0 @@
|
|||
// Copyright (C) 2021 Marius Schellenberger
|
||||
|
||||
package core
|
||||
|
||||
// Contains checks if slice a contains string str
|
||||
func Contains(str string, a []string) (int, bool) {
|
||||
for i, s := range a {
|
||||
if s == str {
|
||||
return i, true
|
||||
}
|
||||
}
|
||||
return -1, false
|
||||
}
|
||||
|
|
@ -1,41 +0,0 @@
|
|||
// Copyright (C) 2021 Marius Schellenberger
|
||||
|
||||
package core
|
||||
|
||||
type Permission int
|
||||
|
||||
const (
|
||||
Invalid Permission = iota
|
||||
Public
|
||||
Private
|
||||
)
|
||||
|
||||
func ParsePermString(p string) Permission {
|
||||
switch p {
|
||||
case "1":
|
||||
return Public
|
||||
case "2":
|
||||
return Private
|
||||
}
|
||||
return Invalid
|
||||
}
|
||||
|
||||
func ReadPerm(username string, p Permission, s Section) bool {
|
||||
switch p {
|
||||
case Public:
|
||||
return true
|
||||
case Private:
|
||||
_, c := Contains(username, s.Members)
|
||||
return c
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func WritePerm(username string, p Permission, s Section) bool {
|
||||
switch p {
|
||||
case Public, Private:
|
||||
_, c := Contains(username, s.Members)
|
||||
return c
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
|
@ -1,77 +0,0 @@
|
|||
// Copyright (C) 2021 Marius Schellenberger
|
||||
|
||||
package core
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestParsePermString(t *testing.T) {
|
||||
data := map[string]Permission{
|
||||
"": Invalid,
|
||||
"1": Public,
|
||||
"2": Private,
|
||||
"123": Invalid,
|
||||
"test": Invalid,
|
||||
}
|
||||
for k, v := range data {
|
||||
t.Run(k, func(t *testing.T) {
|
||||
if ParsePermString(k) != v {
|
||||
t.Fail()
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestReadPerm(t *testing.T) {
|
||||
data := []struct {
|
||||
Username string
|
||||
Perm Permission
|
||||
Section Section
|
||||
OK bool
|
||||
}{
|
||||
{"", Invalid, Section{Members: []string{"admin"}}, false},
|
||||
{"", Public, Section{Members: []string{"admin"}}, true},
|
||||
{"user", Public, Section{Members: []string{"admin"}}, true},
|
||||
{"", Private, Section{Members: []string{"admin"}}, false},
|
||||
{"user", Private, Section{Members: []string{"admin"}}, false},
|
||||
{"", Private, Section{Members: []string{"user"}}, false},
|
||||
{"user", Private, Section{Members: []string{"user"}}, true},
|
||||
{"user", Private, Section{Members: []string{"user", "admin"}}, true},
|
||||
{"admin", Private, Section{Members: []string{"user"}}, false},
|
||||
}
|
||||
for i, v := range data {
|
||||
t.Run(fmt.Sprintf("Read:%d", i), func(t *testing.T) {
|
||||
if ReadPerm(v.Username, v.Perm, v.Section) != v.OK {
|
||||
t.Fail()
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestWritePerm(t *testing.T) {
|
||||
data := []struct {
|
||||
Username string
|
||||
Perm Permission
|
||||
Section Section
|
||||
OK bool
|
||||
}{
|
||||
{"", Invalid, Section{Members: []string{"user"}}, false},
|
||||
{"", Public, Section{Members: []string{"user"}}, false},
|
||||
{"user", Public, Section{Members: []string{"user"}}, true},
|
||||
{"user", Public, Section{Members: []string{"admin"}}, false},
|
||||
{"user", Private, Section{Members: []string{"user"}}, true},
|
||||
{"", Private, Section{Members: []string{"user"}}, false},
|
||||
{"", Private, Section{Members: []string{"user", "admin"}}, false},
|
||||
{"user", Private, Section{Members: []string{"user", "admin"}}, true},
|
||||
{"admin", Private, Section{Members: []string{"user", "admin"}}, true},
|
||||
}
|
||||
for i, v := range data {
|
||||
t.Run(fmt.Sprintf("Write:%d", i), func(t *testing.T) {
|
||||
if WritePerm(v.Username, v.Perm, v.Section) != v.OK {
|
||||
t.Fail()
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
@ -1,94 +0,0 @@
|
|||
// Copyright (C) 2021 Marius Schellenberger
|
||||
|
||||
package core
|
||||
|
||||
import "html/template"
|
||||
|
||||
type Page struct {
|
||||
Title string `json:"title"`
|
||||
StoreTitle string `json:"store_title"`
|
||||
Markdown string `json:"markdown"`
|
||||
HTML template.HTML `json:"html"`
|
||||
Created string `json:"created"`
|
||||
Updated string `json:"updated"`
|
||||
Perm Permission `json:"perm"`
|
||||
}
|
||||
|
||||
type Pages []Page
|
||||
|
||||
func (p Pages) Len() int { return len(p) }
|
||||
func (p Pages) Swap(i, j int) { p[i], p[j] = p[j], p[i] }
|
||||
func (p Pages) Less(i, j int) bool { return p[i].StoreTitle < p[j].StoreTitle }
|
||||
|
||||
type UpdatedPages []Page
|
||||
|
||||
func (p UpdatedPages) Len() int { return len(p) }
|
||||
func (p UpdatedPages) Swap(i, j int) { p[i], p[j] = p[j], p[i] }
|
||||
func (p UpdatedPages) Less(i, j int) bool { return p[i].Updated > p[j].Updated }
|
||||
|
||||
type Section struct {
|
||||
Name string
|
||||
User bool
|
||||
Members []string
|
||||
}
|
||||
|
||||
type Sections []Section
|
||||
|
||||
func (s Sections) Len() int { return len(s) }
|
||||
func (s Sections) Swap(i, j int) { s[i], s[j] = s[j], s[i] }
|
||||
func (s Sections) Less(i, j int) bool { return s[i].Name < s[j].Name }
|
||||
|
||||
type SectionPage struct {
|
||||
Section string
|
||||
Pages UpdatedPages
|
||||
}
|
||||
|
||||
type SectionPages []SectionPage
|
||||
|
||||
func (s SectionPages) Len() int { return len(s) }
|
||||
func (s SectionPages) Swap(i, j int) { s[i], s[j] = s[j], s[i] }
|
||||
func (s SectionPages) Less(i, j int) bool { return s[i].Section < s[j].Section }
|
||||
|
||||
type User struct {
|
||||
Username string
|
||||
Password string
|
||||
Created string
|
||||
Secret string
|
||||
Admin bool
|
||||
Locked int
|
||||
}
|
||||
|
||||
type Users []*User
|
||||
|
||||
func (u Users) Len() int { return len(u) }
|
||||
func (u Users) Swap(i, j int) { u[i], u[j] = u[j], u[i] }
|
||||
func (u Users) Less(i, j int) bool { return u[i].Username < u[j].Username }
|
||||
|
||||
type Result struct {
|
||||
Title string
|
||||
Section string
|
||||
StoreTitle string
|
||||
HTML template.HTML
|
||||
}
|
||||
|
||||
type Stats struct {
|
||||
Goroutines int
|
||||
Alloc string
|
||||
Sys string
|
||||
Docs uint64
|
||||
GoVersion string
|
||||
}
|
||||
|
||||
type Config struct {
|
||||
DataDir string `json:"data_dir,omitempty"`
|
||||
User string `json:"user,omitempty"`
|
||||
Group string `json:"group,omitempty"`
|
||||
ListenAddr string `json:"listen_addr,omitempty"`
|
||||
LogFile string `json:"log_file,omitempty"`
|
||||
Version string `json:"-"`
|
||||
Secret string `json:"secret,omitempty"`
|
||||
Admin bool `json:"admin,omitempty"`
|
||||
Foreground bool `json:"foreground,omitempty"`
|
||||
SecureCookie bool `json:"secure_cookie,omitempty"`
|
||||
Debug bool `json:"debug,omitempty"`
|
||||
}
|
||||
|
|
@ -1,33 +0,0 @@
|
|||
// Copyright (C) 2021 Marius Schellenberger
|
||||
|
||||
package db
|
||||
|
||||
import "git.giftfish.de/ston1th/jwt/v3"
|
||||
|
||||
const blacklistPrefix = "blacklist/"
|
||||
|
||||
func (db *DB) Add(sig string, exp int64) error {
|
||||
return db.store.Set(blacklistPrefix+sig, exp)
|
||||
}
|
||||
|
||||
func (db *DB) Remove(sig string) error {
|
||||
return db.store.Delete(blacklistPrefix + sig)
|
||||
}
|
||||
|
||||
func (db *DB) Check(sig string) (ok bool) {
|
||||
return db.store.Get(blacklistPrefix+sig, nil) == nil
|
||||
}
|
||||
|
||||
func (db *DB) Map() (list jwt.BlacklistMap, err error) {
|
||||
list = make(jwt.BlacklistMap)
|
||||
err = db.store.ForEachPrefix(blacklistPrefix, func(k string, v []byte) error {
|
||||
var exp int64
|
||||
err := db.store.Unmarshal(v, &exp)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
list[k] = exp
|
||||
return nil
|
||||
})
|
||||
return
|
||||
}
|
||||
122
pkg/db/db.go
122
pkg/db/db.go
|
|
@ -1,122 +0,0 @@
|
|||
// Copyright (C) 2021 Marius Schellenberger
|
||||
|
||||
package db
|
||||
|
||||
import (
|
||||
"git.giftfish.de/ston1th/godrop/v2"
|
||||
"git.giftfish.de/ston1th/gowiki/pkg/cache"
|
||||
"git.giftfish.de/ston1th/gowiki/pkg/core"
|
||||
"git.giftfish.de/ston1th/gowiki/pkg/index"
|
||||
"git.giftfish.de/ston1th/gowiki/pkg/render"
|
||||
"git.giftfish.de/ston1th/gowiki/pkg/store"
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
)
|
||||
|
||||
const (
|
||||
defUser = "admin"
|
||||
defPassword = "gowiki"
|
||||
storeFile = "store.db"
|
||||
bcryptCost = 13
|
||||
welcome = `# Welcome to GoWiki
|
||||
This is the [Index](/wiki/Index) page.
|
||||
You can customize it how you like.`
|
||||
)
|
||||
|
||||
const blevePath = "bleve"
|
||||
|
||||
type DB struct {
|
||||
admin bool
|
||||
store store.Store
|
||||
cache *cache.Cache
|
||||
Index *index.Index
|
||||
indexPath string
|
||||
}
|
||||
|
||||
func NewPlain(cfg *core.Config) (db *DB, err error) {
|
||||
db = &DB{admin: cfg.Admin, cache: cache.NewCache()}
|
||||
dbFile := filepath.Join(cfg.DataDir, storeFile)
|
||||
err = godrop.Unveil(dbFile, "rwc")
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
db.store, err = store.NewBoltStore(dbFile, nil)
|
||||
return
|
||||
}
|
||||
|
||||
func New(cfg *core.Config) (db *DB, err error) {
|
||||
db, err = NewPlain(cfg)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
db.indexPath = filepath.Join(cfg.DataDir, blevePath)
|
||||
db.Index, err = index.New(db.indexPath)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
if db.UserExists(defUser) != nil {
|
||||
err = db.CreateUser(defUser, defPassword, true)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
err = db.UnlockUser(defUser)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
if db.SectionExists(core.WikiSection) != nil {
|
||||
err = db.CreateSection(core.WikiSection, []string{defUser}, false)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
_, err = db.CreatePage(core.IndexPage, core.WikiSection, welcome, defUser, core.Public)
|
||||
if err != errPageExists && err != nil {
|
||||
return
|
||||
}
|
||||
err = db.RunMigrations()
|
||||
return
|
||||
}
|
||||
|
||||
func (db *DB) Dump(w io.Writer) error {
|
||||
return db.store.Dump(w)
|
||||
}
|
||||
|
||||
func (db *DB) Restore(r io.Reader) (err error) {
|
||||
err = db.Index.Close()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
err = os.RemoveAll(db.indexPath)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
db.Index, err = index.New(db.indexPath)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
err = db.store.Restore(r)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
err = db.store.ForEachPrefix(pagePrefix, func(k string, _ []byte) error {
|
||||
p := new(core.Page)
|
||||
db.store.Get(pagePrefix+k, p)
|
||||
db.Index.Add(p.Title, p.StoreTitle, render.Render(p))
|
||||
return nil
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
func (db *DB) Close() (err error) {
|
||||
err = db.store.Close()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
if db.Index != nil {
|
||||
return db.Index.Close()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
|
@ -1,55 +0,0 @@
|
|||
// Copyright (C) 2021 Marius Schellenberger
|
||||
|
||||
package db
|
||||
|
||||
import (
|
||||
"crypto/sha256"
|
||||
"git.giftfish.de/ston1th/gowiki/pkg/core"
|
||||
"time"
|
||||
)
|
||||
|
||||
func init() {
|
||||
reservedNames = make([]string, len(reservedURIs))
|
||||
for i, v := range reservedURIs {
|
||||
reservedNames[i] = v[1:]
|
||||
}
|
||||
}
|
||||
|
||||
var reservedURIs = []string{
|
||||
core.LoginURI,
|
||||
core.LogoutURI,
|
||||
core.TotpURI,
|
||||
core.SectionsURI,
|
||||
core.SectionURI,
|
||||
core.SearchURI,
|
||||
core.StatsURI,
|
||||
core.NewURI,
|
||||
core.BlacklistURI,
|
||||
core.UsersURI,
|
||||
core.UserURI,
|
||||
core.ViewURI,
|
||||
core.StaticURI,
|
||||
}
|
||||
|
||||
var reservedNames []string
|
||||
|
||||
func validatePassword(pw string) (b []byte) {
|
||||
b = []byte(pw)
|
||||
if len(pw) <= 56 {
|
||||
return
|
||||
}
|
||||
hash := sha256.New()
|
||||
hash.Write(b)
|
||||
b = hash.Sum(nil)
|
||||
return
|
||||
}
|
||||
|
||||
const timeFmt = "2006-01-02 15:04:05"
|
||||
|
||||
func now() string {
|
||||
return time.Now().Format(timeFmt)
|
||||
}
|
||||
|
||||
func created(username string) string {
|
||||
return username + " " + now()
|
||||
}
|
||||
|
|
@ -1,13 +0,0 @@
|
|||
// Copyright (C) 2021 Marius Schellenberger
|
||||
|
||||
package db
|
||||
|
||||
const lockoutPrefix = "lockout/"
|
||||
|
||||
func (db *DB) lockout(username, created string) error {
|
||||
return db.store.Set(lockoutPrefix+username+created, nil)
|
||||
}
|
||||
|
||||
func (db *DB) LockedOut(username, created string) bool {
|
||||
return db.store.Get(lockoutPrefix+username+created, nil) == nil
|
||||
}
|
||||
|
|
@ -1,115 +0,0 @@
|
|||
// Copyright (C) 2021 Marius Schellenberger
|
||||
|
||||
package db
|
||||
|
||||
import (
|
||||
"git.giftfish.de/ston1th/gowiki/pkg/core"
|
||||
"git.giftfish.de/ston1th/gowiki/pkg/log"
|
||||
"git.giftfish.de/ston1th/gowiki/pkg/render"
|
||||
"git.giftfish.de/ston1th/gowiki/pkg/store"
|
||||
)
|
||||
|
||||
const (
|
||||
versionKey = "version/version"
|
||||
currentVersion = 2
|
||||
)
|
||||
|
||||
type migrator func(*DB, int) error
|
||||
|
||||
var migrators = []migrator{
|
||||
func(_ *DB, _ int) error {
|
||||
return nil
|
||||
},
|
||||
func(db *DB, version int) (err error) {
|
||||
// recreate all pages with new format
|
||||
var pages []string
|
||||
db.store.ForEachPrefix(pagePrefix, func(k string, _ []byte) error {
|
||||
pages = append(pages, k)
|
||||
return nil
|
||||
})
|
||||
var p core.Page
|
||||
for _, v := range pages {
|
||||
k := pagePrefix + v
|
||||
err := db.store.Get(k, &p)
|
||||
if err != nil {
|
||||
log.Printf("db: migrator[%d]: pageGet %s: %s", version, k, err)
|
||||
continue
|
||||
}
|
||||
if p.Perm == 3 {
|
||||
p.Perm = core.Private
|
||||
}
|
||||
err = db.store.Set(k, &p)
|
||||
if err != nil {
|
||||
log.Printf("db: migrator[%d]: pageSet %s: %s", version, k, err)
|
||||
continue
|
||||
}
|
||||
}
|
||||
// create user sections
|
||||
users, err := db.GetUsers()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
for _, u := range users {
|
||||
err = db.CreateSection(u.Username, []string{u.Username}, true)
|
||||
if err != nil {
|
||||
log.Printf("db: migrator[%d]: userSection %s: %s", version, u.Username, err)
|
||||
}
|
||||
}
|
||||
return
|
||||
},
|
||||
func(db *DB, version int) (err error) {
|
||||
// re-render all pages
|
||||
var pages []string
|
||||
db.store.ForEachPrefix(pagePrefix, func(k string, _ []byte) error {
|
||||
pages = append(pages, k)
|
||||
return nil
|
||||
})
|
||||
var p core.Page
|
||||
for _, v := range pages {
|
||||
k := pagePrefix + v
|
||||
err := db.store.Get(k, &p)
|
||||
if err != nil {
|
||||
log.Printf("db: migrator[%d]: pageGet %s: %s", version, k, err)
|
||||
continue
|
||||
}
|
||||
render.Render(&p)
|
||||
err = db.store.Set(k, &p)
|
||||
if err != nil {
|
||||
log.Printf("db: migrator[%d]: pageSet %s: %s", version, k, err)
|
||||
continue
|
||||
}
|
||||
}
|
||||
return
|
||||
},
|
||||
}
|
||||
|
||||
func (db *DB) RunMigrations() (err error) {
|
||||
var version int
|
||||
err = db.store.Get(versionKey, &version)
|
||||
if err == store.ErrKeyNotFound {
|
||||
err = db.store.Set(versionKey, currentVersion)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
err = db.store.Get(versionKey, &version)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
log.Printf("db: database version %d", version)
|
||||
for version < currentVersion {
|
||||
v := version + 1
|
||||
log.Printf("db: running migration version %d", v)
|
||||
err = migrators[v](db, v)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
err = db.store.Set(versionKey, v)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
version = v
|
||||
log.Printf("db: database version %d", version)
|
||||
}
|
||||
return
|
||||
}
|
||||
241
pkg/db/page.go
241
pkg/db/page.go
|
|
@ -1,241 +0,0 @@
|
|||
// Copyright (C) 2021 Marius Schellenberger
|
||||
|
||||
package db
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"git.giftfish.de/ston1th/gowiki/pkg/core"
|
||||
"git.giftfish.de/ston1th/gowiki/pkg/render"
|
||||
"git.giftfish.de/ston1th/gowiki/pkg/store"
|
||||
"sort"
|
||||
"strings"
|
||||
)
|
||||
|
||||
const pagePrefix = "page/"
|
||||
|
||||
var (
|
||||
errPageNotFound = store.ErrKeyNotFound
|
||||
errPageExists = errors.New("db: page already exists")
|
||||
errPermissionDenied = errors.New("db: permission denied")
|
||||
errInvalidPermission = errors.New("db: invalid permission")
|
||||
errPrivateWikiPage = errors.New("db: private wiki pages are not allowed")
|
||||
errDeleteIndexPage = errors.New("db: index page can not be deleted")
|
||||
)
|
||||
|
||||
func (db *DB) GetAllSections(username string, admin bool) (sections core.SectionPages) {
|
||||
m := make(map[string]core.UpdatedPages)
|
||||
db.store.ForEachPrefix(pagePrefix, func(k string, _ []byte) error {
|
||||
if k == core.IndexURI {
|
||||
return nil
|
||||
}
|
||||
p, err := db.getPage(k, username)
|
||||
if err == nil {
|
||||
i := strings.Index(k, "/")
|
||||
if i == -1 {
|
||||
return nil
|
||||
}
|
||||
s := k[:i]
|
||||
if _, ok := m[s]; !ok {
|
||||
m[s] = core.UpdatedPages{}
|
||||
}
|
||||
m[s] = append(m[s], *p)
|
||||
}
|
||||
return nil
|
||||
})
|
||||
if admin {
|
||||
secs, err := db.GetSections()
|
||||
if err == nil {
|
||||
for _, s := range secs {
|
||||
if _, ok := m[s.Name]; !ok {
|
||||
m[s.Name] = core.UpdatedPages{}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
for s, p := range m {
|
||||
sort.Sort(p)
|
||||
l := len(p)
|
||||
if l > 5 {
|
||||
l = 5
|
||||
}
|
||||
sections = append(sections, core.SectionPage{
|
||||
Section: s,
|
||||
Pages: p[:l],
|
||||
})
|
||||
}
|
||||
sort.Sort(sections)
|
||||
return
|
||||
}
|
||||
|
||||
func (db *DB) GetAllSectionPages(section, username string, admin bool) (pages core.Pages, err error) {
|
||||
err = db.SectionExists(section)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
db.store.ForEachPrefix(pagePrefix, func(k string, _ []byte) error {
|
||||
if !strings.HasPrefix(k, section+"/") {
|
||||
return nil
|
||||
}
|
||||
if k == core.IndexURI {
|
||||
return nil
|
||||
}
|
||||
p, err := db.getPage(k, username)
|
||||
if err == nil {
|
||||
pages = append(pages, *p)
|
||||
}
|
||||
return nil
|
||||
})
|
||||
if len(pages) == 0 && !admin {
|
||||
err = errPageNotFound
|
||||
return
|
||||
}
|
||||
sort.Sort(pages)
|
||||
return
|
||||
}
|
||||
|
||||
func (db *DB) ValidatePage(st, username string) bool {
|
||||
p := db.cache.Get(st)
|
||||
if p == nil {
|
||||
p, err := db.getPage(st, username)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
db.cache.Add(st, p)
|
||||
return true
|
||||
}
|
||||
return db.ReadPerm(st, username, p.Perm)
|
||||
}
|
||||
|
||||
func (db *DB) GetPage(section, title, username string) (p *core.Page, err error) {
|
||||
st := render.StoreTitle(section, title)
|
||||
p = db.cache.Get(st)
|
||||
if p == nil {
|
||||
p, err = db.getPage(st, username)
|
||||
db.cache.Add(st, p)
|
||||
return
|
||||
}
|
||||
if !db.ReadPerm(section, username, p.Perm) {
|
||||
err = errPageNotFound
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (db *DB) getPage(title, username string) (p *core.Page, err error) {
|
||||
p = new(core.Page)
|
||||
err = db.store.Get(pagePrefix+title, p)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
if !db.ReadPerm(title, username, p.Perm) {
|
||||
err = errPageNotFound
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (db *DB) CreatePage(title, section, markdown, username string, p core.Permission) (page *core.Page, err error) {
|
||||
if p == core.Invalid {
|
||||
err = errInvalidPermission
|
||||
return
|
||||
}
|
||||
if section == core.WikiSection && p == core.Private {
|
||||
err = errPrivateWikiPage
|
||||
return
|
||||
}
|
||||
st := render.StoreTitle(section, render.Title(title))
|
||||
_, err = db.getPage(st, username)
|
||||
if err != store.ErrKeyNotFound {
|
||||
if err == nil {
|
||||
err = errPageExists
|
||||
}
|
||||
return
|
||||
}
|
||||
s, err := db.GetSection(section)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
if _, ok := core.Contains(username, s.Members); !ok {
|
||||
err = errPermissionDenied
|
||||
return
|
||||
}
|
||||
|
||||
page = &core.Page{
|
||||
Title: title,
|
||||
StoreTitle: st,
|
||||
Markdown: markdown,
|
||||
Created: created(username),
|
||||
Perm: p,
|
||||
}
|
||||
search := render.Render(page)
|
||||
|
||||
err = db.store.Set(pagePrefix+st, page)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
err = db.Index.Add(title, st, search)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
db.cache.Add(st, page)
|
||||
return
|
||||
}
|
||||
|
||||
func (db *DB) UpdatePage(title, section, markdown, username string, p core.Permission) (err error) {
|
||||
if p == core.Invalid {
|
||||
err = errInvalidPermission
|
||||
return
|
||||
}
|
||||
if section == core.WikiSection && p == core.Private {
|
||||
err = errPrivateWikiPage
|
||||
return
|
||||
}
|
||||
st := render.StoreTitle(section, title)
|
||||
page, err := db.getPage(st, username)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
if !db.WritePerm(section, username, page.Perm) {
|
||||
return errPermissionDenied
|
||||
}
|
||||
|
||||
page.Markdown = markdown
|
||||
page.Updated = created(username)
|
||||
page.Perm = p
|
||||
search := render.Render(page)
|
||||
|
||||
err = db.store.Set(pagePrefix+st, page)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
err = db.Index.Add(page.Title, st, search)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
db.cache.Add(st, page)
|
||||
return
|
||||
}
|
||||
|
||||
func (db *DB) deletePage(st string) (err error) {
|
||||
if err = db.store.Delete(pagePrefix + st); err != nil {
|
||||
return
|
||||
}
|
||||
db.cache.Delete(st)
|
||||
return db.Index.Delete(st)
|
||||
}
|
||||
|
||||
func (db *DB) DeletePage(title, section, username string) (err error) {
|
||||
var page *core.Page
|
||||
if section == core.WikiSection && title == core.IndexPage {
|
||||
err = errDeleteIndexPage
|
||||
return
|
||||
}
|
||||
st := render.StoreTitle(section, title)
|
||||
if page, err = db.getPage(st, username); err != nil {
|
||||
return
|
||||
}
|
||||
if !db.WritePerm(section, username, page.Perm) {
|
||||
return errPermissionDenied
|
||||
}
|
||||
return db.deletePage(st)
|
||||
}
|
||||
|
|
@ -1,29 +0,0 @@
|
|||
// Copyright (C) 2021 Marius Schellenberger
|
||||
|
||||
package db
|
||||
|
||||
import (
|
||||
"git.giftfish.de/ston1th/gowiki/pkg/core"
|
||||
"git.giftfish.de/ston1th/gowiki/pkg/log"
|
||||
"git.giftfish.de/ston1th/gowiki/pkg/render"
|
||||
)
|
||||
|
||||
func (db *DB) ReadPerm(st, username string, p core.Permission) bool {
|
||||
section, _ := render.SplitStoreTitle(st)
|
||||
s, err := db.GetSection(section)
|
||||
if err != nil {
|
||||
log.Debugf("db: read: %s", err)
|
||||
return false
|
||||
}
|
||||
return core.ReadPerm(username, p, s)
|
||||
}
|
||||
|
||||
func (db *DB) WritePerm(st, username string, p core.Permission) bool {
|
||||
section, _ := render.SplitStoreTitle(st)
|
||||
s, err := db.GetSection(section)
|
||||
if err != nil {
|
||||
log.Debugf("db: read: %s", err)
|
||||
return false
|
||||
}
|
||||
return core.WritePerm(username, p, s)
|
||||
}
|
||||
|
|
@ -1,118 +0,0 @@
|
|||
// Copyright (C) 2021 Marius Schellenberger
|
||||
|
||||
package db
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"git.giftfish.de/ston1th/gowiki/pkg/core"
|
||||
"git.giftfish.de/ston1th/gowiki/pkg/log"
|
||||
"sort"
|
||||
"strings"
|
||||
)
|
||||
|
||||
const sectionPrefix = "section/"
|
||||
|
||||
var (
|
||||
errSectionExists = errors.New("db: section already exist")
|
||||
)
|
||||
|
||||
func (db *DB) GetSections() (secs core.Sections, err error) {
|
||||
err = db.store.ForEachPrefix(sectionPrefix, func(k string, _ []byte) error {
|
||||
s, err := db.GetSection(k)
|
||||
if err == nil {
|
||||
secs = append(secs, s)
|
||||
}
|
||||
return nil
|
||||
})
|
||||
sort.Sort(secs)
|
||||
return
|
||||
}
|
||||
|
||||
func (db *DB) GetUserSections(username string) (sections map[string]bool, err error) {
|
||||
sections = make(map[string]bool)
|
||||
secs, err := db.GetSections()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
for _, s := range secs {
|
||||
if _, ok := core.Contains(username, s.Members); ok {
|
||||
sections[s.Name] = false
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (db *DB) GetSection(section string) (s core.Section, err error) {
|
||||
err = db.store.Get(sectionPrefix+section, &s)
|
||||
return
|
||||
}
|
||||
|
||||
func (db *DB) SectionExists(section string) error {
|
||||
return db.store.Get(sectionPrefix+section, nil)
|
||||
}
|
||||
|
||||
func (db *DB) CreateSection(section string, members []string, user bool) error {
|
||||
section = userRe.ReplaceAllString(section, "")
|
||||
if _, ok := core.Contains(section, reservedNames); ok {
|
||||
return fmt.Errorf("%s: %s", errNameReserved, section)
|
||||
}
|
||||
if err := db.SectionExists(section); err == nil {
|
||||
return errSectionExists
|
||||
}
|
||||
return db.UpdateSection(section, members, user)
|
||||
}
|
||||
|
||||
func (db *DB) UpdateSection(section string, members []string, user bool) error {
|
||||
section = userRe.ReplaceAllString(section, "")
|
||||
if _, ok := core.Contains(section, reservedNames); ok {
|
||||
return fmt.Errorf("%s: %s", errNameReserved, section)
|
||||
}
|
||||
var m []string
|
||||
if user {
|
||||
m = append(m, section)
|
||||
} else {
|
||||
for _, v := range members {
|
||||
if v == "" {
|
||||
continue
|
||||
}
|
||||
if err := db.UserExists(v); err == nil {
|
||||
m = append(m, v)
|
||||
}
|
||||
}
|
||||
}
|
||||
return db.store.Set(sectionPrefix+section, &core.Section{
|
||||
Name: section,
|
||||
User: user,
|
||||
Members: m,
|
||||
})
|
||||
}
|
||||
|
||||
func (db *DB) DeleteSection(section string) error {
|
||||
if section == core.WikiSection {
|
||||
return errors.New("section '" + core.WikiSection + "' cannot be deleted")
|
||||
}
|
||||
err := db.SectionExists(section)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = db.store.Delete(sectionPrefix + section)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
section += "/"
|
||||
var pages []string
|
||||
db.store.ForEachPrefix(pagePrefix, func(k string, _ []byte) error {
|
||||
if strings.HasPrefix(k, section) {
|
||||
pages = append(pages, k)
|
||||
}
|
||||
return nil
|
||||
})
|
||||
for _, p := range pages {
|
||||
err := db.deletePage(p)
|
||||
if err != nil {
|
||||
log.Debugf("DeleteSection %s, %s", section, err)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
230
pkg/db/user.go
230
pkg/db/user.go
|
|
@ -1,230 +0,0 @@
|
|||
// Copyright (C) 2021 Marius Schellenberger
|
||||
|
||||
package db
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"git.giftfish.de/ston1th/gowiki/pkg/core"
|
||||
"git.giftfish.de/ston1th/gowiki/pkg/otp"
|
||||
"git.giftfish.de/ston1th/jwt/v3"
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
"regexp"
|
||||
"sort"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
const userPrefix = "user/"
|
||||
|
||||
var (
|
||||
errUserLocked = errors.New("db: user locked")
|
||||
errUserLogin = errors.New("db: wrong login")
|
||||
errUserPin = errors.New("db: wrong pin")
|
||||
errUserExists = errors.New("db: user already exist")
|
||||
errNameReserved = errors.New("db: name is reserved")
|
||||
|
||||
userRe = regexp.MustCompile("[^a-zA-Z0-9]+")
|
||||
)
|
||||
|
||||
func (db *DB) GetUsers() (users core.Users, err error) {
|
||||
err = db.store.ForEachPrefix(userPrefix, func(k string, _ []byte) error {
|
||||
u, err := db.GetUser(k)
|
||||
u.Password = ""
|
||||
if err == nil {
|
||||
users = append(users, u)
|
||||
}
|
||||
return nil
|
||||
})
|
||||
sort.Sort(users)
|
||||
return
|
||||
}
|
||||
|
||||
func (db *DB) GetUserWithoutPassword(username string) (u *core.User, err error) {
|
||||
u, err = db.GetUser(username)
|
||||
u.Password = ""
|
||||
return
|
||||
}
|
||||
|
||||
func (db *DB) GetUserSecret(username string) (secret string, err error) {
|
||||
var user *core.User
|
||||
user, err = db.GetUser(username)
|
||||
secret = user.Secret
|
||||
return
|
||||
}
|
||||
|
||||
func (db *DB) GetUser(username string) (u *core.User, err error) {
|
||||
u = new(core.User)
|
||||
err = db.store.Get(userPrefix+username, u)
|
||||
return
|
||||
}
|
||||
|
||||
func (db *DB) UserExists(username string) error {
|
||||
return db.store.Get(userPrefix+username, nil)
|
||||
}
|
||||
|
||||
func (db *DB) CreateUser(username, password string, admin bool) error {
|
||||
username = userRe.ReplaceAllString(username, "")
|
||||
if _, ok := core.Contains(username, reservedNames); ok {
|
||||
return fmt.Errorf("%s: %s", errNameReserved, username)
|
||||
}
|
||||
if err := db.UserExists(username); err == nil {
|
||||
return errUserExists
|
||||
}
|
||||
hash, err := bcrypt.GenerateFromPassword(validatePassword(password), bcryptCost)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = db.CreateSection(username, nil, true)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return db.store.Set(userPrefix+username, &core.User{
|
||||
Username: username,
|
||||
Password: string(hash),
|
||||
Created: strconv.Itoa(int(jwt.Now())),
|
||||
Admin: admin,
|
||||
})
|
||||
}
|
||||
|
||||
func (db *DB) Login(username, password string) (u *core.User, err error) {
|
||||
if username == defUser && !db.admin {
|
||||
err = errors.New("db: user disabled")
|
||||
return
|
||||
}
|
||||
u, err = db.GetUser(username)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
if u.Locked == 3 {
|
||||
err = errUserLocked
|
||||
return
|
||||
}
|
||||
if err = bcrypt.CompareHashAndPassword([]byte(u.Password), validatePassword(password)); err != nil {
|
||||
u.Locked++
|
||||
err = db.store.Set(userPrefix+username, u)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
err = errUserLogin
|
||||
return
|
||||
}
|
||||
if u.Locked > 0 {
|
||||
u.Locked = 0
|
||||
err = db.store.Set(userPrefix+username, u)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (db *DB) Totp(username, pin string) (u *core.User, err error) {
|
||||
u, err = db.GetUser(username)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
if u.Locked == 3 {
|
||||
err = errUserLocked
|
||||
return
|
||||
}
|
||||
if !otp.Validate(pin, u.Secret) {
|
||||
u.Locked++
|
||||
err = db.store.Set(userPrefix+username, u)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
err = errUserPin
|
||||
return
|
||||
}
|
||||
if u.Locked > 0 {
|
||||
u.Locked = 0
|
||||
err = db.store.Set(userPrefix+username, u)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (db *DB) UnlockUser(username string) (err error) {
|
||||
var u *core.User
|
||||
u, err = db.GetUser(username)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
if u.Locked > 0 {
|
||||
u.Locked = 0
|
||||
err = db.store.Set(userPrefix+username, u)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (db *DB) UpdateUserSecret(username, secret string) error {
|
||||
u, err := db.GetUser(username)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
u.Secret = secret
|
||||
return db.store.Set(userPrefix+username, u)
|
||||
}
|
||||
|
||||
func (db *DB) UpdateUserPassword(username, password string) error {
|
||||
if password == "" {
|
||||
return errors.New("empty password")
|
||||
}
|
||||
u, err := db.GetUser(username)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
hash, err := bcrypt.GenerateFromPassword(validatePassword(password), bcryptCost)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
u.Password = string(hash)
|
||||
return db.store.Set(userPrefix+username, u)
|
||||
}
|
||||
|
||||
func (db *DB) ResetAdmin() error {
|
||||
return db.UpdateUserPassword(defUser, defPassword)
|
||||
}
|
||||
|
||||
func (db *DB) AdminUpdateUser(username, password string, admin bool) error {
|
||||
if username == defUser && !admin {
|
||||
return errors.New("user '" + defUser + "' cannot lose admin privileges")
|
||||
}
|
||||
u, err := db.GetUser(username)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
u.Admin = admin
|
||||
if password != "" {
|
||||
hash, err := bcrypt.GenerateFromPassword(validatePassword(password), bcryptCost)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
u.Password = string(hash)
|
||||
}
|
||||
return db.store.Set(userPrefix+username, u)
|
||||
}
|
||||
|
||||
func (db *DB) DeleteUser(username string) error {
|
||||
if username == defUser {
|
||||
return errors.New("user '" + defUser + "' cannot be deleted")
|
||||
}
|
||||
u, err := db.GetUser(username)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = db.store.Delete(userPrefix + username)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = db.lockout(username, u.Created)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return db.DeleteSection(username)
|
||||
}
|
||||
|
|
@ -1,130 +0,0 @@
|
|||
// Copyright (C) 2021 Marius Schellenberger
|
||||
|
||||
package index
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"git.giftfish.de/ston1th/godrop/v2"
|
||||
"git.giftfish.de/ston1th/gowiki/pkg/core"
|
||||
"git.giftfish.de/ston1th/gowiki/pkg/render"
|
||||
"github.com/blevesearch/bleve"
|
||||
"github.com/blevesearch/bleve/mapping"
|
||||
"html/template"
|
||||
"os"
|
||||
)
|
||||
|
||||
const (
|
||||
maxSearchResult = 101
|
||||
docType = "page"
|
||||
titleField = "title"
|
||||
storeTitleField = "store_title"
|
||||
searchField = "search"
|
||||
)
|
||||
|
||||
type indexPage struct {
|
||||
Title string `json:"title"`
|
||||
StoreTitle string `json:"store_title"`
|
||||
Search string `json:"search"`
|
||||
}
|
||||
|
||||
func (*indexPage) Type() string {
|
||||
return docType
|
||||
}
|
||||
|
||||
type Index struct {
|
||||
i bleve.Index
|
||||
}
|
||||
|
||||
func New(path string) (i *Index, err error) {
|
||||
var bi bleve.Index
|
||||
err = godrop.Unveil(path, "rwc")
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
if _, err = os.Stat(path); os.IsNotExist(err) {
|
||||
err = os.Mkdir(path, 0o700)
|
||||
if err != nil {
|
||||
err = errors.New("index: " + err.Error())
|
||||
return
|
||||
}
|
||||
err = godrop.Unveil(path, "rwc")
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
var m *mapping.IndexMappingImpl
|
||||
m, err = createMapping()
|
||||
if err != nil {
|
||||
err = errors.New("mapping: " + err.Error())
|
||||
return
|
||||
}
|
||||
bi, err = bleve.New(path, m)
|
||||
if err != nil {
|
||||
err = errors.New("index: " + err.Error())
|
||||
return
|
||||
}
|
||||
} else {
|
||||
bi, err = bleve.Open(path)
|
||||
if err != nil {
|
||||
err = errors.New("index: " + err.Error())
|
||||
return
|
||||
}
|
||||
}
|
||||
i = &Index{i: bi}
|
||||
return
|
||||
}
|
||||
|
||||
func (i *Index) DocCount() (c uint64) {
|
||||
c, _ = i.i.DocCount()
|
||||
return
|
||||
}
|
||||
|
||||
func (i *Index) Close() error {
|
||||
return i.i.Close()
|
||||
}
|
||||
|
||||
func (i *Index) Add(title, storeTitle, search string) error {
|
||||
doc, _ := i.i.Document(storeTitle)
|
||||
if doc != nil {
|
||||
err := i.Delete(storeTitle)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return i.i.Index(storeTitle, &indexPage{title, storeTitle, search})
|
||||
}
|
||||
|
||||
func (i *Index) Delete(storeTitle string) error {
|
||||
return i.i.Delete(storeTitle)
|
||||
}
|
||||
|
||||
func (i *Index) Search(search string) (results []core.Result, err error) {
|
||||
req := bleve.NewSearchRequest(bleve.NewQueryStringQuery(search))
|
||||
req.Highlight = bleve.NewHighlightWithStyle("html")
|
||||
req.Size = maxSearchResult
|
||||
res, err := i.i.Search(req)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
for _, hit := range res.Hits {
|
||||
if hit == nil {
|
||||
continue
|
||||
}
|
||||
r := core.Result{StoreTitle: hit.ID}
|
||||
r.Section, r.Title = render.SplitStoreTitle(hit.ID)
|
||||
r.Title = render.Untitle(r.Title)
|
||||
d, err := i.i.Document(hit.ID)
|
||||
if d == nil || err != nil {
|
||||
continue
|
||||
}
|
||||
for fragField, frags := range hit.Fragments {
|
||||
if fragField == titleField || fragField == searchField {
|
||||
for _, f := range frags {
|
||||
r.HTML += template.HTML(f)
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
results = append(results, r)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
|
@ -1,56 +0,0 @@
|
|||
// Copyright (C) 2021 Marius Schellenberger
|
||||
|
||||
package index
|
||||
|
||||
import (
|
||||
"github.com/blevesearch/bleve"
|
||||
"github.com/blevesearch/bleve/analysis/analyzer/custom"
|
||||
"github.com/blevesearch/bleve/analysis/token/edgengram"
|
||||
"github.com/blevesearch/bleve/analysis/token/lowercase"
|
||||
"github.com/blevesearch/bleve/analysis/tokenizer/unicode"
|
||||
"github.com/blevesearch/bleve/mapping"
|
||||
)
|
||||
|
||||
const (
|
||||
bigram = "bigram_tokenfilter"
|
||||
ngram = "fulltext_ngram"
|
||||
)
|
||||
|
||||
func createMapping() (m *mapping.IndexMappingImpl, err error) {
|
||||
m = bleve.NewIndexMapping()
|
||||
|
||||
err = m.AddCustomTokenFilter(bigram, map[string]interface{}{
|
||||
"type": edgengram.Name,
|
||||
"side": edgengram.FRONT,
|
||||
"min": 3.0,
|
||||
"max": 25.0,
|
||||
})
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
err = m.AddCustomAnalyzer(ngram, map[string]interface{}{
|
||||
"type": custom.Name,
|
||||
"tokenizer": unicode.Name,
|
||||
"token_filters": []string{
|
||||
lowercase.Name,
|
||||
bigram,
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
doc := bleve.NewDocumentMapping()
|
||||
ti := bleve.NewTextFieldMapping()
|
||||
ti.Analyzer = ngram
|
||||
doc.AddFieldMappingsAt(titleField, ti)
|
||||
st := bleve.NewTextFieldMapping()
|
||||
st.Analyzer = ngram
|
||||
doc.AddFieldMappingsAt(storeTitleField, st)
|
||||
s := bleve.NewTextFieldMapping()
|
||||
s.Analyzer = ngram
|
||||
doc.AddFieldMappingsAt(searchField, s)
|
||||
|
||||
m.AddDocumentMapping(docType, doc)
|
||||
return
|
||||
}
|
||||
|
|
@ -1,63 +0,0 @@
|
|||
// Copyright (C) 2021 Marius Schellenberger
|
||||
|
||||
package log
|
||||
|
||||
import (
|
||||
"git.giftfish.de/ston1th/godrop/v2"
|
||||
"git.giftfish.de/ston1th/gowiki/pkg/core"
|
||||
stdlog "log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
)
|
||||
|
||||
var (
|
||||
log = stdlog.New(os.Stdout, "", stdlog.LstdFlags)
|
||||
debug = false
|
||||
)
|
||||
|
||||
func Stderr() {
|
||||
log = stdlog.New(os.Stderr, "", stdlog.LstdFlags)
|
||||
}
|
||||
|
||||
func InitLogger(cfg *core.Config) {
|
||||
debug = cfg.Debug
|
||||
if cfg.LogFile == "-" {
|
||||
return
|
||||
}
|
||||
logfile := filepath.Join(cfg.DataDir, cfg.LogFile)
|
||||
err := godrop.Unveil(logfile, "rwc")
|
||||
if err != nil {
|
||||
stdlog.Fatal(err)
|
||||
return
|
||||
}
|
||||
f, err := os.OpenFile(logfile, os.O_RDWR|os.O_CREATE|os.O_APPEND, 0o600)
|
||||
if err != nil {
|
||||
stdlog.Fatal(err)
|
||||
}
|
||||
stdlog.SetOutput(f)
|
||||
log = stdlog.New(f, "", stdlog.LstdFlags)
|
||||
}
|
||||
|
||||
func Println(v ...interface{}) {
|
||||
log.Println(v...)
|
||||
}
|
||||
|
||||
func Printf(fmt string, v ...interface{}) {
|
||||
log.Printf(fmt, v...)
|
||||
}
|
||||
|
||||
func Fatal(v ...interface{}) {
|
||||
log.Fatal(v...)
|
||||
}
|
||||
|
||||
func Debug(v ...interface{}) {
|
||||
if debug {
|
||||
log.Println(append([]interface{}{"debug:"}, v...)...)
|
||||
}
|
||||
}
|
||||
|
||||
func Debugf(fmt string, v ...interface{}) {
|
||||
if debug {
|
||||
log.Printf("debug: "+fmt, v...)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,76 +0,0 @@
|
|||
// Copyright (C) 2021 Marius Schellenberger
|
||||
|
||||
package otp
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/base64"
|
||||
"errors"
|
||||
"github.com/pquerna/otp"
|
||||
"github.com/pquerna/otp/totp"
|
||||
"html/template"
|
||||
"image/png"
|
||||
)
|
||||
|
||||
const size = 200
|
||||
|
||||
func base64Image(key *otp.Key) (data template.URL, err error) {
|
||||
var buf bytes.Buffer
|
||||
img, err := key.Image(size, size)
|
||||
if err != nil {
|
||||
err = errors.New("otp: " + err.Error())
|
||||
return
|
||||
}
|
||||
err = png.Encode(&buf, img)
|
||||
if err != nil {
|
||||
err = errors.New("otp: " + err.Error())
|
||||
return
|
||||
}
|
||||
data = template.URL("data:image/png;base64," + base64.StdEncoding.EncodeToString(buf.Bytes()))
|
||||
return
|
||||
}
|
||||
|
||||
type Request struct {
|
||||
Username string
|
||||
Image template.URL
|
||||
Secret string
|
||||
URL string
|
||||
}
|
||||
|
||||
func New(username string) (req Request, err error) {
|
||||
key, err := totp.Generate(totp.GenerateOpts{
|
||||
Issuer: "gowiki",
|
||||
AccountName: username,
|
||||
})
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
data, err := base64Image(key)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
return Request{
|
||||
Username: username,
|
||||
Image: data,
|
||||
Secret: key.Secret(),
|
||||
URL: key.URL(),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func Validate(pin, secret string) bool {
|
||||
if secret == "" {
|
||||
return true
|
||||
}
|
||||
return totp.Validate(pin, secret)
|
||||
}
|
||||
|
||||
func ImageSecretFromURL(url string) (data template.URL, secret string, err error) {
|
||||
key, err := otp.NewKeyFromURL(url)
|
||||
if err != nil {
|
||||
err = errors.New("otp: " + err.Error())
|
||||
return
|
||||
}
|
||||
secret = key.Secret()
|
||||
data, err = base64Image(key)
|
||||
return
|
||||
}
|
||||
|
|
@ -1,25 +0,0 @@
|
|||
// Copyright (C) 2021 Marius Schellenberger
|
||||
|
||||
package otp
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestNew(t *testing.T) {
|
||||
_, err := New("admin")
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestImageSecretFromURL(t *testing.T) {
|
||||
r, _ := New("admin")
|
||||
_, secret, err := ImageSecretFromURL(r.URL)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
if secret != r.Secret {
|
||||
t.Error("secret is not equal")
|
||||
}
|
||||
}
|
||||
|
|
@ -1,87 +0,0 @@
|
|||
// Copyright (C) 2021 Marius Schellenberger
|
||||
|
||||
package render
|
||||
|
||||
import (
|
||||
"git.giftfish.de/ston1th/gowiki/pkg/core"
|
||||
"github.com/blevesearch/bleve/analysis"
|
||||
htmlchar "github.com/blevesearch/bleve/analysis/char/html"
|
||||
"github.com/russross/blackfriday"
|
||||
"html/template"
|
||||
"regexp"
|
||||
"strings"
|
||||
)
|
||||
|
||||
const delim = "+"
|
||||
|
||||
var filter = makeFilter()
|
||||
|
||||
func makeFilter() analysis.CharFilter {
|
||||
f, _ := htmlchar.CharFilterConstructor(nil, nil)
|
||||
return f
|
||||
}
|
||||
|
||||
var (
|
||||
titleRe = regexp.MustCompile("[^a-zA-Z0-9 -]+")
|
||||
whiteSpace = regexp.MustCompile(`\s+`)
|
||||
)
|
||||
|
||||
const (
|
||||
commonHtmlFlags = 0 |
|
||||
blackfriday.HTML_USE_XHTML |
|
||||
blackfriday.HTML_USE_SMARTYPANTS |
|
||||
blackfriday.HTML_SMARTYPANTS_FRACTIONS |
|
||||
blackfriday.HTML_SMARTYPANTS_DASHES |
|
||||
blackfriday.HTML_SMARTYPANTS_LATEX_DASHES |
|
||||
blackfriday.HTML_TOC |
|
||||
blackfriday.HTML_HREF_TARGET_BLANK |
|
||||
blackfriday.HTML_NOREFERRER_LINKS |
|
||||
blackfriday.HTML_NOFOLLOW_LINKS |
|
||||
blackfriday.HTML_NOOPENER_LINKS
|
||||
|
||||
commonExtensions = 0 |
|
||||
blackfriday.EXTENSION_NO_INTRA_EMPHASIS |
|
||||
blackfriday.EXTENSION_TABLES |
|
||||
blackfriday.EXTENSION_FENCED_CODE |
|
||||
blackfriday.EXTENSION_AUTOLINK |
|
||||
blackfriday.EXTENSION_STRIKETHROUGH |
|
||||
blackfriday.EXTENSION_SPACE_HEADERS |
|
||||
blackfriday.EXTENSION_HEADER_IDS |
|
||||
blackfriday.EXTENSION_BACKSLASH_LINE_BREAK |
|
||||
blackfriday.EXTENSION_DEFINITION_LISTS |
|
||||
blackfriday.EXTENSION_AUTO_HEADER_IDS
|
||||
)
|
||||
|
||||
func Render(p *core.Page) string {
|
||||
renderer := blackfriday.HtmlRenderer(commonHtmlFlags, "", "")
|
||||
rend := blackfriday.MarkdownOptions([]byte(p.Markdown), renderer,
|
||||
blackfriday.Options{Extensions: commonExtensions},
|
||||
)
|
||||
html := string(rend)
|
||||
html = strings.ReplaceAll(html, "<table>", `<table class="table">`)
|
||||
p.HTML = template.HTML(html)
|
||||
return whiteSpace.ReplaceAllString(string(filter.Filter(rend)), " ")
|
||||
}
|
||||
|
||||
func Title(title string) string {
|
||||
return whiteSpace.ReplaceAllString(titleRe.ReplaceAllString(title, ""), delim)
|
||||
}
|
||||
|
||||
func Untitle(title string) string {
|
||||
return strings.ReplaceAll(title, delim, " ")
|
||||
}
|
||||
|
||||
func StoreTitle(section, title string) string {
|
||||
return section + "/" + title
|
||||
}
|
||||
|
||||
func SplitStoreTitle(st string) (section, title string) {
|
||||
a := strings.Split(st, "/")
|
||||
switch len(a) {
|
||||
case 1:
|
||||
return a[0], ""
|
||||
case 2:
|
||||
return a[0], a[1]
|
||||
}
|
||||
return
|
||||
}
|
||||
|
|
@ -1,35 +0,0 @@
|
|||
// Copyright (C) 2021 Marius Schellenberger
|
||||
|
||||
package render
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestTitleFunctions(t *testing.T) {
|
||||
section := "section"
|
||||
title := "title"
|
||||
combined := "section/title"
|
||||
t.Run("StoreTitle", func(t *testing.T) {
|
||||
if StoreTitle(section, title) != combined {
|
||||
t.Fail()
|
||||
}
|
||||
})
|
||||
t.Run("SplitStoreTitle", func(t *testing.T) {
|
||||
se, ti := SplitStoreTitle(combined)
|
||||
if se != section || ti != title {
|
||||
t.Fail()
|
||||
}
|
||||
})
|
||||
t.Run("SplitStoreTitleSingle", func(t *testing.T) {
|
||||
se, ti := SplitStoreTitle(section)
|
||||
if se != section || ti != "" {
|
||||
t.Fail()
|
||||
}
|
||||
})
|
||||
t.Run("Title", func(t *testing.T) {
|
||||
if Title("9ßw$%4h4t-8v74 287(G /SV&3[45v}=94#+?)") != "9w4h4t-8v74+287G+SV345v94" {
|
||||
t.Fail()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
|
@ -1,305 +0,0 @@
|
|||
// Copyright (C) 2021 Marius Schellenberger
|
||||
|
||||
package server
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"git.giftfish.de/ston1th/gowiki/pkg/core"
|
||||
"git.giftfish.de/ston1th/gowiki/pkg/log"
|
||||
"git.giftfish.de/ston1th/jwt/v3"
|
||||
"github.com/gorilla/mux"
|
||||
"html/template"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strconv"
|
||||
"time"
|
||||
)
|
||||
|
||||
const (
|
||||
userClaim = "user"
|
||||
adminClaim = "admin"
|
||||
userCreatedClaim = "user_created"
|
||||
pageCreatedClaim = "page_created"
|
||||
sharedClaim = "shared"
|
||||
sectionClaim = "section"
|
||||
titleClaim = "title"
|
||||
totpClaim = "totp"
|
||||
rememberClaim = "remember"
|
||||
refererClaim = "referer"
|
||||
|
||||
empty = "(empty)"
|
||||
)
|
||||
|
||||
func newContext(w http.ResponseWriter, r *http.Request, s *HTTPServer) *Context {
|
||||
h := w.Header()
|
||||
h.Set("X-Frame-Options", "DENY")
|
||||
h.Set("X-Content-Type-Options", "nosniff")
|
||||
h.Set("X-XSS-Protection", "1; mode=block")
|
||||
h.Set("Content-Security-Policy", "default-src 'none';style-src 'self';img-src 'self' https: data:;connect-src 'self';frame-ancestors 'none'")
|
||||
h.Set("Referrer-Policy", "same-origin")
|
||||
return &Context{
|
||||
Request: r,
|
||||
Response: w,
|
||||
Srv: s,
|
||||
Time: time.Now(),
|
||||
}
|
||||
}
|
||||
|
||||
type Context struct {
|
||||
Request *http.Request
|
||||
Response http.ResponseWriter
|
||||
Srv *HTTPServer
|
||||
T *template.Template
|
||||
Status int
|
||||
Err error
|
||||
Time time.Time
|
||||
|
||||
Token jwt.Token
|
||||
|
||||
Data webData
|
||||
}
|
||||
|
||||
type webData struct {
|
||||
Version string
|
||||
Time string
|
||||
|
||||
Title string
|
||||
BodyTitle string
|
||||
|
||||
Admin bool
|
||||
Login bool
|
||||
|
||||
Key string
|
||||
|
||||
User string
|
||||
Token string
|
||||
Msg string
|
||||
Search string
|
||||
|
||||
Data interface{}
|
||||
}
|
||||
|
||||
type loginData struct {
|
||||
User string
|
||||
Referer string
|
||||
}
|
||||
|
||||
type sectionData struct {
|
||||
Name string
|
||||
Members map[string]bool
|
||||
}
|
||||
|
||||
type pageData struct {
|
||||
Page core.Page
|
||||
Sections map[string]bool
|
||||
}
|
||||
|
||||
func (c *Context) Exec() {
|
||||
defer c.log()
|
||||
if c.T == nil {
|
||||
c.Status = http.StatusInternalServerError
|
||||
c.Err = errors.New("template is nil")
|
||||
return
|
||||
}
|
||||
c.Data.Token = newXsrf(c.Token.RawSig())
|
||||
|
||||
c.Data.Version = c.Srv.Config.Version
|
||||
if c.Data.BodyTitle == "" {
|
||||
c.Data.BodyTitle = c.Data.Title
|
||||
}
|
||||
c.Data.Admin = c.Admin()
|
||||
c.Data.User = c.User()
|
||||
c.Data.Login = c.LoggedOn()
|
||||
|
||||
t := strconv.Itoa(int(time.Since(c.Time).Nanoseconds()/1e6)) + "ms"
|
||||
if t == "0ms" {
|
||||
t = strconv.Itoa(int(time.Since(c.Time).Nanoseconds()/1e5)) + "µs"
|
||||
}
|
||||
c.Data.Time = t
|
||||
|
||||
if c.Data.Msg != "" {
|
||||
c.Status = http.StatusBadRequest
|
||||
}
|
||||
|
||||
c.Err = c.T.Execute(c.Response, c.Data)
|
||||
if c.Err != nil {
|
||||
c.Status = http.StatusInternalServerError
|
||||
}
|
||||
if c.Status == 0 {
|
||||
c.Status = http.StatusOK
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Context) log() {
|
||||
if c.Err != nil {
|
||||
log.Printf("%s %s %s %d error: %s\n", c.Request.RemoteAddr, c.Request.Method, c.Request.URL, c.Status, c.Err)
|
||||
return
|
||||
}
|
||||
log.Printf("%s %s %s %d\n", c.Request.RemoteAddr, c.Request.Method, c.Request.URL, c.Status)
|
||||
}
|
||||
|
||||
func (c *Context) Error(i interface{}) {
|
||||
switch v := i.(type) {
|
||||
case string:
|
||||
c.Data.Msg = v
|
||||
case error:
|
||||
c.Data.Msg = v.Error()
|
||||
}
|
||||
c.Exec()
|
||||
}
|
||||
|
||||
func (c *Context) NotFound() {
|
||||
c.Template("notFoundHandler")
|
||||
c.Data = webData{Title: "404"}
|
||||
c.Status = http.StatusNotFound
|
||||
c.Response.WriteHeader(http.StatusNotFound)
|
||||
c.Exec()
|
||||
}
|
||||
|
||||
func (c *Context) Forbidden() {
|
||||
c.Template("forbiddenHandler")
|
||||
c.Data = webData{Title: "403"}
|
||||
c.Status = http.StatusForbidden
|
||||
c.Response.WriteHeader(http.StatusForbidden)
|
||||
c.Exec()
|
||||
}
|
||||
|
||||
func (c *Context) Template(name string) {
|
||||
c.T = c.Srv.templ[name]
|
||||
}
|
||||
|
||||
func (c *Context) Write(buf []byte) (err error) {
|
||||
_, err = c.Response.Write(buf)
|
||||
return
|
||||
}
|
||||
|
||||
func (c *Context) SetHeader(name, value string) {
|
||||
c.Response.Header().Set(name, value)
|
||||
}
|
||||
|
||||
func (c *Context) Redirect(uri string, code int) {
|
||||
if len(uri) > 0 && uri[0] != '/' {
|
||||
uri = "/" + uri
|
||||
}
|
||||
log.Printf("%s %s %s %d %s\n", c.Request.RemoteAddr, c.Request.Method, c.Request.URL, code, uri)
|
||||
http.Redirect(c.Response, c.Request, uri, code)
|
||||
}
|
||||
|
||||
func (c *Context) Method() string {
|
||||
return c.Request.Method
|
||||
}
|
||||
|
||||
func (c *Context) Path() string {
|
||||
return c.Request.URL.Path
|
||||
}
|
||||
|
||||
func (c *Context) RefererURI() string {
|
||||
u, err := url.Parse(c.Request.Header.Get("Referer"))
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
return u.RequestURI()
|
||||
}
|
||||
|
||||
func (c *Context) FormSlice(name string) []string {
|
||||
// does nothing if called twice
|
||||
c.Request.ParseForm()
|
||||
return c.Request.PostForm[name]
|
||||
}
|
||||
|
||||
func (c *Context) Form(name string) string {
|
||||
return c.Request.PostFormValue(name)
|
||||
}
|
||||
|
||||
func (c *Context) Var(name string) (ret string) {
|
||||
ret = mux.Vars(c.Request)[name]
|
||||
return
|
||||
}
|
||||
|
||||
func (c *Context) Forwarded() (ret string) {
|
||||
ret = c.Request.Header.Get("X-Forwarded-For")
|
||||
if ret == "" {
|
||||
ret = empty
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// CheckXsrf validates the xsrf token
|
||||
func (c *Context) CheckXsrf() (ok bool) {
|
||||
ok = checkXsrf(c.Form("token"), c.Token.RawSig())
|
||||
if !ok {
|
||||
c.Error("wrong csrf token")
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (c *Context) LoggedOn() (ok bool) {
|
||||
_, ok = c.Token.Claims.Get(userClaim)
|
||||
return
|
||||
}
|
||||
|
||||
func (c *Context) LogSetCookie(msg string, err error) {
|
||||
log.Printf("ctx: %s %s\n", msg, err)
|
||||
c.SetCookie(nil, 0)
|
||||
}
|
||||
|
||||
func (c *Context) Login(u *core.User, remember string) {
|
||||
var d time.Duration
|
||||
if remember != "" {
|
||||
d = day * 7
|
||||
}
|
||||
c.SetCookie(jwt.Claims{userClaim: u.Username, userCreatedClaim: u.Created, adminClaim: u.Admin}, d)
|
||||
}
|
||||
|
||||
func (c *Context) SetCookie(claims jwt.Claims, d time.Duration) {
|
||||
c.setCookie(jwt.NewToken(claims, nil), d)
|
||||
}
|
||||
|
||||
func (c *Context) setCookie(t *jwt.Token, d time.Duration) {
|
||||
if d == 0 {
|
||||
d = jwt.DefaultExpiry
|
||||
}
|
||||
t.Claims[jwt.ExpClaim] = jwt.NewExp(d)
|
||||
if err := c.Srv.JWT.Sign(t); err != nil {
|
||||
log.Println(err)
|
||||
return
|
||||
}
|
||||
c.Token = *t
|
||||
cookie := &http.Cookie{
|
||||
Name: cookieName,
|
||||
Value: c.Token.String(),
|
||||
Path: "/",
|
||||
MaxAge: int(d.Seconds()),
|
||||
Secure: c.Srv.Config.SecureCookie,
|
||||
HttpOnly: true,
|
||||
SameSite: http.SameSiteStrictMode,
|
||||
}
|
||||
if cookie.MaxAge > 0 {
|
||||
cookie.Expires = time.Now().Add(d)
|
||||
} else if cookie.MaxAge < 0 {
|
||||
cookie.Expires = time.Unix(1, 0)
|
||||
}
|
||||
http.SetCookie(c.Response, cookie)
|
||||
}
|
||||
|
||||
func (c *Context) User() string {
|
||||
return c.Token.Claims.GetString(userClaim)
|
||||
}
|
||||
|
||||
func (c *Context) Totp() string {
|
||||
return c.Token.Claims.GetString(totpClaim)
|
||||
}
|
||||
|
||||
func (c *Context) Remember() string {
|
||||
return c.Token.Claims.GetString(rememberClaim)
|
||||
}
|
||||
|
||||
func (c *Context) Referer() string {
|
||||
return c.Token.Claims.GetString(refererClaim)
|
||||
}
|
||||
|
||||
func (c *Context) Admin() bool {
|
||||
return c.Token.Claims.GetBool(adminClaim)
|
||||
}
|
||||
|
||||
type ctxHandler func(*Context)
|
||||
|
|
@ -1,890 +0,0 @@
|
|||
// Copyright (C) 2021 Marius Schellenberger
|
||||
|
||||
package server
|
||||
|
||||
import (
|
||||
"git.giftfish.de/ston1th/gowiki/pkg/core"
|
||||
"git.giftfish.de/ston1th/gowiki/pkg/log"
|
||||
"git.giftfish.de/ston1th/gowiki/pkg/otp"
|
||||
"git.giftfish.de/ston1th/gowiki/pkg/render"
|
||||
"git.giftfish.de/ston1th/jwt/v3"
|
||||
"net/http"
|
||||
"time"
|
||||
)
|
||||
|
||||
type notFoundHandler struct {
|
||||
s *HTTPServer
|
||||
}
|
||||
|
||||
func (nf *notFoundHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
newContext(w, r, nf.s).NotFound()
|
||||
}
|
||||
|
||||
func jwtHandler(h ctxHandler) ctxHandler {
|
||||
return func(ctx *Context) {
|
||||
if c, err := ctx.Request.Cookie(cookieName); err == nil {
|
||||
t, err := jwt.DecodeToken(c.Value)
|
||||
if err != nil {
|
||||
ctx.LogSetCookie("DecodeToken:", err)
|
||||
ctx.Redirect(core.IndexURI, http.StatusFound)
|
||||
return
|
||||
}
|
||||
if err = ctx.Srv.JWT.Verify(t); err != nil {
|
||||
ctx.LogSetCookie("VerifyToken:", err)
|
||||
ctx.Redirect(core.IndexURI, http.StatusFound)
|
||||
return
|
||||
}
|
||||
if t.Claims.GetString(totpClaim) != "" {
|
||||
path := ctx.Path()
|
||||
if path == core.TotpURI || path == core.LoginURI || path == core.LogoutURI {
|
||||
ctx.Token = *t
|
||||
h(ctx)
|
||||
return
|
||||
}
|
||||
ctx.Redirect(core.TotpURI, http.StatusFound)
|
||||
return
|
||||
|
||||
}
|
||||
if !ctx.Srv.DB.LockedOut(t.Claims.GetString(userClaim), t.Claims.GetString(userCreatedClaim)) {
|
||||
ctx.Token = *t
|
||||
h(ctx)
|
||||
return
|
||||
}
|
||||
if err = ctx.Srv.JWT.Invalidate(t); err != nil {
|
||||
log.Println("Invalidate:", err)
|
||||
}
|
||||
}
|
||||
ctx.SetCookie(nil, 0)
|
||||
h(ctx)
|
||||
}
|
||||
}
|
||||
|
||||
func totpAuthHandler(h ctxHandler) ctxHandler {
|
||||
return func(ctx *Context) {
|
||||
if ctx.Totp() != "" {
|
||||
h(ctx)
|
||||
return
|
||||
}
|
||||
ctx.Redirect(core.IndexURI, http.StatusFound)
|
||||
}
|
||||
}
|
||||
|
||||
func adminAuthHandler(h ctxHandler) ctxHandler {
|
||||
return func(ctx *Context) {
|
||||
if ctx.Admin() {
|
||||
h(ctx)
|
||||
return
|
||||
}
|
||||
ctx.Forbidden()
|
||||
}
|
||||
}
|
||||
|
||||
func userAuthHandler(h ctxHandler) ctxHandler {
|
||||
return func(ctx *Context) {
|
||||
if ctx.Admin() {
|
||||
h(ctx)
|
||||
return
|
||||
}
|
||||
if ctx.User() == ctx.Var("user") {
|
||||
h(ctx)
|
||||
return
|
||||
}
|
||||
ctx.Forbidden()
|
||||
}
|
||||
}
|
||||
|
||||
func authHandler(h ctxHandler) ctxHandler {
|
||||
return func(ctx *Context) {
|
||||
if ctx.LoggedOn() {
|
||||
h(ctx)
|
||||
return
|
||||
}
|
||||
ctx.Forbidden()
|
||||
}
|
||||
}
|
||||
|
||||
func indexHandler(ctx *Context) {
|
||||
ctx.Redirect(core.IndexURI, http.StatusFound)
|
||||
}
|
||||
|
||||
func loginHandler(ctx *Context) {
|
||||
if ctx.LoggedOn() {
|
||||
ctx.Redirect(core.IndexURI, http.StatusFound)
|
||||
return
|
||||
}
|
||||
ctx.Template("loginHandler")
|
||||
ctx.Data = webData{
|
||||
Title: "Login",
|
||||
BodyTitle: "Login",
|
||||
}
|
||||
var data loginData
|
||||
switch ctx.Method() {
|
||||
case "GET":
|
||||
data.Referer = ctx.RefererURI()
|
||||
ctx.Data.Data = data
|
||||
ctx.Exec()
|
||||
case "POST":
|
||||
if !ctx.CheckXsrf() {
|
||||
return
|
||||
}
|
||||
data.User = ctx.Form("user")
|
||||
data.Referer = ctx.Form("referer")
|
||||
ctx.Data.Data = data
|
||||
if data.Referer == "" {
|
||||
data.Referer = core.IndexURI
|
||||
}
|
||||
password := ctx.Form("password")
|
||||
if data.User == "" || password == "" {
|
||||
ctx.Error("empty username or password")
|
||||
return
|
||||
}
|
||||
u, err := ctx.Srv.DB.Login(data.User, password)
|
||||
if err != nil {
|
||||
log.Printf("login: %s from: %s %s", data.User, ctx.Forwarded(), err)
|
||||
ctx.Error("bad username or password")
|
||||
return
|
||||
}
|
||||
if u.Secret == "" {
|
||||
log.Println("login:", u.Username)
|
||||
ctx.Login(u, ctx.Form("remember"))
|
||||
ctx.Redirect(data.Referer, http.StatusFound)
|
||||
return
|
||||
}
|
||||
ctx.SetCookie(jwt.Claims{
|
||||
totpClaim: u.Username,
|
||||
rememberClaim: ctx.Form("remember"),
|
||||
refererClaim: data.Referer,
|
||||
}, time.Minute)
|
||||
ctx.Redirect(core.TotpURI, http.StatusFound)
|
||||
}
|
||||
}
|
||||
|
||||
func loginTotpHandler(ctx *Context) {
|
||||
if ctx.LoggedOn() {
|
||||
ctx.Redirect(core.IndexURI, http.StatusFound)
|
||||
return
|
||||
}
|
||||
ctx.Template("loginTotpHandler")
|
||||
ctx.Data = webData{
|
||||
Title: "TOTP Verification",
|
||||
BodyTitle: "TOTP Veriftcation",
|
||||
}
|
||||
switch ctx.Method() {
|
||||
case "GET":
|
||||
ctx.Exec()
|
||||
case "POST":
|
||||
if !ctx.CheckXsrf() {
|
||||
return
|
||||
}
|
||||
pin := ctx.Form("pin")
|
||||
if pin == "" {
|
||||
ctx.Error("empty pin")
|
||||
return
|
||||
}
|
||||
user := ctx.Totp()
|
||||
u, err := ctx.Srv.DB.Totp(user, pin)
|
||||
if err != nil {
|
||||
log.Printf("totp: %s from: %s %s", user, ctx.Forwarded(), err)
|
||||
ctx.Error(err)
|
||||
return
|
||||
}
|
||||
ref := ctx.Referer()
|
||||
log.Println("login:", u.Username)
|
||||
ctx.Login(u, ctx.Remember())
|
||||
ctx.Redirect(ref, http.StatusFound)
|
||||
}
|
||||
}
|
||||
|
||||
func searchHandler(ctx *Context) {
|
||||
ctx.Template("searchHandler")
|
||||
ctx.Data = webData{
|
||||
Title: "Search",
|
||||
BodyTitle: "Search",
|
||||
}
|
||||
switch ctx.Method() {
|
||||
case "GET":
|
||||
ctx.Exec()
|
||||
case "POST":
|
||||
if !ctx.CheckXsrf() {
|
||||
return
|
||||
}
|
||||
search := ctx.Form("search")
|
||||
|
||||
res, err := ctx.Srv.DB.Index.Search(search)
|
||||
if err != nil {
|
||||
ctx.Error(err)
|
||||
return
|
||||
}
|
||||
user := ctx.User()
|
||||
r := make([]core.Result, len(res))
|
||||
i := 0
|
||||
for _, v := range res {
|
||||
if ctx.Srv.DB.ValidatePage(v.StoreTitle, user) {
|
||||
r[i] = v
|
||||
i++
|
||||
}
|
||||
}
|
||||
|
||||
ctx.Data.Data = r[:i]
|
||||
ctx.Data.Search = search
|
||||
ctx.Exec()
|
||||
}
|
||||
}
|
||||
|
||||
func sectionsHandler(ctx *Context) {
|
||||
ctx.Template("sectionsHandler")
|
||||
ctx.Data = webData{
|
||||
Title: "All Sections",
|
||||
BodyTitle: "All Sections",
|
||||
Admin: ctx.Admin(),
|
||||
}
|
||||
ctx.Data.Data = ctx.Srv.DB.GetAllSections(ctx.User(), ctx.Admin())
|
||||
ctx.Exec()
|
||||
}
|
||||
|
||||
func sectionHandler(ctx *Context) {
|
||||
ctx.Template("sectionHandler")
|
||||
section := ctx.Var("section")
|
||||
ctx.Data = webData{
|
||||
Title: section,
|
||||
BodyTitle: section,
|
||||
Admin: ctx.Admin(),
|
||||
}
|
||||
var err error
|
||||
ctx.Data.Data, err = ctx.Srv.DB.GetAllSectionPages(section, ctx.User(), ctx.Admin())
|
||||
if err != nil {
|
||||
ctx.NotFound()
|
||||
return
|
||||
}
|
||||
ctx.Exec()
|
||||
}
|
||||
|
||||
func sectionNewHandler(ctx *Context) {
|
||||
ctx.Template("sectionNewHandler")
|
||||
ctx.Data = webData{
|
||||
Title: "New Section",
|
||||
BodyTitle: "New Section",
|
||||
}
|
||||
users, err := ctx.Srv.DB.GetUsers()
|
||||
if err != nil {
|
||||
ctx.Error(err)
|
||||
return
|
||||
}
|
||||
sd := sectionData{Members: make(map[string]bool)}
|
||||
for _, u := range users {
|
||||
sd.Members[u.Username] = false
|
||||
}
|
||||
switch ctx.Method() {
|
||||
case "GET":
|
||||
ctx.Data.Data = sd
|
||||
ctx.Exec()
|
||||
case "POST":
|
||||
if !ctx.CheckXsrf() {
|
||||
return
|
||||
}
|
||||
section := ctx.Form("section")
|
||||
members := ctx.FormSlice("members")
|
||||
for _, m := range members {
|
||||
sd.Members[m] = true
|
||||
}
|
||||
ctx.Data.Data = sd
|
||||
if section == "" {
|
||||
ctx.Error("empty section")
|
||||
return
|
||||
}
|
||||
err = ctx.Srv.DB.CreateSection(section, members, false)
|
||||
if err != nil {
|
||||
ctx.Error(err)
|
||||
return
|
||||
}
|
||||
log.Printf("create: section %s created by %s\n", section, ctx.User())
|
||||
ctx.Redirect(core.SectionsURI, http.StatusFound)
|
||||
}
|
||||
}
|
||||
|
||||
func sectionEditHandler(ctx *Context) {
|
||||
ctx.Template("sectionEditHandler")
|
||||
ctx.Data = webData{
|
||||
Title: "Edit Section",
|
||||
BodyTitle: "Edit Section",
|
||||
}
|
||||
users, err := ctx.Srv.DB.GetUsers()
|
||||
if err != nil {
|
||||
ctx.Error(err)
|
||||
return
|
||||
}
|
||||
sd := sectionData{Members: make(map[string]bool)}
|
||||
for _, u := range users {
|
||||
sd.Members[u.Username] = false
|
||||
}
|
||||
s, err := ctx.Srv.DB.GetSection(ctx.Var("section"))
|
||||
if err != nil {
|
||||
ctx.Error(err)
|
||||
return
|
||||
}
|
||||
sd.Name = s.Name
|
||||
switch ctx.Method() {
|
||||
case "GET":
|
||||
for _, m := range s.Members {
|
||||
sd.Members[m] = true
|
||||
}
|
||||
ctx.Data.Data = sd
|
||||
ctx.Exec()
|
||||
case "POST":
|
||||
if !ctx.CheckXsrf() {
|
||||
return
|
||||
}
|
||||
members := ctx.FormSlice("members")
|
||||
for _, m := range members {
|
||||
sd.Members[m] = true
|
||||
}
|
||||
ctx.Data.Data = sd
|
||||
err = ctx.Srv.DB.UpdateSection(s.Name, members, s.User)
|
||||
if err != nil {
|
||||
ctx.Error(err)
|
||||
return
|
||||
}
|
||||
log.Printf("update: section %s updated by %s\n", s.Name, ctx.User())
|
||||
ctx.Redirect(core.SectionsURI, http.StatusFound)
|
||||
}
|
||||
}
|
||||
|
||||
func sectionDelHandler(ctx *Context) {
|
||||
ctx.Template("sectionDelHandler")
|
||||
ctx.Data = webData{
|
||||
Title: "Delete Section",
|
||||
BodyTitle: "Delete Section",
|
||||
}
|
||||
s, err := ctx.Srv.DB.GetSection(ctx.Var("section"))
|
||||
if err != nil {
|
||||
ctx.Error(err)
|
||||
return
|
||||
}
|
||||
if s.User {
|
||||
ctx.Error("user section can not be deleted")
|
||||
return
|
||||
}
|
||||
switch ctx.Method() {
|
||||
case "GET":
|
||||
ctx.Data.Data = s.Name
|
||||
ctx.Exec()
|
||||
case "POST":
|
||||
if !ctx.CheckXsrf() {
|
||||
return
|
||||
}
|
||||
err := ctx.Srv.DB.DeleteSection(s.Name)
|
||||
if err != nil {
|
||||
ctx.Error(err)
|
||||
return
|
||||
}
|
||||
log.Printf("delete: section %s deleted by %s\n", s.Name, ctx.User())
|
||||
ctx.Redirect(core.SectionsURI, http.StatusFound)
|
||||
}
|
||||
}
|
||||
|
||||
func pageNewHandler(ctx *Context) {
|
||||
ctx.Template("pageNewHandler")
|
||||
ctx.Data = webData{Title: "New Page"}
|
||||
user := ctx.User()
|
||||
secs, err := ctx.Srv.DB.GetUserSections(user)
|
||||
if err != nil {
|
||||
ctx.Error(err)
|
||||
return
|
||||
}
|
||||
switch ctx.Method() {
|
||||
case "GET":
|
||||
secs[user] = true
|
||||
ctx.Data.Data = pageData{core.Page{Perm: core.Public}, secs}
|
||||
ctx.Exec()
|
||||
case "POST":
|
||||
if !ctx.CheckXsrf() {
|
||||
return
|
||||
}
|
||||
section := ctx.Form("section")
|
||||
title := ctx.Form("title")
|
||||
markdown := ctx.Form("markdown")
|
||||
perm := core.ParsePermString(ctx.Form("perm"))
|
||||
p := core.Page{
|
||||
Title: title,
|
||||
Markdown: markdown,
|
||||
Perm: perm,
|
||||
}
|
||||
secs[section] = true
|
||||
ctx.Data.Data = pageData{p, secs}
|
||||
if title == "" {
|
||||
ctx.Error("empty title")
|
||||
return
|
||||
}
|
||||
if perm == core.Invalid {
|
||||
ctx.Error("invalid permission")
|
||||
return
|
||||
}
|
||||
|
||||
page, err := ctx.Srv.DB.CreatePage(title, section, markdown, user, perm)
|
||||
if err != nil {
|
||||
ctx.Error(err)
|
||||
return
|
||||
}
|
||||
ctx.Redirect(page.StoreTitle, http.StatusFound)
|
||||
}
|
||||
}
|
||||
|
||||
func pageHandler(ctx *Context) {
|
||||
var pageCreated string
|
||||
ctx.Data = webData{
|
||||
Admin: ctx.Admin(),
|
||||
}
|
||||
section := ctx.Var("section")
|
||||
title := ctx.Var("title")
|
||||
user := ctx.User()
|
||||
key := ctx.Var("key")
|
||||
if key != "" {
|
||||
t, err := jwt.DecodeToken(key)
|
||||
if err != nil {
|
||||
log.Println("share: DecodeToken:", err)
|
||||
ctx.NotFound()
|
||||
return
|
||||
}
|
||||
if err = ctx.Srv.JWT.Verify(t); err != nil {
|
||||
log.Println("share: VerifyToken:", err)
|
||||
ctx.NotFound()
|
||||
return
|
||||
}
|
||||
user = t.Claims.GetString(sharedClaim)
|
||||
if ctx.Srv.DB.LockedOut(user, t.Claims.GetString(userCreatedClaim)) {
|
||||
if err = ctx.Srv.JWT.Invalidate(t); err != nil {
|
||||
log.Println("share: Invalidate:", err)
|
||||
}
|
||||
ctx.NotFound()
|
||||
return
|
||||
}
|
||||
section = t.Claims.GetString(sectionClaim)
|
||||
title = t.Claims.GetString(titleClaim)
|
||||
pageCreated = t.Claims.GetString(pageCreatedClaim)
|
||||
ctx.Data.Key = key
|
||||
if ctx.T == nil {
|
||||
ctx.Template("pageViewHandler")
|
||||
}
|
||||
} else if ctx.T == nil {
|
||||
ctx.Template("pageHandler")
|
||||
}
|
||||
page, err := ctx.Srv.DB.GetPage(section, title, user)
|
||||
if err != nil {
|
||||
log.Println(err)
|
||||
ctx.NotFound()
|
||||
return
|
||||
}
|
||||
if key != "" && page.Created != pageCreated {
|
||||
ctx.NotFound()
|
||||
return
|
||||
}
|
||||
ctx.Data.Data = page
|
||||
ctx.Data.Title = section + " | " + page.Title
|
||||
ctx.Data.BodyTitle = section
|
||||
ctx.Exec()
|
||||
}
|
||||
|
||||
func pageMDHandler(f ctxHandler) ctxHandler {
|
||||
return func(ctx *Context) {
|
||||
ctx.Template("pageMDHandler")
|
||||
f(ctx)
|
||||
}
|
||||
}
|
||||
|
||||
func pageShareHandler(ctx *Context) {
|
||||
ctx.Template("pageShareHandler")
|
||||
ctx.Data = webData{}
|
||||
section := ctx.Var("section")
|
||||
title := ctx.Var("title")
|
||||
user := ctx.User()
|
||||
page, err := ctx.Srv.DB.GetPage(section, title, user)
|
||||
if err != nil {
|
||||
ctx.NotFound()
|
||||
return
|
||||
}
|
||||
if page.Perm != core.Private {
|
||||
ctx.Error("only private pages can be shared")
|
||||
return
|
||||
}
|
||||
ctx.Data.Data = page.StoreTitle
|
||||
ctx.Data.Title = page.Title
|
||||
ctx.Data.BodyTitle = page.Title
|
||||
switch ctx.Method() {
|
||||
case "GET":
|
||||
ctx.Exec()
|
||||
case "POST":
|
||||
if !ctx.CheckXsrf() {
|
||||
return
|
||||
}
|
||||
|
||||
d, err := duration(ctx.Form("duration"), ctx.Form("mode"))
|
||||
if err != nil {
|
||||
ctx.Error(err)
|
||||
return
|
||||
}
|
||||
t := jwt.NewToken(map[string]interface{}{
|
||||
sharedClaim: user,
|
||||
userCreatedClaim: ctx.Token.Claims.GetString(userCreatedClaim),
|
||||
pageCreatedClaim: page.Created,
|
||||
sectionClaim: section,
|
||||
titleClaim: title,
|
||||
jwt.ExpClaim: jwt.NewExp(d),
|
||||
}, nil)
|
||||
|
||||
if err = ctx.Srv.JWT.Sign(t); err != nil {
|
||||
log.Println("share: SignToken:", err)
|
||||
ctx.NotFound()
|
||||
return
|
||||
}
|
||||
ctx.Data.Key = t.String()
|
||||
ctx.Exec()
|
||||
}
|
||||
}
|
||||
|
||||
func pageBlacklistHandler(ctx *Context) {
|
||||
ctx.Template("pageBlacklistHandler")
|
||||
ctx.Data = webData{
|
||||
Title: "Blacklist",
|
||||
BodyTitle: "Blacklist",
|
||||
}
|
||||
switch ctx.Method() {
|
||||
case "GET":
|
||||
ctx.Exec()
|
||||
case "POST":
|
||||
if !ctx.CheckXsrf() {
|
||||
return
|
||||
}
|
||||
|
||||
t, err := jwt.DecodeToken(ctx.Form("share"))
|
||||
if err != nil {
|
||||
ctx.Error(err)
|
||||
return
|
||||
}
|
||||
err = ctx.Srv.JWT.Verify(t)
|
||||
if err != nil {
|
||||
ctx.Error(err)
|
||||
return
|
||||
}
|
||||
user := t.Claims.GetString(sharedClaim)
|
||||
if user == "" {
|
||||
ctx.Error("blacklisting denied: no share token")
|
||||
return
|
||||
}
|
||||
if ctx.User() != user {
|
||||
ctx.Error("blacklisting denied: token was issued by " + user)
|
||||
return
|
||||
}
|
||||
err = ctx.Srv.JWT.Invalidate(t)
|
||||
if err != nil {
|
||||
ctx.Error(err)
|
||||
return
|
||||
}
|
||||
ctx.Exec()
|
||||
}
|
||||
}
|
||||
|
||||
func pageEditHandler(ctx *Context) {
|
||||
ctx.Template("pageEditHandler")
|
||||
ctx.Data = webData{}
|
||||
section := ctx.Var("section")
|
||||
title := ctx.Var("title")
|
||||
page, err := ctx.Srv.DB.GetPage(section, title, ctx.User())
|
||||
if err != nil {
|
||||
ctx.NotFound()
|
||||
return
|
||||
}
|
||||
ctx.Data.Title = page.Title
|
||||
ctx.Data.BodyTitle = page.Title
|
||||
switch ctx.Method() {
|
||||
case "GET":
|
||||
ctx.Data.Data = page
|
||||
ctx.Exec()
|
||||
case "POST":
|
||||
if !ctx.CheckXsrf() {
|
||||
return
|
||||
}
|
||||
page.Markdown = ctx.Form("markdown")
|
||||
page.Perm = core.ParsePermString(ctx.Form("perm"))
|
||||
ctx.Data.Data = page
|
||||
|
||||
err = ctx.Srv.DB.UpdatePage(title, section, page.Markdown, ctx.User(), page.Perm)
|
||||
if err != nil {
|
||||
ctx.Error(err)
|
||||
return
|
||||
}
|
||||
ctx.Redirect(page.StoreTitle, http.StatusFound)
|
||||
}
|
||||
}
|
||||
|
||||
func pageDelHandler(ctx *Context) {
|
||||
section := ctx.Var("section")
|
||||
title := ctx.Var("title")
|
||||
ctx.Template("pageDelHandler")
|
||||
ctx.Data.Data = render.StoreTitle(section, title)
|
||||
ctx.Data.Title = title
|
||||
ctx.Data.BodyTitle = title
|
||||
switch ctx.Method() {
|
||||
case "GET":
|
||||
ctx.Exec()
|
||||
case "POST":
|
||||
if !ctx.CheckXsrf() {
|
||||
return
|
||||
}
|
||||
if err := ctx.Srv.DB.DeletePage(title, section, ctx.User()); err != nil {
|
||||
ctx.Error(err)
|
||||
return
|
||||
}
|
||||
ctx.Redirect(core.IndexURI, http.StatusFound)
|
||||
}
|
||||
}
|
||||
|
||||
func usersHandler(ctx *Context) {
|
||||
if !ctx.LoggedOn() {
|
||||
ctx.NotFound()
|
||||
return
|
||||
}
|
||||
ctx.Template("usersHandler")
|
||||
ctx.Data = webData{
|
||||
Title: "Users",
|
||||
BodyTitle: "Users",
|
||||
User: ctx.User(),
|
||||
}
|
||||
us, err := ctx.Srv.DB.GetUsers()
|
||||
if err != nil {
|
||||
ctx.Data.Msg = err.Error()
|
||||
}
|
||||
ctx.Data.Data = us
|
||||
ctx.Exec()
|
||||
}
|
||||
|
||||
func userNewHandler(ctx *Context) {
|
||||
ctx.Template("userNewHandler")
|
||||
ctx.Data = webData{
|
||||
Title: "New User",
|
||||
BodyTitle: "New User",
|
||||
}
|
||||
switch ctx.Method() {
|
||||
case "GET":
|
||||
ctx.Exec()
|
||||
case "POST":
|
||||
if !ctx.CheckXsrf() {
|
||||
return
|
||||
}
|
||||
user := ctx.Form("user")
|
||||
password := ctx.Form("password")
|
||||
repeat := ctx.Form("repeat")
|
||||
admin := ctx.Form("admin")
|
||||
adm := false
|
||||
if admin == "0" {
|
||||
adm = true
|
||||
}
|
||||
|
||||
if user == "" || password == "" {
|
||||
ctx.Error("empty user or password")
|
||||
return
|
||||
}
|
||||
if password != repeat {
|
||||
ctx.Error("passwords do not match")
|
||||
return
|
||||
}
|
||||
if err := ctx.Srv.DB.CreateUser(user, password, adm); err != nil {
|
||||
ctx.Error(err)
|
||||
return
|
||||
}
|
||||
log.Printf("create: user %s created by %s\n", user, ctx.User())
|
||||
ctx.Redirect(core.UsersURI, http.StatusFound)
|
||||
}
|
||||
}
|
||||
|
||||
func userEditHandler(ctx *Context) {
|
||||
ctx.Template("userEditHandler")
|
||||
ctx.Data = webData{
|
||||
Title: "Edit User",
|
||||
BodyTitle: "Edit User",
|
||||
}
|
||||
u, err := ctx.Srv.DB.GetUserWithoutPassword(ctx.Var("user"))
|
||||
if err != nil {
|
||||
ctx.Error(err)
|
||||
return
|
||||
}
|
||||
ctx.Data.Data = u
|
||||
switch ctx.Method() {
|
||||
case "GET":
|
||||
ctx.Exec()
|
||||
case "POST":
|
||||
if !ctx.CheckXsrf() {
|
||||
return
|
||||
}
|
||||
user := ctx.Var("user")
|
||||
password := ctx.Form("password")
|
||||
repeat := ctx.Form("repeat")
|
||||
admin := ctx.Form("admin")
|
||||
adm := false
|
||||
if admin == "0" {
|
||||
adm = true
|
||||
}
|
||||
|
||||
if password != repeat {
|
||||
ctx.Error("passwords do not match")
|
||||
return
|
||||
}
|
||||
if ctx.Admin() {
|
||||
if err := ctx.Srv.DB.AdminUpdateUser(user, password, adm); err != nil {
|
||||
ctx.Error(err)
|
||||
return
|
||||
}
|
||||
log.Printf("update: user %s updated by %s\n", user, ctx.User())
|
||||
ctx.Redirect(core.UsersURI, http.StatusFound)
|
||||
return
|
||||
}
|
||||
if err := ctx.Srv.DB.UpdateUserPassword(user, password); err != nil {
|
||||
ctx.Error(err)
|
||||
return
|
||||
}
|
||||
log.Printf("update: %s updated by %s\n", user, ctx.User())
|
||||
ctx.Redirect(core.IndexURI, http.StatusFound)
|
||||
}
|
||||
}
|
||||
|
||||
func userTotpHandler(ctx *Context) {
|
||||
ctx.Template("userTotpHandler")
|
||||
ctx.Data = webData{
|
||||
Title: "TOTP",
|
||||
BodyTitle: "TOTP",
|
||||
}
|
||||
req := otp.Request{Username: ctx.Var("user")}
|
||||
u, err := ctx.Srv.DB.GetUser(req.Username)
|
||||
if err != nil {
|
||||
ctx.Error(err)
|
||||
return
|
||||
}
|
||||
if u.Secret == "" {
|
||||
switch ctx.Method() {
|
||||
case "GET":
|
||||
req, err = otp.New(req.Username)
|
||||
if err != nil {
|
||||
ctx.Error(err)
|
||||
return
|
||||
}
|
||||
case "POST":
|
||||
req.URL = ctx.Form("url")
|
||||
req.Image, req.Secret, err = otp.ImageSecretFromURL(req.URL)
|
||||
if err != nil {
|
||||
ctx.Error(err)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
switch ctx.Method() {
|
||||
case "GET":
|
||||
ctx.Data.Data = req
|
||||
ctx.Exec()
|
||||
case "POST":
|
||||
if !ctx.CheckXsrf() {
|
||||
return
|
||||
}
|
||||
pin := ctx.Form("pin")
|
||||
if pin == "" {
|
||||
ctx.Error("empty pin")
|
||||
return
|
||||
}
|
||||
ctx.Data.Data = req
|
||||
secret := req.Secret
|
||||
if u.Secret != "" {
|
||||
secret = u.Secret
|
||||
}
|
||||
valid := otp.Validate(pin, secret)
|
||||
user := ctx.User()
|
||||
if user != req.Username && ctx.Admin() {
|
||||
valid = true
|
||||
}
|
||||
|
||||
if !valid {
|
||||
ctx.Error("validation failed")
|
||||
return
|
||||
}
|
||||
if err := ctx.Srv.DB.UpdateUserSecret(req.Username, req.Secret); err != nil {
|
||||
ctx.Error(err)
|
||||
return
|
||||
}
|
||||
if req.Secret == "" {
|
||||
log.Printf("totp: disabled by %s for %s\n", user, req.Username)
|
||||
} else {
|
||||
log.Printf("totp: enabled by %s for %s\n", user, req.Username)
|
||||
}
|
||||
ctx.Redirect(core.UserURI+"/edit/"+req.Username, http.StatusFound)
|
||||
}
|
||||
}
|
||||
|
||||
func userUnlockHandler(ctx *Context) {
|
||||
user := ctx.Var("user")
|
||||
if err := ctx.Srv.DB.UnlockUser(user); err != nil {
|
||||
log.Println("unlock:", err)
|
||||
}
|
||||
log.Printf("unlock: %s unlocked by %s\n", user, ctx.User())
|
||||
ctx.Redirect(core.UsersURI, http.StatusFound)
|
||||
}
|
||||
|
||||
func userDelHandler(ctx *Context) {
|
||||
ctx.Template("userDelHandler")
|
||||
ctx.Data = webData{
|
||||
Title: "Delete User",
|
||||
BodyTitle: "Delete User",
|
||||
}
|
||||
switch ctx.Method() {
|
||||
case "GET":
|
||||
user, err := ctx.Srv.DB.GetUserWithoutPassword(ctx.Var("user"))
|
||||
if err != nil {
|
||||
ctx.Error(err)
|
||||
return
|
||||
}
|
||||
ctx.Data.Data = user
|
||||
ctx.Exec()
|
||||
case "POST":
|
||||
if !ctx.CheckXsrf() {
|
||||
return
|
||||
}
|
||||
self := ctx.User()
|
||||
user := ctx.Var("user")
|
||||
if err := ctx.Srv.DB.DeleteUser(user); err != nil {
|
||||
log.Println("delete:", err)
|
||||
}
|
||||
log.Printf("delete: user %s deleted by %s\n", user, self)
|
||||
if user == self {
|
||||
ctx.Redirect(core.LogoutURI, http.StatusFound)
|
||||
return
|
||||
}
|
||||
ctx.Redirect(core.UsersURI, http.StatusFound)
|
||||
}
|
||||
}
|
||||
|
||||
func statsHandler(ctx *Context) {
|
||||
ctx.Template("statsHandler")
|
||||
ctx.Data = webData{
|
||||
Title: "Stats",
|
||||
BodyTitle: "Stats",
|
||||
}
|
||||
if ctx.Method() == "GET" {
|
||||
ctx.Data.Data = getStats(ctx.Srv.DB.Index)
|
||||
ctx.Exec()
|
||||
}
|
||||
}
|
||||
|
||||
func logoutHandler(ctx *Context) {
|
||||
var totp string
|
||||
user := ctx.User()
|
||||
if user == "" {
|
||||
user = ctx.Totp()
|
||||
if user != "" {
|
||||
totp = " (totp)"
|
||||
}
|
||||
}
|
||||
if user != "" {
|
||||
ctx.Srv.JWT.Invalidate(&ctx.Token)
|
||||
} else {
|
||||
user = empty
|
||||
}
|
||||
log.Printf("logout: %s%s", user, totp)
|
||||
ctx.SetCookie(nil, 0)
|
||||
ctx.Redirect(core.IndexURI, http.StatusFound)
|
||||
}
|
||||
|
|
@ -1,66 +0,0 @@
|
|||
// Copyright (C) 2021 Marius Schellenberger
|
||||
|
||||
package server
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"git.giftfish.de/ston1th/gowiki/pkg/core"
|
||||
bleve "git.giftfish.de/ston1th/gowiki/pkg/index"
|
||||
"runtime"
|
||||
"strconv"
|
||||
"time"
|
||||
)
|
||||
|
||||
const (
|
||||
day = time.Hour * 24
|
||||
min = time.Second * 30
|
||||
max = day * 90
|
||||
|
||||
div = 1048576
|
||||
)
|
||||
|
||||
func duration(duration, mode string) (d time.Duration, err error) {
|
||||
switch mode {
|
||||
case "1":
|
||||
d, err = time.ParseDuration(duration)
|
||||
if err != nil {
|
||||
err = errors.New("could not parse duration")
|
||||
return
|
||||
}
|
||||
case "2":
|
||||
var t int
|
||||
t, err = strconv.Atoi(duration)
|
||||
if err != nil {
|
||||
err = errors.New("could not parse duration")
|
||||
return
|
||||
}
|
||||
d = time.Duration(int(day) * t)
|
||||
default:
|
||||
err = errors.New("invalid mode")
|
||||
return
|
||||
}
|
||||
|
||||
if d < min {
|
||||
err = errors.New("minimum duration is 30 seconds")
|
||||
}
|
||||
if d > max {
|
||||
err = errors.New("maximum duration is 90 days")
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func fmtMem(i uint64) string {
|
||||
return strconv.FormatFloat(float64(i)/div, 'f', 2, 64)
|
||||
}
|
||||
|
||||
func getStats(i *bleve.Index) *core.Stats {
|
||||
mem := new(runtime.MemStats)
|
||||
runtime.ReadMemStats(mem)
|
||||
return &core.Stats{
|
||||
Goroutines: runtime.NumGoroutine(),
|
||||
Alloc: fmtMem(mem.Alloc),
|
||||
Sys: fmtMem(mem.Sys),
|
||||
Docs: i.DocCount(),
|
||||
GoVersion: runtime.Version(),
|
||||
}
|
||||
}
|
||||
|
|
@ -1,187 +0,0 @@
|
|||
// Copyright (C) 2021 Marius Schellenberger
|
||||
|
||||
package server
|
||||
|
||||
import "git.giftfish.de/ston1th/gowiki/pkg/core"
|
||||
|
||||
type route struct {
|
||||
Path string
|
||||
Handler ctxHandler
|
||||
Methods []string
|
||||
}
|
||||
|
||||
var routes = []route{
|
||||
{
|
||||
core.RootURI,
|
||||
jwtHandler(
|
||||
indexHandler),
|
||||
[]string{"GET"},
|
||||
},
|
||||
{
|
||||
core.SectionsURI,
|
||||
jwtHandler(
|
||||
sectionsHandler),
|
||||
[]string{"GET"},
|
||||
},
|
||||
{
|
||||
core.SectionURI + "/new",
|
||||
jwtHandler(
|
||||
adminAuthHandler(
|
||||
sectionNewHandler)),
|
||||
[]string{"GET", "POST"},
|
||||
},
|
||||
{
|
||||
core.SectionURI + "/edit/{section:[a-zA-Z0-9]+$}",
|
||||
jwtHandler(
|
||||
adminAuthHandler(
|
||||
sectionEditHandler)),
|
||||
[]string{"GET", "POST"},
|
||||
},
|
||||
{
|
||||
core.SectionURI + "/del/{section:[a-zA-Z0-9]+$}",
|
||||
jwtHandler(
|
||||
adminAuthHandler(
|
||||
sectionDelHandler)),
|
||||
[]string{"GET", "POST"},
|
||||
},
|
||||
{
|
||||
core.SearchURI,
|
||||
jwtHandler(
|
||||
searchHandler),
|
||||
[]string{"GET", "POST"},
|
||||
},
|
||||
{
|
||||
core.NewURI,
|
||||
jwtHandler(
|
||||
authHandler(
|
||||
pageNewHandler)),
|
||||
[]string{"GET", "POST"},
|
||||
},
|
||||
{
|
||||
core.LoginURI,
|
||||
jwtHandler(
|
||||
loginHandler),
|
||||
[]string{"GET", "POST"},
|
||||
},
|
||||
{
|
||||
core.TotpURI,
|
||||
jwtHandler(
|
||||
totpAuthHandler(
|
||||
loginTotpHandler)),
|
||||
[]string{"GET", "POST"},
|
||||
},
|
||||
{
|
||||
core.BlacklistURI,
|
||||
jwtHandler(
|
||||
authHandler(
|
||||
pageBlacklistHandler)),
|
||||
[]string{"GET", "POST"},
|
||||
},
|
||||
{
|
||||
core.UsersURI,
|
||||
jwtHandler(
|
||||
adminAuthHandler(
|
||||
usersHandler)),
|
||||
[]string{"GET"},
|
||||
},
|
||||
{
|
||||
core.UserURI + "/new",
|
||||
jwtHandler(
|
||||
adminAuthHandler(
|
||||
userNewHandler)),
|
||||
[]string{"GET", "POST"},
|
||||
},
|
||||
{
|
||||
core.UserURI + "/edit/{user:[a-zA-Z0-9]+$}",
|
||||
jwtHandler(
|
||||
userAuthHandler(
|
||||
userEditHandler)),
|
||||
[]string{"GET", "POST"},
|
||||
},
|
||||
{
|
||||
core.UserURI + "/del/{user:[a-zA-Z0-9]+$}",
|
||||
jwtHandler(
|
||||
userAuthHandler(
|
||||
userDelHandler)),
|
||||
[]string{"GET", "POST"},
|
||||
},
|
||||
{
|
||||
core.UserURI + "/totp/{user:[a-zA-Z0-9]+$}",
|
||||
jwtHandler(
|
||||
userAuthHandler(
|
||||
userTotpHandler)),
|
||||
[]string{"GET", "POST"},
|
||||
},
|
||||
{
|
||||
core.UserURI + "/unlock/{user:[a-zA-Z0-9]+$}",
|
||||
jwtHandler(
|
||||
adminAuthHandler(
|
||||
userUnlockHandler)),
|
||||
[]string{"POST"},
|
||||
},
|
||||
{
|
||||
core.StatsURI,
|
||||
jwtHandler(
|
||||
adminAuthHandler(
|
||||
statsHandler)),
|
||||
[]string{"GET"},
|
||||
},
|
||||
{
|
||||
core.LogoutURI,
|
||||
jwtHandler(
|
||||
logoutHandler),
|
||||
[]string{"GET", "POST"},
|
||||
},
|
||||
{
|
||||
core.ViewURI + "/{key}",
|
||||
jwtHandler(
|
||||
pageHandler),
|
||||
[]string{"GET"},
|
||||
},
|
||||
{
|
||||
core.ViewURI + "/{key}/md",
|
||||
jwtHandler(
|
||||
pageMDHandler(
|
||||
pageHandler)),
|
||||
[]string{"GET"},
|
||||
},
|
||||
{
|
||||
"/{section:[a-zA-Z0-9]+}",
|
||||
jwtHandler(
|
||||
sectionHandler),
|
||||
[]string{"GET"},
|
||||
},
|
||||
{
|
||||
"/{section:[a-zA-Z0-9]+}/{title:[a-zA-Z0-9+-]+}",
|
||||
jwtHandler(
|
||||
pageHandler),
|
||||
[]string{"GET"},
|
||||
},
|
||||
{
|
||||
"/{section:[a-zA-Z0-9]+}/{title:[a-zA-Z0-9+-]+}/md",
|
||||
jwtHandler(
|
||||
pageMDHandler(
|
||||
pageHandler)),
|
||||
[]string{"GET"},
|
||||
},
|
||||
{
|
||||
"/{section:[a-zA-Z0-9]+}/{title:[a-zA-Z0-9+-]+}/share",
|
||||
jwtHandler(
|
||||
pageShareHandler),
|
||||
[]string{"GET", "POST"},
|
||||
},
|
||||
{
|
||||
"/{section:[a-zA-Z0-9]+}/{title:[a-zA-Z0-9+-]+}/edit",
|
||||
jwtHandler(
|
||||
authHandler(
|
||||
pageEditHandler)),
|
||||
[]string{"GET", "POST"},
|
||||
},
|
||||
{
|
||||
"/{section:[a-zA-Z0-9]+}/{title:[a-zA-Z0-9+-]+}/del",
|
||||
jwtHandler(
|
||||
authHandler(
|
||||
pageDelHandler)),
|
||||
[]string{"GET", "POST"},
|
||||
},
|
||||
}
|
||||
|
|
@ -1,99 +0,0 @@
|
|||
// Copyright (C) 2021 Marius Schellenberger
|
||||
|
||||
package server
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"embed"
|
||||
"errors"
|
||||
"git.giftfish.de/ston1th/godrop/v2"
|
||||
"git.giftfish.de/ston1th/gowiki/pkg/core"
|
||||
"git.giftfish.de/ston1th/gowiki/pkg/db"
|
||||
"git.giftfish.de/ston1th/gowiki/pkg/log"
|
||||
"git.giftfish.de/ston1th/jwt/v3"
|
||||
"github.com/gorilla/mux"
|
||||
"html/template"
|
||||
"net"
|
||||
"net/http"
|
||||
"time"
|
||||
)
|
||||
|
||||
const (
|
||||
cookieName = "gowiki_session"
|
||||
)
|
||||
|
||||
type HTTPServer struct {
|
||||
Config *core.Config
|
||||
|
||||
listener net.Listener
|
||||
srv *http.Server
|
||||
|
||||
DB *db.DB
|
||||
JWT *jwt.JWT
|
||||
|
||||
templ map[string]*template.Template
|
||||
res map[string][]byte
|
||||
}
|
||||
|
||||
func NewHTTPServer(cfg *core.Config, l net.Listener) (srv *HTTPServer) {
|
||||
srv = &HTTPServer{
|
||||
Config: cfg,
|
||||
listener: l,
|
||||
}
|
||||
srv.loadTemplates()
|
||||
srv.srv = &http.Server{
|
||||
Handler: srv.buildRoutes(),
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (s *HTTPServer) Start() (err error) {
|
||||
s.DB, err = db.New(s.Config)
|
||||
if err != nil {
|
||||
return errors.New("db: " + err.Error())
|
||||
}
|
||||
err = godrop.UnveilBlock()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
opts := []jwt.JWTOption{jwt.WithBlacklist(s.DB), jwt.WithNonce()}
|
||||
if s.Config.Secret != "" {
|
||||
opts = append(opts, jwt.WithSecret(bytes.NewBufferString(s.Config.Secret)))
|
||||
}
|
||||
s.JWT, err = jwt.New(opts...)
|
||||
if err != nil {
|
||||
return errors.New("server: " + err.Error())
|
||||
}
|
||||
go func() {
|
||||
log.Println(s.srv.Serve(s.listener))
|
||||
}()
|
||||
return
|
||||
}
|
||||
|
||||
func (s *HTTPServer) Stop() error {
|
||||
s.DB.Close()
|
||||
s.JWT.Stop()
|
||||
ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)
|
||||
defer cancel()
|
||||
return s.srv.Shutdown(ctx)
|
||||
}
|
||||
|
||||
//go:embed static/*
|
||||
var static embed.FS
|
||||
|
||||
func (s *HTTPServer) buildRoutes() http.Handler {
|
||||
m := mux.NewRouter()
|
||||
m.NotFoundHandler = ¬FoundHandler{s}
|
||||
m.Handle("/static/{file}", http.StripPrefix("/", http.FileServer(http.FS(static))))
|
||||
for _, v := range routes {
|
||||
m.HandleFunc(v.Path, s.contextWrapper(v.Handler)).Methods(v.Methods...)
|
||||
}
|
||||
return m
|
||||
}
|
||||
|
||||
func (s *HTTPServer) contextWrapper(h ctxHandler) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
h(newContext(w, r, s))
|
||||
}
|
||||
}
|
||||
12
pkg/server/static/bootstrap.css
vendored
12
pkg/server/static/bootstrap.css
vendored
File diff suppressed because one or more lines are too long
Binary file not shown.
|
Before Width: | Height: | Size: 5.3 KiB |
|
|
@ -1,67 +0,0 @@
|
|||
// Copyright (C) 2021 Marius Schellenberger
|
||||
|
||||
package server
|
||||
|
||||
import (
|
||||
"embed"
|
||||
"errors"
|
||||
"git.giftfish.de/ston1th/gowiki/pkg/log"
|
||||
"html/template"
|
||||
"io/fs"
|
||||
)
|
||||
|
||||
//go:embed templates/*
|
||||
var templates embed.FS
|
||||
|
||||
func (s *HTTPServer) loadTemplates() {
|
||||
var fatal bool
|
||||
s.templ = make(map[string]*template.Template)
|
||||
s.res = make(map[string][]byte)
|
||||
tfs, err := fs.Sub(templates, "templates")
|
||||
if err != nil {
|
||||
log.Fatal("parse: ", err)
|
||||
}
|
||||
parse := func(html ...string) (temp *template.Template) {
|
||||
var err error
|
||||
temp, err = template.ParseFS(tfs, html...)
|
||||
if err != nil {
|
||||
log.Println("parse:", err)
|
||||
fatal = true
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// pages
|
||||
s.templ["notFoundHandler"] = parse("index.html", "menu.html", "notFound.html")
|
||||
s.templ["forbiddenHandler"] = parse("index.html", "menu.html", "forbidden.html")
|
||||
s.templ["loginHandler"] = parse("index.html", "menu.html", "login.html")
|
||||
s.templ["loginTotpHandler"] = parse("index.html", "menu.html", "loginTotp.html")
|
||||
s.templ["searchHandler"] = parse("index.html", "menu.html", "search.html")
|
||||
s.templ["pageHandler"] = parse("index.html", "menu.html", "page.html")
|
||||
s.templ["pageNewHandler"] = parse("index.html", "menu.html", "pageNew.html")
|
||||
s.templ["pageEditHandler"] = parse("index.html", "menu.html", "pageEdit.html")
|
||||
s.templ["pageViewHandler"] = parse("index.html", "menu.html", "pageView.html")
|
||||
s.templ["pageMDHandler"] = parse("index.html", "menu.html", "pageMD.html")
|
||||
s.templ["pageShareHandler"] = parse("index.html", "menu.html", "pageShare.html")
|
||||
s.templ["pageBlacklistHandler"] = parse("index.html", "menu.html", "pageBlacklist.html")
|
||||
s.templ["pageDelHandler"] = parse("index.html", "menu.html", "pageDel.html")
|
||||
// sections
|
||||
s.templ["sectionHandler"] = parse("index.html", "menu.html", "section.html")
|
||||
s.templ["sectionsHandler"] = parse("index.html", "menu.html", "sections.html")
|
||||
s.templ["sectionNewHandler"] = parse("index.html", "menu.html", "sectionNew.html")
|
||||
s.templ["sectionEditHandler"] = parse("index.html", "menu.html", "sectionEdit.html")
|
||||
s.templ["sectionDelHandler"] = parse("index.html", "menu.html", "sectionDel.html")
|
||||
// users
|
||||
s.templ["usersHandler"] = parse("index.html", "menu.html", "users.html")
|
||||
s.templ["userNewHandler"] = parse("index.html", "menu.html", "userNew.html")
|
||||
s.templ["userEditHandler"] = parse("index.html", "menu.html", "userEdit.html")
|
||||
s.templ["userDelHandler"] = parse("index.html", "menu.html", "userDel.html")
|
||||
// totp
|
||||
s.templ["userTotpHandler"] = parse("index.html", "menu.html", "userTotp.html")
|
||||
// stats
|
||||
s.templ["statsHandler"] = parse("index.html", "menu.html", "stats.html")
|
||||
|
||||
if fatal {
|
||||
log.Fatal("parse: ", errors.New("parsing templates failed"))
|
||||
}
|
||||
}
|
||||
|
|
@ -1,17 +0,0 @@
|
|||
{{define "body"}}
|
||||
<div class="page-header">
|
||||
<div class="row">
|
||||
<div class="col-xs-4 offset-4">
|
||||
<div class="card border-danger mx-auto">
|
||||
<div class="card-header">
|
||||
<strong>403 - Forbidden</strong>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<h5>Access to this page is restricted</h5>
|
||||
<a href="/" class="btn btn-sm btn-primary">Back</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{{end}}
|
||||
|
|
@ -1,46 +0,0 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<title>GoWiki | {{.Title}}</title>
|
||||
<link rel="shortcut icon" type="image/x-icon" href="/static/favicon.ico" integrity="sha256-bgotk/IPq4Yvt6s8AQGPTM5ZjPjQ6rrBRa2EyxpnFSc="></link>
|
||||
<link rel="stylesheet" type="text/css" href="/static/bootstrap.css" media="screen" integrity="sha256-3YeveuySvBgp2GItUS8eV1R/9T1zcPMyvw22SyVf8qo="></link>
|
||||
<link rel="stylesheet" type="text/css" href="/static/custom.css" media="screen" integrity="sha256-IwnADqysqqVpTpQ4sbG3Bz7v8FM4lqMiozQPozsFBUQ="></link>
|
||||
<meta name="theme-color" content="#375a7f">
|
||||
<meta name="description" content="{{.Title}}">
|
||||
</head>
|
||||
<body id="top">
|
||||
<div class="navbar navbar-expand fixed-top navbar-dark bg-primary">
|
||||
<div class="container">
|
||||
<a href="/" class="navbar-brand">GoWiki</a>
|
||||
<div class="collapse navbar-collapse">
|
||||
{{template "menu" .}}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="container">
|
||||
<div class="proper-content">
|
||||
{{if .Msg}}
|
||||
<div class="alert alert-danger msg">
|
||||
<h4>Error!</h4>
|
||||
<p>{{.Msg}}</p>
|
||||
</div>
|
||||
{{end}}
|
||||
{{template "body" .}}
|
||||
</div>
|
||||
<div class="push"></div>
|
||||
<footer>
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
<ul class="list-unstyled">
|
||||
<li class="float-md-right"><a href="#top">Back to top</a></li>
|
||||
{{if .Login}}
|
||||
<li><a href="/blacklist">Blacklist</a></li>
|
||||
{{end}}
|
||||
</ul>
|
||||
<p>© GoWiki {{.Version}} Request: <strong>{{.Time}}</strong></p>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -1,34 +0,0 @@
|
|||
{{define "body"}}
|
||||
<div class="page-header">
|
||||
<div class="row">
|
||||
<div class="col-xs-4 offset-4">
|
||||
<div class="card border-primary mx-auto">
|
||||
<div class="card-header">
|
||||
<strong>{{.BodyTitle}}</strong>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<form class="form-horizontal" action="/login" method="post">
|
||||
<input type="hidden" name="token" value="{{.Token}}">
|
||||
<input type="hidden" name="referer" value="{{.Data.Referer}}">
|
||||
<div class="form-group">
|
||||
<label class="col-form-label" for="user">Username</label>
|
||||
<input class="form-control input-sm" type="text" id="user" name="user" value="{{.Data.User}}" autofocus required>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="col-form-label" for="password">Password</label>
|
||||
<input class="form-control input-sm" type="password" id="password" name="password" required>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<div class="custom-control custom-checkbox">
|
||||
<input type="checkbox" class="custom-control-input" id="remember" name="remember">
|
||||
<label class="custom-control-label" for="remember">Remember Me</label>
|
||||
</div>
|
||||
</div>
|
||||
<button class="btn btn-sm btn-primary" type="submit">Login</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{{end}}
|
||||
|
|
@ -1,24 +0,0 @@
|
|||
{{define "body"}}
|
||||
<div class="page-header">
|
||||
<div class="row">
|
||||
<div class="col-xs-4 offset-4">
|
||||
<div class="card border-primary mx-auto">
|
||||
<div class="card-header">
|
||||
<strong>{{.BodyTitle}}</strong>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<form class="form-horizontal" action="/totp" method="post">
|
||||
<input type="hidden" name="token" value="{{.Token}}">
|
||||
<div class="form-group">
|
||||
<label class="col-form-label" for="pin">PIN</label>
|
||||
<input class="form-control input-sm" type="text" id="pin" name="pin" autocomplete="off" autofocus required>
|
||||
</div>
|
||||
<button class="btn btn-sm btn-primary" type="submit">Verify</button>
|
||||
<a href="/logout" class="btn btn-sm btn-primary">Cancel</a>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{{end}}
|
||||
|
|
@ -1,27 +0,0 @@
|
|||
{{define "menu"}}
|
||||
<ul class="navbar-nav">
|
||||
<li class="nav-item"><a class="nav-link" href="/wiki/Index">Index</a></li>
|
||||
<li class="nav-item"><a class="nav-link" href="/sections">Sections</a></li>
|
||||
{{if .Login}}
|
||||
<li class="nav-item"><a class="nav-link" href="/new">New Page</a></li>
|
||||
{{end}}
|
||||
</ul>
|
||||
<form class="form-inline" method="post" action="/search">
|
||||
<input type="hidden" name="token" value="{{.Token}}">
|
||||
<input type="text" class="form-control mr-sm-2 menu" placeholder="Search" name="search" value="{{.Search}}" autocomplete="off">
|
||||
<button class="btn btn-sm btn-info" type="submit">Search</button>
|
||||
</form>
|
||||
<ul class="nav navbar-nav ml-auto">
|
||||
{{if .Login}}
|
||||
{{if .Admin}}
|
||||
<li class="nav-item"><a class="nav-link" href="/users">Users</a></li>
|
||||
<li class="nav-item"><a class="nav-link" href="/stats">Stats</a></li>
|
||||
{{else}}
|
||||
<li class="nav-item"><a class="nav-link" href="/user/edit/{{.User}}">Profile</a></li>
|
||||
{{end}}
|
||||
<li class="nav-item"><a class="nav-link" href="/logout">Logout</a></li>
|
||||
{{else}}
|
||||
<li class="nav-item"><a class="nav-link" href="/login">Login</a></li>
|
||||
{{end}}
|
||||
</ul>
|
||||
{{end}}
|
||||
|
|
@ -1,17 +0,0 @@
|
|||
{{define "body"}}
|
||||
<div class="page-header">
|
||||
<div class="row">
|
||||
<div class="col-xs-4 offset-4">
|
||||
<div class="card border-danger mx-auto">
|
||||
<div class="card-header">
|
||||
<strong>404 - Page Not Found</strong>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<h5>The requested page could not be found</h5>
|
||||
<a href="/" class="btn btn-sm btn-primary">Back</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{{end}}
|
||||
|
|
@ -1,40 +0,0 @@
|
|||
{{define "body"}}
|
||||
<div class="row">
|
||||
<div class="nav-container full-width relative">
|
||||
<ol class="breadcrumb full-width">
|
||||
<li class="breadcrumb-item"></li>
|
||||
<li class="breadcrumb-item"><a href="/{{.BodyTitle}}">{{.BodyTitle}}</a></li>
|
||||
<li class="breadcrumb-item active">{{.Data.Title}}</li>
|
||||
</ol>
|
||||
<div class="btn-group btn-breadcrumb">
|
||||
<a href="/{{.Data.StoreTitle}}/md" class="btn btn-sm btn-primary">Markdown</a>
|
||||
{{if .Login}}
|
||||
<a href="/{{.Data.StoreTitle}}/edit" class="btn btn-sm btn-primary">Edit</a>
|
||||
{{if eq .Data.Perm 2}}
|
||||
<a href="/{{.Data.StoreTitle}}/share" class="btn btn-sm btn-primary">Share</a>
|
||||
{{end}}
|
||||
{{if ne .Data.StoreTitle "wiki/Index"}}
|
||||
<a href="/{{.Data.StoreTitle}}/del" class="btn btn-sm btn-danger">Delete</a>
|
||||
{{end}}
|
||||
{{end}}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row mt-3">
|
||||
<div class="col">
|
||||
<strong>Created by:</strong> {{.Data.Created}}<br>
|
||||
{{if .Data.Updated}}
|
||||
<strong>Updated by:</strong> {{.Data.Updated}}<br>
|
||||
{{end}}
|
||||
<strong>Visibility:</strong>
|
||||
{{if eq .Data.Perm 1}}
|
||||
<span class="badge badge-primary">Public</span>
|
||||
{{end}}
|
||||
{{if eq .Data.Perm 2}}
|
||||
<span class="badge badge-danger">Private</span>
|
||||
{{end}}
|
||||
<br>
|
||||
</div>
|
||||
</div>
|
||||
{{.Data.HTML}}
|
||||
{{end}}
|
||||
|
|
@ -1,23 +0,0 @@
|
|||
{{define "body"}}
|
||||
<div class="page-header">
|
||||
<div class="row">
|
||||
<div class="col-xs-4 offset-4">
|
||||
<div class="card border-danger mx-auto">
|
||||
<div class="card-header">
|
||||
<strong>{{.BodyTitle}}</strong>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<form class="form-horizontal" action="/blacklist" method="post">
|
||||
<input type="hidden" name="token" value="{{.Token}}">
|
||||
<div class="form-group">
|
||||
<label class="col-form-label" for="token">Token</label>
|
||||
<input class="form-control input-sm" type="text" id="token" name="share" autocomplete="off" autofocus required>
|
||||
</div>
|
||||
<button class="btn btn-sm btn-danger" type="submit">Blacklist</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{{end}}
|
||||
|
|
@ -1,20 +0,0 @@
|
|||
{{define "body"}}
|
||||
<div class="page-header">
|
||||
<div class="row">
|
||||
<div class="col-xs-4 offset-4">
|
||||
<div class="card border-danger mx-auto">
|
||||
<div class="card-header">
|
||||
<strong>Delete {{.Data}}</strong>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<form class="form-horizontal" action="/{{.Data}}/del" method="post">
|
||||
<input type="hidden" name="token" value="{{.Token}}">
|
||||
<button class="btn btn-sm btn-danger" type="submit">Delete</button>
|
||||
<a href="/{{.Data}}" class="btn btn-sm btn-primary">Back</a>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{{end}}
|
||||
|
|
@ -1,30 +0,0 @@
|
|||
{{define "body"}}
|
||||
<div class="page-header">
|
||||
<div class="row">
|
||||
<h2>{{.BodyTitle}}</h2>
|
||||
</div>
|
||||
</div>
|
||||
<form class="form-horizontal" action="/{{.Data.StoreTitle}}/edit" method="post">
|
||||
<input type="hidden" name="token" value="{{.Token}}">
|
||||
<div class="form-group">
|
||||
<label class="col-form-label" for="title">Title</label>
|
||||
<input class="form-control input-sm" type="text" id="title" name="title" value="{{.Data.Title}}" spellcheck="false" disabled>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="col-form-label" for="markdown">Markdown</label>
|
||||
<textarea class="form-control input-sm md-text" id="markdown" name="markdown" rows="30" spellcheck="false">{{.Data.Markdown}}</textarea>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<div class="custom-control custom-radio">
|
||||
<input type="radio" id="perm1" name="perm" value="1" class="custom-control-input" {{if eq .Data.Perm 1}}checked=""{{end}}>
|
||||
<label class="custom-control-label" for="perm1">Public</label>
|
||||
</div>
|
||||
<div class="custom-control custom-radio">
|
||||
<input type="radio" id="perm2" name="perm" value="2" class="custom-control-input" {{if eq .Data.Perm 2}}checked=""{{end}}>
|
||||
<label class="custom-control-label" for="perm2">Private</label>
|
||||
</div>
|
||||
</div>
|
||||
<button class="btn btn-sm btn-primary" type="submit">Update</button>
|
||||
<a href="/{{.Data.StoreTitle}}" class="btn btn-sm btn-primary">Back</a>
|
||||
</form>
|
||||
{{end}}
|
||||
|
|
@ -1,14 +0,0 @@
|
|||
{{define "body"}}
|
||||
<div class="row">
|
||||
<div class="nav-container full-width relative">
|
||||
<ol class="breadcrumb full-width">
|
||||
<li class="breadcrumb-item"></li>
|
||||
<li class="breadcrumb-item"><a href="/{{.BodyTitle}}">{{.BodyTitle}}</a></li>
|
||||
<li class="breadcrumb-item active"><a href="/{{if .Key}}view/{{.Key}}{{else}}{{.Data.StoreTitle}}{{end}}">{{.Data.Title}}</a></li>
|
||||
</ol>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row mt-3">
|
||||
<textarea class="form-control input-sm md-text" rows="30" spellcheck="false" autofocus>{{.Data.Markdown}}</textarea>
|
||||
</div>
|
||||
{{end}}
|
||||
|
|
@ -1,41 +0,0 @@
|
|||
{{define "body"}}
|
||||
<div class="page-header">
|
||||
<div class="row">
|
||||
<h2>{{.BodyTitle}}</h2>
|
||||
</div>
|
||||
</div>
|
||||
<form class="form-horizontal" action="/new" method="post">
|
||||
<input type="hidden" name="token" value="{{.Token}}">
|
||||
<div class="form-group">
|
||||
<label class="col-form-label" for="section">Section</label>
|
||||
<select class="form-control input-sm" id="section" name="section" required>
|
||||
{{range $section, $selected := .Data.Sections}}
|
||||
{{if $selected}}
|
||||
<option value="{{$section}}" selected>{{$section}}</option>
|
||||
{{else}}
|
||||
<option value="{{$section}}">{{$section}}</option>
|
||||
{{end}}
|
||||
{{end}}
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="col-form-label" for="title">Title</label>
|
||||
<input class="form-control input-sm" type="text" id="title" name="title" placeholder="Title" value="{{.Data.Page.Title}}" spellcheck="false" autocomplete="off" autofocus required>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="col-form-label" for="markdown">Markdown</label>
|
||||
<textarea class="form-control input-sm md-text" id="markdown" name="markdown" rows="30" spellcheck="false">{{.Data.Page.Markdown}}</textarea>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<div class="custom-control custom-radio">
|
||||
<input type="radio" id="perm1" name="perm" value="1" class="custom-control-input" {{if eq .Data.Page.Perm 1}}checked=""{{end}}>
|
||||
<label class="custom-control-label" for="perm1">Public</label>
|
||||
</div>
|
||||
<div class="custom-control custom-radio">
|
||||
<input type="radio" id="perm2" name="perm" value="2" class="custom-control-input" {{if eq .Data.Page.Perm 2}}checked=""{{end}}>
|
||||
<label class="custom-control-label" for="perm2">Private</label>
|
||||
</div>
|
||||
</div>
|
||||
<button class="btn btn-sm btn-primary" type="submit">Create</button>
|
||||
</form>
|
||||
{{end}}
|
||||
|
|
@ -1,43 +0,0 @@
|
|||
{{define "body"}}
|
||||
<div class="page-header">
|
||||
<div class="row">
|
||||
<div class="col-xs-4 offset-4">
|
||||
<div class="card border-primary mx-auto">
|
||||
<div class="card-header">
|
||||
<strong>Share {{.Data}}</strong>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
{{if .Key}}
|
||||
<div class="form-group">
|
||||
<label class="col-form-label" for="key">Key</label>
|
||||
<input class="form-control input-sm" type="text" id="key" value="{{.Key}}" disabled>
|
||||
</div>
|
||||
<a href="/view/{{.Key}}" class="btn btn-sm btn-danger">Share Link</a>
|
||||
<a href="/{{.Data}}/share" class="btn btn-sm btn-primary">Back</a>
|
||||
{{else}}
|
||||
<form class="form-horizontal" action="/{{.Data}}/share" method="post">
|
||||
<input type="hidden" name="token" value="{{.Token}}">
|
||||
<div class="form-group">
|
||||
<label class="col-form-label" for="duration">Lifetime</label>
|
||||
<input class="form-control input-sm" type="text" id="duration" name="duration" autocomplete="off" autofocus required>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<div class="custom-control custom-radio">
|
||||
<input type="radio" id="mode1" name="mode" value="1" class="custom-control-input" checked="">
|
||||
<label class="custom-control-label" for="mode1">Parse (15m, 10h)</label>
|
||||
</div>
|
||||
<div class="custom-control custom-radio">
|
||||
<input type="radio" id="mode2" name="mode" value="2" class="custom-control-input">
|
||||
<label class="custom-control-label" for="mode2">Day(s) (Numbers only)</label>
|
||||
</div>
|
||||
</div>
|
||||
<button class="btn btn-sm btn-primary" type="submit">Share</button>
|
||||
<a href="/{{.Data}}" class="btn btn-sm btn-primary">Back</a>
|
||||
</form>
|
||||
{{end}}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{{end}}
|
||||
|
|
@ -1,31 +0,0 @@
|
|||
{{define "body"}}
|
||||
<div class="row">
|
||||
<div class="nav-container full-width relative">
|
||||
<ol class="breadcrumb full-width">
|
||||
<li class="breadcrumb-item"></li>
|
||||
<li class="breadcrumb-item"><a href="/{{.BodyTitle}}">{{.BodyTitle}}</a></li>
|
||||
<li class="breadcrumb-item active">{{.Data.Title}}</li>
|
||||
</ol>
|
||||
<div class="btn-group btn-breadcrumb">
|
||||
<a href="/view/{{.Key}}/md" class="btn btn-sm btn-primary">Markdown</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row mt-3">
|
||||
<div class="col">
|
||||
<strong>Created by:</strong> {{.Data.Created}}<br>
|
||||
{{if .Data.Updated}}
|
||||
<strong>Updated by:</strong> {{.Data.Updated}}<br>
|
||||
{{end}}
|
||||
<strong>Visibility:</strong>
|
||||
{{if eq .Data.Perm 1}}
|
||||
<span class="badge badge-primary">Public</span>
|
||||
{{end}}
|
||||
{{if eq .Data.Perm 2}}
|
||||
<span class="badge badge-danger">Private</span>
|
||||
{{end}}
|
||||
<br>
|
||||
</div>
|
||||
</div>
|
||||
{{.Data.HTML}}
|
||||
{{end}}
|
||||
|
|
@ -1,30 +0,0 @@
|
|||
{{define "body"}}
|
||||
<div class="page-header">
|
||||
<div class="row">
|
||||
<h2>{{.BodyTitle}}</h2>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row mt-3">
|
||||
<div class="col-xs-6">
|
||||
<form class="form-horizontal" action="/search" method="post">
|
||||
<input type="hidden" name="token" value="{{.Token}}">
|
||||
<div class="form-group">
|
||||
<div class="input-group">
|
||||
<input type="text" class="form-control input-sm" placeholder="Search" name="search" value="{{.Search}}" autocomplete="off" autofocus>
|
||||
<div class="input-group-append">
|
||||
<button class="btn btn-sm btn-primary" type="submit">Search</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row mt-3">
|
||||
<div class="col">
|
||||
{{range $item := .Data}}
|
||||
<a href="/{{$item.StoreTitle}}"><b>{{$item.Section}} / {{$item.Title}}</b></a>
|
||||
<pre class="wrap">{{$item.HTML}}</pre><br>
|
||||
{{end}}
|
||||
</div>
|
||||
</div>
|
||||
{{end}}
|
||||
|
|
@ -1,39 +0,0 @@
|
|||
{{define "body"}}
|
||||
<!--<div class="page-header">
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
<h2>{{.BodyTitle}}</h2>
|
||||
</div>
|
||||
<div class="col">
|
||||
{{if .Admin}}
|
||||
<div class="btn-group float-right">
|
||||
<a class="btn btn-sm btn-primary" href="/section/edit/{{.BodyTitle}}">Edit</a>
|
||||
<a class="btn btn-sm btn-danger" href="/section/del/{{.BodyTitle}}">Delete</a>
|
||||
</div>
|
||||
{{end}}
|
||||
</div>
|
||||
</div>-->
|
||||
<div class="row">
|
||||
<div class="nav-container full-width relative">
|
||||
<ol class="breadcrumb full-width">
|
||||
<li class="breadcrumb-item"></li>
|
||||
<li class="breadcrumb-item">{{.BodyTitle}}</li>
|
||||
</ol>
|
||||
{{if .Admin}}
|
||||
<div class="btn-group btn-breadcrumb">
|
||||
<a class="btn btn-sm btn-primary" href="/section/edit/{{.BodyTitle}}">Edit</a>
|
||||
<a class="btn btn-sm btn-danger" href="/section/del/{{.BodyTitle}}">Delete</a>
|
||||
</div>
|
||||
{{end}}
|
||||
</div>
|
||||
</div>
|
||||
{{if .Data}}
|
||||
<div class="row">
|
||||
<ul type="circle">
|
||||
{{range $item := .Data}}
|
||||
<li><a href="/{{$item.StoreTitle}}"><b>{{$item.Title}}</b></a></li>
|
||||
{{end}}
|
||||
</ul>
|
||||
</div>
|
||||
{{end}}
|
||||
{{end}}
|
||||
|
|
@ -1,23 +0,0 @@
|
|||
{{define "body"}}
|
||||
{{if .Data}}
|
||||
<div class="page-header">
|
||||
<div class="row">
|
||||
<div class="col-xs-4 offset-4">
|
||||
<div class="card border-danger mx-auto">
|
||||
<div class="card-header">
|
||||
<strong>Delete {{.Data}}?</strong>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<p>You are about to delete section <a href="/{{.Data}}">{{.Data}}</a>.<br>This will also remove all pages of this section.</p>
|
||||
<form class="form-horizontal" action="/section/del/{{.Data}}" method="post">
|
||||
<input type="hidden" name="token" value="{{.Token}}">
|
||||
<button class="btn btn-sm btn-danger" type="submit">Delete</button>
|
||||
<a href="/{{.Data}}" class="btn btn-sm btn-primary">Back</a>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{{end}}
|
||||
{{end}}
|
||||
|
|
@ -1,34 +0,0 @@
|
|||
{{define "body"}}
|
||||
<div class="row">
|
||||
<div class="col-xs-4 offset-4">
|
||||
<div class="card border-primary mx-auto">
|
||||
<div class="card-header">
|
||||
<strong>{{.BodyTitle}}</strong>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<form class="form-horizontal" action="/section/edit/{{.Data.Name}}" method="post">
|
||||
<input type="hidden" name="token" value="{{.Token}}">
|
||||
<div class="form-group">
|
||||
<label class="col-form-label" for="section">Section</label>
|
||||
<input class="form-control input-sm" type="text" id="section" name="section" value="{{.Data.Name}}" disabled>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="col-form-label" for="members">Members</label>
|
||||
<select class="form-control input-sm sections" id="members" name="members" multiple required>
|
||||
{{range $user, $member := .Data.Members}}
|
||||
{{if $member}}
|
||||
<option value="{{$user}}" selected>{{$user}}</option>
|
||||
{{else}}
|
||||
<option value="{{$user}}">{{$user}}</option>
|
||||
{{end}}
|
||||
{{end}}
|
||||
</select>
|
||||
</div>
|
||||
<button class="btn btn-sm btn-primary" type="submit">Update</button>
|
||||
<a href="/{{.Data.Name}}" class="btn btn-sm btn-primary">Back</a>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{{end}}
|
||||
|
|
@ -1,34 +0,0 @@
|
|||
{{define "body"}}
|
||||
<div class="row">
|
||||
<div class="col-xs-4 offset-4">
|
||||
<div class="card border-primary mx-auto">
|
||||
<div class="card-header">
|
||||
<strong>{{.BodyTitle}}</strong>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<form class="form-horizontal" action="/section/new" method="post">
|
||||
<input type="hidden" name="token" value="{{.Token}}">
|
||||
<div class="form-group">
|
||||
<label class="col-form-label" for="section">Section</label>
|
||||
<input class="form-control input-sm" type="text" id="section" name="section" autocomplete="off" value="{{.Data.Name}}" autofocus required>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="col-form-label" for="members">Members</label>
|
||||
<select class="form-control input-sm sections" id="members" name="members" multiple required>
|
||||
{{range $user, $member := .Data.Members}}
|
||||
{{if $member}}
|
||||
<option value="{{$user}}" selected>{{$user}}</option>
|
||||
{{else}}
|
||||
<option value="{{$user}}">{{$user}}</option>
|
||||
{{end}}
|
||||
{{end}}
|
||||
</select>
|
||||
</div>
|
||||
<button class="btn btn-sm btn-primary" type="submit">Create</button>
|
||||
<a href="/sections" class="btn btn-sm btn-primary">Back</a>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{{end}}
|
||||
|
|
@ -1,37 +0,0 @@
|
|||
{{define "body"}}
|
||||
<div class="page-header">
|
||||
<div class="row">
|
||||
<h2>{{.BodyTitle}}</h2>
|
||||
</div>
|
||||
</div>
|
||||
{{if .Admin}}
|
||||
<div class="row mb-3">
|
||||
<div class="col-xs-5">
|
||||
<a class="btn btn-sm btn-primary" href="/section/new"><b>New Section</b></a>
|
||||
</div>
|
||||
</div>
|
||||
{{end}}
|
||||
{{if .Data}}
|
||||
<div class="row">
|
||||
{{range $index, $item := .Data}}
|
||||
<div class="col-xs-4 section">
|
||||
<div class="card border-primary mx-auto">
|
||||
<div class="card-header">
|
||||
<strong><a href="/{{$item.Section}}">{{$item.Section}}</a></strong>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
{{if $item.Pages}}
|
||||
{{range $page := $item.Pages}}
|
||||
<strong><a href="/{{$page.StoreTitle}}">{{$page.Title}}</a></strong><br>
|
||||
{{end}}
|
||||
{{else}}
|
||||
<strong>Empty section</strong>
|
||||
{{end}}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="offset-1"></div>
|
||||
{{end}}
|
||||
</div>
|
||||
{{end}}
|
||||
{{end}}
|
||||
|
|
@ -1,33 +0,0 @@
|
|||
{{define "body"}}
|
||||
<div class="page-header">
|
||||
<div class="row">
|
||||
<h2>{{.BodyTitle}}</h2>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<table class="table table-stripped table-hover">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td><b>Goroutines</b></td>
|
||||
<td>{{.Data.Goroutines}}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><b>Memory usage</b></td>
|
||||
<td>{{.Data.Alloc}} MiB</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><b>Memory allocated</b></td>
|
||||
<td>{{.Data.Sys}} MiB</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><b>Documents</b></td>
|
||||
<td>{{.Data.Docs}}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><b>Go Version</b></td>
|
||||
<td>{{.Data.GoVersion}}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{{end}}
|
||||
|
|
@ -1,23 +0,0 @@
|
|||
{{define "body"}}
|
||||
{{if .Data}}
|
||||
<div class="page-header">
|
||||
<div class="row">
|
||||
<div class="col-xs-4 offset-4">
|
||||
<div class="card border-danger mx-auto">
|
||||
<div class="card-header">
|
||||
<strong>Delete {{.Data.Username}}?</strong>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<p>You are about to delete user <a href="/{{.Data.Username}}">{{.Data.Username}}</a>.<br>This will also remove all pages of this user.</p>
|
||||
<form class="form-horizontal" action="/user/del/{{.Data.Username}}" method="post">
|
||||
<input type="hidden" name="token" value="{{.Token}}">
|
||||
<button class="btn btn-sm btn-danger" type="submit">Delete</button>
|
||||
<a href="/users" class="btn btn-sm btn-primary">Back</a>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{{end}}
|
||||
{{end}}
|
||||
|
|
@ -1,65 +0,0 @@
|
|||
{{define "body"}}
|
||||
{{if .Data}}
|
||||
<div class="row">
|
||||
<div class="col-xs-4 offset-4">
|
||||
<div class="card border-primary mx-auto">
|
||||
<div class="card-header">
|
||||
<strong>{{.BodyTitle}}</strong>
|
||||
{{if .Data.Secret}}
|
||||
<a href="/user/totp/{{.Data.Username}}" class="btn btn-sm btn-danger float-right">Disable TOTP</a>
|
||||
{{else}}
|
||||
<a href="/user/totp/{{.Data.Username}}" class="btn btn-sm btn-success float-right">Enable TOTP</a>
|
||||
{{end}}
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<form class="form-horizontal" action="/user/edit/{{.Data.Username}}" method="post">
|
||||
<input type="hidden" name="token" value="{{.Token}}">
|
||||
<div class="form-group">
|
||||
<label class="col-form-label" for="user">Username</label>
|
||||
<input class="form-control input-sm" type="text" id="user" name="user" value="{{.Data.Username}}" disabled>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="col-form-label" for="password">Password</label>
|
||||
<input class="form-control input-sm" type="password" id="password" name="password" autofocus>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="col-form-label" for="repeat">Repeat</label>
|
||||
<input class="form-control input-sm" type="password" id="repeat" name="repeat">
|
||||
</div>
|
||||
{{if .Admin}}
|
||||
<div class="form-group">
|
||||
<div class="custom-control custom-checkbox">
|
||||
{{if .Data.Admin}}
|
||||
<input type="checkbox" class="custom-control-input" id="admin" name="admin" value="0" checked>
|
||||
{{else}}
|
||||
<input type="checkbox" class="custom-control-input" id="admin" name="admin" value="0">
|
||||
{{end}}
|
||||
<label class="custom-control-label" for="admin">Admin Privileges</label>
|
||||
</div>
|
||||
</div>
|
||||
{{end}}
|
||||
<button class="btn btn-sm btn-primary" type="submit">Update</button>
|
||||
{{if .Admin}}
|
||||
<a href="/users" class="btn btn-sm btn-primary">Back</a>
|
||||
{{else}}
|
||||
<a href="/" class="btn btn-sm btn-primary">Back</a>
|
||||
{{end}}
|
||||
<a class="btn btn-sm btn-danger" href="/user/del/{{.Data.Username}}">Delete</a>
|
||||
</form>
|
||||
{{if .Admin}}
|
||||
{{if eq .Data.Locked 3}}
|
||||
<form class="form-horizontal" action="/user/unlock/{{.Data.Username}}" method="post">
|
||||
<input type="hidden" name="token" value="{{.Token}}">
|
||||
<div class="form-group">
|
||||
<label class="col-form-label" for="unlock">Locked</label>
|
||||
<button class="btn btn-sm btn-warning" type="submit" id="unlock">Unlock</button>
|
||||
</div>
|
||||
</form>
|
||||
{{end}}
|
||||
{{end}}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{{end}}
|
||||
{{end}}
|
||||
|
|
@ -1,36 +0,0 @@
|
|||
{{define "body"}}
|
||||
<div class="row">
|
||||
<div class="col-xs-4 offset-4">
|
||||
<div class="card border-primary mx-auto">
|
||||
<div class="card-header">
|
||||
<strong>{{.BodyTitle}}</strong>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<form class="form-horizontal" action="/user/new" method="post">
|
||||
<input type="hidden" name="token" value="{{.Token}}">
|
||||
<div class="form-group">
|
||||
<label class="col-form-label" for="user">Username</label>
|
||||
<input class="form-control input-sm" type="text" id="user" name="user" autocomplete="off" autofocus required>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="col-form-label" for="password">Password</label>
|
||||
<input class="form-control input-sm" type="password" id="password" name="password" required>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="col-form-label" for="repeat">Repeat</label>
|
||||
<input class="form-control input-sm" type="password" id="repeat" name="repeat" required>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<div class="custom-control custom-checkbox">
|
||||
<input type="checkbox" class="custom-control-input" id="admin" name="admin" value="0">
|
||||
<label class="custom-control-label" for="admin">Admin Privileges</label>
|
||||
</div>
|
||||
</div>
|
||||
<button class="btn btn-sm btn-primary" type="submit">Create</button>
|
||||
<a href="/users" class="btn btn-sm btn-primary">Back</a>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{{end}}
|
||||
|
|
@ -1,38 +0,0 @@
|
|||
{{define "body"}}
|
||||
{{if .Data}}
|
||||
<div class="row">
|
||||
<div class="col-xs-4 offset-4">
|
||||
<div class="card border-primary mx-auto">
|
||||
<div class="card-header">
|
||||
<strong>{{.BodyTitle}} - {{.Data.Username}}</strong>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<form class="form-horizontal" action="/user/totp/{{.Data.Username}}" method="post">
|
||||
<input type="hidden" name="token" value="{{.Token}}">
|
||||
{{if .Data.Secret}}
|
||||
<input type="hidden" name="url" value="{{.Data.URL}}">
|
||||
<div class="form-group">
|
||||
<img src="{{.Data.Image}}" alt="{{.Data.Secret}}">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="col-form-label" for="secret">Secret</label>
|
||||
<pre><code id="secret">{{.Data.Secret}}</code></pre>
|
||||
</div>
|
||||
{{end}}
|
||||
<div class="form-group">
|
||||
<label class="col-form-label" for="pin">PIN</label>
|
||||
<input class="form-control input-sm" type="text" id="pin" name="pin" autocomplete="off" autofocus required>
|
||||
</div>
|
||||
{{if .Data.Secret}}
|
||||
<button class="btn btn-sm btn-success" type="submit">Enable</button>
|
||||
{{else}}
|
||||
<button class="btn btn-sm btn-danger" type="submit">Disable</button>
|
||||
{{end}}
|
||||
<a href="/user/edit/{{.Data.Username}}" class="btn btn-sm btn-primary">Back</a>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{{end}}
|
||||
{{end}}
|
||||
|
|
@ -1,55 +0,0 @@
|
|||
{{define "body"}}
|
||||
<div class="page-header">
|
||||
<div class="row">
|
||||
<h2>{{.BodyTitle}}</h2>
|
||||
</div>
|
||||
</div>
|
||||
{{if .Admin}}
|
||||
<div class="row mb-3">
|
||||
<div class="col-xs-5">
|
||||
<a class="btn btn-sm btn-primary" href="/user/new"><b>New User</b></a>
|
||||
</div>
|
||||
</div>
|
||||
{{end}}
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
{{if .Data}}
|
||||
<table class="table table-hover">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Username</th>
|
||||
<th>Flags</th>
|
||||
<th>Options</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{{$user := .User}}
|
||||
{{$admin := .Admin}}
|
||||
{{range $item := .Data}}
|
||||
<tr>
|
||||
<td>{{$item.Username}}</td>
|
||||
<td>
|
||||
{{if $item.Secret}}<span class="badge badge-success">TOTP</span>{{end}}
|
||||
{{if eq $item.Locked 3}}<span class="badge badge-warning">Locked</span>{{end}}
|
||||
{{if $item.Admin}}<span class="badge badge-danger">Admin</span>{{end}}
|
||||
</td>
|
||||
<td>
|
||||
<div class="btn-group">
|
||||
{{if $admin}}
|
||||
<a class="btn btn-sm btn-primary" href="/user/edit/{{$item.Username}}">Edit</a>
|
||||
<a class="btn btn-sm btn-danger" href="/user/del/{{$item.Username}}">Delete</a>
|
||||
{{else}}
|
||||
{{if eq $item.Username $user}}
|
||||
<a class="btn btn-sm btn-primary" href="/user/edit/{{$item.Username}}">Edit</a>
|
||||
{{end}}
|
||||
{{end}}
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
{{end}}
|
||||
</tbody>
|
||||
</table>
|
||||
{{end}}
|
||||
</div>
|
||||
</div>
|
||||
{{end}}
|
||||
|
|
@ -1,44 +0,0 @@
|
|||
// Copyright (C) 2021 Marius Schellenberger
|
||||
|
||||
package server
|
||||
|
||||
import (
|
||||
"crypto/hmac"
|
||||
"crypto/rand"
|
||||
"crypto/sha256"
|
||||
"encoding/base64"
|
||||
"git.giftfish.de/ston1th/gowiki/pkg/log"
|
||||
)
|
||||
|
||||
const (
|
||||
keySize = 16
|
||||
fullSize = keySize * 2
|
||||
)
|
||||
|
||||
func genKey(length int) (bytes []byte) {
|
||||
bytes = make([]byte, length)
|
||||
if _, err := rand.Reader.Read(bytes); err != nil {
|
||||
log.Println("genKey:", err)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func newXsrf(secret []byte) string {
|
||||
mac := hmac.New(sha256.New, secret)
|
||||
rnd := genKey(keySize)
|
||||
mac.Write(rnd)
|
||||
return base64.RawURLEncoding.EncodeToString(append(rnd, mac.Sum(nil)[:keySize]...))
|
||||
}
|
||||
|
||||
func checkXsrf(xsrf string, secret []byte) bool {
|
||||
t, err := base64.RawURLEncoding.DecodeString(xsrf)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
if len(t) != fullSize {
|
||||
return false
|
||||
}
|
||||
mac := hmac.New(sha256.New, secret)
|
||||
mac.Write(t[:keySize])
|
||||
return hmac.Equal(t[keySize:], mac.Sum(nil)[:keySize])
|
||||
}
|
||||
|
|
@ -1,31 +0,0 @@
|
|||
// Copyright (C) 2021 Marius Schellenberger
|
||||
|
||||
package server
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestXsrf(t *testing.T) {
|
||||
sec := genKey(keySize)
|
||||
if len(sec) != keySize {
|
||||
t.Fatal("len(sec) != keySize")
|
||||
}
|
||||
xsrf := newXsrf(sec)
|
||||
if xsrf == "" {
|
||||
t.Fatal("xsrf is empty")
|
||||
}
|
||||
|
||||
if !checkXsrf(xsrf, sec) {
|
||||
t.Fatal("valid xsrf check failed")
|
||||
}
|
||||
|
||||
if checkXsrf("test", sec) {
|
||||
t.Fatal("invalid xsrf check succeeded")
|
||||
}
|
||||
|
||||
rnd := genKey(keySize)
|
||||
if checkXsrf(xsrf, rnd) {
|
||||
t.Fatal("invalid xsrf check succeeded")
|
||||
}
|
||||
}
|
||||
|
|
@ -1,139 +0,0 @@
|
|||
// Copyright (C) 2021 Marius Schellenberger
|
||||
|
||||
package store
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
bolt "go.etcd.io/bbolt"
|
||||
"io"
|
||||
)
|
||||
|
||||
const (
|
||||
defaultBoltBucket = "default"
|
||||
fileMode = 0o600
|
||||
)
|
||||
|
||||
type BoltStore struct {
|
||||
Marshaler
|
||||
db *bolt.DB
|
||||
}
|
||||
|
||||
func NewBoltStore(file string, m Marshaler) (Store, error) {
|
||||
if m == nil {
|
||||
m = NewGOB()
|
||||
}
|
||||
db, err := bolt.Open(file, fileMode, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := db.Update(func(tx *bolt.Tx) (err error) {
|
||||
_, err = tx.CreateBucketIfNotExists([]byte(defaultBoltBucket))
|
||||
return
|
||||
}); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &BoltStore{m, db}, nil
|
||||
}
|
||||
|
||||
type dump struct {
|
||||
K string `json:"k"`
|
||||
V []byte `json:"v"`
|
||||
}
|
||||
|
||||
func (bs *BoltStore) Dump(w io.Writer) (err error) {
|
||||
if w == nil {
|
||||
return ErrWriterIsNil
|
||||
}
|
||||
var d []dump
|
||||
err = bs.ForEach(func(k string, v []byte) error {
|
||||
b := make([]byte, len(v))
|
||||
copy(b, v)
|
||||
d = append(d, dump{k, b})
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
return json.NewEncoder(w).Encode(d)
|
||||
}
|
||||
|
||||
func (bs *BoltStore) Restore(r io.Reader) (err error) {
|
||||
if r == nil {
|
||||
return ErrReaderIsNil
|
||||
}
|
||||
var d []dump
|
||||
err = json.NewDecoder(r).Decode(&d)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
if err = bs.db.Update(func(tx *bolt.Tx) (err error) {
|
||||
if err = tx.DeleteBucket([]byte(defaultBoltBucket)); err != nil {
|
||||
return
|
||||
}
|
||||
_, err = tx.CreateBucket([]byte(defaultBoltBucket))
|
||||
return
|
||||
}); err != nil {
|
||||
return
|
||||
}
|
||||
return bs.db.Update(func(tx *bolt.Tx) (err error) {
|
||||
b := tx.Bucket([]byte(defaultBoltBucket))
|
||||
for _, v := range d {
|
||||
b.Put([]byte(v.K), v.V)
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
func (bs *BoltStore) Get(key string, v interface{}) (err error) {
|
||||
err = bs.db.View(func(tx *bolt.Tx) error {
|
||||
b := tx.Bucket([]byte(defaultBoltBucket)).Get([]byte(key))
|
||||
if b == nil {
|
||||
return ErrKeyNotFound
|
||||
}
|
||||
if v != nil {
|
||||
return bs.Unmarshal(b, v)
|
||||
}
|
||||
return nil
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
func (bs *BoltStore) Set(key string, v interface{}) error {
|
||||
return bs.db.Update(func(tx *bolt.Tx) (err error) {
|
||||
var b []byte
|
||||
if v != nil {
|
||||
b, err = bs.Marshal(v)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
return tx.Bucket([]byte(defaultBoltBucket)).Put([]byte(key), b)
|
||||
})
|
||||
}
|
||||
|
||||
func (bs *BoltStore) ForEach(f func(string, []byte) error) error {
|
||||
return bs.db.View(func(tx *bolt.Tx) error {
|
||||
return tx.Bucket([]byte(defaultBoltBucket)).ForEach(func(k, v []byte) error {
|
||||
return f(string(k), v)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func (bs *BoltStore) ForEachPrefix(prefix string, f func(string, []byte) error) error {
|
||||
return bs.ForEach(func(k string, v []byte) error {
|
||||
if trim, ok := hasTrimPrefix(k, prefix); ok {
|
||||
return f(trim, v)
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
func (bs *BoltStore) Delete(key string) error {
|
||||
return bs.db.Update(func(tx *bolt.Tx) error {
|
||||
return tx.Bucket([]byte(defaultBoltBucket)).Delete([]byte(key))
|
||||
})
|
||||
}
|
||||
|
||||
func (bs *BoltStore) Close() error {
|
||||
return bs.db.Close()
|
||||
}
|
||||
|
|
@ -1,98 +0,0 @@
|
|||
// Copyright (C) 2021 Marius Schellenberger
|
||||
|
||||
package store
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
)
|
||||
|
||||
type testData struct {
|
||||
Data string
|
||||
}
|
||||
|
||||
func TestBoltStore(t *testing.T) {
|
||||
data := []testData{
|
||||
{"123"},
|
||||
{"hello"},
|
||||
}
|
||||
dir, err := os.MkdirTemp("", "")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer os.RemoveAll(dir)
|
||||
bs, err := NewBoltStore(filepath.Join(dir, "bolt.db"), nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
err = bs.Set("key0", data[0])
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
err = bs.Set("key1", data[1])
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
t.Run("GetKey0", func(t *testing.T) {
|
||||
var d testData
|
||||
err := bs.Get("key0", &d)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if d != data[0] {
|
||||
t.Error("d is not data[0]")
|
||||
}
|
||||
})
|
||||
t.Run("ForEach", func(t *testing.T) {
|
||||
err := bs.ForEach(func(k string, v []byte) error {
|
||||
if k == "key0" || k == "key1" {
|
||||
return nil
|
||||
}
|
||||
return errors.New("key0 or key1 not found")
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
})
|
||||
t.Run("DumpRestore", func(t *testing.T) {
|
||||
buf := new(bytes.Buffer)
|
||||
err := bs.Dump(buf)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if buf.Len() == 0 {
|
||||
t.Error("no dump written")
|
||||
}
|
||||
err = bs.Restore(buf)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
})
|
||||
t.Run("DeleteKey1", func(t *testing.T) {
|
||||
err := bs.Delete("key1")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
})
|
||||
t.Run("GetKey1", func(t *testing.T) {
|
||||
var d testData
|
||||
err := bs.Get("key1", &d)
|
||||
if err == nil {
|
||||
t.Error("key1 should be deleted")
|
||||
}
|
||||
if d == data[1] {
|
||||
t.Error("d is not data[1]")
|
||||
}
|
||||
})
|
||||
t.Run("Close", func(t *testing.T) {
|
||||
err := bs.Close()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
|
@ -1,13 +0,0 @@
|
|||
// Copyright (C) 2021 Marius Schellenberger
|
||||
|
||||
package store
|
||||
|
||||
import "io"
|
||||
|
||||
type Dumper interface {
|
||||
Dump(io.Writer) error
|
||||
}
|
||||
|
||||
type Restorer interface {
|
||||
Restore(io.Reader) error
|
||||
}
|
||||
|
|
@ -1,25 +0,0 @@
|
|||
// Copyright (C) 2021 Marius Schellenberger
|
||||
|
||||
package store
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/gob"
|
||||
)
|
||||
|
||||
type gobMarshaler struct{}
|
||||
|
||||
func NewGOB() Marshaler {
|
||||
return gobMarshaler{}
|
||||
}
|
||||
|
||||
func (gobMarshaler) Marshal(v interface{}) (b []byte, err error) {
|
||||
buf := new(bytes.Buffer)
|
||||
err = gob.NewEncoder(buf).Encode(v)
|
||||
b = buf.Bytes()
|
||||
return
|
||||
}
|
||||
|
||||
func (gobMarshaler) Unmarshal(data []byte, v interface{}) error {
|
||||
return gob.NewDecoder(bytes.NewBuffer(data)).Decode(v)
|
||||
}
|
||||
|
|
@ -1,39 +0,0 @@
|
|||
// Copyright (C) 2021 Marius Schellenberger
|
||||
|
||||
package store
|
||||
|
||||
import "testing"
|
||||
|
||||
var g = NewGOB()
|
||||
|
||||
func BenchmarkGOBMarshalPage(b *testing.B) {
|
||||
b.ReportAllocs()
|
||||
for i := 0; i < b.N; i++ {
|
||||
g.Marshal(p)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkGOBUnmarshalPage(b *testing.B) {
|
||||
bytes, _ := g.Marshal(p)
|
||||
var pp Page
|
||||
b.ReportAllocs()
|
||||
for i := 0; i < b.N; i++ {
|
||||
g.Unmarshal(bytes, &pp)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkGOBMarshalUser(b *testing.B) {
|
||||
b.ReportAllocs()
|
||||
for i := 0; i < b.N; i++ {
|
||||
g.Marshal(u)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkGOBUnmarshalUser(b *testing.B) {
|
||||
bytes, _ := g.Marshal(u)
|
||||
var uu User
|
||||
b.ReportAllocs()
|
||||
for i := 0; i < b.N; i++ {
|
||||
g.Unmarshal(bytes, &uu)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,11 +0,0 @@
|
|||
// Copyright (C) 2021 Marius Schellenberger
|
||||
|
||||
package store
|
||||
|
||||
// Parts taken from strings.TrimPrefix
|
||||
func hasTrimPrefix(s, prefix string) (string, bool) {
|
||||
if len(s) >= len(prefix) && s[0:len(prefix)] == prefix {
|
||||
return s[len(prefix):], true
|
||||
}
|
||||
return s, false
|
||||
}
|
||||
|
|
@ -1,8 +0,0 @@
|
|||
// Copyright (C) 2021 Marius Schellenberger
|
||||
|
||||
package store
|
||||
|
||||
type Marshaler interface {
|
||||
Marshal(v interface{}) ([]byte, error)
|
||||
Unmarshal(data []byte, v interface{}) error
|
||||
}
|
||||
|
|
@ -1,34 +0,0 @@
|
|||
// Copyright (C) 2021 Marius Schellenberger
|
||||
|
||||
package store
|
||||
|
||||
import "time"
|
||||
|
||||
type Page struct {
|
||||
Text string
|
||||
Date time.Time
|
||||
Number int
|
||||
Test bool
|
||||
}
|
||||
|
||||
type User struct {
|
||||
Username string
|
||||
Password string
|
||||
Number int
|
||||
Test bool
|
||||
Today time.Time
|
||||
}
|
||||
|
||||
var p = &Page{
|
||||
"Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet.",
|
||||
time.Now(),
|
||||
34534534,
|
||||
true,
|
||||
}
|
||||
var u = &User{
|
||||
"svdfnvdnfvon",
|
||||
"func BenchmarkNew(b *testing.B) {",
|
||||
8794355,
|
||||
false,
|
||||
time.Now(),
|
||||
}
|
||||
|
|
@ -1,23 +0,0 @@
|
|||
// Copyright (C) 2021 Marius Schellenberger
|
||||
|
||||
package store
|
||||
|
||||
import "errors"
|
||||
|
||||
var (
|
||||
ErrKeyNotFound = errors.New("store: key not found")
|
||||
ErrWriterIsNil = errors.New("store: writer is nil")
|
||||
ErrReaderIsNil = errors.New("store: reader is nil")
|
||||
)
|
||||
|
||||
type Store interface {
|
||||
Marshaler
|
||||
Dumper
|
||||
Restorer
|
||||
Get(string, interface{}) error
|
||||
Set(string, interface{}) error
|
||||
ForEach(func(string, []byte) error) error
|
||||
ForEachPrefix(string, func(string, []byte) error) error
|
||||
Delete(string) error
|
||||
Close() error
|
||||
}
|
||||
107
render.go
Normal file
107
render.go
Normal file
|
|
@ -0,0 +1,107 @@
|
|||
// Copyright (C) 2017 Marius Schellenberger
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"html/template"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/blevesearch/bleve/analysis"
|
||||
htmlFilter "github.com/blevesearch/bleve/analysis/char_filters/html_char_filter"
|
||||
"github.com/russross/blackfriday"
|
||||
)
|
||||
|
||||
var filter = makeFilter()
|
||||
|
||||
func makeFilter() analysis.CharFilter {
|
||||
f, _ := htmlFilter.CharFilterConstructor(nil, nil)
|
||||
return f
|
||||
}
|
||||
|
||||
var (
|
||||
reHeader = regexp.MustCompile("(<h[1-6])>(.+)(</h[1-6]>)")
|
||||
reLink = regexp.MustCompile(`id="(.+)">`)
|
||||
whiteSpace = regexp.MustCompile(`\s+`)
|
||||
reIndex = regexp.MustCompile("<h([1-6])>(.+)</h[1-6]>")
|
||||
)
|
||||
|
||||
func render(text string) Article {
|
||||
rend := blackfriday.MarkdownCommon([]byte(text))
|
||||
html := string(rend)
|
||||
index := buildIndex(html)
|
||||
html = strings.Replace(html, "<table>", `<table class="table">`, -1)
|
||||
html = reHeader.ReplaceAllString(html, `${1} id="${2}">${2}${3}`)
|
||||
html = reLink.ReplaceAllStringFunc(html, makeLinkTitle)
|
||||
html = strings.Replace(html, "</h1>", `</h1><hr>`, -1)
|
||||
html = strings.Replace(html, "<a", `<a target="_blank"`, -1)
|
||||
return Article{
|
||||
Search: whiteSpace.ReplaceAllString(string(filter.Filter(rend)), " "),
|
||||
Index: template.HTML(index),
|
||||
Text: template.HTML(html),
|
||||
MD: text,
|
||||
}
|
||||
}
|
||||
|
||||
func makeLinkTitle(title string) string {
|
||||
return strings.Replace(title, " ", "+", -1)
|
||||
}
|
||||
|
||||
func buildIndex(html string) (ret string) {
|
||||
hn := reIndex.FindAllStringSubmatch(html, -1)
|
||||
for k, v := range hn {
|
||||
link := makeLinkTitle(v[2])
|
||||
ret += before(hn, k)
|
||||
ret += `<li><a href="#` + link + `">` + v[2] + "</a>"
|
||||
ret += after(hn, k)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func before(arr [][]string, i int) string {
|
||||
c, _ := strconv.Atoi(arr[i][1])
|
||||
if c == 1 {
|
||||
return `<ul type="circle">`
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func after(arr [][]string, i int) string {
|
||||
c, _ := strconv.Atoi(arr[i][1])
|
||||
if len(arr) <= i+1 {
|
||||
return closeTag(c-1) + "</ul>"
|
||||
}
|
||||
n, _ := strconv.Atoi(arr[i+1][1])
|
||||
if n == 1 {
|
||||
return closeTag(c-n) + "</ul>"
|
||||
} else if n < c {
|
||||
return closeTag(c - n)
|
||||
} else if n > c {
|
||||
return openTag(n - c)
|
||||
} else if n == c {
|
||||
return "</li>"
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func openTag(j int) (ret string) {
|
||||
for i := 0; i < j; i++ {
|
||||
ret += `<ul type="circle"><li>`
|
||||
}
|
||||
if j > 1 {
|
||||
ret = strings.Replace(ret, "circle", "none", j-1)
|
||||
}
|
||||
ret = ret[0 : len(ret)-4]
|
||||
return
|
||||
}
|
||||
|
||||
func closeTag(j int) (ret string) {
|
||||
for i := 0; i < j; i++ {
|
||||
ret += "</li></ul></li>"
|
||||
}
|
||||
if j > 1 {
|
||||
ret = strings.Replace(ret, "</li></li>", "</li>", -1)
|
||||
}
|
||||
return
|
||||
}
|
||||
135
routes.go
Normal file
135
routes.go
Normal file
|
|
@ -0,0 +1,135 @@
|
|||
// Copyright (C) 2017 Marius Schellenberger
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/gorilla/mux"
|
||||
)
|
||||
|
||||
type route struct {
|
||||
Path string
|
||||
Handler http.HandlerFunc
|
||||
Methods []string
|
||||
}
|
||||
|
||||
func buildRoutes() http.Handler {
|
||||
m := mux.NewRouter()
|
||||
m.NotFoundHandler = notFoundHandler{}
|
||||
for _, v := range routes {
|
||||
m.HandleFunc(v.Path, v.Handler).Methods(v.Methods...)
|
||||
}
|
||||
return m
|
||||
}
|
||||
|
||||
var routes = []route{
|
||||
{
|
||||
"/",
|
||||
contextWrapper(
|
||||
indexHandler),
|
||||
[]string{"GET"},
|
||||
},
|
||||
{
|
||||
"/bootstrap.css",
|
||||
contextWrapper(
|
||||
staticHandler),
|
||||
[]string{"GET"},
|
||||
},
|
||||
{
|
||||
"/custom.css",
|
||||
contextWrapper(
|
||||
staticHandler),
|
||||
[]string{"GET"},
|
||||
},
|
||||
{
|
||||
"/login",
|
||||
contextWrapper(
|
||||
loginHandler),
|
||||
[]string{"GET", "POST"},
|
||||
},
|
||||
{
|
||||
"/search",
|
||||
contextWrapper(
|
||||
searchHandler),
|
||||
[]string{"GET", "POST"},
|
||||
},
|
||||
{
|
||||
"/new",
|
||||
contextWrapper(
|
||||
wikiNewHandler),
|
||||
[]string{"GET", "POST"},
|
||||
},
|
||||
{
|
||||
"/all",
|
||||
contextWrapper(
|
||||
allHandler),
|
||||
[]string{"GET"},
|
||||
},
|
||||
{
|
||||
"/wiki/{title:[a-zA-Z0-9+]+$}",
|
||||
contextWrapper(
|
||||
wikiHandler),
|
||||
[]string{"GET"},
|
||||
},
|
||||
{
|
||||
"/wiki/{title:[a-zA-Z0-9+]+}/md",
|
||||
contextWrapper(
|
||||
wikiMDHandler(
|
||||
wikiHandler)),
|
||||
[]string{"GET"},
|
||||
},
|
||||
{
|
||||
"/wiki/{title:[a-zA-Z0-9+]+}/edit",
|
||||
contextWrapper(
|
||||
wikiEditHandler),
|
||||
[]string{"GET", "POST"},
|
||||
},
|
||||
{
|
||||
"/wiki/{title:[a-zA-Z0-9+]+}/del",
|
||||
contextWrapper(
|
||||
wikiDelHandler),
|
||||
[]string{"POST"},
|
||||
},
|
||||
{
|
||||
"/users",
|
||||
contextWrapper(
|
||||
adminAuthHandler(
|
||||
usersHandler)),
|
||||
[]string{"GET"},
|
||||
},
|
||||
{
|
||||
"/user/new",
|
||||
contextWrapper(
|
||||
adminAuthHandler(
|
||||
userNewHandler)),
|
||||
[]string{"GET", "POST"},
|
||||
},
|
||||
{
|
||||
"/user/del/{user:[a-z]+$}",
|
||||
contextWrapper(
|
||||
adminAuthHandler(
|
||||
userDelHandler)),
|
||||
[]string{"GET", "POST"},
|
||||
},
|
||||
{
|
||||
"/user/unlock/{user:[a-z]+$}",
|
||||
contextWrapper(
|
||||
adminAuthHandler(
|
||||
userUnlockHandler)),
|
||||
[]string{"POST"},
|
||||
},
|
||||
{
|
||||
"/user/edit/{user:[a-z]+$}",
|
||||
contextWrapper(
|
||||
userAuthHandler(
|
||||
userEditHandler)),
|
||||
[]string{"GET", "POST"},
|
||||
},
|
||||
{
|
||||
"/logout",
|
||||
contextWrapper(
|
||||
logoutHandler),
|
||||
[]string{"GET", "POST"},
|
||||
},
|
||||
}
|
||||
55
scripts/gowiki_init
Executable file
55
scripts/gowiki_init
Executable file
|
|
@ -0,0 +1,55 @@
|
|||
#!/bin/sh
|
||||
### BEGIN INIT INFO
|
||||
# Provides: gowiki
|
||||
# Required-Start: $syslog $network
|
||||
# Required-Stop: $syslog
|
||||
# Default-Start: 2 3 4 5
|
||||
# Default-Stop: 0 1 6
|
||||
# Short-Description: GoWiki
|
||||
# Description: GoWiki
|
||||
### END INIT INFO
|
||||
|
||||
PATH=/sbin:/usr/sbin:/bin:/usr/bin
|
||||
DESC="wiki engine"
|
||||
NAME=gowiki
|
||||
DAEMON=/usr/bin/${NAME}
|
||||
DAEMON_OPTS="-s"
|
||||
PID=$(ps aux|grep "${DAEMON}"|awk '!/grep/{print $2}')
|
||||
|
||||
. /lib/lsb/init-functions
|
||||
|
||||
do_start() {
|
||||
${DAEMON} ${DAEMON_OPTS}
|
||||
log_end_msg ${?}
|
||||
}
|
||||
|
||||
do_stop() {
|
||||
kill ${PID}
|
||||
return "${?}"
|
||||
}
|
||||
|
||||
case "${1}" in
|
||||
start)
|
||||
log_action_msg "Starting ${DESC}" "${NAME}"
|
||||
do_start
|
||||
;;
|
||||
stop)
|
||||
log_daemon_msg "Stopping ${DESC}" "${NAME}"
|
||||
do_stop
|
||||
;;
|
||||
restart)
|
||||
log_daemon_msg "Restarting ${DESC}" "${NAME}"
|
||||
do_stop
|
||||
if [ ${?} -ne 0 ]; then
|
||||
log_end_msg 1
|
||||
fi
|
||||
do_start
|
||||
;;
|
||||
status)
|
||||
status_of_proc -p ${PID} "${DAEMON}" "${NAME}" && exit 0 || exit ${?}
|
||||
;;
|
||||
*)
|
||||
echo "Usage: service ${NAME} {start|stop|restart|status}"
|
||||
exit 3
|
||||
;;
|
||||
esac
|
||||
4
scripts/install.sh
Executable file
4
scripts/install.sh
Executable file
|
|
@ -0,0 +1,4 @@
|
|||
#!/bin/sh
|
||||
useradd wiki
|
||||
mkdir /var/{lib,log}/gowiki
|
||||
chown wiki: /var/{lib,log}/gowiki
|
||||
|
|
@ -1,32 +0,0 @@
|
|||
#!/bin/sh
|
||||
groupadd wiki
|
||||
useradd -g wiki wiki
|
||||
mkdir /var/lib/gowiki
|
||||
chown wiki: /var/lib/gowiki
|
||||
mkdir /etc/gowiki
|
||||
cat <<EOF> /etc/gowiki/gowiki.conf
|
||||
{
|
||||
"data_dir": "/var/lib/gowiki",
|
||||
"secret": "$(openssl rand -hex 32)"
|
||||
}
|
||||
EOF
|
||||
cat <<EOF> /etc/systemd/system/gowiki.service
|
||||
[Unit]
|
||||
Description=GoWiki
|
||||
After=syslog.target
|
||||
After=network.target
|
||||
|
||||
[Service]
|
||||
Type=forking
|
||||
ExecStart=/usr/local/bin/gowiki server -c /etc/gowiki/gowiki.conf
|
||||
Restart=always
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
EOF
|
||||
systemctl enable gowiki
|
||||
cat <<EOF
|
||||
copy your gowiki binary to /usr/local/bin/gowiki
|
||||
|
||||
then run: systemctl start gowiki
|
||||
EOF
|
||||
|
|
@ -1,41 +0,0 @@
|
|||
#!/bin/sh
|
||||
|
||||
cat <<'EOF'> /etc/rc.d/gowiki
|
||||
#!/bin/ksh
|
||||
|
||||
daemon="/usr/local/bin/gowiki"
|
||||
daemon_user="_wiki"
|
||||
daemon_logger="daemon.info"
|
||||
|
||||
. /etc/rc.d/rc.subr
|
||||
|
||||
pexp=".*${daemon}.*"
|
||||
rc_bg=YES
|
||||
rc_reload=NO
|
||||
|
||||
rc_cmd $1
|
||||
EOF
|
||||
chmod 555 /etc/rc.d/gowiki
|
||||
rcctl enable gowiki
|
||||
rcctl set gowiki flags server -c /var/wiki/gowiki.conf
|
||||
|
||||
mkdir /var/wiki
|
||||
groupadd _wiki
|
||||
useradd -d /var/wiki -s /sbin/nologin -g _wiki _wiki
|
||||
chmod 750 /var/wiki
|
||||
chown _wiki:_wiki /var/wiki
|
||||
|
||||
cat <<EOF> /var/wiki/gowiki.conf
|
||||
{
|
||||
"data_dir": "/var/wiki",
|
||||
"secret": "$(openssl rand -hex 32)"
|
||||
}
|
||||
EOF
|
||||
chmod 640 /var/wiki/gowiki.conf
|
||||
chgrp _wiki /var/wiki/gowiki.conf
|
||||
|
||||
cat <<EOF
|
||||
copy your gowiki binary to /usr/local/bin/gowiki
|
||||
|
||||
then run: rcctl start gowiki
|
||||
EOF
|
||||
11
sri.sh
11
sri.sh
|
|
@ -1,10 +1,9 @@
|
|||
#!/bin/bash
|
||||
# Copyright (C) 2021 Marius Schellenberger
|
||||
# Copyright (C) 2017 Marius Schellenberger
|
||||
|
||||
HASH="sha256"
|
||||
DIR="pkg/server/static"
|
||||
FILES="*.css *.js"
|
||||
BINS="*.ico"
|
||||
DIR="templates"
|
||||
FILES="*.css *.js *.ico"
|
||||
echo "SRI (subresource integrity) hashes"
|
||||
cd ${DIR}
|
||||
for f in $(ls ${FILES} 2>/dev/null); do
|
||||
|
|
@ -13,7 +12,3 @@ for f in $(ls ${FILES} 2>/dev/null); do
|
|||
printf "%13s: " ${f}
|
||||
echo "${HASH}-$(cat ${f}|openssl dgst -${HASH} -binary|openssl enc -base64 -A)"
|
||||
done
|
||||
for f in $(ls ${BINS} 2>/dev/null); do
|
||||
printf "%13s: " ${f}
|
||||
echo "${HASH}-$(cat ${f}|openssl dgst -${HASH} -binary|openssl enc -base64 -A)"
|
||||
done
|
||||
|
|
|
|||
641
templates.go
Normal file
641
templates.go
Normal file
File diff suppressed because one or more lines are too long
94
templates.sh
Executable file
94
templates.sh
Executable file
|
|
@ -0,0 +1,94 @@
|
|||
#!/bin/bash
|
||||
# Copyright (C) 2017 Marius Schellenberger
|
||||
|
||||
# this file generates Go source code for the gowiki
|
||||
# it contains html and css and uses the contents
|
||||
# of the templates directory
|
||||
|
||||
templates_go="templates.go"
|
||||
echo "Generating templates.go"
|
||||
|
||||
if [ ! -d templates ]; then
|
||||
echo "error: templates directory not found" 1>&2
|
||||
exit 2
|
||||
fi
|
||||
|
||||
cat << EOF > ${templates_go}
|
||||
// Copyright (C) 2017 Marius Schellenberger
|
||||
|
||||
// This file is auto-generated by build/templates.sh
|
||||
// using the contents of the templates directory
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"html/template"
|
||||
)
|
||||
|
||||
EOF
|
||||
|
||||
echo "const (" >> ${templates_go}
|
||||
|
||||
for f in $(ls templates/*.html); do
|
||||
fname=${f/templates\//}
|
||||
fname=${fname/.html/}
|
||||
echo " ${fname} = \`$(cat ${f})\`" >> ${templates_go}
|
||||
done
|
||||
|
||||
for f in $(ls templates/*.css); do
|
||||
fname=${f/templates\//}
|
||||
fname=${fname/.css/CSS}
|
||||
echo " ${fname} = \`$(cat ${f})\`" >> ${templates_go}
|
||||
done
|
||||
|
||||
cat << EOF >> ${templates_go}
|
||||
)
|
||||
|
||||
var (
|
||||
templ map[string]*template.Template
|
||||
res map[string][]byte
|
||||
)
|
||||
|
||||
func loadTemplates() {
|
||||
var errf bool
|
||||
templ = make(map[string]*template.Template)
|
||||
res = make(map[string][]byte)
|
||||
parse := func(html ...string) (temp *template.Template) {
|
||||
var err error
|
||||
temp = template.New("")
|
||||
for _, s := range html {
|
||||
temp, err = temp.Parse(s)
|
||||
if err != nil {
|
||||
log.Println("parse:", err)
|
||||
errf = true
|
||||
return
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// resources
|
||||
res["bootstrap.css"] = []byte(bootstrapCSS)
|
||||
res["custom.css"] = []byte(customCSS)
|
||||
|
||||
// pages.go
|
||||
templ["notFoundHandler"] = parse(index, menu, notFound)
|
||||
templ["loginHandler"] = parse(index, menu, login)
|
||||
templ["searchHandler"] = parse(index, menu, search)
|
||||
templ["allHandler"] = parse(index, menu, all)
|
||||
templ["wikiNewHandler"] = parse(index, menu, wikiNew)
|
||||
templ["wikiEditHandler"] = parse(index, menu, wikiEdit)
|
||||
templ["wikiHandler"] = parse(index, menu, wiki)
|
||||
templ["wikiMDHandler"] = parse(index, menu, wikiMD)
|
||||
// users.go
|
||||
templ["usersHandler"] = parse(index, menu, users)
|
||||
templ["userNewHandler"] = parse(index, menu, userNew)
|
||||
templ["userEditHandler"] = parse(index, menu, userEdit)
|
||||
templ["userDelHandler"] = parse(index, menu, userDel)
|
||||
|
||||
if errf {
|
||||
log.Fatal("parse: ", errors.New("parsing templates failed"))
|
||||
}
|
||||
}
|
||||
EOF
|
||||
16
templates/all.html
Normal file
16
templates/all.html
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
{{define "body"}}
|
||||
<div class="page-header">
|
||||
<div class="row">
|
||||
<h2>{{.BodyTitle}}</h2>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<ul type="circle">
|
||||
{{if .Data}}
|
||||
{{range $item := .Data}}
|
||||
<li><a href="/wiki/{{$item.LinkTitle}}"><b>{{$item.Title}}</b></a></li>
|
||||
{{end}}
|
||||
{{end}}
|
||||
</ul>
|
||||
</div>
|
||||
{{end}}
|
||||
11
templates/bootstrap.css
vendored
Normal file
11
templates/bootstrap.css
vendored
Normal file
File diff suppressed because one or more lines are too long
|
|
@ -22,12 +22,7 @@
|
|||
float: left !important;
|
||||
}
|
||||
}
|
||||
body {
|
||||
padding-top: 60px;
|
||||
}
|
||||
.msg {
|
||||
margin-top: 30px;
|
||||
}
|
||||
|
||||
a.dark {
|
||||
color: #222222;
|
||||
}
|
||||
|
|
@ -35,16 +30,6 @@ a.para {
|
|||
text-decoration: none;
|
||||
font-size: 18px;
|
||||
}
|
||||
nav {
|
||||
padding-top: 20px;
|
||||
}
|
||||
*[id]:before {
|
||||
display: block;
|
||||
content: " ";
|
||||
margin-top: -75px;
|
||||
height: 75px;
|
||||
visibility: hidden;
|
||||
}
|
||||
.alert-primary {
|
||||
background-color: #375a7f;
|
||||
border-color: #375a7f;
|
||||
|
|
@ -52,6 +37,8 @@ nav {
|
|||
}
|
||||
h1,h2,h3,h4,h5,h6 {
|
||||
font-weight: bold;
|
||||
padding-top: 60px !important;
|
||||
margin-top: -50px !important;
|
||||
-webkit-background-clip: content-box !important;
|
||||
background-clip: content-box !important;
|
||||
}
|
||||
|
|
@ -67,7 +54,10 @@ h3 {
|
|||
h4 {
|
||||
font-size: 1.25em;
|
||||
}
|
||||
h5,h6 {
|
||||
h5 {
|
||||
font-size: 1em;
|
||||
}
|
||||
h6 {
|
||||
font-size: 1em;
|
||||
}
|
||||
.md-text {
|
||||
|
|
@ -87,64 +77,14 @@ pre {
|
|||
border: 1px solid #444444;
|
||||
border-radius: 2px;
|
||||
}
|
||||
pre.wrap {
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
code {
|
||||
padding: 3px 3px 1px 0px;
|
||||
padding: 3px 3px 1px 3px;
|
||||
font-size: 90%;
|
||||
color: #0cdba6;
|
||||
background-color: #444444;
|
||||
border-radius: 2px;
|
||||
}
|
||||
textarea,input,select {
|
||||
textarea,input {
|
||||
color: #e2e2e2 !important;
|
||||
background-color: #444444 !important;
|
||||
}
|
||||
select.sections {
|
||||
height: 120px !important;
|
||||
}
|
||||
input.menu {
|
||||
color: #e2e2e2 !important;
|
||||
background-color: #222222 !important;
|
||||
}
|
||||
.menu-search-bar {
|
||||
padding-top: 4px;
|
||||
}
|
||||
.menu-search-input {
|
||||
background-color: #2f2f2f !important;
|
||||
}
|
||||
html,body,.container,.content {
|
||||
height: 100% !important;
|
||||
}
|
||||
.container,.content {
|
||||
position: relative;
|
||||
}
|
||||
.proper-content {
|
||||
padding-top: 40px;
|
||||
}
|
||||
.section {
|
||||
margin-bottom: 20px;
|
||||
width: 250px;
|
||||
}
|
||||
.wrapper {
|
||||
min-height: 100%;
|
||||
height: auto !important;
|
||||
height: 100%;
|
||||
margin: 0 auto -60px;
|
||||
}
|
||||
.push {
|
||||
height: 60px;
|
||||
}
|
||||
.full-width {
|
||||
width: 100%;
|
||||
}
|
||||
.relative {
|
||||
position: relative;
|
||||
}
|
||||
.btn-breadcrumb {
|
||||
position: absolute;
|
||||
right: 1rem;
|
||||
top: 38%;
|
||||
transform: translateY(-50%);
|
||||
}
|
||||
29
templates/index.html
Normal file
29
templates/index.html
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>GoWiki | {{.Title}}</title>
|
||||
<link rel="stylesheet" type="text/css" href="/bootstrap.css" mdeia="screen" integrity="sha256-KC5lAzGRWwscU0sTmXtd8ka5mt7Vk8a0L5JqOhwA28s="></link>
|
||||
<link rel="stylesheet" type="text/css" href="/custom.css" integrity="sha256-uAIARpL4VhLyhfuh5bS4YWjdcAvN+KjjTli9S8Ud8O4="></link>
|
||||
</head>
|
||||
<body style="padding-top:50px">
|
||||
<div class="navbar navbar-default navbar-fixed-top">
|
||||
<div class="container">
|
||||
<div class="navbar-header">
|
||||
<a href="/" class="navbar-brand">GoWiki</a>
|
||||
</div>
|
||||
<div class="navbar-collapse collapse">
|
||||
{{template "menu" .}}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="container">
|
||||
{{if .Msg}}
|
||||
<div class="alert alert-danger" style="margin-top:20px">
|
||||
<h4>Error!</h4>
|
||||
<p>{{.Msg}}</p>
|
||||
</div>
|
||||
{{end}}
|
||||
{{template "body" .}}
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
37
templates/login.html
Normal file
37
templates/login.html
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
{{define "body"}}
|
||||
<div class="page-header">
|
||||
<div class="row">
|
||||
<div class="col-xs-6 col-xs-offset-3">
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading">
|
||||
<h3 class="panel-title">{{.BodyTitle}}</h3>
|
||||
</div>
|
||||
<div class="panel-body">
|
||||
<form class="form-horizontal" action="/login" method="post">
|
||||
<input type="hidden" name="token" value="{{.Token}}">
|
||||
<fieldset>
|
||||
<div class="form-group">
|
||||
<label class="col-lg-4 control-label">Username</label>
|
||||
<div class="col-lg-6">
|
||||
<input class="form-control input-sm" type="text" name="user" autofocus required>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="col-lg-4 control-label">Password</label>
|
||||
<div class="col-lg-6">
|
||||
<input class="form-control input-sm" type="password" name="password" required>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<div class="col-lg-6 col-lg-offset-4">
|
||||
<button class="btn btn-sm btn-primary" type="submit" name="submit" value="Login">Login</button>
|
||||
</div>
|
||||
</div>
|
||||
</fieldset>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{{end}}
|
||||
49
templates/menu.html
Normal file
49
templates/menu.html
Normal file
|
|
@ -0,0 +1,49 @@
|
|||
{{define "menu"}}
|
||||
{{if .Login}}
|
||||
<ul class="nav navbar-nav">
|
||||
<li><a href="/wiki/Index">
|
||||
Index
|
||||
</a></li>
|
||||
<li><a href="/search">
|
||||
Search
|
||||
</a></li>
|
||||
<li><a href="/all">
|
||||
All
|
||||
</a></li>
|
||||
<li><a href="/new">
|
||||
New
|
||||
</a></li>
|
||||
</ul>
|
||||
<ul class="nav navbar-nav navbar-right">
|
||||
{{if .Admin}}
|
||||
<li><a href="/users">
|
||||
Users
|
||||
</a></li>
|
||||
{{else}}
|
||||
<li><a href="/user/edit/{{.User}}">
|
||||
Profile
|
||||
</a></li>
|
||||
{{end}}
|
||||
<li><a href="/logout">
|
||||
Logout
|
||||
</a></li>
|
||||
</ul>
|
||||
{{else}}
|
||||
<ul class="nav navbar-nav">
|
||||
<li><a href="/wiki/Index">
|
||||
Index
|
||||
</a></li>
|
||||
<li><a href="/search">
|
||||
Search
|
||||
</a></li>
|
||||
<li><a href="/all">
|
||||
All
|
||||
</a></li>
|
||||
</ul>
|
||||
<ul class="nav navbar-nav navbar-right">
|
||||
<li><a href="/login">
|
||||
Login
|
||||
</a></li>
|
||||
</ul>
|
||||
{{end}}
|
||||
{{end}}
|
||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue