Compare commits
22 Commits
Author | SHA1 | Date |
---|---|---|
Aaron Bieber | e3b3dad3ee | |
Aaron Bieber | 14bdf17e49 | |
Aaron Bieber | 0ea90c5ddd | |
Aaron Bieber | a86c07b5e8 | |
Aaron Bieber | d11f802f05 | |
Aaron Bieber | e0ee714d3e | |
Aaron Bieber | db7f06dc5b | |
Aaron Bieber | 68c586f805 | |
Aaron Bieber | 8c113d95c1 | |
Aaron Bieber | 1112c9874e | |
Aaron Bieber | 18a4ed5026 | |
Aaron Bieber | f9de42e801 | |
Aaron Bieber | 4fdac0116f | |
Aaron Bieber | 0205c58868 | |
Ben Tsai | 4e224c9c08 | |
Ysbrand van Eijck | fc9da8e1f3 | |
Ysbrand van Eijck | 3ddc711cc1 | |
Aaron Bieber | 6e2f8a7495 | |
Aaron Bieber | 2551d2d9a0 | |
Aaron Bieber | 3fb82cec02 | |
Aaron Bieber | 574110c52f | |
Aaron Bieber | 3849f2ba51 |
|
@ -1,2 +1,3 @@
|
|||
widdler
|
||||
.htpasswd
|
||||
dist/
|
||||
|
|
|
@ -0,0 +1,55 @@
|
|||
before:
|
||||
hooks:
|
||||
- go mod tidy
|
||||
- go vet
|
||||
- staticcheck
|
||||
- gosec .
|
||||
builds:
|
||||
- env:
|
||||
- CGO_ENABLED=0
|
||||
targets:
|
||||
- openbsd_amd64
|
||||
- openbsd_arm64
|
||||
- openbsd_386
|
||||
- openbsd_arm
|
||||
- freebsd_amd64
|
||||
- freebsd_arm64
|
||||
- freebsd_386
|
||||
- darwin_amd64
|
||||
- darwin_arm64
|
||||
- windows_amd64
|
||||
- netbsd_amd64
|
||||
- netbsd_arm64
|
||||
- netbsd_386
|
||||
- dragonfly_amd64
|
||||
goos:
|
||||
- openbsd
|
||||
- freebsd
|
||||
- netbsd
|
||||
- dragonfly
|
||||
- linux
|
||||
- windows
|
||||
- darwin
|
||||
ldflags:
|
||||
- -s -w -X main.build={{.Version}}
|
||||
archives:
|
||||
- replacements:
|
||||
openbsd: OpenBSD
|
||||
freebsd: FreeBSD
|
||||
netbsd: NetBSD
|
||||
dragonfly: DragonflyBSD
|
||||
darwin: Darwin
|
||||
linux: Linux
|
||||
windows: Windows
|
||||
386: i386
|
||||
amd64: x86_64
|
||||
checksum:
|
||||
name_template: 'checksums.txt'
|
||||
snapshot:
|
||||
name_template: "{{ incpatch .Version }}-next"
|
||||
changelog:
|
||||
sort: asc
|
||||
filters:
|
||||
exclude:
|
||||
- '^docs:'
|
||||
- '^test:'
|
21
README.md
21
README.md
|
@ -4,7 +4,7 @@ widdler
|
|||
widdler is a single binary that serves up
|
||||
[TiddlyWiki](https://tiddlywiki.com)s.
|
||||
|
||||
It can be used to server existing wikis, or to create new ones.
|
||||
It can be used to serve existing wikis, or to create new ones.
|
||||
|
||||
# Features
|
||||
|
||||
|
@ -18,10 +18,16 @@ It can be used to server existing wikis, or to create new ones.
|
|||
|
||||
# Installation
|
||||
|
||||
For Go 1.16:
|
||||
```
|
||||
go get -u suah.dev/widdler
|
||||
```
|
||||
|
||||
For Go 1.17 and up:
|
||||
```
|
||||
go install suah.dev/widdler@latest
|
||||
```
|
||||
|
||||
# Running
|
||||
|
||||
```
|
||||
|
@ -46,3 +52,16 @@ create the wiki file based off the current `empty.html` TiddlyWiki version.
|
|||
|
||||
Simply hit the save button!
|
||||
|
||||
# Updating widdler
|
||||
|
||||
```
|
||||
go install suah.dev/widdler@latest
|
||||
```
|
||||
|
||||
# Running without .htpasswd
|
||||
|
||||
You can disable auth all together by setting the `-auth` flag to false:
|
||||
|
||||
```
|
||||
widdler -auth=false -wikis ~/wiki
|
||||
```
|
||||
|
|
12900
empty-5.1.23.html
12900
empty-5.1.23.html
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
8
go.mod
8
go.mod
|
@ -3,7 +3,9 @@ module suah.dev/widdler
|
|||
go 1.16
|
||||
|
||||
require (
|
||||
golang.org/x/crypto v0.0.0-20210513164829-c07d793c2f9a
|
||||
golang.org/x/net v0.0.0-20210510120150-4163338589ed
|
||||
suah.dev/protect v1.0.0
|
||||
golang.org/x/crypto v0.0.0-20211202192323-5770296d904e
|
||||
golang.org/x/net v0.0.0-20211205041911-012df41ee64c
|
||||
golang.org/x/sys v0.0.0-20211205182925-97ca703d548d // indirect
|
||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211
|
||||
suah.dev/protect v1.2.0
|
||||
)
|
||||
|
|
24
go.sum
24
go.sum
|
@ -1,16 +1,18 @@
|
|||
golang.org/x/crypto v0.0.0-20210513164829-c07d793c2f9a h1:kr2P4QFmQr29mSLA43kwrOcgcReGTfbE9N577tCTuBc=
|
||||
golang.org/x/crypto v0.0.0-20210513164829-c07d793c2f9a/go.mod h1:P+XmwS30IXTQdn5tA2iutPOUgjI07+tq3H3K9MVA1s8=
|
||||
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||
golang.org/x/net v0.0.0-20210510120150-4163338589ed h1:p9UgmWI9wKpfYmgaV/IZKGdXc5qEK45tDwwwDyjS26I=
|
||||
golang.org/x/net v0.0.0-20210510120150-4163338589ed/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||
golang.org/x/sys v0.0.0-20200501145240-bc7a7d42d5c3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/crypto v0.0.0-20211202192323-5770296d904e h1:MUP6MR3rJ7Gk9LEia0LP2ytiH6MuCfs7qYz+47jGdD8=
|
||||
golang.org/x/crypto v0.0.0-20211202192323-5770296d904e/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
|
||||
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||
golang.org/x/net v0.0.0-20211205041911-012df41ee64c h1:7SfqwP5fxEtl/P02w5IhKc86ziJ+A25yFrkVgoy2FT8=
|
||||
golang.org/x/net v0.0.0-20211205041911-012df41ee64c/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210423082822-04245dca01da h1:b3NXsE2LusjYGGjL5bxEVZZORm/YEFFrWFjR8eFrw/c=
|
||||
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1 h1:v+OssWQX+hTHEmOBgwxdZxK4zHq3yOs8F9J7mk0PY8E=
|
||||
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20210917161153-d61c044b1678/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20211205182925-97ca703d548d h1:FjkYO/PPp4Wi0EAUOVLxePm7qVW4r4ctbWpURyuOD0E=
|
||||
golang.org/x/sys v0.0.0-20211205182925-97ca703d548d/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211 h1:JGgROgKl9N8DuW20oFS5gxc+lE67/N3FcwmBPMe7ArY=
|
||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
suah.dev/protect v1.0.0 h1:X8pzDvDIZIiugmkmr6DES6JFO1XUdJWi34Ffmk6CMZY=
|
||||
suah.dev/protect v1.0.0/go.mod h1:ZSgyBM30JUwhVPWJzVHh0jlu5W6Qz1VR6tIhAzqJZ9Y=
|
||||
suah.dev/protect v1.2.0 h1:4G4V43yVYXCjLFzaE9QJR0fLo3rf5vNBS9YxyoI19DU=
|
||||
suah.dev/protect v1.2.0/go.mod h1:Ocn1yqUskqe/is6N2bxQxtT+fegbvQsOFyHbJAQu9XE=
|
||||
|
|
124
main.go
124
main.go
|
@ -15,12 +15,13 @@ import (
|
|||
"path/filepath"
|
||||
"regexp"
|
||||
"strings"
|
||||
"sync"
|
||||
"text/template"
|
||||
"time"
|
||||
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
"golang.org/x/crypto/ssh/terminal"
|
||||
"golang.org/x/net/webdav"
|
||||
"golang.org/x/term"
|
||||
"suah.dev/protect"
|
||||
)
|
||||
|
||||
|
@ -45,34 +46,53 @@ const landingPage = `
|
|||
`
|
||||
|
||||
var (
|
||||
twFile = "empty-5.1.23.html"
|
||||
twFile = "empty-5.2.1.html"
|
||||
|
||||
//go:embed empty-5.1.23.html
|
||||
//go:embed empty-5.2.1.html
|
||||
tiddly embed.FS
|
||||
templ *template.Template
|
||||
)
|
||||
|
||||
type userHandler struct {
|
||||
mu sync.Mutex
|
||||
dav *webdav.Handler
|
||||
fs http.Handler
|
||||
name string
|
||||
}
|
||||
|
||||
type userHandlers struct {
|
||||
dav *webdav.Handler
|
||||
fs http.Handler
|
||||
list []userHandler
|
||||
mu sync.RWMutex
|
||||
}
|
||||
|
||||
func (u *userHandlers) find(name string) *userHandler {
|
||||
for i := range u.list {
|
||||
if u.list[i].name == name {
|
||||
return &u.list[i]
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
var (
|
||||
auth bool
|
||||
auth string
|
||||
davDir string
|
||||
fullListen string
|
||||
genHtpass bool
|
||||
handlers map[string]userHandlers
|
||||
handlers userHandlers
|
||||
listen string
|
||||
passPath string
|
||||
tlsCert string
|
||||
tlsKey string
|
||||
users map[string]string
|
||||
version bool
|
||||
build string
|
||||
)
|
||||
|
||||
var pledges = "stdio wpath rpath cpath tty inet dns unveil"
|
||||
|
||||
func init() {
|
||||
users = make(map[string]string)
|
||||
handlers = make(map[string]userHandlers)
|
||||
dir, err := filepath.Abs(filepath.Dir(os.Args[0]))
|
||||
if err != nil {
|
||||
log.Fatalln(err)
|
||||
|
@ -83,8 +103,9 @@ func init() {
|
|||
flag.StringVar(&tlsCert, "tlscert", "", "TLS certificate.")
|
||||
flag.StringVar(&tlsKey, "tlskey", "", "TLS key.")
|
||||
flag.StringVar(&passPath, "htpass", fmt.Sprintf("%s/.htpasswd", dir), "Path to .htpasswd file..")
|
||||
flag.BoolVar(&auth, "auth", true, "Enable HTTP Basic Authentication.")
|
||||
flag.StringVar(&auth, "auth", "basic", "Enable HTTP Basic Authentication.")
|
||||
flag.BoolVar(&genHtpass, "gen", false, "Generate a .htpasswd file or add a new entry to an existing file.")
|
||||
flag.BoolVar(&version, "v", false, "Show version and exit.")
|
||||
flag.Parse()
|
||||
|
||||
// These are OpenBSD specific protections used to prevent unnecessary file access.
|
||||
|
@ -92,13 +113,12 @@ func init() {
|
|||
_ = protect.Unveil(davDir, "rwc")
|
||||
_ = protect.Unveil("/etc/ssl/cert.pem", "r")
|
||||
_ = protect.Unveil("/etc/resolv.conf", "r")
|
||||
_ = protect.Pledge("stdio wpath rpath cpath tty inet dns unveil")
|
||||
_ = protect.Pledge(pledges)
|
||||
|
||||
templ, err = template.New("landing").Parse(landingPage)
|
||||
if err != nil {
|
||||
log.Fatalln(err)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func authenticate(user string, pass string) bool {
|
||||
|
@ -132,7 +152,7 @@ func createEmpty(path string) error {
|
|||
if os.IsNotExist(fErr) {
|
||||
log.Printf("creating %q\n", path)
|
||||
twData, _ := tiddly.ReadFile(twFile)
|
||||
wErr := ioutil.WriteFile(path, []byte(twData), 0600)
|
||||
wErr := ioutil.WriteFile(path, twData, 0600)
|
||||
if wErr != nil {
|
||||
return wErr
|
||||
}
|
||||
|
@ -145,20 +165,25 @@ func prompt(prompt string, secure bool) (string, error) {
|
|||
fmt.Print(prompt)
|
||||
|
||||
if secure {
|
||||
b, err := terminal.ReadPassword(int(os.Stdin.Fd()))
|
||||
b, err := term.ReadPassword(int(os.Stdin.Fd()))
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
input = string(b)
|
||||
} else {
|
||||
fmt.Scanln(&input)
|
||||
_, err := fmt.Scanln(&input)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
}
|
||||
return input, nil
|
||||
}
|
||||
|
||||
func main() {
|
||||
_ = protect.Pledge("stdio wpath rpath cpath inet dns unveil")
|
||||
|
||||
if version {
|
||||
fmt.Println(build)
|
||||
os.Exit(0)
|
||||
}
|
||||
if genHtpass {
|
||||
user, err := prompt("Username: ", false)
|
||||
if err != nil {
|
||||
|
@ -175,38 +200,41 @@ func main() {
|
|||
log.Fatalln(err)
|
||||
}
|
||||
|
||||
f, err := os.OpenFile(passPath, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0600)
|
||||
f, err := os.OpenFile(filepath.Clean(passPath), os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0600)
|
||||
if err != nil {
|
||||
log.Fatalln(err)
|
||||
}
|
||||
|
||||
defer f.Close()
|
||||
|
||||
if _, err := f.WriteString(fmt.Sprintf("%s:%s\n", user, hash)); err != nil {
|
||||
log.Fatalln(err)
|
||||
}
|
||||
|
||||
err = f.Close()
|
||||
if err != nil {
|
||||
log.Fatalln(err)
|
||||
}
|
||||
|
||||
fmt.Printf("Added %q to %q\n", user, passPath)
|
||||
|
||||
os.Exit(0)
|
||||
}
|
||||
pledges, _ = protect.ReducePledges(pledges, "tty")
|
||||
|
||||
// drop to only read on passPath
|
||||
_ = protect.Unveil(passPath, "r")
|
||||
_ = protect.Pledge("stdio wpath rpath cpath inet dns")
|
||||
pledges, _ = protect.ReducePledges(pledges, "unveil")
|
||||
|
||||
_, fErr := os.Stat(passPath)
|
||||
if os.IsNotExist(fErr) {
|
||||
if auth {
|
||||
if auth == "basic" || auth == "header" {
|
||||
fmt.Println("No .htpasswd file found!")
|
||||
os.Exit(1)
|
||||
}
|
||||
} else {
|
||||
p, err := os.Open(passPath)
|
||||
p, err := os.Open(filepath.Clean(passPath))
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
defer p.Close()
|
||||
|
||||
ht := csv.NewReader(p)
|
||||
ht.Comma = ':'
|
||||
|
@ -218,30 +246,37 @@ func main() {
|
|||
log.Fatal(err)
|
||||
}
|
||||
|
||||
err = p.Close()
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
for _, parts := range entries {
|
||||
users[parts[0]] = parts[1]
|
||||
}
|
||||
}
|
||||
|
||||
if auth {
|
||||
if auth == "basic" || auth == "header" {
|
||||
for u := range users {
|
||||
uPath := path.Join(davDir, u)
|
||||
handlers[u] = userHandlers{
|
||||
handlers.list = append(handlers.list, userHandler{
|
||||
name: u,
|
||||
dav: &webdav.Handler{
|
||||
LockSystem: webdav.NewMemLS(),
|
||||
FileSystem: webdav.Dir(uPath),
|
||||
},
|
||||
fs: http.FileServer(http.Dir(uPath)),
|
||||
}
|
||||
})
|
||||
}
|
||||
} else {
|
||||
handlers[""] = userHandlers{
|
||||
handlers.list = append(handlers.list, userHandler{
|
||||
name: "",
|
||||
dav: &webdav.Handler{
|
||||
LockSystem: webdav.NewMemLS(),
|
||||
FileSystem: webdav.Dir(davDir),
|
||||
},
|
||||
fs: http.FileServer(http.Dir(davDir)),
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
mux := http.NewServeMux()
|
||||
|
@ -260,8 +295,24 @@ func main() {
|
|||
return
|
||||
}
|
||||
|
||||
if auth {
|
||||
if auth == "basic" {
|
||||
user, pass, ok = r.BasicAuth()
|
||||
if !(ok && authenticate(user, pass)) {
|
||||
w.Header().Set("WWW-Authenticate", `Basic realm="widdler"`)
|
||||
http.Error(w, "Unauthorized", http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
} else if auth == "header" {
|
||||
var prefix = "Auth"
|
||||
for name, values := range r.Header {
|
||||
if strings.HasPrefix(name, prefix) {
|
||||
user = strings.TrimLeft(name, prefix)
|
||||
pass = values[0]
|
||||
ok = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if !(ok && authenticate(user, pass)) {
|
||||
w.Header().Set("WWW-Authenticate", `Basic realm="widdler"`)
|
||||
http.Error(w, "Unauthorized", http.StatusUnauthorized)
|
||||
|
@ -269,7 +320,19 @@ func main() {
|
|||
}
|
||||
}
|
||||
|
||||
handler := handlers[user]
|
||||
handlers.mu.RLock()
|
||||
handler := handlers.find(user)
|
||||
handlers.mu.RUnlock()
|
||||
|
||||
if handler == nil {
|
||||
http.NotFound(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
handler.mu.Lock()
|
||||
|
||||
defer handler.mu.Unlock()
|
||||
|
||||
userPath := path.Join(davDir, user)
|
||||
fullPath := path.Join(davDir, user, r.URL.Path)
|
||||
|
||||
|
@ -350,5 +413,4 @@ func main() {
|
|||
log.Printf("Listening for HTTP on 'http://%s'", listen)
|
||||
log.Fatalln(s.Serve(lis))
|
||||
}
|
||||
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue